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.toml 和 praefect.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:token-here@gitlab.com/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 规范 指南。然后:
-
在 Gitaly 的
tools/protogem目录中运行bundle install。 -
从 Gitaly 的根目录构建 RPC 客户端 gem:
BUILD_GEM_OPTIONS=--skip-verify-tag make build-proto-gem -
在 Gitaly 的
_build目录中,解压新创建的.gem文件并创建gemspec:gem unpack gitaly.gem && gem spec gitaly.gem > gitaly/gitaly.gemspec -
将 Rails 的
Gemfile中的gitaly行更改为:gem 'gitaly', path: '../gitaly/_build' -
运行
bundle install以使用修改后的 RPC 客户端。
每次要尝试新更改时,请重新运行步骤 2-5。
将 RPC 包装在功能标志中
以下是在 Gitaly 中将新功能置于功能标志后面的步骤。
Gitaly
-
创建一个包作用域的标志名称:
var findAllTagsFeatureFlag = "go-find-all-tags" -
使用
featureflag包在代码中创建开关:if featureflag.IsEnabled(ctx, findAllTagsFeatureFlag) { // go 实现 } else { // ruby 实现 } -
创建 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 实现 } -
在测试中设置标头:
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 检查集成:
-
标志状态必须是可观察的。要检查它,你必须通过获取 Prometheus 指标来启用它:
- 进入 GDK 根目录。
- 确保你签出了 Gitaly 的正确分支。
- 使用
make gitaly-setup重新编译并使用gdk restart gitaly重启服务。 - 确保你的设置正在运行:
gdk status | grep praefect。 - 检查使用的配置文件:
cat ./services/praefect/run | grep praefect-config标志的值 - 在配置文件中取消注释
prometheus_listen_addr并运行gdk restart gitaly。
-
确保标志尚未启用:
-
执行任何触发你更改所需的操作,如创建项目、提交提交或查看历史。
-
检查当前指标列表是否包含该功能标志的新计数器:
curl --silent "http://localhost:9236/metrics" | grep go_find_all_tags
-
-
在你观察到新功能标志的指标并看到其递增后, 可以启用新功能:
-
进入 GDK 根目录。
-
启动 Rails 控制台:
bundle install && bundle exec rails console -
检查功能标志列表:
Feature::Gitaly.server_feature_flags它应该被禁用
"gitaly-feature-go-find-all-tags"=>"false"。 -
启用它:
Feature.enable('gitaly_go_find_all_tags') -
退出 Rails 控制台并执行任何触发你更改所需的操作,如创建项目、提交提交或查看历史。
-
通过观察其指标验证功能已启用:
curl --silent "http://localhost:9236/metrics" | grep go_find_all_tags
-
在测试中使用 Praefect
默认情况下,测试中的 Praefect 使用内存选举策略。该策略已被弃用,不再在生产环境中使用。 它主要用于单元测试目的。
更现代的选举策略需要与 PostgreSQL 数据库建立连接。
在运行测试时,此行为默认被禁用,但你可以通过在环境中设置 GITALY_PRAEFECT_WITH_DB=1 来启用它。
这需要你运行 PostgreSQL 并已创建数据库。 当你使用 GDK 时,可以按以下方式设置:
- 启动数据库:
gdk start db - 从 GDK 加载环境:
eval $(cd ../gitaly && gdk env) - 创建数据库:
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 特定引用时:
- 确保 Gitaly 将新引用视为隐藏引用。隐藏引用在用户拉取或获取时不可访问。 使 GitLab 特定引用隐藏可以防止它们影响最终用户的 Git 性能。
- 确保定义了生命周期。类似于 PostgreSQL,Git 仓库无法处理无限量的数据。 添加大量引用最终会导致性能问题。因此,任何创建的 GitLab 特定引用在可能时也应被删除。
- 确保引用按其支持的功能进行命名空间划分。为了诊断性能问题,引用必须与 GitLab 中的特定功能或模型相关联。
测试 GitLab 特定引用的更改
更改 GitLab 特定引用的创建时间可能会导致 GitLab UI 或进程在更改部署很长时间后失败, 因为孤立的 Git 对象在被删除前有一个宽限期。
要测试 GitLab 特定引用的更改:
-
在服务器端 Gitaly 仓库上强制运行
git gc:git gc --prune=now