-1

Tại sao Microservices chỉ là một giải pháp "chắp vá" - Và tương lai sẽ trông như thế nào - CScaf #1

Phần I: Cuộc Khủng hoảng Thầm lặng - Nền tảng của Vấn đề

Chương 1: Giả định Nguyên thủy: Mô hình "Một Máy tính"

Để hiểu được sự phức tạp của kiến trúc phần mềm hiện đại, chúng ta phải quay trở lại và kiểm tra giả định nền tảng, gần như vô hình, đã định hình nên mọi ngôn ngữ lập trình chính thống mà chúng ta sử dụng ngày nay. Từ Fortran và C, đến Java và C#, tất cả đều được sinh ra từ một mô hình tư duy duy nhất: chương trình được viết ra để thực thi trên một máy tính duy nhất, trong một không gian địa chỉ bộ nhớ duy nhất.

Lịch sử của sự trừu tượng hóa trong khoa học máy tính là một chuỗi các nỗ lực nhằm che giấu đi sự phức tạp của phần cứng bên dưới. Assembly trừu tượng hóa mã máy. Các ngôn ngữ thủ tục như C trừu tượng hóa kiến trúc thanh ghi và các lệnh nhảy. Lập trình Hướng đối tượng (OOP) trừu tượng hóa việc tổ chức dữ liệu và hành vi. Nhưng tất cả những lớp trừu tượng này đều hoạt động bên trong cùng một "hộp cát" nguyên thủy: một tiến trình (process) đơn lẻ, chạy trên một kernel đơn lẻ. Toàn bộ mô hình an toàn bộ nhớ của Java và C#, cơ chế quản lý luồng, và hệ thống kiểu (type system) đều được tối ưu hóa cho thế giới nội tại của một process. Chúng không được thiết kế với nhận thức về một thế giới tồn tại bên ngoài ranh giới đó.

Tuy nhiên, thực tế của các ứng dụng quy mô lớn trong kỷ nguyên Internet đã hoàn toàn phá vỡ giả định này. Không một ứng dụng quan trọng nào ngày nay chỉ chạy trên một máy tính duy nhất. Chúng là các hệ thống phân tán, bao gồm hàng trăm, thậm chí hàng triệu tiến trình, trải dài trên nhiều trung tâm dữ liệu.

Sự mâu thuẫn giữa một mô hình lập trình được thiết kế cho "một máy" và một thực tế triển khai yêu cầu "nhiều máy" chính là nguồn gốc của gần như mọi sự phức tạp mà chúng ta phải đối mặt. Toàn bộ hệ sinh thái Cloud Native—microservices, containers, Kubernetes, service mesh—không phải là một bước tiến hóa tự nhiên của lập trình. Chúng là một cơ chế hỗ trợ sự sống (life support system) cực kỳ phức tạp, được phát minh ra để bù đắp cho sự thiếu sót nguyên thủy của chính các ngôn ngữ lập trình. Chúng ta đang sử dụng một lớp hạ tầng khổng lồ để "chắp vá", để "đánh lừa" các chương trình được viết cho một máy tính tin rằng chúng có thể hoạt động trong một thế giới phân tán.

Chương 2: Sự Phân đôi Giả tạo: Năng suất vs. Hiệu năng

Sự rạn nứt nền tảng này càng trở nên trầm trọng hơn bởi một sự phân đôi lịch sử khác, một sự đánh đổi mà chúng ta đã chấp nhận như một điều tất yếu: sự lựa chọn giữa Năng suất và An toàn so với Hiệu năng và Kiểm soát.

Lịch sử của các ngôn ngữ lập trình đã rẽ thành hai con đường riêng biệt:

  1. Con đường Ngôn ngữ Được quản lý (Managed Languages): Bắt đầu từ Lisp và Smalltalk, và được thương mại hóa thành công bởi Java và C#, con đường này ưu tiên sự an toàn và năng suất của lập trình viên. Bằng cách giới thiệu một runtime trung gian (JVM, CLR) với Garbage Collector (GC), chúng đã loại bỏ một loạt các lỗi quản lý bộ nhớ nguy hiểm và cho phép các đội ngũ xây dựng các ứng dụng phức tạp một cách nhanh chóng. Tuy nhiên, cái giá phải trả là sự mất đi quyền kiểm soát ở cấp độ thấp. Lập trình viên bị tách biệt khỏi bộ nhớ và phần cứng bởi một lớp trừu tượng, tạo ra một "trần" hiệu năng và độ trễ không thể đoán trước được, điều không thể chấp nhận được trong các ứng dụng yêu cầu độ trễ cực thấp.
  2. Con đường Ngôn ngữ Hệ thống (Systems Languages): Kế thừa di sản của C, con đường này, được hiện đại hóa bởi C++ và gần đây là Rust, ưu tiên hiệu năng và sự kiểm soát tuyệt đối. Các ngôn ngữ này cung cấp quyền truy cập trực tiếp vào bộ nhớ, cho phép tối ưu hóa đến từng chu kỳ lệnh. Đây là lựa chọn bắt buộc cho các tác vụ tính toán hiệu năng cao (HPC), các hệ thống nhúng, và các thành phần cốt lõi của hệ điều hành. Cái giá phải trả là một trải nghiệm lập trình khắc nghiệt, một đường cong học tập dốc đứng, và gánh nặng của việc quản lý bộ nhớ thủ công luôn đè nặng lên vai lập trình viên.

