20. 为NASA1火星探测机器人计划开发的高可靠性企业系统
你是否听说过这样的说法:美存在于他人眼中? 我所遇到的情况是,这个评价是否美的人正是NASA的火星机器人计划。它对软件系统的功能性,可靠性和健壮性要求非常严格。噢~软件必须按计划完成--如果延期了,火星可决不会等你。当NASA讨论如何赶上“发射窗口”时间的时候,它意味着只能成功,不能延期!(it means it in more ways than one!)
本章描述了协作信息入口(CIP)的设计和开发,它是在NASA开发的一个大型的企业信息系统,用来给火星探测计划的管理者,工程师和全球的科学家们使用。
火星人决不容忍丑陋的软件。对于CIP来说,美决不是那些你可以依靠和崇拜的漂亮算法或程序。而是蕴含在一个由一流大师们建造的复杂软件结构中。这些建造者们对相关内容了如指掌,知道在什么地“敲进钉子”。大型应用的美通常不同于小型程序。这是因为随着程序规模的增大,必然性和偶然性都大大增加--大型应用必须处理很多小型程序不关心的事情。在本文的后继部分,我们将首先了解一下CIP的基于Java,面向服务的总体架构,之后针对一个服务进行集中深入的介绍,展示并分析其中的一些代码片段,探讨一些关键部分是如何使系统符合功能性、可靠性和健壮性需求的。
阐述这种软件的美,老实说有点怪怪的。在面向服务的多层架构中,服务是通过服务器上的中间件层实现的。(我们开发了可以共享复用的中间件组件,这大大节省了开发的时间。)中间件将客户端应用程序和后台的数据源的耦合分解开;换言之,一个应用程序不需要了解它所需数据存储在哪里以及是如何保存的。客户端应用仅仅向中间件发送服务器请求并获取结果数据。如果所有的中间件服务都井井有条地工作,企业系统的最终用户根本感觉不到客户端程序在向远程服务发送请求。如果中间件操作自如,用户会感觉他们好像直接在访问自己计算机或者笔记本上的数据。因此越成功的中间件,就越感觉不到。优美的中间件应该根本就是“隐形”的!
20.1. 火星探测计划和写作信息入口
火星探测者(MER: Mars Exploration Rover)计划的主要目的就是探测火星地表上是否曾经存在过液态水。在2003年的六月和七月,NASA分别将两台机器人地质勘探员送往火星。2004年1月,在经过7个月的旅行之后,他们分别到达了火星的正反两面。
每个机器人都由太阳能驱动,可以在火星地面自主行走。它们都装有大量科学仪器,例如在机器手前端的分光计。机器手还带有一个电钻和一个精密图像传感器,它们可以用来探测岩石表面下的物质。机器人还装备了很多摄像头和天线,用以将拍摄到的图片传回地球(见图20-1)。NASA的无人探测计划集软硬件尖端技术于一身。机器人内置的软件即可以用来控制它自主行动,而且还能响应地球基地发来的指令。NASA的地面控制中心JPL实验室(Jet Propulsion Laboratory)坐落于加州的帕萨迪纳(Pasadena)。地面控制中心的软件系统使得火星探测计划的管理者,工程师和科学家们能够下载、分析从火星探测机器人传回的信息,互相协作,制定计划,并且发送新的行动指令给机器人。
在位于加州Mountain View的Ames研究中心,我们设计并开发了火星按测计划所使用的协作信息入口CIP系统。项目团队包括10名软件工程师和计算机科学家。另外还有9名包括项目经理和其他辅助人员负责诸如质保、集成、硬件配置、以及bug追踪等等工作。
图 20-1. 火星探测机器人 (感谢JPL实验室提供图面)
20.2. 任务需求
我们设计的CIP是为了满足火星探测计划的三个主要需求。通过实现这三个需求,CIP为火星探测计划的所有人员提供了关键的“situational awareness”:
时间管理
-
在任何复杂的项目中,保持每个人同步是成功的一个关键。火星探测计划对时间管理提出了特别的挑战。由于参与火星探测的人员分布于全世界各地,所以CIP根据时区不同显示各种时间。并且由于机器人分别在火星的相反两面着陆,所以还存在两个不同的火星时区。探测计划将以火星时间开始,也就是说所有排好的会议和活动(例如从火星下载数据)都必须使用火星正面时区的时间或者反面时区的时间,具体使用哪一个火星时区取决降落在火星相反两面的机器人。火星上的一天大约比地球上的一天长40分钟,所以相对于地球时间,参与探测计划的人员作息时间每天都会逐渐向后推迟。而他们的家人或者同事却仍然使用地球时间作息,因此CIP的时间管理功能变得特别重要。
人员管理
-
围绕每个机器人都有一支独立的团队,有些人员还可能在两个团队之间互相调动,所以持续管理记录每一个人是另外一个关键的功能。CIP管理着一个人员名单并且可以通过甘特图形式的进度表来显示某个时间谁在哪里进行着什么样的工作。
-
CIP也允许参与人员之间进行协作。他们可以发出广播消息,共享数据分析结果和图像,上载报告和相互批阅报告。
数据管理
-
从遥远的太空获取数据是NASA所有飞行器和宇航计划的核心内容,因此,在火星探测计划中,CIP也扮演着同样的主要角色。地球上的陆基阵列天线从火星探测机器人接收到数据和图像后,就把它们传送到JPL实验室进行分析并存储到专门的服务器上。当火星探测计划管理人员公布这些处理过的数据和图像后,CIP就会产生一组用于分类所有文件的元数据(metadata),例如从那个机器人上的仪器传来的数据,那个摄像头拍摄的图像,使用什么设置,什么配置,在哪里,什么时间拍摄到的等等。这样CIP的使用者就可以使用这些分类条件进行数据和图像的检索,并把他们感兴趣的数据通过互联网从火星探测服务器上下载到他们自己的笔记本电脑或者工作站上。
-
CIP还实现了数据安全。例如,根据用户的身份(并且她是否是美国公民),某些数据和图像可能禁止被访问。
20.3. 系统架构
对于一个企业系统来说,优美的代码会部分来自优美的结构——代码的组织方式。架构绝不仅仅是美学问题。在大型应用中,架构决定了软件组件如何相互作用,并且影响整体系统的稳定性。
我们使用了一个三层结构的面向服务体系(SOA service-oriented architecture)。我们坚持严格的工业标准和最优秀的实践原则,只要可能,我们就使用商业化的成熟软件(COTS: commercial off the shelf)。我们基本使用Java语言编程,并使用J2EE(Java 2 Enterprise Edition)标准(见图20-2)。
客户层主要由带有GUI界面的独立Java程序和一些Web应用组成,Java程序的GUI使用了Swing的组件。中间层是一个符合J2EE标准的程序程序服务器,它作为中间件提供所有的服务,用以响应客户程序发来的请求。我们使用企业JavaBean(EJB)来实现这些服务。数据层包含数据源和处理数据的工具程序。也是使用Java编写的,这些工具监控保存数据处理结果和图像的文件服务器。只要文件被火星探测计划的管理人员发布,工具程序就会为他们生成元数据(metadata)。
图 20-2. CIP面向服务体系的三层架构
使用基于J2EE的SOA架构,我们就可以在设计大型企业应用时使用良好定义的Java Bean。无状态的会话bean负责处理服务请求,而无需记住各个请求见的任何状态信息。另一方面,专门管理会话状态的bean负责维护客户端的状态信息,并且一般会管理从数据存储区读出和写入持久化信息。对于开发大型复杂的应用来说,在设计阶段拥有众多选择非常重要。
在中间件内,我们为每一个服务实现了一个无状态的会话bean作为服务提供者。它作为一个建立web服务的facade,客户应用程序使用它创建服务请求,并且获取响应。每个服务可以访问一个或者多个无状态的会话bean,它们作为业务对象提供所有必需的业务逻辑。这些业务逻辑必须维护服务请求之间的状态,例如在一个数据库响应到数据请求之间,业务逻辑接下来应该从哪里读取下一部分信息。有效的做法是,无状态的bean通常为有状态bean的进行服务分法,而真正的工作由有状态的bean来处理。
在这种类型的架构中,有大量优美的地方!它包含了一些关键的设计原则:
基于标准
-
在一个研究机构中(例如我们设计开发CIP系统的NASA的Ames研究中心),人们特别喜欢尝试发明新东西,尽管有时结果不过是重复发明了轮子。火星探测机器人计划非常紧急,根本没有时间和资源允许我们在CIP中搞各种发明和研究,我们必须集中精力,为火星探测计划开发出产品质量的代码。
-
在所有的大型应用中,决定成功的是集成而不是编码。所以在坚持工业标准的背后,优美的实现在于通过使用成熟的商业化软件来尽量减少编码,并且由于它们具有通用的接口,所以这些组件能够很好的彼此协作。这一点是的我们能够向火星参测计划的管理者们保证,我们能够准时地发布能够工作的可靠代码。
-
我们降低了客户应用和中间件服务之间的耦合。这样,一旦应用程序的开发者和服务的开发者达成了一致的接口,他们就可以同时独立进行开发。只要保证接口稳定,在任何一方的变化都不会影响到另一方。解除耦合是我们能够按时完成一个大型多层SOA应用的另一个关键因素。
语言无关
-
客户端应用和中间件服务使用web服务彼此进行通信。web服务使用的协议是语言无关的工业标准协议。大部分的CIP客户端应用使用Java编写,但是中间件也能够给用C++和C#编写的程序提供服务。当我们完成一个可以向Java客户端提供的服务后,使它能够同时服务于其他用任何语言写成的客户端也非常容易。这样在不花费额外时间和资源的情况下,CIP系统的功能和易用性都大大增强了。
模块化
-
随着应用大小的增加,模块化的重要性程指数上升。CIP系统中,每一个服务都是一个独立完整(self-contained)中间件组件,而不依赖于其他服务。如果一个服务需要和其他服务协作,它就如同一个客户应用一样,向另一个服务发送请求。这样我们就可以独立地开发所有服务,并且增加了我们同时并行开发不同服务的可能性。模块化的服务是非常优美的的设计,它经常使用在一些成功的大型SOA应用中。
在客户层,应用程序经常组合多种服务,它们或将多个服务的结果合并起来,或者将结果作为新的请求发送到下一个服务。
伸缩性(Scalability)
-
当火星探测计划控制中心发布新的数据结果或者图像文件后,CIP的使用就会出现一个突出的峰值,特别是当某个机器人获取到重大发现后,人们就会迫不及待地下载最新的文件来看。我们必须保证CIP中间件能够处理这样的使用压力。因为这时候一旦使用速度变慢(更糟糕的是系统崩溃)就会造成严重的影响。
J2EE架构的一个优美特典就是它如何处理伸缩性。应用服务程序维护一组bean的缓冲池(bean pool),根据需要,它可以自动创建更多的无状态会话bean服务提供对象。这是一个J2EE提供的“免费”特性,它非常受中间件服务开发人员欢迎。
可靠性
-
作为久经业界考验的标准,J2EE架构被证明极为稳定。我们不妨用数据来说话(We avoided pushing the envelope of what it was designed to do),经过2年的使用,CIP正常运行的记录达到了99%。
我们不是仅仅依靠J2EE来获得可靠性。你可以在后续的案例分析中了解到,我们在开发中付出了很多努力来提高整个系统的可靠性。
20.4. 案例分析: 流服务
前面我们讲述了CIP系统架构上的优美,下面我们将集中了解一个中间件服务--流服务--进行案例分析,分析它使用了怎样的办法来满足火星探测计划的严格要求,包括功能方面的,可靠性方面的,和健壮性方面的各种努力。你将会看到,我们使用的各种技术本身并不华丽,真正的优美在于它们恰到好处、各得其所。
20.4.1. 功能性
火星探测计划中,数据管理的一个重要需求就是要允许用户从JPL实验室中的文件服务器上下载数据和图像文件到个人工作站或者笔记本电脑上。如前文所述,CPI的数据层工具会创建院数据用于用户进行文件检索的条件。同时用户还需要把他们分析数据的报考上载的服务器上。
CIP的流服务执行具体的文件下载和上载。我们之所以这样命名该服务是因为它可以安全地在JPL实验室和用户本地计算机间进行数据的流传输。它使用web服务协议,所以客户应用程序可以用任何支持此协议的语言来开发,并且这些应用程序可以自由使用它们需要的GUI。
20.4.2. 服务的架构
流服务如同其他的中间件服务一样,使用web服务来接受客户端的请求并返回响应。所有的请求都先经由流服务提供者(service provider)进行过滤,该提供者是通过无状态会话bean来实现的。服务提供者会创建一个文件读取器,它是使用一个具有状态的会话bean实现了,真正为客户端进行下载的工作由这个bean来完成。同样,服务提供者还会创建一个文件写入器进行上载文件内容的工作,它也是由一个具有状态的会话bean来实现的(见图20-3)。
图 20-3. 流服务架构
在任何时刻,可以有任意多的用户下载或上载文件,同时任何一个用户也可以同时下载多个文件或上载多个文件。因此在中间件中,会有大量的文件读取器和文件写入器bean在活动。除非服务的负载太大,一般只有唯一的一个状态无关的流服务提供者bean负责所有的请求。当负载过重时,应用服务器会创建出更多的服务提供者bean。
为什么每个文件读取器和文件写入器都必须是具有状态的会话bean呢?因为除非是小文件,否则流服务会一次传送文件的一个块(block),以响应“读取数据块”或“写入数据块”的客户端请求。(中间件服务器配置可以改变下载块的大小。而上载块的大小由客户应用程序决定。)从一个请求到下一个请求,具有状态的bean会记录火星探测计划文件服务器上打开的目标文件或源文件的内部位置,以决定下一个数据块的读取或写入的位置。
这是一个非常简单的架构,但是它对于处理多用户并发下载多个文件的问题非常有效。图20-4显示了从火星探测计划文件服务器上下载一个文件到用户本地计算机上的事件顺序。
图20-4. 双层服务结构如何处理文件读取
注意其中的流服务提供者并不维护服务请求中的任何状态信息。它的功能很像一个快速的服务分发器,仅仅把真正的工作打包交给具有状态的文件读取器bean来处理。由于它不需要跟踪请求或者维护状态,所以它可以处理多个客户应用程序混合在一起的请求。每个文件读取bean负责为每个客户应用程序维护状态信息(从哪里开始读取下一个数据块),整体上的效果就是应用程序可以同时发送多个“读取数据块”的请求来下载一个完整的文件。这个架构使得流服务可以为多个客户并发下载多个文件,并且从整体上保证吞吐量满足要求。
从用户的本地计算机向火星探测计划的文件服务器上载文件的过程也比较直观,如图20-5。
图20-5. 双层服务如何处理文件写入
这些表中虽然没有显示,实际上,除了每个文件具有一个token之外,每个客户请求也都包含一个用户token。用户首先使用正确的用户名和密码成功向中间件用户管理服务登录以后,用户就获得了授权,于是客户程序就获取了用户tokan。一个用户token包含了标识一个特定用户会话的信息,其中包含用户的身份。这使得流服务可以验证一个请求是否是来自一个合法的用户。它检查用户的身份以确定用户是否有权下载某个特定的文件。例如,火星探测计划不允许国外用户(非美国用户)访问某些文件,CIP系统必须符合这样的安全限制。
20.5. 可靠性
可靠的代码能够持续运行而不发生问题。即使有问题,也很少崩溃。你可以想象,在火星探测机器人上运行的代码必须极为可靠,因为根本不可能去火星进行代码维护。可是,火星探测计划要求在地球上运行的控制软件也非常可靠。人们不希望由于软件问题影响正在进行的火星探测。
正如前面提到的,CIP项目使用了一些措施来保证系统的内在稳定性:
我们还进一步使用了服务日志和监控来增强可靠性。虽然这些特性也可以debug小程序,但是他们对于持续跟踪大型应用的运行时行为是必须的。
20.5.1. 日志
在开发中,我们使用开源的Apache Log4J Java包来记录发生在中间件服务中的所有内容。它在开发中进行debug非常有用。记录日志是的我们可以写出更加可靠的代码。任何时候如果存在bug,日志就可以告诉我们问题的背后发生了什么,所以我们就能更好的修复bug。
我们本来打算当CIP投入运行后,就不再记录除重要消息之外的日志内容。但是我们最终保留了绝大部分日志,因为记录日志的开销几乎不对系统的总体性能造成影响。并且我们发现,这些日志不仅给出了每个服务运行情况的有用信息,还记录了客户应用程序是如何使用服务的。通过分析日志(我们称之为“日志挖掘”),我们可以根据实验数据提高服务的性能(见本章后面的“动态重配置”部分)。
这里给出一些流服务提供者bean中的代码片段,描述了我们是如何记录文件下载的日志的。getDataFile()方法处理从客户应用程序发出的每一个“获取数据文件”请求(通过web服务)。该方法立即记录请求的内容(第15-17行),包括用户的ID和目标文件的filepath:
1 public class StreamerServiceBean implements SessionBeandoFileDownload( )方法创建一个新的文件token(第30行)和一个文件读取bean(第41行),接着调用读取bean的getDataFile( )方法(第42行)。cacheStats成员是用来处理运行时监控的,我们将在后继部分解释它:
2 {
3 static {
4 Globals.loadResources("Streamer");
5 };
6
7 private static Hashtable readerTable = new Hashtable( );
8 private static Hashtable writerTable = new Hashtable( );
9
10 private static BeanCacheStats cacheStats = Globals.queryStats;
11
12 public FileToken getDataFile(AccessToken accessToken, String filePath)
13 throws MiddlewareException
14 {
15 Globals.streamerLogger.info(accessToken.userId( ) +
16 ": Streamer.getDataFile("
17 + filePath + ")");
18 long startTime = System.currentTimeMillis( );
19
20 UserSessionObject.validateToken(accessToken);
21 FileToken fileToken = doFileDownload(accessToken, filePath);
22 cacheStats.incrementTotalServerResponseTime(startTime);
23 return fileToken;
24 }
25
26 private static FileToken doFileDownload(AccessToken accessToken,方法readDataBlock( )处理从客户应用程序发来的每个“读取数据块”请求。它先查找到正确的文件读取bean(第71行),然后调用文件读取bean的readDataBlock( )方法(第79行)。当读取到源文件结尾后,就删除掉文件读取bean(第91行):
27 String filePath)
28 throws MiddlewareException
29 {
30 FileToken fileToken = new FileToken(accessToken, filePath);
31 String key = fileToken.getKey( );
32
33 FileReaderLocal reader = null;
34 synchronized (readerTable) {
35 reader = (FileReaderLocal) readerTable.get(key);
36 }
37
38 // Create a file reader bean to start the download.
39 if (reader == null) {
40 try {
41 reader = registerNewReader(key);
42 reader.getDataFile(filePath);
43
44 return fileToken;
45 }
46 catch(Exception ex) {
47 Globals.streamerLogger.warn("Streamer.doFileDownload("
48 + filePath + "): " +
49 ex.getMessage( ));
50 cacheStats.incrementFileErrorCount( );
51 removeReader(key, reader);
52 throw new MiddlewareException(ex);
53 }
54 }
55 else {
56 throw new MiddlewareException("File already being downloaded: " +
57 filePath);
58 }
59 }
60
61 public DataBlock readDataBlock(AccessToken accessToken, FileToken fileToken)
62 throws MiddlewareException
63 {
64 long startTime = System.currentTimeMillis( );
65 UserSessionObject.validateToken(accessToken);
66
67 String key = fileToken.getKey( );
68
69 FileReaderLocal reader = null;
70 synchronized (readerTable) {
71 reader = (FileReaderLocal) readerTable.get(key);
72 }
73
74 // Use the reader bean to download the next data block.
75 if (reader != null) {
76 DataBlock block = null;
77
78 try {
79 block = reader.readDataBlock( );
80 }
81 catch(MiddlewareException ex) {
82 Globals.streamerLogger.error("Streamer.readDataBlock("
83 + key + ")", ex);
84 cacheStats.incrementFileErrorCount( );
85 removeReader(key, reader);
86 throw ex;
87 }
88
89 // End of file?
90 if (block == null) {
91 removeReader(key, reader);
92 }
93
94 cacheStats.incrementTotalServerResponseTime(startTime);
95 return block;
96 }
97 else {
98 throw new MiddlewareException(
99 "Download source file not opened: " +
100 fileToken.getFilePath( ));
101 }
102 }
103
registerNewReader( )方法和removeReader( )方法分别创建和销毁具有状态的文件读取bean:
104 private static FileReaderLocal registerNewReader(String key)
105 throws Exception
106 {
107 Context context = MiddlewareUtility.getInitialContext( );
108 Object queryRef = context.lookup("FileReaderLocal");
109
110 // Create the reader service bean and register it.
111 FileReaderLocalHome home = (FileReaderLocalHome)
112 PortableRemoteObject.narrow(queryRef, FileReaderLocalHome.class);
113 FileReaderLocal reader = home.create( );
114
115 synchronized (readerTable) {
116 readerTable.put(key, reader);
117 }
118
119 return reader;
120 }
121
122 private static void removeReader(String key, FileReaderLocal reader)
123 {
124 synchronized (readerTable) {
125 readerTable.remove(key);
126 }
127
128 if (reader != null) {
129 try {
130 reader.remove( );
131 }
132 catch(javax.ejb.NoSuchObjectLocalException ex) {
133 // ignore
134 }
135 catch(Exception ex) {
136 Globals.streamerLogger.error("Streamer.removeReader("
137 + key + ")", ex);
138 cacheStats.incrementFileErrorCount( );
139 }
140 }
141 }
142 }
下面是文件读取bean中的代码片段。cacheStats成员和fileStats成员是用来进行运行时监控的,我们稍后介绍它们。getDataFile( )方法记录了文件下载开始的日志(第160到161行):
143 public class FileReaderBean implements SessionBeanreadDataBlock( )方法从源文件读取每个数据块。当整个源文件都被读取完以后,它就记录下成功完成的日志(第191到193行):
144 {
145 private static final String FILE = "file";
146
147 private transient static BeanCacheStats cacheStats = Globals.queryStats;
148 private transient static FileStats fileStats = Globals.fileStats;
149
150 private transient int totalSize;
151 private transient String type;
152 private transient String name;
153 private transient FileInputStream fileInputStream;
154 private transient BufferedInputStream inputStream;
155 private transient boolean sawEnd;
156
157 public void getDataFile(String filePath)
158 throws MiddlewareException
159 {
160 Globals.streamerLogger.debug("Begin download of file '"
161 + filePath + "'");
162 this.type = FILE;
163 this.name = filePath;
164 this.sawEnd = false;
165
166 try {
167
168 // Create an input stream from the data file.
169 fileInputStream = new FileInputStream(new File(filePath));
170 inputStream = new BufferedInputStream(fileInputStream);
171
172 fileStats.startDownload(this, FILE, name);
173 }
174 catch(Exception ex) {
175 close( );
176 throw new MiddlewareException(ex);
177 }
178 }
179
180 public DataBlock readDataBlock( )下面是一些流服务的日志记录条目的例子:
181 throws MiddlewareException
182 {
183 byte buffer[] = new byte[Globals.streamerBlockSize];
184
185 try {
186 int size = inputStream.read(buffer);
187
188 if (size == -1) {
189 close( );
190
191 Globals.streamerLogger.debug("Completed download of " +
192 type + " '" + name + "': " +
193 totalSize + " bytes");
194
195 cacheStats.incrementFileDownloadedCount( );
196 cacheStats.incrementFileByteCount(totalSize);
197 fileStats.endDownload(this, totalSize);
198
199 sawEnd = true;
200 return null;
201 }
202 else {
203 DataBlock block = new DataBlock(size, buffer);
204 totalSize += size;
205 return block;
206 }
207 }
208 catch(Exception ex) {
209 close( );
210 throw new MiddlewareException(ex);
211 }
212 }
213 }
2004-12-21 19:17:43,320 INFO : jqpublic:
Streamer.getDataFile(/surface/tactical/sol/120/jpeg/1P138831013ETH2809P2845L2M1.JPG)
2004-12-21 19:17:43,324 DEBUG: Begin download of file '/surface/tactical/sol/120/
jpeg/1P138831013ETH2809P2845L2M1JPG'
2004-12-21 19:17:44,584 DEBUG: Completed download of file '/surface/tactical/sol/120/
jpeg/1P138831013ETH2809P2845L2M1.JPG': 1876 bytes
图20-6显示了我们通过挖掘日志数据,获得的一张有用的曲线。它显示了火星探测计划进行了数月之中,下载量(下载的文件数量和下载的字节数)的变化趋势。在短期内,一旦机器人获得了有趣的发现,曲线就会显示下载量出现一个峰值。
图20-6. 从CIP流服务日志“发掘”出的曲线
20.5.2. 监控
记录日志使得我们可以分析服务的性能,通过日志可以了解一段时间内哪些服务做了什么。日志条目对于指出问题在哪里发生帮助最大,与此不同的是,运行时的监控帮助我们了解当前的服务性能如何。它给了我们进行动态调整提高性能或阻止潜在问题的机会。正如前面讲到的,能够监控执行行为通常对于大型应用的成功很关键。
前面列出的代码包含了一些更新性能数据的语句,这些性能数据是通过成员变量cacheStats和fileStats引用全局的静态对象实现的。一个监控中间件服务针对服务请求测试性能数据。虽然我们并没有展示这些成员变量引用的全局对象是如何实现的,但是你应该能够猜到它们的大致内容。关键是收集这些有用的运行时性能数据并不复杂。
我们把CIP中间件的监控工具实现为一个客户端应用程序,它定时想中间件监控服务发送请求以获取当前的性能数据。图20-7是该工具的数据tab页的屏幕截图。它显示了各种运行时数据,包括已经下载和上载的文件数目和大小,以及发生了多少文件错误(例如客户应用程序给出了一个错误的文件名)。
图20-7. 中间件监控工具数据tab页的屏幕截图
其中,流服务提供者bean中的doFileDownload( )和readDataBlock( )方法都会更新文件错误数量全局变量的值(前面“日志”一节中例子代码的第50行和地84行)。getDataFile( )和readDataBlock( )方法则会增加全部服务响应时间全局变量的值(第22行和第94行)。如图20-7所示,中间件监控工具中的“全部服务响应(Total Server Response)”标签下面显示了平均响应时间值。
文件读取bean的getDataFile( )方法在每个文件开始下载的时候进行了记录(第172行)。readDataBlock( )方法增加了全部文件数量和大小的全局变量的值(第195行和第196行)并且记录了下载完毕的信息(第197行)。图20-8是监控工具文件tab页的屏幕截图,它显示换了当前和最近的文件下载与上载的情况。
图20-8. 中间件监控工具文件tab页的屏幕截图
20.6. 健壮性
变化是不可避免的,优美的代码即使在投入运行后仍然能够优雅地应对变化。我们使用了一些方法来保证CIP系统健壮性,使它能够处理运行参数的变化:
20.6.1. 动态重配置
绝大多数中间件服务都有某种关键的执行参数。例如,上面的流服务使用块来下载文件,所以块的大小就是重要的参数。我们并没有把块的大小硬编码到程序中,而是把这个参数放入一个配置文件中,每次服务启动时都会读取这一参数。这一过程发生在流服务提供者bean被加载的时候(见“日志”一节中示例代码的地3行到第5行)。
所有的服务都共用一个中间件的配置文件middleware.properties,它在服务启动是被加载,该文件中包含如下一行:
middleware.streamer.blocksize = 65536
文件读取bean的readDataBlock( )方法随或就可以引用这一参数值(第183行)。
每个中间件服务在启动时都会加载若干参数值。有经验的软件开发者的技能之一就是知道什么可以作为服务启动时需要加载的参数值。这在开发过程中非常有帮助;例如,我们可以在开发过程中,不断尝试各种不同的块大小,而不需要每次都重新编译流服务。
而且,可加载的参数对于将代码投入运行也极为关键。在大多数产品环境中,对正在运行的软件进行变更是非常困难和昂贵的。这一点对于火星探测计划来说尤为如此,为此火星探测计划专门成立了一个变更变化委员会,在项目进行过程中评审所有的代码变更。
无论大小软件,避免对参数值硬编码当然是金科玉律(a basic Programming 101 dictum)。但是对于大型应用,这一点特别重要,因为大型应用中会有大量的参数值分散在海量的代码之中。
20.6.2. 热交换(Hot swapping)
在我们使用的CIP中间件中,热交换是商业应用服务程序中的重要特性。使用热交换,就可以将新的中间件服务部署到CIP中,而无需停止原先正在运行的中间件。
我们使用热交换来强制服务加载变更后的参数,方法就是让一个服务在自己上面重新加载。当然,如果例如流服务这样含有状态会话bean(文件读取和写入bean)的服务会因此丢失全部的状态信息。所以我们只能等到某些“寂静”的时候才能对这样的服务进行热交换,这样在该服务没有正在被用户使用的情况下,进行加载操作就是安全的。对于流服务,我们可以使用中间件监控工具的文件tab页(见图20-8)来了解当前的使用情况是否允许热交换。
热交换对于大型企业应用的环境来说特别有意义,因为在更换某些部分的同时保持其他部分能够正常运行十分重要。而对于小型程序,你也许只需要重新运行程序来进行更新。
--------------------
1. 美国宇航局
2. 芒廷维尤:美国加利福尼亚州西部城市,位于圣何塞西北的旧金山湾畔。它是一个有研究设施的制造业中心。人口67,460