阅读笔记:微服务架构
微服务架构
回顾下微服务架构的一些知识,包括服务发现、负载均衡、可用性,很多东西没有实操过所以难以记住,需要多多回顾。
来自极客时间的课程 https://time.geekbang.org/column/intro/100551601
后端工程师的高阶面经
服务注册与发现
-
服务端启动需要向注册中心里注册自身的信息,保持心跳
-
客户端调用,需要找注册中心获取服务节点,并且缓存列表。
-
客户端与注册中心也要保持心跳和数据同步,服务端发生变动需要通知客户端更新
-
客户端发送请求,服务端返回响应
如果服务端下线怎么办?
服务端通知注册中心准备下线,注册中心通知客户端更新列表,客户端不再发请求到该服务端,服务端等待一段时间后下线。
等待时间是需要的
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/
隔离
隔离比较少见?但对于高可用也很重要,分离普通用户和 VIP 用户等等,也可以提高可用性、性能和安全性。
实例隔离:某个服务独享某个实例全部资源,无共享。而不是一台机器多个服务共享。
分组隔离:
连接池隔离/线程池隔离:
第三方依赖隔离:越是关键的业务,业务上越是关键的路径,就越要小心隔离
超时控制
超时控制同样也是可用性的一个方案,指在规定的时间内完成操作,如果不能完成,那么就返回一个超时响应。
- 确保客户端能在预期的时间内拿到响应,而不是没有任何响应。
- 及时释放资源,尤其是释放线程和连接,比如 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
- 定义 IDL 文件比如 protobuf,生成 stub 文件(静态库和函数映射)
- 网络传输的数据是二进制数据,需要 encode decode 参数和结果
- 根据 RPC 协议约定数据头、元数据、消息体,保证有
- 基于 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/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