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

性能指南

本文档描述了确保 GitLab 良好且一致性能的各种指南。

性能文档

工作流程

解决性能问题的过程大致如下:

  1. 确保某个地方已打开问题(例如在 GitLab CE 问题跟踪器中),如果没有则创建一个。示例见 #15607
  2. 在生产环境(如 GitLab.com)中测量代码性能(参见下面的 工具 部分)。性能应至少测量 24 小时。
  3. 根据测量周期将您的发现(图表截图、计时等)添加到步骤 1 中提到的问题中。
  4. 解决问题。
  5. 创建合并请求,分配"性能"标签,并遵循 性能审查流程
  6. 一旦变更部署完成,请确保再次至少测量 24 小时,以查看您的变更是否对生产环境产生影响。
  7. 重复直到完成。

提供计时数据时,请确保提供:

  • 第 95 百分位数
  • 第 99 百分位数
  • 平均值

提供图表截图时,请确保 X 轴和 Y 轴以及图例清晰可见。如果您有幸可以访问 GitLab.com 自身的监控工具,还应提供相关图表/仪表板的链接。

工具

GitLab 提供内置工具来帮助提高性能和可用性:

GitLab 团队成员可以使用位于 dashboards.gitlab.netGitLab.com 性能监控系统,这需要您使用 @gitlab.com 电子邮件地址登录。非 GitLab 团队成员建议设置自己的 Prometheus 和 Grafana 堆栈。

基准测试

基准测试几乎总是无用的。基准测试通常只孤立地测试少量代码,并且往往只测量最佳情况场景。此外,库(如 Gem)的基准测试往往偏向于该库。毕竟,作者发布显示性能不如竞争对手的基准测试几乎没有好处。

基准测试只有在需要大致(强调"大致")了解变更影响时才真正有用。例如,如果某个方法很慢,可以使用基准测试来查看您所做的变更是否对该方法的性能有任何影响。然而,即使基准测试显示您的变更提高了性能,也不能保证在生产环境中性能也会提高。

编写基准测试时,您应该几乎总是使用 benchmark-ips。Ruby 标准库附带的 Benchmark 模块很少有用,因为它要么运行单次迭代(使用 Benchmark.bm),要么运行两次迭代(使用 Benchmark.bmbm)。运行这么少的迭代意味着外部因素(如后台视频流)很容易使基准测试统计数据产生偏差。

Benchmark 模块的另一个问题是它显示的是计时而不是迭代次数。这意味着如果一段代码在很短的时间内完成,就很难在特定变更前后比较计时。这又会导致如下模式:

Benchmark.bmbm(10) do |bench|
  bench.report 'do something' do
    100.times do
      ... work here ...
    end
  end
end

然而,这会引出问题:我们应该运行多少次迭代才能获得有意义的统计数据?

benchmark-ips gem 处理了所有这些甚至更多。因此您应该使用它而不是 Benchmark 模块。

GitLab Gemfile 还包含 benchmark-memory gem,它的工作方式与 benchmarkbenchmark-ips gem 类似。然而,benchmark-memory 返回的是基准测试期间分配和保留的内存大小、对象和字符串。

简而言之:

  • 不要相信您在网上找到的基准测试。
  • 不要仅基于基准测试做声明,始终在生产环境中测量以确认您的发现。
  • 如果您不知道 X 比 Y 快 N 倍对生产环境有什么影响,那么这个数字就没有意义。
  • 生产环境是唯一始终说真话的基准测试(除非您的性能监控系统设置不正确)。
  • 如果必须编写基准测试,请使用 benchmark-ips Gem 而不是 Ruby 的 Benchmark 模块。

使用 Stackprof 进行性能分析

通过定期收集进程状态快照,性能分析可以让您看到时间在进程中的花费位置。Stackprof gem 包含在 GitLab 中,允许您详细分析哪些代码在 CPU 上运行。

对应用程序进行性能分析会改变其性能。不同的性能分析策略有不同的开销。Stackprof 是一个采样性能分析器。它以可配置的频率(例如 100 Hz,即每秒 100 个堆栈)从运行线程中采样堆栈跟踪。这种类型的性能分析具有相当低(尽管非零)的开销,通常被认为在生产环境中是安全的。

