導讀
上海外灘建築群包括古典主義風格的亞細亞大樓(1915年),英國古典式的上海總會大樓(1911年),歐洲古典折中主義的海關大樓(1925年),仿意大利文藝複興風格的彙中飯店大樓(1906年)等,這些建築曆經百年風雨,仍巍然屹立固若堡壘,保持其原本風貌,讓今人得以深切領略一個世紀以前的壯麗繁華。這一切,除得益于人為保護之外,最主要原因是建築自身主體結構具備較高的穩定可靠性。
而相比傳統建築工程,軟體工程有兩個顯著特點,一是具備規模化快速複制擴散的能力,二是在竣工之後依然可以被改造并保持高速進化。這也就導緻了軟體工程一點點的不足也可能被快速擴散、無限放大造成大規模損失,進化中既有的穩定性結構可能會被不斷打破,可能導緻大量的救火投入,疲于奔命,極大地消耗有效而寶貴研發資源,阻礙軟體的進行甚至帶來災難性後果。是以對于任何一家上規模的軟體開發的企業來說,穩定性保障都是必須面對和解決好的課題。我個人加入阿裡之初是在國際支付寶核心團隊長期負責金融系統穩定性保障,其後紮根淘系技術三年有餘,參與了多種不同類型系統設計與穩定性建設,以及大促穩定性保障工作。對比總結下來無論電商、金融、物流、ERP型軟體工程,其穩定性保障是政策是有較多共性經驗的。本文主要結合金融、電商兩種場景下的個人以及所在團隊實踐,談談穩定性保障的一些思路方法。
穩定性保障圍繞的核心
穩定性保障涉及機房、網絡、硬體部署到業務場景、互動設計,再到應用架構代碼品質、流量與封網管控、攻防複盤等,是一項非常系統化的工程。而分工協作使得上述大部分工作流程化标準化,比如硬體問題、網絡問題、Jvm監控等均可以由專業化團隊提供統一配套方案,做為業務開發主要是利用這些工具更快的發現問題,幹預度相對有限。但具體業務系統實作距離拖拽式批量生産尚需時日,最終産出實作方案差異可能很大,是以所有穩定性工作的核心還是圍繞系統與代碼本身來開展,如下圖所示,無論是品質團隊、各方管控平台,還是研發流程保障機制,最終的穩定性還是展現在系統代碼上。是以,對于業務開發的來說,穩定性保障更應該着眼于系統設計實作環節的控制,如下橙色部分:

而正是需求不受控、架構局限、代碼擴充性不足、監控日志不統一、下遊依賴不統一之類問題導緻了每到大促前夕的緊張慌亂,重複的梳理、反複的打更新檔,需要額外的進行壓力測試,預案配置演練,消耗巨大且效果有限。是以我們需要貫穿于系統實作各環節,來制定合理的穩定性政策。系統實作核心環節與穩定性關系如下圖所示:
接下來着重針對上圖所示各環節,做一下總結。
業務大環境與業務架構穩定性
一個人的命運必須要考慮曆史程序影響,談系統穩定性則不可以忽略大的業務環境,業務環境是指所在的部門業務現狀,以及自己所在業務部門與其它業務部門的合作現狀,可能發生的業務調整重組。
有時候為了滿足業務上的發展目标,老闆們不得不頻繁的做業務調整。随之而來的是技術團隊與系統架構的擁抱變化,系統拆分、合并、重建一些列工作,必然會極大地打亂既有的穩定性保障政策,是以必須要從業務環境業務架構的層面來審視并調整以跟上變化。
比如天貓技術部和淘寶技術部合并、拆分,洋淘業務下線,逛逛品牌上線,躺平業務獨立,導緻原有支援的統一社群營運工作台不再work,各自垂直業務線因發展需要或者被迫不得不獨立出自己的營運背景管理,自己的索引,遷移相容老資料,影響不可謂不深遠。
再比如淘寶社交賬号體系原來是作為淘寶内部的一個體系,僅僅面向淘寶會員服務,但随着閑魚、躺平的獨立,需要延伸出新的特性化需求,那麼以現有的形态繼續支援跨BU的業務發展是否是最優的方案。如果繼續支援面臨人力、機器資源的保障的投入,那這些與目前BU的價值與方向是否吻合?這些是我們業務上要去考慮的問題。
平台化産品思維下,所有人都想建平台并急于讓更多的人接入進來,以發揮自己平台影響力,展現自身價值。而一旦業務目标調整,團隊方向跟着調整,平台這一塊兒不再是重點,角色反轉,之前苦苦拉過來的客戶被告知不再提供服務,甚至限期遷移,這對自己、對客戶方的系統穩定性都極其不利。是以,越往上層的團隊,建平台越審慎,先做好自己的主業而不是盲目擴充邊界。一個基于短期目的,或者缺乏長遠規劃的所謂平台直接提供給别人用,也是不負責任的表現。
而對于一個新的業務來說,我們也需要要求産品營運不僅僅是提出一個商業需求商業模式,而是要系統化思考一個業務的心智及演變趨勢。從天馬行空的想法到一個可行性方案,技術人員需要發揮自身優勢,和産品營運同學一起思考,将商業模式進行細化,想清楚,想透徹,往往業務上一點點的權衡或者調整對技術上的改變将會帶來四兩撥千斤的效果。
系統是業務的直覺回報,業務架構很大程度上決定了進一步的技術架構。如果産研團隊對業務了解不深刻不透徹,隻着眼于未來一年甚至幾個月短期需求與利益,想到哪裡做到哪裡,那麼技術層面就無法做好提前的布局與設計,堆砌、重複就會随之而來。大的環境背景不穩則限制了天花闆,後續實作過程上無論怎麼努力隻能治标難以治本!是以對目前身處的業務的環境整體大圖輪廓的認知,是建立全局穩定性思考的前提。
▐ 電商系統業務架構示意
如果想做一個最小型的電商網站,至少也需要包含這幾個業務子產品,會員、商戶庫存、下單子產品、店鋪管理中心,規模稍大之後每個子產品可能要支援獨立的營運,可能還要考慮支援商業化接入,還可能延伸出物流、售後、資金管理等延伸子產品,那麼這些子產品将怎麼去協作,這些東西我們需要在業務大圖中有一定預見。
▐ 支付系統業務架構示意
比如泰國使用者小A在登入速賣通(www.aliexpress.com)後,使用支付寶支付泰铢,購買了一雙鞋子。那麼這個支付過程至少就會涉及到上面幾方。在這個過程中,速賣通是做為一個外部商戶之一,支付寶要服務好千萬個這樣的商戶。就需要獨立的收單對接團隊,獨立出收單子產品。在使用者支付的過程中,還可能根據與銀行之間支付服務費差異,營銷活動等決策要給使用者推薦的支付方式,那麼就需要一個營運可幹預的支付路由子產品,而下遊同樣提供泰铢支付的泰國支付機構也有很多家,我們對于每一家的接入都是要有商務協定和技術準入,那麼把金融管道看做一個業務單元,既然有資金流動那麼獨立的财務核算子產品就是必不可少的,包括圍繞這個支付活動可以沉澱下來的會員體系,決定了在支付平台中要有使用者子產品。
▐ 導購場頁面架構示意
類似于手機淘寶中首頁、我的淘寶、收藏夾這類通用的強入口,這種兵家必争之地更需要技術、産品、營運一起考慮清楚業務架構。盡可能提供通用的UI标準與接入方案,前瞻性考慮頁面業務架構,能保證我們的頁面複雜度不随業務方呈現線性增強趨勢,業務接入可以批量化、可配置化。而如果局限于應付單次的特性化需求堆砌,幾經疊代,業務邏輯越來越臃腫,每次調整将牽一發而動全身,最終難以為繼。産品擁抱變化,加之人員調整頻繁,新一批的産品、技術沒有人能講清楚這個頁面的真實運作情況。
業務架構并不隻是部門老闆的運籌決策,也不是必須面對特别大的場景才需要考慮。即使我們隻是一個小頁面甚至小區塊的産品、開發,那麼對這個頁面這個元件也可以有很好的業務規劃,比如如下區塊雖小,但做為一個産品的流量入口來說卻具有決定性意義。
和産品設計達成一緻後可以抽象出穩定模型如下:
可分為:logo區、數字标、小紅點、頭像區,引導文案區、圖檔氛圍區、色彩氛圍區。
基于這個穩定的區塊互動架構, 我們定義統一标準協定,之後服務端就可以前瞻性規劃,考慮各類營銷場景、AB方案,大做文章以支援多樣化業務營運需求且不會因為頻繁的前後端關聯修改引入穩定性問題。
先要清晰認識了解所在部門大的業務環境、背景、未來趨勢。確定我們和業務方能建立業務架構上的共識,目标一緻。以此為基石,保證方向正确的前提下,貼合實際去考慮系統架構與邊界問題,做配套的穩定性方案,才具有更好的可行性!
業務需求把控與穩定性關系
從業務需求層面考慮穩定性,主要是做兩個方面,一是業務需求過濾(價值判斷),二是需求模型簡化。
關于需求價值判斷,尤其是對于創新型産品,産品設計同學思維很發散,天馬星空idea層出不窮,這是創新源動力,是好事,但作為技術人員精力很有限,必須腳踏實地的思考可行性問題,必須要對需求方原始需求進行合理質疑,砍掉一些表面上的浮華,實際沒有核心價值的僞需求,同時需求精簡模型,将有限的研發精力投入到真正有業務價值的地方。
做價值判斷,必要時候用資料分析、資料驅動手段去證明,很可能分析結論發現整個盤子的天花闆就在那裡,那麼一些需求就自然沒有存在的理由,無價值的需求上去了除了浪費開發資源,還會帶來系統複雜度的無意義升高,穩定性風險自然就升高。
有些時候業務方傾向于把一個需求方案複雜化,我們需要做模型簡化,考慮是不是有必要設計如此複雜的規則。如果我們把規則簡化到20%以下,是否可以滿足90%以上的需求了,而剩下10%是否可以有更輕量的方式去解決。
如下針對淘友圈所在的我的淘寶入口,和業務上達成一緻後,簡化後的邏輯實作複雜度下降50%,那麼穩定性風險也會下降50%,且對業務上帶來的是同樣的使用者體驗,體感如下:
如今紛繁複雜的無線頁面形态并不是越絢越好,而是需要真正找到對使用者有吸引力,有價值的點。正如逍遙子和蔣凡在2020年雙11所要求的,簡單、好逛。很多時候,技術必須要協助産品去做減法,而業務上的去僞存真化繁為簡對于系統複雜度往往帶來決定性影響,做穩定性保障必須重視這一環節的把控。
業務領域模型穩定性
基于對業務的了解把控,我們拿到一個相對靠譜的業務需求,開始抽象領域模型。領域模型是從純粹客戶需求轉化為技術人員可了解的語言,是對業務的高度抽象,根本目的是幫助我們了解和分析業務,以指導進一步的技術實作。
這個階段需要與産品經理反複對焦,充分了解題意深挖出潛在邏輯(實際必須要支援的邏輯,但是局限于需求方認知在需求階段沒有提到),使用uml工具梳理出面向對象程式設計中的對象,以及對象之間的轉化關系,需要抓住整體而不是一上來就陷入細節。
比如我們建一個面向頁面資源位的投放系統,分析後可以得到如下:
▐ 領域模型示意
▐ 系統領域子產品劃分示意
領域模型分析不等于資料庫ER圖設計,但有了清晰準确的領域模型,再細化出ER圖就變得很确定性了。
這一步是從微觀層面了解業務,基于業務架構抽象出了業務的核心主體及其主體之間的協作關系,確定清晰準确的了解了需求現在要做,未來要做,未來可能做的事情有哪些,基于這個充分的了解,設計穩定可靠可擴充的模型,確定業務領域模型的穩定。
技術架構與選型穩定性
技術架構與選型這一步需要确定程式設計語言,資料庫,系統拆分,以及你的系統之間大緻是如何産生關聯作用,并最終提供完整的業務能力。
技術選型主要包括:技術架構選型,資料存儲索引選型,資料互動流轉選型等大的子產品。對配置有專門中間件團隊的公司,出于效率與技術統一性考慮,代碼架構一般都有一套成熟穩定的配套方案,業務團隊無需在這個上面過渡投入。比如在阿裡,可以在一站式研發協同平台aone上一鍵建立最新應用。主要精力是花在資料存儲與資料互動流轉技術的選擇上,需要結合自身業務特點來做選擇。基于業務架構與領域模組化、資料規模、未來趨勢、團隊能力限制綜合決策權衡選擇合适的架構,沒有最好的架構,隻有最合适的架構。同時需要有對業務的預判能力,至少産品主線上可能出現的較大變化,要預留架構上的可能性。
比如系統是否有必要一開始就應該考慮到讀寫分離,拆分為幾個系統。比如基于業務特性拆分成了讀寫分離的A,B兩個系統,A主要做了門面抗流量,B主要異步任務,定時寫入,這樣避免因為瞬時異步流量過高影響生産讀服務。
但因為項目人員變動較大,時間緊任務重,導緻沒有堅守,有些服務似乎直接寫在A中更加節省工作量,妥協一下,這樣A與B的職責越來越模糊,久而久之A系統中也有了較多的瞬時流量風險,風險就不可控。這就要求必須從架構上确定系統職責邊界,該撕的一定要撕,如果表面總是一團和氣,必然是在某些方面做了一些放棄,把所有的毛刺都按在床底下,日積月累總有一天會爆發。
而如果拆成讀寫分離,勢必會導緻有些其實都強依賴的子產品,不太好界定的子產品也要遠端通信。那麼可以達成共識,統一提供一個非client類型的公共二方包,在B中開發,同時供給A、B使用。
而如果你做為上遊入口,是否考慮提供spi機制,避免後續接入N個下遊,就要引入N個二方包的情況。
比如考慮一緻性保障政策是用分布式事務,還是使用差錯補償機制來處理。
比如關于存儲,我們是需要用nosql存儲還是關系型資料庫,還是按照業務特性寫多份異構存儲來提供更好的性能。做這一步決策就需要綜合了解mysql,lindrom特性及應用場景,lindorm與redis做為索引的差異化,以及opensearch的可靠性、延時性問題。資料庫設計考慮不足,導緻容量瓶頸。資料庫的分庫分表,則應該考慮未來5至10年的業務規模。關于存儲與索引技術的選型,是整個技術選型中的核心,後續将另謀篇幅詳談。
技術架構及選型直接決定了我們的系統結構上是否穩定合理,決定了在未來可預期的時間段内是否會被推到重來,是系統穩定的基礎。
代碼實作規範穩定性
業務架構->領域模組化->技術架構與選型決定了我們整個工程的宏觀可行性與各個關鍵節點的解決方案。接下來進入到編碼環節,但這個環節來說,不同的施工隊、不同的人員操作上是不一緻的,可能一兩個點的細節看不出主要影響,但是兩邊引起質變,最終會決定成敗。我們需要有一套規範來保障細節的可控與标準化,來確定系統微觀層面的穩定性,比如:
- 比如包裝類型與基本類型使用場景,判斷對象相等方法,對象做json序列化注意事項。
- 金額字段處理,統一規範的封裝工具類。金額統一收口服務端處理。
- 響應給上遊的result到底是代表通信成功,還是業務成功。這個在注釋上必要詳盡說明,避免異常情況下扯皮。
- 資料庫,核心資源操作應始終對照:一鎖二判三更新的基本原則。
- 盡量避免使用|、&、異或(^)、位運算(<<),因為可讀性較差,代碼的可讀性可維護性是除了代碼本身業務價值外,技術人員對公司最大的貢獻。
關于代碼規範的制定,推薦《阿裡巴巴Java開發手冊》一書,它是阿裡内部Java工程師所遵循的開發規範,涵蓋程式設計規約、單元測試規約、異常日志規約、MySQL規約、工程規約、安全規約等,這是近萬名阿裡Java開發人員經驗總結,并經曆了多次大規模一線實戰檢驗及完善,具備較強的參考意義。
代碼通用子產品穩定性
結合自身系統特點,理清楚變與不變。把可以反複使用的部分、易出錯的部分以及系統核心引擎抽象出來,做為系統不變的部分。把伴随着業務的變化或者新業務方接入而不斷調整的部分提取出來,做為系統中可變的部分。
對于不變的部分,運用設計模式及常用套路将其固化下來形成公共子產品,降低類似功能重複編寫帶來的風險,提高增量業務疊代效率。這部分代碼投入核心精力讓它像工具類一樣穩定可靠,之後反複運作。這部分的抽象決定了系統的核心代碼層次,保障大樓的上相似的子產品統一穩定,有統一的管控手段。
對于變的部分,提供擴充點。這部分是會随着業務的變化而不斷疊代,同時要考慮讓變化的部分具備隔離性。即當改動一個子業務需求時,盡可能從從架構上限定住它的影響範圍。
追求高内聚,低耦合,滿足開閉原則易擴充易維護的代碼層次結構。
當然這裡要避免一個誤區,即濫用設計模式,或者為了模式而模式。比如在一個小小的項目裡本來簡單一個方法調用就能實作,确要過度套用設計模式去編碼,折騰出來好幾層,開發成本高且給代碼的維護帶來了困難。私下練手可以。但在工程層面,則要更多的考慮實用價值,實際一定要結合場景需要。
下面簡要列舉幾個常見的點:
▐ 統一對外服務層異常兜底
對于多數應用來說為上遊提供hsf是其主要職責。标準是當service層抛異常,我們需要自己處理掉異常,以Result結果中的結果碼的形式與外界通信,而不是直接抛運作時異常給業務方。那麼我們可以抽象出AOP層,對Service層的異常統一捕獲,傳回兜底錯誤碼給上遊。
▐ 統一抽象摘要日志子產品
方法的核心出入參,是我們監控的關鍵。但是穿插在業務代碼中列印,總是容易遺漏而且侵入性很強。那麼可以抽象摘要日志注解子產品,無侵入的列印方法的摘要日志。
▐ 統一下遊依賴子產品
在淘系,我們做導購型産品,基本繞不開淘系3C,即UIC(使用者中心)、TC(交易中心)、IC(商品中心),可能還會有一個SC(店鋪中心),而這些中心又因為曆史的原因提供了多套對接查詢方法。曾經看到一個系統中對IC的直接依賴有10+處之多,同樣是查詢商品對象,但由于不同的開發人員依賴了不同的方法。
那麼每當大促鍊路梳理的時候,或者IC包做更新的時候,就需要梳理回歸多個入口,給系統的鍊路梳理帶來了極大的不确定性。
正确的做法是我們對于同一個下遊入口,包裝出唯一的代理類,唯一的方法,統一維護、監控,反複使用。這裡的下遊不僅僅指業務系統,同時包括對于一些中間件的依賴,比如:針對hbase、redis、ldb、opensearch、odps的通路封裝。
▐ Java線程池使用
單個應用對異步線程的管理,應該有統一的類收口,使用者隻需要傳遞線程池名及所需要的變量即可。
這樣通過統一的監控配置一目了然就能看到整個應用對異步線程池的使用管理情況,快速診斷出是哪裡的線程池使用不合理導緻的系統線程數飙高報警,也便于後續的交接維護。這個類就可以完成對線程池的所有幻想,屏蔽掉對線程池工具類的直接通路,至少包括這些類的行為:
1)建立指定線程池
2)并發執行/單個執行
3)同步執行/異步執行
統一的線程池管理工具類示例:
/**
* 說明:通用線程池工具類
*/
public class CommonExecutorManager {
public static final String POOL_DEFAULT = "POOL_DEFAULT";
/**
* 線程池map
*/
private Map<String, ExecutorService> threadPoolMap = null;
/**
* 預設線程池
*/
private ExecutorService defaultExecutor = null;
@PostConstruct
public void init() {
threadPoolMap = new HashMap<>(2);
/**
* 預設線程池
*/
ThreadFactory defFactory = new com.google.common.util.concurrent.ThreadFactoryBuilder().setNameFormat(
"AsyncManager-default-%d").build();
this.defaultExecutor = new ThreadPoolExecutor(8, 16,
100L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), defFactory,
new ThreadPoolExecutor.DiscardPolicy());
threadPoolMap.put(POOL_DEFAULT, defaultExecutor);
}
@PreDestroy
public void close() {
if (defaultExecutor != null) {
defaultExecutor.shutdown();
}
for (ExecutorService pool : threadPoolMap.values()) {
pool.shutdown();
}
}
/**
* 異步執行supplier
*
* @param poolName 線程池
* @param supplier supplier
* @param <T> 結果類型
* @return
*/
public <T> CompletableFuture<T> supplyAsync(String poolName, Supplier<T> supplier) {
if (poolName == null) {
poolName = POOL_DEFAULT;
}
ExecutorService executorService = threadPoolMap.getOrDefault(poolName, defaultExecutor);
return CompletableFuture.supplyAsync(supplier, executorService);
}
/**
* 并發執行supplier, 并等待結束
*
* @param poolName 線程池
* @param supplierList supplier list
* @param <T> 結果類型
* @return 結果清單
*/
public <T> List<T> supplyListSync(String poolName, List<Supplier<T>> supplierList) {
if (poolName == null) {
poolName = POOL_DEFAULT;
}
ExecutorService executorService = threadPoolMap.getOrDefault(poolName, defaultExecutor);
List<CompletableFuture<T>> futures = supplierList.stream()
.map(supplier -> CompletableFuture
.supplyAsync(supplier, executorService))
.collect(Collectors.toList());
return futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
/**
* 查詢線程池
*
* @param poolName 線程池名稱
* @return
*/
public ExecutorService getExecutorService(String poolName) {
return threadPoolMap.getOrDefault(poolName, defaultExecutor);
}
/**
* 建立線程池
* @param corePoolSize 核心線程數
* @param maxSize 最大線程數
* @param threadName 線程名稱
* @param daemon
* @return
*/
public static ExecutorService createThreadPool(int corePoolSize, int maxSize, String threadName, boolean daemon) {
ExecutorService executorService = new ThreadPoolExecutor(corePoolSize, maxSize, 5, TimeUnit.MINUTES, new
LinkedBlockingQueue<>(), new com.google.common.util.concurrent.ThreadFactoryBuilder().setNameFormat(
threadName + "-%d").setDaemon(daemon)
.build());
return executorService;
}
/**
* @param nThreads
* @param threadName
* @return
*/
public static ScheduledThreadPoolExecutor createScheduledPool(int nThreads, String threadName) {
return new ScheduledThreadPoolExecutor(nThreads,
new com.google.common.util.concurrent.ThreadFactoryBuilder().setNameFormat(threadName + "-%d").build());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
}
▐ 定時任務使用
同管理線程池一樣,單個應用對定時任務的使用,是可以有通用的部分抽象出來,比如對于網格任務中的根任務與子任務的識别。具體的定時任務執行個體隻需要聚焦在子任務的特性讀取與單條任務的計算處理上。
這樣通過統一的監控配置一目了然就能看到整個應用定時任務執行的情況,快速診斷出是哪個定時任務的運作導緻系統資源報警。統一的job抽象類示例:
public abstract class AbstractMapJob extends MapJobProcessor {
@Override
public ProcessResult process(JobContext context) throws Exception {
try {
String taskName = context.getTaskName();
Object task = context.getTask();
if (isRootTask(context)) {
//生成子任務
List<IndexChildTask> childTasks = generateChildTasks(MsgSwitch.childTaskCount, context);
return map(childTasks, getChildTaskName());
} else if (getChildTaskName().equals(taskName)) {
try {
//子任務處理
handleChildTask(task);
return new ProcessResult(true);
} catch (Throwable t) {
log.error("handleChildTask error.", t);
return new ProcessResult(false);
}
}
} catch (Exception e) {
log.error("process error", e);
}
return new ProcessResult(true);
}
/**
* 根據總記錄數拆分生成子任務
* @param taskCount 要拆分的子任務個數
* @return
* @throws Exception
*/
private List<IndexChildTask> generateChildTasks(int taskCount, JobContext context) throws Exception {
long totalCount = getTotalCount(context);
log.info("total count: {}", totalCount);
if (totalCount == 0) {
return Collections.emptyList();
}
long taskSize = (long)Math.ceil(totalCount * 1.0 / taskCount);
List<IndexChildTask> taskList = new ArrayList<>();
for (long i = 0; i < taskCount; i++) {
long start = i * taskSize;
IndexChildTask childTask;
if (i == taskCount - 1) {
childTask = new IndexChildTask(start, totalCount - (i * taskSize));
} else {
childTask = new IndexChildTask(start, taskSize);
}
taskList.add(childTask);
}
return taskList;
}
/**
* 擷取要處理的總記錄數
* @return
* @throws Exception
*/
public abstract long getTotalCount(JobContext context) throws Exception;
/**
* 處理子任務
* @param task
*/
public abstract void handleChildTask(Object task);
/**
* 擷取子任務名稱
* @return
*/
public abstract String getChildTaskName();
}
▐ 消息訂閱處理
将消息接收的注冊、序列化為業務類型,異常處理,監控日志做統一的封裝,收口,使得消息的消費監控變得簡單易于執行,同時便于後續的開發維護,示例類:
public abstract class MetaqCommonConsumer<T> implements MessageListenerConcurrently {
private MetaPushConsumer consumer;
private String topic;
private String tag;
private String consumerGroup;
/**
* 具體的消息實體類型
*/
protected Class messageObjClass;
public MetaqCommonConsumer() {
// 擷取父類帶泛型的類型
Type genericSuperclassType = getClass().getGenericSuperclass();
ParameterizedType parameterizedType = (ParameterizedType)genericSuperclassType;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
messageObjClass = (Class)actualTypeArguments[0];
}
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
if (list == null || list.isEmpty()) {
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
for (MessageExt msg : list) {
return consumeSingleMsg(msg, context);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
/**
* 消費消息
*
* @param msg
* @return
*/
private ConsumeConcurrentlyStatus consumeSingleMsg(MessageExt msg, ConsumeConcurrentlyContext context) {
if (msg == null) {
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
try {
String bodyJsonStr = new String(msg.getBody(), "UTF-8");
// 轉換為實際類型
T messageObj = (T)JSON.parseObject(bodyJsonStr, messageObjClass);
return process(messageObj, msg, context);
} catch (Exception e) {
log.error("consumeMessage error, {}", JSON.toJSONString(msg), e);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
/**
* 消息處理方法
*
* @param messageObj 轉換為具體類型的消息obj
* @param messageExt 完整的消息體
* @return
*/
protected abstract ConsumeConcurrentlyStatus process(T messageObj, MessageExt messageExt,
ConsumeConcurrentlyContext context);
/**
* 初始化
*/
public void start() {
try {
consumer = new MetaPushConsumer(consumerGroup);
consumer.subscribe(topic, tag);
consumer.registerMessageListener(this);
consumer.start();
log.info("start success consumerGroup: {}, topic: {}, tag: {}", consumerGroup, topic, tag);
} catch (Exception e) {
log.error("start consumer error consumerGroup: {}, topic: {}, tag: {} , errorMsg: {}", consumerGroup, topic,
tag, e.getMessage());
}
}
public void setTopic(String topic) {
this.topic = topic;
}
public void setTag(String tag) {
this.tag = tag;
}
public void setConsumerGroup(String consumerGroup) {
this.consumerGroup = consumerGroup;
}
▐ 封包網關互動
比如封包網關系統無論和外部怎樣的通信差別變化的部分在于封包格式不同需要有不同的行處理邏輯,不變的是通過http進行request、response的通信邏輯,以及對json、xml格式封包的解析方式。
▐ 檔案解析子產品抽取
而檔案網關系統則一定是現将檔案轉換成流讀取到記憶體,再轉換成一行,針對這一行做特性化處理,最後關閉流操作,那麼我們可以把檔案處理的的邏輯抽成模闆方法,後續來一種新的檔案,隻需要實作模闆方法,專注處理某一行的業務邏輯就可以了。
通過通用子產品的抽取,我們可以極大的規避同一類的風險,也為後續的統一發現能力的建設提供了基礎。
發現能力基礎建設
這裡主要強調為了做發現能力建設,系統需要滿足怎樣的設計更合理,圍繞代碼實作談談建設思路。
在前述架構合理、分層得當、代碼規範到位、兜底保護完善的情況下確定我們項目可以順利驗收上線運作。
但在運作過程中都會受到各種因素的影響,難免還是會出問題,出問題并不可怕,關鍵是能否盡可能早的在第一時間發現問題并解決問題,而發現問題往往比解決問題更具有挑戰性。試想如果不該列印日志的地方列印了日志,不該配置報警的地方配置報警,錯誤碼粒度雜亂無法區分, 那麼真正的問題就不能被及時發現。
對于一個直面無線app的服務端應用,系統層面很容易得到如下通用視角:
抽象來看系統層面發現能力建設主要圍繞以下4個要素:
-
淺談系統實作層面穩定性保障導讀穩定性保障圍繞的核心業務大環境與業務架構穩定性業務需求把控與穩定性關系業務領域模型穩定性技術架構與選型穩定性代碼實作規範穩定性代碼通用子產品穩定性發現能力基礎建設極限兜底保護穩定性資金操作穩定性持續重構與穩定性的關系可壓測性能力建設服務端CI能力建設上下遊SLA穩定性總結
絕大部分的穩定工作都是圍繞上述四個要素在投入,梳異常鍊路、映射錯誤碼、加日志、加監控。針對這些問題,我們需要有統一的、盡可能低成本低侵入的解決方案和标準,在日常的研發過程中就盡量随時準備好。
▐ 統一錯誤碼标準
需要建立錯誤碼标準,分外部錯誤碼、内部錯誤碼,建立錯誤碼統一維護政策。
需要上遊業務感覺,或者友善上下遊之間定位問題,可以定義外部錯誤碼,這部分錯誤碼的變化需要非常謹慎,特别是上遊已有引用已有了解的情況下。而内部錯誤碼是指系統的各個層次之間内部執行異常得到的錯誤碼,通常不對外輸出,主要用于業務場景監控與異常定位。
錯誤碼的設計要結合自身業務特點,制定适合自身業務的錯誤碼。比如銀行的結果碼細分品類繁多,不得不用數字碼來替代,但導購應用需要前端感覺的結果碼很有限,這個時候可讀性占據主導地位,就不建議輸出使用數字編碼,這樣的格式機器可讀,但人不可讀。
▐ 統一異常處理機制
面向異常程式設計,定義統一業務異常類,通過業務錯誤碼進行異常分級分類。當抛出原生運作時必須列印完整堆棧。當抛出系統業務異常,對于業務預期之中的則權衡是否需要堆棧資訊。
異常分類的方式有兩種,一種是通過繼承設計不同的異常類來做異常區分,另一種是通過在異常中增加異常結果碼。實踐上,推薦使用異常結果碼這種比較靈活和友善的方式。
▐ 統一日志輸出規範
日志是我們發現、定位問題的基礎,幾乎絕大部分的穩定性工作都是在圍繞着日志進行。
基本原則
1)鷹眼id、壓測标、ip、時間、線程等業務無關的必要資訊,由日志架構層統一解決。
2)日志檔案功能分級,在預設application.log之外至少分出方法xflush摘要日志檔案、異常日志檔案。
3)統一監控日志格式,針對xflush日志檔案,定義行列結構
4)謹慎列印日志,不多打,也不少打,注意日志打爆磁盤的問題。
設計示例
格式化摘要日志:
日期|eagleyeId|ip|壓測辨別位|日志級别|預留1|預留2|線程号|(層級^場景)(類名^方法名^業務成功表示位^結果碼列^異常根原因^耗時)(監控屬性1^監控屬性2^監控屬性3)(業務屬性1^業務屬性2^業務屬性N)
其中監控屬性區:存放比如使用者是否種子使用者,傳回動态條數等,可能用于監控分析的字段,主要用于監控。
業務屬性區:存放比如userId,動态id,紅包id之類的字段,主要用于異常定位。
靈活而業務無侵入的使用方式:
/****
* 構造開圈頁面入口的動态消息
* @param friendPageDTO
* @return
*/
@Digest(logger = "COMMON-XFLUSH",layer ="manager", success ="!ret?.isEmpty()",monitor = {"ret?.size()"} ,attrs= {
"userId"}
)
public List<OpenTipsVO> getTaoMomentsOpenPageTips(Long userId, Optional<FriendPageDTO>friendPageDTO) {
//do something
}
輸出效果:
2020-12-22 16:40:44.618|0b13045816086264438145928e77fb|11.23.98.0||WARN |HSFBizProcessor-DEFAULT-8-thread-47|(manager^-)(TaoMomentsEnterTipsManager^getTaoMomentsOpenPageTips^Y^-^-^86ms)(2^-^-^-^-)(2200782267981)
▐ 統一貼合業務的監控機制
監控是手段和目标,需要在錯誤碼、異常、日志三個要素的基礎之上基于業務特點細化到每一段核心代碼邏輯,能夠及時發現預期之外的運作邏輯,報警精準程度則直接決定了線上運維成本。根據适用場景可以把分為通知型與大盤型,通知型是指直接通過某種通訊工具報警觸達至接收者,而大盤型是指使用者定期主動浏覽,通過實時或者T+1(h+1)報表能夠進行業務統攬以發現關鍵問題。
通用監控告警層
這裡抛開硬體及基礎設施監控先不談,僅看系統代碼層面的情況。
通用監控是指具有業務無關性,隻要是無線服務端應用都會需要的監控。
比如mtop層方法執行超過500ms、錯誤碼環比大漲、異常量大漲、出現未知異常。這類異常應該由統一解決方案,無需按照業務子產品重複建設。
特定業務場景告警層
結合系統所承載的業務本身,對關鍵鍊路做特定監控,比如淘友圈特定業務場景監控:
建立業務系統統一大盤
針對一個系統,我們會有aop監控層、各個子業務場景監控點。随着功能的疊加監控點會越來越多。這些監控點會在具體場景出問題時報警給我們。但假設就在某一個時刻,想知道系統各業務是否都運作正常,就需要有全局視角的業務大盤。在這個大盤上可以覆寫系統的主要業務場景,承擔系統晴雨表的效果,關鍵時候可以一目了然、心中有數。
恢複能力基礎建設
這裡主要強調為了做發現能力建設,系統需要滿足怎樣的設計更合理。
當通過發現能力發現問題後,進一步面臨的是如何恢複的問題,一般有如下幾種手段:
▐ 業務降級
當發現服務接口逾時,定位到是某一個下遊依賴所緻。且判斷該依賴并非産品核心流程,此時需要一鍵關閉掉對該服務的調用,進而保障主流程能夠正常工作。要求我們在編碼的時候能夠梳理強弱依賴,對弱依賴增加必要的降級開關,并形成預案。
▐ 重試補償與人工介入
重試補償分自動重試補償和人工補償兩種情形。
自動重試如消息重試,線程内循環重試,一般是因為機器程序中斷,網絡延時等原因導緻的差錯場景,這類場景經過系統自動重試機制是可以完成的。
而人工補償主要面對人為引起的髒資料,或者依賴外部機構帶來的不可控因素。
比如一個客戶的支付單據,因為國外銀行端的接口bug,導緻傳回了不确定的流水單号或者金額資料或者指定導緻校驗不通過,因而支付狀态未推進。這種情況可能必須要通過客服手段去聯系銀行人員,因為确認後我們需要手工觸發單據到支付完成狀态。如果偶爾幾筆可以由開發人員通過資料訂正手段完成,如果這樣的情況時有發生,那麼就需要考慮是否開發差錯恢複功能,讓業務營運可以在限定條件下直接介入推進單據狀态,完成狀态補償。
▐ 自動切換
這裡想說的其實類似于分布式服務發現Zookeeper的自動探測能力,zk通過探測容器是否存活決定是否将容器從可用清單中移除或增加。而對于業務接口我們有時也需要這種探測能力,通過探測接口的可用性,決定是否啟用。尤其在有多個下遊服務競争,且穩定性得不到充分保障的情況下訴求較為強烈,比如使用者錢包有N個銀行卡可用,當某個銀行接口出現故障的時候,我們應該具備自動識别的能力。比如超過一定的錯誤閥值及失敗率,自動将這個銀行接口切換為不可用将支付方式置灰并給使用者以文案提示,每隔一定的時間再做自動的探測嘗試,發現成功後再自動切換為可用狀态。
極限兜底保護穩定性
汽車的安全氣囊,雖然阻止不了車禍,但很可能會挽救一次生命。兜底保護相當于一些安全手段,比如緊急情況事故無可避免,但是我們可以把損失降低到最小,比如無論在任何情況下,目前端或者服務端出現任何異常的情況下,不允許使用者看到諸如500,404,空白頁之類的頁面。這需要全面的異常分支分析+與産品人員的充分對焦+充分的兜底場景測試。
1)單機qps限流2)norya自适應系統load限流3)消息接收處理線程數控制,就是無論什麼時候,要保障你的系統是活着的,研發人員必須建立這樣的認識。
使用meta的線程數來進行削峰平谷,保護應用。
資金操作穩定性
資金操作穩定性保障即資損防控
1)如何定義資損?
廣義的資損應該包括資金流轉活動中,資金未按照業務規則預期流動的情況,導緻業務參與方中的任何一方或多方遭受了資金損失。比如紅包活動中,如果因為技術故障給使用者少發了5毛或者多發了5毛,無論誰吃了虧,沾了光,隻要是不符合預期的,均定義為資損,都應該界定為資損。
2)資損也可以歸屬于穩定性範疇,為什麼要單獨拎出來?
個人了解資損是穩定性問題中危害最大波及面最廣的一種,尤其是對金融系統來說。
我曾經在支付寶金融核心組參與研發工作4年,看過公司内外不少案例。深刻體會到資金風險可能給業務及公司帶來的災難性後果,金融軟體要求我們在所有的地方都做到最好,容不得半點損失,多一分錢少一分錢都要複盤追責。彼時資損案例的發生直接與績效直接挂鈎,每一行代碼都需要被自動化腳本覆寫到,花5分鐘寫的代碼,可能需要花2小時來編寫、運作測試腳本,要求極其嚴苛,但在金融系統中,這一切都是值得的。
3)電商團隊資損防控看法
近兩年以來,淘系對于資損重視程度逐漸收緊,不少防控機制從螞蟻引入。因為雖然電商不屬于金融系統,但做為一個經濟體,任何細小的波動都可能給品牌形象帶來巨大破壞和影響。但無論怎樣,其要求的嚴苛程度與金融軟體是不一樣的,不能所有的問題都一刀切,不斷疊加的機制、流程會帶來新的問題。尤其針對電商創新型業務,是需要結合具體業務場景具體分析,在穩定性保障與業務創新疊代效率之間找到一個平衡點,定義最合适粒度的資損防控政策。
持續重構與穩定性的關系
在業務先赢的時代背景下,特定時候不得不做妥協,不得不上臨時方案,這樣導緻即使原本優雅穩定的系統實作,依然會被一點點扭曲,形成越來越多的技術債。這些技術債會成為越來越大的絆腳石,除了會障礙業務快速發展,更會直接帶來額外穩定性風險和巨大的人力開銷。我們必須要做持續的重構來償還,下面舉幾個示例:
▐ 無線頁面接口爆炸問題
比如下圖所示的某産品首頁,一旦進去首頁會同時打開8個接口,而這種暴漲增長的接口,勢必給後續的開發疊代及穩定性工作帶來較大挑戰,同時也影響了低端機使用者體驗。你可能說這一個業務很複雜,是以不得不使用8個。但我們抓取手淘首頁接口,發現那麼複雜的頁面結構,隻要一個接口搞定了,因為統一的協定标準定義的科學、通用。
而抽象出這樣的接口,是需要對産品形态及未來趨勢、前後端研發協作機制有充分考慮。
▐ 系統對商品服務的依賴問題
比如對IC、主搜尋依賴,全站點下對商品的狀态、價格統一處理的問題,這些問題在一兩個功能下都不需考慮太多問題。但功能點越來越多,問題就會逐漸出現,就需要進行統一收口,把複雜易出錯的問題集中在一個地方,集中精力解決一次。
▐ feeds流加載邏輯問題:
做為一個朋友圈形态的電商内容場的頁面,其feeds塊類型包括商品、紅包、遊戲、玩法各種類型,需要從商品、營銷、關系、互動擷取各種類型資料來透出,那麼怎樣去組織代碼結構,才能在後續盡量小的侵入去接入新品類,需要斟酌推敲。
▐ 我的淘寶動态入口讀寫模式問題:
當你進入我的淘寶,過去的做法是直接pull的形式,拉取你的好友的動态,為了保護我淘頁面的性能,逾時設定為100ms。但随着動态類型越來越多,逾時的情況時有發生,這個時候就需要基于穩定性與可持續擴充層,提出新的方案并改進既有代碼。
諸如上述所列,有些重構可能和短期的業務目标無直接的聯系,尤其是産品營運同學不能直接感覺的情況,很容易被忽略,得不到重視。但如果不做,坑就會在那裡越積越多,引發線上的故障額機率就會越來越大。
可壓測性能力建設
這裡主要強調為了支援可壓測,系統需要滿足怎樣的設計更合理。
導購型業務流量大是其最主要特點,當我們開發新功能的時候,很容易因為節奏的問題而急于上線,完全不考慮這個代碼在雙11的情況下是否OK。導緻到了壓測的時候才發現這段邏輯沒有識别壓測流量,可能會導緻将壓測資料寫入到正式庫中。理想的狀況是在項目一開始,就考慮對壓測的支援方案,預留擴充點,項目時間緊可以了解,但是一定要能提前布局。否則會因為突擊壓測能力改造導緻代碼侵入極強,時間上比較被動,最後臨近封網,有些流程其實沒有經過壓測,也隻好硬着頭皮上線,具有較大不确定性。
服務端CI能力建設
這裡主要強調服務端CI能力,依然是我們系統工程中需要代碼實作的部分。
對于服務端開發來說,自動化回歸主要關注單元測試和接口內建測試。單元測試關注單個類單個方法的邏輯正确性。接口內建測試一般關注單個系統單個service方法的邏輯正确性。具體這個層面做到什麼程度需要依賴業務場景而定,比如高風險、模型穩定的核心系統就必須需要嚴格要求自動化覆寫率,而創新型快速疊代的新業務講究快跑覆寫核心流程比較合适。
比如支付寶多數系統都與資金相關穩定性要求極高,要求每次代碼變更的自動化行覆寫率甚至分支覆寫率不能下降,且手工測試不計入内,似乎為了規避風險無論怎樣嚴苛的卡控都是一種政治正确。曾有人做過局部統計,發現寫1行業務代碼需要5行測試代碼來覆寫,改一行代碼需要重跑整個系統的數百個用例可能耗時一天, 以保障其業務系統高度穩定可用,但弊端是降低了生産效率。
而電商内容業務創新型團隊,業務變化極快。産品經理idea天馬星空,更看重的是業務快速上線快速試錯的能力,我們需要兩害相權取其輕做決策。針對這樣情況建議梳理出業務核心主流程,針對主流程建立自動化回歸用例持續運作。
長遠來看,服務端CI自動化能力是保障系統核心子產品穩定性不可擷取的手段,但需要視場景、合理規劃。忌不切合和業務實際的一刀切要求。
上下遊SLA穩定性
這裡主要強調系統之間的SLA,自身做的再好,合作方上下遊沒有規範好是不夠的,依然隐患重重。這裡上下遊是指使用你系統服務的前端、上遊服務端,以及被你調用的下遊服務。
SLA隻是一種約定,目的是確定接口提供方和使用方能夠對接口有一緻的了解,能夠更好的保障業務活動進行,同時在發生線上故障必須要進行定責的時候,是一份參照标準,一份免責聲明。個人認為可以不局限于形式,但一定要可追溯,文檔或者清晰的接口注釋都具備同樣效力。與上下遊之間做好SLA協定簽訂,在金融級系統幾乎是必須要做的事情。
但在淘系這邊業務開發的過程中 ,發現有時候兩個團隊的開發一碰頭,或者口頭約定或者釘釘上溝通,就開始調用接口了。也不管result.success到底代表是業務執行成功,業務受理成功,還是不确定。或者當時溝通清楚了,但是也沒有關鍵性文檔沉澱下來,就發上線了,潛在風險很大。
比如A依賴B以check是否可以給B使用者發紅包,那就必須對B的入參、出參,結果碼有100%的了解,且有明确文檔落下來。
/**
*使用者資質檢查服務
*/
public interface CheckService {
Result check(QueryParam queryParam, UserInfo userInfo);
}
對比一下,如下Result,就很清晰明了,讓人一看就很有确定性:
/**
* 開放API2.0_活動投放開放API
*/
public interface DeliveryOpenService {
/**
* 投放活動-支援單資源位單執行個體
* 1)result.isSuccess=ture代表業務上資料寫入成功,此時data為工匠平台的活動id,建議業務方做存儲,便于後續問題的定位排查
* 2)result.isSuccess=false情況下,errorCode會有相信錯誤資訊,業務方需做監控并采取必要的處理
* @param singleTypeActivityRequest 活動對象
* @return <activityId,#activityId#>
*/
OpenResult<Map<String,String>> createActivity(SingleTypeActivityRequest singleTypeActivityRequest);
總結
上醫治未病,中醫治欲病,下醫治已病。本文大緻圍繞系統設計實作各環節,分析了各環節對穩定性保障的影響,同時介紹了對應的一些套路和經驗。着眼點主要在于未病、欲病階段的思考投入,盡可能把一些問題的解決和規避前置均攤到業務需求受理把控、系統分析設計與編碼實作階段,同時持續改進健全,最終達到提高全局穩定性保障工作效能的目的。