天天看點

軟體設計雜談

軟體設計雜談

2015-04-17程式人生 程式人生

程式人生

功能介紹 十年漫漫程式人生,打過各種雜,也做過讓我驕傲的軟體;管理過十多人的團隊,還帶領一班兄弟姐妹創過業。關注程式人生,了解程式猿,學做程式猿,讓我們的人生不再屌絲化。

disclaimer: 本文所講的設計,非UI/UE的設計,單單指軟體代碼/功能本身在技術上的設計。UI/UE的主題請出門右轉找特贊(Tezign)。:)

在如今這個Lean/Agile橫掃一切的年代,設計似乎有了被邊緣化的傾向,做事的周期如此之快,似乎已容不下人們更多的思考。MVP(Minimal Viable Produce)在很多團隊裡演化成一個形而上的圖騰,于是工程師們找到了一個完美的借口:我先做個MVP,設計的事,以後再說。

如果純屬個人玩票,有個點子,hack out還說得過去;但要嚴肅做一個項目,還是要下工夫設計一番,否則,沒完沒了的返工會讓你無語淚千行。

工程師大多都是很聰明的人,聰明人有個最大的問題就是自負。很多人拿到一個需求,還沒太搞明白其外延和内涵,代碼就已經在腦袋裡流轉。這樣做出來的系統,縱使再精妙,也免不了承受因需求了解不明确而導緻的返工之苦。

搞懂需求這事,說起來簡單,做起來難。需求有正确的但表達錯誤的需求,有正确的但沒表達出來的需求,還有過度表達的需求。是以,拿到需求後,先不忙尋找解決方案,多問問自己,工作夥伴,客戶follow up questions來澄清需求模糊不清之處。

搞懂需求,還需要了解需求對應的産品,公司,以及(潛在)競争對手的現狀,需求的上下文,以及需求的限制條件。人有二知二不知:

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

澄清需求的過程,就是不斷驅逐無知,掌握現狀,上下文和限制條件的過程。

這個主題講起來很大,且非常重要,但畢竟不是本文的重點,是以就此帶過。

如果對問題已經有不錯的把握,接下來就是解決方案的發現之旅。這是個考察big picture的活計。同樣是滿足孩子想要個汽車的願望,你可以:

去玩具店裡買一個現成的

買樂高積木,然後組裝

用紙糊一個,或者找塊木頭,刻一個

這對應軟體工程問題的幾種解決之道:

購買現成軟體(acuquire or licensing),二次開發之(如果需要)

尋找building blocks,組裝之(glue)

自己開發(build from scratch, or DIY)

大部分時候,如果a或b的TCO [1] 合理,那就不要選擇c。做一個産品的目的是為客戶提供某種服務,而不是證明自己能一行行碼出出來這個産品。

a是個很重要的點,可惜大部分工程師腦袋裡沒有錢的概念,或者出于job security的私心,而忽略了。工程師現在越來越貴,能用合理的價格搞定的功能,就不該雇人去打理(自己打臉)。一個産品,最核心的部分不超過整個系統的20%,把人力資源鋪在核心的部分,才是軟體設計之道。

b我們稍後再講。

對工程師而言,DIY出一個功能是個極大的誘惑。一種DIY是源自工程師的不滿。任何開源軟體,在處理某種特定業務邏輯的時候總會有一些不足,眼裡如果把這些不足放在,卻忽略了人家的好處,是大大的不妥。前兩天我聽到有人說 "consul sucks, …, I’ll build our own service discovery framework…",我就苦笑。我相信他能做出來一個簡單的service discovery tool,這不是件特别困難的事情。問題是值不值得去做。如果連處于consul這個層次的基礎元件都要自己去做,那要麼是心太大,要麼是沒有定義好自己的軟體系統的核心價值(除非系統的核心價值就在于此)。代碼一旦寫出來,無論是5000行還是50行,都是需要有人去維護的,在系統的生命周期裡,每一行自己寫的代碼都是一筆債務,需要定期不定期地償還利息。

另外一種DIY是出于工程師的無知。「無知者無畏」在某些場合的效果是正向的,有利于打破陳規。但在軟體開發上,還是知識和眼界越豐富越開闊越好。一個無知的工程師在面對某個問題時(比如說service discovery),如果不知道這問題也許有現成的解決方案(consul),自己鉚足了勁寫一個,大半會有失偏頗(比如說沒做上遊服務的health check,或者自己本身的high availability),結果bug不斷,辛辛苦苦一個個都啃下來,才發現,自己走了很多彎路,費了大半天勁,做了某個開源軟體的功能的子集。當然,對工程師而言,這個練手的價值還是很大的,但對公司來說,這是一筆沉重的無意義的支出。

眼界定義了一個人的高度,如果你每天見同類的人,看同質的書籍/視訊,(讀)寫隸屬同一domain的代碼,那多半眼界不夠開闊。網際網路的發展一日千裡,變化太快,如果把自己禁锢在一方小天地裡,很容易成為陶淵明筆下的桃花源中人:乃不知有漢,無論魏晉。

如果說之前說的都是廢話,那麼接下來的和真正的軟體設計能扯上些關系。

軟體設計是一個把大的問題不斷分解,直至原子級的小問題,然後再不斷組合的過程。這一點可以類比生物學:原子(keyword/macro)組合成分子(function),分子組合成細胞(module/class),細胞組合成組織(micro service),組織組合成器官(service),進而組合成生物(system)。

一個如此組合而成系統,是滿足關注點分離(Separation of Concerns)的。大到一個器官,小到一個細胞,都各司其職,把自己要做的事情做到極緻。心髒不必關心腎髒會幹什麼,它隻需要做好自己的事情:把新鮮血液通過動脈排出,再把各個器官用過的血液從靜脈回收。

分解群組合在軟體設計中的作用如此重要,以至于一個系統如果合理分解,那麼日後維護的代價就要小得多。同樣講關注點分離,不同的工程師,分離的方式可能完全不同。但究其根本,還有有一些規律可循。

首先我們要把系統的總線定義出來。人體的總線,大的有幾條:血管(動脈,靜脈),神經網絡,氣管,輸尿管。它們有的完全負責與外界的互動(氣管,輸尿管),有的完全是内部的資訊中樞(血管),有的内外兼修(神經網絡)。

總線把生産者和消費者分離,讓彼此互不依賴。心髒往外供血時,把血壓入動脈血管就是了。它并不需要知道誰是接收者。

同樣的,回到我們熟悉的計算機系統,CPU通路記憶體也是如此:它發送一條消息給總線,總線通知RAM讀取資料,然後RAM把資料傳回給總線,CPU再擷取之。整個過程中CPU隻知道一個記憶體位址,毋須知道通路的具體是哪個記憶體槽的哪塊記憶體 —— 總線将二者屏蔽開。

學過計算機系統的同學應該都知道,經典的PC結構有幾種總線:資料總線,位址總線,控制總線,擴充總線等;做過網絡裝置的同學也都知道,一個經典的網絡裝置,其軟體系統的總線分為:control plane和data plane。

有了總線的概念,接下來必然要有路由。我們看人體的血管:

每一處分叉,就涉及到一次路由。

路由分為外部路由和内部路由。外部路由處理輸入,把不同的輸入dispatch到系統裡不同的元件。做web app的,可能沒有意識到,但其實每個web framework,最關鍵的元件之一就是url dispatch。HTTP的偉大之處就是每個request,都能通過url被dispatch到不同的handler處理。而url是目錄式的,可以層層演進 —— 就像分形幾何,一個大的系統,通過不斷重複的模式,組合起來 —— 非常利于系統的擴充。遺憾的是,我們自己做系統,對于輸入既沒有總線的考量,又無路由的概念,if-else下去,久而久之,代碼便繞成了意大利面條。

再舉一例:DOM中的event bubble,在javascript處理起來已然隐含着路由的概念。你隻需定義當某個事件(如onclick)發生時的callback函數就好,至于這事件怎麼通過eventloop抵達回調函數,無需關心。好的路由系統剝繭抽絲,把繁雜的資訊流正确送到處理者手中。

外部路由總還有「底層」為我們完成,内部路由則需工程師考慮。service級别的路由(資料流由哪個service處理)可以用consul等service discovery元件,service内部的路由(資料流到達後怎麼處理)則需要自己完成。路由的具體方式有很多種,pattern matching最為常見。

無論用何種方式路由,資料抵達總線前為其定義Identity(ID)非常重要,你可以管這個過程叫data normalization,data encapsulation等,總之,一個消息能被路由,需要有個用于路由的ID。這ID可以是url,可以是一個message header,也可以是一個label(想象MPLS的情況)。當我們為資料賦予一個個合理的ID後,如何路由便清晰可見。

對于那些并非需要立即處理的資料,可以使用隊列。隊列也有把生産者和消費者分離的功效。隊列有:

single producer single consumer(SPSC)

single producer multiple consumers(SPMC)

multiple producers single consumer(MPSC)

multiple producers multiple consumers(MPMC)

仔細想想,隊列其實就是總線+路由(可選)+存儲的一個特殊版本。一般而言,system bus之上是系統的各個service,每個service再用service bus(或者queue)把micro service chain起來,然後每個micro service内部的元件間,再用queue連接配接起來。

有了隊列,有利于提高流水線的效率。一般而言,流水線的處理速度取決于最慢的元件。隊列的存在,讓慢速元件有機會運作多份,來彌補生産者和消費者速度上的差距。

存儲在隊列中的資料,除路由外,還有一種處理方式:pub/sub。和路由相似,pub/sub将生産者和消費者分離;但二者不同之處在于,路由的目的地由路由表中的表項控制,而pub/sub一般由publisher控制[2]:任何subscribe某個資料的consumer,都會到publisher處注冊,publisher由此可以定向發送消息。

一旦我們把系統分解成一個個service,service再分解成micro service,彼此之間互不依賴,僅僅通過總線或者隊列來通訊,那麼,我們就需要協定來定義彼此的行為。協定聽起來很高大上,其實不然。我們寫下的每個function(或者每個class),其實就是在定義一個不成文的協定:function的arity是什麼,接受什麼參數,傳回什麼結果。調用者需嚴格按照協定調用方能得到正确的結果。

service級别的協定是一份SLA:服務的endpoint是什麼,版本是什麼,接收什麼格式的消息,傳回什麼格式的消息,消息在何種網絡協定上承載,需要什麼樣的authorization,可以正常服務的最大吞吐量(throughput)是什麼,在什麼情況下會觸發throttling等等。

頭腦中有了總線,路由,隊列,協定等這些在computer science 101中介紹的基礎概念,系統的分解便有迹可尋:面對一個系統的設計,你要做的不再是一道作文題,而是一道填空題:在若幹條system bus裡填上其名稱和流進流出的資料,在system bus之上的一個個方框裡填上服務的名稱和服務的功能。然後,每個服務再以此類推,直到感覺毋須再細化為止。

有些管理性質的服務,盡管和業務邏輯直接關系不大,但無論是任何系統,都需要考慮建構,這裡羅列一二。

一個活着的生物時時刻刻都進行着新陳代謝:每時每刻新的細胞取代老的細胞,同時身體中的「垃圾」通過排洩系統排出體外。一個運轉有序的城市也有新陳代謝:下水道,垃圾場,污水處理等維持城市的正常功能。沒有了代謝功能,生物會凋零,城市會荒蕪。

軟體系統也是如此。日志會把硬碟寫滿,軟體會失常,硬體會失效,網絡會擁塞等等。一個好的軟體系統需要一個好的代謝系統:出現異常的服務會被關閉,同樣的服務會被重新啟動,恢複運作。

代謝系統可以參考erlang的supervisor/child process結構,以及supervision tree。很多軟體,都運作在簡單的supervision tree模式下,如nginx。

每個人都有兩個腎。為了apple watch賣掉一個腎,另一個還能保證人體的正常工作。當然,人的兩個腎是Active-Active工作模式,内部的腎元(micro service)是 N(active)+M(backup) clustering 工作的(看看人家這service的做的),少了一個,performance會一點點有折扣,但可以忽略不計。

大部分軟體系統裡的各種服務也需要高可用性:除非完全無狀态的服務,且服務重新開機時間在ms級。服務的高可用性和路由是息息相關的:高可用性往往意味着同一服務的備援,同時也意味着負載分擔。好的路由系統(如consul)能夠對路由至同一服務的資料在多個備援服務間進行負載分擔,同時在檢測出某個失效服務後,将資料路隻由至正常運作的服務。

