网络IO框架 Netty v1.3

微风

2019/03/24 发布于 技术 分类

文字内容
1. ⽹网络IO框架 - Netty by 家纯
2. Why Netty? • Blocking IO的瓶颈 • Non-blocking IO • Java原⽣生nio api从⼊入⻔门到放弃 • Nio代码实现⽅方⾯面的⼀一些缺点 • Netty Native Transport • epoll介绍 • Netty Thread Model • ChannelPipeline + 异步事件驱动 • Pooling & reuse • 最佳实践 • 从Netty源码中学到的代码技巧 • 如何利利⽤用Netty写⼀一个⾼高性能RPC框架
3. Blocking IO 的瓶颈 • 性能: • • • Thread-per-request导致服务端在并发接 ⼊入和吞吐量量⽅方⾯面受到极⼤大的限制, ⾼高并发 场景会带来较多的context-switch开销; 应⽤用内存吃紧(Java中每个线程默认线程 栈⼤大⼩小1M); 可靠性: • 很难做业务级别的线程池隔离,个别业务 处理理慢会直接挂住IO线程, 最终阻塞整个 系统;
4. Non-blocking IO • IO多路路复⽤用: • • 虽然IO多路路复⽤用也是阻塞的, 但只是阻塞 在select/poll/epoll这样的system call上, 并不不需要阻塞recvfrom这样的IO操作; IO处理理和业务处理理可以隔离: • IO处理理通常为CPU密集型任务 • 业务处理理通常为IO密集型任务
5. Java原⽣生nio api从⼊入⻔门到放弃 • • 复杂度⾼高: • api复杂难懂, ⼊入⻔门困难; • 粘包/半包问题⽐比较难处理理; • 需超强的并发编程功底, 否则很难写出⾼高效稳定的实现; 稳定性差, 坑多且深: • • 调试困难, 偶尔遭遇匪夷所思极难重现的bug, 边哭边查是常有的事⼉儿; linux下EPollArrayWrapper.epollWait直接返回导致空轮训进⽽而导致100% cpu的bug⼀一直也 没解决利利索, netty帮你work around(通过rebuilding selector);
6. Nio代码实现⽅方⾯面的⼀一些缺点 • Selector.selectedKeys() 产⽣生太多垃圾: • • netty修改了了sun.nio.ch.SelectorImpl的实现, 使⽤用双数组代替HashSet存储来selectedKeys: • - 相⽐比HashSet(迭代器器, 包装对象等)少的多的垃圾制造(help GC); • - 轻微的性能收益(1~2%); nio的代码到处是synchronized (⽐比如allocate direct buffer): • • 对于allocate direct buffer, netty的pooledBytebuf有前置TLAB(Thread-local allocation buffer); netty native transport中锁少了了很多;
7. Nio代码实现⽅方⾯面的⼀一些缺点 • fdToKey映射: • • • • EPollSelectorImpl#fdToKey维持着所有连接的fd(描述符)对应SelectionKey的映射, 是个 HashMap; 每个worker线程有⼀一个selector, 也就是每个worker有⼀一个fdToKey, 这些fdToKey⼤大致均分了了 所有连接; 想象⼀一下单机hold⼏几⼗十万的连接的场景, HashMap从默认size=16, ⼀一步⼀一步rehash... Selector在linux平台是epoll LT实现: • netty native transport⽀支持epoll ET;
8. Nio代码实现⽅方⾯面的⼀一些缺点 • Direct Buffers事实上还是由GC管理理: • • • • DirectByteBuffer.cleaner这个虚引⽤用负责free direct memory, DirectByteBuffer只是个壳⼦子, 这个壳⼦子如果坚强的活下去熬过新⽣生代的年年龄限制最终晋升到⽼老老年年代将是⼀一件让⼈人伤⼼心的事 情… ⽆无法申请到⾜足够的direct memory会显式触发GC, Bits.reserveMemory() -> { System.gc() }, ⾸首先因为GC中断整个进程不不说, 代码中还sleep 100毫秒, 醒了了要是发现还不不⾏行行就OOM; 更更糟的是如果你听信了了个别的谗⾔言设置了了-XX:+DisableExplicitGC参数, 悲剧终于还是发⽣生了了... netty的UnpooledUnsafeNoCleanerDirectByteBuf去掉了了cleaner, 由netty框架维护引⽤用计数 来实时的去释放;
9. Netty Native Transport • 相⽐比nio创建更更少的对象, 更更⼩小的GC压⼒力力; • 针对linux平台优化, ⼀一些specific features: • • SO_REUSEPORT - 端⼝口复⽤用(允许多个socket监听同⼀一个IP+端⼝口, 与RPS/RFS协作, 可进⼀一 步提升性能); • TCP_FASTOPEN - 3次握⼿手时也⽤用来交换数据; • 等等… ⽀支持 epoll ET(关键哥);
10. 多路路复⽤用器器: select/poll/epoll之间的区别 • select/poll: • • 本身的实现机制上的限制(采⽤用轮询⽅方式检测就绪事件, 时间复杂度: O(n), 每次还要将肥⼤大的 fd_set在⽤用户空间和内核空间拷⻉贝来拷⻉贝去), 并发连接越⼤大, 性能越差; • poll相⽐比select没有很⼤大差异, 只是取消了了最⼤大⽂文件描述符个数的限制; • select/poll都是LT模式; epoll: • • 采⽤用回调⽅方式检测就绪事件, 时间复杂度: O(1), 每次epoll_wait调⽤用只返回已就绪的⽂文件描 述符; epoll⽀支持LT和ET模式;
11. epoll涉及的数据结构
12. epoll三个⽅方法简介 • 主要代码: linux-2.6.11.12/fs/eventpoll.c • int epoll_create(int size) • 创建rb-tree(红⿊黑树)和ready-list(就绪链表); • • • - size参数已经没什什么意义, 早期epoll实现是hash表, 所以需要size参数; int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) • • - 红⿊黑树O(logN), 平衡效率和内存占⽤用, 在容量量需求不不能确定并可能量量很⼤大的情况下红⿊黑树是最佳选择; 把epitem放⼊入rb-tree并向内核中断处理理程序注册ep_poll_callback, callback触发时把该epitem放进readylist; int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout) • ready-list —> events[];
13. epoll_wait⼯工作流程概述 • epoll_wait调⽤用ep_poll: • • ⽂文件描述符fd的events状态改变: • • • buffer由不不可读变为可读或由不不可写变为可写, 导致相应fd上的回调函数ep_poll_callback被触发; ep_poll_callback被触发: • • 当rdlist(ready-list)为空(⽆无就绪fd)时挂起当前线程, 直到rdlist不不为空时线程才被唤醒; 将相应fd对应epitem加⼊入rdlist, 导致rdlist不不空, 线程被唤醒, epoll_wait得以继续执⾏行行; 执⾏行行ep_events_transfer函数: • 将rdlist中的epitem拷⻉贝到txlist中, 并将rdlist清空; • 如果是epoll LT, 并且fd.events状态没有改变(⽐比如buffer中数据没读完并不不会改变状态), 会再重新将epitem放回rdlist; 执⾏行行ep_send_events函数(关键哥来了了): • 扫描txlist中的每个epitem, 调⽤用其关联fd对应的poll⽅方法取得较新的events; • 将取得的events和相应的fd发送到⽤用户空间;
14. epoll LT vs ET • • 概念: • LT: level-triggered ⽔水平触发; • ET: edge-triggered 边沿触发; 可读: • • buffer不不为空的时候fd的events中对应 的可读状态就被置为1, 否则为0; 可写: • buffer中有空间可写的时候fd的events 中对应的可写状态就被置为1, 否则为0;
15. Netty Thread Model multi-reactor netty-reactor
16. Netty中⼏几个重要概念 • EventLoopGroup • EventLoop • Boss/Worker • Channel • Pipeline • ChannelHandler
17. Netty中⼏几个重要概念及其关系 • EventLoopGroup中包含⼀一组EventLoop; • EventLoop的⼤大致数据结构是: • • • • ⼀一个任务队列列(mpsc_queue:'>queue: 多⽣生产者单消费者 lock-free); • ⼀一个延迟任务队列列(delay_queue:'>queue: ⼀一个⼆二叉堆结构的优先级队列列, 复杂度为O(log n)); • EventLoop绑定了了⼀一个Thread, 这直接避免了了pipeline中的线程竞争; • 每个EventLoop有⼀一个Selector, Boss⽤用Selector处理理accept, Worker⽤用Selector处理理read, write等; Boss可理理解为Reactor模式中的mainReactor⻆角⾊色, Worker可理理解为subReactor⻆角⾊色; • Boss和Worker共⽤用EventLoop的代码逻辑; • 在不不bind多端⼝口的情况下BossEventLoopGroup中只需要包含⼀一个EventLoop, 也只能⽤用上⼀一个, 多了了没⽤用; • WorkerEventLoopGroup中⼀一般包含多个EventLoop, 经验值⼀一般为 cpu cores * 2(根据场景测试找出最佳值才是王道); • Netty server启动后会把⼀一个监听套接字ServerSocketChannel注册到Boss中; • Boss主要责任就是accept连接(channel), 然后轮询打赏给Worker; • Worker接到Boss打赏的channel后负责处理理此channel后续的read/write等IO事件; Channel分两⼤大类ServerChannel和Channel, ServerChannel对应着监听套接字(ServerSocketChannel), Channel对 应着⼀一个⽹网络连接; Pipeline是责任链模式的设计, ⾥里里⾯面有多个handler的, 上⾏行行事件顺序执⾏行行pipeline, 下⾏行行事件逆序执⾏行行pipeline;
18. ChannelPipeline + 异步事件驱动
19. Pooling & reuse • PooledByteBufAllocator • 基于 jemalloc paper (3.x); • ThreadLocal caches for lock free; • • • 这个做法导致曾经有坑: 申请(Bytebuf)线程与归还(Bytebuf)线程不不是同⼀一个导致内存泄漏漏, 后来⽤用⼀一个mpsc_queue解 决, 代价就是牺牲了了⼀一点点性能; Different size classes; Recycler • ThreadLocal + Stack • • • • 曾经有坑, 申请(元素)线程与归还(元素)线程不不是同⼀一个导致内存泄漏漏; 后来改进为不不同线程归还元素的时候放⼊入⼀一个WeakOrderQueue中并关联到stack上, 下次pop时如果stack为空则先扫 描所有关联到当前stack上的weakOrderQueue; WeakOrderQueue是多个数组的链表, 每个数组默认size=16; 问题: ⽼老老年年代对象引⽤用新⽣生代对象对GC的影响;
20. 最佳实践 • 业务线程池必要性: • 业务逻辑尤其是阻塞时间较⻓长的逻辑, 不不要占⽤用netty的IO线程, dispatch到业务线程池中去; • WriteBufferWaterMark, 注意默认的⾼高低⽔水位线设置(32K~64K), 根据场景适当调整; • 重写MessageSizeEstimator来反应真实的⾼高低⽔水位线: • • • • 默认实现不不能计算对象size, 由于write时还没路路过任何⼀一个outboundHandler就已经开始计算message size, 此时对象还没有被 encode成Bytebuf, 所以size计算肯定是不不准确的(偏低); 注意EventLoop#ioRatio的设置(默认50), 这是EventLoop执⾏行行IO任务和⾮非IO任务的⼀一个时间⽐比 例例上的控制; 对象的序列列化/反序列列化操作建议在IO线程之外处理理, ⼀一个是这类操作占⽤用IO线程的cpu时间, 再⼀一个是可能有略略严重的锁竞争; 空闲链路路检测⽤用谁调度? • • Netty4.x默认使⽤用IO线程调度, 使⽤用eventLoop的delayQueue, ⼀一个⼆二叉堆实现的优先级队列列, 复杂度为O(log N), 每个worker处 理理⾃自⼰己的链路路监测, 有助于减少上下⽂文切换, 但是⽹网络IO操作与idle会相互影响; 如果总的连接数⼩小, ⽐比如⼏几万以内, 上⾯面的实现并没什什么问题, 连接数⼤大建议⽤用HashedWheelTimer实现⼀一个IdleStateHandler, HashedWheelTimer复杂度为 O(1), 同时可以让⽹网络IO操作和idle互不不影响, 但有上下⽂文切换开销;
21. 最佳实践 • 使⽤用ctx.writeAndFlush还是channel.writeAndFlush? • ctx.write直接⾛走到下⼀一个outbound handler, 注意别让它违背你的初衷绕过了了空闲链路路检测; • channel.write从末尾开始倒着向前挨个路路过pipeline中的所有outbound handlers; • 使⽤用Bytebuf.forEachByte() 来代替循环 ByteBuf.readByte()的遍历操作, 避免rangeCheck(); • 使⽤用CompositeByteBuf来避免不不必要的内存拷⻉贝: • • 如果要读⼀一个int, ⽤用Bytebuf.readInt(), 不不要Bytebuf.readBytes(buf, 0, 4): • • 缺点是索引计算时间复杂度⾼高, 请根据⾃自⼰己场景衡量量; 这能避免⼀一次memory copy (long, short等同理理); 配置UnpooledUnsafeNoCleanerDirectByteBuf来代替jdk的DirectByteBuf, 让netty框架基于引⽤用计数来 释放堆外内存: • io.netty.maxDirectMemory • • • < 0: 不不使⽤用cleaner, netty⽅方⾯面直接继承jdk设置的最⼤大direct memory size, (jdk的direct memory size是独⽴立的, 这将导致总的direct memory size将是jdk配置的2倍); == 0: 使⽤用cleaner, netty⽅方⾯面不不设置最⼤大direct memory size; > 0: 不不使⽤用cleaner, 并且这个参数将直接限制netty的最⼤大direct memory size, (jdk的direct memory size是独⽴立的, 不不受此参数限制);
22. 最佳实践 • • • 最佳连接数: • ⼀一条连接有瓶颈, ⽆无法有效利利⽤用cpu; • 连接太多也⽩白扯, 最佳实践是根据⾃自⼰己场景测试; 使⽤用PooledBytebuf时要善于利利⽤用 -Dio.netty.leakDetection.level 参数: • 四种级别: DISABLED(禁⽤用), SIMPLE(简单), ADVANCED(⾼高级), PARANOID(偏执); • SIMPLE, ADVANCED采样率相同, 不不到1%(按位与操作 mask ==128 - 1); • 默认是SIMPLE级别, 开销不不⼤大; • 出现泄漏漏时⽇日志会出现”LEAK:'>LEAK: ”字样, 请时不不时grep下⽇日志, ⼀一旦出现”LEAK:'>LEAK: ”⽴立刻改为ADVANCED级别再跑, 可以报 告泄漏漏对象在哪被访问的; • PARANOID: 测试的时候建议使⽤用这个级别, 100%采样; Channel.attr(), 将⾃自⼰己的对象attach到channel上; • 拉链法实现的线程安全的hash表, 也是分段锁(锁链表头), 只有hash冲突的情况下才有锁竞争; • 默认hash表只有4个桶哦, 使⽤用时不不要太任性;
23. 从Netty源码中学到的代码技巧 • 海海量量对象场景中 AtomicIntegerFieldUpdater --> AtomicInteger: • • • AtomicIntegerFieldUpdater作为static field去操作volatile int; FastThreadLocal, 相⽐比jdk的实现更更快: • • Java中对象头12 bytes(开启压缩指针的情况下), ⼜又因为Java对象按照8字节对⻬齐, 所以对象最⼩小16 bytes, AtomicInteger⼤大⼩小为16 bytes, AtomicLong⼤大⼩小为 24 bytes; index原⼦子⾃自增的数组存储 —> 拉链法的Hash表; IntObjectHashMap / LongObjectHashMap … • 这个流⾏行行不不是⼀一天两天了了, 并不不是netty先这样玩的, ⾸首先int代替Integer作为key直接省了了12个字节的内存, 哈希冲突的解决⽅方式是线性扩展⽽而⾮非HashMap⾥里里⾯面的拉链法; • RecyclableArrayList, 基于前⾯面说的Recycler, 频繁new ArrayList的场景可考虑; • JCTools: ⼀一些jdk没有的 SPSC/MPSC/SPMC/MPMC 并发队列列;
24. 如何利利⽤用Netty写⼀一个⾼高性能RPC框架 • 协议格式 • Proxy • ⽹网络层可扩展 • 泛化调⽤用 • 压榨性能(Don’t trust it, Test it) • Example
25. 协议格式 • 协议头: * * * * * * * * * * * * * * ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ 2 │ 1 │ 1 │ ├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │ │ │ MAGIC Sign Status │ │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ Protocol ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ 8 │ 4 │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │ Invoke Id Body Length Body Content │ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤ │ ┘ 消息头16个字节定⻓长 = 2 // MAGIC = (short) 0xbabe + 1 // 消息标志位, 低地址4位⽤用来表示消息类型Request/Response/Heartbeat等, ⾼高地址4位⽤用来表示序列列化类型 + 1 // 状态位, 设置请求响应状态 + 8 // 消息 id, long 类型 + 4 // 消息体 body ⻓长度, int类型 (byte) (sign & 0x0f) (byte) ((((int) sign) & 0xff) >> 4) • ┐ // 低地址4位 // ⾼高地址4位 协议体: • metadata: group, version, providerName; • methodName • parameterTypes[] 真的需要? • • 问题: 1. 反序列列化时触发ClassLoader.loadClass()潜在的锁竞争; 2. 协议体码流⼤大⼩小; 3: 泛化调⽤用多了了这个恼⼈人的参数类型; ⽅方法重载问题解决: 根据参数类型找到最匹配的⽅方法, 规则参考JLS $15.12.2.5 Choosing the Most Specific Method 章节; 或 者直接使⽤用现成的实现commons-lang#MethodUtils.getMatchingAccessibleMethod(); • args[] • 其他: traceId, appName…
26. Proxy = >: ) = < > )= >: ) Proxy < >< A > , < A >< A >< = >< A A A (A = A = Provider (A = . = L N A & = >: ) >< A A M I >< A A = : (A = N >< A = = * 若是netty4.x的线程模型: IO Thread(worker) —> Map 代替全局Map能更更好的避免线程竞争
27. ⽹网络层可扩展&泛化调⽤用 • • • SPI: • -java.util.ServiceLoader • -META-INF/services/com.xxx.Xxx 剥离对netty#channel的依赖: • Map.put(nettyChannel, myChannel)? • nettyChannel.attr(name).setIfAbsent(myChannel)或许更更有利利于减少锁竞争; 泛化: Object $invoke(String methodName, Object... args); • 不不需要Class[] parameterTypes;
28. 压榨性能(Don’t trust it, Test it) • 客户端Proxy对象(jdkProxy&javassist&cglib&asm&bytebuddy); new ByteBuddy() .subclass(interfaceType) .method(isDeclaredBy(interfaceType)) .intercept(to(handler, "handler").filter(not(isDeclaredBy(Object.class)))) .make() .load(interfaceType.getClassLoader(), INJECTION) .getLoaded().newInstance() 1. filter(not(isDeclaredBy(Object.class)))), 避免toString/equals等⽅方法被代理理进⾏行行远程调⽤用; 2. 轻微的性能优势(不不要相信我, JMH test之 http://openjdk.java.net/projects/code-tools/jmh/); • 服务端不不可避免的反射⽤用cglib#FastClass代替(记得FastClass要缓存); • TCP_NODELAY设置为true; • 避免反序列列化占⽤用IO线程: • • 选择⾼高效的序列列化/反序列列化框架(kryo/protobuf/protostuff/hessian/fastjson/…); • • decoder中根据header拿到bytes[]的body就好, 反序列列化交给业务线程; https://github.com/eishay/jvm-serializers/wiki IO线程绑定cpu? netty没做这个, 提前意淫下; • linux平台参考: https://github.com/OpenHFT/Java-Thread-Affinity • 压测时你会发现并发量量⼤大时瓶颈在客户端, 客户端改为协程? (kilim&quasar) • Netty Native Transport & PooledByteBufAllocator (减⼩小GC带来的波动); • 减少IO线程占⽤用时间, 尽量量减少线程上下⽂文切换;
29. Example • 玩票性质: https://github.com/fengjiachun/Jupiter • ⼼心中标杆: https://github.com/alibaba/dubbo
30. 参考资料料: • • • • Netty:https://github.com/netty/nettyhttps://www.infoq.com/presentations/apple-netty JDK-source: • /jdk/src/solaris/classes/sun/nio/ch/EPollSelectorImpl.java • /jdk/src/solaris/classes/sun/nio/ch/EPollArrayWrapper.java • /jdk/src/solaris/native/sun/nio/ch/EPollArrayWrapper.c Linux-source: • linux-2.6.11.12/fs/eventpoll.c • https://code.csdn.net/chenyu105/linux_kernel_2-6-11-12_comment/tree/masterhttps://github.com/torvalds/linux RPS/RFS: • • https://my.oschina.net/guol/blog/113144 请翻⻚页