ActiveRecord w/ PostgreSQL: 零Downtime在大資料表增減index

服務運行中,無痛加index

袁浩 Harry Yuan
7 min readNov 18, 2020
如何在服務運作同時加入index?

Scenario

有時候我們開發的feature,需要經常針對某個欄位搜尋。你可能會想:「很常搜尋某個欄位,就要加Index啊!」

但今天如果我的資料表有一定的規模,而且Service必須常駐,他必須不間斷的運行(不會有維護時間)。我們如果直接加index,migrate下去時整個資料表會鎖住,因為:

Rails will run your migrations inside a transaction if your database supports it

CREATE INDEX CONCURRENTLY

當資料表被鎖住時,雖然資料是可讀的,但是所有寫入動作(update, insert, delete)都會被擋住無法執行。

而我們的服務在運行,如果我們貿然的加index下去,就很有可能因為資料表鎖住引發downtime。

幸好,PostgresQL在index的增減上提供CONCURRENTLY的方式。也就是在執行過程中不會把table鎖住,服務可以正常運作。

class AddLocaleIndexToUsers < ActiveRecord::Migration
def change
add_index :users, :locale, algorithm: :concurrently
end
end

這邊我們必須透過disable_ddl_transaction!,來指名說這個migration不要在transaction內執行:

class AddLocaleIndexToUsers < ActiveRecord::Migration
disable_ddl_transaction!
def change
add_index :users, :locale, algorithm: :concurrently
end
end

測試環境中執行結果如下:

root@8b07817f5aa4:/# bundle exec rails db:migrate
== 20201027083412 AddIndexToLocale: migrating =============
-- add_index(:users, :locale, {:algorithm=>:concurrently})
-> 0.0505s
== 20201027083412 AddIndexToLocale: migrated (0.0517s)============

Production環境下執行,服務一切正常:

bundle exec rails db:migrate== 20201027083412 AddIndexToLocale: migrating =============-- add_index(:users, :locale, {:algorithm=>:concurrently})(53448.5ms) CREATE INDEX CONCURRENTLY "index_users_on_locale" ON "users" ("locale")-> 53.4510s== 20201027083412 AddIndexToLocale: migrated (53.4511s) ============

完成!

我不想要這個index了

若你也希望用同樣的algorithm還原,如果是當下就要回復,直接 bundle exec rails db:rollback 即可,他會照原來的algorithm來還原(但要注意版本!官方在Rails 5.2這隻PR才修好)。

但是若隔了一陣子才想到要更改,就得另外寫migration。

DROP INDEX CONCURRENTLY

我們來嘗試寫migration還原,使用 remove_index的方法並加入algorithm: :concurrently

class RemoveIndexOnLocaleToUsers < ActiveRecord::Migration[6.0]     
disable_ddl_transaction!
def change
remove_index :users, {column: :locale, algorithm: :concurrently}
end
end

注意

移除index時,Rails 5.2以上才有以下功能:

  1. remove_index的option支援algorithm
  2. add_index後,rollback時採同樣algorithm

當你的專案Rails ≥ 5.2時,直接跑上面的migration會如期執行:

== 20201117030959 RemoveIndexOnLocaleToUsers: migrating ==========
-- remove_index(:users, {:column=>:locale, :algorithm=>:concurrently})
(10.0ms) DROP INDEX CONCURRENTLY "index_users_on_locale"
-> 0.0131s
== 20201117030959 RemoveIndexOnLocaleToUsers: migrated (0.0131s) ===

基本上到這裡已經完成了。

但若你的專案Rails < 5.2 (以4.2為例)執行後會發現

==  RemoveIndexOnLocaleToUsers: migrating ================
-- remove_index(:users, {:column=>:locale, :algorithm=>:concurrently})
(0.5ms) DROP INDEX "index_users_on_locale"
-> 0.0053s
== RemoveIndexOnLocaleToUsers: migrated (0.0060s) ========

哪尼?竟然沒有CONCURRENTLY!若當下是在Production就糟了。

所以現在我們知道了,remove_index在舊的版本並不能幫你註名 CONCURRENTLY

來自己寫

這時候我們可以直接用 execute 來執行SQL。打開schema.rb找到要移除的index(此處範例為 index_users_on_locale

class RemoveIndexOnLocaleToUsers < ActiveRecord::Migration[6.0]      
disable_ddl_transaction!
def change
execute "DROP INDEX CONCURRENTLY index_users_on_locale"
end
end

執行結果如下:

==  RemoveIndexOnLocaleToUsers: migrating ================
-- execute("DROP INDEX CONCURRENTLY index_users_on_locale")
(17.0ms) DROP INDEX CONCURRENTLY index_users_on_locale
-> 0.0189s
== RemoveIndexOnLocaleToUsers: migrated (0.0232s) ===============

完成。

最後提醒一下,若你的資料表很常寫入,沒事不要加太多的index,不然寫入會變慢。

分享到這邊,感謝閱讀。若有其他想法歡迎交流指正!

References

--

--

袁浩 Harry Yuan

Software Engineer | Ruby on Rails 喜歡學習前後端技術。希望文章白話到阿嬤都看得懂。