Go进阶训练营 – 微服务概览与治理三:gRPC & 服务发现

2022-09-21 10:30:20 浏览数 (1)

什么是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 有一个标准的健康检查协议,默认提供用于设置运行状态的功能。

使用示例

  1. 定义健康检查的API
  2. golang使用gRPC健康检查
client
代码语言:javascript复制
    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
代码语言:javascript复制
healthcheck.SetServingStatus(serverName, healthpb.HealthCheckResponse_SERVING)

健康检查的应用

服务发现双保险

消费者可以直接感知提供者的状态,保障消费者和注册中心网络不稳定的情况下,也能及时将异常服务提供者从本地负载均衡池中移除。同理,提供者正常运行后,也能被消费者感知,重新加入负载均衡池。

应用平滑发布
老版本注销
  1. k8s向注册中心发起注销请求
  2. k8s向容器发送SIGTERM信号,相当于kill命令。
  3. 应用将gRPC服务状态设置为不健康,并等待两个心跳周期,保障那些没有被注册中心通知到的消费者感知到,避免流量进入。
  4. 等待正在处理的请求处理完毕,k8s可以做个兜底,2分钟没退出就强制干掉容器,相当于kill -9 。
  5. 进入优雅退出过程,断开连接之类的。
  6. 退出容器
新版本创建
  1. 创建容器
  2. 通过外挂的方式,检查应用状态。
  3. 应用启动完毕后,设置gRPC服务状态设置为健康。
  4. 外挂的辅助脚本检测到应用健康,进行注册到注册中心。
为什么不是应用自己去注册?

剥离注册功能,下沉到辅助脚本里。避免所有应用(不同语言)都需要配置、提供注册功能。

为什么不直接使用k8s的探针来检测应用健康?

因为k8s 1.23以前不支持检测gRPC服务,只能发送http请求,或者是检测TCP端口的连通性。

k8s 1.23 以前如何检测 gRPC 服务状态?
  • 在 Kubernetes 上对 gRPC 服务器进行健康检查
  • grpc-health-probe

服务发现 - 客户端发现

由注册中心做服务发现,并下发服务注册表到消费者,负载均衡在客户端完成。

  • 去中心化——微服务核心理念
  • 直连,性能更好
  • 缺点:需要所有应用内置本地负载均衡组件,不同语言的应用,使用的负载均衡组件还不同。可使用service mesh优化,在k8s中使用sidecar来做负载均衡,从应用中独立出来,下沉为单独组件。
  • ribbon就是这么干的,之前有写过博客:【SpringCloud】五、Ribbon

服务发现 - 服务端发现

由注册中心做服务发现,并提供一个负载均衡器,从注册中心查询服务注册信息,客户端统一请求负载均衡器。

  • 类似设计模式里的中介模式的概念。
  • 应用无需关心负载均衡如何实现。
  • 流量热点,如果使用nginx做负载均衡,单个nginx承受不住,还得使用lvs nginx集群,增加复杂度。
  • 相关实现有:Consul Template Nginxkubernetes 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 张动图让网络性能的四大指标:带宽、时延、抖动、丢包

0 人点赞