This page contains information related to upcoming products, features, and functionality. It is important to note that the information presented is for informational purposes only. Please do not rely on this information for purchasing or planning purposes. The development, release, and timing of any products, features, or functionality may be subject to change or delay and remain at the sole discretion of GitLab Inc.
Status Authors Coach DRIs Owning Stage Created
proposed @qmnguyen0711 devops enablement 2023-05-30

Gitaly 적응형 동시성 제한

요약

Gitaly는 Git 서버로, 사건 발생 위험을 줄이기 위해 클라이언트에게 밀어 넣어야 합니다. 이전에는 각각의 RPC 동시성 한도와 pack-objects 동시성 한도를 도입했었습니다. 이러한 시스템들은 성공적이었지만, 설정은 정적이었기 때문에 심각한 단점이 있었습니다. 본 청사진은 정적 제한의 단점을 극복하기 위해 적응형 동시성 제한 시스템을 제안합니다. 이 알고리즘은 주로 가산 증가/곱셈 감소 접근을 사용하여 일반 처리 중에 제한을 서서히 증가시키고 사건 발생 시에는 빠르게 줄이는 것을 목표로 합니다. 이 알고리즘은 Gitaly가 리소스 부족과 심각한 지연 감소를 기준으로 트러블이 있는지를 판단합니다.

동기

사건 발생의 위험을 줄이고 스스로를 보호하기 위해 Gitaly는 일부 제한이 도달되었음을 판단하면 클라이언트에게 밀어 넣을 수 있어야 합니다. 이전 시도에서는 각 RPC 동시성 한도 및 pack-objects 동시성 한도를 도입하여 백프레셔에 기초를 두었습니다.

RPC 동시성 한도를 통해 동시에 전송되는 최대 요청량을 구성할 수 있습니다. 이는 RPC 및 리포지터리별로 한도를 설정합니다. pack-objects 동시성 한도는 IP별로 동시에 Git 데이터 전송 요청을 제한합니다. 단, pack-objects 동시성 한도는 캐시 미스에만 적용됩니다. 이 한도를 초과할 경우 요청은 대기열에 넣거나 대기열이 가득 찼다면 거부됩니다. 요청이 너무 오랫동안 대기열에 머무를 경우 또한 거부됩니다.

이들은 모두 GitLab.com에서 유망한 결과를 냈지만, 특히 동시성 한도의 값이 정적이기 때문에 일부 단점이 있습니다. 이로 인해 다음과 같은 단점이 있습니다:

  • 동시성 한도에 대한 합당한 값을 유지하는 것이 귀찮습니다. 이 운영 구성을 살펴보면 각 한도는 서로 다른 정보에서 나온 단서들을 기반으로 매우 조정되어 있습니다. 전반적인 상황이 변할 때마다 다시 조정해야 합니다.
  • 정적 한도는 모든 사용 패턴에 적합하지 않습니다. 일괄된 값을 선택하는 것이 현실적이지 않습니다. 한도가 너무 낮으면 대규모 사용자에게 영향을 미칩니다. 값이 너무 넉넉하면 보호 효과가 상실될 수 있습니다.
  • 서버가 유휴 상태이더라도 요청이 거부될 수 있습니다. 왜냐하면 속도는 서버에 가해지는 부하를 반드시 나타내는 지표가 아니기 때문입니다.

모든 이러한 단점을 극복하고 동시성 제한의 이점을 유지하면서 가장 유망한 솔루션 중 하나는 노드의 현재 사용 가능한 처리 용량에 맞추어 동시성 제한을 적응적으로 만드는 것입니다. 우리가 제안하는 이 새로운 모드를 “적응형 동시성 제한”이라고 합니다.

목표

  • Gitaly가 과부화 상태일 때 트래픽을 밀어내어 신뢰성과 신속성을 높이는 데 있다.
  • Gitaly 포화 사건의 발생 빈도를 최소화합니다.
  • 클라이언트가 잘못된 방식으로 동시성 한도에 도달하는 가능성을 줄여 ResourceExhausted 오류율을 낮춥니다.
  • 동시성 한도의 원활한 또는 완전 자동화된 조정을 용이하게 합니다.

