7월 1일 (수) 뉴스 보기

2026년 7월 1일 · 4² AI 뉴스레터

OpenAI, 18년 된 버그 해결의 비밀: 코어 덤프 분석

OpenAI

파이랩 정리

OpenAI의 18년 된 버그 해결: 코어 덤프 분석

OpenAI의 모델과 에이전트는 추론 시점에 관련 데이터를 검색하기 위해 확장 가능한 데이터 인프라에 점점 더 의존하고 있습니다. 이러한 서비스 중 일부는 C++로 작성되어 시스템에 대한 저수준 제어를 통해 성능을 극대화하고 메모리 사용을 최소화할 수 있습니다. 그러나 C++의 메모리 안전성 부족은 잘못된 메모리 주소에 쓰기를 시도할 경우 충돌을 일으킬 수 있습니다.

몇 달 전, OpenAI는 Rockset 서비스 내부에서 충돌을 관찰했습니다. Rockset은 ChatGPT 데이터 인프라의 맞춤형 부분으로, 많은 데이터 플러그인과 대화 검색에 중요한 역할을 합니다. 이러한 충돌에서 일반적인 C++ 함수가 완료된 후 잘못된 주소로 반환되어 커널이 프로그램을 중지시켰습니다. 때로는 스택 프레임의 반환 주소 슬롯이 NULL이었고, 때로는 스택 포인터 CPU 레지스터 자체가 8바이트 정도 잘못된 것처럼 보였습니다. 이러한 경우 모두 반환 시 충돌이 발생했습니다.

이러한 실패 모드는 애플리케이션 코드에서 일반적이지 않습니다. 저장된 반환 주소에만 착륙하는 잘못된 쓰기는 가능하지만 매우 드뭅니다. %rsp를 8로 잘못 정렬하는 버그는 인라인 어셈블리, setcontext, longjmp를 사용하지 않고도 발생할 수 있습니다. 이러한 버그는 컴파일된 코드가 함수 서두와 끝에서만 해당 레지스터를 직접 조정하기 때문에 더욱 이상합니다. 우리가 생각할 수 있는 모든 가설에는 강력한 반증이 있었기 때문에 이 버그는 불가능해 보였습니다.

결국 우리가 하나의 문제라고 생각했던 것은 우연히 동시에 발견된 두 개의 관련 없는 버그로 밝혀졌습니다. 첫 번째는 Azure 호스트의 하드웨어 손상으로 CPU가 수학 연산을 올바르게 수행하지 못한 경우였습니다. 두 번째는 GNU libunwind에서 18년 된 경쟁 조건으로, 널리 사용되는 오픈 소스 라이브러리에서 발견되지 않은 버그였습니다.

이 글은 전염병학자처럼 사고하고 충돌 전체 인구에 대한 고품질 데이터 세트를 구축하여 설명할 수 없는 충돌을 식별하고 수정한 이야기입니다.

첫 번째 디버깅 시도: 코어 덤프의 세심한 검사

Rockset은 OpenAI의 여러 내부 사용 사례에 사용되는 클라우드 네이티브 데이터 시스템입니다. Rockset의 실행 계층은 C++로 작성되었습니다. C++ 언어는 CPU에 대한 저수준 접근을 제공하여 성능과 효율성을 높일 수 있지만, 애플리케이션 버그가 잘못된 메모리 접근 및 세그먼트 오류로 이어질 수 있습니다. 이러한 문제를 추적하기 위해 우리는 충돌이 발생할 때 스택 추적을 기록하는 folly의 치명적 신호 처리기를 사용하고, 해당 코어 덤프를 Azure 블롭 스토리지에 업로드하여 나중에 분석합니다. Rockset의 쿼리 처리 리프는 복제되어 충돌의 클라이언트 영향을 최소화합니다. 그러나 각 세그먼트 오류는 신뢰성과 품질 목표를 달성하기 위해 수정해야 하는 버그에 해당합니다.

우리의 초기 접근 방식은 이러한 코어를 전통적인 디버깅 문제로 취급하는 것이었습니다. 몇 개의 코어 덤프를 매우 면밀히 검사하고 가설을 세워 하나씩 배제하는 것이었습니다.

