Dubbo的一些編碼約定和設計原則

編碼約定

代碼風格

Dubbo 的源代碼和 JavaDoc 遵循如下的規範:html

異常和日誌

  • 儘量攜帶完整的上下文信息,好比出錯緣由,出錯的機器地址,調用對方的地址,連的註冊中心地址,使用 Dubbo 的版本等。
  • 儘可能將直接緣由寫在最前面,全部上下文信息,在緣由後用鍵值對顯示。
  • 拋出異常的地方不用打印日誌,由最終處理異常者決定打印日誌的級別,吃掉異常必需打印日誌。
  • 打印 ERROR 日誌表示須要報警,打印 WARN 日誌表示能夠自動恢復,打印 INFO 表示正常信息或徹底不影響運行。
  • 建議應用方在監控中心配置 ERROR 日誌實時報警,WARN 日誌每週彙總發送通知。
  • RpcException 是 Dubbo 對外的惟一異常類型,全部內部異常,若是要拋出給用戶,必須轉爲 RpcException。
  • RpcException 不能有子類型,全部類型信息用 ErrorCode 標識,以便保持兼容。

配置和 URL

  • 配置對象屬性首字母小寫,多個單詞用駝峯命名 。
  • 配置屬性所有用小寫,多個單詞用"-"號分隔 。
  • URL參數所有用小寫,多個單詞用"."號分隔 。
  • 儘量用 URL 傳參,不要自定義 Map 或其它上下文格式,配置信息也轉成 URL 格式使用。
  • 儘可能減小 URL 嵌套,保持 URL 的簡潔性。

單元和集成測試

  • 單元測試統一用 JUnit 和 EasyMock,集成測試用 TestNG,數據庫測試用 DBUnit。
  • 保持單元測試用例的運行速度,不要將性能和大的集成用例放在單元測試中。
  • 保持單元測試的每一個用例都用 try...finally 或 tearDown 釋放資源。
  • 減小 while 循環等待結果的測試用例,對定時器和網絡的測試,用以將定時器中的邏輯抽爲方法測試。
  • 對於容錯行爲的測試,好比 failsafe 的測試,統一用 LogUtil 斷言日誌輸出。

擴展點基類與 AOP

  • AOP 類都命名爲 XxxWrapper,基類都命名爲 AbstractXxx。
  • 擴展點之間的組合將關係由 AOP 完成,ExtensionLoader 只負載加載擴展點,包括 AOP 擴展。
  • 儘可能採用 IoC 注入擴展點之間的依賴,不要直接依賴 ExtensionLoader 的工廠方法。
  • 儘可能採用 AOP 實現擴展點的通用行爲,而不要用基類,好比負載均衡以前的 isAvailable 檢查,它是獨立於負載均衡以外的,不須要檢查的是URL參數關閉。
  • 對多種類似類型的抽象,用基類實現,好比 RMI, Hessian 等第三方協議都已生成了接口代理,只需將將接口代理轉成 Invoker 便可完成橋接,它們能夠用公共基類實現此邏輯。
  • 基類也是 SPI 的一部分,每一個擴展點都應該有方便使用的基類支持。

模塊與分包

  • 基於複用度分包,老是一塊兒使用的放在同一包下,將接口和基類分紅獨立模塊,大的實現也使用獨立模塊。
  • 全部接口都放在模塊的根包下,基類放在 support 子包下,不一樣實現用放在以擴展點名字命名的子包下。
  • 儘可能保持子包依賴父包,而不要反向。

設計原則

1、魔鬼在細節

http://javatar.iteye.com/blog/1056664java

最近一直擔憂 Dubbo 分佈式服務框架後續若是維護人員增多或變動,會出現質量的降低, 我在想,有沒有什麼是須要你們共同遵照的,根據平時寫代碼時的一習慣,總結了一下在寫代碼過程當中,尤爲是框架代碼,要時刻牢記的細節。可能下面要講的這些,你們都會以爲很簡單,很基礎,但要作到時刻牢記。在每一行代碼中都考慮這些因素,是須要很大耐心的, 你們常常說,魔鬼在細節中,確實如此。ios

防止空指針和下標越界

這是我最不喜歡看到的異常,尤爲在覈心框架中,我更願看到信息詳細的參數不合法異常。這也是一個健狀的程序開發人員,在寫每一行代碼都應在潛意識中防止的異常。基本上要能確保一次寫完的代碼,在不測試的狀況,都不會出現這兩個異常纔算合格。web

保證線程安全性和可見性

對於框架的開發人員,對線程安全性和可見性的深刻理解是最基本的要求。須要開發人員,在寫每一行代碼時都應在潛意識中確保其正確性。由於這種代碼,在小併發下作功能測試時,會顯得很正常。但在高併發下就會出現莫明其妙的問題,並且場景很難重現,極難排查。算法

儘早失敗和前置斷言

儘早失敗也應該成爲潛意識,在有傳入參數和狀態變化時,均在入口處所有斷言。一個不合法的值和狀態,在第一時間就應報錯,而不是等到要用時才報錯。由於等到要用時,可能前面已經修改其它相關狀態,而在程序中不多有人去處理回滾邏輯。這樣報錯後,其實內部狀態可能已經混亂,極易在一個隱蔽分支上引起程序不可恢復。spring

分離可靠操做和不可靠操做

