累计 ACK 停在缺失位置,SACK 描述之后已经到达的字节范围。

SACK 报告缺口之后已收到的范围,帮助定位需要恢复的数据

TCP 累计确认标识按序接收时下一个期待的字节。如果某段缺失、后续字节却已到达,累计确认不能跨过这个空洞。选择确认 SACK 补充报告累计边界之后已经收到的字节范围,帮助发送端区分真正缺失的数据与接收端已经排队的数据。它改善恢复算法获得的信息,而不是替代全部丢包判断、拥塞控制或应用交付语义。 坏掉的样子 没有 SACK,并不意味着累计 ACK 强制发送端重传后面的每一个段。发送端仍可根据重复确认、计时器和恢复算法推断丢失,只是反馈不够明确,尤其在存在多个缺口时。把普通 TCP 描述为必须重发整个后缀,是把信息能力的限制误写成强制恢复策略,会夸大差异,也无法准确解释真实抓包中的局部重传。 SACK 块报告已经收到的范围,不是直接列出缺失范围。每块在序列空间中具有包含的左边界和不包含的右边界。缺口需要结合这些块、累计确认及尚未确认数据推导。SACK 不改变普通 ACK 字段含义,应用仍获得有序字节流,而不是乱序的选择确认块。传输层暂存后续字节,并不等于已经把它们交给应用。 该能力还需要对当前连接取得许可。SYN 中的 SACK-Permitted 表示发送该 SYN 的一端允许对端向自己发送 SACK 选项,不证明一次无丢失传输中实际出现过 SACK 块。反过来,缺少 SACK 也不自动证明防火墙剥离选项。判断原因之前应核对端点设置与真实握手,不能把某种中间设备问题当成唯一解释,忽略连接本身的协商条件。 快速验证 将脚本保存为 check-sack.py,准备 Docker 后运行 python3 check-sack.py。一次性环境使用 Python 3.11.16、iproute2 6.1.0、nftables 1.0.6 和 tcpdump 4.99.3,在 Linux 6.12.76 上验证。NET_ADMIN 与 SYS_ADMIN 支持嵌套网络命名空间及临时 proc 挂载。实验仅使用新建 veth 和命名空间内部设置,不改变宿主现有流量: ...

九月 11, 2027 · 3 分钟 · XB Field Notes
观察、决策、行动三个方框,旁边强调会话恢复不等于零往返承诺。

TLS 会话恢复省一次往返

“省一次往返”需要结合协议版本和 early data 模式理解。TLS 1.3 的普通完整握手,以及不携带 early data 的恢复握手,通常都需要一次 TLS 往返。恢复仍然可以减少证书传输和身份认证开销,但不能把标题理解成“只要开启票据,每个 HTTPS 请求就一定提前一个 RTT 开始”。 新连接仍要派生新的流量密钥 TLS 会话恢复使用先前握手派生的 PSK,为一条新连接建立安全上下文。NewSessionTicket 提供的是与恢复状态关联的不透明标识,不是让后续连接直接沿用原来的流量密钥。新的握手仍会根据自身上下文派生密钥。服务器可以用密码学方法保护票据状态,也可以在不透明标识后保存服务端状态;应用不应把票据解析成一种通用登录凭据。 服务器接受 PSK 认证时,不再发送自己的 Certificate 和 CertificateVerify 消息。恢复可以结合一次新的临时 Diffie-Hellman 交换,获得相应的前向保密;协议也允许仅使用 PSK 的模式,后者没有这项保护。因此,“拿到了票据”既不能证明实际选用了哪种密钥交换,也不能独自证明节省了多少网络等待。 可选的零往返模式允许符合条件的应用数据跟随 ClientHello 一起发送,这与普通会话恢复是两件事。early data 不具有前向保密,并可能跨连接被重放;某条连接后来完成握手,也不能消除另一条连接上已经发生或可能发生的重放。HTTP 服务需要按资源的实际副作用决定能否接受重放风险,并正确处理拒绝,包括规范要求的 425 Too Early 响应。 坏掉的样子 客户端可能根本没有启用票据缓存,可能在处理握手后票据之前就关闭连接,也可能每次请求都重建并丢弃缓存。服务器重启、密钥轮换或票据过期后,也可能不再接受原来的恢复状态。一次恢复尝试最终走完整握手,不自动等于 TLS 服务不可用;恢复比例和建连成功率应该分别观察。 ...

