目录

阅读笔记:微服务架构

微服务架构

回顾下微服务架构的一些知识,包括服务发现、负载均衡、可用性,很多东西没有实操过所以难以记住,需要多多回顾。

来自极客时间的课程 https://time.geekbang.org/column/intro/100551601

后端工程师的高阶面经

服务注册与发现

  1. 服务端启动需要向注册中心里注册自身的信息,保持心跳

  2. 客户端调用,需要找注册中心获取服务节点,并且缓存列表。

  3. 客户端与注册中心也要保持心跳和数据同步,服务端发生变动需要通知客户端更新

  4. 客户端发送请求,服务端返回响应

如果服务端下线怎么办?

服务端通知注册中心准备下线,注册中心通知客户端更新列表,客户端不再发请求到该服务端,服务端等待一段时间后下线。

等待时间是需要的

etcd / ZooKeeper 可以当作注册中心,用一致性协议比如 Raft, ZAB 保证一致性

为什么需要服务注册和发现?

HTTP 需要域名解析,多级缓存,而 RPC 一般需要绑定多个端口,DNS 不能注册端口?

RPC 一般需要更新服务,DNS 多级缓存难以更新

高可用

服务注册与发现怎么保证高可用

注册服务端崩溃检测

注册中心和服务端之间维持心跳

比如租约、长连接 ws 等等,但最重要的问题是,心跳失败后的处理,是连续几次心跳失败才认为失败还是?连续重试,还是间隔重试,间隔是?

参考指数退避算法:一开始重试几次,间隔较短,然后间隔指数上升,类似冷启动?

客户端容错

注册中心或者服务端节点出问题时,请求依旧能发送到正确的服务端节点

failover 换节点

没太理解,意思就是客户端维护可用列表?

注册中心选型

注册中心更加关注 CAP 中选 CP 还是选 AP 的问题

C:Consistency,数据一致性

A:Availability,服务可用性

P:Partition-tolerance,分区容错性

CAP 理论最多满足其中两个,CP 表示一致 + 分区容错,AP 表示可用 + 分区容错

P 分区容错性是肯定要选的,

选 C(一致性) 还是选 A(可用性)?哪个更加重要?

可用性,也就是选 AP

如果注册中心选了 AP,那么客户端可能拿到不一致的可用节点列表,如果客户端发送到已经失效的服务端节点,那么就会发生错误,就需要客户端容错,failover 换节点重试。

ZooKeeper/ETCD 实际上是一个 CP 系统,而不是 AP 系统。

它们的一致性协议出色

但是 CP 面对负载较高的情况会出现什么问题呢?写入?

总结

微服务架构可以看成三角形,围绕哪条边/哪个点会出问题来进行容错

      注册中心
      /    \
客户端 ----- 服务端

负载均衡

负载均衡是很重要的,问题在于考虑到性能和可用、请求该发给哪个服务端?

常见的负载均衡算法:

轮询:依次分配 加权轮询:每个服务器有权重,权重高的优先分配 随机:随机 加权随机:每个服务器有权重 哈希:根据请求特征比如客户端 IP 计算哈希分配给对应的服务器 一致性哈希:服务器映射成环,请求也会映射到环上分配到最近的服务器

一致性哈希比较出名,但存在一些问题,负载并不完美均衡,但可以很显著解决节点增加或减少导致的数据迁移,只需要部分迁移。通过增加虚拟节点(每个节点会存在多个副本)、随机权重可以提高负载均衡效果

轮询与加权轮询

加权轮询如何实现?能不能直接 [1,1,1,2,3]

如果不想连续选择同一个节点,可以使用平滑加权轮询算法,比如 [1,2,1,3,1]

平滑加权 smooth weighted round-robin balancing

除了定义的 weight 还有个动态改变的 current_weight

每次选择 weight 最大的,并且将其减去 total_weight = sum(current_weight)

然后每个节点 current_weight += weight

重复这两步选择节点

图例:https://blog.csdn.net/gqtcgq/article/details/52076997

哈希与一致性哈希

简单哈希:(请求的一些参数或者 ip 得到哈希值) % n 个节点 = 序号

但如果哈希算法选的不好,可能会导致热点,并不均匀。

一致性哈希:服务端节点落在哈希环上,客户端请求参数计算哈希值,落在环上最近的服务端节点(顺时针)

一致性哈希并不保证服务端节点均匀分散在哈希环上,需要通过加权随机 (Rendezvous hashing)、虚拟节点(分散多个副本,尽可能均匀)

为什么用哈希,一个原因可能是和缓存有关,如果一样的请求可以一直到同一个服务端,利用缓存可以提高效率加快响应速度。

最少连接数:每次选择当前连接数最少的

但当前连接数并不代表实际负载,尤其是可以多路复用

最少活跃数:活跃请求是已经接受但是还没返回的请求,客户端自己维护,每次选择最少的。但同样这不代表负载,活跃请求有可能是延迟/大请求造成的。

最快响应时间:客户端维持每个节点的响应时间,而后每次挑选响应时间最短的。

这些都是单一指标,也可以用 CPU 等负载作为均衡算法,但可能需要中间代理来记录这些信息,然后转发

让客户端来维护就很奇怪

基于一致性哈希的改进:

Rendezvous hashing 能获得更好的负载均衡,因为它需要计算所有节点的哈希值 hashfunction.hash(key + node.key()); 最后选择最大的

而一致性哈希只计算当前 key 的哈希值,在哈希环上找

详细对比:https://blog.prochase.top/2024/08/rendezvous-hashing/

熔断

熔断,限流,降级等等是微服务架构可用性的一些保障

熔断:微服务本身出问题时,拒绝服务直到恢复,类似股票的熔断机制 circuit breaker。熔断期间就可以恢复服务端.

判定服务的健康状态

如何判断微服务出现问题?一些指标:响应时间、错误率等等,但问题是什么阈值才是最合适的,超过阈值之后什么时候触发熔断

比如响应时间超过 1s 2s 之类的,如果 p99 = 1s 那么可以设置 1.2s 作为熔断阈值

一旦超过就要触发熔断吗?还是需要响应时间超过一段时间后才触发。防止偶发和抖动 jitter

服务恢复

熔断后需要拒绝服务,结束熔断后需要恢复服务。但如果发生抖动,频繁在正常和熔断两个状态切换怎么办?比如就根本支撑不了 QPS 1000 的服务,熔断后还是有 QPS 1000 的请求。

熔断可以逐步放开流量,还是让客户端控制流量去请求别的节点。

降级

熔断、降级、限流都是微服务架构的可用性保障

高峰期间会关闭一些服务,腾出服务器资源和减少公共组件比如数据库的压力,

降级的条件和熔断很像,如何判断什么时候降级,降级后怎么恢复,怎么处理抖动?

但还有个问题,什么服务能降级什么服务不能?

写服务一般不能降级,从前端接受数据然后写到数据库这种服务一般不降级。

  • 跨服务降级:资源不够的时候暂停某些服务,让给其他更重要和核心的服务。
  • 本服务提供有损服务:比如 app 首页也存在降级,触发降级后跨年不会做个性化推荐而使用静态页面。

降级思路:返回默认、禁用可观测组件,埋点,降低采样率等等、同步转异步、简化流程…

降级的恢复和抖动?主要是控制流量,服务端或者客户端,部分流量依旧熔断/降级。

例子:读服务 QPS 更高更重要,那么就可以降级写服务,比如商店页面读,商家写,高峰期间可以暂停写入。

以前我会以为高峰期间增加冗余和副本才是更好的选择,但实际上增加副本可能导致更多的成本和问题,对公共组件增加更多的负担,甚至多个副本间产生一致性问题。

为了高可用,拒绝服务熔断和降级保证可用应该是成本更低的手段。

限流

limiting 限流就更加常见了,但也同样更复杂,阈值更加难定。

限流通过限制流量来提高可用性,防止异常流量突发打崩系统,

限流算法

  • 静态限流:令牌桶、漏桶、固定窗口、滑动窗口
  • 动态限流(自适应限流):BBR 等利用一系列指标来判定是否应该减少流量或方法流量,类似 TCP 拥塞控制。

令牌桶:系统以恒定速率产生令牌,放到桶里,每个请求只有拿到了令牌才能执行。

漏桶:限流器以均匀速度交给业务逻辑,来处理不稳定的请求速度,可以类比匀速的令牌桶

固定窗口:在一个固定时间段只执行固定数量的请求,比如一秒内一百个请求

滑动窗口:同样,一个窗口内只能执行固定数量的请求,但是会平滑移动窗口

如何限流?借助 redis 中间件记录流量和阈值,或者利用网关来限流。

这里也有一些业务逻辑,vip 用户不限流什么的,ip 限流,普通用户限流等等。

BBR 动态限流:自适应(QPS 指标,响应时间 RT) + 滑动窗口 + 限流公式

bilibili 开源的奎托斯 go kratos 框架 https://github.com/go-kratos/kratos

其限流器就是用 bbr limiter 实现的

https://go-kratos.dev/en/docs/component/middleware/ratelimit/

https://github.com/go-kratos/aegis/tree/main/ratelimit/bbr

隔离

隔离比较少见?但对于高可用也很重要,分离普通用户和 VIP 用户等等,也可以提高可用性、性能和安全性。

实例隔离:某个服务独享某个实例全部资源,无共享。而不是一台机器多个服务共享。

分组隔离:

连接池隔离/线程池隔离:

第三方依赖隔离:越是关键的业务,业务上越是关键的路径,就越要小心隔离

超时控制

超时控制同样也是可用性的一个方案,指在规定的时间内完成操作,如果不能完成,那么就返回一个超时响应。

  1. 确保客户端能在预期的时间内拿到响应,而不是没有任何响应。
  2. 及时释放资源,尤其是释放线程和连接,比如 go 协程会被一直占有。释放 RPC 连接和数据库连接等等。

实际操作中也遇到过这种问题,go 协程一直未被释放导致协程挤压和切换最后连接数据库超时了

  • 超时控制形态:调用超时、链路超时

如何确定超时时间:用户体验、被调用接口响应时间、压测、代码。。。

