계측
시스템이 자기 동작에 관한 신호를 밖으로 내보내도록 코드를 심는 일입니다. 심어 둔 자리에서만 신호가 나옵니다. 무엇을 심었는지가 나중에 무엇을 물어볼 수 있는지를 정합니다.
상세
공장 배관의 어느 구간이 막혔는지 알아내려면 구간마다 온도계와 압력계를 달아 두어야 합니다. 달아 둔 자리에서만 눈금이 읽힙니다.
계측은 시스템을 이루는 구성요소가 자기 동작에 관한 신호를 내보내도록 코드를 심는 일입니다. 신호는 트레이스와 메트릭과 로그 같은 것입니다. 심는 자리는 애플리케이션 코드일 수도 있고, 그 코드가 가져다 쓰는 라이브러리일 수도 있고, 그 아래 커널일 수도 있습니다. 심어 넣은 코드는 재려는 대상 안에서 그 대상과 같이 돕니다.
관측성은 시스템이 지닌 성질이고 계측은 그 성질을 만들어 내는 행위입니다. 시스템이 관측 가능하려면 계측되어 있어야 합니다. 구성요소의 코드가 신호를 내보내야 한다는 뜻입니다.
심는 일이 한 자리에서 한 번에 끝나지는 않습니다. 코드를 쓰는 사람이 직접 넣을 수도 있고, 컴파일러가 대신 끼워 넣을 수도 있고, 이미 돌고 있는 프로세스에 나중에 붙일 수도 있습니다. 언제 끼워 넣느냐가 방식을 가릅니다.
배경
시스템이 커지고 복잡해지면서 성능 분석을 하는 쪽이 옮겨 갔습니다. 개발 중인 개발자가 아니라 운영하는 시스템 통합자가 프로덕션에서 성능을 들여다보게 됐습니다. 부품 단위로 나눠 만들고 여러 애플리케이션을 한 자리에 몰아 넣는 흐름이 이 변화를 밀어붙였습니다. 시스템 통합자가 기성 부품을 원래 개발자가 예상하지 않은 방식으로 엮는 일이 늘었기 때문입니다. 그런데 성능 분석 기반은 대체로 이 변화를 따라가지 못했습니다. 여전히 개발자 쪽이나 개발 시스템 쪽, 또는 둘 다에 초점이 맞춰져 있었습니다.
그래서 돌고 있는 시스템을 세우지 않고 안을 봐야 한다는 요구가 붙습니다. DTrace 를 만든 사람들이 2004년에 낸 논문은 프로덕션 시스템에서 쓸 만하려면 두 가지를 지켜야 한다고 적습니다. 꺼져 있을 때 탐침 효과가 0 이어야 하고, 켜져 있을 때는 절대적으로 안전해야 한다는 것입니다. 있다는 사실만으로 시스템을 조금이라도 더 느리게 만들어서는 안 되고, 잘못 써서 시스템 장애를 일으킬 길이 없어야 한다는 뜻입니다.
재려는 대상에 재는 장치를 붙이는 일을 계측이라 부릅니다. 소프트웨어에서는 그 장치가 코드입니다. 붙이는 자리와 붙이는 시점이 여럿이라 방식이 갈립니다.
갈래
계측 코드를 언제 끼워 넣느냐가 축입니다. 같은 신호를 얻더라도 소스 코드를 고쳐야 하는지, 다시 컴파일해야 하는지, 프로세스를 다시 띄워야 하는지가 이 축 위에서 갈립니다.
flowchart TD
A["코드를 쓸 때 · 소스 코드에 직접"] --> B["컴파일할 때 · 진입과 퇴장에 훅"]
B --> C["불러들일 때 · 바이트코드 수정"]
C --> D["돌고 있을 때 · 붙였다 뗀다"]
왼쪽으로 갈수록 코드를 쥔 쪽이 직접 넣습니다. 오른쪽으로 갈수록 이미 만들어져 돌고 있는 것에 나중에 붙입니다.
소스 코드에 직접
개발자가 서비스 코드에 계측 API(Application Programming Interface, 응용 프로그램 인터페이스)를 불러 넣습니다. OpenTelemetry 공식 문서는 만드는 것이 무엇이냐에 따라 의존성이 갈린다고 적습니다. 실행 가능한 바이너리에 얹혀 쓰이는 라이브러리나 부품을 만든다면 API 에만 의존합니다. 만드는 것이 독립 프로세스나 서비스라면 API 와 SDK(Software Development Kit, 소프트웨어 개발 키트) 둘 다에 의존합니다.
라이브러리가 자기 안에 계측을 미리 품는 경우도 이 갈래입니다. 라이브러리 계측을 다루는 같은 프로젝트 문서는 이것이 보통 라이브러리 훅이나 몽키 패칭으로 이루어진다고 적습니다. 라이브러리가 계측을 직접 품으면 라이브러리 쪽이 훅을 밖으로 내놓고 문서화할 필요가 없어집니다.
컴파일할 때
컴파일러가 함수마다 훅 호출을 끼워 넣습니다. GCC(GNU Compiler Collection) 는
-finstrument-functions 로 함수 진입과 퇴장에 대한 계측 호출을 생성합니다. 함수에 들어간
직후와 함수에서 나가기 직전에 프로파일링 함수가 현재 함수의 주소와 호출 자리의 주소를 받아
불립니다.
이 계측은 다른 함수 안에 인라인으로 전개된 함수에 대해서도 이루어집니다. 특정 함수를 빼고
싶으면 그 함수에 no_instrument_function 속성을 줍니다. 그러면 그 함수에는 계측이 걸리지
않습니다.
적재할 때
실행 파일이나 바이트코드를 불러들이는 순간 에이전트가 고쳐 넣습니다. OpenTelemetry 는 이것을 zero-code 계측이라 부르고, 보통 에이전트나 에이전트에 준하는 설치의 꼴로 API 와 SDK 능력을 애플리케이션에 더한다고 적습니다. 구체적인 메커니즘은 언어마다 달라질 수 있습니다. 바이트코드 조작, 몽키 패칭, eBPF(extended Berkeley Packet Filter) 등으로 갈립니다. 같은 문서는 이 방식이 보통 쓰고 있는 라이브러리에 계측을 더한다고 적습니다. 요청과 응답, 데이터베이스 호출, 메시지 큐 호출 등이 계측됩니다. 그렇지만 애플리케이션 자신의 코드는 보통 계측되지 않습니다.
자바는 이 자리를 표준 API 로 갖고 있습니다. java.lang.instrument 패키지 설명은 자바 에이전트가
JVM(Java Virtual Machine, 자바 가상 머신) 위에서 도는 프로그램을 계측할 수 있게 하는 서비스를
제공한다고 적습니다. 계측 메커니즘은 메서드의 바이트코드를 고치는 것입니다. 에이전트는 클래스를
적재 시점에 임의의 방식으로 변환할 수도 있고, 모듈을 변환할 수도 있고, 이미 적재된 클래스의
메서드 바이트코드를 변환할 수도 있습니다.
실행 중에
이미 돌고 있는 프로세스에 붙였다 뗍니다. DTrace 논문은 정적 계측이 언제나 얼마간의 꺼진 탐침 효과를 낸다고 적습니다. 프로덕션에 필요한 0 의 꺼진 탐침 효과를 얻으려고 DTrace 는 동적 계측만 쓴다고 적습니다. 쓰지 않을 때 시스템은 DTrace 가 아예 없는 것과 똑같이 동작합니다.
같은 논문이 이 갈래가 필요한 이유를 짚습니다. 바이너리를 미리 계측해 두고 원본 대신 계측된 바이너리를 돌리는 정적 방식은 프로덕션 환경에 맞지 않습니다. 그 환경에서 애플리케이션 재시작은 받아들일 수 없는 서비스 중단이기 때문입니다. 리눅스 커널의 kprobe 기반 트레이스 이벤트도 트레이스포인트 기반 이벤트와 달리 동적으로, 그 자리에서 추가하고 제거할 수 있습니다. uprobe 기반 트레이스 이벤트는 이것과 비슷하되, 사용자가 객체 안 프로브 지점의 오프셋을 직접 계산해 줄 것을 기대합니다.
예시
GCC 의 함수 진입·퇴장 훅
void __cyg_profile_func_enter (void *this_fn, void *call_site);
void __cyg_profile_func_exit (void *this_fn, void *call_site);
-finstrument-functions 를 켜면 함수마다 이 두 함수가 불립니다. 첫 인자는 지금 도는 함수가
시작되는 주소이고, 심볼 테이블에서 정확히 찾아낼 수 있습니다. 둘째 인자는 그 함수를 부른
자리의 주소입니다. 계측 코드를 쓰는 쪽은 이 두 함수의 몸통을 채워 넣어 진입과 퇴장을 기록합니다.
자바 에이전트의 -javaagent
-javaagent:<jarpath>[=<options>]
JVM 이 뜰 때 에이전트 JAR(Java ARchive) 를 붙이는 명령줄 옵션입니다. <jarpath> 는 에이전트
JAR 파일의 경로이고 <options> 는 에이전트에 넘기는 옵션입니다. 그 JAR 의 메인 매니페스트에는
Premain-Class 속성이 있어야 하고, 값은 JAR 안 에이전트 클래스의 이진 이름입니다.
public static void premain(String agentArgs, Instrumentation inst)
JVM 은 에이전트 클래스를 적재하고 이 메서드를 불러 에이전트를 시작합니다. 이 메서드는
애플리케이션의 main 메서드보다 먼저 불립니다. 애플리케이션 코드가 돌기 전에 바이트코드를
고칠 자리를 얻는 것입니다.
파이썬에서 스팬 열기
def roll():
# This creates a new span that's the child of the current one
with tracer.start_as_current_span("roll") as rollspan:
res = randint(1, 6)
rollspan.set_attribute("roll.value", res)
return res
소스 코드에 직접 넣은 계측입니다. start_as_current_span 이 지금 스팬의 자식으로 새 스팬을
엽니다. set_attribute 는 그 스팬에 roll.value 라는 이름으로 주사위 눈을 붙입니다. 주사위를
굴리는 함수 하나가 이 세 줄만으로 트레이스에 나타납니다.
관련 항목
내보내는 신호
끼워 넣는 수단
바이트코드 조작 · 몽키 패칭 · eBPF · 에이전트 · 컴파일러 훅 · uprobe · kprobe · DTrace
같은 수단을 쓰는 다른 목적
코드가 심어지는 층
애플리케이션 · 라이브러리 · 커널
정의하는 표준·문서
OpenTelemetry · GCC · JVM
다루는 성질
뒷받침하는 관측 활동
다른 이름: instrumentation · 인스트루멘테이션