NGINX缓存原理及源码分析(一)
1.7k 次浏览
发表于 2021-01-07 20:57

概述

缓存是计算机领域的一种非常重要设计。主要用来提升响应速度。比如计算机系统中的存储体系就是这一经典缓存的实现。它是由寄存器,缓存,内存,磁盘,网络组成的一个层级体系,用来存储各种信息。类比日常生活中,我们对于知识的存储系统也是一个层级的缓存体系。它是由我们的大脑,笔记本,身边朋友,书籍,网络组成的。这些层级系统有一个共同的特点就是层级越高它的容量越小,速度越快但是同时价格也越贵。

在我们使用浏览器获取信息时,也有一个缓存体系在发挥作用。那就是客户端浏览器缓存,中间各级代理设备缓存,服务器缓存,服务器源文件。

这种网络的缓存对于提升响应速度有非常好的作用。尤其是在分布式系统中,起到了非常大的作用。具体说来,网络缓存的主要作用有:

1.     缓存减少了冗余的数据传输,节省了网络费用。

2.     缓存减缓了网络瓶颈的问题,不需要更高的带宽就能够更快地加载页面。

3.     缓存减少了对原始服务器的请求,服务器可以更快地响应,避免过载。

4.     缓存减缓了距离时延,因为从较远的地方加载页面会更慢一些。

NGINX作为代理设备通过自身的缓存功能来提高响应速度。下面我们试着去分析NGINX缓存功能的工作原理和源码实现原理。


原理

如下图所示,NGINX缓存功能就是通过把上游服务器的文件存放到本地磁盘上,然后在一定条件下直接使用它们来应答下游请求。 这样省去了和上游服务器的交互的时间,从而提高了效率,减轻了网络和上游服务器压力。

从实现角度来讲,如下图所示,NGINX从上游服务器获取到要缓存的文件后,需要在内存中维护一份它的元数据(meta data),并且在磁盘上保存文件完整数据。同时NGINX需要对两者进行有效的管理,比如维护两者的一致性,在一定条件下删除某些文件等。

NGINX的缓存功能都是围绕上述数据结构进行的。主要包括以下四个功能:

1.     管理

维护上图数据的一致性,定期删除过期缓存文件或者强制删除某些缓存文件来释放磁盘空间。

2.     加载

NGINX启动时,如果对应目录中存在缓存文件,则需要生成对应内存中的文件元数据。

3.     生成

从上游获取文件时,如果需要缓存则创建内存中的元数据以及对应磁盘中的文件。

4.     使用

新的请求到来时,如果有对应的缓存文件可用,则直接使用磁盘中的文件返回。

配置了缓存功能后,NGINX就会启动Cache Manager进程和Cache Loader进程用来分别完成功能1和功能2Cache Manager是一个常驻进程,它周期性地运行来淘汰过期缓存或者强制删除某些缓存文件释放磁盘空间。在两次缓存管理器启动的间隔,缓存的数据量可能短暂超过配置的大小。

Cache Manager不同,Cache Loader进程只在启动时运行一次,完成任务后就退出。NGINX 启动1分钟之后,Cache Loader进程生成现有的缓存文件的元数据并且加载到共享内存区域中。

功能3和功能4NGINXworker进程在处理HTTP请求过程中完成的。

NGINX缓存实现涉及的总体内容比较多,我们将分成两部分进行分析。本篇文章,我们侧重分析功能1和功能2,也就是NGINX是如何管理和维护缓存数据的,以及在启动阶段,磁盘中的文件是如何生成对应的内存中的元数据的。


配置

如下所示,只需要proxy_cache_path proxy_cache 两条指令就可以开启内容缓存,前者用来设置缓存的路径和配置,后者用来启用缓存。

http {

    ...

    proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g    inactive=60m     use_temp_path=off;

    proxy_cache_valid 30m;

    server {

        proxy_cache mycache;

        location / {

            proxy_pass http://localhost:8000;

        }

    }

}

指令proxy_cache_path:

Syntax:proxy_cache_path path [levels=levels] [use_temp_path=on|off] keys_zone=name:size [inactive=time] [max_size=size] [min_free=size] [manager_files=number] [manager_sleep=time] [manager_threshold=time] [loader_files=number] [loader_sleep=time] [loader_threshold=time];

Default:          

Context:          http

指令用来设置cache的路径和其他参数。Cache数据存放在文件中。文件在Cache中的名字就是对请求特定的keys实施MD5的结果。


参数Path:指定了缓存存放的目录。


参数Level:在上述Path目录下再设置层次结构的目录。最多可以设置3层,每一层的目录名字最多为两个字节。设置层级结构是因为将大量的文件放置在单个目录中会导致文件访问缓慢。

比如配置:proxy_cache_path /data/nginx/cache levels=1:2:1 keys_zone=one:10m;

对应的磁盘中的文件如下所示:

/data/nginx/cache/c/29/0/b7f54b2df7773722d382f4809d65029c

需要注意的是,要保证NGINX进程有权限在配置的目录下创建文件。


参数use_temp_pathNGINX 最初会先将缓存文件放入一个临时存储区域,然后通过重命名存放到参数Path指定的目录中。需要注意的是,如果临时区域目录和Path指定的目录不在一个文件系统,那么重命名操作就会变成copy操作,从而带来额外的时间开销。所以,如果启用临时存储区域,建议和Path参数指定的Cache目录位于同一个文件系统上。如果此参数设置为on或者被忽略,指令proxy_temp_path指定的目录将会被用做临时存储区域。如果被设置为off,文件会直接写入Path参数指定的cache目录。


参数key_zone:定义缓存文件元数据和key值存放的共享内存区域。需要同时指定namesize参数。其中size定义共享内存区域的大小。在开源版本的NGINX中,1M大小内存区域可以存放大于8千个key值。

参数inactive:定义缓存的文件如果在inactive时间内没有被访问,不管该文件是否过期 都需要从磁盘中删除。该参数默认值为 10 分钟(10m)。注意,inactive时间有别于过期时间。缓存的过期时间是由上游服务器通过缓存控制头部决定的。缓存的inactive时间是指定现在距离上次被使用的时间。NGINX 不会自动删除过期时间到期的缓存。缓存在 inactive 指定时间内没有被访问的情况无论是否过期都会被删除。

参数max_size:设置了缓存占用磁盘空间大小的上限。如果不指定具体值,那就是允许缓存不断增长,占用所有可用的磁盘空间。当缓存达到这个上线,NGINX便调用 Cache manager 来移除最近最少被使用(LRU)的文件,这样把缓存的使用空间降低至这个限制之下。

参数min_free:从1.19.1版本开始加入。指定如果Cache目录所在的文件系统的剩余可用空间小于min_free指定的数值,NGINX也会调用Cache manager来移除最近最少被使用(LRU)的文件,这样把文件系统的可用空间增加至参数规定数值之上。

参数manager_files, manager_threshold, manager_sleep:在上述的Cache Manager运行过程中,为了避免长时间占用CPU资源,Cache Manager采用了分批处理的策略。每一次执行最多会删除manger_files(默认100)个文件,运行的最长时间是manager_threshold毫秒(默认200)。两者只要一个条件满足,一次执行就结束。另外每两次执行之间需要睡眠manager_sleep毫秒(默认50)从而释放CPU资源。

参数loader_files, loader_threshold, loader_sleepNGINX在启动1分钟后会启动Cache Loader进程来生成现有缓存文件对应的内存中的元数据和key值。与Cache Manager进程一样,Cache Loader进程的执行也是分批进行的。在一次执行中最多处理loader_files个文件,或者执行loader_threshold毫秒。两者只要一个条件满足,一次执行就结束。另外每两次执行之间需要睡眠loader_sleep毫秒(默认50)从而释放CPU资源。

指令proxy_cache:

Syntax:proxy_cache zone | off;

Default:proxy_cache off;

Context:http, server, location

指令proxy_cache主要是在特定的context下引用指令proxy_cache_path定义的缓存区域。默认值是off,用来关闭从上一层配置中定义的Cache


源码分析

数据结构

与缓存相关的数据结构有ngx_path_t, ngx_http_file_cache_s , ngx_http_file_cahce_sh_t, ngx_http_file_cache_node_t, ngx_http_cache_s, ngx_http_file_cache_header_t等。

它们的主要用途是:

ngx_path_t :目录的路径 (name)、子目录层级定义 (level) 和 可定制 的缓存管理行为 (manager, loader回调函数),对应创建的Cache等。每一条proxy_cache_path指令对应一个ngx_path_t结构。

ngx_http_file_cache_t :表示每条 proxy_cache_path 指令创建的 cache以及inactive等参数。

ngx_http_file_cache_sh_t :维护 LRU 结构用于保存缓存节点以及缓存的当前状态 (是否正在从磁盘加载、当前缓存大小等),同时维护红黑树保存缓存文件的元信息,同时还有共享内存的使用情况等信息。这个结构来自于共享内存区域以便多个worker进程共享使用。

