สร้าง Microservices ด้วย Spring Boot + Spring Cloud

Phayao Boonon
8 min readJul 18, 2019

--

Photo by Tyler Casey on Unsplash

วันก่อนเจอ 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-service
eureka:
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

Employee Service — Instance 1
Employee Service — Instance 2

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 เดียวและไม่สามารถตรวจสอบได้

--

--

Phayao Boonon
Phayao Boonon

Written by Phayao Boonon

Software Engineer 👨🏻‍💻 Stay Hungry Stay Foolish

Responses (1)