Trong những năm gần đây, khái niệm “microservices” đã trở thành một từ khóa phổ biến trong lĩnh vực phát triển phần mềm. Nhưng thực sự, việc áp dụng kiến trúc microservices luôn đi kèm với những rủi ro và thách thức, và vì thế, những sai sót trong quá trình triển khai microservices là điều khó thể tránh khỏi. Trong bài viết này, chúng ta sẽ cùng tìm hiểu về top 5 những sai lầm phổ biến khi triển khai một hệ thống microservices cùng các giải pháp để giải quyết chúng.

Đây là bài blog thuộc series System Design của mình, nếu bạn quan tâm đến các vấn đề liên quan đến thiết kế hệ thống phần mềm, hãy theo dõi series này để cập nhật những kiến thức mới nhất nhé!

1. Không chia nhỏ service đúng cách

Một trong những lý do chính khiến cho việc triển khai microservices trở nên phức tạp là do việc chia nhỏ service không đúng cách. Mặc dù điều này tương đối phụ thuộc vào từng dự án cụ thể, nhưng có một phương pháp chia nhỏ service mà tôi muốn chia sẻ với các bạn. Đó chính là chia nhỏ service dựa trên Domain-Driven Design (DDD). DDD là một phương pháp thiết kế hệ thống phần mềm tập trung vào việc hiểu rõ về domain của dự án và xây dựng các service dựa trên domain đó. Bằng cách này, chúng ta có thể chia nhỏ service một cách hợp lý và dễ dàng quản lý hơn.

DDD là một chủ đề khá rộng và tương đối hay ho, mình sẽ dành một bài viết riêng để trình bày chi tiết về DDD 😉. Trong khuôn khổ bài viết này, chúng ta sẽ tập trung vào cách áp dụng DDD vào việc chia nhỏ service.

DDD là gì?

Domain-Driven Design (DDD) là một phương pháp tiếp cận trong phát triển phần mềm, tập trung vào việc mô hình hóa phần mềm dựa trên sự hiểu biết sâu sắc về domain (lĩnh vực hoặc nghiệp vụ) mà phần mềm đó phục vụ. DDD khuyến khích sự hợp tác chặt chẽ giữa các chuyên gia domain và đội ngũ phát triển để tạo ra một mô hình phần mềm phản ánh chính xác các quy tắc, khái niệm và logic của domain. Những viên gạch xây dựng nên một mô hình DDD bao gồm Entities, Value Objects, Aggregates, Services, Repositories, và Ubiquitous Language.

Áp dụng DDD vào việc chia nhỏ service một cách hợp lý

Đầu tiên, DDD khuyến khích việc xác định các Bounded Contexts - những vùng ngữ nghĩa rõ ràng và riêng biệt trong domain. Mỗi Bounded Context sẽ tương ứng với một hoặc nhiều microservice. Việc phân chia này giúp tránh sự chồng chéo và phức tạp không cần thiết giữa các service, đồng thời giúp đội ngũ phát triển có thể tập trung vào từng phần cụ thể của hệ thống một cách hiệu quả hơn.

Copyright from Microsoft

Copyright from Microsoft

Tiếp theo, khi đã xác định được các Bounded Contexts, bạn cần thiết kế các service sao cho chúng chỉ đảm nhiệm những chức năng cụ thể và không phụ thuộc quá nhiều vào các service khác. Điều này có thể đạt được bằng cách xác định rõ các Entities, Value Objects, và Aggregates trong từng Bounded Context. Mỗi service nên xử lý các nghiệp vụ và dữ liệu liên quan đến một Aggregate duy nhất, giúp duy trì tính nhất quán và giảm thiểu sự phức tạp khi tích hợp giữa các service.

Cuối cùng, việc sử dụng Ubiquitous Language - ngôn ngữ chung mà cả đội ngũ phát triển và chuyên gia domain đều hiểu và sử dụng - là một yếu tố quan trọng trong DDD. Ubiquitous Language giúp đảm bảo rằng tất cả các bên liên quan đều hiểu rõ về cấu trúc và chức năng của từng service, từ đó giúp giảm thiểu các sai sót và hiểu lầm trong quá trình phát triển và triển khai.

Bằng cách áp dụng DDD vào việc chia nhỏ service, bạn không chỉ tạo ra một hệ thống microservices dễ quản lý và mở rộng mà còn đảm bảo rằng các service được thiết kế và phát triển theo cách phù hợp với domain và nghiệp vụ của dự án.

Nếu muốn tìm hiểu thêm về DDD, bạn có thể tham khảo cuốn sách Domain-Driven Design: Tackling Complexity in the Heart of Software của Eric Evans.

2. Giao tiếp giữa các service chưa tối ưu

