本文開始抡蛙,我們實(shí)戰(zhàn)一個(gè)新的項(xiàng)目:灰度發(fā)布組件安吁,這里對灰度發(fā)布組件進(jìn)行需求分析待榔,搞清楚這個(gè)組件應(yīng)該具有哪些功能性和非功能性需求。
需求場景
- 我們開發(fā)了一個(gè)公共服務(wù)平臺涤妒,提供公共業(yè)務(wù)功能苏研,給其他產(chǎn)品的后端系統(tǒng)調(diào)用菊霜,避免重復(fù)開發(fā)相同的業(yè)務(wù)代碼奠衔。
- 最初,公共服務(wù)平臺提供的是戚绕,基于某個(gè)開源 RPC 框架的 RPC 格式的接口纹坐。在上線一段時(shí)間后,我們發(fā)現(xiàn)這個(gè)開源 RPC 框架的 Bug 很多舞丛,多次因?yàn)榭蚣鼙旧淼?Bug耘子,導(dǎo)致整個(gè)公共服務(wù)平臺的接口不可用,但又因?yàn)閳F(tuán)隊(duì)成員對框架源碼不熟悉球切,并且框架的代碼質(zhì)量本身也不高谷誓,排查、修復(fù)起來花費(fèi)了很長時(shí)間吨凑,影響面非常大捍歪。所以,我們評估下來怀骤,覺著這個(gè)框架的可靠性不夠费封,維護(hù)成本、二次開發(fā)成本都太高蒋伦,最終決定替換掉它。
- 對于引入新的框架焚鹊,我們的要求是成熟痕届、簡單,并且與我們現(xiàn)有的技術(shù)棧(Spring)相吻合末患。這樣研叫,即便出了問題,我們也能利用之前積累的知識璧针、經(jīng)驗(yàn)來快速解決嚷炉。所以,我們決定直接使用 Spring 框架來提供 RESTful 格式的遠(yuǎn)程接口探橱。
- 把 RPC 接口替換成 RESTful 接口申屹,除了需要修改公共服務(wù)平臺的代碼之外,調(diào)用方的接口調(diào)用代碼也要做相應(yīng)的修改隧膏。除此之外哗讥,對于公共服務(wù)平臺的代碼,盡管我們只是改動接口暴露方式胞枕,對業(yè)務(wù)代碼基本上沒有改動杆煞,但是,我們也并不能保證就完全不出問題。所以决乎,為了保險(xiǎn)起見队询,我們希望灰度替換掉老的 RPC 服務(wù),而不是一刀切构诚,在某個(gè)時(shí)間點(diǎn)上蚌斩,讓所有的調(diào)用方一下子都變成調(diào)用新的 Resful 接口。
- 我們來看下具體如何來做唤反。
- 因?yàn)樘鎿Q的過程是灰度的凳寺,所以老的 RPC 服務(wù)不能下線,同時(shí)還要部署另外一套新的 RESTful 服務(wù)彤侍。我們先讓業(yè)務(wù)不是很重要肠缨、流量不大的某個(gè)調(diào)用方,替換成調(diào)用新的 RESTful 接口盏阶。經(jīng)過這個(gè)調(diào)用方一段時(shí)間的驗(yàn)證之后晒奕,如果新的 RESTful 接口沒有問題,我們再逐步讓其他調(diào)用方名斟,替換成調(diào)用新的 RESTful 接口脑慧。
- 但是,如果萬一中途出現(xiàn)問題砰盐,我們就需要將調(diào)用方的代碼回滾闷袒,再重新部署,這就會導(dǎo)致調(diào)用方一段時(shí)間內(nèi)服務(wù)不可用岩梳。而且囊骤,如果新的代碼還包含調(diào)用方自身新的業(yè)務(wù)代碼,簡單通過 Git 回滾代碼重新部署冀值,會導(dǎo)致新的業(yè)務(wù)代碼也被回滾也物。所以,為了避免這種情況的發(fā)生列疗,我們就得手動將調(diào)用新的 RESTful 接口的代碼刪除滑蚯,再改回為調(diào)用老的 RPC 接口。
- 除此之外抵栈,為了不影響調(diào)用方本身業(yè)務(wù)的開發(fā)進(jìn)度告材,調(diào)用方基于回滾之后的老代碼,來做新功能開發(fā)竭讳,那替換成新的 RESTful 接口的那部分代碼创葡,要想再重新 merge 回去就比較難了,有可能會出現(xiàn)代碼沖突绢慢,需要再重新開發(fā)灿渴。
- 怎么解決代碼回滾成本比較高的問題呢洛波?
- 在替換新的接口調(diào)用方式的時(shí)候,調(diào)用方并不直接將調(diào)用 RPC 接口的代碼邏輯刪除骚露,而是新增調(diào)用 RESTful 接口的代碼蹬挤,通過一個(gè)功能開關(guān),靈活切換走老的代碼邏輯還是新的代碼邏輯棘幸。代碼示例如下所示焰扳。如果 callRestfulApi 為 true,就會走新的代碼邏輯误续,調(diào)用 RESTful 接口吨悍,相反,就會走老的代碼邏輯蹋嵌,繼續(xù)調(diào)用 RPC 接口育瓜。
boolean callRestfulApi = true;
if (!callRestfulApi) {
// 老的調(diào)用RPC接口的代碼邏輯
} else {
// 新的調(diào)用Resful接口的代碼邏輯
}
- 不過,更改 callRestfulApi 的值需要修改代碼栽烂,而修改代碼就要重新部署躏仇,這樣的設(shè)計(jì)還是不夠靈活。優(yōu)化的方法腺办,相信你應(yīng)該已經(jīng)想到了焰手,把這個(gè)值放到配置文件或者配置中心就可以了。
- 為了更加保險(xiǎn)怀喉,不只是使用功能開關(guān)做新老接口調(diào)用方式的切換书妻,我們還希望調(diào)用方在替換某個(gè)接口的時(shí)候,先讓小部分接口請求躬拢,調(diào)用新的 RESTful 接口驻子,剩下的大部分接口請求,還是調(diào)用老的 RPC 接口估灿,驗(yàn)證沒有問題之后,再逐步加大調(diào)用新接口的請求比例缤剧,最終馅袁,將所有的接口請求,都替換成調(diào)用新的接口荒辕。這就是所謂的“灰度”汗销。
- 那這個(gè)灰度功能又該如何實(shí)現(xiàn)呢?
- 首先抵窒,我們要決定使用什么來做灰度弛针,也就是灰度的對象。我們可以針對請求攜帶的時(shí)間戳信息李皇、業(yè)務(wù) ID 等信息削茁,按照區(qū)間宙枷、比例或者具體的值來做灰度。舉個(gè)例子來解釋一下茧跋。
- 假設(shè)慰丛,我們要灰度的是根據(jù)用戶 ID 查詢用戶信息接口。接口請求會攜帶用戶 ID 信息瘾杭,所以诅病,我們就可以把用戶 ID 作為灰度的對象。為了實(shí)現(xiàn)逐漸放量粥烁,我們先配置用戶 ID 是 918贤笆、879、123(具體的值)的查詢請求調(diào)用新接口讨阻,驗(yàn)證沒有問題之后芥永,我們再擴(kuò)大范圍,讓用戶 ID 在 1020~1120(區(qū)間值)之間的查詢請求調(diào)用新接口变勇。
- 如果驗(yàn)證之后還是沒有問題恤左,我們再繼續(xù)擴(kuò)大范圍,讓 10% 比例(比例值)的查詢請求調(diào)用新接口(對應(yīng)用戶 ID 跟 10 取模求余小于 1 的請求)搀绣。以此類推飞袋,灰度范圍逐步擴(kuò)大到 20%、30%链患、50% 直到 100%巧鸭。當(dāng)灰度比例達(dá)到 100%,并且運(yùn)行一段時(shí)間沒有問題之后麻捻,調(diào)用方就可以把老的代碼邏輯刪除掉了纲仍。
- 實(shí)際上,類似的灰度需求場景還有很多贸毕。比如郑叠,在金融產(chǎn)品的清結(jié)算系統(tǒng)中,我們修改了清結(jié)算的算法明棍。為了安全起見乡革,我們可以灰度替換新的算法,把貸款 ID 作為灰度對象摊腋,先對某幾個(gè)貸款應(yīng)用新的算法沸版,如果沒有問題,再繼續(xù)按照區(qū)間或者比例兴蒸,擴(kuò)大灰度范圍视粮。
- 除此之外,為了保證代碼萬無一失橙凳,提前做好預(yù)案蕾殴,添加或者修改一些復(fù)雜功能笑撞、核心功能,即便不做灰度区宇,我們也建議通過功能開關(guān)娃殖,靈活控制這些功能的上下線。在不需要重新部署和重啟系統(tǒng)的情況议谷,做到快速回滾或新老代碼邏輯的切換炉爆。
需求分析
- 從實(shí)現(xiàn)的角度來講,調(diào)用方只需要把灰度規(guī)則和功能開關(guān)卧晓,按照某種事先約定好的格式芬首,存儲到配置文件或者配置中心,在系統(tǒng)啟動的時(shí)候逼裆,從中讀取配置到內(nèi)存中郁稍,之后,看灰度對象是否落在灰度范圍內(nèi)胜宇,以此來判定是否執(zhí)行新的代碼邏輯耀怜。但為了避免每個(gè)調(diào)用方都重復(fù)開發(fā),我們把功能開關(guān)和灰度相關(guān)的代碼桐愉,抽象封裝為一個(gè)灰度組件财破,提供給各個(gè)調(diào)用方來復(fù)用。
- 這里需要強(qiáng)調(diào)一點(diǎn)从诲,我們這里的灰度左痢,是代碼級別的灰度,目的是保證項(xiàng)目質(zhì)量系洛,規(guī)避重大代碼修改帶來的不確定性風(fēng)險(xiǎn)俊性。實(shí)際上,我們平時(shí)經(jīng)常講的灰度描扯,一般都是產(chǎn)品層面或者系統(tǒng)層面的灰度定页。
- 所謂產(chǎn)品層面,有點(diǎn)類似 A/B Testing绽诚,讓不同的用戶看到不同的功能拯勉,對比兩組用戶的使用體驗(yàn),收集數(shù)據(jù)憔购,改進(jìn)產(chǎn)品。所謂系統(tǒng)層面的灰度岔帽,往往不在代碼層面上實(shí)現(xiàn)玫鸟,一般是通過配置負(fù)載均衡或者 API-Gateway,來實(shí)現(xiàn)分配流量到不同版本的系統(tǒng)上犀勒。系統(tǒng)層面的灰度也是為了平滑上線功能屎飘,但比起我們講到的代碼層面的灰度妥曲,就沒有那么細(xì)粒度了,開發(fā)和運(yùn)維成本也相對要高些钦购。
- 來具體看下檐盟,灰度組件都有哪些功能性需求。
- 我們還是從使用的角度來分析押桃。組件使用者需要設(shè)置一個(gè) key 值葵萎,來唯一標(biāo)識要灰度的功能,然后根據(jù)自己業(yè)務(wù)數(shù)據(jù)的特點(diǎn)唱凯,選擇一個(gè)灰度對象(比如用戶 ID)羡忘,在配置文件或者配置中心中,配置這個(gè) key 對應(yīng)的灰度規(guī)則和功能開關(guān)磕昼。配置的格式類似下面這個(gè)樣子:
features:
- key: call_newapi_getUserById
enabled: true // enabled為true時(shí)卷雕,rule才生效
rule: {893,342,1020-1120,%30} // 按照用戶ID來做灰度
- key: call_newapi_registerUser
enabled: true
rule: {1391198723, %10} //按照手機(jī)號來做灰度
- key: newalgo_loan
enabled: true
rule: {0-1000} //按照貸款(loan)的金額來做灰度
- 灰度組件在業(yè)務(wù)系統(tǒng)啟動的時(shí)候,會將這個(gè)灰度配置票从,按照事先定義的語法漫雕,解析并加載到內(nèi)存對象中,業(yè)務(wù)系統(tǒng)直接使用組件提供的灰度判定接口峰鄙,給業(yè)務(wù)系統(tǒng)使用浸间,來判定某個(gè)灰度對象是否灰度執(zhí)行新的代碼邏輯。配置的加載解析先馆、灰度判定邏輯這部分代碼发框,都不需要業(yè)務(wù)系統(tǒng)來從零開發(fā)。
public interface DarkFeature {
boolean enabled();
boolean dark(String darkTarget); //darkTarget是灰度對象煤墙,比如前面提到的用戶ID梅惯、手機(jī)號碼、金額等
}
- 總結(jié)一下的話仿野,灰度組件跟限流框架很類似铣减,它也主要包含兩部分功能:灰度規(guī)則配置解析和提供編程接口(DarkFeature)判定是否灰度。
- 跟限流框架類似脚作,除了功能性需求葫哗,我們還要分析非功能性需求。不過球涛,因?yàn)榍懊嬉呀?jīng)有了限流框架的非功能性需求的講解劣针,對于灰度組件的非功能性需求,這里不在給出亿扁,你可以自己思考下捺典,可以對照下文做參考。
小結(jié)
- 灰度發(fā)布可以分為三個(gè)不同層面的灰度:產(chǎn)品層面的灰度从祝、系統(tǒng)層面的灰度和代碼層面的灰度襟己。我們今天重點(diǎn)講解代碼層面的灰度引谜,通過編程來控制是否執(zhí)行新的代碼邏輯,以及灰度執(zhí)行新的代碼邏輯擎浴。
- 代碼層面的灰度员咽,主要解決代碼質(zhì)量問題,通過逐漸放量灰度執(zhí)行贮预,來降低重大代碼改動帶來的風(fēng)險(xiǎn)贝室。在出現(xiàn)問題之后,在不需要修改代碼萌狂、重新部署档玻、重啟系統(tǒng)的情況下,實(shí)現(xiàn)快速地回滾茫藏。相對于系統(tǒng)層面的灰度误趴,它可以做得更加細(xì)粒度,更加靈活务傲、簡單凉当、好維護(hù),但也存在著代碼侵入的問題售葡,灰度代碼跟業(yè)務(wù)代碼耦合在一起看杭。