빌드 성능 분석

문제 신고 소스 보기

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 벤치는 이 측정항목에 대한 벤치마크도 제공합니다.
    • 또한 peak-heap-size, max-heap-size, used-heap-sizecommitted-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) 영구 작업자가 사용하는 메모리 양을 줄이는 데 도움이 될 수 있습니다. Google은 Bazel의 리소스 관리자 및 스케줄러를 더 스마트하게 만들어 향후 이러한 조정이 미세하게 줄어들도록 적극적으로 노력하고 있습니다.

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

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

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

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

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

그림 3. 시스템 전체의 네트워크 사용량을 포함하는 프로필

원격 실행 사용 시 네트워크 사용량은 많지만 네트워크 사용량은 빌드에서 병목 현상을 의미할 수 있습니다. 아직 사용하지 않는다면 --remote_download_minimal을 전달하여 바이트 없이 빌드를 사용 설정하는 것이 좋습니다. 이렇게 하면 불필요한 중간 아티팩트의 다운로드를 방지하여 빌드 속도를 높일 수 있습니다.

또 다른 옵션은 로컬 디스크 캐시를 구성하여 다운로드 대역폭을 절약하는 것입니다.