OpenTerracotta简介

Java应用程序在单个JVM中运行时,最容易编写和测试。 但是,使应用程序具有可伸缩性和高度可用性的要求迫使Java应用程序必须在多个JVM上运行。 在本文中,我们介绍了OpenTerracotta,这是一种企业级的开源JVM级别的集群解决方案。

JVM级集群通过使应用程序可以部署在多个JVM上,而又像在同一JVM上运行一样相互交互,从而简化了企业Java。

在单个JVM中,线程通过对堆上的对象进行更改并通过语言级别的并发原语相互交互:“ synchronized”关键字和Object方法等待,通知和notifyAll。 Open Terracotta允许JVM群集中的线程使用扩展为在整个群集范围内具有的内置JVM功能,跨JVM边界彼此交互。 这些集群功能会在运行时注入到应用程序类的字节码中,因此无需编码为特殊的集群API。

使用这些群集工具,兵马俑最常用于以下情况:

  • HTTP会话复制
  • 分布式缓存
  • POJO集群/ Spring集成
  • 协作,协调和活动

一个简单的例子

为了具体说明所有这些含义,让我们从一些示例代码入手,您可以将这些代码插入其中。 示例域是一个零售系统,其中包含可以添加到客户的购物车中的产品目录。 在任何时候,都可以通过例如管理或报告控制台查看活动购物车集。

该示例代码是使用简单的Java数据结构编写的。 为了简单起见,对某些问题域进行了理想化处理。 例如,封装在真实系统中的产品,目录,客户和订单类中的业务数据很可能由关系数据库支持,可能还带有某种对象关系系统。 但是,临时购物车数据最好完全表示为没有记录支持系统的简单Java对象。

Product类包含有关特定产品的数据:产品名称,SKU和价格:

package example;
import java.text.NumberFormat;
public class ProductImpl implements Product {
private String name;
private String sku;
private double price;

public ProductImpl(String sku, String name, double price) {
this .sku = sku;
this .name = name;
this .price = price;
}

public String getName() {
return this .name;
}

public String getSKU() {
return this .sku;
}

public synchronized void increasePrice( double rate) {
this .price += this .price * rate;
}
}

产品保存在目录中,该目录将产品的SKU映射到产品对象。 该目录可用于显示产品并通过SKU查找产品,以便将其放置在购物车中。 这是目录:

package example;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class Catalog {

private final Map<String, Product> catalog;

public Catalog() {
this.catalog = new HashMap<String, Product>();
}

public Product getProductBySKU(String sku) {
synchronized (this.catalog) {
Product product = this.catalog.get(sku);
if (product == null) {
product = new NullProduct();
}
return product;
}
}

public Iterator<Product> getProducts() {
synchronized (this.catalog) {
return new ArrayList<Product>>(this.catalog.values()).iterator();
}
}

public int getProductCount() {
synchronized (this.catalog) {
return this.catalog.size();
}
}

public void putProduct(Product product) {
synchronized (this.catalog) {
this.catalog.put(product.getSKU(), product);
}
}

}

ShoppingCart类包含购物者浏览过并暂时想购买的产品列表:

package example;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class ShoppingCartImpl implements ShoppingCart {

private List<Product> products = new LinkedList<Product>();

public void addProduct(final Product product) {
synchronized (products) {
this.products.add(product);
}
}
}

ShoppingCart有一个名为ActiveShoppingCarts的伴随类,它对活动购物车进行一些记账:

package example;
import java.util.LinkedList;
import java.util.List;

public class ActiveShoppingCarts {

private final List<ShoppingCart> activeShoppingCarts
= new LinkedList<ShoppingCart>();

public void addShoppingCart(ShoppingCart cart) {
synchronized (activeShoppingCarts) {
this.activeShoppingCarts.add(cart);
}
}

public List getActiveShoppingCarts() {
synchronized (this.activeShoppingCarts) {
List<ShoppingCart> carts
= new LinkedList<ShoppingCart>(this.activeShoppingCarts);
return carts;
}
}
}

出于封装的目的,有一个名为Roots的类,该类保存对该应用程序使用的群集对象图的根的引用。 不需要将根字段放在特殊的类中,但是对于此示例很方便:

package example;

import
java.util.concurrent.CyclicBarrier;
public class Roots {
private final CyclicBarrier barrier;
private final Catalog catalog;
private final ActiveShoppingCarts activeShoppingCarts;

public Roots(CyclicBarrier barrier, Catalog catalog,
ActiveShoppingCarts activeShoppingCarts) {
this .barrier = barrier;
this .catalog = catalog;
this .activeShoppingCarts = activeShoppingCarts;
}

public ActiveShoppingCarts getActiveShoppingCarts() {
return activeShoppingCarts;
}

public CyclicBarrier getBarrier() {
return barrier;
}

public Catalog getCatalog() {
return catalog;
}
}

以下代码显示了如何在多线程环境中使用这些类。 假设有两个线程进入run()方法,并且它们在各个点使用CyclicBarrier相互协调。 这个示例使用模式是完全人为设计的,但是您可以想象一下该代码如何在实际应用程序中工作:

package example;

import java.util.Iterator;
import java.util.concurrent.CyclicBarrier;

public class Main implements Runnable {

private final CyclicBarrier barrier;
private final int participants;
private int arrival = -1;
private Catalog catalog;
private ShoppingCartFactory shoppingCartFactory;
private ActiveShoppingCarts activeCarts;

public Main(int participants, CyclicBarrier barrier, Catalog catalog, ActiveShoppingCarts activeCarts,
ShoppingCartFactory shoppingCartFactory) {
this.barrier = barrier;
this.participants = participants;
this.catalog = catalog;
this.activeCarts = activeCarts;
this.shoppingCartFactory = shoppingCartFactory;
}

public void run() {
try {
display("Step 1: Waiting for everyone to arrive. I'm expecting " + (participants - 1) + " other thread(s)...");
this.arrival = barrier.await();
display("We're all here!");

String skuToPurchase;
String firstname, lastname;

display();
display("Step 2: Set Up");
boolean firstThread = arrival == (participants - 1);

if (firstThread) {
display("I'm the first thread, so I'm going to populate the catalog...");
Product razor = new ProductImpl("123", "14 blade super razor", 12);
catalog.putProduct(razor);

Product shavingCream = new ProductImpl("456", "Super-smooth shaving cream", 5);
catalog.putProduct(shavingCream);

// I'm going to be John Doe and I'm going to buy the razor
skuToPurchase = "123";
firstname = "John";
lastname = "Doe";
} else {
// I'm going to be Jane Doe and I'm going to buy the shaving cream...
skuToPurchase = "456";
firstname = "Jane";
lastname = "Doe";
}

// wait for all threads.
barrier.await();

display();
display("Step 3: Let's do a little shopping...");
ShoppingCart cart = shoppingCartFactory.newShoppingCart();

Product product = catalog.getProductBySKU(skuToPurchase);
display("I'm adding \"" + product + "\" to my cart...");
cart.addProduct(product);
barrier.await();
display();
display("Step 4: Let's look at all shopping carts in all JVMs...");
displayShoppingCarts();

display();
if (firstThread) {
display("Step 5: Let's make a 10% price increase...");
for (Iterator i = catalog.getProducts(); i.hasNext();) {
Product p = i.next();
p.increasePrice(0.1d);
}
} else {
display("Step 5: Let's wait for the other JVM to make a price change...");
}
barrier.await();
display();
display("Step 6: Let's look at the shopping carts with the new prices...");
displayShoppingCarts();

} catch (Exception e) {
// You wouldn't really do this here.
throw new RuntimeException(e);
}
}

// ... setup and convenience code omitted

public static void main(String[] args) throws Exception {
int participants = 2;
if (args.length > 0) {
participants = Integer.parseInt(args[0]);
}

Roots roots = new Roots(new CyclicBarrier(participants), new Catalog(), new ActiveShoppingCarts());
if (args.length > 1 && "run-locally".equals(args[1])) {
// Run 'participants' number of local threads. This is the non-clustered
// case.
for (int i = 0; i < participants; i++) {
new Thread(new Main(participants, roots.getBarrier(), roots.getCatalog(), roots.getActiveShoppingCarts(),
new ShoppingCartFactory(roots.getActiveShoppingCarts()))).start();
}
} else {
// Run a single local thread. This is the clustered case. It is assumed that main() will be called
// participants - 1 times in other JVMs
new Main(participants, roots.getBarrier(), roots.getCatalog(), roots.getActiveShoppingCarts(),
new ShoppingCartFactory(roots.getActiveShoppingCarts())).run();
}

}

}

到目前为止,此代码在单个JVM的上下文中运行良好。 多个线程与目录,产品,ShoppingCart,客户和订单对象作为简单的POJO进行交互,并且可以使用标准Java库util.concurrent类(即CyclicBarrier)相互协调(如果需要)。

但是,如果这不仅仅是一个示例应用程序,我们希望将其部署在至少两个物理服务器上以实现高可用性,并可以选择添加其他服务器以实现可扩展性,因为使用率随着时间的增长而增加。 添加服务器会导致出现在单个JVM部署方案中不存在的许多需求:

  • 所有活跃的购物车在所有JVM中都应该可用,这样浏览的客户请求可以发送到任何服务器,而不会丢失该客户购物车中的物品。
  • 要查看所有活动购物车,将需要访问每个JVM中的所有活动购物车。
  • 使用CyclicBarrier在示例代码中表达的线程交互必须扩展到多个JVM中的线程。
  • 如果目录数据足够大,则可能无法容纳在RAM中。 可以根据需要从产品数据库中检索它,但是该数据库将成为瓶颈。 如果使用缓存来减轻数据库瓶颈,则每个JVM将需要访问该缓存。 为了避免数据库使用率出现严重的高峰,应从数据库中一次加载缓存并在JVM之间共享缓存,而不是由每个JVM分别加载。

通过使用少量配置且无需更改代码的Open Terracotta,可以满足在群集中部署应用程序所引入的所有要求。 让我们快速了解一下实现此目标的配置。

示例配置

第一步配置是确定集群中应共享应用程序中的哪些对象。 通过声明特定变量为“根”来指定这些共享库图。 从根对象通过引用可访问的每个对象都变为一个共享对象,该共享对象可用于群集范围内的所有JVM。 到目前为止,在我们的示例中,我们具有三个根,所有根都在Roots类中声明。 这是在Terracotta配置中指定的,如下所示:

<roots>
<root>
<field-name>example.Roots.barrier</field-name>
</root>
<root>
<field-name>example.Roots.catalog</field-name>
</root>
<root>
<field-name>example.Roots.activeShoppingCarts</field-name>
</root>
</roots>

下一步配置是确定在加载时应该对哪些类的字节码进行检测。 成为共享对象图一部分的任何对象的类在加载时都必须由Terracotta对其字节码进行检测。 该检测过程是如何将Terracotta的透明集群功能注入到应用程序中的。 对于此示例,我们要做的就是将所有内容都包含在example。*包中。 Terracotta自动包含CyclicBarrier类,因为它是需要检测的Java库类核心集合的一部分。 Instrumented-classes配置部分如下所示:

<instrumented-classes>
<include>
<!--include all classes in the example package for bytecode instrumentation-->
<class-expression>example..*</class-expression>
</include>
</instrumented-classes>

最后的配置步骤是确定哪些方法应将感知群集的并发语义注入其中。 就本示例而言,我们将使用包含所有包含的类中的所有方法的正则表达式进行“自动锁定”:

<locks>
<autolock>
<method-expression>void *..*(..)</method-expression>
<lock-level>write</lock-level>
</autolock>
</locks>

此配置将指示Terracotta在所检测的每个类的方法中查找所有同步的方法和块以及对wait()和notify()的所有调用,并使它们具有在整个集群范围内的含义。

运行示例

您可以在以下示例中获取源代码:

http://wiki.terracotta.org/confluence/display/labs/CatalogExample

配置完成后,可以从命令行或从Eclipse插件完成在集群中运行应用程序。 在本文中,我们将使用Eclipse,但在线教程和下载工具包中有多个从命令行启动Terracotta的示例。 第一步是启动Terracotta服务器。 在Eclipse中,这可以从Terracotta菜单中完成。

服务器启动后,即可启动应用程序的实例。 对于此示例,我们必须运行Main类两次:

启动第一个应用程序实例时,您应该在输出控制台中看到以下内容:

2007-01-18 15:49:42,204 INFO - Terracotta, version 2.2 as of 20061201-071248.
2007-01-18 15:49:42,811 INFO - Configuration loaded from the file at
'/Users/orion/Documents/workspace/TerracottaExample/tc-config.xml'.
2007-01-18 15:49:42,837 INFO - Log file:
'/Users/orion/Documents/workspace/TerracottaExample/terracotta/client-logs/terracotta-client.log'.
Waiting for everyone to arrive. I'm expecting 1 other thread(s)...

这表明第一个实例中的主线程在barrier.await()处阻塞。 由于屏障是共享对象,因此主线程将一直阻塞,直到此JVM或Terracotta群集中的另一个JVM中的另一个线程调用barrier.await()为止。 然后,将允许两个线程继续进行。 如果我们启动另一个应用程序实例,将会发生这种情况。 启动第二个应用程序实例后,您应该在控制台中看到类似以下输出的内容(根据线程到达各个障碍点的顺序,您可能会看到略有不同的输出):

步骤1:等待所有人到达。 我期待其他1个线程...我们都在这里!

步骤4:让我们看一下所有JVM中的所有购物车...

=========================

购物车
项目1:价格:$ 12.00; 名称:14刀片超级剃须刀
=========================
购物车
项目1:价格:5.00美元; 名称:超滑剃须膏

第5步:让我们将价格提高10%...

第6步:让我们看看具有新价格的购物车...

=========================

购物车
物品1:价格:$ 13.20; 名称:14刀片超级剃须刀
=========================
购物车
项目1:价格:5.50美元; 名称:超滑剃须膏

在另一个应用程序实例的控制台输出中,您应该看到类似以下内容:

步骤1:等待所有人到达。 我期待其他1个线程...我们都在这里!

步骤2:设定

步骤4:让我们看一下所有JVM中的所有购物车...

=========================

购物车
项目1:价格:$ 12.00; 名称:14刀片超级剃须刀
=========================
购物车
项目1:价格:5.00美元; 名称:超滑剃须膏

步骤5:让我们等待其他JVM进行价格更改...

第6步:让我们看看具有新价格的购物车...

=========================

购物车
物品1:价格:$ 13.20; 名称:14刀片超级剃须刀
=========================
购物车
项目1:价格:5.50美元; 名称:超滑剃须膏

在步骤1中,不同应用程序实例中的两个线程正在等待彼此启动并到达同一集合点。 在步骤2中,第一个线程创建了Product对象,并将它们添加到集群Catalog中。 在步骤3中,每个线程通过SKU将产品对象从群集目录中拉出,请注意,一个JVM中的第一个线程添加的产品可在另一个JVM中的目录中自动获得。 在步骤4中,两个线程都遍历所有JVM中的所有活动购物车并打印内容。 在第5步中,第一个线程通过遍历目录中的所有产品而价格上涨了10%,而第二个线程处于阻塞状态。 在步骤6中,两个线程都显示所有活动的购物车-注意,通过操纵目录在一个线程中完成了价格上涨,新价格自动反映在两个JVM中的所有购物车中。

可以在Terracotta管理控制台中实时查看所有群集对象。 每个根都可以视为原始图元和引用的树。 这是目录的视图:

您可以在“目录” HashMap中看到Product对象。 您还可以看到购物车引用的相同产品对象:

此视图显示ShoppingCart中LinkedList引用的产品。

秦始皇兵马俑如何工作?

既然我们已经看到了Terracotta的透明集群在起作用,那么让我们讨论其工作原理的细节。

建筑

Terracotta使用中心辐射型架构,其中运行集群应用程序的JVM在启动时连接到中央Terracotta服务器。 Terracotta服务器存储对象数据并协调JVM之间的线程并发。 应用程序JVM中的Terracotta DSO库在类加载时处理字节码检测,并在同步边界处传输对象数据以及锁定和解锁请求,以及应用程序JVM和Terracotta之间的wait()notify()请求服务器在运行时。

集群注入和字节码检测

