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

Redis Lua 脚本可以先读取数据、判断条件,再更新数据,而不让另一个客户端的命令插进这些步骤之间。但这不意味着每个语句都会成功,也不意味着后面发生错误时会撤销前面的写入,更不意味着执行结果自动具备持久性。原子执行与失败回滚,是必须拆开理解的两个问题。
原有 Lua 实验中,旧 owner-a 请求删除时保留当前 owner-b;owner-b 匹配时才删除,键不存在则返回零。未捕获的 redis.call 错误保留此前写入但停止后续语句;明确的 NOSCRIPT 表示代码缺失。 仅为原有实验示意,本轮未重跑。 把判断放在数据旁边 例如,仅当令牌仍属于当前持有者时才删除它。客户端先 GET、稍后再 DEL,中间可能已经被新持有者替换了值;随后删除就误伤了别人。脚本可以在同一次执行中比较当前值,匹配才删除。因此旧持有者即使带着之前读到的令牌来操作,也不会直接删除新持有者的值。
脚本应通过 redis.call 访问 Redis 数据,这正是脚本接口的用途。沙箱不提供普通文件系统或网络访问,不能把邮件、支付或文件读写搬进脚本。所有会访问的键都应通过 KEYS 传入,普通值用 ARGV,不要根据数据库里读出的内容临时拼接键名。Redis Cluster 中的常规多键使用还要求相关键位于同一哈希槽。
...

Prometheus 序列由指标名与完整标签集合识别。在计数器上增加客户或请求身份,即使所有面板都会立刻把这些身份求和,也仍可能创建大量序列。查询聚合改变返回结果,不会删除已经入库的原始序列。存储和写入成本在用户打开面板之前就已经产生,因此不能只看面板返回多少行来判断仪表化是否足够节制。
查询聚合减少返回行数;在生产端累计所需的路由总量才会减少导出的身份。数字示意原文的固定总量样例;本轮未重跑 Prometheus 集成。 统计实际组合,而不是孤立取值 一个新的标签值,不是在整个数据库中自动对应一条新序列。它在指标名及其他标签形成不同组合的位置上,分别构成新身份。在明确指标与目标范围内,可能取值数的乘积给出笛卡尔组合上界;只有每种组合实际出现时,它才等于观测数量。维度之间存在约束时,上界可能远大于现实。真实数量还受到目标标签与指标族形态影响,不能把理论乘积直接宣称为已经发生的序列数量。
来自少量路由模板的标签可以有边界,包含标识符和查询参数的完整 URL 往往没有。客户身份、请求编号、任意错误文本和时间戳也需要明确审视。带多个桶序列的直方图,还会进一步放大每组标签组合的影响。预算应依据真实输出和预期工作负载,而不是只看某一个维度有多少取值,也不能因为每条样本数值很小便忽略身份成本。
查询少几行,不代表少存几行 按路由求和的表达式,可以读取十二条客户级序列后只返回三个结果,但十二条输入仍然存在。如果指标契约需要的就是路由总量,仪表化可以直接维护路由级计数器,只导出三条序列。这是在源头减少输出身份,不是要求每个消费者都重复隐藏额外维度。两种方式可能显示相同总数,却具有不同的数据保留和成本含义。
入库时删除标签不等于完成上述累计。不同样本可能获得同一个身份,而重新标记不会替它们决定如何相加。应使用明确的仪表化变更,或定义清楚的记录查询;后者会新增派生序列,原始数据仍按照存储策略保留。确实需要详细关联时,可以放入日志或追踪,但它们同样需要各自的权限、保留期限与成本约束,不是完全没有代价的替代存储。
使用正确范围的清单诊断增长 TSDB 状态接口提供头部统计,其中包括按指标名汇总的序列数量。这些值有助于定位大型指标族,但不是完整成本模型。采样间隔、标签长度、身份更替、数据块处理、保留策略和存储行为都会影响资源。不能把一个小实验中减少的序列比例,直接换算成生产内存或磁盘会减少的相同比例,更不能把该比例当成容量压测结果。
当一些头部序列已经没有近期可选择样本时,即时查询计数与头部序列统计可能不同;历史存储块又是另一种范围。比较报告时应记录测量时刻和统计总体。某次发布改变标签值后,旧身份仍可能留在存储中,因此查询当前返回更少结果,不代表之前的全部成本都已经消失。需要分别观察新增速度、当前可见序列和保留历史。
每次抓取的样本上限是保护措施,不是选择性聚合。指标重新标记之后仍超过该上限时,Prometheus 会把整个抓取视为失败,并不是悄悄保留一个便宜的子集。因此,保护资源的同时也可能失去有用观测。应根据真实输出制定预算,并对抓取失败设置清楚的信号,不能把限制触发后的缺失误解成业务没有流量。
坏掉的样子 新增无边界标签可能持续增加写入与内存压力,直到抓取或查询受到影响。应先定位指标族与仪表化改动,再从源头或经过审阅的样本过滤策略阻止不必要的新身份。立即删除历史数据不是所有场景都必须采用的恢复步骤,改变保留时间也不会瞬间清除全部资源成本。处理方式应依据实际瓶颈,而不是套用统一删除命令。
更隐蔽的失败是查询聚合很成功,面板看起来很小,但原始指标仍持续增长。应同时审阅导出身份和查询输出,为新增维度说明预期取值范围、有用消费者与负责人。生产计数器速率查询还应保留正确的重置处理;本实验用于比较固定总量的即时求和,不能代替先求速率再聚合的运行监控语义。
快速验证 保存程序为 cardinality.py,执行 python3 cardinality.py。需要 Docker 与 Prometheus 3.5.1,必要时下载镜像。本地文本端点为三个路由分别导出四个合成客户身份,同时独立导出已经累计好的路由总量。真实服务器抓取这些样本,并提供查询结果与 TSDB 统计,实验不会使用任何真实客户信息。
...

