JGroups 手册

 
 
Copyright Red Hat 1998 - 2015

本文档以 "Creative Commons Attribution-ShareAlike (CC-BY-SA) 3.0" 许可证发布。

本文是 JGroups 手册。它提供的信息包括:

  • 安装和配置

  • 使用 JGroups (API)

  • JGroups各种协议的配置

本文的焦点是如何使用 JGroups,而不是JGroups是如何实现的。

贯穿本书全文,我遵循下面2点:

  • 我喜欢简洁。我努力把各种概念描述的尽可能的清楚 (对我这个母语不是英语的人来说),并克制自己不要说得太多,描述清楚既可。

  • 我喜欢简单。简单原则。这是我编写本手册以及编写JGroups的最大目标之一。用复杂的措辞来解释简单的概念是很容易的,但是用简单的措辞来解释一个复杂的系统则很难。我尽量做后者。

那么,一切是如何开始的?

我从1998年到1999年,在康奈尔大学读博士后,在Ken Birman的小组里面。Ken由于发明了"分组通信范式"而成功,尤其是"虚拟同步模型"。那个时候,他们工作在他们的第三代分组通信原型上,该原型叫做Ensemble。Ensemble紧随Horus(由RobbertVanRenesse以C语言编写)之后,Horus又紧随ISIS (由Ken Birman编写,也是用C语言)。Ensemble是用OCaml语言编写的,在INRIA开发,它是一个函数语言,和ML相关。我从来没有喜欢过OCaml语言,在我的脑子里它的语法太丑陋。因此,我也从没有对Ensemble热心过。

然而,Ensemble有一个Java接口(由一个学生在其学期项目中实现),这使得我能够以Java语言编程并使用Ensemble的底层功能。Ensemble的Java部分要求 Ensemble进程 运行在相同的机器上,通过双向管道连接到Ensemble进程上。这个学生已经开发出了一个简单的协议,用于和Ensemble引擎进行交互,并扩展了该引擎,使之能够响应Java。

然而,我依旧需要为每个不同的平台编译并安装Ensemble运行环境,这正是为什么当初要开发Java的原因:可移植性。

因此,我开始编写一个简单的框架 (就是现在的JChannel),它使得我能够简单地将 Ensemble 当作另外一个分组通信传输协议,它将随时被纯Java解决方案替换掉。不久我就发现,我自己工作在分组通信传输协议的一个纯粹的Java实现上(现在的: ProtocolStack)。我发现一个纯粹的Java实现 将会比Ensemble中编写的一些东西 更有影响力。最终,我没有花费太多的时间写谁都不会看的科技论文 (我猜我不是一个好的科学家,至少不是一个好的理论科学家),而是花时间编写JGroups,它将会有更大的影响力。对于我而言,得知现实生活中的项目/产品正在使用JGroups 比 一篇论文被一个会议或者期刊接收 更有满足感。

这就是为什么在我的学期结束之后,我离开了康奈尔以及学院 并在硅谷的电信行业开始工作的原因。

就在那个时期前后 (2000年5月),SourceForge刚刚开放站点,我决定用它来托管JGroups。这是JGroups的主要爆发时期,因为其它开发人员可以工作在该代码之上。从那以后,JGroups的页面点击和下载量稳步上升。

在2002年秋天,Sacha Labourey联系了我,告诉我 JGroups 被用作JBoss的集群实现。我在2003年加入了JBoss,然后一直工作在JGroups和JBossCache项目上。我的目标是,让JGroups成为Java界中使用最广泛的集群软件...

我想要感谢所有 现在和以前的JGroups贡献者的工作。没有你们,该项目就不会得以成功。

我也想要感谢Ken Birman和Robbert VanRenesse,关于分组通信和分布式系统的方方面面的许多有价值的讨论。

我想要将本手册献给Jeannette, Michelle 以及 Nicole。

Bela Ban, San Jose, Aug 2002, Kreuzlingen Switzerland 2014

1. Overview

分组通信使用了术语 分组(group)成员(member)。成员是分组的一部分。在更加常规的术语中,一个成员是一个节点,一个分组是一个集群。我们混用这些数据。

一个节点是一个进程,位于某个主机上。一个集群可以拥有一个或多个节点。在相同的主机上可以存在多个节点,所有节点可以全部属于相同的集群,也可以只有部分节点属于相同的集群。节点,当然也可以运行在其它主机上。

JGroups是一个用于可靠分组通信的工具。多个进程可以加入一个分组,发送消息到所有组成员或某个成员,也可以从分组中的某些成员接收消息。JGroups系统会追踪每个分组中的各个成员,并且,当有新成员加入分组时 或者 现有成员离开分组或崩溃时,JGroups系统会通知分组成员。一个分组由其名字来标识。不需要明确地创建分组;当一个进程加入一个不存在的分组时,该分组将会被自动创建。一个分组中的多个线程可以位于相同的主机上,可以在相同的LAN之内,也可以跨越一个WAN。一个成员可以属于多个分组。

JGroups的架构展示在 The architecture of JGroups 中。

Figure 1. The architecture of JGroups

它包含3部分: (1) Channel供程序员用于构建可靠的分组通信程序,(2) building blocks, 基于channel之上,提供一个更高的抽象层,(3) 协议栈,实现为特定信道指定的属性。

本文描述如何安装使用JGroups,也就是Channel API以及building blocks。它所面向的读者是想要利用JGroups来构建可靠的、需要分组通信功能的分布式程序的程序员。

一个信道被连接到一个协议栈。任何时候当应用程序发送一条消息的时候,该信道都会把该消息传递给协议栈,协议栈又会把该消息传递给最顶层的协议。这个顶层协议会处理该消息,然后把它传递给位于它的下一级的协议。就这样,该消息被从一个协议传递到另一个协议,直到底层的(传输)协议把它放到网络上。反方向时,消息处理相同:传输协议监听网络上的消息。当收到消息时,消息会被传递给协议栈,直到该消息到达信道。信道然后调用 应用程序中的receive()回调函数 来递交消息。

当一个应用程序连接到信道时,协议栈就会被启动,当它从该协议栈断开时,协议栈就会被停止。当信道被关闭时,协议栈会被销毁,释放它所占用的资源。

下面的三小节将会概述信道、building blocks以及协议栈。

1.1. Channel(信道)

一个进程要想加入一个分组并发送消息,它必须创建一个信道,然后用分组名字连接到该信道上 (所有带有相同名字的信道会形成一个分组)。信道是分组的句柄(XIANG: 操作杆)。当一个成员连接到分组之后,它可以向分组中的所有其它成员发送消息,也可以从它们那里接收消息。客户端通过断开信道来离开一个分组。信道可以被重用:客户端在断开信道之后,可以重新连到该信道上。然而,同一时刻,一个信道只允许一个客户端连上。如果一个客户端想要加入多个分组,那么它可以创建并连接到多个信道上。客户端可以通过关闭信道来表明它不想要再使用一条信道了。之后,该信道就再也不能被使用了。

每条信道都有一个唯一的地址。各个信道总是知道 在这个相同的分组中还包含哪些其它成员:它可以从任何一条信道获取一个成员地址列表。该列表叫做 视图/view。一个进程可以从该列表中选择一个地址,然后发送一条单播消息到该地址 (也可以发给自己),或者发送一条多播消息给位于当前view中的所有成员 (也包括它自己)。任何时候当一个进程加入一个分组或者离开一个分组时,或者检测到有进程崩溃时,一个新的视图将会被发送给所有剩余的组成员。当一个成员进程被怀疑已经崩溃时,所有没问题的成员都会收到一条 怀疑信息。因此,各信道能够接收正常的消息 以及 视图通知和怀疑通知。

通常,信道的属性定义在一个XML文件中,但是JGroups也允许通过简单的字符串、URIs、DOM树、甚至通过程序 进行配置。

Channel API及其相关类 在后面的 API 章节中描述。

1.2. Building Blocks(构建块)

信道既简单又原始。它们提供了关于分组通信的朴素功能,它们是根据sockets简单模型而设计的,该模型被广泛使用,而且很好理解。原因是,应用程序可以仅仅使用JGroups的一小部分,而不需要包含所有的、复杂的、甚至不需要的类。此外,相对简单的接口更容易被理解:一个客户端只需要知道5个方法就能够创建并使用一个信道。

信道提供了异步消息发送和接收,有点类似UDP。消息的发送动作本质上是把它放到网络上,然后send()方法立即返回。概念性的请求或它的响应的接收顺序不确定,应用程序必须自己负责对消息及其响应进行匹配。

JGroups提供了构建块,它提供了 基于Channer的、更加复杂的APIs。构建块要么在内部创建和使用信道,要么在创建一个构建块的时,要求指定一个现有的信道。应用程序直接和构建块通信,而不是直接和信道通信。构建块的目的是节省程序员的时间,让他们不必编写冗长乏味的代码以及循环代码,比如对请求-响应进行关联,因此构建块提供了分组通信的一个更高层面的抽象。

构建块在 Building Blocks 描述。

1.3. The Protocol Stack(协议栈)

协议栈在一个双向列表中包含了大量的协议层。所有通过信道发送和接收的消息都必须通过所有的协议。每个协议层都可以修改、重新排序或者丢弃消息,或者为消息添加包头。一个分割层可能会将一条消息分割成几条更小的消息,并为每个消息块添加一个带有ID的包头,然后在接收端重组这些消息块。

协议栈的组成,也就是它的各种协议,由信道的创建者来决定:一个XML文件定义了要使用的各种协议 (以及每个协议的各种参数)。该XML配置就可以用于创建一个协议栈。

在应用程序中,只是使用信道的话,关于协议栈的知识不是必需的。然而,如果一个应用程序想要忽略协议栈的默认属性,并配置自己的协议栈时,那么就需要每个协议层是用来做什么的相关知识

2. Installation and configuration

YBXIANG:

  • 本章重要的小节为2.4,是关于日志系统配置的。
  • 其余信息可以参见链接。我们不在本章花费太多时间。将来如果有必要,再来处理。

The installation refers to version 3.x of JGroups. Refer to the installation instructions (INSTALL.html) that areshipped with the JGroups version you downloaded for details.

The JGroups JAR can be downloaded from SourceForge.It is named jgroups-x.y.z, where x=major, y=minor and z=patch version, for example jgroups-3.0.0.Final.jar.The JAR is all that’s needed to get started using JGroups; it contains all core, demo and (selected) testclasses, the sample XML configuration files and the schema.

The source code is hosted on GitHub. To build JGroups,ANT is currently used. In Building JGroups from source we’ll show how to build JGroups from source.

2.1. 要求

  • JGroups 3.x requires JDK 6 or higher.

  • There is no JNI code present so JGroups should run on all platforms.

  • Logging: by default, JGroups tries to use log4j2. If the classes are not found on the classpath, itresorts to log4j, and if still not found, it falls back to java.util.logging logger.See Logging for details on log configuration.

2.2. 源代码版的结构

The source version consists of the following directories and files:

src

the sources

tests

unit and stress tests

lib

JARs needed to either run the unit tests, or build the manual etc. No JARs from here are required at runtime !Note that these JARs are downloaded automatically via ivy.

conf

configuration files needed by JGroups, plus default protocol stack definitions

doc

documentation

2.3. 从源代码构建JGroups

  • Download the sources from GitHub, either via git clone, or thedownload link into a directory JGroups, e.g. /home/bela/JGroups.

  • Download ant (preferably 1.8.x or higher)

  • Change to the JGroups directory

  • Run ant

  • This will compile all Java files (into the classes directory). Note that if the lib directory doesn’t exist,ant will download ivy into lib and then use ivy to download the dependent libraries defined in ivy.xml.

  • To generate the JGroups JAR: ant jar

  • This will generate the following JAR files in the dist directory:

    • jgroups-x.y.z.jar: the JGroups JAR

    • jgroups-sources.jar: the source code for the core classes and demos

  • Now add the following directories to the classpath:

    • JGroups/classes

    • JGroups/conf

    • All needed JAR files in JGroups/lib. Note that most JARs in lib are only required for running unit tests andgenerating test reports

  • To generate JavaDocs simple run: ant javadoc and the Javadoc documentation will be generated in dist/javadoc

2.4. 日志

JGroups has no runtime dependencies; all that’s needed to use it is to have jgroups.jar on the classpath.For logging, this means the JVM’s logging (java.util.logging) is used.

However, JGroups can use any other logging framework. By default, log4j and log4j2 are supported if thecorresponding JARs are found on the classpath.

2.4.1. log4j2

To use log4j2, the API and CORE JARs have to be found on theclasspath. There’s an XML configuration for log4j2 in the conf dir, which can be used e.g. via-Dlog4j.configurationFile=$JGROUPS/conf/log4j2.xml.

log4j2 is currently the preferred logging library used by JGroups, and will be used even if the log4jJAR is also present on the classpath.

2.4.2. log4j

To use log4j, the log4j JAR has to be found on the classpath. Note though thatif the log4j2 API and CORE JARs are found, then log4j2 will be used, so those JARs will have to be removed if log4jis to be used. There’s an XML configuration for log4j in the conf dir, which can be used e.g. via-Dlog4j.configuration=file:$JGROUPS/conf/log4j.properties.

2.4.3. JDK logging (JUL)

To force use of JDK logging, even if the log4j(2) JARs are present, -Djgroups.use.jdk_logger=true can be used.

2.4.4. Support for custom logging frameworks

JGroups allows custom loggers to be used instead of the ones supported by default. To do this, interfaceCustomLogFactory has to be implemented:

public interface CustomLogFactory {
 Log getLog(Class clazz);
 Log getLog(String category);
}

The implementation needs to return an implementation of org.jgroups.logging.Log.

To force using the custom log implementation, the fully qualified classname of the custom logfactory has to be provided via -Djgroups.logging.log_factory_class=com.foo.MyCustomLogger.

2.5. 测试你的设置

To see whether your system can find the JGroups classes, execute the following command:

java org.jgroups.Version

or

java -jar jgroups-x.y.z.jar

You should see the following output (more or less) if the class is found:

$ java org.jgroups.Version

 Version: 3.5.0.Final

2.6. 运行演示程序

To test whether JGroups works okay on your machine, run the following command twice:

java -Djava.net.preferIPv4Stack=true org.jgroups.demos.Draw

2 whiteboard windows should appear as shown in Screenshot of 2 Draw instances.

Figure 2. Screenshot of 2 Draw instances

If you started them simultaneously, they could initially show a membership of 1 intheir title bars. After some time, both windows should show 2. This means that the two instances foundeach other and formed a cluster.

When drawing in one window, the second instance should also be updated. As the default group transportuses IP multicast, make sure that - if you want start the 2 instances in different subnets- IP multicast is enabled. If this is not the case, the 2 instances won’t find each other and theexample won’t work.

You can change the properties of the demo to for example usea different transport if multicast doesn’t work (it should alwayswork on the same machine). Please consult the documentation to see how to do this.

State transfer (see the section in the API later) can also be tested by passing the -state flag to Draw.

2.7. 在没有网络连接的情况下使用IP多播

Sometimes there isn’t a network connection (e.g. DSL modem is down), or we want to multicast only on the local machine.For this the loopback interface (typically lo) can be configured, e.g.

route add -net 224.0.0.0 netmask 240.0.0.0 dev lo

This means that all traffic directed to the 224.0.0.0 network will be sent to the loopback interface, which means itdoesn’t need any network to be running. Note that the 224.0.0.0 network is a placeholder for all multicast addressesin most UNIX implementations: it will catch all multicast traffic.

The above instructions may also work for Windows systems, but this hasn’tbeen tested. Note that not all operating systems allow multicast traffic to use the loopback interface.

Typical home networks have a gateway/firewall with 2 NICs:the first (e.g. eth0) is connected to the outside world (InternetService Provider), the second (eth1) to the internal network, withthe gateway firewalling/masquerading traffic between the internaland external networks. If no route for multicast traffic is added,the default will be to use the fdefault gateway, which willtypically direct the multicast traffic towards the ISP. To preventthis (e.g. ISP drops multicast traffic, or latency is too high),we recommend to add a route for multicast traffic which goes tothe internal network (e.g. eth1).

2.8. 不工作!

The section below refers to JGroups versions prior to 3.5. In 3.5 and later versions, mcast (see below) should be used.

Make sure your machine is set up correctly for IP multicasting. There are 2 test programs that can be used to detectthis: McastReceiverTest and McastSenderTest. Start McastReceiverTest, e.g.

java org.jgroups.tests.McastReceiverTest

Then start McastSenderTest:

java org.jgroups.tests.McastSenderTest

If you want to bind to a specific network interface card (NIC), use -bind_addr 192.168.0.2, where 192.168.0.2is the IP address of the NIC to which you want to bind. Use this parameter in both sender and receiver.

You should be able to type in the McastSenderTest window andsee the output in the McastReceiverTest. If not, try to use -ttl 32 in the sender. If this still fails,consult a system administrator to help you setup IP multicast correctly. If you arethe system administrator, look for another job :-)

Other means of getting help: there is a public forum on JIRAfor questions. Also consider subscribing to the javagroups-users mailing list to discuss such and other problems.

2.8.1. mcast

Instead of McastSender and McastReceiver, a single program mcast can be used. Start multiple instances of it.The options are:

-bind_addr

the network interface to bind to for the receiver. If null, mcast will join allavailable interfaces

-port

the local port to use. If 0, an ephemeral port will be picked

-mcast_addr

the multicast address to join

-mcast_port

the port to listen on for multicasts

-ttl

The TTL (for sending of packets)

2.9. IPv6相关问题