비목표

  • 사용자 또는 관리자의 시스템의 작업량 또는 복잡성을 증가시키는 것은 목표가 반대입니다. 여기서 제안된 적응성은 반대의 목표를 가집니다.

제안

제안된 적응형 동시성 제한 알고리즘은 주로 가산 증가/곱셈 감소(AIMD) 접근법을 사용합니다. 이 방법은 일반 프로세스 기능 중에 제한을 서서히 증가시키는 것을 목표로 하지만 문제 발생 시에는 빨리 감소시키는 것을 목표로 합니다. Gitaly가 트러블이 있는지를 판단하는 여러 가지 기준이 있습니다. 본 제안에서는 다음에 초점을 맞춥니다.

  • Git 프로세스를 처리하는 데 필수적인 리소스인 메모리와 CPU의 부족.
  • 심각한 지연 감소.

본 제안 솔루션은 다른 회사들의 관련 자료들을 참고로 크게 영감을 받았으며, 특히 다음을 매우 고려하였습니다:

우리는 신중한 고려없이 솔루션을 적용하고 이상없이 작동한다고 예상하는 것은 할 수 없습니다. 제안된 접근 방식은 cgroup 이용률 및 upload-pack RPC 등 Gitaly의 구체적인 제약 조건과 특징을 고려합니다.

제안된 솔루션은 RPC 동시성pack objects 동시성에 대한 기존의 제한을 교체하는 것이 목적이 아니며 자동으로 매개변수를 조정합니다. 이는 대기, 대기열 시간 초과, 대기열 길이, 분할 및 범위와 같은 다른 측면은 변경되지 않을 것입니다. 제안된 솔루션은 현재의 동시성 제한의 에만 중점을 둡니다.

설계 및 구현 세부정보

AIMD 알고리즘

적응형 동시성 제한 알고리즘은 주로 가산 증가/곱셈 감소(AIMD) 접근법을 사용합니다. 이 방법은 일반 기능 중에 제한을 서서히 증가시키는 것을 목표로 하지만 문제 발생 시에는 빨리 감소시키는 것을 목표로 합니다.

초기화 중에 다음 매개변수를 구성합니다:

  • initialLimit: 시작할 동시성 제한. 이 값은 사실상 현재 정적 동시성 제한과 동일합니다.
  • maxLimit: 최대 동시성 제한.
  • minLimit: 프로세스가 정상적으로 작동 중으로 간주되기 위한 최소 동시성 제한. 0이면 모든 곧오는 요청을 거부합니다.
  • backoffFactor: 백오프 이벤트가 발생했을 때 제한이 얼마나 빨리 줄어드는지(0 < backoff < 1, 기본값은 0.75)

Gitaly 프로세스가 시작되면 limit = initialLimit를 설정합니다. 여기서 limit는 한 번에 처리할 수 있는 최대 요청입니다.

정기적으로, 아마 15초에 한 번, limit의 값이 다시 보정됩니다:

  • 마지막 보정 이후에 백오프 이벤트가 발생하지 않았다면 limit = limit + 1으로 설정됩니다. 새로운 제한은 maxLimit를 초과할 수 없습니다.
  • 그렇지 않으면 limit = limit * backoffFactor로 설정됩니다. 새로운 제한은 minLimit보다 낮아질 수 없습니다.

프로세스가 더 이상 요청을 처리할 수 없거나 곧 처리할 수 없게 되면 이를 백오프 이벤트로 지칭합니다. 이상적으로는 Gitaly가 최대 용량으로 오랫동안 있을 수 있을 때가 가장 바람직합니다.

적응형 동시성 제한 플로우

이상적인 경우, 최소/최대값은 과부하 중에조차 절대로 목표가 되지 않을 안전장치입니다. 사실, 둘 중 하나를 초과하는 것은 무언가 잘못되어 동적 알고리즘이 충분히 작동하지 않는 것을 의미할 수 있습니다.

요청 처리 방법

