Dubbo作者聊 設(shè)計(jì)原則

以下內(nèi)容均來自 梁飛 的個(gè)人博客 http://javatar.iteye.com/blog/1056664

  • 魔鬼在細(xì)節(jié)
  • 一些設(shè)計(jì)上的基本常識(shí)
  • 談?wù)剶U(kuò)充式擴(kuò)展與增量式擴(kuò)展
  • 配置設(shè)計(jì)
  • 設(shè)計(jì)實(shí)現(xiàn)的健壯性
  • 防癡呆設(shè)計(jì)
  • 擴(kuò)展點(diǎn)重構(gòu)

魔鬼在細(xì)節(jié)中

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

最近一直擔(dān)心Dubbo分布式服務(wù)框架后續(xù)如果維護(hù)人員增多或變更,會(huì)出現(xiàn)質(zhì)量的下降,
我在想,有沒有什么是需要大家共同遵守的,
根據(jù)平時(shí)寫代碼時(shí)的一習(xí)慣,總結(jié)了一下在寫代碼過程中,尤其是框架代碼,要時(shí)刻牢記的細(xì)節(jié),
可能下面要講的這些,大家都會(huì)覺得很簡單,很基礎(chǔ),但要做到時(shí)刻牢記,
在每一行代碼中都考慮這些因素,是需要很大耐心的,
大家經(jīng)常說,魔鬼在細(xì)節(jié)中,確實(shí)如此。

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

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

3. 盡早失敗和前置斷言
盡早失敗也應(yīng)該成為潛意識(shí),在有傳入?yún)?shù)和狀態(tài)變化時(shí),均在入口處全部斷言,
一個(gè)不合法的值和狀態(tài),在第一時(shí)間就應(yīng)報(bào)錯(cuò),而不是等到要用時(shí)才報(bào)錯(cuò),
因?yàn)榈鹊揭脮r(shí),可能前面已經(jīng)修改其它相關(guān)狀態(tài),而在程序中很少有人去處理回滾邏輯,
這樣報(bào)錯(cuò)后,其實(shí)內(nèi)部狀態(tài)可能已經(jīng)混亂,極易在一個(gè)隱蔽分支上引發(fā)程序不可恢復(fù)。

4. 分離可靠操作和不可靠操作
這里的可靠是狹義的指是否會(huì)拋出異?;蛞馉顟B(tài)不一致,
比如,寫入一個(gè)線程安全的Map,可以認(rèn)為是可靠的,
而寫入數(shù)據(jù)庫等,可以認(rèn)為是不可靠的,
開發(fā)人員必須在寫每一行代碼時(shí),都注意它的可靠性與否,
在代碼中盡量劃分開,并對(duì)失敗做異常處理,
并為容錯(cuò),自我保護(hù),自動(dòng)恢復(fù)或切換等補(bǔ)償邏輯提供清晰的切入點(diǎn),
保證后續(xù)增加的代碼不至于放錯(cuò)位置,而導(dǎo)致原先的容錯(cuò)處理陷入混亂。

5. 異常防御,但不忽略異常
這里講的異常防御,指的是對(duì)非必須途徑上的代碼進(jìn)行最大限度的容忍,
包括程序上的BUG,比如:獲取程序的版本號(hào),會(huì)通過掃描Manifest和jar包名稱抓取版本號(hào),
這個(gè)邏輯是輔助性的,但代碼卻不少,初步測(cè)試也沒啥問題,
但應(yīng)該在整個(gè)getVersion()中加上一個(gè)全函數(shù)的try-catch打印錯(cuò)誤日志,并返回基本版本,
因?yàn)間etVersion()可能存在未知特定場(chǎng)景異常,或被其他的開發(fā)人員誤修改邏輯(但一般人員不會(huì)去掉try-catch),
而如果它拋出異常會(huì)導(dǎo)致主流程異常,這是我們不希望看到的,
但這里要控制個(gè)度,不要隨意try-catch,更不要無聲無息的吃掉異常。

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

7. 降低修改時(shí)的誤解性,不埋雷
前面不停的提到代碼被其他人修改,這也開發(fā)人員要隨時(shí)緊記的,
這個(gè)其他人包括未來的自己,你要總想著這個(gè)代碼可能會(huì)有人去改它,
我應(yīng)該給修改的人一點(diǎn)什么提示,讓他知道我現(xiàn)在的設(shè)計(jì)意圖,
而不要在程序里面加潛規(guī)則,或埋一些容易忽視的雷,
比如:你用null表示不可用,size等于0表示黑名單,
這就是一個(gè)雷,下一個(gè)修改者,包括你自己,都不會(huì)記得有這樣的約定,
可能后面為了改某個(gè)其它BUG,不小心改到了這里,直接引爆故障。
對(duì)于這個(gè)例子,一個(gè)原則就是永遠(yuǎn)不要區(qū)分null引用和empty值。

8. 提高代碼的可測(cè)性
這里的可測(cè)性主要指Mock的容易程度,和測(cè)試的隔離性,
至于測(cè)試的自動(dòng)性,可重復(fù)性,非偶然性,無序性,完備性(全覆蓋),輕量性(可快速執(zhí)行),
一般開發(fā)人員,加上JUnit等工具的輔助基本都能做到,也能理解它的好處,只是工作量問題,
這里要特別強(qiáng)調(diào)的是測(cè)試用例的單一性(只測(cè)目標(biāo)類本身)和隔離性(不傳染失敗),
現(xiàn)在的測(cè)試代碼,過于強(qiáng)調(diào)完備性,大量重復(fù)交叉測(cè)試,
看起來沒啥壞處,但測(cè)試代碼越多,維護(hù)代價(jià)越高,
經(jīng)常出現(xiàn)的問題是,修改一行代碼或加一個(gè)判斷條件,引起100多個(gè)測(cè)試用例不通過,
時(shí)間一緊,誰有這個(gè)閑功夫去改這么多形態(tài)各異的測(cè)試用例?
久而久之,這個(gè)測(cè)試代碼就已經(jīng)不能真實(shí)反應(yīng)代碼現(xiàn)在的狀況,很多時(shí)候會(huì)被迫繞過,
最好的情況是,修改一行代碼,有且只有一行測(cè)試代碼不通過,
如果修改了代碼而測(cè)試用例還能通過,那也不行,表示測(cè)試沒有覆蓋到,
另外,可Mock性是隔離的基礎(chǔ),把間接依賴的邏輯屏蔽掉,
可Mock性的一個(gè)最大的殺手就是靜態(tài)方法,盡量少用。


一些設(shè)計(jì)上的基本常識(shí)

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

最近給團(tuán)隊(duì)新人講了一些設(shè)計(jì)上的常識(shí),可能會(huì)對(duì)其它的新人也有些幫助,
把暫時(shí)想到的幾條,先記在這里。

1. API與SPI分離

框架或組件通常有兩類客戶,一個(gè)是使用者,一個(gè)是擴(kuò)展者,
API(Application Programming Interface)是給使用者用的,
而SPI(Service Provide Interface)是給擴(kuò)展者用的,
在設(shè)計(jì)時(shí),盡量把它們隔離開,而不要混在一起,
也就是說,使用者是看不到擴(kuò)展者寫的實(shí)現(xiàn)的,
比如:一個(gè)Web框架,它有一個(gè)API接口叫Action,
里面有個(gè)execute()方法,是給使用者用來寫業(yè)務(wù)邏輯的,
然后,Web框架有一個(gè)SPI接口給擴(kuò)展者控制輸出方式,
比如用velocity模板輸出還是用json輸出等,
如果這個(gè)Web框架使用一個(gè)都繼承Action的VelocityAction和一個(gè)JsonAction做為擴(kuò)展方式,
要用velocity模板輸出的就繼承VelocityAction,要用json輸出的就繼承JsonAction,
這就是API和SPI沒有分離的反面例子,SPI接口混在了API接口中,
合理的方式是,有一個(gè)單獨(dú)的Renderer接口,有VelocityRenderer和JsonRenderer實(shí)現(xiàn),
Web框架將Action的輸出轉(zhuǎn)交給Renderer接口做渲染輸出。

image.png
image.png

2. 服務(wù)域/實(shí)體域/會(huì)話域分離

任何框架或組件,總會(huì)有核心領(lǐng)域模型,比如:
Spring的Bean,Struts的Action,Dubbo的Service,Napoli的Queue等等
這個(gè)核心領(lǐng)域模型及其組成部分稱為實(shí)體域,它代表著我們要操作的目標(biāo)本身,
實(shí)體域通常是線程安全的,不管是通過不變類,同步狀態(tài),或復(fù)制的方式,
服務(wù)域也就是行為域,它是組件的功能集,同時(shí)也負(fù)責(zé)實(shí)體域和會(huì)話域的生命周期管理,
比如Spring的ApplicationContext,Dubbo的ServiceManager等,
服務(wù)域的對(duì)象通常會(huì)比較重,而且是線程安全的,并以單一實(shí)例服務(wù)于所有調(diào)用,
什么是會(huì)話?就是一次交互過程,
會(huì)話中重要的概念是上下文,什么是上下文?
比如我們說:“老地方見”,這里的“老地方”就是上下文信息,
為什么說“老地方”對(duì)方會(huì)知道,因?yàn)槲覀兦懊娑x了“老地方”的具體內(nèi)容,
所以說,上下文通常持有交互過程中的狀態(tài)變量等,
會(huì)話對(duì)象通常較輕,每次請(qǐng)求都重新創(chuàng)建實(shí)例,請(qǐng)求結(jié)束后銷毀。
簡而言之:
把元信息交由實(shí)體域持有,
把一次請(qǐng)求中的臨時(shí)狀態(tài)由會(huì)話域持有,
由服務(wù)域貫穿整個(gè)過程。

image.png
image.png

3. 在重要的過程上設(shè)置攔截接口

如果你要寫個(gè)遠(yuǎn)程調(diào)用框架,那遠(yuǎn)程調(diào)用的過程應(yīng)該有一個(gè)統(tǒng)一的攔截接口,
如果你要寫一個(gè)ORM框架,那至少SQL的執(zhí)行過程,Mapping過程要有攔截接口,
如果你要寫一個(gè)Web框架,那請(qǐng)求的執(zhí)行過程應(yīng)該要有攔截接口,
等等,沒有哪個(gè)公用的框架可以Cover住所有需求,允許外置行為,是框架的基本擴(kuò)展方式,
這樣,如果有人想在遠(yuǎn)程調(diào)用前,驗(yàn)證下令牌,驗(yàn)證下黑白名單,統(tǒng)計(jì)下日志,
如果有人想在SQL執(zhí)行前加下分頁包裝,做下數(shù)據(jù)權(quán)限控制,統(tǒng)計(jì)下SQL執(zhí)行時(shí)間,
如果有人想在請(qǐng)求執(zhí)行前檢查下角色,包裝下輸入輸出流,統(tǒng)計(jì)下請(qǐng)求量,
等等,就可以自行完成,而不用侵入框架內(nèi)部,
攔截接口,通常是把過程本身用一個(gè)對(duì)象封裝起來,傳給攔截器鏈,
比如:遠(yuǎn)程調(diào)用主過程為invoke(),那攔截器接口通常為invoke(Invocation),
Invocation對(duì)象封裝了本來要執(zhí)行過程的上下文,并且Invocation里有一個(gè)invoke()方法,
由攔截器決定什么時(shí)候執(zhí)行,同時(shí),Invocation也代表攔截器行為本身,
這樣上一攔截器的Invocation其實(shí)是包裝的下一攔截器的過程,
直到最后一個(gè)攔截器的Invocation是包裝的最終的invoke()過程,
同理,SQL主過程為execute(),那攔截器接口通常為execute(Execution),原理一樣,
當(dāng)然,實(shí)現(xiàn)方式可以任意,上面只是舉例。

image.png

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

這里先要講一個(gè)事件和上面攔截器的區(qū)別,攔截器是干預(yù)過程的,它是過程的一部分,是基于過程行為的,
而事件是基于狀態(tài)數(shù)據(jù)的,任何行為改變的相同狀態(tài),對(duì)事件應(yīng)該是一致的,
事件通常是事后通知,是一個(gè)Callback接口,方法名通常是過去式的,比如onChanged(),
比如遠(yuǎn)程調(diào)用框架,當(dāng)網(wǎng)絡(luò)斷開或連上應(yīng)該發(fā)出一個(gè)事件,當(dāng)出現(xiàn)錯(cuò)誤也可以考慮發(fā)出一個(gè)事件,
這樣外圍應(yīng)用就有可能觀察到框架內(nèi)部的變化,做相應(yīng)適應(yīng)。

image.png

5. 擴(kuò)展接口職責(zé)盡可能單一,具有可組合性

比如,遠(yuǎn)程調(diào)用框架它的協(xié)議是可以替換的,
如果只提供一個(gè)總的擴(kuò)展接口,當(dāng)然可以做到切換協(xié)議,
但協(xié)議支持是可以細(xì)分為底層通訊,序列化,動(dòng)態(tài)代理方式等等,
如果將接口拆細(xì),正交分解,會(huì)更便于擴(kuò)展者復(fù)用已有邏輯,而只是替換某部分實(shí)現(xiàn)策略,
當(dāng)然這個(gè)分解的粒度需要把握好。

6. 微核插件式,平等對(duì)待第三方

大凡發(fā)展的比較好的框架,都遵守微核的理念,
Eclipse的微核是OSGi, Spring的微核是BeanFactory,Maven的微核是Plexus,
通常核心是不應(yīng)該帶有功能性的,而是一個(gè)生命周期和集成容器,
這樣各功能可以通過相同的方式交互及擴(kuò)展,并且任何功能都可以被替換,
如果做不到微核,至少要平等對(duì)待第三方,
即原作者能實(shí)現(xiàn)的功能,擴(kuò)展者應(yīng)該可以通過擴(kuò)展的方式全部做到,
原作者要把自己也當(dāng)作擴(kuò)展者,這樣才能保證框架的可持續(xù)性及由內(nèi)向外的穩(wěn)定性。

7. 不要控制外部對(duì)象的生命周期

比如上面說的Action使用接口和Renderer擴(kuò)展接口,
框架如果讓使用者或擴(kuò)展者把Action或Renderer實(shí)現(xiàn)類的類名或類元信息報(bào)上來,
然后在內(nèi)部通過反射newInstance()創(chuàng)建一個(gè)實(shí)例,
這樣框架就控制了Action或Renderer實(shí)現(xiàn)類的生命周期,
Action或Renderer的生老病死,框架都自己做了,外部擴(kuò)展或集成都無能為力,
好的辦法是讓使用者或擴(kuò)展者把Action或Renderer實(shí)現(xiàn)類的實(shí)例報(bào)上來,
框架只是使用這些實(shí)例,這些對(duì)象是怎么創(chuàng)建的,怎么銷毀的,都和框架無關(guān),
框架最多提供工具類輔助管理,而不是絕對(duì)控制。

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

因?yàn)槭褂铆h(huán)境的不確定因素很多,框架總會(huì)有一些配置,
一般都會(huì)到classpath直掃某個(gè)指定名稱的配置,或者啟動(dòng)時(shí)允許指定配置路徑,
做為一個(gè)通用框架,應(yīng)該做到凡是能配置文件做的一定要能通過編程方式進(jìn)行,
否則當(dāng)使用者需要將你的框架與另一個(gè)框架集成時(shí)就會(huì)帶來很多不必要的麻煩,
另外,盡可能做一個(gè)標(biāo)準(zhǔn)約定,如果用戶按某種約定做事時(shí),就不需要該配置項(xiàng)。
比如:配置模板位置,你可以約定,如果放在templates目錄下就不用配了,
如果你想換個(gè)目錄,就配置下。

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

這個(gè)是契約式設(shè)計(jì)的一部分,盡量遵守有返回值的方法是查詢方法,void返回的方法是命令,
查詢方法通常是冪等性的,無副作用的,也就是不改變?nèi)魏螤顟B(tài),調(diào)n次結(jié)果都是一樣的,
比如get某個(gè)屬性值,或查詢一條數(shù)據(jù)庫記錄,
命令是指有副作用的,也就是會(huì)修改狀態(tài),比如set某個(gè)值,或update某條數(shù)據(jù)庫記錄,
如果你的方法即做了修改狀態(tài)的操作,又做了查詢返回,如果可能,將其拆成寫讀分離的兩個(gè)方法,
比如:User deleteUser(id),刪除用戶并返回被刪除的用戶,考慮改為getUser()和void的deleteUser()。
另外,每個(gè)方法都盡量前置斷言傳入?yún)?shù)的合法性,后置斷言返回結(jié)果的合法性,并文檔化。

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


談?wù)剶U(kuò)充式擴(kuò)展與增量式擴(kuò)展

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

我們平臺(tái)的產(chǎn)品越來越多,產(chǎn)品的功能也越來越多,
平臺(tái)的產(chǎn)品為了適應(yīng)各BU和部門以及產(chǎn)品線的需求,
勢(shì)必會(huì)將很多不相干的功能湊在一起,客戶可以選擇性的使用,
為了兼容更多的需求,每個(gè)產(chǎn)品,每個(gè)框架,都在不停的擴(kuò)展,
而我們經(jīng)常會(huì)選擇一些擴(kuò)展的擴(kuò)展方式,也就是將新舊功能擴(kuò)展成一個(gè)通用實(shí)現(xiàn),
我想討論是,有些情況下也可以考慮增量式的擴(kuò)展方式,也就是保留原功能的簡單性,新功能獨(dú)立實(shí)現(xiàn),
我最近一直做分布式服務(wù)框架的開發(fā),就拿我們項(xiàng)目中的問題開涮吧。

比如:遠(yuǎn)程調(diào)用框架,肯定少不了序列化功能,功能很簡單,就是把流轉(zhuǎn)成對(duì)象,對(duì)象轉(zhuǎn)成流,
但因有些地方可能會(huì)使用osgi,這樣序列化時(shí),IO所在的ClassLoader可能和業(yè)務(wù)方的ClassLoader是隔離的,
需要將流轉(zhuǎn)換成byte[]數(shù)組,然后傳給業(yè)務(wù)方的ClassLoader進(jìn)行序列化,
為了適應(yīng)osgi需求,把原來非osgi與osgi的場(chǎng)景擴(kuò)展了一下,
這樣,不管是不是osgi環(huán)境,都先將流轉(zhuǎn)成byte[]數(shù)組,拷貝一次,
然而,大部分場(chǎng)景都用不上osgi,卻為osgi付出了代價(jià),
而如果采用增量式擴(kuò)展方式,非osgi的代碼原封不動(dòng),
再加一個(gè)osgi的實(shí)現(xiàn),要用osgi的時(shí)候,直接依賴osgi實(shí)現(xiàn)即可。

再比如:最開始,遠(yuǎn)程服務(wù)都是基于接口方法,進(jìn)行透明化調(diào)用的,
這樣,擴(kuò)展接口就是,invoke(Method method, Object[] args),
后來,有了無接口調(diào)用的需求,就是沒有接口方法也能調(diào)用,并將POJO對(duì)象都轉(zhuǎn)換成Map表示,
因?yàn)镸ethod對(duì)象是不能直接new出來的,我們不自覺選了一個(gè)擴(kuò)展式擴(kuò)展,
把擴(kuò)展接口改成了invoke(String methodName, String[] parameterTypes, String returnTypes, Object[] args),
導(dǎo)致不管是不是無接口調(diào)用,都得把parameterTypes從Class[]轉(zhuǎn)成String[],
如果選用增量式擴(kuò)展,應(yīng)該是保持原有接口不變,
增加一個(gè)GeneralService接口,里面有一個(gè)通用的invoke()方法,
和其它正常業(yè)務(wù)上的接口一樣的調(diào)用方式,擴(kuò)展接口也不用變,
只是GeneralServiceImpl的invoke()實(shí)現(xiàn)會(huì)將收到的調(diào)用轉(zhuǎn)給目標(biāo)接口,
這樣就能將新功能增量到舊功能上,并保持原來結(jié)構(gòu)的簡單性。

