สื่อสารระหว่าง Microservice ด้วย RabbitMQ + Spring AMQP
การสื่อสารระหว่าง Service ของ Microservice นั้นมีได้หลายวิธี มีทั้ง Synchronous ที่ sender ต้องรอให้ receiver ดำเนินการเรียบร้อยและตอบมาถึงจะสิ้นสุดการสื่อสาร หรือ Asynchronous ที่ไม่ต้องรอให้ receiver ดำเนิการเรียบร้อยเพียงแต่ตอบกลับมาว่าได้รับ message แล้ว หลังจากนั้น receiver ค่อยดำเนินการต่อไปจนเสร็จสิ้น เป็นสถาปัตยกรรม Event-Driven
Message Queue ก็เป็นการสื่อสารแบบ Asynchronous แบบหนึ่งที่นิยมแพร่หลายเป็นอย่างมาก ซึ่งก็มีหลากหลายเจ้าที่เสนอระบบนี้ และ RabbitMQ ก็เป็นตัวหนึ่งที่มีคนใช้มากและง่าย ดังนั้นในบทความนี้จะนำเสนอการใช้งาน RabbitMQ ร่วมกับ Spring AMQP ในการสือสารของ Microservice ที่สร้างด้วย Spring Boot
RabbitMQ คืออะไร
เป็นซอฟต์แวร์ message broker ตัวหนึ่งที่ open source โดยเริ่มแรกสร้างด้วยโปรโตคอล AMQP (Advanced Message Queuing Protocol) แต่หลังๆ ได้ขยายไปรองรับ STOMP, MQTT และ โปรโตคอลอื่นๆ อีกด้วย
ซึ่ง message broker จะทำหน้าที่ รับ (accept) และ ส่งต่อ (forward) message เปรียบได้กับไปรษณีย์ เมื่อเราส่งจดหมายที่ต้องการไปที่ตู้ไปรษณีย์ ก็จะมีเจ้าหน้าที่ไปรษณีย์มาเก็บจดหมายของเราแล้วส่งไปยังปลายทางตามที่เราจ่าหน้าซองไว้ ซึ่ง RabbitMQ เป็นทั้งกล่องไปรษณีย์ ที่ทำการไปรษณีย์ และเจ้าหน้าที่ไปรษณีย์ เรียกได้ได้ว่าเป็นระบบไปรษณีย์เลยก็ว่าได้ แต่จะส่งข้อมูล (data — message) แทนที่จะเป็นจดหมายกระดาษ และใช้คำศัพย์ที่แตกต่างไป ดังนี้
Producing — เป็นการส่ง (sending) message และผู้ส่ง message เรียกว่า Producer
Queue — เป็นกล่องไปรษณีย์ซึ่งอยู่ภายใน RabbitMQ แม้ว่า message จะว่าผ่าน RabbitMQ แต่จะถูกเก็บไว้ใน Queue โดยที่ Queue จะผูกกับ host memory และ disk limit ซึ่งจำเป็นต้องมี buffer ขนาดใหญ่เพียงพอ ซึ่งหลายๆ producer สามารถส่ง message มาที่เพียงหนึ่ง Queue ได้และหลายๆ consumer สามารถรอรับข้อมูลจาก Queue ได้
Consuming — เป็นการรับ (receiving) message และผู้ที่รอรับ message เรียกว่า Consumer
แบ่งรูปแบบของการ Exchange ออกเป็น 3 รูปแบบ
- Direct exchange —จะส่ง message ไปยัง Queue โดยตรงด้วย message routing key
- Fanout exchange — จะส่ง message กระจายไปทุก Queue ที่เชื่อมโยงอยู่กัน exchange
- Topic exchange — จะส่ง message ไปยัง Queue โดยขึ้นอยู่กับ matching ระหว่าง message routing key และ pattern ที่เชื่อมโยง Queue กับ Exchange
Spring AMQP
เป็น project ที่ประยุกต์แนวคิดหลักของ Spring เพื่อพัฒนาระบบส่ง message ด้วยโปรโตคอล AMQP โดยมี “template” เป็น high-level abstraction สำหรับส่งและรับ message รองรับ POJO (Pain old Java Object) ด้วย “listener container” โดยรองรับ dependency injection และ declarative configuration ซึ่งจะมี 2 ส่วนคือ spring-amqp ที่เป็น AMQP-base abstraction และ spring-rabbit ที่เป็น RabbitMQ implementation โดยใช้ spring-boot-starter-amqp
dependency สำหรับ Spring Boot
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
Use Case
ในบทความนี้จะสร้างการสื่อสารระหว่าง Microservice ดังนั้นจะสร้าง 2 Service โดยที่ User จะ POST Customer data มาที่ Customer Service จะบันทึกข้อมูลของ Customer ใน H2 database แล้วก็ส่งข้อมูล Customer มายัง Email Service เพื่อทำการส่ง email ให้ Customer
Customer Service จะเป็นฝั่ง Sender (Producer) และ Email Service จะเป็นฝั่ง Receiver (Consumer) และส่งข้อมูลผ่าน RabbitMQ โดยสร้าง Queue และ Exchange Topic customer.topic
ด้วย routing key email.customer
ติดตั้ง RabbitMQ
เราสามารถติดตั้ง RabbitMQ ในเครื่องของเราด้วย ใน MacOS ด้วยคำสั่ง
$ brew update
$ brew install rabbitmq
และรัน RabbitMQ server ด้วยคำสั่ง
$ rabbitmq-server
หรือว่าจะรันจาก Docker ก็ได้ ด้วยคำสั่ง
$ docker run -d --rm \
-p 15672:15672 \
-p 5672:5672 \
-v rabbit-data:/data \
--hostname rabbit-service \
--name awesome-rabbit \
rabbitmq:3-management
และตรวจสอบ Docker log ของ RabbitMQ ด้วยคำสั่ง docker logs
$ docker logs awesome-rabbit
.....
Starting RabbitMQ 3.7.7 on Erlang 20.3.8.5
Copyright (C) 2007-2018 Pivotal Software, Inc.
Licensed under the MPL. See http://www.rabbitmq.com/## ##
## ## RabbitMQ 3.7.7. Copyright (C) 2007-2018 Pivotal Software, Inc.
########## Licensed under the MPL. See http://www.rabbitmq.com/
###### ##
########## Logs: <stdout>Starting broker...
2018-09-16 00:35:01.251 [info] <0.197.0>
node : rabbit@rabbit-service
home dir : /var/lib/rabbitmq
config file(s) : /etc/rabbitmq/rabbitmq.conf
cookie hash : 1wAGd4fKTt0iWB5FU2H4pQ==
log(s) : <stdout>
database dir : /var/lib/rabbitmq/mnesia/rabbit@rabbit-service
....
Sender Service (Producer)
สร้าง Customer Service ที่เป็น RESTful Service ด้วย Spring Initializr โดยเลือก dependency เป็น Web
Lombok
JPA
H2
และ RabbitMQ
เพื่อที่จะเป็น CRUD Service เก็บข้อมูลใน H2 local database
สร้าง Customer Data/Entity
Entity เป็นตัวแทนของข้อมูลของ Service ดังนั้นสร้าง Entity สำหรับ Customer โดยใช้ @Data annotation ที่จะสร้าง Getter/Setter ให้เราเอง และจะต้อง implement interface Serializable
เพราะว่าเราจะส่งข้อมูลผ่าน RabbitMQ ต้องการ Object ที่สามารถ Serialize ได้จากฝั่ง Sender และ Deserialize ในฝั่ง Receiver
สร้าง Customer Controller
บทความนี้จะสร้าง Microservice อย่างง่ายดังนั้น สร้าง endpoint /customers
เดียวโดย mapping กับ POST method รับ input เป็น Customer แล้วเรียก createService ของ CustomerService
ใน Service layer ที่จะส่ง message ไปหา RabbitMQ
สร้าง Customer Service
ใน Service layer สร้าง Customer Service เพื่อ insert Customer data ใน H2 โดยเรียก save
method ใน Customer Repository และส่งข้อมูล Customer เป็น message ไปยัง RabbitMQ ผ่านทาง sendEmail
method ของ MessageSender
สร้าง Customer Repository
โดยเป็น interface ที่ extends หรือขยายจาก CrudRepository เพื่อดำเนินการเกี่ยวกับ Database ซึ่งในที่นี้เป็น H2 database
สร้าง Sender Config
ในฝั่งของ Sender สร้าง Message configuration ที่กำหนดชื่อของ Topic Exchange และ Sender instance ที่เป็นคลาส MessageSender
โดยสร้างเป็น Bean ขึ้นมาเพื่อจะ Inject ในภายหลังของ Spring AMQP
สร้าง Message Sender
เป็นคลาสที่เป็นตัวส่ง message ไปหา RabbitMQ ด้วย RabbitTemplate ซึ่งจะส่งเป็น ชื่อ topic, key email.customer
และ Customer data โดยอาศัยโปรโตคอล AMQP
ตั้งค่า Application Config
เราตั้งค่าให้ RabbitMQ host เป็น localhost
และ port 5672
ซึ่งเป็น default port ของ RabbitMQ
Receiver Service (Consumer)
สร้าง Email Service ด้วย Spring Initializr โดยเลือก dependency เป็น Lombok
RabbitMQ
และ Mail
เพื่อที่จะเป็น Mail Service ที่จะรับ message และส่ง email หา Customer
สร้าง Customer Data
การที่จะรับข้อมูลจาก RabbitMQ ดังนั้นเราจะต้องมี data structure เดียวกันเพื่อการ Deserialize ข้อมูลกลับมาเป็น Object โดยการใช้ @Data annotation ที่จะสร้าง Getter/Setter ให้เรา และ implement interface Serialiable
สร้าง Message Config
ในฝั่งของ Receiver สร้าง Message configuration กำหนดชื่อของ Topic Exchange customer.topic
ให้ตรงกับฝั่ง Sender เพื่อที่จะรับข้อมูลได้ถูกต้อง และ Receiver instance ที่เป็นคลาสMessageReceiver
และ Subscribe Queue ด้วยการ binding ระหว่าง Topic Exchange และ Queue ด้วย routing key email.customer
โดยสร้างเป็น Bean ขึ้นมาเพื่อ Inject ในภายหลัง
สร้าง Message Receiver
เป็นคลาสที่เป็นตัวรับ (consuming) message จาก RabbitMQ โดยอาศัยโปรโตคอล AMQP โดยที่ใช้ @RabbitListener annotation คอยตรวจสอบ autoDeletedQueue
ว่ามี message เข้ามาหรือไม่ เมื่อได้รับ message แล้วจะส่ง email ไปหา Customer ด้วย sendMessageToCustomer
ของ Email Service
สร้าง Email Service
เพื่อที่จะส่ง email ไปหา Customer ด้วย email ที่มากับ Customer data ด้วยการ EmailService
interface โดยใช้ JavaMailSender
ตั้งค่า Application Config
กำหนดให้ server port แตกต่างจาก Customer Service เนื่องจากรันบนเครื่องเดียวกันและ กำหนดให้ส่งแบบ SMTP ของ Gmail ผ่าน port 587
ทดสอบ RabbitMQ
รันทั้ง Customer Service ในฝั่ง Sender และ Email Service ในฝั่ง Receiver สามารถตรวจสอบ connection ได้ด้วย Management Plugin localhost:15672
ใน Tab Queues
จะมี Queue ที่สร้างขึ้น
ใน Tab Exchanges
จะมี customer.topic
เป็น topic สำหรับ exchange message
ใน Tab Connection
จะมีการเชื่อมต่อจาก Receiver ที่ Listening message อยู่
ใช้ Postman ส่ง Customer data ด้วย POST method ไปที่ Customer Service ถ้าสำเร็จจะได้ status code 201 (Created)
เมื่อตรวจสอบหน้า Management ใน Tab Overview
จะสังเกตเห็นมี message ส่งมาที่ RabbitMQ
และ Tab Connections
จะมีการเชื่อมต่อจากฝั่ง Sender
สรุป
จากที่เราได้ทดลองสื่อสารระหว่าง Microservice ด้วย RabbitMQ กับ Spring AMQP นั้น ซึ่งเป็นการสื่อสารแบบ Asynchronous ที่ไม่ต้องรอให้ Receiver ทำงานเสร็จก่อนเพียงแต่ตอบกลับมาว่าได้รับ message แล้ว ในตัวอย่างของบทความนี้ แสดงให้เห็นว่าสามารถสื่อสารได้ด้วย Spring Boot ไม่อยาก โดยที่ User ส่งข้อมูลไปที่ Customer Service หลังจากที่ได้รับข้อมูลก็บันทึกข้อมูลใน Database และส่งไปที่ RabbitMQ โดยระบุ Queue และ Exchange Topic ด้วย Routing Key โดยที่ Email Service ได้ Subscribe ไว้กับ RabbitMQ ด้วยการ Listening ว่ามี message เข้ามาใน Queue แล้วหรือยัง ถ้ามีจะรับ (consuming) message ไปดำเนินการส่ง email หา Customer แต่ก็สามารถมีหลาย Sender/Receiver โดยแยก Exchange Topic หรือ Routing key เพื่อจะส่งไปที่ Receiver ที่แตกต่างกันได้