推送服务
还记得一年半前,做的一个我的项目须要用到 Android 推送服务。和 iOS 不同,Android 生态中没有对立的推送服务。Google 尽管有 Google Cloud Messaging ,然而连国外都没对立,更别说国内了,间接被墙。
所以之前在 Android 上做推送大部分只能靠轮询。而咱们之前在技术调研的时候,搜到了 jPush 的博客,下面介绍了一些他们的技术特点,他们次要做的其实就是挪动网络下的长连贯服务。单机 50W-100W 的连贯确实是吓我一跳!起初咱们也采纳了他们的收费计划,因为是一个受众面很小的产品,所以他们的免费版够咱们用了。一年多下来,运作稳固,十分不错!
时隔两年,换了部门后,居然接到了一项工作,优化公司本人的长连贯服务端。
再次搜寻网上技术材料后才发现,相干的很多难点都被攻破,网上也有了很多的总结文章,单机 50W-100W 的连贯齐全不是梦,其实人人都能够做到。然而光有连贯还不够,QPS 也要一起下来。
所以,这篇文章就是汇总一下利用 Netty 实现长连贯服务过程中的各种难点和可优化点。
Netty 是什么
Netty: http://netty.io/
Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.
官网的解释最精准了,其中最吸引人的就是高性能了。然而很多人会有这样的疑难:间接用 NIO 实现的话,肯定会更快吧?就像我间接手写 JDBC 尽管代码量大了点,然而肯定比 iBatis 快!
然而,如果理解 Netty 后你才会发现,这个还真不肯定!
利用 Netty 而不必 NIO 间接写的劣势有这些:
- 高性能高扩大的架构设计,大部分状况下你只须要关注业务而不须要关注架构
- Zero-Copy 技术尽量减少内存拷贝
- 为 Linux 实现 Native 版 Socket
- 写同一份代码,兼容 java 1.7 的 NIO2 和 1.7 之前版本的 NIO
- Pooled Buffers 大大加重 Buffer 和开释 Buffer 的压力
- ……
个性太多,大家能够去看一下《Netty in Action》这本书理解更多。
另外,Netty 源码是一本很好的教科书!大家在应用的过程中能够多看看它的源码,十分棒!
瓶颈是什么
想要做一个长链服务的话,最终的指标是什么?而它的瓶颈又是什么?
其实指标次要就两个:
- 更多的连贯
- 更高的 QPS
所以,上面就针对这两个指标来说说他们的难点和留神点吧。
更多的连贯
非阻塞 IO
其实无论是用 Java NIO 还是用 Netty,达到百万连贯都没有任何难度。因为它们都是非阻塞的 IO,不须要为每个连贯创立一个线程了。
欲知详情,能够搜寻一下BIO,NIO,AIO的相干知识点。
Java NIO 实现百万连贯
ServerSocketChannel ssc = ServerSocketChannel.open(); Selector sel = Selector.open(); ssc.configureBlocking(false); ssc.socket().bind(new InetSocketAddress(8080)); SelectionKey key = ssc.register(sel, SelectionKey.OP_ACCEPT); while(true) { sel.select(); Iterator it = sel.selectedKeys().iterator(); while(it.hasNext()) { SelectionKey skey = (SelectionKey)it.next(); it.remove(); if(skey.isAcceptable()) { ch = ssc.accept(); } } }
这段代码只会承受连过去的连贯,不做任何操作,仅仅用来测试待机连接数极限。
大家能够看到这段代码是 NIO 的根本写法,没什么特地的。
Netty 实现百万连贯
NioEventLoopGroup bossGroup = new NioEventLoopGroup(); NioEventLoopGroup workerGroup= new NioEventLoopGroup(); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup); bootstrap.channel( NioServerSocketChannel.class); bootstrap.childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); //todo: add handler }}); bootstrap.bind(8080).sync();
这段其实也是非常简单的 Netty 初始化代码。同样,为了实现百万连贯基本没有什么非凡的中央。
瓶颈到底在哪
下面两种不同的实现都非常简单,没有任何难度,那有人必定会问了:实现百万连贯的瓶颈到底是什么?
其实只有 java 中用的是非阻塞 IO(NIO 和 AIO 都算),那么它们都能够用单线程来实现大量的 Socket 连贯。不会像 BIO 那样为每个连贯创立一个线程,因为代码层面不会成为瓶颈。
其实真正的瓶颈是在 Linux 内核配置上,默认的配置会限度全局最大关上文件数(Max Open Files)还会限度过程数。所以须要对 Linux 内核配置进行肯定的批改才能够。
这个货色当初看似很简略,依照网上的配置改一下就行了,然而大家肯定不晓得第一个钻研这个人有多难。
如何验证
让服务器反对百万连贯一点也不难,咱们过后很快就搞定了一个测试服务端,然而最大的问题是,我怎么去验证这个服务器能够撑持百万连贯呢?
咱们用 Netty 写了一个测试客户端,它同样用了非阻塞 IO ,所以不必开大量的线程。然而一台机器上的端口数是有限度的,用root权限的话,最多也就 6W 多个连贯了。所以咱们这里用 Netty 写一个客户端,用尽单机所有的连贯吧。
NioEventLoopGroup workerGroup = new NioEventLoopGroup(); Bootstrap b = new Bootstrap(); b.group(workerGroup); b.channel( NioSocketChannel.class); b.handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); //todo:add handler } }); for (int k = 0; k < 60000; k++) { //请自行批改成服务端的IP b.connect(127.0.0.1, 8080); }
代码同样很简略,只有连上就行了,不须要做任何其余的操作。
这样只有找到一台电脑启动这个程序即可。这里须要留神一点,客户端最好和服务端一样,批改一下 Linux 内核参数配置。
怎么去找那么多机器
依照下面的做法,单机最多能够有 6W 的连贯,百万连贯起码须要17台机器!
如何能力冲破这个限度呢?其实这个限度来自于网卡。咱们起初通过应用虚拟机,并且把虚拟机的虚构网卡配置成了桥接模式解决了问题。
依据物理机内存大小,单个物理机起码能够跑4-5个虚拟机,所以最终百万连贯只有4台物理机就够了。
讨巧的做法
除了用虚拟机充沛压迫机器资源外,还有一个十分讨巧的做法,这个做法也是我在验证过程中偶尔发现的。
依据 TCP/IP 协定,任何一方发送FIN后就会启动失常的断开流程。而如果遇到网络瞬断的状况,连贯并不会主动断开。
那咱们是不是能够这样做?
- 启动服务端,千万别设置 Socket 的keep-alive属性,默认是不设置的
- 用虚拟机连贯服务器
- 强制敞开虚拟机
- 批改虚拟机网卡的 MAC 地址,重新启动并连贯服务器
- 服务端承受新的连贯,并放弃之前的连接不断
咱们要验证的是服务端的极限,所以只有始终让服务端认为有那么多连贯就行了,不是吗?
通过咱们的试验后,这种办法和用实在的机器连贯服务端的体现是一样的,因为服务端只是认为对方网络不好罢了,不会将你断开。
另外,禁用keep-alive是因为如果不禁用,Socket 连贯会主动探测连贯是否可用,如果不可用会强制断开。
更高的 QPS
因为 NIO 和 Netty 都是非阻塞 IO,所以无论有多少连贯,都只须要大量的线程即可。而且 QPS 不会因为连接数的增长而升高(在内存足够的前提下)。
而且 Netty 自身设计得足够好了,Netty 不是高 QPS 的瓶颈。那高 QPS 的瓶颈是什么?
是数据结构的设计!
如何优化数据结构
首先要相熟各种数据结构的特点是必须的,然而在简单的我的项目中,不是用了一个汇合就能够搞定的,有时候往往是各种汇合的组合应用。
既要做到高性能,还要做到一致性,还不能有死锁,这里难度真的不小…
我在这里总结的教训是,不要过早优化。优先思考一致性,保证数据的精确,而后再去想方法优化性能。
因为一致性比性能重要得多,而且很多性能问题在量小和量大的时候,瓶颈齐全会在不同的中央。所以,我感觉最佳的做法是,编写过程中以一致性为主,性能为辅;代码实现后再去找那个 TOP1,而后去解决它!
解决 CPU 瓶颈
在做这个优化前,先在测试环境中去狠狠地压你的服务器,量小量大,天壤之别。
有了压力测试后,就须要用工具来发现性能瓶颈了!
我喜爱用的是 VisualVM,关上工具后看抽样器(Sample),依据自用工夫(Self Time (CPU))倒序,排名第一的就是你须要去优化的点了!
备注:Sample 和 Profiler 有什么区别?前者是抽样,数据不是最准然而不影响性能;后者是统计精确,然而十分影响性能。如果你的程序十分耗 CPU,那么尽量用 Sample,否则开启 Profiler 后升高性能,反而会影响准确性。
还记得咱们我的项目第一次发现的瓶颈居然是ConcurrentLinkedQueue这个类中的size()办法。量小的时候没有影响,然而Queue很大的时候,它每次都是从头统计总数的,而这个size()办法咱们又是十分频繁地调用的,所以对性能产生了影响。
size()的实现如下:
public int size() { int count = 0; for (Node<E> p = first(); p != null; p = succ(p)) if (p.item != null) // Collection.size() spec says to max out if (++count == Integer.MAX_VALUE) break; return count; }
起初咱们通过额定应用一个AtomicInteger来计数,解决了问题。然而拆散后岂不是做不到高一致性呢?没关系,咱们的这部分代码关怀最终一致性,所以只有保障最终统一就能够了。
总之,具体案例要具体分析,不同的业务要用不同的实现。
解决 GC 瓶颈
GC 瓶颈也是 CPU 瓶颈的一部分,因为不合理的 GC 会大大影响 CPU 性能。
这里还是在用 VisualVM,然而你须要装一个插件:VisualGC
有了这个插件后,你就能够直观的看到 GC 流动状况了。
依照咱们的了解,在压测的时候,有大量的 New GC 是很失常的,因为有大量的对象在创立和销毁。
然而一开始有很多 Old GC 就有点说不过去了!
起初发现,在咱们压测环境中,因为 Netty 的 QPS 和连接数关联不大,所以咱们只连贯了大量的连贯。内存调配得也不是很多。
而 JVM 中,默认的新生代和老生代的比例是1:2,所以大量的老生代被节约了,新生代不够用。
通过调整 -XX:NewRatio 后,Old GC 有了显著的升高。
然而,生产环境又不一样了,生产环境不会有那么大的 QPS,然而连贯会很多,连贯相干的对象存活工夫十分长,所以生产环境更应该调配更多的老生代。
总之,GC 优化和 CPU 优化一样,也须要一直调整,一直优化,不是欲速不达的。
其余优化
如果你曾经实现了本人的程序,那么肯定要看看《Netty in Action》作者的这个网站:Netty Best Practices a.k.a Faster == Better。
置信你会受益匪浅,通过外面提到的一些小小的优化后,咱们的整体 QPS 晋升了很多。
最初一点就是,java 1.7 比 java 1.6 性能高很多!因为 Netty 的编写格调是事件机制的,看似是 AIO。可 java 1.6 是没有 AIO 的,java 1.7 是反对 AIO 的,所以如果用 java 1.7 的话,性能也会有显著晋升。
最初成绩
通过几周的一直压测和一直优化了,咱们在一台16核、120G内存(JVM只调配8G)的机器上,用 java 1.6 达到了60万的连贯和20万的QPS。
其实这还不是极限,JVM 只调配了8G内存,内存配置再大一点连接数还能够下来;
QPS 看似很高,System Load Average 很低,也就是说明瓶颈不在 CPU 也不在内存,那么应该是在 IO 了!下面的 Linux 配置是为了达到百万连贯而配置的,并没有针对咱们本人的业务场景去做优化。
因为目前性能齐全够用,线上单机 QPS 最多才 1W,所以咱们先把精力放在了其余中央。置信前面咱们还会去持续优化这块的性能,期待 QPS 能有更大的冲破!
起源:dozer.cc/2014/12/netty-long-connection.html