网上赞扬Netty高性能的文章不要太多,但如何利用Netty写出高性能网络应用的文章却甚少,此文权当抛砖引玉。
估计此文很快就要被拍砖然后修改,因此转载请保持原文链接:
http://calvin1978.blogcn.com/articles/netty-performance.html ,否则视为侵权。。。
Netty Native 是由Twitter将Tomcat Native的移植过来,用C++编写JNI调用的Socket Transport。
经测试,Netty Native的确比JDK NIO更省CPU。
也许有人问,JDK的NIO也用EPOLL啊,大家有什么不同? Norman Maurer这么说的:
* Netty的 epoll transport使用 epoll edge-triggered 而 java的 nio 使用 level-triggered.
* C++的代码,更少GC,更少synchronized。
* netty的epoll transport 暴露了更多的配置参数。
第一条没看懂,反正测试结果的确更快省更CPU。
用法倒是简单,只要把NioEventLoopGrouph等几个类改名成EpollEventLoopGroup之类即可,详见Netty的 官方文档1 , 文档2
但要注意,首先它跟OS相关,而且基于GLIBC2.10编译,而CentOS 5.8就只有GLIBC2.5(别问为什么,厂大怪事多,我厂就是还有些CentOS5.8的机器),所以最好还是不要狠狠的直接全文搜索替换,而是用System.getProperty(“os.name”) 和 System.getProperty(“os.version”) 取出操作系统名称与版本,做成一个开关。
另外,Netty很多版本都有修复Netty Native相关的bug,看得人心里发毛,好在最近的版本终于不再说了,所以要用就用Netty的新版。
最后,Netty Native还包含了Google的boringssl(A fork of OpenSSL),JDK的原生SSL实现比较慢而且GC甚多,而大家把SSL Provider配置成OpenSSL时,又要担心操作系统有没装OpenSSL,或者版本会不会太旧。现在好了。
使用NIO最牛头不对马嘴的事情就是,给它配一个类似Commons Pool这样,有借有还的连接池。
因为NIO的本质就是轮询事件,不阻塞不独占,所以无论做什么事情,根本不需要独占一条连接,不需要把它借出去,再还回池里。Netty现在也有个ChannelPool了,不过我还是不知道有什么用。一来连接池出入之间有锁,二来并发请求一多就要无厘头的狂建连接,到连接池上限时还要白白等待别人释放连接,而这在NIO的世界里其实毫无必要。
所以,建议直接建一个连接数组,随机到哪个连接就用哪个连接发数据。如果数组里的某个连接还没建立或者已经失效,那就重新建立连接。
顺便说一句,异步的世界里,连建立连接的过程也是异步的,主线程不要等在建连接上,而是把发送的动作封成一个ChannelCallback,等连接建立了,再回调它发送数据,避免因为连接建立的缓慢或网络根本不通,把线程都堵塞了。
NIO这么神奇,有一种做法是只建一条连接,如Memcached的客户端SpyMemcached。还有一种是既然你能支持海量连接,几千几万的,那我就无节制的可劲的建了。
测试表明,一条连接有瓶颈,毕竟只用到了一个CPU核。 海量连接,CPU和内存在燃烧。。。。
那最佳连接数是传说中的CPU核数么?依然不是。
一切还是看你的场景,连接数在满足传输吞吐量的情况下,越少越好。
举个例子,在我的Proxy测试场景里:
TCP/Socket的大路设置,无非 SO_REUSEADDR, TCP_NODELAY, SO_KEEPALIVE 。
另外还有SO_LINGER , SO_TIMEOUT, SO_BACKLOG, SO_SNDBUF, SO_RCVBUF ,建议缓冲区大小不要显式设定覆盖内核参数里设定的动态适配。
而用了Native后还有其他的配置,比如TCP_CORK和KeepAlive包发送的时间间隔(默认2小时),在Java的标准Socket里居然只能设是否KeepAlive而KeepAlive的其他参数要通过内核参数设定,比较奇葩,详见 EpoolSocketChannelConfig的JavaDoc 。
所有这些参数的含义,不一一描述了,自己搜索,比如 Linux下高性能网络编程中的几个TCP/IP选项 。
CONNECT_TIMEOUT_MILLIS,Netty自己起一个定时任务来监控建立连接是否超时,默认30秒太长谁也受不了,一般会弄短它。
WRITE_BUFFER_HIGH_WATER_MARK 与 WRITE_BUFFER_LOW_WATER_MARK是两个流控的参数,默认值分别为32*2K与32K.。如果在writer buffet里排队准备输出的字节超过上限,Channel就不是writable的,NIO的事件轮询里就会把它摘掉,直到它低于32k才重新变回writable。建议没有足够的测试不要动它。
大家都知道,Boss Group用于服务端处理建立连接的请求,WorkGroup用于处理I/O。
EventLoopGroup的默认大小都是是2倍的CPU核数,但这并不是一个恒定的最佳数量,为了避免线程上下文切换,只要能满足要求,这个值其实越少越好,比如WorkerGroup我的场景里就只设了max(2, 核数1/2),核数再少也得有两条,否则就是核数一半。
至于Boss Group,如果都是长连接,它要做的事情也不多。幸亏它只有忙起来才会多起线程,平时就只占1条,所以我设了类似的核数/4。
在服务化的应用里,一般处理上游请求的同时,也会向多个下游的服务集群发送请求,就会有多个BootStrap,但调优指南里都说,尽量,全部收发重用同一个EventLoopGroup。还是那句,减少线程总数,除非你有特别的,隔离的需求。
同时,多个Channel可能会对应一个EventLoop线程,但对于一个Channel来说只能对应一个EventLoop线程。也就是在同一个请求里上下游的不同Channel的处理还是很可能会切换到不同的线程里。
Netty线程的数量一般固定且较少,所以很怕线程被堵塞,比如同步的数据库查询,比如下游的服务调用(又来罗嗦,future.get()式的异步在执行future.get()时还是堵住当前线程的啊)。
所以,此时就要把处理放到一个业务线程池里操作,即使要付出线程上下文切换的代价,甚至还有些ThreadLocal需要复制。
像发送超时控制之类的任务,不要使用JDK自己的ScheduledExecutorService,而是使用如下语句:
ctx.executor().schedule(new WriteTimeoutTask(p), 30, TimeUnit.SECONDS)
首先,JDK的ScheduledExecutorService是一个大池子,多线程争抢并发锁。而上面的写法,TimeoutTask只属于当前的EventLoop,没有任何锁。
其次,如果发送成功,需要从长长Queue里找回任务来取消掉它。现在每个EventLoop一条Queue,明显长度只有原来的N分之一。
文章太长没人看,写到这里就停笔了。未完待续。下篇会继续写,内存篇,工具类篇,使用方法篇
另外,不来唯品会的基础架构部的话,我可能永远不会写这种文章,本文也有来自其他同事的经验总结,简历请发 calvin.xiao@vipshop.com
有个叫“我是猫”的同学离职有一阵了,有点想念他,配图一幅。