2007年11月6日星期二

为NASA火星探测机器人计划开发的高可靠性企业系统 > 计划和协作信息入口

20. 为NASA1火星探测机器人计划开发的高可靠性企业系统

Ronald Mak


你是否听说过这样的说法:美存在于他人眼中? 我所遇到的情况是,这个评价是否美的人正是NASA的火星机器人计划。它对软件系统的功能性,可靠性和健壮性要求非常严格。噢~软件必须按计划完成--如果延期了,火星可决不会等你。当NASA讨论如何赶上“发射窗口”时间的时候,它意味着只能成功,不能延期!(it means it in more ways than one!)


本章描述了协作信息入口(CIP)的设计和开发,它是在NASA开发的一个大型的企业信息系统,用来给火星探测计划的管理者,工程师和全球的科学家们使用。


火星人决不容忍丑陋的软件。对于CIP来说,美决不是那些你可以依靠和崇拜的漂亮算法或程序。而是蕴含在一个由一流大师们建造的复杂软件结构中。这些建造者们对相关内容了如指掌,知道在什么地“敲进钉子”。大型应用的美通常不同于小型程序。这是因为随着程序规模的增大,必然性和偶然性都大大增加--大型应用必须处理很多小型程序不关心的事情。在本文的后继部分,我们将首先了解一下CIP的基于Java,面向服务的总体架构,之后针对一个服务进行集中深入的介绍,展示并分析其中的一些代码片段,探讨一些关键部分是如何使系统符合功能性、可靠性和健壮性需求的。


正如你所想象的,NASA太空计划所使用的软件都必须是高度可靠的。项目耗资巨大,通常需要数年的计划和成百上千万美元,这些决不能因为错误的程序而毁于一旦。软件工作中最困难的地方,当然就是调试和修补距离地球数百万公里意外航天器上的软件。但是即使是地面系统也必须可靠;人们绝不愿意由于一个软件bug中断项目或者损失宝贵的数据。

阐述这种软件的美,老实说有点怪怪的。在面向服务的多层架构中,服务是通过服务器上的中间件层实现的。(我们开发了可以共享复用的中间件组件,这大大节省了开发的时间。)中间件将客户端应用程序和后台的数据源的耦合分解开;换言之,一个应用程序不需要了解它所需数据存储在哪里以及是如何保存的。客户端应用仅仅向中间件发送服务器请求并获取结果数据。如果所有的中间件服务都井井有条地工作,企业系统的最终用户根本感觉不到客户端程序在向远程服务发送请求。如果中间件操作自如,用户会感觉他们好像直接在访问自己计算机或者笔记本上的数据。因此越成功的中间件,就越感觉不到。优美的中间件应该根本就是“隐形”的!

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实验室提供图面)


为NASA火星探测机器人计划开发的高可靠性企业系统 > 任务需求

20.2. 任务需求

我们设计的CIP是为了满足火星探测计划的三个主要需求。通过实现这三个需求,CIP为火星探测计划的所有人员提供了关键的“situational awareness”:


时间管理

在任何复杂的项目中,保持每个人同步是成功的一个关键。火星探测计划对时间管理提出了特别的挑战。由于参与火星探测的人员分布于全世界各地,所以CIP根据时区不同显示各种时间。并且由于机器人分别在火星的相反两面着陆,所以还存在两个不同的火星时区。探测计划将以火星时间开始,也就是说所有排好的会议和活动(例如从火星下载数据)都必须使用火星正面时区的时间或者反面时区的时间,具体使用哪一个火星时区取决降落在火星相反两面的机器人。火星上的一天大约比地球上的一天长40分钟,所以相对于地球时间,参与探测计划的人员作息时间每天都会逐渐向后推迟。而他们的家人或者同事却仍然使用地球时间作息,因此CIP的时间管理功能变得特别重要。


人员管理

围绕每个机器人都有一支独立的团队,有些人员还可能在两个团队之间互相调动,所以持续管理记录每一个人是另外一个关键的功能。CIP管理着一个人员名单并且可以通过甘特图形式的进度表来显示某个时间谁在哪里进行着什么样的工作。

CIP也允许参与人员之间进行协作。他们可以发出广播消息,共享数据分析结果和图像,上载报告和相互批阅报告。


