sofabolt-sofa体系的基石


theme: cyanosis

highlight: agate

前言


最近从sofarpc摸到sofabolt,它是整个sofa体系的基石,比如说sofarpc里面bolt协议,还有sofaregistry里面pingpong、注册机制也是基于sofabolt通讯框架来实现,可想而知这个框架是基础框架中的基础,哈哈。

下面我们通过源码来认识下sofabolt的实现思路~

概括


image.png

sofa这套框架自定义了对应protocol,底层通过netty交互,rpc框架则不同,不能要求大家统一走你的bolt协议对吧,需要支持http协议、tcp协议这些。

image.png

sofabolt 实现了以上功能点,底层接入netty框架,对应中间件pingpong心跳机制也得有,然后通讯里面有同步、异步,请求自然会有超时的情况需要做兜底处理,比如里面有failfast、failover,我们回忆一下熔断,一般会写一个rollback方法,而不是直接返回一串系统默认的超时信息。

请求过程


BaseRemoting

com.alipay.remoting.BaseRemoting#invokeSync

``` protected RemotingCommand invokeSync(final Connection conn, final RemotingCommand request, final int timeoutMillis) throws RemotingException, InterruptedException { int remainingTime = remainingTime(request, timeoutMillis); if (remainingTime <= ABANDONINGREQUESTTHRESHOLD) { // already timeout LOGGER .warn( "already timeout before writing to the network, requestId: {}, remoting address: {}", request.getId(), conn.getUrl() != null ? conn.getUrl() : RemotingUtil.parseRemoteAddress(conn .getChannel())); return this.commandFactory.createTimeoutResponse(conn.getRemoteAddress()); }

final InvokeFuture future = createInvokeFuture(request, request.getInvokeContext());
conn.addInvokeFuture(future);
final int requestId = request.getId();
InvokeContext invokeContext = request.getInvokeContext();
if (null != invokeContext) {
    invokeContext.put(InvokeContext.BOLT_PROCESS_CLIENT_BEFORE_SEND, System.nanoTime());
}
try {
    conn.getChannel().writeAndFlush(request).addListener(new ChannelFutureListener() {

        @Override
        public void operationComplete(ChannelFuture f) throws Exception {
            if (!f.isSuccess()) {
                conn.removeInvokeFuture(requestId);
                future.putResponse(commandFactory.createSendFailedResponse(
                    conn.getRemoteAddress(), f.cause()));
                LOGGER.error("Invoke send failed, id={}", requestId, f.cause());
            }
        }

    });
    if (null != invokeContext) {
        invokeContext.put(InvokeContext.BOLT_PROCESS_CLIENT_AFTER_SEND, System.nanoTime());
    }
} catch (Exception e) {
    conn.removeInvokeFuture(requestId);
    future.putResponse(commandFactory.createSendFailedResponse(conn.getRemoteAddress(), e));
    LOGGER.error("Exception caught when sending invocation, id={}", requestId, e);
}
RemotingCommand response = future.waitResponse(remainingTime);

if (null != invokeContext) {
    invokeContext.put(InvokeContext.BOLT_PROCESS_CLIENT_RECEIVED, System.nanoTime());
}

if (response == null) {
    conn.removeInvokeFuture(requestId);
    response = this.commandFactory.createTimeoutResponse(conn.getRemoteAddress());
    LOGGER.warn("Wait response, request id={} timeout!", requestId);
}

return response;

} ``` 超时处理,如果当前时间已经超过超时时间,那么就处理response,比如说debug模式很容易就超时。

InvokeFuture

这个是client客户端保存future,通过里面response来获取请求结果。那么多的future怎么知道我回传的时候是之前那个请求的呢?

答案是requestId,就是每次请求的时候会生成一个请求id,然后netty回传信息的时候,通过requestd回查我之前是那个invokeFuture请求的,然后将数据塞回去。

``` conn.getChannel().writeAndFlush(request).addListener(new ChannelFutureListener() {

@Override
public void operationComplete(ChannelFuture f) throws Exception {
    if (!f.isSuccess()) {
        conn.removeInvokeFuture(requestId);
        future.putResponse(commandFactory.createSendFailedResponse(
            conn.getRemoteAddress(), f.cause()));
        LOGGER.error("Invoke send failed, id={}", requestId, f.cause());
    }
}

}); ```

这段就是netty代码了,拿到channel往里面怼数据,如果发送失败则直接塞回去response。

RemotingCommand response = future.waitResponse(remainingTime);

上面这段是等待请求响应,具体方法实现在下面

@Override public ResponseCommand waitResponse(long timeoutMillis) throws InterruptedException { this.countDownLatch.await(timeoutMillis, TimeUnit.MILLISECONDS); return this.responseCommand; }

DefaultInvokeFuture

这一块的客户端请求的过程,里面也会有一些有意思的点,会是会通过CountDownLatch来阻塞请求链接,当请求还没响应的时候,阻塞请求线程,当超时或者响应回复之后,CountDownLatch countDown方法将线程通下。

这个比较有意思的地方,我们可以学习一下,就是通过CountDownLatch来控制阻塞,然后通过超时方法,如果response字段为空,塞个空的值回去。

请求回传过程


RpcHandler

com.alipay.remoting.rpc.RpcHandler#channelRead

@ChannelHandler.Sharable
public class RpcHandler extends ChannelInboundHandlerAdapter {
    private boolean                                     serverSide;

    private ConcurrentHashMap<String, UserProcessor<?>> userProcessors;

    public RpcHandler(ConcurrentHashMap<String, UserProcessor<?>> userProcessors) {
        serverSide = false;
        this.userProcessors = userProcessors;
    }

    public RpcHandler(boolean serverSide, ConcurrentHashMap<String, UserProcessor<?>> userProcessors) {
        this.serverSide = serverSide;
        this.userProcessors = userProcessors;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ProtocolCode protocolCode = ctx.channel().attr(Connection.PROTOCOL).get();
        Protocol protocol = ProtocolManager.getProtocol(protocolCode);
        protocol.getCommandHandler().handleCommand(
            new RemotingContext(ctx, new InvokeContext(), serverSide, userProcessors), msg);
        ctx.fireChannelRead(msg);
    }
}

处理请求,我们再看.alipay.remoting.rpc.protocol.RpcCommandHandler#handle 这方法,会判断msg的类型,如果是list的话是批量处理,如果是单个则单独处理。

image.png

com.alipay.remoting.rpc.protocol.RpcRequestProcessor#process

@Override
public void process(RemotingContext ctx, RpcRequestCommand cmd, ExecutorService defaultExecutor)
                                                                                                throws Exception {
    if (!deserializeRequestCommand(ctx, cmd, RpcDeserializeLevel.DESERIALIZE_CLAZZ)) {
        return;
    }
    UserProcessor userProcessor = ctx.getUserProcessor(cmd.getRequestClass());
    if (userProcessor == null) {
        String errMsg = "No user processor found for request: " + cmd.getRequestClass();
        logger.error(errMsg);
        sendResponseIfNecessary(ctx, cmd.getType(), this.getCommandFactory()
            .createExceptionResponse(cmd.getId(), errMsg));
        return;// must end process
    }

    // set timeout check state from user's processor
    ctx.setTimeoutDiscard(userProcessor.timeoutDiscard());

    // to check whether to process in io thread
    if (userProcessor.processInIOThread()) {
        if (!deserializeRequestCommand(ctx, cmd, RpcDeserializeLevel.DESERIALIZE_ALL)) {
            return;
        }
        // process in io thread
        new ProcessTask(ctx, cmd).run();
        return;// end
    }

    Executor executor;
    // to check whether get executor using executor selector
    if (null == userProcessor.getExecutorSelector()) {
        executor = userProcessor.getExecutor();
    } else {
        // in case haven't deserialized in io thread
        // it need to deserialize clazz and header before using executor dispath strategy
        if (!deserializeRequestCommand(ctx, cmd, RpcDeserializeLevel.DESERIALIZE_HEADER)) {
            return;
        }
        //try get executor with strategy
        executor = userProcessor.getExecutorSelector().select(cmd.getRequestClass(),
            cmd.getRequestHeader());
    }

    // Till now, if executor still null, then try default
    if (executor == null) {
        executor = (this.getExecutor() == null ? defaultExecutor : this.getExecutor());
    }

    cmd.setBeforeEnterQueueTime(System.nanoTime());
    // use the final executor dispatch process task
    executor.execute(new ProcessTask(ctx, cmd));
}

到这里我们就明白了,通讯回传的时候是在netty接收的,然后是批量则拆开处理,然后通过ProcessTask线程来处理响应的内容,这个类里面主要逻辑是如果是超时的话,set超时结果到future里面。

性能问题


对于开发者来讲,框架都是简单易用,很少会关心内部实现情况,所以一旦框架出现问题,那么排查起来会很麻烦。我认为框架设计的时候,需要对线程池有监控措施

我们从上面源码介绍可以知道,com.alipay.remoting.rpc.protocol.RpcRequestProcessor#process,这个方法里面会将netty响应结果,通过异步塞回到responseFuture里面,这样我们很容易联想到性能问题。

里面有个代码,如果当前io是被占用的话,通过新建线程来处理,一般来说是交给线程池来执行,就会出现棘手的问题,比如说在大并发的请求下,线程池会出现请求溢出的情况,默认的拒绝策略是拒绝,这样不管是rpc框架还是注册中心框架都是不友好的。即使netty远程正常响应,但是客户端线程池已经爆了,那很多请求没有响应结果,这是不能被容忍的。

所以我想法是对线程池有对应的监控,或者说有手册可以清晰将这个问题说明,比如说线程池最多承接5k请求,当超过这个程度动态自定义线程池,或者通过增加节点来避免这种情况。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SOFABolt 是蚂蚁金融服务集团开发的一套基于 Netty 实现的网络通信框架。 为了让 Java 程序员能将更多的精力放在基于网络通信的业务逻辑实现上,而不是过多的纠结于网络底层 NIO 的实现以及处理难以调试的网络问题,Netty 应运而生。 为了让中间件开发者能将更多的精力放在产品功能特性实现上,而不是重复地一遍遍制造通信框架的轮子,SOFABolt 应运而生。 Bolt 名字取自迪士尼动画-闪电狗,是一个基于 Netty 最佳实践的轻量、易用、高性能、易扩展的通信框架。 这些年我们在微服务与消息中间件在网络通信上解决过很多问题,积累了很多经验,并持续的进行着优化和完善,我们希望能把总结出的解决方案沉淀到 SOFABolt 这个基础组件里,让更多的使用网络通信的场景能够统一受益。 目前该产品已经运用在了蚂蚁中间件的微服务 (SOFARPC)、消息中心、分布式事务、分布式开关、以及配置中心等众多产品上。 SOFABolt的基础功能包括: 1、基础通信功能 ( remoting-core ) 基于 Netty 高效的网络 IO 与线程模型运用 连接管理 (无锁建连,定时断链,自动重连) 基础通信模型 ( oneway,sync,future,callback ) 超时控制 批量解包与批量提交处理器 心跳与 IDLE 事件处理 2、协议框架 ( protocol-skeleton ) 命令与命令处理器 编解码处理器 心跳触发器 3、私有协议定制实现 - RPC 通信协议 ( protocol-implementation ) RPC 通信协议的设计 灵活的反序列化时机控制 请求处理超时 FailFast 机制 用户请求处理器 ( UserProcessor ) 双工通信

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值