4. 网络通信基石:gRPC vs Thrift vs 自研TCP协议、序列化选型(Protobuf/FlatBuffers)、连接池与心跳机制
聊到交易引擎的网络通信,我脑子里第一个蹦出来的词就是「延迟」。你想想看,一笔订单从客户端发出,到撮合引擎落地,中间经过的每一微秒都可能是真金白银。我早年做第一个交易系统时,天真地用了HTTP JSON,结果压测一上来,延迟直接飙到十几毫秒——嗯,那会儿我就明白了,网络通信这块,必须得较真。
4.1 协议选型:gRPC、Thrift 还是自研TCP?
选协议,说白了就是在「通用性」和「极致性能」之间做权衡。我个人的习惯是:先看场景,再定方案。
4.1.1 gRPC:生态好,但别迷信
gRPC 基于 HTTP/2,支持双向流、多路复用。如果你团队里微服务已经用上了 Kubernetes,那 gRPC 几乎是标配。我在一个做行情推送的项目里用过它,流式推送确实方便,客户端订阅后,服务端可以持续推送数据。
但注意,gRPC 的序列化用的是 Protobuf,这没问题。可 HTTP/2 的头部压缩、流控机制,在极端高并发下反而会成为瓶颈。我曾经压测过,单连接 10 万 QPS 时,gRPC 的 CPU 开销比自研 TCP 高了 30% 左右。
4.1.2 Thrift:Facebook 的老兵,依然能打
Thrift 比 gRPC 更早,它最大的特点是「代码生成」——你定义好 IDL,它帮你生成各种语言的客户端和服务端。我在一个老项目中用过 Thrift,它的二进制协议比 JSON 快得多,而且支持多种传输层(TCP、HTTP)。
但 Thrift 有个坑:它的版本兼容性做得不太好。我曾经因为升级了 Thrift 版本,导致旧客户端连不上新服务端,排查了半天才发现是协议头变了。嗯,这里要注意,用 Thrift 的话,版本管理一定要严格。
4.1.3 自研TCP协议:极致性能的代价
说到自研TCP协议,很多人觉得这是「造轮子」。但我在做高频交易系统时,不得不走这条路。为什么?因为 gRPC 和 Thrift 的协议头太「重」了。你想想看,一笔订单可能只有几十个字节,但加上协议头、序列化框架的元数据,一下子就膨胀到几百字节。
自研TCP协议,说白了就是自己定义报文格式。比如我设计过一个极简协议:
// 报文结构
// 4字节:消息长度(包含自身)
// 1字节:消息类型
// 4字节:序列号
// N字节:消息体(Protobuf 或 FlatBuffers 序列化后的数据)
这个结构只有 9 字节的固定开销。相比 gRPC 动辄几十字节的头部,优势很明显。
4.2 序列化选型:Protobuf vs FlatBuffers
序列化,说白了就是把内存里的对象变成二进制流。选型时主要看三点:序列化速度、反序列化速度、数据体积。
4.2.1 Protobuf:行业标准,稳如老狗
Protobuf 是 Google 的杰作,几乎成了分布式系统的标配。它的压缩率很高,而且跨语言兼容性极好。我在大多数项目里都用它,包括交易引擎的订单、成交、行情数据。
但 Protobuf 有个问题:反序列化时需要分配内存。在高频场景下,频繁的内存分配会导致 GC 压力。我遇到过一台 32G 内存的机器,因为 GC 停顿导致订单延迟从 50 微秒飙升到 2 毫秒——嗯,这在高频交易里是不可接受的。
4.2.2 FlatBuffers:零拷贝,为极致性能而生
FlatBuffers 也是 Google 的,但它和 Protobuf 的思路完全不同。它允许你直接访问序列化后的数据,而不需要反序列化。说白了就是「零拷贝」——你拿到二进制流,直接按偏移量读取字段。
我在做行情快照推送时用过 FlatBuffers。行情数据每秒更新几千次,如果用 Protobuf,每次都要反序列化整个对象,CPU 开销很大。换成 FlatBuffers 后,客户端只需要解析自己关心的字段,延迟从 10 微秒降到了 2 微秒。
| 特性 | Protobuf | FlatBuffers |
|---|---|---|
| 序列化速度 | 中等 | 快(零拷贝) |
| 反序列化速度 | 慢(需分配内存) | 极快(直接访问) |
| 数据体积 | 小 | 略大(需对齐) |
| GC 压力 | 高 | 低 |
| 跨语言支持 | 优秀 | 良好 |
4.3 连接池与心跳机制
连接池和心跳,听起来简单,但做不好会出大问题。我见过一个系统,因为连接池配置不当,导致大量连接泄漏,最终 OOM 了。
4.3.1 连接池:别让连接成为瓶颈
连接池的核心参数就三个:最大连接数、最小空闲连接数、超时时间。我个人的习惯是:
- 最大连接数: 根据压测结果来定。一般设为 CPU 核心数的 2-4 倍。比如 16 核机器,最大连接数设为 32-64。
- 最小空闲连接数: 保持 2-4 个空闲连接,避免突发流量时临时创建连接。
- 超时时间: 连接超时设为 500ms,读取超时设为 1s。交易系统里,超过 1s 的请求基本可以视为失败。
4.3.2 心跳机制:保活与探测
心跳的作用有两个:一是保活(防止 NAT 或防火墙断开连接),二是探测对端是否存活。交易系统里,心跳间隔一般设为 5-10 秒。
我设计过一个简单的心跳机制:
// 客户端定时发送心跳请求
// 服务端收到后回复心跳响应
// 如果客户端连续 3 次未收到响应,则认为连接断开
// 伪代码
if (missedHeartbeats >= 3) {
closeConnection();
reconnect();
}
但要注意,心跳不能太频繁。我曾经见过一个系统,心跳间隔设为 1 秒,结果心跳流量占到了总流量的 30%——这显然不合理。一般来说,心跳流量控制在总流量的 1% 以内比较合适。
4.4 本章小结
网络通信这块,没有银弹。gRPC 适合通用场景,Thrift 适合 Java 生态,自研TCP 适合极致性能。序列化方面,Protobuf 是万金油,FlatBuffers 是性能利器。连接池和心跳,看似简单,但细节决定成败。
我个人觉得,选型时不要盲目追求「最新」或「最快」。先搞清楚你的场景:延迟要求是多少?QPS 有多高?团队的技术栈是什么?想清楚这些,选型自然就清晰了。