Giao tiếp giữa các service là một yếu tố then chốt trong kiến trúc microservices. Một lỗi phổ biến là sử dụng giao thức không phù hợp hoặc không tối ưu hóa việc giao tiếp, dẫn đến hiệu suất kém và độ trễ cao.

Để giải quyết vấn đề này, trước hết bạn cần xác định rõ là trong kiến trúc microservices gồm có những loại giao tiếp nào, từ đó chọn lựa giao thức và công nghệ phù hợp. Giao tiếp giữa các service trong môi trường microservices là một chủ đề phức tạp, nhưng nó đều dựa trên các nền tảng cơ bản, hãy cùng tôi tìm hiểu nhé.

Các phương thức giao tiếp trong kiến trúc microservices

Việc giao tiếp giữa các service trong kiến trúc microservices có thể được thực hiện thông qua chủ yếu 2 phương cách chính: Synchronous (đồng bộ) và Asynchronous (bất đồng bộ). Với mỗi phương cách, chúng lại bao gồm 2 loại giao tiếp chính: One-to-one (một-một) và One-to-many (một-nhiều). Từ đó, tôi có một bảng tổng hợp như sau:

One-to-oneOne-to-many
Synchronous・ Request/response
Asynchronous・ Async req/resp
・ One-way notification
・ Publish/Subscribe
・ Publish/ async response

Lựa chọn giao thức và công nghệ phù hợp

Khi đã xác định được loại giao tiếp phù hợp, bạn cần chọn lựa giao thức và công nghệ phù hợp để triển khai. Đối với mỗi loại hình giao tiếp, có những ưu và nhược điểm riêng, phù hợp với các trường hợp sử dụng cụ thể. Đối với kinh nghiệm của bản thân, tôi có thể chia sẻ một số gợi ý như sau:

  • Synchronous: Có thể lựa chọn text-based protocols như HTTP/Rest, GraphQL, hoặc binary-based protocols như gRPC, Avro.

    • HTTP/Rest: Cực kỳ phổ biến, dễ triển khai và tích hợp. Điều này đặc biệt hữu ích khi bạn cần tương tác với các service bên ngoài hệ thống, hoặc giữa client (frontend, devices, etc.) và server.
    • gRPC: Hiệu suất cao, hỗ trợ nhiều ngôn ngữ lập trình, thân thiện hơn khi giao tiếp giữa các service trong hệ thống với nhau.
  • Asynchronous: Đây là lựa chọn phù hợp khi bạn cần thiết kế hệ thống có tính mở rộng cao (scalability), chịu tải cao (high load), hoặc cần xử lý các công việc bất đồng bộ. Hiện nay, theo mình thấy thì Kafka, Pulsar, hoặc NATS là những công nghệ phổ biến và mạnh mẽ để triển khai các hệ thống bất đồng bộ, thậm chí nếu thiết kế một cách hiệu quả thì bạn còn có thể xây dựng hệ thống near-realtime (thông lượng cao và độ trễ thấp).

3. Quản lý dữ liệu phân tán không đúng cách

Trong môi trường microservices, mỗi service thường có cơ sở dữ liệu riêng. Việc quản lý dữ liệu phân tán có thể trở nên phức tạp và dễ dẫn đến tình trạng không nhất quán. Một sai lầm thường gặp là cố gắng duy trì một cơ sở dữ liệu chung cho nhiều service, điều này đi ngược lại với nguyên tắc phân tán.

Để giải quyết vấn đề này, bạn cần thiết kế hệ thống sao cho mỗi service có cơ sở dữ liệu riêng và sử dụng các kỹ thuật như Event Sourcing hoặc CQRS để đảm bảo tính nhất quán dữ liệu. Khi mà hệ thống được áp dụng DDD, việc quản lý dữ liệu phân tán sẽ trở nên rõ ràng hơn, mỗi service sẽ chịu trách nhiệm về dữ liệu của mình và không phụ thuộc vào các service khác. Bound contexts và aggregates sẽ giúp xác định rõ ràng phạm vi của dữ liệu và cách thức truy cập dữ liệu, giúp giảm thiểu rủi ro không nhất quán dữ liệu.

Mỗi service phải có cơ sở dữ liệu riêng

Cần nhấn mạnh lại rằng:

📌 Mỗi service phải có cơ sở dữ liệu riêng của nó, không được chia sẻ cơ sở dữ liệu với các service khác, và cũng không được phép truy cập trực tiếp vào cơ sở dữ liệu của service khác.

