資料整合:讓資料流到對的地方
一份訂單,同時要進資料庫、搜尋索引、報表倉儲——誰說了算,順序才不會兜不起來
沒有一種資料庫,能把每件事都做到最好
這本書一路走來反覆出現同一個主題:同一個問題,往往有好幾種解法,每種都有不同的優缺點與取捨。儲存引擎有 log-structured、B-tree、column-oriented 三種路線;複寫有 single-leader、multi-leader、leaderless 三種路線。沒有哪一種是「唯一正確答案」——連號稱「general-purpose」的資料庫,骨子裡也是為某種特定的 使用模式(usage pattern) 設計的。
「想把所有事情塞進同一個軟體,幾乎保證做出來的東西樣樣稀鬆。」——這就是為什麼複雜應用裡,同一份資料常常要被好幾套系統分頭伺候:OLTP 資料庫管交易、 全文檢索索引(full-text search index) 管關鍵字搜尋、資料倉儲(data warehouse) 管分析,還有快取、機器學習、推薦、通知……一個都不能少。
不可能有單一軟體同時擅長這一切,所以你勢必要把好幾個系統拼起來——這就是本章要談的 資料整合(data integration) 問題。
工程師常說「以我的經驗,99% 的人根本不需要某功能」。這句話透露的其實是說話者自己經驗的侷限,而不是這個技術真正有沒有用。資料的用途廣得驚人,某人眼中的冷門功能,可能是另一個人的核心需求——資料整合的必要性,往往要拉遠到「整個組織的資料流」才看得清楚。
直接讀原文:整合的核心問題是什麼
書裡用很精簡的幾句話,把「資料整合」到底在整合什麼講清楚了。原文在左,白話在右,一句對一句。
複雜應用裡,同一份資料常被用在很多不同地方。不太可能有一套軟體,同時適合所有這些不同的使用情境。
當同一份資料的副本要維護在好幾個儲存系統裡,才能滿足不同的存取模式時,你必須非常清楚:資料先寫到哪裡?哪些表示法是從哪些來源衍生出來的?
如果 CDC(變更資料擷取)是更新索引的唯一途徑,你就能確信索引完全是「真相來源」的衍生品,因此一定跟它一致。
如果讓應用程式同時直接寫資料庫和搜尋索引,就會出現這個問題:兩個客戶端幾乎同時送出互相衝突的寫入,而兩個儲存系統卻用不同順序去處理它們。
這種情況下,資料庫和搜尋索引都沒有「說了算」,順序沒人決定,於是兩邊可能做出互相矛盾的決定,永遠對不起來。
櫃台的點餐單就是真相來源(system of record)——所有事情以它為準。廚房的出餐板、收銀機的帳單、外送平台的通知,都是照著點餐單「衍生」出來的 衍生資料(derived data)。 如果讓客人同時對廚房、收銀機、外送三邊各喊一次(也就是雙重寫入 dual writes),三邊聽到的順序可能不一樣,菜、帳、通知單就全部兜不起來了。
兩種整合方式:蜘蛛網 vs 星型日誌
解法就在原文最後一句話:如果沒人「說了算」,順序就會亂。所以與其讓每個系統互相直接同步,不如讓大家都只認同一份「有順序的日誌」。點點看下面兩種架構,感受一下差在哪裡:
更新衍生系統的動作,通常可以做成確定性(deterministic)且 冪等(idempotent)——同一事件重放幾次結果都一樣,所以故障後很容易復原:把日誌從某個點重新播放一次就好。
「一份總順序」聽起來很美,但撐不住規模
把所有輸入漏斗進一個單一系統,由它決定所有寫入的順序,這件事在術語上叫 全序廣播(total order broadcast)—— 本質上就是狀態機複寫(state machine replication)的應用。系統夠小時完全可行:單一 leader 就能建出這樣一條總順序的日誌。但規模一大,限制就一個個冒出來:
單一 leader 吞吐量撐不住時要分區,不同分區之間的事件順序就變得不明確。
每個資料中心通常各有自己的 leader(因為跨中心同步太慢),跨中心事件沒有定義好的順序。
各服務各自管理自己的狀態,跨服務事件之間沒有既定順序。
客戶端先在本地更新、之後才同步,客戶端與伺服器很可能看到不同的事件順序。
銀行只有一台抽號機(單一 leader)時,每個人抽張號碼牌,櫃員嚴格照號叫號——這是完美的總順序,誰先誰後一清二楚。但生意一大,開了好幾台抽號機(分區),A 機的 5 號和 B 機的 5 號到底誰先,沒人說得準;不同分行各自一台(跨資料中心),台北跟高雄的號碼根本無法比較。決定總順序,正式講法等於 共識(consensus) ——而大多數共識演算法都假設單機吞吐量就夠用,如何讓共識「超越單機、跨地域」仍是研究中的難題。
不是所有事件都需要排序——但因果依賴例外
好消息是:事件之間如果沒有因果關聯,誰先誰後其實無所謂,任意排都不影響結果。真正麻煩的是有 因果依賴(causal dependency) 的事件。書裡舉了一個經典反例:
使用者 A 把前任 B 移除好友(unfriend 事件),接著發訊息向其他朋友抱怨 B(message 事件)。A 的本意是「B 不該看到這則抱怨」。但如果好友狀態存在一處、訊息存在另一處,通知服務可能先處理 message、後處理 unfriend,於是錯誤地通知了 B——因果依賴一旦沒被系統保留,順序顛倒就會出包。
書中列出幾個能派上用場的起手式,但也坦白說:目前沒有一勞永逸的簡單答案。
不需協調就能給出總順序,但收方仍要處理亂序到達的事件,還得傳遞額外的 metadata。
給使用者看到的狀態一個唯一 ID,後續事件引用它,藉此記錄因果依賴。
能處理亂序到達的事件、維持狀態正確,但對已經發生的外部副作用(例如已寄出的通知)無能為力。
單一物件被多次更新時很簡單:把同一個物件 ID 的更新,都路由到同一個日誌分區,就能在該分區內全序排好——真正棘手的,是像上面這種橫跨不同物件、不同系統的因果依賴。
批次 vs 串流:Lambda 架構讓兩條路一起跑
資料整合的目標,是讓資料以正確形式出現在所有正確的地方——這需要消費輸入、轉換、join、過濾、聚合,最後寫到適當的輸出。批次(batch)與串流(stream)處理器,就是做這件事的工具。兩者的根本差異只有一個:串流處理 無界(unbounded) 資料,批次處理已知有限大小的資料。Lambda 架構就是讓這兩條路「同時跑」,一快一慢互補:
好處看起來很美,代價也不小:同樣的計算邏輯,要在批次與串流兩套框架裡各寫一份,除錯、調校、維運全部加倍;兩條管線各自輸出,簡單聚合還好合併,但 join、sessionization 這類複雜運算就很難對齊;對大資料集頻繁 重新處理(reprocessing) 代價又高,常被迫改成增量批次,反而把批次層搞複雜。
19 世紀英國鐵路統一軌距時,做法不是停運硬換,而是先在舊軌旁加第三條軌(混合軌距),新舊火車並行一段時間,最後才拆掉多餘的軌道。資料系統的 reprocessing 也一樣:讓新舊 schema 視圖並存,先把一小撮使用者導到新視圖,逐步提高比例,最後淘汰舊視圖——每一步都可逆,所以你才敢更大膽、更快地改進系統。統一批次與串流的新做法,需要能重放歷史事件、串流 exactly-once 語意,以及用 事件時間(event time) 而非處理時間來開窗——例如 Apache Beam 這類 API,可以跑在 Flink 或 Cloud Dataflow 上。
小試身手
資料整合的核心,就是「順序」跟「誰說了算」。來兩題檢查一下:
某團隊抱怨他們的 Lambda 架構維運成本太高、常常人力吃緊。下列哪一點最符合書中指出的 Lambda 痛點?
如果任何衍生資料都可以由日誌重新算出來,那「資料庫」這個一體成型的黑盒子,是不是可以被拆開重組?
拆解資料庫:把資料庫拆開重組
如果整個公司的資料流就是一個超大資料庫,那 batch/stream 處理器就是它的觸發器——現在,我們要把這台「全自動料理機」拆成一間開放式廚房
你以為的「不同系統」,其實是同一件事的不同分身
資料庫、Hadoop、作業系統,最抽象地看都在做同一件事:把資料存起來,讓你之後能處理與查詢它。差別只在包裝。
想想你每次下 CREATE INDEX 時發生了什麼:資料庫掃過一份一致的快照,挑出要索引的欄位、排序、寫出索引——這流程跟「建立新的複寫節點」「串流系統做初始快照的 CDC」根本是同一套動作,只是換了名字。
把整個組織的資料流攤開來看,它其實就像一個巨大的資料庫。當 batch、stream、ETL 流程把資料從一個地方搬到另一個地方、從一種形式變成另一種形式,它做的事跟資料庫內部「維護索引與物化視圖」的子系統一模一樣。
換句話說,batch 與 stream 處理器就像精緻加強版的 trigger、 stored procedure、 物化視圖維護程式;它們所維護的衍生系統,就像不同種類的索引。唯一不同的是:這些能力不再是單一資料庫產品的內建功能,而是由不同軟體、不同機器、不同團隊各自提供。
直接讀原文:兩條組合之路
既然沒有一種資料模型能滿足所有存取模式,作者在書裡提出兩條「把工具組合成一致系統」的路——這段是全模組的思想核心,值得逐句對照原文看。
我推測有兩條路,能把不同的儲存與處理工具組合成一套一致的系統。
聯邦式資料庫:統一「讀」。可以提供一個統一的查詢介面,去查各式各樣底層的儲存引擎與處理方式——這種做法叫做聯邦式資料庫,或叫 polystore。
聯邦式查詢介面延續了關聯式資料庫的傳統:一套整合系統、一種高階查詢語言、優雅的語意——但實作起來很複雜。
拆解式資料庫:統一「寫」。聯邦式解決了跨系統的唯讀查詢,卻沒解決一個問題:怎麼把「寫入」同步到那些系統裡。
拆解式做法延續了 Unix 的傳統:小工具各做好一件事,透過統一的低階介面溝通,再用高階語言(shell)把它們組裝起來。
關聯式資料庫像一台全自動料理機:按個鈕就出菜,磁碟結構、並行、當機復原全部幫你藏好。Unix 則像一間開放式廚房:給你刀、鍋、爐——一堆檔案和管線,都只是位元組序列,你自己把小工具串起來做菜。「拆解資料庫」,就是把那台全自動料理機拆開,變回開放式廚房。
把「全自動料理機」的外殼掀開
資料庫平常幫你把下面這些功能都包好、藏在同一個產品裡。拆解,就是把它們一個一個攤開,讓你看見:它們其實都只是「用資料流方式維護的衍生資料」,各自可以獨立抽出來,交給專門的工具做。
挑出被索引欄位的值、排序、寫出索引——資料庫內建 CREATE INDEX。轉換方式太標準化,所以被包進產品裡。
套語言偵測、斷詞、詞幹還原、同義詞,再建反向索引——像 Elasticsearch 這種專門系統做得更好。
預先算好、存起來的查詢結果,本質上是一種會隨資料更新的快取。
把資料的副本,持續同步到其他節點——讓別的機器也能保有一份最新資料。
資料庫把這些功能包好,你只要下一句宣告式查詢,優化器、索引、join、複寫全部自動幫你動起來——省心,但你管不到內部。拆開重組後,你可以自由挑最適合的工具(像用 mysql | elasticsearch 這樣的管線),但也要自己扛起「同步」與「一致性」的責任。天下沒有白吃的彈性。
拆解之後,「寫入」要怎麼同步到每個地方?
聯邦式資料庫解決了「統一讀」,但沒解決「跨系統同步寫」。作者認為傳統做法——跨系統的分散式交易——並不實際:不同團隊寫的系統之間根本沒有共同的交易協定,而且分散式交易本身容錯與效能都差。更務實的做法,是有序事件日誌加上 冪等消費者。按下一步,看一筆寫入怎麼安全地擴散到三個異質系統。
系統層面:非同步事件流讓故障被侷限,不會像分散式交易的同步互動那樣,把局部故障升級成大範圍故障。人的層面:不同團隊能各自獨立開發維護自己的元件——事件日誌的介面夠強(有持久性與順序)也夠通用,幾乎任何資料都適用。
程式碼和狀態,其實該分家
每一份衍生資料背後,都藏著一個
衍生函式
——次要索引的轉換很標準化,所以被內建成 CREATE INDEX;但全文搜尋的語言處理、機器學習的特徵工程,往往高度應用相關,需要自訂程式碼,這正是傳統資料庫的弱項:trigger、stored procedure、UDF 雖然存在,卻一直不好用。
理論上資料庫可以像作業系統一樣執行任意應用程式碼,但實務上它們不擅長相依管理、版本控制、滾動升級、監控。反倒是 Kubernetes、Docker 這類工具,因為專注做一件事,把「跑應用程式碼」這件事做得好得多。
1979 年的 VisiCalc 試算表裡,你在某格放一條公式,只要任何輸入變了,結果就自動重算。這正是資料系統理想的樣子:紀錄一變,它的索引就自動更新,依賴它的快取與聚合也自動刷新——但現實中的資料庫是被動的,像一個可變的共享變數,你只能不斷 poll(輪詢)去問它變了沒。
這種「訂閱變化、而非每次去問」的精神,放到真實場景裡是什麼樣子?來看一個經典案例:跨幣別購買。顧客用一種貨幣付款,買的是另一種貨幣計價的商品,需要即時匯率換算。這是 stream-table join 的經典場景,有兩種做法:
處理購買的程式,每次都跑去問匯率服務要「現在」的匯率。簡單直覺,但每次都是一次網路請求,也綁死在對方服務是否可用上。
事先訂閱匯率更新串流,每當匯率變就記進本地資料庫;處理購買時只查本地。最快、最可靠的網路請求,就是根本不發網路請求。
這個 join 是時間相依的:若日後重新處理同一筆購買事件,匯率早就不同了。要重建當初的原始輸出,必須拿到「購買當下」的歷史匯率——不管你查服務還是訂閱串流,都得處理這個問題。大方向仍是:訂閱變化、而非每次查當前狀態,讓系統更接近試算表式的運算模型。
寫入路徑、讀取路徑:一條可以移動的界線
直接讀原文,這是本節最核心的一句總結:
這樣看的話,快取、索引、物化視圖的角色很單純:它們移動了讀取路徑與寫入路徑之間的界線。
它們讓我們能在寫入時多做一點——事先算好結果——來省下讀取時的力氣。
資料的一生分兩段: 寫入路徑 是資料一進來就急切先做好的處理; 讀取路徑 是只有人來查才發生的處理。以搜尋為例:完全沒索引,寫入輕鬆、但每次查都要全掃;預先算好所有查詢結果,寫入工作量趨近無限、讀取幾乎零成本——物化視圖與快取,就落在這兩個極端之間。
完全現點現做:出餐慢,但不浪費。熱門便當先做好擺著:客人一點就拿,超快,冷門口味才現做。把菜單所有組合都先做好:不可能,口味組合無窮多。店長其實是在移動「事先做」與「現場做」的界線——名人帳號就像超人氣招牌便當,值得預先大量備好;冷門帳號現點現做就好。這呼應全書開頭 Twitter 的例子:界線可以為不同使用者畫在不同地方。
把寫入路徑一路推到你的手機螢幕
傳統假設是「客戶端無狀態、伺服器握有資料權威」。但今天的裝置能存大量狀態、甚至離線運作——換個角度看,裝置上的狀態,其實就是伺服器狀態的快取:螢幕上的像素是 model 的物化視圖,model 又是遠端資料中心狀態的本地副本。
傳統 web 只在載入那一刻讀了資料,之後資料變了要靠使用者重新整理才知道。server-sent events 與 WebSocket 打破了這個假設,讓伺服器能主動把變化推給瀏覽器——用寫入/讀取路徑的語言說:這等於把寫入路徑一路延伸到終端使用者。裝置離線期間錯過的訊息,靠 consumer offset 這個老概念補回來——每個裝置都是一個小小的事件流訂閱者,重連後從書籤位置繼續讀。
把資料庫拆開重組後,正確性不再是資料庫一手包辦,我們得自己扛起責任。
追求正確性:在故障中守住資料
網路會斷、訊息會重送、磁碟會悄悄壞掉——這一站,我們去看系統怎麼在一團混亂裡,還能讓帳目對得起來
重送一次,就可能多扣一次款
訊息處理出錯時,你其實只有兩個選擇:放棄(丟掉訊息,資料遺失),或是重試(但搞不好第一次其實成功了,只是你沒收到回報,於是同一件事被做了兩次)。處理兩次,本身就是一種資料損毀——重複扣款、計數器多加一次,都是活生生的例子。
書裡把這個目標叫做 精確一次(exactly-once,也叫 effectively-once) ——不是「真的只執行一次」,而是「不管重試幾次,最後效果都像只做了一次」。最有效的做法,是讓操作 冪等(idempotent) ——但把一個天生不冪等的操作變冪等,需要用心維護額外資訊(例如記下處理過的操作 ID),還要處理節點切換時的邊界情況。
轉帳 11 元、扣一邊加一邊,常被拿來示範交易的原子性。但如果客戶端送出 COMMIT 後、還沒收到回應就斷線,它根本不知道交易到底成功了沒。重連後重試,這筆非冪等的轉帳可能就變成轉了 22 元。
TCP 靠序號去除重複封包——但這只在「同一條 TCP 連線」內有效。使用者手動重按送出,是全新的連線、全新的請求,早就跳出了 TCP 能罩到的範圍。
資料庫交易保護得了資料庫、TCP 保護得了那條連線——但真正會讓使用者重複下單的地方,是「使用者本人」跟「應用伺服器」之間,沒有任何底層機制天生罩得住這一段。
直接讀原文:端到端論證是怎麼講的
這一段是本章、甚至整本書最重要的原則之一。左邊是原文,右邊是白話——一句對一句,慢慢讀。
某個功能,只有在通訊「兩端的應用程式」都具備知識與協助的情況下,才能被完整、正確地實作出來。
因此,把這個功能做成通訊系統本身的一部分,是不可能的。
(底層機制提供的「不完整版本」,有時仍然有用——當作效能優化。)
光靠 TCP、資料庫交易、串流處理器,都沒辦法完全擋住這種重複。
要解決,得靠一個「端到端」的方案:一個從使用者端一路帶到資料庫的交易識別碼。
去重、完整性檢查、加密——這些事只有「頭」跟「尾」兩端的應用程式最清楚該怎麼判斷,中間經過的網路、資料庫、訊息佇列頂多是幫忙降低出錯機率,但沒辦法單獨保證正確。
跑一遍:一次網路逾時的重送,怎麼被接住
手機訊號差,你按下「送出訂單」。轉圈圈太久,你又按了一次。跟著下面的角色,看端到端的操作 ID 怎麼在最後一刻攔住重複扣款。
網路一定會逾時、使用者一定會重按——這些擋不住。真正有效的做法,是讓伺服器和資料庫「認得出」這兩個請求其實是同一件事,靠的就是那個從頭跟到尾的 操作識別碼。
動手配配看:哪一層該負責防止重複?
下面幾種故障場景,各自該由哪一層機制負責擋住重複?拖拖看,配完按「對答案」。
TCP 去重、串流的 exactly-once,都能降低問題出現的機率——但只要有一段路徑跳出了它們的管轄範圍(像使用者手動重按),就得靠應用層的端到端方案補上最後一哩。
搶同一個 username,誰說了算?
很多功能都需要 唯一性(uniqueness) :帳號名稱不能重複、同一個座位不能賣兩次、帳戶餘額不能變負的。在分散式環境裡,強制唯一性本質上需要 共識 ——因為一旦有多個並行請求帶著相同的值,總得有人拍板決定誰留下、誰被拒絕。
書裡給的做法很像「單行道收費亭」:把要求唯一的值(例如 username)雜湊後路由到同一個分區,由一個 單一物件寫入 天生原子的特性撐腰的串流處理器,單執行緒、依序處理這個分區——誰先到、誰就拿到這個名字,後到的一律被拒絕。要衝吞吐量,就多開幾條互不相干的「單行道」(增加分區)。
非同步多主(multi-master)複寫會被排除——因為不同的 master 可能並行接受了衝突的寫入,值就不再唯一了。想「立即拒絕」違反限制的寫入,同步協調無可避免。
轉帳 A→B 橫跨請求 ID、付款方、收款方三個分區。與其做昂貴的 原子提交,不如先把「請求」當一筆不可變事件寫進日誌(單一物件寫入天生原子),再從它衍生出扣款、入帳兩則訊息,下游各自依 request ID 去重套用——照樣正確,卻不必讓吞吐量為了跨分區協調而受害。
「一致性」裡藏著兩件很不一樣的事
作者認為「consistency」這個詞混淆了兩種該分開看的需求:
你看到的是不是「最新」的?讀到舊資料只是暫時性的落後,等一下重讀通常就對了——就像餐廳上菜慢了五分鐘,惱人,但菜終究會來。
帳目對不對得起來?遺失、矛盾、錢憑空消失,都是完整性被破壞——就像帳單金額跟你點的菜對不上,這不是等等就好,得有人查帳、修正。
「違反即時性,是 eventual consistency(最終一致);違反完整性,是 perpetual inconsistency(永久不一致)。」前者等等就會自己好,後者不會——需要明確檢查與修復。作者更斷言:多數應用裡,完整性遠比即時性重要。
正因為完整性比即時性重要,很多業務其實能接受寬鬆一點的唯一性——先做、有問題再事後修正,這叫 補償交易。航空公司故意超賣機位,賭的就是總有人不會來,真的衝突了就退款、升等打發——這是一個商業決策,不是技術上的失敗。這樣的 避免協調的資料系統 ,用較弱的即時性換來高可用與高效能,卻依然守住完整性。
別只是相信,要親自查
硬體不完美:磁碟資料會靜靜損毀、記憶體會因輻射出現隨機翻轉位元、就連 MySQL、PostgreSQL 這樣身經百戰的資料庫,作者都親眼見過唯一性約束失守、隔離機制出錯。這叫我們對 系統模型 多一分警覺——「很少發生」不等於「絕不會發生」。
HDFS、S3 這類大型儲存系統的做法很像一個聰明的倉庫管理員:相信貨在架上,但仍然 定期親自盤點 ——背景程序持續讀回檔案、和其他副本比對,才能在損毀變嚴重前發現它。檢查資料完整性、找出並追查損毀源頭,這件事就叫 稽核。
把使用者輸入表示成一筆不可變事件、狀態由確定性的程式衍生而來(event sourcing),你就能重跑整條管線來驗證:拿同一串事件重算一次,看是不是得到一樣的結果。檢查涵蓋越多系統,損毀就越無處遁形——沿途的磁碟、網路、服務都隱含被驗證到了。
小試身手
這一站談的都是「怎麼在故障中守住正確性」,來檢查一下有沒有抓到重點。
信用卡帳單上,24 小時內的交易還沒顯示出來;另一種情況是帳單餘額和交易總和對不上。依本節的區分,哪一種才是真正嚴重的問題?
技術上,我們守住了正確性——重複的操作被認出來、唯一性被撐住、損毀能被稽核發現。但這是全書最後一站,也該談談技術之外的事:資料背後,是一個個真實的人。
做對的事:資料背後的人與責任
當演算法能決定誰能借到錢、誰能租到房、誰會被貼上「高風險」——工程師手上握的不只是程式碼,還有別人的人生
技術沒有善惡,用法才有
整本書走到這裡,我們談了可靠、可擴展、可維護的地基,談了資料模型、儲存引擎、複製、分區、交易、一致性,也談了批次與串流。但最後一章,作者把鏡頭轉向一個問題:這些系統最終服務的,是人。每個系統都為某個目的而建,每個行動都有預期與非預期的後果——身為工程師,認真思考這些後果,是我們的責任。
先看 預測分析 的兩種臉孔。用資料預測天氣、預測疾病傳播是一回事;用來預測「某人是否會再犯罪」「是否會違約」「是否會提出昂貴的保險理賠」,是完全不同層次的事——後者直接影響一個人的人生。
錯失一筆生意,成本很低;放出一筆壞帳、雇到一個問題員工,成本高得多。所以組織天生傾向謹慎。但當這種「說不」被演算法大量複製,一個被(正確或錯誤地)貼上「高風險」標籤的人,可能同時被工作、旅行、保險、租屋、金融服務系統性地拒於門外——這被稱為 演算法監獄。
原文最重的一句話:機器學習像替偏見洗錢
這一段是本章語氣最犀利的地方。左邊是原文,右邊是白話——你會看到作者怎麼一步步把「資料很客觀」這個常見迷思拆穿。
如果演算法的輸入資料有系統性偏見,系統很可能會學習並放大那個偏見。
在種族隔離的社區,一個人的郵遞區號、甚至 IP 位址,都能強烈預測他的種族。
相信演算法能吃進帶偏見的資料、卻吐出公正無偏的結果,其實很荒謬。
但這種想法卻常隱含在「資料驅動決策」的擁護者身上,這種心態被諷刺為「機器學習就像替偏見洗錢」。
預測分析系統做的事,只是把過去外推到未來;如果過去是歧視性的,它就把那份歧視原封不動地編碼進去。
如果我們想要未來比過去更好,就需要「道德想像力」——而這是只有人類才能提供的東西。
髒錢進去、髒錢出來,就算多繞幾手也不會變乾淨——偏見資料也一樣。單純換一個模型、換一種演算法,不會讓輸出自動變公正。受保護特徵就算沒被直接使用,也可能被郵遞區號這類「代理變數」悄悄找回來。
只看後照鏡開車,會一路歪下去
書裡另一個生活化的畫面:預測分析像一個只盯著後照鏡開車的司機——他用「過去的路」決定「未來怎麼走」。如果過去那條路是歪的、坑坑疤疤的,他只會一路歪下去,因為他完全照著後方的軌跡前進,還披著「數學很客觀」的外衣。
更麻煩的是 回饋迴圈 ——書中舉了信用分數與就業的例子:一時的財務困難壓低信用分數,雇主用信用分數篩選求職者,分數低的人更難找到工作,失業又把人推向更深的貧窮,分數因此更慘。跟著下面的動畫走一遍這個下坡螺旋。
但書中強調:把人也納入考量的 系統思考 能幫我們預見許多後果。該問的問題是:這個系統是在強化並放大人與人之間既有的差距(富者愈富、貧者愈貧),還是在對抗不公?
傷害有跡可循,因應原則也是
把前面談的攤開來看,預測分析可能造成的傷害,其實對應著幾條更負責任的設計原則。左邊是傷害,右邊是書中提到、能對症下藥的做法。
輸入資料若帶有系統性偏見,演算法很可能學習並放大它——就算不用受保護特徵,郵遞區號這類代理變數也能悄悄重現歧視。
分數下降、求職被拒、更加貧窮,再壓低分數——輸出回頭餵養下一輪輸入,弱勢者更難翻身,且藏在「數學很嚴謹」的外衣下。
預測分析看的常是「和你相似的人過去怎麼做」,等於用居住地、消費紀錄把人歸類、貼標籤——統計上正確不代表個案正確。
人犯錯可被追責、受影響者可以申訴;演算法做錯決定時,也該留一條說明與救濟的路,而不是無聲無息地說「不」。
若你的機器學習決策接受司法審查,你能不能向法官解釋演算法是怎麼做出這個決定的?可被檢視、可被解釋,是問責的前提。
資料與模型該是工具,不是主人。用系統思考檢視系統是在放大差距還是對抗不公,並在收集資料時就克制、只留必要的。
把「資料」換成「監控」再唸一次
作者做了一個很挑釁的思想實驗:把常見的資料科學術語裡的「data」全部換成「surveillance(監控)」,再讀一次同一句話:
「在我們『監控』驅動的組織,我們收集即時『監控』串流、存進『監控』倉儲。我們的『監控』科學家用進階分析與『監控』處理,得出新的洞見。」
連最極權、最壓迫的政權,都只能夢想在每個房間裝一支麥克風,逼每個人隨身攜帶能追蹤位置與行動的裝置。
但我們卻自願、甚至熱情地投入這個全面監控的世界。唯一的差別,只是資料收集者換成了企業,而不是政府機關。
聽起來毛骨悚然,對吧?並非所有資料收集都等於監控,但用「監控」這個詞重新檢視一次,能幫我們看清自己跟資料收集者之間,究竟是什麼關係。
想像一座「免費」遊樂園,門口寫著不用錢,但每個攤位都偷偷記下你走哪條路、在哪停留、買了什麼——再把這份「你的人生地圖」賣給想推銷的廠商。當服務靠廣告獲利,真正的顧客是廣告主,你以為自己是顧客,其實是商品。
隱私不是保密一切,資料是資產也是負債
有人說「隱私已死」,因為很多人什麼都往社群上貼。書裡認為這是誤解。
擁有隱私,不代表把一切都藏起來;而是擁有自由,可以自己決定哪些事要揭露給誰、什麼要公開、什麼要保密。
隱私權是一種決定權:它讓每個人能自己決定,在每個情境裡,自己想站在「保密」與「透明」這條光譜的哪個位置。
當資料透過監控基礎設施被從人身上抽取出來時,隱私權不一定會消失,而是被轉移給了資料收集者。
換句話說, 隱私作為決定權 一旦被公司說「相信我們會妥善處理你的資料」拿走,「該揭露還是該保密」的決定權,就從你手上交到了公司手上。
而公司手上的這批行為資料,常被叫做「data exhaust(資料廢氣)」,好像是沒用的廢料。但如果它撐起了整套廣告獲利模式,它其實是核心資產——只是這個資產很難保全,會被駭客竊取、被內部人員外洩、甚至在公司易主或政權更迭時被迫交出。所以書裡說,資料不只是資產,更是 有毒資產。
「安裝一套日後可能助長警察國家的技術,是很差的公民衛生習慣」——收集資料時,不只要衡量眼前的好處,還要考慮所有未來可能上台的政府。工程師能做的具體起手式:不必要的資料別留、不再需要就清除;用加密協定而不是只靠公司政策,去落實存取控制。
小試身手
這是全書最後一個模組的隨堂測驗,來兩題壓壓底。
這趟旅程,從一開始「可靠、可擴展、可維護」這三根支柱打地基,走到資料模型與儲存引擎怎麼把資料放好、複製與分區怎麼把系統撐大,再一頭栽進分散式系統裡種種不可靠與共識的難題,接著看批次與串流處理如何把資料流動起來——最後,我們把鏡頭轉回這一切背後真正的主角:人。演算法會犯錯、資料會被濫用、隱私會被悄悄轉移,而工程師手上握的不只是程式碼,還有別人的人生。這不是「下一站」,而是這趟旅程真正的終點:技術終究要服務於人,而不是相反。