概述
微服务与docker结合是必然的趋势,本文介绍spring cloud与docker swarm集群实现微服务极简部署,不需要编写Dockerfile,不需要docker-compose编排容器。
上一篇文章已经介绍了docker swarm的环境搭建,这里不再叙述。
主要包含步骤:
- 结合maven docker 插件直接编译jar 包,生成和推送应用镜像。
- 通过docker swarm 命令创建服务。
- 线下和线上结合,自动化部署
制作java基础镜像
java镜像是spring boot应用的基础,这里基于openjdk:8,加入了时区,制作成了openjdk-tiejia:8
- 新建Dockerfile
FROM openjdk:8
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo 'Asia/Shanghai' >/etc/timezone
- 生成镜像
docker build -t 192.168.0.37:5000/openjdk-tiejia:8 - 推送
docker push 192.168.0.37:5000/openjdk-tiejia:8
docker-maven-plugin 插件与规范
docker-maven-plugin 插件可以通过maven生成镜像,作用跟Dockerfile类似。
docker-maven-plugin也很简单:先编译java项目生成jar包,然后加入到openjdk-tiejia:8 镜像,生成新的镜像,最终新的镜像就是java环境+jar包。
spring cloud maven 项目结构(可以参考之前几篇文章)
下面以finance金融微服务来说明。
- 公共父级pom文件
cehome-cloud-parent-0.0.1-SNAPSHOT.pom 是所有项目的公共父级pom文件 ,里面包含 spring boot/cloud 版本信息和 引用了docker maven 插件。这样的好处无须在每个项目都引入docker maven 插件。 - 项目目录结构
每个微服务都包含了api和service两个模块,api是接口定义,service是代码实现。
spring_cloudfinancepom.xml:项目父pom模块
spring_cloudfinancefinance-servicepom.xml: 主体服务
spring_cloudfinancefinance-apipom.xml: 接口定义
pom详情以及docker-maven-plugin 插件使用
也就是说,有4中pom文件,分别是公共父pom、项目父pom、api定义pom、service实现pom
1)公共父pom
这个文件除了定义spring cloud 依赖,还包含了 docker-maven-plugin(https://github.com/spotify/docker-maven-plugin),插件说明可以直接查看xml中的注释。此插件对应命令为 mvn package docker:build 。
在properties中设置开关属性skipDockerBuild=true ,意思是缺省忽略docker编译。一般都多模块的项目只有主模块需要生成镜像,其它是不需要的,所以缺省是忽略。例如我们的项目api模块不需要DockerBuild,只有service模块需要生成的spring boot jar包、需要打镜像,则在properties中设置skipDockerBuild为false即可。
插件配置entryPoint的java命令我们已经加入了部分固定的jvm参数,
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.cehome.cloud</groupId>
<artifactId>cehome-cloud-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<mybatis-version>1.3.0</mybatis-version>
<skipDockerBuild>true</skipDockerBuild>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.5.7.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.SR3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<distributionManagement>
<repository>
<id>internal-releases</id>
<name>Local Release Repository</name>
<url>http://192.168.0.12:8080/nexus/content/repositories/releases/</url>
</repository>
<snapshotRepository>
<id>internal-snapshots</id>
<name>Local Snapshot Repository</name>
<url>http://192.168.0.12:8080/nexus/content/repositories/snapshots/</url>
</snapshotRepository>
</distributionManagement>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.4.13</version>
<configuration>
<!-- skipDockerBuild 是开关,skipDockerBuild=true ,意思是忽略docker编译 -->
<skipDockerBuild>${skipDockerBuild}</skipDockerBuild>
<!-- 随便起名 -->
<imageName>192.168.0.37:5000/cehome/${project.artifactId}</imageName>
<!-- 基础镜像,这里用java 8,也可以 java或java:7 -->
<baseImage>192.168.0.37:5000/openjdk-tiejia:8</baseImage>
<!-- 启动后执行的命令 -->
<entryPoint>["java","-verbose:gc", "-Xloggc:/root/logs/gc.log", "-XX:+PrintGCTimeStamps", "-XX:+PrintGCDetails","-XX:+HeapDumpOnOutOfMemoryError", "-XX:HeapDumpPath=/root/dump","-Dsystem.out.filename=/root/logs/%tF.out" ]</entryPoint>
<!-- 往基础镜像里面拷贝的资源,这里只拷贝编译后的jar -->
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
</configuration>
</plugin>
</plugins>
</build>
</project>
2)项目父pom
只需要集成公共父pom、包含finance-api、finance-service模块。
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.cehome.cloud</groupId>
<artifactId>cehome-cloud-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>finance</artifactId>
<packaging>pom</packaging>
<modules>
<module>finance-api</module>
<module>finance-service</module>
</modules>
</project>
3)api定义pom
引入了公共父pom、依赖feign , 还有一个spring-boot-thin-layout 插件作用是编译成普通jar包而不是spring boot肥jar。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.cehome.cloud</groupId>
<artifactId>cehome-cloud-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>finance-api</artifactId>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot.experimental</groupId>
<artifactId>spring-boot-thin-layout</artifactId>
<version>1.0.5.RELEASE</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
3)service实现pom
继承项目父pom。
它是生成镜像的模块,需要设置属性
<skipDockerBuild>false</skipDockerBuild>
依赖spring cloud 模块、依赖finance-api接口模块。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.cehome.cloud</groupId>
<artifactId>finance</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>finance-service</artifactId>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<skipDockerBuild>false</skipDockerBuild>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.cehome.cloud</groupId>
<artifactId>finance-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
maven 编译、生成和推送镜像
mvn clean package docker:build -U -DpushImage -DdockerImageTags -DdockerImageTags=`date +%Y%m%d`
-DdockerImageTags 为镜像tag,这里用了当前日期变量(如:20171125)
-DpushImage 指生成镜像后还继续推送镜像,镜像的目标地址在公共父pom中已经指定好了(192.168.0.37:5000/cehome/${project.artifactId}),这个镜像在后面创建服务的时候需要用到。
部署 eureka注册中心
注册中心建议通过单机方式来部署
- eureka项目代码开发
代码略,eureka是个单模块的项目,只给出pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.cehome.cloud</groupId>
<artifactId>cehome-cloud-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<properties>
<!-- build docker images-->
<skipDockerBuild>false</skipDockerBuild>
</properties>
<artifactId>eureka</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
- maven编译和推送镜像
- clean package docker:build -U -DpushImage -DdockerImageTags -DdockerImageTags=
date +%Y%m%d
- 在0.37/0.38两台机器分别执行如下代码启动注册中心
docker pull 192.168.0.37:5000/cehome/eureka
docker stop eureka
docker kill eureka
docker run -itd --rm --net=host --hostname vm-eureka --name eureka 192.168.0.37:5000/cehome/eureka\
-Xms512m -Xmx512m -XX:MaxMetaspaceSize=256m -jar ./eureka.jar
- 注意问题:eureka 两个节点是互为注册,eureka.client.serviceUrl.defaultZone 参数不能包含两个节点的url,只能是对方的url,例如在0.37上,defaultZone要指向0.38, 参考:
eureka.client.fetch-registry=false
eureka.client.register-with-eureka=true
eureka.client.serviceUrl.defaultZone=http://192.168.0.38:8761/eureka/
logging.level.root=info
server.port=8761
spring.application.name=eureka
为了保持一份代码,一种方式是java 命令行中带上了spring boot支持的 --eureka.client.serviceUrl.defaultZone参数;另一个办法就是在ureka.client.serviceUrl.defaultZone配上两个地址,然后在代码中动态去掉本机的地址,这个稍微麻烦。
- 访问 192.168.0.37:8761
看到ds replicas 和下面的registered-replicas、available-replicas指向0.38就是对了。
同样访问192.168.0.38:8761,ds replicas等指向0.37才对。
集群服务部署
这里用operation运营微服务来说明。
生成、推送镜像
- 进入服务目录
- spring_cloud/operation
- 生成、推送镜像
- clean package docker:build -U -DpushImage -DdockerImageTags -DdockerImageTags=
date +%Y%m%d
第一次部署服务(创建服务)
docker service create \
--replicas 2 \
--name operation-service\
--network tiejia \
--publish 8804:8804\
--hostname="vm-{{.Service.Name}}-{{.Task.Slot}}" \
--mount type=volume,src={{.Service.Name}}-{{.Task.Slot}},dst=/root \
--detach=false \
192.168.0.37:5000/cehome/operation-service\
-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m -jar ./operation-service.jar
命令说明:
docker service create //创建服务命令
--replicas 2 // 两个节点
--name operation-service //服务名称
--network tiejia //服务使用网络,参考前一篇文章中的网络创建
--publish 8804:8804 //对外:对内端口映射
//{{.Service.Name}} 指向--name,即operation-service;{{.Task.Slot}}指节点编号:1,2
--hostname="vm-{{.Service.Name}}-{{.Task.Slot}}"
// 目录映射
--mount type=volume,src={{.Service.Name}}-{{.Task.Slot}},dst=/root
--detach=false
192.168.0.37:5000/cehome/operation-service //镜像地址
-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m -jar ./operation-service.jar //java 参数
hostname、mount 两个参数可以不用
mount type=volume 方式会自动创建指定的目录,但必须挂载在/var/lib/docker/volumes/ 路径下;type=bind 方式需要手工创建目标目录,但可以是任意路径。微服务涉及多个节点,用volume 不用手工去创建目录了。
第二次部署服务(更新服务)
docker service update \
--update-delay 40s\
--detach=false\
operation-service
当原有镜像有修改则会重新部署,否则不会部署。
可以加入 --force 参数,不管镜像更新不更新都会重启。
--update-delay 两个节点启动间隔。这个不会判断第一个节点是否启动完毕了,只是第一个节点开始部署40秒后就部署第二个节点。后面会讲到如何避免此类问题。
伸缩服务节点
docker service scale --detach=false operation-service=3
回滚到上一版本
--rollback 只能回滚到上一次执行 docker service update 之前的状态,并不能无限制地回滚
docker service rollback operation-service
或
docker service update --rollback operation-service
回滚到任意版本
前面push镜像时(查dockerImageTags)都会打上了一个日期的tag,通过这个tag可以重新启动任意日期的版本,达到回滚的目的。
- 查看已有的版本
- images
- 启动20180725版本
docker service update \
--image 192.168.0.37:5000/cehome/operation-service:20180725\
--update-delay 40s\
--detach=false\
operation-service
重新修改java 参数
docker service update --args "-Xms512m -Xmx512m -XX:MaxMetaspaceSize=256m -jar ./operation-service.jar" --detach=false operation-service
查看集群日志
docker service logs operation-service
线下和线上结合,自动化部署
方案实施
上面介绍的内容可以认为是线下环境,我们还需要考虑线下、线上结合Jenkins实现自动化部署。
- 线下通过Jenkins 实现svn或git拉取代码、maven编译jar包和生成、推送镜像。
- 通过 Jenkins来调用docker service 命令 ,实现线下自动化部署。
- 部署后进行线下测试。
- 测试完毕后,通过Jenkins进行预发和线上环境的部署。
注意问题:
- 我们所有项目都使用配置中心来管理线下和线上的配置,代码和配置是分离的。这样只需要线下打包(生成镜像)一次就能线上直接使用,从效率和安全方面都得到提高。
- 线上也需要访问私有镜像仓库,以便拉取镜像启动服务。如果线上无法访问私有镜像仓库,则需要在线上单独创建一个私有镜像仓库,从线下在push到线上去
镜像打上线上dockerhub.tiejia.com标签
docker tag 192.168.0.37:5000/cehome/eureka dockerhub.tiejia.com/cehome/eureka
推送,第一次推送会慢一些,第二次是增量的,只把添加到镜像的jar包上传。
docker push dockerhub.tiejia.com/cehome/eureka
线上环境准备
预发环境集群至少需要1台机器,推荐3台机器,全部部署成manager节点。
生产环境集群至少需要3台生产机器,全部部署成manager节点。 manager需要奇数个以便选举主节点,推荐是7台
具体部署参考之前的文章。
微服务预发和生产环境区分
微服务通过eureka注册中心shi实现互相访问,所以在预发环境部署一套eureka,生产环境部署两个eureka。各服务分别连到预发或生产的eureka就可以自动区分开来。
镜像、容器维护
本地镜像、容器删除
docker system prune -a
-a, --all Remove all unused images not just dangling ones
私服镜像维护
查看所有镜像(忽略版本)
http://192.168.0.37:5000/v2/_catalog
{"repositories":["cehome/account-service","cehome/audit-service","cehome/call-center-service","。。。
查看某一个镜像
http://192.168.0.37:5000/v2/cehome/account-service/tags/list
{"name":"cehome/account-service","tags":["20180408","latest","20180410","20180411","20180413",
清除旧镜像
- 安装pip
yum -y install epel-release
yum install python-pip
pip install --upgrade pip
https://blog.csdn.net/yulei_qq/article/details/52984334
- 安装requests
pip install requests
- 安装清理主程序
https://github.com/burnettk/delete-docker-registry-image
1)安装
curl https://raw.githubusercontent.com/burnettk/delete-docker-registry-image/master/delete_docker_registry_image.py | sudo tee /usr/local/bin/delete_docker_registry_image >/dev/null
sudo chmod a+x /usr/local/bin/delete_docker_registry_image
2)修改 /usr/local/bin/delete_docker_registry_image,找到registry_data_dir 改为实际的私服存储路径(也可以在环境变量REGISTRY_DATA_DIR指定):
if 'REGISTRY_DATA_DIR' in os.environ:
registry_data_dir = os.environ['REGISTRY_DATA_DIR']
else:
registry_data_dir = "/root/hub/docker/registry/v2"
requests 模块在下面的工具要依赖。
- 继续安装清理脚本
curl https://raw.githubusercontent.com/burnettk/delete-docker-registry-image/master/clean_old_versions.py | sudo tee /usr/local/bin/clean_old_versions >/dev/null
sudo chmod a+x /usr/local/bin/clean_old_versions
- 执行
clean_old_versions --image '^.*' --exclude 'latest' -l 2 --registry-url http://192.168.0.37:5000
clean_old_versions --image '^cehome/user-service' --exclude 'latest' -l 2 --registry-url http://192.168.0.37:5000
{"name":"cehome/user-service","tags":["latest","20180627","20180705"]}
实践问题
docker limit问题
ulimit -n
centos7.2宿主机缺省是1024需要设置,docker容器缺省是65536可以不用设置
双物理机器,一台重启,当掉的服务会自动切换到另一台,第二台可能无法承受,如何处理?
自动启动服务本来这也是集群的一个优点,但对于仅2台机器来说则不太好。目前没找到禁止自动启动服务的办法,最好是集群有3台机器。
docker swarm 集群里面服务之间无法调用
1)部署operation-service 2个点后,到注册中心一看,只有一个点,地址是10.255.0.4,
2)进入容器发现有多个网卡,10.255.0.4这个网址在2个点都有,正确的应该是10.0.0.7
3)如何选择ip参考
http://www.mamicode.com/info-detail-1873672.html
具体的几个配置参考
http://blog.csdn.net/xichenguan/article/details/76557065
4)最终一种巧妙的解决办法,配置ip选择参数:
spring.cloud.inetutils.preferred-networks=none
none或随便其它非ip字符串则导致一个都ip都匹配不上!
最终结果让其忽略所有网卡ip,这样最终会取本地host:InetAddress.getLocalHost();
这个跟/etc/hosts 配置的一样,所以OK了。
附:
//前缀匹配10.0.0开头的ip
spring.cloud.inetutils.preferred-networks=10.0.0
//正则允许所以ip
spring.cloud.inetutils.preferred-networks=.+
健康检查与回滚
健康检查可以保证一个节点启动完毕才会启动另一个节点
docker service update --force --detach=false\
--health-cmd "curl --fail http://127.0.0.1:8820/startup-status?p=0 || exit 1" \
--health-interval 5s\
--health-retries=20 \
--health-timeout 2s\
gateway
1)url必须返回0,表示成功;curl一般用127.0.0.1和内部端口,用实际ip不行(因为命令是在docker里面执行的 )
2)上面表示每5秒执行一次,每次2秒超时,一共尝试20次。 超时必须设置大一些,否则超时后会不断重启
3)如果url错误导致启动失败可以--no-healthcheck 恢复:
docker service update --force --detach=false --no-healthcheck gateway
加上回滚参数:
如下面把端口8820改成8830肯定curl是会失败,则会回滚
docker service update --force --detach=false\
--health-cmd "curl --fail http://127.0.0.1:8830/startup-status?p=0 || exit 1" \
--health-interval 5s\
--health-retries=20 \
--health-timeout 2s\
--update-failure-action rollback\
gateway
--update-failure-action Action on update failure ("pause"|"continue"|"rollback")
缺省是paused
这个比--update-delay duration 保险,--update-delay 只是是两个点启动间隔,只是大概估计的时间,所以在健康检查上面的基础上加个几秒即可:--update-delay 5s
启动超时
加入健康检查后,如果重试若干次后超时,则会导致容器不断重启!!加入 restart-condition=none 来避免。
猜测:是app正常启动后有问题会触发update-failure-action;如果程序还没启动完就超时则触发restart-conditionjava