13. 13
Why
• 트러블 슈팅이 힘들다.
• 콘솔 들어가기 싫다
• 수십기가의 로그
• 환경, 옵션
• 수많은 종류의 라이브러리 + 다양한 버전의 라이브러리
• 야근하기 싫다
• 자신을 자동화
• 트러블슈팅시 생각하던 바, 관찰하던 뷰를 다른 사람에게도 제공
• 글로 문제를 설명하려니 어렵더라
• 잘못된 분석으로 인해 문제가 재발
19. 19
풀어야 할 문제
RPC 추적의 의미
Node1과 Node2 사이의 RPC간의
관계를 어떻게 찾을것인가?
메시지를 연관관계를 나타내는 TAG
20. 20
TAG 동작
• Span : RPC 추적을 위한 기본 단위. RPC가 도착했을 때 처리한 작업
• Trace : 연관된 Span의 집합. Span의 집합은 TransactionId가 같음.
Trace는 SpanId와 ParaentSpanID를 통해 트리 구조로 정렬됨
• TraceId
• TrasantionId는 message id로 전체 서버군에서 unique 한 아이디
• SpanId, ParentId로 message의 관계를 정렬
24. 24
TomcatA
어플리케이션 적용 예
@Controller
public class TestController {
@RequestMapping("/test")
@ResponseBody
public String test() throws IOException {
HttpGet get = new HttpGet("http://TomcatB/hello");
HttpResponse response = httpClient.execute(get);
return EntityUtils.toString(response.getEntity());
}
}
TomcatB
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "world!";
}
}
• Hello world! sample
25. 25
TomcatA
@Controller
public class TestController {
@RequestMapping("/test")
@ResponseBody
public String test() throws IOException {
HttpGet get = new HttpGet("http://TomcatB/hello");
HttpResponse response = httpClient.execute(get);
return EntityUtils.toString(response.getEntity());
}
}
TomcatB
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "world!";
}
}
어플리케이션 적용 예
26. 26
TomcatA
@Controller
public class TestController {
@RequestMapping("/test")
@ResponseBody
public String test() throws IOException {
HttpGet get = new HttpGet("http://TomcatB/hello");
HttpResponse response = httpClient.execute(get);
return EntityUtils.toString(response.getEntity());
}
}
TomcatB
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "world!";
}
}
어플리케이션 적용 예
27. 27
TomcatA
@Controller
public class TestController {
@RequestMapping("/test")
@ResponseBody
public String test() throws IOException {
HttpGet get = new HttpGet("http://TomcatB/hello");
HttpResponse response = httpClient.execute(get);
return EntityUtils.toString(response.getEntity());
}
}
TomcatB
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "world!";
}
}
어플리케이션 적용 예
28. 28
TomcatA
@Controller
public class TestController {
@RequestMapping("/test")
@ResponseBody
public String test() throws IOException {
HttpGet get = new HttpGet("http://TomcatB/hello");
HttpResponse response = httpClient.execute(get);
return EntityUtils.toString(response.getEntity());
}
}
TomcatB
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "world!";
}
}
어플리케이션 적용 예
29. 29
TomcatA
@Controller
public class TestController {
@RequestMapping("/test")
@ResponseBody
public String test() throws IOException {
HttpGet get = new HttpGet("http://TomcatB/hello");
HttpResponse response = httpClient.execute(get);
return EntityUtils.toString(response.getEntity());
}
}
TomcatB
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "world!";
}
}
어플리케이션 적용 예
30. 30
TomcatA
@Controller
public class TestController {
@RequestMapping("/test")
@ResponseBody
public String test() throws IOException {
HttpGet get = new HttpGet("http://TomcatB/hello");
HttpResponse response = httpClient.execute(get);
return EntityUtils.toString(response.getEntity());
}
}
TomcatB
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "world!";
}
}
어플리케이션 적용 예
31. 31
TomcatA
@Controller
public class TestController {
@RequestMapping("/test")
@ResponseBody
public String test() throws IOException {
HttpGet get = new HttpGet("http://TomcatB/hello");
HttpResponse response = httpClient.execute(get);
return EntityUtils.toString(response.getEntity());
}
}
TomcatB
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "world!";
}
}
• Distributed Transaction TAG
• TraceId 생성
TRANSACTION_ID : 전체 RPC 호출을 하나로 묶을수 있는 Key
SPAN_ID : 나의 ID
PARENT_SPAN_ID : 부모의 ID
내부동작
32. 32
TomcatA
@Controller
public class TestController {
@RequestMapping("/test")
@ResponseBody
public String test() throws IOException {
HttpGet get = new HttpGet("http://TomcatB/hello");
HttpResponse response = httpClient.execute(get);
return EntityUtils.toString(response.getEntity());
}
}
TomcatB
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "world!";
}
}
• Distributed Transaction TAG
• TraceId 생성
TRANSACTION_ID : TomcatA^시작시간^1
SPAN_ID : 10 (Random)
PARENT_SPAN_ID : -1 (ROOT)
내부동작
33. 33
TomcatA
@Controller
public class TestController {
@RequestMapping("/test")
@ResponseBody
public String test() throws IOException {
HttpGet get = new HttpGet("http://TomcatB/hello");
HttpResponse response = httpClient.execute(get);
return EntityUtils.toString(response.getEntity());
}
}
TomcatB
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "world!";
}
}
• Distributed Transaction TAG
• Spring Controller Method 레코딩
내부동작
34. 34
TomcatA
@Controller
public class TestController {
@RequestMapping("/test")
@ResponseBody
public String test() throws IOException {
HttpGet get = new HttpGet("http://TomcatB/hello");
HttpResponse response = httpClient.execute(get);
return EntityUtils.toString(response.getEntity());
}
}
TomcatB
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "world!";
}
}
• Distributed Transaction TAG
• HttpClient.execute()의 호출을 가로채서 HttpGet에 TRACE_ID를 세팅한다.
• Child TraceId 생성
TRANSACTION_ID : TomcatA^시작시간^1 -> TomcatA^시작시간^1
SPAN_ID : 10 -> 20
PARENT_SPAN_ID : -1 -> 10
• Child TraceId 를 HttpGet에 세팅
HttpGet.setHeader(PINPOINT_TRANSACTION_ID, “TomcatA^시작시간^1”)
HttpGet.setHeader(PINPOINT_SPAN_ID, “20”)
HttpGet.setHeader(PINPOINT_PARENT_SPAN_ID, “10”)
내부동작
35. 35
TomcatA
@Controller
public class TestController {
@RequestMapping("/test")
@ResponseBody
public String test() throws IOException {
HttpGet get = new HttpGet("http://TomcatB/hello");
HttpResponse response = httpClient.execute(get);
return EntityUtils.toString(response.getEntity());
}
}
TomcatB
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "world!";
}
}
• Distributed Transaction TAG
• TAG된 Request를 TomcatB로 전송.
Tag
Request
내부동작
36. 36
TomcatA
@Controller
public class TestController {
@RequestMapping("/test")
@ResponseBody
public String test() throws IOException {
HttpGet get = new HttpGet("http://TomcatB/hello");
HttpResponse response = httpClient.execute(get);
return EntityUtils.toString(response.getEntity());
}
}
TomcatB
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "world!";
}
}
• Distributed Transaction TAG
• TomcatB accept
Check Header : HttpServletRequest.getHeader(PINPOINT_TRANSACTION_ID)
• Header에서 TraceId 를 인식하여 Child로 동작
TRANSACTION_ID : TomcatA^시작시간^1
SPAN_ID : 20
PARENT_SPAN_ID : 10
Tag
Request
내부동작
37. 37
TomcatA
@Controller
public class TestController {
@RequestMapping("/test")
@ResponseBody
public String test() throws IOException {
HttpGet get = new HttpGet("http://TomcatB/hello");
HttpResponse response = httpClient.execute(get);
return EntityUtils.toString(response.getEntity());
}
}
TomcatB
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "world!";
}
}
• Distributed Transaction TAG
HBase
TRANSACTION_ID : TomcatA^시작시간^1
SPAN_ID : 20
PARENT_SPAN_ID : 10
Collector
RowKey
TomcatA^시작시간^1
20
10
Hello() 호출정보
TraceData
내부동작
38. 38
TomcatA
@Controller
public class TestController {
@RequestMapping("/test")
@ResponseBody
public String test() throws IOException {
HttpGet get = new HttpGet("http://TomcatB/hello");
HttpResponse response = httpClient.execute(get);
return EntityUtils.toString(response.getEntity());
}
}
TomcatB
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "world!";
}
}
• Distributed Transaction TAG
HBase
TRANSACTION_ID : TomcatA^시작시간^1
SPAN_ID : 10
PARENT_SPAN_ID : -1
Collector
RowKey
TomcatA^시작시간^1
20 10
10
Hello() 호출정보
-1
Test() 호출정보
TraceData
내부동작
40. 40
트러블 슈팅
• 인프라의 각 구성요소가 정상적으로
구축이 되었는가?
• 해외Proxy에서 한국의 특정 서버로
사용자 요청의 흐름이 기대한 대로 인가?
• 응답시간이 느린 구간과 API가 있는가?
• 느린 구간에 대한 프로파일링 데이타 제공
• 성능 패턴 데이타 제공
• 개발->QA->운영단계에서의 연속적인 확인
44. 44
WARNING
• 가능한 최신 버전을 Hadoop 패밀리 사용
• 관리되는 Cloudera(CDH), Hortonworks(HDP) 권장
• Pinpoint를 사용하는 사람은 Java 개발자
• 겸사겸사 HBase, Hadoop도 살펴보고, 기왕이면 소스도 까보자
• 사실 저도 다 모릅니다.
- 뭐든지 아는건 아니야. 알고 있는것만 알뿐
Backend java developer 입니다.
Java 기반의 네크워크 라이브러리, 프레임워크, 트러블슈팅을 지원했고,
현재는 pinpiont의 TL을 하고 있구. pinpoint를 직접개발했습니다.
이 책의 저자이기도 합니다.
과거에는 사용자가 얼마 안되고, 서버도 얼마 안됬습니다. 서버의 대수도 작았고, 단순했습니다.
현재는 사용자가 많아지고, 서비스가 점점 복잡해지면서, 복잡도가 증가하였습니다.
옆 화면은 netflix의 서버모습인데. 굉장히 복잡한것을 볼수 있습니다.
상황을 살펴 보면 시스템이 점점 복잡해 지면서 문제가 발생하고 있는것을 볼수 있는데.
더 이상은 기존의 방식. 즉 로그를 살펴 다던가? 개별 시스템을 살펴본다든가 하는것으로는 시스템의 복잡도를 커버하기 힘들어지고 있다는 것을 느낄수 있습니다.
즉 이 상황을 개선할수 있는 새로운 무엇이 필요하다는 생각을 할수 있습니다.
구글은 이미 이 문제를 해결하기 위해서 dapper란것을 2010년에 발표 했습니다. 아쉽게 비공개입니다.
Twitter는 dapper를 기반으로 zipkin이라는 플랫폼을 개발했습니다.
여러가지 상용 솔류션들도 있습니다.
한국에서 가장 유명한 제니퍼도 있구요.
세계에서 가장 좋다고 하는 3개의 제품군도 있습니다.
네이버도 이문제를 해결하기위해서 Pinpoint를 개발했습니다.
네이버의 시스템도 정말 복잡하거든요.
우리 시스템의 보잡도를 모르고 있다가, Pinpoint를 통해서 우리가 이렇게 복잡했나를 발견하기도 합니다.
개인적으로 필요했습니다.
트러블슈팅을 담당했지만. 시스템이 복잡하니 트러블슈팅이 점점 힘들어졌습니다.
알아야 될것도 많구요.
일일이 서버 콘솔들어가기도 싫고, 수십기가 로그 볼려니 눈도 아프고 힘들고, 개별 라이브러리를 다 살펴보려니 피곤하더라구요
그리고 트러블슈팅하느라 야근하기도 싫구요.
또 하나는 여러 조직에서 트러블슈팅하고 계신데, 이걸 좀더 편하게 지원을 하고 싶었습니다.
구지 제가 메일을 쓰고 분석을 하지 않더라도, 제가 보고 있는 좀더 편하게 제공하고 싶었습니다.
앞에는꺼는 아무래도 좀 좋은 동기고, 살작 애매한 동기는
트러블슈팅도 계속하다 보니. 시스템이 안정화되서 할일이 없어지더라구요.
최초 시작할때는 거의 1년 내내 장애지원을 했는데. 해가 갈수록 빈도가 줄거나, 기존에 해결한 방안을 대입하면 되는거라. 시간이 줄었습니다.
아무튼 할일이 전반적으로 줄었습니다. 그런데 할일이 줄면 어떻게 될까요?
뭐긴 뭐가요. 해고죠.
진정한 해결책입니다. 더군다나 공짜
유지보수가 어렵게 짜면 평생 개발자로 먹고살수 있습니다. ㅎㅎ. 이건농담인거 아시죠.
아무튼 이건 나쁜방향으로의 해결이죠. 이렇게 하면 회사도 망하고 자신의 발전도 없어집니다.
좋은 방향의 해결책으로 이런 문제를 타계할수 있는 pinpint를 개발하게 되었습니다.
Pinpoint의 github 내용인데.
간단하게 설명하면 Pinpoint는 APM툴이고, java의 성능 모니터링을 하는 플랫폼입니다.
시스템 모니터링 툴과는 다르게, APM툴은 걷을 보는게 아니고,
실제 어플리케이션의 수행하는 정보를 프로파일 할수 있는 툴입니다.
시스템 모니터링이 사진기라면, APM툴은 X-ray 처럼 내부를 볼수 있는 툴입니다.
말로 들으면 어려우실거니. 직접 화면을 보도록 하겠습니다.
화면을 보면 단순한데, 이와 같이 어플리케이션이 어떻게 동작하는 비주얼 하게 볼수 있습니다.
핀포인트가 해결한 문제를 보겠습니다
중요한 RPC 추적에 대한 내용인데요.
핵심의 메시지의 관계를 어떻게 짝것인가에 대한 문제입니다.
이문제를 해결하기 위해서 핀포인트는 메시지에 추적을 위한 tag를 달고 있습니다.
추적태그 는 해당 그림과 같이 동작합니다.
중요한 추적키는 transaction id 전체 를 관통하는 유니크 아이디가 존재하구요.
이 키를 기반으로 부모 자식관계를 정렬할수 있는 spanId와 parentSpan가 존재합니다.
이 추적키를 기반으로 RPC메시지를 추적합니다.
그리고 이 추적 태그에 처리는 bytecode instrumentation을 기반으로 자동으로 핀포인에서 처리합니다.
원리는 간단한게. 추적할 메소드를 대상으로 추적 코드를 앞뒤로 심는것으로 이해하면 됩니다.
그리고 이 추적 태그에 처리는 bytecode instrumentation을 기반으로 자동으로 핀포인에서 처리합니다.
원리는 간단한게. 추적할 메소드를 대상으로 추적 코드를 앞뒤로 심는것으로 이해하면 됩니다.
Pinpoint의 핵심 가치는 이 두가지를 기반으로 합니다.
분산 RPC추적이 중요한 이유는 네이버의 서비스가 복잡하기 때문입니다.
마이크로 서비스 아키텍쳐 등이 요즘 유행하고 있는데. 네이버에서는 이미 유사한 스타일로 서비스가 구성되고 있습니다.
그리고 코드를 수정하지 않는것 또한 중요합니다.
네이버의 서비스수가 엄청 많기 때문입니다.
좀더 쉽게 애플리케이션 적용예를 보겠습니다.
해당 애플리케이션에 pinpoint를 적용하면 아래와 같은 데이터를 얻을수 있습니다. 코드를 따라가 보겠습니다.
Tomcat b로 원격 호출이 들어간것을 모습이구요.
이와 같은 성능 데이터를 핀포인트를 통해 얻을수 있구요.
이러한 성능 정보를 핀포인트가 어떻게 추적하지는 내부동작을 보겠습니다.
핀포인트는 메시지를 받으면 traceid라고 추적키의 집합을 생성합니다.
실제 데이터의 예는 이와 같습니다.
스프링 콘트롤러 메소드 정보를 레코딩하구요
원격 호출시 핀포인트에서 자동으로 추적에 필요한 traceId를 Get메시지에 세팅합니다.
태그된 리퀘스트가 원격지로 가게됩니다.
톰캣 b는 태크된 리퀘스트를 보고 자식 노드로 동작하게 되구요.
Tomcatb의 로직이 종료하면 성능 데이터를 collector로 보내게 됩니다.
Tomcat a의 로직이 종료하면 역시 성능 데이터를 collector로 보내게 되구요.
저장된 정보를 읽어서 보여주게 되면 이와 같은 성능 정보를 얻을수 있습니다.
이를 활용한 트러블슈팅 사래는 많습니다.
일단 데이터를 통해 대부분의 성능 문제를 바로바로 집어 낼수 있습니다.
서버맵 데이터와, 개별 request데이터를 살펴 보시면 느린 구간을 직관으로 잡아 낼수 있습니다.
핀포인트를 개발하면서 어려운 점이 있는데요.
핀포인트의 백앤드가 되는 하둡패밀리들이 굉장히 어렵습니다. 분산환경을 기반으로 하고 있어서 복잡하기도 하구요.
핀포인트 질문중에 가장많이 물어보는 빈도도 제일높은것 같습니다.
실제로도 어렵습니다.
Hbase도 죽고, hadoop도 무한루프도 돌구요.
재시작해도 회복이 안되기도 했습니다.
이런 일이 발생하면 굉장히 난감한데. 제심정은
이랬습니다.
일단 이런 문제는 이미 다 해결된 상태고, 운영중인 버전을 너무 구버전을 써서 발생한 문제이기도 합니다.
버전업을 게을리한데서 나온 대가네요.
만약 문제가 생겨도 절대 당황하지 마시기 바랍니다.
성능데이터에 좀 문제 생겨도 비즈니스에는 큰문제가 없으니. 쉼호흡을 하고 한개씩 해결 하시면 됩니다.
Pinpoint를 사용하는 사람은 자바 개발자분들이 많을건데.
기회삼아서 이번기회에 hbase도 보고, 하둡도 살펴보고, 기왕이면 소스도 보면 좋잖아요.
저도 다 아는게 아니기도 합니다.
보다 보면 전문가가 될수도 있으니 한번 해는것도 괜찮을것 같습니다.
어떤 사람도 처음부터 전문가가 아니잖아요, 해봐야 아는겁니다.
핀포인트를 하면서 기회가 되면 겸사겸사 해보는게 나쁘지는 않을거 같아요.
혹시 제가 말한것처럼 생각하신분 있나요? 그렇다면
세상일은 잘 모르는건데. 여기 듣는 여러분이 대상일수도 있어요.
계획대로네요. 최초 만들때부터 가능한 핀포인트 기술 선택을 향후에 충분히 활용할수 있을 만한것으로 선택했습니다.
아무래도 개인의 발전이나, 동기유발에 긍정적으로 접근하기 위해서요.
그리고 핀포인트도 오픈소스잖아요. 소스가 열려 있구요. 이것도 충분히 투자해 볼만하지 않아 합니다.
한편으로는 다행인점도 있습니다
중요설계 사상이 있는데.
백앤드가 죽어도, 어플리케이션은 정상으로 돌아가는것을 최우선 목표로 하고 있습니다.
그렇지만 사상은 사상이고, 이론과 현실은 다르잖아요,
말로는 다되죠.
사실 제가 앞에서는 저렇게 큰소리를 쳤는데. 알고보면 뒤에서
이랬습니다. 테스트나 이론으로는 검증했지만. 진짜로 그렇게 동작할지는 리얼환경에서 실제 동작해봐야 아는거라서요.
그런데 다행이 현실에서 검증되었습니다. 안심하셔도 됩니다.
그렇다고 해서 방심은 하면 안되니. 반드시 개발에서 먼저 검증해서 운영으로 넘어가는걸 추천드립니다.
이 과정에 버그가 발생하면 저희한테 꼭 알려주셨으면 하구요.
앞의 로드맵은 이와 같습니다.
코어 기능에만 집중하다 보니. 좀 미뤄진 경향이 있는데. 만들려고 하고있습니다.
실시간 데이터 같은 경우는 ActiveThread정도 필요하시잖아요. 이것도 만들려고 하고 있습니다.
점점더 발전시킬거니. 기대해주셔
아마 5월에도 D2 오픈 세미나가 있을것 같습니다. 관심있으시면 참석 부탁드리구요.