性能分析器在开发过程中可能是一个非常有用的工具,即使它在非代表性环境中运行。特别是,一个方法仅仅因为执行次数多或执行时间长而未必就有问题。性能分析器是您可以用来更好地理解应用程序中正在发生什么工具——明智地使用这些信息取决于您!

有多种方法可以使用 Stackprof 创建性能分析。

包装代码块

要分析特定代码块,您可以将该块包装在 Stackprof.run 调用中:

StackProf.run(mode: :wall, out: 'tmp/stackprof-profiling.dump') do
  #...
end

这会创建一个 .dump 文件,您可以 读取。有关所有可用选项,请参见 Stackprof 文档

性能条

使用 性能条,您可以选择使用 Stackprof 分析请求并立即将结果输出到 Speedscope 火焰图

使用 Stackprof 进行 RSpec 性能分析

要从规范创建性能分析,识别(或创建)一个执行有问题的代码路径的规范,然后使用 bin/rspec-stackprof 助手运行它,例如:

$ bin/rspec-stackprof --limit=10 spec/policies/project_policy_spec.rb

8/8 |====== 100 ======>| Time: 00:00:18

Finished in 18.19 seconds (files took 4.8 seconds to load)
8 examples, 0 failures

==================================
 Mode: wall(1000)
 Samples: 17033 (5.59% miss rate)
 GC: 1901 (11.16%)
==================================
    TOTAL    (pct)     SAMPLES    (pct)     FRAME
     6000  (35.2%)        2566  (15.1%)     Sprockets::Cache::FileStore#get
     2018  (11.8%)         888   (5.2%)     ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#exec_no_cache
     1338   (7.9%)         640   (3.8%)     ActiveRecord::ConnectionAdapters::PostgreSQL::DatabaseStatements#execute
     3125  (18.3%)         394   (2.3%)     Sprockets::Cache::FileStore#safe_open
      913   (5.4%)         301   (1.8%)     ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#exec_cache
      288   (1.7%)         288   (1.7%)     ActiveRecord::Attribute#initialize
      246   (1.4%)         246   (1.4%)     Sprockets::Cache::FileStore#safe_stat
      295   (1.7%)         193   (1.1%)     block (2 levels) in class_attribute
      187   (1.1%)         187   (1.1%)     block (4 levels) in class_attribute

您可以通过传递任何 RSpec 通常接受的参数来限制运行的规范。

在生产环境中使用 Stackprof

Stackprof 也可用于分析生产工作负载。

要为 Ruby 进程启用生产性能分析,您可以将 STACKPROF_ENABLED 环境变量设置为 true

可以配置以下配置选项:

  • STACKPROF_ENABLED: 在 SIGUSR2 信号上启用 Stackprof 信号处理程序。默认为 false
  • STACKPROF_MODE: 参见 采样模式。默认为 cpu
  • STACKPROF_INTERVAL: 采样间隔。单位语义取决于 STACKPROF_MODE。对于 object 模式,这是每个事件的间隔(每 nth 个事件被采样),默认为 100。对于其他模式(如 cpu),这是频率间隔,默认为 10100 μs(99 hz)。
  • STACKPROF_FILE_PREFIX: 存储性能分析文件的文件路径前缀。默认为 $TMPDIR(通常对应于 /tmp)。
  • STACKPROF_TIMEOUT_S: 性能分析超时(秒)。性能分析将在此时间后自动停止。默认为 30
  • STACKPROF_RAW: 是否收集原始样本或仅聚合。原始样本需要生成火焰图,但它们具有更高的内存和磁盘开销。默认为 true

一旦启用,可以通过向 Ruby 进程发送 SIGUSR2 信号来触发性能分析。进程开始采样堆栈。可以通过发送另一个 SIGUSR2 来停止性能分析。或者,它在超时后自动停止。

性能分析停止后,性能分析将写入磁盘上的 $STACKPROF_FILE_PREFIX/stackprof.$PID.$RAND.profile。然后可以通过 stackprof 命令行工具进一步检查,如 读取 Stackprof 性能分析部分 所述。

当前支持的性能分析目标包括:

  • Puma worker
  • Sidekiq

