【Spring错误笔记】spring.jpa.hibernate.ddl-auto=update造成删除索引的线上事故

spring.jpa.hibernate.ddl-auto=update造成删除索引的线上事故


  • 事故背景
    • 技术习惯
    • 业务背景
    • 事故回放
  • 事故起因
    • 事故起因
    • 为什么Hibernate会执行删除索引再重建索引的操作?
  • 事故结论
    • 事故原因
    • 事故结论

事故背景


技术习惯

  • 公司技术习惯,无论是线上环境,还是预线上,测试环境,都习惯使用Spring Data Jpa作为ORM工具
  • 为了快速迭代,通常对于表的更新的DDL语句,都依赖JPA的自动更新机制,包括线上环境,即使用spring.jpa.hibernate.ddl-auto=update配置

业务背景

  • 重构项目,需要迁移数据,进行合库
  • 重构项目的部分接口 QPS 峰值为5k级别 ,日查询流量上亿
  • 重构项目涉及数据库,多个单表数据达百万,部分单表数据为千万级
  • 区别于旧服务,新建一个新的服务,迁移接口,沿用旧数据库

事故回放

第一波

  • 重构服务,新服务准备0流量上线,线上环境配置为spring.jpa.hibernate.ddl-auto=update,数据库实体映射关系未修改
  • 服务上线之后,旧服务高QPS接口出现抖动,接口可用率下滑,延迟攀升,出现服务告警。
  • 简单查询后,发现是数据库使用率超百分百, 正常情况下CPU使用率为百分之10左右
  • 相关人员排查问题,没有发现具体原因,后续没有发现原因,一段时间后,服务可用率自行恢复
  • 当时没有想到跟ddl-auto=update有关系, 因为实体映射关系没有改动,且新服务没有流量打入,理论上不会有什么关系,所以并没有跟新服务上线联想在一起

第二波

  • 一段时间后,新服务修改代码后重新上线,服务再次告警, 数据库使用率再次百分百, 旧服务线上接口接近0可用率
  • 查询MySQL processlist , 发现在千万级的表上有drop/creator index的DDL语句操作
  • 因为可用率已为0, 慌乱之中,只好kill掉processlist队列中阻塞的操作 (其中包括一个creator index操作 , 当时并没在意)
  • 然而数据库使用率依然跑满,processlist充满了大量的select操作。排查一段时间后,怀疑有人drop了索引,导致千万数据的表的查询语句阻塞在队列中
  • 宕机修复,造成线上服务停机十来分钟

事故解决

  • 停止线上服务读写请求
  • 删除唯一失效期间,插入的重复脏数据
  • 重新生成索引
  • 修改auto-ddl策略为none
  • 重新启动线上服务,可用率恢复

事故起因


初步原因

初步原因排查结果

  • 经过事故排查,是有人drop掉了某张千万级数据的表索引, 而造成了服务可用率下降,延迟攀升。同时在之后的修复过程中,因为kill掉了数据库create索引的操作,造成索引没有恢复,大量读请求没有走索引查询,造成队列阻塞
  • 经过事故定位,发现drop索引的操作是由新服务发出了, 服务启动时,Hibernate执行了drop索引再create索引两条DDL操作。同时新服务是集群部署,会执行多次ddl-auto操作, 重复的大表DDL操作,加重了阻塞程度
  • 在Hibernate执行drop索引和create索引的间隙,旧服务还有大量的写请求进入相关数据表,在该unique索引失效期间,插入了重复数据,也导致了后续create表语句失效。unique索引再建失败

综上,初步原因是

  • 重复大表DDL语句阻塞SQL队列,同时索引失效,大量读请求透过缓存层打到数据库,加重了SQL队列的阻塞

为什么Hibernate会执行删除索引再重建索引的操作?

(一) 为什么Hibernate为删除索引 ?
最有可能相关的配置就是spring.jpa.hibernate.ddl-auto=update , 但是按照理解,update配置不同于create, create-drop 等配置,是不会删除数据库已有的关系的。

  • none
    不配置
  • validate
    加载 Hibernate 时,验证创建数据库表结构
  • create
    每次加载 Hibernate,重新创建数据库表结构
  • create-drop
    加载 Hibernate 时创建,退出是删除表结构
  • update
    加载 Hibernate 时自动更新数据库结构

