磁盘IO告警问题排查记录-MyBatis日志源码分析

背景

业务凌晨收到磁盘IO告警,每秒内IO操作时长平均占用比例大于90%

分析

查看磁盘IO监控指标

发现有一个操作,该操作导致大量的磁盘读写IO。

cp catalina.out catalina.out-2021-12-18

了解背后的原因发现,业务日志大量输出到了tomcat的catalina.out中,期间有收到磁盘满的告警,因此才做了这样的一个日志轮转:先拷贝一份,然后旧的日志清空。

分析结果:业务日志写入tomcat catalina.out中,导致磁盘占满,所以做了日志轮转策略,导致凌晨IO操作频繁。

为什么业务日志会输出到tomcat catalina.out中

1、首先了解什么样的日志会输出到catalina.out中?

catalina.out主要记录运行的一些信息,尤其是一些异常错误日志信息。

  • tomcat运行日志
  • catalina.out其实是tomcat的标准输出(stdout)和标准出错(stderr)
  • 在tomcat的启动脚本里指定,如果没有修改的话stdout和stderr会重定向到这里。所以我们在应用里使用System.out、System.err打印的日志都会输出到这里。
  • 如果我们在应用里使用其他的日志框架,配置了向Console输出的,则也会在这里出现。以logback为例,如果配置ch.qos.logback.core.ConsoleAppender则会输出到catalina.out里。

2、查看线上的log4j配置文件,发现并没有配置org.apache.log4j.ConsoleAppender,只有测试环境配置了

3、询问运维,目前预发都是什么样的业务日志,发现都是SQL打印日志,询问同事发现他们的版本中有日志打印开关,但是预发线上均已经关闭,但是运维依然可以看到SQL日志还在打印。查看MyBatis的SQL日志配置,发现MyBatis配置开启了SQL日志打印。xml配置文件中配置了如下内容:

<setting name="logImpl" value="org.apache.ibatis.logging.stdout.StdOutImpl" />

4、查看StdOutImpl源码,发现方法中均有标准输出以及标准输错,去掉后问题得以解决。

public class StdOutImpl implements Log {
 
  public StdOutImpl(String clazz) {
    // Do Nothing
  }
 
  @Override
  public boolean isDebugEnabled() {
    return true;
  }
 
  @Override
  public boolean isTraceEnabled() {
    return true;
  }
 
  @Override
  public void error(String s, Throwable e) {
    System.err.println(s);
    e.printStackTrace(System.err);
  }
 
  @Override
  public void error(String s) {
    System.err.println(s);
  }
 
  @Override
  public void debug(String s) {
    System.out.println(s);
  }
 
  @Override
  public void trace(String s) {
    System.out.println(s);
  }
 
  @Override
  public void warn(String s) {
    System.out.println(s);
  }
}

Mybatis 日志源码分析

日志加载部分

Log接口

Log接口比较简单,源码如下:

public interface Log {
  // 是否启用debug
  boolean isDebugEnabled();
  // 是否启用debug
  boolean isTraceEnabled();
  // 错误日志级别输出方法
  void error(String s, Throwable e);
  //重载错误日志级别输出方法
  void error(String s);
  //debug日志级别输出方法
  void debug(String s);
  //trace日志级别输出方法
  void trace(String s);
  //warn日志级别输出方法
  void warn(String s);
}

关于Log的实现类较多,参考org.apache.ibatis.logging包下的实现类,如Jdk14LoggingImpl、Log4jImpl、Log4j2Impl、Slf4jImpl、StdOutImpl等,Mybatis实现了当前很多日志框架的适配。

日志组件加载

用户未在xml中设置logImpl属性时,默认加载方式为LogFactory的static静态代码块,默认优先加载slf4j。如果设置了logImpl,则使用用户自定义的log组件。

public final class LogFactory {
 
  /**
   * Marker to be used by logging implementations that support markers
   */
  public static final String MARKER = "MYBATIS";
 
  private static Constructor<? extends Log> logConstructor;
  // 类加载时尝试设置使用的日志组件,依次按序加载,slf4j默认为第一个;第一个加载成功后,logConstructor 设置对应的值;此种加载方式生效的场景为用户未在xml中设置logImpl属性
  static {
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useSlf4jLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useCommonsLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useLog4J2Logging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useLog4JLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useJdkLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useNoLogging();
      }
    });
  }
 
  private LogFactory() {
    // disable construction
  }
 
  public static Log getLog(Class<?> aClass) {
    return getLog(aClass.getName());
  }
 
  public static Log getLog(String logger) {
    try {
      return logConstructor.newInstance(logger);
    } catch (Throwable t) {
      throw new LogException("Error creating logger for logger " + logger + ".  Cause: " + t, t);
    }
  }
  // 用户设置了logImpl之后,会调用该方法覆盖默认加载的日志组件,logConstructor会替换掉
  public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
    setImplementation(clazz);
  }
 
  public static synchronized void useSlf4jLogging() {
    setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
  }
 
  public static synchronized void useCommonsLogging() {
    setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
  }
 
  public static synchronized void useLog4JLogging() {
    setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
  }
 
  public static synchronized void useLog4J2Logging() {
    setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
  }
 
  public static synchronized void useJdkLogging() {
    setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
  }
 
  public static synchronized void useStdOutLogging() {
    setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
  }
 
  public static synchronized void useNoLogging() {
    setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
  }
  // 加载时会进行logConstructor是否为空的判断,为空才加载,有值不加载
  private static void tryImplementation(Runnable runnable) {
    if (logConstructor == null) {
      try {
        runnable.run();
      } catch (Throwable t) {
        // ignore
      }
    }
  }
  //根据传入的实现类,获取对应的构造函数,反射获取实例,重新设置logConstructor值
  private static void setImplementation(Class<? extends Log> implClass) {
    try {
      Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
      Log log = candidate.newInstance(LogFactory.class.getName());
      if (log.isDebugEnabled()) {
        log.debug("Logging initialized using '" + implClass + "' adapter.");
      }
      logConstructor = candidate;
    } catch (Throwable t) {
      throw new LogException("Error setting Log implementation.  Cause: " + t, t);
    }
  }
 
}

Mybatis配置加载时会调用org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.Reader, java.lang.String, java.util.Properties)方法,该方法中使用XMLConfigBuilder进行配置的加载及解析。

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      // 配置加载
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      // 配置解析
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        reader.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }
  //配置解析
  private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
     // 解析settings属性
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      // 加载settings中的配置项
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
 
  private void settingsElement(Properties props) throws Exception {
    // 省略其他加载操作 直接看加载logImpl操作,首先根据logImpl属性对应的value值反射生成Class实例,设置logImpl属性值,
    @SuppressWarnings("unchecked")
    Class<? extends Log> logImpl = (Class<? extends Log>)resolveClass(props.getProperty("logImpl"));
    configuration.setLogImpl(logImpl);
  }
  //设置logImpl属性值
  public void setLogImpl(Class<? extends Log> logImpl) {
    if (logImpl != null) {
      this.logImpl = logImpl;
      // 设置用户自定义的日志组件,参考上文提到的useCustomLogging方法,将logConstructor值替换掉
      LogFactory.useCustomLogging(this.logImpl);
    }
  }

因此在xml中配置logImpl属性会干预默认的日志组件。

日志打印部分

MyBatis在构建MappedStatement时会调用LogFactory.getLog(logId)方法,由于上文中已经将日志组件替换为StdOutImpl,因此此时mappedStatement.statementLog = StdOutImpl。

public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
      mappedStatement.configuration = configuration;
      mappedStatement.id = id;
      mappedStatement.sqlSource = sqlSource;
      mappedStatement.statementType = StatementType.PREPARED;
      mappedStatement.parameterMap = new ParameterMap.Builder(configuration, "defaultParameterMap", null, new ArrayList<ParameterMapping>()).build();
      mappedStatement.resultMaps = new ArrayList<ResultMap>();
      mappedStatement.sqlCommandType = sqlCommandType;
      mappedStatement.keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
      String logId = id;
      if (configuration.getLogPrefix() != null) {
        logId = configuration.getLogPrefix() + id;
      }
      // 调用LogFactory的getLog获取实际的日志组件
      mappedStatement.statementLog = LogFactory.getLog(logId);
      mappedStatement.lang = configuration.getDefaultScriptingLanguageInstance();
    }
  // 上文中已经将logConstructor替换为StdOutImpl的构造方法,因此该方法返回StdOutImpl的日志组件
  public static Log getLog(String logger) {
    try {
      return logConstructor.newInstance(logger);
    } catch (Throwable t) {
      throw new LogException("Error creating logger for logger " + logger + ".  Cause: " + t, t);
    }
  }

Mybatis的日志打印基类为BaseJdbcLogger,实现子类有四个:ConnectionLogger、PreparedStatementLogger、ResultSetLogger、StatementLogger。

已获取连接为例,会返回一个代理对象,在执行对象代理方法时,会进行日志打印。

// 传入的statementLog为StdOutImpl,发现isDebugEnabled为true,则返回一个ConnectionLogger
protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      return connection;
    }
  }
  //利用jdk动态代理生成Connection代理类,new ConnectionLogger会初始化statementLog属性为StdOutImpl
  public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
    InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
    ClassLoader cl = Connection.class.getClassLoader();
    return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
  }
 
public Object invoke(Object proxy, Method method, Object[] params)
      throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }   
      if ("prepareStatement".equals(method.getName())) {
        // 条件为true,进入debug方法
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
        }       
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else if ("prepareCall".equals(method.getName())) {
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
        }       
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else if ("createStatement".equals(method.getName())) {
        Statement stmt = (Statement) method.invoke(connection, params);
        stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else {
        return method.invoke(connection, params);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }
  //new ConnectionLogger已经将statementLog设置为StdOutImpl,因此实际调用的StdOutImpl的debug方法
  protected void debug(String text, boolean input) {
    if (statementLog.isDebugEnabled()) {
      statementLog.debug(prefix(input) + text);
    }
  }
  //StdOutImpl的debug方法
  public void debug(String s) {
    System.out.println(s);
  }

其他的类似,此时整条链路就串起来了,就是通过config.xml文件中configuration → settings → setting中的logImpl,更改MyBatis的日志组件,进而在sql执行时进行isDebugEnabled方法判断,调用debug方法进行打印。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

执笔晓浮沉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值