天天看點

微服務架構:如果不用Spring Boot,還可以選擇誰

前言

在 Java 和 Kotlin 中, 除了使用Spring Boot建立微服務外,還有很多其他的替代方案。

名稱 版本 釋出時間 開發商 GitHub
Helidon SE 1.4.1 2019年 甲骨文 連結
Ktor 1.3.0 2018年 JetBrains
Micronaut 1.2.9 Object Computing
Quarkus 1.2.0 Red Hat
Spring Boot 2.2.4 2014年 Pivotal

本文,基于這些微服務架構,建立了五個服務,并使用

Consul

的服務發現模式實作服務間的 互相通信。是以,它們形成了異構微服務架構(Heterogeneous Microservice Architecture, 以下簡稱 MSA):

微服務架構:如果不用Spring Boot,還可以選擇誰

本文簡要考慮了微服務在各個架構上的實作(更多細節請檢視源代碼:

https

:

//github.com/rkudryashov/heterogeneous-microservices

  • 技術棧:
  • JDK 13
  • Kotlin
  • Gradle (Kotlin DSL)
  • JUnit 5
  • 功能接口(HTTP API):
  • GET /application-info{?request-to=some-service-name}
    • -- 傳回微服務的一些基本資訊(名稱、架構、釋出年份)
  • GET /application-info/logo
    • -- 傳回logo資訊
  • 實作方式:
  • 使用文本檔案的配置方式
  • 使用依賴注入
  • HTTP API
  • MSA:
  • 使用服務發現模式(在Consul中注冊,通過用戶端負載均衡的名稱請求另一個微服務的HTTP API)
  • 建構一個 uber-JAR

先決條件

從頭開始建立應用程式

要基于其中一個架構上生成新項目,你可以使用web starter 或其他選項(例如,建構工具或 IDE):

Web starter 指南 支援的開發語言
Helidon (MP) (SE) Java,Kotlin
Groovy、Java、Kotlin
Java、Kotlin、Scala

Helidon服務

該架構是在 Oracle 中建立以供内部使用,随後成為開源。Helidon 非常簡單和快捷,它提供了兩個版本:标準版(SE)和MicroProfile(MP)。在這兩種情況下,服務都是一個正常的 Java SE 程式。(在

上了解更多資訊)

Helidon MP 是 Eclipse

MicroProfile

的實作之一,這使得使用許多 API 成為可能,包括 Java EE 開發人員已知的(例如 JAX-RS、CDI等)和新的 API(健康檢查、名額、容錯等)。在 Helidon SE 模型中,開發人員遵循“沒有魔法”的原則,例如,建立應用程式所需的注解數量較少或完全沒有。

Helidon SE 被選中用于微服務的開發。因為Helidon SE 缺乏依賴注入的手段,是以為此使用了

Koin

以下代碼示例,是包含 main 方法的類。為了實作依賴注入,該類繼承自KoinComponent。

首先,Koin 啟動,然後初始化所需的依賴并調用startServer()方法----其中建立了一個WebServer類型的對象,應用程式配置和路由設定傳遞到該對象;

啟動應用程式後在Consul注冊:

object HelidonServiceApplication : KoinComponent {

   @JvmStatic

   fun main(args: Array) {

       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"))

           .send(applicationInfoService.getLogo())

   .error(Exception::class.java) { req, res, ex ->

       log.error("Exception:", ex)

       res.status(Http.Status.INTERNAL_SERVER_ERROR_500).send()

   .build()

該應用程式使用

HOCON

格式的配置檔案:

webserver {

 port: 8081

application-info {

 name: "helidon-service"

 framework {

   name: "Helidon SE"

   release-year: 2019

 }

還可以使用 JSON、YAML 和properties 格式的檔案進行配置(在

Helidon 配置文檔

中了解更多資訊)。

Ktor服務

該架構是為 Kotlin 編寫和設計的。和 Helidon SE 一樣,Ktor 沒有開箱即用的 DI,是以在啟動伺服器依賴項之前應該使用 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) {

   startKoin {

       modules(koinModule)

   val server = embeddedServer(Netty, commandLineEnvironment(args))

   server.start(wait = true)

應用程式需要的子產品在配置檔案中指定(HOCON格式;更多配置資訊參考

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]

 name: "ktor-service"

   name: "Ktor"

   release-year: 2018

在 Ktor 和 Koin 中,術語“子產品”具有不同的含義。

在 Koin 中,子產品類似于 Spring 架構中的應用程式上下文。Ktor的子產品是一個使用者定義的函數,它接受一個 Application類型的對象,可以配置流水線、注冊路由、處理請求等:

fun Application.module() {

   val applicationInfoService: ApplicationInfoService by inject()

   if (!isTest()) {

       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。

下面是基于Round-robin算法結合用戶端負載均衡實作服務發現模式的代碼:

class ConsulFeature(private val consulClient: Consul) {

   class Config {

       lateinit var consulClient: Consul

   companion object Feature : HttpClientFeature {

       var serviceInstanceIndex: Int = 0

       override val key = AttributeKey("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請求服務的執行個體清單,然後通過循環算法定義一個執行個體正在調用。是以,以下調用成為可能:

fun getApplicationInfo(serviceName: String): ApplicationInfo = runBlocking {

   httpClient.get("http://$serviceName/application-info")

Micronaut 服務

Micronaut 由

Grails

架構的建立者開發,靈感來自使用 Spring、Spring Boot 和 Grails 建構服務的經驗。該架構目前支援 Java、Kotlin 和 Groovy 語言。依賴是在編譯時注入的,與 Spring Boot 相比,這會導緻更少的記憶體消耗和更快的應用程式啟動。

主類如下所示:

object MicronautServiceApplication {

       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()

Micronaut 中對 Kotlin 的支援建立在

kapt

編譯器插件的基礎上(參考

Micronaut Kotlin 指南

了解更多詳細資訊)。

建構腳本配置如下:

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

JSON、properties和 Groovy 檔案格式也可用于配置(參考

Micronaut 配置指南

檢視更多詳細資訊)。

Quarkus服務

Quarkus是作為一種應對新部署環境和應用程式架構等挑戰的工具而引入的,在架構上編寫的應用程式将具有低記憶體消耗和更快的啟動時間。此外,對開發人員也很友好,例如,開箱即用的實時重新加載。

Quarkus 應用程式目前沒有 main 方法,但也許未來會出現(GitHub 上的

問題

)。

對于熟悉 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()

   @Path("/logo")

   @Produces("image/png")

   fun logo(): Response = Response.ok(applicationInfoService.getLogo()).build()

如你所見,bean 是通過@Inject注解注入的,對于注入的 bean,你可以指定一個範圍,例如:

@ApplicationScoped

class ApplicationInfoService(

...

為其他服務建立 REST 接口,就像使用 JAX-RS 和 MicroProfile 建立接口一樣簡單:

@Path("/")

interface ExternalServiceClient {

   @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

但是它現在缺乏對服務發現 (

Eureka

) 的内置支援,因為該架構主要針對雲環境。是以,在 Helidon 和 Ktor 服務中, 我使用了Java類庫方式的

Consul 用戶端

首先,需要注冊應用程式:

class ConsulRegistrationBean(

   @Inject private val consulClient: ConsulClient

   fun onStart(@Observes event: StartupEvent) {

       consulClient.register()

然後需要将服務的名稱解析到其特定位置;

解析是通過從 Consul 用戶端獲得的服務的位置替換 requestContext的URI 來實作的:

@Provider

class ConsulFilter(

) : 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

Quarkus也支援通過properties 或 YAML 檔案進行配置(參考

Quarkus 配置指南

Spring Boot服務

建立該架構是為了使用 Spring Framework 生态系統,同時有利于簡化應用程式的開發。這是通過auto-configuration實作的。

以下是控制器代碼:

@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      

也可以使用properties檔案進行配置(更多資訊參考

Spring Boot 配置文檔

啟動微服務

在啟動微服務之前,你需要

安裝Consul 啟動代理

-例如,像這樣:consul agent -dev。

你可以從以下位置啟動微服務:

  • IDE中啟動微服務IntelliJ IDEA 的使用者可能會看到如下内容:
微服務架構:如果不用Spring Boot,還可以選擇誰
  • 要啟動 Quarkus 服務,你需要啟動quarkusDev的Gradle 任務。
  • console中啟動微服務在項目的根檔案夾中執行:

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,你将看到:

微服務架構:如果不用Spring Boot,還可以選擇誰

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

 "requestedService": {

   "name": "ktor-service",

   "framework": {

         "name": "Ktor",

         "releaseYear": 2018

   },

   "requestedService": null

  • GET http://localhost:8081/application-info/logo傳回logo資訊

你可以使用

Postman

、IntelliJ IDEA

HTTP 用戶端

、浏覽器或其他工具測試微服務的 API接口 。

不同微服務架構對比

不同微服務架構的新版本釋出後,下面的結果可能會有變化;你可以使用

此GitHub項目

自行檢查最新的對比結果 。

程式大小

為了保證設定應用程式的簡單性,建構腳本中沒有排除傳遞依賴項,是以 Spring Boot 服務 uber-JAR 的大小大大超過了其他架構上的類似物的大小(因為使用 starters 不僅導入了必要的依賴項;如果需要,可以通過排除指定依賴來減小大小):

備注:什麼是 maven的uber-jar

在maven的一些文檔中我們會發現 "uber-jar"這個術語,許多人看到後感到困惑。其實在很多程式設計語言中會把super叫做uber (因為super可能是關鍵字), 這是上世紀80年代開始流行的,比如管superman叫uberman。是以uber-jar從字面上了解就是super-jar,這樣的jar不但包含自己代碼中的class ,也會包含一些第三方依賴的jar,也就是把自身的代碼和其依賴的jar全打包在一個jar裡面了,是以就很形象的稱其為super-jar ,uber-jar來曆就是這樣的。

微服務 程式大小(MB)
17,3
22,4
17,1
24,4
45,2

啟動時長

每個應用程式的啟動時長都是不固定的:

開始時間(秒)
2,0
1,5
2,8
1,9
10,7

值得注意的是,如果你将 Spring Boot 中不必要的依賴排除,并注意設定應用的啟動參數(例如,隻掃描必要的包并使用 bean 的延遲初始化),那麼你可以顯著地減少啟動時間。

記憶體使用情況

對于每個微服務,确定了以下内容:

  • 通過-Xmx參數,指定微服務所需的堆記憶體大小
  • 通過負載測試服務健康的請求(能夠響應不同的請求)
  • 通過負載測試50 個使用者 * 1000 個的請求
  • 通過負載測試500 個使用者 * 1000 個的請求

堆記憶體隻是為應用程式配置設定的總記憶體的一部分。例如,如果要測量總體記憶體使用情況,可以參考

本指南

對于負載測試,使用了

Gatling Scala腳本
  • 負載生成器和被測試的服務在同一台機器上運作(Windows 10、3.2 GHz 四核處理器、24 GB RAM、SSD)。
  • 服務的端口在 Scala 腳本中指定。
  • 通過負載測試意味着微服務已經響應了所有時間的所有請求。
堆記憶體大小(MB)
對于健康服務 對于 50 * 1000 的負載 對于 500 * 1000 的負載
11 9
13 15
17 19
21
18 23

需要注意的是,所有微服務都使用 Netty HTTP 伺服器。

結論

通過上文,我們所需的功能——一個帶有 HTTP API 的簡單服務和在 MSA 中運作的能力——在所有考慮的架構中都取得了成功。

是時候開始盤點并考慮他們的利弊了。

Helidon标準版

優點

建立的應用程式,隻需要一個注釋(@JvmStatic)

缺點

開發所需的一些元件缺少開箱即用(例如,依賴注入和與服務發現伺服器的互動)

Helidon MicroProfile

微服務還沒有在這個架構上實作,是以這裡簡單說明一下。

Eclipse MicroProfile 實作

本質上,MicroProfile 是針對 MSA 優化的 Java EE。是以,首先你可以通路各種 Java EE API,包括專門為 MSA 開發的 API,其次,你可以将 MicroProfile 的實作更改為任何其他實作(例如:Open Liberty、WildFly Swarm 等)

  • 輕量級的允許你僅添加執行任務直接需要的那些功能
  • 應用參數所有參數的良好結果

  • 依賴于Kotlin,即用其他語言開發可能是不可能的或不值得的
  • 微架構:參考
  • 目前最流行的兩種 Java 開發模型(Spring Boot/Micronaut)和 Java EE/MicroProfile)中沒有包含該架構,這會導緻:
    • 難以尋找專家
    • 由于需要顯式配置所需的功能,是以與 Spring Boot 相比,執行任務的時間有所增加

  • AOT如前所述,與 Spring Boot 上的模拟相比,AOT 可以減少應用程式的啟動時間和記憶體消耗
  • 類Spring開發模式有 Spring 架構經驗的程式員不會花太多時間來掌握這個架構
  • Micronaut for Spring 可以改變現有的Spring Boot應用程式的執行環境到Micronaut中(有限制)

  • 平台成熟度和生态系統對于大多數日常任務,Spring的程式設計範式已經有了解決方案,也是很多程式員習慣的方式。此外,starter和auto-configuration的概念簡化了開發
  • 專家多,文檔詳細

我想很多人都會同意 Spring 在不久的将來仍将是 Java/Kotlin開發領域領先的架構。

  • 應用參數多且複雜但是,有些參數,如前所述,你可以自己優化。還有一個 Spring Fu 項目的存在,該項目正在積極開發中,使用它可以減少參數。
Helidon SE 和 Ktor 是 微架構 ,Spring Boot 和 Micronaut 是全棧架構,Quarkus 和 Helidon MP 是 MicroProfile 架構。微架構的功能有限,這會減慢開發速度。

我不敢判斷這個或那個架構會不會在近期“大更新”,是以在我看來,目前最好繼續觀察,使用熟悉的架構解決工作問題。

同時,如本文所示,新架構在應用程式參數設定方面赢得了 Spring Boot。如果這些參數中的任何一個對你的某個微服務至關重要,那麼也許值得關注。但是,我們不要忘記,Spring Boot 一是在不斷改進,二是它擁有龐大的生态系統,并且有相當多的 Java 程式員熟悉它。此外,還有未涉及的其他架構:Vert.x、Javalin 等,也值得關注。

參考連結:

https://dzone.com/articles/not-only-spring-boot-a-review-of-alternatives

繼續閱讀