Level of Indirection 的反思

fcamel
fcamel的程式開發心得
6 min readJun 27, 2023
Generated by MidJourney: /imagine A technological masterpiece encased in transparent glass, a computer reveals its electronic soul. The vibrant motherboard, a cityscape of capacitors and transistors, sits below spinning fans, humming softly. Glistening metal drives nestle nearby, archiving information silently. This glass sanctuary echoes an orchestra of silent, synchronized computing. — v 5

在計算機科學有這麼一句名言:

All problems in computer science can be solved by another level of indirection

甚至可以開玩笑地說,如果一層間接層無法解決,就再加一層!這個說法表達了抽象、間接性和解決問題的本質。

間接性 (Indirection) 的威力

間接性的核心是將兩個模組或計算方法解耦,使一個組件的變化不會影響到另一個組件。這個概念構成了模組設計的骨幹,從而堆疊出更強大的系統。

耳熟能詳的實例:DNS

在瀏覽器中輸入網址時,DNS 服務器將網址的網域 ( 例如 www.google.com ) 換為 IP 地址,這是網站在 Internet 上的實際位置。這個轉換過程提供了一個間接層。管理者可以更新 DNS 記錄指向新的伺服器,而不需要要求所有使用者更新網站的位置。因此,管理者可以先更新好未上線服務的伺服器內容,再替換 DNS 指向更新好內容的伺服器,作到無縫更新網站內容。

GeoDNS:具有地理感知的 DNS

和傳統的 DNS 相比,GeoDNS 會依使用者所在的地理位置改變同一個網域查到的 IP。例如,歐洲的使用者嘗試訪問網站時,GeoDNS 可以將他們導向歐洲的伺服器,而不是北美的服務器,減少載入網站的時間。

在這個例子裡,間接性層不僅使系統更有彈性,還提升了效能。

Anycast:多個位置,一個地址

Anycast 又加了一層間接層。在 Anycast 網路中,多個伺服器可以共享同一個 IP 地址。當一個使用者嘗試訪問這個 IP 時,網路會將使用者導向距離最近的伺服器。

DNS 和 Anycast 的間接層還有其它好處,例如分散伺服器的負擔和提升可靠度,就不再詳述細節。

總結來說,DNS、GeoDNS 和 Anycast 展現出間接性的額外優點:不僅是解耦實作細節提供模組間的彈性,還能縮短反應時間和提升系統穩定性。

其它例子

諸如 Docker container、程式語言 (Go、Java、Python) 的 virtual machine (byte code) ,或是 Web Framework (React、Django、RoR) 等,都運用了間接層的概念。

間接層造成除錯的困境

以 GeoDNS 和 Anycast 為例

假設你正在運行一個使用 GeoDNS 和 Anycast 的全球服務。有一天,某些使用者回報服務變慢或無回應。這些使用者分布在全球各地,而且問題並不一致。一些使用者遇到問題,而其他人則沒有。

由於涉及到多個間接層,除錯會更為複雜:

  1. GeoDNS:由於 GeoDNS 會依使用者地理位置導向最近的服務器,因此可能是最接近報告問題的使用者的伺服器有問題。但是,要確定這一點有些困難,因為你的位置(或你監控服務的位置)的 DNS 查詢可能會被導向不同的伺服器。
  2. Anycast:由於使用者會被導向在最近的服務器,因此有可能使用者導向一個正在遇到問題的服務器。網路路徑可以動態變化 ,可能很難重現使用者遇到的情況。

在這兩種情況下,因為不容易重製一樣的問題,開發者不容易鎖定應用程式的錯誤。也許這個錯誤就剛好發生在和間接層另一側的連續互動 ( 比方剛好同一個使用者端向 DNS 連查兩次卻查到不同 IP), 也有可能實際的問題不在應用程式本身,例如是 GeoDNS 的 bug!

為了有效地除錯這些問題,你需要了解這些系統的底層行為,比典型的應用程序除錯複雜得多。

以 ORM 為例

ORM 本質上提供了一個間接層,使處理資料庫像處理代碼中的物件一樣簡單。它們抽象了許多 SQL 和資料庫管理細節,提供更簡單的操作方式,例如 myTable.save()就能視情況轉成 INSERT或是 UPDATE的 SQL。

假設你需要用 ORM 刪除數千行資料。ORM 提供了一個簡單的函數來執行此操作,直接呼叫即可,一切看起來都很正常。然而,當操作開始運行時,你可能會發現它花費的時間異常地長,極端的情況下,甚至不可能完成。

為了排查問題,你必須深入了解幕後發生了什麼。也許你會發現 ORM 竟然產生了 10,000 個物件,再用迴圈走訪所有物件呼叫 delete,從而產生 10,000 的 DELETESQL,這個作法額外浪費了 10,000 個物件的記憶體,並且呼叫 DELETE10,000 次,對應用程式本身和資料庫都相當地無效率。實際上,你可能只需要下一個 DELETE 就能善用資料庫的 index 或是 table scan 刪除 10,000 筆資料。

The Law of Leaky Abstraction

Joel Spolsky 對此現象提出一個精闢的說法:The Law of Leaky Abstraction

All non-trivial abstractions, to some degree, are leaky.

該法則認為,所有非微不足道的抽象,一定程度上都會「漏洞百出」,也就是說,它們不能完全封裝它們試圖隱藏的複雜性。

從 ORM 例子中可以看出,抽象化讓操作資料庫像是呼叫物件方法一樣,但是當在大規模操作中 (例如刪除數千行資料),你開始看到抽象的限制或 “漏洞”。ORM的簡單刪除功能對於較小的任務來說很好,但對於大規模操作來說,並不是很有效。

因此,你被迫理解底層資料庫索引和解讀 SQL 的機制,以弄清楚為什麼基於ORM 的操作對這種例子並不有效。你發現自己不得不剝去抽象的層次,以便在更基本的層面上解決問題 — — 直接寫出對資料庫友善的 SQL。

本質上,The Law of Leaky Abstraction 強調:無論我們的工具和抽象層提供多麼高層級的功能,總會有情況需要深入到更低層的複雜性中解決某些問題。

總結

儘管間接層 在管理複雜性和提高效率方面提供了巨大的價值,它們也存在潛在的問題,例如加深除錯困難度或是增加團隊成員上手的門檻。從除錯的角度來說,可以看成另一種「技術債」( 用了框架卻「還沒」了解框架怎麼運作 )。

並不是說我們應該避免間接層,而是作好必須理解底層系統的準備。哪怕是系統上線出事時,或是交接找不到熟悉該套框架的人等忙碌時刻。因此,謹慎地考慮使用間接層,避免增加不必要的風險因子。

--

--