八月 8, 2027 · 2 分钟 · XB Field Notes
观察、决策、行动三个方框,旁边强调跟踪各阶段并区分连接复用。

TLS 握手会拉长延迟,但可以缩小

HTTPS 延迟由多个阶段组成,重复访问可以跳过其中一些阶段,却不一定是加密运算本身变快了。DNS、获取连接、TCP 建连、TLS 认证、服务端处理和响应传输,各自有不同的计时边界。分析前应先确认哪些阶段真的执行过;单看一次请求总耗时,无法区分复用既有连接与新连接上的 TLS 会话恢复。 消息会按批次传输 TLS 握手虽然包含多条消息,但并不是每条消息都需要单独一次往返,多条消息可以在同一批次中发送。通常的 TLS 1.3 完整握手在普通客户端应用数据之前需要一次 TLS 往返,HelloRetryRequest 会再增加一次。TCP 建连和之后的应用请求、响应,还属于另外的成本。 不使用 early data 的普通 TLS 1.3 会话恢复,通常也需要一次 TLS 往返。它能减少证书传输与认证工作,却仍可能执行新的临时密钥交换;恢复时派生的是新流量密钥,不是原封不动继续使用旧加密密钥。可选零往返数据有额外的重放安全限制,不能把一次快速重复请求直接叫作零往返。 HTTP 连接复用则跳过了建立另一条连接的步骤,因此该请求没有新的 TLS 握手。HTTP/2 多路复用也可以让多个流共享已建立连接。相反,关闭空闲连接再重新连接,即使缓存票据允许 TLS 恢复,仍然需要重新完成传输层建立,不能把两种节省混成同一个动作。 坏掉的样子 正常请求若每次创建新的 transport,就会丢掉连接池。响应正文没有读完或关闭,也可能妨碍复用。空闲超时、入口变化或并发需求都可能增加新连接数量,因此在把延迟变化归因于证书算法之前,应先按真实复用情况和握手事件分类,确认自己比较的是同一种路径。 尝试恢复后仍发生完整握手,并不必然意味着服务故障。票据可能过期或被拒绝,客户端也可能没有可用缓存。应保留普通证书验证,并观察最终选择的行为;为赢得性能数字而减少认证检查,改变的是安全约定,不是在同一个操作下获得了免费优化。 计时名称也要有定义。首字节时间包含的不只有 TLS,总时间还包括消费响应内容。跟踪回调可能来自不同协程,收集器必须正确同步。应比较有代表性的分布及连接类别,不能拿几次回环请求的微秒数去预测公网加速,也不能把偶然更快的一次执行写成稳定性能承诺。 快速验证 保存为 latency_probe.go,使用 Go 1.23 或更新版本运行 go run latency_probe.go。实验固定使用经过验证的 TLS 1.3 和 HTTP/1.1,服务端为接受的 TCP 连接编号。第一个 transport 同时保留 HTTP 连接池和 TLS 会话缓存,另一个独立 transport 刻意不配置会话缓存。 ...

六月 28, 2027 · 3 分钟 · XB Field Notes
在多个请求之间复用同一条 TCP 连接。

HTTP keep-alive:复用同一条连接

