iKonekt

隔离

由数据库守住的边界,而不是代码

每个CRM都声称自己是多租户的。这种声明很廉价;真正重要的是由哪一层来守住这条线。如果答案是“我们的应用总是加上正确的过滤条件”,那么这个保证的可靠性就取决于写查询的那个最缺觉的开发者。

iKonekt从第一次数据库迁移起就是多租户的,而不是事后补装的。这听起来像自夸,实际上是一种坦白:事后补装多租户几乎不可能做得令人信服,而每个尝试过的人都有故事可讲。

隔离存在于数据库中。二十六张表中的每一张都带有它所属的租户,检查这一点的策略在每次读取和每次写入时都会执行,完全在应用层之下。一条忘记边界的查询不会返回别人的客户。它什么都不返回。

行级安全,强制执行

不是约定,不是代码评审规则。是数据库拒绝。七十二条策略,一条一条手写出来而不是生成的——因为生成的策略是没人读过的策略。

复合键

子记录同时引用它的租户和它的父记录,作为一个整体。来自错误公司的杂散标识符无法解析——它根本就不是一个有效的引用。

永远没有默认租户

一个没有作用域的请求不会悄悄变成对零号租户的请求。它在到达数据库之前就会抛出异常。不存在可以忘记关闭的“无作用域”模式。

那个无法自我暴露的缺陷

做租户隔离工作的时候,大多数错误都是响亮的。给两家公司同一个客户域,数据库对象立刻报错,带着错误码和堆栈跟踪;午饭前就能修好。

有一个却没有吭声。一张可编辑提示词覆盖表只以提示词的种类作为键——而不是以种类和租户。第二家公司编辑自己的副本时会悄悄覆盖第一家的,用的是完全合法的 SQL,没有错误、没有警告、也没有失败的测试。它是在改了一个键之后,怀疑响亮的错误还有同伙,于是把剩下的每一个唯一索引逐个读了一遍才被发现的。

我们提这件事,因为它是这项工作诚实的样子。隔离不是一个可以打开的功能;它是一种习惯——去读 schema 里那些无聊的部分,并假定里面有什么东西不同意你的看法。

顺便记录一下测试:共 942 个,每次改动都在一个从零重建的数据库上运行。它们的存在不是为了证明设计有多聪明。它们存在是因为这种特定设计的失败方式是无声的,而无声的失败需要有人替它们喊出来。

隔离能给普通人带来什么

  • 一个集团可以在同一套安装上运营多家公司,其中一家的销售永远不会看到另一家的客户——不是靠政策,而是靠构造。
  • 合作伙伴计划、一组提示词、对客户的认领:全部按公司划分范围,所以一个租户的配置不会变成所有人的。
  • 删除请求在一个边界之内得到执行,而不是跨越一堆共享数据,这正是能够诚实回答的原因。
  • 而且谁都不用记住这些。这就是重点——一个依赖被记住的保证不是保证。

还有一句与数据库无关的话

家族里的每个同级服务都持有自己的命名凭证。没有任何东西用共享凭证通信,所以没有服务能意外地以另一个的身份行动——这条小小的纪律直接来自曾经用过共享令牌并且很不愉快的经历。

这和数据库规则是同一种直觉。让错误的事情变得不可能而不是不被鼓励,你就能把注意力花在客户身上而不是警惕上。

边界应用层之下
  • 126 张表每一张都带有租户字段,检查被强制开启。
  • 272 条策略逐条写出,所以每一条都被一个真人读过。
  • 3942 个测试在每次改动都从零重建的数据库上运行。
  • 40 个默认租户没有作用域的请求永远到不了数据。

在正式构建上实测

“我们的应用总是按公司过滤。”

“总是”这个词在那句话里承担了太多。

把边界下移到数据库里,一个被遗忘的 WHERE 子句返回的是空结果,而不是别人的客户。空白的屏幕比一次尴尬的介绍要好。

面向运营多个品牌的集团 →