nanomsg / NNG 消息库完全教程 / 第 6 章:可扩展性与性能
6.1 性能特征概述
nanomsg / NNG 的设计目标是低延迟、低内存、高吞吐。相比 ZeroMQ,它们在轻量级场景下有明显优势。
6.1.1 设计对性能的影响
| 设计决策 |
性能影响 |
| 纯 C 实现 |
无 GC 开销,启动快 |
| 无外部依赖 |
无额外内存/线程开销 |
| 事件驱动 I/O |
高并发下低 CPU 占用 |
| 零拷贝消息 |
减少内存拷贝,适合大消息 |
| 紧凑协议头 |
低带宽占用 |
6.2 延迟特征
6.2.1 延迟组成
应用层延迟 ──┬── 序列化/反序列化
├── 协议层处理
├── 操作系统内核 ── TCP 栈 / IPC
└── 网络传输 (TCP 场景)
6.2.2 典型延迟数据
以下数据基于 Linux 5.x、千兆网卡、x86_64 平台:
| 场景 |
延迟 (P50) |
延迟 (P99) |
说明 |
| inproc (线程间) |
~1 μs |
~5 μs |
共享内存,零拷贝 |
| IPC (Unix Socket) |
~10 μs |
~50 μs |
本地 Socket |
| TCP (localhost) |
~30 μs |
~100 μs |
回环网络 |
| TCP (局域网) |
~100 μs |
~500 μs |
千兆以太网 |
| TCP (跨数据中心) |
~5 ms |
~20 ms |
取决于物理距离 |
以上数据仅供参考,实际性能取决于硬件、OS 配置、消息大小等因素。
6.2.3 降低延迟的技巧
| 技巧 |
效果 |
适用场景 |
| 使用 inproc 传输 |
延迟降至 ~1 μs |
同进程线程间通信 |
| 启用 TCP_NODELAY |
消除 Nagle 延迟 |
低延迟要求的 RPC |
| 使用零拷贝 |
减少内存拷贝 |
大消息 (>1KB) |
| 减小消息大小 |
降低序列化开销 |
所有场景 |
| 预分配缓冲区 |
避免运行时分配 |
高频消息 |
// 启用 TCP_NODELAY
int nodelay = 1;
nng_setopt(sock, NNG_OPT_TCP_NODELAY, &nodelay, sizeof(nodelay));
// 启用 TCP Keepalive
int keepalive = 1;
nng_setopt(sock, NNG_OPT_TCP_KEEPALIVE, &keepalive, sizeof(keepalive));
6.3 吞吐量特征
6.3.1 吞吐量衡量指标
| 指标 |
说明 |
单位 |
| Messages/sec (msg/s) |
每秒处理消息数 |
msg/s |
| Throughput (MB/s) |
每秒数据量 |
MB/s |
| Connection scalability |
最大并发连接数 |
connections |
6.3.2 典型吞吐量
| 传输方式 |
小消息 (64B) |
中消息 (1KB) |
大消息 (64KB) |
| inproc |
~5M msg/s |
~3M msg/s |
~500K msg/s |
| IPC |
~500K msg/s |
~400K msg/s |
~200K msg/s |
| TCP (localhost) |
~300K msg/s |
~250K msg/s |
~150K msg/s |
| TCP (局域网) |
~200K msg/s |
~180K msg/s |
~100K msg/s |
6.3.3 消息大小与吞吐量的关系
吞吐量 (MB/s)
│
│ ╭───────────────────
│ ╱
│ ╱
│ ╱
│ ╱
│───╯
│
└──────────────────────────── 消息大小 (bytes)
64 256 1K 4K 16K 64K
小消息: 协议开销占比高,吞吐量受 msg/s 限制
大消息: 数据占比高,吞吐量受带宽限制
6.4 连接数扩展
6.4.1 连接数上限
nanomsg / NNG 的连接数上限主要受操作系统限制:
| 资源 |
nanomsg |
NNG |
瓶颈 |
| 文件描述符 |
受系统 ulimit |
受系统 ulimit |
ulimit -n |
| 内存 |
~4KB/连接 |
~2KB/连接 |
系统内存 |
| 线程 |
2 个内部线程 |
线程池 (可配置) |
CPU 核心数 |
6.4.2 调整系统限制
# 查看当前限制
ulimit -n
# 临时增加(当前 shell)
ulimit -n 65536
# 永久增加(/etc/security/limits.conf)
# * soft nofile 65536
# * hard nofile 65536
# sysctl 调优
sysctl -w net.core.somaxconn=65535
sysctl -w net.ipv4.tcp_max_syn_backlog=65535
6.4.3 NNG 线程池配置
// NNG 默认使用 4 个 I/O 线程
// 可在程序启动前设置环境变量
// NNG_NUM_TASKQ_THREADS=8
// 或在代码中设置
// (NNG 内部使用 nng_taskq_set_threads 但非公开 API)
环境变量方式:
export NNG_NUM_TASKQ_THREADS=8
./myapp
6.5 内存使用
6.5.1 内存组成
总内存 = Socket 基础内存
+ 连接数 × 每连接内存
+ 发送缓冲区 × 缓冲区大小
+ 接收缓冲区 × 缓冲区大小
+ 消息队列 × 消息大小
6.5.2 内存占用估算
| 组件 |
nanomsg |
NNG |
| Socket 基础 |
~2 KB |
~4 KB |
| 每连接开销 |
~2 KB |
~2 KB |
| 发送缓冲区 (默认) |
128 KB |
128 条消息 |
| 接收缓冲区 (默认) |
128 KB |
128 条消息 |
估算示例: 100 个连接,每个 Socket 128 条消息缓冲
| 场景 |
估算内存 |
| nanomsg |
2KB + 100×2KB + 256KB ≈ 458 KB |
| NNG |
4KB + 100×2KB + 256×1KB ≈ 460 KB |
6.5.3 减少内存使用
// 减小缓冲区队列
int bufsize = 16; // 默认 128
nng_setopt(sock, NNG_OPT_RECVBUF, &bufsize, sizeof(bufsize));
nng_setopt(sock, NNG_OPT_SENDBUF, &bufsize, sizeof(bufsize));
// 限制最大接收消息大小
size_t maxsz = 4096; // 默认 1MB
nng_setopt(sock, NNG_OPT_RECVMAXSZ, &maxsz, sizeof(maxsz));
6.5.4 消息内存管理
// nanomsg 零拷贝
void *msg = nn_allocmsg(1024, 0);
memcpy(msg, data, 1024);
nn_send(sock, &msg, NN_MSG, 0);
// 发送后不要释放 msg
// NNG 零拷贝
nng_msg *msg;
nng_msg_alloc(&msg, 1024);
memcpy(nng_msg_body(msg), data, 1024);
nng_sendmsg(sock, msg, 0);
// 发送后不要释放 msg
6.6 消息大小
6.6.1 默认限制
| 库 |
默认最大消息 |
可配置上限 |
| nanomsg |
1 MB |
无硬限制 |
| NNG |
1 MB |
NNG_OPT_RECVMAXSZ |
6.6.2 大消息处理策略
| 消息大小 |
推荐策略 |
说明 |
| < 1 KB |
直接发送 |
性能最优 |
| 1 KB - 64 KB |
直接发送 + 零拷贝 |
减少拷贝开销 |
| 64 KB - 1 MB |
调整缓冲区 + 零拷贝 |
需增大缓冲区 |
| > 1 MB |
应用层分片 |
避免单消息过大 |
6.6.3 应用层分片
#define CHUNK_SIZE 65536 // 64KB
int send_large_message(nng_socket sock, const void *data, size_t len) {
size_t offset = 0;
while (offset < len) {
size_t chunk = (len - offset > CHUNK_SIZE) ? CHUNK_SIZE : (len - offset);
// 构造消息: [4字节总长][4字节偏移][数据]
nng_msg *msg;
nng_msg_alloc(&msg, 8 + chunk);
void *body = nng_msg_body(msg);
uint32_t total = (uint32_t)len;
uint32_t off = (uint32_t)offset;
memcpy(body, &total, 4);
memcpy(body + 4, &off, 4);
memcpy(body + 8, data + offset, chunk);
int rv = nng_sendmsg(sock, msg, 0);
if (rv != 0) return rv;
offset += chunk;
}
return 0;
}
6.7 基准测试
6.7.1 使用 nngcat 测试
NNG 自带的 nngcat 工具可用于快速基准测试:
# 终端 1:启动服务端
nngcat --rep --listen tcp://*:5555 --count 10000
# 终端 2:运行客户端(发送 10000 条消息)
time nngcat --req --dial tcp://localhost:5555 \
--data "Hello" --count 10000 --interval 0
6.7.2 自定义延迟基准测试
// bench_latency.c —— REQ/REP 往返延迟测试
#include <nng/nng.h>
#include <nng/protocol/reqrep0/req.h>
#include <nng/protocol/reqrep0/rep.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/time.h>
#include <pthread.h>
#define MSG_SIZE 64
#define ITERATIONS 10000
static double now_ms(void) {
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec * 1000.0 + tv.tv_usec / 1000.0;
}
void *server_thread(void *arg) {
nng_socket sock;
nng_rep0_open(&sock);
nng_listen(sock, "inproc://bench", NULL, 0);
for (int i = 0; i < ITERATIONS; i++) {
char buf[MSG_SIZE];
size_t sz = sizeof(buf);
nng_recv(sock, buf, &sz, 0);
nng_send(sock, buf, sz, 0);
}
nng_close(sock);
return NULL;
}
int main() {
pthread_t srv;
pthread_create(&srv, NULL, server_thread, NULL);
// 等待服务端就绪
usleep(100000);
nng_socket sock;
nng_req0_open(&sock);
nng_dial(sock, "inproc://bench", NULL, 0);
char data[MSG_SIZE];
memset(data, 'A', MSG_SIZE);
double start = now_ms();
for (int i = 0; i < ITERATIONS; i++) {
nng_send(sock, data, MSG_SIZE, 0);
char buf[MSG_SIZE];
size_t sz = sizeof(buf);
nng_recv(sock, buf, &sz, 0);
}
double elapsed = now_ms() - start;
printf("Iterations: %d\n", ITERATIONS);
printf("Message size: %d bytes\n", MSG_SIZE);
printf("Total time: %.2f ms\n", elapsed);
printf("RTT avg: %.3f μs\n", elapsed * 1000.0 / ITERATIONS);
printf("Throughput: %.0f msg/s\n", ITERATIONS / (elapsed / 1000.0));
nng_close(sock);
pthread_join(srv, NULL);
return 0;
}
cc bench_latency.c -lnng -lpthread -o bench_latency
./bench_latency
6.7.3 自定义吞吐量基准测试
// bench_throughput.c —— PUB/SUB 吞吐量测试
#include <nng/nng.h>
#include <nng/protocol/pubsub0/pub.h>
#include <nng/protocol/pubsub0/sub.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#include <pthread.h>
#define MSG_COUNT 100000
#define MSG_SIZE 256
static double now_ms(void) {
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec * 1000.0 + tv.tv_usec / 1000.0;
}
void *publisher(void *arg) {
nng_socket *sock = (nng_socket *)arg;
// 等待订阅者连接
usleep(500000);
char data[MSG_SIZE];
memset(data, 'B', MSG_SIZE);
double start = now_ms();
for (int i = 0; i < MSG_COUNT; i++) {
nng_send(*sock, data, MSG_SIZE, 0);
}
double elapsed = now_ms() - start;
printf("Published %d messages in %.2f ms\n", MSG_COUNT, elapsed);
printf("Throughput: %.0f msg/s (%.2f MB/s)\n",
MSG_COUNT / (elapsed / 1000.0),
(double)MSG_COUNT * MSG_SIZE / elapsed / 1000.0);
return NULL;
}
int main() {
nng_socket pub, sub;
pthread_t pub_thread;
nng_pub0_open(&pub);
nng_sub0_open(&sub);
nng_setopt_ms(sub, NNG_OPT_RECVTIMEO, 10000);
nng_setopt(sub, NNG_OPT_SUB_SUBSCRIBE, "", 0);
nng_listen(pub, "inproc://throughput", NULL, 0);
nng_dial(sub, "inproc://throughput", NULL, 0);
pthread_create(&pub_thread, NULL, publisher, &pub);
int received = 0;
char buf[MSG_SIZE];
size_t sz;
while (received < MSG_COUNT) {
sz = sizeof(buf);
if (nng_recv(sub, buf, &sz, 0) == 0) {
received++;
}
}
printf("Received %d messages\n", received);
nng_close(pub);
nng_close(sub);
pthread_join(pub_thread, NULL);
return 0;
}
6.8 性能调优清单
6.8.1 传输层选择
| 优先级 |
传输方式 |
适用场景 |
| 1 |
inproc |
同进程线程间 |
| 2 |
IPC (Unix Socket) |
同机进程间 |
| 3 |
TCP |
跨机器通信 |
6.8.2 调优参数
| 参数 |
调优方向 |
说明 |
NNG_OPT_TCP_NODELAY |
设为 1 |
消除 Nagle 算法延迟 |
NNG_OPT_RECVBUF |
增大 |
提高接收吞吐 |
NNG_OPT_SENDBUF |
增大 |
提高发送吞吐 |
NNG_OPT_RECVMAXSZ |
按需调整 |
防止过大消息 |
NNG_OPT_RECONNMINT |
按需调整 |
控制重连频率 |
6.8.3 操作系统调优
# 增大文件描述符限制
ulimit -n 65536
# 增大 TCP 缓冲区
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216
sysctl -w net.ipv4.tcp_rmem="4096 131072 16777216"
sysctl -w net.ipv4.tcp_wmem="4096 131072 16777216"
# 启用 TCP Fast Open
sysctl -w net.ipv4.tcp_fastopen=3
# 调整 TCP 拥塞算法
sysctl -w net.ipv4.tcp_congestion_control=bbr
6.9 与 ZeroMQ 性能对比
6.9.1 典型对比数据
| 指标 |
nanomsg |
NNG |
ZeroMQ |
| inproc RTT |
~1.5 μs |
~1.2 μs |
~1.0 μs |
| TCP RTT (localhost) |
~35 μs |
~30 μs |
~25 μs |
| TCP 吞吐 (1KB) |
~400K msg/s |
~450K msg/s |
~500K msg/s |
| 内存/连接 |
~4 KB |
~4 KB |
~8 KB |
| 启动时间 |
< 1 ms |
< 1 ms |
~5 ms |
ZeroMQ 在纯性能上略优,但 NNG 在内存占用和启动速度上更优。
6.9.2 何时性能差异重要
- 嵌入式设备:NNG 内存优势明显
- 大规模连接(>10K):NNG 内存优势显著
- 超低延迟(<10 μs):差异不大,取决于传输选择
- 批量数据传输:性能差异可忽略
6.10 注意事项
基准测试环境:性能数据高度依赖环境。务必在目标硬件上进行基准测试,不要直接引用文档数据。
消息大小分布:实际应用中的消息大小通常不均匀,基准测试应模拟真实分布。
协议开销:不同协议的开销不同。REQ/REP 需要维护请求-响应状态,PUB/SUB 有主题过滤开销。
6.11 扩展阅读
上一章:第 5 章:NNG 现代 API 详解 | 下一章:第 7 章:TLS 与安全通信