ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PostHog 数据库 Schema 变更安全指南:迁移设计、锁风险与规模化实践

PostHog 数据库 Schema 变更安全指南:迁移设计、锁风险与规模化实践 PostHog 数据库 Schema 变更安全指南迁移设计、锁风险与规模化实践【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthogPostHog 的后端以 Django ORM 驱动 PostgreSQL同时用 ClickHouse 支撑事件类分析负载其数据库 Schema 与应用功能一样持续演进。然而每一次 Schema 变更都伴随风险一次设计糟糕的迁移可能锁住大表、拖垮热表上的所有请求或在滚动发布期间让新旧代码并行运行时出现列不存在级别的线上事故。本文以 PostHog 内部工程手册《Making schema changes safely》为骨架结合仓库中posthog/migration_helpers的迁移辅助工具、posthog/management/migration_analysis的迁移风险分析器以及 ClickHouse 事件表迁移实践系统梳理在 PostHog 这样的零停机滚动发布环境中安全做 Schema 变更的完整方法论。读完本文你将掌握变更前需要权衡的核心问题、为什么绝不能随意删除/重命名 Django 模型与字段、如何在不锁表的情况下给大表加索引与约束、如何分阶段下线列与表以及 ClickHouse Schema 变更相比 Postgres 的特殊复杂度。变更前的通用考量先问四个问题PostHog 的手册在动手前给出四组自检问题它们构成了任何 Schema 变更的第一道防线真的需要这个 Schema 变更吗很多需求用应用层代码变更就能解决完全没有必要动数据库。数据库变更成本与风险远超代码变更能省则省。变更是否向后兼容在 PostHog Cloud 与自托管部署中新旧两版代码必然并行运行滚动发布破坏性变更会直接引发故障。能否将 Schema 变更与代码变更分开部署对非平凡的变更应先单独部署 Schema 变更保证可回滚、向后兼容再部署依赖它的代码。是否在做阻塞式迁移任何会锁住大表的迁移都极易造成线上故障。总原则是凡是有点棘手的变更都要先想清楚它在生产环境中的操作方式——锁的持有时间、锁等待队列的后果、bin/migrate重试时的行为这些细节决定了迁移是否会从几分钟的 DDL演变成数小时的服务不可用。绝不删除或重命名 Django 模型与字段手册中反复强调的一条铁律删除和重命名表、列——即使是完全无人使用的——被强烈反对。原因在于 Django ORM 生成的SELECT查询总是显式列出要查询的表和列SELECT posthog_mymodel.id, posthog_mymodel.name, posthog_mymodel.myfield FROM ...当一次迁移移走了表/列而新服务器尚未完全部署完成时仍存活在线上、运行旧代码的服务器会继续SELECT那个已经不存在的资源唯一的回执就是数据库错误。这会带来一段虽短暂但影响巨大的用户痛苦期。规避方式不是干脆不做清理而是换一套策略名字不再合适数据库里保持原名不动Python/JS 代码中的命名随意改但禁止把改名反映到数据库字段不再需要在代码中用# DEPRECATED注释清晰标记让字段不再被查询通过在 Manager 对象的get_queryset中覆盖逻辑实现手册引用的示例是 PostHog 的 PR #13512。这套代码退役、列留库中的思路正是后面deprecate_field()与untrack_field()两个辅助工具的出发点。设计时考虑规模本地、自托管与云环境一视同仁迁移必须能在本地开发、自托管实例与 PostHog Cloud 三处顺畅运行。避免在事件events、用户persons、用户 distinct IDperson distinct IDs、日志logs等大表上逐行处理数据的迁移——它们要么耗时过长要么直接锁住整张表。这一点在仓库的 ClickHouse 事件表迁移文档clickhouse-event-table-migrations.md中有充分佐证2022 年初 PostHog 曾因重构 events 表 Schemaissue #5684在 Cloud 上做了持续数月、多次返工的迁移。那次实践总结出的四条目标同样适用于任何大规模数据迁移数据必须正确无重复、无丢失、必须及时完成、对摄入延迟影响最小、必须保留物化列。热表Hot Table上的 ALTER真正的风险是锁等待队列posthog_team、posthog_user、posthog_organization、posthog_project这四张表几乎在每个请求上都会被读取。任何ALTER TABLE——包括手册中那些安全模式如加一个可空列——都需要ACCESS EXCLUSIVE锁。危险的并不是 ALTER 本身可空的ADD COLUMN是纯元数据操作拿到锁后毫秒级完成而是锁等待队列当 ALTER 在在途查询后面排队时Postgres 会把之后到达的、针对该表的每一个查询都排在它后面。在热表上这意味着全站范围的请求堆积与 5xx直到lock_timeout取消 ALTER而bin/migrate会用指数退避重试于是这种停滞会一轮轮重复直到 ALTER 最终赢得锁竞争。这不是理论推演一个普普通通的可空AddField正是安全模式推荐的写法曾在生产中因反复输掉锁竞争引发约一小时的周期性 5xx 波动。安全方案尽量别动热表对于Team绝大多数新字段本就不该落在表上。领域特定字段应该放到Team 扩展模型上参见 posthog/models/team/README.md——那是一次CREATE TABLE完全不会对posthog_team加锁。CREATE INDEX CONCURRENTLY通过SafeAddIndexConcurrently也是安全的它只取SHARE UPDATE EXCLUSIVE锁不阻塞读写。确实要动热表显式承担风险仓库中的迁移风险分析器HotTableAlterPolicypolicies.py会在 CI 中拦截针对这些表的任何 DDL。若确实要接受风险确认该字段真的是核心字段团队身份、跨产品设置、SDK 配置而不是扩展模型的候选将app_label.migration_name追加到 hot_table_acknowledged_migrations.txt——这是明确的我接受风险动作在评审中可见与 #team-infrastructure 协调在低流量窗口部署。指向热表的外键引用侧同样会锁父表危险还会从引用侧引爆任何产品 app 中一个普通的CreateModel或AddField只要带ForeignKey(toposthog.team, ...)或解析为posthog_user的settings.AUTH_USER_MODEL创建外键约束时就会对被引用的父表取SHARE ROW EXCLUSIVE锁——即使子表是全新创建的。该锁与父表上每个INSERT/UPDATE/DELETE持有的ROW EXCLUSIVE锁冲突写流量下锁请求就会排在在途写入之后lock_timeout将其取消每次bin/migrate重试又重复一次停滞。HotTableAlterPolicy会标记 FK 目标解析为热表的CreateModel/AddField跳过声明了db_constraintFalse的 FK。两种解法方案 A——db_constraintFalse唯一真正无锁的路径不发射 FK 约束对父表零锁参照完整性仅靠应用代码保证# CreateModel / AddField 不发射任何 FK 约束对父表不加锁 team models.ForeignKey(posthog.Team, on_deletemodels.CASCADE, db_constraintFalse)方案 B——两阶段补真实数据库约束先用db_constraintFalse完成CreateModel/AddField不加锁随后在后续迁移中用AddForeignKeyNotValid补约束、再用ValidateForeignKey校验# 00xx_add_fk_not_valid.py对父表有短暂 SHARE ROW EXCLUSIVE 锁见下方说明 from posthog.migration_helpers import AddForeignKeyNotValid operations [ AddForeignKeyNotValid( model_namemymodel, namemymodel_team_id_fk, columnteam_id, to_tableposthog_team, to_columnid, ), ] # 00yy_validate_fk.py对子表取 SHARE UPDATE EXCLUSIVE不锁父表 from posthog.migration_helpers import ValidateForeignKey operations [ ValidateForeignKey(model_namemymodel, namemymodel_team_id_fk), ]需要诚实看待锁ADD CONSTRAINT ... NOT VALID为目录元数据添加仍会对父表取短暂的SHARE ROW EXCLUSIVE锁它跳过了行校验扫描把锁窗口压缩到仅元数据操作但并没有消除父表锁。真正无锁的只有方案 A。后续的VALIDATE CONSTRAINT在SHARE UPDATE EXCLUSIVE下扫描子表行不锁父表。若 FK 必须在添加时锁热表就按热表 DDL 的同样流程走追加到hot_table_acknowledged_migrations.txt并协调部署。删除表与列分阶段退役而非一次 DROP为什么直接删除危险DeleteModel会立即删表RemoveField会立即删列。它们破坏部署期间的向后兼容且无法回滚——数据一旦删除任何回滚部署都会因表/列不存在而失败数据丢失是永久性的。部署风险分析器将裸RemoveField评为 5 分最高风险。安全删表三步走第 1 步一个 PR 内移除模型与所有引用。删除所有引用该模型的代码导入、API 端点、视图、序列化器、查询/写入的业务逻辑、Celery/插件等后台任务、cron 任务从models.py删除模型类运行makemigrations生成DeleteModel再把它包进SeparateDatabaseAndState只改 Django 状态、不动数据库from posthog.migration_helpers import DropForeignKey class Migration(migrations.Migration): dependencies [] operations [ migrations.SeparateDatabaseAndState( state_operations[ migrations.DeleteModel(nameOldFeature), ], database_operations[ # 当表有指向热父表Team、User、Organization、Project的 FK 时必须做。 # Django 无法级联到它看不见的表子行会活过父表删除 # 约束是 DEFERRABLE INITIALLY DEFERRED父表删除完成级联后会在 COMMIT 时报错。 DropForeignKey(posthog_oldfeature, to_tableposthog_team), ], ), ]这里必须附带说明如果跳过这个约束删除父表删除将永远失败。一旦模型离开 Django 状态Team.objects.delete()的级联不再触达该表其行会持续引用 team因为约束是延迟的Postgres 在整个级联跑完后于COMMIT时抛错删除永远无法成功。对该状态负有责任的表会让团队删除、组织删除在所有含该表行的租户上失效直到有人手动删除该表。可以用python manage.py audit_orphan_hot_table_fks针对长期运行的数据库列出已处于此状态的表。测试基础设施方面若表有指向频繁被 TRUNCATE 的表User、Team、Organization的 FK会出现cannot truncate a table referenced in a foreign key constraint的测试失败——TransactionTestCase用TRUNCATE清理而模型已从 Django 状态移除Django 不知道要把它纳入截断清单在database_operations中删除 FK 约束可一并解决且本就是必需的。第 2 步等待安全窗口。至少等一个完整的部署周期确保没有回滚或 hotfix 会重新引入依赖该表的代码也确保所有应用服务器、worker 与后台任务完成滚动。第 3 步可选删除表。未使用的表可以暂时留着但长期会弄乱 Schema 自省并拖慢迁移。删除前确认没有其他模型通过外键引用它Django 不会自动级联。DROP TABLE会对其自身外键引用的每张表取ACCESS EXCLUSIVE锁所以绝不要在会触及热父表的 DROP 上SET lock_timeout 0要点是快速失败、让bin/migrate重试而不是排队等锁。迁移默认在MIGRATE_LOCK_TIMEOUTposthog/settings/data_stores.py 中默认 20 秒下运行这对触及热父表的 DROP 仍意味着排 20 秒的ACCESS EXCLUSIVE请求因此该场景应设置更短的SET LOCAL lock_timeout。用RunSQL加DROP TABLE IF EXISTS实现显式控制与幂等class Migration(migrations.Migration): dependencies [] operations [ migrations.RunSQL( sqlDROP TABLE IF EXISTS posthog_oldfeature, reverse_sqlmigrations.RunSQL.noop, ), ]反面示例直接用裸DeleteModel非SeparateDatabaseAndState是危险写法禁止使用。在 PR 描述中引用模型移除 PR如Model removed in #12345, deployed X days ago让评审者能核实安全窗口。安全删列先让字段离开 ORM推荐做法是不删列把字段从 ORM 里移除就停手。一个未使用的列几乎零成本数据保留也无需部署协调。但删掉字段再跑 makemigrations并不是达成此目的的方式——Django 会在它写的每条SELECT/INSERT中指名每个具体字段于是makemigrations会生成裸RemoveField在同一部署中既停止代码引用该列又删除该列。加注释标记弃用同样没用因为字段仍在模型上、ORM 仍会指名它。仓库提供了两种真正把字段移出 ORM 的受支持方式均在 posthog/migration_helpers 下deprecate_field()——字段留在模型上不写任何迁移from posthog.migration_helpers import deprecate_field class MyModel(models.Model): myfield deprecate_field(models.BooleanField(nullTrue, blankTrue))读写会记录DeprecationWarning该列从所有查询中消失在makemigrations/migrate命令下真实字段会回来保证 Django 的迁移状态与 Schema 保持不变。传raise_on_accessTrue可把警告升级为错误用于证明已无调用方。字段必须已经是nullTrue隐藏后没有任何东西再写该列NOT NULL列会拒绝后续所有插入辅助函数会拒绝不符合要求的字段。注意deprecate_field()对关系字段不可用——它不写迁移外键约束就没有着落隐藏列恰好会制造上述孤儿引用问题。untrack_field()——现在就把字段从模型状态移除放在一个纯状态迁移里from posthog.migration_helpers import untrack_field operations [untrack_field(mymodel, myfield)]从模型删除字段、跑makemigrations再把生成的RemoveField替换为它。列保留在 Postgres 中。这里同样要求字段已nullTrue辅助函数只收字段名、无法代查需自行确认外键列通常NOT NULLuntrack 之前要检查。若列带有外键必须在同一迁移中删除约束——用DropForeignKey它会从pg_constraint中读取约束名无需硬编码from posthog.migration_helpers import DropForeignKey, untrack_field operations [ untrack_field( mymodel, owner, database_operations[DropForeignKey(posthog_mymodel, columnowner_id)], ), ]DropForeignKey的实现drop_foreign_key.py通过pg_constraint联查pg_class/pg_attribute解析真实约束名这是其幂等性的来源。绝不要手写ALTER TABLE ... DROP CONSTRAINT IF EXISTS nameDjango 以外键哈希后缀命名约束IF EXISTS会把错误的猜测变成迁移成功但什么都没删。如果必须最终删列在上述任一方式部署至少一个完整部署周期后用RunSQL删除operations [ migrations.RunSQL( sqlALTER TABLE posthog_mymodel DROP COLUMN IF EXISTS myfield;, reverse_sqlALTER TABLE posthog_mymodel ADD COLUMN IF NOT EXISTS myfield varchar(32) NULL;, ), ]已经部署的那个版本才是删除的安全保证没有任何运行中的 Pod 再指名该列滚动期间不会失败。reverse_sql只能重新加回空列无法恢复数据删列不可逆。删除前还要检查 Django 之外的读取方——nodejs/、rust/、Temporal workers、Metabase 查询都不走 ORM把字段藏起来对它们没有任何告知作用。删除整个产品/app 的正确顺序移除一个产品products/name/是对每个模型执行上述分阶段删表外加 app 收尾不是删个文件夹。正确顺序先清除所有用法与模型类、用分阶段方式删表状态级DeleteModel→ 等一个部署周期 →DROP TABLE并保持 app 在INSTALLED_APPS中让迁移继续跑等删表迁移到处部署完毕后再把 app 移出INSTALLED_APPS并删除products/name/文件夹可选地执行DELETE FROM django_migrations WHERE app app_label清理孤儿行。切勿通过删除迁移文件来移除表。repo-checksCI 作业中的迁移删除检查hogli lint:migration-deletions会拦截任何删除 master 上存在迁移的行为确有意图且经过评审的删除产品迁移、回滚、squash需在.github/scripts/migration-deletion-allowlist.txt中声明。删文件什么都不删表与 FK 约束仍留在所有已跑过该迁移的数据库里django_migrations行成为永不清理的孤儿新数据库不会建表导致生产与 CI 分叉迁移风险分析器还会把仍在途分支中的该文件当作全新迁移重新分析。加列三阶段部署避免整表重写给大表加一个无默认值的NOT NULL列或带uuid4()、now()等易变默认值Postgres 必须重写整张表来填充值会锁表、耗时可能以分钟到小时计、超时、并阻塞所有写入。安全做法是三阶段部署第 1 步以可空列添加class Migration(migrations.Migration): operations [ migrations.AddField( model_namemymodel, namenew_field, fieldmodels.CharField(max_length100, nullTrue), # 允许 NULL ), ]第 2 步回填数据。中小表、静态值用简单 UPDATE大表用分批回填避免长时间锁from django.db import migrations def backfill_in_batches(apps, schema_editor): MyModel apps.get_model(myapp, MyModel) batch_size 10000 while True: ids list( MyModel.objects.filter(new_field__isnullTrue) .values_list(id, flatTrue)[:batch_size] ) if not ids: break MyModel.objects.filter(id__inids).update(new_fielddefault_value) class Migration(migrations.Migration): operations [ migrations.RunPython(backfill_in_batches), ]第 3 步加 NOT NULL 约束class Migration(migrations.Migration): operations [ migrations.AlterField( model_namemymodel, namenew_field, fieldmodels.CharField(max_length100, nullFalse), # 现在 NOT NULL ), ]或对更多控制权使用RunSQLALTER TABLE mymodel ALTER COLUMN new_field SET NOT NULL反向DROP NOT NULL。加索引CONCURRENTLY 的正确打开方式普通CREATE INDEX在索引创建的整个期间持有排他锁大表上可能阻塞所有写入数小时。但直接用 Django 的AddIndexConcurrently/RemoveIndexConcurrently也不够它们不阻塞但不幂等——Django 发射的是裸CREATE/DROP INDEX CONCURRENTLY没有IF [NOT] EXISTS也没有钩子去禁用lock_timeout/statement_timeout。部署在lock_timeout下跑迁移bin/migrate失败时以指数退避重跑整个迁移而CREATE INDEX CONCURRENTLY非事务性、以atomic False运行一旦构建被取消一次瞬时的lock_timeout就够了OOM、部署超时、statement_timeout、SIGTERM、PG 重启同理Postgres 会留下一个invalid 索引且无处回滚。下次重试重发同样的裸语句报relation ... already exists迁移卡死、阻塞所有部署直到有人手工删除或REINDEX这个 invalid 索引。仅加IF NOT EXISTS也不够——Postgres 的IF NOT EXISTS是名称级而非状态级同名即跳过不管索引是否有效indisvalid false会让迁移成功而索引形同虚设。ConcurrentIndexIdempotencyPolicypolicies.py会在迁移风险分析器中拦截任何不带IF [NOT] EXISTS的并发索引操作。推荐SafeAddIndexConcurrently辅助函数它接收model_name DjangoIndex与 Django 自带操作相同自己跟踪状态——无需SeparateDatabaseAndState、无需把索引改写成裸 SQL——并补上裸 Django 操作缺失的安全禁用lock_timeout/statement_timeout、已存在的有效索引直接跳过、对上次中断构建留下的indisvalid false残留索引自动重建。from django.db import migrations, models from posthog.migration_helpers import SafeAddIndexConcurrently class Migration(migrations.Migration): atomic False # CONCURRENTLY 必需 operations [ SafeAddIndexConcurrently( model_namemymodel, indexmodels.Index(fields[field_name], namemymodel_field_idx), ), ]从实现concurrent_index.py可见其完整逻辑database_forwards先确认不在事务内、禁用超时、用pg_class/pg_index查indisvalid有效则直接返回重试即 no-op无效则先 DROP 再add_index(..., concurrentlyTrue)重建并输出日志面包屑让自动恢复可见。删除索引用镜像的SafeRemoveIndexConcurrentlymodel_name 索引名。裸 SQL 变体CreateIndexConcurrently/DropIndexConcurrently索引无法干净映射到 DjangoIndex如 ORM 无法建模的表达式时使用。它们继承RunSQL、不碰 Django 状态必须包在SeparateDatabaseAndState中并配对的AddIndex/RemoveIndexfrom django.db import migrations, models from posthog.migration_helpers import CreateIndexConcurrently class Migration(migrations.Migration): atomic False # CONCURRENTLY 必需 operations [ migrations.SeparateDatabaseAndState( state_operations[ migrations.AddIndex( model_namemymodel, indexmodels.Index(fields[field_name], namemymodel_field_idx), ), ], database_operations[ CreateIndexConcurrently( index_namemymodel_field_idx, table_namemymodel, columns(field_name), ), ], ), ]该变体还支持unique、using如gin与where部分索引谓词参数。退化方案分区表、自定义操作符类等辅助函数未覆盖的场景裸RunSQL只要带上IF [NOT] EXISTS仍被策略接受migrations.RunSQL( sql SET lock_timeout 0; SET statement_timeout 0; CREATE INDEX CONCURRENTLY IF NOT EXISTS mymodel_field_idx ON mymodel (field_name); , reverse_sqlDROP INDEX CONCURRENTLY IF EXISTS mymodel_field_idx;, )但此形式严格弱于辅助函数它不检测、不清理indisvalid false残留若先前部署因 lock_timeout 以外的原因中断需手动REINDEX INDEX CONCURRENTLY或DROP INDEX CONCURRENTLY IF EXISTS后才能重跑。优先用辅助函数。加约束NOT VALID 两阶段模式常规加CHECK/FOREIGN KEY约束会校验所有既有行并锁表。Postgres 允许拆成两步仓库在 not_valid_constraint.py 中实现第 1 步AddConstraintNotValid加 NOT VALID 约束——短暂锁、不扫表只对新/变更行生效from django.db import migrations, models from django.db.models import Q from posthog.migration_helpers import AddConstraintNotValid class Migration(migrations.Migration): operations [ AddConstraintNotValid( model_namemymodel, constraintmodels.CheckConstraint(conditionQ(field_value__gt0), namemymodel_field_check), ), ]第 2 步ValidateConstraint单独校验——在SHARE UPDATE EXCLUSIVE下扫描既有行允许正常读写from django.db import migrations from posthog.migration_helpers import ValidateConstraint class Migration(migrations.Migration): operations [ ValidateConstraint(model_namemymodel, namemymodel_field_check), ]两阶段必须放在独立迁移中避免 ADD 的短暂ACCESS EXCLUSIVE锁被 VALIDATE 扫描长时间持有或同一迁移内用atomic False。校验阶段先禁用lock_timeout/statement_timeout否则部署期 statement_timeout 会在大表扫描中途杀死进程每次重试都重复ADD 阶段刻意不碰超时——它是ACCESS EXCLUSIVE下的短暂元数据 ALTER应当在锁竞争时快速失败而不是排队堵表。两阶段都幂等add 在约束已存在时跳过validate 在已校验时跳过。若校验失败Django 将该迁移标记为未应用——清理违规行后重跑即可。外键同样适用AddForeignKeyNotValid/ValidateForeignKey但指向热表的 FK 需额外谨慎ADD CONSTRAINT ... NOT VALID仍会短暂锁父表见前文。数据迁移分批、暂停、可恢复RunSQL中的UPDATE/DELETE会长时间锁行RunPython可能慢且持锁。原则是把大更新拆成小批量并加入间隔四个经典模式Pattern 1RunSQL 分批 UPDATE——DO $$ ... $$循环每次取LIMIT batch_size行更新GET DIAGNOSTICS rows_updated判断退出pg_sleep(0.1)暂停。注意DO块在单个事务内运行锁贯穿循环、部分进度无法提交真正要分段提交就用 Python 级分批或后台任务。Pattern 2RunPython 分批 UPDATE——batch_size 10000循环取id列表、filter(id__inids).update(...)每批time.sleep(0.1)并打印进度。批间天然分段可提交。Pattern 3.iterator(chunk_size...)内存高效遍历——避免一次性加载全部行到内存逐行计算后save(update_fields[...])。Pattern 4.bulk_update()批量写回——攒满 batch 后一次bulk_update(objects_to_update, [new_field])远快于逐条 save。关键点批大小 1,000–10,000 行按行宽调优批间小延迟降低系统负载用 WHERE 限定范围每 N 行打日志监控进度先在类生产数据上验证性能百万级行用后台任务而非迁移用.iterator()省内存用.bulk_update()提速。SeparateDatabaseAndState状态与数据库解耦SeparateDatabaseAndState把 Django 迁移状态与实际数据库变更分离是安全多阶段部署的核心工具。两个典型场景安全移除模型——见删表章节state_operations放DeleteModel数据库操作留到后续迁移为已存在的表添加模型——表已由手工或其他系统创建只需更新 Django 状态class Migration(migrations.Migration): operations [ migrations.SeparateDatabaseAndState( state_operations[ migrations.CreateModel( nameExistingTable, fields[ (id, models.BigAutoField(primary_keyTrue)), (name, models.CharField(max_length255)), ], ), ], database_operations[], # 表已存在仅更新 Django 状态 ), ]没有它makemigrations可能生成试图同步状态的错误迁移有了它就能把Django 认为存在什么与数据库里实际有什么分开管理防止状态漂移。一般性最佳实践每个迁移只做一个高风险操作——拆开多个AddIndexRunSQL UPDATEAddField的组合便于回滚、降低部署风险。atomic False只用于 CONCURRENTLY 操作——CONCURRENTLY无法在事务内运行但普通 DDL、数据迁移等应保持原子。原因在于重试问题atomicTrue下失败则什么都没提交、重试干净atomicFalse下部分变更已提交重试会报 column already exists。若一个迁移既需要 Schema 变更又需要并发索引拆成两个迁移Schema 用默认原子并发索引用atomicFalse。若确因长任务需要atomicFalse用IF NOT EXISTS、WHERE NOT EXISTS保证幂等或改用 async migrations参见 async-migrations.md。用IF EXISTS/IF NOT EXISTS保证幂等——让操作可安全重跑。始终有回滚计划——搞清楚失败时会怎样、哪些操作不可回滚DeleteModel、RemoveField不可逆、如何从部分完成中恢复atomicFalse下失败后 Django 不会自动回滚务必手动核对 Schema 一致性记住基础设施团队随时可能因任何原因回滚部署。ClickHouse Schema 变更两个额外的复杂度ClickHouse 是 PostHog 可扩展分析能力的核心其 Schema 同样可以通过迁移变更但多了两个关键复杂度没有传统意义上的索引。每张表只有一个排序键sorting key定义在表的ORDER BY子句中决定数据在磁盘上的布局ClickHouse 按布局顺序读数据因此排序键必须对表的用例最优。以事件表为例其排序键形如ORDER BY (team_id, toDate(timestamp), event, cityHash64(distinct_id), cityHash64(uuid))配合SAMPLE BY cityHash64(distinct_id)支撑多租户下的采样查询。存储事件的表在 PostHog Cloud 上是分片 分布式的sharded distributed。这提升了多租户架构的性能但也意味着这些表的更新不像普通表那样直接可能需要手动写入集群跨 shard 执行 DDL、管理 ZooKeeper 路径、协调复制。为确保新的 ClickHouse 迁移同时解决这两点务必请有丰富 ClickHouse 运维经验的人评审并在#team-clickhouseSlack 频道征求意见。Cloud 级的事件表迁移参考案例可见 clickhouse-event-table-migrations.md其策略是先在每个 shard 的单个节点上创建不含物化列的新表 → 用调优后的设置高速INSERT拷贝数据max_block_size200000, max_insert_threads20, optimize_on_insert0等实测约百万行/秒→ 挂接新 Kafka topic 物化视图 分布式表追赶摄入 → 补建物化列 →OPTIMIZE ... FINAL DEDUPLICATE去重 → 校验 → 复制到各节点 → 停止摄入、RENAME TABLE换名 → 确认无误后删旧表。那次实践还总结了为何弃用 clickhouse-copier拷贝慢、chunk 超过max_table_size_to_drop报错、摄入中难以保证正确性等这些教训同样适用于未来的异步迁移。快速回顾变更前自问真的需要吗向后兼容吗能分开部署吗会锁大表吗绝不删除/重命名模型与字段需要退役时用# DEPRECATED标记、用 Managerget_queryset屏蔽查询、用deprecate_field()/untrack_field()让列离开 ORM。热表上的 ALTER 风险在锁等待队列新字段优先进扩展模型绕不开就显式写进hot_table_acknowledged_migrations.txt。删表/删列必须分阶段先移除代码引用与状态 → 等一个部署周期 → 再动数据库FK 约束务必同步处理。加列走可空 → 回填 → NOT NULL三阶段加索引用SafeAddIndexConcurrentlyatomic False加约束用 NOT VALID → VALIDATE 两阶段。数据迁移分批 暂停 幂等SeparateDatabaseAndState是状态/数据库解耦的瑞士军刀。ClickHouse 迁移额外关注排序键与分片/分布式特性变更前请 ClickHouse 专家评审。仓库中的安全迁移辅助工具集中在 posthog/migration_helpers含配套测试test_concurrent_index.py、test_deprecate_field.py、test_untrack_field.py、test_drop_foreign_key.py、test_not_valid_constraint.py迁移风险分析器在 posthog/management/migration_analysis/policies.py它们在 CI 中把上述规则变成可执行的闸门——理解并善用它们是让每一次 Schema 变更既顺利又安全的关键。【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表