Sự phân đôi này không chỉ là một vấn đề học thuật. Nó có những hậu quả trực tiếp và tốn kém về mặt kiến trúc. Nó buộc các tổ chức phải xây dựng các hệ thống đa ngôn ngữ (polyglot). Logic nghiệp vụ có thể được viết bằng C# để phát triển nhanh, nhưng các service xử lý thuật toán cốt lõi lại phải được viết bằng C++ để đạt được hiệu năng cần thiết.

Kết luận của Phần I:

Chúng ta đang bị mắc kẹt. Chúng ta bị mắc kẹt bởi những ngôn ngữ được thiết kế cho một máy tính duy nhất trong một thế giới nhiều máy tính. Và chúng ta bị mắc kẹt bởi sự lựa chọn giữa việc xây dựng nhanh và việc xây dựng nhanh chóng.

Hai vấn đề này kết hợp lại đã dẫn đến một kết quả không thể tránh khỏi: một codebase bị phân mảnh một cách tàn bạo. Sự phân mảnh này không chỉ là một quyết định kiến trúc; nó là một triệu chứng của những giới hạn sâu sắc trong chính các công cụ mà chúng ta đang sử dụng. Để xây dựng các hệ thống của tương lai, chúng ta không cần một framework tốt hơn hay một công cụ DevOps thông minh hơn. Chúng ta cần một loại công cụ mới, một ngôn ngữ được xây dựng trên một bộ giả định hoàn toàn khác.

Phần II: Một Mô hình Mới - Lập trình Hướng Cấu trúc

Chương 3: Sự Tiến hóa của Trừu tượng hóa và Sự ra đời của SOP

Lịch sử của khoa học máy tính, về bản chất, là lịch sử của sự trừu tượng hóa. Mỗi bước nhảy vọt về năng suất và khả năng đều đến từ việc chúng ta phát minh ra một lớp trừu tượng mới, cho phép chúng ta quản lý một quy mô phức tạp lớn hơn. Chúng ta đã tiến hóa từ việc trừu tượng hóa lệnh máy (Assembly), đến luồng điều khiển (Ngôn ngữ Thủ tục), và sau đó là tổ chức code (Lập trình Hướng đối tượng - OOP).

Mỗi bước tiến hóa này đều không thay thế hoàn toàn cái cũ; nó xây dựng một lớp mới bên trên nó. OOP, dù là một cuộc cách mạng, vẫn hoạt động bên trong mô hình thực thi tuần tự của các ngôn ngữ thủ tục. Ngày nay, chúng ta đang đứng trước một bức tường phức tạp mới. Nguồn gốc của sự phức tạp không còn nằm bên trong một class hay một module nữa. Nó nằm ở cấu trúc, sự tương tác, và môi trường triển khai của các thành-phần-quy-mô-lớn trong một hệ thống. OOP không có từ vựng hoặc các khái niệm cơ bản để mô tả và quản lý loại phức tạp này.

Đây là lúc cần đến bước tiến hóa tiếp theo: Lập trình Hướng Cấu trúc (Structural-Oriented Programming - SOP).

SOP không phải là một sự thay thế cho OOP. Nó là một siêu tập hợp (superset), một lớp trừu tượng hóa mới được xây dựng bên trên OOP. Nếu OOP là về vi kiến trúc (micro-architecture) của các đối tượng, thì SOP là về vĩ kiến trúc (macro-architecture) của toàn bộ hệ thống. Triết lý cốt lõi của SOP là nâng các khái niệm kiến trúc—những thứ hiện đang được quản lý bởi các file YAML, các công cụ DevOps, và các quy ước bất thành văn—trở thành những công dân hạng nhất (first-class citizens) của chính ngôn ngữ lập trình. CScaf là nỗ lực đầu tiên để hiện thực hóa triết lý này.

