HSJ实现(一)

来源:互联网 发布:生风灿烂的日子 知乎 编辑:程序博客网 时间:2024/05/17 06:42
       HSJ基于netty实现。netty作为基于NIO的一个网络框架,因为它良好的性能表现,现在越来越多的应用使用它。HSJ作为一个异步实现的客户端,但其又是一个DB操作客户端,所以面临的一个棘手问题就是异步发送请求后必须准确知道服务方执行的结果。作为异步发送API大致有以下几种策略(原文参见http://blog.chinaunix.net/space.php?uid=20357359&do=blog&id=3063161摘录到本文):
同步发送API的设计相对简单,实现类似于send和recv的接口就可以了,recv阻塞等待应答的到来。但是这样会使得API的性能受限,除了采用多线程同步API通过并发来提高性能,另外一种方式就是通过异步发送API实现。
异 步API的好处就是不用阻塞发送执行上下文,交由另外一个执行上下文进行具体的发送(可以是内核,也可以是另外一个线程),有应答的时候调用预先设置的 callback。通常情况,由于执行callback的线程一般与调用API的线程是不同的。这个时候要注意线程安全,以及锁的处理。

同步发送API的消息交互流程图:
  1. client    --- message1 --->    server
  2. client    <--- ack1 ---            server
  3. client    --- message2 --->    server
  4. client    <--- ack2 ---            server
  5. client    --- message3 --->    server
  6. client    <--- ack3 ---            server
异步发送API的消息交互流程图:
  1. client    --- message1 --->    server
  2. client    --- message2 --->    server
  3. client    <--- ack1 ---            server
  4. client    <--- ack2 ---            server
  5. client    --- message3 --->    server
  6. client    <--- ack3 ---            server
异步发送API实现的时候,主要是两点:
1> message的发送没有前后依赖关系,后面的message发送的时候不用等待前面message的ack。
2> 收到应答的时候进行处理,调用设置的callback。

这里面第1点很容易实现,发送线程不停的发送就可以了,但是对于第2点如何判断收到应答,这个比较麻烦。因为传统的同步API是阻塞等待对方的应答,对于异步发送API,需要有一定的机制来获得应答。后面的几种异步发送API的实现方法中,分别采用不同的方式来获取应答。

1. 专门的发送线程,专门的接收线程
使用libpcap和libnet编写过程序的同学对这种模式不会陌生,业务线程或者是专门的发送线程使用libnet直接发送message,接收线程使用libpcap接收并进行处理。

这种方法的本质是,通过另外的接收线程同步接收应答。

2. event loop线程
业务线程构造发送数据,添加到event线程的req_list中。并通过Pipe通知event线程,event线程进行异步发送,有应答后,调用相应的callback处理。
event loop线程中实现消息的状态机读取,读取到完整的消息,进行相应的逻辑处理后,执行设置的callback函数。

这种方法的本质是,依靠select, poll, epoll等事件通知API实现,但是在linux下面只能拿到IO就绪事件,而不是IO完成事件,所以需要辅以协议状态机,转化为IO完成事件。

3. 后台发送线程
业务线程构造发送数据,添加到后台发送线程的req_list中,并通过pthread_cond来通知后台发送线程。后台线程直接进行发送,并检查是否有应答,如果有应答就进行处理。

这种方法的本质是,发送线程每发送1个message,就检测一些是否有应答到来。具体检测跟2的一样,也是通过select等实现。

对于HSJ来说,两种方式的实现:
1. 发送请求时入队列,接收到请求时出队列;(参见HSClientSeqHandler.java)
2. 记录连接的id,发送请求时加锁,接收到请求后释放;(参见HSClientHandler.java)
两种方式各位利弊,我们来一一分析。
方式一:
1.网络连接有限的情况下,也能保持较好的性能表现。
     在大并发请求下,网络连接是争用最厉害的资源。这种情况下因为没有互斥操作,连接是共享的,所以不会产生影响。
2.因为这种实现方式存在入队列和出队列的操作,如果入队/出队的元素非常多,就会拖慢整个系统的吞吐。HS4J qps和hs宣传的qps出入很大,这个是一个重要原因。
3.这种方式的容错性、可靠性不高。一旦出现网络错误或执行耗时等问题都会导致出队列和入队列对应不起来,导致严重错误。目前来看一旦出现这种问题很难解决,HS4J就是这种实现方式。
方式二:
      首先说明一下,netty框架的每一个channel都有一个id,HSJ就是依据这个id来实现方式二方案的。如果你不知道但又想了解请访问netty官网。
      1.运行结果很准确,不会出现差错。
      2.如果设置的连接池很小,管理发送channel队列就会经常出现lock,导致性能表现很差。但关于这点需要辩证的看待。对于开发者来讲,你的应用需要多少台机器支撑,每台机器每秒的处理请求数最大是多少,应该都了然于胸。对于HSJ每台机器的连接池大小设置成这个最大值即可,你会发现HSJ这时的表现会很好。这里需要说明一点,HSJ是支持你设置初始连接池数和最大连接池数的。最大连接池数设置为此即可。当连接不够时HSJ会自动增加连接的,直到最大数。当然在访问低谷期HSJ也会回收关闭这些连接,节约机器资源。
       只要连接池大小设置得当,HSJ的qps可以达到很大。
       
        在同等条件下(50个线程,每次发1000次insert请求),HS4J和HSJ的表现对比:
HS4J 方式一 100连接池
Concurrency 50 threads,repeats=1000,duration=3500ms,tps=14285
Concurrency 50 threads,repeats=1000,duration=3407ms,tps=14675

HSJ 方式二 100连接池
Concurrency 50 threads,repeats=1000,duration=4703ms,tps=10631
增大连接池后
Concurrency 50 threads,repeats=1000,duration=218ms,tps=229357