在 AI/机器学习项目中,我们经常需要把模型权重、数据集、图片或音频文件纳入版本控制。直接使用 git add 添加一个几 GB 的 .safetensors 文件,很快就会发现仓库越来越大、clone 越来越慢,甚至被托管平台拒绝推送。Git LFS(Large File Storage)正是为了解决这个问题。本文介绍它的工作原理、日常用法、迁移、CI、托管、自托管和替代方案。

1. 为什么需要 Git LFS

1.1 Git 处理大文件的困难

Git 是一种内容寻址的快照系统:每个文件的每个版本都会作为 blob 对象存入 .git/objects,并由内容哈希索引。这种设计适合源代码,但大型二进制文件会带来几个问题:

  • 历史会保留旧版本。 文件一旦提交就会留在历史中,之后删除文件也不会从现有 clone 中移除这些对象。一个 2 GB 的模型有十个版本,未压缩时可能占约 20 GB。
  • 差异压缩通常效果有限。 Git 会尝试在 packfile 中对相近 blob 做 delta 压缩。文本文件效果很好,但图片、视频和模型权重等压缩二进制格式通常会整体变化,难以有效压缩。
  • 普通 clone 会下载历史。 默认的 git clone 会获取可达历史及其对象,包括当前可能用不到的旧版本。
  • 托管平台有限制。 许多平台限制单个文件大小。例如 GitHub 会阻止普通 Git 文件超过 100 MiB。

1.2 Git LFS 的核心思路

Git LFS 在 Git 仓库中保存一个很小的文本指针文件,把真实文件内容存放在 LFS 服务器上。

LFS 服务器不是 Git 内置的。Git 负责版本控制,另一个实现 Git LFS API 的服务负责保存 LFS 对象。最常见的方式是使用托管平台内置的 LFS 服务,例如 GitHub、GitLab、Gitea/Forgejo、Bitbucket 和 Hugging Face;它们与仓库共用地址和访问控制。客户端通常能根据 Git remote URL 推导出 LFS endpoint(见第 2.3 节)。你也可以选择内置 LFS 的自托管 Git 服务,或另行部署独立 LFS 服务器。

只通过 SSH 访问的裸仓库,例如 ssh://server/srv/repo.git,只提供 Git,没有 LFS 服务器。除非另行提供 LFS endpoint,否则推送 LFS 文件会失败。

  • 添加文件时,Git LFS 把真实内容存入本地 LFS 缓存,并向 Git 提供指针;推送时再上传 LFS 对象。
  • 检出指针时,Git LFS 获取对应内容并写入工作区。是否立即下载取决于客户端设置;也可以先跳过,再运行 git lfs pull。

因此,Git 历史保存的是小型指针,普通 Git 对象的下载量和仓库大小会减少。LFS 数据仍单独存储,也有自己的传输和存储成本。

2. 工作原理

2.1 指针文件格式

由 LFS 管理的文件,在 Git 对象库中实际存放的是这样的文本文件:

version https://git-lfs.github.com/spec/v1
oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
size 12345
字段含义
version指针规范版本,目前为 spec/v1。
oid原始文件内容的 SHA-256 哈希,也是 LFS 对象的唯一标识。
size原始文件的字节数。

由于对象 ID 是内容哈希,完全相同的文件即使出现在不同路径和提交中也只需存一份。文件哪怕只改动一个字节,也会产生一个全新的完整对象;这对配额和替代方案很重要。

可以查看 Git 中保存的指针:

# 查看 HEAD 中该路径在 Git 对象库里的实际内容(即指针)
git cat-file -p HEAD:models/model.safetensors

# 或用 LFS 命令解析指针
git lfs pointer --file=models/model.safetensors

2.2 Clean 与 Smudge Filter

Git LFS 没有修改 Git 本身,而是使用 Git 内置的 filter 机制,在工作区和对象库之间转换文件。filter driver 有两个方向:

方向触发时机作用
cleangit add:工作区到暂存区/对象库把工作区内容转换为存入 Git 的形式。
smudgecheckout:对象库到工作区把 Git 中保存的内容转换为工作区需要的形式。

两个命令都从标准输入读取文件内容,再把转换结果写入标准输出。Git 负责调用它们,但不关心具体转换方式。

