网络I/O优化:减少延迟与带宽消耗

网络I/O,说白了就是我们SDK跟远端服务器通信的那条路。这条路顺不顺,直接影响吞吐量。我见过太多项目,代码写得挺漂亮,结果一跑线上,全卡在网络I/O上了。今天咱们就聊聊,怎么把这条路打通。

我个人习惯把网络I/O优化分成四个维度:连接管理、数据压缩、协议选择、流量整形。下面一个一个拆开说。

网络I/O优化方法论框架 网络I/O优化目标 连接管理 数据压缩 协议选择 流量整形 连接复用/保活 连接池参数调优 DNS解析优化 有效载荷压缩 首部压缩 HTTP/2 多路复用 gRPC / Protobuf 协议协商优化 请求合并与去重

连接管理:少握手,多办事

网络I/O第一大坑,就是频繁建立连接。三次握手加TLS握手,一次完整连接建立少说几十毫秒。如果每次请求都来一遍,吞吐量直接打三折。

连接复用是最直接的优化手段。我建议你们用连接池,而不是每次新建连接。看看这个配置:

// 连接池配置示例
typedef struct {
    int max_connections;      // 最大连接数,默认50
    int keep_alive_timeout;   // 空闲保活,单位秒,推荐60
    int connect_timeout;      // 连接超时,单位ms,推荐3000
    int max_idle_connections; // 最大空闲连接,默认10
} conn_pool_config_t;

这里有个关键点:max_idle_connections 不能设太大。我在项目里曾经把空闲连接设到50,结果高峰期连接数暴涨,把服务器打挂了。后来改成20,吞吐量反而稳定了。你想想看,空闲连接太多,每一条都要保活心跳,也是开销。

技巧:连接池大小建议跟CPU核心数关联。8核机器,最大连接数32-48就够。太多反而因为线程切换导致延迟上升。

数据压缩:能省则省

带宽是成本,也是瓶颈。减少传输数据量,延迟自然降下来。

我记得有个项目,SDK每次上报的数据量在50KB左右,用户量大了之后带宽吃紧。后来我们加了gzip压缩,数据量降到8KB,带宽消耗降了80%。你想想看,这效果多明显。

压缩算法选择,我一般这样取舍:

算法 压缩比 CPU开销 适用场景
gzip 中等 (60-70%) 较低 通用场景,JSON/XML
zstd 高 (70-80%) 中等 服务端高性能场景
lz4 低 (40-50%) 极低 实时性要求极高场景
brotli 极高 (80%+) 高 预压缩静态资源

对于SDK场景,我个人偏好zstd。压缩比和性能平衡得很好。不过要注意,压缩在客户端做,解压在服务端做。客户端CPU太弱的话,压缩反而拉高延迟。

// 使用zstd压缩数据示例
#include <zstd.h>

size_t compress_data(const char* src, size_t src_size, 
                     char* dst, size_t dst_capacity) {
    return ZSTD_compress(dst, dst_capacity, 
                         src, src_size, 3); // 压缩等级3
}
注意:压缩等级不是越高越好。等级19比等级3压缩比只高5%,但CPU开销大了10倍。默认3级就够。

协议选择:选对路,少走弯路

协议选不对,后面怎么优化都白搭。举个例子,HTTP/1.1的队头阻塞问题,在高并发场景下简直是性能杀手。

我现在做新项目,优先考虑HTTP/2或者gRPC。HTTP/2的多路复用,能在一个连接上并行多个请求,延迟降低非常明显。

我曾在项目里从HTTP/1.1切到HTTP/2,同样的业务场景,吞吐量从2000 QPS提升到6000 QPS。为什么?就是因为一个连接可以同时发送多个请求,不再傻傻排队。

如果你们用Protobuf替换JSON,数据量能再省40%。我之前做了一个对比:

传输格式 典型大小(100字段) 序列化时间 反序列化时间
JSON 约2KB 约0.5ms 约0.8ms
Protobuf 约0.6KB 约0.1ms 约0.15ms
MessagePack 约1.2KB 约0.3ms 约0.4ms

从表格里能看出来,Protobuf几乎在所有维度都胜出。不过它有个缺点:可读性差,调试麻烦。但为了性能,这代价值得。

流量整形:别让网络成为瓶颈

最后说流量整形。说白了就是,控制你发数据的节奏,避免把网络打满。

我遇到过一种典型情况:SDK启动时,所有模块同时发请求。网络瞬间拥堵,请求超时率飙升。后来我们做了请求合并和去重,把相同数据源的请求合并成一个,冲突解决了。

流量整形几个常用手段:

  • 请求合并:多个小请求合并成一个大请求,减少握手次数
  • 请求去重:相同数据内容只发一次,服务端广播
  • 速率限制:每秒最多发N个请求,避免打满带宽
  • 优先级队列:关键请求优先发送,非关键请求延后
核心思路:尽量把并发请求往后推,或者合并再发。你想想看,100个1KB的请求跟1个100KB的请求,网络开销差了很多倍。合并之后,TCP窗口利用率也更高。

曾经有个线上事故,SDK在弱网环境下频繁重试,导致客户端发送队列爆满,内存飙升。后来我们加了指数退避算法,第一次失败等1秒,第二次等2秒,第三次等4秒...这样既保证了可靠性,又不会把网络打满。

嗯,网络I/O优化就是这样。连接管理、数据压缩、协议选择、流量整形,层层叠加之后,吞吐量提升50%以上是很常见的。关键是这些技术都没什么黑科技,就是细节做到位。希望你们的SDK也能跑得更快、更稳。