如何用Netty写一个高性能的分布式服务框架?( 三 )

如何用Netty写一个高性能的分布式服务框架?
文章插图
 
2 Java 原生 NIO API 从入门到放弃
 
复杂度高
 

  • API复杂难懂 , 入门困 。
  • 粘包/半包问题费神 。
  • 需超强的并发/异步编程功底 , 否则很难写出高效稳定的实现 。
 
稳定性差 , 坑多且深
 
  • 调试困难 , 偶尔遭遇匪夷所思极难重现的bug , 边哭边查是常有的事儿 。
 
  • linux 下 EPollArrayWrapper.epollWait 直接返回导致空轮训进而导致 100% cpu 的 bug 一直也没解决利索 , Netty帮你 work around (通过rebuilding selector) 。
 
NIO代码实现方面的一些缺点
 
1)Selector.selectedKeys() 产生太多垃圾
 
Netty 修改了 sun.nio.ch.SelectorImpl 的实现 , 使用双数组代替 HashSet 存储来 selectedKeys:
 
  • 相比HashSet(迭代器 , 包装对象等)少了一些垃圾的产生(help GC) 。
  • 轻微的性能收益(1~2%) 。
 
Nio 的代码到处是 synchronized (比如 allocate direct buffer 和 Selector.wakeup() ):
 
  • 对于 allocate direct buffer , Netty 的 pooledBytebuf 有前置 TLAB(Thread-local allocation buffer)可有效的减少去竞争锁 。
 
  • wakeup 调用多了锁竞争严重并且开销非常大(开销大原因: 为了在 select 线程外跟 select 线程通信 , linux 平台上用一对 pipe , windows 由于 pipe 句柄不能放入 fd_set , 只能委曲求全用两个 tcp 连接模拟) , wakeup 调用少了容易导致 select 时不必要的阻塞(如果懵逼了就直接用 Netty 吧 , Netty中有对应的优化逻辑) 。
 
  • Netty Native Transport 中锁少了很多 。
 
2)fdToKey 映射
 
  • EPollSelectorImpl#fdToKey 维持着所有连接的 fd(描述符)对应 SelectionKey 的映射 , 是个 HashMap 。
 
  • 每个 worker 线程有一个 selector , 也就是每个 worker 有一个 fdToKey , 这些 fdToKey 大致均分了所有连接 。
 
  • 想象一下单机 hold 几十万的连接的场景 , HashMap 从默认 size=16 , 一步一步 rehash...
 
3)Selector在linux 平台是 Epoll LT 实现
 
  • Netty Native Transport支持Epoll ET 。
 
4)Direct Buffers 事实上还是由 GC 管理
 
  • DirectByteBuffer.cleaner 这个虚引用负责 free direct memory , DirectByteBuffer 只是个壳子 , 这个壳子如果坚强的活下去熬过新生代的年龄限制最终晋升到老年代将是一件让人伤心的事情…
 
  • 无法申请到足够的 direct memory 会显式触发 GC , Bits.reserveMemory() -> { System.gc() } , 首先因为 GC 中断整个进程不说 , 代码中还 sleep 100 毫秒 , 醒了要是发现还不行就 OOM 。
 
  • 更糟的是如果你听信了个别<XX优化宝典>谗言设置了-XX:+DisableExplicitGC 参数 , 悲剧会静悄悄的发生...
 
  • Netty的UnpooledUnsafeNoCleanerDirectByteBuf 去掉了 cleaner , 由 Netty 框架维护引用计数来实时的去释放 。
 
五 Netty 的真实面目
 
1 Netty 中几个重要概念及其关系
 
EventLoop
 
  • 一个 Selector 。
 
  • 一个任务队列(mpsc_queue: 多生产者单消费者 lock-free) 。
 
  • 一个延迟任务队列(delay_queue: 一个二叉堆结构的优先级队列 , 复杂度为O(log n)) 。
 
  • EventLoop 绑定了一个 Thread , 这直接避免了pipeline 中的线程竞争 。
 
Boss: mainReactor 角色 , Worker: subReactor 角色
 
  • Boss 和 Worker 共用 EventLoop 的代码逻辑 , Boss 处理 accept 事件 , Worker 处理 read , write 等事件 。