非阻塞 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次。结果如下:
非阻塞式,使用 100 并发,循环10次。结果如下:
采用非阻塞式IO,在大并发的情况下,平均时延基本就 1s,与接口耗时吻合。
可见,响应时间大幅下降,吞吐量大幅上升,从此不再吞吞吐吐。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。