极狐 GitLab

性能分析

为了更轻松地追踪性能问题,极狐GitLab 提供了一套性能分析工具,其中一些默认可用,另一些则需要显式启用。

对 URL 进行性能分析#

系统提供了 Gitlab::Profiler.profile 方法以及相应的 bin/profile-url 脚本,可以对特定 URL 的 GET 或 POST 请求进行性能分析,既可以作为匿名用户(默认设置),也可以作为指定的用户来进行分析。

传递给性能分析器的第一个参数可以是完整的 URL(包含实例主机名),也可以是绝对路径(需包含前导斜杠)。

默认情况下,性能分析报告转储会保存在一个临时文件中,你可以使用 Stackprof API 与它进行交互。

使用该脚本时,可以通过不传递任何参数来查看命令行文档。

在交互式控制台会话中使用该方法时,该控制台会话内对应用程序代码的任何更改,都会反映在性能分析器的输出中。

例如:

ruby
1Gitlab::Profiler.profile('/my-user') 2# 返回存储报告转储的临时文件位置 3class UsersController; def show; sleep 100; end; end 4Gitlab::Profiler.profile('/my-user') 5# 返回存储报告转储的临时文件位置 6# 其中 100 秒都花在了 UsersController#show 中

对于需要授权的路由,你必须向 Gitlab::Profiler 提供一个用户。你可以按以下方式来操作:

ruby
Gitlab::Profiler.profile('/gitlab-org/gitlab-test', user: User.first)

Gitlab::Profiler.profile 传递一个 logger: 关键字参数,会将 ActiveRecord 和 ActionController 的日志输出发送到该记录器。关于更多选项,可以参考该方法的源码文档。

ruby
Gitlab::Profiler.profile('/gitlab-org/gitlab-test', user: User.first, logger: Logger.new($stdout))

传入一个 profiler_options 哈希值,可以配置采样数据的输出文件(out)。例如:

ruby
Gitlab::Profiler.profile('/gitlab-org/gitlab-test', user: User.first, profiler_options: { out: 'tmp/profile.dump' })

阅读 GitLab::Profiler 报告#

你可以通过对采样数据运行 Stackprof 来获取时间消耗的摘要。例如:

shell
stackprof tmp/profile.dump

采样数据示例:

plaintext
1================================== 2 模式: wall(1000) 3 样本数: 8745 (6.92% 未命中率) 4 垃圾回收: 1399 (16.00%) 5================================== 6 总计 (百分比) 样本数 (百分比) 帧 7 1022 (11.7%) 1022 (11.7%) Sprockets::PathUtils#stat 8 957 (10.9%) 957 (10.9%) (标记) 9 493 (5.6%) 493 (5.6%) Sprockets::PathUtils#entries 10 576 (6.6%) 471 (5.4%) Mustermann::AST::Translator#decorator_for 11 439 (5.0%) 439 (5.0%) (清除) 12 630 (7.2%) 241 (2.8%) Sprockets::Cache::FileStore#get 13 208 (2.4%) 208 (2.4%) ActiveSupport::FileUpdateChecker#watched 14 206 (2.4%) 206 (2.4%) Digest::Instance#file 15 544 (6.2%) 176 (2.0%) Sprockets::Cache::FileStore#safe_open 16 176 (2.0%) 176 (2.0%) ActiveSupport::FileUpdateChecker#max_mtime 17 268 (3.1%) 147 (1.7%) ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#exec_no_cache 18 140 (1.6%) 140 (1.6%) ActiveSupport::BacktraceCleaner#add_gem_filter 19 116 (1.3%) 116 (1.3%) Bootsnap::CompileCache::ISeq.storage_to_output 20 160 (1.8%) 113 (1.3%) Gem::Version#<=> 21 109 (1.2%) 109 (1.2%) block in <main> 22 108 (1.2%) 108 (1.2%) Gem::Version.new 23 131 (1.5%) 105 (1.2%) Sprockets::EncodingUtils#unmarshaled_deflated 24 1166 (13.3%) 82 (0.9%) Mustermann::RegexpBased#initialize 25 82 (0.9%) 78 (0.9%) FileUtils.touch 26 72 (0.8%) 72 (0.8%) Sprockets::Manifest.compile_match_filter 27 71 (0.8%) 70 (0.8%) Grape::Router#compile! 28 91 (1.0%) 65 (0.7%) ActiveRecord::ConnectionAdapters::PostgreSQL::DatabaseStatements#query 29 93 (1.1%) 64 (0.7%) ActionDispatch::Journey::Path::Pattern::AnchoredRegexp#accept 30 59 (0.7%) 59 (0.7%) Mustermann::AST::Translator.dispatch_table 31 62 (0.7%) 59 (0.7%) Rails::BacktraceCleaner#initialize 32 2492 (28.5%) 49 (0.6%) Sprockets::PathUtils#stat_directory 33 242 (2.8%) 49 (0.6%) Gitlab::Instrumentation::RedisBase.add_call_details 34 47 (0.5%) 47 (0.5%) URI::RFC2396_Parser#escape 35 46 (0.5%) 46 (0.5%) #<Class:0x00000001090c2e70>#__setobj__ 36 44 (0.5%) 44 (0.5%) Sprockets::Base#normalize_logical_path