HTTP 持久连接允许多次请求与响应复用同一条连接,减少重复的 TCP 建连;使用 HTTPS 时,也可能省去新的 TLS 协商。收益取决于网络延迟、响应大小、服务端处理时间以及池中是否已经有可用连接。连接复用不会消除业务计算,也没有承诺某个套接字永远保持打开。讨论优化之前,先明确观察的是哪一段连接、哪个协议版本。 完整报文才允许复用 HTTP/1.1 默认允许持久连接,除非某一方发送 Connection: close,或实际关闭传输连接。通常不必额外添加 Connection: keep-alive。客户端连接池可以保留空闲连接,供符合源、代理、证书和池策略的后续请求使用。连续执行两次独立的命令行程序,通常不会共享内存中的连接池,因而不能凭两次响应成功就断言已经复用。 复用的前提是双方都能识别每条消息的结束位置。响应可以通过准确的 Content-Length、分块传输编码,或无正文响应的协议规则界定边界。如果只能依赖关闭连接表示正文结束,这条连接自然无法留给下一次请求。客户端需要读完响应正文;服务器需要读完请求正文,或者关闭连接。残留字节一旦被当成下一条消息解析,问题就从性能退化变成报文错位。 HTTP 持久连接与 TCP keepalive 也是两回事。后者在传输连接空闲足够久后发送探测,帮助识别失联的对端,不负责 HTTP 消息分界,也不能覆盖代理或应用自己的空闲超时。仅仅打开操作系统的探活选项,并不会让一个已经被代理回收的连接重新可用。排查时应分别记录应用层关闭、传输层错误和连接池回收动作。 顺序复用还不同于流水线请求。客户端可以等前一个响应完成后再发送下一条请求,同时一直使用相同连接。HTTP/1.1 流水线允许提前发送请求,但响应仍须按序返回。因此,仅有持久连接并不具备 HTTP/2 多路复用提供的独立响应流,不能把两者的并发收益混为一谈。 每一跳管理自己的超时 空闲超时按照具体实现的规则衡量无活动时间,请求截止时间则限制一次操作能等待多久,两者需要独立配置。反向代理同时拥有客户端连接和上游连接,两侧可能使用不同协议、连接池和超时。因此,浏览器到边缘复用了连接,并不能证明边缘到源站也复用;看到一侧握手减少,也不能直接解释另一侧的延迟变化。 连接池应结合已知的服务端限制提前淘汰空闲连接,但始终可能发生竞态:客户端刚发送下一条请求,服务器恰好开始关闭。监控应同时包含新建连接数、成功复用次数、连接重置、池占用和请求延迟。保留更久可以减少握手,也会占用文件描述符与内存。应依据实际请求间隔和资源预算确定保留策略,不能仅凭“站点很忙”推导一个固定超时。 坏掉的样子 正文长度错误可能让读取方一直等待,或让下一条响应的开头混进上一条正文。遇到请求挂起时先检查消息分界,再考虑调大超时。另一个常见故障是调用方放弃流式响应,却既不读完也不关闭,导致池中的连接长期占用。此时服务器还有处理能力,新的请求却全在客户端等空位,扩大服务端容量无法解决这个问题。 陈旧连接报错不代表服务器没有执行请求。如果支付已经提交而响应在回程丢失,自动重连并原样重发可能重复扣款。是否重试必须依照方法语义和业务幂等契约判断,不能把传输异常当成回滚证明。HTTP/2 虽然可以在连接上复用多个流,却禁止 Connection、Keep-Alive 等 HTTP/1.x 专属字段;把这些头搬进 HTTP/2 配置没有优化意义。 快速验证 下面是可直接运行的 Python 3 实验:服务器绑定系统分配的回环端口,客户端连续请求四次,结束后关闭测试服务器。它不需要外部服务;内置 HTTP 服务器只用于本地验证,不是部署配置。 ...

五月 28, 2027 · 1 分钟 · XB Field Notes
把查询计划画成从顺序扫描经索引到结果的路线,并高亮慢的那一步。

EXPLAIN 计划揭示查询路径