這裏的可靠是狹義的指是否會拋出異常或引發狀態不一致,好比,寫入一個線程安全的 Map,能夠認爲是可靠的,而寫入數據庫等,能夠認爲是不可靠的。開發人員必須在寫每一行代碼時,都注意它的可靠性與否,在代碼中儘可能劃分開,並對失敗作異常處理,併爲容錯,自我保護,自動恢復或切換等補償邏輯提供清晰的切入點,保證後續增長的代碼不至於放錯位置,而致使原先的容錯處理陷入混亂。數據庫

異常防護,但不忽略異常

這裏講的異常防護,指的是對非必須途徑上的代碼進行最大限度的容忍,包括程序上的 BUG,好比:獲取程序的版本號,會經過掃描 Manifest 和 jar 包名稱抓取版本號,這個邏輯是輔助性的,但代碼卻很多,初步測試也沒啥問題,但應該在整個 getVersion() 中加上一個全函數的 try-catch 打印錯誤日誌,並返回基本版本,由於 getVersion() 可能存在未知特定場景異常,或被其餘的開發人員誤修改邏輯(但通常人員不會去掉 try-catch),而若是它拋出異常會致使主流程異常,這是咱們不但願看到的。但這裏要控制個度,不要隨意 try-catch,更不要無聲無息的吃掉異常。apache

縮小可變域和儘可能 final

若是一個類能夠成爲不變類(Immutable Class),就優先將它設計成不變類。不變類有自然的併發共享優點,減小同步或複製,並且能夠有效幫忙分析線程安全的範圍。就算是可變類,對於從構造函數傳入的引用,在類中持有時,最好將字段 final,以避免被中途誤修改引用。不要覺得這個字段是私有的,這個類的代碼都是我本身寫的,不會出現對這個字段的從新賦值。要考慮的一個因素是,這個代碼可能被其餘人修改,他不知道你的這個弱約定,final 就是一個不變契約。編程

下降修改時的誤解性,不埋雷

前面不停的提到代碼被其餘人修改,這也開發人員要隨時緊記的。這個其餘人包括將來的本身,你要總想着這個代碼可能會有人去改它。我應該給修改的人一點什麼提示,讓他知道我如今的設計意圖,而不要在程序裏面加潛規則,或埋一些容易忽視的雷,好比:你用 null 表示不可用,size 等於 0 表示黑名單,這就是一個雷,下一個修改者,包括你本身,都不會記得有這樣的約定,可能後面爲了改某個其它 BUG,不當心改到了這裏,直接引爆故障。對於這個例子,一個原則就是永遠不要區分 null 引用和 empty 值。json

提升代碼的可測性

這裏的可測性主要指 Mock 的容易程度,和測試的隔離性。至於測試的自動性,可重複性,非偶然性,無序性,完備性(全覆蓋),輕量性(可快速執行),通常開發人員,加上 JUnit 等工具的輔助基本都能作到,也能理解它的好處,只是工做量問題。這裏要特別強調的是測試用例的單一性(只測目標類自己)和隔離性(不傳染失敗)。如今的測試代碼,過於強調完備性,大量重複交叉測試,看起來沒啥壞處,但測試代碼越多,維護代價越高。常常出現的問題是,修改一行代碼或加一個判斷條件,引發 100 多個測試用例不經過。時間一緊,誰有這個閒功夫去改這麼多形態萬千的測試用例?長此以往,這個測試代碼就已經不能真實反應代碼如今的情況,不少時候會被迫繞過。最好的狀況是,修改一行代碼,有且只有一行測試代碼不經過。若是修改了代碼而測試用例還能經過,那也不行,表示測試沒有覆蓋到。另外,可 Mock 性是隔離的基礎,把間接依賴的邏輯屏蔽掉。可 Mock 性的一個最大的殺手就是靜態方法,儘可能少用。

2、一些設計上的基本常識

http://javatar.iteye.com/blog/706098

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

API 與 SPI 分離

框架或組件一般有兩類客戶,一個是使用者,一個是擴展者。API (Application Programming Interface) 是給使用者用的,而 SPI (Service Provide Interface) 是給擴展者用的。在設計時,儘可能把它們隔離開,而不要混在一塊兒。也就是說,使用者是看不到擴展者寫的實現的。

好比:一個 Web 框架,它有一個 API 接口叫 Action,裏面有個 execute() 方法,是給使用者用來寫業務邏輯的。而後,Web 框架有一個 SPI 接口給擴展者控制輸出方式,好比用 velocity 模板輸出仍是用 json 輸出等。若是這個 Web 框架使用一個都繼承 Action 的 VelocityAction 和一個 JsonAction 作爲擴展方式,要用 velocity 模板輸出的就繼承 VelocityAction,要用 json 輸出的就繼承 JsonAction,這就是 API 和 SPI 沒有分離的反面例子,SPI 接口混在了 API 接口中。

mix-api-spi

合理的方式是,有一個單獨的 Renderer 接口,有 VelocityRenderer 和 JsonRenderer 實現,Web 框架將 Action 的輸出轉交給 Renderer 接口作渲染輸出。

seperate-api-spi

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

任何框架或組件,總會有核心領域模型,好比:Spring 的 Bean,Struts 的 Action,Dubbo 的 Service,Napoli 的 Queue 等等。這個核心領域模型及其組成部分稱爲實體域,它表明着咱們要操做的目標自己。實體域一般是線程安全的,無論是經過不變類,同步狀態,或複製的方式。

服務域也就是行爲域,它是組件的功能集,同時也負責實體域和會話域的生命週期管理, 好比 Spring 的 ApplicationContext,Dubbo 的 ServiceManager 等。服務域的對象一般會比較重,並且是線程安全的,並以單一實例服務於全部調用。