检查点为 PostgreSQL 崩溃恢复建立一个可用起点。它同步这个检查点所需的数据文件变更,并记录相应恢复信息。依赖某段日志的数据页写出之前,相关 WAL 必须先持久化。这样的顺序让事务提交不必每次都把全部修改过的表页立即刷到磁盘;发生崩溃时,再从恢复起点重放后续必要变更。
坏掉的样子 检查点不是事务提交。数据页可以包含尚未提交事务写入的版本,读者究竟能看到哪些行,仍然由可见性与事务状态决定。检查点完成,不会把这些写入变成已提交,也不会把随手复制一份正在变化的数据目录变成有效基础备份。并发活动还可能再次弄脏页面,所以不能把完成提示理解成缓冲池从此全部干净。
日志保留也有别的限制。最大 WAL 大小是与自动检查点调度有关的软目标,不是磁盘使用硬上限;归档失败或复制槽保留,都可能让日志超过这个值。最小 WAL 大小则涉及回收并预留文件供以后复用,不是另一条强制触发检查点的阈值。如果理解错这些参数,可能一边看到检查点间隔正常,一边忽略实际上还在持续增长的归档积压。
维护进度与事务可见性分开;最终值 11 有断言,检查点比较是观察。双会话运行仍待验证。 快速验证 准备一台安静的 PostgreSQL 18 可丢弃实例,开启甲、乙两个全新的 psql -X -v ON_ERROR_STOP=1 会话。显式检查点和控制文件信息使用具有相应权限的管理角色。保留正常开启的同步落盘与同步提交设置。乙先创建计数器,并保存当前检查点位置:
SHOW fsync; SHOW synchronous_commit; CREATE SCHEMA checkpoint_demo; CREATE TABLE checkpoint_demo.items (id integer PRIMARY KEY,n integer); INSERT INTO checkpoint_demo.items VALUES (1,10); CREATE TABLE checkpoint_demo.baseline AS SELECT checkpoint_lsn FROM pg_control_checkpoint(); 显示的两个设置都应为开启,初始已经提交的值是十。甲在一个保持打开的事务中把它改成九十九:
...