高可用性還意味着非關鍵服務,即便不可恢複,也隻會導緻系統降級,而不會讓整個系統無法通路。就像壁虎的尾巴斷了不妨礙壁虎逃命,人摔傷了手臂還能吃飯一樣,一個軟體系統裡統計子產品的異常不該讓使用者無法通路他的個人頁面。

安保服務分為主動安全和被動安全。authentication/authorization + TLS + 敏感資訊加密 + 最小化輸入輸出接口可以算是主動安全,防火牆等安防系統則是被動安全。

繼續拿你的腎來比拟 —— 腎髒起碼有兩大安全系統:

輸入安全。腎器的厚厚的器官膜,保護器官的輸入輸出安全 —— 主要的輸入輸出隻能是腎動脈,腎靜脈和輸尿管。

環境安全。腎器裡有大量脂肪填充,避免在撞擊時對核心功能的損傷。

除此之外,人體還提供了包括免疫系統,皮膚,骨骼,空腔等一系列安全系統,從各個次元最大程度保護一個器官的正常運作。如果我們仔細研究所學生物,就會發現,安保是個一攬子解決方案:小到細胞,大到整個人體,都有各自的安全措施。一個軟體系統也需如此考慮系統中各個層次的安全。

任何系統,任何服務都是有服務能力的 —— 當這能力被透支時,需要一定的應急計劃。如果使用擁有auto scaling的雲服務(如AWS),動态擴容是最好的解決之道,但受限于所用的解決方案,它并非萬靈藥,AWS的auto scaling依賴于load balancer,如Amazon自有的ELB,或者第三方的HAProxy,但ELB對某些業務,如websocket,支援不佳;而第三方的load balancer,則需要考慮部署,與Amazon的auto scaling結合(需要寫點代碼),避免單點故障,保證自身的capacity等一堆頭疼事。

在無法auto scaling的場景最通用的做法是back pressure,把壓力回報到源頭。就好像你不斷熬夜,最後大腦受不了,逼着你睡覺一樣。還有一種做法是服務降級,停掉非核心的service/micro-service,如analytical service,ad service,保證核心功能正常。

完成了分解群組合,也嚴肅對待了諸多與業務沒有直接關系,但又不得不做的必要功能後,接下來就是要把設計在白闆上畫下來,講給任何一個利益相關者聽。聽他們的回報。設計不是一個閉門造車的過程,全程都需要和各種利益相關者交流。然而,很多人都忽視了設計定型後,繼續和外界交流的必要性。很多人會認為:我的軟體架構,設計結果和工程有關,為何要講給工程師以外的人聽?他們懂麼?

其實pitch本身就是自我學習和自我修正的一部分。當着一個人或者幾個人的面,在白闆上畫下腦海中的設計的那一刻,你就會有直覺哪個地方似乎有問題,這是很奇特的一種體驗:你自己畫給自己看并不會産生這種直覺。這大概是面對公衆的焦灼産生的腎上腺素的效果。:)

此外,從聽者的表情,或者他們提的聽起來很傻很天真的問題,你會進一步知道哪些地方你以為你搞通了,其實自己是一知半解。太簡單,太基礎的問題,我們take it for granted,不屑去問自己,非要有人點出,自己才發現:啊,原來這裡我也不懂哈。這就是破解 "you don’t know what you don’t know" 之法。

記得看過一個video,主講人大談企業文化,有個哥們傻乎乎發問:so what it culture literally? 主講人愣了一下,拖拖拉拉講了一堆自己都不能讓自己信服的廢話。估計回頭他就去查韋氏詞典了。

最後,總有人在某些領域的知識更豐富一些,他們會告訴你你一些你知道自己不懂的事情。填補了 "you know that you don’t know" 的空缺。

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,先確定自己做足了功課再說。

設計不是一錘子買賣,改變不可避免。我之前的一個老闆,喜歡把:change is your friend 挂在口頭。軟體開發的整個生命周期,變更是家常便飯,以至于變更管理都生出一門學問。軟體的設計期更是如此。人總會犯錯,設計總有缺陷,需求總會變化,老闆總會指手畫腳,PM總有一天會亮出獠牙,不再是貼心大哥,或者美萌小妹。。。是以,據理力争,然後接受必要的改變即可。連凱恩斯他老人家都說:

What do you do, sir?