驽马十驾 驽马十驾

驽马十驾,功在不舍

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

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

开篇

项目使用的是 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

其中使用到的 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的是我们公司的基础镜像,其中包括了JavaArthasSkywalking等基础环境和监控组件。
  • 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

    • 其中定义了项目所需的启动参数,你可以根据自己的项目进行定制
    • 因为健康检测是放在容器里面进行,为了不阻碍shell继续执行,所以此处采用的是nohup启动
    • 你也可以选择在外部脚本进行健康检测,此时这里可以不用nohup启动
  • health_check

    • 通过curl进行http状态的检测
    • 如果启动时间超过了阈值,那么通过钉钉机器人进行提醒,并退出启动
    • 如果启动成功,那么也会提醒一次。
  • 最后的 while循环不优雅,这么做的目的是因为:容器启动的第一个进程是 shell脚本,如果没有这个死循环,那么shell脚本就算执行成功,然后会退出,但是因为他是1号进程,一旦其退出,容器也终止服务了,所以此处搞了一个死循环。

这里有2个点值得探讨:

  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的容器化部署,感慨一句:“真香”。

不过我相信这个过程也会有不少坑会踩,不过不踩坑,进步就缓慢,好技术就要尽快实践。

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