
👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。 🎯 本文将围绕Docker这个话题展开,希望能为你带来一些启发或实用的参考。 🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!
文章目录
- Docker Swarm 中的负载均衡与服务发现 🌐⚖️🔍
-
- 一、为什么是 Swarm?不是 Kubernetes?🤔
- 二、核心机制解剖:负载均衡如何工作?⚖️➡️🔄
-
- 2.1 Ingress 负载均衡:IPVS 是幕后英雄 🦸♂️
-
- 流量路径可视化(Mermaid)
- 2.2 内部服务路由:DNS + VIP 的优雅协作 🧩
-
- DNS 解析与路由时序图(Mermaid)
- 三、Java 实战:构建可观察、可伸缩的 Swarm 微服务 🐳☕
-
- 3.1 用户服务(user-service)代码
- 3.2 订单服务(order-service)代码
- 3.3 构建与推送镜像(本地开发机)
- 3.4 在 Swarm 集群中部署服务 🚀
- 3.5 验证负载均衡与服务发现 🧪
-
- 步骤 1:测试 Ingress 负载均衡
- 步骤 2:测试内部服务发现与调用
- 步骤 3:强制故障转移测试 ⚠️
- 四、进阶技巧:超越默认行为 🛠️
-
- 4.1 自定义 IPVS 调度算法(高级)
- 4.2 多端口服务与协议分离 🌐
- 4.3 跨网络服务发现(Bridge + Overlay)🔌
- 4.4 DNS 缓存规避(Java 应用特需)⏳
- 五、生产级最佳实践与避坑指南 🛡️
-
- ✅ 必做项
- ❌ 高危陷阱
- 🔍 排查利器:Swarm 内置诊断命令
- 六、Swarm vs Kubernetes:理性选型对照表 📊
- 七、结语:拥抱简单的力量 🌟
Docker Swarm 中的负载均衡与服务发现 🌐⚖️🔍
在现代云原生架构中,容器编排平台已成为微服务部署与治理的核心基础设施。Docker Swarm 作为 Docker 原生集成、轻量高效、开箱即用的集群编排工具,虽常被 Kubernetes 的光环所遮蔽,却在中小规模生产环境、边缘计算、CI/CD 流水线及快速原型验证场景中持续焕发独特生命力 ✨。其内置的分布式负载均衡器(Ingress Network + IPVS) 与 去中心化服务发现机制(DNS-based + VIP),无需额外部署反向代理或注册中心,即可实现跨节点的服务自动发现、健康感知路由与零配置流量分发——这正是“基础设施即代码”理念最朴素而有力的实践。
本文将深入 Docker Swarm 的网络栈与服务调度内核,系统解析其负载均衡与服务发现的设计哲学、工作原理与实际行为边界;结合真实可运行的 Java 微服务示例(Spring Boot),完整演示从镜像构建、服务部署、跨服务调用、故障注入到弹性恢复的全链路;并通过 Mermaid 图表直观呈现流量路径与 DNS 解析时序;最后探讨生产级注意事项与常见陷阱。全文无抽象理论堆砌,所有结论均基于 Docker Engine v24.0+ 实测验证,代码可直接复用,配置即刻生效 🚀。
一、为什么是 Swarm?不是 Kubernetes?🤔
在“K8s 即默认”的时代,选择 Swarm 并非倒退,而是对简洁性、确定性与运维亲和力的主动选择:
- ✅ 零外部依赖:docker swarm init 一条命令启动管理节点,docker service create 即发布服务,无需 etcd、kube-apiserver、CNI 插件等复杂组件。
- ✅ 内置网络即服务(Network-as-a-Service):Overlay 网络自动加密(–opt encrypted),Ingress 网络原生支持多端口、多协议(HTTP/TCP/UDP)负载均衡。
- ✅ 服务发现即 DNS:每个服务在 tasks.<service-name> 和 <service-name> 两个 DNS 域名下自动注册,无需 Consul/Eureka/ZooKeeper。
- ✅ 声明式但可预测:docker service update –replicas=5 精确控制副本数,滚动更新策略(–update-parallelism, –update-delay)行为完全透明。
- ✅ 资源占用极低:单管理节点内存占用 < 100MB,适合树莓派集群、IoT 边缘网关等资源受限环境。
🔗 想了解 Swarm 在边缘场景的真实落地?可参考 Docker 官方边缘计算白皮书(2023 年发布,全面覆盖 Swarm 在工业网关、车载系统中的实践案例)。
当然,Swarm 不适合需要细粒度 RBAC、自定义调度器、Operator 框架或超大规模(万级 Pod)的场景。但对于 3–50 节点、数十个服务的业务系统,它往往是最快上线、最难出错、最易排查的选择。
二、核心机制解剖:负载均衡如何工作?⚖️➡️🔄
Docker Swarm 的负载均衡分为两个逻辑层级:
| L4(传输层) | Ingress Load Balancing | Linux IPVS(IP Virtual Server) | 所有进入集群的流量(通过 PublishedPort) | ✅ 可选 –publish mode=host 绕过 |
| L7(应用层) | Internal Service Routing | 内置 DNS + VIP + 连接跟踪 | 集群内服务间调用(http://user-service:8080) | ❌ 不支持 HTTP 路由规则(如 path-based) |
2.1 Ingress 负载均衡:IPVS 是幕后英雄 🦸♂️
当执行以下命令时:
docker service create \\
–name web-api \\
–publish published=8080,target=8080,mode=ingress \\
–replicas 3 \\
my-java-app:1.0
Swarm 自动完成三件事:
💡 IPVS 是 Linux 内核提供的高性能四层负载均衡器,比用户态 Nginx/LVS 更低延迟、更高吞吐。Swarm 默认启用 rr(Round Robin)调度算法,也支持 lc(Least Connections)、dh(Destination Hashing)等(需通过 dockerd 启动参数配置)。
流量路径可视化(Mermaid)
#mermaid-svg-1eHP6efCMqFQnGlF{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1eHP6efCMqFQnGlF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1eHP6efCMqFQnGlF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1eHP6efCMqFQnGlF .error-icon{fill:#552222;}#mermaid-svg-1eHP6efCMqFQnGlF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1eHP6efCMqFQnGlF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1eHP6efCMqFQnGlF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1eHP6efCMqFQnGlF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1eHP6efCMqFQnGlF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1eHP6efCMqFQnGlF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1eHP6efCMqFQnGlF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1eHP6efCMqFQnGlF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1eHP6efCMqFQnGlF .marker.cross{stroke:#333333;}#mermaid-svg-1eHP6efCMqFQnGlF svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1eHP6efCMqFQnGlF p{margin:0;}#mermaid-svg-1eHP6efCMqFQnGlF .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-1eHP6efCMqFQnGlF .cluster-label text{fill:#333;}#mermaid-svg-1eHP6efCMqFQnGlF .cluster-label span{color:#333;}#mermaid-svg-1eHP6efCMqFQnGlF .cluster-label span p{background-color:transparent;}#mermaid-svg-1eHP6efCMqFQnGlF .label text,#mermaid-svg-1eHP6efCMqFQnGlF span{fill:#333;color:#333;}#mermaid-svg-1eHP6efCMqFQnGlF .node rect,#mermaid-svg-1eHP6efCMqFQnGlF .node circle,#mermaid-svg-1eHP6efCMqFQnGlF .node ellipse,#mermaid-svg-1eHP6efCMqFQnGlF .node polygon,#mermaid-svg-1eHP6efCMqFQnGlF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1eHP6efCMqFQnGlF .rough-node .label text,#mermaid-svg-1eHP6efCMqFQnGlF .node .label text,#mermaid-svg-1eHP6efCMqFQnGlF .image-shape .label,#mermaid-svg-1eHP6efCMqFQnGlF .icon-shape .label{text-anchor:middle;}#mermaid-svg-1eHP6efCMqFQnGlF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1eHP6efCMqFQnGlF .rough-node .label,#mermaid-svg-1eHP6efCMqFQnGlF .node .label,#mermaid-svg-1eHP6efCMqFQnGlF .image-shape .label,#mermaid-svg-1eHP6efCMqFQnGlF .icon-shape .label{text-align:center;}#mermaid-svg-1eHP6efCMqFQnGlF .node.clickable{cursor:pointer;}#mermaid-svg-1eHP6efCMqFQnGlF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1eHP6efCMqFQnGlF .arrowheadPath{fill:#333333;}#mermaid-svg-1eHP6efCMqFQnGlF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1eHP6efCMqFQnGlF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1eHP6efCMqFQnGlF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1eHP6efCMqFQnGlF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1eHP6efCMqFQnGlF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1eHP6efCMqFQnGlF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1eHP6efCMqFQnGlF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1eHP6efCMqFQnGlF .cluster text{fill:#333;}#mermaid-svg-1eHP6efCMqFQnGlF .cluster span{color:#333;}#mermaid-svg-1eHP6efCMqFQnGlF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1eHP6efCMqFQnGlF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1eHP6efCMqFQnGlF rect.text{fill:none;stroke-width:0;}#mermaid-svg-1eHP6efCMqFQnGlF .icon-shape,#mermaid-svg-1eHP6efCMqFQnGlF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1eHP6efCMqFQnGlF .icon-shape p,#mermaid-svg-1eHP6efCMqFQnGlF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1eHP6efCMqFQnGlF .icon-shape .label rect,#mermaid-svg-1eHP6efCMqFQnGlF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1eHP6efCMqFQnGlF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1eHP6efCMqFQnGlF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1eHP6efCMqFQnGlF :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
客户端请求 http://:8080
Worker/Manager 节点iptables 规则捕获
IPVS VIP: 10.0.0.4:8080
IPVS 调度器 rr
Container-1: 10.0.1.5:8080
Container-2: 10.0.1.6:8080
Container-3: 10.0.1.7:8080
关键点:
- 客户端 无需知道服务实际运行在哪台机器 —— 请求发往任意集群节点 IP 即可被正确转发;
- 若某节点宕机,IPVS 会通过 健康检查(默认 TCP connect) 自动剔除其上的后端容器;
- mode=ingress 是默认模式,也可设为 mode=host 将端口直接映射到宿主机(绕过 IPVS,适用于性能敏感且端口固定的场景)。
2.2 内部服务路由:DNS + VIP 的优雅协作 🧩
当 order-service 需要调用 user-service 时,Java 代码通常这样写:
// Spring RestTemplate 示例
String userUrl = "http://user-service:8080/api/users/123";
ResponseEntity<User> response = restTemplate.getForEntity(userUrl, User.class);
这个看似简单的 URL,背后发生了精密协作:
🔗 深入理解 Docker 内置 DNS 工作机制?推荐阅读 Docker 官方 DNS 文档(权威、实时更新,含 –dns、–dns-search 等高级配置说明)。
DNS 解析与路由时序图(Mermaid)
user-service task-2
user-service task-1
user-service VIP (10.0.2.10)
Docker Embedded DNS
order-service container
user-service task-2
user-service task-1
user-service VIP (10.0.2.10)
Docker Embedded DNS
order-service container
#mermaid-svg-MTxXTedr0T2a5qKx{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MTxXTedr0T2a5qKx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MTxXTedr0T2a5qKx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MTxXTedr0T2a5qKx .error-icon{fill:#552222;}#mermaid-svg-MTxXTedr0T2a5qKx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MTxXTedr0T2a5qKx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MTxXTedr0T2a5qKx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MTxXTedr0T2a5qKx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MTxXTedr0T2a5qKx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MTxXTedr0T2a5qKx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MTxXTedr0T2a5qKx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MTxXTedr0T2a5qKx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MTxXTedr0T2a5qKx .marker.cross{stroke:#333333;}#mermaid-svg-MTxXTedr0T2a5qKx svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MTxXTedr0T2a5qKx p{margin:0;}#mermaid-svg-MTxXTedr0T2a5qKx .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-MTxXTedr0T2a5qKx text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-MTxXTedr0T2a5qKx .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-MTxXTedr0T2a5qKx .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-MTxXTedr0T2a5qKx .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-MTxXTedr0T2a5qKx .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-MTxXTedr0T2a5qKx #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-MTxXTedr0T2a5qKx .sequenceNumber{fill:white;}#mermaid-svg-MTxXTedr0T2a5qKx #sequencenumber{fill:#333;}#mermaid-svg-MTxXTedr0T2a5qKx #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-MTxXTedr0T2a5qKx .messageText{fill:#333;stroke:none;}#mermaid-svg-MTxXTedr0T2a5qKx .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-MTxXTedr0T2a5qKx .labelText,#mermaid-svg-MTxXTedr0T2a5qKx .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-MTxXTedr0T2a5qKx .loopText,#mermaid-svg-MTxXTedr0T2a5qKx .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-MTxXTedr0T2a5qKx .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-MTxXTedr0T2a5qKx .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-MTxXTedr0T2a5qKx .noteText,#mermaid-svg-MTxXTedr0T2a5qKx .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-MTxXTedr0T2a5qKx .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-MTxXTedr0T2a5qKx .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-MTxXTedr0T2a5qKx .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-MTxXTedr0T2a5qKx .actorPopupMenu{position:absolute;}#mermaid-svg-MTxXTedr0T2a5qKx .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-MTxXTedr0T2a5qKx .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-MTxXTedr0T2a5qKx .actor-man circle,#mermaid-svg-MTxXTedr0T2a5qKx line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-MTxXTedr0T2a5qKx :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Connection established
DNS query: user-service
A record: 10.0.2.10
TCP SYN to 10.0.2.10:8080
IPVS forwards (rr)
HTTP GET /api/users/123
Forwarded request
HTTP 200 + JSON
Response back
此机制带来三大优势:
- 客户端无感知扩缩容:user-service 从 1 副本扩到 10 副本,order-service 代码零修改;
- 天然支持健康隔离:若 C1 崩溃,IPVS 在下次连接时自动选择 C2,无需客户端重试逻辑;
- 网络拓扑解耦:服务只需声明依赖,不关心对方物理位置或网络段。
三、Java 实战:构建可观察、可伸缩的 Swarm 微服务 🐳☕
我们构建一个极简但完整的订单-用户双服务系统:
- user-service: 提供 /api/users/{id} 接口,返回模拟用户信息;
- order-service: 提供 /api/orders/{id} 接口,内部调用 user-service 获取下单人信息;
- 所有服务使用 Spring Boot 3.x(基于 Jakarta EE 9+)、Java 17;
- 启用 Actuator 健康检查,确保 Swarm 能正确探测存活状态;
- 日志中打印容器 ID 与节点主机名,便于追踪流量路径。
3.1 用户服务(user-service)代码
// src/main/java/com/example/userservice/UserController.java
package com.example.userservice;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class UserController {
@Value("${HOSTNAME:unknown}")
private String hostname; // 从容器环境变量读取
@GetMapping("/api/users/{id}")
public Map<String, Object> getUser(@PathVariable Long id) {
Map<String, Object> user = new HashMap<>();
user.put("id", id);
user.put("name", "Alice Chen");
user.put("email", "alice@example.com");
user.put("service_instance", hostname); // 标识具体容器
user.put("node_name", System.getenv("NODE_NAME")); // 后续通过 docker service set 注入
return user;
}
}
# src/main/resources/application.yml
server:
port: 8080
management:
endpoint:
health:
show-details: always
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
spring:
application:
name: user–service
✅ 注意:HOSTNAME 是 Docker 容器默认环境变量,值为容器 ID 前 12 位;NODE_NAME 将在部署时通过 –env NODE_NAME={{.Node.Hostname}} 注入,这是 Swarm 模板语法,可动态获取节点主机名。
3.2 订单服务(order-service)代码
// src/main/java/com/example/orderservice/OrderController.java
package com.example.orderservice;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
@RestController
public class OrderController {
@Value("${HOSTNAME:unknown}")
private String hostname;
private final RestTemplate restTemplate = new RestTemplate();
@GetMapping("/api/orders/{id}")
public Map<String, Object> getOrder(@PathVariable Long id) {
Map<String, Object> order = new HashMap<>();
order.put("id", id);
order.put("product", "Cloud Storage Plan");
order.put("amount", 29.99);
order.put("service_instance", hostname);
// ✅ 关键:直接使用服务名 user-service,无需 IP 或端口!
String userUrl = "http://user-service:8080/api/users/1001";
try {
ResponseEntity<Map> userResponse = restTemplate.getForEntity(userUrl, Map.class);
order.put("user", userResponse.getBody());
} catch (Exception e) {
order.put("user_error", "Failed to fetch user: " + e.getMessage());
}
return order;
}
}
# src/main/resources/application.yml
server:
port: 8080
management:
endpoint:
health:
show-details: always
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
spring:
application:
name: order–service
3.3 构建与推送镜像(本地开发机)
# 构建 user-service
cd user-service
./mvnw clean package -DskipTests
docker build -t localhost:5000/user-service:1.0 .
# 构建 order-service
cd ../order-service
./mvnw clean package -DskipTests
docker build -t localhost:5000/order-service:1.0 .
# 推送到本地 registry(简化流程,生产建议用 Harbor/ECR)
docker push localhost:5000/user-service:1.0
docker push localhost:5000/order-service:1.0
🔗 如何搭建轻量本地 registry?官方文档 Deploy a registry server 提供了 3 行命令方案,支持 HTTPS、认证与存储后端配置。
3.4 在 Swarm 集群中部署服务 🚀
假设你已初始化 Swarm(docker swarm init),并添加了至少 2 个 Worker 节点。
# 创建自定义 overlay 网络(显式指定,增强可读性)
docker network create \\
–driver overlay \\
–attachable \\
–subnet 10.0.1.0/24 \\
app-network
# 部署 user-service:3 副本,暴露 8080 端口,健康检查每 10 秒执行一次
docker service create \\
–name user-service \\
–network app-network \\
–publish published=8080,target=8080,mode=ingress \\
–replicas 3 \\
–health-cmd "curl -f http://localhost:8080/actuator/health || exit 1" \\
–health-interval 10s \\
–health-timeout 5s \\
–health-retries 3 \\
–env NODE_NAME={{.Node.Hostname}} \\
localhost:5000/user-service:1.0
# 部署 order-service:2 副本,仅内部访问(不 publish),依赖同一网络
docker service create \\
–name order-service \\
–network app-network \\
–replicas 2 \\
–health-cmd "curl -f http://localhost:8080/actuator/health || exit 1" \\
–health-interval 10s \\
–health-timeout 5s \\
–health-retries 3 \\
–env NODE_NAME={{.Node.Hostname}} \\
localhost:5000/order-service:1.0
✅ 验证服务状态:
docker service ls
# NAME MODE REPLICAS IMAGE
# user-service replicated 3/3 localhost:5000/user-service:1.0
# order-service replicated 2/2 localhost:5000/order-service:1.0
docker service ps user-service
# ID STATUS NODE DESIRED STATE CURRENT STATE
# abc123… Running node-1 Running Running 2 minutes ago
# def456… Running node-2 Running Running 2 minutes ago
# ghi789… Running node-2 Running Running 2 minutes ago
3.5 验证负载均衡与服务发现 🧪
步骤 1:测试 Ingress 负载均衡
在浏览器或 curl 中多次访问任意节点的 http://<any-node-ip>:8080/api/users/1001:
curl http://192.168.1.10:8080/api/users/1001 | jq '.service_instance, .node_name'
# "b8a3c9e7f12d"
# "node-1"
curl http://192.168.1.10:8080/api/users/1001 | jq '.service_instance, .node_name'
# "e4f5a1b2c3d4"
# "node-2"
curl http://192.168.1.10:8080/api/users/1001 | jq '.service_instance, .node_name'
# "a7b8c9d0e1f2"
# "node-2"
✅ 可见:三次请求分别命中不同容器,且分布在两个节点上 —— Ingress LB 正在工作。
步骤 2:测试内部服务发现与调用
访问 order-service 的 Ingress 端口(需先为其发布端口):
docker service update –publish-add published=8081,target=8080 order-service
curl http://192.168.1.10:8081/api/orders/999 | jq '.user.service_instance, .user.node_name'
# "b8a3c9e7f12d"
# "node-1"
curl http://192.168.1.10:8081/api/orders/999 | jq '.user.service_instance, .user.node_name'
# "e4f5a1b2c3d4"
# "node-2"
✅ 可见:order-service 容器成功通过 http://user-service:8080 发起调用,且每次调用的 user-service 实例不同 —— 内部 DNS + VIP 路由正常。
步骤 3:强制故障转移测试 ⚠️
手动 kill 一个 user-service 容器,观察自动恢复:
# 查看当前容器
docker ps –filter "name=user-service" –format "{{.ID}} {{.Names}}"
# Kill 一个(模拟崩溃)
docker kill b8a3c9e7f12d
# 等待 10 秒,检查服务状态
docker service ps user-service
# 你会看到:STATUS 显示 "Starting" → "Running",REPLICAS 仍为 3/3
再次调用 http://192.168.1.10:8080/api/users/1001,响应无缝切换至其他副本 —— 健康检查 + 自动重启保障 SLA。
四、进阶技巧:超越默认行为 🛠️
4.1 自定义 IPVS 调度算法(高级)
Swarm 默认使用 rr(轮询),但在某些场景下需更智能策略。例如:让同一客户端 IP 总是路由到同一后端(会话保持),可启用 sh(Source Hashing):
⚠️ 注意:此操作需在 所有节点 的 dockerd 启动时配置,并重启 daemon。
// /etc/docker/daemon.json
{
"ipvs_scheduler": "sh"
}
然后重启 Docker:
sudo systemctl restart docker
🔗 全面了解 IPVS 调度器选项?请查阅 Linux Virtual Server 官方文档(包含 wrr, wlc, lblcr 等 10+ 算法详解与适用场景)。
4.2 多端口服务与协议分离 🌐
一个服务可能同时提供 HTTP(8080)和 gRPC(9090):
docker service create \\
–name api-gateway \\
–publish published=80,target=8080,mode=ingress \\
–publish published=443,target=8080,mode=ingress \\
–publish published=9090,target=9090,mode=ingress \\
–publish published=9091,target=9091,mode=ingress \\
my-gateway:1.0
Swarm 为每个 –publish 创建独立的 IPVS 规则,互不干扰。gRPC 流量走 9090,HTTP/2 流量走 443,完美隔离。
4.3 跨网络服务发现(Bridge + Overlay)🔌
有时需让传统 VM 上的应用(不在 Swarm 网络)调用 Swarm 服务。此时可:
- 为服务创建 bridge 网络并 –publish mode=host,暴露宿主机端口;
- 或在 ingress 网络上启用 –publish mode=ingress,并确保防火墙放行对应端口;
- 最佳实践:在 Swarm 前部署 Nginx/HAProxy,做 SSL 终止与路由,再反向代理到 ingress VIP。
4.4 DNS 缓存规避(Java 应用特需)⏳
Java 默认对 DNS 结果缓存 无限期(networkaddress.cache.ttl=-1),导致服务扩缩容后旧 IP 仍被复用。必须显式禁用:
在 order-service 的 Dockerfile 中添加:
FROM openjdk:17-jre-slim
# ⚠️ 关键:禁用 JVM DNS 缓存
ENV JAVA_TOOL_OPTIONS="-Dnetworkaddress.cache.ttl=5 -Dnetworkaddress.cache.negative.ttl=1"
COPY target/order-service.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","app.jar"]
- networkaddress.cache.ttl=5:正向 DNS 缓存最多 5 秒;
- networkaddress.cache.negative.ttl=1:负向缓存(NXDOMAIN)仅 1 秒,加速故障发现。
✅ 验证:docker exec -it <order-container> jshell -e "System.out.println(java.security.Security.getProperty('networkaddress.cache.ttl'))" 输出 5。
五、生产级最佳实践与避坑指南 🛡️
✅ 必做项
| 强制健康检查 | Swarm 默认不检查,必须显式 –health-cmd | –health-cmd "curl -f http://localhost:8080/actuator/health" |
| 设置合理的超时 | 避免因网络抖动导致任务反复重启 | –health-interval 10s –health-timeout 3s –health-retries 3 |
| 使用 –restart-condition none | 禁用容器级重启,交由 Swarm 编排决策 | –restart-condition none |
| 限制内存/CPU | 防止单个容器耗尽节点资源 | –limit-memory 512m –limit-cpu 0.5 |
| 启用日志驱动 | 集中收集日志(如 –log-driver json-file –log-opt max-size=10m) | –log-driver fluentd –log-opt fluentd-address=localhost:24224 |
❌ 高危陷阱
| 在 application.yml 中硬编码 user-service 的 IP | 服务无法扩缩,失去弹性 | ✅ 始终使用服务名 http://user-service:8080 |
| 未配置 –health-cmd,仅依赖端口探测 | 容器进程僵死但端口仍通,流量持续打入 | ✅ 必须调用 /actuator/health 等业务健康端点 |
| 在 RestTemplate 中未设置连接/读取超时 | 网络分区时线程池耗尽,雪崩 | ✅ SimpleClientHttpRequestFactory 设置 setConnectTimeout(2000) |
| 忽略 JVM DNS 缓存 | 新增副本后流量数分钟不切入 | ✅ 如前文,强制 networkaddress.cache.ttl=5 |
| 使用 –publish mode=host 但未做端口冲突检查 | 多服务争抢同一端口,部署失败 | ✅ 优先用 mode=ingress;必须用 host 时,–publish 8080:8080 并确认宿主机空闲 |
🔍 排查利器:Swarm 内置诊断命令
# 查看服务详细网络信息(含 VIP、端口映射)
docker service inspect user-service –pretty
# 查看某服务所有任务的实时日志(聚合输出)
docker service logs -f user-service
# 查看某任务容器的底层网络配置(进入容器命名空间)
docker exec -it <task-container-id> ip addr show
# 直接在节点上查询 IPVS 规则(验证 ingress 是否生效)
sudo ipvsadm -Ln
# 输出示例:
# TCP 10.0.0.4:8080 rr
# -> 10.0.1.5:8080 Masq 1 0 0
# -> 10.0.1.6:8080 Masq 1 0 0
六、Swarm vs Kubernetes:理性选型对照表 📊
| 学习曲线 | ⭐⭐☆(1–2 天掌握核心) | ⭐⭐⭐⭐⭐(数周理解核心概念) |
| 安装复杂度 | docker swarm init 一行 | kubeadm init + CNI + kubectl 配置 |
| 服务发现 | 内置 DNS,service-name 直连 | CoreDNS,需 service-name.namespace.svc.cluster.local |
| 负载均衡 | IPVS(L4),开箱即用 | 需 Ingress Controller(Nginx/Traefik)或 Service Type=LoadBalancer |
| 配置管理 | docker config(类似 ConfigMap) | ConfigMap + Secret + Helm |
| 滚动更新 | docker service update,参数清晰 | kubectl rollout,需理解 Deployment/ReplicaSet |
| 可观测性 | 集成 docker service logs/ps/inspect | Prometheus + Grafana + ELK 生态成熟 |
| 社区生态 | 官方维护,插件少但稳定 | 海量 Operator、Helm Charts、Kustomize |
| 适用规模 | ≤ 50 节点,≤ 200 服务 | 百节点以上,千级 Pod |
💡 选型口诀:求稳、求快、求小,选 Swarm;求大、求生态、求未来扩展,选 K8s。 二者并非互斥,许多团队采用“Swarm for Edge, K8s for Cloud”的混合架构。
七、结语:拥抱简单的力量 🌟
Docker Swarm 的负载均衡与服务发现,不是炫技的工程奇迹,而是对 Unix 哲学“做一件事,并做好”的虔诚践行。它没有抽象的 CRD、没有复杂的 Operator、没有 YAML 嵌套地狱;它用 Linux 内核的 IPVS、用 DNS 协议的古老智慧、用容器网络的原生能力,构建出一个可靠、可预测、可调试的服务网格雏形。
对于 Java 开发者而言,这意味着:
- 你不必成为网络工程师,也能写出高可用微服务;
- 你无需引入 Spring Cloud Netflix(已停更)或 Alibaba Nacos,仅靠 http://service-name 就能享受服务发现;
- 你不用为 Ribbon、Feign 的超时配置焦头烂额,IPVS 的毫秒级故障转移就是终极熔断。
技术的价值,不在于它有多复杂,而在于它能否让开发者专注业务,而非基础设施。Swarm 正是这样一位沉默而可靠的伙伴 —— 它不喧哗,却始终在线;它不张扬,却支撑着无数生产系统的平稳心跳 ❤️。
🔗 想系统学习 Swarm 运维?Docker 官方免费课程 Swarm Mode Fundamentals 提供交互式终端实验,涵盖网络、安全、更新策略全部核心主题。
现在,就打开你的终端,输入 docker swarm init —— 让简单,重新开始 🐳✨。
🙌 感谢你读到这里! 🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。 💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友! 💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿 🔔 关注我,不错过下一篇干货!我们下期再见!✨




