네이버 오픈소스 세미나 - Performance does matter
2019.07.11
<세션 요약>
네이버 서비스에서 사내 서버리스 플랫폼까지 흘러가는 트랜잭션을 추적하고 분석하기 위해 개발한 Pinpoint의 Apache Openwhisk 플러그인과 그 개발 과정을 소개합니다.
Apache Openwhisk는 서버리스 플랫폼을 구축할 수 있는 오픈소스 프로젝트로 스칼라 언어와 Akka 라이브러리를 사용한 Actor 모델에 기반하고 있습니다. 스칼라 언어로 작성된 애플리케이션을 위한 Pinpoint 플러그인을 만들면서 겪었던 문제들과 해결했던 과정들을 위주로 설명드릴 예정입니다.
<연사 소개>
네이버에서 Serverless 플랫폼을 개발하고 있으며, 다양한 오픈소스 프로젝트에 관심이 많습니다.
Apache Openwhisk contributor로 활동하면서, Openwhisk 기반 서버리스 플랫폼의 트레이싱을 위한 Pinpoint 플러그인을 개발하고 컨트리뷰션을 진행하고 있습니다.
12. Why?
‣ 핀포인트가 적용된 서비스에서 액션을 호출했을 때 Distributed Tracing 불가능
‣ 사용자는 플랫폼 내부 동작을 전혀 알 수 없음
‣ 플랫폼 운영 관점에서 복잡한 분산 시스템에서 문제가 발생했을 때 원인 파악이 어려움
Service A Service B
serverless platform
navercorp.com
서버리스 플랫폼을 사용하면
Tracing이 중단됨
15. 소개 페이지에서도 Java, PHP에 대해서만..
Scala 어플리케이션에서 핀포인트 agent가 잘 동작 하나요..?
16. Scala Language
- Functional language
- Scala is compiled to java byte-codes and run on JVM
- Scala is compatible with Java
17. Scala Language
- Functional language
- Scala is compiled to java byte-codes and run on JVM
- Scala is compatible with Java
‣ Scala 애플리케이션에서 핀포인트 agent가 잘 동작하나요?
- 네, 플러그인이 없어서 모르셨겠지만… 핀포인트 agent가 동작합니다.
18. Pinpoint Agent에서 플러그인의 동작 방식
1. Pinpoint Agent 는 JVM이 시작할 때 활성화된 플러그인들을 로드
2. 각 플러그인은 타겟 클래스의 Bytecode를 변경(BCI, Bytecode Instrumentation)하여 타겟 메
소드에 Interceptor 를 추가하거나 새로운 필드(field)를 추가
3. 이후 타겟 메소드가 호출되면 추가된 Interceptor의 before과 after 메소드가 각각 타겟 메소드
앞과 뒤에서 호출되며 Trace 데이터를 기록함
=> BCI로 Bytecode를 변경하기 때문에 스칼라에 의해 Bytecode로 컴파일된
타겟 클래스 명을 알아야 함
19. public static final java.lang.Object $anonfun$validateProperties$1$adapted(scala.collection.immutable.Map,
org.apache.openwhisk.common.Logging, java.lang.String);
Code:
0: aload_0
1: aload_1
2: aload_2
3: invokestatic #246 // Method $anonfun$validateProperties$1:(Lscala/collection/immutable/Map;Lorg/apache/openwhisk/
common/Logging;Ljava/lang/String;)Z
6: invokestatic #252 // Method scala/runtime/BoxesRunTime.boxToBoolean:(Z)Ljava/lang/Boolean;
9: are-turn
Scala의 JVM Bytecode
- 플러그인을 통해 타겟 메소드를 변형하려고 보니 Java와 다르게 Scala에 의해 bytecode 로 컴파일
된 클래스와 메소드 명에는 수많은 $ 기호가 포함
- javap -c common.scala.build.classes.scala.main.org.apache.openwhisk.common.Config$
$
클래스가 $로 끝남
$ $ $ $
메소드 이름에 $가 포함되기도
20. Scala로 컴파일된 JVM Bytecode (Case 1)
- 클래스 명에 $가 들어가는 경우 : Scala object
- Hello.class : Hello$.main() 메소드를 호출하는 wrapper 클래스
- Hello$.class : main 메소드의 실제 구현체
object를 사용하여 클래스 명에
포함된 $는 어색하지만
실제 구현체이므로 사용할 수 있음
object Hello {
def main(arg: Array[String]) {
println("hello world")
}
}
21. Scala로 컴파일된 JVM Bytecode (Case 1)
- 이외에도 클래스 명에 $가 들어가는 경우 : Value Class, Traits …
- 규칙을 가지고 컴파일 되는것을 확인
22. object Hello {
def main(arg: Array[String]) {
var inc = (x:Int) => x+1
var mul = (x: Int, y: Int) => x*y
}
}
Scala로 컴파일된 JVM Bytecode (Case 2)
- 문제는.. Scala 익명함수 ($anonfun$)
- Hello$.$anonfun$main$1 … (x:Int) => x+1
- Hello$.$anonfun$main$2 … (x: Int, y: Int) => x*y
익명함수의 이름은 규칙성을 가지지만
순서만 바뀌어도 언제든지 변경 가능한 값
첫 번째로 정의된 익명함수
두 번째로 정의된 익명함수
23. 어플리케이션에서 사용하는 익명함수
- 함수형 언어 특성상 어쩔 수 없이 익명 함수를 직접 잡아야 하는 경우가 있음
- org.apache.openwhisk.http.BasicHttpService.$anonfun$assignId$2
- akka.http.scaladsl.server.directives.ExecutionDirectives.
$anonfun$handleExceptions$2
24. 익명 함수는 pinpoint.config 설정으로 제공
- 파라미터와 메소드를 정확히 알아야 타겟 메소드를 변경할 수 있음
- 우선은 변경될 가능성이 있는 익명 함수는 설정(pinpoint.config)으로 제공하는 방식을 채택
profiler.openwhisk.transform.targetname=org.apache.openwhisk.http.BasicHttpService.
$anonfun$assignId$2
profiler.openwhisk.transform.targetparameter=org.apache.openwhisk.http.BasicHttpService,
boolean,akka.http.scaladsl.server.RequestContext
이름과 파라미터를 가지고 메소드를 찾는 방식으로 구현
가능한 익명 함수를 타겟 메소드로 잡는것은 최대한 피해야..
25. Akka 의 비동기 메시지 추적하기
- Akka는 오픈소스 프로젝트이며 동시성(concurrent) 및 분산 처리를 위한 툴킷으로
액터(Actor) 프로그래밍 모델을 제공
- Openwhisk는 Actor 모델을 사용하기도 하고 Akka HTTP를 사용하여 HTTP 서버를 구축
- 사용자 요청의 첫 시작점인 Akka HTTP 플러그인을 만들기 시작 (by @lopiter)
26. Thread A
Akka Actor
- 액터가 수행하는 모든 것은 비동기적
(Asynchronous)으로 실행
- Akka의 Dispatcher는 스레드를 할당하고 메시지
를 액터에 푸쉬
- 사용자 요청이 동일한 스레드에서 동작한다는 것
을
보장하지 않음
Actor
Actor
Actor
Mailbox
Mailbox
Mailbox
Thread B
Thread
27. ThreadLocal 에 Trace를 바인딩할 수 없음
“Trace objects are bound to the thread that first created them via
ThreadLocal and whenever the execution crosses a thread boundary,
trace objects are lost to the new thread”
Pinpoint는 Trace 객체를
처음 생성된 스레드에 바인딩
새로운 스레드로 전환되면
Trace 객체를 유실
28. 핀포인트에서 Asynchronous task 처리
In order to trace tasks across thread boundaries, you must take care of
passing the current trace context over to the new thread. This is done
by injecting an AsyncContext into an object shared by both the
invocation thread and the execution thread.
AsyncContext 객체를
특정 객체에 주입하여 공유해야 함
29. Akka HTTP Plugin 에서 비동기 처리
- Akka Http 의 라우터에서 비동기 처리를 위해 RequestContext 객체에 Pinpoint의
AsyncContext를 생성하여 주입
RequestContext
BCI를 통해 AsyncContext를 담을 수 있는 새로운 필드 추가된 상태
target.addField(AsyncContextAccessor.class)
Incoming HttpRequest
Router (Directives)
Pinpoint
AsyncContext
AsyncContextAccessor 를 사용하여
AsyncContext 주입
30. Akka HTTP Plugin 에서 비동기 처리
- RequestContext는 요청의 흐름을 타고 비동기로 호출되는 complete, failed 메소드로 전달
됨
- 이때 AsyncContext를 RequestContext 객체에서 가져오고 Trace를 종료
RequestContextImpl
def complete()
RequestContextImpl
def failed()
RequestContext
Pinpoint
AsyncContext
Trace 종료
31. Akka HTTP Plugin 구현
- 비동기로 동작하는 Akka HTTP 의 트레이싱 결과
비동기 컨텍스트(AsyncContext)로 동작
33. Welcome to the Scala Future
익명함수
익명함수
Future에 의한 비동기 호출 (Async stack trace)
Future에 의한 비동기 호출 (Async stack trace)
AsyncContext를
매번 전달해야됨
34. Welcome to the Scala Future
- 트레이싱이 필요한 수많은 로직들이 Scala
Future API에 의해 비동기로 동작
- 익명함수로 호출되고 심지어는 인자를
전달하지 않기도 함
- AsyncContext를 전달하기가 굉장히 어렵기
에 비동기 Trace를 유지할 방법을 찾아야 함
익명함수
익명함수
Future에 의한 비동기 호출 (Async stack trace)
Future에 의한 비동기 호출 (Async stack trace)
35. Openwhisk 의 TransactionId 를 활용
- Openwhisk의 TransactionId는 요청 흐름을 추적하고 로깅을 수월하게 하기 위해
내부적으로 정의하여 사용되는 객체
- Scala의 암시적 파라미터(implicit parameter)를 통해 동기 또는 비동기적으로 호출되는 메소
드간에는 TransactionId 객체를 공유하며 컨텍스트를 유지
/**
* Receives a message and runs the router.
*/
def route: Route = {
assignId { implicit transid =>
respondWithHeader(transid.toHeader) {
/**
* Spawns a container in detached mode.
*
* @param image the image to start the container with
* @param args arguments for the docker run command
* @return id of the started container
*/
def run(image: String, args: Seq[String] = Seq.empty[String])
(implicit transid: TransactionId): Future[ContainerId]
요청의 처음 시작에서
TransactionId 객체가 만들어지고 할당됨
36. TransactionId에 AsyncContext 주입
- AsyncContext는 Akka HTTP에서 만들어진 것을 사용
- Akka HTTP Plugin에서 시작된 AsyncContext를 통해 Trace를 지속할 수 있도록
RequestContext에 주입되어 있는 AsyncContext 를 다시 TransactionId 객체에 주입
TransactionId
Pinpoint
AsyncContext
RequestContext
Pinpoint
AsyncContext
Akka HTTP Plugin Openwhisk Plugin
37. TransactionId에 AsyncContext 주입
- 이후 트레이싱이 필요한 메소드들은 암시적 파라미터(implicit parameter)로 전달받은
TransactionId의 AsyncContext 를 사용하여 AsyncTrace를 지속할 수 있음
TransactionId
Openwhisk Plugin
Pinpoint
AsyncContext
/**
* Spawns a container in detached mode.
*
* @param image the image to start the container with
* @param args arguments for the docker run command
* @return id of the started container
*/
def run(image: String, args: Seq[String] = Seq.empty[String])
(implicit transid: TransactionId): Future[ContainerId]
이 경우 플러그인에서 run 메소드를
Transform 하면 TransactionId 객체 접근 가능
41. 최소한의 메소드만 변형할수는 없을까?
- Openwhisk는 내부적으로 로그를 출력하거나 메트릭을 수집할 때 특정 메소드의 실행 시간을
측정하기 위해 TransactionId 에 구현된 started(), finished() 메소드를 사용 중
메소드 시작 지점에 started 호출
종료 지점에 finished 호출
42. started, finished 메소드를 Transform
- 하나의 메소드에서 트레이싱이 이루어지는
기존의 방식을 두 개의 메소드로 분리
def started() {
Interceptor.before()
// something
Interceptor.after()
}
def finished() {
Interceptor.before()
// something
Interceptor.after()
}
로직 실행
started 메소드가 호출되면 Interceptor 에서 Trace 시작
(일반적인 Interceptor와 다르게 Trace를 종료하지 않음)
이후 호출되는 finished 메소드에서 Trace 종료
(started 메소드에서 시작된 Trace를 종료함)
Interceptor.before()
Interceptor.before()
Interceptor.after()
Interceptor.after()
def someMethod() {
}
43. started, finished 메소드를 Transform
- 실제 메소드명이 아닌 started 메소드로 전달된 로그 마커를 메소드 이름으로 기록하고
로그 메시지는 새로운 marker.message annotation을 정의하고 기록
[2019-07-08T08:01:21.644Z] [INFO] [#tid_uZ9wiZOezssfabRZW2wcVPHxWqys2aF2]
[ActionsApi] action action activation id: 81355efc90c847ddb55efc90c887dd74
[marker:controller_loadbalancer_start:8760]
44. 이제는?
- 어플리케이션에서 started, finished 메소드로 로그를 찍어주기만 하면 Pinpoint에서 Tracing 가능
- 플러그인 관리 주기가 길어짐 (최소 6개월 이상)
46. 컴포넌트간의 메시지 전달하기
- Controller -> Invoker 컴포넌트간에는 ActivationMessage 객체를 주고 받음
- OpenTracing에서 사용하는 traceContext 필드에 플러그인에서 Pinpoint Transaction 정보
를 주입
Kafka에 메시지를 보내기 전에 traceContext를 조작
47. 컴포넌트간의 메시지 전달하기
- Invoker는 ActivationMessage를 받으면 traceContext 필드에서 PinpointTransaction 정보
를 가지고 다시 Tracing을 시작
Invoker
48. 다 잊어버리셔도 됩니다
‣ Pinpoint agent 를 어플리케이션 실행 시 추가하면 바로 사용 가능 (핀포인트의 장점)
‣ -javaagent:${pinpointPath}/pinpoint-bootstrap-1.8.3.jar
-Dpinpoint.applicationName=ApplicationName
-Dpinpoint.agentId=AgentId
참 쉽죠..?
50. 정리하면..
‣ 핀포인트로 스칼라(Scala) 어플리케이션을 Tracing할 수 있습니다.
‣ 익명함수는 최대한 피하고.. 피할 수 없다면 설정으로 하는게 좋습니다.
(더 좋은 방법이 있다면 알려주세요!)
‣ 비동기 호출을 요청하기 위해 AsyncContext를 전달 하는 방법을 먼저 찾아야 합니다.
‣ 코드 변경이 많은 프로젝트는.. 공통 인터페이스를 찾아 Transform 하면 좋습니다.