跳到主要内容
版本:开发版

租户删除

删除租户是一个生命周期过程。删除操作立即切断访问,随后在后台回收租户数据;只有回收完成后,租户标识令牌才能再次使用。

本页介绍这两个时刻之间会发生什么、哪些情况会阻碍删除完成,以及平台有意保留哪些记录。

各阶段的时间安排​

立即撤销登录权限,禁用租户并标记为正在删除,同时记录访问切断时刻。约一分钟内,拒绝新的设备连接、数据接入和指令下发。
一到两分钟内后台协调器开始从每个持有租户数据的存储系统回收数据,持续执行直到所有系统都报告不再持有数据。
所有存储系统持续保持已清空直至静默窗口结束(默认五分钟)删除被记录为已清除:平台确认该租户的数据已不存在。令牌仍被保留。
至少 12 小时后移除租户记录,其标识令牌可再次使用。

删除操作本身具有幂等性。删除不存在或已经处于删除过程中的租户会成功返回,不产生其他变更。因此,可以重新运行中途失败的清理脚本。

必须先移除成员关系

如果租户仍有用户成员关系,删除会被拒绝,因为移除成员关系才是真正撤销用户访问权限的操作。先移除成员关系,再删除租户。

为什么保留令牌 12 小时​

租户数据通常在最初一到两分钟内就被清除。继续保留的是令牌;平台报告删除完成前,必须满足两项独立等待条件。

每个存储系统必须持续报告已清空。 一次干净的结果不足以证明清除完成:清扫结束时,一个已经在执行的写入可能随后落地。因此,平台要求持续静默期(默认五分钟);任何一轮仍发现需要删除的数据,都会重新开始计时。

删除之前建立的连接不能再写入。 已连接的设备会保留消息代理凭据直到凭据过期,最长 12 小时。设备凭据记录已被删除,因此无法重新连接,但现有连接仍存活。如果提前释放令牌,迟到写入可能落在一个之后供新租户使用的名字下,导致新租户继承这些数据。整个过程正是为了防止这种情况,因此等待时间与凭据有效期一致,并从最后一个存储系统完成擦除该租户数据时开始计算;此后不会再为该租户签发新的凭据。

两项等待条件都满足之前,令牌保持占用。使用该令牌创建租户会返回相应错误。

删除哪些数据​

租户数据分散在多个地方,回收过程依次检查每个存储系统:

  • 主数据库:其中所有功能领域的模式;
  • 遥测数据库:保存事件历史;
  • 消息代理:租户的流,以及 MQTT 网关自身的会话状态、排队消息和订阅;
  • 键值存储:缓存的查找和解析结果;
  • 运行中的事件处理引擎:打开的检测窗口、运行中的定时器和已启用的数据缺失检查。这些状态存在于内存中,因此要求引擎驱逐租户,而不是通过查询清理;
  • 对象存储:租户品牌标志等上传资源。

只有所有系统都报告已清空,删除才能完成。无法访问的存储系统会在下一轮重试,不会跳过。

需要手动移除的内容​

有些指向租户的设置属于服务配置,而不是租户数据,因此删除操作不会处理它们。

LwM2M 设备身份。 每个 LwM2M DTLS-PSK 身份都在 lwm2m-ingest 配置中绑定到一个租户和一个外部 ID。删除租户后,请从该配置中移除其身份:对于使用 dcctl 安装的实例,从 --lwm2m-identities 指定的文件中移除这些身份,然后使用该文件运行 dcctl upgrade --lwm2m-identities(如果一个身份都不剩,则使用 --withdraw-lwm2m-identities)。否则这些绑定仍然有效,并会应用于之后以相同令牌创建的任何租户。

有意保留的内容​

部分记录会有意保留,它们都不包含租户自身的数据:

  • 用户。 用户账户属于整个实例,不属于租户。被移除的是用户与租户之间的成员关系。
  • 实例级定义:角色、层级、OAuth 客户端、签名密钥和系统设置。它们在每套部署中只存在一份,不随租户删除。
  • 删除记录。 每次删除都会持久记录删除了什么、何时删除,并为每个存储系统保留一条记录。记录不会保留租户名称或联系信息,否则清除证据反而成了最后一个仍存放客户信息的地方。
  • 审计日志,其中的身份信息已销毁。 详见下文。

