Help us learn about your current experience with the documentation. Take the survey.

Gitaly 开发指南

Gitaly 是 GitLab Rails、Workhorse 和 GitLab Shell 使用的高层 Git RPC 服务。

深入解析

2019年5月,Bob Van Landuyt 在 Gitaly 项目 上举办了一次深入解析(仅限 GitLab 团队成员:https://gitlab.com/gitlab-org/create-stage/-/issues/1)。 内容包括作为 Ruby 开发者如何贡献代码,以及与未来可能在这个代码库中工作的人员分享领域特定知识。

你可以在 YouTube 上找到 录像, 以及 Google 幻灯片PDF

本次深入解析的内容在 GitLab 11.11 版本时是准确的,虽然具体细节可能有所变化,但仍可作为良好的入门指南。

新手指南

首先阅读 Gitaly 仓库的 Gitaly 贡献新手指南。 它描述了如何设置 Gitaly、Gitaly 的各种组件及其功能,以及如何运行其测试套件。

开发新的 Git 功能

要读写 Git 数据,必须向 Gitaly 发送请求。这意味着如果你正在开发一个需要 lib/gitlab/git 中尚未可用数据的新功能, 就必须对 Gitaly 进行修改。

gitlab 仓库中,任何地方都不应出现通过磁盘访问直接操作 Git 仓库的新代码。 任何需要直接访问 Git 仓库的功能都必须在 Gitaly 中实现,并通过 RPC 暴露。

如果你先在 GitLab 中修改代码以使用新功能,并在单独的 merge request 中提交,通常更容易在 Gitaly 中开发新功能。 这样可以在 Gitaly 的 merge request 合并后立即测试你的更改。

  • 有关使用修改后的 Gitaly 版本运行 GitLab 测试的说明,请参见下文
  • 在 GDK 中运行 gdk install 并使用 gdk restart 重启 GDK,以使用本地修改的 Gitaly 版本进行开发

Gitaly 相关的测试失败

如果你的测试套件因 Gitaly 问题而失败,首先尝试运行:

rm -rf tmp/tests/gitaly

在 RSpec 测试期间,Gitaly 实例会将日志写入 gitlab/log/gitaly-test.log

TooManyInvocationsError 错误

在开发和测试过程中,你可能会遇到 Gitlab::GitalyClient::TooManyInvocationsError 失败。 GitalyClient 试图通过在单个 Rails 请求或 Sidekiq 执行中调用 Gitaly 超过 30 次时抛出此错误, 来阻止潜在的 n+1 问题。

作为临时措施,导出 GITALY_DISABLE_REQUEST_LIMITS=1 来抑制该错误。这会在你的开发环境中禁用 n+1 检测。

在 GitLab CE 或 EE 仓库中创建 issue 来报告问题。包含标签 ~Gitaly ~performance ~“technical debt”。 确保 issue 包含 TooManyInvocationsError 的完整堆栈跟踪和错误信息。 如果可能,还应包含任何已知的失败测试。

隔离 n+1 问题的源头,这通常是一个循环,导致为数组中的每个元素都调用 Gitaly。 如果你无法隔离问题,请联系 Gitaly 团队 成员寻求帮助。

找到源头后,将其包装在 allow_n_plus_1_calls 块中,如下所示:

# n+1: 链接到 n+1 问题
Gitlab::GitalyClient.allow_n_plus_1_calls do
  # 原始代码
  commits.each { |commit| ... }
end

代码包装在此块后,此代码路径将被排除在 n+1 检测之外。

请求数量

提交和其他 Git 数据现在通过 Gitaly 获取。这些获取操作可以像数据库一样批量处理。 这提高了客户端、Gitaly 本身以及用户的性能。为了保持性能稳定并防止性能退化, 可以统计 Gitaly 调用次数,并在测试中验证该次数。这需要设置 :request_store 标志。

describe 'Gitaly 请求数量测试' do
  context '当请求存储被激活时', :request_store do
    it '正确统计发出的 gitaly 请求数量' do
      expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(10)
    end
  end
end

使用本地修改的 Gitaly 版本运行测试

通常,GitLab CE/EE 测试使用 tmp/tests/gitaly 中的本地 Gitaly 克隆, 该版本固定在 GITALY_SERVER_VERSION 指定的版本。 GITALY_SERVER_VERSION 文件也支持分支和 SHA,以使用 仓库 中的自定义提交。

随着 Gitaly 自动部署的引入,GITALY_SERVER_VERSION 的格式与 Omnibus 语法保持一致。 它不再支持 =revision,而是将文件内容作为 Git 引用(分支或 SHA)进行评估。 只有当它匹配语义版本时,才会添加 v 前缀。

如果你想使用修改后的 Gitaly 版本在本地运行测试, 可以将 tmp/tests/gitaly 替换为符号链接。这要快得多, 因为它避免了每次运行 rspec 时重新安装 Gitaly。

确保此目录包含 config.tomlpraefect.config.toml 文件。 你可以从 config.toml.example 复制 config.toml, 从 config.praefect.toml.example 复制 praefect.config.toml。 复制后,请务必编辑它们,使所有内容指向正确的路径。

rm -rf tmp/tests/gitaly
ln -s /path/to/gitaly tmp/tests/gitaly

在运行测试之前,请确保在本地 Gitaly 目录中运行 make。 否则,Gitaly 将无法启动。

如果在两次测试运行之间修改了本地 Gitaly,需要手动再次运行 make

CI 测试不使用你本地修改的 Gitaly 版本。 要在 CI 中使用自定义 Gitaly 版本,必须按照本节开头所述更新 GITALY_SERVER_VERSION

要使用不同的 Gitaly 仓库(例如,如果你的更改在 fork 中),可以在运行测试时指定 GITALY_REPO_URL 环境变量:

GITALY_REPO_URL=https://gitlab.com/nick.thomas/gitaly bundle exec rspec spec/lib/gitlab/git/repository_spec.rb

如果你的 Gitaly fork 是私有的,可以生成 部署令牌 并在 URL 中指定:

GITALY_REPO_URL=https://gitlab+deploy-token-1000:[email protected]/nick.thomas/gitaly bundle exec rspec spec/lib/gitlab/git/repository_spec.rb

要在 CI/CD 中使用自定义 Gitaly 仓库,例如希望你的 GitLab fork 始终使用你自己的 Gitaly fork, 请将 GITALY_REPO_URL 设置为 CI/CD 变量

使用本地修改的 Gitaly RPC 客户端

如果你正在修改 RPC 客户端,例如添加新端点或为现有端点添加新参数, 请遵循 Gitaly protobuf 规范 指南。然后:

  1. 在 Gitaly 的 tools/protogem 目录中运行 bundle install

  2. 从 Gitaly 的根目录构建 RPC 客户端 gem:

    BUILD_GEM_OPTIONS=--skip-verify-tag make build-proto-gem
  3. 在 Gitaly 的 _build 目录中,解压新创建的 .gem 文件并创建 gemspec

    gem unpack gitaly.gem &&
    gem spec gitaly.gem > gitaly/gitaly.gemspec
  4. 将 Rails 的 Gemfile 中的 gitaly 行更改为:

    gem 'gitaly', path: '../gitaly/_build'
  5. 运行 bundle install 以使用修改后的 RPC 客户端。

每次要尝试新更改时,请重新运行步骤 2-5。


返回开发文档

将 RPC 包装在功能标志中

以下是在 Gitaly 中将新功能置于功能标志后面的步骤。

Gitaly

  1. 创建一个包作用域的标志名称:

    var findAllTagsFeatureFlag = "go-find-all-tags"
  2. 使用 featureflag 包在代码中创建开关:

    if featureflag.IsEnabled(ctx, findAllTagsFeatureFlag) {
      // go 实现
    } else {
      // ruby 实现
    }
  3. 创建 Prometheus 指标:

    var findAllTagsRequests = prometheus.NewCounterVec(
      prometheus.CounterOpts{
        Name: "gitaly_find_all_tags_requests_total",
        Help: "FindAllTags 的 go 与 ruby 实现计数器",
      },
      []string{"implementation"},
    )
    
    func init() {
      prometheus.Register(findAllTagsRequests)
    }
    
    if featureflag.IsEnabled(ctx, findAllTagsFeatureFlag) {
      findAllTagsRequests.WithLabelValues("go").Inc()
      // go 实现
    } else {
      findAllTagsRequests.WithLabelValues("ruby").Inc()
      // ruby 实现
    }
  4. 在测试中设置标头:

    import (
      "google.golang.org/grpc/metadata"
    
      "gitlab.com/gitlab-org/gitaly/internal/featureflag"
    )
    
    //...
    
    md := metadata.New(map[string]string{featureflag.HeaderKey(findAllTagsFeatureFlag): "true"})
    ctx = metadata.NewOutgoingContext(context.Background(), md)
    
    c, err = client.FindAllTags(ctx, rpcRequest)
    require.NoError(t, err)

GitLab Rails

在 Rails 控制台中通过设置功能标志进行测试:

Feature.enable('gitaly_go_find_all_tags')

注意标志名称和 Rails 控制台中使用的名称之间的区别。 它们有所不同(破折号替换为下划线,名称前缀已更改)。 确保所有标志都以 gitaly_ 为前缀。

如果未在 GitLab 中设置,功能标志从控制台读取为 false,Gitaly 使用其默认值。 默认值取决于 GitLab 版本。

使用 GDK 进行测试

为确保标志设置正确并传递到 Gitaly,你可以使用 GDK 检查集成:

  1. 标志状态必须是可观察的。要检查它,你必须通过获取 Prometheus 指标来启用它:

    1. 进入 GDK 根目录。
    2. 确保你签出了 Gitaly 的正确分支。
    3. 使用 make gitaly-setup 重新编译并使用 gdk restart gitaly 重启服务。
    4. 确保你的设置正在运行:gdk status | grep praefect
    5. 检查使用的配置文件:cat ./services/praefect/run | grep praefect -config 标志的值
    6. 在配置文件中取消注释 prometheus_listen_addr 并运行 gdk restart gitaly
  2. 确保标志尚未启用:

    1. 执行任何触发你更改所需的操作,如创建项目、提交提交或查看历史。

    2. 检查当前指标列表是否包含该功能标志的新计数器:

      curl --silent "http://localhost:9236/metrics" | grep go_find_all_tags
  3. 在你观察到新功能标志的指标并看到其递增后, 可以启用新功能:

    1. 进入 GDK 根目录。

    2. 启动 Rails 控制台:

      bundle install && bundle exec rails console
    3. 检查功能标志列表:

      Feature::Gitaly.server_feature_flags

      它应该被禁用 "gitaly-feature-go-find-all-tags"=>"false"

    4. 启用它:

      Feature.enable('gitaly_go_find_all_tags')
    5. 退出 Rails 控制台并执行任何触发你更改所需的操作,如创建项目、提交提交或查看历史。

    6. 通过观察其指标验证功能已启用:

      curl --silent "http://localhost:9236/metrics" | grep go_find_all_tags

在测试中使用 Praefect

默认情况下,测试中的 Praefect 使用内存选举策略。该策略已被弃用,不再在生产环境中使用。 它主要用于单元测试目的。

更现代的选举策略需要与 PostgreSQL 数据库建立连接。 在运行测试时,此行为默认被禁用,但你可以通过在环境中设置 GITALY_PRAEFECT_WITH_DB=1 来启用它。

这需要你运行 PostgreSQL 并已创建数据库。 当你使用 GDK 时,可以按以下方式设置:

  1. 启动数据库:gdk start db
  2. 从 GDK 加载环境:eval $(cd ../gitaly && gdk env)
  3. 创建数据库:createdb --encoding=UTF8 --locale=C --echo praefect_test

Gitaly 使用的 Git 引用

Gitaly 使用许多 Git 引用 (refs) 来为 GitLab 提供 Git 服务。

标准 Git 引用

这些标准 Git 引用在任何 Git 仓库中被 GitLab(通过 Gitaly)使用:

  • refs/heads/。用于分支。参见 git branch 文档。
  • refs/tags/。用于标签。参见 git tag 文档。

GitLab 特定引用

没有 Git 引用指向的提交链在 housekeeping 运行时可以被删除。 对于必须保持对 GitLab 进程或 UI 可访问的提交链,GitLab 会创建 GitLab 特定的引用指向这些提交链, 以阻止 housekeeping 删除它们。

这些提交链无论用户对仓库执行什么操作(如删除分支或强制推送)都会保留。

现有的 GitLab 特定引用

这些 GitLab 特定引用仅由 GitLab(通过 Gitaly)使用:

  • refs/keep-around/<object-id>。指向在 UI 中用于合并请求、管道和注释的提交。 由于 keep-around 引用没有生命周期,不要将它们用于任何新功能。
  • refs/merge-requests/<merge-request-iid>/合并 将两个历史合并在一起。 此引用命名空间跟踪以下引用下的合并信息:
    • head。合并请求的当前 HEAD
    • merge。合并请求的提交。每个合并请求在 refs/keep-around 下创建一个提交对象。
    • 如果启用了合并列车train。合并列车的提交。
  • refs/pipelines/<pipeline-iid>。管道的引用。临时用于存储管道提交对象 ID。
  • refs/environments/<environment-sslug>。部署到环境的提交的引用。

创建新的 GitLab 特定引用

GitLab 特定引用有助于确保 GitLab UI 继续正常运行,但必须仔细管理,否则可能导致创建它们的 Git 仓库性能下降。

创建新的 GitLab 特定引用时:

  1. 确保 Gitaly 将新引用视为隐藏引用。隐藏引用在用户拉取或获取时不可访问。 使 GitLab 特定引用隐藏可以防止它们影响最终用户的 Git 性能。
  2. 确保定义了生命周期。类似于 PostgreSQL,Git 仓库无法处理无限量的数据。 添加大量引用最终会导致性能问题。因此,任何创建的 GitLab 特定引用在可能时也应被删除。
  3. 确保引用按其支持的功能进行命名空间划分。为了诊断性能问题,引用必须与 GitLab 中的特定功能或模型相关联。

测试 GitLab 特定引用的更改

更改 GitLab 特定引用的创建时间可能会导致 GitLab UI 或进程在更改部署很长时间后失败, 因为孤立的 Git 对象在被删除前有一个宽限期。

要测试 GitLab 特定引用的更改:

  1. 在文件系统中定位测试仓库

  2. 在服务器端 Gitaly 仓库上强制运行 git gc

    git gc --prune=now