ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

一个简单操作是怎么在分布式环境下变复杂的

一个简单操作是怎么在分布式环境下变复杂的 最近碰到一个问题事后想想很适合拿来讲分布式系统里一个很普遍的现象一个在单节点上简单到不用过脑子的事放到多节点环境下复杂度会一层一层地叠上去每加一层就多一类你绕不开的问题。单节点两个 if 的事需求是这样的用户请求来了需要从 S3 拉一份代码到本地解压然后执行。这里的 S3 指兼容 S3 协议的对象存储简单理解就是一个可以按路径下载文件的远程存储服务。为了避免同一份代码被反复下载流程里加了一步先看本地有没有有就跳过没有再拉。单节点上这个逻辑简单到不需要设计if (文件不存在) { 下载并解压 } 执行就两个 if没了。你甚至不会觉得这是个需要讨论的问题。但这里藏了一个前提判断和执行发生在同一个进程里中间没有别人能插一脚。文件存在你就跳过文件不存在你就下载。检查和执行之间没有间隙。加共享存储文件系统不是本地的了项目是多节点部署的不能每个节点都存一份代码——几十个节点一份代码复制几十份浪费存储不说版本管理也是灾难。所以用了 CFS一种基于 NFS 协议的分布式共享文件系统。简单说就是所有节点通过网络挂载同一个存储卷看起来像本地目录但实际数据在远端所有节点共享同一份文件。现在问题变了。共享文件系统意味着节点 A 看到文件不存在不等于下一秒文件还不存在。节点 B 可能正在写。你的检查和下载之间多了一个时间窗口——在这个窗口里别人可能抢先动手了。单节点上检查→下载这两个动作是连续的中间没有间隙。换成共享存储后检查是检查下载是下载它们之间没有原子性保证。你检查到文件不存在正准备开始下载另一个节点可能已经写到一半了。这就是经典的 TOCTOU —— time of check to time of use。检查时的结论到真正动手的时候已经失效了。加多节点谁来做文件系统不归你管你也没法让所有节点排队——请求是并发的每个节点都独立收到请求都独立判断。现在的问题是多个节点同时检查到文件不存在都认为自己应该去下载。这不是会不会的问题是一定会。几十个节点每个节点独立做判断只要竞争窗口存在迟早有一天两个节点会同时踩进去。结果可能是重复下载浪费带宽和时间文件冲突两个写操作同时往一个路径写结果不可预期你需要的不是让检查更快也不是让下载更快而是让判断该不该下载这件事在多节点之间有共识。单节点上的那个 if在分布式环境里变成了一道协调题不是你能不能做而是大家一致同意让谁来做。每一层引入的复杂度单节点 → 共享存储 → 多节点每一步加的不是硬件是一类新问题单节点到共享存储把检查和操作之间没有间隙这个隐含前提打破了。你需要处理的不是逻辑错了是时序错了两个操作之间外界状态可能会变。共享存储到多节点把谁来做变成了一个需要协调的问题。单节点上不需要问谁该做因为只有一个进程。多节点上每个节点都不知道别人在不在做也不能假设别人没在做——你假设了就是竞态条件。有意思的是每一步引入的问题都不是上一层的解法能解决的。共享文件系统上你可以用文件锁——flock、fcntl——保证同一时刻只有一个进程在写。这在单机多进程场景是标准做法但在多节点场景下文件锁依赖的是文件系统本身对锁操作的一致性支持。CFS 能不能保证flock跨节点原子这取决于 NFS 服务端的锁实现。有些 NFS 实现不支持跨客户端锁有些支持但延迟很高有些支持但锁的语义在不同客户端上不一样。文件锁不靠谱你就得上分布式锁Redis、etcd、ZooKeeper。分布式锁解决了互斥问题但引入了新的复杂度锁的超时怎么设锁服务本身挂了怎么办锁续期是推还是拉关键不在锁在把窗口缩小到只争一次回头看这个问题真正让人难受的不是锁选哪个而是每次新版本被请求时都要走一遍检查→抢锁→下载→释放的流程几十个节点同时踩进去大部分在等锁。换个角度看真正需要协调的只有文件不存在的那个瞬间。文件一旦存在后面的请求都是读不需要互斥。所以工程上的解法通常分两层先无锁检查——大多数时候文件已经在了直接跳过不抢锁检查失败再抢锁——只在文件确实不存在的时候才去争锁争到了就去下载争不到就等着读这样锁只在首次写入时起作用。一旦写完后续请求全走快路径。竞争窗口从每个请求都在窗口里缩小到只有首次写入的那个瞬间。这是一条通用原则不要把锁放在所有请求的必经之路上。把互斥限定在真正需要互斥的那一小段其余时间走无锁的快速路径。再多一层锁争到了的人下载完文件做一个原子标记比如把文件从临时路径 rename 到正式路径这个 rename 是文件系统层面的原子操作优雅地解决了下载到一半被人读到的问题。总结一个在单节点上只需要两个 if 的操作放到分布式环境里被逼着处理了时序问题检查和使用之间状态会变协调问题谁来做大家得同意锁的可靠性问题文件锁不一定跨节点生效惊群问题几十个节点等一把锁每一步都不是代码逻辑错了是假设变了。单节点的代码假设世界是静止的分布式系统的代码要假设世界随时在变而且你不知道变成什么样。这个差距就是分布式系统复杂度的来源。
返回列表