通过在将应用程序类加载到JVM中时检测它们的字节码,将集群行为注入到应用程序中。 Terracotta使用的字节码注入技术类似于在许多面向方面的编程框架(如AspectJ和AspectWerkz)中发现的字节码注入技术。 类的字节码在加载时被拦截,并由Terracotta透明库检查。 然后在将字节码传递给JVM作为类之前,根据配置对其进行了修改。

为了保持对象更改,PUTFIELD和GETFIELD字节码指令被重载。 捕获了PUTFIELD指令,以记录对集群对象字段的更改。 如果GETFIELD字节码指令尚未从服务器中检索相关字段所引用的对象并将其实例化到堆上,则它会根据需要从服务器中检索对象数据。

为了管理线程协调,MONITORENTER和MONITOREXIT字节码指令以及各种Object.wait()Object.notify()方法的INVOKEVIRTUAL指令都已重载。 MONITORENTER表示对对象监视器的线程请求。 线程将阻塞此指令,直到被授予对该对象的锁定。 授予锁定后,该线程将对该对象持有排他锁,直到对该对象执行MONITOREXIT指令为止。 如果有问题的对象碰巧是集群锁,则Terracotta确保除了请求对该对象的本地锁之外,线程还将阻塞,直到它收到该对象的排他的集群范围的锁为止。 当线程释放本地锁时,它将释放相应的群集范围锁。

在示例代码中,example。*包中的所有同步方法和同步块均配置为“自动锁定”,这意味着将对MONITORENTER和MONITOREXIT指令进行扩充。 还可以在Terracotta配置中将方法声明为锁定方法,以便将集群添加到尚未进行显式同步的应用程序代码中。

还检测了wait()notify()方法的调用站点。 在共享对象上调用wait()方法时,Terracotta服务器将调用线程添加到等待该对象的整个群集中的线程集中。 当调用notify()方法之一时,服务器确保通知集群中适当数量的线程。 在一个JVM上调用notify()时 ,服务器选择一个等待线程(如果有),并导致该线程被通知。 调用notifyAll()时 ,服务器将通知所有在JVM上等待该对象的线程。

根和聚类对象图

群集对象始于共享对象图的根。 根由一组特定的一个或多个字段标识,并在Terracotta配置中用唯一名称声明。 在该示例中,所有根均在Roots类中声明。 这是为了方便起见,任何字段都可以声明为根​​。

首次实例化根时,根对象和从顶层根对象通过引用可访问的所有对象将成为群集对象。 他们的现场数据被传送到Terracotta服务器并由其存储。 在任何JVM中创建根之后,对该字段的所有其他分配都将被忽略,而将群集根对象的值分配给该字段。 在第二个应用程序实例创建Roots对象的示例中,会发生这种情况。 在Roots构造函数中对根字段所做的分配将被忽略,因为这些根已经由第一个应用程序实例创建。 Terracotta透明库没有像源代码那样为根字段分配在构造函数中传递的参数的值,而是从服务器检索根,在本地堆上实例化它,并将对它的引用分配给根变量问题。 这是Terracotta透明机制对应用程序语义所做的唯一重大更改。

当非群集对象变得可从群集对象引用时,新对象和新对象可通过引用到达的整个对象图将成为群集对象。 对象聚类后,将为其分配一个群集范围内的唯一对象ID,并在其生命周期的剩余时间内保持聚类。 当某个对象变得无法通过任何根图访问,并且在任何集群JVM上都没有该对象的实例时,则可以由服务器中的集群垃圾收集器删除该对象。

同步,群集锁和对象更改

同步方法,同步块和声明要在Terracotta配置中锁定的方法提供了Terracotta事务的边界。 Terracotta事务的概念与JTA事务有些不同。 它与Java内存模型中使用的事务非常相似。

如前所述,将共享对象上的MONITORENTER指令扩展为集群锁请求,以便调用线程将一直阻塞,直到为该对象授予本地锁和集群锁为止。 Terracotta会在本地事务记录中收集在MONITORENTER和相应的MONITOREXIT之间进行的所有对象更改。 Terracotta保证,在允许线程继续执行MONITORENTER指令之前,所有与集群JVM中的特定对象的锁关联的所有事务中所做的所有更改都将在本地应用。 事务可以包含对任何对象的更改,而不仅是其锁与该事务相关联的对象。

细粒度的更改复制

包含对象更改的事务仅包含已更改字段的数据。 这些事务被发送到服务器和其他群集的JVM,以保持群集的一致性。 服务器仅将事务发送到具有在事务中表示的堆上实例化的对象的其他JVM。 同样,它仅将事务的一部分发送到必须应用的JVM。 例如,如果线程对对象'a'中的字段'p'和对象'b'中的字段'q'进行了更改,则仅ap的字段数据和bp的字段数据被放入事务中并发送到服务器。 服务器确定哪些其他JVM具有a或b的实例。 如果另一个JVM具有对象a的实例但没有对象b的实例,则它将接收ap的字段数据,但不接收bq的字段数据。

在我们的示例代码中,当在Product对象上更新价格时,只有价格字段被运送到集群,而不是名称字段或SKU字段,价格字段没有变化。

对象标识和序列化

因为对象更改是在字段级别跟踪的,并且事务包含对象片段而不是整个对象图,所以Terracotta不使用Java序列化来复制对象更改。 在该示例中,当我们提高Product对象的价格时,我们需要运送到集群中的唯一东西是更改的对象的对象ID,该对象上已更改的字段的标识符以及包含价格字段。 产品对象的其余部分将被忽略。 使用序列化将序列化Product对象的每个字段,并且,如果Product对象是一个深图,其中包含对其他对象的大量引用,而其他对象又具有对其他对象的引用,则Java序列化将对整个深图进行序列化-所有更改为双精度值。

这种方法比通过序列化进行复制要有效得多,因为它只在集群中移动已更改的数据,而不是整个序列化的对象图。 但是,除了效率之外,使用对象字段作为更改单位还有一个关键的体系结构优势:对象标识的保留。

如果使用Java序列化在集群中移动更改,则更改后的对象将在集群应用程序的JVM中反序列化,并且必须以某种方式替换现有的对象实例。 这就是为什么许多其他群集和缓存技术都需要GET / PUT API的原因,其中必须使用某种“ GET”调用从群集中检索群集对象,并且当对该对象进行更改时,必须将其放回到通过一些“ PUT”调用进行集群。

兵马俑没有这样的限制。 集群对象就像其他所有对象一样都驻留在堆上。 在本地对该对象进行更改时,将对堆上的对象进行更改。 当对该对象进行远程更改时,该事务将由本地JVM接收,并直接应用于已在堆上的现有对象。 这意味着在给定的时间堆上只有一个群集对象的实例(当使用多个类加载器时,此图会变得稍微复杂一些,但这超出了本文的范围)。

使用Terracotta,您不必记住要获得对象的新副本,也不必记住在完成处理后将其放回去。 而且,因为没有副本,所以群集对象只是堆对象上的普通对象,其行为与其他任何对象一样:您对群集对象所做的任何更改都可用于引用更改的对象。目的。 同样,对于对象'bar'的引用'foo'和对同一对象'bar'的引用'baz',则foo == baz是true,而不仅仅是foo.equals(baz)

在我们的示例中,通过更改目录中找到的Product对象的价格时,购物车中找到的Product对象的价格也发生了更改,您可以看到工作对象标识的保留。 这是因为目录中的产品对象与购物车中的产品在堆上的对象相同。

对象身份的这种保留使群集的多JVM应用程序的行为更像常规的单JVM应用程序。 这种简单性和跨集群保存对象身份的功能使集群问题与设计和实现应用程序的问题正交。 集群行为被下推到JVM级别的Terracotta层,并融化到基础架构中。 就像垃圾回收允许内存管理从应用程序代码中消失一样,Terracotta允许群集和分布式计算行为也消失。

虚拟堆/网络附加内存

