1. 程式人生 > 其它 >如何設計可靠的灰度方案

如何設計可靠的灰度方案

簡介:一個較大的業務或系統改動,往往會影響整個產品的使用者體驗或操作流程。為了控制影響面,可以選取一批特定使用者、流程、單據等,只允許這一部分使用者或資料按照變更後的新邏輯在系統中流轉,而另一部分使用者仍然執行變更前的老邏輯。這一步是線上系統灰度方案的起點。

作者 | 既同

來源 | 阿里技術公眾號

一 灰度的基本概念

1 一個典型的灰度方案

一個較大的業務或系統改動,往往會影響整個產品的使用者體驗或操作流程。為了控制影響面,可以選取一批特定使用者、流程、單據等,只允許這一部分使用者或資料按照變更後的新邏輯在系統中流轉,而另一部分使用者仍然執行變更前的老邏輯。這一步是線上系統灰度方案的起點。

將使用者按照特定規則分隔為兩類之後,我們主要需要關注命中灰度的這部分使用者,是否按照預期執行了新邏輯、產生了符合預期的資料,以及系統整體的變化等。此階段即灰度觀察階段,線上驗證工作也是其中的關鍵步驟。

隨著系統中使用新邏輯的使用者、訂單等資料的逐步累計,即可證明新系統的正確性、有效性,那麼更多的使用者就應當被遷移進新邏輯中,這一階段一般稱作灰度推進。灰度推進有時是小流量驗證後立即切全量的,也有需要逐步放量的,這需要結合實際業務&系統能力做出決定。

最終,全部使用者被納入到新邏輯的範圍內,此時需要決定是否將灰度邏輯本身和系統中的老業務邏輯同步下線,全部使用者僅可以使用新邏輯,此時即灰度完成。也有由於歷史資料原因,長期無法完成全量灰度切換的,此時業務系統中將會長期駐留兩套邏輯。

2 灰度在解決什麼問題

一個變更如果在釋出後立即全量上線,那麼如果出現系統、邏輯、資料等問題,將會是災難性的,比如全部使用者無法建立新訂單、全部新訂單出現髒資料等,甚至有可能會影響到變更前的資料。

灰度過程就是在規避變更過程中這個最大的風險:全域性影響。通過減小影響範圍,再配合灰度線上驗證、監控報警等手段,將出現問題時影響面,控制在有限的範圍內,如減少訂正的資料量或降低資損金額等。

安全生產規則中所謂的“無灰度,不釋出”就是這個思想,通過灰度儘可能的減少問題的影響面。如果通過灰度過程發現一個線上問題,那麼去掉灰度的保護,可能就會產生一個嚴重的故障。

3 灰度會帶來什麼風險

灰度方案可以規避全域性性的影響,但是會不會帶來其他的風險呢?答案是肯定的,工程中沒有一勞永逸的銀彈。

首先是如何發現灰度過程中的問題

這與上線過程中的監控報警有一定的相似性,二者主要都是依賴日誌&監控&報警規則的建設和配置;但二者又存在一定的差異,如報警閾值如何配置才能有效發現小流量異常?灰度名單外的老邏輯會不會觸發新邏輯的監控報警?灰度系統影響的上下游是否也有對應的灰度監控?這些問題都可能影響灰度問題能否被發現與發現問題的時效性。

此外,對灰度系統要重點關注資損風險。資損欄位在上線前一定要做好核對的保障,或者至少應當在灰度開始階段之前完成,尤其是對新變更引入或影響的資損欄位,要做到全覆蓋,“無核對不上線”。

灰度過程中還可以協同客戶、運營、產品等多條線的同學做好佈防,及時感知處理相關輿情,使用非技術手段作為問題發現的兜底與補充。

其次,如何控制灰度中問題的影響面

灰度過程中產生的灰度資料,不能侵入非灰度資料,反之亦然,要確保二者的充分隔離。

但是灰度系統需要與上下游聯動,灰度本身也需要推進,一旦遇到問題,還需要進行灰度停止、灰度回退等更復雜的操作,因此灰度整體是一個動態的過程,而在整個動態過程中,需要嚴格保持灰度資料&非灰度資料的隔離,否則將會導致問題影響面擴大化,危及整個系統,甚至發生嚴重故障。

這裡尤其需要注意的是灰度停止與灰度回退的複雜性:如果灰度停止手段不能生效,那麼問題影響就無法得到有效控制;灰度回退則需要涉及阻斷灰度流程、修改已有灰度資料、修復錯誤資料等,一般來說是整套灰度方案中最複雜的部分。

最後,發生問題時的處理也會比較複雜

生產系統往往沒有太多的資源或條件進行AB-test,灰度與非灰度資料都是真實的業務資料,一旦出現問題,並不能通過刪除灰度資料或髒資料的方式解決問題,一般需要進行資料訂正,或釋出新的變更進行修復。資料訂正的數量、訂正資料的正確性、如何甄別灰度使用者、如何保證新變更的正確性、如何保證新變更可以有效修復問題資料等,都是恢復過程中的難點工作與潛在風險。

本章結語:

複雜的灰度方案會引入各種各樣問題與風險,整個系統的複雜度也將成倍的增加,對灰度的質量保障方案也會同時變得更為複雜。那麼如何有效的控制這些風險,同時高質量的達成專案目標呢?我們常說,好質量不是測出來的,對於複雜的灰度系統來說,這句話同樣適用。一個高質量的灰度方案,不僅需要完善的測試,更要依賴於良好的設計。保障安全生產和達成專案目標二者絕不是矛盾的,只要灰度方案設計得當,魚與熊掌可兼得之。

二 灰度設計要解決的基本問題

1 灰度維度的選取

生產系統中常見的灰度的規則,有使用者id尾號、業務單據id尾號、白名單、黑名單、時間戳等。

白名單常用於線上測試,如使用測試賬號等進行單獨的驗證。這種方式不適合單獨使用,因為無法快速擴大灰度範圍,但是推薦與其他方式聯合使用,增加灰度過程的靈活性。

黑名單則是一種兜底手段,可以對特殊使用者(如資料量特別大使用者、重點客戶等)進行遮蔽,減少或避免其受到灰度的影響,尤其是在灰度過程出現問題時,直接阻斷其進入系統中的問題邏輯。