Điều này dẫn chúng ta đến lời hứa cốt lõi của nó:

Slogan: "Code once, grow forever."

Câu nói này có ý nghĩa gì? "Code once" không có nghĩa là bạn chỉ viết code một lần và không bao giờ thay đổi nó. Nó có nghĩa là bạn có thể xây dựng toàn bộ hệ thống của mình, từ một MVP đơn giản đến một nền tảng quy mô toàn cầu, trong một codebase duy nhất, mạch lạc. "Grow forever" có nghĩa là kiến trúc của codebase đó có thể phát triển và thích ứng với mọi yêu cầu về quy mô mà không cần phải trải qua những cuộc "đập đi xây lại" tốn kém. Nó hứa hẹn một sự tăng trưởng bền vững, nơi sự phức tạp được quản lý bởi ngôn ngữ, không phải bởi sức lực của con người.

Chương 4: Giải phẫu của CScaf - Các Primitive của Kiến trúc

Để hiện thực hóa lời hứa này, CScaf giới thiệu một bộ ba các khái niệm cơ bản (primitives) hoàn toàn mới. Chúng không mô tả hành vi của code, mà mô tả bối cảnh và cấu trúc mà trong đó code được thực thi.

1. Space: Đơn vị của Sự cô lập Phân tán (The Unit of Distributed Isolation)

Space là khái niệm trung tâm của CScaf. Một Space là một dịch vụ logic (logical service) hoàn chỉnh, một đơn vị kiến trúc có thể được triển khai và thực thi một cách độc lập.

Nguồn gốc của Space không phải là nhu cầu về sandboxing, mà là sự thừa nhận một sự thật cơ bản của các hệ thống hiện đại: một thành phần logic không nhất thiết phải tồn tại trong cùng một không gian vật lý với các thành phần khác. Trong một ứng dụng quy mô lớn, BillingService của bạn có thể đang chạy trên một cụm máy chủ ở Virginia, trong khi UserService lại chạy ở Frankfurt.

Các ngôn ngữ hiện tại không có một khái niệm gốc nào để mô tả sự thật này. CScaf làm cho sự thật này trở thành một công dân hạng nhất. Khi bạn định nghĩa một Space, bạn đang nói với trình biên dịch: "Thành phần này, về mặt khái niệm, là một thực thể độc lập. Hãy đối xử với nó như thể nó có thể nằm ở bất cứ đâu trên thế giới."

Sự cô lập của Space không phải là một "hàng rào" nhân tạo được dựng lên để bảo mật. Sự cô lập đó là hệ quả tự nhiên của việc nó có thể được triển khai ở một vị trí vật lý khác. Bạn không thể truy cập trực tiếp vào bộ nhớ của một máy chủ ở bên kia địa cầu, và do đó, ngôn ngữ cũng không cho phép bạn làm điều đó.

2. Mode: Quang phổ của Môi trường Thực thi (The Spectrum of Execution Environment)

Mode là một thuộc tính của Space cho phép lập trình viên phá vỡ sự phân đôi "Năng suất vs. Hiệu năng". Nó định nghĩa các quy tắc vật lý của vũ trụ Space đó.

  • Tại sao chúng ta cần nó? Bởi vì một ứng dụng hiện đại có nhiều phần với các yêu cầu khác nhau. Logic xử lý thanh toán cần sự an toàn tuyệt đối của một môi trường Managed (có GC). Nhưng thuật toán đề xuất sản phẩm lại cần hiệu năng tối đa của một môi trường Unmanaged (không có GC).
  • Các ngôn ngữ hiện tại buộc bạn phải tách chúng ra hai codebase, hai ngôn ngữ khác nhau. Mode cho phép bạn đưa ra quyết định đánh đổi đó cho từng phần của ứng dụng, giữ chúng cùng tồn tại một cách hài hòa trong một codebase duy nhất.

3. Runtime: Quang phổ của Mô hình Triển khai (The Spectrum of Deployment Model)

