在如今這個Lean/Agile橫掃一切的年代,設(shè)計似乎有了被邊緣化的傾向,做事的周期如此之快,似乎已容不下人們更多的思考。MVP(Minimal Viable Produce)在很多團(tuán)隊里演化成一個形而上的圖騰,于是工程師們找到了一個完美的借口:我先做個MVP,設(shè)計的事,以后再說。
如果純屬個人玩票,有個點子,hack out還說得過去;但要嚴(yán)肅做一個項目,還是要下工夫設(shè)計一番,否則,沒完沒了的返工會讓你無語淚千行。
<h1>設(shè)計首先得搞懂要解決的問題</h1>
工程師大多都是很聰明的人,聰明人有個最大的問題就是自負(fù)。很多人拿到一個需求,還沒太搞明白其外延和內(nèi)涵,代碼就已經(jīng)在腦袋里流轉(zhuǎn)。這樣做出來的系統(tǒng),縱使再精妙,也免不了承受因需求理解不明確而導(dǎo)致的返工之苦。
搞懂需求這事,說起來簡單,做起來難。需求有正確的但表達(dá)錯誤的需求,有正確的但沒表達(dá)出來的需求,還有過度表達(dá)的需求。所以,拿到需求后,先不忙尋找解決方案,多問問自己,工作伙伴,客戶follow up questions來澄清需求模糊不清之處。
搞懂需求,還需要了解需求對應(yīng)的產(chǎn)品,公司,以及(潛在)競爭對手的現(xiàn)狀,需求的上下文,以及需求的約束條件。人有二知二不知:
I know that I know
I know that I don’t know
I don’t know that I know
I don’t know that I don’t know
澄清需求的過程,就是不斷驅(qū)逐無知,掌握現(xiàn)狀,上下文和約束條件的過程。
這個主題講起來很大,且非常重要,但畢竟不是本文的重點,所以就此帶過。
<h1>尋找(多個)解決方案</h1>
如果對問題已經(jīng)有不錯的把握,接下來就是解決方案的發(fā)現(xiàn)之旅。這是個考察big picture的活計。同樣是滿足孩子想要個汽車的愿望,你可以:
去玩具店里買一個現(xiàn)成的
買樂高積木,然后組裝
用紙糊一個,或者找塊木頭,刻一個
這對應(yīng)軟件工程問題的幾種解決之道:
購買現(xiàn)成軟件(acuquire or licensing),二次開發(fā)之(如果需要)
尋找building blocks,組裝之(glue)
自己開發(fā)(build from scratch, or DIY)
大部分時候,如果a或b的TCO [1] 合理,那就不要選擇c。做一個產(chǎn)品的目的是為客戶提供某種服務(wù),而不是證明自己能一行行碼出出來這個產(chǎn)品。
a是個很重要的點,可惜大部分工程師腦袋里沒有錢的概念,或者出于job security的私心,而忽略了。工程師現(xiàn)在越來越貴,能用合理的價格搞定的功能,就不該雇人去打理(自己打臉)。一個產(chǎn)品,最核心的部分不超過整個系統(tǒng)的20%,把人力資源鋪在核心的部分,才是軟件設(shè)計之道。
b我們稍后再講。
對工程師而言,DIY出一個功能是個極大的誘惑。一種DIY是源自工程師的不滿。任何開源軟件,在處理某種特定業(yè)務(wù)邏輯的時候總會有一些不足,眼里如果把這些不足放在,卻忽略了人家的好處,是大大的不妥。前兩天我聽到有人說 "consul sucks, …, I’ll build our own service discovery framework…",我就苦笑。我相信他能做出來一個簡單的service discovery tool,這不是件特別困難的事情。問題是值不值得去做。如果連處于consul這個層次的基礎(chǔ)組件都要自己去做,那要么是心太大,要么是沒有定義好自己的軟件系統(tǒng)的核心價值(除非系統(tǒng)的核心價值就在于此)。代碼一旦寫出來,無論是5000行還是50行,都是需要有人去維護(hù)的,在系統(tǒng)的生命周期里,每一行自己寫的代碼都是一筆債務(wù),需要定期不定期地償還利息。
另外一種DIY是出于工程師的無知。「無知者無畏」在某些場合的效果是正向的,有利于打破陳規(guī)。但在軟件開發(fā)上,還是知識和眼界越豐富越開闊越好。一個無知的工程師在面對某個問題時(比如說service discovery),如果不知道這問題也許有現(xiàn)成的解決方案(consul),自己鉚足了勁寫一個,大半會有失偏頗(比如說沒做上游服務(wù)的health check,或者自己本身的high availability),結(jié)果bug不斷,辛辛苦苦一個個都啃下來,才發(fā)現(xiàn),自己走了很多彎路,費了大半天勁,做了某個開源軟件的功能的子集。當(dāng)然,對工程師而言,這個練手的價值還是很大的,但對公司來說,這是一筆沉重的無意義的支出。
眼界定義了一個人的高度,如果你每天見同類的人,看同質(zhì)的書籍/視頻,(讀)寫隸屬同一domain的代碼,那多半眼界不夠開闊?;ヂ?lián)網(wǎng)的發(fā)展一日千里,變化太快,如果把自己禁錮在一方小天地里,很容易成為陶淵明筆下的桃花源中人:乃不知有漢,無論魏晉。
<h1>構(gòu)建靈活且有韌性的系統(tǒng)</h1>
如果說之前說的都是廢話,那么接下來的和真正的軟件設(shè)計能扯上些關(guān)系。
<h1>分解和組合</h1>
軟件設(shè)計是一個把大的問題不斷分解,直至原子級的小問題,然后再不斷組合的過程。這一點可以類比生物學(xué):原子(keyword/macro)組合成分子(function),分子組合成細(xì)胞(module/class),細(xì)胞組合成組織(micro service),組織組合成器官(service),進(jìn)而組合成生物(system)。
一個如此組合而成系統(tǒng),是滿足關(guān)注點分離(Separation of Concerns)的。大到一個器官,小到一個細(xì)胞,都各司其職,把自己要做的事情做到極致。心臟不必關(guān)心腎臟會干什么,它只需要做好自己的事情:把新鮮血液通過動脈排出,再把各個器官用過的血液從靜脈回收。
分解和組合在軟件設(shè)計中的作用如此重要,以至于一個系統(tǒng)如果合理分解,那么日后維護(hù)的代價就要小得多。同樣講關(guān)注點分離,不同的工程師,分離的方式可能完全不同。但究其根本,還有有一些規(guī)律可循。
<h1>總線(System Bus)</h1>
首先我們要把系統(tǒng)的總線定義出來。人體的總線,大的有幾條:血管(動脈,靜脈),神經(jīng)網(wǎng)絡(luò),氣管,輸尿管。它們有的完全負(fù)責(zé)與外界的交互(氣管,輸尿管),有的完全是內(nèi)部的信息中樞(血管),有的內(nèi)外兼修(神經(jīng)網(wǎng)絡(luò))。
總線把生產(chǎn)者和消費者分離,讓彼此互不依賴。心臟往外供血時,把血壓入動脈血管就是了。它并不需要知道誰是接收者。
同樣的,回到我們熟悉的計算機系統(tǒng),CPU訪問內(nèi)存也是如此:它發(fā)送一條消息給總線,總線通知RAM讀取數(shù)據(jù),然后RAM把數(shù)據(jù)返回給總線,CPU再獲取之。整個過程中CPU只知道一個內(nèi)存地址,毋須知道訪問的具體是哪個內(nèi)存槽的哪塊內(nèi)存 <h1>—— 總線將二者屏蔽開。</h1>
學(xué)過計算機系統(tǒng)的同學(xué)應(yīng)該都知道,經(jīng)典的PC結(jié)構(gòu)有幾種總線:數(shù)據(jù)總線,地址總線,控制總線,擴展總線等;做過網(wǎng)絡(luò)設(shè)備的同學(xué)也都知道,一個經(jīng)典的網(wǎng)絡(luò)設(shè)備,其軟件系統(tǒng)的總線分為:control plane和data plane。
<h1>路由(routing)</h1>
有了總線的概念,接下來必然要有路由。我們看人體的血管:
每一處分叉,就涉及到一次路由。
路由分為外部路由和內(nèi)部路由。外部路由處理輸入,把不同的輸入dispatch到系統(tǒng)里不同的組件。做web app的,可能沒有意識到,但其實每個web framework,最關(guān)鍵的組件之一就是url dispatch。HTTP的偉大之處就是每個request,都能通過url被dispatch到不同的handler處理。而url是目錄式的,可以層層演進(jìn) —— 就像分形幾何,一個大的系統(tǒng),通過不斷重復(fù)的模式,組合起來 —— 非常利于系統(tǒng)的擴展。遺憾的是,我們自己做系統(tǒng),對于輸入既沒有總線的考量,又無路由的概念,if-else下去,久而久之,代碼便繞成了意大利面條。
再舉一例:DOM中的event bubble,在javascript處理起來已然隱含著路由的概念。你只需定義當(dāng)某個事件(如onclick)發(fā)生時的callback函數(shù)就好,至于這事件怎么通過eventloop抵達(dá)回調(diào)函數(shù),無需關(guān)心。好的路由系統(tǒng)剝繭抽絲,把繁雜的信息流正確送到處理者手中。
外部路由總還有「底層」為我們完成,內(nèi)部路由則需工程師考慮。service級別的路由(數(shù)據(jù)流由哪個service處理)可以用consul等service discovery組件,service內(nèi)部的路由(數(shù)據(jù)流到達(dá)后怎么處理)則需要自己完成。路由的具體方式有很多種,pattern matching最為常見。
無論用何種方式路由,數(shù)據(jù)抵達(dá)總線前為其定義Identity(ID)非常重要,你可以管這個過程叫data normalization,data encapsulation等,總之,一個消息能被路由,需要有個用于路由的ID。這ID可以是url,可以是一個message header,也可以是一個label(想象MPLS的情況)。當(dāng)我們?yōu)閿?shù)據(jù)賦予一個個合理的ID后,如何路由便清晰可見。
<h1>隊列(Queue)</h1>
對于那些并非需要立即處理的數(shù)據(jù),可以使用隊列。隊列也有把生產(chǎn)者和消費者分離的功效。隊列有:
single producer single consumer(SPSC)
single producer multiple consumers(SPMC)
multiple producers single consumer(MPSC)
multiple producers multiple consumers(MPMC)
仔細(xì)想想,隊列其實就是總線+路由(可選)+存儲的一個特殊版本。一般而言,system bus之上是系統(tǒng)的各個service,每個service再用service bus(或者queue)把micro service chain起來,然后每個micro service內(nèi)部的組件間,再用queue連接起來。
有了隊列,有利于提高流水線的效率。一般而言,流水線的處理速度取決于最慢的組件。隊列的存在,讓慢速組件有機會運行多份,來彌補生產(chǎn)者和消費者速度上的差距。
<h1>Pub/Sub</h1>
存儲在隊列中的數(shù)據(jù),除路由外,還有一種處理方式:pub/sub。和路由相似,pub/sub將生產(chǎn)者和消費者分離;但二者不同之處在于,路由的目的地由路由表中的表項控制,而pub/sub一般由publisher控制 [2]:任何subscribe某個數(shù)據(jù)的consumer,都會到publisher處注冊,publisher由此可以定向發(fā)送消息。
<h1>協(xié)議(protocol)</h1>
一旦我們把系統(tǒng)分解成一個個service,service再分解成micro service,彼此之間互不依賴,僅僅通過總線或者隊列來通訊,那么,我們就需要協(xié)議來定義彼此的行為。協(xié)議聽起來很高大上,其實不然。我們寫下的每個function(或者每個class),其實就是在定義一個不成文的協(xié)議:function的arity是什么,接受什么參數(shù),返回什么結(jié)果。調(diào)用者需嚴(yán)格按照協(xié)議調(diào)用方能得到正確的結(jié)果。
service級別的協(xié)議是一份SLA:服務(wù)的endpoint是什么,版本是什么,接收什么格式的消息,返回什么格式的消息,消息在何種網(wǎng)絡(luò)協(xié)議上承載,需要什么樣的authorization,可以正常服務(wù)的最大吞吐量(throughput)是什么,在什么情況下會觸發(fā)throttling等等。
頭腦中有了總線,路由,隊列,協(xié)議等這些在computer science 101中介紹的基礎(chǔ)概念,系統(tǒng)的分解便有跡可尋:面對一個系統(tǒng)的設(shè)計,你要做的不再是一道作文題,而是一道填空題:在若干條system bus里填上其名稱和流進(jìn)流出的數(shù)據(jù),在system bus之上的一個個方框里填上服務(wù)的名稱和服務(wù)的功能。然后,每個服務(wù)再以此類推,直到感覺毋須再細(xì)化為止。
組成系統(tǒng)的必要服務(wù)
有些管理性質(zhì)的服務(wù),盡管和業(yè)務(wù)邏輯直接關(guān)系不大,但無論是任何系統(tǒng),都需要考慮構(gòu)建,這里羅列一二。
<h1>代謝(sweeping)</h1>
一個活著的生物時時刻刻都進(jìn)行著新陳代謝:每時每刻新的細(xì)胞取代老的細(xì)胞,同時身體中的「垃圾」通過排泄系統(tǒng)排出體外。一個運轉(zhuǎn)有序的城市也有新陳代謝:下水道,垃圾場,污水處理等維持城市的正常功能。沒有了代謝功能,生物會凋零,城市會荒蕪。
軟件系統(tǒng)也是如此。日志會把硬盤寫滿,軟件會失常,硬件會失效,網(wǎng)絡(luò)會擁塞等等。一個好的軟件系統(tǒng)需要一個好的代謝系統(tǒng):出現(xiàn)異常的服務(wù)會被關(guān)閉,同樣的服務(wù)會被重新啟動,恢復(fù)運行。
代謝系統(tǒng)可以參考erlang的supervisor/child process結(jié)構(gòu),以及supervision tree。很多軟件,都運行在簡單的supervision tree模式下,如nginx。
<h1>高可用性(HA)</h1>
每個人都有兩個腎。為了apple watch賣掉一個腎,另一個還能保證人體的正常工作。當(dāng)然,人的兩個腎是Active-Active工作模式,內(nèi)部的腎元(micro service)是 N(active)+M(backup) clustering 工作的(看看人家這service的做的),少了一個,performance會一點點有折扣,但可以忽略不計。
大部分軟件系統(tǒng)里的各種服務(wù)也需要高可用性:除非完全無狀態(tài)的服務(wù),且服務(wù)重啟時間在ms級。服務(wù)的高可用性和路由是息息相關(guān)的:高可用性往往意味著同一服務(wù)的冗余,同時也意味著負(fù)載分擔(dān)。好的路由系統(tǒng)(如consul)能夠?qū)β酚芍镣环?wù)的數(shù)據(jù)在多個冗余服務(wù)間進(jìn)行負(fù)載分擔(dān),同時在檢測出某個失效服務(wù)后,將數(shù)據(jù)路只由至正常運作的服務(wù)。
高可用性還意味著非關(guān)鍵服務(wù),即便不可恢復(fù),也只會導(dǎo)致系統(tǒng)降級,而不會讓整個系統(tǒng)無法訪問。就像壁虎的尾巴斷了不妨礙壁虎逃命,人摔傷了手臂還能吃飯一樣,一個軟件系統(tǒng)里統(tǒng)計模塊的異常不該讓用戶無法訪問他的個人頁面。
<h1>安保(security)</h1>
安保服務(wù)分為主動安全和被動安全。authentication/authorization + TLS + 敏感信息加密 + 最小化輸入輸出接口可以算是主動安全,防火墻等安防系統(tǒng)則是被動安全。
繼續(xù)拿你的腎來比擬 —— 腎臟起碼有兩大安全系統(tǒng):
輸入安全。腎器的厚厚的器官膜,保護(hù)器官的輸入輸出安全 —— 主要的輸入輸出只能是腎動脈,腎靜脈和輸尿管。
環(huán)境安全。腎器里有大量脂肪填充,避免在撞擊時對核心功能的損傷。
除此之外,人體還提供了包括免疫系統(tǒng),皮膚,骨骼,空腔等一系列安全系統(tǒng),從各個維度最大程度保護(hù)一個器官的正常運作。如果我們仔細(xì)研究生物,就會發(fā)現(xiàn),安保是個一攬子解決方案:小到細(xì)胞,大到整個人體,都有各自的安全措施。一個軟件系統(tǒng)也需如此考慮系統(tǒng)中各個層次的安全。
<h1>透支保護(hù)(overdraft protection)</h1>
任何系統(tǒng),任何服務(wù)都是有服務(wù)能力的 —— 當(dāng)這能力被透支時,需要一定的應(yīng)急計劃。如果使用擁有auto scaling的云服務(wù)(如AWS),動態(tài)擴容是最好的解決之道,但受限于所用的解決方案,它并非萬靈藥,AWS的auto scaling依賴于load balancer,如Amazon自有的ELB,或者第三方的HAProxy,但ELB對某些業(yè)務(wù),如websocket,支持不佳;而第三方的load balancer,則需要考慮部署,與Amazon的auto scaling結(jié)合(需要寫點代碼),避免單點故障,保證自身的capacity等一堆頭疼事。
在無法auto scaling的場景最通用的做法是back pressure,把壓力反饋到源頭。就好像你不斷熬夜,最后大腦受不了,逼著你睡覺一樣。還有一種做法是服務(wù)降級,停掉非核心的service/micro-service,如analytical service,ad service,保證核心功能正常。
<h1>把設(shè)計的成果講給別人聽</h1>
完成了分解和組合,也嚴(yán)肅對待了諸多與業(yè)務(wù)沒有直接關(guān)系,但又不得不做的必要功能后,接下來就是要把設(shè)計在白板上畫下來,講給任何一個利益相關(guān)者聽。聽他們的反饋。設(shè)計不是一個閉門造車的過程,全程都需要和各種利益相關(guān)者交流。然而,很多人都忽視了設(shè)計定型后,繼續(xù)和外界交流的必要性。很多人會認(rèn)為:我的軟件架構(gòu),設(shè)計結(jié)果和工程有關(guān),為何要講給工程師以外的人聽?他們懂么?
其實pitch本身就是自我學(xué)習(xí)和自我修正的一部分。當(dāng)著一個人或者幾個人的面,在白板上畫下腦海中的設(shè)計的那一刻,你就會有直覺哪個地方似乎有問題,這是很奇特的一種體驗:你自己畫給自己看并不會產(chǎn)生這種直覺。這大概是面對公眾的焦灼產(chǎn)生的腎上腺素的效果。:)
此外,從聽者的表情,或者他們提的聽起來很傻很天真的問題,你會進(jìn)一步知道哪些地方你以為你搞通了,其實自己是一知半解。太簡單,太基礎(chǔ)的問題,我們take it for granted,不屑去問自己,非要有人點出,自己才發(fā)現(xiàn):啊,原來這里我也不懂哈。這就是破解 "you don’t know what you don’t know" 之法。
記得看過一個video,主講人大談企業(yè)文化,有個哥們傻乎乎發(fā)問:so what it culture literally? 主講人愣了一下,拖拖拉拉講了一堆自己都不能讓自己信服的廢話。估計回頭他就去查韋氏詞典了。
最后,總有人在某些領(lǐng)域的知識更豐富一些,他們會告訴你你一些你知道自己不懂的事情。填補了 "you know that you don’t know" 的空缺。
設(shè)計時的tradeoff
Rich hickey(clojure作者)在某個演講中說:
everyone says design is about tradeoffs, but you need to enumerate at least two or more possible solutions, and the attributes and deficits of each, in order to make tradeoff.
所以,下回再腆著臉說:偶做了些tradeoff,先確保自己做足了功課再說。
<h1>設(shè)計的改變不可避免</h1>
設(shè)計不是一錘子買賣,改變不可避免。我之前的一個老板,喜歡把:change is your friend 掛在口頭。軟件開發(fā)的整個生命周期,變更是家常便飯,以至于變更管理都生出一門學(xué)問。軟件的設(shè)計期更是如此。人總會犯錯,設(shè)計總有缺陷,需求總會變化,老板總會指手畫腳,PM總有一天會亮出獠牙,不再是貼心大哥,或者美萌小妹。。。所以,據(jù)理力爭,然后接受必要的改變即可。連凱恩斯他老人家都說: