预计阅读本页时间:-
8.4.6 基于协作的中间件
分布式系统的最后一个范型是所谓基于协作的中间件(coordination-based middleware)。我们将从Linda系统开始,这是一个开启了该领域的学术性研究项目。然后考察主要由该项目所激发的两个商业案例:pubilsh/subscribe以及Jini。
1.Linda
Linda是一个由耶鲁大学的David Gelernter和他的学生Nick Carriero(Carriero与Gelernter,1986;Carriero与Gelernter,1985)研发的用于通信和同步的新系统。在Linda系统中,相互独立的进程之间通过一个抽象的元组空间(tuple space)进行通信。对整个系统而言,元组空间是全局性的,在任何机器上的进程都可以把元组插入或移出元组空间,而不用考虑它们是如何存放的以及存放在何处。对于用户而言,元组空间像一个巨大的全局共享存储器,如同我们前面已经看到的(见图8-21c)各种类似的形式。
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
一个元组类似于C语言或者Java中的结构。它包括一个或多个域,每个域是一个由基语言(base language)(通过在已有的语言,如C语言中添加一个库,可以实现Linda)所支持的某种类型的值。对于C-Linda,域的类型包括整数、长整数、浮点数以及诸如数组(包括字符串)和结构(但是不含有其他的元组)之类的组合类型。与对象不同,元组是纯粹的数据;它们没有任何相关联的方法。在图8-40中给出了三个元组的示例。

在元组上存在四种操作。第一种out,将一个元组放入元组空间中。例如
out("abc",2,5);
该操作将元组("abc",2,5)放入到元组空间中。out的域通常是常数、变量或者是表达式,例如
out("matrix-1",i,j,3.14);
输出一个带有四个域的元组,其中的第二个域和第三个域由变量i和j的当前值所决定。
通过使用in原语可以从元组空间中获取元组。该原语通过内容而不是名称或者地址寻找元组。in的域可以是表达式或者形式参数。例如,考虑
in("abc",2,?i);
这个操作在元组空间中“查询”包含字符串“abc”、整数2以及在第三个域中含有任意整数(假设i是整数)的元组。如果发现了,则将该元组从元组空间中移出,并且把第三个域的值赋予变量i。这种匹配和移出操作是原子性的,所以,如果两个进程同时执行in操作,只有其中一个会成功,除非存在两个或更多的匹配元组。在元组空间中甚至可以有同一个元组的多个副本存在。
in采用的匹配算法是很直接的。in原语的域,称为模板(template),(在概念上)它与元组空间中的每个元组的同一个域相比较,如果下面的三个条件都符合,那么产生出一个匹配:
1)模板和元组有相同数量的域。
2)对应域的类型一样。
3)模板中的每个常数或者变量均与该元组域相匹配。
形式参数,由问号标识后面跟随一个变量名或类型所给定,并不参与匹配(除了类型检查例外),尽管在成功匹配之后,那些含有一个变量名称的形式参数会被赋值。
如果没有匹配的元组存在,调用进程便被挂起,直到另一个进程插入了所需要的元组为止,此时该调用进程自动复活并获得新的元组。进程阻塞和自动解除阻塞意味着,如果一个进程与输出一个元组有关而另一个进程与输入一个元组有关,那么谁在先是无关紧要的。惟一的差别是,如果in在out之前被调用了,那么会有少许的延时存在,直到得到元组为止。
在某个进程需要一个不存在的元组时,阻塞该进程的方式可以有许多用途。例如,该方式可以用于信号量的实现。为了要建立信号量S或在信号量S上执行一个up操作,进程可以执行如下操作
out("semaphore S");
要执行一个down操作,可以进行
in("semaphore S");
在元组空间中("semaphore S")元组的数量决定了信号量S的状态。如果信号量不存在,任何要获得信号量的企图都会被阻塞,直到某些其他的进程提供一个为止。
除了out和in操作,Linda还提供了原语read,它和in是一样的,不过它不把元组移出元组空间。还有一个原语eval,它的作用是同时对元组的参数进行计算,计算后的元组会被放进元组空间中去。可以利用这个机制完成一个任意的运算。以上内容说明了怎样在Linda中创建并行的进程。
2.发布/订阅(Pubilsh/Subscribe)
由于受到Linda的启发,出现了基于协作的模型的一个例子,称作pubilsh/subscribe(Oki等人,1993)。它由大量通过广播网网络互联的进程组成。每个进程可以是一个信息生产者、信息消费者或两者都是。
当一个信息生产者有了一条新的信息(例如,一个新的股票价格)后,它就把该信息作为一个元组在网络上广播。这种行为称为发布(publishing)。在每个元组中有一个分层的主题行,其中有多个用圆点(英文句号)分隔的域。对特定信息感兴趣的进程可以订阅(subscribe)特定的专题,这包括在主题行中使用通配符。在同一台机器上,只要通知一个元组守护进程就可以完成订阅工作,该守护进程监测已出版的元组并查找所需要的专题。
发布/订阅的实现过程如图8-41所示。当一个进程需要发布一个元组时,它在本地局域网上广播。在每台机器上的元组守护进程则把所有的已广播的元组复制进入其RAM。然后检查主题行看看哪些进程对它感兴趣,并给每个感兴趣的进程发送一个该元组的副本。元组也可以在广域网上或Internet上进行广播,这种做法可以通过将每个局域网中的一台机器变作信息路由器,用来收集所有已发布的元组,然后转送到其他的局域网上再次广播的方法来实现。这种转送方法也可以进行得更为聪明,即只把元组转送给至少有一个需要该元组的订阅者的远程局域网。不过要做到这一点,需要使用信息路由器交换有关订阅者的信息。

