笔记:使用 Docker Swarm 部署 Spring cloud 微服务案例
微服务的架构是基于使用资源换能力的思路设计的(SOA当然也是其核心思路),因此,在实际部署应用时会出现大量的集群式部署,这会带来两方面的挑战:
而在开发阶段,也会给测试环境带来不小的困扰,如何有效配置、隔离个人测试环境呢?
对于开发环境,显而易见的答案是使用docker。而生产环境也是可以使用docker方便的构建、更新集群的。
本文以SpringCloud alibaba 体系为例,完成在Docker Swarm集群下的环境搭建。
本章内容相当基础,有经验的读者可直接转到:7.1.6 Demo 构建及启动,验证Demo项目的构建部署。
本部分使用 Dockerfile/Compose 等文件均在 docker 目录下。
本文采用Windows WSL ubuntu虚拟机来搭建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
使用 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
添加组之后,重新登录即可生效。
docker使用Container容器来运行应用。 可以将Container比作一个沙盒,运行于操作系统之上,且与隔离。 Docker 与 虚拟机的差别在于,虚拟机提供了一组虚拟化的硬件(cpu,磁盘等)。而docker仅提供了虚拟化的操作系统,因此,docker的成本远低于虚拟机。两者关系大概是:
使用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,可以认为它只是一个独立的应用程序在运行,至于运行哪个应用程序,这是由镜像里的内容来决定的。
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。
可以“登录”运行中的容器,进行一些检查、处理。 使用 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 提供终端可以进行输入、输出。当然也可以执行其他命令。
Container stop 之后,仍占用着存储空间,可以通过 rm 命令删除:
$ docker rm myweb
myweb
运行的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
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
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
可见,文件已经复制过去了。
可以登录并使用 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操作自然是太过麻烦,因此,为何不考虑将这些内容放入一个新的镜像里呢?
本节使用简单的命令来建立镜像。
回顾上一节,为了在容器中运行Spring APP,我们执行了下列操作:
将这些步骤写成一个脚本,是不是就可以方便的运行这个应用了呢?
Docker 提供了类似的脚本机制来构建镜像:Dockerfile。 简单的讲,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就像一个脚本,这里引入了五个关键字,也是最常用的关键字。通过与脚本命令的对比,很容易理解其含义。
利用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 这样可提高启动速度。
与之前一样,使用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端口。
使用容器对外提供服务时,如果容器程序意外退出,是否有办法自动重新启动呢? 容器启动时可以指定重启动的策略,包括:
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次后)。
SWARM(蜂群),显而易见,这是一种集群技术。当需要一个或多个宿主机集群上部署多实例的应用时,Docker Swarm可以帮助快速实现这一目的,并且简洁易用。
Node 就是加入Swarm集群中的宿主机。 Swarm 集群将Node分成两种角色:
另外,管理节点也是可以运行任务的。因此,最简的Swarm集群是:1个管理节点。本文大部分demo均使用该模式运行。
注意:Swarm各节点应配置时钟同步服务。
服务和任务是面向用户的核心概念。使用Swarm的目的就是发布服务和任务。
Swarm支持服务运行时的负载均衡,Swarm采用内置的DNS服务实现服务发现,并可通过DNSrr方式在集群内实现负载均衡。
在集群之外,任何已发布的服务,都可以通过集群宿主机进行访问,而不必关心任务实际运行地址。
为体验多宿主机环境,可以使用 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进行粘贴。
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节点有两种办法:
$ 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
# 在新主机运行上面的命令即可
$ 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
同样,使用 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>
...
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
可见五个服务均匀分布在集群的五个节点上。
为了理解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集群作为一个整体对外提供服务,外部应用不需要关心服务实际运行在哪里。
Swarm内置服务发现功能,在Swarm集群内部,部署了DNS 服务,可以有效的对容器服务进行发现和路由。这也是实现微服务动态部署的基础。
Docker 容器使用的网络与宿主机网络隔离的(当然,允许连接 Host network,但这不具备扩展性)。 Docker 使用三种网络类型:
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 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网络上。
Swarm通过ingress形成路由网,所有Service发布的端口,均可从宿主机访问。如下图:
在Swarm外可使用Nginx、HAProxy等代理服务实现外部负载均衡(当然可使用HA模式的负载均衡)。
在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通过命令连接该网络。可以使用
--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 起作用。
微服务通常都是一组服务,比如:
相应的,很多系统需要网络隔离环境,以避免依赖混乱。
对应到Swarm集群上,这就包括了两部分,Service 和 Network。(当然还会有 Config / Secrets/ Volume)。 使用命令行逐一建立服务、Network并不现实,这里要用到Docker Compose使用声明文件来一次性做完上述工作。
Docker Compose 使用声明式语法来定义Service,Network以及Docker其他顶级对象。这些定义使用YAML格式编写在文件中。
下面的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
本文件定义了:
以上等价与命令:
$ 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
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
属性声明了需要依赖的其他服务,这会建立一个正确的启动顺序:
使用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之类的系统,方便快捷。
Docker Stack 沿用了 Compose 的声明式语法,进一步扩展成为应用于集群部署的编排工具。
为了更直观的看到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
还记得创建服务时 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
可见副本数已经变成三个了。
注意到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。
Docker Container自身会有一套文件系统,这些文件会保存在Container文件中,直至Container被删除。这些文件是Container私有的文件,同时也无法使用特定的文件系统,比如NFS、SSD独立存储。
因此,Docker 提供了 Volume 并可使用不同的 Volume Driver 将存储挂载在 Conatiner。
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个目录:
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 的内容。
Docker 可以在容器中挂载宿主机的目录,类似于Linux 的 mount 命令,这样,容器可以访问宿主机文件,而不必将文件复制到容器内。
最简单的用法是直接将宿主机任意目录挂载到容器, 作为测试,建立一个临时目录/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 来挂载卷:
在inspect 中也可以看见,该Volume 的类型type为bind。源和目的都于 -v 命令一致。
可以在compose 中使用该选项,如:
services:
webserver:
image: nginx/alpine
volumes:
- "/tmp/html/:/usr/share/nginx/html"
那么,如果在Swarm集群中使用呢? 需要确保每个宿主机上都有这个/tmp/html/目录,并确保其内容相同。
这样,就带来一个问题:如何确保这些集群中的文件同步?
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"
}
]
除了 local 之外, Docker 还可以使用 很多主流的 分布式文件系统
可以在官网查看: https://docs.docker.com/engine/extend/legacy_plugins/#volume-plugins
比如可以用挂载 NFS 文件系统来支持集群内的文件共享
程序日志是很重要的信息,日志监控、分析也是大型应用系统运营维护、业务分析 的重要内容。在Swarm集群模式下,如何方便的管理日志呢?
考虑容器的目标是独立运行单一应用程序,因此,大部分镜像设计时均已经日志信息通过 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将容器输出日志收集了起来。
那么,容器日志如何保存呢?
$ 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 类型的 日志文件,有三个途径来配置日志:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3",
"labels": "production_status",
"env": "os,customer"
}
}
这对所有container生效。
$ 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
可以使用日志收集系统的 log-driver 比如 journald 或者 fleuntd 等来完成在线日志采集,并可通过这些系统实现日志分析、检索等功能。
之前章节介绍过简单的 镜像制作方法,本节通过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/
使用maven插件制作镜像虽然方便,但通常部署时需要结合不同环境,大部分配置参数都需要调整,因此,有两种解决方案:
以下通过自行构建镜像,来进一步了解Dockfile的构建方法。
回顾之前的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即可。
最简单的思路是,将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构建其他应用, 可自行实验。
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。
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)。
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 add
,COPY
。
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 构建新镜像。
Ansible的使用参考 https://gitee.com/jeffwang78/ansible-spring-application
在上面的笔记中,使用 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
整个构建发布过程包括:
执行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
Docker compose 文件可以定义镜像构建信息。并使用 compose build 命令执行构建。
编制一个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"
]
}
可见已经推送成功。
上述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
两个副本已启动。
可以使用 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 %}
SpringCloud Alibaba采用下列组件:
本Demo还使用OpenFeign执行REST服务调用,使用Redis作为分布式存储及简单事务处理。
本章简要介绍如何在SWARM搭建运行环境。
Nacos 在生产环境应使用集群部署,Nacos还需要MySQL数据库的支持。
个人认为需要外接数据库是Nacos部署的一个弊端。
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数据库的初始化。
具体脚本在 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: "node{{ .Task.Slot }}-nacos"
。.Task.Slot 指replica实例的顺序号(从1开始)。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个集群,运行正常。
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
。
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
Sentinel 的运行核心是在 Sentinel client 上,每个部署在Spring 应用内部的 client 通过 拦截器模式,完成限流操作。因此,实际运行时,Sentinel并不需要控制台。
但alibaba仍提供了Sentinel 控制台,其作用是完成拦截规则的可视化配置,实现可视化监视。
Sentinel 的配置信息极度依赖于 Nacos 配置中心,Sentinel 控制台、Nacos、Spring 客户端形成了一个很奇妙的关系:
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。
可见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部分中加以介绍。
TODO
TODO
TODO
除了必要的应用外,还需要其他监视、管理类应用方能更好的运行整个应用系统:
安装 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 等信息。
在portainer中可以创建、管理container/service/stack。
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文件一起启动。
微服务整个系统部署时,通常划分为三个层次,五个区:
而应用区又可以划分为前台/Gateway、中台、后台,或按照业务领域水平再扩展多个模块。
按照项目的结构,划分网络如下:
Demo 项目逻辑简单,仅用于验证而已。
样例应用包含4个项目:
/var/{varName}
存取参数数据。项目部署分为三部分:
编译部分很简单,使用 mvn clean package 即可。但需要使用不同的profile区分开发和部署环境。
制作镜像部分可以集成到 Compose中。
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
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 部分配置,即可实现选择性打包配置文件。
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>
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 监听来实现复杂的刷新逻辑。
使用Compose文件可以发布镜像并构建,之前已有介绍。demo 项目compose文件在: docker/final-demo-compose.yml
。
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。
各 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
使用了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
Sentinel Dashboard 使用nacos 服务名:
sentinel-dashboard:
image: sentinel/sentinel-dashboard:v1.8.6
ports:
- "9090:8080"
environment:
- NACOS_NAMESPACE=nacos-test
- NACOS_SERVER=nacos
在父项目的 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
构建并启动Demo过程简单:
$ 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] ------------------------------------------------------------------------
$ 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
...
$ 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 等启动失败的情况,耐心等待一会儿即可。
后继章节将详细介绍 各类组件的 使用、配置方法。
使用OpenFeign需要包含 openfeign starter。
OpenFeign服务包括:
/**
* 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 失败的情况。
服务发现使用 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
Dubbo 是较为独立、完备的系统,经历了较长时间的实际应用和演进,具有完善的体系和开发运维生态。 Dubbo结合SpringCloud后,应用可轻松集成Duboo,系统内部应用间采用RPC通信机制无疑性能更为优异。 本Demo中 Factorial 阶乘 服务使用Dubbo。
Alibaba 提供 starter,可以很容易引用 dubbo。项目中引入依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>
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,因此写一个不存在的服务名 避免该告警。
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
可避免此异常。
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 的标准写法,含义如下:
/calc/**
。
predicates:
- name: Path
args:
- regexp: /calc/**
SpringCloud Alibaba 已经提供了较完善的Sentinel适配,只需要引入以下依赖即可:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
这将开启 Sentinel对 REST 访问的拦截。
在 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的运行模式:
这个逻辑很简单,难点在于算法和效率。
Sentinel Client 运行在Spring内部,Dashboard只是用于监控,也可进行可视化的配置断路器参数,但并非必须的。
Sentinel Client 启动了一个WebServer,默认端口 8719,启动后,将连接Dashboard进行注册。注册之后,Dashboard会从 Client 定时拉取metric数据。该Metric数据格式是自定义的。
两者的关系是:
上图可见, Client 和 Dashboard 之间是双向可见的,因此,在网络上应尽量放在一个子网内。
下面进行简单的测试:
下面结合 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}
右侧的 + 流控/plus/
,间或出现:Blocked by Sentinel (flow limiting)这说明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 种断路器配置
断路器分类和配置可参考:https://sentinelguard.io/zh-cn/docs/ 以及 https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel ,这里仅做概要介绍。
限流可选的指标是 QPS (每秒流量,或每秒访问量),和并发线程数。
两者的区别在于,QPS是统计 "Entry" 的数量,而并发线程数 统计的是 "执行中" 的数量。 比如:每秒有10次访问,这是QPS;每个访问在服务器执行了 2 秒才结束,那么,并发线程数会累计成20个。
0 1 2 3 (s)
QPS 10 = =
= =
= =
线程数 10 20 20 10
可见,当服务器被长的访问占用时,会占用线程池造成不可用。因此,当存在较长调用链或耗时服务时,应考虑限制此类服务占用的线程数。
上图展示了 Dashboard 的流控规则配置界面,对应界面和之前的规则JSON,易于理解配置属性的定义:
warmUpPeriodSec
, 使流量在此期间逐步增加到20,之后匀速放行。maxQueueingTimeMs
,当匀速器需要进行排队时,检查预期排队时间是否超出该参数,如超出,则将直接拒绝该请求。如 maxQueueingTimeMs = 2 * 1000ms,通过计算,2秒内仅能通过 20 * 2 = 40个请求,则后80个请求会被拒绝。TODO关联资源 limitApp 配置
熔断,或称"降级",顾名思义,是将某个不稳定的链路从调用链中断开, 熔断期间,入站流量均被拒绝。
TODO: 需要查看熔断时是否会调整 Spring health 从而 禁止 路由 至该应用。
熔断的指标包括:
一旦触发熔断后,将在指定熔断时间内拒绝请求,超过熔断时间后,进入所谓的 "HALF_OPEN",这时,一旦新请求到达时出现超过RT或异常现象,将再次进入熔断,否则将解除熔断状态。
上图展示了 慢调用比例 的熔断配置信息,使用 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对应关系如下:
上图是异常比例配置,相应的 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对应关系如下:
上图是异常数配置,相应的 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对应关系如下:
热点限流的作用是针对资源调用的参数值进行限流,比如:某项商品被频繁访问,这时如果仅对整体进行限流,则可能被该商品占满,其他商品无法访问的情况。
热点限流的原理是指定资源的参数,默认会将该参数值进行统计,单位时间内,对占比很高的参数进行局部限流。
更多的用法是指定参数值和限流阀值进行限流。
热点参数部分在feign部分介绍。
授权规则是指白名单、黑名单,根据调用方来限制。通常不需要使用此类规则。
系统规则是使用系统load/cpu情况作为参考指标,结合QPS、RT、并发线程数综合进行流控。
当系统因应用压力过高(表现为 Load1 cpu RT 高),为保护系统而进行限流。可参考:https://sentinelguard.io/zh-cn/docs/system-adaptive-protection.html , 官方文档讲的透彻。
TODO 资料待查 sentinel 源码 配置包括:
Sentinel dashboard 并不能保存配置数据,重启后全部消失,因此,需要一种持久化的方式。
这存在两难的问题:
既想使用 可视化编辑、实时监视,又想持久化、高可用的方案,需要较多的改造才能实现。
在应用实际部署时,通常会在两个环节进行配置和调整:
由于微服务体系是一定要使用服务注册中心,因此,Nacos必然存在,这样,随便使用Nacos的配置中心能力也是惠而不费的。由此,可以不费力处理 Dashboard 的持久化问题,而集中在 Dashboard 编辑结果是否能有效推送至 Nacos。
另外,如果监控集中到其他系统,如Skywalking/Prometheus,那么也可以放弃Dashboard,或仅将其作为Sentinel 功能验证,而采用 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
其中:
spring.cloud.nacos.discovery
的配置,毕竟nacos.discovery
是必然存在的。数据源可定义多个,通常应针对每种类型建立一个。
当Nacos配置变化时,会自动向 app 推送规则。
对Sentinel dashboard 改造的例子有很多,包括自动发布到Nacos,将配置、监控数据持久化至各类数据库(MySQL, InfuxDB)。 Dashboard提供了三个接口:
很不幸,Dashboard 的 UI Controller 并未全面支持 Dynamic接口,仅有v2/FlowControllerV2实现了一个Demo。依此模式。当然可以依次生成其他是个ControllerV2,并将 webapp 中的api连接指向 v2,但这样做太麻烦。
本文提出一种简化的改造实现方式。
考察 Sentinel dashbord 源代码,其缺省实现为:
Dashboard 的处理完全是针对单用户、单应用的,即:每次拉取某个应用 Rules,修改后再推送回去,它的 InMemoryRepositry 每次拉取Rules时都会清空内存,仅保留最后一次拉取数据。 注意:
考虑多个用户同时操作时,此处理是否会出错?
由此,简单的实现思路是:当dashboard 调用 SentinelApiClient 向 Application 推送 时,同时向 Nacos 推送。 使用AspectJ 可轻松实现该功能。
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
可拦截 SentinelApiClient fetchGatewayFlowRules()
系列函数,使其直接从 nacos 来获取配置。
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 网关流控规则。
TODO: 网关规则
TODO: API Group
网关限流可以为后端应用提供保护。
Feign 的流控配置很简单,在application.yml 加入:
feign.sentinel.enabled: true
Sentinel会扫描 @FeignClient
并进行拦截。如下图:
上图可见,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 这条规则定义的阈值。
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 服务。
不同版本的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>
Sentinel热点规则是指针对调用参数进行单独的限流设定。之前的 REST / Gateway / Feign 的资源定义,并没有包含参数。而Sentinel Dubbo Adapter支持Dubbo的参数。
使用
@SentinelResource
在函数上定义资源时,即可使用参数。
启动 calc/fact/gateway项目,并访问 localhost:8080/calc/puls/1/2!/
计算 1 + 2! 的结果后。在Dashboard可看到 service-fact 的信息如下:
在 jf.free.org.ssca.demo.services.Factorial:factorial(int)
增加 热点限流:
整体限流设置了 QPS 100阀值。在高级选项中 添加 针对参数 10 的特殊限流,QPS 2。
这样,当计算10的阶乘时,仅允许其QPS=2。可以通过实际测试来验证。
类似于 阶乘 这类计算,当 参数值过高时,运算时间也会拉长,假如能在 参数中设置简单的条件,如:
> 100
就更好了。
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拦截保护的机制:
SphU.entry(资源名)
是否成功。而资源调用层次,是通过每次资源 SphU.entry之后,形成嵌套的 Context 捕获的。
当资源受保护被阻止时,会抛出 BlockException,BlockException具有几个子类,可以判别阻止的规则信息:
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 处理机制,返回更为合适的信息。
集群限流目前尚未有 稳定版本,本次未做验证。
Sentinel 采用 Token Server 令牌方式组建集群,并分享集群整体限流数据,通过对服务实例的健康监测来实时调整限流策略。
SpringCloud-Alibaba 对 Sentinel 的支持做的很好。可以看到,stater 基本就可以完成资源注册的工作了,对原代码基本无侵入。
Sentinel Dashboard 作为 一个简单的应用,可以满足开发环境需要。配合 Nacos也可满足轻量级地维护的生产需要。 Sentinel Dashboard 的资源监控功能,将在后文将其集成到 Prometheus。
另需注意:
sentinel.transport.port
。此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。