빌드 성능 분석

문제 신고 소스 보기 Nightly · 7.4 .

Bazel은 복잡하며 빌드 과정에서 여러 가지 작업을 실행하며, 그중 일부는 빌드 성능에 영향을 줄 수 있습니다. 이 페이지에서는 이러한 Bazel 개념 중 일부를 빌드 성능에 미치는 영향과 매핑하려고 합니다. 광범위하지는 않지만 측정항목 추출을 통해 빌드 성능 문제를 감지하는 방법과 문제를 해결하기 위해 취할 수 있는 조치의 예를 몇 가지 포함했습니다. 이를 통해 빌드 성능 회귀를 조사할 때 이러한 개념을 적용할 수 있기를 바랍니다.

클린 빌드와 증분 빌드 비교

클린 빌드는 모든 것을 처음부터 빌드하는 반면 증분 빌드는 이미 완료된 작업을 재사용합니다.

특히 Bazel 캐시의 상태에 종속된 측정항목 (예: 빌드 요청 크기 측정항목)을 수집 / 집계하는 경우 클린 빌드와 증분 빌드를 별도로 살펴보는 것이 좋습니다. 또한 두 가지 사용자 환경을 나타냅니다. 클린 빌드를 처음부터 시작하는 것 (콜드 캐시로 인해 시간이 더 오래 걸림)과 비교하면 개발자가 코드를 반복할 때 증분 빌드가 훨씬 더 자주 발생합니다 (일반적으로 캐시가 이미 웜 상태이므로 더 빠름).

BEP의 CumulativeMetrics.num_analyses 필드를 사용하여 빌드를 분류할 수 있습니다. num_analyses <= 1인 경우 클린 빌드이고, 그렇지 않은 경우 대략적으로 증분 빌드일 가능성이 높습니다. 사용자가 다른 플래그나 다른 타겟으로 전환하여 효과적으로 클린 빌드를 실행했을 수 있기 때문입니다. 성과 증분에 대한 더 엄격한 정의는 휴리스틱의 형태로 제공되어야 할 수 있습니다. 예를 들어 로드된 패키지 수(PackageMetrics.packages_loaded)를 확인해야 합니다.

빌드 성능의 프록시로서 결정론적 빌드 측정항목

특정 측정항목 (예: Bazel의 CPU 시간 또는 원격 클러스터의 대기열 시간)은 비결정론적 특성으로 인해 빌드 성능을 측정하기가 어려울 수 있습니다. 따라서 Bazel에서 실행한 작업량의 대리 항목으로 결정론적 측정항목을 사용하는 것이 유용할 수 있으며, 이는 실적에 영향을 미칩니다.

빌드 요청의 크기는 빌드 성능에 큰 영향을 미칠 수 있습니다. 빌드가 클수록 빌드 그래프를 분석하고 구성하는 데 더 많은 작업이 필요할 수 있습니다. 종속 항목이 더 많이 추가/생성되어 빌드의 복잡성이 증가하고 빌드 비용이 더 많이 들게 되면 빌드가 자연스럽게 증가합니다.

이 문제를 다양한 빌드 단계로 나누고, 각 단계에서 실행되는 작업의 프록시 측정항목으로 다음 측정항목을 사용할 수 있습니다.

  1. PackageMetrics.packages_loaded: 로드된 패키지 수입니다. 여기서 회귀는 로드 단계에서 각 추가 BUILD 파일을 읽고 파싱하기 위해 더 많은 작업이 필요하다는 것을 나타냅니다.

    • 이는 종속 항목을 추가하고 그들의 전이 폐쇄를 로드해야 하기 때문인 경우가 많습니다.
    • query / cquery를 사용하여 새 종속 항목이 추가되었을 수 있는 위치를 찾습니다.
  2. TargetMetrics.targets_configured: 빌드에 구성된 타겟 및 측면 수를 나타냅니다. 회귀는 구성된 타겟 그래프를 구성하고 순회하는 데 있어 더 많은 작업을 의미합니다

    • 이는 종속 항목이 추가되고 전이 종결 그래프를 구성해야 하기 때문에 발생하는 경우가 많습니다.
    • cquery를 사용하여 새 종속 항목이 추가되었을 수 있는 위치를 찾습니다.
  3. ActionSummary.actions_created: 빌드에서 생성된 작업을 나타내며 회귀는 작업 그래프를 구성할 때 추가 작업을 나타냅니다. 여기에는 실행되지 않았을 수 있는 사용되지 않는 작업도 포함됩니다.

  4. ActionSummary.actions_executed: 실행된 작업 수입니다. 회귀는 이러한 작업을 실행하는 데 더 많은 작업이 직접적으로 발생했음을 나타냅니다.

    • BEP는 가장 많이 실행된 작업 유형을 보여주는 작업 통계 ActionData를 작성합니다. 기본적으로 상위 20개 작업 유형을 수집하지만 --experimental_record_metrics_for_all_mnemonics를 전달하여 실행된 모든 작업 유형에 대해 이 데이터를 수집할 수 있습니다.
    • 그러면 어떤 종류의 작업이 추가로 실행되었는지 파악하는 데 도움이 됩니다.
  5. BuildGraphSummary.outputArtifactCount: 실행된 작업으로 생성된 아티팩트 수입니다.

    • 실행된 작업 수가 증가하지 않았다면 규칙 구현이 변경되었을 가능성이 큽니다.

이러한 측정항목은 모두 로컬 캐시의 상태에 영향을 받으므로 이러한 측정항목을 추출하는 빌드가 클린 빌드인지 확인해야 합니다.

이러한 측정항목의 회귀는 실행 시간, CPU 시간, 메모리 사용량의 회귀를 동반할 수 있습니다.

로컬 리소스 사용

Bazel은 로컬 머신에서 다양한 리소스를 사용합니다 (빌드 그래프 분석 및 실행 유도, 로컬 작업 실행 모두). 이로 인해 빌드 및 기타 작업을 실행할 때 머신의 성능 / 가용성에 영향을 줄 수 있습니다.

소요 시간

노이즈에 가장 취약한 (빌드마다 크게 다를 수 있는) 측정항목은 시간, 특히 실제 경과 시간, CPU 시간, 시스템 시간입니다. bazel-bench를 사용하여 이러한 측정항목의 벤치마크를 가져올 수 있으며, 충분한 수의 --runs를 사용하면 측정의 통계적 유의성을 높일 수 있습니다.

  • 실제 경과 시간은 실제 경과 시간입니다.

    • 실행 시간이 회귀하는 경우 JSON 트레이스 프로필을 수집하고 차이를 확인하는 것이 좋습니다. 그렇지 않은 경우, 다른 하락한 측정항목을 조사하는 것이 더 효율적일 수 있습니다. 이러한 측정항목이 실행 시간에 영향을 미쳤을 수 있기 때문입니다.
  • CPU 시간은 CPU가 사용자 코드를 실행하는 데 소요된 시간입니다.

    • 두 프로젝트 커밋에서 CPU 시간이 감소하는 경우 Starlark CPU 프로필을 수집하는 것이 좋습니다. 또한 --nobuild를 사용하여 빌드를 분석 단계로 제한해야 할 수도 있습니다. 분석 단계에서 대부분의 CPU 작업이 실행되기 때문입니다.
  • 시스템 시간은 커널에서 CPU가 소비한 시간입니다.

    • 시스템 시간이 회귀하는 경우 이는 대부분 Bazel이 파일 시스템에서 파일을 읽을 때의 I/O와 관련이 있습니다.

시스템 전체 로드 프로파일링

Bazel 6.0에서 도입된 --experimental_collect_load_average_in_profiler 플래그를 사용하여 JSON 트레이스 프로파일러는 호출 중에 시스템 부하 평균을 수집합니다.

시스템 부하 평균이 포함된 프로필

그림 1. 시스템 부하 평균이 포함된 프로필입니다.

