首页
/ Sentry Django ORM 模型设计实战指南:Silo 归属、复制、Relocation 与跨 Silo 外键的架构决策

Sentry Django ORM 模型设计实战指南:Silo 归属、复制、Relocation 与跨 Silo 外键的架构决策

2026-09-08 10:35:58作者:晏闻田Solitary

Sentry 的后端是一个庞大的 Django 代码库,其中的每个 Model 都不仅仅是"一张表的映射",还隐含着一整套由单机多租户演进到"Cell / Control 双 Silo"与"Hybrid Cloud"后的架构约束:数据放哪个 Silo、是否要被另一个 Silo 看见、能否随组织一起被导出迁移、外键的删除语义由谁来保证。本文基于仓库中用于指导新模型设计的规范文档 django-models/SKILL.md,并结合 base.pyhybrid_cloud_foreign_key.pyscopes.py 等源码,完整讲解设计一个新 Django ORM 模型时需要依次拍板的四项决策、字段与约束层面的既定惯例,并给出一个开箱可用的 cell-silo 模型骨架。读完本文,你将能在 Sentry 代码库中独立设计、定位并落地一个符合全部架构约定的新模型。

这份指南的定位与使用边界

.agents/skills/django-models/SKILL.md 是一份面向"在 Sentry 中添加 Django ORM 模型"这一任务的架构决策手册。它的触发场景包括:为某个功能设计模型、决定某份新数据应存放在哪里、选择外键类型、重构已有模型的 Silo 归属,或者新建数据库表。它捕获的是建模时那些"改错一个就要连带迁移修一片"的架构性决策,因此不重复讲解 Django 语法、import 顺序或迁移生成——这些属于配套技能的职责:

同时要注意该技能的范围:它只适用于 Django ORM 模型,不适用于 Pydantic 模型、dataclass、机器学习模型或 Protobuf。

定义 Sentry 模型的四项决策

Sentry 的建模规范强调:在写任何字段之前,先依次定下四个问题。这四者彼此耦合——任何一个判断失误,都会连带引发后续用于修正其他决策的迁移。

决策一:这份数据活在哪个 Silo?

Sentry 的运行模型把数据划分为两个 Silo:Cell Silo(承载单组织相关的业务数据)与 Control Silo(承载跨组织共享、或必须与其他 Control 资源强一致的数据,例如认证信息、集成安装、API Token、slug 预留)。

默认选择是 cell silo,即使用 @cell_silo_model 装饰器;只有当下述条件成立时才应改用 @control_silo_model

  • 数据在多个组织之间共享;或
  • 数据必须与其它 control-silo 资源(auth、integration installs、API tokens、slug reservations)保持强一致。

源码中的两个装饰器分别绑定到 ModelSiloLimit(SiloMode.CELL)ModelSiloLimit(SiloMode.CONTROL),定义见 base.pyModelSiloLimit 通过替换模型的 objects manager 以及把 alters_data 方法替换为"不可用即抛 AvailabilityError"的守卫来实现运行时隔离:当服务运行在非该模型所属的 Silo 模式时,调用该模型的写入路径会直接失败。

规范的决策心法不是"这是面向用户的功能所以放 control",而是反过来问:"让这份数据正确存活所需的最小 Silo 是哪一个?它与所有会一起变更它的事物是否同处一个 Silo?" 如果某个 Cell 永远读不到这份数据,它就不属于该 Cell——正如装饰器 docstring 所言,cell 模型是"属于单个组织、或需要与其他 Region(Cell)资源强一致"的模型,而 control 模型是"被多个组织共享、或需要与其他 Control 资源强一致"的模型。

决策二:另一个 Silo 需要看到这份数据吗?

如果需要,模型就不能继承裸的 Model,而必须继承 ReplicatedCellModel(cell 侧)或 ReplicatedControlModel(control 侧),并声明:

category: ClassVar[OutboxCategory] = OutboxCategory.MY_THING

这两类复制基类定义于 outbox/base.pyReplicatedCellModel,属 CellOutboxProducingModel)与同文件 L336-L347ReplicatedControlModel)。它们各自要求声明一个 OutboxCategory,并且 base.py 中的 class_prepared 钩子会在类定义完成时自动把该 category 的更新事件接到模型上——也就是说,一旦模型继承复制基类并给出 category,写入时的 outbox 通知便被自动装配。

