# Springcloud on Docker Swarm **Repository Path**: jeffwang78/springcloud-on-docker-swarm ## Basic Information - **Project Name**: Springcloud on Docker Swarm - **Description**: 笔记:使用 Docker Swarm 部署 Spring cloud 微服务案例 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 8 - **Forks**: 2 - **Created**: 2022-11-22 - **Last Updated**: 2025-01-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: Docker, Swarm, SpringCloud, alibaba, sentinel ## README # Springcloud on Docker Swarm ## 介绍 笔记:使用 Docker Swarm 部署 Spring cloud 微服务案例 ## 前言 微服务的架构是基于使用资源换能力的思路设计的(SOA当然也是其核心思路),因此,在实际部署应用时会出现大量的集群式部署,这会带来两方面的挑战: * 系统拓扑结构复杂 * 部署更新困难 而在开发阶段,也会给测试环境带来不小的困扰,如何有效配置、隔离个人测试环境呢? 对于开发环境,显而易见的答案是使用docker。而生产环境也是可以使用docker方便的构建、更新集群的。 本文以SpringCloud alibaba 体系为例,完成在Docker Swarm集群下的环境搭建。 ## Part 1: Docker 基础 本章内容相当基础,有经验的读者可直接转到:[7.1.6 Demo 构建及启动](#c716),验证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 文件: ```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的成本远低于虚拟机。两者关系大概是: ```mermaid 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 ... Welcome to nginx! ... ``` 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: ```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 # 在新主机运行上面的命令即可 ``` 2. 将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 ...

Welcome to nginx!

