Build quality in: từ Deming, Toyota đến đêm mình tự tắt CI
Anh đồng nghiệp người Đức nói 'build quality in' ở mọi kickoff, chưa bao giờ ở lúc cuối. Mình gật gù nửa năm rồi mới hiểu, đúng lúc tự tay tắt CI để kịp hotfix.
Có một lão đồng nghiệp người Đức, hơn mình gần hai chục tuổi, mà mình mất nguyên nửa năm mới để ý ra cái thói quen ngôn ngữ của lão. Cứ mỗi lần team chuẩn bị bắt tay vào việc gì đó mới, một feature, một pipeline, một lần refactor, lão lại buông đúng một câu: "Let's build quality in."
Lúc đầu mình nghe câu đó như kiểu cửa miệng vô hại, đại loại "nhớ viết test nhé" hay "làm cho cẩn thận vào". Mình gật gù. Ai mà chẳng muốn làm cho chất lượng. Gật xong rồi quay lại với việc của mình, chẳng nghĩ thêm.
Phải tới khi nghe đủ nhiều lần, mình mới thấy một chi tiết kỳ lạ: lão không bao giờ nói câu đó ở cuối. Không bao giờ ở lúc review, lúc QA trả bug về, lúc chuẩn bị release. Lão chỉ nói nó ở đúng một thời điểm: lúc bắt đầu, khi chưa có lấy một dòng code nào được viết.
Và đó, hóa ra, không phải tình cờ. Đó là toàn bộ ý nghĩa của câu nói, gói gọn trong việc lão chọn nói nó lúc nào.
Nó cũ hơn cả ngành phần mềm
Mình về tra "build quality in", càng tra càng thấy nó sâu hơn mình nghĩ nhiều. Cụm từ này không sinh ra từ giới phần mềm. Nó già hơn cả ngành chúng ta.
Nguồn gốc là W. Edwards Deming, một ông người Mỹ dạy về quản lý chất lượng. Trong cuốn Out of the Crisis (1982), điểm thứ ba trong 14 nguyên tắc quản lý của ông viết: "Cease dependence on inspection to achieve quality. Eliminate the need for inspection on a mass basis by building quality into the product in the first place." [1]
Cụm "build quality in" sinh ra từ đúng câu đó. Và ý của Deming sắc hơn vẻ ngoài của nó nhiều. Trước Deming, cách làm chất lượng phổ biến ở các nhà máy là: cứ sản xuất hàng loạt đã, rồi cuối dây chuyền cho người đi soi, nhặt hàng lỗi ra loại bỏ. Deming nói thẳng cách đó vừa đắt vừa vô nghĩa. Câu của ông đáng để dịch cẩn thận: kiểm tra không cải thiện chất lượng, nó chỉ đo xem chất lượng đang thiếu tới đâu. Build quality in nghĩa là nhúng chất lượng vào từng công đoạn ngay từ đầu, để khỏi cần một khâu soi lỗi ở cuối.
Đọc tới đây mình hơi giật mình. Vì cái mô hình mà Deming chê, làm xong rồi soi, chính xác là cách phần lớn team phần mềm đang vận hành. Dev viết xong, ném qua tường cho QA. QA soi, tìm bug, ném ngược lại. Chúng ta gọi đó là quy trình, thậm chí gọi nó là một giai đoạn trong SDLC. Deming gọi đó là bằng chứng của thất bại.

