一般只用 20% 的代码就可以解决 80% 的问题。但要想解决剩下 20% 的问题的话,则需要额外 80% 的代码。
sniper/thought.md at master · bilibili/sniper https://github.com/bilibili/sniper/blob/master/thought.md
RPC 协议
有了语言,接下来就要确定通信协议。首先不要使用 REST 风格接口。 REST 中看不中用。REST 的核心是资源和状态,所有的变更都对应状态的转变。
对于简单的场景,REST 看似完美,如:GET /user/123
表示查询。
但如果是发送一条短信呢?一种方案是使用 POST /sms
表示创建一条短信资源,另一种方案则是 POST /sms:send
直接发送。
但不管哪种方式,都不如 RPC 调用直观,其原因有二:
- 一是 http 的方法(GET, POST, PUT, DELETE 等)太少,基本都是面向静态资源的,表达能力有限
- 二是将业务过程转成资源状态变化本身就比较烧脑,而且存在无法转化的场景
REST 还有一个比较大的问题就是 url 中有数字 id,统计 prometheus 监控指标的时候必须做归一化处理。
所以,不用 REST。
Weisai RPC
这得从原来在 L 部门用的 Weisai-RPC 说起。该 RPC 基于 TCP 传输,消息结构如下:
typedef struct swoole_message {
uint32_t header_magic; // magic 字段 默认2233
uint32_t header_ts; // unix时间戳
uint32_t header_check_sum; // 校验和, 暂未定义, 默认为0
uint32_t header_version; // 版本号
uint32_t header_reserved; // 保留字段, 默认0, live-api转发时设置为1
uint32_t header_seq; // 序列号
uint32_t header_len; // body长度
char cmd[32]; // 命令字符串
// 格式 {message_type}controller.method,
// message_type 0 request, 1 response
// 长度没满右端补充\0, 超过自动右端截断.
char* body; // 可变 长度为header_len 格式为JSON:
// {"header":..., "body":....}
} rpc_message_t;
典型的面向 c 语言的设计,方便 c 语言解析,但不太灵活。
比如,cmd 字段只有 32 字节,也就是说接口名字最多只能是 32 字节。还有 body 是字符串,但实际传输的是 JSON,需要二次解析。使用结构化二进制消息就是为了提高解析速度,但这种改进跟 JSON 解码相比又可以忽略。所以,这种混合型的设计除了看上去比较复杂以外,确实没什么优点了。
因为没有采用 HTTP 协议,后来不得不在 body 中定义了 header 字段用来传输 HTTP 请求的 header。像 nginx, curl, tcpdump 这样的标准也基本上无法正常使用。为此,还专门引入了一个接入层负责 RPC 和 HTTP 之间的相互转换。
切实体会到了 Weisai-RPC 的不便之后,我决定业务 RPC 协议只用 HTTP 传输,原则上不使用二进制消息格式。
关于 gRPC
说到 HTTP 就不得不说说 gRPC。gRPC 是 Google 开放的一种 RPC 协议,其主要特性:
- 只支持 protobuf 编码
- 强依赖 HTTP2 协议
- 支持 stream 接口
- 每个消息都有五字节的二进制前缀 其他细节请参考 PROTOCOL-HTTP2。
protobuf 本身是支持 JSON 的,不明白为什么 gRPC 的实现不支持。而支持 stream 接口则是 gRPC 的一大特色,使 gRPC 能够胜任诸如语音实时识别等场景。但这一类场景是比较少见的。我们绝大多数业务场景都是一问一答的。为了实现这个 stream 特性,gRPC 不得不依赖 HTTP2,不得不自行定义了一种有固定五字节头的消息格式。与此同时,gRPC 也就放弃了 HTTP 协议原生的压缩功能,也没法使用 HTTP 协议的 content-length 头传递消息长度。这也是 gRCP 消息五字节头的功能所在,头一个字节表示是否压缩,后四个字节表示消息长度。
有个所谓的 2-8 原则:
一般只用 20% 的代码就可以解决 80% 的问题。但要想解决剩下 20% 的问题的话,则需要额外 80% 的代码。
gRPC 的 stream 接口就是剩下的 20% 的问题。
gRPC 还有个 web 支持的问题。浏览器的 js 无法使用 HTTP2 的特性,所以不能直接与 gRPC 服务通信。于是有了 grpc-web,还有 grpc-gateway。
所以,如果没有 stream 接口需求,则完全没有必要使用 gRPC;如果真的有这类需求,也不可能太多,直接使用原生 TCP/WebSocket 协议开发也不是难事。
最终我们选择了 twirp。twirp 可以看作是简化版的 gRPC,同样用 protobuf 描述,不依赖 HTTP2,同时支持 protobuf 和 JSON,没有五字节的二进制前缀。但我们对原生的 twirp 做了修改,形成了自己的版本,主要改动就是添加了对 www-form-urlencoded 编码格式的支持,这是移动端的历史包袱导致的,没办法。
现在的移动端使用 www-form-urlencoded 编码,更加简单;管理后台使用 JSON 编码,更加灵活。如果对性能有要求也可以使用 protobuf 编码,但没目前没有用,估计也不会有人喜欢用。