Java 多租户简介

 

Graeme Johnson 和 Michael Dawson
2013 年 11 月 21 日发布/更新: 2014 年 9 月 25 日

WeiboGoogle+用电子邮件发送本页面

Comments

 

0

云供应商必须权衡运行系统和提供服务所需的基础架构的成本与供应商所获得的效益。这些成本效益考虑因素促使供应商考虑应采用多种架构。他们的选择范围涵盖从无共享 架构到共享多租户 架构的一系列架构。在无共享架构中,供应商提供了完全专用于每个客户的硬件、软件和应用程序。在共享多租户的架构中,可以使用单个应用程序支持多个客户的应用程序,而且所有底层的硬件和软件都是共享的。

沿着这个架构系列进行发展时的主要权衡因素是隔离 和密度。密度是由一组特定的硬件和软件交付的系统和服务的数量。共享的资源越多,密度就越高。密度越高,供应商的成本也就越低。同时,更多共享可以降低租户 之间的隔离水平,这里的租户指的是所提供的单独系统或服务。隔离是一个租户对其他租户的活动和数据的影响程度。

对于基于 Java 的租户,在架构系列中的定位包括共享或不共享 JVM。在共享顶层应用程序的任何架构中,必须共享 JVM。共享 JVM 既能节省内存,又能节省处理器时间。但是,如果使用传统的 JVM 技术,那么共享 JVM 通常会从基础架构层中删除所有剩余的隔离,顶层应用程序被要求自身提供这种隔离。 在 IBM 的 7 R1 版中,以技术预览的形式提供了对本文介绍的多租户功能的试用(请参阅 参考资料)。此特性使得用户可以通过部署获得共享 JVM 的优点,尽管与共享传统 JVM 时获得的隔离水平相比,该特性可以提供更高水平的隔离。

多租户 JVM 的成本和效益

使用多租户 JVM 的主要优势是,可以通过部署来避免通常与使用多个标准 JVM 有关的内存消耗。此开销是由以下几个原因造成的:

  • Java 堆消耗数百 MB 的内存。即使对象是相同的,JVM 之间也无法共享堆对象。此外,JVM 往往会使用分配给它们的所有堆,即使它们只在很短的时间内需要峰值量。
  • 即时(JIT)编译器占用了几十 MB 的内存,因为生成的代码是私有的,所以它会占用内存。生成代码也需要大量的处理器周期,这会从应用程序中窃取时间。
  • 类的内部构件(其中的许多构件,比如 String 和 Hashtable,都存在于所有应用程序中)占用了一些内存。每个 JVM 都存在所有这些构件的一个实例。
  • 在默认情况下,每个 JVM 都有垃圾收集器辅助线程(一个核心一个),还有多个编译线程。编译或垃圾收集活动可以同时出现在一个或多个 JVM 中,这可能是次优方案,因为 JVM 会竞争有限的处理器时间。

除了降低内存和处理成本之外,与在单一传统 JVM 中运行多个应用程序相比,多租户 JVM 提供了更好的隔离。

另一个好处是,从共享 JVM 的第一租户开始,后续的应用程序需要更少的时间来启动,因为 JVM 已经在运行。减少启动时间对于短期运行的应用程序特别有用,这些应用程序通常用于脚本。

使用多租户 JVM 的主要成本是,相对于在单独的 JVM 中运行的多个应用程序,租户的隔离水平较低。例如,多租户 JVM 中的本机崩溃会影响所有租户。

另外,JVM 为了实现多租户扩展而必须做的一些工作会导致微小的性能下降。不过,这种性能影响会随着租户数量的增加而降低,因为在同一系统中运行多个 JVM 可以避免增加处理器和内存成本。

使用多租户 JVM

为了选择与其他租户共享一个运行时,当启动应用程序时,应用程序用户在命令行中添加一个 -Xmt 参数。例如:

1

java -Xmt -jar one.jar