慢查询首先是一个路线问题。EXPLAIN 逐节点画出规划器选择的路径,让你针对真正耗时的步骤去修,而不是凭猜测。 把计划当作路线来读 EXPLAIN 输出是一棵树:根节点是最终答案,真正的活是子节点干的。每个节点有操作名、行数估算和成本,真正值得盯的是离群点——行数估算远大于实际返回数的大扫描,或者与实际执行差距巨大的估算。读树的顺序很关键:要顺着成本最大的节点找,而不是盯着模式里第一个出现的那层。把 EXPLAIN 和 EXPLAIN ANALYZE 并排看,能直接看到规划器「猜的」和真实「跑出来」的差距,慢查询多半就藏在那里。 统计信息决定路线 有索引只是「有了选项」,不等于「会走到那里」。规划器是根据观测到的统计量——行数、可空性、列值分布——来选路线,所以一次 autovacuum、一次大量导入、一次缓存命中率变化,都可能让同一条 SQL 改道。卡在热点路径上的查询,应该在数据画像变化时重新分析,而不是只在建的时候看一次。统计信息过期时,一个 ANALYZE 有时比一顿索引微调改写更能改变计划。把计划当成需要拿真实 EXPLAIN 数据反复复核的证据,而不是背下来就完事的快照。 确认路线真的变了 把计划塞进改动循环:先留下基线 EXPLAIN ANALYZE,加索引或用改写后的 SQL,确认路线真的移动了再宣布成功。如果目标节点还是那个大的扫描、成本结构没变,那这改动无论听起来多顺,都只是表面功夫。经验再丰富,也不如前后两份计划来得实在;真实的执行耗时、取行数也要一并记录。新计划生效后,把前后对比放进合入请求里,让这次收益可追溯、可复现。 坏掉的样子 统计信息过期会让规划器走错路。一次大批量导入之后,如果很久没有跑过 ANALYZE,规划器可能还以为表里只有几千行,而实际已经有几百万行,于是继续选择只在小数据量下才划算的索引或嵌套循环。EXPLAIN ANALYZE 会露出马脚:估算行数与实际行数差出数量级。先跑 ANALYZE 再对比这两个数字,确认路线是基于新鲜画像选出来的,而不是急着加索引。数据分布变化之后不复核计划,再合理的索引也可能建在错误的假设上。 预编译语句会把某一次参数的计划复用到所有参数上。围绕高区分度的首个参数缓存下来的计划,对它很快,对匹配半张表的值却很慢;而你用 EXPLAIN 拿字面量测出的计划,往往并不是线上真正绑定的那个值。排查时要把区分度最高和最低的两个极端值都测一遍,并确认到底命中了定制计划还是通用计划,再下结论。只测一个最顺的值,就等于只看了一半的路线图。 隐式转换会悄悄让索引退休。拿 varchar 列直接和数字比较、排序规则不一致,或者在列上套 LOWER、日期截断,规划器只能全表扫描再过滤,索引明明存在却用不上。症状很固定:Seq Scan 上被过滤掉的行数远大于最终返回行数。修法是把调用处的类型改对,或者补一个匹配表达式的函数索引,让写法和索引站在同一边。类型不对时加再多索引,也只是给用不上的路线添摆设。 只看 cost 是最后一个陷阱。cost 是无量纲的规划估算,不是执行耗时;估算最便宜的计划,实际可能打出天文数字的共享缓冲命中,或者把排序溢出到临时文件。务必用 EXPLAIN (ANALYZE, BUFFERS) 确认,把实际耗时、buffers、被过滤掉的行数和 cost 并排读,几个数字一致向好,才能说这条路线真的健康。脱离 buffers 谈 cost,和脱离实际行数谈估算一样,都是在看路线的广告而不是路况。 ...

五月 21, 2027 · 2 分钟 · XB Field Notes
标题为‘在工作入队前设限’,旁侧的观察、判断、行动三个步骤由橙色箭头连接。

负载削减保护系统

