简介:Netty 是一个致力于创立高性能网络应用程序的成熟的 IO 框架。相比拟与间接应用底层的 Java IO API,不须要先成为网络专家就能够基于 Netty 去构建简单的网络应用。业界常见的波及到网络通信的相干中间件大部分基于 Netty 实现网络层。
作者 | 家纯
起源 | 阿里技术公众号
一 什么是 Netty? 能做什么?
Netty 是一个致力于创立高性能网络应用程序的成熟的 IO 框架。
相比拟与间接应用底层的 Java IO API,你不须要先成为网络专家就能够基于 Netty 去构建简单的网络应用。
业界常见的波及到网络通信的相干中间件大部分基于 Netty 实现网络层。
二 设计一个分布式服务框架
1 Architecture
2 近程调用的流程
- 启动服务端 (服务提供者) 并公布服务到注册核心。
- 启动客户端 (服务消费者) 并去注册核心订阅感兴趣的服务。
- 客户端收到注册核心推送的服务地址列表。
- 调用者发动调用,Proxy 从服务地址列表中抉择一个地址并将申请信息 <group,providerName,version>,methodName,args[] 等信息序列化为字节数组并通过网络发送到该地址上。
- 服务端收到收到并反序列化申请信息,依据 <group,providerName,version> 从本地服务字典里查找到对应 providerObject,再依据 <methodName,args[]> 通过反射调用指定办法,并将办法返回值序列化为字节数组返回给客户端。
- 客户端收到响应信息再反序列化为 Java 对象后由 Proxy 返回给办法调用者。
以上流程对办法调用者是通明的,所有看起来就像本地调用一样。
3 近程调用客户端图解
重要概念:RPC 三元组 <ID,Request,Response>。
PS: 若是 netty4.x 的线程模型,IO Thread(worker) —> Map<InvokeId,Future> 代替全局 Map 能更好的防止线程竞争。
4 近程调用服务端图解
5 近程调用传输层图解
6 设计传输层协定栈
协定头
协定体
1)metadata: <group,providerName,version>
2)methodName
3)parameterTypes[] 真的须要吗?
(a)有什么问题?
- 反序列化时 ClassLoader.loadClass() 潜在锁竞争。
- 协定体码流大小。
- 泛化调用多了参数类型。
(b)能解决吗?
- Java 办法动态分派规定参考 JLS <Java 语言标准 > $15.12.2.5 Choosing the Most Specific Method 章节。
(c)args[]
(d)其余:traceId,appName…
三 一些 Features& 好的实际 & 压迫性能
1 创立客户端代理对象
1)Proxy 做什么?
集群容错 —> 负载平衡 —> 网络
2)有哪些创立 Proxy 的形式?
jdk proxy/javassist/cglib/asm/bytebuddy
3)要留神的:
留神拦挡 toString,equals,hashCode 等办法防止近程调用。
4)举荐的(bytebuddy):
2 优雅的同步 / 异步调用
- 先往上翻再看看“近程调用客户端图解”
- 再往下翻翻看看 Failover 如何解决更好
- 思考下如何拿到 future?
3 单播 / 组播
- 音讯派发器
- FutureGroup
4 泛化调用
5 序列化 / 反序列化
协定 header 标记 serializer type,同时反对多种。
6 可扩展性
Java SPI:
- java.util.ServiceLoader
- META-INF/services/com.xxx.Xxx
7 服务级别线程池隔离
要挂你先挂,别拉着我。
8 责任链模式的拦截器
太多扩大须要从这里起步。
9 指标度量(Metrics)
10 链路追踪
OpenTracing
11 注册核心
12 流控(利用级别 / 服务级别)
要有能不便接入第三方流控中间件的扩大能力。
13 Provider 线程池满了怎么办?
14 软负载平衡
1)加权随机 (二分法,不要遍历)
2)加权轮训(最大公约数)
3)最小负载
4)一致性 hash (有状态服务场景)
5)其余
留神:要有预热逻辑。
15 集群容错
1)Fail-fast
2)Failover
异步调用怎么解决?
Bad
Better
3)Fail-safe
4)Fail-back
5)Forking
6)其余
16 如何压迫性能(Don’t trust it,Test it)
1)ASM 写个 FastMethodAccessor 来代替服务端那个反射调用
2)序列化 / 反序列化
在业务线程中序列化 / 反序列化,防止占用 IO 线程:
- 序列化 / 反序列化占用数量极少的 IO 线程工夫片。
- 反序列化经常会波及到 Class 的加载,loadClass 有一把锁竞争重大(可通过 JMC 察看一下)。
抉择高效的序列化 / 反序列化框架:
- 如 kryo/protobuf/protostuff/hessian/fastjson/…
抉择只是第一步,它 (序列化框架) 做的不好的,去扩大和优化之:
- 传统的序列化 / 反序列化 + 写入 / 读取网络的流程:java 对象 –> byte[] –> 堆外内存 / 堆外内存 –> byte[] –>java 对象。
- 优化:省去 byte[] 环节,间接 读 / 写 堆外内存,这须要扩大对应的序列化框架。
- String 编码 / 解码优化。
- Varint 优化:屡次 writeByte 合并为 writeShort/writeInt/writeLong。
- Protostuff 优化举例:UnsafeNioBufInput 间接读堆外内存 /UnsafeNioBufOutput 间接写堆外内存。
3)IO 线程绑定 CPU
4)同步阻塞调用的客户端和容易成为瓶颈,客户端协程:
Java 层面可选的并不多,临时也都不完满。
5)Netty Native Transport & PooledByteBufAllocator:
- 减小 GC 带来的稳定。
6)尽快开释 IO 线程去做他该做的事件,尽量减少线程上下文切换。
四 Why Netty?
1 BIO vs NIO
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 等事件。
- Boss 监听并 accept 连贯 (channel) 后以轮训的形式将 channel 交给 Worker,Worker 负责解决此 channel 后续的 read/write 等 IO 事件。
- 在不 bind 多端口的状况下 BossEventLoopGroup 中只须要蕴含一个 EventLoop,也只能用上一个,多了没用。
- WorkerEventLoopGroup 中个别蕴含多个 EventLoop,经验值个别为 cpu cores *
2(依据场景测试找出最佳值才是王道)。
Channel 分两大类 ServerChannel 和 Channel,ServerChannel 对应着监听套接字(ServerSocketChannel),Channel 对应着一个网络连接。
2 Netty4 Thread Model
3 ChannelPipeline
4 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 的影响?
5 Netty Native Transport
相比 Nio 创立更少的对象,更小的 GC 压力。
针对 linux 平台优化,一些 specific features:
- SO_REUSEPORT – 端口复用(容许多个 socket 监听同一个 IP+ 端口,与 RPS/RFS 合作,可进一步晋升性能):可把 RPS/RFS 含糊的了解为在软件层面模仿多队列网卡,并提供负载平衡能力,防止网卡收包发包的中断集中的一个 CPU core 上而影响性能。
- TCP_FASTOPEN – 3 次握手时也用来替换数据。
- EDGE_TRIGGERED (反对 Epoll ET 是重点)。
- Unix 域套接字(同一台机器上的过程间通信,比方 Service Mesh)。
6 多路复用简介
select/poll
- 自身的实现机制上的限度(采纳轮询形式检测就绪事件,工夫复杂度: O(n),每次还要将臃肿的 fd_set 在用户空间和内核空间拷贝来拷贝去),并发连贯越大,性能越差。
- poll 相比 select 没有很大差别,只是勾销了最大文件描述符个数的限度。
- select/poll 都是 LT 模式。
epoll
- 采纳回调形式检测就绪事件,工夫复杂度: O(1),每次 epoll_wait 调用只返回已就绪的文件描述符。
- epoll 反对 LT 和 ET 模式。
7 略微深刻理解一点 Epoll
LT vs ET
概念:
- LT:level-triggered 程度触发
- ET:edge-triggered 边际触发
可读:
- buffer 不为空的时候 fd 的 events 中对应的可读状态就被置为 1,否则为 0。
可写:
- buffer 中有空间可写的时候 fd 的 events 中对应的可写状态就被置为 1,否则为 0。
图解:
epoll 三个办法简介
1)次要代码:linux-2.6.11.12/fs/eventpoll.c
2)int epoll_create(int size)
创立 rb-tree(红黑树)和 ready-list (就绪链表):
- 红黑树 O(logN),均衡效率和内存占用,在容量需要不能确定并可能量很大的状况下红黑树是最佳抉择。
- size 参数曾经没什么意义,晚期 epoll 实现是 hash 表,所以须要 size 参数。
3)int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event)
- 把 epitem 放入 rb-tree 并向内核中断处理程序注册 ep_poll_callback,callback 触发时把该 epitem 放进 ready-list。
4)int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout)
- ready-list —> events[]。
epoll 的数据结构
epoll_wait 工作流程概述
对照代码:linux-2.6.11.12/fs/eventpoll.c:
1)epoll_wait 调用 ep_poll
- 当 rdlist(ready-list) 为空 (无就绪 fd) 时挂起以后线程, 直到 rdlist 不为空时线程才被唤醒。
2)文件描述符 fd 的 events 状态扭转
- buffer 由不可读变为可读或由不可写变为可写,导致相应 fd 上的回调函数 ep_poll_callback 被触发。
3)ep_poll_callback 被触发
- 将相应 fd 对应 epitem 退出 rdlist,导致 rdlist 不空,线程被唤醒,epoll_wait 得以继续执行。
4)执行 ep_events_transfer 函数
- 将 rdlist 中的 epitem 拷贝到 txlist 中,并将 rdlist 清空。
- 如果是 epoll LT,并且 fd.events 状态没有扭转(比方 buffer 中数据没读完并不会扭转状态),会再从新将 epitem 放回 rdlist。
5)执行 ep_send_events 函数
- 扫描 txlist 中的每个 epitem,调用其关联 fd 对应的 poll 办法获得较新的 events。
- 将获得的 events 和相应的 fd 发送到用户空间。
8 Netty 的最佳实际
1)业务线程池必要性
- 业务逻辑尤其是阻塞工夫较长的逻辑,不要占用 netty 的 IO 线程,dispatch 到业务线程池中去。
2)WriteBufferWaterMark
- 留神默认的高下水位线设置(32K~64K),依据场景适当调整(能够思考一下如何利用它)。
3)重写 MessageSizeEstimator 来反馈实在的高下水位线
- 默认实现不能计算对象 size,因为 write 时还没路过任何一个 outboundHandler 就曾经开始计算 message size,此时对象还没有被 encode 成 Bytebuf,所以 size 计算必定是不精确的(偏低)。
4)留神 EventLoop#ioRatio 的设置(默认 50)
- 这是 EventLoop 执行 IO 工作和非 IO 工作的一个工夫比例上的管制。
5)闲暇链路检测用谁调度?
- Netty4.x 默认应用 IO 线程调度,应用 eventLoop 的 delayQueue,一个二叉堆实现的优先级队列,复杂度为 O(log N),每个 worker 解决本人的链路监测,有助于缩小上下文切换,然而网络 IO 操作与 idle 会相互影响。
- 如果总的连接数小,比方几万以内,下面的实现并没什么问题,连接数大倡议用 HashedWheelTimer 实现一个 IdleStateHandler,HashedWheelTimer 复杂度为 O(1),同时能够让网络 IO 操作和 idle 互不影响,但有上下文切换开销。
6)应用 ctx.writeAndFlush 还是 channel.writeAndFlush?
- ctx.write 间接走到下一个 outbound handler,留神别让它违反你的初衷绕过了闲暇链路检测。
- channel.write 从开端开始倒着向前挨个路过 pipeline 中的所有 outbound handlers。
7)应用 Bytebuf.forEachByte() 来代替循环 ByteBuf.readByte()的遍历操作,防止 rangeCheck()
8)应用 CompositeByteBuf 来防止不必要的内存拷贝
- 毛病是索引计算工夫复杂度高,请依据本人场景掂量。
9)如果要读一个 int,用 Bytebuf.readInt(),不要 Bytebuf.readBytes(buf,0,4)
这能防止一次 memory copy (long,short 等同理)。
10)配置 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 是独立的,不受此参数限度)。
11)最佳连接数
- 一条连贯有瓶颈,无奈无效利用 cpu,连贯太多也白扯,最佳实际是依据本人场景测试。
12)应用 PooledBytebuf 时要长于利用 -Dio.netty.leakDetection.level 参数
- 四种级别:DISABLED(禁用),SIMPLE(简略),ADVANCED(高级),PARANOID(偏执)。
- SIMPLE,ADVANCED 采样率雷同,不到 1%(按位与操作 mask ==128 – 1)。
- 默认是 SIMPLE 级别,开销不大。
- 呈现透露时日志会呈现“LEAK:”字样,请时不时 grep 下日志,一旦呈现“LEAK:”立即改为 ADVANCED 级别再跑,能够报告透露对象在哪被拜访的。
- PARANOID:测试的时候倡议应用这个级别,100% 采样。
13)Channel.attr(),将本人的对象 attach 到 channel 上
- 拉链法实现的线程平安的 hash 表,也是分段锁(只锁链表头),只有 hash 抵触的状况下才有锁竞争(相似 ConcurrentHashMapV8 版本)。
- 默认 hash 表只有 4 个桶,应用不要太任性。
9 从 Netty 源码中学到的代码技巧
1)海量对象场景中 AtomicIntegerFieldUpdater –> AtomicInteger
- Java 中对象头 12 bytes(开启压缩指针的状况下),又因为 Java 对象依照 8 字节对齐,所以对象最小 16 bytes,AtomicInteger 大小为 16 bytes,AtomicLong 大小为 24 bytes。
AtomicIntegerFieldUpdater 作为 static field 去操作 volatile int。
2)FastThreadLocal,相比 jdk 的实现更快
- 线性探测的 Hash 表 —> index 原子自增的裸数组存储。
3)IntObjectHashMap / LongObjectHashMap …
- Integer—> int
- Node[] —> 裸数组
4)RecyclableArrayList
- 基于后面说的 Recycler,频繁 new ArrayList 的场景可思考。
5)JCTools
- 一些 jdk 没有的 SPSC/MPSC/SPMC/MPMC 无锁并发队以及 NonblockingHashMap(能够比照 ConcurrentHashMapV6/V8)
原文链接
本文为阿里云原创内容,未经容许不得转载。