Dokcer高级

  • Docker底层技术
  • 优化Docker镜像
  • docker build
  • docker build的Cache机制
  • 问题点
  • Cache机制注意事项
  • 传统build流程
  • dockerfile中的multi-stage
  • 容器中的应用优雅的退出
  • 进程的退出
  • 信号
  • 容器中的信号


Docker底层技术

  • Linux Container
  • Cgroup
  • NameSpace
  • Chroot
  • Overlay FS
  • Iptables
  • Bridge

优化Docker镜像

docker build

中所周知,一个Dockerfile唯一定义了一个Docker镜像。如此依赖,Docker必须提供一种方式,将Dockerfile转换为Docker镜像,采用的方式就是docker build。

FROM ubuntu:14.04
RUN apt-get update
ADD ./run.sh /
VOLUME /data
CMD ["./run.sh"]

docker容器运行必须有一个前台进程,一般此Dockerfile的当前目录下,必须包含文件run.sh。通过执行命令

docker build -t my_new_image .

可以将当前目录下的Dockerfile构建成一个名为my_new_image的镜像,镜像的默认tag为latest。对于以上docker build请求,Docker Daemon新创建了4层镜像,除了FROM命令,其余的RUN、ADD、VOLUME以及CMD都会创建一层镜像。

docker build的Cache机制

Docker daemon通过dockerfile构建镜像时,当发现即将新构建出的镜像与以后的某镜像重复时,可以选则放弃构建新建的镜像。而是选择用已有的镜像作为构建结构,也就是采取本地已经cache的镜像作为结果

问题点

  • “即将构建出的镜像”属于仍未构建完成的镜像,通过何种方式来标识此镜像?
  • 涉及到镜像比较,重复时选择放弃构建,那镜像比较时重复的标准是什么?

Cache机制注意事项

cache机制很大程度上做到了镜像的复用,降低存储空间的同是,还大大缩短了构建时间。但是要想用好cache机制,就必须了解利用cache的一些注意事项

  • ADD命令和COPY命令:Dockerfile没有发生任何改变,但是命令ADD run.sh /中Dockerfile当前目录下的run.sh却发生了改变,从而将直接导致镜像层文件系统内容的更新,原则上不应该再使用cache。那么判断ADD命令或者COPY命令后紧接的文件是否发生变化,则成为是否延用cache的重要依据。Docker采取的策略是:获取Dockerfile下内容(包括文件的部分inode信息),计算出一个唯一的hash值,若hash值未发生变化,则认为文件内容没有发生变化,可以使用cache机制。
  • RUN命令存在外部依赖:一旦RUN命令存在外部依赖,如RUN apt-get update,那么随着时间的推移,基于同一个基础镜像,一年的apt-get update和一年后的apt-get update,由于软件源软件的更新,从而导致产生的镜像理论上应该不同。如果继续使用cache机制,将存在不满足用户需求的情况。Docker一开始的设计既考虑了外部依赖的问题,用户可以使用参数–no-cache确保最新的外部依赖,命令docker build --no-cache -t my_new_image
  • 树状的镜像关系决定了,一次新镜像的成功构建将导致后续的cache机制全部失效:一旦产生一个新的镜像,同时意味着产生一个新的镜像ID,而当前宿主机环境中肯定不会存在一个镜像

传统build流程

创建编译文件的环境的dockerfile

FROM golang:.1.7.3
WORKDIR /go/src/github.com/sparkdevo/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

创建运行环境的dockerrfile

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY app .
CMD ["./app"]

使用脚本创建

#!/bin/bash
echo building sparkdevo/href-counter:build
#构建编译应用程序的镜像
docker build --na-cache -t sparkdevo/href-counter:build . -f dockerfile.build
#创建应用程序
docker create --name extract sparkdevo/href-counter:build
#拷贝编译好的应用程序
docker cp extract:/go/src/github.com/sparkdevo/href-counter/app ./app
docker rm -f extract

echo building sparkdevo/href-counter:latest
#构建运行应用程序的镜像
docker build --na-cache -t sparkdevo/href-counter:latest . -f dockerfile.latest

dockerfile中的multi-stage

FROM golang:1.7.3
WORKDIR /go/src/github.com/sparkdevo/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go    .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/sparkdevo/href-counter/app .
CMD ["./app"]

或者用as玩一下

FROM golang:1.7.3 as builder
WORKDIR /go/src/github.com/sparkdevo/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go    .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/sparkdevo/href-counter/app .
CMD ["./app"]

