如何做到C100K–基于Mina高性能服务器设计(二)

声明 本文为Gleasy原创文章,转载请指明引自Gleasy团队博客
第一节: 如何做到C100K–基于Mina高性能服务器设计(一)
第二节: 如何做到C100K–基于Mina高性能服务器设计(二)
概要:本文通过Gleasy项目中一具体实例,介绍了如何使用Mina一步步实现达到C100K的高性能应用服务器。

3. 程序架构
本章假设读者已经对Mina有比较扎实的基础,知道NIO的基本原理,了解MINA的IO线程和执行线程(多线程)的概念及工作原理。

要实现redis的代理,有几点要求:
a. 必须保证命令的执行顺序,比如通过一个连接连续发送2条命令,这2条命令的执行顺序必须和发送顺序保持一致。
b. 有些命令可能会执行相当长的时间,比如redis的blpop命令,会一直等待结果返回。
基于上面的要求,特别是要求2,就决定了IO线程是远远不够用的,正常情况下IO线程的数量为CPU的核数。也就是说以i5 4核的机器为例,如果不采用多线程方式,只要4个BLPOP发过来,整个程序就挂死了。

3.1 尝试1,每个连接一个线程

a. 连接建立成功之后,立刻启动一个线程,并且建一个到redis服务器的socket,单独处理该连接的请求。
b. IO线程收到命令,分析协议,得到一条redis命令,立刻交给连接对应的执行线程
c. 执行线程维护一个FIFO的队列,从IO线程过来的命令就放在队尾。
d. 执行线程会不断从队列中取出一条一条命令,发送给REDIS服务器执行,执行完毕后将结果写回给请求者。
e. 如果FIFO为空,则执行线程会等待唤醒信号(WAIT)
f. 新的请求到达,会发送唤醒信号给执行线程。

该方案看上去够美的吧?使用了Mina的多线程模型,使用了生产者-消费者机制。这些曾经被教导高性能妙招都用上了。好了,跑一跑看看。。

实验5:通过Mina proxy server 尝试1
[root@gleasy redis]# bin/redis-benchmark -h 192.168.0.11 -p 26677 -t get -n 1000000 -c 200
====== GET ======
1000000 requests completed in 12.03 seconds
200 parallel clients
3 bytes payload
keep alive: 1
11.86% <= 1 milliseconds
40.96% <= 2 milliseconds
49.32% <= 3 milliseconds
56.91% <= 4 milliseconds
21568.34 requests per second
tps:22K

结果出来了,TPS为22K。问题出在哪里呢?众所周知,REDIS为单线程模型,本方案的代理服务器为每一个客户端都建立一个到REDIS服务器的SOCKET连接,这些连接最终在REDIS端还是串行处理的,难道问题出在到REDIS服务器连接数据过多?GLEASY自主研发了Fast redis client,换成快速客户端再试试?

3.2 尝试2 引入fast redis client

a. 连接建立成功之后,立刻启动一个线程。
b. IO线程收到命令,分析协议,得到一条redis命令,立刻交给连接对应的执行线程
c. 执行线程维护一个FIFO的队列,从IO线程过来的命令就放在队尾。
d. 执行线程会不断从队列中取出一条一条命令,交给Fast redis Client,执行完毕后将结果写回给请求者。
e. 如果FIFO为空,则执行线程会等待唤醒信号(WAIT)
f. 新的请求到达,会发送唤醒信号给执行线程。

好了,改造的点就在于访问redis的时候,使用了Fast Redis client。大大减少了到redis服务器的连接数。现在再来看看结果

实验6:通过Mina proxy server 尝试2
[root@gleasy redis]# bin/redis-benchmark -h 192.168.0.11 -p 26677 -t get -n 1000000 -c 200
====== GET ======
1000000 requests completed in 12.03 seconds
200 parallel clients
3 bytes payload
keep alive: 1
11.86% <= 1 milliseconds
40.96% <= 2 milliseconds
59.32% <= 3 milliseconds
66.91% <= 4 milliseconds
34568.34 requests per second
tps:34K

性能有明显改善,但还是徘徊在30K-40K,离我们的目标还差得远。通过改造到redis端的请求,并未获得质的飞跃,那问题就肯定存在于接受请求端了,即Mina端。难道多线程模型不给力?去除了多线程模型,那blpop这种命令不就支持不了了?死马当活马死,先去掉多线程模型吧。

3.3 尝试3 去除多线程模型

a. 连接建立成功之后,为该连接建一个会话对象(SessionObject),用于存储不完整协议片段(由于采用 NIO,收到的消息可能未必是完整的协议)。
b. IO线程收到命令,分析协议(分析协议过程中用到SessionObject),得到一条redis命令,立刻调用Fast Redis Client
c. 执行完毕后将结果写回给请求者

这下够简单了吧,分析协议,得到命令,调用后立刻返回结果。再跑一次看看。

实验6:通过Mina proxy server 尝试3
[root@gleasy redis]# bin/redis-benchmark -h 192.168.0.11 -p casino online 26677 -t get -n 1000000 -c 200
====== GET ======
1000000 requests completed in 12.03 seconds
200 parallel clients
3 bytes payload
keep alive: 1
11.86% <= 1 milliseconds
56.96% <= 2 milliseconds
67.32% <= 3 milliseconds
78.91% <= 4 milliseconds
64568.14 requests per second
tps:64K

