(vnCloud.vn) Trong bài viết này, tôi sẽ trình bày một cách tường tận khái niệm microservices, giải thích tại sao thiết kế này lại có sức hút đến như vậy, và sau đó chỉ ra những khó khăn của việc áp dụng thiết kế microservices.
Cuối bài viết sẽ là những câu hỏi định hướng sẽ giúp bạn trả lời microservices có phải là một thiết kế phù hợp với mình hay không?
Microservices là một chủ đề phổ biến trong vài năm gần đây. Hội chứng cuồng Microservice ám chỉ lối tư duy như sau: Nhóm phát triển Netflix là chuyên gia về devops. Họ sử dụng microservices. Suy ra: Nếu tôi sử dụng microservices, tôi là chuyên gia về devops.
Contents
Hãy bắt đầu với những khái niệm cơ bản.
Sau đây là cách thức thiết kế của một nền tảng chia sẻ video giả định, trước hết là dạng monolith (đơn khối lớn) và sau đó là dạng microservices:
Sự khác biệt giữa hai hệ thống thể hiện ở chỗ thiết kế thức nhất bao gồm một khối duy nhất kích thước lớn, hay một monolith, còn hệ thống thứ hai là một tập hợp các dịch vụ nhỏ và cụ thể trong đó mỗi dịch vụ có vai trò riêng biệt.
Ở cấp độ mô hình này, ta dễ dàng thấy được sức hấp dẫn của thiết kế microservices. Nó được thể hiện ở những lợi ích tiềm năng sau:
Các thành phần nhỏ, độc lập có thể được xây dựng bởi các nhóm nhỏ, độc lập. Một nhóm có thể thực hiện những thay đổi đối với dịch vụ “Upload” mà không làm ảnh hưởng đến dịch vụ “Transcode”, hoặc thậm chí là hiểu biết về nó. Lượng thời gian cần thiết để nắm bắt được một thành phần giảm đi đáng kể, và việc phát triển những tính năng mới dễ hơn rất nhiều.
Mỗi thành phần độc lập có thể được triển khai một cách độc lập. Điều này cho phép các tính năng mới được xuất bản nhanh chóng hơn và rủi ro được giảm thiểu. Các bản vá hoặc tính năng cập nhật của thành phần “Streaming” có thể được triển khai mà không yêu cầu sự thay đổi trên các thành phần khác.
Mỗi thành phần có thể được mở rộng quy mô một cách độc lập với các thành phần khác. Trong suốt thời gian hoạt động của sản phẩm, những shows mới có thể được xuất bản, thành phần “Download” có thể được mở rộng để tăng khả năng chịu tải, mà không cần thiết mở rộng các thành phần khác, khiến việc mở rộng trở nên linh hoạt hơn và giảm thiểu được nhiều chi phí.
Mỗi thành phần thực hiện một chức năng nhỏ, cụ thể. Điều này có nghĩa là chúng có thể dễ dàng thích nghi trên các hệ thống, dịch vụ hay sản phẩm khác. Thành phần “Transcode” có thể được sử dụng cho các đơn vị nghiệp vụ khác, hoặc thậm chí trở thành một nghiệp vụ hoàn toàn mới, được sử dụng bởi các nhóm phát triển khác.
Ở tầm nhìn này, lợi ích của mô hình microservices rõ ràng là vượt trội so với mô hình monolithic. Nếu vậy, tại sao thiết kế này chỉ mới nổi lên gần đây?
Có hai câu trả lời. Thứ nhất là nó có được sử dụng, ít nhất là theo những gì chúng ta được biết. Thứ hai là những tiến bộ kỹ thuật gần đây mới cho phép ta đưa nó vào thực tiễn.
Cứ coi như là chúng ta có thể sử dụng thiết kế microservices cho sản phẩm của mình, điều đáng xem xét cẩn thận là liệu chúng ta có nên hay không. Ta dễ dàng thấy được những lợi ích về mặt lý thuyết ở mức tổng quan, nhưng còn những khó khăn thì sao?
Mọi thứ có thể trở nên phức tạp hơn rất nhiều cho các nhà phát triển. Trong trường hợp một nhà phát triển muốn làm việc trên một tính năng với quy mô trải rộng trên nhiều dịch vụ khác nhau, họ buộc phải chạy tất cả chúng trên máy của mình, hoặc kết nối tới chúng. Điều này thường là rắc rối hơn so với việc đơn giản chạy một chương trình duy nhất.
Thách thức này có thể phần nào được giảm nhẹ bằng việc sử dụng các công cụ hỗ trợ, nhưng chừng nào mà số lượng các dịch vụ cấu thành nên một hệ thống còn tăng lên, những thách thức mà nhà phát triển phải đối mặt khi vận hành hệ thống nhìn chung là vẫn không thể tránh khỏi.
Với những nhóm không phát triển dịch vụ, mà là bảo trì chúng, có một sự bùng nổ về độ phức tạp tiềm ẩn. Thay vì chỉ phải quản lý một số dịch vụ đang được vận hành, họ phải quan tâm tới hàng chục, hàng trăm hoặc thậm chí hàng ngàn dịch vụ khác nhau. Số lượng dịch vụ càng nhiều, các nhiều giao tiếp giao tiếp được thực hiện và càng có nhiều khả năng thất bại.
Theo thiết kế monoservices, một cách tự nhiên, hai công đoạn vận hành và phát triển được tách biệt rõ ràng. Ngày nay, khi mà devops đi vào thực tiễn, liệu nó có giúp giải quyết những vấn đề trong hai luận điểm trên hay không?
Vấn đề là nhiều tổ chức vẫn hoạt động với hai nhóm phát triển và vận hành riêng biệt – và một tổ chức như vậy thường gặp nhiều khó khăn để thích nghi với microservices.
Với những tổ chức có bộ phận devops, những thách thức vẫn tồn tại. Cùng lúc phát triển và vận hành vốn đã là một bài toán khó, nhưng để hiểu được những khác biệt giữa các hệ thống điều phối container – đặc biệt là các hệ thống phát triển với nhịp độ nhanh chóng – lại càng phức tạp hơn nữa. Điều này dẫn tới luận điểm tiếp theo.
Khi được thực hiện bởi các chuyên gia, kết quả đạt được có thể rất khả quan. Nhưng hãy tưởng tượng trong một tổ chức nơi mà mọi việc có thể sẽ không được vận hành một cách trơn tru trên một hệ thống monolithic. Liệu có gì đảm bảo rằng mọi thứ sẽ trở nên tốt hơn thông qua việc gia tăng số lượng hệ thống, yếu tố sẽ làm tăng thêm mức độ phức tạp vận hành?
Với tự động hóa, giám sát và điều phối một cách hiệu quả, điều này là hoàn toàn khả thi. Nhưng thách thức thực sự thường không phải từ mặt công nghệ – khó khăn nằm ở việc tìm kiếm được những người có thể sử dụng hiệu quả hệ thống. Nhu cầu thị trường về nhóm kỹ năng này đang rất cao, và rất khó để tuyển được một nhân sự phù hợp.
Trong tất cả các ví dụ đã được sử dụng để mô tả những lợi ích của microservices, ta đều nói về các thành phần độc lập. Tuy nhiên trên thực tế, các thành phần này thường không lý tưởng như vậy. Trên giấy tờ, một số lĩnh vực cụ thể nhìn có vẻ được khoanh vùng rõ ràng, nhưng khi xem xét tới những chi tiết cụ thể, ta sẽ phát hiện ra mức độ khó khăn trong việc mô hình hóa là cao hơn dự đoán ban đầu rất nhiều.
Đây là nơi mà mọi việc trở nên vô cùng phức tạp. Nếu các ranh giới không được định nghĩa rõ ràng, tình trạng tất yếu sẽ xảy ra là mặc dù về mặt lý thuyết các dịch vụ có thể được triển khai riêng lẻ, nhưng do sự phụ thuộc lẫn nhau nội bộ giữa các dịch vụ này, ta sẽ phải triển khai một tập hợp các nhiệm vụ theo một nhóm.
Thực trạng này dẫn tới việc ta cần quản lý các phiên bản cố kết của các dịch vụ từng được chứng minh và kiểm thử khi làm việc cùng nhau. Ta không thực sự có một hệ thống được triển khai độc lập, vì khi cài đặt một tính năng mới, ta cần điều phối một cách cẩn thận sự triển khai của nhiều dịch vụ khác nhau.
Trong ví dụ trước, ta đã đề cập rằng việc triển khai một tính năng có thể đồi hỏi sự triển khai đồng thời nhiều phiên bản khác nhau của những dịch vụ có liên quan. Có thể giả định rằng các kỹ thuật triển khai hợp lý sẽ giảm thiểu việc này, ví dụ như blue/green deployment (hầu hết cách nền tảng điều phối dịch vụ đều dễ dàng thực hiện được điều này); hoặc nhiều phiên bản của một dịch vụ được chạy song song, khi đó các kênh có vai trò quyết định phiên bản nào sẽ được sử dụng.
Những kỹ thuật này giúp giảm thiểu một số lượng lớn những trở ngại đã nêu nếu các dịch vụ là phi trạng thái. Các dịch vụ phi trạng thái thường tương đối dễ để xử lý. Trên thực tế, nếu có một dịch vụ phi trạng thái, ta thường cân nhắc bỏ qua hoàn toàn thiết kế micorservices để sử dụng một mô hình serverless.
Trên thực tế, nhiều dịch vụ đòi hỏi việc lưu trữ trạng thái. Một ví dụ cho nền tảng chia sẻ video là dịch vụ thu phí định kỳ. Một phiên bản dịch vụ thu phí định kỳ mới có thể sẽ lưu trữ dữ liệu trong cơ sở dữ liệu ở khuôn dạng khác. Nếu ta chạy đồng thời hai dịch vụ song song tức là ta đang chạy hệ thống với hai schema cùng lúc. Nếu ta thực hiện một blue green deployment, và các dịch vụ khác phụ thuộc vào dữ liệu theo khuôn dạng mới, thì chúng cần được cập nhật tại cùng một thời điểm. Khi ấy nếu việc triển khai dịch vụ thu phí định kỳ không thành công và phải roll back, các dịch vụ phụ thuộc cũng cần phải roll back tương ứng, với những hậu quả chồng chất.
Có thể người ta sẽ cho rằng với cơ sở dữ liệu NoSQL, những vấn đề liên quan đến schema sẽ được loại bỏ, nhưng thực sự thì không. Cơ sở dữ liệu không quy định rõ ràng về schema không dẫn tới một hệ thống schemaless – chúng chỉ có nghĩa là schema có xu hướng được quản lý tại tầng ứng dụng, thay vì ở tầng cơ sở dữ liệu. Khó khăn cơ bản của việc định khuôn dạng dữ liệu vẫn không thể bỏ qua.
Khi ta xây dựng một hệ thống lớn các dịch vụ phụ thuộc lẫn nhau, thường sẽ có rất nhiều các giao tiếp giữa các dịch vụ. Điều này gây ra một số khó khăn. Thứ nhất, có rất nhiều thời điểm mọi thứ gặp thất bại. Chúng ta phải lường trước được việc các lời gọi trong hệ thống sẽ thất bại, điều đó có nghĩa là khi một dịch vụ gọi tới một dịch vụ khác, nó nhiều khả năng sẽ phải thử lại ít nhất một vài lần nữa. Do một dịch vụ có thể sẽ cần gọi nhiều lần tới các dịch vụ khác, mọi thứ bắt đầu trở nên rối rắm.
Hãy tưởng tượng tình huống một người dùng tải lên một video sử dụng dịch vụ chia sẻ video. Chúng ta có thể cần chạy dịch vụ upload, truyền dữ liệu tới dịch vụ transcode, cập nhật thu phí định kỳ, update kết quả gợi ý và nhiều việc nữa. Tất cả những lời gọi ở đây đòi hỏi một mức độ điều phối nhất định, nếu có giai đoạn nào thất bại chúng ta cần phải thử lại.
Logic thử lại này có thể khó quản lý. Cố gắng thực hiện các công việc một cách đồng bộ thường là bất khả thi, do có quá nhiều khả năng thất bại có thể xảy ra. Trong trường hợp này, một giải pháp đáng tin cậy hơn là sử dụng thiết kế bất đồng bộ để kiểm soát các giao tiếp. Khó khăn ở đây là các thiết kế bất đồng bộ về cơ bản sẽ khiến một hệ thống trở thành có trạng thái. Như đã đề cập ở luận điểm trước, các hệ thống có trạng thái và hệ thống với trạng thái phân tán thường rất khó quản lý.
Khi một hệ thống microservices sử dụng hàng đợi thông điệp cho các giao tiếp giữa các dịch vụ, về cơ bản ta sẽ có một cơ sở dữ liệu lớn (hàng đợi thông điệp hoặc thành phần trung gian) để gắn kết hệ thống lại. Cho dù ban đầu đây có vẻ không phải là một thách thức, nhưng phiền toái một lần nữa lại được tạo ra bởi hệ thống schema. Một dịch vụ tại phiên bản X có thể sinh ra một thông điệp theo một khuôn dạng cụ thể, các dịch vụ phụ thuộc vào thông điệp này cũng cần được cập nhật khi mà dịch vụ gửi thông điệp thay đổi các chi tiết mà nó gửi đi.
Ta có thể sử dụng những dịch vụ có khả năng làm việc với những thông điệp có nhiều khuôn dạng khác nhau, nhưng chúng rất khó quản lý. Khi đó, mỗi khi triển khai một phiên bản mới của các dịch vụ, ta sẽ gặp phải những trường hợp mà hai phiên bản khác nhau của cùng một dịch vụ cùng xử lý các thông điệp trong cùng một hàng đợi, thậm chí đó có thể là các thông điệp gửi tới từ các phiên bản khác nhau của cùng một dịch vụ khác nữa. Điều này dẫn tới những trường hợp ngách phức tạp. Để tránh rơi vào những trường hợp ngách này, ta chỉ nên chấp nhận sự tồn tại của những phiên bản thông điệp cụ thể, có nghĩa là ta sẽ phải triển khai một tập hợp các phiên bản theo một khối gắn liền, nhưng phải đảm bảo những phiên bản cũ hơn đã bị loại bỏ hoàn toàn khỏi hệ thống.
Điều này một lần nữa nhấn mạnh quan điểm triển khai độc lập có thể không được như kỳ vọng khi xem xét các vấn đề một cách chi tiết.
Để giảm thiểu những thách thức được đề cập ở các mục trên, quản lý phiên bản cần được thực hiện hết sức cẩn thận. Có nhiều ý kiến cho rằng tuân theo một quy tắc được chuẩn hóa như semver có thể giải quyết được vấn đề. Không. Semver là một hệ thống ký pháp phù hợp để sử dụng, nhưng bản thân ta vẫn phải tự ghi lại những phiên bản dịch vụ và API có thể làm việc được với nhau.
Mức độ khó khăn ở đây tăng lên một cách nhanh chóng, và có thể dẫn tới thời điểm mà ta không còn biết được phiên bản nào của các dịch vụ còn thực sự cùng hoạt động được.
Quản lý sự phụ thuộc trong hệ thống phần mềm luôn được biết đến là một bài toán khó, cho dù các thành phần được viết bằng Node, Java, C hay bất kỳ ngôn ngữ nào. Những thách thức về sự xung khắc giữa các thành phần phụ thuộc khi được sử dụng bởi một thực thể riêng lẻ thường rất khó để giải quyết.
Những vấn đề này khó ngay cả khi các phụ thuộc là tĩnh, và có thể được vá lỗi, cập nhật, chỉnh sửa, nhưng một khi bản thân các phụ thuộc là các dịch vụ đang hoạt động, ta không thể đơn giản chỉ cập nhật chúng – mà phải chạy nhiều phiên bản (với những khó khăn đã được mô tả) hoặc tạm ngưng hệ thống cho tới khi nó hoàn toàn được sửa chữa.
Trong những trường hợp ta cần các giao dịch được toàn vẹn trong cả một quá trình, microservices có thể rất khó áp dụng. Các trạng thái phân tán thường rất khó giải quyết, nhiều đơn vị nhỏ có thể thất bại khiến việc điều phối các giao dịch rất khó khăn.
Có thể ngăn ngừa vấn đề này bằng cách tạo ra những quy trình bất biến (idempotent operation), cung cấp cơ chế thử lại…, và trong nhiều trường hợp cách làm này có thể có tác dụng. Nhưng trong nhiều hoàn cảnh, ta đơn giản chỉ cần một giao dịch thành công hoặc thất bại, chứ không bị rơi vào một trạng thái trung gian nào cả. Những nỗ lực để vượt qua vấn đề này trong hệ thống microservices có thể phải trả giá đắt.
Thật vậy, các dịch vụ và thành phần riêng lẻ có thể được triển khai độc lập, tuy nhiên trong hầu hết trường hợp ta cần sử dụng một số nền tảng điều phối, như Kubernetes. Nếu bạn đang sử dụng một dịch vụ quản lý, như Google’s GKE hay Amazon’s EKS thì một lượng lớn các thao tác quản lý cụm phức tạp đã được thực hiện sẵn giúp bạn.
Tuy nhiên, nếu muốn tự thực hiện việc quản lý cụm, ta sẽ phải quản lý một hệ thống lớn, phức tạp và mang tính sống còn. Cho dù các dịch vụ đơn lẻ có thể có được những lợi ích đã được trình bày trước đây, ta cần đặc biệt cẩn trọng trong việc quản lý cụm. Việc triển khai, cập nhật và bảo vệ hệ thống đều rất khó khăn.
Trong nhiều trường hợp những lợi ích tổng thể nhìn chung là rõ rệt, nhưng ta vẫn không được xem nhẹ hay đánh giá thấp những trở ngại phát sinh của việc quản lý một hệ thống lớn, phức tạp khác. Các dịch vụ được quản lý có thể hữu ích, nhưng hầu hết những dịch vụ này còn rất sơ khai (Ví dụ: Amazon EKS mới chỉ được công bố tại thời điểm cuối năm 2017).
Ngăn ngừa hội chứng này bằng việc đưa ra những quyết định cẩn thận và có cân nhắc kỹ lưỡng. Dưới đây là một số câu hỏi bạn có thể tự đặt ra và câu trả lời tương ứng.
Không có kiến trúc microservices nào cả. Microservices chỉ là một mẫu thiết kế và triển khai của các thành phần, không hơn không kém. Cho dù chúng có mặt trong hệ thống hay không, kiến trúc là một vấn đề khác.
Microservices liên quan nhiều hơn tới các quy trình kỹ thuật xung quanh việc đóng gói và vận hành, hơn là thiết kế phức tạp của hệ thống. Xác định ranh giới thích hợp cho các thành phần vẫn luôn là một trong những bài toán khó nhất trong các hệ thống kỹ thuật.
Bất chấp kích thước dịch vụ của bạn là như thế nào, cho dù chúng có nằm trong Docker container hay không, bạn vẫn luôn cần suy xét kỹ lưỡng về việc làm thế nào để xây dựng nên một hệ thống. Không có câu trả lời đúng duy nhất, và luôn có rất nhiều lựa chọn khác nhau.
Theo dtecrypto.wordpress.com