文章摘要
本文以《Netty权威指南(第二版)》第23章可靠性为基础,结合实际项目中的应用,实现RPC框架的可靠性设计。当前权作为自己的学习笔记,后续慢慢填充自己在项目中的实践(如果懒癌没犯的话)。
[TOC]
内容
RPC 框架的可靠性设计
1. 故障
1.1 分布式调用引入的故障
1.消息的序列化和反序列化故障,例如,不支持的数据类型。
2.路由故障:包括服务的订阅、发布故障,服务实例故障之后没有及时刷新路由表,导致 RPC 调用仍然路由到故障节点。
3.网络通信故障,包括网络闪断、网络单通、丢包、客户端浪涌接入等。
1.2 第三方服务依赖
数据库服务、文件存储服务、缓存服务、消息队列服务
1.网络通信类故障, 如果采用 BIO 调用第三方服务,很有可能被阻塞。
2.“雪崩效用”导致的级联故障,例如服务端处理慢导致客户端线程被阻塞。
3.第三方不可用导致 RPC 调用失败。
2. 通信层的可靠性设计
2.1 链路有效性检测
心跳检测机制分为三个层面:
1.TCP 层面的心跳检测,即 TCP 的 Keep-Alive 机制,它的作用域是整个 TCP 协议栈。
2. 协议层的心跳检测,主要存在于长连接协议中。例如 MQTT 协议。
3. 应用层的心跳检测,它主要由各业务产品通过约定方式定时给对方发送心跳消息实现。
心跳检测策略如下:
1.连续 N 次心跳检测都没有收到对方的 Pong 应答消息或者 Ping 请求消息,则认为链路已经发生逻辑失效,这被称作心跳超时。
2.读取和发送心跳消息的时候如何直接发生了 IO 异常,说明链路已经失效,这被称为心跳失败。
2.2 客户端断连重连
1.服务端因为某种原因,主动关闭连接,客户端检测到链路被正常关闭。
2.服务端因为宕机等故障,强制关闭连接,客户端检测到链路被 Rest 掉。
3.心跳检测超时,客户端主动关闭连接。
4.客户端因为其它原因(例如解码失败),强制关闭连接。
5.网络类故障,例如网络丢包、超时、单通等,导致链路中断。
1 | public int checkHealth() {} |
2.3 缓存重发
消息队列中积压了部分消息,此时链路中断,这会导致部分消息并没有真正发送给通信对端
1.调用 Netty ChannelHandlerContext 的 write 方法时,返回 ChannelFuture 对象,我们在 ChannelFuture 中注册发送结果监听 Listener。
2.在 Listener 的 operationComplete 方法中判断操作结果,如果操作不成功,将之前发送的消息对象添加到重发队列中。
3.链路重连成功之后,根据策略,将缓存队列中的消息重新发送给通信对端。
1 | //最终写入数据 |
2.4 客户端超时保护
1. 在同步阻塞 I/O 模型中,连接操作是同步阻塞的,如果不设置超时时间,客户端 I/O 线程可能会被长时间阻塞,这会导致系统可用 I/O 线程数的减少。
2. 业务层需要:大多数系统都会对业务流程执行时间有限制,例如 WEB 交互类的响应时间要小于 3S。客户端设置连接超时时间是为了实现业务层的超时。
2.5 针对客户端的并发连接数流控
检查登录次数
目前简单判断一段时间内不允许再登录
2.6 内存保护
1. 链路总数的控制:每条链路都包含接收和发送缓冲区,链路个数太多容易导致内存溢出。
2. 单个缓冲区的上限控制:防止非法长度或者消息过大导致内存溢出。
缓冲区的创建方式通常有两种:
1. 容量预分配,在实际读写过程中如果不够再扩展。
1 | ByteBuf byteBuf = com.eastmoney.quote.service.core.common.BufAllocator.newBuffer(resMinData.getByteData().length + 28); |
2. 根据协议消息长度创建缓冲区。
3. 缓冲区内存释放:防止因为缓冲区使用不当导致的内存泄露。
4. NIO 消息发送队列的长度上限控制。
1 | if (buf.readableBytes() >= ServerInfo.getInstance().getChunkSize()) { |
3. RPC 调用层的可靠性设计
3.1 RPC 调用异常场景
3.1.1 服务路由失败
3.1.2 服务端超时
3.1.3 服务端调用失败
3.2 RPC 调用可靠性方案
3.2.1 注册中心与链路检测双保险机制
3.2.2 集群容错策略
4. 第三方服务依赖故障隔离
4.1 总体策略
4.2 异步化
4.3. 基于 Hystrix 的第三方依赖故障隔离