Redis是典型的單線程架構(gòu),所有的讀寫操作都是在一條主線程中完成的腹躁。當(dāng)Redis用于高并發(fā)場景時(shí)南蓬,這條線程就變成了它的生命線。如果出現(xiàn)阻塞烧颖,哪怕是很短時(shí)間窄陡,對于我們的應(yīng)用來說都是噩夢。導(dǎo)致阻塞問題的場景大致分為內(nèi)在原因和外在原因:
內(nèi)在原因包括:不合理地使用API或數(shù)據(jù)結(jié)構(gòu)鳖悠、CPU飽和优妙、持久化阻塞等套硼。
外在原因包括:CPU競爭胞皱、內(nèi)存交換、網(wǎng)絡(luò)問題等雾鬼。
本章我們聚焦于Redis阻塞問題宴树,通過學(xué)習(xí)本章可掌握快速定位和解決Redis阻塞的思路和技巧。
發(fā)現(xiàn)阻塞
當(dāng)Redis阻塞時(shí)又憨,線上應(yīng)用服務(wù)應(yīng)該最先感知到,這時(shí)應(yīng)用方會(huì)收到大量Redis超時(shí)異常寒匙,比如Jedis客戶端會(huì)拋出JedisConnectionException異常锄弱。常見的做法是在應(yīng)用方加入異常統(tǒng)計(jì)并通過郵件/短信/微信報(bào)警祸憋,以便及時(shí)發(fā)現(xiàn)通知問題。開發(fā)人員需要處理如何統(tǒng)計(jì)異常以及觸發(fā)報(bào)警的時(shí)機(jī)狈谊。何時(shí)觸發(fā)報(bào)警一般根據(jù)應(yīng)用的并發(fā)量決定沟沙,如1分鐘內(nèi)超過10個(gè)異常觸發(fā)報(bào)警。在實(shí)際異常統(tǒng)計(jì)要注意赎瞎,由于Redis調(diào)用API會(huì)分散在項(xiàng)目的多個(gè)地方颊咬,每個(gè)地方都監(jiān)聽異常并加入監(jiān)控代碼必然難以維護(hù)。這是可以借助于日志系統(tǒng)敞临,如Java語言可以使用logback或log4j。當(dāng)異常發(fā)生時(shí)挺尿,異常信息最終會(huì)被日志系統(tǒng)收集到Appender(輸出目的地)炊邦,默認(rèn)的Appender一般是具體的日志文件,開發(fā)人員可以自定義一個(gè)Appender窄俏,用于專門統(tǒng)計(jì)異常和觸發(fā)報(bào)警邏輯碘菜,如下圖所示:
以Java的logback為例限寞,實(shí)現(xiàn)代碼如下:
public class RedisAppender extends AppenderBase<ILoggingEvent> {
//使用guava的AtomicLongMap昆烁,用于并發(fā)計(jì)數(shù)
public static final AtomicLongMap<String> ATOMIC_LONG_MAP = AtomicLongMap.create();
static{
//自定義Appender加入到logback的rootLogger中
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
Logger rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
ErrorStatisticsAppender errorStatisticsAppender = new ErrorStatisticsAppender();
errorStatisticsAppender.setContext(loggerContext);
errorStatisticsAppender.start();
rootLogger.addAppender(errorStatisticsAppender);
}
}
//重寫接收日志時(shí)間方法
protexted void append(ILoggingEvent event) {
//只監(jiān)控error級別日志
if(event.getLevel() == Level.ERROR){
IThrowableProxy throwableProxy = event.getThrowableProxy();
//確認(rèn)拋出異常
if (throwableProxy != null) {
//以每分鐘為key静尼,記錄每分鐘異常數(shù)量
String key = DateUtil.formatDate(new Date(), "yyyyMMddHHmm");
long errorCount = ATOMIC_LONG_MAP.increnmentAndGet(key);
if (errorCount > 10) {
//超過10次觸發(fā)報(bào)警代碼
}
//清理歷史計(jì)數(shù)統(tǒng)計(jì)传泊,防止極端情況下內(nèi)存泄露
for (String oldKey : ATOMIC_LONG_MAP.asMap().keySet()) {
if (!StringUtils.equals(key, oldKey)) {
ATOMIC_LONG_MAP.remove(oldKey);
}
}
}
}
}
開發(fā)提示:借助日志系統(tǒng)統(tǒng)計(jì)異常的前提是,需要項(xiàng)目必須使用日志API進(jìn)行異常統(tǒng)一輸出拦盹,比如所有的一場都通過logger.error打印普舆,這應(yīng)該作為開發(fā)規(guī)范推廣。其他編程語言也可以采用類似的日志系統(tǒng)實(shí)現(xiàn)異常統(tǒng)計(jì)報(bào)警沼侣。
應(yīng)用方加入異常監(jiān)控之后還存在一個(gè)問題歉秫,當(dāng)開發(fā)人員接到異常報(bào)警后,通常會(huì)去線上服務(wù)器查看錯(cuò)誤日志細(xì)節(jié)轧膘。這是如果應(yīng)用操作的是多個(gè)Redis節(jié)點(diǎn)(比如使用Redis集群)兔甘,如何決定是哪一個(gè)節(jié)點(diǎn)超時(shí)還是所有的節(jié)點(diǎn)都有超時(shí)呢?這是線上很常見的需求裂明,但絕大多數(shù)的客戶端類庫并沒有在異常信息中打印ip和port信息,導(dǎo)致無法快速定位是哪個(gè)Redis節(jié)點(diǎn)超時(shí)。不過修改Redis客戶端成本很低提岔,比如Jedis只需要修改Conneciton類下的connect、sendCommand碱蒙、readProtocalWithCheckingBroken方法專門捕獲連接夯巷,發(fā)送命令哀墓,協(xié)議讀取時(shí)間的異常。由于客戶端類庫都會(huì)保存ip和port信息后雷,在異常發(fā)生時(shí)很容易打印出對應(yīng)節(jié)點(diǎn)的ip和port吠各,輔助我們快速定位問題節(jié)點(diǎn)。
除了在應(yīng)用方加入統(tǒng)計(jì)報(bào)警邏輯之外贾漏,還可以借助Redis監(jiān)控系統(tǒng)發(fā)現(xiàn)阻塞問題,當(dāng)監(jiān)控系統(tǒng)監(jiān)測到Redis運(yùn)行期的一些關(guān)鍵指標(biāo)出現(xiàn)不正常時(shí)會(huì)觸發(fā)報(bào)警梳码。Redis相關(guān)的監(jiān)控系統(tǒng)開源的方案有很多掰茶,一些公司內(nèi)部也會(huì)自己開發(fā)監(jiān)控系統(tǒng)硕盹。一個(gè)可靠的Redis監(jiān)控系統(tǒng)首先需要做到對關(guān)鍵指標(biāo)全方位監(jiān)控和異常識(shí)別,輔助開發(fā)運(yùn)維人員發(fā)現(xiàn)定位問題啊胶。如果Redis服務(wù)沒有引入監(jiān)控系統(tǒng)作輔助支撐,對于線上的服務(wù)是非常不負(fù)責(zé)任和危險(xiǎn)的焰坪。這里推薦CacheCloud系統(tǒng)聘惦,它內(nèi)部的統(tǒng)計(jì)監(jiān)控模塊能夠很好地輔助工程師發(fā)現(xiàn)定位問題。
監(jiān)控系統(tǒng)所監(jiān)控的關(guān)鍵指標(biāo)有很多黔漂,如命令消耗晶默、慢查詢、持久化阻塞剂跟、連接拒絕减途、CPU/內(nèi)存/網(wǎng)絡(luò)/磁盤使用過載等。當(dāng)出現(xiàn)阻塞時(shí)如果相關(guān)人員不能深刻理解這些關(guān)鍵指標(biāo)的含義和背后的原理辽剧,會(huì)嚴(yán)重影響解決問題的速度。后面的內(nèi)容將圍繞引起Redis阻塞的原因做重點(diǎn)說明怕轿。