如何善用缓存提升系统的健壮性?(上)
622 次浏览
发表于 2020-05-19 13:58

分布式系统提升可用性时,最有效的方案就是在空间维度上,将资源复制一份作为缓存,并把缓存放在离用户更近的地方。这样,通过缩短用户的访问路径,不只可以降低请求的时延,多份资源还能提升系统的健壮性。比如WEB服务中的CDN就是这样一个缓存系统。

 

Nginx由于具有下面3个特性,因此是最合适的缓存系统:

l  首先,高并发、低延迟赋予了Nginx优秀的性能;

l  其次,多进程架构让Nginx具备了很高的稳定性;

l  最后,模块化的开源生态,以及从开放中诞生的Openresty、Kong等其他体系,这都让Nginx的功能丰富而强大。

 

所以,Nginx往往部署在企业最核心的边缘位置,在最外层的Nginx上部署共享缓存,能够给服务带来更大的收益,更短的访问路径带来了更佳的用户体验。然而,当整个系统的可用性极度依赖Nginx的缓存功能时,我们必须仔细地配置Nginx,还得使用到缓存的许多进阶功能。

 

比如,在超大流量下如果热点资源的缓存失效,那么在巨大的流量穿透Nginx缓存后,非常有可能把脆弱的上游服务打挂。此时合并回源请求功能,就是你的最佳应对手段!再比如,在源服务器暂时不可用时,使用失效过期的缓存,为用户提供有限的服务,可以通过降级体验来提升系统的健壮性,这要比给用户返回“系统暂时不可用”要好得多。再比如,Nginx重启后,需要为磁盘上大量的缓存文件,在共享内存中建立起索引,这一过程可能很漫长,我们必须防止它对正常的服务产生过大的影响,降低用户体验。

 

如果你不清楚Nginx缓存的这些用法,就无法在超大流量场景中,用Nginx缓存保障服务的高可用性。当然,你可能觉得加机器扩容简单粗暴又有效,可是,一旦Nginx缓存正常工作后,这些新增的机器利用率又变得极低,这提升了公司的IT成本。因此,Nginx缓存的这些进阶用法,是你必须掌握的知识点!

 

这篇文章将分为上、下两个部分,先带着你回顾下Nginx缓存的基本用法,再来看如何使用缓存的进阶功能。

 

缓存的基本工作流程

时间局部性原理,决定了缓存的有效性。比如,当客户端首次获取到/url1对应的资源时,它可以用key/value的形式,将请求与响应同时缓存到磁盘上,其中,key就是/url1,而value就是响应中的资源。只要服务器没有更改/url1对应的资源内容,那么在时间维度上,后续访问/url1的请求,就一直可以使用磁盘上的缓存代替网络访问来提升性能,如下图所示:

通过缓存,请求的时延得到极大的下降,因为访问磁盘不过几十毫秒,但经过以秒级计量的网络再到达源服务器,并由源服务器再读取磁盘,速度就慢了很多,尤其在网络是不稳定、不可控的时候。

 

然而,服务器上的资源一旦发生了更新,客户端的缓存就失效了,使用这样的缓存有可能导致业务逻辑出错,怎么解决这一问题呢?通常,由于服务器最清楚资源的变更频率,这样,在服务器发送的响应中,设置一个预估缓存失效时间的HTTP头部,就可以解决这一问题。就像Cache-Control: max-age头部,或者Expires头部,等等。

 

客户端基于服务器告知的失效时间设立定时器,当定时器归零触发时,再将缓存设置为失效即可。注意,失效的缓存仍然有多方面的价值

 

首先,客户端的失效定时器,毕竟来源于服务器的预估时间,时间过期后服务器上的资源也可能并未发生变化。这样,如果我们给每份资源生成唯一的ID(也就是缓存摘要,RFC规范用etag头部表示摘要),而客户端在判定缓存失效后,能够携带着摘要访问服务器,这就允许服务器通过304 Not Modified空包体回复客户端。如果资源是一个数百MB的视频文件,这一下就省去了数百MB字节的网络传输,如下图所示:

图片.png

 

