协商缓存在nginx的应用与实践

1、前言

缓存是一个高效减轻网络与服务器压力的机制,具有减少冗余数据传输、缓解网络瓶颈以及降低时延等优点。通常客户端在请求数据时,会发送请求到原始服务器获取,重复的数据可能会在网络中多次传输,但是如果有缓存,客户端就可以直接从缓存中获取数据,减少重复的流量。例如在浏览器首次请求某些静态资源时,状态码会是200 ok,但是刷新页面,状态码就会变为200 ok ( from memory cache),这是因为浏览器对这些资源进行了缓存,客户端的数据并不是发送请求到原始服务器获取的,而是从缓存中获取的。

缓存流程图

但是问题也是显而易见的,如果原始服务器的数据发生了改变,而缓存并没有及时更新数据,在客户端请求时返回了过期的数据,这就会导致了数据的不准确。已缓存的数据应当与原始服务器的数据保持一致,更准确的来说,是缓存返回的数据应当与原始服务器的数据保持一致。那么如何在缓存的基础上,避免这个问题呢?事实上,HTTP协议是提供了多种机制来保证数据一致性的。

2、“使用期”与“新鲜度“

使用期是指数据在服务器响应返回后的总时间,可以简单理解为数据在缓存使用的时间,从服务器将数据发出去开始计时;新鲜度是指数据在服务器响应发出去后,缓存可以使用的时间。如果使用期小于新鲜度,说明数据是“新鲜的”,缓存可以继续使用。反之,缓存需要判断数据是否发生了更新,是否需要重新拉取数据,如何更新这取决于服务端采用的HTTP缓存策略。

使用期:

服务器用HTTP协议的响应头部date表示发送数据时的时间,如果客户端与服务端使用同样的、完全精确的时钟,已缓存数据的使用期(data_age)就可以是当前时间(current_time)减去服务器发送数据时(Date_header_value)的时间。

data_age = current_time – date_header_value

但是并不是所有的计算机都实现了时钟同步,当服务器和客户端的时钟不同步时,使用期可能是很大或者甚至是负的,如果是负的,就需要将其设置为零。

data_age = max(0,current_time – date_header_value)

date_header_value的值代表着原始服务器发出数据的时间,所以在经过代理时,一定不能进行修改。

新鲜度:

1
2
3
Expires : Fri, 09 Sep 2022, 05:27:57 GMT

Cache-Control : max-age=3600

服务器用HTTP/1.0+的Expires或HTTP/1.1的Cache-Control:max-age响应头部指定数据的过期时间。Expires指定的是绝对时间,即数据到这个时间就过期了。而Cache-Control:max-age指定的是相对时间,表示缓存收到数据后可以在缓存存活的时间。由于Expires依靠于时钟的准确性,因此目前更多的使用后者。

通过比对使用期与新鲜度,缓存可以判断当前存储的数据是否足够新鲜,如果足够新鲜,则直接返回缓存中的数据,不然就只能重新从原始服务器拉取数据。但是数据在缓存中已经过期,而原始服务器中并未发生更新,缓存依旧需要发送请求获取数据,这会消耗大量不必要的网络资源。对于网络传输而言,应当遵守以最小数据量传输而保证最大信息量传输的原则,因此为了减少冗余数据的传输,HTTP协议提供了协商缓存机制,用以减少数据的传输。

使用期与新鲜度

3、协商缓存:no-store、no-cache与must-revalidate

1
2
3
4
5
6
7
8
9
10
11
HTTP/1.0:

Pragma: no-cache

HTTP/1.1:

Cache-Control: no-store

Cache-Control: no-cache

Cache-Control: must-revalidate

服务器可以通过no-store来禁止缓存对数据进行存储,因此每次客户端请求数据时,缓存都需要发送请求到原始服务器获取,这样就可以保证客户端获取数据的新鲜度。

