RE:NODE

Quarkus და Micronaut Java სერვერზე: fast-jar და heap

ავტორი RE:NODE-ის გუნდი
18 წუთის საკითხავი


Quarkus და Micronaut ორივე ჩვეულებრივ Java სერვერზე ჩვეულებრივი jar-ის სახით ეშვება და მცირე გეგმაზე მათი ჩატევა უფრო ადვილია: dependency injection-ის მუშაობას ისინი build-ის დროს ასრულებენ, ამიტომ ერთ-ორ წამში სტარტავენ და შედარებით Spring Boot სერვისზე მცირე heap-ი სჭირდებათ. სამ რამეზე ხალხი ყველაზე ხშირად ებჯინება: artefact-ის სტრუქტურა (Quarkus ნაგულისხმევად ერთ ფაილს არ აგენერირებს), მეხსიერების ლიმიტი (JVM სიამოვნებით მოითხოვს იმაზე მეტს, ვიდრე გადახდილი გაქვს) და health endpoint-ების მისამართები, რომლებიც Spring-ის მისამართებისგან განსხვავდება. ეს პოსტი სამივეს მოიცავს, ასევე კითხვას, რომელსაც ყველა პირველად სვამს: native executable-ი აჯობებს თუ უბრალოდ JVM-ზე გაუშვა?

ეს პოსტი განზრახ არის Spring Boot აპლიკაციის production-ზე გაშვების თანამგზავრი, სადაც fat jar-ის ზოგადი გზაა აღწერილი - Java-ს ვერსიები, გარედან მართვადი კონფიგურაცია, HikariCP, proxy header-ები. აქ მხოლოდ იმას განვიხილავთ, რაც Quarkus-სა და Micronaut-ში სხვაგვარადაა, და მხოლოდ იმას ვიმეორებთ, რაც გასაგებად გჭირდება.

რით განსხვავდება Spring Boot-ის fat jar-ისგან#

Spring Boot-ის build ერთ გასაშვებ ფაილს გაძლევს. Quarkus და Micronaut იმავეს სხვადასხვა ფორმით გაძლევენ, და სწორედ ფორმა წყვეტს, რას ტვირთავ და როგორია გაშვების ბრძანება.

