Building Relay

Phần 0 · Chương 0.5

Quyết định thành tiếng — bản SAD và thói quen viết ADR

Bạn sẽ tạo ra: Bảng động lực thiết kế (drivers); hai bản ADR viết từ con số không · khoảng 110 phút, bao gồm bài tập

Tài liệu gốc: SAD — Tài liệu kiến trúc phần mềm · ADR — phân tích chuyên sâu (tiếng Anh)

Chương 0.4 khép lại bằng một cuốn sổ cái: 224 yêu cầu, mỗi yêu cầu là một lời hứa có địa chỉ, có độ ưu tiên, và có cách để bị chứng minh là sai. Nhưng cuốn sổ cái ấy gác lại một câu hỏi mà tự nó không thể trả lời: chúng ta định giữ ngần ấy lời hứa bằng cách nào? Kiến trúc chính là câu trả lời — song không phải dưới hình hài mà phần lớn các tutorial vẫn quen gợi ý. Nó không phải một tấm sơ đồ hộp-và-mũi-tên, dù có đẹp đến đâu. Tài liệu kiến trúc của Relay, bản SAD, tự trong cốt lõi là một tập hợp những quyết định mà bạn có thể tranh luận cùng: mỗi quyết định được viết ra rõ ràng, gọi tên những yêu cầu đã ép nó ra đời, những phương án nó đã bác bỏ, và những điều kiện mà khi chạm tới, nó nên bị đảo ngược. Chương này dành trọn cho thói quen ấy — quyết định thành tiếng — và đó là điều cuối cùng Phần 0 cần trao lại trước khi công cuộc xây dựng bắt đầu.

Vấn đề đầu tiên thuộc về số học. Hai trăm hai mươi tư yêu cầu không thể mỗi dòng đều có một lá phiếu về hình hài của hệ thống; một thiết kế cố trả lời mọi thứ cùng lúc là một thiết kế chẳng trả lời điều gì. Vì thế bản SAD mở màn bằng một động tác nén mà chuỗi giấy tờ của chúng ta chưa từng chứng kiến — và chính phép nén ấy là kỹ năng đầu tiên chương này muốn trao.

Từ 224 lời hứa đến tám động lực thiết kế

Mục thứ hai của bản SAD là một chiếc bảng vỏn vẹn tám dòng, mở đầu bằng một câu nói toạc điều ai cũng ngầm hiểu: "The handful of requirements that actually shape the architecture. Everything else is implementation." — nhóm nhỏ những yêu cầu thực sự nhào nặn kiến trúc; mọi thứ còn lại là chuyện triển khai. Câu văn ấy đang gánh gồng cả một khối lượng công việc khổng lồ. Nó tuyên bố rằng trong 224 lời hứa, chỉ khoảng tám dòng thực sự phát ra lực nắn cấu trúc — phần còn lại sẽ được code tôn trọng, chứ không phải được hình hài tôn trọng. Một động lực thiết kế (architectural driver) là một yêu cầu vượt qua đúng bài sát hạch đó: thỏa mãn nó làm thay đổi nơi mọi thứ trú ngụ, thứ gì sở hữu thứ gì, hay thứ gì được phép đổ vỡ.

Hãy dõi theo trọn vẹn một phép suy dẫn. Chương 0.4 trao cho chúng ta FR-MSG-05 (một lần gửi chỉ được xác nhận sau khi thông điệp đã nằm bền vững trong kho) và FR-MSG-06 ("một thông điệp đã được xác nhận sẽ không bao giờ bị mất" — dòng yêu cầu được kiểm chứng bằng phân tích, bởi không phép thử đơn lẻ nào chứng minh nổi một mệnh đề phủ định cho mọi tương lai). Ép hai dòng ấy lại cùng chỉ tiêu độ tin cậy NFR-REL-02, và bạn nhận về driver D1 — kèm theo, quan trọng hơn cả, một hệ quả được tuyên bố thành văn:

D1 · No acknowledged message may be lost
Source: FR-MSG-05/06, NFR-REL-02
Consequence: Ack only after durable commit; ordering and persistence
in one transactional store

D8 · One engineer must be able to run and reason about it
Source: Portfolio reality; NFR-MNT-03
Consequence: Few services with sharp boundaries, not many with fuzzy
ones; boring technology