nginx 请求限流使用漏桶方法,依据配置速率计算超额请求。burst 决定可以接受多少超额,nodelay 和 delay 则影响已经被接受的超额请求何时继续处理。这里涉及准入与调度,并不是所有超过名义速率的请求都会失败,也不是直接测量应用里有多少并发工作。理解这几个不同维度,才能解释一次突发里为何同时出现成功、等待和拒绝。
坏掉的样子 把 nodelay 解释成“立即丢弃超额”,正好说反了它的作用。在突发额度以内,它允许超额请求不经过通常的延迟,直接继续处理;超过额度才拒绝。已经接受的突发仍消耗需要随时间恢复的容量。后端很快完成,并不会让下一批突发自动获得全新额度,因此不能把 nodelay 理解成只管单次批次、不累计速率债务。
被延迟的请求也不会自动变成一串 503。突发范围内获准等待的请求,可以在等待之后得到成功应用响应;超过配置额度才会按所选状态拒绝。客户端自己的截止时间可能在等待中到期,所以服务器能接受的排队,不一定满足客户端延迟契约。讨论结果时应区分服务端拒绝、获准等待和客户端主动超时。
配置语法同样需要验证。在 nginx 1.28.0 中,显式写 burst=0 会被拒绝,尽管省略 burst 的默认超额额度就是零。无效配置不是一种更严格但正常工作的限流策略。应同时做语法检查和真实请求验证,并确认受保护 location 确实进入限流阶段,而不是在更早的 rewrite 阶段通过 return 提前结束。
六客户端计数假定在额度恢复前到达。请求速率决定不限制长时间并发占用;旧标签链接保留,显示名称已为“漏桶”。 快速验证 下文结果归属于历史文章报告,本轮没有重跑;保留的验证文件中未找到原始集成运行日志。运行需备好本地固定镜像并启用 Python 断言;清理仅尽力执行,不能据此保证所有资源已删除。
将脚本保存为 check-burst.py,准备 Python、Docker 和固定镜像后执行。记录环境使用 nginx 1.28.0 和真实 HTTP 后端。每个案例采用独立的公开测试 key、每秒一个请求的速率,并同时放行六个客户端。nginx 在响应头中暴露本次限流决定,入口只发布到宿主回环,结束后尝试清理自己的实验资源。预期计数假定请求在下一次额度恢复前到达;调度延迟可能使这个时间前提失效:
...

位置与分数:示意。
Redis 列表保存允许重复值的有序序列;有序集合保存唯一成员,并按数值分数排序。真正有用的区别不是谁总是更快或更省内存,而是应用需要按位置处理序列、维持唯一成员、查询分数范围,还是需要另一种数据模型才能表达的组合语义。
先定义什么算不同条目 使用 RPUSH 时,列表按追加顺序保留值,也保留重复项。随后 LPOP 移除最旧值,RPOP 则移除最新值。换一种推入与弹出端的组合,顺序也会变化,因此不能不看生产者命令就断言列表天然是先进先出。多个工作进程并行执行时,移除顺序也不保证业务完成顺序。
ZADD 为每个成员关联一个分数。再次加入同名成员,是更新分数,不是增加第二条记录。默认返回值统计新增成员数量,所以返回零也可能是成功更新。如果直接拿重复的事件载荷当成员名,多次出现会合并成同一个成员;当每次出现都代表独立任务时,这样建模就会丢掉业务区别。
分数允许并列,同分时按成员字节排序,不按到达顺序,也不是本地化文字排序;REV 同时反转同分成员的先后。排行榜可能接受这个规则,时间索引则可能需要独立事件标识和明确的并列处理方式。分数使用浮点数,因此还要选择能够保留所需数值精度的单位,不能把排序正确等同于任意精度都能表达。
位置和分数选择的是不同内容 列表不只支持两端访问。LINDEX 可以读取一个位置,LRANGE 返回包含边界的位置区间。到达内部位置需要遍历;两端访问便宜,不代表返回大量元素也无需付出相应成本。对只读取头尾、长度受控的短列表来说,保留一个小窗口完全可能是适合的方案。
ZRANGE 默认解释为从零开始的排名位置,结束位置也包含在内。BYSCORE 才把两个边界解释为数值分数,在分数前加左圆括号则排除该边界。官方给出的范围读取复杂度是 O(log N + M),其中 N 为成员数,M 为返回数量。因为有索引,就认为大结果集或很深的偏移扫描没有成本,是不成立的。
持续变化的两种结构都不会让按排名分页变成快照。插入或分数更新可能移动后续页的位置,分开的请求就可能重复或遗漏。选择容器之前应先决定读取契约。内存开销也没有普遍相近的保证:底层表示、成员数量、载荷长度和配置都会改变结果。容量是决策因素时,应测量具有代表性的数据,而不是根据类型名字推断。
坏掉的样子 时间戳分数不是成员的过期截止时间。ZREMRANGEBYSCORE 显式删除数值区间,LTRIM 显式保留按位置选择的窗口;两者都不会因为时间过去或有人读取而自动清理。追加与清理如果分成两个命令,就是两个操作,保留逻辑仍然需要自己的触发与并发策略。
有序集合也不是完整的延迟任务调度器。ZPOPMIN 会立即移除最低分成员,即使分数表达的是未来时刻。先查到期成员、再用另一条命令删除,可能让多个工作进程选到同一任务。原子认领、处理确认以及遗弃任务恢复都是额外要求,不会因为分数放了时间戳就自然具备。
列表弹出同样发生在业务处理完成之前。移除后消费者失败,若没有合适的处理协议,任务就可能丢失。需要序列操作时选列表,需要按分数索引唯一成员时选有序集合;不能从任何一种排序方式推导出送达保证。数据结构选择与业务处理协议应分别判断,避免用容器替代完整语义。
列表位置保留每次出现,有序集合分数排列唯一成员。未来分数示例说明立即移除,不代表完整调度器。来源夹具未重跑。 快速验证 原文报告的环境是 macOS、Python 3.14.4、Redis 8.10.1 和 redis-py 8.1.0;本轮查阅的证据中未找到匹配的执行日志,也没有重新运行。需要 POSIX、C 编译器、make、curl、tar 和 Python 3.10 或更新版本。必须启用断言,不能使用 -O 或 PYTHONOPTIMIZE,否则检查及其中必需的调用都会被跳过。
...

