多租户(MultiTenancy/Tenant)是一种软件架构,其定义是:
在一台服务器上运行单个应用实例,它为多个租户提供服务。
在SaaS实施过程中,有一个显著的考量点,就是如何对应用数据进行设计,以支持多租户,而这种设计的思路,是要在数据的共享、安全隔离和性能间取得平衡。
传统的应用,仅仅服务于单个租户,数据库多部署在企业内部网络环境,对于数据拥有者来说,这些数据是自己“私有”的,它符合自己所定义的全部安全标准。而在云计算时代,随着应用本身被放到云端,导致数据层也经常被公开化,但租户对数据安全性的要求,并不因之下降。同时,多租户应用在租户数量增多的情况下,会比单租户应用面临更多的性能压力。本文即对这个主题进行探讨:多租户在数据层的框架如何在共享、安全与性能间进行取舍,同时了解一下市面上一些常见的数据厂商怎样实现这部分内容。
独立数据库是一个租户独享一个数据库实例,它提供了最强的分离度,租户的数据彼此物理不可见,备份与恢复都很灵活;共享数据库、独立Schema将每个租户关联到同一个数据库的不同Schema,租户间数据彼此逻辑不可见,上层应用程序的实现和独立数据库一样简单,但备份恢复稍显复杂;最后一种模式则是租户数据在数据表级别实现共享,它提供了最低的成本,但引入了额外的编程复杂性(程序的数据访问需要用tenantId来区分不同租户),备份与恢复也更复杂。这三种模式的特点可以用一张图来概括:
上图所总结的是一般性的结论,而在常规场景下需要综合考虑才能决定那种方式是合适的。例如,在占用成本上,认为独立数据库会高,共享模式较低。但如果考虑到大租户潜在的数据扩展需求,有时也许会有相反的成本耗用结论。
而多租户采用的选择,主要是成本原因,对于多数场景而言,共享度越高,软硬件资源的利用效率更好,成本也更低。但同时也要解决好租户资源共享和隔离带来的安全与性能、扩展性等问题。毕竟,也有客户无法满意于将数据与其他租户放在共享资源中。
目前市面上各类数据厂商在多租户的支持上,大抵都是遵循上文所述的这几类模式,或者混合了几种策略,这部分内容将在下面介绍。
JSR338定义了JPA规范2.1,但如我们已经了解到的,Oracle把多租户的多数特性推迟到了JavaEE8中。尽管这些曾经在JavaOne大会中有过演示,但无论是在JPA2.0(JSR317)还是2.1规范中,都依然没有明文提及多租户。不过这并不妨碍一些JPAprovider在这部分领域的实现,Hibernate和EclipseLink已提供了全部或部分的多租户数据层的解决方案。
Hibernate是当今最为流行的开源的对象关系映射(ORM)实现,并能很好地和Spring等框架集成,目前Hibernate支持多租户的独立数据库和独立Schema模式。EclipseLink也是企业级数据持久层JPA标准的参考实现,对最新JPA2.1完整支持,在目前JPA标准尚未引入多租户概念之际,已对多租户支持完好,其前身是诞生已久、功能丰富的对象关系映射工具OracleTopLink。因此本文采用Hibernate和EclipseLink对多租户数据层进行分析。
Hibernate是一个开放源代码的对象/关系映射框架和查询服务。它对JDBC进行了轻量级的对象封装,负责从Java类映射到数据库表,并从Java数据类型映射到SQL数据类型。在4.0版本Hibenate开始支持多租户架构——对不同租户使用独立数据库或独立Sechma,并计划在5.0中支持共享数据表模式。
在Hibernate4.0中的多租户模式有三种,通过hibernate.multiTenancy属性有下面几种配置:
如果是独立数据库,每个租户的数据保存在物理上独立的数据库实例。JDBC连接将指向具体的每个数据库,一个租户对应一个数据库实例。在Hibernate中,这种模式可以通过实现MultiTenantConnectionProvider接口或继承AbstractMultiTenantConnectionProvider类等方式来实现。三种模式中它的共享性最低,因此本文重点讨论以下两种模式。
对于共享数据库,独立Schema。所有的租户共享一个数据库实例,但是他们拥有独立的Schema或Catalog,本文将以多租户酒店管理系统为案例说明Hibernate对多租户的支持和用使用方法。
publicclassTenantIdResolverimplementsCurrentTenantIdentifierResolver{publicStringresolveCurrentTenantIdentifier(){returnLogin.getTenantId();}}
publicclassSchemaBasedMultiTenantConnectionProviderimplementsMultiTenantConnectionProvider,Stoppable,Configurable,ServiceRegistryAwareService{privatefinalDriverManagerConnectionProviderImplconnectionProvider=newDriverManagerConnectionProviderImpl();@OverridepublicConnectiongetConnection(StringtenantIdentifier)throwsSQLException{finalConnectionconnection=connectionProvider.getConnection();connection.createStatement().execute("USE"+tenantIdentifier);returnconnection;}@OverridepublicvoidreleaseConnection(StringtenantIdentifier,Connectionconnection)throwsSQLException{connection.createStatement().execute("USEtest");connectionProvider.closeConnection(connection);}……}与表guest对应的POJO类Guest,其中主要是一些getter和setter方法。
为了区分多个租户,我在Schema的每个数据表需要添加一个字段tenant_id以判定数据是属于哪个租户的。
根据上图在MySQL中创建DATABASEhotel。
我们在OR-Mapping配置文件中使用了Filter,以便在进行数据查询时,会根据tenant_id自动查询出该租户所拥有的数据。
不过Filter只是有助于我们读取数据时显示地忽略掉tenantId,但在进行数据插入的时候,我们还是不得不显式设置相应tenantId才能进行持久化。这种状况只能在Hibernate5版本中得到根本改变。
基于独立Schema模式的多租户实现,其数据表无需额外的tenant_id。通过ConnectionProvider来取得所需的JDBC连接,对其来说一级缓存(Session级别的缓存)是安全的可用的,一级缓存对事物级别的数据进行缓存,一旦事物结束,缓存也即失效。但是该模式下的二级缓存是不安全的,因为多个Schema的数据库的主键可能会是同一个值,这样就使得Hibernate无法正常使用二级缓存来存放对象。例如:在hotel_1的guest表中有个id为1的数据,同时在hotel_2的guest表中也有一个id为1的数据。通常我会根据id来覆盖类的hashCode()方法,这样如果使用二级缓存,就无法区别hotel_1的guest和hote_2的guest。
在共享数据表的模式下的缓存,可以同时使用Hibernate的一级缓存和二级缓存,因为在共享的数据表中,主键是唯一的,数据表中的每条记录属于对应的租户,在二级缓存中的对象也具有唯一性。Hibernate分别为EhCache、OSCache、SwarmCache和JBossCache等缓存插件提供了内置的CacheProvider实现,读者可以根据需要选择合理的缓存,修改Hibernate配置文件设置并启用它,以提高多租户应用的性能。
EclipseLink是Eclipse基金会管理下的开源持久层服务项目,为Java开发人员与各种数据服务(比如:数据库、webservices、对象XML映射(OXM)、企业信息系统(EIS)等)交互提供了一个可扩展框架,目前支持的持久层标准中包括:
EclipseLink前身是OracleTopLink,2007年Oracle将后者绝大部分捐献给了Eclipse基金会,次年EclipseLink被Sun挑选成为JPA2.0的参考实现。
注:目前EclipseLink2.5完全支持2013年发布的JPA2.1(JSR338)。
在完整实现JPA标准之外,针对SaaS环境,在多租户的隔离方面EclipseLink提供了很好的支持以及灵活地解决方案。
应用程序隔离
数据隔离
对于多租户数据源隔离主要有以下方案
本节重点介绍多租户在EclipseLink中的共享数据表和一租户一个表的实现方法,并也以酒店多租户应用的例子展现共享数据表方案的具体实践。
EclipseLinkAnnotation@Multitenant
与@Entity或@MappedSuperclass一起使用,表明它们在一个应用程序中被多租户共享,如清单10。
@Entity@Table(name="room")@Multitenant...publicclassRoom{}表1.Multitenant包含两个属性Annotation属性描述缺省值booleanincludeCriteria是否将租户限定应用到select、update、delete操作上trueMultitenantTypevalue多租户策略,SINGLE_TABLE,TABLE_PER_TENANT,VPD.SINGLE_TABLE共享数据表(SINGLE_TABLE)Metadata配置依靠租户区分列修饰符@TenantDiscriminatorColumn实现。
@Entity@Table(name="hotel_guest")@Multitenant(SINGLE_TABLE)@TenantDiscriminatorColumn(name="tenant_id",contextProperty="tenant.id")publicclassHotelGuest{}或者在EclipseLink描述文件orm.xml定义对象与表映射时进行限制,两者是等价的。
三种方式的属性配置,按优先生效顺序排序如下
例如EntityManagerFactory可以间接通过在persistence.xml中配置持久化单元(PersistenceUnit)或直接传属性参数给初始化时EntityManagerFactory。
按共享粒度可以作如下区分,
用户需要通过eclipselink.session-name提供独立的会话名,确保每个租户占有独立的会话和缓存。
EntityManagerFactory的默认模式,此级别缺省配置为独立二级缓存(L2cache),即每个mutlitenant实体缓存设置为ISOLATED,用户也可设置eclipselink.multitenant.tenants-share-cache属性为真以共享,此时多租户Entity缓存设置为PROTECTED。
这种级别下,一个活动的EntityManager不能更换tenantId。
这种级别下,共享session,共享L2cache,用户需要自己设置缓存策略,以设置哪些租户信息是不能在二级缓存共享的。
同样,一个活动的EntityManager不能更换tenantID。
几点说明:
@TenantDiscriminatorColumns({@TenantDiscriminatorColumn(name="tenant_id",contextProperty="tenant.id"),@TenantDiscriminatorColumn(name="guest_id",contextProperty="guest.id")})租户区分列的名字和对应的上下文属性名可以取任意值,由应用程序开发者设定。生成的Schema可以也可以不包含租户区分列,如tenant_id或guest_id。租户区分列可以映射到实体对象也可以不。注意:当映射的时候,实体对象相应的属性必须标记为只读(insertable=false,updatable=false),这种限制使得区分列不能作为实体表的identifier。
persist,find,refresh,namedqueries,updateall,deleteall。
这种多租户类型使每个租户的数据可以占据专属它自己的一个或多个表,多租户间的这些表可以共享相同Schema也可使用不同的,前者使用前缀(prefix)或后缀(suffix)命名模式的表的租户区分符,后者使用租户专属的Schema名来定义表的租户区分符。
依靠数据表的租户区分修饰符@TenantTableDiscriminator实现
@Entity@Table(name=“CAR”)@Multitenant(TABLE_PER_TENANT)@TenantTableDiscriminator(type=SCHEMA,contextProperty="eclipselink-tenant.id")publicclassCar{}或
与另外两种多租户类型一样,默认情况下,多租户共享EMF,如不想共享EMF,可以通过配置PersistenceUnitProperties.MULTITENANT_SHARED_EMF以及PersistenceUnitProperties.SESSION_NAME实现。
或在persistence.xml配置属性。
酒店多租户应用实例(EclipseLink共享(单)表)
数据库Schema和测试数据与上文Hibernate实现相同,关于对象关系映射(ORmapping)的配置均采用JPA和EclipseLink定义的JavaAnnotation描述。
关于几个基本操作
若用JPQL实现则示例如下:
部分测试数据如下(MySQL):
运行附件MT_Test_Hotels.zip中的测试代码(请参照readme)来看看多租户的一些典型场景。
能得到输出片段如下:
注:上文中提及的全部源码都可以在附件中找到。
独立数据库和独立Sechma的模式,为每个租户备份数据比较容易,因为他们存放在不同的数据表中,只需对整个数据库或整个Schema进行备份。
在共享数据表的模式下,可以将所有租户的数据一起备份,但是若要为某一个租户或按租户分开进行数据备份,就会比较麻烦。通常需要另外写sql脚本根据tenant_id来取得对应的数据然后再备份,但是要按租户来导入的话依然比较麻烦,所以必要时还是需要备份所有并为以后导入方便。
独立数据库:性能高,但价格也高,需要占用资源多,不能共享,性价比低。
共享数据库,独立Schema:性能中等,但价格合适,部分共享,性价比中等。
共享数据库,共享Schema,共享数据表:性能中等(可利用Cache可以提高性能),但价格便宜,完全共享,性价比高。如果在某些表中有大量的数据,可能会对所有租户产生性能影响。
对于共享数据库的情况下,如果因为太多的最终用户同时访问数据库而导致应用程序性能问题,可以考虑数据表分区等数据库端的优化方案。
为了支持多租户应用,共享模式的应用程序往往比使用独立数据库模式的应用程序相对复杂,因为开发一个共享的架构,导致在应用设计上得花较大的努力,因而初始成本会较高。然而,共享模式的应用在运营成本上往往要低一些,每个租户所花的费用也会比较低。
多租户数据层方案的选择是一个综合的考量过程,包括成本、数据隔离与保护、维护、容灾、性能等。但无论怎样选择,OR-Mapping框架对多租户的支持将极大的解放开发人员的工作,从而可以更多专注于应用逻辑。最后我们以一个Hibernate和EclipseLink的比较来结束本文。