最新要闻
- 当前最新:《狂飙》取景地拍照收费摊主已搬离:没给续签合同
- 环球速看:索尼A7M4将发布2.0固件 或下放部分索尼A7R5功能
- 80%的人出错了 你的数码相机用对了吗?
- 全球热讯:爱奇艺奇迹一般的赚钱了!但是 就这它也好意思?
- 快看点丨AMD Zen4锐龙9 7945HX大放异彩!16核心打平Intel 24核心
- 世界今头条!长色斑的原因有哪些_脸上为什么会长色斑
- 环球观焦点:AMD发布23.2.2版驱动:RX 7900显卡小打鸡血 性能提升14%
- 天天最新:大理州5个新能源装备制造项目投产
- 深圳一外卖小哥疑送餐时猝死:曾拼命跑上六楼
- 环球热头条丨向上捅破天 吉利银河支持低轨卫星技术:全球无盲区定位
- 13代酷睿i9+满血4060显卡!华硕天选4正式开售 到手价8999元
- 当前短讯!造车新势力 电动自行车品牌“VELOTRIC”A轮融资:获5000万元
- 中疾控提醒:近期水痘处于高发期 要注意做好防护!
- 13代酷睿+RTX 4060!七彩虹将星X16 Pro图赏
- 19999元起 雷蛇推出2023款灵刃15游戏本:i7+RTX 4060
- 股价大涨7% 阿里发布Q3财报:营收2477.6亿 利润超456亿
手机
iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
家电
每日短讯:干翻 nio ,王炸 io_uring 来了 !!(图解+史上最全)
文章持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版为您奉上珍贵的学习资源 :
(资料图片仅供参考)
免费赠送 :《尼恩Java面试宝典》持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》面试必备 + 大厂必备 +涨薪必备 加尼恩免费领免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》面试必备 + 大厂必备 +涨薪必备 加尼恩免费领免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取
大趋势:全链路异步化,性能提升10倍+
随着业务的发展,微服务应用的流量越来越大,使用到的资源也越来越多。
在微服务架构下,大量的应用都是 SpringCloud 分布式架构,这种架构总体上是全链路同步模式。
全链路同步模式不仅造成了资源的极大浪费,并且在流量发生激增波动的时候,受制于系统资源而无法快速的扩容。
全球后疫情时代,降本增效是大背景。如何降本增效?一条好的路径:全链路同步模式,升级为 全链路异步模式。
全链路异步模式改造 具体的内容,请参考尼恩的深度文章:全链路异步,让你的 SpringCloud 性能优化10倍+
先回顾一下全链路同步模式架构图
全链路同步模式,如何升级为 全链路异步模式, 就是一个一个 环节的异步化。
40岁老架构师尼恩,持续深化自己的3高架构知识宇宙,当然首先要去完成一次牛逼的全链路异步模式 微服务实操,下面是尼恩的实操过程、效果、压测数据(性能足足提升10倍多)。
全链路异步模式改造 具体的内容,请参考尼恩的深度文章:全链路异步,让你的 SpringCloud 性能优化10倍+
并且,上面的文章,作为尼恩 全链路异步的架构知识,收录在《尼恩Java面试宝典》V52版的架构专题中
注:本文以 PDF 持续更新,最新尼恩 架构笔记、面试题 的PDF文件,请从这里获取:码云
全链路异步化的最终目标
全链路异步化的最终目标,如下图所示:
- 应用层:编程模型的异步化
- 框架层:IO线程的异步化
- OS层:IO模型的异步化
一:应用层:编程模型的异步化
这个请大家去看 尼恩的 《响应式 圣经 PDF》电子书
随着 云原生时代的到来, 底层的 组件编程 越来越 响应式、流化, 从命令式 编程转换到 响应式 编程,在非常多的场景 ,是大势所趋。
而响应式编程, 学习曲线很大, 大家需要多看,多实操。
二:框架层:IO线程的异步化
这个大家 都选择 具有异步 回调功能的 异步线程模型,如 Reactor 线程模型
这个是面试的绝对重点
IO的王者组件,Netty框架,整体就是一个 Reactor 线程模型 实现
也是非常核心的知识,这里不做展开,请大家去看尼恩的畅销书《Java 高并发核心编程卷 1 加强版》。
三:OS层:IO模型的异步化
目前的一个最大难题,是IO模型的异步化。
注意,Netty 底层的IO模型,咱们一般用的是select或者 epoll,是同步IO,不是异步IO.
有关5大IO模型,是本文的基础知识,也是非常核心的知识,这里不做展开,请大家去看尼恩的畅销书《Java 高并发核心编程卷 1 加强版》。
第二层:线程模型的异步化
首先来看线程模型的异步化。
Reactor模式
了解了BIO和NIO的一些使用方式,Reactor模式就呼之欲出了。
NIO是基于事件机制的,有一个叫做Selector的选择器,阻塞获取关注的事件列表。获取到事件列表后,可以通过分发器,进行真正的数据操作。
上图是Doug Lea
在讲解NIO时候的一张图,指明了最简单的Reactor模型的基本元素。
你可以对比这上面的NIO代码分析一下,里面有四个主要元素:
- Acceptor 处理client的连接,并绑定具体的事件处理器
- Event 具体发生的事件
- Handler 执行具体事件的处理者。比如处理读写事件
- Reactor 将具体的事件分配给Handler
我们可以对上面的模型进行近一步细化,下面这张图同样是Doug Lea
的ppt中的。
它把Reactor部分分为mainReactor和subReactor两部分。mainReactor负责监听处理新的连接,然后将后续的事件处理交给subReactor,subReactor对事件处理的方式,也由阻塞模式变成了多线程处理,引入了任务队列的模式。
这两个线程模型,非常重要。
一定要背到滚瓜烂熟。
这里不做展开,请大家去看尼恩的畅销书《Java 高并发核心编程卷 1 加强版》。
第三层:OS中IO模型的异步化
目前的一个最大难题,是IO模型的异步化。
注意,Netty 底层的IO模型,咱们一般用的是select或者 epoll,是同步IO,不是异步IO.
首先看看五大IO模型吧:
IO模型层的异步化
- 阻塞式IO (bio)
- 非阻塞式IO
- IO复用 (nio)
- 信号驱动式IO
- 异步IO(aio)
1.阻塞IO模型
如上图,是典型的BIO模型,每当有一个连接到来,经过协调器的处理,就开启一个对应的线程进行接管。
如果连接有1000条,那就需要1000个线程。线程资源是非常昂贵的,除了占用大量的内存,还会占用非常多的CPU调度时间,所以BIO在连接非常多的情况下,效率会变得非常低。
就单个阻塞IO
来说,它的效率并不比NIO
慢。但是当服务的连接增多,考虑到整个服务器的资源调度和资源利用率等因素,NIO
就有了显著的效果,NIO非常适合高并发场景。
2.非阻塞IO模型
其实,在处理IO动作时,有大部分时间是在等待。
比如,socket连接要花费很长时间进行连接操作,在完成连接的这段时间内,它并没有占用额外的系统资源,但它只能阻塞等待在线程中。这种情况下,系统资源并不能被合理的利用。
Java的NIO,在Linux上底层是使用epoll实现的。epoll是一个高性能的多路复用I/O工具,改进了select和poll等工具的一些功能。在网络编程中,对epoll概念的一些理解,几乎是面试中必问的问题。
epoll的数据结构是直接在内核上进行支持的。通过epoll_create和epoll_ctl等函数的操作,可以构造描述符(fd)相关的事件组合(event)。
这里有两个比较重要的概念:
fd
每条连接、每个文件,都对应着一个描述符,比如端口号。内核在定位到这些连接的时候,就是通过fd进行寻址的event
当fd对应的资源,有状态或者数据变动,就会更新epoll_item
结构。在没有事件变更的时候,epoll就阻塞等待,也不会占用系统资源;一旦有新的事件到来,epoll就会被激活,将事件通知到应用方
关于epoll还会有一个面试题:相对于select,epoll有哪些改进?
这里直接给出答案:
- epoll不再需要像select一样对fd集合进行轮询,也不需要在调用时将fd集合在用户态和内核态进行交换
- 应用程序获得就绪fd的事件复杂度,epoll时O(1),select是O(n)
- select最大支持约1024个fd,epoll支持65535个
- select使用轮询模式检测就绪事件,epoll采用通知方式,更加高效
有关5大IO模型,是本文的基础知识,也是非常核心的知识,非常重要
这里不做展开,请大家去看尼恩的畅销书《Java 高并发核心编程卷 1 加强版》。
为啥需要IO模型异步化
这里有一个很大的性能损耗点,同步IO中,线程的切换、 IO事件的轮询、IO操作, 都是需要进行 系统调用完成的。
系统调用的性能耗费在哪里?
首先,线程是很”贵”的资源,主要表现在:
- 线程的创建和销毁成本很高,线程的创建和销毁都需要通过重量级的系统调用去完成。
- 线程本身占用较大内存,像Java的线程的栈内存,一般至少分配512K~1M的空间,如果系统中的线程数过千,整个JVM的内存将被耗用1G。
- 线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。过多的线程频繁切换带来的后果是,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统CPU sy值特别高(超过20%以上)的情况,导致系统几乎陷入不可用的状态。
在Linux的性能指标里,有us
和sy
两个指标,使用top
命令可以很方便的看到。
us
是用户进程的意思,而sy
是在内核中所使用的cpu占比。
如果进程在内核态和用户态切换的非常频繁,那么效率大部分就会浪费在切换之上。一次内核态和用户态切换的时间,普遍在 微秒级别以上,可以说非常昂贵了。
cpu的性能是固定的,在无用的东西上浪费越小,在真正业务上的处理就效率越高。
影响效率的有两个方面:
进程或者线程的数量,引起过多的上下文切换。
进程是由内核来管理和调度的,进程的切换只能发生在内核态。所以,如果你的代码切换了线程,它必然伴随着一次用户态和内核态的切换。
IO的编程模型,引起过多的系统态和内核态切换。
比如同步阻塞等待的模型,需要经过数据接收、软中断的处理(内核态),然后唤醒用户线程(用户态),处理完毕之后再进入等待状态(内核态)。
注意:一次内核态和用户态切换的时间,普遍在 微秒级别以上,可以说非常昂贵了。
IO模型的异步化的第一个目标: 减少线程数量,减少线程切换系统调用带来 CPU 上下文切换的开销。
IO模型的异步化的第一个目标: 减少IO系统调用,减少线程切换系统调用带来的带来 CPU 上下文切换开销。
用户空间内核空间、用户态内核态,又是一组极致复杂的概念。同样是本文的基础知识,也是非常核心的知识,非常重要。这里不做展开,请大家去看尼恩的3 高架构笔记 《高性能之葵花宝典》。
线程模型和IO模型的概念误区
在尼恩的疯狂创客圈社群(50+)中, 经常有人被 IO模型, Reactor反应器模型,同步、异步搞晕。
尼恩用几十年的经验总结,给大家做一个简单梳理:
- 一定要分层,就想 WEB应用架构要分层一样。
- 线程模型和IO模型,要分开来看,不能混为一谈。
很多小伙伴把Reactor 反应器,一定认为底层的IO模型是NIO, 大家去看看Netty源码, Netty反应器,支持各种IO模,包括BIO。
所以,一定要分层去看。
尼恩把线程模型和IO模型的,给大家分为三层: 应用层、框架层、 OS层。
具体如下图所示:
Netty的 Reactor 模式,对应到是:线程模型。不是对应到 IO模型。
在IO模型的层面,Tomcat 也用了 NIO,大家一定不要以为Tomcat还用BIO,还用 ,大部分的HTTPClient客户端组件,都用了NIO,都不会使用BIO模型的。
在线程模型的层面,很多的HTTPClient组件,要么没有使用 Reactor模型,要么是使用了Reactor反应性线程模型,但是我们的业务程序不用,咱们的业务程序,用的还是其同步阻塞线程模型的API代码。
如何进行IO模型的异步化
大家都知道BIO非常的低效,而网络编程中的IO多路复用普遍比较高效。
Linux中,一直没有成熟的异步IO内核组件。
现在,io_uring已经能够挑战NIO的,功能非常强大。
io_uring在2019加入了Linux内核,目前5.1+的内核,可以采用这个功能。
随着一步步的优化,系统调用这个大家伙,调用次数越来越少了。
让我们先看看 linux 中的各种异步 IO,也就是 AIO。
1. glibc aio
官方地址:Perform I/O Operations in Parallel(官方文档用的字眼比较考究)
glibc 是 GNU 发布的 libc 库,该库提供的异步 IO 被称为 glibc aio,在某些地方也被称为 posix aio。glibc aio 用多线程同步 IO 来模拟异步 IO,回调函数在一个单线程中执行。
该实现备受非议,存在一些难以忍受的缺陷和bug,极不推荐使用。详见:http://davmac.org/davpage/linux/async-io.html
2. libaio
linux kernel 2.6 版本引入了原生异步 IO 支持 —— libaio,也被称为 native aio。
ibaio 与 glibc aio 的多线程伪异步不同,它真正的内核异步通知,是真正的异步IO。
虽然很真了,但是缺陷也很明显:libaio 仅支持 O_DIRECT 标志,也就是 Direct I/O,这意味着无法利用系统缓存,同时读写的的大小和偏移要以区块的方式对齐。
3. libeio
由于上面两个都不靠谱,所以 Marc Lehmann 又开发了一个 AIO 库 —— libeio。
与 glibc aio 的思路一样,也是在用户空间用多线程同步模拟异步 IO,但是 libeio 实现的更高效,代码也更稳定,著名的 node.js 早期版本就是用 libev 和 libeio 驱动的(新版本在 libuv 中移除了 libev 和 libeio)。
libeio 提供全套异步文件操作的接口,让用户能写出完全非阻塞的程序,但 libeio 也不属于真正的异步IO。
libeio 项目地址:https://github.com/kindy/libeio
4. io_uring
接下来就是 linux kernel 5.1 版本引入的 io_uring 了。
io_uring 类似于 Windows 世界的 IOCP,但是还没有达到对应的地位,目前来看正式使用 io_uring 的产品基本没有,
目前还是没有一个成熟的基础框架与其匹配,至于 Netty 对 io_uring 的封装,看下来的总体感受是:Netty 为了维持编程模型统一,完全没有发挥出 io_uring 的长处。具体 Netty 是如何封装的,后面会一起探讨一下。
但是在未来,一定是异步IO的天下, 今天,咱们就从io_uring 的学习开始吧。
io_uring (用户环形IO)
前面讲到,NIO依然有大量的系统调用,那就是Epoll的epoll_ctl。
另外,获取到网络事件之后,还需要把socket的数据进行存取,这也是一次系统调用。
虽然相对于BIO来说,上下文切换次数已经减少很多,但它仍然花费了比较多的时间在切换之上。
IO只负责对发生在fd描述符上的事件进行通知。事件的获取和通知部分是非阻塞的,但收到通知之后的操作,却是阻塞的。
即使使用多线程去处理这些事件,它依然是阻塞的。
如果能把这些系统调用都放在操作系统里完成,那么就可以节省下这些系统调用的时间,io_uring就是干这个的。
尼恩提示:这里io_uring娶一个 io_uring 这样名字,非常反人性,
在取名字上面,可以叫做 io_ring,ring_io更合适。u 是user的意思,ring是环形的意思。
一看到这里的ring,很容易知道,这里用了 环形队列。
环形队列是一个高性能的基础结构,大家去看 队列之王Disruptor、缓存之王 Caffeine ,里边用的就是环形队列。
关于环形队列,这里不做展开,请大家去看尼恩的3 高架构笔记 《穿透缓存之王Caffeine 源码和架构》、3 高架构笔记 《穿透队列之王Disruptor源码和架构》。
从io_uring的名字uring
我们就可以看出来,该机制的核心即user
和ring
:其申请了一块用户态和内核态共享的内存作为环形数组,并在共享内存中通过ringBuf
环形队列的方式来实现内核态和用户态的通信,
后文中会出现大量的简写,在这里先做一些介绍。
缩略语 | 英语 | 中文 | 解析 |
---|---|---|---|
SQ | Submission Queue | 提交队列 | 一整块连续的内存空间存储的环形队列。 用于存放将执行操作的数据。 |
CQ | Completion Queue | 完成队列 | 一整块连续的内存空间存储的环形队列。 用于存放完成操作返回的结果。 |
SQE | Submission Queue Entry | 提交队列项 | 提交队列中的一项。 |
CQE | Completion Queue Entry | 完成队列项 | 完成队列中的一项。 |
Ring | Ring | 环 | 比如 SQ Ring,就是“提交队列信息”的意思。 包含队列数据、队列大小、丢失项等等信息。 |
io_uring 的环形队列长成啥样?
闲话少说,这里简单说一下io_uring 的环形队列长成啥样?
前面讲到,io_uring 中,应用程序可以使用两个队列来和 Kernel 进行通信:
- Submission Queue(SQ)
- Completion Queue(CQ) 。
而这两个队列中的保存的主要是指针或者编号(index),真正的IO请求,保存在一个基于数组结构的环形队列中,这个环形队列的结构如下图:
这块内存共分为三个区域,分别是 SQ,CQ,SQEs。
SQEs是一个环形数组,保存实际的IO请求,之所以采用了一个额外数组保存 SQEs,是为了方便通过 RingBuffer 提交内存上不连续的请求。
两个队列 SQ 和 CQ 中每个节点,保存的并不是IO请求,保存的都是 SQEs 数组的偏移量,实际的请求只保存在 SQEs 数组中。一个 SQE 条目的结构,主要包含以下的内容:
- Opcode:描述要进行的系统调用的IO 操作码。如果是读,操作码IORING_OP_READV。
- Flags:修饰符,可以通过任何请求传递
- Fd:要读取的文件描述符
- Address:对于我们的readv调用,它创建了一个缓冲区(或向量)数组来读入数据。因此,address字段包含了该数组的地址。
- Length: Address 缓冲区 向量数组的长度。
- User Data:通常这是一个指针,指向一些结构体,其中保存了请求的元数据,来识别应用的请求。当请求从CQ 队列中出来时,并不能保证IO结果与 请求SQEs的顺序相同。如果一定保证有序的就会降低性能, 就违背了异步API的初衷。因此,我们需要一些东西来识别我们发出的请求。User Data这可以达到这个目的。
CQE包含
- Result:readv系统调用的返回值。如果成功,就会有读取的字节数; 否则它将有一个错误代码。
- User Data:在SQE中传递的指针。
注意:由于 SQ,CQ,SQEs 是在内核中分配的,所以用户态程序并不能直接访问。
应用程序如何和内核进行队列共享呢?
io_setup 的返回值是一个 fd,应用程序使用这个 fd 进行 mmap,和 kernel 共享一块内存。
注意,是应用程序拿到这个 fd 进行 mmap,映射到自己的内存地址。
映射完了之后,根据 offset 偏移量,进行 访问。
而偏移量,和内核的偏移量地址,是相同的。创建 kernel 返回的 io_sqring_offset 和 io_cqring_offset 两个偏移量:
- 返回 io_sqring_offset ,表示 SQ 的指针在 mmap 中的 offset
- 返回 io_cqring_offset ,表示 CQ 的指针在 mmap 中的 offset
这里很关键,用到了文件映射, 共享内存映射,有关文件映射和内存映射的原理和实操,请参见
MappedByteBuffer 详解(图解+秒懂+史上最全) - 疯狂创客圈 - 博客园 (cnblogs.com)
这个知识点,一定要掌握
内核io_uring的三个系统调用
在io_uring在准备阶段,会涉及到三个系统调用:
425 io_uring_setup426 io_uring_enter427 io_uring_register
syscall 1:io_uring_setup 设置
io_uring_setup 需要两个参数,entries 和 io_uring_params。
(1)entries 代表 queue depth。要创建的sqe的数量
(2)param s 代表 用户层指定的参数。
/*entries: 要创建的sqe的数量params: 用户层指定的参数*/static long io_uring_setup(u32 entries, struct io_uring_params __user *params){ struct io_uring_params p; int i; // 把用户空间的params复制到内核空间 if (copy_from_user(&p, params, sizeof(p))) return -EFAULT; // resv是保留的空间,所以不能用 for (i = 0; i < ARRAY_SIZE(p.resv); i++) { if (p.resv[i]) return -EINVAL; } /* flags只支持这些标志,如果有其它标志都会报错 #define IORING_SETUP_IOPOLL(1U << 0)// io poll 模式#define IORING_SETUP_SQPOLL(1U << 1)// sq poll 模式#define IORING_SETUP_SQ_AFF(1U << 2)// 指定线程cpu时指定这个参数#define IORING_SETUP_CQSIZE(1U << 3)// 应用设置完成队列大小#define IORING_SETUP_CLAMP(1U << 4)// 当用户指定的entries太大时,可以把值改小#define IORING_SETUP_ATTACH_WQ(1U << 5)//添加到当前已经存在的wq里#define IORING_SETUP_R_DISABLED(1U << 6)// 如果是sq-poll模式,一开始不启动sq-thread */ if (p.flags & ~(IORING_SETUP_IOPOLL | IORING_SETUP_SQPOLL | IORING_SETUP_SQ_AFF | IORING_SETUP_CQSIZE | IORING_SETUP_CLAMP | IORING_SETUP_ATTACH_WQ | IORING_SETUP_R_DISABLED)) return -EINVAL; return io_uring_create(entries, &p, params);}
io_uring_params 的定义如下。
struct io_uring_params {__u32 sq_entries;__u32 cq_entries;__u32 flags;__u32 sq_thread_cpu;__u32 sq_thread_idle;__u32 resv[5];struct io_sqring_offsets sq_off;struct io_cqring_offsets cq_off;};struct io_sqring_offsets {__u32 head;__u32 tail;__u32 ring_mask;__u32 ring_entries;__u32 flags;__u32 dropped;__u32 array;__u32 resv1;__u64 resv2;};struct io_cqring_offsets {__u32 head;__u32 tail;__u32 ring_mask;__u32 ring_entries;__u32 overflow;__u32 cqes;__u64 resv[2];};
io_uring_params 参数包括两种:
- 输入参数
- 输出参数
其中:
- flags、sq_thread_cpu、sq_thread_idle 属于输入参数,由应用负责设置,用于定义 io_uring 在内核中的行为。
- 其他参数属于输出参数,由内核负责设置。
syscall 2:io_uring_create
static int io_uring_create(unsigned entries, struct io_uring_params *p, struct io_uring_params __user *params){struct user_struct *user = NULL;struct io_ring_ctx *ctx;struct file *file;bool limit_mem;int ret; ....省略几万字 // 调用trace接口trace_io_uring_create(ret, ctx, p->sq_entries, p->cq_entries, p->flags);return ret;err:io_disable_sqo_submit(ctx);io_ring_ctx_wait_and_kill(ctx);return ret;}
io_uring_create是setup的主流程:
- 计算sq_entries, cq_entries的大小
- 分配 一个 io_ring_ctx 上下文 对象, 这是io_uring运行过程的上下文
- 分配 sqe, cqe这些数组空间
- 如果是sq-poll模式则创建内核线程
- 创建io_wq 对象及相应的worker
- 如果是sq-poll, 且需要启动线程 , 则启动之
- 把sq, cq的一些信息写到用户空间的params里, 这些信息用来在setup成功后, 映射内核内存
- 创建io_uring 对应的文件及socket, 这个文件的fd用来与用户空间通信 , 这个 fd,是一个匿名fd
重点提一下匿名 fd 的事情,为什么会有匿名 fd ? 什么是匿名?
在 Linux 里一切皆文件,你理解的常见“文件”有什么特性?
文件的名称是path 路径,匿名的意思说的就是没有路径。匿名 fd 其实说的是匿名 inode 。
在 Linux 的文件体系中,一个文件句柄,对应一个 file 结构体,关联一个 inode 。
file/dentry/inode
这三驾马车是一定要配齐的,就算是匿名的(无 path,无效 dentry ),对于 file 结构体来说,一定要绑定 inode 和 dentry ,哪怕是伪造的、不完整的 inode。anon_inodefs 就应运而生了,内核就帮你搞出来一个公共的 inode
这就节省了所有有这样需求的内核模块,避免了内存的浪费,省了冗余重复的 inode 初始化代码。
匿名 fd 背后的是一个叫做 anon_inodefs 的内核文件系统( 位于
fs/anon_inodes.c
),这个文件系统极其简单,整个文件系统只有一个 inode ,这个 inode 是文件系统初始化的时候创建好的。
之后,所有需要一个匿名 inode 的句柄都直接跟这个 inode 关联即可。
syscall 3: io_uring_register
涉及的文件描述符的引用操作,比较低性能:
- 应用 每次将 文件描述符填充到 sqe ,然后提交给内核时,内核都必须检索对 文件描述符 的引用,也是低性能的
- 当 IO 完成后,会再次删除文件引用,由于文件引用要保障的原子性,也是低性能的
这样对高 IOPS 的工作场景而言,速度会明显下降。
为了缓解此问题,io_uring 提供了一种对 io_uring 实例预注册文件集的方法
int io_uring_register(unsigned int fd, unsigned int opcode, void *arg, unsigned int nr_args);
fd
是io_uring 实例
的文件描述符opcode
执行的注册类型。
对于注册文件集来说,必须是 IORING_REGISTER_FILES
。
arg
必须指向应用准备打开的文件描述符数组nr_args
便是数组的大小
一旦 io_uring_register 成功将文件集注册后,应用就可以将文件集数组的索引(而不是使用实际的文件描述符)赋值给 sqe->fd 了,并设置 sqe->flags 字段为 IOSQE_FIXED_FILE 来标记 sqe->fd 是一个文件集索引
应用可以继续使用未注册的文件,即使是注册过的文件也可以通过文件描述符
赋值 sqe->fd
,sqe->flags
不设置 IO_FIXED_FILE
来正常使用文件描述符
当 io_uring 实例被移除后,注册的文件集会自动释放,或者使用 IORING_UNREGISTER_FILES opcode 来调用 io_uring_register
梳理一下io_uring的核心流程
在 io_setup 设置的时候,内核会初始化两个队列 SQ 和 CQ 和一个数组 SQEs ( Submission Queue Entries)
如图, 每一个io_uring
实例,都会被分配一个fd
,该过程是通过io_uring_setup()
系统调用实现的。
io_uring_setup()
调用会根据用户提供的参数,分配一块共享内存。
这块共享内存中,包含了一个SQ
(提交队列)、一个CQ
(完成队列)和一个SQE
(提交实体)数组。
其中,SQ
和CQ
是两个环形队列,队列中的元素是SQE
在SQE
数组中的偏移量,使用这种方式可以使得提交实体能够被随机访问,提高灵活性。
io_uring_setup()
调用返回的fd,该内存可以通过mmap()
的方式映射到用户态
用户从CQ
的头部获取SEQ
,将想要执行的操作(如文件的读写)初始化到其中,并添加到SQ
队列的尾部,然后使用io_uring_enter()
系统调用来进行提交队列的处理。
用户态和内核态共享 提交队列(submission queue)和 完成队列(completion queue),这两条队列通过mmap共享,高效且安全。
提交队列(SQ)给内核源源不断的布置任务,然后从另外一条队列完成队列(CQ)获取结果;
内核则按需进行 epoll(),并在一个线程池中执行就绪的任务。
用户态支持Polling模式,不会发生中断,也就没有系统调用,通过轮询即可消费事件;
内核态也支持Polling模式,同样不会发生上下文切换。
可以看出关键的设计在于,内核通过一块和用户共享的内存区域进行消息的传递,可以绕过Linux 的 syscall 机制。
内核会从SQ
中依次取出对应的io request 提交实体,并根据io request 提交实体中定义的动作来执行对应的操作。由于用户只操作SQ
尾部,而内核只操作头部,因此两者对于共享队列的访问并不会产生冲突,节省了锁的开销。
内核侧的主要操作流程如下:
上图中为内核的处理流程简图,为了提高性能、降低时延,内核并不是一定会采用异步的方式来处理提交实体,而是会检查该实体所对应的文件系统是否支持非阻塞式的操作。
在操作完成后,内核会将完成了的提交实体放到CQ
队列的尾部,方便用户继续进行操作的提交。通过ringBuf
的使用,io_uring
获得了以下几点收益:
- 能够以批量的方式进行IO的提交,减少了系统调用的次数,节省了开销;
- 通过共享内存的使用,避免了用户态与内核态频繁的系统调用参数拷贝,提升了性能。
io_uring 三种工作模式
中断驱动模式
默认模式。
可通过系统调用 io_uring_enter() 提交IO请求,然后检查CQ状态判断是否完成
轮询模式 / poll 模式。
需要文件系统和块设备支持。相比中断驱动,延迟更低,但可能会消耗更多CPU资源
内核轮询模式 / 提交
sqpoll
轮询模式。创建内核线程执行SQ轮询。
当前应用更新 SQ ring 并填充一个新的 sqe,内核线程 sqthread 会自动完成提交,这样应用无需每次调用 io_uring_enter() 系统调用来提交 IO。
应用可通过
IORING_SETUP_SQ_AFF
和 sq_thread_cpu 绑定特定的 CPU。同时,为了节省无 IO 场景的 CPU 开销,该内核线程会在一段时间空闲后自动睡眠。
应用在下发新的 IO 时,通过
IORING_ENTER_SQ_WAKEUP
唤醒该内核线程,用户态可以通过 sqring 的 flags 变量获取 SQ 线程的状态。
中断驱动模式
常规的块设备IO使用的都是中断模式,即进程将IO请求提交给块设备后会进入睡眠(D)状态,块设备在处理完IO请求后会触发硬中断,硬中断中会唤醒进程并通知其IO的完成。
轮询模式 / poll 模式
什么是IO轮询(poll)模式?
轮询模式是相对于中断模式的。io_uring
提供了一种block
层的轮询模式,即IO请求提交后不进入睡眠,而是循环检查硬件设备的完成状态。
该模式下,io_uring
会额外启动一个内核进程来循环检查IO的完成。
由于不需要等待硬件设备的通知,因此可以更快地获取到IO请求的完成,这对于延迟非常低以及IOPS
很高的设备,能够显著提高性能,同时避免了高频的中断所带来的性能开销。
内核轮询模式 / 提交sqpoll
轮询模式
通过ringBuf
的使用,我们现在可以批量地进行IO操作的提交,降低了系统调用次数。
io_uring
还提供了另一种机制用于进一步降低系统调用次数、提高IO效率,即:提交队列轮询SQPOLL
模式。
这个功能让采用内核线程 Polling 的模式收割用户的请求。
当没有使用 SQ 线程时,io_uring_enter 函数会主动的 Poll,以检查提交给 应用层的请求是否已经完成,而不是挂起,并等待 Block 层完成后再被唤醒。
使用 SQ 线程时也是同理。
该模式下,内核会启动一个内核进程专门用于SQE
提交实体的处理,该进程会循环检查提交队列中是否存在实体。
用户态程序只需要取出完成队列中的SEQ
,进行初始化并添加到提交队列中即可,整个过程都不需要产生系统调用。
为了降低开销,内核进程会有一个超时时间,在该时间段内如果都没有检测到提交队列中存在实体,就会进入睡眠状态,同时将进程的状态更新到共享内存中。
用户进程在提交SQE
之后,会通过IORING_SETUP_SQPOLL 标志位检查poll
进程是否在运行。
若未运行,则通过io_uring_enter
系统调用唤醒poll
进程。
可以看出,在高IO频率的情况下,使用该模式可以大幅降低系统调用的次数,同时减少由于系统调用而带来的IO延迟。
图解:io_uring 用户侧+内核侧的完整执行流程
- 用户侧提交IO请求
- 应用创建SQ entries(SQE),更新SQ tail
- 内核消费SQE,更新SQ head
- 内核侧完成
- 内核为完成的一个或多个请求创建CQ entries(CQE),更新CQ tail
- 用户侧收割结果
- 应用消费CQE,更新CQ head,消费CQE无需切换到内核态
IO 提交
IO 提交的做法是找到一个空闲的 SQE,根据请求设置 SQE,并将这个 SQE 的索引放到 SQ 中。
SQ 是一个典型的 RingBuffer,有 head,tail 两个成员,如果 head == tail,意味着队列为空。
SQE 设置完成后,需要修改 SQ 的 tail,以表示向 RingBuffer 中插入一个请求。
当所有请求都加入 SQ 后,就可以使用下面的方法来提交 IO 请求 :
int io_uring_enter(unsigned int fd, u32 to_submit, u32 min_complete, u32 flags);
io_uring_enter 被调用后, 进程会陷入到内核,这里存在着CPU上下文切换。
- to_submit 表示一次提交多少个 IO。
- 如果 flags 设置了 IORING_ENTER_GETEVENTS,并且 min_complete > 0,那么这个系统调用会同时处理 IO 收割。
- min_complete 是最少的完成数量,这个系统调用会一直 block,直到 min_complete 个 IO 已经完成。
这里和epoll类似,IO 提交的过程中依然会产生系统调用。
不过不急, io_uring有三种模式,这里只能算第一种。
在第三种模式中,如果在调用 io_uring_setup 时设置了 IORING_SETUP_SQPOLL 的 flag,内核会额外启动一个内核线程,我们称作 SQ 线程。
这个内核线程可以运行在某个指定的 core 上(通过 sq_thread_cpu 配置)。
这个内核线程会不停的 Poll SQ (轮询),除非在一段时间内没有 Poll 到任何请求(通过 sq_thread_idle 配置),才会被挂起。
当程序在用户态设置完 SQE,并通过修改 SQ 的 tail 完成一次插入时,如果此时 SQ 线程处于唤醒状态,那么可以立刻捕获到这次提交,这样就避免了用户程序调用 io_uring_enter 这个系统调用。
如果 SQ 线程处于休眠状态,则需要通过调用 io_uring_enter,并使用 IORING_SQ_NEED_WAKEUP 参数,来唤醒 SQ 线程。
如何知道 SQ 线程处于休眠状态 呢? 用户态可以通过 sqring 的 flags 变量获取 SQ 线程的状态。
接下来以图的方式,介绍 io_uring
的内核和应用交互方式,具体如下:
提交任务的过程如下:
- 将 SQE 写入 SQEs 区域,
- 将 SQE 的 index (编号,或者类似数组下标)写入 SQ。
- 更新用户态记录的队头。
- 如果有多个任务需要同时提交,用户不断重复上面的过程。
- 将最终的队头编号写入与内核共享的
io_uring
上下文。
用户侧IO 收割
接下来我们简要介绍内核获取任务、内核完成任务、用户收割任务的过程。
当 IO 完成时,内核负责将完成 IO 在 SQEs 中的 index 放到 CQ 中。
- 内核态获取任务的方式是,从队尾读取 SQE,并更新
io_uring
ctx 上下文的 SQ tail。 - 内核态完成任务:往 CQ 中写入 CQE,更新上下文 CQ head。
- 用户态收割任务:从 CQ 中读取 CQE,更新上下文 CQ tail。
由于 IO 在提交的时候可以顺便返回完成的 IO,所以收割 IO 不需要额外系统调用。
这是跟 IO提交有比较大的不同,省去了一次系统调用。
当然,如果使用了 IORING_SETUP_SQPOLL 参数,IO 收割也不需要系统调用的参与。
由于内核和用户态共享内存,所以收割的时候,用户态遍历 [cq->head, cq->tail) 区间,这是已经完成的 IO 队列,然后找到相应的 CQE 并进行处理,最后移动 head 指针到 tail,IO 收割就到此结束了。
所以在最理想的情况下,IO 提交和收割都不需要使用系统调用。
内存可见性和有序性保证:
由于提交和收割的时候需要访问共享内存的 head,tail 指针,所以需要使用 rmb/wmb 内存屏障操作确保时序。
io_uring 与 epoll 的使用对比
epoll 通常的编程模型如下:
struct epoll_event ev; /* for accept(2) */ ev.events = EPOLLIN; ev.data.fd = sock_listen_fd; epoll_ctl(epollfd, EPOLL_CTL_ADD, sock_listen_fd, &ev); /* for recv(2) */ ev.events = EPOLLIN | EPOLLET; ev.data.fd = sock_conn_fd; epoll_ctl(epollfd, EPOLL_CTL_ADD, sock_conn_fd, &ev); 然后在一个主循环中: new_events = epoll_wait(epollfd, events, MAX_EVENTS, -1); for (i = 0; i < new_events; ++i) { /* process every events */ ... }
epoll本质上是实现类似如下事件驱动结构:
struct event { int fd; handler_t handler; };
将fd通过epoll_ctl进行注册,当该fd上有事件ready, 在epoll_wait返回时可以获知完成的事件,然后依次调用每个事件的handler, 每个handler里调用recv(2), send(2)等进行消息收发。
io_uring的编程模型如下(这里用到了liburing提供的一些接口):
/* 用sqe对一次recv操作进行描述 */ struct io_uring_sqe *sqe = io_uring_get_sqe(ring); io_uring_prep_recv(sqe, fd, bufs[fd], size, 0); /* 提交该sqe, 也就是提交recv操作 */ io_uring_submit(&ring); /* 等待完成的事件 */ io_uring_submit_and_wait(&ring, 1); cqe_count = io_uring_peek_batch_cqe(&ring, cqes, sizeof(cqes) / sizeof(cqes[0])); for (i = 0; i < cqe_count; ++i) { struct io_uring_cqe *cqe = cqes[i]; /* 依次处理reap每一个io请求,然后可以调用请求对应的handler */ ... }
Netty 对 io_uring 的封装
3个(NativeTransports)本地传输
Netty提供了三种特定于平台的JNI(Native Transports)本地传输:
- epoll on Linux
- io_uring on Linux (Incubator)
- kqueue on MacOS/BSD
如果适当的库在其运行时可用,则Lettuce默认为本机传输。
与基于NIO的传输相比,使用本机传输会添加特定于特定平台的功能,产生更少的垃圾,并通常会提高性能。
通过Unix域套接字连接本机传输是必需的,并且也适用于TCP连接。
本机传输可用于:
最低Netty版本为4.0.26.Final
的Linux epoll x86_64系统,需要netty-transport-native-epoll
,分类器linux-x86_64
io.netty netty-transport-native-epoll ${netty-version} linux-x86_64
Linux io_uring x86_64系统的最低Netty版本为4.1.54.Final,需要netty-incubator-transport-native-io_uring,分类器为linux-x86_64。
- 请注意,此传输仍处于实验阶段。
io.netty.incubator netty-incubator-transport-native-io_uring 0.0.1.Final linux-x86_64
最低Netty版本为4.1.11.Final
的MacOS kqueue x86_64系统,需要netty-transport-native-kqueue
,分类器osx-x86_64
io.netty netty-transport-native-kqueue ${netty-version} osx-x86_64
你可以通过系统属性禁用本机传输。
将io.lettuce.core.epoll
, io.lettuce.core.iouring
设置为false
(如果未设置,则默认为true
)。
通过Netty使用io_uring
是通过 《Java高并发核心编程 卷1 加强版》随书源码改的,改动没有超过 5行, 没有超过5行
参考的代码如下:
package com.crazymakercircle.imServer.server;import com.crazymakercircle.im.common.codec.SimpleProtobufDecoder;import com.crazymakercircle.im.common.codec.SimpleProtobufEncoder;import com.crazymakercircle.imServer.handler.NettyEchoServerHandler;import io.netty.bootstrap.ServerBootstrap;import io.netty.buffer.PooledByteBufAllocator;import io.netty.channel.*;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioServerSocketChannel;import io.netty.handler.logging.LogLevel;import io.netty.handler.logging.LoggingHandler;import io.netty.incubator.channel.uring.IOUringEventLoopGroup;import io.netty.incubator.channel.uring.IOUringServerSocketChannel;import lombok.Data;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;import java.net.InetSocketAddress;@Data@Slf4j@Service("EchoIOUringServer")public class EchoIOUringServer { // 服务器端口 @Value("${server.port}") private int port; // 通过nio方式来接收连接和处理连接 private EventLoopGroup bg; private EventLoopGroup wg; // 启动引导器 private ServerBootstrap b = new ServerBootstrap(); public void run() { //连接监听线程组 bg = new IOUringEventLoopGroup(1); //传输处理线程组 wg = new IOUringEventLoopGroup(1); try { //1 设置reactor 线程 b.group(bg, wg); //2 设置nio类型的channel b.channel(IOUringServerSocketChannel.class); //3 设置监听端口 b.localAddress(new InetSocketAddress(port)); //4 设置通道选项 // b.option(ChannelOption.SO_KEEPALIVE, true); b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); b.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); //5 装配流水线 b.childHandler(new ChannelInitializer() { //有连接到达时会创建一个channel protected void initChannel(SocketChannel ch) throws Exception { // 管理pipeline中的Handler ch.pipeline().addLast(NettyEchoServerHandler.INSTANCE); } }); // 6 开始绑定server // 通过调用sync同步方法阻塞直到绑定成功 ChannelFuture channelFuture = b.bind().sync(); log.info( "疯狂创客圈 EchoIOUringServer 服务启动, 端口 " + channelFuture.channel().localAddress()); // 7 监听通道关闭事件 // 应用程序会一直等待,直到channel关闭 ChannelFuture closeFuture = channelFuture.channel().closeFuture(); closeFuture.sync(); } catch (Exception e) { e.printStackTrace(); } finally { // 8 优雅关闭EventLoopGroup, // 释放掉所有资源包括创建的线程 wg.shutdownGracefully(); bg.shutdownGracefully(); } }}
从 Netty 官方给的这个例子来看,io_uring 的使用方式与 epoll 一样,初步来看线程模型也是一样的,
也是分了 bossGroup 和 workerGroup 两个EventLoopGroup,
从名字猜测 bossGroup 还是处理连接创建,workerGroup 还是处理网络读写。
io_uring 的具体逻辑都封装在了 IOUringEventLoopGroup 和 IOUringServerSocketChannel 中。
Netty源码 IOUringEventLoopGroup
Netty 的线程模型是面试的核心重点,也比较复杂,此处不再赘述,详见《Java高并发核心编程 卷1 加强版》第四章,有太多小伙伴通过此章掌握了Netty 的线程模式。
我们先看一下 IOUringEventLoop 构造方法:
IOUringEventLoop(IOUringEventLoopGroup parent, Executor executor, int ringSize, int iosqeAsyncThreshold, RejectedExecutionHandler rejectedExecutionHandler, EventLoopTaskQueueFactory queueFactory) { super(parent, executor, false, newTaskQueue(queueFactory), newTaskQueue(queueFactory), rejectedExecutionHandler); // Ensure that we load all native bits as otherwise it may fail when try to use native methods in IovArray IOUring.ensureAvailability(); ringBuffer = Native.createRingBuffer(ringSize, iosqeAsyncThreshold); eventfd = Native.newBlockingEventFd(); logger.trace("New EventLoop: {}", this.toString());}
可见每个事件循环处理线程都创建了一个 io_uring ringBuffer,另外还有一个用来通知事件的文件描述符 eventfd。
深入 Native.createRingBuffer(ringSize, iosqeAsyncThreshold) 看一下:
ringSize 默认值为 4096,iosqeAsyncThreshold 默认为 25
Netty 的这个 RingBuffer 封装基本上与 io_uring 的结构一一对应。
再深入看一下 io_uring_setup 的 JNI 封装,发现 Netty 当前的实现并没设置任何 flag,使用默认 中断模式,也就是通过 io_uring_enter 提交任务。
在实现层面,该模式倒是与 Netty 的线程模型很匹配,如果要支持 SQPOLL 模式,Netty的源码架构, 可能需要较大改动。
回过头来再看一下 IOUringEventLoop 的事件循环:
@Overrideprotected void run() { final IOUringCompletionQueue completionQueue = ringBuffer.ioUringCompletionQueue(); final IOUringSubmissionQueue submissionQueue = ringBuffer.ioUringSubmissionQueue(); // Lets add the eventfd related events before starting to do any real work. addEventFdRead(submissionQueue); for (;;) { try { logger.trace("Run IOUringEventLoop {}", this); // Prepare to block wait long curDeadlineNanos = nextScheduledTaskDeadlineNanos(); if (curDeadlineNanos == -1L) { curDeadlineNanos = NONE; // nothing on the calendar } nextWakeupNanos.set(curDeadlineNanos); // Only submit a timeout if there are no tasks to process and do a blocking operation // on the completionQueue. try { if (!hasTasks()) { if (curDeadlineNanos != prevDeadlineNanos) { prevDeadlineNanos = curDeadlineNanos; submissionQueue.addTimeout(deadlineToDelayNanos(curDeadlineNanos), (short) 0); } // Check there were any completion events to process if (!completionQueue.hasCompletions()) { // Block if there is nothing to process after this try again to call process(....) logger.trace("submitAndWait {}", this); submissionQueue.submitAndWait(); } } } finally { if (nextWakeupNanos.get() == AWAKE || nextWakeupNanos.getAndSet(AWAKE) == AWAKE) { pendingWakeup = true; } } } catch (Throwable t) { handleLoopException(t); } // Avoid blocking for as long as possible - loop until available work exhausted boolean maybeMoreWork = true; do { try { // CQE processing can produce tasks, and new CQEs could arrive while // processing tasks. So run both on every iteration and break when // they both report that nothing was done (| means always run both). maybeMoreWork = completionQueue.process(this) != 0 | runAllTasks(); } catch (Throwable t) { handleLoopException(t); } // Always handle shutdown even if the loop processing threw an exception try { if (isShuttingDown()) { closeAll(); if (confirmShutdown()) { return; } if (!maybeMoreWork) { maybeMoreWork = hasTasks() || completionQueue.hasCompletions(); } } } catch (Throwable t) { handleLoopException(t); } } while (maybeMoreWork); }}
先交代两个非主干逻辑的细节:
- addEventFdRead(submissionQueue) 将 eventfd 的读操作提交 io_uring,其作用主要用于唤醒事件循环线程。由于 submissionQueue.submitAndWait() 这一步是阻塞的,想要唤醒事件循环,向 eventfd 执行一个写操作即可。
- submissionQueue.addTimeout(deadlineToDelayNanos(curDeadlineNanos), (short) 0) 用于处理延迟执行的任务,可以暂且忽略。
搞清楚上述两个细节,主干流程就很清晰了:
- submissionQueue.submitAndWait() 提交任务,等待至少一个任务完成;
- completionQueue.process(callback) 处理已经完成的任务,回调方法也就是 void handle(int fd, int res, int flags, byte op, short data);
- 最后就是向 submissionQueue 添加任务。原来的epoll 模型是,epoll_wait 等待就绪事件,然后执行相关的 IO 系统调用;
Netty 当前的实现并没为 io_uring 设置任何 flag,使用默认 中断模式, 没有使用 内核轮询模式,
前面的三种模式的介绍到: 中断模式是性能最差的一种。
可见,Netty 要努力迭代呀。
作为 IO之王, 大家可以通过尼恩对Netty源码的解读发现,可谓金碧辉煌、编程界的世界屋脊,
尼恩相信,Netty这种的王者组件,一定会在 aio这块提交出一份顶级的代码。
这一天,一定不会太远。
总结
可以看到,io_uring 是完全为性能而生的新一代 native async IO 模型。
通过全新的设计,共享内存,IO 过程不需要系统调用,由内核完成 IO 的提交, 以及 IO completion polling 机制,实现了高IOPS,高 Bandwidth。
注:本文以 PDF 持续更新,最新尼恩 架构笔记、面试题 的PDF文件,请从这里获取:码云
参考文献
https://blog.csdn.net/BUG_zhentan/article/details/119538429
https://zhuanlan.zhihu.com/p/62682475
https://zhuanlan.zhihu.com/p/400927380
https://blog.csdn.net/u012549626/article/details/111520493
https://blog.csdn.net/qq_17045267/article/details/117953632
https://www.skyzh.dev/posts/articles/2021-06-14-deep-dive-io-uring/
推荐阅读:
《SpringCloud+Dubbo3 = 王炸 !》
《响应式圣经:10W字,实现Spring响应式编程自由》
《4次迭代,让我的 Client 优化 100倍!泄漏一个 人人可用的极品方案!》
《100亿级订单怎么调度,来一个大厂的极品方案》
《Linux命令大全:2W多字,一次实现Linux自由》
《阿里一面:谈一下你对DDD的理解?2W字,帮你实现DDD自由》
《阿里一面:你做过哪些代码优化?来一个人人可以用的极品案例》
《网易二面:CPU狂飙900%,该怎么处理?》
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
《场景题:假设10W人突访,你的系统如何做到不 雪崩?》
《2个大厂 100亿级 超大流量 红包 架构方案》
《Nginx面试题(史上最全 + 持续更新)》
《K8S面试题(史上最全 + 持续更新)》
《操作系统面试题(史上最全、持续更新)》
《Docker面试题(史上最全 + 持续更新)》
《Springcloud gateway 底层原理、核心实战 (史上最全)》
《Flux、Mono、Reactor 实战(史上最全)》
《sentinel (史上最全)》
《Nacos (史上最全)》
《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》
《clickhouse 超底层原理 + 高可用实操 (史上最全)》
《nacos高可用(图解+秒懂+史上最全)》
《队列之王: Disruptor 原理、架构、源码 一文穿透》
《环形队列、 条带环形队列 Striped-RingBuffer (史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》
《红黑树( 图解 + 秒懂 + 史上最全)》
《分布式事务 (秒懂)》
《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》
《缓存之王:Caffeine 的使用(史上最全)》
《Docker原理(图解+秒懂+史上最全)》
《Redis分布式锁(图解 - 秒懂 - 史上最全)》
《Zookeeper 分布式锁 - 图解 - 秒懂》
《Netty 粘包 拆包 | 史上最全解读》
《Netty 100万级高并发服务器配置》
-
全球快看:CPU推理|使用英特尔 Sapphire Rapids 加速 PyTorch Transformers
在最近的一篇文章中,我们介绍了代号为SapphireRapids的第四代英特尔至强CPU及其新的先进矩阵扩展(AMX)...
来源: 每日短讯:干翻 nio ,王炸 io_uring 来了 !!(图解+史上最全)
全球快看:CPU推理|使用英特尔 Sapphire Rapids 加速 PyTorch Transformers
世界视点!死锁面试题
18、实体类对象比对-JSON
产品经理,项目经理,FTO
当前最新:《狂飙》取景地拍照收费摊主已搬离:没给续签合同
环球速看:索尼A7M4将发布2.0固件 或下放部分索尼A7R5功能
80%的人出错了 你的数码相机用对了吗?
全球热讯:爱奇艺奇迹一般的赚钱了!但是 就这它也好意思?
快看点丨AMD Zen4锐龙9 7945HX大放异彩!16核心打平Intel 24核心
世界今头条!长色斑的原因有哪些_脸上为什么会长色斑
热门:007 - 研究
每日信息:47.多态
第二章 物理层
day02-自己实现Mybatis底层机制-01
世界快资讯:CSS背景设置与Emmet语法
环球观焦点:AMD发布23.2.2版驱动:RX 7900显卡小打鸡血 性能提升14%
天天最新:大理州5个新能源装备制造项目投产
深圳一外卖小哥疑送餐时猝死:曾拼命跑上六楼
环球热头条丨向上捅破天 吉利银河支持低轨卫星技术:全球无盲区定位
13代酷睿i9+满血4060显卡!华硕天选4正式开售 到手价8999元
当前短讯!造车新势力 电动自行车品牌“VELOTRIC”A轮融资:获5000万元
中疾控提醒:近期水痘处于高发期 要注意做好防护!
13代酷睿+RTX 4060!七彩虹将星X16 Pro图赏
19999元起 雷蛇推出2023款灵刃15游戏本:i7+RTX 4060
股价大涨7% 阿里发布Q3财报:营收2477.6亿 利润超456亿
全球快报:第八章 从源文件到可执行文件
天天通讯!《黑豹》《蚁人》皆扑街 漫威超级英雄为何在国内失宠?
男子一脚急刹车醒来直接四肢瘫痪 医生提醒:车祸受伤后别随意拖拽
焦点快看:系列首次支持:三星Galaxy Tab S9平板终于支持IP67防水
每日热文:三星S23首销:仅重168g的骁龙8 Gen2旗舰 屏幕比小米13更小
涨价2000只是开胃菜!销售:特斯拉还要涨价
全球今热点:电工化身“Tony”,为留守老人解决“头”等大事
环球聚焦:邓超谈《中国乒乓》排片少:大家的不容易我非常理解
今亮点!摩根大通已限制员工使用ChatGPT:数据安全更重要
世界视讯!我国第三款国产ECMO产品获批上市:性能达到国际水平
全球即时:月薪4万招人去非洲养鸡当场长?企业回应:环境艰苦 不招年轻人
当前焦点!Windows 上 Docker 部署 MongoDb 并构建数据持久化
2022最新整理iOS app上架app详细教程
webrtc QOS笔记二 音频buffer数据不足生成很多gap的问题
当前最新:记录--前端项目中运行 npm run xxx 的时候发生了什么?
前沿资讯!科考中的意外收获!中国科学家在非洲发现消失百余年的濒危植物
天天速读:极氪001再遭奇葩故障:中控黑了、仪表花了、HUD“涂马赛克”
刚买两个月的谷歌Pixel 7 Pro翻车:绿屏无响应 用户绝望了
世界今头条!《和平精英》将推出开放世界玩法“绿洲世界” 能发射火箭
焦点信息:连过三科!新疆小伙1天拿到驾驶证
今日热议:低代码选型,论协同开发的重要性
【JVM】运行时内存分配
opencv-python 批量更改图像分辨率并且保留图像原有的透明度
热点!数据库概念
世界滚动:k8s~ingress限流机制
全球即时看!方正证券研究报告:行到水穷处,坐看云起时
环球快资讯丨不怕零下40℃极寒!我国复兴号高寒智能动车组投入运行
天天播报:为防员工摸鱼办公桌旁装监控 员工:很无语、感觉没隐私
《王者荣耀》花木兰新皮肤明日上线:143元 美背歌姬
世界时讯:海底捞回应禁止自带菜:可自带酒水饮品
天天短讯!RTX 4090高画质如何?《原子之心》PC平台性能分析:多配置流畅运行
【世界热闻】单特征线性回归
全球实时:为什么说《ps1屠龙刀》是awk、sed的恩人?
天天最资讯丨C语言在线代码运行编译工具推荐
全球快播:联想首款GeekPro游戏本真机公布:1TB SSD 超高性价比
环球微头条丨我国研发人员总量稳居世界首位 顶尖科技人才加速涌现
跑着跑着天窗会掉 奔驰中国召回超2万辆汽车
68岁成龙18年后再拍《神话2》 定名《传说》 古力娜扎主演
观速讯丨外卖员穿工装禁入成都高端商场 成都SKP回应:内部规定
焦点日报:135期 手绘汽车壁纸|插画 卫士还是老款好看 路虎卫士无水印手机壁纸
每日快讯!手把手教你为基于Netty的IM生成自签名SSL/TLS证书
当前热门:《我想进大厂》之Spring夺命连环10问
《分布式技术原理与算法解析》学习笔记Day20
世界看点:数据治理如何做?火山引擎DataLeap帮助这款产品3个月降低计算成本20%
世界今头条!装饰器设计模式这样学,保你必懂!
当前速讯:手机端ChatGPT搜索来了!微软2周火速上线 @Bing即用
多辆房车霸占高速服务区露营:有车主搭帐篷还晒咸菜
魔兽等游戏停服一个月 暴雪真不着急:两家中国公司抢破头
P1219 [USACO1.5]八皇后 Checker Challenge
世界新资讯:Java单元测试浅析(JUnit+Mockito)
81python装饰器
环球聚焦:《龙之家族》第二季明年首播
环球播报:Nginx基础03:配置文件nginx.conf(Part2)
【独家焦点】Python工具箱系列(二十六)
有奖调研!第五期(2022-2023)传统行业云原生技术落地调研——金融篇
全球快资讯丨A/B 测试成为企业“新窗口”:增长盈利告别经验主义,数据科学才是未来
晨光文具批发总部电话_石家庄晨光办公用品有限公司
环球要闻:新款特斯拉Model 3实车谍照曝光:疑似加长、内饰大改
可口可乐风味饮品!柠檬道日式气泡酒大促:12罐不到30元
前沿资讯!《嗜血印》将推出女祭司1/4比例雕像 性感热辣吸睛
焦点快播:送礼新思路!淘宝公布2023年度丑东西:网红青蛙服入选“年度五丑”
天天快报!C# Socket 通信时,怎样判断 Socket 双方是否断开连接
易基因|ChIP-seq等组学研究鉴定出结直肠癌的致癌超级增强子:Nature子刊
报道:顶象APP加固的“蜜罐”技术有什么作用
为什么带NOLOCK的查询语句还会造成阻塞
全球今亮点!支付宝二面:使用 try-catch 捕获异常会影响性能吗?大部分人都会答错!
名山大川是什么意思?名山大川有哪些?
喜欢被剧透的人是什么心理?喜欢被剧透的人是什么倾向?
每日快报!58岁清华毕业找不到超5000元工作 网友:我不是清华的咋办
天天热门:爷青结!《DNF》端游正式更名《地下城与勇士:创新世纪》:图标、文字调整
全球即时:《原子之心》优化非常好 但Xbox Series S仍存掉帧问题
精致又简约!雷孜LaCie新棱镜移动硬盘评测:速度全程稳如初
小米13 Ultra手机壳曝光:中分四摄、凸起严重
戴拿奥特曼飞鸟信扮演者怎么了?戴拿奥特曼的结局是什么?