동시성 제한은 한 번에 처리되는 인플라이트 요청(IFR)의 총 수를 제한합니다.

  • IFR < 제한 일 때는 Gitaly는 새 요청을 대기하지 않고 즉시 처리합니다. 증분 후, Gitaly는 대기열에서 다음 요청을 즉시 처리합니다(있는 경우).
  • IFR = 제한일 때는 제한에 도달한 것을 의미합니다. 후속 요청은 대기열에 들어가 차례를 기다립니다. 대기열 길이가 구성된 제한에 도달하면 Gitaly는 새 요청을 즉시 거부합니다. 요청이 대기열에 오랜 시간 머무르면 Gitaly가 자동으로 해당 요청을 삭제합니다.
  • IRF > 제한인 경우 이는 적절히 백오프 이벤트의 결과입니다. 이것은 새로 지정된 제한보다 더 많은 요청을 Gitaly가 처리한다는 것을 의미합니다. 위의 경우와 유사하게 다가오는 요청을 대기열에 넣는 것 외에도 Gitaly는 이러한 상황이 충분히 해결되지 않으면 인플라이트 요청을 로드 쉐딩할 수 있습니다.

개별 시점에서 대기열 의미론을 변경하고자 여러 번 토론했습니다. 현재 우리는 대기열 헤드(FIFO)에서 대기 중인 프로세스를 허용하고 있지만 대기열 끝(LIFO)에서 프로세스를 허용하는 것이 더 좋을 것으로 몇 번이나 제안되었습니다.

요청 거부의 이유와 상관없이 클라이언트는 나중에 다시 시도하도록 하고 싶다는 신호로써 ResourceExhausted 응답 코드를 받습니다. 대부분의 Gitaly의 직접 클라이언트는 내부적이므로 특히 GitLab Shell 및 Workhorse는 실제 사용자에게 친숙한 메시지를 받습니다. Gitaly는 내부 클라이언트가 백 오프하도록 강제하기 위해 지수적인 밀어내기 헤더를 추가할 수 있습니다. 그러나 이것은 다소 무자비하며 예상치 못한 결과로 이어질 수 있습니다. 나중에 이에 대해 고려할 수 있습니다.

백오프 이벤트

각 시스템은 각자의 신호 집합을 갖고 있으며, Gitaly의 경우 고려해야 할 두 가지 측면이 있습니다.

  • 리소스 부족, 특히 메모리 및 CPU는 git-pack-objects(1)와 같은 Git 프로세스를 처리하는 데 필수적입니다. 이러한 리소스가 제한되거나 고가화되면 Gitaly가 더 많은 요청을 받는 것은 의미가 없습니다. 이렇게 하면 포화도가 더 심해지며, Gitaly는 이 문제를 cgroup을 활용하여 대응합니다. 다음 섹션에서 cgroup을 사용한 계산 방법을 설명합니다.
  • 심각한 지연 감소. Gitaly는 Git 데이터를 제공하는 것 외에도 다양한 목적을 위해 다양한 RPC를 제공합니다. 전반적인 지연의 급격한 감소는 Gitaly가 더 이상 요청을 받아들이지 않아야 함을 나타냅니다. 아래 섹션에서 합리적으로 지연 감소를 확정하는 방법에 대해 설명합니다.

위의 신호들과 별개로, 미래에 시스템을 더 똑똑하게 만들기 위해 추가적인 신호를 고려할 수 있습니다. 몇 가지 예는 Go 가비지 수집기 통계, 네트워킹 통계, 파일 디스크립터 등이 있습니다. 회사에 따라 시간 드리프팅을 사용하여 CPU 포화를 추정하는 것과 같이 똑똑한 속임수를 사용하는 곳도 있습니다.

Upload Pack RPC의 백오프 이벤트

Upload Pack RPC 및 해당 수많은 PackObjects RPC는 Gitaly에 고유한 것입니다. 이들은 가장 무거운 작업에 사용됩니다: 대량의 Git 데이터 전송입니다. 각 작업은 몇 분에서 몇 시간이 소요될 수 있습니다. 각 작업의 시간은 요청된 객체 수와 클라이언트의 인터넷 속도가 가장 중요한 요인입니다.

따라서 지연은 백오프 이벤트를 결정하는 데 부적절한 신호입니다. 이러한 종류의 RPC는 현재 이 단계에서 리소스 계산에만 의존해야 합니다.

다른 RPC의 백오프 이벤트