Runtime là một thuộc tính của Space cho phép lập trình viên phá vỡ sự đánh đổi "Monolith vs. Microservices".

  • Tại sao chúng ta cần nó? Bởi vì quyết định kiến trúc không nên là một quyết định phải đưa ra một lần và mãi mãi. Runtime định nghĩa sự hiện thực hóa vật lý của sự cô lập logic mà Space đã thiết lập.
    • ThreadGroup Runtime: Bạn nói với trình biên dịch: "Mặc dù về mặt logic BillingSpace là độc lập, nhưng trong lần triển khai này, hãy đặt nó chạy bên trong cùng một process để có hiệu năng tối đa."
    • Isolate Runtime: Bạn nói: "Bây giờ, hãy hiện thực hóa sự cô lập logic đó thành sự cô lập vật lý. Hãy triển khai BillingSpace như một process riêng biệt."
  • Sự kết hợp Thiên tài: Bộ đôi Space và Runtime cho phép bạn viết code một lần duy nhất, dựa trên một mô hình kiến trúc phân tán, an toàn. Sau đó, bạn có thể "tinh chỉnh" hiệu năng và mô hình triển khai của nó bằng cách thay đổi cấu hình, mà không cần phải thay đổi một dòng code logic nào. Đây chính là chìa khóa để có một codebase có thể "phát triển mãi mãi" một cách mượt mà và hiệu quả.

Bằng cách kết hợp ba primitive này, CScaf trao cho lập trình viên một bộ điều khiển kiến trúc chưa từng có, cho phép họ điêu khắc hệ thống của mình để đáp ứng chính xác các yêu cầu về cả logic, hiệu năng, và triển khai.

4. Overspace: Tính Đa hình ở Cấp độ Kiến trúc (Polymorphism at the Architectural Level)

Cuối cùng, để cho phép các Space với các Mode khác nhau (ví dụ: Managed và Unmanaged) có thể hợp tác một cách liền mạch, SOP giới thiệu một hình thức đa hình mới, mạnh mẽ hơn: overspace.

Trong OOP, đa hình cho phép các đối tượng khác nhau đáp ứng cùng một thông điệp. Trong SOP, overspace cho phép cùng một khái niệm logic (ví dụ: một DataBuffer) có những biểu diễn và hành vi hoàn toàn khác nhau tùy thuộc vào Space mà nó đang tồn tại. Một DataBuffer có thể là một List<byte> an toàn bên trong một Managed Space, nhưng lại là một con trỏ thô (byte*) hiệu năng cao bên trong một Unmanaged Space.

overspace là cơ chế cho phép một codebase duy nhất có thể bắc cầu qua hai thế giới "Năng suất" và "Hiệu năng" một cách an toàn và có cấu trúc. Đây là một khái niệm sâu sắc và sẽ được khám phá chi tiết trong một LDM chuyên biệt trong tương lai.

Phần III: Các Cơ chế Cốt lõi - "Nó hoạt động như thế nào?"

Sau khi đã hiểu về triết lý và các khái niệm kiến trúc nền tảng của SOP, câu hỏi tiếp theo là: Các cơ chế kỹ thuật nào cho phép CScaf thực hiện được những lời hứa táo bạo đó? Phần này sẽ đi sâu vào hai cơ chế cốt lõi làm nên sự khác biệt của CScaf: mô hình giao tiếp và mô hình thực thi.

Chương 5: Hợp đồng Máy móc - Cuộc Cách mạng của swait

Trong các hệ thống phân tán truyền thống, giao tiếp giữa các service là một trong những nguồn gây ra lỗi và sự phức tạp lớn nhất. CScaf giải quyết vấn đề này bằng cách giới thiệu một primitive giao tiếp mới, swait, và một mô hình hợp đồng hoàn toàn khác.

1. Vấn đề: Sự Cứng nhắc của Hợp đồng Định nghĩa trước (Pre-defined Contracts)

  • Hiện trạng là gì? Giao tiếp giữa các microservice hiện nay dựa trên các hợp đồng cố định, được định nghĩa trước, thường là các file .proto của gRPC.
  • Tại sao nó lại là vấn đề? Mô hình này buộc các lập trình viên phải dự đoán trước mọi khả năng. Để tránh phải thay đổi schema liên tục, các file .proto thường được định nghĩa với rất nhiều trường dữ liệu optional hoặc các cấu trúc chung chung, phòng cho các trường hợp sử dụng trong tương lai. Điều này dẫn đến các "request" cồng kềnh, mang theo nhiều dữ liệu thừa không cần thiết cho một kịch bản cụ thể. Chúng ta phải định nghĩa một "hợp đồng" cho tất cả các kịch bản có thể xảy ra, thay vì một hợp đồng tối ưu cho kịch bản đang diễn ra.

