빌드 성능 분석

문제 신고하기 소스 보기

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 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 proto를 사용하여 호출 중에 네트워크 트래픽을 모니터링하는 것이 좋습니다(--experimental_collect_system_network_usage 전달 필요).

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

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

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

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

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