什麼是會話?就是一次交互過程。會話中重要的概念是上下文,什麼是上下文?好比咱們說:「老地方見」,這裏的「老地方」就是上下文信息。爲何說「老地方」對方會知道,由於咱們前面定義了「老地方」的具體內容。因此說,上下文一般持有交互過程當中的狀態變量等。會話對象一般較輕,每次請求都從新建立實例,請求結束後銷燬。簡而言之:把元信息交由實體域持有,把一次請求中的臨時狀態由會話域持有,由服務域貫穿整個過程。

ddd

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

若是你要寫個遠程調用框架,那遠程調用的過程應該有一個統一的攔截接口。若是你要寫一個 ORM 框架,那至少 SQL 的執行過程,Mapping 過程要有攔截接口;若是你要寫一個 Web 框架,那請求的執行過程應該要有攔截接口,等等。沒有哪一個公用的框架能夠 Cover 住全部需求,容許外置行爲,是框架的基本擴展方式。這樣,若是有人想在遠程調用前,驗證下令牌,驗證下黑白名單,統計下日誌;若是有人想在 SQL 執行前加下分頁包裝,作下數據權限控制,統計下 SQL 執行時間;若是有人想在請求執行前檢查下角色,包裝下輸入輸出流,統計下請求量,等等,就能夠自行完成,而不用侵入框架內部。攔截接口,一般是把過程自己用一個對象封裝起來,傳給攔截器鏈,好比:遠程調用主過程爲 invoke(),那攔截器接口一般爲 invoke(Invocation),Invocation 對象封裝了原本要執行過程的上下文,而且 Invocation 裏有一個 invoke() 方法,由攔截器決定何時執行,同時,Invocation 也表明攔截器行爲自己,這樣上一攔截器的 Invocation 實際上是包裝的下一攔截器的過程,直到最後一個攔截器的 Invocation 是包裝的最終的 invoke() 過程;同理,SQL 主過程爲 execute(),那攔截器接口一般爲 execute(Execution),原理同樣。固然,實現方式能夠任意,上面只是舉例。

filter-chain

重要的狀態的變動發送事件並留出監聽接口

這裏先要講一個事件和上面攔截器的區別,攔截器是干預過程的,它是過程的一部分,是基於過程行爲的,而事件是基於狀態數據的,任何行爲改變的相同狀態,對事件應該是一致的。事件一般是過後通知,是一個 Callback 接口,方法名一般是過去式的,好比 onChanged()。好比遠程調用框架,當網絡斷開或連上應該發出一個事件,當出現錯誤也能夠考慮發出一個事件,這樣外圍應用就有可能觀察到框架內部的變化,作相應適應。

event-listener

擴展接口職責儘量單一,具備可組合性

好比,遠程調用框架它的協議是能夠替換的。若是隻提供一個總的擴展接口,固然能夠作到切換協議,但協議支持是能夠細分爲底層通信,序列化,動態代理方式等等。若是將接口拆細,正交分解,會更便於擴展者複用已有邏輯,而只是替換某部分實現策略。固然這個分解的粒度須要把握好。

微核插件式,平等對待第三方

大凡發展的比較好的框架,都遵照微核的理念。Eclipse 的微核是 OSGi, Spring 的微核是 BeanFactory,Maven 的微核是 Plexus。一般核心是不該該帶有功能性的,而是一個生命週期和集成容器,這樣各功能能夠經過相同的方式交互及擴展,而且任何功能均可以被替換。若是作不到微核,至少要平等對待第三方,即原做者能實現的功能,擴展者應該能夠經過擴展的方式所有作到。原做者要把本身也看成擴展者,這樣才能保證框架的可持續性及由內向外的穩定性。

不要控制外部對象的生命週期

好比上面說的 Action 使用接口和 Renderer 擴展接口。框架若是讓使用者或擴展者把 Action 或 Renderer 實現類的類名或類元信息報上來,而後在內部經過反射 newInstance() 建立一個實例,這樣框架就控制了 Action 或 Renderer 實現類的生命週期,Action 或 Renderer 的生老病死,框架都本身作了,外部擴展或集成都無能爲力。好的辦法是讓使用者或擴展者把 Action 或 Renderer 實現類的實例報上來,框架只是使用這些實例,這些對象是怎麼建立的,怎麼銷燬的,都和框架無關,框架最多提供工具類輔助管理,而不是絕對控制。

可配置必定可編程,並保持友好的 CoC 約定

由於使用環境的不肯定因素不少,框架總會有一些配置,通常都會到 classpath 直掃某個指定名稱的配置,或者啓動時容許指定配置路徑。作爲一個通用框架,應該作到凡是能配置文件作的必定要能經過編程方式進行,不然當使用者須要將你的框架與另外一個框架集成時就會帶來不少沒必要要的麻煩。

另外,儘量作一個標準約定,若是用戶按某種約定作事時,就不須要該配置項。好比:配置模板位置,你能夠約定,若是放在 templates 目錄下就不用配了,若是你想換個目錄,就配置下。

區分命令與查詢,明確前置條件與後置條件

這個是契約式設計的一部分,儘可能遵照有返回值的方法是查詢方法,void 返回的方法是命令。查詢方法一般是冪等性的,無反作用的,也就是不改變任何狀態,調 n 次結果都是同樣的,好比 get 某個屬性值,或查詢一條數據庫記錄。命令是指有反作用的,也就是會修改狀態,好比 set 某個值,或 update 某條數據庫記錄。若是你的方法即作了修改狀態的操做,又作了查詢返回,若是可能,將其拆成寫讀分離的兩個方法,好比:User deleteUser(id),刪除用戶並返回被刪除的用戶,考慮改成 getUser() 和 void 的 deleteUser()。 另外,每一個方法都儘可能前置斷言傳入參數的合法性,後置斷言返回結果的合法性,並文檔化。

增量式擴展,而不要擴充原始核心概念

參見:談談擴充式擴展與增量式擴展

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

http://javatar.iteye.com/blog/690845

咱們平臺的產品愈來愈多,產品的功能也愈來愈多。平臺的產品爲了適應各 BU 和部門以及產品線的需求,勢必會將不少不相干的功能湊在一塊兒,客戶能夠選擇性的使用。爲了兼容更多的需求,每一個產品,每一個框架,都在不停的擴展,而咱們常常會選擇一些擴展的擴展方式,也就是將新舊功能擴展成一個通用實現。我想討論是,有些狀況下也能夠考慮增量式的擴展方式,也就是保留原功能的簡單性,新功能獨立實現。我最近一直作分佈式服務框架的開發,就拿咱們項目中的問題開涮吧。

好比:遠程調用框架,確定少不了序列化功能,功能很簡單,就是把流轉成對象,對象轉成流。但因有些地方可能會使用 osgi,這樣序列化時,IO 所在的 ClassLoader 可能和業務方的 ClassLoader 是隔離的。須要將流轉換成 byte[] 數組,而後傳給業務方的 ClassLoader 進行序列化。爲了適應 osgi 需求,把原來非 osgi 與 osgi 的場景擴展了一下,這樣,無論是否是 osgi 環境,都先將流轉成 byte[] 數組,拷貝一次。然而,大部分場景都用不上 osgi,卻爲 osgi 付出了代價。而若是採用增量式擴展方式,非 osgi 的代碼原封不動,再加一個 osgi 的實現,要用 osgi 的時候,直接依賴 osgi 實現便可。

再好比:最開始,遠程服務都是基於接口方法,進行透明化調用的。這樣,擴展接口就是, invoke(Method method, Object[] args),後來,有了無接口調用的需求,就是沒有接口方法也能調用,並將 POJO 對象都轉換成 Map 表示。由於 Method 對象是不能直接 new 出來的,咱們不自覺選了一個擴展式擴展,把擴展接口改爲了 invoke(String methodName, String[] parameterTypes, String returnTypes, Object[] args),致使無論是否是無接口調用,都得把 parameterTypes 從 Class[] 轉成 String[]。若是選用增量式擴展,應該是保持原有接口不變,增長一個 GeneralService 接口,裏面有一個通用的 invoke() 方法,和其它正常業務上的接口同樣的調用方式,擴展接口也不用變,只是 GeneralServiceImpl 的 invoke() 實現會將收到的調用轉給目標接口,這樣就能將新功能增量到舊功能上,並保持原來結構的簡單性。

再再好比:無狀態消息發送,很簡單,序列化一個對象發過去就行。後來有了同步消息發送需求,須要一個 Request/Response 進行配對,採用擴展式擴展,天然想到,無狀態消息實際上是一個沒有 Response 的 Request,因此在 Request 里加一個 boolean 狀態,表示要不要返回 Response。若是再來一個會話消息發送需求,那就再加一個 Session 交互,而後發現,原來同步消息發送是會話消息的一種特殊狀況,全部場景都傳 Session,不須要 Session 的地方無視便可。

open-expand

若是採用增量式擴展,無狀態消息發送原封不動,同步消息發送,在無狀態消息基礎上加一個 Request/Response 處理,會話消息發送,再加一個 SessionRequest/SessionResponse 處理。

close-expand

4、配置設計

http://javatar.iteye.com/blog/949527

Dubbo 如今的設計是徹底無侵入,也就是使用者只依賴於配置契約。通過多個版本的發展,爲了知足各類需求場景,配置愈來愈多。爲了保持兼容,配置只增不減,裏面潛伏着各類風格,約定,規則。新版本也將配置作了一次調整,去掉了 dubbo.properties,改成全 spring 配置。將想到的一些記在這,備忘。

配置分類

首先,配置的用途是有多種的,大體能夠分爲:

  1. 環境配置,好比:鏈接數,超時等配置。
  2. 描述配置,好比:服務接口描述,服務版本等。
  3. 擴展配置,好比:協議擴展,策略擴展等。

配置格式

一般環境配置,用 properties 配置會比較方便,由於都是一些離散的簡單值,用 key-value 配置能夠減小配置的學習成本。

而描述配置,一般信息比較多,甚至有層次關係,用 xml 配置會比較方便,由於樹結構的配置表現力更強。若是很是複雜,也能夠考自定義 DSL 作爲配置。有時候這類配置也能夠用 Annotation 代替, 由於這些配置和業務邏輯相關,放在代碼裏也是合理的。

另外擴展配置,可能不盡相同。若是隻是策略接口實現類替換,能夠考慮 properties 等結構。若是有複雜的生命週期管理,可能須要 XML 等配置。有時候擴展會經過註冊接口的方式提供。

配置加載