대부분의 충돌은 DocumentTree::updateDocument라는 메서드에서 발생했습니다. 이러한 충돌에서 updateDocument는 알 수 없는 함수 X를 호출한 것으로 보였고, X가 활성 상태일 때 스택이 손상된 후 X가 실행 가능한 코드가 아닌 주소로 반환되었습니다. 일부 경우 X의 방금 팝된 프레임은 저장된 반환 주소가 NULL인 것을 제외하고는 유효해 보였습니다. 다른 경우에는 스택 포인터 자체가 잘못된 것처럼 보였지만, 다음 유효한 프레임은 여전히 updateDocument인 것처럼 보였습니다.

스택이 언제 손상되었는지 알 수 없었기 때문에 검색 공간이 매우 넓었습니다. updateDocument는 많은 인라인 처리를 거치는 큰 메서드이기 때문에 X의 후보 수가 압도적이었습니다.

이것이 우리 C++ 코드의 버그였을까요? 컴파일러나 링크 문제일까요? 런타임 라이브러리 중 하나의 문제일까요? 리눅스 커널의 신호 전달이나 컨텍스트 전환과 관련된 버그일까요? 더 희귀한 무언가일까요? 만약 이것이 잘못된 쓰기였다면, 왜 우리의 ASAN 스테이징 환경에서 잡히지 않았을까요?

우리는 애플리케이션 수준의 로그를 사용하여 문제의 모든 발생을 식별하려 했지만, 스택 손상 버그는 로그만으로 분류하기 어려웠습니다. 기록된 스택 추적 자체가 손상되었거나 누락되었기 때문입니다. 우리는 거짓 양성과 거짓 음성이 없는 로그 쿼리를 구성할 수 없었습니다. 더 많은 코어를 수동으로 검사하여 추가 예를 찾았지만, 그 과정은 신뢰할 수 있는 데이터 세트를 제공하기에는 너무 많은 노동이 필요했습니다.

이 조사 단계에서 우리는 (잘못된) 하드웨어 버그를 배제했습니다. 여러 지역과 여러 하드웨어 유형에서 충돌이 발생했기 때문에 소프트웨어만의 원인을 찾고 있었습니다. 며칠 동안 우리는 잘못 정렬된 %rsp 충돌에 대해 매우 깊이 파고들어 스택과 레지스터 내용을 사용하여 충돌 전 역사를 재구성했습니다. 이는 몇 가지 가능한 단서를 제공했지만, 모든 버그가 동일한 원인을 가지고 있다고 처음에 결론을 내린 것을 놓지 않았기 때문에 우리는 막다른 길에 빠졌습니다.

스택에서 얻은 단서

조사의 전환점에 도달하기 전에, 우리는 코어 파일에서 어떤 종류의 정보를 추출했는지 설명하는 것이 중요합니다.

Rockset은 -fno-omit-frame-pointer로 컴파일되어 활성 스택 프레임은 항상 %rbp를 통해 접근할 수 있으며, 호출자는 프레임 포인터의 연결 목록을 형성합니다.

리눅스 x86_64에서 AMD64 System V ABI는 %rsp 아래 128바이트를 레드 존으로 예약합니다. 이 영역은 사용자 공간 코드에 사용 가능하며, 중요한 것은 커널이 신호를 전달할 때 이를 덮어쓰지 않겠다고 약속합니다.

레드 존은 반환 후 충돌을 디버깅하는 데 중심이 되었습니다. 반환 전의 일부 정보를 보존하기 때문입니다. SIGSEGV가 발생하면 folly의 치명적 신호 처리기가 충돌하는 스레드의 스택에서 실행됩니다. 더 이상 활성 상태가 아닌 스택 프레임은 신호 처리기에 의해 덮어쓰이지만, 마지막 128바이트는 예외입니다. 그래서 우리는 "X의 방금 팝된 스택 프레임이 유효해 보였지만, 반환 주소가 NULL이었다"고 말할 수 있습니다. 레드 존은 비활성 프레임의 일부 또는 때로는 단지 하나의 비활성 프레임의 꼬리를 보존합니다.

우리는 모든 관련 함수가 매우 작은 잘못 정렬된 스택 충돌을 발견했습니다. 이를 통해 %rsp가 비교적 간단한 함수의 실행 중에 잘못 정렬되었고, 이후 더 많은 호출이 성공했음을 알 수 있었습니다. 프로그램은 활성 함수가 마침내 반환을 시도할 때만 충돌했습니다. 이러한 코드 경로 중 어느 것도 예외, 인라인 어셈블리, setcontext, longjmp를 사용하지 않았기 때문에, 스택 포인터가 코어에서 제안한 방식으로 실제로 변경되었다면 사용자 공간 코드의 그 어떤 버그도 문제를 설명하지 못했습니다.

그것은 우리를 커널 쪽으로 밀어붙였습니다.

Rockset은 대부분의 프로그램보다 신호를 더 공격적으로 사용합니다. 쿼리 실행은 데이터를 교환하는 많은 경량 작업으로 나뉩니다. 이는 높은 QPS 작업량을 효율적으로 처리하는 데 중요하지만, 많은 쿼리의 작업이 동일한 스레드 풀에 다중화되기 때문에 쿼리당 CPU 계정이 어색해집니다.

우리의 솔루션은 coarse_thread_cputime_clock이라고 부르는 것입니다. 이는 작업 경계마다 샘플링하기에 충분히 저렴한 clock_gettime(CLOCK_THREAD_CPUTIME_ID, ...)의 근사치입니다. timer_create API를 사용하여 CPU 시간 누적을 포함한 여러 시간 경과 개념을 기반으로 주기적인 신호 전달을 예약할 수 있습니다. 우리는 몇 밀리초의 CPU 시간마다 신호(SIGUSR2)를 전달하도록 예약하고, 이 시점에서 신호 처리기가 스레드 로컬 값을 업데이트합니다. 많은 작업이 실행 중일 때 거친 시계가 진행되지 않는다고 볼 수 있지만, 모든 델타를 합산하면 쿼리의 실제 CPU 시간에 대한 편향되지 않은 추정치를 제공합니다.

우리가 신호를 자주 전달하기 때문에 컨텍스트 전환이나 신호 전달과 관련된 드문 커널 버그가 있을 가능성이 있었습니다. 우리는 버그 보고서, 커널 소스 코드, Azure 전용 커널 패치를 읽는 데 시간을 보냈습니다. 스트레스 테스트를 시도했습니다. 관련된 것으로 보이는 것을 찾을 수 없었습니다.

그 시점에서 우리는 한 걸음 물러서서 다른 접근 방식을 시도하기로 결정했습니다.

의사 또는 전염병학자?

이와 같은 문제를 디버깅하는 두 가지 주요 방법이 있습니다.

하나는 일종의 의사처럼 행동하는 것입니다. 한 환자에게 집중하고 많은 테스트를 수행하며, 자세한 증거를 통해 단일 사례를 진단하려고 시도하는 것입니다.

다른 하나는 전염병학자처럼 행동하는 것입니다. 전체 인구를 살펴보고 단일 사례로는 드러나지 않는 패턴이 있는지 묻는 것입니다. 버그가 특정 릴리스에서 시작되었습니까? 특정 하드웨어 SKU(특정 CPU 및 서버 모델), 지역, 또는 커널 버전과 상관관계가 있습니까? 하나의 증후군처럼 보이는 것 안에 여러 개의 뚜렷한 클러스터가 숨어 있습니까?

우리는 주로 의사 모드에 있었습니다. 핵심 변화는 고품질의 인구 데이터를 수집해야 한다는 결정을 내린 것이었습니다.

데이터 정리

이 문제의 모든 사례를 자동으로 찾으려는 이전 시도는 로그에 대한 텍스트 검색을 사용하려 했기 때문에 실패했습니다. 코어 덤프 자체에는 훨씬 더 많은 정보가 있지만, 수동으로 살펴보는 것은 확장되지 않았습니다. 우리는 코어 덤프를 자동으로 분석할 수 있는 파이프라인을 구축하는 데 노력을 기울이기로 결정했습니다.

