一什么是Netty?它能做什么?Netty是一个成熟的IO框架,致力于创建高性能的网络应用程序。与直接使用底层JavaIOAPI相比,你不需要成为网络专家就可以基于Netty构建复杂的网络应用。业界常见的网络通信相关的中间件,大部分都是基于Netty实现网络层的。2设计一个分布式服务框架1架构2远程调用过程启动服务器(服务提供者),将服务发布到注册中心。启动客户端(服务消费者)并到注册中心订阅感兴趣的服务。客户端收到注册中心推送的服务地址列表。当调用者发起调用时,Proxy从服务地址列表中选择一个地址并将请求信息,methodName,args[]等信息序列化为字节数组,通过网络发送给该地址.服务端接收并反序列化请求信息,根据从本地服务字典中找到对应的providerObject,然后根据通过反射调用指定的方法,并返回该方法值被序列化为字节数组并返回给客户端。客户端收到响应信息并反序列化为Java对象后,Proxy返回给方法调用者。上面的过程对方法调用者来说是透明的,一切看起来就像是本地调用。3图远程调用客户端重要概念:RPC三元组。PS:如果是netty4.x的线程模型,IOThread(worker)—>Map而不是globalMap可以更好的避免线程竞争。4远程调用服务器图5远程调用传输层图6设计传输层协议栈协议头协议体1)metadata:2)methodName3)parameterTypes[]真的需要吗?(一)那里有什么?问题?反序列化时与ClassLoader.loadClass()的潜在锁争用。协议主体流大小。广义调用有更多的参数类型。(b)能解决吗?参考JLS$15.12.2.5ChoosingtheMostSpecificMethodforstaticdispatchrulesofJavamethods。(c)args[](d)Others:traceId,appName...三个一些特性&良好实践&挤压性能1创建客户端代理对象1)Proxy做什么?ClusterFaultTolerance—>LoadBalancing—>Network2)它们是什么?如何创建代理?jdkproxy/javassist/cglib/asm/bytebuddy3)注意:注意拦截toString、equals、hashCode等方法,避免远程调用。4)推荐(bytebuddy):2优雅的同步/异步调用先往上翻看《RemoteCallClientDiagram》再往下翻看Failover是怎么处理的想想以后怎么弄?3命令Broadcast/multicastmessagedispatcherFutureGroup4generalizationcall5serialization/deserializationprotocolheadermarkserializertype,同时支持多种类型。6ScalabilityJavaSPI:java.util.ServiceLoaderMETA-INF/services/com.xxx.Xxx7服务级线程池隔离先吊你,别拉我。8责任链模式的拦截器太多的扩展从这里开始。9Metrics10LinkTrackingOpenTracing11RegistrationCenter12FlowControl(ApplicationLevel/ServiceLevel)必须具备方便接入第三方流控中间件的能力。13Provider线程池满了怎么办?14软负载均衡1)加权随机(二分法,不遍历)2)加权循环训练(最大公约数)3)最小负载4)一致性哈希(有状态服务场景)5)其他注意事项:必须有预热逻辑.15集群容错1)Fail-fast2)Failover如何处理异步调用?BadBetter3)Fail-safe4)Fail-back5)Forking6)Others16Howtosqueezeperformance(Don'ttrustit,Testit)1)ASMwriteaFastMethodAccessorinstead服务器端的反射调用2)序列化/反序列化被序列化/在业务线程中反序列化,避免占用IO线程:序列化/反序列化占用极少量的IO线程时间片。反序列化往往涉及到Class的加载,loadClass存在严重的锁竞争(可以通过jmc观察)。选择一个高效的序列化/反序列化框架:比如kryo/protobuf/protostuff/hessian/fastjson/...选择只是第一步,它(序列化框架)做的不好,要扩展和优化它:传统序列化的流程转换/反序列化+写入/读取网络:java对象-->byte[]-->堆外内存/堆外内存-->byte[]-->java对象。优化:省略byte[]链接,直接读写堆外内存,需要扩展相应的序列化框架。字符串编码/解码优化。Varint优化:多个writeByte合并成writeShort/writeInt/writeLong。Protostuff优化示例:UnsafeNioBufInput直接读堆外内存/UnsafeNioBufOutput直接写堆外内存。3)IO线程与CPU绑定4)客户端同步阻塞调用,容易成为瓶颈。客户端协程:Java层面的选择不多,暂时还不够完善。5)NettyNativeTransport&PooledByteBufAllocator:减少GC带来的波动。6)尽快释放IO线程去做他该做的事情,尽量减少线程上下文切换。四为什么选择Netty?1BIOvsNIO2JavaNativeNIOAPI从入门到放弃高复杂度API复杂难懂,上手难。粘包/半包的问题比较麻烦。需要很强的并发/异步编程能力,否则很难写出高效稳定的实现。稳定性差,坑多,深度调试困难。偶尔遇到难以想象且极难复现的bug,哭着查也是常有的事。linux下EPollArrayWrapper.epollWait直接返回导致空训练,100%cpu的bug一直没有解决。Netty帮助您变通(通过重建选择器)。NIO代码实现的一些不足1)Selector.selectedKeys()产生过多垃圾Netty修改了sun.nio.ch.SelectorImpl的实现,使用双数组代替HashSet存储selectKeys:相比于HashSet(迭代器,包装对象等).)更少的垃圾产生(帮助GC)。轻微的性能提升(1~2%)。Nio的代码是处处同步的(比如allocatedirectbuffer和Selector.wakeup()):对于allocatedirectbuffer,Netty的pooledBytebuf有pre-TLAB(Thread-localallocationbuffer)可以有效减少锁的竞争。唤醒调用太多,锁竞争严重,开销非常大(开销大的原因:为了在select线程外与select线程通信,在linux平台上使用了一对管道,并且因为在windows上管道句柄不能放在fd_set中,只能妥协两个tcp连接模拟),较少的wakeupcalls很容易导致select时不必要的阻塞(迷茫的话直接用Netty就可以了,Netty有相应的优化)逻辑)。NettyNativeTransport中的锁要少很多。2)fdToKey映射EPollSelectorImpl#fdToKey维护了所有连接的fd(描述符)对应SelectionKey的映射,是一个HashMap。每个worker线程都有一个selector,也就是每个worker都有一个fdToKey,这些fdToKey大致划分了所有的连接。想象一个场景,单机有几十万个连接,HashMap从默认的size=16开始,一步步rehashes...3)Linux平台的Selector是EpollLT,实现了NettyNativeTransport,支持Epoll等。4)DirectBuffers其实还是由GC管理的。DirectByteBuffer.cleaner的幻像引用负责释放直接内存。DirectByteBuffer只是一个外壳。如果这个shell熬过了新生代的年龄限制,最终被提升到老年代,那将是一件悲哀的事情……无法申请到足够的直接内存会显式触发GC,Bits.reserveMemory()->{System.gc()},首先因为GC打断了整个过程,所以代码还休眠了100毫秒,如果醒来发现不行就会OOM。更糟糕的是,如果听信某些的诽谤,设置了-XX:+DisableExplicitGC参数,悲剧就会悄悄发生……Netty的UnpooledUnsafeNoCleanerDirectByteBuf去掉了cleaner,Netty框架维护了引用计数到实时删除它释放。五Netty的真面目1Netty中的几个重要概念及其关系EventLoopaSelector。一个任务队列(mpsc_queue:多生产者单消费者无锁)。延迟任务队列(delay_queue:二叉堆结构的优先级队列,复杂度为O(logn))。EventLoop绑定了一个Thread,直接避免了管道中的线程竞争。Boss:mainReactor角色,Worker:subReactor角色Boss和Worker共享EventLoop的代码逻辑,Boss处理accept事件,Worker处理read、write等事件。Boss监听并接受连接(channel)后,将channel以循环方式交给Worker,Worker负责处理channel的后续IO事件,如read/write。在不绑定多个端口的情况下,只需要在BossEventLoopGroup中包含一个EventLoop,并且只能使用一个,多了也没用。WorkerEventLoopGroup一般包含多个EventLoop,经验值一般为cpucores*2(根据场景测试找到最佳值才是王道)。Channel分为两类,ServerChannel和Channel。ServerChannel对应一个监听套接字(ServerSocketChannel),Channel对应一个网络连接。2Netty4ThreadModel3ChannelPipeline4Pooling&reusePooledByteBufAllocatorisbasedonjemallocpaper(3.x)ThreadLocalcachesforlockfree:这种做法导致了一个坑——申请(Bytebuf)线程和返回(Bytebuf)线程不一样,导致内存泄露,后来用一个mpsc_queue解决,代价是牺牲一点点性能。不同的尺寸等级。RecyclerThreadLocal+堆栈。曾经有一个坑,申请(元素)线程和返回(元素)线程不是同一个,导致内存泄漏。之后,当不同的线程返回元素时,它们被放入一个WeakOrderQueue并与堆栈相关联。如果下次栈为空,则优先扫描当前栈关联的所有weakOrderQueues。WeakOrderQueue是多个数组的链表,每个数组默认size=16。存在的问题:想想老年代对象引用新生代对象对GC的影响?5NettyNativeTransport创建的对象比Nio更少,GC压力也更小。针对linux平台优化,一些具体的特性:SO_REUSEPORT-端口复用(允许多个socket监听同一个IP+端口,配合RPS/RFS进一步提升性能):RPS/RFS可以模糊理解为模拟在软件级多队列网卡,并提供负载均衡能力,避免网卡在一个CPU核上收发包中断,影响性能。TCP_FASTOPEN-也用于在3次握手期间交换数据。EDGE_TRIGGERED(重点支持epollET)。Unix域套接字(同一台机器上的进程间通信,如ServiceMesh)。6Multiplexing简介select/poll本身实现机制的局限性(使用轮询的方式检测就绪事件,时间复杂度:O(n),并且每次都将用户空间和内核空间臃肿的fd_set复制到),越大并发连接越多,性能越差。poll和select之间没有太大区别,只是取消了对文件描述符最大数量的限制。select/poll都是LT模式。epoll使用回调方式检测就绪事件,时间复杂度:O(1),每次epoll_wait调用只返回就绪文件描述符。epoll支持LT和ET模式。7稍微深入理解一下EpollLTvsET的概念:LT:level-triggeredleveltriggeringET:edge-triggerededgetriggering可读:当buffer不为空时,fd的events中对应的可读状态置1,否则为0。可写:当缓冲区有空间可写时,将fd的事件中对应的可写状态设置为1,否则为0。图:epoll的三个方法介绍1)主要代码:linux-2.6.11.12/fs/eventpoll.c2)intepoll_create(intsize)创建rb-tree(红黑树)和ready-list(就绪链表):红黑树O(logN),平衡效率和内存使用,当容量需求不确定且容量可能很大时,红黑树是最佳选择。大小参数没有意义。早期的epoll实现是哈希表,所以需要size参数。3)intepoll_ctl(intepfd,intop,intfd,structepoll_event*event)将epitem放入rb-tree并向内核中断处理程序注册ep_poll_callback,并在触发回调时将epitem放入就绪列表.4)intepoll_wait(intepfd,structepoll_event*events,intmaxevents,inttimeout)ready-list—>events[].epoll数据结构epoll_wait工作流概览控制代码:linux-2.6.11.12/fs/eventpoll.c:1)epoll_wait在rdlist(ready-list)为空时调用ep_poll(没有就绪fd)挂起当前线程,直到rdlist线程被唤醒当它不为空时。2)文件描述符fd的事件状态由不可读变为可读或由不可写变为可写,导致相应fd上的回调函数ep_poll_callback被触发。3)ep_poll_callback被触发将对应fd对应的epitem添加到rdlist中,导致rdlist不为空,线程被唤醒,epoll_wait可以继续执行。4)执行ep_events_transfer函数将rdlist中的epitem复制到txlist中,并清空rdlist。如果是epollLT,并且fd.events的状态没有变化(比如buffer中的数据没有被读取,状态不会变化),epitem会重新放回rdlist。5)执行ep_send_events函数扫描txlist中的每一个epitem,调用其关联的fd对应的poll方法获取较新的事件。将获取到的事件和对应的fd发送到用户空间。8Netty的最佳实践1)业务线程池的必要性业务逻辑,尤其是阻塞时间长的逻辑,不要占用netty的IO线程,dispatch到业务线程池。2)WriteBufferWaterMark注意默认的高低水位设置(32K~64K),根据场景适当调整(大家可以想想怎么用)。3)重写MessageSizeEstimator以反映真实的高低水印。默认实现无法计算对象大小。由于在写入时传递任何outboundHandler之前已经计算了消息大小,此时对象还没有被编码到Bytebuf中,所以大小计算一定是不准确的(偏低)。4)注意EventLoop#ioRatio的设置(默认50)。这是EventLoop执行IO任务和非IO任务的时间比例控制。5)谁安排空闲链路检测?Netty4.x默认使用IO线程调度,使用eventLoop的delayQueue,二叉堆实现的优先级队列,复杂度为O(logN),每个worker处理自己的链道监听有助于减少上下文切换,但是网络IO操作和闲置会相互影响。如果连接总数比较少,比如几万以内,上面的实现是没有问题的。如果连接数较多,建议使用HashedWheelTimer实现一个IdleStateHandler。HashedWheelTimer的复杂度为O(1)。同时,网络IO操作和空闲不能互相影响。但是存在上下文切换开销。6)使用ctx.writeAndFlush还是channel.writeAndFlush?ctx.write直接进入下一个outboundhandler,注意不要让它违背初衷,绕过idlelinkdetection。channel.write从末尾开始,逐一遍历管道中的所有出站处理程序。7)使用Bytebuf.forEachByte()代替loopByteBuf.readByte()的遍历操作,避免rangeCheck()8)使用CompositeByteBuf,避免不必要的内存拷贝缺点是索引计算时间复杂度高,请根据你自己的场景。9)如果你想读取一个int,使用Bytebuf.readInt()而不是Bytebuf.readBytes(buf,0,4)这样可以避免内存复制(长,短等)。10)配置UnpooledUnsafeNoCleanerDirectByteBuf来替代jdk的DirectByteBuf,让netty框架根据引用计数释放堆外内存。内存大小是独立的,这会导致直接内存总大小是jdk配置的2倍)。==0:使用cleaner,netty没有设置最大直接内存大小。0:不使用cleaner,这个参数会直接限制netty的最大直接内存大小(jdk的直接内存大小是独立的,不受这个参数限制)。11)最佳连接数。一个连接出现瓶颈,无法有效利用CPU。太多的连接是没有用的。最好的做法是根据自己的场景进行测试。12)使用PooledBytebuf时,要善于使用-Dio.netty.leakDetection.level参数的四个级别:DISABLED(禁用)、SIMPLE(简单)、ADVANCED(高级)、PARANOID(偏执)。SIMPLE、ADVANCED采样率相同,小于1%(按位AND运算掩码==128-1)。默认为SIMPLE级别,开销很小。发生泄漏时,日志将显示“LEAK:”字样。请不时grep日志。一旦出现“LEAK:”,立即切换到ADVANCED级别再次运行。您可以报告泄漏对象的访问位置。PARANOID:建议测试时使用该级别,100%抽样。13)Channel.attr(),将自己的对象附加到通道上zipper方法实现的线程安全哈希表中。也是分段锁(只锁链头),只有在hash冲突的情况下才会有锁竞争(类似ConcurrentHashMapV8版本)。默认的哈希表只有4个桶,所以不要随意使用。9从Netty源码中学到的代码技巧1)海量对象场景下的AtomicIntegerFieldUpdater-->AtomicIntegerJava对象头12字节(启用压缩指针时),由于Java对象按8字节对齐,最小对象大小为16字节,AtomicInteger的大小为16个字节,AtomicLong的大小为24个字节。AtomicIntegerFieldUpdater将volatileint作为静态字段进行操作。2)FastThreadLocal,相比jdk的Hash表实现更快的线性检测—>裸数组存储,索引原子自增。3)IntObjectHashMap/LongObjectHashMap…Integer—>intNode[]—>nakedarray4)RecyclableArrayList基于上面提到的Recycler,可以考虑频繁newArrayList的场景。5)部分JCToolsJDK没有SPSC/MPSC/SPMC/MPMC无锁并发组和NonblockingHashMap(可以和ConcurrentHashMapV6/V8对比)