文章轉載出處:微信公眾號——鍋外的大佬 鏈接:https://mp.weixin.qq.com/s/MdBByJ0ju-rROKg7jsWygA
今天我們將比較兩個在JVM
上構建微服務的框架:Spring Boot
和Micronaut
。Spring Boot
是JVM
世界中最受歡迎和最具代表性的框架。Micronaut
作為Spring Boot
的競爭對手,通過構建無服務器功能或低內存占用微服務,迅速流行起來。我們將比較Spring Boot
的2.1.4
版本和Micronaut
的1.0.0.RC1
。比較標准是:
- 內存使用情況(堆和非堆)
- 生成
fat JAR
文件的MB數 - 應用程序啟動時間
- 應用程序的性能,在樣本負載測試期間REST端口的平均響應時間的含義
為了使測試盡可能准確,我們將收集兩個幾乎相同的應用程序的統計數據。當然,唯一的區別在於構建的框架。示例應用程序非常簡單。它為一個實體公開了一些在內存中操作CRUD的端口。它還公開了info
和health
端點,以及通過Swagger API
自動生成所有端點的文檔。
我將在JDK 11
上測試示例應用程序性能。在啟動和負載測試期間使用Yourkit
分析和監視內存使用情況,並使用Gatling
來構建性能API測試。首先,簡要概述一下示例應用程序。
1. 源代碼
我已經實現了非常簡單的內存存儲bean,它將新對象添加到列表中,並提供了通過在add方法中生成的id搜索對象的find方法。
public class PersonRepository { List<Person> ersons = new ArrayList<>(); public Person add(Person person) { person.setId(persons.size()+1); persons.add(person); return person; } public Person findById(Long id) { Optional<Person> person = persons.stream().filter(a -> a.getId().equals(id)).findFirst(); if (person.isPresent()) return person.get(); else return null; } public List<Person> findAll() { return persons; } }
Repository Bean
被注入到controller
(控制器)。Controller
公開了兩種HTTP
方法。其中第一個(POST
)用於添加新對象,而第二個(GET)用於通過id搜索它。這是Spring Boot應用程序中的控制器實現:
@RestController@RequestMapping("/persons")public class PersonsController {private static final Logger LOGGER = LoggerFactory.getLogger(PersonsController.class);@AutowiredPersonRepository repository;@PostMappingpublic Person add(@RequestBody Person person) {LOGGER.info("Person add: {}", person);return repository.add(person);}@GetMapping("/{id}")public Person findById(@PathVariable("id") Long id) {LOGGER.info("Person find: id={}", id);return repository.findById(id);}@GetMappingpublic List<Person> findAll() {LOGGER.info("Person find");return repository.findAll();}}
接下來是Micronaut
的類似實現。為了實現REST端點:healthcheck
和Swagger API
,我們需要添加一些依賴項。以下是Spring Boot的依賴項列表:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.4.RELEASE</version> </parent> <groupId>pl.piomin.services</groupId> <artifactId>sample-app</artifactId> <version>1.0-SNAPSHOT</version> <properties> <java.version>11</java.version> <maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.target>${java.version}</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> </dependencies>
以下是Micronaut所需的類似依賴列表:
<dependency> <groupId>io.micronaut</groupId> <artifactId>micronaut-http-server-netty</artifactId> </dependency> <dependency> <groupId>io.micronaut</groupId> <artifactId>micronaut-inject</artifactId> </dependency> <dependency> <groupId>io.micronaut</groupId> <artifactId>micronaut-runtime</artifactId> </dependency> <dependency> <groupId>io.micronaut</groupId> <artifactId>micronaut-management</artifactId> </dependency> <dependency> <groupId>io.micronaut</groupId> <artifactId>micronaut-inject-java</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>io.swagger.core.v3</groupId> <artifactId>swagger-annotations</artifactId> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> <scope>runtime</scope> </dependency>
在
application.yml
中必須使用一些額外的配置來啟用Swagger
和healthchecks
:
micronaut: router: static-resources: swagger: paths: classpath:META-INF/swagger mapping: /swagger/**endpoints: info: enabled: true sensitive: false
2.開啟應用
首先,使用Intellij開始應用程序。基於Spring Boot
構建的示例應用程序大約需要6-7秒。如下所示,正好需要6.344s.
建立在Micronaut
之上的類似應用程序開啟大約3-4秒。如下所示,正好占用3.463。
無論如何,當我通過公共代理來啟動應用程序時,都必須設置VM選項Dmicronaut.cloud.platform=BARE_METAL
來規避環境的影響。
這是Spring Boot
和Micronaut
啟動時間差異的圖表。
3. 應用構建
我們還將檢查應用程序fat JAR
的大小。為此,推薦使用mvn clean install命令構建應用程序。對於Spring Boot
,我們使用了兩個標准的starter
包: Web
,Actuator
和Swagger SpringFox
庫。因此,包含了50多個庫。當然,我們可以做一些去除或不使用starter
包,但我選擇了最簡單的方法來構建應用程序。fat JAR
的大小為24.2 MB。同樣的,基於Micronaut
的應用程序就要小很多。fat JAR
的大小為12.1 MB。我在pom.xml
中包含了更多的庫,最后包含了37個庫。Spring Boot
在標准配置中包含更多庫,但在另一方面它比Micronaut
有更多的功能和自動配置。這是Spring Boot
和Micronaut
目標JAR
大小差異的圖表。
4. 內存管理在啟動之后,Spring Boot
應用程序為堆分配了305 MB,為non-heap
(非堆)分配了81 MB。我沒有使用Xmx
或任何其他選項設置任何內存限制。在堆中,old gen
(老年代)消耗了8 MB,eden
區消耗了60 MB,survivor
消耗了15 MB。大多數non-heap
(非堆)由metaspace
消耗 - 52 MB。運行性能負載測試后,堆分配增加到369 MB,non-heap
(非堆)到87 MB。這是性能測試之前和測試期間的CPU和RAM使用情況的截圖。
在啟動之后,Micronaut
應用程序為堆分配了254 MB,為非堆分配了51 MB。我沒有使用Xmx或任何其他選項設置任何內存限制 - 與Spring Boot
應用程序相同。在堆中,old gen
(老年代)消耗了2.5 MB,eden
區消耗了20 MB,survivor
消耗了7 MB。大多數non-heap
非堆內存由metaspace
消耗 - 35 MB。運行性能負載測試后,堆分配沒有改變,非堆增加到63 MB。這是性能測試之前和測試期間CPU和RAM使用情況的截圖。
這是Spring Boot
和Micronaut
啟動之后堆內存使用情況比較。
non-heap:
5. 性能測試使用Gatling
來構建性能負載測試。此工具允許在Scala
中創建測試方案。我們使用20個線程同時發送40k樣本請求。這是為POST
方法實現的測試類。
class SimpleTest extends Simulation {val scn = scenario("AddPerson").repeat(2000, "n") {exec(http("Persons-POST").post("http://localhost:8080/persons").header("Content-Type", "application/json").body(StringBody("""{"name":"Test${n}","gender":"MALE","age":100}""")).check(status.is(200)))}setUp(scn.inject(atOnceUsers(20))).maxDuration(FiniteDuration.apply(10, TimeUnit.MINUTES))}
這是為GET
方法實現的測試類。
class SimpleTest2 extends Simulation {val scn = scenario("GetPerson").repeat(2000, "n") {exec(http("Persons-GET").get("http://localhost:8080/persons/${n}").check(status.is(200)))}setUp(scn.inject(atOnceUsers(20))).maxDuration(FiniteDuration.apply(10, TimeUnit.MINUTES))}
post/person
方法的性能測試結果如下圖所示。一秒鍾內平均處理請求數是1176。
下面的截圖顯示了響應時間隨時間變化的百分位數的直方圖。GET / persons / {id}
方法的性能測試結果如下圖所示。一秒鍾內平均處理請求數是1428。
以下截圖顯示了響應時間隨時間變化的百分位數的直方圖。
現在,我們為Micronaut
應用程序進行相同的Gatling
負載測試。 POST /person 方法的性能測試結果如下圖所示。在一秒鍾內平均處理請求數是1290。
以下截圖顯示了響應時間隨時間變化的百分位數的直方圖。GET / persons / {id}
方法的性能測試結果如下圖所示。一秒鍾內平均處理請求數是1538。
以下截圖顯示了響應時間隨時間變化的百分位數的直方圖。
Spring Boot和Micronaut的處理時間沒有太大差別。時間上的微小差異可能與框架無關,而與基礎的服務器有關。默認情況下,Spring Boot使用Tomcat,而Micronaut使用Netty。