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

概述

微服务与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 cloud与docker swarm集群
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就是对了。

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

同样访问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

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

  • 启动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
微服务实战之春云与刀客(五)—— spring cloud与docker swarm集群
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

上一篇:Java多线程中使用synchronized说明


下一篇:Easy Task —— 强大而简单的分布式任务调度平台