Another source of problems might be the use of IPv6, and/or misconfiguration of /etc/hosts. If you communicate betweenan IPv4 and an IPv6 host, and they are not able to find each other, try the -Djava.net.preferIP4Stack=trueproperty, e.g.

java -Djava.net.preferIPv4Stack=true org.jgroups.demos.Draw -props /home/bela/udp.xml

The JDK uses IPv6 by default, although is has a dual stack, that is, it also supports IPv4.Here’s more details on the subject.

2.10. Wiki

There is a wiki which lists FAQs and their solutions at http://www.jboss.org/wiki/Wiki.jsp?page=JGroups. It isfrequently updated and a useful companion to this manual.

2.11. 我发现了一个bug!

If you think that you discovered a bug, submit a bug report onJIRA or send email to the jgroups-users mailing list if you’re unsure about it.Please include the following information:

  • ✓ Version of JGroups (java org.jgroups.Version)

  • ✓ Platform (e.g. Solaris 8)

  • ❏ Version of JDK (e.g. JDK 1.6.20_52)

  • ❏ Stack trace in case of a hang. Use kill -3 PID on UNIX systems or CTRL-BREAK on windows machines

  • ✓ Small program that reproduces the bug (if it can be reproduced)

2.12. 支持的classes

JGroups project has been around since 1998. Over this time, some of the JGroups classeshave been used in experimental phases and have never been matured enough to be used in today’s productionreleases. However, they were not removed since some people used them in their products.

The following tables list unsupported and experimental classes. These classes are not actively maintained, andwe will not work to resolve potential issues you might find. Their final faith is not yet determined; theymight even be removed altogether in the next major release. Weight your risks if you decide to use them anyway.

2.12.1. 试验性的classes

Table 1. Experimental
Package Class

org.jgroups.util

HashedTimingWheel

org.jgroups.blocks

GridInputStream

org.jgroups.blocks

GridOutputStream

org.jgroups.blocks

PartitionedHashMap

org.jgroups.blocks

ReplCache

org.jgroups.blocks

GridFilesystem

org.jgroups.blocks

Cache

org.jgroups.blocks

GridFile

org.jgroups.auth

Krb5Token

org.jgroups.client

StompConnection

org.jgroups.protocols

TCP_NIO

org.jgroups.protocols

FD_ALL2

org.jgroups.protocols

SHUFFLE

org.jgroups.protocols

DAISYCHAIN

org.jgroups.protocols

RATE_LIMITER

org.jgroups.protocols

TUNNEL

org.jgroups.protocols

SEQUENCER2

org.jgroups.protocols

PRIO

org.jgroups.protocols

SWIFT_PING

2.12.2. 不再支持的classes

Table 2. Unsupported
Package Class

org.jgroups.util

HashedTimingWheel

org.jgroups.blocks

PartitionedHashMap

org.jgroups.blocks

ReplCache

org.jgroups.blocks

ReplicatedHashMap

org.jgroups.blocks

ReplicatedTree

org.jgroups.blocks

Cache

org.jgroups.protocols

TCP_NIO

org.jgroups.protocols

DISCARD

org.jgroups.protocols

DISCARD_PAYLOAD

org.jgroups.protocols

HDRS

org.jgroups.protocols

FD_PING

org.jgroups.protocols

EXAMPLE

3. API

本章介绍JGroups中用来构建可靠分组通信应用程序的那些classes。焦点是创建和使用信道。

本文中的信息可能不是最新的,但是我们在这里描述的JGroups中的这些classes的性质是相同的。要想获取最新的信息,请参见doc/javadoc目录下的、通过Javadoc生成的文档。

在这里讨论的所有的classes都位于 org.jgroups 包下面,除非额外提到的那些classes。

3.1. 辅助类

org.jgroups.util.Util 这个类包含了非常有用的常规性功能,不能归并到其它包中。

3.1.1. objectToByteBuffer(), objectFromByteBuffer()

第一方法以一个对象作为其参数,该方法用于把该对象序列化为一个byte buffer (该对象必需实现java.io.Serializable接口 或者 java.io.Externalizable接口)。该方法返回一个字节数组。这个方法通常用于将对象序列化为消息的byte buffer(YBXIANG:参见Message(Address dest, Address src, Object obj)这个构造函数如何调用org.jgroups.Message.setObject(Object))。第二个方法从一个buffer返回一个重构之后的对象。这2个方法会抛出异常,如果对象不能被序列化或反序列化的话。

3.1.2. objectToStream(), objectFromStream()

第一个方法接受一个对象作为参数,该方法将该对象 写入 作为参数传入的输出流。第二个方法接受一个输入流,并从这个输入流中读取一个对象。这两个方法会抛出异常,如果该对象不能被序列化或反序列化的话。

3.2. 接口

这些接口 被下面所描述的APIs使用,因此首先把它们列出来。

3.2.1. MessageListener(消息监听器)

下面这个MessageListener接口为消息接收以及状态的读取与设置提供了回调函数:

public interface MessageListener {
 void receive(Message msg);
 void getState(OutputStream output) throws Exception;
 void setState(InputStream input) throws Exception;
}

一旦收到了消息,receive()方法就会被调用。getState()setState() 方法用于读取和设置 分组的状态(比如,成员在加入分组的时候读取分组的状态)。关于状态转移的讨论,参见State transfer。

3.2.2. MembershipListener(成员关系监听器)

MembershipListener接口类似于上面的MessageListener接口:每当收到一个新的视图、一条可疑消息或者一个阻塞事件时,实现MembershipListener的类的相应方法就会被调用。

public interface MembershipListener {
  void viewAccepted(View new_view);
 void suspect(Object suspected_mbr);
 void block();
 void unblock();
}

通常,唯一需要实现的回调方法是viewAccepted(View new_view),它会在新的成员加入了分组或者现有的成员离开了分组或者崩溃的时候,通知接收者。当一个成员被怀疑已经崩溃、但是还没有从分组中排除掉的时候,JGroups就会调用回调函数suspect(Object suspected_mbr)[1]

block()方法的用于通知成员:它将马上被禁止发送消息。这是通过 FLUSH 协议实现的,比如当正在进行状态转移或者视图安装时,可以用它来确保没有人正在发送消息。当 block() 返回时,任何正在发送消息的线程都将会被阻塞,直到 FLUSH 解锁该线程以后,比如说在状态成功转移之后。

因此 block() 可以用于发送挂起中的消息 或者 完成某些其它工作。请注意,block()应该简洁,否则的话,整个 FLUSH 协议都会被阻塞。

unblock()方法用于通知成员:FLUSH 协议已经完成了,成员可以重新发送消息了。如果在block()方法调用之后,成员依旧没有停止发送消息,则FLUSH 协议只是阻塞这些消息,后面会重新开始,因此不需要成员做额外的处理。是否实现 unblock() 回调是可选的。(YBXIANG:这里的意思是,如果我们的协议栈包含了FLUSH协议,那么成员可以在block()方法中让自己停止消息发送,可以在unblock()方法中 重新启动自己的消息发送工作。如果成员不实现这2个函数,依旧不断地向Channel中插入消息,则Channel会根据FLUSH协议阻塞这些消息的发送,直到Channel得知FLUSH解锁之后。)

请注意,通常情况下,扩展ReceiverAdapter (见下)并只实现必需的回调方法 比 实现这两个接口的所有方法 简单一些,因为大多数回调方法都不是必需的。

3.2.3. Receiver

public interface Receiver extends MessageListener, MembershipListener;

Receiver 可以用于接收消息以及视图变化通知;一旦有消息到达,receive()方法就会被调用;一旦有新的视图被安装viewAccepted()方法就会被调用。

3.2.4. ReceiverAdapter

整个class实现了 Receiver接口,对该接口做了空操作实现。当我们要实现一个回调函数的时候,我们可以简单地扩展 ReceiverAdapter 并覆写其 receive()方法,以避免实现Receiver接口的所有回调函数。

ReceiverAdapter看起来如下:

public class ReceiverAdapter implements Receiver {
 void receive(Message msg) {}
 void getState(OutputStream output) throws Exception {}
 void setState(InputStream input) throws Exception {}
 void viewAccepted(View view) {}
 void suspect(Address mbr) {}
 void block() {}
 void unblock() {}
}

ReceiverAdapter是实现各种回调方法的推荐方式。

请注意,任何可能导致阻塞的操作都应该在回调函数中执行。包括发送消息,如果我们的协议栈包含了FLUSH协议,并且在viewAccepted()这个回调方法中发送消息的话,那么会发生下面的情况:FLUSH协议在安装一个视图之前,会阻塞所有的(多播)消息,然后才安装该视图,然后解锁。然而,由于视图的安装操作触发了 viewAccepted() 这个回调方法,在viewAccepted()中的消息发送操作将会被阻塞。这反过来又阻塞了 viewAccepted() 线程,因此flush操作永远不会返回!
如果我们需要再一个回调方法中发送一条消息,那么该发送动作应该在一个单独的线程中执行,或者作为一个定时器任务提交给定时器来执行。

3.2.5. ChannelListener

public interface ChannelListener {
 void channelConnected(Channel channel);
 void channelDisconnected(Channel channel);
 void channelClosed(Channel channel);
}

实现ChannelListener的类 可以使用 Channel.addChannelListener()方法 将其注册到一个channel中,用于获取channel的状态改变信息。不论合适,如果一个信道被关闭了、断开了或者打开了,相应的回调方法就会被触发。

3.3. Address

分组中的每个成员都有一个地址,该地址用于唯一标识该成员。这样的地址的接口是 Address,该接口要求具体的实现提供诸如地址比较和排序之类的功能。JGroups的地址必需实现下面的接口:

public interface Address extends Externalizable, Comparable, Cloneable {
 int size();
}

size()方法需要返回 地址实例被序列化后的字节数,用于序列化目的。

请永远不要直接使用一个地址实现类;你应该总是使用Address接口,作为某个集群节点的标识!

地址的实际实现类通常由最底层的协议层(比如UDP或者TCP)来生成。这就使得各种地址都可以在JGroups中使用。

既然一个地址唯一地标识一个信道,也因此能够唯一标识一个分组成员,那么地址就可以用来 将消息发送到该分组成员,也就是说在Messages中设置地址 (参见下一节)。

地址的默认实现是 org.jgroups.util.UUID。它唯一地标识一个节点,当一个节点从一个集群中断开然后重新连接到该集群时,在重连时,该节点就会被分配一个新的UUID。

UUIDs从不直接显示出来,而是显示为一个逻辑名字 (参见 3.8.2 Logical names)。这是通过用户或者JGroups为一个节点设置的名字,其唯一的目的是使得日志输出信息可读性更好。

UUIDs 可以映射到 IpAddresses,后者包括IP地址和端口。IP地址和端口最终会被传输协议用于发送消息。

3.4. Message

数据在成员之间以消息(org.jgroups.Message)的形式进行发送。一个成员可以发送一条消息到单个成员,或者发送给分组中的所有成员,在分组中信道是端点(YBXIANG:即发送者/接收者)。消息的结构显示在 “Structure of a message” 这幅图中。

Figure 3. Structure of a message

一条消息有5个区域:

Destination address

这是接收者的地址。如果是null的话,消息就会被发送给当前所有的分组成员。Message.getDest()返回消息的目标地址。

Source address

这是发送者的地址。可以是是null,在消息被放到网络上传输之前,由传输协议(比如UDP)来填写这个地址。Message.getSrc()返回源地址,也就是消息发送者的地址。

Flags

这是一个字节,用于存贮标记。当前可以被识别的标记有 OOB, DONT_BUNDLE, NO_FC,NO_RELIABILITY, NO_TOTAL_ORDER, NO_RELAY 以及 RSVP。关于OOB,请参见对 5.4 concurrent stack 的讨论。关于标记的用法,请参见 5.13 the message flags。

Payload

实际的数据(也就是一个byte buffer)。Message 这个类包含了方便的方法,用于设置一个可序列化对象 以及 从中提取该序列化对象,它使用序列化功能 将对象转化为一个byte buffer,或者将byte buffer转化为对象。如果该buffer只是一个更大的buffer的自己,那么消息也可以有偏移量和长度信息。

Headers

这是一列可以粘附到消息上的头数据。任何不应该出现在payload中的数据,都可以作为一个头数据粘附到消息上。Message 的 putHeader()getHeader() 以及 removeHeader()方法可以用于操控头数据。
请注意,头数据只供 协议实现者 使用;应用程序代码不应该添加或删除头数据!

一条消息类似一个IP数据包,包含 payload (a byte buffer) 以及 发送者和接收者的地址 (Addresses)。任何放到网络上的数据都可以被转发到其目标(接收者地址),可以将响应 返回到 接收者地址。

发送者在发送消息的时候,通常不需要在消息中填充发送者的地址;在消息被放到网络上之前,协议栈会自动处理。然而,有些情况下,消息发送者不想要填入自己的地址,比如说,用来将响应返回给其它成员。

目标地址(接收者) 可以是一个Address,它代表了一个成员的地址,比如说,从之前接收到的消息中获取到该地址,该地址也可以是null,这就意味着该消息将会被发送给分组中的所有成员。一个常规的多播消息,比如说发送字符串"Hello"给所有成员,看起来像这样:

Message msg=new Message(null, "Hello");
channel.send(msg);

3.5. Header(头数据)

头数据 是客户化的信息,可以添加到每条消息。JGroups大量地使用了头数据,比如为每条消息添加序列号(NAKACK 和 UNICAST),这样这些消息就会以发送时的顺序被传递给接收者。

3.6. Event

事件 是JGroups中各种协议的交互方式。事件和Messages在分组成员之间的网络上传输相反,事件只在协议栈中上下传递。

头数据和事件只供协议实现者使用;应用程序代码不需要它们!

3.7. View

视图(org.jgroups.View)是分组中的各成员的一个列表。它包含了一个 ViewId,唯一标识该视图(见下),以及一个成员列表。当一个新的成员加入分组或者一个现有的成员离开分组(或崩溃)时,底层的协议栈会自动地将视图安装到信道中。一个分组中的所有成员看到的都是相同的视图。

请注意,视图的第一个成员是 协调者 (它会发送出新的视图)。这样,任何时候,当成员关系发生变化时,每个成员都可以轻易地判定谁是协调者,取出视图中的第一个成员既可,而不需要和其它成员进行联系。

下面的代码展示了如何发送一条(单播)消息到视图的第一个成员 (我们省略了错误监测代码):

View view=channel.getView();
Address first=view.getMembers().get(0);
Message msg=new Message(first, "Hello world");
channel.send(msg);

任何时候,当一个应用程序被通知 新的视图被安装了 (比如,通过 Receiver.viewAccepted()来通知应用程序),该视图在该信道中已经安装好了。比如说,在viewAccepted()回调方法中调用Channel.getView()可能会返回相同的视图 (如果这时信道已经有了一个新的视图,那么也可能返回这个新的视图!)。【YBXIANG:这里的意思是,信道安装好视图之后,才调用回调方法,比如Receiver.viewAccepted()

YBXIANG 带有多个节点的View.toString():
[WIN7U-20140428K-59794|0] (1) [WIN7U-20140428K-59794]
[WIN7U-20140428K-59794|1] (2) [WIN7U-20140428K-59794, WIN7U-20140428K-21936]
[WIN7U-20140428K-59794|2] (3) [WIN7U-20140428K-59794, WIN7U-20140428K-21936, WIN7U-20140428K-30842]
[WIN7U-20140428K-59794|3] (4) [WIN7U-20140428K-59794, WIN7U-20140428K-21936, WIN7U-20140428K-30842, WIN7U-20140428K-10755]
格式:
[协调者|其它节点数量] (所有节点数量) [协调者, 第二加入的节点, 第三加入的节点, ...]


3.7.1. ViewId

ViewId用于唯一地为视图进行编号。它包含 视图创建者的地址 以及 一个序列号。ViewIds可以进行比较,可以放入一个 hashmaps 中,因为ViewIds实现了 equals() 和 hashCode()方法。

请注意,后面这2个方法只考虑ID。

3.7.2. MergeView

任何时,当一个分组分裂成多个子分组(比如说,由于网络分裂导致),然后子分组又合并回来,那么应用程序将会收到一个 MergeView,而不是一个View。MergeView是View的子类,包含了额外的变量,存储合并的视图列表。举个例子,如果一个分组view V1:(p,q,r,s,t)被分裂成了子分组V2:(p,q,r)V2:(s,t),那么合并之后的视图可能是V3:(p,q,r,s,t)。在这种情况下,MergeView 将包含 一个视图列表,该视图列表包含 V2:(p,q,r)V2:(s,t)。

3.8. JChannel

一个进程要想缴入一个分组然后发送消息,它必需创建一条信道。信道就像socket。当客户端连接到一条信道上时,会给出该客户端想要加入的分组的名字。这样,一条信道总是和一个特顶的分组相关联(在其连接状态中)。协议栈负责让带有相同分组名字的各条信道 相互找到对方:任何时候,当一个客户端连接到一条信道时,为该信道设置一个分组名字G,然后该信道就会查找带有相同分组名字的、已经存在的信道,然后加入它们,导致一个新的视图(包含新的成员)被装入这些信道。如果不存在现有的成员,那么就会创建一个新的分组。

信道的主要状态的状态转移图 展示在下面的 [ChannelStatesFig] 这幅图中。

当一条信道首次被创建时,它处于 未连接(unconnected) 状态。

如果视图执行只有在已连接(connected)状态下才有效的操作(比如发送/接收消息)的话,就会导致异常。

在客户端成功连接之后,信道就会进入 已连接(connected)状态。现在,该信道将会从其它成员接收消息,也可以相其它成员或者整个分组发送消息,当有新的成员加入或离开时,该信道会被通知。在这个状态下,获取该信道的本地地址 肯定是一个合法的操作 (见下)。

