up 脚本运行了,以零退出,但它里面的任务却无法访问一个私有服务,而两秒后从终端访问同一个服务却完全正常。直觉会归因于时机,而通常的修法是加一个 sleep,这并没有理解竞争,只是把它藏了起来。更有用的解读是:“up” 是一个钩子的名字,而不是就绪保证。它在隧道设备被配置时触发,这早于路由、DNS,当然也早于隧道后面的任何应用。

shell 测试之所以成功,是因为它在一切都稳定之后才运行。钩子只在某个特定时刻运行。

up 在启动顺序中的位置

OpenVPN 配置隧道设备,然后运行 up 钩子。路由安装是更晚的阶段,并且有自己的钩子,所以放在 up 里的依赖路由的任务可能看到一个没有可用路径的设备。把路由工作放进 route-up,时机问题通常就消失了。

手册记录了这个顺序以及每个钩子接收的环境。这个顺序完整解释了经典的“在我的 shell 里能跑”报告,也解释了为什么同一个脚本在一台机器上通过,却在路由阶段更慢的另一台机器上失败。这两种结果都不能说明脚本的质量。

DNS 和应用就绪还要更晚。即使路由已经安装,也不证明远端端点正在监听、某个名字能解析,或者存在可供用户级任务使用的桌面会话。

钩子实际继承了什么

钩子在 OpenVPN 进程的账户和环境之下运行,这与你的交互式 shell 并不相同。它可能缺少用户的会话总线、桌面环境,或一个只在登录时才解锁的凭据。写下来:任务真正需要其中的哪一些。

这对那些看起来像网络、却依赖用户上下文的任务很重要:用用户凭据挂载共享、设置每用户代理,或配置打印机。特权隧道钩子对它们是错误的位置,而再多的等待也修不好缺失的会话。

当任务在钩子里失败时,记录账户、环境,以及第一个失败的操作。这能把过早的时机与权限或上下文问题区分开,而这两者的修法完全不同。在两者之间猜测,正是五分钟的修复变成一整个下午的方式。

为什么加一个 sleep 不是就绪检查

sleep 会让脚本延迟并移动竞争窗口。快的那天任务成功;慢的那天它又失败,而且失败更难复现,因为延迟看起来好像有效。只在轻负载下有效的延迟是巧合,不是修复。

如果依赖的是路由,就用与路由安装对应的钩子。如果依赖的是隧道后面的服务,就用一个有上限的检查,等待那个特定条件,并在它始终不出现时明确失败。

让失败保持显式。一个在真实工作失败时仍以零退出的钩子,比一个失败的钩子更糟,因为服务管理器会记录一次成功,然后就再也没有人看。来自脚本自身的一行日志,是让这一点保持诚实的最便宜办法。

清理和重连也值得同样的用心

改变共享状态的脚本需要一条相配的撤销路径。钩子添加的路由、解析器条目和防火墙标记应当是幂等且可逆的,否则一次重连会把第二份副本叠在第一份之上。

重连还可能比一次完整的服务启动更频繁地运行钩子。一个会重新建立会话的持久接口可能再次触发 up 与 route-up 循环,因此假设每次启动只执行一次的脚本最终会行为异常。在一整天的抖动中数一数日志里的执行次数,看看它实际运行得有多频繁。然后让脚本在那么多次运行下也是安全的。

测试那个失败的生命周期,而不是之后的一条命令。重现启动或重连,观察钩子执行,并确认任务在它真正运行的那一刻成功了。之后从交互式 shell 成功,是关于依赖的证据,而不是启动路径可用的证明。

给每个任务依赖的事件命名,然后把任务挂到那个事件上。其余的一切都只是假装成检查的延迟。

相关文章