大量删除之后,PostgreSQL 表可能只剩少量记录,却仍保留很大的文件。普通清理让旧空间可以在表内复用,完全清理则把存活内容写入新文件,同时重建索引。更彻底的空间回收,需要另一套锁与容量预算,应作为有计划的维护操作,而不是一看到文件大小偏高就立刻执行的日常反应。
坏掉的样子 重写需要的是 ACCESS EXCLUSIVE 表锁,普通读取和写入都与它冲突。某个查询虽然已经执行完,但事务仍然打开、继续持有表锁,也会挡住重写。把它叫成独占行锁,或者以为只有写请求受影响,会低估实际中断范围。排队等待重写的强锁请求,还可能影响随后进入的其他取锁请求,不能只看当前命令有没有开始复制数据。
替代文件成功建立之前,原文件仍然存在,所以操作需要额外磁盘空间。等文件系统已经写满才着手完全清理,可能反而没有容纳新文件的余量。应提前评估替代堆表与索引、磁盘读写和日志影响,以及能够承受的持锁时长。常规自动回收不会自动替你执行这类完整重写。
VACUUM FULL 需要冲突表锁和替代文件余量。SQLSTATE 断言提案仍未在双会话中执行;未声明生产耗时结果。 快速验证 早先审阅记录只报告在 PostgreSQL 18.6 单用户模式中执行了准备与成功压缩的 SQL 部分,双会话锁实验仍未执行;本轮源文审阅没有运行任何 SQL。
只有在获准的真实实验中,才在同一个 PostgreSQL 18 可丢弃数据库中开启甲、乙两个全新的 psql -X -v ON_ERROR_STOP=1 会话。把乙的每个代码块存成文件,通过 \i 加载;任何脚本错误都必须停止整个练习。清理命令必须在显式事务块之外执行。乙先创建五千行、删除前四千五百行,然后进行普通清理:
CREATE SCHEMA compact_demo; CREATE TABLE compact_demo.items (id integer PRIMARY KEY,payload text) WITH (autovacuum_enabled=false); INSERT INTO compact_demo.items SELECT g,repeat(md5(g::text),10) FROM generate_series(1,5000) AS g; CREATE TABLE compact_demo.baseline AS SELECT pg_relation_size('compact_demo.items') AS heap_bytes, pg_relation_filenode('compact_demo.items') AS file_id; DELETE FROM compact_demo.items WHERE id<=4500; VACUUM (ANALYZE) compact_demo.items; SELECT pg_relation_size('compact_demo.items')=heap_bytes AS same_heap_size, pg_relation_filenode('compact_demo.items')=file_id AS same_file FROM compact_demo.baseline; 两个比较都返回真:堆文件大小与文件标识没有改变。存活行位于原文件尾部,末尾不存在可以截断的连续空页,所以普通清理不能通过截断来缩小这个特定文件。这不代表普通清理永远不会缩文件;如果末尾确实存在可释放的空页,就可能归还相应空间。这里要区分样本布局和命令的一般能力。
...

