MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

HTTP/1.1协议下的缓存控制机制优化

2023-10-252.6k 阅读

HTTP/1.1 协议概述

HTTP(Hyper - Text Transfer Protocol)是用于在万维网上传输超文本的应用层协议。HTTP/1.1 作为 HTTP 协议的一个重要版本,于 1999 年发布,它在 HTTP/1.0 的基础上进行了许多改进,以提高性能、增强功能和优化网络资源利用。

HTTP/1.1 的主要特点

  1. 持久连接:HTTP/1.0 每次请求 - 响应完成后,连接就会关闭。而 HTTP/1.1 默认采用持久连接(persistent connection),即 TCP 连接在完成一次请求 - 响应后不会立即关闭,可以在该连接上继续发送其他请求,减少了建立和关闭连接的开销,提高了传输效率。例如,一个网页包含多个资源(如图片、脚本等),在 HTTP/1.1 下,这些资源可以通过同一个 TCP 连接进行传输。
  2. 管道化:在持久连接的基础上,HTTP/1.1 支持管道化(pipelining)。客户端可以在没有收到前一个响应的情况下,就连续发送多个请求。这样可以进一步减少等待时间,提高传输效率。然而,由于存在一些潜在的问题,如服务器可能按照请求顺序处理响应,导致队头阻塞(Head - of - line blocking),实际应用中管道化并没有得到广泛应用。
  3. 缓存机制增强:HTTP/1.1 对缓存机制进行了更细致和强大的定义,通过各种首部字段来控制缓存行为,使得资源的缓存和复用更加合理,减少不必要的数据传输,降低网络负载。这也是本文重点探讨的内容。

HTTP/1.1 缓存控制机制基础

缓存的作用

在网络通信中,缓存是一种存储经常访问数据的机制。对于 HTTP 协议来说,缓存的主要作用有以下几点:

  1. 减少响应时间:如果客户端请求的资源在缓存中存在且仍然有效,那么可以直接从缓存中获取,而不需要再次从服务器获取,大大缩短了响应时间,提高了用户体验。例如,用户多次访问同一个静态网页,网页中的静态资源(如 CSS 文件、图片等)如果被缓存,第二次及以后的访问可以快速加载。
  2. 降低服务器负载:缓存命中(即从缓存中获取到有效资源)意味着服务器不需要再次处理相同的请求并发送响应,减少了服务器的计算资源和带宽消耗。对于高流量的网站,合理的缓存策略可以显著降低服务器的负载压力。
  3. 节省网络带宽:减少了客户端和服务器之间的数据传输量,对于用户来说,可以节省流量费用,对于网络服务提供商来说,可以更有效地利用网络带宽资源。

缓存相关的 HTTP 首部字段

  1. Cache - Control
    • 语法:Cache - Control: directive1[=value1][,directive2[=value2],…]
    • 常见指令及含义
      • public:表明响应可以被任何对象(包括中间代理、CDN 等)缓存。例如,一个公共的图片资源设置 Cache - Control: public,那么浏览器、代理服务器等都可以缓存该图片。
      • private:表示响应只能被终端用户(如浏览器)缓存,中间代理不能缓存。常用于包含用户敏感信息的响应,如用户的个人资料页面。
      • no - cache:并不是禁止缓存,而是在使用缓存之前,必须先向服务器验证缓存的有效性。即每次请求时,客户端会先发送请求到服务器,服务器检查缓存是否仍然有效,如果有效则返回 304(Not Modified)状态码,客户端再从缓存中获取资源。
      • no - store:绝对禁止缓存,无论是客户端还是中间代理都不能缓存该响应。这通常用于包含敏感信息且不希望被缓存的响应,如银行转账的确认页面。
      • max - age = seconds:设置缓存的最大有效时间(以秒为单位)。例如,Cache - Control: max - age = 3600 表示该资源在 3600 秒(1 小时)内是有效的,超过这个时间,缓存就会失效,客户端需要重新获取资源。
  2. Expires
    • 语法:Expires: date
    • 含义:这是一个 HTTP/1.0 就存在的首部字段,指定资源过期的日期和时间。如果在这个日期和时间之后客户端请求该资源,缓存就会失效。例如,Expires: Thu, 01 Dec 2022 16:00:00 GMT,表示资源在 2022 年 12 月 1 日 16:00:00 GMT 之后过期。然而,由于它依赖于服务器和客户端的时钟同步,在实际应用中存在一定的局限性,HTTP/1.1 推荐使用 Cache - Control 中的 max - age 来替代它。
  3. Last - ModifiedETag
    • Last - Modified
      • 语法:Last - Modified: date
      • 含义:服务器在响应中使用该首部字段表明资源的最后修改时间。客户端在后续请求中可以通过 If - Modified - Since 首部字段将这个时间发送给服务器,服务器通过比较这个时间和资源的实际最后修改时间来判断资源是否有更新。如果没有更新,服务器返回 304(Not Modified)状态码,客户端从缓存中获取资源。
    • ETag
      • 语法:ETag: entity - tag
      • 含义:ETag 是资源的唯一标识符(通常是一个哈希值),由服务器生成并在响应中返回。客户端在后续请求中可以通过 If - None - Match 首部字段将这个 ETag 发送给服务器,服务器通过比较这个 ETag 和资源当前的 ETag 来判断资源是否有更新。ETag 比 Last - Modified 更精确,因为它可以检测到文件内容的微小变化,而不仅仅依赖于修改时间。

HTTP/1.1 缓存控制机制优化策略

合理设置 Cache - Control 指令

  1. 对于静态资源
    • 像 CSS 文件、JavaScript 文件、图片等静态资源,通常可以设置较长的缓存时间。例如,对于一个网站的通用 CSS 文件,可以设置 Cache - Control: public, max - age = 31536000(一年的秒数)。这样,在一年的时间内,客户端请求该 CSS 文件时,如果缓存有效,就可以直接从缓存中获取,大大减少了服务器的负载和网络传输。
    • 示例代码(以 Python Flask 框架为例):
from flask import Flask, send_file

app = Flask(__name__)


@app.route('/static/css/style.css')
def serve_css():
    headers = {
        'Cache - Control': 'public, max - age = 31536000'
    }
    return send_file('static/css/style.css', headers = headers)


if __name__ == '__main__':
    app.run(debug = True)
  1. 对于动态资源
    • 动态资源(如根据用户请求生成的 HTML 页面、API 响应等)的缓存设置需要更加谨慎。如果资源变化不频繁,可以设置较短的缓存时间,如 Cache - Control: public, max - age = 60(1 分钟)。这样既能在一定程度上利用缓存提高性能,又能保证用户不会长时间看到旧数据。
    • 示例代码(以 Java Spring Boot 框架为例):
import org.springframework.http.CacheControl;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

@RestController
public class DynamicResourceController {

    @GetMapping("/dynamic - data")
    public ResponseEntity<String> getDynamicData() {
        CacheControl cacheControl = CacheControl.maxAge(60, TimeUnit.SECONDS);
        return ResponseEntity.ok()
              .cacheControl(cacheControl)
              .body("Dynamic data response");
    }
}
  1. 避免过度缓存
    • 虽然缓存可以提高性能,但过度缓存可能导致用户看到陈旧的数据。对于一些实时性要求较高的资源,如股票行情、实时新闻等,应该设置 no - cache 或较短的缓存时间。例如,对于股票行情 API,可以设置 Cache - Control: no - cache,这样每次客户端请求时,都会先向服务器验证缓存的有效性,确保获取到最新的数据。
    • 示例代码(以 Node.js Express 框架为例):
const express = require('express');
const app = express();

app.get('/stock - quote', (req, res) => {
    res.set('Cache - Control', 'no - cache');
    // 模拟获取股票行情数据
    const stockQuote = { symbol: 'ABC', price: 123.45 };
    res.json(stockQuote);
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

利用 ETag 和 Last - Modified 进行缓存验证优化

  1. ETag 的优化使用
    • ETag 可以精确地表示资源的内容。在服务器端,当资源内容发生变化时,需要重新生成 ETag。例如,在一个文件存储系统中,当文件内容更新时,计算文件的新哈希值作为 ETag。
    • 示例代码(以 Python Django 框架为例):
import hashlib
from django.http import HttpResponse
from django.views.decorators.http import etag

def calculate_etag(request):
    # 假设这里获取文件内容
    file_content = b'Some file content'
    hash_object = hashlib.md5(file_content)
    return hash_object.hexdigest()


@etag(calculate_etag)
def serve_file(request):
    return HttpResponse('File content')
  1. Last - Modified 的优化使用
    • 对于文件类型的资源,获取文件的最后修改时间作为 Last - Modified 的值比较简单。但对于一些动态生成的资源,需要根据业务逻辑来确定合适的修改时间。例如,一个数据库查询结果的 Last - Modified 时间可以设置为数据库中相关数据最后更新的时间。
    • 示例代码(以 PHP 为例):
<?php
$lastModified = filemtime('data.txt');
header('Last - Modified: '. gmdate('D, d M Y H:i:s', $lastModified).'GMT');
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= $lastModified) {
    header('HTTP/1.1 304 Not Modified');
    exit;
}
// 输出文件内容
readfile('data.txt');
?>
  1. ETag 和 Last - Modified 的结合使用
    • 在实际应用中,可以结合 ETag 和 Last - Modified 来进行更灵活和精确的缓存验证。服务器可以同时返回 ETag 和 Last - Modified 首部字段,客户端在请求时发送 If - None - Match(包含 ETag)和 If - Modified - Since(包含 Last - Modified 时间)。服务器优先检查 ETag,如果 ETag 匹配且资源未修改,则返回 304 状态码;如果 ETag 不匹配,再检查 Last - Modified 时间。这样可以在保证精确性的同时,利用 Last - Modified 的简单性提高验证效率。

缓存分层策略

  1. 客户端缓存
    • 客户端(如浏览器)的缓存是最接近用户的一层缓存。浏览器通常会根据 HTTP 首部字段的设置来管理缓存。可以通过设置 Cache - Control 中的 private 指令来确保只有客户端可以缓存资源。客户端缓存对于提高用户再次访问相同页面的速度非常有效,因为不需要与服务器进行额外的通信。
  2. 中间代理缓存
    • 中间代理(如 CDN 节点、企业内部的代理服务器等)缓存可以在更广泛的范围内共享缓存资源。当多个客户端请求相同的资源时,中间代理可以直接从缓存中提供响应,减少了源服务器的负载。对于一些热门的静态资源,如流行的 JavaScript 库,可以设置 Cache - Control: public 来允许中间代理缓存。
  3. 服务器端缓存
    • 服务器端也可以实现自身的缓存机制,如应用服务器缓存经常查询的数据库结果。服务器端缓存可以在请求到达应用逻辑之前就处理请求,提高响应速度。例如,在一个基于 MySQL 数据库的 Web 应用中,使用 Memcached 或 Redis 来缓存数据库查询结果。当有相同的查询请求时,直接从缓存中获取结果,而不需要再次查询数据库。
    • 示例代码(以 Python 使用 Redis 进行服务器端缓存为例):
import redis
import mysql.connector

r = redis.Redis(host = 'localhost', port = 6379, db = 0)


def get_user_data(user_id):
    cache_key = f'user:{user_id}'
    data = r.get(cache_key)
    if data:
        return data.decode('utf - 8')
    else:
        conn = mysql.connector.connect(user = 'user', password = 'password', host = '127.0.0.1', database = 'test')
        cursor = conn.cursor()
        cursor.execute(f'SELECT * FROM users WHERE id = {user_id}')
        result = cursor.fetchone()
        conn.close()
        if result:
            user_data = str(result)
            r.set(cache_key, user_data)
            return user_data
        else:
            return None

处理缓存失效和更新

  1. 缓存失效策略
    • 当资源发生变化时,需要使相关的缓存失效。对于客户端缓存,可以通过更新资源的 URL(如在文件名后添加版本号)来强制客户端重新获取资源。例如,将 style.css 改为 style - v2.css,这样客户端会认为这是一个新的资源,不会从缓存中获取。
    • 对于中间代理缓存,一些 CDN 提供商提供了缓存刷新的接口。当资源更新时,通过调用 CDN 的 API 来清除相关节点的缓存,确保新的资源能够被及时分发。
  2. 缓存更新策略
    • 在更新资源时,要确保缓存能够及时更新。一种方法是在更新资源的同时,更新相关的缓存控制首部字段。例如,当更新一个静态文件时,重新计算 ETag 和更新 Last - Modified 时间,并在响应中返回新的首部字段。这样客户端和中间代理在下次请求时,能够正确判断缓存是否有效。
    • 示例代码(以 Ruby on Rails 框架为例,更新文件后更新 ETag 和 Last - Modified):
class StaticAssetsController < ApplicationController
  def serve_image
    image_path = 'public/images/logo.png'
    last_modified = File.mtime(image_path)
    etag = Digest::MD5.file(image_path).hexdigest
    headers['Last - Modified'] = last_modified.httpdate
    headers['ETag'] = etag
    send_file image_path
  end
end

缓存控制机制优化的性能评估

评估指标

  1. 响应时间
    • 可以通过测量从客户端发送请求到接收到完整响应的时间来评估缓存优化的效果。使用工具如 Apache JMeter、Gatling 等进行性能测试。在缓存优化前和优化后分别进行测试,比较平均响应时间、最大响应时间和最小响应时间等指标。例如,如果在优化前,请求一个图片资源的平均响应时间是 200ms,优化后降低到 50ms,说明缓存优化显著提高了响应速度。
  2. 带宽利用率
    • 带宽利用率可以通过网络监控工具(如 Wireshark、nload 等)来测量。缓存优化后,由于减少了不必要的数据传输,网络带宽的利用率应该得到改善。例如,在一个网站中,优化前每月的带宽使用量是 100GB,优化后降低到 80GB,表明缓存优化有效地节省了带宽。
  3. 服务器负载
    • 服务器负载可以通过系统自带的工具(如 top、htop 等)或者服务器监控软件(如 Zabbix、Prometheus 等)来监测。合理的缓存策略应该能够降低服务器的 CPU 使用率、内存使用率和网络 I/O 等。例如,在优化前,服务器的 CPU 使用率经常达到 80%以上,优化后稳定在 50%左右,说明缓存优化减轻了服务器的负载压力。

性能评估示例

  1. 使用 Apache JMeter 进行响应时间测试
    • 步骤:
      • 打开 Apache JMeter,创建一个新的测试计划。
      • 添加一个线程组,设置线程数(如 100)、循环次数(如 10)等参数来模拟多个用户的多次请求。
      • 在线程组下添加 HTTP 请求,设置请求的 URL(如 /static/css/style.css)。
      • 添加监听器,如聚合报告,用于查看响应时间等指标。
      • 分别在缓存优化前和优化后运行测试计划,记录聚合报告中的平均响应时间、中位数响应时间等数据进行比较。
  2. 使用 Wireshark 进行带宽利用率分析
    • 步骤:
      • 启动 Wireshark,选择要监控的网络接口。
      • 在缓存优化前,开始捕获网络数据包,然后模拟客户端请求资源(如通过浏览器多次刷新网页)。停止捕获后,分析捕获的数据包,统计 HTTP 流量的大小。
      • 进行缓存优化后,重复上述步骤,再次统计 HTTP 流量的大小。比较两次的流量数据,评估带宽利用率的变化。
  3. 使用 Zabbix 进行服务器负载监测
    • 步骤:
      • 在服务器上安装 Zabbix Agent,并配置与 Zabbix Server 的连接。
      • 在 Zabbix Server 中创建主机,添加相关的监控项,如 CPU 使用率、内存使用率、网络 I/O 等。
      • 在缓存优化前和优化后,观察 Zabbix 界面上这些监控项的图表变化,分析服务器负载的变化情况。

通过对这些性能指标的评估,可以全面了解 HTTP/1.1 缓存控制机制优化的效果,并根据评估结果进一步调整和优化缓存策略。