
作者 | 既同
來源 | 阿裡技術公衆号
一 灰階的基本概念
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 為灰階建立核對規則
為了保證新項目上線後及時發現線上資料可能存在的問題,最晚在灰階啟動之前就要将相關的核對規則全部上線。
在灰階項目中,常會出現灰階命中與未命中時落表不一緻的情況。此時建立核對就要考慮如何選取左表。我們把系統的全量請求作為全集,把命中灰階的部分作為子集,那麼灰階命中子集中的資料,必然要與全集的資料保持一定的關系。反之則不然,因為全集中還有部分灰階未命中的資料,無法與灰階命中子集中的資料保持一緻。
舉例來說,灰階系統位于下遊,需要與上遊系統進行核對,確定上遊發來的請求全部被正确的處理了。這時就要用命中灰階之後的表作為左表,上遊請求的表作為右表來建立核對。
此外,對于灰階未命中的部分也需要建立核對來保障一緻性。這裡的處理方式有兩類:如果灰階命中隻是新增落表,而不影響原有落表邏輯,那麼可以先為舊邏輯做全量核對,即在灰階啟動後,無論是否命中,都仍然應當遵循舊邏輯下的一緻性限制;如果灰階命中後資料将會從舊表中遷走,隻寫入新表,就需要對灰階未命中的部分也進行上下遊關系、子集全集關系的分析,然後選取子集作為左表建立核對,這與灰階命中的處理方式是相似的。
上述的原則可以幫助我們檢查在灰階命中與未命中兩種情況下,資料總是一緻的,但是無法確定灰階命中與否這一結果的正确性。要想確定這一點,主要還是要依賴基本的功能測試,其次可以考慮在核對規則中引入與灰階規則等價的條件語句,并在每次灰階推進之後,同步修改這個條件語句。不過這種解法一般隻能用在實時或準實時核對中,對離線資料的核對可能并不适用,因為離線表中曆史資料所遵循的灰階規則,與當下的灰階規則可能是不一緻的。如有需要,可以通過手工單次查詢離線表,并結合灰階開關操作記錄對結果進行判斷。
最後,對于灰階系統的核對規則,我們還要适當提升時效性,因為從發現問題效率的角度講,實時類型的核對是遠優于離線與隔日核對的。灰階初期發現問題的機率更大,修複的成本也更小,但前提是能夠及時的發現。
灰階方案的品質保障政策與設計政策是相比對的,複雜的灰階系統設計一定會對應複雜的灰階測試方案。回到灰階本身的意義來看,它本就是服務于安全生産的,是以對灰階系統進行良好的全面的測試覆寫更是底線中的底線,務必要作為測試工作的重點。本文在此抛磚引玉,希望大家能在灰階品質保障這個話題上,分享更多的經驗與心得。
《實時計算 Flink 版中級課程》
本課程由阿裡雲開發者學院與實時計算 Flink 版産品共同出品,課程内容不僅考慮到大家日常所需的基礎理論知識,還有阿裡技術專家坐鎮,全方位講授如何讓大家聽完課程後能夠實際運用到工作場景,手把手實操示範,讓開發者們掌握更好的大資料處理開發能力。同時,在内容上也會聚焦如何搭建實時數倉、任務調優、高效管理等;3節課程,幫助大家真正解決實操難點,快速實作技術更新!
點選這裡,檢視課程~