Phần làm mình ở lại lâu nhất là câu chuyện lịch sử của chính Deming. Ông là người Mỹ, nhưng nước Mỹ phớt lờ ông gần ba mươi năm. Sau Thế chiến thứ hai, hàng Mỹ bán chạy bất kể chất lượng tốt xấu, nên chẳng ai buồn nghe một ông thống kê học nói về chuyện phòng lỗi từ gốc.
Người chịu nghe là Nhật Bản. Mùa hè năm 1950, hiệp hội JUSE mời Deming sang giảng. Tại hội nghị ở núi Hakone tháng 8 năm đó, khán phòng là dàn lãnh đạo công nghiệp đại diện cho khoảng 75% tư bản công nghiệp Nhật lúc bấy giờ. [2] Thông điệp của ông gọn lỏn: cải thiện chất lượng sẽ làm giảm chi phí, đồng thời tăng năng suất và thị phần. Nghe ngược đời, vì ai cũng tin chất lượng cao thì phải đắt hơn. Người Nhật tin ông. Họ làm theo. Và phần còn lại là cái mà sách giáo khoa gọi là phép màu kinh tế hậu chiến.
Nước Mỹ chỉ tỉnh ra vào năm 1980, khi xe Nhật bắt đầu đè bẹp Detroit, qua một phóng sự truyền hình có cái tên thẳng thừng: "If Japan Can... Why Can't We?". Ba mươi năm. Cùng một ý tưởng, một bên đói khát thì ôm trọn, một bên tự mãn thì bỏ lỡ cả một thế hệ.
Lúc đó mình mới hiểu lão không nhắc một mẹo vặt gì cả. Lão đang đứng trong một truyền thống bảy mươi năm tuổi mà với dân kỹ thuật châu Âu gần như là một niềm tin nghề nghiệp. Với họ, dựa vào người soi lỗi ở cuối dây chuyền không phải một lựa chọn quy trình. Nó là một kiểu lười, và hơi thiếu tôn trọng nghề.
Toyota biến triết lý thành cơ chế sờ được
Deming cho cái tư tưởng. Toyota cho cái cơ chế. Và đây là phần mình thích nhất, vì nó cụ thể tới mức gần như chạm được.
Toyota có ba thứ.
Thứ nhất là jidoka, thường dịch là "tự động hóa có chất người". Gốc của nó từ năm 1896, khi Sakichi Toyoda chế ra một cái máy dệt tự dừng lại ngay khi sợi chỉ bị đứt. [3] Nghe đơn giản, nhưng ý tưởng bên dưới rất nặng: máy tự phát hiện bất thường và tự dừng, thay vì cứ dệt tiếp ra một mét vải lỗi rồi để người ta phát hiện sau. Lỗi bị chặn ngay khoảnh khắc nó xuất hiện.
Thứ hai là andon cord, một sợi dây thừng treo phía trên mỗi trạm làm việc. Bất kỳ công nhân nào, kể cả người mới nhất, thấy một lỗi đều có quyền giật sợi dây đó để dừng toàn bộ dây chuyền. Không phải báo cáo lên cấp trên rồi chờ. Giật dây, cả dây chuyền dừng, cả team xúm lại xử lý tận gốc ngay tại chỗ.
Thứ ba là poka-yoke, chống lỗi từ thiết kế. Thay vì dặn người ta đừng làm sai, bạn thiết kế sao cho thao tác sai trở nên bất khả thi về mặt vật lý. Giống cái phích cắm chỉ cắm được đúng một chiều, bạn không thể cắm ngược kể cả khi cố.
Để ý ba cái này có chung một điểm: tất cả đều đặt cái bẫy lỗi ngay tại chỗ lỗi sinh ra, không phải ở cuối dây chuyền. Đó là build quality in ở dạng vật lý. Và đó là lý do lão luôn nói câu đó ở lúc bắt đầu.
Bức tường giữa hai team
Mình kể bạn nghe chỗ mình nhìn thấy nguyên lý này rõ nhất, hay đúng hơn, chỗ nó vắng mặt rõ nhất.
Ở chỗ mình làm có hai team đứng cạnh nhau: Data Ingestion và Data Science Modeling. Ingestion lo kéo data về, làm sạch, đổ vào kho. Modeling lấy data đó ra train model. Trên giấy, đây là một dây chuyền đẹp: data chảy từ trái sang phải, mỗi bên lo một khúc.
Trên thực tế, ranh giới giữa hai team đôi khi thành một bãi chiến trường nhỏ. Modeling train model, kết quả ra vô lý, đào ngược lên thì phát hiện data có gì đó sai: một cột đổi nghĩa mà không ai báo, null tràn vào chỗ lẽ ra không được null, một cái schema âm thầm đổi ở thượng nguồn. Modeling quay sang Ingestion. Ingestion nói lại, hợp lý không kém: ai mà biết tụi mày cần cái cột đó sạch tới mức nào, tụi mày có nói đâu.
Cả hai bên đều đúng. Và đó chính là vấn đề.

Vì cái cảnh đó, nhìn qua lăng kính Deming, lộ ra ngay: team Modeling đang đóng vai người soi lỗi ở cuối dây chuyền cho chất lượng data. Họ phát hiện data hỏng ở khâu muộn nhất có thể, sau khi nó đã chảy qua cả pipeline, đã được train thành một model vô dụng, đã tốn vài tiếng GPU. Đắt nhất, muộn nhất. Đúng cái Deming bảo là vừa đắt vừa ngu.
Và cuộc cãi nhau giữa hai team không phải vấn đề con người. Không ai lười, không ai ẩu. Nó là một vấn đề kiến trúc trách nhiệm: không ai build quality in ở ngay cái ranh giới giữa hai team, nên cái ranh giới đó thành nơi lỗi tích tụ rồi nổ. Sợi dây andon, nếu có, phải nằm đúng ở chỗ data bàn giao từ Ingestion sang Modeling. Nhưng ở đó chẳng có sợi dây nào cả.
Mình làm modeling, nên mình hay diễn cái cảnh này ra thành công thức cho dễ nghĩ. Coi pipeline có \(n\) trạm, một lỗi sinh ra ở đầu, mỗi trạm \(i\) có xác suất \(p_i\) bắt được nó. Xác suất một bản ghi lỗi sống sót tới tận Modeling là tích của tất cả các lần thoát:
\[P_{\text{reach}} = \prod_{i=1}^{n} (1 - p_i)\]
Mô hình "ném qua tường" nghĩa là mọi \(p_i = 0\) ở thượng nguồn, nên \(P_{\text{reach}} = 1\): mọi lỗi đều tới Modeling, và Modeling buộc phải làm cái trạm soi cuối cùng. Build quality in không phải là thuê một người soi giỏi hơn ở cuối để nâng cái \(p_n\) lên. Nó là đặt một poka-yoke ngay ở nguồn để \(p_1 \to 1\), kéo cả cái tích về \(0\). Đẩy xác suất bắt lỗi về sát đầu pipeline, chứ không phải về cuối. Đây là mô hình đồ chơi để sắp suy nghĩ cho gọn thôi, không phải định luật đo được, nhưng nó nói đúng một điều: dí cái gate về cuối thì dù gate có tốt tới đâu, lỗi cũng đã kịp đi hết cả quãng đường rồi.
Build quality in cho data, cụ thể, là gì
Nếu bạn là dev hay tech lead, tới đây bạn sẽ muốn một câu trả lời cụ thể hơn khẩu hiệu. Build quality in ở cái ranh giới đó, cụ thể, là gì? Mình thử map thẳng ba cái của Toyota sang:
Poka-yoke là data contract. Ingestion và Modeling ngồi với nhau định nghĩa rõ: cột này kiểu gì, được phép null hay không, range nào hợp lệ, schema đổi thì version ra sao. Rồi đóng đinh nó thành một thứ máy đọc được, Pydantic, JSON Schema, dbt contract, bất cứ thứ gì. Sau đó data sai contract bị chặn ngay ở cửa, không cắm ngược được.
Jidoka là validation tự dừng ở điểm ingest. Khi một batch data fail expectation, pipeline tự quarantine nó lại thay vì để rác chảy xuống cho Modeling. Great Expectations, dbt test, Soda, công cụ gì cũng được, miễn là nó dừng đúng lúc.
Andon cord là khi Modeling có quyền chặn một data set lỗi, và quan trọng hơn, là khi Ingestion không coi điều đó là một lời buộc tội. Sợi dây chỉ hoạt động nếu người giật nó không bị trừng phạt.

Còn cái "three amigos" mà người ta hay nói, Ingestion với Modeling với product ngồi định nghĩa contract trước khi một dòng code nào được viết, chính là cái lão làm ở mỗi kickoff. Lúc đó mình mới thật sự hiểu vì sao lão luôn nói câu đó ở đầu. Vì build quality in cho data, dịch ra thành hành động cụ thể, nghĩa là: thống nhất contract trước, đặt validation tại nguồn, đừng đợi model train ra kết quả vô lý mới đi truy ngược cả pipeline.
Lúc bắt đầu là lúc duy nhất việc đó còn rẻ.
Rồi tối về, mình làm ngược lại tất cả
Tới đây thì giọng mình nghe có vẻ thông tuệ quá. Để mình kéo nó về thực tế chút đã.
Vì mình gật gù với tất cả những điều trên ở chỗ làm, rồi tối về mở laptop làm cái sản phẩm LMS của riêng mình, và làm ngược lại sạch sẽ tất cả.
LMS của mình có một bộ automation test, khoảng 800 test case, chạy qua CI/CD. Nghe thì đúng bài build quality in lắm. Vấn đề là cái pipeline đó chạy mất 30 phút. Ba mươi phút cho một lần chạy đầy đủ, phần lớn thời gian là dựng temp database để test cho đủ các edge case. Và demand của mình thì không chờ ba mươi phút: QA team trả về tầm 50 bug mỗi CR mỗi ngày, mỗi bug là một hotfix cần lên càng nhanh càng tốt.
Bạn đoán được mình làm gì rồi. Có những lúc, để kịp ship một hotfix, mình tắt cái CI/CD automation test đi. Chỉ một lần thôi, mình tự nhủ. Ship cái này đã, lát chạy test sau.
Tốc độ tăng vù vù. Cảm giác cực kỳ đã. Rồi chuyện phải tới đã tới: test case dần outdated, vì code đổi mà test không được cập nhật kịp, cho tới khi một mảng lớn trong 800 case kia biến thành một đống rác mà chính mình cũng không dám tin tưởng nữa. Cái bộ test sinh ra để bảo vệ chất lượng, giờ thành thứ mình né tránh.
Đây là chỗ mình nhận ra một điều mà phiên bản giáo điều của build quality in không nói cho bạn.
Sợi dây quá nặng để giật
Có một câu chuyện về andon cord mà mình nghĩ về suốt mấy hôm. Khi các hãng xe Mỹ sao chép hệ thống của Toyota, họ lắp đúng sợi dây andon lên mỗi trạm. Nhưng có một thứ họ không sao chép được: ở nhà máy Mỹ, công nhân không dám giật dây. [4] Vì giật dây là làm dừng cả dây chuyền, mà làm dừng dây chuyền thì bị để ý, bị hỏi tội. Sợi dây có đó. Quyền giật thì không.
Tụi mình đã có CI/CD, có automation test, có monitoring rồi. Vậy là build quality in xong rồi còn gì.
Mình cũng từng tin đúng như vậy. Nhưng có tool không có nghĩa là có văn hóa. Bạn có thể mua Playwright, dựng CI/CD, gắn đủ thứ test, rồi vẫn để chất lượng là việc của riêng QA, vẫn rush release khi pipeline đỏ, vẫn ngầm coi người dừng lại fix gốc là người làm chậm sprint. Tool có, empowerment không. Sợi dây treo đó cho đẹp.
Câu chuyện "thiếu văn hóa chứ không thiếu tool" đó đúng. Nhưng kinh nghiệm cái LMS của mình cho mình thấy một nửa thứ hai mà bài học kinh điển hay bỏ qua.
Cái CI/CD 30 phút của mình không bị tắt vì mình không tin vào chất lượng. Mình tin chứ. Mình tắt nó vì sợi dây andon của mình quá nặng để giật. Toyota thiết kế andon để giật trong một giây. Còn mình, mỗi lần "giật dây" tốn 30 phút và một cơn đau đầu dựng database. Dưới áp lực 50 bug một ngày, một sợi dây nặng 30 phút thì sớm muộn cũng bị người ta cắt phăng đi cho rảnh.
Viết thành công thức thì nó trần trụi thế này. Gọi \(g\) là thời gian chạy gate một lần, \(f\) là số lần phải qua gate mỗi ngày. Nếu mỗi thay đổi đều phải qua gate đầy đủ, cái thuế thời gian mà gate bắt cả ngày trả là:
\[T_{\text{tax}} = f \times g = 50 \times 30\ \text{min} = 1500\ \text{min} \approx 25\ \text{h}\]
Một ngày làm việc có tám tiếng. Cái andon của mình đòi hai mươi lăm giờ. Nó không nặng, nó bất khả thi. Và khi một sợi dây đòi nhiều thời gian hơn cả ngày làm việc thì câu hỏi không còn là nó có bị cắt hay không, mà là bị cắt lúc nào.

