一、背景
公司项目有springmvc和springboot两种类型,springmvc项目主要通过tomcat启动,springboot项目主要通过java [option] -jar *.jar的方式启动。我们准备把springmvc项目和springboot项目先做成镜像先在灰度环境跑,如果没有问题我们再把镜像push到生产镜像仓库,最后选择在灰度验证通过的镜像运行在k8s上从而实现springmvc迁移。
二、实现原理
2.1 主要流程
- 从测试环境拉取已经编译好的代码
- 修改测试配置为生产配置
- 通过Dockerfile把已经修改好配置的程序打包成业务镜像
- 把镜像push到灰度镜像仓库,然后通过灰度镜像仓库的业务镜像启动
- 如果需要增量更新,可以预先把程序通过docker cp 拷贝出来,然后更新指定文件后docker cp回去,docker restart imageName,如果没有启动没有问题,可以docker commit imageName newImageName,最后push到生产镜像仓库。
三、应用运行在灰度环境
3.1 参数化构建过程参数
- 项目名称,包含容器名称和项目名称
- 版本号,用于打镜像tag
- 测试环境地址,包含ip地址,测试环境标识
- 是否为springboot项目,springmvc和springboot项目配置文件结构不一样,走的逻辑不同
- 是否更新到灰度环境【1.226】,因为这个自由风格的项目运行在一台专门打镜像的机器上【0.26】,所以如果需要更新到灰度机器需要连接到灰度机器【1.226】执行运行指定业务镜像的脚本
3.2 参数化构建的执行时shell
3.2.1 执行时shell的基本步骤
- 通过shell字符串切割把传入的projectVals拆分成想要的字符串
- 把指定测试环境的服务代码拷贝到执行此项目的机器上【0.26】
- 进入WEB-INF/classes目录下把测试环境配置替换为生产配置,为制作生产镜像做准备
- 拷贝一份已经写好的springmvc的Dockerfile 【在3.2.2节中已说明】,替换项目名称为传入的项目名称
- 通过修改后的Dockerfile生成镜像,并push到灰度镜像仓库
- 如果需要在灰度机器上跑已经制作好的镜像,可以远程到灰度机器,执行projectName.sh脚本 【在3.2.3节中已说明】
- 如果需要增量更新,通过docker cp 把webapps/finalName 文件夹拷贝出来,增量更新后 restart容器,启动无误可以commit,最后push到生产镜像仓库供线上k8s使用
contentName="$project"
currentTime=`date "+%Y%m%d%H%M"`
echo $currentTime
export PATH="/opt/apache-maven-3.3.3/bin:$PATH"
projectVals=(${projectVals//,/ })
projectName=${projectVals[0]}
containerArray=(${projectName//-/ })
container=${containerArray[1]}
project=${projectVals[1]}
finalName=${projectVals[2]}
if [ "$finalName" == "" ]; then
finalName=$project
fi
tag=$finalName-${gitVersion##*/}-$currentTime
TestEnvs=(${TestEnv//,/ })
serverName=${TestEnvs[0]}
serverHost=${TestEnvs[1]}
if [ "$gitVersion" == "" ]; then
echo "gitVersion不能为空"
exit
fi
cd /plk/source
if [ "true" != "$isSpringBoot" ] ; then
rm -rf $finalName
sshpass -p $remotePasswd scp -r -P 3721 $serverHost:/programs/$project/target/$finalName .
cd $finalName/WEB-INF/classes
pwd
ls
if [ -f "$finalName-mq.properties" ] ; then
sed -i 's|.mg.addr|-mg-addr|g' $finalName-mq.properties
sed -i 's|mq-mg-addr|mq1-mg-addr:9876;mq2-mg-addr:9876;mq3-mg-addr|g' $finalName-mq.properties
fi
echo "set prod zookeeper"
sed -i 's|{dubbo.registry.address}|{dubbo.registry.cluster.address}|g' $finalName-*.xml
if [ ! -z "`grep 'jdbc.properties' $finalName-spring-context-server.xml`" ]; then
echo "set prod mysql config"
sed -i 's|jdbc.mg.addr:3306/moni|rm-***.mysql.rds.aliyuncs.com:3306/moni_b|g' *jdbc.properties
sed -i 's|jdbc.mg.addr:3306/game|rm-***.mysql.rds.aliyuncs.com:3306/game_b|g' *jdbc.properties
sed -i 's|.username=***|.username=***|g' *jdbc.properties
sed -i 's|.password=***|.password=***|g' *jdbc.properties
fi
if [ ! -z "`grep 'mongo.properties' $finalName-spring-context-server.xml`" ]; then
echo "set prod mongo config"
sed -i 's|=mongo.mg.addr:27017|=dds-***.mongodb.rds.aliyuncs.com:3717,dds-***.mongodb.rds.aliyuncs.com:3717|g' *-mongo.properties
sed -i 's|mongo.mg.addr|dds-***.mongodb.rds.aliyuncs.com|g' *-mongo.properties
sed -i 's|mongo.readonly.mg.addr|dds-***.mongodb.rds.aliyuncs.com|g' *-mongo.properties
sed -i 's|database=statistics|database=statistics_b|g' *-mongo.properties
sed -i 's|database=everydaykline|database=everydaykline_b|g' *-mongo.properties
sed -i 's|database=evaluation|database=evaluation_b|g' *-mongo.properties
sed -i 's|database=safedata|database=safedata|g' *-mongo.properties
sed -i 's|credentials=evaluation:***@evaluation|credentials=***:***@evaluation_b|g' *-mongo.properties
sed -i 's|credentials=statistics:***@statistics|credentials=***:***@statistics_b|g' *-mongo.properties
sed -i 's|credentials=everydaykline:***@everydaykline|credentials=***:***@everydaykline_b|g' *-mongo.properties
sed -i 's|credentials=safedata:***@safedata|credentials=***:***@safedata|g' *-mongo.properties
sed -i 's|27017|3717|g' *-mongo.properties
fi
if [ ! -z "`grep 'redis' $finalName-spring-context-server.xml`" ]; then
echo "set prod redis config"
sed -i 's|redis-cluster|redis|g' $finalName-spring-context-server.xml
sed -i 's|=redis.mg.addr|=mgsc-***.redis.rds.aliyuncs.com|g' $finalName-*redis.properties
sed -i 's|=Mogu07550831..|=Mogu07550831**|g' $finalName-*redis.properties
fi
cd /plk/source
cp -f /plk/source/Dockerfile.mvc Dockerfile/Dockerfile.${finalName}
sed -i "s|{project}|${finalName}|g" Dockerfile/Dockerfile.${finalName}
else
//次数逻辑为springboot项目逻辑,下篇文章详细展开
fi
sudo docker build -f /plk/source/Dockerfile/Dockerfile.${finalName} -t registry-vpc.cn-shenzhen.aliyuncs.com/moguyun-pre/$dockerStore:$tag .
sudo docker login --username $dockerHubName --password $dockerHubPasswd registry-vpc.cn-shenzhen.aliyuncs.com
sudo docker push registry-vpc.cn-shenzhen.aliyuncs.com/moguyun-pre/$dockerStore:$tag
if [ "true" == "$isPreUpdate" ] ; then
sshpass -p Mogupro0601sz ssh root@$preEnvHost "cd /docker/k8s; sh -x ${projectName}.sh $tag 2>&1 &"
fi
if [ "true" == "$isIncUpdate" ] ; then
sshpass -p Mogupro0601sz ssh root@$preEnvHost "cd /opt/apps; rm -rf $finalName && docker cp $container:/opt/tomcat/webapps/$finalName ."
fi
3.2.2 springMVC项目的Dockerfile模板
- 定义源路径和目标路径,为拷贝文件做准备
- 定义RUN_OPTION,JVM内存大小分配【 tomcat 生产内存调优】,字符集编码
- 定义pname,这个参数会作为sysinit.sh 的第二个位置参数传递给脚本,最终传入CATALINA_OPTS,tomcat启动加载CATALINA_OPTS实现向pinpoint服务端的注册。【详情参考:通过Dockerfile编写tomcat镜像 pinpoint 客户端配置】
- 定义RUNPORT,这个参数会作为sysinit.sh 的第三个参数传递给脚本,最终会做一个if判断,如果RUNPORT为""或者为0,tomcat里面的server.xml默认的8080端口就不会改变。【详情参考: 通过Dockerfile编写tomcat镜像】
] # vim Dockerfile.mvc
FROM registry.moguyun.com/tomcat:9.26
MAINTAINER lijun moguyun.com
ENV SOURCEPATH {project}
ENV TARGETPATH /opt/tomcat/webapps
ENV RUN_OPTION "-Xms1g -Xmx4g -Xmn256m -Dfile.encoding=UTF-8"
ENV PNAME {project}
ENV RUNPORT 0
ADD $SOURCEPATH $TARGETPATH/$SOURCEPATH
EXPOSE 8080
CMD ["sh", "-c", "/bin/sysinit.sh \"$RUN_OPTION\" \"$PNAME\" \"$RUNPORT\" "]
3.2.3 灰度机器启动镜像脚本
- 定义项目名称,用于拼接项目日志路径
- 定义pname,通过docker -e加载的参数会覆盖Dockerfile里面定义的pname
- 定义运行端口号,使用默认的桥接模式可以允许所有容器都使用8080端口,暴露在宿主机上的端口自定义 【详情参考: Docker网络模型对比 】
- 定义镜像名称,${3} 为镜像tag,通过jenkins传入
- 定义远程DEBUG端口号,这个参数被JVM_OPTION加载,JVM_OPTION作为一个变量被RUN_OPTIONS应用,作为sysinit的第一个位置参数,最终被CATALINA_OPTS变量引用,作为一个启动参数注入到tomcat里面 【详情参考: 通过Dockerfile编写tomcat镜像】
- 通过docker 默认的桥接网络类型启动镜像,docker run -e 指定参数会覆盖在Dockerfile里面定义好的变量【详情参考:Docker run 命令的使用方法】
] # vim commom.sh
#!/bin/sh
project=${1}
pro=${project//_/-}
pname=${2}
RUN_PORT=${4}
DEBUG_PORT=${5}
version="registry-vpc.cn-shenzhen.aliyuncs.com/moguyun-pre/env-b:${3}"
JVM_OPTION="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=${DEBUG_PORT}
-Duser.timezone=GMT+08 -Dfile.encoding=UTF-8 -server -Xms256m -Xmx1024m "
echo "/docker/tomcat/logs/$project"
echo "JVM_OPTION=$JVM_OPTION"
echo "a1=$1"
echo "a2=$2"
echo "a3=$3"
echo "a4=$4"
echo "a5=$5"
docker stop $pname && docker rm $pname
docker run -d --name $pname \
-v /opt/image_data:/image_data:rw \
-v /opt/image_data:/opt/image_data:rw \
-v /etc/hosts:/etc/hosts:ro \
-v /docker/tomcat/logs/$project:/opt/tomcat/logs:rw \
-p $RUN_PORT:8080 \
-p $DEBUG_PORT:$DEBUG_PORT \
-e RUN_OPTION="$JVM_OPTION" \
-e PNAME="$pname" \
$version
sleep 35
docker logs --tail 400 $pname
- 依次传入 project,pname,镜像tag,宿主机上运行端口号,远程debug端口号
] # vim financial.sh
/docker/k8s/common.run financial-shop-server financial $1 6207 9909
四、增量更新灰度应用
开发有时需要增量更新一个类,而不是全量,因为这个服务可能多个开发同时在开发,如果在上一个自由风格项目勾选了【isPreUpdate】,就会把容器里面的代码拷贝一份出来到 /opt/apps目录,然后通过winscp连接到灰度【1.226】机器,把增量类替换/opt/apps目录下的服务,然后docker cp拷贝到容器里面,重启容器。
4.1 增量更新
- 通过winscp连到灰度机器【1.226】,然后把需要更新的类替换/opt/apps/目录下的指定服务类,下图就是已经把本地的类替换到灰度机器【1.226】的指定目录
扫描二维码关注公众号,回复:
8915282 查看本文章
4.2 重启容器
通过执行restartFinancial就可以重新启动了,我们把restartFinancial定义成一个别名,这个别名执行restartServer.sh脚本
4.2.1 重启服务脚本
- $1为应用名称
- $2为容器名称
] # vim restartServer.sh
#!/bin/bash
docker cp /opt/apps/$1 $2:/opt/tomcat/webapps/ && docker restart $2 ; docker logs $2 -f --tail 400
4.2.2 定义别名
] # vim ~/.bashrc
alias restartFinancial='sh -x /opt/apps/restartServer.sh financial-shop-server financial'
五、推送镜像到生产
推送镜像到远程仓库主要实现过程就是docker commit容器成镜像,然后docker push到生产镜像仓库。
5.1 参数化构建过程参数
- 项目名称,包含容器名称和项目名称
- 镜像仓库名称,定义了生产镜像存放仓库
- 镜像仓库用户名/密码,用于登录远程镜像仓库
- 运行节点,灰度机器【1.226】
5.2 执行时shell
- 把灰度重启之后不报错的容器打包成镜像
- 登录远程镜像仓库,由于本地登录远程镜像仓库的信息隔一段时间就会失效,所以每次push前都会docker login一次
- 推送镜像到远程仓库
currentTime=`date "+%Y%m%d%H%M"`
echo $currentTime
projectVals=(${projectVals//,/ })
containerName=${projectVals[0]}
projectName=${projectVals[1]}
echo "cd /opt/apps"
cd /opt/apps
echo "tag=${projectName}_${currentTime}"
tag=${projectName}_${currentTime}
echo "docker commit -m "$containerName is commited at $currentTime" $containerName registry-vpc.cn-***.com/moguyun-prod/env-b:$tag"
docker commit -m "$containerName is commited at $currentTime" $containerName registry-vpc.cn-***.com/moguyun-prod/env-b:$tag
echo "docker login --username $dockerHubName --password $dockerHubPasswd registry-vpc.cn-shenzhen.aliyuncs.com"
docker login --username $dockerHubName --password $dockerHubPasswd registry-vpc.cn-shenzhen.aliyuncs.com
echo "docker push registry-vpc.cn-shenzhen.aliyuncs.com/moguyun-prod/$repository:$tag"
docker push registry-vpc.cn-shenzhen.aliyuncs.com/moguyun-prod/$repository:$tag
六、更新生产镜像
6.1 使用指定镜像更新生产K8S
- 进入K8S无状态应用编辑页面,选取指定的镜像名称和tag进行更新
6.2 验证应用
- 无状态应用app-financial管理的pod全部都跑起来了
- 查看financial应用的启动日志,由图中可以看出启动没有报错
至此:已经完成了SpringMVC应用迁移到生产K8S集群的所有步骤