Puma 主进程不受支持。 向它发送 SIGUSR2 会触发重启。对于 Puma,请确保只向 Puma worker 发送信号。

这可以通过 pkill -USR2 puma: 来完成。: 区分 puma 4.3.3.gitlab.2 ...(主进程)和 puma: cluster worker 0: ...(工作进程),选择后者。

对于 Sidekiq,信号可以发送到 sidekiq-cluster 进程,使用 pkill -USR2 bin/sidekiq-cluster,它会将信号转发给所有 Sidekiq 子进程。或者,您也可以选择特定的感兴趣 PID。

读取 Stackprof 性能分析

默认情况下,输出按 Samples 列排序。这是采样次数的计数,其中方法是当前执行的方法。Total 列显示采样次数的计数,其中方法(或它调用的任何方法)被执行。

要创建调用堆栈的图形视图:

stackprof tmp/project_policy_spec.rb.dump --graphviz > project_policy_spec.dot
dot -Tsvg project_policy_spec.dot > project_policy_spec.svg

要在 KCachegrind 中加载性能分析:

stackprof tmp/project_policy_spec.rb.dump --callgrind > project_policy_spec.callgrind
kcachegrind project_policy_spec.callgrind # Linux
qcachegrind project_policy_spec.callgrind # Mac

您还可以生成并查看生成的火焰图。要查看 bin/rspec-stackprof 创建的火焰图,运行 bin/rspec-stackprof 时必须添加 --raw=true 选项。

根据输出文件大小,生成可能需要一些时间:

# 生成
stackprof --flamegraph tmp/group_member_policy_spec.rb.dump > group_member_policy_spec.flame

# 查看
stackprof --flamegraph-viewer=group_member_policy_spec.flame

要将火焰图导出为 SVG 文件,请使用 Brendan Gregg 的 FlameGraph 工具

stackprof --stackcollapse  /tmp/group_member_policy_spec.rb.dump | flamegraph.pl > flamegraph.svg

也可以通过 Speedscope 查看火焰图。您可以在使用 性能条分析代码块 时执行此操作。bin/rspec-stackprof 不支持此选项。

您可以通过使用 --method method_name 分析特定方法:

$ stackprof tmp/project_policy_spec.rb.dump --method access_allowed_to

ProjectPolicy#access_allowed_to? (/Users/royzwambag/work/gitlab-development-kit/gitlab/app/policies/project_policy.rb:793)
  samples:     0 self (0.0%)  /    578 total (0.7%)
  callers:
     397  (   68.7%)  block (2 levels) in <class:ProjectPolicy>
      95  (   16.4%)  block in <class:ProjectPolicy>
      86  (   14.9%)  block in <class:ProjectPolicy>
  callees (578 total):
     399  (   69.0%)  ProjectPolicy#team_access_level
     141  (   24.4%)  Project::GeneratedAssociationMethods#project_feature
      30  (    5.2%)  DeclarativePolicy::Base#can?
       8  (    1.4%)  Featurable#access_level
  code:
                                  |   793  |   def access_allowed_to?(feature)
  141    (0.2%)                   |   794  |     return false unless project.project_feature
                                  |   795  |
    8    (0.0%)                   |   796  |     case project.project_feature.access_level(feature)
                                  |   797  |     when ProjectFeature::DISABLED
                                  |   798  |       false
                                  |   799  |     when ProjectFeature::PRIVATE
  429    (0.5%)                   |   800  |       can?(:read_all_resources) || team_access_level >= ProjectFeature.required_minimum_access_level(feature)
                                  |   801  |     else

使用 Stackprof 分析规范时,性能分析包括测试套件和应用程序所做的工作。因此您可以使用这些性能分析来调查缓慢的测试。然而,对于较小的运行(如本示例),这意味着设置测试套件的成本往往占主导地位。

RSpec 性能分析

GitLab 开发环境还包括 rspec_profiling gem,用于收集规范执行时间的数据。这对于分析测试套件本身的性能,或查看规范性能如何随时间变化很有用。

要在本地环境中激活性能分析,请运行以下命令:

export RSPEC_PROFILING=yes
rake rspec_profiling:install