(Dịch nghĩa: D1 · Không một thông điệp đã xác nhận nào được phép mất — Nguồn: FR-MSG-05/06, NFR-REL-02 — Hệ quả: chỉ xác nhận sau khi đã commit bền vững; thứ tự và lưu trữ nằm trong một kho giao dịch duy nhất. D8 · Một kỹ sư phải có thể vận hành và hiểu thấu toàn bộ hệ thống — Nguồn: thực tế portfolio; NFR-MNT-03 — Hệ quả: ít service với ranh giới sắc nét, thay vì nhiều service với ranh giới lờ mờ; công nghệ nhàm chán.)

Hãy đọc lại cột hệ quả của D1 một lần nữa, bởi đó chính là khoảnh khắc yêu cầu hóa thành kiến trúc: một kho giao dịch duy nhất. Ba yêu cầu bước vào; một câu văn mang tầm vóc cấu trúc bước ra — sẽ có một nơi duy nhất mà sự bền vững trú ngụ, và không gì được phép tuyên bố một thông điệp đã an toàn chừng nào nơi ấy chưa commit nó. Mọi chiếc hộp trong tấm sơ đồ sau này hoặc cúi đầu trước câu văn đó, hoặc sai.

Giờ đọc đến D8, mẫu vật thứ hai trong khối trích và cũng là dòng kỳ lạ nhất của chiếc bảng. Cột nguồn của nó không trích một câu shall nào; nó ghi "Portfolio reality" — thực tế portfolio: hệ thống này do một kỹ sư xây, để một kỹ sư vận hành và đứng ra bảo vệ. D8 là một driver mà hoàn toàn không phải là một yêu cầu, và chính sự trung thực ấy là bài học. Bản SAD dành hẳn cho nó một đoạn nhấn mạnh: số service đúng là số nhỏ nhất vẫn còn phô bày được những ranh giới hệ phân tán thực thụ, và nạn phình service để làm đẹp CV — "fifteen services where five suffice", mười lăm service nơi năm cái là quá đủ — tự thân nó đã là một mùi kiến trúc đáng ngờ, và các reviewer đều ngửi ra. Quy mô đội ngũ, deadline, những kỹ năng bạn thực sự nắm trong tay: các lực này nhào nặn mọi hệ thống có thật trên đời, và một bảng driver bỏ sót chúng là đang nói dối bằng sự im lặng. Gọi tên D8 thành tiếng chính là điều cho phép bản SAD nói "sáu service, không phải mười lăm" rồi đứng ra bảo vệ câu ấy như một quyết định, thay vì cúi đầu thú nhận nó như một giới hạn.

Giải phẫu một quyết định

Driver là những lực đẩy; quyết định là điều bạn làm trước những lực ấy. Bản SAD ghi mỗi quyết định thành một bản Architecture Decision Record, và thay vì mô tả cái khuôn một cách trừu tượng, chúng ta sẽ mổ xẻ một bản thật — ADR-03, được chọn vì một lý do rất riêng: bản SRS của chương 0.4 đã thành thật để ngỏ Open Question 1 của nó (những con số thứ tự per-channel nghiêm ngặt sinh ra từ đâu, trong một hệ thống chạy nhiều instance?), và ADR-03 chính là quyết định khép câu hỏi ấy lại. Một tài liệu yêu cầu được phép kết thúc bằng câu hỏi; một bản kiến trúc thì không. Đây là bản ghi cô đọng của nó, chỉ xuống dòng lại đôi chút:

ADR-03 · Ordering: per-channel sequence via last_sequence row lock
Status: accepted · resolves SRS Open Question 1 · Drivers: D1, D3

