對軟件系統(tǒng)的一些理解
前言
這篇文章是想表達(dá)我對系統(tǒng)軟件的一些理解,風(fēng)格跟之前的不太一樣,整體偏“務(wù)虛”。我自己其實是不太擅長“務(wù)虛”的,甚至是有點排斥。就跟相比起看論文,我更喜歡看code,當(dāng)然我也看論文,只不過相對來說少些。 畢業(yè)以來一直在數(shù)據(jù)庫存儲引擎領(lǐng)域工作,過去5年主要精力集中在阿里自研LSM-Tree存儲引擎X-Engine研發(fā)上,并且在過去兩年多時間我們完成了X-Engine的云原生架構(gòu)升級和商業(yè)化,在公有云上承接一定規(guī)模的客戶并穩(wěn)定運行,在業(yè)界應(yīng)該也是首個基于LSM-Tree架構(gòu)實現(xiàn)云原生能力的TP存儲引擎。
完整經(jīng)歷一個TP存儲引擎的架構(gòu)規(guī)劃、設(shè)計研發(fā)、落地上線,穩(wěn)定性運維的全周期,并且得益于從我進(jìn)入數(shù)據(jù)庫領(lǐng)域一路以來經(jīng)歷的高水平團(tuán)隊、technology leader以及整個團(tuán)隊成員的出色工程能力和技術(shù)視野,加上我自己在此過程中的一些思考,階段性的形成了一些自己的心得體會。 另外,跟業(yè)界一些優(yōu)秀的架構(gòu)師和工程師交流,發(fā)現(xiàn)對于系統(tǒng)工程的理解有很多的共鳴,也收到很多非常有價值的輸入,當(dāng)然也存在一些不同的觀點。這也是促使我寫這篇文章的主要原因,希望能將我自己的一些理解表達(dá)清楚,這些觀點并不fashion,更談不上創(chuàng)新,更多的是一些自己的思考和經(jīng)驗之談。
對系統(tǒng)軟件的看法
觀點1:軟件的本質(zhì)是對硬件資源的消耗。不同軟件的區(qū)別在于,消耗硬件資源去解決什么問題以及如何分配硬件資源的消耗。軟件架構(gòu)設(shè)計中經(jīng)常提到"抽象"和“trade-off”,抽象本質(zhì)上的就是"解決什么問題","trade-off"其實就是"如何分配硬件資源"。 舉個例子TP存儲引擎和AP存儲引擎,從實現(xiàn)上可以列舉出一大堆不同的地方,行存 VS 列存、二級索引 VS ZoneMap索引、強(qiáng)事務(wù) VS 弱事務(wù)等等。這些不同之處其實都是結(jié)果,導(dǎo)致這些的根本原因是:
1)兩者解決的問題不同,TP場景主要是online實時業(yè)務(wù),這些業(yè)務(wù)的特征是整體數(shù)據(jù)規(guī)模相對較小(真正需要online處理的數(shù)據(jù),歷史數(shù)據(jù)可能很多)、請求短平快、數(shù)據(jù)locality明顯、高并發(fā)低時延等,而AP場景整體的數(shù)據(jù)規(guī)模大、計算密度高、高吞吐等。(解決什么問題)
2)TP引擎的完整事務(wù)支持使得業(yè)務(wù)的并發(fā)控制簡化很多,其實就是把業(yè)務(wù)系統(tǒng)本來需要做的事情,TP引擎自己做了,當(dāng)然也就意味著TP引擎需要為此消耗一部分硬件資源。而AP引擎為了加快數(shù)據(jù)入庫的速度,事務(wù)的支持比較弱,這部分工作還是由業(yè)務(wù)系統(tǒng)來完成(比如ETL),也就不需要為此消耗硬件資源。(如何分配硬件資源)
觀點2: 系統(tǒng)軟件的重大變革,背后基本都是硬件發(fā)展所推動的。這跟觀點1)是相呼應(yīng)的,系統(tǒng)軟件領(lǐng)域的理論在進(jìn)入21世紀(jì)之前,學(xué)術(shù)界已經(jīng)做了廣泛深入的研究。從最開始計算機(jī)的出現(xiàn),到大型機(jī)和小型機(jī),再到家庭PC和廉價通用服務(wù)器,以及現(xiàn)在的云計算IAAS服務(wù),基本上系統(tǒng)軟件發(fā)展也是跟隨這個脈絡(luò)在發(fā)展。系統(tǒng)軟件的再次火熱,本質(zhì)上也是因為IAAS這個“新硬件”所推動的。整個IAAS的on-demand獲取,打破了系統(tǒng)軟件之前在物理資源受限的背景下很多設(shè)計,這也就是為什么云原生系統(tǒng)軟件會迎來新的機(jī)會。
觀點3: 幾乎不存在某一種系統(tǒng)架構(gòu)全面領(lǐng)先另外一種架構(gòu)。這跟觀點1)2)是相呼應(yīng)的,不同的架構(gòu)選擇背后都是不同的trade-off,所謂有得必有舍。經(jīng)常聽到一些說法,你看這篇論文、這篇文章,他們這種架構(gòu)就沒有某問題,我們這種架構(gòu)就有這個問題。我聽到這些觀點的第一反應(yīng)是質(zhì)疑,這里邊主要有三個原因:
1)很多論文和文章的實驗結(jié)果是沒法復(fù)現(xiàn)的,也就說很有可能他的結(jié)論就有問題;
2)很多時候只會強(qiáng)調(diào)“得”的部分,而“舍”的部分是沒有講的。
3)我們系統(tǒng)所存在的問題到底影響有多大,是不是可以解決的,這些需要量化的數(shù)據(jù)才能確定。輕易地被各種論文和文章的結(jié)論影響,很有可能會做出一個不倫不類的系統(tǒng)。就像習(xí)武之人各個門派的武功都學(xué)學(xué),最終很容易走火入魔。
觀點4:條條大路通羅馬,最終系統(tǒng)對外呈現(xiàn)的區(qū)別,更多的是工程實現(xiàn)的原因,而非架構(gòu)的原因。不同的系統(tǒng)架構(gòu)需要解決的大部分問題本質(zhì)上其實是一樣的,并且組成一個系統(tǒng)的零部件都差不多,只是根據(jù)需要選擇哪些零部件來構(gòu)建系統(tǒng)。只有躬身入局,真正地去面對問題、分析問題、解決問題,才能認(rèn)清楚其中的本質(zhì),否則很容易變成紙上談兵。
舉個例子:經(jīng)常有人問我LSM-Tree架構(gòu)中持續(xù)寫入數(shù)據(jù)時,compaction問題對性能影響很大。這個問題我是這么看的,首先LSM-Tree架構(gòu)上寫入吞入優(yōu)勢的其中一個原因是,相比于innodb這種磁盤B+ Tree在寫入的時候直接sort on write(page內(nèi)有序,全局有序),LSM-Tree架構(gòu)選擇將一部分sort轉(zhuǎn)移到sort on compaction、sort on read,本質(zhì)上是將寫入時排序的資源消耗,轉(zhuǎn)移到了compaction或read。
刷臟其實是包含兩個動作:生成臟頁,將臟頁刷盤。innodb相當(dāng)于是在寫入的時候生成臟頁,在刷臟的時候就是單純的io操作。而compaction其實是同時做了生成“臟頁”和“臟頁”刷盤。innodb如果持續(xù)寫入的話,也會有刷臟來不及時導(dǎo)致影響寫入性能的問題。因為innodb刷臟和compaction之所以成為問題,本質(zhì)上都是因為內(nèi)存和磁盤寫入速度的差異,導(dǎo)致生產(chǎn)者消費者模型失衡。所以innodb的刷臟和LSM-Tree的compaction本質(zhì)上是相同的問題,只是通過不同的方法來將這個過程對系統(tǒng)的影響降到最低。
系統(tǒng)軟件構(gòu)建的七個面向
接下來的內(nèi)容,主要是在進(jìn)行詳細(xì)設(shè)計的時候我認(rèn)為比較重要的原則。這些原則的道理其實很容易理解,并且“軟件工程”這門學(xué)科已經(jīng)研究的很充分,但是實際操作的時候其實是蠻困難的,可能是歷史包袱的原因,也有可能是外界環(huán)境的原因,需要根據(jù)實際情況做出不同的trade-off。值得注意的是,我們做出的trade-off一定是要經(jīng)過仔細(xì)考慮的,而不是草率的,否則很容易出現(xiàn)“有舍沒有得”。
另外遵守這些原則設(shè)計實現(xiàn)出來的系統(tǒng)和不完全遵守這些原則設(shè)計實現(xiàn)出來的系統(tǒng),結(jié)果其實是“好和更好的區(qū)別”,但是“好多少”這個量在系統(tǒng)做出來之前,其實很難衡量。這七個原則不是獨立存在的,而是相輔相成的。
面向場景: 首先我們需要明確要解決什么問題,這是整個系統(tǒng)構(gòu)建的出發(fā)點。one size fit all的系統(tǒng)在過去是不存在的,在未來也不一定存在。系統(tǒng)的完善,必然是要靠不斷的迭代來完成的,那么如何迭代本質(zhì)上就是我們在那些階段解決哪些問題。一個系統(tǒng)可以有遠(yuǎn)大的目標(biāo)去解決很多問題,但是所有問題的路標(biāo)需要有相對清晰的規(guī)劃,以達(dá)到既可以快速滿足需求,同時保留向未來演進(jìn)和擴(kuò)展的基礎(chǔ)。
實際研發(fā)過程中,可能發(fā)生的兩類錯誤是:
1)想采用敏捷開發(fā)的方式來進(jìn)行工程管理,以滿足整個迭代的需求。敏捷開發(fā)本質(zhì)上先定義最小功能集,也就是首先想清楚解決什么問題,然后快速的迭代擴(kuò)充功能,有點像小步快走。在實操上,很容易把敏捷開發(fā)搞成了"快、糙、猛",有點大干30天趕英超美的味道。
2)問題定義不清楚,系統(tǒng)的“不變式”設(shè)置就容易草率。每個系統(tǒng)都有一些“不變式”,隨后很多設(shè)計都是基于這些不變式進(jìn)行展開的,比如在LSM-Tree系統(tǒng)中一個常見的“不變式”是更新版本的數(shù)據(jù)在更低的層次,同一行的數(shù)據(jù)的多個版本如果同時在memtable、level0、level1中存在,那么必然memtable中對應(yīng)的版本是最新的,level0中的版本也比level1中的更新。如果在迭代的過程中發(fā)現(xiàn)之前設(shè)置的“不變式”不合理的,那么進(jìn)行改動的代價是非常之大的。
面向解耦:無論是自上而下的去設(shè)計系統(tǒng),還是自下而上的去設(shè)計系統(tǒng),很重要的一個思考邏輯就是將各個模塊間的耦合度降到最低。解耦做地比較好的系統(tǒng),往往意味著:
1)每個模塊的功能是考慮的比較清楚,方案的完整度是比較高的;
2)有利于專注的將某個模塊實現(xiàn)的更加高效,避免其他模塊的影響;
3)有利于之后的迭代,影響面可控;
4)出了問題好排查,單個模塊的問題是比較好排查,真正那些難搞的問題往往是問題在各個模塊間傳導(dǎo)后才暴露出來,比如A模塊出問題,經(jīng)過模塊B、C、D,最后在模塊E暴露出來。
有些質(zhì)疑的觀點會說,面向解耦的思路去設(shè)計,有可能會犧牲系統(tǒng)的整體性能。其實這個跟不要一開始就為性能做過度的設(shè)計是一樣的道理,真到了某些解耦的設(shè)計影響了性能,那么該耦合的就去耦合。
把兩個模塊耦合在一起的難度往往是低于把耦合在一起的兩個模塊拆開。 面向防御:這個就是防御性編程的邏輯,要假設(shè)調(diào)用的函數(shù)都是有可能出錯的, ,比如內(nèi)存分配可能出錯,io可能出錯,基礎(chǔ)庫的調(diào)用可能出錯等等,基于此來考慮如果出錯,系統(tǒng)的行為是什么。有一個非常簡單的原則就是"fail stop", 如果沒有完整的防御,那么即使fail了也很難立即stop,最終造成一些很奇怪的表象。 通常的質(zhì)疑是:
1)你看這個函數(shù)的邏輯肯定不會失敗的。也許從當(dāng)前來看這個函數(shù)確實不會失敗,但是很難保證隨著迭代增加邏輯,之后沒有失敗的可能性。
2)加了這么多防御,防御代碼比實際邏輯的代碼還多,會影響性能。首先,現(xiàn)在cpu的分支預(yù)測能力,基本上可以做到絕大部分情況下防御代碼不會影響性能。另外跟對于面向耦合的質(zhì)疑一樣,真到某些防御代碼成為了性能瓶頸,該優(yōu)化就優(yōu)化。優(yōu)化一個防御,總比去解決一個因為沒有防御而導(dǎo)致的問題代價更低吧。
面向測試:在測試階段修復(fù)問題的代價是遠(yuǎn)低于在生產(chǎn)環(huán)境修復(fù)問題的代價,因此讓系統(tǒng)變得可測試是非常重要的。系統(tǒng)可測試的標(biāo)準(zhǔn)就是,能方便的進(jìn)行單元測試、集成測試,并覆蓋絕大部分的代碼路徑??蓽y試的系統(tǒng),隨著不斷的迭代,會累積越來越多的測試case,不斷的夯實穩(wěn)定性基礎(chǔ)。面向測試跟面向解耦、面向防御是相輔相成的。只有模塊間耦合度足夠的低,才有可能做更多的測試,否則做一個模塊的測試需要mock很多亂七八糟的東西。面向防御會使得測試的行為可以更好的預(yù)期,不然輸入了一個異常的參數(shù),具體怎么失敗是不確定的,那測試case就很難寫了。
面向運維:bug是一定會有的,對于復(fù)雜的系統(tǒng),不管前期做多少準(zhǔn)備都很難避免生產(chǎn)環(huán)境中遇到未知的問題。面向運維的主要目的是,遇到問題的時候,能用代價最低的手段去及時止損。遇到線上問題,動態(tài)調(diào)參數(shù)就能解決比需要重啟才能解決的代價更低,重啟能解決比需要發(fā)版才能解決的代價更低。面向運維不僅僅是加幾個參數(shù),加幾個開關(guān)那么簡單,而是需要把“面向運維”作為設(shè)計方案的重要組成部分來考慮,保證出了問題有運維手段,有運維手段敢用,用了以后有效果。
面向問題本質(zhì):當(dāng)去解決一個問題的時候,一定要多思考這個問題的本質(zhì)原因是什么,簡單的問題復(fù)雜化和復(fù)雜的問題簡單化,都是因為沒有抓住本質(zhì)。如果能思考清楚其背后的本質(zhì)原因,從源頭避免掉是更加徹底的解決方式,否則很容易陷入不斷打補丁的狀態(tài),我一直有個觀點:“沒有抓住問題本質(zhì)去解決問題,結(jié)果往往是在制造問題”。另外一個經(jīng)驗是,如果一個模塊連續(xù)出了好幾次問題,那么就要想想是不是在最開始的設(shè)計上就有需要改進(jìn)的地方。
面向可視化:可視化的目標(biāo)主要是以更加直觀的形式,來展現(xiàn)系統(tǒng)運行狀況,這對于系統(tǒng)調(diào)優(yōu)和診斷是非常重要的。當(dāng)系統(tǒng)異常時,可視化的方式可以幫助快速定位到系統(tǒng)哪里出了問題。另外一方面是,可以提供接口給監(jiān)控系統(tǒng)做歷史狀態(tài)的追蹤。比如oracle的診斷監(jiān)控就是一個非常優(yōu)秀的案例,而SnowFlake對于內(nèi)部狀態(tài)的打點監(jiān)控也是近乎瘋狂。
總結(jié)
說了這么多,最終系統(tǒng)還是靠一行行的code實現(xiàn)出來的,保持匠心、嚴(yán)謹(jǐn)、較真的態(tài)度去打造系統(tǒng)是非常樸素正確,但又很難做到的事情,共勉!