重建百万级上传数据耗时太长?试试添加这个模板!

parallel-fs-ops.template.yml (2.8 KB)

让我们来谈谈一个扩展性问题,当 Discourse 站点积累了大量上传库时,这个问题会变得非常棘手。

过去,chown 命令在巨大的 uploads 目录上运行需要几分钟,现在只需几秒钟!

背景

Discourse 的重建过程可以执行递归操作,例如:

chown -R ...
chmod -R ...

更具体地说,是 templates/web.template.yml 中的这一行:

- chown -R discourse:www-data /shared/log/rails /shared/uploads /shared/backups /shared/tmp

这些命令以串行方式遍历文件系统,逐个处理。

对于小型安装来说,这完全合理。但是,当 shared/uploads 包含数十万甚至数百万个文件时,递归的所有权和权限操作可能会主导整个部署过程。即使 CPU、存储和网络容量充足,但一个进程每次只处理一个 inode,遍历整棵树。

对于上传量大的社区,结果可能是:

  • 极其漫长的重建时间
  • 更长的维护窗口
  • 部署和安全更新延迟
  • 快速或分布式存储利用率低
  • 在处理巨大的文件树时,部署看起来像卡住了
  • 在 NFS、JuiceFS、CephFS 和其他远程文件系统上性能尤其糟糕

令人沮丧的是,这些文件中的许多是独立的。它们的权限可以并发处理。

解决方案:并行文件系统操作模板

我创建了一个 pups 模板,它透明地将递归的 chmodchown 操作替换为并行的 findxargs 管道。

每当拦截到递归操作时,包装器都会宣布自己:

echo "[parallel-fs-ops] chmod -R override active: $*" >&2

以及:

echo "[parallel-fs-ops] chown -R override active: $*" >&2

那个带括号的缀前缀使优化在部署日志中易于识别。

部署看起来像什么

在部署的早期阶段,模板会确认哪些二进制文件将处理后续的文件系统操作:

[parallel-fs-ops] chmod -> /usr/local/bin/chmod
[parallel-fs-ops] chown -> /usr/local/bin/chown

当上游模板稍后执行递归权限更改时,部署输出将包含类似以下的一行:

[parallel-fs-ops] chmod -R override active: -R 0755 /var/www/discourse/public

递归所有权更改会产生:

[parallel-fs-ops] chown -R override active: -R discourse:www-data /shared/log/rails /shared/uploads /shared/backups /shared/tmp

对于上传量大的安装,您可能会看到类似以下内容:

[parallel-fs-ops] chown -R override active: -R discourse:www-data /shared/log/rails /shared/uploads /shared/backups /shared/tmp

确切的路径和参数取决于所使用的模板,但重要的是可见的标记:

[parallel-fs-ops]

如果没有该模板,部署可能会在递归文件系统操作期间长时间暂停。有了该模板,日志会告诉您:

  1. 包装器已正确安装。
  2. 检测到递归操作。
  3. 并行实现已激活。
  4. 正在处理的原始参数是可见的。

这在故障排除期间特别有价值,因为它可以区分缓慢的并行文件系统遍历和挂起的构建。

操作完成后,部署继续其正常的 pups 输出。包装器本身不会为每个文件打印一行,因此即使包含数百万个上传的树也不会淹没部署日志。

模板

