美文网首页
sofa-bolt学习

sofa-bolt学习

作者: zxRay | 来源:发表于2019-01-20 22:15 被阅读87次

    sofa-bolt是蚂蚁开源的一款基于Netty的网络通信框架。在Netty的基础上对网络编程常见问题进行了一层简单封装,让中间件开发者更关注于中间件产品本身。项目地址:sofa-bolt
    大体功能为:

    1. 连接管理
    2. 请求处理

    在网络编程时,IO模型的选择自然是首先需要考虑的,sofa-bolt作为蚂蚁内部中间件底层框架,当然是服务于高性能,高并发的场景。这种场景在Java体系下,必然选择非阻塞IO复用了,那么自然选择基于Netty进行开发

    连接管理

    连接管理,通常需要处理:

    1. 连接创建与销毁
    2. 心跳管理
    3. 空闲连接管理
    4. 断线重连
    5. 慢连接处理
    6. 作为一个框架,当然还需要把各种连接事件分派给用户进行定制

    第一个需要明确的是sofa-bolt使用的长连接,这很明显,因为它服务于相对稳定的内部网络,同时对RT有比较高的要求。另外,长连接排查问题也相对更有迹可循

    连接创建

    可以用socket(localIp,localPort, remoteIp,remotePort )代表一个连接,在Netty中用Channel来表示,在sofa-bolt使用Connection对象来抽象一个连接,一个连接在client跟server端各用一个connection对象表示

    有了Connection这个抽象之后,自然的需要提供接口来管理Connection, 这个接口就是ConnectionFactory 那么Connection是如何跟Netty进行联动呢,我们知道在Netty中,client连接到server后,server会回调initChannel方法,在这个方法我们会初始化各种事件handler, sofa-bolt就在这里创建Connection,并在Netty的Channel对象上打上Connection标,后续通过Channel就可以直接找到这个Connection
     protected void initChannel(SocketChannel channel) {
         ...
         createConnection(channel);
     }
    
    private void createConnection(SocketChannel channel) {
         new Connection(channel,..);
         ...
    }
    Connection(channel, ...){
        this.channel.attr(CONNECTION).set(this);
    }
    

    客户端的处理方式为:

    ChannelFuture future = bootstrap.connect(...);
    future.awaitUninterruptibly();
    Channel channel = future.channel();
    Connection conn = new Connection(channel,...);
    

    连接事件分派

    sofa-bolt是一个底层的通信框架,需要将连接事情分派给用户自行处理,在sofa-bolt中连接事件有:

    1. CREATE:连接创建
    2. CLOSE:连接销毁

      有ConnectionEventHandler转给ConnectionEventListener,再有ConnectionEventListener分派给用户自定义的ConnectionEventProcessor,为啥会有一个listen不得而知。
      因为用户的事件逻辑不可预知,放到了一个线程池中运行,防止阻塞IO

    心跳

    因为使用的是长连接,所以需要知道连接是否还正常,有多种情况可以导致系统认为连接还处在,但是实际上该连接已经死亡

    1. 中间某个链路断了,此时发送数据要么timeout,要么返回主机不可达
    2. 对端主机崩溃,同上
    3. 对端主机崩溃后又重启了服务,返回RST报文

    Tcp本身提供了KeepAlive机制,但使用这个功能需要操作系统做相关配置,同时,它不能发现应用进程导致的连接问题。比如,进程还在,但是卡在IO上,或者再FullGC又或者各种原因已经假死了,这个时候实际上也已经无法响应请求了。

    所以,通常应用层会自己去做心跳功能,其主要目的不仅仅为了探测网络是否正常,更重要的是探测对端服务进程是否还能正常提供服务。

    bolt采用client端定时发送心跳请求,服务端收到了响应心跳,使用了是Netty的IdleHandler机制,具体的心跳协议就不做描述了。

    pipeline.addLast("heartbeatHandler", heartbeatHandler);
    
    ## heartbeatHandler
    public void userEventTriggered(final ChannelHandlerContext ctx, Object evt) throws Exception {
            if (evt instanceof IdleStateEvent) {
                ProtocolCode protocolCode = ctx.channel().attr(Connection.PROTOCOL).get();
                Protocol protocol = ProtocolManager.getProtocol(protocolCode);
                protocol.getHeartbeatTrigger().heartbeatTriggered(ctx);
            } else {
                super.userEventTriggered(ctx, evt);
            }
        }
    
    # 心跳发送伪码
    heartbeatTriggered()
       boolean ack = send()
       if ack==false:
           failcount++
           if failcount > maxCount:
              closeConn()
       else 
           failcount = 0
    

    说到Netty的IdleState其实就是在每个连接channelActive创建了最多3个(write,read,all)任务放到NioEventLoop的任务队列里去。这对于客户端来说,连接比较少,压力应该不大

    空闲连接管理

    通常一个服务端需要维护很多连接,如果一个连接很长一段时间啥也没干(心跳算不算呢? bolt好像算),那么维持这个连接的收益也不大还白白浪费了服务器资源,那么需要把它关掉。通常做法也有两种:

    1. 利用Netty的IdleState,每个连接一个定时任务。bolt采用的就是这种方式,简单方便,连接不是巨多特别适用
    2. 还有一种是自己写一个定时任务,用时间轮管理所有空闲连接,比如muduo

    对于Client来说,它会不停发心跳维持连接,但是如果一直没干正事,光在那发心跳包也是蛮浪费,所以有些框架client端也会close这种空闲连接,但是在bolt中好像没有看到

    慢连接

    这里的慢连接是指,接受请求的一方处理比较慢,比如消息的Consumer方处理特别慢(过载,DB走错索引,bug导致FullGC等等)
    Consumer方线程模型会导致不同的情况:

    1. IO线程直接做业务处理,那么IO线程会被阻塞,消息没有被read,导致数据堆积到tcp的接收缓冲区,进一步导致发送方的发送缓冲器满,消息堆积到应用层的buffer中(Netty的ChannelOutboundBuffer),不做处理的话,导致发送方触发大量GC,直到OOM
    2. 通常的模式是,IO线程收到数据,解包后交个一个业务线程池处理。这里又有两种case。 第一是业务线程池采用无界队列,那么请求对象会一直堆积到队列中,直到OOM。第二是采用的有界队列,队列满后,采用拒绝策略,直接抛异常

    所以,发送方跟接收方都需要处理慢连接,而处理方式就是在发送数据的时候,看下目前堆积在应用层(Netty的ChannelOutboundBuffer)的待发送数据还有多少, 在Netty已经提供了一整套完整的水位控制机制,我们只需要设置好高低水位,然后再发送数据的时候,判断下是否可以发送即可(channel.isWritable), bolt直接采用了该方法

    断线自动重连

    Rpc框架会在请求的时候获取连接,获取不到或者连接失效了,重连一次即可,一般也不需要自动重连。但是bolt不仅仅为rpc服务,所以它提供了自动重连的功能,根据根据实际场景选择是否开启。
    重连实际上是一个比较简单的事,只需要记住一定要重用bootstrap跟workgroup即可。
    它的入口是channelInactive,在这里把重连事件放到一个任务队列去跑即可

    请求处理

    对于bolt这种网络框架而已,请求的处理涉及到协议的设计,如何将请求转交给用户的代码进行等等问题。

    对于协议bolt支持多协议,同时一个协议可以有多个版本,它的执行链路大体如下

    回调超时机制

    nio中io跟业务调用天生是异步,所谓的同步也只是框架在biz线程中调用了future,get(timeout),biz线程会阻塞等待,如果超时了,框架会移除相关的数据。

    那,还有一种callback的回调方式,我们的业务向框架注册了一个回调函数,框架收到IO响应后执行回调代码。发现没有,回调的时机是收到IO响应,那如果IO一直没有响应呢,或者想要一个超时的控制呢,所以框架提供一个后台的线程去扫描这些callback是否超时了。

    翻了Dubbo等框架发现并没有类似的功能(可能没仔细没找到),solf-bolt提供了这个功能,使用Netty自带的HashedWheelTimer, 也就是时间轮定时器。在设置进行一个callback调用时,将这个callback任务放到HashedWheelTimer队列中,有HashedWheelTimer的后台线程去扫描是否超时

    相关文章

      网友评论

          本文标题:sofa-bolt学习

          本文链接:https://www.haomeiwen.com/subject/huyerqtx.html