你也可以生成火焰图:

shell
stackprof --d3-flamegraph tmp/profile.dump > flamegraph.html

更多详细信息,请参阅 Stackprof 文档

Speedscope 火焰图#

你可以通过在性能栏中选择火焰图采样模式按钮,或通过向请求中添加 performance_bar=flamegraph 参数,来为特定 URL 生成火焰图。

Speedscope

有关视图的更多信息,请查阅 Speedscope 文档

有关不同采样模式的更多信息,请查阅 Stackprof 文档

此功能对所有能访问性能栏的用户开放。

Bullet#

Bullet 是一个用于追踪 N+1 查询问题的 Gem。它会将查询问题记录到 Rails 日志和浏览器控制台。Bullet 部分显示在性能栏上。

Bullet

默认情况下,Bullet 仅在开发模式下启用。然而,其日志记录功能是关闭的,因为 Bullet 的日志输出会比较冗长。要配置 Bullet 及其日志记录功能:

  • 要在特定环境中手动启用或禁用 Bullet,请将以下行添加到 config/gitlab.yml 文件中,并根据需要修改 enabled 的值:

    yaml
    bullet: enabled: false
  • 要启用 Bullet 日志记录,请在启动极狐GitLab 前,将 ENABLE_BULLET 环境变量设置为非空值:

    shell
    ENABLE_BULLET=true bundle exec rails s

在通过 Bullet 找到 N+1 查询之后,可以考虑编写一个 QueryRecoder 测试来防止性能回退。

系统状态#

在性能分析期间或之后,你可能需要获取关于 Ruby 虚拟机进程的详细信息,例如内存消耗、CPU 耗时或垃圾回收器统计信息。这些统计数据可以通过各种工具单独生成,但为了方便起见,系统提供了一个汇总端点,以 JSON 负载的形式导出这些数据:

shell
curl localhost:3000/-/metrics/system | jq

输出示例:

json
1{ 2 "version": "ruby 2.7.2p137 (2020-10-01 revision a8323b79eb) [x86_64-linux-gnu]", 3 "gc_stat": { 4 "count": 118, 5 "heap_allocated_pages": 11503, 6 "heap_sorted_length": 11503, 7 "heap_allocatable_pages": 0, 8 "heap_available_slots": 4688580, 9 "heap_live_slots": 3451712, 10 "heap_free_slots": 1236868, 11 "heap_final_slots": 0, 12 "heap_marked_slots": 3451450, 13 "heap_eden_pages": 11503, 14 "heap_tomb_pages": 0, 15 "total_allocated_pages": 11503, 16 "total_freed_pages": 0, 17 "total_allocated_objects": 32679478, 18 "total_freed_objects": 29227766, 19 "malloc_increase_bytes": 84760, 20 "malloc_increase_bytes_limit": 32883343, 21 "minor_gc_count": 88, 22 "major_gc_count": 30, 23 "compact_count": 0, 24 "remembered_wb_unprotected_objects": 114228, 25 "remembered_wb_unprotected_objects_limit": 228456, 26 "old_objects": 3185330, 27 "old_objects_limit": 6370660, 28 "oldmalloc_increase_bytes": 21838024, 29 "oldmalloc_increase_bytes_limit": 119181499 30 }, 31 "memory_rss": 1326501888, 32 "memory_uss": 1048563712, 33 "memory_pss": 1139554304, 34 "time_cputime": 82.885264633, 35 "time_realtime": 1610459445.5579069, 36 "time_monotonic": 24001.23145713, 37 "worker_id": "puma_0" 38}

此端点仅对 Rails Web 工作进程可用。Sidekiq 工作进程无法通过此方式检查。

影响性能的设置#

应用程序设置#

  1. development 环境默认开启了热重载功能,这会使 Rails 在每次请求时检查文件变更,并可能导致潜在的竞争锁,因为热重载是单线程的。
  2. development 环境可以在请求被触发时懒加载代码,这会导致首个请求通常较慢。

要在进行性能分析与基准测试时禁用这些功能,请在启动极狐GitLab 前将 RAILS_PROFILE 环境变量设置为 true。例如,在使用 GDK 时:

  • 在 GDK 根目录下创建一个 env.runit 文件
  • export RAILS_PROFILE=true 添加到你的 env.runit 文件中
  • 使用 gdk restart 重启 GDK

此环境变量仅适用于开发模式。

垃圾回收设置#

Ruby 的垃圾回收器(GC)可以通过各种环境变量进行调整,这些变量会直接影响应用程序的性能。

下表列出了这些变量及其默认值。

环境变量默认值
RUBY_GC_HEAP_INIT_SLOTS10000
RUBY_GC_HEAP_FREE_SLOTS4096
RUBY_GC_HEAP_FREE_SLOTS_MIN_RATIO0.20
RUBY_GC_HEAP_FREE_SLOTS_GOAL_RATIO0.40
RUBY_GC_HEAP_FREE_SLOTS_MAX_RATIO0.65
RUBY_GC_HEAP_GROWTH_FACTOR1.8
RUBY_GC_HEAP_GROWTH_MAX_SLOTS0 (禁用)
RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR2.0
RUBY_GC_MALLOC_LIMIT(_MIN)(16 * 1024 * 1024 /* 16MB */)
RUBY_GC_MALLOC_LIMIT_MAX(32 * 1024 * 1024 /* 32MB */)
RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR1.4
RUBY_GC_OLDMALLOC_LIMIT(_MIN)(16 * 1024 * 1024 /* 16MB */)
RUBY_GC_OLDMALLOC_LIMIT_MAX(128 * 1024 * 1024 /* 128MB */)
RUBY_GC_OLDMALLOC_LIMIT_GROWTH_FACTOR1.2

(来源)

极狐GitLab 可能会决定更改这些设置,以加速应用程序性能、降低内存需求,或两者兼顾。

你可以通过运行 scripts/perf/gc/collect_gc_stats.rb 脚本,来了解这些设置如何影响空闲的极狐GitLab 实例的垃圾回收性能、内存使用和应用程序启动时间。该脚本会将垃圾回收统计数据和常规计时数据以 CSV 格式输出到标准输出。

调查性能问题的案例#

流水线创作团队致力于解决流水线创建性能问题,他们使用了现有的性能分析方法,例如 stackprof 火焰图memory_profiler,还采用了一种新方法 ruby-prof

使用 stackprof 火焰图#

性能栏 是获取 stackprof 报告并通过单击查看火焰图的绝佳工具;

性能栏火焰图链接

但是,它不适用于非 GET 请求。

要获取 POST 请求的火焰图,我们在 API 请求中使用 performance_bar=flamegraph 参数。在我们的案例中,我们想查看合并请求的流水线创建端点的火焰图。

通常,我们可以使用以下命令以 JSON 文件的形式获取 stackprof 报告,但是我们的用户控制 Gitlab::PerformanceBar.allowed_for_user?(request.env['warden']&.user) 只允许通过 Web 界面认证的用户。

