2 Star 6 Fork 1

Jeff Wang / Springcloud on Docker Swarm

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README
Apache-2.0

Springcloud on Docker Swarm

介绍

笔记:使用 Docker Swarm 部署 Spring cloud 微服务案例

前言

微服务的架构是基于使用资源换能力的思路设计的(SOA当然也是其核心思路),因此,在实际部署应用时会出现大量的集群式部署,这会带来两方面的挑战:

  • 系统拓扑结构复杂
  • 部署更新困难

而在开发阶段,也会给测试环境带来不小的困扰,如何有效配置、隔离个人测试环境呢?

对于开发环境,显而易见的答案是使用docker。而生产环境也是可以使用docker方便的构建、更新集群的。

本文以SpringCloud alibaba 体系为例,完成在Docker Swarm集群下的环境搭建。

Part 1: Docker 基础

本章内容相当基础,有经验的读者可直接转到:7.1.6 Demo 构建及启动,验证Demo项目的构建部署。

本部分使用 Dockerfile/Compose 等文件均在 docker 目录下。

1.1 环境

本文采用Windows WSL ubuntu虚拟机来搭建Docker环境。

1.2 Docker 安装

方便起见,使用 apt 安装 Docker。 首先需要增加Docker 的gpg Key:

$ sudo mkdir-p/etc/apt/keyrings
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o/etc/apt/keyrings/docker.gpg 

然后添加Repository:

$ echo \"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable"| sudo tee/etc/apt/sources.list.d/docker.list >/dev/null

可能需要提前安装:

$ sudo apt-get install \ 
   ca-certificates \
   curl \
   gnupg \
   lsb-release

之后安装Docker CE

$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin

参看版本:

$ docker -v
Docker version 20.10.21, build baeda1f

1.3 Docker 简单配置

使用 systemctl 或 service 启动 docker daemon :

$ sudo service docker start 

若要使用国内源,编辑/etc/docker/deamon.json 文件:

{

    "registry-mirrors": [
        "http://hub-mirror.c.163.com",
        "https://registry.docker-cn.com",
        "https://docker.mirrors.ustc.edu.cn"
    ]

}

重启动后生效:

$ sudo service docker restart 

检查是否生效:

$ sudo docker info | grep http
 Registry: https://index.docker.io/v1/
  http://hub-mirror.c.163.com/
  https://registry.docker-cn.com/
  https://docker.mirrors.ustc.edu.cn/

Ubuntu 必须使用 root 使用 docker 命令,每次使用 sudo 很麻烦,可以将当前用户添加到 docker 用户组中,即可正常使用 docker 。

$ sudo adduser your-user-name docker
$ groups # 显示是否添加成功
adm sudo netdev docker

添加组之后,重新登录即可生效。

1.3 Docker container

docker使用Container容器来运行应用。 可以将Container比作一个沙盒,运行于操作系统之上,且与隔离。 Docker 与 虚拟机的差别在于,虚拟机提供了一组虚拟化的硬件(cpu,磁盘等)。而docker仅提供了虚拟化的操作系统,因此,docker的成本远低于虚拟机。两者关系大概是:

graph BT
  subgraph Host宿主机
    hw[硬件]
    os[操作系统]
  end
  
  app[应用程序]
  
  subgraph VirtualMachine  
    vmhw[虚拟硬件]
    vmos[操作系统]
  end
 
  subgraph Docker  
    vos[虚拟操作系统]
  end

  hw --> os 
  os --> vmhw 
  os --> vos
  vmhw --> vmos --> app
  vos --> app

  os -.-> app  

1.3.1 运行container

使用docker 启动容器很简单、快速,以nginx为例:

$ docker run -d --name myweb -p 8080:80 nginx
e86eef6514efd6c18d958720f75c1b81eda67e2129380c97580472c126c9a401
$ curl localhost:8080
...
<title>Welcome to nginx!</title>
...

ngnix 已经在运行了。

看一下运行的 container 信息:

docker ps -a
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS                                   NAMES
e86eef6514ef   nginx          "/docker-entrypoint.…"   2 minutes ago   Up 2 minutes   0.0.0.0:8080->80/tcp, :::8080->80/tcp   myweb

信息中的重点:

  • CONTAINER ID: container 的唯一编号。是run之后那一大段的字符串的前12位。docker命令中使用它来代表container。
  • IMAGE: 容器使用的镜像名称。镜像里包含一个沙盒系统和应用程序。
  • COMMAND: 容器启动后,执行的命令。本例中,执行的是 nginx 启动命令。
  • STATUS:容器状态,表示是否在运行。使用 docker stop 或 start 或 restart 命令控制。
  • PORTS: 容器对外暴露的端口。0.0.0.0:8080->80/tcp表示 宿主机的 TCP 8080端口,代理 容器的 TCP 80 端口。也即,访问宿主机 8080端口都将被转交给 容器的 80端口。
  • NAME: container 的名称。上文通过 --name 指定的助记符,使用名字可替代 container id.
  • -d: --dettach 表示执行后,脱离该容器,也相当于将容器在后台启动。

简化的思路来看待container,可以认为它只是一个独立的应用程序在运行,至于运行哪个应用程序,这是由镜像里的内容来决定的。

docker 的启动、停止、重启动:

$ docker stop e86eef6514ef
e86eef6514ef
$ docker ps -a
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS                     PORTS     NAMES
e86eef6514ef   nginx          "/docker-entrypoint.…"   30 minutes ago   Exited (0) 5 seconds ago             myweb
$ docker start myweb
myweb
$ docker ps -a
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS         PORTS                                   NAMES
e86eef6514ef   nginx          "/docker-entrypoint.…"   30 minutes ago   Up 4 seconds   0.0.0.0:8080->80/tcp, :::8080->80/tcp   myweb
$ docker restart myweb
myweb

可见,使用 name 或 id 均可控制 container。

1.3.2 登录container

可以“登录”运行中的容器,进行一些检查、处理。 使用 docker exec 命令:

$ docker exec -i --tty myweb bash
# -i 交互式
# --tty 提供伪终端,这两项可连写为 -it 
# myweb 容器名称
# bash 相当于 /bin/bash,启动容器上的shell。            
root@e86eef6514ef:/# whoami
root
root@e86eef6514ef:/# hostname
e86eef6514ef
root@e86eef6514ef:/#

登录实际上是运行了conatiner上的 shell,并使用 -it 提供终端可以进行输入、输出。当然也可以执行其他命令。

1.3.4 删除Conatiner

Container stop 之后,仍占用着存储空间,可以通过 rm 命令删除:

$ docker rm myweb
myweb

1.4 Container文件操作

1.4.1 使用 Alpine

运行的Container是一个虚拟机,当然包含一个操作系统,及其上的应用程序。为了检验这一点,使用 Docker 推荐用于学习的 Alpine 操作系统作为示例。Alpine镜像只有8M,使用方便。

首先启动一个纯净的Alpine操作系统:

$ docker run -d -t --name alpine alpine
d97cb6d531474311a4a60b379ba98dc6dd76cb36492c1c0fda2e6b0f13c93532
docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS     NAMES
d97cb6d53147   alpine         "/bin/sh"                7 seconds ago   Up 6 seconds             alpine

1.4.2 安装JDK

apline 使用 apk 安装包。先连接至alpine容器,再执行安装命令:

docker exec -it alpine sh
/ # apk add openjdk8
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/community/x86_64/APKINDEX.tar.gz
...
(44/46) Installing openjdk8-jre-base (8.345.01-r0)
(45/46) Installing openjdk8-jre (8.345.01-r0)
(46/46) Installing openjdk8 (8.345.01-r0)
Executing busybox-1.35.0-r17.trigger
Executing fontconfig-2.14.0-r0.trigger
Executing mkfontscale-1.2.2-r0.trigger
Executing java-common-0.5-r0.trigger
Executing ca-certificates-20220614-r0.trigger
OK: 126 MiB in 60 packages
/ # exit

1.4.3 安装Spring APP

JDK安装完毕后,将需要运行的Springboot jar包复制到容器内:

# 首先建立目录 /app/
$ docker exec alpine mkdir /app
# cp 复制文件至 containerID或NAME:目的
$ docker cp ansible-spring-example1-0.0.1.jar alpine:/app/
$ docker exec -it alpine sh
/ # ls -l app
total 17220
-rw-r--r--    1 1000     1000      17629271 Nov 12 14:21 ansible-spring-example1-0.0.1.jar

可见,文件已经复制过去了。

1.4.3 启动Spring APP

可以登录并使用 java 来启动,也可以通过exec命令来启动。如:

$ docker exec alpine java -jar /app/ansible-spring-example1-0.0.1.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.7.5)
2022-11-23 02:11:37.739  INFO 197 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2022-11-23 02:11:38.941  INFO 197 --- [           main] j.f.o.a.AnsibleSpringExample1Application : Started AnsibleSpringExample1Application in 4.867 seconds (JVM running for 5.898)

这里没有使用 -d 参数,因此,exec命令没有返回。添加 -d 后即可让Spring在容器的后台执行。

Springboot 启动在 8080端口,使用curl访问:

$ curl localhost:8080
curl: (7) Failed to connect to localhost port 8080: Connection refused  

Springboot运行在 Container 中,其网络空间与宿主机隔离,未发布端口(即将Conatiner端口映射到宿主机端口上)时,无法从Container外部访问。 因此,需发布端口,但当前容器alpine已经无法追加端口,因此,需将alpine删除,并重新使用 -p 8080:8080 启动。

这里存在一个问题:镜像只读属性,即容器内的文件变化,并不会改变镜像内容,因此,在之前安装的openjdk8 Java 环境和spring app jar 文件都不会出现在新启动的容器中。

每次重复的执行安装、copy操作自然是太过麻烦,因此,为何不考虑将这些内容放入一个新的镜像里呢?

1.5 镜像制作

本节使用简单的命令来建立镜像。

1.5.1 理解Dockerfile

回顾上一节,为了在容器中运行Spring APP,我们执行了下列操作:

  1. 运行一个alpine操作系统:run ... alpine
  2. 安装JDK: apk add openjdk8
  3. 安装jar: docker cp .jar alpine:/app/
  4. 进入/app/目录:cd /app/
  5. 启动jar: java -jar ...

将这些步骤写成一个脚本,是不是就可以方便的运行这个应用了呢?

Docker 提供了类似的脚本机制来构建镜像:Dockerfile。 简单的讲,Dockerfile就是由上述命令组合而成,当然与之不同的是命令的格式。

1.5.2 第一个 Dockerfile

在Java 项目目录建立一个文本文件Dockerfile:

# First docker file
# 1. 运行一个alpine操作系统:run ... alpine  
FROM alpine 
# 2. 安装JDK: apk add openjdk8
RUN apk add openjdk8
# 3. 安装jar: docker cp .jar /app/
COPY target/ansible-spring-example1-0.0.1.jar /app/ 
# 声明暴露 8080端口
EXPOSE 8080/tcp
# 4. 进入/app/目录:cd /app/
WORKDIR /app/
# 5. 启动jar: java -jar ...
ENTRYPOINT java -jar ansible-spring-example1-0.0.1.jar

最简单的 Dockerfile就像一个脚本,这里引入了五个关键字,也是最常用的关键字。通过与脚本命令的对比,很容易理解其含义。

1.5.3 构建镜像

利用Dockerfile 构建的过程类似与执行了 Dockerfile中的各项命令,并将结果保存在“image镜像”中,构建结果可以复制、分发、运行。

使用 docker build命令进行构建:

$ docker build --tag springapp:0.1 ./
# docker build 自动在当前目录下寻找 Dorkerfile。
# 如需要指定 使用 -f some-docker-file-name
Sending build context to Docker daemon   35.4MB
Step 1/5 : FROM alpine
 ---> bfe296a52501
Step 2/5 : RUN apk add openjdk8
 ---> Running in 470fbb4d7ad3
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/community/x86_64/APKINDEX.tar.gz
...
(46/46) Installing openjdk8 (8.345.01-r0)
OK: 126 MiB in 60 packages
Removing intermediate container 470fbb4d7ad3
 ---> d202e956fd20
Step 3/5 : COPY target/ansible-spring-example1-0.0.1.jar /app/
 ---> 173eb3f32e41
Step 4/5 : WORKDIR /app/
 ---> Running in 469675eefc5b
Removing intermediate container 469675eefc5b
 ---> e72847f59a05
Step 5/5 : ENTRYPOINT java -jar ansible-spring-example1-0.0.1.jar
 ---> Running in 56d77a106ff5
Removing intermediate container 56d77a106ff5
 ---> d70f6f096ed9
Successfully built d70f6f096ed9
Successfully tagged springapp:0.1

上述命名构建了一个 名为 springapp, 版本号 0.1 的 镜像。

使用 image ls 来查看构建好的镜像 :

$ docker image ls
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
springapp    0.1       d70f6f096ed9   3 minutes ago   149MB
nginx        latest    88736fe82739   7 days ago      142MB
alpine       latest    bfe296a52501   10 days ago     5.54MB

spring app 已经构建完成。

注意:

使用 maven 可直接构建镜像。 Spring推荐将jar解压后打入image 这样可提高启动速度。

1.5.4 使用镜像启动程序

与之前一样,使用spingapp:0.1镜像启动程序并发布8080端口:

$ docker run --name myapp -p 8080:8080 -d springapp:0.1
bcb68fbbd2588a85e62cb8ad16f95fc198d2b72630f8ecd058b1bf2fce1bc1fc

$ docker ps
CONTAINER ID   IMAGE           COMMAND                  CREATED         STATUS         PORTS                                       NAMES
bcb68fbbd258   springapp:0.1   "/bin/sh -c 'java -j…"   8 seconds ago   Up 7 seconds   0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   myapp

$ curl localhost:8080/info
config: value=Value 1, host=192.168.2.130

容器的好处在于可以启动多个应用实例,可尝试分别启动 myapp1,myapp2并将端口发布到宿主机的8081,8082端口。

1.5.6 容器的自动重启动

使用容器对外提供服务时,如果容器程序意外退出,是否有办法自动重新启动呢? 容器启动时可以指定重启动的策略,包括:

Policy Result
no Do not automatically restart the container when it exits. This is the default.
on-failure[:max-retries] Restart only if the container exits with a non-zero exit status. Optionally, limit the number of restart retries the Docker daemon attempts.
always Always restart the container regardless of the exit status. When you specify always, the Docker daemon will try to restart the container indefinitely. The container will also always start on daemon startup, regardless of the current state of the container.
unless-stopped Always restart the container regardless of the exit status, including on daemon startup, except if the container was put into a stopped state before the Docker daemon was stopped.

下面是一个简单的示例:

$ docker run -d --restart=on-failure:5 --name test1 alpine sh -c "date && sleep 10 && exit 1"

这样 test1 当执行失败时,也就是 exit 1 之后,将进行5次重启。

检查重启情况:

$ docker ps -a 
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS         PORTS     NAMES
c5354bf97672   alpine         "sh -c 'date && slee…"   20 seconds ago   Up 9 seconds             test1 
..
$ docker ps -a 
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS                  PORTS     NAMES
c5354bf97672   alpine         "sh -c 'date && slee…"   34 seconds ago   Up Less than a second             test1

$ docker ps -a
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS                     PORTS     NAMES
c5354bf97672   alpine         "sh -c 'date && slee…"   5 minutes ago   Exited (1) 3 minutes ago             test1

从 CREATED 时间 和 STATUS中 UP 时间 相对比可知,启动了多次,最后停止服务(重启动5次后)。

Part 2: Docker Swarm

SWARM(蜂群),显而易见,这是一种集群技术。当需要一个或多个宿主机集群上部署多实例的应用时,Docker Swarm可以帮助快速实现这一目的,并且简洁易用。

2.1 Swarm 基本概念

2.2.1 Nodes 节点

Node 就是加入Swarm集群中的宿主机。 Swarm 集群将Node分成两种角色:

  • Manager: 管理节点,顾名思义,用于集群管理。其中有一台管理节点是集群Leader。(集群选举采用Raft,所以,manager node 数量必须是单数,否则会脑裂。推荐 3,5,7。)
  • Worker: 工作节点。也即无法执行管理命令的节点。(但Swarm很灵活,可以轻松的改变节点角色)。

另外,管理节点也是可以运行任务的。因此,最简的Swarm集群是:1个管理节点。本文大部分demo均使用该模式运行。

注意:Swarm各节点应配置时钟同步服务。

2.2.2 Services & Tasks

服务和任务是面向用户的核心概念。使用Swarm的目的就是发布服务和任务。

  • Service:服务,是对Swarm中运行容器任务的定义(声明)。 Swarm 使用服务定义来创建容器、执行任务,监视和调度任务。
  • Task: 任务,任务是指运行在Swarm中的容器。任务使用的镜像、命令、参数、副本数量、部署方式等均由服务定义。当服务提交到Swarm集群后,相应的任务就将执行。

2.2.3 Load balancing

Swarm支持服务运行时的负载均衡,Swarm采用内置的DNS服务实现服务发现,并可通过DNSrr方式在集群内实现负载均衡。

在集群之外,任何已发布的服务,都可以通过集群宿主机进行访问,而不必关心任务实际运行地址。

2.2 Swarm 体验

为体验多宿主机环境,可以使用 play-with-docker.com 的服务。

2.2.1 play-with-docker.com

