Dubbo作者聊 設計原則

以下內容均來自 梁飛 的個人博客 http://javatar.iteye.com/blog/1056664

  • 魔鬼在細節(jié)
  • 一些設計上的基本常識
  • 談談擴充式擴展與增量式擴展
  • 配置設計
  • 設計實現(xiàn)的健壯性
  • 防癡呆設計
  • 擴展點重構

魔鬼在細節(jié)中

轉于自己在公司的Blog:
http://pt.alibaba-inc.com/wp/experience_1301/code-detail.html

最近一直擔心Dubbo分布式服務框架后續(xù)如果維護人員增多或變更玻熙,會出現(xiàn)質量的下降嗦随,
我在想肌毅,有沒有什么是需要大家共同遵守的,
根據(jù)平時寫代碼時的一習慣笨奠,總結了一下在寫代碼過程中,尤其是框架代碼蔚袍,要時刻牢記的細節(jié)晋辆,
可能下面要講的這些,大家都會覺得很簡單霸饲,很基礎,但要做到時刻牢記,
在每一行代碼中都考慮這些因素精钮,是需要很大耐心的,
大家經常說,魔鬼在細節(jié)中脓杉,確實如此。

1. 防止空指針和下標越界
這是我最不喜歡看到的異常,尤其在核心框架中屋讶,我更愿看到信息詳細的參數(shù)不合法異常,
這也是一個健狀的程序開發(fā)人員乐疆,在寫每一行代碼都應在潛意識中防止的異常,
基本上要能確保一次寫完的代碼,在不測試的情況筒占,都不會出現(xiàn)這兩個異常才算合格。

2. 保證線程安全性和可見性
對于框架的開發(fā)人員,對線程安全性和可見性的深入理解是最基本的要求,
需要開發(fā)人員墨叛,在寫每一行代碼時都應在潛意識中確保其正確性,
因為這種代碼,在小并發(fā)下做功能測試時丸边,會顯得很正常,
但在高并發(fā)下就會出現(xiàn)莫明其妙的問題,而且場景很難重現(xiàn)蜓萄,極難排查。

3. 盡早失敗和前置斷言
盡早失敗也應該成為潛意識堂竟,在有傳入參數(shù)和狀態(tài)變化時,均在入口處全部斷言,
一個不合法的值和狀態(tài)郎仆,在第一時間就應報錯牺氨,而不是等到要用時才報錯夷狰,
因為等到要用時,可能前面已經修改其它相關狀態(tài)进倍,而在程序中很少有人去處理回滾邏輯,
這樣報錯后楷扬,其實內部狀態(tài)可能已經混亂,極易在一個隱蔽分支上引發(fā)程序不可恢復镣衡。

4. 分離可靠操作和不可靠操作
這里的可靠是狹義的指是否會拋出異痴舅冢或引起狀態(tài)不一致黍图,
比如曾雕,寫入一個線程安全的Map奴烙,可以認為是可靠的,
而寫入數(shù)據(jù)庫等剖张,可以認為是不可靠的,
開發(fā)人員必須在寫每一行代碼時搔弄,都注意它的可靠性與否幅虑,
在代碼中盡量劃分開,并對失敗做異常處理顾犹,
并為容錯倒庵,自我保護,自動恢復或切換等補償邏輯提供清晰的切入點炫刷,
保證后續(xù)增加的代碼不至于放錯位置擎宝,而導致原先的容錯處理陷入混亂。

5. 異常防御浑玛,但不忽略異常
這里講的異常防御绍申,指的是對非必須途徑上的代碼進行最大限度的容忍,
包括程序上的BUG顾彰,比如:獲取程序的版本號极阅,會通過掃描Manifest和jar包名稱抓取版本號,
這個邏輯是輔助性的涨享,但代碼卻不少狡相,初步測試也沒啥問題涌穆,
但應該在整個getVersion()中加上一個全函數(shù)的try-catch打印錯誤日志,并返回基本版本,
因為getVersion()可能存在未知特定場景異常,或被其他的開發(fā)人員誤修改邏輯(但一般人員不會去掉try-catch)贡必,
而如果它拋出異常會導致主流程異常,這是我們不希望看到的,
但這里要控制個度栈源,不要隨意try-catch,更不要無聲無息的吃掉異常竖般。

6. 縮小可變域和盡量final
如果一個類可以成為不變類(Immutable Class)甚垦,就優(yōu)先將它設計成不變類,
不變類有天然的并發(fā)共享優(yōu)勢涣雕,減少同步或復制艰亮,而且可以有效幫忙分析線程安全的范圍,
就算是可變類挣郭,對于從構造函數(shù)傳入的引用迄埃,在類中持有時,最好將字段final兑障,以免被中途誤修改引用侄非,
不要以為這個字段是私有的,這個類的代碼都是我自己寫的流译,不會出現(xiàn)對這個字段的重新賦值逞怨,
要考慮的一個因素是,這個代碼可能被其他人修改福澡,他不知道你的這個弱約定叠赦,final就是一個不變契約。

7. 降低修改時的誤解性革砸,不埋雷
前面不停的提到代碼被其他人修改除秀,這也開發(fā)人員要隨時緊記的,
這個其他人包括未來的自己算利,你要總想著這個代碼可能會有人去改它册踩,
我應該給修改的人一點什么提示,讓他知道我現(xiàn)在的設計意圖笔时,
而不要在程序里面加潛規(guī)則棍好,或埋一些容易忽視的雷,
比如:你用null表示不可用允耿,size等于0表示黑名單借笙,
這就是一個雷,下一個修改者较锡,包括你自己业稼,都不會記得有這樣的約定,
可能后面為了改某個其它BUG蚂蕴,不小心改到了這里低散,直接引爆故障俯邓。
對于這個例子,一個原則就是永遠不要區(qū)分null引用和empty值熔号。

8. 提高代碼的可測性
這里的可測性主要指Mock的容易程度稽鞭,和測試的隔離性,
至于測試的自動性引镊,可重復性朦蕴,非偶然性,無序性弟头,完備性(全覆蓋)吩抓,輕量性(可快速執(zhí)行),
一般開發(fā)人員赴恨,加上JUnit等工具的輔助基本都能做到疹娶,也能理解它的好處,只是工作量問題伦连,
這里要特別強調的是測試用例的單一性(只測目標類本身)和隔離性(不傳染失敗)雨饺,
現(xiàn)在的測試代碼,過于強調完備性除师,大量重復交叉測試沛膳,
看起來沒啥壞處扔枫,但測試代碼越多汛聚,維護代價越高,
經常出現(xiàn)的問題是短荐,修改一行代碼或加一個判斷條件倚舀,引起100多個測試用例不通過,
時間一緊忍宋,誰有這個閑功夫去改這么多形態(tài)各異的測試用例痕貌?
久而久之,這個測試代碼就已經不能真實反應代碼現(xiàn)在的狀況糠排,很多時候會被迫繞過舵稠,
最好的情況是,修改一行代碼入宦,有且只有一行測試代碼不通過哺徊,
如果修改了代碼而測試用例還能通過,那也不行乾闰,表示測試沒有覆蓋到落追,
另外,可Mock性是隔離的基礎涯肩,把間接依賴的邏輯屏蔽掉轿钠,
可Mock性的一個最大的殺手就是靜態(tài)方法巢钓,盡量少用。


一些設計上的基本常識

轉于自己在公司的Blog:
http://pt.alibaba-inc.com/wp/experience_886/software_design_general_knowledge.html

最近給團隊新人講了一些設計上的常識疗垛,可能會對其它的新人也有些幫助症汹,
把暫時想到的幾條,先記在這里贷腕。

1. API與SPI分離

