博客 / 詳情

返回

非阻塞 SpringBoot 之 Kotlin 協程實現

非阻塞 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...

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.