對於環境配置,在 java 世界裏,比較常規的作法,是在 classpath 下約定一個以項目爲名稱的 properties 配置,好比:log4j.properties,velocity.properties等。產品在初始化時,自動從 classpath 下加載該配置。咱們平臺的不少項目也使用相似策略,如:dubbo.properties,comsat.xml 等。這樣有它的優點,就是基於約定,簡化了用戶對配置加載過程的干預。但一樣有它的缺點,當 classpath 存在一樣的配置時,可能誤加載,以及在 ClassLoader 隔離時,可能找不到配置,而且,當用戶但願將配置放到統一的目錄時,不太方便。

Dubbo 新版本去掉了 dubbo.properties,由於該約定常常形成配置衝突。

而對於描述配置,由於要參與業務邏輯,一般會嵌到應用的生命週期管理中。如今使用 spring 的項目愈來愈多,直接使用 spring 配置的比較廣泛,並且 spring 容許自定義 schema,配置簡化後很方便。固然,也有它的缺點,就是強依賴 spring,能夠提編程接口作了配套方案。

在 Dubbo 即存在描述配置,也有環境配置。一部分用 spring 的 schame 配置加載,一部分從 classpath 掃描 properties 配置加載。用戶感受很是不便,因此在新版本中進行了合併,統一放到 spring 的 schame 配置加載,也增長了配置的靈活性。

擴展配置,一般對配置的聚合要求比較高。由於產品須要發現第三方實現,將其加入產品內部。在 java 世界裏,一般是約定在每一個 jar 包下放一個指定文件加載,好比:eclipse 的 plugin.xml,struts2 的 struts-plugin.xml 等,這類配置能夠考慮 java 標準的服務發現機制,即在 jar 包的 META-INF/services 下放置接口類全名文件,內容爲每行一個實現類類名,就像 jdk 中的加密算法擴展,腳本引擎擴展,新的 JDBC 驅動等,都是採用這種方式。參見:ServiceProvider 規範

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

可編程配置

配置的可編程性是很是必要的,無論你以何種方式加載配置文件,都應該提供一個編程的配置方式,容許用戶不使用配置文件,直接用代碼完成配置過程。由於一個產品,尤爲是組件類產品,一般須要和其它產品協做使用,當用戶集成你的產品時,可能須要適配配置方式。

Dubbo 新版本提供了與 xml 配置一對一的配置類,如:ServiceConfig 對應 <dubbo:service />,而且屬性也一對一,這樣有利於文件配置與編程配置的一致性理解,減小學習成本。

配置缺省值

配置的缺省值,一般是設置一個常規環境的合理值,這樣能夠減小用戶的配置量。一般建議以線上環境爲參考值,開發環境能夠經過修改配置適應。缺省值的設置,最好在最外層的配置加載就作處理。程序底層若是發現配置不正確,就應該直接報錯,容錯在最外層作。若是在程序底層使用時,發現配置值不合理,就填一個缺省值,很容易掩蓋表面問題,而引起更深層次的問題。而且配置的中間傳遞層,極可能並不知道底層使用了一個缺省值,一些中間的檢測條件就可能失效。Dubbo 就出現過這樣的問題,中間層用「地址」作爲緩存 Key, 而底層,給「地址」加了一個缺省端口號,致使不加端口號的「地址」和加了缺省端口的「地址」並無使用相同的緩存。

配置一致性

配置總會隱含一些風格或潛規則,應儘量保持其一致性。好比:不少功能都有開關,而後有一個配置值:

  1. 是否使用註冊中心,註冊中心地址。
  2. 是否容許重試,重試次數。

你能夠約定:

  1. 每一個都是先配置一個 boolean 類型的開關,再配置一個值。
  2. 用一個無效值表明關閉,N/A地址,0重試次數等。

無論選哪一種方式,全部配置項,都應保持同一風格,Dubbo 選的是第二種。類似的還有,超時時間,重試時間,定時器間隔時間。若是一個單位是秒,另外一個單位是毫秒(C3P0的配置項就是這樣),配置人員會瘋掉。

配置覆蓋

提供配置時,要同時考慮開發人員,測試人員,配管人員,系統管理員。測試人員是不能修改代碼的,而測試的環境極可能較爲複雜,須要爲測試人員留一些「後門」,能夠在外圍修改配置項。就像 spring 的 PropertyPlaceholderConfigurer 配置,支持 SYSTEM_PROPERTIES_MODE_OVERRIDE,能夠經過 JVM 的 -D 參數,或者像 hosts 同樣約定一個覆蓋配置文件,在程序外部,修改部分配置,便於測試。

Dubbo 支持經過 JVM 參數 -Dcom.xxx.XxxService=dubbo://10.1.1.1:1234 直接使遠程服務調用繞過註冊中心,進行點對點測試。還有一種狀況,開發人員增長配置時,都會按線上的部署狀況作配置,如:<dubbo:registry address="${dubbo.registry.address}" /> 由於線上只有一個註冊中心,這樣的配置是沒有問題的,而測試環境可能有兩個註冊中心,測試人員不可能去修改配置,改成: <dubbo:registry address="${dubbo.registry.address1}" />, <dubbo:registry address="${dubbo.registry.address2}" />,因此這個地方,Dubbo 支持在 ${dubbo.registry.address} 的值中,經過豎號分隔多個註冊中心地址,用於表示多註冊中心地址。

配置繼承

配置也存在「重複代碼」,也存在「泛化與精化」的問題。好比:Dubbo 的超時時間設置,每一個服務,每一個方法,都應該能夠設置超時時間。但不少服務不關心超時,若是要求每一個方法都配置,是不現實的。因此 Dubbo 採用了方法超時繼承服務超時,服務超時再繼承缺省超時,沒配置時,一層層向上查找。