当信道被断开时,它就回到 未连接(unconnected)状态。已连接(connected)和未连接(unconnected)状态下的信道都可以被关闭,这样做该信道就不能再使用了。任何操作都会导致异常。当一条处于 已连接(connected) 状态的信道被直接关闭时,它会首先被断开,然后被关闭。

现在,我们描述创建和操作信道的各种方法。

3.8.1. Creating a channel

我们通过信道的公开构造函数(比如 new JChannel())来创建一条信道。

JChannel 最常用的构造函数如下:

public JChannel(String props) throws Exception;

这个 props 参数指向一个XML文件,该XML文件它包含了将要被使用的协议栈的配置信息。该参数可以是一个字符串,但是还有其它的构造函数,它们可以接受诸如DOM元素或URL之类的参数 (详细信息请参见 javadoc)。

下面的实例代码展示了如何基于一个XML配置文件创建一条信道:

JChannel ch=new JChannel("/home/bela/udp.xml");

如果这个 props 参数是 null,则使用默认的属性。如果无法创建该信道,那么就会抛出异常。可能的原因包括 没有找到由这个prop参数所指定的协议,或者协议参数错误。

比如,我们可以用下面的命令启动 Draw 示例:

java org.javagroups.demos.Draw -props file:/home/bela/udp.xml

或者

java org.javagroups.demos.Draw -props http://www.jgroups.org/udp.xml

后面这种情况下,应用程序会从一台服务器下载它的协议栈,这使得我们可以对应用程序的属性进行集中管理。

一个示例XML配置文件看起来如下(根据 udp.xml 修改):

<config xmlns="urn:org:jgroups"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="urn:org:jgroups http://www.jgroups.org/schema/jgroups.xsd">
 <UDP
   mcast_port="${jgroups.udp.mcast_port:45588}"
   ucast_recv_buf_size="20M"
   ucast_send_buf_size="640K"
   mcast_recv_buf_size="25M"
   mcast_send_buf_size="640K"
   loopback="true"
   discard_incompatible_packets="true"
   max_bundle_size="64K"
   max_bundle_timeout="30"
   ip_ttl="${jgroups.udp.ip_ttl:2}"
   enable_diagnostics="true"

   thread_pool.enabled="true"
   thread_pool.min_threads="2"
   thread_pool.max_threads="8"
   thread_pool.keep_alive_time="5000"
   thread_pool.queue_enabled="true"
   thread_pool.queue_max_size="10000"
   thread_pool.rejection_policy="discard"

   oob_thread_pool.enabled="true"
   oob_thread_pool.min_threads="1"
   oob_thread_pool.max_threads="8"
   oob_thread_pool.keep_alive_time="5000"
   oob_thread_pool.queue_enabled="false"
   oob_thread_pool.queue_max_size="100"
   oob_thread_pool.rejection_policy="Run"/>

 <PING timeout="2000"
   num_initial_members="3"/>
 <MERGE3 max_interval="30000"
   min_interval="10000"/>
 <FD_SOCK/>
 <FD_ALL/>
 <VERIFY_SUSPECT timeout="1500" />
 <BARRIER />
 <pbcast.NAKACK use_stats_for_retransmission="false"
     exponential_backoff="0"
     use_mcast_xmit="true"
     retransmit_timeout="300,600,1200"
     discard_delivered_msgs="true"/>
 <UNICAST timeout="300,600,1200"/>
 <pbcast.STABLE stability_delay="1000" desired_avg_gossip="50000"
     max_bytes="4M"/>
 <pbcast.GMS print_local_addr="true" join_timeout="3000"
    view_bundling="true"/>
 <UFC max_credits="2M"
   min_threshold="0.4"/>
 <MFC max_credits="2M"
   min_threshold="0.4"/>
 <FRAG2 frag_size="60K" />
 <pbcast.STATE_TRANSFER />
config>

一个协议栈用 封装起来,它列举了从底层(UDP)到顶层(STATE_TRANSFER)的所有协议。每个元素都定义一个协议。

每个协议都由一个Java类实现。当我们根据上面的XML文件创建一个协议栈时,第一个元素("UDP")变成了最底部的协议层,第二个元素(对应的协议层)被放在第一个(元素对应的协议成)之上,等等;协议栈是从底部 创建到 顶部的。

每个元素的名字 都必需是 位于org.jgroups.protocols包中的某个Java类的名字。请注意,只需要给出基础名字,不需要指定完整的类名 (用UDP,而不用org.jgroups.protocols.UDP)。如果没有找到这个协议对应的Java类,那么JGroups就会假设给定的名字是一个完整路径的类名,因此会视图实例化这个类。如果实例化失败,则抛出异常。这使得协议实现类可以位于不同的包中,比如一个有效的协议名字可能是 com.sun.eng.protocols.reliable.UCAST【YBXIANG:注意,这个XML文件中的pbcast.STABLE协议,其实是org.jgroups.protocols.pbcast.STABLE】

每个协议层都有0个或多个参数,直接在尖括号中的 协议名字的后面 用一列name/value pairs来指定参数。这上面这个例子中,我们为 UDP协议配置了一些选项,其中一个是 IP多播端口(mcast_port),它被设置为45588 或者 系统属性jgroups.udp.mcast_port的值(如果设置了该系统属性的话)。

请注意,在该分组中的所有成员必需使用相同的协议栈。

通过程序创建协议栈

通常,我们通过传入JChannel()构造函数中的一个XML配置文件来创建信道。基于这个声明式的配置之上,JGroups提供了一个API,用于通过程序的方式来创建一条信道。

做法是,首先创建一个JChannel,然后创建一个ProtocolStack实例,然后把所有期望的协议都添加到该协议栈中,最后调用该协议栈的init()方法来设置该协议栈。其余的步骤,比如 JChannel.connect() 和声明式创建方式一样。

下面是如何通过程序方式来创建一条信道的例子 (拷贝自ProgrammaticChat):

public class ProgrammaticChat {

 public static void main(String[] args) throws Exception {
  JChannel ch=new JChannel(false);   // (1)
  ProtocolStack stack=new ProtocolStack(); // (2)
  ch.setProtocolStack(stack);
  stack.addProtocol(new UDP().setValue("bind_addr",
            InetAddress.getByName("192.168.1.5")))
    .addProtocol(new PING())
    .addProtocol(new MERGE3())
    .addProtocol(new FD_SOCK())
    .addProtocol(new FD_ALL().setValue("timeout", 12000)
           .setValue("interval", 3000))
    .addProtocol(new VERIFY_SUSPECT())
    .addProtocol(new BARRIER())
    .addProtocol(new NAKACK())
    .addProtocol(new UNICAST2())
    .addProtocol(new STABLE())
    .addProtocol(new GMS())
    .addProtocol(new UFC())
    .addProtocol(new MFC())
    .addProtocol(new FRAG2());  // (3)
  stack.init();       // (4)

  ch.setReceiver(new ReceiverAdapter() {
   public void viewAccepted(View new_view) {
    System.out.println("view: " + new_view);
   }

   public void receive(Message msg) {
    Address sender=msg.getSrc();
    System.out.println(msg.getObject() + " [" + sender + "]");
   }
  });

  ch.connect("ChatCluster");


  for(;;) {
   String line=Util.readStringFromStdin(": ");
   ch.send(null, line);
  }
 }

}

首先,我们创建了一个 JChannel (1)。其中的false参数告诉该信道,不要创建一个ProtocolStack。这么做是必需的,因为我们自己将在后面创建一个协议栈,并将它设置到该信道中 (2)。

接着,所有协议被添加到该协议栈中(3)。请注意,添加顺序是从底部协议(传输协议)到顶部协议。因此作为传输协议的 UDP被首先加入,然后是 PING 协议以及其它协议,直到 FRAG2协议,它是顶层协议。每个协议都可以通过setters进行配置,但是还有一个通用的 setValue(String attr_name, Object value)方法,也可以用于配置协议,就像在例子中所展示的。

一旦协议栈被配置好了,我们就调用 ProtocolStack.init()方法,以便 将所有协议正确地链接起来 并且 调用每个协议实例的init()方法 (4)。这之后,信道就可供使用了,然后就可以执行 后续的操作(比如 connect())了。当init()方法返回时,我们在本质上得到的是new JChannel(config_file)的等价物。

3.8.2. Giving the channel a logical name

我们可以为信道指定一个逻辑名字,信道就会使用这个逻辑名字 而不是使用 信道地址.toString()作为逻辑名字了。逻辑名字可以显示信道的作用,比如"HostA-HTTP-Cluster",它比 一个UUID 3c7e52ea-4087-1859-e0a9-77a0d2f69f29 可读性好多了。

比如,假如我们有3个信道,使用了逻辑名字,那么我们肯那个看到这样的一个视图{A,B,C},这比 {56f3f99e-2fc0-8282-9eb0-866f542ae437,ee0be4af-0b45-8ed6-3f6e-92548bfa5cde,9241a071-10ce-a931-f675-ff2e3240e1ad} 好多了!

如果没有设置逻辑名字,那么JGroups会生使用主机名以及随机数成一个,比如 linux-3442。如果这不是期望的名字,并且应该显示UUIDs,那么使用系统属性 -Djgroups.print_uuids=true

可以用下面的方法来设置信道的逻辑名字:

public void setName(String logical_name);

必须在连接信道之前设置逻辑名字。注意,逻辑名字一致驻留在信道中,直到信道被销毁,否则(如果没有设置逻辑名字),信道会在链接的时候创建一个UUID。

当JGroups启动的时候,它可能会打印出逻辑名字以及相关的物理地址:

-------------------------------------------------------------------
GMS: address=mac-53465, cluster=DrawGroupDemo, physical address=192.168.1.3:49932
-------------------------------------------------------------------

逻辑名字是 mac-53465,物理地址是 192.168.1.3:49932。这里没有显示UUID。

YBXIANG测试:

* 1. Default:
* GMS: address=WIN7U-20140428K-21521, cluster=ChatCluster,
*         physical address=2001:0:5ef5:79fd:499:1867:7804:e013:59340
*
* 2. System.setProperty("jgroups.print_uuids","true")
* GMS: address=3fdee765-36ed-2e70-df5d-4b8bad25f952, cluster=ChatCluster,
*         physical address=2001:0:5ef5:79fd:499:1867:7804:e013:59341
*
* 3. channel.setName("My.Channel.1");
* GMS: address=My.Channel.1, cluster=ChatCluster,
*         physical address=2001:0:5ef5:79fd:499:1867:7804:e013:60110
*
* 4. System.setProperty("jgroups.print_uuids","true") and channel.setName("My.Channel.1")
* GMS: address=e53053dd-686e-2d7c-dc2f-61e5421b502e, cluster=ChatCluster,
*         physical address=2001:0:5ef5:79fd:499:1867:7804:e013:60111

3.8.3. Generating custom addresses

自从 2.12 版本之后,地址生成功能是可以插入的。这意味着,如果一个应用程序可以决定它想要使用什么样的地址。默认的地址类型是 UUID,由于某些协议使用UUID,因此,如果你要插入自己的地址生成功能的话,推荐你提供客户化的、UUID的子类。

客户化的地址可以用于传递额外的数据,比如关于节点的位置信息。请注意,在客户化的地址(UUID的子类)中,不应该修改UUID父类的equals()、hashCode() 以及 compare() 方法。

要想使用客户化的地址,必须实现 org.jgroups.stack.AddressGenerator

对于任何 CustomAddress类,要想正确地对它进行序列化的话,需要将它注册到ClassConfigurator中:

class CustomAddress extends UUID {
 static {
 ClassConfigurator.add((short)8900, CustomAddress.class);
 }
}
请注意,选择的ID 不能和定义在 jg-magic-map.xml 中的任何IDs重复。

然后在 JChannel的setAddressGenerator(AddressGenerator)方法中设置该地址生成器。该操作必须在信道链接之前完成。

JGroups中有个子类例子是 org.jgroups.util.PayloadUUID,JGroups还带有更多的这样的子类。

YBXIANG实践(JGroups-3.5.1):

channel.addAddressGenerator(new AddressGenerator(){
    @Override
    public Address generateAddress() {
        return new MyUUID();
    }});

3.8.4. Joining a cluster

当一个客户端想要加入一个集群的时,它会利用想要加入的集群的名字来连接到一条信道:

public void connect(String cluster) throws Exception;

这里的集群名字是客户端将要加入的集群的名字。所有用相同的名字进行连接的信道会形成一个集群。发送到集群中的任何一条信道上的消息都会被所有的成员收到 (包括发送者)。

本地信息的投递功能 可以用 setDiscardOwnMessages(true) 关闭。

connect()在成功加入集群后会立即返回。如果信道处于关闭状态 (参见 channel states),就会抛出异常。如果没有其他成员,也就是没有其它成员连接到带有相同的名字的集群上,那么就会创建一个新的集群,该成员就是第一个加入该集群的成员。集群中的第一个成员会变成集群的协调者(coordinator)。一旦成员关系有变化,集群协调者负责安装新的视图。

3.8.5. Joining a cluster and getting the state in one operation

客户端可以在一个操作中执行 加入集群并获取集群状态。理解连接和获取状态的方法的概念的最好的办法是将其看作是对常规的 connect()getState() 方法执行的连续调用。然而,使用同时执行连接并获取状态的 connect(...) 方法 相对于常规的 connect() 方法有多个优势。首先,底层的信息交换被高度优化,尤其是在使用了flush协议的情况下。但是,更重要的是,从客户端的角度来看,connect() 和 获取状态操作变成了一个原子操作。

public void connect(String cluster, Address target, long timeout) throws Exception;

就像在常规的 connect() 方法中一样,cluster名字表示将要加入的集群。target参数标识了将要从中获取状态的那个集群成员。如果target为null,则表明应该从集群的协调者(cluster coordinator)那里获取状态。如果想要从某个特定的成员中获取状态,而不是从协调者获取,那么客户端只要提供该成员的地址既可。timeout参数设置了整个加入集群和获取状态操作的时间限制。如果超时了,就会抛出异常。

3.8.6. Getting the local address and the cluster name

getAddress()方法返回信道的地址。当信道处于 未连接 状态时,该地址可能可用,也可能不可用。

public Address getAddress();

getClusterName()返回该成员所加入的集群的名字。

public String getClusterName();

再一次,如果信道处于 断开或关闭 状态,该方法的结果是不确定的。

3.8.7. Getting the current view

下面这个方法可以用于获取某个信道的当前视图:

public View getView();

该方法返回这个信道的当前视图。每当一个新的视图被安装(通过viewAccepted()回调方法)到信道中时,信道的当前视图就会被更新。

对处于 未连接或已关闭 状态的信道调用这个方法,返回结果是由信道的实现来定义的。信道可以返回null,也可以返回它所知道的最后一个视图。

3.8.8. Sending messages

一旦信道连接上,就可以通过send()方法来发送消息:

public void send(Message msg) throws Exception;
public void send(Address dst, Serializable obj) throws Exception;
public void send(Address dst, byte[] buf) throws Exception;
public void send(Address dst, byte[] buf, int off, int len) throws Exception;

第一个send()方法只有一个参数,该参数就是将要被发送的消息。这条消息的目的地址应该是接收者的地址(单播)或者null(多播)。如果目标地址是null,那么该消息将会被发生给集群中的所有成员(包括它自己)。

其它 send() 方法都是辅助方法;它们接受一个byte[]buffer 或者 serializable 参数,创建一条消息,然后调用send(Message)。

如果信道没有连接上,或者关闭了,在试图通过这些方法发送消息时,异常就会被抛出。

这里有一个将一条消息发送给所有成员的一个例子:

Map data; // any serializable data
channel.send(null, data);

null值作为目标地址,意味着该消息将被发送给处于该集群中的所有成员。负载是一个hashmap,它将被序列化存入该消息的buffer,然后在接收者那一侧被反序列化。此外,通过其它别的方法来 生成一个字节buffer 然后消息的buffer指向它(比如Message.setBuffer()),也是可以的。

这里有一个将一条单播消息发送给某分组中的第一个成员(协调者)的例子:

Map data;
Address receiver=channel.getView().getMembers().get(0);
channel.send(receiver, "hello world");

该示例代码找到了协调者(试图中的第一个成员),然后向它发送了一条"hello world"消息。

Discarding one’s own messages

YBXIANG:抛弃自己发出的消息,前面已经有描述。

有时候,我们不想要处理自己的消息,也即是说,由自己发送出去的消息。要想做到这点,可以将 JChannel.setDiscardOwnMessages(boolean flag) 设置为true (默认为false)。这意味着,每个集群节点都会收到由P发送出去的消息,而P自己不会收到。

请注意,这个方法替代了旧的 JChannel.setOpt(LOCAL, false)方法,这个旧方法在 3.0 中被删除了。

Synchronous messages【同步发送消息】

经 JGroups 确保了消息最终会被发送给所有 无故障的成员,有时候,这可能需要一点时间。比如,如果我们有个基于"negative acknowledgments"重新传输协议,最后一条发送出去的消息丢失了,那么在该消息被重新传输之前,接收者必须等待,直到这个稳定性协议注意到该消息已经丢失了。

可以通过在消息中设置 Message.RSVP 标志位来改变这种行为:当遇到这个标志位时,消息发送操作就会阻塞,直到所有的成员都告知 该消息已经收到 为止 (当然,不包括已经崩溃或者正在离开的那些成员)。

该功能还可以用于另外一个目的:如果我们发送了一条打过RSVP标签的消息,那么,当send()返回时,我们被确保:之前发送出去的消息 也已经被全部交付给所有成员了。因此,举个例子,如果P发送消息1-10,并将消息10打上RSVP标签,那么当JChannel.send()返回时,P就会直到所有成员都已经收到了从P发出的消息 1-10。

请注意,由于打过RSVP标记的消息的发送操作 开销比较大,可能会将发送者阻塞一会儿,因此应该保守地使用。比如,在运算一个工作单元时(也就是P发送N条消息),P需要确定所有消息都被所有成员收到了,因此可以使用RSVP。

要想使用RSVP,需要做2件事:

首先,需要在配置文件中添加RSVP协议,在诸如NAKACKUNICAST(2)之类的可靠传输协议之上的某个地方添加该协议,比如:

<config>
 <UDP/>
 <PING />
 <FD_ALL/>
 <pbcast.NAKACK use_mcast_xmit="true"
 discard_delivered_msgs="true"/>
 <UNICAST timeout="300,600,1200"/>
 <RSVP />
 <pbcast.STABLE stability_delay="1000" desired_avg_gossip="50000"
 max_bytes="4M"/>
 <pbcast.GMS print_local_addr="true" join_timeout="3000"
 view_bundling="true"/>
 ...
config>

第二,将那些期望确认收到的消息标记为RSVP

Message msg=new Message(null, null, "hello world");
msg.setFlag(Message.RSVP);
ch.send(msg);

这里,我们将一条消息发送给所有集群成员(dest == null)。(请注意,RSVP也可用于将消息发送给一个单播地址的场合)。send()在收到所有当前成员的确认响应之后会立即返回。如果有4个成员,A, B, C 和 D,并且A已经收到了来自它自己以及B和C的确认响应,但是没收到D的确认响应,而且D虽然处于崩溃状态却没有达到被踢的超时时间,那么,这种情况下,send()就不会返回了,就好像D已经发送过一个确认响应一样。

如果 timeout 属性大于0,并且我们在超时时间(毫秒)允许范围内 没有收到所有的确认响应,那么 TimeoutException 就会被抛出(如果RSVP.throw_exception_on_timeout是true的话)。那么,应该程序就可以捕获这个 (runtime)异常,然后处理该异常,比如重试。

关于RSVP的配置 RSVP 小节描述。

RSVP在版本3.1中添加。
Non blocking RSVP

有时候,发送者期望某条消息被重发,直到它被确认收到或者出现超时为止,但是不期望阻塞发送者。比如 RpcDispatcher.callRemoteMethodsWithFuture() 需要立即返回,甚至在结果不可用的情况下。如果该调用的选项中包含了 RSVP标志,那么只有当收到所有响应之后,才会返回future。这明显不是期望的行为。

要想解决这个问题,可以使用 RSVP_NB 标志 (非阻塞)。它的行为类似RSVP,但是调用者不会被 RSVP 协议阻塞。当超时出现时,将会打印一条警告消息,但是由于 调用者没有阻塞,因此该调用不会抛出一个异常。

3.8.9. Receiving messages

我们可以覆写ReceiverAdapter (或 Receiver)中的receive()方法来接收 消息、视图以及状态变迁回调。

public void receive(Message msg);

我们可以利用JChannel.setReceiver()在信道中注册一个Receiver。所有收到消息、视图变化以及状态变迁请求 都会调用这个被注册的Receiver中的对应的回调方法。

JChannel ch=new JChannel();
ch.setReceiver(new ReceiverAdapter() {
 public void receive(Message msg) {
 System.out.println("received message " + msg);
 }
 public void viewAccepted(View view) {
 System.out.println("received view " + new_view);
 }
});
ch.connect("MyCluster");

3.8.10. Receiving view changes

如上所示,任何时候,当集群成员关系发生变化时,我们可以用ReceiverAdapter的viewAccepted()回调方法来获取回调。需要通过JChannel.setReceiver(Receiver)来设置Receiver

如我们在ReceiverAdapter中所讨论的,在回调中的代码必须避免任何需要消耗很多时间的东西,也要避免 阻塞操作;JGroups将该回调方法(YBXIANG:指的是ReceiverAdapter.viewAccepted()方法)作为视图安装的一个步骤进行调用,如果在该回调方法中的用户代码阻塞住了,那么视图的安装过程也会阻塞。

3.8.11. Getting the group’s state

YBXIANG提醒:本小节所描述的getState()方法有2个,一个是状态请求者通过信道读取状态org.jgroups.JChannel.getState(Address, long);另外一个是状态提供者通过 org.jgroups.ReceiverAdapter.getState(OutputStream)回调方法 对外提供状态。

一个新加入的成员在开始工作之前,可能想要接收该集群的状态。可以通过 getState()(YBXIANG: 指的是JChannel.getState(Address, long))来实现:

public void getState(Address target, long timeout) throws Exception;

该方法返回一个成员的状态 (通常是最老的那个成员,也就是协调者)。在向当前的协调者请求状态时,target参数通常可以是null。如果获取状态超时了,那么就会抛出异常。超时时间0将会等待,直到所有状态都传输完毕。

我们不直接将集群的状态作为JChanlle.getState()的结果返回的原因是:相对于其它消息而言,该状态必须在数据流中的恰当位置进行返回。直接将其返回将会破坏信道的FIFO特性,状态传递也会出错!

要想参与状态传递,状态提供者和状态请求者都必须实现下面来自ReceiverAdapter (Receiver)的回调方法:

public void getState(OutputStream output) throws Exception;
public void setState(InputStream input) throws Exception;

getState()回调方法会在状态提供者(通常是协调者)那一侧被调用。它需要将其状态写入给定的output流。请注意,在读取状态完成之后或者出现异常时,你不需要关闭该output流;JGroups会关闭它。

setState()方法会在状态请求者那一侧被调用;也就是调用JChannel.getState()方法的那个成员(YBXIANG: 注意不是ReceiverAdapter.getState()方法)。它需要从input流中读取状态,然后将该状态设置为它的内部状态。请注意,在读取状态完成之后或者出现异常时,你不需要关闭该output流;JGroups会关闭它。

在一个包含了A、B和C的集群中,如果D视图加入该集群,并调用了Channel.getState(),这将会触发下面的一系列回调方法:

  • D 调用 JChannel.getState()。JGroups将会从最老的成员,A,那里获取状态。

  • A的 getState()回调方法被调用。A将其状态写入getState()作为参数传入的output stream。

  • D的 setState() 回调方法被调用,该方法有个input stream参数。D从该input stream读取状态,然后将该状态设置为自己的内部状态,覆盖任何以前的数据。

  • D:JChannel.getState()返回,请注意,只有当状态成功传输之后、或者超时之后、或者状态提供者或提供者抛出了一个异常之后,该方法才会返回。这种异常会被 getState() 重新抛出。比如说,当状态提供者的getState()回调方法视图 将一个non-serializable class系列化以便写入output stream时,就会发生异常。

下面的代码片段展示了一个分组成员如何参与状态传递:

public void getState(OutputStream output) throws Exception {
 synchronized(state) {
 Util.objectToStream(state, new DataOutputStream(output));
 }
}

public void setState(InputStream input) throws Exception {
 List list;
 list=(List)Util.objectFromStream(new DataInputStream(input));
 synchronized(state) {
 state.clear();
 state.addAll(list);
 }
 System.out.println(list.size() + " messages in chat history):");
 for(String str: list)
 System.out.println(str);
 }
}

该代码来自JGroups教程中的Chat例子,其状态是一列字符串。

这个getState()回调方法的实现同步了state变量 (因此,进入的消息在状态传递期间无法修改它),并使用了JGroups的辅助方法objectToStream()

将数据写入一个output stream时的性能

如果将大量的、更小的数据片段写入一个output stream,最好将该output stream封装成一个BufferedOutputStream,比如:

Util.objectToStream(state,
 new BufferedOutputStream(
 new DataOutputStream(output)));

这个setState()回调方法的实现也使用了 Util.objectFromStream()辅助方法来从input stream中读取状态,然后将该状态赋值给它的内部列表。

State transfer protocols

要想使用状态传递功能,必须在配置中包含状态传递协议。可以是STATE_TRANSFERSTATE或者STATE_SOCK。关于相关协议的更多信息可以参见protocols list小节。

STATE_TRANSFER

这时原始的状态传递协议,它用于传输byte[] buffers。该协议功能依旧,但是它在JGroups内部被转化为对getState()setState()回调方法的调用,这2个回调方法使用了input 和 output streams。

请注意,由于byte[] buffers可以被转化为input 和 output streams,因此该协议不应该用于传递大型状态。

更多细节请参见 pbcast.STATE_TRANSFER。

STATE

这时 STREAMING_STATE_TRANSFER 协议,在3.0中重新命名过了。它将完整的状态以块的方式从状态提供者发送到状态请求者,因此,内存消耗最小。

更多细节请参见pbcast.STATE.

STATE_SOCK

STREAMING_STATE_TRANSFER一样,到那时在状态提供者和状态请求者之间使用了TCP连接进行状态传输。

For details see STATE_SOCK.

3.8.12. Disconnecting from a channel

通过下面的方法从一条信道断开:

public void disconnect();

如果该信道已经处于断开状态或者关闭状态,该方法不起任何作用。如果信道处于连接状态,它将离开该集群。该功能是通过发送一条离开请求到当前协调者来实现的(对于一个信道用户而言,是透明的)。后者将会因此从视图中删除这个正在离开的节点,并将新的视图安装到所有的剩余成员中。

信道在成功断开之后,它将进入 未连接 状态,它可以在以后重连。

3.8.13. Closing a channel

要想摧毁一个信道实例(摧毁相关的协议栈并释放所有的资源),可以使用close()方法:

public void close();

对一条已经连接的信道进行关闭,会首先断开该信道。

该close()方法会将信道移入关闭状态,在该状态下,再也不能执行操作了 (当调用一条已经关闭的信道时,大多数情况下会抛出一个异常l)。在该状态下,信道实例再也不能被应用程序使用了  — 当重新设置该实例的reference时,该信道实际上只会处于游荡状态,直到被Java运行系统进行垃圾回收。

4. Building Blocks

构建块是基于信道之上的,当需要更高层面的接口时,可以使用它,而不是信道。

信道是简单的、类似socket的结构,而构建块可以提供复杂得多的接口。在某些情况下,构建块提供对底层信道的访问,因此如果构建块不能提供某种功能,那么可以直接访问其信道。构建块位于相关类位于org.jgroups.blocks这个包中。

4.1. MessageDispatcher

Channels是用于异步发送和接收消息的简单模式。然而,在分组通信中,大量的通信模式需要同步通信。比如,某个发送者希望发送一条消息到分组中,并等待所有的响应。或者,应用程序期望发送一条消息到分组中,然后等待直到大多数接收者发回了响应或者出现超时

MessageDispatcher提供了阻塞的(以及非阻塞的)请求发送以及响应关联。它提供了同步的(以及异步的)消息发送功能 以及 请求-响应相关联功能,比如将一个或多个响应和原来的请求进行匹配。

使用这个class的一个例子是:发送一条消息到所有的集群成员,然后阻塞等待,直到收到所有的确认响应或者出现超时。

相对于RpcDispatcher而言,MessageDispatcher处理的是 发送消息请求 以及 关联消息的响应,而RpcDispatcher处理的是 触发方法调用 以及关联响应。RpcDispatcher 扩展了MessageDispatcher,提供了基于MessageDispatcher之上的、更高级别的抽象。

RpcDispatcher本质上是一种对一个集群触发远程过程调用的方法。

MessageDispatcher和RpcDispatcher两者都基于信道之上;因此MessageDispatcher的实例是利用一个信道作为参数进行创建的。它既可扮演客户端角色,也可以扮演服务器端角色:一个客户端发送请求并接收响应,一个服务器端接收请求并发送响应。MessageDispatcher允许应用程序同时扮演两种角色。要想扮演服务器端的角色对请求作出响应,可以实现RequestHandler.handle()方法:

Object handle(Message msg) throws Exception;

任何时候,当收到一个请求之后,handle()方法就会被调用。它必须返回一个值 (必须是可序列化的,也可以是null)或者一个抛出异常。返回值将会被发送给发送者,异常也会被传递给发送者。

在研究MessageDispatcher的方法之前,让我们首先看看RequestOptions。

4.1.1. RequestOptions

在MessageDispatcher发送的每条消息或者在RpcDispatcher中触发的每个请求触发都由一个RequestOptions实例进行管理。该class可以被传递给一个调用,用于定义和该调用相关的各种选项,比如超时时间、调用是否应该阻塞以及各种标志信息(参见5.13 Tagging messages with flags) 等等。

各种选项包括:

  • 响应模式:响应模式定义了该调用是否是阻塞式的,如果是,那么需要阻塞多久。相关模式有:

    GET_ALL

    持续阻塞,直到收到来自所有成员(排除有嫌疑的那些成员)的响应。

    GET_NONE

    不等待任何成员。这会让该调用变成非阻塞的。

    GET_FIRST

    持续阻塞,直到(从任何成员)收到第一条响应。

    GET_MAJORITY

    持续阻塞,直到大多数成员(也就是无故障的成员)作出了响应。

  • 超时时间: 我们将要阻塞的毫秒数。如果在超时发生之后,该调用还没有结束,那么就会抛出TimeoutException。超时时间0表示永远等待。如果该调用是非阻塞模式的(mode=GET_NONE),那么忽略该超时时间。

  • Anycasting:如果设置为true,这意味着我们将对单个成员使用单播,而不是发送多播消息。比如,如果我们使用TCP作为传输协议,集群是{A,B,C,D,E},如果我们通过MessageDispatcher发送一条消息,该消息的目标地址dests={C,D},并且,我们不想要将该消息发送给所有成员,那么我们可以设置anycasting=true。这么配置的话,JGroups就会仅仅将请求以单播模式发送给成员C和D,在我们使用诸如TCP之类的传输协议的情况下,这么做会更好,因为TCP不能使用IP多播(将一个数据包发送给所有成员)。

  • 响应过滤器:一个RspFilter允许对响应进行过滤,以及对一个调用的中断进行用户定义。比如,如果我们期望收到10个成员的响应,但是允许在收到3条非空响应之后就返回,那么就可以使用一个RspFilter。关于响应过滤器的讨论,请参见 Response filters。

  • 范围:一个short类型,定义了一个范围。它允许并发地发送来自相同发送者的消息。关于范围的讨论,参见 Scopes: concurrent message delivery for messages from the same sender。

  • 标志: 可以将各种标志传递给消息,更多信息请参见关于 message flags 的章节。

  • 排除列表:我们可以传入应该被排除掉的成员(地址)的一个列表。比如,如果视图是A,B,C,D,E,我们的排除列表包含A,C,那么调用者就会等待除了A和C之外的所有成员的响应。此外,在该排除列表中的每个接收者都将抛弃该消息。

如何使用 RequestOptions 的一个例子是:

RpcDispatcher disp;
RequestOptions opts=new RequestOptions(Request.GET_ALL)
 .setFlags(Message.NO_FC, Message.DONT_BUNDLE);
Object val=disp.callRemoteMethod(target, method_call, opts);

发送请求的方法是:

public  RspList
 castMessage(final Collection
dests, Message msg, RequestOptions options) throws Exception; public NotifyingFuture> castMessageWithFuture(final Collection
dests, Message msg, RequestOptions options) throws Exception; public T sendMessage(Message msg, RequestOptions opts) throws Exception; public NotifyingFuture sendMessageWithFuture(Message msg, RequestOptions options) throws Exception;

castMessage()将消息发送给有定义在dests中的成员。如果 dests 是null,那么消息将会被发送给当前集群中的所有成员。请注意,在消息中的目标地址集合可能会被覆写。如果消息是通过同步方式发送的 (用options.mode定义),那么options.timeout就定义了等待享用的最大时间(单位是毫秒)。

castMessage() 返回一个RspList,它包含了一个{addresses, Rsps}map;列举在dests中每个成员都有一个Rsp。

一个Rsp实例包含了响应值(或null)、异常(如果目标方法 handle()抛出异常的话)、目标成员是否是可疑成员(YBXIANG: 崩溃掉了或者正在脱离集群),等等。更多细节请参见下面的例子。

castMessageWithFuture()立即返回,返回结果是一个future。这个future可被用于获取 响应列表(现在或者稍后),它使得 任何时候一旦feature被完成时,我们设置的回调方法就会被调用。关于如何使用NotifyingFutures的细节,请参见Asynchronous calls with futures。

sendMessage()使得编程人员能够将一个单播消息发送给某单个集群成员并接收其响应。该消息的目的地址必须被设置为non-null (某个成员的有效地址)。mode参数将会被忽略(它默认被设置为ResponseMode.GET_FIRST),除非它被设置为GET_NONE,在这情况下,请求就会变成异步的,也就是我们不再等待请求的响应。

sendMessageWithFuture()立即返回,返回结果是一个future,它可被用于获取结果。

使用这些构建块的好处之一就是,故障成员会被从期望的响应集合中删除。比如,当我们发送一条消息给10个成员并等待所有响应的时,2个成员在将响应发送出去之前崩溃了,该调用就会返回8个有效的响应以及2个被标记为失败的响应。castMessage() 的返回值是一个RspList,它包含了所有的响应(我们没有显示所有的方法):

public class RspList<T> implements Map<Address,Rsp> {
 public boolean isReceived(Address sender);
 public int numSuspectedMembers();
 public List getResults();
 public List
getSuspectedMembers(); public boolean isSuspected(Address sender); public Object get(Address sender); public int size(); }

