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

---
stage: none
group: unassigned
info: 任何拥有 Maintainer 角色的用户均可合并对此内容的更新。详情请参见 https://docs.gitlab.com/development/development_processes/#development-guidelines-review.
title: 性能分析
---

为便于追踪性能问题,GitLab 提供了一系列性能分析工具,其中部分默认可用,部分需手动启用。

## 分析 URL

`Gitlab::Profiler.profile` 方法及对应的 `bin/profile-url` 脚本,支持对特定 URL 的 GET 或 POST 请求进行分析(默认以匿名用户身份,或指定用户身份)。

分析器的第一个参数需为完整 URL(包含实例主机名)或绝对路径(需包含前导斜杠)。

默认情况下,报告转储将存储在临时文件中,可通过 [Stackprof API](#reading-a-gitlabprofiler-report) 交互使用。

使用脚本时,可通过无参数调用查看命令行文档。

在交互式控制台会话中使用该方法时,会话内应用代码的变更会实时反映在分析器输出中。

例如:

```ruby
Gitlab::Profiler.profile('/my-user')
# 返回报告转储存储的临时文件位置
class UsersController; def show; sleep 100; end; end
Gitlab::Profiler.profile('/my-user')
# 返回报告转储存储的临时文件位置
# 其中 100 秒时间消耗在 UsersController#show

对于需要授权的路由,必须向 Gitlab::Profiler 提供用户,例如:

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

Gitlab::Profiler.profile 传入 logger: 关键字参数,可将 ActiveRecord 和 ActionController 日志输出到指定日志器。更多选项请参考方法源码文档。

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

传入 profiler_options 哈希配置采样数据的输出文件(out),例如:

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

读取 GitLab::Profiler 报告

对采样数据运行 Stackprof 可获取时间消耗摘要。例如:

stackprof tmp/profile.dump

示例采样数据:

==================================
  Mode: wall(1000)
  Samples: 8745 (6.92% miss rate)
  GC: 1399 (16.00%)
==================================
     TOTAL    (pct)     SAMPLES    (pct)     FRAME
      1022  (11.7%)        1022  (11.7%)     Sprockets::PathUtils#stat
       957  (10.9%)         957  (10.9%)     (marking)
       493   (5.6%)         493   (5.6%)     Sprockets::PathUtils#entries
       576   (6.6%)         471   (5.4%)     Mustermann::AST::Translator#decorator_for
       439   (5.0%)         439   (5.0%)     (sweeping)
       630   (7.2%)         241   (2.8%)     Sprockets::Cache::FileStore#get
       208   (2.4%)         208   (2.4%)     ActiveSupport::FileUpdateChecker#watched
       206   (2.4%)         206   (2.4%)     Digest::Instance#file
       544   (6.2%)         176   (2.0%)     Sprockets::Cache::FileStore#safe_open
       176   (2.0%)         176   (2.0%)     ActiveSupport::FileUpdateChecker#max_mtime
       268   (3.1%)         147   (1.7%)     ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#exec_no_cache
       140   (1.6%)         140   (1.6%)     ActiveSupport::BacktraceCleaner#add_gem_filter
       116   (1.3%)         116   (1.3%)     Bootsnap::CompileCache::ISeq.storage_to_output
       160   (1.8%)         113   (1.3%)     Gem::Version#<=>
       109   (1.2%)         109   (1.2%)     block in <main>
       108   (1.2%)         108   (1.2%)     Gem::Version.new
       131   (1.5%)         105   (1.2%)     Sprockets::EncodingUtils#unmarshaled_deflated
      1166  (13.3%)          82   (0.9%)     Mustermann::RegexpBased#initialize
        82   (0.9%)          78   (0.9%)     FileUtils.touch
        72   (0.8%)          72   (0.8%)     Sprockets::Manifest.compile_match_filter
        71   (0.8%)          70   (0.8%)     Grape::Router#compile!
        91   (1.0%)          65   (0.7%)     ActiveRecord::ConnectionAdapters::PostgreSQL::DatabaseStatements#query
        93   (1.1%)          64   (0.7%)     ActionDispatch::Journey::Path::Pattern::AnchoredRegexp#accept
        59   (0.7%)          59   (0.7%)     Mustermann::AST::Translator.dispatch_table
        62   (0.7%)          59   (0.7%)     Rails::BacktraceCleaner#initialize
      2492  (28.5%)          49   (0.6%)     Sprockets::PathUtils#stat_directory
       242   (2.8%)          49   (0.6%)     Gitlab::Instrumentation::RedisBase.add_call_details
        47   (0.5%)          47   (0.5%)     URI::RFC2396_Parser#escape
        46   (0.5%)          46   (0.5%)     #<Class:0x00000001090c2e70>#__setobj__
        44   (0.5%)          44   (0.5%)     Sprockets::Base#normalize_logical_path

也可生成火焰图:

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,在 config/gitlab.yml 中添加以下行(根据需要修改 enabled 值):

    bullet:
      enabled: false
  • 启用 Bullet 日志,启动 GitLab 前设置 ENABLE_BULLET 环境变量为非空值:

    ENABLE_BULLET=true bundle exec rails s

使用 Bullet 发现 N+1 查询后,建议编写 QueryRecorder 测试 防止回归。

系统统计信息

在分析期间或之后,您可能需要获取 Ruby 虚拟机进程的详细信息,例如内存消耗、CPU 时间或垃圾回收器统计。这些信息可通过各种工具单独获取,为方便起见,我们添加了汇总端点以 JSON 格式导出数据:

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

示例输出:

{
  "version": "ruby 2.7.2p137 (2020-10-01 revision a8323b79eb) [x86_64-linux-gnu]",
  "gc_stat": {
    "count": 118,
    "heap_allocated_pages": 11503,
    "heap_sorted_length": 11503,
    "heap_allocatable_pages": 0,
    "heap_available_slots": 4688580,
    "heap_live_slots": 3451712,
    "heap_free_slots": 1236868,
    "heap_final_slots": 0,
    "heap_marked_slots": 3451450,
    "heap_eden_pages": 11503,
    "heap_tomb_pages": 0,
    "total_allocated_pages": 11503,
    "total_freed_pages": 0,
    "total_allocated_objects": 32679478,
    "total_freed_objects": 29227766,
    "malloc_increase_bytes": 84760,
    "malloc_increase_bytes_limit": 32883343,
    "minor_gc_count": 88,
    "major_gc_count": 30,
    "compact_count": 0,
    "remembered_wb_unprotected_objects": 114228,
    "remembered_wb_unprotected_objects_limit": 228456,
    "old_objects": 3185330,
    "old_objects_limit": 6370660,
    "oldmalloc_increase_bytes": 21838024,
    "oldmalloc_increase_bytes_limit": 119181499
  },
  "memory_rss": 1326501888,
  "memory_uss": 1048563712,
  "memory_pss": 1139554304,
  "time_cputime": 82.885264633,
  "time_realtime": 1610459445.5579069,
  "time_monotonic": 24001.23145713,
  "worker_id": "puma_0"
}

此端点仅对 Rails Web Worker 可用。Sidekiq Worker 无法通过此方式检查。

影响性能的设置

应用设置

  1. development 环境默认启用热重载,Rails 会在每次请求时检查文件变更,因热重载是单线程的,可能产生潜在竞争锁。
  2. development 环境可在请求触发后延迟加载代码,导致首次请求始终较慢。

为在性能分析/基准测试中禁用这些功能,启动 GitLab 前设置 RAILS_PROFILE 环境变量为 true。例如使用 GDK 时:

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

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

GC 设置

Ruby 的垃圾回收器(GC)可通过多种环境变量调优,直接影响应用性能。

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

环境变量 默认值
RUBY_GC_HEAP_INIT_SLOTS 10000
RUBY_GC_HEAP_FREE_SLOTS 4096
RUBY_GC_HEAP_FREE_SLOTS_MIN_RATIO 0.20
RUBY_GC_HEAP_FREE_SLOTS_GOAL_RATIO 0.40
RUBY_GC_HEAP_FREE_SLOTS_MAX_RATIO 0.65
RUBY_GC_HEAP_GROWTH_FACTOR 1.8
RUBY_GC_HEAP_GROWTH_MAX_SLOTS 0 (disable)
RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR 2.0
RUBY_GC_MALLOC_LIMIT(_MIN) (16 * 1024 * 1024 /* 16MB */)
RUBY_GC_MALLOC_LIMIT_MAX (32 * 1024 * 1024 /* 32MB */)
RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR 1.4
RUBY_GC_OLDMALLOC_LIMIT(_MIN) (16 * 1024 * 1024 /* 16MB */)
RUBY_GC_OLDMALLOC_LIMIT_MAX (128 * 1024 * 1024 /* 128MB */)
RUBY_GC_OLDMALLOC_LIMIT_GROWTH_FACTOR 1.2

(来源)

GitLab 可能会调整这些设置以提升应用性能、降低内存需求或两者兼顾。

运行 scripts/perf/gc/collect_gc_stats.rb 脚本可查看各设置对空闲 GitLab 实例的 GC 性能、内存使用和启动时间的影响。脚本会将 GC 统计和常规时间数据以 CSV 格式输出到标准输出。

性能问题调查示例

Pipeline Authoring 团队解决了 流水线创建性能问题,同时使用了现有分析方法(如 stackprof 火焰图memory_profiler)以及新方法 ruby-prof

使用 stackprof 火焰图

性能栏 是通过单击获取 stackprof 报告和火焰图的优秀工具:

Performance Bar Flamegraph Link

但该工具仅适用于 GET 请求。

为获取 POST 请求的火焰图,我们在 API 请求中使用 performance_bar=flamegraph 参数。例如分析 合并请求的流水线创建端点

通常可用以下命令获取 JSON 格式的 stackprof 报告,但用户控制逻辑 Gitlab::PerformanceBar.allowed_for_user?(request.env['warden']&.user) 仅允许通过 Web 界面认证的用户:

# 生产环境无效

curl --request POST \
  --output flamegraph.json \
  --header 'Content-Type: application/json' \
  --header 'PRIVATE-TOKEN: :token' \
  "https://gitlab.example.com/api/v4/projects/:id/merge_requests/:iid/pipelines?performance_bar=flamegraph"

解决方案:复制请求为 curl 命令并在终端执行:

Performance copy as curl

得到类似以下命令:

curl "https://gitlab.com/api/v4/projects/:id/merge_requests/:iid/pipelines" \
  -H 'accept: application/json, text/plain, */*' \
  -H 'content-type: application/json' \
  -H 'cookie: xyz' \
  -H 'x-csrf-token: xyz' \
  --data-raw '{"async":true}'
  • 注意请求体中的 async 参数,需移除以获取实际流水线创建性能。
  • 需添加 performance_bar=flamegraph 参数。
  • 需添加 --output flamegraph.json 保存 JSON 响应。
  • 最后指定仅接受 JSON 响应。
curl "https://gitlab.com/api/v4/projects/:id/merge_requests/:iid/pipelines?performance_bar=flamegraph" \
  -X POST \
  -o flamegraph.json \
  -H 'accept: application/json' \
  -H 'content-type: application/json' \
  -H 'cookie: xyz' \
  -H 'x-csrf-token: xyz'

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

Speedscope flamegraph example

例如分析此火焰图时,发现 kubernetes_variables 方法耗时较长,因此创建了 issue

Speedscope flamegraph Kubernetes example

使用 ruby-prof

另一种查看时间消耗分布的方法是使用 ruby-prof。它未包含在 Gemfile 中,需先添加到 Gemfile 并运行 bundle install

调查问题需要副本仓库,可通过将生产仓库镜像到开发环境实现。然后运行 ruby-prof 分析器查看时间消耗:

# RAILS_PROFILE=true GITALY_DISABLE_REQUEST_LIMITS=true rails console

require 'ruby-prof'

ActiveRecord::Base.logger = nil
project = Project.find_by_full_path('root/gitlab-mirror')
user = project.first_owner
merge_request = project.merge_requests.find_by_iid(1)

profile = RubyProf::Profile.new
profile.exclude_common_methods! # 参考 https://github.com/ruby-prof/ruby-prof/blob/1.7.0/lib/ruby-prof/exclude_common_methods.rb

profile.start

Gitlab::SafeRequestStore.ensure_request_store do
  Ci::CreatePipelineService
    .new(project, user, ref: merge_request.source_branch)
    .execute(:merge_request_event, merge_request: merge_request)
    .payload
end; nil

result = profile.stop

callstack_printer = RubyProf::CallStackPrinter.new(result)
File.open('tmp/ruby-prof-callstack-report.html', 'w') do |file|
  callstack_printer.print(file)
end

::Ci::DestroyPipelineService.new(project, user).execute(Ci::Pipeline.last)

Ruby-prof callstack report

可见调用了 Ci::GenerateKubeconfigService 约 2k 次,这是需要重点优化的指标。

使用 memory_profiler

memory_profiler 是分析内存使用的工具。高内存消耗可能导致性能问题,因此同样重要。

stackprof 类似,也可使用 curl 并添加 performance_bar 参数:

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

但生产环境因 60 秒请求超时限制无法使用,需在开发环境获取内存分析。更多详情请参考 内存分析器文档

# RAILS_PROFILE=true GITALY_DISABLE_REQUEST_LIMITS=true rails console

require 'memory_profiler'

ActiveRecord::Base.logger = nil
project = Project.find_by_full_path('root/gitlab-mirror')
user = project.first_owner
merge_request = project.merge_requests.find_by_iid(1)

# 预热
Ci::CreatePipelineService
  .new(project, user, ref: merge_request.source_branch)
  .execute(:merge_request_event, merge_request: merge_request); nil

report = MemoryProfiler.report do
  Gitlab::SafeRequestStore.ensure_request_store do
    Ci::CreatePipelineService
      .new(project, user, ref: merge_request.source_branch)
      .execute(:merge_request_event, merge_request: merge_request); nil
  end
end; nil

output = File.open('tmp/memory-profile-report.txt', 'w')
report.pretty_print(output, detailed_report: true, scale_bytes: true, normalize_paths: true)

结果:

#
# 注意:已省略与 gems 和 Rails 框架相关的部分。
#       为可读性,输出已精简。
#

总分配:1.30 GB (12974240 个对象)
总保留:29.67 MB (335085 个对象)

按 gem 分配的内存
-----------------------------------
 675.48 MB  gitlab/lib

...

按文件分配的内存
-----------------------------------
 253.68 MB  gitlab/lib/gitlab/ci/variables/collection/item.rb
 143.58 MB  gitlab/lib/gitlab/ci/variables/collection.rb
  51.66 MB  gitlab/lib/gitlab/config/entry/configurable.rb
  20.89 MB  gitlab/lib/gitlab/ci/pipeline/expression/lexeme/base.rb

...

按位置分配的内存
-----------------------------------
 107.12 MB  gitlab/lib/gitlab/ci/variables/collection/item.rb:64
  70.22 MB  gitlab/lib/gitlab/ci/variables/collection.rb:28
  57.66 MB  gitlab/lib/gitlab/ci/variables/collection.rb:82
  45.70 MB  gitlab/lib/gitlab/config/entry/configurable.rb:67
  42.35 MB  gitlab/lib/gitlab/ci/variables/collection/item.rb:17
  42.35 MB  gitlab/lib/gitlab/ci/variables/collection/item.rb:80
  41.32 MB  gitlab/lib/gitlab/ci/variables/collection/item.rb:76
  20.10 MB  gitlab/lib/gitlab/ci/variables/collection/item.rb:72

...

通过按文件和位置分配的内存,可定位内存优化点。在 近期工作 中,我们 找到了优化方案,得到以下结果:

#
# 注意:已省略与 gems 和 Rails 框架相关的部分。
#       为可读性,输出已精简。
#

总分配:1.08 GB (11171148 个对象)
总保留:29.67 MB (335082 个对象)

按 gem 分配的内存
-----------------------------------
 495.88 MB  gitlab/lib

...

按文件分配的内存
-----------------------------------
 112.44 MB  gitlab/lib/gitlab/ci/variables/collection.rb
 105.24 MB  gitlab/lib/gitlab/ci/variables/collection/item.rb
  51.66 MB  gitlab/lib/gitlab/config/entry/configurable.rb
  20.89 MB  gitlab/lib/gitlab/ci/pipeline/expression/lexeme/base.rb

...

此示例流水线总内存减少约 200 MB。