首页
学习
活动
专区
圈层
工具
发布

Facebook 为什么要弃用 Git?

git status敲下去,终端半天不回来,磁盘读得哗哗响。

这种问题放在几万个文件的小项目里,大概率是电脑该清理了。但仓库里塞着几千万个文件、几千万次提交,再加上几千名开发人员同时推代码,版本控制工具就不再是一个命令行软件,而是一套核心基础设施。

Facebook 当年碰到的就是这个问题。

不过“Facebook 弃用 Git”这个说法并不完全准确。

Facebook 最初使用的是 Subversion 服务端,同时维护 Git 镜像。仓库规模持续增长后,他们评估过继续改造 Git,但最后认为 Git 的内部结构不适合做一次大规模、深度的扩展,于是转向了更容易修改的 Mercurial。后来 Mercurial 也扛不住了,Facebook 又做了 Mononoke,最终逐渐演化成现在的 Sapling。

这条路线不是“Git 不好用”,而是 Facebook 的仓库已经把 Git 的一些设计前提顶穿了。

最先扛不住的是工作区扫描。

普通仓库执行一次git status,把工作区文件检查一遍,基本没什么感觉。文件数量上去后,这件事就开始不对劲了。你只是想知道改了哪几个文件,工具却可能要碰一遍整个目录树。

Facebook 当时没有接受这种扫描方式,而是接入 Watchman,持续记录文件系统变化。执行状态检查时,不再从头翻整个仓库,而是直接问:“从上次开始,哪些文件动过?”

官方当时给出的结果是,接入 Watchman 后,Mercurial 的status在其仓库中比 Git 快了五倍以上。

把这个思路缩小到 Java,大概是下面这样。注意,这不是 Facebook 的源码,只是一个简化后的变更索引:

public final class RepoChangeIndex implements AutoCloseable {

  private final WatchService watcher;

  private final Set<Path> dirtyFiles = ConcurrentHashMap.newKeySet();

  private volatile boolean needFullCheck;

  public RepoChangeIndex() throws IOException {

      this.watcher = FileSystems.getDefault().newWatchService();

  }

  public void watchDirectory(Path directory) throws IOException {

      directory.register(

              watcher,

              StandardWatchEventKinds.ENTRY_CREATE,

              StandardWatchEventKinds.ENTRY_MODIFY,

              StandardWatchEventKinds.ENTRY_DELETE

      );

  }

  public void collectChanges() {

      WatchKey key;

      while ((key = watcher.poll()) != null) {

          Path directory = (Path) key.watchable();

          for (WatchEvent<?> event : key.pollEvents()) {

              if (event.kind() == StandardWatchEventKinds.OVERFLOW) {

                  needFullCheck = true;

                  continue;

              }

              Path changed = directory.resolve((Path) event.context());

              dirtyFiles.add(changed.normalize());

          }

          if (!key.reset()) {

              needFullCheck = true;

          }

      }

  }

  public Set<Path> snapshot() {

      if (needFullCheck) {

          throw new IllegalStateException("文件事件可能丢失,需要重新扫描仓库");

      }

      return Set.copyOf(dirtyFiles);

  }

  @Override

  public void close() throws IOException {

      watcher.close();

  }

}

我写这种监听代码,第一件事不是处理MODIFY,而是盯住OVERFLOW。

文件事件一旦丢了,还继续相信缓存结果,后面提交漏文件才是真麻烦。性能优化可以不完整,数据不能假装完整。Facebook 做 Watchman 集成时也用了长时间对照测试,把监听结果和真实文件系统扫描结果进行比较,而不是接上监听就直接上线。

第二个问题是历史数据太大。

Git 和 Mercurial 都是分布式版本控制系统,传统做法是把大量仓库历史拉到本地。仓库小的时候,这叫离线可用;仓库膨胀以后,这里面全是磁盘、网络和等待时间。

Facebook 后来做的方案更像“按需取数据”:本地保留必要的提交信息,文件历史和内容用到时再从服务端获取并缓存。这样开发人员不用为了改几个 Java 文件,先把多年积累的全部内容搬到电脑上。

再往后,连 Mercurial 服务端的提交吞吐也碰到了上限。

2016 年前后,Facebook 单体仓库的代码变更速度开始逼近 Mercurial 能承受的最大提交速率。源码管理团队没有继续给旧服务打补丁,而是用 Rust 重写了后端 Mononoke。Mononoke 自2019年起成为其单体仓库的生产后端。

这时候已经很难说它仍然只是“改造版 Mercurial”了。

新的存储格式、新的网络协议、新的算法,再加上虚拟文件系统 EdenFS,最后形成了 Sapling。Meta 表示,这套系统可以支撑包含数千万文件、数千万提交以及数千万分支的内部仓库;现在 Sapling 仍在支撑 Meta 的大型单体仓库,同时客户端也能操作普通 Git 仓库。

所以,Facebook 真正放弃的不是 Git 的某个命令,而是“每个开发者都持有完整仓库、客户端自己扫描全部文件、通用服务端处理所有规模”这套默认假设。

这事也别学歪了。

普通公司几十个服务、几百名开发人员,Git 的生态、托管平台和团队熟悉度,通常比那点性能差异值钱得多。仓库没有慢到影响提交,没有大量磁盘和网络消耗,也没有服务端吞吐问题,自己造一套源码管理系统,多半是在给团队增加新故障。

Facebook 不是觉得 Git 不够高级。

是仓库大到一定程度以后,连版本控制工具也得按分布式存储系统来做了。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/Oguo5s6vOhxnX-_9etp3nhGg0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。
领券