Thrift 個人實戰--Thrift RPC服務框架日志的優化


 

前言:
  Thrift作為Facebook開源的RPC框架, 通過IDL中間語言, 並借助代碼生成引擎生成各種主流語言的rpc框架服務端/客戶端代碼. 不過Thrift的實現, 簡單使用離實際生產環境還是有一定距離, 本系列將對Thrift作代碼解讀和框架擴充, 使得它更加貼近生產環境. 本文講述RPC服務框架中, 日志的重要性, 以及logid的引入. 日志不僅包含豐富的數據(就看是否會挖掘), 而且還是線上服務問題追蹤和排查錯誤最好的方式.

日志級別
  采用大家喜聞樂見的log4j作為該RPC服務框架首選的日志庫. 其對日志的級別有如下幾種:
  1). TRACE 最細粒度級別日志級別
  2). DEBUG 對調試應用程序有幫助的日志級別
  3). INFO 粗粒度級別突出強調應用程序的運行
  4). WARN 表明潛在錯誤的情形
  5). ERROR 明確發生錯誤, 但不影響系統繼續運行
  6). FATAL 嚴重的錯誤, 會導致應用工作不正常
  日志級別等級順序如下: TRACE < DEBUG < INFO < WARN < ERROR < FATAL
  而應用具體的輸出取決於日志級別的設置(包含及以上才會輸出), 往往項目該上線采用DEBUG級別(日志量大, 容易寫滿磁盤), 等系統穩定后采用INFO級別

RPC服務日志需求
  上述的日志需求雖然能定位問題, 但往往存在如下問題:
  1). 很多日志只是簡單了記錄該點(代碼行)運行過, 或是運行到該點的數據快照.
  2). 服務由多種模塊(每個模塊由有多個節點構成)組成, 之間的日志串聯不起來.
  而好的日志設計, 必須能滿足
  1). 以完整的一次RPC調用作為單位(不是某個執行點快照, 而是完整的RPC callback過程), 並輸出完整的一行日志記錄, 包括(時間點, 來源, 輸入參數, 輸出參數, 中間經歷的子過程, 消耗時間).
  2). 引入logid, 作為多個模塊之間串聯的依據.

RPC級別的日志解決方案
  嘗試如下navie的方式去實現

public String echo(String msg) {

  StringBuilder sb = new StringBuilder();
  // *) 記錄輸入參數
  sb.append("[request: {msg: msg}]");
  // *) 訪問緩存服務
  sb.append("[action: access redis, consume 100ms]");
  // *). 訪問后端數據庫
  sb.append("[action: dao, consume 100ms]");
  // *). 記錄返回結果
  sb.append("[response: {msg}]");	

  logger.info(sb.toString());	
  return msg;

}

  評注: 這邊的echo函數代表了一個rpc服務調用接口, 且簡化了各個組件的交互. 同時引入StringBuilder, 記錄各個交互的過程和時間消耗, 最后統一由函數出口前使用logger進行日志的統一輸入.
  但是這種方式弊端非常的明顯:
  1. 假設該rpc服務的函數, 存在多個出口
  2. 函數存在嵌套調用, 需要嵌套子函數的過程信息
  如下面的代碼片段, 可參考:

public boolean verifySession() {
  // ***********我要記錄日志(*^__^*) *************** 
}

public String echo(String msg) {

  StringBuilder sb = new StringBuilder();

  // *) 調用子過程
  verifySession();

  // *) 記錄輸入參數
  sb.append("[request: {msg: msg}]");
  // *) 訪問緩存服務
  if ( KeyValueEngine Access Fail ) {
    // *********日志記錄在那里***********
    throw new Exception();
  }
  sb.append("[action: access redis, consume 100ms]");

  // *). 訪問后端數據庫
  if ( Database Access Fail ) {
    // *********日志記錄在那里***********
    throw new Exception();
  }
  sb.append("[action: dao, consume 100ms]");
  // *). 記錄返回結果
  sb.append("[response: {msg}]");

  logger.info(sb.toString());

  return msg;

}

  評注: 子函數verifySession的調用, 需要把StringBuilder對象往里傳, 才能記錄相關的信息, 而多個異常出口, 需要把日志輸入往里添加(這個繁瑣且容易忘記). 這種方案只能說容易想到, 但不是最佳的方案.
  有一點不可否認, rpc調用始終在同一個線程中. 聰明的讀者是否猜到了最佳的解決方案.
  對, 就是大殺器ThreadLocal,其能解決子函數調用的問題, 那多出口問題呢? 讓rpc服務框架去處理, 其作為具體rpc調用的最外層.

  采用動態代理類, 去攔截rpc的handler接口調用.

public class LogProxyHandler<T> implements InvocationHandler {

  private T instance;

  public LogProxyHandler(T instance) {
    this.instance = instance;
  }

  public Object createProxy() {
    return Proxy.newProxyInstance(instance.getClass().getClassLoader(), 
          instance.getClass().getInterfaces(), this);
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable {
    // *) 函數調用前, 攔截處理, 作ThreadLocal的初始化工作
    LoggerUtility.beforeInvoke();	 // -----(1)
    try {
      Object res = method.invoke(instance, args);
      // *) 函數成功返回后, 攔截處理, 進行日志的集中輸出 
      LoggerUtility.returnInvoke();	 // -----(2)
      return res;
    } catch (Throwable e) {
      // *) 出現異常后, 攔截處理, 進行日志集中輸入 // -----(3)
      LoggerUtility.throwableInvode("[result = exception: {%s}]", e.getMessage());
      throw e;
    }

  }

}

  代碼評注:
    (1). 攔截點beforeInvoke用於ThreadLocal的初始話工作, 日志緩存的清空
    (2). 攔截點returnInvoke用於函數成功返回后, 進行日志集中輸出
    (3). 攔截點throwableInvoke用於出現異常后, 進行日志的集中輸出
  同時在rpc服務調用中, 才用LoggerUtility的noticeLog靜態函數(簡單緩存中間日志過程)代替之前的StringBuilder.append來記錄中間子過程
  LoggerUtility的代碼如下所示:

public class LoggerUtility {

  private static final Logger rpcLogger = LoggerFactory.getLogger("rpc");

  public static final ThreadLocal<StringBuilder> threadLocals = new ThreadLocal<StringBuilder>();

  public static void beforeInvoke() {
    StringBuilder sb = threadLocals.get();
    if ( sb == null ) {
      sb = new StringBuilder();
      threadLocals.set(sb);
    }
    sb.delete(0, sb.length());
  }

  public static void returnInvoke() {
    StringBuilder sb = threadLocals.get();
    if ( sb != null ) {
      rpcLogger.info(sb.toString());
    }
  }

  public static void throwableInvode(String fmt, Object... args) {
    StringBuilder sb = threadLocals.get();
    if ( sb != null ) {
      rpcLogger.info(sb.toString() + " " + String.format(fmt, args));
    }
  }

  public static void noticeLog(String fmt, Object... args) {
    StringBuilder sb = threadLocals.get();
    if ( sb != null ) {
      sb.append(String.format(fmt, args));
    }
  }

}

  兩者的結合完美的解決了上述RPC的日志問題, 是不是很贊.

Logid的日志解決方案
  Thrift框架本身是沒有logid的概念的, 我們很難去改動thrift的rpc協議, 去添加它(比如大百度的做法是把logid作為rpc協議本身一部分). 這邊的解決方案是基於約定. 我們采用如下約定, 所有的rpc請求參數都封裝為一個具體Request對象, 所有的返回結構都封裝為一個具體的Response對象, 而每個Request對象首個屬性是logid.
  比如如下的結構定義:

struct EchoRequest {
  1: required i64 logid = 1001,
  2: required string msg	
}
struct EchoResponse {
  1: required i32 status,
  2: optional string msg
}

service EchoService {
  EchoResponse echo(1: EchoRequest req);
}

  評注: Request結構中logid, 就是約定的需要加到rpc的請求結構里去的.
  我一直覺得: 約定優於配置, 約定優於框架.

后續
  中間插入日志處理這塊, 后續講述之前計划的服務發布/訂閱化, 借助zookeeper來構建一個簡單的系統, 敬請期待.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM