轉(zhuǎn)載請(qǐng)附上原文地址:http://www.lxweimin.com/p/0c34a75569b1,謝謝!
前言
工作這些年,先后經(jīng)歷過(guò)兩家公司,有參與過(guò)php語(yǔ)言框架的開(kāi)發(fā)和主導(dǎo)過(guò)go語(yǔ)言技術(shù)棧的落地工作,在此過(guò)程中有一些感悟和總結(jié)。我想以之前我主導(dǎo)的go語(yǔ)言技術(shù)棧為線索,來(lái)陳述當(dāng)時(shí)遇到的一些問(wèn)題,以及分析問(wèn)題和解決問(wèn)題的思路。主要目的是想陳述go技術(shù)體系在團(tuán)隊(duì)中落地的過(guò)程,分析我們?cè)诟鱾€(gè)階段中,遇到的一些問(wèn)題,并將分析問(wèn)題的思路和解決問(wèn)題的方法記錄下來(lái),以便讓后來(lái)的同學(xué)了解go語(yǔ)言在團(tuán)隊(duì)的演進(jìn)過(guò)程,吸取相關(guān)的經(jīng)驗(yàn),以便在今后的系統(tǒng)設(shè)計(jì)和開(kāi)發(fā)上少走彎路。
在系統(tǒng)不斷演進(jìn)的過(guò)程中,有時(shí)候?qū)蚣艿倪x型很隨意,認(rèn)為能滿足現(xiàn)在功能就行,沒(méi)有對(duì)其功能擴(kuò)展性和性能進(jìn)行考量,導(dǎo)致隨著業(yè)務(wù)的發(fā)展,發(fā)現(xiàn)當(dāng)時(shí)選型有誤,但想轉(zhuǎn)又很難。那么現(xiàn)在,我就來(lái)談?wù)劊覀兪侨绾尉駬襁@些事情的。
我們?yōu)槭裁匆蓀hp轉(zhuǎn)向go
最初,大約是在2015年時(shí),平臺(tái)內(nèi)所有的業(yè)務(wù)系統(tǒng)均是由php語(yǔ)言構(gòu)成的,上線沒(méi)多久,平臺(tái)的流量開(kāi)始爆發(fā)性增長(zhǎng),并發(fā)量越來(lái)越大,晚高峰每秒請(qǐng)求由最初的幾千QPS到現(xiàn)在的幾十萬(wàn)QPS。當(dāng)時(shí),最快最有效的優(yōu)化手段無(wú)外乎加機(jī)器和增加php-fpm的數(shù)量,但是,受限于php本身的網(wǎng)絡(luò)模型,終究不適合這種高并發(fā),大流量的場(chǎng)景。面對(duì)這種棘手的問(wèn)題,再加上當(dāng)時(shí)人手有限,業(yè)務(wù)任務(wù)重等因素,于是找其它部門借了一批寫(xiě)lua的外援,幫忙把一些邏輯簡(jiǎn)單且訪問(wèn)量大的接口,換成了lua,暫時(shí)扛過(guò)了晚高峰,因此,有相當(dāng)長(zhǎng)的一段時(shí)間,大部分項(xiàng)目是php+lua共存的一個(gè)狀態(tài)。但由于lua本身的一些局限性,不太適合做一些復(fù)雜的業(yè)務(wù)邏輯,所以最終,在業(yè)務(wù)有強(qiáng)烈的需求的前提下,同時(shí)伴隨著技術(shù)發(fā)展的潮流,于2015年底,我們開(kāi)始選擇轉(zhuǎn)向go語(yǔ)言。
我們?cè)鯓佑蓀hp轉(zhuǎn)向go
由于之前團(tuán)隊(duì)全部都是php棧,在go方面的積累并不多,所以在php轉(zhuǎn)向go的過(guò)程中,面臨了在轉(zhuǎn)型過(guò)程中都會(huì)遇到的問(wèn)題:
1 用什么框架;
2 在業(yè)務(wù)任務(wù)重,人員極其匱乏的情況下如何將php項(xiàng)目重構(gòu)成go。
用什么框架
之前團(tuán)隊(duì)有人仿造內(nèi)部php框架開(kāi)發(fā)過(guò)一個(gè)golang框架,有人提議將其直接拿過(guò)來(lái)用,有人說(shuō)找個(gè)開(kāi)源的如beego,gin,martini等這類流行的框架。我個(gè)人當(dāng)時(shí)不太贊同使用自研的框架,主要有以下幾點(diǎn)原因:1 文檔少,漏洞多;2 需要投入人力去開(kāi)發(fā)和維護(hù),在當(dāng)時(shí)人力極其緊缺的情況下是不現(xiàn)實(shí)的。另外,當(dāng)時(shí)社區(qū)流行的框架也比較多,但是最終也沒(méi)有選擇那些流行的框架,主要是出于以下考慮:時(shí)間短,任務(wù)重,沒(méi)有精力去辨別各個(gè)框架的優(yōu)劣,適用場(chǎng)景以及性能如何。萬(wàn)一冒然使用一個(gè)還沒(méi)有深入了解的框架,線上出問(wèn)題咋辦!尤其在當(dāng)時(shí)系統(tǒng)頻繁出問(wèn)題,頂著各種壓力的情況下。
雖說(shuō),我們無(wú)法在短時(shí)間內(nèi)選一個(gè)合適的框架,但是能夠確定的是:我們的需求是什么?
1 只做高性能的HTTP 接口;
2 需要完整的單元測(cè)試體系;
3 可擴(kuò)展,組件化;
基于以上三點(diǎn),可以發(fā)現(xiàn),go語(yǔ)言自帶的特性就可以滿足這些需求。于是,我們開(kāi)始決定裸寫(xiě)。
此外,還有一個(gè)裸寫(xiě)的原因就是:沒(méi)想好將來(lái)想要什么!當(dāng)然,每個(gè)團(tuán)隊(duì)的背景不一樣,業(yè)務(wù)場(chǎng)景也不同,在人力和時(shí)間充裕的情況下,還是需要選擇一個(gè)合適的框架比較好。
裸寫(xiě)不是亂寫(xiě)
裸寫(xiě)不是亂寫(xiě)。眾說(shuō)周知,用框架的其中一個(gè)好處就是保證團(tuán)隊(duì)代碼風(fēng)格的一致性,當(dāng)然,目前市面上除了beego外的大多數(shù)框架,在代碼風(fēng)格上也并沒(méi)有做約束。為了保證團(tuán)隊(duì)go代碼的規(guī)范性和一致性,按照經(jīng)典的分層架構(gòu)和過(guò)往的經(jīng)驗(yàn),我們制定了一套go編程模版,由上向下:Router層,Service層,Dao層,還有貫穿這三層的Entity層,架構(gòu)圖如圖1所示。其中,Router層負(fù)責(zé)處理與http handler邏輯,請(qǐng)求參數(shù)以及response格式相關(guān)的處理工作;Service層處理業(yè)務(wù)邏輯;Dao層處理數(shù)據(jù)訪問(wèn)邏輯;Entity層負(fù)責(zé)實(shí)體定義相關(guān)的邏輯,并貫穿Router,Service,Dao這三層。層與層之間不直接進(jìn)行耦合,高層模塊不直接依賴與低層模塊,它們都依賴于所定義的抽象接口。
Booch曾經(jīng)說(shuō)過(guò):“所有結(jié)構(gòu)良好的面向?qū)ο髽?gòu)架都具有清晰的層次定義,每個(gè)層次通過(guò)一個(gè)定義良好的,受控的接口向外提供了一組內(nèi)聚的服務(wù)”。
除此之外,我們還維護(hù)了一套常用的公共組件庫(kù),如:日志庫(kù),各種數(shù)據(jù)庫(kù)driver等。
如何重構(gòu)
當(dāng)我們制定好編程模版后,我們就開(kāi)始進(jìn)行項(xiàng)目重構(gòu)工作。由于,業(yè)務(wù)任務(wù)重,人手少,所以,重構(gòu)的基本方向就是:根據(jù)業(yè)務(wù)需求,結(jié)合接口重要性進(jìn)行重構(gòu)。只有這樣,才能保證在業(yè)務(wù)需求不停的情況下,進(jìn)行系統(tǒng)重構(gòu)。所以,在此期間,有相當(dāng)長(zhǎng)的一段時(shí)間是處于php+go進(jìn)行混合編程,混合部署的狀態(tài),如圖2所示,重構(gòu)完的接口,通過(guò)nginx代理到新接口,這種狀態(tài)一直持續(xù)了一年。采取混合編程的思路在重構(gòu)初期,可能會(huì)遇到一些問(wèn)題,比如:同一段業(yè)務(wù)邏輯,需要用go寫(xiě)一遍,用php寫(xiě)一遍,無(wú)疑增加了一定的工作量,當(dāng)然這也是避免不了的。
注意,有些同學(xué)在重構(gòu)的時(shí)候容易走到一個(gè)誤區(qū):一口氣把整個(gè)項(xiàng)目都重構(gòu)了,或者說(shuō)重構(gòu)大部分內(nèi)容。從時(shí)間成本和系統(tǒng)穩(wěn)定性上來(lái)講,這種方式風(fēng)險(xiǎn)比較大,不推薦。推薦的思路:一個(gè)接口一個(gè)接口進(jìn)行重構(gòu)。
最終,為何想引入go-kit
之前這套東西,基本上可以滿足大部分的業(yè)務(wù)場(chǎng)景,但隨著隨著業(yè)務(wù)的發(fā)展,請(qǐng)求量越來(lái)越大,同時(shí),有些請(qǐng)求的鏈路也變長(zhǎng)了,為了繼續(xù)保證接口的高并發(fā)和低時(shí)延特性,團(tuán)隊(duì)有少量業(yè)務(wù)開(kāi)始嘗試GRPC。根據(jù)測(cè)試,壓測(cè)一個(gè)空接口,GRPC的性能大約是HTTP+JSON的2~3倍,在這里推薦一個(gè)壓測(cè)框架fperf 。
但是,針對(duì)GRPC的使用,不要盲目“求新”。以本人經(jīng)驗(yàn),HTTP+JSON的模式基本上可以滿足大部分的業(yè)務(wù)開(kāi)發(fā)場(chǎng)景了,針對(duì)小部分對(duì)接口時(shí)延和并發(fā)量要求極高的場(chǎng)景可以考慮使用GRPC。因?yàn)椋珿RPC本身還是不利于調(diào)試,且會(huì)在一定程度上增加調(diào)用方和服務(wù)方的耦合性,所以,最后的傳輸協(xié)議和格式建議還是以HTTP+JSON為主,以GRPC為輔。
另外,我們還是需要標(biāo)準(zhǔn)化一些中間件的使用,如回路斷流,rate limit等,來(lái)保障系統(tǒng)的穩(wěn)定性。這次的思考,時(shí)間比較充分,所以有精力去研究一些新的東西。最后,框架抉擇的思路和最初是一樣的,就是,明確我們的需求是什么?
1 需要一個(gè)同時(shí)支持多種傳輸協(xié)議,不論是現(xiàn)在的http,thrift,grpc,還是將來(lái)出現(xiàn)的某種新協(xié)議,要有良好擴(kuò)展性的框架;
2 框架本身和業(yè)務(wù)代碼保持一種低耦合的狀態(tài);
3 需要一套通用的middleware,使之與http,grpc等傳輸協(xié)議無(wú)關(guān)。
目前市面上流行的框架都是圍繞著http協(xié)議而展開(kāi)的,包括gin,beego等。經(jīng)調(diào)研,我們發(fā)現(xiàn)go-kit能夠滿足我們的需求。 go-kit本身不是一個(gè)框架,而是一套微服務(wù)工具集。其設(shè)計(jì)思想跟我們初期go編程模版制定的思想也算是不謀而合——分層設(shè)計(jì),組件化,可擴(kuò)展。go-kit的架構(gòu)如圖3所示,分為三層結(jié)構(gòu):Transport層,Endpoint層,Service層。Transport層主要負(fù)責(zé)與傳輸協(xié)議HTTP,GRPC,THRIFT等相關(guān)的邏輯,Endpoint層主要負(fù)責(zé)request/response格式的轉(zhuǎn)換,以及公用攔截器相關(guān)的邏輯;Service層則專注于業(yè)務(wù)邏輯。go-kit除了經(jīng)典的分層架構(gòu)外,還在endpoint層提供了很多公用的攔截器,如log,metric,tracing,circuitbreaker,rate-limiter等,來(lái)保障業(yè)務(wù)系統(tǒng)的可用性。它們?cè)谠O(shè)計(jì)上有一個(gè)共同特點(diǎn):都是同傳輸協(xié)議無(wú)關(guān)的。在之前的一些http框架中,這些攔截器同傳輸協(xié)議是緊緊耦合在一起的,如果,此時(shí)我需要將某些HTTP接口改造成GRPC協(xié)議的接口,那么這些攔截器我還得再基于grpc再實(shí)現(xiàn)一遍,設(shè)計(jì)上存在一定的冗余。因此,借助go-kit這套工具集,我們就能很好的對(duì)transport協(xié)議,middleware進(jìn)行擴(kuò)展,且不會(huì)影響到業(yè)務(wù)本身的設(shè)計(jì)。
怎樣將go-kit集成到現(xiàn)有的業(yè)務(wù)系統(tǒng)中
我們找到了心儀的開(kāi)源工具后,那么我們?cè)鯓右暂^低的成本將其引入到我們業(yè)務(wù)系統(tǒng)中呢?之前我們有提到,我們的go模版是分為三層:router,service和dao。而go-kit也分為三層,我們可以根據(jù)每層職責(zé)的不同進(jìn)行重新組合,如圖4所示,從上到下依次為:transport層,endpoint層,service層,dao層。這樣就能很輕易的將go-kit集成進(jìn)來(lái),當(dāng)然你如果哪天因?yàn)槟撤N原因,不想再繼續(xù)使用go-kit這套東西,直接將endpoint層和Transport層移除即可。在集成的過(guò)程中,需要注意一點(diǎn):之前的代碼中router層不能包含任何業(yè)務(wù)邏輯,否則就無(wú)法集成。
如何高效的使用go-kit
前面有提到,go-kit本身分為三層,針對(duì)這點(diǎn)有同學(xué)會(huì)提出:“每次新建項(xiàng)目,都需要手動(dòng)寫(xiě)下go-kit的這三層邏輯,有點(diǎn)浪費(fèi)時(shí)間,不夠簡(jiǎn)潔”,這確實(shí)是一個(gè)共性問(wèn)題,從go-kit的github的issue中可以發(fā)現(xiàn),也有不少人反饋過(guò)類似問(wèn)題。很慶幸的是,有人給我們鋪好了路,詳見(jiàn)GoKit CLi,其主要功能如圖5所示。
這個(gè)工具可以根據(jù)我們的需求自動(dòng)生成service,transport和endpoint模版,以及生成供調(diào)用方的使用的client library,節(jié)省我們大量的時(shí)間,提高我們的生產(chǎn)效率。具體操作步驟,可以參考GoKit CLi的說(shuō)明,這里不再贅述。
總結(jié)
不論是我之前的一篇文章《淺談互聯(lián)網(wǎng)業(yè)務(wù)系統(tǒng)設(shè)計(jì)》所講的系統(tǒng)設(shè)計(jì),還是這篇文章所陳述的框架選型。我一直在強(qiáng)調(diào)的一點(diǎn)就是:需求是什么?如何在滿足需求的同時(shí),讓框架和系統(tǒng)具有一定的彈性。無(wú)外乎使用經(jīng)典的五大設(shè)計(jì)原則:?jiǎn)我宦氊?zé)原則,開(kāi)放封閉原則,依賴倒置原則,接口隔離原則,為你的設(shè)計(jì)提供堅(jiān)實(shí)的理論基礎(chǔ)和方向指引。另外,在做選型的時(shí)候不要盲從,別人口中好的東西,不一定適合你,只有明確自身需求后,找到適合自己的才是最好的!