【作者简介】
何晓杰,指令集技术专家,知名开源作者,曾就职于 IBM、盛大创新院、安居客、沪江,创过业,也带过创业团队。
长期在技术圈和开源界摸爬滚打,目前主要关注和工作重点在工业物联网、Linux内核、容器化、云计算、编译工具链。
今早一醒来,就看见朋友圈有人在转发 Ktor 2.0 的信息,点进去一看,一些特性吸引了我,简单来说就是 Ktor 2.0 可以支持 Kotlin/Native 了。
为什么这个特性很重要呢,归根到底来说,还是计算资源的匮乏,我们经历过很多事情,很多时候用户并不会愿意为一堆吃掉了好几个G的微服务来买单,因为这对他们也造成了额外开销。这一点在使用 Java 或是任何工作于 JVM 之上的技术栈来说,都没有太好的解决方案,内存问题始终是无解的。
之前简单的做过一个对比,就拿我负责的 License 服务模块来说,其内存占用情况如下所示(我是有多蛋疼才会拿各种技术栈去反复实现一样东西):
语言(及框架) | 刚启动时 | 关键接口 | 关键接口 |
C (无) | 980K | 2.4M | 2.4M |
Go (gin 1.7.7) | 6.4M | 9.7M | 9.8M |
Java (Springboot 2.5.2) | 212M | 380M | 465M |
Kotlin (JVM,Ktor 1.6) | 98M | 136M | 159M |
当然了,这里的 Java 和 Kotlin 是没有本质区别的,只是语言不同,全部都工作于 JVM 之上,只是使用框架的区别,只是我们也可以看到,Ktor 对内存的占用是优于 Springboot 的,Springboot 对内存的占用实在太过可怕。
而诸如 C 或者 Go 这类的原生技术栈,由于没有虚拟机的参与,它们即是以 CPU 在运行程序,性能会提高很多,并且对内存的要求也是出乎意料的小。因此在微服务的开发领域,原生的解决方案将是越来越重要的。
另外,为什么选择 Kotlin,这就是一些个人的观点了,至少在我的观念里,目前还没有在语言能力上强过 Kotlin 的,并且,就算哪里让你不爽,你还可以自己造 DSL 来让自己爽。好了,关于语言的问题不多说了,请各位自行体会,今天主要谈 Ktor。
那就直接点,用代码来说事,官方的文章说 Ktor 2.0 支持 Kotlin/Native,不过我并没有在 Ktor 的项目向导里找到这一能力,只能去翻 Ktor 的文档了,一眼望去也是没有,照理说这么重要的特性应该大篇幅描述,并且一步一步指导实施,只可惜,都没有。好不容易在一个层次非常深的页面里找到了相关的描述,结果发现,根本就没有什么向导,只有一个样例项目,配置都不全的那种,要自己补上参数设置,这个对于新手是直接劝退的,非常的不友好。我估计 Ktor 团队的理念就是这样的吧,一是让你多翻翻文档,这样你就会少提 issue,另外就是提高门槛让一部分人先滚蛋(手动狗头)。
好在作为一个写了几年 Ktor 的人,这点经验还是有的了,直接把项目配置全部补全:
|- ktor-native
| |- build.gradle.kts
| |- gradle.properties
| |- settings.gradle.kts
| |- src
| | |- nativeMain
| | | |- kotlin
| | | | |- Main.kt
其中最关键的文件即是 build.gradle.kts,其内容如下:
val ktor_version: String by project
val kotlin_version: String by project
val logback_version: String by project
plugins {
application
kotlin("multiplatform") version "1.6.21"
kotlin("plugin.serialization") version "1.6.21"
}
repositories {
mavenCentral()
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap") }
}
kotlin {
val hostOS = System.getProperty("os.name")
val hostArch = System.getProperty("os.arch")
val nativeTarget = when(hostOS) {
"Mac OS X" -> when(hostArch){
"aarch64"-> macosArm64("native")
else -> macosX64("native")
}
"Linux" -> linuxX64("native")
else -> throw GradleException("Host OS is not supported in Kotlin/Native")
}
nativeTarget.apply {
binaries {
executable {
entryPoint = "main"
}
}
}
sourceSets {
val nativeMain by getting {
dependencies {
implementation("io.ktor:ktor-server-core:$ktor_version")
implementation("io.ktor:ktor-server-cio:$ktor_version")
implementation("io.ktor:ktor-server-content-negotiation:$ktor_version")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")
}
}
val nativeTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
}
}
然后我们需要写的,就是那个 Main.kt 了,它是 Ktor 服务端的主程序,代码如下:
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.cio.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
@Serializable
data class MyReq(val id: Int, val name: String)
@Serializable
data class MyResp(val code: Int, val message: String, val data: String)
fun main() {
embeddedServer(CIO, port = 9099) {
install(ContentNegotiation) {
json()
}
routing {
get("/") {
call.respondText("Hello, world!")
}
post<MyReq>("/post") { p ->
call.respond(MyResp(0, "", "hello ${p.name}"))
}
}
}.start(wait = true)
}
其实到这里,我们的程序已经可以正常运行了,但是按官方的说法,建议使用新的 Kotlin/Native 内存管理机制,那么就多写一点吧,把相关的配置写到 gradle.properties 里:
kotlin.native.binary.memoryModel=experimental
大功告成,编译并运行一下吧:
$ gradle clean build
$ ./build/bin/native/debugExecutable/ktor-native.kexe
现在即可以看到程序被正确运行了,尝试请求接口也都可以得到正确的返回。
上面提到过,这次的 Ktor 2.0,官方建议用新的内存管理机制,那么新的机制和老的,有多大差别呢,我们同样通过一组数据来展示:
方案 | 启动内存 | 接口压测 | 接口压测 |
Ktor 2.0 (Native,旧方案) | 9.4M | 14.2M | 18.1M |
Ktor 2.0 (Native,新方案) | 6.1M | 8.8M | 8.9M |
由于只是简单的案例项目,没什么复杂逻辑,这里的数据可以比较好的展示框架本身的一些情况。可以比较明显的看出来,当采用旧方案时,启动时内存占用较大,同样的压测后的内存占用也较大,而采用新内存管理方案时,对内存的使用可以说是相当优秀的。但是采用新方案时,会有一个问题,当程序终止并且立即再打开时,会出现 “端口被占用” 的异常,即是说程序被杀死了,但是端口未被释放,当采用旧方案时,这一现象不存在,有理由怀疑为,当采用新方案时,程序默认是执行优雅终止的,即是要做一系列的内存销毁动作,才真正杀死进程,在这个动作未做完时,端口不会被释放(以上观点只是猜测,未经验证)。
好了,看到这里,是不是该说一句 “真香”?但是要将 Ktor 2.0 实际用到项目中,就一点也不香了。
还是按上面的 Kotlin/Native 项目,你知道要如何访问 mysql 或 redis 吗?其实我也不知道,因为我没有找到过任何关于 Kotlin/Native 操作 mysql 或 redis 的内容,我还在 io.ktor 仓库中进行过翻找,也没有找到与数据库相关的东西,目前只有一个为 KMM 而开发的 SQL 框架,说白了就是给安卓开发用的,这个和我们做 Server Side 的可是半毛钱关系都没有。因此现在是可以下断言说 Kotlin/Native 在 Ktor 场景下是不具备操作数据库的能力的。另外,你可能也很难想象,作为一个能承载微服务的技术体系,它不支持 k8s sdk,不支持服务注册发现,甚至还不支持没有“反向代理”的https。
到目前为止,整个 Kotlin/Native 都没有所谓的生态,官方认同 cintrop 和 posix 库,但是又不对它作出任何扩展,光有这几个库基本上啥事都不能干,而要开发者自行给 Kotlin/Native 开发内容,又非常的困难,因为 Kotlin/Native 采用 expect/actual 机制来进行跨平台,它并没有提供可以抹去平台差异的 API 库,因此开发者必须自行处理每个平台的差异,这种研发的难度,对时间精力的消耗都并非一般开发者能承受得起,因此 Kotlin/Native 落到现在这样的境地也是无奈。如果要在 Kotlin/Native 场景下完好的使用 Ktor,那么 Kotlin/Native 本身必须作出巨大的改变。
另外再提一下 Ktor 2.0 的其他新增特性,其实这些东西对于我来说都很熟悉,因为我一开始就提出过要以插件化的方式进行编码,以实现更方便的扩展,并且在实际工作中,也早就推出了指令集特有的 Ktor 扩展包,以实现如 bodyAsText 这类的扩展函数。这次官方也如此做了,在我看来即是证明了我们之前的设计方向正确,以及扩展包对于 Ktor 的补充是极其必要的。所以整个 Ktor 2.0 给我的感觉就是,在讲一个 Kotlin/Native 的故事,但是又没办法把故事讲圆满,着实可惜。
总而言之,目前的 Ktor 2.0,若是用于 JVM 平台,那么依然可用,并且也非常的优秀,针对本次升级或许你看不到它有任何的改变,只是看作一个常规的版本升级。而若是用于 Native 平台,那么它还远远不到可以用的程度,这并非 Ktor 自身能解决的问题,而是必须要等 Kotlin/Native 官方有进一步动作,要么自行打造生态,要么简化开发过程,至少不要让开发者去查各个平台的底层 API 然后进行兼容,这样才可能会有的起色。
最后,是不是还有人想提一下 GraalVM?确实这次 Ktor 2.0 也提到了对 GraalVM 的支持,但是我个人还是奉劝一句,别急,再等个两年,毕竟 SpringNative 的遭遇就摆在那呢。