5. Kiến trúc phần mềm
5.1 Kiến trúc phần mềm là gì?
Kiến trúc phần mềm là bản thiết kế tổng thể của một hệ thống, giúp chúng ta hiểu rõ các bộ phận cấu thành, cách chúng kết nối và các quy tắc chi phối toàn bộ cấu trúc. Nó giống như bản vẽ kỹ thuật của một tòa nhà lớn, như: tòa nhà có bao nhiêu tầng, vật liệu chính là gì, hệ thống điện nước chạy như thế nào; mô tả các bộ phận (móng, cột, tường, mái), cách chúng được liên kết (hệ thống ống nước, dây điện) và mục tiêu chất lượng (chịu được động đất, tiết kiệm năng lượng).
Kiến trúc phần mềm trả lời câu hỏi: "Chúng ta nên xây dựng hệ thống này như thế nào?" thay vì "Hệ thống này làm được gì?" (câu hỏi của URS).
Kiến trúc phần mềm mô tả ba yếu tố chính của một hệ thống:
- Cấu trúc (structure): Hệ thống được chia thành các thành phần (components) hoặc mô-đun nhỏ hơn (ví dụ: mô-đun quản lý người dùng, mô-đun xử lý thanh toán)
- Tương tác (interaction): Các thành phần kết nối và trao đổi dữ liệu với nhau bằng cách nào (ví dụ: dùng API)
- Yếu tố chất lượng (quality attributes / NFR-Non-Functional Requirements): Đây là các yêu cầu phi chức năng mà kiến trúc phải có. Ví dụ: tốc độ, bảo mật, khả năng mở rộng, độ tin cậy
Vai trò quan trọng của Kiến trúc phần mềm:
- Tính ổn định: Đảm bảo hệ thống vững chắc, không bị sụp đổ khi lượng người dùng tăng lên
- Tính dễ hiểu: Giúp các lập trình viên “mới” hoặc các nhóm khác nhau dễ dàng nắm bắt được cách hệ thống hoạt động, từ đó đẩy nhanh quá trình phát triển, nâng cấp và sửa lỗi
- Tính mở rộng: Cho phép thêm tính năng hoặc tăng khả năng chịu tải một cách dễ dàng mà không cần phải phát triển lại toàn bộ hệ thống
5.2 Một số mô hình Kiến trúc phần mềm phổ biến
Có nhiều mô hình Kiến trúc phần mềm khác nhau, mỗi mô hình phù hợp với những yêu cầu và quy mô dự án cụ thể.
Bảng sau tóm tắt một số mô hình Kiến trúc phần mềm phổ biến:
Chúng ta cùng tìm hiểu sơ lược 3 mô hình kiến trúc phần mềm ở trên.
5.3 Kiến trúc nguyên khối
Kiến trúc nguyên khối là mô hình truyền thống, trong đó toàn bộ ứng dụng được xây dựng và triển khai dưới dạng một khối duy nhất. Tất cả các thành phần của ứng dụng như giao diện người dùng, logic nghiệp vụ và truy cập dữ liệu đều được tích hợp chặt chẽ trong cùng một ứng dụng.
Xem hình minh họa:
Trong đó:
- Giao diện người dùng (user interface)
- Logic nghiệp vụ (business layer)
- Truy cập dữ liệu (data interface)
Xem hình ảnh này để bạn dễ tưởng tượng hơn về kiến trúc monolithic:
Ưu điểm:
- Quá trình phát triển, triển khai đơn giản: Do tất cả nằm trong một khối duy nhất, việc phát triển, thử nghiệm và triển khai sẽ đơn giản hơn.
- Hiệu suất cao: Do không có giao tiếp liên bộ phận, ứng dụng thường có hiệu suất cao hơn.
Nhược điểm:
- Khó bảo trì và mở rộng: Khi ứng dụng phát triển lớn hơn, việc bảo trì và thêm tính năng mới trở nên phức tạp
- Khả năng chịu lỗi kém: Một lỗi nhỏ có thể làm gián đoạn toàn bộ hệ thống
Khi nào thì nên sử dụng mô hình nguyên khối:
- Dự án nhỏ, cần triển khai nhanh
- Đội ngũ phát triển ít thành viên
- Tài nguyên, máy móc ít
- Việc giao tiếp giữa các thành phần của ứng dụng cần hiệu suất cao
Ví dụ về các ứng dụng theo kiến trúc nguyên khối:
- Hầu hết các ứng dụng desktop truyền thống đều được thiết kế theo kiểu kiến trúc nguyên khối. Kiến trúc nguyên khối rất phù hợp với môi trường desktop vì toàn bộ ứng dụng chạy cục bộ trên một máy tính duy nhất.
- Hệ quản trị nội dung (CMS) cũ (wordpress), ứng dụng thương mại điện tử nhỏ
5.4 Kiến trúc N-tầng
Mô hình N-tầng (N-tiers) phân chia ứng dụng thành các tầng độc lập, chạy trên các máy chủ hoặc dịch vụ khác nhau. Sự phân tách này đạt được thông qua việc giới hạn sự giao tiếp: mỗi tầng chỉ được phép giao tiếp với tầng liền kề nó.
Mô hình N-tầng phổ biến nhất là Kiến trúc 3-tầng (3-tier architecture).
Kiến trúc 3-tầng
Kiến trúc 3-tầng là một mô hình kiến trúc phần mềm phổ biến. Nó được thiết kế để phân chia ứng dụng thành ba đơn vị logic và vật lý riêng biệt, giúp tăng tính linh hoạt, khả năng mở rộng, và bảo mật.
Kiến trúc 3-tầng được hình thành bằng cách tách ba chức năng chính của ứng dụng thành các tầng (tier) độc lập, thường chạy trên các máy chủ vật lý hoặc logic khác nhau.
Ba tầng đó là:
- Trình bày (presentation)
- Logic nghiệp vụ (business logic)
- Dữ liệu (data)
Xem hình minh họa:
Cụ thể hơn:
Ưu điểm:
- Khả năng tái sử dụng: Logic nghiệp vụ nằm ở tầng giữa, độc lập với tầng trình bày. Vì vậy, một logic nghiệp vụ có thể được sử dụng bởi nhiều giao diện khác nhau. Ví dụ: một API xử lý thanh toán có thể được gọi từ cả ứng dụng web và ứng dụng di động
- Khả năng mở rộng: Đây là ưu điểm lớn nhất so với kiến trúc nguyên khối. Bạn có thể mở rộng độc lập theo từng tầng. Ví dụ: thêm máy chủ cho tầng Logic nghiệp vụ khi có nhiều yêu cầu API hơn, mà không cần thêm máy chủ cho tầng Dữ liệu
- Có tính bảo mật cao: Tầng Dữ liệu được cô lập, chỉ giao tiếp với tầng Logic nghiệp vụ, mà không giao tiếp với người dùng cuối. Điều này giúp bảo vệ cơ sở dữ liệu khỏi các truy cập trái phép từ bên ngoài
- Dễ phát triển và bảo trì: Vì mỗi tầng có trách nhiệm rõ ràng, các lập trình viên có thể tập trung vào một tầng cụ thể mà không làm ảnh hưởng đến các tầng khác. Đội Frontend có thể làm việc song song với đội Backend, chỉ cần tuân thủ giao diện API
Nhược điểm:
- Phức tạp: Việc thiết lập và quản lý kiến trúc phân tán nhiều tầng phức tạp hơn so với kiến trúc nguyên khối
- Độ trễ: Việc truyền dữ liệu qua nhiều tầng (qua mạng) có thể làm tăng độ trễ (latency) so với việc gọi hàm cục bộ
- Chi phí: Việc duy trì nhiều máy chủ/dịch vụ vật lý hoặc máy ảo cho mỗi tầng sẽ tốn kém hơn
Khi nào thì nên sử dụng kiến trúc 3-tầng:
- Khi ứng dụng của bạn phức tạp, dự kiến phát triển lớn trong tương lai, cần khả năng mở rộng cao và bảo mật dữ liệu được ưu tiên
5.5 Kiến trúc vi dịch vụ
Kiến trúc vi dịch vụ (microservices) là một phương pháp thiết kế phần mềm, trong đó một ứng dụng lớn được chia thành một tập hợp các dịch vụ nhỏ, độc lập. Nó là một kiến trúc phần mềm phân tán.
Một số đặc điểm:
- Tính độc lập: Mỗi dịch vụ (service) chạy trong tiến trình (process) riêng biệt của nó
- Chức năng chuyên biệt: Mỗi dịch vụ tập trung vào việc thực hiện một chức năng nghiệp vụ cụ thể (ví dụ: dịch vụ quản lý người dùng, dịch vụ xử lý thanh toán, dịch vụ kho hàng)
- Giao tiếp: Các dịch vụ giao tiếp với nhau qua mạng bằng các giao thức nhẹ, phổ biến nhất là RESTful API hoặc message broker (hàng đợi tin nhắn)
- Công nghệ đa dạng (polyglot): Các dịch vụ khác nhau có thể được xây dựng bằng các ngôn ngữ lập trình, framework và thậm chí là cơ sở dữ liệu riêng (independent database) phù hợp nhất với yêu cầu của dịch vụ đó
Mô hình này trái ngược hoàn toàn với kiến trúc nguyên khối (monolithic), nơi mọi thứ được đóng gói thành một đơn vị duy nhất.
Xem hình minh họa.
Ưu điểm:
- Khả năng mở rộng độc lập: Có thể mở rộng từng dịch vụ riêng biệt dựa trên nhu cầu tải thực tế, giúp tối ưu hóa tài nguyên (ví dụ: chỉ cần thêm server cho dịch vụ giỏ hàng mà không cần thêm server cho dịch vụ tài khoản)
- Linh hoạt công nghệ: Cho phép các nhóm sử dụng công nghệ (tech stack) tốt nhất cho từng dịch vụ cụ thể (polyglot programming)
- Dễ dàng bảo trì và phát triển: Mã nguồn của mỗi dịch vụ nhỏ và dễ hiểu hơn. Các nhóm phát triển có thể làm việc, triển khai và cập nhật dịch vụ của họ độc lập với các dịch vụ khác (decoupling)
- Khả năng chịu lỗi cao: Nếu một dịch vụ bị lỗi, các dịch vụ khác vẫn có thể tiếp tục hoạt động (fault isolation). Hệ thống không bị sập toàn bộ như trong monolithic
- Triển khai liên tục (CI/CD): Dễ dàng áp dụng DevOps và CI/CD vì chỉ cần triển khai lại các dịch vụ nhỏ đã thay đổi, không cần triển khai toàn bộ ứng dụng lớn
Nhược điểm:
- Phức tạp về vận hành: Việc quản lý, giám sát (monitoring) và gỡ lỗi (debugging) một hệ thống gồm hàng chục hoặc hàng trăm dịch vụ phân tán phức tạp hơn nhiều
- Độ trễ mạng (latency): Các dịch vụ giao tiếp qua mạng (API call) thay vì gọi hàm cục bộ, dẫn đến độ trễ cao hơn so với monolithic
- Quản lý dữ liệu phân tán: Xử lý giao dịch trải dài qua nhiều cơ sở dữ liệu (distributed transactions) rất phức tạp và cần các mẫu thiết kế như Saga
- Chi phí cơ sở hạ tầng: Cần nhiều tài nguyên hơn cho việc chạy nhiều tiến trình, nhiều cơ sở dữ liệu và các công cụ quản lý phức tạp (kubernetes, service mesh, API gateway)
- Kiểm thử tích hợp: Việc đảm bảo tất cả các dịch vụ phối hợp nhịp nhàng với nhau khó khăn hơn so với kiểm thử end-to-end trong monolithic
Khi nào thì nên sử dụng mô hình microservices:
Microservices không phải là giải pháp cho mọi vấn đề. Nó đặc biệt phù hợp khi dự án đạt đến một mức độ phức tạp và quy mô nhất định:
- Hệ thống phức tạp và quy mô lớn: Khi ứng dụng có nhiều chức năng nghiệp vụ riêng biệt và mã nguồn (codebase) trở nên quá cồng kềnh
- Yêu cầu khả năng mở rộng cao và độc lập: Khi một số chức năng (ví dụ: tìm kiếm, streaming) có nhu cầu tải (load) cao hơn nhiều so với các chức năng khác
- Tổ chức lớn với nhiều đội (team autonomy): Khi các nhóm khác nhau có thể sở hữu, phát triển và triển khai dịch vụ của riêng họ mà không bị phụ thuộc vào nhau
- Cần triển khai nhanh chóng và thường xuyên: Khi yêu cầu cập nhật, thêm tính năng mới liên tục và cần thời gian ngưng hệ thống (downtime) tối thiểu
- Có kinh nghiệm vận hành (DevOps maturity): Đội ngũ phát triển đã có kinh nghiệm với các công cụ như Docker, Kubernetes, CI/CD, và các hệ thống giám sát phân tán
5.6 Bài tập
Bài 5a. Dựa trên URS ứng dụng của bạn (nhóm), hãy phân tích theo kiến trúc 3-tầng. Bạn có thể tự phân tích hoặc nhờ sự hỗ trợ của chatbot.
Bài 5b. Dựa trên URS ứng dụng của bạn (nhóm), hãy phân tích theo kiến trúc vi dịch vụ. Bạn có thể tự phân tích hoặc nhờ sự hỗ trợ của chatbot.
[Gợi ý làm bài tập]