框架或組件通常有兩類客戶烈菌,一個是使用者,一個是擴展者花履,
API(Application Programming Interface)是給使用者用的芽世,
而SPI(Service Provide Interface)是給擴展者用的,
在設計時诡壁,盡量把它們隔離開济瓢,而不要混在一起,
也就是說妹卿,使用者是看不到擴展者寫的實現(xiàn)的旺矾,
比如:一個Web框架,它有一個API接口叫Action夺克,
里面有個execute()方法箕宙,是給使用者用來寫業(yè)務邏輯的,
然后铺纽,Web框架有一個SPI接口給擴展者控制輸出方式柬帕,
比如用velocity模板輸出還是用json輸出等,
如果這個Web框架使用一個都繼承Action的VelocityAction和一個JsonAction做為擴展方式狡门,
要用velocity模板輸出的就繼承VelocityAction陷寝,要用json輸出的就繼承JsonAction,
這就是API和SPI沒有分離的反面例子其馏,SPI接口混在了API接口中凤跑,
合理的方式是,有一個單獨的Renderer接口叛复,有VelocityRenderer和JsonRenderer實現(xiàn)仔引,
Web框架將Action的輸出轉交給Renderer接口做渲染輸出。

image.png
image.png

2. 服務域/實體域/會話域分離

任何框架或組件褐奥,總會有核心領域模型咖耘,比如:
Spring的Bean,Struts的Action抖僵,Dubbo的Service鲤看,Napoli的Queue等等
這個核心領域模型及其組成部分稱為實體域,它代表著我們要操作的目標本身耍群,
實體域通常是線程安全的义桂,不管是通過不變類找筝,同步狀態(tài),或復制的方式慷吊,
服務域也就是行為域袖裕,它是組件的功能集,同時也負責實體域和會話域的生命周期管理溉瓶,
比如Spring的ApplicationContext急鳄,Dubbo的ServiceManager等,
服務域的對象通常會比較重堰酿,而且是線程安全的疾宏,并以單一實例服務于所有調用,
什么是會話触创?就是一次交互過程坎藐,
會話中重要的概念是上下文,什么是上下文哼绑?
比如我們說:“老地方見”岩馍,這里的“老地方”就是上下文信息,
為什么說“老地方”對方會知道抖韩,因為我們前面定義了“老地方”的具體內容蛀恩,
所以說,上下文通常持有交互過程中的狀態(tài)變量等茂浮,
會話對象通常較輕双谆,每次請求都重新創(chuàng)建實例,請求結束后銷毀励稳。
簡而言之:
把元信息交由實體域持有佃乘,
把一次請求中的臨時狀態(tài)由會話域持有囱井,
由服務域貫穿整個過程驹尼。

image.png
image.png

3. 在重要的過程上設置攔截接口

如果你要寫個遠程調用框架,那遠程調用的過程應該有一個統(tǒng)一的攔截接口庞呕,
如果你要寫一個ORM框架新翎,那至少SQL的執(zhí)行過程,Mapping過程要有攔截接口住练,
如果你要寫一個Web框架地啰,那請求的執(zhí)行過程應該要有攔截接口,
等等讲逛,沒有哪個公用的框架可以Cover住所有需求亏吝,允許外置行為,是框架的基本擴展方式盏混,
這樣蔚鸥,如果有人想在遠程調用前惜论,驗證下令牌,驗證下黑白名單止喷,統(tǒng)計下日志馆类,
如果有人想在SQL執(zhí)行前加下分頁包裝,做下數(shù)據(jù)權限控制弹谁,統(tǒng)計下SQL執(zhí)行時間乾巧,
如果有人想在請求執(zhí)行前檢查下角色,包裝下輸入輸出流预愤,統(tǒng)計下請求量沟于,
等等,就可以自行完成植康,而不用侵入框架內部社裆,
攔截接口,通常是把過程本身用一個對象封裝起來向图,傳給攔截器鏈泳秀,
比如:遠程調用主過程為invoke(),那攔截器接口通常為invoke(Invocation)榄攀,
Invocation對象封裝了本來要執(zhí)行過程的上下文嗜傅,并且Invocation里有一個invoke()方法,
由攔截器決定什么時候執(zhí)行檩赢,同時吕嘀,Invocation也代表攔截器行為本身,
這樣上一攔截器的Invocation其實是包裝的下一攔截器的過程贞瞒,
直到最后一個攔截器的Invocation是包裝的最終的invoke()過程偶房,
同理,SQL主過程為execute()军浆,那攔截器接口通常為execute(Execution)棕洋,原理一樣,
當然乒融,實現(xiàn)方式可以任意掰盘,上面只是舉例。

image.png

4. 重要的狀態(tài)的變更發(fā)送事件并留出監(jiān)聽接口

這里先要講一個事件和上面攔截器的區(qū)別赞季,攔截器是干預過程的愧捕,它是過程的一部分,是基于過程行為的申钩,
而事件是基于狀態(tài)數(shù)據(jù)的次绘,任何行為改變的相同狀態(tài),對事件應該是一致的,
事件通常是事后通知邮偎,是一個Callback接口罗洗,方法名通常是過去式的,比如onChanged()钢猛,
比如遠程調用框架伙菜,當網絡斷開或連上應該發(fā)出一個事件,當出現(xiàn)錯誤也可以考慮發(fā)出一個事件命迈,
這樣外圍應用就有可能觀察到框架內部的變化贩绕,做相應適應。

image.png

5. 擴展接口職責盡可能單一壶愤,具有可組合性

比如淑倾,遠程調用框架它的協(xié)議是可以替換的,
如果只提供一個總的擴展接口征椒,當然可以做到切換協(xié)議娇哆,
但協(xié)議支持是可以細分為底層通訊,序列化勃救,動態(tài)代理方式等等碍讨,
如果將接口拆細,正交分解蒙秒,會更便于擴展者復用已有邏輯勃黍,而只是替換某部分實現(xiàn)策略,
當然這個分解的粒度需要把握好晕讲。

6. 微核插件式覆获,平等對待第三方

大凡發(fā)展的比較好的框架,都遵守微核的理念瓢省,
Eclipse的微核是OSGi弄息, Spring的微核是BeanFactory,Maven的微核是Plexus勤婚,
通常核心是不應該帶有功能性的摹量,而是一個生命周期和集成容器,
這樣各功能可以通過相同的方式交互及擴展蛔六,并且任何功能都可以被替換荆永,
如果做不到微核,至少要平等對待第三方国章,
即原作者能實現(xiàn)的功能,擴展者應該可以通過擴展的方式全部做到豆村,
原作者要把自己也當作擴展者液兽,這樣才能保證框架的可持續(xù)性及由內向外的穩(wěn)定性。

7. 不要控制外部對象的生命周期

比如上面說的Action使用接口和Renderer擴展接口,
框架如果讓使用者或擴展者把Action或Renderer實現(xiàn)類的類名或類元信息報上來四啰,
然后在內部通過反射newInstance()創(chuàng)建一個實例宁玫,
這樣框架就控制了Action或Renderer實現(xiàn)類的生命周期,
Action或Renderer的生老病死柑晒,框架都自己做了欧瘪,外部擴展或集成都無能為力,
好的辦法是讓使用者或擴展者把Action或Renderer實現(xiàn)類的實例報上來匙赞,
框架只是使用這些實例佛掖,這些對象是怎么創(chuàng)建的,怎么銷毀的涌庭,都和框架無關芥被,
框架最多提供工具類輔助管理,而不是絕對控制坐榆。

8. 可配置一定可編程拴魄,并保持友好的CoC約定

因為使用環(huán)境的不確定因素很多,框架總會有一些配置席镀,
一般都會到classpath直掃某個指定名稱的配置匹中,或者啟動時允許指定配置路徑,
做為一個通用框架豪诲,應該做到凡是能配置文件做的一定要能通過編程方式進行职员,
否則當使用者需要將你的框架與另一個框架集成時就會帶來很多不必要的麻煩,
另外跛溉,盡可能做一個標準約定焊切,如果用戶按某種約定做事時,就不需要該配置項芳室。
比如:配置模板位置专肪,你可以約定,如果放在templates目錄下就不用配了堪侯,
如果你想換個目錄嚎尤,就配置下。

9. 區(qū)分命令與查詢伍宦,明確前置條件與后置條件

這個是契約式設計的一部分芽死,盡量遵守有返回值的方法是查詢方法,void返回的方法是命令次洼,
查詢方法通常是冪等性的关贵,無副作用的,也就是不改變任何狀態(tài)卖毁,調n次結果都是一樣的揖曾,
比如get某個屬性值,或查詢一條數(shù)據(jù)庫記錄,
命令是指有副作用的炭剪,也就是會修改狀態(tài)练链,比如set某個值,或update某條數(shù)據(jù)庫記錄奴拦,
如果你的方法即做了修改狀態(tài)的操作媒鼓,又做了查詢返回鸭巴,如果可能瞻赶,將其拆成寫讀分離的兩個方法,
比如:User deleteUser(id)倒慧,刪除用戶并返回被刪除的用戶站玄,考慮改為getUser()和void的deleteUser()枚驻。
另外,每個方法都盡量前置斷言傳入參數(shù)的合法性株旷,后置斷言返回結果的合法性再登,并文檔化。

10. 增量式擴展晾剖,而不要擴充原始核心概念
參見:http://javatar.iteye.com/blog/690845


談談擴充式擴展與增量式擴展

轉于自己在公司的Blog:
http://pt.alibaba-inc.com/wp/experience_760/generic_vs_composite_expansibility.html

我們平臺的產品越來越多锉矢,產品的功能也越來越多,
平臺的產品為了適應各BU和部門以及產品線的需求齿尽,
勢必會將很多不相干的功能湊在一起沽损,客戶可以選擇性的使用,
為了兼容更多的需求循头,每個產品绵估,每個框架,都在不停的擴展卡骂,
而我們經常會選擇一些擴展的擴展方式国裳,也就是將新舊功能擴展成一個通用實現(xiàn),
我想討論是全跨,有些情況下也可以考慮增量式的擴展方式缝左,也就是保留原功能的簡單性,新功能獨立實現(xiàn)浓若,
我最近一直做分布式服務框架的開發(fā)渺杉,就拿我們項目中的問題開涮吧。

比如:遠程調用框架挪钓,肯定少不了序列化功能是越,功能很簡單,就是把流轉成對象诵原,對象轉成流英妓,
但因有些地方可能會使用osgi挽放,這樣序列化時绍赛,IO所在的ClassLoader可能和業(yè)務方的ClassLoader是隔離的蔓纠,
需要將流轉換成byte[]數(shù)組,然后傳給業(yè)務方的ClassLoader進行序列化吗蚌,
為了適應osgi需求腿倚,把原來非osgi與osgi的場景擴展了一下,
這樣蚯妇,不管是不是osgi環(huán)境敷燎,都先將流轉成byte[]數(shù)組,拷貝一次箩言,
然而硬贯,大部分場景都用不上osgi,卻為osgi付出了代價陨收,
而如果采用增量式擴展方式饭豹,非osgi的代碼原封不動,
再加一個osgi的實現(xiàn)务漩,要用osgi的時候拄衰,直接依賴osgi實現(xiàn)即可。

再比如:最開始饵骨,遠程服務都是基于接口方法翘悉,進行透明化調用的,
這樣居触,擴展接口就是妖混,invoke(Method method, Object[] args),
后來轮洋,有了無接口調用的需求制市,就是沒有接口方法也能調用,并將POJO對象都轉換成Map表示砖瞧,
因為Method對象是不能直接new出來的息堂,我們不自覺選了一個擴展式擴展,
把擴展接口改成了invoke(String methodName, String[] parameterTypes, String returnTypes, Object[] args)块促,
導致不管是不是無接口調用荣堰,都得把parameterTypes從Class[]轉成String[],
如果選用增量式擴展竭翠,應該是保持原有接口不變振坚,
增加一個GeneralService接口,里面有一個通用的invoke()方法斋扰,
和其它正常業(yè)務上的接口一樣的調用方式渡八,擴展接口也不用變啃洋,
只是GeneralServiceImpl的invoke()實現(xiàn)會將收到的調用轉給目標接口,
這樣就能將新功能增量到舊功能上屎鳍,并保持原來結構的簡單性宏娄。

再再比如:無狀態(tài)消息發(fā)送,很簡單逮壁,序列化一個對象發(fā)過去就行孵坚,
后來有了同步消息發(fā)送需求,需要一個Request/Response進行配對窥淆,
采用擴展式擴展卖宠,自然想到,無狀態(tài)消息其實是一個沒有Response的Request忧饭,
所以在Request里加一個boolean狀態(tài)扛伍,表示要不要返回Response,
如果再來一個會話消息發(fā)送需求词裤,那就再加一個Session交互刺洒,
然后發(fā)現(xiàn),原來同步消息發(fā)送是會話消息的一種特殊情況亚斋,
所有場景都傳Session作媚,不需要Session的地方無視即可。
如果采用增量式擴展帅刊,無狀態(tài)消息發(fā)送原封不動纸泡,
同步消息發(fā)送,在無狀態(tài)消息基礎上加一個Request/Response處理赖瞒,
會話消息發(fā)送女揭,再加一個SessionRequest/SessionResponse處理。

image.png
image.png

配置設計

轉于自己在公司的Blog:
http://pt.alibaba-inc.com/wp/experience_1182/sofeware-configuration-design.html

Dubbo現(xiàn)在的設計是完全無侵入栏饮,也就是使用者只依賴于配置契約吧兔,
經過多個版本的發(fā)展,為了滿足各種需求場景袍嬉,配置越來越多境蔼,
為了保持兼容,配置只增不減伺通,里面潛伏著各種風格箍土,約定,規(guī)則罐监,
新版本也將配置做了一次調整吴藻,去掉了dubbo.properties,改為全spring配置弓柱,
將想到的一些記在這沟堡,備忘侧但。

1. 配置分類

首先,配置的用途是有多種的航罗,大致可以分為:
(1) 環(huán)境配置禀横,比如:連接數(shù),超時等配置伤哺。
(2) 描述配置燕侠,比如:服務接口描述者祖,服務版本等立莉。
(3) 擴展配置,比如:協(xié)議擴展七问,策略擴展等蜓耻。

2. 配置格式

(1) 通常環(huán)境配置,用properties配置會比較方便械巡,
因為都是一些離散的簡單值刹淌,用key-value配置可以減少配置的學習成本。

(2) 而描述配置讥耗,通常信息比較多有勾,甚至有層次關系,
用xml配置會比較方便古程,因為樹結構的配置表現(xiàn)力更強蔼卡,
如果非常復雜,也可以考自定義DSL做為配置挣磨,
有時候這類配置也可以用Annotation代替雇逞,
因為這些配置和業(yè)務邏輯相關,放在代碼里也是合理的茁裙。

(3) 另外擴展配置塘砸,可能不盡相同,
如果只是策略接口實現(xiàn)類替換晤锥,可以考慮properties等結構掉蔬,
如果有復雜的生命周期管理,可能需要XML等配置矾瘾,
有時候擴展會通過注冊接口的方式提供女轿。

3. 配置加載

(1) 對于環(huán)境配置,
在java世界里霜威,比較常規(guī)的做法谈喳,
是在classpath下約定一個以項目為名稱的properties配置,
比如:log4j.properties戈泼,velocity.properties等婿禽,
產品在初始化時赏僧,自動從classpath下加載該配置,
我們平臺的很多項目也使用類似策略扭倾,
如:dubbo.properties淀零,comsat.xml等,
這樣有它的優(yōu)勢膛壹,就是基于約定驾中,簡化了用戶對配置加載過程的干預,
但同樣有它的缺點模聋,當classpath存在同樣的配置時肩民,可能誤加載,
以及在ClassLoader隔離時链方,可能找不到配置持痰,
并且,當用戶希望將配置放到統(tǒng)一的目錄時祟蚀,不太方便工窍。

Dubbo新版本去掉了dubbo.properties,因為該約定經常造成配置沖突前酿。

(2) 而對于描述配置患雏,
因為要參與業(yè)務邏輯,通常會嵌到應用的生命周期管理中罢维,
現(xiàn)在使用spring的項目越來越多淹仑,直接使用spring配置的比較普遍,
而且spring允許自定義schema言津,配置簡化后很方便攻人,
當然,也有它的缺點悬槽,就是強依賴spring怀吻,
可以提編程接口做了配套方案。

在Dubbo即存在描述配置初婆,也有環(huán)境配置蓬坡,
一部分用spring的schame配置加載,一部分從classpath掃描properties配置加載磅叛,
用戶感覺非常不便屑咳,所以在新版本中進行了合并,
統(tǒng)一放到spring的schame配置加載弊琴,也增加了配置的靈活性兆龙。

(3) 擴展配置,通常對配置的聚合要求比較高敲董,
因為產品需要發(fā)現(xiàn)第三方實現(xiàn)紫皇,將其加入產品內部慰安,
在java世里,通常是約定在每個jar包下放一個指定文件加載聪铺,
比如:eclipse的plugin.xml化焕,struts2的struts-plugin.xml等,
這類配置可以考慮java標準的服務發(fā)現(xiàn)機制铃剔,
即在jar包的META-INF/services下放置接口類全名文件撒桨,內容為每行一個實現(xiàn)類類名,
就像jdk中的加密算法擴展键兜,腳本引擎擴展凤类,新的JDBC驅動等,都是采用這種方式蝶押,
參見:ServiceProvider規(guī)范

Dubbo舊版本通過約定在每個jar包下踱蠢,
放置名為dubbo-context.xml的spring配置進行擴展與集成,
新版本改成用jdk自帶的META-INF/services方式棋电,
去掉過多的spring依賴。

4. 可編程配置

配置的可編程性是非常必要的苇侵,不管你以何種方式加載配置文件赶盔,
都應該提供一個編程的配置方式,允許用戶不使用配置文件榆浓,直接用代碼完成配置過程于未,
因為一個產品,尤其是組件類產品陡鹃,通常需要和其它產品協(xié)作使用烘浦,
當用戶集成你的產品時,可能需要適配配置方式萍鲸。

Dubbo新版本提供了與xml配置一對一的配置類闷叉,
如:ServiceConfig對應<dubbo:service />,并且屬性也一對一脊阴,
這樣有利于文件配置與編程配置的一致性理解握侧,減少學習成本。

5. 配置缺省值

配置的缺省值嘿期,通常是設置一個常規(guī)環(huán)境的合理值品擎,這樣可以減少用戶的配置量,
通常建議以線上環(huán)境為參考值备徐,開發(fā)環(huán)境可以通過修改配置適應萄传,
缺省值的設置,最好在最外層的配置加載就做處理蜜猾,
程序底層如果發(fā)現(xiàn)配置不正確秀菱,就應該直接報錯西设,容錯在最外層做,
如果在程序底層使用時答朋,發(fā)現(xiàn)配置值不合理贷揽,就填一個缺省值,
很容易掩蓋表面問題梦碗,而引發(fā)更深層次的問題禽绪,
并且配置的中間傳遞層,很可能并不知道底層使用了一個缺省值洪规,
一些中間的檢測條件就可能失效印屁,
Dubbo就出現(xiàn)過這樣的問題,中間層用“地址”做為緩存Key斩例,
而底層雄人,給“地址”加了一個缺省端口號,
導致不加端口號的“地址”和加了缺省端口的“地址”并沒有使用相同的緩存念赶。

6. 配置一致性

配置總會隱含一些風格或潛規(guī)則础钠,應盡可能保持其一致性,
比如:很多功能都有開關叉谜,然后有一個配置值:
(1) 是否使用注冊中心旗吁,注冊中心地址。
(2) 是否允許重試停局,重試次數(shù)很钓。
你可以約定:
(1) 每個都是先配置一個boolean類型的開關,再配置一個值董栽。
(2) 用一個無效值代表關閉码倦,N/A地址,0重試次數(shù)等锭碳。
不管選哪種方式袁稽,所有配置項,都應保持同一風格工禾,Dubbo選的是第二種运提,
相似的還有,超時時間闻葵,重試時間民泵,定時器間隔時間,
如果一個單位是秒槽畔,另一個單位是毫秒(C3P0的配置項就是這樣)栈妆,配置人員會瘋掉。

7. 配置覆蓋

提供配置時,要同時考慮開發(fā)人員鳞尔,測試人員嬉橙,配管人員,系統(tǒng)管理員寥假,
測試人員是不能修改代碼的市框,而測試的環(huán)境很可能較為復雜,
需要為測試人員留一些“后門”糕韧,可以在外圍修改配置項枫振,
就像spring的PropertyPlaceholderConfigurer配置,支持SYSTEM_PROPERTIES_MODE_OVERRIDE萤彩,
可以通過JVM的-D參數(shù)粪滤,或者像hosts一樣約定一個覆蓋配置文件,
在程序外部雀扶,修改部分配置杖小,便于測試。
Dubbo支持通過JVM參數(shù)-Dcom.xxx.XxxService=dubbo://10.1.1.1:1234
直接使遠程服務調用繞過注冊中心愚墓,進行點對點測試予权。
還有一種情況,開發(fā)人員增加配置時转绷,都會按線上的部署情況做配置伟件,如:
<dubbo:registry address="{dubbo.registry.address}" /> 因為線上只有一個注冊中心,這樣的配置是沒有問題的议经, 而測試環(huán)境可能有兩個注冊中心,測試人員不可能去修改配置谴返,改為: <dubbo:registry address="{dubbo.registry.address1}" />
<dubbo:registry address="{dubbo.registry.address2}" /> 所以這個地方煞肾,Dubbo支持在{dubbo.registry.address}的值中,
通過豎號分隔多個注冊中心地址嗓袱,用于表示多注冊中心地址籍救。

8. 配置繼承

配置也存在“重復代碼”,也存在“泛化與精化”的問題渠抹,
比如:Dubbo的超時時間設置蝙昙,每個服務,每個方法梧却,都應該可以設置超時時間奇颠,
但很多服務不關心超時,如果要求每個方法都配置放航,是不現(xiàn)實的烈拒,
所以Dubbo采用了,方法超時繼承服務超時莫杈,服務超時再繼承缺省超時术羔,沒配置時,一層層向上查找宣渗。

另外吨铸,Dubbo舊版本所有的超時時間行拢,重試次數(shù),負載均衡策略等都只能在服務消費方配置诞吱,
但實際使用過程中發(fā)現(xiàn)舟奠,服務提供方比消費方更清楚,但這些配置項是在消費方執(zhí)行時才用到的狐胎,
新版本鸭栖,就加入了在服務提供方也能配這些參數(shù),通過注冊中心傳遞到消費方握巢,
做為參考值晕鹊,如果消費方沒有配置,就以提供方的配置為準暴浦,相當于消費方繼承了提供方的建議配置值溅话,
而注冊中心在傳遞配置時,也可以在中途修改配置歌焦,這樣就達到了治理的目的飞几,繼承關系相當于:
服務消費者 --> 注冊中心 --> 服務提供者