Vẽ ra thì xác suất một gate được tôn trọng tụt rất nhanh theo độ trễ của nó. Toyota giữ andon ở mức một giây nên gần như luôn được giật. Cái CI 30 phút của mình nằm tận cuối trục log, chỗ xác suất gần chạm đáy. Không phải mình kém kỷ luật hơn công nhân Toyota. Mình chỉ đang giật một sợi dây nặng gấp gần hai nghìn lần.
Nói cách khác: build quality in không chỉ chết vì văn hóa. Nó còn chết vì chính cái quality gate bị build sai. Khi mỗi lần verify đều chậm và đau, khâu kiểm chứng tự nó thành nút thắt, và một bộ test chạy 30 phút, brittle, không được bảo trì sẽ tự biến thành thứ mà ngay cả người dựng ra nó cũng muốn né. Cái dùng để build quality in, trớ trêu thay, lại không hề được build quality in. Test suite của mình rot vì mình không đối xử với nó như một phần của sản phẩm, mà như một thủ tục phải làm cho xong.
Nhìn lại con số 800 test, temp database, 30 phút, mình nghi đây là dấu hiệu của một cái kim tự tháp test bị lộn ngược: quá nhiều integration test cần database thật, quá ít unit test rẻ và nhanh chạy ngay tại chỗ code sinh ra. Mình đã vô tình dựng một trạm inspection chậm rồi gọi nó là build quality in. Hai thứ đó khác nhau về bản chất. Đây là kiểu kinh điển của chuyện giải sai bài toán ngay từ đầu: mình tối ưu cái việc soi lỗi, trong khi đáng ra phải tối ưu cái việc không sinh ra lỗi.

Cẩn thận với con số nghe quá gọn
Mình muốn dừng ở một chỗ, vì nó liên quan tới con số mà ai viết về chủ đề này cũng trích.
Bạn sẽ nghe câu này ở mọi bài về chất lượng phần mềm: một cái bug nếu tìm thấy lúc thiết kế chỉ tốn 1 đồng, để tới lúc code thì 10 đồng, lúc test thì 100 đồng, lên production thì 1000 đồng. Đường cong chi phí lỗi tăng theo cấp số nhân. Nghe rất thuyết phục, và nó được dùng để biện minh cho gần như mọi thứ.
Đường cong chi phí lỗi 1x/10x/100x/1000x là một con số gần như bịa, không có cơ sở nghiên cứu
Laurent Bossavit, trong cuốn The Leprechauns of Software Engineering, đã truy ngược cái đường cong này tới tận nguồn và phát hiện nó đến từ một tài liệu đào tạo nội bộ của IBM năm 1981, không phải một nghiên cứu peer-review nào cả. Mấy study mà người ta hay trích để chống lưng cho nó thì bị xuyên tạc, thậm chí có study cho ra kết quả ngược chiều với tỉ lệ 2:1. [5]
Mình nói điều này không phải để bác bỏ build quality in. Cái hướng thì đúng: lỗi phát hiện muộn thường đắt hơn, chuyện đó ai làm nghề cũng cảm nhận được. Mình nói để bạn cẩn thận với những con số nghe quá gọn. Build quality in không cần một đường cong giả để biện minh. Nó tự đứng được bằng logic của chính nó: chặn lỗi lúc nó còn rẻ thì hơn là đi dọn lúc nó đã đắt. Không cần phóng đại thành 1000 lần.
Và để công bằng, build quality in không miễn phí. Viết test trước thì chậm hơn lúc đầu. Dừng dây chuyền tốn thời gian thật. Không phải team nào, giai đoạn nào cũng đáng. Một cái POC bạn biết chắc sẽ vứt đi sau hai tuần thì việc gì phải build contract chặt chẽ. Cái nghệ thuật nằm ở chỗ biết khi nào lý tưởng này đáng theo, và khi nào nó chỉ là cầu toàn.
Cái "nghệ thuật" đó cũng viết thành một bất đẳng thức được. Gọi \(N\) là số lỗi bạn sẽ gặp trong cả vòng đời sản phẩm, \(C_{\text{setup}}\) là giá dựng cái gate hoặc cái contract ban đầu, còn \(C_{\text{late}}\) và \(C_{\text{early}}\) là chi phí xử một lỗi khi bắt muộn so với khi bắt sớm. Build quality in đáng theo khi:
\[N \cdot (C_{\text{late}} - C_{\text{early}}) > C_{\text{setup}}\]
POC hai tuần thì \(N\) bé tí, vế trái không bao giờ vượt nổi cái \(C_{\text{setup}}\), nên đừng dựng. Pipeline production sống ba năm thì \(N\) lớn tới mức \(C_{\text{setup}}\) thành con số làm tròn. Toàn bộ chuyện "biết khi nào đáng theo" chỉ là cái bất đẳng thức này, viết bằng ký hiệu cho khỏi cãi nhau bằng cảm tính.

