文章是本站的原创长文。每篇都保持无脚本、来源可溯,并与本站记录的系统现实一致:一个小型、静态、公网服务如何被构建、加固并保持诚实。

缓存策略需要行为测试,而不只是检查头部是否存在。两个响应即使都带 max-age,也可能因为其中一个要求验证、另一个禁止存储而表现不同。请求本身同样影响结果:浏览器可以绕过已有副本,却不修改源站的响应策略,也不删除更早保存的缓存条目。要知道用户真正拿到什么,必须观察完整的重复请求过程。
Fetch no-store 取得版本二,原有缓存版本一仍保留;reload 取得并存入版本三。响应 Cache-Control 策略与 Fetch 缓存模式应分开。 仅为来源示意;本任务未执行示例。 双方都要读同一份约定 Cache-Control 指令具有方向性。请求指令不会自动复制到响应里,RFC 9111 还明确将请求指令描述为建议性的要求;响应指令则约束遵守协议的缓存。中间层即使自己不实现缓存,也必须在转发中保留相应缓存指令。部署配置可能另有覆盖规则,但“代理按自己的配置做了”不等于已经证明源站声明的策略得到执行。
应拆开三个问题:这条响应能否存储,当前年龄是否仍在适用的新鲜度寿命内,这次请求是否允许不经验证直接复用。这里的 no-store 禁止 HTTP 缓存存储;不带字段名限定的 no-cache 允许存储,但复用前必须成功验证。示例不包含能够在特定条件下覆盖 no-store 的 must-understand 组合,不能据此推导所有指令组合的行为。max-age 设定新鲜度,must-revalidate 则禁止响应过期后未经验证就复用。响应仍然新鲜时,must-revalidate 本身并不要求每次回源,不能把它误解成 no-cache。
...

公用表表达式在一条 SQL 内为查询结果命名。它可以先说明数据总体和中间计算,再写最终 SELECT,让复杂报表更容易阅读。这个名字不是持久表,也不自动承诺缓存结果、固定扫描次数或按顺序执行的一组命令。理解 CTE 时,应把可读结构与执行方式分别检查。
先确定作用域,再讨论执行 WITH 子句只属于当前语句。后续语句不能继续引用它的名字,执行结束后也没有一个可供建立索引或查询的 schema 对象。如果计算结果或查询定义需要保留到后续语句继续使用,应选择临时表、视图或其他合适的数据库对象。CTE 适合命名结果与语句作用域相匹配的工作,不能靠命名制造跨请求缓存。
现代 PostgreSQL 可以把非递归、没有 volatile 函数的 SELECT CTE 折叠进外层查询。默认情况下,只引用一次时会进行这种合并,使外层限制参与共同优化。因此,外层按主键查找时,条件可以直接作用于底层表的索引,而不必先生成独立中间结果再过滤。
同一个符合条件的 CTE 被引用多次时,PostgreSQL 默认通常会物化它。这能减少重复昂贵计算,但也可能阻止某个引用位置上的选择性条件被推入共享计算。这个区别从 PostgreSQL 十二开始适用;认为所有 WITH 都是固定优化边界,描述的是旧行为,不能覆盖当前默认规则。应根据实际版本和引用次数检查,而不是依赖旧经验。
物化是一项取舍 MATERIALIZED 请求独立计算。NOT MATERIALIZED 允许多次引用时也进行折叠,可能重复一部分工作,但让每次使用只访问自己需要的数据。对于递归或不是无副作用 SELECT 的 CTE,PostgreSQL 会忽略 NOT MATERIALIZED;它不会因此允许任意复制写入或 volatile 操作。
物化的 SELECT CTE 也不一定完整算到末尾。消费方可能只请求部分结果,不过排序、关联等操作又可能要求读取更多。物化同样不等于为后续语句建立一张带索引的可复用表。决定强制边界之前,应查看计划并测量真实负载,不能因为“物化”听起来像缓存就默认收益更好。
...

