1. 程式人生 > >淺談分散式專案日誌監控

淺談分散式專案日誌監控

 目前公司專案採用dubbo服務化升級之後,原先大而全的幾個主要應用,拆散重構成多個分散式服務。這個公司業務架構和系統架構實現一次升級,併發和業務開發效率得到提升。但是事情是兩面的,引入dubbo服務化之後,導致業務鏈路過長,日誌分散。不能在使用原來的日誌處理方式了。 
   分散式情況下,每個日誌分散到各自服務所在機器,日誌的收集和分析使用原來古老的模式,肯定是過時了,叢集和服務規模小還好,數量一大,我想不管是運維人員還是開發人員都會頭疼。 
   目前處理這個需求最為火熱的中間套件,自然首選是ELK,ELK是java技術棧的。也符合目前公司需求。ELK的安裝就不講述了,感興趣的可以檢視官網或者自行百度,資料還是挺多的。 


   確定了日誌收集和分析的中介軟體,剩下一個就是日誌埋點和怎麼把日誌串起來了。以前單個應用的時代,系統級別的日誌可以通過aop解決。在分散式情況下對每一個獨立服務而言,自身的日誌系統還是通過aop解決,唯一需要的就是怎麼把分散到各自不同應用的日誌串起來。這個有個高大上的說法叫做業務鏈監控。 
   目前國內開源的產品有大眾點評的cat,是整套業務鏈監控解決方案。對於我公司目前來說太重了,我這邊日誌已經有elk,就沒必要在額外引入cat。那如何自己實現呢。 
   既然是鏈路,那自然有入口有出口。我們需要做的就是在入口出生成一個全域性唯一的traceId,然後把這個traceId按照業務鏈路傳遞到各個服務中去。traceId就是一根線,把各個服務的日誌串起來。注意一點,服務的時間要同步,因為是根據來時間排序的。 

   traceId的生成,簡單方案可以採用uuid,其次推薦使用twiiter的snowflake演算法。 
   traceId的傳遞,需要根據rpc框架來實現了。dubbo框架採用dubbo的fiter來實現,參考程式碼如下: 

Java程式碼  收藏程式碼
  1.   // 呼叫過程攔截  
  2. public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {  
  3.     //非同步獲取serviceId,沒獲取到不進行取樣  
  4.     String serviceId = tracer.getServiceId(RpcContext.getContext().getUrl().getServiceInterface());  
  5.     if (serviceId == null) {  
  6.         Tracer.startTraceWork();  
  7.         return invoker.invoke(invocation);  
  8.     }  
  9.     long start = System.currentTimeMillis();  
  10.     RpcContext context = RpcContext.getContext();  
  11.     boolean isConsumerSide = context.isConsumerSide();  
  12.     Span span = null;  
  13.     Endpoint endpoint = null;  
  14.     try {  
  15.         endpoint = tracer.newEndPoint();  
  16.           endpoint.setServiceName(serviceId);  
  17.         endpoint.setIp(context.getLocalAddressString());  
  18.         endpoint.setPort(context.getLocalPort());  
  19.         if (context.isConsumerSide()) { //是否是消費者  
  20.             Span span1 = tracer.getParentSpan();  
  21.             if (span1 == null) { //為rootSpan  
  22.                 span = tracer.newSpan(context.getMethodName(), endpoint, serviceId);//生成root Span  
  23.             } else {  
  24.                 span = tracer.genSpan(span1.getTraceId(), span1.getId(), tracer.genSpanId(), context.getMethodName(), span1.isSample(), null);  
  25.             }  
  26.         } else if (context.isProviderSide()) {  
  27.             Long traceId, parentId, spanId;  
  28.             traceId = TracerUtils.getAttachmentLong(invocation.getAttachment(TracerUtils.TID));  
  29.             parentId = TracerUtils.getAttachmentLong(invocation.getAttachment(TracerUtils.PID));  
  30.             spanId = TracerUtils.getAttachmentLong(invocation.getAttachment(TracerUtils.SID));  
  31.             boolean isSample = (traceId != null);  
  32.             span = tracer.genSpan(traceId, parentId, spanId, context.getMethodName(), isSample, serviceId);  
  33.         }  
  34.         invokerBefore(invocation, span, endpoint, start);//記錄annotation  
  35.         RpcInvocation invocation1 = (RpcInvocation) invocation;  
  36.         setAttachment(span, invocation1);//設定需要向下遊傳遞的引數  
  37.         Result result = invoker.invoke(invocation);  
  38.         if (result.getException() != null){  
  39.             catchException(result.getException(), endpoint);  
  40.         }  
  41.         return result;  
  42.     }catch (RpcException e) {  
  43.         if (e.getCause() != null && e.getCause() instanceof TimeoutException){  
  44.             catchTimeoutException(e, endpoint);  
  45.         }else {  
  46.             catchException(e, endpoint);  
  47.         }  
  48.         throw e;  
  49.     }finally {  
  50.         if (span != null) {  
  51.             long end = System.currentTimeMillis();  
  52.             invokerAfter(invocation, endpoint, span, end, isConsumerSide);//呼叫後記錄annotation  
  53.         }  
  54.     }  
  55. }  

  dubbo通過invocation.setAttachmen來在消費者和呼叫者之間傳遞traceId。 
  如果是http介面呼叫實現的rpc建議採用在request的head裡面傳遞traceId。 
  在本地服務裡面通過threadlocal變數來傳遞traceId。 
  如果想列印sql語句,通過orm框架的攔截器機制實現,以下是mybatis的參考程式碼 
Java程式碼  收藏程式碼
  1.   @Intercepts({ @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }),  
  2.         @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,  
  3.                 RowBounds.class, ResultHandler.class }) })  
  4. public class MidaiLogMybatisPlugn implements Interceptor {  
  5.     @Override  
  6.     public Object intercept(Invocation invocation) throws Throwable {  
  7.         Object result = null;  
  8.         //從當前執行緒獲取trace  
  9.         MidaiLogTrace trace = MidaiLogTraceService.getMidaiLogTrace();  
  10.         if(trace !=null){  
  11.           Object[] arguments = invocation.getArgs();  
  12.           MidaiLogTraceService.traceSqlLog(trace.getTraceId(), getSqlStatement(arguments));  
  13.         }  
  14.         try {  
  15.             result = invocation.proceed();        
  16.         } catch (Exception e) {  
  17.             throw e;  
  18.         }  
  19.         return result;  
  20.     }  
  21.     @Override  
  22.     public Object plugin(Object target) {  
  23.         return Plugin.wrap(target, this); // mybatis提供的包裝工具類  
  24.     }  
  25.     @Override  
  26.     public void setProperties(Properties properties) {  
  27.     }  
  28.     private String getSqlStatement(Object[] arguments) {  
  29.         MappedStatement mappedStatement = (MappedStatement) arguments[0];  
  30.         Object parameter = null;  
  31.         if (arguments.length > 1) {  
  32.             parameter = arguments[1];  
  33.         }  
  34.         String sqlId = mappedStatement.getId();  
  35.         BoundSql boundSql = mappedStatement.getBoundSql(parameter);  
  36.         Configuration configuration = mappedStatement.getConfiguration();  
  37.         String sql = showSql(configuration, boundSql);  
  38.         StringBuilder str = new StringBuilder(100);  
  39.         str.append(sqlId);  
  40.         str.append(":");  
  41.         str.append(sql);  
  42.         str.append(":");  
  43.         return str.toString();  
  44.     }  
  45.     public String showSql(Configuration configuration, BoundSql boundSql) {  
  46.         Object parameterObject = boundSql.getParameterObject();  
  47.         List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();  
  48.         String sql = boundSql.getSql().replaceAll("[\\s]+"" ");  
  49.         if (parameterMappings.size() > 0 && parameterObject != null) {  
  50.             TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();  
  51.             if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {  
  52.                 sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));  
  53.             } else {  
  54.                 MetaObject metaObject = configuration.newMetaObject(parameterObject);  
  55.                 for (ParameterMapping parameterMapping : parameterMappings) {  
  56.                     String propertyName = parameterMapping.getProperty();  
  57.                     if (metaObject.hasGetter(propertyName)) {  
  58.                         Object obj = metaObject.getValue(propertyName);  
  59.                         sql = sql.replaceFirst("\\?", getParameterValue(obj));  
  60.                     } else if (boundSql.hasAdditionalParameter(propertyName)) {  
  61.                         Object obj = boundSql.getAdditionalParameter(propertyName);  
  62.                         sql = sql.replaceFirst("\\?", getParameterValue(obj));  
  63.                     }  
  64.                 }  
  65.             }  
  66.         }  
  67.         return sql;  
  68.     }  
  69.     private static String getParameterValue(Object obj) {  
  70.         String value = null;  
  71.         if (obj instanceof String) {  
  72.             value = "‘" + obj.toString() + "‘";  
  73.         } else if (obj instanceof Date) {  
  74.             DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);  
  75.             value = "‘" + formatter.format(new Date()) + "‘";  
  76.         } else {  
  77.             if (obj != null) {  
  78.                 value = obj.toString();  
  79.             } else {  
  80.                 value = "";  
  81.             }  
  82.         }  
  83.         return value;  
  84.     }  
  85. }  

    當系統併發達到一定數量級,log4j日誌列印本身會成為瓶頸,這個時候需要mq來解耦了,不在列印日誌,而是傳送mq訊息,由mq消費端處理。因為目前公司專案併發數量還不足以導致該問題,因此尚未採用。 
    elk收集日誌之後,通過kibana可以提供搜尋。 


    剩下最後的工作量就是提供一個web介面來更好的分析和展示資料。