no-cache与no-store不同,no-cache允许缓存对数据进行存储,缓存需要在原始服务器验证新鲜度之后,才能将数据返回给客户端。通常,如果数据发生了更新,原始服务器会返回更新后的数据;反之,会返回304,表示缓存的数据并没有发生改变,可以把缓存的数据返回给客户端。

而must-revalidate与no-cache类似,同样允许缓存对数据进行存储。如果缓存的数据过期,则must-revalidate与no-cache的行为一致;但是如果数据未过期,则可以直接返回给客户端数据而无需验证。因此must-revalidate通常需要与Expires、max-age进行配合使用。例如:

Cache-Control的使用

现在让我们总结一下这3种缓存机制的特点:

no-store:缓存不可以存储数据,每次请求都需要到服务器获取数据,因此可以保证数据的新鲜度,但是大量冗余数据的传输,会增大网络与服务器的压力,降低系统的整体性能。

no-cache:缓存可以存储数据,每次请求都需要到服务器进行新鲜度的验证,因此可以保证数据的新鲜度。相比于no-store,减少了大量数据的传输。

must-revalidate:缓存可以存储数据,如果数据过期,则需要到服务器进行新鲜度的验证,反之,则可以直接返回给客户端数据。相比于no-cache,减少一定数量的新鲜度验证请求,进一步减少网络与服务器的压力,但是不能保证数据的新鲜度,有一定时间的误差,这取决于新鲜度的设置。

3种机制各有优劣,应该根据具体的业务需求选择合适的缓存机制,但整体来看,no-cache适合绝大部分的场景。

3.1、no-cache验证新鲜度:if-modified-since与if-none-match

当原始服务器采用no-cache缓存模式时,缓存请求数据,服务器的响应会返回响应头Last-Modified与Etag。Last-Modified表示原始服务器修改该数据的最后时间,Etag是一个字符串,不同的系统生成方式也是不同的。缓存在验证新鲜度时,会将这2个值通过2个请求头If-Modified-Since与If-None-Match传送到原始服务器,而原始服务器通过比对这2个值,就可以判断缓存的数据与本地数据是否一致,也就可以决定是否需要返回数据。

响应:

1
2
3
Last-Modified : Tue,06 Sep 2022 03:09:17 GMT

ETag : “6316b9dd-2b1ce”

请求:

1
2
3
If-Modified-Since : Tue,06 Sep 2022 03:09:17 GMT

If-None-Match : “6316b9dd-2b1ce”

4、协商缓存在nginx的应用

4.1、应用no-cache对前端的优化

nginx提供流量分发、协议转换、静态资源代理等功能。本节以HUI前端为例,围绕静态资源代理这一功能,分析nginx何应用no-cache优化前端。

可以对协商缓存进行设置,最后落地到配置文件nginx.conf,具体配置如下图所示,可以设置单个文件采用协商缓存模式如: sysconfig.js;也可以根据文件类型后缀设置协商缓存模式如:html或js。

cache-4

首先在浏览器访问前端,可以看到首次访问时,静态资源的状态码是200 ok,这代表数据是从服务器获取到的。

cache-5

接着刷新界面,可以看到状态码变为304 Not Modified,nginx的日志信息也可以看到客户端是发起了一次请求到nginx获取数据,判断到数据并未发生更新返回304。

cache-6

cache-7

此时,如果对sysconfig.js进行修改,再次刷新界面,如下图所示,可以看到状态码变为200 ok,修改的内容及时返回到了客户端。

修改前:

cache-8

修改:

cache-9

接着刷新界面,可以看到状态码变为200 ok,而且数据已经更新:

cache-10

cache-11

这样既可以保证数据的及时更新,又可以减少大量数据的传输,唯一的网络开销是进行新鲜度的再次验证。

4.2、nginx源码分析If-Modified-Since与If-None-Match

etag的生成

nginx的etag的生成方式比较简单,由last-modified_time与content_length_n转换为十六进制组合而成。其中last-modified_time是通过系统调用,获取的文件最后修改时间,对应操作系统文件结构stat中st_mtime; content-length_n是文件的大小,对应操作系统文件结构stat中st_size;

cache-12

缓存请求数据时,在响应头返回给缓存。

cache-13

If-Modified-SinceIf-None-Match的校验

缓存向nginx验证数据新鲜度时,需要携带If-Modified-Since与If-None-Match请求头。

cache-14

nginx在判断缓存数据的新鲜度时,会先后对If-Modified-Since和If-None-Match与当前数据的last-modified和etag进行比对,只要2者有1个发生了改变,则判断本地数据发生更新,缓存中的数据已过期,就会直接返回更新后的数据,如果都没有变,则会返回304。源码与流程图如下。

cache-15

cache-16

5、实践出真知

5.1、抓包分析-协商缓存验证新鲜度

1、首先用nginx代理一个js文件,浏览器第一次请求资源时,状态码为200 ok,刷线界面,状态码变为304,如下图

cache-17

为了兼容http1.0,pragma也设置了no-cache

cache-18

cache-19

2、抓包查看第一次请求的网络包,可以看到服务端返回了静态资源的数据

cache-20

3、查看第二次请求,可以看到服务端没有返回任何静态资源,只有响应头这些数据

cache-21

4、符合协商缓存的现象

当请求的If-Modified-Since、If-None-Match与服务端不一致时,服务端会返回静态资源

cache-22

但是当请求的If-Modified-Since、If-None-Match与服务端一致时,服务端验证新鲜度足够,就只会返回304

cache-23

5.2、一个GET请求被缓存导致的登录异常

首先来看下定义:

在HTTP规范中,GET请求通常用于从服务器检索数据,而不改变服务器的状态。这种操作被认为是安全的和幂等的,因此响应是可以缓存的。

HTTP规范规定,POST请求是用来提交数据的,可能会导致服务器状态的改变,因此其响应不应被缓存。

我们的网关层实现了cas单点登录,其中登录过程会调用一个免密接口,主要用于从权限系统获取权限数据以及一些会话数据,这个请求是get请求。

问题:当登录成功以后,点击浏览器的回退,这时由于浏览器的TGC没有过期,照理应该重新登录进系统,但是登录后发现,页面空白,请求出现401权限丢失的情况,我发现浏览器并没有真实发起免密接口的调用,而是获取了缓存的数据,这时问题就很明确了。

cache-24

1、接口的设计不好,这个接口会改变服务器的状态,应该设计为post,但是被错误的设计为get

2、如果是get也可以兼容,这个接口在返回响应时,增加no-store的响应头,禁止前端缓存,需要注意的是,这个时候用no-cache是不行的。

照理,接口改为post是最正确的做法,但是现实是要考虑兼容的问题,我们网关层也是需要做修改的。

6、总结

协商缓存不只是一种简单的缓存机制,更是一种很好的理念。对于客户端与服务端数据同步、性能优化都是很好的借鉴,尤其是服务端不能主动向客户端发送请求的场景。例如当有服务以域名的形式注册到nginx时,nginx需要向DNS查询真实ip,为了避免每次请求都会向DNS查询,会对查询到的结果进行缓存,并启动定时器查询真实ip是否发生改变。而定时器的时间就类似于协商缓存的新鲜度,在实际的生产中没有完美的方案,因此需要根据具体的需求偏重来调整可靠性与性能。

本文从HTTP缓存原理出发,介绍了缓存对系统性能优化的意义,并讲解了HTTP缓存发展过程中存在冗余数据多次传输的问题,以及为了解决这个问题而出现的协商缓存机制。通过对协商缓存的原理与nginx实现协商缓存的源码分析,希望大家可以对HTTP缓存有一定的理解。


协商缓存在nginx的应用与实践
https://zjfans.github.io/2024/04/12/协商缓存在nginx的应用与实践/
作者
张三疯
发布于
2024年4月12日
许可协议