下面是兩個 Dockerfile 文件,我們來看看他們之間的區(qū)別是什么私痹?會對運行中的容器產(chǎn)生什么樣的影響侄榴?
第一個癞蚕,執(zhí)行入口以 exec 形式啟動一個 Spring Boot 應(yīng)用程序桦山。
FROM frolvlad/alpine-java:jdk8-slim
RUN set -eux && mkdir -p /home/
RUN set -eux && mkdir -p /home/auth-server
RUN set -eux && mkdir -p /opt/logs/auth-server
RUN set -eux && touch /opt/logs/auth-server/auth-server.log
ADD auth-server.jar /home/auth-server/auth-server.jar
ENV TZ=Asia/Shanghai
ENV JAVA_ENV="-Denv=docker"
ENV JAVA_OPTS="-server -Xmx256m -Xms256m -XX:+UseG1GC"
ENTRYPOINT [ "sh", "-c", "java $JAVA_ENV $JAVA_OPTS -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom -jar /home/auth-server/auth-server.jar" ]
第二個会放,執(zhí)行入口以 exec 形式執(zhí)行一個 shell 腳本咧最,因為容器中可能還需要運行一些日志收集矢沿、鏈路監(jiān)控的程序捣鲸。所以容器運行時通過執(zhí)行一個 shell 腳本來一起啟動這些程序栽惶。
FROM frolvlad/alpine-java:jdk8-slim
RUN set -eux && mkdir -p /home/
RUN set -eux && mkdir -p /home/auth-server
RUN set -eux && mkdir -p /opt/logs/auth-server
RUN set -eux && touch /opt/logs/auth-server/auth-server.log
ADD auth-server.jar /home/auth-server/auth-server.jar
COPY entrypoint.sh /home/auth-server/entrypoint.sh
RUN chmod +x /home/auth-server/entrypoint.sh
ENTRYPOINT ["/home/auth-server/entrypoint.sh"]
entrypoint.sh
#!/bin/sh
ENV="-Denv=docker"
export JAVA_OPTS="-server -Xmx256m -Xms256m -XX:+UseG1GC"
export JAVA_OPTS="$JAVA_OPTS -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom"
java ${ENV} $JAVA_OPTS -jar /home/auth-server/auth-server.jar
echo "java ${ENV} $JAVA_OPTS -jar /home/auth-server/auth-server.jar"
echo "start success"
區(qū)別就在于 Dockerfile 中的執(zhí)行入口 ENTRYPOINT 的參數(shù)不同代承,通過這兩個 Dockerfile 制作的鏡像次泽,在容器運行時又有什么區(qū)別呢?下面是兩個鏡像玖像,啟動容器后里面的進程信息。
- ENTRYPOINT 執(zhí)行 java -jar 啟動應(yīng)用程序
/ # ps aux
PID USER TIME COMMAND
1 root 0:10 java -Denv=docker -server -Xmx512m -Xms512m -XX:+UseG1GC -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom -jar /home/auth-server/auth-server.jar
25 root 0:00 /bin/sh
34 root 0:00 ps aux
- ENTRYPOINT 執(zhí)行 shell 腳本啟動應(yīng)用程序
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 /bin/sh /home/auth-server/entrypoint.sh
6 root 0:15 java -Denv=docker -server -Xmx512m -Xms512m -XX:+UseG1GC -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom -jar /home/auth-server/auth-server.jar
76 root 0:00 /bin/sh
81 root 0:00 ps aux
區(qū)別就在于誰是容器里的 1 號進程祖驱。 容器里的 1 號進程和非 1 號進程又有什么區(qū)別握恳?
如果使用過 Kubernetes,應(yīng)該知道 Kubernetes 并沒有提供重啟 Pod 的命令捺僻,只能通過 kubectl apply 來重建 Pod乡洼,而一般研發(fā)的操作發(fā)布入口崇裁,都是通過 Jenkins 工具自動構(gòu)建一個新的鏡像到 Harbor,然后再自動發(fā)布到 Kubernetes 平臺來重建應(yīng)用程序束昵。
想重啟一下應(yīng)用程序拔稳,而且使用的是單進程模式,應(yīng)用進程就是容器里的 1 號進程锹雏,在不讓運維介入的情況下礁遵,那你可能必須走上面的 Jenkins 發(fā)布流程晰赞。
如果你們的 Dockerfile 鏡像模板使用的是上述第二種方式,也就是說應(yīng)用進程并不是容器中的 1 號進程褐墅,則還有另外一種方式逝钥,就是通過在 Dashboard 界面進入到 Pod 里蜘欲,手動 kill 掉應(yīng)用進程,這時配合 Kubernetes 的存活探針 livenessProbe 可以達到重啟 Pod 的效果,而且這個 Pod 的 IP 不會變化箱硕。
livenessProbe:
tcpSocket:
port: 9096
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
successThreshold: 1
timeoutSeconds: 10
Liveness 指針是存活探針惠昔,它用來判斷容器是否存活、判斷 pod 是否 running啦扬。如果 Liveness 指針判斷容器不健康勤晚,此時會通過 kubelet 殺掉相應(yīng)的 pod揉忘,并根據(jù)重啟策略來判斷是否重啟這個容器狂丝。如果默認不配置 Liveness 指針,則默認情況下認為它這個探測默認返回是成功的哈蝇。
單進程模式下性芬,應(yīng)用進程就是容器中的 1 號進程鸡挠,不能通過 kill 1 來實現(xiàn)嗎姓惑?你可以嘗試一下,不管是通過 kill -9 還是 kill -15踊兜,這個 1 號進程都是殺不死的筷频。
生產(chǎn)環(huán)境建議容器都是單進程,應(yīng)用進程既是容器的主進程(1號進程)示罗。
上述兩種容器運行的方式禽额,對于 Kubernetes 平臺中 Pod 的滾動更新,或者僅僅用 Docker 時馍佑,容器的 Restart 會對服務(wù)產(chǎn)生什么樣的影響雏亚?
先來介紹下在 linux 中兩個終止進程的命令:kill -9 pid 和 kill -15 pid遣铝,代表兩種信號 SIGKILL 和 SIGTERM。
[root@ ~]# kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
......略
SIGTERM 是軟終止莉擒,SIGKILL 用于立即終止進程酿炸,他們都可以用于終止程序,但是是有區(qū)別的:
- SIGTERM 優(yōu)雅的終止進程涨冀,而 SIGKILL 會立即終止進程填硕。
- SIGTERM 信號可以處理、忽略和阻止,而 SIGKILL 不能被處理或阻止扁眯。
- SIGTERM 不會殺死子進程壮莹。SIGKILL 會殺死子進程。
我們以一個 Spring Cloud 服務(wù)舉例姻檀,服務(wù)啟動后注冊到 Eureka Server命满,當服務(wù)停止時通知 Eureka Server 下線注銷實例(這個有一個前提,服務(wù)必須是優(yōu)雅停止 graceful shutdown)绣版,服務(wù)下線通知是怎么實現(xiàn)的胶台。如下:
@Singleton
public class DiscoveryClient implements EurekaClient {
/**
* Shuts down Eureka Client. Also sends a deregistration request to the
* eureka server.
*/
@PreDestroy
@Override
public synchronized void shutdown() {
if (isShutdown.compareAndSet(false, true)) {
logger.info("Shutting down DiscoveryClient ...");
......
logger.info("Completed shut down of DiscoveryClient");
}
}
}
就是通過 @PreDestroy 注解,在 Bean 實例銷毀之前做一些操作杂抽,對于 DiscoverClient 來說就是在 shutdown 時發(fā)送一個 HTTP 請求Sending request: DELETE /eureka/apps/API-GATEWAY/{instance-id} HTTP/1.1
主動通知一下 Eureka Server 自己要下線了诈唬。接下來經(jīng)過多次同步之后,其它客戶端感知到服務(wù)下線缩麸。
如果通過 kill -9 來強制殺死應(yīng)用讯榕,Spring Boot 應(yīng)用就來不及做這些善后工作,直接被終止了匙睹。
Docker 提供了有兩種方式來停止容器:docker stop 和 docker kill
-
docker stop:容器內(nèi)的主進程(PID為1的進程)將收到 SIGTERM 信號愚屁,如果在寬限時間后(默認 10s)進程還沒有退出,將發(fā)送 SIGKILL 信號痕檬。使用 docker stop 時霎槐,docker 守護進程在發(fā)送 SIGKILL 信號之前等待的秒數(shù)是可以控制的,參數(shù)如下:
Name, shorthand Default Description --time , -t 10 Seconds to wait for stop before killing it -
docker kill:默認向容器內(nèi)的主進程(1號進程)發(fā)送 SIGKILL 信號梦谜,或者用 --signal 選項指定的信號丘跌。也就是說默認情況下,docker kill 命令不會給容器進程一個優(yōu)雅地退出的機會唁桩,它只是發(fā)出一個 SIGKILL 信號來終止容器闭树。但是,它有一個 --signal 入?yún)⒒脑瑁梢韵蛉萜鬟M程發(fā)送 SIGKILL 以外的信號报辱。
Name, shorthand Default Description --signal , -s KILL Signal to send to the container
對于 Kubernetes 平臺來說,Pod 銷毀的寬限時間默認是 30s单山。通過 terminationGracePeriodSeconds: 30 參數(shù)設(shè)置 碍现。如果容器在寬限期后仍在運行,SIGKILL 將強制移除 Pod米奸,終止操作完成昼接。
下面通過命令看下上面兩種 Dockerfile 文件構(gòu)建的鏡像,在容器停止時悴晰,容器內(nèi)進程接收到的信號有什么區(qū)別慢睡。
使用 docker top auth-server 命令可以查看容器內(nèi)進程在宿主機上的 PID 號负拟。
第一種方式啟動的容器:應(yīng)用進程在宿主機上的 PID = 6184
[root@ dockerfile]# docker top auth-server
UID PID PPID C STIME TTY TIME CMD
root 6184 6168 22 00:26 ? 00:00:15 java -Denv=docker -server -Xmx512m -Xms512m -XX:+UseG1GC -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom -jar /home/auth-server/auth-server.jar
第二種腳本方式啟動的容器:應(yīng)用進程在宿主機上的 PID = 6716
[root@ dockerfile]# docker top auth-server
UID PID PPID C STIME TTY TIME CMD
root 6698 6682 0 00:30 ? 00:00:00 /bin/sh /home/eureka-server/entrypoint.sh
root 6716 6698 95 00:30 ? 00:00:11 java -Denv=docker -server -Xmx512m -Xms512m -XX:+UseG1GC -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom -jar /home/auth-server/auth-server.jar
docker stop auth-server 在停止容器的同時出吹,使用 strace -p PID 來觀察容器內(nèi)進程接收到的信號情況多搀。
- strace -p 6184
(ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Dfile......")
[root@ eureka]# strace -p 6184
strace: Process 6184 attached
futex(0x7fbd15b2a9d0, FUTEX_WAIT, 6, NULL) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=0, si_uid=0} ---
futex(0x7fbd14ef1580, FUTEX_WAKE_PRIVATE, 1) = 1
rt_sigreturn({mask=[]}) = 202
futex(0x7fbd15b2a9d0, FUTEX_WAIT, 6, NULL <unfinished ...>
+++ exited with 143 +++
- strace -p 6698
(ENTRYPOINT ["/home/auth-server/entrypoint.sh"])
[root@ eureka-server]# strace -p 6698
strace: Process 6698 attached
wait4(-1, 0x7fffd3f084cc, 0, NULL) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=0, si_uid=0} ---
wait4(-1, 0x7fffd3f084cc, 0, NULL) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
+++ killed by SIGKILL +++
- strace -p 6716
(ENTRYPOINT ["/home/auth-server/entrypoint.sh"])
[root@ eureka]# strace -p 6716
strace: Process 6716 attached
futex(0x7fa3569749d0, FUTEX_WAIT, 7, NULL <unfinished ...>
+++ killed by SIGKILL +++
PID | SIGTERM | SIGKILL |
---|---|---|
6184(容器內(nèi) PID = 1) | √ | |
6698(容器內(nèi) PID = 1 )/bin/sh /home/auth-server/entrypoint.sh
|
√ | √ |
6716(容器內(nèi) PID = 6) | √ |
6184 接收到了 SIGTERM 信號彼水,而 6716 只接收到了 SIGKILL 信號被強制殺死翔曲,應(yīng)用進程沒有機會去做一些@PreDestroy 的善后操作搀突;但是 6698 先是接收到了 SIGTERM 而后通過 SIGKILL 被殺死萎攒。
也就是說 docker stop 向容器發(fā)送信號時让歼,SIGTERM 信號僅發(fā)送到 PID = 1 的容器進程饲帅。
由于 /bin/sh 不將信號轉(zhuǎn)發(fā)給任何子進程复凳,應(yīng)用進程接收不到 docker stop <container>發(fā)出的 SIGTERM 信號,只能等寬限時間結(jié)束后被強制終止灶泵,這樣我們的應(yīng)用程序就不能 graceful shutdown 優(yōu)雅終止育八。</container>
對于 Eureka Client 來說,會造成在更嚴重的下線通知延遲赦邻。
在非 graceful shutdown 情況下髓棋,客戶端不會調(diào)用 Eureka API 來更新 registry 注冊列表,而是只能等 Eureka Server 定時清理無效節(jié)點惶洲,這個周期默認是 60s按声,續(xù)約超時的時間默認是 90s,也就是說服務(wù)下線后恬吕,可能需要延遲 150s 之后签则,Eureka Server 中的 registry 對象才會被更新。而后還要經(jīng)過多輪同步铐料,客戶端才能感知到渐裂。
如果不可避免的要在容器里運行多個進程,能讓 1 號 init 進程在收到 SIGTERM 信號的同時钠惩,轉(zhuǎn)發(fā)給其它進程柒凉,就可以解決應(yīng)用非 graceful shutdown 的問題。
下面介紹一種方法來解決上面這個問題篓跛,既要保證應(yīng)用進程可以接收到 SIGTERM 信號膝捞,還要可以在容器內(nèi)手動 kill 掉應(yīng)用進程(這樣可以配合 Kubernetes 的存活探針達到重啟 Pod 的效果)。
使用 Tini 作為 init 進程愧沟。tini 會把它接收到的所有信號都轉(zhuǎn)發(fā)給它的子進程绑警,這正是我們想要的。
將 Dockerfile 文件 和 entrypoint.sh 腳本稍微改造一下:
Dcokerfile 中添加安裝 tini 語句央渣,ENTRYPOINT 使用 Tini 作為 init 進程计盒。
FROM frolvlad/alpine-java:jdk8-slim
RUN set -eux && mkdir -p /home/
RUN set -eux && mkdir -p /home/auth-server
RUN set -eux && mkdir -p /opt/logs/auth-server
RUN set -eux && touch /opt/logs/auth-server/auth-server.log
ADD auth-server.jar /home/auth-server/auth-server.jar
COPY entrypoint.sh /home/auth-server/entrypoint.sh
RUN chmod +x /home/auth-server/entrypoint.sh
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--", "/home/auth-server/entrypoint.sh"]
使用 exec 方式啟動可執(zhí)行程序,它會替換掉當前 /bin/sh 進程芽丹,并保持 PID 不變北启。
#!/bin/sh
ENV="-Denv=docker"
export JAVA_OPTS="-server -Xmx256m -Xms256m -XX:+UseG1GC"
export JAVA_OPTS="$JAVA_OPTS -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom"
exec java ${ENV} $JAVA_OPTS -jar /home/auth-server/auth-server.jar
echo "java ${ENV} $JAVA_OPTS -jar /home/auth-server/auth-server.jar"
echo "start success"
下面是改造之后的容器內(nèi)的進程信息:
[root@ dockerfile]# docker exec -it auth-server /bin/sh
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 /tini -- /home/auth-server/entrypoint.sh
6 root 0:12 java -Denv=docker -server -Xmx512m -Xms512m -XX:+UseG1GC -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom -jar /home/auth-server/auth-server.jar
30 root 0:00 /bin/sh
35 root 0:00 ps aux
再用 strace 監(jiān)測下 docker stop auth-server 時應(yīng)用進程收到的信號,下面可以看到應(yīng)用進程收到了 SIGTERM 信號。
[root@iZ2zece2l8yr2f8qhrnr3lZ ~]# strace -p 1336
strace: Process 1336 attached
futex(0x7f56967149d0, FUTEX_WAIT, 7, NULL) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=1, si_uid=0} ---
futex(0x7f5695adb580, FUTEX_WAKE_PRIVATE, 1) = 1
rt_sigreturn({mask=[]}) = 202
futex(0x7f56967149d0, FUTEX_WAIT, 7, NULL <unfinished ...>
+++ exited with 143 +++
應(yīng)用收到 SIGTERM 信號后咕村,就會做一些終止時的善后操作场钉,下面就是通知 Eureka Server 服務(wù)下線。
INFO [c.n.eureka.DefaultEurekaServerContext ] - Shutting down ...
INFO [c.n.eureka.DefaultEurekaServerContext ] - Shut down
INFO [com.netflix.discovery.DiscoveryClient ] - Shutting down DiscoveryClient ...
INFO [com.netflix.discovery.DiscoveryClient ] - Completed shut down of DiscoveryClient
很多開源項目的官方鏡像中都使用了這種方式懈涛,例如:
使用 tini 的基礎(chǔ)鏡像:
注意: 編寫 shell 腳本時需注意逛万,要讓腳本始終處于運行狀態(tài),因為 Docker 容器僅在 1 號進程運行時才保持運行 批钠,1 號進程退出宇植,Docker 容器也將退出。如果配置了 restart: always
埋心,你會發(fā)現(xiàn)容器一直在嘗試重啟指郁。
微服務(wù)演示項目中:auth-server 服務(wù)我是采用 tini 作為 init 進程來構(gòu)建的鏡像,你可以在 Kubernetes 平臺或者 Docker Compose 中嘗試上面所描述的問題拷呆。
~ END ~闲坎。