Traversal và reachability: câu hỏi "cái gì chạm tới cái gì",
biến câu hỏi phụ thuộc từ một cuộc họp thành một giây
Bốn giờ chiều thứ năm, một service nhỏ chuẩn bị nhận breaking change. Code đã review xong, test đã xanh, changelog đã viết. Người định bấm nút là một kỹ sư cẩn thận, nên trước khi bấm, anh hỏi câu mà mọi tech lead tử tế đều hỏi: những service nào sẽ bị ảnh hưởng?
Phòng im một nhịp. Rồi ai đó mở Slack hỏi team bên cạnh, ai đó khác nói để em grep thử, một người thứ ba lục lại trang wiki có sơ đồ kiến trúc, cập nhật lần cuối mười một tháng trước. Một giờ sau, ba người đã hỏi được bảy team, danh sách service bị ảnh hưởng có chín cái tên, kèm hai dấu hỏi và một dòng "hình như bên billing cũng gọi, cần check lại". Không ai dám chốt. Deploy dời sang sáng mai.
Không phải vì phòng đó thiếu người giỏi. Phòng đó toàn người giỏi. Câu trả lời không nằm trong đầu ai cả, nó nằm rải trong code, trong config, trong những cuộc trao đổi cũ, và một phần trong trí nhớ của những người đã nghỉ việc từ năm ngoái.
Trong hệ thống bạn đang làm, câu hỏi "deploy cái này thì cái gì bị ảnh hưởng" hôm nay mất bao lâu để trả lời, và câu trả lời đáng tin đến mức nào?
Hãy nghĩ về nó một chút.
Nếu chạy bài test của chương hai lên tình huống này, kết quả ra gần như tức thì: quan hệ là dữ liệu chính, độ sâu không biết trước, câu hỏi có tính bắc cầu. Graph problem, không bàn cãi. Cụ thể hơn, đây là một câu hỏi reachability°: từ một node, đi theo edge "phụ thuộc", tập node nào đến được.
Và câu hỏi của buổi chiều hôm đó có anh em sinh đôi ở khắp nơi trong một tổ chức kỹ thuật. Bảng này sai thì dashboard nào sai theo. Gỡ thư viện này thì cái gì gãy, câu hỏi mà cả hệ sinh thái JavaScript từng trả lời bằng một buổi sáng sập npm. User này vì sao đọc được file kia. Tất cả đều là một bộ xương, mặc những bộ quần áo khác nhau, đúng kiểu nguỵ trang mà Part I đã dạy chúng ta nhìn xuyên qua.
Điều đáng nói là khoảng cách giữa hai trạng thái. Một bên là câu hỏi mà ba kỹ sư cộng lại không trả lời nổi trong một giờ. Bên kia là câu hỏi mà một hệ thống trả lời trong dưới một giây, đủ nhanh để gắn vào CI, để chạy trước mỗi lần deploy, để không ai phải dời lịch sang sáng mai. Khoảng cách đó không nằm ở thuật toán. Thuật toán traverse một đồ thị nằm trong sách giáo khoa năm hai, và bạn đã biết nó từ lâu. Khoảng cách nằm ở chỗ tổ chức có hay không có cái graph để hỏi.
Trước khi nói về cách xây cái graph đó, công bằng mà nói, ba cơ chế mà phòng họp kia dùng đều không ngớ ngẩn. Chúng là ba lời giải tự phát mà gần như mọi tổ chức đều đi qua, và mỗi cái đều thắng được một giai đoạn.
Grep thắng khi dependency là lời gọi code trực tiếp trong một repo. Tên hàm, tên endpoint, tên topic nằm đâu đó trong text, và grep tìm text thì không ai qua mặt được. Hỏi Slack thắng khi tổ chức còn nhỏ, khi trí nhớ tập thể còn phủ được toàn bộ hệ thống, khi người viết service A vẫn ngồi cách người viết service B hai dãy bàn. Còn cái spreadsheet "service nào gọi service nào", đội nào lập ra nó cũng đáng được ghi nhận, vì họ là những người đầu tiên trong tổ chức nhìn thấy vấn đề và thử trả lời nó một cách có hệ thống.
Cả ba có chung một ưu điểm lớn: chi phí gần bằng không. Không hạ tầng, không dự án, không cần thuyết phục ai. Và với hệ thống dưới một ngưỡng nào đó, chúng thật sự là lời giải đúng, điểm này chúng ta sẽ quay lại ở cuối chương, một cách nghiêm túc chứ không phải nói cho có.
Nhưng cả ba cũng có chung hai chỗ gãy, và chỗ gãy thứ nhất mang tính kỹ thuật: chúng chỉ thấy dependency trực tiếp. Grep tìm ra ai gọi A. Nó không tìm ra ai gọi người gọi A. Câu hỏi của tech lead lại là câu hỏi bắc cầu: A đổi thì B lung lay, B lung lay thì C rung theo, và sự cố, nếu nổ, thường nổ ở C, nơi không ai trong phòng họp hôm đó nghĩ tới. Grep trả lời một bước. Câu hỏi thật không có số bước.
Chỗ gãy thứ hai sâu hơn, và nó giải thích vì sao cái spreadsheet kia luôn chết. Dependency graph thay đổi nhanh hơn mọi tài liệu về nó. Mỗi pull request thêm một lời gọi là một lần graph đổi, mỗi service mới là một chùm edge mới, và không quy trình thủ công nào đu theo nhịp đó được. Spreadsheet sai không phải vì người lập nó lười. Nó sai vì nó là một tấm ảnh chụp, trong khi thứ nó cố mô tả là một cuốn phim. Tài liệu tĩnh về một cấu trúc sống thì mục nát theo thiết kế, không phải theo lỗi của ai.
Nghĩa là: thứ cần xây không phải một tài liệu tốt hơn về dependency. Thứ cần xây là một nơi để dependency tự khai báo, hoặc tự lộ diện, và một cấu trúc trả lời được câu hỏi bắc cầu trên đó. Tức là một graph, sống cùng nhịp với hệ thống, và được đối xử như hạ tầng chứ không phải như tài liệu.
“Tổ chức nào không trả lời được "cái gì phụ thuộc vào cái gì" thì sẽ trả lời nó bằng sự cố.
”
Đặt năm câu hỏi cạnh nhau: service nào bị ảnh hưởng khi mình deploy (impact analysis), sự cố này lan được tới đâu (blast radius°), cài gói này thì kéo theo những gói nào (dependency resolution), bảng này sai thì báo cáo nào sai theo (data lineage°), user này vì sao có quyền đọc file kia (permission inheritance). Năm bài toán, năm đội sở hữu, platform, SRE, build, data, security, năm bộ thuật ngữ riêng. Cùng một bộ xương: một tập node, một loại edge có hướng, và câu hỏi "từ node này, đi theo quan hệ này, đóng bao chứa những gì".
Riêng permission inheritance đáng dừng lại một câu, vì nó là thành viên kín tiếng nhất của họ này. Hệ thống phân quyền kiểu Zanzibar mà chúng ta đã chạm đến ở chương hai, về bản chất, trả lời một câu hỏi reachability ở tần suất production: có tồn tại một đường đi từ user này đến tài nguyên kia, qua các quan hệ thành viên và thừa kế, hay không. Mỗi lần bạn mở được một file trên một hệ thống chia sẻ tài liệu là một lần câu hỏi reachability vừa được trả lời, nhanh đến mức không ai gọi nó bằng tên đó.
Bây giờ, phần mà chương này sẽ không dạy: cách traverse. BFS, DFS, đánh dấu visited, bạn đã học, đã cài, có khi đã phỏng vấn người khác bằng chúng. Trong các hệ thống thật, lựa chọn giữa BFS và DFS gần như không bao giờ là quyết định quan trọng. Ba quyết định dưới đây mới là, và cả ba đều không nằm trong chương trình năm hai, vì cả ba chỉ xuất hiện khi đồ thị không còn được phát sẵn trong đề bài.
Quyết định thứ nhất: hướng của edge là hướng của câu hỏi. Cùng một quan hệ thật, "service A gọi service B", bạn vẽ được hai chiều, A phụ thuộc B, hoặc B được A phụ thuộc. Không chiều nào sai với thực tế. Đây chính là bài học của chương ba đang quay lại trong bộ quần áo vận hành: mô hình cắt theo câu hỏi, không cắt theo thực tế. Cơ chế nằm ở chỗ câu hỏi impact, "sửa B thì ai vỡ", traverse ngược chiều lời gọi. Nếu graph của bạn chỉ lưu chiều xuôi, mỗi lần hỏi ngược là một lần quét toàn bộ để tìm xem ai trỏ vào B, và "quét toàn bộ mỗi lần hỏi" chính là grep, chỉ là grep có cấu trúc hơn. Câu kiểm tra cho quyết định này rất đời thường: viết ra giấy câu hỏi sẽ được hỏi nhiều nhất, nhìn xem traversal bắt đầu từ node nào và đi về phía nào. Nếu cả hai chiều đều có câu hỏi tần suất cao, lưu cả hai chiều, và chấp nhận rằng mỗi lần ghi giờ phải ghi hai nơi.
Câu hỏi reachability bạn gặp nhiều nhất trong công việc, nó bắt đầu từ node nào, và đi xuôi hay ngược chiều quan hệ được lưu trong hệ thống của bạn?
Hãy nghĩ về nó một chút.
Quyết định thứ hai: đối xử với cycle như dữ liệu, không phải như ngoại lệ. Lý thuyết yêu DAG, đồ thị có hướng không vòng, vì trên DAG mọi thứ đều gọn: traversal chắc chắn dừng, thứ tự topo tồn tại, chứng minh nào cũng ngắn. Dữ liệu thật không đọc lý thuyết. Hai service gọi nhau là chuyện có thật, hai bảng nuôi nhau qua hai pipeline là chuyện có thật, và một traversal không đánh dấu visited sẽ chạy vòng quanh chúng cho đến hết bộ nhớ. Nhưng đó mới là tầng nông. Tầng sâu hơn: trong một dependency graph, cycle thường không phải lỗi dữ liệu mà là tín hiệu kiến trúc. Hai thứ gọi lẫn nhau thường là một thứ đã bị tách đôi sai chỗ. Cho nên hệ thống reachability tốt không chỉ sống sót qua cycle, nó nổi cycle lên như một loại phát hiện, một finding đáng được con người nhìn vào. Câu kiểm tra: khi gặp cycle, hệ thống của bạn làm gì, chết, lờ đi, hay báo cáo? Chỉ phương án thứ ba biến cái graph từ công cụ trả lời câu hỏi thành công cụ nhìn hệ thống.
Quyết định thứ ba: materialize hay tính on-demand. Câu trả lời bắc cầu, tập mọi node đến được từ X, có thể được tính sẵn và lưu lại (dân làm graph gọi là transitive closure°, bao đóng bắc cầu), hoặc tính mỗi lần được hỏi. Đây là một trade-off cổ điển, tươi mới đổi lấy tốc độ. Lưu sẵn thì trả lời trong mili giây, nhưng mỗi lần graph đổi là một lần phải cập nhật những gì đã lưu. Tính lúc hỏi thì luôn tươi, nhưng trả giá theo độ sâu, mỗi lần hỏi. Không có đáp án đúng chung, chỉ có đáp án đúng theo tỷ lệ giữa hai tần suất: graph đổi mỗi giờ mà bị hỏi mỗi giây thì materialize, graph đổi mỗi giây mà bị hỏi mỗi tuần thì on-demand. Thứ tự topo, topological sort trong sách, khi nhìn từ góc này hoá ra là một dạng materialize đặc biệt: thứ tự build được tính sẵn chính là câu trả lời đóng gói cho cả một họ câu hỏi "cái gì phải xong trước cái gì". Câu kiểm tra: lấy tần suất hỏi chia tần suất đổi, con số đó quyết định thay bạn.
Ba quyết định, và không quyết định nào là thuật toán. Hai tổ chức dưới đây đã trả lời ba câu này theo hai cách gần như ngược nhau, trên hai loại graph khác nhau, và chính sự ngược nhau đó là bằng chứng tốt nhất rằng chúng ta đang nhìn một problem class, không phải một mẹo của riêng domain° nào.
Bài toán của Google là bài toán của buổi chiều thứ năm kia, nhân lên vài bậc độ lớn. Một monorepo mà gần như toàn bộ công ty cùng sống trong đó, hàng nghìn kỹ sư commit song song, và một câu hỏi lặp lại với tần suất công nghiệp: thay đổi này thì phải rebuild cái gì, phải chạy lại test nào. Trả lời thừa thì hệ thống build chết vì tải, mọi commit kéo theo việc build lại nửa thế giới. Trả lời thiếu thì tệ hơn: một thay đổi lọt qua mà không chạy đúng test của những thứ phụ thuộc nó, và bug đi thẳng vào sản phẩm.
Người trong cuộc nhìn ra từ sớm rằng "cái gì cần rebuild" chính là reachability trên dependency graph, với một chữ nếu rất to: nếu cái graph đó tồn tại, đầy đủ, và đáng tin. Và đây là chỗ quyết định nền của Blaze, sau này là Bazel, được đưa ra. Họ không suy diễn dependency từ code, cách làm tự nhiên nhất và mãi mãi là một phép xấp xỉ, vì code có import động, có phản chiếu, có những đường phụ thuộc mà phân tích tĩnh không nhìn thấy. Họ bắt mọi build target khai báo tường minh dependency của nó trong một file BUILD. Muốn dùng thư viện nào, khai báo. Không khai báo, không thấy, build fail ngay tại bàn của người viết. Codebase, bằng luật chứ không bằng kỳ vọng, trở thành một đồ thị được tuyên bố.
Soi ba quyết định của phần trước vào đây sẽ thấy chúng hiện nguyên hình. Hướng: kỹ sư khai báo theo chiều tự nhiên, tôi phụ thuộc X, nhưng câu hỏi đắt nhất của hệ thống, thay đổi ở X thì rebuild những gì, lại đi chiều ngược, nên hệ thống phải phục vụ cả hai chiều như hạ tầng hạng nhất. Cycle: bị cấm tại cửa. Build graph của Bazel là DAG bằng luật, một vòng phụ thuộc là một lỗi chặn ngay lúc khai báo, và đây là một đặc quyền đáng ghi nhớ, vì như chúng ta sẽ thấy ngay sau đây, không phải domain nào cũng có quyền cấm. Materialize: với tỷ lệ hỏi trên đổi cao như thế, hỏi ở mỗi build của hàng nghìn kỹ sư, hệ thống đi rất xa về phía tính sẵn, bản đồ action và cache dựng trên graph chính là materialize ở quy mô công nghiệp, trả trước chi phí phân tích để mỗi lần build chỉ phải đụng đúng phần bị ảnh hưởng.
Bài học chuyển giao được từ case này cần được phát biểu cẩn thận, vì nó không phải "hãy dùng Bazel". Bài học là: khả năng trả lời câu hỏi reachability trong một giây không miễn phí, và Google đã chọn trả nó bằng kỷ luật ở thời điểm ghi. Mọi kỹ sư, mỗi ngày, bảo trì các file BUILD, đổi lấy việc hệ thống trả lời tức thì câu hỏi mà tổ chức khác trả lời bằng cuộc họp. Rẻ lúc đọc vì đã đắt lúc viết. Không có chiều ngược lại.
Và lời giải này có giới hạn thật, kể ra không làm nó kém đi. Kỷ luật khai báo có chi phí bảo trì hằng ngày đè lên mọi kỹ sư, khai báo sai hoặc thừa trở thành cái sai âm thầm mà chỉ tooling tốt mới bắt được, và toàn bộ cách làm khả thi một phần nhờ bối cảnh rất riêng, một monorepo, một văn hoá đầu tư vào công cụ nội bộ mạnh tay hiếm thấy. Mang nguyên lời giải về một tổ chức khác thường thất bại. Mang nguyên lý trả-giá-lúc-ghi về thì không.
Bây giờ lật sang một thế giới mà điều kiện gần như ngược lại.
Nửa đêm, một bảng nguồn trong data warehouse đổi schema, một cột bị đổi kiểu. Sáng hôm sau, câu hỏi không phải bảng nào hỏng, cái đó đã biết. Câu hỏi là dashboard nào, báo cáo nào, model° nào đang sai theo mà chưa ai phát hiện. Và vài tuần sau, chiều ngược lại sẽ đến: một con số trên dashboard của ban giám đốc trông đáng ngờ, và ai đó phải trả lời cột này được tính từ đâu ra, qua những phép biến đổi nào. Câu thứ nhất là reachability xuôi dòng, câu thứ hai là reachability ngược dòng, cùng trên một đồ thị mà dân data gọi là lineage, phả hệ của dữ liệu. Đây là phiên bản dữ liệu của buổi chiều không ai dám deploy, chỉ khác là sự cố đã nổ rồi.
Điểm làm thế giới này khác hẳn thế giới của Bazel: không có file BUILD, và sẽ không bao giờ có. Dữ liệu trong một công ty lớn chảy qua hàng nghìn pipeline, SQL viết tay, notebook của data scientist, công cụ kéo thả, job cron ai đó viết từ ba năm trước. Bắt tất cả tác giả của tất cả những thứ đó khai báo tường minh "job này đọc bảng nào, ghi bảng nào" là một trận chiến văn hoá không thắng nổi. Cho nên các nền tảng lineage, Databook ở Uber, các hệ dựng quanh chuẩn mở OpenLineage, chọn con đường còn lại: khai quật. Không yêu cầu khai báo, mà parse query log và metadata của job để suy ra các edge "đọc từ, ghi vào". Đồ thị không được tuyên bố, nó được dựng lại từ dấu vết, như cách người ta dựng lại một thành phố cổ từ những gì còn sót.
Ba quyết định vẫn là ba quyết định, nhưng đáp án đổi theo điều kiện. Hướng: ở đây không còn được chọn một chiều làm chính, vì cả hai chiều đều là câu hỏi tần suất cao, impact đi xuôi dòng và truy nguyên đi ngược dòng, nên lineage graph gần như phải phục vụ hai chiều ngay từ thiết kế. Cycle: không cấm được. Bazel có quyền từ chối một vòng phụ thuộc tại cửa, nhưng nền tảng lineage không có cửa nào để đứng gác, nó ghi nhận thế giới như thế giới đang là, và hai pipeline nuôi nhau là chuyện thế giới vẫn làm. Lựa chọn duy nhất còn lại là phương án thứ ba của phần trước: sống sót, và báo cáo. Materialize: một đồ thị dựng từ log, tự nó, đã là một dạng materialize có độ trễ, graph bạn đang hỏi là hệ thống tính đến lần thu thập gần nhất, và độ trễ đó phải được nói thật với người dùng thay vì giấu đi.
Nhưng case này còn thêm vào problem class một thứ mà Bazel không phải đối mặt: độ tin của từng edge. Edge suy ra từ parse có thể thiếu, câu SQL sinh động lúc runtime, job chạy ngoài hệ thống thu thập, và có thể thừa. Một đồ thị khai quật luôn là một phép xấp xỉ của hệ thống thật, và mọi câu trả lời reachability trên nó thừa kế nguyên vẹn tính xấp xỉ đó. Các hệ lineage trưởng thành xử lý điều này bằng sự thành thật: công bố độ phủ, đánh dấu nguồn gốc của edge, thay vì giả vờ đầy đủ. Một câu trả lời "đây là 9 dashboard bị ảnh hưởng, trong phạm vi 85% pipeline mà hệ thống nhìn thấy" đáng tin hơn một câu trả lời "đây là 9 dashboard" tròn trịa.
Đặt hai case cạnh nhau, cái lộ ra không phải hai kỹ thuật mà là hai cái giá. Bazel mua một đồ thị đúng tuyệt đối bằng kỷ luật của hàng nghìn con người tại thời điểm ghi. Các nền tảng lineage mua một đồ thị gần đúng bằng hạ tầng khai quật và sự thành thật về độ phủ. Hai cái giá rất khác nhau, trả cho cùng một tài sản: khả năng trả lời "cái gì chạm tới cái gì" trong một giây thay vì một cuộc họp. Class là một. Con đường đến cái graph là quyết định, và quyết định đó thuộc về điều kiện của domain, không thuộc về sách giáo khoa.
Mình đã thấy các hệ thống reachability hỏng đủ kiểu, nhưng phần lớn quy về ba pattern, và cả ba đều đáng kể lại đủ chậm để nhận ra chúng từ xa.
Cách hỏng thứ nhất: edge ngược hướng với câu hỏi. Trông nó như thế này. Một đội dựng xong graph "service nào gọi service nào", demo chạy đẹp, câu hỏi "A phụ thuộc những gì" trả lời tức thì, mọi người vỗ tay. Ba tháng sau, hoá ra câu hỏi tổ chức thật sự cần lại là chiều kia, "ai đang gọi A", vì người ta muốn biết impact trước khi sửa A chứ không phải sau. Và mỗi lần hỏi câu đó, hệ thống quét toàn bộ đồ thị. Nó vẫn đúng về dữ liệu, chỉ vô dụng về vận hành, người dùng lặng lẽ quay lại hỏi Slack. Tại sao xảy ra: lúc dựng, người ta vẽ theo chiều khai báo tự nhiên hoặc chiều dữ liệu chảy, và không ai viết câu hỏi tần suất cao ra giấy trước, quyết định thứ nhất không bị làm sai, nó bị bỏ qua. Bắt bằng cách nào: trước khi dựng, liệt kê ba câu hỏi sẽ được hỏi nhiều nhất, kèm node bắt đầu và chiều đi; sau khi dựng, đo xem câu hỏi chậm nhất có trùng câu hỏi thường nhất không. Điều đáng tiếc nhất của failure này là lời sửa thường rẻ, thêm một chiều index là xong, cái đắt là ba tháng không ai nhận ra mình đang trả giá.
Cách hỏng thứ hai: quên cycle, vì tin dữ liệu sạch. Trông nó như thế này: traversal treo trên production, hoặc trả về một kết quả phình to vô lý, trong khi toàn bộ test đều xanh. Test xanh vì test chạy trên dữ liệu mẫu, và dữ liệu mẫu được vẽ tay, mà dữ liệu vẽ tay thì hiền, không ai vẽ tay một vòng lặp vào fixture của mình. Tại sao xảy ra: niềm tin "dependency thì làm gì có vòng" là một niềm tin hợp lý về hệ thống được thiết kế tốt, và sai về hệ thống có thật; cộng thêm thói quen từ thời đi học, nơi đề bài lịch sự phát cho chúng ta những DAG sạch sẽ. Bắt bằng cách nào: đưa phát hiện cycle vào pipeline nạp dữ liệu như một bước hạng nhất, không phải như một mệnh đề if phòng thủ trong thuật toán, và đối xử với mỗi cycle mới như một finding được log lại, được con người xem. Cycle bạn không chủ động tìm rồi sẽ tự tìm bạn, thường vào lúc tệ nhất có thể.
Cách hỏng thứ ba: materialize rồi không nuôi nổi. Trông nó như thế này: bao đóng bắc cầu được tính sẵn, truy vấn nhanh như mơ suốt nhiều tháng, cho đến ngày một người dùng phát hiện câu trả lời thiếu mất một service đã lên production từ một quý trước. Lòng tin vào loại hệ thống này sập một lần là sập hẳn, vì giá trị duy nhất của nó là được tin; người ta quay về hỏi Slack, và hệ thống trở thành cái spreadsheet đắt tiền nhất công ty. Tại sao xảy ra: quyết định materialize được đưa ra bằng nửa dữ kiện, người ta nhìn tốc độ đọc mà quên ký vào vế nghĩa vụ, rằng chi phí thật của materialize không nằm ở lần tính đầu tiên, nó nằm ở mọi lần đồ thị thay đổi về sau, kéo dài chừng nào hệ thống còn sống. Bắt bằng cách nào: trước khi tính sẵn bất cứ thứ gì, trả lời được hai câu, cái gì trigger việc tính lại, và độ trễ chấp nhận được là bao nhiêu. Không trả lời được thì on-demand chậm mà thật vẫn hơn nhanh mà dối. Và một hệ thống tử tế nên phơi bày tuổi của câu trả lời, tính lúc mấy giờ, từ snapshot nào, như một phần của chính câu trả lời.
Ba cách hỏng, nếu bạn để ý, là bóng đổ của đúng ba quyết định. Hệ thống không hỏng ở chỗ thuật toán sai. Nó hỏng ở chỗ một trong ba quyết định bị bỏ qua, bị làm bằng nửa dữ kiện, hoặc bị đưa cho may rủi quyết định hộ.
Sau hai case cỡ Google và Uber, chương này nợ bạn một sự cân lại trọng lượng, vì hình ảnh đọng lại trong đầu rất dễ là "muốn làm reachability tử tế thì phải dựng nền tảng". Không phải. Bazel và các lineage platform là lời giải cho nỗi đau ở tần suất công nghiệp, hỏi hàng nghìn lần mỗi giờ, bởi hàng nghìn người và máy. Đa số các đội không sống ở tần suất đó, và bài test thật của chương không phải "đây có phải reachability không", mà là "nỗi đau này có đáng hạ tầng không".
Có ba điều kiện mà khi cùng đúng, lời giải không cần hạ tầng nào cả. Graph nhỏ, vài nghìn node nằm gọn trong một câu query hoặc một script đọc hết vào bộ nhớ. Câu hỏi hiếm, mỗi quý một lần cho một đợt audit thì vài phút chờ là cái giá hoàn toàn hợp lý. Và độ tươi dễ dãi, câu trả lời đúng tính đến hôm qua là đủ tốt cho mục đích đang hỏi. Khi cả ba cùng đúng, một câu recursive CTE° chạy trên những bảng quan hệ bạn đã có sẵn là lời giải đúng, không phải lời giải tạm, không phải món nợ kỹ thuật, không cần ai xin lỗi ai.
Và đây là câu đáng dừng lại lâu nhất của phần này: viết câu query đó vẫn là làm graph. Người viết nó vẫn phải chọn chiều đi của đệ quy, vẫn phải chặn vòng lặp kẻo query không bao giờ dừng, vẫn ngầm chọn on-demand thay vì tính sẵn. Ba quyết định vẫn chạy đủ, chỉ là chúng chạy bên trong một câu query thay vì bên trong một nền tảng. Pattern nằm ở quyết định, không nằm ở hạ tầng. Đó là lý do chương này dạy ba quyết định chứ không dạy một công cụ.
Thứ duy nhất cần kèm theo lựa chọn khiêm tốn đó là một đôi mắt mở về tín hiệu leo thang. Câu hỏi từ mỗi quý thành mỗi tuần. Người hỏi từ một con người thành một service gọi tự động. Yêu cầu độ tươi từ "hôm qua là được" thành "ngay bây giờ". Mỗi tín hiệu là một trong ba điều kiện đang rời đi, và đội đã nhận diện bài toán từ sớm sẽ nâng cấp có chủ đích, đúng như quỹ đạo mà chúng ta nói đến ở cuối chương hai, thay vì vá trong hoảng loạn giữa một sự cố.
Quay lại buổi chiều thứ năm một lần cuối. Giả sử tổ chức đó đã có cái graph của mình, dù là một nền tảng hay chỉ một câu query được cả team tin. Tech lead hỏi, màn hình trả về mười một cái tên trong một giây, không dấu hỏi nào, deploy lăn bánh trước năm giờ. Một câu hỏi từng tốn một cuộc họp giờ tốn một cái liếc mắt.
Nhưng hãy nhìn kỹ cái danh sách ấy. Nó nói cái gì đến được. Nó không nói gì về chuyện đến bằng đường nào, đường nào ngắn, đường nào đáng tin, đường nào nên đi. Với câu hỏi của buổi chiều hôm đó, "đến được" là tất cả những gì cần biết. Có những câu hỏi khác, và chúng nhiều hơn ta tưởng, chỉ vừa mới bắt đầu ở đúng chỗ này.
Tổ chức nào không trả lời được "cái gì phụ thuộc vào cái gì" thì sẽ trả lời nó bằng sự cố.
What question does this leave you with?
In this neighborhood
hand-picked by the author“Chương trước dạy rằng mô hình cắt theo câu hỏi, không cắt theo thực tế. Chương này là lần đầu nguyên lý đó được áp vận hành: hướng của edge phụ thuộc là quyết định mô hình hoá đầu tiên của Part II, và nó quyết định câu hỏi nào được trả lời trong mili giây, câu hỏi nào trong một cuộc họp.”
“Chương hai trao bài test nhận diện và dừng ở đó. Chương này chạy bài test ấy trên một tình huống cụ thể trong đúng một câu, rồi đi tiếp phần chương hai để lại: giải. Reachability là problem class đầu tiên người đọc đi trọn vòng từ requirement đến hệ thống chạy được.”