微服務是近期的熱點。
當我在SoundCloud工作時,負責從一個巨大的Ruby on Rails應用程序里遷移到眾多的微服務上。我已經多次講述這個過程的技術問題了,在演講里,也在SoundCloud的工程師博客里寫了一系列文章。這些是工程師們最感興趣的話題,但是最近我才意識到從來沒有向大家解釋過我們最終使用微服務之前做了什么嘗試。
我很抱歉可能會讓一些技術人員失望,但是我們遷移到微服務更多的是跟生產力相關,而不是單純的技術因素。下文會詳細解釋。
注意:本文有很多修正之處,為了使其更容易理解,將相當混亂的一系列事件簡化成了線性的時間鏈。不過,我相信這很好得展示了在SoundCloud最初幾年所做的工作。
Next項目
當我最初加入公司的時候,手頭最重要的項目內部稱之為v2。該項目對我們的網站做徹底的改版,發布時的商標名稱為The Next SoundCloud。
一開始我加入了后端團隊,App團隊。我們負責巨大的Ruby on Rails應用程序。那時候還不稱其為遺留系統,而稱之為mothership。App團隊負責Rails應用相關的所有事情,包括舊的用戶接口。Next是一個單頁面的JavaScript web應用程序,我們遵照當時的標準實踐,將其構建為公開API的常規客戶端,用Rails monolith實現的。
App和Web這兩個團隊是完全隔離的 -- 甚至在Berlin距離挺遠的不同的大廈辦公。我們幾乎只在所有人都參加的大會上才能看到彼此,主要的溝通工具是問題跟蹤系統和IRC。如果你問任何一個團隊的任何人我們的開發流程時如何工作的,他們可能會這么回答:
有人想到了某個功能,隨后寫了很多描述,并且畫了些模擬圖。
設計師優化了用戶體驗。
編寫代碼。
一些測試之后,將應用部署。
但有時候這個過程會遇到一些障礙。工程師和設計師都抱怨他們加班過多,但同時產品經理和合作伙伴則抱怨他們永遠無法按時得到想要的東西。
作為一個小型消費者企業,我們非常需要能夠確保吸引盡可能多的合作伙伴(就是那些Apple和Google在發布產品時在一頁PPT里列出的合作伙伴),因為這些意味著免費的宣傳和增長。我們也需要在圣誕節前發布Next的內測版本,否則連續的假日就會將我們的所有計劃推延到新年的第二季度,因為我們不想在新網站上線之前推出任何新功能。要想能夠登上Keynote的PPT,并且確保我們不會浪費整個季度,我們必須開始追逐截止日期。
就是這個時候,我們決定嘗試并且理解我們組織增長的流程到底是什么狀態。
流程hacking
在加入SoundCloud之前,我當了好幾年的咨詢師,這些年里我學到的最有用的工具之一就是創建Value Stream Map的理念。我不想詳細講這個技術的為什么和如何做,但是如果你對下文講述的流程感興趣的話,至少已經知道應該搜索什么關鍵字了。
將不同工程師的非正式采訪融合在一起,并且從我們多個自動化系統里收集數據,能夠畫出實際流程的圖,和我們認為的流程作對比。我無法展示實際的文檔,但是實際圖和如下虛構的圖差不多:
實際工作量類似:
有人想到一個功能。他們隨后編寫一個輕量級的spec,有一些模擬屏幕截圖,并且保存到Google Drive文檔里。
spec一直也就是個文檔,直到有人有時間真得實現它。
非常小的設計團隊得到spec,并且為其設計用戶體驗。隨后會變成Web團隊管理的Trello白板上的一個待辦事項。
這一待辦事項會在Trello白板上待上一段時間,最少是一個兩周的迭代之后,工程師才有時間查看它。
工程師可能開始基于這一項工作。在使用偽造/靜態數據將設計轉化成合適的基于瀏覽器的體驗之后,工程師會記錄下要想使這個功能能夠工作的話,Rails API所需的改動。這會進入到Pivotal Tracker,App團隊所選擇的工具。
這項待辦事項會一直呆在Pivotal里,直到App團隊有人有時間查看它,通常又需要另一個兩周的迭代。
App團隊成員可能為API能夠工作而開始編寫代碼,集成測試和任何所需的其他東西。隨后他們會更新Trello issue,讓Web團隊知道他們這部分工作已經完成了。
更新過的Trello待辦事項會在backlog里待上一段時間,等待Web團隊的工程師來完成他們之前開始的工作。
Web團隊開發人員讓他們的客戶端代碼匹配上后臺實現,并且發出可以部署的信號。
因為部署風險很大,很慢并且很困難,App團隊會等待好幾個功能都進入主分支之后才一起部署到生產環境。這意味著功能可能會在源碼控制系統里待好幾天,并且你的功能很可能會因為完全不相關的代碼而被回滾。
某一天,這個功能終于部署到生產系統了。
這些步驟里很可能會有很多的來回,因為大家需要進一步說明或者又有了更好的想法。不過暫時先忽略這些。
總之,一個功能需要花費兩個月才能上線。更為糟糕的是:這個過程超過一半的時間都花在等待上,比如,一些Work In Progress的項目等待某個工程師來完成。
像上文所說的map這樣的工具使得更容易發現上述流程的詭異之處。我們只看圖就能想到的是必須為monolith采用release train的方式,而不是等到有足夠多的功能才一起部署,需要開始每天都進行部署,而不用管有多少功能進入了主分支。雖然這和持續部署還相差很遠,但是已經可以幫助改進一點我們的開發周期了:
低處的水果是行動的最大驅動力,但是我們的例子里最主要的問題顯然是前端和后端開發團隊之間的來回。
在我們將工作分為Web和App團隊時,其實就已經將后端開發人員和實際的產品完全隔離開了。他們會感到沮喪,覺得自己對于產品完全沒有發言權。他們會覺得自己“只需要做像素抄寫員所告知的工作”。在一個比供應鏈需要更多有經驗的開發人員的市場里,這么對待團隊似乎不是很好的做法。
但是如今需要關注的問題是花在開發上的47天里,只有11天真的在干事情。剩余的時間都在隊列里浪費了,基本都是等待時間。
有一種說法認為浪費多少時間取決于等待新迭代的時間,但是即使改成迭代更短的流程,比如Kanban的variation,并沒能幫助多少。
我們隨后決定做一些有爭議的事情:將后端和前端開發人員配對,他們在某個功能完成之前對其負有完全的責任。我們只有8個后端工程師,11個前端工程師,所以該策略的爭論主要是因為需要前端開發人員盡可能早得完成大量工作,從而能讓后端開發人員在每個功能上只需花費盡可能少的時間。這樣策略的啟動靠的是直覺,但是流程映射向我們展示了這樣的策略其實產生了反作用。即使不算足前端和后端開發人員的來回討論的時間,在東西實際上線之前,仍然有太長的等待時間。
我們決定首先單對嘗試,隨后再擴大到其他人。新流程類似于:
每個人終于能將更多的時間花在每個功能的開發上。這其實不相關,因為他們工作的同時,能夠在更少的時間內完成端到端編碼。值得注意的是,即使后臺開發人員之前就和其他App團隊的人比較遠,但是在改動進入Rail應用的主分支之前要求強制的代碼審核(也就是:Pull Request)流程。
時間減少了很多,我們決定在流程的其他步驟里嘗試并完成相同的事情。我們讓設計人員、產品經理以及前端開發人員在某個功能的范圍內緊密工作,周期時間更為縮短了:
的確有相當可觀的時間縮減。更短的工作流,讓我們能夠更容易得在截止日期之前發布Next的第一個版本。我們持續以不同領域配對的方式來進行迭代,最終導致功能團隊構造SoundCloud。但是這是以后要講的主題,這篇文章關注于這個長的Pull Request隊列里有什么東西。
[page]從mothership到遺留系統
讓我最為興奮的一件事情是所加入的SoundCloud有著濃厚的工程師文化。大部分聽上去和我用在ThoughtWorks項目里的方式類似,但是有一個方面是全新的:強制的代碼審核。
那時是2011年,所有的創業公司都在嘗試復制GitHub的模型,Zach Holman的GitHub如何使用GitHub來構建GitHub演講里對此講述得非常清楚。在很多年使用和倡導基于trunk的開發模式后,我迫不及待得想看到SoundCloud這樣的公司以及GitHub能夠使用這一與眾不同的方式。
那時候,所有App團隊的工程師都坐在一個桌子周圍,共享相同的任務backlog,并且互相非常親密。monolith的代碼基已經很古老,成熟并且無聊了。我們在整個代碼基里都遵守相同的原則和模式,代碼提交都是基于現有設計,沒有什么意外情況。這使得Pull Request流程幾乎是一種儀式,大家花不到一個小時的時間審查提交的內容。
因為越來越多的人離開了這個緊密的組織,在開發Next功能時去和Web團隊的配對,正式的溝通渠道被破壞了。有問題的部署變得越來越頻繁,是由一些問題上的誤解所導致的,這些問題包括什么正在部署或者一些功能如何設計等。因為這通常是人為導致的,在很多這樣的問題之后,我們認為方案必須要能夠在合并改動上強制實行一種更為嚴格的流程。從此以后,在將改動放到主分支并且最終部署之前,所有改變必須被第二個工程師“正式得”批準。
如上圖所示,這導致了改動上線之前Pull Request的長時間等待。要想解決這個問題,我們檢查的第一步是確保每個人每天最少花費一個小時來審核團隊外部進來的Pull Request -- 比如,從Next項目上工作的人。這并沒能更快得減少隊列的長度,最終我們意識到一些小的Pull Request被很多人審核,而一些大的Pull Request(Next的Pull Request通常很大)卻無人問津,直到產品經理發怒為止。大改動的審核會花費很多時間,基于我們的Rails代碼的特點,也非常有風險。大家都像躲開瘟疫一樣避開這些大型Pull Request。
我們一致決定從事Next功能開發的開發人員需要將其工作分解到更小、更好管理的Pull Request。這也和每個Pull Request都需要快速審核和合并的理念契合。但是同時將單一功能分解為多個小型Pull Request也會導致審核人員看不到整體情況:有時候一系列看上去都挺好的代碼提交實際上隱藏著危險的架構錯誤。我們確定更好的用戶故事的需求,但是對員工進行培訓可能會花費一些時間,為了業務需要,我們需要一個短期就能見效的方案。
最終應用了書上的古老技巧:結對。看出來了么,我們的需求是代碼必須被另一個開發人員審核。使用結對編程之后,我們一直都在進行實時的審核,這意味著每個代碼提交都能自動+1。大多數人都喜歡結對,不喜歡的人可以選擇保持單獨工作,但是只在和Next項目不相關的任務上。
我們開始嘗試了幾對,但是一件有意思的事情阻止了我們。我們發現monolithic的代碼基太大了,以至于沒有一個人了解所有的代碼。大家圍繞應用程序的子模塊建立了自己的專業領域。當一對人選擇了某個待辦事項,這對人可能發現自己沒有該部分代碼的足夠知識,因此他們不得不要么等待該領域的專家有空閑,要么和熟悉該領域的人重新配對,要么選擇另外的任務,通常是低優先級的任務。這兩種選擇都很糟糕。
這都成了公司的一個笑話,“這里的所有東西都很有趣,像做游戲一樣,直到你不得不學習monolith的時候。”
monolith無法減少的復雜度
要想從所花費的時間里節省出這8天,我們需要后退一步,問問自己,為什么一開始需要這所有的Pull Request。隨著對自己的流程理解的深入,我們的想法隨之改變:
我們為什么需要Pull Request?因為我們知道,基于多年的經驗,大家通常會犯很低級的錯誤,這樣的改動上線后可能會導致整個平臺崩潰幾個小時。
為什么大家這么頻繁得犯錯?因為代碼基太復雜了。很難記住所有事情。
為什么代碼基這么復雜?因為SoundCloud從一個非常簡單的網站起步,但是隨著時間的演進,它發展成了一個大型平臺。我們擁有很多功能,各式各樣不同的客戶應用程序、不同類型的用戶、同步和異步的工作流以及大型規模。代碼基實現并且反應了如今復雜平臺的很多組件。
為什么我們需要單個代碼基來實現很多組件?因為范圍經濟。mothership有著很好的部署流程和工具,架構經歷了尖峰性能和DDOS的實際考驗,也很容易水平擴展等等。如果構建新系統,我們將必須為新系統構建所有的這一切。
為什么我們在多個,或者小型系統里得不到規模經濟?嗯。。。
第五個問題需要長篇大論來回答。我們自己的經驗和同事的調研顯示可能有兩種方案:
(A)為什么我們在多個或者小型系統里得不到規模經濟?問題不是我們不能,而是這樣做并不會比將所有東西放到一個代碼基里更為有效。相反,我們應該圍繞monolith以及開發人員可用性來構建更好的工具和測試。這也是Facebook和Etsy采用的方式。
(B)為什么我們在多個或者小型系統里得不到規模經濟?我們可以。我們需要做一些實驗來找到所需的工具和支持。當然,也取決于構建了多少單獨的系統,我們也需要思考規模經濟,但這是Netflix、Twitter等構建系統的方式。
每種方案都有各自的支持者,并沒有哪一種明顯對或者錯。最大的問號是每個方案需要多少代價。金錢和資源不是問題,但是我們沒有足夠的人或時間來研究任何顛覆性的事情。我們需要一種能夠增量實現的策略,而且從一開始就能帶來價值。
我們從另外的角度審視擁有的東西。我們一直用非常簡單的格式來看待后臺系統:
這樣的思路必然會將整個大盒子實現為一個單一的巨大代碼基。雖然我們在自我反省中發現了這一點,但是實際事情并不像上圖那么明顯。
實際上,如果你打開這個大黑箱,會意識到我們的系統更像如下圖像所示:
我們沒有單一的網站,我們有的是擁有多個組件的平臺。每個組件都有自己的擁有者和stakeholder,以及獨立的生命周期。
比如,subscriptions模塊構建了一次,只在支付網關要求我們在流程里改變什么的時候才需要改動。另一方面,notifications以及其他模塊,和增長以及留存率相關的,則會因為我們這個年輕的創業公司努力發展更多的用戶和內容而每天都有改動。
它們還有不同服務級別的預期。一個小時沒有收到通知不會讓任何人抓狂,但是回放模塊五分鐘的中斷就會讓我們很受傷。
如果嘗試(A)方案,結論是要想使得monolith工作的唯一方式就是讓這些組件顯式化,不僅僅在代碼上,而且要從部署架構上。
在代碼級別,需要確保單個功能的改動能夠在相對隔離的地方開發,而不要求改動其他組件的代碼。需要確保該改動不會引入bug或者改變系統里其他不相關部分的運行時行為。這是業界一直存在的問題,我們知道必須要讓隱式組件顯式化,并且確保充分了解了哪個模塊依賴于哪個模塊。
我們討論了使用Rails引擎和各種工具來實現,類似如下:
在部署方面,需要確保某個功能能夠單獨部署。將某個模塊的改動推送到生產環境不要求非相關模塊的重新部署,并且如果這樣的部署失敗,導致生產環境被破壞,那么被影響的唯一功能就是有改動發生的功能。
要想實現這樣的系統,我們考慮仍舊將相同的artifact部署到所有服務器上,但是使用負載均衡器來確保一組服務器只負責單一功能,將這個功能的問題和其他服務器上的功能隔離開:
要完成這些工作并不簡單。即使上述方案并不要求從一直使用的技術堆棧和工具隔離出去,這些改動還是會帶來了問題和風險。
但是即使一切都很順利,我們也知道monolith的現有代碼無論如何都需要重構。這些代碼在過去幾年里一直帶來很多問題,到處都有欠下的技術賬。除了我們自己制造的麻煩,還需要從Rail 2.x升級到3,這本身也是個巨大的遷移工作。
這些問題都讓我們重新思考(B)方案。我們認為它類似于:
但是至少我們能夠從開始的第一天就從這個方案受益。任何我們構建的新東西都會是一個全新的項目,就不會再受Pull Request的困擾了。
我們決定試一試,并且最終將首個項目構建成服務,從monolith上隔離出來。該項目引入了多個大功能,并且重新思考了subscription模塊,并且比預先計劃提前了2組2個工程師完成。
體驗很棒,我們決定為新東西的構建持續使用該架構。我們的第一個服務使用Clojure和JRuby構建,最終改為使用Scala和Finagle。
[page]必須引用康威定律
從2013年起SoundCloud所構建的新東西幾乎都是服務。不知道從什么時候起,我們開始使用“微服務”來指代這些服務,但是在一開始構建這種架構時并沒有想到這一點(SoundCloud在2013年在郵件里第一次使用單詞‘微服務’,2012年實現了第一個服務)。
使用新架構框架,我們能夠將新功能所花費的時間減少到,雖然和最早的黃金時代比還有不少差距,但是對于一個在競爭異常激烈的音樂領域奮斗的公司而言已經足夠好了:
到這里都很不錯,但是這是針對新功能的。無論何時需要改進現有功能,這些都還在monolith里實現,我們還是不得不回到舊的周期里。更糟糕的是,很多人在這些新微服務上比巨大代碼基上花費更多的時間,因此空閑的審核員數目降低,但是Pull Request隊列持續增長。
每次一些大型改動出現的時候,我們都安排足夠的時間來確保從monolith里提取出舊系統。雖然這從來沒有發生過。大家仍然要么在舊代碼里實現這些改動,要么創建了一些詭異的混合代碼,改動在一個微服務里實現,微服務和巨大代碼基耦合在一起。
這時,App“團隊”更像是后臺開發人員的資源池,他們會和Web團隊,設計師以及產品經理配對,在某個功能上一起工作一段時間。大家會一直從一個功能跳到另一個功能,我們意識到我們并沒有為系統的任何部分指定所有者或者自主權。任何人如果覺得不為某件事情負責,就都不會承擔風險來研究這些歷史代碼。這正印證了古老的格言:所有人都承擔責任就等于沒有人承擔責任。
我們考慮將資源池分解成小型團隊,關注于特定領域。在花費很多時間嘗試找到正確的編組方式之、后,發現我們還是無法達成共識。這很讓人沮喪,有時候我只能將組分解成3-4人的小團隊,幾乎隨機得指定他們的模塊職能。
這些團隊被告知他們對所負責的模塊負全責。這意味著這些模塊造成中斷時會找到他們,同時他們也有自由去開發認為合理的變化。如果他們決定將某些東西保留在monolith里,他們自己決定就好了。他們是維護代碼的人。
你可能會猜到,之后我們看到大量代碼脫離大型代碼基。Messages、stats以及新的iOS應用所需的大部分改進的功能都從主代碼基里抽離了出來。
一切都很順利,但是分解團隊的半隨機的方式是很大的問題:單一團隊負責生態系統里幾乎所有基礎功能和對象,類似跟蹤和用戶元數據和社交圖。該團隊一直扮演救火隊的角色,無暇顧及遷移模塊到微服務上,因為這會帶來更多風險和可能的中斷。
這個問題最近才解決了。我們仍然讓單一團隊負責這些對象,但是現在的架構更加穩定,降低了需要救火的時間。最終讓這些人能夠有時間將項目本身從monolith里將模塊抽離出來。
如今,SoundCloud還有monolith,但是它的重要性每天都在降低。它仍然在很多功能的關鍵路徑上,但是由于strangler系統,它甚至不再是面向互聯網的。我不確定最終是否會消失,其提供的一些功能很小并且很穩定,保持這樣的狀態是最經濟的,但是我們計劃一年時間里將monolith從任何關鍵路徑上移除。
未來
正如本文一開始所述,這是我們微服務探索的簡化版。
我在這個公司的最后12個月關注于我們想引入的范圍經濟。就算我一直重復使用“微服務”的字眼也并不代表什么,可以確定的是如果有人使用該詞匯描述其架構,那么肯定有很多服務。隨著企業的發展,他們需要留意每個服務的固定花費。
我的團隊和我花費了很多時間思考如何利用約束,并且確保該架構的運維不會比monolith更昂貴和復雜。期望一些工作能夠開源,因此一定要訂閱工程師博客哦。我在以后的博客里會繼續介紹更多內容。
這些年我們學到了很多,即使我離開了SoundCloud,我也堅信這樣的架構和團隊組織(這些東西攜手并進)會在接下來的幾年里繼續幫助公司完成目標 -- 可能會一直到“無核”或者“納米服務”流行起來的時候。