資料:逍遙遊
天之蒼蒼,其正色邪?其遠而無所至極邪?其視下也,亦若是則已矣。 - 《逍遙遊》
對程式員來說,資料是我們時刻都在打交道的東西。我們的程式碼如同一臺機器,把進入的資料轉換或者對映成出來的資料。數學上,不過是: y = f(x)
而已。資料就像莊子口中的魚,在程式碼的作用下,一會化身為檔案,在檔案系統裡「沉潛」,一會化身為變數,在記憶體中「浮動」,一會又扶搖直上,化身為位元組流,在網路中「翱翔」。
周易裡說:簡易,變易,不易。資料再變化多端,複雜深奧,我們只要釐清了它的概念,摸清了它的脾性,就能掌控它,駕馭它;再從它的千變萬化之中,找到一些固定的規律,就能成為它的主人。倒時差睡不著覺,突然想到了這個話題,便欲與諸君聊聊。
概念
概念是事物的本元。含混不清的概念會導致含混不清的關係,進而把思維拖入含混不清的泥潭。工欲善其事,必先正其名。我們看看,圍繞著資料,都有哪些概念。
標識
標識是唯一能夠代表資料本身的「名字」。這裡有兩重含義:1)唯一 2)代表。很多時候我們把標識和名字混淆。我的公眾號的名字叫「程式人生」,但這個名字並不代表這個公號,任何公號都可以取名「程式人生」。所以標識必須唯一。在實際的工作中,我們往往只是關注到了標識的唯一性,卻忽視了它的代表性。想想看,ID 是否是一個標識?99.999% 的情況下我們都會說 ID 等同於標識。我們通過王小二的身份證號可以精準找到王小二這個人,但是身份證號和王小二之間的聯絡是脆弱的單向連線關係。我找到了王小二,但單獨從王小二身上我們並不能推演出他的身份證。80後大概還記得,大概十幾年前,我們的身份證號碼經歷了 15 位到 18 位的更迭,舊的號碼依舊唯一,但它失去了和它聯絡在一起的資料。標識既然能夠代表資料,那麼,資料理應也能夠使用某種演算法確定地推匯出其標識;同樣的,資料的標識不可能改變,也不應該改變。
符合這個條件的標識,對於人來說,是虹膜,指紋,DNA,哪怕王小二改名叫王小三,身份證從中國的換成泰國的,她還是那個他。對資料而言,符合這個條件的標識,是資料的 md5,sha1,sha2,sha3 等。
注意,標識唯一代表資料,但資料的標識可以並不唯一,這個不矛盾。
地址
我們剝繭抽絲,把標識從與其糾纏的諸多概念中分離,有利於我們對後續內容的闡述。搞清楚標識後,地址就容易理解多了。地址是用來定位和尋找資料的,它是單向連線的,也是唯一的。從這個角度來說,地址是標識的超集,標識是地址的子集。上文提到的身份證號,就是一個地址。其它常見的地址還有資料庫中一條記錄的 ID,當然,更準確地說,資料庫主機名 + 資料庫名 + 表名 + ID,是這條記錄的地址。包含完整路徑的檔名是檔案內容的地址。一個包含 IP 地址和埠的五元組(src ip, src port, dest ip, dest port, protocol)是一個 socket 連線的地址。
地址和地址對應的內容其實構成了一個邏輯上的 kv store。所以一個檔案系統,本質上是一個 kv store,給我一個檔名(key),我就可以找到並且給你它的內容(value)。
我們說標識是地址的子集,那麼如果我們就用資料的標識來定址資料呢?這打開了一個奇妙的世界 —— 我們標識指向了資料,而資料本身又能推倒出來標識,從而證明 我要定址的內容,的確是我要定址的內容 。仔細咂摸這句話的威力。舉個例子,夜深人靜,你猥瑣地帶上耳機,顫巍巍的右手點開了 蒼井空.avi,開啟卻是「般若波羅蜜多心經」。。。
引用
搞懂了地址,那麼引用是什麼?我們為什麼需要引用?引用是一箇中間人(middleware),它把資料的地址和處理資料的上下文聯絡起來。人的名字就是一個最重要的引用。有了名字,人們便不必費力地記憶 9527,而是使用「華安」來指代 9527。在華府的上下文裡,9527 是華安,出了華府,就是伯虎。所以我們說引用把資料的地址和上下文聯絡起來。從這個意義上講,引用是一種隔離(indirection),它讓被它隔離的上下兩層可以互相不知道對方的存在。計算機世界裡有句名言:Any problem in computer science can be solved with another layer of indirection。由此可見引用的巨大威力。Clojure 之父 Rich Hickey 在他的演講 Simple made easy 中說:引用讓我們避免糟糕的 PLOP - PLace Oriented Programming。
有了引用,你可以使用變數名來指代變數的記憶體地址,從而對資料進行讀寫,避免了程式碼被鎖死在某種 CPU 的定址能力上;你可以使用 git reset HEAD~1
而不用關心當前的 HEAD
究竟指向阿貓還是阿狗;你也可以 symbol link 讓 current.log 指向系統中當前的日誌檔案,從而程式不用關心當前日誌檔案的具體位置。
特性
區分清楚資料的標識,地址和引用後,我們接著講資料應該具備的特性。
不變
資料最重要的一個特性是:資料本質上是不可變的。這個論點很反大家的直覺,但如果我們加上時空的維度,就好理解一些。假設我們有資料 D0,在 t1 時刻,發生了事件 E1,導致這個資料遷移到 D1。這是資料發生變化的基本模式。我們目前的處理方式是:in-place update —— 直接把 D0 指向的記憶體地址或者檔案位置寫入 D1 的內容。資料 D0 變成了 D1,所以,我們認為它是可變的。然而,在 t0 時刻,資料依舊是 D0,這個事實是無法改變的。如果我們認清了這一點,不要做 in-place update,就能大大簡化我們處理問題的方法。
比如在多執行緒的上下文,執行緒 alice,bob 和 tyr 都拿到了 t0 時刻的資料 D0 的引用。alice 使用她的引用,把 D0 遷移到 D1,並不會影響到 bob 和 tyr 對 D0 的使用,在他們各自的世界裡,他們依舊看到的是 D0。這樣,bob 和 tyr 不必擔心 alice 的寫入行為導致他們的程式碼崩潰,因而無所謂關鍵區的保護和上鎖了,也就意味著 alice,bob 和 tyr 儘管作用於同一個資料之上,它們並不需要知道彼此的存在。從這個意義上講,資料的不可變可以大大降低程式碼的上下文依賴。
多執行緒的這個例子放在 git 上,alice,bob 和 tyr 就是從同一個 master 分離出來的幾個分支。它們可以獨立完成它們的工作,互不干擾。只有當 alice 要把自身的工作合併到 master 時,她才需要看看 master 在最新的時刻和自己從 master 分離出來的時刻發生了哪些變化,自己能否適應這些變化:是 rebase,merge 還是 resolve conflict。
「資料不可變」這種看待資料的方式有著彌足珍貴的價值。
資料在記憶體中不可變,可以高效解決併發問題。大部分函數語言程式設計語言使用了這一特性,讓簡潔可併發的程式碼易如反掌。不可變並不意味著資料不可修改,而是修改之後的狀態並不影響修改之前的狀態。要達到這樣的效果,最直白的方法是隻要有修改,就進行記憶體複製,但這種方式低效。persistent data structure 的發明和使用,讓資料的不可變在效能上和效率上具備可操作性。

資料在持久化儲存(比如硬碟)上不可變,可以用於追蹤版本的變化。git 通過這種方式打造了一個極為特別的檔案系統,在 VCS(Version Control System)領域捨我其誰。
不變意味著資料可以用其標識進行定址,這樣資料便和位置無關,可以放在任何地方:可以在遠端的自己並不擁有主權的伺服器上,也可能在本地的某個角落。它可以被快取,可以被驗真,可以被高效地儲存(無失真壓縮),可以衍生出新的資料並且追蹤和驗證這些衍生資料。
解釋一下資料的衍生。比如說一張 3200x1800 的照片 —— 我們將其衍生出 800x450 的縮圖。照片的定址使用 sha2,在衍生出來的縮圖中我們攜帶原圖的 sha2 以及衍生過程所使用的演算法。假設原圖的 sha2 地址是 abcd,縮圖是 efgh,那麼,我們可以從衍生出來的縮圖中找到原圖的地址 abcd,並且進而拿到原圖的內容。之後我們可以用相同的演算法算出縮圖,然後計算其地址,如果是 efgh,那麼可以證明這個縮圖衍生自原圖。
稍稍多說兩句 git。
git 對自己的定位是 content-addressable file system。VCS 只是 git 能力的一種體現而已(而程式碼管理僅僅是 VCS 的一種應用而已,看看,我們就這麼暴殄天物)。所謂 content-addressable,就是上文中我們說可以用資料本身的標識來定址資料。用資料的標識來定址資料,其先決條件一定是是資料不可變,二者相輔相成。因為唯有資料不可變,資料的標識才不變,才能夠用作定址的手段。git 使用這種方式把一個個檔案組織成一棵樹,再生成一個指向這棵樹的 commit。之後,資料內容的變化導致新的樹的產生,最終形成一個龐雜的 DAG(有向無環圖):

然而,我們知道,光有地址還無法無法方便使用資料,我們需要引用,於是有了 head / tag / branch,讓我們可以給一個個枯燥難懂的 sha1 地址起名,比如預設的 master branch,某個 release 的 tag,它們都是對一個個 commit 地址的引用:

IPFS(Inter-Planetary File System)進一步把 git 用來描述檔案和目錄樹的 DAG 和 p2p 網路結合起來,打造了一個去中心化的檔案系統。我們先拋開 p2p 這部分不談,看看把整個檔案系統用 content-addressable 的方式來描述有什麼好處?首先,定址到的檔案能夠自證其白,你想要找的 QmarHSr9(蒼井空.avi 的 content-address),就是你想要的 avi,一幀不多,一幀不少。其次,你可以在檔案的不同版本中漫遊。由於每次改動都會導致產生新的標識,從而生成了一個新的樹,所以任意時刻的改動都可以很方便地被找到。
和現有的檔案系統相比,你也許會擔心這種檔案系統對資源的額外消耗。的確多了不少,但這是值得的。就跟函數語言程式設計語言的 persistent data structure,浪費了一些記憶體,但收穫到的好處遠比這種浪費要多得多。檔案現在有了版本,可以恢復到任意狀態; 可以自證 —— 地址的的確確代表了內容;由於這種定址方式和具體的位置無關,我們還可以把檔案的不同部分分散到不同的機器上 —— 因為檔案的每個部分都可以自證,所以我們不擔心惡意的機器上篡改檔案內容。讓檔案的訪問和檔案在網際網路或者區域網上的位置無關,同樣的內容全網只會邏輯(物理上還是會有冗餘)上存在一份(因為地址相同),可以大大減少儲存(想想 江南 style 全網有多少份拷貝),同時提高訪問效率 —— 想象一下,5G 時代,網路的速度高於磁碟訪問的速度,檔案的不同部分存在不同的地方能夠更快地獲取檔案。
5G 時代會大大改變我們獲取資料的方式:1) 網路的速度高於磁碟訪問的速度,因而資料從本地讀取和從網路讀取幾乎沒有差異 2) 本地的儲存容量在很短時間內被網路資料塞滿(假設 5G 速度達到 10Gbps,你的 1T 硬碟也就能裝 20 分鐘的流量承載的資料),在這樣的環境下,資料的分散式儲存是是唯一選擇。所以,IPFS 背後的技術,在 5G 到來後會有非常廣闊的前景。
連線
所謂聚沙成堆,集腋成裘,資料與資料產生連線會有量變到質變的效果。1個位元組的資料僅能容納 256 個狀態,而 1GB 資料可以承載王者榮耀。一個檔案描述了伏爾加河畔的一個生活場景,而一組檔案可以構成恢弘的「戰爭與和平」。World Wide Web 對人類世界的巨大貢獻是資料通過 URI 有了唯一的地址,這些地址進而通過超文字連結的形式被連線起來。於是,原本零零散散於 FTP 之上的檔案以一種網狀的結構被重新組織 —— 資訊不再是孤零零宅著,而是以某種順序構成一個個鮮活的有機體。
資料的連線大大拓展了資料的邊界和資料本身的意義,然而,web 最大的問題也源自於它的組織方式:URI。URI 是 Rich 口中典型的 PLOP,它混淆了地址和標識。URI 背後的網站可能會消失,URI 對映到的內容可能會損壞,因此,你消費一個 URI 是一件不確定的事情:它可能 404,可能 500,可能兩小時前和兩小時後拿到的內容完全不同。這種不確定性使得我們在處理連線時需要耗費很多額外的精力去處理各種異常。
我們上面講到,git 通過一棵不可變的樹來管理內容。如果內容變化,會產生新的樹。如果把一個網站看成是一個 git repo 組成的小樹林,那麼 web 就是千千萬萬個小樹林交織成的亞馬遜。網站 A 連線網站 B 的資料,並不會因為網站 B 修改該資料而失效,因為在 git 的世界裡,一個對 commit hash 的引用永遠有效(假設 git push -f 被禁用)。
在不遠的將來,我們的 web 會以 git 的方式去連線。當這種連線形成後,web 不再僅僅是一種「空間」上的連線,還是一種「時間」上的連線。到那時,就沒有 internet archive 什麼事了,因為,一切過去,現在,以及未來都會以一種不可變的方式被記錄和連線起來。
以上。
繼續預告一下我們這次區塊鏈漫遊指南北京站的活動,機不可失,歡迎大家翹班來聽:

12:30-13:00: 簽到 13:00-13:50: 公開可驗證:去中心化的必經之路 13:55-14:15: 場景一:去中心化賬本 - 比特幣 14:15-14:35: 場景二:去中心化計算 - 以太坊 14:35-14:55: 場景三:去中心化檔案系統 - IPFS 15:00-15:45: 入門指南:如何在以太坊上構建 dApps 15:50-16:30: 入門指南:如何使用 ArcBlock Forge 鍛造你自己的區塊鏈
這次活動我們和承辦方微軟創新車庫一起,獻上一場長達四個小時的饕餮盛宴。時間上對上班族可能不那麼友好,但內容絕對精彩,不容錯過!
好看的皮囊千篇一律,有趣的靈魂萬里挑一。歡迎大家多多關注「程式人生」,在這裡你能接觸到不一樣的思考,不一樣的人生。