Etag摘要究竟是怎样生成的呢?Nginx会将文件大小和最近修改时间,拼接为一个字符串作为文件摘要(详见《Nginx核心知识100讲》第97课),虽然区分度稍差,但优点是生成速度非常快。

 

其次,在某些场景下,给用户提供不那么及时的过期页面,要比返回500 服务器内部错误好得多。Nginx的proxy_cache_use_stale指令就可以完成这一功能,我们下一篇再细说。

 

缓存可以存放在任何位置!其中,仅存放于终端、只为一个用户服务的缓存,叫做私有缓存(Private Cache),而存放于服务器上,可以被多个用户共享使用的缓存,叫做共享缓存(Public Cache)。比如下图的REST架构中,浏览器User Agent中的$符号($表示现金cash,与cache发音很接近,故常用来表示缓存)表示私有缓存,而Proxy正向代理、Gateway反向代理中的$符号表示共享缓存:

图片.png

 

Nginx上的缓存就是共享缓存,接下来我们看看如何配置共享缓存。

 

如何开启基础版的共享缓存功能?

接下来,我将按照缓存的工作流程,介绍配置基础共享缓存的Nginx指令。

 

首先,我们要确定每个资源对应的key关键字。这里的原则是,既要确保能够区分开不同的资源(正确性),又要尽量被更多的同类用户共享使用(高效率)

 

比如,若多个域名下不同的资源,使用了相同的URL,那么你仅用URL作为key,一定会导致缓存被错误地共享使用。再比如,如果你把客户端的IP地址也写进了key中,那么不同的客户端就无法共享缓存,导致缓存的利用率大幅下降。再比如,若你的服务器同时提供HTTP和 HTTPS服务,那么你将$scheme写入key中,就会导致HTTP和HTTPS不能使用对方产生的缓存。

 

在Nginx的HTTP反向代理中,可以使用proxy_cache_key指令配置缓存的key关键字,其中值可以使用变量,比如:

proxy_cache_key $proxy_host$request_uri;


其他反向代理也有类似的配置(比如python语言的ngx_http_uwsgi_module反向代理模块中,使用相似的uwsgi_cache_key指令完成这一功能)。

 

其次,我们要决定缓存对应的文件放置在磁盘中的哪个位置,这可以通过proxy_cache_path指令完成,例如:

proxy_cache_path /data/nginx/cache levels=2:2 keys_zone=one:10m;


这行指令要求Nginx把缓存文件放在/data/nginx/cache目录下。注意,1个目录中不能存放太多的文件(会影响性能),所以levels选项要求Nginx使用2级缓存子目录,其中每级目录用文件的MD5摘要中2个字符命名。

 

虽然缓存内容是存放在磁盘中的,但为了加快访问速度,Nginx会在共享内存中为缓存建立红黑树索引,这可以为多个worker进程加快处理速度。keys_zone选项定义了共享内存的大小和名称,其中每1MB的共享内存,大约可以存放8千个key关键字

 

定义好了缓存及索引的存放方式后,还需要定义究竟缓存哪些资源。比如,404错误码表示找不到资源,302错误码表示临时重定向,这样的响应是否需要缓存呢?这就是具体情况具体分析,如果访问频率非常高,而且这些错误一时无法恢复,那么缓存它们也是个不错的选择。

 

通过proxy_cache_valid指令可以设置缓存哪些响应码,并可以设置缓存的有效时间,如下所示:

proxy_cache_valid 200 302 10m;


你可能在想,刚刚才说过源服务器会定义缓存的过期时间,为何代理服务器Nginx要多此一举,重新定义过期时间呢?这是因为Nginx的这个过期时间,是用于兜底的,它的优先级很低,仅针对不含有这些过期时间头部的响应。当上游源服务器的响应中含有过期时间时,会优先使用响应中的时间。

 

而且,响应头部中同时出现多个过期时间,Nginx还定义了不同的优先级,比如:Nginx私有的X-Accel-Expires头部优先级最高,其次才是RFC规范中定义的Expires和Cache-Control头部。另外,对于含有Set-Cookie头部的响应,出于安全性考虑,Nginx默认是不会缓存的,包括Vary: *头部也是如此,如果你希望缓存它们,那么可以通过proxy_ignore_headers指令实现。

 