... ``` #### 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 ... THIS is Manager2 $ curl localhost ... ``` 由此可见,第一次访问的是 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](img/ingress-routing-mesh.png) 在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. ```yaml 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中再添加几个项目: ```yaml 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中添加: ```yaml 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配置文件: ```json $ 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 ``` ```json { "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

This is a html on HOST.

$ docker inspect -f "{{json .Mounts}}" html-test |jq ``` ```json [ { "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 中使用该选项,如: ```yaml 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 ``` ```json [ { "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 ``` ```json [ { "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 ``` ```json { "Type": "json-file", "Config": {} } ``` ``` $ docker inspect -f "{{ json .LogPath }}" test "/var/lib/docker/containers/0e0d00c675063fc27de1121f474e9348ed9ae1ee43606aace6e173f1197b04a8/0e0d00c675063fc27de1121f474e9348ed9ae1ee43606aace6e17 3f1197b04a8-json.log" ``` Docker 默认使用 json-file 类型的 日志文件,有三个途径来配置日志: - 在 daemon.json 中配置: ```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 文件中填写: ```yaml 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 命令执行镜像制作。 ```xml org.springframework.boot spring-boot-maven-plugin build-image ``` 简单的将 `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 配置即可: ```xml http://172.24.162.101:5000 org.springframework.boot spring-boot-maven-plugin test test ${docker.registry} ${docker.registry}/${project.artifactId}:${project.version} true ... ``` 为方便起见,定义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: ```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`指令来定义参数: ```Dockerfile ... 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。 ```Dockerfile ENV JVM_OPTS= ... ENTRYPOINT java $JVM_OPTS -jar $APP_JAR_NAME ``` Docker的原则是一个容器仅运行一个服务,因此,Spring应用的端口号可以固定,方便使用: ```Dockerfile 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 来检测服务是否可用,是否健康状态。 ```Dockerfile 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: ```Dockerfile 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 ``` ```json { "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 ```Dockerfile # 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 的信息: ```yaml 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 ```yaml ... - 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 ```yaml - 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 ```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名后面: ```yaml 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或忽略。如: ```yaml 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 模板: ```yaml 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 服务配置: ```yaml 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服务配置: ```yaml 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 ; ```yaml 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 ``` ```json { "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文件中 也可以定义健康检查: ```yaml ... 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 定义: ```yaml ... 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): ```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](img/sentinel-dashboard.png) 可见Sentinel Dashboard已经将自身作为一个客户端注册了。如需取消该功能,需要修改 dockfile。当然,也可以将Dockerfile重写,使其更方便使用。但Sentinel控制台可以使用环境变量进行配置,因此,可以在Compose文件中使用`enviroment`来配置。如: ```yaml 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](img/portainer-ce.png) 在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: 应用网关服务。 ```mermaid 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 来选择,替换配置文件。 ```xml dev true localhost ``` 使用不同的Profile可以定义dependency,property等信息。通常是与resource filtering 结合使用,构造不同的配置文件,如: ```xml ${project.basedir}/src/main/resources/ true bootstrap.yml *-${profile.active}.yml *-${profile.active}.properties ``` 而BootStrap内容引用相应参数: ```yaml ... spring: datasource: url: jdbc:mysql://@mysql.address@:3306/db ``` 则在构建时,Maven会将 @mysql.address@替换成 `localhost`。 Maven构建时可以指定actived profile,未指定时使用 `true` 的Profile。 可以通过参数来指定,如: ``` $ mvn clean package -P dev ``` ##### 7.1.3.2 Spring Profile Spring 提供 profile功能,这样,可以在多个环境进行选择。 ```yaml 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。 ```yaml spring.profile.active: @profile.active@ ``` 配合 maven build resource 部分配置,即可实现选择性打包配置文件。 ##### 7.1.3.3 Spring profile include Spring 可以使用 include 方式,将配置文件 拆分成多个,也可用于将公共部分拆分出来,方便引用和统一修改。 如: ```yaml 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 : ```xml ${project.basedir}/src/main/resources/ true bootstrap.yml *-${profile.active}.yml *-${profile.active}.properties ${project.basedir}/../profiles/${profile.active}/*.yml ``` #### 7.1.4 使用Nacos config Nacos 支持 配置管理,可以将配置信息部署在 Nacos 中,应用启动时从配置中心获取信息。 使用前需要引用依赖: ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config ``` Nacos config 需要使用 bootstrap.yml ,并在其中添加: ```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配置最为全面: ```yaml 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 发布的例子: ```yaml - 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个网络: ```yaml 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 问题,因此,附加了环境变量: ```yaml 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 定义 数据库地址: ```yaml nacos: environment: - MODE=standalone # using MYSQL - SPRING_DATASOURCE_PLATFORM=mysql - PREFER_HOST_MODE=hostname ``` MySQL配置中,定义了 hostname,并使用了 Volume nacos_mysql: ```yml 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 服务名: ```yaml 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, 其中定义了几个参数: ```xml swarm swarm demo nacos:8848 sentinel-dashboard:8080 mysql ``` 其中,nacos.server 和 sentinel.server 均使用了 `服务名:端口号` 方式定义。 这些参数在`application-swarm.yml`中被引用: ```yaml spring.cloud.nacos: discovery: server-addr: @nacos.server@ namespace: nacos-test spring.cloud.sentinel: transport: dashboard: @sentinel.server@ ``` Maven 构建后,将被替换成相应的参数值: ```yaml 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] ------------------------------------------------------------------------ ``` - 2. 构建镜像,如有私有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 ... ``` - 3. 部署到 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 服务。 ```java /** * 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配置如下: ```yaml spring.cloud.nacos: discovery : # server-addr : nacos-server-ip:8848 server-addr : Tasks.demo_nacos:8848 namespace : nacos-test ``` 其中需要注意,application的名字和feign服务名要一致: ```yaml # demo.vars application.yml spring.application: name: service-vars ``` ```java @FeignClient("service-vars") public interface Variables ``` ### 7.3 Dubbo 服务 Dubbo 是较为独立、完备的系统,经历了较长时间的实际应用和演进,具有完善的体系和开发运维生态。 Dubbo结合SpringCloud后,应用可轻松集成Duboo,系统内部应用间采用RPC通信机制无疑性能更为优异。 本Demo中 Factorial 阶乘 服务使用Dubbo。 #### 7.3.1 依赖 Alibaba 提供 starter,可以很容易引用 dubbo。项目中引入依赖: ```xml com.alibaba.cloud spring-cloud-starter-dubbo ``` #### 7.3.2 Dubbo 配置 SpringCloud版本的Dubbo配置如下: ```yaml # 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服务: ```java @DubboService public class FactorialImpl implements Factorial { @Override public int factorial (int n) { } } ``` 引用Dubbo服务同样使用 注解 来声明 服务的本地 Stub, 如: ```java @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 注解: ```java @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 的 路由,过滤。 ```xml org.springframework.cloud spring-cloud-starter-gateway ``` 由于gateway使用Netty作为Web服务器,因此不能引入starter-web(会引入Tomact)。 Gateway 路由通常是依赖于 loadbalancer 进行 服务路由的。 SpringCloud 支持基于配置的路由处理,提供了大量的预制filter,可以对HTTP URL,parameter, Header等进行判断、改变。这里仅示例简单的StripPrefix过滤器: ```yaml 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/**`。 * 这是简写模式,相应的展开写法为: ```yaml 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适配,只需要引入以下依赖即可: ```xml com.alibaba.cloud spring-cloud-starter-alibaba-sentinel ``` 这将开启 Sentinel对 REST 访问的拦截。 #### 7.5.2 sentinel 基础配置 在 application.yml 中添加 sentinel 的配置: ```yaml 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数据格式是自定义的。 两者的关系是: ```mermaid 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 ``` ```json [ { "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 ``` ```json [ { "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 ](img/sentinel-flow.png) 上图展示了 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 ](img/sentinel-degrade.png) 上图展示了 慢调用比例 的熔断配置信息,使用 curl查看配置json: ```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 ](img/sentinel-deg-exp-ratio.png) 上图是异常比例配置,相应的 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对应关系如下: * 比例阀值(slowRatioThreshold):未使用。 * 异常比例(count): 统计时间内的异常比例,超过将触发熔断。 ![alt Senyinel Degrade Exception Count rule UI ](img/sentinel-deg-exp-count.png) 上图是异常数配置,相应的 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对应关系如下: * 比例阀值(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。 ```mermaid 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. 首先引入依赖: ```xml com.alibaba.csp sentinel-datasource-nacos 1.8.3 ``` 在application.yml 中添加配置: ```yaml 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 可轻松实现该功能。 ```mermaid 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` 实现了此思路。代码片段如下: ```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 entities = (List) 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 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: ```yaml 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中添加 扫描包: ```java @SpringBootApplication(scanBasePackages = {"com.alibaba.csp.sentinel.dashboard", "jf.free.org.ssca.demo.sentinel.nacosconfig"}) public class DashboardApplication { ... ``` 改造打包后的 `sentinel-dashboard.jar` 在 sentinel/nacos-config目录下。 使用该包可重新制作一个 Dockerfile: ```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 时,需要引入如下依赖: ```xml org.springframework.cloud spring-cloud-starter-gateway org.springframework.cloud spring-cloud-starter-loadbalancer com.alibaba.cloud spring-cloud-starter-alibaba-sentinel com.alibaba.cloud spring-cloud-alibaba-sentinel-gateway ``` 使用nacos 还需要 nacos-discovery 依赖。Gateway 项目不能引入 starter-web。 application.yml 配置: ```yaml 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 样例配置: ```yaml 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](./img/sentinel-gw-flow.png) TODO: 网关规则 TODO: API Group 网关限流可以为后端应用提供保护。 #### 7.5.6 Sentinel 与 Feign Feign 的流控配置很简单,在application.yml 加入: ```yaml feign.sentinel.enabled: true ``` Sentinel会扫描 `@FeignClient` 并进行拦截。如下图: ![alt sentinel feign resource](img/sentinel-feign.png) 上图可见,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 调用。 ```java @Configration @Primary public class SentinelConfig { @Bean @SentinelRestTemplate(blockHandler = "handleException", blockHandlerClass = ExceptionUtil.class) public RestTemplate getRestTemplate () { return new RestTemplate () ; } } ``` 调用RestTemplate时,使用Autowired获取该Bean: ```java @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 版本,因此,本项目采用依赖: ```xml com.alibaba.csp sentinel-apache-dubbo-adapter ``` ##### 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](img/sentinel-dubbo.png) 在 `jf.free.org.ssca.demo.services.Factorial:factorial(int)` 增加 热点限流: ![alt sentinel param flow](img/sentinel-param-flow.png) 整体限流设置了 QPS 100阀值。在高级选项中 添加 针对参数 **10** 的特殊限流,QPS 2。 这样,当计算10的阶乘时,仅允许其QPS=2。可以通过实际测试来验证。 > 类似于 阶乘 这类计算,当 参数值过高时,运算时间也会拉长,假如能在 参数中设置简单的条件,如: `> 100` 就更好了。 #### 7.5.9 Sentinel 底层机制 Sentinel 限流保护 的对象称之为 **资源**。其底层使用如下机制: ```java 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 函数来进行限流之后的处理。示例: ```java // 原本的业务方法. @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结构如: ```json { "ret" : { "code" : 123, "msg" : "Some Error" }, "data": {} } ``` 那么,可以配置: ```yaml 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