주변에서 Play Framework과 Scala에 대한 관심이 많아지고 있는 것 같다. 지난 2년간 이 조합으로 application 서버를 개발하면서 성능이나 개발 생산성에서 아주 만족하고는 있지만, 아직은 reference가 많지 않아서 중간에 어려움을 많이 겪었다. 오늘은 Scala + Play Framework2의 조합에서, thread를 활용해 성능 향상을 할 수 있는 부분에 대해서 공유해보려고 한다.
Scala 및 Play Framework에 대해서는 이전에 작성한 포스팅을 참고.
1. 접근 방향
일반적인 웹서비스는 보통 다음의 flow로 사용자의 request를 처리하는 것 같다.
- 사용자 request 발생
- DB나 Cache 서버에서 request의 처리에 필요한 값들을 조회
- business 로직 처리
- DB에 변경된 값 저장
- 사용자에게 response를 보내서 처리를 완료
규모가 있거나 좀더 복잡한 서비스의 경우에는 DB서버나 Cache 서버 외에 또 다른 application 서버를 이용하기도 한다. 이때 병목이 되는 부분은, 대개의 경우에는 외부의 서버와 통신하는 부분이다. 즉, DB, Cache, 다른 application 서버에 무언가를 요청하고 결과를 기다리는 작업에서 CPU 연산과는 order가 다른 수준으로 시간이 걸린다. 그래서, 어떻게 외부 서버와 효율적으로 통신을 하느냐가 application 서버의 성능을 결정 짓는 중요한 요소이다.
Thread가 있는 언어의 경우에는 다음과 같은 방법으로 성능을 향상 시킬 수 있는 것 같다.
- thread pool을 분리한다.
응답성을 높이기 위해서, 사용자의 응답을 처리하는 thread pool과 외부 서버와 통신을 하느라 작업이 오래 걸리는 thread pool을 분리 시킬 수 있다. 이렇게 해놓은 다음, 외부 서버와 통신을 하는 thread pool의 개수를 넉넉하게 잡아두면 성능을 향상 시킬 수 있다.
-
반드시 serial하게 처리할 필요가 없는 작업은 parallel 하게 처리한다.
예를 들어, DB 서버가 여러대로 분리되어 있고, 각각의 DB 서버에서 값을 조회한 뒤에 단순히 합쳐서 response를 주는 상황을 생각해 보자. 이때는 굳이 순차적으로 외부 DB 서버들과 통신할 필요 없이, 동시에 여러 DB 서버와 통신을 시작하면, 응답시간을 줄일 수 있다.
그럼 조금 구체적으로 들어가보겠다.
2. Play 2에서 Thread Pool 분리
Play Framework 2에서는 Akka라는 Asynchronous Library를 이용하여 thread pool을 처리한다. Akka를 이용해서 thread pool을 설정하기 위해서는 Play Framework 2 문서의 ‘Understanding Play thread pools’를 참고하면 된다.
Thread pool을 설정하고, Contexts object를 이용해 어떻게 소스코드에서 이용하는지에 대해서는 위의 문서에 간략히 설명되어 있다. 그런데, 설정에 있어서 매우 중요한 부분인데, Play Framework과 Akka 공식 문서 둘다에서 자세히 설명하고 있지 않은 부분이 있다.
2.1 Thread pool 설정
Akka에서 thread pool이 동작하는 방식이 fork-join-executor과 thread-pool-executor 2가지가 있는데 default는 fork-join-executor이고, 이것이 성능이 더 좋은 것으로 알려져있다.
fork-join-executor의 경우에 다음 3가지 값을 설정하면 된다.
- parallelism-min
동시에 활성화 되는 최소 thread 개수
-
parallelism-max
동시에 활성화 되는 최대 thread 개수
-
parallelism-factor
core 개수 대비 최대가 될수 있는 thread 비율. 서버의 core가 4개이고, 이값이 2인 경우에 thread는 8개까지 될 수 있다.
여기서 조심해야 할 것이, 이 3가지가 교집합이라는 것이다.
예를 들어, core가 4개인 서버에서
parallelism-max = 10
parallelism-facotr = 2
라고 설정을 한 경우에는, 최대 thread 개수는 10이 아니라 8이 된다.
만약 위의 값을 생략하면 어떻게 될까? Akka 문서에 따르면, 위의 값 값들의 default는 다음과 같기 때문에 다음 값으로 설정된다.
parallelism-min = 8
parallelism-max = 64
parallelism-factor = 3
그럼, core가 2개인 서버에서 다음과 같이 설정을 한 경우를 보자.
parallelism-max = 10
/* parallelism-factor의 설정은 생략 */
이때는, parallelism-factor 의 값 설정이 생략되어 있기 때문에, 이 값은 default인 3이 된다. 그래서, 최대 thread 개수는 10이 아니라, 2*3=6 이 되게 된다. 나의 경우에 이걸 모르는 바람에 한참을 고생한 기억이 있다.
3. 작업을 parallel하게 처리하기
3.1 Future.sequence 이용
Scala에서는 task를 비동기적으로 처리하기위해 Future로 만든 다음에, 모든 future가 동작이 끝날 때까지 기다리는 Future.sequence 함수를 호출하는 방식으로 처리할 수 있다.
다음과 같은 방식이다.
val future1 = Future { function1() } val future2 = Future { function2() } Future.sequence(Seq(future1, future2)).map { _ => true}
이때 thread pool을 분리해 두었다면, 다음과 같이 처리하는 것도 가능하다.
val future1 = Future { function1() } (pool1) val future2 = Future { function2() } (pool2) Future.sequence(Seq(future1, future2)).map { _ => true} (pool3)
그런데, 실제로 application 서버를 구현하다보면, parallel 하게 처리해야 하는 것과 serial하게 처리해야 하는 것들이 섞여 나올 수 있다. 예를 들면, 다음과 같다.
val future1 = Future { function1() } val future2 = Future { function2() } val future3 = future1 flatMap { r1 => function3(x) } Future.sequence(Seq(future2, future3)).map { _ => true}
위의 예에서는 future1의 결과를 얻은 후에, 이어서 function3을 호출하고 있다.
참고로, function1, function2, function3의 처리 시간을 각각 t1, t2, t3이라고 할 때, 이때 전체 처리시간은 다음과 같이 표현 할 수 있다.
max(sum(t1,t3), t2)
3.2 For 이용
위와 같이, serial하게 처리해야할 것과 parallel하게 처리할 것을 프로그래머가 구분해서 구현 할 수도 있지만, scala에서는 ‘for’ 문법을 이용해 이 부분을 컴파일러에게 넘길 수 있다.
참고로, Scala에서 어려우면서도 유용한 문법이 바로, ‘for’ 이다. 여기서의 future와 관련된 ‘for’의 용법은 Scala 공식 문서에 좀더 자세히 설명이 되어 있다. ‘for’는 여기서 사용하는 future의 처리 외에도, collection iteration과 Option을 처리할 때도 쓰인다.
‘for’를 이용해 앞서 코드를 변경하면 다음과 같다.
val future1 = Future { function1() } val future2 = Future { function2() } for { r1 <- future1 r2 <- future2 r3 <- Future { function3(r1) } } yield true
위의 코드는, r1과 r2는 parallel하게 처리되고, r3는 r2가 처리된 이후에 처리가 된다.
마치며
Play Framework 2와 Scala 조합에서 Thread 관련해서 꼭 알아야 하지만, 잘 다루어지지 않아서 고생했던 부분들 중심으로 정리해 보았다. 이 외에도 성능 측면에서 다루고 싶은 몇가지 소소한 것들이 있는데, 다음에 기회가 되면 이어 갈 수 있도록 하겠다.
Updated 2014-1-27
for에서 future 처리 관련해서 제가 잘못 알고 있던 것이 있어서 수정했어요.