เฮ้ ฮาเบอร์.
ในบทความนี้ ฉันต้องการพูดคุยเกี่ยวกับประสบการณ์ของฉันในการสร้างสภาพแวดล้อมการเรียนรู้สำหรับการทดลองกับไมโครเซอร์วิส เมื่อฉันได้เรียนรู้เครื่องมือใหม่แต่ละชิ้น ฉันอยากลองมาโดยตลอดไม่เพียงแต่ในเครื่องท้องถิ่นเท่านั้น แต่ยังอยากลองใช้ในสภาพที่สมจริงยิ่งขึ้นด้วย ดังนั้นฉันจึงตัดสินใจสร้างแอปพลิเคชันไมโครเซอร์วิสที่เรียบง่ายซึ่งสามารถ "ครอบคลุม" ด้วยเทคโนโลยีที่น่าสนใจทุกประเภทได้ในภายหลัง ข้อกำหนดหลักสำหรับโครงการนี้คือความใกล้ชิดกับระบบจริงสูงสุด
ในตอนแรก ฉันแบ่งการสร้างโครงการออกเป็นหลายขั้นตอน:
-
สร้างบริการสองอย่าง - 'แบ็กเอนด์' (แบ็กเอนด์) และ 'เกตเวย์' (เกตเวย์) บรรจุลงในอิมเมจนักเทียบท่าและตั้งค่าให้ทำงานร่วมกัน
คำสำคัญ: Java 11, Spring Boot, Docker, การเพิ่มประสิทธิภาพรูปภาพ
-
การพัฒนาการกำหนดค่า Kubernetes และการปรับใช้ระบบใน Google Kubernetes Engine คำสำคัญ: Kubernetes, GKE, การจัดการทรัพยากร, การปรับขนาดอัตโนมัติ, ข้อมูลลับ
-
การสร้างแผนภูมิด้วย Helm 3 เพื่อการจัดการคลัสเตอร์ที่ดีขึ้น
แท็ก: Helm 3, การใช้งานแผนภูมิ
-
การตั้งค่า Jenkins และไปป์ไลน์สำหรับการส่งโค้ดไปยังคลัสเตอร์โดยอัตโนมัติ
คำสำคัญ: การกำหนดค่า Jenkins, ปลั๊กอิน, ที่เก็บการกำหนดค่าแยกต่างหาก
ฉันวางแผนที่จะอุทิศบทความแยกต่างหากในแต่ละขั้นตอน
จุดเน้นของบทความชุดนี้ไม่ใช่วิธีการเขียนไมโครเซอร์วิส แต่จะทำให้ไมโครเซอร์วิสทำงานในระบบเดียวได้อย่างไร แม้ว่าสิ่งเหล่านี้มักจะอยู่นอกเหนือความรับผิดชอบของนักพัฒนา แต่ฉันคิดว่ามันยังคงมีประโยชน์ที่จะทำความคุ้นเคยกับสิ่งเหล่านี้อย่างน้อย 20% (ซึ่งอย่างที่คุณทราบให้ผลลัพธ์ 80%) หัวข้อสำคัญที่ไม่มีเงื่อนไขบางหัวข้อ เช่น ความปลอดภัย จะถูกละเว้นจากโปรเจ็กต์นี้ เนื่องจากผู้เขียนมีความเข้าใจเพียงเล็กน้อยเกี่ยวกับระบบนี้ที่ถูกสร้างขึ้นเพื่อการใช้งานส่วนตัวเท่านั้น ฉันยินดีรับความคิดเห็นและคำวิจารณ์ที่สร้างสรรค์
การสร้างไมโครเซอร์วิส
บริการนี้เขียนด้วย Java 11 โดยใช้ Spring Boot การโต้ตอบระหว่างบริการถูกจัดระเบียบโดยใช้ REST โปรเจ็กต์นี้จะรวมการทดสอบจำนวนขั้นต่ำ (เพื่อที่จะได้มีบางอย่างให้ทดสอบในเจนกินส์ในภายหลัง) ซอร์สโค้ดสำหรับบริการมีอยู่ใน GitHub:
เพื่อให้สามารถตรวจสอบสถานะของแต่ละบริการได้ จึงได้เพิ่ม Spring Actuator ในการขึ้นต่อกัน โดยจะสร้างตำแหน่งข้อมูล /actuator/health และส่งคืนสถานะ 200 หากบริการพร้อมที่จะยอมรับการรับส่งข้อมูล หรือ 504 หากมีปัญหา ในกรณีนี้ นี่เป็นการตรวจสอบที่ค่อนข้างสมมติ เนื่องจากบริการต่างๆ นั้นง่ายมาก และในกรณีที่มีเหตุสุดวิสัย บริการเหล่านี้มีแนวโน้มที่จะใช้งานไม่ได้โดยสิ้นเชิงมากกว่าที่ยังคงใช้งานได้บางส่วน แต่ในระบบจริง Actuator สามารถช่วยวินิจฉัยปัญหาก่อนที่ผู้ใช้จะเริ่มต่อสู้กับปัญหานั้น ตัวอย่างเช่น หากมีปัญหาในการเข้าถึงฐานข้อมูล เราสามารถตอบสนองต่อสิ่งนี้ได้โดยอัตโนมัติโดยการหยุดการประมวลผลคำขอที่มีอินสแตนซ์บริการที่ใช้งานไม่ได้
บริการส่วนหลัง
บริการแบ็กเอนด์จะนับและส่งคืนจำนวนคำขอที่ยอมรับ
รหัสตัวควบคุม:
@RestController
public class RequestsCounterController {
private final AtomicLong counter = new AtomicLong();
@GetMapping("/requests")
public Long getRequestsCount() {
return counter.incrementAndGet();
}
}
การทดสอบคอนโทรลเลอร์:
@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"));
}
}
เกตเวย์บริการ
เกตเวย์จะส่งต่อคำขอไปยังบริการแบ็กเอนด์ โดยเสริมด้วยข้อมูลต่อไปนี้:
- รหัสเกตเวย์ จำเป็นเพื่อให้สามารถแยกแยะอินสแตนซ์หนึ่งของเกตเวย์จากอีกอินสแตนซ์หนึ่งได้โดยการตอบกลับของเซิร์ฟเวอร์
- "ความลับ" บางอย่างที่จะมีบทบาทเป็นรหัสผ่านที่สำคัญมาก (จำนวนคีย์เข้ารหัสของคุกกี้ที่สำคัญ)
การกำหนดค่าใน application.properties:
backend.url=http://localhost:8081
instance.id=${random.int}
secret="default-secret"
อะแดปเตอร์แบ็กเอนด์:
@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();
}
}
ตัวควบคุม:
@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);
}
}
ปล่อย:
เราเริ่มต้นแบ็กเอนด์:
./mvnw package -DskipTests
java -Dserver.port=8081 -jar target/microservices-backend-1.0.0.jar
การเริ่มต้นเกตเวย์:
./mvnw package -DskipTests
java -jar target/microservices-gateway-1.0.0.jar
เราตรวจสอบ:
$ curl http://localhost:8080/
Number of requests 1 (gateway 38560358, secret "default-secret")
ทุกอย่างทำงานได้ ผู้อ่านที่เอาใจใส่จะทราบว่าไม่มีสิ่งใดขัดขวางไม่ให้เราเข้าถึงแบ็กเอนด์โดยตรง โดยข้ามเกตเวย์ (
นอกจากนี้ ทั้งสองบริการยังแชร์ระบบไฟล์เดียวกัน สร้างสตรีม และในขณะเดียวกันก็สามารถเริ่มรบกวนซึ่งกันและกันได้ คงจะดีไม่น้อยหากแยกไมโครเซอร์วิสของเราออกจากกัน ซึ่งสามารถทำได้โดยการกระจายแอปพลิเคชันบนเครื่องต่างๆ (ใช้เงินจำนวนมาก ยาก) โดยใช้เครื่องเสมือน (ใช้ทรัพยากรจำนวนมาก เริ่มต้นระบบเป็นเวลานาน) หรือการใช้คอนเทนเนอร์ ตามที่คาดไว้เราเลือกตัวเลือกที่สามและ
นักเทียบท่า
กล่าวโดยสรุป นักเทียบท่าจะสร้างคอนเทนเนอร์ที่แยกได้ หนึ่งคอนเทนเนอร์ต่อแอปพลิเคชัน หากต้องการใช้นักเทียบท่า คุณต้องเขียน Dockerfile - คำแนะนำในการสร้างและเรียกใช้แอปพลิเคชัน ถัดไป คุณสามารถสร้างอิมเมจ อัปโหลดไปยังรีจีสทรีอิมเมจได้ (หมายเลข XNUMX)
ไฟล์นักเทียบท่า
ลักษณะที่สำคัญที่สุดประการหนึ่งของภาพคือขนาดของภาพ รูปภาพขนาดกะทัดรัดจะดาวน์โหลดเร็วขึ้นจากพื้นที่เก็บข้อมูลระยะไกล ใช้พื้นที่น้อยลง และบริการของคุณจะเริ่มเร็วขึ้น รูปภาพใด ๆ ถูกสร้างขึ้นบนพื้นฐานของรูปภาพพื้นฐานและขอแนะนำให้เลือกตัวเลือกที่เรียบง่ายที่สุด ตัวเลือกที่ดีคือ Alpine ซึ่งเป็นระบบปฏิบัติการ Linux ที่สมบูรณ์พร้อมแพ็คเกจขั้นต่ำ
ก่อนอื่น เรามาลองเขียน Dockerfile "บนหน้าผาก" กันดีกว่า (ฉันจะบอกทันทีว่านี่เป็นวิธีที่ไม่ดี อย่าทำ):
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"]
ที่นี่เรากำลังใช้อิมเมจพื้นฐานแบบ Alpine โดยติดตั้ง JDK ไว้แล้วเพื่อสร้างโปรเจ็กต์ของเรา ด้วยคำสั่ง ADD เราจะเพิ่มไดเร็กทอรี src ปัจจุบันให้กับอิมเมจ ทำเครื่องหมายว่าใช้งานได้ (WORKDIR) และเริ่มสร้าง คำสั่ง EXPOSE 8080 จะส่งสัญญาณไปยังนักเทียบท่าว่าแอปพลิเคชันในคอนเทนเนอร์จะใช้พอร์ต 8080 (ซึ่งจะไม่ทำให้แอปพลิเคชันสามารถเข้าถึงได้จากภายนอก แต่จะทำให้แอปพลิเคชันสามารถเข้าถึงได้ เช่น จากคอนเทนเนอร์อื่นบนเครือข่ายนักเทียบท่าเดียวกัน ).
หากต้องการจัดแพคเกจบริการเป็นอิมเมจ คุณต้องรันคำสั่งจากรากของแต่ละโปรเจ็กต์:
docker image build . -t msvc-backend:1.0.0
ผลลัพธ์คืออิมเมจ 456 MB (ซึ่งอิมเมจ JDK พื้นฐานครอบครอง 340 MB) และถึงแม้ว่าคลาสในโครงการของเราสามารถนับได้ด้วยนิ้วเดียวก็ตาม วิธีลดขนาดรูปภาพของเรา:
- เราใช้การประกอบแบบหลายขั้นตอน ในขั้นตอนแรก เราจะสร้างโปรเจ็กต์ ในขั้นตอนที่สอง เราจะติดตั้ง JRE และในขั้นตอนที่สาม เราจะคัดลอกทั้งหมดลงในอิมเมจ Alpine ใหม่ที่ดูสะอาดตา โดยรวมแล้วมีเพียงส่วนประกอบที่จำเป็นเท่านั้นที่จะอยู่ในรูปภาพสุดท้าย
- ลองใช้การทำให้เป็นโมดูลของจาวา เริ่มต้นด้วย Java 9 คุณสามารถใช้เครื่องมือ jlink เพื่อสร้าง JRE จากโมดูลที่คุณต้องการ
สำหรับผู้ที่อยากรู้อยากเห็น ต่อไปนี้เป็นบทความดีๆ เกี่ยวกับแนวทางการลดขนาดภาพ
Dockerfile สุดท้าย:
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"]
เราสร้างภาพขึ้นใหม่ และด้วยเหตุนี้ ภาพจึงสูญเสียน้ำหนักไป 6 เท่า หรือคิดเป็น 77 MB ไม่เลว. หลังจากนั้นสามารถอัปโหลดรูปภาพสำเร็จรูปไปยังรีจิสตรีรูปภาพเพื่อให้รูปภาพของคุณพร้อมให้ดาวน์โหลดจากอินเทอร์เน็ต
บริการร่วมใน Docker
ประการแรก บริการของเราจะต้องอยู่ในเครือข่ายเดียวกัน มีเครือข่ายหลายประเภทในนักเทียบท่าและเราใช้เครือข่ายดั้งเดิมที่สุด - บริดจ์ซึ่งช่วยให้คุณสร้างเครือข่ายคอนเทนเนอร์ที่ทำงานบนโฮสต์เดียวกัน สร้างเครือข่ายด้วยคำสั่งต่อไปนี้:
docker network create msvc-network
ถัดไป ให้เริ่มคอนเทนเนอร์แบ็กเอนด์ชื่อ 'แบ็กเอนด์' ด้วยอิมเมจ microservices-backend:1.0.0:
docker run -dit --name backend --network msvc-net microservices-backend:1.0.0
เป็นที่น่าสังเกตว่าเครือข่ายบริดจ์ให้บริการแบบสำเร็จรูปสำหรับคอนเทนเนอร์ตามชื่อของพวกเขา นั่นคือบริการแบ็กเอนด์จะพร้อมใช้งานภายในเครือข่ายนักเทียบท่าที่
การเริ่มต้นเกตเวย์:
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
ในคำสั่งนี้ เราระบุว่าเรากำลังส่งต่อพอร์ต 80 ของโฮสต์ของเราไปยังพอร์ต 8080 ของคอนเทนเนอร์ เราใช้ตัวเลือก env เพื่อตั้งค่าตัวแปรสภาพแวดล้อมที่สปริงจะอ่านโดยอัตโนมัติและแทนที่คุณสมบัติจาก application.properties
หลังจากเริ่มต้นเราก็โทร
ข้อสรุป
ด้วยเหตุนี้ เราจึงสร้างไมโครเซอร์วิสง่ายๆ สองรายการ บรรจุไว้ในคอนเทนเนอร์นักเทียบท่า และเปิดใช้งานพร้อมกันบนเครื่องเดียวกัน อย่างไรก็ตาม ระบบผลลัพธ์มีข้อเสียหลายประการ:
- ความทนทานต่อข้อผิดพลาดไม่ดี - ทุกอย่างใช้ได้กับเราบนเซิร์ฟเวอร์เดียว
- ความสามารถในการปรับขนาดไม่ดี - เมื่อโหลดเพิ่มขึ้น คงจะดีถ้าปรับใช้อินสแตนซ์บริการเพิ่มเติมโดยอัตโนมัติ และปรับสมดุลโหลดระหว่างอินสแตนซ์เหล่านั้น
- ความซับซ้อนของการเปิดตัว - เราจำเป็นต้องป้อนคำสั่งอย่างน้อย 3 คำและมีพารามิเตอร์บางอย่าง (สำหรับ 2 บริการเท่านั้น)
เพื่อแก้ไขปัญหาข้างต้น มีวิธีแก้ไขปัญหามากมาย เช่น Docker Swarm, Nomad, Kubernetes หรือ OpenShift หากทั้งระบบเขียนด้วย Java คุณสามารถมองไปที่ Spring Cloud (
В
ที่มา: will.com