프로파일링
GitLab v19.1성능 문제를 보다 쉽게 추적할 수 있도록 GitLab은 다양한 프로파일링 도구를 제공합니다. 일부 도구는 기본적으로 사용 가능하며, 나머지는 명시적으로 활성화해야 합니다. `Gitlab::Profiler.profile` 메서드와 이에 대응하는 `bin/profile-url` 스크립트를 사용하면
# 프로파일링
성능 문제를 보다 쉽게 추적할 수 있도록 GitLab은 다양한 프로파일링 도구를 제공합니다.
일부 도구는 기본적으로 사용 가능하며, 나머지는 명시적으로 활성화해야 합니다.
## URL 프로파일링
`Gitlab::Profiler.profile` 메서드와 이에 대응하는 `bin/profile-url` 스크립트를 사용하면
특정 URL에 대한 GET 또는 POST 요청을 프로파일링할 수 있습니다.
익명 사용자(기본값) 또는 특정 사용자로 프로파일링할 수 있습니다.
프로파일러의 첫 번째 인수는 인스턴스 호스트명을 포함한 전체 URL이거나,
앞에 슬래시가 붙는 절대 경로입니다.
기본적으로 보고서 덤프는 임시 파일에 저장되며,
[Stackprof API](/19.1/development/profiling/#reading-a-gitlabprofiler-report)를 사용하여 상호작용할 수 있습니다.
스크립트를 사용할 때는 인수를 전달하지 않으면 명령줄 도움말을 확인할 수 있습니다.
대화형 콘솔 세션에서 메서드를 사용하면, 해당 콘솔 세션 내의 애플리케이션 코드 변경 사항이
프로파일러 출력에 반영됩니다.
예시:
```
Gitlab::Profiler.profile('/my-user')
# Returns the location of the temp file where the report dump is stored
class UsersController; def show; sleep 100; end; end
Gitlab::Profiler.profile('/my-user')
# Returns the location of the temp file where the report dump is stored
# where 100 seconds is spent in 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))#
샘플링 데이터의 출력 파일(`out`)을 구성하려면 `profiler_options` 해시를 전달합니다. 예시:
```
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 in44 (0.5%) 44 (0.5%) Sprockets::Base#normalize_logical_path#
플레임그래프를 생성할 수도 있습니다:
```
stackprof --d3-flamegraph tmp/profile.dump > flamegraph.html#
자세한 내용은 Stackprof 문서를 참조하세요.
## Speedscope 플레임그래프
성능 바에서 플레임그래프 샘플링 모드 버튼을 선택하거나,
요청에 `performance_bar=flamegraph` 파라미터를 추가하여
특정 URL에 대한 플레임그래프를 생성할 수 있습니다.
[
](/19.1/development/img/speedscope_v13_12.png)
뷰에 대한 자세한 정보는 Speedscope 문서를 참조하세요.
다양한 샘플링 모드에 대한 자세한 정보는 Stackprof 문서를 참조하세요.
이 기능은 성능 바에 접근할 수 있는 모든 사용자에게 활성화됩니다.
## Bullet
Bullet은 N+1 쿼리 문제를 추적하는 데 사용할 수 있는 Gem입니다.
쿼리 문제를 Rails 로그와 브라우저 콘솔에 기록합니다.
Bullet 섹션은 [성능 바](/19.1/administration/monitoring/performance/performance_bar/)에 표시됩니다.
[
](/19.1/development/img/bullet_v13_0.png)
Bullet은 기본적으로 개발 모드에서만 활성화됩니다. 그러나 Bullet 로깅은 노이즈가 많기 때문에 로깅은 비활성화되어 있습니다.
Bullet과 그 로깅을 구성하려면 다음과 같이 합니다:
- 환경에서 Bullet을 수동으로 활성화 또는 비활성화하려면 `config/gitlab.yml`에 다음 줄을 추가하고
```
bullet:
enabled: false#
- Bullet 로깅을 활성화하려면 GitLab을 시작하기 전에 `ENABLE_BULLET` 환경 변수를
```
ENABLE_BULLET=true bundle exec rails s#
Bullet로 `N+1` 쿼리를 발견한 후속 조치로,
회귀를 방지하기 위해 [QueryRecorder 테스트](/19.1/development/database/query_recorder/) 작성을 고려해 보세요.
## 시스템 통계
프로파일링 중이나 이후에 메모리 소비량, CPU 소요 시간, 가비지 컬렉터 통계 등
Ruby 가상 머신 프로세스에 대한 상세 정보를 얻고 싶을 수 있습니다.
이러한 정보는 다양한 도구를 통해 개별적으로 쉽게 생성할 수 있지만,
편의를 위해 이 데이터를 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 웹 워커에서만 사용 가능합니다. Sidekiq 워커는 이 방법으로 검사할 수 없습니다.
## 성능에 영향을 미치는 설정
### 애플리케이션 설정
- `development` 환경은 기본적으로 핫 리로딩이 활성화된 상태로 작동합니다.
핫 리로드가 단일 스레드이기 때문에 잠재적인 경쟁 조건 잠금이 발생할 수 있습니다.
- `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 (비활성화) | | 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은 애플리케이션 성능 향상, 메모리 요구 사항 감소 또는 둘 다를 위해
이러한 설정을 변경하기로 결정할 수 있습니다.
유휴 상태의 GitLab 인스턴스에서 이러한 각 설정이 GC 성능, 메모리 사용량 및
애플리케이션 시작 시간에 어떤 영향을 미치는지는 `scripts/perf/gc/collect_gc_stats.rb` 스크립트를 실행하여 확인할 수 있습니다.
GC 통계 및 일반 타이밍 데이터를 CSV 형식으로 표준 출력에 출력합니다.
## 성능 이슈 조사 예시
Pipeline Authoring 팀은 파이프라인 생성 성능 이슈를 해결하기 위해
[stackprof 플레임그래프](/19.1/development/profiling/#speedscope-flamegraphs) 및 [`memory_profiler`](/19.1/development/performance/#using-memory-profiler)와 같은 기존 프로파일링 방법과
새로운 방법인 `ruby-prof`를 함께 사용했습니다.
### stackprof 플레임그래프 사용
[성능 바](/19.1/administration/monitoring/performance/performance_bar/)는 stackprof 보고서를 얻고
클릭 한 번으로 플레임그래프를 볼 수 있는 훌륭한 도구입니다;
[
](/19.1/development/img/performance_bar_flamegraph_link_v18_4.png)
그러나 GET 요청 이외에는 사용할 수 없습니다.
POST 요청에 대한 플레임그래프를 얻으려면 API 요청에 `performance_bar=flamegraph` 파라미터를 사용합니다.
이 경우 [머지 리퀘스트의 파이프라인 생성 엔드포인트](/19.1/api/merge_requests/#create-merge-request-pipeline)에 대한 플레임그래프를 확인하고자 합니다.
일반적으로 다음 명령을 사용하여 stackprof 보고서를 JSON 파일로 가져올 수 있지만,
`Gitlab::PerformanceBar.allowed_for_user?(request.env['warden']&.user)` 사용자 제어로 인해
웹 인터페이스를 통해 인증된 사용자만 허용됩니다.
```
# This will not work on production
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`로 복사하여 터미널에서 사용합니다.
[
](/19.1/development/img/performance_copy_as_curl_v17_7.png)
다음과 같은 `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` 파라미터를 추가해야 합니다.
- JSON 응답을 파일로 저장하기 위해 `--output flamegraph.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'#
그런 다음 `flamegraph.json` 파일을 `https://www.speedscope.app/` 웹사이트에서 사용하여 플레임그래프를 확인합니다.
[
](/19.1/development/img/performance_speedscope_example_v17_7.png)
예를 들어, 이 speedscope 플레임그래프를 조사할 때 `kubernetes_variables` 메서드가
많은 시간을 소비하는 것을 확인하고 이슈를 생성했습니다.
[
](/19.1/development/img/performance_speedscope_example_kubernetes_v17_7.png)
### ruby-prof 사용
시간이 가장 많이 소요되는 곳을 확인하는 또 다른 방법은 `ruby-prof`를 사용하는 것입니다.
Gemfile에 포함된 gem이 아니므로 먼저 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! # see 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) .payloadend; nil
result = profile.stop
callstack_printer = RubyProf::CallStackPrinter.new(result)
File.open('tmp/ruby-prof-callstack-report.html', 'w') do
callstack_printer.print(file)end
::Ci::DestroyPipelineService.new(project, user).execute(Ci::Pipeline.last)#
[
](/19.1/development/img/performance_ruby-prof_example_v17_7.png)
여기서 `Ci::GenerateKubeconfigService`가 약 2,000번 호출되는 것을 확인할 수 있습니다.
이는 추가 조사가 필요하다는 좋은 지표입니다.
### memory_profiler 사용
[`memory_profiler`](/19.1/development/performance/#using-memory-profiler)는 메모리 사용량을 프로파일링하는 도구입니다.
높은 메모리 사용량은 성능 이슈로 이어질 수 있으므로 이 또한 중요합니다.
`stackprof`에서 했던 것처럼 `performance_bar` 파라미터와 함께 `curl`을 사용할 수도 있습니다.
```
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초 타임아웃이 있기 때문에 프로덕션에서는 작동하지 않습니다.
따라서 메모리 프로파일을 얻으려면 개발 환경을 사용해야 합니다.
자세한 정보는 [메모리 프로파일러 문서](/19.1/development/performance/#using-memory-profiler)에서 확인할 수 있습니다.
```
# 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)
# Warmup
Ci::CreatePipelineService
.new(project, user, ref: merge_request.source_branch) .execute(:merge_request_event, merge_request: merge_request); nilreport = 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 endend; nil
output = File.open('tmp/memory-profile-report.txt', 'w')
report.pretty_print(output, detailed_report: true, scale_bytes: true, normalize_paths: true)#
결과:
```
#
# Note: I redacted some parts related to the gems and the Rails framework.
# also, the output is shortened for readability.
#
Total allocated: 1.30 GB (12974240 objects)
Total retained: 29.67 MB (335085 objects)
allocated memory by gem#
675.48 MB gitlab/lib ...allocated memory by file#
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 ...allocated memory by location#
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...
이 예시에서는 파일 및 위치별 할당된 메모리를 확인하여 메모리 사용량을 최적화할 수 있는 곳을 파악합니다.
그리고 최근 작업에서
메모리 사용량을 개선하는 방법을 찾아 다음 결과를 얻었습니다;
```
#
# Note: I redacted some parts related to the gems and the Rails framework.
# also, the output is shortened for readability.
#
Total allocated: 1.08 GB (11171148 objects)
Total retained: 29.67 MB (335082 objects)
allocated memory by gem#
495.88 MB gitlab/lib ...allocated memory by file#
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.