强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 9 章:Docker 与微服务

9.1 Docker 中的 NNG 通信

在容器化环境中,nanomsg / NNG 的轻量特性非常适合微服务间通信。

9.1.1 容器间通信方式

方式地址格式场景性能
TCP(Docker 网络)tcp://container:port跨容器
Unix Socket(卷挂载)ipc:///shared/app.sock同机容器
inprocinproc://name容器内线程最高

9.1.2 Docker 网络模型

┌─────────────────────────────────────────┐
│              Docker Host                │
│                                         │
│  ┌──────────┐    bridge    ┌──────────┐ │
│  │ Container│◄────────────►│ Container│ │
│  │   A      │  172.17.0.x  │    B     │ │
│  │ tcp:5555 │              │ tcp:5555 │ │
│  └──────────┘              └──────────┘ │
│       ▲                        ▲        │
│       │     ┌──────────┐      │        │
│       └────►│ Container│◄─────┘        │
│             │    C     │               │
│             │ tcp:5555 │               │
│             └──────────┘               │
└─────────────────────────────────────────┘

9.2 基础 Dockerfile

9.2.1 NNG 应用的 Dockerfile

# 构建阶段
FROM ubuntu:22.04 AS builder

RUN apt-get update && apt-get install -y \
    build-essential \
    cmake \
    git \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# 编译 NNG
RUN git clone --depth 1 --branch v1.8.0 https://github.com/nanomsg/nng.git /src/nng \
    && cd /src/nng \
    && mkdir build && cd build \
    && cmake .. -DCMAKE_BUILD_TYPE=Release \
                -DNNG_TESTS=OFF \
                -DNNG_TOOLS=OFF \
    && make -j$(nproc) \
    && make install

# 复制并编译应用
WORKDIR /src/app
COPY . .
RUN cc main.c -lnng -o app

# 运行阶段
FROM ubuntu:22.04

RUN apt-get update && apt-get install -y \
    libssl3 \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*

COPY --from=builder /usr/local/lib/libnng* /usr/local/lib/
COPY --from=builder /usr/local/include/nng /usr/local/include/nng
COPY --from=builder /src/app/app /usr/local/bin/app

RUN ldconfig

EXPOSE 5555

CMD ["app"]

9.2.2 多阶段构建优势

优势说明
镜像体积小运行镜像只包含运行时依赖
安全性不包含编译工具和源码
构建缓存NNG 编译层可缓存

9.3 Docker Compose 编排

9.3.1 微服务架构示例

┌──────────────────────────────────────────────┐
│               Docker Compose                 │
│                                              │
│  ┌─────────┐  REQ/REP  ┌─────────┐          │
│  │ gateway  │──────────►│  users   │          │
│  │ :5555   │           │ :5556   │          │
│  └────┬────┘           └─────────┘          │
│       │                                      │
│       │ PUSH/PULL    ┌─────────┐             │
│       └──────────────►│  workers │             │
│                       │ :5557   │ ×N          │
│                       └─────────┘             │
└──────────────────────────────────────────────┘

9.3.2 docker-compose.yml

version: "3.8"

services:
  gateway:
    build: ./gateway
    ports:
      - "5555:5555"    # 对外暴露 TCP
    depends_on:
      - users
      - workers
    networks:
      - nng-net

  users:
    build: ./users
    expose:
      - "5556"         # 仅内部可达
    networks:
      - nng-net

  workers:
    build: ./workers
    deploy:
      replicas: 3
    expose:
      - "5557"
    networks:
      - nng-net

networks:
  nng-net:
    driver: bridge

9.3.3 Gateway 服务示例(REQ/REP)

// gateway/main.c
#include <nng/nng.h>
#include <nng/protocol/reqrep0/rep.h>
#include <nng/protocol/reqrep0/req.h>
#include <stdio.h>
#include <string.h>

int main() {
    nng_socket frontend, backend;
    int rv;

    // 前端:接收外部请求
    nng_rep0_open(&frontend);
    nng_setopt_ms(frontend, NNG_OPT_RECVTIMEO, 1000);
    nng_listen(frontend, "tcp://0.0.0.0:5555", NULL, 0);

    // 后端:转发到用户服务
    nng_req0_open(&backend);
    nng_dial(backend, "tcp://users:5556", NULL, 0);

    printf("Gateway started on port 5555\n");

    while (1) {
        char *buf = NULL;
        size_t sz;
        rv = nng_recv(frontend, &buf, &sz, NNG_FLAG_ALLOC);
        if (rv == NNG_ETIMEDOUT) continue;
        if (rv != 0) break;

        printf("Gateway received: %.*s\n", (int)sz, buf);

        // 转发到后端
        nng_send(backend, buf, sz, 0);
        nng_free(buf, sz);

        // 接收后端响应
        char *reply = NULL;
        size_t rsz;
        if (nng_recv(backend, &reply, &rsz, NNG_FLAG_ALLOC) == 0) {
            // 返回给前端
            nng_send(frontend, reply, rsz, 0);
            nng_free(reply, rsz);
        }
    }

    nng_close(frontend);
    nng_close(backend);
    return 0;
}