shell
1# 这在生产环境中不起作用 2 3curl --request POST \ 4 --output flamegraph.json \ 5 --header 'Content-Type: application/json' \ 6 --header 'PRIVATE-TOKEN: :token' \ 7 "https://gitlab.example.com/api/v4/projects/:id/merge_requests/:iid/pipelines?performance_bar=flamegraph"

为了解决这个问题,我们以 curl 形式复制请求,并在终端中使用它。

以 curl 形式复制性能请求

我们会得到如下 curl 命令:

shell
1curl "https://gitlab.com/api/v4/projects/:id/merge_requests/:iid/pipelines" \ 2 -H 'accept: application/json, text/plain, */*' \ 3 -H 'content-type: application/json' \ 4 -H 'cookie: xyz' \ 5 -H 'x-csrf-token: xyz' \ 6 --data-raw '{"async":true}'
  • 请注意请求体中的 async 参数。 我们需要移除它,才能获取流水线创建端点的真实性能。
  • 我们需要向请求中添加 performance_bar=flamegraph 参数。
  • 我们需要添加 --output flamegraph.json 参数,以将 JSON 响应保存到文件。
  • 最后,我们只需要接受 JSON 响应。
shell
1curl "https://gitlab.com/api/v4/projects/:id/merge_requests/:iid/pipelines?performance_bar=flamegraph" \ 2 -X POST \ 3 -o flamegraph.json \ 4 -H 'accept: application/json' \ 5 -H 'content-type: application/json' \ 6 -H 'cookie: xyz' \ 7 -H 'x-csrf-token: xyz'

然后,我们在 https://www.speedscope.app/ 网站上使用 flamegraph.json 文件查看火焰图。

Speedscope 火焰图示例

例如,在调查这个 speedscope 火焰图时,我们发现 kubernetes_variables 方法花费了大量时间,并因此创建了一个议题

Speedscope 火焰图 Kubernetes 示例

使用 ruby-prof#

另一种查看时间主要花在哪里方法是使用 ruby-prof。它不是 Gemfile 中的内置 gem,因此我们需要先将其添加到 Gemfile 并运行 bundle install

为了调查问题,我们需要一个副本仓库。为此,我们可以将仓库从生产实例镜像到开发实例。然后,我们可以运行 ruby-prof 性能分析器来查看时间都花在了哪里。

ruby
1# RAILS_PROFILE=true GITALY_DISABLE_REQUEST_LIMITS=true rails console 2 3require 'ruby-prof' 4 5ActiveRecord::Base.logger = nil 6project = Project.find_by_full_path('root/gitlab-mirror') 7user = project.first_owner 8merge_request = project.merge_requests.find_by_iid(1) 9 10profile = RubyProf::Profile.new 11profile.exclude_common_methods! # 参见 https://github.com/ruby-prof/ruby-prof/blob/1.7.0/lib/ruby-prof/exclude_common_methods.rb 12 13profile.start 14 15Gitlab::SafeRequestStore.ensure_request_store do 16 Ci::CreatePipelineService 17 .new(project, user, ref: merge_request.source_branch) 18 .execute(:merge_request_event, merge_request: merge_request) 19 .payload 20end; nil 21 22result = profile.stop 23 24callstack_printer = RubyProf::CallStackPrinter.new(result) 25File.open('tmp/ruby-prof-callstack-report.html', 'w') do |file| 26 callstack_printer.print(file) 27end 28 29::Ci::DestroyPipelineService.new(project, user).execute(Ci::Pipeline.last)

Ruby-prof 调用栈报告

在这里,我们可以看到 Ci::GenerateKubeconfigService 被调用了约 2k 次。这是一个很好的指标,表明我们需要对此进行调查。

使用 memory_profiler#

memory_profiler 是一个用于分析内存使用情况的工具。这点同样重要,因为高内存使用率可能导致性能问题。

就像我们使用 stackprof 那样,我们也可以在使用 curl 时带上 performance_bar 参数。