再再比如:無狀態(tài)消息發(fā)送,很簡單,序列化一個(gè)對(duì)象發(fā)過去就行,
后來有了同步消息發(fā)送需求,需要一個(gè)Request/Response進(jìn)行配對(duì),
采用擴(kuò)展式擴(kuò)展,自然想到,無狀態(tài)消息其實(shí)是一個(gè)沒有Response的Request,
所以在Request里加一個(gè)boolean狀態(tài),表示要不要返回Response,
如果再來一個(gè)會(huì)話消息發(fā)送需求,那就再加一個(gè)Session交互,
然后發(fā)現(xiàn),原來同步消息發(fā)送是會(huì)話消息的一種特殊情況,
所有場(chǎng)景都傳Session,不需要Session的地方無視即可。
如果采用增量式擴(kuò)展,無狀態(tài)消息發(fā)送原封不動(dòng),
同步消息發(fā)送,在無狀態(tài)消息基礎(chǔ)上加一個(gè)Request/Response處理,
會(huì)話消息發(fā)送,再加一個(gè)SessionRequest/SessionResponse處理。

image.png
image.png

配置設(shè)計(jì)

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

Dubbo現(xiàn)在的設(shè)計(jì)是完全無侵入,也就是使用者只依賴于配置契約,
經(jīng)過多個(gè)版本的發(fā)展,為了滿足各種需求場(chǎng)景,配置越來越多,
為了保持兼容,配置只增不減,里面潛伏著各種風(fēng)格,約定,規(guī)則,
新版本也將配置做了一次調(diào)整,去掉了dubbo.properties,改為全spring配置,
將想到的一些記在這,備忘。

1. 配置分類

首先,配置的用途是有多種的,大致可以分為:
(1) 環(huán)境配置,比如:連接數(shù),超時(shí)等配置。
(2) 描述配置,比如:服務(wù)接口描述,服務(wù)版本等。
(3) 擴(kuò)展配置,比如:協(xié)議擴(kuò)展,策略擴(kuò)展等。

2. 配置格式

(1) 通常環(huán)境配置,用properties配置會(huì)比較方便,
因?yàn)槎际且恍╇x散的簡單值,用key-value配置可以減少配置的學(xué)習(xí)成本。

(2) 而描述配置,通常信息比較多,甚至有層次關(guān)系,
用xml配置會(huì)比較方便,因?yàn)闃浣Y(jié)構(gòu)的配置表現(xiàn)力更強(qiáng),
如果非常復(fù)雜,也可以考自定義DSL做為配置,
有時(shí)候這類配置也可以用Annotation代替,
因?yàn)檫@些配置和業(yè)務(wù)邏輯相關(guān),放在代碼里也是合理的。

(3) 另外擴(kuò)展配置,可能不盡相同,
如果只是策略接口實(shí)現(xiàn)類替換,可以考慮properties等結(jié)構(gòu),
如果有復(fù)雜的生命周期管理,可能需要XML等配置,
有時(shí)候擴(kuò)展會(huì)通過注冊(cè)接口的方式提供。

3. 配置加載

(1) 對(duì)于環(huán)境配置,
在java世界里,比較常規(guī)的做法,
是在classpath下約定一個(gè)以項(xiàng)目為名稱的properties配置,
比如:log4j.properties,velocity.properties等,
產(chǎn)品在初始化時(shí),自動(dòng)從classpath下加載該配置,
我們平臺(tái)的很多項(xiàng)目也使用類似策略,
如:dubbo.properties,comsat.xml等,
這樣有它的優(yōu)勢(shì),就是基于約定,簡化了用戶對(duì)配置加載過程的干預(yù),
但同樣有它的缺點(diǎn),當(dāng)classpath存在同樣的配置時(shí),可能誤加載,
以及在ClassLoader隔離時(shí),可能找不到配置,
并且,當(dāng)用戶希望將配置放到統(tǒng)一的目錄時(shí),不太方便。

Dubbo新版本去掉了dubbo.properties,因?yàn)樵摷s定經(jīng)常造成配置沖突。

(2) 而對(duì)于描述配置,
因?yàn)橐獏⑴c業(yè)務(wù)邏輯,通常會(huì)嵌到應(yīng)用的生命周期管理中,
現(xiàn)在使用spring的項(xiàng)目越來越多,直接使用spring配置的比較普遍,
而且spring允許自定義schema,配置簡化后很方便,
當(dāng)然,也有它的缺點(diǎn),就是強(qiáng)依賴spring,
可以提編程接口做了配套方案。

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

(3) 擴(kuò)展配置,通常對(duì)配置的聚合要求比較高,
因?yàn)楫a(chǎn)品需要發(fā)現(xiàn)第三方實(shí)現(xiàn),將其加入產(chǎn)品內(nèi)部,
在java世里,通常是約定在每個(gè)jar包下放一個(gè)指定文件加載,
比如:eclipse的plugin.xml,struts2的struts-plugin.xml等,
這類配置可以考慮java標(biāo)準(zhǔn)的服務(wù)發(fā)現(xiàn)機(jī)制,
即在jar包的META-INF/services下放置接口類全名文件,內(nèi)容為每行一個(gè)實(shí)現(xiàn)類類名,
就像jdk中的加密算法擴(kuò)展,腳本引擎擴(kuò)展,新的JDBC驅(qū)動(dòng)等,都是采用這種方式,
參見:ServiceProvider規(guī)范

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

4. 可編程配置

配置的可編程性是非常必要的,不管你以何種方式加載配置文件,
都應(yīng)該提供一個(gè)編程的配置方式,允許用戶不使用配置文件,直接用代碼完成配置過程,
因?yàn)橐粋€(gè)產(chǎn)品,尤其是組件類產(chǎn)品,通常需要和其它產(chǎn)品協(xié)作使用,
當(dāng)用戶集成你的產(chǎn)品時(shí),可能需要適配配置方式。

Dubbo新版本提供了與xml配置一對(duì)一的配置類,
如:ServiceConfig對(duì)應(yīng)<dubbo:service />,并且屬性也一對(duì)一,
這樣有利于文件配置與編程配置的一致性理解,減少學(xué)習(xí)成本。

5. 配置缺省值