採用使用者id尾號或業務單據id尾號作為灰度key,是更常見的灰度區分方式。但如何選取這類灰度key,需要注意幾個要點。

第一,選取的key應當是均勻分佈或近似均勻分佈的,如集團的havanaId等,否則全量使用者無法分批分散的命中新邏輯,灰度的逐步放量的能力就失去了作用,極端地,整個灰度能力會退化為布林化的全域性開關。

這裡容易犯的錯誤並不是使用了全部相同的灰度key,而是誤認為某個id是均勻分佈的。舉例來說,某單元化應用中如果使用使用者id的後四位作為灰度key,那麼很可能會出問題,因為使用者id已經是用於區分單元化的標記了。常見的id的生成本身是隨機的,但觸達業務系統時,可能已經帶有某種特定的規律了,因此需要對此類情況做好識別與防範。

第二,計算key的邏輯需要儘量簡化

系統中使用灰度key來判別走新邏輯還是舊邏輯,這個條件判斷一般會在系統中反覆出現、多次執行,此時如果設計特別複雜計算方式,則會給系統帶來額外的開銷。除此之外,簡化key的計算邏輯也會帶來業務語義上的簡化,便於整個業務鏈上的技術同學與非技術同學快速理解,也便於遇到問題時快速定位與排查,更有利於系統的長期維護。

第三,要結合業務實際選取

如果選取一個當次變更新增的業務欄位作為灰度key,那麼上下游系統是否需要做同步改造?離線資料&報表是否需要配合改造?如果選取一個對下游業務未記錄的或無意義的欄位呢?這些都是通過合理設計可以節省的改造成本。

因此在選取灰度key時,需要選取上下游業務已有的、通用的、具有業務意義的欄位。

2 簡化灰度邏輯

灰度邏輯僅僅是將一個使用者或單據非此即彼的區分開,因此灰度邏輯不僅沒有必要做的太過複雜,而且還應當儘量簡化,如果業務上有條件,最好能用一個欄位或一個變數搞定。

首先,有利於完成灰度進度的調整,如灰度推進,灰度暫停等,可以通過單變數的調整快速完成,否則一次性調整幾個灰度變數,會出現灰度推進情況不符合預期、灰度覆蓋不全,灰度資料不一致等複雜問題。比如同時調整使用者id覆蓋範圍與訂單建立時間,則可能導致一部分使用者被跳過,也可能導致調整後的灰度範圍遠超預期等問題。其實這類問題在實際生產中是最常見的,回想一下,每次在灰度推進或灰度暫停等進度調整時,是不是都需要多人共同監督灰度指令碼,反覆確認釋出內容?甚至在加入瞭如此重的流程之後,仍然不能達到百分百的無問題。

其次,開始灰度後,灰度資料往往錯綜複雜,如果需要多個條件協同判斷,對問題定位則是不利因素,甚至可能會導致誤判。還用上面的使用者id+時間戳的例子來說,原本是灰度邏輯出錯時產生的資料,可能被誤判成由於時間未到而走舊邏輯產生的資料,這種複雜性導致的誤判將會嚴重影響線上問題的止血與處理效率。

最後,對可灰度的使用者或單據,應當寬進嚴出,適當提升灰度准入的門檻,這樣做有利於將大部分資料快速的排除到灰度範圍之外。因為總體而言,當我們決定採用灰度方案去推動變更時,我們總是抱著對系統悲觀的態度,防止潛在的問題快速擴大化。因此在初始階段讓儘可能少的資料走到新邏輯,可以給我們留出時間做人工資料校驗、監控報警有效性校驗、核對有效性校驗等等工作,防止第一波灰度使用者出問題時,直接演變成大問題。那樣的話,就完全失去了做灰度的意義。

這裡還要做一個簡短的解釋,減少灰度變數和灰度命中寬進嚴出二者並不矛盾,前者一般是動態的、配置在開關內供應用讀取的,後者一般是靜態寫在程式碼中的固定條件。舉例來說,某一個變更使用使用者id作為灰度變數,但初期應當設定僅對某等級以上的使用者開放的門檻。

3 灰度資料如何初始化

灰度最好是可以從0啟動的,就是說無需事先通過資料訂正或批量觸發的方式修改初始資料,而是通過某個真實的業務請求來觸發,比如使用者下單等。這裡常見的做法是,當業務請求中的資料命中灰度之後,在建立對應的DB記錄時,打上特殊的標記,用以標識灰度命中。如果有必要的話,還可以單獨建立新的表,在DB中寫入一條新記錄就代表相關的使用者或單據命中灰度。

這種方式的優點就是0啟動,無需前置資料準備流程,但問題是整體的灰度進展可能會變慢。因為在上線前產生的部分部分線上資料已經被確定為僅能走舊邏輯,想要進行全量灰度後的灰度邏輯下線,一般來說,只能等待業務資料自然關閉。

舉一個簡化的例子,灰度啟動前已經付款的訂單走舊邏輯,如果不對這部分訂單資料做處理,那麼只能等待這部分訂單全部確認收貨,才能對灰度邏輯和舊邏輯進行整體下線。而在實際的生產系統,還要考慮退款、計費等等相關流程,所以等待的週期只會變得更長。

但有些灰度方案並不能簡單的通過請求中攜帶的資料進行灰度初始化,還需要對全量的使用者資料做一次初始化。比如將線上A系統中的資料,按一定規則匯入本次變更涉及的B系統中,作為灰度過程的資料準備。這樣做的好處有兩個。

第一是可以在一些場景中簡化灰度門檻的判斷,即可認定所有的資料全部符合某一個前提條件,節約一次判斷。而且這次查詢一般會是一個查庫操作,而使用全量業務資料去查庫,常常會出現DB效能問題,甚至會出現由於灰度資料的分佈問題導致分散式DB出現單庫單表的熱點,這裡的DB問題不做深入。總之這個方案可以有效減輕甚至規避此類問題。

第二就是在業務上可以加速整體的灰度進度,縮短從灰度開始到全量的週期,有時出於業務的考量,我們可能不得不選擇這個方案。

