Giới thiệu về Microservices
Bài đăng này đã không được cập nhật trong 3 năm
Microservices hiện được quan tâm trong giới phần mềm, công nghệ với nhiều bài viết, blog, thảo luận, truyền thông, hội thảo. Kỳ vọng về khả năng của Microservice đang lên đỉnh giống như một xu thế. Ngược lại, một số người cho rằng, microservices không có gì mới lạ, chẳng qua nó là SOA (kiến trúc hướng dịch vụ) được đánh bóng, đổi tên mà thôi. Bài viết này sẽ giải thích tại sao công ty, tổ chức của bạn nên cân nhắc áp dụng kiến trúc microservices
1. Xây dựng ứng dụng một khối (monolithic applications)
Tưởng tượng bạn phải xây dựng một dịch vụ gọi taxi qua di động cạnh tranh với Uber và Hailo. Sau một số buổi họp thu thập yêu cầu và phân tích thiết kế, bạn sẽ chọn công nghệ (technology stack) rồi tạo dự án đại loại như Rails, Spring Boot, Play, hay Maven. Dự án này sẽ có kiến trúc chia khối lục giác (hexagol architecture) hoặc ít hơn. Kiến trúc đa diện giúp ứng dụng chuyên biệt mô hình dữ liệu và đầu vào, đầu ra.
Trong lõi của ứng dụng là business logic được thể hiện bởi các khối dịch vụ, đối tượng cho từng vùng nghiệp vụ (domain objects) và các sự kiện (events: khách đặt xe, khách hủy xe, xe nhận khách...) Xung quanh lõi là bộ chuyển đổi (adapter) ví dụ như kết nối vào cơ sở dữ liệu, gửi nhận thông điệp (messaging), web service hoặc giao diện web front end.
Mặc dù có cấu trúc module hóa hợp lý, nhưng ứng dụng kiểu này sẽ đóng gói và cài đặt thành một khối (monolithic). Mã chạy cụ thể tùy thuộc vào ngôn ngữ lập trình hay thư viện framework. Ví dụ ứng dụng Java đóng trong file WAR, triển khai trên application server như Tomcat hay Jetty. Dùng framework khác, thì ứng dụng Java là một file tự đóng gói để chạy là JAR. Ứng Rails hay Node.js đóng gói theo cấu trúc thư mục phân cấp.
Ứng dụng được viết kiểu trên đây rất phổ biến. Chúng dễ viết, dễ thử nghiệm, dễ copy & paste bởi các công cụ lập trình IDE và dự án mẫu được tối ưu để tạo ra ứng dụng khối đơn nhất. Có thể kiểm thử tự động giao diện web với Selenium. Ứng dụng khối đơn nhất khá dễ triển khai. PHP, Python thì chỉ cần cập nhật mã. Một số khác có chức năng hot reload như Node.js, Play Framework, Revel. Để tăng khả năng chịu tải thì bổ xung thêm web application server giống nhau sau bộ cân bằng tải (load balancer)
2. Điều hành hướng tới Monolithic Hell
Đáng tiếc rằng, cách tiếp cận kiến trúc một khối tuy dễ dàng nhưng lại bộc lộ nhiều khiếm khuyết.
Ứng dụng của bạn thành công, kéo theo đó số lượng người dùng tăng, yêu cầu tính năng mới tăng, dữ liệu tăng, logic phức tạp hơn, giao tiếp với hệ thống khác tăng, và hàng trăm thứ khác dẫn đến một kết quả là ứng dụng phình to ra một cách khủng khiếp. Sau mỗi sprint, hàng loạt tính năng mới được thêm vào, thêm code, thêm bảng, thêm logic… Chỉ sau một thời gian, ứng dụng đơn giản sẽ trở nên kềnh càng như một con quái vật. Và số lượng effort bỏ ra để phát triển cũng như bảo trì con quái vật này sẽ không hề nhỏ.
Khi ứng dụng phình quá to, mọi nỗ lực tối ưu, áp dụng agile method đều không còn hiệu quả. Chỉ một chỉnh sửa nhỏ, sẽ phải tham chiếu đến những chỗ nó sử dụng để xem xét sự ảnh hưởng của nó lên toàn bộ hệ thống. Nó quá khó để cho một lập trình viên có thể nắm và hiểu toàn bộ code hệ thống. Và như một hệ quả tất yếu, việc fix bug, hay thêm tính năng mới trở nên khó hơn và tốn nhiều thời gian hơn.
Trong ứng dụng một khối, sự chặt chẽ là ưu điểm tự nhiên xuất phát từ kiến trúc, nhưng nó tiềm ẩn nguy cơ ràng buộc cứng nhắc (tight coupling). Chi phí, thời gian, nỗ lực phát triển, sửa lỗi, kiểm thử một chức năng sẽ tăng tỷ lệ cấp số nhân theo độ lớn của ứng dụng.
Khi ứng dụng càng lớn, mỗi lần deploy phiên bản mới cũng sẽ là một cực hình, thậm chí tệ hại nếu thời gian down time cho việc khởi động lại là quá lớn. Điều này cũng ảnh hưởng khi lập trình viên đang debug ứng dụng, tưởng tượng một ứng dụng redeploy mất 10 phút thì trong 10 phút đó lập trình viên sẽ làm gì, rõ rành nó ảnh hưởng rất lớn đến năng suất làm việc.
Gần đây, bạn nghe nói nhiều hơn về triển khai đều đặn (continous deployment). Những ứng dụng SaaS (Software application as Service) tiên tiến, cần phải cập nhật vài lần trong một ngày. Quá khó để triển khai lại cả một ứng dụng cực lớn chỉ vì một số nâng cấp nhỏ. Hoạt động bị ngưng trệ, kiểm thử lại sau triển khai sẽ lâu công hơn. Kết quả là triển khai đều đặn khó áp dụng với ứng dụng một khối.
Khả năng mở rộng chịu tải ứng dụng một khối sẽ khó khi các thành phần khác nhau tranh chấp, dị biệt nhu cầu dùng tài nguyên hệ thống. Ví dụ module xử lý ảnh cần triển khai trên Amazon EC2 tối ưu CPU, sẽ khó cho module lưu bộ nhớ tạm (cache) cần rất nhiều bộ nhớ đang ra phải triển khai trên EC2 tối ưu bộ nhớ.
3. Microservice - đơn giản hóa sự phức tạp
Mỗi dịch vụ nhỏ thực hiện một tập các chức năng chuyên biệt như quản lý đơn hàng, quản lý khách hàng. Mỗi dịch vụ là một ứng dụng nhỏ có kiến trúc đa diện lõi là business logic kết nối ra các adapter khác nhau. Một số dịch vụ nhỏ lộ ra giao tiếp lập trình API cho dịch vụ nhỏ khác hay ứng dụng client gọi tới. Khi vận hành, mỗi dịch vụ nhỏ được chạy trong một máy ảo (virtual machine) hoặc Docker container (ảo hóa tầng ứng dụng).
Mỗi chức năng giờ được thực thị bởi một dịch vụ nhỏ. Ứng dụng web cũng có thể chia nhỏ hơn chuyên cho từng đối tượng người dùng (một cho hành khách taxi, một cho tài xế). Thiết kế giao diện cho từng đối tượng người dùng giúp tối ưu trải nghiệm tốt hơn, tốc độ nhanh hơn, dễ tương thích hơn trong khi chức năng tối giản hơn.
Ứng dụng của người dùng cuối sẽ không được kết nối trực tiếp vào back-end services. Thay vào đó có API gateway đứng giữa.
Kiến trúc microservice tương đương trục Y của 3 chiều mở rộng chịu tải (Scale Cube): trục X : bổ xung thêm web application sau bộ phân tải trục Y: chia nhỏ ứng dụng một khối thành nhiều dịch vụ nhỏ trục Z: phân vùng dữ liệu để xử lý song song trên từng vùng
Biểu đồ dưới mô tả làm thế nào dịch vụ quản lý chuyến đi có thể được triển khai với Docker chạy trên Amazon EC2.
Kiến trúc Microservices ảnh hưởng lớn đến quan hệ ứng dụng và cơ sở dữ liệu. Thay vì dùng chung một cơ sở dữ liệu giữa các dịch vụ, mỗi dịch vụ sẽ có cơ sở dữ liệu riêng. Cách này đi ngược lại việc tập trung hóa cơ sở dữ liệu trong truyền thống. Hệ quả là sẽ có dư thừa dữ liệu, cơ chế foreign key ràng buộc quan hệ dữ liệu không thể áp dụng với bảng ở hai cơ sở dữ liệu tách biệt. Thiết kế này sẽ gây sốc đối với nhiều lập trình đã quá quen với mô hình client – server, ở đó cơ sở dữ liệu luôn là một trung tâm, tập hợp mọi bảng. Việc lưu trữ dữ liệu ở từng dịch vụ rất quan trọng nếu bạn muốn kiến trúc microservice thực sự hiệu quả vì nó đảm bảo loose coupling (ít ràng buộc) giữa các dịch vụ với nhau. Nhưng đây vũng chỉ là khuyến cáo, trong thực tế, vài dịch vụ vẫn có thể dùng chung một cơ sở dữ liệu để đảm bảo tính toàn vẹn dữ liệu được ưu tiên cao nhất.
Sơ đồ dưới đây là ví dụ về kiến trúc cơ sở dữ liệu cho các dịch vụ.
Từng dịch vụ nhỏ có thể tùy chọn công nghệ lưu trữ dữ liệu tối ưu nhất. Ví dụ, dịch vụ điều xe cần phải dùng cơ sở dữ liệu hỗ trợ việc truy vấn theo tọa độ tốt nhất.
Nhìn một cách tổng thể, kiến trúc Microservices tương tự như SOA (kiến trúc hướng dịch vụ). Cả hai đều có một tập các dịch vụ. Điểm khác là Microservices không dùng chuẩn do các tập đoàn lớn như IBM, Microsoft, Oracle đặt ra như web service specifications (WS-*) hay Enterprise Service Bus (ESB). Microservices hướng đến các chuẩn lightweight hơn như Protobuf, Thrift hoặc cởi mở, dễ đọc như JSON. Microservice không áp dụng một số phần của SOA như canonical schema. Có thể thấy Microservices gọn hơn, đa dạng hơn trong giao thức cũng như chuẩn dữ liệu.
4. Ưu điểm của Microservices
- Giảm thiểu sự gia tăng phức tạp, rối rắm của hệ thống lớn.
- Chia nhỏ ứng dụng một khối cồng kềnh thành các dịch vụ nhỏ dễ quản lý, bảo trì nâng cấp, tự do chọn, nâng cấp công nghệ mới.
- Mỗi dịch vụ nhỏ sẽ định ra ranh giới rõ ràng dưới dạng RPC hay API hướng thông điệp.
- Microservice thúc đẩy tách rạch ròi các khối chức năng (loose coupling - high cohesion), điều rất khó thực hiện với ứng dụng một khối. Nếu muốn loose coupling - high cohesion trong ứng dụng một khối, sẽ phải thiết kế theo Design Pattern (Gang Of Four) và liên tục tái cấu trúc (refactor)
5. Nhược điểm của microservices
- Phải xử lý sự cố khi kết nối chậm, lỗi khi thông điệp không gửi được hoặc thông điệp gửi đến nhiều đích đến vào các thời điểm khác nhau.
- Đảm bảo giao dịch phân tán (distributed transaction) cập nhật dữ liệu (all or none) vào nhiều dịch vụ nhỏ khác nhau khó hơn rất nhiều, đôi khi là không thể so với đảm bảo giao dịch cập nhật vào nhiều bảng trong một cơ sở dữ liệu trung tâm.
- Theo nguyên tắc CAP (CAP theorem) thì giao dịch phân tán sẽ không thể thỏa mãn cả 3 điều kiện: consistency (dữ liệu ở điểm khác nhau trong mạng phải giống nhau), availablity (yêu cầu gửi đi phải có phản hồi), partition tolerance (hệ thống vẫn hoạt động được ngay cả khi mạng bị lỗi). Những công nghệ cơ sở dữ liệu phi quan hệ (NoSQL) hay môi giới thông điệp (message broker) tốt nhất hiện nay cũng chưa vượt qua nguyên tắc CAP.
- Kiểm thử tự động một dịch vụ trong kiến trúc microservices đôi khi yêu cầu phải chạy cả các dịch vụ nhỏ khác mà nó phụ thuộc. Do đó khi phân rã ứng dụng một khối thành microservices cần luôn kiểm tra mức độ ràng buộc giữa các dịch vụ mềm dẻo hơn hay cứng nhắc - lệ thuộc hơn. Nếu ràng buộc ít đi, lỏng leo hơn, bạn đi đúng hướng và ngược lại.
- Nếu các dịch vụ nhỏ thiết kế phục thuộc vào nhau theo chuỗi. A gọi B, B gọi C, C gọi D. Nếu một mắt xích có giao tiếp API thay đổi, liệu các mắt xích khác có phải thay đổi theo không? Nếu có thì việc bảo trì, kiểm thử sẽ phức tạp tương tự ứng dụng một khối. Thiết kế dịch vụ tốt sẽ giảm tối đa ảnh hưởng lan truyền đến các dịch vụ khác.
- Thực thế không phải dịch vụ nào trong microservice cũng sẽ giao tiếp với tất cả những dịch vụ còn lại. Nhưng nếu không có quy tắc phân luồng - quản lý - đo đếm - theo dõi (manage - meter - monitor) , thì số lượng kết nối giữa các dịch vụ nhỏ gia tăng tùy tiện, chất lượng kết nối không kiểm soát. Hệ thống chậm nhưng không thể biết đoạn nghẽn cổ chai ở đâu?
- Triển khai dịch vụ microservices nếu làm thủ công theo cách đã làm với ứng dụng một khối phức tạp hơn rất nhiều. Ứng dụng một khối bổ xung các server mới giống hệt nhau đằng sau bộ cần bằng tải. Trong khi ở kiến trúc microservice, các dịch vụ nhỏ nằm trên nhiều máy ảo hay Docker container khác nhau, hoặc một dịch vụ có nhiều thực thể phân tán ra nhiều. Trong dịch vụ đám mây, các máy ảo, docker container, thực thể có thể linh động bật tắt, dịch chuyển. Vậy cần thiết phải có một cơ chế phát hiện dịch vụ (service discovery mechanism) để cập nhật tự động địa chỉ IP và cổng, mô tả, phiên bản của mỗi dịch vụ.
5. Kết luận
Kiến trúc một khối sẽ hữu hiệu đối với ứng dụng đơn giản, ít chức năng. Nó bộc lộ nhiều nhược điểm khi ứng dụng phát triển lớn nhiều chức năng. Kiến trúc microservices chia nhỏ kiến trúc một khối ra các dịch vụ nhỏ. Microservices sẽ hiệu quả, phù hợp cho những ứng dụng phức tạp, liên tục phát triển nếu được thiết kế đúng và tận dụng các công nghệ quản lý, vận hành tự động.
Nguồn tham khảo: https://www.nginx.com/blog/introduction-to-microservices/
All rights reserved