配置的缺省值,通常是設(shè)置一個(gè)常規(guī)環(huán)境的合理值,這樣可以減少用戶的配置量,
通常建議以線上環(huán)境為參考值,開發(fā)環(huán)境可以通過修改配置適應(yīng),
缺省值的設(shè)置,最好在最外層的配置加載就做處理,
程序底層如果發(fā)現(xiàn)配置不正確,就應(yīng)該直接報(bào)錯(cuò),容錯(cuò)在最外層做,
如果在程序底層使用時(shí),發(fā)現(xiàn)配置值不合理,就填一個(gè)缺省值,
很容易掩蓋表面問題,而引發(fā)更深層次的問題,
并且配置的中間傳遞層,很可能并不知道底層使用了一個(gè)缺省值,
一些中間的檢測(cè)條件就可能失效,
Dubbo就出現(xiàn)過這樣的問題,中間層用“地址”做為緩存Key,
而底層,給“地址”加了一個(gè)缺省端口號(hào),
導(dǎo)致不加端口號(hào)的“地址”和加了缺省端口的“地址”并沒有使用相同的緩存。

6. 配置一致性

配置總會(huì)隱含一些風(fēng)格或潛規(guī)則,應(yīng)盡可能保持其一致性,
比如:很多功能都有開關(guān),然后有一個(gè)配置值:
(1) 是否使用注冊(cè)中心,注冊(cè)中心地址。
(2) 是否允許重試,重試次數(shù)。
你可以約定:
(1) 每個(gè)都是先配置一個(gè)boolean類型的開關(guān),再配置一個(gè)值。
(2) 用一個(gè)無效值代表關(guān)閉,N/A地址,0重試次數(shù)等。
不管選哪種方式,所有配置項(xiàng),都應(yīng)保持同一風(fēng)格,Dubbo選的是第二種,
相似的還有,超時(shí)時(shí)間,重試時(shí)間,定時(shí)器間隔時(shí)間,
如果一個(gè)單位是秒,另一個(gè)單位是毫秒(C3P0的配置項(xiàng)就是這樣),配置人員會(huì)瘋掉。

7. 配置覆蓋

提供配置時(shí),要同時(shí)考慮開發(fā)人員,測(cè)試人員,配管人員,系統(tǒng)管理員,
測(cè)試人員是不能修改代碼的,而測(cè)試的環(huán)境很可能較為復(fù)雜,
需要為測(cè)試人員留一些“后門”,可以在外圍修改配置項(xiàng),
就像spring的PropertyPlaceholderConfigurer配置,支持SYSTEM_PROPERTIES_MODE_OVERRIDE,
可以通過JVM的-D參數(shù),或者像hosts一樣約定一個(gè)覆蓋配置文件,
在程序外部,修改部分配置,便于測(cè)試。
Dubbo支持通過JVM參數(shù)-Dcom.xxx.XxxService=dubbo://10.1.1.1:1234
直接使遠(yuǎn)程服務(wù)調(diào)用繞過注冊(cè)中心,進(jìn)行點(diǎn)對(duì)點(diǎn)測(cè)試。
還有一種情況,開發(fā)人員增加配置時(shí),都會(huì)按線上的部署情況做配置,如:
<dubbo:registry address="{dubbo.registry.address}" /> 因?yàn)榫€上只有一個(gè)注冊(cè)中心,這樣的配置是沒有問題的, 而測(cè)試環(huán)境可能有兩個(gè)注冊(cè)中心,測(cè)試人員不可能去修改配置,改為: <dubbo:registry address="{dubbo.registry.address1}" />
<dubbo:registry address="{dubbo.registry.address2}" /> 所以這個(gè)地方,Dubbo支持在{dubbo.registry.address}的值中,
通過豎號(hào)分隔多個(gè)注冊(cè)中心地址,用于表示多注冊(cè)中心地址。

8. 配置繼承

配置也存在“重復(fù)代碼”,也存在“泛化與精化”的問題,
比如:Dubbo的超時(shí)時(shí)間設(shè)置,每個(gè)服務(wù),每個(gè)方法,都應(yīng)該可以設(shè)置超時(shí)時(shí)間,
但很多服務(wù)不關(guān)心超時(shí),如果要求每個(gè)方法都配置,是不現(xiàn)實(shí)的,
所以Dubbo采用了,方法超時(shí)繼承服務(wù)超時(shí),服務(wù)超時(shí)再繼承缺省超時(shí),沒配置時(shí),一層層向上查找。

另外,Dubbo舊版本所有的超時(shí)時(shí)間,重試次數(shù),負(fù)載均衡策略等都只能在服務(wù)消費(fèi)方配置,
但實(shí)際使用過程中發(fā)現(xiàn),服務(wù)提供方比消費(fèi)方更清楚,但這些配置項(xiàng)是在消費(fèi)方執(zhí)行時(shí)才用到的,
新版本,就加入了在服務(wù)提供方也能配這些參數(shù),通過注冊(cè)中心傳遞到消費(fèi)方,
做為參考值,如果消費(fèi)方?jīng)]有配置,就以提供方的配置為準(zhǔn),相當(dāng)于消費(fèi)方繼承了提供方的建議配置值,
而注冊(cè)中心在傳遞配置時(shí),也可以在中途修改配置,這樣就達(dá)到了治理的目的,繼承關(guān)系相當(dāng)于:
服務(wù)消費(fèi)者 --> 注冊(cè)中心 --> 服務(wù)提供者


image.png

9. 配置向后兼容

向前兼容很好辦,你只要保證配置只增不減,就基本上能保證向前兼容,
但向后兼容,也是要注意的,要為后續(xù)加入新的配置項(xiàng)做好準(zhǔn)備,
如果配置出現(xiàn)一個(gè)特殊配置,就應(yīng)該為這個(gè)“特殊”情況約定一個(gè)兼容規(guī)則,
因?yàn)檫@個(gè)特殊情況,很有可能在以后還會(huì)發(fā)生,
比如:有一個(gè)配置文件是保存“服務(wù)=地址”映射關(guān)系的,
其中有一行特殊,保存的是“注冊(cè)中心=地址”,
現(xiàn)在程序加載時(shí),約定“注冊(cè)中心”這個(gè)Key是特殊的,
做特別處理,其它的都是“服務(wù)”,
然而,新版本發(fā)現(xiàn),要加一項(xiàng)“監(jiān)控中心=地址”,
這時(shí),舊版本的程序會(huì)把“監(jiān)控中心”做為“服務(wù)”處理,
因?yàn)榕f代碼是不能改的,兼容性就很會(huì)很麻煩,
如果先前約定“特殊標(biāo)識(shí)+XXX”為特殊處理,后續(xù)就會(huì)方便很多。
向后兼容性,可以多向HTML5學(xué)習(xí),參見:HTML5設(shè)計(jì)原理


實(shí)現(xiàn)的健壯性

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

Dubbo作為遠(yuǎn)程服務(wù)暴露、調(diào)用和治理的解決方案,是應(yīng)用運(yùn)轉(zhuǎn)的經(jīng)絡(luò),其本身實(shí)現(xiàn)健壯性的重要程度是不言而喻的。

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

一、日志

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

日志質(zhì)量往往被忽視,沒有日志使用上的明確約定。

重視Log的使用,提高Log的信息濃度。

日志過多、過于混亂,會(huì)導(dǎo)致有用的信息被淹沒。

要有效利用這個(gè)工具要注意:

嚴(yán)格約定WARN、ERROR級(jí)別記錄的內(nèi)容

  • WARN表示可以恢復(fù)的問題,無需人工介入。
  • ERROR表示需要人工介入問題。

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

過多的報(bào)警會(huì)讓人疲倦,使人對(duì)報(bào)警失去警惕性,使ERROR日志失去意義。

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

日志中,盡量多的收集關(guān)鍵信息

哪些是關(guān)鍵信息呢?

  • 出問題時(shí)的現(xiàn)場(chǎng)信息,即排查問題要用到的信息。如服務(wù)調(diào)用失敗時(shí),要給出 使用Dubbo的版本、服務(wù)提供者的IP、使用的是哪個(gè)注冊(cè)中心;調(diào)用的是哪個(gè)服務(wù)、哪個(gè)方法等等。這些信息如果不給出,那么事后人工收集的,問題過后現(xiàn)場(chǎng)可能已經(jīng)不能復(fù)原,加大排查問題的難度。
  • 如果可能,給出問題的原因和解決方法。這讓維護(hù)和問題解決變得簡單,而不是尋求精通者(往往是實(shí)現(xiàn)者)的幫助。

同一個(gè)或是一類問題不要重復(fù)記錄多次

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

如為一個(gè)問題準(zhǔn)備一個(gè)標(biāo)志,出問題后打日志后設(shè)置標(biāo)志,避免重復(fù)打日志。問題恢復(fù)后清除標(biāo)志。

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

二、界限設(shè)置

資源是有限的,CPU、內(nèi)存、IO等等。不要因?yàn)橥獠康恼?qǐng)求、數(shù)據(jù)不受限的而崩潰。

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

Server端用于處理請(qǐng)求的ExectorService設(shè)置上限。

ExecutorService的任務(wù)等待隊(duì)列使用有限隊(duì)列,避免資源耗盡。

當(dāng)任務(wù)等待隊(duì)列飽和時(shí),選擇一個(gè)合適的飽和策略。這樣保證平滑劣化。

在Dubbo中,飽和策略是丟棄數(shù)據(jù),等待結(jié)果也只是請(qǐng)求的超時(shí)。

達(dá)到飽和時(shí),說明已經(jīng)達(dá)到服務(wù)提供方的負(fù)荷上限,要在飽和策略的操作中日志記錄這個(gè)問題,以發(fā)出監(jiān)控警報(bào)。

記得注意不要重復(fù)多次記錄哦。

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

根據(jù)警報(bào)的頻率,已經(jīng)決定擴(kuò)容調(diào)整等等,避免系統(tǒng)問題被忽略。

集合容量

如果確保進(jìn)入集合的元素是可控的且是足夠少,則可以放心使用。這是大部分的情況。

如果不能保證,則使用有有界的集合。當(dāng)?shù)竭_(dá)界限時(shí),選擇一個(gè)合適的丟棄策略。

三、容錯(cuò)-重試-恢復(fù)

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

Dubbo的服務(wù)注冊(cè)中心

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

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

注冊(cè)中心會(huì)內(nèi)存中保證一份提供者和消費(fèi)者數(shù)據(jù),數(shù)據(jù)庫不可用時(shí),注冊(cè)中心獨(dú)立對(duì)外正常運(yùn)轉(zhuǎn),只是拿不到其它注冊(cè)中心的數(shù)據(jù)。

當(dāng)數(shù)據(jù)庫恢復(fù)時(shí),重試邏輯會(huì)內(nèi)存中修改的數(shù)據(jù)寫回?cái)?shù)據(jù)庫,并拿到數(shù)據(jù)庫中新數(shù)據(jù)。

服務(wù)的消費(fèi)者

服務(wù)消息者從注冊(cè)中心拿到提供者列表后,會(huì)保存提供者列表到內(nèi)存和磁盤文件中。

這樣注冊(cè)中心宕后消費(fèi)者可以正常運(yùn)轉(zhuǎn),甚至可以在注冊(cè)中心宕機(jī)過程中重啟消費(fèi)者。

消費(fèi)者啟動(dòng)時(shí),發(fā)現(xiàn)注冊(cè)中心不可用,會(huì)讀取保存在磁盤文件中提供者列表。

重試邏輯保證注冊(cè)中心恢復(fù)后,更新信息。

四、重試延遲策略

上一點(diǎn)的子問題。Dubbo中碰到有兩個(gè)相關(guān)的場(chǎng)景。

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

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

過期注冊(cè)中心和它的相關(guān)數(shù)據(jù) 會(huì)被清除。數(shù)據(jù)庫正常時(shí),這個(gè)機(jī)制運(yùn)行良好。

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

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

可以使用下面邏輯

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

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

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

Client重連注冊(cè)中心

當(dāng)一個(gè)注冊(cè)中心停機(jī)時(shí),其它的Client會(huì)同時(shí)接收事件,而去重連另一個(gè)注冊(cè)中心。

Client數(shù)量相對(duì)比較多,會(huì)對(duì)注冊(cè)中心造成沖擊。

避免方法可以是Client重連時(shí)隨機(jī)延時(shí)3分鐘,把重連分散開。


防癡呆設(shè)計(jì)

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

最近有點(diǎn)癡呆,因?yàn)榻鉀Q了太多的癡呆問題,
服務(wù)框架實(shí)施面超來超廣,已有50多個(gè)項(xiàng)目在使用,
每天都要去幫應(yīng)用查問題,來來回回,
發(fā)現(xiàn)大部分都是配置錯(cuò)誤,或者重復(fù)的文件或類,或者網(wǎng)絡(luò)不通等,
所以準(zhǔn)備在新版本中加入防癡呆設(shè)計(jì),估且這么叫吧,
可能很簡單,但對(duì)排錯(cuò)速度還是有點(diǎn)幫助,
希望能拋磚引玉,也希望大家多給力,想出更多的防范措施共享出來。

(1) 檢查重復(fù)的jar包
最癡呆的問題,就是有多個(gè)版本的相同jar包,
會(huì)出現(xiàn)新版本的A類,調(diào)用了舊版本的B類,
而且和JVM加載順序有關(guān),問題帶有偶然性,誤導(dǎo)性,
遇到這種莫名其妙的問題,最頭疼,
所以,第一條,先把它防住,
在每個(gè)jar包中挑一個(gè)一定會(huì)加載的類,加上重復(fù)類檢查,
給個(gè)示例:

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

檢查重復(fù)工具類:

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);
                    }
                }
            }
            // 如果有多個(gè),就表示重復(fù)
            if (files.size() &gt; 1) {
                logger.error("Duplicate class " + path + " in " + files.size() + " jar " + files);
            }
        } catch (Throwable e) { // 防御性容錯(cuò)
            logger.error(e.getMessage(), e);
        }
    }

}