2. Giải pháp: swait và Hợp đồng Động, được Suy luận tại Thời điểm Biên dịch (Dynamic, Inferred Compile-Time Contracts)

  • swait là gì? swait không chỉ là một lời gọi hàm. Nó là một khối code, một "closure" được gửi đến một Space khác để thực thi.
  • Tại sao chúng ta cần nó? swait được tạo ra để thay thế các hợp đồng cố định, do con người định nghĩa, bằng các hợp đồng động, được máy móc suy luận ra cho từng kịch bản cụ thể.
  • Nó hoạt động như thế nào? Khi trình biên dịch CScaf gặp một khối swait
swait Billing {
        var userProfile = await GetUserProfileAsync(order.UserId);
        if (userProfile.IsPremium) {
            ApplyDiscount(order, 0.1m);
        }
        ProcessPayment(order);
    }

Nó không chỉ kiểm tra chữ ký của một phương thức. Nó làm một việc thông minh hơn nhiều:

  1. Nó phân tích toàn bộ code bên trong thân của khối swait.
  2. Nó xác định chính xác những phương thức nào (GetUserProfileAsync, ApplyDiscount, ProcessPayment) được gọi và những biến nào từ môi trường bên ngoài (order) được sử dụng.
  3. Sau đó, nó tự động xây dựng một schema request tối ưu nhất có thể cho chính xác kịch bản này. Request đó sẽ chỉ chứa order.UserId và order object. Nó sẽ không mang theo bất kỳ dữ liệu thừa nào.
  • Tác dụng gì? Mỗi lời gọi swait sẽ tạo ra một request được "may đo" hoàn hảo. Điều này giúp loại bỏ hoàn toàn vấn đề "over-fetching" dữ liệu, mang lại hiệu năng mạng vượt trội so với các API được thiết kế theo kiểu "một kích cỡ cho tất cả".

3. "Tất cả code đã đều được hash code" - Sự Xác thực Tuyệt đối

  • Vấn đề: Làm thế nào để đảm bảo Service A và Service B đang nói về cùng một phiên bản của một phương thức?
  • Giải pháp: Trong quá trình biên dịch, CScaf có thể gán một hash duy nhất cho chữ ký và thân của mỗi phương thức có thể được gọi từ bên ngoài.
  • Tại sao lại như vậy? Thay vì "quy ước bằng miệng" hay dựa vào versioning, các lời gọi swait sẽ bao gồm cả hash của phương thức mà nó muốn gọi. Runtime ở phía nhận sẽ xác thực hash này. Nếu có bất kỳ sự không khớp nào, nó sẽ báo lỗi ngay lập tức.
  • Tác dụng gì? Điều này mang lại một mức độ an toàn và tin cậy tuyệt đối, loại bỏ hoàn toàn các lỗi do sự không tương thích phiên bản giữa các service.

4. "Object-as-Handle" và Marshalling có Chủ đích

  • Vấn đề: Việc truyền các đối tượng phức tạp qua mạng rất tốn kém, không an toàn, và thường không cần thiết. Làm thế nào để CScaf kiểm soát được dòng chảy dữ liệu một cách hiệu quả?
  • Giải pháp: CScaf giới thiệu một khái niệm mới và nghiêm ngặt về đối tượng liên-Space. Khi bạn có một tham chiếu đến một đối tượng từ một Space khác, bạn không đang giữ một bản sao của đối tượng đó. Bạn chỉ đang giữ một "object-as-handle"—một con trỏ mờ (opaque pointer), một định danh duy nhất trỏ đến đối tượng gốc trong Space của nó. Bản thân handle này không chứa bất kỳ dữ liệu nghiệp vụ nào.
  • Tại sao lại như vậy? Điều này tạo ra một mô hình "an toàn theo mặc định" (secure-by-default). Nó ngăn chặn việc vô tình làm rò rỉ hoặc truyền những đối tượng khổng lồ qua ranh giới Space.
  • Tác dụng gì? Dữ liệu chỉ được truyền đi khi lập trình viên chủ động và tường minh truyền nó dưới dạng các tham số bên trong một khối swait. Mô hình này buộc lập trình viên phải suy nghĩ cẩn thận về đâu là ranh giới của một Space và dữ liệu nào thực sự cần phải vượt qua ranh giới đó, thúc đẩy việc thiết kế các API gọn nhẹ, hiệu quả, và an toàn.

5. SpaceWire: Giao thức Tối ưu hóa theo Kịch bản và Serialization không Schema

  • Vấn đề: Các giao thức hiện tại như gRPC/Protobuf rất mạnh mẽ, nhưng chúng dựa trên các schema cố định, do con người định nghĩa. Ngay cả trong các định dạng nhị phân hiệu quả nhất, chúng vẫn thường phải gửi kèm siêu dữ liệu (metadata) như tên hoặc số thứ tự của các trường (field tags) để bên nhận có thể diễn giải dữ liệu. Điều này tạo ra một chi phí thừa, dù nhỏ.
  • Cơ hội: Vì trình biên dịch CScaf có thể phân tích code bên trong thân của mỗi khối swait, nó có một sự hiểu biết hoàn hảo và đồng bộ về cả hai phía của cuộc giao tiếp. Nó không chỉ biết phương thức nào được gọi, mà còn biết chính xác cấu trúc và thứ tự của từng byte dữ liệu sẽ được gửi đi.
  • Giải pháp: Tận dụng cơ hội này, CScaf giới thiệu một giao thức nhị phân tự động là SpaceWire. Thay vì sử dụng một schema cố định, SpaceWire thực hiện một cuộc cách mạng về serialization:
    1. Tại thời điểm biên dịch, cho mỗi khối swait duy nhất, trình biên dịch sẽ tạo ra một "hợp đồng serialization" (serialization contract) cực kỳ cứng nhắc và tối giản. Hợp đồng này không cần tên trường, không cần thẻ (tags), nó chỉ đơn giản là một thứ tự byte (byte order) đã được thống nhất.
    2. Ví dụ, hợp đồng có thể quy định rằng: "4 byte đầu tiên là một int32 cho userId, 8 byte tiếp theo là một double cho price, và phần còn lại là một chuỗi UTF-8 có độ dài được xác định bởi 2 byte trước đó."
    3. Cả bên gửi và bên nhận đều được sinh ra code để đọc/ghi theo chính xác hợp đồng này bởi cùng một trình biên dịch.
  • Tại sao Mô hình này lại Khả thi và An toàn?
    • Bởi vì đây là một quy ước cứng được thực thi bởi máy móc (a hard, machine-enforced convention), không phải là một quy ước mềm bằng lời nói.
    • Cả hai bên của cuộc giao tiếp đều được sinh ra từ cùng một nguồn chân lý duy nhất (code CScaf) và cùng một công cụ (trình biên dịch).
    • Tỷ lệ lỗi do không khớp (mismatch error) gần như bằng không, bởi vì nếu có bất kỳ sự thay đổi nào trong cấu trúc dữ liệu, toàn bộ dự án sẽ không thể biên dịch được cho đến khi cả hai bên được tái sinh mã nguồn.
  • Tác dụng gì?
  • Hiệu năng Tối đa: SpaceWire có khả năng tạo ra các gói tin (payloads) nhỏ nhất có thể về mặt lý thuyết. Nó loại bỏ gần như toàn bộ chi phí thừa của metadata, chỉ truyền đi dữ liệu thô. Điều này cực kỳ quan trọng trong các hệ thống có thông lượng cao.
  • Năng suất Tối đa: Lập trình viên không bao giờ phải suy nghĩ về serialization. Toàn bộ quá trình phức tạp này là hoàn toàn trong suốt và được tự động hóa, cho phép họ tập trung vào logic nghiệp vụ.

Chương 6: Sự Hài hòa giữa Tuần tự và Song song

1. Vấn đề: Sự Phân đôi Giả tạo

Các ngôn ngữ hiện tại đã tạo ra một sự phân đôi giả tạo và đầy đau đớn giữa code "đồng bộ" (sync) và "bất đồng bộ" (async). Chúng ta bị buộc phải đánh dấu các hàm bằng từ khóa async, tạo ra hai "màu" khác nhau của các hàm, và sự tương tác giữa chúng là nguồn gốc của vô số lỗi phức tạp, điển hình là "sync-over-async" deadlock.

Sự phân đôi này xuất phát từ một sự hiểu lầm cơ bản. Vấn đề không nằm ở bản thân sự tuần tự.

Sự tuần tự, ở một quy mô nhỏ, là tự nhiên và cần thiết. Khi một lập trình viên viết logic bên trong một hàm, họ đang tư duy một cách tuần tự: làm bước A, rồi đến bước B, rồi đến bước C. Đây là một mô hình tư duy mạnh mẽ và rõ ràng. Ngay cả trong một hàm async của C# hiện tại, các dòng code giữa các điểm await vẫn thực thi một cách tuần tự.

Vấn đề thực sự nảy sinh khi chúng ta cố gắng áp đặt sự tuần tự đó ở quy mô lớn, đặc biệt là khi các lời gọi hàm vượt ra khỏi ranh giới của một đơn vị công việc logic.