Linux 的 overcommit 政策控制某些虚拟内存承诺是否可以获准,和分配常驻物理页、选择 OOM 牺牲进程、执行容器内存限制并不是同一件事。一次成功申请可能早于实际物理需求,但这不表示所有分配都没有成本,也不保证后续访问在任何资源约束下都能成功。分析时应把承诺、驻留和故障处理分别记录,而不是都称为“剩余内存”。
虚拟承诺不同于实际驻留 malloc 等分配器可能通过 brk 或 mmap 取得内存,也可能直接复用已有内存区,不发起新的系统调用。匿名页通常按需填入,但地址空间元数据、页表以及实际缺页过程仍然消耗资源。具体成本取决于应用申请什么、访问什么,不能把延迟分配简化成近乎免费地运行整个进程。
Committed_AS 是记账数量,不是所有进程常驻集之和,也不是当前内存压力读数。映射类型会影响成本:私有可写映射可能在页面尚未全部驻留时就形成承诺,普通共享或只读文件映射则采用不同规则。因此,虚拟大小很大的进程未必就是物理内存短缺的责任方,应同时查看实际使用和对应记账范围。
三种准入政策 模式零是默认启发式政策,拒绝明显超额的申请,同时允许超过简单常驻内存预算的分配。它的判断不等于为每个获准字节准备专属 RAM。模式一跳过 overcommit 准入检查,有助于某些稀疏工作负载,但地址空间上限、映射数量以及其他分配失败仍然存在,不能描述成任何荒谬请求都会被无条件接受。
模式二执行更严格的承诺记账。名义 CommitLimit 来自交换空间加上配置的内存额度:比例计算会扣除预留给 hugetlb 的内存,非零 overcommit_kbytes 则用绝对额度代替比例。内核保留量与调用上下文还会影响实际准入。因此,CommitLimit 与 Committed_AS 的差值是有用背景,不是保证任意调用者都能一次申请成功的最大值,也不是当前可用物理内存。
需要记账的分配如果无法纳入预算,可能立即返回 ENOMEM,使部分容量问题更早暴露,包括 fork 和后续改变映射权限所需的承诺。fork 记账与具体映射及写时复制义务有关,并不是简单把父进程常驻集乘二。有些应用有意保留稀疏地址空间,因此政策变化可能影响启动,即使它们预计不会触碰所有页面。
严格记账不是 OOM 免疫 模式二降低一类超额承诺风险,却不会覆盖 cgroup 内存限制、NUMA 约束、其他内核分配以及所有后续需求。任务仍可能遇到内存分配失败或 OOM 事件。不能承诺选了模式二,某个业务工作进程就绝不会被杀,更不能把这个模式当成服务可靠性的单一开关。
...

CronJob 把一份 Job 模板与循环时间表组合起来。CronJob 控制器负责创建 Job,再由 Job 管理执行工作的 Pod。一次运行可以包含重试、替换 Pod,或按照 Job 配置启动多个并行工作者,不应把它简化为每次只启动一个短命容器。两次运行之间不需要专门的应用进程等候触发,但已经完成的 Job 和 Pod 可能继续保留,供排查与审计使用。
时间表只是近似兑现。某个时刻可能生成重复 Job,也可能完全漏掉,因此不能无条件承诺每个计划时刻都至少执行一次。应用既要能安全重复执行,也要记录哪些业务周期真正完成。幂等性解决重复效果,独立的完整性检查解决根本没有运行的工作,这两个问题不能互相替代。
时间表是近似调度,不是持久队列,也不保证每个时刻的工作都完成。startingDeadlineSeconds 限制启动迟到,不限制运行时长。原文命令未重跑;执行状态仍为受阻。 时间表产生的是 Job 五个字段依次表示分、时、日、月与星期,支持区间、步长和 @daily 等宏。*/10 * * * * 表示每十分钟的计划时刻,并不保证 Pod 恰好在那个瞬间开始执行。即使 Job 已经建立,节点资源不足、镜像拉取或调度过程仍可能让实际工作晚于计划。
...

EXPLAIN 展示 PostgreSQL 的执行计划,EXPLAIN ANALYZE 还会真正执行语句并测量工作量。最有用的比较,是同一个节点的估计行数与实际行数。两者都不能单独当成结论:估计来自模型,实测则只描述特定数据、缓存、并发和插桩条件下的一次执行。不能把前者称为谎言,也不能把后者当成脱离环境的永恒事实。
ANALYZE 会真正执行。文章示例区分可恢复的表变更与已消耗的序列值;本图没有重新运行实验。 先比较同一种量 计划中的 cost 是相对成本单位,不是毫秒。两个成本值分别描述启动成本与完成成本,带 LIMIT 的查询可能更重视尽早得到前几行,因此选择不同计划。actual time 才使用毫秒。即使成本和耗时恰好都打印成一百,也不能把它们直接相减或求比例;它们本来就不表示同一件事。
一个节点被重复执行时,actual rows 和节点耗时是每次循环的平均值,需要结合 loops 理解总工作量。但不能把所有节点耗时简单相加,因为父节点已经包含子节点的工作,重复相加会重复计算。并行执行时更不能用这种方式猜总延迟。先沿树看输入输出、循环次数、过滤条件和行数流向,再决定哪个位置值得调查。
BUFFERS 区分 shared、local 和临时块活动。shared hit 表示 PostgreSQL 在共享缓冲区里找到了块;shared read 只表示需要读入,操作系统仍可能从页缓存满足它,不能直接断言每次都访问了物理磁盘。临时表使用本地缓冲区,排序或哈希溢出则可能读写临时文件。父节点计数包含子节点,逐行把所有计数加起来同样会夸大 I/O。
...

布隆过滤器能廉价地排除某个元素属于它所表示的集合,而肯定答案只能解释为“可能存在”。这种不对称依赖更新正确、哈希规则保持一致。对于已经插入的元素,普通只增不减算法没有假阴性;但过时或损坏的过滤器仍可能把数据库里真实存在的键判断成不存在,不能把算法性质无限扩大成应用保证。
位图保存的是候选线索,不是记录 先准备一张包含 m 个零位的位图,并为每个键计算 k 个哈希位置。插入时将这些位置全部置一;查询时,只要任意所需位置为零,就返回否定,否则返回“也许”。不同键可能共用位置,所以全部位置都是一,并不能证明查询的键真的被插入过。
再次插入同一个键,不会改变已经置好的位。过滤器没有保存原始键和值,也不能枚举原集合。如果误判会影响正确性,精确集合或数据库仍然是权威答案;不能仅凭“也许存在”就授予权限、扣款,或者丢弃必须处理的工作。少一次查询的收益不能替代业务判断。
按预计元素规模分配空间 在哈希近似均匀且相互独立的常用模型下,插入 n 个不同元素后的假阳性概率近似为 p = (1 − exp(−kn/m))^k。固定 m 和 n 时,k 接近 (m/n) ln(2) 可以使这个近似值最小。哈希次数并不是越多越好:除了增加计算,它也会更快地把位图填满。
选择合适的哈希数量后,百分之一的误判目标大约需要每个元素九点六位,尚未计算实现开销。一千万个元素因此需要大约一千二百万字节的位图,并不是几百万个位就够用。讨论空间时必须区分位、字节、位图本体以及整个进程的内存,避免把不同口径混在一起。
假阳性率针对的是确定不存在的元素查询,不是所有肯定答案中错误答案所占的比例;后者还取决于调用方有多大概率查询真实存在的元素。应使用已知非成员测量,并将有限样本的观察值与概率模型区分开来,不能看到一次误判较少就宣称达到了严格概率保证。
让表示的集合跟上权威数据 如果准备根据否定答案跳过数据库查询,过滤器就必须包含所有相关数据库插入。先发布新行、稍后才更新过滤器,会产生可见性缺口。重建过程也需要定义快照和增量更新如何交接;过滤器的一次查询成功,并不能证明应用层这些步骤已经原子完成。
写入方和读取方还必须保持位数、哈希构造、种子或域前缀以及字节编码一致。改变这些输入就会改变位置。普通布隆过滤器不能通过清除某个元素对应的位来安全删除,因为其他元素可能依赖同一位;计数型或其他支持删除的结构,使用的是不同契约,不能把接口混为一谈。
坏掉的样子 向固定大小的位图插入过多元素,会提高假阳性率,逐渐失去减少昂贵查询的收益。正确插入的元素仍不会被误判为不存在,但查询越来越难得到否定答案。扩大固定过滤器需要重建或采用相应增长设计,不能只修改取模用的位数,否则已有位的位置含义就变了。
真实产品不一定像下面的固定大小模型。Redis 的 BF.RESERVE 在允许扩展时可以增加子过滤器,禁止扩展时则在达到容量后拒绝插入。子过滤器会增加内存和查询工作;应检查实际配置与实现,不能假定产品会像没有容量检查的插入循环一样静默变得饱和。
更新丢失、编码不一致、快照落后和错误删除,都可能破坏应用期望的否定答案安全性。除了观察假阳性,还要记录插入失败与过滤器新鲜度。过滤器是一项优化,它的维护仍属于数据生命周期的一部分,不是创建一次之后就可以忽略的永久索引。
布隆:也许并不等于证明。依据原文绘制机制;模型值与原文报告的观察不代表本次运行测试。 快速验证 保存为 bloom_check.py,用 Python 3.10 以上版本执行 python3 bloom_check.py。实验已在 Python 3.14.4 上运行,仅用标准库。七个带域前缀的 SHA-256 摘要、明确的 UTF-8 编码和固定键序列保证测试可重现,不需要外部服务、网络访问或文件写入。
...

扩展属性在普通文件内容流之外,为文件系统对象附加有名字的字节值。应用可以使用 user 命名空间存放小型元数据,其他命名空间则具有内核、安全模块或特权相关语义。它是否可靠,取决于文件系统、访问规则和传输工具。只有工作流程按明确契约保留并解释标签,元数据才能成为下游判断的有效依据。
多个名字可以共享 inode 及其属性。复制会产生独立对象,元数据是否保留取决于工具、目标端和权限;内容相同并不足以验证所需属性。 坏掉的样子 属性值不一定是文本,可以包含空字符或任意二进制数据。把它当作以零结尾的字符串,或者假定固定编码,可能破坏原值。空属性和缺失属性也不是同一状态;如果都返回空字符串,就会隐藏元数据到底被刻意设为空,还是在传输中丢失。使用前需要明确编码与缺失语义,而不是依赖显示工具碰巧如何呈现。
同一文件系统内重命名保留对象及属性,复制内容通常却创建另一个 inode。复制工具决定尝试保留哪些元数据,因此文件哈希相同不能证明扩展属性相同。流程可能成功传递每一个内容字节,却遗漏下游需要的分类、来源或应用版本标签。内容校验通过,只能证明它实际覆盖的内容范围。
命名空间名称不会自动建立应用授权规则。设置 user.classification 为 private,不会让本来可读的文件变得保密。user 属性服从适用的文件访问权限,安全属性则有另外的文档语义与策略。把应用标签当作已经强制执行的访问控制,可能在标签看起来完好的同时暴露内容,因此需要检查实际读写行为。
...

CONNECT 请求代理建立隧道,并不让代理从数据路径上消失。准入成功之后,代理仍然向两个方向转发字节,并管理套接字、缓冲区和故障处理。CONNECT 本身不提供加密;HTTPS 是在这条隧道内部另行建立并验证 TLS 会话,不能把两个阶段的成功混为一谈。
在正确的边界切换语义 HTTP/1.1 的 CONNECT 目标是 server.example:443 这样的主机与端口组合,而不是普通路径或完整 HTTPS URL。端口必须明确给出。成功的 2xx 响应在响应头结束后立即切换到隧道模式;非成功响应则表示隧道尚未建立,此时不能直接开始把后续内容当作目标服务的数据。
成功的 CONNECT 响应不能携带 Content-Length 或 Transfer-Encoding。随后出现的字节属于隧道,而不是一份用长度界定的 HTTP 响应正文。自己实现中继时,这一点尤其重要:HTTP 解析器已经缓冲的字节也可能属于隧道,不能因为切换到原始套接字,就把此前预读的数据丢掉。
客户端随后可以穿过中继,与目标协商 TLS。代理返回 200 只说明允许并建立了隧道,不负责证明目标证书有效,也不证明应用请求成功。客户端仍须检查信任链、主机名与应用响应。TLS 拦截属于另一种部署方式,需要客户端信任会终止 TLS 的中间实体,不能把它与普通盲转发混为同一种安全模型。
看不见正文,仍然占用资源 盲中继不必解释加密的应用内容,但仍能观察目标地址、字节量、时序和关闭行为。它可以落实连接预算、超时、流量限制,以及撤销准入之后的断开操作。无法读取 TLS 内部的 HTTP 消息,不等于失去所有运营控制,更不等于不再承担带宽和描述符成本。
真正的正向代理应验证调用方身份,并限制目的地。任意主机和端口访问,可能暴露内部服务,或被用于其他协议的滥用。按主机名放行时,还必须考虑地址解析策略:通过名称检查,并不自动证明最终拨号的 IP 地址符合约束。本地实验只允许一个预先确定的环回地址与端口,因此没有引入任意目标的解析和转发能力。
...