(2) 檢查重復(fù)的配置文件
配置文件加載錯(cuò),也是經(jīng)常碰到的問題,
用戶通常會(huì)和你說:“我配置的很正確啊,不信我發(fā)給你看下,但就是報(bào)錯(cuò)”,
然后查一圈下來,原來他發(fā)過來的配置根本沒加載,
平臺(tái)很多產(chǎn)品都會(huì)在classpath下放一個(gè)約定的配置,
如果項(xiàng)目中有多個(gè),通常會(huì)取JVM加載的第一個(gè),
為了不被這么低級(jí)的問題折騰,
和上面的重復(fù)jar包一樣,在配置加載的地方,加上:

Duplicate.checkDuplicate("xxx.properties");  

(3) 檢查所有可選配置
必填配置估計(jì)大家都會(huì)檢查,因?yàn)闆]有的話,根本沒法運(yùn)行,
但對(duì)一些可選參數(shù),也應(yīng)該做一些檢查,
比如:服務(wù)框架允許通過注冊(cè)中心關(guān)聯(lián)服務(wù)消費(fèi)者和服務(wù)提供者,
也允許直接配置服務(wù)提供者地址點(diǎn)對(duì)點(diǎn)直連,
這時(shí)候,注冊(cè)中心地址是可選的,
但如果沒有配點(diǎn)對(duì)點(diǎn)直連配置,注冊(cè)中心地址就一定要配,
這時(shí)候也要做相應(yīng)檢查。

(4) 異常信息給出解決方案
在給應(yīng)用排錯(cuò)時(shí),最怕的就是那種只有簡單的一句錯(cuò)誤描述,啥信息都沒有的異常信息,
比如上次碰到一個(gè)Failed to get session異常,
就這幾個(gè)單詞,啥都沒有,哪個(gè)session出錯(cuò)? 什么原因Failed?
看了都快瘋掉,因是線上環(huán)境不好調(diào)試,而且有些場(chǎng)景不是每次都能重現(xiàn),
異常最基本要帶有上下文信息,包括操作者,操作目標(biāo),原因等,
最好的異常信息,應(yīng)給出解決方案,比如上面可以給出:
"從10.20.16.3到10.20.130.20:20880之間的網(wǎng)絡(luò)不通,
請(qǐng)?jiān)?0.20.16.3使用telnet 10.20.130.20 20880測(cè)試一下網(wǎng)絡(luò),
如果是跨機(jī)房調(diào)用,可能是防火墻阻擋,請(qǐng)聯(lián)系SA開通訪問權(quán)限"
等等,上面甚至可以根據(jù)IP段判斷是不是跨機(jī)房。
另外一個(gè)例子,是spring-web的context加載,
如果在getBean時(shí)spring沒有被啟動(dòng),
spring會(huì)報(bào)一個(gè)錯(cuò),錯(cuò)誤信息寫著:
請(qǐng)?jiān)趙eb.xml中加入:<listener>...<init-param>...
多好的同學(xué),看到錯(cuò)誤的人復(fù)制一下就完事了,我們?cè)搶W(xué)學(xué),
可以把常見的錯(cuò)誤故意犯一遍,看看錯(cuò)誤信息能否自我搞定問題,
或者把平時(shí)支持應(yīng)用時(shí)遇到的問題及解決辦法都寫到異常信息里。

(5) 日志信息包含環(huán)境信息
每次應(yīng)用一出錯(cuò),應(yīng)用的開發(fā)或測(cè)試就會(huì)把出錯(cuò)信息發(fā)過來,詢問原因,
這時(shí)候我都會(huì)問一大堆套話,
用的哪個(gè)版本呀?
是生產(chǎn)環(huán)境還是開發(fā)測(cè)試環(huán)境?
哪個(gè)注冊(cè)中心呀?
哪個(gè)項(xiàng)目中的?
哪臺(tái)機(jī)器呀?
哪個(gè)服務(wù)?
。。。
累啊,最主要的是,有些開發(fā)或測(cè)試人員根本分不清,
沒辦法,只好提供上門服務(wù),浪費(fèi)的時(shí)間可不是浮云,
所以,日志中最好把需要的環(huán)境信息一并打進(jìn)去,
最好給日志輸出做個(gè)包裝,統(tǒng)一處理掉,免得忘了。
包裝Logger接口如:

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

獲取版本號(hào)工具類:

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ī)范中的版本號(hào)
            String version = cls.getPackage().getImplementationVersion();
            if (version == null || version.length() == 0) {
                version = cls.getPackage().getSpecificationVersion();
            }
            if (version == null || version.length() == 0) {
                // 如果MANIFEST.MF規(guī)范中沒有版本號(hào),基于jar包名獲取版本號(hào)
                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);
                    }
                }
            }
            // 返回版本號(hào),如果為空返回缺省版本號(hào)
            return version == null || version.length() == 0 ? defaultVersion : version;
        } catch (Throwable e) { // 防御性容錯(cuò)
            // 忽略異常,返回缺省版本號(hào)
            logger.error(e.getMessage(), e);
            return defaultVersion;
        }
    }

}

(6) kill之前先dump
每次線上環(huán)境一出問題,大家就慌了,
通常最直接的辦法回滾重啟,以減少故障時(shí)間,
這樣現(xiàn)場(chǎng)就被破壞了,要想事后查問題就麻煩了,
有些問題必須在線上的大壓力下才會(huì)發(fā)生,
線下測(cè)試環(huán)境很難重現(xiàn),
不太可能讓開發(fā)或Appops在重啟前,
先手工將出錯(cuò)現(xiàn)場(chǎng)所有數(shù)據(jù)備份一下,
所以最好在kill腳本之前調(diào)用dump,
進(jìn)行自動(dòng)備份,這樣就不會(huì)有人為疏忽。
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擴(kuò)展點(diǎn)重構(gòu)

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

隨著服務(wù)化的推廣,網(wǎng)站對(duì)Dubbo服務(wù)框架的需求逐漸增多,
Dubbo的現(xiàn)有開發(fā)人員能實(shí)現(xiàn)的需求有限,很多需求都被delay,
而網(wǎng)站的同學(xué)也希望參與進(jìn)來,加上領(lǐng)域的推動(dòng),
所以平臺(tái)計(jì)劃將部分項(xiàng)目對(duì)公司內(nèi)部開放,讓大家一起來實(shí)現(xiàn),
Dubbo為試點(diǎn)項(xiàng)目之一。

既然要開放,那Dubbo就要留一些擴(kuò)展點(diǎn),
讓參與者盡量黑盒擴(kuò)展,而不是白盒的修改代碼,
否則分支,質(zhì)量,合并,沖突都會(huì)很難管理。

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


