线上服务性能调优:RPC可靠性设计

文章摘要

本文以《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
3
4
5
6
7
8
9
10
11
12
13
//最终写入数据
channel.writeAndFlush(byteBuf).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
if ((!dataQueue.isEmpty()) && channel.isWritable() && (!isWriting())) {
dataProcessor();
} else {
//缓存重发
}
}
}
});

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
2
3
4
5
6
7
8
9
10
11
12
13
14
if (buf.readableBytes() >= ServerInfo.getInstance().getChunkSize()) {
writeBuf(buf);
break;
} else {
if (batchBuf == null) {
batchBuf = BufAllocator.newBuffer();
}
batchBuf.writeBytes(buf);
try {
buf.release();
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
}

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 的第三方依赖故障隔离

总结