一、Salesforce简介
Salesforce可能是这个世界上最有名的To B领域的公司,市值2500亿美元,凭借着SaaS化的架构理念,并通过依此构建的CRM产品占据CRM 20%的市场,财富500强的公司中,有83%在使用它。公司雇员超过5万人,其业务范围包括财税管理软件、人力资源管理软件、进销存管理软件、客户管理软件、办公自动化软件、企业内即时交流软件等。
下面列出一些salesforce公司的重大事件:
1999年,Salesforce在美国旧金山成立
2001年,推出第一款SaaS应用CRM,同时也受到众多厂商和客户的热议
2005年,推出名为AppExchange的程序商店,以丰富用户选择
2006年,推出首个运行在云计算平台的语言Apex,并在语法上类似Java
2007年,推出PaaS平台 http://Force.com,来让用户更方便地在Salesforce平台上开发在线应用,同时凭借 http://Force.com得到了华尔街日报的科技创新奖(Technology Innovation Award)
2009年,成为首家年收入达到10亿美元的云计算公司,并在年初推出了名为Service Cloud在线客户服务应用
2010年,推出名为Chatter的企业级在线SNS服务,类似于企业内部的LinkedIn,同时其CRM应用更名为Sales Cloud
2019年,以157亿美元收购Tableau
2020年,以277亿美元收购Slack
Salesforce是云计算SaaS的真正始祖,其产品线非常丰富:
其中的http://Force.com虽然不是一个独立的产品,并不直接为salesforce赚钱,但却是Salesforce CRM产品的底层平台,是其商业成功的关键。
笔者曾经带领团队,搭建了一个完整的类似http://Force.com的平台产品,给大家介绍一下http://Force.com的技术架构,同时也分享一些自己在工程实现过程中的心得。本文所阐述的核心设计,一方面是基于已有的部分公开资料,另一部分结合了之前建设类http://Force.com平台的工程经验,如有纰漏和不尽全的地方,欢迎讨论及指正。
二、http://Force.com多租户架构
2.1 元数据驱动思想
元数据驱动的多租户架构的整体架构,整体架构大概分为 5 个大的逻辑层次:
- 最底层是数据层:存储了离散的系统和用户的业务数据,业务日常运营的数据存储在这里;
- 公共元数据层:存储了应用系统标准的对象和标准的字段定义;
- 租户特定元数据:存储了租户自动的对象和自定义的字段的定义;
- 通用数据字典 UDD(Universal Data Dictionary) :运行引擎层实现了应用对象到底层数据存储的映射,包含对象模型操作、SOQL 语言解析、查询优化,全文搜索等功能;
- 平台服务层:提供 PAAS 层平台服务,提供应用对象模型的创建,权限模型创建,逻辑和工作流程创建以及用户界面的创建包括屏幕布局,数据项,报表等;
- 标准应用层:提供端到端的标准的业务应用功能;
- 租户虚拟应用层,用户可以在标准应用层或者平台服务层之上定义自己特有的业务应用功能,来满足自己特定的业务场景需要。
在http://Force.com架构中,无论数据模型,还是应用代码,都以元数据的方式存储在数据库中。实际运行时,由模型引擎和运行引擎加载元数据并解析执行。对于企业用户来说,发布模型和应用,都变得极为快捷,真正让软件变成一个可以触手即得的产品。
在今天看来,这种理念非常寻常,但是在那个软件的售卖模式都是License授权的年代,Salesforce的想法是石破天惊的,是SaaS的真正的先驱。
2.2 多租户存储
任何一个多租户系统,首要面临的问题就是多租户数据的存储问题,这也是整个http://Force.com的设计精华所在。
关键设计思想就是:当用户定义一个新的用户表或者添加字段时,用户创建的不是数据库中的物理表,而是在系统的元数据表中添加了一条记录,这个记录描述的是用户表的逻辑定义,是虚拟的。
任何数据表、字段的变更都不会带来线上数据库的DDL,Salesforce巧妙的通过七个数据表实现这个设计,使得http://Force.com可以描述任意复杂的模型,这才使得它的CRM产品具备无限的扩展能力和无穷的生命力。
2.2.1 多租户模型存储设计
2.2.1.1 多租户元数据设计
- 对象定义表:Objects
Object 表存储了每个租户为它的应用对象定义的元数据,包含如下核心字段:
- OrgID:应用对象所归属的租户 ID
- ObjID:应用对象唯一标识,全局唯一,具有固定长度和格式,系统内部产生
- ObjName:对象名称,就是通常意义的数据表名,如Account、Student等
- 对象字段定义表:Fields
- OrgID:应用对象所归属的租户 ID
- ObjID:包含该字段的对象ObjID
- FieldID:字段ID,对象内唯一
- FieldName:字段名称,就是通常意义的数据表字段名,如Age、Gender等
- DataType:字段类型,Salesforce支持的数据类型非常丰富,如:text, number, date, date/time, picklist(enum), auto-number, sequence, boolean, email, URI……
- Rule:字段校验规则,对于整数类型,可以定义其最小最大值;字符串类型,可以定义正则表达式校验等。这可作为未来数据插入的强制性校验,不通过Rule校验的数据无法被插入或更新
- FieldNum:字段槽位,下面在数据表设计中会提到
- IsIndexed:是否被索引
2.2.1.2 多租户数据表设计
- 对象数据定义表:Data
Data表存储了Objects和Fields元数据表内定义的数据对象 (表) 所对应的数据
- OrgID:应用对象所归属的租户 ID
- ObjID:对象ID
- GUID:数据记录的主键,全局唯一
- flex列(slots):Val0, Val1, ……Val500,系统维护了flex列和实际field的映射关系,即上面Fields表的FieldNum
- 同一个flex列可以代表任意数据类型,variable-length string,所有数据类型(text,int,date…)都以canonical format存储;
下面是一个存储的例子,不同租户的数据都存储在共享的数据表里面,类型信息被抹除,以canonical format存储。但是引擎在实际返回数据时,还要根据元数据表Fields的DataType,做自定义解析,返回强类型数据,让租户无感知。
- 每个对象最多只允许500个自定义字段,如果要求更多,需要向Salesforce的销售申请临时增加
- 在实际工程实现时,我们并没有采取这个设计,而是做了如下的修改:
字符串、整数、浮点数、日期类型,分别有100个槽位。不像Salesforce的设计,500个槽位都是字符串,而是分别都是强类型。这个设计有如下优缺点:
优点:便于将来做非索引字段的自由检索(索引字段的搜索以及全文搜索的设计,在下面的章节有论述)。
缺点:字符串、整数、浮点数、日期分别只允许最多100个字段,灵活性比Salesforce差一些,但是查询引擎的实现难度大幅度下降,现实中这些字段数量一般总是够用的。
- 对象非结构化数据表(大字段):Clob
Clob用于存储大字符段的存储,记录超大字段(长度超过32000的字符串),Data表只记录Clob ID。
2.2.1.3 多租户索引表设计
从上面的Objects、Fields、Data表的设计看出,我们可以完全描述模型和数据了,但是带来的最大问题是,我们如何实现高效的任意字段搜索?
我们对500个flex列都建立索引?
NO!!一方面性能会非常差(难以想象数据表有500个索引),另一方面flex列已经失去了类型信息,无法做range query等复杂查询。
解决方法是增加索引透视表,如下图所示:
- Indexes表包含强类型的索引列,如StringValue, NumValue, DateValue。
- 任何一条记录,这三个字段有且只有一个有值!这是排它的。
- StringValue, NumValue, DateValue这三个字段都建立了索引
- 我们把原来的500个无类型字段的索引,立刻降低为3个强类型字段的普通索引
为了形象的解释这个设计,下面是一个示例:
对于索引字段的查询,就变成Data表和Indexes的表的关联查询,例如用户输入一个查询SQL语句:
SELECT Id, Name, Age FROM Account WHERE Name='Acme'
这是一个逻辑SQL,实际上根本不存在Account这个数据表。查询引擎根据Objects和Fields元数据,得到Id,Name,Age字段的槽位分别是0,2,5,则会把这个逻辑SQL转换成如下物理SQL进行检索:
SELECT Val0, Val2, Val5 FROM Data INNER JOIN Indexes
ON Data.GUID=Indexes.GUID
WHERE Indexes.OrgID=xxx AND Indexes.ObjID=xxxx
AND Indexes.FieldNum=2 AND Indexes.StringVal='Acme'
最后返回前,还需要把Val0,Val2,Val5转换成强类型数据。
所有过程对于租户都是完全透明的,用户感觉似乎真的存在这样的数据表。这就是Salesforce元数据驱动设计的巧妙之处。
Open Issue:上面的设计看起来只支持单字段的索引,复合索引该如何支持?
这是个非常好的问题!大家在上面的元数据驱动的存储设计中,可以看出index table都是单个字段的,那么Salesforce是如何支持复合索引的呢?
答案很简单:不能。http://Force.com 确实在原生存储上不支持复合索引,查询引擎在对外实现“复合索引”功能时,它只会挑选一个字段做索引表驱动,剩余的字段在结果中做筛选。
考虑到实际业务开发中,复合索引是有意义的,可以提高某些特定场景下的查询性能。我们对索引透视表做了少量修改,使得其支持复合索引:
我们创建了(Idx_Val_1, Idx_Val_2, Idx_Val_3, Idx_Val_4)的复合索引,另外还有一个表,记录了这四个字段的槽位,这样我们就可以支持至多四个字段的复合索引
注意,我们没有考虑StringVal,NumVal,DataVal这样的设计,而是直接用VARBINARY,因为这个类型也支持equal和range查询,可以表达字符串、整数和日期类型。
2.2.1.4 多租户唯一约束表设计
为了实现通常意义的数据库设计的字段唯一性约束,引入了Unique_Indexes表的设计,总体上与Indexes相似,只是加了唯一性约束。
如下情况会报错:
- 应用程序试图在唯一性字段上插入重复值
- 管理员试图在包含重复值的已有字段上强制唯一性
2.2.1.5 工程实现难点
上面基本介绍了整体的多租户模型存储设计,在具体的工程实现中有诸多难点,这里列举一些我们在实现过程中遇到的一些挑战:
- 元数据引擎:元数据非常重要,在数据读写引擎中严重依赖模型的元数据。
元数据大部分情况下不会变化,Salesforce把最近最多使用的元数据保持在内存中,避免磁盘IO。
Open Issue:元数据缓存在内存中,强一致性如何保障(模型可能会更新)?
我们必须保证在同一时刻,分布式集群中的每台服务器都看到相同的元数据,不能有任何延迟和数据不一致的情况,否则数据写入或者读取就会出现问题。应该如何设计,保障元数据的强一致性,并且保证高性能?这个思考题留给大家。
- 多租户查询引擎优化:查询引擎实现复杂度较高,必须对逻辑SQL进行语法分析,并且结合模型元数据,编译成底层的物理SQL,对底层数据库进行访问,个别情况下,还要变成多次的SQL访问。
例如对于下面的SQL查询:
SELECT Id FROM MyUser WHERE FirstName = ‘Jane’ AND LastName = ‘Doe’ AND City = ‘San Francisco’
我们应该选择FirstName,LastName,还是City作为主索引表去驱动查询?一旦选择了一个索引,其它两个字段就只在结果集里面筛选。直观上,选择哪个主索引做驱动查询,就是看哪个字段过滤后的结果数量最少,我们称之为column selectivity;column selectivity越大,作为主索引越合适。
Salesforce在多租户查询引擎上面做了大量的优化工作,对索引表的区分度,有一定的统计算法去估算,为查询优化器提供依据。
- 事务性:每条记录的插入或者更新,都伴随着Data、Indexes、Unique_Indexes数据的变化,它们必须在一个事务中,确保数据强一致性。
- 设计抉择:
Q:多租户的数据存储设计为什么要这么复杂,不单独用数据表或者数据库?
A:主要原因就是成本。如果都用物理表的设计,那么单库最多支持上千个模型,一般来说,一个租户有数百个数据模型,单库就只能支撑至多数十个租户,成本极高;并且运行时,各租户会经常修改数据模型,这些模型的修改如果线上执行数据库DDL,将会极大影响数据库性能,并且会影响其它租户。
Salesforce给出Zero DDL的设计,线上海量的租户可以自由更新模型,模型的变更仅仅是元数据记录的更新而已,非常轻量,单库实例可以轻松支撑十万乃至百万模型以上。如果采用物理库设计,很难做到单实例支撑百万个数据表以及在线DDL。
Q:多租户的数据搜索,为什么不用全文搜索引擎,而要用索引透视表?
A:主要两个原因:
- 全文搜索引擎的数据更新并不是实时的,有一定延迟。不适合实时性要求高的场合。
- 无法支持事务,使得其应用场景受到限制。
Salesforce对这个多租户数据存储系统的设计目标,必须支持完整的事务,用于开发严谨复杂的业务系统。
- 在线模型变更:Salesforce支持模型的在线变更,看起来模型进行编辑,只需要把元数据发布生效即可,但实际上考虑到有存量的模型数据,问题变得非常复杂。
Salesforce是怎么实现在线模型变更的。这里列举一下当时我们是如何进行工程实现的:
- 字段变更
- 字段重命名:字段重命名是最简单的,只需要更改Fields表的记录即可,因为底层数据全部是用ID关联,字段名称只在SQL改写时起作用。查询引擎只需要pick up最新的Fields表,就能实时生效,底层数据不用做任何迁移。
- 字段删除:需要在Fields表删除一条记录,标记FieldNum槽位状态为不可用,查询引擎屏蔽掉该字段的读写;Indexes、Unique_Indexes、Data表的相关记录可以异步删除;等所有数据都删除完毕以后,这个槽位再标记为闲置,字段增加时可以复用这个槽位。
- 字段增加:在Fields表增加一条记录同时分配FieldNum槽位。
- 字段修改类型:这个是最麻烦的。修改字段类型时必须兼容,类型只能变大,不能变小。例如可以由int变成long,而不允许反过来。
我们要分配一个新的槽位(但暂时对租户不可见),Data、Indexes数据进行实时复制,不能阻塞应用的实时运行;等待数据复制全部完成后,把FieldNum更新到新的槽位,查询引擎立刻生效。
- 索引变更
- 索引删除:对索引标记逻辑删除,后续再异步删除Indexes相关记录即可。要注意,此时查询引擎不应当再使用这个索引透视数据。
- 索引增加:对Data数据实时创建Indexes记录,不能阻塞应用的实时运行;等待索引透视数据创建完成后,把索引标记为可用,查询引擎立刻生效该索引。
- 索引编辑:我们不支持编辑索引,用户必须通过先删除再增加索引实现。
- 唯一约束变更
- 唯一约束删除:类似索引删除,不再赘述。
- 唯一约束增加:涉及到存量数据可能会违反唯一约束,为了降低实现难度,我们会对这个模型全局禁写,然后根据Data数据实时创建Unique_Indexes记录,如果中间出现唯一约束违反,则全部回滚。唯一约束创建成功后,再对模型数据放开写权限。
2.2.2 关联模型设计
从上面的设计也可以看到,如果要对不同模型进行Join关联查询,难度是较大的。事实上,Salesforce的SQL查询并不原生支持Join。
如何支持多个模型之间的关联查询呢?Salesforce另辟蹊径,引入了关联模型的概念,来解决Join查询的问题。
在Salesforce中,可以将两个Object建立起一对多的关联关系,主要分为Master-Detail和Look up两种关系类型,其特性对比如下。
2.2.2.1 Master-Detail
- Parent-child relationship,master数据被删除时,detail数据也会自动被删除
- Detail数据必须有parent字段
- Detail数据继承master数据的sharing和security属性
- Detail数据没有owner,owner自动为master数据的owner
- Master数据允许创建rollup summary字段
- Rollup summary是master对象上的计算字段,为所有detail对象的聚合字段,聚合函数可以自定义,如SUM、AVG、MAX等。例如部门对象有个rollup summary字段,代表所有部门成员的薪资总和。rollup summary字段实现时有两种方式:1)查询时实时计算,性能较低; 2)提前计算好并物化,查询性能高,但数据实时性下降。前者实现难度略高,Salesforce实现方式是第一种,我们也做了同样的选择。
- Detail数据不允许为其它数据的parent
- 每个object最多允许2个master-detail关系
2.2.2.2 Look up
- 虽然也是一对多,但是没有父子关系
- 删除不会级联,删除一个对象的数据,不会影响另外一端
- 不允许在Look up relation上创建rollup summary字段
- 不会继承sharing和security属性
- 允许嵌套
- 每个object最多允许25个Look up关系
2.2.3.3 Many-to-Many
Salesforce并不支持原生的多对多关系,这是通过两个master-detail实现的,例如A和B要创建many-to-many,可以创建一个中间对象C。C和A是master-detail,C和B也是master-detail
以下图为例,为了创建 Course(课程)和Classroom(教室)的多对多关系,就需要借助一个中间对象的帮助。
我们在实际设计系统时,对关联模型做了几点改进:
- Master-Detail和Look up都允许嵌套和级联;
- 每个object有不受数量限制的look-up和master-detail的关系,但如果模型关联关系成环要非常小心;
- 在底层直接支持Many-to-Many,不需要通过中间对象的辅助。
2.2.3.4 SOQL查询
Salesforce设计了一种通用查询语言,称之为SOQL(对象查询语言),与标准SQL非常相似,这里列举一些与普通SQL的不同之处:
- 不支持SELECT *
- 不支持视图
- SOQL是只读的
- 不支持原生Join
举一个例子:假如存在一个Master-Detail的关系:
Broker__C为master,Property__C为detail。
如果我们想做Child to Parent Query,即从Property__C驱动查询:
如果我们想做Parent to Child Query,即从Broker__C驱动查询:
2.2.3 全文搜索设计
SOQL并不支持全文检索,如果要支持全文搜索,Salesforce设计了对象搜索语言SOSL,下面是一个例子:
FIND {"Joe Smith" OR "Joe Smythe"}
IN Name Fields
RETURNING contact(name, phone)
意即为:在contact对象里面,搜索Name字段包含"Joe Smith"或"Joe Smythe"的记录,返回name和phone字段。
我当时带领团队做架构设计时,认为SQL是最合适的数据查询语言,为全文搜索再发明一个新的数据检索语言,会增加学习和使用成本,所以决定把对象查询和搜索语言合二为一,只保留SQL这一种检索能力,但是做了SQL语言的扩展,以支持全文模糊检索。上面的搜索可以写成如下的形式:
SELECT name, phone FROM contact WHERE MATCH(name, ""Joe Smith")
OR MATCH(name, "Joe Smythe")
扩展了MATCH语法,查询引擎一旦探测到全文检索条件,就会自动把这个SQL翻译为ElasticSearch的DSL,发送到ES进行检索,否则依然发到DB去检索;从DB到ES会有准实时的自动数据同步。
这个全文搜索的设计,有如下缺陷:
- 数据的更新并不是实时的,有一定数据延迟。也就是说,模型数据更新后,必须经过一定时间后才能被搜索到(一般是1s~5s左右,极端情况下可以到15s),不适合实时性要求高的场合。
- 数据必须在MySQL落库以后,才会自动同步到ES集群,所以无法在事务的当中做全文搜索,只能在事务外部使用,使得其应用场景受到限制。
2.2.4 可视化建模Schema Builder
Salesforce提供了一个可视化的建模工具,即Schema Builder,可以形象的显示用户模型以及关联关系,如下图所示:
2.2.5 安全设计
这里列举一些Salesforce在数据安全方面做的一些设计,我们在实际工程中也做了部分实现。
- 回收站:Salesforce对于数据的删除,提供回收站的功能,当企业用户不小心删除一些数据时,可以快速进行恢复。
- 已删除的记录会在回收站中保留 15 天,并且可在该时段内恢复。要在 15 天保留期之前永久删除已删除项目,在 15 天后,已删除项目会从回收站中清除;在清除后,则无法恢复。
- Master-Detail关联关系也会自动恢复
- Look up的关联关系,Salesforce 仅恢复没有被替换的关系。例如:如果某资产在取消删除原始产品记录前,与其他产品进行了关联,将不会恢复该资产产品关系。
- 字段变更审计:用户指定模型中的某字段用于审计,History Tracking Table就会异步记录该字段的变化信息,如旧值、新值、改变日期、改变人等,这在企业系统中是非常重要的。
Salesforce可以查询字段变更历史至最多18~24个月之前的数据,但是对于长度超过255个字符的字段,仅记录其变更信息,并不包含旧值和新值。
- Salesforce中将数据安全分为若干等级:
组织级别:组织级别的安全设定在整个系统内部都有效。这是最广泛的级别
对象级别:对象级别的安全设定可以限制用户对于对象的权限
字段级别:字段级别的安全设定可以限制用户对于字段的权限
记录级别:记录级别的安全设定可以限制用户对于记录的权限,这是最详细的级别
Salesforce会把数据记录的权限事先计算好,并且保存下来,以保障查询的高性能。这样就会带来一个问题,看似简单的改变可能引起大量的系统后台操作,大大降低了系统的运行效率。比如管理员只是改变某个用户的角色等级时,Salesforce 会重新计算每条相关记录的共享设定,在权限计算进行中,相关的记录会被锁定,其他用户尝试修改这条记录时会出错。
我们在实际开发安全策略时,只实现了组织级别和对象级别,字段级别和记录级别并没有实现。
2.2.6 扩展性设计
Salesforce的租户是共享数据库的,那一个共享数据库怎么来支撑如此巨大的多租户数据库,不仅需要支持巨量数据,并且还可以支撑租户间的数据物理隔离,保证各租户的数据稳定性、可用性和数据安全?
Salesforce 的做法是:分区。所有租户的数据、元数据、透视表结,包含底层数据库索引,都是通过对 OrgID 进行物理分库或者分区的。
分区可以帮助提升性能和扩展性,所有的数据读写,都带有特定的租户身份OrgID,因此查询优化器,仅仅需要考虑访问包含对应租户的数据分区访问,而不是整个表或者索引。
不同OrgId的数据天然相互隔离,永远不会存在需要跨Org的数据读写。
Q:如果某租户数据量特别大,性能会不会无法满足需求?
A:是的,Salesforce存在这个问题,它只能按照OrgID来做分区,无法在单租户上继续分库,因为这样会带来分布式事务的问题。对于此情况,只能把租户的数据库资源独立出来。
2.2.7 模型库
Salesforce提供了建模能力,如果每个业务都要自己完全从零开始建模和开发,工作量非常大。Salesforce从商业CRM产品起家,提供了基础模型库及扩展能力,可以用于销售、市场开发、客服等,拆箱即用;支持在标准模型上自由增加自定义字段,对其它租户没有任何影响。
这里给大家列举一些常用的标准模型:
标准模型库还在不断扩展中,目前大概有400多个。
2.2.8 数据互通Salesforce Connect
客户使用Salesforce的平台,不可避免的需要与存量系统或者其它异构系统进行数据互通。在Salesforce中,管理员或开发者可以通过“外部对象”将其他系统中的数据虚拟地展现为Salesforce的对象,每个外部对象都要连接到一个外部数据源(External Data Source)。通过Salesforce Connect可以在Salesforce里查看、搜索、修改存储在其他系统的数据,而不需要将这些数据存储在Salesforce环境里,比如查看存储在SAP系统的数据,或存储在另一个Salesforce Org的数据。
当时我们对这套多租户模型存储引擎,实现了JDBC Driver,通过阿里巴巴开源的DataX组件,可以和外面多达五十多种数据源互通。但缺陷是不支持实时增量同步,只能实现离线全量同步。
2.2.9 多租户存储架构设计展望
这套元数据驱动的多租户模型设计是非常巧妙的想法,但是缺点大家也很容易看出来:为了保持模型设计的灵活性,其运行性能必定比原生数据库要低得多。实际也是如此,我们当初做出的这个系统,单租户的读写性能做到1000 TPS后,很难再提升。
为了解决性能和扩展性问题,我们当时给出一个复合式多租户存储设计:
租户类型 | 设计特点 | 存储引擎实现 | 运行成本 |
大型租户/高并发应用/核心应用 | 每个租户完全独享数据库实例,所有表和字段都是物理的 | 引擎层进行数据库实例路由,SQL直接下发 技术复杂度低 | 高 |
中等规模租户 | 若干个租户共享一个数据库实例,所有表和字段都是物理的 | 表名添加租户ID前缀,引擎层进行SQL改写和路由 技术复杂度较低 | 较高 |
小型租户/长尾租户 | 使用Salesforce的共享数据库模式,全部是逻辑表和字段 | 技术复杂度高 | 低 |
所有数据库存储架构都遵循同一套模型设计语言,同一个安全设计,对于租户来说,是透明的,他并不需要知道自己底层的数据库是何种设计,建模和编程界面完全一致。
这里有个技术难点:小型租户随着业务的迅猛增长,很可能共享式设计已经不能满足需求,这个时候我们就需要把其数据从共享式存储迁移到独享式设计,这个过程要对租户做到完全透明。我们实现了这三种存储的DataX插件,通过阿里巴巴的DataX,可以方便的进行数据同步;但同步过程中,必须禁写,对业务有一定影响,影响时间取决于其数据量大小。
关于元数据驱动的多租户存储架构设计,还有另外一种技术路线,那就是在数据库引擎底层原生支持元数据驱动,支持schema free,同时还兼容关系数据库模型,支持事务。钉钉的智能人事以及宜搭团队正在和阿里云TableStore团队合作,试图在TableStore底层支持来解决这个问题。因为钉钉智能人事的多租户模型使用ADB实现的,事务以及数据实时性问题始终无法解决。
2.3 Apex编程语言
2.3.1 Apex简介
Salesforce的CRM产品运行在http://Force.com平台上,客户来自各行各业,都有自己独特的需求,为了让客户能够自由扩展和开发CRM的个性化功能,Salesforce设计了Apex编程语言,这是世界上第一门为云计算平台设计的编程语言。
本文不打算对Apex这门编程语言做全面的介绍,详细的Apex语言知识可以在这里阅读:Salesforce Developers
这是一个简单的Apex代码片段:
解释Account a = new Account(Name='Acme2'); insert a; Account[] doomedAccts = [SELECT Id, Name FROM Account WHERE Name = 'DotCom']; try { delete doomedAccts; } catch (DmlException e) { // Process exception here } Account myAcct = [SELECT Id, Name, BillingCity FROM Account WHERE Id = :a.Id]; myAcct.BillingCity = 'San Francisco'; try { update myAcct; } catch (DmlException e) { // Process exception here }
Apex的语法非常类似Java,事实上Apex编译器及运行时引擎的最早的代码,也是从Oracle JVM代码的分支fork而来的。但是Apex语言带有非常多的salesforce元数据驱动的要素,是为http://Force.com单独打造的编程语言,支持多线程、数据库、Web、网络等,非常完备,具有自己的闭环生态。
2.3.2 Apex运行资源限制
由于Apex在多租户环境中运行,因此Apex运行时,严格限制资源的占用可以确保异常的Apex代码或进程不会独占共享资源,确保其它租户的正常运行。这里列举一些限制(说的都是单个Apex事务,即一次Execute调用):
- SOQL语句条数不能超过200
- 一条SOQL查询语句最多返回50000条记录
- 堆占用最大不能超过12MB
- 外部HTTP调用不能超过100次,执行时间累计不能超过120秒
- 总执行时间不能超过10分钟
- ……
2.3.3 工程实践
开发元数据驱动的多租户PaaS平台,不能仅提供模型平台,还要提供低门槛的独立的编程语言,并且要和其它已有的服务互通(例如Apex就可以和任意外部服务互通,只要支持Rest或Web Service就可以),否则这个平台就会变得毫无价值。在这方面 ,我们做了一些探索:
1.可视化的编程:基于Google开源的Blockly,代码逻辑就像搭积木一样组建,完全可视化。
生成的代码大致如下:
缺陷:基本就是一个玩具编程语言,适合少儿编程,但不适合专业的二次开发,无法开发较为复杂的CRM应用,也无法与其它系统进行互操作。我们做了原型demo后,迅速放弃这条技术路线。
2.Javascript:我们需要的是一门图灵完备的编程语言,javascript很快进入我们的视线,并且jvm可以加载js在服务端运行业务逻辑,还可以和java代码进行非常方便的互操作,大部分前端同学都非常熟悉Javascript,这简直是天生为我们设计的。
简单对比一下这两个编程语言的界面:
Salesforce Apex:
List<Account> aa = [SELECT Id, Name FROM Account WHERE Name = 'Acme'];
Javascript:
解释function GetAccountListByName(name) { var aa = Engine.Model.As("Account").Select(["Id", "Name"]).Where("Name=?", name).GetList() // 或者直接发出SQL查询: // var aa = Engine.Query("SELECT Id, Name FROM Account WHERE Name = ?"), name.GetList() return aa }
这一段js代码将会被jvm的Nashorn engine加载并执行。
我们不断为js开发各种plugin,使得用户可以在js里面直接操作各种公司内部的中间件,例如消息队列、分布式缓存、数据库、RPC……几乎把整个中间件生态在这套多租户模型框架上面重新造了一套,并且可以无缝和我们的多租户存储引擎互通,最终使得整套系统形成闭环,可以开发任意复杂的应用。
为了证明平台的完备性,我们甚至尝试自举,用这套元数据模型及编程平台,搭建元数据自身的建模平台(自己开发自己),取得了成功。
缺陷:
- 无法支持单元测试框架,Apex可以支持完备的单元测试;不过这个问题其实并不难解决,只是当时对我们来说并不是最重要的;
- 无法在开发平台上支持在线调试和单步跟踪,只能通过在js代码中打log来进行调试,效率较低;在我们的平台上,所有开发都在Web IDE上完成。我们要在Web上实现单步调试,就必须要根据jvm的JDWP协议开发Web debugger,当时考虑到工程实现难度较大,团队人员不足,所以未能启动这项调研;
- 开发很不方便,js不是强类型语言,代码很难维护,虽然图灵完备,但表达能力仍然有一定缺陷,不如Java和Apex。
3.Java:我们最终还是转向了Java,让用户直接在我们的平台上写Java代码。但是比起写一个完整的Java服务来说,在我们的平台上只需要为每个api写简短的代码片段,这个编程体验已经非常接近于Apex。
我们最终实现的是一个类似FaaS的平台,但是与FaaS平台不同的是:1)不使用容器技术,而是用ClassLoader进行隔离,性能无损耗,没有启动时间,可以支持高并发的常驻内存服务;2)FaaS无法长时间存活,只适合做离线的批处理任务,不太适合做线上的高并发应用。
2.3.4 sandbox javascript engine
多租户平台中必须限制单个租户的服务端代码的资源占用量,就像Apex那样。否则一段恶意或者无意的漏洞代码,就会耗尽大量服务器资源,而影响其它租户的应用运行。
我们在js代码解析执行之前,解析其js语法树,在所有方法调用的前后都自动加入了一段探针代码,并且识别代码中的无穷循环,在探针代码中根据JVM MXBean监控其资源占用,从而中断其执行。最终我们可以做到jvm中加载的js代码,限制CPU占用率、最大内存占用、最大运行时间,超过资源则自动强行终止其执行并抛出错误。
三、总结
http://Force.com是Salesforce在2007年的产品,不要以当前的开源技术体系来评估其技术难度,在那个年代,能够完整的实现这套扩展性极强的PaaS系统,Salesforce实在是非常了不起,它有一个非常强大的技术团队。
元数据驱动的多租户架构,可以用在如下技术产品领域:元数据平台、主数据平台、建模平台、CRM、低代码平台、运营系统平台、智能表单等。