image.png

9. 配置向后兼容

向前兼容很好辦,你只要保證配置只增不減独撇,就基本上能保證向前兼容屑墨,
但向后兼容,也是要注意的纷铣,要為后續(xù)加入新的配置項做好準備卵史,
如果配置出現(xiàn)一個特殊配置,就應該為這個“特殊”情況約定一個兼容規(guī)則搜立,
因為這個特殊情況以躯,很有可能在以后還會發(fā)生,
比如:有一個配置文件是保存“服務=地址”映射關系的啄踊,
其中有一行特殊忧设,保存的是“注冊中心=地址”,
現(xiàn)在程序加載時颠通,約定“注冊中心”這個Key是特殊的址晕,
做特別處理,其它的都是“服務”蒜哀,
然而斩箫,新版本發(fā)現(xiàn)吏砂,要加一項“監(jiān)控中心=地址”,
這時乘客,舊版本的程序會把“監(jiān)控中心”做為“服務”處理狐血,
因為舊代碼是不能改的,兼容性就很會很麻煩易核,
如果先前約定“特殊標識+XXX”為特殊處理匈织,后續(xù)就會方便很多。
向后兼容性牡直,可以多向HTML5學習缀匕,參見:HTML5設計原理


實現(xiàn)的健壯性

轉于自己在公司的Blog:http://pt.alibaba-inc.com/wp/experience_1224/robustness-of-implement.html

Dubbo作為遠程服務暴露、調用和治理的解決方案碰逸,是應用運轉的經絡乡小,其本身實現(xiàn)健壯性的重要程度是不言而喻的。

這里列出一些Dubbo用到的原則和方法饵史。

一满钟、日志

日志是發(fā)現(xiàn)問題、查看問題一個最常用的手段胳喷。

日志質量往往被忽視湃番,沒有日志使用上的明確約定。

重視Log的使用吭露,提高Log的信息濃度吠撮。

日志過多、過于混亂讲竿,會導致有用的信息被淹沒泥兰。

要有效利用這個工具要注意:

嚴格約定WARN、ERROR級別記錄的內容

  • WARN表示可以恢復的問題题禀,無需人工介入逾条。
  • ERROR表示需要人工介入問題。

有了這樣的約定投剥,監(jiān)管系統(tǒng)發(fā)現(xiàn)日志文件的中出現(xiàn)ERROR字串就報警,又盡量減少了發(fā)生担孔。

過多的報警會讓人疲倦江锨,使人對報警失去警惕性,使ERROR日志失去意義糕篇。

再輔以人工定期查看WARN級別信息啄育,以評估系統(tǒng)的“亞健康”程度。

日志中拌消,盡量多的收集關鍵信息

哪些是關鍵信息呢挑豌?

  • 出問題時的現(xiàn)場信息,即排查問題要用到的信息。如服務調用失敗時氓英,要給出 使用Dubbo的版本侯勉、服務提供者的IP、使用的是哪個注冊中心铝阐;調用的是哪個服務址貌、哪個方法等等。這些信息如果不給出徘键,那么事后人工收集的练对,問題過后現(xiàn)場可能已經不能復原,加大排查問題的難度吹害。
  • 如果可能螟凭,給出問題的原因和解決方法。這讓維護和問題解決變得簡單它呀,而不是尋求精通者(往往是實現(xiàn)者)的幫助螺男。

同一個或是一類問題不要重復記錄多次

同一個或是一類異常日志連續(xù)出現(xiàn)幾十遍的情況,還是常常能看到的钟些。人眼很容易漏掉淹沒在其中不一樣的重要日志信息烟号。要盡量避免這種情況。在可以預見會出現(xiàn)的情況政恍,有必要加一些邏輯來避免汪拥。

如為一個問題準備一個標志,出問題后打日志后設置標志篙耗,避免重復打日志迫筑。問題恢復后清除標志。

雖然有點麻煩宗弯,但是這樣做保證日志信息濃度脯燃,讓監(jiān)控更有效。

二蒙保、界限設置

資源是有限的辕棚,CPU、內存邓厕、IO等等逝嚎。不要因為外部的請求、數(shù)據(jù)不受限的而崩潰详恼。

線程池(ExectorService)的大小和飽和策略

Server端用于處理請求的ExectorService設置上限补君。

ExecutorService的任務等待隊列使用有限隊列,避免資源耗盡昧互。

當任務等待隊列飽和時挽铁,選擇一個合適的飽和策略伟桅。這樣保證平滑劣化。

在Dubbo中叽掘,飽和策略是丟棄數(shù)據(jù)楣铁,等待結果也只是請求的超時。

達到飽和時够掠,說明已經達到服務提供方的負荷上限民褂,要在飽和策略的操作中日志記錄這個問題,以發(fā)出監(jiān)控警報疯潭。

記得注意不要重復多次記錄哦赊堪。

(注意,缺省的飽和策略不會有這些附加的操作竖哩。)

根據(jù)警報的頻率哭廉,已經決定擴容調整等等,避免系統(tǒng)問題被忽略相叁。

集合容量

如果確保進入集合的元素是可控的且是足夠少遵绰,則可以放心使用。這是大部分的情況增淹。

如果不能保證椿访,則使用有有界的集合。當?shù)竭_界限時虑润,選擇一個合適的丟棄策略成玫。

三、容錯-重試-恢復

高可用組件要容忍其依賴組件的失敗拳喻。

Dubbo的服務注冊中心

目前服務注冊中心使用了數(shù)據(jù)庫來保存服務提供者和消費者的信息哭当;

注冊中心集群不同注冊中心也通過數(shù)據(jù)庫來之間同步數(shù)據(jù),以感知其它注冊中心上提供者冗澈。

注冊中心會內存中保證一份提供者和消費者數(shù)據(jù)钦勘,數(shù)據(jù)庫不可用時,注冊中心獨立對外正常運轉亚亲,只是拿不到其它注冊中心的數(shù)據(jù)彻采。

當數(shù)據(jù)庫恢復時,重試邏輯會內存中修改的數(shù)據(jù)寫回數(shù)據(jù)庫捌归,并拿到數(shù)據(jù)庫中新數(shù)據(jù)颊亮。

服務的消費者

服務消息者從注冊中心拿到提供者列表后,會保存提供者列表到內存和磁盤文件中陨溅。

這樣注冊中心宕后消費者可以正常運轉,甚至可以在注冊中心宕機過程中重啟消費者绍在。

消費者啟動時门扇,發(fā)現(xiàn)注冊中心不可用雹有,會讀取保存在磁盤文件中提供者列表。

重試邏輯保證注冊中心恢復后臼寄,更新信息霸奕。

四、重試延遲策略

上一點的子問題吉拳。Dubbo中碰到有兩個相關的場景质帅。

數(shù)據(jù)庫上的活鎖

注冊中心會定時更新數(shù)據(jù)庫一條記錄的時間戳,這樣集群中其它的注冊中心感知它是存活留攒。

過期注冊中心和它的相關數(shù)據(jù) 會被清除煤惩。數(shù)據(jù)庫正常時,這個機制運行良好炼邀。

但是數(shù)據(jù)庫負荷高時魄揉,其上的每個操作都會很慢。這就出現(xiàn):