负载削减是在全部接收工作会威胁服务时,主动拒绝其中一部分。目标是保留容量,让仍有机会完成的工作继续进行,而不是保证每个获准请求都足够快、都足够重要。拒绝本身也会消耗资源,因此在哪个阶段做准入判断,以及拒绝路径有多贵,与阈值大小同样重要。 保护还能产生价值的容量 过载队列既占内存,也增加等待时间。客户端可能在任务被工作线程取走之前就已经放弃,服务器却继续为无人等待的答案花费资源。更长的队列可以吸收某些突发,但在需求长期超过处理能力时,它不会创造额外容量,只会推迟暴露问题。 Google SRE 讨论了队列限制、资源压力信号、选择性拒绝和降低响应质量。削减与降级相关,但并不相同:拒绝一个请求,不等于完成一个较便宜的版本。后者必须有清楚的产品契约,说明剩下的结果仍有什么价值,不能只是悄悄少做一部分工作。 不同保护机制也可能针对不同组件。Envoy 过载管理器保护代理自身资源,而它的熔断机制主要保护上游服务。依赖熔断器不会自动解决本地内存、连接或文件描述符耗尽,所以不能只看到请求被快速拒绝,就认定整个入口已经免于过载。 选择有依据的政策 按优先级拒绝,可以保护关键业务路径,但 HTTP 方法或是否容易重试,本身并不能决定重要性。一次读取可能非常关键,一次写入也可能只是允许丢弃的遥测。应使用可信的应用分类,而不是任由调用方把自己的所有请求都声明成最高优先级。 统一准入上限或按比例拒绝,也可以是合理政策,不能因为没有业务优先级就一概认为它们错误。需要明确公平性与容量之间的取舍,包括低优先级工作是否可能长期得不到执行,以及不同租户是否共用同一个限制。拒绝顺序应该服务于真实契约,而不是凭方法名称猜测。 请求数量、字节、执行时间和 CPU 成本是不同预算。只限制三个任务,如果每个任务都可以保留任意大的正文,仍然不能约束内存。应选择与受保护资源对应的信号,并通过真实请求组合验证行为,不能把单一计数上限解释成对所有资源的统一保证。 限制准入,而不只是线程数量 线程池的工作线程上限约束同时执行的任务数,不一定限制已经接受、正在排队的工作。只在工作线程内部获取信号量,会让它前面的队列继续增长。应在昂贵工作提交之前做有界准入,并在任务生命周期真正结束时归还容量,而不是等到执行阶段才开始计数。 名称类似的代理限制,也需要精确理解。NGINX limit_conn 按文档规定的连接规则计数;在 HTTP/2、HTTP/3 中,每个并发请求都被单独计入。不能默认某个“连接数上限”始终只是在统计 TCP socket,否则复用连接下的真实请求并发可能与预想完全不同。 下面的实验同时约束排队和运行中的任务。被拒提交不会创建 future;完成回调会在成功、失败或执行前取消后归还名额,而向已经关闭的执行器提交失败,也会归还尚未使用的名额。这样准入预算对应完整生命周期,而不是只覆盖正常结束的路径。 坏掉的样子 如果已经解析巨大正文、打开数据库连接或执行了业务写入,才开始拒绝,可能已经来不及保护目标资源。昂贵的日志和错误响应生成,也可能让拒绝路径本身过载。应一起观察拒绝成本、获准吞吐量、排队时长、活动工作量与资源压力,而不是仅统计拒绝了多少次。 HTTP 503 可以表达服务暂时不可用,429 则表示调用方在某种政策下请求过多。Retry-After 只是等待信号,不证明重放写操作安全,也不保证之后一定存在容量。立即重试可能把刚削减的负载原样加回来,因此拒绝与客户端行为必须放在同一条链路中理解。 不要仅因为调用方停止等待,就提前回收仍在运行任务的名额。等待 future 结果超时,不会终止工作线程。提前归还会让新任务与实际上尚未结束的旧任务重叠,破坏原先承诺的容量上限;资源记账应以工作真实结束为依据。 快速验证 保存为 shedding_check.py,用 Python 3.10 以上版本执行 python3 shedding_check.py。实验已在 Python 3.14.4 上运行,只使用标准库。真实线程等待测试协调事件,不访问外部服务,也不制造生产负载。前后两种配置接收的是同样十二个任务的突发,用来比较获准与等待数量。 ...

