【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負擔

本文是爲了記錄有這麼回事,以便未來相關問題的排查不至於一頭霧水,同時也對沒能力查明事故根本原因表示遺憾,如果有人遇到了相關的問題,同時查詢了原因,跪求告知!提前感謝

參考資料


  • 時間久遠,忘記記錄了
發佈了103 篇原創文章 · 獲贊 340 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章