有戏了,TPS一下子上到64K了。这可是100%的性能提升,说明此道可行!如何才能够百尺杆头,再进一步呢?细细分析一下,当IO线程得到命令之后,直接调用FAST REDIS CLIENT来向REDIS请求结果,由于FAST REDIS CLIENT执行也需要时间,在这个段时间,IO线程就没有能力去得到更多命令了,它一直在那里执行结果。有没有办法即不使用多线程,亦可以令到IO线程不会一直死等呢?当然有,那就是回调!

3.4 尝试3 使用回调机制,解放IO线程

a. 连接建立成功之后,为该连接建一个会话对象(SessionObject),用于存储不完整协议片段(由于采用 NIO,收到的消息可能未必是完整的协议)。
b. IO线程收到命令,分析协议(分析协议过程中用到SessionObject),得到一条redis命令,生成一个回调对象(Callback对象,该对象中包含了对该连接请求session的引用),把它丢进Fast Redis Client的执行队列,然后就立刻返回
c. Fast Redis Client线程执行队列中的请求(使用MULTI方式),执行完毕,逐个调用命令中携带的回调对象(通过回调方法把结果写回给调用者)

方案4和方案3最大的不是就是,IO线程不用负责将结果写回给请求者,而是由FAST REDIS CLIENT的线程做这项工作,这就解放了IO线程,从而可以得到更多的命令交给FAST REDIS CLIENT去执行。而FAST REDIS CLIENT命令越多,TPS就越高,从而就带动了整个代理服务器的TPS。看上去,好像不错,赶快试试。

实验7:通过Mina proxy server 尝试4
[root@gleasy redis]# bin/redis-benchmark -h 192.168.0.11 -p 26677 -t get -n 1000000 -c 200
====== GET ======
1000000 requests completed in 12.03 seconds
200 parallel clients
3 bytes payload
keep alive: 1
35.86% <= 1 milliseconds
45.96% <= 2 milliseconds
88.32% <= 3 milliseconds
99.01% <= 4 milliseconds
93548.21 requests per second
tps:93K

成功了,太棒了,TPS已经超过原生的REDIS(83K),达到93K的给力速度。

3.5 遗留问题
3.4的方案完美地解决了性能问题,但由于放弃了多线程模型,无法支持BLPOP这种阻塞型的操作。后来笔者通过对BLPOP命令单独引入多线程,完美地解决了Block命令的问题,此为后话,不在本文中详述。

4. 经验与总结
经过漫长的尝试过程,最终得出一些心得:
a. 不能人云亦云,别人的测试结果,永远只对别人有效
b. 多线程也许并不能带来高性能,得视情况而定
c. 越简单的东西性能就越好,在高性能领域,一两句代码的写法异同可能会造成极大的性能影响。
d. 语言本身并不构成性能的决定因素,关键还是看程序如何设计。

5. 关于本文的误区的解释
a.误区1:我们的代理服务在性能上全面超越了redis
本文的方案实现了一种REDIS代理服务器,并且实现了非批量操作的命令在性能上达到C100K(直连redis为73K),超越了直连redis。但并不是说,我们的代理服务器全面超越了REDIS的性能。本服务器利用了REDIS的MULTI命令(批量操作pipeline)性能超高的特性(tps 470K),借用pipeline机制将单条命令汇集成MULTI命令提交后处理,才达到如此好的性能结果。由于在实际使用过程中,绝大多数情况下我们使用的都是非批量的操作,因此,本代理服务器的高性能具备广泛的实用价值。

b. 误区2: 单个请求的处理时间,连接代理服务器比直连redis更短
本方案的代理服务器最终还会将请求发给redis处理,因此处理单个请求时间肯定比直连更长。本方案在大规模并发时有超高的性能表现,但在低并发情况下,是体现不出来优势的。

c 误区3: JAVA在高性能服务器领域超越了C
JAVA虚拟机是用C/C 开发的,理论上不可能超越C。在实践上,由于LINUX JDK1.6的NIO已经采用了epoll,从而保证了在机制上是不会输得太多。另外由于JAVA语言作为上层语言经过了大量的实践优化,再配合程序架构设计上的合理调整,在应用层面,好的JAVA程序在性能绝不输于C语言,这毋庸质疑的。本文旨在说明实用层面设计高质量的程序比依赖于语言特性更为重要。

此条目发表在 Java技术, 分布式技术, 数据库技术 分类目录。将固定链接加入收藏夹。

如何做到C100K–基于Mina高性能服务器设计(二)》有 3 条评论

  1. tuojian 说:

    哈哈,我来学习一下。想表达几个愚见
    1、mina nio是事件轮询方式。可以考虑一下aio,或者C/C++ 的epoll
    2、redis虽然采取的是单线程,但是采用epoll事件模型,采用非阻塞io,大部分在内存操作。因此1000000个并发请求的瓶颈不在redis本身。
    3、对java研究不深。但是线程间同步的消耗在高性能服务器是个杀手级的bug,这也是redis可以堂而皇之用单线程的原因。尽量写不用线程同步,或者直接用同步原语,可以最大情况下避免陷入系统内核。
    4、C/C++与java语言的定位不一样。毕竟jvm都是用C实现出来的。

    • xueke 说:

      嗯 很有道理 C语言从本质上来性能肯定要好过JAVA 这点无须质疑 本文旨在强调程序设计的重要
      另外, MINA NIO 在LINUX上JDK1.6以后以上都采用的是EPOLL。

  2. Pingback 引用通告: Mina 高性能 c100K

发表评论