run:
  - file:
      path: /usr/local/bin/chmod
      chmod: "+x"
      contents: |
        #!/bin/bash
        if [[ "$*" =~ (^|[[:space:]])-R([[:space:]]|$) ]]; then
          echo "[parallel-fs-ops] chmod -R override active: $*" >&2
          args=()
          for arg in "$@"; do
            [[ "$arg" != "-R" ]] && args+=("$arg")
          done
          mode="${args[0]}"
          targets=("${args[@]:1}")
          [[ ${#targets[@]} -eq 0 ]] && targets=(".")
          find "${targets[@]}" -print0 |
            xargs -0 -n 32 -P 128 /bin/chmod "$mode"
        else
          exec /bin/chmod "$@"
        fi

  - file:
      path: /usr/local/bin/chown
      chmod: "+x"
      contents: |
        #!/bin/bash
        if [[ "$*" =~ (^|[[:space:]])-R([[:space:]]|$) ]]; then
          echo "[parallel-fs-ops] chown -R override active: $*" >&2
          args=()
          for arg in "$@"; do
            [[ "$arg" != "-R" ]] && args+=("$arg")
          done
          owner="${args[0]}"
          targets=("${args[@]:1}")
          [[ ${#targets[@]} -eq 0 ]] && targets=(".")
          find "${targets[@]}" -print0 |
            xargs -0 -n 32 -P 128 /bin/chown "$owner"
        else
          exec /bin/chown "$@"
        fi

  - exec:
      cmd: |
        echo "[parallel-fs-ops] chmod -> $(command -v chmod)"
        echo "[parallel-fs-ops] chown -> $(command -v chown)"

该模板在 /usr/local/bin 中安装包装器,这通常出现在 PATH/bin 之前。

当请求正常的非递归操作时,包装器直接委托给标准实用工具:

exec /bin/chmod "$@"

当存在 -R 时,它会移除递归标志,使用空字符分隔符安全地枚举目标,并发处理批次:

find "${targets[@]}" -print0 |
  xargs -0 -n 32 -P 128 /bin/chmod "$mode"

即使 pups 通过 /bin/sh 调用命令,这也行得通。当可执行文件启动时,包装器的 Bash shebang 会被尊重,即使调用 shell 是 Dash。

为什么当上传量大时这最重要

上传量大的社区正是部署行为需要优雅扩展的地方。

长期运行的论坛可能包含:

  • 嵌入在多年帖子中的图片
  • 头像和个人资料背景
  • 原始和优化后的图像变体
  • 视频和音频附件
  • 文档和存档
  • 安全上传
  • 插件管理的媒体
  • 多站点上传树

应用程序代码的数量可能相对稳定,而上传的文件系统对象数量继续增长。文件系统遍历——而不是编译或容器创建——最终可能成为主要的部署成本。

这是一个不寻常的扩展问题:社区越成功、内容越丰富,常规运维工作的成本可能越高。

为什么需要模板

更改 .bashrc 或设置 BASH_ENV 并不能可靠地解决这个问题。pups 通过 /bin/sh 执行 run 命令,而 Dash 既不加载 Bash 配置,也不理解 Bash 特定函数。

模板提供了一种可重复的方法来尽早安装包装器,以便后续的递归操作——包括来自上游模板的操作——都能通过并行实现解析:

templates:
  - "templates/postgres.template.yml"
  - "templates/redis.template.yml"
  - "templates/web.template.yml"
  - "containers/parallel-fs-ops.template.yml"

可配置选项

并行处理选项

模板使用:

find "${targets[@]}" -print0 |
  xargs -0 -n 32 -P 128 /bin/chmod "$mode"

相关的 xargs 参数是:

选项 目的
-0 读取由 find -print0 产生的空字符分隔的路径。这可以安全地处理包含空格、引号、制表符或换行符的文件名。
-n 32 向每个 chmodchown 调用传递最多 32 个路径。这是批次大小。
-P 128 允许最多 128 个 chmodchown 进程并发运行。这是并行度级别。

合起来,-n 32 -P 128 意味着最多可以有 128 个进程同时运行,每个进程处理最多 32 个路径的批次。因此,大约 4,096 个路径可能同时活跃地分布在命令批次中。

选择 -n

-n 控制分配给每个命令的工作量:

  • 较低的数值提供更细的工作分配,但会启动更多进程。
  • 较高的数值减少进程启动开销,但创建更大、分布更不均匀的批次。
  • -n 1 为每个路径运行一个 chmodchown 命令。
  • -n 32 是平衡批处理和并行的合理起点。
  • 非常大的数值可能会降低 -P 的效果,因为创建的总批次较少。

选择 -P

-P 控制可以同时运行多少个命令:

  • 较低的数值减少对 CPU 和文件系统的负载。
  • 较高的数值可以在快速或分布式存储上提高性能。
  • 过度的并行可能会压倒磁盘,饱和元数据服务器,或使性能变差。
  • -P 1 实际上是串行执行。
  • -P 8-P 16 是保守的起点。
  • -P 32 可能适合快速 SSD 支持的存储。
  • -P 128 仅应在文件系统和主机能够维持该并发度时使用。

最佳值取决于文件系统延迟、元数据性能、CPU 容量和文件数量。理想情况下,两者都应该可配置,并针对特定安装进行基准测试。

过多的并行可能会压倒文件系统,饱和元数据服务器,或降低部署性能。因此,批次大小和并发度应该可配置。

这个模板是一个实用的变通方案,但更大的提案更广泛:

Discourse 能否官方支持在部署期间对大型递归文件系统操作进行可配置的并行处理?

上游实现可以:

  • 仅对已知的巨大目录树进行并行化
  • 避免不必要地遍历未更改的上传树
  • 使并发度可配置
  • 检测本地与网络支持的文件系统
  • 保留完整的 chmodchown 参数语义
  • 为非常大的树发出定期进度
  • 记录计时,以便管理员可以识别部署瓶颈

重要注意事项

上面的包装器专注于我们的构建过程使用的递归命令形式。它不是所有可能的 chmodchown 选项组合的完整重新实现。

在生产使用前,应针对站点模板生成的确切命令进行测试。操作员应从保守的并行度开始,并测量其对存储的影响。

但潜在的问题是真实的:当社区积累了巨大的上传树时,串行递归元数据操作无法很好地扩展。

祝好运,我感谢任何评论或建议(即使我可能重复了别人的工作,我也很感激指向那里的指针)!

干杯!

2 个赞