理論到實戰(zhàn),高可用架構(gòu)踩坑說明書
在構(gòu)建高可用系統(tǒng)時,開發(fā)者常常面臨應(yīng)用、數(shù)據(jù)庫、緩存、消息隊列等多維度的挑戰(zhàn)。本文結(jié)合京東真實技術(shù)場景,系統(tǒng)梳理高可用架構(gòu)實踐中常見的技術(shù)陷阱與解決方案,深入剖析每個技術(shù)組件的可用性保障要點。旨在為工程師提供一套踩坑說明書,幫助團隊在系統(tǒng)設(shè)計階段規(guī)避潛在風險,提升線上系統(tǒng)的穩(wěn)定性和容錯能力。
一、前言
通常情況下,我們在說一個事情之前,一定要把事情本身及其定義說得明明白白。那么,在對高可用架構(gòu)具體展開之前,我們先要好好說說,什么是高,怎么樣才算高,多高才算高,其次才是怎么才能做到高。
以系統(tǒng)建設(shè)場景為例,通常意義的高可用的標準至少要達到4個9的水準,即以一天為例,每天至少要保證少于8.64秒的故障產(chǎn)生。如果以更嚴格的5個9的水準來看,每天至少要保證少于近1秒的故障產(chǎn)生。達到了這樣的標準,才算高可用。
試問,假定我們的高可用標準為以上標準,捫心自問下,您能做到么?
筆者有自信,這個標準,很多團隊都做不到。那么,我們有沒有可以遵循的標準,指引我們往這個方向去前進和努力呢。我理解這個需要大家群策群力。筆者能力有限,水平一般。以下內(nèi)容,將基于筆者實際經(jīng)歷的一些常見的問題點及應(yīng)對方案進行展開,期望可以對大家的實際工作有一定的幫助作用。值得注意的是,避開這些問題點,是否可以達成高可用的標準不好說,但是很容易就走到高可用的反面。
在實際展開內(nèi)容之前,要達成高可用的標準,除常規(guī)的發(fā)布變更可能引發(fā)的風險和故障導(dǎo)致高可用標準時效外,我們還需要考慮線上在正常運行態(tài)下,也會存在“一切都不可靠,都會壞,且馬上就會壞”的情況,同時需考慮線上業(yè)務(wù)急速增長可平穩(wěn)支撐的解決方案。包括但不限于考慮容器、db、rpc依賴、redis緩存、mq消息等內(nèi)外部一切可能涉及的因素,考慮出現(xiàn)單點故障或大面積故障后業(yè)務(wù)會引發(fā)的變化,要求增加無死角的各類監(jiān)控(分鐘級和秒級),提前準備改造方案和預(yù)案。且要求對應(yīng)用程序的每一行代碼及依賴中間件的底層原理做到了如指掌,出現(xiàn)問題時才有可能做到快速定位分析、快速恢復(fù)、快速響應(yīng)。
以下內(nèi)容從常見的幾個涉及高可用的主題進行展開,每個主題都會下探一些常見的容易出現(xiàn)問題的場景及應(yīng)對方案。
二、應(yīng)用高可用
2.1 代碼故障
筆者把代碼故障分為兩類,一類是應(yīng)用類的,主要是應(yīng)用系統(tǒng)的開發(fā)人員引入的,此類問題較易發(fā)現(xiàn),定位難度雖有一定的差異,但是往往修復(fù)的方案相對可控,基本都可在應(yīng)用系統(tǒng)對應(yīng)的研發(fā)團隊內(nèi)閉環(huán)解決;另外一類是平臺類,主要包括依賴的包括但不限于JDK平臺,及各類依賴的開源代碼組件及內(nèi)部平臺組件,包括但不限于RPC框架、緩存框架等。
2.1.1 應(yīng)用類故障
主要涉及100%命中或大概率命中的業(yè)務(wù)場景,包括但不限于int溢出、字符長度溢出、除法為0、空指針異常等,此類場景在業(yè)務(wù)命中后,會極易導(dǎo)致此類場景的服務(wù)出現(xiàn)完全不可用的情況。
具體問題可分別描述如下,問題的排名及影響程度不分先后:
2.1.1.1 int溢出
以下圖為例,程序中執(zhí)行了Integer.parseInt的方法,一般情況下功能也沒有什么問題,但是如果出現(xiàn)需要轉(zhuǎn)換的值,超出了int的最大范圍,或者需要轉(zhuǎn)換的值,從數(shù)字類型變更調(diào)整為了字符串類型。兩種場景均會導(dǎo)致轉(zhuǎn)換失敗,如果此項轉(zhuǎn)換出現(xiàn)在主流程或者業(yè)務(wù)流量較大的場景,出現(xiàn)的問題影響及損失就不可計量了。
朋友們,如果你的代碼有這種情況,建議抓緊看看評估下,是否存在業(yè)務(wù)上的潛在風險?
圖片
2.1.1.2 字符長度溢出
比較常見的問題有兩類,一類會造成應(yīng)用程序中的字符長度和數(shù)據(jù)庫的字符長度不匹配,出現(xiàn)后會出現(xiàn)數(shù)據(jù)庫無法保存的故障;一類是業(yè)務(wù)代碼中有對某些字符串有取固定某幾位的邏輯或者判定長度執(zhí)行特定邏輯,這種在上游沒有評估到位出現(xiàn)字符串的長度增加、減少,或者位置出現(xiàn)偏移的時候,也極易出現(xiàn)問題。
比如如下邏輯,識別倉是否為云倉業(yè)務(wù),執(zhí)行的邏輯為判定倉編號是否為9位數(shù)字,且首字母是否為8開頭。此類判定邏輯隨著時間變化,就比較容易產(chǎn)生問題。我們還是好好祈求下,期望業(yè)務(wù)范圍永遠在這個范圍里面,或者后續(xù)要調(diào)整的時候,有人可以識別到相關(guān)所有的依賴方。
圖片
再比如如下邏輯,為京東里面依賴比較多的一個邏輯,即識別商品編號,來判定商品是否為普通圖書、臺版書、音像、電子書等業(yè)務(wù)。識別的邏輯為依賴商品的編號的號段來做邏輯,后來隨著商品的量級急劇膨脹,原來的號段邏輯不夠用了,相關(guān)的團隊不得不引入了一個外部的遠程的配置中心,并提供了獨立的sdk加載遠端的配置來實現(xiàn)復(fù)雜的號段邏輯。此類邏輯,也是高可用實踐中應(yīng)極力避免的場景。
圖片
2.1.1.3 除法故障
在涉及到需要數(shù)學(xué)計算的場景里,這個問題比較常見。通常出現(xiàn)問題一般常見的故障為:1)未設(shè)置小數(shù)點及舍棄位規(guī)則 ,導(dǎo)致除法不能整除;2)業(yè)務(wù)上出現(xiàn)了除數(shù)為0的場景。
上述兩個場景在筆者的經(jīng)歷中多出現(xiàn)過,且出現(xiàn)問題后此類場景的業(yè)務(wù)也會出現(xiàn)完全不可用的情況。
Exception in thread "main" java.lang.ArithmeticException: Rounding necessary
at java.math.BigDecimal.commonNeedIncrement(BigDecimal.java:4179)
at java.math.BigDecimal.needIncrement(BigDecimal.java:4386)
at java.math.BigDecimal.divideAndRound(BigDecimal.java:4361)
at java.math.BigDecimal.setScale(BigDecimal.java:2473)
at java.math.BigDecimal.setScale(BigDecimal.java:2515)2.1.1.4 代碼邏輯故障
這個分類里,會有各種五花八門的故障,而且這些故障只有你想不到,沒有你寫不出來的故障。曾經(jīng)有名產(chǎn)品走到一名研發(fā)跟前說,嘿,這么認真,又在寫bug呢。要破此局,需要大家共勉。
站在消費者的角度而言,一般而言,都期望下完單付完款后,可以盡快收到寶貝。那么技術(shù)上,就要求整體系統(tǒng)的鏈路維持高可用的狀態(tài),即支付后整體履約鏈路在日常情況下,可維持秒級平穩(wěn)且無毛刺出現(xiàn)的情況,基于更高的要求目標,在下游服務(wù)可能出現(xiàn)抖動的情況下,仍要保障整體下傳鏈路的通暢性,保障調(diào)用量級穩(wěn)定。
一般而言,此類復(fù)雜的履約鏈路,往往會依賴內(nèi)外部大量的RPC服務(wù),此類服務(wù)對應(yīng)的性能指標往往不一致,為應(yīng)對這種差異,往往會需要一個流程框架設(shè)置不同的業(yè)務(wù)流程節(jié)點來串聯(lián)相關(guān)的服務(wù),且此流程框架中需對有不同性能的業(yè)務(wù)節(jié)點提供差異化的服務(wù),也即對不同性能節(jié)點的業(yè)務(wù)節(jié)點配置差異化的線程池隊列。那么對此類業(yè)務(wù)場景,應(yīng)對高可用場景,至少應(yīng)有如下場景需重點考慮:
?目標
在待處理任務(wù)充足的情況下,每個業(yè)務(wù)線程都有足夠的待執(zhí)行任務(wù)需要處理,不出現(xiàn)線程饑餓的場景;
在待處理數(shù)據(jù)充足的情況下,主業(yè)務(wù)線程提交的待處理的任務(wù)數(shù)量過大,防止出現(xiàn)雪崩情況產(chǎn)生。
?優(yōu)化方案
業(yè)務(wù)主線程先獲取任務(wù)線程池緩沖區(qū)實時待處理任務(wù)數(shù)量,并動態(tài)決定是否需要提交待處理任務(wù),保障提交的待處理任務(wù)不會超過緩沖區(qū)大小且不會出現(xiàn)業(yè)務(wù)線程饑餓的情況,同時下傳流程框架移除CountDownLatch的邏輯限制,移除按批次處理的邏輯,使業(yè)務(wù)主線程提交一批待處理任務(wù)后,不會做任何等待,可以再次獲取系統(tǒng)執(zhí)行權(quán)。
?優(yōu)化效果
優(yōu)化前,下游服務(wù)抖動,引發(fā)調(diào)用量出現(xiàn)毛刺;優(yōu)化后,即使下游服務(wù)抖動,調(diào)用量依然是相對穩(wěn)定。
2.1.2 平臺類故障
此類故障一般都隱藏得較深,不易發(fā)現(xiàn)。而且及時發(fā)現(xiàn)了,一般平臺的響應(yīng)速度均會較快,可在新的版本中進行修復(fù),應(yīng)用開發(fā)人員將對應(yīng)的版本進行升級即可完成問題修復(fù)。常見出現(xiàn)的問題是應(yīng)用開發(fā)人員使用了一些存在問題的低版本,此處的難點在于如何對涉及的依賴平臺組件完成平穩(wěn)無縫升級,這個問題及應(yīng)對的方案留給大家。在實操中,我們也可以發(fā)現(xiàn),即使是大名鼎鼎的JDK,這個所有人賴以生存的基礎(chǔ)底座,都有一些極其低級的bug。
需要說明的是,下方的較多代碼bug或故障,較多情況并不會對實際業(yè)務(wù)造成影響,但是以高可用的標準來要求的話,還是有必要多多關(guān)注,早日完成問題修復(fù),不留潛在風險。
以下以筆者曾經(jīng)遇到的平臺類故障進行展開描述,問題排名及影響不分先后。
2.1.2.1 JDK故障--數(shù)組越界
此數(shù)組越界問題是在大促備戰(zhàn)中發(fā)現(xiàn)。需要注意的是,此問題實際不影響業(yè)務(wù)功能,因為JDK對此類異常進行了捕獲并處理,但是應(yīng)用系統(tǒng)中因為此問題會導(dǎo)致潛在的高頻拋出和捕獲異常,而眾所周知,高頻拋出異常對應(yīng)用服務(wù)器的壓力及GC耗時均有較大的影響。
需要說明的,下方復(fù)現(xiàn)問題的代碼,您在控制臺默認啟動試運行的時候,是不會出現(xiàn)下方的堆棧信息的。那么什么時候會出現(xiàn)這個異常呢?簡單,我們在JDK的源碼類SignatureParser對應(yīng)的異常代碼里面增加對應(yīng)的斷點即可。通過查閱資料,我們發(fā)現(xiàn)JDK在8u311的版本修復(fù)了這個bug,相關(guān)的bug鏈接為:https://bugs.openjdk.org/browse/JDK-8035424,實際代碼fix的鏈接為:https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/2e292618f87a。
那么這個問題什么時候會出現(xiàn),筆者可以很明確的說:很多場景都會出現(xiàn),最簡單的場景即為復(fù)現(xiàn)代碼中使用json解析的場景即會出現(xiàn)。
那么這個問題,應(yīng)該如何修復(fù)呢?從上方的信息可以看到,JDK在8u311版本中已完成問題修復(fù),那么我們直接升級對應(yīng)的版本即可。看起來很簡單,對么?
但是實際有這么簡單么?那當然不會,否則我也不會寫到這里了。筆者從部署平臺詢遍了所有可使用的鏡像版本,里面涉及到JDK8的版本,沒有一個版本高于8u311版本,這也就意味著,此問題,在所有使用JDK8版本的應(yīng)用里面,都有此類潛在隱患。想想這個問題,是不是覺得很震撼,很奇妙?
問題堆棧為:
"main@1" prio=5 tid=0x1 nid=NA runnable
java.lang.Thread.State: RUNNABLE
at java.lang.ArrayIndexOutOfBoundsException.<init>(ArrayIndexOutOfBoundsException.java:65)
at sun.reflect.generics.parser.SignatureParser.current(SignatureParser.java:95)
at sun.reflect.generics.parser.SignatureParser.parseSuperInterfaces(SignatureParser.java:559)
at sun.reflect.generics.parser.SignatureParser.parseClassSignature(SignatureParser.java:214)
at sun.reflect.generics.parser.SignatureParser.parseClassSig(SignatureParser.java:156)
at sun.reflect.generics.repository.ClassRepository.parse(ClassRepository.java:57)
at sun.reflect.generics.repository.ClassRepository.parse(ClassRepository.java:41)
at sun.reflect.generics.repository.AbstractRepository.<init>(AbstractRepository.java:74)
at sun.reflect.generics.repository.GenericDeclRepository.<init>(GenericDeclRepository.java:49)
at sun.reflect.generics.repository.ClassRepository.<init>(ClassRepository.java:53)
at sun.reflect.generics.repository.ClassRepository.make(ClassRepository.java:70)
at sun.reflect.generics.repository.ClassRepository.<clinit>(ClassRepository.java:43)
at java.lang.Class.getGenericInfo(Class.java:2548)
at java.lang.Class.getGenericSuperclass(Class.java:765)
at org.codehaus.jackson.type.TypeReference.<init>(TypeReference.java:33)復(fù)現(xiàn)問題的代碼為:
public class ATest {
public static void main(String[] args) {
String str = "{\"aa\":\"bb\"}";
try {
Object o = JacksonMapper.getInstance()
.readValue(str, new TypeReference<Map<String, String>>() {
});
} catch (IOException e) {
e.printStackTrace();
}
}
}JDK的修復(fù)方式如下所示,修復(fù)的邏輯可以看到也比較簡單,將原來粗暴的異常修改為前置邏輯判定即可:
圖片
2.1.2.1 RPC框架故障--找不到方法異常
JSF目前為京東平臺內(nèi)部使用的RPC框架,此技術(shù)框架目前提供2種交互協(xié)議:msgpack和hessian。容易出現(xiàn)問題導(dǎo)致存在高可用隱患的場景為:服務(wù)提供方和服務(wù)依賴方,交互的時候使用的是msgpack的協(xié)議,且交互的輸入輸出有使用BigDecimal的話,通過火焰圖等性能分析工具分析應(yīng)用程序運行時狀態(tài),我們會發(fā)現(xiàn)火焰圖中有大量的無法找到方法的異常。
那么這個方案有沒有解決方案呢?有的,按平臺建議,我們將RPC框架的協(xié)議,從默認的msgpack,切換為hessian即可。那么除了這個方案,JSF在msgpack協(xié)議下的這個bug還有沒有其他的解決方案呢。很遺憾的知悉大家,沒有了。那么切換為hessian有沒有什么潛在風險和隱患呢?應(yīng)該還是有一些的,詳細可以參見JSF的文檔說明,另要說明一點,從msgpack協(xié)議,切換為hessian協(xié)議,默認是會有少許的性能損耗的。
火焰圖異常信息為:
圖片
可穩(wěn)定復(fù)現(xiàn)的demo代碼如下所示,此處還需要再強調(diào)一點:在控制臺默認啟動試運行的時候,是不會出現(xiàn)下方的堆棧信息的。要能發(fā)現(xiàn)并定位出這個異常,需要我們在JDK的源碼類對應(yīng)的異常代碼里面增加對應(yīng)的斷點即可。
import com.jd.jsf.gd.codec.msgpack.MsgpackDecoder;
import com.jd.jsf.gd.codec.msgpack.MsgpackEncoder;
import java.math.BigDecimal;
public class ATest {
public static void main(String[] args) {
MsgpackEncoder msgpackEncoder = new MsgpackEncoder();
PayDetailVo payDetailVo = new PayDetailVo();
payDetailVo.setCurrencyPrice(BigDecimal.TEN);
byte[] encode = msgpackEncoder.encode(payDetailVo);
MsgpackDecoder msgpackDecoder = new MsgpackDecoder();
PayDetailVo decode = (PayDetailVo) msgpackDecoder.decode(encode, PayDetailVo.class);
System.out.println(decode.getCurrencyPrice());
}
private static class PayDetailVo {
private BigDecimal currencyPrice;
public BigDecimal getCurrencyPrice() {
return currencyPrice;
}
public void setCurrencyPrice(BigDecimal currencyPrice) {
this.currencyPrice = currencyPrice;
}
}
}此處再多說一點,對這個問題產(chǎn)生的原因再詳細展開下。從上方火焰圖的信息可以看到,實際出現(xiàn)的異常是獲取BigDecimal的構(gòu)造函數(shù),并且是BigDecimal的默認的構(gòu)造函數(shù)時,出現(xiàn)的無法找不到類的異常。但是您猜猜BigDecimal有沒有默認的構(gòu)造函數(shù)呢?答案是真沒有。
詳細參見下方截圖:
圖片
2.1.2.1 緩存框架故障--緩沖區(qū)溢出異常
jimdb為京東內(nèi)部可和redis對應(yīng)的緩存技術(shù)框架。這個問題出現(xiàn)的場景為,所有低于2.1.12(23年8月份發(fā)布)版本的jimdb,特別是之前強烈大家推薦升級的2.2.8-shade版本,在操作任意讀、寫redis命令時,均可穩(wěn)定出現(xiàn)這個異常。
需要說明的是,此類異常,也和上面介紹的異常類似,常規(guī)方式無法看出來,只有對性能及服務(wù)高可用有極致追求,通過壓力測試及火焰圖等工具才能比較快的定位出來。
那么,您能看看,您自己負責的系統(tǒng),使用的jimdb版本是什么版本么。
詳細的異常火焰圖如下:
圖片
具體產(chǎn)生的異常代碼如下所示:
圖片
從上方的信息,比較容易看到是SDK在定義緩沖區(qū)的大小時長度不足預(yù)期,往緩沖區(qū)放的時候出現(xiàn)了溢出異常,但是SDK的代碼捕獲了這類異常,同時將緩沖區(qū)的大小擴大一倍類解決問題。還是那句話,您要問業(yè)務(wù)功能上有沒有問題,那當然是沒有。但是您要問這種寫法真的好么?那筆者覺得這真是一個讓人可以深思的好問題。
SDK對這個問題的解決方案也很粗暴,上方JDK的處理方式有異曲同工之秒,修復(fù)后的代碼詳細可參加下方的截圖:
圖片
2.1.2.1 緩存框架故障--空指針異常
這個問題出現(xiàn)的場景為發(fā)現(xiàn)上方的緩沖區(qū)異常后,我們應(yīng)用開發(fā)人員抓緊升級到2025年7月18號升級的最新版版本2.3.1-HOTFIX-T2,繼續(xù)開啟壓測和火焰圖后,發(fā)現(xiàn)應(yīng)用中存在空指針異常的情況出現(xiàn)。還是說明一下,這個不影響業(yè)務(wù)功能,只是對性能有一定的損耗,具體損耗會依據(jù)應(yīng)用系統(tǒng)的不同略有差異。
這個問題最后定位下來,是應(yīng)用里面的一個jar(titan-profiler-sdk)較為老舊,新版本的緩存SDK依賴的新版的jar功能,最終在運行態(tài)會出現(xiàn)本案例的空指針異常。
這個問題的火焰圖信息如下:
圖片
2.2 單容器故障
這一點想要描述的是線上單點或少批量的機器故障。在一般的認知里面,線上服務(wù)均為集群部署模式,單點或者少量故障,理論上不會對服務(wù)的可用性造成太大的影響。但是,真的是這樣么?
以筆者的經(jīng)驗來看,單容易故障不造成太大的問題,一般出現(xiàn)在業(yè)務(wù)場景不太復(fù)雜的場景。以下以筆者的實操經(jīng)歷,從4個方面來進行詳細展開,描述因單容器故障造成實際影響的案例:
1)線上服務(wù)為web類服務(wù):此類場景,往往在域名控制臺中心綁定了域名和容器IP,假定某一臺或者某幾臺容器出現(xiàn)故障無法訪問,在沒有人工介入的情況下,考慮下此域名的業(yè)務(wù)使用方,是否會出現(xiàn)間歇性不能訪問的情況?是否有些工具可以做到自動切換,但是切換周期有多?以及是否所有的業(yè)務(wù)場景均配置了自動切換功能呢?這個問題,留給大家去思考
2)線上服務(wù)為RPC提供方服務(wù):您服務(wù)對應(yīng)的容器異常宕機了,您收到的一般為容器故障或網(wǎng)絡(luò)連通性異常等告警。但是RPC的接口依賴方,其對應(yīng)的服務(wù)可用率在問題出現(xiàn)期間,是否會出現(xiàn)可用率下跌的問題,及多長時間會自動恢復(fù)?這個問題,也留給大家去思考
3)線上服務(wù)為MQ消費服務(wù):MQ的工作原理,在下文MQ高可用章節(jié)會詳細展開,此處簡單說一下:單機故障后,在MQ客戶度沒有較好的優(yōu)雅關(guān)機的情況下,故障的那一瞬間,假定應(yīng)用程序已從MQ的服務(wù)端拿到了較多的消息在本地進行消費,沒有來得及給MQ服務(wù)端進行應(yīng)答的時候,同時假定MQ服務(wù)端的超時時間設(shè)置比較長的情況下,會出現(xiàn)MQ服務(wù)端的消息短時間內(nèi)出現(xiàn)部分鎖死無法消費,進而出現(xiàn)消息短時間內(nèi)積壓的情況出現(xiàn)。您是否遇到過類似的問題?
4)線上服務(wù)為流程框架調(diào)度類服務(wù):此類服務(wù)一般會有一個流程管理框架,在流程服務(wù)本身依賴的容器故障,或者流程服務(wù)依賴的服務(wù)出現(xiàn)故障時,系統(tǒng)不同的設(shè)計,會有不同的故障反應(yīng)。
以流程服務(wù)本身依賴的容器故障為例,嚴謹?shù)牧鞒炭蚣苄璞U狭鞒烫幚淼臄?shù)據(jù)能夠被及時調(diào)度起來,否則極易出現(xiàn)任務(wù)執(zhí)行延遲導(dǎo)致業(yè)務(wù)同步延遲的情況,那么怎么定義及時調(diào)度起來,如何及時調(diào)度起來呢?這個問題,同樣留給大家去思考。
再以流程服務(wù)依賴的服務(wù)的容器出現(xiàn)故障為例,嚴謹?shù)牧鞒炭蚣芤M量降低依賴服務(wù)的問題造成框架本身的調(diào)度問題,包括但不限于引發(fā)調(diào)度框架的業(yè)務(wù)量級出現(xiàn)突增或者突降的情況,在保障業(yè)務(wù)平穩(wěn)運行的情況下,還要對依賴的服務(wù)做好一定的保護措施,包括但不限于增加一定的措施,防止依賴的服務(wù)短時間內(nèi)調(diào)用量級過大引發(fā)依賴服務(wù)出現(xiàn)雪崩的情況出現(xiàn)。
2.3 機房故障
這類問題不太常見,但是一旦出現(xiàn),就是致命的影響,出現(xiàn)后必定會出現(xiàn)各類人仰馬翻的情況。可以設(shè)想,一旦某一個機房整體出現(xiàn)故障,那么我們可以簡單分析下哪些業(yè)務(wù)會產(chǎn)生影響。
1)流量入口:可以觀察到流量入口對應(yīng)的機房出現(xiàn)了故障,對應(yīng)的流量會出現(xiàn)短時間無法訪問的異常,這時候比較快的方式是摘除對應(yīng)的流量入口,需要考驗的就是定位問題的速度和摘除時的手速了。
2)各類RPC服務(wù):各類垂直調(diào)用的RPC服務(wù),在流量入口摘除整體機房后,理論上相關(guān)的機房的流量就會自動消除,看起來好像沒有什么業(yè)務(wù)影響。但是這個只是理論上的,目前線上還存在一些應(yīng)用沒有做手動或自動的垂直分組隔離,同時也會有一些任務(wù)會出現(xiàn)流量逃逸的情況出現(xiàn)。其他條線的業(yè)務(wù),如果流量入口沒有控制好,那么至少故障機房的影響就需要好好評估下了。
3)各類MQ服務(wù):這個就要依賴MQ的部署機房架構(gòu)了。如果MQ對應(yīng)的服務(wù)端部署在多個機房,理論上發(fā)送端的可用率就很難做到100%。MQ平臺是否可以做到發(fā)送時自動摘流,以筆者有限的經(jīng)驗來看,現(xiàn)階段應(yīng)該還不太行。
4)各類DB服務(wù):這個就更是一團亂麻了。不展開分析了。
5)緩存服務(wù):不展開分析了。
2.4 GC故障
市面上常常有一種說法,說面試造火箭,入廠擰螺絲。指的一般都是各類八股文的考題,其中GC在里面居多。從筆者的觀察來看,在較多的非核心系統(tǒng)里面,GC往往不為大家所關(guān)注。即使在較核心的系統(tǒng)里,GC的問題也不是能引起所有角色的關(guān)注。哪怕是在筆者寫文章的此時此刻,線上某個團隊的某一個0級核心服務(wù)對應(yīng)的TP999數(shù)據(jù)也會出現(xiàn)隨著時間的延長,會出現(xiàn)性能數(shù)據(jù)逐步緩慢攀升;并隨著全量發(fā)布后,性能出現(xiàn)急劇回落至正常水平的情況。即使是我們團隊,GC的耗時較高待優(yōu)化的系統(tǒng),在清單里面的也有較多。
下文以我們實際遇到的2個具體的優(yōu)化案例進行展開,期望對大家有一定的幫助作用。
?gc調(diào)優(yōu)--連接池調(diào)優(yōu)
?表現(xiàn):在某壓測過程中,某一類接單一直卡在5000qps上不去,上游應(yīng)用調(diào)用接單服務(wù)tp99超350ms已上,但是接單服務(wù)應(yīng)用的吞吐率上不去。
?原因:1)通過JVM監(jiān)控發(fā)現(xiàn)youngGC頻繁且峰值耗時在400ms左右,并未觸發(fā)fullGC。經(jīng)分析當前容器是4C8G,jvm配置gc并行處理線程數(shù)是(ParallelGCThreads=4),存在垃圾回收線程不夠的情況。2)數(shù)據(jù)庫連接池參數(shù)配置不合理,導(dǎo)致頻繁創(chuàng)建和回收連接池,整體gc的耗時在400毫秒上下浮動。
?舉措:1)升級到容器規(guī)格由4C8G升級到8C16G,同時調(diào)整JVM堆內(nèi)存、新生代內(nèi)存大小,調(diào)整GC并行處理線程數(shù)到8(ParallelGCThreads=8)。youngGC時長問題解決,youngGC耗時在30ms以下;2)優(yōu)化連接池參數(shù)
結(jié)果:在沒有其他優(yōu)化的前提下,優(yōu)化前平均耗時176.7ms,優(yōu)化后平均耗時17.2ms。
?GC調(diào)優(yōu)--字符串緩存關(guān)閉
?表現(xiàn):在壓測過程中,持續(xù)的高流量試跑導(dǎo)致部分實例在過程出現(xiàn)FGC情況,引發(fā)服務(wù)對應(yīng)的TP99段時間內(nèi)飆高,進而導(dǎo)致整體調(diào)用量級出現(xiàn)毛刺和掉坑。同時觀察發(fā)現(xiàn)的堆內(nèi)存時間隨著時間的推移穩(wěn)定增長,直到觸發(fā)一次fullgc。
?原因:拆分中使用了Jackson1進行json序列化,其中會用 String.intern() 來做字符串的緩存,這樣重復(fù)的字符串就可以只存一份了,因為返回的 Json 里的 Key 是 一個Java生成的UUID,這個key幾乎不會重復(fù),所以導(dǎo)致緩存池里的字符串越積越多,直到GC的時候才會回收。
?舉措:通過代碼關(guān)閉jackson中的字符串緩存,問題解決。此問題還有沒有其他好的解決方案,也留給大家去思考。
三、db高可用
3.1 JED查詢單分片故障
需要說明的是,JED為京東內(nèi)部可和mysql對應(yīng)的一類數(shù)據(jù)庫。和傳統(tǒng)mysql不同,JED自帶路由網(wǎng)關(guān),期望將底層的mysql實現(xiàn)對研發(fā)透明,業(yè)務(wù)邏輯和JED交互的時,若SQL中帶有分片hash鍵,則網(wǎng)關(guān)會計算并路由hash到對應(yīng)的mysql實例上;若SQL中不帶有分片hash鍵,則網(wǎng)關(guān)會將請求發(fā)送給所有的mysql實例,并在網(wǎng)關(guān)層聚合返回結(jié)果。
此處主要要強調(diào)的是,我們要理解JED的工作原理,按一般邏輯,我們和JED交互的時候,最佳實踐是SQL中帶著分片鍵,不然會引發(fā)跨片查詢。跨片查詢存在2類弊端:1)因為是對所有的分片進行并發(fā)查詢,最后完成數(shù)據(jù)的歸集,那么性能會存在一定程度的損耗(損耗程度取決于SQL的復(fù)雜度);2)任意一個mysql分片宕機,并發(fā)查詢的時候必定會命中這個壞的分片,最終會出現(xiàn)查詢結(jié)果完全不可用場景。
3.2 JED事務(wù)故障
業(yè)務(wù)邏輯開啟事務(wù)時,默認會使用select @@session.tx_read_only語句,此語句一是會影響性能;二是此語句在JED場景下,因為此sql并沒有帶分片鍵,會隨機選擇一個分片進行查詢掃描,若很不巧,掃描的分片整好是壞的那臺機器,可能會加大失敗的概率。也即假定我們線上有10個JED的分片,若現(xiàn)在某一個分片出現(xiàn)了故障,那么按推論,故障的比例應(yīng)該是百分之十,但是因為此項事務(wù)機制導(dǎo)致隨機掃分片,會將故障的比例升高擴大至20%。
解決方案:通過jdbcurl上配置useLocalSessionState = true
3.3 JED全局唯一鍵故障
jed如果使用全局自增id,在沒有特殊訴求的情況下,會默認使用第一個分片,即當?shù)谝粋€分片宕機時,按預(yù)期是只有一個分片對應(yīng)的業(yè)務(wù)出現(xiàn)故障,但是這種場景下,最后的結(jié)果與預(yù)期不符,所有的insert均會全部失敗。
3.4 慢sql故障
這個比較好理解,不詳細展開
3.5 大事務(wù)故障
在各個系統(tǒng)設(shè)計中,因為歷史架構(gòu)設(shè)計原因,存在較多大事務(wù),在業(yè)務(wù)量級較小的情況下,此種用法尚沒有太多問題,但是一旦上強度業(yè)務(wù)量級和并發(fā)上來,此種設(shè)計機制對數(shù)據(jù)庫會形成較大的壓力,導(dǎo)致數(shù)據(jù)庫對應(yīng)的qps無法提升,從而影響整體服務(wù)的吞吐。
以履約的其中一個系統(tǒng)舉例,歷史上利用接單防重表的事務(wù)來保障2個RPC寫操作的一致性,該大事務(wù)會導(dǎo)致DB鎖等待,吞吐量上不去。雖然可以通過擴JED分片的機制來減少單分片的鎖等待,提升吞吐量,但還是存在架構(gòu)不合理,性能瓶頸的問題。
解決方案:對防重表增加狀態(tài)機制,通過一次insert和一次update操作保障2個RPC的寫,避免使用insert大事務(wù)。
3.6 流量放大故障
所謂流量放大,以訂單條線為例,假定處理一個訂單,正常業(yè)務(wù)需對應(yīng)10條讀寫sql語句,但是通過監(jiān)控發(fā)現(xiàn)存在一個訂單對應(yīng)10倍到100倍的sql情況產(chǎn)生。
這個問題,比較致命,也比較隱藏,在db沒有壓力瓶頸的情況下,不易發(fā)現(xiàn);在db有壓力瓶頸的情況下,要么不好發(fā)現(xiàn),要么發(fā)現(xiàn)后來不及調(diào)整。建議大家多關(guān)注監(jiān)控,對此類流量放大的情況,提前完成治理工作。
3.7 db字段長度不足故障
這種問題,一般出現(xiàn)在此字段上下游沒有對齊的情況。在業(yè)務(wù)發(fā)展初期,或者在團隊規(guī)模較小的時候,大家的字段長度都保持類似,但是隨著業(yè)務(wù)不斷發(fā)展,原來的字段長度已不足以支撐業(yè)務(wù)發(fā)展,上游將字段提前完成了擴容,但是并沒有好的工具可以梳理到下游的影響,導(dǎo)致下游遺漏了修改動作。在某一個夜黑風高的夜晚,終于出現(xiàn)了寫入db失敗的異常,最終引發(fā)業(yè)務(wù)故障。
3.8 單集群存儲不足故障
此處想強調(diào)的是存儲資源不足的情況。試問下各位,您能說清楚您負責的系統(tǒng),考慮目前的業(yè)務(wù)增長趨勢保持不變、增長趨勢翻10倍、增長趨勢翻100倍的場景下,您負責的db存儲,還可以支撐多久?如果支撐時間很短,必然說只有一個月的話,應(yīng)該有什么解決方案呢?這個問題,留給大家。
較多系統(tǒng)存在單庫或者JED容量已經(jīng)無法滿足業(yè)務(wù)增長的情況,在小流量的情況下,數(shù)據(jù)庫不是瓶頸,一旦流量激增,一個跨分片的SQL或者一個JED單片的故障,都將引發(fā)一場災(zāi)難。但是從傳統(tǒng)的mysql升級到j(luò)ed,包括升級到目前的DongDal組件,還是有一些注意事項需要大家多多關(guān)注:
?check語法是否支持:傳統(tǒng)mysql升級至JED時,check原有SQL中使用的語法在JED是否支持
?關(guān)注網(wǎng)關(guān)性能:升級JED時需關(guān)注負載均衡、網(wǎng)關(guān)的配置及性能波動。例如,JED的負載均衡、網(wǎng)關(guān)、分片都在匯天機房,如果匯天網(wǎng)段出現(xiàn)網(wǎng)絡(luò)不問題,TCP重傳數(shù)高的問題。可能會導(dǎo)致JED查詢、修改等語句執(zhí)行時間TP99升高。
?低峰期執(zhí)行DDL:執(zhí)行對數(shù)據(jù)庫執(zhí)行表結(jié)構(gòu)修改時,需要觀察每個分片的QPS(包括總QPS、read QPS、write QPS),在QPS的低峰時,且各分片QPS均勻無異常波動,才可以執(zhí)行表結(jié)構(gòu)修改。黃金流程鏈路建議凌晨2點后,同時需要如果有大數(shù)據(jù)抽數(shù)任務(wù),也需要提前做協(xié)調(diào)和溝通,避免業(yè)務(wù)系統(tǒng)變更引發(fā)大數(shù)據(jù)側(cè)的事故。
?避免跨片查詢:SQL中最好都帶上增加分片鍵,不然會引發(fā)跨片查詢,跨片查詢的性能會存在一定程度的損耗(損耗程度取決于SQL的復(fù)雜度)
?增加數(shù)據(jù)庫連接池的探活配置:客戶端到JED網(wǎng)關(guān)之間還有LB作為網(wǎng)絡(luò)代理,LB會主動清理空閑10分鐘及以上的連接。需要業(yè)務(wù)側(cè)連接池進行保活或探測連接,否則LB會把連接殺掉,再次使用時會出現(xiàn)異常。
?事務(wù)30秒超時限制:為了提升JED網(wǎng)關(guān)連接池的使用效率,保護底層MySQL實例,針對事務(wù)有30秒的超時限制。
?JED單實例安全水位線:磁盤使用過大:進行數(shù)據(jù)歸檔或結(jié)轉(zhuǎn);QPS過大:增加緩存
類型 | 安全 | 中危 | 高危 |
磁盤使用(非歸檔類) | <=2T | >2T&<4T | >4T |
QPS | <=1萬 | >1萬&<=3萬 | >3萬 |
四、Redis高可用
4.1 JIMDB超時和熱key治理
JIMDB超時時間設(shè)置若不合理,在JIMDB故障時較容易出現(xiàn)無法快速熔斷,阻塞業(yè)務(wù)的情況,同時若應(yīng)用中存在單點熱key的存在,如果該熱key正好在故障的JIMDB分片上,就比較容易造成故障產(chǎn)生。
?超時時間治理:根據(jù)歷史上的業(yè)務(wù)監(jiān)控數(shù)據(jù),我們應(yīng)將JIMDB的超時時間設(shè)置在合理的閾值,實現(xiàn)業(yè)務(wù)快速熔斷。調(diào)整配置的讀、寫命令超時時間以及新建連接超時時間。同時注意 JAVA-SDK版本在 2.1.15及以下版本SDK不支持讀寫超時分開控制;如果應(yīng)用使用了這些SDK,所有的讀寫請求超時會統(tǒng)一使用寫命令超時參數(shù)。另外,新建鏈接超時過大,可能導(dǎo)致無法快速釋放鏈接,進而放大單片故障對業(yè)務(wù)系統(tǒng)的影響
?熱key治理:需要評估該熱key的實現(xiàn)規(guī)則是否合理,尤其避免key為固定常量的寫法
4.2 JIMDB高危命令治理
在使用jimdb時,為了使多個原子操作保持一致性,通常會使用lua腳本將多個原子操作打包處理。jimdb提供的上傳lua腳本方法scriptLoad,會掃描所有節(jié)點并上傳,當有一個節(jié)點不可用時,上述上傳動作會阻塞其他正常節(jié)點的上傳,直至異常節(jié)點超時返回或異常節(jié)點主從切換完成正常返回。該過程會影響正常分片的使用。
圖片
?減少lua腳本上傳:建議在初始化時執(zhí)行一次lua腳本上傳。jimdb內(nèi)部在主從節(jié)點也分別存儲了腳本,在發(fā)生主從切換時,可以不用再次上傳。
?針對特定異常補充上傳:當節(jié)點不可用或擴分片的場景捕獲該異常ScriptNotFoundException,重新上傳lua腳本。
五、MQ高可用
MQ在各個應(yīng)用場景中,被當做一個神兵利器來使用,但是通過這些年的觀察,較多人對其底層原理和各類注意事項并沒有太清晰的認知,此處以JMQ(Jingdong Message Queue)京東自研的低延遲、高并發(fā)、高可用、高可靠的分布式消息流處理平臺為案例,結(jié)合筆者曾經(jīng)遇到的問題展開描述,期望對大家有所幫助。
5.1 JMQ應(yīng)答超時故障
?表現(xiàn):某場景消費消息監(jiān)控在2025-06-24的14:17:10至14:19:03有明顯下降
?原因:上線中消費實例拉取消息后,直接關(guān)閉應(yīng)用tomcat實例,客戶端獲取到的隊列partition的占用就不會釋放,積壓2分鐘內(nèi)消息的積壓持續(xù)增長,2分鐘后又快速消費掉了。
需說明的是:以MQ消費存在的鎖消息及滑動窗口逐批消費的概念及原理,基于MQ的鏈路要想做到秒級無延遲無抖動,極難做到。
以下再舉一個較為形象的例子,來展開說明下MQ的發(fā)送及消費原理:
圖片
與redis、jimdb這種內(nèi)存式存儲的存儲中間件不同,MQ的存儲其實是基于連續(xù)的文件來存儲的,這一點認知特別重要。
以上方的消費示意圖來看,MQ概念中的Broker可以近似認為是各個單獨的容器,隊列則是各個容器中不同的存儲文件,可以簡單認為在一個Broker中每個隊列會對應(yīng)不同的文件,MQ的寫入方會順序?qū)懭胛募琈Q的消費方則會順序讀取文件。
可以觀測到,如果業(yè)務(wù)量級比較大的情況下,消息的消費速度主要首先于以下幾個因素:單個消息消費的耗時、消息Broker的數(shù)量、消息隊列的個數(shù)。可以看到單個消息消費的耗時越小,則消費速度越快;消息Broker的數(shù)量、消息隊列的個數(shù)越大,則消費的速度越快。這也是為什么不同的業(yè)務(wù)場景下,MQ團隊會給我們設(shè)置差異化的消息Broker的數(shù)量、消息隊列的個數(shù)的最主要的原因。
從上方的示意圖也可以看到,在一般的情況下,每個隊列的寫入和讀取都是順序的,以寫入為例,只有新的消息寫入成功后,下一條消息才可以在這個隊列繼續(xù)寫入;以消費為例,只有排在隊頭的消息被成功消費,隊頭后面的消息才有被消費的機會。一般而言,寫入的速度取決于文件存儲磁盤的速度,一般沒有瓶頸;往往有瓶頸的為消費的速度。消費的速度跟不上的情況下,MQ上觀測到的就是會出現(xiàn)消息積壓,業(yè)務(wù)無法及時處理的情況。
這種情況,因為瓶頸點在MQ服務(wù)器,往往增加應(yīng)用服務(wù)器的數(shù)量,并不會有好的改善效果。那么在受限于機器資源等原因?qū)е孪roker的數(shù)量、消息隊列的個數(shù)無法持續(xù)擴大的情況下,MQ團隊提供了一個方案可以進一步加速消費的速度。也即在每個隊列的維度上增加一定的并發(fā),實現(xiàn)原理為使用了滑動窗口的機制,即為每個隊列再虛擬出30個虛擬可并行執(zhí)行的隊列,假定每個隊列的并發(fā)數(shù)是30,則消費方理論上可以同時從這30個虛擬隊列中拿到消息。此種方式變相的相當于了擴大Broker或者隊列的數(shù)量,也是加速消費的比較好的實踐。
那么對這一點,有沒有什么注意的事項呢。當然有。問題點就在這個滑動窗口的機制和原理里。滑動窗口虛擬出30個并行的隊列,繼續(xù)往下滑的前提條件是這30個隊列的每一個消息都被成功消費了,請注意,這里指的是每一條消息都被成功消費了。那么很容易就可以觀測到,消息消費基本是分批處理的機制,每個批次的數(shù)量取決于并行的大小,假定批次中的任意一個消息沒有得到及時應(yīng)答,那么這個隊列后的所有消息,仍然不會被消費到,業(yè)務(wù)上觀測到的就是這個隊列的消息又會出現(xiàn)積壓了。
那么什么時候回出現(xiàn)沒有及時應(yīng)答的情況呢,這個就比較多了,在應(yīng)用服務(wù)器異常宕機、應(yīng)用服務(wù)器對應(yīng)的依賴服務(wù)出現(xiàn)劇烈抖動、少量抖動時,MQ服務(wù)器都不能快速收到消息成功消費的應(yīng)答,也即此隊列的消息有極大的概率會出現(xiàn)被堵住的情況。
這個問題有好的解決方案么?在筆者看來,并沒有太好的解決方案,設(shè)置MQ的默認應(yīng)答時長可以一定程度減緩這個問題的發(fā)生,但是本質(zhì)上無法解決。在考慮高并發(fā)無延遲的情況下,采用MQ的技術(shù)方案一定要慎重。另外,MQ本身設(shè)定的技術(shù)應(yīng)用場景就是為了上下游解耦使用,應(yīng)對的一個場景為削峰填谷,應(yīng)對的是上下游速率較大可能不一致的情況,或應(yīng)對的場景為有多個下游依賴方,指望通過服務(wù)的方式通知所有下游不太現(xiàn)實的情況。
有關(guān)消費的原理,再展開描述如下:
圖片
存儲:消息是存儲在partition里的,是一條挨著一條存儲的。如上圖的“服務(wù)端”。
消費:在客戶端拉取消息時,服務(wù)端會從消費位置(如上圖的“消費位置”)開始,拿一批消息返回給客戶端。客戶端A拉走一批消息后,服務(wù)端要避免,這批消息被另外一個客戶端拉走,所以在客戶端確認結(jié)果之前,該partition會一直被客戶端A占用,保證不會被其他客戶端再拉取消息。客戶端成功消費這些消息后,會給客戶端確認,服務(wù)端收到確認后,會移動消費位置,并釋放占用(鎖),等待下次拉取。
客戶端會有一個消費線程不停的在循環(huán)執(zhí)行:拉取消息 -- > 執(zhí)行消費邏輯 --> 確認結(jié)果 -- > 拉取消息 ………… 這個流程。
當服務(wù)端1號partition的消息被客戶端A拉走后,為了避免這批消息被重復(fù)消費,服務(wù)端會記錄:1號partition正在被客戶端A占用,避免被其他客戶端重復(fù)拉走消息。
這個時候,如果客戶端A突然假死(或者其他異常場景),那客戶端A對1號partition的占用就不會釋放。占用不釋放,1號partition的消息就永遠不會被其他客戶端消費到。為了避免這種情況,服務(wù)端會有一個占用超時的概念,即如果客戶端一個比較長的時間內(nèi)沒有返回消費結(jié)果,那服務(wù)端就認為遇到了特殊情況,客戶端將“永遠”不會再返回結(jié)果了。在這種場景下,服務(wù)端會主動清理占用,保證消費的繼續(xù)進行。占用超時在JMQ的管理端叫“應(yīng)答超時”,默認值是120秒,也正好匹配了此現(xiàn)象產(chǎn)生的原因。
解決方案
?措施一:超時應(yīng)答超時時間
?調(diào)整應(yīng)答超時,將應(yīng)答超時時間調(diào)小,減少上述場景帶來的影響。
?具體調(diào)整到多少:最近一段時間的消費tp999值的10倍。
?如果最近一周的最大tp999是5秒,那就調(diào)整成50秒.
?措施二:升級支持優(yōu)雅關(guān)機:MQ SDK底層增加支持,應(yīng)對此類場景支持優(yōu)雅關(guān)機,可主動釋放MQ服務(wù)端的鎖。
5.2 JMQ消息過大故障
目前消息發(fā)送,如果在消息體超過一定的大小的情況下,默認會開啟壓縮,但是在一些極限的情況下,仍然會出現(xiàn)消息體過大發(fā)送失敗的情況。對于這一點,筆者還是建議各個系統(tǒng)的業(yè)務(wù)模型需要盡量精簡,盡量降低無關(guān)的業(yè)務(wù)內(nèi)容全部一股腦塞到消息隊列中。
5.3 JMQ存儲故障
某次發(fā)現(xiàn)消息突然出現(xiàn)服務(wù)性能飆高的情況,多輪排查后發(fā)現(xiàn)是MQ服務(wù)器對應(yīng)的1G的下行網(wǎng)絡(luò)帶寬被寫滿,進而導(dǎo)致上行網(wǎng)絡(luò)帶寬發(fā)送也存在問題發(fā)送失敗。
出現(xiàn)這個問題,需得滿足幾個前提條件,首先是消息體比較大,其次是消費方比較多。在流量壓力突增的情況下,所有的消費者都從MQ服務(wù)上去獲取下載消息,類似一堆人去一個集中的服務(wù)器去拷片,理論上無上限瓶頸的網(wǎng)卡直接被打滿。























