当前位置: 首页 > 科技观察

NGINX内存池设计

时间:2023-03-11 20:43:05 科技观察

NGINX是高性能高并发服务器的典范。NGINX通常用作HTTP和反向代理Web服务器。作为一个HTTP服务器,NGINX的工作模式是:接收客户端的请求,处理请求,然后吐出响应给客户端。这种工作模式非常适合用内存池优化动态内存分配,NGINX内存池也是其优秀设计的典型案例。NGINX为每个连接创建一个内存池对象。在处理连接请求的过程中,所有的动态内存分配请求都会被提交到内存池中。结果返回后,清理连接的内存池。清理内存池并不会真正将内存归还给系统,一定量的内存会根据策略被缓存和重用。因为请求的处理过程是在一个线程中进行的,所以连接的内存池不需要处理多线程同步(lock-free),也不需要中间释放内存(之后统一释放)结果返回),避免了内存泄漏的隐患。安全性也得到了改善。NGINX内存池设计概述在请求处理期间,所有动态内存分配都应用于连接特定的内存池。大内存按需分配,使用malloc/free或mmap/munmap,大内存块用链表链接在一起。小尺寸内存从内存页分配,批发到零售:先通过malloc/mmap申请一个4K(大小可配置)的内存页(批发),再从内存页(零售)再细分,内存也会被链表站起来。内存池记住当前页指针,下一次分配会先从当前页开始尝试,直到内存页多次不能满足分配请求后才会修改当前页指针。NGINX内存池的设计考虑了动态内存分配请求,并根据参数大小区别对待:(1)如果大于或等于某个阈值,则视为大块内存,大块内存分配直接调用标准C的malloc函数或系统调用mmap,内存池为大块内存维护一个单独的链表统一释放,大块也支持单独释放。(2)如果小于某个阈值,会先判断当前页的剩余内存大小是否能满足本次分配的大小要求:如果满足,则简单移动光标(其实就是对齐要求会考虑),并返回到之前内存大小的Cursor位置(地址),这种情况概率大,效率高。如果不满足,再通过底层接口划分一个内存页,从这个页中划分出一个block来满足分配请求。新分配的内存页会通过链表链接起来。.关于当前页指针:假设当前页还剩500字节,但是下一次分配请求是512字节,因为剩余大小不够,那么内存池会分配一个新的4K的内存页,分512字节从新的内存页返回,将新的内存页串到内存页链表中。当前页指针仍指向剩余的500字节内存页,下一次内存池分配请求仍将从旧内存页开始。只有当一个内存页多次不满足分配大小要求后,才会修改内存池当前页指针,以减少内存浪费。因为如果多次尝试还是不满意,那么这个内存页剩余空间的概率会比较小,此时跳过也是合理的。大多数分配是申请一小块内存。这部分高频请求用简单的移动光标就可以满足,性能非常高。由于分配的小块内存不支持中途释放,因此消除了每个内存的通用内存分配器增加了块的头/尾,提高了有效负载,内存利用率高。大块内存的分配是一个低概率事件。因为大块是由一个单独的链表串在一起的,所以支持统一释放,也可以通过遍历链表中途释放大内存块。因为大块不多,遍历的代价不会很大。之所以需要支持大块内存的中途释放,是为了避免内存使用过度膨胀(大块内存可能非常大),提高内存复用。NGINX内存池总结为每个连接分配一个内存池,让无锁成为可能。NGINX内存池专注于小块内存的分配。大块的分配简单的交给malloc/mmap。它更好地处理了当前内存页面指针的细节,提高了内存利用率。与一般内存池一样,NGINX内存池通过移动游标分配内存,牺牲了中途释放小块内存的能力来换取高性能和高内存利用率;通过提高高速缓存的多路复用,本质上是用空间换取时间。这些都体现了TradeOff的思想。NGINX用少量的代码就取得了很好的实用效果,值得借鉴。