数据管理

从遥远的太空获取数据是NASA所有飞行器和宇航计划的核心内容,因此,在火星探测计划中,CIP也扮演着同样的主要角色。地球上的陆基阵列天线从火星探测机器人接收到数据和图像后,就把它们传送到JPL实验室进行分析并存储到专门的服务器上。当火星探测计划管理人员公布这些处理过的数据和图像后,CIP就会产生一组用于分类所有文件的元数据(metadata),例如从那个机器人上的仪器传来的数据,那个摄像头拍摄的图像,使用什么设置,什么配置,在哪里,什么时间拍摄到的等等。这样CIP的使用者就可以使用这些分类条件进行数据和图像的检索,并把他们感兴趣的数据通过互联网从火星探测服务器上下载到他们自己的笔记本电脑或者工作站上。

CIP还实现了数据安全。例如,根据用户的身份(并且她是否是美国公民),某些数据和图像可能禁止被访问。


为NASA火星探测机器人计划开发的高可靠性企业系统 > 系统架构

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来获得可靠性。你可以在后续的案例分析中了解到,我们在开发中付出了很多努力来提高整个系统的可靠性。


为NASA火星探测机器人计划开发的高可靠性企业系统 > 案例分析:流服务

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系统必须符合这样的安全限制。

为NASA火星探测机器人计划开发的高可靠性企业系统 > 可靠性

20.5. 可靠性

可靠的代码能够持续运行而不发生问题。即使有问题,也很少崩溃。你可以想象,在火星探测机器人上运行的代码必须极为可靠,因为根本不可能去火星进行代码维护。可是,火星探测计划要求在地球上运行的控制软件也非常可靠。人们不希望由于软件问题影响正在进行的火星探测。


正如前面提到的,CIP项目使用了一些措施来保证系统的内在稳定性:

  • 坚持工业标准和最优秀的实践,包括J2EE

  • 只要可能,就使用经过实践检验的COTS软件,包括一个来自中间件提供商的商业化应用服务器

  • 使用一个面向服务的体系结构和模块化的服务

  • 实现简单、直观的中间件服务

我们还进一步使用了服务日志和监控来增强可靠性。虽然这些特性也可以debug小程序,但是他们对于持续跟踪大型应用的运行时行为是必须的。

20.5.1. 日志

在开发中,我们使用开源的Apache Log4J Java包来记录发生在中间件服务中的所有内容。它在开发中进行debug非常有用。记录日志是的我们可以写出更加可靠的代码。任何时候如果存在bug,日志就可以告诉我们问题的背后发生了什么,所以我们就能更好的修复bug。


我们本来打算当CIP投入运行后,就不再记录除重要消息之外的日志内容。但是我们最终保留了绝大部分日志,因为记录日志的开销几乎不对系统的总体性能造成影响。并且我们发现,这些日志不仅给出了每个服务运行情况的有用信息,还记录了客户应用程序是如何使用服务的。通过分析日志(我们称之为“日志挖掘”),我们可以根据实验数据提高服务的性能(见本章后面的“动态重配置”部分)。


这里给出一些流服务提供者bean中的代码片段,描述了我们是如何记录文件下载的日志的。getDataFile()方法处理从客户应用程序发出的每一个“获取数据文件”请求(通过web服务)。该方法立即记录请求的内容(第15-17行),包括用户的ID和目标文件的filepath:

1 public class StreamerServiceBean implements SessionBean
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


doFileDownload( )方法创建一个新的文件token(第30行)和一个文件读取bean(第41行),接着调用读取bean的getDataFile( )方法(第42行)。cacheStats成员是用来处理运行时监控的,我们将在后继部分解释它:
26     private static FileToken doFileDownload(AccessToken accessToken,
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


方法readDataBlock( )处理从客户应用程序发来的每个“读取数据块”请求。它先查找到正确的文件读取bean(第71行),然后调用文件读取bean的readDataBlock( )方法(第79行)。当读取到源文件结尾后,就删除掉文件读取bean(第91行):
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 SessionBean
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


readDataBlock( )方法从源文件读取每个数据块。当整个源文件都被读取完以后,它就记录下成功完成的日志(第191到193行):

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. 监控

记录日志使得我们可以分析服务的性能,通过日志可以了解一段时间内哪些服务做了什么。日志条目对于指出问题在哪里发生帮助最大,与此不同的是,运行时的监控帮助我们了解当前的服务性能如何。它给了我们进行动态调整提高性能或阻止潜在问题的机会。正如前面讲到的,能够监控执行行为通常对于大型应用的成功很关键。


前面列出的代码包含了一些更新性能数据的语句,这些性能数据是通过成员变量cacheStatsfileStats引用全局的静态对象实现的。一个监控中间件服务针对服务请求测试性能数据。虽然我们并没有展示这些成员变量引用的全局对象是如何实现的,但是你应该能够猜到它们的大致内容。关键是收集这些有用的运行时性能数据并不复杂。


我们把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页的屏幕截图


为NASA火星探测机器人计划开发的高可靠性企业系统 >健壮性

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)来了解当前的使用情况是否允许热交换。


热交换对于大型企业应用的环境来说特别有意义,因为在更换某些部分的同时保持其他部分能够正常运行十分重要。而对于小型程序,你也许只需要重新运行程序来进行更新。

为NASA火星探测机器人计划开发的高可靠性企业系统 > 结论

20.7. 结论

协作信息系统证明了——对,即使在如NASA这样的庞大的政府机构中——可以成功地按时完成大型复杂企业软件系统的开发,并符合严格的功能性、可靠性和健壮性需求。火星探测机器人的表现远远超出了最初的预期,见证了硬件和软件、火星和地球、完美的设计和构建,以及精湛的技巧和天才。


与小型程序不同,对大型应用来说,优美不见得是漂亮的算法。对于CIP,优美表现在它那面向服务体系结构的实现中,表现在那些无数简单但却精心选择的组件——他们是软件构建大师们在关键地方敲入的钉子。

--------------------

1. 美国宇航局

2. 芒廷维尤:美国加利福尼亚州西部城市,位于圣何塞西北的旧金山湾畔。它是一个有研究设施的制造业中心。人口67,460

29. 代码如文

