文章是本站的原创长文。每篇都保持无脚本、来源可溯,并与本站记录的系统现实一致:一个小型、静态、公网服务如何被构建、加固并保持诚实。
SSH 代理为公钥认证提供签名操作。对于普通软件密钥,加载加密私钥时会解锁相关材料,此后代理便可以使用它,不必每次连接都重新询问文件口令。正常代理协议向客户端返回公钥身份和签名,而不是导出私钥。硬件支持的身份有所不同:私钥可以一直保留在认证器里,并要求触摸或用户验证,不能把所有代理身份都描述成完整私钥必然存放在代理内存中。
套接字提供的是服务访问能力 OpenSSH 通常通过 SSH_AUTH_SOCK 查找代理,但客户端配置中的 IdentityAgent 可以选择另一个套接字,也可以关闭代理使用。知道路径并不等于有连接权限。套接字和父目录的权限都重要;root 以及与用户同身份运行的其他进程,则是需要考虑的威胁。环境变量本身不能代替操作系统隔离,隐藏路径也不是可靠的保护边界。
能够访问代理,并不保证每个签名请求都会成功。密钥约束、确认要求、硬件在场和协议限制都可能拒绝请求。即使签名成功,远端服务器也不必接受这把密钥或授予权限。因此,本地签名能力与服务端授权决定应分开观察。诊断“代理里有钥匙但登录失败”时,不能只停留在身份列表这一层。
同时限制身份与委托 ssh-add 加载身份,并在需要时询问文件口令。但 ssh 的 AddKeysToAgent 等配置路径也能加载身份,不能说只有 ssh-add 才可能解锁。代理避免反复读取加密私钥文件,却不保证交换空间、转储、备份或其他进程中绝不存在私钥材料。操作系统与密钥存储策略仍然需要独立审阅,代理不是整台机器的保密证明。
-t 为添加的身份设置存活期限。到期后身份会被移除,这个参数不只是提醒;代理也可以提供默认期限;两种期限都没有时,普通身份才可能一直留到显式删除或代理退出。移走、删除或者替换磁盘文件,并不会自动替换代理里已经加载的副本。轮换时应检查实际使用的指纹,而不是只确认文件名已经改变。
-c 通过 ssh-askpass 要求确认。决定是否批准的是辅助程序是否成功退出,而不是远端主机有没有终端。需要验证本地确认机制确实能工作,并拒绝意外请求。确认可以减少无人值守的使用,但仍需理解提示所代表的主机和操作,不能看到弹窗就习惯性通过,也不能把没有远端终端当作必然拒绝的证据。
OpenSSH 8.9 引入的目的地约束使用主机密钥进一步限制允许的认证路径。经过转发时,客户端与服务端都需要支持相关协议,已知主机信息也必须正确识别目标。恶意中转节点仍可绕开协作式 SSH 再次中继套接字,但签名仍受获准目的地限制。这不是防火墙;依赖约束前应核对兼容性、保持更新,并理解主机密钥变化对路径的影响。
坏掉的样子 代理转发允许远端在转发通道存在期间中继签名请求。那里的管理员或被入侵进程可能使用这份能力,而不需要提取私钥。没有必要时应避免转发,并查看客户端最终生效的配置;命令行没有 -A,不足以证明配置文件没有开启它。风险在于签名委托,不在于私钥文件一定已经复制到远端。
从代理删除身份,并不会撤销已经被复制的私钥、删除服务器 authorized_keys 中的条目,也不保证关闭已建立的 SSH 会话。反过来,服务端撤销授权可能阻止新登录,即使代理仍保存这把钥匙。恢复计划应分别处理受影响的副本、授权记录和活动会话,而不是把其中一个动作误当成全部撤销。
现代 OpenSSH 代理还限制远端加载提供方和部分 FIDO 签名用途,但识别远端客户端依赖协作式 SSH 转发;这些机制可减少把转发认证器用于 SSH 之外的特定风险。这些保护不是公开套接字或随意放宽选项的理由。不同版本和平台集成的默认值、套接字位置可能不同;排查时应阅读当前实现的文档,不能把某次输出的路径和权限数字当成永远成立的协议常量。
...
备用节点只有在提升路径可用、旧写者不能破坏新历史时才有价值。故障切换既是决策,也是流程与演练。副本看起来健康,不代表客户端能连上它,也不代表所有已确认写入都已经复制过去。先确定恢复时间目标与可接受的数据损失,再决定何时切换,而不是等报警响起才现场讨论。
提前设计好决定 确定谁有权批准提升、什么证据足以说明故障,以及由自动化还是操作员执行。心跳丢失可能是网络分区,而不是主库已经停止。故障检测本身不能授权两个独立写者。系统需要真正执行所有权边界的机制,例如经过确认的节点隔离,或存储端校验的 fencing,并使它与复制协议的保证相匹配。
单写者服务的安全目标是「最多一个被接受的写者」,并非每一瞬间都恰好有一个可用写者。隔离旧节点后、提升新节点前,暂时没有写者可能正是正确的停机选择。操作手册应写明候选节点、复制位置、可接受的丢失范围、客户端如何重连,以及前提不成立时由谁叫停。自动切换能缩短决策时间,却不能凭空补回尚未复制的数据。
演练真实经过的路径 根据风险决定演练频率,并在相关变更后重测。演练只能证明实际覆盖到的路径:绕过 DNS 的数据库提升不能验证地址缓存;测试客户端使用另一套身份,也不能证明生产权限正确。在安全前提下,加入真实的重连流程和已确认写入核对。记录范围、耗时、复制延迟、拒绝条件及恢复证据,不要把最近一次成功的日期当成完整的就绪评分。
开始前先安排安全停止点和恢复步骤。新主库一旦接受写入,切回旧节点往往就不再是撤销一项配置那么简单。旧主库可能需要对账或重建为副本,不能因为进程重新启动就恢复写权限。备份提供另一条恢复路径,但「有备份」本身也不等于已经验证能恢复。
坏掉的样子 旧主库在提升之后仍可写,就会产生脑裂。控制器里记录一句「已隔离」不够,存储或隔离机制必须真正阻止旧写入。落后的候选节点也可能在切换成功后丢失已确认写入。延迟阈值必须有明确单位,并与数据损失目标相连;允许非零延迟,不能同时宣称零数据丢失。
健康检查反复抖动会触发来回切换,应设置明确的故障判定和恢复条件,不要在重新同步之前自动切回。DNS 修改只影响后续查询:已有连接可能持续使用旧地址,按旧 TTL 缓存的回答也不会立刻消失。事故发生时调低 TTL,不能追溯缩短已经缓存的寿命。除了权威 DNS 的响应,还要验证真实客户端的重试、缓存和重连行为,否则控制台显示成功,用户路径仍可能失败。
先隔离,再提升写者。 依据原文与明确限制静态绘制;未为本图重跑原文夹具。 快速验证 使用 Python 3.9 或更新版本,将下面内容保存为 failover_drill.py,在临时目录运行 python3 failover_drill.py。运行时不要使用 -O、-OO 或非零的 PYTHONOPTIMIZE,以免断言被跳过。它只修改内存对象。健康状态和复制延迟是模拟值,fence() 只是更新集合,并不执行真实隔离。模型保留一个被指定的主节点,但隔离期间允许没有可写节点。候选节点通过检查后会被明确解除隔离,因此切回旧节点时不会出现「已经成为主库,却还留在隔离集合中」的自相矛盾状态。
...
污点排斥没有容忍对应效果的 Pod,容忍度只移除这项障碍,不会强制把负载送到专用硬件。专用节点设计通常还需要标签与必要的节点亲和,再分别检查容量和准入策略。不能把通行资格理解成目的地保证,也不能把容忍度当成工作负载的完整权限边界。
容忍度移除匹配污点这一项障碍。效果决定后果,放置与权限仍是其他机制。 给专用者的标记 NoSchedule 污点在未被容忍时,阻止普通的新调度,但不移动已有 Pod。这可以把 GPU 等专用节点留给特定负载。不能进一步概括为所有污点都不影响运行中程序:NoExecute 具有驱逐行为,PreferNoSchedule 则只是调度偏好。首先看清 effect,才能判断这次变更会影响新放置还是已有实例。
污点不是安全隔离。有权提交合适容忍度的主体可能获得池子资格,直接指定 nodeName 还会绕过普通调度检查。准入限制、授权和设备资源请求是另外的控制;一个 GPU 容忍度既没有请求 GPU,也不保证有空闲 GPU。审阅专用池时,需要同时核对谁能修改模板、谁能添加容忍度以及真正需要哪些硬件资源。
效果决定行为 NoSchedule 对未容忍的 Pod 构成硬性调度排除;PreferNoSchedule 提供软性避让偏好,单靠它不会让节点完全失去资格。NoExecute 还可能移除已经绑定、却没有匹配容忍度的 Pod。匹配 NoExecute 的容忍度可以带 tolerationSeconds,限定污点出现之后还能停留多久;不写时长与设置短时长具有不同后果。
...
窗口分区决定哪些行属于同一组,窗口帧决定其中哪些行参与当前行的某个帧敏感计算。把两种边界混淆,可能让逐笔累计变成重复显示的小计,也可能让 last_value 返回当前值,而报表原本需要的是分区最后一行的值。应先说明计算要回答什么,再选择排序和帧范围。
默认范围取决于是否排序 PostgreSQL 的默认帧是 RANGE:从分区开头到当前行的最后一个并列行。窗口 ORDER BY 中全部表达式都相等的行,属于同一个并列组。因此,有排序时的默认累计和,会包含当前行的全部同值伙伴,即使其中某些行在最终展示结果里排在后面,也已经进入当前帧。
如果窗口没有 ORDER BY,所有行都是并列行,默认帧就覆盖整个分区。因此,sum(amount) OVER (PARTITION BY account) 会重复显示账户总额;添加排序后,计算就变成有顺序的累计结果。“默认总是整个组”和“默认每次恰好多加一行”都不能同时描述这两种情况,不能把其中一种当成通用规则。
窗口顺序与最终输出顺序是分开的。外层 ORDER BY 可以改变展示排列,而不改变帧。反过来,在 RANGE 窗口内部加入唯一标识,可能把原来的并列组拆开,改变计算结果;这不只是让页面看起来稳定,而是修改了语义,应当按需求审阅。
按行、按值域和按并列组 ROWS 计算窗口排序中的位置。从前两行到当前行,最多包含三行;靠近分区起点时还会更少。它不表示三天或三分钟。如果多个事件时间戳相同,而业务需要确定的逐事件顺序,就应加入合适的唯一标识,明确同一时刻的事件谁先谁后。
RANGE 使用排序值和并列边界。指定偏移量时,PostgreSQL 要求只有一个排序表达式,并使用兼容的偏移类型,例如时间戳可以配合 interval。基于时间值域的帧按时间差纳入事件,忙时可能包含很多行,闲时可能很少。GROUPS 则按并列组计数,让帧沿不同排序值对应的组移动,同时保留组内全部并列行。这三者表达的是不同的计量单位。
first_value、last_value、nth_value,以及作为窗口函数使用的聚合,都依赖帧范围。排名和 lag、lead 的定义则基于分区顺序,并非相同规则;缩小帧不会让 lag 忘记分区中的前一行。解释某个窗口函数前,应查看该函数的语义,不能把“只看到帧内的行”套用到所有函数上。
帧成员范围不会覆盖函数本身的空值规则。PostgreSQL 18 的 last_value 及相关取值函数采用 RESPECT NULLS,末行为空时结果也为空;sum 等聚合通常忽略空输入。应先确定报表要末行取值、最后一个非空值,还是数值最大值。这些需求不能仅靠把帧延长到分区末尾就互相替代,改窗口之前要把空值案例写入验收条件。
...
容量规划把需求预测转化成一个有依据的运行范围。真正有用的问题,不是机器在崩溃之前还能接收多少请求,而是服务能以满足延迟和正确性要求的方式持续处理多少工作。答案受到请求成本、共享依赖和故障假设影响。只有把这些假设的来源、适用范围与负责人写清楚,表格里的实例数量才能支持采购与可靠性决策,而不只是把直觉包装成一个数字。
测出能够接受的运行点 压测应使用具有代表性的请求组合、数据规模、缓存状态、并发方式和软件配置。在增加压力时,同时记录延迟、错误、处理器、内存、队列、存储与连接使用情况。满足要求的最高已测试点,是针对该工作负载的证据,并不证明所有中间点或下一版本都表现相同。只有吞吐量而没有结果质量的测试,甚至可能把快速拒绝请求的服务器评成性能优秀。
不同瓶颈需要分别统计。增加应用副本可能带来更多计算资源,却没有扩大共享数据库的连接额度。请求变贵时,每秒请求数与资源消耗的关系也会改变。因此,按请求速率规划只在成本分布可比时才有意义,还需要监测底层资源,并在工作负载变化后修订模型。不能拿安静时段的平均负载,直接推导高峰时的服务承诺,也不能假设所有资源会同时到达上限。
预测需求,并主动减掉会失去的容量 从实际峰值出发,明确准备应对的是增长、季节波动还是某次活动,再把采购或扩容提前期和预测误差写入计划。运行余量应该对应具体原因,例如负载不均、需求低估或新增实例需要时间,而不是沿用旧表格里来源不明的百分比。出现有意义的版本、数据量或流量结构变化后,应同时复查单机测量和需求假设,避免只更新其中一个数字。
接着按可靠性设计减去可能失效的容量。服务分布在三个可用区,如果要求失去一个区后仍能承接同样峰值,所需实例数就不同于只考虑健康集群的方案。还要核对共享依赖是否幸存、流量如何迁移,以及新增实例能否在队列失控前就绪。均匀部署不等于请求必然均匀分配,自动扩缩容也无法瞬间提供资源,这些都应进入故障演练与容量边界。
坏掉的样子 常见错误是用预测流量除以单机最大吞吐量,向上取整后便结束计算。这样的结果可能通过健康状态检查,却在损失一个可用区后立刻超载。另一个问题是重复计算预留容量:必须区分压测已经验证的速率,与规划时主动采用的更低运行速率。汇报剩余容量时也应说明相对于哪个边界,避免把规划额度内的余量误称为硬件物理吞吐上限。
线性扩展可能在共享锁、数据库、网络链路或外部配额处失效。冷缓存和同步重试还可能使故障切换后的请求比原压测更昂贵。应在真实约束下验证失效场景,并记录模型何时不再成立。如果现有资源无法安全承接预测需求,可能需要经过验证的准入控制或降级方式,等待资源到位;在表格里加入乐观假设,不能改变实际排队和超时行为。
按示例规划速率,16 个幸存节点为 2,000 的预测需求提供 2,240 请求/秒。240 的余量相对于规划限制,依赖已声明的扩展与依赖假设。 快速验证 保存为 capacity.py 后,用 Python 3 执行。输入是固定工作负载下的人造压测记录,实验用于核对实例计算与故障假设,不代表当前机器的真实性能。示例把可接受测试点的百分之七十作为规划速率,这是为本例明确选择的余量规则,不是适用于所有系统的利用率建议,实际团队应解释自己的预留原因。
from math import ceil # Synthetic load-test results for one fixed request mix and machine size. samples = [ {"rps": 150, "p99_seconds": 0.11, "error_ratio": 0.0001}, {"rps": 200, "p99_seconds": 0.22, "error_ratio": 0.0003}, {"rps": 250, "p99_seconds": 0.50, "error_ratio": 0.0020}, ] acceptable = [s["rps"] for s in samples if s["p99_seconds"] <= 0.30 and s["error_ratio"] <= 0.001] tested_rps = max(acceptable) forecast_rps = ceil(1600 * 1.25) zones = 3 surviving_zones = zones - 1 planning_rps = tested_rps * 0.70 naive_per_zone = ceil(ceil(forecast_rps / tested_rps) / zones) planned_per_zone = ceil(forecast_rps / (planning_rps * surviving_zones)) naive_survivor_rps = naive_per_zone * surviving_zones * tested_rps planned_survivor_rps = planned_per_zone * surviving_zones * planning_rps assert tested_rps == 200 and forecast_rps == 2000 assert naive_per_zone * zones == 12 and naive_survivor_rps == 1600 assert planned_per_zone * zones == 24 and planned_survivor_rps == 2240 assert naive_survivor_rps < forecast_rps <= planned_survivor_rps print(f"tested={tested_rps} rps/node; forecast={forecast_rps} rps") print(f"naive={naive_per_zone * zones} nodes; zone-loss capacity={naive_survivor_rps} rps") print(f"planned={planned_per_zone * zones} nodes; zone-loss planning capacity={planned_survivor_rps:.0f} rps") print(f"zone-loss spare={planned_survivor_rps - forecast_rps:.0f} rps") 已列出的记录中,每节点每秒二百次请求满足要求,二百五十次的记录则未达到延迟和错误标准。需求经过增长假设后变为每秒二千次。只按测试速率计算并均匀分配的方案共有十二个节点,失去一个区后只剩八个,即使每个节点都运行在已接受的测试速率,也只能承接每秒一千六百次请求,低于预测需求。
...
文件描述符是一个进程内部用于引用已打开资源的小整数。进程的描述符表把这个数字映射到内核维护的打开文件描述;对于普通文件,打开描述包含当前偏移和文件状态标志。另一个进程可以把相同整数用于完全不同的资源,因此数字本身不是全局文件身份。理解描述符表、打开描述与底层文件之间的关系,才能正确判断复制、关闭和继承的效果。
描述符编号只标识进程内的表项。dup 共享打开描述状态,独立 open 拥有独立状态;close 与 unlink 结束的是不同引用。 坏掉的样子 dup 分配另一个描述符,让它引用同一份打开文件描述;它不是简单复制整数,也不是重新按路径打开文件。因此,两份描述符读取普通文件时共享偏移。相反,再次打开相同路径,通常会得到具有独立偏移的新打开描述。设备与 inode 身份相同,不代表打开状态也相同,排查读取位置异常时必须区分这两个层面。
...
环境文件把部署取值与描述进程监管的单元定义分开。systemd 在执行服务命令前读取指定文件,再把取值放入那个进程的环境。修改文件不会直接重写已经运行的进程环境。不过,重载辅助命令本身也是一个新进程,因此可能读到比现有主进程更新的文件值。理解这个执行时点,比笼统记住“改配置后做一次 reload”更可靠。
EnvironmentFile 在服务命令执行时读取。second、third、fourth 是原文样例值,不是本轮观测。文件由 systemd 按自身规则解析,并非执行 shell。本轮保留历史运行验证受阻状态。 坏掉的样子 daemon-reload 要求管理器重新读取单元定义、更新依赖信息;reload 则请求服务执行已配置的重载操作,例如 ExecReload。两者都不是把现有进程环境整体替换掉的通用指令。服务可以自行实现应用配置刷新,但这种能力必须来自应用或重载辅助程序,不能仅凭管理器接受了一个重载请求就认定业务设置已经切换。
EnvironmentFile 路径前加短横线,表示这个文件可以不存在。缺失时 systemd 会忽略它,并不会发出操作者可能期待的那条警告。因此,路径笔误可能让一个覆盖配置悄悄消失。只有缺少该文件确实具有合法含义时,才适合使用可选形式;必需的部署输入不应借这个前缀被静默跳过,否则很容易把供应错误变成难以解释的默认行为。
文件由 systemd 解析,不是交给 shell 加载。它有自己的引号和转义规则,但不会把命令替换和变量展开当作 shell 执行。包含 $HOME 的值仍然是这段文本。后列出的文件覆盖前面的同名值,文件赋值又覆盖单元中的 Environment 赋值。因此,需要检查真实优先级,不能认为自己首先看见的那一行就是最终值。
快速验证 在 PID 1 为 systemd 的隔离 Linux 环境中,以管理员身份运行下面的 Python 脚本。它只使用无害的样本设置与临时路径,恢复文本记载的环境为 Debian 12、systemd 252.39,但原始运行日志不可用。本轮没有执行此样例;以下结果说明其预期断言,不能视为本轮实测。确认运行时单元名称尚未使用。工作程序仅记录四个特定测试值,不会把整个环境全部打印出来:
...
发布订阅没有回放:示意。
Redis 发布订阅面向消息发布时已经生效的订阅广播,不会为断线的听众保留收件箱。它适合可以被后续状态替代的实时通知,但 PUBLISH 返回正数,并不证明某个工作进程完成了任务,也不表示离线消费者回来后能够补领消息。
订阅是一次握手 SUBSCRIBE 会先返回订阅确认,连接随后才能收到后续发布的消息。协调发布者与新听众时,应等待这份确认。仅仅把订阅请求发送出去还不够:Redis 处理请求之前,另一个连接就可能已经发布。重新连接和订阅只恢复未来的投递,不会填补之前的历史空白。
在持续在线的连接上,听众按发布顺序接收消息。这里的顺序反映 Redis 的处理顺序,不是多个独立生产者携带的业务时间戳。即使接收有序,把事件分发给并行处理器后,完成顺序仍可能改变。因此,声称发布订阅完全没有顺序是错误的;把有序接收扩大为业务副作用也必然有序,同样不成立。
多个听众各自收到广播,并不是竞争弹出同一个列表元素。频道订阅与匹配的模式订阅是两条独立投递路径。同时订阅 events 和 event*,一次发布就可能产生一份 message 和一份 pmessage;多个模式重叠也会如此。最多一次描述的是没有重试保证,不是跨订阅路径的业务去重承诺。
严格解释返回值 在固定版本的单机实现中,PUBLISH 分别统计频道和模式匹配的投递次数,因此两个客户端连接可以得到返回值三。固定版本的源码确实在两条路径分别递增计数。对于 Redis Cluster,官方说明计数只包含连接到发布节点的听众,因此它也不是整个集群的受众总数。
该响应不会等待处理器提交数据库事务、完成界面更新或确认业务完成。返回零只说明当时没有计入匹配接收者,返回正数不能证明处理成功。如果应用需要识别重叠订阅或生产者重复尝试造成的重复事件,应在消息中放入稳定的事件标识,不能用返回的数字替代业务确认。
坏掉的样子 订阅中断期间发布的消息会被错过。重连循环无法请求某个偏移之后的内容,因为发布订阅既无保留日志,也无回放游标。用于缓存失效时,一次通知丢失可能让旧值持续存在,直到另一个刷新机制介入;用于账单或任务派发时,则可能直接丢掉必须执行的工作。需要回放就应选择保留消息的队列或流,并明确恢复与持久化策略。
慢听众并不是一变慢就被随机丢掉几次读取。Redis 会缓存待发送数据,客户端输出缓冲触达配置的硬限制,或持续超过软限制后,连接可能被关闭。这限制了客户端缓冲占用,却会造成无法补读的订阅空白。应观察断连重连频率及缓冲占用,限制回调执行的工作量;调大缓冲并不等于获得送达保证。
在线订阅有序投递但无回放。匹配投递可重叠,也不确认业务完成。本轮未运行退订夹具。 快速验证 原文报告的环境是 macOS、Python 3.14.4、Redis 8.10.1 和 redis-py 8.1.0;本轮查阅的证据中未找到匹配的执行日志,也没有重新运行。需要 POSIX、C 编译器、make、curl、tar 和 Python 3.10 或更新版本。必须启用断言,不能使用 -O 或 PYTHONOPTIMIZE,否则检查及其中必需的调用都会被跳过。
...