`systemtap-toolkit`과 `stapxx`: 데이터를 활용하여 어려운 문제를 해결하는 방법은?
API7.ai
December 22, 2022
이전 글에서 소개한 바와 같이, 서버 측 개발 엔지니어로서 우리는 동적 디버깅 도구 세트에 대해 깊이 있게 학습하지 않고, 주로 사용법 수준에서 머물며 간단한 스테이플 스크립트를 작성하는 정도에 그칩니다. CPU 캐시, 아키텍처, 컴파일러 등과 같은 하위 수준은 성능 엔지니어의 영역입니다.
OpenResty에는 openresty-systemtap-toolkit
과 stapxx
라는 두 가지 오픈소스 프로젝트가 있습니다. 이들은 NGINX와 OpenResty를 실시간으로 분석하고 진단하기 위해 systemtap 기반으로 래핑된 도구 세트입니다. 이 도구들은 on-CPU, off-CPU, 공유 딕셔너리, 가비지 컬렉션, 요청 지연, 메모리 풀링, 연결 풀링, 파일 접근 등 일반적인 기능과 디버깅 시나리오를 다룰 수 있습니다.
이 글에서는 이러한 도구들과 그에 해당하는 사용법을 살펴보며, NGINX와 OpenResty의 문제 해결을 위해 문제를 빠르게 찾을 수 있는 도구를 찾는 데 도움을 줄 것입니다. OpenResty 세계에서 이러한 도구를 사용하는 법을 배우는 것은 앞서 나가는 확실한 방법이며, 다른 개발자들과 소통하는 매우 효과적인 방법이기도 합니다. 결국, 도구가 생성한 데이터는 우리가 말로 설명할 수 있는 것보다 더 정확하고 상세할 것입니다.
그러나 OpenResty 버전 1.15.8
부터는 LuaJIT GC64 모드가 기본적으로 활성화되어 있지만, openresty-systemtap-toolkit
과 stapxx
는 이에 따른 변경을 따르지 않아 도구 내부가 제대로 작동하지 않을 수 있습니다. 따라서 이러한 도구를 사용할 때는 이전 버전인 1.13
을 사용하는 것이 좋습니다.
대부분의 오픈소스 프로젝트 기여자들은 파트타임이며, 도구가 계속 작동하도록 유지할 의무가 없습니다. 이는 오픈소스 프로젝트를 사용할 때 유의해야 할 점입니다.
예시: shared dict
오늘 글을 시작하기 위해 우리가 가장 익숙하고 시작하기 쉬운 도구인 ngx-lua-shdict
를 예로 들어보겠습니다.
ngx-lua-shdict
는 NGINX의 shared dict
를 분석하고 그 동작을 추적하는 도구입니다. -f
옵션을 사용하여 dict
와 key
를 지정하면 shared dict
의 데이터를 얻을 수 있습니다. --raw
옵션을 사용하면 지정된 key
의 원시 값을 내보낼 수 있습니다.
다음은 shared dict
에서 데이터를 가져오는 명령어 예시입니다.
# NGINX Worker PID가 5050이라고 가정
$ ./ngx-lua-shdict -p 5050 -f --dict dogs --key Jim --luajit20
Tracing 5050 (/opt/nginx/sbin/nginx)...
type: LUA_TBOOLEAN
value: true
expires: 1372719243270
flags: 0xa
마찬가지로, -w
옵션을 사용하여 특정 key
에 대한 딕셔너리 쓰기 작업을 추적할 수 있습니다:
$./ngx-lua-shdict -p 5050 -w --key Jim --luajit20
Tracing 5050 (/opt/nginx/sbin/nginx)...
Hit Ctrl-C to end
set Jim exptime=4626322717216342016
replace Jim exptime=4626322717216342016
^C
이 도구가 어떻게 구현되었는지 살펴보겠습니다. ngx-lua-shdict
는 Perl 스크립트이지만, 구현은 Perl과 무관하며, Perl은 단지 stap
스크립트를 생성하고 실행하는 데 사용됩니다.
open my $in, "|stap $stap_args -x $pid -" or die "Cannot run stap: $!\n";
우리는 이를 Python, PHP, Go 또는 우리가 좋아하는 어떤 언어로도 작성할 수 있습니다. stap
스크립트에서 중요한 부분은 다음 코드입니다:
probe process("$nginx_path").function("ngx_http_lua_shdict_set_helper")
이것은 이전에 언급한 probe
로, ngx_http_lua_shdict_set_helper
함수를 탐지합니다. 이 함수의 호출은 모두 lua-nginx-module
모듈의 lua-nginx-module/src/ngx_http_lua_shdict.c
파일에 있습니다.
static int
ngx_http_lua_shdict_add(lua_State *L)
{
return ngx_http_lua_shdict_set_helper(L, NGX_HTTP_LUA_SHDICT_ADD);
}
static int
ngx_http_lua_shdict_safe_add(lua_State *L)
{
return ngx_http_lua_shdict_set_helper(L, NGX_HTTP_LUA_SHDICT_ADD
|NGX_HTTP_LUA_SHDICT_SAFE_STORE);
}
static int
ngx_http_lua_shdict_replace(lua_State *L)
{
return ngx_http_lua_shdict_set_helper(L, NGX_HTTP_LUA_SHDICT_REPLACE);
}
이렇게 하면 이 함수를 탐지함으로써 공유 딕셔너리의 모든 작업을 추적할 수 있습니다.
on-CPU, off-CPU
OpenResty를 사용하면서 가장 흔히 마주치는 문제는 성능 문제일 것입니다. 성능이 나쁜 경우는 크게 두 가지로 나뉩니다. 즉, CPU 사용량이 너무 높거나 너무 낮은 경우입니다. 전자의 경우는 이전에 소개한 성능 최적화 방법을 사용하지 않았기 때문일 수 있으며, 후자의 경우는 블로킹 함수 때문일 수 있습니다. 이에 해당하는 on-CPU와 off-CPU 플레임 그래프는 궁극적인 근본 원인을 찾는 데 도움을 줄 수 있습니다.
C 수준의 on-CPU 플레임 그래프를 생성하려면 systemtap-toolkit
의 sample-bt
를 사용해야 합니다. 반면, Lua 수준의 on-CPU 플레임 그래프는 stapxx
의 lj-lua-stacks
로 생성됩니다.
sample-bt
를 사용하는 방법을 예로 들어보겠습니다. sample-bt
는 우리가 지정한 사용자 프로세스(단지 NGINX와 OpenResty 프로세스뿐만 아니라)의 호출 스택을 샘플링하는 스크립트입니다.
예를 들어, 실행 중인 NGINX Worker 프로세스(PID가 8736
)를 5초 동안 샘플링하는 코드는 다음과 같습니다:
$ ./sample-bt -p 8736 -t 5 -u > a.bt
WARNING: Tracing 8736 (/opt/nginx/sbin/nginx) in user-space only...
WARNING: Missing unwind data for module, rerun with 'stap -d stap_df60590ce8827444bfebaf5ea938b5a_11577'
WARNING: Time's up. Quitting now...(it may take a while)
WARNING: Number of errors: 0, skipped probes: 24
이 결과 파일 a.bt
는 FlameGraph 도구 세트를 사용하여 플레임 그래프를 생성하는 데 사용할 수 있습니다:
stackcollapse-stap.pl a.bt > a.cbt
flamegraph.pl a.cbt > a.svg
a.svg
는 생성된 플레임 그래프로, 브라우저에서 열어 볼 수 있습니다. 그러나 샘플링 기간 동안 일정 수준의 요청을 유지해야 합니다. 그렇지 않으면 샘플 수가 0
이 되어 플레임 그래프를 생성할 수 없습니다.
다음으로, off-CPU를 샘플링하는 방법을 살펴보겠습니다. systemtap-toolkit
의 sample-bt-off-cpu
스크립트를 사용하며, 이는 sample-bt
와 유사합니다:
$ ./sample-bt-off-cpu -p 10901 -t 5 > a.bt
WARNING: Tracing 10901 (/opt/nginx/sbin/nginx)...
WARNING: _stp_read_address failed to access memory location
WARNING: Time's up. Quitting now...(it may take a while)
WARNING: Number of errors: 0, skipped probes: 23
stapxx
에서 지연 시간을 분석하는 도구는 epoll-loop-blocking-distr
로, 지정된 사용자 프로세스를 샘플링하고 연속적인 epoll_wait
시스템 호출 간의 지연 시간 분포를 출력합니다.
$ ./samples/epoll-loop-blocking-distr.sxx -x 19647 --arg time=60
Start tracing 19647...
Please wait for 60 seconds.
Distribution of epoll loop blocking latencies (in milliseconds)
max/avg/min: 1097/0/0
value |-------------------------------------------------- count
0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 18471
1 |@@@@@@@@ 3273
2 |@ 473
4 | 119
8 | 67
16 | 51
32 | 35
64 | 20
128 | 23
256 | 9
512 | 2
1024 | 2
2048 | 0
4096 | 0
이 출력에서 볼 수 있듯이, 대부분의 지연 시간은 1ms 미만이지만, 200ms 이상인 경우도 있으며, 이 부분이 주의해야 할 부분입니다.
업스트림 및 단계 추적
OpenResty 코드 자체에서 발생할 수 있는 성능 문제 외에도, OpenResty가 cosocket
이나 proxy_pass
와 같은 업스트림 모듈을 통해 업스트림 서비스와 통신할 때, 업스트림 서비스 자체에 큰 지연이 있으면 전체 성능에 큰 영향을 미칠 수 있습니다.
이때 ngx-lua-tcp-recv-time
, ngx-lua-udp-recv-time
, ngx-single-req-latency
와 같은 도구를 사용하여 분석할 수 있습니다. 여기서는 ngx-single-req-latency
를 예로 들어보겠습니다.
이 도구는 도구 세트 내의 대부분의 도구와는 조금 다릅니다. 대부분의 다른 도구들은 많은 샘플과 통계 분석을 기반으로 분포에 대한 수학적 결론을 도출합니다. 반면, ngx-single-req-latency
는 개별 요청을 분석하고 OpenResty의 rewrite
, access
, content
단계 및 업스트림에서 개별 요청에 소요된 시간을 추적합니다.
구체적인 예시 코드를 살펴보겠습니다:
# ./stap++ 도구를 PATH에서 볼 수 있도록 설정:
$ export PATH=$PWD:$PATH
# nginx worker 프로세스의 pid가 27327이라고 가정
$ ./samples/ngx-single-req-latency.sxx -x 27327
Start tracing process 27327 (/opt/nginx/sbin/nginx)...
POST /api_json
total: 143596us, accept() ~ header-read: 43048us, rewrite: 8us, pre-access: 7us, access: 6us, content: 100507us
upstream: connect=29us, time-to-first-byte=99157us, read=103us
$ ./samples/ngx-single-req-latency.sxx -x 27327
Start tracing process 27327 (/opt/nginx/sbin/nginx)...
GET /robots.txt
total: 61198us, accept() ~ header-read: 33410us, rewrite: 7us, pre-access: 7us, access: 5us, content: 27750us
upstream: connect=30us, time-to-first-byte=18955us, read=96us
이 도구는 시작 후 처음 마주치는 요청을 추적합니다. 출력은 opentracing
과 매우 유사하며, 우리는 systemtap-toolkit
과 stapxx
를 OpenResty의 비침습적 버전의 APM(Application Performance Management)으로 생각할 수도 있습니다.
요약
오늘 이야기한 일반적인 도구 외에도, OpenResty는 당연히 더 많은 도구를 제공하며, 이를 탐색하고 학습할 수 있습니다.
추적 기술 측면에서 OpenResty와 다른 개발 언어 및 플랫폼 간에는 한 가지 더 큰 차이점이 있으며, 이를 천천히 음미할 수 있습니다.
코드를 깨끗하고 안정적으로 유지하고, 프로브를 추가하지 말고, 외부 동적 추적 기술을 통해 샘플링하세요.
OpenResty를 사용하면서 문제를 추적하고 분석하기 위해 어떤 도구를 사용해 보셨나요? 이 글을 공유하고 함께 나누며 발전해 나가길 바랍니다.