shell
1curl "https://gitlab.com/api/v4/projects/:id/merge_requests/:iid/pipelines?performance_bar=memory" \ 2 -X POST \ 3 -o flamegraph.json \ 4 -H 'accept: application/json' \ 5 -H 'content-type: application/json' \ 6 -H 'cookie: xyz' \ 7 -H 'x-csrf-token: xyz'

然而,这在生产环境中不起作用,因为我们对请求有 60 秒的超时限制。因此,我们需要使用开发环境来获取内存分析报告。更多信息可以在 memory profiler 文档 中找到。

ruby
1# RAILS_PROFILE=true GITALY_DISABLE_REQUEST_LIMITS=true rails console 2 3require 'memory_profiler' 4 5ActiveRecord::Base.logger = nil 6project = Project.find_by_full_path('root/gitlab-mirror') 7user = project.first_owner 8merge_request = project.merge_requests.find_by_iid(1) 9 10# 预热 11Ci::CreatePipelineService 12 .new(project, user, ref: merge_request.source_branch) 13 .execute(:merge_request_event, merge_request: merge_request); nil 14 15report = MemoryProfiler.report do 16 Gitlab::SafeRequestStore.ensure_request_store do 17 Ci::CreatePipelineService 18 .new(project, user, ref: merge_request.source_branch) 19 .execute(:merge_request_event, merge_request: merge_request); nil 20 end 21end; nil 22 23output = File.open('tmp/memory-profile-report.txt', 'w') 24report.pretty_print(output, detailed_report: true, scale_bytes: true, normalize_paths: true)

结果:

plaintext
1# 2# 注意:我编辑了部分与 gem 和 Rails 框架相关的输出。 3# 同时,为便于阅读,输出内容也经过缩减。 4# 5 6总计分配: 1.30 GB (12974240 对象数) 7总计保留: 29.67 MB (335085 对象数) 8 9按 gem 分配的内存 10----------------------------------- 11 675.48 MB gitlab/lib 12 13... 14 15按文件分配的内存 16----------------------------------- 17 253.68 MB gitlab/lib/gitlab/ci/variables/collection/item.rb 18 143.58 MB gitlab/lib/gitlab/ci/variables/collection.rb 19 51.66 MB gitlab/lib/gitlab/config/entry/configurable.rb 20 20.89 MB gitlab/lib/gitlab/ci/pipeline/expression/lexeme/base.rb 21 22... 23 24按位置分配的内存 25----------------------------------- 26 107.12 MB gitlab/lib/gitlab/ci/variables/collection/item.rb:64 27 70.22 MB gitlab/lib/gitlab/ci/variables/collection.rb:28 28 57.66 MB gitlab/lib/gitlab/ci/variables/collection.rb:82 29 45.70 MB gitlab/lib/gitlab/config/entry/configurable.rb:67 30 42.35 MB gitlab/lib/gitlab/ci/variables/collection/item.rb:17 31 42.35 MB gitlab/lib/gitlab/ci/variables/collection/item.rb:80 32 41.32 MB gitlab/lib/gitlab/ci/variables/collection/item.rb:76 33 20.10 MB gitlab/lib/gitlab/ci/variables/collection/item.rb:72 34 35...

在此示例中,我们通过查看按文件和位置分配的内存,了解了可以在哪里优化内存使用。

最近的一项工作中,我们找到了一种方法来改善内存使用情况,并得到了以下结果:

plaintext
1# 2# 注意:我编辑了部分与 gem 和 Rails 框架相关的输出。 3# 同时,为便于阅读,输出内容也经过缩减。 4# 5 6总计分配: 1.08 GB (11171148 对象数) 7总计保留: 29.67 MB (335082 对象数) 8 9按 gem 分配的内存 10----------------------------------- 11 495.88 MB gitlab/lib 12 13... 14 15按文件分配的内存 16----------------------------------- 17 112.44 MB gitlab/lib/gitlab/ci/variables/collection.rb 18 105.24 MB gitlab/lib/gitlab/ci/variables/collection/item.rb 19 51.66 MB gitlab/lib/gitlab/config/entry/configurable.rb 20 20.89 MB gitlab/lib/gitlab/ci/pipeline/expression/lexeme/base.rb 21 22...

此示例流水线的总计内存减少量约为 200 MB。