万字长文聊缓存(上)
深入解析 SpringMVC 核心原理:从手写简易版 MVC 框架开始(SmartMvc) : https://github.com/silently9527/SmartMvc
IDEA 多线程文件下载插件: https://github.com/silently9527/FastDownloadIdeaPlugin
公众号:贝塔学 JAVA
摘要
缓存的目的是为了提高系统的访问速度,让数据更加接近于使用者,通常也是提升性能的常用手段。缓存在生活中其实也是无处不在,比如物流系统,他们基本上在各地都有分仓库,如果本地仓库有数据,那么送货的速度就会很快;CPU 读取数据也采用了缓存,寄存器->高速缓存->内存->硬盘/网络;我们经常使用的 maven 仓库也同样有本地仓库和远程仓库。现阶段缓存的使用场景也越来越多,比如:浏览器缓存、反向代理层缓存、应用层缓存、数据库查询缓存、分布式集中缓存。
本文我们就先从浏览器缓存和 Nginx 缓存开始聊起。
浏览器缓存
浏览器缓存是指当我们去访问一个网站或者 Http 服务的时候,服务器可以设置 Http 的响应头信息,其中如果设置缓存相关的头信息,那么浏览器就会缓存这些数据,下次再访问这些数据的时候就直接从浏览器缓存中获取或者是只需要去服务器中校验下缓存时候有效,可以减少浏览器与服务器之间的网络时间的开销以及节省带宽。
Htpp 相关的知识,欢迎去参观 《面试篇》Http协议
Cache-Control
该命令是通用首部字段(请求首部和响应首部都可以使用),用于控制缓存的工作机制,该命令参数稍多,常用的参数:
no-cache: 表示不需要缓存该资源
max-age(秒): 缓存的最大有效时间,当 max-age=0 时,表示不需要缓存
Expires
控制资源失效的日期,当浏览器接受到Expires
之后,浏览器都会使用本地的缓存,在过期日期之后才会向务器发送请求;如果服务器同时在响应头中也指定了Cache-Control
的max-age
指令时,浏览器会优先处理max-age
。
如果服务器不想要让浏览器对资源缓存,可以把Expires
和首部字段Date
设置相同的值
Last-Modified / If-Modified-Since
Last-Modified
Last-Modified
用于指明资源最终被修改的时间。配合If-Modified-Since
一起使用可以通过时间对缓存是否有效进行校验;后面实战会使用到这种方式。
If-Modified-Since
如果请求头中If-Modified-Since
的日期早于请求资源的更新日期,那么服务会进行处理,返回最新的资源;如果If-Modified-Since
指定的日期之后请求的资源都未更新过,那么服务不会处理请求并返回304 Mot Modified
的响应,表示缓存的文件有效可以继续使用。
实战事例
使用 SpringMVC 做缓存的测试代码:
当第一次访问
http://localhost:8080/http/cache
的时候,我们可以看到如下的响应头信息:
前面我们已提到了Cache-Control
的优先级高于Expires
,实际的项目中我们可以同时使用,或者只使用Cache-Control
。Expires
的值通常情况下都是系统当前时间+缓存过期时间
当我们在 15 秒之内再次访问
http://localhost:8080/http/cache
会看到如下的请求头:
此时发送到服务器端的头信息If-Modified-Since
就是上次请求服务器返回的Last-Modified
,浏览器会拿这个时间去和服务器校验内容是否发送了变化,由于我们后台程序在 15 秒之内都表示没有修改过内容,所以得到了如下的响应头信息
响应的状态码 304,表示服务器告诉浏览器,你的缓存是有效的可以继续使用。
If-None-Match / ETag
If-None-Match
请求首部字段If-None-Match
传输给服务器的值是服务器返回的 ETag 值,只有当服务器上请求资源的ETag
值与If-None-Match
不一致时,服务器才去处理该请求。
ETag
响应首部字段ETag
能够告知客服端响应实体的标识,它是一种可将资源以字符串的形式做唯一标识的方式。服务器可以为每份资源指定一个ETag
值。当资源被更新时,ETag
的值也会被更新。通常生成ETag
值的算法使用的是 md5。
强 ETag 值:不论实体发生了多么细微的变化都会改变其值
弱 ETag 值:只用于提示资源是否相同,只有当资源发送了根本上的变化,ETag 才会被改变。使用弱 ETag 值需要在前面添加
W/
通常建议选择弱 ETag 值,因为大多数时候我们都会在代理层开启 gzip 压缩,弱 ETag 可以验证压缩和不压缩的实体,而强 ETag 值要求响应实体字节必须完全一致。
实战事例
ETag 是用于发送到服务器端进行内容变更验证的,第一次请求http://localhost:8080/http/etag
,获取到的响应头信息:
在 30 秒之内,我们再次刷新页面,可以看到如下的请求头信息:
这里的If-None-Match
就是上一次请求服务返回的ETag
值,服务器校验If-None-Match
值与ETag
值相等,所以返回了 304 告诉浏览器缓存可以用。
ETag 与 Last-Modified 两者应该如何选择?
通过上面的两个事例我们可以看出ETag
需要服务器先查询出需要响应的内容,然后计算出 ETag 值,再与浏览器请求头中If-None-Match
来比较觉得是否需要返回数据,对于服务器来说仅仅是节省了带宽,原本应该服务器调用后端服务查询的信息依然没有被省掉;而Last-Modified
通过时间的比较,如果内容没有更新,服务器不需要调用后端服务查询出响应数据,不仅节省了服务器的带宽也降低了后端服务的压力;
对比之后得出结论:**通常来说为了降低后端服务的压力ETag
适用于图片/js/css 等静态资源,而类似用户详情信息需要调用后端服务的数据适合使用Last-Modified
来处理**。
Nginx
通常情况下我们都会使用到 Nginx 来做反向代理服务器,我们可以通过缓冲、缓存来对 Nginx 进行调优,本篇我们就从这两个方面来聊聊 Nginx 调优
缓冲
默认情况下,Nginx 在返回响应给客户端之前会尽可能快的从上游服务器获取数据,Nginx 会尽可能的将上有服务器返回的数据缓冲到本地,然后一次性的全部返回给客户端,如果每次从上游服务器返回的数据都需要写入到磁盘中,那么 Nginx 的性能肯定会降低;所以我们需要根据实际情况对 Nginx 的缓存做优化。
proxy_buffer_size
: 设置 Nginx 缓冲区的大小,用来存储 upstream 端响应的 header。proxy_buffering
: 启用代理内容缓冲,当该功能禁用时,代理一接收到上游服务器的返回就立即同步的发送给客户端,proxy_max_temp_file_size
被设置为 0;通过设置proxy_buffering
为 on,proxymaxtemp_file_size
为 0 可以确保代理的过程中不适用磁盘,只是用缓冲区; 开启后proxy_buffers
和proxybusybuffers_size
参数才会起作用proxy_buffers
: 设置响应上游服务器的缓存数量和大小,当一个缓冲区占满后会申请开启下一个缓冲区,直到缓冲区数量到达设置的最大值proxy_busy_buffers_size
:proxy_busy_buffers_size
不是独立的空间,他是proxy_buffers
和proxybuffersize
的一部分。nginx 会在没有完全读完后端响应就开始向客户端传送数据,所以它会划出一部分 busy 状态的 buffer 来专门向客户端传送数据(建议为proxy_buffers
中单个缓冲区的 2 倍),然后它继续从后端取数据。
proxy_busy_buffer_size
参数用来设置处于 busy 状态的 buffer 有多大。
1)如果完整数据大小小于 busy_buffer 大小,当数据传输完成后,马上传给客户端;
2)如果完整数据大小不小于 busybuffer 大小,则装满 busybuffer 后,马上传给客户端;
Nginx 代理缓冲的设置都是作用到每一个请求的,想要设置缓冲区的大小到最佳状态,需要测量出经过反向代理服务器器的平均请求数和响应的大小;proxy_buffers
指令的默认值 8 个 4KB 或者 8 个 8KB(具体依赖于操作系统),假如我们的服务器是 1G,这台服务器只运行了 Nginx 服务,那么排除到操作系统的内存使用,保守估计 Nginx 能够使用的内存是 768M
每个活动的连接使用缓冲内存:8 个 4KB = 8 4 1024 = 32768 字节
系统可使用的内存大小 768M: 768 1024 1024 = 805306368 字节
所以 Nginx 能够同时处理的连接数:805306368 / 32768 = 24576
经过我们的粗略估计,1G 的服务器只运行 Nginx 大概可以同时处理 24576 个连接。
假如我们测量和发现经过反向代理服务器响应的平均数据大小是 900KB , 而默认的 8 个 4KB 的缓冲区是无法满足的,所以我们可以调整大小
这样设置之后每次请求可以达到最快的响应,但是同时处理的连接数减少了,(768 * 1024 * 1024) / (30 * 32 * 1024)
=819 个活动连接;
如果我们系统的并发数不是太高,我们可以将proxy_buffers
缓冲区的个数下调,设置稍大的proxy_busy_buffers_size
加大往客户端发送的缓冲区,以确保 Nginx 在传输的过程中能够把从上游服务器读取到的数据全部写入到缓冲区中。
缓存
Nignx 除了可以缓冲上游服务器的响应达到快速返回给客户端,它还可以是实现响应的缓存,通过上图我们可以看到
1A: 一个请求到达 Nginx,先从缓存中尝试获取
1B: 缓存不存在直接去上游服务器获取数据
1C: 上游服务器返回响应,Nginx 把响应放入到缓存
1D: 把响应返回到客户端
2A: 另一个请求达到 Nginx, 到缓存中查找
2B: 缓存中有对应的数据,直接返回,不去上游服务器获取数据
Nginx 的缓存常用配置:
proxy_cache_path
: 放置缓存响应和共享的目录。levels
设置缓存文件目录层次, levels=1:2 表示两级目录,最多三层,其中 1 表示一级目录使用一位 16 进制作为目录名,2 表示二级目录使用两位 16 进制作为目录名,如果文件都存放在一个目录中,文件量大了会导致文件访问变慢。keys_zone
设置缓存名字和共享内存大小,inactive
当被放入到缓存后如果不被访问的最大存活时间,max_size
设置缓存的最大空间proxy_cache
: 定义响应应该存放到哪个缓存区中(keys_zone
设置的名字)proxy_cache_key
: 设置缓存使用的 Key, 默认是完整的访问 URL,可以自己根据实际情况设置proxy_cache_lock
: 当多个客户端同时访问一下 URL 时,如果开启了这个配置,那么只会有一个客户端会去上游服务器获取响应,获取完成后放入到缓存中,其他的客户端会等待从缓存中获取。proxy_cache_lock_timeout
: 启用了proxy_cache_lock
之后,如果第一个请求超过了proxy_cache_lock_timeout
设置的时间默认是 5s,那么所有等待的请求会同时到上游服务器去获取数据,可能会导致后端压力增大。proxy_cache_min_uses
: 设置资源被请求多少次后才会被缓存proxy_cache_use_stale
: 在访问上游服务器发生错误时,返回已经过期的数据给客户端;当缓存内容对于过期时间不敏感,可以选择采用这种方式proxy_cache_valid
: 为不同响应状态码设置缓存时间。如果设置proxy_cache_valid 5s
,那么所有的状态码都会被缓存。
设置所有的响应被缓存后最大不被访问的存活时间 6 小时,缓存的大小设置为 1g,缓存的有效期是 1 天,配置如下:
如果当前响应中设置了 Set-Cookie 头信息,那么当前的响应不会被缓存,可以通过使用proxy_ignore_headers
来忽略头信息以达到缓存
如果这样做了,我们需要把 cookie 中的值作为proxy_cache_key
的一部分,防止同一个 URL 响应的数据不同导致缓存数据被覆盖,返回到客户端错误的数据
注意,这种情况还是有问题,因为在缓存的 key 中添加 cookie 信息,那么可能导致公共资源被缓存多份导致浪费空间;要解决这个问题我们可以把不同的资源分开配置,比如:
清理缓存
虽然我们设置了缓存加快了响应,但是有时候会遇到缓存错误的请求,通常我们需要为自己开一个后面,方便发现问题之后通过手动的方式及时的清理掉缓存。Nginx 可以考虑使用ngx_cache_purge
模块进行缓存清理。
该方法要限制访问权限; proxy_cache_purge
缓存清理的模块,cache_one
指定的 key_zone,$host$1$is_args$args
指定的生成缓存 key 的参数
存储
如果有大的静态文件,这些静态文件基本不会别修改,那么我们就可以不用给它设置缓存的有效期,让 Nginx 直接存储这些文件直接。如果上游服务器修改了这些文件,那么可以单独提供一个程序把对应的静态文件删除。
请求首先会去/img
中查找文件,如果不存在再去上游服务器查找;internal
指令用于指定只允许来自本地 Nginx 的内部调用,来自外部的访问会直接返回 404 not found 状态。proxy_store
表示需要把从上游服务器返回的文件存储到 /var/www/data
; proxystoreaccess
设置访问权限
总结
Cache-Control
和Expires
设置资源缓存的有效期使用
Last-Modified / If-Modified-Since
判断缓存是否有效使用
If-None-Match / ETag
判断缓存是否有效通过配置 Nginx 缓冲区大小对 Nginx 调优
使用 Nginx 缓存加快请求响应速度
如何加快请求响应的速度,本篇我们主要围绕着 Http 缓存和 Nignx 反向代理两个方面来聊了缓存,你以为这样就完了吗,不!下一篇我们将从应用程序的维度来聊聊缓存
写到最后 点关注,不迷路
文中或许会存在或多或少的不足、错误之处,有建议或者意见也非常欢迎大家在评论交流。
最后,白嫖不好,创作不易,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏
公众号:贝塔学 JAVA
版权声明: 本文为 InfoQ 作者【Silently9527】的原创文章。
原文链接:【http://xie.infoq.cn/article/72f349ff9bd0b60debf34174f】。文章转载请联系作者。
评论