非阻塞 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,与接口耗时吻合。
可见,响应工夫大幅降落,吞吐量大幅回升,从此不再结结巴巴。
参考文献
https://www.baeldung.com/kotl…