Ảnh native biên dịch ứng dụng Java thành mã máy. Bài này đo nó trên một ứng dụng Spring Boot thật.

Bốn con số

  khởi động  ảnh native : 0,171 s | 0,100 s | 0,103 s
  khởi động  JVM        : 1,188 s | 1,135 s | 1,057 s

  RSS  ảnh native : 115 744 KB   (113 MB)
  RSS  JVM        : 341 924 KB   (334 MB)

  kích thước  ảnh native : 78,9 MB (chạy độc lập)
  kích thước  jar        : 24,7 MB (cần thêm JRE)

  thời gian build native : 45,9 s | đỉnh RSS 4,85 GB | CPU load 9,64

Khởi động nhanh hơn mười lần. Bộ nhớ ít hơn 66%.

Đó là hai lời hứa của ảnh native, và cả hai đều đúng.

Cái giá

Build

45,9 giây cho một ứng dụng gần như trống. Ứng dụng thật với JPA, Security và vài chục phụ thuộc thường mất năm tới mười lăm phút.

Và nó ngốn tài nguyên: đỉnh RSS 4,85 GB, CPU load 9,64. Máy CI của bạn phải chịu được điều đó, và mỗi lần build tốn ngần ấy.

Hệ quả thực tế: vòng lặp phát triển của bạn không dùng ảnh native. Bạn phát triển trên JVM và chỉ dựng native cho bản phát hành — nghĩa là có hai chế độ chạy, và thứ chạy được ở chế độ này có thể hỏng ở chế độ kia.

Phản chiếu

Ảnh native dùng phân tích tĩnh đóng thế giới: mọi thứ được gọi phải biết trước lúc biên dịch. Nhưng phản chiếu, proxy động, và nạp tài nguyên lúc chạy đều không nhìn thấy được bằng phân tích tĩnh.

Spring Boot xử lý phần lớn qua AOT: lúc build, nó chạy trước phần cấu hình và sinh ra siêu dữ liệu cho GraalVM. Nhờ vậy phép đo ở trên chạy được mà tôi không phải khai gì.

Nhưng nó chỉ lo được thứ Spring biết. Thư viện bên thứ ba dùng phản chiếu mà không có tệp hint sẽ hỏng — và hỏng lúc chạy, ở đúng nhánh mã hiếm gặp, chứ không phải lúc build.

Cách xử lý:

@RegisterReflectionForBinding(LopCuaToi.class)

hoặc chạy ứng dụng trên JVM với tác nhân theo dõi rồi lấy siêu dữ liệu nó sinh ra:

java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image -jar app.jar

Cách thứ hai đòi bạn thực thi mọi nhánh mã trong lúc theo dõi — nhánh nào không chạy thì nhánh đó không được ghi lại.

Hiệu năng đỉnh

Tôi đã thử đo thông lượng và không tách được phần của máy chủ khỏi chi phí khởi động tiến trình curl — cả hai cho ra khoảng 3,9 giây cho 2000 request, tức là tôi đang đo curl chứ không đo ứng dụng. Nên tôi không đưa con số đó ra như một kết luận.

Điều biết được từ nguyên lý: ảnh native không có JIT, nên nó không tối ưu theo hồ sơ chạy thật. Bài 85 sê-ri Java nói về các tầng JIT và vì sao C2 quan trọng. Với tải chạy dài, JVM đã làm nóng thường nhanh hơn ảnh native — GraalVM có chế độ PGO để thu hẹp khoảng cách, nhưng nó thuộc bản Oracle GraalVM và thêm một bước build nữa.

Khi nào nó đáng

Nền không máy chủ. Mỗi lời gọi có thể là một lần khởi động nguội. 0,1 giây so với 1,1 giây là khác biệt giữa dùng được và không.

Co giãn theo tải, rất nhanh. Pod mới phục vụ được sau một phần mười giây.

Container nhiều, RAM đắt. 113 MB so với 334 MB, nhân với hàng trăm bản sao.

CLI viết bằng Java. Khởi động JVM là thứ làm CLI Java bị chê, và ảnh native xoá hẳn nó.

Khi nào không

Dịch vụ chạy dài, tải ổn định. Khởi động một lần rồi chạy hàng tuần — 1 giây so với 0,1 giây không có nghĩa gì, mà bạn mất JIT.

Dùng nhiều thư viện chưa hỗ trợ native. Bạn sẽ dành nhiều thời gian viết hint hơn viết tính năng.

Đội chưa quen. Gỡ lỗi ảnh native khó hơn hẳn: không có JFR đầy đủ, không có heap dump như thường, và công cụ mà bài 89 sê-ri Java giới thiệu phần lớn không dùng được.

CI không chịu nổi. 5 GB RAM và mười lăm phút cho mỗi lần build là chi phí thật.

Với phần lớn ứng dụng Spring Boot chạy trong Kubernetes, câu trả lời thành thật là: chưa cần. Bài 56 cho thấy có nhiều cách rẻ hơn để khởi động nhanh hơn — CDS chẳng hạn — mà không đánh đổi gì.

Nếu vẫn muốn thử

<profile>
  <id>native</id>
  <build><plugins>
    <plugin>
      <groupId>org.graalvm.buildtools</groupId><artifactId>native-maven-plugin</artifactId>
    </plugin>
  </plugins></build>
</profile>
mvn -Pnative native:compile
# hoặc dựng thẳng ảnh Docker, không cần cài GraalVM cục bộ:
mvn -Pnative spring-boot:build-image

Ba lời khuyên:

Chạy bộ test ở chế độ nativemvn -PnativeTest test. Đây là cách duy nhất bắt được lỗi phản chiếu trước khi lên sản xuất.

Đọc kỹ khuyến nghị lúc build. Bản build của tôi in ra ba dòng: đặt trần heap, bật --strict-image-heap, bật -march=native. Chúng đáng làm.

Bắt đầu từ dự án mới, đừng chuyển dự án đang chạy. Chuyển đổi là công việc dài và dễ nản.

Thử ba mươi giây

grep "Started .* in" <log khởi động>

Nếu con số đó dưới hai giây và bạn không chạy trên nền không máy chủ — hãy đóng bài này lại. Bài 56 có ba cách cải thiện rẻ hơn nhiều, và không cách nào đòi bạn viết tệp hint phản chiếu.

Còn nếu bạn thấy 0,1 giây là thứ thay đổi được kiến trúc của mình, thì bảng đầu bài là điểm khởi đầu — kèm cả cột chi phí.

Ngày mai — bài cuối: nhìn lại sáu mươi ngày.