五月 16, 2027 · 3 分钟 · XB Field Notes
把一次查询的结果落盘存储、稍后可再刷新的快照表。

物化视图把查询结果存起来复用

物化视图保存定义查询的结果,让后续读取直接访问这些已存储的行。它把每次读取时的计算转移到刷新阶段,适合重复执行的报表查询。相应代价是明确的新鲜度边界,以及另一份需要维护的关系数据;它既不保证读取瞬间完成,也不会自动与源表保持同步。选择时应比较完整的读写成本。 已存储结果有自己的生命周期 普通视图保存查询定义,物化视图还保存查询结果。PostgreSQL 可以给物化结果建立索引,并直接针对它规划读取,而不必每次重跑原来的关联与聚合。源表发生普通插入或更新时,这份结果不会自动变化。应用也需要明确查询物化视图;创建它并不会让所有等价的源表查询自动改走这份缓存。 REFRESH MATERIALIZED VIEW 重新执行定义查询,再替换存储内容。标准 PostgreSQL 的刷新不是自动增量维护;即使源表只变了几行,定义查询仍然需要生成新的结果。比较物化与反复直接查询时,要把刷新计算、临时工作、存储和索引维护都计入,而不能只测读缓存的那条快速查询。 存储结果本身仍可能很大,或者缺少适合的索引,因此物化不代表任意读取都便宜。应根据报表过滤条件和返回字段,确定聚合粒度和索引。需要稳定顺序时,还应在读取查询中使用 ORDER BY;刷新不承诺保留定义查询所产生的物理排列,不能把上次看到的顺序当成输出契约。 并发刷新改变的是读者访问方式 普通刷新在替换物化内容时可能阻塞读取。CONCURRENTLY 允许 SELECT 在刷新期间继续访问已有结果,新内容何时可见仍遵循事务与快照规则。它依然需要锁、消耗资源,并可能等待其他操作;同一个物化视图在同一时刻也只能运行一个刷新任务,并不是允许任意多次刷新相互并行。 并发刷新要求视图已经有数据,并至少存在一个适用的唯一索引:索引只能使用列名,而且必须覆盖全部行。部分唯一索引或表达式索引都不满足这个条件。该索引用于刷新时识别结果行,并不意味着源表因此变得不可变,也不意味着源表在相同聚合粒度上具备同样的唯一性。 WITH NO DATA 会把视图变为未填充、不可扫描的状态。它不等于一个可以正常 SELECT 的空结果,也不会保留旧缓存答案。需要先通过普通刷新填充,再请求并发刷新。CONCURRENTLY 与 WITH NO DATA 不能组合成绕过这些前置条件的捷径,部署时应区分“零行有效结果”和“尚未生成结果”。 坏掉的样子 仅有刷新计划,并不能保证陈旧程度有上限。任务可能晚启动、运行很久、失败,或者排队等待另一个刷新;生成结果所用的源快照,也早于刷新工作完成的时刻。应记录成功刷新及报表所需的源数据覆盖范围,不能把配置了某个时间间隔当成数据已经及时更新的证据。失败任务不能像成功发布新结果一样推进页面的新鲜度标记。 部署预期使用并发读取路径时,缺少唯一索引或索引形态不符,会导致 CONCURRENTLY 直接失败。正式依赖前,应实际测试这条路径。PostgreSQL 十八的刷新还要求适用的 MAINTAIN 权限,所以定时任务所用角色必须确实能够刷新该关系。以所有者身份手工执行成功,不能证明任务角色也拥有所需权限。 快速验证 把下面内容保存为 materialized.sql,在可丢弃的 PostgreSQL 18 数据库中执行 psql -X -v ON_ERROR_STOP=1 -f materialized.sql。角色需要创建测试 schema 的权限。脚本在事务内创建 matview_demo,末尾整体回滚;如果这个名字已经存在,应选择未使用的名字,而不是删除已有 schema。 ...

