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负担
本文是为了记录有这么回事,以便未来相关问题的排查不至于一头雾水,同时也对没能力查明事故根本原因表示遗憾,如果有人遇到了相关的问题,同时查询了原因,跪求告知!提前感谢
参考资料
- 时间久远,忘记记录了