松本行弘(Yukihiro Matsumoto 1

程序和散文有不少相似之处,读者在阅读文章的时候,最重要的一个问题就是:“它是关于什么的?”对于程序,最主要的问题是:“它是做什么的?”实际上,作者的目的应该足够清晰,而这两个问题也根本就不应成为问题。然而,不论是文章还是代码,了解它们是如何写的也非常重要。尽管有些情况下,作者想要表达的思想很好,但是如果他写出的东西读者看不懂,就成了“茶壶里煮饺子,有嘴道(倒)不出”,作者的思想也就无从传播给读者。因此,观点和目的固然重要,文风和代码风格也同样重要。文章和代码的最终目的都是写给他人去阅读和理解的。[*]

[*] 本章由Nevin Tompson从日文翻译为英文。

你也许会问:“计算机程序真的是写给人阅读的么?”通常的观点是,人们写程序告诉计算机如何做,然后计算机利用编译器或解释器编译并理解代码。最终,程序被翻译成只有CPU才理解的机器语言。这一观点无疑正确地描述了整个过程的工作方式,但是这一观点仅仅解释了计算机程序的一个方面。


绝大多数程序并非一次写成。它们在使用过程中被不断的重用和改进。错误必须被解决,需求的变更和功能的增加意味着程序本身也许会产生翻天覆地的变化。在这个过程中,人们必须能够读懂程序原来是到底是干什么的。从这个意义上说,写出人能够看懂的程序比写出计算机能够看懂的程序更重要。


虽然计算机能够毫无怨言地对付复杂的代码,人却无法做到这一点。难以阅读的代码将极大程度地降低人们的生产率。而容易理解的代码则会提高人们的生产率。并且我们会发现这些容易理解的代码的通常很优美。


“是什么使得计算机程序容易理解?”或者说:“什么是优美的代码?”虽然不同人对于什么是优美程序有不同的标准,但是判断计算机代码的质量却不是简单的审美问题,代码的质量直接影响到程序完成任务的好坏。也就是说,“优美的代码”并不是什么虚无飘渺的抽象的东西,而是和程序员的努力紧密联系在一起的。优美的代码的确能够使程序员感到舒服并且能够提高效率。这就是我用来衡量程序是否优美的标准。


简短是的代码优美的特性之一。Paul Graham2说:"简洁就是力量。"在编程领域,简短是一个褒义词。因为它明显会节省人们浏览代码的气力,完美的程序应该不包含任何冗余信息。


例如,为了简短,当没有必要声明类型,或者根本不需要使用类或主函数的情况下,就应该把它们全部去掉。例子29-1展示了这一原则,它们分别是Java和Ruby的Hello Word程序。

例 29-1. Java版"Hello World"和Ruby版的"Hello World"对比

Java Ruby
class Sample {}
public static void main(String[] argv) {
System.out.println("Hello World");
}

print "Hello Worldn"



两个程序都完成相同的任务——简单地显示出“Hello World”——但是Java和Ruby的方法却大相径庭。在Ruby的实现中,所有的一切仅仅是描述基本任务:打印"Hello Word"。没有声明,没有数据类型。在Java中,却不得不包含进来许多和我们最终目的无关的各种东西。当然Java这种将所有内容包含进来的方法也有它的好处。但是由于它无法省略任何这些东西,简短的特性就丧失了。(再深入一些,Ruby的Hello Word实际上是三语通用,在Perl与Ptython中也行得通。)


简短还意味着削除冗余。冗余是一种重复信息。当信息重复时,同时维护它们并保持一致的代价就非常高。因为大量的时间被投入到这种维护工作中,所以冗余会降低编程效率。


尽管有些不同的意见认为冗余可以帮助理解程序的意思,实际的情况却完全相反,因为冗余代码包含了太多过剩的信息。这样“肥胖”的代码造成了工具依赖症。虽然现在依赖IDE来编程日益流行,但是这些工具并不能帮助人们理解程序的意思。真正能够写出优雅代码的窍门是选择一门优雅的语言。Ruby以及其他类似的轻量级语言能够帮助做到这一点。为了消除冗余,我们可以遵守DRY原则:不要重复你自己(Don't Repeat Yourself)。如果同样的代码在许多地方出现,你所想要表达的东西就会变得晦涩难懂。


DRY原则的反面是“复制-粘贴”式编程,某些公司使用代码行数来衡量程序员的生产率,这实际上是暗中鼓励制造冗余。我甚至听到一种说法认为,有时尽量多地复制代码是一种美德。但这是不正确的。


我相信真正的美德存在于简洁。最近Ruby On Rails的流行就是因为它不懈地追求简洁和DRY原则。Ruby语言非常认真地贯彻“绝不重复同样的代码两次”和“简练地描述”原则。Rails从Ruby那里继承了这一哲学。


优美代码让人看起来感到熟悉,这一点可能争议颇多——人类的保守程度其实超过你的想像——大多数人很难接受新的概念或改变他们的想法。相反,很多人宁愿忍受也不愿去改变。如果没有好的理由,大多数人不愿意换掉他们习惯的工具或者学习一门新的语言。他们会不断比较需要学习的新事物和他们以前习以为常的老观念。并且通常不公正地针对新事物做出负面评价。


改变人们思维方式的代价之高远远超过了一般人的估计。为了从一种观念转换到另一种观念(例如,从过程式编程转换到逻辑编程或函数式编程3),他必须通晓大量的知识。人类的大脑对陡峭的学习曲线会感到痛苦。因此这会降低程序员的生产率。


从某种角度来说,由于支持“优美代码”的这一特性,Ruby是一门极为保守的编程语言。虽然Ruby是所谓的“纯面向对象”语言,但是它并不像Smalltalk那样使用基于对象消息传递的先进控制结构。相反,Ruby坚持使用程序员们熟悉的传统控制结构如,if, while等。甚至Ruby还从古老的Algo-系语言中借鉴来了end关键字。


与其他同时代的语言相比,Ruby有时看起来老气守旧。但是“不要太时髦”也是优雅代码的一个关键。


朴素是优美代码的另一特性。我们通常在简单的代码中看到美。如果一段程序难以理解,它就难以被认为优美。并且当代码晦涩难懂时,bug,错误,混乱就接踵而至了。


朴素是编程中被误解最多的概念之一。设计程序语言的人通常都希望保持语言的简洁。尽管这个出发点不错,但是这样往往会造成用那个语言写出的程序更为复杂。Mike Cowlishaw,在IBM设计了Rexx脚本语言,他曾经指出,因为语言的使用者远远多于语言的设计者,所以后者必须让位于前者:


一般情况下,只有极少数人会给一门语言编写解释器或编译器,但是却有无数人会去使用它或者依靠它来工作、生活。所以语言必须为大多数人优化,而不是为少数人改进。编译器的作者因此并不怎么欢迎我,因为Rexx是一门非常难以解释或编译的语言,但是我认为这个代价值得,因为它更适合普通人,尤其是程序员使用。 [dagger]

[dagger] Dr. Dobb's 期刊, 1996年3月号.

我由衷地赞同这一点。Ruby就是这一理想的化身,实际上Ruby语言本身一点也不简单,尽管它提供了简化编程的方法。由于Ruby的不简单,所以用它编写的程序就可以简单。这一点在其他轻量级语言中也是如此;从实现他们的难易程度看,他们根本不轻量,它们之所以被称为轻量级语言,是因为他们的目的就是为了减轻程序员的负担。


为了了解这一点的现实含义,让我们来看看Rake,一个类似Make,被Ruby程序员广泛使用的Build工具。Makefile使用专门的格式编写,而Rakefile却直接由Ruby编写,它是一种具备全面编程能力的DSL(Domain Specific Language,特殊用途语言)。例29-2,展示了使用Rakefile运行一组测试的例子。

Example 29-2. Rakefile例子

task :default => [:test] 
task :test do
ruby "test/unittest.rb"
end

Rakefile具有很多来自了Ruby语法的优点:

  • 方法参数的括号可以被省略。

  • 方法的后面可以放置无括号的哈希键/值对。

  • 代码块可以紧接在方法调用的后面。

你在使用Ruby编程时也可以不使用这些语法糖衣,所以理论上说,他们是冗余的。经常有一些批评指出这些特性造成语言本身更加复杂。但是,例29-3展示了如果不使用这些特性的话,例29-2的程序会变成什么样子。

例 29-3. 不使用简短语法的Rakefile例子

task({:default => [:test]})
task(:test, &lambda(){
ruby "test/unittest.rb"
})


正如你所看到的,如果从Ruby的语法中去掉这些冗余,Ruby语言本身虽然变得更简洁了,可是程序员却不得不做更多的事情,并且他们写出的程序变得难理解了。所以如果用更简单的工具去解决一个复杂问题,其结果是把复杂性转嫁到程序员头上,这真是本末倒置(put the cart before the horse)。


“优美代码”的另外一个重要的元素是灵活性。我这里把灵活性定义为可以摆脱工具的限制。如果程序员由于工具的限制,不得不做违反他们意图的事情,结果只能是紧张和压力。这些压力会对程序员产生不良影响。最终结果根本不会是快乐,并且根据我们对优美代码的定义,这样带来的也根本不会是优美。相对于所有的工具和语言,人才是最宝贵的。计算机应该服务于程序员,使他们开开心心、最大程度地发挥生产率,但是现实中,计算机却经常增加而不是减轻了人的负担。


关于优美代码的最后一点是“平衡”。我上面已经讲过了简短、传统、朴素和灵活。但是没有任何一点可以单独保证程序是优美的。只有当把它们平衡合理地综合到一起,并且从一开始就加以考虑,这些因素才会和谐相处,催生出优美的代码。而且如果你确实从阅读与编写代码中发现乐趣,你就会体会到作为一名程序员的快乐。


Happy Hacking!

- --------------

1. Ruby语言的发明者:http://en.wikipedia.org/wiki/Yukihiro_Matsumoto

2. 著名的Lisp程序员,风险投资家和评论家: http://en.wikipedia.org/wiki/Paul_Graham

3. 常见的C/C++/Java都是过程式编程所使用的程序设计语言,逻辑式编程的著名语言如Prolog,函数式编程的一些著名语言包括,Lisp及其方言Scheme、Haskell和ML等