天天看點

日志級别動态調整--小工具解決大問題背景場景介紹系統基礎架構具體實作招聘

背景

随着外賣業務的快速發展,業務複雜度不斷增加,線上系統環境有任何細小波動,對整個外賣業務都可能産生巨大的影響,甚至形成災難性的雪崩效應,造成巨大的經濟損失,因而每一次客訴、系統抖動等都是對我們技術人員的重大考驗:我們必須立即響應,快速解決問題。但是,如何提高排查問題的效率呢?最有效的方式是通過分析系統日志。如果系統日志全面,會給我們排查解決線上問題帶來絕大幫助,但是要想保證系統日志全面,就必須列印出所有的系統或業務日志,這樣就會帶來另一個問題:日志量的暴漲,過多的日志除了能夠幫助我們解決問題外,同時會直接造成系統性能下降,極端情況下,甚至導緻系統當機。在這種背景下,為了兼顧性能和快速響應線上問題,我們設計開發了日志級别動态調整元件。通過使用該元件,可以在需要解決線上問題時,實時調整線上日志輸出級别,擷取全面的debug日志,幫助工程師提高定位問題的效率。

場景介紹

場景一:

業務依賴複雜,某一時刻,依賴的下遊服務故障,導緻請求大量逾時,尤其是對于外賣這種集中性特别明顯的業務,平均每秒QPS在8000以上,1分鐘的故障就會集中産生大量的錯誤日志,導緻磁盤IO急劇提高,耗費大量cpu,進而導緻整個服務癱瘓。如果該業務不能立即降級,怎麼辦? 

修改日志級别,發版上線,流程長,操作麻煩暫且不談,同時存在引入其它故障的高風險。如果系統恰好使用的log4j版本,在面對極短時間内列印出的海量錯誤日志,會快速耗盡buffer區記憶體,進而拖慢主線程,造成服務性能整體下降,甚至還未來得及修複問題,海量日志已經拖垮服務,造成服務當機了,損失慘重。

場景二:

大量的訂單、結算等客訴問題回報過來,一線工程師大量精力埋沒于排查問題中,而排查定位問題的最終手段仍然是依賴線上日志,由于鍊路較長,任一日志的缺失,都給問題的排查帶來極大的障礙,面對營運的催促,怎麼辦?

工程師為了以後排查問題的友善,在任一可能出現異常的地方,列印出關鍵日志。然後發版上線,好不容易解決了本次問題,還沒來得及收獲喜悅,就又面臨着一個新問題,請看場景三。

場景三:

由于線上業務系統預設日志列印級别是INFO級别,為了排查問題友善,調試型日志都以該級别列印出,給系統帶來了額外的負擔,高峰期大量調試日志拖慢系統性能,增大出故障的風險,怎麼辦?

一方面要快速響應業務,另一方面要兼顧系統性能,是否可以兩方面都兼顧?我們的動态調整日志級别工具正是為了解決這種痛點。

該元件能夠解決什麼問題?

1、日志降級。相容log4j、log4j2和logback主流日志架構,如果遇到場景一,可以通過我們的日志工具,快速調整日志輸出級别,降低系統日志的輸出,進而達到日志降級的效果,同時能夠給RD争取充裕的排查問題時間。

2、規範日志級别濫用,幫助工程師快速定位解決線上問題。使用日志級别動态調整元件,可以實時動态調整線上服務的日志列印級别,調試型日志可以使用低級别列印出,減輕線上服務的負載壓力,遇到排查問題時,可以臨時将日志級别調低,快速得到精準化的日志資訊,排查解決問題。

系統基礎架構

日志級别動态調整元件定位為中間件,在設計之初重點考慮了以下幾點:

1、低侵入性

  • 接入服務僅需要引入jar包和xml配置檔案即可,不存在額外編碼工作,業務耦合低,接入成本小。

2、安全可靠

  • 更改接入服務的日志輸出級别,隻能通過我們授權的背景系統操作,所有的操作記錄有迹可查。
  • 引入權限認證,確定工程師隻能操作自己負責的服務或系統,同時會把操作内容實時周知給系統的所有相關責任人,避免誤傷。

3、可視化操作

  • 操作者可以通過我們提供的管理頁面,定向修改一個或一批服務節點。
  • 提供可視化的操控開關,可以随時關閉或開啟服務。
日志級别動态調整--小工具解決大問題背景場景介紹系統基礎架構具體實作招聘

具體實作

核心調用元件

本元件采用工廠模式實作,保障其高可擴充性。目前已實作日志級别動态調整和方法調用處理單元,下面主要介紹日志級别動态調整處理單元的實作。

日志級别動态調整--小工具解決大問題背景場景介紹系統基礎架構具體實作招聘

目前咱們外賣業務系統基本統一采用的slf4j日志架構,在應用初始化時,slf4j會綁定具體的日志架構,如log4j、logback或log4j2等。具體源碼如下(slf4j-api-1.7.7):

private

final

static

void

bind() {

try

{

Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();

reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);

// the next line does the binding

StaticLoggerBinder.getSingleton();

INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;

reportActualBinding(staticLoggerBinderPathSet);

fixSubstitutedLoggers();

catch

(NoClassDefFoundError ncde) {

String msg = ncde.getMessage();

if

(messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {

INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;

Util.report(

"Failed to load class \"org.slf4j.impl.StaticLoggerBinder\"."

);

Util.report(

"Defaulting to no-operation (NOP) logger implementation"

);

Util.report(

"See "

+ NO_STATICLOGGERBINDER_URL

" for further details."

);

else

{

failedBinding(ncde);

throw

ncde;

}

catch

(java.lang.NoSuchMethodError nsme) {

String msg = nsme.getMessage();

if

(msg != 

null

&& msg.indexOf(

"org.slf4j.impl.StaticLoggerBinder.getSingleton()"

) != -

1

) {

INITIALIZATION_STATE = FAILED_INITIALIZATION;

Util.report(

"slf4j-api 1.6.x (or later) is incompatible with this binding."

);

Util.report(

"Your binding is version 1.5.5 or earlier."

);

Util.report(

"Upgrade your binding to version 1.6.x."

);

}

throw

nsme;

catch

(Exception e) {

failedBinding(e);

throw

new

IllegalStateException(

"Unexpected initialization failure"

, e);

}

}

findPossibleStaticLoggerBinderPathSet方法用來查找目前classpath下所有的org.slf4j.impl.StaticLoggerBinder類。每一個slf4j橋接包中都有一個StaticLoggerBinder類,該類實作了LoggerFactoryBinder接口。具體綁定到哪一個日志架構則取決于類加載順序。

接下來,咱們分三部分,來說說ChangeLogLevelProcessUnit類:

1.初始化:确定所使用的日志架構, 擷取配置檔案中所有的logger記憶體執行個體,并将它們的引用緩存到map容器中。

String type = StaticLoggerBinder.getSingleton().getLoggerFactoryClassStr();

if

(LogConstant.LOG4J_LOGGER_FACTORY.equals(type)) {

logFrameworkType = LogFrameworkType.LOG4J;

Enumeration enumeration = org.apache.log4j.LogManager.getCurrentLoggers();

while

(enumeration.hasMoreElements()) {

org.apache.log4j.Logger logger = (org.apache.log4j.Logger) enumeration.nextElement();

if

(logger.getLevel() != 

null

) {

loggerMap.put(logger.getName(), logger);

}

}

org.apache.log4j.Logger rootLogger = org.apache.log4j.LogManager.getRootLogger();

loggerMap.put(rootLogger.getName(), rootLogger);

else

if

(LogConstant.LOGBACK_LOGGER_FACTORY.equals(type)) {

logFrameworkType = LogFrameworkType.LOGBACK;

ch.qos.logback.classic.LoggerContext loggerContext = (ch.qos.logback.classic.LoggerContext) LoggerFactory.getILoggerFactory();

for

(ch.qos.logback.classic.Logger logger : loggerContext.getLoggerList()) {

if

(logger.getLevel() != 

null

) {

loggerMap.put(logger.getName(), logger);

}

}

ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);

loggerMap.put(rootLogger.getName(), rootLogger);

else

if

(LogConstant.LOG4J2_LOGGER_FACTORY.equals(type)) {

logFrameworkType = LogFrameworkType.LOG4J2;

org.apache.logging.log4j.core.LoggerContext loggerContext = (org.apache.logging.log4j.core.LoggerContext) org.apache.logging.log4j.LogManager.getContext(

false

);

Map<String, org.apache.logging.log4j.core.config.LoggerConfig> map = loggerContext.getConfiguration().getLoggers();

for

(org.apache.logging.log4j.core.config.LoggerConfig loggerConfig : map.values()) {

String key = loggerConfig.getName();

if

(StringUtils.isBlank(key)) {

key = 

"root"

;

}

loggerMap.put(key, loggerConfig);

}

else

{

logFrameworkType = LogFrameworkType.UNKNOWN;

LOG.error(

"Log架構無法識别: type={}"

, type);

}

2.擷取logger清單:從本地map容器取出。

private

String getLoggerList() {

JSONObject result = 

new

JSONObject();

result.put(

"logFramework"

, logFrameworkType);

JSONArray loggerList = 

new

JSONArray();

for

(ConcurrentMap.Entry<String, Object> entry : loggerMap.entrySet()) {

JSONObject loggerJSON = 

new

JSONObject();

loggerJSON.put(

"loggerName"

, entry.getKey());

if

(logFrameworkType == LogFrameworkType.LOG4J) {

org.apache.log4j.Logger targetLogger = (org.apache.log4j.Logger) entry.getValue();

loggerJSON.put(

"logLevel"

, targetLogger.getLevel().toString());

else

if

(logFrameworkType == LogFrameworkType.LOGBACK) {

ch.qos.logback.classic.Logger targetLogger = (ch.qos.logback.classic.Logger) entry.getValue();

loggerJSON.put(

"logLevel"

, targetLogger.getLevel().toString());

else

if

(logFrameworkType == LogFrameworkType.LOG4J2) {

org.apache.logging.log4j.core.config.LoggerConfig targetLogger = (org.apache.logging.log4j.core.config.LoggerConfig) entry.getValue();

loggerJSON.put(

"logLevel"

, targetLogger.getLevel().toString());

else

{

loggerJSON.put(

"logLevel"

"Logger的類型未知,無法處理!"

);

}

loggerList.add(loggerJSON);

}

result.put(

"loggerList"

, loggerList);

LOG.info(

"getLoggerList: result={}"

, result.toString());

return

result.toString();

}

3.修改logger的級别

private

String setLogLevel(JSONArray data) {

LOG.info(

"setLogLevel: data={}"

, data);

List<LoggerBean> loggerList = parseJsonData(data);

if

(CollectionUtils.isEmpty(loggerList)) {

return

""

;

}

for

(LoggerBean loggerbean : loggerList) {

Object logger = loggerMap.get(loggerbean.getName());

if

(logger == 

null

) {

throw

new

RuntimeException(

"需要修改日志級别的Logger不存在"

);

}

if

(logFrameworkType == LogFrameworkType.LOG4J) {

org.apache.log4j.Logger targetLogger = (org.apache.log4j.Logger) logger;

org.apache.log4j.Level targetLevel = org.apache.log4j.Level.toLevel(loggerbean.getLevel());

targetLogger.setLevel(targetLevel);

else

if

(logFrameworkType == LogFrameworkType.LOGBACK) {

ch.qos.logback.classic.Logger targetLogger = (ch.qos.logback.classic.Logger) logger;

ch.qos.logback.classic.Level targetLevel = ch.qos.logback.classic.Level.toLevel(loggerbean.getLevel());

targetLogger.setLevel(targetLevel);

else

if

(logFrameworkType == LogFrameworkType.LOG4J2) {

org.apache.logging.log4j.core.config.LoggerConfig loggerConfig = (org.apache.logging.log4j.core.config.LoggerConfig) logger;

org.apache.logging.log4j.Level targetLevel = org.apache.logging.log4j.Level.toLevel(loggerbean.getLevel());

loggerConfig.setLevel(targetLevel);

org.apache.logging.log4j.core.LoggerContext ctx = (org.apache.logging.log4j.core.LoggerContext) org.apache.logging.log4j.LogManager.getContext(

false

);

ctx.updateLoggers(); 

// This causes all Loggers to refetch information from their LoggerConfig.

else

{

throw

new

RuntimeException(

"Logger的類型未知,無法處理!"

);

}

}

return

"success"

;

}

上面介紹了log架構在啟動加載時,如何拿到系統日志配置檔案中的logger,以及具體修改logger級别的實作方法。

通信方式

我們根據web項目和純粹RPC項目,分别提供http和thrift兩種通信協定。

一、thrift服務

service InvokeService 

extends

fb303.FacebookService {

string invoke(

1

: string jsonString),

}

所有的請求資訊都包含在jsonString的資料結構裡面,其中包含有簽名資訊,請求時簽名驗證失敗将直接抛出異常。

引入元件提供的dynamic-invoker.xml配置,将會在系統中自動注入開啟一個專為日志級别調整的接口服務,該接口是一個單純的thrift服務,能夠通過zookeeper實作服務注冊與發現,并且有可視化的開啟與關閉管理背景,簡單明了,操作友善。

二、http服務

對于一些web項目,暴露一個rpc服務相當不安全。為此,我們提供了http協定接口,接入流程完全一樣,在真正修改日志輸出級别時,會根據系統類型自主判斷使用哪種協定,有獨立實作的簽名認證,安全可靠。

招聘

歡迎感興趣的同學随時與我們溝通交流,我們有高并發的業務,有各種牛逼的中間件技術,期待您的加入,我們一起成長。

履歷砸來:[email protected];

微信二維碼:

日志級别動态調整--小工具解決大問題背景場景介紹系統基礎架構具體實作招聘