Docker build的cache机制实现了镜像的复用,不仅节省了镜像的存储空间,也为镜像构建节省了大量的时间。同时,如何命中cahce镜像,也是衡量Dockerfile书写时候合理的重要标准

1如何把镜像变小
  • 选择overlay2 文件系统
  • 尽可能内核4.10版本以上,保证容的稳定运行
  • 资源限制,防止进程OOME
  • 多层级build让镜像更小,更易于服务器间的传输

容器中的应用优雅的退出

进程的退出

kill的参数

1

SIGHUP

启动被终止的程序,可以让该进程重新读取自己的配置文件,类似重新启动

2

SIGNT

相当于用键盘输入ctrl+c来中断一个程序的进行

9

SIGKILL

代表强制中断一个程序的进行,如果该进程进行到一半,那么尚未完成的部分可能会有“半产品”产生,类似vim会有.filename.swp保留下来

15

SIGTERM

以正常的方式来终止该程序。由于是正常的终止,所以后续的动作会将他完成。不过,如果该进程已经发生问题,就是无法使用正常的方法终止时,输入这个signal也是没有用的。

19

SIGSTOP

相当于键盘输入ctrl+z来暂停一个程序的进行

信号

信号是一种进程之间的通信形式。一个信号就是内核发送个进程的一个信号,告诉进程发生了某种事件。当一个信号被发送给一个进程后,进程会立即中断当前的执行流并开始执行信号的处理程序。如果没有为这个信号执行处理程序。就执行默认的处理程序。
进程需要为自己感兴趣的信号注册处理程序,比如为了能让程序优雅的退出(接到退出的请求后能够对资源进行清理)一般程序都会处理,SIGKILL信号会粗暴的结束一个进程。因此我们的应用应该实现这样的目录:捕捉并处理SIGTERM信号,从而优雅的退出程序。如果失败了,用户只能通过SIGKILL信号这一手段了。除了SIGTERM和SIGKILL,还有像SIGUSR1这样的专门自定义行为的信号

容器中的信号

  • 容器中的进程属于容器中的1号进程
    Docker的stop和kill命令都是用来向容器发送信号的。注意,只有容器的1号进程能够收到信号,这点非常关键!stop命令会首先发送SIGTERM信号,并等待应用优雅的结束。如果发现应用没有结束(用户可以指定等待的时间),就在发送一个SIGKILL信号强行结束程序。kill命令默认发送的是SIGKILL信号,当然你可以通过-s 选项指定任何信号
    1号进程是什么??
    docker的1号进程是 dockerfile中 CMD 或者ENTRYPOINT Q命令指定的命令

docker stop命令为什么有的快有的慢??
首先向进程发送SIGTERM信号,如果进程无响应,则会再次发送SIGTERM信号,如果进程还是不响应,则会发送SIGKILL信号,强行杀死进程。当然也有可能进程在停止前,需要做一些停止前的工作;还有一中情况就是进程识别不了我们发送的信号,这里就需要我们使用脚本来实现了。

#!/bin/bash
# 打开调试级别
set -x

# 指定当前 pid 号
pid=0

# SIGUSR1-handler
my_handler() {
  echo "my_handler"
}


# SIGTERM-handler
term_handler() {
  if [ $pid -ne 0 ]; then
    kill -SIGTERM "$pid"
    # wait是用来阻塞当前进程的执行,直至指定的子进程执行结束后,才继续执行
    wait "$pid"
  fi
  exit 143; # 128 + 15 -- SIGTERM
}
# setup handlers
# on callback, kill the last background process, which is `tail -f /dev/null` and execute the specified handler

#  trap 'commands' signal-list 当脚本收到 signal-list 清单内列出的信号时, trap 命令执行双引号中的命令
trap 'kill ${!}; my_handler' SIGUSR1
trap 'kill ${!}; term_handler' SIGTERM

# run application
node app &
pid="$!"

# wait forever
while true
do
  tail -f /dev/null & wait ${!}
done

创建 Dockerfile2 文件

FROM iojs:onbuild
COPY ./app.js ./app.js
COPY ./app2.sh ./app2.sh
COPY ./package.json ./package.json
RUN chmod +x ./app2.sh
EXPOSE 3000
ENTRYPOINT ["./app2.sh"]
$ docker build --no-cache -t signal-app2 -f Dockerfile2 .
$ docker run -it --rm -p 3000:3000 --name="my-app2" signal-app2