Microservices

Bài viết sau dịch từ một bài báo của Martin Fowler Microservices

Định nghĩa một kiểu kiến trúc mới

Kiến trúc dịch vụ siêu nhỏ-"Microservice Architecture" phát triển nhanh chóng trong nhiều năm gần đây nhằm mô tả cách thiết kế phần mềm ứng dụng mà các dịch vụ có thể triển khai một cách độc lập. Mặc dù không có định nghĩa rõ ràng về kiểu kiến trúc này, chúng ta vẫn có thể kể đến rất nhiều đặc tính chung của các tổ chức, phạm vi nghiệp vụ, tính chất quản lý phân tán cũng như dữ liệu khi nói đến microservices.

"Microservices", vâng lại một khái niệm mới trong vô số khái niệm của kiến trúc phần mềm. Mặc dù theo quan điểm thông thường, chúng ta sẽ chỉ liếc qua và xem nhẹ nó, nhưng thực sự là chỉ một từ ngữ thôi cũng mô tả một phong cách thiết kế hệ thống hết sức lôi cuốn. Chúng tôi đã thấy được rất nhiều dự án sử dụng phong cách này trong những năm gần đây và đạt được hiệu quả đáng kể, thêm nữa là có những đồng nghiệp của chúng tôi coi nó như kiến trúc mặc định cho các ứng dụng thương mại. Tuy nhiên, thật đáng tiếc là không có nhiều thông tin mô tả phong cách này và cách thực hiện nó.

Một cách ngắn gọn, kiến trúc microservice là một cách tiếp cận nhằm phát triển một ứng dụng như một tập của các dịch vụ (service) nhỏ gọn, chạy theo một tiến trình riêng và giao tiếp với nhau thông qua các cơ chế gọn nhẹ, thường là qua giao thức HTTP với API truy cập đến tài nguyên hệ thống. Các dịch vụ này được xây dựng theo phạm vị nghiệp vụ của riêng nó và được triển khai hoàn toàn riêng biệt và tự động. Việc tập trung quản lý các dịch vụ được giảm tới mức tối thiểu, bởi chúng được viết bởi các ngôn ngữ lập trình khác nhau với hệ quản trị dữ liệu khác nhau.

Để giải thích phong cách của microservice, chúng ta cần so sánh nó với phong cách monolithic (nguyên khối). Một ứng dụng nguyên khối được xây dựng với một đơn vị duy nhất. Một ứng dụng thương mại thông thường được xây dựng với 3 thành phần chính: Phần cung cấp giao diện người dùng (bao gồm trang HTML, JAVASCRIPT chạy trên trình duyệt hoặc trên máy người dùng), cơ sở dữ liệu (gồm các bảng có quan hệ với nhau) và ứng dụng phía server. Ứng dụng phía server sẽ xử lý HTTP request, thực hiện xử lý đường dẫn, nhận và cập nhật dữ liệu trong database và xuất ra trang HTML cho trình duyệt. Ứng dụng phía server được xem là nguyên khối - vì nó chỉ là một ứng dụng duy nhất. Bất cứ thay đổi nào đến hệ thống cần triển khai một phiên bản mới cho ứng dụng phía server.

Những server nguyên khối như vậy là cách tiệp cận tự nhiên khi xây dựng các hệ thống. Tất cả logic của hệ thống được chạy trên 1 tiến trình, cho phép chúng ta sử dụng đặc trưng cơ bản của ngôn ngữ lập trình để chia nhỏ các classes, functions và namespaces. Cẩn thận hơn một chút, chúng ta có thể chạy và kiểm tra ứng dụng trên máy tính của lập trình viên để đảm bảo ứng dụng chạy tốt trên môi trường production. Chúng ta có thể mở rộng ứng dứng theo chiều ngang bằng cách sử dụng nhiều server phía đưới 1 load-balancer.

