#
立即生效
sysctl -p
Netty
调优
设置合理的线程数
对于线程池的调优
,
主要集中在用于接收海量设备
TCP
连接、
TLS
握手的
Acceptor
线程
池
( Netty
通常叫
boss NioEventLoop Group)
上
,
以及用于处理网络数据读写、心跳发送的
1O
工作线程池
(Nety
通常叫
work Nio EventLoop Group)
上。
对于
Nety
服务端
,
通常只需要启动一个监听端口用于端侧设备接入即可
,
但是如果服务
端集群实例比较少
,
甚至是单机
(
或者双机冷备
)
部署
,
在端侧设备在短时间内大量接入时
,
需要
对服务端的监听方式和线程模型做优化
,
以满足短时间内
(
例如
30s)
百万级的端侧设备接入的
需要。
服务端可以监听多个端口
,
利用主从
Reactor
线程模型做接入优化
,
前端通过
SLB
做
4
层
门
7
层负载均衡。
主从
Reactor
线程模型特点如下
:
服务端用于接收客户端连接的不再是一个单独的
NO
线程
,
而是一个独立的
NIO
线程池
; Acceptor
接收到客户端
TCP
连接请求并处理后
(
可能包含接
入认证等
),
将新创建的
Socketchanne
注册到
I/O
线程池
(subReactor
线程池
)
的某个
IO
线程
,
由它负责
Socketchannel
的读写和编解码工作
; Acceptor
线程池仅用于客户端的登录、握手和
安全认证等
,
一旦链路建立成功
,
就将链路注册到后端
sub reactor
线程池的
IO
线程
,
由
IO
线程
负责后续的
IO
操作。
对于
IO
工作线程池的优化
,
可以先采用系统默认值
(
即
CPU
内核数×
2)
进行性能测试
,
在
性能测试过程中采集
IO
线程的
CPU
占用大小
,
看是否存在瓶颈, 具体可以观察线程堆栈,
如果连续采集几次进行对比
,
发现线程堆栈都停留在
Selectorlmpl. lockAndDoSelect
,则说明
IO
线程比较空闲
,
无须对工作线程数做调整。
如果发现
IO
线程的热点停留在读或者写操作
,
或者停留在
Channelhandler
的执行处
,
则
可以通过适当调大
Nio EventLoop
线程的个数来提升网络的读写性能。
心跳优化
针对海量设备接入的服务端
,
心跳优化策略如下。
(1)
要能够及时检测失效的连接
,
并将其剔除
,
防止无效的连接句柄积压
,
导致
OOM
等问题
(2)
设置合理的心跳周期
,
防止心跳定时任务积压
,
造成频繁的老年代
GC(
新生代和老年代
都有导致
STW
的
GC,
不过耗时差异较大
),
导致应用暂停
(3)
使用
Nety
提供的链路空闲检测机制
,
不要自己创建定时任务线程池
,
加重系统的负担
,
以及增加潜在的并发安全问题。
当设备突然掉电、连接被防火墙挡住、长时间
GC
或者通信线程发生非预期异常时
,
会导
致链路不可用且不易被及时发现。特别是如果异常发生在凌晨业务低谷期间
,
当早晨业务高
峰期到来时
,
由于链路不可用会导致瞬间大批量业务失败或者超时
,
这将对系统的可靠性产生
重大的威胁。
从技术层面看
,
要解决链路的可靠性问题
,
必须周期性地对链路进行有效性检测。目前最
流行和通用的做法就是心跳检测。心跳检测机制分为三个层面:
(1)TCP
层的心跳检测
,
即
TCP
的
Keep-Alive
机制
,
它的作用域是整个
TCP
协议栈。
(2)
协议层的心跳检测
,
主要存在于长连接协议中
,
例如
MQTT
。
(3)
应用层的心跳检测
,
它主要由各业务产品通过约定方式定时给对方发送心跳消息实现。
心跳检测的目的就是确认当前链路是否可用
,
对方是否活着并且能够正常接收和发送消
息。作为高可靠的
NIO
框架
,Nety
也提供了心跳检测机制。
一般的心跳检测策略如下。
(1)
连续
N
次心跳检测都没有收到对方的
Pong
应答消息或者
Ping
请求消息
,
则认为链路
已经发生逻辑失效
,
这被称为心跳超时。
(2)
在读取和发送心跳消息的时候如果直接发生了
IO
异常
,
说明链路已经失效
,
这被称为
心跳失败。无论发生心跳超时还是心跳失败
,
都需要关闭链路
,
由客户端发起重连操作
,
保证链
路能够恢复正常。
Nety
提供了三种链路空闲检测机制
,
利用该机制可以轻松地实现心跳检测
(1)
读空闲
,
链路持续时间
T
没有读取到任何消息。
(2)
写空闲
,
链路持续时间
T
没有发送任何消息
(3)
读写空闲
,
链路持续时间
T
没有接收或者发送任何消息
对于百万级的服务器,一般不建议很长的心跳周期和超时时长。
接收和发送缓冲区调优
在一些场景下
,
端侧设备会周期性地上报数据和发送心跳
,
单个链路的消息收发量并不大
,
针对此类场景
,
可以通过调小
TCP
的接收和发送缓冲区来降低单个
TCP
连接的资源占用率
当然对于不同的应用场景
,
收发缓冲区的最优值可能不同
,
用户需要根据实际场景
,
结合
性能测试数据进行针对性的调优
合理使用内存池
随着
JVM
虚拟机和
JT
即时编译技术的发展
,
对象的分配和回收是一个非常轻量级的工作。
但是对于缓冲区
Buffer,
情况却稍有不同
,
特别是堆外直接内存的分配和回收
,
是一个耗时的
操作。
为了尽量重用缓冲区
,Nety
提供了基于内存池的缓冲区重用机制。
在百万级的情况下
,
需要为每个接入的端侧设备至少分配一个接收和发送
ByteBuf
缓冲
区对象
,
采用传统的非池模式
,
每次消息读写都需要创建和释放
ByteBuf
对象
,
如果有
100
万个
连接
,
每秒上报一次数据或者心跳
,
就会有
100
万次
/
秒的
ByteBuf
对象申请和释放
,
即便服务
端的内存可以满足要求
,GC
的压力也会非常大。
以上问题最有效的解决方法就是使用内存池
,
每个
NioEventLoop
线程处理
N
个链路
,
在
线程内部
,
链路的处理是串行的。假如
A
链路首先被处理
,
它会创建接收缓冲区等对象
,
待解码
完成
,
构造的
POJO
对象被封装成任务后投递到后台的线程池中执行
,
然后接收缓冲区会被释
放
,
每条消息的接收和处理都会重复接收缓冲区的创建和释放。如果使用内存池
,
则当
A
链路
接收到新的数据报时
,
从
NioEventLoop
的内存池中申请空闲的
ByteBuf,
解码后调用
release
将
ByteBuf
释放到内存池中
,
供后续的
B
链路使用。
Nety
内存池从实现上可以分为两类
:
堆外直接内存和堆内存。由于
Byte Buf
主要用于网
络
IO
读写
,
因此采用堆外直接内存会减少一次从用户堆内存到内核态的字节数组拷贝
,
所以
性能更高。由于
DirectByteBuf
的创建成本比较高
,
因此如果使用
DirectByteBuf,
则需要配合内
存池使用
,
否则性价比可能还不如
Heap Byte
。
Netty
默认的
IO
读写操作采用的都是内存池的堆外直接内存模式
,
如果用户需要额外使
用
ByteBuf,
建议也采用内存池方式
;
如果不涉及网络
IO
操作
(
只是纯粹的内存操作
),
可以使用
堆内存池
,
这样内存的创建效率会更高一些。
IO
线程和业务线程分离
如果服务端不做复杂的业务逻辑操作
,
仅是简单的内存操作和消息转发
,
则可以通过调大
NioEventLoop
工作线程池的方式
,
直接在
IO
线程中执行业务
Channelhandler,
这样便减少了一
次线程上下文切换
,
性能反而更高。
如果有复杂的业务逻辑操作
,
则建议
IO
线程和业务线程分离
,
对于
IO
线程
,
由于互相之间
不存在锁竞争
,
可以创建一个大的
NioEvent Loop Group
线程组
,
所有
Channel
都共享同一个
线程池。
对于后端的业务线程池
,
则建议创建多个小的业务线程池
,
线程池可以与
IO
线程绑定
,
这
样既减少了锁竞争
,
又提升了后端的处理性能。
针对端侧并发连接数的流控
无论服务端的性能优化到多少
,
都需要考虑流控功能。当资源成为瓶颈
,
或者遇到端侧设
备的大量接入
,
需要通过流控对系统做保护。流控的策略有很多种,比如针对端侧连接数的
流控:
在
Nety
中
,
可以非常方便地实现流控功能
:
新增一个
FlowControlchannelhandler
,然后添
加到
ChannelPipeline
靠前的位置
,
覆盖
channelActive()
方法
,
创建
TCP
链路后
,
执行流控逻辑
,
如果达到流控阈值
,
则拒绝该连接
,
调用
ChannelHandler Context
的
close(
方法关闭连接。
JVM
层面相关性能优化
当客户端的并发连接数达到数十万或者数百万时
,
系统一个较小的抖动就会导致很严重 的后果,例如服务端的
GC,
导致应用暂停
(STW)
的
GC
持续几秒
,
就会导致海量的客户端设备掉
线或者消息积压
,
一旦系统恢复
,
会有海量的设备接入或者海量的数据发送很可能瞬间就把服务端冲垮。
JVM
层面的调优主要涉及
GC
参数优化
,GC
参数设置不当会导致频繁
GC,
甚至
OOM
异常
, 对服务端的稳定运行产生重大影响。
1.
确定
GC
优化目标
GC(
垃圾收集
)
有三个主要指标。
(1)
吞吐量
:
是评价
GC
能力的重要指标
,
在不考虑
GC
引起的停顿时间或内存消耗时
,
吞吐量是 GC
能支撑应用程序达到的最高性能指标。
(2)
延迟
:GC
能力的最重要指标之一
,
是由于
GC
引起的停顿时间
,
优化目标是缩短延迟时间或完全消除停顿(STW),
避免应用程序在运行过程中发生抖动。
(3)
内存占用
:GC
正常时占用的内存量。
JVM GC
调优的三个基本原则如下。
(1) Minor go
回收原则
:
每次新生代
GC
回收尽可能多的内存
,
减少应用程序发生
Full gc
的频率。
(2)GC
内存最大化原则
:
垃圾收集器能够使用的内存越大
,
垃圾收集效率越高
,
应用程序运行也越流畅。但是过大的内存一次 Full go
耗时可能较长
,
如果能够有效避免
FullGC,
就需要做 精细化调优。
(3)3
选
2
原则
:
吞吐量、延迟和内存占用不能兼得
,
无法同时做到吞吐量和暂停时间都最优,
需要根据业务场景做选择。对于大多数应用
,
吞吐量优先
,
其次是延迟。当然对于时延敏感 型的业务,
需要调整次序。
2.
确定服务端内存占用
在优化
GC
之前
,
需要确定应用程序的内存占用大小
,
以便为应用程序设置合适的内存
,
提升 GC
效率。内存占用与活跃数据有关
,
活跃数据指的是应用程序稳定运行时长时间存活的
Java
对象。活跃数据的计算方式
:
通过
GC
日志采集
GC
数据
,
获取应用程序稳定时老年代占用的 Java
堆大小
,
以及永久代
(
元数据区
)
占用的
Java
堆大小
,
两者之和就是活跃数据的内存占用大小。
3.GC
优化过程
1
、
GC
数据的采集和研读
2
、设置合适的
JVM
堆大小
3
、选择合适的垃圾回收器和回收策略
当然具体如何做,请参考
JVM
相关课程。而且
GC
调优会是一个需要多次调整的过程,期间不仅有参数的变化,更重要的是需要调整业务代码。
select
、
poll
、
epoll
的区别?
select
,
poll
,
epoll
都是操作系统实现
IO
多路复用的机制。 我们知道,
I/O
多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),
能够通知程序进行相应的读写操作。那么这三种机制有什么区别呢 ?
1
、支持一个进程所能打开的最大连接数
3
、 消息传递方式
总结:
综上,在选择
select
,
poll
,
epoll
时要根据具体的使用场合以及这三种方式的自身特点。
1
、表面上看
epoll
的性能最好,但是在连接数少并且连接都十分活跃的情况下,
select 和 poll
的性能可能比
epoll
好,毕竟
epoll
的通知机制需要很多函数回调。
2
、
select
低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善。
什么是水平触发
(LT)
和边缘触发
(ET)
?
Level_triggered(
水平触发
)
:当被监控的文件描述符上有可读写事件发生时,
epoll_wait() 会通知处理程序去读写。如果这次没有把数据一次性全部读写完(
如读写缓冲区太小
)
,那么
下次调用
epoll_wait()
时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你!如果系统中有大量你不需要读写的就绪文件描述符,
而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率!
Edge_triggered(
边缘触发
)
:当被监控的文件描述符上有可读写事件发生时,
epoll_wait() 会通知处理程序去读写。如果这次没有把数据全部读写完(
如读写缓冲区太小
)
,那么下次调
用
epoll_wait()
时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第 二次可读写事件才会通知你!这种模式比水平触发效率高,系统不会充斥大量你不关心
的就绪文件描述符!
select(),poll()
模型都是水平触发模式,信号驱动
IO
是边缘触发模式,
epoll()模型即支持水平触发,也支持边缘触发,默认是水平触发。JDK 中的 select 实现是水平触发,而 Netty
提供的
Epoll
的实现中是边缘触发。
请说说
DNS
域名解析的全过程
直接内存深入辨析
在所有的网络通信和应用程序中,每个
TCP
的
Socket
的内核中都有一个发送缓冲区 (SO_SNDBUF)和一个接收缓冲区
(SO_RECVBUF)
,可以使用相关套接字选项来更改该缓冲区大
小。
当某个应用进程调用
write
时,内核从该应用进程的缓冲区中复制所有数据到所写套接 字的发送缓冲区。如果该套接字的发送缓冲区容不下该应用进程的所有数据(
或是应用进程的缓冲区大于套接字的发送缓冲区,或是套接字的发送缓冲区中已有其他数据)
,假设该套接字是阻塞的,则该应用进程将被投入睡眠。内核将不从 write
系统调用返回,直到应用进程缓冲区中的所有数据都复制到套接字发送缓冲区。因此,从写一个 TCP
套接字的
write
调用成功返回仅仅表示我们可以重新使用原来的应用进程缓冲区,并不表明对端的 TCP
或应用进程已接收到数据。
Java
程序自然也要遵守上述的规则。但在
Java
中存在着堆、垃圾回收等特性,所以在 实际的 IO
中,在
JVM
内部的存在着这样一种机制:
在
IO
读写上,如果是使用堆内存,
JDK
会先创建一个
DirectBuffer
,再去执行真正的写操作。这是因为,当我们把一个地址通过 JNI
传递给底层的
C
库的时候,有一个基本的要求, 就是这个地址上的内容不能失效。然而,在 GC
管理下的对象是会在
Java
堆中移动的。也就是说,有可能我把一个地址传给底层的 write
,但是这段内存却因为
GC
整理内存而失效了。 所以必须要把待发送的数据放到一个 GC
管不着的地方。这就是调用
native
方法之前,数据—定要在堆外内存的原因。 可见,DirectBuffer
并没有节省什么内存拷贝,只是因为
HeapBuffer
必须多做一次拷贝,使用 DirectBuffer
就会少一次内存拷贝。相比没有使用堆内存的
Java
程序,使用直接内存的Java 程序当然更快一点。从垃圾回收的角度而言,直接内存不受 GC(
新生代的
Minor GC)
影响,只有当执行老年代的 Full GC
时候才会顺便回收直接内存,整理内存的压力也比数据放到
HeapBuffer
要小。
堆外内存的优点和缺点
堆外内存相比于堆内内存有几个优势:
1
减少了垃圾回收的工作,因为垃圾回收会暂停其他的工作(可能使用多线程或者时间片的方式,根本感觉不到)
2
加快了复制的速度。因为堆内在
flush
到远程时,会先复制到直接内存(非堆内存), 然后在发送;而堆外内存相当于省略掉了这个工作。
而福之祸所依,自然也有不好的一面:
1
堆外内存难以控制,如果内存泄漏,那么很难排查。
2
堆外内存相对来说,不适合存储很复杂的对象。一般简单的对象或者扁平化的比较适合。
零拷贝
什么是零拷贝
?
零拷贝
(
英语
: Zero-copy)
技术是指计算机执行操作时,
CPU
不需要先将数据从某处内存 复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省 CPU
周期和内存带宽。
➢
零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率。
➢
零拷贝技术减少了用户进程地址空间和内核地址空间之间因为上
:
下文切换而带来的开销。
可以看出没有说不需要拷贝,只是说减少冗余
[
不必要
]
的拷贝。
下面这些组件、框架中均使用了零拷贝技术:
Kafka
、
Netty
、
Rocketmq
、
Nginx
、
Apache
。
Linux
的
I/O
机制与
DMA
在早期计算机中,用户进程需要读取磁盘数据,需要
CPU
中断和
CPU
参与,因此效率比较低,发起 IO
请求,每次的
IO
中断,都带来
CPU
的上下文切换。因此出现了——
DMA
。
DMA(Direct Memory Access
,直接内存存取
)
是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于 CPU
的大量中断负载。
DMA
控制器,接管了数据读写请求,减少
CPU
的负担。这样一来,
CPU
能高效工作了。 现代硬盘基本都支持 DMA
。
因此
IO
读取,涉及两个过程:
1
、
DMA
等待数据准备好,把磁盘数据读取到操作系统内核缓冲区。
2
、用户进程,将内核缓冲区的数据
copy
到用户空间。
这两个过程,都是阻塞的
传统数据传送机制
比如:读取文件,再用
socket
发送出去,实际经过四次
copy
。
伪码实现如下:
buffer = File.read()
Socket.send(buffer)
1
、第一次:将磁盘文件,读取到操作系统内核缓冲区;
2
、第二次:将内核缓冲区的数据,
copy
到应用程序的
buffer
;
3
、第三步:将
application
应用程序
buffer
中的数据,
copy
到
socket
网络发送缓冲区
(
属于操作系统内核的缓冲区)
;
4
、第四次:将
socket buffer
的数据,
copy
到网卡,由网卡进行网络传输。.
分析上述的过程,虽然引入
DMA
来接管
CPU
的中断请求,但四次
copy
是存在“不必 要的拷贝”的。实际上并不需要第二个和第三个数据副本。应用程序除了缓存数据并将其传 输回套接字缓冲区之外什么都不做。相反,数据可以直接从读缓冲区传输到套接字缓冲区。 显然,第二次和第三次数据 copy 其实在这种场景下没有什么帮助反而带来开销,这也正是零拷贝出现的背景和意义。 同时,read 和
send
都属于系统调用,每次调用都牵涉到两次上下文切换:
总结下,传统的数据传送所消耗的成本:
4
次拷贝,
4
次上下文切换。 4 次拷贝,其中两次是
DMA copy
,两次是
CPU copy
。
Linux
支持的
(
常见
)
零拷贝
目的:减少
IO
流程中不必要的拷贝,当然零拷贝需要
OS
支持,也就是需要
kernel
暴露 api
。
mmap
内存映射
硬盘上文件的位置和应用程序缓冲区
(application buffers)
进行映射(建立一种一一对应关系),由于 mmap()
将文件直接映射到用户空间,所以实际文件读取时根据这个映射关系,
直接将文件从硬盘拷贝到用户空间,只进行了一次数据拷贝,不再有文件内容从硬盘拷贝到内核空间的一个缓冲区。mmap 内存映射将会经历:
3
次拷贝
: 1
次
cpu copy
,
2
次
DMA copy
; 以及 4
次上下文切换
sendfile
linux 2.1
支持的
sendfile
当调用
sendfile()
时,
DMA
将磁盘数据复制到
kernel buffer
,然后将内核中的
kernel buffer 直接拷贝到 socket buffer
;但是数据并未被真正复制到
socket
关联的缓冲区内。取而代之的
是,只有记录数据位置和长度的描述符被加入到
socket
缓冲区中。
DMA
模块将数据直接从内核缓冲区传递给协议引擎,从而消除了遗留的最后一次复制。 一旦数据全都拷贝到 socket buffer
,
sendfile()
系统调用将会
return
、代表数据转化的完成。socket buffer
里的数据就能在网络传输了。 sendfile 会经历:
3
次拷贝,
1
次
CPU copy 2
次
DMA copy
;
以及
2
次上下文切换
splice
Linux
从
2.6.17
支持
splice
数据从磁盘读取到
OS
内核缓冲区后,在内核缓冲区直接可将其转成内核空间其他数据buffer,而不需要拷贝到用户空间。
如下图所示,从磁盘读取到内核
buffer
后,在内核空间直接与
socket buffer
建立
pipe管道。和 sendfile()
不同的是,
splice()
不需要硬件支持。
注意
splice
和
sendfile
的不同,
sendfile
是将磁盘数据加载到
kernel buffer
后,需要一次CPU copy,拷贝到
socket buffer
。而
splice
是更进一步,连这个
CPU copy
也不需要了,直接
将两个内核空间的
buffer
进行
pipe
。 splice 会经历
2
次拷贝
: 0
次
cpu copy 2
次
DMA copy
;
以及
2
次上下文切换
总结
Linux
中零拷贝
最早的零拷贝定义,来源于
Linux 2.4
内核新增
sendfile
系统调用,提供了零拷贝。磁盘数据通过
DMA
拷贝到内核
态
Buffer
后,直接通过
DMA
拷贝到
NIO Buffer(socket buffer)
,无需
CPU
拷贝。这也是零
拷贝这一说法的来源。这是真正操作系统 意义上的零拷贝
(
也就是狭义零拷贝
)
。 但是我们知道,由 OS
内核提供的 操作系统意义上的零拷贝,发展到目前也并没有很多种,也就是这样的零拷贝并不是很多; 随着发展,零拷贝的概念得到了延伸,就是目前的减少不必要的数据拷贝都算作零拷贝的范畴。
Java
生态圈中的零拷贝
Linux
提供的零拷贝技术
Java
并不是全支持,支持
2
种
(
内存映射
mmap
、
sendfile)
;
NIO
提供的内存映射
MappedByteBuffer
NIO
中的
FileChannel.map()
方法其实就是采用了操作系统中的内存映射方式,底层就是调用 Linux mmap()
实现的。 将内核缓冲区的内存和用户缓冲区的内存做了一个地址映射。这种方式适合读取大文件, 同时也能对文件内容进行更改,但是如果其后要通过 SocketChannel 发送,还是需要
CPU
进行数据的拷贝。
NIO
提供的
sendfile
Java NIO
中提供的
FileChannel
拥有
transferTo
和
transferFrom
两个方法,可直接把FileChannel 中的数据拷贝到另外一个
Channel
,或者直接把另外一个
Channel
中的数据拷
贝到
FileChannel
。该接口常被用于高效的网络
/
文件的数据传输和大文件拷贝。在操作系 统支持的情况下,通过该方法传输数据并不需要将源数据从内核态拷贝到用户态,再从用户
态拷贝到目标通道的内核态,同时也避免了两次用户态和内核态间的上下文切换,也即使用了“零拷贝”,所以其性能一般高于 Java IO
中提供的方法。
Kafka
中的零拷贝
Kafka
两个重要过程都使用了零拷贝技术,且都是操作系统层面的狭义零拷贝,一是Producer 生产的数据存到
broker
,二是
Consumer
从
broker
读取数据。
Producer
生产的数据持久化到
broker
,采用
mmap
文件映射,实现顺序的快速写入; Customer 从
broker
读取数据,采用
sendfile
,将磁盘文件读到
OS
内核缓冲区后,直接 转到 socket buffer
进行网络发送。
Netty
的零拷贝实现
Netty
的零拷贝主要包含三个方面:
在网络通信上 :
Netty
的接收和发送
ByteBuffer
采用
DIRECT BUFFERS
,使用堆外直接内存进行 Socket
读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存
(
HEAP BUFFERS)进行
Socket
读写,
JVM
会将堆内存
Buffer
拷贝一份到直接内存中,然后才写入Socket
中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
在缓存操作上 : Netty
提供了
CompositeByteBuf
类,它可以将多个
ByteBuf
合并为一个逻辑上的 ByteBuf
,避免了各个
ByteBuf
之间的拷贝。 通过 wrap
操作,我们可以将
byte[]
数组、
ByteBuf
、
ByteBuffer
等包装成一个
NettyByteBuf 对象,进而避免了拷贝操作。 ByteBuf支持
slice
操作,因此可以将
ByteBuf
分解为多个共享同一个存储区域的
ByteBuf
,避免了内存的拷贝。
在文件传输上 : Netty
的通过
FileRegion
包装的
FileChannel.tranferTo
实现文件传输,它可以直接将文件缓冲区的数据发送到目标 Channel
,避免了传统通过循环
write
方式导致的内存拷贝问题。