Play-with-docker.com (简写为 PWD) 免费提供网页版虚拟机终端,可以在其上进行Docker 的各种测试包括Swarm。

使用PWD之前,需要注册用户,可以使用hub.docker的用户登录。登录之后,选择下方的 Start 按钮,会跳转至虚拟机终端界面。

PWD 提供了Swarm模板,可以一键创建 3 Manager 5 Worker 的 Swarm 集群。

本节多主机操作均在 PWD上完成。

注意:

PWD 终端使用 Ctrl+Ins 复制, Ctrl+Shift+V进行粘贴。

2.2.2 创建Swarm 集群

PWD中点击左侧 + ADD NEW INSTANE 启动一个host。在终端中创建一个Swarm集群:

$ docker swarm init 
Error response from daemon: could not choose an IP address to advertise since this system has multiple addresses on different interfaces (192.168.0.8 on eth0 and 172.18.0.58 on eth1) - specify one with --advertise-addr

多网卡情况下,需要指定一个网卡或地址,如:

$ docker swarm init --advertise-addr eth0 
Swarm initialized: current node (0hx30spp5mszfe6ytpck032c5) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-08zuzjjee7unrbek19fxi0aapiujqskhtpnie9qnusibddpn99-b5wzovk7p3tbr0niadkh1s4zm 192.168.0.8:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

再创建两个instance ,分别让他们作为Worker加入集群,将上面那行 join 命令复制,并在两台主机上执行。

[node2] (local) root@192.168.0.7 ~
$ docker swarm join --token SWMTKN-1-08zuzjjee7unrbek19fxi0aapiujqskhtpnie9qnusibddpn99-b5wzovk7p3tbr0niadkh1s4zm 192.168.0.8:2377
This node joined a swarm as a worker.

在第一台机器(Manager & Leader)查看集群节点信息:

$ docker node ls
ID                            HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
0hx30spp5mszfe6ytpck032c5 *   node1      Ready     Active         Leader           20.10.17
qku9a0sa9zj0ckpekz3b0wmlc     node2      Ready     Active                          20.10.17
0aocykuthmwjspq1p9x69zwz6     node3      Ready     Active                          20.10.17

已有一个Manager 两个 Worker。

再添加两个Manager,构建 3Manager 2 Worker 的集群。

添加Manager节点有两种办法:

  1. 使用join-token manager 加入:
$ docker swarm join-token manager 
To add a manager to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-08zuzjjee7unrbek19fxi0aapiujqskhtpnie9qnusibddpn99-9dfl34rb3fg6abemllnll3iw1 192.168.0.8:2377
# 在新主机运行上面的命令即可
  1. 将Worker提升为 Manager :
$ docker node promote node2 
Node node2 promoted to a manager in the swarm.
# 当然也可以降级:
$ docker node demote node2 
Manager node2 demoted in the swarm.

最后,可以直接使用PWD的模板功能,建立如下Swarm集群:

[manager1] (local) root@192.168.0.8 ~
$ docker node ls 
ID                            HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
jqzuxg0dju0z8qcmc2yvjk2xm *   manager1   Ready     Active         Leader           20.10.17
r759xo0x5ewz4zkz3vv39e26d     manager2   Ready     Active         Reachable        20.10.17
pzch463s4eyk3fcs15q75vb54     manager3   Ready     Active         Reachable        20.10.17
rqf3i5fcwetliead6knl4jego     worker1    Ready     Active                          20.10.17
s8kai33smwyg7ilzz5gaezy24     worker2    Ready     Active                          20.10.17

2.2.3 部署简单服务

同样,使用 nginx来作为样例部署一个简单的服务,在管理机上执行;

$ docker service create --name myweb -p 80:80 nginx 
fwg499h27ex24z5r7hxmanlnj
overall progress: 0 out of 1 tasks 
overall progress: 1 out of 1 tasks 
1/1: running   
verify: Service converged 

创建了一个服务,名为 myweb,镜像为 nginx,发布端口为80:80。 确认是否在执行:

$ curl localhost 
...
<h1>Welcome to nginx!</h1>
...

2.2.4 服务副本

Swarm 允许在集群中同时运行多个副本,使用Swarm scale 命令:

$ docker service scale myweb=5
myweb scaled to 5
overall progress: 5 out of 5 tasks 
1/5: running   
2/5: running   
3/5: running   
4/5: running   
5/5: running   
verify: Service converged 

查看一下服务任务的分布:

$ docker service ps myweb 
$ docker service ps myweb 
ID             NAME      IMAGE          NODE       DESIRED STATE   CURRENT STATE            ERROR     PORTS
i5y2501bz7mw   myweb.1   nginx:latest   manager1   Running         Running 12 minutes ago             
598mgf71oht0   myweb.2   nginx:latest   manager2   Running         Running 30 seconds ago             
qtev1qo00xgo   myweb.3   nginx:latest   worker2    Running         Running 24 seconds ago             
ceap6buum0be   myweb.4   nginx:latest   manager3   Running         Running 21 seconds ago             
g1ef5avrbppk   myweb.5   nginx:latest   worker1    Running         Running 20 seconds ago          

可见五个服务均匀分布在集群的五个节点上。

2.2.5 负载均衡

为了理解swarm内部负载均衡的效果,首先将服务副本数缩减到2个,这样便于观察:

# 使用service update 命令,效果等同于 scale myweb=2
$ docker service update myweb --replicas 2
myweb
overall progress: 2 out of 2 tasks 
1/2: running   
2/2: running   
verify: Service converged 
[manager1] (local) root@192.168.0.8 ~
$ docker service ps myweb 
ID             NAME      IMAGE          NODE       DESIRED STATE   CURRENT STATE            ERROR     PORTS
i5y2501bz7mw   myweb.1   nginx:latest   manager1   Running         Running 22 minutes ago             
598mgf71oht0   myweb.2   nginx:latest   manager2   Running         Running 22 minutes ago             

可见myweb的两个tasks分别运行在 manager1 和 manager 2 上。 在Manger2上查找并登录该容器:

$ docker ps -a 
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS     NAMES
43ce24c967ae   nginx:latest   "/docker-entrypoint.…"   35 minutes ago   Up 35 minutes   80/tcp    myweb.2.598mgf71oht0sxcnt6b6yxcb4
[manager2] (local) root@192.168.0.7 ~
$ docker exec -it 43ce24c967ae bash 
root@43ce24c967ae:/# cd /usr/share/nginx/html/           
root@43ce24c967ae:/usr/share/nginx/html# echo "THIS is Manager2" >> index.html 
root@43ce24c967ae:/usr/share/nginx/html# exit

在manager2 执行两次 curl :

$ curl localhost 
...
</html>THIS is Manager2 
$ curl localhost 
...
</html>

由此可见,第一次访问的是 manager 2 上的容器,第二次访问的是 manager1上的容器。这证明 swarm 内部实现了负载均衡。

更进一步,目前worker1/worker2上都没有运行服务,在其上执行 curl ,可以得到相同的结果。这说明Swarm的宿主机代理了服务的发布端口,并在swarm内部查找真实的容器。

由此,Swarm集群作为一个整体对外提供服务,外部应用不需要关心服务实际运行在哪里。

2.3 服务发现

Swarm内置服务发现功能,在Swarm集群内部,部署了DNS 服务,可以有效的对容器服务进行发现和路由。这也是实现微服务动态部署的基础。

2.3.1 Docker Network

Docker 容器使用的网络与宿主机网络隔离的(当然,允许连接 Host network,但这不具备扩展性)。 Docker 使用三种网络类型:

  • host: 宿主机网络,容器直接使用宿主机的网络环境。
  • bridge : docker 单机环境使用的网络模式,在单机内形成互通网络。
  • overlay: "覆盖"网络,即可在集群范围内形成一个独立网段。

Swarm初始化后会自动产生两个网络:

$ docker network ls
NETWORK ID     NAME              DRIVER    SCOPE
246151484c4d   bridge            bridge    local
fb35c7706da7   docker_gwbridge   bridge    local
e831c5c177e2   host              host      local
s87b9i434vkn   ingress           overlay   swarm
  • docker_gwbridge: 将其他overlay 网络连接至host网络的桥接。
  • ignress:swarm 专用的overlay网络,支持集群内负载均衡的overlay 网络。Swarm service conatainer 启动后自动连接ingress网络。

可使用 docker network inspect 来查看网络的详细信息。

docker network inspect -f "{{json .IPAM }}" ingress
{"Driver":"default","Options":null,"Config":[{"Subnet":"10.0.0.0/24","Gateway":"10.0.0.1"}]}

再查看Conatiner使用的网络:

