How to properly warmup connection pool

See original GitHub issue

Hey team. I am creating an application that needs to call another microservice, and add the minimum possible amount of latency doing so. So for that I wanted to give netty and the Spring webclient a try.

On the first try i saw that calling the first time the netty version was much slower than the simple RestTemplate based ones, after some debugging and trace logging I realized that the reason this initial call was so slow is because the connection pool is only created on the first call.

I tried adding a warmup call, which helped but I find it a not so elegant solution: Without warmup

2020-03-09 22:28:10.696  INFO 4483 --- [nio-8080-exec-1] com.example.demo.ControllerConfigs       : Total took 317
2020-03-09 22:28:14.647  INFO 4483 --- [nio-8080-exec-2] com.example.demo.ControllerConfigs       : Total took 16
2020-03-09 22:28:15.746  INFO 4483 --- [nio-8080-exec-4] com.example.demo.ControllerConfigs       : Total took 14
2020-03-09 22:28:17.923  INFO 4483 --- [nio-8080-exec-6] com.example.demo.ControllerConfigs       : Total took 14

With warmup:

2020-03-09 22:29:05.307  INFO 4489 --- [nio-8080-exec-1] com.example.demo.ControllerConfigs       : Total took 32
2020-03-09 22:29:09.161  INFO 4489 --- [nio-8080-exec-2] com.example.demo.ControllerConfigs       : Total took 14
2020-03-09 22:29:09.558  INFO 4489 --- [nio-8080-exec-3] com.example.demo.ControllerConfigs       : Total took 13
2020-03-09 22:29:09.817  INFO 4489 --- [nio-8080-exec-4] com.example.demo.ControllerConfigs       : Total took 14
2020-03-09 22:29:09.992  INFO 4489 --- [nio-8080-exec-6] com.example.demo.ControllerConfigs       : Total took 13

Is there anything else I can do to optimize the first call performance? In my production I am running the app with very low CPU settings, so the difference is between 200ms for a normal call and 6-8 seconds for the initial one.

package com.example.demo

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.client.reactive.ReactorClientHttpConnector
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.client.RestTemplate
import org.springframework.web.reactive.function.BodyInserters
import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Flux
import reactor.netty.http.client.HttpClient
import reactor.netty.resources.ConnectionProvider

@RestController
@RequestMapping("/sync")
class SyncController(@Value("\${target}") private val target: String, private val restTemplate: RestTemplate) {

    @PostMapping
    fun call(@RequestBody body: String): List<String> {
        val (time, result) = measureTimeMillis {
            body.split("+").fold(arrayListOf<List<String>>(arrayListOf())) { acc, s ->
                acc.add(acc.last().plus(s)); acc
            }.map { it.joinToString(" ") }
                    .map {
                        val (time, result) = measureTimeMillis {
                            restTemplate.postForEntity(target, it, String::class.java)
                                    .body!!
                        }
                        LOG.info("Call for $it took $time")
                        result
                    }
        }
        LOG.info("Total for took $time")

        return result
    }

    companion object {
        val LOG: Logger = LoggerFactory.getLogger(SyncController::class.java)
    }
}

@RestController
@RequestMapping("/coroutine")
class CoroutineController(@Value("\${target}") private val target: String, private val webClient: WebClient) {

    init {
        runBlocking {
            webClient.post()
                    .uri(target)
                    .body(BodyInserters.fromValue("warmup"))
                    .exchange()
                    .flatMap { it.toEntity(String::class.java) }
                    .map { it.body!! }
                    .awaitSingle()
        }
    }

    @PostMapping
    fun call(@RequestBody body: String): List<String> {
        val (time, result) = measureTimeMillis {
            runBlocking {
                withContext(Dispatchers.IO) {
                    body.split("+").fold(arrayListOf<List<String>>(arrayListOf())) { acc, s ->
                        acc.add(acc.last().plus(s)); acc
                    }.map { it.joinToString(" ") }
                            .map {
                                    webClient.post()
                                            .uri(target)
                                            .body(BodyInserters.fromValue(it))
                                            .exchange()
                                            .flatMap { it.toEntity(String::class.java) }
                                            .map { it.body!! }
                                            .awaitSingle()
                            }
                }//.map { it.awaitSingle() }
            }
        }
        LOG.info("Total took $time")

        return result
    }

    companion object {
        val LOG: Logger = LoggerFactory.getLogger(ControllerConfigs::class.java)
    }
}


@Configuration
class ControllerConfigs {
    @Bean
    fun restTemplate(): RestTemplate = RestTemplate()

    @Bean
    fun webClient(configuredHttpClient: HttpClient): WebClient =
            WebClient
                    .builder()
                    .clientConnector(ReactorClientHttpConnector(configuredHttpClient))
                    .build()

    @Suppress("ComplexMethod")
    @Bean
    fun configuredHttpClient(
    ): HttpClient = HttpClient
            .create()

    companion object {
        val LOG: Logger = LoggerFactory.getLogger(CoroutineController::class.java)
    }
}


inline fun <T> measureTimeMillis(block: () -> T): Pair<Long, T> {
    val start = System.currentTimeMillis()
    val t = block()
    return Pair(System.currentTimeMillis() - start, t)
}

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Reactions:5
  • Comments:13 (6 by maintainers)

github_iconTop GitHub Comments

github_iconTop Results From Across the Web

Configuring warmup requests to improve performance
If warmup requests are enabled for your application, App Engine attempts to detect ... Handle your warmup logic here, e.g. set up a...
Read more >
Correct way to implement HTTP Connection Pooling
Beware of how HTTP Client pools work, it may be improving performance during a short period of time. Check the analysis below:.
Read more >
Reactor Netty Reference Guide
1. Connection Pool Timeout. By default, HttpClient uses a connection pool. When a request is completed successfully and if the connection is not...
Read more >
Application Warmup URLs - Product, UX, & Engineering Careers
The warmup URL is called from the deploy script, and the response is checked for a "success string". The success string tells us...
Read more >
HttpClient Performance Optimization Guide
... connection manager prior to disposing the HttpClient instance. This will ensure proper closure of all HTTP connections in the connection pool.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found