HTTP缓存的深入介绍:Cache-Control和Vary

祁兴运
2023-12-01

简介-本文范围 (Introduction - scope of the article)

This series of articles deals with caching in the context of HTTP. When properly done, caching can increase the performance of your application by an order of magnitude. On the contrary, when overlooked or completely ignored, it can lead to some very unwanted side effects caused by misbehaving proxy servers that, in the absence of clear caching instructions, decide to cache anyway and serve stale resources.

本系列文章讨论了HTTP上下文中的缓存。 正确完成后,缓存可以将应用程序的性能提高一个数量级。 相反,如果忽略或完全忽略它,则可能导致代理服务器行为异常,从而导致某些非常有害的副作用,这些代理服务器在没有明确的缓存指令的情况下仍决定缓存并提供过时的资源。

In the first part of this series, we argued that caching is the most effective way to increase performance, when measured by the page load time. In this second part, it is time to shift our focus to the mechanisms at our disposal. To put it in another way: how does HTTP caching actually work?

在本系列的第一部分中,我们认为,以页面加载时间衡量,缓存是提高性能的最有效方法。 在第二部分中,是时候将我们的重点转移到我们掌握的机制上了。 换句话说:HTTP缓存实际上如何工作?

To answer this question, we decided to consider the case of an empty cache that starts progressively caching and serving resources. As it gradually receives incoming HTTP requests, our cache will start behaving accordingly. Serving the resource from the cache when a fresh copy is available, varying over multiple representations, making a conditional request... This way, we can introduce each concept progressively as we need it.

为了回答这个问题,我们决定考虑空缓存的情况,该缓存开始逐渐缓存和提供资源。 随着它逐渐接收传入的HTTP请求,我们的缓存将相应地开始工作。 当有可用的新副本时,从缓存中提供资源,以多种表示形式变化,发出条件请求...这样,我们可以根据需要逐步引入每个概念。

At first, our empty cache will have no choice but to forward requests to the origin server. This will allow us to understand how origin servers instruct our cache on what to do with the resource, such as if it is allowed to store it, and for how long. For this, we will examine each Cache-Control directive and clarify some of them that have been known to have conflicting meanings.

首先,我们的空缓存除了将请求转发到原始服务器外别无选择。 这将使我们了解原始服务器如何指示缓存如何处理资源,例如是否允许存储资源以及存储时间。 为此,我们将检查每个Cache-Control指令,并弄清其中一些已知的含义冲突

Second, we will look at what happens when our cache receives a request for a resource it already knows. How does our cache decide if it can re-use a previously stored response? How does it map a given HTTP request to a particular resource? To answer these, we will learn about representation variations with the Vary header.

其次,我们将看看当缓存接收到对它已经知道的资源的请求时会发生什么。 我们的缓存如何决定是否可以重用先前存储的响应? 如何将给定的HTTP请求映射到特定资源? 为了回答这些问题,我们将了解Vary标头的表示形式变化。

This article is going to focus on knowledge that’s the most valuable from a web developer’s perspective. Therefore, conditional requests are only discussed briefly and will be the focus of another article.

从Web开发人员的角度出发,本文将重点介绍最有价值的知识。 因此,仅简要讨论条件请求,这将是另一篇文章的重点。

Without further ado, let us start with an overview of what we will be exploring.

事不宜迟,让我们首先概述我们将要探索的内容。

HTTP缓存决策树 (The HTTP caching decision tree)

Conceptually, a cache system always involve at least three participants. With HTTP, these participants are the client, the server, and the caching proxy.

从概念上讲,高速缓存系统始终包含至少三个参与者。 使用HTTP,这些参与者是客户端,服务器和缓存代理。

However, when learning about HTTP caching, we strongly encourage you not to think of the client as your typical web browser because these days, they all ship with their own HTTP caching layer. It makes it difficult to clearly separate the browser from the cache. For this reason, we invite you to think of the client as a headless command line program such as cURL or any application without an embedded HTTP cache.

但是,在学习HTTP缓存时,我们强烈建议您不要将客户端视为典型的Web浏览器,因为如今,它们都带有自己的HTTP缓存层。 这使得很难将浏览器与缓存区分开来。 因此,我们邀请您将客户端视为无头命令行程序,例如cURL或任何没有嵌入式HTTP缓存的应用程序。

All precautions aside, let us now deep dive into the subject by taking a look at the following picture: the HTTP caching decision tree.

除了所有预防措施,现在让我们通过查看以下图片深入探讨该主题:HTTP缓存决策树。

This picture illustrates all the possible paths a request can take every time a client asks for a resource to an origin server behind a caching system. A careful examination of this illustration reveals that there are only four possible outcomes.

此图说明了每次客户端向缓存系统后面的原始服务器请求资源时,请求可以采用的所有可能路径。 仔细检查该图可以发现只有四个可能的结果。

Clearly separating these outcomes in our minds is actually very convenient, seeing as each important caching concept (cache instructions, representation matching, conditional requests and resource aging) maps to each one of them.

在我们的脑海中清楚地区分这些结果实际上非常方便,因为每个重要的缓存概念(缓存指令,表示匹配,条件请求和资源老化)都映射到它们中的每一个。

Let us describe succinctly each one by introducing two important terms relating to the HTTP caching terminology: cache hits and cache misses.