Cũng cần nói thẳng: đây vẫn là mô hình đồ chơi, mình không đo được \(C_{\text{late}}\) thật của bạn. Nó chỉ giúp mình không rơi vào hai cái bẫy ngược nhau: build quality in cho một thứ sắp vứt, hoặc bỏ qua nó cho một thứ sẽ sống lâu hơn mình tưởng.
Mình vẫn chưa gỡ xong
Mình kết bài này ở một chỗ thành thật: mình vẫn chưa gỡ xong.
Cái LMS của mình vẫn còn đó, với pipeline 30 phút và một đống test case mình đang phải dọn dần. Mình đang thử vài hướng. Tách một bộ smoke test nhanh để chặn hotfix thay vì nuốt trọn 30 phút. Đổi cách dựng database để nó đỡ ngộp, dùng template thay vì dựng lại từ đầu mỗi lần. Xem trong 800 case kia bao nhiêu thật sự cần database thật và bao nhiêu có thể hạ xuống thành unit test rẻ. Chưa biết có ăn thua không. Khi nào có kết quả thật, mình sẽ kể bạn nghe.
Nhưng có một thứ mình nghĩ mình đã hiểu, cái thứ mà mình gật gù suốt nửa năm mà không thật sự nắm. Vì sao lão luôn nói "build quality in" ở lúc bắt đầu, chưa bao giờ ở lúc cuối.
Vì lúc bắt đầu là lúc duy nhất chất lượng còn rẻ. Sau đó, mọi thứ chỉ còn là đi soi lỗi và đổ lỗi cho nhau. Cái ranh giới giữa Ingestion và Modeling, cái CI 30 phút của mình, cả hai đều là cùng một bài học nhìn từ hai phía: bạn không thể nhét chất lượng vào một thứ đã làm xong. Bạn chỉ có thể xây nó vào từ đầu, hoặc trả giá để dọn về sau.
Nên câu hỏi mình để lại cho bạn, cũng là câu mình đang tự hỏi về chính team mình: ở cái ranh giới giữa hai team của bạn, ai đang được phép giật sợi dây andon? Và khi họ giật, người bên kia coi đó là một bàn tay cứu giúp, hay một lời buộc tội?
Bình
Citations
- W. Edwards Deming. Out of the Crisis. MIT Press. 1982.
- Speech by Dr. Deming to Japanese Business Leaders in 1950. The W. Edwards Deming Institute. 1950.
- Jidoka - Toyota Production System guide. Toyota UK Magazine.
- You're Not Doing DevOps if You Can't Pull the Cord. DevOps.com.
- Laurent Bossavit. The Leprechauns of Software Engineering. 2015.