1. 程式人生 > >[轉]我在系統設計上犯過的14個錯

[轉]我在系統設計上犯過的14個錯

轉自:http://hellojava.info/?p=458

在上篇《架構師畫像》的文章中提到了自己在系統設計上犯過的一些錯,覺得還挺有意義的,這篇文章就來回顧下自己近八年來所做的一些系統設計,看看犯的一些比較大的血淋淋的錯誤(很多都是推倒重來),這八年來主要做了三個基礎技術產品,三個橫跨三年的大的技術專案(其中有兩個還在進行中),發現大的錯誤基本集中在前面幾年,從這個點看起來能比較自豪的說在最近的幾年在系統設計的掌控上確實比以前成熟了很多。

第1個錯
在設計服務框架時,我期望服務框架對使用者完全不侵入,於是做了一個在外部放一個.xml檔案來描述spring裡的哪些bean釋出為服務的設計,這個版本釋出後,第一個小白鼠的使用者勉強在用,但覺得用的很彆扭,不過還是忍著用下去了,到了釋出的時候,發現出現了兩個問題,一是這個xml檔案研發也不知道放哪好,所以到了釋出的時候都不知道去哪拿這個xml檔案。
這個設計的關鍵錯誤就在於在設計時沒考慮過這個設計方式對研發階段、運維階段的影響,後來糾正這個錯誤的方法是去掉了這個xml檔案,改為寫了一個Spring FactoryBean,使用者在spring的bean配置檔案中配置下就可以。
因此對於一個架構師來說,設計時在全面性上要充分考慮。

第2個錯
服務框架在核心應用上線時,出現了前端web應用負載高,處理執行緒數不夠用的現象,當時處理這個故障的方式是回滾了服務框架的上線,這個故障排查了比較長的時間後,查到的原因是服務框架用的JBoss Remoting在通訊時預設時間是60s,導致一些處理速度慢的請求佔據了前端web應用的處理執行緒池。
上面這裡故障的原因簡單來說是分散式呼叫中超時時間太長的問題,但更深層次來思考,問題是犯在了設計服務框架時的技術選型,在選擇JBoss-Remoting時沒有充分的掌握它的執行細節,這個設計的錯誤導致的是後來決定放棄JBoss-Remoting,改為基於Mina重寫了服務框架的通訊部分,這裡造成了服務框架的可用版本釋出推遲了兩個多月。
因此對於一個架構師來說,在技術選型上對技術細節是要有很強的掌控力的。

第3個錯
在服務框架大概演進到第4個版本時,通訊協議上需要做一些改造,突然發現一個問題是以前的通訊協議上是沒有版本號的,於是悲催的只能在程式碼上做一個很齷蹉的處理來判斷是新版本還是老版本。
這個設計的錯誤非常明顯,這個其實只要在最早設計通訊協議時參考下現有的很多的通訊協議就可以避免了,因此這個錯誤糾正也非常簡單,就是參考一些經典的協議重新設計了下。
因此對於一個架構師來說,知識面的廣是非常重要的,或者是在設計時對未來有一定的考慮也是非常重要的。

說到協議,就順帶說下,當時在設計通訊協議和選擇序列化/反序列化上沒充分考慮到將來多語言的問題,導致了後來在多語言場景非常的被動,這也是由於設計時前瞻性的缺失,所謂的前瞻性不是說一定要一開始就把未來可能會出現的問題就解掉,而是應該留下不需要整個改造就可以解掉的方法,這點對於架構師來說也是非常重要的。

第4個錯
在服務框架切換為Mina的版本上線後,釋出服務的應用重啟時出現一個問題,就是發現重啟後集群中的機器負載嚴重不均,排查發現是由於這個版本採用是服務的呼叫方會通過硬體負載均衡去建立到服務釋出方的連線,而且是單個的長連線,由於是通過硬體負載均衡建連,意味著服務呼叫方其實看到的都是同一個地址,這也就導致了當服務釋出方重啟時,服務呼叫方重連就會集中的連到存活的機器上,連線還是長連,因此就導致了負載的不均衡現象。
這個設計的錯誤主要在於沒有考慮生產環境中走硬體負載均衡後,這種單個長連線方式帶來的問題,這個錯誤呢還真不太好糾正,當時臨時用的一個方法是服務呼叫方的連線每傳送了1w個請求後,就把連線自動斷開重建,最終的解決方法是去掉了負載均衡裝置這個中間點。
因此對於一個架構師來說,設計時的全面性要非常的好,我現在一般更多采用的方式是推演上線後的狀況,一般來說在腦海裡過一遍會比較容易考慮到這些問題。

第5個錯
服務框架在做了一年多以後,某個版本中出現了一個嚴重bug,然後我們就希望能通知到用了這個版本的應用緊急升級,在這個時候悲催的發現一個問題是我們壓根就不知道生產環境中哪些應用和機器部署了這個版本,當時只好用一個臨時的掃全網機器的方法來解決。
這個問題後來糾正的方法是在服務釋出和呼叫者在連線我們的一個點時,順帶把用的服務框架的版本號帶上,於是就可以很簡單的知道全網的服務框架目前在執行的版本號了。
因此對於一個架構師來說,設計時的全面性是非常重要的,推演能起到很大的幫助作用。