2. Giải pháp: Tuần tự Vi mô, Bất đồng bộ Vĩ mô

CScaf giải quyết vấn đề này không phải bằng cách loại bỏ sự đồng bộ, mà bằng cách đặt nó vào đúng vị trí của nó. Triết lý của CScaf là:

  • Sự tuần tự là mặc định ở cấp độ vi mô (bên trong thân một hàm).
  • Sự bất đồng bộ là mặc định ở cấp độ vĩ mô (khi tương tác giữa các hàm).

CScaf sẽ loại bỏ hoàn toàn cả hai từ khóa async và sync. Thay vào đó, mô hình thực thi được suy luận một cách tự nhiên từ chính cấu trúc của code:

  1. Thân hàm là Tuần tự:
    • Bên trong một thân hàm, các dòng code vẫn được thực thi theo thứ tự từ trên xuống dưới, giống như lập trình viên mong đợi. Sự đơn giản và dễ hiểu của code tuần tự được bảo tồn hoàn toàn ở cấp độ nhỏ nhất.
  2. Mọi Lời gọi Hàm đều là Bất đồng bộ:
    • Đây là bước nhảy vọt. Trong CScaf, bất kỳ lời gọi nào đến một hàm khác (AnotherFunction()) đều không chặn. Về mặt khái niệm, nó ngay lập tức trả về một "lời hứa" (a future) về một kết quả.
    • Tại sao lại như vậy? Bởi vì CScaf không đưa ra giả định nào về việc AnotherFunction() nằm ở đâu. Nó có thể nằm trong cùng một Space, hoặc nó có thể nằm trên một máy chủ ở bên kia địa cầu. Bằng cách coi mọi lời gọi hàm là bất đồng bộ theo mặc định, CScaf tạo ra một mô hình nhất quán cho cả code cục bộ và code phân tán.
  3. Từ khóa await (hoặc swait) là Cầu nối:
    • Làm thế nào để chúng ta lấy được kết quả từ "lời hứa" đó? Bằng cách sử dụng một từ khóa duy nhất để đồng bộ hóa: await (hoặc swait cho các lời gọi liên-Space để làm rõ ý định kiến trúc).
    • await không phải là thứ "bật" chế độ bất đồng bộ. await là thứ tạm dừng sự thực thi tuần tự của thân hàm hiện tại để chờ đợi kết quả từ một hoạt động bất đồng bộ khác.

3. Tác dụng của Mô hình Hài hòa này

  • Loại bỏ Lỗi Kinh điển: Mô hình này làm cho "sync-over-async" deadlock trở nên bất khả thi về mặt cú pháp. Bạn không thể "chặn" một cách mù quáng (.Result), bạn chỉ có thể "chờ đợi" một cách an toàn (await).
  • Tư duy Tự nhiên: Lập trình viên không cần phải "nhuộm màu" các hàm của họ bằng async. Họ chỉ cần viết code một cách tự nhiên. Dòng chảy tuần tự bên trong một hàm vẫn được giữ nguyên. Sự bất đồng bộ chỉ xuất hiện khi cần thiết, tại các điểm tương tác giữa các hàm.
  • Khả năng Mở rộng Liền mạch: Mô hình này không có sự khác biệt cơ bản giữa việc gọi một hàm trong cùng một process và việc gọi một hàm trên một process khác. Điều này cho phép một codebase CScaf có thể được "kéo dãn" từ một monolith (ThreadGroup) ra thành một hệ thống microservices (Isolate) mà không cần thay đổi logic hay mô hình lập trình.

Bằng cách này, CScaf không buộc lập trình viên phải từ bỏ sự đơn giản của tư duy tuần tự. Thay vào đó, nó đặt sự tuần tự đó vào bên trong những "hộp" an toàn (các thân hàm), và sau đó cung cấp một cơ chế bất đồng bộ, nhất quán, và an toàn để kết nối những chiếc hộp đó lại với nhau thành một hệ thống lớn.

Phần IV: Tầm nhìn - Một Trải nghiệm Phát triển Thống nhất

Những cơ chế và triết lý mà chúng ta đã thảo luận không chỉ là những cải tiến kỹ thuật. Khi kết hợp lại, chúng tạo ra một trải nghiệm phát triển hoàn toàn mới, một trải nghiệm được thiết kế để giải quyết sự hỗn loạn của các hệ thống quy mô lớn.

Hãy tưởng tượng về một tương lai khác cho việc phát triển phần mềm quy mô lớn.