值得强调的是:复制是设计的一部分,而不是事后补上的胶水。"另一个 Silo 的 X 是否应该在不发 RPC 的情况下按 ID 查到这份数据"这个问题本身就是一个复制决策,它会直接改变模型的基类。裸 Model 只留给那些真正永远不会跨越 Silo 边界的数据。如果拿不准,应在最终确定模型前先咨询 hybrid-cloud-outboxes 技能。

决策三:这份数据是否属于组织导出(Relocation)的一部分?

每个具体模型都必须设置 __relocation_scope__——这不是建议而是硬性检查:base.pyclass_prepared 信号处理器会遍历所有 BaseModel 子类,一旦发现缺少该属性立即抛出 ValueError,提示开发者填写该模型在导出/迁移流程中参与的范围。同处的检查还包含两条派生约束:

  • __relocation_scope__ 是一个 set 且其中含有 Excluded,直接报错——Excluded 永远只能作为独立值出现(L383-L390);
  • app_label == "getsentry" 的模型必须为 Excluded,否则报错——getsentry 中的东西不可能迁移到另一个实例(L392-L399)。

RelocationScope 枚举定义在 scopes.py,包含 ExcludedUserOrganizationConfigGlobal 五个取值。规范文档把新模型的常见选择归结为下表:

Scope 适用时机
RelocationScope.Organization 绑定组织的客户数据,应随组织一起在迁移中移动。覆盖大多数 project、设置、member、dashboard、alert。
RelocationScope.Excluded 瞬时数据、遥测数据、缓存、系统内部状态、getsentry app 中的任何东西,或换一个实例后不再有意义的数据。

set 形式配合 get_relocation_scope() 覆写确实存在但很罕见,仅在 relocation 行为需要"按实例条件判断"时才值得使用(base.py 展示了 get_relocation_scope() 会转发 __relocation_scope__,部分模型按实例推导),且 Excluded 不能出现在集合中。

如果不确定,可遵循启发式规则:存储客户工作状态的新模型几乎都是 Organization;为 Sentry 自身运维而存在的新模型(队列、缓存、尝试记录、任务、运行时消费的 feature flag 等)几乎都是 Excluded。仓库中的实例可印证这一惯例,例如 workflow_engine/models/action.pyActionaction_alertruletriggeraction.py 都属于运行时执行相关数据,故二者均标为 RelocationScope.Excluded

决策四:每个外键的跨 Silo 爆炸半径有多大?

外键类型在 Sentry 里是架构声明而非风格选择。规范明确区分三种情况:

  • FlexibleForeignKey("sentry.Project", on_delete=...):外键目标与当前模型同处一个 Silo。会创建真实的数据库约束,级联删除由 Postgres 强制执行。实现上它就是 Django ForeignKey 的子类(见 fields/foreignkey.py),只是把 on_delete 默认值设为 models.CASCADE。由于保留了 ORM 关系,一次 project = FlexibleForeignKey(...) 声明即可同时得到 project(关联对象)与 project_id(列值)两个属性。
  • HybridCloudForeignKey("sentry.User", on_delete="...", ...):外键目标位于对侧 Silo。实现位于 fields/hybrid_cloud_foreign_key.py,本质上是"一个带索引的 BigIntegerField":不产生任何数据库约束、不做强一致性保证,级联行为通过 outbox tombstone(CellTombstone/ControlTombstone)交给删除任务异步、最终一致地完成。因为不存在可解析的 ORM 关系,字段必须以显式 _id 后缀命名(如 user_id = HybridCloudForeignKey(...)),且 on_delete字符串传入。可用取值与源码中的 HybridCloudForeignKeyCascadeBehavior 枚举一一对应(同文件 L60-L63):"CASCADE""SET_NULL""DO_NOTHING",传入的字符串会被 upper() 后解析,最终以大写形式落库。字段默认开启 db_index=True,引用目标须写成 "<app>.<ModelName>" 两段式。
  • 原生 Django ForeignKey:应当避免。从源码结构看,仅少数较老的 workflow_engine 模型仍在使用;新代码的统一约定是 FlexibleForeignKey(同 Silo)或 HybridCloudForeignKey(跨 Silo),因为二者都接入了 Sentry 的删除框架与 hybrid-cloud 管道,而裸 ForeignKey 没有这套集成。

