iKonekt

隔离

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

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

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

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

行级安全,强制执行

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

复合键

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

永远没有默认租户

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

那个无法自我暴露的bug

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

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

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

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

隔离能给普通人带来什么

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

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

家族里的每个兄弟服务都持有自己的命名凭证。没有任何东西用共享凭证通信,所以没有服务能意外地冒充另一个——这条小小的纪律直接来自我们曾经用过共享令牌、并且很不喜欢那段经历。

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

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

在正式构建上实测

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

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

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

给运行多个品牌的集团 →