티스토리 뷰
이 글은 Pinpoint에 대해 공부하는 과정에서 아래 링크의 글을 읽으며 간략하게 정리한 글입니다.
https://d2.naver.com/helloworld/1194202
Pinpoint는 대규모 분산 시스템의 성능을 분석하고 문제를 진단하는 플랫폼.
MSA(Microservices Architecture) 같은 n계층 아키텍처에서 문제를 해결하는데 도움을 주기 위해 n계층 아키텍처를 효과적으로 추적하고 가시성을 제공.
Pinpoint의 특징
- 분산된 애플리케이션의 메시지를 추적할 수 있는 분산 트랜잭션 추적
- 애플리케이션 구성을 파악할 수 있는 애플리케이션 토폴로지 자동 발견
- 대규모 서버군을 지원할 수 있는 수평 확장성
- 코드 수준의 가시성을 제공해 문제 발생 지점과 병목 구간을 쉽게 발견
- bytecode instrumentation 기법으로 코드를 수정하지 않고 원하는 기능 추가
분산 트랜잭션 추적
Google Dapper 스타일의 추적 방식을 변형해서 호출 헤더에 태그 데이터를 추가해 이를 추적에 사용하는 방식을 사용한다. HTTP를 예로 들면, HTTP 요청 전송 시 HTTP 헤더에 메시지 태그 정보를 넣고 이 정보를 메시지 간의 연결 고리로 활용해 메시지를 추적하는 것.
Pinpoint의 자료구조
Pinpoint의 핵심 자료구조는 Span과 Trace, TraceId로 이루어져 있습니다.
- Span: RPC(remote procedure call) 추적을 위한 기본 단위로 RPC가 도착했을 때 처리한 작업을 나타내며 추적에 필요한 데이터가 들어있다.
- Trace: Span의 집합으로, 연관된 RPC(Span)의 집합으로 구성. 연관된 Span이란 TransactionId가 같은 Span들을 의미한다. Trace는 SpanId와 ParentSpanId를 통해 트리 구조로 정렬된다.
- TraceId: TransactionId와 SpanId, ParentSpanId로 이루어진 키의 집합. TransactionId는 메시지의 아이디이며, SpanId와 ParentSpanId는 RPC의 부모 자식 관계를 나타낸다.
- TransactionId(TxId): 분산된 노드를 거쳐 다니는 메시지의 아이디, 전체 서버군에서 중복되지 않아야 한다.
- SpanId: RPC 메시지를 받았을 때 처리되는 작업(job)의 아이디를 정의한다. RPC가 노드에 도착했을 때 생성
- ParentSpanId(pSpanId): 호출한 부모의 SpanId를 나타낸다.
TraceId의 작동 방법
다음과 같이 4개의 노드와 3번의 RPC가 존재하는 경우에서 TraceId가 어떻게 작동하는지 설명
그림 2에서 TransactionId(TxId)는 3개의 RPC가 한 개의 연관된 트랜잭션이라는 것을 표현.
하지만 TxId만으로는 연관되어 있다는 것만 알 수 있다. 따라서 SpanId와 ParentSpanId(pSpanId)를 통해 RPC간의 정렬을 한다.
이처럼 Pinpoint는 TransactionId로 연관된 N개의 Span을 찾아내고, SpanId와 ParentSpanId로 N개의 Span을 트리로 정렬한다.
코드 수정이 필요 없는 bytecode instrumentation
분산 트랜잭션 추적이 좋다고는 하지만 이를 위해 RPC 호출 시 태그 정보를 직접 추가하도록 개발하는 것은 부담스러운 일이다. 이를 해결하기 위해 Pinpoint는 bytecode instrumentation 기법을 도입했다. Pinpoint Agent가 RPC 호출 코드를 가로채 태그 정보를 자동으로 처리한다.
이 방식은 수동방식에 비해 난이도와 위험성이 높은 개발 방법.
하지만 이득을 고려했을 때 bytecode instrumentation 방식이 낫다고 판단하여 이를 적용.
bytecode instrumentation의 숨은 가치
API를 노출하지 않음
API를 노출해 개발자가 API를 사용하면 API 제공자가 API를 쉽게 변경할 수 없다는 제약이 생김.
bytecode instrumentation을 사용하면 추적 API를 사용자에게 노출하지 않아도 되므로 API 의존성 문제를 겁내지 않고 디자인을 지속적으로 개선할 수 있다. API 안정성보다는 기능 발전과 디자인 개선에 유리하다.
손쉬운 적용과 해제
코드를 변경할 필요가 없으므로 적용과 해제가 쉽다.
JVM 구동 시 JVM 시작 스크립트에 Pinpoint Agent 설정 3개만 추가하면 쉽게 Pinpoint를 적용할 수 있다.
- javaagent:$AGENT_PATH/pinpoint-bootstrap-$VERSION.jar
- Dpinpoint.agentId=
- Dpinpoint.applicationName=<동일 서비스임을 나타내는 이름(AgentId의 집합)>
Pinpoint때문에 문제가 발생했다면 이 설정을 제거하기만 하면 적용이 해제된다.
bytecode instrumentation의 작동 방법
Pinpoint는 클래스 로드 시점에 애플리케이션 코드를 가로채 성능 정보와 분산 트랜잭션 추적에 필요한 코드를 주입한다. 애플리케이션 코드에 직접 추적 코드가 주입되므로 성능이 좋다.
Pinpoint는 API 인터셉트 부분과 성능 데이터 기록 부분을 분리했다. 추적 대상 메서드에 인터셉트를 주입해 앞뒤로 before() 메서드와 after() 메서드를 호출하게 하고 before() 메서드와 after() 메서드에 성능 데이터를 기록하는 부분을 구현했다.
Pinpoint Agent의 성능 최적화 부분 - 본 링크 참조
애플리케이션 적용 예
다음은 TomcatA와 TomcatB에 Pinpoint를 설치해 얻을 수 있는 데이터다. 개별 노드의 추적 정보를 한 개의 트랜잭션으로 볼 수 있는데 이것이 분산 트랜잭션 추적 기능이다.
메서드별로 Pinpoint가 하는 일은 다음과 같다.
- 요청이 TomcatA에 도착하면 Pinpoint는 TraceId를 발급한다.
- TX_ID: TomcatA^TIME^1
- SpanId: 10
- ParentSpanId: -1(Root)
- Spring의 Controller 정보를 기록한다.
- HttpClient.execute() 메서드의 호출으르 가로채 HttpGet에 TraceId를 설정한다.
- 자식 TraceId를 생성한다.
- TX_ID: TomcatA^TIME^1 -> TomcatA^TIME^1
- SPAN_ID: 10 -> 20
- PARENT_SPAN_ID: -1 -> 10(부모의 SpanId)
- 자식 TraceId를 HTTP 헤더에 설정한다.
- HttpGet.setHeader(PINPOINT_TX_ID, "TomcatA^TIME^1")
- HttpGet.setHeader(PINPOINT_SPAN_ID, "20")
- HttpGet.setHeader(PINPOINT_PARENT_SPAN_ID, "10")
- 자식 TraceId를 생성한다.
- 태그된 요청이 TomcatB로 전송된다.
- TomcatB는 전송된 요청에서 헤더를 확인한다.
- HttpServletRequest.getHeader(PINPOINT_TX_ID)
- 헤더에서 TraceId를 인식해 자식 노드로 동작한다.
- TX_ID: TomcatA^TIME^1
- SPAN_ID: 20
- PARENT_SPAN_ID: 10
- TomcatB는 전송된 요청에서 헤더를 확인한다.
- SpringController 정보를 기록하고 요청의 메시지 처리를 종료한다.
6. TomcatB의 요청 처리가 끝나면 Pinpoint Agent는 추적 데이터를 Pinpoint Collector에 보내 HBase에 저장한다.
7. TomcatB의 HTTP 호출이 종료된 후 TomcatA의 요청 처리도 종료된다. Pinpoint Agent는 추적 데이터를 Pinpoint Collector로 전송해 HBase에 저장한다.
8. UI는 추적 데이터를 HBase에서 읽고 트리를 정렬해 콜 스택을 생성한다.