
Netty RPC框架设计笔记:协议、线程模型与性能调优
在分布式系统开发中,服务间的远程调用是基础能力。目前团队内各服务使用HTTP+JSON通信,存在以下问题:
本次自研RPC框架的目标是:
TCP流式传输必须解决粘包/拆包问题。参考Dubbo协议设计,采用定长Header + 变长Body结构。
协议格式(共16字节头 + Body):
字段 | 长度 | 说明 |
|---|---|---|
Magic | 4B | 0xCAFEBABE,用于快速校验 |
Version | 1B | 协议版本,便于后续升级 |
SerializeType | 1B | 0=JSON, 1=Protobuf, 2=Hessian |
Type | 1B | 0=请求, 1=响应, 2=心跳 |
Status | 1B | 200成功, 500失败 |
RequestId | 8B | 雪花算法生成,用于请求-响应匹配 |
BodyLength | 4B | Body字节长度 |
Body | 变长 | 序列化后的数据 |
设计决策:
LengthFieldBasedFrameDecoder可零拷贝处理粘包编码器(出站):
继承MessageToByteEncoder,使用PooledByteBufAllocator分配堆外内存,避免数据在JVM堆和内核态之间拷贝。
@Slf4j
public class RpcEncoder extends MessageToByteEncoder<RpcProtocol> {
private final Serializer serializer;
public RpcEncoder(Serializer serializer) {
this.serializer = serializer;
}
@Override
protected void encode(ChannelHandlerContext ctx, RpcProtocol msg, ByteBuf out) {
byte[] bodyBytes = serializer.serialize(msg.getBody());
msg.setBodyLength(bodyBytes.length);
msg.setBody(bodyBytes);
// 使用Direct Buffer,减少GC压力
ByteBuf buffer = ctx.alloc().directBuffer(16 + bodyBytes.length);
buffer.writeInt(msg.getMagic());
buffer.writeByte(msg.getVersion());
buffer.writeByte(msg.getSerializeType());
buffer.writeByte(msg.getType());
buffer.writeByte(msg.getStatus());
buffer.writeLong(msg.getRequestId());
buffer.writeInt(msg.getBodyLength());
buffer.writeBytes(bodyBytes);
out.writeBytes(buffer);
buffer.release(); // 手动释放,引用计数-1
}
}解码器(入站):
使用LengthFieldBasedFrameDecoder处理粘包。关键参数说明:
maxFrameLength = 8MB:防止恶意大包OOMlengthFieldOffset = 16:跳过固定头到长度字段lengthFieldLength = 4lengthAdjustment = -16:因为长度字段不包含头,需回退public class RpcDecoder extends LengthFieldBasedFrameDecoder {
private final Serializer serializer;
public RpcDecoder(Serializer serializer) {
super(8 * 1024 * 1024, 16, 4, -16, 0);
this.serializer = serializer;
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
ByteBuf frame = (ByteBuf) super.decode(ctx, in);
if (frame == null) return null;
try {
int magic = frame.readInt();
if (magic != 0xCAFEBABE) {
throw new IllegalStateException("Invalid magic: " + magic);
}
byte version = frame.readByte();
byte serializeType = frame.readByte();
byte type = frame.readByte();
byte status = frame.readByte();
long requestId = frame.readLong();
int bodyLength = frame.readInt();
byte[] bodyBytes = new byte[bodyLength];
frame.readBytes(bodyBytes);
Object body = null;
if (type == 0) {
body = serializer.deserialize(bodyBytes, RpcRequest.class);
} else if (type == 1) {
body = serializer.deserialize(bodyBytes, RpcResponse.class);
}
return RpcProtocol.builder()
.magic(magic).version(version).serializeType(serializeType)
.type(type).status(status).requestId(requestId)
.bodyLength(bodyLength).body(body).build();
} finally {
frame.release();
}
}
}踩坑记录:lengthAdjustment负值容易算错,实际测试时通过抓包确认偏移;frame.release()必须放在finally,否则内存泄漏。
定义统一接口,支持运行时切换:
public interface Serializer {
<T> byte[] serialize(T obj);
<T> T deserialize(byte[] data, Class<T> clazz);
}JSON实现使用Jackson,Protobuf实现需生成Builder类。选择策略:开发调试用JSON,生产环境用Protobuf(QPS提升约25%)。
客户端使用JDK动态代理,将接口方法调用转为网络请求。关键设计:
CompletableFuture实现异步转同步,支持超时控制ConcurrentHashMap<Long, CompletableFuture<RpcResponse>>用于请求-响应匹配public class RpcClient {
private final ConcurrentMap<Long, CompletableFuture<RpcResponse>> pendingRequests = new ConcurrentHashMap<>();
public CompletableFuture<RpcResponse> sendRequest(String host, int port, RpcRequest request) {
Channel channel = bootstrap.connect(host, port).syncUninterruptibly().channel();
CompletableFuture<RpcResponse> future = new CompletableFuture<>();
pendingRequests.put(request.getRequestId(), future);
RpcProtocol protocol = RpcProtocol.builder()
.requestId(request.getRequestId()).type((byte) 0).body(request).build();
channel.writeAndFlush(protocol).addListener((ChannelFutureListener) f -> {
if (!f.isSuccess()) {
pendingRequests.remove(request.getRequestId());
future.completeExceptionally(f.cause());
}
});
// 超时检测
scheduler.schedule(() -> {
CompletableFuture<RpcResponse> f = pendingRequests.remove(request.getRequestId());
if (f != null && !f.isDone()) {
f.completeExceptionally(new TimeoutException("Timeout after 3s"));
}
}, 3000, TimeUnit.MILLISECONDS);
return future;
}
// 由Handler回调
public void handleResponse(RpcResponse response) {
CompletableFuture<RpcResponse> f = pendingRequests.remove(response.getRequestId());
if (f != null) f.complete(response);
}
}问题与解决:早期版本使用future.get()同步阻塞,导致连接池耗尽。改为CompletableFuture+超时调度后,吞吐量提升约40%。
Netty的EventLoop(I/O线程)只负责解码和轻量级转发,业务逻辑必须提交到独立线程池,否则数据库查询等慢操作会阻塞所有连接。
@Slf4j
public class RpcServerHandler extends SimpleChannelInboundHandler<RpcProtocol> {
// 核心200,最大500,队列2000,拒绝策略为CallerRuns(减缓生产者)
private final ExecutorService bizExecutor = new ThreadPoolExecutor(
200, 500, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2000),
new NamedThreadFactory("rpc-biz-"),
new ThreadPoolExecutor.CallerRunsPolicy()
);
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcProtocol protocol) {
bizExecutor.submit(() -> {
try {
RpcRequest request = (RpcRequest) protocol.getBody();
Object serviceBean = serviceRegistry.get(request.getClassName());
Method method = serviceBean.getClass().getMethod(request.getMethodName(), request.getParamTypes());
Object result = method.invoke(serviceBean, request.getParameters());
RpcResponse response = RpcResponse.builder()
.requestId(request.getRequestId()).status((byte) 200).data(result).build();
ctx.writeAndFlush(RpcProtocol.builder().requestId(request.getRequestId()).type((byte) 1).body(response).build());
} catch (Exception e) {
log.error("Invoke error", e);
// 返回异常响应
}
});
}
}线程池参数依据:根据压测(8核16G),200核心线程可保证CPU利用率在80%左右,队列2000防止内存溢出。
使用Curator实现,服务注册时创建临时节点,会话断开自动移除,实现健康检查。
public class ZkServiceRegistry implements ServiceRegistry {
private final CuratorFramework client;
private final String basePath = "/rpc/services";
@Override
public void register(String serviceName, ServiceInstance instance) {
String path = basePath + "/" + serviceName + "/" + instance.getHost() + ":" + instance.getPort();
client.create().creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL)
.forPath(path, instance.toJson().getBytes(StandardCharsets.UTF_8));
}
@Override
public List<ServiceInstance> getInstances(String serviceName) {
List<String> children = client.getChildren().forPath(basePath + "/" + serviceName);
// 解析每个节点的数据
}
}注意:生产环境需处理ZooKeeper会话超时重连,Curator的ExponentialBackoffRetry已覆盖。
public class WeightedRandomLoadBalancer implements LoadBalancer {
@Override
public ServiceInstance select(List<ServiceInstance> instances) {
int totalWeight = instances.stream().mapToInt(ServiceInstance::getWeight).sum();
int offset = ThreadLocalRandom.current().nextInt(totalWeight);
for (ServiceInstance inst : instances) {
offset -= inst.getWeight();
if (offset < 0) return inst;
}
return instances.get(instances.size() - 1);
}
}一致性Hash实现已做,但加权随机在测试中分布更均匀,且无迁移开销,故作为默认策略。
环境:8核16G,千兆网络,服务端业务为纯内存计算(模拟无I/O阻塞)。
并发数 | QPS | 平均RT | 99% RT | GC频率(Minor/s) |
|---|---|---|---|---|
100 | 8500 | 11ms | 23ms | 0.8 |
500 | 12000 | 41ms | 89ms | 2.1 |
1000 | 13500 | 74ms | 156ms | 4.3 |
优化手段及收益:
PooledByteBufAllocator):GC频率降低约30%writeBufferWaterMark低水位(32KB),减少系统调用次数未解决的问题:并发超过1500时,QPS出现平台期,初步定位为ZooKeeper服务发现缓存更新开销,后续考虑本地缓存+定时刷新。
优雅停机:
bossGroup和workerGroup调用shutdownGracefully(0, 5, TimeUnit.SECONDS),等待现有请求处理完成shutdown() + awaitTermination(10, TimeUnit.SECONDS)全链路追踪:
在RpcRequest中增加traceId字段,服务端Handler中设置MDC.put("traceId", request.getTraceId()),日志自动关联。
心跳与空闲检测:
IdleStateHandler设置读空闲30s,超时发送Ping熔断与降级: 当前通过超时+重试(最多3次)实现简易容错。后续计划接入Resilience4j,按错误率触发熔断。
本次自研RPC框架的核心设计原则:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。