这里可以实现各种语义,包括可靠发送以及保证发送,即使出现崩溃也没有关系。在后一种情形下,有必要存储原有的元组供以后需要时使用。一种存储的方法是将一个数据库系统和该系统挂钩,并让该数据库订阅所有的元组。这可以通过把数据库封装在一个适配器中实现,从而允许一个已有的数据库以发布/订阅模型工作。当元组们经过时,适配器就一一抓取它们并把它们放进数据库中。
发布/订阅模型完全把生产者和消费者分隔开来,如同在Linda中一样。但是,有的时候还是有必要知道,另外还有谁对某种信息感兴趣。这种信息可以用如下的方法来收集:发布一个元组,它只询问:“谁对信息x有兴趣?”。以元组形式的响应会是:“我对x有兴趣。”
3.Jini
50多年来,计算始终是以CPU为中心的,一台计算机就是一个独立的装置,包括一个CPU、一些基本存储器、并总是有诸如硬盘等这样一些大容量的存储器。Sun公司的Jini(基因拼写的变形)则是企图改变这种计算模型的一个尝试,这种模型可以描述为以网络为中心(Waldo,1999)。
在Jini世界中有大量自包含的Jini设备,其中的每一个设备都为其他的设备提供了一种或多种服务。可以把Jini设备插入到网络中,并且立即开始提供和使用服务,这并不需要复杂的安装过程。请注意,这些设备是被插入到网络中,而不是如同传统那样插入到计算机中。一个Jini设备可以是一台传统的计算机,但也可以是一台打印机、掌上电脑、蜂窝电话、电视机、立体音响或其他带有CPU、一些存储器以及一个(可能是无线)网络连接的设备。Jini系统是Jini设备的一个松散联邦,Jini设备可以依照自己的意愿进入和离开该联邦,不存在集权式的管理。
当一个Jini设备想加入Jini联邦时,它在本地局域网上广播一个包,或者在本地无线蜂窝网上询问是否存在查询服务(lookup service)。用于寻找查询服务的协议是发现协议(discovery protocol)以及若干Jini硬线协议中的某一个。(另一种寻找方法是,新的Jini设备可以等待直到有一个周期性的查询服务公告经过,但是我们不会在这里讨论这种机制)。
当查询服务看到有一个新的设备想注册时,它用一段可以用来完成注册的代码作为回答。由于Jini是纯的Java系统,被发送的代码是JVM(Java虚拟机语言)形式的,所有的Jini设备必定能运行它,通常是以解释方式运行。接着,新设备运行该代码,代码同查询服务联系并且在某个固定的时间段中进行注册。在该时间段失效之前,如果有意愿,该设备就可以注册。这一机制意味着,一个Jini设备可以通过关机的方式离开系统,有关该设备的曾经存在的状态很快就会被遗忘掉,不需要任何集中性的管理。注册一定的时间间隔的做法,称为取得一项租约(lease)。
请注意,由于用于注册设备的代码是通过下载进入设备的,因此注册用的代码会随着系统演化而被修改掉,不过系统的演进并不会影响设备的硬件和软件。事实上,设备甚至不用明白什么是注册协议。设备所需明白的只是整个注册过程中的一段,即注册的设备提供的一些属性和代理代码,这样其他设备稍后将会使用这些属性和代理代码,以便访问该设备。
寻找某个特定服务的设备和用户可以请求查询服务是否知道这样的一个特定服务存在。在该请求中可以包含设备在注册时使用的属性。如果请求成功,在该设备注册时所提供的代理就会被送回给请求者,并且加以运行以联络有关设备。这样,设备或用户就可以同其他的设备对话,而无须知道对方在哪里,甚至也无须知道对话所用的协议是何种协议。
Jini客户机和服务(硬件或软件设备)使用JavaSpace进行通信和同步,这方式实际是模仿Linda的元组空间,但存在一些重要的差别。每个JavaSpace由一些强类型的记录项组成。这些记录项与Linda的元组类似,不过它们是强类型的,而Linda的元组则是无类型的。在每个记录项中包含一些域,每个域中有一个基本Java类型。例如,一个雇员类型的记录项可以包括一个字符串(用于姓名)、一个整数(用于部门)、第二个整数(用于电话分机号)以及一个布尔值(用于全时工作)。
在JavaSpace中只定义了四个方法(尽管其中的两个方法还有一个变种):
1)Write:把一个记录项放入JavaSpace。
2)Read:将一个与模板匹配的记录项复制出JavaSpace。
3)Take:复制并移走一个与模板匹配的记录项。
4)Notify:当一个匹配的记录项写入时通知调用者。
write方法提供记录项并确定其租约时间,即何时应该丢弃该记录项。相反,Linda的元组则一直停留着直到被移出为止。在JavaSpace中可以保存有同一个记录项的多个副本,所以它不是一个数学意义上的集合(如同Linda那样)。
read和take方法为要寻找的记录项提供了一个模板。在该模板的每个域中有一个必须匹配的特定值,或者可以包含一个“不在乎”的通配符,该通配符可以匹配所有合适的类型的值。如果发现一个匹配,则返回该记录项,而在take的情形下,该记录项还被移出了JavaSpace空间。这些JavaSpace方法中的每一个都有两个变种,在没有匹配到记录项时,它们之间有所差别。其中一个变种即刻返回一个失败的标识。而另一个则一直等到时间段(作为一个参数给定)到期为止。
notify方法用一个特殊模板注册兴趣。如果以后进来了一个相匹配的记录项,就调用调用者的notify方法。
与Linda中的元组空间不同,JavaSpace支持原子事务处理。通过使用原子事务处理,可以把多个方法聚集在一起。它们要么全部都执行,要么全部都不执行。在该事务处理期间,在该事务处理之外对JavaSpace的修改是不可见的。只有在该事务处理结束之后,它们才对其他的调用者可见。
可以在通信进程之间的同步中运用JavaSpace。例如,在生产者-消费者的情形下,生产者在产品生产出来之后可以把产品放进JavaSpace中。消费者使用take取走这些产品,如果产品没有了就阻塞。JavaSpace保证每个方法的执行都是原子性的,所以不会出现当一个进程试图读出一个记录项时,该记录项仅仅完成了一半进入的危险。