สร้าง Microservices ด้วย Spring Boot + Spring Cloud
วันก่อนเจอ guide ของ baeldung เกี่ยวกับการใช้ Spring Boot กับ Spring Cloud ในการสร้าง Microservice พอเอามาลองทำแล้วก็เห็นว่ามีประโยชน์ วันนี้เลยจะเอามาลองสร้าง Microservice ด้วย Spring Boot กับ Spring Cloud กันดู
Spring Cloud
เป็นโครงการของ Spring Framework ที่ทำให้สามารถพัฒนา Spring Application ทำงานบนระบบ Cloud ได้เป็นอย่างดี โดยเฉพาะอย่างยิง Spring Cloud Netflix ทีทำให้ Spring Boot application เข้ากันได้กับ Netflix OSS ทำให้เราสามารถพัฒนา Microservice ด้วย Spring Boot ที่ทำงานบนระบบ Cloud ได้ง่ายและรวดเร็ว
โดยที่ Spring Cloud รองรับ distributed system ด้วยระบบต่างๆ ดังนี้
- Distributed Configuration
- Service Discovery
- Routing
- Distributed Tracing
- Circuit Breaker
- Load Balancing
- Service-to-Service Call
ซึ่งในบทความนี้จะสร้าง Microservice ด้วย Spring Boot + Spring Cloud ที่คล้ายกับรูปนี้ คือมี Microservice, Service Registry, Config Server, Circuit Breaker และ Distributed Tracing ทำงานร่วมกัน
Create Web Services
ในบทความนี้จะสร้าง RESTful Web Service ด้วย Spring Boot และ Spring Cloud เป็น Microservice ทั้งหมด 3 Service คือ Employee Service, Department Service และ Organization Service โดยใช MongoDB เป็นฐานข้อมูลของแต่ละ Service เลือก dependency ขั้นต้นใน Spring Initilizr เป็น Spring Web Starter
, Spring Boot Actuator
, Spring Data MongoDB
, Cloud Bootstrap
และ Lombok
😁
ใน Spring Initializr สามารถเลือกได้ว่าจะ Generate the project
download ทั้ง project เป็น zip file หรือ Explore the Project
ที่จะดูเฉพาะ POM file อย่างเดียวและ copy ไปใช้ได้เลย หรือ download ทั้ง project ก็ได้ ดังนั้นจะได้ dependency ของทั้ง 3 project ดังนี้
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Employee Service
สร้าง employee
package และเพิ่มคลาส Employee เป็น data model ใน Employee Service
src/main/java/com/iphayao/service/employee/Employee.java
เพิ่ม EmployeeRepository
interface เพื่อเป็น Repository layer โดย extends จาก MongoRepository และเพิ่ม findByDepartmentId
และ findByOrganizationId
เพื่อ query ข้อมูล Employee ด้วย Department ID และ Organization ID ตามลำดับ
src/main/java/com/iphayao/service/employee/EmployeeRepository.java
ใช้งาน EmployeeRepository
ด้วย EmployeeService ที่มีความสามารถของ CRUD operation โดย inject EmployeeRepository
ด้วย @Autowired
annotation
src/main/java/com/iphayao/service/employee/EmployeeService.java
และเพิ่มคลาส EmployeeController
เพื่อรับ HTTP request จาก User และใช้งาน EmployeeService
src/main/java/com/iphayao/service/employee/EmployeeController.java
เมื่อลอง Run ด้วยคำสั่ง mvn spring-boot:run
ถ้าไม่ error ก็แสดงว่า Employee Service ทำงานได้ อาจจะลองใช้ Postman ทดสอบดูก็ได้แต่ในที่นี้จะข้ามไปก่อน
Department Service
จะเหมือนกับ Employee Service โดยที่สร้าง department package และเพิ่ม Department
data model, DepartmentRepository
, DepartmentService
และ DepartmentController
ที่ทำ CRUD operation ได้
ใน Department
data model จะประกอบด้วย Employee
ที่อยู่ใน Department
Organization Service
เช่นเดียวกับ Employee Service และ Department Service เพิ่ม Organization
, OrganizationRepository
, OrganizationService
และ OrganizatinoController
ซึ่ง Organization
จะประกอบด้วย Employee
และ Department
แต่เราไม่สามาถ run ทุก service พร้อมกันได้เนื่องจาก port เริ่มต้นของ Spring Boot จะเป็น port 8080
ดังนั้นเราจำเป็นต้องกำหนด port ของแต่ละ service ให้แตกต่างกัน ด้วยการกำหนด server.port
ใน application config (application.yml)
server:
port: 8081
โดยกำหนด port ของแต่ละ service ดังนี้
employee-serviec -> port 8081
department-service -> port 8082
organization-service -> port 8083
และ run ทั้ง 3 service พร้อมกัน จะเห็นได้ว่า run ได้พร้อมกันไม่มีปัญหาและ service ใน port ตามกำหนด
Service Discovery
เมื่อเรามี Microservice เรียบร้อยแล้ว ต่อไปจะสร้าง Servide Discovery สำหรับ register service ทั้งหมด ด้วย Spring Cloud Netflix Eureka Server
สร้าง Discovery
project ด้วย dependency Eureka Server
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
ใช้ @EnableEurekaServer
annotation เพื่อกำหนดให้ Spring Boot application เป็น Eureka Server (Service Discovery)
src/main/java/com/iphayao/discovery/DiscoveryApplication.java
และกำนหด application properties ดังนี้เพื่อกำหนด port ของ Service Discovery และ ตั้งค่า Eureka เพื่อไม่ให้ register ตัวเองกับ Discovery Server
เมื่อ Run application จะเห็นได้ว่า Service Discovery ทำงานบน port 9001 และดู Eureka dashboard ได้ด้วย path http://localhost:9001
Discovery Client
เมื่อมี Service Discovery แล้วจะทำให้เห็น Microservice ทั้ง 3 Service ที่สร้างขึ้นมาก่อนแล้วนั้น จะต้องเพิ่ม Discovery Client ให้กับแต่ละ Service ด้วย dependency Eureka Discovery Client
ก่อน
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
หลังจากนั้นเพิ่ม @EnableDiscoveryClient
annotation ใน Service Application
กำหนด path ของ Service Discovery eureka.client.service-url
defaultZone ใน application properties application.yml
เป็น path ของ Service Discovery
และจะต้องกำหนดชื่อให้แต่ละ Service ด้วยการกำหนด spring.application.name
ถ้าไม่เช่นนั้นชื่อของ Service จะเป็น UNKNOW
spring:
application:
name: employee-serviceeureka:
client:
service-url:
defaultZone: http://localhost:9001/eureka
เมื่อไปดูที่ Service Discovery Server จะเห็น Service ที่เพิ่ม Discovery Client แสดงบน instance ที่ register อยู่กับ Eureka (Service Discovery) ซึ่งก็คือ 3 Service
Config Server
เมื่อ register Microservice กับ Service Discovery แล้ว ก็จะสร้าง Config Server เพื่อเป็น distributed configuration และให้ Service ที่มองเห็นโดย Service Discovery สามารถมาเอา config จาก Config Server นี้ได้ทำให้สามารถแก้ไข config ได้แยกต่างหากจากตัว Service
สร้าง Config Server project ด้วย dependency Config Server
เพื่อสร้าง Config Server และ Eureka Discovery Client
เพื่อให้ register Config Server กับ Service Discovery Server
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
ใช้ @EnableConfigServer
เพื่อกำหนดให้ Application เป็น Config Server และ ใช้ @EnableDiscoveryClient
เพื่อ register กับ Service Discovery
src/main/java/com/iphayao/config/ConfigApplication.java
กำหนด application properties ให้กับ Config Server โดยกำหนดให้เก็บ config ใน Git server path เป็น file://${user.home}/application-config
หรือจะเป็น path ของ Git บน Github, Gitlab หรือ Git Server อื่นๆ ก็ได้
เมื่อ run Config Server จะเห็น CONFIG-SERVER
แสดงบน Service Discovery
โดยที่เพิ่ม dependencyConfig Client
ใน Service Discovery project
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
และย้าย application properties ของ Service Discovery ไปอยู่บน Config Server ใช้ชื่อเป็น discovery.yml
และเพิ่ม bootstrap properties bootstrap.yml
ซึ่ง Spring Boot จะโหลดก่อน application properties โดยกำหนดชื่อของ config และ URI ของ Config Server
เมื่อ Rerun Service Discovery จะเห็นได้ว่าต้อนเริ่มต้นของ Spring Boot จะทำการ fetching config จาก Config Server และใช้ config นั้นเป็น application properties ของ Spring Application
เพิ่ม dependency Config Client
ให้ทุก Service ย้าย application properties ไปไว้บน Config Server และเพิ่ม bootstrap properties โดยกำหนดชื่อของ config และ service-id
ของ Config Server และ service-url
ของ Service Discovery ซึ่ง Service จะไปหา URL ของ Config Service ด้วย service-id
จาก Service Discovery หลังจากนั้นก็จะอ่าน config file จาก Config Server
API Gateway
เมือมี Service Discovery และ Config Server แล้ว ต่อไปก็จะรวมช่องทาง request เข้ามาทางเดียวผ่านทาง API Gateway ด้วย Spring Cloud Netflix Zuul โดยกำหนด router ให้ไปแต่ละ Service ที่ register กับ Service Discovery
สร้าง Gateway project ด้วย dependency Zuul
เพื่อสร้าง API Gateway และ Eureka Discovery Client
เพื่อให้ register Gateway กับ Service Discovery Server และ Config Client
เพื่อใช้ config จาก Config Server
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
ใช้ @EnableZuulProxy
annotation เพื่อกำหนดให้ Application เป็น API Gateway และ @EanbleDiscoveryClient
เพื่อ register กับ Service Discovery
สร้าง bootstrap properties เพื่อกำหนด config name และ Service Discovery URI และ สร้าง application properties ใน Config Server โดยกำหนด Zuul router ให้ไปยัง Microservice ต่างๆ พร้อมทั้งกำหนด prefix เป็น /api
และ path ของแต่ละ Service
ดังนั้นเมื่อ request หา Employee Service ด้วย http://localhost:8081/employees
ที่ต้องกำหนด port ของ Employee Service เมื่อ request ผ่าน API Gateway ก็จะ request ด้วย http://localhost:8080/api/employees
ซึ่งเป็น port และ prefix ของ Gateway ผ่าน Zuul proxy ไปหา Employee Service และ Service อื่นก็เช่นเดียวกัน
Service-to-Service
ความสัมพันธ์ของ Microservice ทั้ง 3 คือสามารถ request ข้อมูลกันได้ผ่าน RESTful API ซึ่งใน Spring Cloud จะใช้ Spring Cloud OpenFeign เป็น REST Client โดยเพิ่ม dependency OpenFeign
ใน Service ที่ต้องการ request ข้อมูลจาก Service อื่น ด้วย RESTful API
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
ซึ่งในที่นี่คือ Department Service จะ request ไปยัง Employee Service เพื่อ query Employee ของ Department นั้นๆ โดยใช้ @EnableFeignClients annotation เพื่อกำหนดว่า Application นี้จะใช้ OpenFeign client
สร้าง interface EmployeeClient
เพื่อกำหนด method ที่ request ไปหา Employee Service โดยระบุชื่อของ Service ที่ register กับ Service Discovery ด้วย annotation @FeignClient
ในระดับ class และ mapping HTTP method ที่ระดับ method โดยระบุ URL ของ endpoint ที่ query ด้วย Department ID ด้วย @GetMapping
ใน findDepartmentById
method ของ DepartmentService เมื่อได้ Department แล้วก็ใช้ EmployeeClient เพื่อ request ไปยัง Employee Service เพื่อ query Employee ทั้งหมดของ Department นี้
เมื่อใช้ Postman request ไปยัง Department Service ผ่าน API Gateway ก็จะได้ Department พร้อมด้วย Employee ของ Department นี้
สำหรับ Organization Service ก็เช่นเดียวกันจะ request ไปทั้ง Employee Service และ Department Service เพื่อ query data ของ Oraganization
Circuit Breaker
เมื่อมีการ request กันระหว่าง Service ถ้าอีก Service ไม่ได้ทำงานจะเกิด exception error ถ้าไม่ได้เขียน handle ไว้ดีๆ แล้ว Service นั้นก็จะพังได้ ดังนั้นใน Spring Cloud มี Circuit Breaker โดยใช้ Netflix Hystrix เพื่อทำให้ Service มีคุณสมบัติ fault tolerance ไม่พังได้ง่ายๆ จากกรณีนี้
โดยเพิ่ม dependency Hystrix
ใน Service ที่ต้องใช้ Circuit Breaker และใช้ annotation @EnableCirecuitBreaker
เพื่อกำหนดให้ Application รองรับ Circuit Breaker และใช้ annotation @HystrixCommand กับ method ที่ต้องการจัดการกับ error โดยกำหนด fallbackMethod
ที่จะใช้เมื่อมี error จากการ request เกิดขึ้น
ในนี้คือถ้า Employee Service ไม่ได้ทำงาน แต่ Department Service จะต้อง request ไปหา Employee Service เพื่อ query ข้อมูล ก็จะทำให้เกิด error เมื่อเกิด error ก็จะไปเรียกใช้ fallbackMethod นั้นก็คือ findDepartmentByIdRecovery
ซึ่งจะไม่ไป request ไปที่ Employee Service
Client Side Load Balance
เนื่องจาก Employee Service ถูก request จาก Department Service แต่ถ้า Employee Service มีการ scale เป็น N Instance และมี PORT ที่แตกต่างกัน ดังนั้น Client Side Load Balance จะมาช่วยกระจาย request ให้ไปยัง N Instance โดย Spring Cloud Netflix Ribbon ด้วยการเพิ่ม dependency Ribbon
เข้าไปใน Department Service
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
แต่ก่อนอื่น จะต้องทำให้ Employee Service สามารถ scale ได้ 2 Instance (มากกว่า 1 Instance) ก่อน ด้วยการแก้ application properties ของ Employee Service ที่อยู่ใน Config Server ให้สามารถ override Server PORT ได้ก่อน
และ run Employee Service อีก Instance ในอีก PORT ด้วยคำสั่ง run Spring Boot
mvn spring-boot:run -Dserver.port=8084
และเมื่อดูใน Eureka Service Discovery จะมองเห็น EMPLOYEE-SERVICE
มีสถานะ UP อยู่ 2 Instance ที่ PORT 8081
และ 8084
ใน DepartmentService
ที่ request ไปยัง Employee Service การใช้ Ribbon ร่วมกับ Eureka นั้นทำได้โดยการใช้ @RibbonClient
annotation และกำหนดชื่อของ Service ที่จะให้ Load Balance ไปหา นั้นก็คือ employee-service
เมื่อ rerun Department Service และลอง request ดูจะเห็นได้ว่า ใน Employee Service มี call DynamicServerListLoadBalancer
เพื่อเอา service list จาก Eureka
อาจะยังไม่เห็นภาพ ลองเพิ่ม log เข้าไปใน method findEmployeeByDepartmentId
ของ classEmployeeController
เพื่อแสดงว่า method ถูกเรียกใช้จาก Department Service
เมื่อ rerun Employee Service ทั้ง 2 Instance และลอง request จาก Department Service จะเห็นว่า Ribbon Client Load Balance ทำการสลับกันระหว่าง 2 Instance
Distributed Tracing
เมื่อมี service-to-service call แล้ว การที่จะตรวจสอบการ request ของแต่ล่ะ service ดังนั้น distributed tracing จึงเข้ามาช่วยในส่วนของตรงนี้ ซึ่งใน Spring Cloud ก็รองรับด้วย Spring Cloud Sleuth Zipkin เป็นการใช้งาน Zipkin ที่เป็น Distributed Tracing และ Sleuth ที่เป็น tool สำหรับ generate trace id, span id และข้อมูลต่างๆ สำหรับ service call
ในบทความนี้จะ run Zipkin Server ด้วย docker โดยใช้คำสั่งนี้
docker run -d -p 9411:9411 openzipkin/zipkin
และเข้าถึง http://localhost:9411
ใช้ dependency Zipkin Client
เพือให้ service ติดตามได้จาก Zipkin System
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
และเพิ่ม Sleuth sampler ใน application config ที่อยู่ใน Config Server ของแต่ละ service โดยเปลี่ยน sleuth.sampler.probability
เป็น 1.0 (100%) โดย default เป็น 0.1 (10%)
เมื่อ rerun Service และ request ผ่านทาง Department Service ซึ่งจะมีการ call Employee Service ด้วย และดูใน Zipkin Server (อันนี้เปลี่ยนเป็น Lens UI) จะแสดง request ทั้งหมดที่เข้ามาใน Service และค้นหาเฉพาะ department-service
ก็จะแสดงเฉพาะ Department Service
ซึ่ง Zipkin จะแสดงระยะเวลาการ request ลำดับของ request ไปในแต่ละ service พร้อมทั้งเวลาและรายละเอียดของแต่ละ service ด้วย และเมื่อ request fail ก็จะแสดงรายละเอียดของ error นั้นๆ ด้วย และมี tracing ID ที่ generate จาก Sleuth ทำให้ติดตามวิเคาระ request ที่มีปัญหาได้
Code ของบทความนี้ใน repo นี้นะครับ
สรุป
จากการที่ได้ใช้ Spring Boot + Spring Cloud โดยเฉพาะที่เป็น Netflix OSS สร้างระบบ Microservice นั้น จะเห็นได้ว่า Spring Cloud มีเครื่งมือเพียบพร้อมสำหรับ Microservice ทำให้เราสามารถสร้าง Cloud Native Service ได้อย่างง่ายและรวดเร็ว แต่อาจจะต้องใช้เวลาสักหน่อยในการที่จะสร้างทั้งหมดนี้ขึ้นมา แม้บทความนี้จะครอบคลุมแค่บางส่วนของ Spring Cloud ที่มีมากกว่านี้ แต่ก็เพียงพอที่จะทำให้ระบบ Microservice ของเราไม่เป็นเพียงแค่ RESTful Web Service ที่ทำงานเพียง service เดียวและไม่สามารถตรวจสอบได้