RocketMQ 高性能RPC架构源码剖析

891 阅读6分钟

概述

在分析RocketMQ 高性能RPC框架之前,我们先回顾一下RocketMQ部署架构。

RocketMQ部署架构

RPC 协议只规定了 Client 与 Server 之间的点对点调用流程,包括 stub、通信协议、RPC 消息解析等部分。

在实际应用中,还需要考虑服务的高可用、负载均衡等问题,很多 RPC 框架指的是能够完成 RPC 调用的解决方案,除了点对点的 RPC 协议的具体实现外,还可以包括服务的发现与注销、提供服务的多台 Server 的负载均衡、服务的高可用等更多的功能。

目前的 RPC 框架大致有两种不同的侧重方向,一种偏重于服务治理,另一种偏重于跨语言调用。

比如seata和rocketmq都有自己的RPC框架,实现跨系统之间的通信。

关于什么是RPC这篇文章比较全面。

wjhsh.net/GreenForest…

上面部署架构,不同集群之间的每一次通信都是RPC调用的过程。

rocketmq-remoting 模块是 RocketMQ消息队列中负责网络通信的模块,它几乎被其他所有需要网络通信的模块(诸如rocketmq-client、rocketmq-broker、rocketmq-namesrv)所依赖和引用。为了实现客户端与服务器之间高效的数据请求与接收,RocketMQ消息队列自定义了通信协议并在Netty的基础之上扩展了通信模块。

1、rocketmq-remoting模块

image.png

RocketMQ代码分层和代码封装确实是非常的优雅。

2、RocketMQ中RPC模块架构

官方的Remoting通信模块的类结构图

RemotingService.png

1、RemotingService

顶层接口,提供了三个方法。

public interface RemotingService {
    void start();
    void shutdown();
    void registerRPCHook(RPCHook rpcHook);
}

2、RemotingClient/RemotingSever

两个接口继承了最上层接口—RemotingService,分别各自为Client和Server提供所必需的方法

3、NettyRemotingAbstract

Netty通信处理的抽象类,定义并封装了Netty处理的公共处理方法;

4、NettyRemotingServer/NettyRemotingClient

分别实现了RemotingClient和RemotingServer, 都继承了NettyRemotingAbstract抽象类。RocketMQ中其他的组件(如client、nameServer、broker在进行消息的发送和接收时均使用这两个组件)

3、消息协议的设计与编解码

在Client和Server之间完成一次消息发送时,需要对发送的消息进行一个协议约定,因此就有必要自定义RocketMQ的消息协议。同时,为了高效地在网络中传输消息和对收到的消息读取,就需要对消息进行编解码。在RocketMQ中,RemotingCommand这个类在消息传输过程中对所有数据内容的封装,不但包含了所有的数据结构,还包含了编码解码操作。

Header字段类型Request说明Response说明
codeint请求操作码,应答方根据不同的请求码进行不同的业务处理应答响应码。0表示成功,非0则表示各种错误
languageLanguageCode请求方实现的语言应答方实现的语言
versionint请求方程序的版本应答方程序的版本
opaqueint相当于requestId,在同一个连接上的不同请求标识码,与响应消息中的相对应应答不做修改直接返回
flagint区分是普通RPC还是onewayRPC得标志区分是普通RPC还是onewayRPC得标志
remarkString传输自定义文本信息传输自定义文本信息
extFieldsHashMap<String, String>请求自定义扩展信息响应自定义扩展信息

可见传输内容主要可以分为以下4部分:

(1) 消息长度:总长度,四个字节存储,占用一个int类型;

(2) 序列化类型&消息头长度:同样占用一个int类型,第一个字节表示序列化类型,后面三个字节表示消息头长度;

(3) 消息头数据:经过序列化后的消息头数据;

(4) 消息主体数据:消息主体的二进制字节数据内容;

对应源码RemotingCommand类

image.png

4、消息的通信方式和流程

