Mọi đội ngũ kỹ thuật đều mong muốn một hệ thống có thể phát triển mà không gặp trở ngại. Chúng ta hình dung về cảnh lưu lượng truy cập tăng trưởng mượt mà, các máy chủ hoạt động ổn định và doanh thu tăng dần đều. Nhưng rồi thực tế ập đến. Một chiến dịch marketing lan truyền kéo theo một làn sóng người dùng, cơ sở dữ liệu bị treo, và ai đó đang phải cuống cuồng khởi động lại các dịch vụ vào lúc ba giờ sáng. Phản xạ tự nhiên là đổ lỗi cho công cụ. Chúng ta tự nhủ rằng mình cần thêm nhiều nhân CPU hơn, ổ đĩa nhanh hơn, hoặc thêm một lớp bộ nhớ đệm khác. Nhưng sự tăng trưởng không đến từ phần cứng. Nó đến từ cấu trúc. Nếu nền tảng của bạn không thể phân bổ trọng tải, mỗi người dùng mới sẽ trở thành một gánh nặng thay vì một thành tựu.

Tại sao công cụ không thể cứu vãn một nền tảng lỗi thời

Bạn có thể khởi tạo hàng trăm thực thể đám mây, thêm các bộ cân bằng tải giữa các vùng địa lý và lưu bộ nhớ đệm cho mọi tài nguyên tĩnh trong một mạng phân phối nội dung toàn cầu. Đây là những nhân tố khuếch đại sức mạnh. Tuy nhiên, nhân số không vẫn chỉ là số không. Một ứng dụng nguyên khối với các phụ thuộc chồng chéo sẽ nghẹt thở dưới sức nặng của chính nó, bất kể có bao nhiêu phần cứng bên dưới.

Hãy tưởng tượng một cửa hàng trực tuyến nơi danh mục sản phẩm, xử lý thanh toán và xác thực người dùng đều nằm trong một mã nguồn duy nhất. Khi quy trình thanh toán chậm lại, toàn bộ trang web sẽ chạy chậm rì. Trang đăng nhập bị giật lag. Trải nghiệm duyệt web bị ảnh hưởng. Bạn không thể mở rộng điểm nghẽn mà không phải mở rộng tất cả những thứ khác cùng với nó. Điều đó vừa tốn kém, vừa kém hiệu quả, lại vừa mong manh. Cuối cùng, bạn phải trả tiền cho tài nguyên tính toán mà chẳng mang lại lợi ích cho ai, trong khi người dùng phải chờ đợi những trang web lẽ ra phải tải ngay lập tức.

Kiến trúc chính là câu trả lời cho cái bẫy này. Nó là bộ khung vô hình quyết định liệu các công cụ của bạn sẽ hỗ trợ hay gây hại cho hệ thống.

Kiến trúc vững chắc thực sự có nghĩa là gì

Một kiến trúc vững chắc đơn giản là một kế hoạch phân định trách nhiệm. Nó đặt ra những câu hỏi hóc búa ngay từ sớm. Điều gì sẽ xảy ra khi một phần bị hỏng? Bạn có thể thay đổi logic thanh toán mà không cần chạm vào công cụ gợi ý không? Liệu sự gia tăng đột biến lưu lượng truy cập ở một góc của ứng dụng có khiến phần còn lại của hệ thống vẫn hoạt động bình thường không? Những câu hỏi này quan trọng hơn nhiều so với việc bạn chọn ngôn ngữ lập trình, framework hay nhà cung cấp dịch vụ đám mây nào.

Kiến trúc tốt cho bạn không gian để thay đổi quyết định. Nó xác định các ranh giới rõ ràng để thử nghiệm của một nhóm không làm mất ổn định khối lượng công việc trên môi trường production của nhóm khác. Nó coi thất bại là một điều kiện vận hành bình thường thay vì một sự cố bất ngờ. Khi bạn thiết kế có tính đến khả năng xảy ra lỗi, bạn sẽ ngừng xây dựng những "ngôi nhà bằng kính" và bắt đầu xây dựng những cấu trúc có khả năng linh hoạt.

Microservices như một mô hình thực tiễn

Một cách thực tế để đạt được loại cấu trúc đó là chia nhỏ ứng dụng của bạn thành các microservices. Thay vì một mã nguồn khổng lồ, bạn chia ứng dụng thành các phần nhỏ. Mỗi phần xử lý một công việc cụ thể. Dịch vụ thanh toán xử lý các giao dịch. Dịch vụ kho hàng theo dõi hàng tồn kho. Dịch vụ thông báo gửi email và tin nhắn văn bản. Chúng giao tiếp với nhau thông qua các giao diện được định nghĩa sẵn thay vì truy cập bộ nhớ trực tiếp hoặc dùng chung các bảng cơ sở dữ liệu.

Sự tách biệt này tạo ra không gian thực sự để xoay xở, cả về mặt kỹ thuật lẫn tổ chức.

Cập nhật các phần nhỏ mà không làm hỏng toàn bộ hệ thống

Khi các dịch vụ nhỏ và tập trung, bạn có thể vá một phần mà không lo ngại gây ra lỗi dây chuyền. Nếu đội ngũ của bạn phát hiện một lỗi trong thuật toán tính toán vận chuyển, bạn chỉ cần sửa dịch vụ đó và triển khai riêng lẻ. Phần còn lại của ứng dụng vẫn tiếp tục chạy. Người dùng vẫn duyệt sản phẩm, vẫn đăng nhập và vẫn thêm hàng vào giỏ. Phạm vi ảnh hưởng của bất kỳ thay đổi đơn lẻ nào cũng rất nhỏ. Hãy so sánh điều đó với một kiến trúc nguyên khối, nơi một lỗi đánh máy trong một hàm bổ trợ có thể làm hỏng cả quy trình thanh toán, đăng ký và báo cáo cùng một lúc.

Mở rộng các chức năng cụ thể khi lưu lượng truy cập tăng

Lưu lượng truy cập không bao giờ đồng đều trên toàn bộ ứng dụng. Trong một đợt flash sale, luồng xử lý đơn hàng của bạn có thể bị quá tải trong khi hệ thống quản lý nội dung gần như không hoạt động. Trong một hệ thống liên kết chặt chẽ, bạn phải mở rộng tất cả hoặc không gì cả. Với microservices, bạn có thể nhắm mục tiêu tài nguyên một cách chính xác. Khởi tạo thêm nhiều thực thể cho dịch vụ thanh toán. Để danh mục sản phẩm chạy với mức tài nguyên thông thường. Trong một đợt ra mắt sản phẩm, các worker xử lý hình ảnh của bạn có thể phải xếp hàng xử lý hàng ngàn ảnh thu nhỏ trong khi chỉ mục tìm kiếm vẫn hoạt động bình thường. Không có lý do gì để phải mở rộng cụm tìm kiếm chỉ để đáp ứng nhu cầu của các worker xử lý hình ảnh. Bạn chi tiền vào đúng nơi mà người dùng cảm nhận được, và hệ thống của bạn vẫn duy trì được khả năng phản hồi dưới áp lực.

Triển khai mã mới mà không cần thời gian ngừng hoạt động lâu

Các dịch vụ nhỏ cho phép áp dụng các mô hình triển khai giúp loại bỏ hoàn toàn các khoảng thời gian bảo trì (maintenance windows). Bạn có thể sử dụng triển khai cuốn chiếu (rolling deployments), đẩy mã nguồn mới đến một nhóm nhỏ các thực thể (instances) trong khi các thực thể còn lại vẫn tiếp tục phục vụ lưu lượng truy cập. Hãy theo dõi tỷ lệ lỗi, và nếu có dấu hiệu bất thường, hãy chuyển hướng các yêu cầu về phiên bản trước đó chỉ trong vài giây. Triển khai blue-green cho phép bạn thiết lập một môi trường hoàn toàn mới, xác minh nó và chuyển đổi lưu lượng truy cập với rủi ro tối thiểu. Hệ thống không cần phải ngừng hoạt động trong nhiều giờ chỉ để chờ ai đó thực hiện các bước di chuyển cơ sở dữ liệu (database migrations) bằng tay.

Xây dựng các tính năng mới nhanh hơn

Các kho mã nguồn (codebase) lớn thường tạo ra tâm lý thận trọng quá mức. Một thay đổi duy nhất đòi hỏi phải hiểu hàng ngàn dòng logic không liên quan, các bài kiểm thử hồi quy (regression tests) mất hàng giờ đồng hồ, và các lịch trình triển khai cảm giác như những vụ phóng tên lửa. Các dịch vụ nhỏ loại bỏ nỗi sợ hãi đó. Một nhóm có thể xây dựng một tính năng mới bằng cách sửa đổi vài trăm dòng mã trong một dịch vụ mà họ hiểu rõ. Họ commit, kiểm thử và phát hành ngay trong ngày. Tốc độ đó sẽ được cộng dồn. Khi các dịch vụ được giới hạn bởi các trách nhiệm rõ ràng, các nhóm sẽ ngừng can thiệp vào công việc của nhau. Họ làm chủ phạm vi (domain) của mình từ đầu đến cuối.

Sự độc lập giúp ngăn chặn các gián đoạn lớn

