Sentry 開發者貢獻指南 - 資料庫遷移

為少發表於2022-01-18

Django 遷移是我們處理 Sentry 中資料庫更改的方式。

Django 遷移官方文件:https://docs.djangoproject.com/en/2.2/topics/migrations/

這些將涵蓋了解遷移正在執行的操作所需的大部分內容。

命令

請注意,對於所有這些命令,如果在 getsentry 儲存庫中,您可以將 getsentry 替換為 sentry

將您的資料庫升級到最新

sentry upgrade 會自動更新你的遷移。您也可以執行 sentry django migrate 來直接訪問遷移命令。

將您的資料庫移動到特定的遷移

當您要測試遷移時,這會很有幫助。

sentry django migrate <app_name> <migration_name> - 請注意,migration_name 可以是部分匹配,通常數字就是你所需要的。

例如:sentry django migrate sentry 0005

這也可用於回滾遷移。如果你犯了錯誤,在開發中很有用。

為遷移生成 SQL

這對審查您的程式碼的人很有幫助,因為並不總是清楚 Django 遷移實際要做什麼。

sentry django sqlmigrate <app_name> <migration_name>

例如 sentry django sqlmigrate sentry 0003

生成遷移

這會根據您對模型所做的更改自動為您生成遷移。

sentry django makemigrations

或者

sentry django makemigrations <app_name> 用於一個指定的 app

例如 sentry django makemigrations sentry

當您在 pr 中包含遷移時,還要為遷移生成 sql 並將其作為註釋包含在內,以便您的審閱者可以更輕鬆地瞭解 Django 正在做什麼。

您還可以使用 sentry django makemigrations <app_name> --empty 生成空遷移。這對於資料遷移和其他自定義工作很有用。

將遷移合併到 master

合併到 master 時,您可能會注意到與 migrations_lockfile.txt 的衝突。
這個檔案是為了幫助我們避免將具有相同遷移編號的兩個遷移合併到 master,如果您與它發生衝突,那麼很可能有人在您之前提交了遷移。

指南

在執行遷移時,我們需要注意一些事項。

過濾器

如果(資料)遷移涉及大表或未索引的列,最好迭代整個表而不是使用 filter。 例如:

EnvironmentProject.objects.filter(environment__name="none")

因為 EnvironmentProject 行太多,這會一次將太多行帶入記憶體。
相反,我們應該使用 RangeQuerySetWrapperWithProgressBar 遍歷所有 EnvironmentProject 行,因為它會分塊進行。
例如:

for env in RangeQuerySetWrapperWithProgressBar(EnvironmentProject.objects.all()):
	if env.name == 'none':
		# Do what you need

我們通常更喜歡避免將 .filterRangeQuerySetWrapperWithProgressBar 一起使用。
由於它已經通過 id 對錶進行排序,因此我們無法利用欄位上的任何索引,並且可能會為每個塊掃描大量行。
這會執行得更慢,但我們通常更喜歡這樣,因為它在更長的時間內平均負載,並使每個查詢獲取每個塊的成本相當低。

索引

我們更喜歡使用 CREATE INDEX CONCURRENTLY 在現有的大型表上建立索引。當我們這樣做時,我們無法在事務中執行遷移,因此使用 atomic = False 來執行這些很重要。

刪除列/表

由於我們的部署過程,這很複雜。
當我們部署時,我們執行遷移,然後推出應用程式程式碼,這需要一段時間。
這意味著如果我們只是刪除一個列或模型,那麼 sentry 中的程式碼將查詢這些列/表並在部署完成之前出錯。
在某些情況下,這可能意味著 Sentry 在部署完成之前很難停機。

為避免這種情況,請執行以下步驟:

  • 如果列不是空的,則將其標記為空,並建立一個遷移。
  • 部署。
  • 從模型中刪除列,但在遷移中確保我們只將狀態標記為已刪除(removed)。
  • 部署。
  • 最後,建立一個刪除列的遷移。

這是刪除已經可以為空的列的示例。首先我們從模型中刪除列,然後修改遷移以僅更新狀態而不進行資料庫操作。

operations = [
        migrations.SeparateDatabaseAndState(
            database_operations=[],
            state_operations=[
                migrations.RemoveField(model_name="alertrule", name="alert_threshold"),
                migrations.RemoveField(model_name="alertrule", name="resolve_threshold"),
                migrations.RemoveField(model_name="alertrule", name="threshold_type"),
            ],
        )
    ]

一旦部署完成,我們就可以部署實際的列刪除。這個 pr 只會有一個遷移,因為 Django 不再知道這些欄位。請注意,反向 SQL 僅適用於開發人員,因此可以不分配預設值或進行任何型別的回填:

operations = [
        migrations.SeparateDatabaseAndState(
            database_operations=[
                migrations.RunSQL(
                    """
                    ALTER TABLE "sentry_alertrule" DROP COLUMN "alert_threshold";
                    ALTER TABLE "sentry_alertrule" DROP COLUMN "resolve_threshold";
                    ALTER TABLE "sentry_alertrule" DROP COLUMN "threshold_type";
                    """,
                    reverse_sql="""
                    ALTER TABLE "sentry_alertrule" ADD COLUMN "alert_threshold" smallint NULL;
                    ALTER TABLE "sentry_alertrule" ADD COLUMN "resolve_threshold" int NULL;
                    ALTER TABLE "sentry_alertrule" ADD COLUMN "threshold_type" int NULL;

                    """,
                )
            ],
            state_operations=[],
        )
    ]

如果該表在其他表中被引用為外來鍵,則需要格外小心。在這種情況下,首先刪除其他表中的外來鍵列,然後返回到此步驟。

  • 通過在列上設定 db_constraint=False,刪除此表到其他表的任何資料庫級外來鍵約束。
  • 部署
  • sentry 程式碼庫中刪除模型和所有引用。確保遷移僅將狀態標記為已刪除。
  • 部署。
  • 建立一個刪除表的遷移。
  • 部署

這是刪除此模型的示例:

class AlertRuleTriggerAction(Model):
    alert_rule_trigger = FlexibleForeignKey("sentry.AlertRuleTrigger")
    integration = FlexibleForeignKey("sentry.Integration", null=True)
    type = models.SmallIntegerField()
    target_type = models.SmallIntegerField()
    # Identifier used to perform the action on a given target
    target_identifier = models.TextField(null=True)
    # Human readable name to display in the UI
    target_display = models.TextField(null=True)
    date_added = models.DateTimeField(default=timezone.now)

    class Meta:
        app_label = "sentry"
        db_table = "sentry_alertruletriggeraction"

首先,我們檢查了它沒有被任何其他模型引用,它沒有。接下來,我們需要刪除和 db 級外來鍵約束。為此,我們改變這兩列並生成一個遷移:

alert_rule_trigger = FlexibleForeignKey("sentry.AlertRuleTrigger", db_constraint=False)
integration = FlexibleForeignKey("sentry.Integration", null=True, db_constraint=False)

遷移中的操作看起來像

    operations = [
        migrations.AlterField(
            model_name='alertruletriggeraction',
            name='alert_rule_trigger',
            field=sentry.db.models.fields.foreignkey.FlexibleForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, to='sentry.AlertRuleTrigger'),
        ),
        migrations.AlterField(
            model_name='alertruletriggeraction',
            name='integration',
            field=sentry.db.models.fields.foreignkey.FlexibleForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='sentry.Integration'),
        ),
    ]

我們可以看到它生成的 sql 只是刪除了 FK 約束

BEGIN;
SET CONSTRAINTS "a875987ae7debe6be88869cb2eebcdc5" IMMEDIATE; ALTER TABLE "sentry_alertruletriggeraction" DROP CONSTRAINT "a875987ae7debe6be88869cb2eebcdc5";
SET CONSTRAINTS "sentry_integration_id_14286d876e86361c_fk_sentry_integration_id" IMMEDIATE; ALTER TABLE "sentry_alertruletriggeraction" DROP CONSTRAINT "sentry_integration_id_14286d876e86361c_fk_sentry_integration_id";
COMMIT;

所以現在我們部署它並進入下一階段。

下一階段涉及從程式碼庫中刪除對模型的所有引用。所以我們這樣做,然後我們生成一個遷移,從遷移狀態中刪除模型,而不是資料庫。此遷移中的操作如下所示

operations = [
        migrations.SeparateDatabaseAndState(
            state_operations=[migrations.DeleteModel(name="AlertRuleTriggerAction")],
            database_operations=[],
        )
    ]

並且生成的 SQL 顯示沒有發生資料庫更改。所以現在我們部署它並進入最後一步。

在這最後一步中,我們只想手動編寫 DDL 來刪除表。 所以我們使用 sentry django makemigrations --empty 來產生一個空的遷移,然後修改操作如下:

operations = [
        migrations.RunSQL(
            """
            DROP TABLE "sentry_alertruletriggeraction";
            """,
            reverse_sql="CREATE TABLE sentry_alertruletriggeraction (fake_col int)", # We just create a fake table here so that the DROP will work if we roll back the migration.
        )
    ]

然後我們部署它,我們就完成了。

外來鍵

建立外來鍵大多沒問題,但是對於像 ProjectGroup 這樣的大/繁忙的表,由於獲取鎖的困難,它可能會導致問題。
您仍然可以建立 Django 級別的外來鍵,而無需建立資料庫約束。為此,請在定義鍵時設定 db_constraint=False

重新命名錶