让我们通过介绍与HTTP缓存术语有关的两个重要术语来简要地描述每个:缓存命中和缓存未命中。

命中率 (Hits and misses)

The first possible outcome is when the cache finds a matching resource, and is allowed to serve it, which, in the caching world, are indeed two distinct things. This outcome is what we commonly call a cache hit, and is the reason why we use caches in the first place.

第一个可能的结果是,当高速缓存找到匹配的资源并被允许为其提供服务时,在缓存世界中,这确实是两个截然不同的事情。 这个结果就是我们通常所说的缓存命中,也是我们首先使用缓存的原因。

When a cache hit happens, it completely offloads the origin server and the latency is dramatically reduced. In fact, when the cache hit happens in the browser’s HTTP cache latency is null and the requested resource is instantly available.

当发生缓存命中时,它将完全减轻原始服务器的负担,并大大减少了延迟。 实际上,当缓存命中发生在浏览器的HTTP缓存中时,等待时间为null,并且所请求的资源立即可用。

Unfortunately, cache hits account only one of the four possible outcomes. The rest of them fall into the second category, also known as cache misses, which can happen for only three reasons.

不幸的是,缓存命中仅占四个可能结果之一。 它们的其余部分属于第二类,也称为缓存未命中,仅出于三个原因才可能发生。

The first reason a cache miss typically happens is simply when the cache does not find any matching resource in its storage. This is usually a sign that the resource has never been requested before, or has been evicted from the cache to free up some space. In such cases, the proxy has no choice but to forward the request to the origin server, fully download the response and look for caching instructions in the response headers.

高速缓存未命中通常发生的第一个原因仅仅是当高速缓存在其存储中找不到任何匹配的资源时。 这通常表明该资源以前从未被请求过,或者已从高速缓存中逐出以释放一些空间。 在这种情况下,代理别无选择,只能将请求转发到原始服务器,完全下载响应并在响应头中查找缓存指令。

The second reason a cache miss can happen is actually just as detrimental, where the cache detects a matching representation, one that it could potentially use. However, the resource is not considered to be fresh anymore - we will see how exactly in the cache-control section of this article - but is said to be stale.

高速缓存未命中可能发生的第二个原因实际上同样有害,因为高速缓存检测到匹配表示,它可能会使用该表示。 但是,该资源不再被认为是新鲜的 -我们将在本文的缓存控制部分中看到它的精确程度-但据说已经过时了。

In such case, the cache sends a special kind of request, called a conditional request to the origin server. Conditional requests allow caches to retrieve resources only if they are different from the one they have in their local storage. Since only the origin server ever has the most recent representation of a given resource, conditional requests always have to go through the whole caching proxy chain up to the origin server.

在这种情况下,缓存将一种特殊的请求(称为条件请求)发送到原始服务器。 条件请求仅允许高速缓存与本地存储中的资源不同时才检索资源。 由于只有原始服务器具有给定资源的最新表示,因此条件请求始终必须经过整个缓存代理链,直到原始服务器。

These special requests have only two possible outcomes. If the resource has not changed, the cache is instructed to use its local copy by receiving a 304 Not Modified response along with updated headers and an empty body. This outcome, the third one on our list, is called a successful validation.

这些特殊要求只有两个可能的结果。 如果资源未更改,则通过接收304 Not Modified响应以及更新的头和空主体来指示缓存使用其本地副本。 此结果是我们列表中的第三个结果,称为成功验证。

Finally, the last possible outcome is when the resource has changed. In this case, the origin server sends a normal 200 OK response, as it would if the cache was empty and had forwarded the request. To put it another way, cache misses caused by empty cache and failed validation yield exactly the same HTTP response.

最后,最后可能的结果是资源更改时。 在这种情况下,原始服务器将发送正常的200 OK响应,就像缓存为空并转发了请求一样。 换句话说,由空缓存和验证失败导致的缓存未命中会产生完全相同的HTTP响应。

To best visualize these four paths, it is helpful to picture them in a timeline, as illustrated below.

为了最好地可视化这四个路径,在时间线上显示它们很有帮助,如下所示。

At first, the cache is empty. The flow of requests starts with a cache miss (empty cache outcome). On its way back, the cache would read caching instructions and store the response. All subsequent requests for this particular resource would yield to cache hits, until the resource becomes stale and needs to be revalidated.

首先,缓存为空。 请求流以缓存未命中(空缓存结果)开始。 在返回的过程中,缓存将读取缓存指令并存储响应。 对该资源的所有后续请求都将产生缓存命中,直到该资源变得陈旧并需要重新验证为止。

Upon a first revalidation, it is possible that the resource has not changed, hence, a 304 Not Modified would be sent.

首次重新验证后,资源可能没有更改,因此将发送304未修改。

Then, the resource eventually gets updated by a client, typically with a PUT or a PATCH request. When the next conditional request arrives, the origin server detects that the resource has changed and replies a 200 OK with updated ETag and Last-Modified headers.

然后,客户端最终通常使用PUT或PATCH请求来更新资源。 当下一个条件请求到达时,原始服务器检测到资源已更改,并使用更新的ETag和Last-Modified标头答复200 OK。

Knowing about cache hits and cache misses along with the 4 possible paths that every cacheable request could take, should give you a good overview of how caching works.

了解缓存命中和缓存未命中以及每个可缓存请求可能采用的4条可能的路径后,应该可以很好地了解缓存的工作方式。

Though overviews can only get you so far. In the following section, we will give a detailed explanation of how origin servers communicate caching instructions.

尽管概述仅能带您到此为止。 在以下部分中,我们将详细说明原始服务器如何通信缓存指令。

原始服务器如何通信缓存指令 (How origin servers communicate caching instructions)

Origin servers communicate their caching instructions to downstream caching proxies by adding a Cache-Control header to their response. This header is an HTTP/1.1 addition and replaces the deprecated Pragma header, that was never a standard one. Cache-control header values are called directives. The specification defines a lot of them, with various uses and browser-support. These directives are primarily used by developers to communicate caching instructions. However, when present in an HTTP request, clients can also influence the caching decision. Let us now take the time to describe the most useful directives.

原始服务器通过向其响应添加Cache-Control标头,将其缓存指令传达给下游缓存代理。 此标头是对HTTP / 1.1的添加,它取代了不推荐使用的Pragma标头,该标头从来都不是标准标头。 缓存控制标头值称为指令。 规范定义了很多,具有各种用途和浏览器支持 。 这些指令主要由开发人员用来传达缓存指令。 但是,当存在于HTTP请求中时,客户端也会影响缓存决策。 现在让我们花时间描述最有用的指令。

最大年龄 (max-age)

The first important Cache-Control directive to know about is the max-age directive, which allows a server to specify the lifetime of a representation. It is expressed in seconds. For instance, if a cache sees a response containing the header Cache-Control: max-age=3600, it is allowed to store and serve the same response for all subsequent requests for this resource for the next 3600 seconds. During these 3600 seconds, the resource will be considered fresh and cache hits will occur. Past this delay, the resource will become stale and validation will take over.

要了解的第一个重要的Cache-Control指令是max-age指令,它允许服务器指定表示的生存期。 以秒为单位。 例如,如果缓存看到包含标头Cache-Control:max-age = 3600的响应,则在接下来的3600秒内,它可以为该资源的所有后续请求存储并提供相同的响应。 在这3600秒内,该资源将被认为是最新资源,并且将发生缓存命中。 超过此延迟,资源将过时,验证将接管。

无存储,无缓存,必须重新验证 (no-store, no-cache, must-revalidate)

Unlike max-age, the no-store, no-cache and must-revalidate directives are about instructing caches to not cache a resource. However, they differ in subtle ways. no-store is pretty self-explanatory, and in fact, it does even a little more than the name suggests. When present, a HTTP/1.1 compliant cache must not attempt to store anything, and must also take actions to delete any copy it might have, either in memory, or stored on disk.The no-cache directive, on the other hand, is arguably much less self-explanatory. This directive actually means to never use a local copy without first validating with the origin server. By doing so, it prevents all possibility of a cache hit, even with fresh resources.To put it another way, the no-cache directive says that caches must revalidate their representations with the origin server. But then comes another directive, awkwardly named… must-revalidate.If this starts to get confusing for you, rest assured, you are not alone. If what one wants is not to cache, it has to use no-store instead of no-cache. And if what one wants is to always revalidate, it has to use no-cache instead of must-revalidate. Confusing, indeed. As for the must-revalidate directive, it is used to forbid a cache to serve a stale resource. If a resource is fresh, must-revalidate perfectly allows a cache to serve it without forcing any revalidation, unlike with no-store and no-cache. That’s why this header should always be used with a max-age directive, to indicate a desire to cache a resource for some time and when it’s become stale, enforce a revalidation. When it comes to these last three directives, we find the choice of words to describe each of them particularly confusing: no-store and no-cache are expressed negatively whereas must-revalidate is expressed positively. Their differences would probably be more obvious if they were to be expressed in the same fashion. Therefore, it is helpful to think about each of them expressed in terms of what is not allowed:

与max-age不同,no-store,no-cache和must-revalidate指令与指示缓存不缓存资源有关。 但是,它们在微妙的方式上有所不同。 No-store是非常不言自明的,实际上,它甚至比名称所暗示的还要多。 如果存在,则符合HTTP / 1.1的高速缓存不得尝试存储任何内容,并且还必须采取措施删除其可能具有的任何副本(无论是在内存中还是存储在磁盘上)。可以说不那么自省了。 该指令实际上意味着在未先与原始服务器进行验证的情况下,切勿使用本地副本。 这样,它可以防止所有可能发生的高速缓存命中,即使是使用新鲜资源也可以。换句话说,no-cache指令表示高速缓存必须使用源服务器重新验证其表示。 但是随后出现了另一个指令,笨拙地命名为…必须重新验证。如果这开始让您感到困惑,请放心,您并不孤单。 如果要不要缓存,则必须使用无存储而不是无缓存。 而且,如果要始终重新验证,则必须使用无缓存而不是必须重新验证。 确实令人困惑。 至于must-revalidate指令,它用于禁止缓存提供过时的资源。 如果资源是新鲜的,则与无存储和无缓存不同,必须重新验证完全允许缓存提供服务而无需强制进行任何重新验证。 这就是为什么此标头应始终与max-age指令一起使用的原因,以表示希望在一段时间内缓存资源,并且在资源过时时,请强制进行重新验证。 当涉及到最后三个指令时,我们发现描述每个指令的单词选择特别令人困惑:否定和否定缓存表示为否定,而必须重新验证则表示肯定。 如果以相同的方式表达它们的差异可能会更加明显。 因此,考虑用不允许的方式来表达它们是有益的:

  • no-store: never store anything

    无商店:从不存储任何东西

  • no-cache: never cache hit

    无缓存:永不缓存命中

  • must-revalidate: never serve stale

    必须重新验证:永不陈旧

Technically, these directives can appear in the same Cache-Control header. It is not uncommon to see them combined as a comma-separated list of values. A lot of popular websites still seem to behave very conservatively, sending back HTML pages with the following header:

从技术上讲,这些指令可以出现在相同的Cache-Control标头中。 将它们组合为逗号分隔的值列表并不罕见。 许多受欢迎的网站似乎仍然非常保守,将带有以下标头HTML页面发送回:

Cache-Control: no-cache, no-store, max-age=0, must-revalidateWhen you stumble upon this, the intention behind it is usually pretty clear: the web development team wants to ensure that the resource never gets served stale to anyone. However, such cache-buster lines are probably not necessary anymore. Past work done in 2017 already showed that browsers are really rather compliant with the specification in respect to Cache-Control response directives. Therefore, unless you’re planning on setting up a caching stack with decades old software, you should be fine using just the directives you need. The most popular combinations will be analyzed in another article.

缓存控制:无缓存,无存储,max-age = 0,必须重新验证当您偶然发现时,其背后的意图通常非常清楚:Web开发团队希望确保资源永远不会过时地提供给任何人。 但是,这样的缓存破坏线可能不再需要了。 2017年完成的工作已经表明,浏览器在缓存控制响应指令方面确实相当符合规范。 因此,除非打算使用数十年的软件来建立缓存堆栈,否则只使用所需的指令就可以了。 最受欢迎的组合将在另一篇文章中进行分析。

公共,私人 (public, private)

The last important directives we haven’t discussed yet are a little bit different, as they control which types of caches are allowed to cache the resources. These are the public and private directives, private being the default one if unspecified.

我们尚未讨论的最后一个重要指令有些不同,因为它们控制允许哪些类型的缓存来缓存资源。 这些是public和private指令,如果未指定,则private是默认指令。

Private caches are the ones that are supposed to be used by a single user. Typically, this is the web browser’s cache. CDN and reverse-proxies on the contrary, handle requests coming from multiple users.

专用缓存是应该由单个用户使用的缓存。 通常,这是Web浏览器的缓存。 相反,CDN和反向代理处理来自多个用户的请求。

Why do we need to distinguish these two types of caches ? The answer is straightforward: security, as illustrated by the following example.

为什么我们需要区分这两种类型的缓存? 答案很简单:安全性,如以下示例所示。

Many web applications expose convenience endpoints that rely on information coming from elsewhere than the URL. If two users access their profile by requesting /users/me, at  https://api.example/com, and their actual user id is hidden within a Authorization: Bearer 4Ja23ç42….   token, the cache won’t be able to tell these are in fact two very different resources.

许多Web应用程序公开了便捷端点,这些端点依赖于URL之外的其他信息。 如果两个用户通过在https://api.example/com上请求/ users / me来访问其个人资料,并且其实际用户ID隐藏在Authorization:Bearer4Ja23ç42…中。 令牌,缓存将无法分辨出它们实际上是两个截然不同的资源。

Indeed, when constructing their cache key, caches do not inspect HTTP headers unless specifically instructed to do so, as we shall see in the next section.

确实,在构造其缓存键时,除非特别指示,否则缓存不会检查HTTP标头,这将在下一部分中看到。

最大s (s-maxage)

The s-maxage directive is like the max-age directive, except that it only applies to public caches, which are also referred to as shared caches (hence the s- prefix). If both directives are present, s-maxage will take precedence over max-age on public caches and be ignored on private ones.

s-maxage伪指令与max-age伪指令类似,不同之处在于它仅适用于公共缓存,也称为共享缓存(因此为s-前缀)。 如果同时存在这两个指令,则在公共缓存中s-maxage将优先于max-age,而在专用缓存中将被忽略。

When using this directive, the general rule is to always ensure that s-maxage value is below max-age’s. The reasoning behind this rule is that the closer you are to the origin, the more suitable it is to check frequently what the latest representation is.

使用此指令时,一般规则是始终确保s-maxage值低于max-age的值。 该规则背后的原因是,您离原点越近,经常检查最新的表示形式越合适。

Imagine you were to cache for one day in the proxy, and one hour in browsers.

假设您要在代理中缓存一天,在浏览器中缓存一个小时。

Every time a browser would ask a resource to upstream servers, we could know in advance that the proxy will not contact the origin server for at least a day. Therefore, why not put the same TTL directly in the browsers ? As a conclusion, it is a best practice to always leave out a longer TTL in max-age than in s-maxage.

每次浏览器会问一个资源上游的服务器,我们可以事先知道代理不会接触至少一天的原始服务器。 因此,为什么不将相同的TTL直接放在浏览器中? 结论是,最佳做法是始终在max-age中保留比s-maxage中更长的TTL。

stale-while-revalidate and stale-if-errorThese two directives are not technically part of the original specification but are part of an extension which were first described more than 10 years ago. Although their browser support is limited, some popular CDNs have been supported them for more than 5 years!

stale-while-revalidate和stale-if-error这两个指令从技术上讲不是原始规范的一部分,而是扩展的一部分,它们在10多年前就已首次描述。 尽管它们对浏览器的支持是有限的, 已经为它们提供了一些流行的CDN超过5年了!

Though stale-while-revalidate is pretty useful. As the name implies, it allows a cache to “[...] immediately return a stale response while it revalidates it in the background, thereby hiding latency (both in the network and on the server) from clients”.

尽管过时的重新验证非常有用。 顾名思义,它允许缓存“在后台重新验证过时的响应后立即返回陈旧的响应,从而对客户端隐藏了网络和服务器上的延迟”。

This caching extension proves really helpful for things like images, where reducing latency is critical for the user experience, and where having a stale version for a few seconds is often better than a painfully downloading image.

事实证明,这种缓存扩展确实对诸如图像之类的东西很有帮助,其中减少延迟对于用户体验至关重要,而拥有陈旧版本几秒钟通常比痛苦地下载图像更好。

As for stale-if-error, it allows a cache to serve a stale version if the origin server returns a 5xx status code. This gives developers a chance to fix potential issues during a grace period where clients are shielded from irritating error pages.

至于过时错误,如果原始服务器返回5xx状态代码,则它允许缓存提供过时版本。 这为开发人员提供了在宽限期内解决潜在问题的机会,在宽限期内,客户端可以避免恼人的错误页面。

