替代Spring Boot几种微服务框架比较
目前,有很多使用Java和Kotlin创建微服务的框架。本文中考虑了以下内容:
Name | Version | Year of a first release | Developer | GitHub |
Helidon SE | 1.4.1 | 2019 | Oracle | link |
Ktor | 1.3.0 | 2018 | JetBrains | link |
Micronaut | 1.2.9 | 2018 | Object Computing | link |
Quarkus | 1.2.0 | 2019 | Red Hat | link |
Spring Boot | 2.2.4 | 2014 | Pivotal | link |
基于它们创建了四种服务,可以使用Consul实现的服务发现模式通过HTTP API相互进行交互。因此,形成了异构的(在框架级别)微服务架构(以下称为MSA):
从头开始创建应用程序
要在其中一个框架上生成新项目,可以使用Web Starter或指南中考虑的其他选项(例如,构建工具或IDE):
Name | Web starter | Guide | Supported languages |
Helidon | link (MP) | link (SE) link (MP) | Java, Kotlin |
Ktor | link | link | Kotlin |
Micronaut | - (CLI) | link | Groovy, Java, Kotlin |
Quarkus | link | link | Java, Kotlin, Scala |
Spring Boot | link | link | Groovy, Java, Kotlin |
Helidon service
该框架是在Oracle中创建的,供内部使用,随后成为开源。基于此框架的开发模型有两种:标准版(SE)和MicroProfile(MP)。在这两种情况下,该服务都是常规的Java SE程序。在此页面上了解有关差异的更多信息。
简而言之,Helidon MP是Eclipse MicroProfile的实现之一,它使得可以使用许多API,这两个API以前都是Java EE开发人员(例如JAX-RS,CDI)已知的,也可以使用较新的API(Health Check,Metrics,Fault)公差等)。在Helidon SE模型中,开发人员遵循“没有魔力”的原则,尤其是用更少的数量或完全没有创建应用程序所需的注释来表示。
选择Helidon SE来开发微服务。除其他外,它缺乏依赖注入的方法,因此为此使用了Koin。以下是包含main方法的类。为了实现依赖注入,该类从KoinComponent继承。首先,Koin启动,然后初始化所需的依赖关系并startServer()调用方法,在该方法中创建WebServer类型的对象,并将应用程序配置和路由设置传递给该对象。启动应用程序后,在领事馆注册:
object HelidonServiceApplication : KoinComponent { @JvmStatic fun main(args: Array<String>) { val startTime = System.currentTimeMillis() startKoin { modules(koinModule) } val applicationInfoService: ApplicationInfoService by inject() val consulClient: Consul by inject() val applicationInfoProperties: ApplicationInfoProperties by inject() val serviceName = applicationInfoProperties.name startServer(applicationInfoService, consulClient, serviceName, startTime) } } fun startServer( applicationInfoService: ApplicationInfoService, consulClient: Consul, serviceName: String, startTime: Long ): WebServer { val serverConfig = ServerConfiguration.create(Config.create().get("webserver")) val server: WebServer = WebServer .builder(createRouting(applicationInfoService)) .config(serverConfig) .build() server.start().thenAccept { ws -> val durationInMillis = System.currentTimeMillis() - startTime log.info("Startup completed in $durationInMillis ms. Service running at: http://localhost:" + ws.port()) // register in Consul consulClient.agentClient().register(createConsulRegistration(serviceName, ws.port())) } return server }
路由配置如下:
private fun createRouting(applicationInfoService: ApplicationInfoService) = Routing.builder() .register(JacksonSupport.create()) .get("/application-info", Handler { req, res -> val requestTo: String? = req.queryParams() .first("request-to") .orElse(null) res .status(Http.ResponseStatus.create(200)) .send(applicationInfoService.get(requestTo)) }) .get("/application-info/logo", Handler { req, res -> res.headers().contentType(MediaType.create("image", "png")) res .status(Http.ResponseStatus.create(200)) .send(applicationInfoService.getLogo()) }) .error(Exception::class.java) { req, res, ex -> log.error("Exception:", ex) res.status(Http.Status.INTERNAL_SERVER_ERROR_500).send() } .build()
webserver { port: 8081 } application-info { name: "helidon-service" framework { name: "Helidon SE" release-year: 2019 } }
Ktor服务
该框架是为Kotlin编写的,并且是为Kotlin设计的。像在Helidon SE中一样,Ktor并没有开箱即用,因此在启动服务器依赖项之前,应该使用Koin注入:
val koinModule = module { single { ApplicationInfoService(get(), get()) } single { ApplicationInfoProperties() } single { ServiceClient(get()) } single { Consul.builder().withUrl("http://localhost:8500").build() } } fun main(args: Array<String>) { startKoin { modules(koinModule) } val server = embeddedServer(Netty, commandLineEnvironment(args)) server.start(wait = true) }
应用程序所需的模块在配置文件中指定(只能使用HOCON格式;有关在Ktor config docs上配置Ktor服务器的更多详细信息),其内容如下所示:
ktor { deployment { host = localhost port = 8082 environment = prod // for dev purpose autoreload = true watch = [io.heterogeneousmicroservices.ktorservice] } application { modules = [io.heterogeneousmicroservices.ktorservice.module.KtorServiceApplicationModuleKt.module] } } application-info { name: "ktor-service" framework { name: "Ktor" release-year: 2018 } }
在Ktor和Koin中,术语“模块”以不同的含义使用。在Koin中,模块是Spring框架中应用程序上下文的类似物。Ktor模块是用户定义的函数,它接受类型的对象, Application并且可以配置管道,安装功能,注册路由,处理请求等:
fun Application.module() { val applicationInfoService: ApplicationInfoService by inject() if (!isTest()) { val consulClient: Consul by inject() registerInConsul(applicationInfoService.get(null).name, consulClient) } install(DefaultHeaders) install(Compression) install(CallLogging) install(ContentNegotiation) { jackson {} } routing { route("application-info") { get { val requestTo: String? = call.parameters["request-to"] call.respond(applicationInfoService.get(requestTo)) } static { resource("/logo", "logo.png") } } } }
此代码段配置请求的路由,尤其是静态资源logo.png。
Ktor服务可能包含功能。一个特征是嵌入在请求-响应管道(一个功能DefaultHeaders,Compression和在上面的例子中的代码等)。您可以实现自己的功能,例如,下面的代码结合基于循环算法的客户端负载平衡来实现服务发现模式:
class ConsulFeature(private val consulClient: Consul) { class Config { lateinit var consulClient: Consul } companion object Feature : HttpClientFeature<Config, ConsulFeature> { var serviceInstanceIndex: Int = 0 override val key = AttributeKey<ConsulFeature>("ConsulFeature") override fun prepare(block: Config.() -> Unit) = ConsulFeature(Config().apply(block).consulClient) override fun install(feature: ConsulFeature, scope: HttpClient) { scope.requestPipeline.intercept(HttpRequestPipeline.Render) { val serviceName = context.url.host val serviceInstances = feature.consulClient.healthClient().getHealthyServiceInstances(serviceName).response val selectedInstance = serviceInstances[serviceInstanceIndex] context.url.apply { host = selectedInstance.service.address port = selectedInstance.service.port } serviceInstanceIndex = (serviceInstanceIndex + 1) % serviceInstances.size } } } }
方法中的主要逻辑是install:在Render请求阶段的时间(在Send阶段之前执行)中,首先确定被叫服务的名称,然后在consulClient服务的实例列表中请求,然后由Round-robin算法定义一个实例正在打电话。因此,以下调用变为可能:
fun getApplicationInfo(serviceName: String): ApplicationInfo = runBlocking { httpClient.get<ApplicationInfo>("http://$serviceName/application-info") }
Micronaut服务
Micronaut由Grails框架的创建者开发,并受到使用Spring,Spring Boot和Grails构建服务的经验的启发。该框架支持Java,Kotlin和Groovy语言。 也许它将是Scala的支持。与Spring Boot相比,依赖项是在编译时注入的,这导致更少的内存消耗和更快的应用程序启动。
主类如下所示:
object MicronautServiceApplication { @JvmStatic fun main(args: Array<String>) { Micronaut.build() .packages("io.heterogeneousmicroservices.micronautservice") .mainClass(MicronautServiceApplication.javaClass) .start() } }
基于Micronaut的应用程序的某些组件与Spring Boot应用程序中的组件相似,例如,以下是控制器代码:
@Controller( value = "/application-info", consumes = [MediaType.APPLICATION_JSON], produces = [MediaType.APPLICATION_JSON] ) class ApplicationInfoController( private val applicationInfoService: ApplicationInfoService ) { @Get fun get(requestTo: String?): ApplicationInfo = applicationInfoService.get(requestTo) @Get("/logo", produces = [MediaType.IMAGE_PNG]) fun getLogo(): ByteArray = applicationInfoService.getLogo() }
plugins { ... kotlin("kapt") ... } dependencies { kapt("io.micronaut:micronaut-inject-java:$micronautVersion") ... kaptTest("io.micronaut:micronaut-inject-java:$micronautVersion") ... }
以下是配置文件的内容:
micronaut: application: name: micronaut-service server: port: 8083 consul: client: registration: enabled: true application-info: name: ${micronaut.application.name} framework: name: Micronaut release-year: 2018
Quarkus服务
引入该框架是作为一种工具,它可以应对各种挑战,例如新的部署环境和应用程序体系结构;在框架上编写的应用程序将具有较低的内存消耗和启动时间。另外,对于开发人员来说也有好处,例如,即时重新加载。
这里有一个在Quarkus的源代码没有main方法:
对于熟悉Spring或Java EE的人来说,Controller看起来非常典型:
@Path("/application-info") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) class ApplicationInfoResource( @Inject private val applicationInfoService: ApplicationInfoService ) { @GET fun get(@QueryParam("request-to") requestTo: String?): Response = Response.ok(applicationInfoService.get(requestTo)).build() @GET @Path("/logo") @Produces("image/png") fun logo(): Response = Response.ok(applicationInfoService.getLogo()).build() }
如您所见,bean是通过@Inject注解注入的,对于注入的bean,您可以指定一个范围,例如:
@ApplicationScoped class ApplicationInfoService( ... ) { ... }
创建其他服务的REST客户端就像使用适当的JAX-RS和MicroProfile注释创建接口一样简单:
@ApplicationScoped @Path("/") interface ExternalServiceClient { @GET @Path("/application-info") @Produces("application/json") fun getApplicationInfo(): ApplicationInfo } @RegisterRestClient(baseUri = "http://helidon-service") interface HelidonServiceClient : ExternalServiceClient @RegisterRestClient(baseUri = "http://ktor-service") interface KtorServiceClient : ExternalServiceClient @RegisterRestClient(baseUri = "http://micronaut-service") interface MicronautServiceClient : ExternalServiceClient @RegisterRestClient(baseUri = "http://quarkus-service") interface QuarkusServiceClient : ExternalServiceClient @RegisterRestClient(baseUri = "http://spring-boot-service") interface SpringBootServiceClient : ExternalServiceClient
如您所见,使用了baseUri参数服务名称。但是现在不存在对服务发现(Eureka)的内置支持,或者它无法正常运行(Consul),因为该框架主要针对云环境。因此,就像在Helidon和Ktor服务中一样,使用Consul Client for Java 库。首先,需要注册该应用程序:
@ApplicationScoped class ConsulRegistrationBean( @Inject private val consulClient: ConsulClient ) { fun onStart(@Observes event: StartupEvent) { consulClient.register() } }
然后需要将服务名称解析到其特定位置。通过requestContext用Consul客户端获得的服务的位置替换URI即可简单地实现解决方案:
@Provider @ApplicationScoped class ConsulFilter( @Inject private val consulClient: ConsulClient ) : ClientRequestFilter { override fun filter(requestContext: ClientRequestContext) { val serviceName = requestContext.uri.host val serviceInstance = consulClient.getServiceInstance(serviceName) val newUri: URI = URIBuilder(URI.create(requestContext.uri.toString())) .setHost(serviceInstance.address) .setPort(serviceInstance.port) .build() requestContext.uri = newUri } }
SpringBooy服务
创建该框架是为了简化使用Spring Framework生态系统的应用程序开发。这是通过对所用库的自动配置机制来实现的。
以下是控制器代码:
@RestController @RequestMapping(path = ["application-info"], produces = [MediaType.APPLICATION_JSON_VALUE]) class ApplicationInfoController( private val applicationInfoService: ApplicationInfoService ) { @GetMapping fun get(@RequestParam("request-to") requestTo: String?): ApplicationInfo = applicationInfoService.get(requestTo) @GetMapping(path = ["/logo"], produces = [MediaType.IMAGE_PNG_VALUE]) fun getLogo(): ByteArray = applicationInfoService.getLogo() }
微服务由YAML文件配置:
spring: application: name: spring-boot-service server: port: 8085 application-info: name: ${spring.application.name} framework: name: Spring Boot release-year: 2014
开始集成以上四种微服务
在启动微服务之前,您需要安装Consul并 启动代理—例如,如下所示:consul agent -dev。
您可以从以下位置启动微服务:
- 集成开发环境:
- IntelliJ IDEA的用户可能会看到类似以下的内容:
要启动Quarkus服务,您需要启动quarkusDevGradle任务。
控制台:
在项目的根文件夹中执行:
java -jar helidon-service/build/libs/helidon-service-all.jar
java -jar ktor-service/build/libs/ktor-service-all.jar
java -jar micronaut-service/build/libs/micronaut-service-all.jar
java -jar quarkus-service/build/quarkus-service-1.0.0-runner.jar
java -jar spring-boot-service/build/libs/spring-boot-service.jar
在启动所有微服务之后,
http://localhost:8500/ui/dc1/services您将看到:
API测试
例如,测试Helidon服务的API的结果:
- GET http://localhost:8081/application-info
- { "name": "helidon-service", "framework": { "name": "Helidon SE", "releaseYear": 2019 }, "requestedService": null }
GET http://localhost:8081/application-info?request-to=ktor-service
{ "name": "helidon-service", "framework": { "name": "Helidon SE", "releaseYear": 2019 }, "requestedService": { "name": "ktor-service", "framework": { "name": "Ktor", "releaseYear": 2018 }, "requestedService": null } }
应用程序参数比较
在发布新版本的框架之后,以下结果(当然是不科学的,只能在我的机器上复制)将不可避免地过时;您可以使用此GitHub项目和框架的新版本(在中指定gradle.properties)自行检查更新的结果 。
工件尺寸
为了保持设置应用程序的简便性,构建脚本没有排除传递依赖项,因此Spring Boot服务uber-JAR的大小大大超过了其他框架上类似物的大小。如果需要,可以大大减小尺寸):
Microservice | Artifact size, MB |
Helidon service | 17,3 |
Ktor service | 22,4 |
Micronaut service | 17,1 |
Quarkus service | 24,4 |
Spring Boot service | 45,2 |
启动时间
每个应用程序的启动时间是不固定的,并且会落入某个“窗口”中。下表显示了工件的开始时间,未指定任何其他参数:
Microservice | Start time, seconds |
Helidon service | 2,0 |
Ktor service | 1,5 |
Micronaut service | 2,8 |
Quarkus service | 1,9 |
Spring Boot service | 10,7 |
值得注意的是,如果您在Spring Boot上从不必要的依赖中“清理”了应用程序,并注意设置应用程序的启动(例如,仅扫描必要的程序包并使用Bean的惰性初始化),那么您可以减少发射时间。
内存使用情况
对于每个微服务,确定了以下内容:
- -Xmx运行正常(响应不同类型的请求)微服务所需的最小堆内存(由使用参数确定)
- 通过负载测试所需的最小堆内存50个用户* 1000个请求
- 通过负载测试所需的最小堆内存500个用户* 1000个请求
堆内存仅是分配给应用程序的总内存的一部分。如果要测量整体内存使用情况,则可以使用本指南。
对于负载测试,使用了Gatling和Scala脚本 。负载生成器和要测试的服务在同一台计算机上运行(Windows 10、3.2 GHz四核处理器,24 GB RAM,SSD)。服务的端口在Scala脚本中指定。通过负载测试意味着微服务已经在任何时间响应了所有请求。
微服务 | 最小堆内存量,MB | ||
启动healthy服务 | 负载为50 * 1000 | 负载500 * 1000 | |
Helidon服务 | 11 | 9 | 11 |
Ktor服务 | 13 | 11 | 15 |
Micronaut服务 | 17 | 15 | 19 |
Quarkus服务 | 13 | 17 | 21 |
春季启动服务 | 18 | 19 | 23 |
应该注意的是,所有微服务都使用Netty HTTP服务器。
结论
在所有考虑的框架中都成功实现了所需的功能-带有HTTP API的简单服务以及能够在MSA中运行的功能。现在该进行盘点,并考虑其优缺点。
Helidon
标准版
- 优点:
- 应用参数所有参数的结果都不错,“没有魔术”框架证明了开发人员的原则:对于创建的应用程序,只需要一个注释(对于Java-Kotlin互操作,则为—个@JvmStatic)
- 缺点:
- 微框架,一些工业开发所需的某些组件不是开箱即用的(例如,依赖项注入和与Service Discovery服务器的交互)
MicroProfile
微服务尚未在此框架上实现,因此,我仅要注意一点。
- 优点:
- Eclipse MicroProfile实施本质上是针对MSA优化的Java EE。因此,首先,您可以访问各种Java EE API,包括专门为MSA开发的Java EE API;其次,您可以将MicroProfile的实现更改为其他任何实现(Open Liberty,WildFly Swarm等)。
Ktor
- 优点:
- 轻量:仅允许您添加执行任务直接需要的那些功能
- 应用参数:所有参数的结果都不错
- 缺点:
- Kotlin下的“锐化”,也就是说,用其他语言进行开发可能是不可能的,或者是不值得的、
- 一方面,该框架未包含在两个最受欢迎的Java开发模型(类似于Spring(Spring Boot / Micronaut)和Java EE / MicroProfile)中,这可能导致:
寻找专家的问题
由于需要显式配置所需的功能,因此与Spring Boot相比,执行任务的时间增加了
另一方面,与“经典” Spring和Java EE的不同让我们可以更自觉地从不同的角度看待开发过程。
Micronaut
- 优点:
- AOT,如前所述,与Spring Boot上的模拟相比,AOT可以减少应用程序的启动时间和内存消耗。
- Spring开发模式:具有Spring框架经验的程序员不会花很多时间来掌握这个框架
- 应用参数:所有参数的结果都不错
- 此外该Micronaut Spring项目允许,改变现有的Spring Booy应用程序的执行环境到Micronaut(有限制)
Quarkus
- 优点:Eclipse MicroProfile的实现
- 应用参数:所有参数的结果都不错
Spring Boot
- 优点平台成熟度和生态系统:
- 对于大多数日常任务,Spring的编程范例中已经存在一种解决方案,即以许多程序员习惯的方式。此外,通过启动程序和自动配置的概念简化了开发劳动力市场中的大量专家,以及重要的知识库(包括文档和有关Stack Overflow的答案)看法我认为许多人都会同意,Spring在不久的将来仍将是领先的Java / Kotlin框架。
- 缺点:
- 应用参数在该框架中的应用并不是领先者,但是,如前所述,您可以优化一些参数。还应该记住Spring Fu项目的存在,该项目 正在积极开发中,使用它可以减少这些参数
此外,我们可以重点介绍新框架存在的常见问题,但是Spring Boot缺少:
- 欠发达的生态系统?
- 很少有这些技术经验的专家?
- 任务执行时间更长
- 前景不明
所考虑的框架属于不同的权重划分:Helidon SE和Ktor是 微框架,Spring Boot和Micronaut是全栈框架,Quarkus和Helidon MP是MicroProfile框架。微框架的功能是有限的,这会减慢开发速度;为了阐明基于特定框架实现特定功能的可能性,我建议您熟悉其文档。
我不敢判断这个框架或那个框架是否会在不久的将来“爆发”,因此,我认为,目前最好是继续使用现有框架来解决工作任务,观察开发情况。
同时,如本文所示,新框架在考虑到应用程序参数的情况下赢得了Spring Boot的青睐。如果这些参数中的任何一个对于您的一个微服务至关重要,那么可能值得关注显示最佳结果的框架。但是,我们不应忘记,Spring Boot首先会继续改进,其次,它具有庞大的生态系统,并且许多Java程序员都熟悉它。另外,本文中未涉及其他框架:Vert.x,Javalin等。