这里还要注意,proxy_cache_valid指令中的时间是必填项,当只填写了时间时,proxy_cache_valid默认会缓存以下3个错误码的响应:200、301、302

 

在定义好了缓存的处理方式后,最后还得通过proxy_cache指令,告诉Nginx针对哪些域名(在server{ }内配置)、URL(在location{ }内配置)才缓存请求。proxy_cache指令非常简单,指定proxy_cache指令中keys_zone选项的共享内存名即可,比如:

proxy_cache one;


至此,我们就配置好了可以基本工作的Nginx缓存。虽然此时的缓存已经能够大幅度提升性能,但Nginx在缓存失效后,还无法在回源请求中携带etag缓存摘要,自然也无法享受到源服务器返回304空包体响应带来的好处了。你还需要通过proxy_cache_revalidate指令打开这一功能,如下所示:

proxy_cache_revalidate on;


小结

本文介绍了Nginx缓存的工作流程,以及如何配置一个基础版的共享缓存。

 

首先,Nginx作为代理服务器,它提供的是效率更高的共享缓存。因此,我们要通过proxy_cache_key指令,通过Nginx变量选定合适的缓存关键字,既要保证在功能上有正确的区分度,也要在效率上可以为同类用户共享使用。

 

其次,缓存内容存放在磁盘文件中,而为了加快处理速度,Nginx还在共享内存中建立了红黑树索引。通过proxy_pass指令,可以设定磁盘文件的存储方式,以及共享内存的名称及大小。

 

最后,服务器会通过响应头部,预估缓存的过期时间。而在Nginx中,proxy_cache_valid既可以设置通用的、优先级较低的过期时间,也可以缓存非200错误码的响应。当然,缓存过期后,打开proxy_cache_revalidate功能还可以减少源服务器的数据传输量。

 

篇幅所限,我将在下一篇文章中,再来讨论Nginx缓存的进阶功能。比如,如何设置缓存的最大容量,如何在Nginx重启后控制cache loader进程对CPU资源的消耗,如何控制回源请求的数量,如何使用过期缓存提供降级服务等等。

 

最后,留给你一道思考题,你知道Nginx的性能非常高,那么它在实现层面是怎样淘汰过期缓存的吗?欢迎你在留言中与大家一起分享你的看法。

 

感谢阅读,如果你觉得这篇文章对你有一些启发,也欢迎你把它分享给你的朋友。

 


如果您觉得不错,就打赏支持一下吧〜
已有 1 人进行打赏
  • 阿尔巴_672
点击标签,发现更多精彩
发表评论
  • Radio

    谢谢

    2020-06-15 15:32
    2
    回复
  • Nginspher

    实现层面是怎样淘汰过期缓存的吗? 

    通过LRU(最近最少使用算法)进行缓存淘汰政策 

    2020-09-23 14:43
    2
    回复
  • Nginspher

    陶老师你好:

    “比如,在超大流量下如果热点资源的缓存失效,那么在巨大的流量穿透Nginx缓存后,非常有可能把脆弱的上游服务打挂。此时合并回源请求功能,就是你的最佳应对手段!

    (回源合并是当有多个请求时仅一个回源后端服务器,然后建立缓存后响应客户端,,来避免缓存穿透给后端带来大量的流量,进而导致后端响应缓慢或不可用)


    再比如,在源服务器暂时不可用时,使用失效过期的缓存,为用户提供有限的服务,可以通过降级体验来提升系统的健壮性,这要比给用户返回“系统暂时不可用”要好得多。(使用失效缓存也可以避免缓存穿透和大量回源(合并回源)是吗?)

    再比如,Nginx重启后,需要为磁盘上大量的缓存文件,在共享内存中建立起索引,这一过程可能很漫长,我们必须防止它对正常的服务产生过大的影响,降低用户体验。  ”(这个怎么避免?通过部分加载?)

    2020-09-23 14:51
    3
    回复
  • 陶辉 回复 Nginspher

    FIFO

    2020-09-23 15:24
    4
    回复
发表者

陶辉

暂无个人介绍

  • 15

    文章

  • 1

    关注

  • 86

    粉丝

活动推荐
版权所有©F5 Networks,Inc.保留所有权利。京ICP备16013763号-5