但這樣做的缺點也是明顯的。舉例來說,比如資料初始化的方案是從A表匯入B表,那麼首先需要對資料遷移的邏輯進行經過額外的驗證工作;之後進行遷移資料時也需要佔用一定的專案週期;還要在設計中考慮遷移資料過程AB系統的資料一致性如何保障,比如遷移資料的過程中,A系統有產生了新的業務資料,要遷移嗎?還是遷移時要對A表的部分記錄加鎖?或者甚至停掉A表對應的服務?真的需要停服務的話,那這也太不網際網路了。

4 灰度過程中保持資料一致性

前文描述了灰度初始階段的問題,但是灰度過程往往會從前一個業務步驟開始,隨後才會影響下一個業務步驟。舉例來說,同一個使用者在t時刻命中了灰度規則,並在寫表時打標命中灰度;而在之後的t+1時刻,發生了一個需要更新表記錄的操作,但由於灰度回退或其他原因,導致沒有命中灰度規則,這時要怎麼判定?

這類問題其實就是灰度資料一致性的問題,也是灰度設計中最核心的問題。

原則1:以已有的灰度命中資料為準

在很多業務場景下,前一步寫表後一步更新的操作是非常常見的,建立時打標無需多言,更新時的基本的判斷原則應當是將已有的灰度資料作為判斷標準,而不是以灰度key是否命中為判斷標準。即後一步更新操作時總是以查DB的結果為準:DB中記錄為灰度命中,那麼就要執行新邏輯,否則按照灰度未命中的舊邏輯執行。

原則2:優先考慮灰度推進過程中資料的一致性

當灰度推進時,更多的使用者或單據會被納入到灰度命中的範圍內,因此要考慮此部分資料能否進入新邏輯。

舉例來說,以使用者當月賬單id尾號為灰度規則,那麼使用者的當月賬單一旦被打標為灰度命中,後續賬單再次更新時,也一定要遵循新邏輯;而在賬單建立時如果為灰度未命中,那麼這筆賬單將會一直保持舊邏輯直到結清。

這個原則與前一條有一定的相似性,但核心關注的是灰度進展導致的灰度key命中情況在建立和更新兩個階段發生了變化,這時一般仍要遵循以DB記錄打標為準的原則。

另一方面,灰度推進前已命中灰度的資料,要確保在灰度推進後仍能命中灰度。這是一條不言自明的規則,在確保資料一致性的基礎上,只有這樣才能被稱為灰度推進。但在實際操作推進的過程中,有時會因灰度開關配置錯誤等原因違背了這一規則,因此可以考慮對配置項進行一定的防錯設計。

此外,灰度推進過程中,還需要關注叢集內各機器開關資料資料的一致性。首先要確保變更後的灰度開關值被推送到叢集內的全部機器中,其次為了灰度推進時間的一致性,一般會在灰度開關內加入一個生效時間戳,避免開關推送延遲可能帶來的問題。

原則3:如果需要快速推進灰度,可以嘗試在第一個灰度維度全量後,再開始另一個灰度維度

上述原則中提到,更新資料時發現記錄建立未打標的,即使灰度key已被命中,仍應使用舊的業務邏輯。但是這樣做,整體的灰度進展將會被拉到非常長,比如確認收貨後90天內都可以發起退款,那麼是否要等到4個月之後才能全量切到新邏輯?業務上允許這樣做嗎?

對上述這個例子可以這樣做,就是當訂單建立已經全量灰度後,那麼就可以理解為建立已經全部切入新邏輯,此時繼續在付款或確認收貨操作時進行灰度打標,這樣仍然可以保持一次僅對一個變數進行灰度的原則。

這裡給出幾個不是很理想的快速推進灰度的做法。

1、同時對多個灰度維度做推進

這是上文在討論簡化灰度邏輯時,就力圖避免的一種設計。與其這樣做,還不如在驗證充分之後,對第一個灰度維度直接全量,之後再推進第二個灰度維度。

2、在多個入口同時進行灰度打標

這個方式看上去可以加速消除資料建立時未打標的記錄,但多個入口打同一個標,出現問題的時候怎麼排查原因?更新時要不要覆蓋建立時的標記?灰度暫停時如何同步停止打標?總之這是個複雜度高且驗證工作量大的方案。

3、手工資料訂正

既然要做資料訂正,還不如在灰度啟動前就做一輪,這樣在整個灰度方案開始時就可以獲得收益。灰度進展中做訂正,成本更高,收益卻更低,從ROI角度看很不划算。

灰度設計的過程中,不要輕易嘗試去推翻上述這些簡單的原則,因為越是簡單基礎的原則,其影響就越大,對這些原則的改動,往往會造成前述的設計被全盤推翻。

當然,也存在一些只建立記錄,而不再更新的業務,這種業務考慮的重點往往不是灰度推進,而是下面的灰度暫停&回滾策略。

5 灰度暫停與灰度回滾

灰度是服務於安全生產的,那麼相應的,一定要建立適當的熔斷與回滾機制。

原則4:灰度過程務必具備整體暫停能力,也即灰度熔斷。

灰度熔斷不要求對已經進入灰度的資料進行糾正,而是隻需要不繼續產生更多的灰度資料即可。

為什麼灰度不繼續推進了還不行,還需要加入一個這樣的開關?下面舉個例子。

使用使用者id尾號作為灰度key,已有n個使用者進入新邏輯時,我們發現DB側出現了瓶頸需要修復。這時業務層的應用有三種應對方式可選。

第一,立即縮小灰度範圍或對程式碼進行回滾。這是不可取的,已經命中灰度進入新邏輯的使用者,往往不能再輕易的回退到舊邏輯中。

第二,不繼續推進灰度,也不操作灰度開關,放任系統繼續執行。這也是有風險的,因為現階段只有n個使用者進入新邏輯,但按照使用者群體總數*灰度比例測算,可能還有m個使用者即將命中灰度進入新邏輯,甚至m>>n,如果DB問題不能在使用者大量進入之前修復,整個系統將面臨災難性的後果。

第三,灰度熔斷。不操作灰度開關,但停止新使用者命中灰度規則,即目前有n個使用者進入新邏輯,那麼稍後即使有m個使用者命中灰度規則,也仍然不能進入新邏輯,這樣就可以確保在DB問題得到修復之前,系統保持現狀繼續執行。

通過這個例子可以充分說明,建設灰度暫停能力的必要性。

原則5:可操作的灰度回滾方案,才是有意義的灰度回滾方案。

一般來講,我們都希望可以做到灰度的可觀測、可回滾。但前文反覆講述的這類灰度方案中,避免出現數據一致性問題,對業務來講才是更重要和更安全的。出現問題時機械的執行回滾,反而會造成更大的影響,而使用灰度暫停能力進行快速止血,並積極修復問題,反而是更合適的。

那麼回到灰度回滾方案中來,在保證資料一致性等原則的前提下,可以設計一個合理的回滾方案嗎?

我認為應該是可以的,但遺憾的是,我們在專案實踐中沒能成功的做到這一點。因為工程中的資源往往都是有限的,我們不可能把大量的時間和精力,投入到高度複雜的回滾方案中去。

因此對於灰度回滾方案,我有一些比較負面的結論: 灰度回滾方案的複雜性如果難以控制,那麼正確性也將難以驗證;

複雜的設計將帶來開發週期和測試周期的延長,對業務的傷害可能更大;

就像應急預案需要提前演練一樣,沒有人敢在線上直接使用未經驗證的回滾方案,最終做了也是白做。

所以,我建議僅在模型上更為簡化的業務中,才去考慮設計完整的灰度回滾策略。

本章結語:

本章討論的範圍主要是從技術視角出發的,已經基本可以滿足一個常規的灰度方案的設計要求。但可達成不代表做得好,除了技術手段外,還有更多的其他型別的手段可以應用到灰度方案中,幫助我們將方案變得更加完善、健壯,實現可觀測、可度量等工程目標,構建起高質量的灰度設計方案。

三 更完善的灰度方案

1 具備良好的可測性

我們一般在複雜的專案下才會考慮使用較為細緻或複雜的灰度方案,在專案本身的業務複雜度之上,再疊加灰度引入的技術複雜度,此時如何進行完備的測試就成了一個不小的挑戰。我們需要明確,可測性問題是需要在設計中認真考慮的問題。讓系統內的資料流動&狀態遷移都是可觀測的,把請求、處理資料的過程值、開關值、分支判斷結果等資訊明確的、無遺漏的持久化到日誌或DB中,尤其是灰度是否命中、灰度判定規則等關鍵資訊;而不要讓複雜的系統變成一個黑盒,只有起始的輸入和最終的輸出。否則的話,在除錯和測試的階段,都要花費大量的溝通成本,甚至可能埋下無法被發現的缺陷。

對於日誌的處理,應當儘量保持上下游的一致性。最好的程式碼是自解釋的,最好的日誌也應當是自解釋的。上游的系統如果使用了一個灰度標記,則下游的系統應當使用相同的標記;如果有下游有業務語義的變化,可以新增一個欄位,而不是將上游的同名欄位覆蓋或清除。這樣在跨多個系統或團隊進行聯調或處理問題時,大家對同一個標記或概念都持有同一個理解,這是對提效非常有幫助的。舉個具體的例子,比如上游命中AA規則後記錄了AA=true的日誌,下游根據AA規則衍生了BB規則,那麼記錄日誌時可以保留AA欄位的資訊,再額外記錄BB=true。

對於落庫的資料的處理,則要考慮可核對性方面的問題。資料在跨系統傳遞時,應明確各個系統中的業務主鍵是什麼,下游系統要將上游系統的主鍵或唯一鍵落庫,如果條件允許,還要將上游傳入的鍵值作為平鋪欄位,甚至為其在DB中建立索引。這樣做的好處首先是為了後續建設核對方便,方便使用相同的唯一鍵查詢上下游系統的關聯記錄。這也是為將來的系統擴充套件性做考慮,如果將來下游系統的下游還需要再接其他系統,此時通過上游的這個統一鍵值即可有效串聯多個系統。典型的例子是將交易系統唯一id透傳到下游的所有額度明細、賬單明細、退款等系統中。

以上這些提升可測性的設計思路,不僅針對灰度方案,也針對不涉及灰度的方案;不僅需要測試同學在設計階段識別和發現方案的可測性短板,也需要開發同學有意識的去面向可測性進行設計。

2 關注全鏈路的壓力

系統改造中需要關注對下游依賴的壓力變化,灰度設計中也需要考慮這一點,尤其是系統壓力隨著灰度推進而改變的情況。

一種典型的場景是隨著灰度推進,傳遞到下游的請求越來越多,這是一個比較好理解的例子,這裡不做過多展開。這種情況下,主要需要梳理下游請求的增長率,是隨著灰度推進線性增長、對數級增長、還是指數級增長(指數的情況就很可怕了,極易引發故障)?此外,灰度推進結束之後,流量模型是相對穩定的、還是繼續變化的?

實際業務中的情況並不都是這麼簡單,有的場景中,在灰度開始啟動的時,才是下游流量最大的時候。隨著灰度的推進,下游的流量反而會越來越小。舉例來說,開始灰度後,全量使用者都需要查詢某服務,而命中灰度的使用者可以通過其他短路方式規避這個查詢。如果評估發現這種特殊情況,除了按照常規做出壓力評估,也可以考慮對依賴方案做調整以規避這種反直覺的情況。

其他的可能性還會有很多,再舉一個極端的例子:本月的流量是隨著灰度推進逐漸上升的,經過N天后灰度推進至全量,流量保持穩定;但到了下月1日,業務資料需要重新生成,由於已經灰度全量,導致突然爆發出了非常大的流量,給下游系統帶來很大的影響。這種極端的場景如果不能提前發現識別,並做出合理的應對,則可能引起意想不到的嚴重故障。

除了下游的業務系統,我們還要關注DB側可能存在的瓶頸,我們的業務系統一般可以快速地進行叢集平行擴容以應對大流量,但DB的擴容就比較複雜了,可能會涉及到資料遷移、鎖庫、索引重建等操作,有些操作屬於高危操作,如有不當,甚至會影響使用同庫或同表的其他業務。當識別到這類問題時,需要提前與DBA聯絡,討論合理的擴容方案,在灰度啟動之前,預留充足的時間完成擴容。