Điều này giúp giữ cho mỗi service độc lập và không phụ thuộc vào các service khác, giảm thiểu rủi ro không nhất quán dữ liệu và tăng tính linh hoạt của hệ thống. Trong quá trình hệ thống phát triển và mở rộng, việc duy trì cơ sở dữ liệu riêng cho từng service đôi khi có thể gặp khó khăn, đó đôi khi là lúc cả team cần phải thiết kế lại ranh giới giữa các service, hoặc sử dụng các công nghệ như API Gateway để quản lý truy cập dữ liệu giữa các service, thay vì truy cập trực tiếp vào cơ sở dữ liệu của nhau.

Sử dụng Event Sourcing và CQRS

Event Sourcing và CQRS là hai kỹ thuật không nhất thiết phải có trong mọi hệ thống microservices, nhưng chúng có thể giúp giải quyết một số thách thức liên quan đến quản lý dữ liệu phân tán và mở rộng hệ thống sau này. Cả 2 kỹ thuật này cũng không nhất thiết phải được triển khai cùng nhau, tùy thuộc vào yêu cầu cụ thể của dự án. Nhưng theo kinh nghiệm thực tế của tôi, việc sử dụng Event Sourcing và CQRS giúp tăng tính minh bạch và rõ ràng của luồng dữ liệu trong hệ thống. Nào, chúng ta cùng tìm hiểu sơ lược về Event Sourcing và CQRS nhé.

Event Sourcing

Copyright from Microsoft

Copyright from Microsoft

Event Sourcing là một mô hình thiết kế cơ sở dữ liệu, trong đó tất cả các thay đổi dữ liệu được lưu trữ dưới dạng sự kiện (event). Thay vì lưu trữ trạng thái hiện tại của dữ liệu, Event Sourcing lưu trữ lịch sử của dữ liệu, từ đó giúp theo dõi và phục hồi trạng thái của dữ liệu một cách dễ dàng. Event Sourcing cũng giúp tăng tính nhất quán của dữ liệu, giảm thiểu rủi ro mất dữ liệu và giúp phát triển hệ thống một cách linh hoạt hơn.

Nếu dữ liệu ngày càng lớn, bạn cũng có thể áp dụng kỹ thuật snapshotting để giảm tăng hiệu suất truy xuất dữ liệu.

CQRS

CQRS (Command Query Responsibility Segregation) là một mô hình thiết kế phần mềm, trong đó việc đọc dữ liệu (query) và ghi dữ liệu (command) được phân tách ra thành hai phần riêng biệt. CQRS giúp tăng hiệu suất của hệ thống bằng cách tối ưu hóa việc đọc và ghi dữ liệu, từ đó giảm độ trễ và tăng hiệu suất của hệ thống. Một cách dễ hiểu thì CQRS tận dụng Polygot Persistence, tức là sử dụng nhiều cơ sở dữ liệu phù hợp với từng loại truy vấn cụ thể.

Ví dụ, bạn có thể sử dụng PostgreSQL cho việc ghi dữ liệu giúp đảm bảo tính ACID, và sử dụng MongoDB cho việc đọc dữ liệu giúp tăng hiệu suất, Elasticsearch cho việc tìm kiếm full-text, v.v.

Nếu muốn tìm hiểu thêm về CQRS, bạn có thể tham khảo CQRS tại microservices.io.

4. Thiếu chiến lược giám sát và theo dõi hiệu quả

Một trong những thách thức lớn khi triển khai microservices là giám sát và theo dõi hiệu suất của từng service. Thiếu chiến lược giám sát có thể dẫn đến việc khó phát hiện và xử lý sự cố, từ đó kéo dài thời gian downtime và ảnh hưởng đến trải nghiệm người dùng.

Để giải quyết vấn đề này, team kỹ sư cần lên chiến lược triển khai một hệ thống giám sát (observability) rõ ràng và hiệu quả. Một hệ thống giám sát hiệu quả cần bao gồm các thành phần sau:

  • Health checks: Tất nhiên rồi, việc kiểm tra sức khỏe của hệ thống và các service là một yếu tố quan trọng. Health checks giúp xác định trạng thái hoạt động của hệ thống và cảnh báo sớm về các vấn đề tiềm ẩn. Trên môi trường Kubernetes ochestration, bạn có thể sử dụng ReadinessLiveness probe để kiểm tra sức sống của từng container, từ đó giúp cân bằng tải và xử lý sự cố một cách hiệu quả.
  • Logging aggregation: Tập trung và lưu trữ logs từ tất cả các service, giúp theo dõi và phân tích dữ liệu một cách hiệu quả. Bởi vì trong kiến trúc microservices, logs có thể phân tán ở nhiều nơi khác nhau, việc tập trung logs lại giúp giảm thiểu thời gian tìm kiếm và phân tích nguyên nhân của sự cố nhanh chóng hơn.
  • Distribution tracing: Theo dõi và phân tích các request qua các service, giúp xác định nguyên nhân gốc rễ của sự cố, từ đó giảm thiểu thời gian xử lý sự cố cũng như tránh tình trạng lỗi tương tự lặp lại trong tương lai.
  • Metrics collection: Thu thập và theo dõi các metrics quan trọng như CPU, memory, network, latency, error rate, v.v. giúp đánh giá hiệu suất của hệ thống và phát hiện sớm các vấn đề tiềm ẩn.
  • Alerting and monitoring: Thiết lập cảnh báo và theo dõi hiệu suất của hệ thống, giúp phát hiện sớm các vấn đề và xử lý kịp thời trước khi ảnh hưởng đến trải nghiệm người dùng.

Hiện nay, có nhiều công cụ giám sát và theo dõi hệ thống phổ biến như Prometheus, Grafana, Open Telemetry, ELK. Chúng ta có thể đặt một các hệ thống này ở API Gateway hoặc Service Mesh để giám sát toàn bộ hệ thống một cách hiệu quả.

5. Không thống nhất kế hoạch thiết kế API rõ ràng

Cuối cùng, một sai lầm phổ biến khi triển khai microservices là thiếu kế hoạch thiết kế API đồng nhất. Các service chỉ giao tiếp với nhau thông qua API, việc thiết kế API không đồng nhất có thể dẫn đến sự phức tạp trong việc tích hợp giữa các service, cũng như việc giao tiếp giữa các team không hiệu quả. Thực tế, việc thiết kế API của mỗi service nên dựa trên SLA (Service Level Agreement) và đảm bảo rằng API đó đáp ứng đúng yêu cầu của service đó và các service khác giao tiếp với nó.

Contract-First Development

Một trong những phương pháp mà tôi tâm đắc nhất là Contract-First Development. Contract-First Development là một phương pháp phát triển phần mềm, trong đó việc thiết kế API bắt đầu từ việc xác định rõ các contract (hợp đồng) giữa các service, từ đó giúp đảm bảo rằng API đáp ứng đúng yêu cầu và không bị thay đổi một cách đột ngột. Contract-First Development giúp giảm thiểu rủi ro không đồng nhất giữa các service, giúp tăng tính linh hoạt và dễ dàng mở rộng hệ thống. Ở đây tôi gọi là contract là bởi vì nó giống như hợp đồng giữa các service, hay rõ hơn là API Specification của mỗi service, đó có thể là OpenAPI, GraphQL Schema, gRPC schema, hay là AsyncAPI.

Contract first giúp đảm bảo rằng mỗi service đều tuân thủ theo một chuẩn chung, từ đó giúp việc kiểm thử, triển khai, và tích hợp giữa các service trở nên đáng tin cậy và dễ dàng hơn.

API Gateway

API Gateway không chỉ giúp quản lý truy cập dữ liệu giữa các service một cách hiệu quả, mà còn giúp tổng hợp và định dạng lại response trước khi trả về cho client, đây còn được gọi là API Composition.

flowchart TD
    A[Client Request] --> B[API Gateway]
    B --> |Request 1| C[Service A]
    B --> |Request 2| D[Service B]
    B --> |Request 3| E[Service C]
    C --> |Response 1| F[API Gateway]
    D --> |Response 2| F
    E --> |Response 3| F
    F --> G[Aggregate Response]
    G --> H[Client Response]

    subgraph Client
        A
        H
    end

    subgraph APIGateway
        B
        F
        G
    end

    subgraph Services
        C
        D
        E
    end

Qua sơ đồ trên, chúng ta có thể thấy rằng mỗi service sẽ có một API riêng, tuy nhiên nhờ Contract-First Development mà chúng ta có thể đảm bảo rằng các API này đều tuân thủ theo một chuẩn chung, từ đó giúp việc tổng hợp các response từ các service trở nên dễ dàng và chỉ cần trả về cho client một cách tối thiểu số lượng API.

Kết luận

Trong bài viết này, tôi đã chia sẻ với các bạn về top 5 sai lầm phổ biến khi triển khai microservices cùng các giải pháp để giải quyết chúng, đây cũng là những trải ngiệm thực tế mà tôi đã gặp phải trong quá trình làm việc với microservices. Tuy nhiên, qua quá trình bị hành hạ 😅, tôi cũng rút ra được nhiều kinh nghiệm quý giá, từ đó giúp tôi hiểu rõ hơn về kiến trúc microservices và cách triển khai một cách hiệu quả. Tôi hy vọng rằng bài viết này sẽ các bạn hiểu rõ hơn và phòng tránh được những sai sót khi triển khai một hệ thống microservices.

Nếu có bất kỳ ý kiến hoặc thắc mắc nào, hãy để lại comment bên dưới, tôi sẽ cố gắng trả lời sớm nhất có thể. Cảm ơn các bạn đã đọc bài viết 👋.