Lớn lên vào cuối những năm 80 và đầu những năm 90, việc tiếp xúc với máy tính của tôi hầu như chỉ giới hạn ở các máy chơi game (tôi đã cân nhắc các máy tính chơi game Atari 800 và Commodore 64 vì tôi chỉ từng thấy các game được chạy trên chúng) hoặc các hệ thống x86 đời đầu. Mãi cho đến khi vào đại học những năm 2000, tôi mới có được máy trạm SPARC, UNIX và Slackware Linux của Sun Microsystems mà tôi có thể cài đặt trên máy Intel 486 của mình ở nhà.
Trước đó, phát triển phần mềm chủ yếu là về phần mềm chạy cục bộ trên máy của bạn hoặc, nếu bạn có quyền truy cập vào phần mềm đó, một máy tính dùng chung với sức mạnh xử lý cao hơn đáng kể có sẵn để bạn… làm những việc liên quan đến kinh doanh. Ở trường đại học, tôi nhớ đã nghe nói về một chương trình được sử dụng bởi các nhà khoa học máy tính cần một bộ xử lý đa lõi để tạo lịch học của hàng nghìn sinh viên; phải mất hàng tuần để tạo và in. Cho đến nay, tôi vẫn không chắc cái nào mất nhiều thời gian hơn – chạy chương trình hay in ra giấy.
Ngày nay, phần lớn phần mềm được phát triển chạy trên đám mây, chạy trên thiết bị yêu cầu quyền truy cập vào đám mây hoặc cung cấp sức mạnh cho phần mềm khác cũng chạy trên đám mây. Rất hiếm khi làm việc trên một hệ thống phần mềm hoạt động trong không gian hạn chế (ví dụ: hệ thống phần mềm nhúng – embedded software system) không có quyền truy cập vào nền tảng điện toán mạnh hơn ở nơi khác. Các hệ thống kế toán hiện xử lý hàng đống dữ liệu được lưu trữ trong các trang trại máy chủ tại cơ sở của công ty hoặc trong data warehouse. Các hệ thống bán hàng hiện quản lý quan hệ khách hàng do bên thứ ba quản lý với các plugin được phát triển bởi nhiều bên thứ ba hoặc nhà phát triển nội bộ hơn.
Nhưng làm thế nào để các hệ thống phần mềm này được xây dựng phục vụ hàng trăm đến hàng triệu người dùng trong khi vẫn duy trì hiệu suất và khả năng phản hồi mà chúng ta ngày càng mong đợi từ phần mềm chúng ta sử dụng ngày nay?
Là một kỹ sư phần mềm trong hơn 20 năm qua, tôi đã chứng kiến nhiều hệ thống được phát triển từ mọi cấp độ của hệ thống. Trình xử lý ngắt trong những ngày DOS cho tới hoạt ảnh dựa trên JavaScript và thậm chí tạo báo cáo không cần mã. Một vài tuần trước, tôi thậm chí đã dùng ChatGPT-4 để tạo một số mã Python dựa trên một số mô tả mà tôi đã cung cấp cho những gì tôi muốn! Nhưng đó là một câu chuyện cho một ngày khác.
Trong bài viết này, chúng ta sẽ nói về thiết kế hệ thống, cách nó trở thành một phần quan trọng trong thực tiễn software engineering hiện đại và cách nó trở thành một trong những lĩnh vực chính mà các software engineer vẫn có thể mang lại giá trị trong ngắn hạn và trung hạn.
Ngày xưa, tôi là kỹ sư phần mềm trong một công ty gặp vấn đề trong việc xử lý gánh nặng thành công mà chính họ đã mang lại. Tôi sẽ gọi công ty này là Friendster. Khi tôi gia nhập công ty, dự án mà tôi được giao đã bị trễ và có nhiều lỗi liên quan đến quản lý bộ nhớ. Dịch vụ cốt lõi của họ (vâng, nó là một microservice trước khi chúng tôi gọi nó vào năm 2007) được viết bằng C++ nhưng bị rò rỉ bộ nhớ, mất quá nhiều thời gian để xử lý các yêu cầu và được thiết kế để lưu vào bộ đệm và cung cấp dữ liệu trong bộ nhớ của chính nó. Nó cần phải không trạng thái (stateless) nhưng cuối cùng lại trở thành trạng thái (stateful).
Một vài tuần sau khi bắt đầu dự án, tôi đã xin lãnh đạo kỹ thuật cấp cao bỏ việc lặp lại dịch vụ này và thay vào đó hãy viết một cái gì đó từ đầu đáp ứng các yêu cầu; nó sẽ là một sự thay thế của việc triển khai hiện có. Chúng tôi đã phải vượt qua thời hạn vì dịch vụ chỉ có thể xử lý thêm vài tháng tăng trưởng nữa trước khi không thể xử lý kích thước của bộ đệm theo cách cũ nữa.
Khởi động lại dịch vụ mất nhiều thời gian hơn thời gian nó có thể duy trì cho đến khi rò rỉ bộ nhớ làm nó ngừng hoạt động. Đây là khoảnh khắc “đặt cược sự nghiệp của tôi”. Chúng tôi phải làm cho nó hoạt động.
Trong thiết kế hệ thống. Điều đầu tiên chúng tôi làm là liệt kê những yêu cầu mà hệ thống phải đáp ứng, hợp đồng giữa các dịch vụ phụ thuộc (mã PHP frontend) và dịch vụ cốt lõi này và kế hoạch về cách chúng tôi sẽ đáp ứng ba yêu cầu phi kỹ thuật chính : hiệu suất (performance), hiệu quả (efficiency) và khả năng phục hồi (resilience).
Thiết kế hệ thống liên quan đến việc hiểu các ràng buộc theo đó hệ thống phải thực hiện chức năng của nó, các chức năng được yêu cầu là gì và những thuộc tính nào của hệ thống là quan trọng để giữ liên quan đến tất cả các thuộc tính khác. Khi bạn đã xác định những điều này, bạn có thể bắt đầu thiết kế một hệ thống đáp ứng các yêu cầu và lập kế hoạch phân phối giải pháp một cách có hệ thống.
Khi chúng ta nói về thiết kế hệ thống, thường có một số thành phần đòi hỏi điều này:
Nó giúp hiểu những điều đầu tiên như: hệ thống có độc lập không (tức là sẽ không có quyền truy cập vào các tài nguyên bên ngoài) hay nó được phân tán? Nó sẽ có giao diện người dùng hay nó sẽ không tương tác (ví dụ: nó có tạo báo cáo được in ra hay nó sẽ yêu cầu đầu vào của con người hoặc hệ thống khác trong quá trình hoạt động)? Nó có cần xử lý nhiều lưu lượng truy cập không? Nó có nghĩa là chỉ được sử dụng bởi mười người tại bất kỳ thời điểm nào hay 10 triệu người dùng sẽ sử dụng nó tại bất kỳ thời điểm nào?
Khi bạn đã có câu trả lời cho một số câu hỏi này, việc đưa ra quyết định thông qua các nguyên tắc thiết kế hệ thống sẽ dễ dàng hơn.
Một số nguyên tắc chính để thiết kế hệ thống phần mềm trong thời đại hiện đại này đã không xuất hiện cho đến khi hệ thống cần mở rộng quy mô — từ hệ thống một người dùng thành hệ thống có thể xử lý hàng nghìn, thậm chí hàng triệu người dùng cùng một lúc. Dưới đây là một số chúng tôi sẽ đề cập trong bài viết này:
Một hệ thống có khả năng mở rộng khi nó có thể được triển khai để xử lý sự tăng trưởng về tải với sự tăng trưởng tương ứng về tài nguyên. Hệ số mở rộng của một hệ thống được định nghĩa là sự tăng trưởng về lượng tài nguyên cần thiết để phục vụ cho sự tăng trưởng về tải trên hệ thống. Chúng tôi gặp hai trường hợp mở rộng điển hình với các hệ thống phần mềm: mở rộng chiều dọc và mở rộng chiều ngang.
Mở rộng chiều dọc đề cập đến việc cung cấp thêm khoảng không hoặc tài nguyên máy đơn cho hệ thống phần mềm để xử lý sự gia tăng về yêu cầu. Hãy xem xét trường hợp của một thiết bị lưu trữ gắn mạng. Bạn cung cấp càng nhiều dung lượng lưu trữ thông qua thiết bị, thì thiết bị có thể lưu trữ càng nhiều dữ liệu. Nếu bạn cần nó xử lý nhiều kết nối đồng thời hơn và Hoạt động I/O (IOP), thông thường bạn cần bổ sung thêm sức mạnh tính toán và giao diện mạng để xử lý lượng tải tăng lên.
Mở rộng theo chiều ngang đề cập đến việc sao chép một hệ thống hoặc nhiều máy bằng các bản sao của phần mềm để xử lý sự tăng trưởng các yêu cầu. Hãy xem xét trường hợp máy chủ nội dung web tĩnh ẩn sau bộ cân bằng tải. Thêm nhiều máy chủ hơn cho phép nhiều máy khách hơn kết nối và tải xuống nội dung từ các máy chủ web và khi tải đã giảm, số lượng máy chủ web có thể được thu nhỏ xuống kích thước phù hợp với nhu cầu hiện tại.
Một số hệ thống có thể xử lý mở rộng lai hoặc chéo. Ví dụ: một số kiến trúc cơ sở dữ liệu phân tán cho phép tách các nút tính toán và lưu trữ để khối lượng công việc tính toán nặng có thể sử dụng các nút có nhiều tài nguyên tính toán hơn. Ngược lại, khối lượng công việc nặng của IOP có thể chạy trên các nút lưu trữ + tính toán. Ví dụ: các ứng dụng xử lý luồng có thể tách các khối lượng công việc yêu cầu nhiều bộ nhớ và điện toán hơn (ví dụ: khối lượng công việc tìm nguồn cung ứng sự kiện hoặc phân tích) và mở rộng những khối lượng công việc đó một cách thích hợp và độc lập khỏi khối lượng công việc nặng của IOP (ví dụ: nén và lưu trữ).
Một hệ thống đáng tin cậy khi nó có thể chịu được lỗi một phần và phục hồi mà không làm giảm chất lượng dịch vụ nghiêm trọng. Một phần của độ tin cậy của hệ thống bao gồm khả năng dự đoán các hoạt động của nó về độ trễ, thông lượng và tuân thủ phạm vi hoạt động đã thỏa thuận.
Các phương pháp thông thường để đảm bảo độ tin cậy của hệ thống bao gồm:
Điều quan trọng cần nhớ để tạo ra các hệ thống đáng tin cậy là xử lý các lỗi tiềm ẩn theo cách được xác định rõ ràng mà các hệ thống phụ thuộc có thể phản ứng. Điều này nghĩa là nếu có những yếu tố đầu vào có thể khiến hệ thống khả dụng cho tất cả mọi người, thì đó không phải là một hệ thống đáng tin cậy. Tương tự, nếu hệ thống phụ thuộc vào một hệ thống khác có thể không đáng tin cậy, thì nó sẽ xử lý sự không đáng tin cậy đó bằng các chiến lược để đảm bảo độ tin cậy.
Một hệ thống có thể bảo trì được khi được thay đổi với nỗ lực tương xứng và được triển khai với sự gián đoạn tối thiểu của người dùng. Điều này đòi hỏi phải triển khai hệ thống theo cách giả định rằng các yêu cầu sẽ thay đổi và hệ thống đủ linh hoạt để xử lý những thay đổi có thể thấy trước về hướng. Điều đó cũng có nghĩa là đảm bảo rằng mã có thể đọc được để nhóm người bảo trì tiếp theo (có thể là cùng một nhóm nhưng nhìn nó bằng con mắt mới trong tương lai) có thể bảo trì phần mềm và phát triển phần mềm để đáp ứng nhu cầu trong tương lai.
Không ai muốn bị mắc kẹt trong việc duy trì phần mềm cứng nhắc, khó thay đổi, không được tổ chức tốt, tài liệu kém, thiết kế kém, chưa được kiểm tra và lắp ráp lộn xộn.
Đảm bảo chất lượng mã cao là một phần của kỹ thuật xuất sắc phản ánh tính chuyên nghiệp và tay nghề xuất sắc. Đây không chỉ là một việc nên làm mà còn được biết là cho phép các nhóm kỹ thuật có chức năng cao và hiệu suất cao cung cấp phần mềm có thể thay đổi và mở rộng để mang lại giá trị một cách nhất quán.
Nếu dịch vụ của bạn không khả dụng, nó có thể không tồn tại.
Thiết kế hệ thống nên giải quyết cách hệ thống luôn sẵn sàng để duy trì sự liên quan đến khách hàng và người dùng hệ thống. Điều này có nghĩa là:
Tôi đã sớm học được rằng một hệ thống không ổn định và không khả dụng đôi khi có thể là nguyên nhân lớn nhất khiến khách hàng của bạn mất lòng tin. Một khi bạn đã đánh mất lòng tin của khách hàng thì sẽ rất khó lấy lại được.
Thiết kế hệ thống nên coi bảo mật là một khía cạnh quan trọng, đặc biệt là trong thời đại của các hệ thống kết nối internet, nơi các mối đe dọa và lỗ hổng bảo mật có thể gây hại thực sự cho khách hàng của chúng ta và người dùng hệ thống. Mục tiêu của việc xây dựng phần mềm an toàn không phải là để đạt được sự hoàn hảo mà là để hiểu những rủi ro liên quan đến vi phạm và tấn công. Có một mô hình đe dọa bảo mật thích hợp và một cách tiếp cận có hệ thống để hiểu rủi ro nằm ở đâu và loại mối đe dọa nào đáng để ưu tiên và thiết kế các biện pháp giảm thiểu là bước khởi đầu của thực tiễn kỹ thuật và thiết kế an toàn.
Ngày nay, bảo mật không còn là tùy chọn nữa khi các hệ thống phần mềm trở thành một phần của các dịch vụ quan trọng đối với nhiều bộ phận của xã hội hiện đại. Việc coi trọng vấn đề bảo mật trong các hệ thống mà chúng ta thiết kế ngay từ đầu sẽ giúp chúng ta tiến gần hơn đến khả năng tin cậy tốt hơn vào phần mềm mà chúng tôi xây dựng và triển khai để đáp ứng nhu cầu của người dùng. Giành được lòng tin của khách hàng đã đủ khó và chỉ cần vi phạm một lần là mất đi phần lớn niềm tin.
Với các khía cạnh trên, một số hình mẫu cho các hệ thống phân tán hiện đại đã xuất hiện để giải quyết một số khía cạnh này theo những cách khác nhau. Hãy khám phá một số mẫu thiết kế phổ biến hơn mà chúng ta thấy ngày nay liên quan đến năm khía cạnh của thiết kế hệ thống.
Với sự gia tăng của các hệ thống phân tán tập trung vào việc xây dựng độ tin cậy và quy mô thông qua dự phòng, hiệu quả và hiệu suất thông qua mở rộng theo chiều ngang và khả năng phục hồi thông qua việc tách các bộ phận của hệ thống thành các dịch vụ hoạt động độc lập, thuật ngữ “microservice” đã trở nên phổ biến nhờ đạt được những điều sau:
Nhìn qua các khía cạnh của chúng ta, microservice có các thuộc tính hấp dẫn, khiến nó trở thành một mô hình tốt để tuân theo nếu nó áp dụng cho trường hợp sử dụng:
Microservices là một cách tuyệt vời để chia nhỏ một ứng dụng lớn, nơi có thể xác định các phân vùng logic yêu cầu các miền độ tin cậy và tỷ lệ mở rộng của riêng chúng. Tuy nhiên, khi bắt đầu từ đầu, việc thiết kế các vi dịch vụ ngay từ đầu sẽ kém lý tưởng hơn vì nguy cơ chia nhỏ các dịch vụ thành các phần quá nhỏ. Chi phí liên lạc giữa các vi dịch vụ — thường là các yêu cầu HTTP hoặc gRPC — là đáng kể và chỉ phát sinh nếu cần thiết. Một cách tốt để xác định xem chức năng có phù hợp với một dịch vụ hay không bằng cách làm theo một phương pháp như Thiết kế hướng miền (Domain driven design) hoặc Phân rã chức năng (Functional decomposition).
Giống như trong các giải pháp dựa trên vi dịch vụ, việc sử dụng các triển khai serverless sẽ ủy quyền thêm các phần chính của chức năng phục vụ các yêu cầu tới cơ sở hạ tầng bên dưới. Nếu trong Microservices, dịch vụ được phục vụ bởi một quy trình liên tục, thì các giải pháp Serverless thường chỉ triển khai một điểm đầu vào để xử lý yêu cầu tới một điểm cuối (thường là URI qua HTTP hoặc gRPC). Trong triển khai Serverless, không có máy chủ thực tế nào được định cấu hình mà thay vào đó, môi trường triển khai sẽ tạo ra các tài nguyên cần thiết để xử lý các yêu cầu khi chúng đến. Đôi khi, các tài nguyên đó duy trì một thời gian để khấu hao chi phí đưa chúng lên, nhưng điều đó có nghĩa là là một chi tiết thực hiện.
Hãy xem qua các khía cạnh của thiết kế hệ thống để xem các giải pháp Serverless sắp xếp như thế nào:
Các giải pháp không có máy chủ, hay Chức năng dưới dạng Dịch vụ, là một cách rất hấp dẫn để tạo nguyên mẫu và thậm chí triển khai các giải pháp cấp sản xuất bằng cách tập trung vào giá trị và logic kinh doanh, đồng thời để cơ sở hạ tầng cơ bản xử lý khả năng mở rộng, độ tin cậy và tính khả dụng của dịch vụ. Đó là một điểm khởi đầu điển hình để có được một giải pháp với gánh nặng vận hành tối thiểu và đối với hầu hết các nguyên mẫu, đó là một cách tuyệt vời để chứng minh giả thuyết của chúng tôi. Đây cũng là một trải nghiệm điển hình khi một khi các giải pháp này đạt đến giới hạn mở rộng quy mô, chi phí liên quan đến việc vận hành các giải pháp này sẽ trở nên đủ cao. Chúng được biến thành các triển khai vi dịch vụ tối ưu hơn được điều chỉnh theo quy mô cần thiết.
Tuy nhiên, có một số lĩnh vực sự cố nơi mà việc xử lý giao dịch trực tuyến không được yêu cầu và các triển khai vi dịch vụ và không có máy chủ không hoàn toàn phù hợp với hóa đơn. Hãy xem xét các trường hợp xử lý giao dịch có thể được thực hiện trong nền hoặc khi có sẵn tài nguyên. Một trường hợp khác dành cho các hoạt động xử lý nền trong đó kết quả không nhất thiết phải có tính tương tác.
Các hệ thống hướng sự kiện tuân theo mô hình có một nguồn sự kiện (event source) và các bồn sự kiện (event sinks) – nơi các sự kiện (tin nhắn) đến từ đó và được gửi tương ứng. Quá trình xử lý lần lượt xảy ra từ người đăng ký và nhà xuất bản đến các source và sink này. Một ví dụ về hệ thống hướng sự kiện là một chatbot có thể tham gia vào nhiều thảo luận (event source và sinks) và xử lý tin nhắn khi chúng đến.
Các hệ thống hướng sự kiện phân tán có thể có nhiều trình xử lý tin nhắn đồng thời đang chờ trên cùng một nguồn, có khả năng xuất bản nhiều sink đóng vai trò là nguồn cho các trình xử lý tin nhắn khác. Mô hình kết nối các bộ xử lý với nhau thông qua source và sink được gọi là quy trình sự kiện (event pipeline). Thông thường, có một triển khai duy nhất cho các sink và source cung cấp giao diện hàng đợi tin nhắn và mở rộng theo nhu cầu về các tin nhắn đi qua hệ thống. Nhiều hệ thống quản lý hàng đợi phân tán cũng có thể hưởng lợi từ việc mở rộng quy mô theo đường chéo một cách hiệu quả, như Apache Kafka, RabbitMQ, v.v.
Hãy xem xét các hệ thống hướng sự kiện phân tán thông qua năm khía cạnh:
Software engineering hiện đại đòi hỏi phải thiết kế các hệ thống có thể mở rộng, đáng tin cậy, có thể bảo trì, khả dụng và an toàn. Việc thiết kế các hệ thống phân tán đòi hỏi sự nghiêm ngặt đáng kể vì thực tế phức tạp của hệ thống hiện đại ngày càng tăng cùng với nhu cầu của xã hội về các dịch vụ phần mềm tốt hơn. Chúng ta đã xem xét ba hình mẫu thiết kế hiện đại cho các hệ thống phân tán và làm việc thông qua năm khía cạnh của các hệ thống được thiết kế tốt.
Là kỹ sư phần mềm, chúng ta chịu trách nhiệm thiết kế các hệ thống giải quyết các mối quan tâm chính của hệ thống phân tán trong thời hiện đại.
Nguồn: Dean Michael Berris