内容


Java 多租户:配置选项、租户生命周期和所使用的隔离性

IBM SDK Java Technology Edition(第 7 版的第 1 个发行版)中多租户实现的深入研究

Comments

多租户 JVM 作为技术预览版在 IBM SDK Java Technology Edition(第 7 版的第 1 个发行版本)中提供。通过使用这一特性,应用程序部署就可以在共享传统的 JVM 时实现更好的性能和隔离性。上一篇 developerWorks 文章 “Java 多租户简介” 提供了以下高级概述:

  • 多租户 JVM 的优势与成本
  • 如何使用多租户 JVM
  • 如何实现静态域的隔离
  • 通过资源限制实现资源隔离,资源限制也被称为资源消耗管理 (Resource Consumption Management, RCM)

本文将重点介绍多租户框架的简单性。要想在多租户 JVM 中作为租户运行 Java 应用程序,只需将 -Xmt 添加到命令行即可,如下所示:

./java -Xmt Hello

本文将更详细地深入研究多租户框架的两个领域。首先将介绍租户生命周期,让大家对应用程序在多租户 JVM 中运行有一个更深的认识。在此期间,我们还介绍了以下框架中的配置选项:

  • 将选项传递给租户应用程序
  • 将选项传递给运行租户应用程序的 JVM 守护进程 (javad)
  • 将租户应用程序以特定的 JVM 守护进程为目标

其次,我们将介绍静态隔离性在运行应用程序时的优势。

样例应用程序和配置

我们将使用样例应用程序(可以编译和运行的应用程序)来展示配置选项并运行整个生命周期。我们还将提供命令行来运行应用程序,并提供 javad.options 文件来配置 JVM 守护进程(参见 下载 部分,以便获取本文的所有示例代码、命令和脚本。)。

清单 1 显示了样例应用程序。