备份是上述说明的例外。 备份包含整个数据库,因此被删除租户的数据仍留在备份归档中,直到超过数据库的恢复窗口:核心数据默认 30 天(backup_retention_rdb),事件数据默认 7 天(backup_retention_tsdb)。在此之前,恢复到删除之前的时间点会带回这些数据。发送到你提供的对象存储的备份,还受该存储自身生命周期规则约束。使用卷快照基础备份时,集群运行期间会按同一窗口清理云提供商处的快照;如果集群在清理完成前被删除,快照及其中的数据会留在提供商处,直到你在那里删除它们。

审计日志​

实体的每次变更都会记录到审计日志中:发生时间、涉及的表、操作、行数以及操作者。删除租户时保留日志,因为它证明删除确实发生。清除日志会同时抹去清除操作的证据。

任何人的身份信息都不会保留。有两个字段可能标识一个人:

  • 操作者;对于人工登录,这通常是邮箱地址;
  • 受影响行的标签;可能是邮箱,或你为客户、设备、资产选择的名称。

删除租户后,这两个字段都被永久置空。 字段写为空白,而不是混淆或哈希。邮箱很短、容易猜测,持有邮箱列表的人可以将哈希匹配回原地址,那只是名义上的清除。

删除后,租户日志条目仍描述发生了什么(例如“14:02 删除了三台设备”),但不再列出任何人的姓名。这是保留策略正常生效,不是数据缺失。

保留的条目归档在该次删除名下,而不是租户的 token 名下,因此之后以相同 token 新建的租户从空日志开始。删除记录的 retainedAuditTenant 字段就是这些条目归档所用的值:将其作为管理 API auditEvents 查询的 tenant 过滤条件,即可读取该次删除在管理日志中的条目。

在控制台中,打开 **Admin → Audit(管理 → 审计)**并使用租户筛选。它会把每次已完成的删除列为 <token> (deleted <date>),选中其中一项即可查看为该次删除保留的条目。

用已删除租户的 token 新建的租户,也不会继承前一个租户的会话。签发时间早于新租户创建时间(按秒比较)的刷新令牌会被拒绝,对任何调用方都一样,因此原先登录前一个租户的人需要重新登录。

登录记录不属于租户,删除不会触及它们

原成员的登录历史在租户删除后仍保留。如果你的义务也涉及登录记录,应通过数据库和日志保留策略处理,而不是依靠租户删除。

登录分两步:先以个人身份认证,再选择租户。第一步无论成功或失败,都会记录邮箱地址,但不关联租户,因为尚未选择租户。没有租户的记录不可能被任何租户删除操作触及。删除覆盖的是成员在租户内部执行的操作。

另外还有两项较小的限制:

  • 原成员在租户删除完成之后尝试登录,会产生一条标识该用户的新记录;此时该令牌可能已经属于其他租户。
  • 用户自身资料的日志条目仍记录被修改的账户。账户不属于租户,也不随租户删除,因此能够访问两者的运营方仍可以建立关联。销毁的是日志中的姓名,不是日志所引用账户的存在。

确认数据已被清除​

删除有两个应当区分的结果,平台会把两者都记录在删除记录上。

  • 已清除(erasedAt):截至协调器最近一轮,所有存储系统都已报告已清空,并在整个静默窗口内持续保持已清空。这是平台对“该租户的数据已不存在”的声明,lastVerifiedAt 是最近一次确认它的轮次。有人询问某项清除请求何时完成时,应引用这个时刻,并附上下文所述的限定。
  • 已完成(completedAt):令牌还已满足完整的保留期并被释放。它比 erasedAt 晚大约一个令牌保留期,默认 12 小时。只有 completedAt 是最终结果。

erasedAt 是持续有效的声明,而不是日志条目,且只有在所有存储系统一致时才会作出。只要有任何一个存储系统仍持有数据、无法访问,或在最近一轮中仍擦除了数据,就不会作出该声明:只完成一部分的删除不算已清除。在重新检查完所有存储系统之前就中断的一轮也会撤回该声明,因为它没有确认任何事。协调器在令牌保留期内仍持续清扫,因此如果声明作出之后数据重新出现(例如静默窗口之后才落地的迟到写入),声明会被撤回,erasedAt 和 lastVerifiedAt 恢复为空,待各存储再次稳定后重新作出。可通过 lastVerifiedAt 判断声明有多新。带有 completedAt 的记录,其 erasedAt 一定不晚于它。

可通过管理 API 读取;声明有效或删除已完成时,erased 为 true:

query {
tenantDeletions(criteria: {pageNumber: 1, pageSize: 50}) {
results { token epoch erased erasedAt lastVerifiedAt completedAt awaiting blockedBy }
}
}

在终端中,dcctl tenant-deletion status <token> 会输出同一条记录,且除非数据已被清除,否则以非零状态退出,因此清理脚本可以依赖它:

