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

复合 B-tree 索引先按第一个键排序,再在第一个键相同的组内按下一个键排序。这种元组顺序可以让“某个租户的一段时间”很容易定位。应理解的是条件能缩小多少索引扫描范围,而不是禁止查询后面的列,也不是要求 WHERE 中的条件必须按建索引的顺序书写。这几个概念混在一起,会导致错误的索引设计。
租户优先键顺序的局部片段,以及一种可能的后列搜索策略。重复搜索是否划算取决于成本;本图不是计划跟踪,最终输出顺序仍取决于实际路径。 排序的是键,不是 SQL 条件 对于 (tenant, happened) 索引,tenant 等值条件加 happened 范围条件描述了树中的一小段。PostgreSQL 可以定位这一段,而不用检查所有租户。即使把时间条件写在 WHERE 的前面、租户条件写在后面,逻辑上的合取关系也没有改变。数据库定义中的键顺序与查询文本的书写顺序,是两件不同的事情,不能拿文本位置判断索引是否可用。
通常,前导列的等值条件和第一个非等值键上的范围条件,决定必须扫描的区间。更后面的键条件仍可在索引中检查,即使不能继续缩小一个简单连续区间,也可能减少访问堆表的次数。调换索引键会改变这些机会,但不意味着某种排列在所有查询中都完全无用。需要结合查询实际要求的范围和返回列判断。
后面的键仍可能有用 PostgreSQL 18 在合适时可以使用 B-tree skip scan:如果遍历缺少等值条件的前导键取值,并在每组内重新搜索,比读取大部分索引更便宜,就可能采用这种路径。只有少数几个租户时,仅过滤 happened 也可能通过各租户分组完成查找;前导键不同值很多时,这种重复搜索可能太贵,规划器便可能选择顺序扫描或其他索引。
即使没有合算的 skip scan,某些只涉及后面键的条件仍可能扫描复合索引。问题是成本、覆盖范围与选择性,不是 SQL 是否合法,也不是索引必然被忽略。不能把“最左前缀”这种便于记忆的说法扩展成绝对禁令,尤其在目标数据库明确支持其他访问路径时。版本和真实计划必须优先于口号。
把排序和工作负载一起考虑 固定 tenant 后,后面的 happened 顺序可以支持该租户的时间排序。但跨多个租户时,索引先按租户分组,不会直接提供全局时间顺序。Bitmap 扫描也不保留索引顺序,仍可能需要单独排序。因此应看完整计划,而不是看到索引包含排序列就断言不会发生 Sort。LIMIT 对尽快返回前几行的需求也会改变最合适的路径。
...

IPv4 默认路由是前缀 0.0.0.0/0,匹配所有 IPv4 目的地,但具体程度最低。在已经选择的路由表中,更长的匹配前缀通常优先。默认路由不是一个“其他具体路径失败后再重试”的过程。路由策略决定相关表,最终路由描述怎样转发,而不保证完整往返一定成功。匹配范围与路径健康是两类不同属性。
坏掉的样子 没有默认路由的机器,仍能访问直连或明确路由覆盖的目的地;反过来,添加默认路由也不证明能够访问互联网。下一跳需要可达,转发与过滤要允许流量,回复还需要返回路径。显示一个网关,只满足契约的一部分。真正配置是否共同工作,应由实际应用通信证明,而不是从路由文件中有一行 default 就推导全部能力。
更具体的拒绝路由会胜过可工作的默认路由。Linux 的 unreachable 等路由类型,会有意停止匹配目的地的转发。选定网关的邻居查询失败,也不意味着内核自动寻找更健康、却更宽泛的路径。静态路由优先级和 metric 不是服务健康监控或自动故障转移机制,不能把配置顺序当成逐个尝试可用出口的流程。
返回路径错误尤其容易误导。客户端选中预期网关并把流量送往监听中的服务器,服务器却可能无法把回复路由回客户端。反复修改客户端默认路由,可能破坏其他正常目的地,却没有修复缺失的反向路径。将所有连接超时归因于发送方网关之前,应查看两个方向和真实源地址。请求能到与响应能回,需要分别成立。
有去路,还要有回路。 仅为原文预期模型。实验前提缺失,可执行验收受阻;不声称具有保留的运行证据。 快速验证 2026-10-18 源码审阅说明:下述预期结果与历史报告没有在本次审阅中重新运行,当前执行验证仍受阻。
这些命令要求明确记录的 Linux 实验安全策略允许嵌套命名空间和 proc 挂载。NET_ADMIN 与 SYS_ADMIN 不会覆盖 Docker 内置 AppArmor 策略的挂载禁令。初始化权限错误不属于选路测试结果。
将脚本保存为 check-default-route.py,准备 Docker 后运行 python3 check-default-route.py。Python 3.11.16 和 iproute2 6.1.0 在一次性 Linux 容器内建立客户端、路由器、目标拓扑。NET_ADMIN 与 SYS_ADMIN 支持嵌套命名空间和临时 proc 挂载。接口全是新建 veth,不向宿主发布端口或路由,也不修改现有网络设备:
...

