규칙 작성의 어려움

문제 신고 소스 보기 1박 · 7.3 · 7.2 · 7.1 · 7.0 · 6.5

이 페이지에서는 구체적인 문제와 과제를 개략적으로 설명합니다. Bazel 규칙을 작성하는 방법을 배웠습니다

요약 요구사항

  • 가정: 정확성, 처리량, 사용 편의성 및 지연 시간
  • 가정: 대규모 저장소
  • 가정: BUILD와 유사한 설명 언어
  • 기록: 로드, 분석, 실행 간의 하드 분리는 오래되었지만 API에 영향을 줍니다.
  • 내장 기능: 원격 실행 및 캐싱이 어려움
  • 내장 기능: 정확하고 빠른 증분 빌드를 위해 변경 정보 사용 비정상적인 코딩 패턴 필요
  • 내장 기능: 이차 시간 및 메모리 소비를 피하기 어려움

가정

다음은 빌드 시스템에 대한 몇 가지 가정입니다. 정확성, 사용 용이성, 처리량, 대규모 리포지터리를 지원할 수 있습니다. 이 다음 섹션에서는 이러한 가정을 다루고 이러한 가정을 뒷받침하기 위한 가이드라인을 규칙을 효과적으로 작성하는 방법을 배웁니다.

정확성, 처리량, 사용 편의성 및 지연 시간

빌드 시스템이 가장 먼저 정확해야 하고 관련 문제가 있습니다 주어진 소스 트리의 경우 출력 트리 형태에 상관없이 동일한 빌드는 항상 동일해야 함 좋습니다. 첫 번째 근사치에서는 Bazel이 모든 특정 빌드 단계로 이동하는 입력(있는 경우 해당 단계를 다시 실행할 수 있음) 알 수 있습니다. Bazel이 유출될 수 있으므로 수정 방법에 한계가 있습니다 빌드 날짜 / 시간과 같은 일부 정보를 수정하고 특정 유형의 파일 속성 변경과 같은 변경사항을 방지할 수 있습니다 샌드박스 선언되지 않은 입력 파일에 대한 읽기를 방지하여 정확성을 보장합니다. 외 시스템의 근본적인 한계, 몇 가지 알려진 정확성 문제가 있습니다. 대부분은 어려운 파일 세트 또는 C++ 규칙과 관련이 있습니다. 문제를 해결하는 데 도움이 됩니다 Google에서는 이 문제를 해결하기 위해 장기적인 노력을 기울이고 있습니다.

빌드 시스템의 두 번째 목표는 높은 처리량입니다. 우리는 할 수 있는 일의 경계를 영구적으로 확대하는 것은 원격 실행 서비스에 할당할 수 있습니다. 원격 실행이 서비스가 과부하되면 아무도 작업을 수행할 수 없습니다.

사용 편의성은 그 다음입니다. 동일한 (또는 원격 실행 서비스의 풋프린트에서 실행된다면, 우리는 원격 실행 서비스인 더 쉽게 사용할 수 있습니다.

지연 시간은 빌드 시작부터 의도한 결과를 얻기까지 걸리는 시간을 나타냄 성공 또는 실패한 테스트의 테스트 로그든 오류 메시지든 BUILD 파일에 오타가 있다는 메시지가 표시됩니다.

이러한 목표는 종종 중복됩니다. 지연 시간은 처리량이 많은 처리량과 원격 실행 서비스의 일부로 제한해야 합니다.

대규모 저장소

빌드 시스템은 규모는 단일 하드 드라이브에 맞지 않는다는 것을 의미하므로 사실상 모든 개발자 컴퓨터에서 전체 결제를 진행합니다. 중간 크기의 빌드 BUILD 파일 수만 개를 읽고 파싱하여 수십만 개의 glob 이론적으로는 모든 텍스트 텍스트를 읽는 것이 가능하지만 BUILD 파일이 있는 경우 아직 일괄 작업을 실행할 수 없습니다. 만들 수 있습니다 따라서 BUILD 파일이 독립적으로 로드되고 파싱될 수 있습니다

BUILD와 유사한 설명 언어

이 문맥에서는 라이브러리 및 바이너리 규칙 선언의 BUILD 파일과 대체로 유사함 상호 의존성을 나타냅니다 BUILD 파일은 독립적으로 읽고 파싱될 수 있습니다. 그리고 가능한 경우 항상 소스 파일을 보지 않습니다( 존재).

고어

Bazel 버전 간에는 문제를 일으키는 버전과 Bazel 버전 간에 다음 섹션에서 설명합니다.

로드, 분석, 실행을 엄격하게 분리하는 것은 오래되었지만 API에 여전히 영향을 미칩니다.

기술적으로는 규칙으로 작업을 원격 실행으로 보내기 직전에 수행할 수 있습니다. 그러나 원래의 Bazel 코드베이스는 로드 패키지를 엄격하게 분리한 다음 구성을 사용하여 규칙 분석 (기본적으로 명령줄 플래그) 모든 작업을 실행할 수 있습니다 이러한 구분은 여전히 Rules API의 일부입니다. Bazel의 핵심에는 더 이상 필요하지 않지만 (자세한 내용은 아래 참조).

즉, 규칙 API에는 규칙에 대한 선언적 설명이 필요합니다. 인터페이스 (어떤 속성, 속성 유형)를 사용하는지를 보여 주어야 합니다. 몇 가지는 API에서 로드 단계 중에 커스텀 코드를 실행하여 출력 파일의 암시적 이름과 특성의 암시적 값을 계산합니다. 대상 'foo'라는 java_library 규칙을 예로 들 수 있습니다. 암시적으로 빌드 그래프의 다른 규칙에서 참조할 수 있는 'libfoo.jar'.

또한 규칙 분석은 어떠한 소스 파일도 읽거나 작업의 출력 대신 부분 방향성 2분자를 생성해야 함 규칙에서만 결정되는 빌드 단계 및 출력 파일 이름의 그래프 자체 및 그 종속 항목이 포함됩니다

내적

규칙 작성을 어렵게 만드는 몇 가지 내재적 속성이 있습니다. 다음 섹션에서는 가장 일반적인 몇 가지를 설명합니다.

원격 실행 및 캐싱은 어려움

원격 실행 및 캐싱은 다음을 통해 대규모 저장소의 빌드 시간을 개선합니다. 단일 클러스터에서 빌드를 실행하는 것에 비해 약 두 자릿수의 가상 머신을 만드는 법을 배웠습니다 하지만 실행에 필요한 규모는 실로 엄청납니다. Google의 원격 실행 서비스는 각 애플리케이션에 대해 많은 수의 요청을 프로토콜은 불필요한 왕복을 신중하게 방지하고 서비스 측의 불필요한 작업을 수행할 수 있습니다

이때 이 프로토콜은 빌드 시스템이 사전에 주어진 조치를 취할 수 있습니다. 그러면 빌드 시스템이 고유한 작업을 계산하고 지문을 스캔하고 스케줄러에 캐시 적중을 요청합니다. 캐시 적중이 발견되면 스케줄러가 출력 파일의 다이제스트로 응답합니다. 파일 자체는 나중에 다이제스트가 처리합니다 그러나 이렇게 하면 Bazel 모든 입력 파일을 미리 선언해야 합니다.

정확하고 빠른 증분 빌드를 위해 변경 정보를 사용하려면 비정상적인 코딩 패턴이 필요함

위에서 우리는 Bazel이 정확하기 위해 모든 입력 값을 알아야 한다고 주장했습니다. 해당 빌드 단계가 유효한지 감지하기 위해 확인해야 합니다 패키지 로딩과 규칙 분석도 마찬가지입니다. 스카이프레임을 설계하여 살펴보겠습니다 스카이프레임은 그래프 라이브러리 및 평가 프레임워크로 목표 노드 (예: 'build //foo with these options')의 하위 클래스로 분할하고 그 구성 부분들을 평가하여 이를 도출합니다. 표시됩니다. 이 프로세스의 일환으로 스카이프레임은 패키지를 읽고, 규칙을 분석하고, 작업을 실행합니다.

각 노드에서 스카이프레임은 특정 노드에서 계산에 사용된 노드를 정확하게 추적합니다. 목표 노드에서 입력 파일( 스카이프레임 노드이기도 합니다. 이 그래프가 메모리에 명시적으로 표시되도록 하면 이를 통해 빌드 시스템은 특정 노드의 영향을 받는 노드를 정확히 식별할 수 있고 변경 (입력 파일 생성 또는 삭제 포함), 출력 트리를 의도한 상태로 복원하기 위한 최소한의 작업량

이 과정에서 각 노드는 종속 항목 검색 프로세스를 수행합니다. 각 노드는 종속 항목을 선언한 다음 해당 종속 항목의 콘텐츠를 사용할 수 있고 더 많은 종속 항목을 선언할 수 있습니다 원칙적으로 이는 노드당 스레드입니다 그러나 중간 크기의 빌드에는 수백 개의 수천 개의 Skyframe 노드를 사용했으며, 이는 현재의 Java 역사적 이유로 우리는 현재 Java를 사용하는 것에 얽매여 있습니다. 따라서 가벼운 스레드가 없고 연속성도 없습니다.

대신 Bazel은 고정 크기 스레드 풀을 사용합니다. 하지만 이는 노드가 종속 항목을 선언하는 경우 평가하고 다시 시작 (다른 스레드에서 가능) 있습니다. 즉, 노드가 과도한 작업을 해서는 안 됩니다. 가 N개의 종속 항목을 순차적으로 선언하는 노드는 잠재적으로 N번 다시 시작할 수 있습니다. O(N^2) 시간이 소요됩니다. 그 대신, '당사자'에 대한 사전 일괄 선언을 목표로 삼고 있습니다. 코드 재구성이 필요하거나 여러 노드로 분할하여 재시작 횟수를 제한할 수 있습니다

이 기술은 현재 Rules API에서 사용할 수 없습니다. 가 아닌 규칙 API는 여전히 로드, 분석, 살펴보겠습니다 그러나 근본적인 제한 사항은 다른 노드는 프레임워크를 통과하여 종속 항목이 포함됩니다 빌드 시스템에서 사용하는 언어와 상관없이 규칙이 구현되거나 작성되는 경우 (반드시 실제 규칙 작성자는 이러한 정책을 우회하는 표준 라이브러리 또는 패턴을 사용해서는 안 됩니다. 스카이프레임 Java의 경우, 이는 java.io.File뿐만 아니라 이러한 작업을 수행하는 모든 라이브러리가 포함됩니다. 종속 항목을 지원하는 라이브러리 이러한 하위 수준 인터페이스를 삽입하려면 스카이프레임

따라서 규칙 작성자가 전체 언어 런타임에 노출되지 않도록 하는 것이 좋습니다. 있습니다. 이러한 API를 실수로 사용할 경우의 위험이 너무 큽니다. 과거에 Bazel 버그가 안전하지 않은 API를 사용하는 규칙으로 인해 발생하였고 Bazel 팀이나 다른 도메인 전문가가 규칙을 작성했지만

이차 시간 및 메모리 소비를 피하기는 어렵습니다.

설상가상으로, Skyframe에서 요구하는 요구사항 외에도 Java 사용에 대한 과거의 제약 조건과 오래된 규칙 API 실수로 이차 시간 또는 메모리 소비를 유도하는 것은 문제가 있을 수 있습니다. 두 가지 이차 메모리 소비를 초래하는 매우 일반적인 패턴은 2차 시간 소비).

  1. 도서관 규칙 체인 - 라이브러리 규칙 A가 B에 종속되고 C에 종속되며 등등. 그런 다음 또는 Java 런타임 클래스 경로 또는 있습니다 간단히 말해 표준 목록 구현을 사용할 수도 있습니다. 하지만 여기에는 이미 이차 메모리 소비가 발생하고 있습니다. 첫 번째 라이브러리인 클래스 경로에 항목 하나가 포함되고 두 번째 항목, 두 번째 항목, 세 번째 항목도 포함됩니다. 즉, 총 1+2+3+...+N = O(N^2)개 항목을 계산합니다.

  2. 동일한 라이브러리 규칙에 따른 바이너리 규칙 - 동일한 라이브러리에 종속되는 바이너리 집합 고려 동일한 규칙을 테스트하는 여러 개의 테스트 규칙이 있는 경우 찾을 수 있습니다. N개의 규칙 중 절반이 이진 규칙이라고 가정하겠습니다. 나머지 절반은 라이브러리 규칙에 따라 다릅니다. 이제 각 바이너리가 라이브러리 규칙의 전이적 폐쇄에 대해 계산된 일부 속성(예: Java 런타임 클래스 경로 또는 C++ 링커 명령줄을 사용할 수 있습니다. 예를 들어 C++ 링크 작업의 명령줄 문자열 표현을 확장할 수 있습니다. 해당 사항 없음 N/2 요소의 사본은 O(N^2) 메모리입니다.

이차 복잡성을 방지하는 맞춤 컬렉션 클래스

Bazel은 이 두 시나리오 모두의 영향을 많이 받으므로 압축하는 방법을 사용하여 메모리의 정보를 효과적으로 압축하는 복사를 피하는 것이 좋습니다 이러한 데이터 구조는 대부분 시맨틱스라고 부르기 때문에 depset (내부 구현에서는 NestedSet라고도 함) 대부분의 지난 몇 년간 Bazel의 메모리 소비를 줄이기 위한 변화는 이전에 사용된 것 대신 depset를 사용하도록 변경합니다.

안타깝게도 depset을 사용한다고 해서 모든 문제가 자동으로 해결되지는 않습니다. 특히 각 규칙에서 간극을 반복하기만 해도 2차 시간 소비가 있습니다. 내부적으로 NestedSets에는 도우미 메서드도 있습니다. 일반 컬렉션 클래스와의 상호 운용성을 용이하게 하기 위해 안타깝게도 실수로 이러한 메서드 중 하나에 NestedSet를 전달하면 2차 메모리 소비를 다시 도입합니다.