总之,我的简单理解如下

  • 实体没有,数据库有,不修改
  • 实体有,数据库没有,新增更新

总之我查阅了大量资料,也没有update配置删除索引的情况 (反正我是没查到,包括外网,如果有相关资料请告诉我,同时对自己的检索能力进行检讨),只有漫天的劝告,最好不要在线上环境使用spring.jpa.hibernate.ddl-auto配置

(二) 可能的Hibernate删除索引的原因 ?

  • 服务上线时的配置写错了,并不是update
  • update配置没有生效, 并使用其他的模式,比如create等
  • 因为xxx异常导致执行了Herbinate其他底层潜在的逻辑, 或者说是触发了Herbinate的bug

原因嘛,我们要一个一个排查, 首先第一个,我经过重复检查,发现并不是这个问题。那看看是不是update配置没有生效或者被其他配置覆盖了。

所以我看了下HibernateProperties#ddlAuto这段代码


	/**
	 * DDL mode. This is actually a shortcut for the "hibernate.hbm2ddl.auto" property.
	 * Defaults to "create-drop" when using an embedded database and no schema manager was
	 * detected. Otherwise, defaults to "none".
	 */
	private String ddlAuto;

但是还是没有问题呀,我并没有使用一个嵌入式的数据库,所以并不会使用create-drop模式。同时默认情况下的配置应该是none。 同时也看了下其他的代码,好像也没有什么会覆盖配置的地方。

(三) 表面罪魁祸首出现 ?

前面两个原因不是,网上又没有找到更多的资料,所以只能决定看源码分析一下了,Debug, Debug, Debug, 哎,我太难了。

经过多个Debug, 我们先来看看一个线索, UniqueConstraintSchemaUpdateStrategy

/**
 * Unique columns and unique keys both use unique constraints in most dialects.
 * SchemaUpdate needs to create these constraints, but DB's
 * support for finding existing constraints is extremely inconsistent. Further,
 * non-explicitly-named unique constraints use randomly generated characters.
 * 
 * @author Brett Meyer
 */
public enum UniqueConstraintSchemaUpdateStrategy {
	
	/**
	 * Attempt to drop, then (re-)create each unique constraint.  Ignore any
	 * exceptions thrown.  Note that this will require unique keys/constraints
	 * to be explicitly named.  If Hibernate generates the names (randomly),
	 * the drop will not work.
	 * 
	 * DEFAULT
	 */
	DROP_RECREATE_QUIETLY,
	
	/**
	 * Attempt to (re-)create unique constraints, ignoring exceptions thrown
	 * (e.g., if the constraint already existed)
	 */
	RECREATE_QUIETLY,

	/**
	 * Do not attempt to create unique constraints on a schema update
	 */
	SKIP;

	private static final Logger log = Logger.getLogger( UniqueConstraintSchemaUpdateStrategy.class );

	public static UniqueConstraintSchemaUpdateStrategy byName(String name) {
		return valueOf( name.toUpperCase(Locale.ROOT) );
	}

	public static UniqueConstraintSchemaUpdateStrategy interpret(Object setting) {
		log.tracef( "Interpreting UniqueConstraintSchemaUpdateStrategy from setting : %s", setting );

		if ( setting == null ) {
			// default
			return DROP_RECREATE_QUIETLY;
		}

		if ( UniqueConstraintSchemaUpdateStrategy.class.isInstance( setting ) ) {
			return (UniqueConstraintSchemaUpdateStrategy) setting;
		}

		try {
			final UniqueConstraintSchemaUpdateStrategy byName = byName( setting.toString() );
			if ( byName != null ) {
				return byName;
			}
		}
		catch ( Exception ignore ) {
		}

		log.debugf( "Unable to interpret given setting [%s] as UniqueConstraintSchemaUpdateStrategy", setting );

		// default
		return DROP_RECREATE_QUIETLY;
	}
}

重点放在DROP_RECREATE_QUIETLY属性上,按照该策略的描述,它会尝试drop掉唯一索引,然后再重建索引,完美符合事务现场的骚操作,并且该策略还是默认策略。

行,找到一点苗头了,我们在继续瞧瞧,看看使用到该策略的地方, 我们看到AbstractSchemaMigrator#applyUniqueKeys这段代码

protected void applyUniqueKeys(
			Table table,
			TableInformation tableInfo,
			Dialect dialect,
			Metadata metadata,
			Formatter formatter,
			ExecutionOptions options,
			GenerationTarget... targets) {
		if ( uniqueConstraintStrategy == null ) {
			uniqueConstraintStrategy = determineUniqueConstraintSchemaUpdateStrategy( metadata );
		}
			
		// 如果不是SKIP策略,则进入判断
		if ( uniqueConstraintStrategy != UniqueConstraintSchemaUpdateStrategy.SKIP ) {
			final Exporter<Constraint> exporter = dialect.getUniqueKeyExporter();

			final Iterator ukItr = table.getUniqueKeyIterator();
			while ( ukItr.hasNext() ) {
				final UniqueKey uniqueKey = (UniqueKey) ukItr.next();
				// Skip if index already exists. Most of the time, this
				// won't work since most Dialects use Constraints. However,
				// keep it for the few that do use Indexes.
				IndexInformation indexInfo = null;
				if ( tableInfo != null && StringHelper.isNotEmpty( uniqueKey.getName() ) ) {
					indexInfo = tableInfo.getIndex( Identifier.toIdentifier( uniqueKey.getName() ) );
				}
				// 如果没有indexInfo信息,且uniqueConstraintStrategy策略为DROP_RECREATE_QUIETLY策略,就会执行Drop唯一索引操作
				if ( indexInfo == null ) {
					if ( uniqueConstraintStrategy == UniqueConstraintSchemaUpdateStrategy.DROP_RECREATE_QUIETLY ) {
						applySqlStrings(
								true,
								exporter.getSqlDropStrings( uniqueKey, metadata ),
								formatter,
								options,
								targets
						);
					}
					// 生成唯一索引操作
					applySqlStrings(
							true,
							exporter.getSqlCreateStrings( uniqueKey, metadata ),
							formatter,
							options,
							targets
					);
				}
			}
		}
	}

原本打死我也不会相信Hibernate会执行drop索引这种xx操作的我, 看了Hibernate的代码之后,也不得不相信,简直亮瞎了我的钛合金狗眼

经过上面的代码,我们可以知道了,当Hibernate使用了默认的DROP_RECREATE_QUIETLY策略, 并在没有获得唯一索引indexInfo时,就会出现先Drop再Create的场景。 至于为什么会没有获的正确的indexInfo呢? 可能是Hibernate在启动时,没有正确的获取数据库的元信息,因为部分信息的缺失,到导致执行Drop索引的语句。

所以我们知道了是表元信息的缺失导致了这个问题,所以我们继续向上排查。得知在新服务上线的时候,出现过数据库连接不稳定的情况。

The last packet successfully received from the server was xxx milliseconds ago

原因是application.yml的数据配置使用了SSL连接, 默认8.0.15的mysql-java-connector的useSSL配置为true。这的确又涉及到了另外一个问题。

在修改为useSSL=false之后,数据库连接不稳定的情况消失,同时也没有出现drop索引的情况, 难道是数据库连接不稳定导致的Hibernate加载时,没有正确的获取到完整的数据库元信息,导致执行了某种不该走的策略??

事故结论


事故原因

  • 事故排查,因为时间原因,最后虎头蛇尾。大致推断是由于数据库连接不稳定,Hibernate加载时,获取了部分信息缺失的table元信息。在执行唯一索引操作的时候,走了Drop索引的操作。
  • 由于想要知道真正的事故原因,需要花大量的时间去重现Bug, 不断Debug和调试源码,了解Hibernate底层执行逻辑。所以最终追查事故根本原因的计划告吹

事故结论

  • 虽然根本原因和逻辑没有查明,但是也不是完全没有收获,Hibernate的确存在删除索引再重建索引的逻辑,并在一定的特殊情况下回触发。所以千万不要在大数据量的数据库上使用auto-ddl策略
  • 网上网吹的不要在线上使用auto-ddl策略得到了根本的验证,以后还是老实人工DDL
  • 同时启动集群实例,会造成多次执行ddl操作,会加重DDL负担

本文是为了记录有这么回事,以便未来相关问题的排查不至于一头雾水,同时也对没能力查明事故根本原因表示遗憾,如果有人遇到了相关的问题,同时查询了原因,跪求告知!提前感谢

参考资料


  • 时间久远,忘记记录了

你可能感兴趣的:(日常错误)