Consider the case of a meteo third-party script. If the meteo server happens to be unreachable for a few minutes, it’s probably best to display a slightly outdated forecast during this lapse of time, than it is to see a portion of the page be blank (or a whole blank page if the code does not handle third-party scripts loading failures.

考虑一个meteo第三方脚本的情况。 如果meteo服务器在几分钟内碰巧无法访问,则最好在这段时间内显示稍微过时的预测,而不是看到页面的一部分为空白(如果代码确实为空白,则整个页面为空白)无法处理第三方脚本加载失败。

我们还不知道 (What we don’t know yet)

After examining these Cache-Control directives, we now understand how applications that are distributed on the web, tend to leverage HTTP caching mechanisms in multiple ways, depending on what they need.

在研究了这些Cache-Control指令之后,我们现在了解了在Web上分发的应用程序如何根据所需的需求以多种方式利用HTTP缓存机制。

Though what we don’t yet understand is what  cache softwares actually do with the response they receive. They will most likely have to store it somewhere in order to retrieve it later. That’s the core idea of any caching system after all.

尽管我们还不了解缓存软件实际上是如何处理收到的响应的。 他们很可能必须将其存储在某个地方以便以后检索。 毕竟,这是所有缓存系统的核心思想。

Under normal circumstances, this certainly looks like what we would call an implementation detail. It should be merely enough to know that resources are indeed stored some way. Yet in this case, learning just a little more is actually critical.

在正常情况下,这肯定看起来像我们所谓的实现细节。 仅知道资源确实以某种方式存储就足够了。 然而,在这种情况下,多学一点实际上是至关重要的。

Neglecting the mechanisms that govern how caching softwares map objects from the HTTP responses space to their storage space can have really unexpected consequences, such as serving a brotli encoded Chinese document, to a user who does not understand Chinese, using a browser unable to decode brotli ¯\_(ツ)_/¯

忽略用于控制缓存软件如何将对象从HTTP响应空间映射到其存储空间的机制,可能会产生意想不到的后果,例如使用无法解码brotli的浏览器向不懂中文的用户提供brotli编码的中文文档。 ¯\ _(ツ)_ /¯

缓存如何存储和检索资源 (How caches store and retrieve resources)

Albeit unlikely to happen, since most browsers can decode brotli - and since most people know how to 說中文 -  the previous situation can still easily occur. To understand why this is the case, one must consider how caches store their representations.

尽管这种情况不太可能发生,但是由于大多数浏览器都可以解码brotli,而且由于大多数人都知道怎么说中文,因此以前的情况仍然很容易发生。 要理解为什么会这样,必须考虑缓存如何存储其表示。

By virtue of what they try to achieve, most caching softwares ought to be able to quickly retrieve simple text documents. To do so, a very simple yet powerful strategy is to use a key-value store. This strategy fits well in-memory representations. Therefore, the question one must answer when designing is the following: how to construct a cache key from an HTTP response?

依靠他们试图实现的目标,大多数缓存软件应该能够快速检索简单的文本文档。 为此,一种非常简单但功能强大的策略是使用键值存储。 此策略非常适合内存中的表示形式。 因此,设计时必须回答的问题如下:如何从HTTP响应构造缓存键?

What we are looking for here is a way to uniquely identify a resource. Conveniently, this is exactly why URIs - Uniform Resource Identifiers - were invented in the first place!

我们在这里寻找的是一种唯一标识 资源的方法。 方便地,这正是URI的原因-统一资源 标识符-首先被发明!

But URIs don’t tell the whole truth about resources. They never describe them entirely, if only for the fact that resources change over time.

但是URI并不能说明资源的全部真相。 他们永远不会完全描述它们,即使仅仅是因为资源随时间变化而已。

Websites get rebranded, new content gets published and users update their profile. Granted,  not for the same reasons or at the same frequency, though all resources will eventually change. In fact, the entire Conditional request specification is based on this sole observation: nothing is permanent except change.

网站更名,发布新内容,用户更新个人资料。 当然,虽然所有资源最终都会改变,但并非出于相同的原因或频率。 实际上,整个条件请求规范都基于这个唯一的观察结果: 除了 change 之外,没有什么是永久的

Philosophical quotes aside, there is, however, another time-independent reason why resources change. Indeed, any moment, resources may be available in multiple representations. This is why we have Content-Negociation.

除了哲学上的引用外,还有另一个与时间无关的原因来改变资源。 确实,任何时候,资源都可能有多种表示形式。 这就是为什么我们要进行内容协商。

The HTTP request headers Accept, Accept-Language, Accept-Encoding, Accept-Charset (and a few other headers who are not strictly speaking part of content negotiation) add another dimension on which representations can differ. As such, the problem of finding a good cache key becomes more complicated. Since all these representations share the same URI, caches must have a way to distinguish them in order to serve the right representation at each client, honoring content negotiation.

HTTP请求标头Accept,Accept-Language, Accept-Encoding,Accept-Charset(以及其他一些严格不是内容协商的标头)添加了另一个维度,表示方式可能会有所不同。 这样,寻找良好的高速缓存密钥的问题变得更加复杂。 由于所有这些表示形式共享相同的URI,因此缓存必须具有区分它们的方式,以便在每个客户端上提供正确的表示形式,从而尊重内容协商。

And since only origin servers know what different representations are available, it is again the origin server’s responsibility to indicate to a cache based on which headers it will generate a different representation. To do so, the origin servers must add a Vary header containing the value of the request headers that cause different representations to be generated.

而且,由于只有原始服务器知道可用的不同表示形式,因此原始服务器还要负责根据缓存将生成不同表示形式的标头向缓存进行指示。 为此,原始服务器必须添加一个Vary 标头,其中包含导致生成不同表示形式的请求标头的值。

When caches see a response coming from an origin server with, for instance, the header Vary: Accept-Language, it will examine the value of the Accept-Language header,  such as fr-FR, and use this value to construct a more specific cache-key, perhaps like https://example.net/home.html_fr-FR.

当缓存看到来自源服务器的响应(例如带有标头Vary Accept-Language)时,它将检查 Accept-Language头,如FR-FR,并使用该值来构建一个更具体的缓存键,或许象https://example.net/home.html_ FR-FR。

The actual implementation strategy is of little importance to us. Altering the cache key might not even be the best way to do it. It somehow has to use the value of the header to differentiate representations.

实际的实施策略对我们而言并不重要。 更改缓存键甚至可能不是最佳方法。 它必须以某种方式使用价值 标头的名称以区分表示形式。

The Vary header can actually point at more than one header, when resources are available in multiple representations. Selecting a cache key when multiple headers are involved is not really much more complicated than with only one header. The real problem when varying over multiple dimensions is the combinatorial explosion.

当资源有多种表示形式时,Vary标头实际上可以指向多个标头。 当涉及多个标头时,选择一个缓存键并没有比仅包含一个标头复杂得多。 当在多个维度上变化时,真正的问题是组合爆炸

Unfortunately, there are no ways around this. If you are to cache and serve your resources in multiple representations, you have to pay the cost of a large storage. If you decide to lower your vary cardinality, some of your users will receive cache hits for responses that won’t match their requests.

不幸的是,这没有办法。 如果要以多种表示形式缓存和服务资源,则必须支付大容量存储的费用。 如果您决定降低不同的基数,则您的某些用户将收到缓存命中结果,因为这些结果与他们的请求不匹配。

On the other hand, if you vary properly on everything, and do not have enough storage space, chances are your users won’t be seeing cache hits anytime soon.

另一方面,如果您在所有方面都做得适当,并且没有足够的存储空间,则您的用户很可能很快就不会看到缓存命中。

Now, it is important to know that this is only a problem if you decide to use a public cache, for which two different requests coming from two different users are running the same code, at the proxy level. If you decide to leverage the browser’s cache only, then you can skip the Vary header altogether and serve resources in as many representations as you want. This is because each browser’s cache will only cache representations matching the user’s preferences. This is good news!

现在,重要的是要知道,这仅是一个问题,如果您决定使用公共缓存,在代理级别,来自两个不同用户的两个不同请求正在运行同一代码的公共缓存。 如果您决定仅利用浏览器的缓存,则可以完全跳过Vary标头,并根据需要以任意数量的表示形式提供资源。 这是因为每个浏览器的缓存将仅缓存与用户首选项匹配的表示形式。 这是个好消息!

But let’s not get ahead of ourselves just yet. As we said, caches use the value of the header as its input to generate a more specific cache key. But what is to say that all these values are well formatted ? Absolutely nothing! This is the rather inconvenient consequence of RFC father’s robustness principle. HTTP servers are indeed very liberal in what they accept.

但是,让我们暂时不要超越自己。 如前所述,缓存使用标头的作为其输入来生成更具体的缓存键。 但是,所有这些值的格式正确又是什么呢? 绝对没有! 这是RFC 父亲鲁棒性原则的不便之处。 HTTP服务器在接受方面确实非常自由

However there is hope.

但是有希望。

Considering the case of an origin server that can only produce a representation in two different languages, caches must be able to regroup incoming Accept-Content values such as fr, fr-FR, fr_FR.. into something such as FR. Otherwise, just like before with the combinatorial explosion, the number of representations will explode, but in this case, for a misguided reason.

考虑到原始服务器只能生成两种不同语言的表示形式,缓存必须能够将传入的Accept-Content值(例如fr,fr-FR,fr_FR ..)重新组合为诸如FR之类的内容。 否则,就像之前的组合爆炸一样,表示的数量也会爆炸,但是在这种情况下,原因是错误的。

The process by which all these representations are regrouped is called normalization and is often done at the cache. Many caches offer configuration utilities or their own languages to deal with these situations. Sometimes, the functions are even already written, or snippets can easily be found on the Internet. The following pictures illustrates the process for the infamous User-Agent header.

重新组织所有这些表示的过程称为规范化 ,通常在缓存中完成。 许多缓存提供配置实用程序或它们自己的语言来处理这些情况。 有时,这些功能甚至已经编写完毕,或者可以在Internet上轻松找到摘要。 下图说明了臭名昭著的User-Agent标头的过程。

Fastly, a popular CDN, sampled 100 000 requests and found that the Accept-Encoding header was expressed in 44 different ways ! As for the User-Agent header, they found a shy of… 8000 different ones! Without normalization, chances are that the cache will never see any hit.

很快,一个流行的CDN 100 000个请求进行了采样 ,发现Accept-Encoding标头以44种不同的方式表示! 至于User-Agent标头,他们发现了…8000个不同的标头! 如果不进行标准化,则很有可能缓存将永远不会看到任何命中。

This wraps up the section about representation variation. At this point, we know how to instruct caches to store our resources, and have learned to leverage the Vary header to prevent accidents from happening when using public caches. We have now covered enough of the specification to be able to cache resources effectively.

这总结了关于表示变化的部分。 至此,我们知道了如何指示缓存来存储资源,并且学会了利用Vary标头来防止使用公共缓存时发生事故。 现在,我们已经涵盖了足够多的规范,以能够有效地缓存资源。

常见的误解 (Common misconceptions)

By now, you should have a thorough understanding of how HTTP caching works. Freshness control, resource’s representations and cache hits are no longer mysterious concepts to you. And if you start to feel empowered by all this knowledge, we have some good news for you: we’ve covered a large portion of the specification, and you now know pretty much all that’s necessary to be up and running.

到目前为止,您应该对HTTP缓存的工作方式有一个全面的了解。 对您而言,新鲜度控制,资源表示和缓存命中不再是神秘的概念。 而且,如果您开始感到受所有这些知识的启发,那么我们将为您带来一些好消息:我们已经涵盖了大部分规范,现在您几乎知道了启动和运行所需要的一切。

But make no mistake. Caching is a complex topic.

但是请不要误会。 缓存一个复杂的话题。

Experience has shown us that, unless you’re dealing with it on a day-to-day basis, what may be crystal clear today will quickly turn into something rather blurry after a few weeks.  Therefore, we decided to conclude this second article by dispelling two common misconceptions that are all too easy to make.

经验告诉我们,除非您每天进行处理,否则几周后,今天可能很清楚的事情很快就会变得很模糊。 因此,我们决定通过消除两个太容易造成的常见误解来结束第二篇文章。

新鲜度控制和验证 (Freshness-control and validation)

This might seem obvious after reading the previous sections but it is worth repeating many times. Freshness control and validation (which we have slightly discussed in the beginning) are two very distinct mechanisms that serve two very different purposes, and involve HTTP requests between different pieces.

在阅读了前面的部分之后,这似乎很明显,但是值得重复多次。 新鲜度控制和验证( 我们在开始时已经稍作讨论了 )是两种截然不同的机制,它们服务于两种截然不同的目的,并且涉及不同部分之间的HTTP请求。

  • Freshness control always happen in a cache and is solely based on time

    新鲜度控制始终在缓存中进行,并且仅基于时间

  • Validations always happen in the origin server and are based both on time and on identifiers (ETags)

    验证始终在原始服务器中进行,并且基于时间和标识符(ETag)

This is something we find important to remind ourselves. It means that once the cache has received temporal instructions, it can - and best believe it will - serve resources without ever contacting the origin server until the timer expires.

我们发现这很重要,需要提醒自己。 这意味着一旦高速缓存接收到时间指令,它就可以-最好地相信它会-在无需等待计时器到期之前就不联系源服务器的情况下服务资源。

For instance, if your web application’s HTML file reaches a browser and the HTTP response happens to include the header Cache-Control: max-age=86400 the browser will happily serve the same version of your app for a day. In this case, the browser would serve it for one day without any possible action from you or anyone, except the user, if one ever decided to flush his browser’s cache.

例如,如果您的Web应用程序HTML文件到达浏览器,并且HTTP响应恰好包含标头Cache-Control:max-age = 86400,则该浏览器将在一天之内愉快地为您的应用提供相同版本。 在这种情况下,如果一个浏览器决定刷新其浏览器的缓存,则该浏览器将在一天之内运行,而您或除用户之外的任何人都不会采取任何行动。

If you’re thinking everyone can make mistakes, and one day is not so bad, well, brace yourself: the maximum max-age value is… 31536000 seconds! That is to say, one year. This is the reason why HTML files are very dangerous to cache like this, and should generally be declared with Cache-Control: no-cache.

如果您认为每个人都可能犯错,并且一天还不错,那么请做好准备:最大年龄上限是……31536000秒! 也就是说, 一年。 这就是为什么HTML文件这样非常危险地缓存的原因,通常应使用Cache-Control:no-cache来声明。

新鲜度和最新表现 (Freshness and most recent representation)

Another misconception is to believe that cache hits and freshness have anything to do with having the last available version of a resource. This is what we all try to achieve, but one can never truly know if the resource it has been served from a cache is indeed the most up-to-date version. In fact, this holds true even in the absence of cache. It has to do with the nature of distributed applications: other people’s actions can change the things we are interacting with at any time.

另一个误解是认为缓存命中率和新鲜度与拥有资源的最后可用版本有关。 这是我们所有人都试图实现的目标,但是人们永远无法真正知道从缓存提供的资源是否确实是最新版本。 实际上,即使没有缓存,这也适用。 它与分布式应用程序的性质有关:其他人的行为可以随时更改我们正在与之交互的事物。

When querying the state of the application, the ETag header must always be used to always let the server know what our current understanding of the application’s state is. And if it does not match the server’s, 409 Conflict are expected to be received on the client side.

查询应用程序的状态时,必须始终使用ETag标头以始终让服务器知道我们对应用程序状态的当前了解。 并且,如果与服务器不匹配,则可能会在客户端收到409冲突。

结论 (Conclusion)

Along this article, we have described how caching actually works. Now would be a good time to spin up a local dev server and fiddle around with these two core headers: Cache-Control and Vary to see them in action.

在本文中,我们描述了缓存实际上是如何工作的。 现在将是启动本地开发服务器并在这两个核心标头中忙忙碌碌的好时机:Cache-Control和Vary以查看它们的运行情况。

We started by giving an overview of how caching works, illustrating the four possible paths that a request can take : the happy path (cache hit) and the 3 possible ways to have a cache miss : empty cache, failed revalidation and successful revalidation. This overview alone gives the possibility to understand how complex caching topologies can fit together.

我们首先概述了缓存的工作方式,说明了请求可以采用的四种可能路径:快乐路径(缓存命中)和三种可能的缓存缺失方式:空缓存,失败的重新验证和成功的重新验证。 仅此概述就可以了解复杂的缓存拓扑如何组合在一起。

Then, we went deeper and looked at all the most useful Cache-Control headers, and clarified some subtle differences that are all easily missed.

然后,我们更深入地研究了所有最有用的Cache-Control标头,并弄清了一些容易被忽略的细微差别。

We also looked at the Vary header and the fundamental difference between resources and representations, to avoid serving the wrong representation to the right client.

我们还研究了Vary标头以及资源和表示形式之间的根本区别,以避免为正确的客户提供错误的表示形式

Finally, we took some time to review it all through the angle of common misconceptions you might encounter, and hopefully helped you to avoid them.

最后,我们花了一些时间从您可能会遇到的常见误解的角度来回顾所有问题,并希望能帮助您避免这些误解。

In the next article, we’ll apply all of this knowledge to set up a local lab environment in which we will set an innocent node.js app on fire with a load-testing tool, right before rescuing it with the help of a popular caching software.

在下一篇文章中,我们将运用所有这些知识来设置本地实验室环境,在该环境中,我们将使用负载测试工具来启动一个无辜的node.js应用程序,然后在流行的帮助下对其进行抢救缓存软件。

Stay tuned!

敬请关注!

更进一步: (To go further:)

The official specification about the material we covered (and other things)https://tools.ietf.org/html/rfc7234#section-5.3

关于我们涵盖的材料(及其他内容)的官方规范https://tools.ietf.org/html/rfc7234#section-5.3

Google Web’s Fundamentalhttps://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#defining-optimal-cache-control-policy

Google Web的基础https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#defining-optimal-cache-control-policy

About the Cache-Control header:https://developer.mozilla.org/fr/docs/Web/HTTP/Headers/Cache-Control

关于Cache-Control标头: https : //developer.mozilla.org/fr/docs/Web/HTTP/Headers/Cache-Control

About the Vary Header:https://www.smashingmagazine.com/2017/11/understanding-vary-header/https://www.fastly.com/blog/best-practices-using-vary-headerhttps://www.fastly.com/blog/getting-most-out-vary-fastlyhttps://www.fastly.com/blog/understanding-vary-header-browser

关于Vary标头: https: //www.smashingmagazine.com/2017/11/understanding-vary-header/ https://www.fastly.com/blog/best-practices-using-vary-header https:// www.fastly.com/blog/getting-most-out-vary-fastly https://www.fastly.com/blog/understanding-vary-header-browser

翻译自: https://www.freecodecamp.org/news/an-in-depth-introduction-to-http-caching-cache-control-vary/

 类似资料: