计算机底层的秘密读书笔记之三

发布时间 2023-06-17 20:04:37作者: 济南小老虎

计算机底层的秘密读书笔记之三


IO部分之一

我感觉IO应该是最可能给人说明白的一个部分了. 
也是我这种菜鸟改善应用性能最可能的部分了.
CPU内存和cache 很难有优化的空间. 除非是开发去改垃圾代码.后者是升级硬件.
但是IO部分我感觉是有很大的优化空间的. 

1.IO多路复用.
因为时延金字塔的存在, IO层面的处理是最耗时,效率最低的. 
为了节约宝贵的CPU资源不至于浪费于等待乌龟一般的IO
出现了多种IO模型. 
也就是之前说的同步异步,阻塞和非阻塞的几种模式. 
虽然有多进程多线程模型.但是这么多进程线程去等待IO本身管理起来就很痛苦和折磨.
所以这个时候 随着CPU算力的提升, 大家逐渐可以走向IO多路复用的路线. 

IO多路复用其实经历了 select,poll,epoll等多种模式.
最近内核里面最先进的是 io_uring 他采用的模型更加先进. 
书本里面主要是讲解了 select,poll,以及epoll 三种.

select 的缺点是 只能够监听 1024个 fd.
poll   改进了监听的数量. 可以监听多于1024个fd
epoll  改进了poll的队列方式,改为了红黑树, 这样在非常多的fd的情况下,io完成之后比poll的处理效率高的多. 

epoll 是现阶段最成熟,最完善的一个IO多路复用的模型. 
Redis就是一个核心事务单线程, 但是基于epoll的多路复用IO模型实现的 高吞吐量的nosql数据库. 

Redis的 使用的模式就 Reactor模式. 

Redis使用 Reactor 模式来实现高效的网络通信和事件处理。
Reactor 模式的核心是一个事件循环(event loop),它是一种高效的 I/O 多路复用机制,
可以同时处理多个网络连接的 I/O 事件。在 Reactor 模式中,事件循环不停地等待事件的到来,
一旦有事件发生,就立即通知相应的事件处理函数进行处理。
Redis 中的事件循环在 main() 函数中实现,调用了库函数 aeMain(),aeMain() 函数进入一个无限循环,
等待事件的发生。当 Redis 接收到新的连接请求时,会将连接套接字加入到事件循环的监听队列中,同时注册读事件和写事件,
当有数据到达或者可以写入数据时,将触发对应的读处理函数或者写处理函数进行处理。
Redis 使用了一个基于事件驱动的处理模型,所有的网络 I/O 都是异步非阻塞的。
这样可以充分利用 CPU 资源,处理更多的客户端请求。同时,Redis 也减少了线程的切换和锁的竞争,提高了系统的并发性能。

epoll的工作模式

epoll 有两种工作模式,分别是边缘触发模式(Edge-Triggered,简称 ET)和水平触发模式(Level-Triggered,简称 LT)。

边缘触发模式(ET)通常是指当被监测的文件描述符上有读写事件发生时,进程会被立刻唤醒并进行对应的读写操作。
在 ET 模式下,当一个文件描述符上有事件到来时,内核仅仅会触发一次事件,同时该事件仅通知一次。
水平触发模式(LT)通常是指当被监测的文件描述符上有读写事件发生时,进程会被持续唤醒并进行对应的读写操作,直到该文件描述符上的事件被处理完毕为止。
在 LT 模式下,当一个文件描述符上有事件到来时,内核会持续不断地触发该事件,直到该事件被处理完毕。

与水平触发相比,边缘触发模式具有如下优点:
相比水平触发模式,ET 模式能够减少触发事件的次数,在高并发情况下,能够降低系统的 CPU 占用率;
在高并发场景下,边缘触发模式能够有效的避免惊群现象,提高了系统的吞吐量。
需要注意的是,在边缘触发模式下,事件到达时能够立刻被触发和处理,但是也需要保证读写缓冲区中的数据全部读入或写出,否则当下一次尝试进行读写操作时将会立刻触发另外一个事件。

综上所述,ET 模式常用于高并发读写的场景,具有高吞吐量和低资源占用率的特点。

在低并发的情况下,单次事件的处理时间相对较短,因此 select 和 epoll 在性能上的差别并不明显,两者差异主要在可扩展性和适用场景上。

select 在低并发情况下性能表现较好,但是在高并发情况下,select 的性能将会明显下降,因为 select 每次调用都需要遍历所有文件描述符集合,
同时 select 的文件描述符数量受到系统限制,最多只能监听 1024 个文件描述符。
epoll 采用基于事件驱动的方式,当事件到来时,只有该事件对应的文件描述符会被挂起,其他文件描述符不会被挂起,所以在处理大量文件描述符的情况下,
epoll 的效率将会相对较高。同时,epoll 支持 EPOLLET 边缘触发模式,在高并发场景下,能够有效的避免惊群现象,提高了系统的吞吐量。
因此,在低并发的情况下,select 和 epoll 的性能比较接近,但是在高并发的情况下,epoll 的性能优于 select。另外,epoll 也具有更高的可扩展性,可以支持更多的文件描述符。

综上所述,对于低并发的场景下,select 与 epoll 的性能表现相似,具体的选择还需要根据实际应用进行综合考虑。而对于高并发场景下,epoll 是更优的选择。

DMA

DMA Direct memory access
直接内存访问
操作系统有一块内存专门用于DMA, 其实他的机制就像是一个协处理器. 类似于描述性编程语言.
操作系统值需要告诉DMA的协处理器, 我要什么地方的数据, 那么DMA就会自动处理, 并且处理完成后在总线上通过终端告知CPU.
通知CPU来取对应的数据, 

因为有DMA所以就有了对应的RDMA, 然后出现了高速互联网里面的可以跨过机器直接访问远端机器的网卡或者是磁盘的数据.

操作系统为了提高性能. 很多情况下使用的是 zero Copy 的模式
所以zero COPY 大部分指的是避免了 DMA将数据传输到内核态内存后, 不是必须从内核态再copy到用户态, 减少的主要是 内核态和用户态之间的内存复制导致的时延. 

nginx 里面的send 其实就是使用的 zero COPY模式, 可以使用文件FD, 直接将文件从内核态的缓存,放进网卡的发送缓存中, 避免多次内存copy影响性能, 提高效率. 


IO的理解之二

其实不仅仅 磁盘操作 block 设备的操作是IO
网卡的操作也是IO. IO其实直接影响了设备对拥护者使用性能的观感. 

操作系统读取文件时的方式也类似. 既可以使用 read write的方式进行读写文件. 
也可以使用 mmap的方式将磁盘上的文件映射到内存中来,然后使用操作内存的接口来操作文件.
只不过在文件没有加载进内存时通过一次page fault的方式将文件加载内存. 

然后可以通过使用 fsync的方式将修改的文件进行写回. 
其实数据库就是类似这样的处理.
kafka等 消息队列的工具就是使用了mmap的方式实现了快速读写磁盘文件. 

Kafka 通过 mmap(Memory-Map,内存映射)技术来提高磁盘读写性能,
这种技术能够将磁盘文件直接映射到进程的虚拟内存空间中,
使得进程可以像访问内存一样访问文件,从而避免了频繁的磁盘读写操作。

在 Kafka 中,每个 Topic 的数据被存储在一个或多个分区中,
每个分区将数据以一系列可变长度的消息集合的形式保存到磁盘文件中。
Kafka 根据每个 Topic 的不同特性,选择使用不同的存储策略。
其中,Kafka 最常使用的存储策略就是基于 mmap 的存储方式。

具体来说,Kafka 在进行分区文件的创建时,
使用了 Linux 系统调用中的 open 和 mmap 函数。
通过 open 函数打开文件,然后使用 mmap 函数将文件内容映射到进程的虚拟地址空间中,
进程就可以直接访问该文件的内容了,且无论是读取还是写入都是在内存中进行了。

使用 mmap 技术可以带来以下几个好处:

1.  减少磁盘读写,提高性能:通过减少磁盘读写,能够大大提高 Kafka 的读写性能,
    降低磁盘 I/O 的负载,提高了 Kafka 的吞吐能力;
2.  简化内存管理:使用 mmap 技术,在 Kafka 读写分区文件时,无需显式调用 malloc 和 free 函数,
    而是通过直接访问内存映射文件,进行数据的读写操作,简化了内存管理流程;
3.  优化系统性能:在缓存机制方面,使用 mmap 技术也能够充分利用 Linux 操作系统提供的页缓存机制,
    通过预读取数据进行页缓存,尽可能地减少 I/O 操作次数,进一步提高了系统性能。
总的来说,通过内存映射技术,Kafka 能够有效的提高磁盘读写性能,减少磁盘 I/O 的开销,
同时也避免了频繁的内存分配和释放,简化了内存管理流程,提高了系统性能和稳定性。

使用 mmap 技术,Kafka 可以将磁盘中的分区文件映射到内存中,
由于数据是直接读写内存,不再需要进行磁盘 I/O 操作,可以极大地提高 Kafka 的读写性能。
但是,掉电等异常情况可能会导致 Kafka 内存中的数据丢失,因此需要一些措施来避免数据的丢失。

1.  内存映射文件 msync():Kafka 使用了 msync() 函数,可以强制将内存缓存区中的数据刷新到磁盘上,
    并通过 fsync() 函数来将文件从内存中强制刷新到磁盘上,从而防止数据丢失。
2.  双写策略:Kafka 引入了双写策略,即先将消息写入系统磁盘缓冲区,再通过刷盘策略将消息同步到磁盘中。
    两步骤都完成后,Kafka 确认消息已被持久化。这样一来,即使发生了掉电等意外情况,只要磁盘缓冲区中的数据已经同步到了磁盘中,就可以保证消息不会丢失。
3.  常规备份:除了上述方法外,还可以定期将 Kafka 数据备份到其它存储设备中,
    以防止由于掉电等不可预知的情况造成损失,从而更好的保护 Kafka 中的数据。

通过上述多个措施的综合应用,可以更好地提高 Kafka 的稳定性和可靠性,有效防止由于掉电等不可预知的情况造成数据丢失。

Kafka 使用的是顺序写的方式来将消息写入磁盘。
顺序写是指先将数据紧密地顺序写入到内存中,并按照顺序批量写入到磁盘中,这与随机写入的方式不同。
这种顺序写入方式对磁盘的性能和寿命都有很大的帮助,因为磁盘对持续写入的数据的处理能力更强,减少了寻道和旋转操作,以及减少了文件碎片的可能性。

在 Kafka 中,每个分区数据被存储在多个日志文件中,每个日志文件的大小是可以配置的,一般是64M或1G。
每个日志文件保存一段时间内的消息数据,具有相同的 offset,而新的消息将会写入到下一个 offset 大小的日志文件中,以便更好地利用磁盘的顺序读写性能。

Kafka 还采用了批量写入的机制,将一段时间内的多个消息进行批处理,然后一次性写入到磁盘中,减少了频繁的磁盘 I/O 操作。
同时,通过将消息写入到多个分区中,使得不同消费者可以同时消费消息,提高了消息的并发处理能力。

总之,Kafka 的顺序写入方式可以提高磁盘读写性能,减少文件碎片,保护磁盘寿命,采用批量写入的机制可以进一步优化磁盘 I/O 性能。
这些设计与实现,使得 Kafka 能够处理数百万甚至数十亿级别的消息,可靠地、高效地处理各种消息任务。