當然,所有的壓力評估,都可以用壓測來進行檢驗。但是要把壓測當做對設計成果的驗收,而不是作為發現問題的兜底手段。即將上線之前才發現系統性的問題,可能為時已晚,強行上線或延期,成本都將是巨大的。

3 灰度的進展與監控

首先,監控是灰度前期最重要的觀察手段,建立完整全面的監控,對於上線初期、灰度開放初期、灰度放量初期的資料觀察,都至關重要:上線初期我們重點要關注新程式碼下的舊業務邏輯是否能正常執行;灰度開放初期則要觀察何時出現命中新邏輯的資料,以及進入了哪些業務分支;灰度放量期間則要觀察流量的變化是否能與開關調整相匹配,報錯量是繼續處於低位、還是隨之線性甚至更快速的增加。此外,灰度放量過程中所關注的監控,在後續灰度全量後也仍然需要持續觀察,有些還要建立相應的報警規則。

其次,針對灰度方案的核對也有一些不同。常見的核對一般都是以上游系統對應的A表作為左表(即核對資料來源),下游系統的B表作為右表。但是下游系統在灰度階段會將上游資料做二分類,命中灰度的寫B表,未命中的不寫,此時建立核對就要反轉過來,將下游灰度命中後寫入的B表作為左表,反過來與上游A表建立核對,確保所有命中灰度的資料仍與上游保持正確的關聯關係。

但這裡又有一個問題,一個使用者本應命中灰度,但卻沒有寫入B表,我們如何發現這類問題?這裡我提出一個解法,即仍然建立從A到B的核對,但在核對規則中加入灰度規則的等效條件語句,並隨著灰度推進修改核對規則。但這樣做,核對規則將會非常複雜,而且也對如何設計落庫欄位提出了更高的要求。

最終,還可以為整個灰度方案建立一個小型的報表用於速查,從落庫結果的角度判斷某個特定的使用者或資料是否已經命中灰度。更進一步的,還可以在報表中展示灰度相關的聚合與統計資料,判斷資料分佈是否符合灰度推進節奏,下一步需要加快推進還是暫緩。藉助這些資料,一來為技術側同學在做答疑或問題排查時提效,二來可以向業務側的同學提供灰度的整體資料或區域性情況,以便做出更多業務決策。

4 應急策略與修復手段

灰度推進過程中,我們可以通過各種方式和渠道獲取來自系統和使用者的反饋,包括但不限於監控、核對、使用者諮詢等。當發現不符合預期的、甚至存在嚴重問題的資料與場景時,標準的操作是先止血再修復:通用的止血方案可以是先將業務邏輯開關關閉、下線或回滾掉新邏輯的程式碼;隨後在修復階段對錯誤的資料進行訂正。

但是回到最初討論的問題,我們為什麼要做灰度?灰度本身存在的價值就是在問題初期可以控制影響面,如果只是機械的執行上述的通用方案,為何還要設計複雜的灰度方案?

比如灰度方案中的止血開關,可以設計成全量下線新邏輯,也可以設計成不再產生新的灰度使用者,這個已經在上一章中舉過例子,這裡不再贅述;在有多個灰度維度時,還可以設計成調整A維度與調整B維度可分別實現前述的目的。但需要明確的一點是,止血開關的語義應當設計的儘量簡潔、無歧義,因為止血的意義就在於短時間內無需複雜判斷即可立即執行。

詳細判斷與分析的工作,是在下一步的修復階段完成的。上一章也提到,一般意義上的回滾可能引發更大的問題,所以可以在修復程式碼邏輯或修復資料後繼續推進灰度;當然,進行灰度範圍的回滾也是可選項,比如撤銷之前已經命中灰度的使用者或單據,將其修改為未命中。但這類回滾除了要考慮前述的資料一致性、系統複雜度等問題,還要從業務邏輯上看能否做到前向相容,從產品視角去考量使用者體驗能否在回滾後得到保障,這也是一個要不要去做這類複雜灰度回滾方案的重要判斷依據。舉例來說,前一天使用者命中了灰度,可以使用某個新功能;但第二天灰度回滾後反而不能用了,這大概率會引發使用者諮詢甚至投訴。

安全生產應當擺在重要的位置,但工程的目標從來不是單一的。灰度系統的設計在這個部分上一定要有所取捨,並不能一味地從系統穩定性的角度出發貪大求全,而應當結合實際業務情況,平衡由於複雜度提升而引入的設計、開發、測試、運維成本,和對產品、使用者體驗的影響。

5 灰度方案的終點

討論到這裡,我們基本把最複雜的部分與可能面臨的失敗情形都講完了。下面談談在灰度進展一切順利的情況下,還有哪些事項需要我們關注。

首先是灰度完成後,對灰度開關的下線。最明顯的好處是簡化程式碼複雜度,已經完成的灰度,基本等同於無用的業務程式碼。此時還可以把舊邏輯的程式碼也一同下線,全部直接執行新邏輯,也方便後續其他同學閱讀與維護。不過這一步並不是非做不可,而且可能還會遇到一些限制。

灰度的終極目標當然是全量切換到新邏輯中,但實現這個目標有時候需要花費很長的時間。舉個業務上的例子,比如從5月的某一天起開始灰度一個遠月賬單相關的功能,這時已經有部分使用者產生了8月份的非灰度賬單,那麼按照預期,就要等到9月之後才能在理論上實現全量賬單命中灰度。出現這種情況時,一般要和業務方充分溝通,因為業務上可能無法容忍漫長的灰度週期。壓縮整體週期的手段,除了將灰度開關推到全量,還需要通過資料訂正等方式讓加速資料層面的灰度推進。

不過真實的情況可能更復雜,繼續拿上面的例子來說,這筆8月賬單如果出現使用者逾期,那到9月時也仍然沒有實現全量。在交易相關的系統中也有類似的例子,付款、確認收貨、退款,每個週期都可以很長,如果再遇到糾紛等場景疊加在一起的情況,週期更是不可控,所以基本不可能在設計中對這類長尾值進行良好的處理。一般來說,在整體趨近於全量後,總會有個別的異常資料、離群資料的出現。所以從工程的角度來看,只要非灰度的資料趨於收斂,就是符合預期、可以接受的情況。

6 灰度的代價

前邊我們反覆提到過,在設計灰度方案中要有所取捨,尤其是要對最複雜的部分做取捨。不過簡單的部分就沒有代價了嗎?不是的。專案中實現了一套灰度方案,就一定會付出相應的成本。應當總是從成本收益比的角度出發,來評價一個設計方案的價值,進而決定其最終應被保留還是被捨棄。

首當其衝的代價,就是複雜度的提升,這一點已經在上文多次提到。一般來講,我們是能夠在複雜專案中承受這一點的,因為專案本身的複雜度已經不低了,從邊際效應的角度來理解,再加一點複雜度也不會造成太多額外的開銷。但是對簡單的專案,我們就要思考是否需要採用灰度了;或者出於安全生產的需要,我們一定要進行灰度,那簡單的專案也一定要匹配最簡化的灰度方案,避免造成大炮打蚊子式的浪費。

其次要面臨的問題是釋出時間延遲。設計、開發、驗證環節都要花費額外的工作量和研發週期,來保障灰度邏輯的正確性與有效性,而且大概率還會出現修復灰度問題的時間。如何在專案開始時合理評估灰度引入的工作量,也不是容易的事情,因為灰度邏輯往往與業務邏輯正交。單從用例數量的視角看,理論上每增加一個灰度開關,相關的功能用例數量就要翻一倍。當然我們可以結合實際業務排除一些用例,但這樣的用例數量增長趨勢對於專案整體而言,並不是一個好訊號。

接下來還有一個問題,就是灰度會使專案週期拉長。這裡的週期指的是從釋出後到灰度全量的週期。上面已經有案例說明,5月開始灰度的專案,到9月甚至更晚才能真正完成全量,這聽起來就讓人難以接受。極端地,這種漫長的灰度過程還可能會影響下一個專案的設計與上線,甚至會將影響擴散到下游系統中。如果出現這種情況,已經可以算作是設計失敗了。

最後就是要慎重考慮不做灰度。不做灰度其實是反規則的,一般我們也不建議這樣做。但工程上的事情總會有例外,有時也會遇到一些業務場景無法灰度,或者灰度還不如不灰度。如果決定不做灰度方案,那最好把灰度帶來的問題和不做灰度的收益都提前整理好,同時也要充分評估放棄灰度的風險,讓專案組的其他同學都能理解認同這個決策。

本章結語:

一個專案或產品的質量從來不是測試測出來的,而是在設計階段就構建起來的。希望通過上述兩章提及的各類設計手段與思路,給大家更多的輸入與啟發,在將來設計構建出更穩定健壯的工程。也歡迎大家對文章中的各項內容給予補充、指正。

下一章將從測試的角度討論如何保障複雜灰度方案的正確性。

四 灰度方案的質量保障

之前的章節主要針對灰度方案的設計展開,但一個系統的正確性、穩定性,除了要依賴有效的設計,還需要全面合理的測試來保障。這一章就對灰度方案的質量保障體系進行詳細的討論,列舉灰度系統中的各個測試覆蓋要點。

1 灰度基本邏輯

這是最基礎的測試點,即如何將資料非此即彼的區分開:滿足預設的條件即為命中灰度,否則不命中灰度。

灰度命中的結果,不僅要是可預期的,還應當是穩定的。使用同樣的資料與配置,不能在某次請求時命中灰度,另一次請求時卻未命中灰度,否則將產生嚴重的問題。

舉例來說,如果使用者A在第一次命中灰度後,在將命中結果落庫時,但意外的影響了灰度判斷條件,那麼稍後使用者A再來請求時,就可能出現無法再次命中灰度的問題。這型別的低階缺陷要儘早發現,否則會阻塞後續的其他測試。

2 灰度命中後的持久化

命中灰度的資料有時還需要持久化到資料庫中,在測試中除了檢查灰度標記,還要檢查新增的欄位。如果灰度命中後寫入全新的表,也要對全部欄位進行完整的校驗。

落庫的資料與上游有關聯關係的,要檢查記錄是否一致,如果可行,最好推動開發將其設為平鋪欄位,方便在上下游間建立核對。單據號等欄位具備唯一性的,要額外做冪等性測試,防止同一條灰度命中資料多次寫入。

除了結果資料,過程資料也至關重要。判斷灰度過程中可能經歷了多個條件,那麼需要將每個條件的輸入值、判斷結果值都列印在日誌中,方便聯調與後續問題排查。此外還要檢查日誌中變數名的唯一性與變數值的正確性,防止列印語義混淆的廢日誌。

3 灰度相容性

由於灰度過程中的請求會分為兩部分,因此係統內應當對兩類請求都有相應的處理能力,即在灰度全量之前,舊邏輯仍要保持可用。

新版本程式碼中的舊邏輯,在本質上已經和上一個版本的邏輯有所差異,因為上一個版本中是未經灰度判斷,直接執行舊邏輯;而新版本程式碼中是多一層判斷邏輯的。這層判斷邏輯有時可能還會在入參上新增各類標記後,再進入系統的下游模組流轉,並會引發更多複雜的情況。比如系統對未命中灰度的資料加入了一個屬性,但下游流程判斷有任意標記的流量都不再處理,那麼這種情況下的舊邏輯就會受到影響。

如果灰度系統涉及多個應用,還要考慮應用間的相容性。常見的測試要點包括:

灰度系統是否影響了上下游的流程互動,如命中未灰度走A應用,灰度命中則不走A應用,這樣對A應用的監控和核對是否會造成影響;

灰度是否新引入了下游依賴,原有的依賴關係是否被解除或需要削弱,強弱依賴的設計是否合理,具體的依賴關係如何,是否引入了迴圈依賴,或者資料流是否構成迴環;

上下游應用均有改動時,在下游應用先行釋出後,是否會影響尚未釋出的上游應用。

4 灰度推進

灰度從0開始,到部分覆蓋,再到全量覆蓋,這個灰度推進的過程也需要測試重點關注。

首先是一頭一尾的情況,灰度開關配置為全量老邏輯和全量新邏輯的情況下,請求的結果是否符合預期;

其次是灰度推進的過程中,如果使用者A在上一次請求時未命中灰度,但下次請求時由於灰度範圍擴大而命中了灰度,那麼使用者A的請求能否正常處理?使用者A能否按照預期被納入或排除出灰度新邏輯的範圍內?

最後還要評估灰度推進可能引起的相容性問題,這裡要關注的點是在灰度開關變化的情況下,動態的評估內部邏輯的相容性,而這可能是上述靜態的相容性測試不能覆蓋的點。這裡需要結合實際業務與設計方案仔細分析,排除可能的、隱藏較深的、重現條件較為複雜的缺陷。舉例來說,當月A使用者第一次請求時未命中灰度,故寫入一條不帶灰度標記的記錄,意味著本月A使用者將不再命中灰度;當A使用者第二次請求時,查詢是否存在灰度不命中的記錄時服務超時,且由於灰度推進導致A使用者變為灰度命中,故又寫入了一條帶灰度標記的記錄,導致庫中同時存在兩條業務語義存在相矛盾的記錄。

5 灰度暫停或灰度熔斷

上文已經反覆講過,灰度熔斷的功能對灰度方案至關重要,在某些關鍵時刻甚至是系統唯一的逃生路徑,因此對這裡需要格外重視。

第一,熔斷開關關閉時,要確保沒有新增的灰度流量進入。這裡有兩層含義,一方面是未命中灰度的資料不能再命中灰度,另一方面是已經命中灰度的資料,要視灰度系統是否可回滾、是否前向相容,決定是否可以繼續命中灰度。

第二,熔斷開關關閉時,要保證其他部分的灰度邏輯不受影響,這也是基本邏輯測試的一部分。

對此類應急方案的測試,還需要結合實際業務場景進行設計考慮,比如存在多個其他業務邏輯的開關時,是否要對所有開關組合進行測試,還是優先測試業務實際使用的組合,或者僅測試應急場景下必定出現的且數量有限的幾個組合即可。

6 灰度回退

上文提到,在可能涉及灰度資料一致性問題的灰度方案中,我們一般不推薦引入複雜的灰度回退邏輯。但不可否認的是,灰度回退在部分場景下仍然是有價值的,此時也需要通過測試手段保障回退能力的質量。

首先是回退過程不再新增灰度命中資料。這裡的保障要點,與熔斷開關開啟後是一致的。

第二是回退過程中,已命中灰度資料的一致性保障,這裡最需要關注的場景是,在前一個業務流程中已經命中灰度的資料,在下一個業務流程中沒有命中灰度時,系統將會如何處理。如,訂單建立時命中灰度並打標,付款階段反而不命中灰度,則此時需要將灰度標記移除。

此外,還要對灰度開關的回退能力進行測試,如果灰度開關存在多個維度或限制條件,這裡的測試用例組合也會非常複雜,但與灰度推進邏輯的測試方案有一定的相似性,可以作為參考。

最後,灰度回退的過程一般還需要藉助資料訂正的手段對已經落庫的灰度資料做變更,這裡不涉及程式碼流程的測試,可以考慮建立核對規則進行保障。

7 對異常配置的容錯

灰度邏輯底層常會依賴一個switch開關或diamond配置項,但進行配置時也有可能引入錯誤。把整個系統看成一個木桶,那麼配置項常常是最短的那塊木板。我們應當通過優化設計,規避由配置類問題導致的更嚴重問題。

首先,對灰度開關錯配時,應用不能接收,仍應使用上一次的正確配置。雖然在diamond配置項中輸入錯誤的配置值後,中介軟體層總會將這個錯誤值持久化,但應用可以在此時報錯,並棄用中介軟體下發的錯誤值。

此外,如果在業務上有可行性的話,還可以在每次接收到錯誤值時,採用預設值來做兜底處理。

典型的例子是,前一版本的灰度配置包含尾號為00、01的使用者,而後一版本的灰度配置中只包含尾號為0的使用者,不包含尾號為1的。如果這個配置生效,那麼尾號為01的使用者的資料一致性將被破壞;此時若對後一版本的配置做校驗,識別發現尾號為01的使用者原本可命中灰度,但在推進後反而不命中,則可以避免這個問題。

這一點既是測試設計要考慮的異常邏輯,也是方案設計階段需要考慮的防錯機制(Poka-yoke)。

8 對異常資料的容錯或報警

如果灰度過程中發現缺失了某個新欄位,但可以通過一定的回補機制寫入的,那麼最好可以進行靜默處理,容忍這樣的錯誤資料。比如本應在使用者瀏覽商品時對使用者打上灰度標記,但後續加購時發現灰度範圍內的使用者仍然不帶灰度標記,則此時可以再次對使用者進行灰度打標。

但如果系統中核心依賴的欄位遇到資料一致性錯誤時,就應當立即停止繼續處理。如一個已經帶灰度命中標記的訂單,在確認收貨時,缺少了一個應當在付款階段寫入的關鍵的新欄位。那麼此時應當不作處理,通過記錄錯誤日誌、丟擲異常等手段,觸發外部的監控報警,等待人工介入。

這裡可以藉助異常注入類的工具來簡化測試方案。通過破壞灰度資料的一致性,檢驗系統對異常資料的處理是否符合預期。這部分功能的正確性,在遇到灰度回退等複雜情況時,將會起到很大的作用,如首次請求灰度未命中、二次請求時進行灰度命中補償;或回退時資料訂正不完全,系統處理此資料時觸發報警,提醒再次訂正等。

9 對外部系統的影響

除了要關注業務系統內部的資料流轉情況,有時還要考慮對外部系統影響,比如在執行到某個節點時對外發送訊息,而下游有若干外部業務方的監聽者需要在收到訊息後執行對應的系統邏輯;或者最常見的,落庫的資料會定時的寫入離線資料表中。

對外部系統的影響,應該在變更前期、設計方案確認後等關鍵節點,及時向下遊業務方同步,評估下游需要的改動,並在預發環境進行有效的串測、驗收,如有必要還要為新邏輯產生的資料單獨建立監控或核對;此外,在灰度推進階段需要向下遊同步灰度變化節奏,觀察監控變化情況是否符合預期。

對離線表的影響有兩方面,首先要為變更的部分建立新的核對規則,其次也要評估對原先建立在這些離線表上的核對規則是否有影響,是否會導致核對誤報或漏報。

10 灰度流量模型分析

灰度過程的流量模型是動態變化的:首先,在灰度未開始推進的初始狀態下,就已經與上一版本的流量模型存在一定差異;隨後隨著灰度的推進,流量模型又會逐漸發生變化;最終在灰度全量後達到穩定。

在變更上線後、灰度啟動前的階段,一般不會與上一個版本的服務或DB依賴存在太大的出入,否則這些變化也應當被納入灰度流程。這階段主要需要對服務呼叫和DB新增欄位進行評估,判斷是否存在複雜的計算邏輯,或對DB讀寫存在影響。

相比之下,灰度推進階段需要分析的點會比較多。灰度推進過程中,灰度判斷邏輯的查詢介面,按灰度命中結果分流後兩套業務邏輯介面,落庫時的DB,其他依賴或下游方的流量,都在同步的變化著。這裡需要對這些變化點做逐個梳理,再分析流量變化可能引起的後果。

下面列舉幾個常見的隨著壓力逐漸變大,效能出現較大問題的場景:

  • 觸發下游服務限流,導致本系統的業務失敗率升高;
  • 下游服務rt變長,本系統業務隨之超時,失敗率升高;
  • 灰度推進時,命中灰度的key值選取不當,經過分庫分表規則後,導致單庫熱點;
  • 灰度推進時,推進範圍過大,導致短時間寫庫請求過大,引起整庫流量或效能抖動;
  • 為灰度欄位新增的DB索引,不適用於灰度推進過程中的流量模型,導致DB效能不及預期。

灰度全量之後,流量模型將會達到或逐步達到一個新的穩定態,除了繼續觀察上述灰度推進過程中的各個要點,還要考慮在全量之後做切換的動作,比如對灰度判斷邏輯做短路,以減少一次查詢;或者將灰度條件的查詢操作,從一個介面遷移到另一個性能更好的介面上。總之這個階段可能只有效能優化,不太會有讓整體效能變差的情況,此類優化除了確保基本功能的正確性外,無需過多關注。

11 對灰度系統進行壓測

從上一節的列舉的情況看,壓力瓶頸常會出現在新增的服務介面與DB這兩處,需要結合業務具體分析。但分析並不是萬能的,新的介面或新的庫表在上線前一般要按照規劃的流量要求進行一輪壓測,確保沒有因為分析遺漏導致的隱藏缺陷。

壓測流量的設定,需要結合當前線上業務的介面呼叫量進行評估,可按灰度全量後的流量值再放大1.2~2倍計算。放大的目的一方面是為了應對峰值流量,另一方面是為了快速暴露問題。常見的問題是流量在下游被成倍放大,比如一次請求,呼叫了兩次某介面,當流量較小時,二者間的倍數關係體現的不明顯,可能還會被誤認為同時間段線上真實流量增大引起的擾動,導致無法發現問題;但流量較大時,倍數關係將會立即顯現。

如果壓測流量較大,需要在釋出上線後使用線上叢集做壓測,那麼還要考慮影子資料與真實資料隔離的問題。使用影子請求壓測時需要按照全量灰度命中的新邏輯來執行,而對線上真實請求還不能開放灰度。這種情況需要在程式碼中額外新增一個供壓測使用的開關,通過在入口處判斷請求的壓測流量標記欄位,判斷是否執行灰度邏輯。

12 為灰度建立核對規則

為了保證新專案上線後及時發現線上資料可能存在的問題,最晚在灰度啟動之前就要將相關的核對規則全部上線。

在灰度專案中,常會出現灰度命中與未命中時落表不一致的情況。此時建立核對就要考慮如何選取左表。我們把系統的全量請求作為全集,把命中灰度的部分作為子集,那麼灰度命中子集中的資料,必然要與全集的資料保持一定的關係。反之則不然,因為全集中還有部分灰度未命中的資料,無法與灰度命中子集中的資料保持一致。

舉例來說,灰度系統位於下游,需要與上游系統進行核對,確保上游發來的請求全部被正確的處理了。這時就要用命中灰度之後的表作為左表,上游請求的表作為右表來建立核對。

此外,對於灰度未命中的部分也需要建立核對來保障一致性。這裡的處理方式有兩類:如果灰度命中只是新增落表,而不影響原有落表邏輯,那麼可以先為舊邏輯做全量核對,即在灰度啟動後,無論是否命中,都仍然應當遵循舊邏輯下的一致性約束;如果灰度命中後資料將會從舊錶中遷走,只寫入新表,就需要對灰度未命中的部分也進行上下游關係、子集全集關係的分析,然後選取子集作為左表建立核對,這與灰度命中的處理方式是相似的。

上述的原則可以幫助我們檢查在灰度命中與未命中兩種情況下,資料總是一致的,但是無法確保灰度命中與否這一結果的正確性。要想確保這一點,主要還是要依賴基本的功能測試,其次可以考慮在核對規則中引入與灰度規則等價的條件語句,並在每次灰度推進之後,同步修改這個條件語句。不過這種解法一般只能用在實時或準實時核對中,對離線資料的核對可能並不適用,因為離線表中歷史資料所遵循的灰度規則,與當下的灰度規則可能是不一致的。如有需要,可以通過手工單次查詢離線表,並結合灰度開關操作記錄對結果進行判斷。

最後,對於灰度系統的核對規則,我們還要適當提升時效性,因為從發現問題效率的角度講,實時型別的核對是遠優於離線與隔日核對的。灰度初期發現問題的概率更大,修復的成本也更小,但前提是能夠及時的發現。

本章結語:

灰度方案的質量保障策略與設計策略是相匹配的,複雜的灰度系統設計一定會對應複雜的灰度測試方案。回到灰度本身的意義來看,它本就是服務於安全生產的,因此對灰度系統進行良好的全面的測試覆蓋更是底線中的底線,務必要作為測試工作的重點。本文在此拋磚引玉,希望大家能在灰度質量保障這個話題上,分享更多的經驗與心得。

原文連結

本文為阿里雲原創內容,未經允許不得轉載。