跳到主要内容

多租户

每个 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,因此删除租户并不是拆除基础设施。租户的数据包括行、消息流、缓存查询结果和上传对象,分散在实例使用的各个存储系统中,都以租户令牌为标识。

因此,删除是一个生命周期:

  1. 立即切断访问。
  2. 在后台回收数据。
  3. 保留令牌,直到回收完成,且删除前建立的连接都无法再使用该令牌写入。

参见租户删除。

为什么共享微服务​

所有租户共用一组服务,可降低集群资源占用,并简化运维模型。强制执行的行级作用域,加上消息总线的主题作用域,提供了所需的隔离。共享服务会确定每个请求或消息所属的租户,并自动将所有数据访问限制在该租户内。

API 路径上的租户来自调用方经过验证的 RS256 JWT 的租户声明。唯一例外是内部服务间调用:服务令牌没有租户声明,因此调用服务通过请求标头指定租户;只有服务令牌签名通过验证后,才会接受该标头。共享 Pod 通过通配主题消费所有租户的消息,并从每条消息的主题中确定其租户。早期临时采用的、信任网关设置的租户标头的机制已经移除。

状态

数据路径上的运行时租户作用域现已强制执行。没有租户的租户作用域查询会被拒绝。