在RocketMQ消息队列中支持通信的方式主要有以下三种:

  1. 同步(sync)
  2. 异步(async)
  3. 单向(oneway)

其中“单向”通信模式相对简单,一般用在发送心跳包场景下,无需关注其Response。

4.1 异步通信流程

image.png

官网上异步通信的流程图 1、客户端异步通信流程

客户端调用异步通信接口—invokeAsync,

@Override
public void invokeAsync(String addr, RemotingCommand request, long timeoutMillis, InvokeCallback invokeCallback)
    throws InterruptedException, RemotingConnectException, RemotingTooMuchRequestException, RemotingTimeoutException,
    RemotingSendRequestException {
    long beginStartTime = System.currentTimeMillis();
    // 根据addr获取相应的channel(如果本地缓存中没有则创建)
    final Channel channel = this.getAndCreateChannel(addr);
    if (channel != null && channel.isActive()) {
        try {
            doBeforeRpcHooks(addr, request);
            long costTime = System.currentTimeMillis() - beginStartTime;
            if (timeoutMillis < costTime) {
                throw new RemotingTooMuchRequestException("invokeAsync call timeout");
            }
            // 调用invokeAsyncImpl方法,将数据流转给抽象类NettyRemotingAbstract处理
            this.invokeAsyncImpl(channel, request, timeoutMillis - costTime, invokeCallback);
        } catch (RemotingSendRequestException e) {
            log.warn("invokeAsync: send request exception, so close the channel[{}]", addr);
            this.closeChannel(addr, channel);
            throw e;
        }
    } else {
        this.closeChannel(addr, channel);
        throw new RemotingConnectException(addr);
    }
}

真正做完发送请求动作的是在NettyRemotingAbstract抽象类的invokeAsyncImpl方法里面。

NettyRemotingAbstract#invokeAsyncImpl 方法

public void invokeAsyncImpl(final Channel channel, final RemotingCommand request, final long timeoutMillis,
                            final InvokeCallback invokeCallback)
        throws InterruptedException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException {
    long beginStartTime = System.currentTimeMillis();
​
    //opaque 对应于request ID
    //RemotingCommand会为每一个request产生一个request ID, 从0开始, 每次加1
    final int opaque = request.getOpaque();
​
    //尝试获得 semaphore 信号量,semaphore 默认为65535。
    boolean acquired = this.semaphoreAsync.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS);
    if (acquired) {
        final SemaphoreReleaseOnlyOnce once = new SemaphoreReleaseOnlyOnce(this.semaphoreAsync);
        long costTime = System.currentTimeMillis() - beginStartTime;
        if (timeoutMillis < costTime) {
            once.release();
            throw new RemotingTimeoutException("invokeAsyncImpl call timeout");
        }
​
        //根据request ID构建ResponseFuture
        final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis - costTime, invokeCallback, once);
        //将ResponseFuture放入responseTable
        this.responseTable.put(opaque, responseFuture);
        try {
            //使用Netty的channel发送请求数据
            channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture f) throws Exception {
                    if (f.isSuccess()) {
                        //如果发送消息成功给Server,那么这里直接Set发出标记后return
                        responseFuture.setSendRequestOK(true);
                        return;
                    }
​
                    //发送失败处理
                    requestFail(opaque);
                    log.warn("send a request command to channel <{}> failed.", RemotingHelper.parseChannelRemoteAddr(channel));
                }
            });
        } catch (Exception e) {
            responseFuture.release();
            log.warn("send a request command to channel <" + RemotingHelper.parseChannelRemoteAddr(channel) + "> Exception", e);
            throw new RemotingSendRequestException(RemotingHelper.parseChannelRemoteAddr(channel), e);
        }
    } else {
        if (timeoutMillis <= 0) {
            throw new RemotingTooMuchRequestException("invokeAsyncImpl invoke too fast");
        } else {
            String info =
                    String.format("invokeAsyncImpl tryAcquire semaphore timeout, %dms, waiting thread nums: %d semaphoreAsyncValue: %d",
                            timeoutMillis,
                            this.semaphoreAsync.getQueueLength(),
                            this.semaphoreAsync.availablePermits()
                    );
            log.warn(info);
            throw new RemotingTimeoutException(info);
        }
    }
}