IPv4 前缀长度表示地址中有多少前导位标识网络。对于 192.0.2.5/24,网段是 192.0.2.0/24,普通广播地址是 192.0.2.255。配置接口通常会建立相应直连路由,但前缀不是完整路由算法。内核还要依据路由策略与路由表,为具体目的地选择路径,不能只看地址是否位于某个网段就推断全部行为。
坏掉的样子 前缀太短,会让主机把原本远端的目的地当成直接相连。保留地址 192.0.2.5,却把 /24 改成 /16,就把 192.0.3.9 纳入直连范围。在真实以太网中,这种错误可能让主机尝试解析一个根本不在当前链路上的邻居。于是地址看起来正确,下一跳决策却已经改变;反复核对地址字符串本身无法发现问题。
“不在自己的子网就走默认网关”也不完整。更具体的路由可以选择其他网关或接口;在已经选择的路由表内,更长的匹配前缀优先。明确的主机路由甚至可以覆盖本来位于接口子网内的目的地。应检查实际查询结果,而不是仅凭接口地址重新猜测内核会怎么做。默认路由只处理没有更具体匹配的相应流量。
地址数量计算也需要上下文。普通 /24 的网络地址与定向广播地址通常不用于主机分配,但“减二”不是统一公式。RFC 3021 允许点到点 /31 链路的两个地址都用作端点,/32 则标识单个地址。这些规则解释地址语义,没有证明配置中的两端能够通过当前物理拓扑交换数据。可用地址集合和实际通信仍须区分。
更长前缀可以覆盖子网。 依据原文与明确限制静态绘制;未为本图重跑原文夹具。 快速验证 2026-10-18 源码审阅说明:下述预期结果与历史报告没有在本次审阅中重新运行,当前执行验证仍受阻。
将脚本保存为 check-subnet.py,准备 Docker 后执行 python3 check-subnet.py。程序给固定 Python 镜像安装 iproute2 6.1.0,再只向一次性容器授予 NET_ADMIN;容器没有外部网络接入。脚本创建虚拟 dummy 接口并修改该容器的路由,不改变宿主接口、路由或生产网关。构建下载与运行时路由实验也处于不同阶段:
...

BGP 在独立运营的网络之间交换可达性,但互联网并不存在一张统一、权威的转发表。每台路由器都学习经过策略过滤的视图,选择路由,再尝试把可用下一跳安装进转发系统。两台路由器为同一个前缀选择不同路径,完全可能是正常情况。理解 BGP,需要把通告、选路策略和数据包转发分开,不能把它们合并成一个放之四海皆准的答案。
AS 路径同时承载历史与策略 普通 eBGP 场景中,路由器向外宣告时会在 AS 路径前加上自己的号码。接收方通常排除已经包含自己 AS 的路径,用于避免自治系统之间的环路,但存在显式配置的例外和协议扩展。自治系统内部的 iBGP 通常不再增加 AS 号码;它的转发通告限制及路由反射器规则处理的是另一类内部环路问题。
AS_PATH 还是过滤和选路的输入。重复前插可能让一条路由在看重路径长度的网络里吸引力下降,却不能推翻对方优先级更高的本地策略。也没有一条通用规则说,前插之后路由就会被过滤。必须结合真正的运营商策略、客户授权和接收条件,理解该字段在当前网络里的作用。
LOCAL_PREF 会作为属性分发给内部 BGP 对等体,通常不传给外部对等体。部分实现提供的 weight 则只属于本地路由器,也不是标准 BGP 路径属性。把两者都描述成不会进入更新报文,会掩盖关键区别:本地优先级可以影响自治系统内其他设备的决策,而 weight 的效果一般局限在一台路由器。
先比较同一个前缀 最佳路径过程比较的是同一前缀的多个候选路径。具体顺序取决于实现和配置,包括路径资格、本地策略、AS 路径长度、MED 比较范围及下一跳可达性。某个产品的 weight 优先列表并不是协议完整定义。MED 通常在来自相同邻居 AS 的路径之间比较,但实现可以提供改变这一行为的选项,不能把默认规则写成不可改变的限制。
之后,转发过程在已安装目的路由中做最长前缀匹配。一个 /25 与覆盖它的 /24,并不是通过同一套最佳路径平局规则互相竞争。只要两者均被接受并安装,前者就匹配那一半地址,即使后者的 AS 路径更短。策略路由或其他额外转发机制还需要分别检查,不能从这条普通查找规则推断全部设备行为。
...

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

