聊聊Docker - Swarm中的负载均衡与服务发现

这两天一直在研究这个话题,踩了几个坑,把遇到的东西整理成文,供有需要的朋友参考。

👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。 🎯 本文将围绕Docker这个话题展开,希望能为你带来一些启发或实用的参考。 🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!

文章目录

2.2 内部服务路由:DNS + VIP 的优雅协作 🧩 三、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. 和 `` 两个 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 BalancingLinux 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 自动完成三件事:

1. 在所有 Worker 节点(包括 Manager)的 ingress 网络上绑定 0.0.0.0:8080
2. 创建一个 VIP(Virtual IP),举个例子 10.0.0.4,该 IP 仅存在于 ingress 网络的 Linux network namespace 中;
3. 使用 IPVS 内核模块 在 VIP 下注册 3 个真实后端(Real Servers)——即 3 个 web-api 任务的容器 IP + 端口。

💡 IPVS 是 Linux 内核给出的高性能四层负载均衡器,比用户态 Nginx/LVS 更低延迟、更高吞吐。Swarm 默认启用 rr(Round Robin)调度算法,也支持 lc(Least Connections)、dh(Destination Hashing)等(需通过 dockerd 启动参数配置)。
流量路径可视化(Mermaid)

客户端请求 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 response = restTemplate.getForEntity(userUrl, User.class);

这个看似简单的 URL,背后发生了精密协作:

1. order-service 容器内发起 DNS 查询:dig user-service.default.svc.cluster.local(Swarm 使用 default.svc.cluster.local 作为默认搜索域);
2. 内置 DNS 服务器(dockerd 进程托管)返回 user-serviceVIP 地址(如 10.0.2.10),而非某个具体容器 IP;
3. 容器通过 10.0.2.10:8080 发起连接 → 流量进入 user-service 的 overlay 网络;
4. overlay 网络的 VXLAN 封装将数据包路由至任一运行 user-service 任务的节点;
5. 该节点的 IPVS(或 netfilter conntrack)将 VIP 流量分发给本地的一个 user-service 容器。

🔗 深入理解 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

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 getUser(@PathVariable Long id) {
        Map 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 getOrder(@PathVariable Long id) {
        Map 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 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-serviceDockerfile 中添加:

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  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:理性选型对照表 📊

维度Docker SwarmKubernetes学习曲线⭐⭐☆(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/inspectPrometheus + 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 —— 让简单,重新开始 🐳✨。


🙌

以上就是这次整理的全部内容,希望对你有所启发。如果有不同见解,欢迎在评论区交流讨论。

评论 (0)

暂无评论