OTCA — 오픈텔레메트리 인증 어소시에이트 · API·SDK·계측 · 이론
자동 계측이 주는 것은 네트워크 경계 하나뿐이다
한 줄 요약
자동 계측을 켜면 얻는 것은 정확히 하나, 네트워크 경계입니다. HTTP 서버와 클라이언트, gRPC, DB 드라이버, 레디스, 메시지 큐 클라이언트에 스팬이 생깁니다. 프로세스 안에서 일어나는 일은 어떤 스팬으로도 나타나지 않고, 부모 스팬의 self time 이라는 공백으로만 드러납니다.
왜 이게 필요했나
계측에 실패하는 팀은 거의 같은 방식으로 실패합니다. 코드에 수동 스팬부터 심기 시작해서, 2주 뒤에 스팬은 300개인데 트레이스는 여전히 서비스 경계에서 끊겨 있는 상태가 됩니다. 작동하는 순서는 정해져 있습니다.
| 순서 | 하는 일 | 건너뛰면 |
| --- | --- | --- |
| 1 | 자동 계측을 켜고 데이터가 도착하는지 확인 | 이후 모든 디버깅이 추측이 된다 |
| 2 | 리소스 속성 확정 | 나중에 바꾸면 과거 데이터와 끊긴다 |
| 3 | 서비스 경계를 넘는 전파 검증 | 스팬을 늘려도 트레이스가 조각난다 |
| 4 | self time 이 큰 구간에만 수동 스팬 | 자동 계측의 빈칸이 영원히 남는다 |
| 5 | 컬렉터로 가공·샘플링 이관 | 정책 바꿀 때마다 전 서비스 재배포 |
3번이 4번보다 앞이라는 점이 핵심입니다. 전파가 끊긴 상태에서 수동 스팬을 추가하는 것은 조각난 트레이스를 더 잘게 조각내는 일입니다.
어떻게 동작하나
자동 계측을 켜면 이런 트레이스가 나옵니다.
SERVER checkout-api POST /v1/orders 1421ms├─ CLIENT GET http://auth.internal/verify 31ms├─ CLIENT SELECT carts WHERE id = ? 6ms├─ CLIENT redis GET promo:rules:t-8871 2ms├─ CLIENT POST http://payment.internal/charge 74ms└─ (나머지 1308ms 는 어떤 스팬에도 속하지 않음)마지막 줄이 전부입니다. 자동 계측은 어디가 문제가 아닌지를 1,308ms 의 공백으로 알려 줍니다. 그 공백이 self time 이고, 수동 스팬은 여기에만 넣습니다.
자동 계측이 절대 보지 못하는 것은 다음과 같습니다. 프로세스 안의 CPU 작업(직렬화·압축·템플릿 렌더링·암호화), 락 대기와 커넥션 풀 대기, GIL 경합과 이벤트 루프 지연, 계측 패키지가 없는 서드파티 SDK 호출, 그리고 비즈니스 로직의 분기.
수동 스팬을 넣을 자리는 다섯 곳입니다.
1. 루프와 배치 경계 — 반복 횟수를 속성으로 남깁니다
2. 캐시 조회 — 히트 여부를 속성으로 남기면 캐시 효율이 트레이스에서 바로 보입니다
3. 계측 패키지가 없는 서드파티 SDK 호출
4. 락, 큐, 커넥션 풀 대기
5. CPU 를 오래 쓰는 구간 — 직렬화, 압축, 리포트 생성
넣고 나면 아까의 공백이 이렇게 채워집니다.
├─ INTERNAL checkout.apply_promotions 1298ms│ ├─ INTERNAL promotion.load_rules cache.hit=false 1241ms <-- 여기│ └─ INTERNAL promotion.evaluate evaluated=812 54ms스팬 이름 규칙 하나만 지키면 됩니다. 이름은 저카디널리티여야 합니다. GET /v1/orders/A-99183 이 아니라 GET /v1/orders/:id 이고, 구체적인 값은 전부 속성으로 갑니다. 백엔드는 스팬 이름으로 그룹핑해서 지연 통계와 서비스 그래프를 만들기 때문에, 이름에 ID 가 들어가면 그 집계 뷰 전체가 무너집니다.
리소스 속성은 나중에 못 고칩니다. 스팬 속성과 달리 리소스 속성은 그 프로세스가 내보내는 모든 신호에 붙고, service.name 을 바꾸는 순간 대시보드·알림·서비스 그래프·과거 데이터와의 연결이 전부 끊깁니다.
| 속성 | 예 | 바꿀 수 있는가 |
| --- | --- | --- |
| service.name | checkout-api | 사실상 불가 |
| service.namespace | commerce | 어렵다 |
| service.version | 2.7.1 | 매 배포마다 바뀜 |
| deployment.environment.name | prod | 불가 |
| service.instance.id | 파드 이름 | 매 재시작마다 바뀜 |
여기서 자주 틀리는 것 두 가지. 첫째, 환경 속성의 이름은 deployment.environment.name 입니다. 예전 이름 deployment.environment 는 더 이상 쓰지 않고, 이름이 다르면 두 개의 별개 속성이 되어 대시보드 변수는 그중 하나만 읽습니다. 둘째, service.name 은 배포 단위가 아니라 서비스 단위입니다. 카나리를 checkout-api-canary 로 부르는 순간 서비스 그래프에 유령 노드가 생깁니다.
샘플러는 처음에 parentbased_always_on 으로 시작하는 것을 권합니다. 처음부터 비율 샘플링을 켜면 트레이스가 안 보일 때 계측 문제인지 샘플링 때문인지 구분할 수 없습니다. 비율로 넘어갈 때도 반드시 parentbased_traceidratio 를 씁니다. 부모 결정을 따르지 않고 서비스마다 독립적으로 확률 판정을 하면 트레이스가 중간에서 잘려 나갑니다.
현장에서 만나는 모습
계측이 앱을 망가뜨리는 방식은 몇 가지로 반복됩니다.
| 증상 | 원인 | 대응 |
| --- | --- | --- |
| 배포 후 메모리가 계속 증가 | 백엔드 지연으로 익스포터 큐가 참 | 큐 상한 명시, 컬렉터 경유로 전환 |
| 스팬이 일부만 도착 | 종료 시 flush 없이 죽음 | shutdown 호출, 종료 유예 시간 확대 |
| 스팬 하나가 수백 KB | 요청 본문 전체를 속성에 넣음 | 속성 값 길이 상한 설정 |
| 서비스 그래프에 유령 노드 | 카나리를 별도 service.name 으로 배포 | service.name 은 서비스 단위 고정 |
속성 값 길이에는 SDK 기본 상한이 없습니다. 속성 개수는 기본 128개로 제한되지만 값 길이는 무제한이라, 요청 본문이 통째로 들어가면 전송과 저장 비용이 그대로 따라옵니다. 명시적으로 걸어 두는 편이 안전합니다.
다음 실습에서 할 것
/root/otca-sdk/ 아래에 SDK 환경변수 파일을 쓰고, 리소스 속성을 규약에 맞게 정하고, 쿠버네티스 Downward API 로 파드 이름과 네임스페이스를 주입하는 Deployment 를 만들어 실제로 적용합니다. 마지막에는 스팬 이름 목록을 정리하고, ID 가 박힌 이름을 잡아내는 린터를 직접 작성합니다.