Nginx Proxy Cache分析
本文从几个部分来详细介绍Nginx的proxy cache功能。第一部分,主要介绍proxy cache的过期、空间管理等。第二部分,主要介绍在Nginx(作为反向代理服务器)收到请求之后,如何检查本地的缓存来确定是否要向后端服务器发起请 求。第三部分,主要介绍Nginx向后端服务器发起请求并收到回复的情况下,如何把响应回复缓存到本地。
第一部分
在Nginx中,如果启用了proxy cache功能,master process会在启动的时候启动管理缓存的两个子进程(区别于处理请求的子进程)来管理内存和磁盘的缓存个体。第一个进程的功能是定期检查缓存,并将过 期的缓存删除;第二个进程的作用是在启动的时候将磁盘中已经缓存的个体映射到内存中(目前Nginx设定为启动以后60秒),然后退出。
具体的,在这两个进程的 ngx_process_events_and_timers()函数中,会调用ngx_event_expire_timers()。Nginx的 ngx_event_timer_rbtree(红黑树)里面按照执行的时间的先后存放着一系列的事件。每次取执行时间最早的事件,如果当前时间已经到了 应该执行该事件,就会调用事件的handler。两个进程的handler分别是ngx_cache_manager_process_handler和 ngx_cache_loader_process_handler。因为ngx_process_events_and_timers()是被循环调用 的,所以上面两个handler也会被调用多次。
1. ngx_cache_manager_process_handler
这个函数调用了每个磁盘缓存路径对应的manager()函数,即ngx_http_file_cache_manager()函数。这个函数很简单,就是检查缓存队列,看里面的索引信息有没有过期,如果过期,就把缓存的文件从磁盘删掉,并把索引从内存中释放。
- static time_t ngx_http_file_cache_manager(void *data)
- {
- ngx_http_file_cache_t *cache = data;
- //先删过期的缓存
- next = ngx_http_file_cache_expire(cache);
- for ( ;; ) {
- //获取最更新的缓存空间的大小
- ngx_shmtx_lock(&cache->shpool->mutex);
- size = cache->sh->size;
- ngx_shmtx_unlock(&cache->shpool->mutex);
- //如果空间在指定范围内,不用再删了。return
- //...
- //如果size超过磁盘的使用空间,即size >= cache->max_size
- //强制把部分缓存删除,以保证缓存使用的空间在指定范围内
- next = ngx_http_file_cache_forced_expire(cache);
- //休息一下以后继续删
- //...
- }
- }
2. ngx_cache_loader_process_handler
这个函数跟 ngx_cache_manager_process_handler差不多,不过在里面调用了对应路径的loader()函数,即 ngx_http_file_cache_loader()。这个函数的作用是给磁盘缓存路径下面子路径下面的所有缓存文件建立内存索引,并根据文件名得 到key来插入红黑树,并放入过期删除队列。
- static void ngx_http_file_cache_loader(void *data)
- {
- ngx_http_file_cache_t *cache = data;
- ngx_tree_ctx_t tree;
- //进入loading状态
- //...
- //初始化tree
- tree.init_handler = NULL;
- tree.file_handler = ngx_http_file_cache_manage_file;
- tree.pre_tree_handler = ngx_http_file_cache_noop;
- tree.post_tree_handler = ngx_http_file_cache_noop;
- tree.spec_handler = ngx_http_file_cache_delete_file;
- tree.data = cache;
- tree.alloc = 0;
- tree.log = ngx_cycle->log;
- //ngx_walk_tree 是递归函数,打开每层路径(dir)直到每个文件(file),根据其路径和文件名得到key,在缓存的rbtree(红黑树)里面找这个key(部 分),如果没有找到的话,就在内存中分配一个映射这个文件的node(但是不会把文件的内容进行缓存),然后插入到红黑树中和加入队列。
- //ctx->file_handler=>
- //ngx_http_file_cache_manage_file=>
- //ngx_http_file_cache_add_file=>
- //ngx_http_file_cache_add
- //从n = ngx_read_file(...)函数可以看出,每个磁盘缓存文件的开头的sizeof(ngx_http_file_cache_header_t)个byte存放了跟缓存相关的信息
- ngx_walk_tree(&tree, &cache->path->name);
- //...
- }
第二部分
在http的请求发送给有proxy_cache和proxy_pass 定义的location的时候,会调用到ngx_http_proxy_handler()。其中在决定是否要向后端发送请求的时候,先检查一下本地是否 有缓存一个copy。具体的位置是在ngx_http_upstream_init_request()函数的一开始,在发起向后的连接之前。
- static void ngx_http_upstream_init_request(ngx_http_request_t *r)
- {
- //在"proxy_cache"的set()即ngx_http_proxy_cache()中设置
- ////就是plcf->upstream.cache
- //这儿cache是ngx_shm_zone_t类型的
- if (u->conf->cache) {
- //寻找缓存
- //如果返回NGX_DECLINED就是说缓存中没有,请向后端发送请求
- rc = ngx_http_upstream_cache(r, u);
- if (rc == NGX_BUSY) {
- //重新试一次
- r->write_event_handler = ngx_http_upstream_init_request;
- return;
- }
- r->write_event_handler = ngx_http_request_empty_handler;
- if (rc == NGX_DONE) {
- //在缓存中找到了
- return;
- }
- if (rc != NGX_DECLINED) {
- ngx_http_finalize_request(r, rc);
- return;
- }
- }
- //向后端发起请求
- //...
- }
第三部分
在Nginx收到后端服务器的响应之后,会把这个响应发回给用户。而如果缓存功能启用的话,Nginx就会把响应存入磁盘里。
- static void ngx_http_upstream_send_response(ngx_http_request_t *r, ngx_http_upstream_t *u)
- {
- rc = ngx_http_send_header(r);
- //...
- #if (NGX_HTTP_CACHE)
- //是否要缓存,即proxy_no_cache指令
- //...
- //如果要缓存,设置有效时间等
- //...
- #endif
- //...
- //ngx_http_upstream_process_upstream ==> 1. ngx_event_pipe ==>
- //ngx_event_pipe_read_upstream ==> ngx_event_pipe_write_chain_to_temp_file ==>
- //ngx_write_chain_to_temp_file ==> ngx_create_temp_file --> ngx_write_chain_to_file
- //==>2. ngx_http_upstream_process_request ==> ngx_http_file_cache_update
- //在这儿把从后端获取的文件写到磁盘
- ngx_http_upstream_process_upstream(r, u);
- }
附:proxy cache的配置、set()函数和初始化函数
在Nginx配置文件里面设置proxy_cache_path指令,指定缓存内存的大小和在disk文件系统的路径。如:
- #...
- http {
- #...
- proxy_cache_path /var/proxy_cache_dir levels=1:2 keys_zone=cache_one:500m max_size=30g;
- server {
- #...
- location / {
- proxy_cache cache_one;
- #...
- }
- #...
- }
- #...
- }
在解析配置文件的时候,"proxy_cache_path"指令的set()函数是ngx_http_file_cache_set_slot()。
- char * ngx_http_file_cache_set_slot(...)
- {
- //分配内存给cache结构
- ngx_http_file_cache_t *cache;
- cache = ngx_pcalloc(cf->pool, sizeof(ngx_http_file_cache_t));
- cache->path = ngx_pcalloc(cf->pool, sizeof(ngx_path_t));
- //获取缓存在disk文件系统的路径
- value = cf->args->elts;
- cache->path->name = value[1];//第一个arg
- //去掉path最后的"/"
- //如果path不是以"/"开头,即是一个相对路径,就在前面加上$nginx_home的路径
- //...
- //解析其他的参数
- for(;;){
- //"level"
- //设置cache->path->level,cache->path->len
- //"keys_zone"
- //设置name.data和name.len,即缓存的名称
- //"inactive"
- //设置缓存驻留时间
- //"max_size"
- //设置硬盘大小
- }
- cache->path->manager = ngx_http_file_cache_manager;
- cache->path->loader = ngx_http_file_cache_loader;
- cache->path->data = cache;
- //检查cache->path是否已经存在
- //如果不存在,则添加到cf->cycle->pathes
- ngx_add_path(cf, &cache->path);
- //name是缓存区名称,size是内存缓存大小
- //遍历cf->cycle->shared_memory的part链表寻找是否已经存在同名的内存空间。如果不存在,就分配一个大小为ngx_shm_zone_t的内存单元,放在part链表最后一个单元的elts。
- //shared_memory为ngx_list_t结构,part为ngx_list_part_s结构
- cache->shm_zone = ngx_shared_memory_add(cf, &name, size, cmd->post);
- //注册回调函数等
- cache->shm_zone->init = ngx_http_file_cache_init;
- cache->shm_zone->data = cache;
- cache->inactive = inactive;
- cache->max_size = max_size;
- }
在cycle初始化的时候,会初始化内存索引的空间
- ngx_cycle_t * ngx_init_cycle(...)
- {
- //...
- part = &cycle->shared_memory.part;
- shm_zone = part->elts;
- //遍历链表中每个ngx_list_part_t的每个单元
- for(;;){
- //检查当前ngx_list_part_t是否已经遍历完成。若是,则跳到下个ngx_list_part_t
- //...
- //检查每个单元,即ngx_shm_zone_t的shm的size是否为0
- //检查shm的init函数是否有注册,如果没有就说明这个shm没有被用到
- //...
- shm_zone[i].shm.log = cycle->log;
- //old_cycle的shared_memory
- opart = &old_cycle->shared_memory.part;
- oshm_zone = opart->elts;
- //遍历old_cycle的shared_memory每个ngx_list_part_t的每个单元
- for(;;){
- //检查当前ngx_list_part_t的所有单元是否已经遍历完成。若是,则跳到下个ngx_list_part_t
- //...
- //检查是否同名,不同名则跳到下一个单元
- //...
- //找到同名且shm的size和addr匹配
- //调用init函数,并跳到下一个new cycle的shm_zone
- shm_zone[i].init(&shm_zone[i], oshm_zone[n].data);
- //同名但size和addr不匹配,说明old_cycle的oshm_zone无效,把它释放
- ngx_shm_free(&oshm_zone[n].shm);
- break;
- }
- //在old_cycle没有找到跟新cycle的shm_zone[i]匹配的shm
- //分配共享内存给&shm_zone[i].shm,用mmap或者shmget/shmat
- ngx_shm_alloc(&shm_zone[i].shm);
- //把 &shm_zone[i].shm初始化一个ngx_slab_pool_t结构的变量sp
- //对sp成员赋值,调用ngx_shmtx_create()给sp->mutex初始化锁
- //ngx_slab_init(),对分配的共享内存进行初始化
- ngx_init_zone_pool(cycle, &shm_zone[i]);
- //ngx_http_file_cache_init()
- shm_zone[i].init(&shm_zone[i], NULL);
- }
- }
- ngx_http_file_cache_init(ngx_shm_zone_t *shm_zone, void *data)
- {
- ngx_http_file_cache_t *ocache = data;
- cache = shm_zone->data;
- //如果ocache不是NULL,即有old cache,就比较缓存路径和level等,如果match的话就继承ocache的sh、shpool、bsize等
- //...
- //如果没有old cache
- cache->shpool = (ngx_slab_pool_t *) shm_zone->shm.addr;
- //ngx_slab_alloc_locked()
- cache->sh = ngx_slab_alloc(cache->shpool, sizeof(ngx_http_file_cache_sh_t));
- cache->shpool->data = cache->sh;
- //初始化红黑树
- //把rbtree的insert函数设为ngx_http_file_cache_rbtree_insert_value()
- ngx_rbtree_init(&cache->sh->rbtree, &cache->sh->sentinel, ngx_http_file_cache_rbtree_insert_value);
- //初始化一个queue
- ngx_queue_init(&cache->sh->queue);
- cache->sh->cold = 1;
- cache->sh->loading = 0;
- cache->sh->size = 0;
- //获取文件系统的block size
- cache->bsize = ngx_fs_bsize(cache->path->name.data);
- //max_size成了总共的block数量
- cache->max_size /= cache->bsize;
- len = sizeof(" in cache keys zone \"\"") + shm_zone->shm.name.len;
- cache->shpool->log_ctx = ngx_slab_alloc(cache->shpool, len);
- }
索引缓存文件ngx_http_file_cache_lookup
文件缓存格式
缓存过期队列