$ docker ps -a 
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS     NAMES
43ce24c967ae   nginx:latest   "/docker-entrypoint.…"   35 minutes ago   Up 35 minutes   80/tcp    myweb.2.598mgf71oht0sxcnt6b6yxcb4
$ docker inspect -f "{{json .NetworkSettings.Networks }}" 43ce24c967ae
{"ingress":{"IPAMConfig":{"IPv4Address":"10.0.0.81"},"Links":null,"Aliases":["adb8d46973d8"],"NetworkID":"s87b9i434vknuzehzwgyotxzr","EndpointID":"e1ef001f46ae846c2b80266001538b77ceb413b59b952e4ae8de8ac158d9ff5e","Gateway":"","IPAddress":"10.0.0.81","IPPrefixLen":24,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:0a:00:00:51","DriverOpts":null}} 

Swarm 的服务Conatiner连接了ingress网络。

再来看单机的Conatiner :

$  docker inspect -f "{{json .NetworkSettings.Networks }}" alpine
{"bridge":{"IPAMConfig":null,"Links":null,"Aliases":null,"NetworkID":"246151484c4d46f06ce4217481a047e143b967e266f7e7dbf6b8729fc34334ca","EndpointID":"244f93e90a7b6c8520dce7018f7ec8bafcd180da55d0522623cc2784944ccf5b","Gateway":"172.17.0.1","IPAddress":"172.17.0.2","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:ac:11:00:02","DriverOpts":null}}

单机Container绑定在brige网络上。

2.3.2 Swarm Ingress Routing Mash

Swarm通过ingress形成路由网,所有Service发布的端口,均可从宿主机访问。如下图:

alt ingress network

在Swarm外可使用Nginx、HAProxy等代理服务实现外部负载均衡(当然可使用HA模式的负载均衡)。

2.3.3 服务发现(通过DNS访问Service和Task)

在Swarm集群中,Task所在的Container无论数量还是IP都是动态的,在微服务环境下如何实现服务发现呢?

ingress网络由Swarm使用,联通整个集群中的各个host和container,只需要再建立一个Overlay网络,就可以实现服务发现。

$ docker network create --driver overlay --attachable webnet
mwtsslhdhdul2k4umt2qbdzgm
[manager1] (local) root@192.168.0.18 ~
$ docker network ls 
NETWORK ID     NAME              DRIVER    SCOPE
327c51042095   bridge            bridge    local
1e9bbed6b521   docker_gwbridge   bridge    local
1197a9ecf5ce   host              host      local
j8vbzy2f3p4n   ingress           overlay   swarm
f039b36a286c   none              null      local
mwtsslhdhdul   webnet            overlay   swarm

使用 docker network create 创建网络:

  • --driver overlay: 指定网络为overlay,默认值为 bridge。
  • --attachable: 允许container通过命令连接该网络。
  • webnet: 网络名称。

可以使用--subnet 来指定 子网 信息。

将之前的 myweb 连接至该网络:

$ docker service update --network-add webnet --replicas 6 myweb 
myweb
overall progress: 6 out of 6 tasks 
1/6: running   
2/6: running   
3/6: running   
4/6: running   
5/6: running   
6/6: running   
verify: Service converged 

这里使用 service update 方式,为服务添加网络,并将副本数调整为 6 个。可使用inspect 参看网络状态。

加入webnet网络的Container,使用Swarm内置的服务名Tasks.或就可以直接访问Service,而无需使用IP。

新建容器 client 并连接webnet :

$ docker run -td --name client --network webnet alpine
$ docker exec -it client sh 
/ # ping -c 1 Tasks.myweb
PING Tasks.myweb (10.0.1.17): 56 data bytes
64 bytes from 10.0.1.17: seq=0 ttl=64 time=0.448 ms

--- Tasks.myweb ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.448/0.448/0.448 ms

/ # ping -c 1 myweb
PING myweb (10.0.1.13): 56 data bytes
64 bytes from 10.0.1.13: seq=0 ttl=64 time=0.164 ms

--- myweb ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.164/0.164/0.164 ms

可见,在相同网络内部,可通过服务名来访问,且每次域名解析的IP会发生变化,这是overlay网络内部的 load balancer 起作用。

Part 3: Docker Compose 和 Stack

微服务通常都是一组服务,比如:

  • gateway : 应用网关
  • REST Service:前端接口、API
  • Backend Service: 后端业务(可能再进一步拆分为中台、后台)
  • Enviromenet Systems: Database, MQ, Redis, ELK
  • Monitor:Prometheus...

相应的,很多系统需要网络隔离环境,以避免依赖混乱。

对应到Swarm集群上,这就包括了两部分,Service 和 Network。(当然还会有 Config / Secrets/ Volume)。 使用命令行逐一建立服务、Network并不现实,这里要用到Docker Compose使用声明文件来一次性做完上述工作。

3.1 Compose

Docker Compose 使用声明式语法来定义Service,Network以及Docker其他顶级对象。这些定义使用YAML格式编写在文件中。

3.1.1 简单的 COmpose 文件

下面的Compose文件simple-app-compose.yml编制了一个简单Spring App.

name: simple 
version: “3.9”
services:
  app:
    image: springapp:0.1 
    networks:
      - webnet
    ports:
      - "8080:8080"
networks:
  webnet:
    driver: overlay 

本文件定义了:

  • Project: 名为 simple
  • Service: 服务 app
    • 使用镜像 springapp:0.1
    • 使用网络 webnet
    • 发布端口 8080
  • Network: 名为web_net,类型为 overlay。

以上等价与命令:

$ docker network create --driver overlay webnet 
$ docker run --name app --network webnet -p 8080:8080 springapp:0.1 

使用compose up 启动:

$ docker compose -f simple-app-compose.yml up -d 
[+] Running 2/2
 ⠿ Network simple_webnet   Created                   0.0s
 ⠿ Container simple-app-1  Started                   2.1s

compose创建了网络:simple_webnet 和 容器 simple-app-1。

Docker compose 使用 project name作为前缀创建docker元素。

检查是否正常运行:

$ curl localhost:8080/info
config: value=Value 1, host=192.168.2.130

3.1.2 多服务的compose 文件

Docker Compose 的能力当然不仅于此,Compose 的目的在于管理一组服务。 为简化实验过程,以下仅使用alpine镜像作为例子。 在simple-app-compose.yml中再添加几个项目:

services:
  app: 
  ...
    depends_on:
      - database
  gateway:
    image: nginx:alpine 
    depends_on:
      - app
    ports: 
      - "8001:80"
    networks:
      - webnet 
  database:
    image: nginx:alpine
    ports: 
      - "8002:80" 
    networks:
      - webnet    

添加了两个新的服务:gateway 和 database,两者都加入同一个网络webnet。 depends_on属性声明了需要依赖的其他服务,这会建立一个正确的启动顺序:

  • database
  • app
  • gateway

使用docker compose命令执行:

$ docker compose -f simple-app-compose.yml down 
[+] Running 2/0
 ⠿ Container simple-app-1       Removed                   0.0s
 ⠿ Network simple_webnet        Removed         
$ docker compose -f simple-app-compose.yml up -d 
[+] Running 4/4
 ⠿ Network simple_webnet        Created                                 0.0s
 ⠿ Container simple-database-1  Created                                 0.1s
 ⠿ Container simple-app-1       Created                                 0.1s
 ⠿ Container simple-gateway-1   Created    

注意上述执行顺序,是按照依赖关系依次启动的。

Compose 很适合用于单机搭建环境,比如,开发人员的测试环境,使用 compose 搭建独立的 APP+Databse+Redis之类的系统,方便快捷。

3.3 Docker Stack

Docker Stack 沿用了 Compose 的声明式语法,进一步扩展成为应用于集群部署的编排工具。

3.3.1 从Compose到Stack

为了更直观的看到Compose和Stack的差别,直接使用上一节中的simple-app-compose.yml在Swarm部署:

$ docker stack deploy -c simple-app-compose.yml simple 
(root) Additional property name is not allowed

这个错误信息说明,Stack不允许使用name属性定义,只能在命令行定义。 将simple-app-compose.yml复制改名为app-stack-compose.yml,注释掉第一行 name: simple。

$ docker stack deploy -c app-stack-compose.yml simple 
Creating network simple_webnet
failed to create network simple_webnet: Error response from daemon: network with name simple_webnet already exists

提示simple_network已经存在。这是之前建立的,可以使用compose down将其删掉,也可以命令行修改 simple 改成其他名字。本例中删除了旧服务,再次执行:

$ docker stack deploy -c app-stack-compose.yml simple 

Creating network simple_webnet
Creating service simple_database
Creating service simple_app
Creating service simple_gateway

Stack 创建成功,查看Stack 情况:

$ docker stack ls 
NAME      SERVICES   ORCHESTRATOR
simple    3          Swarm

这里的 ORCHESTRATOR 是运行在 Swarm上了。

$ docker stack services simple 

ID             NAME              MODE         REPLICAS   IMAGE           PORTS
9o3hxdm9g2th   simple_app        replicated   1/1        springapp:0.1   *:8080->8080/tcp
7f1l0ktoou3i   simple_database   replicated   1/1        nginx:alpine    *:8002->80/tcp
x3t7xbhn6p55   simple_gateway    replicated   1/1        nginx:alpine    *:8001->80/tcp
# 三个服务:app database gateway。注意,名称是项目名+下划线,而非减号了。

$ docker service ls 

ID             NAME              MODE         REPLICAS   IMAGE           PORTS
9o3hxdm9g2th   simple_app        replicated   1/1        springapp:0.1   *:8080->8080/tcp
7f1l0ktoou3i   simple_database   replicated   1/1        nginx:alpine    *:8002->80/tcp
x3t7xbhn6p55   simple_gateway    replicated   1/1        nginx:alpine    *:8001->80/tcp

可见,这三个服务都已经成为Swarm service。同样可以将其当作 service 来单独管理,比如:

$ docker service scale simple_app=2

simple_app scaled to 2
overall progress: 2 out of 2 tasks 
1/2: running   
2/2: running   
verify: Service converged 

3.3.2 Stack 多副本部署

还记得创建服务时 service create --replicas 2 这个指定副本数的参数吗? stack compose文件同样支持该定义。

副本数是在服务部署阶段的属性,因此,在app-stack-compose.yml中添加:

services:
  app:
...
    deploy:
      replicas: 3

重新部署stack simple:

$ docker stack deploy -c app-stack-compose.yml simple 

Updating service simple_app (id: 9o3hxdm9g2thms5mmcjkg7zlc)

image springapp:0.1 could not be accessed on a registry to record
its digest. Each node will access springapp:0.1 independently,
possibly leading to different nodes running different
versions of the image.

Updating service simple_gateway (id: x3t7xbhn6p55l7d038gd0n8i4)
Updating service simple_database (id: 7f1l0ktoou3i45hu7vqmqi6li)

$ docker stack services simple 

ID             NAME              MODE         REPLICAS   IMAGE           PORTS
9o3hxdm9g2th   simple_app        replicated   3/3        springapp:0.1   *:8080->8080/tcp
7f1l0ktoou3i   simple_database   replicated   1/1        nginx:alpine    *:8002->80/tcp
x3t7xbhn6p55   simple_gateway    replicated   1/1        nginx:alpine    *:8001->80/tcp

可见副本数已经变成三个了。

3.3.3 Docker Registry

注意到Stack simple 部署时出现警告信息:

image springapp:0.1 could not be accessed on a registry to record
its digest. Each node will access springapp:0.1 independently,
possibly leading to different nodes running different
versions of the image.

由于之前构造 springapp:0.1的镜像时,仅仅在一台宿主机上执行了,这个镜像仅保留在本地,而没有保存在一个镜像服务器上,因此,swarm 警告可能出现各个宿主机部署的副本失败或者版本不一致的情况,。

解决方法是,将镜像上传到Docker hub 上(默认的docker镜像源)。或者,搭建独立的镜像源。

简单起见,使用 docker 来搭建镜像源。

$ docker service create --name registry -p 5000:5000 --env registry:2
hyh770abqu4p9jsl402hvl0eh
overall progress: 1 out of 1 tasks 
1/1: running   
verify: Service converged 

将Registry地址加入每个宿主机的docker配置文件:

$ sudo vim /etc/docker/daemon.json
{
  "insecure-registries": ["172.24.162.101:5000"]
}
$ sudo service restart docker 

将springapp0.1 推送至 Registry:

$ docker image tag springapp:0.1 172.24.162.101:5000/springapp:0.1
# 使用Registry地址定义 标签

# 再推送
$ docker image push 172.24.162.101:5000/springapp:0.1
The push refers to repository [172.24.162.101:5000/springapp]
2565208d46c3: Pushed 
8ac53a700e8d: Pushed 
e5e13b0c77cb: Pushed 
0.1: digest: sha256:d172f52ac4856d8a17766c61e03c3ddedfbe8288bd779ff5680d7c52328dc822 size: 952

这之后,修改 compose文件将镜像指向 Registry :

$ docker stack deploy -c app-stack-compose.yml simple 

Updating service simple_app (id: 9o3hxdm9g2thms5mmcjkg7zlc)
Updating service simple_gateway (id: x3t7xbhn6p55l7d038gd0n8i4)
Updating service simple_database (id: 7f1l0ktoou3i45hu7vqmqi6li)

$ docker stack services simple 
ID             NAME              MODE         REPLICAS   IMAGE                               PORTS
9o3hxdm9g2th   simple_app   replicated   3/3  172.24.162.101:5000/springapp:0.1   *:8080->8080/tcp
7f1l0ktoou3i   simple_database  replicated   1/1        nginx:alpine                        *:8002->80/tcp
x3t7xbhn6p55   simple_gateway    replicated   1/1        nginx:alpine                        *:8001->80/tcp

可见已经应用了registry。

4 存储与持久化

Docker Container自身会有一套文件系统,这些文件会保存在Container文件中,直至Container被删除。这些文件是Container私有的文件,同时也无法使用特定的文件系统,比如NFS、SSD独立存储。

因此,Docker 提供了 Volume 并可使用不同的 Volume Driver 将存储挂载在 Conatiner。

4.1 Docker 文件系统

Docker Container 使用共享文件系统,这种文件系统的特点是分层管理,从Docker Image 构建到Container,延续这一做法,这种做法的优点显而易见: 共享 - 相同层的文件可以在不同的容器中共享而不需要多份copy。

目前Docker主要使用 Overlay2 文件系统。

在Conatiner中,可以创建,修改,删除文件,这些变化,会被保存在 Container的文件系统中。

# 创建一个 test 容器
$ docker run --name test -d alpine 
# 创建测试文件
$ docker exec -it test 
/ mkdir /test 
/  echo "This is a test file." > /test/a.txt 
/ exit 

那么,这个新文件 /test/a.txt 是如何保存的呢?

使用 inspect 可查看Conatiner的文件系统:

$ docker inspect -f "{{json .GraphDriver }}" test  | jq
{
  "Data": {
    "LowerDir": "/var/lib/docker/overlay2/8f84ecf7c69260d5e35095a4f5bf126253a54d1103f933f37080c42ab6a9e0ad-init/diff:/var/lib/docker/overlay2/818ae2194f691685aaa0f48602608607c749186de6106b3263455d6a4dae0871/diff",
    "MergedDir": "/var/lib/docker/overlay2/8f84ecf7c69260d5e35095a4f5bf126253a54d1103f933f37080c42ab6a9e0ad/merged",
    "UpperDir": "/var/lib/docker/overlay2/8f84ecf7c69260d5e35095a4f5bf126253a54d1103f933f37080c42ab6a9e0ad/diff",
    "WorkDir": "/var/lib/docker/overlay2/8f84ecf7c69260d5e35095a4f5bf126253a54d1103f933f37080c42ab6a9e0ad/work"
  },
  "Name": "overlay2"
}

Name overlay2表示容器使用了 overlay2 文件系统。Data 包含4个目录:

  • LowerDir : 底层镜像的文件路径。
  • UpperDir: 顶层镜像文件路径,顶层镜像即容器创建的文件。
  • WorkDir: 临时文件目录。当文件发生修改时,会暂存在这里。通常该目录是空的。
  • MergedDir: 顾名思义,是Lower + Upper 合并后的文件路径。

jq 是一个格式化JSON输出的命令,可通过 yum/apt install jq 来安装。

查看一下这几个目录内容;

$ sudo tree /var/lib/docker/overlay2/8f84ecf7c69260d5e35095a4f5bf126253a54d1103f93
3f37080c42ab6a9e0ad/diff

/var/lib/docker/overlay2/8f84ecf7c69260d5e35095a4f5bf126253a54d1103f933f37080c42ab6a9e0ad/diff
└── test
    └── a.txt 

$ sudo ls /var/lib/docker/overlay2/8f84ecf7c69260d5e35095a4f5bf126253a54d1103f933f
37080c42ab6a9e0ad/merged

bin  dev  etc  home  lib  media  mnt  opt  proc  root  run  sbin  srv  sys  test  tmp  usr  var

UpperDir 仅包含 容器内新创建的文件。而MergedDir包含LowerDir(镜像)中的目录和UpperDir 的内容。

4.2 Volumes

Docker 可以在容器中挂载宿主机的目录,类似于Linux 的 mount 命令,这样,容器可以访问宿主机文件,而不必将文件复制到容器内。

4.2.1 Bind Volume

最简单的用法是直接将宿主机任意目录挂载到容器, 作为测试,建立一个临时目录/tmp/html/并在其中放置一个index.html,内容可自行编辑。将该文件做为docker nginx 的 html 目录发布。

$ docker run -d --name html-test -v /tmp/html/:/usr/share/nginx/html -p 8010:80 nginx:alpine
47403d811844aa34d91c9821a7ce9af630be9f9ae3aad750210ea2351f5fc498

$ curl localhost:8010
<html>
        <body>
                <p> This is a html on HOST. </p>
        </body>
</html>

$ docker inspect -f "{{json  .Mounts}}" html-test |jq
[
  {
    "Type": "bind",
    "Source": "/tmp/html",
    "Destination": "/usr/share/nginx/html",
    "Mode": "",
    "RW": true,
    "Propagation": "rprivate"
  }
]
$ docker rm html-test -f 

上述命令中指定了 -v 来挂载卷:

  • -v 宿主机目录:容器目录

在inspect 中也可以看见,该Volume 的类型type为bind。源和目的都于 -v 命令一致。

可以在compose 中使用该选项,如:

services:
  webserver:
    image: nginx/alpine 
    volumes:
      - "/tmp/html/:/usr/share/nginx/html"

那么,如果在Swarm集群中使用呢? 需要确保每个宿主机上都有这个/tmp/html/目录,并确保其内容相同。

这样,就带来一个问题:如何确保这些集群中的文件同步?

4.2.2 Docker管理的Volume

Bind Volume是由宿主机系统管理的目录,完全依赖于宿主机。Docker 提供了更具扩展性的Volume机制。

考虑下列场景:

  • 外部存储供容器使用
  • 分布式文件系统用于集群共享

Docker 使用 Volume Driver 机制,不同存储系统实现该机制后,即可供Docker使用。

最简单的 Volume Driver 是 Local, 也即使用本地文件系统作为存储。

下面使用 docker volume 命令来建立Volume:

$ docker volume create local_vol
$ docker volume ls
DRIVER    VOLUME NAME
local     local_vol 

将其挂载到容器:

$ docker run -d --name test -v local_vol:/test alpine
17601ef3eb707ef5017a6b95685af1ab45a2553e99fe03d5aa063e1f2cbe63f7
$ docker inspect -f "{{ json .Mounts  }}" test |jq
[
  {
    "Type": "volume", 
    "Name": "local_vol",
    "Source": "/var/lib/docker/volumes/local_vol/_data",
    "Destination": "/test",
    "Driver": "local",
    "Mode": "z",
    "RW": true,
    "Propagation": ""
  }
]

可见,Driver local 的 Volume, 其文件保存在 /var/lib/docker/volumes/{volume name}/_data目录下.

使用 volume inspect 也能看到同样的内容:

$ docker volume inspect local_vol
[
    {
        "CreatedAt": "2022-11-26T11:46:51+08:00",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/local_vol/_data",
        "Name": "local_vol",
        "Options": {},
        "Scope": "local"
    }
]

4.2.3 Volume Driver

除了 local 之外, Docker 还可以使用 很多主流的 分布式文件系统

可以在官网查看: https://docs.docker.com/engine/extend/legacy_plugins/#volume-plugins

比如可以用挂载 NFS 文件系统来支持集群内的文件共享

4.3 Docker Logging

程序日志是很重要的信息,日志监控、分析也是大型应用系统运营维护、业务分析 的重要内容。在Swarm集群模式下,如何方便的管理日志呢?

4.3.1 docker log

考虑容器的目标是独立运行单一应用程序,因此,大部分镜像设计时均已经日志信息通过 stdout/stderr进行输出,而Docker将这两类标准输出内容视为日志进行管理。

使用 docker log 命令可以看到这一效果:

$ docker rm -f test 
# 启动alpine并执行 ping 
$ docker run --name test  -d alpine ping localhost
$ docker logs -tf  test
2022-11-26T04:44:41.916111200Z PING localhost (127.0.0.1): 56 data bytes
2022-11-26T04:44:41.916167700Z 64 bytes from 127.0.0.1: seq=0 ttl=64 time=0.512 ms
2022-11-26T04:44:42.917316100Z 64 bytes from 127.0.0.1: seq=1 ttl=64 time=0.134 ms
2022-11-26T04:44:43.916988000Z 64 bytes from 127.0.0.1: seq=2 ttl=64 time=0.051 ms
2022-11-26T04:44:44.916987100Z 64 bytes from 127.0.0.1: seq=3 ttl=64 time=0.077 ms
2022-11-26T04:44:45.918192900Z 64 bytes from 127.0.0.1: seq=4 ttl=64 time=0.114 ms
2022-11-26T04:44:46.917766100Z 64 bytes from 127.0.0.1: seq=5 ttl=64 time=0.063 ms

Docker将容器输出日志收集了起来。

  • -t: timestamp显示时间戳。
  • -f: follow持续显示新的日志。

那么,容器日志如何保存呢?

$ docker inspect -f "{{ json .HostConfig.LogConfig }}" test  | jq
{
  "Type": "json-file",
  "Config": {}
}
$ docker inspect -f "{{ json .LogPath }}" test
"/var/lib/docker/containers/0e0d00c675063fc27de1121f474e9348ed9ae1ee43606aace6e173f1197b04a8/0e0d00c675063fc27de1121f474e9348ed9ae1ee43606aace6e17
3f1197b04a8-json.log"

Docker 默认使用 json-file 类型的 日志文件,有三个途径来配置日志:

  • 在 daemon.json 中配置:
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3",
    "labels": "production_status",
    "env": "os,customer"
  }
}

这对所有container生效。

  • 在创建Container时使用 --log-driver 和 --log-opt 进行配置:
$ docker run --name log-test -d \
    --log-driver json-file \
    --log-opt max-size=10m,max-file=3 \
    alpine ping localhost 

也可以在 Compose 文件中填写:

services:
  app:
    logging:
      driver: json-file
      options: 
        max-size: 10m
        max-file: 3
    

4.3.2 使用log-driver plugin

可以使用日志收集系统的 log-driver 比如 journald 或者 fleuntd 等来完成在线日志采集,并可通过这些系统实现日志分析、检索等功能。

Part 5: 制作应用镜像

5.1 使用maven 制作 Spring APP镜像

之前章节介绍过简单的 镜像制作方法,本节通过Maven来进行镜像制作和部署。

spring boot maven plugin 使用 build-image 命令执行镜像制作。

      <plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<executions>
					<execution>
						<goals>
							<!-- goal>repackage</goal -->
							<goal>build-image</goal>
						</goals>
					</execution>
            	</executions>
			</plugin>

简单的将 repackage 命令替换成 build-image 就可以完成镜像构建:

$ mvn package -pl swarm-sca-demo-calc -DskipTests=true 
....
[INFO] --- spring-boot-maven-plugin:2.7.6:repackage (repackage) @ swarm-sca-demo-calc ---
[INFO] Replacing main artifact with repackaged archive
[INFO]
[INFO] <<< spring-boot-maven-plugin:2.7.6:build-image (default) < package @ swarm-sca-demo-calc <<<
[INFO] --- spring-boot-maven-plugin:2.7.6:build-image (default) @ swarm-sca-demo-calc ---
 Building image 'docker.io/library/swarm-sca-demo-calc:0.0.1'
[INFO]
[INFO]  > Pulling builder image 'docker.io/paketobuildpacks/builder:base' 100%
[INFO]  > Pulled builder image 'paketobuildpacks/builder@sha256:eccdad1a81a7a20a90b8199c04a16a6c87c9c5dd94a683c6ee1f956d48d076ed'
[INFO]  > Pulling run image 'docker.io/paketobuildpacks/run:base-cnb' 100%
[INFO]  > Pulled run image 'paketobuildpacks/run@sha256:b578fc198712336ff0d987e80dff7437d0aa3066d3e925dfb25e48f5db05e300'
[INFO]  > Executing lifecycle version v0.15.2
[INFO]  > Using build cache volume 'pack-cache-30d51e3dbcf6.build'
[INFO]
[INFO]  > Running creator
...
[INFO] Successfully built image 'docker.io/library/swarm-sca-demo-calc:0.0.1'
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  01:16 min
[INFO] Finished at: 2022-12-05T10:57:05+08:00

$ docker image ls | grep swarm
swarm-sca-demo-calc             0.0.1      8699851e3485   42 years ago   250MB

可见,构建的镜像名称为 项目名称, tag 为版本号。 可以直接使用该镜像启动。

Swarm环境需要将镜像推送至私有 registry,build-image 也支持该操作,只需在pom.xml中添加Registry 配置即可:

  <properties>
		<docker.registry>http://172.24.162.101:5000</docker.registry>
	</properties>
      <plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
					<docker>
						<publishRegistry>
							<username>test</username>
							<password>test</password>
							<url>${docker.registry}</url>
						</publishRegistry>
					</docker>
					<image>
						<name>${docker.registry}/${project.artifactId}:${project.version}</name>
						<publish>true</publish>
					</image>
				</configuration>
        <executions> ... </executions>
        

