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負擔
本文是爲了記錄有這麼回事,以便未來相關問題的排查不至於一頭霧水,同時也對沒能力查明事故根本原因表示遺憾,如果有人遇到了相關的問題,同時查詢了原因,跪求告知!提前感謝
參考資料
- 時間久遠,忘記記錄了