上一篇文章提到了,如何利用Dockerfile 镜像,以及相关细节。但是文章中,确实存在一些问题,这里将今天查阅资料后的思考和总结梳理下。
在编写start-app.sh
的过程中,因为要实现启动java并做健康检测的工作,我最开始的有一个版本是这么写的,伪代码如下:
nohup java -jar xxx.jar
health_check
而入口的Dockerfile
也仅仅是通过sh start-app.sh
来启动。通过此镜像运行的容器,在健康检测完成后,整个容器就退出了。当时一脸懵逼。
docker ps -a
可以明显看出Status
是exited(0)
。通过查阅资料后,才知道:Docker中第一个启动的进程是1号进程,当1号进程执行完毕后,容器结束。回到脚本中,因为我是直接执行的脚本,所以1号进程是shell,而脚本中在执行了健康检测后,就会直接退出这个shell,虽然此时Java
进程已经启动,但是因为1号进程shell
退出了,所以这个容器也就退出了。
使用docker stop
关闭容器时,会将信号量传递给1号进程,告知其需要关闭,如果10秒内1号进程没有完成关闭,那么就会进行强制关闭。
按照我们上文的写法,很明显java进程不是1号进程,项目每次都是因为超时10秒被强制杀掉进程的,这样风险相当大,所以此处必须优化。
优化思路很明显:Java进程作为1号进程
Java进程作为1号进程有2种常见方式
Dockerfile
中Entrpoint
直接通过java -jar xxx.jar
来启动,比如这样:ENTRYPOINT ["java","-jar","/opt/app/server.jar"]
通过shell脚本启动,但是通过exec
替换进程。
ENTRYPOINT ["sh","/opt/app/start-app.sh"]
exec
执行java进程,但是该指令会沿用进程号,也就是还是1。exec java ${BASE_JVM_OPT} ${JAVA_OPTS} -jar ${JAR_NAME}
推荐采用第二种,因为生产线启动项目,绝对会包含很多环境变量,传入一些JVM参数,见这些写在Dockerfile
中,不优雅
如果把JVM全部写死在start-app.sh
中,也是不够优雅的,不利于扩展,那么怎么做更合适了?我认为必须的参数可以写死到启动脚本中,但是堆的参数可以通过外部传入,经过今天修改和如下所示:
Dockerfile的内容
FROM registry.cn-hangzhou.aliyuncs.com/uewell/java-base:v210728
MAINTAINER CodingOX "codingox@outlook.com"
#编译的时候,传递进来
ARG port
# ENV
ENV spring.profiles.active=test
ENV fastjson.parser.safeMode=true
# 拷贝文件
COPY target/xxx.jar /opt/app/server.jar
COPY start-app.sh /opt/app/start-app.sh
EXPOSE ${port}
ENTRYPOINT ["sh","/opt/app/start-app.sh"]
调用该Dockerfile进行打包的images-build.sh
内容如下所示:
#!/bin/bash
set -xeuo pipefail
# 定义参数
APP=your-server
REPOSITORY=your-repository
VERSION=2107-test
PORT=12008
echo "step-1:开始应用打包:${APP}"
mvn install -Dmaven.test.skip=true
echo "step-2:利用Dockerfile 进行 打包."
docker build \
--build-arg port=${PORT} \
--tag ${REPOSITORY}/${APP}:${VERSION} \
.
echo "step-3:images开始打tag."r
docker tag ${REPOSITORY}/${APP}:${VERSION} registry.cn-hangzhou.aliyuncs.com/xxx/${APP}:${VERSION}
echo "step-4:images开始推送到仓库."
docker push registry.cn-hangzhou.aliyuncs.com/xxx/${APP}:${VERSION}
echo "${APP} 流程成功..."
这个start-app.sh
内容如下所示:
#!/bin/bash
set -xeuo pipefail
# 修改APP_FULL_NAME为云效上的应用名
APP_HOME=/opt/app # 从package.tgz中解压出来的jar包放到这个目录下
JAR_NAME=/opt/app/server.jar # jar包的名字
APP_LOG=${APP_HOME}/logs # 项目日志
START_LOG=${APP_LOG}/start.log # 应用的启动日志
BASE_JVM_OPT="" # 可能出现,unbound varables
# 创建出相关目录
mkdir -p ${APP_HOME}
mkdir -p ${APP_HOME}/gc
mkdir -p ${APP_LOG}
start_application() {
BASE_JVM_OPT="${BASE_JVM_OPT} -XX:+ParallelRefProcEnabled -XX:+PrintCommandLineFlags"
BASE_JVM_OPT="${BASE_JVM_OPT} -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${APP_HOME}/gc"
BASE_JVM_OPT="${BASE_JVM_OPT} -XX:ErrorFile=${APP_HOME}/gc/hs_err_pid%p.log"
BASE_JVM_OPT="${BASE_JVM_OPT} -javaagent:/opt/agent/skywalking-agent.jar"
echo "starting java process"
exec java ${BASE_JVM_OPT} ${JAVA_OPTS} -jar ${JAR_NAME}
echo "started java process"
}
start_application
同上一版本相比变动如下:
exec
启动进程,目的是将Java进程替换为1号进程BASE_JVM_OPT
是基础的JVM参数,描述了一些必须的参数${JAVA_OPTS}
,它是通过docker启动命令带入的环境变量。以上是打包时候的的修改,对于宿主机的重启脚本也进行了完善。
#!/bin/bash
APP_PORT=12008 # 应用端口
HEALTH_CHECK_URL=http://127.0.0.1:${APP_PORT} # 应用健康检查URL
APP_START_TIMEOUT=180 # 等待应用启动的时间
health_check() {
exptime=0
echo "checking ${HEALTH_CHECK_URL}"
while true; do
status_code=$(/usr/bin/curl -L -o /dev/null --connect-timeout 5 -s -w %{http_code} ${HEALTH_CHECK_URL})
if [ "$?" != "0" ]; then
echo "application not started"
else
echo "code is $status_code"
if [ "$status_code" == "200" ]; then
curl 'https://oapi.dingtalk.com/robot/send?access_token=your_token' \
-H 'Content-Type: application/json' \
-d '{"msgtype": "text","text": {"content":"IP报警:服务启动成功"},"at":{"atMobiles":["18711xxxxxxxx"]}}'
break
fi
fi
sleep 3
((exptime = exptime + 3))
echo -e "\rWait app to pass health check: $exptime..."
if [ $exptime -gt ${APP_START_TIMEOUT} ]; then
echo 'app start failed'
curl 'https://oapi.dingtalk.com/robot/send?access_token=your_token' \
-H 'Content-Type: application/json' \
-d '{"msgtype": "text","text": {"content":"IP报警:服务启动失败"},"at":{"atMobiles":["18711xxxxxxxx"]}}'
exit 1
fi
done
}
echo "step1:更新镜像.."
docker pull registry.cn-hangzhou.aliyuncs.com/uewell/xxx:2107-test
echo "step2:停止原容器.."
docker stop smart-server
echo "step3:删除原容器.."
docker rm smart-server
echo "step4:运行新容器.."
docker run -d --name smart-server \
-p 12008:12008 --privileged=true \
-m 3G --memory-swap=3G \
-e JAVA_OPTS="-Xmx2048M -Xms2048M -Xmn768M -XX:MaxMetaspaceSize=412M -XX:MetaspaceSize=412M -XX:+UseG1GC -XX:MaxGCPauseMillis=200" \
registry.cn-hangzhou.aliyuncs.com/uewell/xxxx:2107-test
echo "step5:健康检测..."
health_check
同上一个版本相比主要变化如下:
-e JAVA_OPTS="-Xmx2048M -Xms2048M -Xmn768M -XX:MaxMetaspaceSize=412M -XX:MetaspaceSize=412M -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
越发觉得:解决一个技术难题,结果不是最重要的,最重要的是解决难题过程中的积累,问题的答案可能是1,但是过程可能是5。