一个模型同时存在两种外键完全正常且常见。 出现 HCFK 并不代表模型应该放到对侧 Silo——它只说明"这段关系跨越了 Silo 边界"。一个 cell 模型完全可以既用 FlexibleForeignKey 指向本 Silo 的 sentry.Organization,又用 HCFK 指向 control Silo 的 sentry.User

其他值得固化的建模规范

基类与时间戳

新模型应使用 DefaultFieldsModel,它免费提供 date_addedauto_now_add=True)与 date_updatedauto_now=True),定义见 base.py。真正两个时间戳都不该记录的表极少,所以它"几乎总是你想要的"。而 DefaultFieldsModelExistingbase.py)是仅限遗留模型的基类——其 docstring 明确写着不要在新模型上使用,因为它把 date_added 留成可空以向后兼容早于该字段存在的表。另外,所有 SentryModel 的祖先 Modelbase.py)统一使用 BoundedBigAutoField 作为 id 主键,并默认给出 __repr__ = sane_repr("id")

字段类型的意图

  • 主键用 BoundedBigAutoField;非主键的数字 ID 与计数用 BoundedBigIntegerField / BoundedPositiveIntegerField。这些 bounded 系列字段都定义在 fields/bounded.pyBoundedPositiveIntegerFieldBoundedAutoFieldBoundedBigIntegerFieldBoundedBigAutoField 分别位于该文件约 L31/L68/L78/L108)。"bounded"是运行时溢出守卫而非 Django 的锦上添花——它能在数值越界悄然破坏下游消费方之前把问题拦截下来。
  • 无严格规格的定长文本,倾向 CharField(max_length=256)。Postgres 在 TOAST 阈值以下,varchar(n) 的存储成本不随 n 变化,因此过早选 64 只是"没有收益的约束"。只有列具备真实语义时才取更小的值(如 hash=40/64、UUID=32、slug、有规格的标识符)。
  • 自由文本用 TextField
  • 新 JSON 列优先使用 Django 的 models.JSONField()(jsonb 存储)。sentry.db.models.fields.jsonfield 里的旧版 JSONField 是文本存储,仅为兼容旧列而存在——只有当你刻意要匹配一个既有旧列时才用它。
  • 可变的可调用默认值必须写成 default=dictdefault=list绝不写裸的 default={} / default=[]

软删除是显式的

Sentry 不存在基于 metaclass 的隐式软删除。若模型需要软删除,就显式添加一个使用 ObjectStatusstatus 字段,并让业务逻辑在查询与写入时尊重它。仓库虽保留有 ParanoidModel(定义于 db/models/paranoia.pysentry_app.py 中的 SentryApp 等即继承它),但它是一个重型选择——绝大多数新模型不需要走到这一步。

用 constraints 而非 unique_together

新模型应使用 Meta.constraints = [UniqueConstraint(...)] 替代 unique_together。理由在于 condition=Q(...)部分唯一约束(partial unique constraint)是表达"该列非空时才唯一"这类需求的唯一正确方式,而这种需求在业务中很常见,用 unique_together 表达会静默出错。约束名与索引名应当显式、有描述性,而不是依赖自动生成的名字。

组合索引要匹配查询顺序

如果总是以 (org_id, project_id, type) 一起过滤,就需要一个"字段顺序与先最高选择度字段的过滤模式一致"的组合索引。单靠外键自带的自动索引覆盖不了多列查询的情况——这也是决策四里 FlexibleForeignKey 只解决关联约束、不替你解决查询性能的原因。

模型文件放哪里

  • 默认:放在 src/sentry/<app>/models.py(单文件)或 src/sentry/<app>/models/<thing>.py(一模型一文件)——按该 app 既有先例选择;
  • src/sentry/models/<thing>.py遗留位置,只有在新模型与已住在那里的模型强耦合、挪动反而破坏性更大时,才把新模型放进去;
  • 位于 src/sentry/<app>/ 的新 app 应搭建成真正的 Django app(含 apps.py,以及带 default_app_config__init__.py)。

一个最小可用的 cell-silo 模型骨架

以下是规范给出的起点脚手架——按需裁剪,不要塞入不需要的东西。它不是模板,而是提示"常规新模型长什么样":

from __future__ import annotations

from django.db import models

from sentry.backup.scopes import RelocationScope
from sentry.db.models import DefaultFieldsModel, FlexibleForeignKey, cell_silo_model, sane_repr
from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey


@cell_silo_model
class MyThing(DefaultFieldsModel):
    __relocation_scope__ = RelocationScope.Organization

    organization = FlexibleForeignKey("sentry.Organization")
    project = FlexibleForeignKey("sentry.Project")
    user_id = HybridCloudForeignKey("sentry.User", null=True, on_delete="SET_NULL")

    name = models.CharField(max_length=256)
    config = models.JSONField(default=dict)

    class Meta:
        app_label = "sentry"
        db_table = "sentry_mything"
        constraints = [
            models.UniqueConstraint(
                fields=["organization", "name"],
                name="sentry_mything_org_name_unique",
            ),
        ]

    __repr__ = sane_repr("organization_id", "project_id", "name")

逐行拆解这份骨架,可以看到四项决策与全部惯例是如何落地的:

  • Silo@cell_silo_model 把模型钉在 Cell Silo(决策一);
  • 外键语义organizationprojectFlexibleForeignKey,同 Silo、真实 DB 约束、默认 CASCADEuser_idHybridCloudForeignKey,指向对侧 Silo 的 sentry.User,删除行为取 "SET_NULL" 且显式允许 null=True(决策四)。注意三者的写法差异:前两个能通过 ORM 直接拿到对象,而 HCFK 只有一个 user_id 列值;
  • Relocation__relocation_scope__ = RelocationScope.Organization(决策三)——该表存的是客户工作状态,应随组织一起迁移;
  • 字段类型nameCharField(max_length=256)config 用 jsonb 的 JSONField(default=dict)(可调用默认值);
  • 约束:唯一性通过 UniqueConstraint 表达,并给了显式名字;db_table = "sentry_mything" 遵循 Sentry 的命名惯例;
  • 可调试性:用 sane_repr(定义于 base.py)输出一串 ID 字段作为 __repr__

将其改成其它形态同样直接:

  • 若要控制 Silo 模型,把 @cell_silo_model 换成 @control_silo_model
  • 若这份状态需要被另一个 Silo 可见,把基类改为 ReplicatedCellModel / ReplicatedControlModel 并补上 category = OutboxCategory.MY_THING——其余复制管道的工作交给 hybrid-cloud-outboxes 技能。

模型设计完成之后

一个模型不是写完类就结束了。按规范文档,设计完成后的标准动作依次是:

  1. 生成迁移:调用 generate-migration 技能;
  2. 若模型需要复制:调用 hybrid-cloud-outboxes 技能,接线 payload、signal receivers 与删除处理器;
  3. 若所在 app 的 models/__init__.py 习惯 re-export 模型:把新模型也加进去——这只是方便调用方写 from sentry.<app>.models import Thing,并非 Django 硬性要求,跟随该 app 的先例即可。

HCFK 字段尤其要配合删除框架使用:其模块 docstring(hybrid_cloud_foreign_key.py)列出了接入 HCFK 前应完成的准备——确保目标模型在对侧 Silo、目标模型通过 outbox 在事务内原子地同步 tombstone、为模型在 sentry/deletions 注册合理的级联删除策略,并为新模型编写"父模型删除触发本字段预期级联行为"的模型测试。该 docstring 还特别提醒:把既有字段改成 HCFK 会产生一份默认不可用的迁移,通常需要 hybrid cloud 团队介入精修,所以不要轻率地对存量字段做这种变更。

结语

Sentry 的模型设计规范本质上是在回答一个核心问题:在一套分布式的双 Silo 架构里,一份新数据要"正确地活下去",必须在写第一行字段前就想清楚它住在哪、谁能看见它、它能否被带走、它的外键由谁来守护。把这四项决策连同字段类型、基类选择、约束与索引的惯例一并内化,再配合 generate-migrationhybrid-cloud-outboxeshybrid-cloud-rpc 三个配套技能,就能在 Sentry 中产出一致、可维护、无需反复返工的 Django ORM 模型——这也是 django-models/SKILL.md 希望任何 Agent 或开发者遵循的建模路径。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391