dcctl tenant-deletion status acme --email ops@example.com --password "$PASSWORD" --wait --timeout 1h

--wait 会轮询直到声明有效,若 --timeout 先到则失败。--until complete 改为等待令牌释放。没有删除记录的令牌是错误,绝不会被当作成功。由于令牌可以被重复使用,该命令绝不会报告“该令牌最新的记录”:正在被删除的租户绑定到它自己的那次删除(其当前纪元,从删除的那一刻起就存在),未在删除中的租户是错误,租户消失后则报告最近一次已完成的删除并打印其纪元。--epoch 可绑定到某一次特定的删除。输出包含声明最近一次验证的时间以及各存储系统的备注,例如本实例上不存在的遥测数据库。计数器 devicechain_usermanagement_tenant_purge_erased_total(见下文)会在每次作出声明时计数。

什么会阻碍删除完成​

删除长期未完成,通常由以下情况之一造成;平台日志会明确指出原因:

  • 事件处理服务未运行。 内存中的引擎状态只能由持有它的进程清除。服务缩容到零时,删除会等待,这是正确行为,因为数据仍然存在。启动服务后,下一轮即可处理。
  • 未配置对象存储,但租户曾上传资源。 对象引用已知,对象本身却无法访问。配置保存该对象的存储后端,或通过平台之外的方式移除对象。
  • 存储系统不可访问。 这被视为“稍后重试”,而不是终止失败。系统恢复后,删除自动继续。

这些情况不会导致删除任务丢失。工作清单就在租户自身记录中,因此协调器停止、副本重新调度,甚至系统停机一周后,都会在下一轮继续推进。

如何获知阻塞​

删除阻塞不会让其他功能看起来故障,因此需要独立告警。协调器每轮访问租户,却没有可报告的运行错误,这一轮仍成功。计划任务指标用于发现协调器停止运行,因此删除阻塞期间也可能始终健康;它们回答的是另一个问题。

TenantPurgeStalled 专门回答删除是否阻塞。当最早尚未完成的删除已持续超过配置的令牌保留时间两倍时触发,此时所有强制等待早已结束。管理 API 的 tenantDeletions 查询会列出租户和仍未完成的存储系统。与管理 API 的其他列表一样,它支持分页;读取尚未完成的删除:

query {
tenantDeletions(criteria: {pageNumber: 1, pageSize: 50, completed: false}) {
results { token epoch awaiting blockedBy stores { store complete retaining } }
pagination { totalRecords }
}
}

配套的 TenantPurgeVisibilityLost 在这些数据完全停止采集时触发。没有它,未被抓取指标的用户管理服务,与没有任何未完成删除的服务看起来完全一样。

哪个存储系统在阻塞删除​

TenantPurgeStalled 说明删除已阻塞;下列指标说明阻塞在哪里。用户管理服务导出它们时不带租户标签,因为以租户命名的时间序列会比它所描述的擦除存活得更久:

  • devicechain_usermanagement_tenant_purge_store_blocked{store,reason}:各存储系统当前阻塞的未完成删除数量。store 为 rdb(关系型数据)、tsdb(遥测)、broker(流)、kv(键值存储桶)、detect(检测状态)、blob(对象存储)或 other。reason 为 deferred(存储回答数据仍然存在或仍在写入)或 error(无法向该存储发出请求)。所有组合都会导出,无阻塞时为 0。
  • devicechain_usermanagement_tenant_purge_store_rows_erased_total{store}:各存储已擦除的行、对象、消息或键数量。被阻塞的存储若计数仍在上升,说明正在处理大型租户;计数持平则没有进展。
  • devicechain_usermanagement_tenant_purge_store_erase_duration_seconds{store}:每次擦除调用的耗时,可看出时间花在哪个存储上。
  • devicechain_usermanagement_tenant_purge_erased_total:已作出的清除声明数(见确认数据已被清除)。
  • devicechain_usermanagement_tenant_purge_completed_total、tenant_purge_failures_total、tenant_purge_duration_seconds 和 tenant_purge_clean_duration_seconds:已完成的删除数;协调器未能在一轮中推进某个删除的次数(下一轮会重试);从删除到令牌释放的时间;以及从删除到最后一个存储报告已清空的时间。最后一项不含稳定等待和令牌保留等待,因此衡量的是擦除本身。

