3.5.1.Mô hình hóa các microservices
Để xác định các microservices, chúng ta áp dụng kỹ thuật DDD như đã trình bày trong chương 1 của luận văn. Trước hết, chúng ta cần xác định các vùng ngữ cảnh riêng của hệ thống, thuật ngữ gọi là “Bounded Context”. Sau đó, chúng ta khoanh vùng các hành vi có mối quan hệ và cùng thực hiện một chức năng trong một miền nghiệp vụ lại với nhau. Dựa theo yêu cầu chức năng của CEMS như mô tả ở mục 3.2, ta xác định được sáu vùng ngữ cảnh chính của hệ thống gồm vùng chứa các hành vi liên quan đến người dùng (user context), vùng chứa các hành vi liên quan đến khách hàng (customer contex), vùng chứa các hành vi liên quan đến thiết bị (equipment context), vùng chứa các hành vi liên quan đến tài sản (asset context), vùng chứa các hành vi liên quan đến báo cáo (report context), và vùng chứa các hành vi liên quan đến việc xác thực, phân quyền của hệ thống (authentication context). Mỗi vùng nghiệp vụ đều chứa các hành vi cơ bản như tạo mới (create), cập nhật (update) và tìm kiếm dữ liệu (search). Riêng vùng nghiệp vụ quản lý việc xác thực, phân quyền chứa các hành vi như đăng nhập (login), đăng xuất (logout) và đổi mật khẩu (change password). Các phân vùng nghiệp vụ này được mô tả trong hình 3.4.
47
Hình 3.4. Phân vùng các hành vi trong hệ thống CEMS
Từ việc phân hoạch trên, hệ thống CEMS có sáu microservices gồm: user-service (quản lý thông tin người dùng), customer-service (quản lý thông tin khách hàng), equipment-service (quản lý thông tin thiết bị máy móc), asset-service (quản lý thông tin tài sản), reporting-service (kết xuất báo cáo), và authentication-service (quản lý việc xác thực và phân quyền). Dựa trên các phân vùng hành vi nghiệp vụ này, chúng ta cũng sẽ xác định được các micro-frontends tương ứng để quản lý các dịch vụ tương tác người dùng trong ứng dụng.
3.5.2.Kiến trúc tổng thể của CEMS
Kiến trúc hệ thống CEMS được mô tả trong hình 3.5. Tầng giao diện giao tiếp với các microservices qua cổng API và được thực hiện thông qua REST API. CEMS hỗ trợ chuẩn định dạng dữ liệu dưới dạng JSON. Chi tiết về cách cài đặt và nhiệm vụ của từng thành phần trong kiến trúc này sẽ được giải thích cụ thể trong các mục tiếp theo của luận văn.
48
Hình 3.5. Kiến trúc tổng thể hệ thống CEMS
3.5.3.Xây dựng mô hình dữ liệu
Để đảm bảo nguyên tắc triển khai độc lập và giảm thiểu sự phụ thuộc giữa các thành phần, mỗi microservice trong CEMS sẽ sử dụng một cơ sở dữ liệu riêng biệt. Dựa vào phân vùng nghiệp vụ của bài toán như mô tả ở mục 3.5.1, ta xác định được bốn lược đồ cơ sở dữ liệu chính dành cho các đối tượng gồm người dùng (user-db), khách hàng (customer-db), thiết bị (equipment-db) và tài sản (asset-db). Đối với tác vụ kết xuất báo cáo, dữ liệu sẽ được tổng hợp từ thông tin của khách hàng và tài sản, và được lưu riêng ở vùng dữ liệu dành cho báo cáo (reporting-db). Mô hình tổ chức dữ liệu cho hệ thống được thể hiện trong hình 3.6.
Sau khi có được mô hình cơ sở dữ liệu chung ở trên, chúng ta cần xây dựng cơ sở dữ liệu cho từng microservice. Để thực hiện điều này, trước hết chúng ta áp dụng kỹ thuật thiết kế hướng miền nhằm xác định các lớp thực thể chứa các thông tin cần quản lý liên quan đến từng miền nghiệp vụ của hệ thống, sau đó chúng ta cần xác định mối quan hệ giữa các thực thể dựa theo các quy tắc nghiệp vụ của bài toán để đưa ra được sơ đồ lớp cho các lớp thực thể này. Sơ đồ trong hình 3.7 minh họa mối quan hệ giữa các lớp thực thể trong module user-service.
49
Hình 3.6. Mô hình cơ sở dữ liệu của CEMS
Hình 3.7. Mối quan hệ giữa các lớp thực thể trong module user-service
Theo sơ đồ lớp trong hình 3.7, module user-service có bốn lớp thực thể tham gia bao gồm lớp vị trí (position) lưu thông tin về vị trí, chức danh người dùng; lớp nhóm quyền (group role) lưu thông tin về nhóm quyền, lớp phòng ban (department) lưu thông tin về các phòng ban và lớp người sử dụng ứng dụng (app user) lưu các thông tin liên quan đến người dùng (họ tên, địa chỉ, mật khẩu…). Các lớp thực thể này sẽ được ánh xạ với từng bảng trong cơ sở dữ liệu thông qua kỹ thuật ORM để chuyển đổi mô hình từ các lớp Java sang từng bảng tương ứng trong lược đồ cơ sở dữ liệu. Việc xây dựng cơ sở dữ liệu cho các microservice khác được thực hiện theo cách tương tự.
50
3.6. Thiết kế và cài đặt tầng dịch vụ
Trong phần này, luận văn tập trung trình bày cách thiết kế và cài đặt chi tiết của một microservice. Một số thành phần quan trọng trong tầng dịch vụ như cách xây dựng cổng API, thiết kế mô hình “đăng ký và khám phá dịch vụ” cũng sẽ được làm rõ.
3.6.1.Kiến trúc cụ thể của một microservice
Mỗi microservice phát triển bằng Spring Boot được tổ chức thành các thành phần cơ bản sau:
• Lớp điều khiển (controller): có nhiệm vụ đóng gói các API để kết xuất ra
bên ngoài cho các đối tượng sử dụng. Lớp này đón nhận các yêu cầu từ phía người dùng, tương tác với lớp dịch vụ và gửi trả lại dữ liệu cho người dùng thông qua REST API.
• Lớp dịch vụ (service): có nhiệm vụ đóng gói các logic nghiệp vụ của bài
toán dựa trên các quy tắc về nghiệp vụ. Nó thực hiện tương tác với kho lưu trữ để lấy dữ liệu và xử lý tập dữ liệu đó theo từng yêu cầu cụ thể. Một số tác vụ khác như kiểm tra tính hợp lệ của dữ liệu theo các quy tắc nghiệp vụ cũng có thể được thực hiện tại tầng này.
• Kho lưu trữ (repository): thực hiện tác vụ kết nối và tương tác với cơ sở dữ
liệu để truy xuất hoặc lưu trữ dữ liệu. Để thực hiện các yêu cầu này, kho lưu trữ sẽ phải liên kết với lớp thực thể. Trong ứng dụng Spring Boot, mỗi kho lưu trữ có thể được triển khai bằng Spring Data JPA33 hoặc dùng kết hợp với một số ORM framework khác như Hibernate34 hoặc MyBatis35.
• Lớp thực thể (entity): mỗi entity được gọi là lớp thực thể đại diện cho một
đối tượng cần quản lý trong miền nghiệp vụ của bài toán. Các thực thể này sẽ được ánh xạ với từng bảng trong cơ sở dữ liệu thông qua kỹ thuật ORM.
• Bộ lọc (filter): mỗi filter được gọi là một bộ lọc, nó đóng vai trò như một
thành phần trung gian thực hiện việc tiền xử lý các yêu cầu từ phía người dùng trước khi chúng được gửi tới lớp điều khiển.
• Thành phần “Bean Factory”: có chức năng quản lý việc khởi tạo các đối
tượng cũng như kiểm soát vòng đời hoạt động của các đối tượng trong ứng dụng.
• Thành phần “Bean Configuration”: được dùng để quản lý cấu hình các đối
tượng (beans) trong ứng dụng, ví dụ quản lý thông tin nguồn dữ liệu (data source), quản lý các thành phần để ánh xạ, chuyển đổi dữ liệu (object mapper) và bất cứ đối tượng nào do lập trình viên tự định nghĩa.
33 https://spring.io/projects/spring-data-jpa 34 https://hibernate.org/
51
• Thành phần “DTO” (Data Transfer Object)36: thực hiện việc trung chuyển dữ liệu giữa các tầng, người ta sử dụng các DTO. Mỗi một DTO có nhiệm vụ đóng gói dữ liệu từ tầng nghiệp vụ để kết xuất ra cho phía người dùng. Như vậy, bằng cách phân lớp như trên, mỗi một microservice được chia tách thành các gói riêng biệt. Các thành phần bên trong ứng dụng sẽ được gắn kết với nhau thông qua cơ chế “tiêm sự phụ thuộc” (DI37). Với cách tổ chức này, mỗi microservice đảm bảo được tính đơn nhiệm, độc lập và làm giảm sự phụ thuộc chặt giữa các thành phần. Tổng quan về các thành phần và mối quan hệ giữa chúng trong một microservice được mô tả trong hình 3.8.
Hình 3.8. Kiến trúc tổng thể của một microservice
3.6.2.Xây dựng cổng API
Để quản lý và xử lý tập trung các yêu cầu từ phía máy khách, CEMS sử dụng Zuul API Gateway, một thành phần được cung cấp bởi Spring Cloud38.
Zuul sử dụng Netflix Ribbon39 để triệu gọi các microservices và hỗ trợ sử dụng các thành phần như Eureka Client40, Client Load Balancer41, Caching42. Để thực hiện tiền
36 https://martinfowler.com/eaaCatalog/dataTransferObject.html 37 https://docs.oracle.com/javaee/7/tutorial/injection002.htm 38 https://spring.io/projects/spring-cloud 39 https://github.com/Netflix/ribbon 40 https://cloud.spring.io/spring-cloud-netflix/multi/multi__service_discovery_eureka_clients.html 41 https://spring.io/guides/gs/spring-cloud-loadbalancer/ 42 https://cloud.spring.io/spring-cloud-static/spring-cloud-aws/2.0.0.RELEASE/multi/multi__caching.html
52
xử lý các yêu cầu trước khi gọi tới các microservices, Zuul cung cấp một thành phần gọi là ZuulFilter (cơ chế hoạt động của nó về cơ bản giống như một ServletFilter43).
Một cổng API thực hiện các chức năng như mô tả trong hình 3.9:
• Chức năng định tuyến (routing): có nhiệm vụ tiếp nhận và điều hướng các yêu cầu từ phía client tới từng microservice cụ thể.
• Chức năng bảo mật (security): chức năng này được thiết lập kết hợp với Spring Security44.
• Chức năng lọc (filter): thực hiện việc lọc các yêu cầu.
• Chức năng ghi log: thực hiện logging.
Hình 3.9. Vai trò của cổng API trong CEMS
Để cấu hình một Zuul API Gateway, ta sử dụng các chỉ thị là @EnableEurekaClient và @EnableZuulProxy. Đoạn mã nguồn trong hình 3.10 minh họa cách cấu hình cho một Zuul API Gateway.
43 https://www.oracle.com/java/technologies/filters.html 44 https://spring.io/projects/spring-security
53
Hình 3.10. Cấu hình Zuul API Gateway
3.6.3.Xây dựng mô hình đăng ký và khám phá dịch vụ
Cơ chế đăng ký và khám phá dịch vụ được cài đặt thông qua Eureka Service Registry, một thành phần được cung cấp bởi Spring Cloud Netflix45. Quy trình hoạt động của thành phần này diễn ra như sau:
• Mỗi một microservice được định danh bởi một mã gọi là service-id.
• Mỗi một microservice sử dụng thành phần Eureka Client để đăng ký với Eureka Server. Quá trình này được thực hiện qua ba giai đoạn:
- Đăng ký: các dịch vụ được đăng ký thông qua mã định danh, địa chỉ máy và cổng (service-id, host, port).
- Cấp phát mới: hệ thống sử dụng cơ chế heartbeat để định kỳ đăng ký lại nhằm biết được dịch vụ còn hoạt động hay không.
- Lấy danh sách đăng ký: hoạt động này trả về danh sách thông tin (host, port) của các microservices đã đăng ký theo service-id.
Sơ đồ trong hình 3.11 mô tả cơ chế hoạt động ở trên giữa hai microservices là customer service và user service.
54
Hình 3.11. Cơ chế hoạt động của Eureka Service Registry
Sau khi các dịch vụ được đăng ký thành công vào Eureka Server, các yêu cầu từ phía client gửi đến tầng dịch vụ sẽ được thực hiện thông qua cổng API. Phía client không cần phải biết, cũng không cần phải ghi nhớ địa chỉ của từng microservice để gọi và thực hiện cân bằng tải. Các hoạt động này sẽ được thực hiện bởi cổng API. Như đã trình bày ở mục trước, Zuul API Gateway sẽ dùng Ribbon để gọi các microservices qua các thành phần như Eureka Client và Client Load Balancer, và các bước xử lý được thực hiện một cách tự động. Quá trình triệu gọi các microservices được minh họa trong hình 3.12.
Hình 3.12. Quy trình đăng ký và khám phá dịch vụ
Để cấu hình Eureka Server, ta sử dụng chỉ thị @EnableEurekaServer, ví dụ minh họa được thể hiện trong hình 3.13.
55
Hình 3.13. Cấu hình đăng ký dịch vụ sử dụng Eureka Server
Sau khi quá trình đăng ký dịch vụ thành công, các microservices được đăng ký vào Eureka Server. Thông tin các dịch vụ được đăng ký có thể được kiểm tra bằng cách truy cập vào màn hình trang chủ của Eureka Server như minh họa trong hình 3.14.
56
3.6.4.Quản lý cấu hình tập trung
Trong hệ thống bao gồm nhiều dịch vụ, khi triển khai ứng dụng chúng ta gặp phải một số vấn đề sau:
• Mỗi một dịch vụ đều có các thông số cấu hình đi kèm (ví dụ thông tin về cơ sở dữ liệu, thông tin mô tả cho các dịch vụ gọi là metadata…).
• Các thông tin cấu hình này nằm phân tán ở từng dịch vụ cho nên rất khó kiểm soát.
• Các dịch vụ thường sẽ dùng chung một số thông tin cấu hình, cho nên mỗi khi thay đổi thông số cấu hình ở một dịch vụ này thì sẽ phải thay đổi hàng loạt nội dung liên quan ở các dịch vụ khác.
• Mỗi khi thay đổi thông tin cấu hình, sẽ phải thực hiện việc nạp lại các cấu hình cho từng dịch vụ.
Vì các vấn đề nêu trên, các thông tin cấu hình cần phải được quản lý tập trung. Để cài đặt mô hình này, tác giả sử dụng Spring Cloud Config46. Spring Cloud Config cung cấp cơ chế quản lý tập trung cấu hình ứng dụng như sau:
• Thông số cấu hình có thể được lưu trữ ở một tệp tin vật lý hoặc là lưu tại một kho lưu trữ tập trung (gọi là git repo).
• Tệp tin cấu hình được lưu theo quy ước {service-id}.properties hoặc {service- id}.yml. Trong đó “service-id” là tên định danh của từng dịch vụ.
• Các tệp tin cấu hình được sử dụng theo nguyên tắc kế thừa, ví dụ: theo quy ước, tệp tin application.properties lưu thông tin cấu hình chia sẻ chung cho tất cả các dịch vụ, các tệp tin cấu hình riêng biệt cho từng dịch vụ sẽ thừa hưởng lại các thông số từ tệp chính này.
• Spring Cloud Config còn cung cấp cơ chế tự động nạp lại cấu hình (gọi là auto reload config) bằng cách sử dụng các thành phần như Spring Cloud Bus47
hoặc có thể kết hợp với một số công cụ khác như Rabbit MQ48
Mô hình triển khai quản lý cấu hình tập trung được minh họa trong hình 3.15.
46 https://cloud.spring.io/spring-cloud-config/reference/html/ 47 https://spring.io/projects/spring-cloud-bus
57
Hình 3.15. Quản lý cấu hình tập trung với Spring Cloud Config
3.6.5.Xây dựng chuẩn giao tiếp API
Để phía giao diện và phía dịch vụ giao tiếp được với nhau, hai bên cần có một thống nhất về chuẩn giao tiếp. Trong hệ thống CEMS, việc tương tác này được thực hiện thông qua REST API sử dụng giao thức HTTP và định dạng chuẩn dữ liệu là JSON.
Ở mô hình giao tiếp này, phía máy khách sẽ gửi một một yêu cầu tới máy chủ (http request), máy chủ xử lý yêu cầu và gửi trả lại một phản hồi (http response). Để cài đặt các thao tác nghiệp vụ, các API được quy định sử dụng một quy ước về cách dùng giao thức HTTP như sau:
Quy ước cho Http Request
• Dùng HTTP Post khi muốn tạo mới một tài nguyên.
• Dùng HTTP Put khi muốn cập nhật dữ liệu cho một tài nguyên.
• Dùng HTTP Get khi muốn thực hiện một truy vấn để lấy thông tin về tài nguyên.
• Dùng HTTP Delete khi muốn xóa một tài nguyên.
Quy ước cho Http Response
Mỗi một Http Response bao gồm hai thành phần quan trọng là mã trạng thái (http code) và nội dung (body). Mã trạng thái thể hiện trạng thái kết quả thực hiện sau khi phía máy chủ xử lý một Http Request, còn phần nội dung là thành phần chứa kết quả trả về cho phía người dùng. Thông thường, một số mã trạng thái phổ biến được sử dụng như mô tả trong bảng 3.2 [30].
58
Bảng 3.2. Một số mã HTTP thông dụng
Mã Http Nội dung Ý nghĩa
200 OK một yêu cầu được thực hiện thành công
201 Created một yêu cầu tạo mới một tài nguyên thành công 204 No Content một yêu cầu được thực hiện thành công, nhưng
không có nội dung trả về
400 Bad Request phía máy chủ không xử lý được yêu cầu 403 Forbidden yêu cầu không được phép chấp nhận 404 Not Found tài nguyên được yêu cầu không tìm thấy
406 Not Acceptable nội dung trao đổi giữa máy chủ với máy khách không đúng chuẩn (gửi sai định dạng dữ liệu…) 500 Internal Server Error Các mã thuộc nhóm 5xx liên quan tới những lỗi
thuộc về phía máy chủ
Để làm rõ cơ chế hoạt động của API, chúng ta xem xét một ví dụ sau: để lấy thông tin của một khách hàng theo mã khách hàng “C001”, phía máy khách sẽ gửi một yêu cầu theo phương thức Get đến tầng dịch vụ, máy chủ xử lý và gửi trả lại một phản hồi với mã trạng thái là 200 cùng với các dữ liệu liên quan.