淺談分散式專案日誌監控
阿新 • • 發佈:2018-12-31
目前公司專案採用dubbo服務化升級之後,原先大而全的幾個主要應用,拆散重構成多個分散式服務。這個公司業務架構和系統架構實現一次升級,併發和業務開發效率得到提升。但是事情是兩面的,引入dubbo服務化之後,導致業務鏈路過長,日誌分散。不能在使用原來的日誌處理方式了。
分散式情況下,每個日誌分散到各自服務所在機器,日誌的收集和分析使用原來古老的模式,肯定是過時了,叢集和服務規模小還好,數量一大,我想不管是運維人員還是開發人員都會頭疼。
目前處理這個需求最為火熱的中間套件,自然首選是ELK,ELK是java技術棧的。也符合目前公司需求。ELK的安裝就不講述了,感興趣的可以檢視官網或者自行百度,資料還是挺多的。
確定了日誌收集和分析的中介軟體,剩下一個就是日誌埋點和怎麼把日誌串起來了。以前單個應用的時代,系統級別的日誌可以通過aop解決。在分散式情況下對每一個獨立服務而言,自身的日誌系統還是通過aop解決,唯一需要的就是怎麼把分散到各自不同應用的日誌串起來。這個有個高大上的說法叫做業務鏈監控。
目前國內開源的產品有大眾點評的cat,是整套業務鏈監控解決方案。對於我公司目前來說太重了,我這邊日誌已經有elk,就沒必要在額外引入cat。那如何自己實現呢。
既然是鏈路,那自然有入口有出口。我們需要做的就是在入口出生成一個全域性唯一的traceId,然後把這個traceId按照業務鏈路傳遞到各個服務中去。traceId就是一根線,把各個服務的日誌串起來。注意一點,服務的時間要同步,因為是根據來時間排序的。
traceId的生成,簡單方案可以採用uuid,其次推薦使用twiiter的snowflake演算法。
traceId的傳遞,需要根據rpc框架來實現了。dubbo框架採用dubbo的fiter來實現,參考程式碼如下:
Java程式碼
- // 呼叫過程攔截
- public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
- //非同步獲取serviceId,沒獲取到不進行取樣
-
String serviceId = tracer.getServiceId(RpcContext.getContext().getUrl().getServiceInterface());
- if (serviceId == null) {
- Tracer.startTraceWork();
- return invoker.invoke(invocation);
- }
- long start = System.currentTimeMillis();
- RpcContext context = RpcContext.getContext();
- boolean isConsumerSide = context.isConsumerSide();
- Span span = null;
- Endpoint endpoint = null;
- try {
- endpoint = tracer.newEndPoint();
- endpoint.setServiceName(serviceId);
- endpoint.setIp(context.getLocalAddressString());
- endpoint.setPort(context.getLocalPort());
- if (context.isConsumerSide()) { //是否是消費者
- Span span1 = tracer.getParentSpan();
- if (span1 == null) { //為rootSpan
- span = tracer.newSpan(context.getMethodName(), endpoint, serviceId);//生成root Span
- } else {
- span = tracer.genSpan(span1.getTraceId(), span1.getId(), tracer.genSpanId(), context.getMethodName(), span1.isSample(), null);
- }
- } else if (context.isProviderSide()) {
- Long traceId, parentId, spanId;
- traceId = TracerUtils.getAttachmentLong(invocation.getAttachment(TracerUtils.TID));
- parentId = TracerUtils.getAttachmentLong(invocation.getAttachment(TracerUtils.PID));
- spanId = TracerUtils.getAttachmentLong(invocation.getAttachment(TracerUtils.SID));
- boolean isSample = (traceId != null);
- span = tracer.genSpan(traceId, parentId, spanId, context.getMethodName(), isSample, serviceId);
- }
- invokerBefore(invocation, span, endpoint, start);//記錄annotation
- RpcInvocation invocation1 = (RpcInvocation) invocation;
- setAttachment(span, invocation1);//設定需要向下遊傳遞的引數
- Result result = invoker.invoke(invocation);
- if (result.getException() != null){
- catchException(result.getException(), endpoint);
- }
- return result;
- }catch (RpcException e) {
- if (e.getCause() != null && e.getCause() instanceof TimeoutException){
- catchTimeoutException(e, endpoint);
- }else {
- catchException(e, endpoint);
- }
- throw e;
- }finally {
- if (span != null) {
- long end = System.currentTimeMillis();
- invokerAfter(invocation, endpoint, span, end, isConsumerSide);//呼叫後記錄annotation
- }
- }
- }
dubbo通過invocation.setAttachmen來在消費者和呼叫者之間傳遞traceId。
如果是http介面呼叫實現的rpc建議採用在request的head裡面傳遞traceId。
在本地服務裡面通過threadlocal變數來傳遞traceId。
如果想列印sql語句,通過orm框架的攔截器機制實現,以下是mybatis的參考程式碼
Java程式碼
- @Intercepts({ @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }),
- @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
- RowBounds.class, ResultHandler.class }) })
- public class MidaiLogMybatisPlugn implements Interceptor {
- @Override
- public Object intercept(Invocation invocation) throws Throwable {
- Object result = null;
- //從當前執行緒獲取trace
- MidaiLogTrace trace = MidaiLogTraceService.getMidaiLogTrace();
- if(trace !=null){
- Object[] arguments = invocation.getArgs();
- MidaiLogTraceService.traceSqlLog(trace.getTraceId(), getSqlStatement(arguments));
- }
- try {
- result = invocation.proceed();
- } catch (Exception e) {
- throw e;
- }
- return result;
- }
- @Override
- public Object plugin(Object target) {
- return Plugin.wrap(target, this); // mybatis提供的包裝工具類
- }
- @Override
- public void setProperties(Properties properties) {
- }
- private String getSqlStatement(Object[] arguments) {
- MappedStatement mappedStatement = (MappedStatement) arguments[0];
- Object parameter = null;
- if (arguments.length > 1) {
- parameter = arguments[1];
- }
- String sqlId = mappedStatement.getId();
- BoundSql boundSql = mappedStatement.getBoundSql(parameter);
- Configuration configuration = mappedStatement.getConfiguration();
- String sql = showSql(configuration, boundSql);
- StringBuilder str = new StringBuilder(100);
- str.append(sqlId);
- str.append(":");
- str.append(sql);
- str.append(":");
- return str.toString();
- }
- public String showSql(Configuration configuration, BoundSql boundSql) {
- Object parameterObject = boundSql.getParameterObject();
- List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
- String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
- if (parameterMappings.size() > 0 && parameterObject != null) {
- TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
- if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
- sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));
- } else {
- MetaObject metaObject = configuration.newMetaObject(parameterObject);
- for (ParameterMapping parameterMapping : parameterMappings) {
- String propertyName = parameterMapping.getProperty();
- if (metaObject.hasGetter(propertyName)) {
- Object obj = metaObject.getValue(propertyName);
- sql = sql.replaceFirst("\\?", getParameterValue(obj));
- } else if (boundSql.hasAdditionalParameter(propertyName)) {
- Object obj = boundSql.getAdditionalParameter(propertyName);
- sql = sql.replaceFirst("\\?", getParameterValue(obj));
- }
- }
- }
- }
- return sql;
- }
- private static String getParameterValue(Object obj) {
- String value = null;
- if (obj instanceof String) {
- value = "‘" + obj.toString() + "‘";
- } else if (obj instanceof Date) {
- DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
- value = "‘" + formatter.format(new Date()) + "‘";
- } else {
- if (obj != null) {
- value = obj.toString();
- } else {
- value = "";
- }
- }
- return value;
- }
- }
當系統併發達到一定數量級,log4j日誌列印本身會成為瓶頸,這個時候需要mq來解耦了,不在列印日誌,而是傳送mq訊息,由mq消費端處理。因為目前公司專案併發數量還不足以導致該問題,因此尚未採用。
elk收集日誌之後,通過kibana可以提供搜尋。
剩下最後的工作量就是提供一個web介面來更好的分析和展示資料。