9.3.4 Worker 服务示例(PULL/PUSH)

// workers/main.c
#include <nng/nng.h>
#include <nng/protocol/pipeline0/pull.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main() {
    nng_socket pull;
    int rv;

    nng_pull0_open(&pull);
    nng_setopt_ms(pull, NNG_OPT_RECVTIMEO, 1000);

    if ((rv = nng_listen(pull, "tcp://0.0.0.0:5557", NULL, 0)) != 0) {
        fprintf(stderr, "nng_listen: %s\n", nng_strerror(rv));
        return 1;
    }

    printf("Worker started on port 5557\n");

    while (1) {
        char *buf = NULL;
        size_t sz;
        rv = nng_recv(pull, &buf, &sz, NNG_FLAG_ALLOC);
        if (rv == NNG_ETIMEDOUT) continue;
        if (rv != 0) break;

        printf("Worker processing: %.*s\n", (int)sz, buf);
        nng_free(buf, sz);

        // 模拟处理
        usleep(100000);
    }

    nng_close(pull);
    return 0;
}

9.4 容器间 IPC(Unix Socket)

当多个容器运行在同一主机上时,可以通过共享卷使用 Unix Socket 进行 IPC:

9.4.1 docker-compose.yml(IPC 模式)

version: "3.8"

services:
  server:
    build: ./server
    volumes:
      - nng-sockets:/var/run/nng
    command: ["app", "--socket", "ipc:///var/run/nng/app.sock"]

  client:
    build: ./client
    volumes:
      - nng-sockets:/var/run/nng
    command: ["app", "--socket", "ipc:///var/run/nng/app.sock"]
    depends_on:
      - server

volumes:
  nng-sockets:
    driver: local

注意:共享卷方式的 Unix Socket 只在 Docker Desktop 的 Linux 容器模式下工作良好。macOS 和 Windows 的 Docker Desktop 使用虚拟机,Unix Socket 路径可能不通。


9.5 网络配置

9.5.1 Docker 网络模式

模式说明NNG 适用性
bridge默认,容器间通过虚拟网桥通信✅ 推荐
host使用宿主机网络✅ 性能最佳
overlay跨主机容器通信✅ Swarm/K8s
macvlan容器有独立 MAC 地址✅ 需要直连网络
none无网络❌ 无法通信

9.5.2 使用 host 网络提升性能

version: "3.8"

services:
  high-perf-server:
    build: ./server
    network_mode: host  # 使用宿主机网络
    command: ["app", "--bind", "tcp://0.0.0.0:5555"]

使用 host 网络模式时:

  • 容器直接使用宿主机的网络栈
  • 省去 NAT 和网桥的开销
  • 延迟降低约 20-30%
  • 端口直接映射到宿主机

9.5.3 端口映射

services:
  server:
    ports:
      - "5555:5555"        # TCP: 宿主端口:容器端口
      - "5555:5555/udp"    # 如果需要 UDP

9.5.4 DNS 服务发现

Docker Compose 自动为每个服务创建 DNS 记录:

# 容器内可以直接使用服务名作为主机名
nng_dial(sock, "tcp://users:5556", NULL, 0);
nng_dial(sock, "tcp://workers:5557", NULL, 0);

9.6 高级编排模式

9.6.1 PUB/SUB 广播模式

version: "3.8"

services:
  event-bus:
    build: ./event-bus
    expose:
      - "5560"    # PUB 端口
    networks:
      - events

  service-a:
    build: ./service-a
    depends_on:
      - event-bus
    networks:
      - events

  service-b:
    build: ./service-b
    depends_on:
      - event-bus
    networks:
      - events

networks:
  events:
    driver: bridge

9.6.2 负载均衡 + 任务队列

version: "3.8"

services:
  task-producer:
    build: ./producer
    expose:
      - "5558"
    deploy:
      replicas: 2
    networks:
      - tasks

  task-worker:
    build: ./worker
    deploy:
      replicas: 5      # 5 个 worker 处理任务
      resources:
        limits:
          cpus: "0.5"
          memory: 256M
    networks:
      - tasks

networks:
  tasks:
    driver: bridge

9.6.3 NNG Proxy(代理/桥接)

// proxy/main.c —— 使用 nn_device 或 nng 实现消息代理
#include <nng/nng.h>
#include <nng/protocol/pubsub0/pub.h>
#include <nng/protocol/pubsub0/sub.h>
#include <stdio.h>
#include <string.h>

int main() {
    nng_socket front, back;

    // 前端:订阅者连接(PUB 端)
    nng_pub0_open(&front);
    nng_listen(front, "tcp://0.0.0.0:5560", NULL, 0);

    // 后端:发布者连接(SUB 端)
    nng_sub0_open(&back);
    nng_setopt(back, NNG_OPT_SUB_SUBSCRIBE, "", 0);
    nng_listen(back, "tcp://0.0.0.0:5561", NULL, 0);

    printf("Proxy: SUB:5561 -> PUB:5560\n");

    // 转发消息
    while (1) {
        char *buf = NULL;
        size_t sz;
        if (nng_recv(back, &buf, &sz, NNG_FLAG_ALLOC) == 0) {
            nng_send(front, buf, sz, 0);
            nng_free(buf, sz);
        }
    }

    nng_close(front);
    nng_close(back);
    return 0;
}

9.7 Kubernetes 部署

9.7.1 Deployment YAML

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nng-server
  labels:
    app: nng-server
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nng-server
  template:
    metadata:
      labels:
        app: nng-server
    spec:
      containers:
        - name: server
          image: myregistry/nng-server:latest
          ports:
            - containerPort: 5555
              name: nng-tcp
          resources:
            requests:
              memory: "64Mi"
              cpu: "100m"
            limits:
              memory: "128Mi"
              cpu: "200m"
          livenessProbe:
            exec:
              command:
                - /bin/sh
                - -c
                - "nngcat --req --dial tcp://localhost:5555 --data 'ping' --recv-timeout 2000"
            initialDelaySeconds: 5
            periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: nng-server
spec:
  selector:
    app: nng-server
  ports:
    - port: 5555
      targetPort: 5555
      name: nng-tcp
  type: ClusterIP

9.7.2 服务发现

在 Kubernetes 中,服务通过 DNS 名称访问:

// 连接到 Kubernetes Service
nng_dial(sock, "tcp://nng-server:5555", NULL, 0);

// 或连接到 headless service(获取所有 Pod IP)
nng_dial(sock, "tcp://nng-server-headless:5555", NULL, 0);

9.7.3 Headless Service(多 Pod 直连)

apiVersion: v1
kind: Service
metadata:
  name: nng-server-headless
spec:
  clusterIP: None    # Headless
  selector:
    app: nng-server
  ports:
    - port: 5555

使用 Headless Service 时,DNS 解析会返回所有 Pod 的 IP,NNG 的 REQ/REP 可以自动负载均衡到多个 Pod。


9.8 健康检查与监控

9.8.1 健康检查端点

// health_check.c —— 简单的健康检查实现
#include <nng/nng.h>
#include <nng/protocol/reqrep0/rep.h>
#include <stdio.h>
#include <string.h>

int main() {
    nng_socket health;
    nng_rep0_open(&health);
    nng_listen(health, "tcp://0.0.0.0:5599", NULL, 0);

    printf("Health check on port 5599\n");

    while (1) {
        char *buf = NULL;
        size_t sz;
        if (nng_recv(health, &buf, &sz, NNG_FLAG_ALLOC) == 0) {
            nng_free(buf, sz);
            const char *status = "{\"status\":\"ok\"}";
            nng_send(health, (void *)status, strlen(status), 0);
        }
    }

    nng_close(health);
    return 0;
}

9.8.2 Docker 健康检查

HEALTHCHECK --interval=10s --timeout=3s --retries=3 \
    CMD nngcat --req --dial tcp://localhost:5599 \
              --data 'health' --recv-timeout 2000 || exit 1

9.9 安全注意事项

9.9.1 容器安全

实践说明
非 root 用户Dockerfile 中使用 USER 指令
最小基础镜像使用 alpinedistroless
只读文件系统--read-only 挂载 tmpfs
网络隔离仅连接必要的网络
TLS容器间通信使用 TLS
# 非 root 用户运行
RUN useradd -r -s /bin/false appuser
USER appuser

# 使用最小镜像
FROM gcr.io/distroless/cc-debian12
COPY --from=builder /src/app/app /app
CMD ["/app"]

9.10 注意事项

DNS 解析延迟:首次 DNS 解析可能有延迟,建议在启动时预热连接(使用 NNG 的自动重连机制)。

容器重启:容器重启后 TCP 连接会断开,NNG 的 Dialer 会自动重连,但 Listener 需要其他端重新连接。

日志收集:容器中的 stdout/stderr 会被 Docker 收集,通过 docker logs 查看。


9.11 扩展阅读


上一章第 8 章:IPC 与进程间通信 | 下一章第 10 章:最佳实践与总结