第6個錯
服務框架這種基礎型別的產品,在釋出時會碰到個很大的問題,就是需要通知到使用者去釋出,導致了整個釋出週期會相當的長,當時做了一個決定,投入資源去實現完全動態化的釋出,就是不需要重啟,等到做的時候才發現這完全就是個超級大坑,最終這件事在投入兩個人做了接近半年後,才終於決定放棄,而且最終來看其實升級的問題也沒那麼大。
這個問題最大的錯誤在於對細節把握不力,而且決策太慢。
因此對於一個架構師來說,技術細節的掌控非常重要,同時決策力也是非常重要的。

第7個錯
服務釋出方經常會碰到一個問題,就是一個服務裡的某些方法是比較耗資源的,另外的一些可能是不太耗資源,但對業務非常重要的方法,有些場景下會出現由於耗資源的方法被請求的多了些導致不太耗資源的方法受影響,這種場景下如果要去拆成多個服務,會導致開發階段還是挺痛苦的,因此服務框架這邊決定提供一個按方法做七層路由的功能,服務的釋出方可以在一個地方編寫一個規則檔案,這個規則檔案允許按照方法將生產環境的機器劃分為不同組,這樣當服務呼叫方呼叫時就可以做到不同方法呼叫到不同的機器。
這個功能對有些場景來說用的很爽,但隨著時間的演進和人員的更換,能維護那個檔案的人越來越少了,也成為了問題。
這個功能到現在為止我自己其實覺得也是一直處於爭議中,我也不知道到底是好還是不好…
因此對於一個架構師來說,設計時的全面性是非常重要的。

第8個錯
服務框架在用的越來越廣後,碰到了一個比較突出的問題,服務框架依賴的jar版本和應用依賴的jar版本衝突,服務框架作為一個通用技術產品,基本上沒辦法為了一個應用改變服務框架自己依賴的jar版本,這個問題到底怎麼去解,當時思考了比較久。
可能是由於我以前OSGi這塊背景的原因,在設計上我做了一個決定,引入OSGi,將服務框架的一堆jar處於一個獨立的classloader,和應用本身的分開,這樣就可以避免掉jar衝突的問題,在我做了引入OSGi這個決定後,團隊的1個資深的同學就去做了,結果是折騰了近兩個月整個匹配OSGi的maven開發環境都沒完全搭好,後來我自己決定進去搞這件事,即使是我對OSGi比較熟,也折騰了差不多1個多月才把整個開發的環境,工程的結構,以及之前的程式碼基本遷移為OSGi結構,這件事當時折騰好上線後,效果看起來是不錯的,達到了預期。
但這件事後來隨著加入服務框架的新的研發人員越來越多,發現多數的新人都在學習OSGi模式的開發這件事上投入了不少的時間,就是比較難適應,所以後來有其他業務問是不是要引入OSGi的時候,我基本都會建議不要引入,主要的原因是OSGi模式對大家熟悉的開發模式、排查問題的衝擊,除非是明確需要classloader隔離、動態化這兩個點。
讓我重新做一個決策的話,我會去掉對OSGi的引入,自己做一個簡單的classloader隔離策略來解決jar版本衝突的問題,保持大家都很熟悉的開發模式。
因此對於一個架構師來說,設計時的全面性是非常重要的。

第9個錯
服務框架在用的非常廣了後,團隊經常會被一個問題困擾和折騰,就是業務經常會碰到呼叫服務出錯或超時的現象,這種情況通常會讓服務框架這邊的研發來幫助排查,這個現象之所以查起來會比較複雜,是因為服務呼叫通常是多層的關係,並不是簡單的A–>B的問題,很多時候都會出現A–>B–>C–>D或者更多層的呼叫,超時或者出錯都有可能是在其中某個環節,因此排查起來非常麻煩。
在這個問題越來越麻煩後,這個時候才想起在09年左右團隊裡有同學看過G家的一篇叫dapper的論文,並且做了一個類似的東西,只是當時上線後我們一直想不明白這東西拿來做什麼,到了排查問題這個暴露的越來越嚴重後,終於逐漸想起這東西貌似可以對排查問題會產生很大的幫助。
到了這個階段才開始做這件事後,碰到的主要不是技術問題,而是怎麼把新版本升級上去的問題,這個折騰了挺長時間,然後上線後又發現了一個新的問題是,即使服務框架具備了Trace能力,但服務裡又會調外部的例如資料庫、快取等,那些地方如果有問題也會看不到,排查起來還是麻煩,於是這件事要真正展現效果就必須讓Trace完全貫穿所有系統,為了做成這件事,N個團隊付出了好幾年的代價。
因此對於一個架構師來說,設計時的全面性、前瞻性非常重要,例如Trace這個的重要性,如果在最初就考慮到,那麼在一開始就可以留好口子埋好伏筆,後面再要做完整就不會太複雜。

第10個錯
服務的釋出方有些時候會碰到一個現象是,服務還沒完全ready,就被呼叫了;還有第二個現象是服務釋出方出現問題時,要保留現場排查問題,但服務又一直在被呼叫,這種情況下就沒有辦法很好的完全保留現場來慢慢排查問題了。
這兩個現象會出現的原因是服務框架的設計是通過啟動後和某個中心建立連線,心跳成功後其他呼叫方就可以呼叫到,心跳失敗後就不會被調到,這樣看起來很自動化,但事實上會導致的另外一個問題是外部控制上下線這件事的能力就很弱。
這個設計的錯誤主要還是在設計時考慮的不夠全面。
因此對於一個架構師來說,設計時的全面性非常重要。

第11個錯
在某年我和幾個小夥伴決定改變當時用xen的模式,換成用一種輕量級的“虛擬機器”方式來做,從而提升單機跑的應用數量的密度,在做這件事時,我們決定自己做一個輕量級的類虛擬機器的方案,當時決定的做法是在一個機器上直接跑程序,然後碰到一堆的問題,例如從運維體系上來講,希望ssh到“機器”、獨立的ip、看到自己的系統指標等等,為了解決這些問題,用了N多的黑科技,搞得很悲催,更悲催的是當時覺得這個問題不多,於是用一些機器跑了這個模式,結果最後發現這裡面要黑科技解決的問題實在太多了,後來突然有個小夥伴提出我們試用lxc吧,才發現我們之前用黑科技解的很多問題都沒了,哎,然後就是決定切換到這個模式,結果就是線上的那堆機器重來。
這個設計的主要錯誤在於知識面不夠廣,導致做了個不正確的決定,而且推倒重來。
因此對於一個架構師來說,知識面的廣非常重要,在技術選型這點上非常明顯。

第12個錯
還是上面這個技術產品,這個東西有一個需求是磁碟空間的限額,並且要支援磁碟空間一定程度的超賣,當時的做法是用image的方式來佔磁碟空間限額,這個方式跑了一段時間覺得沒什麼問題,於是就更大程度的鋪開了,但鋪開跑了一段時間後,出現了一個問題,就是經常出現物理機磁碟空間不足的報警,而且刪掉了lxc容器裡的檔案也還是不行,因為image方式只要佔用了就會一直佔著這個大小,只會擴大不會縮小。
當時對這個問題極度的頭疼,只能是刪掉檔案後,重建image,但這個會有個要求是物理機上有足夠的空間,即使有足夠的空間,這個操作也是很折騰人的,因為得先停掉容器,cp檔案到新建立的容器,這個如果東西多的話,還是要耗掉一定時間的。
後來覺得這個模式實在是沒法玩,於是尋找新的解決方法,來滿足磁碟空間限額,允許超賣的這兩需求,最後我們也是折騰了比較長一段時間後終於找到了更靠譜的解決方案。
這個設計的主要錯誤還是在選擇技術方案時沒考慮清楚,對細節掌握不夠,考慮的面不夠全,導致了後面為了換掉image這個方案,用了極大的代價,我印象中是一堆的人熬了多次通宵來解決。
因此對於一個架構師來說,知識面的廣、對技術細節的掌控和設計的全面性都非常重要。

第13個錯
仍然是上面的這個技術產品,在執行的過程中,突然碰到了一個虛擬機器中執行緒數建立太多,導致其他的虛擬機器也建立不了執行緒的現象(不是因為物理資源不夠的問題),排查發現是由於儘管lxc支援各個容器裡跑相同名字的賬號,但相同名字的賬號的uid是相同的,而max processes是限制在UID上的,所以當一個虛擬機器建立的執行緒數超過時,就同樣影響到了其他相同賬號的容器。
這個問題我覺得一定程度也可以算是設計問題,設計的時候確實由於對細節掌握的不夠,考慮的不全導致忽略了這個點。
因此對於一個架構師來說,對技術細節的掌控和設計的全面性都非常重要。

第14個錯
在三年前做一個非常大的專案時,專案即將到上線時間時,突然發現一個問題是,有一個關鍵的點遺漏掉了,只好趕緊臨時討論方案決定怎麼做,這個的改動動作是非常大的,於是專案的上線時間只能推遲,我記得那個時候緊急週末加班等搞這件事,最後帶著比較高的風險上了。
這個問題主要原因是在做整體設計時遺漏掉了這個關鍵點的考慮,當時倒不是完全忽略了這個點,而是在技術細節上判斷錯誤,導致以為不太要做改動。
因此對於一個架構師來說,對技術細節的掌控是非常重要的,這裡要注意的是,其實不代表架構師自己要完全什麼都很懂,但架構師應該清楚在某個點上靠譜的人是誰。