Xây dựng lớp chậm trong Docker. Xây dựng bộ đệm


8

Tôi đang làm dự án đại học nơi chúng tôi cần chạy nhiều ứng dụng Spring Boot cùng một lúc.

Tôi đã cấu hình xây dựng nhiều giai đoạn với hình ảnh docker gradle và sau đó chạy ứng dụng trong hình ảnh openjdk: jre.

Đây là Dockerfile của tôi:

FROM gradle:5.3.0-jdk11-slim as builder
USER root
WORKDIR /usr/src/java-code
COPY . /usr/src/java-code/

RUN gradle bootJar

FROM openjdk:11-jre-slim
EXPOSE 8080
WORKDIR /usr/src/java-app
COPY --from=builder /usr/src/java-code/build/libs/*.jar ./app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

Tôi đang xây dựng và chạy mọi thứ với docker-compose. Một phần của docker-compose:

 website_server:
    build: website-server
    image: website-server:latest
    container_name: "website-server"
    ports:
      - "81:8080"

Tất nhiên xây dựng đầu tiên mất tuổi. Docker đang kéo tất cả các phụ thuộc của nó. Và tôi ổn với điều đó.

Hiện tại mọi thứ đều hoạt động tốt nhưng mỗi thay đổi nhỏ trong mã gây ra khoảng 1 phút thời gian xây dựng cho một ứng dụng.

Một phần của nhật ký xây dựng: docker-compose up --build

Step 1/10 : FROM gradle:5.3.0-jdk11-slim as builder
 ---> 668e92a5b906
Step 2/10 : USER root
 ---> Using cache
 ---> dac9a962d8b6
Step 3/10 : WORKDIR /usr/src/java-code
 ---> Using cache
 ---> e3f4528347f1
Step 4/10 : COPY . /usr/src/java-code/
 ---> Using cache
 ---> 52b136a280a2
Step 5/10 : RUN gradle bootJar
 ---> Running in 88a5ac812ac8

Welcome to Gradle 5.3!

Here are the highlights of this release:
 - Feature variants AKA "optional dependencies"
 - Type-safe accessors in Kotlin precompiled script plugins
 - Gradle Module Metadata 1.0

For more details see https://docs.gradle.org/5.3/release-notes.html

Starting a Gradle Daemon (subsequent builds will be faster)
> Task :compileJava
> Task :processResources
> Task :classes
> Task :bootJar

BUILD SUCCESSFUL in 48s
3 actionable tasks: 3 executed
Removing intermediate container 88a5ac812ac8
 ---> 4f9beba838ed
Step 6/10 : FROM openjdk:11-jre-slim
 ---> 0e452dba629c
Step 7/10 : EXPOSE 8080
 ---> Using cache
 ---> d5519e55d690
Step 8/10 : WORKDIR /usr/src/java-app
 ---> Using cache
 ---> 196f1321db2c
Step 9/10 : COPY --from=builder /usr/src/java-code/build/libs/*.jar ./app.jar
 ---> d101eefa2487
Step 10/10 : ENTRYPOINT ["java", "-jar", "app.jar"]
 ---> Running in ad02f0497c8f
Removing intermediate container ad02f0497c8f
 ---> 0c63eeef8c8e
Successfully built 0c63eeef8c8e
Successfully tagged website-server:latest

Mỗi lần nó đóng băng Starting a Gradle Daemon (subsequent builds will be faster)

Tôi đã suy nghĩ về việc thêm âm lượng với các phụ thuộc lớp được lưu trong bộ nhớ cache nhưng tôi không biết đó có phải là cốt lõi của vấn đề không. Ngoài ra tôi không thể tìm thấy ví dụ tốt cho điều đó.

Có cách nào để tăng tốc độ xây dựng?


Tôi không thực sự quen thuộc với Java và Gradle, nhưng đó không phải là hành vi giống như trong sự phát triển cục bộ? Ý tôi là nếu bạn đã thực hiện một số thay đổi cho mã của mình, bạn cần biên dịch lại dự án cũng như áp dụng các thay đổi đó vào thời gian chạy. Có lẽ điều bạn muốn nói là Gradle biên dịch lại tất cả các dự án thay vì chỉ thay đổi các phần?
Charlie

Dockerfile được đăng hoạt động tốt nhưng vấn đề là tốc độ. Xây dựng cục bộ phải mất ~ 8 giây và trong Docker ~ 1 đến 1,5 phút. Tôi đã tự hỏi nếu có một cách để tăng tốc độ xây dựng docker.
PAwel_Z

Câu trả lời:


13

Build mất rất nhiều thời gian vì Gradle mỗi khi hình ảnh Docker được xây dựng tải xuống tất cả các plugin và phụ thuộc.

Không có cách nào để gắn một âm lượng tại thời điểm xây dựng hình ảnh. Nhưng có thể giới thiệu giai đoạn mới sẽ tải xuống tất cả các phụ thuộc và sẽ được lưu trữ dưới dạng lớp hình ảnh Docker.

FROM gradle:5.6.4-jdk11 as cache
RUN mkdir -p /home/gradle/cache_home
ENV GRADLE_USER_HOME /home/gradle/cache_home
COPY build.gradle /home/gradle/java-code/
WORKDIR /home/gradle/java-code
RUN gradle clean build -i --stacktrace

FROM gradle:5.6.4-jdk11 as builder
COPY --from=cache /home/gradle/cache_home /home/gradle/.gradle
COPY . /usr/src/java-code/
WORKDIR /usr/src/java-code
RUN gradle bootJar -i --stacktrace

FROM openjdk:11-jre-slim
EXPOSE 8080
USER root
WORKDIR /usr/src/java-app
COPY --from=builder /usr/src/java-code/build/libs/*.jar ./app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

Gradle plugin và bộ đệm phụ thuộc được đặt trong $GRADLE_USER_HOME/caches. GRADLE_USER_HOMEphải được đặt thành một cái gì đó khác với /home/gradle/.gradle. /home/gradle/.gradletrong cha mẹ Gradle Docker hình ảnh được định nghĩa là âm lượng và bị xóa sau mỗi lớp hình ảnh.

Trong mã mẫu GRADLE_USER_HOMEđược đặt thành /home/gradle/cache_home.

Trong buildergiai đoạn Gradle cache được sao chép để tránh tải lại các phụ thuộc : COPY --from=cache /home/gradle/cache_home /home/gradle/.gradle.

Sân khấu cachesẽ được xây dựng lại chỉ khi build.gradleđược thay đổi. Khi các lớp Java thay đổi, lớp hình ảnh được lưu trong bộ nhớ cache với tất cả các phụ thuộc được sử dụng lại.

Sửa đổi này có thể giảm thời gian xây dựng nhưng cách xây dựng hình ảnh Docker sạch hơn với các ứng dụng Java là Jib của Google. Có một plugin Jib Gradle cho phép xây dựng hình ảnh chứa cho các ứng dụng Java mà không cần tạo Dockerfile theo cách thủ công. Xây dựng hình ảnh với ứng dụng và chạy container tương tự như:

gradle clean build jib
docker-compose up

2
Xây dựng nhiều giai đoạn với một giai đoạn chỉ bao gồm build.gradletừ bối cảnh chắc chắn là con đường để đi. Bằng cách chỉ sao chép build.gradletrong cachebạn, đảm bảo các phụ thuộc sẽ chỉ được tải xuống một lần nếu tệp xây dựng Gradle không thay đổi (Docker sẽ sử dụng lại bộ đệm)
Pierre B.

4

Docker lưu trữ hình ảnh của nó trong "lớp". Mỗi lệnh mà bạn chạy là một lớp. Mỗi thay đổi được phát hiện trong một lớp nhất định sẽ làm mất hiệu lực các lớp xuất hiện sau nó. Nếu bộ đệm bị vô hiệu, thì các lớp không hợp lệ phải được xây dựng từ đầu, bao gồm cả các phụ thuộc .

Tôi sẽ đề nghị tách các bước xây dựng của bạn. Có một lớp trước đó chỉ sao chép đặc tả phụ thuộc vào hình ảnh, sau đó chạy một lệnh sẽ dẫn đến việc Gradle tải xuống các phụ thuộc. Sau khi hoàn thành, sao chép nguồn của bạn vào cùng một vị trí nơi bạn vừa làm điều đó và chạy bản dựng thực sự.

Bằng cách này, các lớp trước sẽ chỉ bị vô hiệu khi các tập tin lớp thay đổi.

Tôi đã không làm điều này với Java / Gradle, nhưng tôi đã theo mô hình tương tự với một dự án Rust, được hướng dẫn bởi bài đăng trên blog này .


1

Bạn có thể thử và sử dụng BuildKit (hiện được kích hoạt theo mặc định trong docker -compose 1.25 mới nhất )

Xem " Tăng tốc độ xây dựng hình ảnh Docker ứng dụng java của bạn với BuildKit! " Từ Aboullaite Med .

(Điều này là dành cho maven, nhưng ý tưởng tương tự áp dụng cho lớp)

Hãy xem xét Dockerfile sau:

FROM maven:3.6.1-jdk-11-slim AS build  
USER MYUSER  
RUN mvn clean package  

Sửa đổi dòng thứ hai luôn làm mất hiệu lực bộ đệm maven do phụ thuộc sai, làm lộ ra vấn đề bộ đệm không hiệu quả.

BuildKit giải quyết giới hạn này bằng cách giới thiệu trình giải đồ thị xây dựng đồng thời, có thể chạy các bước xây dựng song song và tối ưu hóa các lệnh không ảnh hưởng đến kết quả cuối cùng.

Ngoài ra, Buildkit chỉ theo dõi các bản cập nhật được thực hiện cho các tệp giữa các lệnh xây dựng lặp lại để tối ưu hóa quyền truy cập vào các tệp nguồn cục bộ. Vì vậy, không cần phải đợi các tệp cục bộ được đọc hoặc tải lên trước khi công việc có thể bắt đầu.


Vấn đề không liên quan đến việc xây dựng hình ảnh Docker, nhưng chạy các lệnh trong Dockerfile. Tôi nghĩ đó là vấn đề bộ nhớ đệm. Tôi đã thử lưu vào bộ nhớ cache nhưng nó vẫn tải Gradle, v.v. mỗi lần chạy. Tôi cũng đã thử kết hợp các điểm đến âm lượng khác nhau.
Neel Kamath

@NeelKamath "chạy các lệnh trong Dockerfile" là một phần của "xây dựng hình ảnh Docker"! Và BuildKit được tạo để xây dựng bộ nhớ đệm và tăng tốc các bản dựng docker . Hãy thử một lần.
VonC

Chỉ sử dụng BuildKit sẽ không giải quyết được vấn đề này: bằng cách sao chép toàn bộ bối cảnh khi bắt đầu xây dựng và sử dụng RUN, BuildKit sẽ luôn xây dựng lại mọi thứ trên mỗi thay đổi mã (vì bối cảnh đã thay đổi), nhưng ngoài câu trả lời @Evgeniy Khyst nó có thể tiến tới một kết quả tốt hơn
Pierre B.

@PierreB. ĐỒNG Ý. Vì vậy, bất kỳ giải pháp sẽ phức tạp hơn tôi nghĩ.
VonC

0

Tôi không biết nhiều về phần bên trong docker, nhưng tôi nghĩ rằng vấn đề là mỗi docker buildlệnh mới , sẽ sao chép tất cả các tệp và xây dựng chúng (nếu nó phát hiện các thay đổi trong ít nhất một tệp). Sau đó, điều này rất có thể sẽ thay đổi một số lọ và các bước thứ hai cũng cần phải chạy.

Đề nghị của tôi là xây dựng trên thiết bị đầu cuối (bên ngoài docker) và chỉ docker xây dựng hình ảnh ứng dụng.

Điều này thậm chí có thể được tự động hóa với một plugin gradle:


Vì vậy, xây dựng lớp trong docker là một cách sai lầm để đi? Ý tưởng là bạn sẽ không cần bất kỳ phụ thuộc nào được cài đặt để xây dựng và chạy mã trong môi trường của bạn.
PAwel_Z

Ồ tôi hiểu rồi! Tôi không nghĩ rằng bạn đề cập đến câu hỏi của bạn. Trong trường hợp đó, có vẻ như giải pháp hiện tại vẫn ổn ... sẽ mất thời gian. Một câu hỏi khác là, tại sao bạn muốn dev env của bạn không có sự phụ thuộc? nó được gọi là dev env vì nó sẽ có dev dev trong đó.
Vetras

Đó là một điểm hay. Tôi nên được cụ thể hơn. Tất cả các docker trong phát triển container là do thực tế rằng dự án đang được chỉnh sửa bởi khoảng 10 người. Vì vậy, tôi nghĩ rằng tôi sẽ tốt đẹp khi không có bất kỳ phụ thuộc hệ điều hành hoặc sdk nào. Nhưng có lẽ đó là một quá mức cần thiết.
PAwel_Z

Theo kinh nghiệm của tôi (các đội lên tới 6/7 dev) mọi người đều có thiết lập cục bộ. Thông thường có một tệp readme trên mỗi gốc repo với các lệnh bước và tất cả những gì cần thiết lập, cho kho lưu trữ đó. Tôi hiểu vấn đề của bạn, nhưng tôi không nghĩ rằng docker là công cụ phù hợp cho việc này. Có thể, cố gắng đơn giản hóa / thu nhỏ thiết lập cần thiết ở vị trí đầu tiên, ví dụ: bằng mã tái cấu trúc, đặt mặc định tốt hơn, sử dụng quy ước đặt tên, ít phụ thuộc hơn, tài liệu thiết lập readme tốt hơn.
Vetras

0

Cũng giống như bổ sung cho câu trả lời của người khác, nếu kết nối internet của bạn chậm, vì nó tải phụ thuộc mỗi lần, bạn có thể muốn thiết lập sonatype nexus, để giữ cho các phụ thuộc đã được tải xuống.


0

Như các câu trả lời khác đã đề cập, docker lưu trữ từng bước trong một lớp. Nếu bạn bằng cách nào đó chỉ có thể nhận được các phụ thuộc được tải xuống vào một lớp, thì nó sẽ không phải được tải xuống lại mỗi lần, giả sử các phụ thuộc không thay đổi.

Thật không may, gradle không có một nhiệm vụ tích hợp để làm điều này. Nhưng bạn vẫn có thể làm việc xung quanh nó. Đây là những gì tôi đã làm:

# Only copy dependency-related files
COPY build.gradle gradle.properties settings.gradle /app/

# Only download dependencies
# Eat the expected build failure since no source code has been copied yet
RUN gradle clean build --no-daemon > /dev/null 2>&1 || true

# Copy all files
COPY ./ /app/

# Do the actual build
RUN gradle clean build --no-daemon

Ngoài ra, hãy đảm bảo .dockerignoretệp của bạn có ít nhất các mục này để chúng không được gửi trong ngữ cảnh xây dựng docker khi hình ảnh được xây dựng:

.gradle/
bin/
build/
gradle/
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.