#258 Logrotate

2018-05-03

配置

# cat /etc/logrotate.conf
weekly
su root adm
rotate 4
create
#dateext
#compress
include /etc/logrotate.d

以 Nginx 配置为例:

# cat /etc/logrotate.d/nginx
/var/log/nginx/*.log {
        daily       # 按日切割
        missingok   # 如果文件不存在,则不创建
        rotate 14   # 最多保留 14 个日志文件
        compress    # 压缩
        delaycompress   # 延迟压缩
        notifempty      # 如果文件为空,则不创建
        create 0640 www-data adm

        # 可能一次切割多个日志,
        # 但是后面遇到的每个脚本都只执行一次,
        # 在所有日志切割之前或之后
        sharedscripts

        prerotate
                if [ -d /etc/logrotate.d/httpd-prerotate ]; then \
                        run-parts /etc/logrotate.d/httpd-prerotate; \
                fi \
        endscript

        postrotate
                invoke-rc.d nginx rotate >/dev/null 2>&1
        endscript
}

其他常用选项:

  • dateext 部分日志需要添加日期后缀
  • lastaction/endscript 最后执行的指令,很有用,比如最后将日志备份到某些地方

比如:

rsync -au *.gz 192.168.64.234:/backup/nginx-logs/`hostname -s`/

参考资料与拓展阅读

#257 关于 DNS(历史和未来)

2018-05-02

历史

从阿帕网(ARPANET)时代一直到互联网的早期,网络节点比较少,都是通过本地 hosts 文件来实现主机名到 IP 地址的映射。

根据维基百科的信息,斯坦福研究所(SRI-NIC,Stanford Research Institute Network Information Center)负责维护一个公共 HOSTS.TXT 文件(RFC 606、RFC 608),各个网络节点定期通过 FTP 下载最新版本进行同步。

PS:这个时候如果有主机名重复了谁来管?打电话过去让他们改名?

这套机制一直运行了十几年。随着互联网规模扩大,公共 HOSTS.TXT 文件变得越来越大,变化也越来越频繁。每当有新的主机加入网络、IP 地址发生变化,都需要重新同步整个文件。

这种模式存在几个明显问题:

  • 中央管理机构压力越来越大;
  • 全网频繁同步效率较低;
  • 主机数量增长后难以维护;
  • 名称冲突和管理问题越来越复杂。

后来人们开始设计域名和域名相关的公共设施。RFC 819《The Domain Naming Convention for Internet User Applications》和 RFC 881《The Domain Names Plan and Schedule》提出了域名系统的早期设计思路。

最终在 1983 年形成了下面两个 RFC 文档:

  • RFC 882, DOMAIN NAMES - CONCEPTS and FACILITIES
  • RFC 883, DOMAIN NAMES - IMPLEMENTATION and SPECIFICATION

几年后(1987),正式的 DNS 标准 RFC 1034 和 RFC 1035 推出。我们现在常见的 A / MX / NS / CNAME / PTR / SOA 等记录类型都是在这个标准中定义的。

  • RFC 1034, Domain Names Concepts and Facilities
  • RFC 1035, Domain Names Implementation and Specification

这套标准一直运行到现在。虽然几十年来不断增加新的记录类型、支持国际化域名(IDN)、引入 DNSSEC、安全加密传输等扩展能力,但其核心设计思想基本没有改变:

  • 分层命名;
  • 分布式管理;
  • 缓存加速;
  • 递归解析。

DNS 可以说是互联网历史上寿命最长、最成功的基础设施之一。并且,在可以预见的未来,DNS 仍然会是互联网最重要的基础设施之一。

DNS over TCP

最初 DNS 主要运行于 UDP 53 端口。RFC 1035 同时规定了 TCP 支持,最早 TCP 主要用于:

  • 区域传送(AXFR);
  • 超大响应数据返回。

今天绝大多数 DNS 服务仍同时支持 UDP 和 TCP。

DNS 的管理权问题

互联网本是美国国防部高级研究计划局(DARPA)的产物,早期域名分配几乎由南加州大学教授乔恩·波斯特尔一人代管。1998年,克林顿政府为避免让联合国下属的国际电信联盟(ITU)——一个由各国政府和官营电信主导的机构——接管互联网,刻意设计了一条"私有化"路线:成立非营利组织 ICANN,负责全球域名系统(DNS)和顶级域名分配,与美国商务部签合同但通过"私人身份"运作,以此保持互联网开放、不受单一政府直接操控。然而 ICANN 总部在加州、受美国法律管辖,全球13台根服务器多数也在美国手里,等于事实上的互联网域名最高仲裁者仍是美国。这让包括中国、俄罗斯在内的大量国家长期不满,伊拉克战争期间".iq"域名被暂停解析一事更被视为美国"悬剑在手"的象征。

2013年斯诺登曝光美国大规模监听后,国际社会对"美国垄断互联网命门"的反弹到达顶点,联合国层面甚至出现推动另建政府主导治理体系的呼声。为消解这一合法性危机、顺应互联网去中心化叙事,奥巴马政府于2014年宣布启动移交程序——将美国商务部对 ICANN 的"合同监管权"解除。2016年3月,ICANN 正式提交脱离美国政府联系的过渡计划。

真正的问题不在"美国是否善良",而在于:这根命门本来就不该由任何一个国家独握。移交的本质是把制度合法性从"美国庇护下的开放"转向"多方利益攸关体(multi‑stakeholder)自我维持的透明与问责"。但争议也正在于此——保守派担心脱离美国后,ICANN 会被威权国家通过政府咨询渠道逐步蚕食;支持者则认为根服务器的分布式结构和自下而上的共识机制本身就是防火墙。美国让出的不是善意,而是一张越来越护不住的垄断椅子。

新的发展

年份 技术/标准 RFC 主要创新
1983 DNS 概念与实现 RFC 882, RFC 883 首次提出 DNS,替代 HOSTS.TXT
1987 DNS 正式标准 RFC 1034, RFC 1035 建立现代 DNS 架构,至今仍是核心基础
1997 动态更新(DDNS) RFC 2136 DNS UPDATE 接口,允许客户端动态修改 DNS 记录
1998 NOTIFY RFC 1996 主 DNS 主动通知从 DNS 更新,不用定时轮询
1999 IXFR(增量同步) RFC 1995 只同步变更部分,降低 Zone Transfer 成本
1999 EDNS0 RFC 2671 -> 6891 打破 UDP 512 字节限制(拓展到 KB 级别)
2000 SRV 记录 RFC 2782 DNS 服务发现机制
2003 国际化域名(IDNA) RFC 3490 系列 支持中文、日文等 Unicode 域名
2005 DNSSEC RFC 4033/4034/4035 DNS 数据签名与验证
2008 Source Port Randomization RFC 5452 提高抗缓存投毒能力
2010 NSEC3 RFC 5155 防止 DNSSEC Zone Walking(枚举所有域名)
2011 CAA 记录 RFC 6844 -> 8659 允许域名所有者声明:仅承认某些 CA 给该域名颁发的证书
2013 DNS Cookies RFC 7873 防御 DNS 放大攻击
2016 DNS over TLS (DoT) RFC 7858 DNS 加密传输
2018 QNAME Minimization RFC 7816 降低 DNS 查询隐私泄露
2018 DNS over HTTPS (DoH) RFC 8484 DNS 走 HTTPS 通道
2020 SVCB / HTTPS RR RFC 9460 服务发现和 HTTPS 配置优化
2021 Oblivious DoH (ODoH) RFC 9230 隐藏客户端 IP
2022 DNS over QUIC (DoQ) RFC 9250 基于 QUIC 的 DNS
2023 DDR RFC 9462 自动发现 DoH/DoT 服务
  • DDNS

RFC 2136: Dynamic Updates in the Domain Name System (DNS UPDATE)

复用 DNS 报文格式,但把 Opcode 设为 5(UPDATE),直接在 UDP/TCP 53 端口上向权威 DNS 服务器发送增删改指令(四段式结构):

段 作用
Zone​ 声明你要改哪个 zone(如 home.arpa.或 example.com.)
Prerequisite​ 前置条件——"只有当 host A == 1.2.3.4时才更新"(原子 compare-and-swap,防竞态)
Update​ 实际操作——ADD / DELETE 某条 RR
Additional​ 认证载体——通常是 TSIG(RFC 2845)或 GSS-TSIG(RFC 3645, Kerberos)

注意:DDNS 本身可以说是一个需求,就是动态调整 DNS,虽然 RFC 2136 经常被称之为 DDNS,但实质是 DDNS 的一种技术方案。
有一些脚本或者路由器插件轮询 IP 然后通过云服务商 API 来动态修改 DNS 记录,在很多场景下也被称之为 DDNS,不要混淆。
云服务商 DNS 都不对外提供 UPDATE 操作,它们通过 HTTP API(IAM/Key 鉴权)来提供 DNS 记录更新。

RFC 2136 DDNS 的典型阵地是你自有/私有部署的受控权威服务器(BIND、PowerDNS、Windows AD DNS、Infoblox 等),其中最常见生产场景是:
客户端发起 DHCP 请求时会带上 hostname(Option 12),DHCP 服务器分配地址后,通过 TSIG/GSS-TSIG 向可写 zone 发 UPDATE,自动注册内部名 A 记录及对应 PTR(反向区也要可被管理)。

  • DNSSEC
    安全

  • DNSCrypt
    2011 年提出。
    DNSCrypt 在 DNS 请求和解析服务器之间建立加密通信,并验证服务器身份。
    它并非 IETF 标准,但曾经被 OpenDNS 等服务广泛采用。

  • DNS over TLS(DoT)
    2016 年成为标准。RFC 7858 Specification for DNS over Transport Layer Security (TLS)
    DNS 全程加密,默认使用 TCP 853 端口。

  • DNS over HTTPS(DoH)
    2018 年成为标准。RFC 8484 DNS Queries over HTTPS (DoH)
    特点:

  • 基于 HTTPS;
  • 默认使用 HTTPS 443 端口;
  • 更容易穿透网络限制;
  • 可以复用现有 Web 基础设施。

  • DNS over QUIC(DoQ)
    RFC 9250。
    基于 QUIC,兼具 UDP 的低延迟和 TLS 的安全性。

  • DNS over Tor
    通过 Tor 网络转发 DNS 查询,其主要目标是隐藏客户端来源地址,提高匿名性。
    它不是 DNS 标准协议,而是一种部署方案。

  • Oblivious DNS-over-HTTPS(ODoH)
    ODoH 在 DoH 基础上增加代理节点,确保代理服务器看不到查询内容,DNS 服务器看不到客户端 IP,从而进一步提升隐私保护能力。

参考资料与拓展阅读

#256 配置 Node 环境

2018-04-27
sudo apt install -y nodejs npm
# 已经不需要这句了:
# sudo ln -s `which nodejs` /usr/bin/node

# node -v
# npm -v

npm config set registry=https://registry.npm.taobao.org
sudo npm upgrade -g npm

# sudo npm install -g yarn --registry=https://registry.npm.taobao.org
curl --compressed -o- -L https://yarnpkg.com/install.sh | bash

yarn config set registry https://registry.npm.taobao.org

配置

echo registry=https://registry.npm.taobao.org > ~/.npmrc

npm config get registry
https://registry.npmjs.org/
npm config set registry=https://registry.npm.taobao.org

yarn config get registry
https://registry.yarnpkg.com
yarn config set registry https://registry.npm.taobao.org

#255 SMTP 拓展

2018-04-24

RFC#821 定义的 SMTP 协议非常简单(简陋)。
1993 年,RFC#1425 SMTP Service Extensions 定义了 SMTP 协议的拓展框架。
这个向前兼容的安全拓展框架是通过 EHLO 命令来实现。

#254 邮件发送中会遇到的各种地址

2018-04-16

注意:这边不是讨论 邮箱地址的格式。

格式

  • 邮箱地址
  • "名称" <邮箱地址>

含义

  • SMTP 会话(投递)
  • Mail From 真实投递的发信人
  • Rcpt To 真实投递的收信人
  • 邮件内容(显示)
  • From 发信人
    • 如果和 Mail From 地址不同,可能会显示:由 xxx 代发
  • To 收信人
  • Cc 抄送人
  • Bcc 密送人
  • Rely-To 回复地址
    • 客户端点击回复的时候用的
    • 如果没有这个字段,就会回复 From 地址
  • Sender 发信人
  • Return-Path / Reverse-Path / Envelope-From
    • 作用是在邮件投递出现问题的时候,邮件服务将邮件退回这个地址
    • 如果我们看到这几个名字
    • 可能是发信人自己在邮件中声明
    • 可能是收信方收到邮件之后添加的,单独字段,或放在 Received 头中

关于抄送和密送

碳式复写纸 carbon paper
副本,抄送 carbon copy => CC
密送 blind carbon copy => BCC

按照设计,密送地址不希望被其他收信人、抄送人察觉,只是密送地址才知道自己是密送。

CC, BCC in SMTP

SMTP 服务器不处理 CC、BCC,SMTP 客户端应该自行处理
TO 地址 + CC 地址 + BCC 地址一起放到 SMTP 会话的 RCPT TO 字段

所以,按照我的理解,邮件客户端:

在一次 SMTP 会话中,如果有 3 个 TO/CC 地址,2 个 BCC 地址,应该对那 3 个地址批量发送,然后对那 2 个 BCC 地址分别加上 BCC 头,分别发送。
更稳妥一点:如果是批量发送邮件,不要放 BCC 到邮件头!!!显示一个 密送:xxx 也没啥意义。

  • PS:MSN(Outlook),网易邮箱发出去的邮件,不会加 BCC 头
    甚至网易可能在显示邮件原文的时候会移除 BCC 头(给网易邮箱发的 BCC 头都不见了)
  • PS:Gmail,QQ 邮箱发出去的邮件,密送人会看到 BCC 头
from_addr = "from@markjour.com"
to_addrs = ["to@markjour.com"]
cc_addrs = ["cc1@markjour.com", "cc2@markjour.com"]
bcc_addrs = ["bcc@markjour.com"]

msg = f"""
From: {from_addr}
To: {", ".join(to_addrs)}
Cc: {", ".join(cc_addrs)}

Hello World
""".strip()

send_to = to_addrs + cc_addrs + bcc_addrs

server = smtplib.SMTP('smtp.126.com')
server.set_debuglevel(1)
server.login(api_user, api_key)
server.sendmail(from_addr, send_to, msg)
server.quit()

#253 Python SMTP

2018-04-13
import logging
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

logging.basicConfig(level=logging.DEBUG)

SMTP_HOST = 'smtp.example.com'
SMTP_PORT = 587
SMTP_USERNAME = 'your_username'
SMTP_PASSWORD = 'your_password'
SMTP_STARTTLS = True

sender = 'sender@example.com'
recipients = ['rcpt01@example.com', '中国 <rcpt02@example.com>']
subject = 'Test Email'
content_text = 'This is a test email.'
content_html = '<html><h1>Hello</h1></html>'


def encode_recipient(name, addr):
    pass


msg = MIMEMultipart()
msg['From'] = sender
msg['To'] = ', '.join(recipients)
msg['Subject'] = subject
msg.attach(MIMEText(content_text))
msg.attach(MIMEText(content_html))
print(msg.as_string())


def smtp_debug(self, *args):
    msg = ' '.join(map(str, args))
    logging.debug(msg)


smtp = smtplib.SMTP(SMTP_HOST, SMTP_PORT)
smtp._print_debug = smtp_debug
smtp.set_debuglevel(1)
smtp.ehlo()
if SMTP_STARTTLS:
    smtp.starttls()
    smtp.ehlo()
if SMTP_USERNAME and SMTP_USERNAME:
    smtp.login(SMTP_USERNAME, SMTP_PASSWORD)

smtp.sendmail(sender, recipients, msg.as_string())
smtp.quit()

#252 邮件中的时间格式

2018-04-10

比如:

Sun, 20 Jun 2018 00:47:04 -0700 (PDT)
Thu, 10 Jun 2021 16:10:03 -0700 (PDT)
Thu, 10 Jun 2021 08:06:31 -0700 (PDT)

定义

定义在 RFC 822 的 5. DATE AND TIME SPECIFICATION。

date-time   =  [ day "," ] date time        ; dd mm yy hh:mm:ss zzz
day         =  "Mon"  / "Tue" /  "Wed"  / "Thu" /  "Fri"  / "Sat" /  "Sun"
date        =  1*2DIGIT month 2DIGIT        ; day month year e.g. 20 Jun 82
month       =  "Jan"  /  "Feb" /  "Mar"  /  "Apr" /  "May"  /  "Jun" /  "Jul"  /  "Aug" /  "Sep"  /  "Oct" /  "Nov"  /  "Dec"
time        =  hour zone                    ; ANSI and Military
hour        =  2DIGIT ":" 2DIGIT [":" 2DIGIT]
                                            ; 00:00:00 - 23:59:59
zone        =  "UT"  / "GMT"                ; Universal Time
                                            ; North American : UT
            /  "EST" / "EDT"                ;  Eastern:  - 5/ - 4
            /  "CST" / "CDT"                ;  Central:  - 6/ - 5
            /  "MST" / "MDT"                ;  Mountain: - 7/ - 6
            /  "PST" / "PDT"                ;  Pacific:  - 8/ - 7
            /  1ALPHA                       ; Military: Z = UT;
                                            ;  A:-1; (J not used)
                                            ;  M:-12; N:+1; Y:+12
            / ( ("+" / "-") 4DIGIT )        ; Local differential
                                            ;  hours+min. (HHMM)

总结就是:

[day-of-week,] day month year hour:minute[:second] timezone
  1. 周几和秒是可选的,据我观察,没有邮件省略这两部分
  2. 周几和月份采用三字母英文缩写(首字母大写)
  3. 年份是 2 位数字,后来的规范更新中建议采用 4 位数字。出于兼容性考虑,一般都保留了对 RFC 822 两位数字年份的支持。
  4. 时区除了数字之外,可以使用 UT、GMT、EST、EDT、CST、CDT、MST、MDT、PST、PDT,
    还有 25 个字母(J 没有使用),Z 表示 UTC/GMT 时间,A - M 表示 -1 ~ -12 时区,N - Y 表示 1 到 12 时区。
  1 2 3 4 5 6 7 8 9 10 11 12
West A B C D E F G H I K L M
Eest N O P Q R S T U V W X Y

Python

生成符合要求的时间字符串比较简单:

import time
time.strftime('%a, %d %b %Y %H:%M:%S %z')
# 'Tue, 10 Apr 2018 09:10:05 +0800'

但是由于这个灵活度比较大,解析起来最好借助专业的库(email.utils)来做这个事。

import time
import datetime
import email.utils
import pytz

# 解析 ############################################

date_str = 'Sun, 20 Jun 2018 00:47:04 -0700 (PDT)'
email.utils.parsedate_to_datetime(date_str)
# datetime.datetime(2018, 6, 20, 0, 47, 4, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
email.utils.parsedate_tz(date_str)
(2018, 6, 20, 0, 47, 4, 0, 1, -1, -25200)

# 生成 ############################################

# email.utils.formatdate(timeval=None, localtime=False, usegmt=False)
email.utils.formatdate()
# 'Tue, 10 Apr 2018 09:10:41 -0000'

# email.utils.format_datetime(dt, usegmt=False)
dt = datetime.datetime.now()
email.utils.format_datetime(dt)
# 'Tue, 10 Apr 2018 09:16:43 -0000'

tz = pytz.timezone('Asia/Shanghai') # <DstTzInfo 'Asia/Shanghai' LMT+8:06:00 STD>
dt = datetime.datetime(2018, 4, 10, 9, 10, 0, tzinfo=tz)
# datetime.datetime(2018, 4, 10, 9, 10, tzinfo=<DstTzInfo 'Asia/Shanghai' LMT+8:06:00 STD>)
email.utils.format_datetime(dt)
# 'Tue, 10 Apr 2018 09:10:00 +0806'

#251 邮箱地址的格式

2018-04-07

规范

规范定义比较复杂,甚至支持注释。

我简化一下(去掉注释,去掉双引号,去掉 [IPv4] / [IPv6] / 主机名 做域):

  1. 格式:域内部分@域
  2. 域内部分:

  3. 长度不超过 64

  4. 大小写字母 + 数字(62)
  5. ASCII 标点符号(19)

    !#$%&'*+-/=?^_`{|}~
    
  6. 可以加入点号(.)隔开,不放首尾,不连续出现

  7. 域名

  8. 每一级域名 1 - 63 个字符,总长度不超过 253
    这个限制和 DNS 报文设计有关
    国际化域名转换成 Punycode 之后也必须遵守这个约定

  9. 允许包含数字、字母(大小写不敏感)和短横线(-)
  10. 短横线不能出现在首尾位置

实践

实际上的邮件地址会更加简单:

  1. 长度限制
  2. QQ 邮箱 3 - 18
  3. 网易邮箱 6 - 18
  4. Gmail 6 - 30
  5. 新浪邮箱 4 - 16
  6. 字符限制:字母数字 + .-_
  7. 一般大小写不敏感
  8. 连字符(.-_)不可连续出现
    1. 网易免费邮箱只支持下划线,网易 VIP 邮箱支持点和下划线
    2. Gmail 只支持点和加号
      1. 在实际投递中,点和加号会被忽略
      2. 点可以用作单词风格
      3. 加号通常用做来信归类,比如注册淘宝时 +taobao,订阅开发者头条时 +toutiao,相关邮件就方便搜索归类。
  9. 部分邮箱不支持全数字(别有用途,或是避免 QQ 号冲突,或是避免手机号冲突)
  10. 开头结尾字符限制:
    1. 字母数字开头 + 字母数字结尾
    2. 字母开头 / 字母数字结尾

正则表达式

以规范为准,参考真实场景下的实践:

域内部分:

/[a-z0-9]+([.-_#][a-z0-9]+)+/;

域名部分:

/[a-z0-9]+(-[a-z0-9]+)?(\.[a-z0-9]+(-[a-z0-9]+)?)+/;

汇总在一起就是:

/^[a-z0-9]+([.-_#][a-z0-9]+)+@[a-z0-9]+(-[a-z0-9]+)?(\.[a-z0-9]+(-[a-z0-9]+)?)+$/;

参考资料与拓展阅读

#250 HTTP 缓存总结

2018-04-06

Your Cache Headers Could Probably be More Aggressive
您的缓存标头可能更具侵略性
https://macarthur.me/posts/more-aggressive-cache-headers

HTTP 缓存的核心并不复杂,本质上解决两个问题:

  1. 缓存还能不能直接使用?
  2. 不能直接使用时,资源有没有发生变化?

第一次请求资源后,浏览器可能保存完整的 HTTP Response(Headers + Body)。
之后再次请求时,先判断缓存的新鲜度。新鲜则直接使用;过期则向服务器验证。
资源未变化服务器返回 304,否则返回 200 + body。

flowchart TB
    s([准备发起请求]) --> hasCache{有缓存?}

    hasCache -- 否 --> request[发起请求] --> e([结束])
    hasCache -- 是 --> isCacheExpired{缓存过期?}

    isCacheExpired -- 否 --> readCache[读取缓存] --> e
    isCacheExpired -- 是 --> hasEtag{有 ETag?}

    hasEtag -- 是 --> addHeaderIfNoneMatch[添加 If-None-Match 头] --> request
    hasEtag -- 否 --> hasLastModified{有 Last-Modified?}

    hasLastModified -- 是 --> addHeaderIfModifiedSince[添加 If-Modified-Since 头] --> request
    hasLastModified -- 否 --> request

    request --> response{响应?}

    response -- 是 --> e
    response -- 否 --> readCache

一、HTTP 缓存相关 Header

现代 HTTP 缓存主要围绕以下 Header 工作:

Header 作用
Cache-Control 定义缓存策略和新鲜度
ETag 标识资源版本
Last-Modified 标识资源最后修改时间
If-None-Match 根据 ETag 进行条件请求
If-Modified-Since 根据修改时间进行条件请求
Expires HTTP/1.0 时代的过期时间
Pragma 历史兼容机制

其中最核心的是:

Cache-Control
ETag / Last-Modified
If-None-Match / If-Modified-Since

Expires 和 Pragma 主要用于历史兼容,现代应用优先使用 Cache-Control。

二、HTTP Header 常见问题

1. no-cache 是不是不缓存?

不是。
表示允许缓存,但使用前必须重新验证。
no-store 才表示禁止存储响应。

指令 允许存储 使用前验证
no-cache 是 是
no-store 否 不适用

2. max-age 控制浏览器还是 CDN?

都可能。

Cache-Control: public, max-age=60, s-maxage=3600

通常表示普通缓存的新鲜时间为 60 秒,而共享缓存(如 CDN)可以使用 s-maxage 的 3600 秒。

3. ETag 是不是文件 MD5?

不是。

ETag 只是服务器定义的实体标签,可以由 Hash、版本号等方式生成。

4. 304 是不是重新返回资源?

不是。

200 → 返回资源
304 → 资源没变化,继续使用已有缓存

304 不重新传输原来的 Response Body。

5. 浏览器刷新为什么还可能出现 304?

刷新不等于清空缓存。浏览器可能要求重新验证缓存,于是发送:

If-None-Match: "abc123"

服务器确认资源没有变化后返回 304 Not Modified。

具体行为还取决于浏览器、刷新方式以及开发者工具设置。

三、强缓存

“强缓存”不是 HTTP 标准术语,而是开发领域对缓存新鲜时直接使用这一行为的称呼。

例如:

Cache-Control: max-age=3600

第一次请求:

Browser → Server → 200 + Resource
                       ↓
                  Browser Cache

缓存仍然新鲜时:

Browser → Browser Cache

无需向服务器发送请求。

因此:强缓存解决的是“要不要发网络请求”。

需要注意,缓存过期并不意味着缓存立即被删除,而是通常不能再无条件使用,需要进入重新验证流程。

四、协商缓存与 304

缓存过期后,客户端可以利用 ETag 或 Last-Modified 发起条件请求。

  • ETag 可以理解成文件版本:

例如第一次请求服务器返回 ETag: "abc123"
后续的请求就可以带上 If-None-Match: "abc123"

服务器比对本地资源 ETag 就可以根据资源变化情况返回 304 或 200 + 新 ETag。

  • Last-Modified 是变更时间:
# 对应 ETag
Last-Modified: Tue, 08 Sep 2026 10:00:00 GMT

# 对应 If-None-Match
If-Modified-Since: Tue, 08 Sep 2026 10:00:00 GMT

总之,服务器端和浏览器根据这些头来协商缓存,确认资源有没有变化,以及是否需要重新传输 Body。

五、浏览器与 CDN 的多级缓存

生产环境通常不是:

Browser → Origin

而是:

Browser → CDN → Origin

甚至:

Browser → Proxy → CDN → Origin

浏览器缓存命中时,请求根本不会到达 CDN。

浏览器缓存过期后,请求到 CDN;如果 CDN 仍然有新鲜缓存,也无需回源。

因此每一层都可能独立判断:是否存在缓存?缓存是否 Fresh?是否需要重新验证?是否需要继续向下一层请求?

这也是理解 CDN 缓存的基础。

六、生产环境缓存策略

不同资源应该采用不同策略。

  • HTML:让浏览器保存,但使用前重新验证,及时获取最新入口:Cache-Control: no-cache
  • JS / CSS

    如果文件名带内容 Hash,比如 app.8f3a2c.js,可以长期缓存:

    Cache-Control: public, max-age=31536000, immutable
    

    内容变化后生成新的 URL,从根本上避免旧版本缓存问题。

  • 公共 API:根据实时性设置:Cache-Control: public, max-age=60

  • 用户私有数据:Cache-Control: private
    敏感数据则考虑:Cache-Control: no-store

#249 OpenStack 相关网络技术

2018-03-24

相关文章:

物理设备

  1. VLAN 虚拟局域网,设备层面上的网络分区,网络设备提供的功能
    网络报文给 VLAN Tag 分配了四个字节,其中 3 个字节用于 VLAN ID,1 个字节用于 VLAN Priority。
    作为 VLAN ID 的 12bit(0-4095)中,首位两数作为保留值,也就是说 VLAN 技术支持的最大网络数是 4094。

Linux 网络技术

在 Linux 内核的网络设备管理层,虚拟设备和物理设备是同等地位。

  1. network namespace 网络隔离,虚拟化的基础
  2. bridge 网桥,相当于交换机,二层数据交换
  3. veth 虚拟网口,成对出现,两个虚拟网口之间可以相互连接(可以跨 namespace)
  4. tap/tun
  5. tap TAP 设备,虚拟二层网络,处理 TCP/UDP 包,
    有自己的 MAC 地址,可以桥接到物理网卡
  6. tun TUN 设备,虚拟三层网络,处理 IP 包
  7. iptables 网络管理
    确切的说是以 iptables 为代表的一系列网络管理技术

KVM / Neutron

  1. qvb neutron 网络桥
  2. qvo neutron 网络虚拟接口

Neutron 网络模式:

  1. VLAN
  2. VXLAN 虚拟拓展局域网,在三层 UDP 协议中封装二层数据包,突破 VLAN 的限制
  3. GRE Gerneral Routing Encapsulation,通用路由封装协议