设置 filter 需要两部分:

  1. 在 Git 配置(.git/config 或 ~/.gitconfig)中定义 filter 命令。
  2. 在 .gitattributes 中指定哪些路径使用该 filter。

下面是一个与 LFS 无关的例子:把提交到 Git 的配置文件中的密码替换掉,但在本地仍保留原文:

# .git/config
[filter "hidepass"]
    clean = sed -e 's/^password = .*/password = REDACTED/'
    smudge = cat
# .gitattributes
config.ini filter=hidepass

filter 命令不会随仓库分发:.gitattributes 会提交,但 .git/config 不会。如果协作者没有配置对应的 filter,Git 可能会把文件原样加入普通 Git 历史,而不是存成指针。因此需要在使用 LFS 的用户账户上安装 Git LFS 并运行 git lfs install。

Git 比较和存储的是 clean filter 的输出。工作区里的文件内容本身不是 Git 对象库记录的内容。

Git LFS 如何使用 filter

Git LFS 用 clean 把大文件换成指针,用 smudge 把指针换回原文件。运行 git lfs install 后,全局 Git 配置中会有类似设置:

[filter "lfs"]
    clean = git-lfs clean -- %f
    smudge = git-lfs smudge -- %f
    process = git-lfs filter-process
    required = true

仓库的 .gitattributes 会选择对应路径:

*.safetensors filter=lfs diff=lfs merge=lfs -text
  • clean: 执行 git add 时,git-lfs clean 计算 SHA-256,将原始文件存入 .git/lfs/objects/<oid前两位>/<接下来两位>/<oid>,然后输出供 Git 存为 blob 的指针。
  • smudge: checkout 时,git-lfs smudge 读取指针,先检查本地缓存;没有对象时就下载,再把原始内容写进工作区。
  • process: 保持 filter 进程运行,避免 Git 每处理一个文件就启动一次 git-lfs。
  • required = true: 本机已配置的 filter 执行失败时,Git 应让操作失败。它无法替未安装 Git LFS 的协作者配置 filter。
  • diff=lfs: 使用 LFS 的 diff 行为,不尝试对大型二进制内容做普通 diff。
  • merge=lfs: 在适用时使用 LFS merge driver。
  • -text: 关闭换行符转换,避免损坏二进制文件。

git lfs install 还会安装仓库 hook,其中最重要的是 pre-push。Git 更新远端引用前,hook 会上传远端尚未拥有、且这些提交所引用的 LFS 对象。在已有 clone 中运行此命令可以安装或更新对应 hook。

2.3 传输流程与 Batch API

LFS 传输使用独立于 Git 协议的 HTTP API。LFS endpoint 通常从 Git remote URL 推导,例如:

https://git.example.com/user/repo.git  →  https://git.example.com/user/repo.git/info/lfs

也可以在仓库的 .lfsconfig 中设置 lfs.url,例如让 Git 和 LFS 使用不同的主机。

核心 endpoint 是 Batch API。下载流程大致如下:

  Git 客户端                    LFS 服务器                 对象存储(S3 等)
      │                              │                              │
      │ 1. checkout 遇到指针          │                              │
      │──── POST /objects/batch ────▶│                              │
      │   {operation: download,      │                              │
      │    objects: [{oid,size}]}    │                              │
      │                              │ 2. 验证权限并查找对象         │
      │◀──── 200 actions.download ───│                              │
      │   {href, header, expires_in} │                              │
      │                              │                              │
      │──────── 3. GET href(可能是预签名 URL)────────────────────▶│
      │◀──────────────────────── 4. 文件内容 ─────────────────────│
      │                              │                              │
      │ 5. 校验 SHA-256,保存到本地 LFS 缓存和工作区                │

请求大致如下:

POST /user/repo.git/info/lfs/objects/batch
Accept: application/vnd.git-lfs+json
Content-Type: application/vnd.git-lfs+json

{
  "operation": "download",
  "transfers": ["basic"],
  "ref": { "name": "refs/heads/main" },
  "objects": [
    { "oid": "4d7a2146...", "size": 12345 }
  ]
}

服务器会为每个对象返回 action:

  • 下载 action 包含 href、需要附带的 header 和过期时间。
  • 上传 action 若对象已存在,可能不会返回;有些服务器还会提供 verify action,在上传后要求客户端确认。

