계측

계측 (Instrumentation)

모니터링의 시작은 결국 코드에 메트릭을 심는 계측(instrumentation)이에요. 이 문서는 여러분의 코드에 메트릭을 계측할 때 따르면 좋은, 다소 의견이 담긴(opinionated) 지침을 정리한 거예요. 짧게 말하면 "모든 것을 계측해라"예요. 모든 라이브러리, 하위 시스템, 서비스는 적어도 몇 개의 메트릭을 가져서 대략적으로 어떻게 동작하는지 알 수 있어야 해요.

계측은 코드의 필수적인 일부가 되어야 해요. 메트릭 클래스를 사용하는 파일 안에서 인스턴스화하세요. 그러면 오류를 추적할 때 알림에서 콘솔로, 코드로 이동하기가 쉬워져요. 이 문서는 서비스 유형별 핵심 메트릭부터 라벨 사용, 카운터와 게이지 선택까지 실무 팁을 알려 드려요.

출처: 문서

본문

이 페이지는 여러분의 코드를 계측하기 위한 의견이 담긴 지침 집합을 제공해요.

어떻게 계측할까 (How to instrument)

짧은 답은 모든 것을 계측하는 것이에요. 모든 라이브러리, 하위 시스템, 서비스는 적어도 몇 개의 메트릭을 가져서 대략적으로 어떻게 성능이 나오는지 알 수 있어야 해요.

계측은 여러분의 코드의 필수적인 일부가 되어야 해요. 메트릭 클래스를 사용하는 것과 같은 파일에서 인스턴스화하세요. 이렇게 하면 오류를 추적할 때 알림에서 콘솔로, 코드로 이동하기가 쉬워져요.

서비스의 세 가지 유형 (The three types of services)

모니터링 목적으로 서비스는 일반적으로 세 가지 유형으로 나눌 수 있어요: 온라인 서빙(online-serving), 오프라인 처리(offline-processing), 배치 잡(batch jobs). 그 사이에는 겹침이 있지만, 모든 서비스는 이 범주 중 하나에 잘 들어맞는 경향이 있어요.

온라인 서빙 시스템 (Online-serving systems)

온라인 서빙 시스템은 사람이나 다른 시스템이 즉각적인 응답을 기대하는 시스템이에요. 예를 들어 대부분의 데이터베이스와 HTTP 요청이 이 범주에 들어요.

이런 시스템의 핵심 메트릭은 수행된 쿼리 수, 오류, 지연시간이에요. 진행 중인 요청 수(In-progress requests)도 유용할 수 있어요.

실패한 쿼리 수를 세는 것에 대해서는 아래 실패(Failures) 섹션을 보세요.

온라인 서빙 시스템은 클라이언트 측과 서버 측 양쪽 모두에서 모니터링해야 해요. 두 측이 다른 동작을 본다면 그건 디버깅에 매우 유용한 정보예요. 서비스에 클라이언트가 많다면 서비스가 각각을 개별적으로 추적하는 것은 비실용적이므로, 각자 자신의 통계에 의존해야 해요.

쿼리를 시작할 때 셀지 끝날 때 셀지를 일관되게 유지하세요. 끝날 때 세는 것을 권장해요. 오류와 지연시간 통계와 일치하고, 코딩하기도 더 쉬운 경향이 있거든요.

오프라인 처리 (Offline processing)

오프라인 처리에서는 아무도 응답을 적극적으로 기다리지 않고, 작업의 배치(batching)가 흔해요. 처리 단계가 여러 개일 수도 있어요.

각 단계에 대해 들어오는 항목 수, 진행 중인 항목 수, 마지막으로 무언가를 처리한 시간, 내보낸 항목 수를 추적하세요. 배치를 한다면 들어오고 나가는 배치도 추적해야 해요.

시스템이 마지막으로 무언가를 처리한 시간을 아는 것은 정체(stall)를 감지하는 데 유용하지만, 매우 국지적인 정보예요. 더 나은 접근 방식은 시스템을 통해 하트비트(heartbeat)를 보내는 것이에요: 끝까지 통과하는 일부 더미 항목으로, 삽입된 시각의 타임스탬프를 포함하는 거예요. 각 단계는 자신이 본 가장 최근 하트비트 타임스탬프를 내보내서, 항목이 시스템을 통해 전파되는 데 얼마나 걸리는지 알려 줘요. 처리가 발생하지 않는 조용한 기간이 없는 시스템에서는 명시적 하트비트가 필요하지 않을 수 있어요.

배치 잡 (Batch jobs)

오프라인 처리는 배치 잡으로 수행될 수 있으므로, 오프라인 처리와 배치 잡 사이에는 애매한 선이 있어요. 배치 잡은 연속적으로 실행되지 않는다는 점에서 구별되는데, 이 때문에 스크랩하기가 어려워요.

배치 잡의 핵심 메트릭은 마지막으로 성공한 시간이에요. 잡의 각 주요 단계가 얼마나 걸렸는지, 전체 런타임, 그리고 잡이 마지막으로 완료된 시간(성공 또는 실패)을 추적하는 것도 유용해요. 이들은 모두 게이지이며 PushGateway로 푸시되어야 해요. 일반적으로 처리된 총 레코드 수 같은 잡 전체에 대한 특정 통계도 추적하면 유용해요.

실행하는 데 몇 분 이상 걸리는 배치 잡의 경우 풀 기반 모니터링으로도 스크랩하는 것이 유용해요. 이렇게 하면 다른 유형의 잡과 마찬가지로 리소스 사용이나 다른 시스템과 통신할 때의 지연시간 같은 메트릭을 시간에 따라 추적할 수 있어요. 잡이 느려지기 시작하면 디버깅에 도움이 될 수 있어요.

아주 자주 실행되는 배치 잡(예: 15분마다보다 더 자주)은 데몬으로 전환해 오프라인 처리를 하는 잡으로 다루는 것을 고려해 보세요.

하위 시스템 (Subsystems)

세 가지 주요 서비스 유형 외에도 시스템에는 모니터링해야 할 하위 부분이 있어요.

라이브러리 (Libraries)

라이브러리는 사용자가 추가 구성 없이도 계측을 제공해야 해요.

프로세스 밖의 리소스(예: 네트워크, 디스크, IPC)에 접근하는 데 사용되는 라이브러리라면, 최소한 전체 쿼리 수, 오류(오류가 가능하다면), 지연시간을 추적하세요.

라이브러리가 얼마나 무거운지에 따라 라이브러리 내부의 내부 오류와 지연시간, 그리고 유용할 것 같은 일반 통계도 추적하세요.

라이브러리는 애플리케이션의 여러 독립적인 부분이 서로 다른 리소스에 대해 사용할 수 있으므로, 적절한 곳에 라벨로 사용을 구분하도록 주의하세요. 예를 들어 데이터베이스 연결 풀은 통신 중인 데이터베이스를 구분해야 하지만, DNS 클라이언트 라이브러리의 사용자를 구분할 필요는 없어요.

로깅 (Logging)

일반 규칙으로, 로깅 코드 줄마다 증가시키는 카운터도 있어야 해요. 흥미로운 로그 메시지를 발견하면, 그것이 얼마나 자주, 얼마나 오랫동안 발생했는지 볼 수 있어야 해요.

같은 함수에 밀접하게 관련된 로그 메시지가 여러 개 있다면(예: if나 switch 문의 서로 다른 분기), 모두에 대해 단일 카운터를 증가시키는 것이 합리적일 수 있어요.

또한 애플리케이션이 기록한 총 info/error/warning 줄 수를 내보내고, 릴리스 과정의 일부로 유의미한 차이를 확인하는 것도 일반적으로 유용해요.

실패 (Failures)

실패는 로깅과 비슷하게 처리해야 해요. 실패할 때마다 카운터를 증가시켜야 해요. 로깅과 달리, 오류는 여러분의 코드 구조에 따라 더 일반적인 오류 카운터로 거품처럼 올라갈(bubble up) 수도 있어요.

실패를 보고할 때는 일반적으로 총 시도 수를 나타내는 다른 메트릭을 가져야 해요. 그러면 실패 비율을 계산하기 쉬워져요.

스레드풀 (Threadpools)

어떤 종류의 스레드풀에 대해서든 핵심 메트릭은 대기 중인 요청 수, 사용 중인 스레드 수, 총 스레드 수, 처리된 작업 수, 그리고 걸린 시간이에요. 큐에서 얼마나 오래 기다렸는지 추적하는 것도 유용해요.

캐시 (Caches)

캐시의 핵심 메트릭은 총 쿼리 수, 히트 수, 전체 지연시간, 그리고 그다음 캐시 앞에 있는 온라인 서빙 시스템의 쿼리 수·오류·지연시간이에요.

컬렉터 (Collectors)

사소하지 않은(non-trivial) 커스텀 메트릭 컬렉터를 구현할 때는 컬렉션에 걸린 시간(초)과 만난 오류 수를 게이지로 내보내는 것이 좋아요.

이것은 요약이나 히스토그램 대신 게이지로 지속시간을 내보내도 괜찮은 두 가지 경우 중 하나예요. 다른 하나는 배치 잡 지속시간이에요. 둘 다 시간에 따른 여러 지속시간을 추적하기보다는 그 특정 push/scrape에 대한 정보를 나타내기 때문이에요.

주의할 점 (Things to watch out for)

모니터링을 할 때 알아둬야 할 일반적인 것들과, 특히 Prometheus 특유의 것들이 몇 가지 있어요.

라벨 사용 (Use labels)

라벨과 그것을 활용하는 표현식 언어의 개념을 가진 모니터링 시스템은 드물어서, 익숙해지는 데 시간이 좀 걸려요.

더하거나/평균내거나/합하고 싶은 메트릭이 여러 개라면, 보통 여러 메트릭보다는 라벨이 있는 단일 메트릭이어야 해요.

예를 들어 http_responses_500_totalhttp_responses_403_total 대신, HTTP 응답 코드를 위한 code 라벨이 있는 http_responses_total이라는 단일 메트릭을 만드세요. 그러면 규칙과 그래프에서 전체 메트릭을 하나로 처리할 수 있어요.

엄지손가락 규칙으로, 메트릭 이름의 어떤 부분도 절차적으로 생성되어서는 안 돼요(대신 라벨을 사용하세요). 유일한 예외는 다른 모니터링/계측 시스템에서 메트릭을 프록시할 때예요.

naming 섹션도 참고하세요.

라벨을 남용하지 마세요 (Do not overuse labels)

각 labelset은 RAM, CPU, 디스크, 네트워크 비용이 드는 추가 타임시리즈예요. 보통 그 오버헤드는 무시할 만하지만, 많은 메트릭과 수백 대의 서버에 걸친 수백 개의 labelset이 있는 시나리오에서는 빠르게 쌓일 수 있어요.

일반 지침으로, 메트릭의 카디널리티(cardinality)를 10 미만으로 유지하려고 노력하고, 그것을 초과하는 메트릭은 전체 시스템에 걸쳐 소수로 제한하는 것을 목표로 하세요. 대다수의 메트릭은 라벨이 없어야 해요.

카디널리티가 100을 넘거나 그렇게 커질 가능성이 있는 메트릭이 있다면, 차원 수를 줄이거나 분석을 모니터링에서 범용 처리 시스템으로 옮기는 것 같은 대안 솔루션을 조사하세요.

숫자가 어떤지 더 잘 알려면 node_exporter를 살펴봐요. node_exporter는 마운트된 모든 파일시스템에 대한 메트릭을 노출해요. 각 노드는 예를 들어 node_filesystem_avail에 대해 수십 개의 타임시리즈를 가질 거예요. 노드가 10,000개라면 node_filesystem_avail에 대해 대략 100,000개의 타임시리즈를 갖게 되는데, 이는 Prometheus가 처리하기에 괜찮아요.

이제 사용자당 쿼터(quota)를 추가한다면, 10,000개 노드의 10,000명 사용자로 빠르게 수백만 단위에 도달할 거예요. 이것은 Prometheus의 현재 구현으로는 너무 많은 거예요. 더 작은 숫자라도, 이 머신에 잠재적으로 더 유용한 다른 메트릭을 더 이상 가질 수 없다는 기회 비용(opportunity cost)이 있어요.

확신이 없으면 라벨 없이 시작하고, 구체적인 사용 사례가 생김에 따라 시간이 지나며 라벨을 추가하세요.

카운터 vs 게이지, 요약 vs 히스토그램

주어진 메트릭에 네 가지 주요 메트릭 타입 중 어느 것을 사용할지 아는 것이 중요해요.

카운터와 게이지 사이에서 고르는 간단한 엄지손가락 규칙이 있어요: 값이 내려갈 수 있다면 게이지예요.

카운터는 올라갈 수만 있어요(그리고 프로세스 재시작 같은 때 리셋돼요). 이벤트 수를 누적하거나, 각 이벤트에서 무언가의 양을 누적하는 데 유용해요. 예를 들어 총 HTTP 요청 수, HTTP 요청에서 보낸 총 바이트 수예요. 원시 카운터는 거의 유용하지 않아요. rate() 함수를 사용해 증가하는 초당 비율을 얻으세요.

게이지는 설정하거나, 올리거나 내릴 수 있어요. 진행 중인 요청, 여유/총 메모리, 온도 같은 상태의 스냅샷에 유용해요. 게이지에 절대 rate()를 취하면 안 돼요.

요약과 히스토그램은 더 복잡한 메트릭 타입으로, 별도 섹션에서 다뤄요.

타임스탬프, 시간이 아니라 (Timestamps, not time since)

무언가가 발생한 이후의 시간을 추적하고 싶다면, 발생한 이후 경과한 시간이 아니라 발생한 Unix 타임스탬프를 내보내세요.

타임스탬프를 내보내면 time() - my_timestamp_metric 표현식을 사용해 이벤트 이후 시간을 계산할 수 있어서, 업데이트 로직의 필요를 없애고 업데이트 로직이 멈추는 것으로부터 보호해 줘요.

내부 루프 (Inner loops)

일반적으로 계측의 추가 리소스 비용은 운영과 개발에 가져오는 이점보다 훨씬 크지 않아요.

성능에 중요한 코드이거나 주어진 프로세스 안에서 초당 100k번 이상 호출되는 코드는, 업데이트하는 메트릭 수에 대해 주의를 기울이고 싶을 수 있어요.

Java 카운터는 경합(contention)에 따라 증가하는 데 12-17ns가 걸려요. 다른 언어도 비슷한 성능일 거예요. 만약 그 시간이 여러분의 내부 루프에 유의미하다면, 내부 루프에서 증가시키는 메트릭 수를 제한하고 가능하면 라벨을 피하세요(또는 Go의 With()나 Java의 labels()의 반환 값 같은 라벨 조회 결과를 캐시하세요).

또한 시간이나 지속시간을 포함하는 메트릭 업데이트를 조심하세요. 시간을 얻는 것이 syscall을 수반할 수 있으니까요. 성능에 중요한 코드와 관련된 모든 문제와 마찬가지로, 벤치마크가 주어진 변경의 영향을 판단하는 최선의 방법이에요.

누락된 메트릭 피하기 (Avoid missing metrics)

무언가 발생할 때까지 존재하지 않는 타임시리즈는 다루기 어려워요. 일반적인 단순 연산으로는 더 이상 올바르게 처리할 수 없기 때문이에요. 이를 피하려면 미리 존재할 것으로 아는 모든 타임시리즈에 대해 0 같은 기본값을 내보내세요.

대부분의 Prometheus 클라이언트 라이브러리(Go, Java, Python 포함)는 라벨이 없는 메트릭에 대해 자동으로 0을 내보내 줘요.

더 알아보기 (Learn more)