为方便起见,定义properties docker.registry指向私库地址。 在plugin中 定义 docker registry的url 和用户名,密码,使用 ${docker.registry}/${project.artifactId}:${project.version} 定义 image.name,并设置 publish true,该镜像将push至registry。 再次执行 package :

$ mvn package -pl swarm-sca-demo-calc -DskipTests=true 
[INFO] Successfully built image '172.24.162.101:5000/swarm-sca-demo-calc:0.0.1'
[INFO]
[INFO]  > Pushing image '172.24.162.101:5000/swarm-sca-demo-calc:0.0.1' 100%
[INFO]  > Pushed image '172.24.162.101:5000/swarm-sca-demo-calc:0.0.1'
[INFO] ------------------------------------------------------------------------

$ docker image ls 
172.24.162.101:5000/swarm-sca-demo-calc   0.0.1      add0d8b26970   42 years ago   250MB

运行该镜像并登入:

$ docker run -d --name calc 172.24.162.101:5000/swarm-sca-demo-calc:0.0.1
043ac76cacdb69c92a84771dec92a20ccf40830fd5e875721732c579d2edc8c0
$ docker exec -it calc bash
cnb@043ac76cacdb:/workspace$ ls -ltr
total 12
drwxr-xr-x 3 cnb cnb 4096 Jan  1  1980 org
drwxr-xr-x 3 cnb cnb 4096 Jan  1  1980 META-INF
drwxr-xr-x 1 cnb cnb 4096 Jan  1  1980 BOOT-INF

可见,这是将jar文件解压后分层构建的镜像。

build-image 还支持启动参数等功能。可参考:https://docs.spring.io/spring-boot/docs/2.7.6/maven-plugin/reference/htmlsingle/

5.2 使用Dockerfile制作镜像

使用maven插件制作镜像虽然方便,但通常部署时需要结合不同环境,大部分配置参数都需要调整,因此,有两种解决方案:

  • 使用 maven 的 profile: 利用不同的 Profile来完成不同环境的配置和发布。
  • 结合Jenkins/Ansible等自动部署工具,实现功能增强的集群自动部署。

以下通过自行构建镜像,来进一步了解Dockfile的构建方法。

5.2.1 回顾

回顾之前的Dockerfile:

# First docker file
# 1. 运行一个alpine操作系统:run ... alpine  
FROM alpine 
# 2. 安装JDK: apk add openjdk8
RUN apk add openjdk8
# 3. 安装jar: docker cp .jar /app/
COPY target/ansible-spring-example1-0.0.1.jar /app/ 
# 声明暴露 8080端口
EXPOSE 8080/tcp
# 4. 进入/app/目录:cd /app/
WORKDIR /app/
# 5. 启动jar: java -jar ...
ENTRYPOINT java -jar ansible-spring-example1-0.0.1.jar

这里使用 alpine 操作系统,安装openjdk8, 再将jar包复制进去,最后使用java -jar 命令启动。

每个spring jar 都需要上述步骤,是否能简化 Dockerfile 的制作呢?比如:存在一个基准镜像,其他应用仅使用不同的jar即可。

5.2.2 使用参数ARG/ENV

最简单的思路是,将Dockerfile中的jar文件名提取并参数化,使用ARG指令来定义参数:

...
ARG app_name
ARG app_version
ENV APP_JAR_NAME=$app_name-$app_version.jar
COPY $app_name/target/$APP_JAR_NAME /app/
...
ENTRYPOINT java -jar $APP_JAR_NAME

上文还出现了ENV指令,这是因为ENTRYPOINT 指令不会进行变量替换,因此,需要使用 ENV 来定义环境变量,并在启动脚本中替换。

注意:

之所以使用ARG是因为它可以在Build过程中使用,并且方便在命令行指定参数值,结合Compose时就需要使用ENV了。

这里使用 $app_name/target/作为jar文件目录,是为了方便在父项目目录自行构建命令。

使用 docker build 构建镜像:

$  docker build -f ./app-base1.Dockerfile \
  -t test:latest \
  --build-arg app_name=swarm-sca-demo-calc \
  --build-arg app_version=0.0.1 .
Sending build context to Docker daemon  88.98MB
Step 1/9 : FROM alpine
...
Successfully built 0f5217561a13
Successfully tagged test:latest
$ docker run test
 .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.7.6)
 2022-12-05 06:24:31.165  INFO 1 --- [           main] j.f.o.s.d.c.SwarmScaDemoCalcApplication  : Started SwarmScaDemoCalcApplication in 18.729 seconds (JVM running for 20.514)

同样,可以使用该Dockerfile构建其他应用, 可自行实验。

5.2.3 添加其他参数

Spring应用运行时通常还需要使用特定的JVM参数,如内存配置,GC配置等。因此,增加一个参数JVM_OPTS。

ENV JVM_OPTS=
...
ENTRYPOINT java $JVM_OPTS -jar $APP_JAR_NAME

Docker的原则是一个容器仅运行一个服务,因此,Spring应用的端口号可以固定,方便使用:

ENV SERVER_PORT=8080 
EXPOSE $SERVER_PORT 
...
ENTRYPOINT java $JVM_OPTS -jar $APP_JAR_NAME --server.port=$SERVER_PORT

使用 docker build 构建镜像:

$  docker build -f ./app-base2.Dockerfile \
  -t test:latest \
  --build-arg app_name=swarm-sca-demo-calc \
  --build-arg app_version=0.0.1 .
Sending build context to Docker daemon  88.98MB
Step 1/9 : FROM alpine
...
Successfully built 0f5217561a13
Successfully tagged test:latest
$ docker run test
2022-12-05 07:42:09.224  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-12-05 07:42:09.277  INFO 1 --- [           main] c.a.c.n.registry.NacosServiceRegistry    : nacos registry, DEFAULT_GROUP service-calc 172.17.0.5:8080 register finished
2022-12-05 07:42:09.316  INFO 1 --- [           main] j.f.o.s.d.c.SwarmScaDemoCalcApplication  : Started SwarmScaDemoCalcApplication in 16.987 seconds (JVM running for 20.483)

注意其端口已修改为 8080。

5.2.4 使用HEALTHCHECK

demo 应用中集成了 actuator ,可以通过该功能来检查Spring应用是否健康,

通过 health probes 来检测服务是否可用,是否健康状态。

ENV APP_HEALTH_URI=actuator/health/liveness

# 10秒后开始检查,每15s检查一次,5s超时失败,尝试5次即认为状态不健康。
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=5 \
  CMD curl --fail http://localhost:$SERVER_PORT/$APP_HEALTH_URI || exit 1

为使用 curl,还需在镜像中安装 curl:

RUN apk add openjdk8 curl

为测试该功能,配置 JVM_OPTS 打开 DEBUG开关:

$ docker build -f ./app-base2.Dockerfile \
  -t test:latest \
  --build-arg app_name=swarm-sca-demo-calc \
  --build-arg app_version=0.0.1 .
  ....
Successfully tagged test:latest
$ docker run --rm --env JVM_OPTS=-DDEBUG=true test
...

2022-12-05 08:54:24.287 DEBUG 1 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : GET "/actuator/health/liveness", parameters={}
2022-12-05 08:54:24.484 DEBUG 1 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed 200 OK

可见,health url 被执行了。 使用 docker ps 参看容器状态,已更改为: Up About a minute (healthy)


将probes配置去除,来模拟不健康的状态:
```yaml
# application.yml
management.health:
  probes:
    enabled: false

重新构建镜像并执行:

2022-12-05 08:45:28.383 DEBUG 1 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : GET "/actuator/health/liveness", parameters={}
2022-12-05 08:45:28.499 DEBUG 1 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed 404 NOT_FOUND

如此重复5次后,容器状态变为:Up About a minute (unhealthy)。

5.2.5 分层镜像

Docker 镜像按照指令分层构建,每一层都会产生缓存,如果内容没变化,则不需要重复构建。而相同内容的层次,在不同镜像之间也可以共享。

Spring Boot 应用中大量的依赖jar包是通用的,因此,将其分层,也可以提高缓存利用率,提高构建速度,减小容器存储。

考察现有的镜像:

$ docker image inspect -f "{{ json .RootFS }}" test:latest | jq
{
  "Type": "layers",
  "Layers": [
    "sha256:e5e13b0c77cbb769548077189c3da2f0a764ceca06af49d8d558e759f5c232bd",
    "sha256:75843c4e15a95c0e5c19f995f9f2c821965096812fc30cd5b9f26ea46cc6a6b6",
    "sha256:babd8e73ca6e377b5efa30b280cf173f6b8ea8078e5669ac49e0d3e5d9d89d40"
  ]
}

可见其分为三层,分别对应着 FROM, RUN apk addCOPY

Springboot推荐使用 layer tools 实现分层。

参见:https://docs.spring.io/spring-boot/docs/2.7.6/reference/html/container-images.html

# app-layer.Dockerfile 
# 第一个alpine别名为builder
FROM alpine as builder
...

# fat jar 复制到 tmp目录
COPY $app_name/target/$APP_JAR_NAME /app/tmp/

WORKDIR /app/tmp/
# 使用layertools解压
RUN java -Djarmode=layertools -jar $APP_JAR_NAME extract

# 新建一个基础镜像
FROM alpine
RUN apk add openjdk8 curl

WORKDIR /app/
# 分层复制各支持jar和主程序
COPY --from=builder /app/tmp/dependencies/ /app/
COPY --from=builder /app/tmp/spring-boot-loader/ /app/
COPY --from=builder /app/tmp/snapshot-dependencies/ /app/
COPY --from=builder /app/tmp/application/ /app/

...
ENTRYPOINT java $JVM_OPTS org.springframework.boot.loader.JarLauncher --server.port=$SERVER_PORT

可以使用app-layer.Dockerfile 构建新镜像。

5.3 使用ansible构建、发布镜像

Ansible的使用参考 https://gitee.com/jeffwang78/ansible-spring-application

5.3.1 回顾 app_meta_config.yml

在上面的笔记中,使用 app_meta_config.yml 来定义spring app 的信息:

app_meta_list:
  - name: ansible-spring-example1 
    path: ../ansible-spring-example1/
    version: 0.0.1
    install_path: /tmp
    jvm_args: 
      - -Xms128M
      - -Xmx256M
  - name: example2 
    path: ../example2/
    version: 0.0.1
    install_path: /tmp
    jvm_args: 
      - -Xms64M
      - -Xmx128M 

其中的 name, path, version, jvm_args都是需要使用的信息。

使用Ansible 建立 docker_app role,这部分引用之前的 app_meta_config.yml

5.3.1 定义Ansible Role

整个构建发布过程包括:

  • git 下载源代码: 这一步骤通常在Jenkins里进行.
  • 执行mmvn package:这一步也可在Jenkins完成,因为这样可以集成Junit Test report.
  • 执行docker build
  • 执行docker push

执行docker命令时,使用community.docker

建立 roles/docker_app/tasks/main.yml

...
- name: build {{ app_meta.name }}:{{ app_meta.version }} and push it to private registry
  community.docker.docker_image:
    state: present 
    build:
      path: "{{ app_meta.path }}"
      dockerfile: app-layer.Dockerfile 
      args: 
        app_name: "{{ app_meta.name }}"
        app_version: "{{ app_meta.version }}"
        jvm_opts: "{{ app_meta.jvm_args | default (['']) | join (' ') }}"
    name: "{{ docker_registry }}/{{ app_meta.name }}"
    tag: 
      - v{{ app_meta.version }}
      - latest
    force_tag: true 
    push: true
    source: build

在部署的Playbook中, 调用role docker_app

- hosts: localhost
  gather_facts: no
  vars_files:
    - vars/app_meta_config.yml 
  tasks: 
    - name: call to role docker_app 
      include_role:
        name: docker_app 
      vars: 
        app_meta: "{{ list_item }}"
      with_list: "{{ app_meta_list }}"
      loop_control:
        loop_var: list_item

5.3 使用 compose 文件构建

Docker compose 文件可以定义镜像构建信息。并使用 compose build 命令执行构建。

5.3.1 构建及发布示例

编制一个app-stack-compose.yaml

# name: test 
version: "3.9"
services:
  app:
    image: 172.24.162.101:5000/demo/swarm-sca-demo-calc 
    build:
      context: ./
      dockerfile: app-layer-Dockerfile
      tags: 
        - v0.0.1
        - latest 
      args:
        app_name: "swarm-sca-demo-calc"
        app_version: "0.0.1"
        jvm_opts: "-Xms64M -Xmx128M"
  
    networks:
      - webnet
    ports:
      - "8080:8080"
    deploy:
      replicas: 2
networks:
  webnet: 
    driver: overlay

注意:

dockerfile 的路径是相对于 context 目录的。

使用compose build执行构建:

$ docker compose -f ./app-stack-build-compose.yml build
[+] Building 0.7s (16/16) FINISHED
 => [internal] load build definition from app-layer-Dockerfile                                                       0.1s
 => exporting to image                                                      0.1s
 => => exporting layers                                                     0.0s
 => => writing image sha256:bacdc108ac819b15950df636e7e33364396281a28c422046025be26cf49ffff2    0.0s
 => => naming to 172.24.162.101:5000/demo/swarm-sca-demo-calc               0.0s
 => => naming to docker.io/library/v0.0.1                                   0.0s
 => => naming to docker.io/library/latest                                   0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

使用 image ls 查看构建后的image:

 docker image ls
REPOSITORY                                     TAG       IMAGE ID       CREATED          SIZE
172.24.162.101:5000/demo/swarm-sca-demo-calc   latest    bacdc108ac81   12 minutes ago   171MB
172.24.162.101:5000/demo/swarm-sca-demo-calc   v0.0.1    bacdc108ac81   12 minutes ago   171MB

可见,创建了一个镜像,两个tag(latest和v0.0.1)

将镜像push到registry:

$ docker compose -f ./app-stack-build-compose.yml push
...

使用curl查看registry中是否推送成功:

# 查看repository
$ curl localhost:5000/v2/_catalog | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    68  100    68    0     0   8500      0 --:--:-- --:--:-- --:--:--  8500
{
  "repositories": [
    "demo/swarm-sca-demo-calc",
  ]
}
# 查看 tags
$ curl localhost:5000/v2/demo/swarm-sca-demo-calc/tags/list  | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    63  100    63    0     0   4500      0 --:--:-- --:--:-- --:--:--  4500
{
  "name": "demo/swarm-sca-demo-calc",
  "tags": [
    "latest",
    "v0.0.1"
  ]
}

可见已经推送成功。

5.3.2 stack 部署

上述compose文件虽然可以顺利构建、发布,但用于stack deploy是不行的。

$ docker stack deploy -c app-stack-build-compose.yml test
services.app.build Additional property tags is not allowed

这是因为,stack 和 compose 并不完全兼容。

最简单的解决方案是将 tags去掉,tag 放在 image名后面:

services:
  app:
    image: 172.24.162.101:5000/demo/swarm-sca-demo-calc:v0.0.1 

但这样就没有 Latest 镜像了。而且当版本号变化时,需要手动修改 compose文件。

另一个问题是将 build使用的 compose文件和stack deploy文件分开。

如果使用ansible,那么这个问题就可以解决,在 stack 的 compose 文件中,不再需要包含 build信息,而Image版本号也可以直接写成 latest或忽略。如:

services:
  app:
    image: 172.24.162.101:5000/demo/swarm-sca-demo-calc 

推荐使用ansbile,因为其部署构建能力灵活而又方便。

本例使用最简方案,将修改后的文件重新进行部署:

$ docker stack deploy -c app-stack-build-compose2.yml test
Ignoring unsupported options: build

Creating network test_webnet
Creating service test_app

$ docker stack services test 
ID             NAME       MODE         REPLICAS   IMAGE                                                 PORTS
w4y521fbi0b3   test_app   replicated   2/2        172.24.162.101:5000/demo/swarm-sca-demo-calc:v0.0.1   *:8080->8080/tcp

两个副本已启动。

5.3.3 Ansible 生成 compose 文件

可以使用 Ansible 来自动生成 compose 文件。如下的 j2 模板:

version: "3.9"
services:
{% for app_meta in app_meta_list %}
  app:
    image: {{ docker_registry }}/{{ project_name }}/{{ app_meta.name }}:v{{ app_meta.version }}
    build:
      context: ./
      dockerfile: app-layer.Dockerfile
      # tags: 
      #   - v{{ app_meta.version }}
      #   - latest 
      args:
        app_name: {{ app_meta.name }}
        app_version: {{ app_meta.version }}
        jvm_opts: "{{ app_meta.jvm_args | default (['']) | join (' ') }}"
    networks:
      - webnet
    ports:
      - "{{ app_meta.http_port }}:8080"
    deploy:
      replicas: {{ app_meta.replicas }}
{% end for %}

Part 6: SpringCloud Alibaba 环境部署

SpringCloud Alibaba采用下列组件:

  • Nacos:注册中心及配置中心。
  • Dubbo: RPC服务框架。
  • Sentinel: 断路器。
  • RocketMQ: 高吞吐量消息队列框架
  • Seata: 分布式事务框架

本Demo还使用OpenFeign执行REST服务调用,使用Redis作为分布式存储及简单事务处理。

本章简要介绍如何在SWARM搭建运行环境。

6.1 Nacos 部署

Nacos 在生产环境应使用集群部署,Nacos还需要MySQL数据库的支持。

个人认为需要外接数据库是Nacos部署的一个弊端。

6.1.1 Nacos + MySql镜像

Nacos官方提供了可用的镜像,nacos/nacos-server:v2.1.2

可访问:https://github.com/nacos-group/nacos-docker/ 获取 dockerfile和样例。

如使用单机版standalone模式运行,则只需要直接运行 :

$ docker run --name nacos-quick -e MODE=standalone -p 8849:8848 -d nacos/nacos-server

此时 nacos 使用本地数据库。

集群部署则需要准备MySQL,官方提供了MySQL镜像 nacos/nacos-mysql 。 镜像包括 5.7 和 8.0 两个版本,本文使用了 5.7 版本。

利用 /etc/mysql/conf.d/ 在启动时完成了nacos数据库的初始化。

6.1.2 Nacos集群

具体脚本在 nacos/nacos-cluster-dc.yml,nacos 服务配置:

services:
  nacos:
    hostname: "node{{ .Task.Slot }}-nacos"
    image: nacos/nacos-server:v2.1.2
    ports:
      - "8848:8848"
    environment:
    #nacos dev env mysql 
      - PREFER_HOST_MODE=hostname
      - NACOS_SERVERS= node1-nacos:8848 node2-nacos:8848 node3-nacos:8848
      - MYSQL_SERVICE_HOST=Tasks.demo_mysql
      - MYSQL_SERVICE_DB_NAME=nacos_devtest
      - MYSQL_SERVICE_PORT=3306
      - MYSQL_SERVICE_USER=nacos
      - MYSQL_SERVICE_PASSWORD=nacos
      - MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8

注意以下几点配置:

  • 使用了自定义的hostname:hostname: "node{{ .Task.Slot }}-nacos"。.Task.Slot 指replica实例的顺序号(从1开始)。
  • 环境变量 NACOS_SERVERS,使用了 hostname:port 形式,罗列三个实例的cluster。
  • MYSQL_SERVER_HOST使用了 服务名: `Tasks.demo_mysql。

