빌드 성능 분석

문제 신고하기 소스 보기

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와 상관관계가 있습니다.

시스템 전체 부하 프로파일링

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

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

그림 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-size, committed-heap-size (문서 참고)도 있지만 관련성이 낮습니다.
  • BEPMemoryMetrics.peak_post_gc_heap_size: GC 후 최대 JVM 힙 크기(바이트)입니다(전체 GC를 강제하려는 --memory_profile을 설정해야 함).

메모리 사용량의 회귀는 일반적으로 종속 항목이 추가되거나 규칙 구현의 변경으로 인해 발생하는 빌드 요청 크기 측정항목의 회귀로 인해 발생합니다.

Bazel의 메모리 공간을 더 세분화하여 분석하려면 규칙에 내장 메모리 프로파일러를 사용하는 것이 좋습니다.

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

영구 작업자는 빌드 속도를 크게 높이는 데 도움이 될 수 있지만 (특히 인터프리트 언어의 경우) 메모리 공간은 문제가 될 수 있습니다. Bazel은 작업자의 측정항목을 수집합니다. 특히 WorkerMetrics.WorkerStats.worker_memory_in_kb 필드는 작업자가 사용하는 메모리 양을 (니모닉별로) 알려줍니다.

JSON 트레이스 프로파일러는 또한 Bazel 6.0의 새로운 기능인 --experimental_collect_system_network_usage 플래그를 전달하여 호출 중에 영구 작업자 메모리 사용량을 수집합니다.

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

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

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

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

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

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

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

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

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

원격 실행을 사용할 때 네트워크 사용량이 많지만 다소 균일하지 않다면 네트워크가 빌드의 병목 현상임을 나타낼 수 있습니다. 아직 네트워크를 사용하고 있지 않다면 --remote_download_minimal를 전달하여 Bytes 없이 Build를 사용 설정하는 것이 좋습니다. 이렇게 하면 불필요한 중간 아티팩트의 다운로드를 방지하여 빌드 속도를 높일 수 있습니다.

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