ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • API Latency를 줄이는 방법 (Part. 1)
    극락코딩 2023. 8. 29. 23:43
    API의 응답속도 및 지연에 민감한 서비스인 경우, 어떤 방식으로 latency를 줄일 수 있을까?

     

    이전 게시글에서는 기본적인 Project Setup을 진행했다. 이번에는 아래의 상황이 적용된 API를 만들어 본다.

    1. 해당 API는 TPS 500이 넘는다고 가정한다. (그 만큼 Call이 많은 서비스)
    2. 해당 서비스는 다수의 쿼리를 조회하고, Redis 및 Client 호출이 많다. (Wrapping API라고 생각하자)
    3. 해당 서비스는 에러가 발생하더라도, 이를 방어할 수 있는 로직이 필요하다. (에러 발생보다는, 로그를 남기고 넘어가는 방향으로 진행)

     

    위의 조건을 맞추기 위해, 지금 만드는 API에서는 다음과 같은 IO 발생 로직들을 추가할 것이다.

    1. Database 조회 4번

    2. Redis 조회 4번

    3. Cpu IO 로직 4번

    4. Network IO 로직 4번

     

    다음으로는 코드를 작성해보자..

     

    TestController.kt

    @RestController
    class TestController(
        private val testV1Service: TestV1Service
    ) {
        @GetMapping("/api/ral/v1/test")
        fun getTestV1(
            request: TestRequest
        ) = testV1Service.getTestV1(request).wrap()
    }

    ral은 reduct-api-latency를 줄인 것..

    여기서 TestRequest는 RequestParam으로 받을 필드들의 집합이다.

     

    TestRequest.kt

    data class TestRequest(
        val test1Id: Set<Long>,
        val test2Id: Set<Long>,
        val test3Id: Set<Long>,
        val test4Id: Set<Long>,
    )

     

     

    다음으로는, TestV1Service를 작성해보자. (실제 비즈니스 로직이 동작..)

     

     

    TestV1Service.kt

    @Service
    class TestV1Service(
        private val test1Repository: Test1Repository,
        private val test2Repository: Test2Repository,
        private val test3Repository: Test3Repository,
        private val test4Repository: Test4Repository,
        private val cacheService: CacheService,
        private val mathEngine: MathEngine,
        private val googleClient: GoogleClient
    ) {
        fun getTestV1(request: TestRequest): TestResponse {
            /** Database */
            val test1Model = test1Repository.findAllById(request.test1Id)
                .map { test1 -> Test1Model.from(test1) }
            val test2Model = test2Repository.findAllById(request.test2Id)
                .map { test2 -> Test2Model.from(test2) }
            val test3Model = test3Repository.findAllById(request.test3Id)
                .map { test3 -> Test3Model.from(test3) }
            val test4Model = test4Repository.findAllById(request.test4Id)
                .map { test4 -> Test4Model.from(test4) }
    
            /** Redis Cache */
            val test1Cache = cacheService.get("test1:key:${request.test1Id}")
            val test2Cache = cacheService.get("test2:key:${request.test2Id}")
            val test3Cache = cacheService.get("test3:key:${request.test3Id}")
            val test4Cache = cacheService.get("test4:key:${request.test4Id}")
    
            /** Cpu Logic */
            val result1 = mathEngine.execute()
            val result2 = mathEngine.execute()
            val result3 = mathEngine.execute()
            val result4 = mathEngine.execute()
    
            /** WebClient Api Call */
            val realTrend1 = runBlocking { googleClient.getRealTimeTrends() }
            val realTrend2 = runBlocking { googleClient.getRealTimeTrends() }
            val realTrend3 = runBlocking { googleClient.getRealTimeTrends() }
            val realTrend4 = runBlocking { googleClient.getRealTimeTrends() }
    
            return TestResponse.of(
                cacheModel = TestCacheModel(
                    test1 = test1Cache,
                    test2 = test2Cache,
                    test3 = test3Cache,
                    test4 = test4Cache
                ),
                test1s = test1Model,
                test2s = test2Model,
                test3s = test3Model,
                test4s = test4Model,
                result = listOf(result1, result2, result3, result4),
                trendModels = listOf(realTrend1, realTrend2, realTrend3, realTrend4)
            )
        }
    }

    하나씩 살펴보겠지만, 위의 로직 조건에 맞추어 진행한 것이다. 먼저 Response Model은 아래와 같다.

     

    TestResponse.kt

    data class TestResponse(
        val cacheModel: TestCacheModel?,
        val test1: List<Test1Model>,
        val test2: List<Test2Model>,
        val test3: List<Test3Model>,
        val test4: List<Test4Model>,
        val engineResult: List<String>,
        val trendModels: List<GoogleRealTimeSearchTrendModel>
    )

    크게 중요한 부분은 아니다. 오직 테스트를 위한 model일뿐..

     

    Database 로직은 ids값으로 다수의 값을 조회하는 로직들이다. in query가 동작한다.

    Redis 로직을 살펴보면...

     

    CacheService.kt

    @Service
    class CacheService(
        private val stringRedisTemplate: StringRedisTemplate
    ) {
        fun get(key: String): String? {
            return stringRedisTemplate.opsForValue().get(key)
        }
    }

    단순하게, StringRedisTemplate을 통해 특정 key를 조회하고, 반환하는 로직이다.

     

    다음으로 CpuLogic을 살펴본다.

     

    MathEngine.kt

    @Component
    class MathEngine {
        /** this functions takes approximately 50ms. */
        fun execute(): String {
            (0..25_000_000).forEach { val a = it }
    
            return "Math Engine Finish"
        }
    }

    25,000,000번 for문을 실행하는 로직인데, 현재 PC의 상태에 따라 수행시간이 다르지만..

    본인의 PC에서는 대략 50~55ms 정도 시간이 걸린다.

     

    다음으로는 WebClient를 통해 Network IO를 발생하는 부분이다.

     

    GoogleClient.kt and SuspendableGoogleClient.kt

    interface GoogleClient {
        /** 실시간 검색 트랜드 */
        suspend fun getRealTimeTrends(): GoogleRealTimeSearchTrendModel
    }
    
    class SuspendableGoogleClient(
        private val webClient: WebClient
    ) : GoogleClient {
        private val logger = mu.KotlinLogging.logger {}
    
        override suspend fun getRealTimeTrends(): GoogleRealTimeSearchTrendModel {
            return webClient.get()
                .uri("/trends/api/realtimetrends") { builder ->
                    builder
                        .queryParam("hl", "ko")
                        .queryParam("tz", -540)
                        .queryParam("cat", "all")
                        .queryParam("fi", 0)
                        .queryParam("fs", 0)
                        .queryParam("geo", "US")
                        .queryParam("ri", 300)
                        .queryParam("rs", 20)
                        .queryParam("sort", 0)
                        .build()
                }
                .retrieve()
                .awaitBody<String>().trimIndent().substringAfter("\n").run {
                    mapperGoogleResponseModel(this)
                }
        }
    
        private inline fun <reified T> mapperGoogleResponseModel(response: String): T {
            return runCatching {
                mapper.readValue<T>(response)
            }.getOrElse { e ->
                logger.error { "fail to mapping / ${e.message}" }
                throw RuntimeException()
            }
        }
    }

    해당 로직은 Google의 실시간 트랜드 조회 API를 호출하는 로직이다.

    WebClient로 호출하도록 구현되어 있다.

     

    이쪽 로직을 자세히 살펴보고 싶으신 분이 계신다면, 해당 레포를 참고해주실 바란다.

     

    여기까지 따라왔다면, 대부분의 IO 발생 로직에 대해서 파악이 완료된 것이다.

    그럼 latency test를 진행해보자.

     

    Jmeter를 통해 테스트를 진행할 것이다.

     

    Jmeter는 Apache에서 만든 API Test Tool이다. 나름 간단하게 쓸 수 있어서 자주 사용한다.

     

     

    jmeter 설치

    brew install jmeter

     

    jmeter 실행

    open /opt/homebrew/bin/Jmeter

     

    설치하고 나서, 실행 명령어를 때리면, 다음과 같은 화면을 볼 수 있다.

     

    이왕 하는거, Jmeter Test 방법도 작성해본다..

     

    여기서 Thread Group 설정을 진행하는데, 몇명의 User를 만들것인지 선택하는 것이다. 1Thread = 1 Person이라고 생각하자.

     

    10명의 인원을 선택하고, 각각의 user들이 10번씩 API를 호출하도록 설정한다.

     

    Thread Group을 설정했다면, API 호출 및 통계 정보를 파악할 수 있는 Report 정보를 기록한다.

     

    어떤 요청을 보낼 것인지 선택할 때, 우리가 만든 서비스는 HTTP Request로 호출되기 때문에 요거를 선택!

     

     

    파라미터를 설정하는건 귀찮기 때문에, queryString 쪽은 전부 다 때려박는다.

     

     

    다음으로는 Test 이후 어떤 Report를 만들 것인지 선택해야 한다.

    이번에는 다음과 같은 항목을 추가할 것이다.

    (사람의 욕심이 끝이 없듯.. 계속 추가했다..)

     

    자 그럼 테스트를 시작한다. 

     

    테스트를 돌리니, 열심히 로그가 찍히고 있다..

     

    Test를 완료하고 나서, 추가했던 Report들을 살펴보자

     

    latency가 평균적으로 1Seconds를 전부 초과하고 있다.

     

    처음 호출이 늦은 이유는, Spring Bean Init 때문... spring 프로젝트가 실행되고 나서, 실제 호출이 발생할 때 부가적인 Bean들을 주입받는데 시간이 걸림

     

     

    여기까지 기본 설정 및 Jmeter를 통한 Test 작업을 완료하였다.

    다음 포스팅 부터는 정말정말 Api Latency를 줄이는 개선 작업에 대해 소개하겠다.

     

     

     

     

    Reference

    GITHUB-Reduce-Api-Latency

    Search-Trend-Api

     

극락코딩