nacos 使用的 MYSQL服务配置:

mysql:
    hostname: nacos-mysql
    image: nacos/nacos-mysql:5.7
    environment:
      - MYSQL_ROOT_PASSWORD=root
      - MYSQL_DATABASE=nacos_devtest  
      - MYSQL_USER=nacos
      - MYSQL_PASSWORD=nacos
    volumes:
      - nacos_mysql:/var/lib/mysql  
    deploy:
      replicas: 1

MySQL仅配置一个实例(没有使用主从模式)。 mysql数据需要持久化,不能仅仅保存在 container中,因此,使用 volume nacos_mysql 并将其挂载在 /var/lib/mysql目录下。这样,当容器销毁,重建时,仍能保留数据库数据。

由于Stack不支持 depends_on 附加 health 条件,因此,当nacos启动后,mySQL可能还没启动,第一次启动会失败,但Swarm 会自动重新启动新的容器。 启动成功后,使用浏览器登录 nacos ,查看 集群配置,可见有3个集群,运行正常。

6.1.3 Nacos客户端配置

Swarm 的 APP 配置nacos服务器,可直接使用 Tasks.demo_nacos ;

spring.cloud.nacos:
    discovery:
    server-addr: Tasks.demo_nacos:8848
    namespace: nacos-test

可在nacos界面上看到相应服务注册的状态。

注意:

需要手动在nacos中创建相应的名空间。否则界面上无法看到服务。

名空间信息保存在MySQL数据库中,如不使用 volume,则数据不会保留,可以将mysql 的 volume 注释掉,重新部署stack,会发现,界面中的 名空间列表已经没有 nacos-test

6.1.4 Swarm 服务状态管理

Swarm 会监视个服务实例的状态,并可配置相应的重启动策略。

这种机制类似于dockerfile中定义的 HEALTHCHCEK。

nacos 提供 open API: /nacos/v1/ns/operator/metrics 来探查健康状态。

bash
$ curl -X GET '127.0.0.1:8848/nacos/v1/ns/operator/metrics'
{"status":"UP"}

当 nacos 集群只有一个节点存活时,该接口会返回 503 错误:

$ curl -v -X GET '127.0.0.1:8848/nacos/v1/ns/operator/metrics'
Note: Unnecessary use of -X or --request, GET is already inferred.
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8848 (#0)
> GET /nacos/v1/ns/operator/metrics HTTP/1.1
> Host: 127.0.0.1:8848
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 503
< Content-Length: 87
< Date: Wed, 07 Dec 2022 01:14:55 GMT
< Connection: close
<
* Closing connection 0
server is DOWNnow, detailed error message: Optional[Distro protocol is not initialized]

因此,可以勉强作为一个健康检查的接口。

与 Dockerfile 健康检查类似,同样可以采用命令,间隔时间,重试次数等参数进行配置。 首先通过 service update 命令来验证:

$ docker service update -d --health-cmd \
  "curl -s --fail http://localhost:8848/nacos/v1/ns/operator/metrics || exit 1"  \
  --health-start-period 30s \
  --health-timeout 5s \
  --health-interval 30s \
  --health-retries	6 \
  demo_nacos 

等待一段时间后,检查是否执行了 健康检查 命令

$ docker ps 
CONTAINER ID   IMAGE                                                 COMMAND                  CREATED         STATUS                   PORTS                 NAMES
cc3e1a8a7714   nacos/nacos-server:v2.1.2                             "bin/docker-startup.…"   5 minutes ago   Up 4 minutes (healthy)   8848/tcp              demo_nacos.1.dic225xswuxb6hqhipx0z1w8u
$ docker inspect -f '{{ json .State.Health }}' cc3e1a8a7714  | jq
{
  "Status": "healthy",
  "FailingStreak": 0,
  "Log": [
    {
      "Start": "2022-12-07T10:40:12.2908689+08:00",
      "End": "2022-12-07T10:40:12.4796198+08:00",
      "ExitCode": 1,
      "Output": ""
    },
    {
      "Start": "2022-12-07T10:40:42.5123139+08:00",
      "End": "2022-12-07T10:40:42.666261+08:00",
      "ExitCode": 0,
      "Output": "{\"status\":\"UP\"}"
    }
  ]
}

在 Compose文件中 也可以定义健康检查:

...
    healthcheck:
      test: "curl -s --fail http://localhost:8848/nacos/v1/ns/operator/metrics || exit 1"
      start_period: 30s
      interval: 30s
      timeout: 5s 
      retries: 6

Swarm也可以依赖健康情况重启动任务容器。这在 compose中使用 deploy.restart_policy 定义:

    ...
    deploy:
      restart:
        condition: on-failure
        delay: 15s
        max_attempts: 3 

这些配置同样可直接用于命令行,形为 --restart-condition/ delay/ max-appempts

6.2 Sentinel 部署

Sentinel 的运行核心是在 Sentinel client 上,每个部署在Spring 应用内部的 client 通过 拦截器模式,完成限流操作。因此,实际运行时,Sentinel并不需要控制台。

但alibaba仍提供了Sentinel 控制台,其作用是完成拦截规则的可视化配置,实现可视化监视。

Sentinel 的配置信息极度依赖于 Nacos 配置中心,Sentinel 控制台、Nacos、Spring 客户端形成了一个很奇妙的关系:

  • 你可以在Sentinel 控制台可视化配置断路器参数,该参数会立即推送至客户端,但,该信息只存在于内存中,没有持久化,当控制台和Spring客户端重启后,配置会消失。
  • 可以通过客户端连接Nacos,dashboard可将配置推送到 Nacos。 要考虑Nacos的配置合并问题。

6.2.1 Sentinel Dashboard 部署

Dashboard尚不支持集群部署,可选的方案是使用 外部HA 。但HA在Docker环境运行并不恰当。因此,暂时仅部署单机版本。

Dashboard安装很简单,官方未发布镜像,但提供了Dockerfile。由于它是一个简单Jar包,因此,也可以使用 之前的 app-layer.Dockerfile 进行构建。这里将使用官方Dockerfile(保存在 sentinel/Dockerfile):

# 略去下载部分
...
FROM openjdk:8-jre-slim

# copy sentinel jar
COPY --from=installer ["/home/sentinel-dashboard.jar", "/home/sentinel-dashboard.jar"]

ENV JAVA_OPTS '-Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080'

EXPOSE 8080

CMD java ${JAVA_OPTS} -jar /home/sentinel-dashboard.jar

可见,Dashboard 默认暴露 8080端口。

也可通过 -Dserver.port=8080 指定新的端口号,但对于 Docker 来讲,这样没什么意义。 -Dcsp.sentinel.dashboard.server=localhost:8080 是将 Dashboard也接入到 Dashboard

制作镜像并启动:

# 由于从 github下载jar,速度会慢一些。
$  docker image build sentinel/ -t sentinel:1.8.6
Successfully built bf6871d240d5
Successfully tagged sentinel:1.8.6
$ docker image tag sentinel:1.8.6  172.21.102.245:5000/sentinel:1.8.6
$ docker image ls
REPOSITORY                                     TAG           IMAGE ID       CREATED          SIZE
172.21.102.245:5000/sentinel                   1.8.6         bf6871d240d5   11 minutes ago   240MB
sentinel                                       1.8.6         bf6871d240d5   11 minutes ago   240MB
# 推送到本地 Registry
$ docker image push  172.21.102.245:5000/sentinel:1.8.6

使用 docker run 启动即可:

$ docker run  --name sentinel-dashboard -d -p 9090:8080 sentinel:1.8.6

浏览器访问 9090即可见 登录界面,默认用户名密码为: sentinel/sentinel。

alt sentinel dashboard default

可见Sentinel Dashboard已经将自身作为一个客户端注册了。如需取消该功能,需要修改 dockfile。当然,也可以将Dockerfile重写,使其更方便使用。但Sentinel控制台可以使用环境变量进行配置,因此,可以在Compose文件中使用enviroment来配置。如:

services:
  sentinel_dashboard:
    image: sentinel:1.8.6 
    enviroment:
    # 用户名
      sentinel_dashboard_auth_username: sentinel
    # 密码
      sentinel_dashboard_auth_password: 12345678      

Sentinel断路器配置使用将在下文APP部分中加以介绍。

6.3 Seata 部署

TODO

6.4 RokectMQ 部署

TODO

6.5 Redis 部署

TODO

6.6 支持性应用

除了必要的应用外,还需要其他监视、管理类应用方能更好的运行整个应用系统:

  • portainer:用于可视化操作swarm集群。
  • docker-registry-frontend:Docker Registry管理界面。
  • Skywalking: 用于微服务链路跟踪。
  • Loki: Docker日志采集。
  • Prometheus:用作系统监控以及TSDB。
  • Granfana: 用于自定义监控界面。
  • ELK:同样是日志收集及监视系统。

6.6.1 Portainer

安装 Portainer Community Edition,使用镜像 portainer/portainer-ce 。

$ docker run --name portainer-ce -d -p 9000:9000 \
  -v "/var/run/docker.sock:/var/run/docker.sock"
  -v "/var/lib/portainer:/data" \
  --restart always \
  portainer/portainer-ce
6da17148aa7d4a4e4205dd123dbf48328a34c3046c82bc0b73ea43e68bdd047a

$ docker logs portainer-ce
2022/12/07 08:03AM INF github.com/portainer/portainer/api/cmd/portainer/main.go:530 > encryption key file not present | filename=portainer
2022/12/07 08:03AM INF github.com/portainer/portainer/api/cmd/portainer/main.go:549 > proceeding without encryption key |
2022/12/07 08:03AM INF github.com/portainer/portainer/api/database/boltdb/db.go:124 > loading PortainerDB | filename=portainer.db
2022/12/07 08:03:46 server: Reverse tunnelling enabled
2022/12/07 08:03:46 server: Fingerprint f9:a3:f3:e1:14:6e:22:38:1a:77:9b:8d:6b:3e:02:ee
2022/12/07 08:03:46 server: Listening on 0.0.0.0:8000...
2022/12/07 08:03AM INF github.com/portainer/portainer/api/cmd/portainer/main.go:789 > starting Portainer | build_number=25294 go_version=1.19.3 image_tag=linux-amd64-2.16.2 nodejs_version=18.12.1 version=2.16.2 webpack_version=5.68.0 yarn_version=1.22.19
2022/12/07 08:03AM INF github.com/portainer/portainer/api/http/server.go:337 > starting HTTPS server | bind_address=:9443
2022/12/07 08:03AM INF github.com/portainer/portainer/api/http/server.go:322 > starting HTTP server | bind_address=:9000

在这里仅开放了 http 9000端口,如果需要使用 https,需要开放9443端口。

服务已启动,使用浏览器即可访问,选择默认的 local 环境即可查看到当前的 service 等信息。

alt portainer-ce UI

在portainer中可以创建、管理container/service/stack。

6.6.2 Docker Registry forntend

fornt-end 可以使用UI界面来浏览registry信息。

docker run --name registry-frontend -d \
  -p 9080:80 \
  --env ENV_DOCKER_REGISTRY_HOST=172.24.162.101 \
  --env ENV_DOCKER_REGISTRY_PORT=5000 \
  --restart always \
  konradkleine/docker-registry-frontend:v2

其中,需要设置 registry 服务地址,使用两个环境变量HOST和PORT进行设置。

实践中可以将这registry和 frontend 使用一个compose文件一起启动。

6.7 网络规划

微服务整个系统部署时,通常划分为三个层次,五个区:

  • 底层基础设施:如:数据库,MQ,ES等。这部分有可能在多个系统间共享。
  • 中层业务应用:部署本系统所需的 APP。这里又分成三部分:
    • 应用区:应用程序及其基础运行环境。
    • 服务治理区:各类配置(Nacos)、监控(Promethes等),
    • DevOps: 按照DevOps理念,还会有持续集成类的应用比如Jenkins, GitLab 等。
  • 上层接入及安全:一般由代理、交换机防火墙等形成保护。

而应用区又可以划分为前台/Gateway、中台、后台,或按照业务领域水平再扩展多个模块。

按照项目的结构,划分网络如下:

  • backend_net: 基础设施网。该网络内的服务应单独创建管理。
  • manage_net: 安装各类服务治理工具,以及DevOps系统。
  • app_backend_net: 后台应用服务。
  • app_middle_net : 中台服务。
  • gateway_net: 网关服务。

6.8.2 应用部署

Part 7: Springcloud demo 项目

7.1 简介

Demo 项目逻辑简单,仅用于验证而已。

7.7.1 项目结构

样例应用包含4个项目:

  • service-vars: 在Redis中保存参数值,提供 REST /var/{varName} 存取参数数据。
  • service-fact: 使用 Dubbo 提供阶乘计算接口。
  • service-calc: 提供/plus 接口,进行计算,调用 vars 和 fact。
  • gateway: 应用网关服务。
graph TB 
  subgraph Outer
    ng1[Nginx 1]
    ng2[Nginx 1]
  end 
  ng1 --> gw
  ng2 --> gw
  subgraph App 
    gw[Gateway]
    calc[Service calc]
    vars[Service vars]
    fact[Service fact]
    calc -.-> vars
    calc -.-> fact
    gw -.-> calc 
  end

  subgraph Resource
    db[MySQL]
    redis[Redis]
    mq[Rocket MQ]
  end

  subgraph Management 
    nacos[Nacos Server]
    sd[Sentinel Dashboard]
  end
  subgraph Monitor
    pm[Prometheus]
    gf[Granfana]
  end

  calc --> redis
  nacos --> db
  gf --> pm 
  pm -.-> App 
  App <-.-> nacos 
  App <-.-> sd
  sd --> nacos
  App --> mq 

7.1.2 项目部署

项目部署分为三部分:

  • 编译打包:maven package
  • 制作镜像:docker build & publish
  • 发布Swarm Stack

编译部分很简单,使用 mvn clean package 即可。但需要使用不同的profile区分开发和部署环境。

制作镜像部分可以集成到 Compose中。

7.1.3 Maven 配置

7.1.3.1 Maven profile

Maven 支持 Profile,并可使用 Profile 来选择,替换配置文件。

<profiles>
  <profile>
      <id>dev</id>
      <activation>
        <activeByDefault>true</activeByDefault>
      </activation>
      <properties>
        <mysql.address>localhost</mysql.address>
      </properties>
  </profile>
</profiles>

使用不同的Profile可以定义dependency,property等信息。通常是与resource filtering 结合使用,构造不同的配置文件,如:

<build>
  <resources>
    <resource>
      <directory>${project.basedir}/src/main/resources/</directory>
      <filtering>true</filtering>
      <includes>
        <include>bootstrap.yml</include>
        <include>*-${profile.active}.yml</include>
        <include>*-${profile.active}.properties</include>
      </includes>
    </resource>
  </resources>
</build>

而BootStrap内容引用相应参数:

...
spring:
  datasource:
    url: jdbc:mysql://@mysql.address@:3306/db

则在构建时,Maven会将 @mysql.address@替换成 localhost

Maven构建时可以指定actived profile,未指定时使用 <activeByDefault>true</activeByDefault> 的Profile。 可以通过参数来指定,如:

$ mvn clean package -P dev 
7.1.3.2 Spring Profile

Spring 提供 profile功能,这样,可以在多个环境进行选择。

spring.profiles.active: dev 

当然,遵循Spring的配置,可以从命令行来定义:

$ java -Dspring.profiles.active=dev -jar app.jar
$ java -jar app.jar --spring.profiles.sctive=dev 

这样基本上够用了,唯一问题是多个profile都会被打包到 jar中。

Spring active profile 后,会自动选择 application-{profile}.yml/properties 文件作为配置文件。

与maven profile相结合,可以使用 mvn 构建时指定 profile, 并将其与 spring 的 profile 联系在一起: 则需要使用 Spring 的 bootStrap.yml。

spring.profile.active: @profile.active@

配合 maven build resource 部分配置,即可实现选择性打包配置文件。

7.1.3.3 Spring profile include

Spring 可以使用 include 方式,将配置文件 拆分成多个,也可用于将公共部分拆分出来,方便引用和统一修改。 如:

spring.profile.include:
  - mysql.yml
  - redis.yml 
  - sentinel.yml 

如果此类文件很多,则可以使用maven include来管理,如将文件按照profile分目录保存。

profiles
  dev
    - mysql.yml 
    - redis.yml
  swarm 
    - mysql.yml 
    - redis.yml

在pom.xml文件中 添加 build resources include :

<build>
  <resources>
    <resource>
      <directory>${project.basedir}/src/main/resources/</directory>
      <filtering>true</filtering>
      <includes>
        <include>bootstrap.yml</include>
        <include>*-${profile.active}.yml</include>
        <include>*-${profile.active}.properties</include>
        <include>${project.basedir}/../profiles/${profile.active}/*.yml</include>
      </includes>
    </resource>
  </resources>
</build>

7.1.4 使用Nacos config

Nacos 支持 配置管理,可以将配置信息部署在 Nacos 中,应用启动时从配置中心获取信息。

使用前需要引用依赖:

<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

Nacos config 需要使用 bootstrap.yml ,并在其中添加:

spring.cloud.nacos.config:
  server-addr: @nacos.server-addr@
  namespace: nacos-test
  group: APP_CONFIG
  file-extension: yml

这样使用的Nacos data-id 是: ${spring.application.name}-{spring.profiles.active}.yml

在namespace中创建相应的配置项,即可加载nacos配置。

也可以使用多个共享配置项,Nacos提供了shared-dataids, refreshable-dataids, ext-config,ext-config配置最为全面:

spring.cloud.nacos.config:
  server-addr: @nacos.server-addr@
  namespace: nacos-test
  group: APP_CONFIG
  file-extension: yml

  ext-config: 
    - group: ${spring.cloud.nacos.config.group}
      data-id: ${spring.profiles.active}-mysql.yml 
      refersh: false
    - group: ${spring.cloud.nacos.config.group}
      data-id: ${spring.profiles.active}-redis.yml 
      refersh: false

如果使用Nacos 管理共享配置文件,那么就不需要使用 profiles.include 了。

在初始化时,也可将 本地配置文件 直接发布到 Nacos,使用Nacos Open API 来创建 namespace, 创建 配置项。

下面是使用 Ansbile 发布的例子:

- hosts: localhost
  gather_facts: no
  vars:
    - nacos:
        server: 100.107.200.8
        port: 8848
    - config_data:
        namespace: test_namespace
        data_id: common-mysql.yml
        group: APP_CONFIG 
        config_file: mysql.yml
  tasks:
  - name: add nacos config 
    ansible.builtin.uri:
      url: http://{{ nacos.server }}:{{ nacos.port }}/nacos/v1/cs/configs
      method: POST
      body_format: form-urlencoded
      body: "{{
            'tenant=' + config_data.namespace +
            '&dataId=' + config_data.data_id +
            '&group=' + (config_data.group | default('DEFAULT_GROUP')) +
            '&content=' + lookup ('file', config_data.config_file) + 
            '&type=YAML'
          }}"
      return_content: yes
      status_code:
        - 200
    delegate_to: localhost
    register: post_result

使用Ansible可将发布初始配置作为部署的一个步骤,减少手动干预。

Nacos 的配置自动刷新,在代码中需要支持刷新时,使用 @RefreshScope 注解,如果有进一步需要,可考虑实现Nacos 监听来实现复杂的刷新逻辑。

7.1.5 Demo Compose 文件

使用Compose文件可以发布镜像并构建,之前已有介绍。demo 项目compose文件在: docker/final-demo-compose.yml

7.1.5.1 Networks

Compose 定义了 4个网络:

networks:
  gateway_net: 
    driver: overlay
  app_net: 
    driver: overlay
  back_net:
    driver: overlay
  mgr_net:
    driver: overlay 

gateway_net 用于 gateway 项目。 app_net 用于部署其他应用项目。 mgr_net 用于部署Nacos,Sentinel Dashboard back_net 用于部署 MySQL。

7.1.5.2 App Service

各 Spring APP 使用 app-layer.Dockerfile 构建。

除了gateway发布可 8090端口外,其他app均未发布端口,这是因为这些服务在Swarm 内部网络中,端口可以相互访问的,因此不需要发布。

特别的,对于Dubbo 项目,可能出现 获取错误的本机 IP 问题,因此,附加了环境变量:

environment:
  - JVM_OPTS=-Ddubbo.network.interface.preferred=eth0  

指定 dubbo 使用 eth0 网卡地址。

注意:

服务名必须使用标准的主机名,不能包含下划线。否则,使用服务名访问 Tomcat 会出现 400 Bad Request 异常。

为节约资源,所有APP均只使用了一个副本。如需多个副本,修改 replicas 数量即可。或,使用 service scale命令,如:

docker service scale demo_app-gateway=4
7.1.5.3 Nacos & MySQL

使用了Nacos单机版,配合MYSQL数据库。这是因为 Nacos 集群占用资源过多,如想使用集群部署,可以采用nacos-cluster-dc.yml中的集群配置。

Nacos 使用了 MySQL 的 hostname 定义 数据库地址:

nacos:
  environment:
    - MODE=standalone
    # using MYSQL 
    - SPRING_DATASOURCE_PLATFORM=mysql
    - PREFER_HOST_MODE=hostname

MySQL配置中,定义了 hostname,并使用了 Volume nacos_mysql:

mysql:
  hostname: nacos-mysql
  image: nacos/nacos-mysql:5.7
  volumes:
    - nacos_mysql:/var/lib/mysql
7.1.5.3 Sentinel Dashboard

Sentinel Dashboard 使用nacos 服务名:

sentinel-dashboard:
  image: sentinel/sentinel-dashboard:v1.8.6
  ports:
    - "9090:8080"
  environment:
    - NACOS_NAMESPACE=nacos-test
    - NACOS_SERVER=nacos
7.1.5.4 Swarm profile

在父项目的 pom.xml 中,定义了 swarm profile, 其中定义了几个参数:

<profile>
  <id>swarm</id>
  <properties>
    <profile.active>swarm</profile.active>
    <swarm.project.name>demo</swarm.project.name>
    <nacos.server>nacos:8848</nacos.server>
    <sentinel.server>sentinel-dashboard:8080</sentinel.server>
    <mysql.server>mysql</mysql.server>
  </properties>
</profile>

其中,nacos.server 和 sentinel.server 均使用了 服务名:端口号 方式定义。

这些参数在application-swarm.yml中被引用:

spring.cloud.nacos:
  discovery:
    server-addr: @nacos.server@
    namespace: nacos-test

spring.cloud.sentinel:
  transport:
    dashboard: @sentinel.server@

Maven 构建后,将被替换成相应的参数值:

spring.cloud.nacos:
  discovery:
    server-addr: nacos:8848
    namespace: nacos-test

spring.cloud.sentinel:
  transport:
    dashboard: sentinel-dashboard:8080

7.1.6 Demo 构建及启动

构建并启动Demo过程简单:

    1. 下载并构建项目
$ cd your_project_path
$ git@gitee.com:jeffwang78/springcloud-on-docker-swarm.git
$ cd springcloud-on-docker-swarm/
$ mvn clean package -P swarm
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary for swarm-spring-cloud 0.0.1:
[INFO]
[INFO] swarm-spring-cloud ................................. SUCCESS [  1.594 s]
[INFO] swarm-sca-demo-gateway ............................. SUCCESS [ 11.829 s]
[INFO] swarm-sca-demo-services ............................ SUCCESS [  0.744 s]
[INFO] swarm-sca-demo-calc ................................ SUCCESS [  4.262 s]
[INFO] swarm-sca-demo-vars ................................ SUCCESS [  3.338 s]
[INFO] swarm-sca-demo-fact ................................ SUCCESS [  1.242 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  24.642 s
[INFO] Finished at: 2022-12-19T14:00:23+08:00
[INFO] ------------------------------------------------------------------------
    1. 构建镜像,如有私有Registry,则发布镜像
$ cd docker
$ export DOCKER_REGISTRY=Your_Private_registry/
$ docker compose -f final-demo-compose.yml build
...
$ docker image ls
REPOSITORY                         TAG           IMAGE ID       CREATED          SIZE
sentinel/sentinel-dashboard        v1.8.6        755933e5537d   29 seconds ago   252MB
demo/swarm-sca-demo-vars           v0.0.1        b7b93e72c0a0   38 minutes ago   172MB
demo/swarm-sca-demo-gateway        v0.0.1        cb6b03114cd8   38 minutes ago   175MB
demo/swarm-sca-demo-fact           v0.0.1        5bb48b1cf7b1   38 minutes ago   182MB
demo/swarm-sca-demo-calc           v0.0.1        d937fda7081a   38 minutes ago   182MB
$ docker compose -f final-demo-compose.yml push
... 
    1. 部署到 Swarm.
$ docker stack deploy -c final-demo-compose.yml demo
Ignoring unsupported options: build
Creating network demo_back_net
Creating network demo_mgr_net
Creating network demo_app_net
Creating network demo_gateway_net

creating service demo_nacos (id: apbwne1dobtfwjtgcl2chxwbj)
Creating service demo_sentinel-dashboard (id: lfjmhvv48sgh25fxwbsrtggol)
Creating service demo_app-gateway (id: kdenfxeurzv3r6hmr3akulu4d)
Creating service demo_app-calc (id: 4gil0q9sl7whz48pezp3hqy0b)
Creating service demo_app-vars (id: ds3il75oom04mqxsqfenctm3e)
Creating service demo_app-fact (id: lam5k6px7n1y1ojf82urmiwl2)
Creating service demo_mysql (id: dpv1du4zntw4qyamku5le5tvd) 

$ docker stack ps demo 
ID             NAME                        IMAGE                                NODE              DESIRED STATE   CURRENT STATE          ERROR     PORTS
at3o2b9mhfzl   demo_app-calc.1             demo/swarm-sca-demo-calc:v0.0.1      DESKTOP-60J9GOH   Running         Running 2 hours ago             
redrw91nob83   demo_app-fact.1             demo/swarm-sca-demo-fact:v0.0.1      DESKTOP-60J9GOH   Running         Running 2 hours ago             
j0bx4al2ip0n   demo_app-gateway.1          demo/swarm-sca-demo-gateway:v0.0.1   DESKTOP-60J9GOH   Running         Running 4 hours ago             
08p9vmh2nh8m   demo_app-vars.1             demo/swarm-sca-demo-vars:v0.0.1      DESKTOP-60J9GOH   Running         Running 4 hours ago             
qujxvsb61kwf   demo_mysql.1                nacos/nacos-mysql:5.7                DESKTOP-60J9GOH   Running         Running 4 hours ago             
jdas5f9qsan7   demo_nacos.1                nacos/nacos-server:v2.1.2            DESKTOP-60J9GOH   Running         Running 2 hours ago             
0qdze0o6mtkv   demo_sentinel-dashboard.1   sentinel/sentinel-dashboard:v1.8.6   DESKTOP-60J9GOH   Running         Running 2 hours ago             
  • 验证效果:
$ curl localhost:8090/calc/a/3!
6
$ curl localhost:8090/calc/a/4!
30

注意,由于MySQL 启动较慢,因此,可能会出现 Nacos 等启动失败的情况,耐心等待一会儿即可。

后继章节将详细介绍 各类组件的 使用、配置方法。

7.2 Nacos服务发现及OpenFeign

7.2.1 OpenFeign

使用OpenFeign需要包含 openfeign starter。

OpenFeign服务包括:

  • demo.services 模块包括feign接口:
    • Calculator:定义plus整数相加的服务接口。
    • Variables: 定义变量的 set/get/list服务接口。
  • demo.calc模块包括Calculator 服务实现。
  • demo.vars模块包含Variables服务实现。
  • demo.calc模块使用openFeign调用vars get/set 服务。
  /**
	 * Variable feign client .
	 */
  @Autowired
  protected Variables vars ;
    ...
  public int plus (
			@PathVariable String v1,
			@PathVariable String v2)
	{
    ...
    // 调用 getVar 服务,获取 v1 变量值。
    int n1 = vars.getVar (v1) ;

    int r = n1 + n2 ;

    // 调用 setVar 服务,保存v1 变量值
    vars.setVar (v1, r) ;

    return r ;
  }

注意:

需要在`@EnableFeignClients添加service接口所在的package名称,否则会出现 Autowired 失败的情况。

7.2.2 Nacos 服务发现

服务发现使用 nacos 服务器,application.yml配置如下:

spring.cloud.nacos:
  discovery :
    # server-addr : nacos-server-ip:8848
    server-addr : Tasks.demo_nacos:8848
    namespace : nacos-test

其中需要注意,application的名字和feign服务名要一致:

# demo.vars application.yml
spring.application:
  name: service-vars
@FeignClient("service-vars")
public interface Variables

7.3 Dubbo 服务

Dubbo 是较为独立、完备的系统,经历了较长时间的实际应用和演进,具有完善的体系和开发运维生态。 Dubbo结合SpringCloud后,应用可轻松集成Duboo,系统内部应用间采用RPC通信机制无疑性能更为优异。 本Demo中 Factorial 阶乘 服务使用Dubbo。

7.3.1 依赖

Alibaba 提供 starter,可以很容易引用 dubbo。项目中引入依赖:

<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>

7.3.2 Dubbo 配置

SpringCloud版本的Dubbo配置如下:

# dubbo config
# spring boot > 2.6
spring.main.allow-circular-references: true
# 
dubbo.cloud.subscribed-services: never-exists-service-provider

dubbo:
#  application.name: dubbo-${spring.application.name}
# 包含dubbo服务的包名
  scan.base-packages: jf.free.org.ssca.demo
  protocol:
    name: dubbo
#    host: localhost
    port: 20880
  registry:
    address: spring-cloud://localhost

注册中心使用 spring-cloud(SpringCloud 实际使用 Nacos)。

注意:

spring.main.allow-circular-references: true, Spring Boot 2.6 时,打开该配置,否则Dubbo启动失败。

dubbo.cloud.subscribed-services: never-exists-service-provider, 如果Dubbo不依赖任何服务,Dubbo会不停提示WARN,因此写一个不存在的服务名 避免该告警。

7.3.3 Dubbo服务定义

Dubbo服务实例使用注解进行定义最为方便,本例中fact项目中提供了一个 阶乘计算 的Dubbo服务:

@DubboService
public class FactorialImpl implements Factorial
{
	@Override
	public int factorial (int n)
  {
    
  }
}

引用Dubbo服务同样使用 注解 来声明 服务的本地 Stub, 如:

  @DubboReference
	protected Factorial fact ;
  
  protected int getVal (String arg, String [] varName)
	{
		if (isFact)
		{
			val = fact.factorial (val) ;
		}

		return val ;
	}

Dubbo会根据 DubboReference 引用的接口,生成 stub。

Spring 的 Applcaiton 需要加上 EnableDubbo 注解:

@EnableDubbo(scanBasePackages = "jf.free.org.ssca.demo")
public class SwarmScaDemoFactApplication
{
}

注意:

使用Java 11 运行会出现安全问题,应使用 Java 1.8。

DubboReference 创建时,会检查依赖服务是否存在,如不存在,会出现 No provider available 异常,终止运行。使用 dubbo.consumer.check=false 可避免此异常。

7.4 gateway

SpringCloud 提供 stater-gateway 依赖,支持 Gateway 的 路由,过滤。

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

由于gateway使用Netty作为Web服务器,因此不能引入starter-web(会引入Tomact)。

Gateway 路由通常是依赖于 loadbalancer 进行 服务路由的。

SpringCloud 支持基于配置的路由处理,提供了大量的预制filter,可以对HTTP URL,parameter, Header等进行判断、改变。这里仅示例简单的StripPrefix过滤器:

spring.cloud.gateway:
  routes:
  - id: calc_service_route
    predicates:
      - Path=/calc/**
    uri: lb://service-calc    
    filters:
      - StripPrefix=1
  - id: vars_service_route
    predicates:
      - Path=/vars/**
    uri: lb://service-vars
    filters:
      - StripPrefix=1

上文是 route 的标准写法,含义如下:

  • id: 路由规则的唯一名称。用于引用路由规则。
  • predicates: 使用本路由规则的需符合的判断条件,可以有多个。
  • Path=/calc/**:表示判定URL路径部分是否符合 /calc/**
    • 这是简写模式,相应的展开写法为:
      predicates:
        - name: Path
          args: 
            - regexp: /calc/** 
  • uri: 本路由规则的出口URI,本例中使用了loadbalancer的URL
  • filters: 过滤器,对请求进行修改。本例使用了 StripPrefix。表示去掉路径的第一个目录项,结果为lb://service-calc/**,/calc/被去掉了。

7.5 Sentinel 断路器

7.5.1 Sentinel 引入

SpringCloud Alibaba 已经提供了较完善的Sentinel适配,只需要引入以下依赖即可:

<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

这将开启 Sentinel对 REST 访问的拦截。

7.5.2 sentinel 基础配置

在 application.yml 中添加 sentinel 的配置:

spring.cloud.sentinel:
# 默认值,开启sentinel 
  enabled: true
# 默认值,是否预加载
  eager: false
  transport:
  # Sentibel Client 应用运行的IP 地址。后面会讲到。
    client-ip: 172.25.16.1
  # Sentinel Client 应用绑定的端口号。默认8719
    port: 8719
  # 连接至 Sentinel dashboard 地址。
    dashboard: localhost:9090
#    heartbeat-interval-ms:
  filter:
  # Sentinel 拦截器 覆盖的 URL,默认值为 /*。
    url-patterns: "/**"
    enabled: true
  log:
  # 日志缺省目录是 $HOME/logs/。BTW, nacos client 日志也在这里。
    # dir:   

Sentinel Spring Cloud 适配时,会使用SentinelWebInterceptor 拦截器拦截指定的 URL。在启动日志中会看到:

[Sentinel Starter] register SentinelWebInterceptor with urlPatterns: [/**].

这样也较容易理解Sentinel的运行模式:

  • 拦截URL
  • 查找断路器配置(限流熔断等),如有:
    • 检查是否超出断路器配置
      • 未超出:允许执行。
      • 超出:阻止(抛出异常)。
  • 结束

这个逻辑很简单,难点在于算法和效率。

7.5.2 Sentinel Client 和 Dashboard

Sentinel Client 运行在Spring内部,Dashboard只是用于监控,也可进行可视化的配置断路器参数,但并非必须的。

Sentinel Client 启动了一个WebServer,默认端口 8719,启动后,将连接Dashboard进行注册。注册之后,Dashboard会从 Client 定时拉取metric数据。该Metric数据格式是自定义的。

两者的关系是:

flowchart LR 
  subgraph client
  sc[Sentinel Client]
  port[port:8719]
  sc -. listen .-> port 
  end 
  sd[Sentinel Dashboard]
  client --1. POST /registry/machine-->sd
  sd--2. GET /jsonTree 获取资源树--> client
  sd--3. GET /metric   刷新监控数据--> client
  sd--4. POST /setRule 推送规则--> client
  sd--5. GET /getRules 获取规则--> client

上图可见, Client 和 Dashboard 之间是双向可见的,因此,在网络上应尽量放在一个子网内。

下面进行简单的测试:

  • 启动Demo应用和Sentinel Dashboard。
  • 访问一次 Demo 应用(因使用eager: false,首次访问时才会初始化 Sentinel Client)。
  • Dashboard 左侧出现应用的名称: service-calc (1/1):这表示该服务有一个节点,且健康。
  • 选择 “簇点链路”,右侧显示 Client 的管理的资源。
    • sentinel_default_context
    • sentinel_spring_web_context
      • /plus/{v1}/{v2}
  • 任意访问几次 plus。再看实时监控情况,能看到访问数量的统计。按照QPS单位显示。

下面结合 API 来查看数据情况:

$ curl 172.25.16.1:8719/jsonTree?type=root | jq
[
  {
    "averageRt": 0,
    "blockQps": 0,
    "exceptionQps": 0,
    "id": "67a33ade-9af3-4d46-b29a-f4f1a7829738",
    "oneMinuteBlock": 0,
    "oneMinuteException": 0,
    "oneMinutePass": 0,
    "oneMinuteTotal": 0,
    "passQps": 0,
    "resource": "machine-root",
    "successQps": 0,
    "threadNum": 0,
    "timestamp": 1670814449071,
    "totalQps": 0
  },
  {
    "parentId": "67a33ade-9af3-4d46-b29a-f4f1a7829738",
    "passQps": 0,
    "resource": "sentinel_default_context",
    
  },
  {
    "parentId": "67a33ade-9af3-4d46-b29a-f4f1a7829738",
    "resource": "sentinel_spring_web_context",
   },
  {
    "parentId": "773f1667-ae15-41be-8740-a3a05f09f341",
    "passQps": 0,
    "resource": "/plus/{v1}/{v2}",
  }
]
// 返回了 Client 全部资源信息。部分信息删除了。
$ curl 172.25.16.1:8719/metric?startTime=167081463800
1670814232000|__total_inbound_traffic__|2|1|2|0|31|0|0|0
1670814233000|/plus/{v1}/{v2}|2|1|2|0|11|0|0|1
1670814233000|__total_inbound_traffic__|2|1|2|0|11|0|0|0
1670814234000|/plus/{v1}/{v2}|2|1|2|0|7|0|0|1
1670814234000|__total_inbound_traffic__|2|1|2|0|7|0|0|0
1670814783000|__cpu_usage__|1046|0|0|0|0|0|0|0
// 返回了 Client cpu 和 资源访问 情况,为减小消耗,使用了紧凑的格式。

这样就可以理解Dashboard监视的实现方法了。

Dashboard另一功能是配置断路器参数。以最简单的 QPS限流参数为例:

  • 在 簇点链路 界面 选择 /plus/{v1}/{v2} 右侧的 + 流控
  • 阀值类型选 QPS(每秒流量),单机阀值 填写 2。
  • 点 新增按钮 添加该限流规则。
  • 连续刷新 /plus/ ,间或出现:Blocked by Sentinel (flow limiting)
  • 在实时监控界面,可见 QPS图存在拒绝信息。

这说明QPS 2 限流设置生效了。这时 Dashboard 将规则 POST 到 Client /setRule。之后,Dashboard会访问 getRule 来检查是否设置成功了。

$ curl 172.25.16.1:8719/getRules?type=flow | jq
[
  {
    "clusterConfig": {
      "acquireRefuseStrategy": 0,
      "clientOfflineTime": 2000,
      "fallbackToLocalWhenFail": true,
      "resourceTimeout": 2000,
      "resourceTimeoutStrategy": 0,
      "sampleCount": 10,
      "strategy": 0,
      "thresholdType": 0,
      "windowIntervalMs": 1000
    },
    "clusterMode": false,
    "controlBehavior": 0,
    "count": 2,
    "grade": 1,
    "limitApp": "default",
    "maxQueueingTimeMs": 500,
    "resource": "/plus/{v1}/{v2}",
    "strategy": 0,
    "warmUpPeriodSec": 10
  }
]

这就是刚刚配置的限流参数。下一节将进一步了解Sentinel 提供的 4 种断路器配置

7.5.3 断路器配置

断路器分类和配置可参考:https://sentinelguard.io/zh-cn/docs/ 以及 https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel ,这里仅做概要介绍。

7.5.3.1 限流 (type=flow)

限流可选的指标是 QPS (每秒流量,或每秒访问量),和并发线程数。

两者的区别在于,QPS是统计 "Entry" 的数量,而并发线程数 统计的是 "执行中" 的数量。 比如:每秒有10次访问,这是QPS;每个访问在服务器执行了 2 秒才结束,那么,并发线程数会累计成20个。

       0  1  2  3 (s)
QPS 10 =  =
          =  =
             =  =
线程数 10 20 20 10

可见,当服务器被长的访问占用时,会占用线程池造成不可用。因此,当存在较长调用链或耗时服务时,应考虑限制此类服务占用的线程数。

alt Senyinel Flow rule UI

上图展示了 Dashboard 的流控规则配置界面,对应界面和之前的规则JSON,易于理解配置属性的定义:

  • 资源名(resource): 规则管控的资源名称。
  • 针对来源(limitApp): 根据来源方的名称进行管控。
  • 阀值类型(grade): 选用的指标:
    • QPS: 1
    • 并发线程数: 0
  • 单机阀值(count):限流数量。超出的将被阻拦。
  • 是否集群(clusterMode):集群模式开关,true/false。
  • 流控模式(strategy):链路关系
    • 直接: 0,仅针对该资源指标进行控制
    • 关联:1, 根据关联的资源指标,来限制本资源。
    • 链路:2, 根据调用方来源(链路)进行限制。
  • 流控效果(controlBehavior):当超出阀值时的处理方式。
    • 快速失败:0, 直接抛出异常BlockException。
    • Warm up: 1, 冷启动,即当流量突然增大时,通过阀值控制缓慢释放流量。
    • 匀速器:2,即漏桶算法,使流量匀速通过。
    • Warm up + 匀速器:3,两者结合。本质与 warm up 相同,但可使用 maxQueueingTimeMs 参数。
    • 在 设定 count = 20情况下,瞬间请求QPS为 100 的情况下,Warm up 和 匀速器的差别在于:
      • Warm up 配合 参数 warmUpPeriodSec, 使流量在此期间逐步增加到20,之后匀速放行。
      • 匀速器 直接将QPS拉升到 20, 剩余80个请求在后4秒内依次放行。
      • 另一个参数是: maxQueueingTimeMs,当匀速器需要进行排队时,检查预期排队时间是否超出该参数,如超出,则将直接拒绝该请求。如 maxQueueingTimeMs = 2 * 1000ms,通过计算,2秒内仅能通过 20 * 2 = 40个请求,则后80个请求会被拒绝。

TODO关联资源 limitApp 配置

7.5.3.2 熔断 (type=degrade)

熔断,或称"降级",顾名思义,是将某个不稳定的链路从调用链中断开, 熔断期间,入站流量均被拒绝。

TODO: 需要查看熔断时是否会调整 Spring health 从而 禁止 路由 至该应用。

熔断的指标包括:

  • 响应时间RT:当大量请求响应时间过长时,认为该节点不稳定,需要断开,防止问题扩散。
  • 异常:请求频繁出现异常时(不包括Sentinel BlockException),应断开。

一旦触发熔断后,将在指定熔断时间内拒绝请求,超过熔断时间后,进入所谓的 "HALF_OPEN",这时,一旦新请求到达时出现超过RT或异常现象,将再次进入熔断,否则将解除熔断状态。

alt Senyinel Degrade SlowCall rule UI

上图展示了 慢调用比例 的熔断配置信息,使用 curl查看配置json:

$ curl 172.25.16.1:8719/getRules?type=degrade | jq
[
  {
    "count": 300,
    "grade": 0,
    "limitApp": "default",
    "minRequestAmount": 5,
    "resource": "/plus/{v1}/{v2}",
    "slowRatioThreshold": 0.8,
    "statIntervalMs": 1000,
    "timeWindow": 10
  }
]

各配置项和json对应关系如下:

  • 资源名(resource):资源的名字
  • 熔断策略(grade):
    • 慢调用比例: 0, 当统计时长内慢调用超出比例后,将熔断应用。该策略与 RT 和 比例阀值 联合使用。
    • 异常比例:1,当统计时长内异常次数/调用次数超过 比例阀值,将熔断。
    • 异常数:2,当统计时长内异常次数达到 比例阀值,将熔断。
  • 最大RT(count): 该参数居然名为 count,不大合适。用于界定慢调用的 阀值,单位毫秒。
  • 比例阀值(slowRatioThreshold):慢调用比例阀值(0 - 1)。
  • 熔断时长(timeWindow):熔断时长。
  • 最小请求数(minRequestAmount):当QPS低于该阀值时,不触发熔断规则。
  • 统计时长(statIntervalMs):统计时长。(如果是 滑动窗口模式 更佳)。

alt Senyinel Degrade Exception ratio rule UI

上图是异常比例配置,相应的 json 数据为:

$ curl 172.25.16.1:8719/getRules?type=degrade | jq
[
  {
    "count": 0.6,
    "grade": 1,
    "limitApp": "default",
    "minRequestAmount": 5,
    "resource": "/plus/{v1}/{v2}",
    "slowRatioThreshold": 1,
    "statIntervalMs": 1000,
    "timeWindow": 10
  }
]

新配置项和json对应关系如下:

  • 比例阀值(slowRatioThreshold):未使用。
  • 异常比例(count): 统计时间内的异常比例,超过将触发熔断。

alt Senyinel Degrade Exception Count rule UI

上图是异常数配置,相应的 json 数据为:

$ curl 172.25.16.1:8719/getRules?type=degrade | jq
[
  {
    "count": 8,
    "grade": 2,
    "limitApp": "default",
    "minRequestAmount": 5,
    "resource": "/plus/{v1}/{v2}",
    "slowRatioThreshold": 1,
    "statIntervalMs": 1000,
    "timeWindow": 10
  }
]

新配置项和json对应关系如下:

  • 比例阀值(slowRatioThreshold):未使用。
  • 异常数(count): 统计时间内的异常数量,超过将触发熔断。
7.5.3.3 热点参数限流 (param-flow)

热点限流的作用是针对资源调用的参数值进行限流,比如:某项商品被频繁访问,这时如果仅对整体进行限流,则可能被该商品占满,其他商品无法访问的情况。

热点限流的原理是指定资源的参数,默认会将该参数值进行统计,单位时间内,对占比很高的参数进行局部限流。

更多的用法是指定参数值和限流阀值进行限流。

热点参数部分在feign部分介绍。

7.5.3.4 授权规则 (authority)

授权规则是指白名单、黑名单,根据调用方来限制。通常不需要使用此类规则。

7.5.3.5 系统负载规则 (system)

系统规则是使用系统load/cpu情况作为参考指标,结合QPS、RT、并发线程数综合进行流控。

当系统因应用压力过高(表现为 Load1 cpu RT 高),为保护系统而进行限流。可参考:https://sentinelguard.io/zh-cn/docs/system-adaptive-protection.html , 官方文档讲的透彻。

TODO 资料待查 sentinel 源码 配置包括:

  • highestSystemLoad: load1 。
  • highestCpuUsage: cup 需考虑CPU核数。
  • avgRt:平均响应时间。
  • qps:QPS
  • maxThread:并发线程数。

7.5.4 Sentinel 与 Nacos 配置中心

Sentinel dashboard 并不能保存配置数据,重启后全部消失,因此,需要一种持久化的方式。

这存在两难的问题:

  • Nacos 可以完成持久化和规则推送,但缺乏可视化编辑界面。
  • Dashboard 没有持久化能力,虽可改造Dashboard 增加数据库持久化并配合HA实现高可用,但其编辑界面并未完全支持全部参数编辑。

既想使用 可视化编辑、实时监视,又想持久化、高可用的方案,需要较多的改造才能实现。

在应用实际部署时,通常会在两个环节进行配置和调整:

  • 部署之初预设定断路器设置:这类配置可由开发运维人员编辑,并以文件形式,或使用OPENAPI推送至Nacos。
  • 运行时的即时调整:最好是与监控系统结合,通过界面快速调整参数。这可以在Nacos配置中心手动编辑JSON,或在Dashboard可视化编辑。编辑结果应保存在Nacos配置中心。

由于微服务体系是一定要使用服务注册中心,因此,Nacos必然存在,这样,随便使用Nacos的配置中心能力也是惠而不费的。由此,可以不费力处理 Dashboard 的持久化问题,而集中在 Dashboard 编辑结果是否能有效推送至 Nacos。

graph LR 
  file[初始配置 Rule Files]
  sd[Sentinel dashboard] 
  nacos((Nacos))
  sc[sentinel client]
  file--publish-->nacos
  sd--push-->nacos
  nacos--push-->sc

另外,如果监控集中到其他系统,如Skywalking/Prometheus,那么也可以放弃Dashboard,或仅将其作为Sentinel 功能验证,而采用 Nacos 集中进行规则的编辑、发布。

7.5.4.1 sentinel-datasource-nacos

采用Sentinel提供的 nacos 数据源,通过引入该依赖,可由 Nacos 自动推送 Rules. 首先引入依赖:

<dependency>
  <groupId>com.alibaba.csp</groupId>
  <artifactId>sentinel-datasource-nacos</artifactId>
  <version>1.8.3</version>
</dependency>

在application.yml 中添加配置:

spring.cloud.sentinel:
  datasource:
    ds1.nacos:
      server-addr: ${spring.cloud.nacos.discovery.server-addr}
      namespace: ${spring.cloud.nacos.discovery.namespace}
      group-id: SENTINEL_DEFAULT
      rule-type: flow
      data-type: json
      data-id: ${spring.application.name}-flow-rules

其中:

  • ds1.nacos: 定义了数据源:ds1, 使用 nacos 数据源。其server-addr/namespace定义引用了spring.cloud.nacos.discovery的配置,毕竟nacos.discovery是必然存在的。
  • group-id: 配置所在的组名。
  • rule-type: 规则类型,flow, degrade, system, param-flow, gw-flow。
  • data-type: json
  • data-id: 配置的唯一id,这里采用{app}-{type}-rules。

数据源可定义多个,通常应针对每种类型建立一个。

当Nacos配置变化时,会自动向 app 推送规则。

7.5.4.2 Dashboard 结合 Nacos datasource

对Sentinel dashboard 改造的例子有很多,包括自动发布到Nacos,将配置、监控数据持久化至各类数据库(MySQL, InfuxDB)。 Dashboard提供了三个接口:

  • Repositry:保存配置数据的接口,提供Save/Delete 接口,实现该接口可将Rules保存数据库中。
  • DynamicRuleProvider: 提供Rule数据源,可用于实现从Nacos config拉取 rules。
  • DynamicRulePublisher:发布Rule 数据,可用于实现向Nacos config 推送 rules。

很不幸,Dashboard 的 UI Controller 并未全面支持 Dynamic接口,仅有v2/FlowControllerV2实现了一个Demo。依此模式。当然可以依次生成其他是个ControllerV2,并将 webapp 中的api连接指向 v2,但这样做太麻烦。

本文提出一种简化的改造实现方式。

考察 Sentinel dashbord 源代码,其缺省实现为:

  • Application 注册到 Dashboard.
  • 当选中某类规则链接时,dashboard 通过 SentinelApiClient 类 访问 Application 拉取 Rules.
  • 当添加,修改,删除规则时,dashboard 通过 SentinelApiClient 类 向Application推送 Rules.

Dashboard 的处理完全是针对单用户、单应用的,即:每次拉取某个应用 Rules,修改后再推送回去,它的 InMemoryRepositry 每次拉取Rules时都会清空内存,仅保留最后一次拉取数据。 注意:

考虑多个用户同时操作时,此处理是否会出错?

由此,简单的实现思路是:当dashboard 调用 SentinelApiClient 向 Application 推送 时,同时向 Nacos 推送。 使用AspectJ 可轻松实现该功能。

graph LR
  sd[Sentinel dashboard] 
  nacos((Nacos))
  sc[sentinel client]
  sd --pull-->sc
  sd--push-->nacos
  nacos--push-->sc

ssca-demo-sentinel-dashboard-nacos-config 使用 NacosInterceptorPublisher.java 实现了此思路。代码片段如下:

/**
	 * Intercept SentinelApiClient.setFlowRuleOfMachineAsync.
	 * @param point the JoinPoint.
	 * @return result of point.
	 */
	@Around ("execution (* com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient.setFlowRuleOfMachineAsync (..))")
	public Object interceptFlowRule (ProceedingJoinPoint point)
	{
		String type = FLOW_RULE_TYPE ;
		return wrap (type, point) ;
	}
  /**
	 * Wrapper of {@link  #publishRules(String, String, List)}.
	 * @param type the rule type, one of: flow, system, authority, degrade,param-flow.
	 * @param point the point.
	 * @return result of point.
	 */
	protected Object wrap (String type, ProceedingJoinPoint point)
	{
		Object [] args = point.getArgs () ;
		String app = (String) args [0];

		List<? extends RuleEntity> entities = (List<? extends RuleEntity>) args [3];

//		call to publish rule
		try
		{
			boolean result = publishRules (app, type, entities);

			Object ret = point.proceed ();

			logger.info (" intercept end [" + point.getSignature () + "]");

			return ret ;
		}
		catch (Throwable t)
		{
			logger.error ("CutPoint proceed Error.", t);
			throw new RuntimeException (t) ;
		}

	}
  /**
	 * Publish config to nacos server.
	 * @param app name of application.
	 * @param type type of rules.
	 * @param entities rules list.
	 * @return true if published successfully.
	 * @throws NacosException thrown by Nacos.
	 */
	public boolean publishRules (String app,
		String type, List<? extends RuleEntity> entities) throws NacosException
	{
//		make nacos dataId ;
		String dataId = buildDataId (app, type);

//		copy from sentinel client code .
		String data = JSON.toJSONString(
				entities.stream().map(r -> r.toRule())
						.collect(Collectors.toList()));

		return nacosConfigService.publishConfig (dataId, this.group, data, "json") ;
	}

代码简单,不需要更多解释。

应用端需预先设定全部类型的ds:

spring.cloud.sentinel:
  datasource:
    ds1.nacos:
      rule-type: flow
      ...
    ds2.nacos:
      rule-type: degrade
      ...
    ds3.nacos:
      rule-type: system
      ...
    ds4.nacos:
      rule-type: authority
      ...
    ds5.nacos:
      rule-type: param-flow
      ...  

以上类型的data-id命名,和 NacosInterceptorPublisher 中一致。

在开发该项目时,sentinel dashboard 在 Maven 上没有最新版本,因此最初采用了简单的做法,直接下载了 sentinel 源代码并在其中修改,因此,此处仅将新增的java放在了本项目中,同时应在 DashboardApplication中添加 扫描包:

@SpringBootApplication(scanBasePackages =
        {"com.alibaba.csp.sentinel.dashboard",
        "jf.free.org.ssca.demo.sentinel.nacosconfig"})
public class DashboardApplication {
  ...

改造打包后的 sentinel-dashboard.jar 在 sentinel/nacos-config目录下。

使用该包可重新制作一个 Dockerfile:

FROM openjdk:8-jre-slim

# copy sentinel jar

COPY ["sentinel-dashboard.jar", "/home/sentinel-dashboard.jar"]

RUN chmod -R +x /home/sentinel-dashboard.jar

EXPOSE 8080

ENV SERVER_OPTS '-Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080'
ENV JAVA_OPTS ''
ENV NACOS_NAMESPACE 'public'
ENV NACOS_SERVER 'localhost:8848'

CMD java ${SERVER_OPTS} -Dsentinel.nacos.config.server-addr=${NACOS_SERVER} -Dsentinel.nacos.config.namespace=${NACOS_NAMESPACE} ${JAVA_OPTS} -jar /home/sentinel-dashboard.jar

也可以使用前面章节中的 app-layer.Dockerfile 构建镜像,但构建时需要使用 JVM_OPTS 传入参数。

制作镜像:

$ docker image build --tag sentinel/sentinel-dashboard:v1.8.6 .
Sending build context to Docker daemon  28.68MB
Step 1/9 : FROM openjdk:8-jre-slim
...
Successfully built e8536e9d4ba3
Successfully tagged sentinel/sentinel-dashboard:v1.8.6

使用docker run 启动dashboard(也可以使用compose 或者 service):

$ docker run --name sentinel-dashboard-1.8.6 -d \\
  --hostname sentinel-dashboard-server \\
  --restart unless-stopped \\
  -p 9090:8080 \\
  -e NACOS_NAMESPACE=nacos-test \\
  -e NACOS_SERVER=172.17.0.2 \\
  sentinel/sentinel-dashboard:v1.8.6

# 方便起见可使用 host 模式,这样宿主机与容器共享 IP。
# --hostname 设定hostname
# --restart unless-stopped 表示自动重启,除非被手动停止

# 如资源紧张,可启动Nacos单机版来进行测试, 减少内存占用
$ docker run --name nacos-server -d \
  --hostname sl-nacos-server \
  --restart unless-stopped \
  --mount src=nacos_data,dst=/home/nacos/data \
	-p 8848:8848 -p 9848:9848 -p 9849:9849 \
	-e MODE=standalone \
	-e JVM_XMS=256m -e JVM_XMX=512m -e JVM_XMN=128m \
	-e JVM_MS=32m -e JVM_MMS=128m \
	nacos/nacos-server 
7.5.4.3 进一步修改

可拦截 SentinelApiClient fetchGatewayFlowRules() 系列函数,使其直接从 nacos 来获取配置。

7.5.5 Sentinel gateway

SpringCloud Gateway 使用 Sentinel 时,需要引入如下依赖:

<!-- spring cloud gateway -->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- Spring cloud loadbalancer: 用于 locate lb://服务 -->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- Sentinel -->
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- Sentinel gateway support -->
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>

使用nacos 还需要 nacos-discovery 依赖。Gateway 项目不能引入 starter-web。

application.yml 配置:

spring.cloud.sentinel:
  ...
  filter:
  # 关闭 URL filter
    enable: false 
  datasource:
    ds1.nacos:
      server-addr: ${spring.cloud.nacos.discovery.server-addr}
      namespace: ${spring.cloud.nacos.discovery.namespace}
      group-id: SENTINEL_DEFAULT
# 类型是 gw-flow  
      rule-type: gw-flow
      data-type: json
      data-id: ${spring.application.name}-gw-flow-rules  

Sentinel gateway 使用 routeFilter来截取Route信息,将每一个route id 作为一个资源进行管理,因此,这里需要设置 filter.enable : false。规则配置类型 rule-type 使用 gw-flow。

以下为 route 样例配置:

spring.cloud.gateway:
#  discovery.locator.enable: true
  routes:
  - id: calc_service_route
    uri: lb://service-calc
    predicates:
      - Path=/calc/**
    filters:
      - StripPrefix=1
  - id: vars_service_route
    uri: lb://service-vars
    predicates:
      - Path=/vars/**
    filters:
      - StripPrefix=1

上例使用了最简单的 StripPrefix 过滤器,将 gateway/calc/** 映射到 service-calc。

同样启动gateway,可在 Dashboard 上看到相应的资源,并可设置 gw-flow 网关流控规则。

alt sentine gateway flow rule

TODO: 网关规则

TODO: API Group

网关限流可以为后端应用提供保护。

7.5.6 Sentinel 与 Feign

Feign 的流控配置很简单,在application.yml 加入:

feign.sentinel.enabled: true 

Sentinel会扫描 @FeignClient 并进行拦截。如下图: alt sentinel feign resource 上图可见,feign的资源名称形为:GET:http://service-vars/var/{varName}, 即:httpmethod:http://service-name/requestMappingPath

并且,feign 资源 在 plus 服务的下级,这样,就很容易理解到上文的 limitApp配置。

limitApp可以体现调用链,根据来源进行限流。如相同的资源有多个调用源,可选择其中一个或几个进行精确的限流控制,以下引用自https://sentinelguard.io/zh-cn/docs/flow-control.html:

限流规则中的 limitApp 字段用于根据调用方进行流量控制。该字段的值有以下三种选项,分别对应不同的场景:

default:表示不区分调用者,来自任何调用者的请求都将进行限流统计。如果这个资源名的调用总和超过了这条规则定义的阈值,则触发限流。

{some_origin_name}:表示针对特定的调用者,只有来自这个调用者的请求才会进行流量控制。例如 NodeA 配置了一条针对调用者caller1的规则,那么当且仅当来自 caller1 对 NodeA 的请求才会触发流量控制。

other:表示针对除 {some_origin_name} 以外的其余调用方的流量进行流量控制。例如,资源NodeA配置了一条针对调用者 caller1 的限流规则,同时又配置了一条调用者为 other 的规则,那么任意来自非 caller1 对 NodeA 的调用,都不能超过 other 这条规则定义的阈值。

7.5.7 Sentinel 与 RestTemplate

Sentinel 结合 RestTemplate 时,需要使用 Sentinel 注解 提供RestTemplate实例。这样,Sentinel才可以拦截RestTemplate 调用。

@Configration
@Primary 
public class SentinelConfig 
{
  @Bean 
  @SentinelRestTemplate(blockHandler = "handleException", blockHandlerClass = ExceptionUtil.class)
  public RestTemplate getRestTemplate ()
  {
    return new RestTemplate () ;
  }
}

调用RestTemplate时,使用Autowired获取该Bean:

  @Autowired
  private RestTemplate restTemplate ; 
  private String callRestTemplate ()
  {
      return this.restTemplate.getForObject ("lb://service-calc/plus/1/2", String.class);
  }

推荐使用 Feign 调用REST 服务。

7.5.8 Sentinel 与 Dubbo

7.5.8.1 依赖

不同版本的Dubbo,需要使用不同的 sentinel adapter,如下表:

Dubbo sentinel Adapter 备注
2.6 sentinel-dubbo-adapter
2.7 sentinel-apache-dubbo-adapter
3.0 sentinel-apache-dubbo3-adapter

SpringCloud-Alibaba-2021 使用 2.7 版本,因此,本项目采用依赖:

<dependency>
  <groupId>com.alibaba.csp</groupId>
  <artifactId>sentinel-apache-dubbo-adapter</artifactId>
</dependency>
7.5.8.2 配置热点规则

Sentinel热点规则是指针对调用参数进行单独的限流设定。之前的 REST / Gateway / Feign 的资源定义,并没有包含参数。而Sentinel Dubbo Adapter支持Dubbo的参数。

使用@SentinelResource在函数上定义资源时,即可使用参数。

启动 calc/fact/gateway项目,并访问 localhost:8080/calc/puls/1/2!/ 计算 1 + 2! 的结果后。在Dashboard可看到 service-fact 的信息如下:

alt sentinel dubbo

jf.free.org.ssca.demo.services.Factorial:factorial(int) 增加 热点限流: alt sentinel param flow

整体限流设置了 QPS 100阀值。在高级选项中 添加 针对参数 10 的特殊限流,QPS 2。

这样,当计算10的阶乘时,仅允许其QPS=2。可以通过实际测试来验证。

类似于 阶乘 这类计算,当 参数值过高时,运算时间也会拉长,假如能在 参数中设置简单的条件,如: > 100 就更好了。

7.5.9 Sentinel 底层机制

Sentinel 限流保护 的对象称之为 资源。其底层使用如下机制:

Entry entry = null;
// 务必保证finally会被执行
try {
  // 资源名可使用任意有业务语义的字符串
  entry = SphU.entry("自定义资源名");
  // 被保护的业务逻辑
  // do something...
} catch (BlockException e1) {
  // 资源访问阻止,被限流或被降级
  // 进行相应的处理操作
  // 参见 Fallback
} finally {
  if (entry != null) {
    entry.exit();
  }
}

其本质是,在需要保护的任意代码前,定义一个资源Entry,并对这段代码增加一对entry/exit。这样,就可以在entry时检查其限流条件;在执行结束后统计资源执行时间;在出现异常时,统计异常数量(针对REST API 还需检查 HTTP Response code是否异常)。

Sentinel 支持通过 @SentinelResource 注解定义资源并配置 blockHandler 和 fallback 函数来进行限流之后的处理。示例:

// 原本的业务方法.
@SentinelResource(blockHandler = "blockHandlerForGetUser")
public User getUserById(String id) {
    throw new RuntimeException("getUserById command failed");
}

// blockHandler 函数,原方法调用被限流/降级/系统保护的时候调用
public User blockHandlerForGetUser(String id, BlockException ex) {
    return new User("admin");
}

如此就可以搞清楚Sentinel拦截保护的机制:

  • WEB资源:通过 Fileter 机制,在Filter 执行时检查 SphU.entry(资源名)是否成功。
  • Feign:在调用Feign前,检查。
  • RestTemplate: 类似于SentinelResource注解方式。
  • Dubbo: 类似于SentinelResource注解方式。

而资源调用层次,是通过每次资源 SphU.entry之后,形成嵌套的 Context 捕获的。

7.5.10 异常及Fallback 机制

当资源受保护被阻止时,会抛出 BlockException,BlockException具有几个子类,可以判别阻止的规则信息:

  • FlowException: 限流规则 生效时抛出的异常。
  • DegradeException: 熔断规则生效时抛出的异常。
  • AuthorityException:授权规则生效时抛出的异常。
  • SystemBlockException: 系统规则生效时抛出的异常。

Web应用的异常,可以使用Sentinel提供的配置来处理:

配置项 说明 缺省值
spring.cloud.sentinel.scg.fallback.mode Spring Cloud Gateway 流控处理逻辑 (选择 redirect or response)
spring.cloud.sentinel.scg.fallback.redirect Spring Cloud Gateway 响应模式为 'redirect' 模式对应的重定向 URL
spring.cloud.sentinel.scg.fallback.response-body Spring Cloud Gateway 响应模式为 'response' 模式对应的响应内容
spring.cloud.sentinel.scg.fallback.response-status Spring Cloud Gateway 响应模式为 'response' 模式对应的响应码 429
spring.cloud.sentinel.scg.fallback.content-type Spring Cloud Gateway 响应模式为 'response' 模式对应的 content-type application/json

例如,假设对所有REST API 访问均使用通常的Response结构如:

{
  "ret" :
  {
    "code" : 123,
    "msg" : "Some Error"
  },
  "data": {}
}

那么,可以配置:

spring.cloud.sentinel.scg.fallback:
  mode: response 
  response-body: "{ \"ret\" : { \"code\" : 123, \"msg\" : \"Sentinel Block Exception\"}, \"data\": {}}"
  response-status: 429
  content-type: application/json

使用 @SentinelResource 注解时,可使用Spring @ExceptionHandler异常处理的方式。 针对特定的资源,比如:feign/Dubbo 调用,可以考虑单独定义 Fallback 处理机制,返回更为合适的信息。

7.5.11 集群限流

集群限流目前尚未有 稳定版本,本次未做验证。

Sentinel 采用 Token Server 令牌方式组建集群,并分享集群整体限流数据,通过对服务实例的健康监测来实时调整限流策略。

7.5.12 Sentinel 小结

SpringCloud-Alibaba 对 Sentinel 的支持做的很好。可以看到,stater 基本就可以完成资源注册的工作了,对原代码基本无侵入。

Sentinel Dashboard 作为 一个简单的应用,可以满足开发环境需要。配合 Nacos也可满足轻量级地维护的生产需要。 Sentinel Dashboard 的资源监控功能,将在后文将其集成到 Prometheus。

另需注意:

  • 单机运行多个应用时,注意需要修改 sentinel.transport.port
  • 必须访问过的资源,才会出现在 Sentinel dashboard 中。

7.6 Redis分布式事务

Part 8: CD/CI

Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

简介

笔记:使用 Docker Swarm 部署 Spring cloud 微服务案例 展开 收起
Java 等 2 种语言
Apache-2.0
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
Java
1
https://gitee.com/jeffwang78/springcloud-on-docker-swarm.git
git@gitee.com:jeffwang78/springcloud-on-docker-swarm.git
jeffwang78
springcloud-on-docker-swarm
Springcloud on Docker Swarm
master

搜索帮助