isReceived()检查是否已经收到了来自某个发送者的响应。请注意,只有在还没有收到响应并且没有成员被标记为failed的情况下,检查结果才是true。numSuspectedMembers()返回在等待期间出现故障(比如崩溃)的成员的数量。getResults()返回由各成员返回的值组成的一个列表。get()方法返回某特定成员的返回值。

4.1.2. Requests and target destinations

当一个非空的地址列表(作为目标地址列表)被传递给MessageDispatcher.castMessage()RpcDispatcher.callRemoteMethods()时,这并不意味着只有包含在该列表中的成员才会收到该消息,而是意味着,如果该调用是阻塞式的,那么我们只等待来自这些成员的响应。

如果我们想要将某消息的接收者限制为这些目标成员,那么有几个方法可疑实现:

  • 如果我们只想要将该消息发送给几个目标地址,那么使用多个单播报文。

  • 使用anycasting。比如说,如果我们有个成员关系视图{A,B,C,D,E,F},但是只期望A和C收到消息,那么将目标地址列表设置为包含A和C的一个列表,并且在将要被传递给该调用的RequestOptions中,启用anycasting(见上)。这意味着,相关传输会发出2条单播报文。

  • 使用排除列表。如果我们有个成员关系视图{A,B,C,D,E,F},并且想要将一条消息发送给除了D和E之外的所有成员,那么我们可以定义一个排除列表:将目标地址列表设置为null (等价于发送给所有成员) 或者{A,B,C,D,E,F},并且在传递给该调用的RequestOptions中,将排除列表设置为包含D和E的一个列表。

4.1.3. Example

本节展示一个如何使用MessageDispatcher的例子。

public class MessageDispatcherTest implements RequestHandler {
 Channel   channel;
 MessageDispatcher disp;
 RspList   rsp_list;
 String    props; // to be set by application programmer

 public void start() throws Exception {
  channel=new JChannel(props);
  disp=new MessageDispatcher(channel, null, null, this);
  channel.connect("MessageDispatcherTestGroup");

  for(int i=0; i < 10; i++) {
   Util.sleep(100);
   System.out.println("Casting message #" + i);
   rsp_list=disp.castMessage(null,
    new Message(null, null, new String("Number #" + i)),
    ResponseMode.GET_ALL, 0);
   System.out.println("Responses:\n" +rsp_list);
  }
  channel.close();
  disp.stop();
 }

 public Object handle(Message msg) throws Exception {
  System.out.println("handle(): " + msg);
  return "Success !";
 }

 public static void main(String[] args) {
  try {
   new MessageDispatcherTest().start();
  }
  catch(Exception e) {
   System.err.println(e);
  }
 }
}

这个例子首先创建了一条信道。接着,基于该信道创建了一个MessageDispatcher实例。然后连接该信道。从现在开始,MessageDispatcher将会发送请求,接收匹配的响应(扮演客户端角色),同时,接收请求并发送响应(扮演服务器端角色)。

然后我们发送10条消息到分组中并等待所有的响应。超时时间参数设置为0,这会导致相关调用一直阻塞着,直到收到所有的响应。

handle()方法简单地打印出一条消息并返回一个字符串。该字符串将会被作为一个响应值(在Rsp中的值)发回到调用者。如果该调用抛出一个异常,那么就会使用Rsp异常

最后,MessageDispatcher和该信道都被关闭。

4.2. RpcDispatcher【YBXIANG:这一节非常重要!】

RpcDispatcher继承自MessageDispatcher。它使得程序员能够触发 所有(或单个)集群成员的远程方法,并可选地等待返回值。通常,应用程序会首先创建一个信道,然后基于该信道创建一个RpcDispatcher。RpcDispatcher可被用于触发远程方法(扮演客户端角色),同时也可以被其它成员调用(扮演服务器端角色)。

和MessageDispatcher比较起来,没有handle()让它实现。相反,将要被调用的方法可以被直接放入使用常规方法定义的一个class中(见下面的例子)。JGroups会使用反射来触发相关方法。

要想触发远程方法调用(单播和多播),使用下面的方法:

public  RspList
 callRemoteMethods(Collection
dests, String method_name, Object[] args, Class[] types, RequestOptions options) throws Exception; public RspList callRemoteMethods(Collection
dests, MethodCall method_call, RequestOptions options) throws Exception; public NotifyingFuture> callRemoteMethodsWithFuture(Collection
dests, MethodCall method_call, RequestOptions options) throws Exception; public T callRemoteMethod(Address dest, String method_name, Object[] args, Class[] types, RequestOptions options) throws Exception; public T callRemoteMethod(Address dest, MethodCall call, RequestOptions options) throws Exception; public NotifyingFuture callRemoteMethodWithFuture(Address dest, MethodCall call, RequestOptions options) throws Exception;

调用callRemoteMethods()方法簇时,可以将一个接收者地址列表作为参数传入。如果该地址列表是null,那么该方法就会在所有的集群成员(包括发送者)上进行调用。每个调用都接受的参数有:将要被调用的目标成员 (null 意味着在所有集群成员上面进行调用),一个方法以及一个RequestOption

可以通过(1)方法名、(2)参数、和(3)参数类型来指定将要被调用的方法,也可以使用MethodCall (包含了一个java.lang.reflect.Method和相关参数)。

和MessageDispatcher一样,返回值为一个 RspList 或者 RspList对应的一个future。

callRemoteMethod()方法簇携带大多数相同的参数,除了只有一个目标地址(而不是一个地址列表)之外。如果 dest 参数是null,那么该调用将会失败。

callRemoteMethod()方法簇的调用会返回实际的结果(类型为 T),或者抛出异常(如果在目标成员上该方法抛出异常的话)。

在目标成员中,我们可以用Java的反射API,根据方法名和所提供的参数的数量以及类型,找出要执行的确切方法。如果无法找出某个方法,将会抛出一个运行期异常。

请注意,我们也应该使用method IDs 以及 MethodLookup 接口来判定方法,这会更快,也会让每个RPC携带更少的数据穿过网络。要想看看如何做,请参见MethodLookup的实现中的一些代码,比如 在RpcDispatcherSpeedTest中的相关代码。

4.2.1. Example

下面的代码展示了如何使用RpcDispatcher的一个例子:

public class RpcDispatcherTest {
 JChannel channel;
 RpcDispatcher disp;
 RspList rsp_list;
 String props; // set by application

 public static int print(int number) throws Exception {
 return number * 2;
 }

 public void start() throws Exception {
 MethodCall call=new MethodCall(getClass().getMethod("print", int.class));
 RequestOptions opts=new RequestOptions(ResponseMode.GET_ALL, 5000);
 channel=new JChannel(props);
 disp=new RpcDispatcher(channel, this);
 channel.connect("RpcDispatcherTestGroup");

 for(int i=0; i < 10; i++) {
 Util.sleep(100);
 rsp_list=disp.callRemoteMethods(null,
   "print",
   new Object[]{i},
   new Class[]{int.class},
   opts);
 // Alternative: use a (prefabricated) MethodCall:
 // call.setArgs(i);
 // rsp_list=disp.callRemoteMethods(null, call, opts);
 System.out.println("Responses: " + rsp_list);
 }
 channel.close();
 disp.stop();
 }

 public static void main(String[] args) throws Exception {
 new RpcDispatcherTest().start();
 }
}

RpcDispatcher这个类定义了一个 print()方法,我们随后会调用它。该程序的入口点 start()方法创建了一个信道,然后基于该信道创建了一个RpcDispatcher。callRemoteMethods()然后在所有集群成员上(也包括调用者)调用远程print()方法。当收到所有的响应后,该调用就返回,响应就会被打印。

如我们所见,RpcDispatcher这个构建块 通过提供应用程序和原始信道之间的一个更高的抽象层,减少了大量的、用于实现基于RCP的分组通信的应用程序代码。

Asynchronous calls with futures

当我们触发一个同步调用时,调用线程将会被阻塞,直到收到响应为止。

Future返回允许调用者立即返回,并在之后抓取调用结果。在2.9版本中,RpcDispatcher新增了2个方法,它们返回futures。

public NotifyingFuture
 callRemoteMethodsWithFuture(Collection
dests, MethodCall method_call, RequestOptions options) throws Exception; public NotifyingFuture callRemoteMethodWithFuture(Address dest, MethodCall call, RequestOptions options) throws Exception;

NotifyingFuture 扩展了 java.util.concurrent.Future,带有后者的常规方法,比如isDone(),get() 以及 cancel()。NotifyingFuture添加了setListener方法,当结果可用时,它就会被通知。如下代码所示:

NotifyingFuture future=dispatcher.callRemoteMethodsWithFuture(...);
future.setListener(new FutureListener() {
 void futureDone(Future future) {
 System.out.println("result is " + future.get());
 }
});

4.2.2. Response filters

响应过滤器允许应用程序代码 对从集群成员接收响应 进行干涉,也可以让 {请求-响应}的执行与关联操作代码知道:(1)是否可以接收某条响应,(2)是否需要接收更多的响应,或者说,该调用(如果是阻塞式的)是否可以返回。RspFilter接口看起来如下:

public interface RspFilter {
 boolean isAcceptable(Object response, Address sender);
 boolean needMoreResponses();
}

isAcceptable() 被传入一个响应值和发送该响应的成员的地址,它需要判定是有效(应该返回true)还是无效(应该返回false)。

needMoreResponses() 用于判定调用是否可以返回。

下面的实例代码展示了如何使用一个RspFilter:

public void testResponseFilter() throws Exception {
 final long timeout = 10 * 1000 ;

 RequestOptions opts;
 opts=new RequestOptions(ResponseMode.GET_ALL,
  timeout, false,
  new RspFilter() {
  int num=0;
  public boolean isAcceptable(Object response,
    Address sender) {
   boolean retval=((Integer)response).intValue() > 1;
   if(retval)
   num++;
   return retval;
  }
  public boolean needMoreResponses() {
   return num < 2;
  }
  });

 RspList rsps=disp1.callRemoteMethods(null, "foo", null, null, opts);
 System.out.println("responses are:\n" + rsps);
 assert rsps.size() == 3;
 assert rsps.numReceived() == 2;
}

这里,我们调用了一个集群范围内的RPC (dests=null),该调用会阻塞着(mode=GET_ALL),最多等待10秒(timeout=10000),但是我们也传入了一个RspFilter实例到该调用中(置于options中)。

该过滤器接受所有值大于2的响应,一旦收到满足上述条件的2个响应之后,就立即返回。

如果我们有一个RspFilter,它没有中断相关调用,即便是收到了所有成员的响应,那么我们可能会永久地阻塞着 (如果没有设置超时时间的话) ! 比如说,在上述代码中,我们有10个成员,每个成员的返回值都是1或2,那么isAcceptable()一直会返回false,因此永远不会累加 num变量,而needMoreResponses()会总是返回true;如果没有设置10秒钟的超时时间的话,该过滤器就永远不会中断相关调用!
这个问题在3.1中被修复了;如果我们收到的响应的数量和dests参数中的成员的个数相同,那么一个阻塞式调用总是会返回,而不论RspFilter如何实现。

4.3. Asynchronous invocation in MessageDispatcher and RpcDispatcher

默认情况下,MessageDispatcher 或 RpcDispatcher会通过调用RequestHandler的handle()方法,将接收到的消息派发给应用程序:

public interface RequestHandler {
 Object handle(Message msg) throws Exception;
}

在RpcDispatcher中,它的handle()方法将消息的内容转化成了一个方法调用,然后基于目标对象 执行对该方法的调用,最后返回结果(或抛出异常)。handle()的返回值然后被发送回 该消息的发送者。

该调用是同步的,也就是说,它是在一个线程中执行的,该线程负责将这条特殊的消息从网络派发出去,向上传递经过协议栈,进入应用程序。因此,在这个方法调用期间,该线程是无用的。

如果调用需要一些时间,比如,需要获取锁或者应用程序需要等待某些I/O操作,由于当前线程处于繁忙状态,所以就会使用另外一个线程来处理不同的请求消息。这就会很快地导致线程池被耗尽,或者,如果线程池带有关联的队列的话,许多消息就会进入队列进行排队。

因此,我们发明了一种新的方法,用于将消息派发给应用程序;异步调用API:

public interface AsyncRequestHandler extends RequestHandler {
 void handle(Message request, Response response) throws Exception;
}

AsyncRequestHandler接口扩展了RequestHandler,并添加了一个额外的方法,该方法接收一个请求消息(Message)参数和一个Response对象参数。该请求消息包含的信息和前面的请求消息包含的信息一样 (比如,一个方法调用及其参数)。当处理完毕时,这个Response参数用于在之后发送一个回复(如果需要的话)。

public interface Response {
 void send(Object reply, boolean is_exception);
}

Response封装了与请求的相关信息 (比如请求ID以及发送者),它有一个用于发送响应的reply()方法。is_exception 参数用于标记 该回复是否是一个异常,比如handle()方法在运行应用程序代码时所抛出的异常。

新的API的优势是,它可以,但不是必须的,被异步使用。其默认实现,依旧使用同步调用风格:

public void handle(Message request, Response response) throws Exception {
 Object retval=handle(request);
 if(response != null)
 response.send(retval, false);
}

handle()方法被调用时,它会进行异步调用并进入应用程序代码中,然后返回一个结果,之后可以将该结果送回给请求消息的发送者

然而,应用程序可以扩展MessageDispatcher或RpcDispatcher (就像在Infinispan中所做的一样),也可以通过MessageDispatcher.setRequestHandler()设置一个客户化的RequestHandler,并实现handle()方法,将其处理操作派发给来自线程池的某个线程。因此,将消息从网络上导入到这个位置的那个线程可以被立即释放得出来,然后供其它消息使用。一旦应用程序代码的触发操作完成,响应就会被发送出去,这样,线程池中的这个线程就不会被诸如I/O、获取锁、以及其它在应用程序代码中引起阻塞的任何事情之类的事情阻塞了I/O。

可以使用MessageDispatcher.asyncDispatching(boolean)方法来设置要使用的模式。甚至可以在运行期间修改模式,在同步和异步调用风格之间进行切换。

通常,异步调用和应用程序的线程池联合使用。应用程序知道(JGroups不知道)哪些请求是可以并行处理的,哪些是不能的。比如,所有的OOB调用都应该被直接地派发给线程池,因为OOB请求的顺序不重要,但是常规的请求应该被添加到一个队列中,按照次序处理。

这样做的主要好处是,请求派发(以及排序)现在处于应用程序的掌控之中了,如果应用程序想要这么做的话。如果应用程序不想掌控这些,我们依旧可以使用同步调用。

使用异步调用显得很合理的一个例子是,web session复制。如果一个集群节点A有1000个web sessions,那么在集群之中,对这些web sessions的更新信息进行复制所产生的消息来自A。由于JGroups会按照顺序将来自相同发送者的消息递交出去,因此,它也会按照严格的顺序将 与用户消息不相关的web session的更新信息 发送出去。

有了异步调用,应用程序可以设计出消息派发策略,该策略可以将不同的(不相关的)web session的更新信息指派给线程池中任何可用的线程,但是将相同的session的更新信息放入队列,然后用相同的线程处理它们,从而按照顺序处理相同session的更新信息。这么做会加速整体处理速度,因为对A上的某个web session 1的更新操作 不必等到 同样来自A的、与web session 1不相关的web session 2的所有更新处理完毕之后才进行。

这和SCOPE协议试图实现的功能类似。

异步调用API在3.3版的JGroups中加入

4.4. ReplicatedHashMap

该class用于演示如何在集群中的各节点之间共享状态。我们从没有对它进行过高度的测试,因此不能直接将其用于产品环境中。

ReplicatedHashMap在内部使用了一个并发的hashmap,允许在不同的进程中创建多个hashmaps实例(YBXIANG: 指的是ReplicatedHashMap)。所有这些实例总是具有完全相同的状态。在创建这样的实例时,集群的名字决定了集群中的哪些hashmaps参与复制。新的实例在对请求提供服务之前,将会从现有的集群成员中查询状态,并更新它自己的状态。如果还不存在集群成员,那么它会以空状态启动。

诸如put(), clear()remove()之类的修改操作 将会按照顺序被传递到所有的复制品中(YBXIANG: 指的是ReplicatedHashMap)。诸如get()之类的只读请求,只在本地hashmap上进行调用。

由于hashtable的keys和values都会被通过网络发送出去,因此,它们必须是可序列化的。如果将一个不可序列化的值放入map中,在对其序列化期间会导致一个异常。

ReplicatedHashMap 允许注册以监听通知,比如当数据被加入或删除通知。当这样的事件发生时,所有的监听器都会被通知。通知总是是在本地的;比如,在删除一个元素的时候,在所有复制品(YBXIANG: 指的是ReplicatedHashMap)中,第一个元素都会被删除,然后这些复制品 会将该删除事件(在删除动作完成后)通知给的它们的监听器。

ReplicatedHashMap 允许分组中的各成员共享共同的状态,各成员可以跨越进程,乃至跨越机器。

4.5. ReplCache

ReplCache是一个分布式缓存,和ReplicatedHashMap比较起来,它不会将它的值复制到其它所有的集群成员中,而是仅仅复制到选定的备份成员中。

put(K,V,R)方法具有一个复制计数R,它决定了{key K 和 value V}应该被存储在多少集群成员中。如果我们有10个集群成员,并且R=3,那么{K 和 V}将会被存储在3个集群成员中。如果其中的一个成员宕机了,或者离开了集群,那么另外一个不同的成员将会被告知存储{K 和 V}。ReplCache总是试图让R个集群成员都存储{K 和 V}。

复制技术取值为-1的话,这意味着{K 和 V}应该被存储在所有的集群成员中。

一个key K以及应该存储它的集群成员之间的映射 总是确定的,是通过一个持续性哈希函数计算出来的。

