您现在的位置是:首页 > 文章详情

微服务实战之春云与刀客(五)—— spring cloud与docker swarm集群

日期:2018-07-24点击:524

概述

微服务与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是代码实现。

image
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编译和推送镜像
  1. 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就是对了。

image
image

同样访问192.168.0.38:8761,ds replicas等指向0.37才对。

集群服务部署

这里用operation运营微服务来说明。

生成、推送镜像

  • 进入服务目录
  1. spring_cloud/operation
  • 生成、推送镜像
  1. 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可以重新启动任意日期的版本,达到回滚的目的。

  • 查看已有的版本
  1. images

image

  • 启动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
image
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

原文链接:https://yq.aliyun.com/articles/618620
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章