一般来说都根据用户体验,比如产品经理认为 300ms 等待时间是合理的。根据响应时间,可以选择 p99 或者 p999 等 tail latency

压力测试可以测到 p99 和 p999 线等,但如果很难压力测试,可以尝试代码推算,比如有三次数据库操作和 redis 操作和发送消息操作,需要将他们全部 加起来,并且加一些余量。

超时中断业务:如果业务逻辑含有多个业务,其中一个超时怎么办?比如链路超时,可以用协议头传递超时时间,比如 rcp 协议头,http 协议头等等, 至于传递剩余时间还是超时时间,前者需要考虑网络传输时间,后者需要考虑时钟同步和偏移问题。

调用第三方

任何系统都可能需要和第三方打交道,但是如何保证第三方接口的可用性?

比如微信支付等等,实习期间也需要接入各种三方 API,比如 loki query 等等,

三方 API 基本会有限流,失败了怎么办?重试?

测试环境下一般需要 mock 第三方服务的响应

综合

如何保证微服务应用的高可用性?高并发、高可用和大数据

可用性:SLA Service Level Aggrement 比如 99.9%可用性,全年只有 8.76h 停机时间。如何做到高可用?

  • 容错:熔断、重试、限流、降级、负载均衡、隔离等等
  • 限制故障影响范围:隔离,相互依赖,共享基础设施。
  • 出现故障,快速修复:完备的观测和告警系统
  • 规范变更流程:review 等等

RPC

常规 HTTP 调用,通过域名,发送参数和协议头等等,中间需要经过 DNS 等等

而 RPC 更像调用本地方法,和 HTTP 不同的是,采用了体积更小的 protobuf 序列化协议来保存结构体,也不需要考虑 HTTP 状态码比如 302 重定向等等,这在微服务架构中表现更好。

HTTP 是应用层的协议,RPC 可以基于 TDP 也可以基于 UDP,比 HTTP 更早出现,

gRPC 底层用的 HTTP/2

所以是一种架构概念而不是通用协议?

完整的 RPC 流程: stub -> serialization -> tcp

  1. 定义 IDL 文件比如 protobuf,生成 stub 文件(静态库和函数映射)
  2. 网络传输的数据是二进制数据,需要 encode decode 参数和结果
  3. 根据 RPC 协议约定数据头、元数据、消息体,保证有
  4. 基于 TCP/UDP 传输

stub 其实就是一段代码,客户端 stub 可以是远端代码的表示,服务端 stub

HTTP 一般用 JSON 序列化,一般需要用反射来得到类型

而 Protobuf 体积更小,序列化和反序列化更快

HTTP 一般有很多协议头,而微服务一般不需要这些,用 RPC 更适合

gRPC 用 HTTP/2 拥有多路复用、优先级控制、头部压缩等优势,具有 连接池

RPC 框架一般需要生成代码,比如 protobuf。需要序列化和反序列化,将 object 变成二进制字节流。

具有安全性、通用性、和兼容性,同时性能很好。

协议层

支持解析多种协议,包含 HTTP, HTTP2, HTTP3 自定义 RPC 协议,私有协议等等

大厂内部大部分使用自定义 RPC 协议,TCP 中的二进制数据包会被拆分、合并,需要应用层协议确定边界。

gRPC, Thrift 等等

网络传输层一般使用成熟的网络通信框架,比如 Netty 和 RPC 解耦。

IO 多路复用,实现可靠传输等等。

RPC 不足

RPC 协议本身无法解决微服务集群的问题:服务发现等等,需要其他工具

调用方比如客户端,对服务端的 RPC 接口有强依赖关系,需要自动化工具、版本管理工具来保证代码级别的依赖,比如 stub 文件的更新。

RPC 热门框架

跨语言调用:grpc, thrift 提供基础的 RPC 通信能力,专注跨语言调用等等,但不带有服务治理等机制,需要其他框架来实现服务发现和负载均衡等等。

服务治理:rpcx, kitex, dubbo 等,提供 rpc 通信(多消息传输协议比如序列化协议、多网络通信协议比如 TCP, UDP, HTTP/2 和 QUIC 等等、服务定义和函数映射)并且提供服务发现、负载均衡等服务。

https://github.com/cloudwego/kitex

https://www.cloudwego.io/

https://www.cloudwego.io/docs/kitex/getting-started/pre-knowledge/

Kitex Demo

https://github.com/cloudwego/kitex-examples

https://github.com/cloudwego/kitex-examples/tree/main/bizdemo/kitex_gorm

使用 thrift 生成 RPC IDL(Remote Procedure Call Interface Definition Language)

生成的文件在 kitex_gen/user

然后在 handler 上实现

其他

设计幂等接口,针对写请求,可以对请求进行去重,确保同一个请求处理一次和多次的结果是相同的

  • 请求方每次请求生成唯一的 id,首次调用和重试时,唯一的 ID 保持不变
  • 服务端接受请求时,检查 ID 是否被处理过,如果处理过不需要再重复执行业务逻辑。

分布式锁如何实现幂等性:https://juejin.cn/post/6965740344335925279

幂等设计 https://juejin.cn/post/7049140742182141959