Học cách triển khai microservice. Phần 1. Spring Boot và Docker

Học cách triển khai microservice. Phần 1. Spring Boot và Docker

Xin chào, Habr.

Trong bài viết này, tôi muốn nói về trải nghiệm của mình khi tạo môi trường học tập để thử nghiệm các dịch vụ vi mô. Khi tìm hiểu mọi công cụ mới, tôi luôn muốn dùng thử nó không chỉ trên máy cục bộ của mình mà còn trong điều kiện thực tế hơn. Vì vậy, tôi quyết định tạo ra một ứng dụng microservice đơn giản hóa, sau này có thể “treo” đủ loại công nghệ thú vị. Yêu cầu chính đối với dự án là sự gần gũi về mặt chức năng tối đa của nó với hệ thống thực.

Ban đầu, tôi chia việc tạo dự án thành nhiều bước:

  1. Tạo hai dịch vụ - 'phụ trợ' và 'cổng', đóng gói chúng thành hình ảnh docker và định cấu hình chúng để hoạt động cùng nhau

    Từ khóa: Java 11, Spring Boot, Docker, tối ưu hình ảnh

  2. Phát triển hệ thống cấu hình và triển khai Kubernetes trên Google Kubernetes Engine

    Từ khóa: Kubernetes, GKE, quản lý tài nguyên, tự động mở rộng quy mô, bí mật

  3. Tạo biểu đồ bằng Helm 3 để quản lý cụm hiệu quả hơn

    Từ khóa: Helm 3, triển khai biểu đồ

  4. Thiết lập Jenkins và đường dẫn để tự động gửi mã đến cụm

    Từ khóa: Cấu hình Jenkins, plugin, kho config riêng biệt

Tôi dự định dành một bài viết riêng cho từng bước.

Trọng tâm của loạt bài viết này không phải là cách viết microservice mà là cách làm cho chúng hoạt động trong một hệ thống duy nhất. Mặc dù tất cả những điều này thường nằm ngoài trách nhiệm của nhà phát triển, nhưng tôi nghĩ việc làm quen với chúng ít nhất 20% vẫn hữu ích (được biết là chiếm tới 80% kết quả). Một số chủ đề cực kỳ quan trọng, chẳng hạn như bảo mật, sẽ bị loại khỏi dự án này vì tác giả hiểu rất ít về vấn đề này; hệ thống này được tạo ra dành riêng cho mục đích sử dụng cá nhân. Tôi hoan nghênh mọi ý kiến ​​​​và phê bình mang tính xây dựng.

Tạo microservice

Các dịch vụ được viết bằng Java 11 bằng Spring Boot. Giao tiếp giữa các dịch vụ được tổ chức bằng REST. Dự án sẽ bao gồm một số lượng thử nghiệm tối thiểu (để sau này sẽ có thứ gì đó để thử nghiệm trong Jenkins). Mã nguồn của dịch vụ có sẵn trên GitHub: phụ trợ и Cổng vào.

Để có thể kiểm tra trạng thái của từng dịch vụ, Bộ truyền động mùa xuân đã được thêm vào phần phụ thuộc của chúng. Nó sẽ tạo điểm cuối /thiết bị truyền động/sức khỏe và sẽ trả về trạng thái 200 nếu dịch vụ sẵn sàng chấp nhận lưu lượng truy cập hoặc 504 trong trường hợp có sự cố. Trong trường hợp này, đây là một kiểm tra khá hư cấu, vì các dịch vụ rất đơn giản và trong một số trường hợp bất khả kháng nào đó, chúng có nhiều khả năng ngừng hoạt động hoàn toàn hơn là vẫn hoạt động một phần. Nhưng trong các hệ thống thực, Thiết bị truyền động có thể giúp chẩn đoán sự cố trước khi người dùng bắt đầu xử lý nó. Ví dụ: nếu có vấn đề phát sinh khi truy cập vào cơ sở dữ liệu, chúng tôi sẽ có thể tự động phản hồi vấn đề này bằng cách dừng xử lý các yêu cầu có phiên bản dịch vụ bị hỏng.

Dịch vụ phụ trợ

Dịch vụ phụ trợ sẽ chỉ đếm và trả về số lượng yêu cầu được chấp nhận.

Mã điều khiển:

@RestController
public class RequestsCounterController {

    private final AtomicLong counter = new AtomicLong();

    @GetMapping("/requests")
    public Long getRequestsCount() {
        return counter.incrementAndGet();
    }
}

Kiểm tra bộ điều khiển:

@WebMvcTest(RequestsCounterController.class)
public class RequestsCounterControllerTests {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void firstRequest_one() throws Exception {
        mockMvc.perform(get("/requests"))
            .andExpect(status().isOk())
            .andExpect(MockMvcResultMatchers.content().string("1"));
    }
}

Dịch vụ cổng

Cổng sẽ chuyển tiếp yêu cầu đến dịch vụ phụ trợ, bổ sung thông tin sau:

  • id cổng. Cần thiết để một phiên bản của cổng có thể được phân biệt với phiên bản khác bằng phản hồi của máy chủ
  • Một “bí mật” nhất định sẽ đóng vai trò là mật khẩu rất quan trọng (số khóa để mã hóa một cookie quan trọng)

Cấu hình trong application.properties:

backend.url=http://localhost:8081
instance.id=${random.int}
secret="default-secret"

Bộ chuyển đổi để liên lạc với chương trình phụ trợ:

@Service
public class BackendAdapter {

    private static final String REQUESTS_ENDPOINT = "/requests";

    private final RestTemplate restTemplate;

    @Value("${backend.url}")
    private String backendUrl;

    public BackendAdapter(RestTemplateBuilder builder) {
        restTemplate = builder.build();
    }

    public String getRequests() {
        ResponseEntity<String> response = restTemplate.getForEntity(
backendUrl + REQUESTS_ENDPOINT, String.class);
        return response.getBody();
    }
}

Bộ điều khiển:

@RestController
@RequiredArgsConstructor
public class EndpointController {

    private final BackendAdapter backendAdapter;

    @Value("${instance.id}")
    private int instanceId;

    @Value("${secret}")
    private String secret;

    @GetMapping("/")
    public String getRequestsCount() {
        return String.format("Number of requests %s (gateway %d, secret %s)", backendAdapter.getRequests(), instanceId, secret);
    }
}

Phóng:

Hãy khởi chạy phần phụ trợ:

./mvnw package -DskipTests
java -Dserver.port=8081 -jar target/microservices-backend-1.0.0.jar

Hãy bắt đầu cổng:

./mvnw package -DskipTests
java -jar target/microservices-gateway-1.0.0.jar

Chúng tôi kiểm tra:

$ curl http://localhost:8080/
Number of requests 1 (gateway 38560358, secret "default-secret")

Mọi thứ đang hoạt động. Người đọc chú ý sẽ lưu ý rằng không có gì ngăn cản chúng tôi truy cập trực tiếp vào phần phụ trợ, bỏ qua cổng (http://localhost:8081/requests). Để khắc phục điều này, các dịch vụ phải được kết hợp thành một mạng và chỉ cổng vào mới được “nhô ra” bên ngoài.
Ngoài ra, cả hai dịch vụ đều chia sẻ cùng một hệ thống tệp, tạo các luồng và tại một thời điểm có thể bắt đầu can thiệp lẫn nhau. Sẽ rất tốt nếu tách biệt các dịch vụ vi mô của chúng tôi. Điều này có thể đạt được bằng cách phân phối ứng dụng trên các máy khác nhau (nhiều tiền, khó khăn), sử dụng máy ảo (ngốn nhiều tài nguyên, khởi động lâu) hoặc sử dụng phương pháp container hóa. Đúng như dự đoán, chúng tôi chọn phương án thứ ba và phu bến tàu như một công cụ để container hóa.

phu bến tàu

Nói tóm lại, Docker tạo các thùng chứa riêng biệt, mỗi thùng chứa cho một ứng dụng. Để sử dụng Docker, bạn cần viết Dockerfile - hướng dẫn build và chạy ứng dụng. Tiếp theo, bạn có thể xây dựng hình ảnh, tải nó lên sổ đăng ký hình ảnh (No. Dockerhub) và triển khai vi dịch vụ của bạn trong bất kỳ môi trường cố định nào bằng một lệnh.

Dockerfile

Một trong những đặc điểm quan trọng nhất của hình ảnh là kích thước của nó. Hình ảnh nhỏ gọn sẽ tải xuống nhanh hơn từ kho lưu trữ từ xa, chiếm ít dung lượng hơn và dịch vụ của bạn sẽ khởi động nhanh hơn. Bất kỳ hình ảnh nào cũng được xây dựng trên cơ sở một hình ảnh cơ bản và bạn nên chọn tùy chọn tối giản nhất. Một lựa chọn tốt là Alpine, một bản phân phối Linux chính thức với số lượng gói tối thiểu.

Đầu tiên chúng ta hãy thử viết trực tiếp một Dockerfile (mình sẽ nói ngay rằng đây là một cách tồi, đừng làm như vậy):

FROM adoptopenjdk/openjdk11:jdk-11.0.5_10-alpine
ADD . /src
WORKDIR /src
RUN ./mvnw package -DskipTests
EXPOSE 8080
ENTRYPOINT ["java","-jar","target/microservices-gateway-1.0.0.jar"]

Ở đây chúng tôi đang sử dụng hình ảnh cơ sở dựa trên Alpine với JDK đã được cài đặt để xây dựng dự án của chúng tôi. Sử dụng lệnh ADD, chúng ta thêm thư mục src hiện tại vào hình ảnh, đánh dấu nó là đang hoạt động (WORKDIR) và bắt đầu quá trình xây dựng. Lệnh EXPOSE 8080 báo hiệu cho docker rằng ứng dụng trong vùng chứa sẽ sử dụng cổng 8080 của nó (điều này sẽ không làm cho ứng dụng có thể truy cập được từ bên ngoài, nhưng sẽ cho phép truy cập ứng dụng, chẳng hạn như từ một vùng chứa khác trên cùng mạng docker ).

Để đóng gói các dịch vụ thành hình ảnh, bạn cần chạy các lệnh từ thư mục gốc của mỗi dự án:

docker image build . -t msvc-backend:1.0.0

Kết quả là chúng ta thu được một hình ảnh có kích thước 456 MB (trong đó hình ảnh JDK 340 cơ sở chiếm MB). Và tất cả mặc dù thực tế là các lớp trong dự án của chúng tôi có thể được đếm trên đầu ngón tay. Để giảm kích thước hình ảnh của chúng tôi:

  • Chúng tôi sử dụng lắp ráp nhiều bước. Trong bước đầu tiên, chúng tôi sẽ tập hợp dự án, trong bước thứ hai, chúng tôi sẽ cài đặt JRE và trong bước thứ ba, chúng tôi sẽ sao chép tất cả những điều này vào một hình ảnh Alpine sạch mới. Tổng cộng, hình ảnh cuối cùng sẽ chỉ chứa các thành phần cần thiết.
  • Hãy sử dụng mô đun hóa java. Bắt đầu với Java 9, bạn có thể sử dụng công cụ jlink để tạo JRE chỉ từ các mô-đun bạn cần

Đối với những người tò mò, đây là một bài viết hay về các phương pháp giảm kích thước hình ảnh https://habr.com/ru/company/ruvds/blog/485650/.

Dockerfile cuối cùng:

FROM adoptopenjdk/openjdk11:jdk-11.0.5_10-alpine as builder
ADD . /src
WORKDIR /src
RUN ./mvnw package -DskipTests

FROM alpine:3.10.3 as packager
RUN apk --no-cache add openjdk11-jdk openjdk11-jmods
ENV JAVA_MINIMAL="/opt/java-minimal"
RUN /usr/lib/jvm/java-11-openjdk/bin/jlink 
    --verbose 
    --add-modules 
        java.base,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument 
    --compress 2 --strip-debug --no-header-files --no-man-pages 
    --release-info="add:IMPLEMENTOR=radistao:IMPLEMENTOR_VERSION=radistao_JRE" 
    --output "$JAVA_MINIMAL"

FROM alpine:3.10.3
LABEL maintainer="Anton Shelenkov [email protected]"
ENV JAVA_HOME=/opt/java-minimal
ENV PATH="$PATH:$JAVA_HOME/bin"
COPY --from=packager "$JAVA_HOME" "$JAVA_HOME"
COPY --from=builder /src/target/microservices-backend-*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app.jar"]

Chúng tôi đã tạo lại hình ảnh và cuối cùng nó mỏng hơn 6 lần, tương đương 77 MB. Không tệ. Sau đó, những hình ảnh hoàn thiện có thể được tải lên sổ đăng ký hình ảnh để hình ảnh của bạn có thể tải xuống từ Internet.

Chạy các dịch vụ cùng nhau trong Docker

Để bắt đầu, các dịch vụ của chúng tôi phải nằm trên cùng một mạng. Có một số loại mạng trong Docker và chúng tôi sử dụng loại nguyên thủy nhất trong số đó - bridge, cho phép bạn kết nối các vùng chứa chạy trên cùng một máy chủ. Hãy tạo một mạng bằng lệnh sau:

docker network create msvc-network

Tiếp theo, hãy khởi chạy vùng chứa phụ trợ có tên 'backend' với hình ảnh microservices-backend:1.0.0:

docker run -dit --name backend --network msvc-net microservices-backend:1.0.0

Điều đáng chú ý là mạng cầu nối cung cấp dịch vụ khám phá ngay lập tức cho các container theo tên của chúng. Nghĩa là, dịch vụ phụ trợ sẽ có sẵn trong mạng Docker tại http://backend:8080.

Hãy bắt đầu cổng:

docker run -dit -p 80:8080 --env secret=my-real-secret --env BACKEND_URL=http://backend:8080/ --name gateway --network msvc-net microservices-gateway:1.0.0

Trong lệnh này, chúng tôi chỉ ra rằng chúng tôi đang chuyển tiếp cổng 80 của máy chủ sang cổng 8080 của vùng chứa. Chúng tôi sử dụng các tùy chọn env để đặt các biến môi trường sẽ được đọc tự động vào mùa xuân và ghi đè các thuộc tính từ application.properties.

Sau khi khởi chạy, hãy gọi http://localhost/ và đảm bảo rằng mọi thứ đều hoạt động, như trong trường hợp trước.

Kết luận

Kết quả là chúng tôi đã tạo ra hai vi dịch vụ đơn giản, đóng gói chúng trong các thùng chứa docker và khởi chạy chúng cùng nhau trên cùng một máy. Tuy nhiên, hệ thống kết quả có một số nhược điểm:

  • Khả năng chịu lỗi kém - mọi thứ đều hoạt động trên một máy chủ đối với chúng tôi
  • Khả năng mở rộng kém - khi tải tăng lên, sẽ rất tốt nếu tự động triển khai các phiên bản dịch vụ bổ sung và cân bằng tải giữa chúng
  • Độ phức tạp khi khởi chạy - chúng tôi cần nhập ít nhất 3 lệnh, với các tham số nhất định (điều này chỉ dành cho 2 dịch vụ)

Để giải quyết các vấn đề trên, có một số giải pháp như Docker Swarm, Nomad, Kubernetes hay OpenShift. Nếu toàn bộ hệ thống được viết bằng Java thì bạn có thể hướng tới Spring Cloud (bài báo hay).

В phần tiếp theo Tôi sẽ kể cho bạn nghe về cách tôi thiết lập Kubernetes và triển khai dự án lên Google Kubernetes Engine.

Nguồn: www.habr.com

Thêm một lời nhận xét