什么是gRPC?
- A high-performance, open-source universal RPC framework。
- 语言中立,公司里可能存在不同语言的服务需要交互。
- 统一采用gRPC作为服务之间的通信协议,可能存在其他性能更好的解决方案,但不要过早关注性能问题,先标准化更重要。
- 轻量级、高性能,序列化比JSON性能好
- 可插拔,支持插件,例如gRPCGetway插件可以生成http接口,默认只生成gRPC接口。协议可插拔,支持JSON、xml等。
- IDL:代码即文档,避免接口文档未更新的情况。
- 快速生成接口服务端、客户端代码,服务端已定义好接口,自己实现下就行。
- 移动端:基于标准的 HTTP2 设计,支持双向流、消息头压缩、单 TCP 的多路复用、服务端推送等特性,这些特性使得 gRPC 在移动端设备上更加省电和节省网络流量。
- 数据库连接池产生的原因是协议设计缺陷,具体参照下面的http协议演进
- 负载无关的:支持 protocol buffers、JSON、XML 和 Thrift。
- 流:Streaming API,一边传输,一边读取,不用等大文件传输完再读取。
- 阻塞式和非阻塞式:支持异步和同步处理在客户端和服务端间交互的消息序列。
- 元数据交换:类似http header,常见的横切关注点,如认证或跟踪,依赖数据交换。
- 标准化状态码:客户端通常以有限的方式响应 API 调用返回的错误。约束状态码名称空间,以使这些错误处理决策更加清晰。
- 相对于直接定义restful接口的优势:接口定义更加明确,请求体、响应体通过message定义出来,而直接定义restful接口,体现方式不统一,接口文档(维护性差)?第三方平台?服务端直接定义+swagger?
gRPC - HealthCheck
gRPC 有一个标准的健康检查协议,默认提供用于设置运行状态的功能。
使用示例
- 定义健康检查的API
- golang使用gRPC健康检查
client
echoClient := pb.NewEchoClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.UnaryEcho(ctx, &pb.EchoRequest{})
if err != nil {
fmt.Println("UnaryEcho: _, ", err)
} else {
fmt.Println("UnaryEcho: ", r.GetMessage())
}
server
healthcheck.SetServingStatus(serverName, healthpb.HealthCheckResponse_SERVING)
健康检查的应用
服务发现双保险
消费者可以直接感知提供者的状态,保障消费者和注册中心网络不稳定的情况下,也能及时将异常服务提供者从本地负载均衡池中移除。同理,提供者正常运行后,也能被消费者感知,重新加入负载均衡池。
应用平滑发布
老版本注销
- k8s向注册中心发起注销请求
- k8s向容器发送SIGTERM信号,相当于kill命令。
- 应用将gRPC服务状态设置为不健康,并等待两个心跳周期,保障那些没有被注册中心通知到的消费者感知到,避免流量进入。
- 等待正在处理的请求处理完毕,k8s可以做个兜底,2分钟没退出就强制干掉容器,相当于kill -9 。
- 进入优雅退出过程,断开连接之类的。
- 退出容器
新版本创建
- 创建容器
- 通过外挂的方式,检查应用状态。
- 应用启动完毕后,设置gRPC服务状态设置为健康。
- 外挂的辅助脚本检测到应用健康,进行注册到注册中心。
为什么不是应用自己去注册?
剥离注册功能,下沉到辅助脚本里。避免所有应用(不同语言)都需要配置、提供注册功能。
为什么不直接使用k8s的探针来检测应用健康?
因为k8s 1.23以前不支持检测gRPC服务,只能发送http请求,或者是检测TCP端口的连通性。
k8s 1.23 以前如何检测 gRPC 服务状态?
服务发现 - 客户端发现
由注册中心做服务发现,并下发服务注册表到消费者,负载均衡在客户端完成。
- 去中心化——微服务核心理念
- 直连,性能更好
- 缺点:需要所有应用内置本地负载均衡组件,不同语言的应用,使用的负载均衡组件还不同。可使用service mesh优化,在k8s中使用sidecar来做负载均衡,从应用中独立出来,下沉为单独组件。
- ribbon就是这么干的,之前有写过博客:【SpringCloud】五、Ribbon
服务发现 - 服务端发现
由注册中心做服务发现,并提供一个负载均衡器,从注册中心查询服务注册信息,客户端统一请求负载均衡器。
- 类似设计模式里的中介模式的概念。
- 应用无需关心负载均衡如何实现。
- 流量热点,如果使用nginx做负载均衡,单个nginx承受不住,还得使用
lvs+nginx集群
,增加复杂度。 - 相关实现有:
Consul Template+Nginx
,kubernetes+etcd
服务发现
各注册中心对比
B站为什么从zookeeper切换到eureka?
zk保证了写操作一致性、分区容错。但leader节点挂掉后,会进行选举新的leader节点。期间整个zk是不能对外提供服务,大概会持续几十秒。从而失去可用性。并且大量服务长连接导致性能瓶颈。
而使用gRPC的服务发现这个场景下,一致性是可以弱一点的,带来的影响是:1、消费者没能拿到新注册的提供者地址,那就等一会呗。2、消费者拿到已注销的提供者地址,由于做了gRPC健康检查,并不会去调用该服务,会选择存活的服务。
而eureka就属于AP阵营,保证可用性,没有主节点,其他节点都挂掉,只剩一个节点也能对外提供服务。
服务发现架构
eureka详细介绍
- 通过 Family(appid) 和 Addr(IP:Port) 定位实例,除此之外还可以附加更多的元数据:权重、染色标签、集群等。
- appid: 使用三段式命名,business.service.xxx
- Provider 注册后定期(30s)心跳一次,注册,心跳,下线都需要进行同步,注册和下线需要进行长轮询推送。
- 新启动节点,需要 load cache,JVM 预热。
- 故障时,Provider 不建议重启和发布。
- 某个eurak挂了,不要马上主动重启,因为新启动的enuraka里没有任何服务注册信息,这时候有服务来拉取服务注册信息,就会导致该服务无法访问其他服务。如果立即重启,需要做控制,例如启动后等待数据同步完毕才对外提供服务。
- 全部eureka节点都挂了,全部重启后,需要等待2、3个心跳周期,等服务都注册好后,在对外提供查询服务。
- Consumer 启动时拉取实例,发起30s长轮询。故障时,需要 client 侧 cache 节点信息。
- 长轮询:客户端发送请求拉取数据,如果此时服务端没有产生的数据,就不暂时不响应,等有数据或者达到超时时间(例如30秒),再响应。也就是这个请求会挂起。有效减少轮询场景下的请求数量。
- Server 定期(60s) 检测失效(90s)的实例,失效则剔除。短时间里丢失了大量的心跳连接(15分钟内心跳低于期望值*85%),开启自我保护,保留过期服务不删除。
B站是重写了eureka,做了这些改进,不清楚这是eureka本身的特性还是B站改进后的。
其他
分布式系统的 CAP 定理
分布式系统的三大指标
Eric Brewer 说,这三个指标不可能同时做到。这个结论就叫做 CAP 定理。
Partition tolerance
分区容错性,多数分布式系统都分布在多个子网络,每个子网络就是一个区。分区容错的意思是,允许区间通信失败,也就是节点之间通信失败。强调节点的独立性。
分布式系统中,网络异常是很难避免的,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们,剩下的 C 和 A 无法同时做到。
Consistency
一致性,由于是分布式系统,多节点设计,就得考虑一致性问题,即访问分布式系统,和访问单体应用一样,下一次读操作一定能读到上一次的写操作结果。是不是有点像多线程并发,只是将线程换成了节点。
Availability
可用性,整个分布式系统能正常对外提供服务。
为什么一致性和可用性不能同时满足?
假如节点之间数据同步失败,整个系统还是要对外提供服务,也就是以分区容错为基础。
- 如果要保证一致性,那么同步失败的节点就不能对外提供服务,得等到数据同步成功才能恢复,失去可用性。
- 如果保证可用性,那么访问到未同步数据的节点,就会得到脏数据,失去一致性。
所以得结合实际使用场景,在设计阶段,对一致性和可用性进行取舍。
http协议演进
Http 1.0
存在的问题
- 无法复用链接:每个请求都需要建立TCP链接。
- 对头阻塞:第二个请求必须等第一个请求响应后才能发起。
Http 1.1
特性
- 默认使用长连接,可配置Keep-alive来控制连接时间
- 支持请求管道化,客户端可以发送多个请求,而不用等待前一个请求响应。
存在的问题
- 为了让客户端识别请求对应的响应,服务端响应时,必须按照请求的顺序进行响应,哪怕第二个响应准备好了,也得等第一个响应先返回。
Http 2.0
特性
- 采用二进制格式传输数据,而非Http 1.x的文本格式,解析更高效。
- HTTP 1.x的请求和响应报文,都是由起始行,首部和实体正文(可选)组成,各部分之间以文本换行符分隔。HTTP2.0 将请求和响应数据分割为更小的帧,并且它们采用二进制编码,乱序发送,最终组装。
- 压缩header,并缓存header。
- 同个域名只需要占用一个TCP连接,使用一个连接并行发送多个请求和响应。
- 多个请求之间、多个响应之间互不影响,实现并发。
存在的问题
- 由于复用一个TCP链接,一旦出现丢包,就得重传,后面所有请求都被阻塞。
Http 3.0
特性
- 谷歌基于UDP 协议来定义的 QUIC 协议,应用到Http 3.0上。
- 不需要链接,所以没有额外的链接时间。
- 一个连接上的多个stream之间没有依赖,如果某个Stream丢了一个包,是不影响后续的Stream。
- 向前纠错:每个数据包除了它本身的内容之外,还包括了部分其他数据包的数据,因此少量的丢包可以通过其他包的冗余数据直接组装而无需重传。
- 加密认证的报文: TCP协议头部没有经过任何加密和认证,所以在传输过程中很容易被中间网络设备篡改。QUIC 所有报文头部都是经过认证的,报文Body都是经过加密的。
- QUIC在移动端的表现也会比TCP好。因为TCP是基于IP和端口去识别连接的,这种方式在多变的移动端网络环境下是很脆弱的。但是QUIC是通过ID的方式去识别一个连接,不管你网络环境如何变化,只要ID不变,就能迅速重连上。
如何衡量网络的质量?
带宽
单位时间内传输的数据量。
时延
发送数据到就收数据总共花费的时间,包含发送时延,处理时延(网络设备),排队时延(网络设备),传播时延。
抖动
最大时延与最小时延的差值。
丢包
某些原因下会出现丢包,例如网络阻塞:某个网络设备处理不了这么多数据,有的数据包排队很久了,就可能会被丢掉。
多个微服务共享db
微服务中大部分是独占db,也有sharedatabase的情况,例如:具有高级权限的admin服务和面向用户的服务共享db
网关层如何收敛客户端多版本?
可通过请求的header进行路由
微服务之间的服务调用理论上是有向无环图(DAG, Directed Acyclic Graph),如何避免闭环调用?
通过链路追踪得到服务调用关系图,并在服务调用申请权限时进行阻止。
简单接口也要在BFF层定义吗?
BFF支持直接透传,例如一些简单的接口,不需要在BFF做什么,只需调用业务中台的服务就行。但流量还是不能直接从网关层到业务中台,需要在BFF配置路由,让流量经过BFF再到业务中台。
参考
CAP 定理的含义
分布式系统的CAP理论
Http1.0 1.1 2.0 3.0工作原理探究
9 张动图让网络性能的四大指标:带宽、时延、抖动、丢包