重新命名錶很危險,會導致停機。發生這種情況的原因是在部署期間將執行舊/新程式碼的混合。
因此,一旦我們在 Postgres 中重新命名該表,如果舊程式碼嘗試訪問它,它就會立即開始出錯。有兩種方法可以處理重新命名錶:

  • 不要在 Postgres 中重新命名錶。相反,只需在 Django 中重新命名模型,並確保將 Meta.db_table 設定為當前表名,這樣不會有任何中斷。這是首選方法。
  • 如果你真的想重新命名錶,那麼步驟將是:
  • 使用新名稱建立一個表
  • 開始對舊錶和新表進行雙重寫入,最好是在事務中。
  • 將舊行回填到新表中。
  • model 更改為從新表開始讀取。
  • 停止寫入舊錶並從程式碼中刪除引用。
  • 丟棄舊錶。
  • 一般來說,這是不值得做的,與回報相比,這需要冒很多風險/付出很多努力。

新增列

建立新列時,它們應始終建立為可為空的。這是出於兩個原因:

  • 如果存在現有行,新增非空列需要設定預設值,新增預設值需要完全重寫表。這是危險的,很可能會導致停機
  • 在部署期間,新舊程式碼混合執行。如果舊程式碼嘗試向表中插入一行,則插入將失敗,因為舊程式碼不知道新列存在,因此無法為該列提供值。

向列新增 NOT NULL

not null 新增到列可能很危險,即使該列的表的每一行都有資料。
這是因為 Postgres 仍然需要對所有行執行非空檢查,然後才能新增約束。
在小表上這可能沒問題,因為檢查會很快,但在大表上這可能會導致停機。
這裡有幾個選項可以確保安全:

  • ALTER TABLE tbl ADD CONSTRAINT cnstr CHECK (col IS NOT NULL) NOT VALID; ALTER TABLE tbl VALIDATE CONSTRAINT cnstr;. 首先,我們將約束建立為無效。然後我們之後驗證它。我們仍然需要掃描整個表來驗證,但我們只需要持有一個 SHARE UPDATE EXCLUSIVE 鎖,它只會阻止其他 ALTER TABLE 命令,但允許讀/寫繼續。這很有效,但會有 0.5-1% 的輕微效能損失。在 Postgres 12 之後,我們可以擴充套件這個方法來新增一個真正的 NOT NULL 約束。
  • 如果表足夠小並且體積足夠小,那麼建立一個普通的 NOT NULL 約束應該是安全的。小是幾百萬行或更少。

新增具有預設值的列

向現有表新增具有預設值的列是危險的。這需要 Postgres 鎖定表並重寫它。相反,更好的選擇是:

  • Postgres 中新增沒有預設值的列,但在 Django 中新增預設值。這使我們能夠確保所有新行都具有預設值。這是通過修改遷移檔案以包含 migrations.SeperateDatabaseAndState 來完成的
operations = [
    migrations.SeparateDatabaseAndState(
        database_operations=[
            migrations.AddField(
                model_name="mymodel",
                name="new_field",
                # Don't use a default in Postgres, a data migration can be used afterward to backfill
                field=models.PositiveSmallIntegerField(null=True),
            ),
        ],
        state_operations=[
            migrations.AddField(
                model_name="mymodel",
                name="new_field",
                # Use the default in Django, new rows will use the specified default
                field=models.PositiveSmallIntegerField(null=True, default=1),
            ),
        ],
    )
    ]
  • 通過資料遷移使用預設值回填預先存在的行。

改變列型別

改變列的型別通常是危險的,因為它需要重寫整個表。有一些例外:

  • varchar(<size>) 更改為更大尺寸的 varchar
  • 將任何 varchar 更改為 text
  • numeric 更改為 numeric,其中 precision 更高但 scale 相同。

對於任何其他型別,最好的前進路徑通常是:

  • 建立具有新型別的列。
  • 開始對新舊列進行雙重寫入。
  • 回填並將舊列值轉換為新列。
  • 更改程式碼以使用新欄位。
  • 停止寫入舊列並從程式碼中刪除引用。
  • 從資料庫中刪除舊列。

通常,這值得在 #discuss-backend 中討論。

重新命名列

重新命名列是危險的,會導致停機。
發生這種情況的原因是在部署期間將執行舊/新程式碼的混合。
因此,一旦我們在 Postgres 中重新命名該列,如果舊程式碼嘗試訪問它,它就會立即開始出錯。有兩種方法可以處理重新命名列:

  • 不要重新命名 Postgres 中的列。相反,只需在 Django 中重新命名欄位,並在定義中使用 db_column 將其設定為現有的列名,這樣就不會中斷。這是首選方法。
  • 如果你真的想重新命名列,那麼步驟將是:
    • 建立具有新名稱的列
    • 開始對新舊列進行雙重寫入。
    • 將舊列值回填到新列中。
    • 將欄位更改為從新列開始讀取。
    • 停止寫入舊列並從程式碼中刪除引用。
    • 從資料庫中刪除舊列。
    • 一般來說,這是不值得做的,與回報相比,這需要冒很多風險/付出很多努力。

更多

相關文章