多租户
每个 DeviceChain 实例运行一组共享的微服务,为其所有租户提供服务。它不会为每个租户启动一套独立的 Pod。租户之间的隔离由消息层和存储层强制执行。
实例及其租户
平台本身由一个 Kubernetes 自定义资源建模:
Instance(集群作用域):每次安装一个,代表平台本身。
租户不是 Kubernetes 资源。租户是控制平面的数据库记录:注册表条目加上租户专属配置。你可以根据需要,通过实例管理 API 和 /admin 控制台创建租户。租户共享实例的服务,没有专属 Pod。
新实例没有租户。它只创建一个超级用户,由该用户通过管理控制台创建第一个租户。
隔离
-
存储(已强制执行)。 每条租户所有的数据行都携带租户信息:几乎所有表都使用
tenant_id列(少数检测引擎表使用tenant)。统一的数据库作用域机制会针对表实际使用的租户列,为每次读取添加WHERE tenant_id = …条件,并在每次写入时标记租户。如果租户作用域的查询上下文没有租户,该机制会拒绝查询,因此漏掉过滤条件不会泄露其他租户的数据。对于 API 请求,租户来自调用方经过验证的 JWT 中的租户声明(内部服务调用是唯一例外,见下文)。对于消息,租户从消息主题中推导。 -
消息传递(已强制执行)。 主题按租户划分作用域(
{instance}.{tenant}.{suffix}),因此每个租户的流量在消息总线上都有独立的命名空间。代理在设备平面强制执行这一点:- MQTT/NATS 监听器使用 TLS;
- NATS auth-callout 机制让代理请求平台授权每个连接,将设备连接限定在自身租户的主题范围内;
- 消息写入和订阅入口拒绝格式错误的租户片段。
因此,设备无法向其他租户的主题发布消息,也无法订阅这些主题。
-
认证。 JWT 携带租户声明,用于确定请求所属的租户。服务在本地验证,无需为每个请求发起网络调用。
删除租户
租户是数据库记录,而不是一组 Pod,因此删除租户并不是拆除基础设施。租户的数据包括行、消息流、缓存查询结果和上传对象,分散在实例使用的各个存储系统中,都以租户令牌为标识。
因此,删除是一个生命周期:
- 立即切断访问。
- 在后台回收数据。
- 保留令牌,直到回收完成,且删除前建立的连接都无法再使用该令牌写入。
参见租户删除。
为什么共享微服务
所有租户共用一组服务,可降低集群资源占用,并简化运维模型。强制执行的行级作用域,加上消息总线的主题作用域,提供了所需的隔离。共享服务会确定每个请求或消息所属的租户,并自动将所有数据访问限制在该租户内。
API 路径上的租户来自调用方经过验证的 RS256 JWT 的租户声明。唯一例外是内部服务间调用:服务令牌没有租户声明,因此调用服务通过请求标头指定租户;只有服务令牌签名通过验证后,才会接受该标头。共享 Pod 通过通配主题消费所有租户的消息,并从每条消息的主题中确定其租户。早期临时采用的、信任网关设置的租户标头的机制已经移除。
数据路径上的运行时租户作用域现已强制执行。没有租户的租户作用域查询会被拒绝。