ngx_http_file_cache_node_t :保存磁盘缓存文件在内存中的描述信息,不包含实际内容。这些信息作为索引信息存放在上面ngx_http_file_cache_sh_t结构中的红黑树节点和LRU链表中。

ngx_http_file_cache_header_t :缓存文件系统中文件头结构信息。存储缓存文件的相关信息 (修改时间、缓存 key crc32 值、用于指明 HTTP 响应包头和包体在缓存文件中偏移位置的字段等)。此结构信息也存储在磁盘缓存文件的头部。

ngx_http_cache_t:某一个请求对应的缓存条目的完整信息 (请求使用的缓存 file_cache、缓存条目对应的缓存节点信息 node、缓存文件 filekey 值及其检验 crc32 等等) 都临时保存于此(r->cache) 结构体中,这个结构体中的信息量基本上相当于 ngx_http_file_cache_header_t ngx_http_file_cache_node_t 的总和。

这些数据结构之间的相互关系如下图所示:

它们在系统中的位置和引用关系如下图所示:

配置解析和进程启动

模块fastcgi/proxy/scgi/uwsgi在对缓存的使用方式上几乎是一致的。它们分别通过各自的指令来定义和使用缓存功能。我们将以proxy模块为例进行源码分析,此模块对应的指令是proxy_cache_path


指令proxy_cache_path解析流程:

1.     NGINX解析指令proxy_cache_path时,ngx_http_file_cache_set_slot函数得以调用。函数生成一个ngx_http_file_cache_t结构然后解析proxy_cache_path指令中各种参数并把他们存放到ngx_http_file_cache_t结构中。

2.     初始化ngx_http_file_cache_t结构中的类型为ngx_path_tpath成员。成员path存放指令proxy_cache_path定义的zonename,level等参数。同时把函数指针ngx_http_file_cache_managerngx_http_file_cache_loader分别赋值给managerloader成员。这两个指针分别在Cache ManagerCache Loader进行中被调用用来完成相应的缓存清理和缓存载入等工作。

3.     调用函数ngx_add_pathngx_http_file_cache_t结构中的path成员加入到全局路径数组 (cycle->paths) 中,这些路径在配置解析完毕后,由 NGINX调用ngx_create_paths函数检查并创建相应的目录。

4.     初始化ngx_http_file_cache_t结构中的shm_zone成员。调用函数ngx_shared_memory_add根据指令proxy_cache_path中定义的namesize参数生成一片共享内存区域。然后把内存区域的初始化函数设置为ngx_http_file_cache_init。此函数用来初始化共享内存区域。在配置解析完毕后,由 NGINX 完成共享内存创建和初始化工作。关于NGINX共享内存可以参考这篇文章

5.     把生成的cache结构存放到对应配置结构ngx_http_proxy_main_conf_t中的caches数组中。


指令proxy_cache解析流程:

1.     函数ngx_http_proxy_cache用来解析proxy_cache指令。此函数的主要作用是初始化ngx_http_proxy_loc_conf_t结构中upstream成员的cache_zone成员。

2.     获取指令proxy_cache中的name参数。然后调用函数ngx_shared_memory_add来初始化ngx_http_proxy_loc_conf_t结构中upstream成员的cache_zone成员。参数name对应的缓存必须是由proxy_cache_path指令事先定义的。

3.     指令解析完成以后,ngx_http_proxy_loc_conf_t结构中upstream成员的cache_zone 就可以引用到已经初始化的缓存 (ngx_http_file_cache_t 类型结构体) 了。如此以来,当一个请求匹配到对应的location后,通过ngx_http_proxy_loc_conf_t结构就可以很容易地找到此location对应的Cache区域。

配置解析完毕以后,NGINXmaster进程调用函数ngx_start_cache_manager_processes来判断是否需要启动Cache ManagerCache Loader进程。判断的条件是,如果对应的全局配置的path数组不为空,并且至少有一个数组元素的manager函数指针被设置,则启动Cache Manager进程。如果至少有一个数组元素的loader函数指针被设置,则启动Cache Loader进程。

至此,相关的配置已经得到解析,Cache ManagerCache Loader进程也被启动起来。

接下来,我们开始分析两个进行进程的运行逻辑。


Cache Manager进程

Cache Manager进程主要负责清理过期缓存以及强制清理部分缓存来释放磁盘空间。


Cache Manager进程的启动和运行逻辑:

1.     配置解析完成以后, 在NGINXmaster进程中通过函数ngx_start_cache_manager_processes调用函数ngx_spawn_process启动Cache ManagerCache Loader进程。

2.     两个进程的入口函数都是ngx_cache_manager_process_cycle。不同的是传入的参数。对于Cache Manager进程,对应的参数是ngx_cache_manager_ctx, 而对于Cache Loader进程,对应的参数是ngx_cache_loader_ctx

3.     这两个参数对应的数据结构是ngx_cache_manager_ctx_t,它的定义如下:

static ngx_cache_manager_ctx_t  ngx_cache_manager_ctx = {

    ngx_cache_manager_process_handler, "cache manager process", 0

};

在这个结构中,第一项是回调函数,就是对应整个进程运行的主函数。第二项是进程的名称,第三项是进程需要延迟多久执行。

4.     当函数ngx_cache_manager_process_cycle运行时,因为两个进程属于管理进程,本身不处理连接请求,所以通过ngx_close_listening_sockets函数关闭所有打开的listening socket

5.     然后把对应的ngx_cache_manager_ctx_t结构中的hander函数赋值到ngx_event_t结构中。然后通过ngx_add_timer把上述的ngx_event_t结构加入到全局的timer系统。

6.     最后通过ngx_process_events_and_timers处理对应的回调函数。这样Cache Manager进程的功能就可以通过ngx_cache_manager_process_handler函数得以实现。


函数ngx_cache_manager_process_handler的流程:

1.     函数ngx_cache_manager_process_handler遍历全局变量ngx_cycle中的paths数组。然后执行数组元素的manager回调函数。对于proxy模块来说就是函数ngx_http_file_cache_manager

2.     函数ngx_http_file_cache_manager完成缓存删除工作并且按照一定算法计算Cache Manager进程下次运行的时间。


函数ngx_http_file_cache_manager的流程:

1.     函数ngx_http_file_cache_manager调用ngx_http_file_cache_expire去淘汰过期缓存。

2.     然后如果当前缓存目录的使用率超过了watermark(默认是7/8),则循环调用函数ngx_http_file_cache_forced_expire来强制删除部分缓存以释放部分磁盘空间。如果强制删除文件数量超过配置的manager_files数量或者本次运行的时间超过配置的manager_threshold,则本次强制删除运行结束。


函数ngx_http_file_cache_expire的流程:

1.     找到缓存对应的共享内存区域后通过lock函数进行同步锁定。

2.     查找共享内存区域中LRU链表。如果链表为空,则表示当前缓存没有文件需要处理,则直接退出。

3.     LRU链表尾部开始遍历,如果有对应缓存文件的已经过期(自上次最近使用时间已经超过inactive时间),并且文件没有被使用,则通过ngx_http_file_cache_delete函数把缓存文件的meta信息(LRU,rbtree node等)从共享内存区域删除。然后找到对应的磁盘文件然后调用ngx_delete_file删除。

4.     在遍历处理过程中,如果处理的文件数量超过配置的manager_files数量或者本次运行的时间超过配置的manager_threshold,则本次运行结束。

5.     函数还会根据各种情况计算下次Cache Manager运行的时间。比如有文件已经过期但是还在被使用,则下次运行的时间就需要相应变短。


函数ngx_http_file_cache_forced_expire流程:

1.     找到缓存对应的共享内存区域后通过lock函数进行同步锁定。

2.     查找共享内存区域中LRU链表。如果链表为空,则表示当前缓存没有文件需要处理,则直接退出。

3.     LRU链表尾部开始遍历,如果文件没有被使用,不论它是否过期,通过ngx_http_file_cache_delete函数删除文件内存和磁盘中的信息。

总结说来,Cache Manager进程的主要逻辑是将距离上次使用时间超过inactive的资源删除。另外,当缓存目录的使用率超过了一定水位(7/8),则通过LRU链表强制删除未使用的老缓存文件信息。同时为了防止过度占用CPU资源,Cache Manger进程采用了分批处理的策略。


Cache Loader进程

Cache Loader进程只在NGINX启动期间运行一次。它一般是在NGINX启动60秒以后开始运行,遍历缓存目录下存在的文件,然后生成内存中文件对应的元数据。当处理完所有的磁盘的文件后,Cache Loader进程就会退出。

如同在Cache Manager进程描述的那样,NGINXCache ManagerCache Loader的进程的入口函数同为ngx_cache_manager_process_cycle,但是对应的传入参数不同。对应Cache Loader进程,其参数如下:

static ngx_cache_manager_ctx_t  ngx_cache_loader_ctx = {

    ngx_cache_loader_process_handler, "cache loader process", 60000

};

从上面参数我们可以看到,参数60000规定了需要延迟60000ms再运行。函数ngx_cache_loader_process_handlerCache Loader的主要逻辑。


函数ngx_cache_loader_process_handler流程:

1.     函数ngx_cache_loader_process_handler遍历全局变量ngx_cycle中的paths数组,然后执行数组元素的loader回调函数。对于proxy模块其对应的loader回调函数是ngx_http_file_loader_manager

2.     初始化一个ngx_tree_ctx_t结构。结构中ngx_tree_ctx_t定义了一组回调函数在遍历缓存目录时使用。

file_handler = ngx_http_file_cache_manage_file 文件节点为普通文件时调用

pre_tree_handler = ngx_http_file_cache_manage_directory 在递归进入目录节点时调用

post_tree_handler = ngx_http_file_cache_noop在递归遍历完目录节点后调用

spec_handler = ngx_http_file_cache_delete_file文件节点为特殊文件时调用

3.     调用ngx_walk_tree函数使用上面的各个函数递归处理缓存路径中的各个文件。对于缓存这种普通文件,调用的函数是ngx_http_file_cache_manage_file


函数ngx_http_file_cache_manage_file流程:

1.     调用函数ngx_http_file_cache_add_file把磁盘文件的元信息加入到缓存目录对应的共享内存区域。

2.     判断本次运行处理的文件个数是否大于loader_files参数规定或者运行时间大于loader_threshold参数值。如果是,则通过ngx_msleep函数睡眠loader_sleep参数规定的时间。睡眠结束以后,继续处理文件,等处理完所有的文件,Cache Loader 进程退出。


函数ngx_http_file_cache_add_file流程:

1.     首先检查缓存文件的有效性 (文件名长度是否符合规则、文件大小是否满足最小缓存文 件大小要求等)

2.     将文件名中的 32 字节字符摘要转换为 16 字节二制形式。

3.     调用 ngx_http_file_cache_add 函数将此缓存文件的元信息节点加入 ngx_http_file_cache_sh_t 类型的缓存管理机制中。


函数ngx_http_file_cache_add流程:

1.     函数ngx_http_file_cache_add首先通过节点的key值检查文件是否已经在共享内存的红黑树中存在。

2.     如果不存在则生成一个新的节点并且把节点插入到红黑树中。

3.     如果已经存在就先把节点从LRU链表中移除。

4.     再根据inactive参数更新节点的过期时间然后在把节点加入到LRU链表的头部。

到此为止,缓存磁盘文件就被加载到内存中了。

需要注意的是,如果配置文件中指令proxy_cache_pathpath或者level参数发生了变化,则在NGINX重新启动时,原来的缓存文件就不会被加载到内存中。


结语

NGINX缓存功能是其最重要的功能之一,其代码实现也比较复杂。在实际使用中,缓存使用涉及到浏览器,NGINX代理服务器,源服务等几方面的行为。而且这几个方面需要根据HTTP协议的规定进行配合,整个过程相对比较复杂。

本文仅仅从原理和整体框架方面对NGINX的缓存功能进行了分析。阐述了Cache ManagerCache Loader两个管理进程的工作原理。下篇,我们将试着从请求的角度去分析NGINX是如何生成新的缓存文件已经如何使用缓存文件来响应客户端的。


发表评论
  • lo.no

    请问cache loader为什么要在nginx启动后60s才执行?

    2021-11-10 15:15
    0
    回复
  • ra8063

    图片不是多么清楚,大胸弟

    2022-01-18 11:19
    0
    回复
  • 皮皮鲁 回复 ra8063

    谢谢小弟弟的关注,其实图片就是一个示意,大体理解就可以了。如果想特别详细还要自己看代码。

    2022-01-24 20:02
    0
    回复
  • Listen Youngʚ😊ɞ 回复 皮皮鲁

    说的好

    2022-10-24 19:16
    0
    回复
  • Listen Youngʚ😊ɞ 回复 皮皮鲁

    说的太好了

    2022-10-24 19:16
    0
    回复
发表者

皮皮鲁

暂无个人介绍

  • 27

    文章

  • 19

    关注

  • 28

    粉丝

活动推荐
Copyright 公安部网络安全保卫局 All Rights Reserved
京公网安备 11010502047880号    京ICP备05070602号