Mỗi dịch vụ hoạt động độc lập. Sự độc lập đó không chỉ đơn thuần là một sự tiện lợi về mặt tổ chức; nó là một lớp bảo hiểm về mặt cấu trúc. Nếu công cụ gợi ý (recommendation engine) gặp sự cố, cửa hàng vẫn phải bán được hàng. Nếu luồng phân tích (analytics pipeline) bị nghẽn do một sự kiện sai định dạng, dịch vụ đăng nhập vẫn phải xác thực được người dùng. Bạn thiết kế các bộ ngắt mạch (circuit breakers) và các đường dẫn dự phòng (fallback paths) giữa các dịch vụ để một lỗi đơn lẻ không lan rộng thành một sự cố ngừng hoạt động toàn diện. Hệ thống phát triển cùng với người dùng của bạn vì nó có thể hấp thụ áp lực mà không bị tan vỡ.

Một lời cảnh báo: Đừng chia tách một cách mù quáng

Không có nghĩa là bạn nên chia nhỏ kho mã nguồn của mình ngay từ ngày đầu tiên. Microservices đòi hỏi các ranh giới rõ ràng. Nếu các nhóm của bạn vẫn chưa biết một phạm vi kết thúc ở đâu và phạm vi khác bắt đầu từ đâu, họ sẽ tạo ra một mớ hỗn độn phân tán thay vì một hệ thống phân tán. Bạn sẽ đánh đổi độ phức tạp của mã nguồn lấy độ phức tạp về vận hành, và đột nhiên bạn phải quản lý độ trễ mạng, các giao dịch phân tán (distributed transactions), các đợt bão thử lại (retry storms) và khả năng quan sát (observability) trên hàng chục luồng log khác nhau. Việc gỡ lỗi một quy trình thanh toán chậm giờ đây có thể đồng nghĩa với việc phải truy vết một yêu cầu duy nhất qua bốn bước nhảy mạng (network hops) và ba kho lưu trữ dữ liệu khác nhau.

Nếu đội ngũ của bạn chưa sẵn sàng cho "khoản thuế" đó, thì cách chữa trị còn tệ hơn cả căn bệnh. Đôi khi, bước đi thông minh hơn là bắt đầu với một kiến trúc monolith mô-đun (modular monolith). Hãy giữ logic thanh toán tách biệt với logic kho hàng bên trong cùng một kho mã nguồn, ngay cả khi chúng được triển khai cùng nhau. Thực thi các ranh giới bằng các API nội bộ và các lược đồ cơ sở dữ liệu (database schemas) riêng biệt trong cùng một engine. Khi các điểm phân tách đó chứng minh được sự ổn định và các mô hình lưu lượng truy cập chứng minh được sự cần thiết của việc tăng chi phí vận hành, lúc đó hãy tách chúng thành một dịch vụ riêng. Kiến trúc nên là một chuỗi các cánh cửa được tính toán kỹ lưỡng, chứ không phải là những bức tường được xây lên qua đêm chỉ vì bạn vừa đọc một bài blog.

Bắt đầu với mục đích rõ ràng

Kiến trúc vững chắc không phải là việc dự đoán lưu lượng truy cập của năm năm tới. Đó là việc tạo ra các lựa chọn cho chính mình. Bạn không thể chỉ dựa vào các công cụ để phát triển ứng dụng web, nhưng bạn có thể dùng tư duy để thoát khỏi rắc rối trước khi áp lực đè nặng. Hãy tôn trọng ranh giới giữa các trách nhiệm. Xây dựng các phần nhỏ, tập trung và có khả năng tự quyết định vận mệnh của chính chúng. Hãy trao cho các nhóm quyền tự chủ để di chuyển nhanh mà không làm hỏng toàn bộ hệ thống. Khi bạn bắt đầu với một kiến trúc vững chắc, bạn sẽ tiết kiệm được thời gian và công sức về sau vì bạn không phải viết lại logic cốt lõi trong khi trang web đang gặp sự cố nghiêm trọng.

Bài học cốt lõi

Khả năng mở rộng (scalability) không phải là một tính năng bạn lắp thêm vào khi sự tăng trưởng ập đến. Nó là kết quả tự nhiên của những lựa chọn mà bạn đã thực hiện từ sớm về cách thức trách nhiệm luân chuyển trong hệ thống của mình. Hãy chọn đúng các điểm phân tách. Cô lập lỗi. Mở rộng những phần đang gặp khó khăn, và để yên những phần đang hoạt động tốt. Làm được điều đó, các công cụ bạn thêm vào sau này sẽ thực sự có một nền tảng vững chắc để dựa vào.