Dispatcher 설정 최적화로 서버 성능 개선 및 동시 처리 속도 향상
배경
큐리어슬리 회사에 LX 스쿼드에서 근무할 때, 계약사의 유저 수가 급증하면서 전체 수강 현황을 다운로드하는 데 시간이 지나치게 오래 걸리는 문제가 발생했다. 특히, 월말에는 각 계약사의 관리자가 수천 명에 달하는 수강생들의 수강 현황을 Excel 또는 CSV 파일로 다운로드해야 했고, 이 과정에 20~30분이 소요되는 경우가 발생했었다.
문제파악
해당 과정이 어떤 절차로 처리가되는지 정리를 하고 각 과정마다 걸리는 시간들을 책정했다.
유저목록불러오기 -> 모든 유저가 학습한 클래스의 정보 가져오기 -> 각 유저마다의 학습별 기록 가져오기
이 중에서 각 유저마다의 학습별 기록 가져오기 에서의 시간이 가장 오래 걸렸다.
동시처리를 위해 썼던 코루틴이 제대로 동작하지 않는 것으로 보였다. 어떤 부분이 문제일지 찾아보다가 Dispatcher 설정이 문제임을 깨닫게 되었다.
https://www.baeldung.com/kotlin/io-and-default-dispatcher
[Dispatcher.Default vs Dispatcher.IO]
기존 코드에서는 Dispatcher 설정에 대해 특별한 값을 지정하지 않았다. 만약 Dispatcher를 따로 설정하지 않으면, 하드웨어에서 제공하는 CPU 코어 수에 맞춰 자동으로 스레드 풀에서 스레드 수를 할당한다. 이때 사용되는 스레드는 CPU 바운드(CPU-bound) 스레드로, 각 스레드는 작업이 완료될 때까지 CPU 자원을 계속해서 소비한다. 예를 들어, 2코어 CPU를 사용하는 인스턴스에서는 2개의 스레드가 동시에 실행되며, 각 스레드는 작업이 끝날 때까지 CPU 자원을 계속 사용하게 된다.
하지만, Dispatcher.IO를 설정하여 I/O 바운드 작업을 처리하게 되면, 이와 다른 방식으로 동작한다. Dispatcher.IO 설정을 사용하면, I/O 바운드(IO-bound) 스레드를 할당하여, I/O 작업이 끝날 때까지 CPU 자원을 사용하지 않게 된다. I/O 작업이 완료될 때까지 스레드는 대기 상태에 빠지지만, 그동안 CPU 자원은 다른 작업에 할당될 수 있기 때문에, 다른 스레드들이 동시에 작업을 수행할 수 있게 된다. 이로 인해 I/O 작업이 많은 경우, Dispatcher.IO는 CPU 바운드 스레드보다 더 효율적으로 CPU 자원을 활용할 수 있다.
Dispatcher.IO는 최대 64개의 스레드를 동시에 사용할 수 있도록 설계되어 있으며, 각 스레드는 I/O 대기 중에 CPU를 사용하지 않기 때문에 CPU 자원을 유휴 상태로 두지 않고 다른 작업을 동시에 처리할 수 있는 장점이 있다.
해결
기존코드에 Dispatcher.IO 설정을 추가함으로써 동시 처리 속도를 향상시켰다.
[실험코드]
아래의 코드는 Dispatcher 설정에 따른 결과를 테스트코드로 비교한 것이다.
주의점 : delay는 코루틴에서 제공하는 함수로 Non-Blocking 방식으로 동작하여 스레드를 차단하지 않는다. 따라서 delay를 사용하면 코루틴이 다른 작업을 계속 처리할 수 있어, 실제로 스레드를 차단하는 효과를 기대할 수 없다. 반면, Thread.sleep은 Blocking 방식으로 동작하여 실제로 스레드를 차단한다. 그래서 Thread.sleep을 사용해야만 원하는 결과를 얻을 수 있다.
class Test {
private val iterations = 100
private val sleepDuration = 2000L
@Test
fun `Dispatcher설정에 따른 동시처리 결과 비교`() = runBlocking {
val ioTime = measureTimeMillis {
withContext(Dispatchers.IO) {
repeat(iterations) {
launch {
performBlockingTask()
}
}
}
}
val defaultTime = measureTimeMillis {
withContext(Dispatchers.Default) {
repeat(iterations) {
launch {
performBlockingTask()
}
}
}
}
println("Dispatchers.IO time: $ioTime ms")
println("Dispatchers.Default time: $defaultTime ms")
assertTrue(ioTime < defaultTime)
}
private fun performBlockingTask() {
Thread.sleep(sleepDuration)
}
}
결과
(2core cpu를 사용한 결과)
Dispatchers.IO time: 4025 ms
Dispatchers.Default time: 26067 ms