这会在 tmp/rspec_profiling 中创建一个 SQLite3 数据库,每次使用 RSPEC_PROFILING 环境变量运行规范时,统计数据都会保存到其中。

可以对收集的结果进行临时调查,在交互式 shell 中:

$ rake rspec_profiling:console

irb(main):001:0> results.count
=> 231
irb(main):002:0> results.last.attributes.keys
=> ["id", "commit", "date", "file", "line_number", "description", "time", "status", "exception", "query_count", "query_time", "request_count", "request_time", "created_at", "updated_at"]
irb(main):003:0> results.where(status: "passed").average(:time).to_s
=> "0.211340155844156"

也可以通过设置 RSPEC_PROFILING_POSTGRES_URL 变量将这些结果放入 PostgreSQL 数据库中。这用于在 CI 环境中运行时分析测试套件。

我们在 gitlab.com 上默认分支的夜间计划 CI 作业中也存储这些结果。这些性能分析数据的统计信息 在线可用。例如,您可以找到哪些测试运行时间最长或执行最多查询。使用它来优化我们的测试或识别代码中的性能问题。

内存优化

我们可以使用一组不同的技术,通常结合使用,来跟踪内存问题:

  • 保持代码完整,在其周围包装性能分析器
  • 使用请求和服务的内存分配计数器
  • 在禁用/启用我们怀疑可能有问题的代码部分时监控进程的内存使用情况

内存分配

GitLab 附带的 Ruby 包含一个特殊补丁,允许 跟踪内存分配。此补丁默认适用于 OmnibusCNGGitLab CIGCK,并且可以额外为 GDK 启用。

此补丁提供以下指标,使理解给定代码路径的内存使用效率更容易:

  • mem_total_bytes: 由于新对象被分配到现有对象槽位以及为大对象分配的额外内存(即 mem_bytes + slot_size * mem_objects)而消耗的字节数。
  • mem_bytes: malloc 为不适合现有对象槽位的对象分配的字节数。
  • mem_objects: 分配的对象数。
  • mem_mallocs: malloc 调用次数。

对象和字节数的分配影响 GC 周期发生的频率。较少的对象分配会导致响应性显著提高的应用程序。

建议 Web 服务器请求不要分配超过 100k mem_objects100M mem_bytes。您可以在 GitLab.com 上查看当前使用情况。

检查自己代码的内存压力

有两种测量自己代码的方法:

  1. 查看包含内存分配计数器的 api_json.logdevelopment_json.logsidekiq.log
  2. 对给定代码块使用 Gitlab::Memory::Instrumentation.with_memory_allocations 并记录它。
  3. 使用 测量模块
{"time":"2021-02-15T11:20:40.821Z","severity":"INFO","duration_s":0.27412,"db_duration_s":0.05755,"view_duration_s":0.21657,"status":201,"method":"POST","path":"/api/v4/projects/user/1","mem_objects":86705,"mem_bytes":4277179,"mem_mallocs":22693,"correlation_id":"...}

不同类型的分配

mem_* 值代表 Ruby 中对象和内存分配的不同方面:

  • 以下示例将创建大约 1000mem_objects,因为字符串可以被冻结,虽然底层字符串对象保持不变,但我们仍然需要分配 1000 个对此字符串的引用:

    Gitlab::Memory::Instrumentation.with_memory_allocations do
      1_000.times { '0123456789' }
    end
    
    => {:mem_objects=>1001, :mem_bytes=>0, :mem_mallocs=>0}
  • 以下示例将创建大约 1000mem_objects,因为字符串是动态创建的。每个字符串不会分配额外内存,因为它们适合 40 字节的 Ruby 槽位:

    Gitlab::Memory::Instrumentation.with_memory_allocations do
      s = '0'
      1_000.times { s * 23 }
    end
    
    => {:mem_objects=>1002, :mem_bytes=>0, :mem_mallocs=>0}
  • 以下示例将创建大约 1000mem_objects,因为字符串是动态创建的。每个字符串会分配额外内存,因为字符串大于 40 字节的 Ruby 槽位:

    Gitlab::Memory::Instrumentation.with_memory_allocations do
      s = '0'
      1_000.times { s * 24 }
    end
    
    => {:mem_objects=>1002, :mem_bytes=>32000, :mem_mallocs=>1000}
  • 以下示例将分配超过 40 kB 的数据,并且只执行一次内存分配。现有对象将在后续迭代中被重新分配/调整大小:

    Gitlab::Memory::Instrumentation.with_memory_allocations do
      str = ''
      append = '0123456789012345678901234567890123456789' # 40 bytes
      1_000.times { str.concat(append) }
    end
    => {:mem_objects=>3, :mem_bytes=>49152, :mem_mallocs=>1}
  • 以下示例将创建超过 1k 个对象,执行超过 1k 次分配,每次都变异对象。这确实会导致复制大量数据并执行大量内存分配(由 mem_bytes 计数器表示),表明这是非常低效的追加字符串方法:

    Gitlab::Memory::Instrumentation.with_memory_allocations do
      str = ''
      append = '0123456789012345678901234567890123456789' # 40 bytes
      1_000.times { str += append }
    end
    => {:mem_objects=>1003, :mem_bytes=>21968752, :mem_mallocs=>1000}

使用 Memory Profiler

我们可以使用 memory_profiler 进行性能分析。

memory_profiler gem 已经存在于 GitLab Gemfile 中。它也存在于 性能条 中,用于当前 URL。

要在代码中直接使用内存性能分析器,使用 require 添加它:

require 'memory_profiler'

report = MemoryProfiler.report do
  # 您要分析的代码
end

output = File.open('/tmp/profile.txt','w')
report.pretty_print(output)

报告显示了按 gem、文件、位置和类分组的保留和分配内存。内存性能分析器还执行字符串分析,显示字符串被分配和保留的频率。

保留与分配

  • 保留内存:由于执行代码块而保留的长期内存使用和对象计数。这直接影响内存和垃圾收集器。
  • 分配内存:代码块期间的所有对象分配和内存分配。这可能对内存影响最小,但对性能影响很大。您分配的对象越多,完成的工作就越多,应用程序就越慢。

作为一般规则,保留 始终小于或等于 分配

实际 RSS 成本总是略高,因为 MRI 堆没有被压缩到大小,并且内存碎片化。

Rbtrace

内存占用增加的原因之一可能是 Ruby 内存碎片化。

要诊断它,您可以可视化 Ruby 堆,如 Aaron Patterson 的这篇文章 中所述。

首先,您要将您正在调查的进程的堆转储到 JSON 文件中。

您需要在您探索的进程内部运行命令,您可以使用 rbtrace 来实现。rbtrace 已经存在于 GitLab Gemfile 中,您只需要 require 它。可以通过使用环境变量设置为 ENABLE_RBTRACE=1 来运行 Web 服务器或 Sidekiq 来实现。

要获取堆转储:

bundle exec rbtrace -p <PID> -e 'File.open("heap.json", "wb") { |t| ObjectSpace.dump_all(output: t) }'

拥有 JSON 后,您可以使用 Aaron 提供的脚本 或类似脚本渲染图片:

ruby heapviz.rb heap.json

碎片化的 Ruby 堆快照可能如下所示:

Ruby heap fragmentation

可以通过调整 GC 参数 如本文所述 来减少内存碎片化。这应被视为权衡,因为它可能影响内存分配和 GC 周期的整体性能。

Derailed Benchmarks

derailed_benchmarks 是一个 gem,描述为"一系列可用于对 Rails 或 Ruby 应用程序进行基准测试的工具"。我们在 Gemfile 中包含 derailed_benchmarks

我们在每个带有 test 阶段的流水程中运行 derailed exec perf:mem,在一个名为 memory-on-boot 的作业中。(阅读示例作业。。) 您可以在以下位置找到结果:

  • 在合并请求 概览 选项卡中,在合并请求报告区域,在 指标报告 下拉列表 中。
  • memory-on-boot 工件中获取完整报告和依赖分解。

derailed_benchmarks 还提供其他方法来调查内存。有关更多信息,请参见 gem 文档。大多数方法(derailed exec perf:*)尝试在 production 环境中启动您的 Rails 应用程序并对其运行基准测试。 这在 GDK 和 GCK 中都可行:

  • 对于 GDK,遵循 gem 页面上的 说明。您必须对 Redis 配置做类似操作以避免错误。
  • GCK 开箱即用 包含 production 配置部分。

变更的重要性

在进行性能改进时,始终问自己"改进这段代码的性能有多重要?“这个问题很重要。并非所有代码都同等重要,花费一周时间试图改进只影响我们一小部分用户的事情是浪费。例如,花费一周时间试图从方法中挤出 10 毫秒是浪费时间,而您本可以花一周时间在其他地方挤出 10 秒。

没有明确的步骤可以遵循以确定某段代码是否值得优化。您唯一能做的就是:

  1. 思考代码的作用、如何使用、被调用的次数以及相对于总执行时间(例如 Web 请求中花费的总时间)在其中花费的时间。
  2. 询问他人(最好以问题的形式)。

一些不太重要/不值得努力的变更示例:

  • 用单引号替换双引号。
  • 当值列表非常小时,用 Set 替换 Array 的使用。
  • 当两个库都只占总执行时间的 0.1% 时,用库 B 替换库 A。
  • 在每个字符串上调用 freeze(参见 字符串冻结)。

慢操作与 Sidekiq

慢操作,如合并分支,或容易出错的操作(使用外部 API)应尽可能在 Sidekiq worker 中执行,而不是直接在 Web 请求中执行。这有许多好处,例如:

  1. 错误不会阻止请求完成。
  2. 慢速进程不会影响页面加载时间。
  3. 如果失败,您可以重试该进程(Sidekiq 会自动处理)。
  4. 通过将代码与 Web 请求隔离,应该更容易测试和维护。

处理 Git 操作时尽可能使用 Sidekiq 特别重要,因为这些操作根据底层存储系统的性能可能需要相当长的时间才能完成。

Git 操作

应注意避免不必要的 Git 操作。例如,使用 Repository#branch_names 检索分支名称列表可以在不显式检查存储库是否存在的情况下完成。换句话说,而不是这样:

if repository.exists?
  repository.branch_names.each do |name|
    ...
  end
end

您可以只写:

repository.branch_names.each do |name|
  ...
end

缓存

经常返回相同结果的操作应使用 Redis 缓存,特别是 Git 操作。在 Redis 中缓存数据时,确保在需要时刷新缓存。例如,标签列表的缓存应在推送新标签或删除标签时刷新。

为存储库添加缓存过期代码时,此代码应放在 Repository 类中的一个 before/after hook 中。例如,如果应在导入存储库后刷新缓存,则应将此代码添加到 Repository#after_import。这确保缓存逻辑保持在 Repository 类内部,而不是泄漏到其他类中。

缓存数据时,还应在实例变量中记忆结果。虽然从 Redis 检索数据比原始 Git 操作快得多,但它仍然有开销。通过在实例变量中缓存结果,对同一方法的重复调用不会在每次调用时从 Redis 检索数据。在实例变量中记忆缓存数据时,还应在刷新缓存时重置实例变量。示例:

def first_branch
  @first_branch ||= cache.fetch(:first_branch) { branches.first }
end

def expire_first_branch_cache
  cache.expire(:first_branch)
  @first_branch = nil
end

字符串冻结

在最近的 Ruby 版本中,对字符串调用 .freeze 会导致它只被分配一次并重用。例如,在 Ruby 2.3 或更高版本中,这只会分配一次"foo"字符串:

10.times do
  'foo'.freeze
end

根据字符串的大小以及它将被分配的频率(在添加 .freeze 调用之前),这可能会使事情更快,但这不能保证。

冻结字符串可以节省内存,因为每个分配的字符串至少使用一个 RVALUE_SIZE 字节(x64 上为 40 字节)的内存。

您可以使用 内存性能分析器 查看哪些字符串经常分配并且可能受益于 .freeze

字符串在 Ruby 3.0 中默认冻结。为了为我们的代码库做好准备,我们在所有 Ruby 文件中添加以下标题:

# frozen_string_literal: true

这可能导致期望能够操作字符串的代码中的测试失败。不要使用 dup,而使用一元加号来获取未冻结的字符串:

test = +"hello"
test += " world"

添加新的 Ruby 文件时,检查您是否可以添加上述标题,因为省略它可能导致样式检查失败。

Banzai 流水线和过滤器

编写或更新 Banzai 过滤器和流水线 时,可能很难理解过滤器的性能以及它对整体流水线性能的影响。

要执行基准测试,请运行:

bin/rake benchmark:banzai

此命令生成如下输出:

--> Benchmarking Full, Wiki, and Plain pipelines
Calculating -------------------------------------
       Full pipeline     1.000  i/100ms
       Wiki pipeline     1.000  i/100ms
      Plain pipeline     1.000  i/100ms
-------------------------------------------------
       Full pipeline      3.357  (±29.8%) i/s -     31.000
       Wiki pipeline      2.893  (±34.6%) i/s -     25.000  in  10.677014s
      Plain pipeline     15.447  (±32.4%) i/s -    119.000

Comparison:
      Plain pipeline:       15.4 i/s
       Full pipeline:        3.4 i/s - 4.60x slower
       Wiki pipeline:        2.9 i/s - 5.34x slower

.
--> Benchmarking FullPipeline filters
Calculating -------------------------------------
            Markdown    24.000  i/100ms
            Plantuml     8.000  i/100ms
          SpacedLink    22.000  i/100ms

...

            TaskList    49.000  i/100ms
          InlineDiff     9.000  i/100ms
        SetDirection   369.000  i/100ms
-------------------------------------------------
            Markdown    237.796  (±16.4%) i/s -      2.304k
            Plantuml     80.415  (±36.1%) i/s -    520.000
          SpacedLink    168.188  (±10.1%) i/s -      1.672k

...

            TaskList    101.145  (± 6.9%) i/s -      1.029k
          InlineDiff     52.925  (±15.1%) i/s -    522.000
        SetDirection      3.728k (±17.2%) i/s -     34.317k in  10.617882s

Comparison:
          Suggestion:   739616.9 i/s
               Kroki:   306449.0 i/s - 2.41x slower
InlineGrafanaMetrics:   156535.6 i/s - 4.72x slower
        SetDirection:     3728.3 i/s - 198.38x slower

...

       UserReference:        2.1 i/s - 360365.80x slower
        ExternalLink:        1.6 i/s - 470400.67x slower
    ProjectReference:        0.7 i/s - 1128756.09x slower

.
--> Benchmarking PlainMarkdownPipeline filters
Calculating -------------------------------------
            Markdown    19.000  i/100ms
-------------------------------------------------
            Markdown    241.476  (±15.3%) i/s -      2.356k

这可以让您了解各种过滤器的性能,以及哪些可能运行最慢。

测试数据对过滤器性能有很大影响。如果测试数据中没有专门触发过滤器的任何内容,它可能看起来运行得非常快。请确保在 spec/fixtures/markdown.md.erb 文件中为您的过滤器提供相关测试数据。

基准测试特定过滤器

可以通过将过滤器名称指定为环境变量来基准测试特定过滤器。例如,要基准测试 MarkdownFilter,请使用

FILTER=MarkdownFilter bin/rake benchmark:banzai

它生成输出

--> Benchmarking MarkdownFilter for FullPipeline
Warming up --------------------------------------
            Markdown   271.000  i/100ms
Calculating -------------------------------------
            Markdown      2.584k (±16.5%) i/s -     23.848k in  10.042503s

从文件和其他数据源读取

Ruby 提供了几个处理文件内容或一般 I/O 流的便利函数。诸如 IO.readIO.readlines 之类的函数使将数据读入内存变得容易,但当数据变大时它们可能效率低下。因为这些函数将数据源的整个内容读入内存,内存使用量至少增加数据源的大小。对于 readlines,它甚至增长更多,因为 Ruby VM 必须执行额外的簿记来表示每一行。

考虑以下程序,它读取磁盘上 750 MB 的文本文件:

File.readlines('large_file.txt').each do |line|
  puts line
end

以下是程序运行时进程内存的读取情况,显示我们确实将整个文件保存在内存中(RSS 以千字节为单位报告):

$ ps -o rss -p <pid>

RSS
783436

以下是垃圾收集器正在做什么的摘录:

pp GC.stat

{
 :heap_live_slots=>2346848,
 :malloc_increase_bytes=>30895288,
 ...
}

我们可以看到 heap_live_slots(可到达对象数)跳到 ~2.3M,这比逐行读取文件大约高出两个数量级。不仅仅是原始内存使用量增加,垃圾收集器(GC)也响应这种变化,预期未来的内存使用。我们可以看到 malloc_increase_bytes 跳到 ~30 MB,与"新鲜"Ruby 程序的 ~4 kB 相比。这个数字指定了 Ruby GC 在下次内存不足时从操作系统声明多少额外堆空间。我们不仅占用了更多内存,还改变了应用程序的行为,以更快地增加内存使用。

IO.read 函数表现出类似的行为,区别在于没有为每个行对象分配额外内存。

建议

与其将数据源完整读入内存,不如逐行读取。这并不总是可行的,例如当您需要将 YAML 文件转换为 Ruby Hash 时,但每当您有数据,其中每一行代表可以处理然后丢弃的某个实体时,您可以使用以下方法。

首先,将 readlines.each 的调用替换为 eacheach_lineeach_lineeach 函数逐行读取数据源,而不将已访问的行保留在内存中:

File.new('file').each { |line| puts line }

或者,您可以使用 IO.readlineIO.gets 函数显式读取单独的行:

while line = file.readline
   # 处理行
end

如果存在允许提前退出循环的条件,这可能更可取,不仅可以节省内存,还可以节省在处理您不感兴趣的行时在 CPU 和 I/O 上花费的不必要时间。

反模式

这是一组 反模式 集合,除非这些变更对生产环境有可测量、显著和积极的影响,否则应避免。

将分配移至常量

将对象存储为常量以便只分配一次可能会提高性能,但这不能保证。查找常量会影响运行时性能,因此使用常量而不是直接引用对象甚至可能减慢代码速度。例如:

SOME_CONSTANT = 'foo'.freeze

9000.times do
  SOME_CONSTANT
end

您应该这样做的唯一原因是防止某人变异全局字符串。然而,由于您可以在 Ruby 中重新分配常量,没有什么可以阻止有人在代码的其他地方这样做:

SOME_CONSTANT = 'bar'

如何用数百万行种子数据库

您可能希望在本地数据库中有数百万个项目行,例如,比较相对查询性能,或重现错误。您可以通过 SQL 命令手动执行此操作,或使用 批量插入 Rails 模型 功能。

假设您正在使用 ActiveRecord 模型,您可能还会发现这些链接有帮助:

示例

您可能会在 此代码片段 中找到一些有用的示例。

ExclusiveLease

Gitlab::ExclusiveLease 是一个基于 Redis 的锁定机制,让开发者能够在分布式服务器上实现互斥。有几个包装器可供开发者使用:

  1. Gitlab::ExclusiveLeaseHelpers 模块提供了一个辅助方法,可以阻塞进程或线程,直到租约可以过期。
  2. ExclusiveLease::Guard 模块帮助为正在运行的代码块获取独占租约。

您不应在数据库事务中使用 ExclusiveLease,因为任何慢速 Redis I/O 都可能增加空闲事务持续时间。.try_obtain 方法检查租约尝试是否在任何数据库事务中,并在 Sentry 和 log/exceptions_json.log 中跟踪异常。

在测试或开发环境中,任何在数据库事务中的租约尝试都会引发 Gitlab::ExclusiveLease::LeaseWithinTransactionError,除非在 Gitlab::ExclusiveLease.skipping_transaction_check 块中执行。您应尽可能在规范中使用跳过功能,并将其放置在靠近租约的位置以便于理解。为了保持规范 DRY,代码库中有两个部分重用了事务检查跳过:

  1. Users::Internal 被修补以在 let_it_be 中为 bot 创建跳过事务检查。
  2. :deploy_keyFactoryBot 工厂在 DeployKey 模型创建期间跳过事务。

任何在非规范或非夹具文件中使用 Gitlab::ExclusiveLease.skipping_transaction_check 的内容都应包含指向 infradev 问题的链接,以计划删除它。