其结果是,应用程序的行为(由于我们在本文稍后描述的 限制)就好像它在一个专用 JVM 上运行一样。但在现实中,它是与其他应用程序并行运行的。多租户 JVM 中的扩展支持这种方式的启动,并提供了共享 JVM 的租户之间的隔离。

当租户启动时,JVM 启动程序要么会定位现有的共享 JVM 守护进程 (javad) ,要么会启动它(如有必要),如图 1 所示:

图 1. JVM 启动程序自动定位了共享的 JVM 守护进程(如果有必要,还会启动它)

屏幕截图和图表显示了自动定位和启动共享的 JVM 守护进程 (javad) 的 JVM 启动程序

当第二个租户启动时,租户会发现现有的共享 JVM 守护进程,并在该 JVM 内运行,如图 2 所示:

图 2. JVM 启动程序被定位并连接到现有的 JVM 守护进程

屏幕截图和图表显示了定位并连接到现有的 JVM 守护进程 (javad) 的 JVM 启动程序

结果,javad 进程中存在两个租户共用的引导代码的一个副本。这样的安排使得租户可以共享大部分运行时结构。

使用多租户 JVM 运行现有的应用程序很容易,因为只需更改有限的命令行即可。

实现隔离

在相同的(常规) JVM 中运行的两个或两个以上的应用程序通常不会彼此隔离。每个应用程序的活动都会影响另一个应用程序的结果。此外,通过静态字段共享的数据也可以供所有应用程序访问。多租户 JVM 有两种解决这些问题的方法:静态字段隔离 和资源限制

静态字段隔离

在多租户 JVM 中,各租户之间共享类的不变的部分。这些部分包括编译后的方法代码、JVM 使用的数据结构,以及其他类似的构件。此分享可以节省内存,因为不必在使用多个 JVM 的情况下提供独立的副本。然而,多租户 JVM 为每个租户提供了它们自己的静态字段副本。由于静态字段隔离(以及每个租户通常只能访问自己创建的实例的对象这个事实),每个租户只能访问与自身有关联的数据。其结果是租户之间的数据隔离。

资源限制:应对不良行为

在一个完美的世界中,租户能够以适当的方式共同运营和使用共享资源。然而,在这个不完美的世界中,程序错误和恶意行为都有可能出现。多租户 JVM 提供了可配置的控制,以便限制租户执行错误操作和以影响其他租户的方式使用资源的能力。可以控制的值包括:

  • 处理器时间
  • 堆大小
  • 线程数量
  • 文件 I/O:读带宽和写带宽
  • 套接字 I/O:读带宽和写带宽

可以在 -Xmt 命令行中指定这些控制。例如:

  • -Xlimit:cpu=10-30 (最小 10% 的 CPU,最大 30%)
  • -Xlimit:cpu=30(最大 30% 的 CPU)
  • -Xlimit:netIO=20M(最大带宽为 20 Mbps)
  • -Xms8m-Xmx64m(初始的堆大小为 8 MB,最大值 64 MB)

Java 7 R1 文档包括所有可用选项的信息(请参阅 参考资料)。

性能和内存占用

为了比较无共享 JVM 和多租户 JVM 上的应用程序性能和内存占用,在该测试中,我们将应用程序添加到每个 JVM 配置,直到执行系统置换(system swap)。(当系统置换时,我们认为该系统是 “满的”。)在无共享的情况下,我们在单独的 JVM 中运行应用程序,并为每个额外的应用程序启动一个新的 JVM。在多租户的情况下,我们在单一的多租户 JVM 中将应用程序作为另一个租户运行。

表 1 和 表 2 显示了我们使用一台配置了 1 GB 内存的计算机和一个 64 位 JVM(在所有情况下,都使用了压缩的引用 JVM 和均衡的垃圾回收策略)所得到的结果。在两个表中的 “手工调整” 列显示了我们手工调整了命令行选项,以尽量达到最佳密度(表 1)或启动时间(表 2)后,从常规 JVM 获得的结果。“默认” 列显示使用常规 JVM 与默认选项的结果。