Những ứng dụng nguyên khối có thể thành công, nhưng càng ngày càng nhiều người gặp thất bại với nó, nhất là với việc gia tăng ứng dụng được triển khai thông qua điện toán đám mây. Sự thay đổi bị gắn chặt lại với nhau, một thay đổi đến một phần nhỏ của ứng dụng khiến toàn bộ ứng dụng cần phải được triển khai lại. Theo thời gian, rất khó để giữ được một kiến trúc tốt, phân tách các mô-đun, khiến cho chúng ta khó có thể thay đổi mà chỉ ảnh hưởng đến một mô-đun. Việc mở rộng khiến chúng ta mở rộng toàn bộ ứng dụng, thay vì chỉ là mở rộng một phần của nó.

Figure 1: Monoliths and Microservices

Sự khác biệt giữa kiến trúc nguyên khối và Microservices

Các thất bại này hướng chúng ta đến kiến trúc microservice: Xây dựng ứng dụng bằng cách xây dựng nhiều dịch vụ. Ngoài việc mỗi dịch vụ có thể được triển khai và mở rộng một cách độc lập, nó có tạo ra biên giới cho các dịch vụ, khiến các dịch vụ khác nhau có thể viết bởi ngôn ngữ khác nhau. Chúng còn có thể được quản lý bởi các nhóm khác nhau.

Chúng tôi không có ý cho rằng microservice là một kiến trúc mới và đột phá, bởi nguồn gốc của nó quay về nguyên tắc thiết kế của Unix. Tuy nhiên chúng tôi cho rằng không nhiều người biết đến và hiểu kiến trúc microservice, do vậy chúng tôi nghĩ thấy sẽ tốt hơn nếu họ sử dụng nó.

Tính chất của kiến trúc Microservice

Chúng ta không thể đưa ra một định nghĩa chính thức cho kiến trúc microservice, nhưng chúng ta có thể mô tả những đặc tính của kiến trúc này. Với tất cả tính chất được nêu ra, không phải tất cả các kiến trúc đều có đầy đủ các tính chất đó, nhưng chúng ta mong chờ sẽ có thật nhiều kiến trúc microservice thoả mãn các đặc tính này. Chúng tôi, những người tác giả là thành viên của một công động mở, chia sẽ kinh nghiệm chúng tôi thấy được trong công việc của mình cũng như của những người chúng tôi biết.

Cấu thành từ các dịch vụ (Componentization via Services)

Trong suốt quá trình làm việc trong ngành phần mềm, chúng tôi luôn mong muốn xây dựng một hệ thống từ các thành phần khác nhau, giống như cái cách chúng ta thấy được trong đời sống thực tế. Trong khoảng hai thập kỷ gần đây, chúng tôi đã thấy được sự lớn mạnh không ngừng của các thư viện chung được xây dựng bởi các ngôn ngữ lập trình phổ biến.

Khi nói chuyện về thành phần, chúng ta khá khó để định nghĩa nó. Định nghĩa của chúng tôi là, thành phần - component là một đơn vị của phần mềm mà có thể thay thế và nâng cấp độc lập.

Kiến trúc microservice sẽ sử dụng thư viện, nhưng cách ban đầu tạo nên ứng dụng đó là việc chia nhỏ ra thành các dịch vụ - services. Chúng tôi định nghĩa thư viện - libraries là các thành phần mà liên kết với chương trình và được gọi thông qua lời gọi trong bộ nhớ, trong khi dịch vụ - services là thành phần hoạt động không liên quan đến tiến trình, thay vào đó tương tác với hệ thống thông qua một request trên web hoặc gọi thủ tục từ xa(đây là một khái niệm khác với khác niệm service object trong các chương trình hướng đối tượng).

Lý do chính để sử dụng dịch vụ thay vì thư viện, là bởi nó có thể được triển khai một cách độc lập. Nếu bạn có một ứng dụng bao gồm nhiều thư viện chạy trong một tiến trình, thay đổi đến bất cứ thành phần nào cũng khiến bạn phải triển khai lại toàn bộ ứng dụng. Nếu ứng dụng đó được tách ra thành nhiều dịch vụ, bạn có thể cho rằng thay đổi ở dịch vụ nào chỉ khiến mình nó phải triển khai lại. Điều này không hoàn toàn đúng, bởi thay đổi ở một dịch vụ có thể gây ra thay đổi cách thức hoạt động tầng giao diện khiến thay đổi các dịch vụ liên quan. Tuy nhiên mục tiêu của một ứng dụng dùng kiến trúc microservice là giảm thiệu sự phụ thuộc giữa các hệ thống và phát triển dựa trên giao kèo giữa các hệ thống (service contracts).