对于异步通信来说,invokeCallback是在收到消息响应的时候能够根据responseTable找到请求码对应的回调执行方法,

2、服务端响应流程

Server端接收消息的处理入口在NettyServerHandler类的channelRead0方法中,其中调用了processMessageReceived方法

NettyRemotingAbstract#processMessageReceived

public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception {
    final RemotingCommand cmd = msg;
    if (cmd != null) {
        switch (cmd.getType()) {
            case REQUEST_COMMAND:
                processRequestCommand(ctx, cmd);
                break;
            case RESPONSE_COMMAND:
                processResponseCommand(ctx, cmd);
                break;
            default:
                break;
        }
    }
}

根据RemotingCommand的请求业务码来匹配到相应的业务处理器;然后生成一个新的线程提交至对应的业务线程池进行异步处理。

image.png

/**
 * This container holds all processors per request code, aka, for each incoming request, we may look up the
 * responding processor in this map to handle the request.
 */
protected final HashMap<Integer/* request code */, Pair<NettyRequestProcessor, ExecutorService>> processorTable =
        new HashMap<Integer, Pair<NettyRequestProcessor, ExecutorService>>(64);

4.2 同步消息通信流程

NettyRemotingClient#invokeSync

参考官方的图异步通信流程,基本差不多

4.3 单向消息同步流程

NettyRemotingClient#invokeOneway

总结:设计的小技巧

  1. semaphore 信号量,semaphore 默认为65535, 异步消息流控
  2. SemaphoreReleaseOnlyOnce 无锁编程 CAS的
  3. RocketMQ这种做法是为了给不同类型的请求业务码指定不同的处理器Processor处理,同时消息实际的处理并不是在当前线程,而是被封装成task放到业务处理器Processor对应的线程池中完成异步执行。最大程度的保障并发。

客户端的响应缓存Map堆积处理?

responseTable的本地缓存Map可能出现堆积情况。

在发送消息时候,如果遇到异常情况 。比如服务端没有response返回给客户端或者response因网络而丢失 这个时候需要一个定时任务来专门做responseTable的清理回收。

在RocketMQ的客户端/服务端启动时候会产生一个频率为1s调用一次来的定时任务检查所有的responseTable缓存中的responseFuture变量,判断是否已经得到返回, 并进行相应的处理。

image.png

public void scanResponseTable() {
    final List<ResponseFuture> rfList = new LinkedList<ResponseFuture>();
    Iterator<Entry<Integer, ResponseFuture>> it = this.responseTable.entrySet().iterator();
    while (it.hasNext()) {
        Entry<Integer, ResponseFuture> next = it.next();
        ResponseFuture rep = next.getValue();

        if ((rep.getBeginTimestamp() + rep.getTimeoutMillis() + 1000) <= System.currentTimeMillis()) {
            rep.release();
            it.remove();
            rfList.add(rep);
            log.warn("remove timeout request, " + rep);
        }
    }

    for (ResponseFuture rf : rfList) {
        try {
            executeInvokeCallback(rf);
        } catch (Throwable e) {
            log.warn("scanResponseTable, operationComplete Exception", e);
        }
    }
}

5、Reactor多线程设计

RocketMQ的RPC通信采用Netty组件作为底层通信库,同样也遵循了Reactor多线程模型,同时又在这之上做了一些扩展和优化。

rocketmq_design_6.png

这个后面配合单测,重新梳理一下,这边的代码比较复杂。

参考文档

github.com/apache/rock…
RocketMQ 原理 、部署、使用架构师-尼恩的博客-CSDN博客
什么是RPC?