Thực tế ngày nay là một "quần đảo microservice": hàng trăm repository riêng biệt, mỗi cái là một hòn đảo cô lập. Để sửa một lỗi hay phát triển một tính năng xuyên suốt, một lập trình viên phải trở thành một nhà khảo cổ học, đào bới qua vô số tài liệu wiki để hiểu được các mối phụ thuộc. IDE của họ trở thành một trình soạn thảo văn bản đơn giản, nơi tính năng "Go to Definition" dừng lại ở một lời gọi HTTP vô hồn. Việc tái cấu trúc một mô hình dữ liệu cốt lõi là một nỗi sợ hãi, một nhiệm vụ kéo dài hàng năm trời với một "bán kính nổ" không thể lường trước được.

Đây không phải là một sự thất bại của các kỹ sư. Đây là một sự thất bại của các công cụ. Đó là lý do tại sao các gã khổng lồ công nghệ như Google phải đầu tư hàng tỷ đô la vào các hệ thống monorepo phức tạp như Piper. Họ hiểu một sự thật cơ bản: ngay cả khi các dịch vụ được triển khai độc lập, việc có một codebase thống nhất là một lợi thế khổng lồ để quản lý sự phức tạp.

CScaf và Lập trình Hướng Cấu trúc (SOP) đưa triết lý này đến kết luận logic của nó. Nó không chỉ đặt code vào cùng một nơi; nó làm cho ngôn ngữ và trình biên dịch nhận thức được toàn bộ kiến trúc hệ thống.

Hãy tưởng tượng về thế giới đó:

  • Một kỹ sư checkout một repository duy nhất.
  • IDE hiểu được toàn bộ hệ thống. "Go to Definition" trên một lời gọi swait sẽ ngay lập-tức-đưa-họ từ Order Space đến Billing Space, dù chúng được định mệnh để chạy trên hai lục địa khác nhau.
  • Việc tái cấu trúc một mô hình User trở thành một hoạt động không còn đáng sợ, được đảm bảo an toàn bởi trình biên dịch trên toàn bộ một triệu dòng code.

Đây không phải là một giấc mơ xa vời. Đây là lời hứa về việc khôi phục lại sự minh mẫn và trao lại siêu năng lực cho các nhà phát triển để quản lý sự phức tạp đang áp đảo những đội ngũ tốt nhất của chúng ta.

Phần V: Lời kết và Những bước Tiếp theo

Chúng ta đã đi qua một hành trình dài, từ việc phân tích những rạn nứt trong nền tảng của lập trình hiện đại đến việc phác thảo một mô hình mới—Lập trình Hướng Cấu trúc—và sự hiện thực hóa ban đầu của nó thông qua CScaf.

Tôi đã lập luận rằng các vấn đề về sự phân mảnh codebase, sự đánh đổi giữa năng suất và hiệu năng, và sự phức tạp của các hệ thống phân tán không phải là những định mệnh không thể tránh khỏi. Chúng là những triệu chứng của các công cụ được thiết kế cho một kỷ nguyên đã qua.

CScaf là một giả thuyết: rằng bằng cách tích hợp kiến trúc vào ngôn ngữ, bằng cách chấp nhận bất đồng bộ là mặc định, và bằng cách cung cấp các hợp đồng được máy móc xác thực, chúng ta có thể tạo ra một thế hệ ứng dụng mới—những ứng dụng có thể phát triển từ một hạt mầm đơn giản thành một cây cổ thụ khổng lồ một cách tự nhiên và bền vững.

Đây là một tầm nhìn tham vọng. Và nó không thể được xây dựng một mình.

Hành trình này chỉ mới bắt đầu. Bước tiếp theo không phải là xây dựng toàn bộ thế giới này ngay lập tức. Bước tiếp theo là bắt đầu cuộc đối thoại. Tôi sẽ bắt đầu bằng việc xây dựng một bộ tài liệu thiết kế chi tiết và một nguyên mẫu khả thi để chứng minh những ý tưởng cốt lõi này.

Nếu bạn, cũng giống như tôi, tin rằng có một cách tốt hơn để xây dựng phần mềm, rằng chúng ta có thể và nên yêu cầu nhiều hơn từ các công cụ của mình, thì tôi mời bạn tham gia vào cuộc hành trình này. Cuộc thảo luận đang diễn ra, và tương lai đang chờ được viết nên.

Nếu bạn có hứng thú, hãy theo dõi repo https://github.com/axx83/CScafLang và chờ những thông tin mới nhất về dự án.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí