OpenResty의 다양한 디버깅 방법

API7.ai

December 16, 2022

OpenResty (NGINX + Lua)

OpenResty의 커뮤니케이션 그룹에서 개발자들은 종종 이 질문을 합니다: OpenResty에서 어떻게 디버깅을 할까요? 제가 아는 한, OpenResty에는 VSCode 플러그인을 포함한 몇 가지 중단점 디버깅을 지원하는 도구들이 있지만, 지금까지는 널리 사용되지 않고 있습니다. 저자 agentzh를 포함하여 제가 아는 몇몇 기여자들도 모두 가장 간단한 ngx.logngx.say를 사용하여 디버깅을 합니다.

이는 대부분의 초보자에게는 친절하지 않습니다. OpenResty의 많은 핵심 유지보수자들이 어려운 문제를 만났을 때 로그를 출력하는 원시적인 방법만 사용한다는 뜻일까요?

물론 아닙니다. OpenResty 세계에서는 SystemTap과 플레임 그래프가 까다로운 문제와 성능 문제를 다루는 표준 도구입니다. 메일링 리스트나 이슈에서 이에 대한 질문을 하면, 프로젝트 유지보수자는 플레임 그래프를 업로드하라고 요청하며, 텍스트 설명보다는 그래픽 설명을 요구할 것입니다.

다음 두 글에서 저는 디버깅에 대해 이야기하고, OpenResty가 디버깅을 위해 특별히 만든 도구 세트를 소개할 것입니다. 오늘은 먼저 디버깅 프로그램에 사용할 수 있는 것들부터 살펴보겠습니다.

중단점과 로그 출력

오랜 시간 동안 저는 IDE(통합 개발 환경)의 고급 디버깅 기능에 의존하여 프로그램을 추적했습니다. 이는 자연스러워 보였습니다. 테스트 환경에서 재현할 수 있는 문제라면, 아무리 복잡하더라도 그 근본 원인을 찾을 수 있다는 자신감이 있었습니다. 그 이유는 버그가 반복적으로 재현될 수 있기 때문이며, 중단점을 설정하고 로그를 출력함으로써 원인을 찾을 수 있습니다. 필요한 것은 단지 인내심뿐입니다.

이 관점에서 보면, 테스트 환경에서 꾸준히 재현되는 버그를 해결하는 것은 물리적인 작업입니다. 제가 업무에서 해결하는 대부분의 버그는 이 범주에 속합니다.

그러나 두 가지 전제 조건이 있습니다: 테스트 환경과 안정적인 재현입니다. 현실은 항상 이상적이지 않습니다. 만약 버그가 프로덕션 환경에서만 재현된다면, 디버깅할 방법이 있을까요?

여기서 저는 Mozilla RR이라는 도구를 추천합니다. 이를 리피터로 사용하여 프로그램의 동작을 기록한 후 반복적으로 재생할 수 있습니다. 솔직히 말해서, 프로덕션 환경이든 테스트 환경이든, 버그의 "증거"를 기록할 수 있다면, 이를 "법정 증거"로 사용하여 천천히 분석할 수 있습니다.

이진 탐색 알고리즘과 주석

그러나 일부 대형 프로젝트의 경우, 예를 들어 버그가 여러 서비스 중 하나에서 발생할 수 있거나, 데이터베이스를 쿼리하는 SQL 문에 문제가 있을 수 있습니다. 이 경우 버그가 안정적으로 재현되더라도, 버그가 발생한 부분을 확신할 수 없습니다. 따라서 Mozilla RR과 같은 기록 도구는 실패합니다.

이때, 고전적인 "이진 탐색 알고리즘"을 떠올릴 수 있습니다. 먼저 코드의 절반을 주석 처리하고, 문제가 지속되면 버그는 주석 처리되지 않은 코드에 있으므로 나머지 절반의 로직을 주석 처리하고 이 과정을 반복합니다. 몇 번 안에 문제를 완전히 관리 가능한 크기로 좁힐 수 있습니다.

이 방법은 약간 멍청해 보일 수 있지만, 많은 시나리오에서 효율적입니다. 물론 기술이 발전하고 시스템 복잡성이 증가함에 따라, OpenTracing과 같은 표준을 사용하여 분산 추적을 하는 것을 권장합니다.

OpenTracing은 시스템의 다양한 부분에 매립될 수 있으며, 여러 Span으로 구성된 호출 체인과 이벤트 추적을 Trace ID를 통해 서버에 보고하여 분석하고 그래픽으로 표시할 수 있습니다. 이는 개발자들이 많은 숨겨진 문제를 찾는 데 도움을 주며, 역사적 데이터가 저장되어 언제든지 비교하고 볼 수 있습니다.

또한, 시스템이 더 복잡한 경우, 예를 들어 마이크로서비스 환경이라면, Zipkin, Apache SkyWalking 등이 좋은 선택입니다.

동적 디버깅

위에서 설명한 디버깅 방법들은 대부분의 문제를 해결하기에 충분합니다. 그러나 프로덕션에서 가끔 발생하는 결함을 만난다면, 로그와 이벤트 추적을 추가하여 추적하는 데 상당한 시간이 걸릴 것입니다.

몇 년 전, 저는 매일 새벽 1시쯤 데이터베이스 리소스가 고갈되어 전체 시스템이 붕괴되는 시스템을 담당했습니다. 당시 우리는 낮에는 코드의 스케줄링된 작업을 확인하고, 밤에는 팀이 회사에서 버그가 재현되기를 기다렸다가 재현될 때 서브모듈의 실행 상태를 확인했습니다. 우리는 세 번째 밤이 되어서야 버그의 원인을 찾았습니다.

제 경험은 Dtrace를 만든 몇몇 Solaris 시스템 엔지니어들의 배경과 유사합니다. 당시 Solaris 엔지니어들도 이상한 프로덕션 문제를 해결하기 위해 밤낮으로 고생했고, 결국 하나의 설정이 잘못된 것이 원인이라는 것을 알게 되었습니다. 하지만 저와 달리, Solaris 엔지니어들은 이 문제를 완전히 피하기로 결정하고 Dtrace를 발명했습니다. 이는 특히 동적 디버깅을 위해 만들어졌습니다.

GDB와 같은 정적 디버깅 도구와 달리, 동적 디버깅은 온라인 서비스를 디버깅할 수 있습니다. 전체 디버깅 과정은 디버깅 대상 프로그램에 대해 비침습적이고 비침투적이며, 코드를 수정하거나 재시작할 필요가 없습니다. 비유하자면, 동적 디버깅은 X-ray와 같아서 환자의 몸을 검사할 때 혈액 검사나 위내시경이 필요하지 않습니다.

Dtrace는 최초의 동적 추적 프레임워크 중 하나였으며, 그 영향으로 다른 시스템에서도 유사한 동적 디버깅 도구가 등장했습니다. 예를 들어, Red Hat의 엔지니어들은 Linux에서 Systemtap을 만들었으며, 이는 제가 다음에 이야기할 내용입니다.

Systemtap

Systemtap은 자체 DSL을 가지고 있으며, 이를 사용하여 프로브 포인트를 설정할 수 있습니다. 더 자세히 들어가기 전에, 추상적인 것을 넘어서기 위해 Systemtap을 설치해 보겠습니다. 여기서는 시스템의 패키지 관리자를 사용하여 설치합니다.

sudo apt install systemtap

Systemtap으로 작성된 hello world 프로그램을 살펴보겠습니다:

# cat hello-world.stp
probe begin
{
  print("hello world!")
  exit()
}

쉬워 보이지 않나요? sudo 권한으로 실행해야 합니다.

sudo stap hello-world.stp

이렇게 하면 hello world!가 출력됩니다. 대부분의 시나리오에서 우리는 분석을 위해 자신만의 stap 스크립트를 작성할 필요가 없습니다. 왜냐하면 OpenResty에는 이미 정기적인 분석을 위한 많은 준비된 stap 스크립트가 있기 때문입니다. 다음 글에서 이를 소개하겠습니다. 따라서 오늘은 stap 스크립트에 대해 간략히 이해하는 것으로 충분합니다.

몇 가지 실습을 한 후, 개념으로 돌아가면, Systemtap은 위의 stap 스크립트를 C로 변환하고 시스템 C 컴파일러를 실행하여 커널 모듈을 생성하는 방식으로 작동합니다. 이 모듈이 로드되면, 커널을 후킹하여 모든 프로브 이벤트를 활성화합니다.

예를 들어, begin은 프로브의 시작 부분에서 실행되며, 이에 상응하는 end도 있습니다. 따라서 위의 hello world 프로그램은 다음과 같이 작성할 수도 있습니다:

probe begin
{
  print("hello ")
  exit()
}

probe end
{
print("world!")

여기서 저는 Systemtap에 대해 매우 피상적인 소개만 했습니다. Systemtap의 저자인 Frank Ch. Eigler는 Systemtap tutorial이라는 전자책을 썼으며, 이 책은 Systemtap을 상세히 소개합니다. 더 깊이 이해하고 싶다면, 이 책을 시작점으로 삼는 것이 최고의 학습 경로입니다.

기타 동적 추적 프레임워크

Systemtap은 커널 및 성능 분석 엔지니어에게 충분하지 않습니다.

  1. Systemtap은 기본적으로 시스템 커널에 진입하지 않습니다.
  2. 작동 방식이 느리며, 시스템의 정상 작동에 영향을 미칠 수 있습니다.

eBPF(extended BPF)는 최근 Linux 커널에 추가된 새로운 기능입니다. Systemtap에 비해 eBPF는 커널 지원이 직접적이며, 충돌이 없고 빠른 시작이 가능합니다. 동시에 DSL을 사용하지 않고 C 구문을 직접 사용하므로 시작하기가 훨씬 쉽습니다.

오픈소스 솔루션 외에도, Intel의 VTune도 최고의 도구 중 하나입니다. 직관적인 인터페이스 조작과 데이터 표시를 통해 코드를 작성하지 않고도 성능 병목 현상을 분석할 수 있습니다.

플레임 그래프

마지막으로, 이전 글에서 언급한 플레임 그래프를 다시 떠올려 보겠습니다. 앞서 언급했듯이, perfSystemtap과 같은 도구에서 생성된 데이터는 플레임 그래프를 사용하여 더 시각적으로 표시할 수 있습니다. 다음 그림은 플레임 그래프의 예입니다.

flame graph

플레임 그래프에서 색상 블록의 색상과 명암은 의미가 없으며, 단순히 다른 색상 블록을 구분하기 위한 것입니다. 플레임 그래프는 매번 샘플링된 데이터의 중첩이므로, 사용자 데이터는 블록의 너비와 길이입니다.

CPU에 대한 플레임 그래프에서, 색상 블록의 너비는 함수가 차지하는 CPU 시간의 백분율입니다: 블록이 넓을수록 성능 소모가 큽니다. 만약 평평한 꼭대기가 있다면, 그곳이 성능 병목 지점입니다. 반면, 색상 블록의 길이는 함수 호출의 깊이를 나타내며, 상단의 상자는 실행 중인 함수를 나타내고 그 아래의 모든 상자는 해당 함수의 호출자입니다. 따라서 아래의 함수는 위의 함수의 상위 유형입니다: 꼭대기가 높을수록 함수 호출이 깊습니다.

요약

동적 추적과 같은 비침습적 기술도 완벽하지 않다는 것을 아는 것이 중요합니다. 이는 특정 개별 프로세스만 감지할 수 있으며, 일반적으로 우리는 짧은 시간 동안만 이를 켜서 그 동안 샘플링된 데이터를 사용합니다. 따라서 여러 서비스에 걸쳐 또는 장기간 감지가 필요한 경우, 여전히 opentracing과 같은 분산 추적 기술이 필요합니다.

일상 업무에서 어떤 디버깅 도구와 기술을 사용하시나요? 댓글을 남겨 저와 함께 토론해 보세요. 또한 이 글을 친구들과 공유하여 함께 배우고 성장할 수 있기를 바랍니다.