除了在JVM之间共享对象和发出信号线程的功能外,Terracotta还允许将本地JVM堆有效地用于非常大的对象图。 随着共享对象图的增长,它可能无法舒适地容纳在单个JVM的堆中。 Terracotta通过根据共享实例图的本地使用实例的使用模式来对其进行修剪来处理该实例。 Terracotta在集群对象图上保留了一个可配置的窗口,以便根据缓存策略将不适合特定百分比堆的块清除掉。 由于需要这些缺失的部分,因此它们会自动从服务器故障转移到JVM中。 您可以将Terracotta群集视为任意大的虚拟堆或网络连接的内存。

此功能允许任意大的对象图适合标准堆大小。 它还允许灵活的运行时数据分区。 在我们的示例代码中,您可以想象如果目录变得非常大而拥有成千上万的产品以及千兆字节的产品数据,将会发生什么。 要填充如此庞大的目录可能需要几分钟或几小时。 如果没有Terracotta,要使其全部适应单个JVM的堆,可能需要具有4GB以上RAM的64位OS。 并且,要获得高可用性,您至少需要两台这样的应用程序服务器计算机。 为了获得可伸缩性,您需要添加许多此类应用程序服务器计算机。 添加的每个不带Terracotta的应用程序服务器计算机都需要独立加载目录。

因为Terracotta充当网络附加的内存,所以您可以将整个Catalog(无论它有多大)都适合到单个群集对象图中。 该目录仅需填充一次,从而大大减少了其他应用程序实例的启动时间,但立即可用于集群的每个成员。

您如何以及何时使用兵马俑?

在兵马俑最有效的四个主要用例中:

  • HTTP会话复制
  • 分布式缓存
  • POJO聚类
  • 协作,协调和活动

HTTP会话复制

也许最熟悉的用例是在带有粘性负载平衡器的情况下的多应用程序服务器环境中进行HTTP会话复制。 在应用程序服务器发生故障的情况下,保持会话在应用程序服务器之间的可用性一直是一个昂贵且难以解决的问题。 应用程序设计的当前趋势已经从在会话中存储应用程序状态(如购物车)转移到了将应用程序状态存储在某些外部系统(如数据库)中的所谓“无状态”应用程序设计。

这种“无状态”方法并不是真正的无状态-应用程序状态尚未消失,只是从应用程序外部化了。 这有两个不幸的副作用。 首先是性能:将应用程序状态写入数据库会使该数据库成为瓶颈,因此通过添加应用程序服务器来扩展集群的收益将减少。 第二个问题是应用程序的编程体系结构受到外部化应用程序状态所需的API的污染。

将会话范围的数据作为简单的Java对象放入HTTP会话要容易得多。 这就是它最初设计的目的。 Terracotta会话复制通过将应用程序会话数据保留在何处以及以何种形式保留,从而使Web应用程序的软件体系结构保持简单。

Terracotta会话复制通过允许任何应用程序服务器访问任何活动会话(无论该会话在何处创建)来提供高可用性。 它可以很好地扩展,因为仅复制会话中已更改的数据,并且仅将其发送到需要的地方。 如果会话中没有数据更改,则任何地方都不会写入数据。 如果在会话中更改了一个字节,则仅将该字节(而不是整个会话图)发送到Terracotta服务器。 如果没有其他应用程序服务器在堆中拥有该会话(这是粘性负载均衡器所面向的群集的常见情况),则不会向其他应用程序服务器发送更改。 仅在应用程序服务器发生故障的情况下,会话数据才会被复制到另一台应用程序服务器,并且仅在需要时才会发生。

由于Terracotta集群了常规的Java类,因此您不必将会话数据分割为人为分割的属性。 您的会话对象可以与您的应用程序一样简单或复杂。 您可以更新深层对象图,并且这些更改将对集群可用,而无需记住在Session上调用setAttribute() 。 由于Terracotta在不使用Java序列化的情况下对对象进行集群,因此您可以将未实现Serializable的对象放入会话中。 例如,购物车可以按原样放入会话中,而不必将其切成单独的属性以进行有效复制。 从应用程序在集群中任何位置的任何地方对购物车所做的更改都会自动反映出来,而无需调用setAttribute()

在集群运行时,您可以查看整个集群中所有会话的内容。 在开发时,这已被用来发现会话的滥用情况,这些意外事件本不应将其放置在会话图中的对象意外插入其中。在生产中,您可以立即查看有多少活动会话并观察它们的变化。即时的。 例如,您可以查看从Terracotta控制台中看到的人们正在放入购物车中的内容。

Terracotta使用许多流行的Web框架,例如Struts,Spring Web Flow和Wicket。

POJO和Spring聚类

示例应用程序中的所有对象都是简单的POJO(普通的旧Java对象)。 Terracotta使在集群中使用POJO就像在单个JVM中使用POJO一样简单。 如果您的应用程序使用Spring Bean,则尤其如此。

Terracotta for Spring通过启用群集的Spring bean来保持群集中Spring框架的非侵入性。 您可以照常开发单JVM Spring应用程序,然后定义要集群的那些Spring应用程序上下文和那些Bean。 Terracotta将透明地对这些bean和应用程序上下文事件进行群集,并且在整个群集中的语义与在单个JVM上的语义相同。 Terracotta for Spring还包含对Spring Web Flow和延续的支持。 当该功能托管在Terracotta群集上时,此功能可启用Spring Web应用程序的会话状态故障转移。

分布式缓存

群集对象图自然可以构成良好的分布式缓存。 如果本例中的目录数据是从数据库加载的,则为整个集群填充一次目录,而不是为集群中的每个应用程序实例填充一次目录,这会减少启动时数据库的负载。 因为整个缓存适合集群数据结构,所以应用程序实例不必转储缓存中不适合堆的部分,然后在再次需要时从数据库中重新加载该数据。 因为Terracotta保留了对象身份,所以可以使用标准Java库数据结构语义以最简单的方式来更新目录数据,并且这些更新对于整个集群中集群对象图中每个对象的每个引用都是立即可用的。 与开发人员每天使用的基本数据结构一样,维护整个Terracotta群集中的群集对象一致性和缓存一致性是很容易的。

协作,协调和活动

Terracotta的集群并发功能使其非常适合JVMS之间的信令。 在示例代码中,我们使用库存的CyclicBarrier类在单独的JVM中的线程之间进行协调。 集群事件机制可以以类似直接的方式实现。

添加并行性的一种常见方法是管理者实体将任务分配给多个并发工作人员并收集结果。 Master-Worker模式是简化为实践的最常见方法之一。

Master-Worker模式由两个逻辑实体组成:一个Master和一个或多个Worker实例。 主机通过创建一组任务来启动计算,将它们放置在共享的空间中,然后等待工作人员完成任务。

共享空间通常是某种共享队列。 使用此模式的优点之一是该算法自动平衡负载。 由于工作组是共享的,因此工人继续从工作组中撤出工作,直到没有其他工作要做。 该算法通常具有良好的可伸缩性,只要任务数量远远超过工作人员的数量,并且任务完成所需的时间大致相同即可。

这种模式的常见应用是,例如财务风险分析和其他模拟,大型数据集的搜索和汇总以及销售订单管道处理。

Java中目前存在库,以支持基于Master-Worker模式构建Work / Manager系统。 Terracotta使得这种系统可以部署在横向扩展集群上,这样,不仅可以将工作分配给单个JVM上的工作人员,还可以分配给整个JVM群集上的工作人员。 通过简单地对工作和结果队列进行集群,可以完全利用工作服务器场中的所有处理器,并且可以根据需要添加更多处理器,而不会严重破坏应用程序体系结构。

有关在Terracotta上构建Work / Manager应用程序的更多信息,请参见terracotta.org上的Master-Worker教程: http : //wiki.terracotta.org/confluence/display/orgsite/TutorialTerracottaDsoWorkManager1

结论

Open Terracotta是新的轻量级软件堆栈的基础。 JVM级集群使Tomcat,Spring,Geronimo之类的开源组件和许多开源应用程序框架可以组装在一起并以易于使用的企业级可用性和可伸缩性进行部署。

关于作者

Orion Letizi是Terracotta的联合创始人兼软件工程师,致力于其JVM级别的集群产品Open Terracotta。 他从事企业Java工作近十年。 在Terracotta之前,他是Walmart.com的软件架构师。

要下载,了解更多信息或成为Open Terracotta的贡献者,请访问http://www.terracotta.org。

翻译自: https://www.infoq.com/articles/open-terracotta-intro/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值