우리는 ChatGPT에게 각 코어 파일의 접두사를 다운로드하고, 레지스터를 추출하고, 로그를 사용하여 알려진 거짓 양성을 필터링하고, 충돌을 반환-NULL, 잘못 정렬된 스택 또는 기타로 자동으로 레이블을 지정하는 스크립트를 작성하도록 했습니다. 그런 다음 우리는 지난 1년 동안의 모든 프로덕션 Rockset 코어 덤프에 대해 병렬로 해당 스크립트를 실행했습니다.

이것이 전환점이었습니다.

깨끗한 데이터 세트를 얻자마자 상관관계가 즉시 나타났습니다. 우리가 하나의 이상한 버그로 취급했던 것은 실제로 두 개의 별도 충돌 인구였습니다.

반환-NULL 코어는 여러 클러스터와 지리적 지역에 걸쳐 퍼져 있었습니다. 빈도는 최근에 증가했지만, 명확한 시작 날짜나 깨끗한 인프라 경계는 없었습니다.

잘못 정렬된 스택 충돌은 완전히 다르게 보였습니다. 모든 충돌이 한 지역에서 발생했으며, 명확한 시작 날짜가 있었고, 오랫동안 실행된 노드에서는 발생하지 않았습니다. 여러 Azure VM(클라우드에 호스팅된 가상 머신)을 포함했음에도 불구하고, 패턴은 하나의 물리적 머신이 문제가 되어 해당 VM이 배치된 모든 VM에 문제를 일으키는 것처럼 보였습니다.

그것이 우리가 두 개의 버그를 정신적으로 혼동하고 있었다는 것을 깨달았던 순간이었습니다. 두 버그 모두의 반례를 혼합했기 때문에 단일 일관된 설명을 찾을 수 없었습니다.

버그 #1: 불량 호스트

깨끗한 Kubernetes 노드 및 타임스탬프 목록을 갖춘 우리는 잘못 정렬된 스택 충돌을 단일 물리적 호스트로 추적할 수 있었고, 이를 쉽게 블랙리스트에 올릴 수 있었습니다.

우리는 몇 주간의 스트레스 테스트 후에도 해당 호스트에서 레지스터 손상을 제어된 환경에서 재현할 수 없었습니다. 그러나 문제가 있는 호스트가 서비스에서 제외되자 잘못 정렬된 스택 충돌은 사라졌습니다.

불량 호스트를 제거하는 것은 동일한 문제가 다시 발생하는 것을 방지하는 영구적인 솔루션은 아닙니다. 그러나 소프트웨어를 변경하여 유사한 문제가 재발할 경우 쉽게 감지하고 처리할 수 있도록 할 수 있습니다. 우리는 치명적 신호 처리기를 개선하여 레지스터 상태를 포함시켜 로그에서만 재발을 감지할 수 있도록 했습니다(코어 덤프 필요 없음). 우리는 제어 평면을 변경하여 VM이 재활용되는 대신 일반적으로 재사용되도록 하여, 인프라 스택 수준에서 불량 노드 감지를 훨씬 쉽게 만들었습니다. 또한 이 가능성을 포함하도록 실행 계획서(및 팀의 정신 모델)를 업데이트했습니다.

불량 호스트 충돌을 분리한 후 남은 반환-NULL 코어는 훨씬 더 쉽게 이해할 수 있었습니다. 이전에 우리는 예외 언와인딩을 배제했는데, 예외가 확실히 사용되지 않은 코드 경로에서 충돌이 발생했다고 생각했기 때문입니다. 그러나 이러한 반례는 모두 하드웨어 손상 클러스터에서 나온 것이었습니다.

남은 코어를 다시 살펴보았을 때, 이 결론은 정확히 반대였습니다. 충돌은 모두 예외 언와인딩 중에 발생하고 있었습니다.

예외 처리는 동적 제어 전송입니다

C++에서 예외가 발생하면 런타임은 어떤 catch 블록이 이를 수신해야 하는지, 그리고 어떤 소멸자나 정리 핸들러가 실행되어야 하는지를 발견해야 합니다. 컴파일러는 이 메타데이터를 생성하지만, 실제 매칭은 런타임에 동적으로 발생합니다.

예외 언와인딩은 throw를 호출하는 함수가 수행하는 것이 아니라, 컴파일된 코드에 의해 호출되는 헬퍼 함수에 의해 수행됩니다. 이러한 런타임 루틴은 스택을 검사하고, 스택에서 발견된 함수에 대한 메타데이터를 가져오고, 정리 핸들러와 catch 블록을 동적으로 찾은 다음, 이러한 위치 중 하나로 제어를 전송합니다. 제어 전송에는 모든 중간 스택 프레임(헬퍼 함수의 프레임 포함)을 언와인딩하는 것이 포함됩니다.

운영적으로, 이는 일반적인 호출 및 반환보다는 longjmp 또는 섬유 전환에 훨씬 더 가깝습니다. 호출자 저장 레지스터는 물론 스택 프레임 레지스터 %rbp%rsp도 복원해야 합니다.

우리의 바이너리는 C++ 예외 언와인딩을 수행하는 함수의 구현을 포함하는 두 개의 라이브러리와 연결됩니다: libgcc와 GNU libunwind. GNU libunwind의 정의가 동적 링커에 의해 선택되었습니다. 이는 우리를 놀라게 했습니다. 심볼 버전 규칙 때문에 libgcc 구현이 선택될 것으로 예상했지만, 실행 중인 바이너리를 검사한 결과 그렇지 않았습니다.

마지막 가정 철회

이 시점에서 우리의 작업 가설이 변경되었습니다. 하나의 버그만 있다고 생각했을 때 했던 또 다른 가정을 완화했습니다.

우리는 NULL로 반환하는 일반적인 함수가 아니라, 제어가 전송되기 전에 대상 명령 포인터가 NULL이 된 언와인드 전송을 보고 있었을 가능성을 고려했습니다. 즉, 스택의 반환 주소 슬롯이 잘못된 것이 아니라 언와인드 라이브러리의 잘못된 데이터일 수 있습니다.

이것은 문제를 크게 좁혔습니다. GNU libunwind가 잘못된 대상 상태를 계산했거나, 올바른 상태를 계산했지만 적용되기 전에 무언가가 이를 손상시켰을 가능성이 있었습니다.

우리는 GNU libunwind 소스를 읽고, 스택에 ucontext_t를 생성하고, 정리 핸들러의 프레임에 대한 원하는 레지스터 상태를 채운 다음, 해당 구조체에 대한 포인터를 내부 어셈블리 루틴 _Ux86_64_setcontext에 전달한다는 것을 발견했습니다.

이 시점에서 우리는 모든 조각을 가지고 있었습니다.

생성된 ucontext_t_Ux86_64_setcontext가 실행되는 동안 언와인드되는 스택 프레임 중 하나에 존재합니다. _Ux86_64_setcontext%rsp를 변경한 후에 구조체를 읽고 있었습니까? 그렇다면 구조체는 더 이상 활성 스택의 일부가 아니며, 신호 전달에 의해 덮어쓰일 수 있습니다. 예를 들어, 우리의 빈번한 SIGUSR2와 같은 신호가 전달될 수 있습니다.

버그 #2: libunwind 버그

답은 예였습니다.

다음은 우리가 사용하던 GNU libunwind 버전에서 _Ux86_64_setcontext의 마지막 여섯 개의 명령어입니다. 주로 메모리에서 대상 레지스터로 로드하는 mov 명령어로 구성되어 있습니다.

(%rdi는 스택에 할당된 ucontext_t를 가리키고, UC_MCONTEXT_* 매크로는 특정 레지스터가 저장된 고정 오프셋으로 확장됩니다.)

첫 번째 명령어는 경쟁 창의 시작입니다. 이는 %rsp를 활성 스택의 새 하단을 가리키도록 업데이트합니다. 이것이 발생하는 즉시 %rdi가 가리키는 구조체는 더 이상 활성 스택(또는 레드 존)의 일부가 아니며, 커널에 의해 덮어쓰이지 않겠다는 약속도 더 이상 유효하지 않습니다.

보통 이것은 문제가 되지 않지만, 신호가 정확히 맞는(잘못된?) 순간에 도착하면 커널은 %rsp-128에 신호 프레임을 구축합니다. 이는 %rdi가 가리키는 메모리를 덮어쓸 수 있습니다.

만약 그 일이 다음 명령어가 UC_MCONTEXT_GREGS_RIP(%rdi)를 읽기 전에 발생한다면, 복원된 명령 포인터가 손상될 수 있습니다. 우리의 충돌에서는 NULL이 되었습니다.

그것이 버그입니다.

왜 코어가 일반적인 잘못된 반환으로 위장되었는가

이 어셈블리는 우리가 혼란스러웠던 관찰 중 하나를 설명합니다. 왜 함수 X가 이전 스택 프레임의 반환 주소 슬롯에 NULL을 가지고 있었는지 말입니다.

setcontext는 모든 레지스터를 복원하도록 작성되었기 때문에, %rdi를 사용하여 제어 전송의 마지막 순간에 UC_MCONTEXT_GREGS_RIP(%rdi)를 읽을 수 없습니다. 대신, 값을 더 일찍 읽고 스택에 저장한 다음, 몇 개의 레지스터를 더 복원하고, 저장된 값을 읽고 retq를 사용하여 제어를 전송합니다.

코어에서 "함수가 NULL로 반환되었다"고 보였던 것은 실제로 "언와인더가 스택에 대상 반환 주소를 생성했지만, 전송이 완료되기 전에 해당 대상이 손상되었다"는 것입니다. 우리는 반환 주소 슬롯의 손상이 제자리에서 발생해야 한다고 가정했는데, 반환 주소 슬롯에 의도적으로 데이터를 쓰는 곳을 알지 못했기 때문입니다.

단일 명령어 경쟁 창

이 버그가 터무니없게 보이는 이유는 이 경쟁 창이 얼마나 좁은지 때문입니다. 이러한 종류의 경쟁 조건에서는 외부 이벤트(신호)가 다른 스레드가 두 단계를 수행하는 사이에 발생해야 합니다. 그 단계들이 서로 가까울수록 경쟁 조건이 발생할 가능성이 낮아집니다.

이 경우 취약한 창은 문자 그대로 한 명령어 너비입니다! 신호는 %rsp가 변경된 후, 그러나 다음 명령어가 %rip을 로드하기 전에 전달되어야 합니다. 현대의 초스칼라 비순차 CPU에서는 이러한 간단한 명령어가 주기당 여러 개 실행될 수 있으므로, 경쟁 창은 대략 100피코초입니다.

우리가 이 경쟁을 발견했을 때, 우리의 첫 반응은 관찰된 충돌률을 설명하기에는 너무 드물 것이라는 것이었습니다. 우리는 함대 전체에서 하루에 12개 이상의 반환-NULL 충돌을 보고 있었습니다. 예외 정리 중의 한 명령어 경쟁이 정말로 그것을 설명할 수 있을까요?

우리는 페르마 추정에 의존했습니다. 취약한 창이 대략 $1 0^{- 10}$초이고 SIGUSR2가 CPU 시간의 $1 0^{- 2}$초마다 도착한다면, 각 예외 정리 핸들러나 catch 블록은 경쟁에서 패배할 확률이 대략 $1 0^{- 8}$입니다.

Rockset은 내부 수신 압력 메커니즘의 일부로 예외를 사용합니다. 단일 과부하 호스트는 초당 $1 0^{4}$ 예외를 던질 수 있습니다. 이는 백프레셔를 사용하는 호스트의 평균 고장 간격이 $1 0^{4}$초, 즉 몇 시간마다 한 번의 충돌을 의미합니다. 함대 규모에서는 관찰된 충돌 빈도를 설명하기에 충분합니다.

왜 libunwind 버그가 지금 나타났는가?

GNU libunwind 버그는 오래된 것입니다. C++ 예외 언와인딩을 지원하는 첫 번째 x86_64 버전에 존재한 지 18년이 넘었습니다.

그렇다면 왜 지금 나타났을까요?

충돌률은 대략 던져진 예외 수와 전달된 신호 수에 비례합니다. 또한 신호 처리기가 소비하는 스택의 양에 따라 달라집니다.

Rockset은 이 세 가지 축에서 모두 비정상적입니다. 우리는 정상적인 과부하 제어의 일부로 높은 비율로 예외를 던지고, coarse_thread_cputime_clock 때문에 SIGUSR2를 비정상적으로 자주 전달하며, 올해 초 SIGUSR2 핸들러가 더 많은 스택을 사용하도록 timer_getoverrun 호출을 추가하여 병합된 신호를 계정할 수 있도록 했습니다.

마지막 변경 사항이 중요했던 것으로 보입니다. 핸들러가 충분히 적은 스택을 사용하면, 오래된 ucontext_t 메모리에 도달하여 덮어쓰지 않을 수 있습니다. 그 변경 이전에는 이러한 충돌을 전혀 관찰하지 않습니다. 변경 후 비율은 낮았지만, 백프레셔 메커니즘을 스트레스 테스트하는 일부 사용 사례에 대한 부하를 증가시켰을 때 비율이 증가했습니다.

즉, libunwind 버그는 항상 존재했지만, 우리의 예외 비율, 신호 비율, 핸들러 스택 사용량의 곱이 최근에야 운영적으로 눈에 띄게 된 것입니다.

이 메커니즘은 또한 하드웨어 버그와 libunwind 버그가 주로 DocumentTree::updateDocument 내부에서 충돌한 우연을 설명합니다. libunwind의 충돌은 이 메서드로 강하게 편향되었습니다. 이는 수신 압력을 적용하기 위해 예외를 던지는 시점에 항상 활성화되어 있기 때문입니다. 또한 잘못 정렬된 %rsp 충돌에 강하게 선택되었습니다. 불량 하드웨어 노드는 대량 수신에 사용하는 SKU였으며, 이 메서드에서 CPU 시간을 대부분 소비합니다.

우리의 즉각적인 완화 조치는 GNU libunwind에서 libgcc의 언와인더로 전환하는 것이었습니다. 이는 자체적으로 좋은 거래였습니다. libgcc의 구현은 대규모 VM으로 확장할 때 중요한 잠금 경합을 줄이기 위한 많은 작업의 혜택을 받았습니다.

우리는 또한 자립형 재현자와 GNU libunwind에 대한 수정⁠(새 창에서 열림)을 업스트림하고, 다른 언와인더가 유사한 문제가 없는지 확인했습니다.

인구 수준 진단의 힘

이 디버깅 여정은 동적 링크, DWARF 언와인드 메타데이터, Linux 신호 전달, System V ABI, C++ 예외 기계에 대한 특정 세부 사항에 대해 많은 것을 가르쳐 주었습니다. 그러나 주요 교훈은 그 어떤 것보다 간단했습니다.

가장 중요한 단계는 기발한 어셈블리 읽기나 세부 사항에 대한 깊은 지식이 아니었습니다. 고품질의 데이터 세트를 구축하는 것이었습니다. 이 데이터 세트가 없었다면, 우리는 두 개의 뚜렷한 현상을 하나의 이야기로 혼합하고 혼란에서 벗어나려고 노력하고 있었습니다. 정확하고 완전한 인구 데이터를 얻자 문제의 구조가 명확해졌습니다. 하나의 충돌 인구는 불량 호스트에 속했고, 다른 하나는 libunwind의 경쟁에 속했습니다. 데이터가 개선되자 디버깅이 더 쉬워졌습니다.

Rockset과 같은 인프라 시스템에서는 이것이 매우 중요합니다. 이 조사는 깊은 계측, 자동화된 조사, 운영 도구의 지속적인 개선에 대한 우리의 헌신을 강화했습니다. 신뢰성은 버그가 발생한 후 수정하는 것뿐만 아니라, 불가능한 문제를 진단 가능하고 해결 가능한 문제로 바꾸는 데이터, 워크플로, 기술을 구축하는 것입니다.

이메일만 수집하며, 광고·스팸 없이 뉴스레터 발송에만 사용합니다.