请注意,这个class用于展示如何在集群中的节点之间共享状态。我们从没有对它进行过高度的测试,因此不能直接将其用于产品环境中。

4.6. Cluster wide locking

在2.12版本中,添加了一个新的分布式的锁定服务,用于替换DistributedLockManager。这个新的服务作为一个协议实现,可以通过org.jgroups.blocks.locking.LockService来使用该服务。

LockService通过事件与锁定协议进行交互。分布式锁的主要抽象是对java.util.concurrent.locks.Lock的一个实现。

下面是一个经典的如何使用LockService的例子:

// locking.xml需要包含一个锁定协议,比如CENTRAL_LOCK
JChannel ch=new JChannel("/home/bela/locking.xml");
LockService lock_service=new LockService(ch);
ch.connect("lock-cluster");
Lock lock=lock_service.getLock("mylock"); // gets a cluster-wide lock
lock.lock();
try {
 // do something with the locked resource
}
finally {
 lock.unlock();
}

在这个例子中,我们创建了一个信道,然后创建了一个 LockService,接着连接该信道。如果该信道的配置没有包含一个锁定协议,那么就会抛出一个异常。然后,我们获取一个叫做"mylock"的锁,我们将会锁定它,接着释放它。如果其它成员P已经得到了"mylock",那么我们就会阻塞在那里,知道P释放掉该锁,或者P离开该集群或崩溃。

请注意,锁的拥有者总是集群中的某个线程,因此,锁的拥有者具有JGroups地址以及相关线程的ID。这意味着,如果位于相同JVM中的不同的线程试图获取名字相同的锁,那么它们就会竞争该锁。如果thread-22首先得到了该锁,那么thread-5就会阻塞着,直到thread-22释放掉该锁。

如果我们想要让该锁的拥有者只有地址(没有线程ID),那么可以将属性use_thread_id_for_lock_owner设置为false。这意味着在某个节点之内,所有的线程都可以锁定和解锁某个锁。比如: 线程 T1 锁定了 "lock", 但是线程 T2 可以对它解锁。这和java.util.concurrent.locks.Lock的意义不一样了,但是在某些场合下非常有用(在3.6中介绍过)。

JGroups包含了一个实例类(org.jgroups.demos.LockServiceDemo),可以将其用于需要使用分布式锁的交互式实验。LockServiceDemo -h 会显示出所有命令行选项。

当前 (Jan 2011),JGroups中有2个提供锁功能的协议:PEER_LOCK (过时了) 以及 CENTRAL_LOCK (推荐)。锁协议必须位于或者靠近协议栈的顶部(靠近信道)。【YBXIANG:协议栈的顶部,指的是靠近应用程序的那一段;协议栈的底部,指的是靠近传输网络的那一端,通常是UDP/TCP协议】

4.6.1. Locking and merges

下面的场景是对网络分区和后期合并敏感的场景:我们有一个集群视图{A,B,C,D},然后该集群分裂成了{A,B}和{C,D}。假设B和D仙子都获取了一个锁"mylock"。这是事情的发生经过(锁定协议是CENTRAL_LOCK):

  • 有两个协调者:{A,B}的协调者是A,{C,D}的协调者是C。

  • B成功地从A获取到了"mylock"

  • D成功地从C获取到了"mylock"

  • 这2个分区合并回了{A,B,C,D}。现在,A是协调者,而C不再是协调者

  • 问题:D依旧持有一个锁,实际上该锁已经无效了!没有简单的方法(通过Lock API)将该锁从D删除。比如说,我们可以简单地释放掉D对"mylock"的锁定,但是没有办法告诉D:它持有的这个锁实际上是无效的(stale: 陈旧的)!

因此,这里的推荐解决方案是,让各节点监听MergeView变化,如果它们预料到将要发生分区合并,那么在合并之后,重新获取它们的锁,比如:

Lock l1, l2, l3;
LockService lock_service;
...
public void viewAccepted(View view) {
 if(view instanceof MergeView) {
 new Thread() {
  public void run() {
  lock_service.unlockAll();
  // stop all access to resources protected by l1, l2 or l3
  // every thread needs to re-acquire the locks it holds
  }
 }.start
 }
}

4.7. Cluster wide task execution

2.12版的JGroups中,加入了一个分布式的执行服务。这个新的服务作为一个协议实现,通过org.jgroups.blocks.executor.ExecutionService进行使用。

ExecutionService 扩展了 java.util.concurrent.ExecutorService,会将提交给他的各任务 在集群范围内进行分发,尽可能均匀地将这些任务分发给各集群成员。当一个集群成员离开或者死亡了,正在该成员中处理的任务将会被重新分发给集群中的其它成员。

ExecutionService 利用事件和执行协议进行交互。主要的抽象是对java.util.concurrent.ExecutorService的一个实现。支持ExecutorService所有的方法。然而,限制是 callable或runnable 任务必须是实现 Serializable或Externalizable或Streamable。此外,由future产生的结果也需要实现Serializable或Externalizable或Streamable。如果Callable或Runnable不是这样,那么就会立即抛出IllegalArgumentException。如果future的结果不是这样,那么包含有相关class信息的NotSerializableException将会被作为异常原因返回给Future。

下面是一个经典的如何使用ExecutionService的一个例子:

// executing.xml needs to have an execution protocol, e.g. CENTRAL_EXECUTOR
JChannel ch=new JChannel("/home/bela/executing.xml");
ExecutionService exec_service =new ExecutionService(ch);
ch.connect("exec-cluster");
Future future = exec_service.submit(new MyCallable());
try {
 Value value = future.get();
 // Do something with value
}
catch (InterruptedException e) {
 e.printStackTrace();
}
catch (ExecutionException e) {
 e.getCause().printStackTrace();
}

在这个例子中,我们创建了一个信道,然后创建了一个ExecutionService,接着连接该信道。然后我们提交我们的Callable任务,该操作返回一个Future。然后我们等待该Future返回我们的值,然后对该值进行相关处理。如果出现任何异常,我们打印出该异常的栈追踪信息。

ExecutionService 严格地遵循Producer-Consumer模式。在这种模式下,ExecutionService作为Producer。因此,该服务只将要被处理的任务传递出去,不对这些任务执行任何实际的调用。我们单独为consumer编写了一个类,可以在集群中的任何节点上运行它。这个类是ExecutionRunner,它实现了java.lang.Runnable接口。

JGroups要求用户在集群中的一个节点上运行一个或多个ExecutionRunner实例。通过让一个线程运行某个ExecutionRunner实例,这样,该线程不会主动地运行通过ExecutionService提交到集群中的任何任务。这使得集群中的任何节点都可以参与或者不参与运行这些任务,此外,任何节点可以选择性地运行多个ExecutionRunner,如果该节点具有额外的运行能力的话。一个ExecutionRunner会无休止地运行下去,直到运行该ExecutionRunner的线程被中断。如果该ExecutionRunner被中断时,一个任务正在运行,那么该任务也会被中断。【YBXIANG:如何中断某个任务(比如正在写文件的任务)?】

下面有个例子,展示了启动一个单一节点,然后让10个分布式任务同时在它上面运行是多么容易:

int runnerCount = 10;
// locking.xml needs to have a locking protocol
JChannel ch=new JChannel("/home/bela/executing.xml");
ch.connect("exec-cluster");

ExecutionRunner runner = new ExecutionRunner(ch);

ExecutorService service = Executors.newFixedThreadPool(runnerCount);//注意,ExecutorService来自JDK!
for (int i = 0; i < runnerCount; ++i) {
 // If you want to stop the runner hold onto the future
 // and cancel with interrupt.
 service.submit(runner);
}

在这个例子中,我们创建一个信道,然后连接该信道,接着创建了一个ExecutionRunner。然后我们创建了一个java.util.concurrent.ExecutorService,我们用它启动10个工作线程,每个工作线程都运行该ExecutionRunner。这样,该节点具有10个线程,不断地接受并处理 通过ExecutionService提交到集群中的各个请求。

由于ExecutionService不允许非序列化的class实例作为任务进行传递,JGroups提供了2个辅助类来解决这个我难题。对于那些习惯了使用带有Executor的CompletionService的用户,JGroups提供了一个对等的ExecutionCompletionService,它为用户提供了相同的功能。JGroups本来打算优先使用ExecutorCompletionService,但是由于它的实现使用了 非序列化对象,因此JGroups实现了ExecutionCompletionService,用于和ExecutorService联合使用。

JGroups也设计了辅助类,用于帮助用户提交通过非序列化class实现的任务。Executions类包含了一个叫做serializableCallable的方法,该方法允许用户传入实现了Callable接口的class的一个构造函数及其参数,然后返回给用户一个Callable,它将会在运行期间自动地根据该构造函数创建相关对象,并将相关参数传给它,然后调用该对象的call方法,最后将其结果作为一个常规的callable返回。所有提供的参数仍然必须是可序列化的,返回对象如前所述。

JGroups包含了一个演示类 (org.jgroups.demos.ExecutionServiceDemo),可以将其用于带有分布式排序算法和性能的交互式实验。该演示类用于功能演示目的,不能期望其性能由于本地运行时的性能。ExecutionServiceDemo -h 会显示所有的命令行选项。

当然 (July 2011),JGroups带有1个提供执行功能的协议:CENTRAL_EXECUTOR。该执行协议必须位于或者靠近协议栈的顶部(靠近信道)。

4.8. Cluster wide atomic counters

集群范围内的计数器提供了带有名字的计数器(类似AtomicLong),可以原子性地改变。如果2个节点都累加相同的、初始值为10的计数器,那么我们将分别看到11和12。

遵循下面的步骤来创建一个带有名字的计数器:

  • ✓ 将COUNTER协议添加到协议栈配置的顶部。

  • ✓ 创建一个CounterService实例。

  • ✓ 创建一个新的带有名字的计数器,或者获取已经存在的带有名字的计数器。

  • ✓ 使用该计数器进行底层、递减、get、set、compare-and-set等操作

第一步,我们添加将COUNTER协议添加到协议栈配置的顶部:


 ...
 "2M"
   min_threshold="0.4"/>
 "60K" />
  bypass_bundling="true" timeout="5000"/>

COUNTER协议的配置在 COUNTER 描述。

接着,我们创建一个CounterService,我们将用它来创建和删除带有名字的计数器:

ch=new JChannel(props);
CounterService counter_service=new CounterService(ch);
ch.connect("counter-cluster");
Counter counter=counter_service.getOrCreateCounter("mycounter", 1);

在上面的实例代码中,我们首先创建了一个信道,然后创建了引用该信道的CounterService。接着我们连接该信道,最后创建一个新的、叫做"mycounter"的计数器,初始值为1。如果计数器已经存在,那么这个已经存在的计数器就会被返回,这里提供的初始值会被忽略。

CounterService不消耗它锁使用的信道中的任何消息;相反,它获取COUNTER协议的一个reference,然后直接调用其方法。其优点是 它是非侵入性的:可以基于相同的信道创建许多实例。CounterService甚至和其它使用相同机制的服务共存,比如LockService或ExecutionService (见上)。

返回的计数器实例实现了Counter接口:

package org.jgroups.blocks.atomic;

public interface Counter {

 public String getName();

 /**
  * Gets the current value of the counter
  * @return The current value
  */
 public long get();

 /**
  * Sets the counter to a new value
  * @param new_value The new value
  */
 public void set(long new_value);

 /**
  * Atomically updates the counter using a CAS operation
  *
  * @param expect The expected value of the counter
  * @param update The new value of the counter
  * @return True if the counter could be updated, false otherwise
  */
 public boolean compareAndSet(long expect, long update);

 /**
  * Atomically increments the counter and returns the new value
  * @return The new value
  */
 public long incrementAndGet();

 /**
  * Atomically decrements the counter and returns the new value
  * @return The new value
  */
 public long decrementAndGet();


 /**
  * Atomically adds the given value to the current value.
  *
  * @param delta the value to add
  * @return the updated value
  */
 public long addAndGet(long delta);
}

4.8.1. Design

我们在CounterService.txt中详细地描述了COUNTER的设计。

简而言之,在集群中,当前的协调者维护了 带有名字的计数器 的一个hashmap。成员发送请求 (递增,递减,等等)到协调者,协调者原子性地应用这些请求,并将响应发送回去。

这种集中处理方法的优势是:不论集群有多大,每个请求都只有固定的执行开销,也就是一个网络循环。

JGroups按照下面的方式处理ygie崩溃的或正在离开集群的协调者。协调者维护者每个计数器值一个版本号。一旦计数器的值有变化,该版本号就会被累加。对于每个修改计数器的请求,计数器的值和版本号都会被返回给请求者。请求者在本地缓存器中缓存所有计数器的值以及该值的相关版本号。

当协调者离开集群或者崩溃时,紧挨着的成员就会变成新的协调者。然后它就会启动一个协调周期,并抛弃所有的请求直到协调周期完成。协调周期 向所有成员征求它们的缓存值以及缓存值的版本号。为了降低流量,请求也携带了所有版本号。

客户端的返回值的版本比新的协调者的值的版本要高。新的协调者等待所有成员的响应或者直到超时。然后,它就会用版本比它自己的版本高的值来更新它自己的hashmap。最后,它停止抛弃请求,并发送一条重发消息给所有的客户端,以便让客户端重新发送可能处于挂起状态的请求。

我们还要考虑另外一种边界情况:如果一个客户端P更新了一个计数器,然后P和协调者都崩溃了,那么相关更新就会丢失。要想减少这种情况发生的概率,COUNTER协议可以启用 将所有计数器变化复制到一个或多个备份协调者中的功能。叫做 num_backups 的属性 定义了这样的备份协调者的数量。一旦计数器在当前的协调者中有所变化,那么也会导致备份协调者做响应更新(异步地)。如果将num_backups设置为0,表示禁止该功能。

5. Advanced Concepts

本章讨论关于如何正确地使用和配置JGroups的一些更高级的概念。

5.1. Using multiple channels

当我们使用一个完整的、虚拟的、同步的协议栈的时候,性能可能不是很好,因为协议栈中出现了大量的协议。然而,对于某些应用程序而言,吞吐量比顺序更重要,比如视频/音频流 或 飞机追踪。在后面这种情况下,正确地将飞机在管制区之间进行 切换控制是非常重要的,但是,如果有一定数量(少量)的雷达追踪消息(用于判定飞机的具体位置)丢失了,这不是什么大问题。第一种消息不会非常频繁地出现 (通常,每个小时出现一定数量的这种消息),然而,第二种消息可能以10-30条每秒的速率进行发送。这种场景也适应于一个分布式白板(YBXIANG:说的是org.jgroups.demos.Draw这个演示类,它会弹出一个共享白板):代表视频或音频数据流的消息 必须被尽快地递交出去,然而代表在白板上绘制的图形的那些消息 或 代表新成员加入该共享白板的那些消息 必须按照一定的顺序递交出去。

我们可以通过使用两条单独的信道来解决这种应用程序的要求:一条信道用于发送控制类型的消息,比如分组成员关系,floor控制等等,另外一条信道用于发送数据类型的消息,比如视频/音频流 (实际上,你可以考虑使用一条信道来发送音频,使用另外一条发送视频)。控制类型的信道可以用于虚拟同步,该信道相对慢一些,但是,加强了排序和重发功能,数据信道可以使用一条简单的UDP信道,可以包含一个分割/重组协议层,但是不应该包含重传协议成 (重传丢失的数据包的话开销太大)。

5.2. Sharing a transport between multiple channels in a JVM

传输协议 (UDP, TCP) 可使用协议栈的所有资源:默认的线程池、OOB线程池以及定时器线程池。如果我们在相同的JVM中使用多条信道,那么我们可以创建一个singleton传输协议,让所有信道的协议栈(比如说4个协议栈)共享该传输协议,而不是为每条信道的协议栈单独创建一个传输协议。

如果各信道所使用的传输协议恰好相同 (比如说,4条信道都使用了UDP),那么我们就可以让多条信道共享传输协议,比如,只创建一个UDP实例。传输协议的实例只创建并启动一次;在创建第一条信道时 创建该传输协议实例,在最后一条信道被关闭时 关闭该传输协议实例。

如果我们4条信道,位于同一个JVM之内 (就像在诸如JBoss这样的应用程序服务器中的那样),那么我们就创建12个单独的线程池(每个传输协议使用3个线程池,总共4个传输协议(YBXIANG: 这里说的是不共享传输协议的情形,每条信道都拥有自己单独的传输协议))。如果这4条信道共享传输协议的话,那么线程池的个数就可以减少到3。

每条基于共享的传输协议层而创建的信道 必须加入 不同的集群。如果某条使用共享的传输协议层的信道 试图 连接到某个集群上,而另外一条信道已经通过相同的传输协议层连接到该集群上了,那么就会抛出异常。【YXIANG: 这里说的是位于相同JVM中的多条信道。如果使用共享的传输协议层的多条信道加入相同的集群,那么就乱套了,传输协议层无法知道某条消息来自/去向哪条信道!】

共享传输协议的做法需要 在"共享的传输协议层与基于该协议层的各种不同协议栈"之间 复用和解复用 消息;假设我们有3条信道 (C1连接到"cluster-1"上,C2连接到"cluster-2"上,C3连接到"cluster-3上"),它们基于共享的传输协议层发送消息,JGroups使用各信道所连接的集群名字将消息复用到共享的传输协议层上:当C1发送一条消息的时,就为该消息添加一个包含了集群名字("cluster-1")信息的包头

当某条带有"cluster-1"包头的消息被共享的传输协议层接收到时,该包头就会被用于解复用该消息,然后将其派发到正确的信道上 (本例中,是C1) 进行处理。

多条信道如何共享同一个传输协议层:

这里,我们可以看见,4条信道使用了2个传输协议。注意,前3条共享传输协议层"tp_one"的信道,在这个共享传输协议层之上的其它协议都是相同的。这不是必须的;在"tp_one"之上的、分别属于3条信道的各个协议可以是不同的,只要使用该共享传输协议成的所有应用程序 对 传输协议的配置要求 是一样的就可以了。

传输协议"tp_two"供右边的应用程序使用。

请注意,共享传输协议的信道的物理地址 对于所有连接上的信道而言,是相同的,因此共享第一个传输协议的所有应用程序具有相同的物理地址192.168.2.5:35181

要想使用共享的传输协议,我们只需要在传输协议的配置中添加一个"singleton_name"属性既可。所有使用相同singleton名字的信道都会被共享<UDP singleton_name="tp_one"/>

现在,所有使用该配置的信道,都会共享传输协议"tp_one"。在右边的信道使用了不同的配置,其singleton_name是"tp_two"。

5.3. Transport protocols

传输协议指的是在协议栈底层的协议,它负责从网络上收发消息。JGroups支持很多传输协议。我们将在下面的小节中讨论这些协议。

一个使用UDP的经典协议栈配置如下:

<config xmlns="urn:org:jgroups"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="urn:org:jgroups http://www.jgroups.org/schema/jgroups.xsd">
 <UDP
 mcast_port="${jgroups.udp.mcast_port:45588}"
 ucast_recv_buf_size="20M"
 ucast_send_buf_size="640K"
 mcast_recv_buf_size="25M"
 mcast_send_buf_size="640K"
 loopback="true"
 discard_incompatible_packets="true"
 max_bundle_size="64K"
 max_bundle_timeout="30"
 ip_ttl="${jgroups.udp.ip_ttl:2}"
 enable_diagnostics="true"

 thread_pool.enabled="true"
 thread_pool.min_threads="2"
 thread_pool.max_threads="8"
 thread_pool.keep_alive_time="5000"
 thread_pool.queue_enabled="true"
 thread_pool.queue_max_size="10000"
 thread_pool.rejection_policy="discard"

 oob_thread_pool.enabled="true"
 oob_thread_pool.min_threads="1"
 oob_thread_pool.max_threads="8"
 oob_thread_pool.keep_alive_time="5000"
 oob_thread_pool.queue_enabled="false"
 oob_thread_pool.rejection_policy="Run"/>

 <PING timeout="2000"
 num_initial_members="3"/>
 <MERGE3 max_interval="30000"
 min_interval="10000"/>
 <FD_SOCK/>
 <FD_ALL/>
 <VERIFY_SUSPECT timeout="1500" />
 <BARRIER />
 <pbcast.NAKACK2 use_mcast_xmit="true"
  retransmit_timeout="300,600,1200"
  discard_delivered_msgs="true"/>
 <UNICAST3 timeout="300,600,1200"/>
 <pbcast.STABLE stability_delay="1000" desired_avg_gossip="50000"
  max_bytes="4M"/>
 <pbcast.GMS print_local_addr="true" join_timeout="3000"
 view_bundling="true"/>
 <UFC max_credits="2M"
 min_threshold="0.4"/>
 <MFC max_credits="2M"
 min_threshold="0.4"/>
 <FRAG2 frag_size="60K" />
 <pbcast.STATE_TRANSFER />
config>

简而言之,这些协议包括:

UDP

这是传输协议。它使用了IP多播,将消息发送到整个集群中 或者 单个节点上。其他传输协议包括TCP和TUNNEL。

PING

这是发现协议。它使用IP多播 (默认) 来寻找初始成员。一旦发现了初始成员,就会找出当前的协调者,然后发送一条单播JOIN请求到协调者,以便加入集群。

MERGE3

该协议用于将 "子集群" 合并成一个集群,在网络分区愈合之后,消除"子集群"。

FD_SOCK

基于sockets的故障探测 (在成员之间,以环的形式探测)。如果某成员出故障了,就会产生通知。

FD / FD_ALL

基于心跳消息"are-you-alive"的故障探测。如果某成员出故障了,就会产生通知。

VERIFY_SUSPECT

重复验证某个可疑成员是否死掉了,如果没死,由它下面的协议生成的 含可疑成员信息的数据包 就会被丢弃。

BARRIER

用于传递状态;该协议会阻塞 那些会修改共享状态的消息,直到收到一条摘要之后,才会解阻塞所有的线程。如果没有共享状态,就不需要该协议。

pbcast.NAKACK2

该协议确保 (a) 消息的可靠性 以及 (b) FIFO。消息可靠性 确保了 消息一定会被收到。如果没有收到,那么接收者将会要求重发。FIFO确保了从发送者P发出的所有消息 会被 按照P发送它们的顺序进行接受。

UNICAST3

和NAKACK相同,用于单播消息:从P发送出的消息不会被丢失 (如果需要的话会重新传输),并且会按照FIFO顺序收发消息 (概念上,和TCP/IP中的TCP一样)

pbcast.STABLE

该协议用于删除已经被所有成员看到的消息 (分布式消息的垃圾回收)

pbcast.GMS

成员关系协议。负责处理 加入/离开的成员 以及 安装新的成员关系视图。

UFC

单播流控。为两个成员之间的通信提供了数据流速控制。

MFC

多播流控。为某个发送者和所有集群成员之间的通信提供了数据流速控制。

FRAG2

将大型的消息分割成更小的消息,并在接收者那一侧组装起来。用于多播和单播消息。

STATE_TRANSFER

确保状态被正确地从一个现有成员(通常是协调者)传递到一个新的成员。

5.3.1. Message bundling

在发送许多很小的消息时,消息捆绑功能就很有用;该功能会将这些小消息进行排队,直到这些小消息能够汇聚成具有一定尺寸的消息,或者汇聚等待超时。之后,排队的小消息就会被组装成一个大一点的消息,然后就会将这个大消息发送出去。在接收端,这个大消息被拆卸,然后将这些小消息向协议栈的上方发送。

在发送许多小消息的时候,消息的负载和消息的包头的比率可能很小;假设我们发送了一个"hello"字符串,负载是7个字节,然而地址和包头(取决于协议栈的配置)可能是30个字节。然而,如果我们将(假设)100条消息捆绑在一起,那么这条大消息的负载就是700字节,而包头依旧是30字节。这样,通过一条大的消息,而不是许多小的消息,我们就能够在网络上发送更多的有效数据。

消息捆绑在概念上类似TCP的零窗口(Nagling)算法.

一个实例配置如下:

<UDP
 enable_bundling="true"
 max_bundle_size="64K"
 max_bundle_timeout="30"
/>

这里,捆绑功能被启用了 (默认的)。最大汇聚尺寸为 64'000 字节,我们最多等待 30毫秒。如果在T0时刻,我们准备将总大小为2'000字节的10条小消息捆绑发送出去,但是接着不会发送更多消息,那么JGroups就会等待直到30毫秒之后超时,之后就将这些小消息打包成一条大消息M,并将M发送出去。如果我们要发送1000条消息,每条消息为100字节,那么在达到64'000 字节之后 (大约64条消息之后),我们将吧这条大消息发送出去,这可能只需要3毫秒。

在 3.x版本的JGroups中,消息绑定是默认的,因此再也不能启用或禁用了 (相关配置被忽略了)。然而,可以为某条消息设置 DONT_BUNDLE 标志位来跳过捆绑。
Message bundling and performance

尽管消息捆绑功能在异步地发送许多小消息时性能很好,不过在触发同步RPC时性能可能就会很差了:比如,我们正在通过RpcDispatcher (参见RpcDispatcher)在集群中触发10个同步的(阻塞式的) RPCs,由每个调用的所有序列化参数构成的负载小于64K。

由于RPC是阻塞式的,我们将会等待该调用返回之后才调用下一个RPC。

对于每个RPC而言,其请求都会等满30毫秒的超时时间,同样,每个响应也会等满30毫秒的超时时间,这样每次调用总共要消耗60毫米昂。因此10个阻塞式RPCs 总计会消耗600毫秒!

很明显,这是无法接受的。然而,这里有个简单的解决方案:我们可以使用消息标志位(参见 Tagging messages with flags)来覆盖在传输协议中设置的默认绑定行为:

RpcDispatcher disp;
RequestOptions opts=new RequestOptions(ResponseMode.GET_ALL, 5000)
  .setFlags(Message.DONT_BUNDLE);
RspList rsp_list=disp.callRemoteMethods(null,
   "print",
   new Object[]{i},
   new Class[]{int.class},
   opts);

RequestOptions.setFlags(Message.DONT_BUNDLE)会为消息打上DONT_BUNDLE标签。当消息被传输协议发送时,它就会被立即发送出去,而不管是否在传输协议中启用了绑定功能。

对于10个阻塞式的RPCs而言,使用DONT_BUNDLE标志位来触发print()只需要几毫秒,不使用该标志位则需要 600 毫秒。

除了设置DONT_BUNDLE标志位,还有一种方法,那就是使用 futures 来触发这10个阻塞式RPCs:

List> futures=new ArrayList>();
for(int i=0; i < 10; i++) {
 Future future=disp.callRemoteMethodsWithFuture(...);
 futures.add(future);
}

for(Future future: futures) {
 RspList rsp_list=future.get();
 // do something with the response
}

这里,我们使用了 callRemoteMethodsWithFuture(),它会立即返回(尽管该调用是阻塞式的!) 一个 future。在触发这10次调用之后,我们才从它们的futures中获取相关调用结果。

和上面的几个毫秒比较起来,这段代码大约需要 60 毫秒 (30毫秒用于请求,30毫秒云南哦关于响应),但这依旧比不使用DONT_BUNDLE标志位时消耗的600毫秒好很多。请注意,如果这10个请求的汇聚尺寸超过了 max_bundle_size,那么这个大的消息就会被立即发送出去,因此发送请求可能会快于30毫秒。【YBXIANG:如果我们有12个RPCs调用,难道我们要用这段代码首先发送前10个,然后再用前面的代码将后2个打上DONT_BUNDLE标志位后再发送?或者打成一个超过max_bundle_size的大消息(万一该消息超过了64K字节怎么办)?此外,我们如何知道每个RPCs的负载究竟有多大?这段代码其实是控制代码,类似流控代码,写的不好可能会出大问题。该解决办法不现实!还是直接使用DONT_BUNDLE标志位更好】

5.3.2. UDP

UDP使用了IP多播来将消息发送给集群中的所有成员,使用UDP数据报处理单播消息(发送给单个成员)。UDP启动时,它打开一个单播和多播socket:单播socket被用于收/发单播消息,而多播socket被用于收/发多播消息。信道的物理地址是 单播socket的UDP地址和端口号。

Using UDP and plain IP multicasting

使用UDP作为传输协议的协议栈,通常被用于集群中,其成员运行在相同的host上,或者分布在LAN中。请注意,在不同的子网中运行多个协议栈实例之前,管理员应该确保IP多播包允许跨越这些子网。通常会出现IP多播包无法跨越子网的情况。如何运行一个测试程序,来判定各集群成员是否能够通过IP多播进行交互,请参见 It doesn’t work ! 这一节。如果还是不行,那么该协议栈就不用使用UDP的IP多播作为其传输协议。在这种情况下,协议栈必须使用非多播的UDP,或者使用不同的传输协议,比如TCP。

Using UDP without IP multicasting

使用UDP和PING作为底层协议的协议栈 默认使用IP多播 来将消息发送到所有成员(UDP) 以及发现初始成员 (PING)。然而,如果不能使用IP多播,我们可以配置 UDP协议和PING协议,让它们发送多条单播消息,而不是一条多播消息。

尽管多条单播消息不如一条多播消息高效(而且使用更多的带宽),有时候这是能够到达所有集群成员的唯一办法。

要想配置UDP,让它通过多条单播消息来发送 一条分组消息,而不是使用IP多播来发送,那么必须将 ip_mcast 属性设置为 false。

如果我们禁用了ip_mcast,那么我们改变发现协议(PING)。因为PING协议要求在该传输协议中启用IP多播,我们不能使用它。替代品有:TCPPING (成员地址的静态列表)、TCPGOSSIP (外部查询服务)、FILE_PING (共享的目录)、BPING (使用广播) 或者 JDBC_PING (使用共享的数据库)。

关于如何配置各种发现协议的细节,请参见Initial membership discovery。

5.3.3. TCP

在不能使用IP多播的情况下,TCP是UDP的一个替代品。当通过WAN操作时,就是这样的情况,在WAN中,路由器可能会抛弃IP多播包。通常UDP在LAN中作为传输协议,而TCP用于集群分布在多个WANs中的场合。

一个基于TCP的经典协议栈的配置属性看起来如下 (为了简便起见,我们修改过):

<TCP bind_port="7800" />
<TCPPING timeout="3000"
 initial_hosts="${jgroups.tcpping.initial_hosts:HostA[7800],HostB[7801]}"
 port_range="1"
 num_initial_members="3"/>
<VERIFY_SUSPECT timeout="1500" />
<pbcast.NAKACK2 use_mcast_xmit="false"
 retransmit_timeout="300,600,1200,2400,4800"
 discard_delivered_msgs="true"/>
<pbcast.STABLE stability_delay="1000" desired_avg_gossip="50000"
 max_bytes="400000"/>
<pbcast.GMS print_local_addr="true" join_timeout="3000"
 view_bundling="true"/>
TCP

传输协议,使用TCP (来自TCP/IP) 来发送单播和多播消息。在后面这种场合中,它发送多条单播消息。

TCPPING

该协议用于发现初始成员关系,从而判定协调者。接着,会发送加入请求给协调者。

VERIFY_SUSPECT

重复验证某个可疑成员是否已经死了。

pbcast.NAKACK

可靠的FIFO消息传递

pbcast.STABLE

对被所有成员看见的消息进行分布式垃圾回收。

pbcast.GMS

成员关系服务。负责处理 加入和删除 新的/旧的成员,发出视图变化通知。

当使用TCP时,发送给所有集群成员的消息会被当作多条单播消息发送出去 (每个成员对应一条消息)。由于不能使用IP多播来发现初始成员,必须使用其它机制来发现初始成员关系。这里有一些可选机制(关于所有发现协议的讨论,请参见 Initial membership discovery ):

  • TCPPING: 使用一个包含已知的分组成员的列表,用于请求初始成员关系。

  • TCPGOSSIP: 该协议需要一个 GossipRouter (见下),这是一个外部进程,扮演查询服务的角色。集群成员利用其集群名字进行注册,新的成员查询该GossipRouter来获取集群的初始成员关系信息。

下面谅解展示如何将TCP和TCPPING/TCPGOSSIP联合使用:

Using TCP and TCPPING

一个使用了TCP和TCPPING的协议栈看起来如下 (省略了其它协议):

<TCP bind_port="7800" /> +
<TCPPING initial_hosts="HostA[7800],HostB[7800]" port_range="2"
 timeout="3000" num_initial_members="3" />

TCPPING背后的理念是:我们选中的某些集群成员 承担着 提供初始成员关系的已知主机角色。在本例中,HostA和HostB就是被选定的成员,TCPPING通过这两个成员来查询初始的成员关系。在TCP中的属性"bind_port"意味着每个成员都应该试图为其自己指定端口7800。如果不能使用该端口,则试用下一个端口,以此类推,直到它找到一个未被试用的端口为止。

TCPPING试图跟HostA和HostB联系,从端口7800开始测试,直到端口 {7800 + port_range},在上面这个例子中,端口范围为78007802。假设HostA或HostB至少有一个启动好了,那么TCPPING就会收到一条响应。要想能够绝对地收到一条响应的话,推荐将所有带有该配置的集群成员所在的主机都添加到hosts属性中。

Using TCP and TCPGOSSIP

TCPGOSSIP 使用一个或多个GossipRouters来:(1) 注册该成员自己 (2) 获取已经注册过的集群成员的信息。配置看起来如下:

<TCP />
<TCPGOSSIP initial_hosts="HostA[5555],HostB[5555]" num_initial_members="3" />

initial_hosts属性的值是一个以逗号分隔的GossipRouter列表。在这个例子中,有2个GossipRouters,位于HostA和HostB上,端口是5555

一个成员总是会将其自己注册到所有列举在initial_hosts属性中的GossipRouters中,但是从第一个可访问的GossipRouter获取信息。如果某个无法访问某个GossipRouter,那么就会将其标识为出故障的GossipRouter,并从该列表中删除。然后启动一个任务,周期性地试图重连到出故障的GossipRouter。如果重连成功,那么这个故障的GossipRouter就会被标识为无故障的,并被重新插入到该列表中。

使用多个GossipRouters的好处是,只要有至少一个GossipRouter在运行着,那么新的成员总是能够获取到初始成员关系信息。

请注意,在任何成员启动之前,我们应该首先启动GossipRouter。

YBXIANG提示:GossipRouter应该是和JGroups进程无关的一个Gossip路由器,专门负责存储集群的共享信息。为了防止GossipRouter成为单点故障,因此应该使用多个GossipRouter。

5.3.4. TUNNEL(透传:集群成员通过隧道服务器进行中转透传)

防火墙通常置于内网连接到互联网之前。防火墙通过筛选进入内网的数据流拒绝外部机器试图连接防火墙之内的主机,从而保护了本地网络免遭外部攻击。大多数防火墙系统,允许位于防火墙之内的主机连接到防火墙之外的主机上 (输出的数据流),然而大多数情况下会彻底禁止输入的数据流。【YBXIANG:有时候,防火墙只允许外部主机连接到内部主机的特殊端口上,比如80端口,以获取内部主机对外提供的服务。有时候,防火墙彻底禁止外部主机连接到内部主机上,也就是说禁止内部主机作为SocketServer;只允许内部主机连接防火墙之外的SocketServer,内部主机和外部主机之间建立起一条Socket通道,它们利用Socket的全双工特性进行交互。这就是TUNNEL协议作为中转透传隧道(SocketServer)的应用的场景。】

隧道主机协议,该协议封装了其它协议,它会在某一端复用其它协议,然后在另外一端解复用这些协议。任何协议 都可以通过 一个隧道协议 进行 透传。

防火墙的大多数约束性设置 通常是 禁用 所有 流入的数据流,只启用一些选定的端口供 输出数据流(YBXIANG: 禁止外部主机连接进来,只准内部主机连接出去)。在下面的解决方案中,我们假设防火墙启用了一个TCP端口,以供内部主机向外连接到GossipRouter上。

JGroups有一种允许程序员透传防火墙的机制。该解决方案涉及到一个GossipRouter,它位于防火墙之外,因此其它成员(可能位于防火墙之内)可以访问它。

该解决方案工作方式如下。位于防火墙之内的某条信道 必须使用 TUNNEL协议,而不是UDP或TCP作为传输协议。推荐的发现协议为PING协议。这里有个配置:

<TUNNEL gossip_router_hosts="HostA[12001]" />
<PING />

TUNNEL协议使用了一个运行在HostA上、端口为12001的GossipRouter (位于防火墙之外)来进行透传。请注意,如果使用了TUNNEL协议,就不推荐使用TCPGOSSIP提供发现服务,请使用PING协议。TUNNEL协议可以接受一个或多个提供透传功能的GossipRouters;可以在gossip_router_hosts属性中,以“host[port]”格式列举这些GossipRouters,并用逗号隔开。

TUNNEL协议会建立一条TCP连接到GossipRouter进程(位于防火墙之外),GossipRouter接受来自各成员的消息,并将这些消息传递给其它成员。该连接由位于防火墙之内的主机发起,只要该信道连接在一个集群上,该连接就会一直存在。GossipRouter将会使用相同的连接将incoming消息发送回这条发起连接的信道。 这么做是完全合法的,因为TCP连接是全双工的。请注意,如果GossipRouter试图建立它自己的TCP连接到防火墙之内的信道上,就会失败。不过,重用由信道创建的、现成的TCP连接就够了。

请注意,必须为TUNNEL协议设置GossipRouter进程所在的hostname以及端口号。这个例子假设某个GossipRouter运行在HostA上的12001端口上。TUNNEL协议可以接受一个或多个GossipRouter主机,可以在gossip_router_hosts属性中,以“host[port]”格式列举这些GossipRouters,并用逗号隔开。

任何时候,当信道发出一条消息时,TUNNEL协议就会将该消息转发给GossipRouter,GossipRouter负责将其派发给其接收者:如果消息的目标域为null (发送到分组中的所有成员),那么GossipRouter就会查找属于该分组的所有成员,然后通过这些成员在连接该GossipRouter时创建的那些TCP连接,将该消息转发给这些成员。如果该目标地址是一个有效的成员地址,那么GossipRouter就会查找该成员的TCP连接,然后将该消息转发给它。

为了实现这种功能,GossipRouter维护了 集群名字和成员地址、TCP连接之间的 一个映射关系。

单个GossipRouter不是单点故障。在设置多个gossip routers的时候,这些routers不会相互通信,每个信道简单地与多条可用的routers相连接,这样就避免了单点故障。在一个或多个routers宕机的情况下,集群成员依旧可以通过剩余的可用的router实例进行交互,只要还有这样的router实例既可。

对于每个发送调用而言,信道会查询连接routers的、可用的连接列表,利用每条连接来发送消息,直到发送成功为止。如果任何连接都不能将该消息发送出去,那么就会出现异常。如何选择连接的默认策略是:随机性的选择。不过,我们提供了一个插件解开,以供使用其它策略。

GossipRouter配置是静态的,在信道的生命周期内是无法更新的。必须通过信道的配置文件 来提供 可用的routers列表。

要想让JGroups透传一个防火墙,需要采取下面的步骤:

  • ✓ 确认在防火墙中启用了一个TCP端口(比如说12001),以供输出数据流。

  • ✓ 启动GossipRouter:

java org.jgroups.stack.GossipRouter -port 12001
  • ✓ 按照上面的指示配置TUNNEL协议层。

  • ✓ 创建一条信道

常规设置参见Tunneling a firewall:

Figure 4. Tunneling a firewall

首先,在主机B上创建了一个GossipRouter进程。请注意,主机B应该位于防火墙之外,位于相同分组中的所有信道 应该使用相同的GossipRouter进程。当在主机A上创建一条信道时,该信道的TCPGOSSIP协议层就会将其地址注册到GossipRouter中,然后获取初始成员关系信息 (假设初始成员为C)。现在,主机A就建立了一条连接到该GossipRouter的TCP连接;该连接将一直存在,直到A崩溃或者主动地离开该集群。当A多播一条消息到集群时,GossipRouter就会查询所有的集群成员 (在本例中,为A和C),然后利用这些成员的TCP连接将该消息转发给所有的成员。在本例中,A会收到由它自己发出的那条多播消息的一个拷贝,另外一份拷贝会被发送给C。

这种设计允许,比如Java applets(它们只被允许连接回提供下载它们的主机),使用JGroups:HTTP server位于主机B上,GossipRouter daemon也运行在该主机上。一个applet被下载到A或者C上,该applet被允许创建一条连接到B的TCP连接。此外,位于防火墙之内的多个应用程序可以相互交互并形成一个集群。

然而,这种方案有好几个缺陷:首先,必须维护一条TCP连接,该连接持续存在,这会耗尽其主机系统的资源(比如GossipRouter的资源),从而导致扩展性问题,此外,如果有几个信道位于防火墙之内,那么该方案就不妥了,绝大多数信道实际上可以使用IP多播进行通信,最后,在一个防火墙内的2个端口上允许outgoing数据流通过并不总是可能的,比如说,当防火墙不属于用户时。

YBXIANG:我们通常不会在相同的主机上创建不同的GossipRouter供位于相同集群中的成员访问,这么做纯粹是浪费资源,如法避免GossipRouter主机宕机这种单点故障。因此,上图有点问题,第一个GossipRouter应该位于B上,第二个GossipRouter应该位于D上,这样才有意义。本人猜测,作者的本意是,这2个位于相同主机上的GossipRouter是供不同的集群使用的。

5.4. The concurrent stack

并发协议栈 (在2.5中引入)对前期版本做了大量的改进, 前期版本有些缺陷:

  • 大量的线程:默认情况下,每个协议有2个线程,一个用于上行队列,一个用于下行队列。可以通过设置协议的up_thread=false/down_thread=false来禁用这2个线程。在新的模型中,这些线程被删除了。

  • 按顺序发送消息:JGroups以前有一个单独的队列,用于对输入的消息进行排队,由一个线程来处理这些消息。因此,来自不同发送者的消息依旧以FIFO的顺序进行处理。在2.5中,来自不同发送者的消息可以被并行地处理。

  • 带外消息:当一个应用程序不关心消息的顺序属性时,可以为之设置OOB标志位,JGroups将在递交这种特殊的消息时,忽略其顺序。(YBXIANG:OOB:Out of Band)

5.4.1. Overview

并发协议栈的架构显示在The concurrent stack中。对传输协议(TP, 其子类包括 UDP, TCP and TCP_NIO)做了彻底的修改。因此,用户要想配置这个并发协议栈,就必须在XML文件中修改 比如UDP的配置。

Figure 5. The concurrent stack

这个并发协议栈包含了2个线程池(java.util.concurrent.Executor):带外(OOB)线程池和常规线程池。数据报文由 多播或者单播接收线程(UDP) 接收,或者由ConnectionTable (TCP, TCP_NIO)接收。被表示为OOB的报文(使用Message.setFlag(Message.OOB)来打标记) 被派发给OOB线程池,其它报文被派发给常规线程池。

如果某个线程池被禁掉了,那么我们就是Caller所在的线程(也就是单播或多播接收器线程 或者 ConnectionTable)来将消息发送给上面的协议栈,然后向上传入应用程序。否则,消息就会被来自线程池的线程处理,这些线程会将消息发送给上面的协议栈。当所有的当前线程处于繁忙状态时,JGroups就会创建另外一个线程来处理消息,直到达到定义的最大线程数。此外,还可以选择将报文放入队列进行排队,直到有空闲线程来处理。

使用线程池的一个要点是,接收者线程应该只负责接收报文并将这些报文转发给线程池处理,因为对消息进行反序列化并做相关处理 比单纯接收消息要慢一些,前者(对消息进行反序列化并做相关处理)可以从并行处理中获益。

Configuration

请注意,这只是初步的定义,名字或属性可能会有所改变。

我们正在考虑:通过编程方式,将线程池暴露出来,这意味着开发者能够通过编程的方式来配置线程池,比如说,使用TP.setOOBThreadPool(Executor executor)这样的代码进行配置。

下面是新配置的一个例子:

<UDP
 thread_naming_pattern="cl"

 thread_pool.enabled="true"
 thread_pool.min_threads="1"
 thread_pool.max_threads="100"
 thread_pool.keep_alive_time="20000"
 thread_pool.queue_enabled="false"
 thread_pool.queue_max_size="10"
 thread_pool.rejection_policy="Run"

 oob_thread_pool.enabled="true"
 oob_thread_pool.min_threads="1"
 oob_thread_pool.max_threads="4"
 oob_thread_pool.keep_alive_time="30000"
 oob_thread_pool.queue_enabled="true"
 oob_thread_pool.queue_max_size="10"
 oob_thread_pool.rejection_policy="Run"/>

这2个线程池的属性分别以 thread_pool和oob_thread_pool 开头。

相关属性列举在下面。大致和JDK5中的java.util.concurrent.ThreadPoolExecutor选项相对应。

Table 3. Attributes of thread pools
Name Description

thread_naming_pattern

该属性决定了如何命名在并发协议栈中的线程池的工作线程。有效值包括任何"cl"字母组合,这里"c"包括集群的名字,"l"包括信道的地址。默认值是"cl"

 

enabled

是否使用线程池。如果设置为false,则使用调用者的线程。

 

min_threads

所使用的工作线程的最小数目。

 

max_threads

所使用的工作线程的最大数目。

 

keep_alive_time

工作线程进入空闲状态之后,被放回线程池之前,空闲的毫秒数。

 

queue_enabled

是否要使用一个(有边界的)队列。如果允许,那么当所有最少线程处于繁忙状态时,工作任务就会被添加到队列中。当队列满了之后,就会(为每个新加入的任务)创建额外线程,达到max_threads数。当达到max_threads时(而且队列是满的),就会参照拒收策略执行。

 

max_size

队列中元素的最大数量。如果禁止使用队列,则忽略该属性。

 

rejection_policy

该属性决定 当线程池满了(队列也满了,如果启用了队列的话)的时候该怎么做。默认是利用调用者的线程来处理。"Abort"选项会抛出一个运行期异常。"Discard"会抛弃新消息。"DiscardOldest"会抛弃在队列中的最老的条目。请注意,这些选项值可能会改变,比如,在未来可能会加入"Wait"值。

 

5.4.2. Elimination of up and down threads

通过消除每个协议层对应的2个队列和2个线程,我们有效地减少了处理消息的线程数,因此减少了上下文切换时的开销。Channel.send()的语义也更加清晰明了:现在,所有消息都从调用者线程开始,沿着协议栈向下发送,只有当消息已经被发送到网络上之后,send()调用才返回。此外,如果消息依旧没有被放入一个重发缓存的话,异常将会被传递回该调用者。否则,JGroups就会简单地打印错误消息的日志,而不是继续重发该消息。因此,如果调用者得到了一个异常,调用者就应该重发该消息。

在接收端,一条消息被一个线程池处理,要么是常规线程池,要么是OOB线程池。这两个线程池都可以被完全地去除,这样我们就能节省更多的线程,从而进一步地减小上下文切换开销。要点是:现在,开发者需要能够几乎完全地控制线程行为。

5.4.3. Concurrent message delivery

直到2.5版,所有消息都是由一个单独的线程处理,即使这些消息由不同的发送者发送。比如,如果A发送了消息1,2 和 3,而B发送了消息34 和 45,如果A的消息已经被全部收到了,那么B的消息 34 和 35 很可能在A的1~3消息被处理完毕之后,才被处理!

现在,我们可以并行地处理来自不同发送者的消息,比如来自A的消息1,2和3可以被线程池中的某个线程处理,来自B的消息34和35可以由另外一个不同的线程处理。

这样,如果一个集群有N个节点,每个节点都正在发送消息,而且我们为线程池配置了至少N个线程,那么我们的速度就快了将近N倍。有一个实际的单元测试(ConcurrentStackTest.java)来展示该并发协议栈的性能。

5.4.4. Scopes: concurrent message delivery for messages from the same sender

在3.3版中,SCOPE被MessageDispatcher和RpcDispatcher中的异步触发取代。在4.x版本中,SCOPE可能会被删除。

在前面的段落中,我们描述了并发协议栈如何并发地递交来自不同发送者的消息。但是来自相同发送者P的所有消息(非OOB消息)会按照P发送它们的顺序被递交出去。然而,这对于某些应用程序而言并不足够好。

让我们分析一个应用程序,它会复制HTTP sessions。如果我们有 sessions X, Y和Z,那么对这些sessions的更新信息就会被按照更新它们时的顺序递交出去,比如:X1, X2, X3,Y1, Z1, Z2, Z3, Y2, Y3, X4。这意味着 更新信息Y1 必须等到 更新信息X1-3 被交出出去之后。如果这些更新需要耗费一些时间,比如说,花费在获取锁或反序列化上的时间,那么所有紧接着的消息就会被延迟, 延迟时间为 它们前面的那些消息 按顺序交付时 所消耗的时间总和。

然而,在大多数情况下,不同的web session的更新信息应该是完全不相关的,因此,应该被并发地递交出去。比如,对会话X的修改不应该对会话Y有任何影响。因此,对X, Y和Z的更新信息 都能够被 并发地递交出去。

这个问题的一个解决方案是带外(OOB)消息 (参见下一个段)。然而,OOB消息不能确保顺序,因此,更新信息X1-3 可能会被按照 X1, X3, X2 这个顺序递交出去。如果这不是我们想要的,而是想要 与某个特定web session会话的所有消息都被并发地递交出去,然后在这个特定session之内排序,那么我们可以求助于scoped messages。

Scoped messages只能用于常规消息(非OOB消息),在scopes之间进行并发递交,然后在给定的scope之内进行排序。比如,我们将上面的sessions用作(比如jsessionid)作为scopes,那么递交顺序可能如下:(-> 表示顺序的,|| 表示并行的):

X1 -> X2 -> X3 -> X4 || Y1 -> Y2 -> Y3 || Z1 -> Z2 -> Z3

这意味着,X的所有更新信息 与 Y的所有更新信息、Z的所有更新信息 被并行地递交出去。然而,在一个特定的SCOPE之内,更新信息 被 按照它们被更新的顺序 递交出去。因此,X1在X2之前被递交出去,X2在X3之前被递交出去,依次类推。

回到上面的例子,这次我们使用scoped messages,更新信息Y1 不必 等到更新信息X1-3递交完毕,可以立即处理它。

要想设置一条消息的scope,请使用Message.setScope(short)方法。

Scopes是在 叫做SCOPE的、单独的协议中实现的。该协议必须被放置在诸如UNICAST、NAKACK(或处理这个问题的SEQUENCER)之类的排序协议的上面某个地方。

scopes的唯一性
请注意,scopes 应当尽量是唯一的。和哈希算法比较起来,碰撞越少,并发性就越好。因此,比如,两个web sessions使用了相同的scope,那么这些sessions的更新信息 就应该被 按照它们被发送的顺序 递交出去,而不是并发地递交出去。尽管这么做,不会导致错误的行为,但这有悖于SCPPE的目的。
还要注意的是,如果多播和单播消息具有相同的SCOPE,那么这些消息会被按顺序递交出去。因此如果A多播发送了一些SCOPE是25的消息到集群中,而A同时单播了一些SCOPE也是25的消息到B,那么A的多播和单播消息 都将会被 按照发送给B的顺序 递交出去。再一次,这么做是正确的,但是由于多播消息和单播消息是不相关的,这么做可能会降低速度!

5.4.5. Out-of-band messages

OOB消息完全忽略协议栈可能带有的任何顺序约束。任何被标记为OOB的消息,都将会被OOB线程池处理。当我们不想要某条消息的处理 必须 等到所有来自相同发送者的其它消息被处理之后,才被处理,比如心跳这种情况:如果发送者P发送了5条消息,然后发送了对来自其它节点的某个心跳请求的一个响应,那么处理P的5条消息所消耗的时间可能比心跳超时时间长一些,因此,P就会被错误地作为嫌疑对象!然而,如果我们将心跳响应标记为OOB,那么心跳响应就会被OOB线程池处理,因此 它和在它前面发送的5条消息是并行的,因此不再会导致错误的嫌疑。【YBXIANG:这里的嫌疑指的是被列为已经崩溃的或正在离开集群的嫌疑对象】

两个单元测试 UNICAST_OOB_Test 和 NAKACK_OOB_Test 展示了OOB消息如何影响单播和多播消息的排序。

主页   顶部
联系我们 Email: [email protected], QQ: 935112067, QQ群: 310848283
沪ICP备13027300号

你可能感兴趣的:(分布式,开源,java)