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