多租户 JVM 实现了无共享 JVM 的 1.2 倍至 4.9 倍的密度(该值根据不同的应用而有所不同),如表 1 所示:

表 1. 并发应用程序的最大数量

应用程序描述多租户手工调整默认通过多租户 JVM 实现的提高
Hello World打印 “HelloWorld” 然后进入睡眠状态30973634.2 倍到 4.9 倍
Jetty启动 Jetty 并等待请求34-181.9 倍
Tomcat启动 Tomcat 并等待请求28-132.1 倍
JRuby启动 JRuby 并等待请求3226151.2 倍到 2.1 倍

更高的密度来自关键构件的共享,这些构件包括:

  • 通过引导和扩展类来加载器加载的类和相关构件、加载器加载的每个类的堆 Class 对象,以及可以安全地跨租户共享的堆对象(例如,实习 String)。
  • JIT 编译的代码和 JIT 编译的类的元数据。
  • 堆:如果其他租户不需要使用堆中的可用空间,那么租户可以使用这些空间。

表 2 显示,我们通过多租户 JVM 实现了快 1.2 倍到 6 倍的平均启动时间:

表 2. 启动时间(第一个/平均)

应用程序描述多租户手工调整默认通过多租户 JVM 实现的提高
Hello World打印 “HelloWorld” 然后进入睡眠状态5709/138ms514/400ms3361/460ms3.3 倍
Jetty启动 Jetty 并等待请求7478/2116ms-6296/12624ms6 倍
Tomcat启动 Tomcat 并等待请求9333/6005ms-7802/7432ms1.2 倍
JRuby启动 JRuby 并等待请求12391/3277ms14847/4101ms7849/6058ms1.25 倍到 1.8 倍

从 表 2 中可以看到,多租户 JVM 上的第一个应用程序实例的启动时间通常比标准 JVM 上的慢。这是预料之中的结果,因为多租户扩展会造成额外的路径长度,导致第一个实例出现一些额外的启动延时。后续实例的启动时间始终是多租户 JVM 表现更好一些。

这些早期的结果是因为使用开发 JVM 而产生的,有可能会获得更大的改进。此外,这些示例并不影响当应用程序在不同时间需要资源时进行的共享。在一个典型的 JVM 中,在整个生命周期中,每个 JVM 需要的内存占用往往会不断增大。在标准 JVM 中,这种内存占用不是共享的。如果使用多租户 JVM,在资源需求不重叠时,可以更容易地共享堆的内存和本机构件。

限制

多租户 JVM 的一个目标是无需修改就能运行所有 Java 应用程序。这在目前是不可能实现的,原因是 Java 规范方面的一些限制以及我们目前的实现中的一些限制。关键的已知限制包括:

  • Java Native Interface (JNI) natives:多租户 JVM 没有为 JNI natives 提供隔离。如果使用多租户 JVM,运行包含用户提供的 JNI natives 的应用程序可能是不安全的。此类应用程序可能会影响整体 JVM 操作和其他租户对数据的访问。在需要对 natives 有足够 “信任” 的情况下(例如,知名的中间件),风险可能是可以接受的。此外,操作系统允许共享的 JVM 进程只加载一个共享库副本,即 natives 所在的位置。其结果是,如果多个租户在同一个共享库中,那么他们将无法加载相同的 natives。
  • Java Virtual Machine Tool Interface (JVMTI):因为调试和分析活动会影响共享 JVM 服务器的所有租户,所以目前多租户 JDK 不支持这些特性。这是我们计划投入更多工作的一个领域。
  • GUI 程序:SWT 等库在原生层保持全局状态,因此在多租户 JDK 中也不支持它们。

结束语

本文介绍了多租户 JVM、如何使用它,以及使用它的成本和收益。我们希望本文激起了您的兴趣,并且希望您会试用 beta,并向我们提供反馈。我们相信,多租户 JVM 可以为适当的环境提供明显的好处。