Một hệ quả tất yếu khác của sử dụng các dịch vụ là khiến cho tầng giao diện trở nên rõ ràng hết mức có thể. Hầu hết các ngôn ngữ đều không có một cơ chế rõ ràng cho việc định nghĩa một giao diện công khai rõ ràng. Thông thường nó được định nghĩa và kiểm soát để client không phá vỡ kiến trúc của các component, khiến chúng tự dưng bí dính chặt vào nhau. Việc sử dụng nhiều dịch vụ buộc chúng ta phải có một cơ chế giao tiếp rõ ràng.

Sử dụng các dịch vụ theo kiểu đó có nhiều hạn chế. Lời gọi đến bên ngoài luôn tốn kém hơn là gọi trong chính tiến trình, việc bắt buộc phải gọi các API đến nơi khác đôi khi khiến chúng ta bối rối. Khi chúng ta muốn thay đổi cách thức giao tiếp, kiến trúc này sẽ khiến chúng ta khó thực hiện hơn bởi chúng ta đang phải vượt qua rào cản giữa các tiến trình khác nhau.

Một cách gần đúng, chúng ta có thể coi các dịch vụ như các tiến trình được thực hiện trong thời gian chạy ứng dụng. Tuy nhiên đó mới chỉ là gần đúng. Một dịch vụ có thể bao gồm nhiều tiến trình luôn luôn được phát triển và triển khai cùng nhau, ví dụ như tiến trình của dịch vụ và cơ sở dữ liệu được sử dụng bởi dịch vụ đó.

Tổ chức theo năng lực nghiệp vụ

Thông thường, khi tách biệt ứng dụng ra nhiều thành phần, chúng ta thường tập trung vào phân tầng chức năng, tức là sẽ tách ra nhóm làm giao diện (UI), nhóm làm logic server và nhóm làm cơ sở dữ liệu. Khi chúng ta làm như vậy, chỉ cần một thay đổi đơn giản cũng khiến các nhóm cùng tốn thời gian và ngân sách. Một team khôn khéo sẽ tìm cách tối ưu và giảm thiểu tác hại, bằng cách chói buộc logic vào hệ thống mà nó truy cập tới. Nói cách khác là logic có ở mọi nơi. Đây là một ví dụ của luật Conway (Conway's Law).

Một tổ chức khi thiết kế một hệ thống (định nghĩa rộng rãi) sẽ tạo ra thiết kế mà cấu trúc của nó là bản sao của cấu trúc giao tiếp bên trong tổ chức đó

Melvyn Conway, 1967

conways-law.png

Ví dụ của luật Conway

Microservices có cách tiếp cận khác, nó tách thành các service nhỏ mà mỗi phần trong đó đều có khả năng hoàn thành một nghiệp vụ cụ thể. Mỗi dịch vụ thực hiện phát triển đầy đủ các chức năng cho phần nghiệp vụ của nó, bao gồm giao diện người dùng, lưu trữ dữ liệu và bất cứ thao tác với tổ chức bên ngoài nào khác. Do vậy, team trở thành đa chức năng (cross-functional), bao gồm đầy đủ các kỹ thuật cần thiết cho phát triển: Giao diện người dùng, cơ sở dữ liệu, quản lý dự án.

PreferFunctionalStaffOrganization.png

Ranh giới giữa các dịch vụ được xác định bởi ranh giới giữa các team

comparethemarket.com là một công ty tổ chức theo kiểu này. Các team đa chức năng có trách nhiệm xây dựng và quản lý sản phẩm, mà mỗi sản phẩm gồm nhiều dịch vụ giao tiếp với nhau thông qua các message.

Những ứng dụng nguyên khối lớn luôn được tách biệt thành các thành phần nhỏ dựa theo nghiệp vụ, mặc dù nó không quá phổ biến. Rõ ràng chúng ta có thể thúc đẩy một team lớn tách ra nhiều thành phần dựa theo nghiệp vụ. Nhưng vấn đề ở đây là việc quản lý sẽ bị phụ thuộc bởi quá nhiều yêu tố. Nếu một team nguyên khối trải ra nhiều mô-đun, nó rất khó để các thành viên có thể thực hiện tốt công việc. Thêm nữa, khoảng cách giữa các mô-đun buộc các thành viên phải tuân thủ rất nhiều quy tắc. Do vậy, việc tách biệt rõ ràng các thành phần là rất cần thiết để làm rõ sự tách biệt giữa các nhóm.

Sản phẩm, không phải dự án

Hầu hết quá trình phát triển ứng dụng đều theo mô hình dự án, có nghĩa là chúng ta cố gắng đưa ra một vài phần mềm được xem như đã hoàn thành. Sau khi kết thúc, phần mềm sẽ được bàn giao cho tổ chức bảo trì, và đội ngũ phát triển sẽ giải tán.

Kiến trúc microservice có thể phòng tránh được điều đó, bằng cách gắn vòng đời sản phẩm vào đội ngũ phát triển. Tư tưởng này được Amazon khơi dậy trong câu nói "You build, you run it" - "Bạn tạo nó, ban chạy nó", qua đó đội ngũ phát triển nằm toàn bộ trách nhiệm với sản phẩm của họ. Điều đó khiến cho các lập trình viên phải kiểm tra sản phẩm của nó hàng ngày, thường xuyên liên lạc với người dùng, hay nói cách khác là mang trên mình gánh nặng duy trì sản phẩm.

Về mặt tinh thần, sản phẩm có liên kết đới một nghiệp vụ nhất định. Thay vì xem phần mềm như một tập các chức năng hoàn thiện, có một mối quan hệ không ngừng diễn ra, nơi mà câu hỏi, làm cách nào để đem đến sản phẩm tốt nhất cho người dùng luôn thường trực.

Thực sự không có lý do gì để cách làm này không thể tiếp cận bởi kiến trúc nguyên khối, tuy nhiên, dịch vụ càng nhỏ bao nhiêu thì càng dễ dàng tạo lập mối quan hệ khăng khít bấy nhiêu giữa lập trình viên và người sử dụng dịch vụ.

Quan điểm cá nhân

Cá nhân tôi biết đến microservices từ nhiều nguồn khác nhau, ban đầu tôi khá xem nhẹ khái niệm này, vì từ cái tên "dịch vụ siêu nhỏ" khiến tôi cho rằng nó chỉ làm được cái nhỏ. Tuy nhiên, ý nghĩa thực sự của nó là việc tạo ra ứng dụng LỚN từ các dịch vụ NHỎ. Đây thực sự là một tư tưởng rất hay.

Cho đến thời điểm nay tôi chưa từng làm microservice bao giờ, cũng có thể vì tôi chưa làm dịch vụ LỚN bao giờ. Tôi chọn đọc bài việt của Martin Fowler với mong muốn tìm hiểu về đặc tính, kiến trúc và có thể là các best practises của microservices. Qua bài dịch hy vọng có thể chia sẽ đôi chút với bạn đọc.

Bài dịch trên mới là một nửa bài viết của Martin Fowler, và tôi sẽ dịch nửa còn lại trong thời gian gần nhất. Nửa đầu tiên nổi bật nhất là cho chúng ta thấy được sự khác biệt của kiến trúc nguyên khối - kiến trúc truyền thống nhất trong làm web, so với kiến trúc microservices. Nó cho chúng ta một khái niệm mới về cách tạo lập team. Phần cuối nói về sản phẩm, lại càng làm rõ hơn điều đó, khi chúng ta có một team với các cá nhân có đầy đủ khả năng cần thiết bổ trợ cho nhau, cùng nhau phát triển cùng nhau bảo trì sản phầm. Do việc tách biệt rõ ràng hệ thống, nên khi xảy ra lỗi cùng rất dễ dàng "đổ tội" cho dịch vụ bị lỗi, thay vì nhập nhằng giữa các mô-đun trong kiến trúc nguyên khối.

Nửa còn lại đi sâu vào chi tiết hơn khi nói đến sự phân tách trong kiến trúc database cũng như quy trình deploy tự động. Rất mong có thể đem đến bài dịch cho bạn đọc sớm nhất có thể.