從用戶功能開始構架系統框架
本文討論一下從用戶功能開始做系統構架的細節問題。
我在前面有一篇博客討論了需要在開源項目上製造真空,有人在下面說,這得這個項目牛逼才行。我們這裡就來討論一下怎麼樣才能讓「項目牛逼」。
一個軟體項目怎麼樣才會牛逼?良好的架構?優秀的演算法?優雅的代碼組織?不是的,一個軟體項目牛逼是因為它有用,並且把它的競爭對手都逼死了,它就牛逼了。
所以,研究一個項目是否有前途,第一研究它是否必須的,第二研究它如何保證它相對別人的優勢。這是要守(關於「守」的概念,參考這裡)的第一級要素。比如用OpenSSL加密數據,用Libz壓縮數據,這是個剛性要求,你做了一個這樣的庫,它就具有生存的基礎。但實現一個優雅的,計算把《道德經》進行SHA-1加密的庫,無論做成什麼樣子,它都不會牛逼起來。
所以,牛逼,就是有需求驅動你(有人,足夠的人,要用你),你比別人好,你就沒有辦法不牛逼。所以,剩下的問題就是怎麼控制你的設計比別人有競爭力。在架構設計上,保證競爭力,很大程度上是如何對資源進行控制。
其實,不但開源項目需要製造真空,所有的架構設計都是在製造真空。作為架構師,你經常需要體驗那種從一兩個人開始項目,然後慢慢擴展到幾十到上百人的規模的經驗。我在這個過程中的體會是,你覺得你一開始有滿滿的自由度,實際上很快你就要面對激烈的沖刷。人力規模一上來,你代碼都看不過來,你前期的所有準備,都是怎麼防住這些衝擊。如果你沒有很好的控制邏輯,這些快速增加的代碼量,會很輕鬆沖跨你當初簡單的設計,讓你的理想落空。而缺乏控制的洪水,是形成不了力量的,這樣的項目就不會牛逼。
代碼不來自架構師,代碼來自資源投入,大家都在拼資源,為什麼你的資源比別人高效?那就是你的資源都被用到刀刃上了。
所以,架構師的時間是很緊迫的,在前期人少時的架構設計不上心,後面死無葬身之地。而且,真的就和我前面說的,大部分人死都不知道自己怎麼死的,還在歸咎於資源不到位啦,工程師編程水平不行啦,合作夥伴不肯合作啦……作為一個集體,你要批評誰,總能找到理由的,但失敗是真真切切的。
因此,構架設計是當你只有一兩個人的時候,你就要花所有的精力來構築水道,保證洪水進來的時候你能夠控制這些資源都轉化為你的競爭力。
我們還是拿前面這個OpenSSL和Libz為例,假設你要給這樣的庫做一個加速器的框架,你首先控制什麼?
顯然不是你的硬體做成什麼樣子,那個不是你這個框架生存的原因,你的框架生存的原因是能讓libz和OpenSSL跑得更快(註:由於這兩個東西的概念可以互相借用,後面暫時先用比較簡單的libz來推演),不是因為你有一個加速器硬體。
所以,控制這個框架應該做成什麼樣子,守的是libz這個庫的使用模式,然後才是你硬體的限制。所以,你的介面最優雅的方式應該是這樣的:
acce_compress(algorithm, input_buffer, output_buffer)n
但這樣從libz庫來說就不好用,因為壓縮演算法就有很多種,而且每個演算法有參數,更合適的做法是這樣的:
init_algorithm(algorithm, parameter);nacce_compress(algorigthm, input_buffer, output_buffer);n
這是原始需求,也就是前面說的「有用」,這裡不考慮任何實現的困難,僅從「有用」這個角度來說,設計成這樣才是合理的。這是我們要守的第一層,任何超越這一層範圍的,都需要有足夠的理由(證明對手不會有可能因為這點超過我)
很多人上來就開始考慮設備管理,考慮打開設備。但這些是錯誤的方向,設備管理是你硬體本身的事情,不是我libz引入的事情,我可沒有要求你有「設備」啊,我只是要壓縮,你設備打不打開關我鳥事?
我們從libz這個角度守,就能盡量降低我們自行引入的,破壞最優競爭力的要素,這樣我們才不會被後續增加的複雜度左右了方向。所有破壞最優競爭力的要素,就必須有合理的理由,並且保證其他對手找不到更好的手段來超越我。
我們下面來推演一下幾個不得不引入的幾個破壞性要素:
1. 設備數量限制:如果通過加速器加速,加速設備不足怎麼辦?
2. 數據傳遞成本:把數據送入硬體,再從硬體中送出來如何處理
3. 系統調用成本
首先考慮數據傳遞成本,前面列出的4個破壞性要素中,2,3都是傳遞成本。這個成本是用硬體加速特有的,如果用CPU來實現這個演算法,根本沒有這個問題。用硬體加速要贏CPU加速,首先要解決這個問題。我用如下方法來保證這個問題可以被解決:a. 壓縮演算法本身不通過系統調用來對硬體發起請求。讓acce_compress從用戶態直接訪問硬體。這樣就降低系統調用的成本了
b. 做一個分界線,小buffer壓縮仍使用CPU演算法,不通過硬體加速
c. buffer不直接放在設備上,而是放在內存中,讓硬體直接操作內存來實現對數據的處理
策略b和整個方案沒有關係,我們可以用一個highlevel_acce_compress()來封裝這個功能,所以暫時可以在推演中忽略它。策略a和c卻會帶來新的問題。對於策略a,它又帶來兩個問題:
i. 要求用戶態認知硬體,對硬體或者軟體的升級帶來挑戰(硬體升級,用戶態的庫也要升級)。這個看來無解,只能盡量讓硬體統一,還有就是把輸入和Buffer分開,減少被迫需要拷貝的可能(也就是在buffer上用兩片內存,metadata用一片,數據用另一片,這樣,主數據流的數據就有自由度可以放在任何地方,包括放在用戶程序的堆或者棧中)
ii. 用戶態認知硬體介面,容易帶來安全性問題。這個的解決思路是:硬體不依賴用戶態提供數據的安全性,只保證對傳進來的數據進行處理,而這些數據都在一個進程之內,這個安全性不比你用CPU壓縮差,因為你基於CPU的庫也可能有bug,弄死一個進程,我只要保證用於加速的硬體死不了,弄死進程並沒有帶來額外的安全問題。
策略c也帶來一個問題:要讓設備操作內存,就要處理DMA,要做DMA就需要把內存pin住。所以這可以有兩個解決方案,第一個方案是用SMV,也就是從設備一側發起缺頁:你扔一個虛擬地址給加速器,加速器工作的時候如果缺頁,請求發回給CPU,CPU把這個缺失的頁載入回內存,然後加速器繼續工作。但這個方案對匯流排系統要求很高,我到現在只看過片內設備做成功的,沒有見過通過PCIE介面做成功的,所以,完全寄望於這個似乎不是個好的選擇。
第二種方法是主動pin住這個內存,Linux中可以用gup(get_user_page)來實現,但這個東西的限制是只能在一次系統調用中有效,不適合我們的情況,要加上VM_PIN才有可能成功,而後者還進不了主線內核。為了讓我們的框架可行,我們得拼一把,把這個障礙突破了。但即使如此,我們還是不得不更改了對外介面了:
init_algorithm(algorithm, parameter);nacce_pin_memory(algorithm, buffer);nacce_compress(algorigthm, input_buffer, output_buffer);nacce_unpin_memory(algorithm, buffer);n
從這個例子中,我們就可以看到了:只要你開始進入細節,你就不能不引入破壞當初」最優設定「的循環。但有最初設定和沒有最初設定,我們眼中看到的設計是不同的,上面的4個函數,有兩個屬於最初設定,另兩個是我們的包袱。如果我們看到了包袱,而不覺得它是高大上的設計。我們就能隨時看到競爭對手的優勢,保證在面對各種挑戰和細節問題的時候,不會離開主航道。這裡其實還有不少細節可以摳,比如,有沒有必要讓acce_pin_memory帶algorithm參數?不帶這個參數,這個功能就可以獨立於加速器框架存在。帶這個參數,我們就有機會在提供演算法的硬體支持SVE的時候自動把這個操作變成空操作。這也是權衡。這也說明了,為什麼構架設計是必須的,這種參數,選和不選都是一種選,選了,後面的設計一旦加進來,就再也改不動了。你後面補工程師,增加演算法,增加加速引擎,增加調試功能,優化速度……所有這些演算法都補上來的時候,如果你沒有守的東西,你的構架就成了一個大雜燴,那時你想改什麼都是句廢話了。
解決了數據傳遞問題,硬體數量不足問題也就好解決了:既然我們有highlevel_acce_compress,我們完全可以在這裡封裝這個差異,當申請不到設備的時候,我們退化為CPU演算法就好了。最壞最壞,我們也不會比現有方案差,只要保證硬體處理過程是有競爭力的,這個框架就可以生存下去了。
從這個過程我們可以看到,構架設計的過程,就好像修補一張網。一開始你有滿滿的自由度,只是找到幾個固定的點,然後你不斷填補邏輯,越到後面,你的餘地就越少。等到你幾乎沒有餘地了,這個時候就是你的系統最強的時候(因為所有的邏輯都修補完了),也是它最弱的時候(因為已經沒有面對變化的能力了)。所以,如果你可以看得長遠,其實構架設計一開始就沒有什麼餘地,因為在特定的用戶需求之下,最優設計也就那麼幾種。
推薦閱讀:
TAG:软件架构 |