欢迎光临
我们一直在努力

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

在这里插入图片描述

👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。 🎯 本文将围绕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 自动完成三件事:

  • 在所有 Worker 节点(包括 Manager)的 ingress 网络上绑定 0.0.0.0:8080;
  • 创建一个 VIP(Virtual IP),例如 10.0.0.4,该 IP 仅存在于 ingress 网络的 Linux network namespace 中;
  • 使用 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)

    #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,背后发生了精密协作:

  • order-service 容器内发起 DNS 查询:dig user-service.default.svc.cluster.local(Swarm 使用 default.svc.cluster.local 作为默认搜索域);
  • 内置 DNS 服务器(dockerd 进程托管)返回 user-service 的 VIP 地址(如 10.0.2.10),而非某个具体容器 IP;
  • 容器通过 10.0.2.10:8080 发起连接 → 流量进入 user-service 的 overlay 网络;
  • overlay 网络的 VXLAN 封装将数据包路由至任一运行 user-service 任务的节点;
  • 该节点的 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

    #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: userservice

    ✅ 注意: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: orderservice

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

    维度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/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 —— 让简单,重新开始 🐳✨。


    🙌 感谢你读到这里! 🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。 💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友! 💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿 🔔 关注我,不错过下一篇干货!我们下期再见!✨

    赞(0)
    未经允许不得转载:171主机测评 » Docker - Swarm中的负载均衡与服务发现
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址