Zipkin原理學習--日誌追蹤 MySQL 執行語句

        目前Zipkin官方提供了插件用於支持對MySQL語句執行過程的日誌追蹤,提供了對MySQL5、MySQL6和MySQL8的支持,官方地址:https://github.com/openzipkin/brave/tree/master/instrumentation

一、介紹及示例

配置示例:

1、引入相關jar包:

<dependency>
    <groupId>io.zipkin.brave</groupId>
	<artifactId>brave-instrumentation-mysql</artifactId>
	<version>5.4.3</version>
</dependency>

2、在url中添加攔截器和服務名:statementInterceptors=brave.mysql.TracingStatementInterceptor&zipkinServiceName=myDatabaseService

public void mysql() throws Exception{
		Class.forName("com.mysql.jdbc.Driver");
		System.out.println("成功加載驅動");

		Connection connection = null;
		Statement statement = null;
		ResultSet resultSet = null;

		try {
			String url = "jdbc:mysql://localhost:3306/db?user=root&password=root&useUnicode=true&characterEncoding=UTF8&statementInterceptors=brave.mysql.TracingStatementInterceptor&zipkinServiceName=myDatabaseService";
			connection = DriverManager.getConnection(url);
			System.out.println("成功獲取連接");

			statement = connection.createStatement();
			String sql = "select * from tbl_user";
			resultSet = statement.executeQuery(sql);

			resultSet.beforeFirst();
			while (resultSet.next()) {
				System.out.println(resultSet.getString(1));
			}
			System.out.println("成功操作數據庫");
		} catch(Throwable t) {
			// TODO 處理異常
			t.printStackTrace();
		} finally {
			if (resultSet != null) {
				resultSet.close();
			}
			if (statement != null) {
				statement.close();
			}
			if (connection != null) {
				connection.close();
			}
			System.out.println("成功關閉資源");
		}

	}

3、zipserver中結果示例:

二、實現原理

        其實現原理也還是挺容易理解的,利用MySQL JDBC提供的攔截器機制,在sql語句執行前新建一個span調用,在sql語句執行後結束span調用,記錄整個調用過程的耗時及sql語句信息。

public class TracingStatementInterceptor implements StatementInterceptorV2 {

  /**
   * Uses {@link ThreadLocalSpan} as there's no attribute namespace shared between callbacks, but
   * all callbacks happen on the same thread.
   *
   * <p>Uses {@link ThreadLocalSpan#CURRENT_TRACER} and this interceptor initializes before
   * tracing.
   */
  @Override
  public ResultSetInternalMethods preProcess(String sql, Statement interceptedStatement,
      Connection connection) {
    // Gets the next span (and places it in scope) so code between here and postProcess can read it
	//新生成一個Span
    Span span = ThreadLocalSpan.CURRENT_TRACER.next();
    if (span == null || span.isNoop()) return null;

    // When running a prepared statement, sql will be null and we must fetch the sql from the statement itself
    if (interceptedStatement instanceof PreparedStatement) {
      sql = ((PreparedStatement) interceptedStatement).getPreparedSql();
    }
    int spaceIndex = sql.indexOf(' '); // Allow span names of single-word statements like COMMIT
    span.kind(Span.Kind.CLIENT).name(spaceIndex == -1 ? sql : sql.substring(0, spaceIndex));
    span.tag("sql.query", sql);
    parseServerIpAndPort(connection, span);
	//記錄啓動時間
    span.start();
    return null;
  }

  @Override
  public ResultSetInternalMethods postProcess(String sql, Statement interceptedStatement,
      ResultSetInternalMethods originalResultSet, Connection connection, int warningCount,
      boolean noIndexUsed, boolean noGoodIndexUsed, SQLException statementException) {
    Span span = ThreadLocalSpan.CURRENT_TRACER.remove();
    if (span == null || span.isNoop()) return null;

    if (statementException != null) {
      span.tag("error", Integer.toString(statementException.getErrorCode()));
    }
	//記錄服務停止時間
    span.finish();

    return null;
  }

  /**
   * MySQL exposes the host connecting to, but not the port. This attempts to get the port from the
   * JDBC URL. Ex. 5555 from {@code jdbc:mysql://localhost:5555/database}, or 3306 if absent.
   */
  static void parseServerIpAndPort(Connection connection, Span span) {
    try {
      URI url = URI.create(connection.getMetaData().getURL().substring(5)); // strip "jdbc:"
      String remoteServiceName = connection.getProperties().getProperty("zipkinServiceName");
      if (remoteServiceName == null || "".equals(remoteServiceName)) {
        String databaseName = connection.getCatalog();
        if (databaseName != null && !databaseName.isEmpty()) {
          remoteServiceName = "mysql-" + databaseName;
        } else {
          remoteServiceName = "mysql";
        }
      }
	  //添加服務名
      span.remoteServiceName(remoteServiceName);
      String host = connection.getHost();
      if (host != null) {
        span.remoteIpAndPort(host, url.getPort() == -1 ? 3306 : url.getPort());
      }
    } catch (Exception e) {
      // remote address is optional
    }
  }

  @Override public boolean executeTopLevelOnly() {
    return true; // True means that we don't get notified about queries that other interceptors issue
  }

  @Override public void init(Connection conn, Properties props) {
    // Don't care
  }

  @Override public void destroy() {
    // Don't care
  }
}

思考:其實Zipkin官方給出的這種方案還是能給我們一些啓發的,目前對於數據庫官方只支持了mysql,對於Postgresql、Oracle 和 SQL Server 等可以基於技術方案有以下兩種侷限解決方案:

(1)利用mybatis的攔截器機制來實現,和上面的實現類似

(2)利用數據庫池 Druid的過濾器同樣可以實現。

以上兩種方案的好處:對於數據庫通用支持。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章