위에서 언급했듯이, Gitaly는 다양한 목적을 위해 다양한 RPC를 제공합니다. 수용 가능한 지연 및 언제 지연 감소를 인식해야 하는 지점에서 다양할 수도 있습니다. 다행스럽게도 현재의 RPC 동시성 제한 구현은 RPC 및 리포지터리를 개별적으로 구성합니다. 이러한 설정에서 지연 신호는 의미가 있습니다.

지연 외에도 리소스 사용은 중요한 역할을 합니다. 따라서 다른 RPC는 지연 메트릭과 리소스 계산 신호를 모두 사용해야 합니다.

cgroup을 사용한 리소스 계산

포화로 인한 문제는 대부분 Gitaly 자체가 아니라 대부분의 작업을 처리하는 생성된 Git 프로세스에 의해 일어납니다. 이러한 프로세스는 cgroup 내에 포함되어 있으며, cgroup을 위한 버킷화 알고리즘은 여기에서 찾을 수 있습니다. 일반적으로 Gitaly는 대상 리포지터리를 기반으로 요청에 대한 적절한 cgroup을 선택합니다. 또한 모든 리포지터리 수준 cgroup이 속하는 상위 cgroup도 있습니다.

cgroup 통계는 널리 접근할 수 있습니다. Gitaly는 cgroup 제어 파일의 다음 정보를 통해 리소스 용량과 현재 리소스 사용량을 손쉽게 검색할 수 있습니다:

  • memory.limit_in_bytes
  • memory.usage_in_bytes
  • cpu.cfs_period_us
  • cpu.cfs_quota_us
  • cpuacct.usage

이러한 통계를 가져오는 것은 약간의 오버헤드를 의미할 수 있습니다. 이러한 정보를 실시간으로 유지할 필요는 없습니다. 따라서 제한 조정 주기에서 주기적으로 해당 통계를 처리할 수 있습니다.

과거에 cgroup은 생성된 프로세스가 한도를 초과하지 않도록 신뢰할 수 있었습니다. 프로세스가 속한 cgroup이 설정한 한도에 도달했을 때(100%), 주로 증가한 페이지 폴트, 느린 시스템 호출, 메모리 할당 문제, 심지어 메모리 고갈 죽음과 같은 다양한 문제가 발생합니다. 이러한 사건의 결과는 이 예제에서 강조되고 있습니다. 인플리트 요청이 크게 영향을 받아 수용할 수 없는 지연, 타임아웃, 심지어 취소로 이어집니다.

또한, 과거의 다양한 관찰 결과, git-pack-objects(1)와 같은 일부 Git 프로세스는 시간이 지날수록 메모리를 낭비합니다. git-pull(1) 요청이 발생하면 여러 메모리 소비 프로세스로 노드가 쉽게 가득 찰 수 있습니다. 이러한 축적을 미리 차단하는 것이 훨씬 나은 접근입니다.

결과적으로, 과부하를 피하기 위해 Gitaly는 하드 제한에 의존하는 대신에 메모리 용량의 75%만 사용하는 등의 소프트 제한 세트를 활용합니다. 이러한 소프트 한계에 도달하면 동시성 조정자가 곱셈 방식으로 동시성 제한을 줄입니다. 이 전략은 노드에 충분한 여유 공간이 있는지 확인하여 잠재적인 과부하 사건을 처리합니다.

이론적으로 cgroup 계층구조를 통해 과부하 상태를 개별적으로 판단할 수 있습니다. 따라서 Gitaly는 각 리포지터리별로 동시성 제한을 조정할 수 있습니다. 그러나 이러한 접근은 실제로는 복잡할 수 있습니다. 오히려 운영자에게 혼란을 야기시킬 수 있습니다.

좋은 시작으로, Gitaly는 한 제한 중 어느 것이든(부모 cgroup과 포함된 모든 리포지터리 cgroup의 소프트 제한) 과부하 이벤트를 인식합니다.

  • 상위 cgroup의 소프트 제한이 도달한 경우
  • 어떤 리포지터리 cgroup의 소프트 제한이 도달한 경우

두 조건 중 어느 하나가 있을 때 과부하 이벤트를 인식하는 것은 논리적입니다. 이것은 리포지터리 cgroup의 용량 제한이 상위 cgroup의 용량 제한에 중요한 영향을 미칠 수 있기 때문입니다. 이는 리포지터리 cgroup이 한계에 도달하면 다른 cgroup에 사용 가능한 리소스가 줄어든다는 뜻입니다. 따라서 동시성 제한을 줄이면 과부하 발생이 지연되게 합니다.

지연 시간 메트릭

동시성 제한을 재보정할 때 업로드 팩 외의 RPC에 대한 지연 시간이 고려됩니다.
지연 시간을 메트릭할 때 고려해야 할 두 가지 사항은 다음과 같습니다.

  • 지연 시간을 기록하는 방법
  • 지연 시간의 저하를 인식하는 방법

Gitaly와 같은 강력한 gRPC 서버는 노드 당 초당 수천 개의 요청을 처리할 수 있습니다.
운영 서버는 초당 수천 개의 요청을 처리할 수 있습니다. 응답 시간을 정확하게 추적하고 저장하는 것은 현실적이지 않습니다.

공정한지 여부를 결정하는 휴리스틱은 흥미롭습니다. 가장 단순한 솔루션은 정적 지연 시간 임계 값을 사전에 정의하는 것입니다. 각 RPC마다 다른 임계값이 있을 수 있습니다.
유사한 정적 동시성 제한과 마찬가지로 합리적이고 최신 값을 선택하는 것은 도전적이고 지루할 수 있습니다.

다행히도 TCP 혼잡 제어 분야에 적용된 이 문제에 대한 몇 가지 유명한 알고리즘이 있습니다.

이 두 알고리즘은 사전에 정의된 구성 없이도 지연 시간 임계값을 자동으로 결정할 수 있습니다.
실세계 시나리오에서 매우 효율적이고 통계적으로 신뢰할 수 있습니다. 제 의견으로는 두 알고리즘이 모두 특정 사용 사례에 매우 적합하다고 생각합니다.

부하 분산

Gitaly가 과부하 상태에 갇히게 되면 두 가지 특징으로 표시될 수 있습니다.

  • 특정 수의 연이은 백오프 이벤트
  • 특정 수의 처리량 제한을 초과한 진행 중인 요청

이러한 경우 특정 cgroup 또는 전체 Gitaly 노드가 임시로 사용할 수 없게 될 수 있습니다.
진행 중인 요청은 취소되거나 시간 초과될 가능성이 높습니다.
GitLab.com 제품의 경우 사건이 트리거되어 사람의 개입을 요청합니다. 이러한 상황을 개선하기 위해 부하 분산을 할 수 있습니다.

이 메커니즘은 고의적으로 진행 중인 요청을 선택적으로 중단시킵니다.
주요 목적은 모든 진행 중인 요청의 연쇄적 실패를 방지하는 것입니다.
일부 요청이 삭제된 후에는 cgroup/노드가 사람의 개입 없이 빠르게 정상 상태로 회복될 수 있기를 희망합니다.
결국, 이는 순수 가용성 및 내구성 향상으로 이어집니다.

어떤 요청을 취소할지 선택하는 것은 까다롭습니다. 많은 시스템에서는 요청의 중요도가 고려됩니다. 하향 스트림에서의 요청은 중요도 점수가 지정됩니다. 점수가 낮은 요청이 우선 대상이 됩니다.
유감스럽게도, GitLab에는 유사한 시스템이 없습니다. 긴급성 시스템이 있지만 중요성보다는 응답 시간 약속에 사용됩니다.

대신, 시스템에 가장 많은 피해를 입히는 요청을 우선 순위로 두는 것이 좋습니다. 고려해야 할 기준 몇 가지는 다음과 같습니다.

  • 의미 있는 양의 메모리를 사용하는 요청
  • 시간 이상으로 상당한 양의 CPU를 사용하는 요청
  • 느린 클라이언트
  • 최근에 트래픽을 지배하는 IP에서의 요청
  • 대기 중인 요청/초기 단계의 요청. 거의 완료된 요청을 거절하고 싶지 않습니다.

시작하려면 먼저 앞의 두 기준을 선택할 수 있습니다. 나중에 제품에서 배우면 디렉터리을 보강할 수 있습니다.

참고 문헌