文章是本站的原创长文。每篇都保持无脚本、来源可溯,并与本站记录的系统现实一致:一个小型、静态、公网服务如何被构建、加固并保持诚实。
Redis Cluster 把键的落点与机器身份分成两层:键先映射到一万六千三百八十四个固定槽之一,再由集群元数据描述各槽的主节点和副本。增加机器不会改变槽哈希函数,运维需要重新分配槽,客户端则发现当前拓扑并处理路由变化。槽、分片、主节点与副本不是可以相互替代的概念。
原有实验把四个等长槽段分配给 A、B、A、C。访问错误节点返回 MOVED。相同标签的键同槽,但同主节点不同槽的 MSET 失败且不改值。没有副本的三个主节点无法替代故障主节点。 键选择槽,不要求机器持有连续区间 普通映射使用键的 CRC16/XMODEM 值对一万六千三百八十四取模。如果第一个左花括号与其后第一个右花括号之间存在非空标签,则只哈希这部分。例如 profile:{42} 和 quota:{42} 的完整键名不同,却位于同一个槽。标签只是数据放置规则,不是权限控制或租户隔离机制。
一个主节点可以拥有多个不连续的槽段。拓扑稳定时,一个槽有权威主节点;整个键属于该槽,大 hash 或有序集合不会被自动拆到多台机器。CLUSTER SHARDS 能显示分片成员及其槽段。尤其不能把“同一主节点”误当成“同一个槽”:前者是当前机器分配关系,后者才是多键操作需要的条件。
客户端也参与路由 支持集群的客户端维护槽映射,并连接拓扑中公布的节点。访问错误的主节点可能收到带目标地址的 MOVED,而不是得到键不存在。MOVED 要求更新路由;ASK 是迁移期间的临时重定向,需要先向目标发送 ASKING 再发送被重定向的命令,不能因此把该槽永久改归目标节点。
迁移也不是所有参与方瞬间同步更新地图。不同客户端可能暂时持有不同拓扑,多键涉及的数据在迁移期间分散到两端时,还可能得到 TRYAGAIN。应使用维护中的集群客户端,不能假设随便连一个入口就会自动代理所有命令。公布的地址不仅要让 Redis 节点互相可达,也必须能从应用所在网络访问。
...
cherry-pick 把选定提交引入的改动应用到当前开发线,适合旧发布分支只需要一个修复、不希望同时接收整条开发分支的场景。它不会搬走或删除源提交,也不会为目标建立与源分支的合并关系。移植后的结果必须在目标环境中重新审阅;源分支上已经通过的测试,只能说明原来的组合,不能直接证明换了依赖与历史之后仍然正确。
cherry-pick 移植相对父级的变化 选择的是相对父提交的变化 对于普通的单父提交,相关变化是它相对父级的差异。Git 利用可用历史和合并机制,把这份变化应用到目标,而不是简单地用源提交的完整文件快照覆盖目标。目标原本已有的其他修改应继续保留,除非选定变化或人工解决明确要求改变它们。审阅时既要看搬来了什么,也要确认没有误把目标自己的工作一起替换掉。
通常,cherry-pick 会在目标末端后面记录新提交。父级不同,结果目录树也可能不同,因此对象身份会变化。原作者身份和作者日期通常保留,而提交者表示实际记录这次移植的人;消息不一定变化。不能把这项操作解释成必然换作者、必然改写消息,更不能根据对象标识不同就断言原作者署名已经丢失。
默认行为会创建提交,但不提交选项会把变化留在索引与工作树中,供审阅或组合;使用允许快进的选项时,满足条件的操作还可以直接前进到已有源提交。这些明确模式说明,“所有 cherry-pick 都一定生成新对象”也不是普遍成立的规则。解释一次具体结果时,应把实际参数与所处历史一起纳入判断。
移植之前检查依赖 修复可能依赖目标分支尚不存在的接口、数据结构、配置或前置重构。补丁在文本上干净落地,并不能证明行为正确。应阅读源变化及其前提,在真正的维护分支运行受影响行为测试,并只引入该修复实际必需的依赖。如果需要适配,说明为何不能原样采用,以及新分支上的实现如何满足同一修复目标。
在共享分支之间干净应用时,-x 会把源提交标识写进新消息,方便追踪,但它不是等价性或批准状态的密码学证明。手动解决冲突后也要检查最终消息,确认来源关系没有丢失。原提交的签名不会自动成为新对象的有效签名;目标要求签名时,应遵循目标发布流程,而不是把保留作者信息等同于继承了所有验证证据。
坏掉的样子 在默认创建提交的模式中,发生冲突时,HEAD 停在最后一个已成功创建的提交,Git 用 CHERRY_PICK_HEAD 记录当前难以应用的源提交。使用 --no-commit 时不会设置该引用,因此必须检查状态和未合并条目,不能仅凭这个标记是否存在来判断冲突。能够干净应用的路径可能已经更新,冲突路径则在索引中保留未合并条目。文本冲突标记便于查看,但二进制冲突及其他类型不能只靠搜索标记字符串识别,还应检查工作区状态和索引,确认当前到底有哪些未完成事项。
解决时应确定目标需要的行为,暂存具体已解决路径,运行相关测试,再继续序列。文件已经暂存只说明冲突被标记为解决,不证明代码逻辑正确。中止会返回序列开始之前的状态;退出序列管理则只是遗忘操作状态,并不执行相同的回退。开始前应保证工作树干净且状态明确,不能把中止功能当作保存无关未提交工作的替代品。
目标已经包含某项变化时,应用结果可能为空。跳过之前,要确认目标确实具有预期行为,而不是只看到类似代码就作判断。选取合并提交还会多一个语义选择:主线参数指定以编号中的哪个父级作为差异基准。这不是通用冲突绕过开关,必须依据想移植的实际变化选择,而且结果不会重现原合并提交的多父级拓扑。
快速验证 在 POSIX 系统上使用 Python 3 与 Git 2.28 或更新版本运行下面实验。它建立私有临时仓库,排除个人 Git 配置和继承的 Git 路由变量,使用示例身份,关闭钩子与签名,没有远端。一条目标线能够接收修复;另一条故意把同一行改成不同内容,用来观察冲突停止及中止恢复。全部修改都限于这份临时数据,不触碰当前项目的提交或文件。
...
建议锁让应用使用一个标识来协调工作,不要求这个标识对应数据库里某一行。比如,多名工作者可以约定,同一时间只能有一个连接刷新某份报表。PostgreSQL 负责管理锁的持有与竞争,应用则负责定义标识的业务含义,以及哪些操作必须先拿锁才能执行。这个分工决定了它能提供的保障,也决定了绕过协议时会发生什么。
坏掉的样子 建议锁的键只能是一个大整数,或者两个普通整数,没有接受文本名字的函数重载。这两种键空间彼此独立。如果应用通过哈希把名称映射为数字,必须明确一致的编码规则,也要考虑碰撞;同一资源被不同算法映射到不同数字,同样会破坏协调。直接维护有文档的整数命名空间,通常比隐藏在各个调用方里的临时哈希规则更容易核对。
建议锁是参与者共同遵守的协议。忽略它的连接,仍然可以更新所谓受保护的表行;它不能代替唯一约束、事务或权限检查。会话锁在回滚后仍然保留,直到对应次数的解锁完成,或服务器结束该会话。事务锁则随事务结束释放。如果处于自动提交模式,一条语句取得的事务锁,在后续语句开始之前就已经释放,无法保护接下来才执行的工作。
会话持有次数在回滚后保留;事务持有随事务结束。数值为人工预期,不是本轮竞争观察。 快速验证 本轮没有重跑原样保留的双会话示例。保留的审阅记录给出源文核对后的预期,没有原始多会话输出;单用户后端不能证明会话间竞争。
准备同一个 PostgreSQL 18 可丢弃数据库,开启甲、乙两个全新的 psql -X -v ON_ERROR_STOP=1 会话。使用同一角色并具备创建模式权限,确认 advisory_demo 尚不存在;显式事务外保持自动提交,且没有会中断调度的超时。为本实验预留下面的整数键,不要与已有业务约定混用。按照块的顺序切换终端。甲先创建计数器:
CREATE SCHEMA advisory_demo; CREATE TABLE advisory_demo.counter (id integer PRIMARY KEY,n integer); INSERT INTO advisory_demo.counter VALUES (1,0); 甲传入显式文本类型的键,预期返回 42883,即不存在匹配的函数。故意报错这一段结束后恢复遇错停止设置:
\set ON_ERROR_STOP off SELECT pg_advisory_lock('job-name'::text); \echo text_key_state :SQLSTATE \set ON_ERROR_STOP on 甲在事务里把同一把会话锁取两次,然后回滚。注意函数名使用的是会话级形式:
...
内容协商是在服务器能够实际生成的表示之间进行选择。请求偏好只是选择的输入,并不能证明响应已经满足这些偏好,客户端仍需检查收到的表示。Vary 记录影响复用的请求维度,不负责命名最后选中的格式、语言或压缩方式。把选择输入与响应结果混为一谈,会让排查从一开始就看错字段。
分开看待不同维度 请求偏好 所选表示的响应元数据 Accept Content-Type Accept-Language Content-Language Accept-Encoding 实际施加内容编码时的 Content-Encoding 这些维度相互独立,请求 JSON 不等于请求英文或 gzip。Content-Language 描述内容所面向受众的语言,不保证每个字符都属于某一种语言。如果服务器只有英文 HTML 和中文 JSON,不能把分别算出的“最优格式”和“最优语言”拼成根本不存在的中文 HTML。应先列清实际可用的完整组合,再定义选择规则;偏好不能凭空增加服务端的生成能力。
质量值范围是零到一,省略时为一,最多允许三位小数。零表示不可接受,不是“稍微不喜欢”。对媒体类型,一个候选的质量值由匹配它的最具体范围决定。显式的 text/html;q=0 不会被更宽泛的 */*;q=1 解禁。如果只是找整串字段里最大的数字,或者拿逗号前第一项,就会丢掉“哪个范围约束哪个候选”的关系,得到看似按权重处理、实际上违反排除条件的结果。
坏掉的样子 缺失某个偏好字段通常表示在该维度上没有偏好,并不等同于显式拒绝。RFC 9110 允许服务器在没有可接受表示时返回 406,也允许忽略该字段,因此默认响应可能仍然无法被客户端使用。承诺严格协商的 API 必须自己明确这项策略。客户端可以表达排除条件,却不能仅靠发出这个字段,强迫所有现网服务器都执行严格拒绝逻辑。
各类字段还存在不同的匹配语义。Accept 使用媒体范围,语言协商需要约定语言标签匹配方式,Accept-Encoding 则有 identity、字段缺失和空值的专门规则。通用列表解析器不会自动补齐所有业务策略。特别是不能把缺失字段悄悄变成一个空集合,再推导出“客户端拒绝一切”。应用选择默认值与解析器返回没有匹配项,是两个需要明确区分的事实。
相同权重也没有一条跨所有服务器通用的顺序规则。应用应让并列时的选择可预测,并实际测试。完整的语言偏好还可能帮助识别用户,因此记录全部偏好档案、把所有可能字段都放进 Vary,并非天然合理。Vary 只处理匹配问题,缓存资格、新鲜度和授权边界仍需各自的规则,不能因为增加了这个字段就认定内容可以共享。
具体程度、排除与候选顺序决定解析结果;端点仍需单独决定如何把无匹配转换成 HTTP 响应。 快速验证 准备 Python 3.11 或更新版本及 POSIX shell,在临时环境安装本次验证的解析器,再用 "$accept_lab/venv/bin/python" 运行 Python 代码。安装需要外网,实验本身不发请求、不启动服务器。它验证的是 Werkzeug 3.1.8 的候选选择行为,不是假装复现任意浏览器或 CDN 的完整实现。
...
缓存雪崩或击穿所造成的一个关键风险,是并发未命中对同一依赖发起大量重复工作。同时到期只是触发条件之一:容量淘汰、刚部署的空缓存、缓存服务不可用,都可能造成类似突发。即使只有一把热门键,也足以压垮后端。判断风险时应数清楚:一份结果尚未计算完的这段时间内,到底有多少次重建真正抵达数据源。
文章的重叠调用样例在明确的 Group 边界内统计工作量。一个 Group 合并一轮在途工作,两个独立 Group 各自加载,下一轮仍重新执行。完成结果的存储仍由应用负责。 在真实边界内合并工作 Go 的 singleflight Group 对同一键,在这个 Group 内最多保持一次在途函数执行;重复调用者共享结果,也共享错误。不同 Group 之间不会协调,即使它们位于同一个进程;不同服务实例通常也各有自己的 Group。因此它既不是分布式锁,也不是保存已完成结果的缓存。把 Group 放在每个请求内部重新创建,无法实现跨请求合并。
实际使用时,应让请求共享同一个 Group,并使键准确表示等价工作。不同租户、权限、币种或响应表示如果错误地使用同一个键,可能拿到不属于自己的结果。对于缓存回填,在共享函数内部应再次检查缓存,确实仍缺失才访问数据源,并在函数返回前完成成功结果的缓存写入。这些存储操作都不是 singleflight 自动提供的能力。
错开时间不能改变新鲜度承诺 随机抖动可以打散不同条目或独立副本的到期时间,却不能阻止访问同一过期热键的人一起未命中。如果最大 TTL 是业务规定的新鲜度上限,在有界范围内随机缩短寿命,不会偷偷越过这个上限;任意增加正向抖动则改变了原有承诺。不能脱离请求量、计算成本和允许陈旧程度,宣称几个百分点或几秒钟就普遍足够。
...
veth 对由两个互联的虚拟以太网设备组成。从一端发送的帧到达另一端,再由接收网络栈或网桥处理。两端可以放在不同 Linux 网络命名空间,因此常用于容器网络。创建这一对设备只提供链路,地址、路由、桥接关系、转发和访问策略都需要另外配置。不能把接口已经出现,等同于完整网络已经准备好。
坏掉的样子 接口显示管理状态 UP,仍可能因为 veth 对端关闭而没有载波。只读取 UP 标志,会让尚未完成的配置看起来已经就绪。应检查两个端点,并区分管理状态与 LOWER_UP。路由也可能已经指向这个接口,但链路仍无法承载有效流量,所以路由列表本身不是连通性检查,更不能代替一次真实应用交换。
veth 对不会把 IP 载荷限制为对端接口自身地址。路由选择报文经过哪个链路,随后以太网转发或接收网络栈决定下一步处理。发往容器直接邻居之外的流量,也可以通过这一对设备到达路由器或网桥。声称“不发给对端的包都会留在本地”,混淆了链路交付与 IP 目的地选择,容易误导容器出口排障。
两端生命周期也相互关联。删除一个 veth 端点,会移除另一个端点,即使对端由另一个命名空间管理。因此宿主侧清理动作可能删除容器接口。影响活跃工作负载时,应有意识地安排停止流量、调整路由和删除设备的顺序。命名空间分离了管理视图,却没有让成对设备变成两项互不影响的资源。
veth:跨命名空间设备对:概念机制与预期检查,不是新增运行证据。 快速验证 将脚本保存为 check-veth.py,仅在已获授权的一次性 Linux 实验环境中,使用核实为本地的 Docker daemon,并保留 Python 断言。配方注明 Python 3.11.16、iproute2 6.1.0,但镜像架构、包与宿主内核仍需核对。NET_ADMIN、SYS_ADMIN 无法覆盖强制策略的挂载拒绝,Docker 默认 AppArmor 配置即存在此限制。应使用兼容且获批的实验环境,不应为运行示例削弱宿主保护。嵌套命名空间与 proc 挂载需要实际内核权限,运行时隔离不代表拉取镜像和构建离线;清理命令仅尝试执行,未全部核对退出码。
...
Kubernetes 的 securityContext 配置进程身份及部分操作系统约束,可以缩小被攻陷应用允许执行的操作,但本身不会建立完整安全边界。应先确定应用实际需要什么操作,再配置对应字段,并在最终容器中同时测试允许与拒绝的行为。只有一份看起来严格的清单,还不能说明运行中的进程已经按预期受到限制,也不能代替应用本身的权限设计。
原文 Kubernetes 1.34.11 历史样例,本次未重跑。结果反映组合设置;RuntimeDefault 取决于运行时。没有测试容器逃逸抵抗、全部 seccomp 规则、敌对多租户隔离或应用授权。执行仍为受阻。 把设置放在正确层级 Pod 级配置可以提供用户和组身份、共享卷的组权限行为,以及 seccomp 配置。容器级字段可以覆盖适用的默认值,并设置 capabilities、权限提升和只读根文件系统。需要核对字段真实 API 范围,把熟悉的名称写在错误位置,并不会获得等价约束。相同的 securityContext 名称之下,不代表所有字段都能互换放置。
...
一次 schema 变更可能适用于二进制消息,却不适用于同一服务的 JSON 网关。Protobuf 二进制格式按编号识别字段,ProtoJSON 通常暴露字段名称。因此应把生产方、消费方和中间转换组成兼容矩阵,而不是只给变更贴一个“向后兼容”的标签。编译成功,或两个端点互相读懂,并不能证明所有转发环节都保留了它们交换的信息。
新增字段是否安全,取决于表示形式 使用全新编号增加字段,通常属于二进制线上安全变更。旧解析器可以保留未知字段,同时继续读取熟悉的内容;新版解析器读取旧数据时,则得到相应默认值或缺失状态。缺失是否符合业务要求仍是另一项决定,不能在旧写方还无法提供新值时,就让新读方强制依赖它。格式接受能力与功能实施能力必须分开验收。
ProtoJSON 的规则不同。解析器默认拒绝未知字段,所以旧 JSON 消费方可能拒绝带有新增属性的消息。配置忽略未知字段,可以让解析继续,却不会为后续消费方保留被丢弃的信息。这是明确允许信息损失的策略,不是二进制未知字段转发的等价实现。应先确认新字段是否只是补充说明,还是会影响业务动作的必要输入。
保持编号而修改字段名,能很好地展示这个差别。二进制读方仍然可以按编号取得数值,但生成代码的访问器和默认 JSON 属性会变化,旧属性对新 JSON 解析器可能变成未知字段。json_name 选项同样参与这个契约。发布前应列出实际输出与接受的名称,而不是只比较编号表,便宣布改名完全没有代价。
跟踪中间转换,不只比较两个端点 旧消息对象能够在内部保留未知二进制数据,即便公开字段接口无法描述它们。把这个对象序列化成 JSON 时,只输出已知字段,未知信息就会丢失。遍历已知字段并填入一个新消息,也会产生类似损失。需要保留未知数据时,应优先使用面向整个消息的复制接口,并用测试明确验证保留结果,而不是依赖表面上相同的字段值。
这也不同于序列化字节稳定。等价消息在不同版本或实现间,不一定产生完全相同的二进制输出。比较时应检查所需值与存在状态,不应要求相同字段顺序,除非协议另外定义了规范表示。对重新序列化结果签名或计算身份摘要,需要单独、明确的规则;某一次小样本字节相等,并不能替所有版本建立这种规则。
字段编号退役后应永久保留,并把删除的名称加入 reserved,阻止 schema 中意外复用。但名称保留声明不会让运行时 JSON 解析器继续接受旧属性。deprecated 标记也不会自动删除字段,它更不会清除旧写方、已保存记录或下游依赖。真正删除之前,需要知道哪些对象还在使用它,以及怎样完成迁移。
坏掉的样子 网关可能接受一条新的二进制请求,经过旧 schema 的 JSON 对象再输出,从而悄悄抹掉新增指令。末端消费方仍可解析成功,却因缺失而进入默认分支。另一条路径则是严格旧 JSON 解析器直接拒绝整条消息。需要同时检查拒绝与静默省略,两者的运行症状不同,却可能来自同一个表示转换边界。
也要区分 schema 兼容与生成代码、运行时版本兼容。即便线上格式没有变化,新生成代码也可能要求受支持的特定运行时组合。应遵循供应方支持规则,并测试仍有意义的最老读方、延迟消息和回滚路径。几行把类型名称放进字典的自制检查器,不能证明真实解析器、代码生成或运行时的这些性质,更不能替代跨版本测试。
...