项目使用的是 SpringBoot
,打包为 Docker
镜像的方式也有很多,比如有官方或者第三方的 Maven
插件,但是这些方式都不够灵活,我们项目直接采用的是 Dockerfile
的方式进行打包部署。
build-images.sh
为总的打包入口,其作用有4个
maven
指令编译项目,并生成可以执行的jar
文件Dockerfile
进行images
的制作项目的 shell
脚本如下所示:
#!/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} 流程成功..."
其中使用到的 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/server-2108.jar /opt/app/server.jar
COPY start-app.sh /opt/app/
EXPOSE ${port}
ENTRYPOINT ["sh","/opt/app/start-app.sh"]
FROM
的是我们公司的基础镜像,其中包括了Java
、Arthas
、Skywalking
等基础环境和监控组件。ARG
是传入的参数,此类参数只能用于打包镜像,容器运行时是读取不到的ENV
是环境变量,如果项目启动过程中需要使用的变量,可以在此处设置,比如第一句代码就是设定配置文件为test
COPY
是拷贝文件和目录到镜像中,依次拷贝了jar
文件和app-server
的启动脚本EXPOSE
表示这个镜像要暴露的端口,就是编译镜像的时候,由build-images.sh
中传入进来的ENTRYPOINT
执行的就是启动脚本,如果启动很简单,不需要设置更多参数,这里可以采用如下所示ENTRYPOINT ["java","-jar","/opt/app/server.jar"]
启动脚本负责在 Docker
容器启动后, 进行项目启动和状态监测以及报警。
这里给出我们当前的脚本
#!/bin/bash
set -xeuo pipefail
# 修改APP_FULL_NAME为云效上的应用名
APP_START_TIMEOUT=120 # 等待应用启动的时间
APP_PORT=16667 # 应用端口
HEALTH_CHECK_URL=http://127.0.0.1:${APP_PORT} # 应用健康检查URL
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 #应用的启动日志
# 创建出相关目录
mkdir -p ${APP_HOME}
mkdir -p ${APP_HOME}/gc
mkdir -p ${APP_LOG}
start_application() {
# JAVA 启动参数
JAVA_OPT="${JAVA_OPT} -Xmx2048M -Xms2048M -Xmn768M -XX:MaxMetaspaceSize=412M -XX:MetaspaceSize=412M"
JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
JAVA_OPT="${JAVA_OPT} -XX:+ParallelRefProcEnabled -XX:+PrintCommandLineFlags"
JAVA_OPT="${JAVA_OPT} -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${APP_HOME}/gc"
JAVA_OPT="${JAVA_OPT} -XX:ErrorFile=${APP_HOME}/gc/hs_err_pid%p.log"
JAVA_OPT="${JAVA_OPT} -javaagent:/opt/agent/skywalking-agent.jar"
echo "starting java process"
nohup java ${JAVA_OPT} -jar ${JAR_NAME} >${START_LOG} &
echo "started java process"
}
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":["your_mobile"]}}'
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":["your_mobile"]}}'
exit 1
fi
done
}
# 启动
start_application
# 健康检测
health_check
# docker 容器的 pid 1进程退出的话,容器就停止了。
while true; do
sleep 300
done
简答说下这个脚本的作用
开篇定义了一些参数,方便复用
然后创建了一些用于存储日志文件的目录
start_application
nohup
启动nohup
启动health_check
curl
进行http
状态的检测钉钉机器人
进行提醒,并退出启动最后的 while
循环不优雅,这么做的目的是因为:容器启动的第一个进程是 shell
脚本,如果没有这个死循环,那么shell脚本就算执行成功,然后会退出,但是因为他是1号进程,一旦其退出,容器也终止服务了,所以此处搞了一个死循环。
这里有2个点值得探讨:
- 健康检测在容器内部做还是外部做?
- 这个死循环有些low,如何优化?
上面的内容都是在镜像打包和容器启动时需要的,那么这个镜像上传到仓库后,对应的宿主机如何启动和管理这个镜像。
你可以选择 Rancher
这类容器管理服务软件负责管理,也可以通过自己写脚本进行服务的停止和关闭。这里我们先通过脚本的形式进行容器的管理。
这个脚本要实现的功能:
那么脚本就非常简单了
#!/bin/bash
echo "step1:更新镜像."
docker pull registry.cn-hangzhou.aliyuncs.com/xxx/server:2107-test
echo "step2:停止原容器.."
docker stop xxx-server
echo "step3:删除原容器.."
docker rm xxx-server
echo "step4:运行新容器.."
docker run -d --name xxx-server \
-p 14008:14008 \
-m 3G --memory-swap=3G \
registry.cn-hangzhou.aliyuncs.com/xxx/server:2107-test
之前项目我们用的是云效的一套打包和执行,现在已经逐步替换为了基于 Rancher
的容器化部署,感慨一句:“真香”。
不过我相信这个过程也会有不少坑会踩,不过不踩坑,进步就缓慢,好技术就要尽快实践。