非阻塞 SpringBoot 之 Kotlin 协程实现

Why?

Spring Boot 默认使用 Servlet Web服务器,Tomcat,每个请求分配一个线程。如果服务不是计算密集型,而是存在大量 I/O 等待,那么会浪费大量CPU时间,导致CPU利用率不高。如果强行加大线程池,会耗费大量内存,且增加线程切换的损耗。

于是,我们可以考虑使用 Reactive Web 服务器,Netty,基于事件循环,对于I/O密集型服务,性能极高。

背景介绍

我们有个服务,需要封装调用大量外部接口,然后做防腐转换和数据聚合。随着业务变得复杂,接口响应速度越来越慢,无法满足业务的时延需求。于是我们开始了第一轮优化,使用CompletableFuture + 线程池进行并发调用。一番操作之后,时延降下来了,但是资源利用率不高,单个节点能承受的并发量很小。如果遇到搞活动,并发需求上升时,需要申请大量资源进行扩容,非常浪费。

此时要问:为何做了异步化改造,并发能力还是上不来?

原因在于整个服务的模型还是阻塞式I/O,异步调用的时候,虽然用了一个新线程,但调用过程还是阻塞式的,这条线程就被阻塞了。当服务并发升高时,线程池里就会产生大量被阻塞的线程,而这些线程不是绿色线程(用户态线程),而是抢占式的,会分走宝贵的CPU时间,那么结果就是资源利用率低下,并发能力差了。

How?

为了解决I/O密集型服务并发能力低下的问题,可以改用响应式(Reactive)模型。实际上Spring很早就有相应的解决方案:Reactor + WebFlux,可实现非阻塞式IO。

虽然响应式编程十分强大,但也有其难点:不是过程式的,写业务代码很难懂,而且难以调试和测试。响应式编程不是本文的讨论重点,感兴趣的同学可以研究一下,从最早的 RxJava 到目前的 Project Reactor。

那有没有更简单的方案?不妨看看:协程。

Next:Coroutines

Java 也有协程方案,叫 Quasar(协程在里面叫 Fiber),但是18年之后就没有更新了,据说作者跑去写 Project Loom 了。Loom是下一代Java协程库,但目前还没有成熟,上生产是不可能的了。

虽然Java没有协程,但是JVM语言Kotlin有。下面就用 Kotlin Coroutines 结合 WebFlux 实现非阻塞式 SpringBoot 服务。

假设有个API,/slowInt,经过 1s 返回一个整数。我们要调两次,然后计算 sum。

响应时间 1s 极端一点,不过测试的时候更容易看出区别

我们不妨使用非阻塞式(WebClient)和阻塞式(RestTemplate)的web客户端,分别做性能测试。

import kotlinx.coroutines.*
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate
import org.springframework.web.client.getForObject
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.awaitBody

@Service
class ExampleService {

    @Autowired
    lateinit var webClient: WebClient

    @Autowired
    lateinit var restTemplate: RestTemplate

     /**
     * 使用协程
     */
    suspend fun sumTwo(): Int = coroutineScope {
        // 分别异步调用,换成 getInt2() 再测一遍
        val i1: Deferred<Int> = async { getInt() }
        val i2: Deferred<Int> = async { getInt() }
        // 聚合
        i1.await() + i2.await()
    }

    /**
     * None-Blocking web client
     * very fast
     */
    suspend fun getInt(): Int {
        return webClient.get()
            .uri("/slowInt")
            .accept(APPLICATION_JSON)
            .retrieve().awaitBody()
    }

    /**
     * Blocking web client
     * very slow
     */
    fun getInt2(): Int {
        val result = restTemplate.getForObject<Int>("/slowInt").toInt()
        println(result)
        return result
    }
}
@RestController
class ExampleController {

    @Autowired
    lateinit var exampleService: ExampleService

    @GetMapping("/sum")
    suspend fun sum(): String? = "Sum: ${exampleService.sumTwo()}"
}

性能测试

然后用 JMeter 压一压。

对于阻塞式IO,使用 10 并发,循环10次。结果如下:
image-20221215113250527.png

非阻塞式,使用 100 并发,循环10次。结果如下:
image-20221215113033970.png

采用非阻塞式IO,在大并发的情况下,平均时延基本就 1s,与接口耗时吻合。

可见,响应时间大幅下降,吞吐量大幅上升,从此不再吞吞吐吐。

参考文献

https://www.baeldung.com/kotl...


骚铭科技
1 声望2 粉丝

条路自己行,扑街唔好喊