A注冊中心認為B過期拭宁,刪除B的數(shù)據(jù)洛退。 B發(fā)現(xiàn)自己的數(shù)據(jù)沒有了,重新寫入自己的數(shù)據(jù)杰标。 的反復操作兵怯。這些反復的操作又加重了數(shù)據(jù)庫的負荷,惡化問題腔剂。

可以使用下面邏輯

當B發(fā)現(xiàn)自己數(shù)據(jù)被刪除時(寫入失斆角),選擇等待這段時間再重試桶蝎。

重試時間可以選擇指數(shù)級增長驻仅,如第一次等1分鐘,第二次10分鐘登渣、第三次100分鐘噪服。

這樣操作減少后,保證數(shù)據(jù)庫可以冷卻(Cool Down)下來胜茧。

Client重連注冊中心

當一個注冊中心停機時粘优,其它的Client會同時接收事件,而去重連另一個注冊中心呻顽。

Client數(shù)量相對比較多雹顺,會對注冊中心造成沖擊。

避免方法可以是Client重連時隨機延時3分鐘廊遍,把重連分散開嬉愧。


防癡呆設計

轉于自己在公司的Blog:
http://pt.alibaba-inc.com/wp/experience_1014/design-for-dummy.html

最近有點癡呆,因為解決了太多的癡呆問題喉前,
服務框架實施面超來超廣没酣,已有50多個項目在使用王财,
每天都要去幫應用查問題,來來回回裕便,
發(fā)現(xiàn)大部分都是配置錯誤绒净,或者重復的文件或類,或者網絡不通等偿衰,
所以準備在新版本中加入防癡呆設計挂疆,估且這么叫吧,
可能很簡單下翎,但對排錯速度還是有點幫助缤言,
希望能拋磚引玉,也希望大家多給力漏设,想出更多的防范措施共享出來墨闲。

(1) 檢查重復的jar包
最癡呆的問題,就是有多個版本的相同jar包郑口,
會出現(xiàn)新版本的A類鸳碧,調用了舊版本的B類,
而且和JVM加載順序有關犬性,問題帶有偶然性瞻离,誤導性,
遇到這種莫名其妙的問題乒裆,最頭疼套利,
所以,第一條鹤耍,先把它防住肉迫,
在每個jar包中挑一個一定會加載的類,加上重復類檢查稿黄,
給個示例:

static {  
    Duplicate.checkDuplicate(Xxx.class);  
} 

檢查重復工具類:

public final class Duplicate {

    private Duplicate() {}

    public static void checkDuplicate(Class cls) {
        checkDuplicate(cls.getName().replace('.', '/') + ".class");
    }

    public static void checkDuplicate(String path) {
        try {
            // 在ClassPath搜文件
            Enumeration urls = Thread.currentThread().getContextClassLoader().getResources(path);
            Set files = new HashSet();
            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                if (url != null) {
                    String file = url.getFile();
                    if (file != null &amp;&amp; file.length() &gt; 0) {
                        files.add(file);
                    }
                }
            }
            // 如果有多個喊衫,就表示重復
            if (files.size() &gt; 1) {
                logger.error("Duplicate class " + path + " in " + files.size() + " jar " + files);
            }
        } catch (Throwable e) { // 防御性容錯
            logger.error(e.getMessage(), e);
        }
    }

}

(2) 檢查重復的配置文件
配置文件加載錯,也是經常碰到的問題杆怕,
用戶通常會和你說:“我配置的很正確啊族购,不信我發(fā)給你看下,但就是報錯”陵珍,
然后查一圈下來寝杖,原來他發(fā)過來的配置根本沒加載,
平臺很多產品都會在classpath下放一個約定的配置互纯,
如果項目中有多個瑟幕,通常會取JVM加載的第一個,
為了不被這么低級的問題折騰,
和上面的重復jar包一樣收苏,在配置加載的地方亿卤,加上:

Duplicate.checkDuplicate("xxx.properties");  

(3) 檢查所有可選配置
必填配置估計大家都會檢查,因為沒有的話鹿霸,根本沒法運行,
但對一些可選參數(shù)秆乳,也應該做一些檢查懦鼠,
比如:服務框架允許通過注冊中心關聯(lián)服務消費者和服務提供者,
也允許直接配置服務提供者地址點對點直連屹堰,
這時候肛冶,注冊中心地址是可選的,
但如果沒有配點對點直連配置扯键,注冊中心地址就一定要配睦袖,
這時候也要做相應檢查。

(4) 異常信息給出解決方案
在給應用排錯時荣刑,最怕的就是那種只有簡單的一句錯誤描述馅笙,啥信息都沒有的異常信息,
比如上次碰到一個Failed to get session異常厉亏,
就這幾個單詞董习,啥都沒有,哪個session出錯? 什么原因Failed?
看了都快瘋掉爱只,因是線上環(huán)境不好調試皿淋,而且有些場景不是每次都能重現(xiàn),
異常最基本要帶有上下文信息恬试,包括操作者窝趣,操作目標,原因等训柴,
最好的異常信息哑舒,應給出解決方案,比如上面可以給出:
"從10.20.16.3到10.20.130.20:20880之間的網絡不通畦粮,
請在10.20.16.3使用telnet 10.20.130.20 20880測試一下網絡散址,
如果是跨機房調用,可能是防火墻阻擋宣赔,請聯(lián)系SA開通訪問權限"
等等预麸,上面甚至可以根據(jù)IP段判斷是不是跨機房。
另外一個例子儒将,是spring-web的context加載吏祸,
如果在getBean時spring沒有被啟動,
spring會報一個錯钩蚊,錯誤信息寫著:
請在web.xml中加入:<listener>...<init-param>...
多好的同學贡翘,看到錯誤的人復制一下就完事了蹈矮,我們該學學,
可以把常見的錯誤故意犯一遍鸣驱,看看錯誤信息能否自我搞定問題泛鸟,
或者把平時支持應用時遇到的問題及解決辦法都寫到異常信息里。

(5) 日志信息包含環(huán)境信息
每次應用一出錯踊东,應用的開發(fā)或測試就會把出錯信息發(fā)過來北滥,詢問原因,
這時候我都會問一大堆套話闸翅,
用的哪個版本呀再芋?
是生產環(huán)境還是開發(fā)測試環(huán)境?
哪個注冊中心呀坚冀?
哪個項目中的济赎?
哪臺機器呀?
哪個服務?
记某。司训。。
累啊辙纬,最主要的是豁遭,有些開發(fā)或測試人員根本分不清,
沒辦法贺拣,只好提供上門服務蓖谢,浪費的時間可不是浮云,
所以譬涡,日志中最好把需要的環(huán)境信息一并打進去闪幽,
最好給日志輸出做個包裝,統(tǒng)一處理掉涡匀,免得忘了盯腌。
包裝Logger接口如:

public void error(String msg, Throwable e) {  
    delegate.error(msg + " on server " + InetAddress.getLocalHost() + " using version " + Version.getVersion(), e);  
}  

獲取版本號工具類:

public final class Version {

    private Version() {}

    private static final Logger logger = LoggerFactory.getLogger(Version.class);

    private static final Pattern VERSION_PATTERN = Pattern.compile("([0-9][0-9\\.\\-]*)\\.jar");

    private static final String VERSION = getVersion(Version.class, "2.0.0");

    public static String getVersion(){
        return VERSION;
    }