TCP 三次握手建立字节流的两个方向。每端提供自己的初始序列号,并确认另一端的序列号。这是两个独立序列空间,不是共享墙上时钟;应用建立连接不需要两台机器时钟同步。之后的序列号用于标识字节,支持连接内部的排序、重复处理、确认和重传。把序列号与时间混为一谈,会误解握手真正建立的协议状态。
图示普通握手及受控丢包场景;32 位序号按模回绕。connect 成功不等于应用已能提供有效服务。 坏掉的样子 客户端发送带初始序列号的 SYN,服务器回复 SYN-ACK,给出自己的初始序列号并确认客户端 SYN,客户端再确认服务器响应。即使没有应用数据,SYN 也占用序列空间中的一个位置,所以对应确认号增加一。将这些数值当成时间戳,会错误解释报文,还可能把排障引向无关的时钟同步问题,忽略真正缺失的确认或路由。
两端不会在完全相同的时刻改变状态。客户端收到 SYN-ACK 后,connect 可能成功返回,但最后一个确认在到达服务器前丢失。服务器仍可能停留在 SYN-RECEIVED,Linux 工具显示为 SYN-RECV。后续携带适当确认的数据也能完成交换,因此单独第三个报文缺失,并不必然导致永久失败,不能把抓包中的固定三包模式当成唯一合法过程。
TCP 建立成功也不代表应用健康或对端身份可信。服务可能接受连接却无法生成有效响应,代理可能完成客户端这一侧连接,却无法建立另一个独立上游连接。初始序列号选择有助于区分连接实例、抵御预测攻击,但不是身份认证协议,也不会加密字节流。传输状态、应用结果和安全身份需要各自对应的验证,不能由一项成功代替全部。
快速验证 将脚本保存为 check-tcp-handshake.py,准备 Docker 后运行 python3 check-tcp-handshake.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 支持嵌套网络命名空间。客户端和目标由新建 veth 相连,不移动宿主接口或发布端口:
...

标签是 Kubernetes 用于选择对象的键值元数据。Service 选择器、运维查询和工作负载控制器都可能依赖标签,但每个消费者有自己的 API 和处理方式。因此,标签变化可能改变对象所属集合,同时底层进程仍然健康。审阅时应把标签视为真实消费者的输入,而不是仅用于展示的贴纸,否则看似简单的文字修改也可能改变路由。
这是历史实验观察,不是新的集群运行结果。缺少键时的不等式会影响查询成员;Service 选择不会强制实施网络授权。实验使用普通就绪行为,未执行 NetworkPolicy 隔离。 确认正在使用哪一种选择语言 标签键采用限定名称格式,值也有字符和长度约束。标签值可以为空;不为空时,其允许字符及首尾位置受到限制,最大长度为六十三个字符。任意长段落或不受约束的 JSON 并不适合放在标签值里。描述性信息可以采用 annotation,但仍然要遵守对应格式和消费契约,不能把换字段当成取消所有限制。
等值条件检查某个键是否具有指定值,集合条件还支持成员关系与存在性检查。同一个选择器里的多个条件需要同时满足。有一个容易忽略的细节:track!=canary 也会匹配根本没有 track 键的对象。如果不允许键缺失,应另外要求这个键存在,不能认为“不等于某值”就已经隐含“这个键肯定存在”。
...

Bash 命令中的通配符通常会在目标程序启动前变成一组路径。它让批量处理很方便,也让同一行命令受到当前目录和 shell 选项的影响。程序拿到的是已经形成的参数,不会收到标记来告诉它哪些参数来自星号。因此,写命令时应先确定:希望传入实际匹配的文件,还是希望把模式本身交给另一个程序解释。两者需要不同的引号写法,不能依靠目标程序猜测意图。
文件名展开保留各个匹配项内的空格。未匹配策略和接收程序的语义仍需分别处理。 先匹配,再保留参数边界 Bash 先进行分词,再进行文件名展开。没有引用的星号、问号和方括号模式可能因此生成多个参数。但匹配到的文件名即使包含空格,也仍然是一个参数,因为文件名展开之后不会再执行一次分词。常见问题发生在下一步:循环拿到了完整路径,却用没有引号的变量把它传给另一个命令。循环体中的 "$file" 仍然必要,不能因为前一步通配成功就省掉。
如果接收方需要模式,就应引用模式。例如 find . -name '*.txt' 将匹配规则交给 find;去掉引号后,Bash 会先在当前目录展开它,find 得到的可能是多个文件名,已经不是原来要表达的条件。变量里包含星号时也一样:引用变量会传递字面星号,不引用则可能触发文件名展开。引号只控制当前 shell 如何形成参数,不会禁止接收方按自己的语法解释收到的文本。
通配符匹配目录项,并不保证结果都是普通文件。名为 archive.txt 的目录也能匹配 *.txt,只想处理普通文件时还需要判断类型。普通星号不会越过斜杠,默认也不匹配名字开头的点;需要隐藏文件时,应明确使用相应模式或开启 dotglob。遍历整棵目录树是另一个需求,不能把当前目录的一次展开误认为递归搜索。
环境变量也能改变结果集合。Bash 中非空的 GLOBIGNORE 会过滤匹配结果,并且产生开启 dotglob 的副作用。因此,从交互会话继承状态的脚本可能意外纳入隐藏文件。下面的实验会清除 GLOBIGNORE 及较新版本的排序覆盖变量 GLOBSORT,并明确指定相关选项,让断言对应一套可重复的环境,而不是依赖读者终端的偶然配置。
...