image.png

這里面雖然有部分?jǐn)U展接口,但并不能很好的協(xié)作,
而且擴(kuò)展點(diǎn)的加載和配置都沒有統(tǒng)一處理,所以下面對(duì)它進(jìn)行重構(gòu)。

第一步,微核心,插件式,平等對(duì)待第三方。

即然要擴(kuò)展,擴(kuò)展點(diǎn)的加載方式,首先要統(tǒng)一,
微核心+插件式,是比較能達(dá)到OCP原則的思路,

由一個(gè)插件生命周期管理容器,構(gòu)成微核心,
核心不包括任何功能,這樣可以確保所有功能都能被替換,
并且,框架作者能做到的功能,擴(kuò)展者也一定要能做到,以保證平等對(duì)待第三方,
所以,框架自身的功能也要用插件的方式實(shí)現(xiàn),不能有任何硬編碼。

通常微核心都會(huì)采用Factory,IoC,OSGi等方式管理插件生命周期,
考慮Dubbo的適用面,不想強(qiáng)依賴Spring等IoC容器,
自已造一個(gè)小的IoC容器,也覺得有點(diǎn)過度設(shè)計(jì),
所以打算采用最簡單的Factory方式管理插件,

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

并約定所有插件,都必須標(biāo)注:@Extension("name"),
作為加載后的標(biāo)識(shí)性名稱,用于配置選擇。

第二步,每個(gè)擴(kuò)展點(diǎn)只封裝一個(gè)變化因子,最大化復(fù)用。

每個(gè)擴(kuò)展點(diǎn)的實(shí)現(xiàn)者,往往都只是關(guān)心一件事,
現(xiàn)在的擴(kuò)展點(diǎn),并沒有完全分離,

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

再比如,協(xié)議擴(kuò)展,擴(kuò)展者可能只是想替換序列化方式,或者只替換傳輸方式,
并且Remoting和Http也能復(fù)用序列化等實(shí)現(xiàn),
這樣,需為傳輸方式,客戶端實(shí)現(xiàn),服務(wù)器端實(shí)現(xiàn),協(xié)議頭解析,數(shù)據(jù)序列化,都留出不同擴(kuò)展點(diǎn)。

拆分后,設(shè)計(jì)如下:


image.png

第三步,全管道式設(shè)計(jì),框架自身邏輯,均使用截面攔截實(shí)現(xiàn)。

現(xiàn)在很多的邏輯,都是放在基類中實(shí)現(xiàn),然后通過模板方法回調(diào)子類的實(shí)現(xiàn),
包括:local, mock, generic, echo, token, accesslog, monitor, count, limit等等,
可以全部拆分使用Filter實(shí)現(xiàn),每個(gè)功能都是調(diào)用鏈上的一環(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

}

改成:(鏈?zhǔn)竭^濾器)

public abstract LimitFilter implements Filter {

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

}

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

保持盡可能少的概念,有助于理解,對(duì)于開放的系統(tǒng)尤其重要,
另外,各接口都使用一致的概念模型,能相互指引,并減少模型轉(zhuǎn)換,

比如,Invoker的方法簽名為:

Result invoke(Invocation invocation) throws RpcException;

而Exporter的方法簽名為:

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

但它們的作用是一樣的,只是一個(gè)在客戶端,一個(gè)在服務(wù)器端,卻采用了不一樣的模型類。

再比如,URL以字符串傳遞,不停的解析和拼裝,沒有一個(gè)URL模型類, 而URL的參數(shù),卻時(shí)而Map, 時(shí)而Parameters類包裝,

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

使用一致模型:

export(URL url)
createExporter(URL url);

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

第五步,分層,組合式擴(kuò)展,而不是泛化式擴(kuò)展。
原因參見:http://javatar.iteye.com/blog/690845
泛化式擴(kuò)展指:將擴(kuò)展點(diǎn)逐漸抽象,取所有功能并集,新加功能總是套入并擴(kuò)充舊功能的概念。
組合式擴(kuò)展指:將擴(kuò)展點(diǎn)正交分解,取所有功能交集,新加功能總是基于舊功能之上實(shí)現(xiàn)。
上面的設(shè)計(jì),不自覺的就將Dubbo現(xiàn)有功能都當(dāng)成了核心功能,
上面的概念包含了Dubbo現(xiàn)有RPC的所有功能,包括:Proxy, Router, Failover, LoadBalance, Subscriber, Publisher, Invoker, Exporter, Filter等,
但這些都是核心嗎?踢掉哪些,RPC一樣可以Run?而哪些又是不能踢掉的?
基于這樣考慮,可以將RPC分解成兩個(gè)層次,只是Protocol和Invoker才是RPC的核心,
其它,包括Router, Failover, Loadbalance, Subscriber, Publisher都不核心,而是Routing,
所以,將Routing作為Rpc核心的一個(gè)擴(kuò)展,設(shè)計(jì)如下:

image.png

第六步,整理,梳理關(guān)系。

整理后,設(shè)計(jì)如下:


image.png

個(gè)人介紹:

高廣超:多年一線互聯(lián)網(wǎng)研發(fā)與架構(gòu)設(shè)計(jì)經(jīng)驗(yàn),擅長設(shè)計(jì)與落地高可用、高性能、可擴(kuò)展的互聯(lián)網(wǎng)架構(gòu)。

本文首發(fā)在 高廣超的簡書博客 轉(zhuǎn)載請(qǐng)注明!

簡書博客
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Dubbo是什么 Dubbo是Alibaba開源的分布式服務(wù)框架,它最大的特點(diǎn)是按照分層的方式來架構(gòu),使用這種方式...
    Coselding閱讀 17,443評(píng)論 3 196
  • 本文轉(zhuǎn)自:Dubbo架構(gòu)設(shè)計(jì)詳解,原作者是:時(shí)延軍 Dubbo是Alibaba開源的分布式服務(wù)框架,它最大的特點(diǎn)是...
    程序熊大閱讀 3,547評(píng)論 3 45
  • 下午來了家長會(huì),想要家長和我多溝通,最后還是只有單方面的演講。
    亦荼閱讀 229評(píng)論 0 0
  • 教書那段時(shí)間,養(yǎng)成了每晚睡前看書,11點(diǎn)之前進(jìn)入睡眠的好習(xí)慣,但總是不能夠在7點(diǎn)之前起床呼吸一下清晨的空氣。人啊,...
    六千圈閱讀 455評(píng)論 0 0
  • 大概很多工程師都有這個(gè)夢(mèng)想吧。 自己開一個(gè)軟件公司, 寫一個(gè)軟件改變世界。 我畢業(yè)后第一份工作是一家10幾個(gè)人的小...
    心___塵閱讀 483評(píng)論 0 1

友情鏈接更多精彩內(nèi)容