Frameworkbuild-ის ნაგულისხმევი შედეგიგაშვება
Quarkustarget/quarkus-app/ დირექტორიაjava -jar target/quarkus-app/quarkus-run.jar
Quarkus (uber-jar)target/*-runner.jarjava -jar target/*-runner.jar
Micronaut (Gradle)build/libs/*-all.jarjava -jar build/libs/app-0.1-all.jar
Micronaut (Maven)target/*.jarjava -jar target/app-0.1.jar

Micronaut-ის ფაილების სახელები დამოკიდებულია პროექტის სახელსა და ვერსიაზე და იმაზე, რა plugin-ებია დაყენებული, ამიტომ ზემოთ მოცემული სახელები მაგალითად აღიქვი და build-ის შემდეგ დირექტორია თვითონ ნახე. Gradle-ის შემთხვევაში -all.jar Shadow plugin-იდან და shadowJar task-იდან მოდის; Maven-ის შემთხვევაში გენერირებული პროექტი გასაშვები არქივის შესაქმნელად Shade ან Micronaut Maven plugin-ს იყენებს.

ორივეს საერთო ის არის, რომ ისინი შენს მიერ არჩეულ port-ზე უსმენენ, კონფიგურაციას ფაილებიდან და გარემოს ცვლადებიდან კითხულობენ და მათ სჭირდებათ Java runtime იმ ვერსიისა მაინც, რომლითაც ისინი დაკომპილირდა. RE:NODE-ის Java ხაზი ნებისმიერ jar-ს უშვებს Java 17-ზე, 21-ზე ან 25-ზე, რაც ერთნაირად ფარავს Quarkus-ს, Micronaut-ს, Spring Boot-სა და Ktor-ს. Quarkus 3-ის მიმდინარე რელიზებს და Micronaut 4-ის რელიზებს Java 17 ან უფრო ახალი სჭირდებათ; არჩევამდე საკუთარი framework-ის ვერსია release notes-ში გადაამოწმე და დააკომპილირე --release ფლაგით, რომელიც იმ ყველაზე ძველ Java-ს ემთხვევა, რომელზეც გაშვებას აპირებ. 17-ისთვის აგებული jar 21-სა და 25-ზე მუშაობს, პირიქით - არასდროს.

Quarkus-ის შეფუთვა: fast-jar, uber-jar და დანარჩენი#

Quarkus-ის ნაგულისხმევს fast-jar ჰქვია. ეს ერთი ფაილი არ არის. build დირექტორიას წერს:

code
target/quarkus-app/  quarkus-run.jar  lib/    boot/    main/  app/  quarkus/

quarkus-run.jar პატარა launcher-ია. დამოკიდებულებები lib/-ში jar-ების სახით ინახება, შენი კლასები - app/-ში, build-ის დროს გენერირებული მონაცემები კი - quarkus/-ში. ამ სტრუქტურის მიზეზი ერთია: სტარტი უფრო სწრაფია, როცა runtime-ს ერთი უზარმაზარი არქივის გახსნა და სკანირება არ უწევს. ის დეპლოიმენტებსაც ამცირებს, რადგან დირექტორიის უმეტესი ნაწილი რელიზებს შორის არ იცვლება - იცვლება მხოლოდ app/ და quarkus/, ამიტომ ინკრემენტული ატვირთვა (ან layered container image) ორმოცდაათი მეგაბაიტის ნაცვლად რამდენიმე ასეულ კილობაიტს გადააქვს.

ჰოსტინგისთვის შედეგი მარტივია, მაგრამ ადვილი შესაცდენი: უნდა ატვირთო მთელი `quarkus-app` დირექტორია და არა მხოლოდ `quarkus-run.jar`. მარტო launcher კლასების ჩატვირთვის შეცდომით ვარდება, რადგან lib/ მის გვერდით არ არის. დირექტორია CI-ში დააარქივე, არქივი ატვირთე და ადგილზე ამოაარქივე. პანელის ფაილ მენეჯერი არქივებს იმავე ადგილას ამოარქივებს, სადაც ისინი დევს, ანუ ზუსტად იმას აკეთებს, რაც აქ გჭირდება.

bash
$ ./mvnw -B clean package$ tar -czf quarkus-app.tar.gz -C target quarkus-app$ ls -lh quarkus-app.tar.gz

შეფუთვის ტიპს ერთი property აკონტროლებს, რომელიც application.properties-ში ან ბრძანების ხაზზე -D-ით მიეთითება:

quarkus.package.jar.typeშედეგი
fast-jarზემოთ აღწერილი ნაგულისხმევი დირექტორიის სტრუქტურა
uber-jarერთი გასაშვები *-runner.jar, რომელშიც ყველაფერია
legacy-jarუფრო ძველი ერთი jar პლუს lib/, თავსებადობისთვის დატოვებული
mutable-jarfast-jar, რომელიც სტარტზე თავიდან შეიძლება დამუშავდეს, dev ინსტრუმენტებისთვის

Quarkus-ის ძველ რელიზებზე (3.9-მდე) property იყო quarkus.package.type და მნიშვნელობები იგივე სიტყვები იყო. თუ build შენს პარამეტრს უგულებელყოფს, ალბათ სხვა major ვერსიის დოკუმენტაციას კითხულობ.

application.properties
quarkus.package.jar.type=uber-jar

როდის არის uber-jar სწორი არჩევანი? როცა შენს დეპლოიმენტის გზას მართლა ერთი ფაილი სჭირდება - ერთი jar-ის ფაილ მენეჯერში გადმოთრევა ან გაშვების ბრძანება, რომლისთვისაც ხელის ახლებას არ აპირებ. ფასი უფრო ნელი სტარტია და უფრო დიდი ერთი არქივი, რომელიც ყოველ რელიზზე იცვლება. თუ tarball-ს გაუმკლავდები, fast-jar დატოვე. თუ არა, uber-jar კარგია და ბევრი პატარა სერვისი პრობლემის გარეშე სწორედ ასე მუშაობს.

Micronaut-ში ასეთი არჩევანი არ არსებობს. build გასაშვებ jar-ს გაძლევს, და -all.jar სუფიქსი (Gradle) გიჩვენებს, რომელი ფაილი უნდა ატვირთო. მისი სწრაფი სტარტი annotation processor-ში ahead-of-time დამუშავებიდან მოდის და არა არქივის სტრუქტურიდან.

Native image თუ JVM: რომელი გაუშვა#

ორივე framework-ს შეუძლია აპლიკაცია GraalVM-ით native executable-ად დააკომპილიროს. რიცხვები, რის გამოც ხალხს ეს უნდა, რეალურია: ათეულობით მილიწამში სტარტი და გაცილებით მცირე მეხსიერების მოხმარება. ხარჯებიც რეალურია და პატარა ჰოსტინგულ სერვისზე ისინი ჩვეულებრივ სარგებელს აღემატება.

JVM რეჟიმიNative executable
სტარტიდაახლოებით ერთი-ორი წამიათეულობით მილიწამი
მეხსიერება უქმ რეჟიმშიHeap პლუს JVM overheadშესამჩნევად ნაკლები
პიკური გამტარუნარიანობაგათბობის შემდეგ უფრო მაღალიხშირად უფრო დაბალი, JIT-ის გარეშე
build-ის დროწამებიდან ერთ წუთამდეწუთები
build-ის მეხსიერებაზომიერირამდენიმე გიგაბაიტი
Reflection, proxy-ებიუბრალოდ მუშაობსbuild-ის დროს უნდა იყოს ცნობილი
Debug, profilingJVM-ის სრული ინსტრუმენტებიშეზღუდული
პორტატულობანებისმიერი მანქანა Java-თიერთი OS და CPU არქიტექტურა

ბრძანებები, საცნობაროდ:

bash
# Quarkus: build a native executable (needs GraalVM or Mandrel installed)$ ./mvnw package -Dnative# Quarkus: build it inside a container, so you need no local GraalVM$ ./mvnw package -Dnative -Dquarkus.native.container-build=true# Micronaut, Gradle$ ./gradlew nativeCompile# Micronaut, Maven$ ./mvnw package -Dpackaging=native-image

Quarkus target/-ში *-runner binary-ს წერს. ის მხოლოდ იმ ოპერაციულ სისტემასა და CPU არქიტექტურაზე მუშაობს, რომელზეც აიგო, და სწორედ ამიტომ არსებობს container build-ის ვარიანტი: ის Linux binary-ს გაძლევს, მაშინაც კი, თუ სხვა სისტემაზე ვითარდები.

პატიოსანი რეკომენდაცია ჰოსტინგულ აპლიკაციის სერვერისთვის JVM რეჟიმია. სამი მიზეზი. პირველი, RE:NODE-ზე Java ხაზი jar-ის გაშვებაზეა აგებული Java 17-ზე, 21-ზე ან 25-ზე; native executable სხვა სახის ფაილია და Java ხაზის დოკუმენტაციაში მისი გაშვება არ წერია, ამიტომ მასზე გეგმის დამყარებამდე support-ს დაუკავშირდი. მეორე, native build წუთების განმავლობაში რამდენიმე გიგაბაიტ მეხსიერებას მოითხოვს, რაც იმ production გეგმაზე არ ეკუთვნის, რომლის პატარად დატოვებასაც ცდილობ - მისი ადგილი CI-შია. მესამე, JVM რეჟიმი გონივრული heap ფლაგებით უკვე საკმარისად პატარაა: ზომიერი dependency tree-ის მქონე Quarkus ან Micronaut სერვისი 1 GB გეგმაში ეტევა, რომელიც ყველაზე იაფი ტარიფია. Native მაშინ ღირს, როცა ნულამდე მასშტაბირდები და ხშირად სტარტავ (ფუნქციები, job-ები), როცა ძალიან ბევრ პატარა instance-ს უშვებ, ან როცა უფრო დიდ დეპლოიმენტში მეხსიერება შემზღუდველი ფაქტორია. ერთი გეგმის ერთი სერვისისთვის ეს ხარჯია სარგებლის გარეშე.

თუ მაინც native-ს აირჩევ, გახსოვდეს, რომ reflection, რესურსების ჩატვირთვა და დინამიური proxy-ები build-ის დროს უნდა გამოცხადდეს. ბიბლიოთეკებს, რომლებიც მათზე არიან დამოკიდებული, კონფიგურაცია ან extension სჭირდებათ. ორივე framework-ს გავრცელებულებისთვის extension-ები მოჰყვება, ხოლო გამოტოვებული რეგისტრაცია runtime-ზე ჩნდება როგორც class-not-found ან დაკარგული რესურსი, რომელიც JVM რეჟიმში უპრობლემოდ მუშაობდა - ამიტომ integration test-ები native build-ზეც გაუშვი და არა მხოლოდ JVM-ზე. Quarkus-ს ზუსტად ამისთვის აქვს @QuarkusIntegrationTest.

Port-ები და binding#

ნებისმიერ framework-ში პირველი, რაც ტყდება, listening port-ია, და ორივე აქ განხილულის ნაგულისხმევი არის 8080. შენს სერვერს გამოყოფილი port აქვს და აპლიკაცია მასზე, ყველა interface-ზე უნდა უსმენდეს. გარემოს ცვლადები და secret-ები გვიჩვენებს, სად დაყენდეს ისინი; გასაღებები ქვემოთ არის.

Quarkus - application.properties
quarkus.http.host=0.0.0.0quarkus.http.port=${SERVER_PORT:8080}
Micronaut - application.yml
micronaut:  server:    port: ${SERVER_PORT:8080}

${SERVER_PORT:8080} ნიშნავს „გამოიყენე გარემოს ცვლადი, თუ დაყენებულია, წინააღმდეგ შემთხვევაში 8080“, რაც შენს ლეპტოპზეც მუშაობს და production-შიც, ცვლილებების გარეშე. სანამ სახელ SERVER_PORT-ს დაეყრდნობი, Startup და Network ტაბებში ნახე, რა ცვლადს იძლევა შენი სერვერი; Pterodactyl-ზე დაფუძნებული პანელი გამოყოფილ port-ს ჩვეულებრივ ამ სახელით აჩვენებს, მაგრამ შენს სერვერზე გადაამოწმე.

ორივე framework გარემოს ცვლადებს კონფიგურაციის გასაღებებზეც ასახავს, კოდის გარეშე: გასაღები დიდ ასოებზე გადაიყვანე, წერტილები და ტირეები კი ქვედა ტირეებად აქციე.

გასაღებიგარემოს ცვლადი
quarkus.http.portQUARKUS_HTTP_PORT
quarkus.datasource.jdbc.urlQUARKUS_DATASOURCE_JDBC_URL
micronaut.server.portMICRONAUT_SERVER_PORT
datasources.default.urlDATASOURCES_DEFAULT_URL

Quarkus-ის ნაგულისხმევი bind მისამართი რეჟიმზეა დამოკიდებული: dev რეჟიმში ის loopback მისამართია, ჩვეულებრივ production გაშვებაში კი - ყველა interface. quarkus.http.host=0.0.0.0 პირდაპირ მითითება არაფერი ღირს და ეჭვს აქრობს. სერვისი, რომელიც თავისი container-ის შიგნით 127.0.0.1-ზეა მიბმული, არავისთვის არის ხელმისაწვდომი, და proxy bad gateway-ს აბრუნებს, კონსოლი კი ამტკიცებს, რომ აპლიკაცია გაშვებულია.

პროფილები ორივეში ერთნაირად მუშაობს. Quarkus-ში %prod. პრეფიქსი გასაღებს production პროფილზე ზღუდავს, და სწორედ ამ პროფილით ეშვება java -jar, თუ სხვა რამ არ ჩაწერე; დანარჩენი %dev. და %test. არის. Micronaut-ში გარემოებს MICRONAUT_ENVIRONMENTS=prod ასახელებს, რომელიც application.yml-ის თავზე application-prod.yml-ს ტვირთავს.

application.properties
quarkus.http.port=8080%prod.quarkus.http.port=${SERVER_PORT:8080}%prod.quarkus.log.level=INFO%dev.quarkus.log.level=DEBUG

Heap-ის ფლაგები მცირე მეხსიერების ლიმიტისთვის#

ეს ყველაზე მნიშვნელოვანი ნაწილია. container-ს მეხსიერების მკაცრი ლიმიტი აქვს, JVM კი თვითონ წყვეტს, რამხელა heap მოითხოვოს. დეტალური განხილვა Spring Boot-ის პოსტშია; აქ სამუშაო ვერსიაა.

RE:NODE-ზე, როცა პროცესი მეხსიერების ლიმიტს მიაღწევს, kernel container-ს აჩერებს და ის swap-ის ნაცვლად სუფთად ირესტარტება. ორიგინალი Pterodactyl პირიქით იქცევა. სერვისისთვის ეს უკეთესი ქცევაა, მაგრამ შეცდომის წაკითხვის წესს ცვლის. არსებობს ორი საკმაოდ განსხვავებული შემთხვევა:

  • `java.lang.OutOfMemoryError: Java heap space` კონსოლში. JVM-მა შეამჩნია, რომ heap დაუმთავრდა, და ეს თქვა. Heap მონაცემებისთვის ძალიან პატარაა, ან რაღაც leak-ს იწვევს.
  • Restart Java-ს შეცდომის გარეშე. მთელი პროცესი container-ის ლიმიტს გასცდა და გარედან გაჩერდა. Heap გეგმისთვის ძალიან დიდი იყო, ან heap-ის გარეთ მეხსიერება გაიზარდა.

მეორე უფრო გავრცელებულია, რადგან JVM-ის მოცულობა heap-ზე მეტია. ის ასევე ინახავს metaspace-ს კლასების მეტამონაცემებისთვის, thread stack-ებს, JIT code cache-ს, direct buffer-ებს და garbage collector-ის სტრუქტურებს. Quarkus Netty-სა და Vert.x-ს ამატებს, რომლებიც ქსელის I/O-სთვის direct (heap-ს გარეთ) buffer-ებს გამოყოფენ; ეს ნაწილობრივ ხსნის, რატომ არის Quarkus სერვისის რეალური ზომა მისი -Xmx-ზე დიდი. Netty-ზე მომუშავე Micronaut იგივეს აკეთებს.

Heap დააყენე container-ის ლიმიტის წილად, რომ გეგმის შეცვლისას ის თან გაჰყვეს:

bash
$ java -XX:MaxRAMPercentage=60.0 -XX:InitialRAMPercentage=25.0 \    -XX:+ExitOnOutOfMemoryError \    -jar target/quarkus-app/quarkus-run.jar

თუ გაშვების ბრძანების შეცვლა არ შეგიძლია, ფლაგები გარემოში ჩაწერე; ყოველი JVM, რომელიც ეშვება, JAVA_TOOL_OPTIONS-ს კითხულობს და კონსოლის თავში ბეჭდავს Picked up JAVA_TOOL_OPTIONS: ..., ასე ხედავ, რომ ამოქმედდა:

environment
JAVA_TOOL_OPTIONS=-XX:MaxRAMPercentage=60.0 -XX:+ExitOnOutOfMemoryError

ამას ნუ აურევ JAVA_OPTS-სა და JAVA_OPTS_APPEND-ში. ისინი Red Hat-ისა და Quarkus-ის container image-ების შიგნით არსებულ გაშვების სკრიპტებს აქვთ წასაკითხი და არა თვითონ java binary-ს, ამიტომ უბრალო java -jar გაშვებაზე ისინი არაფერს აკეთებენ.

საწყისი მნიშვნელობები გეგმის ზომის მიხედვით, რომლებიც მხოლოდ ვარაუდია და გრაფიკით უნდა შეიცვალოს:

გეგმის მეხსიერებაHeap-ის პარამეტრირატომ
1 GB-Xmx256m - -Xmx384mდაახლოებით ნახევარი non-heap-სა და OS-ს მიდის
2 GBMaxRAMPercentage=60დაახლოებით 1,2 GB heap, დანარჩენი overhead-ისთვის
4 GBMaxRAMPercentage=65Overhead უფრო მცირე წილია
6 GB და მეტიMaxRAMPercentage=70buffer-ებისა და პიკებისთვის ადგილი დატოვე

საკუთარი რიცხვის პოვნის გონივრული გზა: თავისუფალი მეხსიერების მქონე გეგმაზე განზრახ დიდი heap დააყენე, ერთი დღე რეალური ტრაფიკი გაატარე და კონსოლში მეხსიერების გრაფიკს ლიმიტთან ერთად უყურე. პატარა REST სერვისს ორივე framework-ზე ჩვეულებრივ 128-256 MB heap სჭირდება; სერვისს, რომელიც cache-ს იყენებს ან დიდ payload-ებს ამუშავებს, მეტი სჭირდება. Heap დააყენე იმაზე, რაც დააკვირდი, პლუს მარაგი და არა გეგმის რომელიმე მრგვალ პროცენტზე.

რამდენიმე ფლაგი პატარა გეგმებზე თავის ადგილს იმსახურებს:

ფლაგიეფექტი
-XX:+ExitOnOutOfMemoryErrorHeap OOM-ზე გამოდის, რომ პლატფორმამ სუფთა პროცესი გაუშვას
-XX:MaxMetaspaceSize=128mკლასების მეტამონაცემების ზრდას ზღუდავს
-XX:MaxDirectMemorySize=128mNetty-ის direct buffer-ებს ზღუდავს
-XX:+UseSerialGCყველაზე მცირე overhead-ის collector 1 CPU-სთვის ან პატარა heap-ისთვის
-Xss512kუფრო პატარა thread stack-ები, თუ ბევრ thread-ს უშვებ

-XX:+ExitOnOutOfMemoryError ის ფლაგია, რომელსაც ხალხი ყველაზე ხშირად ტოვებს. JVM, რომელმაც ერთ thread-ში heap OOM-ი ამოაგდო, ხშირად ცოცხალია, მაგრამ დაზიანებული: ზოგ მოთხოვნას ემსახურება, ზოგს ვერა. გამოსვლა სუფთა პროცესს გაძლევს. გაითვალისწინე, რომ restart-ებს თვალს ადევნებენ. Crash watcher ყოველ ორ წუთში ამოწმებს, ხომ არ დაბრუნდა uptime უკან ან სერვერი offline-ზე ხომ არ გადავიდა; შენ მიერ მოთხოვნილი restart-ები არ ითვლება, ერთ საათში სამი restart სერვერის გვერდზე გაფრთხილებას აჩენს და ავტომატურ ticket-ს ხსნის, ექვსი კი შეჩერებას ნიშნავს. out-of-memory მარყუჟში გაჭედილი სერვისი ზუსტად ის არის, რასაც ეს ამოწმებს, ამიტომ ზომები გაასწორე, სანამ მეთვალყურეობის გარეშე დატოვებ.

CPU-ც მნიშვნელოვანია. ის მკაცრად იზღუდება შენ მიერ შეძენილ წილამდე, და 100% ნელია, მაგრამ შეჩერების საფუძველი არასდროს არის. JVM thread pool-ებს იმ CPU-ების რაოდენობით ზომავს, რასაც ხედავს, რამაც ნახევარი ბირთვის წილი ერთამდე შეიძლება დაამრგვალოს. თუ სტარტი ნელია ან latency ყველაზე პატარა ტარიფზე ტალღებად მოდის, -XX:ActiveProcessorCount=1 საცდელად ღირს. CPU თუ RAM თამაშის სერვერებისთვის პოსტის ზოგადი გაკვეთილი უცვლელად მუშაობს: სანამ მეტს იყიდი, გაარკვიე, რომელი რესურსია შემზღუდველი.

Thread-ები და არა მხოლოდ heap

ორივე framework Netty-ზე მუშაობს და ორივეს შეუძლია blocking კოდი worker pool-ზე გაუშვას. Quarkus ამ pool-ის ზომას ნაგულისხმევად აღმოჩენილი CPU-ების რაოდენობით ადგენს, და მის შესაზღუდად გასაღები არის quarkus.thread-pool.max-threads. თითოეული thread-ი stack-ს და გარკვეულ native მეხსიერებას ხარჯავს, ამიტომ 200 worker-იანი pool სერვისზე, რომელსაც ერთდროულად 10 მოთხოვნაზე მეტი არასდროს აქვს, უბრალოდ მოცულობას ზრდის. Micronaut-ში შესატყვისი პარამეტრები micronaut.executors-ის ქვეშაა, მაგალითად micronaut.executors.io.type: fixed და micronaut.executors.io.nThreads: 16. დააყენე იმდენი, რამდენსაც სერვისი მართლა იყენებს.

მონაცემთა ბაზის კავშირები#

კავშირის დეტალები იმ ბაზიდან მოდის, რომელსაც მიაერთებ. აპლიკაციის სერვერებს პანელში შექმნილი ბაზის slot-ები აქვთ, გენერირებული host-ით, მომხმარებლითა და password-ით; დამოუკიდებელი მონაცემთა ბაზის ჰოსტინგის გეგმა PostgreSQL-ს ან MySQL 8.4 LTS-ს იძლევა, თითო სერვერზე გენერირებული credentials-ით. მნიშვნელობები გარემოს ცვლადებში შეინახე და არასდროს jar-ის შიგნით.

environment - Quarkus
QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://db-host:5432/shopQUARKUS_DATASOURCE_USERNAME=shop_appQUARKUS_DATASOURCE_PASSWORD=change-me
application.properties
quarkus.datasource.db-kind=postgresqlquarkus.datasource.jdbc.max-size=8quarkus.datasource.jdbc.min-size=2

Quarkus pool-ად Agroal-ს იყენებს, და quarkus.datasource.jdbc.max-size ზედა ზღვარია. Micronaut-ისთვის datasource datasources.default-ის ქვეშ კონფიგურირდება, ნაგულისხმევი pool კი HikariCP-ა:

Micronaut - application.yml
datasources:  default:    url: ${DATASOURCES_DEFAULT_URL}    username: ${DATASOURCES_DEFAULT_USERNAME}    password: ${DATASOURCES_DEFAULT_PASSWORD}    maximum-pool-size: 8

რომელი pool-იც არ უნდა იყოს, არითმეტიკა იგივეა: ყოველ კავშირს ბაზა ინახავს, ამიტომ ორი instance რვა კავშირის pool-ით, პლუს migration job, ბაზის საკუთარ ლიმიტს ემატება. Connection pool-ები და ლიმიტები ამას დეტალურად განიხილავს. ერთ-ორ ბირთვზე მომუშავე სერვისისთვის ექვსიდან რვამდე pool სავსებით საკმარისია.

Health endpoint-ები Quarkus-სა და Micronaut-ში#

არც ერთი framework Spring-ის /actuator/health-ს არ იყენებს. ორივე health endpoint-ს მხოლოდ dependency-ის დამატების შემდეგ გაძლევს, და მისამართები განსხვავდება.

Quarkus: SmallRye Health

დაამატე extension:

bash
$ ./mvnw quarkus:add-extension -Dextensions="smallrye-health"

ეს ოთხ endpoint-ს გაძლევს /q პრეფიქსის ქვეშ:

მისამართირა კითხვაზე პასუხობს
/q/healthყველაფერი ერთად: live, ready და started
/q/health/liveპროცესი ისეთ მდგომარეობაშია, რომ გაგრძელება შეუძლია?
/q/health/readyუნდა მიიღოს თუ არა ამ instance-მა ტრაფიკი?
/q/health/startedსტარტი დასრულდა?

პასუხი არის {"status":"UP","checks":[...]} HTTP 200-ით, ან DOWN 503-ით. Extension-ები თავადაც ამატებენ შემოწმებებს - მაგალითად, datasource extension ბაზის readiness შემოწმებას ამატებს, ამიტომ ბაზის არარსებობა /q/health/ready-ს წითლად აქცევს, შენგან რაიმე დაწერის გარეშე. შენი შემოწმებები CDI bean-ებია:

java
@ApplicationScoped@Readinesspublic class CacheWarmCheck implements HealthCheck {    @Override    public HealthCheckResponse call() {        return HealthCheckResponse.up("cache-warm");    }}

/q პრეფიქსი Quarkus-ის non-application root-ია და quarkus.http.non-application-root-path-ით კონფიგურირდება. თუ გადაიტანე, განაახლე ყველაფერი, რაც სერვისს აკონტროლებს.

Micronaut: management მოდული

დაამატე micronaut-management build-ში. Endpoint-ები კონფიგურაციით ირთვება:

Micronaut - application.yml
endpoints:  health:    enabled: true    details-visible: NEVER

საბაზო მისამართია /health, მის ქვეშ კი /health/liveness და /health/readiness. details-visible იღებს NEVER, AUTHENTICATED ან ANONYMOUS მნიშვნელობას და წყვეტს, ვის უჩვენოს თითოეული ინდიკატორის დეტალები. საჯარო მისამართზე დატოვე NEVER: დეტალებმა შეიძლება hostname-ები და ვერსიები გაამხილოს. მართვის endpoint-ები საჯარო port-იდან მთლიანად რომ გააქრო, endpoints.all.port მათ ცალკე port-ზე გადაიტანს.

როგორია კარგი შემოწმება

Liveness არასდროს უნდა იყოს დამოკიდებული ბაზაზე ან სხვა სერვისზე. თუ დამოკიდებულია, ბაზის გაუმართაობა პლატფორმას სავსებით ჯანსაღი პროცესის restart-ს აიძულებს, რაც ვითარებას აუარესებს. Readiness არის ადგილი, სადაც დამოკიდებულებები ეკუთვნის. ყოველი შემოწმება იაფი იყოს: ინდიკატორი, რომელიც სამ მესამე მხარის API-ს ეძახის, თითოეულ monitor ping-ს დატვირთვად აქცევს. Graceful shutdown და health შემოწმებები ამას უფრო დეტალურად განიხილავს, ისტორიის shutdown ნაწილთან ერთად. Quarkus-ში quarkus.shutdown.timeout ადგენს, რამდენ ხანს დაელოდოს ის მიმდინარე მოთხოვნების დასრულებას, როცა Restart-ს დააჭერ.

შეამოწმე კონსოლიდან ან შენი მანქანიდან:

bash
$ curl -s -o /dev/null -w "%{http_code}\n" https://api.example.com/q/health/ready200

და საჯარო მისამართზე health-ზე მეტი ნუ გამოაჩენ. Metrics, Quarkus dev UI და Micronaut-ის /env და /beans endpoint-ები კონფიგურაციას აჩვენებს. Quarkus dev UI მხოლოდ dev რეჟიმში მუშაობს, მაგრამ ორჯერ გადაამოწმე, რომ application.properties-ში დამატებული არაფერი რთავს მართვის მისამართებს production-ში.

Proxy-ს უკან: TLS და კლიენტის რეალური მისამართები#

TLS აპლიკაციის შიგნით ნუ დაასრულებ. აპლიკაციის გეგმებს reverse-proxy slot აქვთ: A record მიუთითე ნაჩვენებ მისამართზე და სერტიფიკატი ავტომატურად გაიცემა და განახლდება, 21-დღიან ფანჯარაში. შენი აპლიკაცია მის გამოყოფილ port-ზე უბრალო HTTP-ს ემსახურება, კლიენტის რეალური მისამართი კი X-Forwarded-For-ში მოდის.

არც ერთი framework ნაგულისხმევად არ ენდობა forwarded header-ებს, საფუძვლიანი მიზეზით: ნებისმიერ კლიენტს შეუძლია მათი გაგზავნა. პარამეტრის გარეშე კლიენტად proxy-ის მისამართს დაინახავ, მოთხოვნიდან აგებული redirect-ები კი http://-ით გამოვა.

Quarkus - application.properties
quarkus.http.proxy.proxy-address-forwarding=truequarkus.http.proxy.allow-x-forwarded=truequarkus.http.proxy.enable-forwarded-host=true

Micronaut-ისთვის გასაღები, რომელიც კლიენტის მისამართის მატარებელ header-ს ასახელებს, არის micronaut.server.client-address-header:

Micronaut - application.yml
micronaut:  server:    client-address-header: X-Forwarded-For

ისინი მხოლოდ მაშინ ჩართე, როცა მართლა proxy-ს უკან ხარ, რადგან ნებისმიერს, ვისაც აპლიკაციის port-ზე პირდაპირ მიწვდომა აქვს, შეეძლება თავისი მისამართის გაყალბება. დაყენების შემდეგ გააგზავნე მოთხოვნა და დადგენილი კლიენტის მისამართი debug endpoint-იდან დაბეჭდე და ვარაუდს ნუ დაჯერდები. რას აკეთებს reverse proxy მთელ ჯაჭვს ხსნის.

RE:NODE-ზე დეპლოი და გაშვება#

დეპლოის გზა ნებისმიერი jar-ისთვის ერთნაირია. build CI-ში ან ლოკალურად გააკეთე და ტესტები იქვე გაუშვი. Quarkus-ის ან Micronaut-ის build native-ზე მსუბუქია, მაგრამ მაინც მეტ მეხსიერებას მოითხოვს, ვიდრე პატარა production გეგმამ უნდა დახარჯოს, ხოლო იქ build, სადაც ტესტავ, artefact-ს რეპროდუცირებადს ხდის.

  1. ააგე artefact: Quarkus-ისთვის ./mvnw package (შემდეგ target/quarkus-app დააარქივე), Micronaut-ისთვის კი ./gradlew shadowJar.
  2. ატვირთე ფაილ მენეჯერით ან SFTP-ით. ორივე ყველა აპლიკაციის სერვერზეა, SFTP credentials-ით თითო სერვერზე. ფაილ მენეჯერი არქივებს ადგილზე ამოარქივებს, რაც Quarkus-ის დირექტორიას უხდება.
  3. Startup ტაბზე დააყენე გარემოს ცვლადები: port, datasource-ის მნიშვნელობები, JAVA_TOOL_OPTIONS და პროფილი (QUARKUS_PROFILE=prod ან MICRONAUT_ENVIRONMENTS=prod).
  4. გაშვების პარამეტრები სწორ ფაილზე მიუთითე, შემდეგ კონსოლიდან გაუშვი და framework-ის started ხაზს დააკვირდი.

ალტერნატივაა GitHub-იდან დეპლოი. პანელი ამას GitHub App-ით აკეთებს, მოკლევადიანი token-ებით, ამიტომ private repository-ებიც მუშაობს, ორი გადამრთველით: pull ყოველ სტარტზე და deploy push-ზე, რომელიც გაშვებულ სერვერს რესტარტავს და გაჩერებულს უშვებს. ეს მხოლოდ GitHub-ზეა. ეს წყაროდან სერვერამდე მიმავალი გზაა, ამიტომ ის მხოლოდ მაშინ გამოგადგება, თუ სერვერი build-ის შედეგს შეინახავს; პრაქტიკული ხერხია branch, რომელიც მხოლოდ რელიზის artefact-ს შეიცავს. მიბმამდე pre-flight შემოწმება repository-ს კითხულობს და ჩამოთვლის, რა გაფუჭდება, ყველაზე სერიოზულით დაწყებული, მაგალითად არასწორი port-ის ან host-ის მიბმა.

ახალი Java სერვერი თავის მისამართზე პატარა სამუშაო გვერდით იწყებს, სანამ კოდს დაადეპლოებ, და ეს გვერდი შენს ფაილებს არასდროს გადააწერს.

ატვირთვაjava -jarHTTPSHTTPCI buildmvn ან gradleკლიენტებიArtefactquarkus-app ან -all.jarProxy slotTLS, X-Forwarded-ForJVMheap-ის ფლაგები, portHealth/q/health ან /health
build-იდან გაშვებულ Quarkus ან Micronaut სერვისამდე

პრობლემების მოგვარება#

`Error: Unable to access jarfile quarkus-run.jar` ან დაკარგული `lib` შეცდომა. ატვირთე launcher quarkus-app დირექტორიის დანარჩენის გარეშე. ატვირთე და ამოარქივე მთელი დირექტორია, ან uber-jar-ზე გადადი.

`no main manifest attribute, in app.jar`. ატვირთე thin jar. Micronaut-ისთვის ეს -all.jar-ის ნაცვლად უბრალო jar-ია; Quarkus-ისთვის - target/-ში არასწორი ფაილი. მოძებნე გასაშვები.

`UnsupportedClassVersionError`. jar უფრო ახალი Java-სთვის დაკომპილირდა, ვიდრე სერვერზეა. დააკომპილირე --release 17-ით, ან სერვერისთვის Java 21 ან 25 აირჩიე.

სერვერი stack trace-ის გარეშე ირესტარტება. პროცესი container-ის ლიმიტს გასცდა. შეამცირე MaxRAMPercentage, direct მეხსიერება -XX:MaxDirectMemorySize-ით შეზღუდე და მეხსიერების გრაფიკი ლიმიტს შეადარე. გახსოვდეს, რომ ერთ საათში სამი restart გაფრთხილებას იწვევს.

`java.lang.OutOfMemoryError: Java heap space`. Heap ძალიან პატარაა, ან რაღაც გროვდება. გაზარდე, თუ გეგმაში ადგილია, ან dump გადაიღე -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/container/dump.hprof-ით და მერე ფაილი წაშალე, რადგან ის heap-ის ზომისაა.

`java.lang.OutOfMemoryError: Direct buffer memory`. Netty-ს heap-ს გარეთ ადგილი გაუთავდა, ჩვეულებრივ დიდი მოთხოვნების ან პასუხების ნაკადის დროს. გაზარდე -XX:MaxDirectMemorySize, თუ გეგმაში ადგილია, და მოძებნე კოდი, რომელიც buffer-ებს ინახავს.

Port უკვე გამოიყენება, ან bad gateway მაშინ, როცა აპლიკაცია გაშვებულია. აპლიკაცია გამოყოფილი port-ის ნაცვლად 8080-ზე უსმენს, ან loopback-ზე. Port გარემოდან დააყენე და quarkus.http.host=0.0.0.0.

`/q/health` 404-ს აბრუნებს. smallrye-health extension build-ში არ არის, ან quarkus.http.non-application-root-path გადაიტანა.

Micronaut-ზე `/health` 404-ს აბრუნებს. micronaut-management classpath-ში არ არის, ან endpoints.health.enabled false-ია.

Native image ლოკალურად მუშაობს, მაგრამ სერვერზე ვარდება. ის სხვა ოპერაციული სისტემისთვის ან CPU არქიტექტურისთვის აიგო. ააგე CI-ში Linux container-ში, ან JVM რეჟიმს დაუბრუნდი.

FAQ#

fast-jar უკეთესია uber-jar-ზე?

სტარტისა და ინკრემენტული დეპლოებისთვის - კი. დეპლოის გზისთვის, რომელსაც ერთი ფაილი სჭირდება - არა. Fast-jar ნაგულისხმევია საფუძვლიანი მიზეზით, მაგრამ uber-jar პატარა სერვისზე არც შეცდომაა: დააყენე quarkus.package.jar.type=uber-jar და ერთი ფაილი ატვირთე.

უნდა გამოვიყენო native image პატარა გეგმაზე?

ჩვეულებრივ არა. build-ს რამდენიმე გიგაბაიტი მეხსიერება და წუთები CPU სჭირდება, Java ხაზი დოკუმენტაციაში jar-ების გასაშვებადაა აღწერილი, JVM რეჟიმი კი მორგებული heap-ით უკვე ყველაზე იაფ ტარიფში ეტევა. Native ღირს ნულამდე მასშტაბირებისა და ძალიან ბევრი პატარა instance-ისთვის და არა ერთი სერვისისთვის.

რამდენი მეხსიერება სჭირდება Quarkus ან Micronaut სერვისს?

ბაზიან პატარა REST სერვისს ჩვეულებრივ 128-256 MB heap-ი და 1 GB-ზე გაცილებით ნაკლები პროცესი სჭირდება, ამიტომ 1 GB ტარიფი სამუშაოა. გაზომე კონსოლის მეხსიერების გრაფიკით რეალური ტრაფიკის ერთი დღის განმავლობაში და არ დაეყრდნო არც ერთ ემპირიულ წესს, ამ პოსტისაც ჩათვლით.

მჭირდება -Xmx თუ MaxRAMPercentage?

გამოიყენე MaxRAMPercentage, თუ გეგმის შეცვლა შეიძლება, რადგან ის ლიმიტს მიჰყვება. გამოიყენე -Xmx ფიქსირებული, გაზომილი მნიშვნელობისთვის. თუ ორივეა დაყენებული, -Xmx იმარჯვებს.

რატომ პასუხობს ჩემი health endpoint ერთ port-ზე და მეორეზე არა?

მართვის interface შეიძლება ცალკე port-ზე იყოს. Micronaut-ში ამას endpoints.all.port აკეთებს, Quarkus-საც კონფიგურაციისას შეუძლია მართვა ცალკე interface-ზე გასცეს. გეგმას მხოლოდ ის port-ები აქვს, რაც ჩამოთვლილია, ამიტომ რამის გადატანამდე Network ტაბი ნახე.

შემიძლია Ktor ან Spring Boot ასევე გავუშვა?

კი. ნებისმიერი jar Java ხაზზე მუშაობს და heap ფლაგები იდენტურია. Spring Boot-ის სპეციფიკა Spring Boot-ის დეპლოის გზამკვლევშია.

გამოგადგათ?