非阻塞 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.Autowiredimport org.springframework.http.MediaType.APPLICATION_JSONimport org.springframework.stereotype.Serviceimport org.springframework.web.client.RestTemplateimport org.springframework.web.client.getForObjectimport org.springframework.web.reactive.function.client.WebClientimport org.springframework.web.reactive.function.client.awaitBody@Serviceclass 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 }}
@RestControllerclass ExampleController { @Autowired lateinit var exampleService: ExampleService @GetMapping("/sum") suspend fun sum(): String? = "Sum: ${exampleService.sumTwo()}"}
性能测试
而后用 JMeter 压一压。
对于阻塞式IO,应用 10 并发,循环10次。后果如下:
非阻塞式,应用 100 并发,循环10次。后果如下:
采纳非阻塞式IO,在大并发的状况下,均匀时延根本就 1s,与接口耗时吻合。
可见,响应工夫大幅降落,吞吐量大幅回升,从此不再结结巴巴。
参考文献
https://www.baeldung.com/kotl...