第 9 章:Docker 与微服务
9.1 Docker 中的 NNG 通信
在容器化环境中,nanomsg / NNG 的轻量特性非常适合微服务间通信。
9.1.1 容器间通信方式
| 方式 | 地址格式 | 场景 | 性能 |
|---|---|---|---|
| TCP(Docker 网络) | tcp://container:port | 跨容器 | 中 |
| Unix Socket(卷挂载) | ipc:///shared/app.sock | 同机容器 | 高 |
| inproc | inproc://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 指令 |
| 最小基础镜像 | 使用 alpine 或 distroless |
| 只读文件系统 | --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 章:最佳实践与总结