深入解析 Sentry 异步删除子系统:从 ScheduledDeletion 调度到级联删除任务
深入解析 Sentry 异步删除子系统从 ScheduledDeletion 调度到级联删除任务【免费下载链接】sentryDeveloper-first error tracking and performance monitoring项目地址: https://gitcode.com/GitHub_Trending/sen/sentry导读当你在 Sentry 中删除一个组织、项目或 IssueGroup时涉及的数据并非只删一行那么简单——它们可能横跨数十张 PostgreSQL 关联表、事件存储Eventstore/Nodestore甚至外部服务。Sentry 在 src/sentry/deletions/README.md 中定义了一套完整的异步删除子系统deletion subsystem它通过 PostgreSQL 记录删除任务与状态在后台按计划批量执行并支持级联删除、失败重试与删除取消。读完本文你将掌握该子系统的整体工作流、调度与重试机制、两大基础删除任务的区别、如何为新增模型接入自定义删除逻辑以及如何直接通过 deletions 管理器同步执行删除。子系统概览为什么需要删除子系统在 Sentry 的数据模型中Organization → Project → GroupIssue→ Event 层层嵌套且大量业务模型AlertRule、Rule、Monitor、SentryApp、Release 等都挂靠在组织或项目之下。当应用新增一个模型时就必须考虑这条记录所属的组织或项目被删除后它应当如何被清理删除子系统主要解决三个工程问题异步化与批量化一次性同步删除海量关联行会长时间占用数据库连接并拖垮请求因此删除被拆成可恢复的异步任务逐 chunk 推进。可重试与可靠性删除任务可能因一次发布deploy被打断或因新增外键关系、数据库故障而失败。子系统用 PostgreSQL 表跟踪每次删除的状态是否 in_progress、计划时间等从而支持失败后重新拾取。级联策略可定制不同模型的删除行为差异很大——有的需要逐条触发 Django signal如依赖子关系清理的模型有的可以单条 SQL 批量删除无关联的叶子模型有的还需要联动事件存储与外部分组服务Seer。调度执行核心Taskbroker 定时任务与重试机制README 指出两个关键调度入口run_scheduled_deletions()每 15 分钟执行一次它会查询所有计划时间已到、且当前未被处理不在 in_progress的删除任务并为每一条任务派生spawn对应的删除处理任务。reattempt_deletions()每天执行一次用来清理陈旧任务它会清掉那些卡住的旧任务的in_progress标记让它们能被下一次 15 分钟调度重新拾取。对照当前仓库这一节早已演化成Control / Cell 双轨实现。在 src/sentry/deletions/tasks/scheduled.py 中实际存在一组配对任务run_scheduled_deletions_control处理 Control 侧ScheduledDeletion与run_scheduled_deletions处理 Cell 侧CellScheduledDeletionreattempt_deletions_control与reattempt_deletions同理二者共用_reattempt_deletions()。其中 scheduled.py 中_reattempt_deletions的实现细节是只重置in_progressTrue且date_scheduled早于当前时间 6 小时以上的任务——即若删除进行中且计划时间已过去 6 小时可认为上次任务已经死亡/失败随后把in_progress翻转为False使任务在下一个周期被重新拾取。def _reattempt_deletions(model_class: type[BaseScheduledDeletion]) - None: queryset model_class.objects.filter( in_progressTrue, date_scheduled__ltetimezone.now() - timedelta(hours6) ) queryset.update(in_progressFalse)而调度执行端_run_scheduled_deletions()scheduled.py则通过原子的条件更新来抢占任务避免同一删除被多个 worker 重复执行queryset model_class.objects.filter(in_progressFalse, date_scheduled__ltetimezone.now()) for item in queryset: with transaction.atomic(router.db_for_write(model_class)): affected model_class.objects.filter( iditem.id, in_progressFalse, ).update(in_progressTrue) if not affected: continue process_task.delay(deletion_iditem.id)即只有成功把in_progress从False更新为True的进程才负责执行该删除这正对应 README 中查询过去到期、且未在进行中的任务的描述。实际的删除处理器run_deletion/run_deletion_controlscheduled.py还配置了处理时限processing_deadline_duration分别为 15 分钟control与 20 分钟cell重试策略最多重试 5 次MAX_RETRIES 5每次间隔 5 分钟超过次数后丢弃任务对DeleteAborted删除被取消异常不重试且静默处理。任务主流程_run_deletion()的逻辑是取出ScheduledDeletion→ 通过get_instance()还原真实对象 → 用 deletions 管理器拿到对应删除任务 → 调用task.should_proceed(instance)校验不通过则直接删除调度记录并终止这就是可取消删除的落地→ 首轮发送pending_deletesignal → 反复调用task.chunk()只要返回True就继续投递下一轮子任务直到chunk()返回False全部删完才删除调度记录本身。调度删除ScheduledDeletion 模型对绝大多数应用代码而言进入删除子系统的入口是ScheduledDeletion模型——通过它创建一条未来某个时间点执行的删除任务。README 给出了最核心的用法from sentry.deletions.models.scheduleddeletion import ScheduledDeletion ScheduledDeletion.schedule(organization, days1, hours2)上面这行代码会把该 organization 调度为在1 天零 2 小时后被删除。从源码看schedule() 是BaseScheduledDeletion上的类方法其完整签名是classmethod def schedule( cls, instance: Model, days: int 30, hours: int 0, data: Any None, actor: Any None ) - Self:关键行为说明days默认30hours默认0即默认 30 天后执行README 示例显式传参即可自定义更短的窗口。使用update_or_create以(app_label, model_name, object_id)为唯一键重复调度同一对象只会更新其计划时间而不会产生重复任务。actor会被记录到actor_id字段用于删除审计data存入 JSONField可携带自定义上下文。该方法会校验模型的silo_limit若当前 Silo 模式下无法操作该模型则直接抛出SiloLimit.AvailabilityError防止在错误的 silo 中调度删除。模型字段层面scheduleddeletion.py一次删除任务记录包含字段含义guid32 位 UUID hex删除任务的唯一交易号transaction_idapp_label/model_name被删除模型的 Django 定位信息object_id被删除记录主键date_added创建时间date_scheduled计划执行时间默认now() 30 daysactor_id发起删除的用户可空dataJSON 扩展字段in_progress是否正在处理中调度与重试机制的核心开关此外当前仓库因多区域silo架构将这一模型拆成了两张物理表scheduleddeletion.pyControl 侧的ScheduledDeletion表名sentry_scheduleddeletion由control_silo_model装饰与 Cell 侧的CellScheduledDeletion表名sentry_regionscheduleddeletion由cell_silo_model装饰。README 写作时以单一模型为例接入时需按被删除模型所在 silo 选择对应的类——可以推断monolith 模式下二者都会照常被处理。get_model()内部还会经过 RELOCATED_MODELS 映射把历史上模型应用已迁移的旧任务例如sentry.Monitor→monitors.Monitor翻译到新的 app_label保证旧调度任务仍能被正确还原与执行。删除任务两种内置基础策略README 指出删除系统提供两个基类来覆盖常见场景ModelDeletionTask逐条获取记录并分别删除每个实例。适合依赖 Django signals、或存在子关联的模型例如删除一条Group时逐条触发post_delete让下游联动。当某个模型没有显式注册删除任务时它就是默认实现。BulkModelDeletionTask用单条查询批量删除记录。适合没有任何关联关系的叶子模型例如GroupAssignee、ProjectKey、EnvironmentProject等中间表效率最高。对照源码 base.py 可以看到二者在设计上的具体差异默认 chunk 大小不同。BaseDeletionTask.DEFAULT_CHUNK_SIZE 100base.py而BulkModelDeletionTask将其重写为10000base.py——批删模型单轮可处理万行逐条删除模型则以 100 为粒度精细推进。chunk() 的行为不同。ModelDeletionTask.chunk()base.py在while循环中反复拉取query命中的记录并调用delete_bulk()直到耗尽chunk_size配额一旦某轮查不到更多行就返回False已删完否则返回True还有更多工作需要调度器再次投递。BulkModelDeletionTask.chunk()base.py则直接调用bulk_delete_objects()在unguarded_write与写库路由保护下执行原始批量 DELETE同样以返回值指示是否还有剩余行。delete_bulk() 的级联编排。BaseDeletionTask.delete_bulk()base.py先根据mark_in_progress决定是否把实例状态置为DELETION_IN_PROGRESS再分别从get_child_relations_bulk()批量视角与每个实例的get_child_relations()单实例视角收集子关系用_delete_children()递归调用对应子任务的chunk()全部子关系清理完后再删除自身。内置限速。ModelDeletionTask.chunk()中每轮都会调用_throttle_deletes()base.py若任务配置了rate_limit_option指向一个整型 option 名称系统会基于漏桶限速器LeakyBucketRateLimiterdrip_rate 取 option 值、burst 取 max(rate, query_limit)按删除行数节流吞吐避免海量删除压垮数据库。为新增模型接入删除任务自定义扩展指南当你的模型存在需要额外清理的子关联、或需要覆盖默认删除行为时README 要求按以下两步注册自定义删除任务将删除任务子类加入sentry.deletions.defaults在sentry.deletions.__init__的默认管理器映射中注册该任务当前仓库中defaults目录src/sentry/deletions/defaults/已包含 40 个模型的删除任务定义如organization.py、project.py、group.py、alertrule.py、monitor.py、rule.py、release.py等并通过 defaults/init.py 统一导出。而映射注册位于 src/sentry/deletions/init.py 的load_defaults()它调用manager.register(Model, TaskClass)把模型绑定到具体任务例如manager.register(models.Group, defaults.GroupDeletionTask) manager.register(models.Organization, defaults.OrganizationDeletionTask) manager.register(models.Project, defaults.ProjectDeletionTask) manager.register(models.Activity, BulkModelDeletionTask)模块级还暴露了与 manager 一一对应的便捷函数init.pyget()、register()、exec_sync()、exec_sync_many()。默认管理器由 get_manager() 以DeletionTaskManager(default_taskModelDeletionTask)构建并用functools.cache缓存——任何未显式注册的模型都会回退到ModelDeletionTask默认实现这正是 README 所说未指定删除任务时的默认策略。manager.py 中get()的解析逻辑也印证了这一点self.tasks.get(model, self.default_task)——先查精确注册表未命中则落到default_task。实现子类时通常需要覆写的钩子在 base.py 中BaseDeletionTask预留了清晰的扩展点chunk()核心推进逻辑一般继承ModelDeletionTask即可无需重写should_proceed(instance)根任务在执行前调用用于支持删除被取消详见下节get_child_relations(instance)/get_child_relations_bulk(instance_list)返回该实例的子关联列表元素为ModelRelation(model, query, task)或裸BaseRelation是级联删除的配方来源filter_relations()配合构造参数skip_models剔除不需要处理的子模型mark_deletion_in_progress()默认把带status字段的实例批量更新为ObjectStatus.DELETION_IN_PROGRESS构造参数query、order_by、query_limit、chunk_size、actor_id、transaction_id等用于定制单个任务的查询范围与执行方式。取消删除should_proceed 钩子如果某个记录已被调度删除、但希望之后能够取消README 的指导是让删除任务实现should_proceed钩子。典型实现为def should_proceed(self, instance: ModelT) - bool: return instance.status in { ObjectStatus.PENDING_DELETION, ObjectStatus.DELETION_IN_PROGRESS }含义是只有当记录当前状态仍属于待删除/删除中时才继续执行删除。仓库中最直接的落地例子是 defaults/organization.py 的OrganizationDeletionTask.should_proceed()——它只删除那些没有被撤销删除undeleted的组织class OrganizationDeletionTask(ModelDeletionTask[Organization]): def should_proceed(self, instance: Organization) - bool: return instance.status in { OrganizationStatus.PENDING_DELETION, OrganizationStatus.DELETION_IN_PROGRESS, }其配套支持来自模型侧的 cancel()它会以(model_name, object_id)查找一条in_progressFalse的调度记录并删除它。因此整个可取消闭环是外部 API 先改变记录状态例如恢复为正常状态→ 定时任务到期执行should_proceed()发现状态不符 →调度记录被移除删除不会发生。README 特别强调当删除被该钩子取消时对应的ScheduledDeletion行会被删除。直接使用 Deletions 管理器同步删除多数情况下删除走异步调度但子系统同样支持在代码里同步驱动删除任务。README 以删除一个 organization为例给出了如下写法from sentry import deletions task deletions.get(modelOrganization, query{}) work True while work: work task.chunk()要点拆解deletions.get(model..., query...)会通过默认 manager 解析出该模型对应的删除任务并实例化query为定位待删记录的过滤条件exec_sync/exec_sync_many会把它封装成query{id: instance.id}/{id__in: [...]}的完整循环见 manager.py循环调用task.chunk()每次执行一个 chunk 的数据清理返回值True表示仍有剩余工作False表示实体已彻底移除据此决定是否继续循环。需要说明的是query{}这种全表/全量匹配的写法在示例中意在展示接口形态实际生产路径如run_deletion总是携带query{id: deletion.object_id}精确定位单条记录见 scheduled.py。级联行为取决于对象类型README 强调系统针对 Organization 有默认实现能够高效地级联删除——该行为会因输入对象不同而变化因为任务可以为自己的子级覆写行为。两个典型对照删除 GroupIssue传统逐级批删。GroupDeletionTaskdefaults/group.py以GROUP_CHUNK_SIZE 100的粒度准批量推进它会一次性为列表中的所有 group 组装子关系涵盖GroupHash、GroupAssignee、Activity、UserReport、EventAttachment等DIRECT_GROUP_RELATED_MODELSADDITIONAL_GROUP_RELATED_MODELS中声明的模型并按 Error / IssuePlatform 分类分别投递ErrorEventsDeletionTask与IssuePlatformEventsDeletionTask去清理事件存储中的 Event 数据。这符合 README批处理每个子级如 Event的描述——因为每一条 group 都有独立的事件负载需要异步清掉。删除 Project跳过 Group 任务、直接批量清理间接后代。而当删除一个 Project 时并不会把事件交给已注册的Group任务逐组处理而是采取更高效的路径直接批量删除它的间接后代如 Event。从源码看ProjectDeletionTask.get_child_relations()defaults/project.py把 20 余种子关联分门别类地列出来像ProjectKey、GroupAssignee、EnvironmentProject这类模型直接挂BulkModelDeletionTask一次批量删掉Group、Rule、Monitor、Activity等则走常规逐级任务。这样设计的原因是既然整个 project 都要消失就无需再为每个 Group 单独调度事件删除与级联——用更少的查询把其所有后代行成批清掉即可。设计要点小结与扩展阅读可靠性优先所有删除任务落库为ScheduledDeletion/CellScheduledDeletion记录配合 15 分钟调度与每日重试清理天然容忍发布中断与单次任务失败。按数据形态选择策略无关联的叶子数据用BulkModelDeletionTask默认 chunk 10000、单查询批删依赖 signal/子关系的用ModelDeletionTask默认 chunk 100两者都以chunk()返回值作为是否还有工作的续跑信号。删除可取消通过should_proceed() 对象状态如PENDING_DELETION/DELETION_IN_PROGRESS实现取消时调度行会被清理见ScheduledDeletion.cancel()。模型扩展有纪律新增模型接入只需两步defaults 子类 manager 注册group 相关模型甚至由测试强制约束——defaults/group.py 的模块注释明确提醒任何带group_id外键的新模型必须登记到_GROUP_RELATED_MODELS列表中否则 tests/sentry/deletions/test_validate_group_related_models.py 一类的校验测试会失败。需要继续深入时建议按以下路径阅读仓库源码整体说明文档src/sentry/deletions/README.md任务基类与限速实现src/sentry/deletions/base.py调度管理器与默认注册表src/sentry/deletions/manager.py、src/sentry/deletions/init.py调度模型与定时任务src/sentry/deletions/models/scheduleddeletion.py、src/sentry/deletions/tasks/scheduled.py三份代表性级联配方defaults/organization.py、defaults/project.py、defaults/group.py【免费下载链接】sentryDeveloper-first error tracking and performance monitoring项目地址: https://gitcode.com/GitHub_Trending/sen/sentry创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考