一、重試機制的意義
隨着互聯網技術的成熟,各種創新型服務讓人應接不暇,在徵得市場認同的同時爲了應對急劇增長業務和不斷降低的硬件成本,單機作戰的思維已經越來越被邊緣化,SOA,分佈式服務,微服務的架構設計已成爲當下技術支持能力的主流,出門不聊聊這個都不好意思和別人打招呼,然而正如下圖所示,業務系統之間的調用關係會形成一個比較複雜的服務依賴拓撲結構,而結構中的每一個節點都有着至關重要的作用,某個節點出現問題(暫且忽略多機多工的情況這裏只是爲了表明問題)將會帶來不可估量的損失。
本文不談因技術人員製造的驚天bug導致的服務不可用,單從服務可用性入手來聊一聊失敗重試機制的意義,先舉幾個生產環境中遇到的例子:
- 客戶端發起請求,nginx映射指定後端服務,發現後端服務連通超時
- 業務服務之間存在調用依賴關係,A服務在調用B服務,B服務受到請求並返回,但是在返回時網絡故障,A並未收到結果
- 生產端發出消息,消費端處理過程異常導致消費失敗
- 用戶消費產生積分,積分系統因爲能力有限未能及時處理導致用戶無法獲取相應積分權益
- db數據和elasticsearch進行數據同步過程,出現中斷導致數據不一致
- 網絡傳輸文件過程中止
- … …
以上的問題產生的原因可能多種多樣:節點網絡不通暢,鏈接超時,通道堵塞,超時配置,宕機等
總之說了這麼多,其實我們也能考慮到,所謂重試機制的前提是我們信任並確認遇到的問題是暫時性問題,在該機制下有很大的機率保證業務的成功,否則沒有這個前提重試機制的說法就無法成立,正如DBA刪庫跑路了,服務器重試鏈接數據庫N次也無濟於事。所以這個概念和使用場景一定要區分開。
要保證業務的順利執行卻無法保證百分百的數據一致性,及時性,那如何解決類似的問題,就引出了我們今天的話題:失敗重試機制
在業務設計階段,技術人員有必要對業務內部可能出現的錯誤性風險進行梳理,對需要重試的業務點位做到心中有數,如:依賴調用過程中甲方無法控制另乙方的可用性,一但乙方出現鏈接超時的情況,那甲方就需要嘗試重試鏈接,我們相信99.9%的情況下乙方是可用的。
二、重試機制的使用原則
- 梳理可重試類型和使用場景
- 確定並信任可重試場景的成功率
- 重試並不代表一定成功也不代表可以無限制的嘗試,這樣不但解決不了問題還爲其他服務可用性造成麻煩(筆者記得有一次在使用dubbo的鏈接重試機制沒有設置合理的重試次數和鏈接等待時常,使得服務連接數超高,正常業務無法獲取鏈接,最終產生連鎖反應導致系統奔潰)
- 重試機制只是不得已而保證成功率的備用方案,同樣也是存在一定的使用風險,某些場景下要確保業務功能的冪等性,從而避免極端情況下重試機制下重複處理的情況
三、重試機制的設計策略
- 指數退避算法
(名字聽起來高大上,如果不是寫文章我也不知道這樣叫) 它的定義其實就是我們平時所講的階梯式重試方式,在設定好最大重試次數下,重試次數越多時間跨度越長
例如:第一次失敗1s後重試,第二次失敗後2s後重試,第三次失敗4s後重試開源項目 Apache RocketMQ 是典型使用此法的案例:
在RocketMQ 的類:org.apache.rocketmq.common.message.MessageExt 提供了getReconsumeTimes方法,該方法是用於獲取當前消息消費的重試次數,這樣我們可以有效的控制重試次數:@Override protected MQConsumeResult consumeMessage(String tag, List<String> keys, MessageExt messageExt) { QueueMessage<String> message = (QueueMessage)Serialization.byte2object(messageExt.getBody()); WareDTO wareDTO = JSON.parseObject(message.getBody(),WareDTO.class); wareService.addWare(wareDTO); // 獲取該消息重試次數 int reconsume = messageExt.getReconsumeTimes(); //根據消息重試次數判斷是否需要繼續消費 //消息已經重試了3次,如果不需要再次消費,則返回成功 if(reconsume ==3){ // todo } MQConsumeResult result = new MQConsumeResult(); result.setSuccess(true); return result; }
同樣我們可以通過以下形式來控制重試的時間間隔:
messageDelayLevel=1s 2s 4s 8s 16m 32s 1m 2m
- 數據落庫後繼補充處理
即遇到問題先擱置下來,待到合適時機再做嘗試,此方法有一定的適用範圍,例如:對數據一致性要求嚴格但對及時性處理較弱的情況,筆者在與第三方進行商品信息同步的過程,需要同步對方大量的商品數據然後按照自己的業務數據模型進行二次處理並落庫,但由於對方提供的每個商品都是全量數據且並未標註哪些屬性發生了變更,而我方對商品關鍵屬性(價格,庫存,上下架狀態)同步的及時性要求又較高,如果按照一般方式不管發生何種屬性變更都一股腦的直接落庫勢必會對服務器和solr的IO吞吐能力要求極高,而這期間也存在着部分同步失敗的場景,鑑於此種情況採取的設計方案即:拉取商品數據後先判定是否是價格,庫存,上下架狀態的變更,如果是立即執行,否則視爲非關鍵性屬性變更商品放入緩衝表;如果同步失敗放入失敗重試緩衝表,此兩表中的數據我會拉大同步時間間隔,保證數據最終一致性。
- 分佈式處理方式
分佈式的好處之一就是提高服務可達性,這也屬於其天然能力,多機多功,nginx是常見的HTTP和反向代理服務器,其不但支持負載均衡,同時對容錯也有一定的自身處理能力,在nginx的配置文件中,proxy_next_upstream項定義了什麼情況下進行重試,官網文檔中給出的說明如下:Syntax: proxy_next_upstream error | timeout | >invalid_header | http_500 | http_502 | http_503 | >http_504 | http_403 | http_404 | off ...; Default: proxy_next_upstream error timeout; Context: http, server, location
默認情況下 當請求服務器發生錯誤或超時時,會嘗試到下一臺服務器,還有一個參數影響了重試的次數:proxy_next_upstream_tries,官方文檔中給出的說明如下:
Syntax: proxy_next_upstream_tries number; Default: proxy_next_upstream_tries 0; Context: http, server, location
一句話:不能在一棵樹上吊死,你家不行我換下家
四、重試機制的落地能力
- 開源工具
我們所遇到的問題很大部分已經被大廠和領域方向專業的技術人所解決,什麼場景下存在什麼樣的問題使用什麼樣的重試工具,其實只要用好工具就已經能解決我們生產過程中絕大部分的問題,例如:nginx解決了網絡通訊中的重製機制,dubbo提供了服務接口管理及接口調用重試機制,mysql連接池或中間件解決了數據庫連接的重試機制,RocketMq等消息中間件內部解決了消息隊列生產消費過程中的重試機制,Spring retry 項目封裝了大量的重試策略,利用註解對原有的業務代碼沒有侵入可解決業務場景中的重試策略等等等等。
- 架構思維和經驗
在技術架構設計上如果沒有一定的風險意識,當真正遇到這樣的問題時會非常被動及,所以能夠未雨綢繆的做好預防措施是合格架構師的標準之一
- 見機取巧
遵從一句話,技術是爲業務而服務(純技術性公司不在此列),也就是說遇到業務層面的問題,具體情況具體分析,採用當下可採用的最優方法來實現即可,無需糾結這個是否遵從當下技術主流,同樣路都是一條條踩出來的,遇到了坑填了他形成經驗歸結成技術解決方案你就是主流,不然上文提到的商品同步就沒法做了。
與其說重試機制是一種解決暫時 性問題的方式,不如說是一種生活方式:沒有失敗哪來的成功,遇到一次小小的挫折咋能隨意承認不行,男人嘛,都懂!