变量保存的是一个字符串,但没有引号的展开可能把它变成多个命令参数。Bash 的分词阶段使用 IFS 指定的分隔字符检查展开结果,因此,手动引用后能正常使用的路径,放入变量再传递时却可能突然找不到文件。排查时首先应观察接收命令实际拿到的参数数量和每个参数的内容,也要区分空字符串与完全没有参数,不能仅凭终端上看起来相同的一行文本判断输入一致。
IFS 控制分词,而引用还能阻止文件名展开。空参数和没有参数仍是不同情况。 是否分词取决于上下文 默认 IFS 包含空格、制表符和换行符,它们的连续组合会分隔字段,开头和结尾的这类空白也会被忽略。未引用变量里有两个单词时,命令通常收到两个参数;变量为空时,则通常连一个参数都没有。使用 "$value" 会保留一个参数,即使值是空字符串。对于命令行接口,显式传入空值和省略选项值可能触发完全不同的行为。
分词针对适用上下文中的未引用参数展开、命令替换和算术展开结果,并不是重新解析整条命令。普通赋值语句 copy=$value 会保留原字符串,不执行分词和文件名展开。但随后把 $copy 不加引号地作为命令参数使用,仍会重新引入两个问题。Bash 双方括号条件表达式也具有不同于普通命令参数的展开规则,所以赋值或条件判断成功,不能证明后面的外部程序调用也安全。
IFS 是一组分隔字符,不是多字符分隔串,也不是正则表达式。非空白分隔符和空白的处理并不相同,例如连续两个冒号可以产生一个空字段,而连续默认空白通常只是一个字段边界。把 IFS 设为空字符串会关闭分词,但不会把未引用的空展开变成显式空参数。若数据格式有引号、转义或嵌套结构,应使用理解该格式的解析器,不能把调整 IFS 当作通用解析方法。
坏掉的样子 关闭分词并没有关闭随后的文件名展开。即使 IFS 为空,变量里的星号仍可能匹配当前目录下的文件;目录里刚增加一个文件,同一条命令的参数数量就会改变。引用变量能够同时阻止这两种转换,而修改 IFS 只处理了其中一个阶段。控制通配符的 shell 选项也独立存在,不能把它们和分词配置混为一谈。
...

一条 AAAA 资源记录保存一个一百二十八位 IPv6 地址,A 记录则保存 IPv4 地址。同一个名称可以只有其中一类、同时拥有两类,或者在每类中包含多个地址。A 与 AAAA 属于不同记录集,因此可以使用不同 TTL。发布 AAAA 让地址能够通过 DNS 被发现,却不会替你创建接口、路由、监听套接字或可用应用;这些条件需要在实际服务路径上成立。
AAAA 发布的 IPv6 数据独立于 DNS 查询传输。文章实验另行验证本地 HTTP 可达性,不证明公网 IPv6 连通性或主机名 TLS。 坏掉的样子 AAAA 查询成功之后,连接仍可能失败。发布的地址可能已经停用,服务可能只监听 IPv4,网络也可能缺少可用 IPv6 路由。两种地址族的防火墙与 TLS 配置也可能不同。只通过 IPv4 做可用性检查,会漏掉这些问题,而双栈用户可能正沿着公开的 IPv6 路径遭遇延迟或错误。记录存在与服务可用是两个需要分别观察的事实。
...