Per-channel sequences, assigned by incrementing channels.last_sequence
under SELECT … FOR UPDATE in the message-insert transaction. Contention
is scoped to a single channel — and serialising a channel's writes is
the requirement (FR-MSG-03), so the lock is not a cost, it is the
mechanism. Rejected: per-tenant sequence (single hot row per tenant — a
real bottleneck for zero benefit at v1 scale); Postgres sequences per
channel (unbounded object count, non-transactional gaps break dedup
reasoning); Snowflake-style IDs (globally unique but not gap-free per
channel, which complicates the client's "did I miss something?" logic).

(Dịch nghĩa: ADR-03 · Thứ tự: sequence theo từng channel qua row lock trên last_sequence — Trạng thái: accepted · giải quyết Open Question 1 của SRS · Driver: D1, D3. Sequence theo từng channel, được cấp bằng cách tăng channels.last_sequence dưới SELECT … FOR UPDATE ngay trong transaction chèn thông điệp. Vùng tranh chấp gói gọn trong một channel duy nhất — mà tuần tự hóa các lần ghi của một channel chính là yêu cầu (FR-MSG-03), nên chiếc khóa không phải một cái giá, nó chính là cơ chế. Bác bỏ: sequence theo tenant (một dòng nóng duy nhất cho mỗi tenant — nghẽn cổ chai thật để đổi lấy lợi ích bằng không ở quy mô v1); Postgres sequence theo từng channel (số lượng object không có chặn trên, các khoảng trống phi giao dịch phá vỡ lập luận khử trùng lặp); ID kiểu Snowflake (duy nhất toàn cục nhưng không liền mạch trong từng channel, làm rối logic "mình có bỏ lỡ gì không?" của client).)

Giờ hãy tháo rời từng bộ phận của bộ khung giải phẫu ấy. Dòng trạng thái làm ba việc trong một hơi thở: quyết định này đã accepted; nó tồn tại để giải quyết một câu hỏi mở có tên tuổi; và nó phục vụ driver D1 cùng D3 — nên bất kỳ ai muốn phản bác đều biết ngay mình đang thực sự phản bác những lực nào. Bản thân quyết định đi kèm lập luận khiến nó trở nên đáng bảo vệ, và lập luận này xứng đáng được học thuộc như một khuôn mẫu: một row lock trên mỗi lần gửi trông hiển nhiên là một cái giá phải trả — cho đến khi bạn nhận ra FR-MSG-03 đòi hỏi các thông điệp của một channel phải xếp thành hàng nghiêm ngặt — phải có một cơ chế nào đó đứng ra tuần tự hóa chúng — vậy nên chiếc khóa không phải một cái giá, nó chính là cơ chế. Những quyết định hay nhất thường mang dáng hình này: thứ trông như điểm yếu của thiết kế hóa ra là chính yêu cầu, hiện hình bằng xương bằng thịt.

Rồi đến phần mà hầu hết các đội bỏ qua, còn kỷ luật này thì khước từ bỏ qua: các phương án bị bác bỏ, mỗi phương án kèm lý do của nó. Sequence theo tenant sẽ đánh đổi một vấn-đề-không-hề-tồn-tại lấy một vấn đề rất thật — mọi lần gửi trong một environment đều xếp hàng sau một dòng nóng duy nhất. Postgres sequence là thứ phi giao dịch: một lần gửi bị rollback vẫn đốt mất một giá trị, và những khoảng trống chẳng ứng với commit nào sẽ đầu độc logic "thấy trống nghĩa là phải tải lại" của client. ID kiểu Snowflake chỉ có thứ tự xấp xỉ và không bao giờ liền mạch, nên một khoảng trống thôi không còn mang ý nghĩa gì nữa. Để ý xem ba câu văn ấy mua về điều gì: người tiếp theo "phát kiến" ra sequence theo tenant sẽ thấy ý tưởng của mình đã được cân nhắc từ trước, kèm lý do nó thua cuộc. Một phương án bị bác bỏ được ghi lại cùng lý do là một cuộc họp không bao giờ phải diễn ra.

Và sau cùng là yếu tố khiến bản ghi trở nên trung thực thay vì chỉ tự tin: một điều kiện đảo ngược. Bản deep dive của ADR-03 nêu rõ ngòi nổ của mình — xem xét lại khi một trường hợp sử dụng chính đáng cần hơn 500 thông điệp mỗi giây trong một channel — rồi bồi thêm một câu chốt sắc lẹm: đến lúc đó, câu trả lời sẽ là "đó không phải một channel chat nữa" — một ranh giới sản phẩm, không phải một bản vá kiến trúc. Ngay cả điều kiện đảo ngược cũng là một quyết định.

Một quyết định, hai tài liệu

Có thể bạn đã nhận ra bản ghi cô đọng phía trên không nói gì về số đo chu kỳ khóa hay vì sao các khoảng trống lại vô hại. Phần chi tiết ấy sống ở nơi khác, một cách có chủ đích. Relay tách mỗi quyết định ra hai tài liệu: bản SAD giữ bản ghi cô đọng — một quyết định, các driver của nó, và những phương án bị bác bỏ, gói trong một đoạn văn — còn tài liệu ADR deep-dives giữ bản trường thiên, theo một khuôn cố định cho cả mười bốn bản: Problem → Options → Analysis → Decision → Consequences → Revisit when (vấn đề → các lựa chọn → phân tích → quyết định → hệ quả → xem lại khi nào). Tài liệu deep-dives tự phát biểu sứ mệnh của mình hay hơn bất cứ lời diễn giải nào: bản SAD vẫn là nguồn chân lý cho câu hỏi quyết định gì; bản trường thiên tồn tại để cái vì sao "sống sót qua cú va chạm với một reviewer đa nghi".

Một luật nữa khép kín bộ kỷ luật, và nó hẳn nghe rất quen tai. Mục quy ước của bản SAD viết: các quyết định được đánh số ADR-nn và bất biến một khi đã accepted; muốn thay thế phải viết một ADR mới. Đây chính là luật sổ cái của chương 0.4 khoác lên mình chiếc áo kiến trúc sư — bản SRS đã hấp thụ cú quay xe về media bằng ID mới và không một dòng sửa đè, và cuốn sổ quyết định vận hành y hệt. Nếu một ngày ADR-03 gục ngã trước ngòi nổ 500 msg/s kia, sẽ không ai viết lại ADR-03; một ADR mới sẽ thay thế nó, và cuốn sổ sẽ lưu giữ cả quyết định lẫn sự sụp đổ của nó, theo đúng trình tự, mãi mãi. Một bản kiến trúc với những quyết định bị sửa đè thì không có lịch sử, chỉ có thì hiện tại — và bạn không thể học được gì từ một tài liệu lúc nào cũng đồng ý với bạn.

Chuỗi xích khép lại: cú quay xe cập bến kiến trúc

Phần 0 vẫn âm thầm theo đuổi một mạch truyện xuyên suốt bốn chương, và mạch truyện ấy hạ màn tại đây. Chương 0.1 ghi "không lưu trữ file" làm một non-goal của v1.0, kèm lý do rõ ràng — chi phí băng thông, CDN, và vận hành lưu trữ. Chương 0.3 vẽ những hành trình khiến tệp đính kèm rốt cuộc vẫn thành chuyện hệ trọng. Chương 0.4 chứng kiến cú quay xe cập bến SRS dưới dạng mười bốn yêu cầu FR-MED mới toanh — ID mới, không một dòng sửa đè. Giờ hãy xem nó cập bến bản SAD — không phải như một lần chỉnh sửa bất kỳ quyết định hiện có nào, mà như hai bản ADR hoàn toàn mới, và bản đầu tiên tự khai gốc gác của mình ngay trên dòng trạng thái:

ADR-13 · Media bytes never transit Relay compute:
presigned direct-to-storage
Status: accepted · reverses the v1.0 file-storage exclusion
Drivers: D5, D8, NFR-PRF-08

Uploads: the API service validates declaration and quota, records a
pending row, and returns a presigned PUT URL; the client uploads
straight to S3-compatible storage. Downloads: signed GET URLs minted at
read time, authorised by channel membership (FR-MED-08), never
persisted (DR-16). Relay's services handle metadata only — the original
exclusion's cost argument (bandwidth, CDN, storage ops) is answered by
not building any of it: object storage's durability, bandwidth, and
lifecycle rules are bought, not rebuilt.

(Dịch nghĩa: ADR-13 · Byte media không bao giờ đi qua hạ tầng tính toán của Relay: presigned direct-to-storage — Trạng thái: accepted · đảo ngược lệnh loại trừ lưu trữ file của v1.0 — Driver: D5, D8, NFR-PRF-08. Upload: API service kiểm tra khai báo và hạn mức, ghi một dòng pending, rồi trả về một presigned PUT URL; client tải thẳng lên kho lưu trữ tương thích S3. Download: các signed GET URL được cấp tại thời điểm đọc, ủy quyền theo tư cách thành viên channel (FR-MED-08), không bao giờ được lưu lại (DR-16). Các service của Relay chỉ chạm vào metadata — luận điểm chi phí của lệnh loại trừ ban đầu (băng thông, CDN, vận hành lưu trữ) được trả lời bằng cách không xây bất cứ thứ gì trong số đó: độ bền, băng thông, và các quy tắc vòng đời của object storage là thứ được mua, không phải được xây lại.)

Hãy đọc quyết định này trong thế đối chiếu với lời phản đối năm xưa, và để ý rằng nó không phủ quyết lệnh loại trừ — nó trả lời lệnh loại trừ. Mọi chi phí mà 0.1 từng e sợ — băng thông chảy qua hạ tầng mạng của Relay, vận hành CDN, quản lý lưu trữ — đều được hóa giải "bằng cách không xây bất cứ thứ gì trong số đó": byte đi thẳng giữa client và object storage, nơi độ bền cùng quy tắc vòng đời là thứ mua được, không phải xây lại. Các service của Relay chỉ chạm vào metadata. Những lý do của non-goal ngày ấy vốn dĩ đều đúng; quyết định đảo ngược nó chính là quyết định đã khiến những lý do ấy hết thời. Một cú quay xe lành mạnh trên giấy tờ trông như thế đó: lập luận cũ được thừa nhận, được đối thoại, và bị đánh bại trên chính địa hạt của nó — trong một bản ghi mới, trong khi trang giấy gốc của 0.1 vẫn đọc lên y nguyên như ngày nào.

Người bạn đồng hành ADR-14 gánh gồng nốt nửa khó nhằn của việc lưu trữ file người dùng tải lên — quét nội dung — và giành chỗ đứng trong chương này bằng hai nước đi rất đáng học. Thứ nhất, cánh cổng được đặt với độ chính xác của một nhát dao phẫu thuật: pipeline quét chặn byte, không bao giờ chặn thông điệp. Một thông điệp được phép tham chiếu media còn pending và lên đường ngay lập tức; người nhận thấy một placeholder; không signed URL nào tồn tại chừng nào tệp chưa sang ready. Phương án bị bác bỏ — quét trước khi gửi — sẽ nhét một pipeline ngốn CPU dài 2–10 giây vào giữa đường gửi tin và trói sự sẵn sàng của thông điệp vào sự sẵn sàng của trình quét, đúng cái sự ràng buộc mà driver D5 sinh ra để cấm tiệt. Thứ hai, cỗ máy trạng thái media (pending/ready/rejected) chủ ý soi gương cỗ máy trạng thái thông điệp mà SDK vốn đã cài đặt (sending/sent/failed) — nói bằng chính lời của bản ADR: one mental model, twice applied — một mô hình tư duy, dùng hai lần. Năng lực mới, khái niệm mới bằng không: đó là D8 đang bỏ phiếu trong một quyết định bề ngoài chỉ nói về quét virus.

Đọc mười bốn bản ADR như một tổng thể

Đi hết trọn bộ mười bốn bản ADR — chúng ta vừa mổ xẻ kỹ hai bản, số còn lại đang chờ trong đúng hai tài liệu ấy — và tài liệu deep-dives nhận xét rằng có ba chủ đề cứ trở đi trở lại, rằng gọi tên chúng "chính là bản tóm tắt hay nhất về tính cách của kiến trúc này." Sự bền vững có một mái nhà duy nhất; mọi thứ khác được phép rẻ tiền: Postgres nắm giữ chân lý, và chính lựa chọn nghiêm khắc duy nhất ấy đã mua về mọi lựa chọn thư thái đến ngỡ ngàng khác — một lớp fan-out được phép rơi rớt, presence được phép bốc hơi, kho phân tích được phép sập. Đường ghi là bất khả xâm phạm; tính năng sống bên đường đọc: sequence commit cùng dòng dữ liệu, mọi lần gửi hội tụ về một lối code duy nhất, và khi một tính năng mới đe dọa ngữ nghĩa thông điệp, phản xạ thiết kế là tái công thức nó thành một mối bận tâm lúc đọc. Và mỗi quyết định tự gọi tên sự sụp đổ của chính mình — chủ đề bạn vừa thấy trả công xứng đáng. Không chủ đề nào trong ba từng được viết ra thành yêu cầu. Chúng trồi lên, qua từng quyết định một, từ tám driver đè lực lên mười bốn lựa chọn — và đó là tầng nghĩa sau cùng của mệnh đề "kiến trúc không phải một tấm sơ đồ". Sơ đồ cho thấy hệ thống trông ra sao. Các quyết định cho thấy hệ thống tin vào điều gì.

Đến lượt bạn

Những tài liệu cuối cùng của Phần 0. Bạn đến đây với lát cắt SRS 8–15 dòng từ chương 0.4 (phần mềm đặt lịch phòng khám thú y vẫn chạy tốt chứ?); bạn rời đi với một bảng driver và hai bản ADR — đúng chuỗi xích Relay vừa đi qua, ở quy mô của bạn.

Bài tập 1 — bảng driver. Chưng cất lát cắt của bạn xuống còn 3 đến 6 driver. Con số ấy quan trọng: nếu bạn cho ra một driver trên mỗi yêu cầu, bạn mới chỉ đổi tên lát cắt chứ chưa hề chưng cất — phép nén chính là kỹ năng. Đặt mọi ứng viên trước bài sát hạch đầu vào của tư duy ADR: thỏa mãn yêu cầu này có làm thay đổi nơi mọi thứ trú ngụ, thứ gì sở hữu thứ gì, hay thứ gì được phép đổ vỡ không? Một lời nhắc hẹn phải sống sót qua cú sập server thì nắn được cấu trúc; một lời nhắc hẹn phải viết cho lịch sự thì không. Mỗi dòng gồm ba trường, y hệt bảng của Relay:

Driver: Không một lịch hẹn đã xác nhận nào được phép âm thầm biến mất. Nguồn: VET-REM-01, VET-PRT-02 (ID của bạn, từ lát cắt của bạn) Hệ quả: Lịch hẹn và lời nhắc của nó sống trong một kho bền vững duy nhất; màn hình xác nhận chỉ hiện ra sau khi đã commit.

Bạn được phép — và được khuyến khích — đưa vào một driver kiểu D8: thứ hoàn toàn không phải yêu cầu mà là bối cảnh thành thật: "xây bởi một người, chỉ vào buổi tối" hay "phải chạy trên gói hosting 10 €/tháng sẵn có của phòng khám." Ghi nguồn của nó là bối cảnh, đúng như cách D8 của Relay ghi "portfolio reality". Một ràng buộc bạn dám thừa nhận là một driver; một ràng buộc bạn che giấu là một lời bao biện trong tương lai.

Bài tập 2 — hai bản ADR từ con số không. Chọn hai quyết định cấu trúc mà các driver của bạn ép ra — lý tưởng là mỗi bản đứng ra bảo vệ một yêu cầu sinh từ ★. Với mỗi bản, viết đủ bộ khung giải phẫu của chương này, theo phong cách bản-ghi-cô-đọng:

Tự kiểm. Mỗi câu trả lời có hoặc không:

  1. Mỗi driver có trích ID yêu cầu từ chính lát cắt của bạn — hoặc tự khai là bối cảnh, kiểu D8 — không? (Không driver mồ côi, không ràng buộc buôn lậu.)
  2. Mỗi bản ADR có gọi tên ít nhất một driver từ bảng của bạn — và một người đọc có thể truy vết lựa chọn → driver → yêu cầu → hành trình → chân dung mà không phải hỏi bạn câu nào không?
  3. Một người đồng đội mới có thể tự dựng lại vì sao bạn không chọn phương án hiển nhiên kia chỉ từ các lý do bác bỏ không?
  4. Mọi điều kiện đảo ngược có quan sát được không — một ngưỡng tự phát hỏa, chứ không phải một tâm trạng?

Xếp cả hai tài liệu vào bộ hồ sơ Phần 0 của bạn. Chúng là viên đá đỉnh vòm: mọi tài liệu trước đó tồn tại là để hai tài liệu này có thể được viết ra một cách trung thực.

Những điều đọng lại

Nếu không đọc gì khác trong chương này, hãy giữ lấy những điều sau: