多租户数据隔离_Java多租户:配置选项,租户生命周期和实际隔离

Java多租户:深入探讨数据隔离与生命周期
本文详细介绍了IBM SDK Java Technology Edition,版本7中的多租户JVM功能,强调了它如何提供更好的性能和隔离。文章涵盖了租户生命周期的四个阶段,展示了如何配置选项以实现静态字段隔离,并通过示例应用程序演示了多租户环境中的资源隔离。通过多租户JVM,应用程序可以独立运行,避免相互影响,提高了系统的稳定性和安全性。

多租户JVM在IBM SDK Java Technology Edition,版本7发行版1中作为技术预览提供。通过使用此功能,应用程序部署可以实现比共享传统JVM更好的性能和隔离。 先前的developerWorks文章“ Java多租户简介 ”提供了以下方面的高级概述:

  • 多租户JVM的收益和成本
  • 如何使用多租户JVM
  • 如何实现静态场的隔离
  • 通过资源约束进行资源隔离,称为资源消耗管理(RCM)

该文章强调了多租户框架的简单性。 要将Java应用程序作为租户在多租户JVM中运行,您只需向命令行添加-Xmt ,如下所示:

./java -Xmt Hello

本文将更详细地研究多租户框架的两个领域。 首先,我们遍历租户生命周期,以使您对应用程序在多租户JVM中的运行方式有更深入的了解。 在演练期间,我们还将在框架中引入以下配置选项:

  • 将选项传递到租户应用程序中
  • 将选项传递给运行租户应用程序的JVM守护进程( javad
  • 将租户应用程序定位到特定的JVM守护进程

其次,我们演示了在运行的应用程序中隔离静态的好处。

示例应用程序和配置

我们将使用一个示例应用程序(您可以编译并运行该应用程序)来演示配置选项并经历生命周期。 我们还提供了用于运行应用程序的命令行以及用于配置JVM守护进程的javad.options文件。 (请参阅下载以获取本文的所有示例代码,命令和脚本。)

清单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 MB的网络I / O(MBps),但将租户限制为10MBps。 如果JVM守护程序无法保留此最小带宽,则会将一条错误消息传递给租户启动器,并且不会启动租户应用程序。
-DexampleProperty=1
使此Java属性仅对该租户可见。 在JVM守护程序进程的此实例下运行的所有其他租户都看不到它。

传递JVM守护程序本身的选项的唯一方法是通过javad.options文件。 清单2显示了将用于运行示例应用程序的JVM守护程序使用的javad.options文件。

清单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 / -Djavad.home=/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写入,然后通过套接字将其重定向到租户启动器。 承租人启动器写道:“您叫什么名字?” 到其控制台:

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并生成在由指定的目录中一个javacore文件-Xdump在javad.options文件。 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. 应用程序等待其非守护进程线程终止。
  2. JVM守护程序运行应用程序的关闭挂钩。
  3. JVM守护程序终止应用程序的所有剩余守护程序线程。
  4. 租户启动程序以Java应用程序指定的退出代码终止。

第四步是通过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]

如您所见,在app2调用System.exit() ,app1继续运行,而app1继续运行,直到其正常终止并显示Done为止。 多租户JVM提供的隔离使Goodbye应用程序可以运行现有代码并终止,而不会影响在同一进程中运行的其他应用程序。

由于多租户JVM共享一个进程,因此应用程序可以进行通常会影响JVM进程的调用,同时限制对应用程序本身的影响。 System.exit()是一个简单的示例,但是在其他情况下(关机钩子,系统输入/输出以及许多其他情况System.exit() ,该原理也适用。 从示例中可以看到,此功能限制了一个应用程序影响其他应用程序的能力,而无需更改应用程序本身。 通过隔离,可以保持应用程序不变或进行非常有限的更改,这是其主要优势之一。

考虑一个稍微复杂的示例:在两个应用程序中的每一个中都支持一种不同的(人类)语言。 清单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提供的隔离使两个应用程序可以以正确的方式共享同一进程,而无需更改应用程序。 在这种情况下,默认语言环境存储在静态环境中,多租户功能提供的静态隔离使每个应用程序(租户)都可以使用自己的语言环境。

结论

本文将更详细地研究多租户JVM及其操作,作为“ Java多租户简介 ”的后续文章。 现在,您对租户应用程序的生命周期有了更深入的了解,并且对隔离静电所带来的好处有了更好的了解。

我们鼓励您下载多租户JVM,试用演示,获取文档并编写自己的应用程序。 您还可以加入IBM多租户JVM社区,以了解最新的可用信息,并向我们提供反馈,以帮助确定技术的方向。


翻译自: https://www.ibm.com/developerworks/java/library/j-multitenant-java2/index.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值