分佈式程序設計早知道-關於分佈式程序設計常見問題分析

[url=]分佈式程序設計早知道-關於分佈式程序設計常見問題分析[/url]

雖然系統越來越複雜,以及新分佈式架構設計的思想普及,越來越多的系統採用了分佈式的架構,特別是HTTP爲交互方式的接口調用,移動端和PC端的並行對分佈式架構帶來了很大的推動。各式各樣的服務接口,在處理業務流程之外有一些共性的問題,正視設計和解決這些問題,會大大提高程序的可用性,擴展性和可維護性。
以下總結是筆者工作中對於分佈式設計問題的總結,具體內容如下:

1、日期格式

時間在生活中是一個容易忽視但是影響很大的一個因素,在古代,是否有自己的歷法是評定一個統治者是否建立通知的一個重要的標誌。應用程序中大量存在日期數據,比如數據的創建時間,更新時間,業務處理時間等。接口交互中關於日期傳輸需要考慮兩個問題:格式和精度。

日期的格式主要有兩種主要的方式:可讀方式和不可讀(不容易直接讀)的方式。比如對於當前時間(筆者寫此文章的這一刻),可以使用2017-03-01 20:30:30或者2017/3/1 20:30:30等其他的明確年月日時分秒各個元素可讀格式表示;另外一種方式爲以”1970-1-1 08:00:00″爲基準的long型計數法。可讀型的格式在調試,排查問題時方便易懂,但是在開發過程中可能需要做數據解析,轉換爲開發語言識別的類型用於日期的操作,增加了部分開發工作量。long型格式在執行效率上要高效,但是在開發調試過程中需要轉換表示方式。雖然本人更加贊同long型的格式,但是如果按照可讀型的方式返回,對於大多數應用不會有很大的區別。但是需要統一方式,系統中多個服務提供方對於日期格式的不統一會給客戶端調用帶來大量無效代碼的開發和錯誤引入的可能性。

常用的計算機或者開發語言關於時間的最小單位爲微秒,但大部分的業務使用秒級已經滿足需要了,所以可以定義通用單位爲秒,如果需要接口可以自定義可精確到微秒甚至更低。如果沒有通用單位的定義,在以long型傳輸時會導致差之毫釐謬以千里的錯誤。

2、小數的傳輸

我們先來看一個簡單的浮點數計算小程序,以java語言爲例:

[size=1em]
[size=1em]1

[size=1em]2

[size=1em]3

[size=1em][size=1em]double a = 1;
[size=1em]double b = 0.99;
[size=1em]System.out.println(a - b);



結果爲:0.010000000000000009
計算機的世界只有0和1,現實中的十進制的整數、小數都會轉換爲二級製表示,在進行轉換會導致精度丟失。
浮點數在計算機內部存儲的格式轉換過程如下:

大部分的編程語言都會有更高精度方式處理浮點數的運算,保證計算的精確性,比如Java語言的BigDecimal,我們使用BigDecimal再次測試1-0.99:

[size=1em]
[size=1em]1

[size=1em]2

[size=1em]3

[size=1em][size=1em]BigDecimal abd1 = new BigDecimal(a);
[size=1em]BigDecimal bbd1 = new BigDecimal(b);
[size=1em]System.out.println(abd1.subtract(bbd1));



結果爲:
0.0100000000000000088817841970012523233890533447265625
說好的解決問題呢?
原來我們在已經定義了double類型初始化了a、b,當我們使用a、b來初始化BigDecimal對象時精度已經丟失,所以雖然採用了更高精度表示的BigDecimal類進行浮點數運算,仍然不能正確的獲得差。
正確的方式爲:

[size=1em]
[size=1em]1

[size=1em]2

[size=1em]3

[size=1em][size=1em]BigDecimal abd = new BigDecimal("1");
[size=1em]BigDecimal bbd = new BigDecimal("0.99");
[size=1em]System.out.println(abd.subtract(bbd));



結果爲:0.01

因此我們在數據交互中,浮點數最好以字符串的方式進行傳輸,保證調用方在進行浮點數計算時可以採用不損失精度的方式進行初始化並運算。

3、結果值的格式

如果你服務提供方是否經常會聽到這樣的抱怨:結果值返回格式不統一,結果值的屬性名不一樣,屬性的個數不一樣,值表示的含義不一樣,等等。如果具有通用意義的調用狀態,錯誤碼,錯誤信息提示,業務結果返回方式風格不統一,調用方在進行封裝接口調用,錯誤重試,結果值解析等通用功能時,將不得不花更多的精力,以及犧牲程序可讀性,擴展性。

推薦使用以下格式作爲接口的返回值:
status: 接口調用狀態,用於返回操作結果的狀態 比如 200:成功 400:參數錯誤 500:服務器端錯誤
errorCode: 用於失敗狀態時的原因編碼,用於客戶端接口返回失敗流程判斷或者結果顯示
message: 成功或者失敗的消息提示,用於展示
data:用於表示返回接口業務數據的屬性

分析一些知名公司的開發平臺接口,存在把status和errorCode合成一個屬性表示接口,但筆者認爲分開表示更友好,因爲調用結果狀態是有限的,比如成功,參數錯誤,失敗等,是一個業務無關,但是失敗的原因(errorCode)是業務相關的,並且可以無限多,並且多個服務端的錯誤碼存在共用的可能性,因此建議使用兩個屬性表示結果狀態和錯誤編碼。

對於返回的業務數據,建議把data作爲Map格式,把結果值作爲map的鍵值對存入,方便返回多個業務數據的情況。

4、冪等性

冪等性:指一次和多次請求某一個資源應該具有同樣的副作用。冪等性是分佈式系統設計中十分重要的概念,在分佈式系統中網絡抖動(不可預知的短期不可達到),業務超時等都可能導致調用方沒有收到服務器端的返回值或者不是預期的返回值(比如:應該接收一個JSON字符串,但是返回了一個網絡異常),爲了保證系統的高可用性,重試是一個經常會採用的方法,因此服務器端可能會收到多次調用,保證多次調用具有相同的副作用是接口的重要屬性。

如上圖所示以上5個步驟中任何一個流程出現問題都會導致客戶端不能接收到預期的結果,可能執行重試,從步驟3開始服務器端的業務可能已經執行完畢,如果重試表示服務方會接收兩次相同的調用。服務方識別出重複調用,並且當已經執行過業務邏輯,不再次執行重複並返回正確的結果就是冪等性。

如何實現冪等性呢?
1、業務天然符合冪等性,比如大部分的查詢,無論何時調用不會對服務方數據有變更,因此天然具有冪等性。
2、基於入參的關鍵字段進行邏輯判斷,比如在下單時扣減積分的場景,業務規定了每個訂單隻能扣減一次積分,因此在積分服務可以使用訂單號作爲關鍵字段做積分是否已經扣減的檢查,如果已存在當前扣減積分的訂單號表示已經執行過,直接返回結果,否則執行積分扣減並返回結果。
3、存在一些調用沒有關鍵字段的情況,比如用戶購買商品下單,無論是商品ID,商品數量,價格,會員ID都無法作爲關鍵字段用於檢查是否已執行,有人說可以使用入參是否完全一致作爲判斷條件,實際中這樣是可以作爲判斷依據,但是理論上我們無法完全斷定是用戶的主動行爲還是系統的異常重試。對於這種場景,引入一個業務無關的關鍵字段可以更好的解決問題,無論命名這個字段爲requestId,ticketId或者其他,有如下要求:a、這個字段是由客戶端生成的,b、全局唯一的(至少要一個可能會發生重試的時間範圍或者空間範圍內是唯一的),服務器端可以依據該字段作爲是否重複調用的依據。

如何判斷是否存在關鍵字段,也存在隨着業務發展以前作爲關鍵字段的數據不能作爲判斷依據了,因此在設計開發時所有的接口都添加requestId(假設我們採用了requestId這個命名),使用requestId作爲冪等性的依據可以提高接口的可用性和重試的校驗功能的重用性。

5、接口安全

作爲接口提供方有時候會遇見一些場景:錯誤的入參,導致業務異常、錯誤提示,但是不能確定數據是哪裏產生的;接口升級不知道需要通知哪些調用方,因爲調用方都可以通過文檔自行接入;部分調用方需要特殊業務處理,但是沒有關鍵字段把它與其他調用方區分;數據統計缺少關鍵字段判斷來源等。

因此入參中含有準確的表示調用方字段,並能夠識別是否爲調用方身份,可以提高系統的安全性,擴展性。
常見的方式有:
1、分配標識符appCode,每次調用傳遞該字段作爲判斷依據,優點:使用簡單,缺點:但是appCode可以被複用,導致無法準確判斷身份。
2、分配標識符appCode,並分配祕鑰,採用對稱加密的方式對入參簽名,優點:使用方便,相比非對稱加密效率高,缺點:祕鑰至少需要雙方甚至多方同時保存,安全性和準確性降低。
3、分配標識符appCode,並分配非對稱加密的密鑰對,對入參使用非對稱加密的方式簽名。優點:安全性和準確性高。缺點:效率低。

對比以上常用的方式的優劣,建議採用在性能不敏感(非對稱和對稱加密在對入參進行Hash後簽名的實現方式時效率差別不大)的情況下儘量採用非對稱加密,因爲在多對多的交互場景中,非對稱加密比對稱加密的簽名方式更高的安全性。
對稱加密在多對多交互的場景中交互如下:

上圖所示:公用密碼會導致密碼擴散,非公用密碼導致調用方保存多套密碼。

採用非對稱加密多對多交互方式:

上圖所示:CS祕鑰對分別在調用方和服務方保存,保證了安全性和準確性。

6、數據字典

分佈式架構使系統中不同的模塊可以在不同的團隊開發維護,因此屬性含義和命名的一致性的要求急劇上升,無數次的參數命名轉換就是命名不一致的墓碑。

命名不一致主要包含2個方面:
1、含義不一致,曾經參與開發的一個系統,其中Product表示了至少兩種差別很大的含義。a、表示交易時用戶購買的商品,b、交易完成後對外提供服務的抽象命名,同時多次交易可能修改同一個服務,系統中關於這個數據的轉換滿天飛。
2、命名不一致,因爲各種開發語言天然支持字母類命名的,因此常用的類名,方法名,變量等都會以英文命名,同時程序員英文水平的參差不齊,所以導致同樣的含義命名各式各樣,比如積分,有Point,也有Score,還有命名爲integral。

如果沒有很好溝通和協商,就會導致調用方各種轉換,增加大量無用屬性轉換代碼,並且容易引入錯誤。因此一個符合團隊條件的數據字典管理(wiki,文檔,git,應用程序等)可以提供開發的效率和可用性。

以上幾點是筆者在項目開發時的心得體會,若能在你設計開發分佈式系統時帶來些許幫助,就深感欣慰。當然分佈式設計開發不僅僅以上幾個問題可以探討,比如事務,交互過程的超時處理,重試機制等,限於筆者水平以及關於通用問題的判斷,本文沒有涉及,見諒。
如有錯誤也請不吝賜教。


作者:孫豪傑 http://www.sunhaojie.com


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章