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 的话,版本管理一定要严格。

我的建议: 如果团队里全是 Java,Thrift 是个不错的选择。但如果是多语言混编,gRPC 的生态更友好。

4.1.3 自研TCP协议:极致性能的代价

说到自研TCP协议,很多人觉得这是「造轮子」。但我在做高频交易系统时,不得不走这条路。为什么?因为 gRPC 和 Thrift 的协议头太「重」了。你想想看,一笔订单可能只有几十个字节,但加上协议头、序列化框架的元数据,一下子就膨胀到几百字节。

自研TCP协议,说白了就是自己定义报文格式。比如我设计过一个极简协议:

// 报文结构
// 4字节:消息长度(包含自身)
// 1字节:消息类型
// 4字节:序列号
// N字节:消息体(Protobuf 或 FlatBuffers 序列化后的数据)

这个结构只有 9 字节的固定开销。相比 gRPC 动辄几十字节的头部,优势很明显。

注意: 自研TCP协议意味着你要自己处理粘包、拆包、重连、心跳等所有底层细节。我曾经因为粘包问题排查了整整两天,最后发现是接收缓冲区没清干净。所以,除非你对性能有极致要求,否则别轻易自研。

4.2 序列化选型:Protobuf vs FlatBuffers

序列化,说白了就是把内存里的对象变成二进制流。选型时主要看三点:序列化速度、反序列化速度、数据体积。

4.2.1 Protobuf:行业标准,稳如老狗

Protobuf 是 Google 的杰作,几乎成了分布式系统的标配。它的压缩率很高,而且跨语言兼容性极好。我在大多数项目里都用它,包括交易引擎的订单、成交、行情数据。

但 Protobuf 有个问题:反序列化时需要分配内存。在高频场景下,频繁的内存分配会导致 GC 压力。我遇到过一台 32G 内存的机器,因为 GC 停顿导致订单延迟从 50 微秒飙升到 2 毫秒——嗯,这在高频交易里是不可接受的。

适用场景: 大多数分布式系统、跨语言通信、对 GC 不敏感的场景。

4.2.2 FlatBuffers:零拷贝,为极致性能而生

FlatBuffers 也是 Google 的,但它和 Protobuf 的思路完全不同。它允许你直接访问序列化后的数据,而不需要反序列化。说白了就是「零拷贝」——你拿到二进制流,直接按偏移量读取字段。

我在做行情快照推送时用过 FlatBuffers。行情数据每秒更新几千次,如果用 Protobuf,每次都要反序列化整个对象,CPU 开销很大。换成 FlatBuffers 后,客户端只需要解析自己关心的字段,延迟从 10 微秒降到了 2 微秒。

避坑指南: 我曾经因为 FlatBuffers 的字段对齐问题,导致解析出来的数据全是错的。后来发现,FlatBuffers 要求字段按 4 字节对齐,否则会填充无用字节。所以定义 schema 时,记得把大字段放在前面。
特性 Protobuf FlatBuffers
序列化速度 中等 快(零拷贝)
反序列化速度 慢(需分配内存) 极快(直接访问)
数据体积 略大(需对齐)
GC 压力
跨语言支持 优秀 良好

4.3 连接池与心跳机制

连接池和心跳,听起来简单,但做不好会出大问题。我见过一个系统,因为连接池配置不当,导致大量连接泄漏,最终 OOM 了。

4.3.1 连接池:别让连接成为瓶颈

连接池的核心参数就三个:最大连接数、最小空闲连接数、超时时间。我个人的习惯是:

  • 最大连接数: 根据压测结果来定。一般设为 CPU 核心数的 2-4 倍。比如 16 核机器,最大连接数设为 32-64。
  • 最小空闲连接数: 保持 2-4 个空闲连接,避免突发流量时临时创建连接。
  • 超时时间: 连接超时设为 500ms,读取超时设为 1s。交易系统里,超过 1s 的请求基本可以视为失败。
我曾经踩过的坑: 连接池的「空闲回收」机制一定要开启。有一次我忘了配置,结果连接池里积累了上千个空闲连接,每个连接都占着文件描述符,最后系统报「Too many open files」。嗯,从那以后,我每次都会检查连接池的空闲回收策略。

4.3.2 心跳机制:保活与探测

心跳的作用有两个:一是保活(防止 NAT 或防火墙断开连接),二是探测对端是否存活。交易系统里,心跳间隔一般设为 5-10 秒。

我设计过一个简单的心跳机制:

// 客户端定时发送心跳请求
// 服务端收到后回复心跳响应
// 如果客户端连续 3 次未收到响应,则认为连接断开

// 伪代码
if (missedHeartbeats >= 3) {
    closeConnection();
    reconnect();
}

但要注意,心跳不能太频繁。我曾经见过一个系统,心跳间隔设为 1 秒,结果心跳流量占到了总流量的 30%——这显然不合理。一般来说,心跳流量控制在总流量的 1% 以内比较合适。

我的建议: 心跳和业务请求可以共用同一个连接。不要为了心跳单独开一个连接,那样会增加资源开销。

4.4 本章小结

网络通信这块,没有银弹。gRPC 适合通用场景,Thrift 适合 Java 生态,自研TCP 适合极致性能。序列化方面,Protobuf 是万金油,FlatBuffers 是性能利器。连接池和心跳,看似简单,但细节决定成败。

我个人觉得,选型时不要盲目追求「最新」或「最快」。先搞清楚你的场景:延迟要求是多少?QPS 有多高?团队的技术栈是什么?想清楚这些,选型自然就清晰了。

网络通信选型决策树 网络通信选型 协议选型 序列化选型 连接管理 gRPC(通用场景) Thrift(Java生态) 自研TCP(极致性能) Protobuf(通用) FlatBuffers(零拷贝) 连接池(复用) 心跳机制(保活) 选型原则:根据延迟要求、QPS、团队技术栈综合决策 决策节点 分类节点 协议选项 序列化选项 连接管理

专注资料整理