Bazel 호출 중에 부하가 높으면 Bazel이 머신에 너무 많은 로컬 작업을 동시에 예약하고 있음을 나타낼 수 있습니다. 특히 컨테이너 환경에서 (적어도 #16512가 병합될 때까지는) --local_cpu_resources--local_ram_resources를 조정하는 방법을 살펴볼 수 있습니다.

Bazel 메모리 사용량 모니터링

Bazel의 메모리 사용량을 가져오는 두 가지 기본 소스는 Bazel infoBEP입니다.

  • bazel info used-heap-size-after-gc: System.gc() 호출 후 사용된 메모리 양(바이트)입니다.

    • Bazel bench도 이 측정항목의 벤치마크를 제공합니다.
    • 또한 peak-heap-size, max-heap-size, used-heap-size, committed-heap-size (문서 참고)가 있지만 관련성은 낮습니다.
  • BEPMemoryMetrics.peak_post_gc_heap_size: GC 후의 최대 JVM 힙 크기(바이트 단위)입니다(전체 GC를 강제로 적용하려고 하는 --memory_profile 설정 필요).

메모리 사용량의 회귀는 일반적으로 빌드 요청 크기 측정항목의 회귀로 인한 결과이며, 이는 종속 항목 추가 또는 규칙 구현 변경으로 인한 경우가 많습니다.

Bazel의 메모리 공간을 보다 세분화된 수준에서 분석하려면 규칙에 기본 제공 메모리 프로파일러를 사용하는 것이 좋습니다.

영구 작업자의 메모리 프로파일링

지속형 작업자는 특히 인터프리트된 언어의 경우 빌드 속도를 크게 높일 수 있지만 메모리 사용량이 문제가 될 수 있습니다. Bazel은 작업자에 관한 측정항목을 수집합니다. 특히 WorkerMetrics.WorkerStats.worker_memory_in_kb 필드는 작업자가 사용하는 메모리 양을 니모닉으로 나타냅니다.

JSON 트레이스 프로파일러는 호출 중에 --experimental_collect_system_network_usage 플래그 (Bazel 6.0의 새로운 기능)를 전달하여 영구 워커 메모리 사용량도 수집합니다.

작업자 메모리 사용량이 포함된 프로필

그림 2. 작업자 메모리 사용량이 포함된 프로필입니다.

--worker_max_instances 값을 낮추면(기본값 4) 영구 작업자가 사용하는 메모리 양을 줄이는 데 도움이 될 수 있습니다. Bazel의 리소스 관리자 및 스케줄러를 더욱 스마트하게 만들어 향후 이러한 미세 조정 필요성을 줄이기 위해 적극적으로 노력하고 있습니다.

원격 빌드의 네트워크 트래픽 모니터링

원격 실행에서 Bazel은 작업 실행의 결과로 빌드된 아티팩트를 다운로드합니다. 따라서 네트워크 대역폭이 빌드 성능에 영향을 미칠 수 있습니다.

빌드에 원격 실행을 사용하는 경우 BEPNetworkMetrics.SystemNetworkStats 프로토를 사용하여 호출 중에 네트워크 트래픽을 모니터링하는 것이 좋습니다(--experimental_collect_system_network_usage 전달 필요).

또한 JSON 트레이스 프로필을 사용하면 --experimental_collect_system_network_usage 플래그 (Bazel 6.0의 새로운 기능)를 전달하여 빌드 과정에서 시스템 전체의 네트워크 사용량을 볼 수 있습니다.

시스템 전체 네트워크 사용량이 포함된 프로필

그림 3. 시스템 전체 네트워크 사용량이 포함된 프로필입니다.

원격 실행을 사용할 때 네트워크 사용량이 높지만 평탄한 경우 네트워크가 빌드의 병목 현상일 수 있습니다. 아직 사용하지 않는 경우 --remote_download_minimal를 전달하여 Bytes 없이 빌드를 사용 설정해 보세요. 이렇게 하면 불필요한 중간 아티팩트의 다운로드를 방지하여 빌드 속도가 빨라집니다.

다운로드 대역폭을 절약하기 위해 로컬 디스크 캐시를 구성하는 것도 한 가지 방법입니다.