RE:NODE

Java აპლიკაციის deploy: Spring Boot ან უბრალო jar Java სერვერზე

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


Java აპლიკაცია deploy-დება ერთ გასაშვებ jar-ად. სერვერს შენგან ოთხი რამ სჭირდება: Java-ს ვერსია, რომელიც სულ მცირე იმდენად ახალია, რამდენადაც ის, რომლისთვისაც დააკომპილირე; jar, რომელსაც თავისი დამოკიდებულებები თან აქვს; კოდი, რომელიც სერვერისგან მიღებულ პორტზე უსმენს; და მეხსიერების მოხმარება, რომელიც გეგმის ლიმიტს ჯდება და ცოტა სივრცეც რჩება. დანარჩენი - გარემოს ცვლადები, დომენი, ლოგები, GitHub-იდან deploy-ები - ამ ოთხს ეყრდნობა. თუ ესენი სწორია, java -jar app.jar საკმარისია. თუ ერთი მაინც არასწორია, ნახავ შეცდომას, რომელიც იშვიათად ასახელებს ნამდვილ მიზეზს.

ეს სტატია გადის მთელ გზას ჰოსტინგის Java სერვერზე: რომელი Java-ს ვერსია აირჩიო, როგორ ააწყო fat jar Maven-ით ან Gradle-ით Spring Boot-ისთვის და სხვა ყველაფრისთვის, როგორ აღწევს პორტი შენს კოდს, როგორ უკავშირდება JVM-ის heap-ი კონტეინერის ლიმიტს, როგორ გადასცე კონფიგურაცია, როგორ მიიტანო build-ები GitHub-იდან სერვერზე იქ დაკომპილირების გარეშე და როგორ წაიკითხო და შეინახო ლოგები. სადაც RE:NODE-ის Java ხაზი რამეს განსაკუთრებულად აკეთებს, ამას ვამბობთ; დანარჩენი ნებისმიერ ჰოსტზე მუშაობს, რომელიც jar-ს კონტეინერში უშვებს. Spring-ის production-დეტალებისთვის - HikariCP, Actuator, proxy header-ები - არის მეორე სტატია: Spring Boot-ის deploy production-ში.

როგორ უშვებს Java სერვერი შენს jar-ს#

ჰოსტინგის Java სერვერი არის კონტეინერი, რომელშიც JDK დევს და გაშვების ბრძანება ფიქსირებულია. RE:NODE-ის Java ხაზზე ეს ბრძანება სრულად ასეთია:

start command
java -XX:MaxRAMPercentage=80.0 -Dterminal.jline=false -Dterminal.ansi=true \  -Dserver.port=${SERVER_PORT} -jar {{JARFILE}}

აქედან სამი რამ გამომდინარეობს და ისინი ქვემოთ ყველა განყოფილებას განსაზღვრავს.

პირველი, build-ის ეტაპი არ არსებობს. სერვერი უშვებს jar-ს; Maven-ს ან Gradle-ს ის არ უშვებს. აწყობ შენს მანქანაზე ან CI-ში და სერვერზე მხოლოდ მზა არტეფაქტი მიდის.

მეორე, jar-ის სახელს ცვლადი JARFILE განსაზღვრავს, რომლის ნაგულისხმევი მნიშვნელობაა app.jar და სერვერის საქაღალდეს მიემართება. შეიძლება ქვესაქაღალდეც შეიცავდეს, ამიტომ build/app.jar ან release/service.jar მისაღებია, სანამ სახელი .jar-ით მთავრდება. ცვლი Startup ჩანართში.

მესამე, heap-ს და პორტს ბრძანება წყვეტს და არა შენ. heap-ის ზღვარი გეგმის მეხსიერების 80%-ია, პორტი კი system property-ად server.port გადაეცემა. Spring Boot ამ property-ს თავისით კითხულობს; სხვა framework-ებს კოდის ერთი ხაზი სჭირდება, რაც პორტის განყოფილებაშია.

თვითონ გაშვების ბრძანებას კლიენტი ვერ ცვლის - მხოლოდ მის ცვლადებს. ეს განზრახ არის: სერვერის გაშვება პროგნოზირებადი რჩება. ამასთან, ბრძანების ხაზზე -Xmx-ის ან -XX:+ExitOnOutOfMemoryError-ის დამატების ჩვეულებრივი ხრიკები მიუწვდომელია, რაც პატარა გეგმებისთვის მნიშვნელოვანია და მეხსიერების განყოფილებაში განვიხილავთ.

Java სერვერი პირველად შექმნისას ცარიელი არ არის: მას თავის მისამართზე პატარა სამუშაო გვერდი აქვს, აწყობილი app.jar-ად, რომ პირველი გაშვებისას ჩანდეს რაღაც სასარგებლო და არა crash. ეს გვერდი უბრალოდ jar-ია ნაგულისხმევი სახელით. ატვირთე ან deploy-ე შენი app.jar და ის გვერდს ჩაანაცვლებს; შენი არაფერი არასდროს გადაიწერება.

პანელი ამთავრებს გაშვებას მაშინ, როცა კონსოლის სტრიქონი შეიცავს რამდენიმე სიტყვიდან ერთ-ერთს: Started, Listening, Ready ან Server running (ნებისმიერი რეგისტრით). Spring Boot-ის Started Application in 4.2 seconds ემთხვევა. უბრალო jar, რომელიც გაშვებისას არაფერს ბეჭდავს, პანელში "Starting"-ზე დარჩება, თუმცა ნორმალურად მუშაობს. დაბეჭდე სტრიქონი, მაგალითად Listening on 0.0.0.0:<port>, როგორც კი სერვერი კავშირებს იღებს, და სტატუსი სწორი იქნება.

Java 17, 21 თუ 25#

Java ხაზზე სამი image არის: Java 17, Java 21 და Java 25, სამივე გრძელვადიანი მხარდაჭერის (LTS) რელიზია. ირჩევ Startup ჩანართის Docker Image სიაში, ან GitHub ჩანართის runtime ვერსიის პარამეტრით, თუ სერვერი რეპოზიტორიიდან deploy-დება - იქ შეცვლა სერვერს ახალ image-ზე თავიდან აწყობს.

წესი მარტივია: runtime უნდა იყოს შენი compile target-ის იგივე major ვერსიის ან უფრო ახალი. 17-ისთვის დაკომპილირებული jar 21-ზე და 25-ზე უცვლელად მუშაობს. 21-ისთვის დაკომპილირებული jar 17-ზე არ გაეშვება UnsupportedClassVersionError-ით, ამასთან შეცდომა გვაძლევს class file-ის ვერსიას და არა Java-ს ვერსიას:

Class file-ის ვერსიაJava-ს რელიზიტიპური მიზეზი
61.0Java 17აწყობილია --release 17-ით
65.0Java 21აწყობილია JDK 21-ზე release flag-ის გარეშე
69.0Java 25აწყობილია JDK 25-ზე release flag-ის გარეშე

target-ი დააფიქსირე build-ში და ნუ ენდობი იმას, რაც build-ის მანქანაზე შემთხვევით JDK-ადაა დაყენებული. Maven-ში ეს არის <maven.compiler.release>21</maven.compiler.release> <properties>-ში; Gradle-ში - toolchain:

build.gradle.kts
java {    toolchain {        languageVersion = JavaLanguageVersion.of(21)    }}

რომელი აირჩიო: Spring Boot 3-ს 17 ან უფრო ახალი სჭირდება, ასევე Quarkus-ისა და Micronaut-ის მიმდინარე რელიზებს. თუ დამოკიდებულებები ახალია, 21 გონივრული ნაგულისხმევია - virtual thread-ები იქ სტაბილურია და მხარდაჭერა წლებით რჩება. 25 აიღე, თუ უახლესი LTS გინდა და შენი ბიბლიოთეკები დაწეწილი არ არის; ჯერ ტესტები გაუშვი მასზე, რადგან ახალ რელიზზე ყველაზე ხშირად agent-ებზე დაფუძნებული ხელსაწყოები და ძველი bytecode-ის ბიბლიოთეკები ტყდება. 17 დატოვე კოდისთვის, რომლის გადატანაც არ შეგიძლია, მაგალითად Spring Boot 2.7 სერვისისთვის, და ეს მიიჩნიე მიგრაციად, რომელიც საკუთარ თავს გმართებს.

jar, რომელიც თავისით გაიშვება#

"გასაშვები" ორ რამეს ნიშნავს: manifest-ში მითითებულია Main-Class და ყველა დამოკიდებულება jar-შია ან მის გვერდით. უბრალო mvn package-ით აწყობილ jar-ს არც ერთი არ აქვს და ვარდება შეცდომით no main manifest attribute, in app.jar ან NoClassDefFoundError-ით პირველივე ბიბლიოთეკის კლასზე.

Spring Boot-ისთვის ორივეს Boot plugin აკეთებს. ./mvnw package ან ./gradlew bootJar გამოიმუშავებს გასაშვებ fat jar-ს ჩაშენებული web სერვერით. მიეცი ფიქსირებული სახელი, რომ სერვერის JARFILE ვერსიებს შორის არასდროს შეიცვალოს:

pom.xml
<build>  <finalName>app</finalName>  <plugins>    <plugin>      <groupId>org.springframework.boot</groupId>      <artifactId>spring-boot-maven-plugin</artifactId>    </plugin>  </plugins></build>

ეს მოგცემს target/app.jar-ს. Maven ასევე ტოვებს target/app.jar.original-ს, repackage-მდე თხელ jar-ს; უგულებელყავი. Gradle-ში bootJar წერს build/libs/-ში, jar task კი მის გვერდით მეორე, უბრალო jar-ს წერს -plain სუფიქსით. ატვირთე სუფიქსის გარეშე ვარიანტი ან bootJar task-ზე დააყენე archiveFileName = "app.jar".

სხვა framework-ებს იმავე შედეგამდე საკუთარი გზა აქვთ:

  • Quarkus ნაგულისხმევად აწყობს "fast jar"-ს: target/quarkus-app/quarkus-run.jar და მის გვერდით lib საქაღალდე. ეს მუშაობს, თუ მთელ quarkus-app საქაღალდეს ატვირთავ და JARFILE-ს დააყენებ quarkus-app/quarkus-run.jar-ზე. თუ ერთი ფაილი გინდა, დააყენე quarkus.package.jar.type=uber-jar (3.x-ის უფრო ძველ რელიზებში იყო quarkus.package.type) და მიიღებ ერთ *-runner.jar-ს.
  • Ktor-ს Gradle plugin-ით აქვს buildFatJar task, რომელიც build/libs/-ში -all.jar-ს წერს.
  • Micronaut Launch-ით გენერირებული Micronaut პროექტები Gradle-ისთვის Shadow plugin-ს შეიცავს, ამიტომ ./gradlew shadowJar გამოიმუშავებს -all.jar-ს; Maven-ზე mvn package გასაშვებ jar-ს დამოკიდებულებებით ქმნის.
  • უბრალო Java, JDA Discord ბოტი, Javalin სერვისი: გამოიყენე Maven Shade plugin ან Gradle Shadow plugin და მისი კონფიგურაციაში მიუთითე main კლასი.

შედეგი შეამოწმე, სანამ შენი მანქანიდან გავა. java -jar target/app.jar უნდა გაეშვას classpath-ზე სხვა არაფრის გარეშე, და unzip -p target/app.jar META-INF/MANIFEST.MF აჩვენებს Main-Class სტრიქონს. თუ ლოკალურად უბრალო java -jar-ით გაეშვა იმავე major ვერსიაზე, რომელიც სერვერისთვის აირჩიე, იქაც გაეშვება.

პორტი: SERVER_PORT და server.port#

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

პორტი შენს კოდამდე ორჯერ აღწევს. ის გარემოში არის როგორც SERVER_PORT, და გაშვების ბრძანება მას system property-ად server.port გადასცემს. სწორედ ამ მეორე ფორმის გამო არ სჭირდება Spring Boot-ს არანაირი კონფიგურაცია: Java system property-ები Spring-ის property-თა რიგში application.properties-ს აღემატება, ამიტომ jar-ში მითითებულ ნებისმიერ server.port-ს რეალური გადაფარავს. ეს სასარგებლოა და ზოგჯერ გასაკვირი, თუ შენი მნიშვნელობის გამარჯვებას ელოდი.

ყველა სხვა framework პორტს თავისებურად კითხულობს:

Frameworkსად მიდის პორტი
Spring Bootარაფერი გჭირდება, server.port დაყენებულია
Quarkusquarkus.http.port=${SERVER_PORT:8080}
Micronautmicronaut.server.port: ${SERVER_PORT:8080}
KtorSystem.getenv("SERVER_PORT")
უბრალო JavaSystem.getenv("SERVER_PORT")

Quarkus-ს ასევე სჭირდება quarkus.http.host=0.0.0.0, თუ შეგიცვლია, ხოლო ხელით დაწერილმა სერვერმა ყველა ინტერფეისზე უნდა დაიკავოს პორტი და არა loopback მისამართზე. JDK-ის საკუთარი HTTP სერვერი საკმარისია health გვერდისთვის ან პატარა API-სთვის და მთელ სქემას აჩვენებს:

Main.java
import com.sun.net.httpserver.HttpServer;import java.net.InetSocketAddress;public class Main {    public static void main(String[] args) throws Exception {        int port = Integer.parseInt(System.getenv().getOrDefault("SERVER_PORT", "8080"));        HttpServer server = HttpServer.create(new InetSocketAddress("0.0.0.0", port), 0);        server.createContext("/", exchange -> {            byte[] body = "ok\n".getBytes();            exchange.sendResponseHeaders(200, body.length);            exchange.getResponseBody().write(body);            exchange.close();        });        server.start();        System.out.println("Listening on 0.0.0.0:" + port);    }}

JDA-ზე აგებულ Discord ბოტს ან ნებისმიერ worker-ს, რომელიც მხოლოდ გამავალ კავშირებს ამყარებს, მოსმენა საერთოდ არ სჭირდება. მაგრამ მაინც სჭირდება გაშვების სტრიქონი, რომელსაც პანელი ცნობს, ამიტომ ბოტის დაკავშირების შემდეგ დაბეჭდე Ready.

დომენისა და HTTPS-ისთვის აპლიკაციის წინ proxy slot დააყენე და TLS JVM-ში ნუ დაასრულებ. აპლიკაციის გეგმებში შედის reverse-proxy slot: A ჩანაწერი მიუთითე ნაჩვენებ მისამართზე და სერტიფიკატი ავტომატურად გაიცემა და განახლდება. აპლიკაცია პორტზე უბრალო HTTP-ით ლაპარაკობს, კლიენტის რეალური მისამართი კი X-Forwarded-For-ში მოდის. Spring Boot-ში server.forward-headers-strategy=framework იმას აკეთებს, რომ request.getRemoteAddr() და გენერირებული URL-ები მას ითვალისწინებს. DNS-ის მხარეს განიხილავს შენი დომენი და მისი სერტიფიკატი.

მეხსიერება: heap, JVM-ის დანარჩენი ნაწილი და ლიმიტი#

აქ უშვებს შეცდომას Java-ს deploy-ების უმეტესობა პატარა გეგმებზე, და აქ ამართლებს გაშვების ბრძანების ცოდნა.

კონტეინერს აქვს მკაცრი მეხსიერების ლიმიტი, გეგმის ტოლი. JVM-ის მოხმარება არის heap პლუს ყველაფერი, რაც heap არ არის: metaspace ჩატვირთული კლასებისთვის, JIT code cache, თითო stack თითოეული thread-ისთვის, Netty-სა და NIO-ს direct byte buffer-ები და garbage collector-ის საკუთარი სტრუქტურები. Spring Boot web აპლიკაცია ხშირად ამის რამდენიმე ასეულ მეგაბაიტს ატარებს, სანამ ერთ მოთხოვნას მოემსახურება.

-XX:MaxRAMPercentage=80.0 heap-ს კონტეინერის ლიმიტის 80%-მდე გაზრდის უფლებას აძლევს. JVM ამას ერთბაშად არ იღებს - heap პატარა იწყება და იზრდება, როცა აპლიკაცია უფრო სწრაფად გამოყოფს მეხსიერებას, ვიდრე collector გაათავისუფლებს - მაგრამ დატვირთვისას ზღვრამდე მისვლა შეუძლია, და heap-ის გარეთა მეხსიერება დარჩენილში უნდა ჩაეტიოს:

გეგმის მეხსიერებაheap-ის ზღვარირჩება დანარჩენისთვის
1 GBდაახლოებით 820 MBდაახლოებით 205 MB
2 GBდაახლოებით 1.6 GBდაახლოებით 410 MB
4 GBდაახლოებით 3.3 GBდაახლოებით 820 MB
8 GBდაახლოებით 6.6 GBდაახლოებით 1.6 GB

2 GB-ზე და მეტზე ეს ტიპური სერვისისთვის კომფორტულია. 1 GB-ზე Spring Boot-ისთვის გამოსაჭერია: 205 MB metaspace-ის, code cache-ის და 200 Tomcat thread-ისთვის ცოტა სივრცეს ტოვებს და heap-მა, რომელიც დატვირთვის ზრდისას ივსება, ჯამი ზღვარს გადააცილოს შეიძლება. პატარა Javalin API, JDA ბოტი ან Quarkus სერვისი 1 GB-ზე უპრობლემოდ ეტევა. JPA-იანი Spring Boot აპლიკაცია 2 GB-ზე უკეთ გრძნობს თავს.

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

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

რადგან ბრძანების ხაზი ფიქსირებულია და JAVA, JDK_ ან JVM-ით დაწყებული ცვლადების პანელიდან დაყენება არ შეიძლება, heap-ს JAVA_TOOL_OPTIONS-ით ვერ დაწევ. რასაც აკონტროლებ, heap-ის გარეთა მხარეა, და 1 GB გეგმები ყველაზე ხშირად სწორედ იქ ვარდება:

application.properties
# Fewer request threads: each one has its own stackserver.tomcat.threads.max=50# Create beans when first used instead of all at startspring.main.lazy-initialization=true# Do not keep JPA sessions open through the view layerspring.jpa.open-in-view=false

Lazy initialisation ამცირებს გაშვების მეხსიერებას და დროს, სამაგიეროდ პირველი მოთხოვნები ნელია და შეცდომები boot-ზე კი არა, მოგვიანებით ჩნდება; ენდობამდე გაატესტე. Java 21-სა და 25-ზე spring.threads.virtual.enabled=true platform request thread-ებს virtual thread-ებით ცვლის, რაც I/O-ზე დამოკიდებული სერვისებისთვის stack-ის მეხსიერებას მკვეთრად ამცირებს. ამის გარდა, ამოიღე დამოკიდებულებები, რომლებსაც არ იყენებ - ყოველი starter metaspace-ში კლასებს ამატებს. თუ გაზომე და მაინც სხვა heap პარამეტრი გჭირდება, დაგვიწერე ticket-ში და ლიმიტს ნუ ეჭიდები; ხოლო თუ აპლიკაციას მართლა მეტი მეხსიერება სჭირდება, გულწრფელი პასუხია შემდეგი გეგმა. გრაფიკების წაკითხვას, სანამ ამას გააკეთებ, განიხილავს როდის გადახვიდე უფრო დიდ გეგმაზე.

რესტარტები კონტროლდება. crash watcher ყოველ ორ წუთში ამოწმებს; შენ მოთხოვნილ რესტარტებს არ ითვლის, მაგრამ საათში სამი გეგმარებისგარეშე რესტარტი სერვერის გვერდზე გაფრთხილებას აჩენს და ticket-ს ხსნის, ექვსი კი სერვერს აჩერებს. jar, რომელიც ლიმიტზე ყოველ რამდენიმე წუთში იკლება, ზუსტად ამას ეძებს, ამიტომ ზომა გაასწორე, სანამ ასე დატოვებ. კონსოლი მეხსიერებას ლიმიტთან ერთად გრაფიკზე აჩვენებს; დააკვირდი რეალური ტრაფიკის მთელ დღეზე. ზოგად ფონს იძლევა Linux swap და OOM killer.

გარემოს ცვლადები და საიდუმლოები#

კონფიგურაცია, რომელიც გარემოებს შორის იცვლება - მონაცემთა ბაზის URL-ები, API გასაღებები, პროფილის სახელი - გარემოშია და არა jar-ში. Spring Boot-ის relaxed binding SPRING_DATASOURCE_URL-ს აქცევს spring.datasource.url-ად, SPRING_PROFILES_ACTIVE-ს - spring.profiles.active-ად და ა.შ., ამიტომ პარამეტრების უმეტესობას კოდი არ სჭირდება.

RE:NODE-ზე შენი ცვლადები ემატება სერვერის Variables ჩანართზე, "Your variables"-ში: სახელი და მნიშვნელობა, რომელიც კონტეინერში შემდეგ გაშვებაზე ყენდება, სერვერზე 25-მდე. სახელები არის დიდი ასოები, ციფრები და ქვედა ხაზები, ასოთი დაწყებული. სახელებს, რომლებსაც პანელი, daemon ან ენის runtime უკვე ფლობს, უარყოფს, და სია უფრო ფართოა, ვიდრე წარმოიდგენ: ყველაფერი, რაც იწყება SERVER_, JAVA, JDK_, JVM, APP_, DB_, DATABASE_, PG, POSTGRES, REDIS ან AWS_-ით, ასევე ისეთი სახელები, როგორიცაა PATH და CLASSPATH. ამიტომ DB_PASSWORD არ შეინახება, SPRING_DATASOURCE_PASSWORD ან ORDERS_DB_PASSWORD კი შეინახება.

Variables tab
SPRING_PROFILES_ACTIVE=prodSPRING_DATASOURCE_URL=jdbc:postgresql://db-host:port/appdbSPRING_DATASOURCE_USERNAME=appuserSPRING_DATASOURCE_PASSWORD=change-meDISCORD_BOT_TOKEN=change-me

ზემოთ host და port ნიმუშებია - გამოიყენე ისინი, რასაც შენი მონაცემთა ბაზის სერვერი აჩვენებს. Spring-ის გარეთ ცვლადები წაიკითხე System.getenv("NAME")-ით და თუ აუცილებელი აკლია, გაშვებისას ხმამაღლა ჩავარდი; ნათელი შეცდომა პირველ წამში სჯობს NullPointerException-ს ერთი საათის მერე.

თუ ფაილი გირჩევნია, Spring Boot ასევე კითხულობს application.properties-ს ან application.yml-ს, რომელიც jar-ის გვერდითაა ან მის გვერდით config/ საქაღალდეშია, და ისინი jar-ში ჩაწყობილ ასლს აღემატება. ასეთი ფაილი სერვერზე შეინახე და არასოდეს რეპოზიტორიაში. უფრო ფართო ჩვევები - რა ითვლება საიდუმლოდ, როტაცია, .env-ის Git-ს გარეთ შენახვა - არის სტატიაში გარემოს ცვლადები და საიდუმლოები.

აპლიკაციის გეგმებში შედის ორი მონაცემთა ბაზის slot, რომლებიც პანელში იქმნება გენერირებული host-ით, მომხმარებლით და პაროლით, რაც პატარა სერვისს ესადაგება. PostgreSQL-ის, MongoDB-ის ან MySQL-ისთვის საკუთარ სერვერზე database ჰოსტინგი თითოეულ engine-ს საკუთარ გეგმას და მონაცემებს აძლევს.

deploy GitHub-იდან სერვერზე build-ის გარეშე#

jar-ის ხელით ატვირთვა ფაილ მენეჯერით ან SFTP-ით პირველი deploy-სთვის კარგია. ყველაფრისთვის, რასაც ხშირად ანახლებ, დააკავშირე GitHub.

GitHub ჩანართი რეპოზიტორიას GitHub App-ით აკავშირებს მოკლევადიანი token-ებით, ამიტომ private რეპოზიტორიებიც მუშაობს. ირჩევ production branch-ს და საქაღალდეს კოდისთვის, და ორ გადამრთველს: ყოველ გაშვებაზე უახლესი commit-ის ჩამოტვირთვა და deploy push-ზე, რომელიც მომუშავე სერვერს რესტარტს უკეთებს და გაჩერებულს უშვებს. ყოველი deploy ჩაიწერება.

საქმე იმაშია, რაც გაშვების ბრძანებიდან ვიცით: სერვერი უშვებს jar-ს და არ აწყობს. production branch-ზე Java-ს წყარო კოდის რეპოზიტორია სერვერს გასაშვებს არაფერს აძლევს. სწორი სქემაა, რომ GitHub Actions-მა jar ააწყოს და გამოაქვეყნოს branch-ზე, რომელშიც მხოლოდ არტეფაქტია, და სერვერი იმ branch-ზე მიუთითო.

on pushforce-pushwebhookrestartშენი pushmain branchGitHub Actionsmvnw packagedeploy branchმხოლოდ app.jarRE:NODE პანელიdeploy push-ზეJava სერვერიjava -jar
წყარო main-ზე, jar deploy-ზე, სერვერი უშვებს jar-ს
.github/workflows/build-jar.yml
name: Build jaron:  push:    branches: [main]permissions:  contents: writejobs:  build:    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v4      - uses: actions/setup-java@v4        with:          distribution: temurin          java-version: '21'          cache: maven      - run: ./mvnw -B -DskipTests package      - name: Publish the jar to the deploy branch        run: |          mkdir out && cp target/app.jar out/app.jar          cd out          git init -b deploy          git config user.name "github-actions[bot]"          git config user.email "github-actions[bot]@users.noreply.github.com"          git add app.jar          git commit -m "Build ${GITHUB_SHA::7}"          git push --force \            "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" \            deploy

ტესტები გაუშვი package-მდე ცალკე ნაბიჯად, თუ გინდა, რომ ჩავარდნილმა ტესტმა deploy შეაჩეროს; -DskipTests აქ მხოლოდ მაგალითის მოკლედ დასატოვებლად არის. ახალი ერთ-commit-იანი branch-ის force-push განზრახ არის: jar, რომელიც ყოველ build-ზე წინას თავზე commit-დება, რეპოზიტორიას ყოველ ჯერზე jar-ის ზომით ზრდის, და ამ ისტორიას არავის სჭირდება - წყაროს ისტორია main-ზეა.

სერვერზე production branch დააყენე deploy-ზე, ჩართე deploy push-ზე და JARFILE დატოვე app.jar-ზე. build-ის Java-ს ვერსია და სერვერის image იმავე major ვერსიაზე დააყენე. ერთი გაფრთხილება საქაღალდის პარამეტრზე: რაც კოდის საქაღალდეშია, ყოველ გაშვებაზე რეპოზიტორიით იცვლება. თუ აპლიკაცია წერს ატვირთულ ფაილებს, H2 ან SQLite ფაილს ან ლოგებს, ის ამ საქაღალდის გარეთ ჩაწერე - ან კოდი ქვესაქაღალდეში, მაგალითად release-ში, ჩადე და JARFILE დააყენე release/app.jar-ზე, რომ სერვერის root შენი დარჩეს.

GitHub ჩანართს ასევე აქვს pre-flight შემოწმება, რომელიც რეპოზიტორიას კითხულობს დაკავშირებამდე. მისი runtime შემოწმებები Node.js-ის, Python-ის, Go-ს, Bun-ისა და Deno-სთვისაა დაწერილი და არა Java-სთვის, მაგრამ commit-ში მოხვედრილ .env-ს მაინც სახელით აფიქსირებს მისი გახსნის გარეშე, რაც ნებისმიერ რეპოზიტორიაზე სასარგებლოა. მეტი მასზე - pre-flight შემოწმება, ხოლო deploy-ების პანელის მხარე არის deploy-ების გზამკვლევში.

ლოგები: კონსოლი, ფაილები და დისკი#

კონსოლი აჩვენებს პროცესის standard output-ს ფილტრის გარეშე და ცოცხლად, ბრძანების ხაზით, ჩართვა-გამორთვის ღილაკებით და ლიმიტებთან მიმართებით მეხსიერებისა და CPU-ს გრაფიკებით. სერვისების უმეტესობისთვის ეს არის ლოგი: Spring Boot, Quarkus და Logback ნაგულისხმევად კონსოლში წერენ. Spring Boot-ის ფერადი გამოტანა და კონსოლის მასთან მუშაობა ორივე წესრიგშია; თუ ფერის კოდები დაკოპირებულ ლოგს ავსებს, spring.output.ansi.enabled=never მათ გამორთავს.

კონსოლი ცოცხალი ხედია და არა არქივი. რაც რესტარტის შემდეგ წასაკითხი გჭირდება, ფაილშიც ჩაწერე და შეზღუდე, რადგან დისკის ლიმიტი რეალურია: სერვერი, რომელიც მუშაობისას დისკის ლიმიტს გადასცდება, ჩერდება და იკეტება, სანამ ადგილი არ გათავისუფლდება. Spring Boot-ის Logback rotation ოთხ property-ს საჭიროებს:

application.properties
logging.file.name=logs/app.loglogging.logback.rollingpolicy.max-file-size=10MBlogging.logback.rollingpolicy.max-history=7logging.logback.rollingpolicy.total-size-cap=200MB

total-size-cap დისკს იცავს; მის გარეშე max-history შვიდ დღეს ინახავს, რა ზომისაც არ უნდა იყოს. ფარდობითი გზა სერვერის საქაღალდიდან გამოითვლება, რომელიც სამუშაო დირექტორიაა. ფაილები წაიკითხე ფაილ მენეჯერში ან ჩამოიტანე SFTP-ით.

რა ჩაიწეროს, ცალკე საკითხია, სად ჩაიწეროს - ცალკე. ჩაწერე მაქსიმუმ ერთი სტრიქონი მოთხოვნაზე, შეცდომები stack trace-ით ერთხელ და არ ჩაწერო მოთხოვნის body-ები ან header-ები, რომლებსაც token-ები შეიძლება ჰქონდეს. უფრო შორს მიდის ლოგები, რომლებიც ღირს შენახვად.

გაჩერებაც იმავე სურათის ნაწილია. stop ღილაკი აგზავნის Ctrl+C-ს, რასაც JVM SIGINT-ად მიიჩნევს და shutdown hook-ებს უშვებს. Spring Boot ამ მომენტს graceful shutdown-ისთვის იყენებს: server.shutdown=graceful-ით ახალ მოთხოვნებს აღარ იღებს და მიმდინარეებს ამთავრებინებს, spring.lifecycle.timeout-per-shutdown-phase-მდე (ნაგულისხმევად 30 წამი). სწორედ ამიტომ არის push-ზე deploy-ის რესტარტი მიმდინარე მოთხოვნების მიმართ ზრდილობიანი. დეტალები არის graceful shutdown და health check-ებში.

რატომ არ ეშვება Java სერვერი#

შეცდომები, რომლებსაც ადამიანები რეალურად ხედავენ, და რას ნიშნავს თითოეული:

  • `Error: Unable to access jarfile app.jar`. JARFILE-ში მითითებული ფაილი იქ არ არის. შეამოწმე სახელის რეგისტრი, საქაღალდე და ხომ არ ჩადო GitHub-ის deploy-მა კოდი ქვესაქაღალდეში.
  • `no main manifest attribute, in app.jar`. jar ბიბლიოთეკის jar-ია და არა გასაშვები. ააწყე Boot, Shade ან Shadow plugin-ით, ან ატვირთე სწორი ფაილი - Gradle-ში არა -plain.
  • `UnsupportedClassVersionError ... class file version 65.0`. დაკომპილირებულია უფრო ახალი Java-სთვის, ვიდრე სერვერზე მუშაობს. შეცვალე image Startup ჩანართზე ან build-ის release დააყენე შესაბამისად.
  • `NoClassDefFoundError` ან `ClassNotFoundException` ბიბლიოთეკის კლასზე. დამოკიდებულებები jar-ში არ არის. გამოსწორება იგივეა, რაც manifest-ის შეცდომისა.
  • ეშვება, წერს რომ მზადაა, მაგრამ მისამართი არ პასუხობს. უსმენს სხვა პორტზე ან localhost-ზე. გაშვების ლოგში იპოვე პორტის სტრიქონი და შეადარე გამოყოფას.
  • პანელი სამუდამოდ "Starting"-ს აჩვენებს. აპლიკაცია მუშაობს, მაგრამ გაშვების სიტყვებიდან არც ერთი არ დაუბეჭდავს. დაბეჭდე Listening ან Ready, როგორც კი ამუშავდება.
  • რესტარტდება ყოველ რამდენიმე წუთში შეცდომის გარეშე. პროცესი მეხსიერების ლიმიტს მიადგა. იხილე მეხსიერების განყოფილება.
  • `APPLICATION FAILED TO START` Spring Boot-იდან. წაიკითხე მის ქვემოთ "Description" და "Action" სტრიქონები; ისინი ხშირად მართალია. ყველაზე გავრცელებულია მონაცემთა ბაზის datasource URL-ის არარსებობა, რაც ნიშნავს, რომ ცვლადი არ შეინახა ან შეცდომა აქვს.

FAQ#

შეუძლია სერვერს ჩემი პროექტის Maven-ით ან Gradle-ით აწყობა?

არა. Java სერვერი მზა jar-ს უშვებს და build-ის ეტაპი არ აქვს. ააწყე შენს მანქანაზე ან CI-ში - ზემოთ მოცემული GitHub Actions workflow ჩვეულებრივი გზაა - და deploy-ე მხოლოდ jar.

შემიძლია -Xmx-ის ან სხვა JVM flag-ების შეცვლა?

პანელიდან არა. გაშვების ბრძანება ფიქსირებულია და heap-ის ზღვარს გეგმის მეხსიერების 80%-ზე ადგენს, JAVA_TOOL_OPTIONS კი დაცული ცვლადის სახელია. heap-ის გარეთა მეხსიერების შემცირება აპლიკაციის შიგნიდან შეგიძლია, და თუ სხვა flag-ების გაზომილი საჭიროება გაქვს, გახსენი ticket.

რამდენი მეხსიერება სჭირდება პატარა Java სერვისს?

უბრალო Java ან Javalin API, Quarkus სერვისი ან JDA ბოტი 1 GB-ზე კარგად მუშაობს. JPA-იანი Spring Boot web აპლიკაცია 2 GB-ზე უფრო კომფორტულია, რადგან JVM-ის heap-ის გარეთა მეხსიერება პატარა გეგმის უფრო დიდ წილს იღებს.

მჭირდება ცალკე პორტი Spring Boot Actuator-ისთვის?

ჩვეულებრივ არა. Actuator-ის მთავარ პორტზე დატოვება და მხოლოდ საჭირო endpoint-ების გამოჩენა, მაგალითად health-ისა, მეორე პორტის მართვაზე მარტივია, და ამას აკეთებს ხაზი management.endpoints.web.exposure.include=health. რაც უფრო მგრძნობიარეა, მაგალითად env ან heapdump, საჯარო მისამართზე გამოუჩენელი უნდა დარჩეს.

შემიძლია WAR ფაილის გაშვება?

პირდაპირ არა - servlet კონტეინერი დაყენებული არ არის. Spring Boot-ს იმავე აპლიკაციის გასაშვებ jar-ად შეფუთვა ჩაშენებული Tomcat-ით შეუძლია, და ეს ჩვეულებრივი გზაა. ძველი, მხოლოდ WAR-ზე აგებული აპლიკაცია ჯერ თავიდან უნდა შეიფუთოს.

რჩება თუ არა სერვერი ჩემს არჩეულ Java ვერსიაზე?

დიახ. ვერსია არის image, რომელზეც სერვერი მუშაობს, არჩეული Startup ან GitHub ჩანართზე, და ის არ იცვლება, სანამ თვითონ არ შეცვლი.

გამოგადგათ?