    public static String getVersion(Class cls, String defaultVersion) {
        try {
            // 首先查找MANIFEST.MF規(guī)范中的版本號
            String version = cls.getPackage().getImplementationVersion();
            if (version == null || version.length() == 0) {
                version = cls.getPackage().getSpecificationVersion();
            }
            if (version == null || version.length() == 0) {
                // 如果MANIFEST.MF規(guī)范中沒有版本號,基于jar包名獲取版本號
                String file = cls.getProtectionDomain().getCodeSource().getLocation().getFile();
                if (file != null &amp;&amp; file.length() &gt; 0 &amp;&amp; file.endsWith(".jar")) {
                    Matcher matcher = VERSION_PATTERN.matcher(file);
                    while (matcher.find() &amp;&amp; matcher.groupCount() &gt; 0) {
                        version = matcher.group(1);
                    }
                }
            }
            // 返回版本號陨瘩,如果為空返回缺省版本號
            return version == null || version.length() == 0 ? defaultVersion : version;
        } catch (Throwable e) { // 防御性容錯
            // 忽略異常腕够,返回缺省版本號
            logger.error(e.getMessage(), e);
            return defaultVersion;
        }
    }

}

(6) kill之前先dump
每次線上環(huán)境一出問題,大家就慌了舌劳,
通常最直接的辦法回滾重啟帚湘,以減少故障時間,
這樣現(xiàn)場就被破壞了甚淡,要想事后查問題就麻煩了大诸,
有些問題必須在線上的大壓力下才會發(fā)生,
線下測試環(huán)境很難重現(xiàn),
不太可能讓開發(fā)或Appops在重啟前资柔,
先手工將出錯現(xiàn)場所有數(shù)據(jù)備份一下焙贷,
所以最好在kill腳本之前調用dump,
進行自動備份贿堰,這樣就不會有人為疏忽辙芍。
dump腳本示例:

JAVA_HOME=/usr/java
OUTPUT_HOME=~/output
DEPLOY_HOME=`dirname $0`
HOST_NAME=`hostname`

DUMP_PIDS=`ps  --no-heading -C java -f --width 1000 | grep "$DEPLOY_HOME" |awk '{print $2}'`
if [ -z "$DUMP_PIDS" ]; then
    echo "The server $HOST_NAME is not started!"
    exit 1;
fi

DUMP_ROOT=$OUTPUT_HOME/dump
if [ ! -d $DUMP_ROOT ]; then
    mkdir $DUMP_ROOT
fi

DUMP_DATE=`date +%Y%m%d%H%M%S`
DUMP_DIR=$DUMP_ROOT/dump-$DUMP_DATE
if [ ! -d $DUMP_DIR ]; then
    mkdir $DUMP_DIR
fi

echo -e "Dumping the server $HOST_NAME ...\c"
for PID in $DUMP_PIDS ; do
    $JAVA_HOME/bin/jstack $PID > $DUMP_DIR/jstack-$PID.dump 2>&1
    echo -e ".\c"
    $JAVA_HOME/bin/jinfo $PID > $DUMP_DIR/jinfo-$PID.dump 2>&1
    echo -e ".\c"
    $JAVA_HOME/bin/jstat -gcutil $PID > $DUMP_DIR/jstat-gcutil-$PID.dump 2>&1
    echo -e ".\c"
    $JAVA_HOME/bin/jstat -gccapacity $PID > $DUMP_DIR/jstat-gccapacity-$PID.dump 2>&1
    echo -e ".\c"
    $JAVA_HOME/bin/jmap $PID > $DUMP_DIR/jmap-$PID.dump 2>&1
    echo -e ".\c"
    $JAVA_HOME/bin/jmap -heap $PID > $DUMP_DIR/jmap-heap-$PID.dump 2>&1
    echo -e ".\c"
    $JAVA_HOME/bin/jmap -histo $PID > $DUMP_DIR/jmap-histo-$PID.dump 2>&1
    echo -e ".\c"
    if [ -r /usr/sbin/lsof ]; then
    /usr/sbin/lsof -p $PID > $DUMP_DIR/lsof-$PID.dump
    echo -e ".\c"
    fi
done
if [ -r /usr/bin/sar ]; then
/usr/bin/sar > $DUMP_DIR/sar.dump
echo -e ".\c"
fi
if [ -r /usr/bin/uptime ]; then
/usr/bin/uptime > $DUMP_DIR/uptime.dump
echo -e ".\c"
fi
if [ -r /usr/bin/free ]; then
/usr/bin/free -t > $DUMP_DIR/free.dump
echo -e ".\c"
fi
if [ -r /usr/bin/vmstat ]; then
/usr/bin/vmstat > $DUMP_DIR/vmstat.dump
echo -e ".\c"
fi
if [ -r /usr/bin/mpstat ]; then
/usr/bin/mpstat > $DUMP_DIR/mpstat.dump
echo -e ".\c"
fi
if [ -r /usr/bin/iostat ]; then
/usr/bin/iostat > $DUMP_DIR/iostat.dump
echo -e ".\c"
fi
if [ -r /bin/netstat ]; then
/bin/netstat > $DUMP_DIR/netstat.dump
echo -e ".\c"
fi
echo "OK!"


Dubbo擴展點重構

轉于自己在公司的Blog:
http://pt.alibaba-inc.com/wp/dev_related_1283/dubbo-extension.html

隨著服務化的推廣,網站對Dubbo服務框架的需求逐漸增多羹与,
Dubbo的現(xiàn)有開發(fā)人員能實現(xiàn)的需求有限沸手,很多需求都被delay,
而網站的同學也希望參與進來注簿,加上領域的推動,
所以平臺計劃將部分項目對公司內部開放跳仿,讓大家一起來實現(xiàn)诡渴,
Dubbo為試點項目之一。

既然要開放菲语,那Dubbo就要留一些擴展點妄辩,
讓參與者盡量黑盒擴展,而不是白盒的修改代碼山上,
否則分支眼耀,質量,合并佩憾,沖突都會很難管理拍顷。

先看一下Dubbo現(xiàn)有的設計:


image.png

這里面雖然有部分擴展接口磅甩,但并不能很好的協(xié)作,
而且擴展點的加載和配置都沒有統(tǒng)一處理,所以下面對它進行重構又谋。

第一步,微核心争占,插件式晰奖,平等對待第三方。

即然要擴展致盟,擴展點的加載方式碎税,首先要統(tǒng)一,
微核心+插件式馏锡,是比較能達到OCP原則的思路雷蹂,

由一個插件生命周期管理容器,構成微核心眷篇,
核心不包括任何功能萎河,這樣可以確保所有功能都能被替換,
并且,框架作者能做到的功能虐杯,擴展者也一定要能做到玛歌,以保證平等對待第三方,
所以擎椰,框架自身的功能也要用插件的方式實現(xiàn)支子,不能有任何硬編碼。

通常微核心都會采用Factory达舒,IoC值朋,OSGi等方式管理插件生命周期,
考慮Dubbo的適用面巩搏,不想強依賴Spring等IoC容器昨登,
自已造一個小的IoC容器,也覺得有點過度設計贯底,
所以打算采用最簡單的Factory方式管理插件丰辣,

最終決定采用的是JDK標準的SPI擴展機制,參見:java.util.ServiceLoader
也就是擴展者在jar包的META-INF/services/目錄下放置與接口同名的文本文件禽捆,
內容為接口實現(xiàn)類名笙什,多個實現(xiàn)類名用換行符分隔,
比如胚想,需要擴展Dubbo的協(xié)議琐凭,只需在xxx.jar中放置:
文件:META-INF/services/com.alibaba.dubbo.rpc.Protocol
內容為:com.alibaba.xxx.XxxProtocol
Dubbo通過ServiceLoader掃描到所有Protocol實現(xiàn)。

并約定所有插件浊服,都必須標注:@Extension("name"),
作為加載后的標識性名稱臼闻,用于配置選擇鸿吆。

第二步,每個擴展點只封裝一個變化因子述呐,最大化復用惩淳。

每個擴展點的實現(xiàn)者,往往都只是關心一件事乓搬,
現(xiàn)在的擴展點思犁,并沒有完全分離,

比如:Failover, Route, LoadBalance, Directory沒有完全分開进肯,全由RoutingInvokerGroup寫死了激蹲。

再比如,協(xié)議擴展江掩,擴展者可能只是想替換序列化方式学辱,或者只替換傳輸方式乘瓤,
并且Remoting和Http也能復用序列化等實現(xiàn),
這樣策泣,需為傳輸方式衙傀,客戶端實現(xiàn),服務器端實現(xiàn)萨咕,協(xié)議頭解析统抬,數(shù)據(jù)序列化,都留出不同擴展點危队。

拆分后聪建,設計如下:


image.png

第三步,全管道式設計茫陆,框架自身邏輯金麸,均使用截面攔截實現(xiàn)。

現(xiàn)在很多的邏輯簿盅,都是放在基類中實現(xiàn)钱骂,然后通過模板方法回調子類的實現(xiàn),
包括:local, mock, generic, echo, token, accesslog, monitor, count, limit等等挪鹏,
可以全部拆分使用Filter實現(xiàn),每個功能都是調用鏈上的一環(huán)愉烙。

比如:(基類模板方法)

public abstract AbstractInvoker implements Invoker {

    public Result invoke(Invocation inv) throws RpcException {
        // 偽代碼
        active ++;
        if (active > max)
            wait();
        
        doInvoke(inv);
        
        active --;
        notify();
    }
    
    protected abstract Result doInvoke(Invocation inv) throws RpcException

}

改成:(鏈式過濾器)

public abstract LimitFilter implements Filter {

    public Result invoke(Invoker chain, Invocation inv) throws RpcException {
         // 偽代碼
        active ++;
        if (active > max)
            wait();
        
        chain.invoke(inv);
        
        active --;
        notify();
    }

}

第四步讨盒,最少概念,一致性概念模型步责。

保持盡可能少的概念返顺,有助于理解,對于開放的系統(tǒng)尤其重要蔓肯,
另外遂鹊,各接口都使用一致的概念模型,能相互指引蔗包,并減少模型轉換秉扑,

比如,Invoker的方法簽名為:

Result invoke(Invocation invocation) throws RpcException;

而Exporter的方法簽名為:

Object invoke(Method method, Object[] args) throws Throwable;

但它們的作用是一樣的调限,只是一個在客戶端舟陆,一個在服務器端,卻采用了不一樣的模型類耻矮。

再比如秦躯,URL以字符串傳遞,不停的解析和拼裝裆装,沒有一個URL模型類踱承, 而URL的參數(shù)倡缠,卻時而Map, 時而Parameters類包裝,

export(String url)
createExporter(String host, int port, Parameters params);

使用一致模型:

export(URL url)
createExporter(URL url);

再比如茎活,現(xiàn)有的:Invoker, Exporter, InvocationHandler, FilterChain
其實都是invoke行為的不同階段昙沦,完全可以抽象掉,統(tǒng)一為Invoker妙色,減少概念桅滋。

第五步,分層身辨,組合式擴展丐谋,而不是泛化式擴展。
原因參見:http://javatar.iteye.com/blog/690845
泛化式擴展指:將擴展點逐漸抽象煌珊,取所有功能并集号俐,新加功能總是套入并擴充舊功能的概念。
組合式擴展指:將擴展點正交分解定庵,取所有功能交集吏饿,新加功能總是基于舊功能之上實現(xiàn)。
上面的設計蔬浙,不自覺的就將Dubbo現(xiàn)有功能都當成了核心功能猪落,
上面的概念包含了Dubbo現(xiàn)有RPC的所有功能,包括:Proxy, Router, Failover, LoadBalance, Subscriber, Publisher, Invoker, Exporter, Filter等畴博,
但這些都是核心嗎笨忌?踢掉哪些,RPC一樣可以Run俱病?而哪些又是不能踢掉的官疲?
基于這樣考慮,可以將RPC分解成兩個層次亮隙,只是Protocol和Invoker才是RPC的核心途凫,
其它,包括Router, Failover, Loadbalance, Subscriber, Publisher都不核心溢吻,而是Routing维费,
所以,將Routing作為Rpc核心的一個擴展促王,設計如下:

image.png

第六步掩完,整理,梳理關系硼砰。

整理后且蓬,設計如下:


image.png

個人介紹:

高廣超:多年一線互聯(lián)網研發(fā)與架構設計經驗,擅長設計與落地高可用题翰、高性能恶阴、可擴展的互聯(lián)網架構诈胜。

本文首發(fā)在 高廣超的簡書博客 轉載請注明!

簡書博客
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末冯事,一起剝皮案震驚了整個濱河市焦匈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌昵仅,老刑警劉巖缓熟,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異摔笤,居然都是意外死亡够滑,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進店門吕世,熙熙樓的掌柜王于貴愁眉苦臉地迎上來彰触,“玉大人,你說我怎么就攤上這事命辖】鲆悖” “怎么了?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵尔艇,是天一觀的道長尔许。 經常有香客問我,道長终娃,這世上最難降的妖魔是什么母债? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮尝抖,結果婚禮上,老公的妹妹穿的比我還像新娘迅皇。我一直安慰自己昧辽,他們只是感情好,可當我...
    茶點故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布登颓。 她就那樣靜靜地躺著搅荞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪框咙。 梳的紋絲不亂的頭發(fā)上咕痛,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天,我揣著相機與錄音喇嘱,去河邊找鬼茉贡。 笑死,一個胖子當著我的面吹牛者铜,可吹牛的內容都是我干的腔丧。 我是一名探鬼主播放椰,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼愉粤!你這毒婦竟也來了砾医?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤衣厘,失蹤者是張志新(化名)和其女友劉穎如蚜,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體影暴,經...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡错邦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了坤检。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片兴猩。...
    茶點故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖早歇,靈堂內的尸體忽然破棺而出倾芝,到底是詐尸還是另有隱情,我是刑警寧澤箭跳,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布晨另,位于F島的核電站,受9級特大地震影響谱姓,放射性物質發(fā)生泄漏借尿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一屉来、第九天 我趴在偏房一處隱蔽的房頂上張望路翻。 院中可真熱鬧,春花似錦茄靠、人聲如沸茂契。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽掉冶。三九已至,卻和暖如春脐雪,著一層夾襖步出監(jiān)牢的瞬間厌小,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工战秋, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留璧亚,地道東北人。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓脂信,卻偏偏與公主長得像涨岁,于是被迫代替她去往敵國和親拐袜。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,500評論 2 359

推薦閱讀更多精彩內容

  • Dubbo是什么 Dubbo是Alibaba開源的分布式服務框架梢薪,它最大的特點是按照分層的方式來架構蹬铺,使用這種方式...
    Coselding閱讀 17,227評論 3 196
  • 本文轉自:Dubbo架構設計詳解,原作者是:時延軍 Dubbo是Alibaba開源的分布式服務框架秉撇,它最大的特點是...
    程序熊大閱讀 3,469評論 3 45
  • 下午來了家長會甜攀,想要家長和我多溝通,最后還是只有單方面的演講琐馆。
    亦荼閱讀 160評論 0 0
  • 教書那段時間,養(yǎng)成了每晚睡前看書滋饲,11點之前進入睡眠的好習慣厉碟,但總是不能夠在7點之前起床呼吸一下清晨的空氣。人啊屠缭,...
    六千圈閱讀 426評論 0 0
  • 大概很多工程師都有這個夢想吧箍鼓。 自己開一個軟件公司, 寫一個軟件改變世界呵曹。 我畢業(yè)后第一份工作是一家10幾個人的小...
    心___塵閱讀 450評論 0 1