相关主题

Java应用中实现多租户架构,主要涉及多个方面,包括数据库设计、数据隔离机制、应用层的逻辑处理以及框架支持等。以下是实现多租户架构的关键方法和相关框架。 ### 数据库设计与隔离策略 多租户架构的核心在于如何管理不同租户的数据。常见的策略包括: - **共享数据库,共享表(Shared DB, Shared Schema)**:所有租户共享同一个数据库和表结构,通过租户标识字段区分数据。这种方式易于维护但数据隔离性较差。 - **共享数据库,独立表(Shared DB, Separate Schema/Table)**:每个租户拥有自己的表或模式,数据物理上是隔离的,但共享同一个数据库实例。 - **独立数据库(Separate DB)**:每个租户有自己独立的数据库,提供最高级别的数据隔离,但也带来更高的管理和运维成本[^2]。 ### Java 框架支持 #### Hibernate 多租户支持 Hibernate 提供了对多租户架构的支持,主要包括以下几种模式: - **DATABASE**:每个租户使用独立的数据库。 - **SCHEMA**:每个租户使用独立的 schema。 - **DISCRIMINATOR**:所有租户共享同一套表结构,通过一个字段来区分不同租户。 Hibernate 的多租户功能需要实现 `MultiTenantConnectionProvider` 和 `CurrentTenantIdentifierResolver` 接口来完成动态切换租户的操作[^3]。 #### MyBatis Plus(MP) MyBatis Plus 提供了插件机制,可以通过自定义 SQL 解析器来实现多租户功能。通常的做法是在分页插件中加入多租户解析逻辑,从而在查询时自动添加租户条件到 SQL 语句中,确保只查询当前租户的数据[^4]。 ### 示例代码 下面是一个简单的 Hibernate 配置示例,展示如何设置多租户模式为 SCHEMA: ```java @Configuration @EnableTransactionManagement public class HibernateConfig { @Bean public LocalSessionFactoryBean sessionFactory() { LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean(); // 设置数据源等其他配置... Properties hibernateProperties = new Properties(); hibernateProperties.put("hibernate.multiTenancy", "SCHEMA"); hibernateProperties.put("hibernate.tenant_identifier_resolver", CurrentTenantIdentifierResolver.class.getName()); hibernateProperties.put("hibernate.multi_tenant_connection_provider", MultiTenantConnectionProvider.class.getName()); sessionFactory.setHibernateProperties(hibernateProperties); return sessionFactory; } // 实现 CurrentTenantIdentifierResolver 和 MultiTenantConnectionProvider 接口的具体类 } ``` 此外,在使用 MyBatis Plus 实现多租户时,可以创建一个拦截器来修改 SQL 语句以包含租户信息: ```java @Component @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) public class TenantInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = statementHandler.getBoundSql(); String originalSql = boundSql.getSql(); // 获取当前租户ID String tenantId = TenantContext.getCurrentTenantId(); // 修改SQL语句,添加租户条件 String modifiedSql = modifySqlWithTenantCondition(originalSql, tenantId); // 使用反射替换原始SQL Field field = boundSql.getClass().getDeclaredField("sql"); field.setAccessible(true); field.set(boundSql, modifiedSql); return invocation.proceed(); } private String modifySqlWithTenantCondition(String originalSql, String tenantId) { // 简单示例:假设所有查询都需要加上 WHERE tenant_id = ? return originalSql + " WHERE tenant_id = '" + tenantId + "'"; } } ``` 请注意,这只是一个简化的例子,并没有考虑参数化查询的安全性和复杂情况下的 SQL 改写需求。 ### 总结 实现 Java 应用中的多租户架构不仅涉及到技术选型,还需要仔细规划数据库设计和数据隔离级别。利用像 Hibernate 和 MyBatis Plus 这样的成熟框架可以帮助简化开发工作,提高系统的可扩展性和安全性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值