清单 1. 样例应用程序
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Hello {

   public static void main(String[] args) {

      InputStreamReader inputStream = new InputStreamReader(System.in);
      BufferedReader reader = new BufferedReader(inputStream);

      System.out.println("What is your name?");

      try {
         String name = reader.readLine();
         System.out.println("Hello " + name);
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
}

将应用程序作为租户启动的命令行(作为单行输入)是:

java -Xmt -Djavad.home=/tmp/multitenant_daemons -Xmx100M 
-Xlimit:netIO=5M-100M -DexampleProperty=1 -jar hello.jar

java -Xmt 命令行上的选项只影响租户应用程序。以下是每一个选项的操作:

-Xmt
告知 Java 启动程序将应用程序作为租户应用程序启动。
-Djavad.home=/tmp/multitenant_daemons
以特定 JVM 守护进程为目标。如果未使用 -Djavad.home,那么该用户运行的所有租户都将在同一 JVM 守护进程下运行。
-Xmx100M
告知 JVM 守护进程将该租户在 JVM 对象堆中的部分限制为 100MB。JVM 对象堆的总大小已在 javad.options 文件中指定。如果租户尝试使用的大小超过 100MB,那么该租户就会收到一个 OutOfMemoryError (OOME),但这一 OOME 并不会影响守护进程下运行的其他任何租户。
-Xlimit:netIO=5M-10M
告知 JVM 守护进程为这一租户保留 5 兆字节每秒的网络 I/O,但要将租户限制在 10MBps。如果 JVM 守护进程无法保留这一最小带宽,则会向租户启动程序发送一条错误消息,而且租户应用程序不会启动。
-DexampleProperty=1
只将该 Java 属性显示给这个租户。JVM 守护进程的这一实例下运行的其他所有租户都不会看到这个属性。

要想传递 JVM 守护进程本身的选项,惟一的方法就是通过 javad.options 文件。清单 2 展示了 javad.options 文件,该文件用于将会运行样例应用程序的 JVM 守护进程。

清单 2. javad.options
# Option file for javad
-Xtenant
-Xrcm:consumer=autotenant
-Djavad.persistAfterEmptyTime=1
-Xmx2G
-Xdump:java:label=/tmp/multitenant_daemons/javacore.%pid.%seq.txt

以下是 javad.options 文件中各个选项的操作:

-Xtenant -Xrcm:consumer=autotenant
指示 JVM 实例作为守护进程以多租户模式运行。
-Djavad.persistAfterEmptyTime=1
指示 JVM 守护进程在上一个租户关闭时停止一分钟。
-Xmx2G
将 JVM 守护进程的最大对象堆大小指定为 2GB。JVM 守护进程下运行的所有租户都将使用该 2GB 对象堆大小的其中一部分。
-Xdump:java:label=/tmp/multitenant_daemons/javacore.%pid.%seq.txt
将指定的文件名分配给 JVM 生成的 javacore 文件。(参见 用户指南,了解这一选项的详细信息。)

现在,您已经了解了示例所使用的各个选项,所以您可以设置自己的环境来运行应用程序。

环境设置

由于运行应用程序的命令行指定了 -Djavad.home(以便可以使用特定 JVM 守护进程作为目标),所以您必须创建 -Djavad.home 目录:

mkdir /tmp/multitenant_daemons

接下来,转到 /tmp/multitenant_daemons,将 SDK 中包含的 javad.options 文件复制到这个目录中,并添加我们的其他选项:

cd /tmp/multitenant_daemons
cp /tmp/Java7R1_SR1/jre/bin/javad.options .
echo -Djavad.persistAfterEmptyTime=1 >> javad.options
echo -Xdump:java:label=/tmp/multitenant_daemons/javacore.%pid.%seq.txt >> javad.options
echo -Xmx2G >> javad.options

运行 cat javad.options 并检查 javad.options 文件的内容,验证它是否正确:

# Option file for javad
-Xtenant
-Xrcm:consumer=autotenant
-Djavad.persistAfterEmptyTime=1
-Xdump:java:label=/tmp/multitenant_daemons/javacore.%pid.%seq.txt
-Xmx2G

此时,新创建的 -Djavad.home 目录只包含 javad.options 文件。在启动之后,JVM 守护进程会将连接信息写入这个目录中。

查看租户生命周期

现在,您已经配置好了自己的环境,并拥有了样例应用程序和配置,现在我们就可以逐步查看生命周期。主要阶段有:

  1. 租户启动程序和 JVM 守护进程启动
  2. 应用程序执行
  3. 租户关闭
  4. JVM 守护进程关闭

第 1 阶段:租户启动程序和 JVM 守护进程启动

当 Java 应用程序作为租户启动(通过 -Xmt 命令行选项)时,租户启动程序会注意 JVM 守护进程是否已经在运行。如果该进程尚未运行,那么租户启动程序就会启动一个进程。在启动 JVM 守护进程之后,在该进程与租户启动程序之间就会出现一些 “握手信号”。在这一握手信号中,启动程序进程的环境和租户命令行选项都会发送至 JVM 守护进程。最后,租户应用程序将在 JVM 守护进程下运行。

在示例应用程序的上下文中更详细地查看一下这一序列。首先,运行租户启动程序命令行(输入为单行):

java -Xmt -Djavad.home=/tmp/multitenant_daemons -Xmx100M -Xlimit:netIO=5M-100M 
-DexampleProperty=1 -jar /tmp/hello.jar

Java 启动程序在命令行上发现 -Xmt 并变成租户启动程序。

租户启动程序将会读取 -Djavad.home=/tmp/multitenant_daemons,这样,您就可以同时查看 /tmp/multitenant_daemons 来确定 JVM 守护进程是否已与这个用户和目录有关联。启动程序会确定没有此类的 JVM 守护进程出现。因此它会启动一个守护进程。请记住,要将 javad.options 文件中包含的选项传递给 JVM 守护进程。

在启动 JVM 守护进程之后,它会通过将连接信息写入 -Djavad.home 目录来 “公布” 自己。该守护进程将会创建一个新的表单目录 .java.uid 来存储这一连接信息。例如,此处新目录的名称是 .javad.4068,其中 4068 是当前用户的 UID:

dave /tmp/multitenant_daemons  $ ls -altr
total 32
-rwxr-xr-x 1 dave bgroup 164 Jun 26 17:04 javad.options
drwxrwxrwt. 10 root root 20480 Jun 26 17:05 ..
drwxr-xr-x 3 dave bgroup 4096 Jun 26 17:05 .
drwxr-xr-x 2 dave bgroup 4096 Jun 26 17:05 .javad.4068
dave /tmp/multitenant_daemons $

UID 用来关联 JVM 守护进程和当前用户。其他用户无法连接到 JVM 守护进程,因为 .java.4068 内容的访问权限仅限于开始创建 JVM 守护进程的用户:

dave /tmp/multitenant_daemons $ ls -altr .javad.4068
total 12
drwxr-xr-x 3 dave bgroup 4096 Jun 26 17:05 ..
drwxr-xr-x 2 dave bgroup 4096 Jun 26 17:05 .
-rw------- 1 dave bgroup 103 Jun 26 17:05 4068
dave /tmp/multitenant_daemons $

租户启动程序将会读取 .javad.4068 中的连接信息,创建一个套接字来连接到 JVM 守护进程。租户启动程序和 JVM 守护进程之间的所有后续通信都是通过内部指定的线路协议经由该套接字来实现的。

租户启动程序将其命令行和环境变量的完整副本发送至 javad 守护进程,然后等待 javad 进程的消息。

JVM 守护进程读取 -Xlimit:netIO=5M-100M,并检查其是否满足 5MBps 的最小带宽需求。守护进程通过验证所有租户的最小带宽需求总和未超过 rcm.xml 中指定的值来执行这一检查。

然后,JVM 守护进程将会 创建一个租户 来运行租户启动程序指定的 Java 应用程序。

启动的最后一步需要将租户应用程序的 System.out、System.in 和 System.err 重新定向到连接租户启动程序和 JVM 守护进程的套接字通道。

第 2 阶段:应用程序执行

此时,JVM 已准备好运行应用程序,而且 JVM 守护进程将会调用新创建租户中的 main 方法。看看 清单 1 中每一部分代码将会发生哪些变化。

第一部分只写出了一条消息:

public static void main(String[] args) {

   InputStreamReader inputStream = new InputStreamReader(System.in);
   BufferedReader reader = new BufferedReader(inputStream);

   System.out.println("What is your name?");

JVM 守护进程将这一操作拦截到 System.out,并通过套接字将其重新定向到租户启动程序。租户启动程序将 “What is your name?” 写入其控制台:

dave /tmp/multitenant_daemons $ /tmp/Java7R1_SR1/jre/bin/java 
-Xmt -Djavad.home=/tmp/multitenant_daemons -Xmx100M -Xlimit:netIO=5M-100M 
-DexampleProperty=1 -jar /tmp/hello.jar
What is your name?

下一部分从以下标准中读取信息:

String name = reader.readLine();

JVM 守护进程在 System.in 上拦截这一请求,并将一条消息发送至租户启动程序,表明它在等待用户输入。

收集调试信息

在应用程序等待用户输入期间,正是稍作停顿并了解如何获取运行应用程序相关信息的恰当时间,这对于调试会有所帮助。首先将租户启动程序映射到运行应用程序的 JVM 守护进程 (javad)。这种映射在希望生成 javacore 文件来了解 JVM 守护进程所进行的操作时非常有用。

使用 ps -ef 来查找与租户启动程序相对应的 JVM 守护进程的进程 ID。守护进程和启动程序可以相互映射,这是因为二者有相同的 -Djavad.home 目录。他们注入一个 SIGQUIT

dave /tmp/multitenant_daemons $ ps -ef | grep javad
dave 5092 3632 0 17:05 pts/1 00:00:00 /tmp/Java7R1_SR1/jre/bin/java -Xmt 
-Djavad.home=/tmp/multitenant_daemons -Xmx100M 
-Xlimit:netIO=5M-100M -DexampleProperty=1 -jar /tmp/hello.jar
dave 5094 5092 2 17:05 ? 00:00:01 /tmp/Java7R1_SR1/jre/bin/javad -Djavad.home=/tmp/multitenant_daemons
dave 5164 3543 0 17:07 pts/0 00:00:00 grep javad
dave /tmp/multitenant_daemons $ kill -QUIT 5094
dave /tmp/multitenant_daemons $

JVM 守护进程收到 SIGQUIT,并在 javad.options 文件中由 -Xdump 指定的目录内生成 javacore 文件。然后将 JVMDUMP 消息广播回租户启动程序:

dave /tmp/multitenant_daemons $ /tmp/Java7R1_SR1/jre/bin/java -Xmt 
-Djavad.home=/tmp/multitenant_daemons -Xmx100M -Xlimit:netIO=5M-100M 
-DexampleProperty=1 -jar /tmp/hello.jar
What is your name?
JVMDUMP039I Processing dump event "user", detail "" at 2014/06/26 17:07:20 - please wait.
JVMDUMP032I JVM requested Java dump using 
'/tmp/multitenant_daemons/javacore.5094.0001.txt' in response to an event
JVMDUMP010I Java dump written to /tmp/multitenant_daemons/javacore.5094.0001.txt
JVMDUMP013I Processed dump event "user", detail "".

所有 JVMDUMP 消息都广播到连接 JVM 守护进程的全部租户启动程序。因此,如果 JVM 守护进程失败,则会通知所有租户启动程序。

在识别了 JVM 守护进程 (javad) 并设置好 javad.options 文件中的转储选项之后,就可以在运行多租户 JVM 时使用这一方法来获取任何标准 JVM 诊断功能。

返回到执行

现在,返回到查看 main 方法的执行过程。输入 Dave,这是租户启动程序发送给 JVM 守护进程的代码。

JVM 守护进程将会收到 Dave,并将其定向至租户应用程序的 System.in

然后,租户应用程序将 Hello Dave 写入 System.out

System.out.println("Hello " + name);

JVM 守护进程将该写入操作拦截至 System.out,并通过套接字将其重新定向至租户启动程序。

租户启动程序收到 Hello Dave 并将其写出至自己的控制台。此时,main 方法已完成,而且租户进入了关闭阶段。

第 3 阶段:租户关闭

在关闭多租户框架中运行的应用程序和关闭多租户 JVM 下运行的应用程序期间,主要区别就是多租户注意到运行应用程序的 JVM 守护进程并不会随着 Java 应用程序而终止。

但是,从作为租户运行的 Java 应用程序的角度来看,行为时相同的:

  1. 应用程序等待期 nondaemon 线程直至终止。
  2. JVM 守护进程将会运行应用程序的关闭挂钩。
  3. JVM 守护进程将会终止应用程序的其余所有守护线程。
  4. 租户启动程序利用 Java 应用程序指定的退出代码终止。

第 4 步由 JVM 守护进程来完成,该进程将一个包含应用程序退出代码的消息发送至租户启动程序。这本示例中,租户启动程序是通过退出代码 0 来终止的:

dave /tmp/multitenant_daemons $ /tmp/Java7R1_SR1/jre/bin/java -Xmt -Djavad.home=
/tmp/multitenant_daemons -Xmx100M -Xlimit:netIO=5M-100M -DexampleProperty=1 -jar /tmp/hello.jar
What is your name?
JVMDUMP039I Processing dump event "user", detail "" at 2014/06/26 17:07:20 - please wait.
JVMDUMP032I JVM requested Java dump using '/tmp/multitenant_daemons/javacore.5094.0001.txt' 
in response to an event
JVMDUMP010I Java dump written to /tmp/multitenant_daemons/javacore.5094.0001.txt
JVMDUMP013I Processed dump event "user", detail "".
Dave
Hello Dave
dave /tmp/multitenant_daemons $ echo $?
0
dave /tmp/multitenant_daemons $

第 4 阶段:JVM 守护进程关闭

在默认情况下,JVM 守护进程会无限期地保持正常运行。但是,在本示例中,javad.options 文件指定了 -Djavavd.persistAfterTime=1。因此,JVM 守护进程在租户应用程序关闭一分钟后终止。如果在超时出现之前连接另一个租户启动程序,那么一分钟的计时器就会在没有其他租户相连时重新启动。

到此为止,我们结束了对租户生命周期的研究。下一节将介绍在同一 JVM 守护进程中运行的各个租住之间强制执行的隔离性。

所使用的隔离性

运行未改变或者变化非常有限的应用程序,这种能力就是通过隔离性实现的主要优势之一。

多租户 JVM 利用各个租户之间一定程度的隔离性来限制应用程序可以直接影响其他租户的程度。本节将通过可以自行编译并运行的代码示例来探讨这一关键特性的优势。要想运行这些示例,请 下载 受支持的 Java 代码和 Linux 脚本。

根据 “Java 多租户简介” 中的介绍,实现隔离性的一个主要因素是静态域隔离。为了帮助您在真实世界的应用程序中了解这一概念,我们首先在清单 3 中介绍了两个简单的应用程序。

清单 3. 两个简单的应用程序
public class StillHere extends ExampleBase {
   public static void main(String[] args) {
      while(notDone()) {
         println(args, "Still Here");
         sleep(SECONDS_2);
      }
      println(args, "Done");
   }
}

public class Goodbye extends ExampleBase {
   public static void main(String[] args) {
      println(args, "Hello");
      sleep(SECONDS_4);
      println(args, "About to exit, Goodbye");
      System.exit(-1);
   }
}

第一个应用程序每隔两秒打印一次 Still Here,直至完成为止。第二个应用程序打印 Hello,等待四秒,然后调用 System.exit()。以下是一些应用程序代表(明显的示例):

  • 在不断运行的基础上运行的应用程序,执行定期的任务
  • 执行一些处理然后终止的应用程序

当从以下命令行开始时,这两个应用程序在同一个 javad 进程中同时运行:

./java -Xmt -cp isolationExamples.jar StillHere app1 & 
./java -Xmt -cp isolationExamples.jar Goodbye app2

此外,您可以在定期的 JVM 中运行两个相同的应用程序。首先,将这两个应用程序放入包装程序类,如清单 4 所示。

清单 4. 在常规 JVM 中运行两个相同应用程序的包装程序类
public class HelloGoodBye extends StandardJVMRunner{
   public static void main(final String[] args) {
      Thread stillHereThread = new Thread() {
         public void run() { StillHere.main(APP1); }
      };
      
      Thread goodByeThread = new Thread() {
         public void run() { Goodbye.main(APP2); }
      };
      stillHereThread.start();
      goodByeThread.start();
   }
}

然后,通过以下命令行运行这两个应用程序:

./java -cp isolationExamples.jar HelloGoodBye

两种运行应用程序方法的主要区别就是隔离程度。同时运行不具有多租户特性的应用程序时,输出如下所示:

Run in normal JVM
app1 [Still Here]
app2 [Hello]
app1 [Still Here]
app2 [About to exit, Goodbye]
app1 [Still Here]
app1 [Still Here]

请注意,app2 调用 System.exit() 一结束,您就不会再看到 app1 的输出。之所以出现这种情况,是因为在 app2 中调用 System.exit() 造成了共享 JVM 进程终止,其中包括 app1 的终止。这就是一个应用程序能够直接影响另一个应用程序的极端势力。缺乏隔离性导致 Goodbye 应用程序终止 Still Here 应用程序。

现在,在多租户 JVM 下运行相同的两个应用程序:

Run in MT JVM
app1 [Still Here]
app2 [Hello]
app1 [Still Here]
app1 [Still Here]
app2 [About to exit, Goodbye]
dave /tmp/dave/apr16/jre/bin $ app1 [Still Here]
app1 [Still Here]
app1 [Still Here]
app1 [Still Here]
app1 [Still Here]
app1 [Done]

您可以看到,app1 在 app2 调用 System.exit() 之后继续运行,而且 app1 继续运行直至其正常终止并打印 Done。多租户 JVM 提供的隔离性使得 Goodbye 应用程序能够运行现有的代码并终止,而且不会影响同一进程中运行的其他应用程序。

由于多租户 JVM 共享单个进程,所以应用程序可以进行一些通常会影响 JVM 进程的调用,同时限制对应用程序本身的影响。System.exit() 是一个简单的示例,但同样的原理也适用于其他情况 — 关闭挂钩、system in/out 和其他许多情况。从该示例可以看出,这一功能限制了应用程序影响其他应用程序的能力,无需对应用程序本身进行更改。运行未改变或者变化非常有限的应用程序,这种能力是通过隔离性实现的主要优势之一。

再考虑一个较为复杂的示例:每两个应用程序都支持不同的(人类)语言。清单 5 显示了这两个应用程序。

清单 5. 使用不同语言的应用程序
 public class OutputInDefaultLocale extends ExampleBase {
   public static void main(String[] args) {
      while(notDone()) {
         Locale localeToUse = Locale.getDefault(); 
         ResourceBundle messages = ResourceBundle.getBundle("Messages",localeToUse);
         println(args, messages.getString( "HELLO_KEY"));
         sleep(SECONDS_2);
      }
      println(args, "Done");
   }
}
public class ChangeLocale extends ExampleBase {
   public static void main(String[] args) {
      try { Thread.sleep(SECONDS_4); } catch (Exception e) {};
      println(args, "Changing default locale from:" + Locale.getDefault());
      Locale.setDefault(new Locale("fr","CA"));
      println(args, "Locale is now:" + Locale.getDefault());
      OutputInDefaultLocale.main(args);
   }
}

绑定的文件是:

Messages.properties  -  content ? HELLO_KEY=Hello
Messages_fr.properties? content ? HELLO_KEY=Bonjour

第一个应用程序 (OutputInDefaultLocale) 每隔两秒就获取一次默认语言的语言环境和输出 Hello,直至完成为止。第二个应用程序 (ChangeLocale) 等待四秒并将默认语言环境更改为法语,这样,随后的写入操作 Hello 的结果就是 Bonjour

正如前面的示例所示,您可以作为租户同时运行这两个应用程序,只要使用包含以下命令行的多租户特性即可:

./java -Xmt -cp isolationExamples.jar OutputInDefaultLocale app1 & 
./java -Xmt -cp isolationExamples.jar ChangeLocale app2

此外,和以前一样,您还可以使用包装程序在标准 JVM 中运行应用程序。清单 6 显示了该包装程序。

清单 6. 运行 OutputInDefaultLocaleChangeLocale 应用程序的包装程序
public class LocaleIssues extends StandardJVMRunner {
   public static void main(final String[] args) {
      Thread outputInDefaultLocalThread = new Thread() {
         public void run() { OutputInDefaultLocale.main(APP1); }
      };
      
      Thread changeLocaleThread = new Thread() {
         public void run() { ChangeLocale.main(APP2); }
      };
      
      outputInDefaultLocalThread.start();
      changeLocaleThread.start();
   }
}

运行不具有多租户特性的应用程序的命令行如下所示:

./java -cp isolationExamples.jar LocaleIssues

如果不具有多租户特性,那么输出就是:

Run in normal JVM
app1 [Hello]
app1 [Hello]
app2 [Changing default locale from:en_US]
app2 [Locale is now:fr_CA]
app2 [Bonjour]
app1 [Bonjour]
app2 [Bonjour]
app1 [Bonjour]
app2 [Bonjour]
app1 [Bonjour]
app2 [Done]
app1 [Done]

第一个应用程序开始运行,而且输出为英语形式,这是因为默认语言环境为 en_US。第二个应用程序将完成其四秒的等待,然后将语言环境更改为法语 fr_CA。此后,两个应用程序的输出都是法语。等待 — 这并不是我们想要的!但在这一切发生之时,等待却很有意义,这是因为两个应用程序共享了一个 JVM,而且只有一个默认语言环境。

现在,运行具有多租户特性的相同应用程序:

Run in MT JVM
app1 [Hello]
app1 [Hello]
app2 [Changing default locale from:en_US]
app2 [Locale is now:fr_CA]
app2 [Bonjour]
app1 [Hello]
app2 [Bonjour]
app1 [Hello]
app2 [Bonjour]
app1 [Hello]
app1 [Done]
app2 [Bonjour]
app2 [Done]

在第二个应用程序更改默认语言环境之后,第一个应用程序继续以英语形式输出,而第二个应用程序将以法语形式输出。这就是我们想要的结果。多租户 JVM 提供的隔离性能够使得两个应用程序共享同一进程,而且行为正确,不需要更改应用程序。在这种情况下,默认语言环境存储在静态域中,而多租户特性提供的静态隔离性能够使得每一个应用程序(租户)都使用自己的语言环境。

结束语

本文是 “Java 多租户简介” 的后续文章,详细介绍了多租户 JVM 及其操作。现在,您应该已经对租户应用程序的生命周期有了更深入的理解,而且对静态隔离性提供的优势有了更好的了解。

我们鼓励您下载多租户 JVM、试用一下演示、获取文档,并编写您自己的应用程序。您还可以加入 IBM 多租户 JVM 社区,随时关注最新的可用信息,并为我们提供反馈,帮助我们掌握这一技术的方向。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Cloud computing
ArticleID=983546
ArticleTitle=Java 多租户:配置选项、租户生命周期和所使用的隔离性
publish-date=10092014