另外,Dubbo 舊版本全部的超時時間,重試次數,負載均衡策略等都只能在服務消費方配置。但實際使用過程當中發現,服務提供方比消費方更清楚,但這些配置項是在消費方執行時纔用到的。新版本,就加入了在服務提供方也能配這些參數,經過註冊中心傳遞到消費方, 作爲參考值,若是消費方沒有配置,就以提供方的配置爲準,至關於消費方繼承了提供方的建議配置值。而註冊中心在傳遞配置時,也能夠在中途修改配置,這樣就達到了治理的目的,繼承關係至關於:服務消費者 --> 註冊中心 --> 服務提供者

configuration-override

配置向後兼容

向前兼容很好辦,你只要保證配置只增不減,就基本上能保證向前兼容。但向後兼容,也是要注意的,要爲後續加入新的配置項作好準備。若是配置出現一個特殊配置,就應該爲這個「特殊」狀況約定一個兼容規則,由於這個特殊狀況,頗有可能在之後還會發生。好比:有一個配置文件是保存「服務=地址」映射關係的,其中有一行特殊,保存的是「註冊中心=地址」。如今程序加載時,約定「註冊中心」這個Key是特殊的,作特別處理,其它的都是「服務」。然而,新版本發現,要加一項「監控中心=地址」,這時,舊版本的程序會把「監控中心」作爲「服務」處理,由於舊代碼是不能改的,兼容性就很會很麻煩。若是先前約定「特殊標識+XXX」爲特殊處理,後續就會方便不少。

向後兼容性,能夠多向HTML5學習,參見:HTML5設計原理

5、設計實現的健壯性

http://oldratlee.com/380/tech/java/robustness-of-implement.html

Dubbo 做爲遠程服務暴露、調用和治理的解決方案,是應用運轉的經絡,其自己實現健壯性的重要程度是不言而喻的。

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

日誌

日誌是發現問題、查看問題一個最經常使用的手段。日誌質量每每被忽視,沒有日誌使用上的明確約定。重視 Log 的使用,提升 Log 的信息濃度。日誌過多、過於混亂,會致使有用的信息被淹沒。

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

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

  • WARN 表示能夠恢復的問題,無需人工介入。
  • ERROR 表示須要人工介入問題。

有了這樣的約定,監管系統發現日誌文件的中出現 ERROR 字串就報警,又儘可能減小了發生。過多的報警會讓人疲倦,令人對報警失去警戒性,使 ERROR 日誌失去意義。再輔以人工按期查看 WARN 級別信息,以評估系統的「亞健康」程度。

日誌中,儘可能多的收集關鍵信息

哪些是關鍵信息呢?

  • 出問題時的現場信息,即排查問題要用到的信息。如服務調用失敗時,要給出使用 Dubbo 的版本、服務提供者的 IP、使用的是哪一個註冊中心;調用的是哪一個服務、哪一個方法等等。這些信息若是不給出,那麼過後人工收集的,問題事後現場可能已經不能復原,加大排查問題的難度。
  • 若是可能,給出問題的緣由和解決方法。這讓維護和問題解決變得簡單,而不是尋求精通者(每每是實現者)的幫助。

同一個或是一類問題不要重複記錄屢次

同一個或是一類異常日誌連續出現幾十遍的狀況,仍是經常能看到的。人眼很容易漏掉淹沒在其中不同的重要日誌信息。要儘可能避免這種狀況。在能夠預見會出現的狀況,有必要加一些邏輯來避免。

如爲一個問題準備一個標誌,出問題後打日誌後設置標誌,避免重複打日誌。問題恢復後清除標誌。

雖然有點麻煩,可是這樣作保證日誌信息濃度,讓監控更有效。

界限設置

資源是有限的,CPU、內存、IO 等等。不要由於外部的請求、數據不受限的而崩潰。

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

Server 端用於處理請求的 ExectorService 設置上限。ExecutorService 的任務等待隊列使用有限隊列,避免資源耗盡。當任務等待隊列飽和時,選擇一個合適的飽和策略。這樣保證平滑劣化。

在 Dubbo 中,飽和策略是丟棄數據,等待結果也只是請求的超時。

達到飽和時,說明已經達到服務提供方的負荷上限,要在飽和策略的操做中日誌記錄這個問題,以發出監控警報。記得注意不要重複屢次記錄哦。(注意,缺省的飽和策略不會有這些附加的操做。)根據警報的頻率,已經決定擴容調整等等,避免系統問題被忽略。

集合容量

若是確保進入集合的元素是可控的且是足夠少,則能夠放心使用。這是大部分的狀況。若是不能保證,則使用有有界的集合。當到達界限時,選擇一個合適的丟棄策略。

容錯-重試-恢復

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

Dubbo 的服務註冊中心

目前服務註冊中心使用了數據庫來保存服務提供者和消費者的信息。註冊中心集羣不一樣註冊中心也經過數據庫來之間同步數據,以感知其它註冊中心上提供者。註冊中心會內存中保證一份提供者和消費者數據,數據庫不可用時,註冊中心獨立對外正常運轉,只是拿不到其它註冊中心的數據。當數據庫恢復時,重試邏輯會內存中修改的數據寫回數據庫,並拿到數據庫中新數據。

服務的消費者

服務消息者從註冊中心拿到提供者列表後,會保存提供者列表到內存和磁盤文件中。這樣註冊中心宕後消費者能夠正常運轉,甚至能夠在註冊中心宕機過程當中重啓消費者。消費者啓動時,發現註冊中心不可用,會讀取保存在磁盤文件中提供者列表。重試邏輯保證註冊中心恢復後,更新信息。

重試延遲策略

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

數據庫上的活鎖

註冊中心會定時更新數據庫一條記錄的時間戳,這樣集羣中其它的註冊中心感知它是存活。過時註冊中心和它的相關數據 會被清除。數據庫正常時,這個機制運行良好。可是數據庫負荷高時,其上的每一個操做都會很慢。這就出現:

A 註冊中心認爲 B 過時,刪除 B 的數據。 B 發現本身的數據沒有了,從新寫入本身的數據的反覆操做。這些反覆的操做又加劇了數據庫的負荷,惡化問題。

能夠使用下面邏輯:

當 B 發現本身數據被刪除時(寫入失敗),選擇等待這段時間再重試。重試時間能夠選擇指數級增加,如第一次等 1 分鐘,第二次 10 分鐘、第三次 100 分鐘。

這樣操做減小後,保證數據庫能夠冷卻(Cool Down)下來。

Client 重連註冊中心

當一個註冊中心停機時,其它的 Client 會同時接收事件,而去重連另外一個註冊中心。Client 數量相對比較多,會對註冊中心形成衝擊。避免方法能夠是 Client 重連時隨機延時 3 分鐘,把重連分散開。

6、防癡呆設計

http://javatar.iteye.com/blog/804187

最近有點癡呆,由於解決了太多的癡呆問題。服務框架實施面超來超廣,已有 50 多個項目在使用,天天都要去幫應用查問題,來來回回,發現大部分都是配置錯誤,或者重複的文件或類,或者網絡不通等,因此準備在新版本中加入防癡呆設計。估且這麼叫吧,可能很簡單,但對排錯速度仍是有點幫助,但願能拋磚引玉,也但願你們多給力,想出更多的防範措施共享出來。

檢查重複的jar包

最癡呆的問題,就是有多個版本的相同jar包,會出現新版本的 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); } } }

檢查重複的配置文件

配置文件加載錯,也是常常碰到的問題。用戶一般會和你說:「我配置的很正確啊,不信我發給你看下,但就是報錯」。而後查一圈下來,原來他發過來的配置根本沒加載,平臺不少產品都會在 classpath 下放一個約定的配置,若是項目中有多個,一般會取JVM加載的第一個,爲了避免被這麼低級的問題折騰,和上面的重複jar包同樣,在配置加載的地方,加上:

Duplicate.checkDuplicate("xxx.properties");

檢查全部可選配置

必填配置估計你們都會檢查,由於沒有的話,根本無法運行。但對一些可選參數,也應該作一些檢查,好比:服務框架容許經過註冊中心關聯服務消費者和服務提供者,也容許直接配置服務提供者地址點對點直連,這時候,註冊中心地址是可選的,但若是沒有配點對點直連配置,註冊中心地址就必定要配,這時候也要作相應檢查。

異常信息給出解決方案

在給應用排錯時,最怕的就是那種只有簡單的一句錯誤描述,啥信息都沒有的異常信息。好比上次碰到一個 Failed to get session 異常,就這幾個單詞,啥都沒有,哪一個 session 出錯? 什麼緣由 Failed? 看了都快瘋掉,因是線上環境很差調試,並且有些場景不是每次都能重現。異常最基本要帶有上下文信息,包括操做者,操做目標,緣由等,最好的異常信息,應給出解決方案,好比上面能夠給出:"從 10.20.16.3 到 10.20.130.20:20880 之間的網絡不通,請在 10.20.16.3 使用 telnet 10.20.130.20 20880 測試一下網絡,若是是跨機房調用,多是防火牆阻擋,請聯繫 SA 開通訪問權限" 等等,上面甚至能夠根據 IP 段判斷是否是跨機房。另一個例子,是 spring-web 的 context 加載,若是在 getBean 時 spring 沒有被啓動,spring 會報一個錯,錯誤信息寫着:請在 web.xml 中加入: <listener>...<init-param>...,多好的同窗,看到錯誤的人複製一下就完事了,咱們該學學。能夠把常見的錯誤故意犯一遍,看看錯誤信息可否自我搞定問題, 或者把平時支持應用時遇到的問題及解決辦法都寫到異常信息裏。

日誌信息包含環境信息

每次應用一出錯,應用的開發或測試就會把出錯信息發過來,詢問緣由,這時候我都會問一大堆套話,用的哪一個版本呀?是生產環境仍是開發測試環境?哪一個註冊中心呀?哪一個項目中的?哪臺機器呀?哪一個服務? 累啊,最主要的是,有些開發或測試人員根本分不清,沒辦法,只好提供上門服務,浪費的時間可不是浮雲,因此,日誌中最好把須要的環境信息一併打進去,最好給日誌輸出作個包裝,統一處理掉,省得忘了。包裝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規範中的版本號 
            String version = cls.getPackage().getImplementationVersion(); if (version == null || version.length() == 0) { version = cls.getPackage().getSpecificationVersion(); } if (version == null || version.length() == 0) { // 若是MANIFEST.MF規範中沒有版本號,基於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; } } }

kill 以前先 dump

每次線上環境一出問題,你們就慌了,一般最直接的辦法回滾重啓,以減小故障時間,這樣現場就被破壞了,要想過後查問題就麻煩了,有些問題必須在線上的大壓力下才會發生,線下測試環境很難重現,不太可能讓開發或 Appops 在重啓前,先手工將出錯現場全部數據備份一下,因此最好在 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!"

7、擴展點重構

http://javatar.iteye.com/blog/1041832

隨着服務化的推廣,網站對Dubbo服務框架的需求逐漸增多,Dubbo 的現有開發人員能實現的需求有限,不少需求都被 delay,而網站的同窗也但願參與進來,加上領域的推進,因此平臺計劃將部分項目對公司內部開放,讓你們一塊兒來實現,Dubbo 爲試點項目之一。

既然要開放,那 Dubbo 就要留一些擴展點,讓參與者儘可能黑盒擴展,而不是白盒的修改代碼,不然分支,質量,合併,衝突都會很難管理。

先看一下 Dubbo 現有的設計:

design-step-1

這裏面雖然有部分擴展接口,但並不能很好的協做,並且擴展點的加載和配置都沒有統一處理,因此下面對它進行重構。

第一步,微核心,插件式,平等對待第三方

即然要擴展,擴展點的加載方式,首先要統一,微核心+插件式,是比較能達到 OCP 原則的思路。

由一個插件生命週期管理容器,構成微核心,核心不包括任何功能,這樣能夠確保全部功能都能被替換,而且,框架做者能作到的功能,擴展者也必定要能作到,以保證平等對待第三方,因此,框架自身的功能也要用插件的方式實現,不能有任何硬編碼。

一般微核心都會採用 Factory、IoC、OSGi 等方式管理插件生命週期。考慮 Dubbo 的適用面,不想強依賴 Spring 等 IoC 容器。自已造一個小的 IoC 容器,也以爲有點過分設計,因此打算採用最簡單的 Factory 方式管理插件。

最終決定採用的是 JDK 標準的 SPI 擴展機制,參見:java.util.ServiceLoader,也就是擴展者在 jar 包的 META-INF/services/ 目錄下放置與接口同名的文本文件,內容爲接口實現類名,多個實現類名用換行符分隔。好比,須要擴展 Dubbo 的協議,只需在 xxx.jar 中放置文件:META-INF/services/com.alibaba.dubbo.rpc.Protocol,內容爲 com.alibaba.xxx.XxxProtocol。Dubbo 經過 ServiceLoader 掃描到全部 Protocol 實現。

並約定全部插件,都必須標註:@Extension("name"),做爲加載後的標識性名稱,用於配置選擇。

第二步,每一個擴展點只封裝一個變化因子,最大化複用

每一個擴展點的實現者,每每都只是關心一件事,如今的擴展點,並無徹底分離。好比:Failover, Route, LoadBalance, Directory 沒有徹底分開,全由 RoutingInvokerGroup 寫死了。

再好比,協議擴展,擴展者可能只是想替換序列化方式,或者只替換傳輸方式,而且 Remoting 和 Http 也能複用序列化等實現。這樣,需爲傳輸方式,客戶端實現,服務器端實現,協議頭解析,數據序列化,都留出不一樣擴展點。

拆分後,設計以下:

design-step-2

第三步,全管道式設計,框架自身邏輯,均使用截面攔截實現

如今不少的邏輯,都是放在基類中實現,而後經過模板方法回調子類的實現,包括:local, mock, generic, echo, token, accesslog, monitor, count, limit 等等,能夠所有拆分使用 Filter 實現,每一個功能都是調用鏈上的一環。 好比:(基類模板方法)

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(); } }

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

保持儘量少的概念,有助於理解,對於開放的系統尤爲重要。另外,各接口都使用一致的概念模型,能相互指引,並減小模型轉換,

好比,Invoker 的方法簽名爲:

Result invoke(Invocation invocation) throws RpcException;

而 Exporter 的方法簽名爲:

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

但它們的做用是同樣的,只是一個在客戶端,一個在服務器端,卻採用了不同的模型類。

再好比,URL 以字符串傳遞,不停的解析和拼裝,沒有一個 URL 模型類, 而 URL 的參數,卻時而 Map, 時而 Parameters 類包裝,

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

使用一致模型:

export(URL url)  
createExporter(URL url);

再好比,現有的:Invoker, Exporter, InvocationHandler, FilterChain 其實都是 invoke 行爲的不一樣階段,徹底能夠抽象掉,統一爲 Invoker,減小概念。

第五步,分層,組合式擴展,而不是泛化式擴展

緣由參見:談談擴充式擴展與增量式擴展

泛化式擴展指:將擴展點逐漸抽象,取全部功能並集,新加功能老是套入並擴充舊功能的概念。

組合式擴展指:將擴展點正交分解,取全部功能交集,新加功能老是基於舊功能之上實現。

上面的設計,不自覺的就將 Dubbo 現有功能都當成了核心功能。上面的概念包含了 Dubbo 現有 RPC 的全部功能,包括:Proxy, Router, Failover, LoadBalance, Subscriber, Publisher, Invoker, Exporter, Filter 等, 但這些都是核心嗎?踢掉哪些,RPC 同樣能夠 Run?而哪些又是不能踢掉的?基於這樣考慮,能夠將 RPC 分解成兩個層次,只是 Protocol 和 Invoker 纔是 RPC 的核心。其它,包括 Router, Failover, Loadbalance, Subscriber, Publisher 都不核心,而是 Routing。因此,將 Routing 做爲 Rpc 核心的一個擴展,設計以下:

design-step-3

第六步,整理,梳理關係

整理後,設計以下:

design-step-4

摘自:http://dubbo.apache.org/books/dubbo-dev-book

相關文章
相關標籤/搜索