单个容器中运行多个JAR包-自动化融合部署解决方案
2.0版本更新内容
1、增加主服务(main.sh)运行日志和功能模块调用运行日志
2、对start.sh进行拆分,引入主服务与功能模块架构,功能模块文件放置到指定路径,主服务即可自动集成无需重启
3、修复新版本同名文件上传覆盖历史版本后无法进行版本更新的BUG
4、修复多节点集群部署状态下,部分节点校验md5file文件发现已经存在记录而不启动服务的BUG
5、JAR文件放置路径支持随意层级。
6、加入首次执行标记
7、实现开发规范,变量为下划线模式命名,函数为驼峰命名
8、实现各个Jar文件运行日志集中放置在logs/主目录下的Jar包所在路径的镜像路径下
9、主服务模块轮询时间随机生成(60-120),解决多节点同时调用公用文件的文件占用问题
10、增加java启动自定义参数功能
11、修复校验md5file文件提示binary operator expected导致校验失败
12、文件复制到容器本地存储空间进行启动,修复部分jar直接在obs挂载目录中无法启动的问题
2.0版本相关代码展示
#融合部署基础服务,主要作用是保证后台存在常驻服务使得容器处于存活状态,并引入其它相关模块用户功能拓展 #!/bin/bash first_mark=0 #首次运行标记 container_ip="$(ifconfig | grep inet | grep -v "127.0.0.1" | awk '{print $2}' | head -1)" poll_time="$(($(date +%s%N)%60+60))" #轮询时间 main_path="" if [[ "" == ${main_path} ]];then main_path="/apphub/main" echo -e "$(date) - ${container_ip} - 首次启动,基础服务执行路径:${main_path}" >> "${main_path}/main.log" fi main_log="${main_path}/main.log" echo -e "$(date) - ${container_ip} - 主服务模块轮询时间:${poll_time}s" >> "${main_log}" while true do sleep ${poll_time}s for var in $(find ${main_path} -type f -name "*.sh") do result=$(echo ${var} | grep "${0}") if [[ "" == ${result} ]];then echo -e "$(date) - ${container_ip} - 执行:${var}" >> ${main_log} source ${var} fi done done
#服务管理模块 #!/bin/bash ####变量区############## container_ip="$(ifconfig | grep inet | grep -v "127.0.0.1" | awk '{print $2}' | head -1)" mount_point="$(dirname ${main_path})" mount_point_local="${mount_point}_local" md5_file="${main_path}/md5file.txt" ###初始化 function envInit(){ echo -e "$(date) - ${container_ip} - 环境初始化标记:清理${md5_file}中该容器的历史数据" >> ${main_log} if [ ! -f "${md5_file}" ]; then touch "${md5_file}" fi if [ ! -d "${mount_point_local}" ];then mkdir -p ${mount_point_local} fi sed -i "/${container_ip}/d" ${md5_file} #初始化md5file文件,容器启动时,md5file文件中所记录的该容器历史数据都需要清空 } #容器初始化默认启动所有项目最新版本JAR包 function startAllApp(){ start_all_app_temp="/tmp/startAllApp.tmp" && > ${start_all_app_temp} for var in $(find -L ${mount_point} -type f -name "*.jar") do echo -e "${var%/*}" >> ${start_all_app_temp} done if [[ -s ${start_all_app_temp} ]];then for line in $(cat ${start_all_app_temp} | sort -n | uniq) do app="${line}/$(ls -lt "${line}/" | grep -E "*.jar$" | head -1 | awk '{print $9}')" app_log="${mount_point}/logs${line#*${mount_point}}" opts_conf="${line}/$(ls -lt "${line}/" | grep -E "*.conf$" | head -1 | awk '{print $9}')" if [ -f "${opts_conf}" ];then java_opts=$(cat ${opts_conf}) fi if [[ ! -d ${app_log} ]];then mkdir -p ${app_log} fi app_log="${app_log}/applog.log" echo -e "$(date) - ${container_ip} - 容器启动初始化模块:检测存在${app}" >> ${main_log} kill -9 $(ps -ef | grep -v "color=auto" | grep ${line} | awk '{print $2}') if [[ -f "${mount_point_local}${app}" ]];then rm -rf "${mount_point_local}${app}" fi cp --parents -rf "${app}" "${mount_point_local}" if [ -f "${mount_point_local}${app}" ];then app=${mount_point_local}${app} echo -e "容器启动初始化模块:文件成功复制到${app}" >> ${main_log} nohup java ${java_opts} -jar ${app} >> ${app_log} 2>&1 & fi if [[ '0' == ${?} ]];then echo -e "$(date) - ${container_ip} - 容器启动初始化模块:启动成功${app}" >> ${main_log} echo -e "$(date) - ${container_ip} - $(md5sum ${app})" >> ${md5_file} fi done else echo -e "$(date) - ${container_ip} - 容器启动初始化模块:所有项目中未检测到可启动服务。" >> ${main_log} fi rm -rf ${start_all_app_temp} && echo -e "$(date) - ${container_ip} - 容器启动初始化:删除${start_all_app_temp}" >> ${main_log} } function updateApp(){ update_app_temp="/tmp/updateApp.tmp" && > ${update_app_temp} for var in $(find -L ${mount_point} -type f -name "*.jar") do echo -e "${var%/*}" >> ${update_app_temp} done if [[ -s ${update_app_temp} ]];then for line in $(cat ${update_app_temp} | sort -n | uniq) do app="${line}/$(ls -lt ${line} | grep -E "*.jar$" | head -1 | awk '{print $9}')" app_log="${mount_point}/logs${line#*${mount_point}}" if [[ ! -d ${app_log} ]];then mkdir -p ${app_log} fi app_log="${app_log}/applog.log" opts_conf="${line}/$(ls -lt ${line} | grep -E "*.conf$" | head -1 | awk '{print $9}')" if [ -f "${opts_conf}" ];then java_opts=$(cat ${opts_conf}) fi file_tag=$(md5sum ${app} | awk '{print $1}') check=$(grep -a ${file_tag} ${md5_file} | grep -c ${container_ip}) if [[ '0' == ${check} ]];then echo -e "$(date) - ${container_ip} - 容器应用服务更新模块:检测存在待更新服务${app}" >> ${main_log} if [ ! -d "${mount_point_local}" ];then mkdir -p ${mount_point_local} fi kill -9 $(ps -ef | grep -v "color=auto" | grep ${line} | awk '{print $2}') && sleep 5s if [[ -f "${mount_point_local}${app}" ]];then rm -rf "${mount_point_local}${app}" fi cp --parents -rf "${app}" "${mount_point_local}" if [ -f "${mount_point_local}${app}" ];then app=${mount_point_local}${app} echo -e "容器应用服务更新模块:文件成功复制到${app}" >> ${main_log} nohup java ${java_opts} -jar ${app} >> ${app_log} 2>&1 & fi if [[ '0' == ${?} ]];then echo -e "$(date) - ${container_ip} - 容器应用服务更新模块:更新成功${app}" >> ${main_log} echo -e "$(date) - ${container_ip} - $(md5sum ${app})" >> ${md5_file} fi fi done fi rm -rf ${update_app_temp} } ############业务执行区################ #首次启动执行 if [[ 0 == ${first_mark} ]];then envInit startAllApp fi first_mark=1 #首次启动执行完成标记 updateApp
#日志切割模块 #!/bin/bash ####变量区############## do_time="23:30" #切割开始时间 log_path="/apphub/logs" log_name="applog.log" date_tag=$(date +%F) #############业务逻辑区############# #任务调用逻辑 function doTask(){ #对IFS变量 进行替换处理 OLD_IFS="$IFS" IFS=":" array=(${do_time}) IFS="$OLD_IFS" h=${array[0]} #对应do_time的小时 m=${array[1]} #对应do_time的分钟 if [ $(date +%H) -eq ${h} ];then if [ $(date +%M) -ge ${m} ];then cutLogs fi fi } #日志切割逻辑 function cutLogs(){ for var in $(find ${log_path} -name ${log_name}) do #cutlog.log是日志切割任务的执行记录文件 applog_path="${var%/*}" cut_log="${applog_path}/cutlog.log" if [ ! -f "${cut_log}" ]; then touch "${cut_log}" && echo "创建:${cut_log}" >> ${cut_log} fi do_tag="任务执行成功:${date_tag}:${var}"$'\n'"任务具体执行时间:$(date +"%Y-%m-%d %H:%M:%S")"$'\n'"====================" if [ $(grep -wc "任务执行成功:${date_tag}" ${cut_log}) -eq '0' ];then if [ ! -f "${applog_path}/${date_tag}-${log_name}" ];then cp ${var} "${applog_path}/${date_tag}-${log_name}" && cat /dev/null > ${var} if [ '0' == $? ];then echo "${do_tag}" >> ${cut_log} >> ${cut_log} else echo "任务执行失败:${data_tag}:${var}" >> ${cut_log} fi else echo "${do_tag}" >> ${cut_log} >> ${cut_log} && echo "任务终止标记,检查到已存在文件:${date_tag}" >> ${cut_log} fi fi done } ###############执行区########## doTask
需求背景
公司已经把所有的项目迁移到了云平台,引入k8s架构对系统进行产研维护。但是由于历史原因存在很多的个性化系统,这些系统的GVM价值并不高,但是由于客户承诺不能随意下线。面对众多的个性化系统与成本管控和进一步简化项目维护的需求困境下我们提出了以下两种方案,如下:
- 将众多个性化系统进行代码和业务重构合并为一个项目进行部署与维护。优点:合并后只有一个JAR包,非常便于部署和维护,成本管控优势明显。缺点:每个个性化项目对应一个需求点,多个系统不亚于重新开发一套中型系统且其中涉及到业务梳理重构与业务数据合并,改造人力成本巨大。
- 直接将众多个性化系统部署到同一个容器中,每个个性化系统启动不同的端口,通过URL转发方式面向各自客户。优点:人力成本小,几乎不需要改造原有项目。基本满足成本管控要求。缺点:项目维护难度提升。
综合各方面的因素后,我们决定采用方案2,并着力解决项目维护的难题,自此有了本方案。
方案中所面临的问题点介绍
- 所有的项目集中部署在一个容器中,且容器部署于K8S平台,当需要对其中一个项目进行版本更新时该怎么办。
- 基于当前的DEVOPS工作机制,当进行CI/CD操作重新部署过程中,是否会影响到该容器中部署的其它项目,导致服务中断。
- 当容器进行重启后,如何快速恢复重启前的状态,及同时启动多个项目自动恢复对外服务
解决方案核心示意图
实现方式说明:
- 基于华为云平台创建一个obs对象存储卷并将其配置挂载到容器的指定目录,同时相关项目的启动JAR包均放置在这个挂载目录中。
- 容器的主服务为一个shell脚本,使它一直运行并定时巡查容器挂载目录的文件情况,例如:项目文件新增,项目文件版本更新等。
- 配置流水线,由以前的代码编译后制作对应新版本镜像并进行部署修改为代码编译jar后上传到obs对象存储卷。并由shell主服务进行文件巡查,巡查到为项目新版本这shell对之前启动的jar服务进行kill并重启启动新版本的jar服务。
核心变化点:以前的项目部署都是通过容器的重启来完成,现在变化为容器不在轻易重启,版本更新等操作变为shell脚本直接对jar服务进行操作,由于shell是主服务。只要主服务存在,则jar服务可以随意kill或者启动。
具体的相关操作:
-
- obs对象存储卷创建
-
- 配置容器自动挂载obs对象存储卷
- Devcloud流水线
主要是在编译构建步骤进行修改,不在需要制作镜像和推送到仓库的操作而是改为上传文件到obs,同时后续的部署也不在需要。因为当你推送到obs桶后,shell主服务会接管后续的工作
Docker容器构建改造Dockerfile
FROM swr.cn-south-1.myhuaweicloud.com/itim-ms/jdk-plus:8u212 MAINTAINER <lijun7@sfmail.sf-express.com> USER root WORKDIR /root COPY start.sh /root/start.sh RUN mkdir /apphub && yum install -y coreutils net-tools ENTRYPOINT ["/bin/bash"] CMD ["/root/start.sh"]
Shell主服务代码(start.sh)
#!/bin/bash #####变量区 mount_point="/apphub" md5file="/apphub/md5file" #####环境初始化 function envInit(){ > ${md5file} #初始化md5file文件,每次容器重启都应该是一个全新的文件 } #####业务逻辑区 #容器启动初始化挂载obs目录 #obs挂载使用的是对象存储卷,直接在部署任务中进行配置。所以该方法再次不需要启用。 function mountPath(){ umount ${mount_point} obsfs obsfs_01 ${mount_point} -o url=obs.cn-south-1.myhuaweicloud.com -o passwd_file=/etc/passwd-obsfs -o max_write=131072 -o nonempty -o use_ino if [ $(df -h | grep "obsfs_01") -eq '0' ];then echo -e "obs挂载成功" else echo -e "obs挂载失败,程序退出。" exit 0; fi } #1:多个项目 ##1.1:每个项目应该有一个专属的文件夹 ##1.2:程序需要对每个项目文件夹进行遍历判断 #2:两种场景 ##2.1:容器初始化启动场景:容器中还未启动任何jar包 ##2.2.1应用包版本更新场景:相关服务的jar已经运行,需要进行版本更新 ##2.2.2应用包版本更新场景:容器启动后,在新项目中放入jar,且该项目目前只有一个jar包 #容器初始化默认启动所有项目最新版本JAR包 function startAllApp(){ for var in $(ls -d ${mount_point}/*) do app_name=$(ls -lt ${var} | grep -E *.jar | head -1 | awk '{print $9}') if [ -n "${app_name}" ];then echo -e "当前执行模块:容器初始化默认启动所有项目最新版本JAR包" app_name=${var}/${app_name} echo -e "程序启动:${app_name}" nohup java -jar ${app_name} & if [[ '0' == ${?} ]];then echo -e "启动成功:${app_name}" md5sum ${app_name} >> ${md5file} fi fi done } #版本更新程序启动逻辑 #容器启动后项目新放置了唯一的jar包 function startUpdateApp(){ for var in $(ls -d ${mount_point}/*) do line=$(ls -lt ${var} | grep -E *.jar | wc -l) app_name="$(ls -lt ${var} | grep -E *.jar | head -1 | awk '{print $9}')" if [[ '1' -eq ${line} ]];then app_name=${var}/${app_name} file_tag=$(md5sum ${app_name} | awk '{print $1}') if [ $(grep -c ${file_tag} ${md5file}) -eq '0' ];then echo -e "当前执行模块:项目版本更新模块" echo -e "程序发现项目唯一文件${app_name},同时md5file中不存在记录。将直接进行启动操作" nohup java -jar ${app_name} & md5sum ${app_name} >> ${md5file} fi elif [[ '1' -lt ${line} ]];then app_name=${var}/${app_name} file_tag=$(md5sum ${app_name} | awk '{print $1}') if [ $(grep -c ${file_tag} ${md5file}) -eq '0' ];then echo -e "当前执行模块:项目版本更新模块" echo -e "程序发现最新文件${app_name},同时md5file中不存在记录。将进行版本更新操作" kill -9 $(ps -ef | grep -v "color=auto" | grep ${var} | awk '{print $2}') && echo -e "版本更新操作等待10s" && sleep 10s nohup java -jar ${app_name} & if [[ '0' == ${?} ]];then echo -e "启动成功:${app_name}" md5sum ${app_name} >> ${md5file} fi fi fi done } ###########业务执行区 envInit startAllApp while true do startUpdateApp sleep 60s done
注意事项
Obs对象存储块的目录结构为:/apphub/项目名称/jar文件
Obs对象存储块中的md5file文件是一个标识文件,用于标识那个文件是已经被shell主服务启动过的以及那个同名文件下那个为新版本文件。容器每次重启都会自动清空标识记录。
本文系作者 @Mr.Lee 原创发布在 维简网。未经许可,禁止转载。