文章目录
- 前沿
- RPC框架有哪些供我们选用,怎么选?
- 学gRPC框架应该把重点放哪里?
-
- [理解 gRPC 为什么比 RESTful 快](#理解 gRPC 为什么比 RESTful 快)
-
- [gRPC基于 **HTTP/2 协议栈**(为什么基于htt/2就快呢)](#gRPC基于 HTTP/2 协议栈(为什么基于htt/2就快呢))
- 四种通信模式的应用场景
- 关于protobuf应该学什么?
- 关于go中的protoc-gen-go-grpc和protoc-gen-go
- 其他
前沿
希望你看完这篇小作文有所收获。
学习这个框架其实用处并不大,因为大部分都是维护或者基于已有架构新增功能,只有新项目或者重构才有机会让你设计一个远程调用。但是不得不给那些技术有点菜的面试官活爹准备一些话题。这里我就不普及什么是gRPC相关的基础概念了,因为基础概念小demo这些东西全网到处都是,已经烂大街了。
下面我们要开始正文了。
RPC框架有哪些供我们选用,怎么选?
有很多很成熟的框架,都各有千秋。
| 框架 | 优点 | 缺点 | 适用场景 | 开源企业 |
|---|---|---|---|---|
| grpc | 基于http/2协议和pb格式、社区活跃度高、跨语言友好、云原生友好 | 浏览器支持有限、动态类型不友好(proto文件限制,可以使用Any类型或OneOf绕过,但是序列化和反序列化有性能损耗)、服务治理结合server mesh | 适合微服务间高性能通信场景、实时流式传输、云原生应用,多语言混战 | 谷歌 |
| Apache Dubbo | 服务治理功能丰富、序列化可配(默认Hessian2)、完整的微服务解决方案、性能极高、社区完善、与 Spring 生态深度集成 | 配置复杂、非java语言不友好 | Java 全家桶微服务生态、企业级分布式系统、对服务治理要求高的场景 | 阿里 |
| brpc | 极致性能、丰富的内置服务、支持多种协议 | 主要面向 C++,跨语言不友好、学习曲线陡峭 | 高性能 C++ 服务、基础设施组件、游戏等低延迟服务 | 百度 |
- 如果需要快速出成果,首先你用过什么框架,或者你的团队对什么框架比较熟悉
- 对于性能/延迟的要求到什么级别
- 后期的服务治理有没有方案,是用框架自带的还是有插件
- 监控与运维成本的考量
- 是不是需要支持多语言(不同的语言生态支持不一样)
- 生态也要兼顾一下(要不后期有问题请教都找不到人帮忙)
学gRPC框架应该把重点放哪里?
本文以gRPC框架为基础,系统的梳理一下
底层原理、工程化实践以及在微服务架构中的治理方案
核心底层原理:Protobuf 与 HTTP/2
理解 gRPC 为什么比 RESTful 快
gRPC基于 HTTP/2 协议栈(为什么基于htt/2就快呢)
先铺垫一下http1.1 的传输方式
- 每个http请求都建立一次tcp连接(http2多路复用解决),每次都要3次握手
- 每次建立连接都需要包含http的头部信息(http2通过头部压缩解决)
- http1 有对头阻塞的问题(http2 通过流来解决)
- http\1.x是明文传输
- http1不支持 服务器推送,http2支持
http2对于http1.1有几个特性增加了传输速率
-
http/2 是把数据流按照二进制分帧方式对数据进行切割,发送流数据的时候使用了多路复用技术
- 什么是一个数据流(stream),"流通道"是怎么创建的 (你暂时认为这是一个流通道,后面就知道了他其实不是一个queue)
要想理解一个数据流需要先理解下面几个概念
- 连接(Connection): 1 个 TCP 连接(通常一个域名只建立一个,但是不同的浏览器肯定是建立不同的连接,因为TCP连接的四元组不一样了)
- 数据流(Stream): 连接中的一个虚拟通道,可以承载双向的消息(可以理解成一条双向的4车道公路,货物可以看成data,车牌号可以认为是流ID)。流的出现是为了解决http/1.1中的队头阻塞问题
-- 流的创建:发送一个带有新流 ID 的"HEADERS 帧",就代表创建了一个流 (跟车道不一样)。
-- 客户端和服务端的流ID是通过奇偶来区分的,奇数是客户端,偶数是服务端流ID(流ID单调递增)。
-- 流有优先级区分 - 消息(Message): 对应 HTTP/1 中的请求或响应,由一个或多个"帧"组成。
- 帧(Frame): HTTP/2 通信的最小单位(如 HEADERS 帧、DATA 帧)。
- 什么是多路复用
多路复用的意思是多个流的数据都往同一个tcp连接的缓冲区写数据 。麻烦把前面这句话再读两遍。当某一个流数据阻塞不影响其他数据流继续传送数据(解决了http1的对头阻塞问题)。不过注意 :这里的多路复用跟epoll的I/O多路复用不是一个事情。这里的多路指的是不同的流(stream),可以理解成上面例子中的虚拟车道
- 什么是二进制分帧
二进制分帧是将所有的数据(头部信息和data信息)进行了原子切割然后加上帧头部信息的数据单元。二进制分帧是一个基于http/1的一个巨大进步,因为多路复用和头部压缩这些高级特性都是基于二进制分帧来做的。
为什么需要二进制分帧?
- 数据混杂,无法识别内容怎么拼接
- 无法根据实际需求控制内容输出的先后顺序(比如先加载广告再加载内容)
二进制分帧包含帧头部和帧数据(也叫帧数据部分)
- Length (长度): 帧负载(Payload)的大小
- Type:帧属性(HEADERS、DATA、PING(心跳))
- StreamID(流标识):这个帧属于哪个流(重点字段)
- Payload(负载):这是一个变长字段,实际的传输内容
- http/2 的头部压缩
- http\1.x 没有头部压缩功能,每次请求大需要发相同的user-agent和cookie等信息
- http\2.x(HPACK压缩):新的请求不再重复发送之前已经发送过的头部信息,如果有新的头部信息会进行叠加发送(添加上新的头部信息),老字段只发索引序号
Hpack的实现基础:
- 静态表:协议预定义了61个常用的Header存放在一个静态表(比如:method: GET 对应索引 2,:status: 200 对应索引 8)传输时只传2就好了
- 动态表:不在静态表里的常用数据放在动态表了,比如Cookie或者token,第一次发送完整的kv对,只要连接没断右面只要发动态表单索引即可
注意动态表的生命周期: 动态表是基于 TCP 连接的。连接断开,动态表即销毁- 霍夫曼编码:如果某个字符串必须以字面量形式发送,HPACK 会使用专门为 HTTP 头部优化的静态霍夫曼编码。(很少关注到)
实现原理是:出现频率高的字符(如数字、小写字母)用较短的二进制位表示,频率低的用较长位。
http\2这种思想基本上就是小钱靠省,大钱靠挣,创业起家两手抓的策略
-
Protobuf 编码机制
Protobuf(Protocol Buffers)是Google开发的二进制序列化协议,相比JSON/XML等文本格式,具有:
- 体积小 - 比JSON小3-10倍
- 字段名称使用field number 代替
- 文本数字使用Varint变长编码(小数字用1字节(0-127),300用2字节,普通int要用4字节)
- ZigZag编码格式(有符号整数优化)
- 没有换行符双引号括号等
- 速度快 - 解析速度快20-100倍
- 字段名称是整数比较,json需要比较字符串或者哈希,解析需要解析引号
- pb是强类型,编译时确定的,不需要类型推断。json是运行时判断的,需要反射
- json内存是动态分配的,pb先计算大小再分配内存,是预分配的
- 边界检查有wire_type决定,不需要像json一样检查字符
- 跨语言 - 支持多种编程语言(可执行文件直接支持源码生成)
- 兼容性好 - 向后/向前兼容
- 体积小 - 比JSON小3-10倍
-
TCP连接本身特性
TCP连接提速是梯度提速的,速度每次左移一位,共用一个连接,后面的数据传输快
四种通信模式的应用场景
- Unary RPC: 常规同步调用。
- Server Streaming: 适用于股票行情推送、大文件下载。
- Client Streaming: 适用于大文件上传、批量数据汇报。
- Bi-directional Streaming(双向流): 适用于即时通讯、实时协作、复杂的长连接交互。
关于protobuf应该学什么?
- Oneof 与 Any 类型:
- Oneof: 处理互斥字段,节省内存。
- Any: 处理泛型需求,理解其内部的 type_url 机制。
- Wrapper Types: 学习使用 google.protobuf.Int64Value 等包装类来解决"如何区分默认值 0 和未传值"的问题。
- 性能优化实战
- 字段编号选择: 为什么要将频繁使用的字段放在 1-15 编号内?(1-15 编号: 占用 1 个字节(Tag + 类型),应分配给出现频率最高的字段。16-2047 编号: 占用 2 个字节。)。
- 避免大量嵌套: 理解深层嵌套对解析性能的影响。
- 大对象处理: 探讨 Protobuf 是否适合传输超大文件(通常不建议,建议流式传输或分片)。
- 怎么区分pb里字段设置的是0值还是默认值
- 使用optional 关键字解决
可以为字段添加 optional关键字,这样就会生成 HasXxx()方法
- 如果设置了xx.HasId()返回值是TRUE,否则就是FALSE
- 使用Wrapper类型
比如把 int32 换成google.protobuf.Int32Value 类型
if user.Id != nil {
fmt.Println("id is set:", user.Id.Value) // 已设置
} else {
fmt.Println("id is not set") // 未设置
}
使用oneof
关于go中的protoc-gen-go-grpc和protoc-gen-go
protoc-gen-go
一般使用在只用pb做序列化/反序列化的场景,不提供远程调用功能。比如生产者只往消息队列写数据,消费者来读取msg进行处理。
常见的使用场景:
- kafka
- redis
- 配置文件
protoc-gen-go-grpc
不光是做为消息的传递格式,还要提供远程调用服务
其他
如何删除不再使用的字段
不能直接删除字段,也可以使用oneof 保持向前兼容
protocal
syntax = "proto3";
message User {
// 废弃的字段号
reserved 2, 15, 9 to 11;
// 废弃的字段名
reserved "old_name", "old_email";
string name = 1;
// int32 age = 2; // 已废弃
string email = 3;
int32 new_age = 4;
}
gRPC中的拦截器与中间件
其实就是给server注册函数添加回调函数,跟gin的middleware原理很类似,细节不同。
监控,鉴权,限流都可以在这里实现。
| 特性差异 | grpc | gin |
|---|---|---|
| 执行顺序 | 链式执行 | 顺序执行(洋葱模型) |
| 注册时机 | 服务创建时 | 路由注册时通过use()函数 |
| 数据结构 | go原生的contex.Context | *gin.Context传递 |
| 流式支持 | 支持4种流 | 只支持http请求/响应 |
| 其他特性区别都不重要 | - | - |
http为什么不使用现有的软件压缩方式?(仅作了解)
既然 Gzip,zstd 很成熟,为什么 HTTP/2 要专门搞一个 HPACK?
- 安全性 (CRIME 攻击): 在 2012 年,安全专家发现利用 TLS 压缩和 Gzip 的特性,攻击者可以通过观察压缩后的密文长度变化,逆向推导出加密的 Cookie。
- HPACK 的对策: HPACK 被设计为无状态的 Huffman 编码,且不使用导致 CRIME 攻击的"动态窗口匹配"算法,从而在保证压缩率的同时规避了安全风险。
性能优化与生产环境避坑
- 连接池管理: 虽有 HTTP/2,但在极高并发下是否需要建立多个 Client 或是优化连接参数(如 Keepalive)。
- 平滑重启(Graceful Stop):
- 客户端/服务端支持健康检查和负载均衡
- 健康状态设置为FALSE,停止接收新链接
- 等待已经接收的active状态的连接请求完成,被动关闭
- 与网关的集成: 掌握 gRPC-Gateway(将 gRPC 转为 RESTful 给前端用)或者如何在 Envoy/Nginx 中配置 gRPC 代理。
- context 级联取消,deadline 传播,幂等性设计
- 流量放大与retry 风暴怎么解决
- 客户端做频控、内部错误不重复调用RPC
- 服务器中间件根据错误率动态调整频率
- 限制根据资源(CPU、memory、带宽)限制active的链接数量
- 基于客户端ip限流
尝试回答这个面试题:为什么pb比json快
你自己尝试回答一下这个问题,写在评论区共同学习一下