四月 26, 2027 · 2 分钟 · XB Field Notes
观察、决策、行动三个方框,旁边强调接收之前必须实际计数输入字节。

请求体限制划定边界

请求体限制约束的是实际接受的输入,而不只是 Content-Length 里写出的数字。应在消费正文时执行限制,并在验证完成之前避免业务提交。压缩输入还需要分别限制编码后的字节和解码后的字节。如果昂贵的内存分配或不可逆写入已经发生,最后再返回 413,并没有守住原本要保护的边界。 明确预算到底计算哪些字节 采用长度分帧时,Content-Length 描述消息正文的长度;HTTP/1.1 分块传输则可能没有预先确定的长度。已声明长度过大,可以提前拒绝,但缺少长度不等于允许无限读取。消息分帧交给 HTTP 实现处理,应用再针对它提供的正文读取器施加自己的预算,不能自行混淆报文边界。 正文读取器不包含 HTTP 头和分块编码的分帧开销,因此这里的限制不是整个网络请求的总字节配额。Content-Encoding: gzip 下,这些正文是压缩后的数据,解码表示可能大得多。应在解压之前约束编码输入,在解压之后再次约束输出;一个很小的压缩包,也可能超过合法的解码大小上限。 限制应来自具体端点的契约,包括单位和支持的内容编码。整个请求体的总量限制,与某个 multipart 文件或字段的限制也不同。流式处理避免一次缓存全部内容,却不会自动限制最终存储量、解析工作或慢速发送方占据请求的时间,不能把“使用流”直接等同于“资源已经受控”。 验证完成之后再暴露副作用 Go 的 MaxBytesReader 在读取超过限制时返回 MaxBytesError。普通 LimitReader 则会在边界处返回 EOF;如果没有额外判断,应用可能把被截断的前缀当成完整输入。最多读取“解码上限加一”个字节,才能区分恰好达到上限的合法正文与确实超过上限的正文。 对 gzip 而言,成功读到一个前缀并不代表校验和有效。在接受未超限的表示之前,需要读到结尾;只关闭解码器不会完成校验。文件发布、数据库修改等动作应放在完整验证成功之后,或者采用带明确中止路径的暂存方案。不能忽略读取错误,仅因为读取器同时返回了一些数据就执行提交。 坏掉的样子 只看 Content-Length 会遗漏未知长度输入;只在解压前使用一个大小上限,会遗漏解压膨胀;先解析、后拒绝,则可能已经花掉需要保护的资源。损坏 gzip 流可以在报错时同时返回正文前缀,错误处理若只检查正文非空,就可能把受损数据变成被接受的业务输入。 字节限制不能修复冲突的 HTTP 分帧,不能验证调用方或文件类型,也不能限制所有并发请求的内存总和。它还无法阻止发送方把很小的正文无限拖慢。因此,时间和并发量需要独立预算;不能把字节上限叫作空闲超时,也不能从一次拒绝成功推导出已经具备高负载防护能力。 快速验证 保存为 body_probe.go,使用 Go 1.23 或更新版本运行 go run body_probe.go。环回测试服务允许最多 64 个编码输入字节和八个解码字节,支持无内容编码或单层 gzip。这里刻意使用很小的常量,是为了验证边界,不是推荐生产参数。计数器代表应用提交点,不会真的写入数据库或上传文件。 ...

四月 18, 2027 · 3 分钟 · XB Field Notes