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 იმავეს სხვადასხვა ფორმით გაძლევენ, და სწორედ ფორმა წყვეტს, რას ტვირთავ და როგორია გაშვების ბრძანება.
| Framework | build-ის ნაგულისხმევი შედეგი | გაშვება |
|---|---|---|
| Quarkus | target/quarkus-app/ დირექტორია | java -jar target/quarkus-app/quarkus-run.jar |
| Quarkus (uber-jar) | target/*-runner.jar | java -jar target/*-runner.jar |
| Micronaut (Gradle) | build/libs/*-all.jar | java -jar build/libs/app-0.1-all.jar |
| Micronaut (Maven) | target/*.jar | java -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 დირექტორიას წერს:
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-ში დააარქივე, არქივი ატვირთე და ადგილზე ამოაარქივე. პანელის ფაილ მენეჯერი არქივებს იმავე ადგილას ამოარქივებს, სადაც ისინი დევს, ანუ ზუსტად იმას აკეთებს, რაც აქ გჭირდება.
$ ./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-jar | fast-jar, რომელიც სტარტზე თავიდან შეიძლება დამუშავდეს, dev ინსტრუმენტებისთვის |
Quarkus-ის ძველ რელიზებზე (3.9-მდე) property იყო quarkus.package.type და მნიშვნელობები იგივე სიტყვები იყო. თუ build შენს პარამეტრს უგულებელყოფს, ალბათ სხვა major ვერსიის დოკუმენტაციას კითხულობ.
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, profiling | JVM-ის სრული ინსტრუმენტები | შეზღუდული |
| პორტატულობა | ნებისმიერი მანქანა Java-თი | ერთი OS და CPU არქიტექტურა |
ბრძანებები, საცნობაროდ:
# 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-imageQuarkus 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.http.host=0.0.0.0quarkus.http.port=${SERVER_PORT:8080}micronaut: server: port: ${SERVER_PORT:8080}${SERVER_PORT:8080} ნიშნავს „გამოიყენე გარემოს ცვლადი, თუ დაყენებულია, წინააღმდეგ შემთხვევაში 8080“, რაც შენს ლეპტოპზეც მუშაობს და production-შიც, ცვლილებების გარეშე. სანამ სახელ SERVER_PORT-ს დაეყრდნობი, Startup და Network ტაბებში ნახე, რა ცვლადს იძლევა შენი სერვერი; Pterodactyl-ზე დაფუძნებული პანელი გამოყოფილ port-ს ჩვეულებრივ ამ სახელით აჩვენებს, მაგრამ შენს სერვერზე გადაამოწმე.
ორივე framework გარემოს ცვლადებს კონფიგურაციის გასაღებებზეც ასახავს, კოდის გარეშე: გასაღები დიდ ასოებზე გადაიყვანე, წერტილები და ტირეები კი ქვედა ტირეებად აქციე.
| გასაღები | გარემოს ცვლადი |
|---|---|
quarkus.http.port | QUARKUS_HTTP_PORT |
quarkus.datasource.jdbc.url | QUARKUS_DATASOURCE_JDBC_URL |
micronaut.server.port | MICRONAUT_SERVER_PORT |
datasources.default.url | DATASOURCES_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-ს ტვირთავს.
quarkus.http.port=8080%prod.quarkus.http.port=${SERVER_PORT:8080}%prod.quarkus.log.level=INFO%dev.quarkus.log.level=DEBUGHeap-ის ფლაგები მცირე მეხსიერების ლიმიტისთვის#
ეს ყველაზე მნიშვნელოვანი ნაწილია. 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-ის ლიმიტის წილად, რომ გეგმის შეცვლისას ის თან გაჰყვეს:
$ 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: ..., ასე ხედავ, რომ ამოქმედდა:
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 GB | MaxRAMPercentage=60 | დაახლოებით 1,2 GB heap, დანარჩენი overhead-ისთვის |
| 4 GB | MaxRAMPercentage=65 | Overhead უფრო მცირე წილია |
| 6 GB და მეტი | MaxRAMPercentage=70 | buffer-ებისა და პიკებისთვის ადგილი დატოვე |
საკუთარი რიცხვის პოვნის გონივრული გზა: თავისუფალი მეხსიერების მქონე გეგმაზე განზრახ დიდი heap დააყენე, ერთი დღე რეალური ტრაფიკი გაატარე და კონსოლში მეხსიერების გრაფიკს ლიმიტთან ერთად უყურე. პატარა REST სერვისს ორივე framework-ზე ჩვეულებრივ 128-256 MB heap სჭირდება; სერვისს, რომელიც cache-ს იყენებს ან დიდ payload-ებს ამუშავებს, მეტი სჭირდება. Heap დააყენე იმაზე, რაც დააკვირდი, პლუს მარაგი და არა გეგმის რომელიმე მრგვალ პროცენტზე.
რამდენიმე ფლაგი პატარა გეგმებზე თავის ადგილს იმსახურებს:
| ფლაგი | ეფექტი |
|---|---|
-XX:+ExitOnOutOfMemoryError | Heap OOM-ზე გამოდის, რომ პლატფორმამ სუფთა პროცესი გაუშვას |
-XX:MaxMetaspaceSize=128m | კლასების მეტამონაცემების ზრდას ზღუდავს |
-XX:MaxDirectMemorySize=128m | Netty-ის 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-ის შიგნით.
QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://db-host:5432/shopQUARKUS_DATASOURCE_USERNAME=shop_appQUARKUS_DATASOURCE_PASSWORD=change-mequarkus.datasource.db-kind=postgresqlquarkus.datasource.jdbc.max-size=8quarkus.datasource.jdbc.min-size=2Quarkus pool-ად Agroal-ს იყენებს, და quarkus.datasource.jdbc.max-size ზედა ზღვარია. Micronaut-ისთვის datasource datasources.default-ის ქვეშ კონფიგურირდება, ნაგულისხმევი pool კი HikariCP-ა:
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:
$ ./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-ებია:
@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-ები კონფიგურაციით ირთვება:
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-ს დააჭერ.
შეამოწმე კონსოლიდან ან შენი მანქანიდან:
$ 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.http.proxy.proxy-address-forwarding=truequarkus.http.proxy.allow-x-forwarded=truequarkus.http.proxy.enable-forwarded-host=trueMicronaut-ისთვის გასაღები, რომელიც კლიენტის მისამართის მატარებელ header-ს ასახელებს, არის micronaut.server.client-address-header:
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-ს რეპროდუცირებადს ხდის.
- ააგე artefact: Quarkus-ისთვის
./mvnw package(შემდეგtarget/quarkus-appდააარქივე), Micronaut-ისთვის კი./gradlew shadowJar. - ატვირთე ფაილ მენეჯერით ან SFTP-ით. ორივე ყველა აპლიკაციის სერვერზეა, SFTP credentials-ით თითო სერვერზე. ფაილ მენეჯერი არქივებს ადგილზე ამოარქივებს, რაც Quarkus-ის დირექტორიას უხდება.
- Startup ტაბზე დააყენე გარემოს ცვლადები: port, datasource-ის მნიშვნელობები,
JAVA_TOOL_OPTIONSდა პროფილი (QUARKUS_PROFILE=prodანMICRONAUT_ENVIRONMENTS=prod). - გაშვების პარამეტრები სწორ ფაილზე მიუთითე, შემდეგ კონსოლიდან გაუშვი და framework-ის started ხაზს დააკვირდი.
ალტერნატივაა GitHub-იდან დეპლოი. პანელი ამას GitHub App-ით აკეთებს, მოკლევადიანი token-ებით, ამიტომ private repository-ებიც მუშაობს, ორი გადამრთველით: pull ყოველ სტარტზე და deploy push-ზე, რომელიც გაშვებულ სერვერს რესტარტავს და გაჩერებულს უშვებს. ეს მხოლოდ GitHub-ზეა. ეს წყაროდან სერვერამდე მიმავალი გზაა, ამიტომ ის მხოლოდ მაშინ გამოგადგება, თუ სერვერი build-ის შედეგს შეინახავს; პრაქტიკული ხერხია branch, რომელიც მხოლოდ რელიზის artefact-ს შეიცავს. მიბმამდე pre-flight შემოწმება repository-ს კითხულობს და ჩამოთვლის, რა გაფუჭდება, ყველაზე სერიოზულით დაწყებული, მაგალითად არასწორი port-ის ან host-ის მიბმა.
ახალი Java სერვერი თავის მისამართზე პატარა სამუშაო გვერდით იწყებს, სანამ კოდს დაადეპლოებ, და ეს გვერდი შენს ფაილებს არასდროს გადააწერს.
პრობლემების მოგვარება#
`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-ის დეპლოის გზამკვლევშია.