三条告警使用这些指标。TenantPurgeStoreFailing 在某个存储持续一小时返回错误时触发。TenantPurgeStoreHolding 在某个存储报告数据仍然存在的时间达到令牌保留时间时触发。TenantPurgeCoordinatorFailing 在协调器自身持续无法推进删除时触发。前两条的 store 标签给出存储名称;日志行 “Tenant purge store did not report clean” 和 tenantDeletions 查询给出原因。

删除大型租户需要多久​

与两个有意设置的等待相比,擦除本身很快。这是一次测量,并说明了其配置;它不是服务级别承诺,也不外推到其他规模或硬件。

配置。 每个存储系统的真实擦除代码,针对一个预先填充数据的租户,运行在一台 Windows 工作站上的单节点 kind 集群(Kubernetes 1.37)中,Docker 分得 20 个逻辑 CPU 和 64 GiB 内存。PostgreSQL 16 上的 TimescaleDB 2.28 与一个 NATS JetStream 服务器各以一个无副本的 Pod 运行在本地卷上,因此每次调用都经过集群网络。该租户与另外三个租户共用每个存储,行与其交错;若被删除租户的数据未消失,或其他租户的数据受到影响,该次运行即告失败。

为被删除租户预置的数据数量
事件(所有类型)/ 其中测量值270 万 / 243 万
位置事件 / 事件锚点27 万 / 27 万
连续聚合中的汇总桶243 万
遥测数据块(31 个中有 23 个已压缩,与生命周期策略的结果一致)31
设备、属性、指令、审计条目(主数据库)约 9.9 万行
消息代理消息 / 键值条目 / 已存储对象270 万 / 6.3 万 / 1.8 万
存储系统首轮至清空耗时该轮擦除量
遥测数据库9.4 秒810 万行(748 MiB 预写日志)
主数据库2.5 秒10.8 万行
消息代理2.3 秒270 万条消息
键值存储1.8 秒6.3 万条
对象存储0.3 秒1.8 万个对象
合计(依次执行)16.2 秒

第二轮在所有存储中都未发现任何数据,每个都不到半秒。以上数字是存储内部花费的时间,不包括三项内容:

  • 实时事件处理引擎。 它被要求从内存中驱逐该租户,其耗时由固定的等待上限决定,而与租户大小无关。本次测量中它并未运行。
  • 等待。 当所有系统在静默窗口内持续保持已清空时,数据即被确认为已清除,因此在默认配置下,从删除到 erasedAt 的时间约为协调器的首轮(每分钟运行一次)加 16 秒再加五分钟。令牌在最后一个系统变为已清空 12 小时后释放。这两项等待都是配置,而不是工作量。
  • 真实集群。 这里每类服务器各一台,没有复制。复制的流、远程存储和其他租户的流量都会带来更高的开销。消息代理和键值的数字应视为下限。

要在自己的硬件上重复测量,请在源码仓库中运行 hack/purge-measure-kind.sh(hack/purge-measure.sh 在两个临时容器上执行同样的测量)。它会输出一份 JSON 报告,包含各存储的耗时、预置数据量和服务器设置。

配置​

这些设置位于用户管理服务的 tenantPurge 下。三项设置中,0 都表示“使用默认值”。

设置默认值含义
intervalSeconds60协调器运行间隔。负值会完全禁用协调器。
settleSeconds300每个存储系统必须持续报告已清空的时间。必须大于 140。
tokenHoldSeconds43200删除令牌保持占用的时间,从最后一个存储系统完成擦除该租户数据时开始计算。

禁用协调器是一项受支持的运维操作,例如维护窗口内不应删除任何行时。这是安全的:待处理删除保持待处理,不会丢失;关闭期间不会释放任何令牌。

两个等待窗口都不能禁用。 任一窗口的负值都会在配置加载时被拒绝,而不是生效。关闭静默窗口意味着尚未观察到数据消失,就宣布已清除;关闭令牌保留意味着旧会话仍可用该名称写入时就释放名称。降低 tokenHoldSeconds 是实际的配置选择,因为删除租户的名字会占用这么久,但它影响正确性,不只是清理速度。

删除期间​

设备流量约一分钟内停止。 删除状态被识别后,所有传输方式的新连接、数据接入和指令下发都被拒绝。被拒绝设备获得与超出速率限制时相同的响应,随后退避重试。

已经连接的设备不会被主动断开。 它们保留现有消息代理凭据直到过期,最长 12 小时,过期后无法重新连接。这就是令牌保留的原因。

用户管理服务不可访问时,这些拒绝暂时不生效,直到服务恢复。这是刻意的权衡,否则实例上所有租户的设备连接都会硬性依赖用户管理服务。提前拒绝流量是为了避免回收不断追赶新数据;清除本身并不依赖它。

数据库拒绝写入​

数据库单独拒绝写入,不依赖其他服务。从回收首次触及主数据库和遥测数据库起,各数据库拒绝被删除租户的所有写入,无论写入来自 API、服务消费的流还是自身后台任务。检查在被拒绝写入的同一个数据库事务内运行。

这使保证成为真正的数据清除,而不只是清扫:即使原本用于提前停止流量的措施全部失败,回收掉的行也不能重新出现并长期保留。迟到写入会再次清扫;写入尚未停止时,删除不会完成。只有删除完成时才解除拒绝,这也正是令牌释放时刻,因此用同一令牌创建的新租户从第一个请求起就能正常写入。

被拒绝的写入不会重试。服务处理流中的消息时遇到拒绝,会在消息到达的那次投递中确认该消息,且不记录任何相关内容:不写死信,也不写 failed-events 记录,因为二者都会成为正在清除的数据的新副本。该消息计为 erased(参见消息结果)。服务在一个事务中写入多个租户的消息时,拒绝会一次性了结其中被删除租户的全部消息,其他租户的消息则排除它们后写入。数据库无法判断租户是否已删除时不算拒绝:这些消息与其他失败写入一样重试。

被删除租户的工作也可能在写入之前就失败,例如删除已移除其引用的设备或指令。这种情况同样不写死信,前提是服务能从 user-management 得知该租户已删除。若无法得知,死信会被写入;删除完成之前写入的死信会随该租户的其他记录一并移除。

拒绝检查在事务首次写入某租户时执行。因此,删除开始时已经为该租户写入的事务,仍能提交,包括其后续写入。只要事务在静默窗口结束前提交,删除过程会捕获这些写入。对于服务停止与数据库通信的事务,例如 Pod 冻结或网络中断,数据库提供相应保证:事务空闲 60 秒后终止并回滚,明显短于允许的最小静默窗口。数据库记录 terminating connection due to idle-in-transaction timeout,事务写入内容不会保留。所属请求返回错误,从流中获取的工作会重新投递。

拒绝覆盖保存租户记录的两个数据库。消息代理、键值存储和对象存储由相同回收轮次处理,但没有等效的写入拒绝。迟到消息或对象会在后续轮次清除,而不是立即拒绝。这也说明为什么所有系统必须在整个静默窗口内保持干净,删除才能完成。

对外连接器与通知​

对外连接器同样受拒绝机制保护,而且这里尤其重要。其他地方,稍晚获准的消息只是暂留平台、等待回收的数据。对外连接器会把租户数据发送到你拥有、平台却不拥有的系统(webhook 端点、MQTT 消息代理、Kafka 主题、SNS 或 SQS 队列);发送后,后续回收轮次无法撤回。因此,被删除租户的连接器投递会被拒绝并丢弃,不会留待检查。

通知也会停止。被删除租户的告警不再发送邮件或调用通知 webhook,尚未解除的告警不再升级。这不依赖设备流量:升级会按定时器再次通知未确认告警。如果没有该拒绝,被删除租户的值班人员会继续收到已经不存在租户的告警通知。

拒绝并非即时生效

删除发生时已经执行中的动作会完成;拒绝大约在删除后一分钟内生效,而非立即生效。如果删除必须保证外部系统或收件人不再收到任何内容,请先禁用租户连接器和通知策略,再删除租户。

目前可查看的内容​

正在删除的租户,其管理控制台详情页有 Deletion(删除) 页签。它回答运营方最关心的问题:*完成了吗?如果没有,为什么?*其中包含:

  • 易懂的状态说明;
  • 阻塞事项;
  • 每个存储系统一行,显示已清空、仍持有数据,或失败后正在重试。

这三种行状态有意区分。仍持有数据通常需要有人改变某项条件;运行失败则会在下一轮清扫中自动重试。

某些系统会在“已清空”行中添加简短说明,指出未检查的内容,例如该实例不存在的遥测数据库,或删除有意豁免的内部缓存。这只是“已清空”的脚注,不是需要处理的问题。

已完成删除位于 Admin → Deletions(管理 → 删除记录),而不是租户页面。删除完成会移除租户,因此也不再有租户页面可显示结果。实例级列表是持久证据,记录令牌、请求时间、完成时间、删除量和各存储系统报告。需要证明客户数据已删除时,可查看此列表。

这里有意不提供两项操作:

  • 没有重试按钮。每轮清扫已自动重试,按钮反而会让人误以为没有自动重试。
  • 没有强制完成。它可能将未发生的清除记录为已完成。