驽马十驾 驽马十驾

驽马十驾,功在不舍

目录
利用Dockerfile打包项目并发布上线的记录-2
/  

利用Dockerfile打包项目并发布上线的记录-2

开篇

上一篇文章提到了,如何利用Dockerfile 镜像,以及相关细节。但是文章中,确实存在一些问题,这里将今天查阅资料后的思考和总结梳理下。

改进1:1号进程的问题

在编写start-app.sh的过程中,因为要实现启动java并做健康检测的工作,我最开始的有一个版本是这么写的,伪代码如下:

nohup java -jar xxx.jar
health_check

而入口的Dockerfile也仅仅是通过sh start-app.sh来启动。通过此镜像运行的容器,在健康检测完成后,整个容器就退出了。当时一脸懵逼。

docker ps -a

可以明显看出Statusexited(0)。通过查阅资料后,才知道:Docker中第一个启动的进程是1号进程,当1号进程执行完毕后,容器结束。回到脚本中,因为我是直接执行的脚本,所以1号进程是shell,而脚本中在执行了健康检测后,就会直接退出这个shell,虽然此时Java进程已经启动,但是因为1号进程shell退出了,所以这个容器也就退出了。

1号进程的作用

使用docker stop关闭容器时,会将信号量传递给1号进程,告知其需要关闭,如果10秒内1号进程没有完成关闭,那么就会进行强制关闭。

按照我们上文的写法,很明显java进程不是1号进程,项目每次都是因为超时10秒被强制杀掉进程的,这样风险相当大,所以此处必须优化。

优化思路很明显:Java进程作为1号进程

Java进程作为1号进程有2种常见方式

  • DockerfileEntrpoint直接通过java -jar xxx.jar来启动,比如这样:ENTRYPOINT ["java","-jar","/opt/app/server.jar"]

  • 通过shell脚本启动,但是通过exec替换进程。

    • Dockerfile中,ENTRYPOINT ["sh","/opt/app/start-app.sh"]
    • 然后shell中通过exec执行java进程,但是该指令会沿用进程号,也就是还是1。exec java ${BASE_JVM_OPT} ${JAVA_OPTS} -jar ${JAR_NAME}

推荐采用第二种,因为生产线启动项目,绝对会包含很多环境变量,传入一些JVM参数,见这些写在Dockerfile中,不优雅

改进2:JVM参数的问题

如果把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

同上一版本相比变动如下:

  • 去掉了健康检测,放在外部。1号进程是1个原因,另外一个原因就是Docker自带了健康检测,可以利用他的健康检测。
  • 通过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":["18682695467"]}}'
        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":["18682695467"]}}'
      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

同上一个版本相比主要变化如下:

  • 加入了健康检测和报警
  • docker run的时候,指定了参数:-e JAVA_OPTS="-Xmx2048M -Xms2048M -Xmn768M -XX:MaxMetaspaceSize=412M -XX:MetaspaceSize=412M -XX:+UseG1GC -XX:MaxGCPauseMillis=200"

结语

越发觉得:解决一个技术难题,结果不是最重要的,最重要的是解决难题过程中的积累,问题的答案可能是1,但是过程可能是5。

骐骥一跃,不能十步。驽马十驾,功在不舍。