这个设计可以批量处理请求,并将控制面与数据面分离:LFS 服务器负责授权和签发 URL,文件字节可以由客户端直接从对象存储或 CDN 传输。

补充说明:

  • transfers 用于协商传输方式。basic 使用普通 HTTP PUT/GET;自定义 transfer agent 可以实现分段上传到对象存储等方式。
  • 使用 SSH remote 时,客户端可以通过 SSH 执行 git-lfs-authenticate 获取 HTTP 凭据。Git LFS 3 也支持纯 SSH 传输,但服务器必须实现 git-lfs-transfer。
  • 排查问题时可运行 GIT_TRACE=1 GIT_CURL_VERBOSE=1 git lfs pull 查看请求详情。

3. 日常使用与迁移

3.1 安装并追踪文件

通过平台的软件包管理器安装 Git LFS:

# macOS
brew install git-lfs

# Debian / Ubuntu
sudo apt install git-lfs

# Fedora / RHEL
sudo dnf install git-lfs

# Windows:Git for Windows 已包含 Git LFS

然后初始化 Git 配置:

git lfs install

这个命令会配置 Git 使用 Git LFS,本身不会安装程序。它将 filter 写入用户级 Git 配置;如果在仓库内运行,还会安装或更新仓库 hook。每个用户账户只需运行一次来设置 filter;已有仓库需要安装或修复 hook 时,可在该仓库中再运行一次。

检查全局 filter 配置:

git config --global --get-regexp '^filter\.lfs\.'

常用选项:

命令作用范围
git lfs install当前用户(写入 ~/.gitconfig)。
git lfs install --system机器上的所有用户(系统级 Git 配置,通常需要管理员/root 权限)。
git lfs install --local当前仓库(写入 .git/config)。
git lfs install --skip-repo只写全局配置,不在当前仓库安装 hook。
git lfs uninstall移除对应配置和 hook。

在 CI 中,应在 runner 镜像或工作仓库中明确配置 Git LFS,不要依赖交互式用户的全局配置。

按扩展名或目录追踪文件:

# 给 pattern 加引号,避免 shell 展开 *
git lfs track "*.safetensors"
git lfs track "*.parquet"

# 追踪目录
git lfs track "datasets/**"

# 查看当前规则
git lfs track

git lfs track 会把规则写入 .gitattributes。应提交这个文件,让协作者知道哪些路径使用 LFS:

git add .gitattributes
git add models/model.safetensors
git commit -m "Add model weights via LFS"
git push

常用检查命令:

# 列出当前检出版本中的 LFS 文件(* 表示内容已下载;- 表示只有指针)
git lfs ls-files

# 显示对象大小
git lfs ls-files --size

# 检查暂存区的文件是否作为 LFS 指针存储
git lfs status

# 检查本地 LFS 对象和指针
git lfs fsck

# 显示 endpoint、缓存路径和环境信息
git lfs env

3.2 .gitattributes 常见陷阱

先写规则,再添加文件。 追踪规则只影响之后的 git add。如果文件已作为普通 blob 提交,之后添加规则不会转换旧历史。只想把当前版本转成 LFS 时:

git lfs track "*.bin"
git add --renormalize .
git commit -m "Move *.bin to LFS"

这不会缩小旧历史;要清理历史,请使用第 3.3 节的 git lfs migrate。

pattern 语法与 .gitignore 不完全相同。 .gitattributes 不支持取反(!)。以 / 开头的路径相对于该 .gitattributes 文件。要匹配任意深度的目录,使用 **:

# 只匹配根目录 data/ 下的 CSV
/data/*.csv filter=lfs diff=lfs merge=lfs -text

# 匹配任意目录层级中的 CSV
*.csv filter=lfs diff=lfs merge=lfs -text

# 匹配 assets/ 下的所有文件
assets/** filter=lfs diff=lfs merge=lfs -text

pattern 区分大小写:*.png 不会匹配 IMAGE.PNG。在 macOS 和 Windows 的大小写不敏感文件系统上容易忽略此问题;必要时同时添加两种大小写规则。

不要用 LFS 追踪配置、JSON 或示例 CSV 等小型文本文件。Git 原生处理这些文件更合适,也能保留可读 diff。.gitattributes 本身也不能交给 LFS 追踪;* 这种过宽规则可能匹配到它并破坏配置。

3.3 迁移已有仓库

如果大文件已经作为普通 Git blob 存在,git lfs migrate 可以重写历史并将它们转换为 LFS。

先检查仓库:

# 统计所有 refs 和历史中占空间最大的文件类型
git lfs migrate info --everything --top=10

然后导入选定文件并改写历史:

# 转换所有分支和标签中的这些 pattern
git lfs migrate import --everything --include="*.safetensors,*.bin,*.parquet"

# 或按大小筛选
git lfs migrate import --everything --above=50MB

改写会生成新的提交,因此受影响的 commit ID 都会改变。检查结果后,强制推送被改写的 refs:

git push --force --all
git push --force --tags

改写前务必注意:

  • 先备份,例如用 git clone --mirror 创建镜像副本。
  • 通知协作者。旧分支无法直接与改写后的历史合并;重新 clone 可能更简单。
  • 已打开的 PR/MR 可能需要重新创建。
  • 服务器上的旧对象可能仍占空间,直到托管平台执行垃圾回收或由客服处理。

不改写已有历史地导入:

# 新建一个提交,将符合已追踪规则的文件转换为 LFS
git lfs migrate import --no-rewrite path/to/large.bin

这种方式保留旧提交,但旧版本仍留在 Git 中。它适合团队中途采用 LFS、又不想打乱现有分支的情况。

从 LFS 导出: 如果停止使用 LFS,可以将匹配的指针转回普通 Git blob:

git lfs migrate export --everything --include="*.png"

4. 进阶技巧

4.1 选择性下载

如果仓库有很多 LFS 对象,而你只需要其中一部分,可以先跳过所有下载,再按需获取:

GIT_LFS_SKIP_SMUDGE=1 git clone https://git.example.com/user/models.git
cd models

# 工作区中是指针;只获取需要的文件
git lfs pull --include="llama-8b/*"

设置长期使用的包含和排除规则后,之后的 pull 和 checkout 也会遵循这些规则:

git config lfs.fetchinclude "configs/*,small-model/*"
git config lfs.fetchexclude "datasets/raw/*"

git lfs fetch --recent 还会获取近期分支和近期提交引用的对象。可用 lfs.fetchrecentrefsdays 和 lfs.fetchrecentcommitsdays 调整时间范围,便于提前缓存可能切换到的分支以供离线使用。

Git 原生 partial clone 也可以减少普通 Git 对象的下载量:

git clone --filter=blob:none https://git.example.com/user/repo.git

4.2 清理本地缓存

.git/lfs/objects 可能保留曾检出过的每个版本,时间久了会比工作区还大。git lfs prune 会删除本地不再需要的对象。它会保留当前检出版本、近期引用、stash、其他 worktree 和未推送的对象;具体行为取决于近期保留设置和 remote。

先预览清理结果,再确认远端确实有可达对象后删除本地副本:

# 预览将清理的内容
git lfs prune --dry-run --verbose

# 删除前向远端确认可达对象存在
git lfs prune --verify-remote

prune 只清理本地缓存,不会减少服务器储存量。服务器对象保留和垃圾回收方式取决于平台。改写历史后,旧对象也可能继续存在,直到平台执行垃圾回收;有些平台需要删除仓库或联系支持人员才能释放空间。

4.3 文件锁定

二进制文件不能像源代码那样合并。两个人同时修改同一个 Photoshop 文件或 Unity 场景时,其中一人的成果可能丢失。Git LFS 文件锁定可以帮助协调编辑:

# 标记匹配路径可锁定
git lfs track "*.psd" --lockable

lockable 属性会让检出的文件变为只读,获取锁后才能编辑:

git lfs lock design/banner.psd      # 获取锁;文件变为可写
git lfs locks                       # 查看锁
git lfs unlock design/banner.psd    # 修改并推送后解锁
git lfs unlock --force design/banner.psd   # 管理员强制解锁

锁定验证需要服务器支持 Locking API。按 Git LFS 文档配置 lfs.<url>.locksverify。启用后,pre-push hook 会询问服务器,确认即将推送的文件是否被其他人锁定。依赖这套流程前,先确认所用平台支持文件锁定。纯代码或 ML 项目通常用不到;游戏和设计资产则可能受益。

4.4 在 CI 中使用

CI 任务可能反复下载 LFS 对象。只在任务确实需要文件时启用 LFS checkout。GitHub Actions 的 actions/checkout 提供 lfs: true:

steps:
  - uses: actions/checkout@v4
    with:
      lfs: true

如果要缓存 .git/lfs,缓存键应反映任务需要的对象。只按分支或文件名生成的键可能会在文件更新后复用旧内容。确保 checkout 和缓存步骤顺序正确,并用 git lfs fsck 或工作流中的内容哈希检查确认恢复的对象。lint 和文档任务若不需要二进制文件,就跳过 LFS 下载。

其他建议:

  • 只下载任务需要的文件,例如 git lfs pull --include="tests/fixtures/*"。
  • 不要在不使用大文件的任务中 pull LFS 对象。
  • 自托管 runner 可以保留持久缓存,并用 lfs.storage 指向它。

5. 托管与自托管

5.1 托管平台

GitHub

GitHub 的单文件限制、储存和带宽额度取决于方案,也可能变化。下载流量计入仓库拥有者的额度;fork 流量也可能由上游仓库承担。请查看 GitHub 当前的 LFS 配额与限制,并计算 CI 重复下载的流量。

GitLab

GitLab 支持 LFS。自托管实例可以配置 LFS 对象储存,包括外部对象储存服务。GitLab.com 的储存和流量规则取决于方案与命名空间设置,请参阅当前的 LFS 对象储存文档。

Hugging Face Hub

Hub 现在使用 Xet 作为大型文件储存后端,同时维持 Git 与 Git LFS 指针的兼容路径。Git LFS 按完整文件识别对象;Xet 支持区块级去重。要使用 Xet 的传输优化,请按照 Hub 文档安装并配置支持 Xet 的客户端。不同客户端和版本的行为可能不同。

5.2 自托管 LFS 服务器

自托管可以避开第三方方案的特定配额,但储存、备份、带宽、可用性和运维仍然有成本。常见选择:

方案适用场景
Gitea / Forgejo轻量部署,内置 LFS,并可配置对象储存。
GitLab Self-Managed需要 GitLab 集成能力,并能够自行运维 LFS 储存。
独立 LFS 服务器Git 主机不提供 LFS,且愿意自行整合认证、储存和备份。

请按照实际部署版本查看 LFS 和对象储存文档。配置名称、存储后端和直连下载行为会因产品与版本而异;不要直接复制其他版本的 app.ini 示例。正式上线前测试 clone、checkout、push、访问控制和备份恢复,并确认客户端能够访问服务器生成的下载 URL。

5.3 反向代理注意事项

自托管 LFS 故障经常来自 Git 服务前面的反向代理。大型 HTTP 请求、请求体大小限制、超时、缓冲和上游连接设置都可能影响传输。应按实际服务器配置代理,并使用接近预期最大值的文件进行验证。

Nginx:

server {
    listen 443 ssl;
    server_name git.example.com;

    # 移除请求体大小限制(默认 1 MB)
    client_max_body_size 0;

    location / {
        proxy_pass http://127.0.0.1:3000;

        # 直接转发请求,不先缓冲整个请求体
        proxy_request_buffering off;
        proxy_buffering off;

        # 大文件上传可能需要较长时间
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;

        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

如果 Caddy 或其他代理配置了请求体大小限制,请确保 LFS 上传路径允许预期大小,也检查超时和缓冲行为。

使用 CDN 或 tunnel 时,确认服务方案对请求大小、连接时间和上传方式的限制。如果代理不适合传输大型对象,可按 Git 服务支持的方式配置独立 LFS endpoint 或对象储存直传。检查生成的 URL、认证和 TLS;不要把私有储存 endpoint 暴露给未授权的客户端。

排查线索:

  • 上传返回 413:请求体大小限制。
  • 上传中断并返回 504:超时设置。
  • 下载 URL 指向内网地址:检查公开 URL 或对象储存 endpoint 配置。
  • 认证失败:检查代理是否转发 Authorization header,以及 X-Forwarded-Proto 的协议是否一致。

6. 故障排查与替代方案

6.1 常见问题

clone 后文件内容是指针文本,而不是真实内容

本机可能没有安装或初始化 Git LFS,也可能是有意跳过了 checkout 下载:

git lfs install
git lfs pull

如果 clone 时设置了 GIT_LFS_SKIP_SMUDGE=1,运行 git lfs pull 获取内容。如果 pull 失败,检查凭据、LFS endpoint、对象是否已上传,以及平台提示的配额状态。

smudge filter lfs failed

checkout 下载 LFS 对象失败。依次检查:

  1. 网络和凭据:用 git lfs env 确认 endpoint,并运行 GIT_TRACE=1 git lfs pull 查看详情。
  2. 服务器上是否真的存在对象。有人可能提交了指针,却没有成功上传 LFS 文件,例如在未安装 Git LFS 的环境中操作,或绕过 pre-push。
  3. 是否用尽储存或带宽配额。

可以先跳过 smudge 完成 checkout,之后再重试:

GIT_LFS_SKIP_SMUDGE=1 git checkout <branch>

Encountered N file(s) that should have been pointers, but weren't

.gitattributes 规则匹配的文件以普通 blob 存在,而不是 LFS 指针。常见原因是协作者在未安装 Git LFS 的机器上提交。可以在当前版本中转换而不改写历史:

git add --renormalize .
git commit -m "Fix files that should be LFS pointers"

也可以改写历史,同时转换旧版本:

git lfs migrate import --everything --include="<pattern>"

大文件已作为普通 Git 对象提交并推送

之后再删除文件并不会缩小仓库。使用 git lfs migrate import 改写历史(见第 3.3 节)。如果尚未推送,可运行 git reset --soft HEAD~1,添加 LFS 追踪规则,再重新添加和提交文件。

推送很慢或看起来卡住

  • 运行 git lfs push --dry-run origin main 检查对象数量和大小。
  • 用 git config lfs.concurrenttransfers 8 调整并行传输数。
  • 自托管部署检查反向代理的缓冲配置(见第 5.3 节)。

常用诊断命令:

命令用途
git lfs env显示版本、endpoint、缓存路径和配置。
git lfs status显示暂存区和工作区的 LFS 状态。
git lfs ls-files --size列出 LFS 文件和大小。
git lfs fsck检查本地对象完整性。
git lfs logs last查看最近的错误日志。
GIT_TRACE=1 GIT_CURL_VERBOSE=1输出 HTTP 请求详情。

6.2 何时不该使用 LFS

Git LFS 适合数量和大小适中的二进制文件,并且这些文件需要与代码一起版本化,例如游戏素材、测试样本和小型模型。其他情况可以考虑:

方案适用场景特点
Git LFS需要与代码同步版本化的中等规模二进制文件生态成熟、平台支持广、日常使用透明。
git-annex大量文件、多种存储后端、分布式备份通过符号链接管理内容,并追踪文件所在位置;功能强大但学习成本较高。
DVCML 数据集和实验管线Git 只存 .dvc 元数据,数据放在 S3/GCS 等远端;还提供管线和实验追踪,不依赖 Git 托管平台。
Hugging Face Hub(Xet)公开或团队共享模型和数据集区块级去重适合频繁迭代的大文件,可减少储存和流量。
对象储存加清单文件TB 级且无需细粒度版本历史的数据仓库只保存 URL 和哈希,由脚本下载并校验。

选择时可参考以下原则:

  • 对频繁小幅修改的文件(例如连续训练产生的模型 checkpoint),LFS 每次都会存一个完整新对象。Xet 等区块去重方案或只保留选定版本可能更合适。
  • 数据达到数百 GB 时,托管平台的 LFS 费用可能高于直接使用对象储存。可以比较 DVC 或对象储存加清单文件。
  • 需要数据来源追踪和实验管理时,DVC 不只是储存工具。
  • 对无需版本历史的文件(例如 build 产物),不要用 LFS;应使用 Release 或软件包仓库。

参考资料

总结

Git LFS 使用 Git 的 filter 机制把大文件替换为指针,再通过独立的 HTTP API 传输文件内容。理解 clean/smudge filter 和 Batch API 有助于解释大多数使用与部署问题。先添加 .gitattributes 规则再添加文件,在 CI 中限制下载,并留意平台配额。若选择自托管,还要考虑储存、备份、带宽、反向代理和访问控制:自托管改变了管理容量和成本的方式,但不会消除这些成本。