程序之美系列

架构之美

Beautiful Architecture: Leading Thinkers Reveal the Hidden Beauty in Software Design

[美]斯宾耐立思（Spinellis,D.）

[美]郭西奥斯（Gousios,G.）　编著

王海鹏　蔡黄辉　徐锋　译

ISBN：978-7-111-28356-0

安全之美

Beautiful Security

[美]奥拉姆（Oram,A.）　著

徐波　沈晓斌　译

ISBN：978-7-111-33477-4

数据之美

Beautiful Data:The Stories Behind Elegant Data Solutions

[美]Toby Segaran

Jeff Hammerbacher　编

祝洪凯　李妹芳　段炼　译

ISBN：978-7-111-31512-4

本书纸版由机械工业出版社于2010年出版，电子版由华章分社（北京华章图文信息有限公司）全球范围内制作与发行。

版权所有，侵权必究

客服热线：+ 86-10-68995265

客服信箱：service@bbbvip.com

官方网址：www.hzmedia.com.cn

新浪微博 @研发书局

腾讯微博 @yanfabook

目录

架构之美

O'Reilly Media,Inc.介绍

推荐序一 如何看到一滴水的美丽

推荐序二 架构的架构

推荐序三 美丽架构的含义

推荐序四 美丽架构之道

译者序 架构与美

作译者简介

序 Stephen J.Mellor

前言

第一部分 论架构

第1章 架构概述John Klein David Weiss

1.1 简介

1.2 创建软件架构

1.3 架构结构

1.4 好的架构

1.5 美丽的架构

致谢

参考文献

第2章 两个系统的故事：现代软件神话

2.1 混乱大都市

2.2 设计之城

2.3 说明什么问题

2.4 轮到你了

参考文献

第二部分 企业级应用架构

第3章 伸缩性架构设计Jim Waldo

3.1 简介

3.2 背景

3.3 架构

3.4 关于架构的思考

第4章 记忆留存Michael Nygard

4.1 功能和约束

4.2 工作流

4.3 架构关注点

4.4 用户反应

4.5 结论

参考文献

第5章 面向资源的架构：在Web中Brian Sletten

5.1 简介

5.2 传统的Web服务

5.3 Web

5.4 面向资源的架构

5.5 数据驱动的应用

5.6 应用面向资源的架构

5.7 结论

第6章 数据增长：Facebook平台的架构

6.1 简介

6.2 创建一个社会关系Web服务

6.3 创建社会关系数据查询服务

6.4 创建一个社会关系Web门户：FBML

6.5 系统的支持功能

6.6 总结

第三部分 系统架构

第7章 Xen和虚拟化之美

7.1 简介

7.2 Xenoservers

7.3 虚拟化的挑战

7.4 半虚拟化

7.5 Xen的变换形式

7.6 改变的硬件，改变的Xen

7.7 经验教训

7.8 延伸阅读

第8章 Guardian：一个容错操作系统环境

8.1 Tandem/16，将来所有的计算机都会像这样构建

8.2 硬件

8.3 物理布局

8.4 处理器架构

8.5 处理器间总线

8.6 输入/输出

8.7 进程结构

8.8 消息系统

8.9 文件系统

8.10 轶闻趣事

8.11 弊端

8.12 后继者

8.13 延伸阅读

第9章 JPC：一个纯Java的x86 PC模拟程序

9.1 简介

9.2 概念验证

9.3 PC架构

9.4 Java性能技巧

9.5 把4GB放入4GB：这不起作用

9.6 保护模式的危险

9.7 从事一项毫无成功希望的斗争

9.8 劫持JVM

9.9 终极灵活性

9.10 终极安全性

9.11 第二次做会更好

第10章 元循环虚拟机的力量：Jikes RVM Ian Rogers Dave Grove

10.1 背景

10.2 与运行时环境相关的传言

10.3 Jikes RVM简史

10.4 一个自足执行的运行时自举

10.5 运行时组件

10.6 经验教训

参考文献

第四部分 最终用户应用架构

第11章 GNU Emacs：滋长的特性是其优势

11.1 使用中的Emacs

11.2 Emacs的架构

11.3 滋长的特性

11.4 另外两个架构

第12章 当集市开始构建教堂

12.1 简介

12.2 KDE项目的历史和组织结构

12.3 Akonadi

12.4 ThreadWeaver

第五部分 语言与架构

第13章 软件架构：面向对象与面向函数

13.1 概述

13.2 函数式示例

13.3 函数式解决方案的模块性评价

13.4 面向对象视图

13.5 面向对象模块性的评价和改进

13.6 代理：将操作封装到对象中

致谢

参考资料

第14章 重读经典Panagiotis Louridas

14.1 所有东西都是对象

14.2 类型是隐式定义的

14.3 问题

14.4 砖块和灰浆建筑架构

参考资料

跋 漂亮地构建William J.Mitchell

安全之美

O'Reilly Media,Inc.介绍

译者序

前言

第1章 心理上的安全陷阱

1.1 习得性无助和无从选择

1.1.1 实例：Microsoft是如何允许L0phtCrack的

1.1.2 密码和身份认证可以从一开始就做得更好

1.1.3 客户的习得性无助——无从选择

1.2 确认陷阱

1.2.1 概念简介

1.2.2 分析师确认陷阱

1.2.3 陈腐的威胁模型

1.2.4 正确理解功能

1.3 功能锁定

1.3.1 安全位置的潜在风险

1.3.2 降低成本与未来收益：ISP实例

1.3.3 降低成本与未来收益：能源实例

1.4 小结

第2章 无线网络：社会工程的沃土

2.1 轻松赚钱

2.1.1 设置攻击

2.1.2 隐私的聚宝盆

2.1.3 Web安全的基本缺陷：不要相信可信系统

2.1.4 建立无线信任

2.1.5 采用可靠的解决方案

2.2 无线也疯狂

2.2.1 无线侧信道

2.2.2 无线接入点自身如何

2.3 无线仍然是未来

第3章 美丽的安全度量指标

3.1 安全度量指标的类比：健康

3.1.1 不合理的期待

3.1.2 数据透明性

3.1.3 合理的度量指标

3.2 安全度量指标的实例

3.2.1 巴林银行：内部侵害

3.2.2 TJX：外部侵害

3.2.3 其他公共数据来源

3.3 小结

第4章 安全漏洞的地下经济

4.1 地下网络的组成和基础设施

4.1.1 地下通信基础设施

4.1.2 攻击基础设施

4.2 回报

4.2.1 数据交换

4.2.2 信息来源

4.2.3 攻击向量

4.2.4 洗钱游戏

4.3 如何对抗日益增长的地下网络经济

4.3.1 降低数据的价值

4.3.2 信息的权限分离

4.3.3 构建动力/回报结构

4.3.4 为数据责任建立评估和声誉体系

4.4 小结

第5章 美丽的交易：重新思考电子商务的安全

5.1 解构商业

5.2 微弱的改良尝试

5.2.1 3D安全

5.2.2 安全电子交易

5.2.3 单用途和多用途虚拟卡

5.2.4 破灭的动机

5.3 重塑电子商务：新的安全模型

5.3.1 需求1：消费者必须通过认证

5.3.2 需求2：商家必须通过认证

5.3.3 需求3：交易必须经过授权

5.3.4 需求4：认证数据不应被认证方和被认证方之外的其他各方所共享

5.3.5 需求5：过程不能完全依赖共享秘密

5.3.6 需求6：认证应该是可移植的（不受硬件或协议所限）

5.3.7 需求7：数据和交易的机密性和完整性必须得到维护

5.4 新模型

第6章 捍卫在线广告：新狂野西部的盗匪和警察

6.1 对用户的攻击

6.1.1 充满漏洞的横幅广告

6.1.2 恶意链接广告

6.1.3 欺骗式广告

6.2 广告客户也是受害者

6.2.1 虚假的印象

6.2.2 避开容易受骗的CPM广告

6.2.3 广告客户为何不奋起反击

6.2.4 其他采购环境的教训：在线采购的特殊挑战

6.3 创建在线广告的责任制

第7章 PGP信任网络的演变

7.1 PGP和OpenPGP

7.2 信任、验证和授权

7.2.1 直接信任

7.2.2 层次式信任

7.2.3 累积式信任

7.2.4 基本的PGP信任网络

7.2.5 最早的信任网络的毛边

7.3 PGP和加密的历史

7.3.1 早期的PGP

7.3.2 专利和输出问题

7.3.3 密码战争

7.3.4 从PGP 3到OpenPGP

7.4 对最初信任网络的改进

7.4.1 撤销

7.4.2 伸缩性问题

7.4.3 签名的膨胀和困扰

7.4.4 证书内偏好

7.4.5 PGP全球目录

7.4.6 可变信任评分

7.5 未来研究的有趣领域

7.5.1 超级合法

7.5.2 社交网络和流量分析

7.6 参考资料

第8章 开源Honeyclient：先发制人的客户端漏洞检测

8.1 进入Honeyclient

8.2 世界上第一个开源Honeyclient简介

8.3 第二代Honeyclient

8.4 Honeyclient的操作结果

8.4.1 Windows XP的透明活动

8.4.2 Honeyclient数据的存储和关联

8.5 漏洞攻击的分析

8.6 当前Honeyclient实现的限制

8.7 相关的工作

8.8 Honeyclient的未来

第9章 未来的安全齿轮和杠杆

9.1 云计算和Web服务：这里是单机

9.1.1 创建者和破坏者

9.1.2 云计算和Web服务是拯救方案

9.1.3 新曙光

9.2 结合人、流程和技术：业务流程管理的潜力

9.2.1 发散型世界的发散型安全

9.2.2 BPM作为多站点安全的指导方针

9.3 社交网络：当人们开始通信时，大变革发生了

9.3.1 社交网络的艺术状态和潜力

9.3.2 安全行业的社交网络

9.3.3 数字中的安全

9.4 信息安全经济：超级数据解析和网络新规则

9.5 长尾变型的平台：未来为什么会截然不同

9.5.1 生产工具的大众化

9.5.2 发行渠道的大众化

9.5.3 连接供应和需求

9.6 小结

9.7 致谢

第10章 安全设计

10.1 无意义的指标

10.2 市场还是质量

10.3 符合准则的系统开发周期的作用

10.4 结论：安全之美是系统之美的象征

第11章 促使公司思考：未来的软件安全吗

11.1 隐式的需求也可能非常强大

11.2 公司为什么需要安全的软件

11.2.1 如何制订安全计划

11.2.2 修正问题

11.2.3 把安全计划扩展到外包

11.3 对现有的软件进行安全化

11.4 分析：如何使世界上的软件更安全

11.4.1 最好的软件开发人员创建了具有漏洞的代码

11.4.2 Microsoft领先一步

11.4.3 软件开发商给了我们想要的，却不是我们需要的

第12章 信息安全律师来了

12.1 文化

12.2 平衡

12.2.1 数字签名指南

12.2.2 加利福尼亚数据隐私法

12.2.3 安全的投资回报率

12.3 通信

12.3.1 技术狂为何需要律师

12.3.2 来自顶层的推动力，通过合作实现

12.3.3 数据泄露小虎队

12.4 正确做事

第13章 美丽的日志处理

13.1 安全法律和标准中的日志

13.2 聚焦日志

13.3 什么时候日志是极为珍贵的

13.4 日志所面临的困难

13.5 案例研究：瘫痪服务器的背后

13.5.1 事故的架构和环境

13.5.2 被观察的事件

13.5.3 调查开始

13.5.4 使数据起死回生

13.5.5 小结

13.6 未来的日志

13.6.1 来源的扩大化

13.6.2 未来的日志分析和管理工具

13.7 小结

第14章 事件检测：寻找剩余的68%

14.1 一个常见起点

14.2 改进与上下文相关的检测

14.2.1 用流量分析提高覆盖率

14.2.2 对监测列表进行综合分析

14.3 使用主机日志增强洞察力

14.4 小结

第15章 无需真实数据就能出色完成工作

15.1 数据半透明化的工作原理

15.2 一个现实的例子

15.3 为便利而存储的个人数据

15.4 如何权衡

15.5 进一步深入

15.6 参考资料

第16章 铸造新词：PC安全剧场

16.1 攻击不断增加，防御不断倒退

16.1.1 在Internet的传送带上

16.1.2 不正当行为的回报

16.1.3 暴徒的响应

16.2 揭穿假象

16.2.1 严格审查：传统的和更新的反病毒扫描

16.2.2 沙盒和虚拟化：新的银弹

16.3 桌面安全的更佳实践

16.4 小结

附录 作者简介

数据之美

O'Reilly Media,Inc.介绍

译者序

前言

第1章 在数据中观察生活

个人环境影响报告(PIR)

your.flowingdata(YFD)

个人数据收集

数据存储

数据处理

数据可视化

要点

如何参与

第2章 美丽的人们：设计数据收集方法时牢记用户

简介：用户共鸣正当其时

项目：关于一个新奢侈品的用户调查

数据收集面临的特殊挑战

设计解决方案

结论和反思

第3章 火星上的嵌入式图像数据处理

摘要

简介

一些背景

数据是否打包

三个任务

对图像切槽

传递图像：三个任务间的通信

获取图片：图像下载和处理

图像压缩

“下行”或一切都从这里向下传输

结束语

第4章 PNUTShell中的云存储设计

简介

更新数据

复杂查询

和其他系统的比较

结论

致谢

参考文献

第5章 信息平台和数据科学家的兴起

图书馆和大脑

Facebook具有了“自知之明”

商业智能系统

数据仓库的消亡和重起

超越数据仓库

“猎豹”和“大象”[1]

不合理的数据有效性

新工具和应用研究

MAD技术和Cosmos

作为数据空间的信息平台

数据科学家

结论

第6章 照片档案的地理之美

数据之美：Geograph项目

可视化、美丽和树形图

Geograph在使用条款上的观点

发现之美

反思和结论

致谢

参考文献

第7章 数据发现数据

简介

实时发现的好处

赌桌上的舞弊

企业的可发现性

目录：无价之宝

相关性：什么是重要的以及对谁重要

各个组件及特殊考虑

隐私考虑

结束语

第8章 实时的可移动数据

简介

前沿技术

社交数据规范化

结束语：通过Gnip思考

第9章 探寻Deep Web

什么是Deep Web

提供Deep Web访问的其他可选方案

结论

参考文献

第10章 构建Radiohead的“House of Cards”

这一切是如何开始的

数据捕捉设备

两种数据捕捉系统的优点

数据

捕捉数据，即“拍摄”

处理数据

后期数据处理

发布视频

结束语

第11章 都市数据可视化

引言

背景

解决棘手问题

公开数据

重新回顾

结束语

第12章 Sense.us的设计

可视化和社会数据分析

数据

可视化

协作

“向导”和“偷窥”

结论

参考文献

第13章 数据所做不到的

何时数据无法驱动

结束语

参考文献

第14章 自然语言语料库数据

分词

密码

拼写纠正

其他任务

讨论和结论

致谢

第15章 数据中的生命：DNA漫谈

用DNA存储数据

DNA作为数据源

搏击数据洪流

DNA的未来

致谢

第16章 美化真实世界中的数据

关于真实数据的问题

提供可以追溯到记录本的原始数据

验证开放来源数据

在线发布数据

结束循环：采用可视化技术启发新实验

在开放数据和免费服务下建立数据网络

致谢

参考文献

第17章 数据浅析：探索形形色色的社会定型

引言

预处理数据

探索数据

年龄、魅力和性别

观察标签

哪些单词具有性别化

聚类

结论

致谢

参考文献

第18章 旧金山海湾区之殇：次贷危机的影响

引言

我们是如何获取数据的

地理编码

数据检查

分析

通货膨胀的影响

富者更富，穷者更穷

地理区别

人口普查信息

探索旧金山

结论

参考文献

第19章 美丽的政治数据

实例1：重新划分选区和党派偏好

实例2：估计的时间序列

实例3：年龄和选举

实例4：关于最高法院被提名人的公众舆论和参议院选票

实例5：宾夕法尼亚州的本地党派

结论

参考文献

第20章 连接数据

实际上到底存在哪些公共数据

连接数据的可能性

企业内部

连接数据的障碍

可能的解决方案

集体调解

结论

附录 作者简介

O'Reilly Media,Inc.介绍

为了满足读者对网络和软件技术知识的迫切需求，世界著名计算机图书出版机构O'Reilly Media,Inc.授权机械工业出版社，翻译出版一批该公司久负盛名的英文经典技术专著。

O'Reilly Media,Inc.是世界上在UNIX、X、Internet和其他开放系统图书领域具有领导地位的出版公司，同时也是联机出版的先锋。

从最畅销的《The Whole Internet User's Guide＆Catalog》（被纽约公共图书馆评为20世纪最重要的50本书之一）到GNN（最早的Internet门户和商业网站），再到WebSite（第一个桌面PC的Web服务器软件），O'Reilly Media,Inc.一直处于Internet发展的最前沿。

许多书店的反馈表明，O'Reilly Media,Inc.是最稳定的计算机图书出版商—每一本书都一版再版。与大多数计算机图书出版商相比，O'Reilly Media,Inc.具有深厚的计算机专业背景，这使得O'Reilly Media,Inc.形成了一个非常不同于其他出版商的出版方针。O'Reilly Media,Inc.所有的编辑人员以前都是程序员，或者是顶尖级的技术专家。O'Reilly Media,Inc.还有许多固定的作者群体—他们本身是相关领域的技术专家、咨询专家，而现在编写著作，O'Reilly Media,Inc.依靠他们及时地推出图书。因为O'Reilly Media,Inc.紧密地与计算机业界联系着，所以O'Reilly Media,Inc.知道市场上真正需要什么图书。

推荐序一　如何看到一滴水的美丽

支付宝（中国）公司业务架构师

《大道至简》作者

周爱民（aimingoo）

【一】

架构是一个过程，而非一个结果。

【二】

在大多数人的谈论中，架构是一个目标产物，而作为架构师的责任就是去生产它。所以无论如何，架构是可以“做”出来的，而且也应该有一些“做”的方法、技术、技巧。有人问过我：架构的最主要产出是什么？我的答案是：图。这里面有两层含义：一层含义是如同建筑师描绘的蓝图一样，用于引导实施者；另一层含义是架构师头脑中清晰的目标系统。如果架构师头脑中没有系统清晰的图像，他是没有办法把它画出来的。

【三】

画家画的无非是物我。画物的画家，最终画的还是我见。所以，画家的笔最终描绘的是他自己心里的映像。

【四】

艺术是不可能被“生产”出来的，生产出来的，叫“艺术品”。

【五】

架构这个过程，是架构师洞见系统内在结构、规律、原则和逻辑的过程。真正的架构师是可以将自己放在系统中去的（例如作为系统中的任何一个角色），只有清晰地理解系统，才能简洁地描述它。而当架构师拿出了他所描述的“作品”的时候，架构这一过程就已经结束了。

【六】

一滴水滴落的过程中，有多少个形态的变化？

推荐序二　架构的架构

北京无限讯奇信息技术有限公司产品技术高级总监

黄冬

从编辑手里拿到厚厚的《架构之美》译稿时，恰巧是我刚刚讲完一场消息系统架构的讲座之后。而正是在昨天，一位想要创业的朋友跟我说要寻找一位懂得“架构”的高人与他一起创业。要知道与代码不同的是，“虚幻”的架构常常让人认为其有很多玄妙之处，只因它大多难以落在纸上。特别是与很多大师谈及架构时，经常落入他们的一些“陷阱”，并往往为自己达不到大师的精明与技巧而叹息。殊不知，被我们所津津乐道的这些架构，是他们在日常工作里经历了大量的错误、重复的尝试、无数的代码、长久的考验所积淀下来的只言片语。

本书将数十人的经历与只言片语，经过深思熟虑后抽象出规律，使之可以不断复用。而另一方面，又将架构的过程娓娓道来，尝试让读者思考架构的过程与思路。在这里，更多的过程与思考被展现出来，更多的原因与为什么让我们了解。

这本书里展现了很多绚丽的故事，犹如士兵阅读将军的传记一样，阅读本书将会让你更鼓起勇气追寻大师们的脚步，但永远要记住，每一滴汗水才真正是你成长路上的每一个记号，要在自己的工作里更深地去理解每一处不同，架构出属于自己的系统。

感谢译者和出版者为我们带来这样一本传奇的架构故事书。

推荐序三　美丽架构的含义

腾讯R＆D研发总监（Tencent Director Of R＆D Development）

资深技术专家（Senior Technology Expert）

王速瑜

古人形容美女之美：“……增之一分则太长，减之一分则太短……”，深刻地揭示了“恰到好处”的美丽含义。当我拿到《架构之美》书稿时，我发现美丽的含义如此相似。

美丽至简。美丽的架构应尽可能简单，但不要过于简单。书中通过多种例子表达了这个最基本的道理。我见过很多大型的软件架构，从大型的电信网络管理系统，到大规模应用的互联网架构，以及企业级的ERP软件，系统总是遵循从无到有，从简单到复杂，再到简单这样的过程。最终，支撑这些大型系统稳定可靠运行的就是这个最基本的道理。

美丽的架构应尽可能精益，并且是演进式发展的。当你架构一个亿万人同时在线的大规模网站系统的时候，你无法从一开始就提供最完善的解决方案，它应该是随着用户的增长而可扩展的。精益的思想让你避免了过度设计，也使架构不断演进，趋于完美。书中从企业级应用架构、用户级应用架构等多个角度提供了相应的解决方案，对于架构师无不是一顿美味的大餐。

深夜看完这本书稿后，我发现，架构之美并不简单，它没有定法。但是，它将为架构师们提供一把进入“美丽架构艺术馆”大门的钥匙。拿起它，您将会开启这扇大门！

推荐序四　美丽架构之道

《构建高性能Web站点》作者

Web架构实践者

郭欣

我无法给架构下一个简单的定义，因为任何定义都会束缚你对架构的无限想象。不可否认，架构师早已出现在人类几千年前的各项生产活动中，比如建筑、音乐。而在计算机软件及Web领域，架构的设计直接影响着系统的生产，比如开发过程和效率、代码和组件复用性等，同时也影响着系统的可用性、可伸缩性、性能、容量可预测性等。

本书更加关注架构之美。美丽的架构同样无法定义，可它却一定是自然的、简单的、可复用的、人文的，甚至是外行人也可以细细品味其思想的。当我看到超市的多个收银台排满长队时，便想到服务器并发处理性能和容量；当我看到十字路口的车辆等待转弯时，便想到它通过缓存思想来提高交通吞吐率。

那么如何设计出美丽的架构呢？从代码逻辑到物理网络，从单机到分布式，无数的技术可供架构师选择，如分层、组件化、服务化、标准化、缓存、分离、队列、复制、冗余、代理等，不过它们仍然只是“术”的范畴，而何时何处如何恰到好处地使用它们才是“道”的范畴，比如顿悟变化的道理，在博弈中寻找平衡，以系统化的角度来分析问题，寻找相对与绝对的奥秘、开放的心态……

然而，这个领域实在是太年轻了，我们需要更多的例子和经验，本书将让你大开眼界！

译者序　架构与美

王海鹏

人们在生活和工作中发现美并创造美，软件开发和架构设计也不例外。

架构之美体现了关注点的分离与结合。在软件设计中，设计师需要考虑多方面的关注点。漂亮的架构设计让这些关注点尽可能分离，然后以最简单的机制结合在一起，从而得到高内聚、低耦合的系统。例如在Darkstar项目中，架构师们考虑的重点就是如何将多人在线游戏的游戏逻辑与系统的可伸缩性分离开来，让游戏的开发者只要遵守少量的规则，就能够像编写单机游戏一样编写大规模多人在线游戏。又如REST架构风格，体现了对资源命名、请求处理和资源物理表现形式的关注点分离。资源的名称与请求资源时服务器的处理方式无关，请求者无需知道服务器端采取的技术，并且请求者本来就不关心服务器端的处理技术。资源的物理表示形式可以通过内容协商来决定，使系统可以支持多种物理表示形式，并可以方便地扩展。

架构之美注重表达的简洁性。“Don't Repeat Yourself”，好的架构致力于消除各种类型的信息重复。从结构化程序设计中的子程序和函数，到面向对象程序设计中的继承，无不体现了对表达简洁性的特殊偏爱。在敏捷方法学中，消除重复则是重构的主要目的之一。爱因斯坦说：“让它尽可能简单，但不要过于简单。”我们需要考虑所有必须考虑的关注点，然后用简洁漂亮的架构体现我们的关注。同时，简洁的架构之美也降低了软件的总体成本，从这个意义上说，“简洁性”又可以称为“经济性”。

架构之美需要解决实际问题，它既是艺术，也是生活。软件像建筑一样，它的美不能脱离它的实用价值。Bjarne Stroustrup说，人类文明运行于软件之上。每一个软件都有自己的架构，这些架构有的很美，有的不太美。从艺术的角度来说，美是创造矛盾并解决矛盾。架构的多关注点和表达简洁性就是一种矛盾，美丽的架构提供了这一矛盾的解决方法，让我们的内心产生一种愉快的感觉。

架构之美需要经过专业的学习才能更好地欣赏和创造。和所有的艺术之美一样，不是说不经过专业学习就不能欣赏，但是经过了专业的学习，就能更好地欣赏这种美的种种精妙之处。如果想要创造出这种美，那就必然要经过长期的专业学习。

架构之美经过时间打磨。像Facebook面向数据的Web服务、FQL和FML架构，是在对应不同实际需求的过程中逐渐发展起来。在应用程序架构形成的过程中，设计者不断面对新的关注点需求，不断对已有的架构进行修改，并发展出新的架构组件。这就是所谓的“演进式架构”。只有变化是永恒不变的。在架构设计初期，设计者会将一些关注点有意推迟到将来考虑，例如持久和并发。对于这些暂不考虑的关注点，设计者对它们的实现方式尽可能不做任何假定，从而保留更多的可能性，让不同关注点之间的耦合尽可能小。

架构之美没有定法。虽然有一些法则可供我们参考，却没有非如此不可的。《金刚经》云：“一切贤圣，皆以无为法而有差别。”

参加本书翻译工作的人员还有蔡黄辉、徐锋、王海燕、李国安、周建鸣、范俊、张海洲、谢伟奇、林冀、钱立强、甘莉萍。

在这本书的翻译过程中，我受益良多，因此郑重地向大家推荐它。

作译者简介

作者简介

Till Adam在年轻时学习了哲学、比较文学、美国研究和音乐学，职业是音乐人。由于没有发财和出名，他转而攻读科学硕士，学习了数学、计算机科学和商业。多年从事自由软件的经历（特别是对KDE的贡献）教会了他编程，也为他带来了在Klarälvdalens Datakonsult AB工作的机会，目前他在该公司负责协调KDE的开发和其他与自由软件相关的活动。他和他的妻子、女儿住在德国柏林。

Jim Blandy在1990年至1993年间为自由软件基金会维护GNU Emacs，和Richard Stallman一起发布了Emacs的第19个版本。他是Subversion版本控制系统的最初设计者之一。他也是CVS版本控制系统、GNU调试器（GDB）、Guile扩展语言库和一个编辑基因序列的Emacs程序的贡献者。他现在为Mozilla公司工作，工作内容是SpiderMonkey，即Mozilla的Javascript编程语言的实现。Jim和他的妻子、两个女儿住在俄勒冈州的波特兰。

Mirko Boehm从1997开始就是KDE的开发者，在1996年至2006年间是KDE e.V.委员会的成员。他毕业于德国汉堡Helmut Schmidt大学的商业专业。在他的闲暇时间里，他阅读纸版书籍、与家人在一起，试图远离计算机。他目前在德国柏林为Klarälvdalens Datakonsult AB工作，负责跨平台软件和嵌入式软件开发。

Christopher Dennis自2005年JCP项目开始时就是项目的主要开发者。Chris在牛津大学读博士时开始使用Java。此前，他使用过各种编程语言，从十六进制小键盘上编写的Z80机器码到PHP和JavaScript。他对特殊情况、编码技巧和偶尔有点丑陋的临时编码很有兴趣，喜欢用各种语言编写紧凑的、优雅的代码。

Dave Fetterman是Facebook的工程经理，他在那里创建了Facebook平台项目。在2006年加入Facebook之前，他是一名软件工程师，参加Microsoft开发者部门的项目，包括.NET的通用语言运行环境（CLR）。他喜欢为其他开发者创建软件，也喜欢对愿意听的人发表长篇大论。他拥有应用数学的学士学位，并在2003年获得了哈佛大学的计算机科学硕士学位。

Keir Fraser是XenSource的创始人之一，XenSource现在是Citrix Systems公司的一部分。他也是Xen系统管理程序的首席架构师。Keir在2002实现了Xen的第一个版本，作为他在剑桥计算机实验室攻读博士学位时的一项娱乐。在该项目成为大规模的社区合作的过程中，他继续作为主要的开发者。他因在无锁并发控制方面的工作于2004年获得了博士学位，并在同年成为一名教师。

Pete Goodliffe是一名程序员、专栏作家、演说家和作家，从来不在同一软件领域做过多的停留。Pete的热门书籍《Code Craft》（No Starch Press）是对整个编程追求的实际而有趣的调查—大约600页，真是了不起！他对制革很有热情，而且不穿鞋。

Georgios Gousios是一名职业研究者，接受的教育和软件工程有关，热衷于软件开发。目前，他正在希腊的雅典经济与商业大学完成他的博士论文。他的研究兴趣包括软件工程、软件质量、虚拟机和操作系统，他拥有英国曼彻斯特大学的科学硕士学位。Gousios为多个开源软件项目贡献过代码，并参与了各种学术项目和商业项目的研究与开发。他是SQO-OSS项目的项目经理、设计权威和主要开发成员，为评估软件质量探索一些创新的方法。在他的学术生涯中，Gousios在会议和杂志上发表了10篇技术论文。Gousios是ACM、IEEE、Usenix Association和Technical Chamber of Greece的成员。

Dave Grove是IBM的T.J.Watson研究中心动态优化组的一名研究员。他的主要研究兴趣包括分析和优化面向对象语言、虚拟机设计和实现、JIT编译、在线反馈导向的优化和垃圾收集。他在1998年参加了Jalapeño项目，是这个优化编译器和适应式优化系统首个实现的主要贡献者。自Jalapeño在2001年作为Jikes RVM开放源码以来，他一直是Jikes RVM核心团队和指导委员会的活跃成员。

John Klein是软件工程研究所（SEI）的高级技术人员，他的研究方向是“众系统之系统”的架构方法，并帮助个人、团队和组织机构改进他们的软件架构能力。在加入SEI之前，John是Avaya公司的首席架构师。在Avaya，他负责开发多模式的代理、通信分析的架构，以及为各种客户交互产品创建并改进架构。在此之前，John是Quintus的一名软件架构师，在那里他设计了第一款获得商业成功的多渠道集成联系中心产品，并导致了Quintus兼并了另外两家公司，实现了产品组合的技术集成。在加入Quintus之前，John曾为多家视频会议和视频网络业的公司服务。他的职业生涯开始于Raytheon，在那里他为雷达信号处理、多光谱图像处理、并行处理架构和算法提供硬件和软件解决方案。John拥有Stevens技术学院的学士学位和Northeastern大学的硕士学位。他是ACM和IEEE计算机学会的成员。

Greg Lehey的漫长职业生涯在德国和澳大利业度过，他曾为德国空间研究所工作，也曾为Univac、Tandem、Siemens-Nixdorf和IBM等计算机制造商工作，也曾作为一些没名气的软件公司的大客户，还曾做过独立的咨询顾问。他的活动范围很广，包括从内核开发到产品管理，从系统编程到系统管理，从处理卫星数据到为油泵编程，从生产CD-ROM到把自由软件移植到DSP指令集上。他是FreeBSD核心团队的成员，也是澳大利业UNIX用户协会的主席。他是FreeBSD和NetBSD项目的开发者，也是《Porting UNIX Software》和《The Complete FreeBSD,Fourth Edition》（O'Reilly）的作者。他还以编写商业应用软件而闻名。Greg在2007年退休，将多出来的时间用于品味生活。现在，休闲活动占据了他的大多数时间，但这还不够，他还听古典木纹唱片、烹饪、酿啤酒（他开发了一个计算机控制的发酵系统）、做园艺、骑马和摄影。他也对一些历史题材感兴趣，包括古代的难解的欧洲语言。他的主页是：http://www.lemis.com/grop/。

Panagiotis Louridas在20世纪80年代通过一台Sinclair ZX Spectrum开始涉足计算机。从那时起，他就开始用机器语言进行编程，而且非常喜欢编程。他在雅典大学信息系获得了计算机科学学士学位，在曼彻斯特大学获得了计算机硕士和博士学位。这些年来，他一直为私人机构开发软件，现在，他在希腊研究和教育网络（GRNET）工作。他也是雅典经济与商业大学（AUEB）软件工程和安全（SENSE）研究组的成员。他发表的文章范围很广，从人类学到加密，从仪表展示到软件工程。他特别喜欢寻找计算机世界和其他领域的联系。

Stephen J.Mellor在为软件开发创建有效的工程方法方面，是国际公认的先行者。在1985年，他出版了广为阅读的Ward-Mellor三卷本《Structured Development for Real-Time Systems》（Prentice Hall）；在1998年，他的书首次定义了面向对象分析。Stephen还在2002年出版了《Executable UML：A Foundation for Model-Driven Architecture》（Addison-Wesley Professional）。他最近的一本书《MDA Distilled：Principles of Model-Driven Architecture》（Addison-Wesley Professional）在2004年出版。他在对象管理组织（OMG）中活动积极，是为UML添加可执行动作的协会的主席，他最近完成了可执行UML的标准。他是敏捷宣言的签名者之一。他是OMG架构委员会的两任成员，IEEE软件顾问委员会的主席，最近，他成为Mentor Graphics的嵌入式软件部门的首席科学家。

Bertrand Meyer是ETH Zurich的软件工程教授，也是Eiffel软件的首席架构师，他领导并设计了EiffelStudio环境和大量的库。他是一些畅销书的作者，其中包括获得Jolt大奖的《Object-Oriented Software Construction》（Prentice Hall）。他也因为在对象技术和Eiffel方面的工作获得了ACM软件系统大奖和Dahl-Nygaard大奖，并获得了St.Petersburg州立技术大学的荣誉博士学位。他的研究对象涉及面向对象技术、编程语言、软件验证（包括测试、并发和规范方法）。他也是一名活跃的顾问和讲师。

William J. Mitchell是MIT架构和媒体艺术与科学系的Alexander Dreyfoos教授，他领导着MIT媒体实验室和MIT设计实验室的Smart Cities团队。他以前曾担任MIT架构和计划学院的院长。他最近的新书是《World's Greatest Architect》和《Imagining MIT》（都由MIT出版社出版）。

Derek Murray是剑桥大学计算机实验室的博士生。他在2006年加入Xen项目，主要工作是通过重新设计控制栈来改进Xen的安全性。他现在的研究主要是改进大规模分布式系统的容错性，但他还是偶尔会涉及系统核心。Derek在2006年从爱丁堡大学获得了高性能计算专业的硕士学位，2005年获得了Glasgow大学的计算机学士学位。

Rhys Newman在十多年前于牛津大学完成博士学位时，就开始使用Java，那时Java还只有几年历史。在他早期的研究中，他利用纯Java环境展示了高性能实时场景处理的实现方法，即使当时还是使用早期JIT化的JVM。从那时起，他同时在学界和业界工作，一次次证明Java平台实际上有多灵活、多高效、多快。在超过20年的软件工程生涯中，他获得了多个业界杰出技术奖项，最近他回到了牛津，承担了网格计算领域的突破性研究工作。JPC是最新研究工作的一部分。

Michael Nygard致力于在全国帮助开发者提高水平和减少痛苦。他和他遇到的每一个人分享他对改进的热情和活力，有时甚至没有得到对方的同意。Michael花了20年中的大部分时间学习对专业程序员有意义的事，他关心艺术、质量和技艺。他总是愿意在那些全职的、真心投入工作的开发者（那些“觉醒的”开发者）身上花时间。在另一方面，他不能容忍缺乏兴趣或浪费潜力。Michael在近20年来一直是专业的程序员和架构师。在这段时间里，他为美国政府、军方、银行、金融业、农业和零售业交付了运营系统。通常，Michael都要面对他自己开发的系统。这种实际运营的经历改变了他对软件架构和开发的看法。他参与了一个Tier 1零售网站的初期开发，并且常常作为其他在线业务的“流动解决问题专家”。这些经验让他对在相当不友好的环境下构建高性能、高可靠性的软件有了独特的看法。最近，Michael编写了《Release It！Design and Deploy Production-Ready Software》（Pragmatic Programmers），该书获得了2008年的Jolt生产力大奖。他的其他文字可以在http://www.michaelnygard.com/blog上读到。

Ian Rogers是曼彻斯特大学高级处理器技术研究组的研究员。他的博士研究工作是关于Dynamite二进制翻译器的，该技术实现了商用，现在是许多二进制翻译器产品的一部分，包括Apple的Rosetta。他最近的学术研究工作一直是编程语言设计、运行时环境和虚拟机环境，特别是如何自动创建它们并有效地使用并行技术。他是Jikes研究虚拟机的主要贡献者，是开发团队的核心成员。

Brian Sletten是自由的、受过艺术教育的软件工程师，关注forward-learning技术。他曾担任过系统架构师、开发者、现场指导者和培训师。他在世界各地的会议上发表演讲，并为一些在线出版物编写关于面向Web技术的文章。他的经验涉及国防、金融和商业领域。他曾设计并建造了网络矩阵式交换控制系统、在线游戏、3D仿真/可视化环境、因特网分布式计算平台、P2P和基于Web的语义系统。他拥有William and Mary大学的计算机科学学士学位，目前居住在弗吉尼亚的Fairfax。他是Bosatsu咨询公司的总裁，该公司为Web架构、面向资源的计算、语义Web、高级用户界面、可伸缩系统、安全和其他20世纪末21世纪初的技术提供专业的咨询服务。

Diomidis Spinellis是希腊雅典经济与商业大学管理科学与技术系的副教授。他的研究领域包括软件工程、计算机安全和编程语言。他也编写了两本“开放源码方面”的书，由Addison-Wesley出版：《Code Reading》（获得了2004年的软件开发生产力大奖）和《Code Quality》（获得了2007年软件开发生产力大奖，编辑注）。他也写了几十篇科学论文。他是IEEE Software编辑委员会的成员，负责定期的“Tools of the Trade”栏目。Diomidis是FreeBSD的提交者，也是UMLGraph和其他开源软件包、库和工具的开发者。他拥有软件工程的硕士学位和计算机科学博士学位，都是在Imperial College London获得的。Diomidis是ACM的高级成员，也是IEEE和Usenix Association的成员。

Jim Waldo是Sun微系统实验室的杰出工程师，负责研究下一代大规模分布式系统。他目前是Project Darkstar的技术负责人，该系统是针对大规模多人在线游戏和虚拟世界而设计的多线程、分布式基础设施。在此之前，他曾是Jini的首席架构师，Jini是基于Java的分布式编程系统。Jim编写了《The Evolution of C++：Language Design in the Marketplace of Ideas》（MIT出版社），也是《The Jini Specification《Addison-Wesley》的合著者之一。他曾是美国国家学术委员会的共同主席，编辑并出版了《Engaging Privacy and Information Technology in a Digital Age》一书。Jim也是哈佛大学的辅助教师，在计算机科学系教授分布式计算和策略与技术相关的内容。Jim拥有马萨诸塞大学（Amherst）的哲学博士学位。

David Weiss拥有Union College的计算机科学学士学位，并拥有马里兰大学的计算机科学硕士和博士学位。他目前是Avaya实验室的软件技术研究部的领导，他关注软件开发效率改进的普遍问题和Avaya软件开发过程改进的特殊问题。在第二个问题上，他领导了Avaya软件技术研究中心。以前，他曾是朗迅技术贝尔实验室软件生产研究部的主任，该部门负责研究如何改进软件开发的效率。在加入贝尔实验室之前，他是软件生产力协会（SPC）复用和度量部门的主任，该协会由14个大型的美国航空公司组成。在加入SPC之前，Weiss博士在技术评估办公室度过了一年的时间，在那里他与同事共同完成了Strategic Defense Initiative的技术评估。在1985—1986学年，他是Wang Institute的访问学者，在许多年里，他一直是华盛顿特区Naval研究实验室（NRL）计算机科学和系统部门的研究员。他也是一名程序员和数学家。Dave的主要研究方向是软件工程领域，特别是软件开发过程和方法学、软件设计和软件测量。他最为人知的是发明了软件测量的“目标-问题-测量指标”方法，软件系统模块化结构的工作，以及软件生产线工程的工作。他是Synthesis过程及其后继FAST过程的共同发明人。他与别人共同编著了两本书：《Software Product-Line Engineering》和《Software Fundamentals：Collected Papers of David L.Parnas》（都由Addison-Wesley出版）。

译者简介

王海鹏1994年毕业于华东师范大学。拥有理学士（物理）和文学士（英国语言文学）学位。独立的咨询顾问、培训讲师、译者和软件开发者。已翻译十余本软件开发书籍，主题涵盖敏捷方法学、需求工程、UML建模和测试。拥有15年软件开发经验，目前主要的研究领域是软件架构和方法学，致力于提高软件开发的品质和效率。

蔡黄辉江苏启东人。1999年毕业于上海交通大学，毕业后一直从事软件开发工作，主要使用Java做Web方面的底层开发。现居住在上海。联系方式：caihuanghui@hotmail.com。

徐锋中国系统分析员顾问团（CSAI）软件工程首席顾问，中国软件技术大会杰出贡献专家，资深咨询顾问。主要研究领域为需求工程、系统分析与设计、软件估算，致力于推动软件工程方法论的落地应用。曾在《程序员》等媒体发表了《实战OO》、《项目管理三步曲》、《大话Design》等多个专栏文章，著有《软件需求最佳实践》、《UML面向对象建模基础》等多本书籍，翻译了《UML 2.0实战》、《AOSD中文版》、《Cloud to Code中文版》等多本相关技术书籍。

序　Stephen J.Mellor

使用随意的、非正式的工程技术去开发高性能、高可靠性和高品质的软件系统会遇到非常多的困难。这些技术对于过去要求较低的系统也许还能对付，而目前系统的复杂性已经达到了这样一种程度：如果不开发并维护一个基础架构，利用它将系统组织成一致的整体并避免零碎的实现，那么我们就无法应对，必将导致测试和集成失败。

但是，建立一个架构是一项复杂的任务。很难得到合适的例子，因为要么必须考虑专有权，要么刚好相反，有人需要向各式各样的环境推销某一种架构风格。架构是很大的概念，这使得很难在不吓坏读者的情况下来记录或描述。

但是，美丽的架构展示了一些普遍原则，下面我列出了几点：

一处一个事实

重复导致错误，所以应该避免。每个事实应该是单一的、不可分解的单元，每个事实必须独立于其他事实。当改变发生时（这是不可避免的），只有一个地方需要修改。数据库设计者很熟悉这一原则，它以范式（normalization）之名规范化了。这个原则也不那么规范地应用于行为，名为提取公因式（factoring），即同样的功能提取出来，放到独立的模块中。

美丽的架构找到了一些方法来定位信息和行为。在运行时，形成了分层，即系统可以按层来划分，每个层代表了一层抽象（abstraction）或一个领域（domain）。

自动传播

一处一个事实听起来不错，但出于效率的考虑，某些数据或行为常常会重复。为了维护一致性和正确性，这些事实的传播必须在构建时自动进行。

美丽的架构是由一些构建工具支持的，结果导致了元编程（metaprogramming），将一个事实从一处传播到多处，在那些地方有效地使用它们。

架构也包含构建

架构不仅包含运行时系统，而且必须包含它的构建方式。只关注运行时代码会导致架构随时间的推移而退化。

美丽的架构是经过深思熟虑的。它们不仅在运行时是美丽的，在构建时也是美丽的；利用同样的数据、函数和技术来构建系统，就像在运行时使用的那样。

最少量机制

实现某个功能的最佳方式要视情况而定，但是美丽的架构不会追求“最佳”。例如，有许多种方式可以存储和查找数据，但如果系统利用一种机制达到了性能要求，就会考虑编写、验证和维护较少的代码，以及占用较少的内存。

美丽的架构使用一组最少的机制来满足整体的需求。找到每种情况下的“最佳”，会导致各种容易出错的机制的产生，而用极简的方式来添加机制则会得到更小的、更快的、更健壮的系统。

构建引擎

如果您希望构建脆弱的系统，就采用Ivar Jacobson的建议，将架构建立在用例的基础上，每次实现一个功能（例如，使用“控制器”对象）。但是，可扩展的系统依赖于虚拟机的构建，即由更高层提供的数据来“编程”的引擎，一次实现多个应用功能。

这个原则以多种外观出现。虚拟机的“分层”可以追溯到Edsger Dijkstra。“数据驱动的系统”提供了引擎，依赖于系统中的编码常量，让数据来定义特定情况下的具体功能。这些引擎是高度可复用的，也是美丽的。

O（G），增长的阶

在以前，我们会考虑算法的“阶”，例如，根据对一组一定数量的元素进行排序的时间来分析排序的性能。许多书籍都是围绕这个主题来写的。

这同样适用于架构。例如，一个投票程序在少量数据的情况下工作得很好，但数据量增大时响应时间就变得难以接受。按照中断或事件来组织所有的工作，在开始时会工作得很好，但后来可能突然出现问题。美丽的架构会考虑可能的增长方向，并能够支持这种增长。

抵制熵增

美丽的架构设计了一条最容易维护的路线，随着时间的推移仍能够保持架构，所以延缓了“系统熵增定律”的效果。该定律指出，系统会随着时间的推移变得越来越乱。维护者必须适应该架构，这样变更才能与架构保持一致，不增加系统的熵。

一种方法是利用敏捷开发的隐喻（Metaphor）概念，它是一种简单的方式，说明架构像什么。另一种方法是使用大量的文档，并以解雇相威胁，虽然这种方法难以取得长久的效果。然而，这通常与工具有关系，特别是那些生成系统的工具。美丽的架构必须保持美丽。

这些原则是高度相关的。只有在你实现了自动传播时，一处一个事实才行得通，而自动传播又只有在架构考虑到构建时才有效果。类似地，构建引擎和最少量机制支持一处一个事实。抵制熵增是长时间保持架构的要求，它靠的是架构包含构建方法并支持自动传播。而且，如果忘记考虑系统将以何种方式增长，将导致架构不稳定，最终会在极端但可预见的环境下失败。将最少量机制与构建引擎的观点结合起来，就意味着美丽的架构通常包含一组有限的模式，同时支持构建任意的系统扩展，这相当于“按模式扩展”。简而言之，美丽的架构用更少的机制做更多的工作。

本书由Diomidis Spinellis和Georgios Gousios汇编而成，当你阅读本书时，你可以通过每章提供的具体例子，寻找这些原则并思考它们的意义。你也可以寻找违反这些原则的情况，想想这是否导致架构变得丑陋，或涉及某些更高的原则。

在写这篇序时，作者问我能否说说如何才能成为好的架构师。我笑了起来。如果我知道的话……但是接着我从自己的经历中想到，有一种特效方法[1]可以帮助人们成为美丽的架构师。这种方法就是永远不要相信你最近创建的系统是唯一的，应设法寻找不同方法来解决相同类型的问题。本书提供的美丽架构的示例就能帮助你朝着实现这个目标迈出一步。

[1]或者是锻炼更多，吃得更少。

前言

出版本书的想法是在2007年确立的，它是获奖的畅销书《Beautiful Code》[1]的姊妹篇。在这本书中，虽然范围和目的不一样，但关注点是类似的：让最优秀的设计师和架构师来描述他们所选的软件架构，剥开他们作品的各层，展示他们如何让软件实现功能性、可靠性、易用性、高效率、可维护性、可移植性，当然，还有优雅性。

为了编成这本书，我们联系了一些著名软件项目的主要架构师和一些不太著名但高度创新的软件项目的主要架构师。许多人快速回应，将引人思索的想法寄回给我们。有些想法甚至让我们大吃一惊，他们建议不要写具体的系统，而是调查架构在软件工程中产生影响的深度和广度。

当本书的所有作者听说这本书的版税将捐给Medécins Sans Frontieres（无国界医生组织）时，都感到十分高兴。Medécins Sans Frontieres是一个国际人道主义援助组织，为困难的人提供紧急医疗救助。

本书的组织

我们围绕5个主题领域来组织本书的内容：概述、企业应用、系统、最终用户应用和编程语言。很明显，缺少有关桌面软件架构的章节，但这不是故意的。我们联系了50多位软件架构师，这个结果让我们吃惊不小。难道真的没有美丽桌面软件架构的漂亮例子吗？或者那些天才的架构师避而不谈架构，是因为忙着应付需求，为应用堆彻更多的功能？我们非常期望听听你对于这些问题的见解。

第一部分：论架构

本书的第一部分探讨了软件架构的广度和范围，以及它对软件的开发和演进意味着什么。

第1章由John Klein和David Weiss编写，他们从架构的品质考虑和架构结构的角度，对架构进行了探讨。

第2章由Pete Goodliffe编写，他写了一篇寓言，揭示了软件架构如何影响系统的演进和开发者在项目中的参与情况。

第二部分：企业级应用架构

企业级系统是许多组织机构的IT支柱，它们不仅巨大，而且常常是度身定制的软件集，一般由许多分散的组件构成。它们服务于大量的、支持事务的工作，必须延伸到它们所支持的企业的各个角落，时刻准备着适应不断变化的业务需求。在为这样的系统设计架构时，可伸缩性、正确性、稳定性和可扩展性是最重要的关注点。本书的第二部分包含了一些企业级软件架构的优秀范例。

第3章由Jim Waldo编写，展示了构建大规模多人在线游戏所需的架构技术。

第4章由Michael Nygard编写，介绍了一个多阶段、多地点的数据处理系统的架构，展示了让系统能工作所必需的折中。

第5章由Brian Sletten编写，讨论了创建数据驱动的应用时资源映射的威力，提供了纯面向资源架构的一个优雅的例子。

第6章由Dave Fetterman编写，他提倡以数据为中心的系统，解释了好的架构如何能够创造并支持应用生态系统。

第三部分：系统架构

系统软件可能是在设计方面要求最高的软件，部分原因是因为高效率地使用硬件是少数人才能掌握的神秘艺术，还有部分原因是因为许多人认为系统软件的架构是理所当然的存在。了不起的系统架构很少是从一张白纸开始的，我们今天使用的大多数系统是基于20世纪60年代的设想。第三部分的这几章介绍了4种创新的系统架构，讨论了架构决定背后的复杂性，这正是它们美丽的原因。

第7章由Derek Murray和Keir Fraser编写，提供了一个例子说明深思熟虑的架构如何能够改变操作系统演进的方式。

第8章由Greg Lehey编写，回顾了Tandem的架构选择和组成部分（包括硬件和软件），正是这些让Tandem成为近二十年的高可用性环境的首选平台。

第9章由Rhys Newman和Christopher Dennis编写，描述了如何通过小心地设计软件和很好地理解领域需求来克服编程系统中那些可以察觉到的不足。

第10章由Ian Rogers和Dave Grove编写，介绍了为高级语言创建自优化的、自支持的运行时环境所需的架构选择。

第四部分：最终用户应用架构

最终用户应用程序是我们每天的计算生活都要使用的那些应用程序，是占据我们最多CPU指令周期的软件。这种软件通常不需要仔细地管理资源，也不需要执行大量的事务。但是，它需要易于使用、安全、可定制和可扩展。这些属性可以导致软件广泛流行和使用，以免费开源软件为例，会有大量的自愿者愿意改进它。在第四部分，作者们分析了两个非常流行的桌面软件包的架构和需要的社区过程。

第11章由Jim Blandy编写，解释了一组非常简单的组件和一门扩展语言如何将一个不起眼的文本编辑器变成了一个操作系统[2]，成为程序员工具箱中的瑞士军刀。

第12章由Till Adam和Mirko Boehm编写，展示了冲刺和同级评审这样的社区过程如何帮助软件架构从简单的骨架演变为美丽的系统。

第五部分：语言与架构

正如许多人在他们的著作中指出的那样，我们使用的编程语言影响了我们解决问题的方式。但是编程语言也能影响系统的架构吗？如果是这样，那么是如何影响的？在建筑的架构中，新的材料和CAD系统的采用让人们能够表达更复杂、有时更惊人的漂亮设计。对于计算机编程来说也是如此吗？第五部分包含最后的两章，探讨了我们使用的工具和得到的设计之间的关系。

第13章由Bertrand Meyer编写，比较了面向对象和函数式架构风格的适用性。

第14章由Panagiotis Louridas编写，探讨了现代和经典面向对象软件语言的组件背后的架构选择。

最后，在引人深思的跋中，William J.Mitchell（MIT架构与媒体艺术科学系教授）将真实世界中建筑架构的美丽概念与硅基芯片上软件架构的美丽概念联系在了一起。

原则、特性与结构

在这本书评阅过程的后期，一位审阅者要求我们提供个人的观点作为一种补充，告诉读者可以从每一章中学到些什么。这个想法很有趣，但我们不想再猜测各章作者的意思。要求作者们自己提供他们文章的元分析，就会得到一堆的定义、术语和架构结构，这肯定把读者搞糊涂。我们需要一组通用的架构术语，谢天谢地，我们意识到我们已经有了。在序中，Stephen Mellor讨论了7个原则，所有美丽的架构都基于这7个原则。在第1章中，John Klein和David Weiss提供了4种架构组件和美丽架构的6种特性。细心的读者会发现，Mellor的原则与Klein和Weiss的特性不是彼此无关的。实际上，它们大多数是一致的，这是因为英雄所见略同。他们三人都是非常有经验的架构师，人们曾多次在实际工作中看到过他们描述的概念的重要性。

我们将Mellor的架构原则与Klein和Weiss合并成为两个列表：一个包含了原则和特性（表P-1），另一个包含了结构（表P-2）。然后我们要求各章的作者标出他们认为适用于自己那一章的术语，得到了每一章对应的说明。在这些表中，你可以看到各章说明中出现的每个原则、特性或架构结构的定义。我们希望这些说明对你的阅读提供帮助，它们清晰地总结每一章的内容。但是我们强烈建议你深入阅读每章的内容，而不只是停留在这些说明上。

本书体例

我们在本书中使用下面的印刷惯例：

斜体（Italic）

表示URL、邮件地址。

等宽字体（Constant width）

用于程序清单，以及在段落中引用程序元素，如变量名或函数名、数据库、数据类型、环境变量、语句和关键字。

等宽粗体（Constant width bold）

表示命令或其他由用户输入的文本。

等宽斜体（Constant width italic）

表示应该由用户输入的值或上下文相关的值来替代的文本。

使用代码示例

这本书的目的是帮助你完成自己的工作。一般来说，你可以在程序中使用这本书中的代码和文档。你不需要联系我们获得许可，除非你打算复制大量的代码。例如，写一个程序使用本书中的几段代码是不需要获得许可的。销售或发行来自O'Reilly书籍的示例光盘需要许可。引用本书来回答问题或引用示例代码不需要许可。在产品文档中包含大量本书的示例代码需要获得许可。

我们希望在引用时说明来源，但这不是必需的。来源说明通常包括标题、作者、出版商和ISBN号。例如：“Beautiful Architecture,edited by Diomidis Spinellis and Georgios Gousios.Copyright 2009 O'Reilly Media,Inc.，978-0-596-51798-4.”

如果你觉得对示例代码的使用超过了这里的许可，请通过permissions@oreilly.com联系我们。

我们的联系方式

请把涉及本书的评论和疑问邮寄至以下地址：

美国：

O'Reilly Media,Inc.

1005 Gravenstein Highway North

Sebastopol,CA 95472

中国：

100035北京市西城区西直门南大街2号成铭大厦C座807室

奥莱利技术咨询（北京）有限公司

我们为本书制作了网页，上面列出了勘误表、示例和任何附加的信息。该网页地址为：

http://www.oreilly.com/catalog/9780596517984

http://www.oreilly.com.cn/book.php?bn=978-7-111-28356-0

为了评论或获得本书的技术支持，请发E-mail至以下地址：

bookquestions@oreilly.com

info@mail.oreilly.com.cm

欲获得其他图书、会议、资源中心和O'Reilly网络的更多信息，请访问以下网址：

http://www.oreilly.com

致谢

一本书的出版是一个团队努力的结果，一本合集更是如此。我们要感谢许多人。首先，我们要感谢本书的诸位作者，他们及时地提供了优秀的素材，然后满足了我们多次改动和修订的要求。这本书的审阅者包括Robert A.Maksimchuk、Gary Pollice、David West、Greg Wilson和Bobbi Young，他们给出了许多极好的建议，改进了每一章和整本书。在O'Reilly，我们的编辑Mary Treseler帮助我们联系作者，组织复审过程，高效地管理本书的出版过程。后来，Sarah Schneider作为本书的制作编辑，巧妙地处理了紧张的进度计划和经常发生冲突的要求。文字编辑Genevieve d'Entremont和编索引者Fred Brown灵巧地理顺了来自世界各地的作者的材料，让它们就像是一个人写出来的那样流畅。插图作者Robert Romano设法将我们提供的各种图像格式（包括一些手绘的草图）变成专业的图表。封面设计者Karen Montgomery设计了漂亮的、让人兴奋的封面，切合本书的内容。版式设计者David Futato设计了创新而实用的布局结构，将每章的说明与图书的设计集成在一起。最后，我们要感谢我们的家人和朋友在我们将精力集中在这本书上时给予的支持，这些时间本应属于他们。

[1]本书由机械工业出版社引进出版，其中文版书名为《代码之美》，标准书号为978-7-111-25133-0。

[2]正如某些忠心的用户所说的：“Emacs是我的操作系统，Linux只是提供了设备驱动。”

第一部分　论架构

第1章　架构概述

第2章　两个系统的故事：现代软件神话

第1章　架构概述John Klein David Weiss

1.1　简介

建筑师、音乐家、作家、计算机设计师、网络设计师和软件开发者都在使用“架构”这个术语，其他人也用（你有没有听说过“食物架构”？），然而不同的用法其结果也不同。建筑与交响乐完全不同，但都有架构。而且，所有的架构师都在谈论他们工作中的美，以及因此而导致的结果。建筑师可能会说，一座建筑应该提供适合工作或生活的环境，而且它应该看起来很美。音乐家可能会说，音乐应该能演奏，包含能够辨明的主题，而且它应该听起来很美。软件架构师可能会说，系统应该对用户友好、响应及时、可维护、没有重大错误、易于安装、可靠，应该通过标准的方式与其他系统通信，而且也应该是美的。

这本书为你提供了一些美丽架构的详细例子，它们来自于各类计算机系统。相对来说，计算机是比较年轻的一个学科。因为年轻，所以不像建筑、音乐或写作等领域那样，有那么多的例子；也因为年轻，则需要更多的例子。我们希望这本书能满足这种需要。

在你开始研究这些例子之前，我们希望你考虑以下两个问题：1）什么是架构？2）美丽的架构都有哪些特性？你会在这一章中看到架构的不同定义，每个学科都有自己的定义，所以我们将首先探讨不同学科中的架构有何共同点，以及人们试图用架构解决哪些问题。具体来说，架构有助于确保系统能够满足其利益相关人的关注点，在构想、计划、构建和维护系统时，架构有助于处理复杂性。

然后我们将介绍架构的定义，展示如何将这个定义应用于软件架构，因为软件是本书后面大部分例子关注的核心。这个定义的关键在于，架构由一组结构组成，这些结构的设计目的是让架构师、构建者，以及其他利益相关人看到他们的关注点是如何得到满足的。在本章末尾，我们将讨论美丽架构的特性，并引用一些例子。美的核心在于概念完整性—即一组抽象和规则，在整个系统中尽可能简单地应用它们。

在讨论中，我们将“架构”作为一个名词，它意味着一组工件，包括像蓝图和构建规范这样的文档。这些工件描述了要构建的对象，在这种描述中，该对象被视为一组结构。某些人也把“架构”作为一个动词，用来描述创建这些工件的过程，包括由此而导致的工作。然而，正如Jim Waldo和其他人曾指出的，没有什么过程可以保证你学了以后就能创造出好的系统架构，更不必说美的架构了（Waldo 2006），所以我们将更关注工件，而非过程。

架构：“建造的艺术或科学；特别是设计和建造人类使用的建筑时的艺术或实践，同时考虑到美学因素和实用因素。”

——《The Shorter Oxford English Dictionary》（小型牛津英语字典，第5版）

在所有学科中，架构都提供了一种方式来解决共同的问题：确保建筑、桥梁、乐曲、书籍、计算机、网络或系统在完成后具有某些属性或行为。换言之，架构既是所构建系统的计划，确保由此得到期望的特性，同时也是所构建系统的描述。维基百科上说：“根据这方面已知最早的著作，即Vitruvius的‘On Architecture’，好的建筑应该美观（Venustas）、坚固（Firmitas）、实用（Utilitas）；架构可以说是这三方面的一种平衡和配合，没有哪一个方面比其他方面更重要。”

我们谈到交响乐的“架构（architecture）”，反过来，又将架构（architecture）称为“凝固的音乐”。

——Deryck Cooke，《The Language of Music》（音乐的语言）

好的系统架构展示了架构完整性。也就是说，它来自于一组设计规则，这组规则有助于减少复杂性，并可以用于指导详细设计和系统验证。设计规则可能包含特定的抽象，这些抽象总是以同样的方式使用，诸如虚拟设备等。这些规则可能表现为一种模式，如管道和过滤器。在最理想的情况下，存在一些可以用于验证的规则，如“在设备失效时，所有某一类的虚拟设备都可以用任何其他同类的虚拟设备代替”，或“所有竞争同一资源的进程必须具有相同的调度优先级”。

当代的架构师可能会说，待构建的对象或系统必须具有以下特征：

·具备客户要求的功能。

·能够在要求的工期内安全地构建。

·性能足够好。

·可靠的。

·可用的，并且使用时不会造成伤害。

·安全的。

·成本是可以接受的。

·符合法规标准。

·将超越前人及其竞争者。

我们将计算机系统的架构定义为一组最小的特征集，它们决定了哪些程序将运行，以及这些程序将得到什么结果。

——Gerrit Blaauw和Frederick Brooks，《Computer Architecture》（计算机体系结构）

我们从来没有看到过一个复杂系统能够很好地满足上述特征。架构是一种折中—决定改进其中一个特征常常会对其他特征产生负面影响。架构师必须确定怎样做是足够好的，方法就是发现特定系统的重要关注点，以及充分满足这些关注点的条件。

架构观点中的常见思想是结构，每种结构都由各种类型的组件及其关系构成：它们如何组合、相互调用、通信、同步，以及进行其他交互。组件可以是建筑中的支架横梁或内部腔室、交响乐中的旋律、故事中的章节或人物、计算机中的CPU和内存、通信栈中的层或连接到一个网络上的处理器、协作的顺序过程、对象、编译时的宏、构建时的脚本。每个学科都有自己的一套组件和组件间的相互关系。

从更大的范围来说，术语“架构”总是意味着“不变的深层次结构”。

——Stewart Brand，《How Buildings Learn》

面对不断增长的系统复杂性，以及它们内部和相互之间的交互，由一组结构形成的架构提供了对付复杂性的主要手段，目的是确保得到的系统具备所要求的特征。结构为我们提供途径，将系统化解为一些交互的组件。

每种结构都是为了帮助架构师理解如何来满足特定的关注点，如可变性或性能。展示某些关注点得到满足时，可能会影响到其他方面的关注点，但架构师必须能够说明所有关注点都已得到满足。

网络架构：构成一个网络的通信设备、协议和传输链路，以及它们的组织方式。

——http://www.wtcs.org/snmp4tpc/jton.htm

1.1.1　建筑师的角色

在设计、构建和修复建筑时，我们指定关键的设计师为“建筑师（architects）”，并赋予他们广泛的职责。建筑师准备建筑最初的草图，展示外观和内部布局，与客户讨论这些草图，直至所有相关方都达成一致意见，认为展示的就是他们想要的。这些草图是抽象：它们关注建筑中某些方面的适当细节，而忽略其他的内容。

当客户和建筑师在这些抽象上达成一致意见之后，建筑师会准备或监督准备更为详细的图纸，以及相关的文字规格说明。这些图纸和规格说明描述了建筑的许多“实质性”细节，如管道、壁板材料、窗户玻璃和电线等。

在极少的情况下，建筑师简单地将详细规划交给建造者，建造者将根据规划完成项目。对更重要一些的项目，建筑师会继续参与，定期检查工作，并且可能会建议变更，或接受来自建造者和客户的变更建议。如果建筑师监督项目，仅当他确认项目充分符合了规划和规格说明的要求，项目才算完工。

我们请一名建筑师是为了确保：1）设计满足客户的需要，包括前面提到的那些特征；2）设计具有概念完整性，处处运用了相同的设计原则；3）设计满足法规和安全的要求。建筑师职责的一个重要方面是确保设计概念在实现时得到一致的体现。有时候，建筑师也充当建造者和客户之间的协调人。哪些决定需要由建筑师做出，哪些决定由其他人做出，人们对这个问题常有不同意见，但我们清楚，建筑师将做出重要决定，包括所有对结构的可用性、安全性和可维护性产生影响的那些决定。

音乐作曲与软件架构

虽然人们常用建筑架构设计来类比软件架构，但音乐作曲可能是更好的类比。建筑师创建的是相对静止的结构（该架构必须考虑到人员和服务在建筑内的移动，以及承重结构）的静态描述（蓝图或其他图纸）。在音乐作曲和软件设计中，作曲家（软件架构师）创建一段音乐的静态描述（架构描述和代码），这段音乐以后将演奏（执行）许多次。在音乐和软件中，设计都依靠许多组件的交互来得到期望的结果，结果依赖于演奏者、演奏环境，以及演奏者所做的诠释。

1.1.2　软件架构师的角色

软件开发项目需要一些人在软件构建时扮演架构师的角色，就像构建或修复建筑时传统的建筑师的角色一样。但是，对于软件系统来说，从来就弄不清楚哪些决定属于架构师的职责范围，哪些决定要留给实现者。定义架构师在软件项目中做什么，比建筑师的类似定义更困难，原因有3个因素：缺少传统、产品无形性和系统复杂性。（参见Grinter[1999]，其中描述了软件架构师如何在一个大型软件开发组织中实现她的职责。）具体来说：

·建筑师可以回顾几千年的历史，看看过去的建筑师都做过些什么。他们可以参观并研究那些矗立了几百年的建筑，有时甚至有上千年历史的建筑，而它们仍在使用。在软件业，我们只有几十年的历史，并且我们的设计常常是不公开的。此外，建筑师拥有并利用标准来描述他们制作的图纸和规格说明，这让现在的建筑师能够从记录下来的架构历史中受益。

·建筑是有形的产品，在建筑师制作的规划和工人修造的建筑之间存在着明显的区别。

架构复用

圣索菲亚大教堂（Hagia Sophia，上图），建造于公元6世纪，率先使用了所谓的“穹顶”结构来支撑巨大的圆形屋顶，它是拜占庭建筑之美的代表。在1100年之后，Christopher Wren使用了同样的设计来建造圣保罗大教堂的穹顶（St.Paul's Cathedral，下图），它成为伦敦的地标性建筑。这两座建筑在今天仍在使用。

在大的软件项目中，常常会有许多架构师。某些架构师相当专注于特定领域，如数据库和网络，他们一般作为团队的一部分，但目前我们假定只有唯一一位架构师。

1.1.3　软件架构的含义

如果认为“架构”是一个简单的实体，能够用一份文档或一张图纸来描述，那就错了。架构师必须做出许多设计决定。要想有用，这些决定必须用文档记录下来，这样就能够进行复审、讨论、修改和批准，然后作为后续决定和构建时的约束。对于软件系统，这些设计决定包括行为上的和结构上的。

外部行为描述展示了产品如何与它的用户、其他系统和外部设备进行交互，这应该表现为需求。结构描述展示了产品如何划分为多个部分，以及这些部分之间的关系。我们还需要内部行为描述，用于描述组件之间的交互接口。结构上的描述常常展示相同部分的一些不同视图，因为不可能把所有信息以有意义的方式组织到一张图纸或一份文档中。一个视图中的组件，可能是另一个视图中一个组件的一个部分。

软件架构常常表现为分层的层次结构，这种层次结构将几种不同的结构放在一张图中。20世纪70年代，Parnas指出“层次结构”这个术语已经被滥用，然后精确地定义了它，并给出了几个不同结构的例子，它们在设计不同系统时实现了不同的目的（Parnas 1974）。将架构的结构描述为一组视图（view），每个视图关注不同的部分，现在已成为了广泛接受的标准架构实践（Clements等2003；IEEE 2000）。我们将使用“架构”这个词来代指一组有标注的图纸和功能描述，它说明了设计和构建一个系统时所使用的结构。在软件开发社区中，针对这样的图纸和描述，人们使用并建议了许多不同的形式。在Hoffman和Weiss（2000，第14章和第16章）的著作中可以看到一些例子。

一个程序或计算系统的软件架构是系统的一种结构或一组结构，它包含软件元素、这些元素的外部可见的属性，以及元素之间的关系。

“外部可见”的属性是其他元素对该元素可以做出的假定，诸如它提供的服务、执行时的特征、错误处理、共享资源的使用等。

——Len Bass、Paul Clements和Rick Kazman《Software Architecture in Practice,Second Edition》

1.1.4　架构与设计

架构是系统设计的一部分，它突出了某些细节，并通过抽象省略掉另一些细节。所以，架构是设计的一个子集。关注实现系统组件的开发者可能不会特别关心所有组件如何装配在一起，而是主要关注少数组件的设计和开发，包括他们必须遵守的架构约束和可以应用的规则。因此，开发者和架构师面对的是系统设计的不同方面。

如果说架构关注的是组件之间的关系和系统组件外部可见的属性，那么设计还要关注这些组件的内部结构。例如，如果一组组件包含了一些信息隐藏的模块，那么这些外部可见的属性就构成了这些组件的接口，内部的结构与模块内的数据结构和控制流一同考虑（Hoffman和Weiss 2000，第7章和第16章）。

1.2　创建软件架构

到目前为止，我们已经讨论了一般意义上的架构，并分析了软件架构与其他领域的架构之间有何相似与差异。接下来我们将注意力转到“如何”设计软件架构。当架构师创建软件系统的架构时，她应该关注什么？

软件架构师的首要关注点不是系统的功能。

这是正确的—软件架构师的首要关注点不是系统的功能。

例如，如果我们请你来设计一个“基于Web的应用”，你首先问我们页面布局和导航树，还是问下面这些问题：

·谁提供应用主机托管？托管的环境有什么技术限制吗？

·你想运行在Windows服务器上还是在LAMP栈上？

·你想支持多少并发用户？

·应用需要怎样的安全性？有需要保护的数据吗？应用将运行在公网上还是在私有的内部网上？

·你能为这些答案排列优先级吗？例如，用户数是否比响应时间更重要？

根据我们对这些问题和一些其他问题的回答，你就可以开始画出系统架构的草图。我们还没有谈到应用的功能。

好吧，我们承认耍了点计谋，因为我们问的是“基于Web的应用”，这是一个大家熟悉的领域，所以你已经知道了哪些决定会对你的架构产生最大的影响。类似地，如果我们问的是一个电信系统或一个航空电子控制系统，在这些领域有经验的架构师将考虑到一些功能需求。但是，你仍然可以不必过多担心功能就开始设计架构。你关注的是需要满足的品质。

品质关注点指明了功能必须以何种方式交付，才能被系统的利益相关人所接受，系统的结果包含这些人的既定利益。利益相关人有一些关注点，架构师必须重视。稍后，我们将讨论为了确保系统具有要求的品质，通常会提出的一些关注点。正如我们前面所说的，架构师的一项职责是确保系统设计能满足客户的需要，我们将利用品质关注点来帮助我们理解这些需要。

这个例子突出了成功架构师的两项关键实践：让利益相关人参与以及同时关注功能和品质。作为一名架构师，你首先问我们想从系统中得到什么，有怎样的优先级。在实际项目中，你会找出其他的利益相关人。典型的利益相关人和他们的关注点包括：

·投资人，他们想知道项目是否能够在给定的资源和进度约束下完成。

·架构师、开发人员和测试人员，他们首先考虑的是最初的构建和以后的维护与演进。

·项目经理，他们需要组织团队，制定迭代计划。

·市场人员，他们想通过品质特点实现与竞争者的差异化。

·用户，包括最终用户、系统管理员，以及安装、部署、准备、配置人员。

·技术支持人员，他们关注帮助平台电话呼入的数目和复杂性。

每个系统都有自己的品质关注点。有些关注点可能定义得很好，如性能、安全、可伸缩性等。但是，另一些同样重要的关注点却可能没有详细规定，如可变性、可维护性和可用性等。利益相关人希望把功能放到软件上，而不是放到硬件上，这主要是为了很容易、很快速地修改，然后通常在品质关注方面又对可变性轻描淡写。这很奇怪，不是吗？哪些改变能够迅速、容易地实现，哪些改变需要花时间并且很难实现，架构决定将对此产生重要影响。所以，架构师难道不应该在理解功能需求的同时，也理解利益相关人在“可变性”这样的品质方面的期望吗？

当架构师理解了利益相关人的品质关注点之后，接下来该做些什么？考虑折中。例如，对信息加密将加强安全性，但会损失性能。利用配置文件将增加可变性，但会降低可用性，除非我们能够验证配置是有效的。我们是否应该对这些文件使用标准的表示方式，如XML，还是使用自己发明的格式？创建系统的架构将涉及许多这样的艰难折中。

架构师的第一项任务，就是与利益相关人协作，理解这些品质关注点和约束，并为它们排列优先级。为什么不从功能需求开始？因为通常有许多种可能的系统分解方式。例如，从数据模型开始可能得到一种架构，而从业务处理模型开始则可能得到不同的架构。在极端的情况下，系统没有分解，被开发成单一的软件。这可能会满足所有的功能需求，但可能不会满足品质需求，如可变性、可维护性、可伸缩性等。架构师通常必须进行架构层面的系统重构，例如为了满足伸缩性或性能的要求，将单机部署迁移到分布式部署，从单线程转向多线程，或者将硬编码的参数移到外部配置文件中，因为原来从不改变的参数现在需要修改了。

尽管有许多架构都能满足功能需求，其中却只有一少部分能够满足品质需求。让我们回到Web应用的例子。请考虑提供Web页面的诸多方式—Apache和静态页面、CGI、Servlet、JSP、JSF、PHP、Ruby on Rails、ASP.NET等。选择其中的一种技术是一种架构决定，它将对你满足特定品质需求的能力产生重要影响。例如，像Ruby on Rails这样的方式可能提供快速推向市场的好处，但可能更难维护，因为Ruby语言和Rails框架都在不断地快速发展。也许我们的应用是基于Web的电话，我们需要让电话“响铃”。如果你为了满足性能的要求，需要从服务器向Web页面发出真正异步的事件，那么基于Servlet的架构可能更容易测试和修改。

在真实的项目中，满足利益相关人的关注点需要做出更多的决定，而不仅是选择一个Web框架。你是否真的需要一个“架构”，并需要一名“架构师”来做出这些决定？谁将做出这些决定？是编程人员吗？他们可能会做出许多无意识的、隐含的决定。还是由架构师来做出这些决定？他们全面了解整个系统、利益相关人和系统的演进，然后做出明确的决定。不论哪种方式，你会有一个架构。它是否应该明确地形成并记入文档？或者它应该是隐式的，需要通过阅读代码才能发现？

当然，这种选择通常不是这么死板。但是，随着系统的规模、复杂度和开发人员数目的增长，这些早期决定以及它们的记录方式将产生越来越大的影响。

我们希望你现在已经理解，如果你的系统要满足其品质要求，架构决定是很重要的，你需要注意架构，有意识地做出这些决定，而不只是“让架构自动出现”。

如果系统非常大，情况会怎样？我们之所以运用“分而治之”这样的架构原则，一个原因就是为了降低复杂性，让工作能够并发进行。这让我们能够创建越来越大的系统。架构本身是否能够分解为多个部分，这些部分是否能由不同的人并行开发？考虑到计算机的架构，Gerrit Blaauw和Fred Brooks断定：

……如果，在采取了所有让任务能够由单人处理的方法之后，架构任务仍然巨大而复杂，不能由一人来完成，那么产品肯定是太复杂了，以致不实用且不应构建。换言之，单个用户必须能够理解计算机的架构。如果计划的架构不能由一个人设计，那它也不能被一个人理解。（1997）

你是否需要理解架构的所有方面，才能使用它？架构会分离关注点，所以在大多数情况下，利用架构来构建或维护系统的开发人员或测试人员，不需要一下面对全部的架构，而是只要面对必要的部分，就能完成指定的功能。这让我们能够创建超出个人可以理解的、更大的系统。但是，在我们完全忽略IBM System/360（最长寿的计算机架构之一）创造者的建议之前，让我们先来看看他们为什么这样说。

Fred Brooks说，概念完整性是架构最重要的特征：“最好是让系统……反映一组设计思想，而不是让系统包含许多好的思想，而这些思想却彼此独立而不协调”（1995）。正是这种概念完整性，让开发者在知道了系统的一部分之后，能够迅速理解系统的另一部分。概念完整性来自于处理问题的一致性，如分解的判据、设计模式的应用和数据模式。这让开发者运用在系统中的一部分工作的经验，来开发和维护系统的其他部分。同样的规则应用于整个系统各处。当我们转向“众系统之系统”时，在集成了这些系统的架构中也必须保持概念完整性。例如，可以选择发布/订阅消息总线这样的架构风格，然后将这种风格统一地应用于“众系统之系统”的系统集成中。

架构团队的挑战在于，在创建架构时保持同一种思考方式和同一种哲学。让团队保持尽可能小，让他们在充分沟通、高度协作的环境工作，让一两个“首席架构师”担任仁慈的独裁者，最终做出所有决定。这种架构模式常见于成功的团队，不论是公司开发还是开源开发，由此而得到的概念完整性是美丽架构的一种特性。

好的架构师通常来自于更好的架构师提供的现场指导（Waldo 2006）。原因之一可能是有一些关注点几乎在所有项目中都会出现。我们已经提到过一些，但这里有一份更完整的清单。每个关注点都以问题的方式表述，架构师在项目过程中可能需要考虑它。当然，具体系统会有其他关键的关注点。

功能性（Functionality）

产品向它的用户提供哪些功能？

可变性（Changeability）

软件将来可能需要哪些改变？哪些改变不太可能发生，不需要特别容易进行这些改变？

性能（Performance）

产品将达到怎样的性能？

容量（Capacity）

多少用户将并发使用该系统？该系统将为用户保存多少数据？

生态系统（Ecosystem）

在部署的生态环境中，该系统将与其他系统进行哪些交互？

模块化（Modularity）

如何将编写软件的任务分解为工作指派（模块），特别是这些模块可以独立地开发，并能够准确而容易地满足彼此的需要？

可构建性（Buildability）

如何将软件构建为一组组件，并能够独立实现和验证这些组件？哪些组件应该复用其他的产品，哪些应该从外部供应商处获得？

产品化（Producibility）

如果产品将以几种变体的形式存在，如何开发一个产品线，并利用这些变体的共性？产品线中的产品以怎样的步骤开发（Weiss和Lai 1999）？在创建一条软件产品线时，要进行哪些投资？开发产品线中不同变体的选择，预期会得到怎样的回报？特别是，是否可能先开发最小的有用产品，然后再添加（扩展）组件，在不改变以前编写的代码的情况下，开发产品线的其他成员？

安全性（Security）

产品是否需要用户认证，或者必须限制对数据的访问？数据的安全性如何得到保证？如何抵挡“拒绝服务”攻击或其他攻击？

最后，一个好的架构师会认识到，架构会影响组织机构。Conway指出，系统的结构会反映构建它的组织机构的结构（1968）。架构师可能会认识到，Conway法则可以反过来应用。换言之，一个好的架构可能对组织机构产生影响，让组织机构发生改变，从而更有效地从该架构构建出系统。

1.3　架构结构

那么，好的架构师如何来处理这些关注点？我们曾经提到过，需要将系统组织成一些结构，每种结构都定义了特定类型的组件之间的具体关系。架构师的主要关注点就是对系统进行组织，让每种结构有助于解答一个关注点所定义的问题。关键的结构决定将产品划分为组件，并定义了这些组件之间的关系（Bass、Clements和Kazman 2003；Booch、Rumbaugh和Jacobson 1999；IEEE 2000；Garlan和Perry 1995）。对于任何产品，都有许多结构需要设计。每种结构都必须单独设计，这样它就表现为一个独立的关注点。在接下来的几节中我们会讨论一些结构，你可以利用它们来考虑前面列表中的关注点。例如，“信息隐藏结构”展示了如何将系统组织成一些工作指派。这种结构也可以用作改变的路线图，展示了建议的改变，以及哪些模块支持这些改变。针对每种结构，我们描述了一些组件及其之间的关系，正是这些组件和关系确定了这种结构。对照前面的列表，我们认为下面的结构是最重要的。

1.3.1　信息隐藏结构

组件与关系：主要组件是一些“信息隐藏模块”，每个模块都是针对一组开发人员的工作指派，每个模块都包含了一种设计决定。如果一项决定可以改变，同时又不影响任何其他模块，我们就说这项设计决定是一个模块的秘密（Hoffman和Weiss 2000，第7章和第16章）。模块间最基本的关系是“整体-部分”关系。如果“信息隐藏模块A”的秘密是“信息隐藏模块B”的秘密的一部分，那么A就是B的一部分。请注意，必须能够在改变A的秘密的同时，不改变B的其他部分。否则，根据我们的定义，A就不是B的一个子模块。例如，许多架构都有一些虚拟设备模块，它们的秘密是如何与特定的物理设备通信。如果虚拟设备分成不同类型，那么每种类型可能构成该虚拟设备模块的一个子模块，其中每种虚拟设备类型的秘密将是如何与这种类型的设备进行通信。

每个模块都是一份工作指派，包含了一组要写的程序。根据不同的语言、平台、环境，“程序”可以是能在计算机上执行的方法、过程、函数、子程序、脚本、宏或其他指令序列。第二种信息隐藏模块结构是基于程序和模块之间的“包含”关系。如果模块M的一部分工作指派是要编写程序P，那么M就包含P。请注意，每个程序都包含在一个模块中，因为每个程序必然是某些开发人员的工作指派的一部分。

这些程序中的一些可以通过模块的接口来访问，而另一些则是内部的。模块也可能通过接口发生关系。A模块的接口是一组假定，这些假定包括该模块之外的程序可以对该模块做出的假定，也包括该模块中的程序对其他模块的程序和数据结构所做的假定。如果改变B的接口就要求A也发生改变，那么我们就说A“依赖”B的接口。

“整体-部分”结构是层次状的。在这个层次结构的叶节点上的模块不包含可识别的子模块。“包含”结构也是层次状的，因为每个程序都只包含在一个模块之中。“依赖”关系不一定是层次状的，因为两个模块可能互相依赖，要么是直接互相依赖，要么是通过一个较长的“依赖”关系形成的环。请注意，“依赖”不应该与后面小节中定义的“使用”混淆起来。

信息隐藏结构是面向对象设计方法的基础。如果一个信息隐藏模块设计为一个类，这个类的公有方法就属于该模块的接口。

满足的关注点：信息隐藏结构的设计应该能满足可变性、模块化和可构建性的要求。

1.3.2　使用结构

组件与关系：根据前面我们的定义，信息隐藏模块包含一个或多个程序（在上一小节中定义）。当且仅当两个程序共享一个秘密时，它们才属于同一个模块。“使用结构”（Uses Structure）的组件是一些可以单独调用的程序。请注意，程序可以相互调用，或被硬件调用（例如，被一个中断例程调用），调用也可能来自于不同命名空间的程序，如操作系统例程或远程过程。而且，调用发生的时间可以是任何时候，从编译时到运行时。

只有在相同绑定时间操作的程序之间，我们才考虑形成一种使用结构。首先只考虑运行时操作的程序可能最容易。以后，我们也可以考虑那些编译时或载入时操作的程序之间的使用关系。

如果程序B必须存在并满足其规格说明，程序A才能满足其规格说明，我们就说A使用了B。换言之，B必须存在且操作正常，A才能操作正常。使用关系有时候也称为“要求存在正确的版本”。进一步的解释和例子，参见（Hoffman和Weiss 2000）的第14章。

使用结构确定了我们可以构建并测试怎样的工作子集。在软件系统的使用结构中，期望的属性是它定义了一种层次结构，这意味着其中不出现环。如果在使用关系中出现环，那么环中所有程序都必须存在且正常工作，才能让其他的程序正常工作。由于也许不能够创建完全没有环的使用关系，架构师可能将使用环中的所有程序作为单一的程序，以这种方法来创建子集。子集必须要么包含全部程序，要么都不包含。

如果在使用关系中没有环，软件采用的就是一种层次结构。在最底层，即第0层，是所有不使用其他程序的程序。第n层包含了所有的程序，它们使用了第n-1层或以下层的程序。这些层常常描绘为一系列的层次，每个层次表示了使用关系中的一个或几个层。在使用结构中对相邻的层分组，有助于简化表示，并允许在关系中出现小环的情况。进行这种分组有一个指导原则，即一个层次中的程序应该比它上一个层次中的程序执行速度快9倍，执行频率高9倍（Courtois 1977）。

具有层次使用结构的系统可以同时构造一层或几层。这些层次有时候称为“抽象层”，但这是一种错误的名称。因为这些组件是独立的程序，而不是完整的模块，它们不一定抽象（隐藏）了什么东西。

通常大型的软件系统包含太多的程序，这让程序间使用关系的描述不太容易理解。在这种情况下，使用关系可以用于程序的组合，如模块、类或包。这样的组合描述丧失了重要的信息，但有助于展示“全局”。例如，你有时候可以在信息隐藏模块之间建立使用关系，但是除非一个模块中所有的程序都属于实际使用层次的同一层，否则就会丧失重要的信息。

在某些项目中，系统的使用关系开始并没有完全确定，要到系统实现时才能确定，因为开发者会在实现过程中决定他们使用哪些程序。但是，系统的架构师可能在设计时创建一种“允许使用”关系，约束开发者的选择。今后，我们不会区分“使用”和“允许使用”。定义良好的使用结构将创建系统的适当子集，可以用于驱动迭代式或增量式的开发循环。满足的关注点：产品化和生态系统。

1.3.3　进程结构

组件与关系：信息隐藏模块结构和使用结构是静态的结构，存在于设计时和编码时。我们现在转向运行时结构。参与进程结构的组件是进程。进程是运行时的事件序列，由程序控制（Dijkstra 1968）。每个程序都作为一个或多个进程的一部分执行。一个进程中的事件序列的执行独立于另一进程中的事件序列，除非这两个进程彼此同步，例如一个进程等待来自另一个进程的信号或消息。进程由支持系统分配资源，包括内存和处理器时间。系统可能包含固定数量的进程，也可能在运行时创建和销毁进程。请注意，在Linux和Windows操作系统中实现的线程也符合这个进程定义。进程是几种不同关系中的组件。下面是一些例子。

进程提供工作

一个进程可能会创建工作，该项工作必须由其他进程完成。这种结构在确定系统是否死锁时是很重要的。

满足的关注点：性能和容量。

进程取得资源

在动态分配资源的系统中，一个进程可能控制由另一个进程使用的资源，后者必须请求并归还这些资源。因为发起请求的进程可能从几个控制器那里请求资源，所以每项资源可能都有一个不同的控制进程。

满足的关注点：性能和容量。

进程共享资源

两个进程可能共享资源，如打印机、内存或端口等。如果两个进程共享一项资源，就需要通过同步来防止使用冲突。每一种资源可能有不同的关系。

满足的关注点：性能和容量。

进程包含在模块中

每个进程由一个程序控制，正如前面提到的，每个程序包含在一个模块之中。因此，我们可以认为进程包含在模块之中。

满足的关注点：性能和容量。

1.3.4　访问结构

系统中的数据可能划分成具有属性的段，如果程序对段中的任何数据拥有访问权，就对该段中的所有数据拥有了访问权。请注意，为了简化描述，我们应该让段的规模最大化，具体做法是添加一个条件，即如果两个段被同一组程序访问，这两个段就应该合并。数据访问结构包含两种类型的组件：程序和段。这种关系被命名“有权访问”，它是程序和数据段之间的关系。如果这种结构让程序访问的权限最小化，并且严格执行，我们就认为系统更安全。

满足的关注点：安全性。

1.3.5　结构小结

表1-1总结了前面的软件结构，包括它们的定义和它们满足的关注点。

1.4　好的架构

我们曾提到，架构师玩的是折中的游戏。对于一组给定的功能需求和品质需求，没有唯一的正确架构和唯一的“正确答案”。我们从经验中得知，应该对架构进行评估，确定它是否满足其需求，然后再投入资金来构建、测试和部署这个系统。评估试图回答前面小节中讨论的一个或多个一般关注点问题，或回答特定系统的具体关注问题。

架构评估有两种常见的方式（Clements、Kazman和Klein 2002）。第一种评估方式是确定架构的属性，通常通过建模或模拟系统的一个或多个方面。例如，通过性能建模来评估吞吐量和伸缩性，通过失效树模型来评估可靠性和可访问性。其他类型的模型包括复杂性和耦合指标，用于评估可变性和可维护性。

第二种评估方式，也是最广泛使用的方式，就是通过对架构师提出质询来评估该架构。有许多结构化的质询方法。例如，贝尔实验室提出的软件架构复查委员会（Software Architecture Review Board,SARB）过程利用了组织机构之内的专家，以及他们在电信和相关应用中的深厚领域经验（Maranzano等2005）。

质询方法的另一种变体是架构折中分析方法（Architecture Trade-off Analysis Method,ATAM）（Clements、Kazman和Klein 2002），它寻找架构不能满足品质关注点的风险。ATAM使用了场景分析，每种场景都描述了特定的利益相关人对系统的品质关注点。架构师然后解释该架构如何支持每一种场景。

主动复审是另一种质询方法，它改变了复审过程的开始方式，要求架构师向复审者提供架构师认为重要而需要回答的问题（Hoffman和Weiss 2000，第17章）。然后，复查者利用已有的架构文档和描述来回答这些问题。最后，在网络上查找“software architecture review checklist（软件架构复审检查清单）”，可以得到几十份检查清单，其中某些清单非常通用，另一些则是针对具体的应用领域或技术框架。

1.5　美丽的架构

所有前面的方法都有助于我们判断一个架构是否“足够好”—也就是说，是否有可能指导开发者和测试者构建一个系统，并满足系统的利益相关人的功能和质量关注点。在我们每天使用的系统中存在着许多好的架构。

但是，超越足够好的架构是怎样的呢？如果有一个“软件架构名人堂”，那会怎样？哪些架构会陈列在这个艺术馆的墙上？这个想法可能没有你想象的那么遥远—在软件产品线领域，这样的“名人堂”的确存在。[1]进入“软件产品线名人堂”的条件包括获得商业上的成功、影响其他产品线的架构（其他产品线可能“借用、复制、窃取”这个架构）、拥有足够的文档从而让其他人“不必通过道听途说”就能够理解该架构。

我们将为更一般的“架构名人堂”或“美丽架构艺术馆”的候选者设立怎样的条件呢？首先我们应该认识到，这是一个软件系统的艺术馆，而不是其他艺术馆，我们的系统构建的目的是为了使用。所以，我们也许从一开始就应该关注该架构的实用性：它应该每天被许多人使用。

但是，在使用架构之前，它必须先构建，所以我们应该关注该架构的可构建性。我们会寻找那些具有定义良好的使用结构的架构，它们支持增量式构建，这样，通过每次构建迭代我们都能得到一个有用的、可测试的系统。我们也会寻找那些具有定义良好的模块接口、本来就很好测试的架构，这样，构建的过程就是透明的、可见的。

接来下，我们想寻找那些展示了持久性的架构—也就是说，那些经过了时间考验的架构。我们生活在一个技术环境以从未有过的加速度变化的年代。美丽的架构应该预期到变更的需要，允许期望的修改能够容易而有效地进行。我们想寻找那些避免了“衰老地平线”（Klein 2005）的架构，超过了这条“衰老地平线”，维护将变得代价极大，以至于不可能进行。

最后，我们还想寻找这样一些架构，它们的特征让使用、构建、测试这些架构的开发人员和测试人员，以及由它而形成的系统的用户感到由衷的高兴。为什么开发人员会高兴？因为它让他们的工作变得容易，而且更有可能得到一个高品质的系统。为什么测试人员会高兴？作为测试过程的一部分，他们必须尝试模拟用户的行为。如果他们高兴，可能用户也会感到高兴。如果厨师对他的烹调的菜品感到不高兴，那么品尝这些菜品的顾客也可能会感到不高兴。

不同的系统和应用领域为这些架构提供了许多机会，展示它们特有的令人高兴的特征，但概念完整性是一项跨越所有领域的特征，并且总是让人感到高兴。一致的架构学习起来更容易、更快，当知道了一点之后，你就可以开始预测其余的部分。不需要记住并处理特殊的情况，代码更干净，测试集更小。一致的架构不会为做同一件事情提供两种（或更多）方法，不会让用户浪费时间进行选择。正如Ludwig Mies van der Rohe所说的，好的设计，“少即是多”。爱因斯坦可能会说，美丽的架构就是尽可能简单，但不要过于简单。

有了这些判别条件，我们推荐第一批进入“美丽架构艺术馆”的候选者。

第一个是A-7E舰载飞行处理器（Onboard Flight Processor,OFP）的架构，它由海军研究实验室（Naval Research Laboratory,NRL）在20世纪70年代后期开发，在（Bass、Clements和Kazman 2003）有介绍。尽管这个系统从未实现量产，但它满足了所有其他的判别条件。这个架构对软件架构的实践曾经产生了巨大的影响，它展示在真实世界的系统中，将设计时的信息隐藏模块和使用结构与运行时的进程结构分离。因为美国政府资助并开发了这个架构，所以所有项目文档都提供给了公共领域。[2]这个架构具有定义良好的使用结构，促进了系统的增量式构建。最后，信息隐藏模块结构为分解系统提供了清晰一致的准则，实现了很强的概念完整性。作为嵌入式系统软件架构的榜样，A-7E OFP当然属于我们的艺术馆。我们想放入艺术馆的另一个架构是朗讯5ESS电话交换机的软件架构（Carney等1985）。5ESS取得了全球范围的商业成功，为世界各国的网络提供了核心电话网络交换。它成为性能和可靠性的标准，每个单元每小时能处理超过100万次的连接，平均每年非计划宕机时间少于10秒钟（Alcatel-Lucent 1999）。该架构的一些统一概念，如管理电话连接的“半通话模型”，已经成为电话和网络协议领域的标准模式（Hanmer 2001）。除了保持必须处理的通话类型的数目为2n（其中n是通话协议的数目）之外，半通话模式还在操作系统的进程概念和电话的通话类型概念之间建立起了联系，从而提供了简单的设计原则，引入了漂亮的架构一致性。在过去的25年中，开发团队涉及多达3000个人，他们发展并增强该系统。基于它的商业成功、持久性和影响，5ESS架构是我们艺术馆的一件好藏品。还有一个我们想放入美丽架构艺术馆的系统，它就是万维网（World Wide Web,WWW）的架构。它由Tim Berners-Lee在CERN创建，在（Bass、Clements和Kazman 2003）中有介绍。万维网当然已经取得了商业上的成功，它转变了人们使用因特网的方式。即使创建了新的应用、引入了新的功能，它的架构仍然保持不变。该架构的整体简单性促成了它的概念完整性，但有一些决定导致了该架构的完整性保持不变，如客户端和服务器端使用同一个库，创建分层架构以实现分离关注点等。核心万维网架构的持久性和它对新扩展、新功能持续支持的能力，使它当之无愧地进入了我们的艺术馆。

什么是建筑师？

夏天很热的一个日子里，一个外乡人沿着一条路在行走。他走着走着，来到一个人跟前，此人正在路边敲碎石头。

“你在做什么？”他问那个人。

那个人抬头看着他；“我在敲碎石头。你以为我看起来像在干什么？现在不要妨碍我，让我继续干活。”

这个外乡人继续沿着路走，不久他遇到了第二个在大太阳下敲碎石头的人。这个人正努力工作，汗滴如雨。

“你在做什么？”外乡人问道。这个人抬头看他，露出微笑。

“我在为谋生而工作，”他说，“但这个工作太辛苦了。也许你能给我一份更好的工作？”

外乡人摇了摇头，继续前行。没多久，他遇到了第三个敲碎石头的人。太阳正是最炙热的时候，这个人非常卖力，汗流如注。

“你在做什么？”外乡人问道。

这个人停了一下，喝了一口水，微笑着抬起他的手，指向天空。“我在建一座大教堂。”他喘着气说。

外乡人看了他一会儿，说：“我们正打算开一家新公司。你来做我们的总建筑师怎么样？”

我们的最后一个例子是UNIX系统，它展示了概念完整性，使用广泛，拥有巨大的影响力。管道和过滤器的设计是讨人喜欢的抽象，允许我们快速构建新的应用。

在描述架构、架构师的角色和创建架构时的考虑等方面，我们已经谈了很多，我们也简单介绍了一些美丽架构的例子。接下来我们邀请你阅读后续章节中详细的例子，这些例子来自于那些技艺精湛的架构师，本书介绍了他们创建并使用过的那些美丽架构。

[1]参见http://www.sei.cmu.edu/productlines/plp_hof.html.

[2]参见CHoffman和Weiss2000）的第6章、第15章和第16章，或在NRL Digital Archives（http://torpedo.nrl.navy.mil/tu/ps）中查找“A-7E”。

致谢

David Parnas在几篇论文中定义了我们描述的许多结构，其中包括他的“术语滥用”论文（Parnas 1974）。Jon Bentley为这本书提供了创作灵感，他和Deborah Hill、Mark Klein对早期的草稿提出了许多有价值的建议。

参考文献

Alcatel-Lucent.1999.“Lucent's record-breaking reliability continues to lead the industry according to latest quality report.”Alcatel-Lucent Press Releases.June 2.http://www.alcatel-lucent.com/wps/portal/News Releases/DetailLucent?LMSG_CABINET=Docs_and_Resource_Ctr＆LMSG_CONTENT_FILE=News_Releases_LU_1999/LU_News_Article_007318.xml（accessedMay15，2008）.Bass,L.，P.Clements,and R.Kazman.2003.Software Architecture in Practice,Second Edition.Boston,MA：Addison-Wesley.

Blaauw,G.，and F.Brooks.1997.Computer Architecture：Concepts and Evolution.Boston,MA：Addison-Wesley.

Booch,G.，J.Rumbaugh,and I.Jacobson.1999.The UML Modeling Language User Guide.Boston,MA：Addison-Wesley.

Brooks,F.1995.The Mythical Man-Month.Boston,MA：Addison-Wesley.

Carney,D.L.，et al.1985.“The 5ESS switching system：Architectural overview.”AT＆T Technical Journal,vol.64，no.6，p.1339.

Clements,P.，et al.2003.Documenting Software Architectures：Views and Beyond.Boston,MA：Addison-Wesley.

Clements,P.，R.Kazman,and M.Klein.2002.Evaluating Software Architectures.Boston：Addison-Wesley.

Conway,M.1968.“How do committees invent.”Datamation,vol.14，no.4.

Courtois,P.J.1977.Decomposability：Queuing and Computer Systems.New York,NY：Academic Press.

Dijkstra,E.W.1968.“Co-operating sequential processes.”Programming Languages.Ed.F.Genuys.New York,NY：Academic Press.

Garlan,D.，and D.Perry.1995.“Introduction to the special issue on software architecture.”IEEE Transactions on Software Engineering,vol.21，no.4.

Grinter,R.E.1999.“Systems architecture：Product designing and social engineering.”Proceedings of ACM Conference on Work Activities Coordination and Collaboration（WACC'99）.11-18.San Francisco,CA.

Hanmer,R.2001.“Call processing.”Pattern Languages of Programming（PLoP）.Monticello,IL.http://hillside.net/plop/plop2001/accepted_submissions/PLoP2001/rhanmer0/PLoP2001_rhanmer0_1.pdf.

Hoffman,D.，and D. Weiss.2000.Software Fundamentals：Collected Papers by David L.Parnas.Boston,MA：Addison-Wesley.

IEEE. 2000.“Recommended practice for architectural description of software intensive systems.”Std 1471.Los Alamitos,CA：IEEE.

Klein,John.2005.“How does the architect's role change as the software ages?”Proceedings of the 5th Working IEEE/IFIP Conference on Software Architecture（WICSA）.Washington,DC：IEEE Computer Society.

Maranzano,J.，et al. 2005.“Architecture reviews：Practice and experience.”IEEE Software,March/April 2005.

Parnas,David L.1974.“On a buzzword：Hierarchical structure.”Proceedings of IFIP Congress.Amsterdam,North Holland.[Reprinted as Chapter 9 in Hoffman and Weiss（2000）.]Waldo,J.2006.“On system design.”OOPLSA'06.October 22-26.Portland,OR.

Weiss,D.，and C.T.R.Lai.1999.Software Product Line Engineering.Boston,MA：Addison-Wesley.

第2章　两个系统的故事：现代软件神话

Pete Goodliffe

架构是一种很浪费空间的艺术。

——Philip Johnson

软件系统就像一座由建筑和后面的路构成的城市—由公路和旅馆构成的错综复杂的网络。在繁忙的城市里发生着许多事情，控制流不断产生，它们的生命在城市中交织在一起，然后死亡。丰富的数据积聚在一起、存储起来，然后销毁。有各式各样的建筑：有的高大美丽，有的低矮实用，还有的坍塌破损。随着数据围绕着它们流动，形成了交通堵塞和追尾、高峰时段和道路维护。软件之城的品质直接与其中包含多少城市规划有关。某些软件系统很幸运，创建时由有经验的架构师进行了深思熟虑的设计，在构建时体现出了优雅和平衡，有很好的地图，便于导航。另一些软件系统就没有这么幸运，基本上是一些通过偶然聚集的代码渐渐形成的，交通基础设施不足，建筑单调而平凡，置身于其中时会完全迷失，找不着路。

你的代码愿意待在怎样的“城市”中？你愿意构建哪一种城市？

在本章中，我将讲述这样两个软件城市的故事。这是真实的故事，就像所有好的故事一样，这个故事最终是有教育意义的。人们说经验是伟大的老师，但最好是别人的经验，如果你能从这些项目的错误和成功之中学习，你（和你的软件）可能会避免很多的痛苦。本章中的这两个系统特别有趣，因为它们有很大不同，尽管从表面上看非常相似：

·它们具有相似的规模（大约500000行代码）。

·它们都是“嵌入式”消费音频设备。

·每种软件的生态系统都是成熟的，已经经历了许多的产品版本。

·两种解决方案都是基于Linux的。

·编码采用C++语言。

·它们都是由“有经验的”程序员开发的（在某些情况下，他们本应知道得更多）。

·程序员本身就是架构师。

在这个故事中，人名都已改变，目的是保护那些无辜的人（和有罪的人）。

2.1　混乱大都市

你们修筑、修筑，预备道路，将绊脚石从我百姓的路中除掉。

——《以赛亚书》第57章14节

我们要看的第一个软件系统名为“混乱大都市”。它是我喜欢回顾的一个系统—既不是因为它很好，也不是因为它让参与开发的人感到舒服，而是因为当我第一次参与它的开发时，它教给了我有价值的软件开发经验。

我第一次接触“混乱大都市”，是在我加入了创建它的公司时。初看上去这是一份有前途的工作。我将加入一个团队，参与基于Linux的、“现代”的C++代码集开发，已有的代码集已经开发几年了。如果你像我一样拥有特殊的技术崇拜，就会觉得很兴奋。

工作起初并不顺利，但是你不能指望在加入一个新团队、面对新的代码集时会觉得很轻松。然而，日复一日（周复一周），情况却没有任何好转。这些代码要花极长的时间来学习，没有显而易见的进入系统中的路径。这是个警告信号。从微观的层面来说，也就是从每行程序、每个方法、每个组件来看，代码都是混乱而粗糙地垒在一起的。不存在一致性、不存在风格、也没有统一的概念能够将不同的部分组织在一起。这是另一个警告信号。系统中的控制流让人觉得不舒服，无法预测。这又是一个警告信号。系统中有太多的“坏味道”（Fowler 1999），整个代码集散发着腐烂的气味，是在大热天里散发着刺激性气体的一个垃圾堆。这是一个清晰的警告信号。数据很少放在使用它的地方。经常引入额外的巴罗克式缓存层，目的是试图让数据停留在更方便的地方。这又是一个警告信号。

当我试图在大脑中建立“大都市”的全图时，没有人能解释它的结构；没有人知道它的所有层、它的藤蔓，以及那些黑暗、隔离的角落。实际上，没有人知道它究竟有多少部分是真正能工作的（它实际上靠的是运气和英雄式的维护程序员）。人们知道他们面对的那一小部分区域，但没人了解整个系统。很自然，没有任何文档。这也是一个警告信号。我需要的是一份地图。

这是一个悲伤的故事，我曾是其中的一部分：“大都市”是城市规划的恶梦。在你开始整治混乱之前，先要理解混乱，所以我们花了很大的精力和毅力，得到了一份“架构图”。我们标出了每一条公路、每一条主干道、每一条很少人了解的小路、所有灯光昏暗的辅路，并将它们画在一张主图上。我们第一次看到了这个软件的样子，并不令人赏心悦目。它是一些混乱的区块和线条。为了让它更好理解一些，我们用颜色标出了控制路径，突出了它们的类型。然后我们后退一步看着它。

它令人吃惊。它令人目眩神迷。它就像一只喝醉了的蜘蛛，跌进了一些海报颜料罐里，然后在一张纸上织成了一张彩色的网。它看起来就像图2-1那样（这是一个简化后的版本，细节已经修改了，为了保护那些有罪的人）。事情变得很清楚了。我们画出了伦敦地铁图。它甚至有环线。

图　2-1：“混乱大都市”的“架构”

这就是那种让跑遍各地的销售员恼怒的系统。实际上，它与伦敦地铁的相似性让人印象深刻：从系统的一端到另一端有很多条路线，哪条路最好通常是不明显的。地理位置很近的目的地常常很难到达，你希望能在两点之间再挖掘一条隧道。有时候，走出地铁换乘巴士实际上是更好的选择。或者干脆步行。

无论从哪个角度来看，这都不是一个“好的”架构。“大都市”的问题超出了设计的范畴，它涉及开发过程和企业文化。这些问题实际上导致了许多架构腐烂。代码经过几年的“有机”生长，没有人进行过任何架构设计，而且各个部分是随着时间推移，没有经过太多思考就栓在一起的。我们这么说真的算是客气的了。没有人停下来为代码建立一个理智的结构。它增长、膨胀，成为绝对没有任何架构设计的系统的一个典型。代码集从来不会没有架构。这个系统只是拥有一个很糟糕的架构。

如果我们回顾创建“大都市”的公司的历史，它所处的状态是可以理解的（但是不可宽恕）：这是一个初创的公司，快速提供许多新版本的压力很大。延期是不可容忍的—这会带来财务灾难。软件工程师被迫尽其极限，快速交付。所以代码是以一系列疯狂冲刺的方式垒在一起的。

注意：不好的公司结构和不健康的开发过程将在糟糕的软件架构中得到反映。

2.1.1　后果

“大都市”缺少城市规划，这带来了许多后果，我们将在这里进行分析。这些后果的影响是很严重的，远远超出了你对不良设计的天真想象。地铁变成了云宵飞车，飞速地朝下猛冲。

不可理解

正如你已经看到的，“大都市”的架构以及缺乏强制的结构，导致了一个很难理解的软件系统，实际上几乎不可能修改。新加入项目的团队成员（譬如我）会被复杂性惊呆，不能够搞清楚状况。

坏的设计确实会招致在它上面叠加坏的设计（实际上它简直就是迫使你那样做），因为没有一种明智的方式可以扩展该设计。在所有能解决手上工作的方法之中，阻力最小的总会被采用，没有明显的办法来修复这些结构问题，所以只要能减少麻烦，就会扔进去新的功能。

注意：重要的是要保持软件设计的品质。坏的架构设计会招致更坏的架构设计。

缺乏内聚

系统的组件完全没有内聚性。每个组件本来都应该有一个定义良好的角色，但是它们却包含了一堆杂乱的、不一定相关的功能。这使我们很难确定组件存在的原因，也很难弄明白系统中已经实现了哪项具体的功能。

很自然，这让缺陷修复成为了一场噩梦，严重地影响了软件的品质和可靠性。

功能和数据都放在了系统中错误的地方。许多你认为是“核心服务”的部分却没有在系统的核心部分实现，而是由边远的模块来模拟实现（非常痛苦并且代价很大）。

进一步的软件历史考察揭示了原因：原来的团队中存在个人斗争，所以一些关键程序员开始创建他们自己的软件小帝国。他们把自己认为酷的功能放到他们的模块中，即使它不应该属于那里。为了做到这一点，他们于是又实现了更为巴罗克式的通信机制，把控制连回到正确的地方。

注意：开发团队中健康的工作关系将直接有益于软件设计。不健康的关系和个性膨胀会导致不健康的软件。

内聚和耦合

软件设计的关键品质是内聚和耦合。这不是什么新奇的“面向对象”概念；自从20世纪70年代出现结构化设计开始，开发者对这一概念已经谈论了许多年。我们的目标是通过设计使系统的组件具备下列品质：

·高内聚（Strong cohesion）

内聚是一个测量指标，说明相关的功能如何聚集在一起，模块内的各部分作为一个整体工作得如何。内聚性是将模块粘成一个整体的胶水。

弱内聚的模块是不良分解的信号。每个模块都必须具有清晰定义的角色，而不只是一堆不相关的功能。

·低耦合（Low coupling）

耦合是模块之间独立性的测量指标—它们之间进出“电线”的数量。在最简单的设计中，模块几乎没有什么耦合，所以彼此间的依赖关系较少。显然，模块不能够完全解耦，否则它们将根本不能够一起工作！

模块之间的联系有多种方式，有的是直接的，有的是间接的。模块可以调用其他模块中的函数，或被其他模块所调用。它可能使用其他模块提供的Web服务或设施，可能使用其他模块的数据类型，或提供某些数据让其他模块使用（可能是变量或文件）。

好的软件设计会限制通信的线路，只提供那些绝对需要的。这种通信线路是确定架构的一部分因素。

不必要的耦合

“大都市”没有清晰的分层。模块之间的依赖关系不是单向的，耦合常常是双向的。组件A会到达组件B的内部，目的是完成它的一项任务。在其他的地方，组件B又通过硬编码调用了组件A。系统没有最底层，也没有控制中心。它是整体式的一大块软件。

这意味着系统的各部分之间耦合非常紧密，你想启动系统骨架就不得不创建所有的组件。单个组件的任何改变都会波及其他组件，需要修改许多依赖它的组件。孤立地看代码组件没有任何意义。

这使得低层次的测试不能够进行。不仅是代码层次的测试不可能进行，而且组件层次的集成测试也不能够创建，因为每个组件都依赖于几乎所有其他组件。当然，在公司中，测试从来也不具有很高的优先级（我们根本没有时间来做这种测试），所以这“不成为问题”。不必说，这个软件不太可靠。

注意：好的设计考虑到内部组件连接的连接机制和连接数（以及连接性质）。系统的单个部分应该能够独立存在。紧耦合将导致不可测试的代码。

代码问题

不良的顶层设计所带来的问题也影响到了代码层面。问题会引起其他问题（参见Hunt和Davis[1999]中关于破窗理论的讨论）。因为没有通用的设计，也没有整体项目“风格”，所以也没有人关心共同的编码标准、使用共同的库，或采用共同的惯例。组件、类和文件都没有命名惯例。甚至都没有共同的构建系统。胶带、Shell脚本、Perl胶水与makefile和Visual Studio项目文件混在一起。编译这个怪物被视为一场复杂的成人仪式！“大都市”最微妙而又最严重的问题是重复。由于没有清晰的设计，也不清楚功能应该处于的位置，所以轮子在整个代码集中不断重新发明。一些简单的东西，如通用算法和数据结构，在许多模块中重复出现，每种实现都带有自己的一些未知的缺陷和怪异的行为特征。更大范围的关注点，如外部通信和数据缓存，也实现了许多次。

更多的软件历史考察揭示了原因：“大都市”开始是从一系列独立的原型拼起来的，这些原型本该抛弃。“大都市”实际上是偶然形成的城市群。当代码组件缝合在一起时，组件之间匹配得不好。随着时间的推移，这种随意的缝合开始破裂，所以组件互相拉扯，导致代码集破碎，而不是和谐地协作。

注意：松弛而模糊的架构将导致每个代码组件编写得不好，并且相互之间匹配得不好。它也会导致重复的代码和工作。

代码以外的问题

“大都市”内部的问题已经超越了代码集，在公司中其他的地方导致了混乱。不仅开发团队中有问题，而且架构的腐烂也影响到了支持和使用该产品的人。

开发团队

项目的新成员（例如我）被复杂性惊呆了，不能够搞清楚状况。这很好地解释了为什么很少有新人能在公司里待下来—员工流失率非常高。

那些留下来的人非常努力地工作，项目的压力非常大。规划新的功能会导致极大的恐惧。

缓慢的开发周期

由于维护“大都市”是一项恐怖的任务，所以即使是最简单的变更或“很小的”缺陷修复都不知道要花多少时间。管理软件开发周期非常难。客户只好等着实现重要的特征，管理层对开发团队不能满足业务目标感到越来越沮丧。

支持工程师

在支持这个极不寻常的产品时，产品支持工程师度过了可怕的时光，他们要设法弄明白很小的软件版本差异之间错综复杂的行为差异。

第三方支持

项目开发了一个外部支持协议，支持其他设备远程控制“大都市”。由于它只是软件内部结构上面薄薄的一层，所以它反映了“大都市”的架构，这意味着它也是巴罗克式的、难以理解的、容易偶尔出错的、不可能使用的。第三方工程师的生活也被“大都市”的可怕结构搞得一团糟。

公司内部政治

开发问题导致了公司内部不同“种族”的分裂。开发团队与营销销售团队之间关系紧张，每次新版本要推出时，制造部门总是要承受巨大的压力。经理们已经绝望了。

注意：不良架构的影响不仅限于代码。它会进一步影响到人、团队、过程和时间表。

清晰的需求

软件历史考察凸显了“混乱大都市”之所以混乱的一个重要原因：在项目开始之初，团队并不知道要构建的是什么。

本来的初创公司知道它要占领哪个市场，但不知道哪种产品能占领这个市场。所以他们两面下注，要求一个可以做许多事情的软件平台。噢，我们昨天就想得到它了。所以程序员们急急忙忙创建了一个毫无希望的总体基础设施，它具有做任何事情的潜力（但做得不好），而不是创建一个把一件事情做好的架构，并能够在将来进行扩展，做更多的事情。

注意：重要的是要在开始设计系统之前知道你打算设计什么。如果你不知道它是什么，也不知道它将做什么，暂时不要开始设计它。只设计你知道需要的东西。

在规划“大都市”的早期阶段，有太多的架构师。面对糊涂的需求，他们都拿着一块拼不起来的拼图，试图独自工作。他们在工作时没有考虑到整个项目，所以当他们试图将这些拼图拼在一起时，就拼不起来了。没有时间进一步思考架构，软件设计的各个部分有一些重叠，于是开始了“大都市”的城市规划灾难。

2.1.2　现状

“大都市”的设计几乎完全是无可救药的—相信我，随着时间的推移，我们也尝试过修复它。修复工作需要返工、重构、修改代码结构中的问题，这些已经成为不可能的任务。重写也不是省事的方案，因为支持老的、巴罗克式的控制协议是需求的一部分。你可以看到，“大都市”的“设计”产生的后果是残酷的，并且会无情地变得更糟。很难加入新的特性，所以人们只是忙着添加更多不完善的功能、救急补丁和编造的谎言。没有人在面对代码时感到愉快，项目正盘旋着向下栽。缺乏设计导致了不良的代码，从而又导致了不良的团队精神和不断变长的开发周期。这最终导致了公司严重的财务问题。最后，管理层宣布“混乱大都市”已经不盈利了，它被抛弃了。对于任何组织机构来说，这都是勇敢的一步，特别是这个公司一直都眼高手低，同时又试图避免沉沦。带着团队从以前版本中得到的所有C++和Linux经验，他们在Windows上用C#重写了系统。猜猜看会怎么样。

2.1.3　来自“大都市”的名信片

那么我们学到了什么？不良的架构会产生深远的影响和严重的反弹。在“混乱大都市”中缺少预见性和架构设计，导致了下面的问题：

·低品质的软件和漫长的版本发布周期。

·系统没有弹性，不能够适应变更或添加新的功能。

·无处不在的代码问题。

·员工问题（压力大、士气低、跳槽等）。

·大量混乱的公司内部政治。

·公司不能成功。

·许多痛苦和面对代码深夜加班。

2.2　设计之城

形式永远服从功能。

——Louis Henry Sullivan

“设计之城”软件项目表面上与“混乱大都市”非常相似。它也是用C++写的消费音频产品，运行在Linux操作系统上。但是，它的构建方式有很大不同，所以内部结构也非常不同。

我从一开始就参加了“设计之城”项目。我们用有能力的开发者组成了一个全新的团队，从头开始构建这个产品。团队很小（开始有4名程序员），像“大都市”项目一样，团队的结构是扁平的。幸运的是，没有出现“大都市”项目中的个人对抗，在团队中也没有出现任何争权夺利的事。在此之前，团队成员之间并不非常熟悉，不知道我们在一起可以配合得多好，但我们对这个项目都很热心，喜欢这项挑战。

这样很好。

Linux和C++是项目早期的决定，这项决定确定了团队成员的组成。从一开始，项目就有清晰定义的目标：具体的首个产品和将来功能的路线图，代码集必须能够支持这些功能。这将是一个通用目标的代码集，可以适用于多种产品配置。

开发过程采用了极限编程（XP）（Beck和Andres 2004），很多人相信这种开发过程避开了设计：直接开始编码，不要想太远。实际上，一些旁观者对我们的选择感到震惊，并预言项目将以泪收场，就像“大都市”一样。但这是一种常见的误解。XP没有贬低设计，它贬低的是不必要的工作（即YAGNI原则，You Aren't Going To Need It）。但是，如果需要前端设计，XP就要求你进行设计。它也鼓励使用快速原型（所谓的“spike”），快速展现并验证设计的有效性。这些都非常有用，对最终的软件设计产生了极大的影响。

2.2.1　设计之城的第一步

在设计过程的早期，我们确定了主要的功能领域（这包括核心的音频通道、内容管理和用户控制/界面）。我们考虑了它们如何在系统中适配，推出了初步的架构，包括了实现性能需求所必需的核心线程模型。

系统中各独立部分的相对位置关系体现为传统的分层结构，图2-2展示了简化后的结果。请注意，这并不是庞大的前端设计。它是有意为之的“设计之城”的简单概念模型：图中只有一些大块，这是一个基本的系统设计，可以随着功能模块的添加而轻松地增长。虽然很基本，但这个初始架构为增长提供了坚实的基础。“大都市”没有总体规划，在“方便”的地方嫁接（或修补）功能。

我们在系统的核心上花了额外的设计时间：音频通道。它实际上是整个系统的一个内部子架构。为了确定它，我们考虑了穿过一系列组件的数据流，最后得到了一个“过滤器和管道”音频架构，如图2-3所示。根据产品的不同物理配置，它包含了这样一些管道。同样，开始时这些管道只是一个概念，即图中的一些方块。我们当时还没有决定如何将所有模块拼装在一起。

图　2-2：“设计之城”的初始架构

图　2-3：“设计之城”的音频管道

我们在早期也选择了项目将采用的支持库（例如，可以从http://www.boost.org获得的Boost C++库和一组支持数据库的库）。关于一些基本关注点的决定是这时候做出的，目的是确保代码能够容易而一致地增长，这些决定包括：

·我们如何对事物命名。

·顶层文件结构。

·“内部”展示的风格。

·共用的编码惯例。

·选择单元测试框架。

·支持基础设施（例如版本控制、适合的构建系统和持续集成）。

这些“细节”完美的因素非常重要：它们与软件架构密切相当，影响到后来的许多决定。

2.2.2　故事展开

在团队完成了初始设计之后，“设计之城”项目按照XP过程推进。设计和编码要么以结对的方式完成，要么经过仔细的复审，确保工作的正确性。

随着时间的推移，设计和代码不断发展和成熟；随着“设计之城”的故事逐渐展开，产生了下面的结果。

定位功能

由于从一开始我们就有系统结构的清晰总体视图，所以新的功能单元可以一致地添加到代码集的正确功能区域。代码应该属于哪一块从来就不是问题。在扩展功能或修复问题时，我们总是很容易找到已有功能的实现代码。

现在，把新的代码放到“正确”的位置有时候比简单“嫁接”到方便而不妥的地方而更难一些。所以，架构规划的存在有时候让开发者的工作变得更难一些。这些额外工作的回报就是今后的生活要容易很多，当我们维护或扩展系统时，不愉快的事情会很少。

注意：架构有助于定位功能：添加功能、修改功能或修复缺陷。它为你提供了一个模板，让你将工作纳入到一张系统导航图中。

一致性

整个系统是一致的。各个层次的所有决定都是在整个设计的背景下做出的。开发者从一开始就有意为之，这样得到的所有代码都完全符合系统设计，并与编写的所有其他代码相匹配。

在项目的历史中，尽管有许多变更，涉及代码集的各处（从单行代码到系统结构），但这些变更都符合最初的设计模板。

注意：清晰的架构设计将导致一致的系统。所有决定都应该在架构设计的背景下做出。

顶层设计的好风格和优雅很自然会为较低的层带来好处。即使在最低层，代码也是统一而整洁的。清晰定义的软件设计确保了没有重复，熟悉的设计模式到处使用，熟悉的接口惯例普遍采用，没有特殊的对象生命周期或奇怪的资源管理问题。代码是在城市规划的背景之中写成的。

注意：清晰的架构有助于减少功能重复。

架构的增长

有一些全新的功能领域出现在了设计“全图”中，例如存储管理和外部控制功能。在“大都市”项目中，这是致命的一击，难度超乎想象。但在“设计之城”项目中，事情就不一样了。

系统设计就像代码一样，被认为是可扩展、可重构的。开发团队的一项核心原则就是保持敏捷，没有什么是一成不变的，所以在需要时架构也可以修改。这促使我们让设计保持简单并易于修改。这样一来，代码可以快速地增长，同时又保持好的内部结构。添加新的功能块不是问题。

注意：软件架构不是一成不变的。需要时就改变它。要想做到可以修改，架构就必须保持简单。牺牲简单性的修改要抵制。

延迟设计决定

有一项XP原则确实提高了“设计之城”的架构品质，这就是YAGNI（如果你不是马上需要，就不要去做）。这促使我们在早期只设计了重要的部分，将所有余下的决定推迟，直到我们对实际的需求有了更清晰的理解并知道如何放到系统中最好时，再做出这些决定。这是一种非常强大的设计方法，在很大的程度上解放了我们的思想。

·当你还不理解问题时就开始设计，这是一件糟糕的事。YAGNI迫使你等待，直到你知道真正的问题是什么，它应该怎样由设计来体现为止。这消除了猜测的工作，确保设计是正确的。

·当你开始创建软件设计时就加入所有可能需要的东西（包括厨房水槽）是危险的。你的大部分设计工作会变成无用功，得到的只是额外的负担，你不得不在软件的整个变更生命周期中支持这些设计。它一开始就增加了成本，而且在项目的生命周期中不断增加成本。

注意：延迟设计决定，直到你必须做出这些决定为止。不要在你还不知道需求的时候就做出架构决定。不要猜测。

保持品质

从一开始，“设计之城”就准备好了一些品质控制过程：

·结对编程。

·对没有结对编程的工作进行代码/设计复审。

·对每一段代码进行单元测试。

这些过程确保了系统中从未加入不正确的、不合适的变更。所有不符合软件设计的内容都被拒之门外。这可能听起来有点过于严厉，但这些是开发者们坚信的过程。

这种信念凸显了一个重要的态度：开发者们相信设计，认为设计对项目相当重要。他们拥有设计，对设计负责。

注意：必须保持架构品质。只有当开发者们相信它并对它负责时，才能做到这一点。

管理技术债务

除了这些品质管理方法之外，“设计之城”的开发是相当注重实效的。随着最后期限的临近，一些不太重要的功能被砍掉，让产品能够准时推出。小的代码“瑕疵”或设计问题允许存在于代码集中，要么是为了让功能快一点实现，要么是为了在接近发布时避免高风险的改动。但是，与“混乱大都市”项目不同的是，这些逃避职责的地方被标记为技术债务，并安排在后续的版本发布中修正。这些问题很清楚，开发者对它们不满意，直到将它们处理掉为止。同样，我们看到了开发者对设计的品质负责。

单元测试打造了设计

关于代码集的一项核心决定就是所有代码都要有单元测试（这也是在XP开发中强制要求做到的）。单元测试带来了许多好处，其中一点就是能够修改软件的一些部分，而不必担心在修改的过程中破坏其他的东西。我们对“设计之城”内部结构的某些部分进行了相当激进的返工，单元测试给了我们信心，让我们相信系统的其他部分没有被破坏。例如，线程模型和音频管道的内部连接接口都进行了彻底的改变。这是在子系统开发较晚的时候发生的严重设计变更，但与音频通道接口的其他代码仍然执行得很好。单元测试让我们能够改变设计。随着“设计之城”的逐渐成熟，这种类型的“主要”设计变更越来越少了。在经过一些设计返工之后，情况稳定下来，此后只有一些不重要的设计变更。系统开发得很快：以迭代的方式进行，每一次迭代都改进了设计，直到它达到了相对稳定的状态。

注意：你的系统应该有一组不错的自动化测试，它们让你在进行根本的架构变更时风险最小。这为你提供了工作的空间。

单元测试的另一个主要好处在于，它们在很大程度上定型了代码设计：它们实际上迫使我们实现好的结构。每个小的代码组件都被定型成定义良好的实体，可以独立存在，因为它必须能够在单元测试中构造出来，不需要围绕它构造系统的其他部分。编写单元测试确保了每个代码模块的内聚性，也确保了与系统其他部分之间的松耦合。单元测试迫使我们仔细考虑每个单元的接口，确保该单元的API是有意义的，内部是一致的。

注意：对你的代码进行单元测试将带来更好的软件设计，所以设计时要考虑可测试性。

设计时间

“设计之城”成功的另一个因素是分配的开发时间段，它既不长也不短（就像金发歌蒂的粥，既不热也不冷，刚刚好）。项目需要一个有利的环境才能获得成功。

如果时间太多，程序员常常会想创建他们的巨作（那种总是快要好了，但永远不会实现的东西）。有一点压力是好事，紧迫感有助于完成事情。但是，如果时间太少，就不可能得到任何有价值的设计，你只会得到半生不熟的解决方案，就像“大都市”那样。

注意：好的项目计划将带来优质的设计。分配足够的时间来创建架构杰作，它们不会立即出现。

与设计同行

尽管代码集很大，但它是一致而易于理解的。新的程序员可以比较容易地拿起代码并开始工作。不需要去理解不必要的复杂内部关系，也不需要面对奇怪的遗留代码。

由于代码中产生的问题比较少，工作起来有乐趣，所以团队人员的流失率很低。这是因为开发者们负责设计，并不断希望改进它。

看着开发团队动态地遵守架构设计是一件有趣的事情。“设计之城”的项目原则规定没有人“拥有”哪一部分设计，这意味着任何开发者都可以改动系统的所有地方。每个人都应该写出高品质的代码。“大都市”是许多不协作的、互相争斗的程序创造的一团混乱，而“设计之城”则是由密切合作的同事创建的一组干净、一致、密切合作的软件组件。在很大程度上，Conway法则（注）反过来也生效，团队的组织方式就像软件的组织方式一样。

注意：团队的组织方式必然对它产生的代码有影响。随着时间的推移，架构也会影响到团队协作的好坏。当团队瓦解时，代码的交互就很糟糕。当团队协作时，架构就集成得很好。

2.2.3　现状

在一段时间之后，“设计之城”的架构如图2-4所示。也就是说，它与最初的设计非常相似，同时也包含了一些值得注意的变更。此外，它还包含了大量的经验，证明这个设计是正确的。健康的开发过程，小的、更善于思考的开发团队，适当注意确保一致性，带来了极为简单、清晰、一致的设计。这种简单性为“设计之城”带来了好处，得到了可扩展的代码和快速开发的产品。

在编写本书时，“设计之城”项目已走过了3年。代码集仍在使用，而且扩展出了一些成功的产品。它还在开发、成长、扩展，还在每天发生变化。下一个月它的设计可能与这个很不同，但也可能没有不同。

我要澄清一点：这些代码并不完美。有些地方存在着技术争论，但是它们在整洁的背景下显得特别突出，会在将来得到解决。没有什么是一成不变的，由于适应性强的架构和灵活的代码结构，这些问题都可以解决。几乎所有东西都各就各位，因为架构很好。

注：Conway法则指出，代码结构符合团队的结构。简而言之，“如果你让4个小组开发一个编译器，就会得到一个4阶段编译器。”

图　2-4：“设计之城”的最终架构

2.3　说明什么问题

等那完全的来到，这有限的必归于无有了。

——《哥林多前书》第13章10节

这个关于两个软件系统的简单故事当然不是软件架构的全面介绍，但我已展示了架构如何对软件项目产生深远的影响。架构几乎会影响所有与之相关的人和事，它决定了代码集的健康，也决定了相关领域的健康。就像一个繁荣的城市会为当地带来成功和声望，好的软件架构将帮助项目获得发展，为依赖于它的人带来成功。

好的架构是很多因素的结果，包括以下方面（但不限于此）：

·确实进行有意为之的前端设计。（许多项目甚至还没开始，就因为这一点而失败了。）

·设计者的素质和经验。（以前犯过一些错误是有帮助的，这能在下一次为你指出正确方向！“大都市”项目肯定教会了我一些东西。）

·授权团队负责软件的整体设计，而团队也承担起这一责任。

·在开发过程中，保持清晰的设计观点。

·不要害怕改变设计：没有什么是一成不变的。

·让合适的人加入到团队中，包括设计者、程序员和经理，确保开发团队的规模合适。确保他们具有健康的工作关系，因为这些关系将不可避免地影响代码的结构。

·在合适的时候做出设计决定，当你知道所有必要信息时再做出决定。延迟那些暂时不能做出的决定。

·好的项目管理，以及合适的最后期限。

2.4　轮到你了

绝不要失去神圣的好奇心。

——阿尔伯特・爱因斯坦

你正在读这本书是因为你对软件架构感兴趣，而且你对改进自己的软件感兴趣。所以这里就有一个极好的机会。对于你目前的软件经验，请考虑以下简单的问题：

1.什么是你看到过的最好的系统架构？

·你怎么知道它是好的？

·这个架构在代码集之内和之外带来了什么结果？

·你从中学到了什么？

2.什么是你看到过的最差的系统架构？

·你怎么知道它是差的？

·这个架构在代码集之内和之外带来了什么结果？

·你从中学到了什么？

参考文献

Beck,Kent,with Cynthia Andres.2004.Extreme Programming Explained,Second Edition.Boston,MA：Addison-Wesley Professional.

Fowler,Martin.1999.Refactoring：Improving the Design of Existing Code.Boston,MA：Addison-Wesley Professional.

Hunt,Andrew,and David Thomas. 1999.The Pragmatic Programmer.Boston,MA：Addison-Wesley Professional.

第二部分　企业级应用架构

第3章　伸缩性架构设计

第4章　记忆留存

第5章　面向资源的架构：在Web中

第6章　数据增长：Facebook平台的架构

第3章　伸缩性架构设计Jim Waldo

原则与特性

结构

功能多样性

模块

概念完整性

√依赖关系

修改独立性

进程

自动传播

√数据访问

可构建性

增长适应性

熵增抵抗力

3.1　简介

在设计系统架构时，一个比较有趣的问题就是确保系统在伸缩时的弹性。随着越来越多的系统运行在网络上或在互联网上提供访问，伸缩性正变得越来越重要。对于这样的系统，如果你希望误差的范围在几个数量级以内，那么容量规划的想法显然是荒谬的。如果你架起一个网站，然后它火了，你可能会突然发现有几百万的用户访问你的站点。同样容易出现的情况是，你架起了一个网站，却发现没有人感兴趣，你投入的所有设备都闲置着，消耗着能源和管理成本，浪费钱财（这同样是一种灾难）。在网络世界里，一个站点可以在几分钟内从其中一种状态转变成另一种状态。

只要是将系统连接到网络上，每个人都会遇到伸缩性问题，但是“大型多人在线游戏”（MMO）和虚拟世界特别关注这一点。这些系统必须具备伸缩性，以满足大量的用户。Web服务器的用户常常读取的是静态的内容，而且彼此之间没有交互，但MMO中的玩家或虚拟世界中的居民则不同，他们既需要与所处的世界进行交互（这改变了世界的基本信息），也需要彼此之间的交互。这些交互行为使得这类系统基础设施的伸缩性问题变得更复杂，因为用户与系统的交互几乎是独立的（除了那些不独立的情况），而且不会让世界的状态改变太多。对于一个世界里的任意两个参与者，他们在某个时刻进行交互的几率是非常小的。但是，几乎所有玩家在所有时候都在与他人交互。结果是这种系统并行程度非常高，但只有少数的交互是互相依赖的。

由于这些系统所培育起来的文化，MMO和虚拟世界的伸缩性问题进一步复杂化了。MMO和虚拟世界都源自于视频游戏产品。这是一种从PC游戏和游戏机游戏传统中成长起来的文化，在这种传统中，程序员会假定游戏运行在一台独立的机器或游戏机上。在这样的环境中，机器所有的资源都受游戏程序控制，程序的问题也限于单个用户玩游戏的情况（实际上，缺陷或奇怪的行为常常会被认为是游戏本身逻辑的一部分）。

这些游戏和编写、出品、扩展它们的公司，都属于娱乐行业。编写游戏的团队由一个出品人领导，有剧本和背景故事。游戏的目标是刺激、打动人，最重要的一点，要好玩。可靠性很好，但不一定必需。可扩展性是游戏的特性，让游戏在升级时能够加入新的故事情节和主题，但可扩展性不是代码的特性，不必让代码能以新的、不同的方式使用。在线游戏和虚拟世界的兴起将这种文化带入了一个新的环境，在这个环境中，需求与企业应用开发者所面对的类似。多个用户通过网络在服务器上交互时，由于一个玩家的意外动作而导致的服务器崩溃将影响许多其他玩家。随着这些世界发展出自己的经济（有些经济与现实世界的经济有关系），在线世界的稳定性和一致性就超出了一个游戏的要求。随着这些世界中玩家或居民的人数达到百万级，伸缩的能力就成为了任何架构的首要需求。

Darkstar项目（本章后面将简称为Darkstar）是对这些游戏和虚拟世界创建者的需求挑战的回答。这个项目由Sun公司实验室的一个研究小组承担，它将在架构的伸缩性领域不断探索。这个项目特别有趣之处在于，它是针对MMO和虚拟世界的创建者，这些程序员与我们（作为系统设计者）所熟悉的那些程序员相比，有着非常不同的需求。得到的架构看起来似乎很眼熟，但如果你仔细查看，会发现它的不同之处，它与你的经验有所不同。得到的架构有着属于它自己的美丽，同时它也是一堂实践课，说明了不同的需求如何改变你所想到的构建系统的方式。

3.2　背景

像一座建筑或一个城市的物理架构一样，系统的架构必须适应环境，利用该架构创建的工件将存在于该环境之中。对于物理架构来说，这个环境包括工作的历史环境、它所处位置的气候、本地工人的技能、可以获得的建筑材料，以及建筑的使用意图。对于软件架构，这个环境不仅包括使用该架构的应用程序，也包括那些要使用该架构的程序员，以及由此受到的系统约束。在创建Darkstar架构时，我们[1]意识到的第一件事就是所有针对伸缩性而设计的架构都需要包含多台机器。我们不清楚，就算是最大的大主机系统是否能够满足今天的一些在线游戏的要求（例如《魔兽世界》，据报道它有500万注册用户，几十万的同时在线用户）。就算单台机器能够处理这种负载，我们也不能在一开始就假定游戏会取得如此成功，需要这样的硬件投资。这在经济上是不可行的。这种应用需要能够从很小的系统开始，然后随着用户数的增长而增加处理能力，最后随着大家对游戏兴趣的衰退而降低处理能力。这与分布系统的特点相符，在分布式系统中，我们可以随着请求增长而添加（合理的小）机器，当请求下降时移走机器。所以我们从一开始就知道，总体架构必须是一个分布式系统。

我们也知道系统利用了芯片架构的当前趋势。MMO和虚拟世界（在较小程度上）曾经针对伸缩性利用过摩尔定律。随着处理器的速度倍增，可以创造的世界会在复杂度、丰富程度和互动性方面倍增。没有其他计算领域像游戏世界这样探索过处理器速度的增长所带来的好处。为游戏而设计的个人计算机总是将CPU速度、内存和图形性能推向极致。游戏机更激进地将这些方面推向极致，它们包含的图形系统远远超过了高端工作站上的图形系统，整个机器完全是为了游戏玩家的特殊需要而打造的。

芯片演进方面最近的变化是从不断增加的时钟速度转为实现多核处理器，这已经对游戏中能做的事情产生了影响。新芯片的设计目标不是将一件事做得更快，而是同时做多件事。如果在芯片上运行的多项任务实际上可以同时执行，那么在芯片层面上引入并发执行将带来更好的总体性能。在不改变时钟速度的情况下，4核的芯片应该能比单核的芯片多做3倍的事情。实际上，这种增长不是呈线性的，因为系统的其他一些部分没有以同样的方式实现并发。但是可以通过并发来实现系统总体性能的增长，而且制造这种并发的芯片比制造增加时钟速度的芯片要容易得多。

基于这一事实，MMO和虚拟世界应该是多核芯片和分布式系统的理想候选者。在MMO或虚拟世界中发生的大多数事情就像在真实世界中发生的大多数事情一样，与该世界中发生的其他事情是无关的。玩家继续他们的搜索或装饰他们的房间。他们与怪物交战或设计衣服。即使他们与该世界中的另一个玩家或居民发生交互，也只是与该世界的很少一部分居民发生交互。这正是令人为难的并行计算任务的特点，也正是多核和多机系统应该擅长处理的那种任务。

尽管这些系统中的任务的并行度让人为难，但为这样的系统编程的程序员却没有接受过分布式计算或并发编程方面的训练，也没有这方面的经验。这是极为微妙的领域，即使是在这个领域接受过训练的人和对这些技术相当有经验的人也会感到困难。要让大多数游戏程序员来开发高度并发的、分布式游戏服务器，就是要求他们做超出自己的专长和经验的事。

3.2.1　首要目标

这样的背景为我们确立了该架构的首要目标。对伸缩性的需求表明，系统应该是分布式的、并发的，但我们需要为游戏开发者提供简单得多的编程模型。简而言之，目标就是游戏程序员应该把该系统视为一台单机，运行着一个线程，所有允许部署到多线程和多计算机上的机制都应该由Darkstar项目的基础设施来考虑。

在一般的情况下，对应用程序隐藏分布式和并发是不可能的。但MMO和虚拟世界不是一般的情况。我们试图实现的这种隐藏，其代价就是必需一种非常特别的、严格限制的编程模型。幸运的是，这种模型恰好非常适合游戏服务器和虚拟世界已经采用的编程方式。Darkstar项目要求的一般编程模型是反应式的，在这种编程模型中，游戏的服务器端写成了事件监听器，监听客户端生成的事件（客户端就是游戏玩家使用的机器，通常是一台PC或游戏机）。如果检测到事件，游戏服务器就应该生成一项任务，这个任务是一个短期的计算序列，包括操作虚拟世界中的信息，并与最初生成事件的客户端或其他一些客户端进行通信。任务也可以由游戏服务器自己生成，要么是响应某些内部的变化，要么是周期性地根据时间来生成任务。在这种情况下，游戏服务器可以在游戏或虚拟世界中生成一些角色，这些角色是不受外部玩家控制的。

这种编程模型非常适合游戏和虚拟世界，但它也应用于一些企业级的架构中，如J2EE和Web服务。之所以需要创建一个不同于这些企业计算机制的架构，是因为MMO和虚拟世界存在的环境非常不一样。这种环境几乎刚好和经典企业环境相反，这意味着如果你接受过企业环境方面的训练，你知道的所有事情在这个新世界中几乎都是错的。

经典的企业环境可以描述为一个“瘦”客户端连接到一个“胖”服务器（这个服务器又常常连接到一个更“胖”的数据库服务器）。服务器将保存客户端需要的绝大部分信息，在最理想的情况下，客户端内存不多，没有自己的硬盘，它是服务器的一个称职的显示设备，绝大多数真正的工作在服务器上完成。

[1]在提到Darkstar项目的架构开发时，我通常会说“我们”做了什么，而不是“我”做了什么。这不仅是编辑意义上的“我们”。该架构的设计是一个协作项目，由Jeffrey Kesselman、Seth Proctor和James Megquier发起，并经过Seth、James、Tim Blackman、Ann Wollrath、Jane Loizeaux和我的努力发展到现在的状态。

3.2.2　游戏世界

MMO和虚拟世界的环境始于一个非常胖的客户端：它通常是顶级的PC、具有最强劲的CPU、很大的内存，以及本身计算能力就很强的显卡。它也可以是一台游戏机，专门为图形密集的、高度交互的任务而设计。只要有可能，数据就会存放在这些客户端，特别是那些不会改变的信息，如地理信息、材质贴图和规则集。服务器保持尽可能的简单，通常只保存非常抽象的世界表示和其世界中的实体的表示。而且，服务器的设计目标是尽可能少地进行计算。绝大部分的计算留给了客户端。服务器的真正工作是保存共享的世界真实状态，确保不同客户端对世界的看法差异可以根据需要得到纠正。真实状态需要由服务器来保存，因为控制客户端的玩家很有兴趣让他们的表现变成最强，所以可能会受到诱惑，根据他们的喜好修改共享的真实（如果他们可以）。在一般情况下，如果有可能，玩家就会作弊，所以服务器必须是共享真实的最终来源。

MMO和虚拟世界的数据访问模式也和企业中看到的情况有着很大的区别。企业中的一般经验法则是90%的数据访问都是只读的，大多数任务会读取大量数据，然后再改写少量数据。在MMO和虚拟世界的环境中，大多数任务只访问服务器上少量的状态数据，但在它们访问的数据中，大约一半会被改写。

3.2.3　延迟是敌人

但是，这两种环境中最大的不同要追溯到用户所做的事情的不同。在企业环境中，目标是管理业务，如果总吞吐量得到改进，在处理中有一点延迟是可以接受的。在MMO和虚拟世界的环境中，目标是开心，而延迟是开心的敌人。所以MMO或虚拟世界的基础设施需要围绕着尽可能限定延迟的需求来设计，即便以吞吐量为代价也在所不惜。

在线游戏和虚拟世界显然已经找到了办法来实现伸缩性，以应对数量巨大的用户。目前的方法可以分成两大类。第一类实质上是基于地理位置来实现的。游戏设计成包含一组不同的区域，每个区域运行在一台服务器上。它可能是虚拟世界的一个岛或房间，也可能是在线游戏中的一个小镇或山谷。游戏设计试图让每个区域无关，限定地理区域的大小，这样服务器不会因太多用户进入这个区域而拥塞。在实践中，这样的区域常常能实现自我限制，因为当服务拥塞时，游戏就会变得响应比较慢，趣味性下降。因此，玩家就会转向更有趣的区域，这使得以前拥塞的区域人数减少，响应时间得到改进。

将不同地理区域分配给不同服务器来实现伸缩性的方法有一个问题，即必须在游戏编写时决定哪些区域应该放到一台服务器上。虽然在游戏或虚拟世界中添加新的区域相当容易，但是改变已经分配给服务器的区域却可能需要改动代码。决定哪些区域组成一个伸缩性单位，这必须是开发工作的一部分。

第二种处理游戏或虚拟世界中拥塞区域的方法叫做分区（sharding）。一个分区是该区域的一份副本，运行在它自己的服务器上，独立于其他的分区，它代表了游戏中相同的部分，即原来的区域。这样，分区可能代表了某个房间或村庄的不同副本，允许成倍的玩家进入到世界的这个部分中。分区的缺点是它们不允许处于不同分区的玩家彼此之间进行交互。随着游戏和虚拟世界变成更多的社交体验，而不仅仅是玩游戏，这种缺点就明显了。玩家的目标不只是要出现在虚拟世界中，而是要和他们的朋友（真实的和虚拟的）一起进入虚拟世界。分区阻碍了这个目标的实现。

因此，Darkstar架构的另一个主要目标就是支持随时伸缩，同时不要求游戏逻辑受到伸缩的影响。这个架构应该支持游戏动态地响应负载，而不是让这种响应成为游戏设计工作的一部分。

3.3　架构

Darkstar由一组独立的服务构成，这些服务可以在游戏或虚拟世界的服务器端的地址空间内获得。每个服务都定义为一个小的编程接口。尽管不是出于本意，但Darkstar项目提供的一些基本服务很像经典操作系统的服务，它们支持游戏或虚拟世界的服务器端访问持久存储，调度并执行任务，与游戏或虚拟世界的客户端进行通信。

用一组相互联系的服务来构建这个系统，显然是开始了“分而治之”的过程，分而治之是设计所有大型计算机系统的基本方法。每种服务都可以用一个接口来描述，这让使用该服务的程序不会受到底层实现变更的影响，同时也支持这些实现可以独立地完成。对一个服务的实现进行变更不应该影响到另一个服务的实现，即使其他的服务会利用到这个变更的服务（假定接口和接口的语义没有变更）。

我们采用服务分解的方法还有其他的原因。从一开始，Darkstar项目就设计成一个开放源代码的项目，希望我们能够放大核心团队的工作，支持其他社区成员创建更多的服务，丰富核心的功能。维护一个开源社区在任何情况下都是复杂的，我们相信在组成架构的服务之间拥有最大程度的隔离，将支持在不同服务实现层次之间的更高级的隔离。此外，当时并不清楚是否存在单一一组服务能够适合所有的MMO和虚拟世界。将基础设施设计为一组独立的服务，使得这些服务的不同组合可以在不同的情况下使用，这由使用该基础设施的具体项目的需求来决定。Darkstar栈中具体包含哪些服务可以由一个配置文件来设置。

3.3.1　宏观结构

图3-1展示了基于Darkstar项目基础设施的游戏或虚拟世界的基本结构。一些服务器构成了游戏或虚拟世界的后端。每个服务器运行着一组选定服务的副本（称为Darkstar栈）和游戏逻辑的副本。客户端将连接到其中一个服务器，与服务器保存的该世界的抽象表示进行交互。

与大多数的复制策略不同，游戏逻辑的不同副本不需要处理相同的事件。每个副本可以独立地与客户端进行交互。在这个设计中，复制主要用于支持伸缩性，而不是确保容错（虽然我们后面会看到，容错也实现了）。而且，游戏逻辑本身不知道、也不需要知道在其他机器上运行着服务器的其他副本。游戏程序员编写的代码就像在一台机器上执行一样，不同副本之间的协作由Darkstar项目的基础设施来完成。实际上，如果游戏的容量只需要一台服务器，基于Darkstar的游戏就能够在一台服务器上运行。

客户端连接到游戏逻辑使用的通信机制是基础设施的一部分。这些机制支持客户端到服务器的直接通信，也支持一种“发布-订阅”通道，任何发往通道的消息都会送达该通道的所有订阅者。

图　3-1：Darkstar项目的高层架构

Darkstar栈由一组元服务来协调，这是一组网络访问服务，游戏或虚拟世界的程序员是不可见的。这些元服务支持栈的各个副本之间进行协作，共同运营整个游戏。例如，这些元服务将所有独立的副本持续工作，如果某个副本失效，就会发起失效恢复。这些元服务还会跟踪各副本的负载，在需要的时候重新分配负载，或者随时添加新的服务器，增加总体容量。由于这些服务对于Darkstar项目的用户来说是完全隐藏的，所以它们可以随时改变或移除，或者添加新的服务，这都不需要修改游戏或虚拟世界的代码。

对于在Darkstar项目环境中创建游戏或虚拟世界的程序员来说，可见的架构就是栈中包含的一组服务。服务的全集是可以改变和配置的，但4个基本服务必须存在，它们构成了运营环境的核心，如图3-2所示。

图　3-2：Darkstar栈

3.3.2　基本服务

在这些栈层面的服务中，最基本的服务就是“数据服务”（Data Service），游戏或虚拟世界用它来保存、读取和操作所有持久数据。这里的持久概念可能比其他系统中的持久概念更宽泛。对于在Darkstar项目环境中编写的游戏或虚拟世界，任何存在时间超过一个任务的数据都被视为持久的，必须在“数据服务”中保存。我们曾假定在这种编程模型中任务的时间是短暂的（这也是需求），所以几乎所有用于表示游戏或虚拟世界的服务器端的数据都需要持久。“数据服务”也将运行在不同服务器上的游戏或虚拟世界的副本联系在一起，因为所有这些副本都共享同一个（概念上的）“数据服务”实例。所有的副本都会访问相同的数据，所有的副本都可以根据需要读取或改变存储在“数据服务”中的数据。

虽然“数据服务”看起来像是使用一个数据库的好地方，但是存储的需求实际上与通常条件下对标准数据库的需求有着很大的差别。存储的对象之间静态的关系很少，游戏中也不需要对存储的内容进行复杂的查询。相反，简单的命名策略就足够了，包括在编程语言层面上对对象的引用。“数据服务”也必须针对延迟进行优化，而不是针对吞吐量来优化。特定服务要访问的对象个数可能很少（我们初步的测算基于一些游戏和虚拟世界的原型，这些测算表明每个任务大约访问一打对象），在这些访问的对象中，大约一半会在任务执行中改变。

第二个栈层面的服务是“任务服务”（Task Service），它用于调度或执行任务。这些任务要么是响应从客户端收到的某个事件，要么是由游戏或虚拟世界服务器本身的内部逻辑发起的。绝大部分任务是一次性事件，是由于客户端的某种动作产生的，它们从“数据服务”中读取一些数据，操作这些数据，可能还进行一些通信，然后结束。任务也可能生成其他的任务，或者生成定期任务，在特定的时间执行或以特定的时间间隔执行。所有任务的执行时间必须很短，执行一项任务的最大时间是一个可配置的值，但默认值是100毫秒。

游戏或虚拟世界的程序员会看到因事件或服务器逻辑本身而生成的单个任务，但在底层，Darkstar的基础设施正尽其所能调度最多的任务。特别地，由服务器逻辑生成的任务与响应客户发起的事件而生成的任务是并行执行的，就像响应不同客户端而生成的任务一样。这样的并发执行可能导致数据竞争。要处理这种竞争，就需要“任务服务”和“数据服务”协作。在底层，在服务器程序员不可见的地方，“任务服务”调度的每个任务都包装在一个事务中。这个事务确保了任务中的所有操作要么全部完成，要么都不完成。此外，所有改变“数据服务”中对象的值的操作都由该服务作为中介。如果有多个任务试图改变相同的数据对象，只有一个任务会执行，其他任务都会中止，并安排在稍后执行。执行的那个任务会运行到结束。当执行的任务结束时，其他的任务就可以执行了。虽然服务器程序员可以说明访问的数据将被修改，但这不是必需的。如果数据对象先被读取，然后被修改，“数据服务”会在任务提交之前检测到这种修改。在读取时就说明打算进行修改，这是一种优化，能够更早地检测到冲突，但是不事先说明修改的意图也不会影响程序的正确性。

将任务包装到一个事务中意味着通信机制也必须支持事务，只有当包装了消息发送任务的事务提交时，消息才会发出。这是通过Darkstar栈中余下两项核心服务来完成的。

3.3.3　通信服务

第一个服务是“会话服务”（Session Service），它是客户端和游戏或虚拟世界服务器之间通信的中介。在登录和认证后，客户端与服务器之间就会建立起一个会话。服务器通过会话监听客户端发出的消息，解析消息的内容，确定生成怎样的任务来响应该消息。客户端通过会话接收来自服务器的响应。这些会话隐藏了客户端和服务器的真实端点，这一点对于Darkstar的多机伸缩性策略是很重要的。会话也负责确保维持消息的顺序。如果来自某个客户端的前一条消息所引发的任务还没有完成，后一条消息就不会提交。在“会话服务”对任务进行这样的排序之后，“任务服务”就得到了极大的简化。“任务服务”可以假定它在任何时候收到的任务在本质上都是并发的。对来自特定客户端的消息排序是Darkstar框架中唯一的消息排序保证机制，外部观察者看到的多个客户端之间的消息顺序，与游戏或虚拟世界内看到的顺序有很大不同。

Darkstar栈中总可以得到的第二种通信服务是“通道服务”（Channel Service）。通道是一种一对多的通信机制。在概念上，通道可以由任何数目的客户端加入，任何发往该通道的消息都会送达所有与通道相关的客户端。这里似乎是应用端到端技术的好地方，可以让客户端之间直接通信，不会增加对服务器的负载。但是，这种通信需要由一些受信任的代码来监控，确保玩家不会利用不同的客户端实现来发送不正确的消息或欺骗消息。既然客户端假定是在用户或玩家的控制之下，那么客户端的代码就不能信任，因为很容易把原来的客户端代码换成另外的“定制过的”客户端代码。所以，实际上，所有通道消息都必须经过服务器，（可能）在经过服务器逻辑检查之后。

会话和通道的复杂性有多种原因，其中之一就是它们必须遵守任务的事务语义。因此，会话连接或通道上的实际消息传送不能够在调用相应的send（）方法时发生，它只能够在该方法所处的任务提交时才能发生。

这些通信机制为我们实现伸缩性机制的第二部分奠定了基础。既然所有通信都必须通过Darkstar会话或通道的抽象层，而这些抽象层又不暴露客户端或服务器通信的真实端点，那么在实体通信和通信起止端的实际位置之间就存在着一个抽象层。这意味着我们可以在Darkstar系统中将服务器通信的端点从一台机器移到另一台机器，同时不会改变客户端对这次通信的感觉。从游戏或虚拟世界的视角来看，通信也是经过一个会话或通道。但是底层的基础设施可以随着时间的推移和负载的变化，根据负载平衡的需要，将会话或通道从一台机器移到另一台机器。

3.3.4　任务的可移动性

要实现负载均衡的能力，其关键之处在于，对于我们要求的编程模型和必须使用的基本栈服务，响应客户端事件或游戏内部事件的任务可以从任何一台运行着Darkstar栈和游戏或虚拟世界副本的机器上移动到另一台同样的机器上。任务本身是用Java编写的[1]，这意味着只要（物理）机器的运行时栈中包含相同的Java虚拟机，任务就能够运行。任务读取和操作的所有数据必须从“数据服务”获得，“数据服务”是所有机器上的游戏或虚拟世界的实例和Darkstar栈所共享的。通信由“会话服务”或“通道”来实现中介，它们抽象了通信的真实端点，而且支持特定的会话和通道从一个服务器移动到另一个服务器上。因此，所有任务都可以运行在任意一个游戏服务器的实例上，同时不改变任务的语义。

这使得Darkstar的基本伸缩机制看起来很简单。如果有一台机器超载了，只要将一些任务从这台机器移到另一台负载较小的机器就行了。如果所有的机器都超载了，就向运行着Darkstar栈和游戏/虚拟世界的集群中添加新的机器。底层的负载平衡软件会将负载分发给新的机器。

对单台机器的负载进行监控并在需要时重新分配负载，这是元服务的工作。这些元服务是网络层面的服务，对于游戏或虚拟世界的程序员是不可见的，但是它们对Darkstar栈中的服务是相互可见的。例如，这些元服务会监控哪些机器正在运行（并监控是否有机器失效），哪些用户与某台机器有关，不同的机器当前的负载。由于元服务对于游戏或虚拟世界的程序员是不可见的，所以它们在任何时候都可以改变，不会影响到游戏逻辑的正确性。这让我们能够尝试不同的策略和方法，实现系统的动态负载平衡，也让我们能够丰富基础设施所需的元服务集合。

同样，我们使用实现多机伸缩机制来实现系统的高容错。由于任务和通信机制所使用的数据是与具体机器无关的，所以很明显，我们可以将任务从一台机器移到另一台机器上。但是如果机器失效，我们如何恢复在那台机器上执行的任务呢？答案很简单：任务本身也是持久对象，保存在系统的“数据服务”中。因此，如果一台机器失效，该机器上正在执行的所有任务都被视为中断的事务，会重新调度在不同的机器上执行。尽管这种重新调度比在一台机器上重新调度中断的任务的延迟要长，但系统的正确性是不变的。系统的用户（游戏玩家或虚拟世界的居民）顶多会注意到响应时间暂时有点延长。这样的延迟让人有点不舒服，但总好过现在其他游戏或虚拟世界环境中服务器崩溃所造成的影响。在那些环境中，会导致玩家掉线，还可能导致相当一部分游戏状态的丢失。

[1]更准确地说，所有的任务都由字节码的序列组成，可以在Java虚拟机上执行。我们不关心源语言是什么，我们只关心从源语言编译出来的结果可以运行在所有支持游戏或虚拟世界的分布式环境中。

3.4　关于架构的思考

也许所有人关于架构及其实现的第一个问题就是它的性能。虽然未经深思熟虑就对架构进行优化是诸多罪恶之源，但是我们也可能设计出一个架构，而它的实现根本不可能达到好的性能。由于Darkstar架构的一项基本选择，这种担心是真实的。而且由于游戏行业的特点，确定服务器基础设施的性能是很难的。

要确定游戏或虚拟世界服务器基础设施的性能，其难度源自一个简单的事实：没有针对大规模MMO或虚拟世界的性能测试标准或共同接受的例子。缺少性能测试标准并不奇怪，因为绝大多数游戏或虚拟世界的服务器组件都是针对特定的游戏或虚拟世界从头开发的。只有少数的通用基础设施可以作为可复用的构建块，这些组件一般是事后从特定的游戏或虚拟世界中提取出来的，提供给其他构建类似游戏的人使用。也许是因为游戏行业还比较年轻，或是因为娱乐业中出现重要技术的偶然性，结果是没有共同接受的性能测试标准用于测试新的基础设施，也无法对不同的基础设施进行比较。

关于游戏或虚拟世界的预期计算、数据操作和通信负载也基本上没有什么资料，所以很难创建性能测试标准程序。部分原因是已有的服务器都是定制的。每台服务器都是为特定的游戏或虚拟世界设计的，所以考虑的是那款游戏或虚拟世界的具体负载特征。而且，这种状况也是因为游戏业的强烈的保密性。在游戏业中，关于一款游戏开发的信息是严格保密的，而且发布游戏的实现方式也是严格保密的。同时，行业中的许多人对这些信息是不感兴趣的。大量的思考和讨论集中在艺术设计、故事情节或玩家交互模式上，这些令新游戏更有趣、更好玩，而不是游戏服务器的设计方式和游戏为支持并发玩家（这个数字也是严格保密的）所采取的伸缩性技术。所以，获得现有的游戏和虚拟世界的这种服务器负载信息都很困难。

根据我们的经验，即使我们能够找来开发者，谈论他们的游戏或虚拟世界加在服务器上的负载，他们也常常会报告错误的信息。这不是因为他们想保持商业优势而故意错误报告他们服务器的情况，而是因为他们真的自己不也了解。在游戏服务器上基本不会加入一些手段，让他们收集有关服务器真实性能或完成事务的信息。对于这种服务器的分析一般最多是经验性的。程序员在服务器上工作，直到它让游戏玩起来有趣，这是一种迭代式的工作方式，而不是仔细对代码本身进行测量。在这些系统中，更多的是手工技术活，而不是科学测定。

这并不是说，这些游戏或虚拟世界后面的服务器是一些粗制滥造的代码，也不是说它们做得不好。实际上，许多代码是效率杰作，展示了聪明的编程技巧，也展示了针对高要求的应用构造一次性、专门目的服务器的优势。但是，为每个游戏或虚拟世界构建一个新服务器的习惯意味着人们不太注意积累构建这种服务器所需的知识，也没有共同接受的机制来比较不同的基础设施。

3.4.1　并行与延迟

缺少有关服务器可接受的性能的资料，这一点引起了Darkstar团队的特别关注，因为我们所做的一些关键决定，都是围绕着如何能够从游戏或虚拟世界服务器中获得好的性能。也许Darkstar架构和一般实践之间的最大区别就在于，Darkstar架构拒绝在服务器的主内存中存放任何重要的信息。所有生存周期超过一次具体任务的数据都需要持久在“数据服务”中，这是实现Darkstar基础设施功能的核心。这让基础设施能够检测到并发问题，反过来又让系统能够对程序员隐藏这些问题，同时让服务器能够利用多核架构。它也是实现整体伸缩性的关键组件，支持任务从一台服务器移动到另一台服务器，从而在一组机器上实现负载平衡。

在游戏和虚拟世界服务器领域，任何时候都持久保存游戏状态是一种异端邪说，因为人们普遍很关心延迟。在编写这种服务器时，大家的观点是只有将所有信息都放在内存中才能让延迟足够小，达到要求的响应时间。可以偶尔保存状态的快照，但对交互速度的要求表明，这种长时间的操作只能够偶尔进行，而且要在后台进行。所以，从表面上看，我们的架构似乎绝不可能达到足够好的性能，从而服务于它的目标应用。

虽然要求数据持久肯定是这个架构的主要不同之处，而且要求通过“数据服务”来访问数据会在架构中引入一定的延迟，但我们相信我们所采取的方式更具有竞争力，原因有几点。首先，我们相信能够让访问内存数据和访问“数据服务”中的数据之间的差异远远小于一般人们的看法。虽然在概念上每个生命周期超出一次任务的对象都需要从持久存储中读出，并写入持久存储，但实现这种存储可以利用人们在数据库缓存和一致性方面多年的研究成果，从而减少因这种方式而导致的数据访问延迟。

如果我们能够将访问局限在一个特定服务器上的几组特定对象，就更是如此了。如果用到一组特定对象的那些任务都运行在一台服务器上，那么就可以利用该服务器的缓存，达到接近内存的对象读写速度（受到需要满足的持久性约束的影响）。我们可以识别任务属于哪些玩家或虚拟世界的哪些用户。这样，我们就可以利用基础设施中服务所接收到的数据访问和通信请求，来收集特定时刻游戏或虚拟世界中数据访问模式和通信模式的信息。有了这些信息，我们相信能够非常准确地估计哪些玩家应该与另一些玩家放在一起。因为我们可以根据需要将玩家移动到任何服务器上，所以能够根据观察到的运行时行为，主动地将相关的玩家尽可能地放在同一台服务器上。这让我们能够运用数据库领域熟悉的标准缓存技术，尽量减少访问和保存持久信息的延迟。

这听起来非常像目前在大规模游戏和虚拟世界中为实现伸缩性而采用的地理区域分解技术。在这种技术中，服务器开发者将世界分解成一些区域，将它们指派给一些服务器，不同的区域就成为用户分区的机制。同一区域的玩家比不同区域的玩家进行交互的可能性更大，所以这种集中在一个服务器上的优势就体现出来了。不同之处在于，目前的地理区域分解是在游戏开发过程中进行的，被编入源代码，放到服务器上。而我们的位置集中基于运行时的信息，可以根据游戏中发生的实际玩法和交互模式来实现动态调整。这类似于编译时优化和运行时优化之间的区别。前一种方法试图针对程序所有可能的运行进行优化，而后一种方法试图针对当前的运行进行优化。

我们不相信我们能够消除内存访问和持久访问之间的差别，而且我们也不认为有必要这样做，最后让这种架构比使用内存的架构性能更好。要知道，通过让所有的数据持久，我们可以支持在服务器上使用多线程（从而也支持多核）。尽管我们不相信并发是完美的（即对于每个添加的核，我们都能充分利用），但我们确实相信在游戏和虚拟世界中可以使用大量的并行运算（初步的结果也证实了这种看法）。如果可使用的并行运算超过我们可能引入的延迟，那么游戏或虚拟世界的总体性能就会更好。

3.4.2　赌未来

我们对多核处理器中多线程的信心基本上是在赌处理器将来的发展方向。目前服务器的处理器提供2～32个核，我们相信将来的芯片设计将集中向更多的核发展，而不是让现有的核以更高的时钟频率运行。当我们在几年前开始这个项目时，这种赌博似乎比现在更具投机性。那时候，我们在做展示时常常说这种设计是一种“假定”的演练，说我们正尝试一种架构，如果芯片性能更好地支持多线程而不是单线程的时钟速度，这种架构将是可行的。这就是在研究实验室中进行这类项目的好处之一，可以接受设计方法中存在很高的风险，探索一个将来也许在商业上可行的领域。我们决定构建一个以多线程为中心的架构，与这个决定做出时相比，目前芯片设计的趋势让我们看得更清楚。[1]即使我们只能得到50%的完美并发，如果我们能把使用持久存储的延迟控制在使用内存的延迟的2～16倍，就能够在性能上持平。我们相信在并发方面以及减少访问持久状态和全内存方案之间的差异方面都可以做得更好。但是结果主要取决于构建于这个基础设施之上的应用的使用模式（我们曾提到，这一点很难发现）。

我们也不应认为减少延迟就是这个基础设施的唯一目标。通过将游戏或虚拟世界服务器端的对象全部保存在“数据服务”中，我们把因服务器失效而导致的数据丢失减到了最小。实际上，在大多数情况下，服务器失效时用户只会注意到延时有一点增加，因为任务（它们本身也是持久对象）从失效的服务器移到了另一台服务器上。没有数据会丢失。一些缓存机制可能导致丢失几秒钟的游戏成果，但即使是这样，也比在线游戏和虚拟世界目前使用的机制好得多，它们只是将偶尔进行内存快照作为主要的持久方式。在它们的基础设施中，如果服务器在不巧的时间崩溃，可能会造成数小时的游戏成果丢失。只要延迟是可以接受的，Darkstar所使用持久机制的可靠性更高，这对于在这个基础设施上构建系统的开发者和系统的用户来说都是优点。

[1]再一次说明，在早期的设计决定中少许的运气是很重要的。

3.4.3　简化程序员的工作

实际上，如果在支持伸缩性的同时减少延迟是服务器开发者的唯一目标，那么开发者最好的方法就是专门针对特定的游戏，编写自己的分布式和多线程基础设施。但这要求服务器开发者处理复杂的分布式和并发编程。在为速度需求而过度烦恼之前，我们应该想到Darkstar的第二个同样重要的目标，即在支持多线程、分布式游戏产品的同时，为程序员提供一个单机单线程的开发模型。

在相当大的程度上，我们已经实现了这一目标。通过将所有任务封装到事务中，并在“数据服务”中检测数据冲突，程序员就能够享受到多线程的好处，又不必在他们的代码中引入锁协议、同步和信号量。程序员不必担心如何将玩家从一台服务器移到另一台服务器，因为Darkstar为他们提供了透明的负载平衡。编程模型虽然有自己的风格和限制，但社区中的早期成员认为，这对他们开发的那种游戏和虚拟世界来说是比较自然的。

不幸的是，我们发现我们不能够向程序员隐藏所有的东西。当在Darkstar上编写的第一个游戏表现出极少的并行度（以及意料之外的糟糕性能）时，这一点就明确了。通过检查源代码，我们很快就发现了原因。游戏中的数据结构设计导致了游戏中所有的状态改变都只涉及一个对象，并由它来协调所有的工作。使用这个对象实际上使得游戏中所有动作序列化执行，这使得基础设施不能够发现并利用并发计算。

当我们发现这一点时，我们与游戏开发者进行了长时间的讨论，主题是在设计对象时需要考虑到并发访问。通过对游戏中数据对象进行审查，我们发现了一些类似的情况：数据设计方案排除了并发的可能性（并非出自设计者本意）。当这些对象重新设计之后，系统的整体性能增加了好几个数量级。

这告诉我们，不可能让使用Darkstar的开发者完全不知道系统底层的并发和分布式实质。但是，他们对系统这方面特点的知识不需要包括并发控制、锁，以及在系统的各个分布式部分之间的通信。实际上，他们只需要在设计活动中确保他们的数据对象定义能够充分利用并发。这种设计一般只需要确保对象定义是自包含的，它们的操作不需要依赖其他对象的属性。这一点对于任何系统来说，都不是不好的设计原则。

关于Darkstar架构，我们还有许多方面没有测试，或者说我们并未完全理解。虽然我们已经得到了一个系统，使得多台机器能够利用多线程运行一个游戏或虚拟世界，同时对服务器程序员（几乎）保持透明，但是我们还没有通过添加核心服务之外的其他服务，来检验该架构的能力。由于Darkstar任务的事务本质，这可能比我们开始设想的要复杂得多，但我们希望这些添加的服务不需要参与到核心服务的事务中。我们已经开始试验通过不同的方式来收集系统负载的信息和实现负载平衡。幸运的是，因为实现这种负载平衡的机制对于使用系统的程序员是完全不可见的，所以我们可以移除老的方式，引入新的方式，同时又不影响Darkstar的用户。

作为一个架构，Darkstar展示了一些创新的方法，这使它变得很有趣。它试图构造一个游戏或虚拟世界的基础设施，使其具有企业级软件一样的可靠性，同时又满足游戏行业对延迟、通信和伸缩性的要求。它是目前为数不多的这类尝试之一。通过利用更多机器和更多线程来实现效率，我们希望能够抵消因使用持久存储机制而导致的延迟增加。最后，游戏和虚拟世界环境中极为不同的情况，即客户端的处理很多而服务器端的处理很少，与我们常见的高并发、分布式系统环境形成了鲜明的对比。现在说这个架构是否成功还为时尚早，但我们相信它已经很有趣了。

第4章　记忆留存Michael Nygard

原则与特性

结构

功能多样性

√模块

√概念完整性

√依赖关系

√修改独立性

进程

√自动传播

数据访问

√可构建性

增长适应性

熵增抵抗力

从最早的锡版照相法和达盖尔银板照相法开始，我们一直都认为照片是具有特别意义的，有时候甚至是神奇的。照片记录了一闪而过的时刻，而我们的记忆容易出错，不能做到这一点。但是最好的肖像不仅保存了一个时刻，还阐释了这个时刻，捕捉了某个眼神或表情，捕捉了有特点的姿势，让主角靓丽照人。

如果你的小孩在美国的学校上学，你可能已经熟悉Lifetouch这个名字。Lifetouch每年都为美国几乎所有的小学生、初中生和高中生照相。但是你可能不知道，Lifetouch还经营着高品质的肖像照相馆。Lifetouch肖像照相馆（LPS）开设在美国各大零售商店中，还在大超市中开设了“Flash！”连锁照相馆。在这些照相馆中，LPS的摄像师拍摄那些可以保留一生的照片。

数字摄影改变了整个摄影行业，LPS也不例外。大圈的胶片和上底片框的照相机不见了，取而代之的是专业级的DSLR和闪存卡。自由摄影师可以四处走动，尝试不同角度，比以往任何时候更接近所拍摄对象。简而言之，它们更自由地拍摄这些了不起的肖像。摄影师利用照相机将光子转变为电子，但通过某种方式，在某些地方，某些系统必须将这些电子转变成墨水原子和纸张。

2005年，我和来自Minneapolis的ATI（Advanced Technologies Integration）的同事，与LPS的开发者一起工作，开发了一个新系统来完成这部分工作。

4.1　功能和约束

这两点影响了系统的架构：它必须做什么？它必须在什么限制条件下工作？这两点也确定了问题空间。

通过解决这些重要问题，在要求的行为和限制条件之间进行探索，我们创造并考察解决方案空间。如果对各项限制条件的解决方案形成了一致的整体，我们就可能创造出一个优雅的、美丽的解决方案。我很高兴地说，Creation Center项目做到了这一点。

在这个项目中，我们面对了一些明确的事实。有些只是业务的特点，另一些会改变，但不在我们考虑的范围内。不管怎样，我们认为这些事实是不会变的。这些事实构成了图4-1中的左边一列。

图　4-1：事实、重要问题和Creation Center架构的关注点

多品牌

LPS今天支持多个品牌，将来可能加入更多品牌。至少，Creation Center会有两个视觉上截然不同的界面主题，而且添加界面主题应该不需要大量的工作。

合伙人是摄影师，不是图像艺术家

摄影师接受过如何使用照相机的培训，但没有接受过使用Photoshop的培训。当一个没有经验的用户使用Photoshop时，最可能的结果是得到糟糕的图片。对于强大的用户来说，Photoshop是强大的工具，照相馆的摄影师应该没有必要去体验Photoshop的学习曲线。Photoshop和类似的工具会让照相馆的工作流程变慢。照相馆的合伙人需要快速创造出漂亮的图像。

照相馆是远程的

照相馆在地理上是分布式的，很少或没有本地技术支持。硬件交付或替换需要来回运送部件。

网络是不可靠的

某些照相馆没有网络连接。即便对于有网络连接的照相馆，因断网而导致照相馆停工也是不可接受的。

客户希望得到他们自己的产品

客户收到的照片应该包括他们自己的设计和要求的文字。

生产是集中式的

高品质的照片打印机正变得越来越常见，但制作能够保存几十年的产品需要贵得多的设备。

生产吞吐量很重要

使用到相同的打印机也是生产过程中的约束条件。因此，过程中的其他步骤必须服从于这个约束条件。

这些事实导致了一些重要问题，我们必须权衡考虑。人们常常将这些重要问题视为最基本的，但它们不是的。实际上，它们源自于系统存在的环境。如果环境改变，这些重要因素会无效，甚至走向反面。

我们选择了一些结构来解决这些重要问题。图4-1中右边的一列展示了架构的这些关注点。当然，这些并不是Creation Center的全部值得讨论的特性，但架构中的这些考虑是普遍关注的。我也认为它们同时展示了很好的关注点分离和相互支持的结构。

在深入到具体的特性之前，我们需要补充介绍一点背景知识：系统的工作流。

4.2　工作流

典型的照相馆有2～4个摄影室，备有专业的灯光、背景幕布和支架。摄影师在摄影室中拍照（每张照片被称为一个“姿势”）。在摄影室外面，摄影室也提供客户服务、安排计划，并让客户挑选照片。

当摄影师完成了一个阶段的拍照之后，她会坐到一台工作站前面，从照相机的存储卡中导入照片。

导入了一批照片后，摄影师会删除那些明显不好的照片，包括闭眼的、不高兴表情的、婴儿没有看镜头等照片。删除了这些不好的照片之后，剩下的照片就是“基本照片”。接下来她会从这些基本照片中选择一些进行增强。增强可以是简单色调调整，如变成黑白或深褐色，也可以是精心组合多张照片。例如，摄影师可能为3个小孩拍摄一张合影，然后将照片嵌入设计好的3个“卡槽”中，得到他们的个人照。

在完成了这些增强后，摄影师帮助客户订购打印的各种尺寸和组合。包括8"×10"这样的大照片，或者5"×7"、3"×5"，以及适合放在钱包中的小照片。还有更大的尺寸。客户最大可以订购24"×30"的照片，配上镜框后挂在墙上。

在完成了客户订购后，摄影师就转向下一组照片的拍摄。

每天工作结束时，照相馆经理会制作一张DVD，包含当天所有的订购照片，并将它送到打印工厂。

打印工厂每天会收到几百张DVD。（稍后我会介绍这些DVD的内容。）这些DVD包含了订单和需要打印的照片，照片打印好后会送回照相馆，这样客户就可以取走自己的照片。但在照片打印之前，最终打印分辨率的照片必须渲染成图片。这些可以打印的图片是很大的。如果把24"×30"的肖像渲染成高品质的图片，会超过1亿像素，每个像素都由32位的颜色来表示。所有像素都是根据摄影师在照相馆中的设计来合成的。根据不同的合成方式，渲染的过程可能包含6～10个步骤。简单的演染需要2～5分钟，但大照片的复杂渲染需要超过10分钟的时间。

同时，打印机每分钟都会输出一些完成打印的照片。让打印机保持工作是“生产控制系统”（PCS）的职责，这是一个复杂的系统，它负责处理任务调度、协调渲染工厂、管理图像存储，并将图像提供给打印队列。

当完成的订单送达照相馆时，经理会告知客户可以过来取照片了。

这个工作流部分来自于LPS的业务背景，部分来自于我们对于如何划分系统的考虑。接下来让我们来看看图4-1中不同的关注点。

4.3　架构关注点

将一个多维的、动态的系统简化为一种线性的描述形式总是一项挑战，不论我们是在谈论对还不存在的系统的愿景，还是试图解释已经构建好的系统中各个部分的交互。超链接让我们可以从多个角度来处理这个复杂问题，也许会让事情变得容易一些，但是纸张对超链接的支持还不是很好。

当我们研究每一个架构关注点时，要记住它们是研究整个系统的不同方式。例如，我们使用模块化的架构来支持不同的部署场景。同时，每个模块又是按分层架构来创建的。它们是正交的、相互穿插的考虑。每个模块都按照相同的方式来分层，每个层都可以在所有模块中找到。

实际上，我们感到非常满意，因为我们既能够保持这些关注点的分离，同时又让它们互相支持。

4.3.1　模块和加载程序

一直以来，我们都在考虑“产品家族”而不是单个“应用程序”，因为我们必须支持几种不同的部署场景，而这些场景使用同样的底层代码。特别是，我们从一开始就知道会有以下几种配置：

照相馆客户端

照相馆有2～4台这样的工作站。摄影师使用它们来参与整个工作流，从加载图像到创建订单。

照相馆服务器

每个照相馆的中心服务器上运行着MySQL，保存客户和订单等结构化数据。服务器的存储也比工作站更健壮，使用了RAID来实现可恢复性。我们也利用照相馆服务器将每天的订单刻录到DVD中。

渲染引擎

在生产过程中，我们决定构建自己的渲染引擎。渲染照相馆屏幕和准备打印的图像使用的是相同的代码，所以我们绝对可以肯定客户会拿到他们期望的照片。

最初，我们认为这些不同的部署配置只是一些不同的.jar文件集。我们创建了一些顶层目录来保存每种部署的代码，还有一个Common目录。每个顶层目录都有自己的source、test和bin目录。

没过多久，这种结构就让我们遭遇了挫折。例如，我们有一个庞大的/lib目录，里面积累了许多构建时的库和运行时的库。我们也苦于寻找地方存放那些非代码文件，如图像、颜色描述、Hibernate配置文件、测试图像等。我们中的一些人也觉得必须通过手工的方式来维护.jar文件的依赖关系是不舒服的事情。在刚开始那些日子里，我们常常发现很多包放在了错误的目录中。而在运行时，有些类又会加载失败，因为它依赖于另一个.jar文件中的类。

大约在项目的第3次迭代时我们引入了Spring[1]，这时转折点出现了。我们采用“敏捷架构”的方式：保持最小的架构，只有当避免新架构特性的代价超过实现它的代价时，才采用新的架构。这在“精益软件开发”中称为“最后负责时刻”。在此之前，我们对Spring只有一些不全面的了解，所以我们决定不依赖它，但是我们都预计后来会用到它。

当我们加入Spring时，.jar文件的依赖问题被配置文件问题放大了。每个部署配置都需要它自己的beans.xml，但大约超过一半的beans会在这些文件中重复出现，这明显违反了“不要重复你自己”[2]的原则，而且是缺陷的必然来源。所有人都不应该手工同步几千行XML文件中的bean定义。此外，几千行的XML文件本身难道不是一种坏味道吗？我们需要一个解决方案，让我们能够模块化Spring的beans文件、管理.jar文件的依赖关系、使库贴近使用它们的代码，并管理构建时和运行时的classpath。

ApplicationContext

学习Spring就像探索一片广阔的、不熟悉的地域。它是框架中的NetHack游戏，他们考虑了所有的事情。浏览javadoc文档常常有巨大的收获，在这个项目中，当我无意中看到“应用上下文”类时，我们发现了非常有价值的东西。

所有Spring应用程序的核心都是“bean工厂”（bean factory）。bean工厂支持按名字查找对象，根据需要创建对象，并向其他bean注入配置和引用。简而言之，它管理了一些Java对象和它们的配置。最常用的bean工厂实现会读取XML文件。

应用上下文扩展了bean工厂，提供了一项关键的功能：创建嵌套的上下文链，就像《Design Patterns》（设计模式）（Gamma等1994）一书中描述的“职责链”模式。

ApplicationContext对象正是我们所需要的：将我们的所有bean分开，放到多个文件中，将每个文件装入各自的应用上下文。

然后我们需要利用一种方法来建立应用上下文链，最好是不用巨大的shell脚本。

模块依赖关系

将每个顶层目录看成一个模块，我认为让每个模块包含它自己的元数据是很自然的。通过这种方式，模块只需要声明它用到的classpath和配置文件，并声明它需要哪些其他模块。

我为每个模块提供了自己的manifest文件。例如，下面是StudioClient模块的manifest文件：

Required-Components：Common StudioCommon

Class-Path：bin/classes/lib/StudioClient.jar

Spring-Config：config/beans.xml config/screens.xml config/forms.xml

config/navigation.xml

Purpose：Selling station.Workflow.User Interface.Load images.Burn DVDs.

这个格式显然是源自于.jar文件中的manifest文件。我发现，使用熟悉格式来表示概念上的“manifest文件”是很有用的。

请注意，这个模块使用了4个独立的bean文件。按功能分离bean的定义有额外的好处。它减少了对主配置文件的修改和冲突，而且提供了很好的关注点分离。

我们的团队很喜欢自动生成文档，所以我们在构建过程中包含了一些报告生成步骤。由于所有模块依赖关系明确写入了manifest文件，所以在自动化构建中添加报告生成步骤是很容易的。只需要进行一些文本解析，然后提供给Graphviz，生成图4-2中的依赖关系图。

图　4-2：模块和依赖关系

有了这些manifest文件，我们只需要通过一种方法来解析它们，然后做一些有用的事。

我写了一个加载程序，你可以猜到它就叫“加载程序”（Launcher），来做这些事情。

加载程序

我看到过许多桌面Java应用，它们带有大量的shell脚本或批处理脚本来定位JRE，设置环境变量，设置classpath等。天啊！

对于给定的模块名，加载程序将解析manifest文件，构建模块依赖关系的传递闭包。加载程序很小心，不会将一个模块加载2次，同时它也将一组偏序关系变成全序关系。图4-3展示了StudioClient的全部依赖关系。StudioClient同时将StudioCommon和Common声明为依赖包，但加载程序只会将每个包加载一次。

图　4-3：StudioClient的依赖关系

为了避免来自于主机环境的classpath“污染”（构建机器上的ANT，或工作站上的JRE classpath），加载程序从组合的classpath中创建它自己的类加载器。加载程序将配置路径传递给初始化程序，由初始化程序来创建所有的应用上下文对象。当应用上下文创建好了之后，我们就可以启动并运行应用了。

OSGi框架

当我们在2004年下半年开始这个项目时，OSGi框架刚开始得到较多的关注，这要感谢Eclipse采用了它。我们简单地看了一下，但没有采用它，因为缺少大量可以获得的知识、专家和指导。

但是OSGi的目标却很符合我们所面临的问题。相同的代码集支持多种部署配置，管理模块间的依赖关系，以正确的顺序激活它们……很明显解决的是相同的问题。

我想我们之所以没有使用OSGi，部分原因是因为时间紧迫，部分原因是因为我们不愿意采用技术风险更大的东西。我一般会赞成“获取并集成”，而不是“自己来搞一套”，但似乎有一个盈亏平衡点：很少支持的开源项目和较弱的社区与众所周知的、广泛采用的开源项目相比，具有更大的风险。类似地，我倾向于避免准开源框架，它们实际上是供应商公会。他们服务的社区通常是供应商社区，而不是用户社区。

那时我们不清楚OSGi属于哪一种类型。如果我们今天来做这个项目，我想我们可能会使用OSGi，而不是自已搞一套。

在这个项目的过程中，我们数次重构了模块结构。manifest文件和加载程序提供了巨大的帮助，让我们只需进行小小的改变。我们最后得到了6个非常不同的部署配置，它们都由同样的结构来支持。

这些模块都有着类似的结构，但它们不一定是完全一样的。这是这种方法的副作用之一。每个模块都可以隐藏其他模块不关心的部分。

[1]http://www.springframework.org/.

[2]参见Andrew Hunt和David Thomas的《Pragmatic Programmer》（Addison-Wesley Professional）。

4.3.2　自助服务终端风格的GUI

照相馆合伙人选择的标准是能够很好地使用照相机，并与家庭（特别是孩子）合作，而不是具有很好的计算机技能。在家里，他们可能是Photoshop专家，但在照相馆里，没有人希望他们成为强大的用户。实际上，在忙的季节，照相馆可能会请一些临时工。因此，能够快速上手是很重要的。

一名架构师也承担UI设计师的工作。他总是对界面有着清晰的愿景，即使我们并不总是对实现它的可行性持一致意见。他希望用户界面是友好的、可视的。不会有菜单。用户应该通过直接操作与图像打交道。大的像糖果包装一样的按钮让所有的选项一目了然。简而言之，工作站看起来应该像一个自助服务终端。

剩下来的决定就是选用哪种技术来实现显示。

我们团队中的一个人调查了可用的Java富UI技术，包括主流技术和边缘技术。我们希望找到一个好的声明式UI框架，能够帮助我们避免在与Swing的搏斗中无尽地艰难前行。结果让我们很震惊。

在2005年，即使是Java诞生十年之后，也只有两种基本选择占据了主流：XML地狱或GUI构建工具生成的混乱代码。基于XML的各种技术或多或少地直接将Swing组件映射为XML实体和属性。这对我们没有什么意义。GUI改变要求发布新版本的代码，不论这种改变是直接用Java代码实现的还是用XML文件实现的。为什么要在脑子里记住两种语言（Java和XML schema）？为什么不只使用Java？另外，作为一种编程语言，XML显得很笨拙。

GUI构建工具以前曾对我们都造成过伤害。没有人想将业务逻辑编织到JPanel中嵌入的动作监听者中。

我们很不情愿地采用了纯Swing GUI，但是使用了一些基本原则。经过了几次在本地Applebee的午餐之后，我们想出了一种使用Swing的创新方式，同时又不会陷入其中。

4.3.3　UI和UI模型

典型的分层架构包含“表示层”、“业务领域层”和“持久层”。在实践中，代码的重心集中到了表示层，业务领域层变成了简单的数据容器，持久层则转变成对框架的调用。但在同时，一些重要的信息在各层之间重复出现。例如，姓名的最大长度会体现为数据库中列的宽度，可能也是业务领域中的一项验证规则，还是UI中JTextField的一项属性设置。

同时，表示层嵌入了这样的逻辑：如果选中了这个复选框，那就让这4个文本字段也生效。这看起来是关于UI的论述，但它实际上反映的是一些业务逻辑：如果客户是“肖像俱乐部”的会员，应用需要记录下他们的会员编号和到期时间。

所以在典型的三层架构中，一种类型的信息会散布在各层中，而另一种类型的重要信息存在于GUI控制逻辑中。

最后，我们的解决方案是将GUI与业务领域层的正常关系颠倒过来。我们让业务领域层负责将屏幕的可视外观与它的值和属性的逻辑操作分离开来。

表格

在这个模型中，表格对象将一个或多个领域对象的属性表示成有类型的属性。表格管理着领域对象的生命周期，也负责调用底层事务和持久的外观方法。每个表格代表了一整屏的交互对象，虽然在少数情况下我们使用了子表格。

但这里的技巧在于，表格是完全不可视的。它不处理UI组件，只包含对象、属性以及这些属性之间的交互。UI可以将一个布尔属性绑定到任何类型的UI表示和控制组件上：复选框、标记按钮、文本框或标记开关。表格并不关心表示。它只知道它有一个属性，取值可以是true或false。

表格从不直接调用屏幕对象。实际上，绝大多数表格甚至不知道它们屏幕上的具体类。表格和屏幕间的所有通信都通过属性和绑定来完成。

属性

不像一般的基于表格的应用，表格暴露出来的属性不只是Java的原生类型或像Java.lang.Integer这样的基本类型。属性包含了一个值和这个值的元数据。属性可以回答它是单值的还是多值的，它是否允许空值，它是否是启用的。它也允许注册监听者对象来监听它的变化。

表格及其属性对象为我们提供了用户界面的清晰模型，同时我们又不必处理实际的GUI组件。我们把这一层称为“UI模型”层，如图4-4所示。

属性的每个子类处理一种不同类型的值。具体的子类有自己的方法来访问其值。例如，StringProperty有getStringValue（）和setStringValue（String）方法。属性值总是对象类型，而不是Java原生类型，因为原生类型不允许出现空值。

图　4-4：分层架构

属性类似乎可以无休止地膨胀下去。如果我们为每个领域对象类创建一个属性类，肯定会出现这种情况。多数时候，表格不会直接暴露出领域对象，而是暴露出代表该领域对象不同方面的多个属性值。例如，客户表格暴露出StringProperty对象来表示客户的姓、名、地址、城市和邮编。它也暴露出一个DateProperty来表示客户会员资格的到期时间。

某些领域对象在应用这种方法时会让人觉得很奇怪。连接一个滚动条来控制图像的缩放，或将图案中的图像嵌入到底层的几何图形上可能需要超过半打的属性。让表格具有这么多属性，目的只是为了拖动一个滚动条，这明显是不好的代码味道。另一方面，添加另一种属性类型似乎又在朝着类型膨胀的方向前进。

最后，我们进行了折中，引入了一个对象属性来保存任意类型的Java对象。关于这个类的讨论意见包括“一步错，步步错”和“垃圾场”。幸运的是，我们对这种类进行了检查—我觉得这是类型检查语言的一种危险。

我们创建了一种“命令属性”来处理动作，它封装了一些命令对象，也表明功能启用。因此，我们可以将命令属性对象绑定到GUI按钮，利用属性的启用值的变化来启用或禁用按钮。

UI模型让我们能够将Swing保持在UI层之内。它也为单元测试提供了巨大的好处。我们的单元测试可以通过UI模型的属性来驱动UI模型，并断言这些动作所导致的属性改变。所以，表格本身是不可视的，但它们暴露出命名的、强类型的属性。在某个地方，这些属性必须与可视控件联系起来。这就是绑定层的工作。

绑定

属性针对的是它们的值的类型，而绑定针对的是单个的Swing组件。屏幕创建它们自己的组件，然后注册一些绑定，将这些组件和底层表格对象的属性联系起来。每个屏幕并不知道它所处理的具体表格类型，表格也不知道与它联系在一起的具体屏幕类型。

绝大多数的绑定都会在每次GUI变化时更新它们的属性。例如，文本字段会在每次击键时更新。我们使用立即验证的方式来提供经常的、细致的反馈，而不是让用户输入一串不正确的数据之后再弹出一个对话框。

绑定也负责处理从属性的对象类型到组件的合理视觉呈现之间的转换。所以，文本字段绑定知道如何将整型、布尔型和日期型转换成文本（以及反向转换）。但并不是所有的绑定都能够处理所有的值类型。例如，把图像属性转换成文本字段是没有什么意义的。我们确保所有不匹配的情形在应用启动时都能发现。

当我们构建了这个属性绑定的第一次迭代时，发生了一个有趣的插曲。我们尝试的第一个屏幕是客户注册表格。客户注册是相当简单的，只是一些文本字段、一个复选框和几个按钮。第二个屏幕是相册屏幕，在视觉和交互方面的要求更高一些。它使用了多个GUI组件：2个验证表单、一个大图像编辑器、一个滚动条和几个命令按钮。即使这样，表格也负责通过属性来决定所有的选择、可见性和功能启用。所以相册表格对象知道验证表单的选择会影响中央的图像编辑器，但屏幕对象是不知道的。

一个够了吗？

在某些屏幕上，验证表单允许多项选择；在另一些屏幕上，只允许一项选择。更糟糕的是，某些动作只有在选择某一个缩略图时才允许执行。哪个组件负责决定哪个选择模型适用、何时根据选择来启用其他命令？这明显是UI逻辑，所以它属于UI模型层。也就是说，它属于表格。UI模型永远不应该引入Swing类，那么表格如何能够在不涉及Swing代码表达它们关于选择模型的意见呢？我们决定，没有理由让GUI组件只有一个绑定。换言之，我们可以对组件的某一个方面做多个绑定，这些绑定可以关联到不同的表单属性。

例如，我们常常有不同的绑定，代表组件的内容和它的选择状态。选择绑定可以将组件配置为单项选择和多项选择，这由绑定属性的数目来决定。

虽然花了很长时间来解释属性-绑定架构，我还是认为这是Creation Center最优雅的部分之一。从本质上来说，Creation Center是高度可视化的，带有丰富的用户交互。它的领域是创建和操作照片，所以它不是灰色的、基于表格的业务应用！然而，从一小组容易理解的对象出发，让每个对象都由单一的行为定义，我们组建了非常动态的界面。

客户应用最后支持了拖放、图像中的部分选择、随时图像缩放、主从列表、表格和双击激活等功能。我们从未破坏属性-绑定架构。

应用程序外观

创建强大的领域模型容易犯一个典型的错误。表示层（在这个例子中就是UI模型层）常常与领域模型太过亲密。如果表示层遍历了领域模型中的关系，那么就很难改变领域模型了。像大多数敏捷团队一样，我们需要保持灵活，我们不能允许自己的设计决定导致灵活性随时间的推移而降低。

Martin Fowler的“应用程序外观”模式正适合解决这个问题（参见这一章末尾的第4.6节）。应用程序外观只将领域模型的一部分暴露给表示层。表示层不会跟踪领域对象的关系图，而是让应用程序外观来帮助实现遍历、生命周期管理和激活等操作。

每个表格定义了一个对应的外观接口。实际上，按照应该由客户（而不是提供者）来确定接口的格言，我们将这个外观接口放到了表格的包中。表格要求外观来查询领域对象、建立领域对象之间的关系并将它们持久。实际上，这些外观管理着所有的数据库事务，所以表格从不知道事务边界。

在表格对象和外观对象之间的这些接口，也成为隔离对象、进行单元测试的理想场所。要测试某个表格对象，单元测试会创建一个模拟对象（mock object）来实现外观的接口。测试让模拟对象向表格对象提供一组预期的结果，包括很难通过真正的外观对象再现的错误情况。我想我们都把模拟对象看成是一种双面承诺：尽管我们让单元测试变得可行，但有时候仍然觉得让测试和表格的实现绑得这么紧是不对的。例如，模拟对象必须事先录制预期的方法调用顺序和准确的参数。（新的模拟对象框架更灵活一些。）结果，即使外部可见的行为没有改变，表格内部结构的变化常常也会导致测试失败。在某种程度上来说，这只是你使用模拟对象的代价。

所有的Creation Center应用程序，包括在照相馆和打印工厂中使用的，都使用相同的层。将GUI从驾驶室中挪开让团队不必与Swing进行无休止的搏斗。反向控制也提供了一种统一的结构，每个应用程序，每一对程序员都遵守它。虽然我们创造的架构超出了通常的“三层蛋糕”，但它在分离关注点方面相当有效：Swing限制在UI部分，领域交互在表格中完成，持久在应用外观中实现。

4.3.4　可以互换的工作站

当摄影师完成一段拍摄之后，她可以使用任意一台工作站。她通常会当时就完成这个客户的工作，这取决于照相馆当时的忙闲状况。但是，客户常常也会稍后再回来，甚至可能改天再来。让某个用户只能使用某台工作站是很奇怪的，这不仅在安排上行不通，而且也有风险。工作站可能失效。

所以照相馆中的所有工作站必须能够互换，但是“可互换”带来了一些问题。每次拍摄的图像可能占用将近1GB的空间。

我们简单考虑了一下将工作站连成一个P2P网络，进行分布式复制。最后，我们采用了更为传统的客户端-服务器模式，如图4-5所示。

图　4-5：照相馆部署

与客户端相比，服务器装有多个大容量硬盘，它们构成RAID，保证安全。服务器上运行着MySQL数据库，存放结构化的数据，记录客户、各次拍摄、订单等。但大部分空间用于存放客户的照片。

因为照相馆的距离很远，而且其中的合伙人不精通技术，所以我们知道把基础设施隐藏起来是很重要的。工作人员应该永远不必查看文件系统、分析失效原因或重启任务。他们当然应该从不登录数据库服务器。最坏的情况下，如果网线被碰松了，当它插回去时，所有工作都应该恢复正常，并且应该从临时的问题中自动恢复。

考虑这一点，我们设计了系统和应用的架构。

图像库

要让工作站可以互换，最基本的功能就是自动传输图像，包括从摄影师加载图像的工作站到服务器，以及从服务器到另一台工作站。

照相馆客户端和照相馆服务器都使用了一个核心组件，名为图像库。它负责处理所有的存储、加载和记录图像的工作，包括图像的元数据。在客户端，我们建立了一个本地的、带缓存的、后写入的代理。当调用者请求一张图片时，客户端图像库要么直接从本地缓存中取出它，要么将文件下载到本地缓存中，再取出它。不管用哪种方式，调用者都不必了解具体的细节。

类似地，当在客户端添加图像时，客户端图像库会将它上传到服务器。我们使用一个后台运行的线程池来进行传输，这样用户就不必等待上传。

客户端和服务器都大量运用了多线程。我们创建了一个锁系统，名为“预订”。预订是协作锁的软件形式。当客户想在图像库中添加一幅图像时，它必须先请求并持有“写预订”。通过这种方式，我们可以保证在我们发出该预订时，没有别的线程在读取图片。自然地，读取程序必须获得“读预订”。

虽然我们没有实现分布式事务或两阶段提交，但在实践中，在客户端图像库确认一次写预订和服务器端确认对应的写预定之间，只有很短的时间。当第二次预订确认时，我们就确信能够避免文件冲突了。

在实践中，甚至锁竞争也是很少见的。只有两个摄影师在两个不同工作站访问同一个客户的照片时才会出现。但是，每个照相馆有几台工作站，每个工作站上有许多线程，所以小心一点是值得的。

NIO图像传输

显然，剩下的问题是将图像从客户端传输到服务器。早期我们曾考虑并否决的一种可选方案是CIFS，即Windows共享驱动器。这里我们主要考虑的是容错，但我们也担心传输速度。这些机器需要来回转输许多数据，摄影师和客户要坐在那里等着。

在我们列出的所有现成的可选方案中，没有哪种方案可以同时确保速度、并行、容错和信息隐藏。虽不情愿，我们也只有决定创建自己的文件传输协议，这将我们带入了Creation Center最复杂的部分。图像转输变成了严峻的考验，但我们最后得到了整个系统最健壮的一项功能。

我以前对Java NIO曾有过一些经验，所以我知道我们可以利用它来实现一种非常快速的图像转输机制。实现NIO数据传输本身并不是太困难。我们使用常见的领导者-追随者（leader-follower）模式来提供并发，同时保证NIO选择器在一个线程中执行。

虽然该协议的实现并不困难，却有一些细小的地方需要处理：

·套接字的两端都可以关闭它，特别是在客户端进行缓存的情况下。示例代码没有对这种情况提供合适的处理。

·在处理IO事件时，SelectionKey仍将发出信号，表明它已准备好。如果你不清楚来自这个SelectionKey的兴趣集的操作，就可能导致多个线程调用同一段处理程序。

·领导者必须处理所有对SelectionKey的兴趣集的变更，否则就会在Selector上产生竞态条件，所以我们必须创建一个队列来存放待执行的SelectionKey变更，领导者线程将在调用select之前执行这些变更。

处理这些小细节导致了不同对象之间的耦合比我预期的要多。如果我们当时是要设计一个框架，这个部分就需要更多地关注松耦合。但对于一个应用程序，我们觉得这是可以接受的，可以将服务器上这些操作的对象看成是一个内聚的单元。

当我们运行一个包嗅探器，想看看是否真地获得了最大可能的吞吐量时，出现了特别有趣的一幕。我们没有做到。最初，当接收方从有数据的套接字中读取数据时，它会读满一个缓冲区，然后返回。我们猜想，如果数据多于8192字节，不用等多少时间就会继续读取后面的数据。事实表明照相馆的网络相当快，在下一个线程再次调用处理程序之前就会填满服务器的TCP窗口，所以实际上每次传输都会停顿几乎一半的时间。我们在响应代码中添加了一个循环，它会不断读取，直到缓冲区读空为止。这使得传输时间几乎缩短了一半，而且减少了线程切换和调度的开销。我觉得这很有趣，因为它只在快速、低延迟的网络中有效，而且要在客户端总数较少的时候。对于网络延迟较高或客户端较多的情况，这种处理方式可能会导致某些客户端饥饿。同样，这是在我们的环境中有效的一种折中。

单元测试和代码复审

这个NIO文件服务器让我发现进行大组复审是很有帮助的，即使是在一个完全进行结对编程的敏捷项目中也是如此。

我的结对伙伴和我在大多数迭代中都一起处理多线程、锁和NIO机制。我们尽自己所能进行了单元测试，但在多线程和低级套接字IO之间，我们发现很难对代码产生自信。所以我们执行了另一件最佳实践：我们让更多的眼睛来关注它。但是我们称之为特例。我们对不能够编写足够的单元测试进行了补偿。

一般来说，在所有时间里让两双眼睛盯着代码，可以提供代码复审的所有好处。将结对编程与自动格式化和代码检查结合，就会使代码复审的价值小于它的代价。如果你能在不花代价的情况下获得好处，那为什么还要进行代码复审？

我们在工作室里放了一个投影仪，通过A/B开关连接到两台电脑。当我们有一项技术要展示，或有一个设计模式要分享时，我们就会在午餐后打开投影仪，走查一些代码。这在项目的早期非常方便，尤其是让架构和设计变得更流畅，以及我们在学习如何使用Spring和Hibernate时。这也有助于共享Eclipse的实践和技巧。

投影仪对于迭代展示也很方便。我们可以让所有利益相关人坐在房间里，不必挤在一个屏幕前面。

（更别说在墙上投影有趣的YouTube视频了。）

我知道，快速构建易碎的东西并不难。真正的挑战是让它变得健壮，特别是整个网络都在几百英里以外的照相馆中。这个网络不能让我们远程登录来调试问题，或在失效后清理现场。这个网络要面对小孩、分心的父母，服务器放在学步儿童视线的高度。想想这个不友好的环境！通过电缆传输数据是不够的，我们需要自动的文件传输并保证成功。第一层保障是协议本身。对于“put”操作（将文件从客户端上传到服务器），请求的第一个数据包中包含了文件的MD5校验和。当客户端发出最后一个数据包后，它等待来自服务器的响应。服务器的响应代码包括：OK、TIMEOUT、FAILED_CHECKSUM或UNKNOWN_ERROR。如果收到的响应代码不是OK，客户端就会重发整个文件，我们称之为“快重试”。客户端在传输失败之间要经过3次快重试。

文件传输的问题分成两类。一类是“快速短暂的”，这种问题会自己消失，如网络错误。另一类要求人的干预。这意味着问题不会在几毫秒内消失，或者需要几分钟到几小时来修正。反复重试快速文件传输是没有意义的。如果几次尝试都无效，就不太可能在很短的时间内恢复正常。

因此，如果客户端经过3次快重试仍然失败，就会将文件放到一个文件传输任务队列中。一个后台任务会每20分钟执行一次，检查受阻的文件传输任务。它会再次尝试每个任务，如果还是失败，它会将任务放回到队列中。利用Spring的任务调度支持，这种“慢重试”是很容易实现的。

这种混合快重试和慢重试的策略让我们能够分离服务器和客户端的维护和支持。在升级或替换时，不需要“冷启动”整个照相馆的机器。

快速和健壮

本地和远程图像库及其相关文件传输机制变成了一项相当艰难的任务。但当它完成之后，整个系统向服务器上传图像的速度比从存储卡中读取的速度更快。下载到另一台机器的速度也相当快，用户根本注意不到背后的操作。在从一个屏幕转到另一个屏幕时，客户可以下载整个相册的所有缩略图。

下载屏幕大小的图片尺寸的图像进行完整显示是在点击鼠标时进行的。这样的速度让用户不必费时等待“加载中”对话框。

4.3.5　数据库迁移

想象一下操作600个远程数据库服务器，它们跨了4个时区。它们也可能在一个荒凉的小岛上，而且从数字化的角度来说，它们就是如此。如果数据库管理员需要手工进行变更，他就必须走遍几百个地方。

在这种情况下，一种选择就是在第一次发布之前将数据库设计做得完美无缺，然后再也不改动它。也许还有一些人认为这是可能做到的，但我们团队中的成员肯定都不是这样想的。我们预期在各个层面上都会发生变更，包括数据库，我们甚至盼望着这些变更。

另一种选择就是将发行版声明送到现场。在执行安装时，照相馆经理总是会打电话到服务台，要求一步步的电话指导。也许我们可以将SQL脚本包含在发行版的CD中，让他们输入或复制/粘贴。想到指导他们输入命令时要从“现在输入mysqladmin-u root-p……”开始，我就感到背脊发凉。

最后，我们决定进行自动化数据库更新。Ruby on Rails称之为“数据库迁移”，但在2005年，这还不是一项常见的技术。

将更新作为对象

照相馆的服务器定义了一个bean，名为“数据库更新器”。它保存了一组数据库更新对象，每个对象代表了对数据库的一次原子变更。每个数据库更新对象知道它自己的版本，也知道如何作用于数据库。

在启动时，数据库更新器检查一个表，得知数据库当前的版本。如果它没有找到该表，它就认为没有更新或更新还没有执行。因此，第一次更新将建立这个版本表，并在其中填入一行数据。这行数据包含了一个版本号和一个锁字段。为了避免并发更新，数据库更新器首先会更新这一行，设置锁字段。如果不能设置，那它就认为网络中有别的机器正在执行更新。

我们使用这个迁移功能来执行一些简单的更新和复杂的更新。有一次简单的更新只是为一些影响性能的列加上索引。有一次更新让我们很紧张，它要将所有表的类型从MyISAM改为InnoDB。（MyISAM是默认的表类型，不支持事务和引用完整性。而InnoDB支持。如果我们在发布第一个版本之前就知道这一点，我们开始就会使用InnoDB。）由于我们已经部署了带有生产数据的数据库，所以必须使用一系列的“alter table”语句。它工作得很好。

在送到现场的几个发行版中，我们大约进行了10次更新。没有一次失败。

日常工作

每次我们执行构建时，都会将本地开发数据库恢复到版本0，然后向后升级。这意味着我们每天都会将升级机制执行数十次。

我们还对每次数据库更新进行单元测试。每个测试都会在更新之前对数据库的状态做出一些断言。然后它执行更新，再对之后的状态做出一些断言。

当然，这些测试都是针对“好行为”的数据进行的。但现场会发生奇怪的事情，真实的数据总是比任务测试数据集更乱。我们的更新会创建表，添加索引，填充数据行，以及创建新列。如果数据不是我们期望的那样，有时候这些变更会导致糟糕的结果。由于我们担心更新时的这种风险，所以要寻找一些方法让这个过程更可靠。

安全特性

假定在一次更新中出了一些问题。照相馆就会关门歇业，直到经营者找到一种方法来恢复数据库。如果更新真出了问题，就会让数据库处于某种冲突状态或中间状态。这样照相馆甚至都不能回到应用的前一个版本。为了避免这种灾难性的场景，数据库更新器在开始执行升级之前会先对数据库进行备份。如果不能够完成备份，它就会停止更新过程。如果在更新时发生错误，更新器会自动尝试装入原来的备份。如果这一步也失败了，那么至少还留有一个备份，技术支持人员会告诉照相馆经理进行手工恢复。

实际上，在最坏的情况下，打印工厂总有数据库的一份副本，它与最新数据库的差异不会超过一天。我们使用每日DVD的空闲空间来发送数据库的完整副本。这要求数据库较小而存储空间很多。

现场效果

我们花在自动化数据库更新上的时间有几个方面的回报。首先，我们通过一些早期的更新改进了性能和可靠性。在那个发行版之后，用户的积极反馈马上就来了。其次，运维小组非常喜欢容易部署新版本这个特征。以前的系统要求照相馆来回寄送可移动硬盘，这自然会伴随所有物流方面的问题。最后，有了这种更新机制，我们就可以关注于“刚好够”的数据库设计。我们不会预测未来，也不会过度设计数据库schema。相反，我们只设计足够的schema，支持当前的开发迭代。

4.3.6　不可修改的数据和处处使用的GUID

照相馆合伙人与客户一起，创建一些构图，利用多张照片插入某个设计之中。这些设计来自于公司总部的一个设计小组。一些设计是长期的，另一些设计是季节性的。各种设计的圣诞卡销量很大，至少在圣诞节之前的几周内。一旦过了圣诞节，需求就直线下降了。一个具体的设计包含了一些作为背景的图像，还包含了一段描述，说明有多少空位放基本的图像以及这些空位的几何形状。合伙人可以非常创造性地用照片或其他构图来填充这些空位。

在处理这些设计和放入其中的基本图像时，我们遇到了一些有趣的挑战。例如，在客户下了订单之后，该设计的新版本又发到了照相馆，会产生什么情况？从较小的范围来说，如果合伙人将一个设计嵌套在另一个设计之中（例如将一张深褐色调的照片放在一个边框里），然后又改变或删除了原来的设计，你会怎么办？

最初，这看起来像是一个引用计数或隐式链接的噩梦。我们考虑的每一个创建对象引用网的策略都会导致间断丢失图像或让人吃惊的变化。作为一个团队，我们都相信“最小吃惊法则”，而隐式链接会导致变更从一个产品波及另一个产品，所以不会有好结果。当我们中最有想象力的同事得到了一个简单而清晰的答案时，没过30秒就得到了大家的一致赞同。这个解决方案包括两条规则：

1.在创建之后就不要改变。设计和构图是不可修改的。

2.复制原来的设计，不要引用。

把这两条规则结合在一起，这意味着选择一个设计实际上就是把这个设计复制到了工作空间。如果合伙人把由此产生的构图加到了相册中，那么它就是添加的设计的一个完整的、自包含的副本。同样，将一张处理过的图像嵌套在另一个图像中也会将原来的图像复制并组合到新的构图中。从组合发生之时开始，原来的构图和新的构图就是完全相互独立的。这些副本不只是内存中对象引用的一个技巧。对构图的实际XML描述也包含了完整的设计副本或嵌入的构图。这个描述存在于照相馆的数据库中，与DVD中发送的描述是一样的。当照相馆经理将当天的订单刻成DVD时，StudioServer就将创建最终效果所需的全部内容都进行打包：源图像、背景、alpha遮罩，以及如何将它们组合成最终图像的指令。在DVD上包含整个构图的完整描述（包括设计本身）为生产带来了很大的好处。

以前的系统将设计保存在一个库中，订单通过ID来引用设计。这意味着设计者必须同步照相馆和集中式打印工厂的所有设计ID。所以，设计必须先在生产系统中“注册”，然后才能分发到照相馆。一旦ID没有同步（这种情况有时会发生），错误的设计就会打印出来，客户就不会拿到他们期望的照片。同样，不论设计者何时更新一个设计，可能有几天的DVD使用了该设计的老版本。有时候效果很好，有时候不好。

在新系统中，设计永远不需要注册。进入XML中的内容就会打印出来，这让设计者可以自由地进行更频繁的更改，并随时分发设计。新版本的设计不会影响到已有的订单，因为每份订单都是自包含的。当新版本到达照相馆时，它就会开始出现在订单流程中。唯一不复制的是图像文件本身。它们太大了，所以我们为每个图像（不论是设计的一部分还是照相馆中拍摄的图像）分配了一个GUID。作为一项规定，当图像得到一个GUID后，它就不可修改了。在准备将订单刻录到DVD上时，StudioServer会遍历订单收集GUID（利用有争议的Visitor模式）。它将找到的每个图像添加到DVD中，包括客户的照片和设计背景。

4.3.7　渲染工厂

StudioClient帮助合伙人利用基本图像创建增强的肖像。这些增强的肖像可以是简单的深褐色效果或黑白效果，目的是让肖像看起来更戏剧化，也可以是复杂的多层结构，带有alpha遮罩的背景、文字和柔焦。不论是哪种效果，照相馆的工作站都不会得到最终渲染的图像。打印工厂拥有各种打印机，支持不同尺寸和分辨率。他们可以自由地更换打印机或在任何时候将任务移到其他打印机上。照相馆对于如何得到打印好的图像了解得并不多。

当那些每日生成的DVD到达时，它们被载入生产控制系统（PCS）。PCS负责做出所有决定，包括何时为订单渲染图像，何时打印，使用哪一台打印机打印等。另一个独立的团队负责开发PCS，他们处于另一个时区的某个地点。以前的项目试图与PCS紧密集成，这导致了大量的冲突。所有团队都怀着良好的意图在工作，但沟通的困难让各个团队都慢了下来。我们需要避免这种冲突，所以我们决定主动应用Conway法则（在下一节中定义），在软件中明确创建一个接口，让大家知道团队工作的边界在哪里。

增量式架构

在敏捷社区中一个反复出现的问题就是：“你应该事先创建多少架构？”某些杰出的敏捷思想家会告诉你：“一点也不需要。无情地重构，然后架构就会出现。”我还没有达到这个境界。

重构在不改变代码功能的前提下改进了代码的设计。但是，要通过重构得到更好的设计，首先你必须知道好的设计和不好的设计。我们有一组不错的“代码味道”来指导我们，但我们不知道相应的“架构味道”。其次，必须有可能持续地改进，甚至跨越接口边界。这总是让我相信系统的基本架构必须在开始开发时就确定好。

现在，在Creation Center项目之后，我对这个答案的信心减弱了不少。我们在项目较晚的时候加入了一些主要的架构组成部分。下面是一些例子：

·Hibernate：在两三次迭代之后加入。在此之前我们不需要数据库。

·Spring：在第一个发行版进行到大约三分之一时加入。它很快就成为了我们架构的中心。我不记得在没有它时我们是如何工作的，但情况确是如此。

·FIT：在第一个发行版进行到大约一半时加入。

·DVD刻录软件：在初始开发快结束时购买并加入。

·支持分窗口的UI：在发布前最后两次迭代中加入。

在每一种情况下，我们都会先探索所有可能性，然后再做决定。我们会在“最后可能的时刻”做出决定，即不做决定的代价超过了实现该特征的代价。尽管如果一开始就用Spring，有些事情我们可能会做得不一样，但在后来加入它也没有让我们受苦。在早期的迭代中，我们关注的是发现应用想成为什么样子，而不是Spring希望我们如何构建应用。

应用Conway法则

Conway法则常常在事后被人提起，来解释那些产品中看起来似乎很随意的决定。它说出了有关开发团队的基本事实：在所有团队的边界，你会发现软件的边界。这是因为需要沟通接口而导致的。

我们觉得保持Creation Center对DVD格式和布局的完全控制是很重要的，所以我们在自己的工作范围内增加了一个程序：DvdLoader。DvdLoader运行在打印工厂中，负责读取DVD并调用PCS中的不同存储过程，添加订单、构图和图像。PCS将组合指令看成是一个透明的字符串，我们很小心地避免让PCS“打开”这个字符串中的XML。有时候这意味着我们会有重复信息，诸如对基本图像本身的依赖关系，但这对于维持一个清晰的边界来说是一种可以接受的折中。

类似地，我们定义了一个接口，让RenderEngine从PCS中取得渲染任务，同时保持渲染本身的XML描述处于Creation Center的控制之下。

我们编写了这些接口的规格说明，然后利用运行在开发服务器上的FIT来“敲定”准确的含义。实际上，我们将FIT作为这些接口的可执行的规格说明。后来发现这很重要，因为即使对于参加谈判某个接口的人来说，也会发现在他们认为同意的内容和实际构建的东西之间存在着差异。FIT让我们在开发中就消除了这些差异，不需要在集成测试或生产环境中再来消除，那样会更糟。

DVD加载

DvdLoader程序运行在打印工厂，它实际上是一个批处理器，从DVD上读出订单并将它们装入PCS。像其他程序一样，我们关注的是健壮性。DvdLoader读取一条完整的订单，验证该DVD包含了该订单的所有构成元素，然后将它加入PCS。通过这种方式，我们不会将不完整的订单或有冲突的订单加入数据库。

因为图像可能出现在许多DVD上，加载程序会利用GUID来检查是否某张图像已加载。如果没有，加载程序就加上它。这样，在需要时，订单就可以从照相馆重新发出，就算PCS已经清除了订单和相关的图像也没关系。这也意味着设计中使用的背景图像只在使用该设计的第一份订单到达时加载。

所以，DVD是自包含的，不论加载几次得到的结果都一样。

渲染管道

对于渲染引擎本身，我们使用了经典的管道和过滤器架构。“管道”是对图像渲染的自然隐喻，它将一系列复杂的动作分解成具体的步骤，让单元测试变得简单。

在从PCS取出一项任务时，渲染引擎会创建一个RenderRequest。它将这个RenderRequest送入渲染管道，其中的每一个步骤都作用于该请求。在最后一个步骤，管道将渲染好的图像保存到PCS指定的路径下。在该请求退出管道时，它只保存得到的结果，包括成功标记以及可能有的问题集合。

管道中的每个步骤都有机会报告问题，将错误信息添加到结果中。如果某个步骤发生错误，管道就会终止，引擎将问题报告给PCS。

快速失败

每个系统都有一些失败模式，唯一的问题在于你是针对失败进行设计还是任其发生。我们仔细设计“安全的”失败，特别是在生产过程中。我们绝不希望因为软件的原因而导致生产线停下来。

还有另一个方面。当客户回来取他的订单时，应该取走正确的照片！也就是说，我们交付的产品需要对产品和客户的订单进行匹配。这看起来是小事一桩，但让产品图像的渲染和屏幕图像的渲染保持方式一致是非常重要的。我们努力确保在生产使用的渲染代码与照相馆中使用的相同。我们也确保渲染引擎在生产时使用相同的字体和背景。

在我们的渲染引擎中，我们采用的哲学是“快速失败、大声失败”。当渲染引擎从PCS取出一项任务时，它会检查所有指令，验证该任务需要的所有资源都实际存在。如果该任务包含文字，那么渲染引擎就立即装入字体。如果该任务包含某些背景图或alpha遮罩，那么渲染引擎就立即装入相关的图像。如果缺了什么，它就马上向PCS报告错误并终止该任务。在渲染管道的16个步骤中，前5个都是进行验证。

在上线几个月之后，我们终于发现了一个渲染引擎没有及早检测到的错误：它没有为前面渲染的图像预订磁盘空间。某天当PCS的存储卷满了的时候，渲染任务很晚才失败，而不是尽早失败。在此之前，没有因为失败的渲染而返工的。

伸缩性

每个渲染引擎都是独立操作的。PCS不会保留现有渲染引擎的清单，每个引擎直接从PCS中取任务。实际上，引擎可以根据需要添加或移除。因为每个引擎在完成一项任务之后就立即请求新的任务，我们自动实现了负载均衡的效果，充分利用了每个引擎的能力。快速的渲染引擎以较快的速度完成任务。不同配置的渲染引擎不会引起问题。

唯一的瓶颈将是PCS本身。因为渲染引擎会调用存储过程来取得任务和更新状态，每个渲染引擎每过三五分钟都产生成两个事务。PCS运行在还算不错的Microsoft SQL Server服务器集群上，所以没有很快成为吞吐量瓶颈的危险。

4.4　用户反应

我们的第一个版本安装在两个本地照相馆中，它们的距离容易让我们产生“驱车过去除错”的冲动。合伙人的反馈很及时，也相当积极。一个照相馆经理认为新系统快了许多，而且更易用，所以她预计在假期能够多接待50%的客户。据说有一个客户曾询问在哪里能够买到这个软件的副本。我们常常听说客户直接拿起鼠标自己修饰照片。你可以想象，客户更愿意订购他们自己创建的照片。

我们在生产过程中遇到了一些缺陷，但很快就纠正了。由于我们在加载程序和渲染集群中设计的弹性，打印工厂能够很容易进行伸缩，处理来自比预期更多的照相馆的照片，同时保持很高的生产品质。

4.5　结论

我本可以花更多的时间和篇幅来描述每个类、每次迭代或每个设计决定，就像初为人父的人描述宝宝的每次打嗝和摇晃一样。但这一章浓缩了一年的努力、探索和心血。它展示了Creation Center架构的结构与功能如何从业务的基本需求及环境中浮现。通过保持关注点分离，坚持增量式设计和开发，Creation Center以一种令人满意的方式平衡了各方面的需求。

参考文献

Buschmann,Frank,Kevlin Henney,and Douglas C.Schmidt.2007.Pattern-Oriented.

Software Architecture：A Pattern for Distributed Computing,vol.4.Hoboken,NJ：Wiley.

Fowler,Martin. 1996.Analysis Patterns：Reusable Object Models.Boston,MA：Addison-Wesley.

Fowler,Martin.“Application facades.”http://martinfowler. com/apsupp/appfacades.pdf.

Gamma,Erich,et al.1994.Design Patterns：Elements of Reusable Object-Oriented Software.Boston,MA：Addison-Wesley.

Hunt,Andrew,and David Thomas.1999.The Pragmatic Programmer.Boston,MA：Addison-Wesley.

Lea,Doug.2000.Concurrent Programming in Java,Second Edition.Boston,MA：Addison-Wesley.

Martin,Robert C.2002.Agile Software Development,Principles,Patterns,and Practices.Upper Saddle River,NJ：Prentice-Hall.

第5章　面向资源的架构：在Web中Brian Sletten

原则与特性

结构

功能多样性

模块

√概念完整性

依赖关系

修改独立性

进程

自动传播

√数据访问

可构建性

√增长适应性

√熵增抵抗力

架构是有人居住的雕塑。

——康斯坦丁・布朗库西（Constantin Brâncusi）

在这一章中，我们将说明企业中聚焦信息的架构展示了与Web一样的明确特点：伸缩性、弹性、架构迁移策略、信息驱动的访问控制等。在这个过程中，业务部门有权力根据业务需求来决定资金投入和软件开发，而不是简单地因为选择容易失效的技术而要求业务部门为不断的改变付费。

5.1　简介

作为IT行业从业人员，我们很没面子，因为我们不得不承认这个令人尴尬的事实：大多数的组织机构更容易在Web上找到信息，而不是在他们自己的系统中。想一想这一点。他们更容易通过第三方、在全球信息系统中找到数据，而不是在他们自己能够完全控制和访问的环境中。这种尴尬的事情有许多原因，但最大的问题是我们倾向于在内部使用错误的抽象，过分强调我们的软件和服务，忽视了数据。这种方向性错误在一定程度上导致了业务部门对IT部门感到烦恼。我们忘记了公司并不关心软件，而只是关心它提供的特征和功能。业务部门真正想要的是一些更容易的方法，这些方法能够管理他们收集的数据，对数据进行分析，通过复用数据来支持客户和核心功能。

为什么组织机构内的信息管理与Web有着如此巨大的差别？遗憾的是，答案既与技术选择有关，也与公司策略有关。我们有一些遗留系统，使交互模式变得复杂。我们试图利用供应商的一些解决方案，而这些供应商的利益并不与我们保持一致。我们希望有银弹来解决所有的问题（尽管Brooks博士在多年前就纠正过我们的观念[1]。即使你碰巧进入了一个拥有非常合适的技术基础设施的组织机构，数据管理人和数据用户通常也会互相争夺，这抑制了信息共享。这是公司不像Web那么清楚的原因之一：似乎没有合适的动机来分享数据，尽管分享数据的需求是很明确的。最重要的信息是，有些问题与技术无关。在一定程度上，Web技术也会帮助我们绕过策略问题，因为你不总是需要特别许可才能暴露出你通过其他形式得到的信息。

好消息是我们可以借助Web来指导我们，弄明白这种寻找信息的极好环境背后的原因。在组织机构中应用这些概念将帮助我们解决这些问题，得到类似的好处，如低成本的数据管理、架构迁移的策略、信息驱动的访问控制，以及满足法律法规。Web的成功在很大程度上是因为它增大了信息共享的可能性，同时又降低了门槛。我们已经创建了一些工具和协议，不但支持世界领先的科学家之间的知识传递，而且也支持我们的祖母与家人之间保持联系，并找到他们感兴趣的内容和社区。这不是一件小事，我们应该好好考虑一下导致这些现实背后的思想所产生的影响。我们必须住在我们创建的架构里，所以我们应该建造既能满足我们的需求又能为我们带来灵感的架构。

[1]http://en.wikipedia.org/wiki/No_Silver_Bullet.

5.2　传统的Web服务

在我们开始为信息驱动的环境寻找新的架构之前，我们应该简单回顾一下最近我们如何构建了类似的系统，怎样才能做得更好。在最近10年里，我们一直在推销企业级架构的愿景，这种架构是建立在可复用的业务服务的基础之上的。我们需要提醒自己，Web服务的目标是要成为一种业务策略，成为一种方式，让我们能够在一些地方定义功能，在任何地方、以各种编程语言异步地访问这些功能。我们希望能够在不影响客户的情况下升级这些服务。遗憾的是，与这个目标有关的不断变化的技术组合让人们很迷惑，而且也没有解决在真实组织机构的实际架构中所面临的问题。我们在这个新愿景下的目标不是与以往不同，而是要增加价值，改变我们所看到的“面向服务恶化（Service-Oriented Aggravation）”的状况。

有一组技术构成了我们对Web服务的基本理解：SOAP用于服务调用，WSDL用于协议描述，UDDI用于服务元数据发布和发现。SOAP来自于一些不同的技术，包括远程过程调用（RPC）模型和异步XML消息模式（doc/lit）。第一种技术是容易失效的，伸缩性不佳，在以前的DCOM、RMI和CORBA等名称下，它的实际效果并不太好。尖括号既没有带来新问题，也没有解决老问题，我们只不过试图以这种方式构建一些粒度上错误的系统，并过早地将自己与一个明显不稳定的协议绑定在一起。第二种技术提高了艺术水平，是一种不错的实现策略，但是不太能配得上它从一开始就一直进行的交互性宣传。它甚至把简单的交互弄得更复杂了，因为它的处理过程受到解决大型交互问题的目标的影响。

doc/lit风格让我们用结构化的包来定义请求，这些请求可以在一个工作流中由一些松耦合的参与者进行转手、修正、处理和再处理。就像一颗流动的珍珠，这种信息在由中间节点和最终节点异步处理的过程中，会增加元素和属性。我们通过在一层中添加更多消息处理器来实现水平伸缩性。我们可以在合作伙伴之间和行业边界之间标准化交互方式和业务流程，这些业务流程不能包含在一个单独的环境中。它代表了一种上下文无关的请求，能够处理非常困难的交互模式。

当只靠严格的服务分解和描述（如SOAP和WSDL）被证实不足以解决我们的交互需求时，我们沿着处理栈向上，引入新的业务处理和混合层。于是标准和工具的组合使得已经过于复杂的情况变得更为复杂。当我们跨越领域和组织机构边界时，我们遇到了冲突的术语、业务规则、访问策略和非常真实的WS巴别塔。即使我们信奉这种愿景，我们也没有切实可行的迁移策略，实际上我们对可能实现的交互性撒了谎。Clay Shirky曾把Web服务的交互性总结为“乌龟叠罗汉”[1]

问题在于，当大多数人希望以语言无关和平台无关的方式来调用可复用的功能时，这些技术却成了牛刀杀鸡，它们太复杂了，也泄露了实现的细节。为了调用这些功能，你必须会使用SOAP语言。这是一个不错的实现选择，但在这个松耦合系统的世界中，我们并不总是希望仅仅为了简单的交互方式，就向客户宣传或要求客户知道这些细节。SOAP的大思路是让请求与上下文无关，并在异步的环境中维持事务完整性。但在真实系统的业务现实中，上下文被加入到请求中。首先，我们必须为请求加上唯一标识，然后是证书，然后对消息签名并为敏感的信息加密，等等。发出SOAP请求的“简单”开销受到了交互方式和业务需求的影响。如果组织机构中的某人希望取得某些信息，为什么不能直接索取？而且，当这些问题被回答过一次之后，为什么问同样问题的10个人（或100个人、1000个人）还要将同样的开销通过相同的查询加到后台系统上去呢？

这些问题凸显了传统Web服务技术栈中存在的一些抽象问题，在一定程度上解释了世界各地IT部门对Web服务技术的普遍不满。这些技术是一些实现技术，将我们的调用行为分解成由服务组成的工作流，但我们不能够仅通过服务的概念来表达组织机构的全部需求。我们不能够脱离上下文来识别和确定信息结构，这种上下文是特定调用中所用到的。当我们将自己与特定协议的请求、特定的端口和特定的机器绑在一起时，我们就丧失了松耦合和异步交互的模式，也不能够跟上数据视图的变化。如果不能够唯一地识别通过服务的数据，那么我们就不能够在某个信息层面上应用访问控制。这使得在不断网络化的世界上保护敏感的、有价值的、私密的信息这一问题变得更为复杂，更加难以应付。这里的问题并不在于SOAP和WSDL，但它们也不是完整的解决方案。在我接下来要描述的面向资源的架构中，很可能以doc/lit的方式来使用SOAP，但我们不会把它作为唯一的解决方案。如果没有必要，我们也不需要总是宣称背后使用的是这些技术。为了迈出这一步，我们需要看看Web，弄明白为什么它作为一个可伸缩、灵活、可演变的信息共享平台取得了如此的成功。实现细节对于信息消费者来说通常并不重要。

[1]http://en.wikipedia.org/wiki/Turtles_all_the_way_down.

5.3　Web

在大家的概念中，Web是以文档为中心的。具体来说，当我们想到Web时，就会想到在Web浏览器中看一些文档。但真正的神奇之处却在于公开可获得的信息之间的链接、这些链接所代表的意义，以及我们能够方便地通过创建新窗口来访问链接的内容。没有起点，也看不到终点。只要我们知道要找的是什么，我们通常就能找到它。现在已经出现了一些技术，帮助我们知道要找的是什么，即通过一些搜索引擎或某种形式的推荐系统。我们喜欢为事物命名，因为我们从本质上是面向名称的生物，我们使用名称来区别“这个东西”和“那个东西”。作为儿童，他们的早期沟通行为之一就是为感兴趣的东西命名，指向它们，索取它们。在很大程度上，Web就是这种儿童般的好奇心应用于群体智慧和愚蠢的结果。作为一种对知识永不知足的生物，我们会决定对什么感兴趣，然后就开始索要它。没有集中式的协调，我们自由地记录下我们的探索，出版我们的故事、思想和旅程。我们把Web看成是文档之间的一系列单向链接（参见图5-1）。

图　5-1：Web的传统概念

但是，链接起来的文档只是一部分。Web的愿景中总是包含了数据链接的思想。这种内容可以通过渲染的视图来展现，或在不同的环境里用合适的方式直接引用或操作它们。你可以设想有一个中间层要求XML文档格式的信息，而表示层则希望通过AJAX调用取得JSON对象。同样的名称代表了不同形式的同样数据。通过允许以这种方式指定数据，我们很容易创建分层的应用，它们包含一致的视图，即使它们要求不同的细节程度，或希望数据有不同的表示形式。应用和环境提供与消费数据的方式是松耦合的，应用不再是“在网上”，而是“在网里”。我们正走向数据的Web，它连接了人、文档、数据、服务和概念，如图5-2所示。

图　5-2：数据的Web

在这个环境中，基本的交互是逻辑上的客户端-服务器请求。我们必须有感兴趣的信息的地址。术语“统一资源定位符（URL）”指的是一种标识符，它不仅无歧义地确定了在全球地址空间的一个引用，而且告诉我们如何解释这个请求。在这个过程中，不要求我们事先懂得实现该请求的技术。这让处理过程保持简单，在面对后端变化时有弹性。当我们最喜欢的站点从静态的数据提供变成动态的数据提供，或者改变应用服务供应商时，这些动作对我们来说是不可见的。虽然许多站点在这个过程中没能够有效地处理内容安排，但至少我们能够从相同名字的地址接收不同的表示形式。我们可能希望以不同的格式取回数据，这取决于我们是用计算机发出请求还是用手机发出请求。在后面的讨论中，我们会看到如何利用这个属性来控制细节程度，以满足访问控制和满足法规的要求。

Web使用的命名机制让我们能够标识文档、数据、服务，甚至概念。我们曾经很难区分对Abraham Lincoln的引用和一篇关于它的文章。例如，站点http://someserver/abr ahamlincoln可能是两者之一。W3C技术架构组（TAG）提出了一份建议[1]，非网络可寻址的资源（如不存在于Web上，但我们仍感兴趣的东西）可以通过返回码303来说明，而不是通常的200。这相当于暗示客户端：“是的，你请求的东西是合法的，也很有趣，但实际并不存在于Web上。你可以在这里找到更多的信息……”

Web地址以HTTP协议的引用开始，接下来是响应请求的服务器的名称。在此之后，一种层次化机制反映了信息空间的一条路径。这种逻辑名称描述了数据的某种结构。多条路径可以指向相同的资源，但会在不同的场景下提供价值。http://server/order/open可以返回特定时间点未完成订单的列表，而http://server/order/customer/112345可以返回某个客户的所有未完成订单。很清楚，在这两种逻辑请求的结果中有重叠的部分。当我们不知道具体要问什么的时候，我们会寻求更一般的途径。当我们希望根据某个客户的状态进行查询时，我们就会寻求更直接的途径。我们从这些逻辑URL引用取回结果，要么从系统的其他部分获得，要么基于客户通过用户接口输入的数据来生成结果。

关注点分离是这种交互方式的关键抽象之一。我们分离了我们有兴趣讨论的东西、操作这些东西的动作，以及选择的收发它们的方式。这一点如图5-3所示，该图来自RESTWiki[2]。在“表示层状态转换（REST）”[3]架构方式中，我们会提到资源（名词）、动词和响应的表示形式。资源可以是我们关注的任何东西（包括概念）。动词是GET（取）、POST/PUT（创建/更新）和DELETE（删除）。GET操作限制为没有副作用。这被称为幂等请求（idempotent request）。这种交互的语义将有助于实现可能的缓存机制。如果没有集中授权来响应请求，或者我们还没有办法来寻址资源，通常就会使用POST（例如，在Usenet社区中提交新闻）。在创建一份订单之前，我们不能够为它提供标识符，因为服务器应用负责生成订单ID。因此，我们通常会向某些功能程序（如Servlet）POST这些请求，这些功能程序代表我们接收请求，并在此过程中生成ID。PUT用于更新和重写一个已命名资源的现有状态。DELETE在公共Web上用得不多，但在内部控制的、面向资源的环境中，声明不再需要或关心某些资源则是资源生命周期管理的重要部分。REST方式的基本工作原理是分离关注逻辑命名资源、操作资源的方式，以及选择的表示资源的格式，如图5-3所示。

这种关注点分离与SOAP服务调用的协议本质形成鲜明的对比，在SOAP服务中，请求的结构、调用的行为和返回类型的格式通常通过Web服务定义语言（WSDL）捆绑到一个协议上。协议很有用，除非我们想打破这些协议。Web服务技术栈的一个主要目标就是减少耦合，并引入异步处理模型，这样消息的处理程序就可以根据新的业务逻辑进行更新，同时又不影响客户端。WSDL绑定的方式注意到了这个目标，但做法却刚好相反。我们通常不能在不影响客户端的情况下改变一个端口的后台绑定（而这正是我们明确想避免的）。

图　5-3：REST关注点分离

面向资源的方式让我们在需要时可以强制实现协议，但这并不是必需的。通过分离事物的名称和形式结构，我们可以复用同样的逻辑名称，支持多种类型的交互。我们可以更新后台而不会影响原有的客户端。如果我们要改变一个模型，所有已有的客户端用第1版的消息结构向一个URL POST消息，那么我们可以在后台添加对第2版消息结构，同时在需要时让业务照常进行。如果我们想停止支持老的消息结构也可以，但我们可以选择什么时间这样做。这种灵活性是面向资源的架构有助于我们重新控制业务后端的原因之一：后端系统的改变不一定要求前端也更新。如果用一个RESTful接口将遗留系统包装起来，我们就可以继续使用它，直到有足够的业务理由来改变它为止。当然，其他技术也支持我们用这种方式包装遗留系统。但正是使用逻辑名称的这种一般方式让我们有更大的机会来避免中间件的不断改变，让情况变得不同。

为了努力促进水平的可伸缩性，RESTful方式要求请求是无状态的。这意味着生成响应需要的所有信息都要放在请求中。这让我们能够通过负载均衡机制，由任意多台后端服务器来处理请求。在面对增加的负载时，可以添加更多的硬件来解决问题，任何服务器都可以接收并处理请求。虽然伸缩性是这种架构方式的目标，但将无状态的请求应用于GET请求的语义还带来了另一个大的副作用：我们可以设想对任意请求的结果进行缓存的可能性。响应者的地址（URL的主要部分）加上请求的全部状态（URL层次路径加上查询参数）构成了结果集的一个复合散列键（例如，数据库查询、对一部分数据进行转换等）。你不会不花力气就享受到缓存的好处，但利用这种可能性的环境忽然就变得很容易想到了。NetKernel面向资源环境[4]的一个突出特征就是它充分而全面地利用了这种可能性带来的好处，实现了一种架构上的记忆化[5]，而你几乎不要做什么额外的工作。我们将在稍后的5.6节中进一步讨论。

对所有我们感兴趣的事物使用通用的命名机制，用一个逻辑处理过程来支持事物随时间的变化或在不同上下文中的变化，这样我们就几乎拥有了一个引领组织机构信息管理所需的基础设施。我们最后需要的一个工具就是要能够表示我们所寻找的事物的元数据。这就是“资源描述框架（RDF）”想要解决的问题。W3C的建议使用了一个图式模型，支持对命名实体信息的开放式表述。谁创建了它？何时创建的？它是关于哪方面的？它和什么有关系？我们可以命名和寻址存在关系数据库中的现有数据，这让我们可以描述任何想描述的数据，不必将它们转换成新的格式。这是普遍的预期，也是大家报怨的原因，因为RDF在实践中撑不住。我们通常会让数据留在原处，然后在有意义的层面上进行集成。

在下面的列表中，我们看到了某个RDF的N3表达式，描述了与特定资源相关的创建者、标题、版权日期和许可证。这个例子向我们展示了3个来自“都柏林核心元数据倡议”[6]的术语和1个来自Creative Commons[7]社区的术语。我们可以自由使用来自任何词汇表的术语，或在需要描述新的内容时创建新的术语：

@prefix dc：＜http://purl.org/dc/elements/1.1/＞.

@prefix cc：＜http://creativecommons.org/ns/＞.

＜http://bosatsu.net/team/brian/index.html＞dc：creator

＜http://purl.org/people/briansletten＞.

＜http://bosatsu.net/team/brian/index.html＞dc：title

"Brian Sletten's Homepage".

＜http://bosatsu.net/team/brian/index.html＞dc：dateCopyrighted

"2008-04-26T14：22Z".

＜http://bosatsu.net/team/brian/index.html＞cc：license

＜http://creativecommons.org/licenses/by-nc/3.0/＞.

我们现在不仅能够使用任何想用的术语，也可以在将来任何时候添加新的术语和关系，不影响原有的关系。这种“无schema”的方式对于修改过XML或关系数据库schema的人来说，具有很大的吸引力。它也代表了这样一个数据模型，即不仅能够在不可避免的社会、过程和技术变化中生存下来，而且拥抱这些变化。

这个RDF可以存储在triplestore或其他数据库中，通过SPARQL或类似的语言进行查询。大多数支持语义的容器现在都支持这种存储和查询RDF的方式。包括Mulgara Semantic Store[8]、Sesame Engine[9]、Talis Plateform[10]、甚至Oracle 10g以后的版本。图中的节点可以通过模式匹配条件来选择，所以我们可以针对资源问这样的问题：“谁创建了这个URL？”“给我看Brian创建的一些东西。”或“列出所有最近6个月中创建的Creative Commons许可证的素材”。表示“由谁创建”“具有哪种许可证”这样的术语是在相关的词汇表中列出的，但很容易转成我们所提到的目标。数据模型的灵活性以及查询语言的表达能力使得描述、发现和调用RESTful的服务变得相当简单。这当然比通过UDDI这样的迟钝的、高阻抗的技术来发现和调用服务要舒服得多。

能够寻址和解析任意资源，能够以不同格式取得这些资源，能够以开放世界和混合词汇的方式描述它们，我们就可以将这些思想应用于企业了。我们将描述一种信息驱动的架构，支持在数据组成的网上“冲浪”，就像在文档组成的网上“冲浪”一样。

[1]http://lists.w3.org/Archives/Public/www-tag/2005Jun/0039.

[2]http://rest.blueoxen.net/cgi-bin/wiki.pl?RestTriangle.

[3]http://en.wikipedia.org/wiki/REST.

[4]http://1060.org.

[5]http://en.wikipedia.org/wiki/Memoization.

[6]http://dublincore.org.

[7]http://creativecommons.org/ns.

[8]http://mulgara.org.

[9]http://open rdf.org.

[10]http://talis.com.

5.4　面向资源的架构

面向资源的架构的标识是向命名的资源发起逻辑请求的过程。这些请求由某种引擎解释，转成该资源的物理表现形式（如HTML页面、XML格式、JSON对象等）。参见图5-4。

这张图展示了面向资源的架构（ROA）的基本交互方式。逻辑请求由一个面向资源的引擎来命名、解析，并将结果返回给请求者。命名的请求可能被解释为一个数据库查询，或是某项信息管理功能（如RESTful服务）。响应请求的可能是一个Servlet、一个Restlet [1]、一个NetKernel模块，或某些可以寻址、能够理解该请求的功能，而对此信息感兴趣的人基本上不关心这些响应请求的程序。这个逻辑步骤隐藏了所有的可能性和技术选择，没有向客户透露一点不必要的细节。它确实不能支持所有的交互方式，但你可能会惊讶于有多少东西可以舒服地藏在URL后面。

图　5-4：面向资源的架构

请考虑这个地址：http://server/getemployees＆type=salaried。许多人会认为创建这样的地址就是在采用REST。遗憾的是，这不是一个好的REST服务名称（大多数“拉斯塔法里分子（Restafarians）”会说这根本不是REST！），因为它混合了名词和动词。这就是我常说的“通过URL来寻址行为”或“通过URL来实现RPC”。用REST的方式来区分名词和动词没有什么神奇的，它只是能让我们识别我们关心的事物。前面提到的URL不能够用于更新雇员列表，因为POST一条雇员记录到“/getemployees”没有什么意义。相反，如果URL是http://server/employee/salaried，那么向它发起GET请求将得到相同的信息，而它将成为“领薪雇员”这个业务概念的长期有效地址，正如http://server/employee/hourly可以指按小时付薪的所有雇员一样。我们可以选择不更新这些信息资源，因为它们代表的是对后台数据库的查询。但是，在/employee信息空间内是一致的，我们可以选择通过其他方式来导航。http://server/employee/12345678代表了一个具有特定ID的雇员，而http://server/employee可能代表所有的雇员。向后面一个URL POST一条记录可以代表雇用了某人。向包含具体雇员ID的URL PUT一条记录可以代表在岗位变更、升职或加薪之后更新雇员记录。向该地址发出DELETE请求可以表示这个命名资源在该组织机构中已经不再感兴趣了（他们或者辞职，或者被解雇了）。

这突出了REST和SOAP之间的一个主要区别，如果人们混淆了这两种方式的意图，就会导致困惑。REST是关于信息管理的，而不一定是通过URL来调用任意的行为。当人们开始挠头思考4个动词是否足够完成他们想做的事情时，他们想的也许不是信息，而是在想调用的行为。如果你打算通过URL来实现RPC，你也可以使用SOAP。如果你将重要的业务概念作为可以寻址的信息资源，并且可以在不同环境下进行操作或表示为不同的形式，那么你就在利用REST的长处，可能看到我们在Web上看到的某些好处。即使后台系统使用SOAP来满足请求，你也可以设想RESTful接口带来的好处。提供这样的地址不仅让用户能够“在数据上冲浪”，还可以引入缓存结果的能力，消除变更WSDL协议所带来的某些痛苦。客户端会通过一些逻辑连接，再翻译成SOAP消息，然后生成响应结果。响应的内容可以从返回的信息中抽取出来。我们根本不需要宣传这一事实，就能够在这个过程中实现架构迁移的策略。

正如图5-5所示，同样名称的资源可能在不同环境下返回不同的物理格式，却保留相同的标识符。我们可以设想，某种类型的公司报表在信息空间中的组织方式支持按时间浏览（例如，先按年再按月）。假设只有一种类型的报表，那么http://server/report/2008/02就是一个相当不错的、可以长期使用的名称。将来任何时候都不会改变这一事实：我们有一份2008年2月的报告。我们可能在某种场景下需要XML形式的数据，在另一场景下需要Excel表格，或者渲染成JPEG图像包含在汇总报告中。我们不希望在每种场景下使用不同的名称，所以我们通过内容协商来指定我们的格式偏好。面向资源的引擎需要知道如何响应一种请求类型，但这是很容易支持的。将来可能出现一些现有客户端都不支持的数据格式。已有的客户端不会需要修改，因为我们会在服务器上添加支持，然后某些其他客户端会利用它。这种面对变化的弹性是Web设计中固有的，也是我们在企业系统中希望实现的。客户端和服务器可以在解析过程中，针对命名的资源协商一种特定的格式。这让同样名称的资源在不同环境下表现为不同的结构（例如，在中间层使用XML，在浏览器中使用JSON）。针对每种格式，如果需要，结构化的格式可以由服务器实现缓存。

图　5-5：在面向资源的环境中协商内容

除了支持在解析请求的上下文中选择物理表示形式之外，我们还可以支持服务根据用户的身份和使用的应用程序等信息来决定返回多少引用数据。我们可以想象这样一种场景：呼叫中心的座席人员需要通过一个相关的应用来访问敏感的信息，以解决问题。这些信息可能包括社会保险号码、信用卡号码（可能只是最后4位数字）、家庭地址等。这是一个特殊的业务，需要允许座席人员访问这些信息，所以我们可以用一个声明式的策略来支持这种情况。如果同样的雇员在不同的环境下使用不同的应用程序（也许是一个市场分析软件），在业务上就不太可能需要访问这些敏感信息，虽然我们可能仍然需要查找同样的客户，访问她的统计信息和购买历史。在这种情况下，上下文环境将不允许访问敏感数据，我们将强制通过一个自化的过滤过程来移除或加密敏感信息。采用哪种方式取决于数据需要发送到哪里。加密的数据要求取得密钥，这会带来额外的管理负担。我们很容易移除这些敏感数据，并在需要的时候将它们放在不同的解析上下文里。

管理单点访问控制对于传统的企业架构来说可能不是什么大问题。但是，考虑到不断增加的工作流，显式的、模型化的业务处理过程以及诸如此类的东西，我们有充分的理由认为一个应用程序的用户会需要在多种情况下调用一项功能或一个服务。如果我们在系统之间传递实际的数据，应用程序的开发者就有责任知道在跨应用边界时的访问控制问题。如果我们只是传递数据的引用，那么最初的应用就不再需要负责，我们就可以确保实现信息驱动的、集中式的访问控制。许多现有的SOA系统根据身份或角色来限制访问权限，但它们基本上不支持对服务之间传递的具体数据的限制。这种局限性也是传统Web服务既复杂又不够安全的原因之一。访问控制策略应该在某个上下文环境中应用于行为和数据，但如果不能够命名这些数据并包含上下文信息，这就变得非常困难。

当人们开始研究面向资源的架构时，他们考虑到了通过链接暴露敏感信息的问题。在某种程度上说，在透明的查询之后返回大块的数据似乎更安全。他们很难从上下文中分离数据标识和数据解析的行为。上下文中包含的信息足够决定是否要向特定的用户提供信息。它与请求本身是无关的，要符合组织机构中现有的认证和授权系统。从HTTP基本认证，到IBM Tivoli Access Manager，到OpenID或其他联合身份系统，都可以用来保护数据。我们可以检查谁可以访问什么数据，通过单向或双向SSL来加密传输数据，防止窃听。可以寻址并不等于安全性脆弱。实际上，传递引用的策略比传递数据更安全、伸缩性更好。面向资源的方式不会因为它没有复杂的安全特性（如XML加密、XML签名、XKMS、XACML、WS-Security、WS-Trust、XrML等）变得更脆弱，反而可能更安全，因为人们可以真正明白威胁模式以及如何应用保护策略。

当我们面对需要说明满足法规要求这一令人生畏而又非常严肃事实时，这些思想就变得非常的重要。信用卡公司、医疗监控机构、公司治理审计者等组织机构会对公司的审计动真格，要求证明只有在工作中必须访问这些敏感信息的雇员才能看到这些信息。即使你的公司是满足要求的，但如果很难说明这一点（“首先，看系统中的这部分日志，然后跟踪流过这些中介的消息，它在这里被选取并处理成一个查询，你可以在另一个日志中看到这一点……”），那么审计过程的代价会变得巨大。针对逻辑引用解析来使用声明式的访问控制策略，可以明确谁、何时知道什么（而且容易遵守）。

[1]http://restlet.org.

5.5　数据驱动的应用

当组织机构摆脱了数据寻址的麻烦之后，就可以得到一些好处，除了可以让后台系统缓存结果之外，还可以通过谨慎的方式迁移一些新技术。具体来说，我们可以引入一些全新的数据驱动的应用程序和集成策略。当我们可以对数据命名，并通过应用友好的方式来请求数据时，我们就很容易实现一些数据查看、商务智能、知识管理，让大多数分析师看到它的时候感到异常兴奋。Simile项目[1]是W3C和MIT CSAIL小组的合作项目，他们已经做了大量的工作来展示这些思想，展示真正能够带来多少兴奋。

请考虑这样一种情况：根据网站上的访问量和销售来追踪各种市场策略的效果。我们可能需要从电子表格、数据库和来自Web分析软件的一些日志文件和报表中取得信息。虽然将这些东西结合起来不需要像造火箭那样，但这的确需要不小的工作量来发现、请求、转换和发表这些结果。如果只是生成一份电子表格汇总，然后通过邮件分发它，那么我们实际上在将来需要查看这些结果时，就需要在我们的大堆文件中翻找。采用CMS或其他的文档管理管理系统只会增加得到结果所需的时间。无论生成这些报告的频度如何，我们每次都需要重复这些过程。

在面向资源的架构中，我们可以为每项数据元素确定地址，并请求JSON格式的数据，然后在基于浏览器的环境中使用它。源自Simile的Exhibit项目[2]有一个Timeline视图[3]，差不多为我们提供了这种功能。花一点工夫将Excel表格转换成JSON对象，我们就有了一个可复用的环境，有了它之后，我们就能在几秒钟里组合并发布这些市场报告。接下来你会想到同样的基础设施可以将其他格式的数据放在一起，很容易地实现不同类型的分析和报表，这样你就会意识到Web和可寻址的数据的价值。这种环境正在企业中出现，如果你的组织机构还不能够这样容易地组织数据，那么它应该做到这一点。

[1]http://simile.mit.edu.

[2]http://simile.mit.edu/exhibit.

[3]http://simile.mit.edu/timeline.

5.6　应用面向资源的架构

最近，我通过重新架构我公司的Persistent URL（PURL）系统，创建一个面向资源的系统。原来的PURL[1]是在大约15年前实现的。它是从Apache 1.0中分支出来的，用C语言写成，反映了当时的水平[2]。它从那时起就成了Internet基础设施的一个稳定组成部分，但它逐渐显现老态，需要现代化，特别是需要支持W3C TAG的303建议以及大量的使用。大量的数据可以通过网页或简单的CGI-bin脚本来访问，因为在那时候，浏览器似乎像是唯一要面对的客户端。随着我们逐渐认识到语义网、生命科学、出版业和类似的一些社区需要长期不变的、无歧义的标识符，我们就知道是时候来重新考虑架构，并让它对人和软件更有用了。

PURL系统的设计目的是缓解好的名字和可解析的名字之间的紧张关系。任何在Web上发布过信息的人都知道，当内容被移动时，链接就不能使用了。Persistent URL的思想就是用一个好的、符合逻辑的名称映射到可以解析的位置。例如，一个PURL可以定义从http://purl.org/people/briansletten指向http://bosatsu.net/foaf/brian.rdf并返回一个303来表示“参见”响应。我不是一个网络可寻址的资源，但是从我“朋友的朋友（FOAF）”文件[3]可以找到关于我的更多信息。我可以将这个PURL传给所有想链接到我的FOAF文件的人。如果我转到了其他公司，我可以更新PURL，让我的FOAF文件指向一个新的位置。所有现有的链接将继续有效，它们只是通过303指向了新的位置。这个过程如图5-6所示。PURL服务器实现了W3C技术架构小组（TAG）的建议，即响应码303可以用于提供非网络可寻址的资源的更多信息。

图　5-6：PURL的“参见”重定向

除了支持PURL重定向，我们还想将PURL系统中的每个主要数据部分作为一个可寻址的信息资源。这不仅简化了与用户接口的交互，也支持潜在可能的数据复用，也许超出我们原本的计划。对资源的操作要求所有者凭证，但所有人都可以取得一个PURL的定义。对点击http://purl.org/employee/briansletten这样的PURL有一个直接的解析过程（它将导致303重定向），也有一个过程处理PURL资源的间接RESTful地址（http://purl.org/admin/purl/employee/briansletten），它将返回该PURL的定义，目前看起来是这样的：

＜purl status=”1”＞

＜id＞/employee/briansletten＜/id＞

＜type＞303＜/type＞

＜maintainers＞

＜uid＞brian＜/uid＞

＜/maintainers＞

＜seealso＞

＜url＞http://bosatsu.net/foaf/brian.rdf＜/url＞

＜/seealso＞

＜/purl＞

PURL服务器的客户可以在数据定义上“冲浪”，作为发现有关PURL资源的一种手段，而不必真正解析它。不需要编写代码来取得这些信息。我们可以在浏览器中查看它，或通过curl在命令行访问它。这样，我们就可以设想编写一些shell脚本，利用来自信息源的数据，检查PURL是否指向了有效的资源，并返回合理的结果。如果没有，我们可以找到PURL的所有者，向账号相关的邮件地址发出一份邮件通知。可寻址、可访问的数据可以用于各种意想不到的设计、脚本、应用和桌面控件，因为这样做很容易，也很有用。

有趣的完整故事是，我们没有在最初的版本中支持JSON的请求格式，这使得AJAX用户接口变得复杂了。JavaScript的XML处理远不是人们所期望的。即使我们在内部使用XML格式，我们也应该不怕麻烦，提供JSON格式让浏览器来处理。你可以相信我们很快就会弥补这个疏忽，但是我想在这里有必要突出这一点，如果我们一开始就做对，会很有好处。你不需要一开始就支持所有数据格式，但在今天，从XML和JSON开始是很好的。作为一项有趣的补充说明，我们可以选择几种容器和工具来实现前面介绍的这种架构。所有响应HTTP请求的软件都可以作为PURL服务器。这体现了RESTful接口和面向资源架构的浅显而有用的概念，如图5-7所示。任何Web服务器或应用服务器都可以作为简单的面向资源的引擎。逻辑HTTP请求被解释为对Servlet、Restlet或类似的可寻址功能的请求。

图　5-7：简单的面向资源的架构

我们选择使用NetKernel作为这个架构的基础，因为它是面向资源的架构的化身，具有双重许可证，允许用于开源和商业项目中。利用不同的表示方式来实现各层之间的逻辑耦合的思想，被设计进了软件架构中，提供了类似的灵活性、可伸缩性和简单性等好处。层和层之间的联系是通过异步解析、逻辑名称来实现的。面向资源的架构的这种深刻思想如图5-8所示。NetKernel是一个有趣的软件基础设施，因为它在内部利用了资源逻辑相连的概念，所以HTTP逻辑请求转成其他逻辑请求。这种架构在运行时软件环境中反映了Web的特性。

图　5-8：深层的面向资源的架构

外部URL http://purl.org/employee/briansletten通过通过地址重写被映射到一段名为“访问者”（accessor）的程序[4]。访问者存在于模块中，这些模块输出公开的URI定义，代表了它们负责响应的地址空间。这里的方便之处在于，我们能够在模块的新版本中大幅度地改变实现技术，同时只要修改重写规则，让它指向新的实现就可以了。只要返回的是兼容的响应结果，客户端什么都不需要知道。我们可以在现代的面向对象语言中利用接口近似实现这种灵活性，但这还是要求我们在“物理上”耦合接口的定义。通过只包含逻辑的绑定，我们虽然仍旧需要支持现有客户端的期望，但我们不再与任何实现细节耦合在一起。我们不仅可以在Web上通过URI的通信时看到这种价值，而且在本地运行的软件中也是这样。

在内部，我们使用与请求的方法类型相关联的“命令模式”[5]来实现访问者。HTTP GET方法被映射到GetResourceCommand方法，该方法不保持状态。在收到请求时，我们从映射表中找到命令并向它发出请求。REST的无状态风格确保了请求中已包含回答请求所需的全部信息，所以我们不需要在命令实例中保持状态。我们可以在后面的代码中通过上下文实例来访问请求的状态。这些代码对于Java开发者来说显得很直白。我们在Java对象上调用方法，捕捉异常，取得返回值。重要的是要注意IURAspect接口的使用。从本质上说，我们不关心资源采取何种形式。它可以是一个DOM实例、一个JDOM实例、一个字符串，或一个字节数组。从我们的目的来说，这都不重要。基础设施会将它转换成一个字节流，再带上元数据，作为请求的响应结果发出。如果我们希望基础设施支持特定的格式，我们就会直接向它请求那种格式。这种声明式的、面向资源的方式有助于大幅度减少用于操作数据的代码的数量，让我们能利用正确的工具做正确的事情：

if（resStorage.resourceExists（context,uriResolver））{

IURAspect asp=resStorage.getResource（context,uriResolver）；

//Filter the response if we have a filter

if（filter！=null）{

asp=filter.filter（context,asp）；

}

//Default response code of 200 is fine

IURRepresentation rep=NKHelper.setResponseCode（context,asp，200）；

rep=NKHelper.attachGoldenThread（context，"gt："+path,rep）；

retValue=context.createResponseFrom（rep）；

retValue.setCacheable（）；

retValue.setMimeType（NKHelper.MIME_XML）；

}else{

IURRepresentation rep=NKHelper.setResponseCode（context，

new StringAspect（”No such resource：”

+uriResolver.getDisplayName（path）），404）；

retValue=context.createResponseFrom（rep）；

retValue.setMimeType（NKHelper.MIME_TEXT）；

}

当GET请求发出后，大部分信息资源会返回200。显然，PURL超越了这种行为，可以返回302、303、307、404等。如果我们看看resStorage.getResource（）方法的面向PURL的实现，就会知道这种的面向资源的趣事。

INKFRequest req=context.createSubRequest（"active：purl-storage-query-purl"）；

req.addArgument（"uri"，uri）；

IURRepresentation res=context.issueSubRequest（req）；

return context.transrept（res,IAspectXDA.class）；

简而言之，我们通过“active：purl-storage-query-purl”这个URI发出一个逻辑请求，带有“ffcpl：/purl/employee/briansletten”作为参数。忽略掉这个不寻常的URI格式，这只是在NetKernel中用于表示内部的请求。我们不知道什么代码会被调用，以取得要求格式的PURL，我们并不关心它。在面向资源的环境中，我们只需说：“响应这个URI的程序将生成响应结果”。现在我们可以自由地在设计阶段先向客户端提供一些静态文件，然后在构建阶段利用基于Hibernate映射到关系数据库。我们可以通过重写对“active：purl-storage-query-purl”这个URI的响应来实现这种转换。客户端代码不需要知道这种区别。如果我们改变了PURL的解析方式，从本地持久层变为远程获取，客户端代码也可以不必关心。这就是我们已经提到的好处，在一个强大的软件环境中，面向资源的企业级计算的思想被具体化了。

我们不仅用这种方式实现了层与层之间的松耦合，而且我们在这个环境中也享受到了幂等的、无状态的请求所带来的好处。取得PURL定义的早期代码片断逐渐在内部扁平化，变成了对“active：purl-storage-query-purl+uri@ffcpl：/purl/employee/briansletten”这个URI的异步请求。正如我们前面所讨论的，这变成了一个复合散列键，代表了查询持久层而得到的结果。即使我们对调用的代码一无所知，NetKernel仍然能够缓存这些结果。这就是我前面提到过的架构上的记忆化。实际的过程稍稍有点差异，但在概念上，就是这样的。如果其他人试图从内部或通过HTTP RESTful接口来解析同样的PURL，我们就可以从缓存中拿出结果。尽管这一点可能不能打动在Web页面上已经建立了缓存机制的人，但如果你再仔细想想，就会发现这实际上是很令人佩服的。所有可能的URI请求都以这种方式进行缓存，不论我们是打算从磁盘上读取文件、通过HTTP取得文件、通过XSLT文件转换一份XML文档，或是将PI（圆周率）计算到10000位小数。每次调用都得到逻辑的、无状态的、异步的结果，每次结果都可能缓存起来。这种面向资源的架构方式让我们的软件可伸缩、有效率、支持缓存，并且是通过统一的、逻辑的接口来工作的。这导致了本质上更健壮、更灵活、可伸缩的架构，就像Web一样，原因也是一样的。

[1]http://purl.org.

[2]此代码库构成非常成功的服务的基础TinyURL（http://tinyurl.com）service.

[3]http://foaf-project.org.

[4]http://docs.1060.org/docs/3.3.0/book/gettingstarted/doc_intro_code_accessor.html.

[5]http://en.wikipedia.org/wiki/Command_pattern.

5.7　结论

面向资源的架构方法很优雅地实现了一些折中。一方面，对于传统的方法来说，这些方法可能看起来有些奇怪，而且没有尝试过。如果人们关心自已的简历，就会希望停留在尝试过的、真正在使用的方法。另一方面，对于那些研究过Web和它的基本组成模块的人来说，它很有意义，代表了人们设想和实现过的最大、最成功的网络软件架构。一方面，它要求一种完全不同的思考方式。另一方面，它支持一种强大的机制，包装并复用已有的代码、服务和基础设施，为它们提供逻辑命名接口，对所有形式的交互都不透露实现细节。我们可以自由地调整服务器端的技术而不会影响原有的客户端。随着时间的推移，我们可以为同样的数据提供新的结构形式支持。我们可以迁移后端的实现，不会影响我们的客户。另外，这些设计选择导致了伸缩性强、易于缓存、信息驱动的访问易控制、好的法规兼容性等特点。

软件开发者通常不关心数据，他们关心算法、对象、服务和其他类似的结构。对于J2EE、.NET和基于SOAP的架构，我们已经拥有一些相当具体的推荐蓝图和技术。遗憾的是，大多数已有的蓝图没有把信息作为一等公民。它们让我们局限于某些具体的绑定，使我们难以在不影响已有客户端的情况下进行变更。这是我们沿用多年的脚踏式水车，业务部门对此已感到疲倦。Web服务本应该是一种退出策略，但不恰当的抽象和过于复杂的边界条件用例，使得整个过程成为了对Web服务完全不满意的经历。现在是时候从以软件为中心的架构中走出来，开始关注信息及其流动了。我们仍会利用我们知道和喜爱的工具来编写软件，只是这些工具不会成为架构绑定的焦点。

面向资源的方法在业务部门和支持它们的技术部门之间提供了引人注目的桥梁。以信息为中心的视图和我们的各系统之间连接的方式，为我们提供了真正的效率和业务价值主张。我们不需要根据供应商们的伟大思想从头干起，我们可以从Web上学到有价值的经验，了解它的架构方式引发的重要特征。架构是有人居住的雕塑，我们不得不在一段时间内承受这些选择的影响。我们应该抓住机会，让功能性、美和弹性渗入到架构之中，使我们在架构中的生活变得更舒适。

第6章　数据增长：Facebook平台的架构

Dave Fetterman

原则与特性

结构

√功能多样性

√模块

概念完整性

√依赖关系

√修改独立性

进程

√自动传播

√数据访问

可构建性

√增长适应性

√熵增抵抗力

给我看你的流程图而藏起你的表，我将仍然莫名其妙。如果给我看你的表，那么我将不再需要你的流程图，因为它们太明显了。

—Fred Brooks，《The Mythical Man-Month》（人月神话）

6.1　简介

当前大多数计算机科学的学生将Fred Brooks的这句话理解为：“给我看你的代码而藏起你的数据结构……”信息架构师坚信，处于大多数系统核心的是数据，而不是算法。随着Web的兴起，用户产生和消费的数据比以往更加推动了信息技术的使用。Web用户不会去接触QuickSort（快速排序）。他们会访问一个数据仓库。

这些数据可以是通用的，如一本电话簿；也可以是私有的，如一个在线仓库；也可以是个人的，如一个博客；也可以是开放的，如当地的天气情况；还可以是严格保护的，如在线银行记录。在任何情况下，Web呈现的几乎所有面对用户的功能归根结底都是提供一个界面，访问站点专有的一组核心数据。这些信息构成了几乎所有网站的核心价值，不论它是由顶级员工研究团队创建的还是由世界各地的用户创建的。数据推动了用户喜欢的产品，所以架构师围绕数据创建了其余的传统“n层”软件栈（逻辑层与显示层）。

这个故事讲的是Facebook的数据，以及它如何与Facebook平台的创建一起发展。

Facebook（http://facebook. com）是一个很有用的围绕数据建立架构的例子，包括用户提交的个人关系映射表、传记信息，以及文本或其他媒体内容。Facebook的工程师在构建站点其余部分的架构时，关注的是显示和操作这些社会关系数据。这个站点的大多数业务逻辑与这些社会关系数据密切相关，诸如对各种页面的流程和访问模式，搜索的实现，查看新闻内容，以及对内容应用可见性规则。对于用户来说，这个站点的价值直接来自于他和与他有关的人对该系统所贡献的数据的价值。

“Facebook社会关系网站”在概念上是一个标准的n层栈，用户的请求会从Facebook的内部库中取出数据，然后通过Facebook的逻辑进行转换，最后通过Facebook的界面输出。Facebook的工程师意识到这些数据的用处超过了这些容器的限制。Facebook平台的创建显著地改变了Facebook数据访问系统的形态，它包含的愿景远远超出了n层栈的分离功能，目标是以应用的形式来集成外部的系统。利用居于架构中心的用户社会关系数据，该平台开发了一组Web服务（Facebook平台应用编程接口，或Facebook API）、一门查询语言（Facebook查询语言，或FQL），以及一种数据驱动的标记语言（Facebook标记语言，或FBML），目的是将应用开发者的系统与Facebook的系统结合在一起。

随着某些数据集越来越广泛地提供出来，而且用户要求跨越多个网和桌面应用来统一使用他们的数据，阅读本章的架构师可能会发现自己已经是这样一个平台的消费者，或者围绕着自己站点的数据建立了类似的平台。本章将向读者展示Facebook以一种受控的方式向外界开放数据的过程，跟随数据演进的每一步的架构选择，以及调和数据开放与渗透在社会关系系统中独特的隐私需求的过程。它包括：

·促进这些类型的集成。

·将数据功能从内部栈调用移到外部可见的Web服务上（Facebook API）。

·授权访问这个Web服务，注意保持这个社会关系系统的隐私性。

·创建一种数据查询语言，减轻这个Web服务的新客户端的负担（Facebook FQL）。

·创建一种数据驱动的标记语言，将应用的显示集成回Facebook，同时也支持使用其他方式不能访问的数据（Facebook FBML）。

当我们将应用的架构从分离的栈进行了足够的演进之后：

·创建一些技术来弥补Facebook体验与外部应用体验之间的差异。

对于数据平台的使用者，本章展示了我们所做的设计决定和这些决定背后的理由。用户会话、身份认证、Web服务和各种处理应用逻辑的方式等概念将不断重复出现，它们是Web上所有这些类型的平台的主题。理解它们背后的思想为数据架构提供了巨大的实践机会，而且考虑到这些平台制造者将来可能创建的功能和形式，这种理解也相当重要。我们鼓励数据平台制造者心里想着自己的数据集，然后从Facebook开放其数据模型的方式中学习。某些设计选择和折中可能只适合Facebook，或只适合处理有隐私保护的社会关系数据，可能不完全适用于给定的数据集。但不管怎样，在每一步我们都给出了一个实际的问题、一个数据驱动的解决方案，以及该解决方案的高层实现。对于每个新的解决方案，我们基本上会创建一个新的产品或平台，所以在任何时候我们都必须让这个新产品符合用户的预期。反过来，我们会伴随每一步的演进创造一些新技术，有时候会改变围绕应用的Web架构。

Facebook平台的开源版本可以从http://developers.facebook.com/获得。就像这个版本一样，本章的代码是用PHP写的。请随意查看，不过请注意，出于清晰性的考虑，这里的代码是缩写过的。

我们从这些类型的集成的动机开始，通过一个例子来讲解一个“外部的”应用逻辑和数据（一个书店）、Facebook的社会关系数据（用户信息和朋友关系），以及它们的集成。

6.1.1　某些应用核心数据

Web应用，即使是不提供也不使用任何的数据平台，基本上仍然是由它们内部的数据来驱动的。以http://fettermansbooks.com为例，它一是个假想的网站，提供书籍方面的信息（如果用户感兴趣，它可能也提供购买这些书的功能）。这个站点的功能可能包括可查找的库存索引、关于每件产品的基本信息，以及用户每本书作出的评论。访问这些具体的信息构成了这个应用的核心，驱动了架构的其他部分。该站点可能使用Flash和AJAX技术，支持通过移动设备来访问，并提供一个一流的用户界面。然而http://fettermansbooks.com存在的根本理由是让访问者能够利用某些方法得到示例6-1中这样的核心映射关系。

例6-1：书籍数据映射的例子

book_get_info：isbn-＞{title,author,publisher,price,cover picture}

book_get_reviews：isbn-＞set（review_ids）

bookuser_get_reviews：books_user_id-＞set（review_ids）

review_get_info：review_id-＞{isbn,books_user_id,rating,commentary}

所有这些最终都实现为类似简单集合的东西，能够从一个经索引的数据表中取出。这样的书籍站点如果要有存在的价值，可能还会实现其他一些不太简单的功能，如例6-2中的简单“查找”。

例6-2：简单查找映射

search_title_string：title_string-＞set（{isbn,relevance score}）

这些功能中包含的每个键值通常都会表现为http://fettermansbooks.com上的一个或多个页面—有一组特有的逻辑围绕着这批数据，通过一种特有的方式显示出来。例如，要查看评论者X提交的一些评论，http://fettermansbooks.com的用户可能会被引向页面fettermansbooks.com/reviews.php?books_user_id=X，或者要看ISBN号为Y的某本书的所有信息（包括所有对个人评论页面的链接），用户会被引向页面http://fettermansbooks.com/book.php?isbn=Y。

像http://fettermansbooks.com这样的站点有一个特点是值得注意的，即几乎所有数据都对所有用户开放。它在book_get_info这样的映射中生成所有的内容，帮助用户发现有关某本书的尽可能多的信息。这对于一个卖书的站点可能是好事，但在接下来的使用社会关系数据的例子中，可见性限制决定了数据访问层的许多架构考虑。

6.1.2　一些Facebook核心数据

随着所谓“Web 2.0”的网络技术逐渐流行，数据在系统中的核心地位就变得更明显了。Web 2.0展现的核心主题就是它们是数据驱动的，用户本身提供了绝大部分的数据。Facebook像http://fettermansbooks.com一样，主要由一组核心数据映射构成，它们驱动着网站的观感和功能。这些Facebook映射的极端精简集合看起来如例6-3所示。

例6-3：社会关系数据映射示例

user_get_friends：uid-＞set（uids）

user_get_info：uid-＞{name,pic,books,current_location，……}

can_see：{uid_viewer,uid_viewee,table_name,field_name}-＞0 or 1

这里的uid指的是（数字化的）Facebook用户标识符，从user_get_info返回的info指的是用户的描述信息（参见Facebook开发文档中的users.getInfo），可能包含了用户最喜欢的书籍名称，因为他们曾在http://facebook.com上输入过。这个系统从核心上来看与http://fettermansbooks.com区别不大，只有中心数据不同，因此站点的功能也不同，这些功能围绕着用户与其他用户的联系（“朋友”），用户的内容（“描述信息”），以及内容的可视法则（“can_see”）。

这个can_see数据集是很特殊的。Facebook对于用户生成的数据有一个非常核心的隐私概念，即用户X查看用户Y的信息的业务规则。这种数据从不直接可见，但它驱动了一些重要的考虑，当我们查看外部应用的逻辑、数据与Facebook的逻辑、数据集成的例子时，会看到这些考虑反复出现。就其本身而言，Facebook到处使用这种数据集令它与http://fettermansbooks.com这样的站点区别开来。

Facebook平台和其他社会关系平台认识到这种社会关系映射是有用的，这种用处不仅体现在http://facebook.com这样的站点内部，也体现在与http://fettermansbooks.com这样的站点功能进行集成时。

6.1.3　Facebook的应用平台

对于http://fettermansbooks.com和http://facebook.com的共同用户来说，此时因特网应用的图景如图6-1所示。

在一般的n层架构中，应用将输入（对于Web来说，就是GET、POST和cookie信息的集合）映射为对原始数据的请求，这些原始数据可能存在于数据库中。它们被转换为内存中的数据，并通过一些业务逻辑进行智能化处理。输出模块将针对显示对这些数据对象进行转换，变成HTML、JavaScript、CSS等。这里，在图的顶部，是运行在基础设施之上的应用程序n层栈。在应用出现在Facebook平台之前，Facebook完全运行在同样的架构上。重要的是，在两个架构中，业务逻辑（包括Facebook的隐私）实际上都是根据一些规则来执行的，这些规则建立在系统的某些数据组件之上。

图　6-1：分离的Facebook和n层应用栈

更大量的相关数据意味着业务逻辑可以提供更多个人定制的内容，所以在http://fettermansbooks.com（或其他应用）上浏览书籍，写书评、阅读或购买的体验，会被来自Facebook的用户社会关系数据加强和放大。具体来说，显示朋友的书评、期望清单和购买情况将有助于用户的购买决定，发现新的书籍，或强化与其他用户之间的联系。如果Facebook的内部映射user_get_friends可以由http://fettermansbooks.com这样的其他外部应用访问，就会为这些原本分离的应用提供强大的社会关系上下文，让应用程序不需要创建它自己的社会关系网络。所有这种类型的应用都可以与这种数据进行很好的集成，因为开发者可以将这些核心Facebook映射应用于无数其他Web应用，用户在这些应用里提供或消费内容。

Facebook平台的技术通过在社会关系网络和数据架构方面的一系列改进，实现了这一点：

·应用可以通过Facebook平台的数据服务来访问有用的社会关系数据，为外部的Web应用、桌面操作系统应用和其他设备上的应用提供社会关系上下文。

·应用可以通过一种名为FBML的数据驱动标记语言来实现显示，在http://facebook.com的页面上集成他们的应用体验。

·通过FBML所要求的架构改变，开发者可以使用Facebook平台的cookie和Facebook JavaScript（FBJS），从而让应用出现在http://facebook.com上所需的改动最小。

·最后，应用可以获得这些功能，同时不必牺牲隐私，也不必放弃对于Facebook为用户数据和显示提供的用户体验的期望。

最后一点是最有趣的。Facebook平台的架构并非一直是美丽的—它主要被看成是社会关系平台领域的先行者。大多数的架构考虑是为了创建统一可用的社会关系上下文，它体现了这样的阴阳关系：数据可获得性和用户隐私。

6.2　创建一个社会关系Web服务

回过头来看一看像http://fettermansbooks.com这样一个简单的例子，我们就很清楚大多数因特网应用都会因为在数据显示时添加社会关系上下文而受益。但是，我们会遇到一个实际的问题：这种数据的可获得性。

实际问题：应用可以利用在Facebook上的用户社会关系数据，但这种数据是不可访问的。

数据解决方案：通过一个外部可以访问的Web服务来提供Facebook数据（图6-2）。

为Facebook架构添加了Facebook API，就开始通过Facebook平台为外部应用和Facebook建立了关系，本质上为外部应用栈添加了Facebook数据。对于Facebook用户，当他显式地授权外部应用可以代表他获得社会关系数据时，这种集成就开始了。

例6-4展示了http://fettermansbooks.com的登录页面在没有Facebook集成的情况下可能的样子。

例6-4：书籍网站逻辑示例

$books_user_id=establish_booksite_userid（$_REQUEST）；

$book_infos=user_get_likely_books（$books_user_id）；

display_books（$book_infos）；

图　6-2：应用栈通过Web服务使用Facebook数据

这个user_get_likely_books函数操作完全源自于这个书籍应用控制的数据，可能使用智能的关联技术来猜测用户的兴趣。

但是，假定Facebook为在其他站点的用户提供了两个简单的远程过程调用（RPC）方法：

·friends.get（）

·users.getInfo（$users，$fields）

通过它们，并添加从http://fettermansbooks.com的用户标识符到Facebook的用户标识符的映射关系，我们就可以为http://fettermansbooks.com上的所有内容添加社会关系上下文。请考虑这个针对Facebook用户的新流程，如例6-5所示。

例6-5：包含社会关系上下文的书籍站点逻辑

$books_user_id=establish_booksite_userid（$_REQUEST）；

$facebook_client=establish_facebook_session（$_REQUEST，$books_user_id）；

if（$facebook_client）{

$facebook_friend_uids=$facebook_client-＞api_client-＞friends_get（）；

foreach（$facebook_friend_uids as$facebook_friend）{

$book_site_friends[$facebook_friend]

=books_user_id_from_facebook_id（$facebook_friend）；

}

$book_site_friend_names=$facebook-＞api_client-＞

users_getInfo（$facebook_friend_uids，'name'）；

foreach（$book_site_friends as$fb_id=＞$booksite_id）{

$friend_books=user_get_reviewed_books（$booksite_id）；

print"＜hr＞".$book_site_friend_names[$fb_id]."'s likely picks：

＜br＞"；

display_books（$friend_books）；

}

}

这个例子中的粗体部分就是书籍应用使用Facebook平台提供的数据的代码。如果我们能够弄清楚函数establish_facebook_session背后的代码，这个架构就可以提供更多的数据，从而将这个了解书籍的应用变成了一个完全了解用户的应用。

让我们来看看Facebook的API如何支持这一点。首先，我们会简单浏览一下Web服务包装Facebook数据的技术，这是通过使用合适的元数据以及名为Thrift的灵活的代码生成器来生成的。开发者可以使用下一节中提到的这些技术，有效地创建各种Web服务，不论开发者手中的数据是公有的还是私有的。

但是请注意，Facebook的用户并不认为他们的Facebook数据全部是公有的。所以在技术概述之后，我们会探讨Facebook层面的隐私，这是通过平台API中的主要认证方式来实现的，即用户会话。

6.2.1　数据：创建一个XML Web服务

为了能够在一个示例应用中提供基本的社会关系上下文，我们已经建立了两个远程方法调用，即friends.get和users.getInfo。访问这些数据的内部功能可能存在于Facebook代码树的某个库中，为Facebook站点上的类似请求提供服务。例6-6展示了一些例子。

例6-6：社会关系映射示例

function friends_get（$session_user）{……}

function users_getInfo（$session_user，$input_users，$input_fields）{……}

我们接下来要创建一个简单的Web服务，将通过HTTP的GET和POST输入转换成对内部栈的调用，以XML的格式输出结果。在Facebook平台中，目标方法的名称以及它的参数是在HTTP请求中传递的，还包括一些与调用应用相关的证书（称为“api key”），与用户-应用对相关的证书（称为“用户会话key”），与请求实例本身相关的证书（称为请求“签名”）。我们稍后将在6.2.2节中讨论会话key。要服务一个针对http://api.facebook.com的请求，其大致过程如下：

1.检查传递的证书（第6.2.2节），验证调用应用程序的身份，用户当前在该应用中的授权，以及请求的可信度。

2.将进入的GET/POST请求解释为带有相应参数的方法调用。

3.对内部方法进行单次调用，将结果保存为内存中的数据结构。

4.将这些数据结构转换成已知的输出格式（如XML或JSON）并返回。

创建外部可使用的接口，难度主要在于第2步和第4步。为外部使用者提供这些数据接口的一致维护、同步和文档是很重要的，手工打造一个代码框架来确保这种一致性则是一项无人赞赏而又耗时的工作。另外，我们可能需要将这些数据提供给多种语言编写的内部服务来使用，或者以不同的Web协议将结果提供给外部开发者，如XML、JSON或SOAP。

那么这里的优美解决方案，就是利用元数据来封装数据类型和描述API的方法签名。Facebook的工程师创建开源的跨语言进程间通信（IPC）系统，名为Thrift（http://developers.facebook.com/thrift），干净利落地实现了这个目标。

深入一步，例6-7展示了一个针对1.0版API的“.thrift”文件的例子，在这个版本里，Thrift包实现了这个API的大部分机制。

例6-7：通过Thrift对Web服务定义

xsd_namespace http://api.facebook.com/1.0/

/***

*Definition of types available in api.facebook.com version 1.0

*/

typedef i32 uid

typedef string uid_list

typedef string field_list

struct location{

1：string street xsd_optional，

2：string city，

3：string state，

4：string country，

5：string zip xsd_optional

}

struct user{

1：uid uid，

2：string name，

3：string books，

4：string pics，

5：location current_location

}

service FacebookApi10{

list＜uid＞friends_get（）

throws（1：FacebookApiException error_response），

list＜user＞users_getInfo（1：uid_list uids，2：field_list fields）

throws（1：FacebookApiException error_response），

}

这个例子中的类型是原生类型（string）、结构（location、user）或泛型方式的集合（list＜uid＞）。因为每个方法描述都有精心设计类型的方法签名，定义复用的类型的代码就可以直接在任何语言中生成。例6-8展示了针对PHP的部分生成结果。

例6-8：Thrift生成的服务代码

class api10_user{

public$uid=null；

public$name=null；

public$books=null；

public$pic=null；

public$current_location=null；

public function__construct（$vals=null）{

if（is_array（$vals））{

if（isset（$vals['uid]））{

$this-＞uid=$vals['uid']；

}

if（isset（$vals['name']））{

$this-＞name=$vals['name']；

}

if（isset（$vals['books']））{

$this-＞books=$vals['books']；

}

if（isset（$vals['pic']））{

$this-＞pic=$vals['pic']；

}

if（isset（$vals['current_location']））{

$this-＞current_location=$vals['current_location']；

}

//……

}

//……

}

返回user类型的所有内部方法都会创建全部需要的字段，结束的语句类似例6-9的样子。

例6-9：一致地使用生成的类型

return new api_10_user（$field_vals）；

例如，如果current_location（当前位置）出现在这个用户对象中，那么$field_vals['current_location']就会在例6-9的代码执行之前，被赋值为new api_10_user（……）。字段的名称和类型本身实际上会生成XML输出所需的schema，以及相应的XML Schema文档（XSD）。例6-10展示了整过RPC过程实际输出的XML。

例6-10：Web服务调用的XML输出

＜users_getInfo_response list="true"＞

＜users type="list"＞

＜user＞

＜name＞Dave Fetterman＜/name＞

＜books＞Zen and the Art,The Brothers K,Roald Dahl＜/books＞

＜pic＞＜/pic＞

＜current_location＞

＜city＞San Francisco＜/city＞

＜state＞CA＜/state＞

＜zip＞94110＜/zip＞

＜/current_location＞

＜/user＞

＜/users＞

＜/users_getInfo_response＞

Thrift生成类似的代码来声明RPC函数调用、序列化成已知的输出格式，并将内部的异常转化成外部错误代码。其他像XML-RPC或SOAP这样的工具集也提供这样一些好处，但可能需要更多的CPU和带宽开销。

使用像Thrift这样的漂亮工具有以下好处：

自动化类型同步

在user类型中添加“favorite_records”，或将uid转换成i64需要在所有使用或生成这些类型的方法中进行。

自动化绑定生成

所有读写类型的麻烦工作都不需要了，转换函数调用生成XML的RPC方法要求函数声明、类型检查和错误处理，这些都由Thrift自动完成。

自动化文档

Thrift生成公开的XML Schema文档，它将作为外界看到的无二义的文档，通常比在“手册”上看到的文档要好得多。这种文档也可以直接在一些外部工具中使用，生成客户端的绑定。

跨语言同步

这个服务可以由外部的XML客户端或JSON客户端调用，内部是通过各种语言（PHP、Java、C++、Python、Ruby、C#等）写的服务程序通过套接口来通信的。这要求基于元数据的代码生成，这样服务的设计者就不必在每次小改动时花时间更新这些代码。

我们已经有了社会关系网站服务的数据组件。接下来我们将弄清楚如何建立这些会话键，在所有Facebook扩展上强制实现用户期望的隐私模型。

6.2.2　简单的Web服务认证握手

一个简单的认证策略让我们能够在尊重Facebook用户的隐私观点的前提下访问这些数据。用户对Facebook系统的数据有某种特定的视图，这取决于用户是谁、用户的隐私设定，以及与用户有关系的人的隐私设定。用户可以授权单个应用来继承这一视图。用户通过某个应用可以看到的信息，是用户通过Facebook可以看到的信息中有意义的一部分（但不会超出通过Facebook可以看到的信息）。

在独立应用站点的架构中（图6-1），用户认证通常采用浏览器发送cookie的方式，这些cookie是该站点在最初执行过认证动作之后生成的。但是在图6-2中，通常作为Facebook用法一部分的cookie不再提供了—外部应用需要在没有用户浏览器的帮助下从Facebook平台请求信息。为了修正这一点，我们在会话键映射的基础上设计Facebook，如例6-11所示。

例6-11：会话键映射

get_session：{user_id,application_id}-＞session_key

Web服务的客户端只要在每次请求时发送session_key，让Web服务知道这代表的是哪个用户的请求执行。如果用户（或Facebook）禁用了这个应用，或者他从未用过这个应用，安全检查就会通不过，会返回一个错误。否则，外部应用站点会把这个会话键记入它自己的用户记录，或者放到该用户的cookie中。

但在最开始如何得到这个会话键呢？在http://fettermansbooks.com应用代码中的establish_facebook_session是一个占位符，为这个过程保留的。每个应用都有它自己特有的“应用键”（也称为api_key），开始应用认证流程（图6-3）：

1.用户通过一个已知的api_key重定向到Facebook登录界面。

2.用户在Facebook上输入口令，对这个应用授权。

3.用户带着会话键和用户ID重定向到已知的应用。

4.应用现在获得了授权，可以代表用户调用API方法（除非会话超时或被删除）。

要帮助用户发起这个流程，可以使用下面包含应用键（即“abc123”）的链接或按钮：

＜a href="http://www.facebook.com/login.php?api_key=abc123"＞

如果用户通过Facebook上口令输入同意授权给这个应用（注意，口令是Facebook最需要保护的数据），用户就被重定向回这个应用站点，带着有效的会话键和Facebook用户ID。这个会话键是非常私密的，所以对于将来的验证，应用的所有调用都会带有从这个共享秘密生成的散列值。

假定开发者隐藏了他的api_key和应用私密数据，establish_facebook_session可以很简单地按图6-3中的流程来编写。尽管这种类型的系统握手的细节可以不同，但重要的是只有当用户在Facebook上的关键步骤中输入了他的口令，才会产生授权。很有趣的是，一些早期的应用只是使用了这种认证握手来作为它们的口令系统，而根本没有使用其他的Facebook数据。

图　6-3：对Facebook平台API的认证访问

但是，某些应用不容易适应这种第二步“重定向”的方式。“桌面”风格的应用、基于设备的应用（如手机应用），或浏览器内建的应用有时候也相当有用。在这种情况下，我们采用一种稍微不同的机制来使用第二次认证令牌。令牌是应用通过API请求得到的，在第一次登录时传递给Facebook，然后在现场用户认证之后，应用换到一个会话键和会话专有的一些私密信息。

6.3　创建社会关系数据查询服务

通过一个带有用户控制的认证握手的Web服务，我们已经将我们的内部库扩展到外部世界。通过这个简单的改变，Facebook的社会关系数据现在可以驱动其用户决定认证的任何其他应用程序，通过普遍关注的社会关系上下文，在应用的数据中创建新的关系。

随着用户渐渐了解这些数据交换的无缝性，使用这些平台API的开发者知道这些数据集是很独特的。开发者访问自己的数据的模式与访问Facebook数据的模式有着很大的不同。例如，Facebook的数据位于HTTP请求的另一端，通过许多HTTP连接来调用这些方法增加了开发者自己页面的延迟和开销。他自己的数据库也提供了更大粒度的访问，优于Facebook平台API中的几十个方法。使用他自己的数据和SQL这样熟悉的查询语言，他可以选择一个表的特定字段，对结果集排序或进行限制，匹配其他的指标，或进行嵌套查询。如果平台的API不能够让开发者在平台的服务器上进行智能的处理，开发者就必须经常获取相关数据的超集，收到数据后再在他自己的服务器上进行这些标准的逻辑转换。这可能成为严重的负担。

实际问题：从Facebook平台API获取数据要比获取内部数据的开销大很多。

随着应用越来越多地使用外部数据平台，诸如带宽占用、CPU负载和请求延迟等因素很快累积起来。难道我们没有在自己的单个应用栈的数据层中对此进行优化吗？没有技术让我们通过一次调用取得多个数据集吗？如果在这个数据层中进行选择、限制和排序，结果会怎样？

数据解决方案：类似内部数据采用的模式，实现外部数据访问模式：一种查询服务。

Facebook的解决方案称为FQL，我们将在6.3.2节中详细介绍。FQL很像SQL，但它将平台数据转换成字段和表，而不是简单松散地定义为XML schema中的对象。这让开发者能够在Facebook的数据上使用标准的数据查询语义，这种方式可能与他们取得自己数据的方式一样。同时，将计算推到平台一端的好处与将操作通过SQL推到数据层的好处是相似的。在这两种情况下，开发者有意识地避免了在应用逻辑中进行这种处理的代价。FQL代表了基于Facebook的内部数据的另一项数据架构改进，是标准的黑盒Web服务的进步。但是首先，我们先来看一种容易而明显的方法，它让开发者能够消除多次数据请求的来回开销，同时我们也要说明为什么这是不够的。

6.3.1　批量方法调用

对于负载问题最简单的解决方案，就是类似于Facebook的batch.runAPI方法。这消除了多次通过HTTP栈对http://api.facebook.com进行调用的来回开销，一批接受多个方法调用的输入，一次返回输出的多棵XML树。在客户端，这个过程转变成类似例6-12中的代码。

例6-12：批量方法调用

$facebook-＞api_client-＞begin_batch（）；

$friends=＆$facebook-＞api_client-＞friends_get（）；

$notifications=＆$facebook-＞api_client-＞notifications_get（）；

$facebook-＞api_client-＞end_batch（）；

在Facebook平台的PHP5客户端库中，end_batch实际上是向平台服务器发起请求，取得所有结果，并针对每个结果更新引用的变量。这里我们从一次用户会话中批量获取了用户数据。通常，人们用批量查询机制将许多设置操作归为一组，如大量的Facebook个人描述更新，或大量突发的用户通知。

这些批量操作很有效，但这也揭示了这种批量操作的主要问题。问题是，每次调用必须与其他调用的结果无关。对多个不同用户的批量操作通常具备这种特点，但有一种常见的情况仍然不能处理，即使用一次调用的结果作为下次调用的输入。例6-13展示了不能利用批量机制的一种常见情况。

例6-13：批量机制的不正确用法

$fields=array（'uid，'name'，'books'，'pic'，'current_location'）；

$facebook-＞api_client-＞begin_batch（）；

$friends=＆$facebook-＞api_client-＞friends_get（）；

$user_info=＆$facebook-＞api_client-＞users_getInfo（$friends，$fields）；//NO！

$facebook-＞api_client-＞end_batch（）；

当客户端发出users_getInfo请求时，$friends的内容显然还不存在。

FQL模型优雅地解决了这个问题和其他问题。

6.3.2　FQL

FQL是一种简单的查询语言，它包装了Facebook的内部数据。输出的格式通常与Facebook平台API的输出格式一样，但输入超出了简单的RPC库的模型，变成了SQL的查询模型：命名的表和字段，包含已知的关系。像SQL一样，这种技术添加了选择实例或范围的能力，从数据行中选择字段子集的能力，并通过嵌套查询将更多的工作推到数据服务器端，避免了通过RPC栈进行多次调用。

举个例子，如果期望的输出是所有用户中我朋友的“uid”、“name”、“book”、“pic”和“current_location”字段，在我们的纯API模型中，我们会使用例6-14中的过程。

例6-14：在客户端串联方法调用

$fields=array（'uid'，'name'，'books'，'pic'，'current_location'）；

$friend_uids=$facebook-＞api_client-＞friends_get（）；

$user_infos=users_getInfo（$friend_uids，$fields）；

这导致了对数据服务器的多次调用（这里是2次），更大的延迟，更大的失败可能性。相反，对于查看用户编号8055（实际上是你的），我们在例6-15中写出这样的FQL语法并进行一次调用。

例6-15：利用FQL在服务器端串联方法调用

$fql="SELECT uid,name,books,pic,current_location FROM profile

WHERE uid IN（SELECT uid2 from friends where uid1=8055）"；

$user_infos=$facebook-＞api_client-＞fql_query（$fql）；

我们在概念上将users_getInfo引用到的数据视为一个表，它基于一个索引（uid），包含一些可选择的字段。如果正确地扩展，这种新的语法可以支持一些新的数据访问能力：

·限定范围查询（例如根据事件发生的时间）。

·嵌套查询（SELECT fields_1 FROM table WHERE field IN（SELECT fields_2 FROM……））。

·结果集大小限制和排序。

FQL的架构

开发者通过fql_query API来调用FQL。问题的要点是在FQL的命名“表”和“字段”中，统一外部API的命名“对象”和“属性”。我们仍然继承了标准API的流程：通过内部方法取得数据，应用跟这个方法的API调用相关的规则，然后根据第6.2.1节介绍的Thrift系统，转换到输出。对于每个数据读取API方法，在FQL中都有一个对应的“表”，代表了这次查询背后的数据抽象。例如，API方法users_getInfo，它提供给定用户ID的姓名、照片、书籍和当前位置等字段，在FQL中它就表现为用户表和对应的字段。fql_query的输出实际上也符合标准API的输出（如果修改XSD来允许省略对象小的字段），所以在用户表上调用fql_query返回的输出与相应的users_getInfo调用是等价的。事实上，像user_getInfo这样的调用在Facebook的服务器端通常是实现为FQL调用的！

注意：在编写本章时，FQL只支持SELECT，不支持INSERT、UPDATE、REPLACE、DELETE和其他操作，所以只有读取方法可以通过FQL来实现。大多数操作这类数据的Facebook平台API方法现在是只读的。

我们从这个用户表开始，以它为例，创建FQL系统来支持对它的查询。在平台的各个数据抽象层之下（内部调用、users_getInfo外部API调用，以及新的FQL的用户表），想象Facebook在自己的数据库中有一个名为“user”的表（例6-16）。

例6-16：Facebook数据表示例

＞describe user；

+———————+———————+——-+

|Field|Type|Key|

+———————+———————+——-+

|uid|bigint（20）|PRI|

|name|varchar（255）||

|pic|varchar（255）||

|books|varchar（255）||

|loc_city|varchar（255）||

|loc_state|varchar（255）||

|loc_country|varchar（255）||

|loc_zip|int（5）||

+———————+———————+——-+

在Facebook的程序栈中，支持我们访问这个表的方法是：

function user_get_info（$uid）

它在我们选择的语言（PHP）中返回一个对象，通常此后再应用隐私逻辑，并展现在http://facebook.com上。我们的Web服务实现做的事情相当类似，将Web请求的GET/POST内容转给这样一个调用，得到类似的栈对象，应用隐私逻辑，然后通过Thrift将它变成一个XML响应（图6-2）。

我们可以在FQL中将user_get_info包装起来，实际实现这个模型，将表、字段、内部函数和隐私组织成一个逻辑上的、可重复的形式。

下面是例6-15中的FQL调用创建的一些关键对象，以及描述它们的关系的方法。讨论所有的字符串解析、语法实现、可选索引、交集查询和实现许多不同的组合表达式（比较、“in”语句、交集、非交集）超出了本章的范围。这里我们只是关注面向数据的部分：FQL中数据的对应字段和表对象的高层规范，并将查询输入语句转换每个字段的can_see和evaluate函数（例6-17）。

例6-17：FQL字段和表示例

class FQLField{

//e.g.table="user"，name="current_location"

public function__construct（$user，$app_id，$table，$name）{……}

//mapping："index"id-＞{0，1}（visible or invisible）

public function can_see（$id）{……}

//mapping："index"id-＞Thrift-compatible data object

public function evaluate（$id）{……}

}

class FQLTable{

//a static list of contained fields：

//mapping：（）-＞（'books'=＞'FQLUserBooks'，'pic'-＞'FQLUserPic'，……）

public function get_fields（）{……}

}

FQLField和FQLTable对象构成了这个访问数据的新方法。FQLField包含了针对数据的逻辑，将“行”（如用户ID）和查看者的信息（用户和app_id）转换成我们内部的栈数据调用。在此之上，我们确保隐私评估利用要求的can_see方法得以正确实现。在我们处理一个请求时，我们可以在内存中为每个命名的表格（“user”）创建这样一个FQLTable对象，为每个命名的字段创建一个FQLField对象（为“books”创建一个，为“pic”创建一个，等等）。对应到一个FQLTable中的每个FQLField对象一般会使用底层相同的数据访问程序（在下面的例子里，是user_get_info），虽然不一定是这样—这只是一个方便的接口。例6-18展示了用户表中典型的字符串字段的例子。

例6-18：将核心数据库映射到FQL字段定义

//base object for any simple FQL field in the user table.

class FQLStringUserField extends FQLField{

public function__construct（$user，$app_id，$table，$name）{……}

public function evaluate（$id）{

//call into internal function

$info=user_get_info（$id）；

if（$info＆＆isset（$info[$this-＞name]））{

return$info[$this-＞name]；

}

return null；

}

public function can_see（$id）{

//call into internal function

return can_see（$id，$user，$table，$name）；

}

}

//simple string data field

class FQLUserBooks extends FQLStringUserField{}

//simple string data field

class FQLUserPic extends FQLStringUserField{}

FQLUserPic和FQLUserBooks的区别仅限于它们的内部属性$this-＞name，这是由它们的构造方法在处理过程中设置的。请注意，在底层，我们针对表达式中需要的每次求值调用user_get_info；只有系统将这些结果缓存在内存中，才能取得较好的性能。Facebook的实现就是这样做的，整个查询执行的时间与标准平台API调用的时间是同一量级的。

下面是一个更复杂的字段，表示current_location，它采用的是同样的输入，展示了同样的使用模式，但输出了一个我们前面曾看到过的结构类型对象（例6-19）。

例6-19：更复杂的FQL字段映射

//complex object data field

class FQLUserCurrentLocation extends FQLStringUserField{

public function evaluate（$id）{

$info=user_get_info（$id）；

if（$info＆＆isset（$info['current_location']））{

$location=new api10_location（$info['current_location']）；

}else{

$location=new api10_location（）；

}

return$location；

}

}

像api10_location这样的对象是6.2.1小节中所说的生成的类型，Thrift和Facebook数据服务知道如何将它返回为良好类型的XML。现在我们知道，为什么就算是新的输入形式，FQL的输出也不会与Facebook API产生不兼容的情况。

在下面的例子中，FQLStatement的主要求值循环告诉了我们FQL实现的大致思想。在这段代码中我们引用了FQLExpression，但在简单的查询中，我们更有可能提到的是FQLFieldExpression，它包装了对FQLField自己的求值和can_see方法的内部调用，如例6-20所示。

例6-20：一个简单的FQL表达式类

class FQLFieldExpression{

//instantiated with an FQLField in the"field"property

public function evaluate（$id）{

if（$this-＞field-＞can_see（$id））

return$this-＞field-＞evaluate（$id）；

else

return new FQLCantSee（）；//becomes an error message or omitted field

}

public function get_name（）{

return$this-＞field_name；

}

}

要发起整个流程，类似SQL的字符串输入通过lex和yacc转换成主要FQLStatement的$select表达式数组和$where表达式。FQLStatemenet的evaluate（）函数将返回我们请求的对象。例6-21中的主语句求值循环包括了以下步骤，说明了简单的大致顺序：

1.取得我们希望返回的行在索引上的约束。例如，如果在用户表上选取，这就是我们想查询的那些UID。如果我们在一个按时间索引的事件表上查询，这就是时间边界。

2.将这些转换成表的规范ID。用户表也可以按字段名查询，如果FQL表达式使用了字段名称，这个函数就会使用内部的user_name到user_id的查找函数。

3.针对每个候选ID，看看它是否满足RHS表达式子句（布尔逻辑、比较、“IN”操作等）。如果不满足，就抛弃它。

4.对每个表达式求值（在我们的例子里，就是SELECT子句中的字段），然后创建＜COL_NAME＞COL_VALUE＜/COL_NAME＞格式的XML元素，其中COL_NAME是FQLTable中的字段名称，COL_VALUE是字段通过它对应的FQLField的求值函数进行求值的结果。

例6-21：FQL的主求值流程

class FQLStatement{

//contains the following members：

//$select：array of FQLExpressions from the SELECT clause of the query

//corresponding to,say，"books"，"pic"，and"name"

//$from：FQLTable object for the source table

//$where：FQLExpression containing the constraints for the query.

//$user，$app_id：calling user and app_id

public function__construct（$select，$from，$where，$user，$app_id）{……}

//A listing of all known tables in the FQL system.

public static$tables=array（

'user'=＞'FQLUserTable'，

'friend'=＞'FQLFriendTable'，

）；

//returns XML elements to be translated to service output

public function evaluate（）{

//based on the WHERE clause,we first get a set of query expressions that

//represent the constraints on values for the indexable columns contained

//in the WHERE clause

//Get all"right hand side"（RHS）constants matching ids（e.g.X,in'uid=X'）

$queries=$this-＞where-＞get_queries（）；

//Match to the row's index.If we were using'name'as an alternative index

//to the user table,we would transform it here to the uid.

$index_ids=$this-＞from_table-＞get_ids_for_queries（$queries）；

//filter the set of ids by the WHERE clause and LIMIT params

$result_ids=array（）；

foreach（$ids as$id）{

$where_result=$this-＞where-＞evaluate（$id）；

//see if this row passes the'WHERE'constraints

//is not restricted by privacy

if（$where_result＆＆！（$where_result instanceof FQLCantSee））

$result_ids[]=$id；

}

$result=array（）；

$row_name=$this-＞from_table-＞get_name（）；//e.g."user"

//fill in the result array with the requested data

foreach（$result_ids as$id）{

foreach（$this-＞select as$str=＞$expression）{//e.g."books"or"pic"

$name=$expression-＞get_name（）；

$col=$expression-＞evaluate（$id）；//returns the value

if（$col instanceof FQLCantSee）

$col=null；

$row-＞value[]=new xml_element（$name，$col）；

}

$result[]=$row；

}

return$result；

}

FQL还有其他一些精妙之处，但这个总体流程说明了已有的内部数据访问和隐私规则实现与全新的查询模型的结合。这让开发者能够更快地处理它的请求，能够以比API更好的粒度来访问数据，同时又保持了SQL类似的语法。

由于我们的许多API在内部包装了对应的FQL方法，我们的整体架构演变为图6-4所示的状况。

图　6-4：通过Web和查询服务来使用Facebook数据的应用栈

6.4　创建一个社会关系Web门户：FBML

前面讨论的服务让外部的应用栈能够在它们的系统中包含社会关系平台的数据，这是很大的进步。这些数据架构实现了让社会关系平台数据更开放的承诺：外部应用（如http://fettermansbooks.com）和数据平台（如http://facebook.com）的共同用户可以共享信息，每个新的社会关系应用就不需要一个新的社会关系网络。但是，即使有了这些新的能力，这些应用还是不能享受Facebook这样的社会关系网站的全部强大功能。应用还需要让许多用户发现，才会变得有价值。而且，并不是所有支持社会关系平台的内部数据都可以提供给这些外部的应用栈。平台的创建者需要解决这些问题，我们将依次讨论。实际问题：对于社会关系应用来说，要获得引人注目的关键性用户数，支持它的社会关系网络上的用户必须要能注意到其他用户在利用这些应用进行交互。这意味着应用与社会关系网站更深层次上的集成。

这个问题在早期的软件中就存在了：我们难以让数据、产品或系统得到广泛使用。缺少用户成为Web 2.0空间中特别值得一提的困难，因为如果没用户使用并且（特别是）生成内容，我们的系统什么时候才有用呢？

Facebook支持大量的用户，他们对在社会联系之间共享信息感兴趣，而且Facebook的特点就是把应用的内容和它自己的内容等同视之。让外部的应用出现在Facebook站点上，就会让大小开发者开发的应用更容易发现，帮助他们获得支持好的社会关系功能所需的关键性用户数。

不管我们创建什么解决方案，应用都需要在Facebook站点上有独特的显示展现。Facebook平台向应用提供了这方面的支持，为Facebook上的应用内容展现保留了URL路径http://apps.facebook.com/fettermansbooks/……。我们稍后会看到平台是如何集成应用的数据、逻辑和显示的。

6.2.1　节和6.3.2节中介绍的数据服务衍生出第二个问题，它也同样不好处理。

实际问题：外部应用不能够使用Facebook没有通过Web服务暴露出来的那些核心数据元素。在Facebook提供网站（http://facebook.com）的内容时，Facebook为它的用户提供了大量的数据。隐私信息本身（第6.1.2节中提到的can_see映射）就是一个好例子—不能被Facebook站点的用户显式地看到，can_see映射对于数据服务也是不可见的。但是强制实现Facebook用户的这种隐私设置是所有良好集成的应用的特点，也是对社会关系系统上用户期望的支持。Facebook为了保护用户的隐私，不能通过数据服务将这些数据开放出来。开发者怎样才能利用这些数据呢？

对这些问题的最优雅解决方案就是结合Facebook的数据和外部应用的数据、逻辑和显示，同时让用户在一个受信任的环境下操作。

数据解决方案：开发者通过一种数据驱动的标记语言，在社会关系站点上创建应用执行和显示的内容，与Facebook交互。

只使用第6.2.1节和第6.3.2节中介绍的Facebook平台元素的应用，在Facebook之外创造了一种社会关系体验，因Facebook的社会关系数据而变得更强大。利用本节介绍的数据和Web架构，应用本身也变成一种数据服务，支持针对Facebook的内容显示在http://apps.facebook.com之下。像http://apps.facebook.com/fettermansbooks/……这样的URL不再映射到Facebook生成的数据、逻辑和显示，而是会查询http://fettermansbooks.com的服务，生成应用的内容。

我们必须同时记得我们的资产和约束。一方面，我们有一个访问频率很高的社会关系系统，让用户能发现外部的内容，并有大量的社会关系数据来增强这种社会关系应用。另一方面，请求需要从社会关系站点（Facebook）上发起，将应用作为服务来使用，然后将内容渲染成HTML、JavaScript和CSS，并且不违反Facebook用户的隐私或期望。

首先，我们来看一些不正确的尝试。

6.4.1　Facebook上的应用：直接渲染HTML、CSS和JS

假定一个外部应用的配置现在包含两个字段，名为application_name和callback_url。通过输入“fettermansbooks"这样的名字和http://fettermansbooks.com/fbapp/这样的URL,http://fettermanbooks.com声明它将在自己的服务器上为用户提供服务，对http://apps.facebook.com/fettermansbooks/PATH?QUERY_STRING的请求将转向http://fettermansbooks.com/fbapp/PATH?QUERY_STRING。

对http://apps.facebook.com/fettermansbooks/……的请求于是简单地取出应用服务器上的HTML、JS和CSS等内容，并在Facebook上的页面主内容区域进行显示。这基本上是将外部站点作为一个HTML Web服务来渲染的。

这对应用的n层模型进行了重要改变。以前，应用栈会通过数据服务来使用Facebook的内容，这个数据服务是直接服务于对http://fettermansbooks.com的请求的。现在，应用在它的Web根下维护了一个树型结构，它自己提供HTML服务。Facebook通过在线请求这个新应用服务（该服务又可能用到Facebook的数据服务）而取得内容，将它包装成一般的Facebook站点导航元素，显示给用户。

但是，如果Facebook直接在它的页面中渲染一个应用的HTML、JavaScript或CSS，这就会允许应用完全违反用户对http://facebook.com上受控体验的期望，让站点和用户暴露在各种安全攻击之下。允许外部用户直接订制标记语言和脚本几乎从来都不是好主意。实际上，代码或脚本注入通常是攻击者的目标，所以这并不是一个很好的特征。

而且，没有新数据！尽管这为应用栈的改变奠定了基础，但这个解决方案没有完全解决前面的两个实际问题。

6.4.2　Facebook上的应用：iframe

还有一种更安全的显示应用内容的方法，可以显示另一个站点的可视化上下文和界面流转，这种方法已包含在浏览器中，即iframe。

为了复用前一节中提到的映射，对http://apps.facebook.com/fettermans books/PATH?QUERY_STRING的请求将导致输出这样的HTML：

＜iframe src="http://fettermansbooks.com/fbapp/PATH?GET_STRING"＞＜/iframe＞

这个URL的内容将显示在Facebook页面的一个帧中，在它自己的沙盒环境中可以包含任何类型的Web技术：HTML、JS、AJAX、Flash等。

这实际上是让浏览器成为请求代理者，而不是由Facebook作为请求代理者。这比前一节中的模型有改进，浏览器也维护所得页面中其他元素的安全性，所以开发者可以在这个帧中随意创建他们想要的用户体验。

对于某些应用，如果开发者希望花最小的代价将他们的代码从他们的站点移到平台上，那么iframe的方式也是有意义的。实际上，Facebook继续支持完整页面生成的iframe模型。虽然这解决了第一个实际问题，将应用纳入到社会关系站点，但第二个实际问题仍未解决。虽然基于iframe的请求流程可以确保安全，但除了API服务暴露出来的数据之外，这些开发者并不能利用其他的新数据。

6.4.3　Facebook上的应用：FBML是数据驱动的执行标记语言

前两节中提到的解决方案尝试都有其优点。HTML的解决方案采用了直观的方法，将应用本身变成Web服务，将触点带回到Facebook上显示。iframe方式的好处在于将开发者的应用内容放在一个独立的（安全的）执行沙盒中。最佳解决方案将保留“应用即服务”的模型和iframe的安全和可信，同时又让开发者能够使用更多的社会关系数据。

问题是，为了让社会关系应用提供独特的使用体验，开发者必须通过他们自己的应用栈来提供数据、逻辑和展现。但是，生成这些输出必须用到那些不能离开Facebook的用户数据。

解决方案是什么？不是发回HTML，而是一种特定的标记语言，其中定义了足够的标记来表现应用的逻辑和显示，也包含对受保护数据的请求，完全让Facebook在受信任的服务器环境中渲染它！这就是FBML的前提（图6-5）。

在这个流程中，对http://apps.facebook.com的请求同样被转换成对应用的请求，应用栈会使用Facebook的数据服务。但是，开发者不会让应用返回HTML，而是重写应用，返回FBML。FBML中包含了许多HTML元素，而且添加了Facebook特别定义的标签。当这个请求返回其内容时，Facebook的FBML解释器将这段标记语言转换成它自己的数据、执行和显示实例，生成应用页面。用户就会收到一个页面，其中包含了Facebook页面的一般Web元素，而且也包含了应用的数据、逻辑和观感。不论FBML返回什么，它都能在技术上确保Facebook强制实现其隐私理念和良好的用户体验元素。

图　6-5：应用即FBML服务

FBML是XML的一个特例，它包含了许多熟悉的HTML标签，增加了在Facebook上显示的平台专有的标签。FBML同样体现了FQL的高级模式：修改已知的标准（HTML，对FQL来说就是SQL），将执行和决定延迟到Facebook平台服务器上进行。如图6-5所示，FBML解释器让开发者通过FBML数据，自己能够控制在Facebook服务器上执行的逻辑和显示。这是数据处于执行中心的绝妙例子：FBML只是声明式的执行，而不是必须服从的控制流（如在C、PHP等语言中）。

现在来看具体细节。FBML是一个XML实例，所以它由标签、属性和内容组成。标签可以在概念上分成以下几大类。

直接的HTML标签

如果FBML服务返回标签＜p/＞，Facebook将在输出页上直接渲染为＜p/＞。作为Web展现的基石，大多数HTML标签都是支持的，少数违反Facebook层面的信任或设计期望的标签除外。

所以FBML字串＜h2＞Hello,welcome to＜i＞Fetterman's books！＜/i＞＜/h2＞在渲染成HTML时，实际上是保持不变的。

数据显示标签

这里是体现数据威力的地方。假定个人简介照片不能转到其他站点。通过指定＜fb：profile-pic uid="8055"＞，开发者就可以在他们的应用中显示更多的Facebook用户信息，同时不要求用户完全信任开发者，将这部分信息交给开发者处理。

例如：

＜fb：profile-pic uid="8055"linked="true"/＞

翻译成FBML：

＜a href="http://www.facebook.com/profile.php?id=8055"

onclick="（new Image（））.src='/ajax/ct.php?app_id=……"＞

＜img uid="8055"linked="true"src="……"alt="Dave Fetterman"title="Dave Fetterman"/＞

＜/a＞

注意：复杂的onclick属性在生成时会在Facebook页面显示中限制Javascript。

请注意，即使信息受到了保护，这些内容也不会返回到应用栈中，只是显示给用户看。在容器端执行使这些数据可以查看，但不要求将它们交给应用程序！

数据执行标签

作为使用隐藏数据的一个更好的例子，用户的隐私限制只能通过内部的can_see方法来访问，它是应用体验的一个重要部分，但不能通过数据服务从外面进行访问。利用＜fb：-if-can-see＞标签和其他类似的标签，应用可以通过属性来指定一个目标用户，这样只有当查看者能够看到目标用户的特定内容时，那些子元素才会渲染出来。因此，隐私数据本身不会暴露给应用，同时应用又能满足强制实现的隐私设置。

从这个角度来说，FBML是一个受信任的声明式执行环境，与C或PHP这样的必须服从的执行环境不同。严格来说，FBML不像这些语言那样是“图灵完备”的（例如，没有提供循环结构）。像HTML一样，除了树状遍历所隐含的状态外，在执行时不保存任何状态；

例如，＜fb：tab-item＞只在＜fb：tabs＞之内有意义。但是，通过让受信任系统中的用户获得数据，FBML提供了大量功能，这些功能正是大多数开发者希望提供给他们的用户的。

FBML实际上有助于定义执行应用的逻辑和显示，同时又让应用可以在应用服务器上显示独特的内容。

只面向设计的标签

Facebook因其设计标准而受到称赞，许多开发者都选择以某种方式复用Facebook的设计元素，保持Facebook的观感。通常，他们是通过利用http://facebook.com的JavaScript和CSS来实现的，但FBML提供了类似“设计宏”的库，以更可控的方式来满足这种需求。例如，Facebook应用已知的CSS类来，将输入＜fb：tabs＞……/fb：tabs＞渲染成特定的tab标签结构，位于开发者页面的顶部。这些设计元素也可以包含执行语义，如＜fb：narrow＞……＜/fb：narrow＞只有在这次执行显示用户简介框中的少数列时，会在FBML中渲染它的子内容。

例6-22展示了一些使用只面向设计的标签的FBML。

例6-22：只面向设计的FBML的例子

＜fb：tabs＞

＜fb：tab-item href="http://apps.facebook.com/fettermansbooks/mybooks.php"

title='My Books'selected='true'/＞

＜fb：tab-item href="http://apps.facebook.com/fettermansbooks/recent.php"

title='Recent Reviews'/＞

＜/fb：tabs＞

这将渲染为一组可视的tab标签，链接到对应用的内容，并使用了Facebook自己的HTML、CSS和Javascript包。

替代HTML标签

HTML造成了一些信任风险，但没有暴露数据，所以FBML中的替代标签只是修改或限制一组参数，如Flash自动播放。这不是所有显示平台都严格要求的，它们只是强制应用满足容器站点的默认显示行为。但是，随着多个应用发展成为一个生态系统，它们都反映容器站点的观感，这种修改就变得很重要了。

请看这个FBML例子：

＜fb：flv src=http://fettermansbooks.com/newtitles.flv height="400"

width="400"title="New Releases"＞

这会翻译成一段相当长的JavaScript，渲染一个视频播放组件，这个元素由Facebook控制，特意禁止了自动播放这样的行为。

“功能包”标签

某些Facebook FBML标签包含了整套的常见Facebook应用功能。＜fb：friend-selector＞创建了类型前置的朋友选择器软件包，常见于许多Facebook页面，包括Facebook数据（朋友、主要网络）、CSS样式和针对键盘动作的JavaScript。像这样的标签让容器站点可以推广某些设计模式和应用间的公用元素，也让开发者能够快速实现他们想要的功能。

FBML：一个小例子

请回忆一下我们在创建假想的外部网站时，通过引入friends.get和users.getInfo API对原来的http://fettermansbooks.com代码实现改进。接下来我们将展示一个例子，看看FBML如何能够结合社会关系、私有业务逻辑和完全集成的应用的感觉。如果我们能够通过数据库调用book_get_all_reviews（$isbn）获得一本书的全部书评，那么我们就可以将朋友数据、私有业务逻辑和“墙式”风格结合起来，利用FBML在容器站点上显示书评，代码如例6-23所示。

例6-23：利用FBML创建一个应用

//Wall-style social book reviews on Facebook

//FBML Tags used：＜fb：profile-pic＞，＜fb：name＞，＜fb：if-can-see＞，

＜fb：wall＞

//from section 1.3

$facebook_friend_uids=$facebook_client-＞api_client-＞friends_get（）；

foreach（$facebook_friend_uids as$facebook_friend）{

if（$books_user_id=books_user_id_from_facebook_id（$facebook_friend））

$book_site_friends[]=$books_user_id；

}

//a hypothesized mapping,returning

//books_uid-＞book_review object

$all_reviewers=get_all_book_reviews（$isbn）；

$friend_reviewers=array_intersect（$book_site_friends,array_keys（$all_reviewers））；

echo'Friends'reviews：＜br/＞'；

echo'＜fb：wall＞'；

//put friends up top.

foreach（$friend_reviewers as$book_uid=＞$review）{

echo'＜fb：wallpost uid="'.$book_uid.'"＞'；

echo'（'.$review['score'].'）'.$review['commentary']；

echo'＜/fb：wallpost＞'；

unset（$all_reviewers[$book_uid]）；//don't include in nonfriends below.

}

echo'Other reviews：＜br/＞'；

//only nonfriends remain.

foreach（$all_reviewers as$book_uid=＞$review）{

echo'＜fb：if-can-see uid="'.$book_uid.'"＞'；//defaults to'search'visibility

echo'＜fb：wallpost uid="'.$book_uid.'"＞'；

echo'（'.$review['score'].'）'.$review['commentary']；

echo'＜/fb：wallpost＞'；

echo'＜/fb：if-can-see＞'；

}

echo'＜/fb：wall＞'；

虽然这采用的是输出FBML的服务的形式，而不是输出HTML的Web调用，但一般流程是不变的。这里，Facebook数据让应用能够在无关的书评之前，显示更多的相关书评（朋友的书评），并且使用了FBML来显示结果，采用了Facebook上相应的隐私逻辑和设计元素。

6.4.4　FBML架构

将开发者提供的FBML翻译成显示在http://facebook.com上的HTML，需要一些技术和概念综合作用：将输入字符串解析成一棵句法树，将这棵树中的标签转换成内部方法调用，应用FBML语法规则，保持容器站点的约束。像FQL一样，这里我们将关注点主要放在FBML与平台数据的交互上，对其他的技术则不作详细探讨。FBML处理了一个复杂的问题，FBML的全部实现细节是相当多的—我们省略的内容包括FBML的错误日志、为后来的渲染事先缓存内容的能力、表单提交结果的安全性签名等。

首先，看看解析FBML的低层问题。在继承了浏览器的某些角色的同时，Facebook也继承了它的一些问题。为了方便开发者，我们不要求提供的输入可以通过schema验证，甚至不要求是结构良好的XML—不封闭的HTML标签，如＜p＞（与XHTML不同，即＜p/＞）打破了输入必须作为真正的XML进行解析的假定。因为这一点，我们需要一种方法将输入的FBML字符串先转换成结构良好的句法树，包含标签、属性和内容。

为了做到这一点，我们采用了采用了一个开放源代码浏览器的一些代码。本章将这部分处理视为一个黑盒，所以我们现在假定，在接收到FBML并经过这样的处理流程后，我们得到了名为FBMLNode的树状结构，它让我们能够查询生成的句法树中任何节点的标签、属性键值对和原始内容，并能够递归查询子元素。

从最高的层面上看，我们可以注意到FBML出现在Facebook站点的所有地方：应用“画布”页面、新闻信号源的故事内容、个人简介框的内容等。每种上下文中或每种“风味”的FBML都定义了对输入的约束，例如，画布允许使用iframe，而个人简介框则不允许。很自然，因为FBML维护数据隐私的方式与API类似，所以执行上下文中必须包含查看用户的ID和生成该内容的应用ID。

所以，在我们真正开始有效使用FBML之前，先要看看环境的规则，它由FBMLFlavor类来封装，如例6-24所示。

例6-24：FBMLFlavor类

abstract class FBMLFlavor{

//constructor takes array containing user and application_id

public function FBMLFlavor（$environment_array）{……}

public function check（$category）{

$method_name='allows_'.$category；

if（method_exists（$this，$method_name））{

$category_allowed=$this-＞$method_name（）；

}else{

$category_allowed=$this-＞_default（）；

}

if（！$category_allowed））

throw new FBMLException（'Forbidden tag category'.$category.'in

this flavor.'）；

}

protected abstract function_default（）；

}

下面是这个抽象类的一个子类，它对应于渲染FBML的页面或元素。例6-25是一个例子。

例6-25：FBMLFlavor类的一个子类

class ProfileBoxFBMLFlavor extends FBMLFlavor{

protected function_default（）{return true；}

public function allows_redirect（）{return false；}

public function allows_iframes（）{return false；}

public allows_visible_to（）{return$this-＞_default（）；}

//……

}

这种风味类的设计很简单：它包含了隐私上下文（用户和应用），实现了检查方法，为稍后将展示的FBMLImplementation类中包含的丰富逻辑建立了规则。与平台API的实现层很像，这个实现类为服务提供了实际的逻辑的数据访问，其他的代码为这些方法提供了访问入口。每个Facebook特有的标签，如＜fb：TAG-NAME＞，将有一个对应的实现方法fb_TAG_NAME（例如，类方法fb_profile_pic将实现＜fb：profile-pic＞标签的逻辑）。每个标准的HTML标签也都有一个对应的处理方法，名为tag_TAG_NAME。这些HTML处理方法通常让数据无变化地通过，但是即便是对一些“普通”的HTML元素，FBML常常也需要进行检查和转换。

让我们来看看某些标签的实现，然后将它们结合起来讨论。每个实现方法都接收一个来自FBML解析器的FBMLNode，以字符串的方式返回输出的HTML。下面是一些直接的HTML标签、数据显示标签和数据执行标签的实现示例。请注意，这些程序清单用到了一些功能，在这里没有完整而详细地列出。

在FBML中实现直接的HTML标签

例6-26包含了＜img＞标签的内部FBML实现。图像标签的实现包含更多的逻辑，有时候需要将图像源的URL重写到Facebook服务器上图像缓存的URL。这体现了FBML的强大：应用栈可以返回与HTML非常相似的标记语言，支持它自己的站点，而Facebook可以通过纯技术的手段强制实现平台所要求的行为。

例6-26：fb：img标签的实现

class FBMLImplementation{

public function__construct（$flavor）{……}

//＜img＞：example of direct HTML tag（section 4.3.1）

public function tag_img（$node）{

//images are not allowed in some FBML contexts-

//for example,the titles of feed stories

$this-＞_flavor-＞check（'images'）；

//strip of transform attribute key-value pairs according to

//rules in FBML

$safe_attrs=$this-＞_html_rewriter-＞node_get_safe_attrs（$node）；

if（isset（$safe_attrs['src']））{

//may here rewrite image source to one on a Facebook CDN

$safe_attrs['src']=$this-＞safe_image_url（$safe_attrs['src']）；

}

return$this-＞_html_rewriter-＞render_html_singleton_tag（$node-＞

get_tag_name（），$safe_attrs）；

}

}

在FBML中实现数据显示标签

例6-27展示了通过FBML使用Facebook数据的例子。＜fb：profile-pic＞用到了uid、size和title属性，将它们结合起来，根据内部数据产生HTML输出，并符合Facebook的标准。在这个例子中，输出是指定用户名的个人简单照片，链接到用户的个人简介页面，只在当查看者能看到这部分内容时才显示。这个功能也存在于FBMLImplementation类中。

例6-27：fb：profile-pic标签的实现

//＜fb：profile-pic＞：example of data-display tag

public function fb_profile_pic（$node）{

//profile-pic is certainly disallowed if images are disallowed

$this-＞check（'images'）；

$viewing_user=$this-＞get_env（'user'）；

$uid=$node-＞attr_int（'uid'，0，true）；

if（！is_user_id（$uid））

throw new FBMLRenderException（'Invalid uid for fb：profile_pic（'.$uid.'）'）；

$size=$node-＞attr（'size'，"thumb"）；

$size=$this-＞validate_image_size（$size）；

if（can_see（$viewing_user，$uid，'user'，'pic'））{

//this wraps user_get_info,which consumes the user's 'pic'data field

$img_src=get_profile_image_src（$uid，$size）；

}else{

return''；

}

$attrs['src']=$img_src；

if（！isset（$attrs['title']））{

//we can include the user name information here too.

//again,this function would wrap internal user_get_info

$attrs['title']=id_get_name（$id）；

}

return$this-＞_html_renderer-＞render_html_singleton_tag（'img'，$attrs）；

}

FBML中的数据执行标签

FBML解析的递归本质使得＜fb：if-can-see＞标签就像是标准的必须服从的控制流中的if语句一样，它是FBML实际控制执行的一个例子。这是FBML实现类中的另一个方法，例6-28列出了它的细节。

例6-28：fb：if-can-see标签的实现

//＜fb：if-can-see＞：example of data-execution tag

public function fb_if_can_see（$node）{

global$legal_what_values；//the legal attr values（profile,friends,wall,etc.）

$uid=$node-＞attr_int（'uid'，0，true）；

$what=$node-＞attr_raw（'what'，'search'）；//default is 'search'visibility

if（！isset（$legal_what_values[$what]））

return''；//unknown value?not visible

$viewer=$this-＞get_env（'user'）；

$predicate=can_see（$viewer，$uid，'user'，$what）；

return$this-＞render_if（$node，$predicate）；//handles the else case

for us

}

//helper for the fb_if family of functions

protected function render_if（$node，$predicate）{

if（$predicate）{

return$this-＞render_children（$node）；

}else{

return$this-＞render_else（$node）；

}

}

protected function render_else（$node）{

$html=''；

foreach（$node-＞get_children（）as$child）{

if（$child-＞get_tag_name（）=='fb：else'）{

$html.=$child-＞render_children（$this）；

}

}

return$html；

}

public function fb_else（$ignored_node）{return''；}

如果某对“观察者-目标”通过了can-see检查，引擎就会递归地渲染＜fb：if-can-see＞节点的子节点。否则，就会渲染可选标签＜fb：else＞子节点下的内容。请注意fb_if_can_see直接访问＜fb：else＞子节点的方式；如果＜fb：else＞出现在这样的一个“if风格”的FBML标签之外，标签和它的子标签就不会返回任何内容。所以，FBML不仅仅是一个简单的转换式例程，它会注意到文档的结构，因此可以包含条件控制流的元素。结合在一起前面讨论的每个功能，都需要注册为一个回调，在解析输入的FBML时使用。在Facebook（以及它的开放源代码平台实现中），这个“黑盒”解析器是用C写的PHP扩展，每个回调都存在于PHP树中。要完成这种高层控制流，我们必须向FBML解析引擎声明这些标签。和其他地方一样，出于简单性考虑，例6-29也是经过了大量编辑的。

例6-29：FBML主要求值流程

//As input to this flow：

//$fbml_impl-the implementation instantiated above

//$fbml_from_callback-the raw FBML string created by the external

application

//a list of"Direct HTML"tags

$html_special=$fbml_impl-＞get_special_html_tags（）；

//a list of FBML-specific tags（＜fb：FOO＞）

$fbml_tags=$fbml_impl-＞get_all_fb_tag_names（）；

//attributes of all tags to rewrite specially

$rewrite_attrs=array（'onfocus'，'onclick'，/*……*/）；

//this defines the tag groups passed to flavor's check（）function

//（e.g.'images'，'bold'，'flash'，'forms'，etc.）

$fbml_schema=schema_get_schema（）；

//Send the constraints and callback method names along

//to the internal C FBML parser.

fbml_complex_expand_tag_list_11（$fbml_tags，$fbml_attrs，

$html_special，$rewrite_attrs，$fbml_schema）；

$parse_tree=fbml_parse_opaque_11（$fbml_from_callback）；

$fbml_tree=new FBMLNode（$parse_tree['root']）；

$html=$fbml_tree-＞render_html（$fbml_impl）；

FBML利用回调扩展了浏览器的解析技术，包装了由Facebook创建和管理的数据、执行和展现宏。这个简单的思想实现了应用的完全集成，支持使用通过API暴露出来的内部数据，同时保持安全性方面的用户体验。FBML本身几乎就是一种编程语言，它也是充分发展后的数据：外部提供的声明式执行，安全地控制了Facebook上的数据、执行和显示。

6.5　系统的支持功能

现在，开发者创建的软件运行在Facebook的服务之上，不仅是结合了界面组件，而是全部的应用。在这个过程中，我们创造了一个社会关系网络应用的完全不同的概念。我们从一个典型的Web应用的独立数据、逻辑和显示的标准设置开始，不考虑所有社会关系数据，只是让用户可以确信能够作出贡献。现在，我们取得了充分的进展，应用使用了Facebook的社会关系数据服务，同时它自己又成为一个FBML服务，完全集成到容器站点之中。

Facebook数据也获得了长足的发展，不再仅仅是本章第一节讨论的内部库。但是，仍有一些重要的、常见的Web使用场景和技术，目前平台还未能支持。通过将应用变成一个返回FBML的服务，而不是直接由浏览器解读的HTML/CSS/JS，我们接触到了关于现代Web应用的一些重要假定。让我们来看看Facebook平台如何修正这样一些问题。

6.5.1　平台cookie

应用的新Web架构排除了浏览器内建的一些技术，许多Web应用栈可能依赖于这些技术。可能其中最重要的一点是，过去浏览器用于保存用户与应用栈交互信息的cookie不再可以得到了，因为应用的目标消费者不再是浏览器，而是Facebook平台。

初看上去，伴随对应用栈的请求发送一些cookie似乎是一个不错的解决方案。但是，这些cookie的作用域现在是“http://facebook.com”，而实际上，cookie信息属于该应用领域所提供的用户体验。

解决方案是什么？让Facebook具有浏览器的职责，在Facebook自己的存储库中复制这种cookie功能。如果应用的FBML服务送回请求头，试图设置浏览器cookie,Facebook就保存这个cookie信息，以（user,application_id）对为主键。Facebook然后“重新创建”这些cookie，就像用户向这个应用栈发出后续请求时浏览器所做的一样。

这个解决方案很简单，在开发者从HTML栈方式转向FBML服务方式转变时，只需要很少的改变。请注意，当用户决定在这个应用提供的HTML栈上导航时，这种信息是不能使用的。另一方面，它可以有效地分离用户在Facebook上的应用体验和在应用的HTML站点上的应用体验。

6.5.2　FBJS

当应用栈作为一个FBML服务被使用，而不是直接由用户的浏览器来使用，Facebook就没有机会执行浏览器端的脚本。直接返回未修改过的开发者提供的内容（一个不充分的解决方案，这在FBML小节的一开始就讨论过）可以解决这个问题，但它违反了Facebook在显示体验上所加的约束。例如，当加载用户的简介页面时，Facebook不希望在加载事件上触发一个弹出窗口。但是，限制所有的JavaScript会排除许多有用的功能，如AJAX或在不重新加载的情况下动态操作页面的内容。

相反，FBML在解释开发者提供的＜script＞树和其他页面元素的内容时会考虑到这些约束。在此之上，Facebook提供了一些JavaScript库，让这些场景容易实现，同时又受到控制。这些修改共同构成了Facebook的平台JavaScript仿真套件，称为FBJS，它通过以下几点，让应用既动态又安全：

·重写FBML属性，确保实现虚拟文档范围。

·延迟激活脚本内容，直到用户在页面或元素上发起动作时。

·提供一些Facebook库，以受控的方式来实现常见的脚本使用场景。

很清楚，不是所有的实现自有平台的容器站点都需要这些修改，但FBJS向我们展示了几种解决方案，这样的新Web架构需要这些解决方案来绕过一些困难。我们在这里只展示了这些解决方案的一般思想，FBJS的许多部分还需要不断改进，与FBML和可扩展的专有JavaScript库进行融合。

首先，JavaScript通常可以访问包含它的文档的整个文档对象模型（DOM）树。但是在平台画布页面中，Facebook包含了许多它自己的元素，开发者不允许对它们进行修改。解决方案是什么？在用户提供的HTML元素和JavaScipt符号之前加上前缀，即应用的ID（如app1234567）。通过这种方式，在开发者的JavaScript中如果试图调用不允许调用的alert（）函数，就会调用未定义的函数app1234567_alert，并且只有开发者自己提供的那部分文档的HTML可以被document.getElementById这样的JavaScript代码访问。作为FBJS需要对提供的FBML（包括＜script＞元素）进行这种转换的一个例子，我们创建了一个简单的FBML页面，实现了AJAX功能，如例6-30所示。

例6-30：一个使用FBJS的FBML页面

These links demonstrate the Ajax object：

＜br/＞＜a href="#"onclick="do_ajax（Ajax.RAW）；return false；"＞AJAX

Time！＜/a＞＜br/＞

＜div＞

＜span id="ajax1"＞＜/span＞

＜/div＞

＜script＞

function do_ajax（type）{

var ajax=new Ajax（）；//FBJS Ajax library.

ajax.responseType=type；

switch（type）{

＜！—note FBJS's Ajax object also implements AJAX.JSON and AJAX.FBML，

omitted

for brevity—＞

case Ajax.RAW：ajax.ondone=function（data）{

document.getElementById（'ajax1'）.setTextValue（data）；

}；

break；

}；

ajax.post（'http://www.fettermansbooks.com/testajax.php?t='+type）；

}

＜/script＞

FBML和我们的FBJS修改动作将这些输入转变成了例6-31中的HTML。这个例子中的NOTE注释指出了每种需要的转换，不是实际输出的一部分。

例6-31：HTML和JavaScript输出的例子

＜！—NOTE 1—＞

＜script type="text/javascript"src="http://static.ak.fbcdn.net/

……/js/fbml.js"＞＜/script＞

＜！—Application's HTML—＞

These links demonstrate the Ajax object：

＜br＞

＜！—NOTE 2—＞

＜a href="#"onclick="fbjs_sandbox.instances.a1234567.bootstrap（）；

return fbjs_dom.eventHandler.call（

[fbjs_dom.get_instance（this，1234567），function（a1234567_event）{

a1234567_do_ajax（a1234567_Ajax.RAW）；

return false；

}

，1234567]，new fbjs_event（event））；return true"＞

AJAX Time！＜/a＞

＜br＞

＜div＞

＜span id="app1234567_ajax1"fbcontext="b7f9b437d9f7"＞＜/span＞＜！—NOTE 3

—＞

＜/div＞

＜！—Facebook-generated FBJS bootstrapping—＞

＜script type="text/javascript"＞

var app=new fbjs_sandbox（1234567）；

app.validation_vars={＜！—Omitted for clarity—＞}；

app.context='b7f9b437d9f7'；

app.contextd=＜！—Omitted for clarity—＞；

app.data={"user"：8055，"installed"：false，"loggedin"：true}；

app.bootstrap（）；

＜/script＞

＜！—Application's script—＞

＜script type="text/javascript"＞

function a1234567_do_ajax（a1234567_type）{＜！—NOTE 3—＞

var a1234567_ajax=new a1234567_Ajax（）；＜！—NOTE 3—＞

a1234567_ajax.responseType=a1234567_type；

switch（a1234567_type）{

case a1234567_Ajax.RAW：

a1234567_ajax.ondone=function（a1234567_data）{

a1234567_document.getElementById（'ajax1'）.setTextValue（a1234567_data）；

}；

break；

}；

＜！—NOTE 4—＞

a1234567_ajax.post（'http://www.fettermansbooks.com/testajax.php?t='+a1234

567_type）；

}

＜/script＞

下面是这段代码中的NOTE的解释：

NOTE 1

Facebook需要包含它自己的特殊JavaScript，包括fbjs_sandbox的定义，目的是渲染开发者的脚本。

NOTE 2

还记得前面FBML初始化流程中的$rewrite_attrs元素吗？FBML会重写这个列表中的属性，变成Facebook特有的功能；这实际上是FBJS的一部分。所以这里的onclick会激活这个页面的其他元素，这些元素在用户执行这个动作之前是非激活的。

NOTE 3

请注意在HTML和脚本中的元素如何加上了该应用的应用ID作为前缀。这意味着开发者对alert（）的调用将变成对app1234567_alert（）的调用。如果Facebook的后台JavaScript在这个上下文中允许这个方法，它将最终转向执行alert（）。如果不允许，这将是未定义的调用。类似地，这种加前缀的方式实际上为DOM树提供了命名空间，所以对该文档某些部分的改变只限于开发者定义的那些部分。类似的沙盒技术也允许开发者提供限制范围的CSS。

NOTE 4

Facebook提供了一些专门的JavaScript对象，如Ajax和Dialog，目的是支持（并且常常改进了）常见的使用场景。例如，通过Ajax（）对象发出的请求实际上能获得FBML作为结果，所以它们被重定向到Facebook域的一个代理上，在这里Facebook完成在线的FBML到HTML的转换。

支持FBJS需要对FBML进行改动、专门的JavaScript和AJAX代理这样的服务器端组件，才能够绕过应用Web架构的一些限制，但结果是很强大的。开发者因此可以享受绝大多数的JavaScript功能（甚至改进了这些功能，如支持FBML的AJAX），而且平台确保了应用内容提供了用户在Facebook上期望的受控体验，这完全是通过技术手段来实现的。

6.5.3　服务改进小结

解决了新的n层社会关系应用的概念带来的剩下一些问题之后，我们又改进了服务架构，添加了COOKIE和FBJS等项，如图6-6所示。

图　6-6：Facebook平台服务

随着开发者的社会关系应用越来越成为Facebook使用的一项集成服务，而不是由浏览器使用的外部站点，我们已经重新创建或重新设计了浏览器的某些功能（通过平台cookie、FBJS等）。在试图改变或重建“应用”的概念时，这是必需的两个重要修改的例子。

Facebook平台包括类似的其他一些架构上的巧妙设计，这里没有详细介绍，其中包括数据存储API和浏览器端Web服务客户端。

6.6　总结

Facebook的用户贡献的社会关系有效地提高了http://facebook.com上几乎所有页面的效用。而且，这种数据非常通用，所以当它与外部开发者的应用栈结合在一起时，它的最佳使用就出现了，这都是通过Facebook平台的Web服务、数据查询服务和FBML等技术来实现的。从取得用户的朋友或简介信息的简单内部API开始，我们在本章中详细介绍的全部改进展示了如何协调不断扩展的数据访问方法和容器网站的预期，特别是对数据隐私和站点体验集成方面的要求。每次对数据架构的新改动都发现了Web架构的一些新问题，我们又通过对数据访问模式的更强改进来解决这些问题。

虽然我们将关注重点完全放在那些使用Facebook的社会关系数据平台的应用的潜力和约束上，但像这样的新型数据服务不一定局限于社会关系信息。随着用户贡献和使用的信息越来越多，这些信息在许多容器站点上都很有用（如内容收集、评论、位置信息、个人计划、协作等数据），各式各样的平台提供者可以应用Facebook平台特有的数据和Web架构背后的这些思想，并从中获益。

第三部分　系统架构

第7章　Xen和虚拟化之美

第8章　Guardian：一个容错操作系统环境

第9章　JPC：一个纯Java的x86 PC模拟程序

第10章　元循环虚拟机的力量：Jikes RVM

第7章　Xen和虚拟化之美

Derek Murray Keir Fraser

原则与特性

结构

√功能多样性

√模块

概念完整性

√依赖关系

√修改独立性

进程

自动传播

数据访问

可构建性

√增长适应性

熵增抵抗力

7.1　简介

Xen是一个虚拟化平台，它来自于一项学术研究成果，现已发展成一个重要的开源项目。它容许用户在单个物理机器上运行几个操作系统，并特别强调性能、隔离性和安全性。Xen项目在多个领域造成了很大的影响：从软件到硬件，从学术研究到商业开发。它的成功很大一部分应归于采用了GPL（GNU General Public License）的开源发布方式。然而，开发人员并非某一天突发奇想决定编写一个开源的系统管理程序。它最初是一个大的（甚至野心勃勃的）研究项目Xenoservers的一部分。这个项目催生了开发Xen的动机，所以，在这儿我们将用它来解释虚拟化的必要性。

把Xen开源，这不仅使大量的用户可以使用它，也使它能够和其他开源项目享有共生关系。Xen的独特之处在于，当它第一次发布时，使用了半虚拟化（Paravirtualization）来运行像Linux这样的常规操作系统。半虚拟化需要修改运行在Xen之上的操作系统，既提升性能又简化了Xen自身。然而，半虚拟化只能做到这样，而且只有在处理器供应商的硬件支持下，Xen才能运行未修改的操作系统，如Microsoft Windows。处理器发展的前沿之一是增加新特性来支持虚拟机并消除一些性能开销。

随着新特性的增加及新的硬件变得可用，Xen的架构正在慢慢地发展。然而，从最初的原型到现在的版本，它的基本结构一直保持不变。在这一章中，我们介绍Xen的架构如何从早期的一个研究项目经过三个主要的版本，一直演变到目前的情况。

7.2　Xenoservers

与Xen相关的工作于2002年4月在剑桥大学开始。它最初作为Xenoservers项目的一部分，而Xenoservers的目标是创建一个“全球的分布式计算结构”。

几乎在同一时期，网格计算（grid computing）发展为利用分布于全球的计算资源的最好方式。最初的网格方案把计算机时间塑造成一种能力，就像电一样，这种能力可以通过协作计算机的网络或网格来获得。然而，后续的实现集中到了虚拟组织（virtual organization）上：可能建立了复杂信任关系的公司或机构组，这通过采用重量级的公钥加密来认证和授权而得以加强。

Xenoservers从相反的方向来解决这个问题。顾客通过一个代理（众所周知的XenoCorp）选择公开市场上的一个资源，而不是和服务提供商形成信任关系。XenoCorp保存一系列的xenoservers（由第三方提供出租的计算机）把顾客与服务器相对应，根据使用情况收取费用并支付报酬。至关重要的是，在顾客和提供商之间是相互不信任的：顾客不能损坏提供商的机器，提供商不能篡改顾客的工作。

信任

不信任是一个有用的架构特征，这听起来似乎违反常识。然而，在这个环境中，安全的主要目标是防止其他人访问或破坏敏感数据。因此，可信的系统是一个允许访问数据的系统。当不信任被构建入架构时，可信组件的数量会减到最少，并由此默认地提供了安全性。

下面开始讨论虚拟化。提供商为顾客提供一个新的虚拟机任其使用，而不是为顾客提供服务器上的一个账号。然后，顾客可以运行任何操作系统和任何应用程序（参见图7-1）。

图　7-1：虚拟机架构

虚拟软件确保这些与机器的其他部分隔离开，其他部分可能会租赁给更多的顾客。运行在虚拟机上面的系统管理程序（hypervisor）包含两个主要部分：一个引用监控器和一个调度程序，引用监控器确保一个虚拟机不能访问另一个虚拟机的资源（尤其是虚拟机的数据），调度程序确保每个虚拟机平等共享机器的CPU。

你不能只使用一个操作系统？

自20世纪60年代以来，分时操作系统就已经存在了，它能够使几个相互不信任的用户同时运行进程。比如说，在基于UNIX的机器上给每个用户一个账号还不够吗？

这确实让用户共享了计算的资源。然而，这不能令人满意，因为用户不具有灵活性和性能隔离。

在灵活性方面，用户只能运行与这台机器的操作系统兼容的软件，没有办法运行一个不同的操作系统，或改变操作系统。确实，（没有管理员的支持）用户不可能安装需要root权限的软件。

至于性能隔离，对于一个操作系统内核（这是一个极端复杂的软件块）来说，计算某一用户使用的所有资源比较困难。fork炸弹（fork bomb）就是一个例子，在这里一个用户的进程数呈指数级增长。这会迅速地消耗掉处理器的所有资源并导致对其他用户拒绝服务。因此，多用户系统通常需要用户之间有一定的信任和规范，以便不会出现这样的攻击。

在图7-1中，我们看到两个虚拟机运行在一个系统管理程序上。第一个虚拟机运行操作系统1（例如Microsoft Windows）和两个应用程序；第二个虚拟机运行操作系统2（例如Linux）、一个应用程序和一个服务。

这表明虚拟化对于其他事情也有用。例如，在许多数据中心，每个应用程序都有一个专门的服务器可用，如一个数据库服务器或一个Web服务器，但是，每个服务器只使用它的一小部分处理器。然而，仅仅在同一个操作系统上运行那些应用程序会引起糟糕的结果。当各种各样的应用程序一起运行时，可能会导致不可预知的差的性能。更糟糕的是，存在关联失败的风险，当一个应用程序崩溃时，也会引起其他应用程序崩溃。通过把每个应用程序单独放在一个虚拟机，然后在一个系统管理程序之上运行它们，系统管理程序可以保护这些应用程序并确保每个应用程序平等共享服务器的资源。

用虚拟化来进行效用计算（utility computing）的观点在最近几年开始流行。最知名的效用计算服务之一是Amazon的EC2，它允许顾客在Amazon数据中心的服务器上创建虚拟机。然后，顾客为他的虚拟机所使用的处理器时间和网络带宽付费。实际上，这些服务器运行Xen作为它们的虚拟化软件，使得它更接近于Xenoservers（虽然它只容许单个服务提供商）。

虚拟化对网格计算也有影响。网格计算事实的标准中间件Globus，现在支持虚拟工作空间（virtual workspace），它把虚拟机和现有的网格安全及资源管理协议结合在了一起。这样做的一个额外的好处是，如果条件改变，一个虚拟工作空间（就像任意的虚拟机）可以迁移到另一个物理位置。

采用Xenoservers模式的虚拟机的关键优势是它们可以用来运行流行的操作系统和现有的应用程序。实际上，这意味着它运行在主流的x86架构之上，这给系统管理程序的开发人员提出了一些挑战。

7.3　虚拟化的挑战

从高层次看来，操作系统虚拟化用于把几个虚拟机复合到单台物理机器上。虚拟机可以运行操作系统；物理机器也可以运行操作系统。那么，在虚拟机和物理机器之间有什么区别呢？

硬件是最显而易见的区别。在一台物理机器上，操作系统直接控制所有相连的硬件：网卡、硬盘驱动器、显卡、鼠标和键盘。然而，虚拟机不能直接访问这些硬件，否则它们将破坏每个虚拟机之间的隔离性。例如，某个虚拟机可能不想让其他的虚拟机查看它在二级存储器中存储的内容，或去读取它的网络信息包。此外，确保调度中的公平使用也会很困难。你可以为每个虚拟机配置某种类型的设备，但是这会给虚拟化的开销和能力带来负面影响。解决办法是给每个虚拟机一组虚拟硬件，这组虚拟硬件提供和真实硬件一样的功能，但是，这组虚拟硬件会复用物理设备。

当操作系统在虚拟机中运行时，一个更微妙的区别出现了。一般来说，操作系统内核是运行在计算机上的最有特权的软件，这允许它执行用户程序不能执行的某些指令。在虚拟化的情况下，系统管理程序是最有特权的，而操作系统内核在相对较低的权限下运行。如果操作系统现在试图执行这些指令，那它们就会失败，但是它们失败的方式是至关重要的。如果它们引起系统管理程序会捕获的一个错误，系统管理程序可以恰当地模拟这个指令并继续控制虚拟机。然而，在x86上存在一些在较低权限下表现不同的指令—例如，默默地失败而不触发系统管理程序。对于虚拟化来说这是坏消息，因为这妨碍了操作系统在虚拟机上适当地工作。显然，必须改变这些指令，普遍的技术（至少在Xen之前）是在运行时扫描操作系统代码，寻找特定的指令并用直接调用系统管理程序的代码替换它们。

实际上，在Xen之前，大多数虚拟化软件打算使虚拟硬件看起来完全像物理硬件。所以，虚拟设备就像物理设备一样运转，模拟相同的协议，当重写代码时确保操作系统不做修改就能运行。虽然这具有理想的兼容性，但在性能上有很大的开销。当发布Xen时，它表明，通过放弃理想的兼容性，性能会显著地提高。

7.4　半虚拟化

半虚拟化的观点是移除一个架构（例如x86）中难以虚拟化或如果进行虚拟化开销很大的所有特性，用与虚拟化层直接通信的半虚拟化操作替换它们。Denali中第一次使用了这种技术，Denali是移植了专门编写的Ilwaco客户操作系统的虚拟机监视器。Xen通过运行商业操作系统的半虚拟化版本而更进了一步。[1]

一个操作系统的半虚拟化包括重写与半虚拟化架构不兼容的所有代码。性能改进了，因为开发人员已经预先进行了转换，而不是在运行时。为了展示半虚拟化的能力，Xen团队需要一个他们可以改变的操作系统。幸运的是，可以使用Linux，它是开源的，而且广泛使用。对Linux内核只修改或增加2995行代码，就可以使它能够在Xen上运行：这连x86 Linux代码的2%都不到。通过半虚拟化（就像通过虚拟化一样），所有现有的用户应用程序都可以继续使用而不作任何修改，所以，总的修改不是非常扩散的。

为了实现半虚拟化，你必须自己写操作系统（Denali的方式），或修改一个现有的开源操作系统（例如Linux或BSD），或使得操作系统开发人员确信半虚拟化代码是值得的。最初的Xen研究版本在修改过的Linux上运行时在许多方面获得了令人印象非常深刻的接近本地的性能。性能，还有Xen作为开源软件进行发布的事实，使得操作系统开发人员开始半虚拟化部分的代码，以便他们的操作系统可以在Xen的系统管理软件上更高效地运行。更鼓舞人心的是，为Xen和其他系统管理软件开发的半虚拟化操作已经标准化到最新版的Linux内核中。通过在标准内核中支持Xen（和其他系统管理软件），虚拟化的推进变得容易得多。

半虚拟化是如何工作的呢？全部的细节太过复杂以致不能在这里进行阐述，但本章末尾的7.8节列出了深入讨论这一技术的论文。这里，我们将探究两个半虚拟化的例子：虚拟内存和虚拟设备。

半虚拟化一个操作系统的第一步是意识到它不是运行在这台计算机上最有权限的软件；那个级别的权限给予了系统管理软件。大部分的处理器至少拥有两个模式：超级用户（supervisor）和用户（user）模式。通常，操作系统内核会以超级用户的模式运行，但这已经留给了Xen，所以它必须修改为以用户模式运行。[2]然而，当以用户模式运行时，有几个操作是不合法的。在一个常规的操作系统中，进程之间必须相互保护，这至关重要。因此，内核必须使用一种名为超级调用（hypercall）的机制来要求系统管理程序代表它执行这些操作。超级调用类似于系统调用（从用户进程调用内核），只不过它用于在内核和系统管理程序之间通信，而且它通常实现较低级别的操作。

虚拟内存用来确保进程不能和其他进程的数据或代码进行交互。每个进程都给定虚拟地址空间，这确保那个进程只能存取分配给它的内存。内核通过维护页表（页表把虚拟地址映射到确定内存芯片中数据的实际位置的物理地址）来负责创建虚拟地址空间。当它在一个虚拟机中运行时，内核不能自由地管理这些表，因为它可以映射到属于另一个虚拟机的内存。因此，Xen必须验证页表的所有更新，而当内核想要修改任何页表时必须通知系统管理程序。如果系统管理程序涉及每个页表更新，那么效率会非常低（例如，当启动一个新的进程并最初构建它的页表时）。然而，结果表明这种情况相对较少，而且，Xen可以通过批处理这些更新请求或在页表更新时“松开”页表来分摊转给系统管理程序的开销。

请看图7-2。所有的虚拟机分享了整个物理内存。[3]然而，这些内存可能不是相邻的，而且，在大多数情况下也不会从物理地址0开始。因此，每个虚拟机内核都处理两种类型的地址：物理地址（或机器地址）和伪物理地址。物理地址相应于内存芯片中数据的实际位置，而伪物理地址向虚拟机提供了从0开始的连续物理地址空间的假象。伪物理地址对于依赖这个假定的、否则必须半虚拟化的某些算法或子系统是有用的。

图　7-2：虚拟机内存配置

在任何实际的使用中，都必须能够和虚拟机进行交互。虚拟机至少需要一个磁盘（严格来说，称之为块设备）和一块网卡。[4]因为大多数操作系统至少支持一个块设备和一块网卡，让系统管理程序模拟这些设备以便可以使用最初的驱动程序，这似乎很诱人。然而，软件的实现会努力模拟实际设备的性能，模拟设备的模式可能不得不绕弯路（例如实现硬件协议），用软件实现硬件的功能是不必要和低效的。

还有其他实现方式吗？

虚拟化虚拟内存（当你不能改变操作系统时）的标准方式是使用影子页表。通过它们，访客会处理伪物理地址（例如，相邻的和从0开始的地址）而不是物理地址。

访客靠着这个地址空间维护他自己的页表。然而，它们不能由硬件使用，因为它们并不对应真实的物理地址。因此，系统管理程序更新这些访客页表，并用它们构建一张会把虚拟地址转化为物理地址的影子页表。

这个方法无疑会有一些开销，但是，当你不能修改操作系统时这是必需的。

Xen对硬件虚拟化的访客使用了这个方法的一个变体，正如本章后面所述。

因为Xen不必一定支持未修改的操作系统，所以它可以自由地引入虚拟块和网卡驱动。两者以相似的方式运作：它们包含访客虚拟机中的前端驱动和虚拟软件中的后端驱动。这两个设备利用环形缓冲进行通信，环形缓冲是用来在虚拟机之间传输大容量数据的一种高性能机制。这产生一个柔性层架构（如图7-3所示）：前端实现了操作系统的网络或块设备接口以便它对于操作系统就像是一个常规的硬件设备，后端把这个虚拟设备和真实的硬件连接起来。虚拟块设备可能会连接到一个包含磁盘映像的文件或一个实际的磁盘分区；虚拟网卡设备可能会连在一个软件网桥上，而它本身连在一块真实的网卡上。环形缓冲这个抽象概念确保前端和后端完全分离。一个后端可以支持来自Linux、BSD或Windows的前端，反之，相同的前端可以和各种后端一起使用，所以，像写时复制（copy-on-write）、加密和压缩这样的特性可以显而易见地加给访客。就像互联网协议，Xen的分设备模式可以操作大批硬件，而且它支持多数较高级别的客户端，如图7-3所示。

半虚拟化包括的内容远远超过这些例子。例如，当一个操作系统可以从CPU换出时，时间的意义就改变了，而Xen引入了虚拟时间的概念来确保操作系统仍然像预期的那样运行。还有许多其他的虚拟设备，内存的半虚拟化利用其他的优化来确保高效的性能。如果想要了解更多关于这方面的资料，请阅读本章末尾的7.8节。

图　7-3：分块设备的沙漏型架构

[1]当然，关于VM/370（20世纪60年代IBM的操作系统，虚拟化的起源）是第一个半虚拟化操作系统的说法可能存在争议。然而，自从IBM设计了指令集、操作系统和虚拟机监视器，这种方式应对了不同的挑战，直到现代的半虚拟化。

[2]x86架构有四个权限级别（或称为ring），0表示最大权限，3表示最小权限。在它的32位版本中，Xen以ring 0运行，半虚拟化内核以ring 1运行，用户应用程序和往常一样，以ring 3运行。然而，在64位版本中，由于内存分段硬件的差异，半虚拟化内核以ring 3运行。

[3]请注意，Xen不支持过量使用物理内存，所以没有虚拟机的交换技术。然而，可以利用一个名为ballooning的进程改变虚拟机的内存覆盖区。

[4]你可能认为对于交互来说鼠标、键盘和视频输出是必需的，但是，这些可以由像VNC这样的远程桌面客户端提供。不过，最新版本的Xen已经包括了对这些虚拟设备的支持。

7.5　Xen的变换形式

一个以Xen为基础的系统的传统描述表明几个虚拟机（Xen中称为域（domain））设置在系统管理程序之上，而系统管理程序本身直接设立在硬件上（如图7-4所示）。当系统启动时，系统管理程序启动一个特殊的域，即众所周知的零域（domain zero）。零域拥有可以让它管理系统其余部分的特殊权限，它类似于常规操作系统中的一个根或管理员进程。图7-4显示了一个基于Xen的典型的系统，其中，零域和几个客户域（Xen术语中称为DomU）运行在Xen系统管理程序之上。

图　7-4：Xen系统架构

宿主式虚拟化

Xen是原生式虚拟化（也被称为第一类虚拟化）的一个例子。另一种方式是在宿主操作系统上运行一个系统管理程序。在这种情况下，每个虚拟机实际上成了宿主操作系统的一个进程。宿主操作系统负责零域在Xen上执行的管理职能。宿主式的系统管理程序和管理软件就像一个常规的应用程序，设置（或联结）在一个常规操作系统之上；如图7-5所示。

宿主式系统管理程序通常用于其他虚拟化产品的“工作站”版本中，例如VMWare工作站、Parallels工作站和Microsoft的虚拟PC。这种方式的主要优点在于：安装一个宿主式的系统管理程序就像安装一个新应用程序一样简单，反之，安装一个原生的系统管理程序（例如Xen）就像安装一个新的操作系统。因此，宿主式虚拟化更适合于非专家的用户。

另一方面，原生式系统管理程序的优点在于它可以获得更好的性能，因为本地系统管理程序比组合了宿主式操作系统和系统管理程序的软件层薄很多。宿主式虚拟机受宿主操作系统支配，如果有其他应用程序和系统管理程序正一起运行，就会导致性能降低。相反，因为零域像一个常规虚拟机一样调度，在那里运行的应用程序对其他虚拟机的性能没有影响。

宿主式虚拟机通常用于桌面虚拟化：例如，允许运行Mac OS X的用户在计算机的一个窗口中运行Linux。这对于要运行宿主操作系统不支持的应用程序有用，而且，当使用交互式应用程序时，性能影响不太容易注意到。原生式虚拟化更适合于服务器设施，在那里，初始的性能和可预测性至关重要。

图　7-5：宿主式虚拟化系统的架构

当设计Xen架构的时候，首要关注的是只要有可能，就把策略和机制分开来。系统管理程序设计成用来管理低级硬件的薄层，担当引用监控器和调度程序，复用对硬件设备的访问。然而，因为系统管理程序在最高级别运行（这里的一个缺陷会危及整个系统），较高级别的管理委派给了零域。

例如，当创建一个新的虚拟机的时候，大部分工作是在零域中完成的。从系统管理程序的角度来看，它分配了新的域，连同一部分物理内存，还映射了其中的部分内存（为了载入操作系统），并且这个域是未中止的。零域负责许可权限控制，设置虚拟设备并为新域构建存储映像。这种分离在发展过程中非常有用，在零域中调试管理软件比在系统管理程序中调试容易很多。而且，它支持把不同的操作系统加入到零域而不是系统管理程序中，后者所带来的额外复杂度通常是难以想象的。

先前我们提到Xen如何受益于可以使用一个开源操作系统，这个开源系统为半虚拟化提供了测试台。使用Linux的第二个好处是它支持非常多的各不相同的硬件设备。Xen几乎能够支持存在Linux驱动的任何设备，因为它重用了Linux的驱动代码。Xen总是重用Linux驱动以支持多种硬件。然而，在1.0和2.0版本之间，重用的特征发生了明显的改变。在Xen 1.0中，所有的虚拟机（包括零域）都通过虚拟设备访问硬件，正如先前所述。系统管理程序负责把这些访问复合到真实的硬件，因此，它包含Linux硬件驱动的前端版本和虚拟驱动后端。虽然这简化了虚拟机，但是增加了系统管理程序的复杂度并把支持新驱动的责任交给了Xen开发团队。

图7-6演示了Xen在1.0版本和2.0版本之间设备架构的改变。在1.0版本中，虚拟后端在系统管理程序中实现。所有的域，包括零域，都通过这些设备访问硬件。在2.0版本中，系统管理程序进行了简化，零域通过本地驱动访问硬件。因此，后端驱动移动到了零域。

图　7-6：Xen设备架构在1.0版本和2.0版本之间的改变

在Xen 2.0的开发中，设备架构全部进行了重新设计：本地设备驱动和虚拟后端移出了系统管理程序，移入了零域。[1]现在，前端驱动和后端驱动利用设备通道（device channel）进行通信，它使得域之间能够进行有效和安全的通信。通过使用设备通道，Xen的虚拟设备达到了接近本地的性能。它们的性能依赖两个原则：无复制传递和异步通知。

请看图7-7，这个图演示了一个共享设备如何使用。用户向前端驱动提供了一页内存，其中包含了要写的或读入的数据（1）。前端驱动在共享环形缓冲下一个可用的空档中放置一个请求，其中包含了到提供页的引用（2）。前端驱动告诉系统管理程序通知驱动域有一个请求在等待（3）。后端醒过来并把提供页映射入它的地址空间（4）以便硬件可以与它使用的直接内存存取（DMA）进行交互（5）。最后，后端通知前端这个请求已经处理完成（6），前端通知用户应用程序（7）。

图　7-7：一个共享设备的分析

用CPU复制数据的开销是昂贵的，这就是为什么使用像直接内存存取（Direct Memory Access,DMA）这样的技术，以便在设备和内存之间直接传输数据而不需要CPU参与的原因。然而，当数据必须在地址空间之间移动时，Xen必须专门处理以避免复制。Xen支持一种称为授权表（grant table）的共享内存机制，由此每个虚拟机维护一张定义它的哪些页能够由其他虚拟机访问的表。这张表中的索引称为授权索引（grant reference），当给与另一个虚拟机时，它代表一种能力。系统管理程序确保只有预期的接收者可以映射这个授权引用，这反过来维持了内存的隔离性。设备通道本身用于发送授权引用，然后用于映射为了发送或接收数据的缓存。

当作出新的请求或响应的时候，发送者必须通知接收者。这通常使用同步通知—类似于一个函数调用—因此，发送者必须等待，直到它知道通知已经被接收。如图7-8所示，这种操作模式导致糟糕的性能，尤其当只有单个处理器可以使用的时候。Xen实际上使用事件通道（event channel）来发送异步的通知。事件通道执行虚拟中断，但是，只有当目标域下一次调度时虚拟中断才会服务。因此，在目标域调度来执行它们之前，请求者可以产生多个请求，每次都唤醒事件通道。然后，当调度目标域时，它又通过异步的方式处理多个请求并发送响应。

请看图7-8。对于同步通知，前端必须等待后端完成它的工作之后才能进行下一个请求。这意味着等待后端域调度，然后等待前端域调度。比较而言，对于异步通知，前端在被调度的时候可以发送尽可能多的请求，而后端可以发送尽可能多的响应。这增加了吞吐量。

图　7-8：异步通知的优点

当然，如果你把过多的功能移到零域，它会变为一个单个故障点。这尤其适用于设备故障，它可以使整个操作系统（由此引起整个虚拟系统）崩溃。因此，Xen考虑到驱动域，零域可以把一个或多个设备的控制委托给它。通过把后端驱动放到驱动域并把一些I/O权限授予这个域就可以简单地实现这些功能。然后，如果一个驱动失败，这个故障会隔离在这个驱动域，它可以重新启动而不损害系统或客户域。

这种模式已经应用于零域的其他部分。Xen的最新版本包括了存根域（stub domain），它为“虚拟硬件”域提供设备支持（在接下来的章节中介绍）。把这些代码移到单独的域中是考虑为了获得更好的性能隔离，提高健壮性并改善初始性能（这有点令人惊讶）。随着开发的延续，越来越多的特性可能会移出零域，特别是当这样做可以改善安全性时。

[1]实际上，这个架构允许任何授权的虚拟机访问硬件，并因此作为驱动域（driver domain）。

7.6　改变的硬件，改变的Xen

直到目前，我们的讨论集中在半虚拟化。然而，在Xen 2.0版本和3.0版本之间，Intel和AMD在处理器中引入了有区别但类似的硬件虚拟机支持。在虚拟机中运行未修改的操作系统（包括Microsoft Windows或Linux）已经成为可能。这是否会招致半虚拟化的终结？首先，我们看一下硬件虚拟机是如何实现的。Intel和AMD都引入了一个新的模式（Intel中称为nonroot模式，AMD中称为guest模式），试图在这个模式中执行一个特权操作，甚至在最高的（虚拟）权限级别，以生成一个通知系统管理程序的异常。因此，不必再扫描代码并替换这些指令（在运行时或通过半虚拟化预先处理）。系统管理程序可以利用影子页表（shadow page table）向虚拟机提供连续内存的假象，接着它可以触发I/O操作以模拟物理设备。

Xen在3.0版本中增加了对硬件虚拟机的支持。开源开发为这次改变提供了很大的帮助。因为Xen是一个开源项目，所以，来自Intel和AMD的开发人员可以贡献支持新处理器的底层代码。此外，由于它的GPL身份，Xen可以合并其他开源项目的代码。例如，新的硬件虚拟机需要一个模拟的BIOS和模拟的硬件设备；实现其中的任何一个都需要巨大的开发工作。幸运的是，Xen可以使用来自Bochs项目的开源BIOS和来自QEMU的模拟设备。

现在你可能想知道有这么多优点的半虚拟化到底怎样了。模拟设备、影子页表、额外的异常等一定会导致糟糕的性能吗？一个草率的硬件虚拟化操作系统通常比半虚拟化操作系统运行得更糟糕，但有两个缓和因素。

首先，处理器厂商正在不断地开发优化虚拟化的新特性。就像存储器管理单元（memory management unit,MMU）使程序员处理虚拟地址而不是物理地址一样，IOMMU对于输入和输出设备来说同样如此。IOMMU可以用来使虚拟机（无论是虚拟化硬件的或半虚拟化硬件的虚拟机）安全、直接地访问一个硬件（参见图7-9）。虚拟机直接访问硬件的常见问题是许多设备可以操作DMA，因此，如果没有IOMMU，它可以读取或重写其他虚拟机的内存。IOMMU可以用来确保：在控制某一虚拟机时，DMA只可以使用属于那台虚拟机的内存。

模拟与虚拟化的对比

最新版本的Xen包含了Bochs和QEMU的代码，而它们都是模拟程序。在模拟和虚拟化之间有什么区别，两者怎样才可以合并呢？

Bochs用软件提供了x86处理器家族以及所支持硬件的开源实现。QEMU模拟了好几个架构，包括x86。两者都可以用于运行未修改的x86操作系统和应用程序。而且，因为它们都包括了硬件的完整实现（包括CPU），所以，它们可以运行在使用不兼容指令集的硬件上。

虚拟系统和模拟系统的区别在于两者的指令执行方式不同。在虚拟系统中，应用程序和大部分操作系统都直接在处理器上运行。而在模拟系统中，模拟程序必须模拟或转换每个指令以执行它。因此，对于相同的平台，模拟程序比虚拟机监控器引入更多的开销。[1]

然而，即使它们使用了部分的Bochs和QEMU,Xen的硬件虚拟机仍然是虚拟化的，而不是模拟的。Bochs的代码提供了支持启动进程的BIOS，而QEMU提供了许多常规设备的模拟驱动程序。然而，只有在启动和试图进行I/O操作时才调用这些代码。其他大部分指令直接在CPU上运行。

图7-9演示了一个来自一个虚拟机（DomIO）的利用IOMMU的简化的DMA请求。当硬件驱动程序和设备通信时使用伪物理（特定虚拟机的）地址（1）。这个设备使用这些地址进行DMA请求（2），IOMMU（利用由系统管理程序配置的I/O页表）把这些地址转变为使用物理地址（3）。IOMMU也会终止虚拟机对任何不属于它的内存地址的访问。

图　7-9：利用IOMMU直接访问设备

增强内存管理硬件也可以避免使用影子页表。[2]AMD和Intel都拥有在伪物理地址和物理地址之间进行转换的技术，它们分别称为快速虚拟化索引（Rapid Virtualization Indexing）和加强页表（Enhanced Page Table）。因此无需系统管理程序创建影子页表，因为整个转换都由硬件进行处理。

当然，一个廉价得多的解决方案是吸取半虚拟化的经验并把它们应用到未修改的客户操作系统。虽然不太可能改变操作系统的核心部分，但是我们可以增加设备驱动程序。此外，Xen可以修改操作系统运行的虚拟硬件。最后，模拟硬件提供一个Xen平台设备（Xen platform device），它看起来像是未修改客户操作系统的一个PCI设备并提供对虚拟平台的访问。然后就可以为未修改的操作系统编写前端设备，它会像半虚拟化操作系统中的前端一样运行。这样我们可以在硬件虚拟机中获得与半虚拟化情形相当的I/O性能。当这一章前面介绍半虚拟化的时候，我们说过，使一个常用操作系统像半虚拟化客户操作系统一样运行的最好方式是我们自己做或使操作系统开发人员相信他们应该这样做。作为半虚拟化成功的一个证明，Microsoft在Windows Server 2008中已经包含了一些启蒙技术（enlightenment），当在虚拟机中运行时会提高内存管理的性能。这些启蒙技术相当于半虚拟化操作，因为它们依赖超级调用来通知当前运转的系统管理程序。

[1]KQEMU是容许用户模式的代码（及一些内核模式的代码）直接在CPU上运行的Linux内核模块。在宿主和目标平台相同的地方，这个模块可以提供巨大的加速。结果是模拟和虚拟的混合。

[2]需要提到的是，Xen的影子页表实现方式非常优秀，其性能很有竞争力，但是，当和半虚拟页表进行比较时仍然显得开销略大。

7.7　经验教训

回顾一下，我们可以从Xen中获得两个主要的经验：半虚拟化的重要性和开源开发的优点。

7.7.1　半虚拟化

首先是半虚拟化的成功。一句名言提醒我们：

计算机科学中的任何问题都可以用另外的间接层解决，但是这通常会引发另一个问题。

—David Wheeler

虚拟化仅仅是一种间接的形式，即使现代的计算机从硬件上支持虚拟化，盲目地依赖这种支持会导致糟糕的性能。当你盲目地利用任何类型的虚拟化时都会出现同样的问题。例如，虚拟内存使用一块硬盘来产生有很多内存可用的假象。然而，假如你编写了一个试图使用所有内存（就像它是真正的物理内存一样）的程序，那性能会非常糟糕。在这种情况下，你可以想象把那个程序“半虚拟化”以使它意识到这个物理限制，结合虚拟内存系统来改变用来使它高效运行的算法和数据结构。

在操作系统的环境中，Xen已经表明半虚拟化（无论是它增加虚拟驱动程序，还是大规模地改变操作系统，还是在选定区域中有意识地提升性能）是在虚拟环境中运行时提升性能的一项重要技术。

7.7.2　开源开发

在Xen发展过程中采取的最大胆的决策也许是当其他系统管理程序还只作为私有软件时把Xen作为开源软件。

这个决策绝对使Xen受益，因为这样，Xen可以利用非常多的软件：从Linux内核和QEMU机器模拟器，到启动时绘制Xen标识的小程序。[1]没有这些软件，Xen项目将需要非常大工作量的重复实现。通过包括这些来自其他项目的软件，当它们更新时，Xen也会受益，而其他项目也受益于Xen开发人员提交的补丁。

Xen最初是剑桥大学一位研究生的兼职项目，现在已经成长为包括来自全球的超过100位贡献者。一些大的贡献来自Intel和AMD，他们提供了许多代码来支持硬件虚拟机。这使得Xen能够成为领先的系统管理程序之一来支持这些处理器扩展。

除此之外，因为可以自由获得Xen，一些项目已经采用它。主流的Linux版本，例如Debian、Red Hat、SUSE和Ubuntu，都包含了Xen包并给这个项目反馈代码，还有用于使用Xen的有用的工具。一些贡献者努力把Xen移植到其他架构，甚至移植到其他操作系统上，使操作系统直接运行在这个系统管理程序上。Xen已经用于运行半虚拟化的OpenSolaris、FreeBSD和NetBSD等。Xen现在也涉及Itanium架构，作用是把它移植到ARM处理器。后者非常令人激动，因为它使得Xen能够运行“非传统”的设备，例如移动电话。

当我们着眼于未来时，一些最有趣的Xen用途在于研究团体。Xen出现在2003年的操作系统原理座谈会（Symposium on Operating Systems Principles,SOSP）上并成为一系列研究（在它最初的研究组织内外都有）的基础。关于Xen的最早的论文之一来自Clarkson大学，一组研究人员在其中重复了SOSP论文中的结果。作者们强调开源软件改善了计算机科学，因为它使重复研究成为可能，并且反过来巩固任何关于性能或其他特性的主张。最近更多的研究工作直接导致Xen中的新特性变得令人感兴趣。一个特殊的例子是动态迁移（live migration），它使得一台虚拟机用几乎可以忽略的时间在物理计算机之间迁移。这在2005年的论文中进行了详细描述，并增加到了Xen的2.0版本中。

[1]Figlet：http://www.figlet.org.

7.8　延伸阅读

本章只能够简单地介绍一下Xen项目，相关的研究论文是详细资料的最好来源。

下面两篇论文分别描述了Xen 1.0和2.0的架构：

Barham,Paul,et al."Xen and the art of virtualization，"Proceedings of the 19th ACM Symposium on Operating System Principles,October，2003.

Fraser,Keir,et al."Safe hardware access with the Xen virtual machine monitor，"Proceedings of the 1st OASIS Workshop,October，2004.

下面的论文讲述了一些已经开发用来帮助虚拟化的新的芯片集及处理器技术：

Ben-Yehuda,Muli,et al."Using IOMMUs for virtualization in Linux and Xen，"Proceedings of the 2006 Ottawa Linux Symposium,July，2006.

Dong,Yaozu,et al."Extending Xen with Intel virtualization technology，"Intel®Technology Journal,August，2006.

最后，Xen正在积极地发展并不断地演变。保持与新开发平行的最好方式是下载源代码并加入邮件列表。它们都可以在http://www.xen.org/找到。

第8章　Guardian：一个容错操作系统环境

Greg Lehey

原则与特性

结构

√功能多样性

√模块

√概念完整性

√依赖关系

√修改独立性

√进程

√自动传播

√数据访问

可构建性

√增长适应性

√熵增抵抗力

架构不是什么新东西。真实的建筑架构已经存在了几千年，一些最漂亮的建筑架构也已经存在了几千年。当然，计算机还没有存在这么久，但是，过去也有许多漂亮架构的例子。正如建筑一样，风格并不总是保持不变，本章将讲述这样一个架构并探讨它为什么几乎没有影响力。

Guardian是Tandem的容错“NonStop”系列计算机的操作系统。它设计成与硬件并行以便用最小的开销提供容错性。

本章将讲述最初的Tandem机器，它设计于1974年—1976年，发行于1976年—1982年。它最初称为“Tandem/16”，但是在“NonStop II”成功之后，它重命名为“NonStop I”。Tandem经常把术语“T/16”使用在系统和随后的架构上。

我从1977年到1991年一直专门从事与Tandem的硬件相关的工作。与Tandem机器打交道既令人愉快又不寻常。在本章中，我很乐意地回顾程序员对这种机器的一些感觉。T/16是一种容错的机器，但是，这不是它唯一的特性，在本章的讨论中，我会提到许多与容错没有直接关系的方面—实际上，会降低容错性！所以，请准备回到过去，回到1980年，从Tandem的一个市场口号开始。

8.1　Tandem/16，将来所有的计算机都会像这样构建

Tandem把计算机描述成有多个处理器的单独的计算机，但是，从21世纪的观点来看，它们更像是与单个计算机一样运行的计算机网络。尤其是每个处理器都几乎完全不依赖其他处理器而独立地工作，而系统可以从任何单个组件（包括处理器）的故障中恢复。和传统联网的处理器最大的区别是整个系统都从单个内核映像开始运行。

8.2　硬件

Tandem的硬件设计成不可能存在“单个故障点”（single point of failure）：系统、硬件或软件的任何一个组件都可以出故障而不引起整个系统出故障。它设计成优雅降级（graceful degradation）。在大多数情况下，尽管系统有多个故障，但总体上还能继续运行，虽然这很大程度上依赖个别故障的种类。

这种架构的首要含义是每种组件都必须至少有两个，以防一个发生故障。这意味着系统至少需要两个CPU。

但是，CPU之间应该怎样连接呢？一贯的传统方法是CPU通过共享内存进行通信。在Tandem中，我们称之为紧耦合多处理器（tightly coupled multiprocessor）。但是，如果处理器共享内存，那么，内存就成了单个故障点。

从理论上来说，双重内存是可能的（实际上，随后的Tandem架构做到了这一点），但是，这样做非常昂贵，而且，这样做造成了重大的同步问题。Tandem改为在硬件级别选择一对高速并行总线—处理器间总线（interprocessor bus,IPB），有时候这也称为Dynabus，它在分开的CPU之间传输数据。有时候这种架构称为松耦合多处理器（loosely coupled multiprocessor）。

当然，一台计算机不仅仅有CPU。I/O系统和数据存储也非常重要。在这里，基本的方式也是进行硬件备份，后面将深入讲解。

最终的架构看起来有点像图8-1，这就是所谓的Mackie图，是以Tandem的副总裁Dave Mackie命名的。

这很容易导致一个系统的开销加倍（甚至更大），照“热备份”系统现在的样子，一个组件的存在只是为了等待它的伙伴出故障。Tandem对比较昂贵的组件（例如CPU）采用不同的方式。在T/16中，每个CPU都是活跃的，而不是为操作系统进程提供热备份功能。

图　8-1：Mackie图

8.2.1　诊断

当一个组件出故障的时候操作系统必须能够察觉。在许多情况下，这勿庸置疑：如果系统出现灾难性的故障，它会完全停止响应。但是，在另一些情况下，一个出故障的组件会继续运行，只是会产生不正确的结果。

Tandem对这个问题的解决方案既不优雅也不高效。这个软件设计成多疑的，最初的建议是：如果有些东西发生了故障，那么操作系统就停止这个CPU—由另一个CPU来接管工作。如果磁盘控制器返回一个无效的状态，那么它会被中止—另一个会不间断地继续处理。但是，如果故障很微小，它就可以继续运行而不被察觉，在极少的情况下会导致数据异常。

这样判断CPU出故障当然还不够；其他CPU必须发觉它已经出故障了。这里的解决方案是Watchdog。每个CPU每隔1.2秒在双方的总线上广播一个信息，即所谓的“我还活着”的信息。如果一个CPU连续两次没有收到另一个CPU的“我还活着”的信息，它会假定那个CPU出故障了。如果这两个CPU共享了资源（进程或I/O），那么，探测到这个故障的CPU会接管这些资源。

8.2.2　修复

让一个有缺陷的组件离线还不够；为了保持容错性和性能，它必须尽可能快地恢复（“起来”），而且，当然不能引起其他组件离线（“倒下去”）。

如何实现这一点取决于组件和故障的类型。如果操作系统在一个CPU中崩溃了（可能是故意的），它可以在线重启（“重新载入”）。重启一个系统的标准方式是先从磁盘启动一个处理器，然后通过IPB启动其他处理器。出故障的处理器也会通过IPB重新启动。

另一方面，如果硬件出现故障，那么就必须替换它。所有的系统组件都是可热插拔的：它们可以在系统运行的时候移除并替换。如果一个CPU因为硬件问题而出故障，那就会换一块合适的电路板，然后该CPU会如前所述通过总线重新启动。

8.3　物理布局

系统设计成包含尽可能少的电路板，所以，所有的电路板都非常大，大概是边长为50厘米的正方形。所有的电路板都用低功耗的肖特基TTL逻辑。

CPU由两块电路板组成，分别为处理器和MEMPPU。MEMPPU包含了与内存（包括虚拟内存逻辑）的接口和与I/O总线的接口。T/16最多可以配置512KW（1MB）的半导体内存或256KW的核心内存。内存板有三个尺寸：32kW核心内存、96KW半导体内存及192KW半导体内存。这意味着用完全组装的板没有办法得到精确的1MB半导体内存。核心内存有奇偶校验码保护，而半导体内存有ECC保护，它可以纠正一位的错误和发现两位的错误。

处理器机柜大概有6英尺高，可以容纳4个带半导体内存的CPU或4个带核心内存的CPU。这些处理器放置在机柜的顶部，I/O控制器放在下面相邻的第二个机架上。在那下面是风扇，而在机柜底部有电池，在电源出故障时可以保持内存中的内容。

大多数结构还有另一个用来放置磁带机的机柜。磁带机是独立式的14英寸的部件。还有一个系统控制台，一个DEC LA-36的打印终端。

8.4　处理器架构

CPU采用的是一个与Hewlett-Packard 3000很相似的定制的TTL设计。它拥有一个2KB页面大小的虚拟内存、一个基于栈的指令集和宽度固定为16位的指令。原始的处理器速度大约为每个处理器0.8MIPS，一个配置满16个处理器的系统的速度达到13MIPS。

8.4.1　内存寻址

T/16是16位的机器，它的地址空间的宽度限制为16位。即使在20世纪70年代后期，这也开始成为一个问题，Tandem通过在任何时候都提供4个地址空间来解决这个问题：

用户代码

地址空间包含了可执行的代码。它是只读的，而且可以在所有使用它的进程之间共享。由于这个架构（为每个CPU分隔内存），这个代码只能分配给特定的某个CPU。

用户数据

用户进程的数据空间。

系统代码

内核的代码。

系统数据

内核的数据空间。

有一个例外：在任何时候只有一个数据空间和一个代码空间可以访问。它们在环境寄存器（Environment Register）中指定，环境寄存器包含了许多描述CPU当前状态的标记，如图8-2所示。

图　8-2：环境寄存器

SD位确定数据空间，SC位确定代码空间。SG相关的寻址模式是这个规则（总是在系统数据中寻址）的一个例外。

为了保持可靠性和数据完整性，环境寄存器中的陷阱（trap）位和其他的位一起能够捕获运算溢出。这样就有了不设置条件代码的运算指令的“逻辑”等价物。

CPU拥有由两个寄存器（S寄存器或栈指针，以及L寄存器）寻址的一个硬件栈。L寄存器指向当前的栈帧（stack frame）。L寄存器是一个相对较新的概念，它指向当前帧的底部。与S寄存器不同，在一个程序的执行过程中它不会改变。[1]出于寻址的考虑，这个栈限制为当前数据空间最初的32KB，而且与其他的机器不同，它会向上增加。[2]除硬件栈之外，还有一个具有8个16位字的寄存器栈。这些寄存器的号码从R0编到R7，但是，指令集把它们用作一个循环栈，其中栈的顶部由环境寄存器的RP位定义。在下面这个例子中，RP设置为3，这就意味着R3是栈的顶部，视为A寄存器（如图8-3所示）。

图　8-3：寄存器栈

假设最初这个寄存器栈是“空”的，一个典型的指令序列如下所示：

LOAD var^a—把变量a压入栈（R0）

LOAD var^b—把变量b压入栈（R1）

ADD—把A和B（R1和R0）加起来，把结果保存在R0（A）中

STOR var^c—把A的值赋给变量c

指令的宽度都是16位，这没有为一个地址字段剩下许多空间：只有9位。为了解决这个问题，Tandem基于一系列寄存器的偏移量进行寻址（如图8-4所示）。

只有下列的内存区域可以直接定位（换句话说，不用间接寻址）：

·当前数据空间的前256个字，称为“G”（global）模式。这些经常用于间接指针。

·L寄存器正偏移的前128个字，称为L+。这些是当前程序调用的全局变量，在C中，这些称为自变量。

·系统数据的前64个字（“SG+”模式）。系统调用运行在用户数据空间，所以，CPU需要一些方法让授权的过程访问系统数据。对于没有授权的过程来说，它们是不能访问的，甚至是只读的。

图　8-4：内存寻址

·L寄存器当前值下方的前32个字。这包括调用者栈帧（3个字）和传给过程的最多29个参数字。

·在这个栈顶部下方的前32个字（S-寻址）。它们用于子过程（在其他过程中定义的过程），子过程调用不会剩下一个栈帧。因此，这个地址既处理子过程的本地变量也处理子过程的参数。

这些地址模式都编码在指令地址字段的最初几位中，如图8-5所示。

图　8-5：地址形式

%符号代表八进制数，如同%377等于十进制的255，或十六进制的7F。Tandem没有使用十六进制。

这个指令形式也规定了单级的间接寻址：如果I位设置进指令，找回的数据字会作为最终操作数的地址，它必须在相同的地址空间。这个地址空间和数据字的宽度都是16位，所以，不存在多级间接寻址的可能性。

这种实现的一个问题是数据的单位是一个16位的字，不是一个字节。这个指令集也通过一个不同的寻址方法提供了“字节指令”：地址的低位指定字中的字节，而地址的其余部分是这个字地址的低15位。对于数据存取，这种方法把字节寻址限制到数据空间的前32KB；对于代码存取，这种方法把字节存取限制到当前指令地址空间的一半长。这导致一个过程不能越过代码空间中的32KB边界。

还有两个指令：LWP（从程序载入字）和LBP（从程序载入字节），它们可以访问当前代码空间中的数据。

[1]这与21世纪大多数的处理器使用的通用指针寄存器相同。

[2]在20世纪70年代，栈是相当新的概念。如同它的前身HP 3000，Tandem对栈的支持明显超越了其他系统（例如，DEC的PDP-11，它是那时候最有影响的基于栈的另一台机器）对栈的支持。

8.4.2　过程调用

Tandem的编程模式受Algol和Pascal的很大影响，所以，它为返回一个值的函数提供了保留字function，并为不返回值的函数提供了保留字procedure。有两个指令用于调用一个过程：PCAL用于当前代码空间中的过程，SCAL用于系统代码空间中的过程。SCAL实现了其他架构中的系统调用的功能。

所有调用都是间接通过一个过程入口点表（Procedure Entry Point Table,PEP），它占据了每个代码空间的前512个字。PCAL或SCAL指令的最后9位是在这个表中的一个索引。这种方式有危险也有好处：内核和用户代码使用完全相同的函数调用方法，这简化了编码规则并容许代码在内核和用户空间之间相互移动。另一方面，至少从理论上来说，SCAL指令使任何用户程序都能够调用任何内核函数。

系统基于环境寄存器中的priv位来保护敏感过程的使用。它分为三种过程：

·非特权过程：任何过程都可以访问它，无论这些过程是否有权限。

·特权过程：只有从其他有特权的过程中才可以访问它。

·可调用过程：可以从任何过程中调用它，但是，一旦调用后就会设置priv位。它们在特权过程和非特权过程之间提供了链接。

特权过程、非特权过程和可调用过程之间的区别取决于它们在PEP中的位置。因此，在系统PEP（有时候称为SEP）中也可能存在非特权库的过程。这张表的结构如图8-6所示。

图　8-6：过程入口点表

8.4.3　PCAL和SCAL指令的行为

PCAL指令完成下列行为：

·如果环境寄存器中的priv位没有设置（意味着这个调用过程是非特权的），检查“第一个priv”的值（代码空间的字为1）。如果指令中的偏移量更大或相等，这个过程正在设法调用一个特权过程。生成一个保护阀。

·如果环境寄存器中的priv位没有设置，检查“第一个可调用过程”的值（代码空间的字为0）。如果指令中的偏移量更大或相等，设置环境寄存器中的priv位。

·把P寄存器（程序计数器）中当前的值压入栈。

·把环境寄存器中原来的值压入栈。

·把L寄存器当前的值压入栈。

·把S寄存器（栈指针）复制到L寄存器。

·把环境寄存器的RP字段设置为7（空）。

·把这个指令定位的PEP字的内容载入P寄存器。

SCAL指令的工作方式完全相同，除此之外，它还设置环境寄存器中的SC位，因而确保在内核空间中继续执行。数据空间不会改变。

PCAL和SCAL指令非常相似，程序员通常不必区分它们。由系统在执行的时候区分它们。因此，库过程可以在用户代码和系统代码之间相互移动而不必重新编译。

8.5　处理器间总线

CPU之前的所有通信都通过处理器间总线（IPB）进行。实际上有两条总线，称为X和Y（如图8-1所示），以防一条出故障。与其他的组件不同，当两条总线正常时会并行使用。数据以长度固定为16字的包的形式通过总线。总线的速度可以快到使两个CPU的内存满负荷，所以，客户端CPU利用SEND指令在调度程序中同步地执行它。目的（服务器）CPU在启动一个传输的时候为它预留缓存空间。在传输完成时，目的CPU收到一个总线接收中断信号并处理这个包。

8.6　输入/输出

每个处理器都有一个最多有32个控制器的单I/O总线。所有的控制器都采用双通道并连接到两个不同的CPU。任何时候，只有一个CPU可以访问任意特定的控制器。在CPU和控制器之间的关系称为拥有关系：控制的CPU“拥有”控制器。备份的通道不会使用，直到主通道出故障或系统操作员手动切换到它（所谓的主切换）。

磁盘问题是一个特别敏感的问题，因为许多组件都可能出故障。出故障的可能是磁盘本身、磁盘的物理连接（线）、磁盘控制器、I/O总线或磁盘连接的CPU。结果，除双通道的控制器之外，每个磁盘都有物理的备份（至少在理论上），而且它也是双通道并连接到两个不同的控制器，这两个控制器都连接到相同的两个CPU。在任何时候只有一个CPU可能访问其中某个控制器，这一约束保留了下来，但是，两个CPU分别拥有这两个控制器中的一个是有可能的。从性能的观点来看这是不适当的。

图8-1展示了一个典型的配置：正如灰色高亮的线路所示，系统磁盘$SYSTEM的I/O过程通过CPU 0和第一个磁盘控制器访问$SYSTEM，而另一个连接到相同两个控制器的磁盘$DATA的I/O过程通过CPU 1和第二个磁盘控制器访问$DATA。CPU 0“拥有”第一个控制器，而CPU 1“拥有”第二个控制器。如果CPU 0出故障，CPU 1中$SYSTEM的备份的I/O过程会接管并夺走这个控制器的拥有权，然后继续处理。如果第二个磁盘控制器出故障，$DATA的I/O过程不能使用第一个磁盘控制器，因为它属于CPU 0，所以，这个I/O过程首先会进行主切换，在切换之后，主I/O过程会在CPU 0中运行。它然后通过和$SYSTEM相同的路径访问$DATA。

总之，理论上来说就是如此。实际上，磁盘和驱动器是很贵的，许多人以降级模式运行他们的一些磁盘，不准备双重的驱动器硬件。这也会如你预料的那样运行，但是，当然也不存在任何容错能力：事实上，其中的一些磁盘已经出故障了。

8.7　进程结构

Guardian是一个微核系统：除了低级别的中断处理器（一个单独的过程，IOINTERRUPT）和一些非常低级别的代码之外，所有的系统服务都由运行在系统代码和数据空间中的系统进程处理。

比较重要的进程有：

·系统监控程序，每个CPU中的PID 0，负责启动和停止其他进程，还负责其他像返回状态信息、生成硬件错误信息和维护时间这样的各种各样的任务。

·内存管理器，每个CPU中的PID 1，负责虚拟内存系统的I/O。

·I/O进程，负责控制I/O设备。系统中无论什么地方对I/O设备的访问都通过它专用的I/O进程。I/O控制器连接到两个CPU，所以，每个设备都控制于运行在这些CPU中的一对I/O进程：完成工作的主进程和一个备份进程，备份进程跟踪主进程状态并等待主进程出故障或等待主进程主动把控制权移交给它（“主切换”）。

选择主CPU中的主要问题是CPU负载，CPU负载必须手动平衡。例如，假设你在CPU 2和CPU 3之间连接着6个设备，你可能会把其中的3个主进程放在CPU 2中，另外3个主进程放在CPU 3中。

进程对

进程对的概念不限于I/O进程。它是这种容错方式的基础之一。要理解它们的工作方式，我们必须理解消息传入系统的方式。

8.8　消息系统

正如我们看到的那样，T/16和其他传统计算机之间最大的区别在于没有一个组件是必需的。系统的任何部分都可以出故障而不会引起系统崩溃。这使得它比传统的共享存储器多处理器机器更像一个网络。

这对于操作系统设计来说具有深远的意义。一个磁盘可以任意连接到2～16个CPU。其他的CPU如何访问它？现代的网络使用NFS或CIFS等运行在网络协议之上的文件系统来处理这种特殊的情况。但是，在T/16上这不是特殊情况；这是标准。

不仅文件系统需要这种通信方式：所有进程间的通信也都需要它。

Tandem针对这个问题的解决方法是在操作系统中以非常低级别运行消息系统。用户程序不能直接访问它。

消息系统在进程之间传输数据，在许多方面它类似于后面的TCP或UDP。消息的发起者称为请求者，而目标则称为服务器。[1]

进程之间所有的通信都通过消息系统，即使在相同的CPU中。下列的数据结构实现了通信功能：

·每个消息都联合了两个链接控制块（Link Control Block,LCB），一个用于请求者，一个用于服务器。这些小的数据对象设计成适合单个IPB包。如果需要能比LCB容纳更多数据，那就会附上一个单独的缓冲。

·为了发起一次传输，请求者调用过程link。这个程序把消息发给服务器进程并让这个LCB在它的消息队列中排队。此时，服务器进程还没有参与进来，但发送器会用一个链接请求（LREQ,link request）事件唤醒这个过程。

在请求者端，对link的调用会立即返回信息以识别这个请求；请求者不必等待服务器处理这个请求。

·经过若干时间，服务器过程发现了这个LREQ事件并调用监听listen，监听从消息队列中取走第一个LCB。

·如果有一个缓冲与这个LCB关联了并含有传给这个服务器的数据，服务器会调用readlink来读入数据。

·服务器然后执行任何必需的进程，接着调用writelink响应这个消息，仍然有可能包含一个数据缓冲。这会通过LDONE来唤醒请求者。

·请求者发现这个LDONE事件，检查结果，然后通过调用breaklink来结束这次交互，断开链接会释放相关的资源。

只不过内核的其他部分直接使用消息系统；文件系统用它和I/O设备及其他进程进行通信。进程间通信的处理方式几乎和I/O完全相同，而且，它也用于支持容错进程对。

这种方式本质上是异步的和多线程的：在调用link后，请求者继续它的操作。许多请求者可以把请求发给同一个服务器，即使当服务器不处理请求的时候。服务器不必立即响应链接请求。当服务器答复时，请求者不必立即确认这个答复。相反，每次当进程可用时会由一个它可以处理的事件唤醒。

8.8.1　回顾一下进程对

容错的要求之一是单个故障不能导致系统崩溃。我们已经看到I/O进程通过使用进程对解决了这个问题，而且很明显，这也是处理CPU故障的常规方式。因此Guardian支持用户级进程对的创建。

所有进程对都以一个主进程和一个备份进程的方式运行。主进程进行处理，同时备份进程处于“热备份”状态。主进程时常通过一个名为checkpointing的进程更新备份进程的内存映像。如果主进程出故障或主动放弃控制，备份进程会从最后的检查点开始继续。许多程序实现了checkpointing，它是由消息系统执行的：

·备份进程调用检查监视器（checkmonitor）以等待主进程的检查点消息。它一直调用检查监视器，直到主进程出故障或放弃控制。在这段时期，这个CPU的唯一用处是和消息系统通信以更新它的数据空间，以及打开和关闭消息系统以更新文件信息。

·主进程调用检查点把它的部分数据空间和文件信息复制给备份进程。程序员可以决定哪些数据和文件在什么时候应该复制到检查点。

·主进程对关于文件打开的检查点信息调用checkopen。这实际上产生一个来自备份进程的调用来打开。I/O进程认出这是一个来自备份进程的打开请求并把它和主进程的打开请求同等处理。

·主进程对关于文件关闭的检查点信息调用checkclose。这实际上产生一个来自备份请求的调用来关闭。

·主进程可能会调用checkswitch来主动释放对进程对的控制。当发生这种情况时，主进程和备份进程互换角色。

当备份进程从检查监视器返回时，它已经变为新的主进程。它回到原主进程最后调用checkpoint的位置，而不是回到它被调用的位置。然后，它从这个位置开始继续处理。一般而言，一个进程对的生命周期如表8-1所示。

[1]在功能方面，这些名字非常接近于现在的术语：客户端和服务器。

8.8.2　同步

这种方式是非常可靠的，它可以提供比纯粹的锁步（lockstep）方式更好的可靠性。在一些程序错误中，特别在竞态条件（race condition）下，以锁步方式运行的进程会遇到完全相同的程序错误，还会崩溃。采用连接比较松散的方式通常可以避免完全相同的情形并使程序继续执行。

有两个问题不是非常明显：

·checkpointing是CPU密集（CPU-intensive）的。一个进程的检查点设置应该多频繁？在检查点设置应该处理哪些数据？这些决定留给程序员。如果他做错了或忘记在检查点处理重要的数据，或做的时间不恰当，那么备份进程中的内存映像将会不协调，备份进程就可能出故障。

·如果主进程执行了外部可见的操作，例如在记入检查点之后但在出故障之前执行了I/O操作，备份进程在接管之后会重复这些操作。这可能会导致数据异常。

实际上，检查点选取异常的问题还没有证实是一个问题，但是，重复I/O的确是一个问题。系统通过给每个I/O请求关联一个序号（称为同步ID）来解决这个问题。I/O进程记录了这些请求，如果它收到一个重复的请求，就简单地返回这个请求第一次调用的完成状态。

8.8.3　网络：EXPAND和FOX

T/16的消息系统实际上是一个独立的网络。通过把消息系统扩展到全世界，Guardian就可能很好地提供大范围的网络。这种实现称为EXPAND。

从程序员的观点来看，EXPAND几乎是完全无缝的。它可以连接多达255个系统。

1.系统名字

每个系统都有一个以反斜线符号开头的名字，例如\ESSG或\FOXII，加上一个节点号。节点号不像现在的IP地址那么易懂：从程序员的角度来看，仅仅为了给文件名编码才必须需要它们，随后我们将看到它们。

EXPAND是消息系统的一个扩展，所以，大部分细节对程序员都是隐藏的。唯一的问题在于速度和存取需求之间的不同。

2.FOX

完全由于实际的约束，构建一个拥有多于16个CPU的系统是困难的；特别是硬件的约束把处理器间总线的长度限制到只有几米，所以，实际的限制是16个CPU。除此之外，Tandem提供一种把多达14个系统连在一起就像本地集群一样的快速光纤连接能力。在许多方面，这是一个更高速度的EXPAND版本。

8.9　文件系统

Tandem使用术语“文件系统”来表示这种可以提供数据（“读”）或接受数据（“写”）的对系统资源的存取方式。除磁盘文件之外，文件系统也处理设备，例如终端、打印机、磁带单元，还处理进程（进程间通信）。

8.9.1　文件命名

设备、磁盘文件和进程有一个通用的命名规则，但是很遗憾，由于有许多例外，这个规则有点复杂。进程可以拥有名字，但只有I/O进程和进程对才必须拥有名字。不管怎样，文件“名字”的长度为24个字符，由分别为8字节的3个部分组成。只有第一部分是必需的；其他两个部分只用于磁盘文件和指定的进程。

没有命名的进程只使用名字的前8个字节。未配对的系统进程（例如监视器或内存管理器）的命名格式如图8-7所示。

图　8-7：未配对系统进程的命名格式

未配对用户进程的命名格式如图8-7所示。

图　8-8：未配对用户进程的命名格式

CPU和PIN组合在一起形成了进程ID或PID。PIN是CPU内部的进程标识号。这就限制了每个CPU最多拥有256个进程。

真正的名字以一个“$”符号开始。设备只使用前8个字节，而磁盘文件使用全部的三个部分。单个部分看起来像是磁盘名、目录和文件，虽然实际上每个磁盘只有一个目录。进程可以把其他两个部分用来相互传递信息。

表8-2中列出了一些典型的文件名字。

如果名字的某一部分的长度少于8个字节，就会用ASCII的空格填补。从外表来看，名字表示为有句点的ASCII字符，例如$SYSTEM.SYSTEM.LOGFILE和$SPLS.#DEFAULT。

在命名规则中还有较多的约定。进程的子名称必须以井号（#）开始，而用户进程的名字（不包括设备的名字，它是实际的I/O进程的名字）在第一部分的最后有PID（如图8-9所示）。

图　8-9：指定用户进程的命名格式

这个例子中的PID是主进程的PID。这样就把用户进程名的长度限制到6个字符，包括起始的$。

这样似乎还不够，对于远程系统上指派的进程、磁盘文件或设备还有一组单独的命名。在这种情况下，起始的“$”符号替换为“\”符号，名字的第二个字节是系统号码，把名字的其余部分向右移一个字节。这样，如果一个进程是网络可见的，那么，该进程的名字的长度将限制为最多5个字符。所以，从另一个系统来看，我们先前看到的打印缓冲进程将拥有\ESSG.$SPLS这个外部名称，内部的格式如图8-10所示。

图　8-10：网络可见进程的命名格式

数字173是系统\ESSG的节点数。

8.9.2　异步I/O

文件系统接口的重要特性之一是强调异步I/O。我们已经知道消息系统本质上是异步的，所以，这实现起来相对简单。

进程在打开一个文件的时候可以选择同步的或异步（“不等待”）的I/O。当以不等待的方式打开一个文件时，这个I/O请求会立即反馈，只有一些显而易见的错误会报告出来，例如，如果这个文件描述符是不公开的。随后，用户调用awaitio来检查该请求的状态。这引起这样一种编程方式：一个进程发起许多不等待的请求，然后进入调用awaitio的中心循环并处理完成的请求，典型的是发起一个新的请求。

8.9.3　进程间通信

在文件系统级别，进程间通信是到消息系统的一个相对直接的接口。这引起一个问题：消息系统是不对等的。请求者发送一个消息并可能接收一个响应。响应文件系统的一个read命令是没有意义的。在服务器端，服务器读取一个消息并响应它；响应一个write命令也没有意义。

文件系统提供读和写的过程，但是读的过程只对I/O进程起作用，把它们映射到消息系统的请求。在进程间通信级别，读过程没有用，而且，实际上，写过程也用得不多。请求者改为调用一个名为writeread的过程先写一个消息传给服务器，然后从服务器获得一个响应。消息或响应都可以为空（长度为0）。

这些消息到达服务器的消息队列。在文件服务器级别，消息队列是一个名为$RECEIVE的伪文件。服务器打开$RECEIVE并通常使用readupdate过程来读取一个消息。随后，它可以用reply过程进行响应。

8.9.4　系统消息

系统使用$RECEIVE把消息传给进程。最重要的消息之一是startup消息，它把参数传给新启动的进程。下面的例子是用TAL编写的，TAL是Tandem的低级别系统编程语言（虽然这个名字表示“Tandem Application Language”）。TAL源自于HP的SPL，而且它类似于Pascal和Algol。它的最不寻常的特征之一是在标识符中使用脱字符号（^）；下划线字符（_）是不允许使用的。这个例子应该和C非常相似，并很容易理解。它展示了这样一个进程：启动一个子服务器进程，然后和它通信。

第一部分代码展示了父进程（请求者）：

call newprocess（program^file^name，process^name）；—start the server process

call open（process^name,process^fd）；—open process

call writeread（process^fd,startup^message，66）；—write startup message

while 1 do

begin

read data from terminal

call writeread（process^fd，

data,data^length，—write data

reply,max^reply，—read data back

@reply^length）；—return real reply length

if reply^length＞0

write data back to terminal

end；

接下来的代码展示了子进程（服务器）：

call open（receive,receive^fd）；

do

call read（receive^fd,startup^message，66）；

until startup^message=-1；—first word of startup message is-1.

while 1 do

begin

call readupdate（receive^fd,message,read^count,count^read）；

process message received,replacing buffer contents

call reply（message,reply^length）；

end；

子进程接收到最初的消息是系统消息：子进程的父进程open给子进程发送了一个open消息，然后，最初的writeread调用发送了启动的消息。子进程处理这些消息并响应它们。它可以使用这个open消息来记录请求者或接收这个文件名最后的16个字节所传过来的信息。只有这时候这个进程才从它的父进程接收正常的消息。这时候，其他进程也可以和这个子进程通信。同样地，当一个请求者关闭服务器时，服务器收到一个close的系统消息。

8.9.5　设备I/O

记住这很重要：设备I/O（包括磁盘文件I/O）都由I/O进程处理，所以，“打开一个设备”实际上是打开了I/O进程。还要记住，到设备的I/O和到文件的I/O的实现稍微有些不同，虽然文件系统的过程都一样。特别地，典型的用于访问文件的程序是比较常规的读和写，而通常的磁盘I/O不是“不等待”的。

8.9.6　安全

为了使时间一致，T/16不是一个绝对安全的系统。实际上，这没有引起任何严重的问题，但是，有一个问题值得提一下：从非特权程序到特权程序的转换是基于程序入口点在PEP表中的位置及环境寄存器中priv位的值。在早期，这种利用就十分明显。如果你可以让一个特权程序通过一个指针返回一个值并让这个程序以priv位的值被设置的方式重写栈上保存的环境寄存器，这个进程可以依赖那个程序的返回值保持特权。这是可调用程序的职责：检查它们的指针参数以确保它们没有任何寻址异常，而且它们返回的值仅适用于用户环境。程序setlooptimer（设置watchdog定时器并任意地返回原值）中一个缺陷使变为SUPER.SUPER（root用户，拥有255、255或-1的ID）成为可能。

proc make^me^super main；

begin

int.TOS='S'；—top of stack address

call setlooptimer（%2017）；—set a timer value

call setlooptimer（0，@TOS[4]）；—reset,return old value to saved E reg

pcb[mypid.＜8：15＞].pcbprocaid：=-1；—dick in my PCB and make me super

end；

对setlooptimer的第二次调用把原值%2017返回给栈中保存的环境寄存器的内容，尤其是设置priv位，这使这个进程处于特权状态。理论上，这个值可能会缩减到%2016，但是，这没有任何区别（这是保存的RP区域，不能恢复）。这个程序然后在它自己的进程控制块（PCB）中使用相对SG的寻址来修改用户信息。mypid是返回当前进程的PID的一个函数，而最后的8位（＜8：15＞）是PIN，用作为PCB表中的索引。

当然，这个缺陷很快就修复了，但是它显示了这种方式的一个缺点：它依赖程序员去检查传给可调用程序的参数。在这个架构的整个生命周期中，又重新出现了这样的问题。

8.9.7　文件访问安全

Tandem文件访问安全的方式类似于UNIX的方式，但是，用户可以只属于单个组，那是用户名的一部分。因此，我的用户名SUPPORT.GREG（用数字写成20，102）表示我只属于SUPPORT组（20），而在那个组内我的用户ID是102。这些字段的每一个都是8位长，所以，完整的用户ID正好一个字。如果我要成为另一个组的成员，我将需要另一个用户ID，可能会有一个不同的数字—例如，用户ID为255，17的SUPER.GREG。

每个文件都有许多位描述了拥有者（owner）、组（group）或所有用户（all）对这个文件的访问权限。然而，不像UNIX，这些位以不同的方式进行组织：四个权限是read、write、execute和purge。purge是Tandem对delete的称呼，这是必需的，因为目录不具有它们自身的安全设置。

对于每一种访问模式，都可以选择允许谁使用它们：

·Owner意味着只有这个文件的拥有者。

·Group意味着同一组中的任何人。

·All意味着所有人。

所有这些都只涉及文件所在的同一个系统。对于网络，还引入了另一组模式来控制其他系统的用户的访问：

·User意味着只有具有与该文件拥有者相同的用户名和组编号的用户。

·Class意味着具有和该文件拥有者相同的组编号的任何人。

·Network意味着任何人，任何地方。

对于设备没有任何安全性，用户进程必须自己进行控制。前者在网络环境中尤其是一个缺点。在1989年初的一个安全性讨论会上，仅仅通过在这个系统控制台上放置一个假的提示符，我能够演示偷到一个位于美国库珀蒂诺管理区域中部的\TSII系统上的SUPER.SUPER（root）的口令。那时候我在德国的杜塞尔多夫。

8.10　轶闻趣事

回到21世纪早期，很容易忘记用计算机进行工作的乐趣。Tandem是一个有趣的公司，而且这个公司关心它的职员。在1974年末的一个星期五，在这个系统的开发初期，几个创始人最终使这个软件运行在硬件上了；在这之前，这个软件一直在模拟器上进行开发。你可以想象那个激动的场面。故事开始了，一个副总裁出去并带回来一箱啤酒，然后，所有人都围坐在啤酒箱周围，庆祝并讨论将来。他们讨论的一件事是应该每周一次喝一箱啤酒，就这样，Tandem的啤酒会诞生了。它真的延续到了20世纪90年代，然后它变得逐渐不适宜，最后取消了。

Tandem提出了许多口号和文字游戏，当然，“Tandem”这个名字本身就是其中的一个。那时候，我们拥有印着像“So nice,so nice,we do it twice（非常好，非常好，我们做了两遍）”、“There's no stopping us（没有什么可以使我们停下来）”和“Tandem users do it with mirrors（Tandem用户用镜像完成了任务）”这样口号的T恤衫。当然，当有好奇心特别强烈的人问起时，标准答案是“这是为了防止其中一个出现故障”。

最后的口号不仅仅是俏皮话。它深深地影响我们的思考进程。在1977年5月，从Tandem最初的五个星期培训回来后，我难过地发现我的猫逃走了。在确定它不会回来后，我出去并又领了两只新猫。不久之后我意识到这是成功洗脑的结果。即使在今天，我仍然讨厌重启计算机，除非绝对不能避免。

8.11　弊端

就预期的目标而言，T/16是一款非常成功的机器（美国的ATM一度有超过80%由Tandem系统控制），但是当然也存在缺点。有些缺点（例如，比常规系统更高的成本）是不可避免的。另一些缺点对于设计者来说不是那么显而易见的。

8.11.1　性能

当增加硬件时性能获得接近线性的提高，Tandem有理由对此感到自豪。引用了随后的TXP系统的Hors和Chou（1985）展示了一个FOX集群如何能够从2个处理器线性地扩展到32个处理器。

Bartlett（1982）展示了它的弊端：消息系统的性能限制了系统的速度（即使是小系统）。没有附加数据的单个消息需要花2毫秒来传输，在每个方向上都有2000个字节数据的消息需要花4.6毫秒（相同的CPU）到7.0毫秒（不同的CPU）。这是单个I/O操作的开销，即使在当时，这也显得很慢。对一个磁盘文件的连续I/O请求之间的延迟如此长，以致它们直到数据传输到了磁盘头才会发生，这意味着磁盘每转一周才能满足一个请求。一个从磁盘中连续读取2KB数据并进行处理的程序（例如，类似于grep）将只能获得120KB/s的吞吐量。较小的I/O能力（例如512字节）可能使吞吐量受制于软盘的速度。

8.11.2　硬件限制

正如名字“Tandem/16”所示，设计者有一个16位的头脑。在20世纪70年代中期，这相当典型，但是，事实是“真的”计算机拥有32位的字。随着时间的推移，后续的机器会解决其中的很多问题。在1981年，Tandem通过一个向上兼容的指令集及少量硬件限制引入了NonStop II系统。在接下来的10年里，许多兼容但更快的机器引入了进来。没有一个特别快，但它们对于在线交易处理已经足够快了。另外，操作系统也进行了重写以解决比较紧迫的问题，而且，随着时间的推移还进行了额外的改进。这些改进包括：

·引入了一个31位的寻址模式以给用户进程“无限的”存储空间。这个模式使用字节地址，但是，它没有移除栈大小和代码跨越32KB边界的限制，因为老的指令格式仍然保留着。

·增加了硬件虚拟内存映像的数量。T/16只有4个，用于代码空间和数据空间。TNS/II，正如它的名字一样，总共拥有16个内存映像，这意味着处理器可以直接寻址到2MB而不必使用内存管理器。这些映像之一用作为类似于转换旁视缓冲以处理31位扩展地址。

·Guardian II，与TNS/II一起发布的Guardian新版本，也展示了系统库和用户库空间，它们把进程可用的总空间增加到了384KB。尽管如此，随后，通过段切换（segment switching），库空间的数量从2个（系统和用户）增加到多达62个（系统和用户各31个）。在任何时候只有一个用户库和系统库映射可以是活动的。

·消息队列的大小被证实是一个问题。监视器进程每隔一定间隔把状态信息发送给需要它们的每个进程。如果这些进程不读取信息，非常多的资源（LCB和消息缓冲）会用于复写消息。为了解决这个问题，Guardian II引入了这样一个进程：保持这些状态信息的单个副本并当其收到提醒时把消息发送给一个进程。

8.11.3　错过的机会

T/16是一台革命性的机器，除此之外，它还提供了当时其他机器很少拥有的一个环境。不过说到底，妨碍的正是这些小事情。例如，设备无关性是操作系统最持久的目标之一，Tandem为实现这个目标提供了很大的帮助。不过，由于命名问题和几乎不必要的不相容性，它们失去了全部的潜力。为什么进程间通信不可能使用读取的方式？为什么进程名和设备名在格式上必须区分？为什么在第九个字节需要一个#符号？

8.11.4　分裂的大脑

一个比较严重的问题在于探测错误的基本方式。当只有一个组件出故障时这种方式工作得很好，即使两个组件出故障，这种方式通常也工作得相当好。但是，如果两个处理器间总线都出故障，那会发生什么呢？每个CPU都假定另一个已经出故障并接管I/O设备—不是一次，而是会持续不断。这样的情况确实出现过，但很幸运，非常少，它们通常导致在两个CPU之间共享的磁盘数据完全损坏。

8.12　后继者

从1990年开始，许多因素导致Tandem的销售出现滑坡：

·计算机硬件总体正变得更可靠，这减小了Tandem的优势。

·计算机硬件的速度提高了许多，从而突出了架构的一些基本性能极限。

在20世纪90年代，T/16处理器架构被基于MIPS的解决方案所替代，虽然剩下的许多架构仍然在使用。另一方面，性能上的差异仍然很大，以致即使到了2000年，Tandem仍然一直使用MIPS处理器来模拟T/16指令。其中的理由之一是Tandem的大多数系统级软件仍然用与T/16架构紧密相连的TAL语言编写。把源代码移植到C的提议由于高昂的费用而被否决了。

对于这样一个革命性的系统，Tandem/16对行业和现代计算机的设计只产生了极小的影响。很多功能现在很容易用到—镜像磁盘、网络文件系统、客户端-服务器模式或热插拔硬件。但是，很难看到其中存在由Tandem引领的功能。这可能是因为T/16与大部分系统差异很大，当然，还因为把它开发出来的纯商业环境的改变。

8.13　延伸阅读

在Hewlett-Packard的网站上有许多论文；从位于http://www.hpl.hp.com/techreports/tandem/的Tandem技术报告开始看起。尤其是：

Bartlett,Joel."A NonStop Kernel，"June 1981.http://www.hpl.hp.com/techreports/tandem/TR-81.4.html?jumpid=reg_R1002_USEN.（关于操作系统环境的更多信息。）

Bartlett,Joel,et al."Fault tolerance in Tandem computer systems，"May 1990.http://www.hpl.hp.com/techreports/tandem/TR-90.5.html.（描述硬件的细节。）

Gray,Jim."The cost of messages，"March 1988.http://www.hpl.hp.com/techreports/tandem/TR-88.4.html.（从理论视角描述性能问题。）

Horst,Robert,and Tim Chou."The hardware architecture and linear expansion of Tandem nonstop systems，"April 1985.http://www.hpl.hp.com/techreports/tandem/TR-85.3.html.

第9章　JPC：一个纯Java的x86 PC模拟程序

Rhys Newman Christopher Dennis

原则与特性

结构

功能多样性

√模块

概念完整性

依赖关系

修改独立性

√进程

√自动传播

数据访问

可构建性

√增长适应性

√熵增抵抗力

“模拟程序很慢，Java也很慢；因此，两者的结合只能意味着以蜗牛一样的速度进行计算。”正如这个传统智者所说，第一个JPC原型比真的机器慢10 000倍。

不过，纯Java x86 PC模拟程序是一个引人注意的想法—想象一下，在一个安全的Java沙箱内启动Linux和Windows时速度仍然保持快到足于实际使用。这个任务可能不容易，因为它需要复制人类已经创造的最复杂的机器之一的内核。仿制物理x86 PC的设计、部署在Java虚拟机之上，然后在Java Applet沙箱的安全限制之内装配这些成果，完成这个任务将是一个困难重重的探索之旅。

我们在这个过程中体验了现代软件工程师很少遇到的运算的挑战，但是这也对那些通常认为理所当然的基础提出了适时的提醒。现在，我们拥有了一个架构，它表明了纯Java的x86硬件模拟程序是可行的且速度最终能够快到足于实际使用。

9.1　简介

随着处理器速度的加快以及家用计算机用户使用的网络性能的提高，越来越多在几年前还认为是不切实际的事情正变得平常。十年前，当一家名为VMWare的技术小公司在加利福尼亚成立时，把一台完全虚拟的计算机作为软件运行在一台物理计算机内，这种观点还被认为相当难以理解。毕竟，如果你拥有一台计算机，为什么仅仅为了要运行你已经在运行的系统而增加一个虚拟层，从而使计算机变慢呢？你使用的软件需要硬件全力来运行，如果需要，可以直接买更多的机器来做更多的工作，为什么这样做呢？

十年后我们都看到了虚拟机的好处。硬件的速度如此快，以致现代的机器可以运行许多虚拟机而对整体的性能没有大的影响，软件服务的重要性如此显而易见，以致在虚拟机中完全隔离软件服务的安全性和可靠性的优点非常明显。

然而，纯虚拟化也有它的问题，因为纯虚拟化在一定程度上依赖硬件的支持[1]来运行，因此，它也受到与物理机器如此紧密地连接而导致的不稳定性的影响。与此相比较，模拟程序是完全用软件构建的虚拟计算机，因此，对底层的硬件没有特别的要求。这意味着模拟的计算机和实际的硬件完全分开。与通常的应用程序软件相比，它在系统上的存在既不遭受也不引起额外的不稳定性。正如最初运行应用程序软件是计算机存在的原因一样，模拟程序将始终是创建虚拟机的最稳定和最安全的方式。

正如十年前对虚拟化一样，目前对模拟程序的批评集中在它引起的速度损失上，这通常很明显。然而，历史证明，速度问题会随着技术的进步而得到解决，即使是现代日益复杂的硬件和软件栈，甚至在硬件、操作系统和应用程序软件之间微妙交互而引起的更困难的问题。在非常低的层次以非常强壮和安全的方式分开系统，与此同时仍然共享物理资源，这已经变得日益必要，但也日益困难。模拟程序提供了必需的强壮性、安全性和灵活性，因此，它会成为一个日益令人注意的选择。

目前有许多模拟程序可以使用，Bochs和QEMU是模拟x86 PC最著名的例子。两者都开发了很多年，都能够准确地启动现在的操作系统并运行应用程序软件。然而，两者都是用本机码（C/C++）编写的，如果它们要运行在新的底层硬件架构/OS栈上（例如，一个新型的宿主系统），就需要重新编译。此外，对于安全级别非常高的应用程序，总是有这样的担忧：模拟程序已经被恶意篡改了，或让访客的代码做恶意的事情，或包含一个容许这些问题的缺陷。例如，QEMU使用动态二进制转换来达到可接受的速度，如果这个进程被损坏或被发现有一个缺陷，那么，这个软件就会变得不可靠或破坏安全保护措施。

如果用户能够接受模拟程序在安全可以绝对保证的时候的速度损失，那么，为什么不在部署最广泛和最安全的Java虚拟机（Java VM,JVM）上建造一个虚拟程序呢？JVM作为运行代码的一个安全方式已经测试了十多年，用户通常愿意让从因特网下载下来的未认证代码在Applet沙箱（JVM提供的安全容器）中执行。这是因为JVM提防基础的编程错误，例如内存溢出和读取未分派内存中的数据。此外，安装了安全管理器以加强沙箱的JVM可以禁止访客软件尝试的任何敏感操作。

JPC完全是用Java编写的一个x86 PC模拟程序。在JPC中没有本机码；它模拟了一个x86 PC的所有标准硬件组件且完全在Java Applet沙箱内。因此，只要不越出这些安全约束，在JPC内运行的x86操作系统和软件就完全和底层的硬件隔离，甚至连硬件也不必是一台x86 PC。[2]从宿主的角度来看，JPC代表的仅仅是一个Java应用程序/Applet，因此，宿主可以确信运行的代码（无论它是什么）是安全的。JPC可以启动DOS和现代的Linux，让访客的软件和操作系统完全无拘无束地访问虚拟机中的所有硬件，包括root/admin的权限，而所有的这些都在Java的沙箱内。

为了攻破JPC，攻击者必须在JPC的代码中发现与JVM中缺陷相符的缺陷，这可以使一个敏感的动作在一台也在运行这个JVM的用户的权限之内的宿主计算机上运行。这意味着打破了3个完全独立的安全层。通常每一层都是由完全不同的公司构建的，而且因为在如此多不同的环境中每一层都是必需的，它们的安全一直在不停地进行独立的测试和检查。还要注意在破坏这些层中的一致性要求。在JPC中发现一个缺陷后接着去攻破JVM是行不通的；黑客必须在JPC发现一个直接（且已经）连接到正使用的JVM内的一个相配安全缺陷的缺陷。由于JPC是开源的，检查代码并构建一个“干净”的版本是一个相对简单的任务，可以把一个安全性增强的JVM用于要求高安全性的应用程序。因此，JPC成为有恶意的x86代码无法攻破的屏障，而且真正提供了最可靠、便捷和安全的方式来检查运行中的恶意x86代码。

和虚拟化一样，当硬件的速度加快时，模拟的应用程序可以从安全领域延伸到需要保证健壮性的负责重要任务的系统。无论模拟的机器如何崩溃（或如何损坏），宿主都可以不受影响并能够继续运行其他模拟的实例。这个技术可以避免在现代的硬件支持虚拟化的有用特征中即使经过最仔细的思考还可能存在的问题。[3]

在这一章的其余部分，我们将讨论用于构建原型和开发JPC的进程，我们还将说明如何对设计进行许多逐步改进才形成今天这个可用的JPC。然后，我们再略述一些应用程序和JPC呈现的独特技术的含义。

[1]最起码你需要和“虚拟化的硬件”相同的硬件。因此，像Xen和VMWare这样的产品使虚拟的x86 PC只能创建在x86硬件上。

[2]JPC已经适合在J2ME环境中运行并能够在一个基于ARM11的移动电话上启动最初的和未更改的DOS！

[3]例如，硬件支持的x86 CPU虚拟化（Intel VT、AMD Pacifica）由于多核芯片的L1/L2高速缓冲寄存器共享而具有安全弱点。

9.2　概念验证

x86 PC已经存在了三十多年，并从许多不同代的硬件中发展而来。在每一个时期，它都一直保持向后兼容，所以，即使在今天，最初的8086程序也可以运行在一台新的PC上。虽然这具有毋庸置疑的优点且导致这个平台空前的成功，但是，这也意味着它为了避免破坏现有的代码，在合并新技术时变得非常复杂。如果现在重新构建一台PC，硬件的许多方面都会截然不同，而且几乎肯定会简单得多。

然而，x86平台到处存在，当今世界有超过10亿台，并且每年生产超过2亿台。因此，最有用的模拟程序将是以x86 PC架构为目标的模拟程序。

然而，这不是一项容易的工作。必须用软件模拟的硬件就包括x86处理器、磁盘（和它的控制器）、键盘和鼠标、VGA图形显卡、DMA控制器、PCI总线、PCI主桥、内部定时器、实时时钟、中断控制器和PCI ISA桥。每个设备都有它自己的规范说明书，必须阅读它们并翻译成软件。x86处理器手册有1500多页，技术手册总共大概有2000页。x86指令集非常大，一个程序代码中可能用到多至65000条指令，在内存页上设置有4个不同的保护级别，4个不同的处理器“模式”，在每个模式中，指令都可能（而且确实）有不同的效果。

所以，在着手这项巨大的任务之前，评估预期的成果是否值得，这很重要。Bochs和QEMU项目都完成了这个壮举，通过检查他们解决这些问题的途径，我们可以获得一些信心。然而，这两个项目都是C/C++的程序，因此，只能获得有限的帮助，因为JPC必须运行在有额外设计约束和性能考虑的纯Java环境中。

通过选择模拟一组简单的硬件进行一些简化是有可能的。因为处理器模拟将是系统中的主要瓶颈，模拟一个复杂但快速的硬盘控制器（或驱动器）没有多大用处。一个简单并可靠的硬盘模拟程序就可以容易地满足，这对于其他的硬件组件同样适用。即使在考虑处理器的时候，可以选择奔腾Ⅱ作为目标，那就没有必要模拟最新芯片中出现的扩展指令集。因为现在的软件，包括操作系统，即使没有这些指令保证向后的兼容性，但也能运行得相当好，这个决定没有明显的局限性。

然而，模拟程序的速度仍然是主要的挑战。获得最好性能的显而易见的方式是使用某种形式的动态二进制转换从不流畅的按部就班的软件模拟程序变成一个编译模式，在编译模式中，底层硬件的自身速度可以更有效地使用。这种技术使用在许多不同的外表下；现代的x86处理器把首次使用的x86指令分解为更小的微码，然后缓存下来以便快速地重复执行。即时编辑的Java环境同样把字节码编译成本机码以提高重复许多次的代码的执行速度。这种技术的效率是基于这个事实：在几乎所有的软件中，10%的代码使用了90%的执行时间。[1]找到提高重复这10%“热”代码的速度的简单的方法可以显著地提高整体的速度。还要注意，应用这种技术并不意味着编译所有的代码；选择性的优化可以导致巨大的性能提升而不会引起无法接受的延迟，如果所有代码都进行优化，就可能发生这种情况。

几乎所有的软件（无论是从C/C++或Java字节码编译到本机的x86）在执行前都由现代的计算机进行或多或少的动态二进制转换，这个事实显示了这种技术的能力和效果。因此，当着手处理创建JPC的这个任务时，如果可以使用相似的技术，获得合理的速度是有希望的。

通过从现有的模拟程序获得这项工作是可行的信心，通过选择一个简单的PC架构作为最初的目标，我们把最初的范围降低到一个可完成的级别。然后，我们需要评估动态二进制转换提高速度并确保达到一个（即使在大体上）实际可用的性能的可能性。如果JPC在应用了所有的编程技巧后的最优结果是以1%的速度运行，那么，这个项目将是不值得做的。

潜在的处理器性能测试

为了估计使用各种模拟策略后能获得的潜在性能级别，作为一个简单的模型，一个“Toy”处理器发明了出来。这个Toy处理器有13个指令、2个寄存器和128字节的RAM。用Toy的汇编语言编写了一个相当于下面这段C代码的简单程序以进行速度测试：

for（int i=0；i＜10；i++）

for（int j=0；j＜50；j++）

memory[51+j]+=4；

存储器最初都是零，这个程序的输出是最终的存储器状态（存储器的最初50个字节留给这个程序代码使用）。

Toy架构的一个Java模拟程序构建了出来，在Sun HotSpot VM上连续运行这个程序十万次花了8000毫秒。在相同的硬件上，一个用GCC编译的C程序花了86毫秒（用所有静态优化进行编译）。毫不奇怪，本机模拟引起了两个数量级的性能损耗。但是，这个简单的模拟程序确实是非常简单，每次都从寄存器中读取下一个汇编指令，通过一个switch语句选择要执行的操作，而且一直循环直到完成。

一个较好的版本将会除去这个查找-分派过程。这表现了内联的普通诀窍；C编译器在编译的时候内联经常用到的代码，而HotSpot VM在运行时进行内联以响应真正的执行遥测。做到这一点后，模拟程序花了800毫秒，速度提升了10倍。

当前，当使用查找-分派时，指令指针必须在每个指针之后更新以便处理器知道从哪儿获取下一个指针。然而，用内联代码，只有在整个内联块执行完成时指令指针才必须更新。移除这些交叉存取的增量也可以帮助HotSpot优化代码；它可以集中在关键操作上而不必担心这个指令指针寄存器必须始终与进度保持一致。由于把指令指针的更新移到内联程序部分的末端，执行的时间减少到250毫秒，又额外提高了3.2倍。

然而，由一个过分简化的自动算法翻译的汇编代码导致在内存和寄存器之间许多不必要的数据移动。在好几个地方，一个结果刚存储到内存的字节数组就立即再次被读出。通过一个简单的流控制分析就可以自动发现这些无效率的地方并移除，这导致执行的时间减少到80毫秒。这和优化的本机程序一样快！

因此，通过适当的运行时编译（这并不是非常复杂），一个Java模拟程序可以以和本地硬件相同的速度运行这个Toy本机代码。很明显，在分析x86 PC的许多更复杂的硬件时期望获得100%的速度是不实际的，但是，这些测试确实表明实际可用的速度是可达到的。

所以，不论最初对纯Java x86模拟程序的整个概念存什么怀疑，现在都有充分的证据表明可以构建这样的技术。接下来的章节详述了如何利用x86 PC硬件设计的架构观点及如何映射到JVM以编写一个高效的模拟程序。还会概要地讲述许多完全基于JVM如何运转的非常重要的软件设计决策在什么地方拥有显著的性能优点。

[1]在DOS的启动次序中和在玩许多的DOS游戏时，利用JPC，这些单凭经验的说法实际上已经得到了证实。可以整体控制一个模拟程序后，编译出这些与程序执行有关的统计很容易。也可以参考Donald E.Knuth的“An empirical study of FORTRAN programs.”（Software Practice and Experience，1：105-133.Wiley，1971.）

9.3　PC架构

现代的PC是一个非常复杂的怪兽。它的硬件已经优化和迭代了许多次以形成一个高效和普遍的计算平台。然而，它也保留了设计来维持它向后兼容的遗留组件和功能。在某些方面这是好事。IA-32机器的基本架构从它自1985年的386中首次出现以来没有改变过。实际上，在系统架构方面，386本身也称不上背离它的x86前身。

虽然图9-1在某些方面非常简单，有一些不同的文字和重复的方框，但是，这可以作为JPC本身的架构图来容易地传递信息。

图　9-1：现代PC的基础架构

JPC的大部分设计是相对简单的系统分析，对于这个模拟程序来说，把初始的系统映射到JPC也几乎是在硬件规范和Java类之间进行一一对应。例如，JPC中的串行端口由单个SerialPort类表示，它实现了HardwareComponent和IOPortCapable。这种简化的方式导致了一种易于理解和掌握的设计，基本上，这个架构内的对象彼此都是松散耦合的。这使JPC具有了非常灵活的优点，就像在实际的机器中，虚拟设备可以插入PCI总线，组件可以相互交换来构建符合很多规范的虚拟机。

违背这种方式的唯一理由是设计和模块化的清晰度直接和性能相抵触。这种情况在JPC中出现在两个关键的地方：一次在计算的中心（处理器），一次在带宽的中心（存储器系统）。这两个热点对这个项目有两个影响：

·代码基变得不平衡。像IDE接口这样简单的硬件设备仅仅是规范文档的翻译，对应于两个类：IDEChannel和PIIX3IDEInterface。处理器，一个复杂得多的设备，相对拥有很多相关的代码。总体上，它由8个不同的包和50多个类来代表。

·作为开发人员，我们发现必须变得和代码基一样分裂。坦率地讲，当你处理存储器系统或处理器系统之外的硬件模拟程序时，你的目标是最终代码的清晰和模块化设计。当你在处理器系统或存储器系统内工作时，你的目标是最终的性能。

在处理架构的敏感部分时对独创性必须有悲观的心理准备。为了获得尽可能高的性能，我们必须不断地准备试验。即使对源代码的小改动也必须持怀疑态度去审视，直到能证明它们至少对性能没有负面的影响。

在模拟程序的瓶颈处要求最高性能，这使JPC成为一个令人愿意去做和愿意与其合作的有趣项目。

这一章的其余部分集中在我们如何达到我们坚信的可维护和合理设计，且不牺牲系统的性能。

9.4　Java性能技巧

优化的第一准则：不要优化。

优化的第二准则（仅限于专家）：还是不要优化。

——Michael A. Jackson

和所有的性能技巧一样，下面的技巧是指导方针，不是准则。良好设计和清晰编码的代码几乎总是优于“优化的”代码。只有看到对设计有积极的影响或对性能真的有必要时再参考这些指导方针。

技巧#1：创建对象不好

过多的对象实例（尤其是短期的对象）会导致不好的性能。这是因为大量对象引起频繁的年轻代（young generation）垃圾收集，而年轻代垃圾收集算法几乎是“全部停止”类型的。

技巧#2：静态好

如果一个方法可以定义成静态的，那就定义成静态的。静态方法不是虚的，所以，不会动态分派。与实例方法相比，高级的VM更容易也更愿意内联静态的方法。

技巧#3：表切换好，查找切换不好

标签集适度紧凑的switch语句比标签集的值更分散的switch语句要快。这是因为Java对switch有两种字节码：tableswitch和lookupswitch。表切换是利用一个间接调用执行的，用switch的值把偏移量提供到一个函数表中。查找切换要慢得多，因为它们按对应关系进行查找以发现一个匹配的值：函数对。

技巧#4：方法越小越好

小块的代码好，因为即时的环境通常按一个方法的粒度去理解代码。包含一个“热”区域的大方法将会整个编译。由此产生的大块本机代码会引起更多的缓存代码未能利用，这会影响性能。

技巧#5：Exception用于异常的情况

Exception应该用于异常条件，不要用于错误。把异常用作为非正常环境中的流控制会提示VM优化正常的路径，从而带来更好的性能。

技巧#6：小心使用装饰（decorator）模式

从设计的角度来看，装饰模式是好的，但是，它的额外间接性开销很大。请记住，移除和增加装饰模式都是允许的。这种移除会被认为是一次“异常事件”并可以通过抛出一个专门的异常来实现。

技巧#7：对类进行instanceof的操作更快

对一个类进行instanceof的操作比用接口来操作它快得多。Java的单继承模式意味着对于一个类，instanceof仅仅是一次减法和一次数组查找；对于一个接口，那是一次数组搜索。

技巧#8：最低程度地使用同步

把同步的块减到最小；它们会引起不必要的开销。如果可能，则考虑把它们替换为原子的或volatile引用。

技巧#9：小心外部的库

避免使用超出你的目的的外部库。如果任务简单而且重要，那就认真地考虑在内部编码实现它；定制的解决方案可能更适合这个任务，导致更好的性能和更少的外部依赖。

9.5　把4GB放入4GB：这不起作用

在开发和设计JPC时我们碰到的很多问题都是与开销有关的问题。当设法在一个运算环境内部模拟一个完全的运算环境时，妨碍你获得100%本机性能的唯一的东西就是模拟程序的开销。其中有些开销与时间有关，例如在处理器模拟程序中，而另外一些与空间相关。

由于空间的开销而导致问题的最明显的地方是在地址空间：虚拟计算机的4GB内存空间（32位地址）并不会放入实际（宿主）硬件中可用的4GB（或更小）内存空间中。即使拥有很大的宿主内存，我们也不能直接声明byte[]memory=new byte[4*1024*1024*1024]；。我们必须以某种方式缩小模拟地址空间以放入宿主上的单个进程中，理论上有许多空间可以节省！

为了节省空间，我们首先观察这4GB地址空间是否总是不会装满。典型的机器不会超过2GB的物理RAM，而且，在大多数情况下都要少很多。所以，通过观察到不是所有的虚拟内存都会被物理RAM占用，我们可以非常快地把我们的4GB压下来。

设计我们的模拟物理地址空间中的第一步就有对未来的考虑。如果我们看一下它的发展道路，我们将看到IA-32存储器管理单元的特征之一有助于指导我们的地址空间的架构。在保护模式中，这个CPU的存储器管理单元把地址空间切成不可分的4KB块（众所周知的页）。所以，显而易见要做的事是按相同的比例分割内存。

把地址空间分成4KB的块意味着地址空间不再直接存储数据。数据改为存储在原子存储器单元（描绘成Memory的各种子类）中。地址空间然后保持这些对象的引用。最终的结构和内存存取方式如图9-2所示。

注意：为了优化instanceof的查找方式，我们为Memory对象设计了继承链而没有使用接口。

图　9-2：物理地址空间块结构

这个结构有220个块，每个块都需要一个32位的引用来持有它。如果我们把这些引用放在一个数组中（最显而易见的选择），我们将要花掉4MB的内存，这在大多数情况下没有很大的影响。

技巧#7对类进行instanceof的操作更快

对一个类进行instanceof的操作比用接口来操作它快得多。Java的单继承模式意味着对于一个类，instanceof仅仅是一次减法和一次数组查找；对于一个接口，那是一次数组搜索。

在这点开销也成问题的地方，我们可以进行进一步优化。请注意，物理地址空间中的存储器分为三个不同的种类：

随机存取存储器（RAM）

物理的随机存取存储器从零地址向上映射。它被频繁地存取且等待时间很短。

只读存储器（ROM）

只读存储器芯片可以存在于任何地址。它们很少读取且等待时间很短。

I/O

I/O映射的存储器可以存在于任何地址。它被相当频繁地存取，但等待时间通常比随机存取存储器更长。

为了对那些落入实际机器的RAM中的信息进行寻址，我们使用一阶段查找。这确保对RAM存取的等待时间尽可能地低。为了存取其他地址（ROM芯片和I/O映射存储器占据的地址），我们使用两阶段查找，如图9-3所示。

图　9-3：两阶段查找的物理地址空间

现在，从RAM“get”一段内存有3个阶段：

return addressSpace.get（address）；

return blocks[i＞＞＞12].get（address＆0xfff）；

return memory[address]；

从更高地址“get”有4个阶段：

return addressSpace.get（address）；

return blocks[i＞＞＞22][（i＞＞＞12）＆0x3ff].get（address＆0xfff）；

return memory[address]；

这个两层优化节省了内存，同时避免在每次RAM存储器存取时形成瓶颈。在一个存储器“get”中的每次调用和间接放置都执行一个函数。确实应该使用的间接方式—不是为了交互，而是为了在性能和复杂度之间达到最好的平衡。

JPC在有可能用不到存储的任何地方还使用了延迟初始化。因此，一个新的JPC实例拥有一个映射到不占据空间的Memory对象的物理地址空间。当RAM的一个4KB区域第一次读或写时，这个对象将完全初始化，如例9-1所示。

例9-1：延迟初始化

public byte getByte（int offset）

{

try{

return buffer[offset]；

}catch（NullPointerException e）{

buffer=new byte[size]；

return buffer[offset]；

}

}

9.6　保护模式的危险

保护模式的到来带来了一个完整的存储器管理系统，同时在物理地址空间之上又增加了一个复杂的层。在保护模式中，内存分页可以是激活的，这容许物理地址空间的4KB块进行重新排列。这种重新排列由保存在存储器中的、可以由运行在这机器上的代码动态修改的一系列表进行控制。图9-4演示了一个完整的分页转换所经过的路线。

大体上，机器上的每次内存存取都需要这样一个完整的查找次序：通过内存分页结构找到特定线性地址映射到的物理地址。由于这个过程太复杂且开销很大，实际的机器会把这些查找的结果缓存在转换后备缓冲（TLB）中。除了这些增加的间接层之外，内存分页还有额外的保护特征。每个映射的分页都可以给定一个用户或管理员和读或读/写的状态。没有足够的权限而试图存取分页的代码，或设法存取不存在分页的代码会引起一个在某种意义上类似于一个软件中断的处理器异常。

为了优化这样的结构，我们的首要战略应该是采用实际处理器的途径，这非常清楚；换句话说，我们必须拥有某种形式的查找缓存。为了设计它，我们做出了两个关键的选择：

图　9-4：IA32架构的分页机制（仅仅4KB页）

·分页的重新映射以4KB的粒度进行。不是巧合，而是为了方便，这也是我们为物理地址空间选的粒度。

·当一个保护模式进程存取内存时，它只看这些4KB块的重新映射图（虽然其中的一些块引起处理器异常）。物理地址空间仅仅是最初的Memory对象的一种可能的顺序（在那里，所有的对象都同时按地址顺序排列），与其他的任何顺序相比，没有特别的意义。

根据这两个选择，我们明白缓存（例如，TLB）的最自然的形式是复制物理地址空间结构。内存分页依照lookup分页表确定的新的顺序进行映射。第一次请求一个给定表内的一个地址时，会完整遍历这个表的结构以找到匹配的物理地址空间内存对象。然后，对这个对象的一个引用会放入线性地址空间内的正确位置，从那时起它就一直缓存着，直到我们选择清除它。

为了解决读/写和用户/管理员的问题，我们做了一个战术性的决定，为了速度而牺牲一些内存开销。我们为每个组合（读-用户、读-管理员、写-用户和写-管理员）制造一个线性地址空间。内存“get”使用读标记，“set”使用写标记。因此，在用户模式和管理员模式之间的转换只需要改变内存系统中的两个引用，如图9-5所示。

图　9-5：线性地址空间中的保护级别切换

注意：这非常适合Linux内核的分页方法。在Linux中，每个用户模式进程都映射到它的线性地址空间的内核分页。在硬件中，这防止了日常维护操作或系统调用在转移到内核代码时的上下文切换。我们到内核空间的切换仅仅是数组引用的翻转，这也是一个低开销的进程。

这还给我们留下页错误和保护系统侵犯的问题。我们处理这些情形的态度在某些方面就是我们判断是否是异常的指示，既在低级别的处理器方面也在Java语言中。这个态度可以总结如下：

Exception是异常，Error是错误。

详细地说：Exception应该代表极少或例外的情形，但不一定是毁灭性的；Error应该代表毁灭性的或相当严重的不期望的情形。

就Java语言中异常处理实现的程度而言，许多程序员不愿意使用对他们有利的异常处理机制，这有些奇怪。对软件来说，异常代表了处理罕见但也可以纠正的情形的最有效和简捷的方式。抛出一个异常会由于有效地优化常规路线而使执行开销有偏向，同时会增加处理不可避免的异常情形的开销。这样说来，页错误和保护系统侵犯是一个异常的但应该映射到一个Exception实例的正常事情，这是显而易见的。

技巧#5：Exception用于异常的情况

Exception应该用于异常条件，不要用于错误。把异常用作为非正常环境中的流控制会提示VM优化正常的路径，给你带来更好的性能。

在处理页错误和异常时，对异常的使用，我们有两个比较常用的惯例：

·ProcessorException，是代表Intel处理器所有异常类型的一个类，继承了RuntimeException。

·页错误，轻度的保护系统侵犯，是异常的情形，但为了控制牺牲性能，它们非常普通，我们使用静态的异常实例抛出它们。

这两个决定都有很好的理由，但很大一部分与JPC项目的特性无关。

决定选择继承RuntimeException主要是考虑代码的美观。一般而言，运行时异常用于不能捕获或不应该处理的异常。大部分材料推荐说如果抛出这类异常，它们允许传播并引起抛异常的线程终止。我们知道，在运行时异常和检查的异常之间的区别是类似于泛型、自动封装或for-each循环的代码“糖果”。倒不是我们想要诋毁这些结构，而是发现异常的分类对编译后的代码没有影响，我们可以安全地选择最方便的类型而不会引起任何性能瓶颈。在我们的项目中，ProcessorException是一个运行时异常以避免必须在很多方法中声明throws ProcessorException。

为页错误和系统保护侵犯抛出静态已初始化的异常实例（高效单例异常），这在异常处理链的两端都产生好处。首先，我们自己节省了在每次抛出异常时初始化一个新对象的开销。这是避免了重新构建调用线程的栈跟踪的开销（完全不像在现在的JVM中那么微不足道）。其次，我们自己节省了判断thrown类型的开销，由于有了有限的一组静态异常来判断thrown类型，我们只需进行一系列引用比较。

9.7　从事一项毫无成功希望的斗争

IA-32架构中没有什么比指令集更流行。经过这么多年，曾经在8080时代基于累加器的简单架构已经成为一个非常巨大和复杂的指令组。IA-32已经成为一个拥有无数固定扩展和许多寻址模式的类似于RISC的芯片。

当作为Java开发人员看到这样的前景展望时，回复到打字和开始编写类，这非常诱人，就好像编的结构越多，问题就越简单。如果我们正在开发一个分拆器，那这种方式即使不完美，也会很好。在这种创建非常多的对象的系统中，不可避免也会有许多的垃圾收集。这导致双重的速度瓶颈。我们不仅承受大量的对象分配的开销，还承受频繁的垃圾收集。在一个现代的按代区分的垃圾收集的环境中（Sun JVM就是这样的），小的、短生命周期的对象都创建于初生代，而几乎所有的初生代收集算法都是阻塞一切的。所以，一个有大量对象紊流的译码器不仅承受不必要的对象分配的可怜性能，还承受收集器清除所有对象紊流时很多非常短暂的GC暂停。

因为这个原因，降低推动解析执行的译码器内的对象紊流显得非常重要。在真实模式的译码器中，这种最低限度的方式导致一个6500行代码的类只有42个“new”关键字实例：·在类载入时间有4个new Boolean[]（static final）。

·用于一个旋转缓冲的3个new Operation（）。

·用于Operation中扩展缓冲的2个new int[]。

·用于异常情形的33个new IllegalStateException（）。

一旦创建这个译码器的一个实例，唯一必须构建的对象是用于Operation中int[]缓冲的扩展。一旦这些缓冲扩展完成，就不再有对象构建和垃圾收集，因此，那是较少暂停的译码。

译码器的设计阐述了我们考虑的编程（在这个情形中，是Java的）重要原则之一：仅仅因为你可以这样做，并不意味着你应该这样做。

在这个情形中，仅仅因为JVM可以进行自动垃圾收集，并不意味着将迫使你去运用它。在代码的关键性能部分，慎重地使用对象实例。小心处理像Iterator、String和varargs调用这样的类的不活跃对象实例。

技巧#1：创建对象不好

过多的对象实例（尤其是短期的对象）会导致不好的性能。这是因为大量对象引起频繁的年轻代（young generation）垃圾收集，而初生代垃圾收集算法几乎是“全部停止”类型的。

微码：少即是多或多即是少

我们已经有了一个用于解析IA-32指令流的GC较少的译码器，但是，我们还没有讨论这样的一个译码器应该如何译码。IA-32架构不是一个固定长度的指令系统；指令长度的范围从单个字节到最多15个字节。指令集的许多复杂性源自于可以用于一个指令的任意特定操作数的内存-寻址模式过多。

初始最高级别的分解把每个运算分为4个阶段：

输入操作数

从寄存器或内存中载入运算的数据。

运算

对输入操作数进行数据处理。

输出操作数

把运算的结果输出到寄存器或内存。

标记运算

调整标记寄存器的位来表示运算的结果。

对操作数和运算的这种分解使我们能够把运算的简单性和它的操作数的复杂性分离开。像add eax，[es：ecx*4+ebx+8]这样的一个运算最初分解为5个运算：

load eax

load[es：ecx*4+ebx+8]

add

store eax

updateflags

立即就可以清楚地知道Load[es：ecx*4+ebx+8]完全不是一个简单的运算，它可以容易地分解到很多更小的元素。实际上，仅对于这个寻址格式就有：

·6个可能的内存段

·8个可能的索引寄存器

·8个可能的基址寄存器

仅对于这个寻址模式就产生了384种可能的组合。显然，对这些地址计算还需要更多的分解。因此，把这种类型的内存存取进一步分解，直到我们得到：

load eax

memoryreset

load segment es

inc address ecx*4

inc address ebx

inc address imm 8

load[segment：address]

add

store eax

updateflags

为了生成可执行的模拟指令集，我们必须平衡这两方面的优先级：

·首先，我们必须平衡译码时间和执行时间以优化整体的执行速度。我们必须记住，在解析处理器中，我们主要的目标是起始执行的响应时间短。常规执行的代码应该在随后的优化阶段处理。在这里，我们最初的目标是使代码编译出来而不要阻塞整个模拟器。随后的优化可以异步地进行，这里花的时间延迟了一切。所以，我们一直在寻找一个可以快速译码的相对简单的指令集。

·其次，我们必须平衡指令集的大小和“编译后”代码的长度。小的指令集自然会产生冗长的代码，而较大的指令集应该会使代码更紧凑。我们说“应该”是因为如果选择不好，大的指令集仍然需要较长的区域。一个较小指令集的解析器在代码和记录日志上会更小，因此，执行它的每个运算的速度会快很多，但是，相对地，它有更多的指令集要执行。所以，我们一直在指令集大小和代码长度之间寻找一个合理的平衡以使解析器达到接近最优的性能。

在寻找这两个方面的最佳平衡点时，牢记Hoare的格言[1]非常重要：

过早的优化是所有不幸的根源。

——C.A.R.Hoare

这些优化的精确平衡是由系统决定的。在一个Java环境中，系统不仅包括物理硬件，还包括JVM。加上在这个环境中的Java组件总是即时编译这个因素，小范围的性能基准众所周知是不可靠的。说到底，我们不要过度使用这样的基准来指导编码选择，只有当性能出现大的改变时依赖基本原理和信任的基准。在一个即时编译的环境中，最好的情况下，在这些微小基准中的小改变在一个单独的系统中不可重复。在最坏的情况下，它们如此依赖基准场景以致在同一个系统上都不一定可重复。

注意：微代码集的一个重要特征是为它设置的整数值常量是连续的。这个解析器的核心是对这个常量集的一个switch语句，我们必须确保这个switch语句运行得尽可能快。

牢记了这些因素，在几次试验后，我们推断对于全部整数或浮点的模拟程序而言，750行左右的代码集表现出很好的平衡效果。这给我们从x86运算到微代码一个近似10的转换因数。虽然这个集合似乎有点大，但是，它解析的速度很快，而且运算也相当小。这使得它们可以成为供给随后的优化阶段的很好的候选。

技巧#3：表切换好，查找切换不好

标签集适度紧凑的switch语句比标签集的值更分散的switch语句要快。这是因为Java对switch有两个字节码：tableswitch和lookupswitch。表切换是利用一个间接调用执行的，switch的值把偏移量提供到一个函数表中。查找切换要慢得多，因为它们按对应关系进行查找以发现一个匹配的值：函数对。

[1]http://en.wikiquote.org/wiki/C._A._R._Hoare.

9.8　劫持JVM

“Java很慢”的说法至今仍然困扰着Java开发人员。很多这样的评论来自于非Java开发人员对20世纪90年代中后期的JVM的体验。自那以后，使用Java的人们知道它已经获得了大幅的改进。在这改进背后的驱动力也是加速JPC的关键：常规Java进程的环境可以非常简单地划分为程序区域和数据区域。

在图9-6中，我们可以看到数据区域可以进一步划分为静态数据（在编译期间就可以知道）和动态数据（在编译期间不能知道）。Java环境中大多数的静态数据是从类路径载入的类字节。虽然这些类字节作为数据载入，但它们实际上代表的是代码，这非常清楚，它们会在运行时由JVM进行解析。所以，很明显我们愿意把这些类字节移动到这张图的另一边。

图　9-6：一个Java进程中的程序区域和数据区域

在一个即时编译的环境中，例如Sun HotSpot，常用的字节码区域分解析或动态编译到宿主的本地指令集。这样就把类字节从数据区域移入了代码区域。然后，这些类就像本地代码一样执行，并把程序加速到本地的速度（参见图9-7）。

图　9-7：Java环境中的即时编译

在JPC中，我们利用这个事实：在JVM启动的时候不必知道所有的类数据。实际上，用Java的说法，“静态数据”作为“最终数据”来引用比较好。当一个类加载时，它的类字节将固定并不能修改（为了方便，让我们忽略JVM TI[1]）。这允许我们在运行时定义新的类，那些使用插件架构、Applet或J2EE Web容器的人会立即熟悉这个概念。

接下来我们重复一下JVM执行的即时编译技巧，但仅仅在JPC的级别。对于JPC，在Java运行时环境的范围内我们有两层的程序-数据信息划分。现在，我们的编译有两个阶段：

1.在JPC内，IA-32机器代码在需要时会编译成字节码。这些x86块由此成为了可以由JVM加载的有效的Java类文件。

2.类由JVM编译成本地代码。由于JVM并不区分由手写代码组成的原始的“静态”类和自动构建的动态类，两种类型都尽可能地获得最佳的本地性能。

在这两个编译阶段后，我们最初的IA-32机器代码已经翻译成宿主的架构（参见图9-8）。幸运的是，新指令的数量与最初的数量相比没有明显增多，这意味着性能不会比本机慢很多。

图　9-8：JPC架构中的三个编译器

目前我已经知道如何使我们的模拟程序速度更快，但是，我们稍微掩盖了一些详细信息。这些详细信息（我们的新问题）现在落入两个不同的区域：我们如何进行这样的编译，以及我们应该如何载入最终的类？

9.8.1　编译：如何多此一举

我们这里讲述的编译器不是最理想的例子，但是，我们所处的情况也略有些不平常。javac和JPC编译器都仅是第一阶段的编译器，这意味着它们仅仅把它们的输出提供给第二阶段，一个字节码解析程序或一个即时编译器。我们知道javac几乎不优化它的输出；javac生成的字节码只是输入的Java代码的一个翻译（参见例9-2）。

例9-2：Javac的未优化的编译

public int function（）public int function（）；

{0：iconst_1

boolean a=true；1：istore_1

if（a）2：iload_1

return 1；3：ifeq 8

else 6：iconst_1

return 0；7：ireturn

}8：iconst_0

9：ireturn

很容易证明进行最小限度的优化是恰当的，因为大部分的优化工作都留给编译的后续阶段来进行。在JPC中，我们不仅有理由，也有额外的时间压力：

·我们想要使编译的开销最小化以避免占用其他模拟程序线程的CPU时间。

·我们想要使编译的延迟时间最短以便解析的类可以尽快地替换。一个高延迟的编译器将发现当编译完成一个类时它的代码已经不再需要了。

简单的代码生成

现在，JPC中的编译任务就是把单个解析的基础块的微代码转换到一组Java字节码这么简单的一件事。我们最有可能会假设这个基础块不能抛出异常。这意味着这样的基础块是一个严格定义的基础块，只有一个入口和一个出口。现在，由一个特定基础块改进的每个变量都可以由包含一组输入寄存器和内存状态的一个函数来表示。在JPC内部，我们共同把这些函数表示成一个有向无环图。

图的源点（source）

源点表示以寄存器值的形式或直接以指令的形式的输入数据。

图的汇点（sink）

汇点表示以寄存器值和内存的形式的输出数据或写I/O端口的输出数据。对于受到块影响的每个状态变量，都有一个汇点。

图的边界（edge）

边界表示变量值在图的内部传播。

图的节点（node）

节点表示对输入边界的运算，其结果会沿着输出边界传播。在JPC中，这些运算是对已解析微代码的单态修正的组成部分。因此，如果微代码影响多个变量，它就会映射到多个图节点。

把已解析基础块的这种图形表示法转换到Java字节码，只是图形上每个汇点依次简单的最浅层的移动。图中的每个节点把栈中最顶端的元素作为输入，然后把它的结果放在最顶端，准备由任意子节点进行处理（参见图9-9）。

注意：图的经典的最佳移动顺序需要每个节点上的优先权都应该按深度的顺序进行计算。距离最远源的路线最长的节点应该最先计算，路线最短的节点最后计算。在一个基于寄存器的目标上，这会生成最短的代码，因为它消除了大多数的寄存器修改。在一个基于栈的机器上，例如JVM，同样的决策过程会引起编码的栈深度最小。在JPC中，我们忽略这些细节并依靠JVM消除其中的差异，仅追踪所有节点的深度这个额外的复杂情况就不值得这么做。

图　9-9：把x86运算表示成一个有向无环图

这种汇点接着汇点、最浅层分析的方式引起图的自然优化，如图9-10所示。对应于无用代码的图的孤立部分不能通过汇点访问，所以会自动删除，因为解析永远都不会达到那儿。在图的解析中，重用的代码区域会发现自身计算了多次。我们可以把它们的结果缓存到一个本地变量中，在对这个节点进行后续访问时载入这个结果，由此避免了所有的代码重复。

与一棵树中的节点相关联的代码表示成由源编译成的部分JPC代码的单个静态函数。那样，我们生成的代码只是对处理器变量的一系列推动并直接压入栈，紧接着是对每个节点的一系列静态调用，最后是取出成一系列的处理器对象。

技巧#2：静态好

如果一个方法可以定义成静态的，那就定义成静态的。静态方法不是虚的，所以，不会动态分派。与实例方法相比，高级的VM更容易也更愿意内联静态的方法。

图　9-10：有向无环图的特性

处理异常

既然我们可以处理基础块的编译，那么，我们必须把一些异常情况恢复正常。正如我们先前讨论的那样，异常并不总是错误。页错误和保护系统侵犯很自然地任意抛出。当异常抛出时，处理器的状态必须和最后成功执行的那个操作一致。这对于编译器来说显然有相当重要的含义。把IA-32中的异常映射到Java异常，我们知道这个问题的唯一可用的解决办法是在异常处理器中捕获这个异常，然后确保处理器的状态与最后成功的操作一致。

任意特定基础块内的异常行为路线就像基础块本身的路线。它有一个入口和一个出口，和主路线唯一的区别是出口。因为异常的路线和基础块本身的路线的区别不是很大，描绘它的最自然的方式是在它自己的有向非循环图内。异常路线的图将和基础块共享相同的一组节点，但是，它另外拥有一组映射到它自己不同的出口的汇点。

然后，异常处理器的代码通过遍历选定的路线图而生成了。当异常抛出时，与主路线共享的节点重新设置回主路线遍历它们时的原来状态。这意味着从主路线来的所有缓存和计算值都可以在异常处理器中重用，因此避免了重复任何工作。

字节码处理

已经有许多已建立和设计良好的解决方案来把我们已编译的字节码转变成可载入的类。Apache的字节码工程库（Byte Code Engineering Library,BCEL）自身定位于便利地“分析、创建和处理（二进制的）Java类文件”。[2]ASM是一个“完全致力于Java字节码处理和分析的框架”。[3]

遗憾的是，我们要做的只是修改单个框架类中的单个方法（总是修改同一个方法）。我们只能生成一小部分可能的字节码序列，不必提供任何分析工具。这样看来，BCEL和ASM对我们的要求来说有些重量级了。我们改为开发一个只有非常有限能力的完全符合我们需要的定制的字节码处理库。例如，我们的栈深度算法调整为快速确定方法的最大栈深度（以便它们可以通过验证）。虽然这个算法不适用于常规的类编译，但是，对于我们的用途来说，它足够了，而且更有效。

技巧#9：小心外部的库

避免使用超出你的目的的外部库。如果任务简单而且重要，那就认真地考虑在内部编码实现它；定制的解决方案可能更适合这个任务，导致更好的性能和更少的外部依赖。

[1]对于那些喜欢摆弄JVM顽皮代码的人来说，位于http://java.sun.com/javase/6/docs/technotes/guides/jvmti/的Tool Interface可以用来做许多非常有趣的事，包括重新定义类文件。

[2]http://jakarta.apache.org/bcel.

[3]http://asm.objectweb.org/.

9.8.2　大规模的类加载和卸载

既然有了类，那么，它们必须进行加载。第一个显而易见的问题是“有多少？”图9-11给了我们关于这个问题的一个说明。可以证实把100000个类加载到单个JVM中只是一个微不足道的挑战。

图　9-11：一个现代的GNU/Linux启动期间的类的数量

因此，我们必须为所有的这些类寻找空间。类文件存储在JVM中一个位于正常对象堆（即众所周知的“永久区域（Permanent Generation）”）之外的特定的内存区域。在典型的Sun JVM中，永久区域初始有16MB，最多可以增加到64MB。很显然，100000个类不能全部载入64MB的堆。我们有两个方法可以解决这个问题。

第一个办法是用命令java-XX：MaxPermSize=128m。虽然这个办法比较笨拙，但它确实有助于解决这个问题。遗憾的是，这不是根本的解决办法，因为我们所做的只是延缓问题的发生。最终，我们会加载足够的类来填满新的空间，我们不能一直增加空间。后一种解决办法涉及减少加载类的数量。实际上，垃圾收集器不应该清除所有不用的类吗？对于垃圾收集器来说，类和堆的对象没有实质性的区别。只要一个类没有活动的引用指向它就可以垃圾收集掉（卸载）。当然，类的每个实例都对它这个类型的Class对象保持了一个强引用。所以，对于一个要收集的类，首先必须不存在这个类的活动实例；其次，没有这个类的其他的引用。因此，如果我们定义如例9-3所示的定制类加载器，那或许就可以解决这个问题。

例9-3：简单的不保持引用的ClassLoader

public class CustomClassLoader extends Classloader

{

public Class createClass（String name,byte[]classBytes）

{

return defineClass（name,classBytes，0，classBytes.length）；

}

@Override

protected Class findClass（String name）throws ClassNotFoundException

{

throw new ClassNotFoundException（name）；

}

}

例9-3中的ClassLoader不保持它定义的类的引用。每个类都是没有联系的单例，所以，findClass可以安全地抛出ClassNotFoundException而其他都保持正常。一旦某个类的单例成为GC的候选，那这个单例和这个类本身都可以收集掉。这运行得很好，所有的类都加载了。然而，某个地方出错了。由于未知的原因，永远没有类卸载掉。似乎在某处有一个对象对我们的类一直保持着引用。

让我们看一下定义一个新类的调用清单：

java.lang.ClassLoader：defineClass（……）

java.lang.ClassLoader：defineClass1（……）

ClassLoader.c：Java_java_lang_ClassLoader_defineClass1（……）

vm/prims/jvm.cpp：JVM_DefineClassWithSource（……）

vm/prims/jvm.cpp：jvm_define_class_common（……）

vm/memory/systemDictionary.cpp：SystemDictionary：resolve_from_stream（……）

vm/memory/systemDictionary.cpp：SystemDictionary：define_instance_class（……）

当你不满足于“因为事实就是这样”的回答时，就会发生这种情况。我们深入到JVM的内部，找到了这一小段代码：

//Register class just loaded with class loader（placed in Vector）

//Note we do this before updating the dictionary,as this can

//fail with an OutOfMemoryError（if it does,we will*not*put this

//class in the dictionary and will not update the class hierarchy）.

if（k-＞class_loader（）！=NULL）{

methodHandle m（THREAD,Universe：loader_addClass_method（））；

JavaValue result（T_VOID）；

JavaCallArguments args（class_loader_h）；

args.push_oop（Handle（THREAD,k-＞java_mirror（）））；

JavaCalls：call（＆result,m，＆args,CHECK）；

}

这是Java级的对正在处理加载的classloader实例的一个回调：

//The classes loaded by this class loader.The only purpose of this table

//is to keep the classes from being GC'ed until the loader is GC'ed.

private Vector classes=new Vector（）；

//Invoked by the VM to record every loaded class with this loader.

void addClass（Class c）{

classes.addElement（c）；

}

现在我们知道是这个淘气的小对象一直保持着对所有类的引用，从而阻止了对它们的垃圾收集。遗憾的是，对于这一点，我们无能为力。好吧，在不违背基本原理的情况下我们可以做的就是在java.lang包中声明一个类。我们现在知道我们的超类将一直有用地为我们保持着引用。这对于类卸载来说意味着什么呢？

图　9-12：类的GC根路径

在图9-12中的GC根路径中，我们可以看到，在由一个classloader加载的所有类的所有实例都成为GC候选之前，所有的类都会保持加载。所以，一个活动的代码块可以阻止n个类卸载。这一点都不好。

有一个简单的办法可以缓解这个问题。没有人说我们必须让n无限变大。如果我们限制任意加载器加载类的数量，那么，我们就可以减少类相互制约的机会。在JPC中，我们有一个默认只加载10个类的定制类加载器。加载第十个类会触发构建一个新加载器，它会用于下10个类，等等。这个方法意味着任意一个类都只能制约其他10个类（参见例9-4）。

例9-4：JPC中的ClassLoader实现

private static void newClassLoader（）

{

currentClassLoader=new CustomClassLoader（）；

}

private static class CustomClassLoader extends ClassLoader

{

private int classesCount；

public CustomClassLoader（）

{

super（CustomClassLoader.class.getClassLoader（））；

}

public Class createClass（String name,byte[]b）

{

if（++classesCount==CLASSES_PER_LOADER）

newClassLoader（）；

return defineClass（name,b，0，b.length）；

}

protected Class findClass（String name）throws ClassNotFoundException

{

throw new ClassNotFoundException（name）；

}

}

HotSpot代码缓存

在JPC中，当运行加载大量类的复杂工作时，会遇到另一个内存限制。在Sun HotSpot JVM中，即时编译的代码保存在一个名为代码缓存的非堆（nonheap）区域。不仅JPC会生成许多类，其中的一些类是为HotSpot准备的，这也很不寻常。这意味着HotSpot的缓存会很快填满。这正好意味着增加永久保存区的大小也要增加代码缓存的大小。

CodeBlock替换

现在我们拥有一个已编译、已加载和已实例化的定制代码块实例。无论如何我们必须把这个块放置到需要的地方。如何做到这一点和如何执行这些块的最初安排也紧密相关（参见例9-5）。

例9-5：编译安排的装饰模式

public class CodeBlockDecorator implements CodeBlock

{

private CodeBlock target；

public int execute（）

{

makeSchedulingDecision（）；

target.execute（）；

}

public void replaceTarget（CodeBlock replacement）

{

target=replacement；

}

}

例9-5演示了装饰代码块如何截取执行调用，然后可以对是否排队进行编译进行决策。除此之外，我们还有一个方法可以用一个不同的块实例来替换这个装饰的目标。一旦这个块编译完成，这个装饰就没用了，所以，理论上我们愿意替换它。这个替换实际上可以非常容易地实现。通过用如例9-6所示的块代替最初的解析块，我们可以传播新的块到了调用栈的通知。一旦这个异常达到了合适的标准，我们可以把对这个装饰的引用直接替换为对已编译块的直接引用。

例9-6：Block替换CodeBlock

public class CodeBlockReplacer implements CodeBlock

{

private CodeBlock target；

public int execute（）

{

throw new CodeBlockReplacementException（target）；

}

static class CodeBlockReplacementException extends RuntimeException

{

private CodeBlock replacement；

public CodeBlockReplacementException（CodeBlock compiled）

{

replacement=compiled；

}

public CodeBlock getReplacementBlock（）

{

return replacement；

}

}

}

技巧#6：小心使用装饰（decorator）模式

从设计的角度来看，装饰模式是好的，但是，它的额外间接性开销很大。请记住，移除和增加装饰模式都是允许的。这种移除会被认为是一次“异常事件”并可以通过抛出一个专门的异常来实现。

9.9　终极灵活性

使用了这些技巧，现在我们有了一个不必进行重要的架构调整就可以改进和扩展的高度优化的模拟系统。一个更好的编译器可以插入这个系统的后端，可以使用其他的组件和替换不同的实现来适应大范围的目标。例如：

·组成虚拟硬盘的数据实际上可以由世界上任意地方的服务器提供（只要需要）。

·模拟系统的用户交互（虚拟屏幕、键盘和鼠标）可以通过一个远程系统来进行。

·JPC可以在任意标准的Java 2虚拟机上运行x86软件，因此，底层的硬件选择与操作系统和软件的选择无关。另外，虚拟机的全部状态都可以保存，模拟的机器也可以及时“冻结”。可以在随后的某天在一台不同的物理机器上恢复，虚拟机上的任何宿主软件都不会感到任何的改变。

9.9.1　灵活的数据

有了JPC，可以通过一根存储棒来携带你的磁盘镜像，连同一份完整的JVM和JPC代码。然后你可以把它插到任意的计算机上并“启动”你的机器来处理所有私人邮件和其他工作，当你完成工作并拔出存储棒时，在宿主上不会留下任何东西。

另外，你的硬盘镜像也可以放在因特网上的一台服务器上，只要载入一个本地JPC并把它指向你的服务器，你就可以从世界的任何地方访问你自己的机器。加上适当的认证和传输安全，这成为移动办公的一个强大的工具。JPC对Java空间的自然适应性意味着几乎任何设备都可以用作为远程访问的终端入口，从嵌入Java的浏览器到移动设备。

对于高安全环境中的敏感工作，通过在一个其硬件驱动位于安全服务器上的本地JPC实例上工作，可以在最基础的硬件级别保证数据的安全性和完整性。每个工作人员都可以完全控制他们正在使用的虚拟硬件，从而使他们能够高效地工作。然而，他们没有办法从系统中抽取数据，即使他们设法破坏掉他们前面的计算机的物理安全：本地机器实际上完全不知道运行在JVM内、JPC内、访客操作系统内等的应用程序。

即使当雇员的信任不是一个问题时，在虚拟机（尤其像JPC那样灵活的虚拟机）上工作意味着物理硬件完全可以随时替换。因此，对于灾后恢复和最终备份来说，机器的全部状态都备份了起来，而不仅仅是硬盘数据，像JPC这样的模拟程序在即时故障转移很重要的地方具有很明显的优势（即使通过广域网）。

因为JPC的硬件不可知性，这些情形也可以完全适用于非x86的硬件，对于瘦客户端来说这是完美的解决方案，而不论什么情况，用户都可以得到他们喜欢的x86环境。

9.9.2　灵活的审计和支持

通过适当的授权系统，可以远程检查和接管一个运转的JPC实例的屏幕、键盘和鼠标。通过远程收集可疑活动的键盘输入和屏幕截图以作为证据，可以有效地监控欺诈行为。JPC使低级别的硬件访问成为可能，而那些试图从模拟操作系统中探测并移除监控软件的技术能人不可能破坏这个特性。即使赋予了（访客系统上）管理员权限的用户也不能够逃避监控，无论他多有见识。

一个审计的JPC系统可以只是记录活动、即时扫描并标记动作，或更进一步防止某些行为。例如，与适当的服务器软件合作，一个监控的实例可以扫描不恰当图像的视频输出并在虚拟显卡级别模糊（或用其他内容替换）它们。这样低级别的监控意味着用户不能通过安装其他的查看软件来破坏内容保护系统。

如果一个帮助桌面可以逐字逐句地完全看见整个屏幕在做什么并直接在虚拟硬件级别用键盘和鼠标进行交互，那么，远程辅助可以变得高效得多。即使当JPC在运行那些从来都没有实现过远程访问的操作系统（例如DOS，它的使用遍及世界的许多行业和国家）时，这都有可能。

9.9.3　灵活的计算

比依赖本地资源或必要时仅仅从远程资源中导入数据来运行主模拟程序更好，核心的模拟程序可以在一台集中的“JPC”服务器上执行。因为JPC只需要一个标准的JVM就能运行，这台集中的JPC服务器可以基于完全不同于常规的x86 PC的硬件。已经有一些候选可以做到这一点，JPC已经可以在一个96核心的Azul计算服务器（Azul compute appliance）上演示了。Sun基于Niagara的服务器和来自于移动电话技术的系统（JPC已经在Nokia N95这个基于ARM11的系统上启动了DOS）也有可能。

但是，为什么要集中一台服务器来运行所有这些JPC实例呢？大概因为因特网上的任何资源都可以运行一个代表其他人的JPC实例，而屏幕输出和用户输入通过网络传输到虚拟机的拥有者。按这种工作方式，整个世界都可以看成是多个用户和多台机器，在前端的用户和后端的机器之间都没有固定的硬件拥有关系了。如果有一台机器空闲了，任意一个用户都可以使用它，远程启动一个JPC实例来处理他们个人的磁盘镜像数据。如果这个空闲的机器突然有了其他的用途，这个JPC实例可以“冻结”，它的状态可以移动到另一台空闲的物理资源上。

虽然后面这种情形对于交互式用户来说也许难以想象，对于他们来说，通过因特网进行冻结和恢复会花太长的时间，从而会觉得不太方便，但对于那些需要同时运行许多并行虚拟机而没有很多交互的用户来说，这就很有意义。这是目前那些使用大量机器来运行大规模并行任务（例如渲染动画电影的画面、通过分子模拟搜索药物、优化工程设计问题和给复杂的金融工具定价）的用户的体验。

9.10　终极安全性

容许未检查过的代码在你的机器上运行，这充满了危险，而且这种危险正在加剧。因特网上的恶意软件“malware”（也称为“特洛伊”、“键盘记录器”、“劫持软件”、“间谍软件”和“病毒”）的清单正在快速增长。你可能会丢失数据、身份失窃和受骗，最坏的是，如果你在运行从不知名的或未认证的源下载的软件时没有小心地检查，那就可能会牵连到犯罪事件中。

对于流行的操作系统和因特网浏览器的制造商修补的每个安全漏洞，似乎在原来修复的地方又会新增长两个漏洞。意识到这一点后，你如何才能运行那些真正增强你的浏览体验或提供有用服务的代码呢？

运行在Java Applet沙箱中的Java代码在十多年前就已经提供这种安全级别了。增加由JPC呈现的额外的独立安全层，你就拥有了两层保险的沙箱来在其中运行未检查过的代码。JPC的Web站点（http://www.jpc.physics.ox.ac.uk）演示了JPC如何能够在一个标准的Applet中作为网页的一部分来启动DOS和运行许多经典的游戏；换句话说，他们演示了一段未检查过的x86（DOS）可执行代码在任意机器上的完全安全的容器中运行。

在Applet沙箱中运行JPC有一个主要的不利方面：安全限制不允许JPC创建类加载器，因此，对JPC的速度有很大提高的动态编译禁止了。一个好消息是通过利用JPC设计的内在灵活性，可以避开这个问题而不影响安全性。

在Applet沙箱中的Java代码可以按需从网络加载类，只要它们来自和Applet代码最初所在的相同的服务器。利用这一点，我们已经构建了这样一个远程编译器：对运行在Applet中的JPC实例所需要的类进行编译，当负责运行这些JPC实例的JVM请求时把编译好的类发送回这些实例。本机JVM仅仅把这些类看作凑巧需要而不是JPC内其他类需要的静态资源，而实际上，这些类已经由这些JPC Applet实例按需编译了。

这样，即使在标准的Java Applet沙箱中，我们也获得了已编译类的速度，而用户可以放心，无论JPC正在运行什么，JVM都遵守在一个危险环境中使执行代码这个行为变得安全的基本限制。

远程编译的一个好处是随着时间的推移，许多JPC实例利用它，它构建了一个可以在JPC实例之间共享的已编译类的库。因此，编译服务器很快成为一个先前已编译的类的Web服务器。此外，通过计算每个已编译类的请求的次数，服务器知道哪些类最常使用，因此知道对哪些类花更多的时间进行优化。虽然后面这个特性还没有实现，但是，我们相信，这样集中的优化可以弥补由于在加载类时网络延迟所造成的JPC Applet客户端所遭受的执行速度瓶颈。因此，JPC Applet客户端可以像那些本地实现编译的JPC应用程序客户端一样好。[1]

[1]当然，当编译服务器和JPC Applet通过100M网络连接时，实际上不会察觉到由于网络问题造成的性能瓶颈。使编译在其他地方进行的好处是释放了本地的CPU资源，而这似乎平衡了网络的延迟。

9.11　第二次做会更好

每个人都知道一件事在第二次做的时候可能会做得更好。

——Henry Ford，《我的生活和工作》

在学术的环境中开发伴随着它自己的挑战，这些挑战和商业背景中的挑战有些微小的不同。在学术的环境中，性能目标主要是自我要求，这既有好处也有坏处。对于开发人员的这一方面需要训练以保持项目的前进路线和防止项目的中心偏移。然而，自由的环境也容许想法获得快速的发展和测试以确认或反驳它们的好处。对于非常有创造力和雄心的项目，这种氛围对于最终的成功非常关键。

JPC的架构只有一小队的开发人员就进展得非常好，[1]这是由于对编码采取迭代的态度才获得的。如图9-13所示，在这个项目的发展过程中已经编写了500000行代码。而至今，只有85000行代码留了下来。这个模拟程序的每个部分都经历了许多重写，包括一次完全清除和重写。

周而复始的净化、重写和精炼是难以达到的，虽然在学术环境中比较容易。倘若你在情绪上没有非常依恋你的代码，代码删除的过程就像是泻药，这不仅适合于代码，也适合于开发人员对于它的态度。Henry Ford是对的：你的第二次尝试几乎总是会更好。在这一系列的迭代改进达到终止的条件时，一切都会变得很好。

图　9-13：JPC中积累的代码

所以，达到“美丽架构”的途径是一个分为4个步骤的计划：

1.全面接受一个大而复杂的问题，找到一组比较简单的、能够使构建一个完善的端到端的原型系统成为可能的阶段。每个阶段代表一个比最终目标简单和具有较少功能的系统，但是，每个阶段都可以在它的设计限制内作为完整系统原型的一部分进行测试，而不是作为最终设计的小部分单独原型（例如，比较传统的单元测试）。

2.在构建每个阶段的每一部分之前，清楚地知道正在开发哪个方面和为什么。理论上，每个阶段的瓶颈都可以容易地识别，对这些瓶颈的改进将是下一阶段或下面一些阶段的主要目标。原则上，在着手大块的工作之前要努力找到办法证明这个方法是否可行—即使对于每个阶段的每一部分。

3.完成每个阶段的编码并对整个原型进行系统测试，抵制在阶段设计限制参数上快速推进的诱惑。一定要做到对每个阶段进行完整的系统测试才通知下一个阶段的测试。

4.对设计进行迭代并返回到第二步，无论在什么情况下，你都不应该担心重写全部的组件。

在几乎不存在商业压力的学术环境中，容易在第四阶段修改代码。在商业背景中，面对必要的残酷是需要勇气的，但是我们认为在这种关键时刻缺少勇气是项目失败的一个潜在的和通常没有识别出来的主要原因，尤其对于最有创新和挑战的项目。

为了获得一个具有完全料想不到的优点的美丽架构，忠于你的信念，并无所畏惧。

[1]在为期30个月的主开发阶段，团队的平均人数为2.5个程序员，并不都是Java专家。

第10章　元循环虚拟机的力量：Jikes RVM Ian Rogers Dave Grove

原则与特性

结构

功能多样性

√模块

概念完整性

依赖关系

√修改独立性

√进程

自动传播

数据访问

√可构建性

增长适应性

熵增抵抗力

在一个托管运行时环境中运行代码是当今开发人员的普遍选择。实际上，大部分已开发代码都适合于托管运行时环境。然而，虽然运行时环境日趋流行，但是，多数代码都是用异于运行时环境支持的语言编写的。在Java虚拟机（Java应用程序的运行时环境）的情况下，一般用编程语言C和C++来实现这个运行时环境本身。

在这一章中，我们概要介绍一个名为Jikes RVM的成熟虚拟机，它采用Java语言编写来运行Java运行程序。不但运行时系统是用Java编写的，而且这个架构的所有其他组件都是用Java编写的。这些组件包括自适应和优化的编译系统、线程、异常处理和垃圾收集。我们在这儿概要介绍一下这些系统，还将解释为什么用单一的语言、运行时和实现所产生的系统更具有内在的吸引力和潜在的优势。

10.1　背景

如何开发一种新的编程语言是拥有像T型图（Aho 1986）形式的计算机科学的主要组成部分。图10-1演示了这样的一个T型图：利用一个C编译器（它运行并创建PowerPC机器码）来编译一个用C语言编写的、创建PowerPC机器码的Pascal编译器，生成一个运行PowerPC机器码的编译器，并创建PowerPC机器码。

与传统的编程语言（把代码编译到程序想要在上面运行的计算机的机器码）不同，大多数现代的语言都能够编译成架构中立的机器码。在Java中，这称为Java字节码。中立的机器码容许应用程序导出到任意运行时环境。所以，Java可以运行在现在的任意一台Java虚拟机上。

图　10-1：演示了一个从Pascal到PowerPC机器码编译器的创建的T型图

现代语言希望通过解决潜在的编程语言缺陷来帮助开发人员。最普遍的特性是拥有内存安全性，通过限制开发人员如何使用数据类型和只允许自动的垃圾收集释放内存。另一个特性是抛出异常的能力。

自足执行（self-hosting）被看作是编程语言的一个重要原则。自足执行的意思是编程语言应该允许足够的表达式以使得该编程语言可以用它自身的编程语言编写。例如，用Pascal编写的Pascal编译器是自足执行的，而用C编写的Pascal编译器就不是。自足执行允许编程语言的开发人员为他们负责的内容使用该语言的特性。关键的是，自足运行形成了一个高效的循环，在这个循环中，语言实现者希望在这个语言实现的性能关键部分使用高级的和有表现力的语言特性，因此，经常发现创新的方式来有效地实现上述语言特性。虽然使编译器自足执行很重要，但是，许多运行时环境不是由它们通常运行的语言编写的。例如，一个用C或C++编写的运行时可能会运行Java应用程序。如果这个运行时存在一个与内存安全相关的缺陷，它就会破坏Java应用程序，即使这个Java应用程序本身内存安全。消除缺陷是拥有一个自足执行的运行时的重要理由。

随着计算机系统更好地理解和发展，编程语言的要求也发生了改变。例如，编程语言C和C++没有利用多处理器处理线程的标准库（虽然像POSIX线程和OpenMP这样流行的扩展确实存在）。现代语言将使这些特性设计到语言和标准库中。允许运行时利用更好的库和抽象是拥有自足执行的运行时的另一个重要理由。

最后，无论一个运行时和它运行的应用程序在什么时候相互通信，都有用来通信的一个层。这个通信层的一项工作可能是调整对象，把一种编程语言的格式改变成另一种语言的格式。对于这些对象，通信层还必须记住不要回收在这个托管运行时之外使用的任何对象。这样的通信层不是必需的，或至少在运行时自足执行的许多情形下不是必需的。

我们希望已经提供了足够的理由来制造一个自足执行的运行时。在这章中，我们概要介绍一个这样的运行时，Jikes RVM，它由Java编写而成并运行Java应用程序。自足执行运行时环境被称为元循环（Abelson等1985）。Jikes RVM不是唯一的元循环，实际上，它从类似于Lisp（McCarthy等1962）和Smalltalk的Squeak虚拟机（Ingalls等1997）这样的运行时系统中汲取了灵感。成为一个用Java编写的元循环虚拟机允许使用杰出的工具、开发环境和库。因为Java在某些团体中缺少可信性，我们将首先处理一些导致人们相信元循环Java运行时有内在缺陷的传言。

10.2　与运行时环境相关的传言

关于如何最好地创建适用于不同环境的应用程序，仍然有许多激烈的争论。在应用程序将运行的地方的可用资源、开发应用程序的开发人员的生产力的挑战和开发环境的成熟度等因素都会有影响。如果应用程序以相同的方式实现，性能和内存需求将是开发环境的一个特性。接下来，我们驳斥关于托管环境最普遍的传言。

10.2.1　因为运行时编译器必须快，所以它们必须简单

关于运行时环境的一个误解是它们对纯粹的即时（just-in-time,JIT）编译感兴趣。即时编译必须快速地创建代码，因为代码一准备好就会投入使用。虽然这个简单的执行模式用于许多早期的JVM和原型运行时环境，但是，大部分现代生产的虚拟机依赖选择性优化（selective optimization）的形式。在选择性优化中，在线剖析用于识别部分的执行方法来用一个激进优化的编译器进行优化；其他方法在执行之前立即由一个非常快速的非优化编译器进行解析或编译。正是选择性使得在运行时能够使用成熟的优化编译器。

10.2.2　静态编译器中的无限分析必然意味着更好的性能

因为运行时环境会由许多应用程序大量地使用，所以，优化它们以在所有的应用程序使用时获得更好的性能是有意义的。然而，如果运行时没有作为一个动态环境的一部分来创建，一些优化将不能实现：

在线剖析

运行时的情况在运行时会发生变化—例如，数据块的平均大小或基于不同设计模式的特殊的编码风格。在线剖析允许及时地使用这些信息来降低开销，例如分支预测，还允许更高级的优化，例如数值推测。Java的一个数值推测的例子是推测主要的流输出操作出现在java.lang.System.out文件流中。数值推测是局部估值的一个扩展，我们将在后面的“局部求值”一节（10.5.3节）进一步讨论局部估值。

底层系统中的差异

一个应用程序运行的系统差异正逐渐变得越来越大。不同处理器的能力、内存的数量、不同处理器的数量、电能要求和运行时在上面执行的系统的负载，这些对于了解运行时应该如何才能最好地适应都很重要。

过程内分析

过程内分析是一个优化编译器的重要工具，允许跨方法边界优化。虽然离线分析也没有限制，但通常会导致太多的数据以至编译器不能确定哪些数据很重要。因为运行时反馈更及时，它可以更好地指导程序内及其他编译器优化。

10.2.3　运行时分析使用许多资源

拥有一个运行时环境就存在这个环境所需要的内存开销。对于传统的应用程序所使用的标准库来说，类似的需要也同样存在。除此之外，运行时环境必须保存将来可以帮助它编译和执行的信息。这些内存需要是适度的，而且，通过及时和有效的内存取样，运行时环境可以用最小的开销获得最大的好处。

10.2.4　动态类加载抑制了性能

许多现代的运行时环境，包括Java，都有能力动态地扩展。这在用户想要把一个系统的不同部分插在一起的环境中是有用的；就Java而言，组件可以从因特网下载下来。这种技术虽然有用，但是，这意味着编译器不能确定关于一个应用程序的所有信息对于它来说是否都有用，反之则不然。例如，如果一个对象的一个方法正在调用，而且对于那个对象只有一个可能的类，避免进行动态的方法分派并直接调用这个方法会更好。在优化的运行时环境中有专门的优化来处理这种情况，我们将在10.5.3中讨论它们。

10.2.5　垃圾收集比显式内存管理更慢

自动的垃圾收集是计算机科学研究的一个高级领域。当内存不再需要时会被请求并回收。显式内存管理利用给运行时环境发送明确的命令来请求然后释放内存。虽然显式内存管理易于出错，但是，人们经常坚持这对于性能来说是必需的。然而，这忽略了由于显式内存管理所引起的许多复杂性。用了显式内存管理，就必须记录哪些内存块在使用并维护没有使用的内存块的清单。当这结合了许多线程并发地请求内存时，内存碎片的问题，以及把较小的区域合并起来可以形成较大的供分配的内存区域，显式内存管理器的工作会变得复杂。显式内存管理器还不能在内存中移动东西—例如，为了减少碎片。

内存管理器的需求是应用程序特有的，在一个元循环运行时的背景中，一个简单的即时编译器不必进行很多的内存分配，所以，显式内存管理或自动垃圾收集的方案都能很好地工作。对于一个比较成熟的优化编译器而言，除了垃圾收集减少了缺陷的可能性之外，两者的区别不是很明显。对于更复杂的运行时系统的其他部分（稍后我们将在10.5.5小节中讨论），虽然我们不能反驳垃圾收集比显式内存管理更慢这种说法，但是，它明确改进了系统的“处理能力”。

10.2.6　小结

因为开发工具本身就是应用程序，我们最初关于如何最好地开发一个应用程序的问题成为了自指（self-referential）。托管语言消除了缺点并提高了开发人员的生产率。简化开发模型，探索更多的机会来优化应用程序和运行时，以元循环的方式来做这项工作，这会允许开发人员从它们引入的特性中获益而避免遇到在应用程序、运行时和系统的编译器之间的障碍。在下面的几节，我们将介绍Jikes RVM，一个把这些原理组合在一起的运行时环境。

10.3　Jikes RVM简史

Jikes RVM起源于IBM的Jalapeño项目。Jalapeño项目开始于1997年11月份，目标是开发一个灵活的研究架构来研究高性能虚拟机设计中的想法。在1998年初，一个最初的功能原型可以使用了，它能够运行小的Java程序。在1998年的春天，关于优化编译器的工作开始了，这个项目的规模快速地增长。在2000年初，项目成员已经发表了好几篇讲述Jalapeño方面的学术论文，大学研究人员开始表示有兴趣把这个系统用作为他们自己的研究成果的基础。

到2001年10月这个系统开源的时候，已经有16个大学在IBM的许可协议下使用Jikes RVM。这个团体快速地扩张，现在已经包括了好几百个研究人员及一百多个机构。Jikes RVM已经成为发表在学术刊物中的多达188遍论文的基础，而且它已经构成了至少36篇学术论文的基础。

Jikes RVM从第2版开始开源，而且已经支持Intel和PowerPC架构。已经有一些垃圾收集算法可用，包括引用计数（reference counting）、标记清除（mark-sweep）和半空间（semi-space）。一年后，2.2版本的Jikes RVM发布了。一个主要增强是全新实现了一个名为内存管理工具包（Memory Management Toolkit,MMTK）的内存管理子系统。内存管理工具包已经成为垃圾收集研究团体中一个使用非常广泛的框架，而且已经移植到Jikes RVM之外的其他运行时。我们将在10.5.9小节进一步讨论内存管理工具包和垃圾收集技术。优化编译器和自适应优化系统也有显著的提高，运行时的开发通过转到开源的GNU Classpath标准类库而简化了。在2003年4月，Jikes RVM 2.2.1成为能够运行Eclipse IDE重要部分的最早开源的Java运行时之一。

在2.2版本和2.4.6版本之间的近四年内，在功能和性能方面都有了许多明显的改进，但是，源的结构和架构大部分都没有改变。

2008年8月Jikes RVM 3.0发布了，它体现了团队近两年改进和使这个系统现代化的共同成果。代码中采用了Java 5.0的语言特性，构建的系统转为使用Apache Ant，而且还开发了一个改良很多的测试基础设施来增加系统稳定性和性能。另外，还进行了许多功能和性能的改进，导致许多程序的性能可以与使用现代的生产JVM所达到的性能（用传统的运行时语言实现的，如C/C++）相媲美。

Jikes RVM有太多的贡献者，以致在这儿不能一一提及，但是，我们要感谢Jikes RVM的开发团队。只有不多于100个人为Jikes RVM贡献了代码，有19个人作为Jikes RVM核心团队的成员。要了解全部的感谢名单，可以去Jikes RVM的站点。在2005年《IBM System Journal》（IBM系统杂志）的一篇文章（Alpern等2005）中可以了解关于Jikes RVM的早期历史和它的开源团体的成长的更多信息。

10.4　一个自足执行的运行时自举

与一个传统编译器（图10-1）的自举相比，元循环运行时的自举涉及更多的技巧。图10-2演示了描述这个过程的一个T型图。

图　10-2：演示了Jikes RVM在一个C编写的现有JVM上自举的T型图

这个启动映像包含了当这个系统自足执行时代表内存的好几个文件（图10-2中最右边的T）。启动映像的内容是代码和数据，类似于在一个常规编译器的对象文件中的东西。Jikes RVM的启动映像中额外包含了由垃圾收集器创建的根映射。我们将在后面的10.5.9节讲述根映射。启动映像的创建者是使用Jikes RVM的编译器创建这个启动映像文件的一个程序，在一个自举JVM上执行。一个加载器负责把这个启动映像加载到恰当的内存区域，在Jikes RVM中，这个加载器是众所周知的启动映像管理器。

10.4.1　对象布局

启动映像记录器必须把对象布置在磁盘上，因为它们将用于运行的Jikes RVM中。Jikes RVM中的对象模型是可配置的，在保持系统的其他部分固定的同时允许可选设计进行评估。一个可选设计的例子是是否为每个对象提供更多的位来容纳对象散列，或为每个对象提供更多的位来实现同步的快速锁定。Jikes RVM对象布局的纵览如图10-3所示。

图　10-3：Jikes RVM中对象的布局

Jikes RVM中默认的32位对象模式通常对于对象头使用两个字：第一个字引用类型信息块，第二个字保存对象锁定、散列和垃圾搜集的状态信息。位于对象头之后的是对象域。对于数组来说，第一个域是数组的长度，其余的是数组的元素。为了避免存取数组的移位，数组的大小就是对象头的大小和数组长度域，对对象的所有引用实际上引用这个对象内三个字的位置。这样就允许数组中的第一个元素在这个对象内的偏移为零，但是，这也意味着对象头总是在一个对象引用之后的三个字，而且，一个对象的第一个域始终位于这个对象的引用的负偏移位置。

类型信息块负责保存特定类型的每个对象的共同数据。这些数据主要用于虚方法和接口方法分派。方法分派是识别与特定对象相关并应该调用的方法的过程。类内部的方法在类型信息块中分配位置，以便能够进行快速和有效的方法分派。类型信息块还保存那些容许快速确定运行时类型信息的值，这加速了Java的instanceof和checkcast的操作。它还保存了特别的方法以在垃圾收集期间处理一个对象。

和对象一样，属于类的静态数据也必须跟踪。静态数据保存在一个名为Java内容表（Java Table Of Contents,JTOC）的位置。Java内容表在两个方向进行组织：Java内容表中的正向偏移保存了包含引用和引用文字的静态域的内容。Java中文字的值是类似于一个字符串的东西，是字节码可以直接引用但没有域位置的东西。Java内容表中的负地址负责保存基础类型的值。按这种方式分离值，使垃圾收集器更容易地判断静态域的哪些引用应该防止一个对象被认为是垃圾。

10.4.2　运行时内存配置

启动映像记录器负责当虚拟机开始启动时布置它的内存。在启动映像中必需的所有对象都在那儿，因为它们都由将启动Java应用程序的代码引用。图10-4显示了当Jikes RVM执行时使用的内存区域的概览。

图　10-4：Jikes RVM的运行时内存配置

在图10-4中可以看到许多关键项：

启动映像管理器

启动映像管理器和它的栈由负责载入启动映像的加载器组成。

本机库

类库使用的本机库所需要的内存。这将在10.5.7节进一步讲述。

MMTK空间

有不同的垃圾收集堆供MMTK使用来支持运行的应用程序。

根映射

垃圾收集器在启动映像中可获得的域的信息。在随后的10.5.9节将进一步讲述。

代码映像

分别从Java内容表和类型信息块中可以直接获得的静态方法和虚方法的可执行代码。代码写入了单独的启动映像区域以提供内存保护的支持。

数据映像

数据映像是通过先记录启动记录和Java内容表然后对Java内容表可获得的对象进行移动所创建的。利用Java的反射API，这些对象移入自举JVM中。对象移入的顺序会影响位置，由此影响性能，所以，这个移入机制是可配置的（Rogers等2008）。

Java内容表

正如先前在10.4.1节中所述，Java内容表负责保存字和静态的域值。移动Java内容表产生启动映像。

启动记录

位于数据映像开头部分的包含在启动映像管理器和Jikes RVM之间共享数据的表。这些值在自举期间通常不能确定。

10.4.3　编译原生类并填充Java内容表

原生类是必须构建到启动映像中以让它运行的类的集合。最重要的原生类是负责启动虚拟机的org.jikesrvm.VM。如果某些类不是启动映像的一部分，那就不是原生类，因而就是引用的。当一个引用的对象在运行时被访问时，会导致类加载器加载并链接这个引用的类。原生类的清单是在自举期间通过搜索目录和读取一个明确要编译的类的清单来生成的。这个明确的清单对数组类型特别重要。通过重复编译和增加构建到启动映像中的类，就可以能生成原生类的清单。但是，这会显著地增加构建Jikes RVM的时间。一个建议的替代方案是使用Java的annotation来标示哪些是原生类。

在移动对象图和记录启动映像之前，启动映像记录器会编译原生类。编译一个原生类涉及用Jikes RVM的类加载器加载它的类，这个类加载器会自动地在Java内容表和类型信息块中分配必需的空间，然后遍历所有的方法并用Jikes RVM的一个编译器编译它们。因为这全是纯Java代码，启动映像记录器利用Java的并发API来尽可能地并行执行这个任务。一旦核心的原生类编译完成，主机JVM堆中的对象图就提供了充足的功能让Jikes RVM启动自身、分配额外的对象、开始载入和执行用户类。为了完成这个自举过程，核心对象图就利用主机JVM提供的Java的反射API的能力进行移动并记录到磁盘上Jikes RVM的对象模型中。

10.4.4　启动映像管理器和VM.boot

正如10.4节所述，启动映像管理器负责把编译后的映像载入内存。这种方式的具体细节根据操作系统而改变，但是，这些映像都设置成按请求分页载入内存。按请求分页的意思是启动映像中的页保持在磁盘上，直到需要它们。

一旦载入内存，启动映像管理器初始化启动记录，然后载入机器寄存器来把执行移交给Jikes RVM的方法org.jikesrvm.VM.boot（或简写的VM.boot）。Jikes RVM负责所有的内存布置，使高效的垃圾收集技术和有效处理Java异常的一个栈体制成为可能（参见后面的10.5.4节）。一旦进入VM.boot方法，就需要专门的封装器在启动映像管理器中的本机代码和C库之间进行传递（这些将在随后的10.5.7节进一步讲述）。

VM. boot的工作是确保这个VM处于执行程序的就绪状态。它通过初始化在记录启动映像时不能初始化的那些RVM组件来做到这一点。一些组件必须显式地启动—例如，垃圾收集器。其余的组件是那些由于在自举程序和Jikes RVM类文件之间有冲突而没有完全写入启动映像的小部分原生类。为了初始化这些类，必须运行这些类的静态初始化方法。初始化线程系统是VM.boot方法的一个重要部分。它创建必须的垃圾收集线程、一个用来运行对象的finalizer方法的线程和负责自适应优化系统的线程。还会创建一个调试器线程，但只有当操作系统给Jikes RVM发送一个信号时这个线程才会安排执行。最后才创建并开始执行的线程是负责运行这个Java应用程序的主线程。

10.5　运行时组件

前面一节讲述了使得Jikes RVM准备好执行。在本节中，我们看一些Jikes RVM主要的运行时组件，从直接负责执行Java字节码的组件开始，然后看一些支持执行的其他虚拟机子系统。

10.5.1　基础的执行模型

Jikes RVM不包括解释器；所有的字节码都必须通过Jikes RVM的一个编译器转变为本机机器码。这种编译的单位是方法，而方法的编译延迟到其第一次由程序调用时才进行。最初的编译由Jikes RVM的基线编译器进行，这是一个非常快速地生成低质量代码的简单未优化编译器。随着执行的继续，Jikes RVM的自适应系统监控程序的执行以发现程序的热点并用Jikes RVM的优化编译器选择性地重新编译它们。这是一个可以生成更高质量代码的成熟得多的编译器，但是，在编译时和编译内存使用上比基线编译器的开销大很多。

这种选择性优化的模式不是Jikes RVM所特有的。所有现代的生产JVM都依赖某种形式的选择性优化把编译资源优化成将产成最大益处的部分程序方法。正如先前所述，选择性优化是使成熟的优化编译器能够作为动态编译器进行部署的关键。

10.5.2　自适应优化系统

从架构而言，自适应优化系统实现为一组松散的同步实体。因为它是用Java实现的，我们能够利用内建的语言特性（例如线程和监视器）来构建代码。

随着程序执行，基于定时器的样例由运行的Java线程累积入取样线程。两种类型的概要数据会收集起来：当前执行的方法的样例（为了指导识别优化编译的候选方法）和调用栈的样例（为了识别重要的概要数据定向内联的调用图边界）。当取样缓冲充满时，低级别的概要代理给一个高级别的Organizer（用单独的Java线程实现的）发出一个信号来总结和记录原始的概要数据。一个Controller线程会周期性地分析当前的概要数据并利用一个分析模型来确定哪些方法（如果有）应该安排进行优化编译。这些决策利用来自“滑雪板租赁（ski rental）”[1]问题在线算法的一个标准2-竞争解决方案所制定的。一个方法没有选定进行优化，直到优化它所预期的好处（将来调用中的加速）超过预期的成本（编译时间）。这些成本效益计算是通过结合在线概要数据（候选方法在当前的执行过程中取样的频率）和（离线的）描述预期的相对加速与优化编译器的每个优化级别的编译成本的经验驱动的常量来进行的（众所周知的编译器的DNA）。

[1]查看http://en.wikipedia.org/wiki/Ski_rental_problem来了解更多的信息。

10.5.3　优化编译

作为一个元循环运行时，Jikes RVM自己进行编译而不是依赖另一个编译器以确保良好的性能。元循环创建了一个有效力的循环：我们要在虚拟机的实现中编写清楚、雅观和高效的Java代码，这个强烈的愿望驱使我们发展创新的编译器优化和运行时实现技术。在这一节中，我们将介绍优化编译器，它由许多状态组成，这些状态归并为三个主要阶段：

1.高级中间表示（HIR）

2.低级中间表示（LIR）

3.机器级中间表示（MIR）

所有这些阶段都在一个由基础块组成的控制流程图上进行，这些基础块由如图10-5所示的一些指令组成。一条指令由几个操作数和一个运算符组成。因为运算符逐渐变得更加针对特定的机器，所以，运算符在这个指令的生命周期内会发生改变（变异）。操作数按定义的操作数和使用的操作数来进行组织。主要类型的操作数包括固定操作数和由寄存器编码的操作数。基础块是分支指令只出现在末端的一列指令。有专门的指令标识基础块的开始和结尾。控制流程图通过边界线连接着不同的代码区域。异常会进行特殊处理，这在后面会进行讨论。由于编译的三个主要阶段都使用相同的基础中间显示，许多优化都应用于不止一个阶段。接下来我们介绍这三个编译器阶段的主要任务。

图　10-5：优化编译器的中间表示的概览

高级中间表示

高级中间表示由一个名为BC2IR的编译器阶段生成，它获取字节码作为输入。这个初始阶段基于字节码操作数栈进行传播。这个栈是模拟的，字节码合在了一起来生成HIR指令，而不是生成分开的操作。除了用一个极大的符号寄存器来代替一个表达式栈之外，HIR级的运算符等同于字节码中执行的运算。

一旦指令形成，它们就转变为更简单的操作（如果更简单的操作存在）。在这之后，其余的调用指令都考虑内联。BC2IR阶段的主要部分进行递归地编写，所以，内联递归地使用BC2IR阶段。

本地优化在HIR阶段进行。本地优化考虑降低基础块的复杂性。考虑一个基础块的时候，这个基础块内的依赖也会考虑。这简化了编译器阶段，不必考虑分支和循环的结果，这些情况会引入不同种类的数据依赖。本地优化包括：

常量传播

传播常量值以避免把它们放入寄存器中。

副本传播

利用最初的寄存器替换寄存器的副本。

简化

基于运算的操作数把运算改为更简单的运算。

表达式合并

把运算目录合并起来。例如，“x=y+1；z=x+1”可以把表达式“y+1”合并到“z=x+1”，生成“x=y+1；z=y+2”。

共用子表达式消除

查找对相同的操作数进行相同运算的指令，用前者结果的副本消除后面的运算。

闲置代码消除

如果一个寄存器在两次定义之间没有使用，那么，执行最初定义的指令是闲置代码，可以消除。

分支优化考虑改进控制流程图以便最有可能的路径按对编译器的目标来说最优的方式进行布置。其他的分支优化希望去除多余的测试和解开循环。

逃逸分析考虑对象是否可以只在编译的代码的上下文中存取，还考虑一个不只在局部上下文中存取的对象是否可以在线程之间共享。如果一个对象只是局部存取，那么，就可以排除为它分配内存的需求并把这个对象的域移入寄存器。如果一个对象只在一个线程中访问，那么，对这个对象的同步操作可以消除。

后面将介绍使用增加到这个中间形式的额外信息的许多优化。

低级中间表示

低级中间表示把先前类似于字节码的运算转换为更类似于机器码的运算。对域的运算转换为载入和存储运算；像new和checkcast这样的运算符扩充为调用提供这些运算的运行时服务并可能进行内联。适用于高级中间表示的大部分优化也适用于低级中间表示，但是，像逃逸分析这样的优化不行。由于支持的运算类型不同，因此对于不同的架构，在低级中间表示之间有一些微小的差异。对于低级中间表示指令而言，非常多的符号寄存器可以用于低级中间表示指令。

机器级中间表示

创建最终的机器级中间表示包括编译器后端的三个相互依存也相互竞争的转换。说这些转换相互竞争是因为它们的顺序可以决定生成的机器码的性能。在Jikes RVM中，第一个转换是指令选择。指令选择是类似于RISC（精简指令集计算机）的低级中间表示指令转换成已经存在于实际机器上的指令的过程。有时候，执行一个低级中间表示指令需要不止一个指令；例如，在32位的架构体系上，一个低级中间表示的长整型加法需要两个指令。在其他时候，树型模式匹配指令选择器（一个著名的自底向上重写的系统（Bottom Up Rewrite system,BURS）把几个指令合并到一个指令。图10-6演示了模式匹配的一个例子。两个模式都可以在IA32架构中找到：一个模式创建三条指令用了41条机器指令，另一个模式把加载和存储编成单个指令的内存操作，开销只有17条机器指令。最终选择了开销最小的模式。

图　10-6：自底向上重写系统指令选择

在指令选择后，无限的寄存器必须映射到一个真实机器所提供的有限寄存器。这个过程称为寄存器分配。当没有足够的寄存器时，可以依靠栈把值保存在内存中；依靠栈交换实际寄存器的值，这种技术称为溢流（spilling）和填充（filling）。寄存器分配器必须将溢流和填充减到最少，也要考虑任何架构的需要—例如，要求乘法和除法必须用固定的寄存器进行。

Jikes RVM有一个进行寄存器快速分配的线性扫描寄存器分配器，但是可能会产生额外的复制和内存操作。当机器上可用寄存器的数量很少时这是一个大问题。随着寄存器数量的增加和线性扫描算法的改进，对于某些代码来说，进行一次比较昂贵的寄存器分配的好处越来越不明显。

创建机器级中间表示指令的最后部分是指令调度。调度把指令分开以容许处理器充分使用指令级的平行性。

在机器级中间表示级别，只进行了很少的优化。由于与机器指令相关的很多副作用，说服并据此优化指令的行为很困难。机器级中间表示级的其他编译器阶段关注于确保遵守调用、异常和其他规定。

分解的控制流程图

如果一个空指针用于访问内存或数组的索引越界，Java程序会创建运行时异常。这些运行时异常控制了代码的流向并会因此结束基本程序块。这导致小的基础程序块具有较少的局部优化范围。为了增大基础程序块，运行时异常依赖的控制流在高级中间表示和低级中间表示级变为人工的数据依赖。这种依赖确保操作按异常的语义学恰当地排序，因此，可能在执行的中间过程中离开基础程序块来处理一个运行时异常。当这种中间形态在程序块的中间存在异常出口时，就可以说控制流程图被分解了（Choi等1999）。

明确测试运行时异常的指令被创建并生成合适的保护结果。那些接下来需要排序的指令使用这些保护结果。可以生成一个异常的指令就是众所周知的潜在异常指令（Potentially Exceptioning Instruction,PEI）。所有的潜在异常指令会生成一个合适的保护结果，那些用来移出潜在异常指令（如果潜在异常指令是多余的）的指令也同样如此。例如，测试null的分支对多余的同样的值进行空指针测试。来自这个分支的保护结果会代替来自空指针测试的保护结果以确保指令不能重新排在这个分支之前。

图10-7演示了单个数组赋值：把它组成的运行时异常转变为确保代码按正确顺序执行的潜在异常指令和保护依赖。

图　10-7：分解控制流程图中指令的一个例子

分等级和扩展的数组SSA形式

静态单一赋值（Static Single Assignment,SSA）形式减少了编译器优化必须关心的依赖。这种形式确保任何寄存器（更普通的叫法是变量；在Jikes RVM的高级中间表示阶段和低级中间表示阶段，所有的变量都保存在寄存器中）几乎立即写入。一次编译器转换必须处理三种依赖：

真依赖

在这种情况下，一个寄存器写入然后读出。

输出依赖

在这种情况下，一个寄存器写入，然后再次写入（第二次写入必须发生在第一次有了任何转换之后）。

反向依赖

在这种情况下，一个寄存器读出，然后写入（写入必须发生在读出有了任何转换之后）。

知道一个寄存器写入一次就意味着不会发生输出依赖和反向依赖。这个特性意味着我们先前的局部优化可以应用于全局。为了处理循环和其他分支，专门的Φ指令用于对控制流程图中不同地方的特殊值的合并进行编码。

数组静态单一赋值形式是静态单一赋值形式的一个扩展，在其中，加载和存储认为是定义一个特殊的名为堆的变量。按这种方式对内存存取建模使编译器推论如果两次读取都发生在相同的数组位置以及相同名字的一个堆，第二次读取可以替代为第一次读取的一个副本。多余的存储也以类似的方式处理。它还允许存取无关的堆来重新组织—例如，用浮点和整数运算。数组静态单一赋值形式最初设计用于FORTRAN；扩展的数组静态单一赋值形式增加了形式因素，例如Java的域不能相互同名（Fink等2000）。

还是在静态单一赋值形式中，Jikes RVM构建了π指令，放在一个分支后面，使用一个操作数。这个π指令使用和分支一样的操作数并给它一个新名字以代替后续指令中使用的操作数。利用π指令，编译器可以推断：一个分支进行一个测试（例如一个null测试），那么，利用这个π指令的寄存器结果的任何null测试都是多余的。类似地，数组边界检查也可以取消（Bodik等2000）。

高级中间表示的静态单一赋值形式中的循环倒置优化也消除了可能的异常。循环倒置把异常检查代码移出了循环并明确地测试异常是否会在进入循环之前发生。如果会出现异常，那就会执行一个产生异常代码的循环版本。如果不会出现异常，那就会执行一个没有异常的循环版本。

局部求值

高级中间表示优化（包括静态单一赋值优化）能够减少局部代码的复杂度，但是，这受限于可用的常量值。当一个值来自数组时通常不能确定它是否是常量，在Java中，数组总是容许它的值发生改变。最常发生这种情况的是对于字符串。Jikes RVM引入了纯粹的annotation作为Java的扩展。利用反射，annotation能在编译期使一个有常量参数的方法求值。出于这个目的而利用注释和反射在元循环运行时是不复杂的。

局部求值有能力完全消除一些开销，例如运行时安全检查。安全检查通常遍历栈来确定哪个方法调用了一个受限方法，然后检查以了解那个方法是否有权限调用这个受限方法。因为方法在调用时进入栈的时间可以在编译器内确定，如果把进入栈作为内联的结果，精确的方法就简单地产生了。通过对安全检查代码求值，或如果这检查是纯粹的进行一次反射调用，那么安全检查的结果就可以是确定的或如果总是通过就可以取消。

当前栈替换

当前栈替换（On-Stack Replacement,OSR）是当代码在栈上执行时交换执行代码的过程。例如，如果一个运行时间长的循环正由JIT基线编译器创建的代码执行，当优化编译器创建优化代码后用优化代码交换这段代码是有好处的。它的用处的另一个例子是如果一个运行的方法有不安全的虚构的类层级属性（例如一个类没有子类，这容许在内联上进行改进），那就可以使这个方法无效。

当前栈替换通过引入保存一个方法的执行状态的新的字节码（在JIT基线编译器的情况）或新的指令（在优化编译器的情况）来工作。一旦执行状态保存下来，代码可以交换为新编译的代码，通过载入保存的状态来继续执行。

小结

本节概要介绍了Jikes RVM的优化编译器的许多高级组件。用Java编写这些组件已经给这些系统带来了许多内在的好处。线程是有用的，允许系统的所有组件都支持多线程。系统设计成线程安全，例如，容许多个编译器线程并行运行。另一个优点来自Java通用的集合，它提供了易于理解的库，简化了开发，并容许系统的组件集中在它们的角色上，而不是底层的数据结构管理。

10.5.4　异常模式

异常是例外情况吗？虽然许多程序员对这个问题都抱着肯定的观点，但是，虚拟机必须对基准测试中常见的用例进行优化。有问题的用例是读取一个输入流并当一个预期的模式出现时抛出一个异常。这种模式既出现在SPECjvm98[1]jack基准测试中又出现在DaCapo 2006[2]lusearch基准测试中。

首先让我们考虑一个异常需要什么。每个异常必须创建在抛出异常时当前栈的一个方法清单（大家知道的栈跟踪）并把控制传给处理这个异常的catch块。通过允许出现内存保护失败，空指针异常（例如，读或写不存在的内存页）就有效地处理了。当产生一个错误时，错误指针的地址会提供给一个处理器。正是因为如此，栈跟踪和处理器信息必须确定下来。

用C语言编写的虚拟机会设法使用C的调用方式来调用Java的即时编译代码。其中的一个问题是C的调用方式不记录正在运行的代码。为了解决这个问题，C的虚拟机可以设法使用返回地址来计算正在运行的方法。必须搜索虚拟机内的每个方法来了解它对应的编译后代码是否包含特定的返回地址。对于当前栈中的每个方法都必须进行重复搜索。由于Jikes RVM控制了它的内存配置，栈配置成包含额外的信息来快速地处理异常。Jikes RVM特别在栈上放置了一个标识符，以便不用提供方法信息就能进行简单地查找。为了确定字节码出现异常的地方和处理器的位置，需要利用返回地址或错误指针地址进行计算。这意味着处理一个异常的速度与栈的深度成比例，而不由虚拟机中方法的数量来决定。

和其他虚拟机一样，Jikes RVM尽可能优化地远离异常，最好把异常压缩到分支。它还把许多方法内联到一个方法中，因此，在栈上放置一个标识符的情况很少发生，只有当处理未内联的方法时才这样处理。

[1]http://www.spec.org/jvm98/.

[2]http://dacapobench.org/.

10.5.5　Magic、annotation，使事情流畅地进行

由编译器生成的代码有直接存取内存的指令。有时候需要直接存取内存—例如，为了实现垃圾收集程序或为了锁住对象而访问对象头。为了容许强类型地内存存取，Jikes RVM利用一个名为VM Magic的库，它已经作为一个标准接口并用于其他的JVM项目中。VM Magic定义了这两种类：编译器pragma annotation（扩展了Java语言）或未封装类型（描绘对底层内存系统的存取）。未封装类型的地址用于直接存取内存。

Jikes RVM的编译器内特别处理了VM Magic的未封装类型。对未封装类型的所有方法调用都作为直接操作一个宽度为一个字的值来对待。例如，“plus”这个方法会作为累加机器底层一个字的宽度来对待，从而直接影响通常是这个对象的指针的内容。由于操作的这个值不是一个对象的引用，编译器会标明，垃圾收集器对保存未封装类型的位置和保存基础字段（例如一个int）的内存地址同样感兴趣。

拥有强类型存取内存还不足于完全容许Jikes RVM完好地运行。还需要一些专门的方法向编译器指明内在的事情正在发生或它们应该简化部分的Java强类型语法。这些专门的方法存在于org.jikesrvm.runtime.Magic中。一个内在操作的例子是平方根方法，对于这个方法，Java没有提供字节码，但许多计算机架构都有直接获得结果的一条指令或一个程序。绕过Java的强语义的一个例子是在线程调度的过程中避免转换，因为在某些关键时刻运行时异常是不允许的。

所有的神奇操作都采用不同于常规方法的方式编译，而且它们不能作为单独的方法进行编译。方法确实在那儿，但如果一旦执行它们，就会抛出一个异常。编译器根据方法来确定哪个神奇运算正在执行，然后寻找编译器必须为它提供哪些运算。在启动映像的创建期间，提供了一些未封装的神奇类型和方法的替换。例如，对象在配置好之前在启动映像中的地址是未知的。启动映像的未封装类型知道标识符和它们映射的对象。在启动映像的创建期间，这些标识符链接到它们引用的对象。

虽然神奇运算和未封装类型允许Jikes RVM中的Java代码像C或C++中的指针一样有权访问内存，但是，指针是强类型，不能用来代替引用。强类型使程序员的错误在编译期就能发觉，优秀的集成IDE和对查找缺陷的工具的支持，所有这些都帮助了Jikes RVM的开发。

10.5.6　线程模式

Java有完整的线程支持。在1998年，操作系统对线程的支持还不能很好地适合许多Java应用程序。尤其是，典型的操作系统线程实现方式不适合支持单个进程中有好几百个线程，锁操作（实现Java的同步方法所必需的）的开销非常昂贵。为了避免这个开销，一些JVM自己实现线程，利用一个名为绿色线程（green threading）的操作模式。在绿色线程中，线程是通过让JVM自己管理这个过程（把多个Java线程复合到较少的操作系统线程上，通常是复合到一个操作系统线程，或在多处理器系统中每个处理器一个线程）来实现的。绿色线程的主要优点在于实现了Java语义中特有的锁和线程调度并使它非常容易扩展。主要缺点是操作系统不知道JVM正在做什么，这会导致许多性能反常，尤其当Java应用程序与本机代码频繁交互的时候（它自身对线程模式进行了假定）。然而，实现绿色线程的JVM已经为许多Java操作系统项目（能够避免绿色线程在标准操作系统上的性能反常）提供了基础。

随着处理器性能和操作系统实现的改进，在JVM中管理线程的优势已经不明显了——虽然在JVM内管理非竞争的锁操作仍然具有优势。

利用Java，一个清楚的线程API已经创建以允许Jikes RVM拥有不同的底层线程模式，就像由不同的操作系统提供的那些模式一样。将来，在语言和操作系统之间拥有一个灵活的接口可以容许Jikes RVM适合新的程序员行为（例如，支持数千个线程）。

10.5.7　本机接口

很不幸，Jikes RVM不能全部使用Java代码。访问操作系统程序必须分配内存页。Jikes RVM的类库把Java代码与现有的库（例如用于窗口化的库）结合起来。Jikes RVM提供了两种方式来访问本机码：

Java本机接口（Java Native Interface,JNI）

JNI是容许Java应用程序与通常用C编写的本机应用程序结合的一个标准。JNI的一个特性是可以处理Java对象，也可以调用Java方法。为了防止本机代码访问的对象被当作垃圾收集掉，这些对象必须记录下来。当使用JNI调用本机方法时，这会产生一些开销。

系统调用

在系统调用的声明中，它们与本机方法相似，只是它们有一个额外的annotation。由于限制了本机代码不能通过JNI接口回调入虚拟机，系统调用容许更有效地与本机代码进行传入和传出。Jikes RVM利用默认的操作系统调用惯例把系统调用实现为对一个本机方法的简单的进程调用。

10.5.8　类加载器和反射

类加载器负责把类载入Jikes RVM并把这个过程与对象模式连接起来。反射允许一个应用程序查询对象的类型，甚至对在静态编译期还不知道其类型的对象进行方法调用。反射是通过应用程序使用像java.lang.Class这样的对象或像java.lang.reflect包中那些对象（它们是在Jikes RVM的运行时执行的API封装器）来进行的。

由于一些优化依赖类的层次结构，重要的运行时钩子存在于类加载器中，如果先前保存的假定不再正确时它们可以触发重新编译。关于这些内容，我们已经在前面的10.5.3节详细地讨论过。

10.5.9　垃圾收集

内存管理工具包（Memory Management Toolkit,MMTk）是构建高性能内存管理实现的一个框架（Blackburn等，2004年5月）。经过5年时间，MMTk已经成为垃圾收集研究团体密集使用的核心基础。它的高便捷和可配置架构是它取得成功的关键。除用作Jikes RVM的内存管理系统之外，它也移植到了其他语言的运行时，例如Rotor。本节将略微讨论一些MMTk的关键设计概念。关于垃圾收集算法和概念的完整信息，最好的参考是《Garbage Collection：Algorithms for Automatic Dynamic Memory Management（Jones和Lins 1996）》。

MMTk的一个底层元法则完全用在了Jikes RVM中：通过依赖一个激进的优化编译器，可以以高层次、可扩展和面向对象的风格编写出高性能的代码。这个法则在MMTk中得到最大的发展，即使对性能而言非常关键的操作（例如快速的对象分配路径和写障碍）也完全表示成结构良好的、面向对象的Java代码。表面上，这种风格与高性能（分配单个对象，有许多代码级的虚拟调用和相当多复杂的数值计算）完全不相容。然而，当分配序列由优化编译器递归内联和优化后，它会产生紧密内联的代码序列（类似于分配一个对象并初始化该对象头的10个机器指令），这些代码序列等同于那些通过在汇编代码中手写的快速路径序列或把它硬写入编译器而获得的代码序列。

MMTk把内存组织成空间（Space），这是（可能不连续的）虚拟内存区域。MMTk提供了空间的许多不同的实现，每个实现都体现了一个分配和回收由空间管理的虚拟内存块的特殊基础机制。例如，CopySpace利用碰（bump）指针分配并通过把所有活的对象复制到另一个空间来回收；MarkSweepSpace通过这种方式组织内存：利用空闲列表把空闲的内存块链在一起，通过“清扫”无记号的（死的）对象并把它们重新链接到适当的空闲列表来支持分配。

计划（Plan）是MMTk关于一个垃圾收集算法的典型构想的术语。计划是通过把一个或多个空间实例以不同的方式组合在一起来定义的。例如，MMTk的GenMS计划实现了相当标准的按代的标记来清除的算法，组合了一个CopySpace和一个MarkSweep空间来分别实现新生代和壮年代的空间。除了用于管理用户级Java堆的空间之外，所有的MMTk计划还包括好几个用于管理虚拟机级内存的空间。特别地，专门的空间用于分配即时生成的代码、Jikes RVM启动映像和低级别的虚拟机实现对象（例如类型信息块）。在MMTk 3.0版本中预定义和发布了15个计划（也就是15个不同的垃圾收集算法）。此外，许多其他的计划已经由MMTk研究团体开发出来并发布在学术文献中。

MMTk和它的主机运行时环境通过两个仔细定义的、说明MMTk暴露给主机虚拟机哪些API和MMTk希望主机提供哪些虚拟机服务的接口来进行交互。为了保持MMTk对于多个主机虚拟机的轻便性，构建过程通过依靠虚拟机接口的一个存根实现单独编译MMTk来严格控制这两个接口。或许这些接口最复杂的是用于允许MMTk和Jikes RVM协作识别用于垃圾收集的根的那部分。大部分MMTK的计划都代表轨迹收集器。在一个轨迹收集器中，垃圾收集器从一组根对象（通常是这个程序的全局变量和线程栈结构中的引用）开始并对它们的引用域求传递闭包来找到所有可获得的对象。可获得的对象认为是活的且不会收集掉。传递闭包中不可获得的任何对象都是死的；这些都会由收集器安全地回收并用于满足将来的内存分配请求。在Jikes RVM中，垃圾收集的根来自寄存器、栈、Java内容表和启动映像。启动映像中的引用根据根映射来确定，而根映射压缩了包含引用的启动映像中的所有偏移。要确定栈上和寄存器中的引用，这个方法和在它内部的位置都按异常模式中所用的相同方式来确定（参见前面的10.5.4节）。根据编译器确定的方法，它可以为MMTk提供一个处理寄存器和栈来返回这些引用的迭代器。

Jikes RVM集成

Jikes RVM在对象表示的初始创建期间和MMTk结合在一起，提供迭代器来处理引用、对象分配和障碍实现。对象分配和障碍会影响性能；因为MMTk是用Java编写的，所以，关联的代码可以直接连接到高效的编译代码。因为多种原因，障碍在垃圾收集计划中是必需的。例如，读障碍对于捕获可能被一个并行垃圾收集器复制的对象的使用情况是必需的，而写障碍用于分代收集器中，在那儿，写一个旧的对象就意味着必须把这个旧的对象当作年轻代回收的根。

小结

MMTk提供了一组功能强大而且流行和精确的垃圾收集器。能够容易地把Jikes RVM和MMTk的不同模块链接起来，这使开发变得容易而且减少了开销，出于性能的考虑，还使Jikes RVM内联了部分MMTk。用Java编写垃圾收集算法使得垃圾收集器的实现人员不用考虑编译器内部发生的情况。Java固有的线程已经使得垃圾收集器并行且和运行时模式结合在一起。Java的库为收集器的接口提供了灵感，这意味着用MMTk编写新的垃圾收集器已经显著地简化了。

10.6　经验教训

Jikes RVM是一个成功的实验虚拟机，以一种灵活和易于扩展的方式提供近似于艺术级的性能。以运行时支持的语言来编写，这允许紧密的集成和组件重用。Java的使用使简单易懂的代码、好的模块性和高质量工具的使用（例如集成开发环境）成为可能。

Java语言和JVM的实现正在发展中，Jikes RVM已经而且还将不断地随着这些改变而发展。一个有趣的发展是X10编程语言，它通过提供线程安全的保证来处理程序员如何为许多核心系统开发应用程序的问题，正如Java中垃圾收集提供内存安全保证一样。Jikes RVM的代码已经为X10的开发提供了一个出色的测试环境。因为JVM的实现引起了新的优化，例如新的垃圾收集技术或对象内联，所以，把这些优化嵌入到一个框架（例如RVM）中意味着运行时、编译器和代码基也改进和展示了元循环的效率。

元循环环境的扩展性使Jikes RVM成为一个进行多语言虚拟机试验的出色的平台。这种扩展也允许Jikes RVM的概念以不同的编程语言实现。

Jikes RVM有许多其他有趣的扩展和相关的项目，包括通过二进制翻译支持像C和C++这样的语言，通过扩展在虚拟机内部提供面向方面的编程，使整个虚拟机进入一个操作系统以消除运行时优化的障碍。虽然最初存在争议，但是现在，垃圾收集、自适应优化和链接时优化是开发人员所期望的。Jikes RVM演示了如何通过一个帮助开发的共同的元循环环境来达到和适应这些特性。Jikes RVM支持一个大的研究团体并获得了高性能。通过拥有一个美丽的架构，Jikes RVM可以为将来的运行时环境连续地提供一个平台。

参考文献

Abelson,Harold,Gerald Jay Sussman,and Julie Sussman.1985.Structure and Interpretation of Computer Programs.Cambridge,MA：MIT Press.

Aho,Alfred,Ravi Sethi,and Jeffrey Ullman.1986.Compilers,Principles,Techniques,and Tools.Boston,MA：Addison-Wesley.

Alpern,Bowen,et al. 2005."The Jikes Research Virtual Machine project：Building an open-source research community."IBM Systems Journal,vol.44，issue 2.

Blackburn,Steve,Perry Cheng,and Kathryn McKinley. 2004.Oil and water?High performance garbage collection in Java with MMTk（pp.137-146）.International Conference on Software Engineering,Edinburgh,Scotland.ACM,May'04.

Bodik,Rastislav,Rajiv Gupta,and Vivek Sarkar. 2000.ABCD：eliminating array-bounds checks on demand.ACM SIGPLAN Conference on Programming Language Design and Implementation（PLDI 2000），Vancouver,British Columbia,Canada.ACM'00.

Choi,Jong-Deok,et al. 1999.Efficient and Precise Modeling of Exceptions for the Analysis of Java Programs.ACM SIGPLAN-SIGSOFT Workshop on Program Analysis for Software Tools and Engineering（PASTE'99），Toulouse,France：ACM,Sept.'99.

Fink,Stephen,Kathleen Knobe,and Vivek Sarkar.2000.Unified Analysis of Array and Object References in Strongly Typed Languages.Static Analysis Symposium（SAS 2000），Santa Barbara,CA：Springer Verlag.

Ingalls,Daniel,et al. 1997."Back to the future：the story of Squeak,a practical Smalltalk written in itself."ACM SIGPLAN Notices,vol.13，issue 10：318-326.

Jones,Richard and Rafael Lins.1996.Garbage Collection：Algorithms for Automatic Dynamic Memory Management.Hoboken,NJ：John Wiley and Sons.

McCarthy,John,et al. 1962.LISP 1.5 Programmer's Manual.Cambridge,MA：MIT Press.Piumarta,Ian,and Fabio Riccardi.1998."Optimizing direct threaded code by selective inlining."ACM SIGPLAN Notices,vol.33，issue 5：291-300.

Rogers,Ian,Jisheng Zhao,and Ian Watson. 2008.Boot Image Layout For Jikes RVM.Implementation,Compilation,Optimization of Object-Oriented Languages,Programs and Systems（ICOOOLPS'08），Paphos,Cyprus.July'08.

第四部分　最终用户应用架构

第11章　GNU Emacs：滋长的特性是其优势

第12章　当集市开始构建教堂

第11章　GNU Emacs：滋长的特性是其优势

Jim Blandy

原则与特性

结构

√功能多样性

√模块

概念完整性

依赖关系

√修改独立性

进程

自动传播

数据访问

可构建性

√增长适应性

√熵增抵抗力

我常使用Emacs，它是一种功能超强的文字处理程序。它的作者是Richard Stallman。它是用Lisp语言编写，这是唯一的一种优美的计算机编程语言。它很庞大，而且只能编辑纯ASCII的文本文件，也就是说，没有字体、不能加粗、无法添加下划线……如果你是一名专业的作者，即不需要考虑对文字设置格式、生成打印稿，那么Emacs会使其他编辑软件相形见绌，就像正午的太阳相对于星星一样。它不仅庞大、光芒四射；而且能使其他的东西化为乌有。

——Neal Stephenson

GNU Emacs文本编辑器和其恶名并不相配。它的支持者认为它是无可比拟的，甚至顽固地拒绝使用现在更流行的文本编辑器。而其反对者说它隐晦、复杂，而且和Visual Studio这样的主流开发环境相比，它显然有些过时了。甚至有些爱好者也抱怨，Emacs中复杂的组合键弄伤了自己的手腕。

Emacs所激起的争论和Emacs所提供的功能一样多。当前版本的Emacs拥有110万行代码，它是用Emacs自己的编程语言Emacs Lisp编写的。其中有一些代码是用来帮助你更好地使用C、Python以及其他语言完成编程任务的。同时还有一些代码是用来帮助你完成程序调试，在程序员之间进行协作，阅读电子邮件和电子新闻，浏览和搜索文件夹，以及解决符号代数问题。

继续探究其内幕，情况就有些奇怪了。Emacs Lisp并不支持对象，它的模块系统就是一种命名规范，所有基本的文本编辑操作使用了隐晦的全局变量，甚至局部变量也不是很局部。Emacs对许多广泛接受的、有用的、有价值的软件工程原则都不屑一顾。其代码已经有24年的历史了，体系庞大，是由成百上千的不同开发人员共同完成的。总的来说，整个代码在不断膨胀中。

但它却工作得很好。其功能集不断地成长；用户界面中不断增加了许多令人着迷的新行为；而且整个项目有效避免了对基础架构的大幅修改、频繁的交互、领导者的冲突和分歧。其活力的源泉是什么？它有什么局限性？

最后，其他软件可以从Emacs中学到什么？如果有一个新架构需要实现与Emacs相同的目标，在度量是否成功时应问什么问题？综观全章，我们将分析这三个问题，相信这样就能够挖掘出Emacs架构中最有价值的特性。

11.1　使用中的Emacs

在探讨其架构之前，我们先简单看看Emacs是什么。它和你经常使用的其他文本编辑器类似。当你用Emacs打开一个文件时，将弹出一个窗口，并显示出该文件的内容。你可以对其内容进行修改，然后保存这些修改后退出。但是，Emacs并不一定在所有情况下都十分有效，如果它的功能对你不是全都有用，那么你可能会抱怨它的启动速度要比流行的文本编辑程序更慢。当遇到这种情况时，我不会使用Emacs。

Emacs的设计预想是你需要时只启动一次，然后一直运行着它。你可以在一个Emacs会话中对多个文件进行编辑，并保存修改。Emacs能将这些文件保存在内存中而不用显示它们，所以你看到的就是当前的文件，当你需要对其他文件进行编辑时也能够马上切换过去。有经验的Emacs用户只会在发现计算机内存不足时才关闭文件，因此运行很久的Emacs会话可能会打开成百上千个文件。图11-1所示的屏幕截图展示了打开了两个帧的一个Emacs会话。左边这个帧分成了三个窗口，分别显示了Emacs广告页、可浏览的目录列表以及展示Lisp交互界面的缓冲区（buffer）。右边的帧只有一个窗口，显示的是存储源代码的缓冲区。

这里涉及了三种最基本的对象：帧、窗口、缓冲区。

帧是Emacs对计算机图形界面中窗口的称呼。在前面的屏幕截图中显示了两个紧挨着的帧。如果以文本终端使用Emacs，例如通过telnet或ssh连接，那么这个终端也是Emacs中的帧。Emacs可以管理多个图形帧，也可以同时管理多个终端帧。

窗口是帧的一部分。[1]要创建一个新的窗口，只能通过将原有窗口分成两个，当你关闭某个窗口时，将把空间给邻近的窗口；这样的结果是一个帧中的窗口始终会填满整个帧的内容。只有当前选中的窗口，才会对键盘命令进行响应。窗口是轻量级的，在使用时经常会频繁地创建和关闭。

图　11-1：使用中的Emacs

而缓冲区用来保存可编辑的文本内容。Emacs将会把打开的文件中的文本内容保存在缓冲区中，但缓冲区中的内容并不一定必须与某个文件关联：它可能包括搜索结果、在线文档，以及刚输入还没有保存到任何文件中的内容。每个窗口将显示某些缓冲区的内容，而一个缓冲区可能保存0个、1个或多个窗口。

还有一个很重要的地方，那就是除了每个窗口底部的模式行以及其他类似的东西，Emacs在向用户显示文本信息时首先会将其放在缓冲区中，然后将缓冲区的内容显示在某些窗口中。帮助信息、搜索结果、目录列表以及其他类似的内容，保存在缓冲区中的内容都有相应的名称。这看起来是一个拙劣的实现技巧，虽然它简化了Emacs的内核，但实际上它是相当有价值的，因为这样做可以使得这些不同的内容都被当作可编辑的文本内容来处理：你可以使用相同的命令来在这些内容中导航、搜索、组织、剪裁、排序，就像文本缓冲区中的其他内容那样。任何命令的输出都可以作为其他命令的输入。这与诸如Microsoft Visual Studio之类的编程环境形成了鲜明的对比，在这些环境中，搜索结果等只能在相应的工具中才有用。不过这也不是Microsoft Visual Studio独有的，绝大多数图形界面的编程环境都有这样的缺点。

例如，在上面的屏幕截图中，左边的帧中位于中间的窗口展示了一个目录列表。和大多数目录浏览器一样，该窗口为复制、删除、重命名、文件比较、通过匹配模式或正则表达式筛选一组文件、通过Emacs打开文件提供相应的快捷键命令。但与大多数目录浏览器不同，在Emacs中该列表本身也是纯文本的，保存在一个缓冲区中。Emacs中所有通用的搜索机制（包括优秀的增量搜索命令）都可以使用。只要你需要，都可将列表中的内容复制并粘贴到临时缓冲区中，然后删除列表中的其他元数据以得到一个文件名列表，接着使用正则表达式剔除我们不感兴趣的文件名，最后就能够得到一个传给新命令的文件名列表。当你习惯了这种操作，那么使用常见的目录浏览器时就会感到恼火：不能对显示出来的信息进行处理，很多命令受到了限制。在有些时候，甚至会觉得命令行中的组合命令也不好用，因为要想看到这些命令的中间结果是很不容易的。

这将使我们想起本章最开始时提到的三个问题中的第一个，我们可以问所有用户界面一个问题：如何方便地将一个命令的输出结果作为另一个命令的输入？用户界面提供的命令能否相互组合？或者在命令运行结束之前，中间结果能否显示？我认为许多程序员喜欢UNIX命令行环境的一个主要原因在于这些命令能够有效地作为一部分整合到复杂的脚本中，虽然这些环境存在一些无理由的不一致、数据模型过于简单以及其他的一些缺点。由于写一个能够脚本化的程序是十分简单的，因此很少走入死胡同。Emacs实现了相同的目标，尽管是以完全不同的方式实现的。

[1]绝大多数图形界面中都称为窗口，在Emacs中称为帧，因为Emacs将术语“窗口”用来描述更小的部分。这似乎是不合时宜的，不过Emacs的术语早在图形界面被广为接受之前就确定了，因此Emacs的维护人员通常不倾向对它进行修改。

11.2　Emacs的架构

Emacs架构所采用的是在交互式应用程序中应用广泛的模型-视图-控制器模式，如图11-2所示。在该模式中，模型是程序所操作数据的底层描述；视图则是向用户展示数据的方法；而控制器则负责实现用户与视图的交互（按键、点击鼠标、选择菜单项等），并对模型进行相应的更新。在本章中，我谈及该模式时将使用模型、视图和控制器进行描述。在Emacs中，控制器基本上是完全用Emacs Lisp语言编写的。Lisp最初是用来操作缓冲区中的内容（模型）以及窗口布局的。负责重绘的代码（视图）将完成对显示内容的更新，它无须直接受Lisp代码控制。不管是缓冲区的操作还是负责重绘的代码都可以通过Lisp代码进行定制。

11.2.1　模型：缓冲区

Emacs是用来编辑文本文件的，因此Emacs的模型中最为核心的是用来保存文本信息的缓冲区。缓冲区就是简单的字符串，在每一行的行末是新行字符；它并不是一个行的列表，也不是像Web浏览器展示HTML文档时使用的文档对象模型那样的节点树。Emacs Lisp对于缓冲区的基本操作包括添加、删除文本，以字符串的形式析取缓冲区中文本的一部分，通过精确的字符串或正则表达式来搜索匹配的字符串等。缓冲区能够保存各种字符集中的字符，包括亚洲、欧洲可能使用到的一些字符。

图　11-2：Emacs中的模型-视图-控制器模式

每个缓冲区都有一种模式（mode），它用来指定针对特定类型文本进行编辑时的缓冲区行为。Emacs中提供了针对C语言程序、XML文本等大量类型的模式。在Lisp等级，模式将使用缓冲区局部键绑定来为用户提供特定于某种模式的命令，然后使用缓冲区的局部变量来维护缓冲区状态。通过它们的组合，这些功能使缓冲区的模式与对象的类十分类似：模式用来确定哪些命令是可用的，并根据这些命令所依赖的Lisp实现来提供相应的变量。

对于每个缓冲区，基本的文本编辑功能维护了一个“撤销”日志，它为撤销某些操作保存了足够的信息。这个“撤销”日志将记住各个用户命令之间的边界，因此每个用户命令都承载了用来撤销该操作的基本操作。

当Emacs Lisp代码对缓冲区进行操作时，可以通过整数在缓冲区文本中定位某个字符。这是一种简单的方法，但在对特定的文本块进行插入和删除操作时，必将导致表示位置的数字发生变化。要在缓冲区内容被修改时跟踪其位置，Lisp代码可以创建一个标记（marker）对象，它将随着文本的移动而变化。各种基本操作既可以接受以整数表示的位置参数，也能够接受标记对象的方法表示的位置参数。

Lisp代码可以为缓冲区中的字符添加文本属性。每个文本属性都将有一个名称和一个值，它们的内容可以是任何Lisp对象。从逻辑上看，每个字符属性都是相互独立的，不过对文本属性的表示法却在同样的文本属性中有效地存储了一组连贯的特性，并且Emacs Lisp还提供了一些基本操作，用来快速地定位文本属性被修改的字符位置，因此实际上文本属性还能够用来标记文本的范围。文本属性能够告诉Emacs该如何显示文本，文本如何应对鼠标的操作。文本属性还能够指定当鼠标放在某个文本上时可以提供什么样的键盘命令。缓冲区的“撤销”日志除了记录文本本身的改变之外，还将记录文本属性的改变。Emacs Lisp中的字符串也可以拥有文本属性：从缓冲区中析取一段文本并以字符串形式保存，并且可以将该字符串插入到缓冲区的任何位置，它将拥有相同的字符属性。覆盖图（overlay）用来表示缓冲区中某些文字的相邻区域。覆盖图的起点和终点包含其覆盖的文本以及标记。和文字属性类似，覆盖图也会影响显示的结果，对于其包含的文本也能够响应鼠标的操作，但与文本属性相比也有不同之处，覆盖图不被视为缓冲区文本的一部分：字符串是不能拥有覆盖图的，而且“撤销”日志也无法记录对覆盖图终点的修改。

11.2.2　视图：Emacs重绘引擎

当用户编辑文本信息、在窗口上进行操作时，Emacs的重绘引擎将确保显示信息做出相应的更新。Emacs重绘引擎有两个重要的特性：

Emacs将自动更新显示

Lisp代码无法指定如何对显示结果进行更新。它可以自由地操纵缓冲区文本、属性、覆盖图，对窗口进行所需的配置，而不用关心显示结果最终将做出怎么样的改变。当进行这些操作时，Emacs的重绘代码将审视这些修改，然后确定一组有效的界面重绘操作，以便使得界面显示体现出模型的新状态。通过解除Lisp代码管理显示结果的职责，Emacs显著地简化了编写正确扩展插件的任务。

Emacs仅当等待用户输入时更新显示

一个命令将执行多个缓冲区基本操作，对缓冲区的任意部分进行修改，或者对多个不同的缓冲区进行修改。同样，用户可以调用一个事先录制好的、包含一系列命令的键盘宏。对于这种情况，Emacs不是快速地（看起来就像在闪烁）显示出缓冲区的中间状态，而是直到用户输入结束之后更新显示，只展示最后的效果。这样就避免了Lisp程序员尝试对缓冲区和窗口基本操作进行优化，以便展示出更平滑的显示效果。最简单的方法看起来也相当好。

通过更新显示，以自动、高效地体现编辑操作的效果，Emacs彻底地简化了Lisp创建者的工具。有些系统也借鉴了类似的行为模式，最有代表性的是JavaScript，嵌入在Web页面中的程序将对页面内容进行编辑，而浏览器将进行相应的显示更新工作。不过，很多系统要求扩展插件认真处理扩展插件代码和显示更新代码之间的交互，这大大增加了扩展插件创建者的负担。

当Emacs最早出现时，它所针对的用户是那些通过串口线或调制解调器连接到计算机上的独立终端。这样的连接通常不是很稳定，因此文本编辑器是否能够找到一组合理的重绘操作来完成显示的更新操作，对于编辑器的可用性有很大的影响。Emacs在解决这方面的问题上花费了很多的心思，提供了一个更新策略体系，从“快速但受限”的策略到“工作更多但也很彻底”的策略一应俱全。后来还使用了诸如diff之类文件比较程序中常用的动态编程技术，以寻找一组最小的操作集，将原始的屏幕显示转化成新的屏幕显示。直到现在，虽然Emacs仍然使用这些算法来使更新工作最小化，但大部分努力都是多余的，随着CPU处理速度、网络速度越来越快，采用简单的算法也能够很好地完成这些工作。

11.2.3　控制器：Emacs Lisp

Emacs的核心是用自己独立的Lisp语言变体开发的。在Emacs实现的模型-视图-控制器模式中，Lisp代码扮演的是控制器的角色：你调用的所有命令，不管是通过键盘、菜单还是名称，几乎都是一个Lisp功能模块。Emacs Lisp是Emacs能够成功地随着其发展提供大量功能的核心基础。

Lisp的五分钟教程

第一次接触Lisp的开发人员总是会发现它是一种难以阅读的语言。其主要原因是Lisp提供的语法构造要比绝大多语言少得多，但基于这些语法构造的确能够获得更丰富的功能。为了让读者有些直观的感受，我们来看看将Python程序转成Lisp时的一些规则：

1.程序控制结构的写法与功能调用类似。例如，while x*y＜z：q（）将写成while（x*y＜z,q（））。虽然表示方法修改了，但它仍然是一个while循环。

2.中缀表达式的写法与功能调用类似，写出来的结果就像是一个名字古怪的功能函数。例如表达式x*y＜z将写成＜（*（x,y），z）。这里使用的圆括号只是用来分组的（而不是用来指定功能函数的参数），可以将多余的删除掉。

3.现在所有的东西看起来都像是功能调用，包括程序控制结构和基本操作。将功能函数名称之后每个“调用”的左括号移到功能函数名称的左边，然后去除所有的逗号。例如，f（x,y，z）将写成（fxyz）。对于前一个例子，＜（*（x,y），z）将写成（＜（*xy）z）。

将这三个规则组合在一起，如果将用Python编写的代码while

x*y＜z：q（）用Lisp写出来，将变成（while（＜（*xy）z）（q））。

这就是正确的Emacs Lisp表达式，它和Python的表达式的含义是一样的。

当然Lisp的语法还有很多，不过这是它的本质。Lisp中的主要功能术语（诸

如＜和*）和控制结构（诸如while），在上面的例子中都有体现。

下面定义的是一组交互式Emacs命令，用来统计当前选择文本的字数。如果我告诉你“\\＜”在Emacs中是用来匹配单词开始的正则表达式，你或许能够自己读懂这段代码的含义：

（defun count-region-words（start end）

"Count the words in the selected region of text."

（interactive"r"）

（save-excursion

（let（（count 0））

（goto-char start）

（while（re-search-forward"\\＜"end t）

（setq count（+count 1））

（forward-char 1））

（message"Region has%d words."count））））

有一种观点认为Lisp为人类读者在阅读代码、理解代码方面提供的可视化线索太少，以致大家倾向于放弃这种乏味的语法风格，选择那些对基本操作、函数调用、程序结构控制予以更明显区别的语法风格。不过，Lisp中有一些十分重要的功能（我们稍后将会谈及）是依赖于这种一致性的；任何修改这种模式的努力都并不顺利。根据我的经验，许多Lisp程序员都感觉为让该语言能够如此强大、灵活，付出这样的代价也是合理的。

在Emacs中，命令就是程序员指定的负责与用户交互的Emacs Lisp功能函数（再加上定义之前的一些简要注释）。命令的名称，也就是用户在Meta+x之后输入的名称就是x Lisp功能函数的名称。keymaps用来绑定执行该命令按键序列、鼠标点击操作以及菜单项。核心Emacs代码将会在keymaps中查询与用户按键和鼠标操作相一致的定义，然后将控制权分发给相应的命令。和Emacs的视图层所提供的自动显示管理机制类似，keymap分发操作意味着Lisp代码几乎不用负责对一个事件循环中的事件进行处理，这对于很多用户界面的开发人员而言大大减轻了工作负担。

Emacs及其Lisp语言有几个关键的特性：

·Emacs Lisp的机制是轻量级的。小型的自定义和扩展是很容易的：在你用户主目录下的.emacs文件中放上一行表达式，那么Emacs在启动时就会自动载入，可以载入原有的Lisp程序包，设置影响Emacs行为的变量，重新定义键盘序列等。你可以通过几行代码写出一个有用的命令，同时添加其在线文档。（要完成一个有用的Emacs命令的定义，请参见前面的“Lisp的五分钟教程”。）

·Emacs Lisp是交互式的。你可以在缓冲区中输入函数定义或表达式，Emacs将立即执行它。对这些定义进行修改，那么Emacs会马上重新执行它；你无需重新编译或重新启动Emacs。Emacs实际上就是Emacs Lisp程序的一种有效的集成开发环境。

·你自己写的Emacs Lisp程序将是一等公民。Emacs中每个非凡的编辑功能都是用Lisp语言开发的，因此在你自己的Lisp程序代码中，同样可以使用到Emacs本身的基本功能函数和程序库。缓冲区、窗口和其他与编辑相关的对象，在Lisp代码中就是一个平常的值。它们可以作为功能函数的参数、返回值，还可以将它们存储到数据结构等。尽管Emacs的模型层和视图层组件都是硬编码的，Emacs称在控制器层没有为自己提供任何特权。

·Emacs Lisp是一种很完善的编程语言，很适合编写大型的程序（包含成千上万行代码的程序）。

·Emacs Lisp是很安全的。有很多人揭示了许多将导致错误产生的Bug成灾的Lisp代码，但它们并不影响Emacs。Bug引发的问题可以通过其他途径解决。例如，缓冲区中内建的日志机制可以让你撤销预期外的操作。这些Lisp开发给人留下更舒适、更积极的体验。

·Emacs Lisp代码很容易生成文档。在功能函数的定义中可以包含一个docstring（文档字符串），其中的文字用来说明该功能函数的意图和使用方法。Emacs提供的所有功能函数都有docstring，意味着对于指定设施的帮助不会比命令少。当Emacs显示一个功能函数的docstring时，还将提供一个指向功能函数源代码的超链接，使得Lisp代码更加容易阅读。（显然，docstring并不适用于大型的Lisp包，因此这些通常将放在更传统的结构化手册中。）

·Emacs Lisp不是一个模块化系统。取而代之的是通过内建的命名规范来实现在同一个Emacs会话中载入不相关的包时避免相互之间的干扰，因此用户可以相互共用Emacs Lisp代码包，不需要Emacs开发人员进行协调与确认。这也意味着在一个包中的所有功能函数对于其他包而言都是可见的。如果一个功能函数有足够的价值，就会有很多包使用它，并且依赖于其行为细节。

奇怪的是，这样的做法并没有产生人们所预期的大量问题。一种猜测是Emacs Lisp包之间独立性很强，不过在Emacs的标准版本中，大约包括1100个Lisp文件，其中有500个文件使用到了其他包里的功能函数。我的猜测是这些与Emacs一同发布的包的开发人员仔细地处理了与其相关包的兼容性，并且这些开发人员形成了一个紧密的小组，会对那些不兼容的修改进行相互的协商。那些不属于Emacs的包或许就无法做到这点，这些包的开发人员可能会收到加盟的邀请。

11.3　滋长的特性

Emacs不断滋长的特性是其架构的直接成因。下面是一个典型特性的生命周期：

1.当你发现一个想拥有的特性时，尝试实现它是很容易的；Emacs的机制不官僚，从而使进入门槛很低。Emacs为Lisp开发提供了一个舒适的交互式环境。简单的缓冲区模型和显示自动更新机制，使得你可以只关注于手头的任务。

2.当你定义了一个可以运行的命令之后，只需将其放在自己的.emacs文件中就可以使其永久有效。如果你经常使用这个功能，还可以用代码将其与一组快捷键绑定起来。

3.最初你将从一个简单的命令开始，逐渐将演变成一组相互协作的命令集，这时你可能希望将它们收集在一起，封装到一个包中分享给朋友们。

4.最后，有一些比较流行的包将纳入Emacs发行版本中，使其标准功能得到了扩展。

在内建的代码基线中，采用的处理过程是类似的。当拥有编写自定义Emacs命令的经历之后，就将更容易理解Emacs本身的代码了，因此当你发现现有命令中的潜在改进点时，则可以阅读该命令的源代码（前面说过，在帮助文本中就有相应的链接），然后尝试动手对其进行改进。当你在Emacs工作时重新定义Emacs Lisp功能函数，使得测试更加容易。当你修改完成之后，可以将修改版本放在自己的.emacs文件中以供自己使用，也可以为官方源代码提供一个补丁，以供大家使用。

当然，真正的创新想法是很少的。对于经验丰富的用户可言，对Emacs的改进想法会有类似的感觉，看看其提供的文档，可能会发现已经有人实现过了。由于有一群喜欢Lisp的Emacs用户，不断地对Emacs进行调整和修改，至今已经将近20年了，因此大部分人会发现自己所需要的各种功能，都已经有人做过相应的开发了。

不过，不管是通过添加新的包，还是打上用户贡献的补丁，Emacs的发展都是一个民众主导的过程，充分反应了用户的兴趣：存在的功能，不管是显然需要还是很少使用的，都是因为有人编写了它、有人认为它很有用。Emacs维护者的职责除了修正bug、认可补丁之外，还包括添加有用的新操作，换句话说，在社群中已经使用的包中选择最流行的、开发得不错的，将其添加到官方源代码中。

如果这就是全部，那么滋长的特性也就不成什么问题了。不过，它通常会带来两个方面的副作用：程序的用户界面将变得复杂、难以理解，程序本身变得难以维护。Emacs对第一个问题做了一些成功的处理，但对于后一个问题则彻底不管，相对而言更重视程序的功效。

11.3.1　滋长的特性和用户界面复杂性

在评价一个应用程序的用户界面复杂度时，有两个常见的维度：要维护的模型的复杂度，以及操作该模型的命令集的复杂度。

模型的复杂度

在用户自认为对应用程序的模型有所需的理解之前，需要花多长时间来学习？如果模型对用户而言是隐藏的，或者比较隐晦，会影响其应用吗？

微软的Word文档就拥有一个十分复杂的模型。例如，Word能够自动编译文档中一个小节或一个子小节的字数，自动对后续的文本片段进行编号，更新文本中对特定小节的引用。不过，如果需要这些特性按自己的预期执行，那么就需要对Word样式表有很好的理解。对于那些不会对文档内容产生可视影响的操作是容易出错的，例如如何避免对列表进行重编号（另外再举一个例子，你可以问一个文员，在Word 2003中如何“自动更新样式”）。Emacs为了避免此类问题，采用了十分简单的方法：不支持样式表、自动编号小节、页眉/页脚、表格等在现代字处理软件中很常见的功能。它只是一个纯文本编辑器，Emacs的缓冲区只是一个字符串。在几乎所有情况下，缓冲区的所有状态只关心其外观。这样的做法，其带来的结果之一就是使人容易知道Emacs对其文件内容做了什么修改。

命令集的复杂度

在任何时候，发现相关的和有用的操作是否容易？发现一个还未使用过的功能是否容易？在这方面，Emacs的用户界面相当复杂。一个刚启动的不带任何定制的Emacs会话，提供了大约2400个命令和700个快捷键，并且随着使用过程，会话将载入更多的命令和快捷键。

幸运的是，新用户并不是突然面对这些东西，就像UNIX用户不需学习所有的shell命令一样。对于初次尝试Emacs的新用户而言，需要掌握的操作和使用带有图形用户界面的编辑器类似，用鼠标选择某段文本，通过键盘上的箭头键移动光标，通过菜单命令载入和保存文件。不熟悉这些命令，不会影响最基本的、可见的功能的使用。

不过，以这种方式使用Emacs得到的效果肯定不会比选择其他文本编辑器更好。成为专业的Emacs用户必须阅读操作手册和在线文档，以及学习如何有效地搜索这些资源。诸如grep、编译缓冲区中的内容、交互式调试工具、源代码索引等功能是Emacs与其他文本编辑器的主要区别。但是如果不使用它们，这些功能都是不可见的；同时如果你不知Emacs提供了这些功能，也就不会使用它们。

为了使这类探索更加容易，Emacs还提供了apropos命令集，你可以通过输入一个字符串或一个正则表达式来获得一组名字、与名称或文档字符串匹配的命令列表和自定义变量。尽管它无法取代操作手册，但在你对要查询的命令有一些基本概念时，apropos命令是十分有用的。

在此类复杂性方面，Emacs用户界面具有命令行界面的很多共性：提供了大量可用的命令，用户并不需要全部操作它们（甚至很多都可以不掌握），并且为用户发现新功能付出了很多努力。

幸运的是，Emacs社区在命令的命名规范的建立方面做出了很大的努力，因此在不同包之间的一致性方面还是很好的。例如，几乎所有Emacs命令都是非模态的：对于移动、搜索、切换缓冲区、重新排列窗口等标准命令在任何时候总是可用的，因此你无需担心如何“退出”一个命令。再举一个例子，绝大多数的命令在要求用户输入参数时都采用了标准的Emacs设施，以确保不同包之间的提示行为是高度一致的。

11.3.2　滋长的特性和可维护性

显然，当你拥有越来越多的代码时，维护它就需要付出更多的努力。当一个开发人员的Lisp包被选入Emacs标准版本时，主维护人员将邀请这个包的作者一起参与Emacs的后续维护工作，因此随着包的不断增多，Emacs的维护团队也会相应扩大。如果某些开发人员放弃对包的维护职责，那么主维护人员必须再寻找一位志愿者，或者将这个包从Emacs标准版本中去除。

对于这个解决方案而言，关键在于Emacs是一个包的组合体，而不是统一定义的整体。从某种意义上说，从Emacs的动态维护效果上它更像是一个平台（例如操作系统），而不像是一个独立的应用程序。它不是由一个独立的设计团队来安排工作优先级、分配工作任务，而是由一个自主的开发人员组成的社区开发的，每个人都有自己的工作目标，然后通过一个选择和合并的过程，将他们的工作合并到发布版本中。最后，没有任何一个人能够承担整个系统的维护工作。

在这个过程中，Lisp语言扮演了重要的抽象边界的角色。和绝大多数流行的解释型语言一样，Emacs Lisp代码和Lisp解释程序细节、底层处理器架构是高度隔离的。同样，Lisp所提供的文本编辑类基本操作隐藏了缓冲区、文本属性及其他编辑所需对象的底层操作细节；Lisp中的可见特性绝大部分都取决于开发人员长期的贡献与支持。这为Emacs中核心的C程序代码的改进和扩展工作提供了很大的自由度，减少了破坏Lisp包中现有内容的兼容性。例如，Emacs的缓冲区在对文本属性、覆盖图、多字符集提供支持之前，对此前存在的特性提供了良好的兼容性支持。

11.4　另外两个架构

有很多应用程序都为用户提供了添加扩展的机制。从协作软件开发网站系统（如Trac插件）到字处理软件（Open Office的Universal Network Objects），再到版本控制软件（Mercurial的扩展插件），都提供了扩展接口。在此我们将对Emacs和另外两个支持用户扩展的架构进行比较。

11.4.1　Eclipse

虽然很多人都知道Eclipse是一个流行的、开源的、针对Java和C++的集成开发环境，不过它实际上并没有提供任何功能。它只是为各种插件提供了一个框架，为支持各种特定开发功能（如编写Java代码、调试程序或者使用版本控制软件）的组件提供方便的通信机制。Eclipse的架构使开发人员能够将针对某个问题的有效解决方案集成到平台中，以得到一个统一的、功能齐备的开发环境。

作为一个开发环境，Eclipse提供了许多Emacs所欠缺的有价值功能。例如，插件Java Development Tools就对重构和代码分析提供了广泛的支持。与其比较，Emacs只是能够理解正在编辑的程序代码的语义结构，无法提供类似的支持。

如果Eclipse的架构不是处处预留端口，那么它将毫无价值，所有有效的功能都是由插件提供的。在此没有二等公民：最流行的插件也和其他一样，构建在同一个用户界面上。因为Eclipse为每个插件提供了对其输入和显示的底层控制，插件可以根据自己的目的自由地选择模型、视图、控制器层。

不过，这种方法也有许多缺点：

·Eclipse插件开发是不安全的，这是Emacs Lisp代码所具备的。一个充满bug的插件很容易导致Eclipse崩溃或死锁。在Emacs中，Lisp解释程序能够确保用户能中止失控的Lisp代码，在Lisp代码和模型实现之间的强边界可以避免用户数据被破坏。

·由于Eclipse插件之间的接口相对复杂，因此编写一个Eclipse插件更像是为一个深奥精妙的应用程序添加一个模块，而不仅仅是写一段脚本。当然，这些接口正是实现Eclipse功能的基础，但插件的作者将不仅和大型复杂的项目打交道，还将涉及一些头脑简单的项目。

·一个插件需要有足够的样板文件代码，在Eclipse中提供了一个帮助大家编写插件的插件。Eclipse Plug-in Development Environment（Eclipse插件开发环境）能够生成代码的骨架，生成默认的XML配置文件和manifest文件，同时还将创建测试环境中的回收（tear down）机制。此外还提供了一个自动生成样板文件代码的“向导”，它使插件开发人员更易于动手，但它无法降低底层接口的复杂性。

所有这一切使得Eclipse插件成为了一种不太优雅的扩展机制。插件并不是一个能够轻易制造完成的自动化任务，提供的用户界面体验也不太友好。

这让我想起在本章开始处提出的三个问题中的第二个，其中我认为值得考虑的就是关于插件机制的问题：在插件中可以使用哪类接口？它是否足够简单，只需通过脚本化语言就能够完成快速开发？插件开发人员能否在较高的抽象级别上开发，也就是更接近问题域？以及应用程序如何保护数据不被充满bug的代码破坏？

11.4.2　Firefox

在当前深奥精妙的Web应用（如Google Mail、Facebook等）中大量使用了诸如动态HTML、AJAX之类的技术，以提供更加流畅的用户体验。这些应用程序的网页中包含了在本地对用户输入进行应答的JavaScript代码，然后根据需要与底层的服务进行通信。JavaScript代码访问和修改网页内容时使用的是标准的接口，名为文档对象模型（Document Object Model,DOM），还有一些标准用来确定页面的显示效果。所有现代浏览器都在某种程度上实现了这些标准。

虽然Web浏览器不是一个文本编辑器而是一个浏览器，但它的架构和Emacs的架构却有着惊人的相似之处：

·虽然Emacs Lisp和JavaScript在语法层面完全不同，但在语义层面却有许多本质的共性：例如Emacs Lisp和JavaScript都是解释型的、高度动态的、安全的。都是提供了垃圾回收机制的语言。

·和Emacs Lisp类似，经常是先从一小段改变页面局部行为的JavaScript片段开始，然后逐渐发展成更加庞大、深奥精妙的应用。其进入门槛很低，但它也能够解决很多大型的问题。

·和Emacs类似，页面显示的管理也是自动的。JavaScript代码只是对将在网页上展现的文本节点进行修改，浏览器将会根据需要及时地在页面完成相应的更新。

·和Emacs类似，将输入事件分发给JavaScript代码的过程是由浏览器管理的。

Firefox将负责确定事件是针对页面中的哪个元素，然后确定事件句柄并调用它。不过，Firefox进一步应用了现代Web应用程序背后的思想：Firefox自己的用户界面是使用相同的底层代码实现的，它们负责显示网页、处理对其的交互。有个名为chrome的软件包集描述了界面的结构和样式，并通过JavaScript赋予其行为[1]。该架构允许第三方开发人员编写一个对chrome包扩展的用户界面进行扩展的扩展（add-ons）。进一步采用相同的技术，开发人员能够替换Firefox chrome包所提供的标准界面，甚至彻底重新构建其用户界面，例如可以通过这样的方法使其适用于移动设备。

与Eclipse插件类似，Firefox的chrome包中有许多元数据。与Eclipse Plug-in Development Environment（插件开发环境）插件类似，Firefox也提供了一个帮助大家开发Firefox插件的插件。因此在你能够扩展或修复Firefox之前，有大量重要的工作要做。不过，Firefox提供的自动页面显示管理和简化的事件处理机制使得Firefox插件的开发工作量要比开发Eclipse插件小得多。

Firefox开发人员更在改进其JavaScript实现的性能。这不仅对用户访问应用了大量JavaScript脚本的网站是很有意义的，同时也能够使Firefox开发人员将浏览器本身的更多功能从C++移植为JavaScript，它是一种使用更舒适、灵活性更高的语言。因此，Firefox的架构将更接近于Emacs，它自己的控制器层就都是用Lisp语言编写的。

这让我想起三个问题中的最后一个，我可以问各种使用过的插件开发语言一个问题：该插件开发语言是否是为该应用程序添加新功能的最好方法？如果不是，那么是什么原因导致了该限制？是语言本身的缺陷？是和模型层之间的接口过于麻烦？无论是哪种情况，相同的缺点或许会以相同的形式影响插件开发人员，使插件成为二等公民。（对于插件用户界面，也可以问同样的问题。）与Emacs类似，Firefox将其插件开发语言视为架构的核心，最主要的观点是该语言和应用程序之间的关系应该正确地设计。

作为一位热心的Emacs用户，我关心其未来。我对Firefox特别感兴趣的原因是它在很多方面看起来都与Emacs类似：一个实现自动页面显示管理的视图层，一个基于解释性的动态语言的控制器层，以及一个覆盖Emacs各种功能的模型层。如果有人乐于研究Emacs Lisp代码所积累的各种文献，那么可能会在chrome的基础上花一些时间开发出在架构上与Emacs很相似的文本编辑器，不过它将具有更丰富的模型，与当前技术发展结合更紧密。Emacs架构课题的最大价值在于教会我们学会不要遗忘。

[1]显然在chrome中使用的JavaScript代码能够读写偏好选择文件、书签表以及原始的用户文件，从网页中下载的代码是不具备这些特权的。

第12章　当集市开始构建教堂

KDE社区是如何发展ThreadWeaver和Akonadi项目的，以及它们是如何依次成形的Till Adam Mirko Boehm

结构

功能多样性

√模块

√概念完整性

√依赖关系

√修改独立性

√进程

原则与特性

自动传播

数据访问

√可构建性

√增长适应性

√熵增抵抗力

12.1　简介

KDE项目是当今世界上最大的几个自由软件[1]成果之一。在10多年的发展历程中，贡献者来自很多不同的人群，有学生、经验丰富的专业开发人员、沉溺于某种癖好的开发人员、公司人员、政府机构人员等，他们贡献了大量用来解决不同问题、完成不同任务的软件，不仅仅有完善的桌面环境（带有Web浏览器、群件、文件管理器、字处理软件、电子表格及演示工具），还有一些诸如天文学工具之类的专用应用程序。这些应用程序的基础是所有项目共同构建和维护的共享程序库。除了他们首选的KDE开发社区本身成员的东西之外，也使用了许多第三方开发人员的东西，包括商业的和非商业的，以创造出成千上万个附加的应用程序和组件。

虽然KDE项目最初的目标是为开源的UNIX操作系统（特别是GNU/Linux）提供一个集成桌面环境，但实际上KDE的范围是相当广泛的，它所包括的软件不仅仅是各种风格的UNIX中所拥有的，还包括许多在Microsoft Windows和Mac OS X甚至是嵌入式平台中所拥有的。这就意味着，在编写KDE程序库代码时将和许多不同的工具链打交道，应付不同平台的特性，以灵活、可扩展的方式整合系统服务，明智、谨慎地使用硬件资源。加上其程序库拥有大量的目标受众，因此其提供的API必须是易于理解、有用的，并适用于不同背景程序员。对于那些习惯于和Windows平台中的微软技术打交道的程序员，可能会拥有不同的偏见和习惯，对于那些拥有Java背景的嵌入式开发人员、有经验的Mac开发人员也是如此。其目标是使所有程序员都能够舒适、高效地工作，使他们能够快速地解决问题，同时（从某种角度看也是最重要的）受益于他们的贡献，包括建议、改进和扩展。

这是一个很灵活、形式多样和相互竞争的生态系统，绝大多数贡献者都十分乐意通过协作来改进其软件，通过不断地相互评审来提升他们的技巧。大家可以自由地发表意见，争论可能十分激烈。做某件事总有更好的方法，代码总是会被很多聪明的人不断审查。在大学的课堂上，会有很多计算机专业的学生对其实现进行分析。许多公司会从中发现缺陷、发布安全建议。新的贡献者会通过对现有代码的改进来展示自己的技能。硬件厂商会将该桌面移植到手机上。人们对他们的工作以及他们完成工作的方法很感兴趣，并给它取了一个现在大家众所周知的称呼：集市。[2]而且当遇到那些开发、维护大量程序库和应用程序的传统组织所面临的挑战时，它将显得更加自由和狂热。

有些挑战是来自技术方面的。软件将面对不断增长的数据量，而且数据将变得越来越复杂，个体和组织所需的工作流也是如此。当公司或政府管理者想实现产业转型时，还需要和他们使用的其他软件（开源的或私有的）实现整合，例如迁移到SOA（面向服务架构）时就需要做一些适应性修改。对于政府和公司的关键任务将会提出更高的安全要求，需要良好地自动部署大量的软件。新手、小孩、中老年人会有完全不同的需求，然而这些关注点都是有效的，并且是同等重要的。[3]和其他产业相似，一般的开源软件和KDE的应用越来越普及，分布式处理的需求也不断提高。用户显然会要求更易于使用、更清晰、更优美的界面；和软件的交互响应更快、更好，而不是显得过于复杂；同时还希望系统高可靠、稳定，数据更加安全。开发人员希望扩展点对语言中立，API更好维护，提供兼容性更好的二进制程序和源代码，移植路径更清晰，和他们每天习惯使用的商业软件提供的功能更相近。

但对于开源软件项目而言，更容易让人畏惧的是社会性和协调方面的工作更加巨大。充分交流是非常关键的，它可能会受到地区、文化的阻碍，或多或少地反映了某些人的爱好和偏见，也可能只是受到了时空距离的影响。通过站立会议只要15分钟就能结束的讨论，在跨越半球的成员之间通过邮件列表的讨论可能需要花费好几天的时间。达成意见统一的过程可能和做出决策一样重要，它需要将整个团队的意见进行融合。显然，要让大家达成共识，需要更多的耐心、理解和说服技巧。它将征得更多精通数学、物理知识的勇气十足的人的意见，而不是简单的意见修改，因为他们不喜欢和诸如附庸者之类的非程序员交流。基于这样的想法，气氛友好的Akademy年会总是拥有一个良好的体验（参见下一小节）。我们从来没有看到过这么庞大的、由来自世界各地的老年人和年轻人组成的团队。他们来自相互竞争的国家、多样的民族，甚至是来自于不同的世界，不同肤色的人们激烈地但又不失友好地在争论着与C++细节相关的问题。

除了技术、人文方面的话题之外，第三个主流的有影响的话题是开源软件的组织结构。在某些方面，组织结构对于开源软件团队来说是必需的。特别是大型的、知名的软件项目需要拥有商标，接受来自各方的贡献，组织年会等。当这段时间可以从与配偶相处和周末旅行中挤出来时，通常也是该项目被公众认识的时候。从某种角度来看，组织结构（或缺乏组织结构）将决定了社区的命运。到底是团结但略显迟钝，还是疯狂但略显混乱的模式，这是一个很重要的抉择。答案并不重要，因此它涉及了多种竞争项需要选择：采用基金模式还是自由影响模式；稳定的开发过程还是吸引才华横溢的穴居者；[4]可视性、想法共享还是聚焦于给人留下深刻印象的技术产物。组织结构是必需的，不过在管理层级上将有很多不同的选择。有些项目甚至没有董事会，而有些则拥有宣称自己是乐善好施的独裁者。在成功和不成功的社区中都有最终形成该结果的例子。

在我们详细说明两个具体示例项目的技术、社会和组织问题以了解KDE组织之前，先介绍一些KDE相关的背景和历史是十分有价值的。因此在接下来的小节中，我们将介绍KDE社区当前的情况。

[1]它通常也称为开源软件。

[2]http://en.wikipedia.org/wiki/The_cathedral_and_the_Bazaar.

[3]它和完全由市场生成的软件是根本不同的。开源软件能够迎合价值不大的需求，例如，它无须保证所有努力是满足市场销售意图的，也不用证明能够收回投资。这也是KDE为什么比商业公司开发的竞争产品应用更广泛的一个重要原因。

[4]使用这个术语带有很多感情因素。无法否定，我很多亲爱的朋友总的来说将贡献一些阳光的、健康的养料。

12.2　KDE项目的历史和组织结构

KDE或称为K桌面环境，最初是从失望中孕育的。那时FVWM是最常见的桌面环境，Xeyes是屏幕上总会出现的东西，Motif是与XForms竞争的工具集，它们大量吞噬了开发人员的脑细胞，而且没有什么吸引力，KDE的创立是由一个革命性的目标引领的：将UNIX的原生动力和漂亮、养眼的用户体验结合起来。之所以该目标能够达成，是由于挪威的Trolltech公司发布了第一个版本的QT，它是针对C++的面向对象GUI工具集的基础。QT为GUI程序员提供了一种系统的、面向对象的、优雅的、易于学习的、文档齐备的、有效的工具。1996年，当Matthias Ettrich还是Tuebingen大学的一名学生时，第一次提出了其于QT开发完整的桌面环境的想法。该想法很快就得到了约30名开发人员的响应，从那之后，该团队就不断稳定地发展。

KDE 1.0版本是在1998年发布的。尽管其功能从现在看显得不够强大，不过应该以当时的竞争对手作评价标准：那时的Windows 3.1还没有提供内存保护机制，Apple还在努力寻找新内核，Sun还在努力弥补CDE中的缺憾。这时Linux还未被大肆宣传，开源软件的力量还没有被软件产业深刻理解。

甚至在完成KDE 1.0的同时，开发人员已经开始重新设计2.0版本的工作。还缺少很多主要的元素：组件模型、网络抽象层、桌面通信协议、UI样式API等。KDE 2.0是第一个经过理性的架构、设计、实现过程的版本。它曾经考虑过选择CORBA作为组件模型，不过最终还是放弃了。同时也是第一次出现团队动荡。有趣的是，虽然一些早期的核心开发人员离开了，但活跃的KDE开发团队的规模仍然在缓慢且稳定地发展。KDE e.V.，作为KDE贡献者组织，成立于1996年，并在1998年成为KDE的主要提交者。从一开始，该组织就致力于为KDE贡献者提供支持，但不主导KDE的技术方向。他们希望开发过程是活跃的开发人员的工作结果，而不是充分管理的团队。该理念已经被证实是最重要的因素之一，如果不是这样，KDE很难成为开源软件项目中少数几个不受发起人深刻影响的一个。外部支持在KDE中不被视为问题，但很多项目过度依赖于项目主要发起人，当他们离开或失去兴趣时将导致项目停止。正因为如此，KDE没有重蹈覆辙，成为了开源软件世界大力宣扬的项目，拥有着不断持续的开发动力。

在2002年4月，KDE 3.0发布。由于KDE 2.0被认为是设计良好的，因此3.0版本的开发历时6年，经过了5次主要的发布，最终朝着尽善尽美的方向不断演化和完善。基于KDE 3.0技术开发的重要应用程序成为开源桌面环境的标准：磁盘管理程序K3B；通用的、漂亮的音乐播放器Amarok；完善的个人通信套件Kontact。最有意思的是，这些应用程序最初并不是将KDE作为唯一的目标桌面环境的，但都将其作为构建最终用户界面的平台。从3.0版本开始，KDE开始分成两个部分：桌面和环境（通常也称为平台）。但由于KDE仍然受到X11的限制，因此这种分解并不容易被用户认可。这是下一步要做的事情。

在2004年，KDE团队进入了其发展历史上的一段艰苦期。Trolltech打算发布Qt 4.0版本，它和之前的版本以及市场上的其他同类工具集相比，有很大的增强，也有很大的变化。由于该工具集发生了很大的变化，因此从Qt 3.0迁移到Qt 4.0所需的不是修改，而是移植。关键的问题是KDE 4.0是简单地在KDE 3.0的基础上从Qt 3.0移植到Qt 4.0还是重新设计以完成移植。两种方案都有很多支持者，不管采用什么方法都明显引入大量棘手的问题，有大量的工作需要完成。最终的决定是重新设计KDE。虽然现在大家都认为这是一个正确的选择，但它仍然是一种危险的选择，因为这意味着在完成巨大的移植工作之前，KDE 3.0将成为扩展周期的主线。

Qt4.0中主要的新功能需要做些特别的强调。最初Trolltech是对X11版本的Qt采用了GPL和商业双版权模式，现在延伸到了Qt所支持的所有平台，特别是加入了Windows、Mac OS X和嵌入式平台。因此KDE 4.0从某种意义上已经超越了UNIX世界。尽管UNIX桌面仍然是其根基，但为KDE开发的应用程序现在将可以运行在Windows和Mac OS X平台上。这可能会引起一些争议。其中一个主要的观点是，为这些专有的桌面环境开发提供漂亮的应用程序，势必会降低用户从这些桌面环境中迁移到开源软件上的动力。另一个主要的观点是“什么才是我们应该关心的？”更客气一些的说法是“我们为什么要将有限的开发时间投入到非开源的目标系统上呢？”而支持这样做的人则认为，使这些应用程序在各种平台都能使用可以使这些专有平台的用户更容易迁移到开源操作系统上，并逐渐替代其关键的应用程序。这些争论最后的发展完全符合KDE长期以来的魔咒“最终的决定是由大家的工作做出的”。大量贡献者对在新目标平台上的开发产生了浓厚的兴趣，因此最终的结果是，没有任何理由可以剥夺那些期待以久的用户的需求。

为了实现平台无关，KDE 4.0进行了重架构，将其分成了应用开发平台、基于它构建的桌面以及相关的应用程序三部分。当然此时的划分还是很模糊。

这样的历史让我们了解了诸如KDE之样的项目解决问题的方法。这决不是单个开发人员能够解决的，无论他是多么才华横溢。如何能够使知识和经验积累起来，并使其发挥最大的作用？从战略角度思考，如何使一个大型的、来源不同的团队做出困难的决定，如何朝着全部的目标前进，而不会破坏其中的乐趣？如何融合各种观点，使最终的决定符合每个参与者的意愿，以及使KDE像其他开源软件那样成功？换句话说，当大家各自拥有自己想法的时候，如何一起构建大教堂，架构师如何承担起架构的角色，而不至于带上滑稽的帽子。我们之所以从贡献者说起，是因为我们认为开源软件社区首要的是社会组织结构，而不是技术结构。人是最稀有的也是最有价值的资源。

每个项目都有不同的角色，在招募新成员时都没有权威。每个开源软件项目都需要大量的对其宣传的男孩、女孩，以及一批有动力、有技能、务实的黑客、艺术家、管理员、作家和翻译者。使他们像是一个共同的集体的主要原因是他们都需要自发的动力、技能和自我的约束与管理。项目能够吸引不同年龄段的贡献者，从高校学生到退休人士。他们加入项目的动力是成为创建未来技术团队的一员极具吸引力，大家对这些技术感兴趣，并希望推动这些技术，能做更多有意义的事情，而不是写一些最终被抛弃掉的学术论文。功能齐备的开源软件社区是一个极具竞争力的环境。我所遇见过的绝大多数商业性的工程师团队都有很多管制，并对雇员所作贡献的投资做了些政策上的保护。对于开源软件开发人员则不是这样。要将你贡献的代码集成到软件中，唯一的标准是质量，以及有人继续维护它。虽然源代码中可能会暂时存在一些平庸的片段，但在发展过程中都将会被替换掉。代码是公开的，经常被人仔细检查。即使有些代码实现已经拥有很高的质量，但在某些人眼里还不够尽善尽美，那么就会有程序员对它进行改进。由于创新十分频繁，开源软件的开发人员知道自己的作品经常会被别人改掉。这也能够说明为什么开源软件项目的代码质量通常都比商业性项目的代码高，你无法寄希望于代码中的缺陷不被人发现。

令人惊讶的是，很少编码人员会以被用户或媒体认可作为动机。经常，在最终交付之前他们仍会对某些工作感到厌恶。和马拉松选手一样，其满足感来自于内部动机，也就是凭借自己的知识到达了终点线。有时，由于他们不太愿意向公众宣传而使大家缺乏了解，他们大多对歌功颂德没有兴趣。由于它是个人价值的体现，因此在软件项目中是十分自由的，甚至能够对开发目标设置优先级。在选择和决定其个人工作优先级时，代码艺术始终是重要的动机。软件总能完成，是源于有挑战的东西总能引起大家的高度重视。同时，大家对目标和持续引领结果的兴趣，要小于对过程的兴趣。其选择的路径往往和管理良好的商业软件所采用的路径不同，因为商业软件通常要在质量上向最后期限做出妥协。如果认同妥协是一个基本原则，那么通常认为是开源软件项目正处于向商业软件或“不够开源”项目转变的信号。出现这种情况，往往是主要的软件开发成员成立了一个公司来运行该项目，基金合伙人声称将控制主要的贡献者，或者项目本身已经停止了创新，逐渐转成了维护模式。

开源软件项目所承载的各种类型的组织结构都会遇到阻力。贡献者加入KDE项目可能有多种原因，但享受项目的官僚作风肯定不是其中的一个。加入开源软件项目的人，通常都十分清楚使用和保护其自由选择权。同时，大家都认可需要一种组织形式，虽然可能是不太正式的。虽然几乎所有技术挑战对于良好的团队来说都不是问题，但正式的组织结构仍然可以提高效率。许多开源软件项目之所以解散，都是因为策略上的范围决定是由最有影响力的团队成员独断专行的。寻找一种正式的组织形式，是解决项目面临问题的有效方法，此时如何不对未来的技术开发产生影响，是KDE发展历史上的一个关键（或许是唯一的）步骤。实际的情况是，KDE社区寻找到了一种稳定、被认可的组织结构，这对社区的长期稳定发展作了重要的贡献。

在1996年，KDE创建了代表KDE贡献者的KDE e.V.。一个e.V.，或称为“eingetragener联盟”是德国传统的非赢利组织，绝大多数的KDE贡献者当时都聚集在该组织中。驱使成立该组织的主要动力是该项目必须成为一个加入Free Qt基金会的合法主体。Free Qt基金会是Qt制造商Trolltech公司和KDE项目之间的约定，用来确保KDE永远能够将Qt作为免费的工具集来使用。它确保了即使Trolltech停止开发和发布Qt的免费版本，那么最后发布的免费版本的使用也可以无须获得Trolltech授权。由于Qt免费版本最早并不是采用GPL协议，因此该协议是十分重要的。直到现在，该基金会巧妙地解决了版权、开发人员权益等相关问题。该协议之所以重要，是因为KDE贡献者在投身于一个存在商业化风险的项目时难免会犹豫不决，另外KDE作为一个项目也必须拥有合理的权力。在此后不久，KDE e.V.还注册并持有了KDE商标，同时也承担起了举办KDE年会的职责。

关于KDE的架构和设计，该组织始终拥有很大的影响力，虽然它并没有直接管理整个开发过程。由于它假定所有“已完成任务”的KDE贡献者都是其成员，因此KDE e.V.的意见将具有重大影响。同时，它还是Akademy年度的组织者，这也是绝大多数贡献者唯一聚会和讨论的机会。它同时还会吸纳投资，主要是发起成员的会员费和捐赠款，因此还能够对诸如sprint[1]或targeted会议之类的开发活动提供资金资助（有时也不提供）。该组织每年的预算和其产生效果相比，总是令人惊讶的少。开发人员之间的会议经常是自发的，它的资金起到了杠杆效应。积极的贡献者小组总会尽力完成工作，而无需一个概述性的目标描述。

Akademy逐渐成了一个众所周知的协会，甚至是独立于KDE社区的。因为绝大多数的贡献者的确需要一个个人级的会议。由于无法向任何开发活动分配人力资源或资金，很多范围很广的讨论都需要通过这样的会议来进行。通过这样的年会收集决策所需的信息，并进行宽松的协调，这已经成了常态。这个年会通常都是在每年夏天召开，其他开源软件项目的贡献者也会利用这个机会和KDE社区进行交流与协商。2007年的年会上所做的决定之一就是将发布周期改成6个月，这个建议是Ubuntu项目的Marc Shuttleworth建议和拥护的。

Akademy是KDE组织的唯一的全球性会议。除了这场大型会议之外，还有许多由子项目组举行的小规模会议和一些sprint会议。这些会议通常举办更加频繁、区域性更强，同时也更有针对性，因此Akademy通常讨论的是架构性问题，而特定模块或应用程序的设计问题的讨论则是通过这些小会议完成的。有些子项目组通常会在每3个月聚会一次，如KOffice和Akonadi开发人员。

在这些迭代的开发过程中还有相应的高等级或中等级的评审，它们相当高效，同时也能够使开发人员对目标和下一阶段任务有清晰的了解。年会的绝大多数参与者都从中获得了更大的动力，也有效地提高了开发工作的效率。

KDE的组织和结构说明当前对于一个开源项目而言，无法通过一个核心执行组来完成整个项目的组织。其结果就是需要一个迭代的过程来尝试和发现符合核心非技术目标的组织结构，以确保其开源，确保项目基业长青，同时顺应社区的发展需要。对于这个领域而言，自由不仅仅意味着自由的开发、创造开源的软件，同时意味着大家不会受到第三方参与者的干扰和影响。诸如公司、政府组织等外部参与者也会定期参加年会，它们对项目中的发现、经验和贡献也很感兴趣。不过这些利益相关方（Stakeholder）必须避免通过投入足够的资源来获得社区的更多投票权。这看起来有点痴心妄想，不过在其他项目中也曾出现过，KDE社区也明白。因此开源软件项目的永久和健康与是否确保项目的自由度是直接相关的。主要的技术目标，是开发人员和提供资金、物资、组织及其他资源的贡献者的支撑因素，也是比较容易达成的。

其结果是形式一个建立了开发过程的、有生命力的、活跃的、充满活力的社区，贡献者的变动是稳定和健康的，其过程中还充满了乐趣。同时这反过来也对新贡献者产生了更强的吸引力，它也是社区最重要的资源之一。KDE成功地避免了创建越来越大的规范化组织的趋势，同时也没有造就独裁者，大家都是尽可能地乐善好施。KDE是开源社区的原型。

为了理解该社区的实际功能，如何实现架构决策，以及独特的开发过程对技术性输出如何产生影响，我们接下来就进一步分析两个实例：Akonadi，针对KDE 4.0的个人信息管理基础设施层；ThreadWeaver，一个用于高级并发管理的小型程序库。

[1]sprint是Scrum开发过程中的一次迭代，通常称为“冲刺”。

12.3　Akonadi

KDE 4.0平台，既是一个开发平台，也是一个依赖许多“支撑部件”的执行集成应用程序的运行时环境。这些关键的基础设施提供了一些核心的服务，其他应用程序可以简单地在一个现代的桌面环境中自由访问。它提供了一个稳固的硬件交互层，负责向桌面环境提供硬件的相关信息，诸如USB存储设备变得可用、网络掉线等。Phonon提供了一个在程序中很容易使用的多媒体层，通过它可以完成各种媒体文件的播放和用户界面元素的控制。Plasma则提供了一个丰富的、动态的、可缩放的（基于SVG）的用户界面，它要比标准的办公应用更加丰富。

用户的个人信息，包括电子邮件、约会、任务、日志、博客、收藏夹、聊天记录、地址簿等，其不仅包括大量的基础信息（数据的内容）。它还交织着丰富的上下文关系，诸如用户的偏好、社会关系，以及可以学习的工作上下文，这些对于桌面应用程序提供交互体验而言都是很有价值的，有效地利用这些信息将可以提供易用的、深入的、可靠的界面机制。Akonadi框架的目标是提供一个访问和操作用户个人信息、关联的元数据以及这些数据之间关系的服务平台。它将从不同来源收集相关的信息，诸如从电子邮件和群件服务、Web和网格服务以及它所触及的本地应用程序和缓存中收集，并提供相应的访问机制。Akonadi将成为KDE 4.0桌面环境的一个支撑部件，并且我们将会看到，它的目标甚至是超越于此。

在接下来的小节中，我们将介绍这个大型的、功能强大的框架的历史，了解它曾经做过的社会、技术和组织方面的努力，以前作者未来的愿景。同时，我们将进一步审视其技术解决方案，并说明选择这些方案的理由。

12.3.1　背景

在KDE发起小组的开发人员的早期交流中，就开始讨论桌面环境应该为用户提供哪些应用程序才算完整，处理电子邮件，安排日程，管理工作任务、地址簿都被认为是用户显然需要的，而且也是十分重要的。因此KMail、Kaddressbook和KOrganizer（分别是用来处理电子邮件、联系人和工作任务的应用程序）就成为第一批开发的项目，很快也就提供了第一个可用的版本。用户的需求比较简单，接收电子邮件的标准模式是通过本地电子邮件池或POP3服务。电子邮件的内容并不大，电子邮件的文件夹通常是以mbox格式存储（每个文件夹以一个连续的纯文本文件存储）。电子邮件中HTML格式的内容对于KDE所针对的用户群体而言通常是不需要的，各种类型的多媒体内容是很少见的，加密和数字签名也是相当少见的。沿着这一思考线索，在地址簿、日历中的自定义数据格式都是基于文本的，全部信息都将以易于管理的形式存储。要编写一个能够被其他KDE开发人员，以及KDE第一次发布的用户而言，快速引用、功能足够强大的基本的应用程序还是相对简单的。

个人信息管理（PIM）应用程序的早期和持续成功，被证明对于后续的开发而言是一把双刃剑。随着因特网和计算机应用的不断普及，PIM问题领域开始变得越来越复杂。诸如IMAP之类的访问电子邮件的新形式，诸如maildir之类的电子邮件存储格式，都开始被集成进来。很多工作小组开始通过群件服务来共享日程信息和地址簿，或者通过如vcal/ical或vcard之类的新格式存储在本地。存储在LDAP服务器上的公司和大学的姓名地址录逐渐发展到数万条之多。希望使用KDE应用程序的用户开始认识到这些变化，并很快接受了这些新功能。加上参与PIM应用程序开发的贡献者并不多，其带来的结果是其基础架构并没有进行重新思考，也没有随着新功能的增加来进行相应的清理和更新，代码的总体复杂度在不断提高。诸如对电子邮件存储层的访问必须是同步的并且不支持并发，将整个地址簿读到内存中是合理的，用户不会穿越不同时区之类的基本假设在此时仍然是被认同的，也是开发工作的基础，毕竟这些修改所带来的成本是超出时间和资源限制的。这对于电子邮件应用程序KMail而言是绝对的问题，它将会造成后续的演化版本变得令人不快、难以理解、难以扩展和维护，而且庞大和功能臃肿。甚至，这些工作都是由不同的开发人员贡献的，没有人敢对其内核做太大的修改，害怕引发基于他们工作的开发人员的愤怒，这将使他们无法正常地阅读和编写电子邮件。

随着PIM应用程序使用越来越广泛地在不同领域使用，基于这些应用程序的东西也越来越多，不仅仅涉及最初的忠实的开发人员，还吸引了许多新的贡献者。越来越多KDE其他领域甚至是其他开源项目的成员都希望加入这个社区，以便降低学习曲线、减少错误使用的风险，例如它们影响一些企业级的加密功能，即使充分了解也无法自己对其进行测试。另外（或许是部分原因）还有一些非技术的因素，它会使社交气氛（KMail犹为明显）变得不太友好。讨论经常变得激烈、不友好，从个人偏好出发的批评变得越来越常见，尝试加入该团队的贡献者开始发现气氛不太友好。不同应用程序的各自工作几乎是相互隔离的。在KMail的维护团队中甚至出现了一些不太友好的争论。这些对于KDE社区而言是十分罕见的，这也使得其在社区内的声誉无法跻身第一流。

在不断发展的个人应用程序之间，对集成与融合的需求越来越多，用户也越来越希望能够通过一个公共的外壳应用程序访问电子邮件和日程组件，基于这些组件的个人应用也将发生一些变化。他们就开始着手实现更多的交互功能，在接口上达成了一致，就计划和开发进度做了一些交流，开始考虑诸如通过Kontact群件提供的保护机制来标识自己的成果、实现各自负责领域之间的命名统一。与此同时，由于有许多外部的、商业性的利益相关方开始对KDEPIM应用程序和Kontact产生了兴趣，因此纷纷推动KDEPIM社区采用整体的方法实现一致的交流，因此能够更可靠、更专业化地实现相互之间的交互。这些变化有效推动了PIM小组的发展进程，使其逐渐发展成为KDE中最紧密、最友好、最高效的团队。个人之间的差异和过去的怨恨都被抛在一边，新加入的成员彻底地改变了大家的态度。现在，绝大多数的沟通和开发方面的交流都是通过一个邮件列表完成的（＜kde-pim@kde.org＞），开发人员还可以使用一个公共的IRC频道（#kontact），当你问及大家的工作时，得到的回答一定是“KDEPIM”，而不是“KMail”或“KAddressBook”。

在企业环境中，个人信息管理是个关键应用。随着KDE应用程序在很多大型组织中得到了广泛的应用，使得专业化服务、代码修改、扩展、打包等（分发工作的一部分，主要是针对个人应用和部署的场合）需求都在稳定增长，这反过来了也创造了一个提供以上服务的企业生态系统。这些公司自然会首选雇佣最有资格完成这些工作的人，也就是这些KDEPIM项目的开发人员。正因为如此，对于KDEPIM项目中最主要的、活跃的代码贡献者而言，KDEPIM的开发工作已经成为其公司日常工作的一部分。他们中也有一些人不是直接从事KDE项目的开发工作，而是全职的C++和Qt开发人员。他们仍然是志愿者，特别是那些新的代码贡献者，不过核心开发小组都是由专业人士组成的。[1]由于PIM基础设施对于绝大多数计算机用户而言都是十分重要的，不管是个人还是商业环境中都是这样，KDEPIM技术决策过程的指导原则逐渐变得很实用。如果一个想法无法在合理的时间周期内达到可靠的工作成果，它就会被放弃。对于所有的修改，都会仔细地分析它对核心功能的潜在影响，这些核心功能在任何时候都必须处于可运作的状态。对于实验性的、有风险的决策，项目并没有提供太多的空间。从这个角度而言，该项目和绝大多数商业化的和私有的产品团队十分类似，它和KDE中的其他部分以及其他常见的开源软件有些不同。正如前面所说的那样，这样的做法并不永远是积极的，因为它必将会阻碍创新性和创造性。

[1]这是令人惊讶的，因为在我们的直觉中，开源软件都是由拥有大把自由支配时间的学生开发的，但近几年来的事实证明，多半成功的开源软件项目实际上都是由专业人士开发和维护的。Karim Lakhani、Bob Wolf、Jeff Bates和Chris Dibona发表的Hacker Survey v0.73（2002年6月24日，Boston Consulting Group发布，http://freesoftware.mit.edu/papers/lakhaniwolf.pdf）就是一个实证。

12.3.2　Akonadi的演化

KDEPIM社区的会议很有规律，有年会以及其他大型的开发人员聚会，也有一些由5～10名开发人员参与的小型sprint会议。他们会通过几天的时间、聚焦于某个特定问题的方式进行讨论和编码。这些会议为讨论大家关心的主要问题、做出大的决策、制定路线、排定优先级等工作提供了良好的机会。在这些会议中，实现了诸如Akonadi的第一次发布版本、后续的稳定版本的架构奠定了基础。本小节接下来的内容将回顾一下本项目中最重要的决策点，我们将从得出第一个基础性观点的会议开始。

2005年1月，大家在这个每年的冬季会议上碰面时，KDEPIM的基础设施中的有一些部分初步显露出了不胜负荷的迹象。支持联系人、日历信息的多种后端实现的抽象KResources、电子邮件应用程序KMail的存储层中当时所做的一些基本假设都开始变得不再有效。它的假设主要包括：

·想载入地址簿或日历信息的应用程序是十分有限的：也就是说，KAddressbook和KOrganizer是使用这些信息的主要应用程序。同样，它假设只有KMail需要访问系统中存储的电子邮件信息。因此修改通知、并发访问方面的支持是不需要的，或者是很少见的，也就没有提供正确的锁定机制。

·数据总量是十分有限的。毕竟，用户通常需要管理的联系人、约会、任务会有多少？它假设需要处理的数量级是“百”。

·只有C++和Qt程序库、KDE应用程序需要访问这些信息。

·会有多种不同的后端实现“在线”工作，它们可以访问存储在服务器上的数据，而无需将大量数据复制到本地。

·对数据的读/写操作是同步的、足够快速的，锁定用户界面的时间几乎可以忽略不计。

出席2005年年会的人们存在一些意见的分歧，有些人看到的需求是基于当前用户的实际使用场景的，而有些则是从未来的使用场景角度看到的潜在需求，这些东西可能是当前三个主要的子系统的设计无法满足的。数据量在不断上升，源于多个客户端的并发访问变得越来越多，越来越多的复杂的错误场景给系统的健壮性、可靠性带来了更大的压力，使事务存储层与用户界面彻底分离的需求越来越明显。在移动设备上调用KDEPIM程序库时，数据传输将通过低带宽、高延迟的网络上进行，网络连接经常是不可靠的，这些用户数据的访问能力要求已经超出了传统的在应用程序内部的访问，而是在桌面环境中各种的应用都会需要访问它。需要支持的访问不再限于C++和Qt中，还会涉及脚本语言、进程间通信、Web和网格技术所带来的访问需求。

虽然这些高级别的问题和目标曾经是无可非议的，有些开发团队遇到的痛苦是十分具体的，但对每个开发人员而言强度不尽相同，因此大家对如何解决当前紧急的挑战也有着千差万别的意见。其中一个问题是当需要在地址簿中查询一个联系人信息时，需要将整个地址簿载入内存中，因此如果地址簿比较庞大并且包含图片和其他附件的话，速度就会变得很慢，同时会消耗很多内存。由于它是通过一个程序库来访问的，每个进程每次初始化这个程序库时就会创建一个独立的地址簿，而通常在KDE桌面环境中会运行电子邮件、地址簿、日程安排等应用程序，有时还会有约会提醒进程，因此内存中就经常可能出现4个甚至更多个地址簿副本。

为了修正在内存中载入多个地址簿副本的问题，这些应用程序的维护人员提出采用一种基于客户端/服务器模式的方法。更简单的方法是仅由一个进程负责在内存中保存实际数据。当地址簿被载入到内存之后，所有对该数据的访问都需要通过IPC机制，现在最常用的是KDE提供的远程进程调用基础设施DCOP。这样的做法还将隔离访问同一个联系人数据后台服务（如群件服务）的通信，还有人更关心原来的构架。虽然经过了冗长的讨论，关于内存覆盖区的问题已经通过该方法解决了，但还存在一些主要的问题。最显著的是锁定机制、冲突确定还需要在服务层上实现。同时由于现在针对日程安排的API是形式多样的（因此是基于指针机制的），所以对数据进行序列化是十分困难的，这要通过DCOP传输就需要对数据进行序列化。通过IPC接口传输数据始终是十分缓慢的，因此也会引发一些令人关注的问题，特别是用来访问电子邮件的服务，因此它需要提供一个建议信息。

对于内存覆盖区的问题，该会议讨论出了一种更好的解决方案，那就是通过更为灵活的、针对磁盘缓存和内存信息的共享机制，或许可以考虑使用内存映射文件来解决。鉴于改成基于客户端/服务器架构方案的复杂性，大家认为挑战很大，而且无法证明其带来的利益能够抵消妨碍系统正常工作（虽然现在的效果不太令人满意）的风险。作为一种可行的备选方案，Evolution团队采用了Evolution数据服务（Evolution Data Server,EDS）方法被认为是有价值的探索，Evolution是GNOME项目中开发的与Akonadi竞争的PIM套件，它是基于glib和GTK程序库栈，用C语言开发的。

在会议上提出数据服务观点的建议者有些失望，在接下来的几个月中没有太多进展。后来对EDS的基础代码进行了一些尝试性修改，其目的是为KDE调用该程序库提供支持，但最终发现这将涉及C和C++世界的连接问题，特别是思维模式和API样式都存在不同，这样完成的解决方案最好的情况也是不够优雅的，最差的情况则是不可靠、不完整。EDS使用的是CORBA,KDE最初采用的也是CORBA，只不过后来出于种种原因改成了DCOP，因此它并不吸引人。拒绝采用EDS作为KDE新的PIM数据基础设施的关键原因还是技术上的判断，同时也是一些对C的偏见，大家不喜欢C语言实现，认为它的可维护性不好，还有一些“这样做没有创新”的感觉。

到了2005年底，年初讨论的问题开始变得越来越紧急。在诸如桌面搜索代理程序、语义标签、链接框架等都难以访问电子邮件信息。实现电子邮件处理功能的应用程序，都拥有自己的用户界面，在此可以访问搜索出来的电子邮件里的附件，可以打开并对其进行编辑。这些类似的可用性和性能问题开始增加人们对原有基础设施的忧虑。

在印度班加罗尔召开的年会上，负责Evolution的开发团队中的主要成员都参加了，我们就拥有了一次和他们讨论这些问题的机会，同时也可以向他们询问EDS方面的经验。在这些集会中，很明显看出他们正面临着KDEPIM团队所发现的相似问题，他们也正考虑着相似的解决方案，但都不认为在EDS的基础上进行扩展使其支持电子邮件，或者将其移植到CORBA架构上是可行的做法。来自Evolution团队的结论是：如果KDEPIM开发团队针对PIM数据访问构建了新的基础设施，那么他们至少会在概念层面共享其成果，如果它们不是以独立的C++和KDE程序库的形式提供，那么将可能共享其具体实现。

未来可能在整个开源桌面软件中共享这一基础设施，另外如果能够正确地完成该工作，那么将对很多开发社区而言都是有用的，而不仅限于KDEPIM，它也将为客户端/服务器设计思想添上一个重要的砝码。它将以协议或IPC机制的形式提供一个集成点，而不是一个需要链接到程序中的程序库，因此对于其他工具集、语言和开发范型都是开放的。从这一个在KDE和GNOME间共享PIM服务器的场景，我们也可以看到DBUS栈越来越可能成为跨桌面、跨平台的IPC和桌面总线系统，现在它也正被所有桌面环境项目所采用，并通过Freedesktop.org网站实现共享。

另外还有一个跨项目传播的有趣实例，有些PostgreSQL数据库项目的开发人员也参与了那年的年会，并参与了该主题的一些讨论。他们感兴趣的是如何有效地管理、访问以及查询结构复杂、规模庞大的数据，诸如用户邮件、事件、联系人等。他们认为自己的经验和其开发的软件都将成为这样的系统中的重要部分。他们带来了许多诸如搜索效率、类型扩展性设计思想等方面的观点，同时还提出了许多可操作性角度的想法，诸如如何备份这样的系统、如何确保数据的完整性和健壮性等问题。

几个月之后，在德国Osnabrueck召开的年会上关于PIM数据服务器的概念再次提到前台，这次有了更多开发电子邮件应用方面的支持者。这些人都是一年前置疑关于可能会影响性能、增加复杂性[1]观点的人。它所引入的视角超出了KDE项目的范围，他们采用的实际上就是一年前大家所建议的方法之一，只不过当时大家都没有觉悟甚至没有尝试过，这些压力日增的团队关起门来搭建了一个骨架，最终使反对者们重新审视其观点，并认真考虑这一分裂性的修改。

在前一年的这些会议上，通过开发人员们的讨论，使得一个非技术方面的问题得到了答案。团队开始清楚地意识到他们最大的问题是缺乏新的开发人员加盟项目，而其背后的原因就是他们为项目开发的是难以使用的、令人不够愉快的程序库或应用程序。更可怕的是，当前的开发人员不可能永远承担贡献者的责任，而且他们无法将其学习到的东西传给下一代以保持项目的活性。该解决方案让人的感觉是对尽可能融合当前KDEPIM开发人员的经验和知识予以了同样的关注和努力，它可以成为KDE项目下一代贡献者的工作基础，而不仅仅是在构建一个更好的PIM工具。其目标是创建使开发过程更加轻松愉快、文档更齐全、神秘性更低、更现代的开发平台。它希望能够吸引新的贡献者，使PIM领域的创新工作不必陷于令人不快和复杂难解而又不得不用的基础设施中。客户端/服务器方法似乎能够利于该目标的实现。

要注意的是，这一基础性决策将彻底地改写KDEPIM的数据存储基础设施，这也意味着需要重新编写系统中很大部分的代码。我们接受了这一事实，它必将使得聚焦于维护当前代码基线的资源大大减少，以某种特殊方式维护4.0版本，毕竟这么大的一次重构在完成之前会发现很多的变故。也就是说，在KDE 4.0中实际上是没有KDEPIM的，因此将替换上一个有些粗糙但又十分必要的过渡品。

当团队认同这一决策之后，就提出了如下所示的使命描述：

我们的目标是设计一个可扩展的、跨桌面环境的PIM数据及元数据的存储服务，提供并发的读、写和查询访问支持。它将提供唯一的桌面范围域的对象标识和查询服务。

[1]http://pim.kde.org/development/meetings/osnabrueck4/overview.php.

12.3.3　Akonadi的架构

在这次会议中制定的第一份设计草案中，就提到了许多在该架构的后续迭代中仍然存在的关键点。最重要的决策之一就是不使用DBUS来传输实际的负载数据，这虽然是Akonadi中最显然可选的IPC机制。取而代之的是使用了一个名为IMAP的、能够处理成批传输的、独立的传输通道和协议。之所以选择它，主要考虑到它能够对数据传输过程进行控制，因此可以取消冗长的数据传输，这样数据管道就不会阻塞控制管道。基于IMAP的数据传输的负载要比通过IPC机制传输小得多，因为该协议就是面向快速和流式传送大量数据而设计的。而DBUS的传输性能正是放弃它的主要原因之一，在其文档中就已经明确地说明它不是针对这种应用场景设计的。这将可以复用现有的IMAP程序库，在实现该协议时可以减少很多工作，不管是对KDEPIM开发团队还是未来想集成Akonadi的第三方开发团队均是如此。它还可以保留通用的访问邮件内容的功能，不仅仅是Akonadi特定的工具，还可以通过诸如pine或mutt之类的命令行电子邮件应用程序。如果保管在一个系统中的数据将被锁定，无法通过其他手段来访问，那么会增加用户的担心。由于IMAP只识别电子邮件，因此需要对该协议进行扩展，以对其他mime类型提供支持，不过仍然可以保留基本的协议兼容性。在选择数据传输协议时，还曾经讨论过http/webdav，它可以复用诸如Apache之类的现有http服务器，但这种方法并未得到足够的支持。

Akonadi的一个核心观念是为系统中所有的PIM数据和相关的元数据建立一个集中的缓存。然而老框架假定对后端存储的访问通常是在线式的，而Akonadi引用了本地副本机制，这样当需要向用户显示数据时能够马上提供，例如它可以保留许多可能已经获得的数据，以便避免不必要的重新下载。应用程序希望能够直接从内存中获得当前所需显示的信息（例如，在电子邮件应用程序中，只有当前文件夹中的少量电子邮件的头信息是需要显示的），而无需自己维护磁盘缓存。这样就需要使缓存是共享的和保持一致的，减少应用程序对内存的访问，并采用惰性加载（lazy-loaded）等优化技术，不过这样做用户无法马上看到数据。由于缓存是一直可用的，虽然可能不完整，但在绝大多数情况下仍然是可离线使用的。这大大加强了基于不可靠、低带宽、高延迟网络时的健壮性。为了支持非拥塞性的并发访问，该服务器设计成了为每个连接保存一个执行上下文（线程），所有层要考虑的并发上下文的数量可能很庞大。这意味着针对状态的操作将引入事务性语义，需要进行严格的锁定，能够检查可以导致状态不一致的交叉性操作等。它同时也是选择管理系统内容的磁盘存储技术时所需考虑的关键约束，它也必须能够支持高强度的并发读、写操作。由于状态可能会在任何时候被当前会话（连接）所修改，因此需要一种通知机制，可靠、完整、快速地通知所有当前修改所影响的连接终端。这也是将低延迟、低带宽但高相关性的控制信息和高带宽、高延迟（由于存在潜在的服务器往返通信）、时间性不强的批量数据分开的另一个理由，这样就可以避免这样的通知信息被排在了一个很大的电子邮件附件之后之类的问题。如果应用程序是并发的，而在数据传输的同时还将发送带外的通知信息时则更重要。虽然这对于应用Akonadi的潜在应用程序而言并非主流的场景，但有足够的理由认为未来的应用程序将会有更多的并发，而不仅仅是因为存在诸如ThreadWeaver之类的工具，我们在本章的下一个小节中将介绍它。在针对KDE的Akonadi访问程序库中的一部分高等级的辅助类中就已经用到了这一功能。

Akonadi中还有一个基础性的视角，它在其第一次迭代的设计中就已经存在，那就是用来访问特定类型存储后端（如群件服务器）的组件将以单独的进程运行。这样做有好几个好处。与该服务器潜在的错误、缓慢或不可靠的通信都不会影响整个系统的可靠性。代理[1]可能会崩溃，但不会使整个服务器崩溃。如果和服务器之间的同步交互更方便，或者从其他后端获取数据只有这一种方法，那么它将被阻塞而不会阻塞其他与Akonadi的交互。它们可以通过链接第三方程序库的方法，以避免对核心系统的依赖。它们可以独立地授权。它对于需要访问开源软件之外的程序库时十分重要。它同时也将正确地隔离Akonadi服务器的地址空间，因此减少了潜在的安全问题。这样第三方编写它也更简单，也能够更容易地在已运行的系统上部署、测试，无论什么编程语言都能够匹配，同时也提供了DBUS和IMAP的支持。当然它也存在缺点，那就是必须对Akonadi存储空间中的数据进行序列化，存储空间是跨进程边界的。不过这在实践中很少考虑，其原因有两个方面。首先，它是在后台发生的，从用户的视角看不会对UI层面有任何干扰。其次，不管是本地已有的数据，还是在缓存的数据，或者从网络套接字（Socket）中获取的数据，当用户请求时都将把数据传给未解析的Akonadi服务器套接字上，在很多情况下甚至无需复制它。如果用户请求的数据是其中的一部分，那么就将能够以最快速度呈现，避免了过长的等待时间。在多数情况下，代理和服务器存储之间的交互性能并不是关键的。

从并发的观点看，Akonadi有两层。在multiprocessing（多处理）层，每个Akonadi用户（也就是一个应用程序或一个代理）都有自己独立的地址空间和资源获取上下文（文件、网络套接字等）。这些进程可以打开一个或多个到服务器的连接，每个都将分别以一个线程表示。为了平衡线程和进程的好处，最终决定都使用：进程更健壮、资源更独立、实现了安全的隔离是最重要的；而线程采用的地址空间共享机制是为了提高性能的需要，同时其代码也可以通过Akonadi服务器实现本身对其进行控制（而它被假定很少会引发可靠性问题）。

通过合理的努力为系统添加新数据类型的支持，也是最初希望实现的属性之一。从理想的角度看，data management（数据管理）层应该是完全类型无关的，针对每种类型数据（最初的电子邮件、事件和联系人，以及后来的RSS文件、即时通讯聊天记录、书签等）的内容和标签相关的知识应该集中放在一处。虽然最初对于实现该目标是很清晰的，但如何实现却没有明确的构想。当该目标实现时，核心系统本身以及访问程序库API都经历了几次设计迭代。在本小节中，我们还将对其最终结果做进一步说明。

在最初关于架构的讨论中，团队以很传统的方法构建了一个大蓝图，通过自底向下的层分离了各种关注点。他们讨论中在白板上绘制出来的架构快照如图12-1所示，在最底下是一个存储（持久）层，上面是访问它的逻辑层，再上面就是传输层和访问协议层，最后就是应用程序域。

虽然关于应用程序应该采用什么样的API来访问存储层（反对使用代理或资源分发来访问）有许多争论，有些人认为只需要一种访问API，所有实体都通过它来访问存储层，无论它的目标是提供数据还从以用户视角来使用数据。大家很快发现这种选择是更好的，它使得这一切更加简单、也更加均衡。代理所执行的任何操作，都是由数据变化通知触发的，这些都将由负责监控系统其他部分的代理获取。无论资源是否对应用程序有额外的需求（例如，能够向存储层提交大量数据而不会引发提醒消息风暴），实现统一API的努力仍然是足够通用的，也是足够有用的。无论如何，不管是应用程序访问还是资源的需求都需要使API保持简单，这样才能够使第三方能够提供额外的群件服务器后台，也能够使应用程序开发人员接受Akonadi。针对性能、错误校正等特定情况的处理都应该尽可能放在后台处理。要避免出现提醒消息风暴，通过可配制的提醒消息压缩和更新监视系统来有效地解决它，这样系统的用户可以按特定的时间周期来订阅。

图　12-1：Akonadi框架最初的层划分草案

图12-2所示的就是新版本的高层级架构图，它映射出来的想法是将系统的各个层描绘为同心圆或圆弧。

根据前面描述的需求，最底层（或最里层）使用关系型数据库构建显然是最简单的。至少针对实际PIM数据项（如获取时间、本地标签、每个文件夹的策略）的元数据而言，甚至没有考虑过其他解决方案，因为它们的名称是有限的、有类型的、结构化的，可以受益于快速、已索引的随机访问和高效的查询。而对于负载数据本身（如电子邮件、联系人等），它们都是存储在磁盘上的。这一决策并不是那么清晰明确。由于这方面信息的存储应该是类型无关的，如果需要在数据库表中存储的数据无法确定其结构类型，那么通常会被迫以BLOB字段形式存储。当涉及非结构化数据（从数据库的视角看）时，数据库的优势只能够发挥出一小部分。进行有效的索引基本上是不可能了，因为这需要能够对数据字段的内容进行解析。因而对这些字段进行的查询无法高效完成。而且预期的访问模式似乎也不是数据库所喜欢的：采用一种能够处理包含大量本地引用的连续数据流是更可取的方法。对于某个数据项无法使用数据库，意味着对存储层的事务性操作必须手动实现。针对这个问题，大家提议的方法是对maildir标准定义的原则进行扩展，其本质是通过文件系统中原子性的重命名操作来实现无锁的ACID访问[2]。对于最初的实现，决定采用数据库来存储所有的数据和元数据，并决定当以后能够更好地定义搜索需求时再对其进行优化。

图　12-2：从里向外，而非自底向上

将Akonadi存储服务变成存储用户个人信息的权威的缓存，它可以有效地提供和加强高级缓存生命周期管理。通过缓存策略的概念可以实现很精密的控制，包括应该保留哪些数据项、在多长时间内可以输出给用户，并提供了多种多样的使用模式。如果位于应用谱的末端（例如，在网络链接很差、存储很受限的嵌入式设备上），那么开发一个除了头信息之外其他都不存储的策略就很有意义，这样做是很轻量级的，而只等到需要显示时才下载完整的信息，不过对于已下载的数据项将仍然存储在RAM中，直到连接占用了过大的内存或者关机时清空了内存。对于笔记本电脑而言，经常会遇到没有网络连接或连接不可靠的情况，但通常会有很丰富的磁盘空间，那么就可以尽可能多地进行预先的缓存，以便在离线的情况下也能够使用，并且将每隔几天、几周甚至是几个月才清除一部分文件夹。而对于桌面工作站而言，通常都能够通过宽带Internet或本地网络连接到群件服务器上，可以快速地在线访问服务器中的数据。这就意味着除非用户出于备用或节省带宽的想法，希望在本地保留一个副本，否则就可以对缓存采用十分被动的策略，或许只保存已下载的附件以供本地索引和引用。这些缓存策略可以按文件夹、账户、后端进行设置，并且可以由服务器的一个线程中运行的线程来强制执行，对于数据库的低优先级的、常规的查询结果可以根据所有适用的策略进行清理。

在2007年的会议上，发现了架构中一个主要的缺失，那就是如何处理搜索和语义性链接。KDE 4.0平台已经通过Strigi和Nepomuk两个项目为扩散式索引（pervasive indexing）、富元数据管理和语义Web提供了一些强大的解决方案，将其集成到Akonadi是十分有趣的。不清楚的地方在于，是将负责向Strigi提供用来全面索引的数据的组件实现为一个代理，再通过一个独立的进程处理源于内核的消息提醒；或者为了获得更好的性能，将其集成到服务器应用程序中。由于至少全文索引信息会存储在Akonadi之外，那么就将引出一个相关的问题，那就是如何分解搜索查询，如何集成源于Strigi和Akonadi自身的查询结果，以及如何将查询传给能够提供在线查询的后端服务器系统，诸如LDAP服务器。同样，还讨论了应该使用什么样的策略来和Nepomuk划分职责边界，例如添加标签的工作是否可以完全委托给它。从某种程度上看，这些讨论仍旧在继续，其涉及的技术也随着Akonadi的发展而不断变化，而当前使用的方法仍然能够有效地应用到产品中。在本书编写时，有一些负责向Nepomuk和Strigi提供数据的代理；这些是独立的进程，它们和所有其他客户端、资源使用相同的API来访问存储层。进入的搜索查询将以XESAM或SPARQL表示，它们分别是Strigi和Nepomuk中的查询语言，在其他搜索引擎（例如Beagle）中也有相应的实现，然后将其通过DBUS转发出去。这一转发操作发生在Akonadi服务器进程中。其结果将通过DBUS返回，并以标识符列表的形式提供，Akonadi根据这一列表能够生成源于存储层的实际数据项，并将其返回给用户。此时数据层本身并不执行任何搜索和内容索引操作。

针对KDE的、C++访问程序库的API的发展成熟还需一段时间，最主要的原因是它还没有在程序库中清晰地解决服务器如何从类型无关的基础上输出多种类型信息。通过2007年4月的会议，它明确了对访问程序库进行扩展以支持新类型的方法，也就是被称为序列化器（serializer）的插件。这是运行时可载入的程序库，它能够将某种特定格式的数据（以mime类型标识）转化为二进制表示，以便以BLOB类型字段存储在服务器上，同时也能将二进制表示的数据转成特定格式的数据，还可以从序列化后的数据恢复成内存中的表示。它与添加新的存储层后台支持是正交的，例如需要添加一种新的数据格式，它是由一个新的资源进程（一个代理）实现的。这个资源的职责就是将服务器下发的内容转化为有类型的、内存中的表现形式，它知道如何处理这种表现形式。然后，再利用一个序列化插件，将它转化为二进制数据流，以便能够存入Akonadi存储库，并在访问该库时进行反向转换。这个插件还能够将数据分成多个部分，以便对局部访问提供支持（例如只访问邮件正文或附件）。该程序库中的核心类是Akonadi：Item，它表示的是存储层中的一个数据项。它拥有唯一ID，它是一个全局标识符，用来标识在用户桌面中的实体，并作为语义链接的一部分，与其他实体关联（例如remote标识符）。它将映射到一个源存储位置、属性、一个数据负载，以及其他一些有用的基础设施，如标志位（flag）或修订计数器。属性和负载都是强类型的，用来设置和获取它们的方法都是模板化的。Akonadi：Item实例可以自己赋值，是易于复制的、轻量级的。数据项可以对负载和属性的类型进行参数化，而无需使用模板类本身。模板逻辑使其变得有些棘手，但得到的API是十分易用的。负载被假定是一个值类型，以避免赞成所有权语义的不清晰。当负载需要多态时就需要一个指针，或者当它已经是一个用来处理特定类型数据的、基于指针的程序库（例如libkcal程序库，它是KDE中用来实现事件和任务管理的），都将共享指针（如boost：shared_ptr），用来提供值的语义。为负载设置一个原始指针有助于检查模板的特化，还能够设置运行时断言。

下面的示例展示了在Akonadi中添加一种新的数据类型支持是多么简单，在通常情况下，已经有能够处理此种格式数据的程序库。它展示了联系人的序列化器插件的源代码，KDE库将联系人称为KABC：Addressee对象：

bool SerializerPluginAddressee：deserialize（Item＆item，

const QByteArray＆label，

QIODevice＆data，

int version）

{

if（label！=Item：FullPayload||version！=1）

return false；

KABC：Addressee a=m_converter.parseVCard（data.readAll（））；

if（！a.isEmpty（））{

item.setPayload＜KABC：Addressee＞（a）；

}else{

kWarning（）＜＜"Empty addressee object！"；

}

return true；

}

void SerializerPluginAddressee：serialize（const Item＆item，

const QByteArray＆label，

QIODevice＆data，

int＆version）

{

if（label！=Item：FullPayload

||！item.hasPayload＜KABC：Addressee＞（））

return；

const KABC：Addressee a=item.payload＜KABC：Addressee＞（）；

data.write（m_converter.createVCard（a））；

version=1；

}

通过设置负载类型的setPayload以及Item类中的hasPayload方法，开发人员能够直接、方便地使用其数据类型程序库中的原生类型。与存储层的交互通常表示为一个任务，这是command（命令）模式的一个应用。这些任务将跟踪一个操作的生命周期，提供取消操作的功能并能够访问错误上下文，并允许单步跟踪。通过Monitor类可以监控存储层中你感兴趣的范围内的修改，如某种mime类型、某个集合，或者甚至是某些特定数据项。下面这个源于电子邮件通知Applet程序的示例中展现了这些概念。在该例中，负载类型就是多态的，并封装在一个共享的指针中：

Monitor*monitor=new Monitor（this）；

monitor-＞setMimeTypeMonitored（"message/rfc822"）；

monitor-＞itemFetchScope（）.fetchPayloadPart（MessagePart：Envelope）；

connect（monitor,SIGNAL（itemAdded（Akonadi：Item,Akonadi：Collection）），

SLOT（itemAdded（Akonadi：Item）））；

connect（monitor,SIGNAL（itemChanged（Akonadi：Item,QSet＜QByteArray＞）），

SLOT（itemChanged（Akonadi：Item）））；

//开始下载最初的电子邮件，以显示第一封电子邮件

ItemFetchJob*fetch=new ItemFetchJob（Collection（myCollection），this）；

fetch-＞fetchScope（）.fetchPayloadPart（MessagePart：Envelope）；

connect（fetch,SIGNAL（result（KJob*）），SLOT（fetchDone（KJob*）））；

……

typedef boost：shared_ptr＜KMime：Message＞MessagePtr；

void MyMessageModel：itemAdded（const Akonadi：Item＆item）

{

if（！item.hasPayload＜MessagePtr＞（））

return；

MessagePtr msg=item.payload＜MessagePtr＞（）；

doSomethingWith（msg-＞subject（））；

……

}

[1]与Akonadi服务器交互并读、写其数据的实体称为代理（agent）。数据挖掘代理就是一个很常见的例子。特定用来处理本地缓冲和远程服务器间数据同步的代理称为资源（resource）。

[2]http://pim.kde.org/development/meetings/osnabrueck4/icaldir.php.

12.3.4　第一次发布和未来

2008年1月，当开发小组再次聚集在寒冷、多雨的Osnabrueck时，就有开发人员展示了使用Akonadi的第一个应用程序，他们是受邀参加的。Mailody（KDE中默认电子邮件应用程序的一个竞争者）的作者在Akonadi能够帮他们开发出更好的应用程序之前就决定选择它，并且成为第一批尝试使用它的工具和API的团队。他们的反馈是十分有价值的，对于大家找出哪些过于复杂、哪里要进一步细化、哪些概念还没有良好的文档化或良好地实现都是十分有帮助的。Akonadi的另一个早期采用者Kevin Krammer也参加了会议，他曾经着手开始完成一些有意思的任务，希望能让KDE中针对PIM数据的遗留程序库进行修改，通过兼容的代理和资源来使其能够访问Akonadi。他所遇到的问题是如何在API中开些小口，并验证至少所有的功能能够在新工具中存在。

本次会议最值得注意的讨论结果是决定与IMAP在协议级实现向后兼容。它现在已经逐渐变得不再是最初的支撑Akonadi服务器能力的、仅限于电子邮件的标准，而是一个符合标准的IMAP服务器，承担起了电子邮件访问的功能，提供了更大的效益。IMAP协议是一个很好的起点，它的许多概念都在Akonadi访问协议中保留下来，但它不再有理由称为IMAP。或许在该服务器的后续版本中会回到该机制，或许会以兼容的代理服务器模式实现。

由于马上要发布KDE 4.1版本，因此在2008年3月大家都再次聚集在一起，以便在第一次公开发布之前再次对API做一次详细的评审，以便在可预知的未来能够保证其稳定性和二进制兼容。经过两天的讨论，出现了数量惊人的、大大小小的问题，有些文档缺少了，有些实现模式十分怪异，发现了一些不适当的命名模式，因此在接下来的一周内他们做了许多矫正工作。

在本书写作时，KDE 4.1版本马上就要发布，Akonadi团队兴奋地看到了KDE社区中许多应用程序和程序库的开发人员予以反应，这些都是其目标客户。有兴趣为不同的存储后面编写资源的人在逐渐增加，有人开始为Akonadi添加对Facebook地址薄、Delicious书签、MS Exchange电子邮件、基于OpenChange程序库的群件服务器、博客的RSS文件等的支持。现在很高兴地看到整个社区能够简单、深入、可靠地创建针对新数据格式的支持；能够高效地查询；能够对数据进行注释、实现数据的相互链接、为其创建语义和上下文；能够利用它使用户在其软件中完成更多功能，更好地使用它。

对于迄今仍未实现的优化工作，存在两种相关的观点。第一个是避免将负载数据以blob格式存储到数据库中，而是只在数据库表中存储指向文件系统的URL，而数据本身则直接存储在前面提及的文件系统中。采用这种方法之后，就可能避免将数据从文件系统中复制到内存中，避免通过socket通信将其分发到客户端（它涉及另一个处理器），从而导致虽然发布一份副本却在内存中创建了第二个副本。这样做之后，只需向应用系统传送一个文件句柄，以便其在内存中建立指向该文件的映射。无论如何，这样做不会影响健壮性、一致性、安全性以及API约束，整个架构的运转仍然和原来一样。另一种思路是使用MySQL中针对blob数据流的扩展插件，它承诺可以在保留通过关系型数据库API能获得的好处，同时实现与裸文件系统访问相似的性能。

虽然服务器和KDE客户端程序库会随着KDE 4.1同时发布，但其目标仍然是希望能够尽可能地实现开源软件领域的共享。最终，创建了一个放在Freedesktop.org上的新项目，在开发完成时服务器也就迁移到了这里。其DBUS接口的命名就已经采用了桌面环境无关的风格；该服务器唯一依赖的就是Qt程序库的4.0版本，它是Linux Standard Base（Linux标准基础）规范的一部分，它在Linux、Windows、OS X及嵌入式系统（包括Windows CE）中均采用了GNU GPL版权协议。下一个主要阶段将实现第二个访问程序库，例如用Python编写，将会引入许多能够引入有价值效果的基础设施，也可能使用Java语言来编写。

12.4　ThreadWeaver

ThreadWeaver现在是KDE 4.0中的一个核心程序库。在此谈及它的主要原因是其起源在很多方面与Akonadi项目形成了鲜明的对照，可以提供一些有趣的比较。ThreadWeaver是负责并行操作调度的。该项目的提出时间就是大家觉得基于KDE所使用的程序库（也就是Qt）在技术上已经能够实现它的时候。看起来有许多开发人员具有这方面的需求，但只到Qt 4.0逐步成熟和逐渐变成主流时才出现。现在，在Koffice、Kdevelop等主要应用程序中都在使用它。它通常应用在高伸缩性、高复杂性的软件系统中，在这些系统中对并发性、带外（out-of-band）处理的需求越来越明显。

ThreadWeaver是一个并发作业调度程序。其目标是在多线程软件系统中管理和分配资源的使用。其第二个目标是为应用程序开发人员提供一个实现并行机制的工具，与那些在开发GUI应用程序时所使用的方法类似。这些目标都是高层次的，还有一些第二层次的、规模更小的目标：避免过强的同步，为协作的数据串行存取提供手段；充分利用现代C++程序库的功能，如线程安全、隐式共享以及线程之间的信号/槽连接（signal-slot-connections）等；将处理器元素和UI中相应展现的委托进行分离，以集成应用程序的图形用户界面；实现运行时的工作队列节流，以适应当前系统负载；使问题得以简化；等等。

ThreadWeaver程序库最初是针对那些开发事件驱动的GUI程序的开发人员而开发的，而后来变得越来越通用。由于GUI程序是由一个核心事件循环驱动的，因此无法在主线程中处理耗时的操作。如果这样做，那么在操作完成之前将会冻结用户界面。在某些窗口环境中，用户界面只能通过主线程绘制，或者窗口系统本身就是单线程的。因此要想实现反应迅速的、跨平台的GUI应用程序，最自然的方法就是在工作线程中执行所有操作，而在必要时通过主线程更新用户界面。令人惊讶的是，在用户界面上对并发性的要求远比直观感觉少得多，虽然在OS/2、Windows NT和Solaris操作系统都曾经强调过。多线程编程更加复杂，需要对编写的代码的实际运行过程有更深入的理解。多线程看起来是软件架构师、设计师已经良好理解的主题，但对软件维护人员和经验不足的程序员则并不容易理解。另外，有些开发人员认为对绝大多数操作而言，同步执行已经能够满足其执行速度需要，即使是那些从已挂载的文件系统中读取信息之类的操作，它要比CPU中进行处理的操作慢几个数量级。这些误解只是在特殊的环境下产生的，那时文件系统经常处于休眠状态以节约电力，但当文件系统位于网络中时一切都发生了改变。

在接下来的几个小节中，我们从基础概念开始，逐渐介绍该程序库的架构。在本章的末尾，还将说明它是如何找到进入KDE 4.0方法的。

12.4.1　ThreadWeaver简介：当其载入一个文件时有多复杂

要说服开发人员使用并发机制，就需要为其提供便利。以下是在GUI程序中执行一个操作的典型例子，将一个文件载入到内存缓冲区中，然后对其进行处理并显示相应的结果。在命令式程序中，每个操作步骤都是阻塞型的，其复杂较低：

1.检查文件是否存在、可读。

2.打开该文件读取。

3.将文件内容读取到内存中。

4.对它们进行处理。

5.显示处理结果。

要提高用户友好性，可以在每一步骤之后在命令行中显示一条进度消息（如果用户选择了verbose模式）。

在GUI程序中，各种操作将以不同形式体现，因为在这些操作步骤中都需要能够更新屏幕，用户也希望提供一种取消操作的方法。尽管这听起来有点难以置信，甚至在最近的一些关于GUI工具箱的文档中还提出“减少事件检查”的方法。其观点是在执行上述步骤时周期性地检查事件，以完成必要的更新或取消操作。在这种场景下需要十分仔细，因为应用程序状态可能会出现意外的改变。例如，用户可能决定关闭程序，而不会注意到程序正在操作的调用栈中检查某个事件。简单来说，这种轮询方法从来没有良好地运行过，而且也是超常规的做法。

当然更好的方法是使用线程。但如何没有可用的框架来支持，通常会产生不可思议的实现。由于GUI程序是基于事件的，每一步骤都是由事件列出的，并且会有一个事件来说明该步骤已经完成。在C++中，通常会使用信号作为通知机制。有些程序会采用以下方法：

1.用户发出载入某文件的请求，它将通过一个信号或事件来触发一个处理者方法。

2.此时将执行打开并载入文件的操作，然后会链接上一个通知操作已完成的方法。

3.在这个方法中将开始对数据进行处理，并链接上第三个方法。

4.这最后一个方法将完成结果的显示工作。

这组层级式的句柄方法无法很好地跟踪操作的状态，并且通常会有错误的倾向。这同时也暴露了将操作和视图分离的缺点。不过，在许多GUI应用程序中仍然经常看到它的身影。这种情况就是ThreadWeaver能够发挥作用的地方。使用了作业机制之后，其实现将采用如下所示的步骤：

1.用户发出载入某文件的请求，它将通过一个信号或事件来触发一个处理者方法。

2.在这个句柄方法中，用户将创建一个作业序列（该序列是一个作业容器，它将根据添加作业的顺序来执行各个作业）。他将添加一个载入文件的作业，以及一个处理文件内容的作业。序列对象本身也是一个作业，当其包含的作业完成时将会发出一个信号。直到此时，还没有开始执行任何操作；程序员只声明了需要按顺序执行哪些作业。当整个序列设置完成时，用户将其放到应用程序的全局作业队列（一个惰性初始化的单例对象）中。这时将通过工作线程自动执行整个序列。

3.当收到序列发出的done（）信号时，就表示数据做好了显示的准备。

这里揭示了两个方面的信息。首先，我们将一次性地声明所有要执行的每个步骤，然后再执行它。这将大大减轻GUI程序员的工作，因为它是一个开销不大的操作，可以很容易地通过一个事件处理者方法来执行。同步模式所带来的主要问题将在很大程度上被解决，而这是通常一个简单的约定实现的，那就是只有当队列化的线程准备好之后才会接触作业数据。由于工作线程都将访问作业数据，因此对数据的访问将是串行的，不过是以合作模式的。如果程序员想向用户展示进度信息，通过该序列在每个作业完成时产生的信号就可以解决（信号在Qt中可以跨线程发送）。执行过程中GUI仍然可以响应用户的其他操作，能够将作业从队列中删除，也可以取消所有操作。

由于采用这种方法实现I/O操作十分简单，因此程序员很快就采用了ThreadWeaver。它以一种很漂亮、便利的方法解决了这个问题。

12.4.2　核心概念与功能

在前一个示例中，我们提及了作业序列。接下来，我们来看看该程序库中提供的其他构造。序列是作业集合的一种特定形式。作业集合是一个容器，其中排队了一组由原子性操作构成的作业，对于程序而言它们是一个整体。作业集合是复合体，也就是它本身也可以实现为一个作业类。在ThreadWeaver中只实现了一个队列操作，也就是获取某个作业的指针。复合的作业有助于使队列API最小化。

作业队列将可以确保其包含的作业以正确的顺序执行。如果声明两个作业之间有依赖关系，那么就意味着依赖作业只能在被依赖作业执行完成后才能执行。由于我们可以用m：n格式来声明依赖性，在这种声明格式中，可以建模相当多针对依赖操作所预期的控制流（它们都是有向图，无法定义作业循环）。只要执行路线是有向的，那么就可以完成作业甚至是作业队列的执行。呈现一个Web页面就是一个典型的实例，在处理HTML文档本身时，锚元素只会被识别一次。然后为获取和准备每个链接元素添加一个作业，而负责显示的最后一个作业将取决于这些准备性作业。此外，在此无需互斥机制。

这种依赖关系和诸如ThreadWeaver之类的调度系统相比，其区别在于它只是并行处理的一个工具而已。它有效地减轻了程序员关于如何最好地将每个子操作分配到线程中的压力。即使是采用如Future模式[1]之类的现代概念，程序员仍然需要决定操作执行的顺序。基于ThreadWeaver，工作线程会尽快地执行所有可能的作业，其中并不存在未确定的依赖关系。由于并发流的执行具有天生的不确定性，因此通过手动定义执行顺序，其灵活性是不可能很高的。在此可以应用更好的调度程序，计算机科学通常倾向于避免这一方式，而在经济学领域却经常使用随机系统分析来支持它。

你也可以通过优先级来控制执行顺序。它使用的优先级系统十分简单：队列中的每个作业都指定了一个整型的优先级，工作线程会首先执行优先级最高的那个。由于作业基类的实现应用了装饰器模式的，因此修改作业的优先级可以通过编写一个装饰器类来实现，这样既修改了其优先级又不用触及作业的实现。优先级和依赖关系的组合使用会产生有趣的结果，这在稍后就会提及。

ThreadWeaver并不直接依赖于队列行为的实现，而是使用了队列策略。当在执行一个特定的作业时，队列策略不会马上对其产生影响。相反，它影响的是作业的执行顺序，决定了工作线程会从队列中取出哪个作业。ThreadWeaver提供了两个标准实现。第一个就是前面提及的依赖关系。另一个就是资源约束。通过资源约束，可以声明所有已创建的作业中的某个特定的子集（例如，开销很大的本地文件系统I/O操作）在某一时间段内，只能执行一个特定的总量。没有这样的工具，很容易出现某些子系统出现过载现象。资源限制扮演的角色与传统线程机制中的信号机制类似，只不过它不会阻塞正在调用的线程，而只是简单地让某个作业不能执行。线程将检查某个作业是否可以执行，如果不行将会尝试执行另一个作业。

队列机制是为作业指定的，相同的策略对象可以指定给多个作业。同样，它们也是复合的，每个作业可能受限于多个可用策略的组合。从特定的策略驱动的作业基类中继承无法提供这样的灵活性。同样，采用这种方法的作业对象无需任何额外的策略，而且无法通过策略来影响其性能。

[1]Future在金融行业叫期权，市场上有看跌期权和看涨期权，你可以在现在（比如9月）购买年底（12月）的石油，假如你买的是看涨期权，那么如果石油真的涨了，你也可以在12月依照9月商定的价格购买。Future就是你可以获得未来的结果。对于多线程，如果线程A要等待线程B的结果，那么线程A没必要等待B，直到B有结果，可以先拿到一个未来的Future，等B有结果时再取真实的结果。其实这个模式用得很多，比如浏览器下载图片的时候，刚开始通过模糊的图片来代替最后的图片，等下载图片的线程下载完图片后再替换。

12.4.3　声明并发：略缩图阅读器示例

下面我们介绍另一个示例，用来展示这些不同的ThreadWeaver概念是如何整合在一起的。

它将使用作业、作业复合、资源限制、优先级和依赖关系，完成在一个GUI程序中展示略缩图像。我们首先来看看实现该功能需要哪些操作，它们之间有什么依赖关系，用户希望如何展示其结果。该示例是ThreadWeaver源代码中的一部分。

在该示例中，假定载入某个数码相片的略缩图涉及三个操作：从磁盘上读取原始文件数据，将原始数据转成大小不变的图像，然后根据略缩图所需的尺寸进行缩放。可能有人会认为第二和第三步可以合并成一个，但它不是本练习的目标（不是流式载入图像数据），并且它将受限于该图像格式能够在载入时进行缩放操作。同时本例将假定这些图像文件存储在硬盘上。由于每个文件的处理都不会影响或依赖于其他文件的处理，所以所有文件的处理都是并行的。对于每个文件的三个处理步骤都是按顺序进行的。

但这并不是全部内容。由于这是一个图形用户界面的示例，还需要考虑用户的预期。在本示例中，我们假定用户对可视化反馈感兴趣，也就是希望提供可视的进度提示。当图像可用时，应该尽快地提供预览，以进度条的形式展示处理进度是一个不错的方法。用户还希望程序在执行时不会阻塞计算机，例如出现过度的I/O操作。

解决这个问题，可以使用不同的ThreadWeaver工具。首先，每个文件的处理可以实现为由三个作业组成的序列。这些作业都是很平常的，它可以作为应用程序中预先准备的作业类工具箱的一部分。作业包括（其类名与示例源代码是一一对应的）：

·FileLoaderJob，用来从文件系统中将一个文件载入内存中。

·QImageLoaderJob，用来将图像裸数据转换成Qt应用程序中的内存图像表示（使应用程序可以访问框架和在应用程序中注册的可用图像解码器）。

·ComputeThumbNailJob，用来执行图像缩放操作，使其大小符合预览的需要。

这些任务都将添加到JobSequence中，每个这样的序列将添加到JobCollection中。集合类的复合实现使其与最初的问题十分接近，因此程序员会感到十分自然和符合规范。这解决了问题的一部分，也就是不同图像的并行处理，不过它很容易引发其他问题。采用这种方式声明的ThreadWeaver队列会存在问题，那就是无法避免当开始处理图像时尝试同时载入所有文件。尽管这是不太妙的，但我们没有告诉系统其他的处理方式。要确保只有这么多的文件载入操作同时开始，就需要使用资源约束。其代码类似于：

#include"ResourceRestrictionPolicy.h"

……

static QueuePolicy*resourceRestriction（）

{

static ResourceRestrictionPolicy policy（4）；

return＆policy；

}

这些文件载入器只需在在其构造函数中应用该策略即可，如果使用的是泛化机制，那么则在创建时应用：

fileloader-＞assignQueuePolicy（resourceRestriction（））；

但这样仍然没有正确地按预期安排好作业的执行顺序。该队列中可能最开始只立即启动4个文件载入器，但接下来将载入所有文件并计算生成预览（这再次将成为不太妙的行为）。在此需要更多的工具，需要更多的思考，要解决这一问题就将引出优先级。如果用ThreadWeaver行话来解释这个问题，那就是文件载入器作业优先级最低，但需要首先执行；图像载入作业优先于文件载入作业，但图像载入器启动前文件载入作业必须完成；最后，略缩图计算作业的优先级最高，虽然它依赖于其他两个处理阶段。由于这三个作业已经放在一个序列中，它可以确保针对每个图像的三个步骤会按顺序执行，将文件载入器的优先级设置为1，图像载入器的优先级设置为2，略缩图计算作业的优先级设置为3，这样就可以解决该问题了。这样，该队列会尽可能快地完成一个略缩图的显示，但在发现文件载入时间槽可用时仍然不会停止图像的载入作业。由于该问题主要是I/O的限制，因此意味着显示出所有图像略缩图的总时间只比从硬盘中载入这些文件的总时间多一点点（当然还有一些其他因素，如原始图像的分辨率极高）。在任何顺序式的解决方法中，其行为可能会带来更坏的结果。

大家可能会觉得以上关于该解决方案的描述比较复杂，因此提供一些源代码可能解释得更清楚。以下就是在用户选择了一组要处理的图像之后生成相应作业的过程：

m_weaver-＞suspend（）；

for（int index=0；index＜files.size（）；++index）

{

SMIVItem*item=new SMIVItem（m_weaver,files.at（index），this）；

connect（item,SIGNAL（thumbReady（SMIVItem*）），

SLOT（slotThumbReady（SMIVItem*）））；

}

m_startTime.start（）；

m_weaver-＞resume（）；

为了提供正确的进度反馈，在任务添加完成之前将暂停具体的处理过程。只要一个作业序列处理完成，item对象就将发出一个信号以更新整个视图。对于每个选中的文件，将会创建一个特定的item对象，然后它将创建处理一个文件所需的job对象：

m_fileloader=new FileLoaderJob（fi.absoluteFilePath（），this）；

m_fileloader-＞assignQueuePolicy（resourceRestriction（））；

m_imageloader=new QImageLoaderJob（m_fileloader,this）；

m_thumb=new ComputeThumbNailJob（m_imageloader,this）；

m_sequence-＞addJob（m_fileloader）；

m_sequence-＞addJob（m_imageloader）；

m_sequence-＞addJob（m_thumb）；

weaver-＞enqueue（m_sequence）；

优先级将作为作业对象的虚属性进行设置。要注意这里设置的所有对象在添加到队列之前是不会执行的，这一点很重要，在本案例中，只有当显式启动处理过程才会执行。因此创建这些队列和作业的整个操作过程只需花费很短的时间，程序控制权很快会交还给用户，一切都不会有什么问题。当预览图像可用时就会马上更新视图。

12.4.4　从并发到调度：如何系统地实现预期的行为

在前一个示例中，展示了如何分析问题以及如何实际地解决它（我希望这个部分不会令你感到惊讶）。要确保并发处理是用来编写出更好的程序，而不仅仅是提供一个填充线程的工具。它和调度是不一样的：调度需要告诉程序将执行什么操作，以及按什么顺序执行。该方法可以追溯到很早以前的PROLOG编程，有时需要采用相似的方法进行思考。一旦充分地理解了这一理念，其回报是十分有益的。

关于核心的Weaver类中的一个设计决策我们还没有谈及。Weaver类的API有两个互不相关的用户组。内部的Thread对象将访问它并从中获取要执行的任务，而程序员将使用它来管理其并行操作。为了确保公共API最小化，在此组合使用了装饰器模式和外观（facade）模式，以限制了向应用程序开发人员可能公开暴露的公共API的数量。并且通过PIMPL模式进一步实现了内部实现和API的解耦，它在KDE所有的API中都是十分常用的。

12.4.5　一个疯狂的想法

早在ThreadWeaver开始开发时就提出了该想法，但彻底实现该想法是不可能的。实际上最主要的障碍是框架的限制：在Qt程序库中使用了高级的隐性共享机制，也就是引用计数。由于这一隐性共享机制不是线程安全的，传输的每个简单POD（Plain Old Data）对象都是同步点。作者认为这对于用户而言是不切实际的，因此推荐在任何生产环境中使用Qt 3.0所提供的原型。KDEPIM套件的开发人员（也就是现在负责开发Akonadi的团队）真的认为ThreadWeaver是很不错的，并且马上将其初始版本引入到了KMail中，一直沿用到现在。由于已经遇到了ThreadWeaver承诺要解决的许多问题，KMail的开发者很迫切地采用了它，愿意承受其作者指出的一些缺点，甚至不顾他表达的一些愿望。

实际上，该程序库的一些未完成版就已经在KDE中获得了应用，当其发布了beta版本时就使大家纷纷快速地移植到Qt 4.0上来。因此它甚至在KDE 4.0开发周期之前就可用了，虽然它仍然是一个不属于KDElibs的次要模块。在两年的发展过程中，其作者为该程序库做了许多推介，提供了一些还在改进中的曾经简单或复杂的API。换句话说，它是一个在寻找问题的解决方案。多数KDE的开发人员需要时间来了解该程序库，这不仅仅是在理念上的了解，而且是促使他们回头做些投入甚至重新思考一些架构性结构问题，从而对其软件产生显著的改进。推进该程序库的发展进程的不是具体的开发人员需求；而是十分独立地不断发展，因为他相信相关的问题正在日益严重，同时为KDE 4.0平台提供一个好的解决方案是十分重要的。特别是在2005年的西班牙Malaga召开的Akademy年会之后，越来越多的程序开始使用ThreadWeaver，包括KOffice、KDevelop等主流软件，这足以催使其成为KDE 4.0主程序库的一部分。

ThreadWeaver为问题提供了一种备选的方法，当程序库作者以及预期的用户社区开始在其自己的项目中采用它的时候，就逐渐进入了成熟期，很快也就被提议进入KDE 4.0的基础框架。此后社区成员的态度也就发生了改变，从原来适度的尝试转变成了赞赏其所带来的效果。这个示例展示了该社区在技术决策方面的有效性，以及将方法应用于实践时所采用的姿态。如果ThreadWeaver没有通过3～4年朝着KDE项目方向的努力，那么无疑不会比现在的结果更好。而这样的努力也包括KMail开发团队的过早采用。如果不是它最终获得了成功，那么难以想象基于KDE 4.0开发的应用程序能够更好地处理并发，并且为用户提供更好的体验。

ThreadWeaver后续的主要扩展是添加了能够可视化展示队列活动的GUI组件，并引入了更多预先定义的作业类。另一个思想是与操作系统的IPC机制相整合（实现全局资源限制等功能），不过这样的需求受限于跨平台的障碍。不同操作系统所采用的方法千差万别。KDE 4.0凭借着公认的可用性，获得了广泛的用户。由于ThreadWeaver并非为KDE定制的，因此它的下一步发展方向（Freedesktop.org？）还悬而未决。现在，其目标仍然是为应用程序和桌面环境开发人员提供可靠的并发调度功能。

第五部分　语言与架构

第13章　软件架构：面向对象与面向函数

第14章　重读经典

第13章　软件架构：面向对象与面向函数

Bertrand Meyer

支持函数式编程的主要观点是它能够更好地实现模块化设计。按照宣扬该方法的书籍中的阐述，特别是通过其针对金融契约的框架示例，我发现了它与面向对象设计相比的优点和不足。最终的结论是，面向对象的设计，特别是支持高级例程对象或“代理”（agent）的现代形式，包含了函数式编程，在保留其优点的同时，又提供了高层抽象，更好地支持扩展和复用。

13.1　概述

“美”作为软件架构的口号，并不是由旁观者来判定的。其实早就存在一些明确的标准（Meyer，1997）：

可靠性

该架构能否帮助我们创建出正确、健壮的软件？

可扩展性

应对变化是否很容易？

复用性

该解决方案是否具有通用性？或者甚至可以将其作为一个组件直接插入到新的应用程序中，而无需做定制开发？

对象技术的成功在于它能够使最终开发出来的程序在可靠性、可扩展性、复用性等方面获得很大改进，当然前提是你正确地应用了该方法，而不仅仅是使用了一种面向对象编程语言。函数式的编程方法比面向对象编程方法出现得更早些，最早可以追溯到约50年前就已经很流行的Lisp语言。近几年来，它有卷土重来之势，出现了一些诸如Scheme（基于Lisp）、Haskell、OCaml和F#之类的新语言，以及精密类型系统（sophisticated type system）和诸如monads之类的高级语言机制。现在看起来，函数式编程甚至正在变成改进面向对象编程的新技术之一。在本章中，我们将从之前所概括的软件架构评价标准入手，对两种方法进行比较。我们将从反方向来论证它们之间的关系：面向对象的架构融合了函数式编程的思想，特别是吸纳了诸如Eiffel中的代理（agents，有的语言称之为闭包或委托）之类的新技术之后，在保留了其优点的同时克服了其缺点。

要证明这些发现是否合格，需要同时注意这项研究的限制条件，以及为缓和这些限制条件而提出的一些论据。这些限制包括：

较少的数据点

以下的分析将从两个函数式设计的实例开始。这可以让本章内容更具通用性。

细节不足

该示例的来源是一篇文章（Peyton Jones等2000）和一个PPT幻灯片（Eber等2001），我们后面再次提到它们时，将称之为“该文章”和“该幻灯片”（对本小节有帮助的还包括函数式编程的一篇经典论文[Hughes 1989]）。如果和那些更加详细的文档相比，使用该幻灯片可能会忽略掉一些细节。

具体关注

我们只考虑模块化。函数式编程也依赖于其他标准，如声明方式的优雅性。

实验者偏见

本章的作者长期以来一直是面向对象技术的贡献者和支持者。

以下的论据，将减少一些可能的批评：

·函数式编程的示例源于行业的实践；具体来说，是一家通过函数式编程技术实现的应用程序来对外提供商业服务的公司。主要的示例是一个金融工具，用来解决金融行业所面临的复杂问题。根据该文章作者（一位该领域的专家）的说法，这个工具并不能很好地解决当前的问题。这暗示它体现了当前的技术发展水平。（本章中的第一个示例是一个关于布丁的理论性项目，是一个用于教学的试验品。）

·该文章的作者之一，S.Peyton-Jones是一位底层技术研究领域的知名作者，它是Haskell语言的主设计者，是函数式编程领域的著名人物，因此该文章具有很高的可信度。在13.3节中用了一篇文章作为补充的例子，这篇文章极有影响，是函数式编程社群中另一位一流人士J.Hughes所写的。

·虽然有些地方值得探讨，但该文章中描述的解决方案提供了一些清晰、有效的解决方案。

·在这些示例中，我们将不考虑可改变状态的概念，而这正是面向对象编程语言必须支持的特性之一。

在此还将关注诸如代理之类的机制，它是实现完整面向对象解决方案的基本成分，它显然是从函数式编程的观点中获得启发的。因此可以得出一个结论，那就是我们无法忽略函数式编程学派的贡献，只不过从论据中能够看出面向对象风格更适用于可靠、可扩展、可复用软件架构的定义，而这样的架构通常是面向对象和函数式技术的融合体。

关于下面的讨论还有一些要注意的方面：

·在此使用的是Eiffel所提供的对象技术。我们并不打算分析除了多重继承（Java和C#就没有提供）、泛型（在这些语言的早期版本中都没有提供）、契约（除了JML和Spec#之外未提供）以及代理风格设施（Java未提供），或添加如重载、静态函数之后还有什么，这些东西都会影响面向对象的简单性。

·讨论将围绕架构和设计进行。我们不管它叫什么名字，这些任务都与函数式编程（与对象技术类似）是相关的，因为“编程”并不局限于实现。Eiffel通过无缝开发的概念，明确地引入了从规格说明到设计和实现的连续过程。对于其中那些面向实现的属性，在此将不会详细地涉及，虽然它们在实践中也是十分重要的。

·与实现有关的还有与表示式和表示法相关的问题。之所以考虑它们，是因为它们对架构和设计的关键标准有一定影响。不过，这些讨论在很大程度上只讨论语义的本质，而不讨论语义的形式。

还有两个更基本的提示。首先是术语问题，“契约”在此默认是指金融契约，它是与文档中的应用程序域相关的，别和软件中的“契约”（Meyer 1997）混为一谈，软件中的契约是指规约的元素（前置条件、后置条件、不变式）。为了避免混淆，这里将分别采用术语金融契约和软件契约来描述。

其次，我需要表示一下歉意：当在后半部分讨论到面向对象问题时，我将从作者先前的文章和幻灯片中引用或复制更多内容，而没有提出更多自己的判断和观点。这是因为面向对象技术已经有了广泛的应用，已经不缺乏command-query separation（命令-查询分离原则，参见第13.3.5节）之类的精妙的关键原则（我们的观点）；在此只需做一些简单的说明即可。要想了解更完整的内容，可以阅读在此提及的参考文献。

13.2　函数式示例

该文章和该幻灯片的核心目标就是为描述、处理金融契约提供一个便利的机制，特别是针对那些非常复杂的现代金融手段，正如幻灯片中所提到的一个例子那样（数值会在不同时间、不同的主要货币之间呈现不同的关系）：

·针对在12月27日支付2.00美元的承诺（期权的价格），持有者有权在12月4日选择：

·在12月29日接受1.95美元，或

·有权在12月11日选择：

·在12月28日接受2.20欧元，或

·在12月18日选择：

·在12月30日接受1.20英镑，或

·马上多支付1.00欧元并在12月29日接受3.20欧元”

（在本小节中，所有加引号的内容都是从该幻灯片或该文章中节选的。没有加引号的内容则是我们对其的解释和说明。）

为了解释这个问题，该幻灯片首先介绍了一个教学用的虚拟示例：它用“布丁”替代了“金融契约”。要想精确地描述布丁，可能需要涉及“计算所需糖量”、“估计布丁所需的制作时间”，并获取“制作的方法”。下面就是一种解决该问题的“坏方法”：

·列出所有布丁（trifle,lemon upside-down pudding,Dutch apple cake,Christmas pudding）[1]

·对于每种布丁，写下它的糖量、制作时间、制作方法等信息。”

虽然在该幻灯片中没有说明这种方法为什么不好，但我们能够很容易地猜出其理由：这是一个由特定描述组成的集合，不具有可复用性，因为它没有充分发挥“不同布丁可能共用相同原料”的特点；它也不具有可扩展性，因为如果要对某种布丁的某个部分进行任何修改，都需要对依赖这一部分的所有布丁进行修改。

这里的“布丁”是对我们真正感兴趣的“金融契约”的一个隐喻，不过由于它十分易懂，也不需要你事先掌握特定问题域的知识，因此我们还是先继续讨论完这个例子。对于这个问题，以下就是可行的“好方法”之一：

·定义一个小型的‘布丁合成方法’集。

·用这些合成方法定义所有布丁。

·糖量的计算也从这些合成方法中得出。”

图13-1所示的树形结构（节选于该幻灯片）展示了这些合成逻辑的结构。

图　13-1：描述布丁食谱的成分与合成方式

提示：我能够理解有些读者对于该示例中的甜点没有胃口，特别是因为它还出自于巴黎的作者。比较合理的解释是，该幻灯片是针对外国观众的，作者假定他们不熟悉度量系统[2]、不熟悉烹调习惯。而幻灯片中的讨论也是基于这样的假设：餐后甜点本身的味道不够好，并不会导致其语言和架构范型也随之不好。

在图13-1中，非叶子节点表示应用于其子树的合成方式。例如，“Take”就是一种合成方式，它需要两个参数，一个是布丁原料（左边的“Cream”，右边的“Oranges”），另一个是重量（“1 pint”和“6”）。最终这个树形节点在该应用程序的结果将成为一个布丁原料，或者由指定重量的指定原料制作而成的布丁。

它也可以通过一个微型的、“针对布丁描述”的“领域特定语言（DSL）来描述，得出如下所示的结构化文本（黑体字部分是为操作员添加的）：

"salad=on_top_of topping main_part"—为了保持一致，将其改成“OnTopOf"

"topping=whipped（take pint cream）

main_part=mixture apple_part orange_part

apple_part=chopped（take 3 apple）

orange_part=optional（take 6 oranges）"

这里使用了一个匿名的、但很典型的函数式编程表示法的变体（我们可以称之为“香草”），在函数式的应用程序中通常写为函数变量（例如，plus a b在应用程序中表示将a和b相加），而圆括号只是用来完成分组而已。

有了这一基础，我们就可以定义合成方式（如案例分析中“糖量”）所需的操作了（它的定义与递归定义的数学函数一样，都将遵循相同的递归结构）：

"S（on_top_of p1 p2）=S（p1）+S（p2）

S（whipped p）=S（p）

S（take q i）=q*S（i）

etc."

对于“etc.”是什么操作，在此尚未明确，也可能是S处理的可选合成方式；我们必须有一些方法来确定某种特定调合物中是否拥有可选的部分。我们先将这个问题放在一边，这种方法所能够带来的好处主要是：

·只有当我们需要添加新的合成方式或者新的成分时，才需要增强S。

·当定义一个新的食谱时，如果要完成糖量的计算，无需任何其他工作。

当然，我们真实的目标不是布丁而是金融契约。在该幻灯片中提供了该方法的梗概，在其文章中则做了更加详细的介绍。它采用了相同的观点，应用到了更为有趣的元素、合成方式和操作的集合。

元素可以是金融契约、日期和可观测量（如特定日期的特定汇率）。在基本契约示例中，包含0个（可以在任何时候获取，没有权利与义务约束）或1个（c）针对货币c（立即支付给债务人一个单位的c）的契约。

针对这种契约的合成方式包括：or，如获取契约（or c1 c2），表示获取c1或者c2，只有它们都过期，契约才过期；anytime，如（anytime c），表示可以在c的过期之前的任意时间获取，只要c过期该契约就过期；truncate，如（truncate t c），表示除非c的过期时间比t早，否则将其过期时间提前到t；get，如（get c），表示在其过期日获取c。在该文章中，列出了一打这样的基本合成方式，还有一些可观测量和日期。通过它们可以定义出更高级的金融指令，如“欧式期权”可以表示为：

european t u=get（truncate t（or u zero））

在一个操作中，包括契约的过期日期（这是所有建模努力所预期获得的最终的、重要的、具有实际意义的好处）和其值处理过程，以及一个按时间排列的预期值序列。与布丁中的糖量类似，在其案例分析中，该函数的定义是基于相同基本构造的。在以下的例子中，是一个包含前面的基本元素和合成逻辑的H操作，它用来表示过期日期函数，或者“范围”：

H（zero）=∞—预期属性的一个特殊值

H（or c1 c2）=max（H（c1），H（c2）

H（anytime c）=H（c）

H（truncate t c）=min（t,H（c）

H（get c）=H（c）

在值处理过程所遵循的规则中，采用了类似的结构，虽然右边的内容相对比较复杂难懂，涉及许多金融及数学计算。

[1]布丁是一种以面粉、牛奶、鸡蛋等为基料的糊状甜食；括号中列出的是不同口味的布丁，依次是屈莱弗甜食、柠檬布丁、荷兰苹果蛋糕、圣诞布丁。

[2]即米、千克等国际通用的度量单位体系。

13.3　函数式解决方案的模块性评价

在先前的介绍中，虽然忽略了该幻灯片和该文章中的许多观点，但对于讨论函数式方法的架构特性，以及将它们和OO视图进行比较，这些已经足够了。在后续的内容中，我们将会自由地使用“布丁”示例（它能使观点马上为大家所理解）和“金融契约”示例（真实应用程序的视角）。

13.3.1　可扩展性标准

正如前面的介绍中所说的那样，这种方法对于架构所产生的最直接的好处就是添加新的合成方式很简单：“当需要定义一个新的食谱时，无需更多的工作就能够完成糖量的计算”。不过，这只不过是使用函数式编程方法必然带来的效果之一。合成方式这一概念背后的本质是基于基本成分来创建布丁及布丁原料，或者金融契约，这些基本成分要么是原子性的，要么是应用合成方式所生成的成分。

该文章和幻灯片看起来像是针对金融契约问题的新思路，但实际上它也适用于其他问题。我们转到GUI设计领域，如果采用在该幻灯片之初否决的“坏方法”（列出所有布丁的类型，然后分别计算其糖量等），那么将需要以特定的方式，逐一生成能够执行显示、移动、大小调整、隐藏等操作的交互式应用程序界面。当然没有人会这样做；所有GUI设计环境都提供了一些原子性元素，如按钮、菜单项，而通过操作将它们递归式地组合成窗口、菜单以及其他容器元素，最终生成完整的用户界面。这就像布丁的合成方式定义了糖量、根据其成分计算卡路里值，金融契约的合成方式则按照复杂契约的要素定义了其范围和值序列一样，对组件的显示、移动、调整大小、隐藏等操作将针对组件中的所有成分递归执行。EiffelVision程序库（Eiffel软件：EiffelVision文档）就以一种特殊的系统化途径应用了这些成分，不过这种方法十分独特。它当然并不仅限于函数式编程；任何带有子程序和递归机制的框架都能够实现它。

在与模块性相关的问题中，最有趣的并不是在将原有合成方式应用到原有类型的成分时出现的问题，而是当合成方式和成分类型发生改变时遇到的问题。该幻灯片明确指出，“只有当我们需要添加新的合成方式或新的成分时，才需要增强S”（针对合成方式sugar）。大家所感兴趣的问题是，接下来如何在架构中适应这些变化。

相关的变化实际上比其提到的更多：

·不管是原子类型还是合成方式，我们都认为变化在操作中：为“布丁”应用程序添加卡路里计算函数，为“金融契约”应用程序添加“延期”操作，为图形化对象添加翻转操作。

·除了添加各种操作之外，还要考虑到可能修改和删除操作。为了保持简单，我们只考虑添加新操作的情况。

13.3.2　函数式方法的评价

其提供的程序结构很简单，是由一组定义构成的，定义的格式要么形如（Arnout，2004）：

O（a）=b a,O

要么形如（Arnout和Meyer，2006）：

O（c（x,y，……）=f c,O（x,y，……）

对于任何操作O，原子类型a以及合成方式c，右边都是由适当的常量b和函数f组成的。同样，为了保持简单，我们将诸如a之类的原子类型视为O-元合成方式，因此我们只需考虑第二种格式。对于合成方式t（on_top_of、hipped等）和操作f（糖量、卡路里等），我们需要定义t×f。不管采用什么方法，t×f元素都必须提供。其架构性问题是如何将它们组织到模块中，以便对其进行扩展和复用。在该幻灯片和文章中都没有对这个问题进行讨论。当然，对于小型的t和f而言这并不是问题；我们可以将所有的定义都封装到一个独立的模块中。这是一种考虑可扩展性的简单方法：

·添加一个合成逻辑c，并以上述格式在其中添加一个f定义，它是针对现有的所有操作的。

·添加一个操作O，并在其中添加一个t定义，它是针对现有的所有合成方式的。

这种方法的可伸缩性不好，对于较大型的开发项目而言，必然需要将该系统分拆到不同模块中。现在的扩展性问题就将变成了如何使这样的修改影响的模块尽可能少。

即使t和f都很小，这种单模块的解决方案是无法实现复用的。如果其他程序只需要一部分操作和合成方式，那么就必将面临模块化技术中经常遇到的两难选择：

Charybdis[1]

复制-粘贴相关的部分，但这样做也有一些风险，当原始内容发生改变时（可能是修复一个缺陷），派生模块是无法做相应更新的。

Scylla[2]

引用整个模块，使用模块中一部分可用功能，最终担负起不必要的包袱，它会使更新工作更加麻烦，更容易产生冲突（例如，当派生模块定义了一个新的合成方式或函数，而原模块的最新版本也定义了与之重名的合成方式或函数）。

这些观测结果让我想起可复用性和可扩展性之间存在着紧密的联系。关于函数式编程语言OCaml（Steingold 2007）[3]的在线评论中，有一个具体的例子：

在模块之外，很难对模块中的行为进行修改。假设使用了Time模块中定义的Time.date_of_string函数—用来解析ISO8601基本格式（“YYYYMMDD”），但如果还想能够识别ISO8601扩展格式（“YYYY-MM-DD”）。那么很不幸，只能让该模块的开发人员来修改这个函数，无法在自己的模块中重新定义该函数。

随着软件的发展和变化，另一方面的可复用性将变得十分关键，那就是对公共属性的复用。该文章在介绍“欧式期权”合成方式的同时还介绍了“美式期权”的合成方式，它们的函数签名是不同的（分别是Data→Contract→Contract和（Date,Date）→Contract→Contract），对于它们而言，所有的操作都必须分别定义。不过，这样做经常被置疑，因为这两种期权中有许多属性和操作是相同的，同样，不同的布丁也可以分成几种类型。这样的分类，对软件的模块化是有帮助的，同时还带来了额外的好处（如果相同的内容比较多），那就是定义中所需的内容将大大减少，要比t×f少得多。不过，这需要我们花些时间来研究问题域，不仅仅是从函数角度，而是从类型的角度。这样的视图是位于更高抽象等级上的。在实践中，其中最容易引发争论的是函数及其签名的确定。根据该文章的说明，“美式期权要比欧式期权更加灵活。通常，美式期权授予的权利是在两个日期之间的任何时候行权，也可以不这样做。”这会让我们想起该定义的一个变体：要么美式期权是欧式期权的一种变体，要么它们都是某种通用期权的变体。不过，如果我们采用合成方式来定义它们，会马上让它们显得完全不同，因为在签名中将引入额外的日期值。同样，这也是一种基于具体实现来定义某一概念的方法，不管它采用的是数学计算实现还是计算机实现，这仍然意味着该解决方案缺乏抽象度和通用性。使用类型作为基本的模块化机制（正如面向对象设计中那样）将会提升抽象的等级。

[1]西西里海岸附近墨西里海峡的一个漩涡，被拟人化为一个吞噬船只的海怪（希腊神话中的六头女妖）。

[2]锡拉巨岩，正好位于著名大漩涡Charybdis对面。因此在英文中经常用“between Scylla and Charybdis”来表示进退维谷、进退两难、腹背受敌之意。

[3]此处引用做了些删节。并且这也不意味着作者认同该评论中的其他观点。

13.3.3　模块性等级

将模块性作为评价函数式编程的标准之一是合理的，因为该方法所宣扬的主要优点就是提供了更好的模块性。我们在该幻灯片中看到了原作者对这个问题的说明，下面是来源于一篇针对函数式编程的、更为基础的论文（Hughes 1989）中所提到的更通用的描述，它对该方法的描述是：

[程序]可以通过新方法进行模块化和简化。函数式编程的主要价值就是大大改进了程序的模块化程度。采用函数式编程的程序员也必须努力确保程序使用更小、更简单、更通用的模块，并使用新的粘合剂（我们稍后就将介绍）将它们整合在一起。

在Hughes的论文中所提到的“新的粘合剂”，实际上就是我们在前面两个示例中所使用的方法之一，即无状态函数的系统化应用，包括高抽象层次的函数（合成生成），它们将对其他函数产生作用。此外，还有大量使用的列表、其他递归定义的类型，以及延迟计算（lazy evaluation，[1]）等概念。

虽然这很吸引人，不过这些技术仍主要致力于提供更为精细的模块化。Hughes用函数式编程方法开发了一个平方根计算程序，采用的是牛顿迭代法，它将计算数字N的平方根，公差为eps，初始近似值为a0：

sqrt a0 eps N=within eps（repeat（next N）a0）

在此除了使用了within、repeat、next等合成方式之外，作者还将该程序与使用了大量goto语句的FORTRAN程序版本进行了比较。即使我们不考虑这一价值不大的改进（在该论文最初发表时，FORTRAN语言已经略显过时，而且goto语句也已经被弃用了），大家也能理解为什么我们会喜欢这样的解决方案，它完全是基于一些通过合成因子粘合在一起的小型函数实现的，而不是使用循环结构。不过也有人更喜欢循环结构，但我们认为结构良好的程序要比大规模模块化更为重要，这比基础性架构问题更麻烦。在实践中，所展示问题的正确性实际上对于任何方法而言都是相同的。例如，在Hughes的论文中定义的函数里，序列中第一个元素和前一例子并不相同，它处理的是当值小于eps时的情况：

within eps ([a:b:rest])=if abs (a-b)＜=eps then b

else within eps[b：rest]

似乎假定相邻元素之间的距离在减小，并且肯定假定这些差中有一个不大于eps[2]。对这些特点的说明隐含了某种契约式编程的机制，将前置条件与函数联系在一起（在一般的函数式方式中没有这种机制）。确保eps终止的条件在本质上与命令行风格中对应循环终止的条件是一样的。

在这个例子和前面的例子中，该做法似乎对于大粒度模块化或软件架构没有什么贡献。在实际应用中，函数式这种无状态特性看起来对该问题没有什么（正面或负面）影响。

[1]采用此种方法的类将推迟计算工作，直到系统需要这些计算的结果。

[2]在引用Huges文章中的例子时，经他同意，我们使用了列表的新表示法（Haskell），如[a：b：rest]。这种表示法比最初的cons表示法可读性更好，以前需要写成cons a（cons b rest）。

13.3.4　函数式的优势

在之前的例子中，还展示了函数式方法的四个重要优点。

第一个优点是表示法（notational）。毫无疑问，简洁的定义正是函数式编程语言吸引人之处，诸如前面示例中的定义就很简洁（正如大家所知道的那样，诸如Haskell之类的现代函数式语言提供了更加简洁的定义，例如cons a（cons b rest）可以写成[a：b：rest]）。这和常见的命令式编程语言中的例程声明相比，语法负担要小得多。不过，对于这一观测结论，有一些限制因素降低了其价值：

·正如前面所说，从设计的角度看，表示法问题不是关键的。例如，有人可能会针对命令式语言使用函数式的设计。

·诸如Haskell、OCaml等许多现代的函数式语言都是强类型的，这意味着表示法将会稍稍冗长；例如，除非设计师希望依赖于类型推断（这在设计阶段并非好主意），否则在合成方式within中将需要使用类型声明Double—＞[Double]—＞Double。

·不是每个人都能习惯将多参数函数替换为返回函数的函数（就像医学文献中所说的“疯狂的嵌套综合症（RCS）”那样，也就是在那篇关于金融契约的文章中所使用的诸如（a→b→c）→Obs a→Obs b→Obs c之类的函数签名）。

不过，保持表示法的简洁在设计和架构层面上也是一种美德，而函数式编程语言也有一些关于其他设计表示法的经验教训。

第二个优点（Simon Peyton Jones和Diomidis Spinellis在对本章的早期版本提建议时强调了这一点）也与表示法有关，即定义对象的组合子表达式（combinator expression）的优雅性。在强制的面向对象语言中，与下面的组合表达式：

on_top_of topping main_part

对应的应该是一条创建指令：

create pudding.make_top（topping,main_part）

并带有一个创建过程（构造方法）make_top，通过指定的参数来初始化base和top属性。组合子的形式是描述式的，而不是强制式的。但在实践中，在面向对象编程中使用组合子的变种，利用“工厂方法”而不是显式地创建指令，是很容易也是很常见的。另外两个优点则更为基础。第一个是能够让操作成为“一等公民”，这是一个很常用的说法，就像我们说“程序中的对象”或者“数据”是Lisp语言中的“一等公民”，也就是它是最能够被高效处理的部分。许多主流的语言都能够支持将一个例程作为另一个例程的参数，但这不能够被视为基础性的设计技术，而且实际上经常被视为自修改代码所带来的各种不确定性的根源。现代的函数式语言都将高级的函数视为常规的程序对象，并开发了相关的类型系统，以体现其强大。这也是函数式编程的一部分，它在采用主流编程方法的开发过程中展现了直接的效果；正如我们稍后将看到的那样，与代理相关的表示法是直接从这些函数式编程概念中派生的，这是在面向对象框架中很受欢迎的新机制。

函数式编程的第四个主要的优点是延迟计算（lazy evaluation）：它能够描述一个可能无限大的计算，当然该计算的任何具体执行过程都将是有限的。早先对within的定义就假定采用了延迟计算；这在repeat的定义中则更加清晰（在此使用的是Haskell的list表示法，而非Hughes原先采用的cons）：

repeat f a=[a：repeat f（f a））]

谁负责处理（在普通的函数式应用程序表示法中）无限序列a,f（a），f（f（a））……（next N x的定义为（x+N/x）/2）呢，within的定义将会在处理了有限个元素之后，停止对该序列的处理。这是一个很不错的想法。它在软件设计中的通用应用程序会带来两个观测结论。

首先，这里存在正确性问题。这种可能出现无限循环的程序虽然容易编写，但它掩盖了确保其不会永远无法终止的困难。我们看到within中假定了一个前置条件，虽然没有直接描述出来，但该前置条件要求使该元素的值小于eps，在无限序列（它是半可判定的）上是无法执行有限计算的。这对于设计师而言是一种比较麻烦的技术，这也是为什么许多采用函数式编程方法的程序员想寻找新的“明灯”的原因。（这是很难预先知道的。当有任何采用函数式编程方法的程序员改变时，就可能尝试换一盏“灯”，如果失败了，则还会尝试另一盏。）

第二个也是最后一个问题，可能会在非函数式的设计环境中对无限结构采用延迟计算，不需要任何特殊的语言支持。而抽象数据类型方法（也就是众所周知的面向对象设计）则提供了合适的解决方案。在Eiffel程序库中，有限序列和列表都可以通过API来访问，这种访问是基于“光标（cursor）”这一表示法的。

图　13-2：Eiffel列表中的光标

移动光标的命令包括start（移动到第一项）、forth（移动到下一项）和finish。布尔型查询before和after用来确认当前光标是否在第一个元素之前，或者在最后一个元素之后。如果不是以上两种情况，那么item将返回当前光标位置的元素，而index将返回其索引值。

我们很容易将这种方式移植到无限序列中：只要去除finish和after命令即可（当然还有计算项目总数的count）。这也就是Eiffel程序库中的派生（抽象）类COUNTABLE的规约。它的派生类包括PRIMES、FIBONACCI和RANDOM；这些派生类都提供了自己的start、forth和item命令实现（在最后一个派生类中，还提供了plus方法，用来为随机数生成器设置一个种子）。要获得这些无限序列中的后续元素，可以先使用start命令，然后向前查询后续有限个元素即可。

任何无限序列结构都需要提供能够以该风格建模的有限计算。虽然这并未覆盖各种延迟计算的应用场景，但它的优点在于使无限结构更加清晰，也更易于构建正确的延迟计算。

13.3.5　状态调停

函数式方法旨在直接依赖于数学函数的属性，而且拒绝采用假定，并且隐含着命令式方法，其计算操作除了生成结果（数学函数常见的功能）之外，还能够修改计算的状态：可能是修改全局状态，也可能只是修改状态的一部分（在更模块化的方法中），例如某个特定对象的内容。

尽管在各种函数式编程的文章中都强调了这一点，但该属性在本例中却没有涉及，或许因为根据最初问题分析，已经将状态从函数式的结构中去除了。这也是可能的，例如，在一个用来计算金融契约的非函数模型表示法中，操作可能只是将状态转换为对值的更新，而不是基于一个序列（值处理序列）实现的函数。

它仍然可能是在函数式方法的这一基础性决策上做出的通用评价。在任何系统模型（无论是否为计算机系统）中，状态的概念总是难以避免的。甚至有观点认为它是计算的核心概念。（有人辩称[Peyton Jones 2007]无状态编程有助于解决并发编程的一些问题，但是还没有足够的证据能得出一般性结论。）世界不会因每次重要事件而克隆自己我们的计算机内存也是这样，而只是覆盖其单元格而已。我们总是可以对这样的状态改变进行建模，例如通过设置一个值序列的方法，不过这些都是人造的（作为前一个疑问的备选回答，采用函数式编程的程序员并没有换“灯泡”，而是买了一个带有新插槽、新电缆、新灯泡的“灯”）。由于我们认识到作为这些操作的输入和输出的状态是不可能忽略的，加上之前的一些笨拙的尝试（Peyton Jones和Wadler 1993），因此在现代的函数式语言（特别是Haskell）中引入了monad的概念（Wadler 1995），它可以将最初的函数插入到高阶函数中，并且使用了更为复杂的签名；新添加的签名部分可以用来记录状态信息，以及其他一些额外的元素，诸如错误状态（用来对错误进行建模）或输入/输出结果。

使用monad整合状态处理的想法，与上一小节中通过将无限序列建模为一个抽象数据类型来实现延迟计算行为所采用技术的想法是相似的，只不过它是一种反向应用而已：在框架A中模拟一个框架B中隐含的技术T，然后在A中编程实现一个T的显式版本，或者实现一个实现T的关键机制。对于第一个示例而言，T就是无限列表（关键机制就是对无限列表的有限计算）；对于第二个示例而言，T就是状态。

对于编程语言的语义描述（例如，用来简化标记理论）而言，monad的概念是十分漂亮和十分有用的。不过，有人可能不会关心它是否是程序员直接使用的合适的解决方案。在此，我们必须关注于正确的观点。monad也有明显的非直接相关的缺陷，那就是普通的程序员很难学会它；创新的想法，很难在最初引入时就融入到主流的教育体系中。（递归式编程和面向对象编程都曾经被视为超出了“Joe程序员”的范畴。）其中最重要的问题是这个麻烦的方法是否有价值。通过monad为函数式编程引入状态机制，就像在你确认大家信奉贞洁之后，告诉你的追求者，你有孩子。

真是需要首先把状态排除在外？以下两个观测结论就足以引发这一怀疑：

·基本的状态改变操作，如简单的赋值操作，都有清晰的数据模型（在此是霍尔逻辑规则，它是基于“置换”的）。这降低了无状态编程所预期的主要好处：简化与程序相关的数学推理。

·针对与确保设计或实现的正确性相关的更复杂方面而言，函数式方法的优势并不清晰。例如，校验有特定的属性和终止条件的一个递归定义（需要循环不变量和变量）。有效的函数式程序也未必能够不使用链接的数据结构，尽管其导致的问题（如别名混淆）都是与底层的编程模型无关的挑战。

如果对于确保程序正确这一任务而言，函数式编程未能显著简化，那么就将遗留一个主要的实际问题：引用透明。这就是等价替代性的概念：在数学领域中，f（a）始终意味着与指定的f和a相关的某种事情。这在纯函数式方法中也是正确的。但在函数可能带有副作用的编程语言中，f（a）在连续调用时可能会返回不同的结果。通过保留语义推理的常用模式来消除这种可能性，会使程序文本更易于理解；例如，我们都认为g+g和2×g具有相同的意义，但如果g是产生副作用的函数，那么这一结论就无法确保了。这对于自动化验证工具（能够检查函数是否会产生副作用）而言并不困难，实际上是对于人类读者而言有困难。

确保表达式的引用透明是个很合乎大家希望的目标。不过，这正说明将与状态相关的表示法从计算模型中分离出来是合理的。在此回顾一下Eiffel方法中定义的“命令-查询分离原则（Meyer 1997）”规则是很重要的。在这个方法中，类的特性（操作）可以清晰地分成两类：一类是命令，它能够修改目标对象以及它的状态；另一类是查询，它能够提供与对象相关的信息。命令不会返回结果；查询不会改变状态，换句话说，它们满足引用透明原则。在先前的列表示例中，start、forth和finish（针对有限列表）都是命令；item、index、count、before和after（针对有限列表）都是查询。该规则排除了很多通过调用函数获取一个结果并修改其状态的太过于常见的模式，我估计这就是对命令式编程不满意的实际根源，它比先通过一个命令来修改状态，再通过一个（没有副作用的）查询来获取信息更令人烦恼。该原则也先可这样描述：“提问时不修改答案。”例如，它意味着一个典型的输入操作应该写成：

io.read_character

Result：=io.last_character

在此read_character是一个命令，它将从输入中获得一个字符；而last_character则是一个查询，将返回最后读取的字符（它们都是基本I/O程序库提供的特性）。紧接着调用last_character将确保返回相同的结果。不管是哪里提到的，不管是出于理论还是实践的角度（Meyer 1997），命令-查询分离原则都是一种方法性规则，而非语言本身的特性，但所有慎重的Eiffel软件开发过程中都会遵从这一原则，以获得引用透明的优点。尽管其他面向对象编程流派未能应用它（继续采用C风格的函数调用，而没有专门完成修改的程序），但在我们看来这是面向对象方法中很关键的原则。它看起来也是在函数式编程中实现引用透明的可行方法，由于只涉及查询的表达式不会修改状态，因此可以被理解为传统的数学公式可函数式的语言。而在建模系统和计算中状态的概念，则通过命名表示法来完成。

13.4　面向对象视图

现在我们将考虑如何为该幻灯片和该文章中讨论的示例设计一个面对象的架构。

13.4.1　组合子很好，但类型更好

迄今为止，我们处理的都是操作和组合子。操作我们将保留，而关键的步骤是抛弃组合子，并将它们替换成类型（或者称为类，它们的区别是由泛型带来的）。它将明显地提升抽象等级：

·一种组合子描述了基于原有的机制构建新机制的特定方法。组合是以很严格的方式定义的：与一个名为take的组合子（如take 3 apple）关联的是一个数量元素和一个食物元素。正如前面所说的，它和数学公式很像，是按照精确的实现过程定义的结构。

·一个类定义了一个对象类型，它将列出所有可应用的特性（操作）。它提供了抽象数据类型意义上的抽象：它不仅定义了由什么内容构建，还定义了相应对象可应用的操作。我们可以从中领会数据抽象和面向对象设计的原则，采用这种方法意味着不仅知道对象是什么，还知道它有什么（它们公共的特性和相关的契约）。这同样提供了对各种类型继续进行分类的方法，或称之为继承，它一方面能控制模型的复杂度，同时还能充分发挥不同类型存在共性的优势。

从第一种方法改成第二种方法，我们并不会丢失任何东西，因为类可以很容易将组合子当作一种特殊的情况来考虑。它完全能够为指定要素提供所需的特性，并提供能够构建正确对象的相关创建程序（构造函数）。我们来看一个例子：

class REPETITION create

make

feature

base：FOOD

quantity：REAL

make（b：FOOD；q：REAL）

—用quantity个base生成该food元素。

ensure

base=b

quantity=q

end

……Other features……

end

这样我们可以通过创建apple_salad.make（6.0，apple）来获取一个该类型的对象，它和使用组合子的表示式是等价的。

13.4.2　使用软件契约和泛型

由于我们只关注设计，因此其效果实际上已经通过后置条件的形式表示了，但它对于引用实现语句（do base：=b；quantity：=q）而言，实际上并不是什么问题。它对于易于理解的面向对象设计而言是很重要的，它能够缩短实现与设计（以及规约）之间的距离。在所有这些方面，我们可以自由地使用会修改状态的赋值指令，并仍然（在此感谢读者的提问）拥有完整的功能。

不过，类和组合子不同，它的功能并不限于这些。例如，它可以拥有其他创建程序。一种很常见的现象就是将两个相同的东西混合在一起：

make（r1，r2：REPETITION）

—通过合成r1和r2生成该food元素。

require

r1.base=r2.base

ensure

base=r1.base

quantity=q

后置条件表达式说明了和相同基本食物类型混合的数量。这样的需求也可以通过该类型系统以静态的形式实现；使用泛型（在类型化的函数式语言中也实现了，不过其名称会令人感到好奇，即“参数化多态”）将使类的定义变成：

class REPETITION[FOOD]create

……和之前一样……

feature

make（r1，r2：REPETITION[FOOD]）

……在此不一定需要预言……

……其他部分和之前一样……

end

类不仅可以有不同类型的创建程序，还可以拥有更多特性。具体来说，我们之前程序中的操作都可以变成相应类的特性。（读者可能会猜到，变量名t将变成类型，而f将变成特性。）对于pudding类（包括用来描述诸如REPETITION等食物类型的类）而言，将拥有诸如sugar、calorie_content等特性；而对于contract类而言，则将拥有如horizon、value等特性。在此有两点需要说明：

·由于我们是从纯函数式模型开始的，因此迄今为止提到的所有特性要么是创建程序，要么是查询。虽然也可以在面向对象框架中保留这种函数式的风格，但在开发中也可能需要引入命令。例如，因响应特定事件（诸如重新协商）而对金融契约所做的修改。而是否需要维护状态的问题，实际上与模块化的讨论是不相关的。

·最初，value函数是遵从于一个无限序列的。我们可以通过使用COUNTABLE类型来保留这一签名，并允许执行延迟计算；或者我们可以提供一个整型的参数，因此value（i）返回的值将是第i个值。

13.4.3　模块化策略

迄今为止所实现的模块化，阐述了面向对象技术的本质想法（至少是我们找到的本质想法之一）：将类型和模块的概念合并起来（Meyer，1997）。用最简单的话来说，那就是面向对象分析、设计和实现意味着我们基于的每个模块都是系统维护的一种类型的对象。它与其他方法提供的模块化设施相比，这是一个限制更多的原则：一个模块不再是一个软件元素（如操作、类型、变量）的简单结合体，设计师在组合这些时要基于一些合理的标准；它们是一种类型实例所能接受的属性和操作的集合。

类就是这种类型-模块合并的产物。在诸如Smalltalk、Eiffel和C#（不仅限于这些，例如还有C++或Java）等面向对象语言中，这种合并是双向的：不仅仅是类定义了一种类型（例如一种类型模板，或者使用了泛型），反过来任何类型（包括诸如整型之类的类型）也都被定义成了类。

如果想使类只保留类型的角色，并使其完全与模块化结构分离开，这也是可能的。诸如OCaml之类的函数式语言是一个特例，它不仅提供了传统的模块结构，也从面向对象编程中吸纳了类型机制。（Haskell也类似，只不过对类这一概念提供了更多的限制。）反过来，也可以去除所有类型都必须定义为类的限制，例如在C++和Java中，诸如整型（Ininter）之类的基本类型就不是类。从对象技术的视角上看，是假定它们完全合并了，另外理解更高层面的类分组机制（诸如Java和.NET中的包，Eiffel中cluster）也是必要的，但这些机制更多着眼于组织的便利，而非基础性的程序构造。

该方法意味着在定义软件架构时，类型优先于函数。其模块化标准是类型，每个操作（函数）将放在一个类中，反之则不亦然。不过，函数也通过抽象数据类型原则的应用反戈一击：当定义了一个类之后，要理解其他信息，可以通过一个抽象接口（API）列出可应用的操作，以及其定义的语义属性（契约：前置条件、后置条件，以及针对整个类的不变量）。

这种模块化策略的基本原理是遵从更好的模块性原则，包括可扩展性、可复用性以及可靠性（通过使用契约实现）。不过，我们必须检查这些承诺对于我们的示例而言是正确的。

13.4.4　继承

面向对象方法对于模块性目标的最本质贡献是继承。我们预期本书的读者对该技术很熟悉，在此将只回顾一些基本的观点，并勾画其在我们示例中可能的应用方法。

继承采用了“分类法”来组织类，大概可以表示为“is-a”关系，类之间的基本关系还有一种名为“客户”（client）的关系，它表示某类将通过其API（包括操作、签名、契约）来使用该类。继承通常是不遵从信息隐藏原则的，它是一种恰好相反的“is-a”视图。尽管有些作者只针对纯子类型使用继承，但实际上如果用它来实现标准的模块包含机制也是没有问题的。Eiffel实际上就提供了“非一致性继承”机制（Ecma International，2006），它失去了多态的支持，但保留了继承的其他特性。继承的这种双角色与类的双角色（类型和模块）特性是相吻合的。

不管是哪一种，继承都是用来捕获类之间的共性的。对于与布丁相关的元素进行分类，可以用如图13-3所示的继承图来表示。

图　13-3：展示布丁继承结构的类图

继承和客户（client）关系之间的区别是值得注意的。水果沙拉（fruit salad）是一种布丁，它同时也是一种复用品（我们忽略了通用参数）。一种复用品（repetition）并不是一种布丁，而是一种描述食物构成的“布丁成分”。有些“布丁成分”（诸如“合成布丁”）也是布丁，当然也不全是。水果沙拉（fruit salad）是一种布丁，同时也是一种复用品（repetition），它是一种布丁成分（pudding_part）。另一方面，一种奶油水果沙拉（creamy fruit salad）不是一种水果沙拉（fruit salad），即使我们使用这一表示法表示用水果制成的布丁。它拥有水果沙拉和奶油，因此它使用了客户关系。它是一种合成布丁（composite pudding），因为该表示法用来表示它是由几个部分组成的，它和更通用的表示法COMPOSITE_PART一样，也是一种布丁。对于这个成分而言，它和水果沙拉（fruit salad）和奶油（cream）之间是客户关系。

对于金融契约示例而言，我们可以使用相似的方法，可以基于契约的类型将其分成“零息票债券”、“期权”以及其他由问题域专家经过仔细分析所得出的类别。

多重继承对于这种形式的面向对象建模是至关重要的。特别注意组合部分（composite part）的定义，在此应用了一个通用的模式来描述这种组合结构（参见Meyer 1997，5.1节，“composite figures”）：

Class COMPOSITE_PART inherit PUDDING_PARTLIST[PUDDING_PART]

feature

……

end

其中方括号引入了泛型参数一个组成部分（composite part）既是布丁的成分，拥有所有可应用的属性和操作（sugar content等），也是一个布丁成分列表，同样带有一些可应用的列表操作：诸如start、forth之类的指针移动命令，诸如item和index之类的查询，以及插入、删除元素的命令。列表中的元素可以是任何一种布丁成分，包括（递归的）组合部分（composite part）。这样，多态和动态绑定技术也就有了用武之地，我们稍后将对其进行说明。泛型和继承都是很有用的机制。另外，多重继承使针对类的机制更加完整，它不仅仅局限于用接口（Java风格）实现，这此并没有应用到接口。

13.4.5　多态、多态容器和动态绑定

继承和泛化对可扩展性的贡献，以及多态和动态绑定技术所带来的可扩展性，我们可以从COMPOSITE_PART类中的sugar_content中体会到（参见图13-4）：

sugar_content：REAL

do

from start until after loop

Result：=Result+item.sugar_content

forth

end

end

图　13-4：带光标的多态列表

在此直接将LIST类中的操作应用到COMPOSITE_PART中了，这是因为COMPOSITE_PART就是从LIST类中继承的。item命令返回的结果可以是任何类型（包括子类型）的PUDDING（布丁）；由于它的结果是多种类型的对象，也就是众所周知的多态变量（在这个例子中正好是多态查询）。一个完整的COMPOSITE_PART结构，包括不同类型的项目，这也就是众所周知的多态容器。多态容器使多态的合成成为可能，它本身是继承和泛型的结果。（在此有两种完全不同的机制，在函数式编程中，针对泛型的“参数化多态”很容易与这里的多态混淆起来。）

item的多态，意味着后续调用item.sugar_content时可能会应用在不同类型的对象上；相应的类可能会有不同版本的sugar_content查询。动态绑定就是负责确保这样的调用能够应用合适的版本，它将基于item所属对象的实际类型来决定。如果某种成分本身就是组合部分，也就是前面所示的版本，它将递归地应用，而且可以是其他任何版本，例如针对CREAM的版本。

这是针对面向对象设计的最新方法，多态是由类型系统控制的。item值的类型是可变的，不过它肯定是PUDDING及其子孙类，它们是由COMPOSITE_PART的泛型参数指定的。这部分的内容不管对函数式编程的环境，还是面向对象编程的环境都有影响：使用日益复杂的类型系统（仍然是基于诸如继承、泛型和多态等少量概念），将与系统架构相关的、日益发展的部分整合到类型结构中。

13.4.6　延期类和特性

在前面的类结构图中，PUDDING和PUDDING_PART类都被标识为“延期”（deffered）（使用的是BON面向对象建模表示法[Walden和Nerson，1994]中所指定的星号）。这意味着，它们并没有完整实现；换一种说法，可以称之为“抽象类”。一个延期类通常拥有延期特性，拥有一个签名和（更重要的）契约，但没有实现。具体的实现是由非延期的（“已实现的类”）子孙类呈现的，适合的选择是由每个已实现类来实现延期类中定义的通用概念。在该示例中，PUDDING和PUDDING_PART类都拥有延期特性sugar_content和calories；其子孙类将实现它。例如，在COMPOSITE_PART就将sugar_content定义为计算其各成分的糖量总和。在COMPOSITE_PUDDING中，它就从COMPOSITE_PART中继承了该版本，并从PUDDING类中继承了一些延期版本的特性，并给予实现。

注意：当继承了两个同名特性时就将导致名称冲突，这就需要通过重命名来解决，除非一个特性是延期的，另一个是已实现的。在这种情况下，只有一个特性可用，那就是那个已实现的。对于继承机制带来的名字重载会引发难以处理的复杂度，因此通常建议在面向对象语言中不提供这种机制。

延期类要比Java和.NET中的“接口”更为复杂，它们都可以指定一个未来必须实现的契约，而且它们都可以包含已实现的特性，这样它能够提供的就是一个完整的特性集，从全部特性都是延期的类（只描述了要实现的特性）到全部都已实现的类（提供了完整实现的类）。它也可以只实现了一部分特性，这也是在架构和设计中使用面向对象技术的本质。

在金融契约示例中，CONTRACT和OPTION就是纯延期类的候选者，尽管它们可能无需对所有特性的实现进行延期。

13.5　面向对象模块性的评价和改进

在前一个小节中，我们总结了面向对象架构设计在这两个例子中的应用。接下来我们将按照本章之前描述的模块化标准来检查这个初步应用的结果。虽然类型系统和契约对可靠性也有贡献，不过我们在此只专注于可复用性和可扩展性。

13.5.1　复用操作

使用继承的原则之一是将公共特性放在可应用的最高等级类上，这样在子孙类中就无需重复它们；也就是说继承关系就是“as is”。如果既需要保留该功能，又需要修改其实现，那么只需要重新定义（或“重载”）继承而来的版本即可。“保留该功能”意味着仍然应用原来的契约，但其内容需要重写，不管它是已实现还是仍然为延期的。这和动态绑定能够很好地结合：一个客户端可以使用高层级类中定义的操作，例如my_pudding.sugar_content或my_contract.value，而无需知道实际使用的是哪个类中的哪个版本，也不管它是该类定义的还是继承的。

由于继承捕获了类之间的公共信息，因此要定义的特性会明显小于最大的t×f。任何减少代码的工作在此都是有价值的。代码重复始终是有害的，这是软件设计的通用规则，这是因为代码重复意味着在后续的配置管理、维护和调试时（当发现原代码存在问题时，必须修改所有的副本）会带来问题。正如David Parnas所说的那样，复制-粘贴是软件工程师的敌人。

实际上能够减少多少代码量，取决于继承结构的质量。我们发现抽象数据类型的原则在此仍然适用：由于在面向对象设计中，定义类型的关键在于分析其可应用的操作，在一个设计良好的继承结构中，最顶层的类中将收集在大量变体中可应用的特性。

该技术在函数式的模型中没有明确的等价物。在使用组合子时，对于每个组合子都需要定义不同的操作，肯定会存在一些代码重复。

13.5.2　可扩展性：添加类型

面向对象风格的架构对可扩展性的支持如何呢？系统最常见的扩展形式就是添加新的类型：一种新的布丁、布丁成分或金融契约。这种情况，是面向对象技术充分发挥自己作用的地方。你只需要在继承结构中找到最适合新类型的位置（也就是具有最多相同操作的类），然后编写一个新的类，让它继承一些原有的特性，根据自己的情况重新定义或修改原有的操作，并添加一些新的类可以使用的新特性和不变量。

动态绑定在此又起关键作用；面向对象方法的好处在于当客户类执行某个操作时，无需进行多路分支判断，诸如：“如果是水果沙拉（fruit salad），那么就以这种方式计算，而如果它是flan，那么就用那种方式计算，否则……”，这样的判断必须在每种操作中不断重复，更坏的是当添加或修改某种类型时，我们需要分别针对每个客户、每个操作进行相应的更新。这样的结构，需要客户类维护各种复杂的知识，了解不同结构提供的不同概念以及它们依赖的基础，这是架构退化的根源，也是面向对象之前的技术逐渐过时的原因。动态绑定解决了这个问题；客户应用程序只需调用my_contract.value，内建的机制就将自动选择出相应的版本，客户无需知道它们之间的不同。

没有哪种软件架构技术能够提供如此优美的解决方案，这也是面向对象方法所能够提供的最佳解决方案。

13.5.3　可扩展性：添加操作

面向对象技术对于可扩展性的支持，有一部分（此外还有诸如信息隐藏、泛型以及契约等重要特性）源于这样的假设：系统中最最重要的变化是由于添加一个新的类型，它和原有类型共享了部分操作，并添加了一些新的操作。经验确实表明，这些是实际系统中最常见的变化，也是面向对象技术展示其优势的地方。但如果我们想在原有类型中添加一些操作呢？例如，有些要使用pudding类的客户应用程序，可能想确定生产不同种类布丁所需的成本，虽然pudding类中并未包含与成本相关的信息。

与添加类别相比，对于添加新操作而言，函数式编程表现得不好也不坏，不管添加1个到f个，还是添加t个。不过，在面向对象解决方案中是无法享受这种中性的优点。其基本解决方案是在继承层次结构中合适的等级上添加一个特性。但这样做有两个潜在的缺点：

·由于继承是类间的强绑定（“is-a”），因此所有原有的子孙类都会受到影响。一般来说，在继承结构的较高层次的类中添加一个特性，经常会带来一些微妙的麻烦。

·如果客户系统不允许修改原有类或不允许访问其上下文，那么该解决方案甚至可能是不可用的。这在实践中是很常见的场景，特别是当这些类被归入到一个程序库中，例如将其放在一个金融契约程序库中。这样就不可能允许使用该程序库的应用程序作者修改它。

对此，基本的面向对象技术（Meyer 1997）并不够用。针对该问题，广泛应用的标准的面向对象解决方案是访问者模式（Gamma等1994）。在下面只是对该模式的概略描述，并不是标准的介绍，但也足够对该观点做一总结。（它是Meyer的《Touch of class：An Introduction to Programming Well[2008]》一书中所作的总结，它说明了这些基础概念是如何出现的，这本书可以选作第一学期的编程教科书。）图13-5列出了该模式所涉及的角色。

图　13-5：访问者模式的参与者

该模式在应用程序（诸如CLIENT之类的类）和原有类型（诸如针对特定类型T的T_TARGET，在我们的示例中就是PUDDING或CONTRACT）跳“双人芭蕾舞”，并且通过为每个可应用的操作F引入一个访问者类F_VISITOR，使其演变成“三角家庭”。诸如CLIENT之类的应用程序类在调用目标类的操作时，将会把相应的访问者作为参数传入，例如：

my_fruit_salad.accept（cost_visitor）

accept（v：VISITOR）命令将通过调用其参数v（在本例中就是cost_visitor）来执行该操作，它是诸如FRUIT_SALAD_visit类的一个特性，该名称中体现了目标类型。该特性是类描述的一部分，在此是FRUIT_SALAD；它将应用于相应类型的一个对象上（在此是fruit salad对象），它将作为参数传给特性T_visit。Current是Eiffel中针对当前对象的表示法（类似于大家所知道的“this”或“self”）。调用的目标，也就是图中的v，表示该操作将使用相应访问者类型的一个对象，诸如COST_VISITOR。当我们评价可扩展性时，对于软件架构最为关键的问题始终是“知识分发”（distribution of knowledge）。有一种方法能够实现可扩展性，那就是限制模块必须拥有的与其他模块相关的知识总量（这样当添加或修改某个模块时，对原有结构的影响就将最少）。为了让大家理解访问者模式的精妙之处，看看每个参与者必须知道什么、不需要知道什么是十分有必要的：

·目标类知道某种特定的类型，同时也知道类型层次结构中的上下文（例如，FRUIT_SALAD从COMPOSITE_PUDDING中继承，而COMPOSITE_PUDDING又从PUDDING中继承）。它无需知道外部所需要的新操作，诸如获取生产某种布丁所需的成本。

·访问者类必须知道与特定操作相关的所有信息，诸如成本，并为一组相关的类型提供相应的变量，以及通过参数指示相应的对象：在本例中，我们将发现诸如fruit_salad_cost、flan_cost、tart_cost等例程。

·客户类需要对特定类型的对象应用一个指定的操作，因此必须知道这些类型（只需知道它们存在，而不关心它们的其他属性）和操作（只要它们存在并且适用于指定类型，无需知道每个的特定算法）。

某些所需的操作，诸如accept和T_visit特性，必须是源于其祖先类的。图13-6展示了带继承关系（FRUIT_SALAD将简写为SALAD）的完整图。

图　13-6：整合在一起：构建布丁程序的架构

像这样的架构，经常用来解决在带有许多继承关系的原有结构中添加新操作的问题，它无需针对每个操作修改整个结构。语言处理程序（在集成开发环境中的编译及其他工具）就是存在这种情况的常见应用程序，它的底层结构是一个抽象语法树（AST）：每当新工具有需要时，更新AST类都是很麻烦的，根据其意图，需要在该树上执行一个遍历操作，对每个节点应用该工具选择的操作。（这也就是“访问”节点，同时解释了“访问者”这一术语和T_visit这一特性名称的来源。）

对于所有这些操作，客户必须能够针对任何目标类型的任何t执行t.accept（v）操作。这也就假定所有目标类型都是从公共类（在此就是PUDDING）中派生出来的，能够接受该特性的地方将以延期实现的形式声明它。这一需求很精妙，因为整个示例的目标正好是避免对现有的目标类进行修改。设计师在使用访问者模式时，通常会认为该需求是可接受的，也就是意味着我们关心的类必须有相同的祖先类，这是很常见的场景，它们是用来表示相同概念的不同类型，诸如PUDDING或CONTRACT，我们只需要在其祖先类中添加一个延期特性accept即可。

访问者模式应用十分广泛。读者可能已经知道它“美”在什么地方。但在我们的视角中，这并不是最终定论。评价标准包括：

带有特定accept功能的领域特定类需要拥有共同的祖先类，不应该受到与应用领域无关的概念的影响，无论是布丁、金融契约还是其他东西。

更令人不安的是，随着表示每种特定知识（基于特定类型的特定操作）的、小型的F_VISITOR类的大量增加，最终将出现“类爆炸”现象。不过，对于整个软件架构而言，这只是一个小污点。

要清除这个污点，需要在基本的面向对象框架中添加一个新的概念：代理。

13.6　代理：将操作封装到对象中

代理（Eiffel语言在1997年就在其基本的面向对象框架中添加了代理；C#中的“委托”和它是等价的）的基本想法可以用函数式编程相关论文中类似的话表述：我们将操作（函数式编程中的函数，面向对象编程中的特性）视为“一等公民”。在面向对象上下文中，在运行时的一等公民应该是对象，它和静态结构中的类相对应。

13.6.1　代理机制

代理是一个对象，用来表示特定类的一个特性，以便被调用。一个名为x.f（u，……）的特性的完整定义包括特性名f、目标对象x以及参数u，……而代理表达式则将指定f，并指定一部分或全部目标对象和参数，也可以都不指定，全部指定的代理称为已封闭的。对于那些未提供完整信息的代理定义，则称为开放的。该表达式将指定一个对象；该对象指定了该特性，并且包括设置指定值的参数。对于代理对象而言，其中一个可执行的操作是call，用来表示对f的一次调用；如果该代理还有未指定的参数，那么还必须将相应值作为参数传给call（对于已封闭的参数，则将使用代理定义中指定的值）。

最简单的代理表达式是agent f。在该例子中，所有参数都是开放的，但目标对象是已封闭的。因此，如果该代理表示式是a（它是执行赋值语句a：=agent f的结果，或者在调用p（agent f）中，形式变量p的值是a），那么a.call（[u,v]）的效果与x.f（u,v）是等价的。当然，它们之间也是有区别的，f（u,v）直接命名了该特性（尽管动态绑定意味着可能会使用不同的已知特性），而采用代理的形式，a只是一个名称，它是从其他程序单元中获取的。因此，在程序的这个位置上，对于特性而言除了其签名（或者是契约）之外是一无所知的。由于call是通用的程序库例程，它需要的参数类型只有一种。其解决方案是使用tuple，在此是两元素的tuple[u,v]。当采用这种格式时，agent f的目标对象是封闭的（它就是当前对象），而所有的参数都是开放的。

在此还有一个变体，agent x.f，其参数是开放的，目标对象是封闭的：它的目标对象是x而非当前对象。如果想让目标对象是开放的，那么可以使用agent{T}.f，其中T是x的类型。然后调用时就需要使用三个参数的tuple：a.call（[x,u，v]）。为了使某些参数是开放的，你可以使用相同的表示法，诸如agent x.f（{U}，v）（其典型的调用是a.call（[u]）），但由于u的类型U是可以清晰地从上下文中获得的，因此无需显式指定；在此只需使用问号，诸如agent x.f（?，v）。这同时也说明最初表示所有参数均是开放的格式的表达式agent f和agent x.f，实际上就是表达式agent f（?，?）和agent x.f（?，?）的缩写。

call机制就应用了动态绑定：f版本也应用了动态绑定，正如非代理的调用一样，它取决于动态的目标类型。

如果f表示的是一个查询而非命令，那么你可以获取相应的代理，其结果是用item来代替调用，也就是a.item（[x,u，v]）（它将执行一个调用，并获得其执行结果）；你也可以先调用它然后再访问a.last_result，这样就遵从了命令-查询分离原则，它将返回相同的值，在连续调用时无需执行后续的调用。

对于更高级的应用，它不仅是基于一个针对原有特性f的代理，你也可以在同一行中使用代理，例如editor_window.set_mouse_enter_action（agent do text.hightlight end），它展示了图形用户界面上的典型应用；这是EiffelVision程序库（Eiffel软件：EiffelVision文档）中事件驱动编程的基本风格。内联的代理提供了与函数式语言中lambda表达式相同的机制：编写操作，使它们在软件中像数值一样直接可用，就像其他“一等公民”一样。

总而言之，代理使面向对象框架能够定义更高等级的功能模块，就像在函数式语言中那样，并提供了功能相当的表达式。

13.6.2　代理的应用范围

代理现在已经成为了对基本面向对象机制的重要、自然的补充。在实践中，它们广泛应用于：

·迭代：为一个容器结构中的所有元素应用一个可变的操作，因此使用代理是很自然的决策。

·正如前面所提到的GUI编程领域。

·数学计算，正如之前示例中那样对在特定时间间隔内的特定函数进行整合，表示为一个代理。

·反射，一个不仅提供特性的属性（不仅能够通过call和item来调用），还能提供类的代理。

在我们的研究中，代理已经被证实可以用来把设计模式替换为可复用组件（Arnout 2004；Arnout和Meyer 2006；Meyer 2004；Meyer和Arnout 2006）。其原因在于，任何应用程序的设计师在使用模式之前必须详细地学习，包括架构和实现，然后从头开始构建整个应用程序，而可复用组件能够直接通过API来使用它。成功的案例包括观察者设计模式（Meyer 2004；Meyer 2008），没有人在看到基于代理的解决方案之后，还会继续尝试使用该模式，此外还有工厂模式（Arnout和Meyer 2006）和接下来还将讨论的访问者模式。

13.6.3　基于代理的程序库使访问者模式变成多余的

对于用访问者模式笨拙地解决的问题，代理机制提供了更好的解决方法：向现有的类型添加操作，并且不改变其支持的类。该解决方案在（Meyer和Arnout 2006）中详细地介绍过，你可以在ETH Chair of Software Engineering（苏黎士理工大学软件工程主席，网站为http://se.ethz.ch）下载网站的open source library（开源程序库）中找到它。

它可以使客户应用程序接口变得十分简单。对于目标类（诸如PUDDING、CONTRACT）无需做任何修改：它们没有其他要接受的特性。它可以复用需要的类，并接受其子孙类：它不会产生访问者类的大爆炸，只需要一个VISITOR程序库类，学习register和visit两个特性的基本用法即可。客户应用程序的设计师无需理解这些类的内部细节，也不用关心访问者模式的实现，只需要通过API来应用这个基本模式即可：

1.声明一个表示访问者对象的变量，通过VISITOR的泛型参数指定最顶部的目标类型，然后创建相应的对象：

pudding_visitor：VISITOR[PUDDING]

create pudding_visitor

2.针对在目标结构中特定类型的对象，为其中每个要执行的操作注册与访问者相应的代理：

pudding_visitor.register（agent fruit_salad_cost）

3.要执行特定对象（通常是遍历操作的一部分）中的操作是，只需使用程序库类VISITOR的visit特性，如下所示：

pudding_visitor.visit（my_pudding）

这个接口的所有内容只包括：一个访问者对象、可应用操作的注册，以及一个独立的visit操作。我们简单地解释一下这三个属性：

·对于将应用的操作（诸如fruit_salad_cost）都需要写出来，不管选择了什么架构。它们通常是以例程的形式存在的，这样就可以使用表示法agent fruit_salad_cost调用它；如果不是（特别是它只是一个很简单的操作时），客户应用程序可以使用内联的代理来引入一个例程。

·当你最初看到只需一个VISITOR类、一个用来添加访问者的register例程就足够了的时候，可能会感到奇怪。在访问者模式解决方案中，当调用t.accept（v）时，t指出了目标类型（某种特定的布丁），但在此register例程却没有指定任何此类信息。那么它是如何找到需应用的正确的操作变体（是fruit salad的cost操作，还是plan的cost操作）呢？其答案是代理机制中反射属性所带来的结果：一个代理对象收集了与相关特性有关的所有信息，包括其签名。因此agent fruit_salad_cost所包含的信息就是fruit salad可应用的例程（从签名fruit_salad_cost（fs：FRUIT_SALAD）中也可以获得，对于内联代理而言，则可以从其文本中获得）。这样使得我们可以对VISITOR内部的数据结构进行组织，所以在调用visit时（诸如pudding_visitor.visit（my_pudding）），visit例程能够找到正确的例程，或者基于目标对象的动态类型的例程，在此针对特定布丁类型P的pudding_visitor：VISITOR[P]也能够匹配，它是由类型系统强制静态绑定的，对象的类型将动态和参数相关联，在此是多态的my_pudding。

·该技术也享受到了继承和动态绑定在复用方面的好处：如果某个例程注册成通用的布丁类型（也就是COMPOSITE_PUDDING），而且没有任何注册到特定类型（例如，对于所有合成布丁而言，成本计算将采用相同的方法）的例程，那么visit将使用最佳匹配者。

这里描述的机制对传统的面向对象机制提供了一些补充。当添加的类型将提供与原有操作不同的操作时，继承和动态绑定能够提供很好的解决方案。对于和其成对的、想在不修改原有类型的基础上添加操作问题，这里介绍的解决方案也适用。

接下来，我们根据之前关于知识分发的模块性评价标准“谁必须知道什么？”来看看该方法：

·目标类只需要知道体现相应类型的、诸如sugar_content的基本操作。

·应用程序只需要知道它所使用的目标类的接口，以及register和visit这两个本质的特性，它们是由VISITOR程序库类实现的。如果目标类型需要添加新的操作（在目标类设计时是无法预见的），诸如我们示例中的cost（计算成本），只需要提供该操作变量，以及所需的目标类型，另外我们需要知道当缺乏对应的注册项时，将会针对更特定的类型使用更通用的操作。

·程序库类VISITOR完全不需要知道特定的目标类型或特定的应用程序。

如果还想进一步减少系统不同部分所需信息的总量，看起来似乎是不可能的。在我们的观点中，只有一个问题还没有结论，那就是这种基础性机制如果通过程序库实现，或者不知何故屈从于特定的语言构造，那么它是否仍然是可用的。

13.6.4　评价

当引入代理这一概念时，最初大家关注的是它是否会带来引发混乱的冗余，因为它提供了一种也符合标准面向对象机制的备选方法。（这种关注在Eiffel中特别强烈，因为该语言在设计时遵循的原则是“一个好方法，做任何事情”。）这种问题并没有发生：代理在面向对象库中找到了适合自己的位置，设计师在决定何时应该使用时并不会觉得困难。在实践中，代理的各种重要应用（特别是用来代替设计模式的应用）都同时依赖于泛型、继承、多态、动态绑定以及其他高级的面向对象机制。这也使大家强化了“该机制是成功的面向对象技术中必要的组件”的认识。

注意：也有另一种不同的观点，你可以看看Sun公司白皮书中对于Java为什么不需要代理（或委托）之类的机制所做的解释（Sun Microsystems，1997）。它展示了如何通过Java中的“内部类”（inner classes）来模拟这一机制。虽然这很有趣、争论也很有价值、很成功，但我们的观点是，它实际证明了相反的观点。内部类能够完成这一任务，但你不难发现，当用这种方法替代访问者模式时仍然会带来类爆炸问题，与基于代理的解决方案相比，其简洁、优雅、模块性都更逊一筹。

正如之前所说的那样，代理使面向对象设计也能够实现在函数式编程中通过定义更高等级函数（操作可以作为操作的输入或输出，这样就可以递归使用相同的属性）这一机制所实现的威力。甚至在内联代理中还会发现lambda表达式的身影。这一机制对函数式编程产生了公开的影响，并且原则上吸引了其支持者的关注，虽然人们担心，从某种角度看这是罪行向美德所表示的敬意（La Rochefoucauld 1665）。[1]

除了语法之外，最重要的区别在于代理不仅可以封装纯函数（查询），还能够封装命令。不过，绝对的纯粹（没有任何副作用）是不存在的，特别是对于架构的讨论而言更是如此，至少只要我们想遵从命令-查询分离原则，就能够保留该原则带来的好处（表达式的引用透明），而不需在无状态的模型中强行添加一个有状态的模型。

最终，大家认为代理对对象技术的模块性还是有贡献的，但它只是所有元素中的一个，这些在前面的讨论中有涉及，但只有一小部分。它是这些元素的组合体，超出了函数式方法所能够提供的函数，使面向对象设计成为了构建美妙架构的最有效方法。

[1]La Rochefoucauld的名言是Hypocrisy is the homage that vice pays to virtue（虚伪是恶德对美德所表示的崇敬），在此似乎表示的是“面向对象技术”不是美德，“函数式”是美德，因此面向对象技术通过“代理”技术来向“函数式”这一美德表示崇敬。

致谢

我很感谢那些对本章的草稿提出重要意见的朋友，明显，致谢不是担保（同样明显，函数式编程的那些重要人物非常和善地分享了建设性的意见，同时恐怕又完全没有受我的观点的影响）。特别重要的是Simon Peyton Jones、Erik Meijer和Diomidis Spinellis的意见。对我关于John Hughes的经典文章所提的问题，他的回答详细而富于启发。本章最后部分提到的Visitor库是Karine Arnout（Karine Bezault）的作品。我感谢Gloria Muller，他在ETH硕士论文中进一步讨论了针对Eiffel实现像Haskell那样的支持库。我特别感谢这本书的编辑Diomidis Spinellis和Georgios Gousios，他们给我机会出版这些讨论，而且他们对我的延迟交稿也给予了极大的耐心。

参考资料

Arnout,Karine.2004."From patterns to components."Ph.D.thesis,ETH Zurich.可以在http://se.inf.ethz.ch/people/arnout/patterns/找到。

Arnout,Karine,and Bertrand Meyer.2006."Pattern componentization：the Factory example."Innovations in Systems and Software Technology（a NASA Journal）.New York,NY：Springer-Verlag.可以在http://www.springerlink.com/content/am08351v30460827/找到。

Eber,Jean-Marc,based on joint theoretical work with Simon Peyton-Jones and Pierre Weis.2001."Compositional description,valuation,and management of financial contracts：the MLFi language."可以在http://www.lexifi.com/Downloads/MLFiPresentation.ppt找到。Ecma International.2006.Eiffel：Analysis,Design and Programming Language.ECMA-367.可以在http://www.ecma-international.org/publications/standards/Ecma-367.htm找到。Frankau,Simon,Diomidis Spinellis,Nick Nassuphis,and Christoph Burgard.2008.

"Commerical uses：Going functional on exotic trades，"Journal of Functional Programming，19（1）：2745，October.

Gamma,Erich,et al.1994.Design Patterns：Elements of Reusable Object-Oriented Software.Boston,MA：Addison-Wesley.

Hughes,John. 1989."Why functional programming matters."Computer Journal,vol.32，no.2：98-107（revision of a 1984 paper）.可以在http://www.cs.chalmers.se/～rjmh/Papers/whyfp.pdf找到。

La Rochefoucauld,François de.1665.Réflexions ou sentences et maximes morales.

Meyer,Bertrand,and Karine Arnout. 2006."Componentization：the Visitor example."Computer（IEEE），vol.39，no.7：23-30.可以在http://se.ethz.ch/～meyer/publications/computer/visitor.pdf找到。

Meyer,Bertrand.1991.Eiffel：The Language.（second printing.）Upper Saddle River,NJ：Prentice Hall.

Meyer,Bertrand.1997.Object-Oriented Software Construction,Second Edition.Upper Saddle River,NJ：Prentice Hall.可以在http://archive.eiffel.com/doc/oosc/找到。

Meyer,Bertrand.2004."The power of abstraction,reuse and simplicity：An object-oriented library for event-driven design."From Object-Orientation to Formal Methods：Essays in Memory of Ole-Johan Dahl,eds.Olaf Owe,Stein Krogdahl,Tom Lyche,Lecture Notes in Computer Science 2635，pp.236-271.New York,NY：Springer-Verlag.可以在http://se.ethz.ch/～meyer/publications/lncs/events.pdf找到。

Meyer,Bertrand. 2008.Touch of Class：An Introduction to Programming Well.New York,NY：Springer-Verlag.见http://touch.ethz.ch.

Peyton-Jones,Simon,Jean-Marc Eber,and Julian Seward. 2000."Composing contracts：An adventure in financial engineering."Functional pearl,in ACM SIGPLAN International Conference on Functional Programming（ICFP'00），Montreal,Canada,September'00.ACM Press,pp.280-292.可以在http://citeseer.ist.psu.edu/jones00composing.html找到。Peyton-Jones,Simon,and Philip Wadler.1993."Imperative functional programming."Twentieth Annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages.Charleston,South Carolina,pp.71-84.可以在http://citeseer.ist.psu.edu/peytonjones93imperative.html找到。

Steingold,Sam.Online at http://www.podval.org/～sds/ocaml-sucks.html.

Sun Microsystems.1997."About Microsoft's 'Delegates.'"White paper by the Java Language Team at JavaSoft.可以在http://java.sun.com/docs/white/delegates.html找到。

Wadler,Philip.1995."Monads for functional programming."Advanced Functional Programming,Lecture Notes in Computer Science 925.Eds.J.Jeuring and E.Meijer.New York,NY：Springer-Verlag.可以在http://homepages.inf.ed.ac.uk/wadler/papers/marktoberdorf/baastad.pdf找到。

Walden,Kim,and Jean-Marc Nerson.1994.Seamless Object-Oriented Software Architecture.Upper Saddle River,NJ：Prentice Hall.可以在http://www.bon-method.com/index_normal.htm找到。

第14章　重读经典Panagiotis Louridas

似乎在所有的科学领域中，都有一些著作和人名是不能不提的。目前还活着的冠军可能是Norm Chomsky。根据1992年4月在MIT Tech Talk上的一篇文章，Chomsky是之前20年中出版的著作中引用最多的人名。艺术和人文学科引用索引的前10名是Marx、Lenin、Shakespeare、Aristotle、the Bible、Plato、Freud、Chomsky、Hegel和Cicero。在科学引用索引中，他在1972—1992年间，被引用了1619次。

在软件工程方面，勋章可能要颁给《设计模式：可复用面向对象软件的基础》（即“Gang of Four”书[Gamma等1994]，[1]）。在Google上精确查询该书的书名得到了173 000条结果（在2008年春天）。如果我们把注意力放在更为学术化的方面，查询ACM Digital Library得到了1527条结果。设计模式社区是过去20年中软件工程领域最有活力的社区之一。

本章再次增加了这两项的引用计数。

人们可以把设计模式的普及归功于《设计模式》一书。它不仅可以看作是设计模式运动的起始点，也可以看作是返回点：与设计模式相关的内容非常多，但关于这个主题的大多数讨论都是围绕这本书中列出的设计模式。我们并不是排除其他模式，但这本书中提到的23个模式肯定可以称之为经典。

但是这本书中迷人的部分不是那些设计模式本身，而是第1章简介，它提供了许多设计模式背后的理由，也提供了将它们串起来的通用线索。从中我们可以发现可复用面向对象设计的原则。其中第二个原则就是更倾向使用对象组合（“包含”关系），而不是类继承（“是一种”关系）。（第一个原则是“针对接口编程，而不是针对实现编程”，对于过去40年中看到过有关封装的建议的人来说，这一点应该很清楚了。）

对于那些没有跟随面向对象编程在20世纪八九十年代来到中心舞台的程序员来说，这条规则可能似乎没有那么重要。但如果你还能回忆起那个年代，在面向对象编程中的一个决定性概念就是继承。以Bjarne Stroustrup在《The C++Programming Language》（1985）中的描述为例。我们发现有这样的内容：

C++是一种通用目的的编程语言，侧重于系统编程。它：

·是更好的C语言。

·支持数据抽象。

·支持面向对象编程。

·支持泛型编程。

如果我们想弄清楚“支持面向对象编程”究竟指的是什么，我们发现：

编程方式是：

·确定你想要的那些类。

·为每个类提供一组完整的操作。

·通过继承来明确那些共性。

现在对比一下另一本经典著作，Joshua Bloch的《Effective Java》（2008）。我们发现至少有三点劝诫是针对继承的：

·倾向于组合而非继承。

·设计继承并为之编写文档，否则不要用继承。

·倾向于接口而非抽象类。

那么继承应该避免使用吗？这在学术上没有定论。在Microsoft Windows上编程可能会让人非常沮丧，即使是利用很舒服的书（Charles Petzold的《Programming Windows》[1999]）中介绍的方法。当第一批Windows编程框架推出时（源自Borland和Microsoft），它们就像是一股清新的空气。在那之前，创建一个简单的窗口都极其复杂：程序员们满怀兴趣地得知，要在Microsoft Windows中编程，他们必须面对所谓的window类，而这与C++类没有任何关系。在新的框架中，你只需创建一个子类，继承框架提供的类，事情就完成了。我们很高兴能够突然摆脱所有的苦工（或者说几乎所有的苦工），也很高兴能够突然发现面向对象的这样一种简洁应用方式。

在Microsoft Windows上编程只是一个例子，对面向对象和继承的热情无所不在。很奇怪，我们现在知道我们一直搞错了，但也许没有错得那么离谱。继承在本质上可能并不坏。像所有技术一样，它可以用得好，也可以用得不好，不好的继承已经在许多地方都有讨论了（《设计模式》是一个不错的开始）。这里我们将介绍一个漂亮软件系统的例子，它以继承作为基础。这个系统就是Smalltalk。

Smalltalk是一种纯面向对象语言，尽管它从未成为主流语言，但却以多种方式影响了编程语言的发展。也许另一种对后来的计算机语言产生了这么多影响的编程语言就是Algol60，它的影响也超过了它的实际使用。

这不是对Smalltalk编程语言及其环境的介绍（这两方面实际上是在一起的），而是介绍其中基本的架构思想，以及这些思想如何为我们的编程工作提供指导。借用设计心理学的一个术语，这里讨论的是基本设计原则和它们提供给程序员的“可操作暗示”（affordance）。Donald Norman在《The Psychology of Everyday Things》（1988）中透彻地（并且有趣地）解释了可操作暗示的概念。简而言之，一个对象的出现允许我们（有时甚至是诱使我们）做某些事情。悬挂的绳子诱使我们走过去拉一下，水平的把手诱使我们推一下，门把手诱使我们走过去旋转它。同样，编程语言表现出来的样子诱使程序员利用它做某些事情。打造得很漂亮的语言拥有美丽的架构，这会在我们用它编写的程序中体现出来。

这种思想的另一个强烈表达方式是赛故二氏假说（shapir-whorf Hypothesis,SWH），它声称语言决定思想。这一综合症已经让语言学家和编程语言设计者们兴奋了好些年了。《The C++Programming Language》第1版的前言就是从SWH开始的，K.E.Iversion的1980年图灵奖演讲谈的就是表示法对表达思想的重要性。SWH是有争议的，毕竟，每个人都曾经遇到过找不到一些词来表达自己的思想的情况，所以我们能思考的超过了我们能说的。但是在计算机代码中，语言和程序的关系是清楚的。我们知道一些计算机语言是图灵完备的，但我们也知道，对于某些事情来说，一些语言比另一些语言更合适。但除了影响程序的架构之外，语言架构的本身也是很有趣的。我们来看看Smalltalk自身的架构，即它的实现选择、设计概念和模式。今天，我们会在最近的编程语言中看到其中的多个方面，那些现在不再见到的方面则让我们能够停下来，反思一下它们消失的原因。我们在这里并不假定你已具备Smalltalk的相关知识，但到这章结束时，我们就已经介绍Smalltalk的主要部分。我们会突出设计原则，并通过小代码片段来展现这些原则。强大的设计原则有一个好处，就是要学的东西不多，而当你一旦掌握了这些原则，整个基础架构就从这些原则中自然生成了。我们要参考的Smalltalk系统是Squeak（http://www.squeak.org），它是一个开放源代码的实现。某些代码示例在第一次阅读时可能难以理解，因为我们会引入一些非常规的概念，但它们会在随后的示例中得到说明，所以最好是努力读到最后，然后再回来读那些不太能理解的部分。同时，我们没有低估读者的智商。研究Smalltalk将发现一些特征，这些特征在你喜欢的语言里不一定有。这应该不是一个问题。在软件开发中有一条经过时间检验的准则，即你要使用的某一项特征在你所使用的语言中不一定直接支持，经过一些努力，你会在你所选择的语言中找到一种优雅的替代方式。根据Steve McConnell的《Code Complete（2004）》，这被称为“编程为一种语言”：

理解“用一种语言编程”与“编程为一种语言”之间的差异是很重要的……绝大多数重要的编程原则不取决于具体的语言，而是取决于你使用它们的方式。如果你的语言缺少你想使用的结构，或者易于出现其他类型的问题，请试着进行补偿。发明你自己的编码规范、标准、类库和其他扩展。

实际上，与SWH相对的是对程序员创造性（或执着）的推崇。笔者记得，当面向对象在20世纪90年代成为一种必需品时，本地技术书店到了一本书，主题是面向对象的汇编语言。最近，Randall Hide的“High Level Assembler（HLA）”将汇编与类、继承和其他技术结合在了一起。

我们对待一门编程语言的方法就像我们对待一本经典书籍一样。在我们急切开始之前，让我们先来看看Italo Calvino的文章“Why Read the Classics”（1986）中提出的一些定义：

经典作品是那些你经常听人家说“我正在重读……”而不是“我正在读……”的书。

经典作品是这样一些书，它们对读过并喜爱它们的人构成一种宝贵的经验；但是对那些保留这个机会，等到享受它们的最佳状态来临时才阅读它们的人，它们也仍然是一种丰富的经验。经典作品是一些产生某种特殊影响的书，它们要么自己以遗忘的方式给我们的想象力打下印记，要么乔装成个人或集体的无意识隐藏在深层记忆中。

一部经典作品是一本每次重读都好像初读那样带来发现的书。

一部经典作品是一本从不会耗尽它要向读者说的一切东西的书。

经典作品是这样一些书，它们带着以前的解释的特殊气氛走向我们，背后拖着它们经过文化或多种文化（或只是多种语言和风俗习惯）时留下的足迹。

一部经典作品不一定要教一些我们不知道的东西；有时候我们在一部经典作品中发现我们已知道或总以为我们已知道的东西，却没有料到这个作者早就说了，或那个想法与那部经典作品有一种特殊联系。这种发现同时也是非常令人满足的意外，例如当我们弄清楚一个想法的来源，或它与某个文本的联系，或谁先说了，我们总会有这种感觉。

经典作品是这样一些书，我们越是道听途说，以为我们懂了，当我们实际读它们时，我们就越是觉得它们独特、意想不到和新颖。

一部经典作品是一部早于其他经典作品的作品；但是那些先读过其他经典作品的人，一下子就认出它在众多经典作品的系谱图中的位置。

当然，书籍不是计算机语言，但这些定义可能仍适合于我们。

14.1　所有东西都是对象

今天流行的面向对象计算机语言（C++、Java和C#）并不是纯面向对象的。并非所有东西都是对象。某些类型是原生类型。因此，举个例子说，我们不能为一个整数类型提供子类。运算以一般的方式在纯数字上执行，而不是调用对象的方法。这带来了性能上的好处，并且对于从过程式语言转向面向对象的人来说，这也比较容易理解。

但是，如果我们决定将所有东西都作为一个对象，那么情况就大为不同。在Smalltalk中，不超过31位长的整数是SmallInteger类的实例（实际上，存在一个抽象的Integer类及其子类SmallInteger、LargePositiveInteger和LargeNegativeInteger，系统会根据需要自动进行转换）。我们可以对它们执行普通的运算操作。SmallInteger类提供了至少670个方法（用Smalltalk的说法，是selector—选择器），我们可以很容易地发现下面的代码片段：

SmallInteger allSelectors size

考察这段代码如何工作是很有意义的。allSelectors是一个类选择器，它完成的功能正如其名。它返回一个类的所有选择器，放在一个Set中（实际上是一个IdentitySet，但在这里对我们并不重要）。这个Set本身又是一个一级对象，带有自己的选择器。其中的一个选择器名为size，告诉我们它包含的元素的个数。

在SmallInteger的选择器中，我们可以找到期望的运算操作。我们也可以找到三角和对数函数、计算阶乘的函数、计算最大公约数的函数和计算最小公倍数的函数。还有位操作的函数和许多其他函数。

我们在其他语言中遇到的整型原生类型在Smalltalk中实际上都是SmallInteger实例。

这解释为什么带参数的选择器在每个参数前面有一个冒号（：）。算术运算和逻辑运算的选择器，如上例中的+，是这条规则的例外情况。Transcript是一个类，表示类似系统控制台这样的东西。cr代表回车，分号（；）将信息连接在一起，所以cr也被发往Transcript。我们可以在一个解释窗口（在Smalltalk中通常称为工作空间，）中执行这段代码，直接看到结果。当然，并不是所有670个SmallInteger的方法都定义在SmallInteger中。SmallInteger处于一个继承层次结构中，如图14-1所示，其中我们也可以看到SmallInteger的每个祖先的选择器数目。大多数选择器继承自Object，理解Object提供了什么功能就可以解释Smalltalk架构的许多方面（在Squeak中，继承层次结构真正的根是ProtoObject，但这只是一个不太重要的细节）。

2 raisedTo：5

能工作，更有趣的是，这也解释了为什么下面的代码也能工作：

（7+3）timesRepeat：[Transcript show：'Hello,World'；cr]

Object的实例都有自己可以自由处理的比较选择器（包括相等，用=表示，也包括标识符相同，用==表示）；复制选择器（包括深复制，即调用deepCopy，也包括浅复制，即调用shallowCopy）；在流中打印的选择器、出错处理、调试、消息处理及其他选择器。这几百个对象方法中，只有少数是日常编程会用到的。Smalltalk中的方法被分成组，称为协议（protocol），查看协议描述让我们能够更容易地找到方法。

在Smalltalk中，方法本身也是一级对象。为了弄清楚这一点对于整体架构的意义，请看下面的代码：

aRectangle intersects：anotherRectangle

其中aRectangle和anotherRectangle都是Rectangle类的实例。当消息接收者aRectangle（在Smalltalk中接收消息的对象）收到intersects：消息时，Smalltalk解释器会做下面的事情（Conroy和Pelegri-Llopart 1983）：

图　14-1：SmallInteger继承层次结构

1.确定消息接收者的类。

2.在该类及其祖先类中查找消息选择器。

3.取得与找到的类的消息选择器相关联的方法。

在Smalltalk中，不仅像数字这样的东西是对象，类也是对象。所以，SmallInteger、Object、Rectangle等都是对象。当解释器在一个类中查找消息选择器时（上面列表中的第2步），它会查找对应的类对象的内容。更准确地说，会在它的方法字典中查找。Dictionary类的实例将键和值关联在一起，方法字典将每个选择器与对应的Compiled Method实例关联在一起。

顺便说一下，intersects：在Smalltalk中可以优雅地实现为：

（origin max：aRectangle origin）＜（corner min：aRectangle corner）

要明白它的工作原理，你需要知道origin选择器返回矩形左上角的顶点（类Point的一个实例），corner选择器返回矩形右下角的顶点，max：返回消息接收者和参数唯一确定的矩形的右下角，min返回消息接收者和参数唯一确定的矩形的左上角。虽然这种实现很简明，但它不是最优化的解决方案，Squeak提供了另一种方法：

intersects：aRectangle

"Answer whether aRectangle intersects the receiver anywhere."

"Optimized；old code answered：

（origin max：aRectangle origin）＜（corner min：aRectangle corner）"

|rOrigin rCorner|

rOrigin：=aRectangle origin.

rCorner：=aRectangle corner.

rCorner x＜=origin x ifTrue：[^false].

rCorner y＜=origin y ifTrue：[^false].

rOrigin x＞=corner x ifTrue：[^false].

rOrigin y＞=corner y ifTrue：[^false].

^true

这种方法更快，但不那么漂亮。然而，这让我们有机会介绍一些Smalltalk的语法。方法局部使用的变量声明在||中。赋值操作符是：=，^与C++和Java中的return等价，小数点（.）分隔语句。方括号（[]）之内的代码称为一个语句块，这是Smalltalk架构中的一个重要概念。语句块是一个闭包，也就是一段代码，可以访问围绕它的范围内定义的变量。Smalltalk中的语句块由BlockContext来表示。当语句块对象接收到消息值时，语句块的内容就得到执行，在大多数情况下（像这个例子中），消息是隐式发送的。Smalltalk中的注释放在双引号内，单引号用于表示字符串。

BlockContext可以使用消息接收者、参数和临时变量，也可以使用创建它的上下文中的消息发送者。有一个类似的类，名为MethodContext，表示了与方法（正如我们前面看到的，它由CompiledMethod表示，是一个字节码数组）执行相关的所有动态状态。

在一种面向对象的语言中，我们会想到，BlockContext和MethodContext都是ContextPart的子类。ContextPart在其超类InstructionStream的基础上添加了执行语义。InstrucionStream类的实例可以解释Smalltalk代码。Instruction Stream的超类是Object，继承关系到此为止。

除了value选择器之外，语句块还有fork选择器，它在语言中实现了并发。因为Smalltalk中的所有东西都是对象，所以进程就是Process类的实例。Delay类让我们可以将进程的执行挂起一段时间，当它收到wait消息时，Delay对象会将当前执行的进程挂起。将这些结合起来，我们可以利用下面的代码实现一个简单的时钟（Goldberg和Robson，1989）：

[[true]whileTrue：

[Time now printString displayAt：100@100.

（Delay forSeconds：1）wait]]fork

只要whileTrue：选择器的消息接收者语句块的值为true，它就会执行它的语句块参数。@字符是Number类的一个选择器，它将构造Point类的实例。

语句块也为我们提供了基本的错误处理功能，思路就是：当某些东西出错时，我们指定执行某些语句块。例如，在Collection对象（一个对象容器）中，remove：方法会从集合中删除指定的元素。方法remove：ifAbsent：会尝试从集合中删除指定的元素，如果该元素不存在，它就会执行作为ifAbsent：参数的语句块。在一个代码最少化的漂亮例子里，第一个方法是基于第二个方法来实现的：

remove：oldObject

"Remove oldObject from the receiver's elements.Answer oldObject

unless no element is equal to oldObject,in which case,raise an error.

ArrayedCollections cannot respond to this message."

^self remove：oldObject ifAbsent：[self errorNotFound：oldObject]

remove：oldObject ifAbsent：anExceptionBlock

"Remove oldObject from the receiver's elements.If several of the

elements are equal to oldObject,only one is removed.If no element is

equal to oldObject,answer the result of evaluating anExceptionBlock.

Otherwise,answer the argument,oldObject.ArrayedCollections cannot

respond to this message."

self subclassResponsibility

self是对当前对象的引用（与C++和Java中的this等价），它是一个保留的名称，是具有固定语义的伪变量。还有其他一些伪变量：super是对超类的引用（等价于Java中的super）；nil、true和false的意思很明白；最后还有thisContext，我们可以在subclassResponsibility方法中看到它。这个选择器定义在Object中，只是说明子类必须覆写它。

subclassResponsibility

"This message sets up a framework for the behavior of the class's subclasses.

Announce that the subclass should have implemented this message."

self error：'My subclass should have overridden'，thisContext

sender selector printString

thisContext伪变量是对当前执行上下文的引用，即当前执行的方法或语句块，所以它是MethodContext或BlockContext的当前执行实例的引用。sender选择器返回消息发送的上下文，selector给出了该方法的选择器。所有这些都是对象。

将代码当作对象来处理不是新思想，Lisp的强大之处就在于它对代码和数据一视同仁。它允许我们利用反射来编程，也就是说，进行元编程（metaprogramming）。

随着时间的推移，元编程这种思想已经变得越来越重要。C++中的模板元编程是一种相当不同的方式：我们利用了C++编译器会在编译时生成模板代码这一事实，在编译时执行计算（Abrahams和Gurtovoy 2005）。这种技术带来了令人兴奋的可能性，但却需要复杂的编程技能。在Java中元编程是通过反射来实现的，它是语言的一部分，虽然使用反射的Java代码可能会比较麻烦。

当我们需要在运行时刻构建一个菜单时，一个相关的问题就凸显出来了。菜单会将菜单项和处理代码关联起来，当用户选择相关的菜单项时，就会执行对应的处理代码。如果我们能够通过名称来引用这些处理代码，我们就能够使用下面这样的代码动态地构建一个菜单：

CustomMenu new addList：#（

#（'red'#redHandler）

#（'green'#greenHandler）

#（'blue'#blueHandler））；startUpWithCaption：'Colors'.

在Smalltalk中，分隔符#（）代表了数组。我们通过一个包含多个标签-处理代码对的列表，创建了一个新的菜单及其菜单项。这些处理代码是选择器（在真实的代码中，我们需要给出它们的实现）。处理代码前面加上了#字符，这在Smalltalk中表示符号（symbol）。我们可以认为符号和字符串类似。符号通常用于代表类或方法的名称。

反射让我们能够以简洁的方式来实现抽象工厂（Abstract Factory）设计模式（Alpert等1998）。如果我们需要一个工厂类，它在运行时可以根据用户的指定来实例化类的对象，那么我们可以采用下面的方式：

makeCar：manufacturersName

"manufacturersName is a Symbol,such as#Ford，#Toyota,or#Porsche."

|carClass|

carClass：=Smalltalk

at：（manufacturersName，#Car）asSymbol

ifAbsent：[^nil].

^carClass new

当用户给出一个制造商的名称时，我们在它后面加上单词"Car"，创建类名。对于#Ford来说，类名将是#FordCar；对于#Toyota来说，类名将是#ToyotaCar，等等。字符串连接在Smalltalk中是用逗号（，）表示的，我们希望连接得到的字符串成为一个符号，所以我们调用它的asSymbol方法。在Smalltalk中，所有的类名都存在Smalltalk字典中，它是SystemDictionary类的唯一实例。当我们找到要求的类名时，我们就返回该类的一个实例。

我们已经看了几个Smalltalk的例子，但还没有看到类定义。在Smalltalk中，类的构建方式和其他东西的构建方式是一样的：向相应的消息接收者发送必要的消息。我们先在Smalltalk环境中，从完成下面的模板开始：

NameOfSuperclass subclass：#NameOfSubclass

instanceVariableNames：''

classVariableNames：''

poolDictionaries：''

category：'Unknown'

我们用真实的名称替代Name Of Superclass和Name Of Subclass。在instanceVariableNames中，我们列出实例变量，在classVariableNames中列出类变量，在category中标出类所属的分类（在Smalltalk中，类被归到一些分类中，类似于其他语言中的命名空间或包）。poolDictionaries中列出了我们与其他类共享的字典，这是在Smalltalk中共享变量的机制。当该模板的各项细节填入之后，它们就被传递给Class类的subclass选择器。

subclass：t instanceVariableNames：f classVariableNames：d

poolDictionaries：s category：cat

"This is the standard initialization message for creating a new class as a

subclass of an existing class（the receiver）."

^（ClassBuilder new）

superclass：self

subclass：t

instanceVariableNames：f

classVariableNames：d

poolDictionaries：s

category：cat

subclass选择器会创建一个ClassBuilder类的实例，它将创建新类或修改已有的类。我们向ClassBuilder实例发出必要的信息，这样就可以根据我们在类模板中输入的信息创建新的类。

在对象上做所有事情都是通过发送消息来完成，这使我们需要把握的概念不是很多。它也让我们限制了语言中语法结构的数量。让编程语言最小化有很长的历史。在关于Lisp的第一篇论文（McCarthy 1960）中，我们发现Lisp比较了两类表达式：一是S表达式（或句法表达式），它是从列表构建的表达式；二是M表达式（或元表达式），它是将S表达式作为数据的表达式。最后，程序员们总是选择使用S表达式，所以Lisp就变成了我们今天所知道的样子：一种几乎没有语法的语言，因为所有的东西，包括程序和数据，都是列表。它取决于你对Lisp的态度，这证明了一个简单的思想就足够表达最复杂的结构（或者人们可以被强迫接受任何东西）。

但是，Smalltalk不局限于一种句法元素，Smalltalk程序由6种构建块组成：

1.关键字、或伪变量，只有6个（self、super、nil、true、false和thisContext）。

2.常量。

3.变量声明。

4.赋值。

5.语句块。

6.消息。

我们在这个列表中没看到的东西可能比看到的东西更有趣：我们没有看到任何表示控制流的元素，没有条件判断或循环。它们不需要，因为它们是通过消息、对象和语句块（都是对象）来实现的。下面的方法在Integer类中实现了阶乘函数。

factorial

"Answer the factorial of the receiver."

self=0 ifTrue：[^1].

self＞0 ifTrue：[^self*（self-1）factorial].

self error：'Not valid for negative integers'

操作符=和＞是消息选择器，它返回抽象类Boolean的对象，Boolean有两个子类，True和False。如果选择器ifTrue：的消息接收者是True的一个实例，那么它的参数就会被执行。它的参数是[]界定的语句块。还有一个对称的选择器ifFalse：，它具有相反的语义。一般来说，使用循环要好于使用递归，所以下面是阶乘函数的循环实现：

factorial

"Implement factorial function using a loop"

|returnVal|

returnVal：=1.

self＞=0

ifTrue：[2

to：self

do：[：n|returnVal：=returnVal*n]]

ifFalse：[self error：'Not valid for negative integers'].

^returnVal

大部分工作都在两个语句块内完成。第一个语句块针对从2到消息接收者的值为止的正数执行。每次迭代的值都被传入内层的语句块，内层语句块将计算结果。语句块参数之前有冒号（：），与语句块体之由短竖线（|）分隔。可以有多个语句块参数，如下面的阶乘定义（Black等2007）：

factorial：=[：n|（1 to：n）inject：1 into：[：product：each|product*each]].

to：选择器返回类Interval的一个实例，它实际上列出了从1～10的值。对于数值n，阶层语句块将执行下面的动作。首先，它将内层语句块的product参数设为1。然后它将针对从1～n的值，调用内部语句块，计算每次迭代数的乘积和当前的乘积，并将结果保存到product中。要计算10的阶乘，我们需要写下阶乘值：10。借用Herbert Simon在《Sciences of the Artificial》（1996）中对更早的荷兰医生Simon Stevin的引用：

Wonder,en is gheen wonder.这句话的意思是：“漂亮，但并不难懂。”

[1]此书由机械工业出版社引进出版，书号为978-7-111-21126-6。

14.2　类型是隐式定义的

尽管所有东西在Smalltalk中都是对象，甚至类也是，但类并不对应于C++和Java这类语言中的类型。类型是根据它们的行为隐式定义的，而不是根据它们的接口。这被称为潜在类型或鸭子类型。

潜在类型是Smalltalk中唯一的类型机制（在其他一些动态类型语言中也是如此），但这并不意味着它对于强类型语言来说不重要。例如，在C++中，潜在类型是通过模板的泛型编程的基础。看到它第一次出现在这种语言中是很有意义的。请看下面的C++模板的示例介绍（Vandervoorde和Josuttis，2002，2.4）

//maximum of two int values

inline int const＆max（int const＆a,int const＆b）

{

return a＜b?b：a；

}

//maximum of two values of any type

template＜typename T＞

inline T const＆max（T const＆a,T const＆b）

{

return a＜b?b：a；

}

//maximum of three values of any type

template＜typename T＞

inline T const＆max（T const＆a,T const＆b,T const＆c）

{

return：max（：max（a,b），c）；

}

int main（）

{

：max（7，42，68）；//calls the template for three arguments

：max（7.0，42.0）；//calls max＜double＞（by argument deduction）

：max（'a'，'b'）；//calls max＜char＞（by argument deduction）

：max（7，42）；//calls the nontemplate for two ints

：max＜＞（7，42）；//calls max＜int＞（by argument deduction）

：max＜double＞（7，42）；//calls max＜double＞（no argument deduction）

：max（'a'，42.7）；//calls the nontemplate for two ints

}

我们在main（）方法中可以看到，函数：max适用于实现了比较操作符的所有类型。在C++中，这些类型可以是原生类型，也可以是用户定义的类型。没有限制它一定要继承自某个特定的类。它可以是任何类型，只要它在比较方面满足基本需求。隐含的类型定义是：操作符＜有意义的任何类型。

我们在学校中曾学到，有两种方法定义一个集合。一种是显式的，列出集合的元素，这被称为客观式定义。我们知道，自然数集是{1，2，3，……}。另一种定义集合的方法是描述集合中元素的共性，这被称为主观式定义。自然数集的主观式定义是“大于零的整数”。当我们在C++中声明一个对象时，我们通常使用客观式定义：我们说这个对象属于某个指定的类型。但当我们使用模板时，我们实际上使用了主观式定义：我们说这段代码适用于一个对象集，其中的对象具有某种属性（即它们提供了要求的操作）。

遗憾的是，在Java中情况比较混乱。Java提供了泛型编程，但只是在表面上和C++模板的语法相似。Bruce Eckel已经有说服力地解释了这个问题（参见http://www.mindview.net/WebLog/log-0050）。像Python这样的语言支持潜在类型，我们可以这样做：

class Dog：

def talk（self）：print"Arf！"

def reproduce（self）：pass

class Robot：

def talk（self）：print"Click！"

def oilChange（self）：pass

a=Dog（）

b=Robot（）

speak（a）

speak（b）

两次对speak（）的调用都能工作，我们不必关心其参数的类型。我们可以在C++中做同样的事情：

class Dog{

public：

void talk（）{}

void reproduce（）{}

}；

class Robot{

public：

void talk（）{}

void oilChange（）{}

}；

template＜class T＞void speak（T speaker）{

speaker.talk（）；

}

int main（）{

Dog d；

Robot r；

speak（d）；

speak（r）；

}

我们不能在Java中做同样的事情，因为下面的代码不能通过编译：

public class Communicate{

public＜T＞void speak（T speaker）{

speaker.talk（）；

}

}

令人困惑的是，下面的代码能够通过编译，因为在Java中，泛型类型被悄悄转换成了Object的实例（这叫做擦除）：

public class Communicate{

public＜T＞void speak（T speaker）{

speaker.toString（）；//Object methods work！

}

}

所以我们必须像下面这样做：

interface Speaks{void speak（）；}

public class Communicate{

public＜T extends Speaks＞void speak（T speaker）{

speaker.speak（）；

}

}

但是这在很大程度上减少了泛型的好处，因为我们要通过Speak接口来定义一个类型。缺乏一般性也体现在Java的原生类型不能使用泛型机制。作为一种变通方案，Java提供了包装类，即与原生类型对应的真正的对象类。在原生类型和包装类型之间的转换在Java编程中曾是令人不快的工作。在该语言最近的版本中情况有所好转，因为提供了自动装箱（auto-boxing）功能，在特定的环境下进行自动的转换。尽管如此，我们可以写List＜Integer＞，却不能写List＜int＞。

潜在类型最近很流行，因为在Ruby编程语言中大量采用。术语“鸭子定型”（duck typing）是一种对归纳推理的半开玩笑的说法，源自James Whitcomb Riley，原文是这样的：

如果它走路像鸭子，而且叫起来像鸭子，我就会称之为鸭子。

要了解鸭子类型的重要性，让我们以面向对象编程的一项基本特征—多态—为例。多态指的是在相同的上下文中使用不同的类型。实现多态的一种方法是通过继承。子类可以用在（准确地说是"应该能用在"，因为程序员可能不小心）所有可以使用超类的地方。鸭子类型提供了另一种实现多态的方法：只要一个类型提供的方法适用于某个上下文，它就可以用于该上下文。在前面用Python和C++写的宠物和机器人的例子中，Dog和Robot没有共同的超类。

当然，你可以通过编程在只支持继承类型的多态的语言中变通地实现鸭子类型。但是，如果程序员在解决面对的问题时可以使用更多的工具，那他就更富有。只要工具的流行程度不是问题，他就可以选择最合适当前情况的工具。关于这一点，Bjarne Stroustrup在《The Design and Evolution of C++》（1994）中表达得很漂亮：

我对计算机和编程语言的兴趣基本上是实用主义的。

我更习惯经验主义而不是理想主义……也就是说，我更喜欢亚里士多德而不是柏拉图，更喜欢休谟而不是笛卡儿，对帕斯卡只能难过地摇摇头。我发现一些完备的“系统”就像柏拉图和康德梦想的那样，但基本上不能让我满意，因为它们离我的日常经验极其遥远，也与个人的基本特点相去甚远。

我发现克尔凯郭尔对个人的狂热关注和敏锐的心理学洞见对我来说更有吸引力，远甚于黑格尔或马克思对人性的崇高计划和关注。尊重一个群体而不尊重群体中的个人，则根本不是尊重。许多C++的设计决定的根源都在于我不喜欢强制人们用某种特定的方式来做事情。从历史上看，许多最大的灾难都来自于理想主义者试图强迫人们“做对他们有好处的事情”。这种理想主义不仅会导致无辜的受害者遭受痛苦，而且会导致理想主义者应用强制力时的幻觉和腐败。我也发现理想主义者常常倾向于忽略经验和实验，这些经验和实验不巧恰好与教义或理论相抵触。当理想发生冲突时，有时甚至是权威们一致同意时，我倾向于提供支持，让程序员自行选择。

回到Smalltalk，请考虑遍历一个对象集合，对其中每个元素调用一个方法，然后收集结果。实现如下：

collect：aBlock

"Evaluate aBlock with each of the receiver's elements as the argument.

Collect the resulting values into a collection like the receiver.Answer

the new collection."

|newCollection|

newCollection：=self species new.

self do：[：each|newCollection add：（aBlock value：each）].

^newCollection

要理解这个方法，只要知道species方法要么返回消息接收者的类，要么返回一个与之类似的类—这种区别很小，在这里对我们几乎没有区别。有趣的是，要构造新的集合，我们只需要元素具有名为value的选择器，我们将调用它。语句块确实有一个名为value的选择器，所以所有的语句块都可以使用。但我们只是碰巧提到语句块：任何实现了value选择器的类都可以。

所有返回一个值的东西在编程时具有足够的重要性，所以它们得到了一个名称，被称为函数对象，它是C++STL算法的基本构成。从传统来看，它是函数式编程的主要成分，通常称为map函数。在Lisp中当然提供这个函数。在Python中也提供这个函数，让我们能做这样的事：

def negate（x）：return-x map（negate,range（1，10））

在Perl中这样写：

map{-$_}（1..10）

C++STL能做同样的事（Josuttis 1999，9.6.2）：

vector＜int＞coll1；

list＜int＞coll2；

//initialize coll1

//negate all elements in coll1

transform（coll1.begin（），coll1.end（），//source range

back_inserter（coll2），//destination range

negate＜int＞（））；//operation

也许我们这个时代的悲剧就是许多C++程序员只会写对数组循环的代码。

鸭子定型可能会引起争论，它也确实引起了争论。在像C++这样的静态类型语言中，编译器会检查包含潜在类型的表达式中使用的对象是否确实提供了要求的接口。在像Smalltalk这样的动态类型语言中，当运行出错时才会被发现。

这一点不能不考虑。强类型的语言防止了程序员的疏忽，这在大的项目中特别有用，因为好的结构有助于维护。20世纪80年代，从传统UNIX C转到ANSI C的最重要变化就是引入了更强的类型系统：C有了规矩的原型，函数参数会在编译时进行检查。我们现在会对类型间的转换觉得不舒服。简而言之，我们用一些自由换取了一些纪律，或者说用一些混沌换取了一些有序。

在Smalltalk中，通常假定不会导致混沌，因为我们应该编写小的代码片段，同时对它们进行测试。测试在Smalltalk中很容易。因为没有明显的编译器和构建循环，我们可以在工作空间中写一些小的代码片段，直接观察代码的行为。单元测试也很容易，我们可以在写代码之前先编写单元测试，不必担心编译器会报错说有未声明的类型或方法。给我们带来JUnit的社区与Smalltalk社区分享了很多经验，这也许不是偶然的。我们可以通过一些Java脚本语言实现差不多同样的东西，例如BeanShell或Groovy。实际上，强类型语言会带给我们安全的错觉：

如果程序是以强静态类型的语言编译而成的，那么这仅仅说明它通过了某些测试。这意味着语法保证是正确的……但是不能仅因为代码通过了编译就保证它是正确的。如果你的代码似乎能运行，那也不能保证其正确性。

正确性的唯一保证，不论你使用的语言是强类型的或弱类型的，就是它是否能通过保证程序正确性的所有测试。（http://www.mindview.net/WebLog/log-0025）

但是，有时候谨慎一些是有好处的。我们可以向某个集合添加一些对象，并期望这些对象满足特定的接口；我们不喜欢在运行时才看到其中的某些对象实际上并未满足特定的接口。我们可以利用Smalltalk的元编程机制来确保避免这样的问题。

元编程对潜在类型是一种补充，我们能够检查特定的接口是否真正提供，我们有机会在没有提供特定的接口时作出反应。例如，将未实现的调用代理给我们已知实现了该调用的对象，或只是优雅地处理失败，避免程序崩溃。所以在鹤嘴锄书（pickaxe book）中（Thomas等2005），我们看到了下面的Ruby示例，试图向一个字符串中添加一些歌曲信息：

def append_song（result,song）

#test we're given the right parameters

unless result.kind_of?（String）

fail TypeError.new（"String expected"）

end

unless song.kind_of?（Song）

fail TypeError.new（"Song expected"）

end

result＜＜song.title＜＜"（"＜＜song.artist＜＜"）"

end

这是我们在采用Java或C#编程风格时会做的事。在Ruby风格的鸭子类型中，可能就是：

def append_song（result,song）

result＜＜song.title＜＜"（"＜＜song.artist＜＜"）"

end

这段代码适用于所有可以通过＜＜操作符连接的对象，对于不支持＜＜的对象，我们会得到一个异常。如果我们真希望采用防御式编程，我们可以检查该对象的能力，而不是检查它的类型：

def append_song（result,song）

#test we're given the right parameters

unless result.respond_to?（：＜＜）

fail TypeError.new（"'result'needs＜＜'capability"）

end

unless song.respond_to?（：artist）＆＆song.respond_to?（：title）

fail TypeError.new（"'song'needs'artist'and'title'"）

end

result＜＜song.title＜＜"（"＜＜song.artist＜＜"）"

end

Smalltalk提供了respondsTo方法，在Object中定义，我们可以用它在运行时判断特定的消息接收者是否具有特定的选择器。

respondsTo：aSymbol

"Answer whether the method dictionary of the receiver's class contains

aSymbol as a message selector."

^self class canUnderstand：aSymbol

实现很简单，将检查代理给Behavior中定义的canUnderstand选择器：

canUnderstand：selector

"Answer whether the receiver can respond to the message whose selector

is the argument.The selector can be in the method dictionary of the

receiver's class or any of its superclasses."

（self includesSelector：selector）ifTrue：[^true].

superclass==nil ifTrue：[^false].

^superclass canUnderstand：selector

最后，includesSelector：也定义在Behavior中，它最终检查该类的方法字典：

includesSelector：aSymbol

"Answer whether the message whose selector is the argument is in the

method dictionary of the receiver's class."

^self methodDict includesKey：aSymbol

当消息接收者收到一条它不理解的消息时，它的标准反应是向系统发出doesNotUnderstand：消息。如果我们愿意自己处理这种情况，只需要重载这条消息，做类似下面的事情：

doesNotUnderstand：aMessage

"Handles messages not being understood by attempting to

proxy to a target"

target perform：aMessage selector withArguments：aMessage arguments].

我们假定target指向一个代理对象，我们希望它能够处理发错的消息。

潜在类型不是粗心编程的借口。我们模糊了类型，但并没有模糊责任。

14.3　问题

公有继承意味着“是一种”关系。程序员需要仔细思考，才能够得到符合这种模式的类继承关系。如果你有一个类，它包含某一个方法，而在子类中实现这个方法没有什么意义，那么这就超出了公有继承的范畴，应该是不良设计的标志。

在C++中，你可以创建一个无意义的方法，它要么返回一个错误，要么抛出一个异常，从而避免这种情况出现。典型的例子如下（Meyers 2005，item 32）：

class Bird{

public：

virtual void fly（）；//birds can fly

//……

}；

class Penguin：public Bird{//penguins are birds

public：

virtual void fly（）{error（"Attempt to make a penguin fly！"）；}

//……

}；

C++程序员也可以选择隐藏这个令人不快的方法：

class Base{

public：

virtual void f（）=0；

}；

class Derived：public Base{

private：

virtual void f（）{

}

}；

这样，Derived不再是一个抽象类，但它仍然没有可供使用的函数f（）。这样的诡计应该避免。

在Java中，可以通过返回一条错误或抛出一个异常来避免这一点，也可以在子类中创建一个无意义的方法抽象，这样就使得类层次从那一点开始抽象，向下直到你将该方法标识为具体为止。同样，这样的诡计应该避免。

要处理类层次结构中不合适的或不相关的方法，正确的做法是重新设计这个层次结构。为了更好地反映类鸟类世界的奇特性，类层次结构将为Bird类引入一个FlyingBird子类，让Penguin成为Bird的直接子类，而不是FlyingBird的子类。

在Squeak中，我们发现只有45个方法发出shouldNotImplement消息，该消息在某个继承自超类的方法在当前类中不适用的时候使用。这在Smalltalk的所有对象和方法之中只占了很少一部分，所以这门语言并没有充满设计不佳的类层次结构。然而，即使是shouldNotImplement消息实际上也是一种实现。这暗示了Smalltalk中的一个更深层次的问题，即我们没有真正的抽象类或方法。方法在惯例上是抽象的，根本不存在无实现的方法。除了使用shouldNotImplement消息之外，我们也可以规定某个方法是子类的职责，这就是我们看到的subclassResponsibility消息的用途。发出subclass Responsi-bility消息的类按惯例就是抽象类。例如，Collection类给出了一个通用的接口来添加和删除对象，但并没有提供实现，因为实现取决于我们所面对的子类（它可以是字典、数组、链表等）。方法add：将在子类中实现：

add：newObject

"Include newObject as one of the receiver's elements.Answer newObject.

ArrayedCollections cannot respond to this message."

self subclassResponsibility

这个“抽象的”add：定义甚至允许我们在使用它的Collection方法中定义，如add：with Occurences：，是这样定义的：

add：newObject withOccurrences：anInteger

"Add newObject anInteger times to the receiver.Answer newObject."

anInteger timesRepeat：[self add：newObject].

^newObject

我们甚至可以在根本没有定义add：时这样做，add：withOccurrences：还是像刚才那样定义，只要运行时接收该消息的对象定义了add：，Smalltalk就不会中止。（顺便说一句，add：withOccurences是Strategy模式的一小段漂亮实现。）同时，add：中的注释指出，Collection的某些子类，即以ArrayedCollection子类为根的那些子类，根本不应该实现该消息。同样，这只能在运行时使用shouldNotImplement强制实现：

add：newObject

self shouldNotImplement

在编程时运用惯例不是什么根本性的错误，这门艺术的一部分就是要掌握惯例。但可能出现问题的是，只依赖集合来得到要求的结果。如果我们在ArrayedCollection中忘记了实现add：，Smalltalk不会给出警告。我们只会在运行时失败。

我们在前面曾看到，在Smalltalk中实现一个代理类有多么容易。但是实际情况却是，如果我们确实想要一个代理类，让它只作为少数方法的代理，那么事情只会变得更复杂。原因与缺少真正的抽象类有关。代理类可能是我们想代理的那个类的一个子类，在这种情况下，它会继承被代理类的所有方法，而不仅是我们想代理的那些方法。或者，我们可以利用潜在类型，并在代理类中只定义那些被代理的方法；但问题是，由于所有东西都是对象，代理类会继承Object类的所有方法。理想情况下，我们希望代理两个方法的类就只有两个方法，但我们不清楚如何才能做到这一点。这个类会带有从它的祖先那里继承来的所有方法。我们可以求助于一些技巧来减少继承的方法的数量，例如，在一些Smalltalk的方言实现中，可以让某个类成为nil的子类，而不是Object的子类。这样就不会继承任何东西，但我们需要从Object那里复制和粘贴一些必要的方法（Alpert等1998）。在Squeak中，我们可以让它成为ProtoObject的子类，而不是Object的子类。

当我们开始寻找从哪里继承了什么时，事情就变得复杂了。因为在Smalltalk中所有东西都是对象，包括类也是对象，所以我们可能会想知道类是哪一种实例。我们发现是元类（metaclass）的实例，某个类的元类的名称就是该类的名称加上“class”。例如，Object的元类就是Object class。这是合情合理的惯例，但这并没有真正回答这个问题，因为我们会想知道元类是哪一种实例。我们发现元类是Metaclass类的实例（这里没有class后缀）。Metaclass是另一个类的实例，名为Metaclass class（包含class后缀），而它又必须是某个类的实例，所以它被作为Metaclass的实例。（如果读者认为正在阅读Terry Gilliam的“Twelve Monkeys”的手稿，那是很自然的想法，不必受到责怪。）如果我们同时考虑到继承关系，事情就会变得更复杂（到目前为止，我们还没有提到继承）。我们发现Object类是Class的子类，它又是Class类的一个实例，Class类是ClassDescription的子类，Class类是Behavior的子类，Behavior是Object的子类，在Squeak中，ProtoObject是继承层次结构的最顶端。图14-2展示了这种情形。请注意，我们没有包含Object之下的类。读者可以尝试合并图14-1和图14-2（不要忘记添加元类，从SmallInteger开始向上画）。可能是我们不能够拥有太多的好东西，让所有东西都是对象是很好的，但顺着这个公理走下去，不一定导致容易处理的结构。幸运的是，大多数Smalltalk程序员不需要关注这些事情。

“所有东西都是对象”和“所有事情都由消息完成”的规定还有其他一些后果，这些后果是程序员的日常工作需要关心的。所有稍具编程经验的程序员都会预期：

3+5*7==38

结果为真，但不巧的是，在Smalltalk中，下面的表达式为真：

3+5*7==56

原因是二元算术运算符只是数字类的选择器。所以它们像其他消息一样传递，没有什么规定一个二元消息一定要在另一个二元消息之前。确实，当你熟悉了这个游戏规则后，这样的结果是有意义的。但是这还是让人想不明白。在Smalltalk环境中，很容易在工作空间里检查算术运算，但也许最好是程序员永远无需考虑这样的问题。

Smalltalk环境本身是Smalltalk的一项了不起的发明。回到20世纪80年代，那时候图形显示器还很少，大多数编程都在单色文本终端上进行，Smalltalk为基于虚拟机上的一门语言实现了一个图形用户界面。这可能超越了它的时代。虚拟机要再等10年，才由Java带给主流编程社区。GUI赢得了胜利，但只是在硬件价格降下来之后。同时，具备这些要求的环境在当时被认为（也许被认为）不适合大多数的目的。但是还有一些微妙的东西在继续。

Smalltalk环境实际上是多个类构成的生态系统，你可以利用它来工作，也不得不利用它来工作。在Smalltalk中创建一个新类的唯一合理方式，就是利用合适的浏览器找到你想要的类，并从它继承。你可以使用自己喜欢的编辑器和磨练得很好的技能，以及用各种语言创建的命令行工具，但Smalltalk不同。你可能会喜欢Smalltalk设计者精心提供的类库，但当你换个角度想想时，你可做的事情并不多。在它里面时你感觉很好，但你必须在它里面做所有事情。这可能不是程序员愿意接受的。有可能你在读了本章的前面几页之后，就会冲过去下载并安装Smalltalk，亲自体验一下，然后当你意识到所有东西都不一样、不熟悉时，就会举手投降。

图　14-2：Smalltalk中的“意大利面条”

这一点暗示了Smalltalk为什么从未变成主流语言。Smalltalk是一种坚定不移的语言，它不做折中。它定义了一种新的编程模型，它使用的概念后来被许多其他语言所采用，它在很多方面是一个范例，被大家模仿。这一点与建筑界区别不大。

14.4　砖块和灰浆建筑架构

在所有的建筑奇迹中，在美国最受推崇的房子一定是Fallingwater，由Frank Lloyd Wright在1935年设计，“可能是20世纪最频繁出现在举例说明中的房子”（Nuttgens，1997）。Fallingwater建在名为Bear Run的深谷中的一处瀑布之上。这座房子是为Edgar Kaufmann,Sr.建造的，他是匹兹堡的一名百万富商。在1937年—1963年间，它被用作Kaufmann一家周末度假的屋子，之后它被捐赠给了Western Pennsylvania Conservancy，在1964作为博物馆向公众开放。

即使是这所房子的照片（参见图14-3）也能让人感觉平静，分享房子与其周围环境的美。Wright努力追求的是让自然与建筑成为一个整体，让艺术和自然交相辉映。在Fallingwater的设计中，自然进入了建筑内部，建筑成为了自然的一部分。这是现代建筑的杰作，仍然吸引我们思考建筑师希望在他的作品中注入的含义。

但房子不只是用来看的，它需要让人居住。我们不能住在Fallingwater中（我们最多只能参观它），但也许我们可以想象住在里面是什么感觉。

很有可能我们是错的。在Steward Brands的《How Buildings Learn》（1995）中我们得知：

Wright有生之年的最后一件杰作，即宾夕法尼亚的Fallingwater，被AIA投票推举为“有史以来最好的美国建筑”，饱受渗漏的烦恼。渗漏破坏了窗户、石墙，并使水泥结构退化。对于它最初的拥有者来说，Fallingwater被称为“很快发霉”和“七桶之屋”。它确实是一座漂亮而有影响力的房子，但无法居住。（引自Judith Donahue，“Fixing Fallingwater's Flaws，”Architecture,Nov.1989）

这种评判可能过于苛刻。曾在Fallingwater中住过的Edgar Kaufmann,Jr.所说的话有些不同：

一些错误困扰着Fallingwater，但房子特殊的美和它因其建造环境而给居住者带来的喜悦也应该正确评价。住在当中确实有些缺点，需要花些力气来克服。（Kaufmann 1986）

这种评判仍然不能全信。Edgar Kaufmann,Jr.不是利益无关方。他在1934年加入了Wright的Taliesin Fellowship。（23名学徒在1932年来到Spring Green,Wisconsin，开始生活和学习，从此开始了“The Frank Lloyd Wright School of Architecture”，现在这所学校还在。）他向他的父母介绍这所房子，在这所房子设计和建造时，他通常是Wright和他父母之间的中间人。

另一个建筑奇迹可能是20世纪最有影响力的房子（影响力在于它的风格确定了这个世界的现代城市的形态结构），是巴黎之外位于Poissy的萨瓦别墅（Villa Savoye），它由瑞士建筑师Charles-édouard Jeanneret-Gris，即Le Corbusier设计（参见图14-4）。Villa Savoye和Fallingwater一样，设计时是作为周末度假小屋。它是在1928—1931年之间建造的。Wright对于建筑的职责有着强列的观点，Le Corbusier也有很强烈的想法，但思想却相当不同。Wright探索艺术与自然的关系：

Le Corbusier发明了一种按比例的系统，即“模度（Modulor）”，它结合了黄金分割、六英尺的人形和和协的比例，形成了一个精巧的科布森法则，通过设想重新考虑了机械化和“自然秩序”。（Curtis 1996）

图　14-3：Fallingwater

图　14-4：萨瓦别墅

萨瓦别墅激起了人们对建筑师的敬畏：

像所有高度有序的工作一样，萨瓦别墅不容易归为哪个类别。它既简单又复杂、既睿智又感性。它充满了思想，同时又把这些思想直接通过形体、容积和空间的“特定关系”表达出来。这是现代建筑的一个“经典”时刻，同时又与过去的建筑有着密切的关系。这是Le Corbusier哲学的核心关注点，即对现代生活的观点通过建筑形式的持久价值而表达出来，在萨瓦别墅中人们会意识到它对古老经典主题的回应：静谧、均衡、通透以及简单的横梁结构。（Curtis 1996）

人们可能不同意这样的陈述，对于有些人来说，萨瓦别墅可能看起来就像一个白盒子，已准备吊走，毕竟，讨论品味是没有意义的。从另外一些不同的角度来考虑，除了它作为房子的实用价值之外，我们可能会传递更多明白的评判。下面是Le Corbusier的客户采取的相当不同的观点：

尽管萨瓦家一开始就不同意，Le Corbusier还是坚持平顶比斜顶更好（根据技术和经济上的理由来推测）。他向他的客户保证建造的费用更低，更容易维护，在夏天更凉快，而且萨瓦夫人可以在上面跳健身操，不会受到底楼的潮气困扰。但在一家人入住后刚一个星期，屋顶在Roger（萨瓦夫妇的儿子）卧室的位置就开始漏了，进了很多水，导致孩子患上了胸部感染，后来转成了肺炎，最后导致他花了一年的时间在Chamonix的疗养院进行康复。（De Botton 2006）

这听起来就像是建筑师与客户间的一个真实的笑话，但建筑师似乎还有一条窄路逃走：

在1936年，别墅正式完工的6年之后，萨瓦夫人在一封（雨水飞溅过的）信中表达了她对平顶的看法：客厅在下雨，弯道在下雨，车库的墙完全湿透了。更有甚者，我的卫生间仍在下雨，不好的天气里简直是洪水，水从顶灯上掉下来。Le Corbusier保证问题马上就会解决，然后寻找机会提醒他的客户，他的平顶设计在世界各地的建筑评论家那里受到多么热烈的追捧：“你应该在楼的大顶的桌子上放一个签名本，请你的客人在上签下名字和地址。你会发现收集到了许多的漂亮签名”。但这种签名收集的建议对萨瓦一家没带来什么安慰。“在我无数次的申请之后，你终于同意这所你在1929年建造的房子是不能住人的”，萨瓦女士在1937年秋天告诫说。“你的可信度正受危害，而我没有必要为此买单。请马上将它修复得可以居住。我真诚地希望我不必将此事诉诸法律。”只到第二次世界大战爆发，萨瓦一家后来飞离了巴黎，Le Corbusier才有幸不必在法庭上应诉。这一切只因他设计了基本上无法居住的、特别漂亮的“居住机器”（machine-for-living）（De Botton 2006）

另一名现代建筑的名誉领袖Ludwig Mies van der Rohe，使用了基于I型金属梁的极简主义规范系统。他“在书桌旁边保留了全尺寸I型金属梁的详细信息以获得恰好那样的比例。他认为“I型金属梁”是多利安式圆柱（Doric Column）的现代等价物”（Jencks 2006）。

更著名的是他对“少即是多”的运用，通过这个原则，他希望将建筑带回主要本质：没有装饰、装修或多余的元素，除非它们实现了特定的功能。I型金属梁是建筑的基本部分。或者说看起来似乎是这样。Mies van der Rohe的一件主要作品是纽约的Seagram大厦，建成于1958（参见图14-5）。建筑师面临了一个难题：尽管他很想在结构中展示I型金属梁，但在美国的建筑法规中却不允许这样做，法规要求金属支撑必须包以防火材料，如混凝土。但很奇怪，仔细看Seagram大厦会看到它表面的I型金属梁。这些不是真正的支撑I型金属梁。Mies van der Rohe要求在表面添加假的I型金属梁，这样它们就能“揭示”内部的结构。而且，为了不影响大厦的视觉效果，窗户的形状只能处于三种状态：打开、关闭或打开一半。对于防护太阳光来说，这可能不是最佳的安排（Wolfe 1982）。

图　14-5：Seagram大厦

一流的现代建筑师Louis Sullivan（摩天大楼的创造者之一，Wright的指导者，以及其他一些成就）写下了著名的一段话：

不论是翱翔的雄鹰，或盛开的苹果花、辛苦工作的马匹、无忧无虑的天鹅、枝叶繁茂的橡树、蜿蜒的溪流、飘浮的云朵、飞奔的太阳，形式总是服从于功能，这就是自然的法则。功能不变，形式就不变。花岗岩、永恒沉寂的山峦，常年不变；一闪而过的生命，生长成形，转瞬即逝。

这是所有事物的法则，无论有机还是无机、实体的还是形而上的、关于人的还是关于超人的、有关大脑和心灵的所有现象，即生命可以通过它的表达方式来认识，形式总是服从于功能。这就是自然法则。（Sullivan 1896）

Mies van der Rohe接下来似乎在脑门上贴上了“形式服从于功能”的标签。也许可能是这样的格言更有煽动性，而不是描述真实发生的情况。Paul Rand可能是一流的美国图形设计师。他曾负责设计IBM、ABC和最初UPS的标志，他与Steve Jobs在NeXT计算机公司合作过，他写了一些有影响的书籍，介绍他的设计理论。他指出：

分离形式和功能，分离概念和执行，不太可能产生具有美学价值的东西，而这些美学价值是一直以来反复不断呈现的。类似地，如果某系统根本不考虑美学，将艺术家与他的产品分开来，将艺术家和他的产品割裂开来，将个人的工作分开来，或由集体创作，或者由一个创造过程得到“百果馅”，那么从长期来看，不仅产品会贬值，它的创作者也会贬值。（Rand 1985）

责难现代建筑是很容易的，特别是Mies van der Rohe和Le Corbusier的作品在世界范围内被拙劣地模仿，造成了许多丑陋的民居区、犯罪滋生的工人宿舍和没有灵魂的商业中心。更有趣的是深入研究大师们自己的评论。我们看到，Wright、Le Corbusier和Mies van der Rohe都受到了批评，因为他们不让步。他们因坚定不移而受批评，因不做折中而受批评。他们尖锐的观点给我们留下了美丽的建筑，但这些建筑没有给我们带来物质上的享受。

坚定不移不一定是缺点。在《Doctor Dobb's Journal》杂志1996年4月的访谈中，Donald Knuth被问到他对Edsger Dijkstra的看法。“他了不起的地方在于他不做折中。想到用C++编程就会让他身体不舒服。”他的不折中达到了这样一种程度，以至于多年未碰计算机，写了“关于谦恭程序员（Humble Programmer）的真正极好的文章来讨论这一点”。Dijkstra是最有影响力的计算机科学家之一，他的著作今天仍然充满了有价值的建议，当我们被编程领域最新的时尚或银弹吸引时，阅读这些著作是很有益的。对于那些必须在真实世界计算机上编程的程序员来说，他不让步的姿态使他的著作显得尤为珍贵。

这可能是理解Smalltalk的角色的关键，像在它之前的Algol一样，Smalltalk在我们的职业生涯中扮演了重要角色。有一些建筑师开辟了新的道路，为将来的几代人创建了纪念碑，可能这些建筑的本质比实际的房屋或办公楼更是一种宣言。没人会争论Fallingwater打动拜访者的力量以及它为年轻架构师带来的灵感，即使我们可能会争论它是否是合适的住房，或者我们可能争论“形式服从于功能”，并让装饰到处都是，直到后门。类似地，有一些软件系统的成功更多地在于它们对代码的影响，而不是用它们来编写代码。

如果我们想编写代码，为了业务或为了开心，我们就需要从美丽的架构中获取灵感，但我们也许不能直接利用它来工作。我们的工作必须以美丽的架构为榜样，但必须是实际可行的。最纯粹最美丽的智力体系是纯数学，我们可以从中学到很多，但我们不能用它来编程。我们必须开发出能工作的系统，这就是事情开始变得复杂的原因。有时候人们很容易迷失在设计方法学中，忘记了我们的目标是不同的。Christopher Alexander，这位作为设计模式之父的架构师这样说到：

整个学术领域都是围绕着“设计方法”的思想发展起来的—我也被看作是这些所谓的设计方法的拥护者之一。我对此非常报歉，我想公开声明，我完全拒绝将设计方法作为一个课题来研究，因为我认为将设计的研究和设计的实践分离开来是非常荒唐的。实际上，学习设计方法而不实践设计的人几乎都是失败的设计者，他们没有活力，他们丧失了创造事物的冲动，或者说从来就没有过这种冲动。（Alexander 1971）

作为程序员，我们必须创造能工作的系统，而不只是看起来漂亮的东西。这两者不一定不可兼得。图14-6展示了Robert Maillart设计的Salginatobel大桥，它峻工于1930年。Maillart是瑞士人，学的是工程，但他的作品，特别是他的桥，是建筑之美的典范。最重要的是，这些作品不只是漂亮。Mail lart在竞标中赢得合同再建造大桥，以Salginatobel大桥为例，他击败了其他19份竞争设计，赢得了合同。建造大桥和公路当时只花了70万瑞士法郎，在今天接近400万。这座桥不是一个小工程。它的跨度达到90米，拱顶距Salgina河谷底达80米（Billington 2000）。结构的修长和轻巧是它低成本的原因。这座桥的低成本要归因于它的优雅。

Maillart的主要优点在于他的实用主义观点。他通过一种创造性的直觉得到了他的设计。他避开装饰和点缀，而且也没有模仿传统的建筑风格。他设计的结构不能由当时的计算工具分析（而且缺少计算机），所以不能证明设计是可靠的。他利用简化的图形分析来评估设计的可行性。如果Maillart必须等待其设计的严格证明，他的所有作品都不会建造（他于1940年去世）。Maillart“发现创新，尤其是桥梁设计的创新，不是来自于实验室的工作和数学理论，而是来自于设计室和建筑工地。数据在工程中扮演了十分重要的角色。但桥梁设计中的创新是可视的几何学想象的产物，而不是来自于抽象的数值分析，也不是从一般理论推导而来”（Billington 1997）。

编程像建筑一样，是一种实践。我们最好避免教条主义，而是将注意力放在能工作的东西上：

建筑是全能和无能的危险混合。表面上涉及的是“塑造”这个世界，因为他们要成为有影响力的建筑师，就要影响其他人—客户、个人或机构。因此，不连贯，或者更准确地说，随意性，是所有建筑师生涯的底层结构：他们面对的是随意的指令序列，以及他们没有确定的参数，在他们几乎不了解的国家，面对他们几乎没有意识到的问题，人们希望他们处理那些即使聪明得多的人都很难处理的问题。建筑在本质上就是混乱的冒险。（Koolhas等1998）

建筑是混乱的冒险，因为只有美丽的架构是不够的。不仅要美丽，而且要有用，这是建筑和编程等活动的法则。

图　14-6：Salginatobel大桥

参考资料

Abrahams,David,and Aleskey Gurtovoy.2005，C++Template Metaprogramming：

Concepts,Tools,and Techniques from Boost and Beyond.Boston,MA：Addison-Wesley.

Alexander,Christopher.1971.Notes on the Synthesis of Form,Preface to the paperback edition.Cambridge,MA：Harvard University Press.

Alpert,Sherman R.，Kyle Brown,and Bobby Woolf.1998.The Design Patterns Smalltalk Companion.Boston,MA：Addison-Wesley.

Billington,David P.2000."The Revolutionary Bridges of Robert Maillart."Scientific American.July,pp.85-91.

Billington,David P.1997.Robert Maillart：Builder,Designer,and Artist.New York,NY：Cambridge University Press.

Black,Andrew P.，et al.2007.Squeak By Example.Square Bracket Publishing.

Bloch,Joshua. 2008.Effective Java,Second Edition.Boston,MA：Addison-Wesley.

Brand,Stewart. 1997.How Buildings Learn：What Happens After They're Built,Revised.

Edition.London,UK：Phoenix Illustrated.

Calvino,Italo. 1986."Why Read the Classics?"The Uses of Literature.Translated by Patrick Creagh.New York,NY：Harcourt Brace Jovanovich.

Conroy,Thomas J.，and Eduardo Pelegri-Llopart.1983."An Assessment of Method-Lookup Caches for SmallTalk-80 Implementations."Smalltalk-80：Bits of History,Words of Advice.Ed.Glenn Krasner.Boston,MA：Addison-Wesley.

Curtis,William J.R.1996.Modern Architecture Since 1900，Third Edition.New York,NY：Phaidon Press.

De Botton,Alain.2006.The Architecture of Happiness.London,UK：Hamish Hamilton.

Gamma,Erich,et al.1994.Design Patterns：Elements of Reusable Object-Oriented Software.Boston,MA：Addison-Wesley.

Goldberg,Adele,and David Robson.1989.Smalltalk-80：The Language.Boston,MA：Addison-Wesley.

Forman,Ira R.，and Scott H.Danforth.1999.Putting Metaclasses to Work：A New Dimension in Object Oriented Programming.Boston,MA：Addison-Wesley.

Jencks,Charles.2006.The New Paradigm in Architecture：The Language of Post-Modernism.New Haven,CT：Yale University Press.

Josuttis,Nicolai M.1999.The C++Library：A Tutorial and Reference.Boston,MA：Addison-Wesley.

Kaufmann,Edgar Jr.1986.Fallingwater：A Frank Lloyd Wright Country House.New York,NY：Abbeville Press.

Koolhas,Rem,et al.1998.S,M，L,XL,Second Edition.New York,NY：The Monacelli Press.

McCarthy,John.1960."Recursive Functions of Symbolic Expressions and Their Computation by Machine,Part I."Communications of the ACM,April 1960.

McConnell,Steve.2004.Code Complete,Second Edition.Redmond,WA：Microsoft Press.Meyers,Scott.2005.Effective C++：55 Ways to Improve your Programs and Designs.Boston,MA：Addison-Wesley.

Norman,Donald.1988.The Psychology of Everyday Things.New York,NY：Basic Books.

Nuttgens,Patrick.1997.The Story of Architecture,Second Edition.New York,NY：Phaidon Press.

Petzold,Charles.1999.Programming Windows,Fifth Edition.Redmond,WA：Microsoft Press.

Rand,Paul.1985.A Designer's Art.New Haven,CT：Yale University Press.

Simon,Herbert.1996.The Sciences of the Artificial.Cambridge,MA：MIT Press.

Stroustrup,Bjarne.1985.The C++Programming Language.Boston,MA：Addison-Wesley.Stroustrup,Bjarne.1994.The Design and Evolution of C++.Boston,MA：Addison-Wesley.Sullivan,Louis H.1896."The tall office building artistically considered."Lippincott's Magazine,March 1896.

Thomas,David,et al.2005.Programming Ruby：The Pragmatic Programmers Guide,Second Edition.Raleigh,NC,and Dallas,TX：The Pragmatic Bookshelf.

Vandervoorde,David,and Nicolai M.Josuttis.2002.C++Templates：The Complete Guide.Boston,MA：Addison-Wesley.

Wolfe,Tom.1982.From Bauhaus to Our House.London,UK：Jonathan Cape.

跋　漂亮地构建William J.Mitchell

人们常常对软件系统和建筑工作进行宽松的类比。但与初看上去相比，这两种系统间结构上的类似程度实际上更大，可以更严格地具体化。

构成软件系统的代码由一维的符号字符串构成，按照精确的句法规则将一些定义良好的词汇放在一起，目标是在合适的机器上执行时能得到有用的结果。

建筑工作显然不是一维的，但在其他方面则与代码非常相似。它们是离散的物理组件的三维组装，按照严格的句法规格将定义良好的组件词汇放在一起，目标是得到有用的结果。（建筑师在实践中确实比程序员在词汇和句法方面拥有更大的活动余地。）

在两种情况下，我们都可以编写正式的语法来建立规则。总的来说，正式的语法告诉你如何把东西放在一起。更准确地说，根据语言学和计算机科学中的标准定义，正式的语法包括：一个有限的非终结符集合N，一个有限的终结符集合T，一个有限的替代规则集合R，和一个初始符号S。替代规则的左边有一组符号，中间是一个箭头，右边是另一组符号。它规定你可以用右边的那组符号代替左边的那组符号。完整、正确的结构化组件是通过递归地在初始符号S上应用替代规则R而得到的。由该语法确定的语言是所有可以通过这种方式导出的终结符构成的组件的集合。

正式的语法通常应用于单词词汇，告诉你如何将这些单词放在一起构成完整正确的句子，但并不限于此。语法可以应用于许多不同类型的东西，告诉你如何将它们放在一起，得到有用的组件。

在针对编程语言的语法中，放在一起的东西是可识别的符号，替代规则的左边和右边是一维的符号字符串，这些规则推导出这种语言的完整而正确的表达式。在两维的图形语言中，放在一起的东西是两维的形状，替代规则的左边和右边是两维的形状组件，这些规则源于完整而正确的图形设计。在三维的建筑语法中，要放在一起的非终结符是一些建筑结构，要放在一起的终结符是实际的建筑构件，替代规则推导出这些构件完整而正确的组合——换言之，即用该语法确定的语言所表达的设计。

例如，在几十年前，George Stiny和我出版了一本这种形式的正式语法，内容是关于伟大的意大利文艺复兴建筑师Andrea Palladio[1]的著名的漂亮别墅。它推导出了所有已知的Palladio设计的别墅，以及大量确认的伪Palladian别墅。（或者可以说，它推导出了所有Palladio可能设计的别墅，如果他活得更长、有更多客户的话。）而且，它对Palladio的别墅建筑背后的原则提供了让人信服的解释。从那时起，人们针对其他设计工作编写了无数建筑语法。

建筑语法最重要的功能之一就是记录下原则或模块性以及层次结构，它们总结了某些特定建筑风格的特征。例如，在精确定义和广泛使用的经典建筑语言中，圆柱有底座、柱身和顶部。顶部又进一步分解为一个组件层次结构（多立克式、伊奥尼亚式和科林斯式各有不同），等等。沿着子构件的层次结构向上，通常隔开一定距离的圆柱形成柱廊。圆柱、柱上楣构和人字墙形成了门廊。最后，所有的组件和子构建漂亮地结合在一起，构成了完整的、符合语法的经典组合。这些组合可以像句子一样被解析为一些有名称的部分。

从几何学或CAD系统的角度来看，建筑的组件和子构件是一些具体的形状，它们可以被变形和组装，得到更大的空间构造。从供应链和建造的角度来看，它们是一些物料元素，被制造（通常通过许多步骤构成的一个过程，每个步骤都提供附加值）、购买、运送至要求的地点，最后在一个构件中组装到位。从建筑维护的角度来看，它们是可以替换的部分。从功能的角度来看，它们是一些模块，在建筑中执行着可以识别的任务，可以与其他模块一起形成一个子系统，执行更高层次的任务。当模块以这种方式组合时，它们不仅有悦目的空间关系，而且也通过这些空间关系的优点，通过它们的接口传递了一些东西（如结构负荷）。

类似地，编程语言提供了一些方式将代码分解到模块中，层次化地组装模块，得到更高层次的模块，最终完成整个软件系统。所有程序员都知道，好代码不是一团乱麻，它的模块和层次有着清晰的逻辑结构，这种结构是利用语言的抽象和有组织的构建来实现的。经典建筑的组织清晰性为此提供了一个极好的模型。

除了少数的例外情况，建筑工作也遵循其他一些内部秩序原则。例如，圆柱排成一列通常是等间距的。如果想编写代码生成柱廊的CAD模型，那么你不需要为每个圆柱指定位置。你可以利用迭代，按照增量步长来确定位置参数。换言之，你以更精确更优雅的方式表达了建筑的原则，这种方式为代码的阅读者提供了更多的深刻见解。

如果你想生成有规则圆柱栅格怎么办？你可以使用嵌套迭代。首先对一个圆柱进行迭代，生成间距相等的一列圆柱，然后再迭代这一列圆柱，次数由你决定，生成圆柱栅格。

如果你想让角上的圆柱与中间的圆柱不一样，怎么办？（根据建筑转角处存在的不同结构情况或其他情况，建筑师常常这样做。）你可以使用条件：如果是转角，那么换成另一种圆柱设计。如果想改变中间圆柱之间的间距，以突出中轴线的重要性，区分外部的圆柱和内部的圆柱，那么你只要引入更多的条件。

模块性、层次结构和规则地重复并不是建筑师经常使用的全部秩序原则。如果你仔细分析建筑组合，就会经常发现在维度和比例、对称性（和有意破坏的对称性）、类似分形那样的嵌套的自相似性，以及参数化变化的花纹等方面的规律。

但是有时候，似乎缺乏内部的规律。建筑师可能出于某种原因，随机安排圆柱。为什么？似乎最准确地描述这种配置的方法就是单独指定每个圆柱的位置。没有更简短、更经济的描述方式。

就像这些简单的例子所说明的，好的建筑师不是通过随意的、蛮力的方式来构造他们的设计，他们肯定会避免笨拙的做法。根据现场环境、气候、要容纳的活动、材料和组件的供应链、建造过程和预算，他们会设计出相当不同的复杂建筑。但他们试图带着一种概念上的优雅来完成设计——遵守经济性的原则，并严格运用他们自己的奥卡姆剃刀原理。在漂亮的建筑作品所表现出来的不同和复杂性背后，你通常会发现一些关于功能组织和规范秩序的简单而优雅的原则。发现这些原则需要思考，而思考正是建筑的快乐和经验的关键部分。

如果你能弄明白这些原则，就可以通过某种标准编程语言中同样优雅的几行代码构造出这些作品的模型，或者（在更现代的编程环境中）使用一些形状重写规则。你甚至可以一般化，编写代码来生成设计，遵循同样的一些规则，对不同的情况和要求做出适当的反应。但如果你弄不明白这些原则，那么肯定要编写更长的、不包含那么多深刻见解的代码。

建筑明显的复杂性来自于建筑师所面对的需求的复杂性，它是通过蛮力的、逐点的描述的长度来衡量的。建筑师得到设计所遵循的原则通常可以用一段简短得多的代码来表示，执行这段代码就能生成所有这些复杂性。根据相当不错的估计，简短描述与冗长描述之比越小，建筑就越美丽。

建筑师会仰慕那些应用了简单优雅原则来实现许多复杂性的架构之美。类似地，软件架构师和程序员也会仰慕那些清晰而准确执行了许多复杂任务的代码之美（不牺牲可读性和可维护性）。科学家会仰慕那些描述了各种不同现象的简单法则之美和它们的解释能力。

[1]George Stiny and William J.Mitchell,The Palladian Grammar Environment and Planning B,vol.5，no.1，5-18（1978）.

O'Reilly Media,Inc.介绍

为了满足读者对网络和软件技术知识的迫切需求，世界著名计算机图书出版机构O’Reilly Media,Inc.授权机械工业出版社，翻译出版一批该公司久负盛名的英文经典技术专著。

O'Reilly Media,Inc.是世界上在UNIX、X、Internet和其他开放系统图书领域具有领导地位的出版公司，同时也是联机出版的先锋。

从最畅销的《The Whole Internet User's Guide＆Catalog》（被纽约公共图书馆评为20世纪最重要的50本书之一）到GNN（最早的Internet门户和商业网站），再到WebSite（第一个桌面PC的Web服务器软件），O’Reilly Media,Inc.一直处于Internet发展的最前沿。

许多书店的反馈表明，O'Reilly Media,Inc.是最稳定的计算机图书出版商——每一本书都一版再版。与大多数计算机图书出版商相比，O'Reilly Media,Inc.具有深厚的计算机专业背景，这使得O'Reilly Media,Inc.形成了一个非常不同于其他出版商的出版方针。O'Reilly Media,Inc.所有的编辑人员以前都是程序员，或者是顶尖级的技术专家。O'Reilly Media,Inc.还有许多固定的作者群体——他们本身是相关领域的技术专家、咨询专家，而现在编写著作，O'Reilly Media,Inc.依靠他们及时地推出图书。因为O'Reilly Media,Inc.紧密地与计算机业界联系着，所以O'Reilly Media,Inc.知道市场上真正需要什么图书。

译者序

从20世纪末以来，Internet得到了极为迅猛的发展，仅用了十年左右的时间就在全球范围内普及。Internt已经融入绝大多数公司、家庭和个人的工作生活之中。我们可以真真切切地感受到层出不穷的新技术给我们的生活所带来的便捷。我们可以足不出户在网上订购自己喜欢的商品。在车站、机场等候时，我们不必像以前那样靠翻报纸来打发时间，而是可以通过无线上网，漫游精彩的网络世界。我们不必再为无法看到心爱的球队比赛而发愁，丰富的网络直播资源可以帮助我们解决这个烦恼。总之，Internet彻底改变了我们的生活方式，使我们的生活变得更加精彩。

但是，鲜花的后面往往可能就是陷阱。从Internet诞生之日起，网络安全就一直是个引人关注的话题。从历史悠久的病毒、木马到最新的网络诈骗，网络安全威胁一直是一把悬在Internet头上的达摩克利斯之剑。由于网络安全威胁的主流逐渐从单纯以破坏为主的病毒、木马转向以获取经济利益为目标的网络诈骗，因此它的危害也越来越大。近些年来，网络上所出现的网络诈骗案件越来越多，很多网友由于对假冒的网络购买链接警惕性不足而蒙受了经济损失。随着网络越来越广泛地渗透到人们的日常生活中，对网络安全的防范也必将越来越引起人们的重视。

本书萃取了十余位闻名瑕迩的安全专家的智慧，如Philip Zimmermann、Anton Chuvakin、Peiter“Mudge”Zatko等。有些专家着眼于网络安全威胁的心理因素，有些专家侧重于对安全度量指标的讨论，有些专家对电子商务中的网络支付平台的弱点进行了分析，有些专家则对近些年来所发生的几个重大网络犯罪事件进行了详尽分析，还有一些专家对未来的网络安全进行了展望。这些专家的论著对于网络安全从业人员的工作有着非常好的指导意义。对于关注网络安全的人们，这些文章能够极大地拓宽他们的视野，使他们能够发现网络安全的新天地。

本书的翻译工作主要由徐波、沈晓斌完成。另外，姚雪存、陈永军、李福军、杨洁、应巧敏、张瑜等人也为本书的翻译工作作出了贡献。

前言

如果有人相信新闻标题可以揭示趋势，那么对于计算机安全领域而言现在是个有趣的时刻。当本书出版时，我阅读了一个能够打开麦克风和摄像头并窃取数据的软件的部分代码。这个软件在103个国家的超过1200台计算机上安装，尤其是在大使馆和其他敏感的政府部门。另外，一家法庭支持美国调查官在没有得到授权的情况下可以查看电话和Internet记录（只要交谈的另一端是在美国境外）。最新公布的漏洞包括Adobe Acrobat和Adobe Reader的一个缓冲区溢出漏洞（当前常称为漏洞攻击，英文为exploit），允许攻击者在用户打开PDF之后在用户的系统中通过用户的权限执行任意代码。

新闻标题实际上并不能很好地提示趋势，因为在漫长的历史中，它是由微妙的革命性变化所驱动的，而这种变化往往只有少数人注意到，例如编写本书的前沿安全专家们。读者可以在本书中发现安全威胁的发展方向以及针对它们的响应。

我在第一段中所提到的所有令人惊恐的新闻对于安全领域而言只是普通的业务而已。是的，它们正是我们应该担忧的安全趋势的一部分，但我们还需要注意更新的、更不易被觉察的漏洞。本书的作者们数十年来一直奋斗在第一线，努力发现我们的工作习惯中的脆弱环节，并提议用非常规的方式来处理它们。

为什么安全是美丽的

我要求安全专家John Viega想方设法为本书寻找一些作者，以便向普通计算机用户提供一些与安全有关的观点。除了在媒体上所看到的骇人听闻的关于网络入侵和盗窃的新闻之外，普通人一般都觉得安全是一件乏味的事情。

对许多人而言，安全就是系统管理员喋喋不休地提醒他们创建备份文件夹，无穷无尽的在网页显示之前跳出来的要求输入密码的对话框。办公室职员每次抄读办公桌边的笔记本上所记录的密码时都怒目圆睁小声咒骂（笔记本就放在打印出来的预算材料的上面，事实上办公室管理人员要求应该将它锁在抽屉里面）。如果这就是安全，那还会有谁想从事这个职业呢？谁会从O’Reilly购买一本关于安全的书呢？谁会一次花费半分钟以上的时间去思考安全呢？

对于那些肩负创建安全系统任务的人们，他们所付出的努力看上去是毫无希望的。站在旁边的人不会对他们的工作提供任何协助，业务经理也拒绝在安全上多花一分钱。程序员和系统管理员由于他们必须使用的工具和语言存在没完没了的零日攻击和未打补丁的漏洞也逐渐变得懒散起来。

这就是为什么关于安全的书卖得很差（尽管在过去的一两年里销量有所上扬）。关于如何入侵系统的书要比关于如何保护系统的书好卖得多，这个趋势着实令我震惊。

是的，本书应该改变这个现象。它应该向读者展示安全是一项最为激动人心的职业。它并不枯燥，也没有太多的官僚主义，更没有太多的约束。事实上，它和其他技术一样充满着想象力。

多年以来，我编辑过的大多数编程书籍都提供了关于安全的内容。这样的内容当然是非常实用的，因为它们允许作者讲述一些基本原则和一些良好习惯。但是，我已经对这种做法感到厌烦，因为它为安全话题划了一条分界线。它所灌输的都是一些老生常谈的安全观点，是一些锦上添花或者事后诸葛亮的东西。本书将颠覆这些观念。

John为本书选择了一些作者，他们已经在安全领域证明了自己具有独特的观点，并且有一些新的思路要和大家分享。有些作者设计了数以千计的人所依赖的系统，有些作者在大型公司担任高管职位，有些作者曾为法庭作证并为政府部门工作。所有的作者都在寻找普通人所不知道的问题和解决方案，但是这可能需要几年的时间才会收到成效。

本书的作者指出：有效的安全需要你始终保持警惕。它会打破技术、认知和组织结构的边界。安全界的黑帽们千方百计通过创新来取得成功。因此，负责防御他们的人们同样需要创新。

本书的作者肩负着世界范围内的信息安全使命，让他们抽出时间编写本书是一件很困难的事。事实上，许多作者在平衡本职工作和本书的写作任务时感受到了压力。但是，他们所花的时间是值得的，因为本书将会促进他们实现更远大的目标。如果有更多的人对安全领域产生兴趣，决定进一步对它进行探索，并向尝试通过组织上的变化以实现更好保护的人们给予他们的关注和支持，这本书就值得作者所付出的心血。

2009年3月19日，美国参议院商业、科学和交通委员会举行了一个听证会，它的主题是信息技术专家的缺乏以及这种现象对美国的网络安全的危害。让学生和专业人员对安全问题产生兴趣是一项极为迫切的需求，本书就代表了迈向这个目标的一小步。

本书的读者

本书适用于那些对计算机技术感兴趣并希望在最尖端领域体验生活的人们。本书的读者包括可能追求职业生涯的学生、具有一定编程背景的人们以及对计算机有着适度或深入了解的人们。

本书的作者在解释技术时尽量放低门槛，使相对新手级的读者也能领略到攻击和防御活动方式的感觉。专家级的读者能够更多地享受讨论的乐趣，因为本书能够加深他们对安全原则的理解，并提供了未来研究的指导方针。

捐赠

本书的作者们向互联网工程任务组（The Internet Engineering Task Force,IETF）捐赠本书的版税。这个组织对于Internet以及其具有远见的自我管理式的迷人模型的发展极为关键。如果没有IETF具有奉献精神的成员们的科学讨论、灵活的标准制定和明智的妥协，Internet的发展是无法想象的。IETF在自己的网页上把自己描述成“由网络设计者、操作者、生产商和研究人员所组成的大型开放式国际社区”。O’Reilly将把版税汇给互联网社会（The Internet Society,ISOC），该组织长期向IETF提供资金和有组织的支持。

材料的组织

本书内容并没有按任何特定的方案进行排列，但还是经过了整理，以便提供引人入胜的阅读体验，方便读者惊喜地发现新观点。不过，还是将那些讲述相似主题的内容放在了一起。

第1章　心理上的安全陷阱作者Peiter“Mudge”Zatko

第2章　无线网络：社会工程的沃土作者Jim Stickley

第3章　美丽的安全度量指标作者Elizabeth A.Nichols

第4章　安全漏洞的地下经济作者Chenxi Wang

第5章　美丽的交易：重新思考电子商务的安全作者Ed Bellis

第6章　捍卫在线广告：新狂野西部的盗匪和警察作者Benjamin Edelman

第7章　PGP信任网络的演变作者Phil Zimmermann和Jon Callas

第8章　开源Honeyclient：先发制人的客户端漏洞检测作者Kathy Wang

第9章　未来的安全齿轮和杠杆作者Mark Curphey

第10章　安全设计作者John McManus

第11章　促使公司思考：未来的软件安全吗作者Jim Routh

第12章　信息安全律师来了作者Randy V.Sabett

第13章　美丽的日志处理作者Anton Chuvakin

第14章　事件检测：寻找剩余的68%作者Grant Geyer和Brian Dunphy

第15章　无需真实数据就能出色完成工作作者Peter Wayner

第16章　铸造新词：PC安全剧场作者Michael Wood和Fernando Francisco

使用本书的代码示例

本书是为了帮助你完成工作。通常来说，你可以在你的程序和文档中使用本书的代码。除非你使用了本书的大量代码，否则你无需获取我们的许可。例如，写一个程序用到本书的几段代码不需要获得许可；销售和分发O’Reilly丛书的代码需要获得许可；引用本书的样例代码来解决一个问题不需要获得许可；使用本书的大量代码到你的产品文档中需要获得许可。

我们不要求你（引用本书时）给出出处，但是如果你这么做，我们对此表示感谢。出处通常包含标题、作者、出版社和ISBN。例如：“Beautiful Security,edited by Andy Oram and John Viega.Copyright 2009 O’Reilly Media,Inc.，978-0-596-52748-8”。

如果你觉得你对本书样例代码的使用超出了这里给出的许可范围，请和我们联系：permissions@oreilly.com。

如何联系我们

请把对本书的评论和问题发给出版社：

美国：

O'Reilly Media,Inc.

1005 Gravenstein Highway North

Sebastopol,CA 95472

中国：

北京市西城区西直门南大街2号成铭大厦C座807室（100035）

奥莱利技术咨询（北京）有限公司

O’Reilly的每一本书都有专属网页，你可以在那儿找到关于本书的相关信息，包括勘误表、示例代码以及其他的信息。本书的网站地址是：

http://www.oreilly.com/catalog/9780596527488/

对于本书的评论和技术性的问题，请发送电子邮件到：

bookquestions@oreilly.com

关于本书的更多信息、会议、资料中心和网站，请访问以下网站：

http://www.oreilly.com

第1章　心理上的安全陷阱

在我对软件及其所驱动的工具的攻击生涯中，许多同事认为我采用了一种不符合标准的方法。对于这种说法，我颇感诧异。对我而言，我所采用的方法不仅符合逻辑，而且简单明了。反之，我觉得那些学院派采用的方法对于现实世界的应用程序而言显得过于抽象，难以获得普遍的成功。这些看上去更常规的方法几乎不成章法，没有抓住问题的焦点，甚至完全背道而驰。这些方法往往需要花费数百小时进行逆向工程和应用程序跟踪，才可能在精力耗尽之前发现它们所存在的漏洞。

现在，请不要采用这种错误的方法。我并没有诅咒前面所提到的技巧，事实上我同意它们是发现和利用漏洞的关键工具。但是，我相信可以采用一些捷径，并从另一个视角封装、改进甚至绕过这些方法。

在本章中，我将讨论这些另外的视角，看看它们是如何帮助我们深入到那些被称为安全专家的开发人员的思维中。

为什么要深入到开发人员的思维之中呢？原因有很多，但是在本章中，我们将把注意力集中在对代码的创建过程以及对编写代码的人们所施加的各种约束上。从安全的角度出发，这种问题常常会产生并不是最理想的系统。理解了代码完成时所处的环境、心理和哲学框架之后，我们更容易发现系统中包含可能被攻击者所利用的漏洞的区域。在适当的时候，我会和大家分享一些逸闻作为例子，对一些理念问题进行探讨。

在过去几年里，我的精力集中在大规模的环境，例如大公司、政府机构以及它们的各种附属机构，甚至是州政府。虽然有许多要素也适用于更小的环境，甚至适用于个人，但我喜欢从更广的角度展示这些问题，展现更广阔的社会画面。当然，用这种更为粗犷的画笔作画需要进行一些归纳，读者可能会发现有些情况与这些例子存在冲突。由于篇幅的限制，我并不会举出这方面的反例。

本章的目标并不是强调特定的技术，而是讨论一些导致安全脆弱性的环境和心理情况。对技术实现者所受到的外部影响和限制进行考虑是非常重要的，可以更好地理解这些脆弱性在逻辑上是如何产生的。虽然这对于攻击方而言是一种好玩的智力游戏，但是如果防守方也参与到这个游戏中，就需要考虑一些新的因素：1）防止可能会导致攻击的错误，2）使用与攻击者相同的游戏技巧并采用相同的操作方式。此时，这个安全游戏才是我心中最美丽的。

我所讨论的理念可以分为三类：习得性无助和无从选择、验证陷阱和功能锁定。在安全的设计和实现中，这并不是影响因素的安全列表，但它可以作为一个起点，鼓励我们进一步发现自己在系统中所创建或依赖的潜在安全危险。

1.1　习得性无助和无从选择

社会学家和心理学家发现人类和其他动物存在一种现象，这种现象称为习得性无助。它来源于个人在实现自己的目标或者摆脱坏习惯时屡次遭受的挫折。最终，动物们会采取极端的毁灭性措施，就是从内心深处放弃尝试。即使在出现了实现目标的机会或者存在逃脱的良机时，动物们也会表现得很消极，无法利用这些机会。

为了证明即使是资深和理智的软件工程师也会受这个畏缩毛病的影响，我将描述一个由于向后兼容所导致的不良安全性的例子。

向后兼容是现有的技术部署一直存在的一个问题。发现新的技术，并需要部署到不兼容的（甚至在本质上不同的）现有解决方案中。

在系统演化的每个时刻，开发商需要决定是否必须结束现有解决方案的生命，还是提供一条迁移路径或者设计一种方法允许遗留解决方案能够与现代解决方案共存。这样的决定在商业和技术的角度上往往存在很大的分歧。但是，决定通常是从商业的角度作出的，开发人员和工程师必须照办[1]。出现这种情况时，负责创建实际实现方案的人们就会产生这样一个印象，就是决定已经作出，他们只要照章办理就可以了，不需要进一步的评估或考虑。

想象一下有这样一个决策，在进行技术替换时要求维护与旧技术的兼容。管理层进一步决定，以后不会再向遗留解决方案提供进一步的开发或支持工作，这是为了鼓励现有的顾客迁移到新的替代解决方案。

尽管这种决策在许多方面向开发过程施加了压力（涉及安全问题），但是当一个解决方案（通常是新技术）比其他解决方案更为安全时，无疑会更加引人注目。事实上，新技术的开发常常明确要求满足更高的安全需求，但是仍然必须支持旧技术。在这种情况下会产生什么安全问题呢？

实现向后兼容可以采取不同的方法，有些方法较之其他方法更为安全。但是，一旦开发人员理解了需要让更旧、更缺乏安全的技术继续存活时，那些常常可以缓解这种风险的解决方案往往就不在考虑之列。焦点集中在新技术上，遗留技术将在最大限度地减少遗留效果的情况下被嵌入到新技术中（或反过来）。总之，实际新技术的开发队伍通常并不是开发遗留代码的队伍，他们的最终目标是想方设法移植遗留解决方案，是不是这样呢？

最直接的解决方案是对新技术的健壮性和安全强度作出妥协，以匹配遗留解决方案。在本质上就是允许现代技术和遗留技术共存。当开发人员在迁移遗留代码的漏洞时想不出可以做什么（或更糟，想不出应该做什么）时，就出现了习得性无助。遗留代码对他们施加了压力，他们觉得这不应该在他们的职责范围之内（即使把新技术降低到旧技术的水平而影响了它的安全性）。对于公司的决策，他们觉得不知道做什么好，感觉力不从心。

1.1.1　实例：Microsoft是如何允许L0phtCrack的

数年前，为了帮助系统管理员发现漏洞，我编写了一个密码破解工具，用于恢复Microsoft的用户密码。当时，这个工具称为L0phtCrack，后来重命名为LC5。再后来，Symantec（得到了它的版权）担心违反国际武器限制公约（ITAR）而将它停用了。[2]网络和技术书籍上有许多文章描述了L0phtCrack的工作原理，但没人关注它为什么会起作用。L0phtCrack所利用的Microsoft Windows的漏洞可能会产生什么影响呢？

事实上，这个工具直接利用了Windows的加密程序的实现和使用中所存在的大量问题。所有这些问题都源于各种版本的Windows（直到Vista）中继续使用的遗留LAN Manager（LANMAN）的散列函数。它的散列表示形式尽管基于已经成熟的数据加密标准（DES），但并没有包含salt。另外，LANMAN中的密码不区分大小写。这个函数把14个字符（或更短）的密码分解为2个7字节的值，每个值根据同一个键进行加密，并连接在一起。当我于20世纪90年代后期在BugTraq的一个帖子上描述这个问题时，基本的加密序列是（见图1-1）：

1）如果密码小于14个字符，就用null字符填满，以便为密码保留14个字符的空间。反之，如果密码大于14个字符，就将它截断。

2）把14个字符的密码转换为大写形式，并把它分割成7个字符的两半。注意，如果原密码为7个或更少的字符，那么后半部分总是7个null字符。

3）把7个字符的每半部分转换为一个8字节的检校DES键。

4）使用前面所提到的每个键对一个已知的常量（“KGS！@#$%”）进行DES加密。

5）把两个输出连接在一起，形成LM_HASH表示形式。

图　1-1　LANMAN旧算法的总结

由于许多技术上的原因，这种选择组合存在问题。

Windows NT的开发人员意识到了LANMAN散列的脆弱性，并使用一种更健壮的算法进行密码信息的存储，称为NT散列。它维护字符的大小写情况，允许密码长于14个字符，并使用更现代的MD4信息摘录产生它的16字节散列值。

遗憾的是，Windows系统在每个密码的更健壮版本旁边继续存储更脆弱的版本，并在用户每次登录时同时在网络上发送这两个版本。在网络上，更脆弱的16字节LANMAN散列值和更健壮的16字节NT散列值都会经历下面的过程，如图1-2所示：

1）用null字符把散列值填充为21个字节。

2）把21字节的结果分割为3个7字节的子部分。

3）把每个7字节的子部分转换为8字节的检校DES键。

4）使用前面所提及的DES键，对一个8字节的挑战口令（challenge）进行加密，它在网络上发送时是可见的。

5）把步骤4的3个8字节输出连接在一起，创建一个将通过网络发送的24字节表示形式。

图　1-2　在网络上同时处理LANMAN和NT散列

当然，Microsoft希望他们的所有顾客都升级到更新版本的Windows，但是不敢切断使用旧版本的顾客，甚至不敢用新的散列函数对旧版本进行更新。由于密码是网络的一个关键部分，他们不得不假设在可预见的将来，不理解新散列函数的旧系统可以继续连接到使用更安全的散列函数的新系统。

如果登录两端的系统都是使用新散列函数的新系统，就可以使用更健壮的NT散列执行实际的身份认证。但是，在发送更健壮的新散列表示形式时，必须同时发送更旧、更脆弱的LANMAN散列版本。

由于最大限度地照顾了向后兼容，而忽视了它所产生的复杂后果，Microsoft完全破坏了更新的安全技术所带来的技术优点。

L0phtCrack利用了脆弱的LANMAN密码的编码形式，并根据它的结果破解存储在它旁边的更健壮的NTLM表示形式。即使用户选择了长于14个字符的密码，对LANMAN散列的破解仍然要提供前14个字符，只剩下很少一部分需要通过推断或穷举来猜测。和LANMAN不同，NT散列区分大小写。但是，一旦脆弱的版本被破解之后，最多只要尝试2x次（其中x是密码字符串的长度）就可以推断出NT散列的密码大小写情况，因为每个字符最多只有2种选择（大写或小写）。记住，x小于或等于14，因此对于穷举法来说是小意思。

尽管NTLM网络认证引入了一个挑战，可以扮演支持salt机制的角色，但它的输出仍然包含了太多可以被攻击者看到并利用的信息。16个字节的原散列中只有2个字节进入第3个7字节部分。类似，散列值的前半部分中只有1个字节（第8个）进入第2个7字节部分。想象一下，如果原密码是7个字符或更短（对于粗心的用户，情况很可能是这样）的情况。在LANMAN散列中，第2组的7个输入字节都是null字符，因此输出散列中第9～16字节将总是相同的值。这个情况进一步随着NTLM算法而传播。至少，它并没有设法确定24字节的NTLM认证响应中的最后8个字节是否来自一个短于8个字符的密码。

简而言之，新的现代安全解决方案的问题来自于遗留系统的脆弱LANMAN密码，因此把整体安全性能降低到最小公分母的水平。过了没多久，在接收到太多的负面安全报道之后，Microsoft引入了在默认情况下只发送一种（或另一种）散列值的功能，而不是两者都发送。后来，他们甚至停止了在本地系统中同时存储LANMAN散列值和NT散列值的做法。

[1]或者至少让开发人员和工程师觉得应该这样做。

[2]这不是L0phtCrack该有的结局……

1.1.2　密码和身份认证可以从一开始就做得更好

我讲述这个L0phtCrack故事是为了强调一个常见的安全问题。有许多理由支持多种安全实现，即使其中一种被认为优于其他方案。但是如前所述，在许多情况下，这样做的原因是为了支持向后兼容。如果对遗留系统的支持被认为是非常重要的，可以预计在协议和服务方面将会出现相当数量的冗余。

站在安全的角度，现在问题变成了如何在实现向后兼容的同时不降低新系统的安全性。Microsoft的幼稚解决方案嵌入了所有可能性中许多不好的东西：它把不安全的散列值和更安全的散列值存储在一起，通过网络传输这两种散列的表示形式（即使在不需要的情况下），这就为攻击者提供了便利。

记住，习得性无助就是指一个人得出结论，他已经无能为力或者无法通过学习来解决，而不是想方设法对手头上的情况进行实际分析。换句话说，有人告诉你已经没有希望，并且你相信他的原因也仅仅是因为他说“不行”。在工程任务中，习得性无助可以由处于明显权威位置的人们所作的陈述、对向后兼容（或遗留顾客的要求）的消极抵触以及成本或财政压力（预想或真实）而产生。

Microsoft相信遗留系统非常重要，足以阻止废弃这些系统。为此，他们决定继续支持LM散列。

但是，他们采取了第二个关键的步骤，在选择处理遗留系统和现代系统交互的协议问题时迫使新系统同时与当前协议和遗留协议进行对话，而没有考虑遗留系统的安全问题。其实，他们可以要求遗留系统对一些支持登录所需要的函数进行修补，作为遗留系统的最后升级。也许这个解决方案被拒绝的原因是它可能会设置一个危险的前提条件，就是要求支持一些他们已经声称结束了生命的系统。类似，他们可以选择当新旧系统都可以与更现代、更健壮的函数进行对话时，就不在网络上同时发送新旧散列值。这可以帮助他们的旗舰“新技术”同时提供实际的和预想的安全。

最终，由于媒体和顾客对安全脆弱性的持久抱怨，另外也由于像L0phtCrack这样的攻击工具的出现，Microsoft决定让它们的系统避免传递更脆弱的LANMAN散列表示形式。它显示了生产商应该选择一条不同的起始路径，允许终端用户根据自己的安全需求来配置系统。反之，如果相信确实需要遗留系统，必须简单地把它们和新产品放在一起，并让所有的系统把标准降低到最小公分母的水平，终端用户就会成为牺牲品。这是开发商内部设计人员和实现人员的习得性无助的一个例子。

Microsoft并不孤单

为了避免让读者觉得我是专门找Microsoft的茬，我提供了下面这些平等机会（并且可能具有冒犯性）的评论。

在20世纪90年代中期至晚期，Microsoft在市场和媒体上所持的立场是它的系统比Unix更安全。Internet上的服务器大多是Unix系统，Microsoft试图打入这个市场。众所周知，占据公共Internet上的绝大部分系统的各种Unix操作系统已经发现了许多安全漏洞。但是，很少有人从Internet的角度对Microsoft的Windows NT 4.0的安全性进行探索。这是因为NT 4.0系统在网络上只占据了很少一部分的市场份额。

从本质上说，Microsoft的立场就是“我们是安全的，因为我们不是Unix”。但是直到Windows操作系统的Vista版本，Microsoft才真正拥有了具有强大、现代的安全行为的操作系统。Vista存在一些问题，但主要不是安全方面的问题。因此，当NT 4.0作为新产品出现时，Microsoft把矛头对准了Unix，在各种安全问题上对Unix横加指责。后来，情况掉了个头，人们开始指责Microsoft所存在的各种安全问题。现在，Microsoft实际上提供了一种拥有许多强大的安全组件的操作系统，还有哪一家值得挑剔呢？请看Apple。

在历史上，Apple Computer的市场策略与Microsoft有些相似。正如Microsoft以它们并不是Unix为由吹嘘自己的安全性一样，Apple的市场和用户部门声称它们的OS X平台能够更有效地抵御各种攻击和病毒，但所持的理由也仅仅是它们不是Windows。仔细查看了OS X的内核和userland空间之后，我可能断定它还是存在许多安全漏洞（不论是远程还是本地）需要指出和修正。和Microsoft刚开始提供NT时一样，Apple也享受了一段蜜月期。它很少成为攻击目标，因为它的市场份额非常小。但是，随着它的市场份额不断扩大，可以预想它所受到的攻击也会不断增加……

1.1.3　客户的习得性无助——无从选择

正如我们所看到的那样，Microsoft在向后兼容方面作出的选择所导致的不良安全问题可能会让他们的顾客在环境、技术能力以及接受改变的意愿方面产生自暴自弃的观点（不管是否正当）。我把当前网络上的另一个（甚至更大的）安全问题归因于开发商的习得性无助和顾客的无从选择这两个因素的结合。大量的审查显示，大多数网络交换机的生产商有意把交换机设计为“失败时打开”而不是“失败时关闭”。交换机用于在数据链路层上的系统之间移动数据包。在这种情况下，“失败时关闭”意味着设备要么关闭并停止发挥作用，或者以一种“安全的”方式停止操作。这样，数据就不会通过存在问题的系统被传递。反之，“失败时打开”意味着系统停止执行任何智能功能，而是盲目地发送它从所有端口所接收到的数据包[1]。

在本质上，“失败时打开”的交换机相当于把自身变成了一个哑的集线器。如果只想消极地嗅探自己并不想要的网络交通，那么哑的集线器可能正是我们所需要的。功能正常的交换机试图只把流量发送到合适的目的地。

许多机构觉得消极的网络嗅探并不是实实在在的威胁，因为许多交换机都是这样运行的。但在当前，把一个嗅探器连接到一个被交换的LAN并观察自己不应该看到的数据是极为常见的做法，常常会导致该机构的网络部门的极度惊奇。他们并没有意识到生产商不惜一切代价避免连接断开的决定（很可能是害怕顾客由于间歇性中断而产生的狂怒），因此当交换机在遇到缺陷、安全攻击或者对某些数据包的处理缺乏明确的指令等事件时，就把交换机恢复到哑的广播模式。换句话说，生产商安静地为他们的顾客作出了最适合顾客的决定。

我相信如果顾客能够决定哪种方式更适合自己的利益，无疑会让他们处于更加有利的位置。虽然对于装配线而言，让交换机在失败时打开无疑要比在失败时关闭更合适，但也有一些情况下交换机用于分离重要的流量并隔离内部的域和系统。在这种情况下，对于顾客而言，最好的方式就是交换机在失败时关闭并发送一个警报。顾客至少应该拥有选择的权力。

在这里，我们讨论了生产商所面临的习得性无助以及顾客的无法选择。习得性无助来自于生产商对于它能够教育顾客并让顾客获得选择权这个价值所采取的消极态度。它与前面所讨论的遗留系统的兼容性解决方案有相似之处。生产商相信为顾客提供这种额外的可配置性只会让顾客感到困惑，使顾客把事情搞砸或者向生产商拨打大量的服务支持电话。

顾客的无从选择是可以理解的：顾客是从信誉良好的生产商那里购买外观漂亮的系统，当时一切看上去都很顺利。但这种无从选择并不是把系统的实用性降低到不利的程度。是不是必须要让一个系统在任何环境下都可以工作而降低它的安全性？协议是否盲目地允许遗留版本的系统在较弱的安全级别上进行交互？当系统不知所措时，它是否应该恢复到扮演哑的遗留设备的角色？这些情况常常是由于习得性无助所致。

[1]这与电路的情况相反，后者的“失败时关闭”允许电流继续而“失败时打开”将切断电流。

1.2　确认陷阱

大约1997年8月的某天，我和Hobbit（我的一位朋友，是一位非凡的黑客[1]）与Microsoft的一位执行官和一位资深工程师[2]共进晚餐。他们想知道我们为什么能够这么轻而易举地在Microsoft的产品中找到这么多的缺陷。虽然有些细节记得不是特别清楚了，但我相信当时我们是非常诚恳地进行了回答，表示我们的方法是向系统输入一些随机的垃圾。这是一种简明的缺陷和安全测试技巧，有时称为“模糊（fuzzing）”，现在主要的计算机科学出版物都记录了这种方法。但是，当时“黑客”社区还没有普遍采用模糊方法。

我们告诉这位工程师，我们对于Windows在面临垃圾输入时如此频繁地遭到失败而颇为吃惊。接着，我们又问他们执行了哪些类型的健壮性测试，因为适当的QA（质量保证）应该包含坏输入测试，这种测试应该能够发现我们所找到的许多令系统和应用程序崩溃的情况。

这位工程师的回答是他们在所有的产品执行了充分的可用性测试，但是并没有执行试图使产品崩溃的测试。这个答案揭示了问题的原因。Microsoft在努力保证良好的用户体验的同时并没有考虑不良的用户或环境。

例如，开发Microsoft Word的队伍将根据各种可接受的输入格式（Word、Word Perfect、RTF、普通文本等）对文件解析器进行测试。他们并不会对那些可以通过手工创建的但绝不会由可兼容的字处理程序产生的各种预期格式的变型进行测试。但是，恶意攻击者可以用预期格式的不良版本以及随机的垃圾对这些系统进行测试。

当我们在晚餐上询问那些资深的Microsoft代表为什么不发送恶意数据或提供不良文件作为输入对他们的产品进行测试时，他们的答案是：“用户为什么要这样做呢？”当他们看到任何人可以按照有意让软件失败的方法与软件的一部分进行交互时，显得非常的震惊和沮丧。

他们从来没有考虑过他们的应用程序可能被部署在恶意环境中。这种善良的观点来自于另一个可能被恶意攻击者所利用的心理特征：确认陷阱。

1.2.1　概念简介

Microsoft的产品测试的目的是为了确认他们对软件行为的信任，而不是为了打击这种信任。软件架构师和工程师经常会遇到这种盲点。在1968年的一篇论文中，Peter Wason指出“为了获得正确的解决方案，有必要产生一种意愿，就是试图推翻假设，并对那些常常确信是正确的直观想法进行测试”[3]。他通过一个简单的智力测试演示了确认陷阱。

找一些人并通知他们正在进行一个小实验。我们将向参与者提供一个整数数列，它们遵循一个规则，参与者的任务就是猜出这个规则。为了确定这个规则，参与者可以说出另外的数列，然后我们告诉他这个数列是否符合这个未知的规则。当参与者认为他已经猜中了这个规则时，就可以把它说出来。

这个规则实际上非常简单，就是递增的数列。但是我们先不把它说出来。

我们最初提供的数列是2、4和6。

此时，其中一位参与者提出了数列8、10和12。我们应该告诉他8、10和12确实遵循这个规则。另一位参与者可能提出1、3和5。同样，我们告诉他数列1、3和5也遵循这个规则。

当人们看到初始数列2、4和6时，会注意到一个显而易见的关系，就是每个后续的数都比前一个数大2。当他们提出匹配的数列时，可能会让它满足这种关系，但这完全只是他们自己的想法，与我们的秘密规则无关。当他们所提出的数列得到证实时，会进一步驱使他们确信自己原先的猜测是正确的，而不是想方设法否定自己的猜测。

现在，我们可以把这个秘密规则想象为一个接受输入的软件规则，并把这个小实验的参与者想象为软件测试人员，他们相信所有用户的输入都应该按2递增。他们并不会测试其他数列，例如1、14和9076（更不用提像-55、-30和0这样的数列了）。因此，系统最终肯定会接受没有经过测试的输入，这很可能导致系统的崩溃。

为什么会出现确认陷阱？事实上我们都倾向于自己的猜测是正确的而不是错误的。虽然按照严格的逻辑要求，应该用不遵循自己的假设的数列（例如10、9、8）对这个假设（即所有的输入必须是偶数，或者必须按2递增）进行测试，但是试图增强自己的假设而不是驳斥它却是人类的天性。

“这部分软件是否按预想的工作？”这个问题不仅需要用我们所倾向的方式进行测试，而且要用怪异的、恶意的和随机的方法进行测试。但是，内部软件测试很少会重建常规的终端用户和恶意对手很可能对软件进行这类输入的这个真实场景。

[1]在这里，黑客表示受信任的，是正面评价。

[2]有时候，雇用一位协议的密钥发明者并让他“重新发明”这种密钥要比获得这项技术的许可更为便宜。出席这次晚餐的工程师就是Microsoft负责把DCE/RPC“重新实现”为SMB/CIFS的人员之一。

[3]“Reasoning About a Rule，”Peter Wason，《The Quarterly Journal of Experimental Psychology》。

1.2.2　分析师确认陷阱

考虑一位在一个三字母机构（如CIA、FBI）工作的情报分析师。这位分析师希望创建有效、实用的分析报告，以提升她的职业地位。这位分析师从多个来源采集信息，包括她以前所创建的报告。接着，她把这些报告提交给自己的上司。这个过程看上去很简单，但实际上包含了一个潜在的确认陷阱。在她的上司审阅她的工作之前，很可能上司以前也是一位分析师，并且创建了一些报告，并被现在这位分析师作为参考材料。换句话说，输入决策的创建者同时又是决策的审阅者的情况并非罕见。

显然，分析师会产生一种倾向，就是把她的报告与上司的报告保持一致，而不是与之相悖。她很可能会有意识地这样做，特别是当她试图在该社区或机构内获得更好的职业生涯时。当然，她也有可能是无意识地这样做，就像前面Wason的3个升序数字例子一样。至少，这家机构的结构和信息基础形成了一个很强的自增强反馈循环的可能性。

我个人遇到过两个例子，人们认识到确认陷阱的存在，并积极工作以确保不会陷入其中。毫不惊奇的是，这两个例子都涉及用情报分析场景引起我注意的相同人们，他们证实了我对情报报告中经常出现这种错误的怀疑。

1.2.3　陈腐的威胁模型

在上届总统任职期间，我担任了政府部门的一个关键人员小组的顾问。我的重要任务之一是对有些人所收到的关于网络功能（包括攻击性和防御性）的报告发表自己的看法，并指出报告中的哪些研究领域有效或者具有前途。我常常不得不指出，最初的报告在对手模型和技术方面的不准确达到了令人痛心疾首的程度。报告中所描述的技术、策略和功能与具有良好的经济能力和高度的工作动力的对手可能采用的技术相比实在差得太远。报告中所描述的许多只有强有力的国家级对手才有可能使用的技术和策略对于当前的网络发烧友而言只是雕虫小技。

这些报告试图理解网络威胁是如何演变的，但它们却是根据以前的技术进行推断的，显得毫无新意。技术已经发展了，但模型却没有发展，仍然远远落后于现实。因此这些报告所描述的场景对于过去的某个时期而言可能是准确的归纳，但对于当前而言却是过时的和不准确的。这正是确认陷阱的特点。因此，这些需要我进行评论的报告就是由于与前面所提到的分析师陷阱相似的情况下产生的。

1.2.4　正确理解功能

随着L0pht对安全的成功破坏以及像L0phtCrack这样的工具变得广为人知，政府不得不对我们的队伍勉强产生了一点兴趣，希望理解我们能够做些什么。我勉强答应邀请白宫的一群人参观我们的工作，并向他们简单介绍了我们的情况。不得不说，L0pht成员们对于一大群政府代表的来访感到不安，但最终我和其他成员成功说服了每个人，允许这些“政府人员”来到我们的“秘密”地点。

当晚，在会议结束并共进晚餐之后，我陪同政府代表们走到停车场并向他们道别，然后看着他们走向自己的汽车，以确保他们全都离开。当我看到他们停下来悄悄谈话时，心里就疑神疑鬼的。

我快步走到人群中，冲着他们嚷道：“你们不能这样！回到办公室以后，你们想说什么秘密都可以。我们之所以让你们参观，是因为对你们抱有极大的信任。因此，我想知道你们刚才在讨论什么！”这打断了他们的谈话。也许是酒精的作用，使我有勇气对这群颇有身份的人们说出这番话。也许，当时我不知道更好的做法是什么。

我想这番话让他们呆住了。这五个人都是政府高级职员，每个人都把目光投向其中一个人，这个人看上去像是其中地位最高的。他回过头直接注视着我，并解释道：“我们只是在讨论你们是怎么做出这些东西的……”

“您的意思是？”我继续追问。

他回答道：“我们收到的所有报告都表示，你们在这里所完成的功能，如果没有国家的财政支持，是不可能成功的。”我的答复是我们已经向他们证明，我们在没有得到任何资助的情况下已经完成了这些工作（应当注意，低估那些失去面子的好奇的人们的能量是极大的疏忽）。“我们进一步怀疑，”他说道：“有没有任何政府已经采纳了你们的方法或者试图‘雇用’你们？”我很快用我习惯的风格答复道：“没有。至少我还没有注意到。但是您如果愿意，可以成为第一个，我们愿意接受邀请……”

即使我试图摆出幽默的架势，但我们还是不欢而散。

尽管双方都心怀疑虑，并且双方由于立场差异巨大而导致沟通困难，但政府还是明白了我们的成果确实是一些业余发烧友在几乎没有得到任何资助的情况下在业余时间完成的。

这些访问者就是从各个三字母机构接受报告的人。他们意识到这些机构的职业阶梯很容易导致确认偏见。他们被这些机构的官员所说服，以为我们的成果需要大量的资助，只有特定类型的对手才可能实现。因此，他们采取了大胆的步骤，对我们进行了登门拜访，这样他们可能会抵制向他们所灌输的一些基本信仰。他们的步子迈得真是够大的，竟然造访我们隐秘的L0pht基地。但是，他们最终修正了对手发动恐怖的网络攻击真正所需要的条件的假设。

遗憾的是，很多人并不是像他们一样能够或愿意寻找令自己不安的证据来否定自己的假设。在测试软件和系统时，考虑工程师、开发人员和测试人员可能工作的环境以及他们可能具有的先入为主的想法是非常重要的。当涉及应用程序要求做什么以及什么样的输入可能会有意或出乎意料地对它们造成威胁时，这方面的考虑就显得格外重要。

1.3　功能锁定

功能锁定表示无法看到超出某些东西的常见用法范围的其他用法。这与第一印象的概念有点相似，即第一次接触到信息时（例如一篇新闻报告的偏向性标题或起诉方对案件的陈述）所留下的印象，常常会永远地影响听众对这种信息接下来的感知。

当有人提到“锤子”时，一般人首先想到的是一种用于造房子的实用工具。很少有人在听到锤子时立即把它与一种攻击性武器联系在一起。类似，人们在听到“火焰喷射器”这个词时脑海里立即会浮现一幅军事武器的图像，很晚（即使有的话）才有可能想到它是一种通过先发制人的燃烧策略防止火势蔓延的消防工具。

功能锁定就是指不能理解一种工具的最常见用法或“默认”用法之外的其他用法。如果一个人认为一种工具只有一种可能的用法时，我们就把它称为“锁定”。

考虑口袋里的一把零钱中的一枚二角五分硬币。如果有人问你打算怎么使用这枚硬币，你首先的想法很可能是用它来买东西。但是，人们当然也可以把它派到其他用场：

·用来抛硬币，以决定应该怎么做某样事情。

·把它当做起子，例如撬开蛤蜊。

·投掷物，用来击中别人。

·防止门关闭的垫片。

·作为具有美学价值和历史价值的收藏品。

如果忽略这些其他功能，很可能会通过想不到的方式让你感到吃惊，例如有人可能会提出购买你的旧硬币，或者当你把一个硬币送给你家的小孩之后，他却用力把它砸在你的后脑上。

1.3.1　安全位置的潜在风险

既然对功能锁定已经有了一个基本的理解，现在读者可能会疑惑它是如何与计算机和网络产生关联的。

许多人把诸如漏洞扫描器和反病毒软件这样的安全产品看成是能够增加系统或组织安全性的工具。但是，如果这只是你的唯一观点，就有可能遇到功能锁定问题。这类技术的每一个都可能非常复杂，由数千行代码组成。在一个环境中引进它们同时也引进了很大的出现新的漏洞和攻击表面的可能性。

以早年的漏洞扫描器为例，我在自己所在公司的内部网络上设置了一些特殊的系统。这些系统是恶意服务器，目的是攻击当前大多数流行的漏洞扫描器中的客户端漏洞。当时我并没有意识到客户端的漏洞攻击在几年后的恶意软件感染中将会非常频繁地出现。例如，ISS扫描器将连接到一个远程系统上的指名服务（finger service），以收集远程系统信息。但是，这个扫描软件在它的其中一个安全测试中出现了一个典型问题：这个程序没有检查返回信息的长度，而是盲目地把它复制到一个固定长度的缓冲区。这就在程序堆栈上产生了一种很普通的缓冲区溢出。知道了这个扫描器的这个弱点，并且知道了这个扫描器所运行的系统架构之后，我对恶意服务器进行了设置，以攻击这个漏洞。当我所在的公司将要接受年度审核时，作为审核者的评估工作的一部分，将从审核者所带入并连接到公司内部网络的笔记本电脑上运行网络漏洞扫描。当这个扫描器最终遇到我的其中一个恶意服务器时，扫描系统本身由于扫描软件的这个漏洞而遭到攻击。

这常常导致很幽默的场景，它为公司的领导层提供了一些借口回应审核人员。由于被攻击的审核系统通常要对多个客户进行审核，因此我们可以用这个审核系统所暴露的其他公司的审核信息来忽悠他们。公司的领导层可以理直气壮地声称，在我们的内部系统中所找到的漏洞（隐藏在防火墙和其他防御技术的后面）相比审核人员向竞争对手泄露敏感信息而言实在算不上什么风险，而且它本身很可能是审核人员所使用的“安全软件”所造成的。功能锁定可能导致人们忘了安全检查软件本身的安全。

遗憾的是，现代的反病毒软件已经被发现包含了各种类型的常见编程漏洞，例如局部缓冲区溢出、未检查的执行功能以及在自动更新活动中缺乏认证等。因此，安全软件也可能为攻击者开启大门，而不是起到防御作用。

前面的例子是功能锁定的简单例子，可以归结于我在习得性无助这一节中所讨论的无从选择。但是，还存在更为微妙的例子。

1.3.2　降低成本与未来收益：ISP实例

安全的最大阻碍来自公司高层对安全需求的负面认识。其中有些认识代表了功能锁定。几个月前，在著名的分布式拒绝服务攻击Internet上主要服务供应商和商业实体（包括eBay、CNN、Yahoo！），并导致它们临时关闭前，（[1]）我曾有机会为一家一级ISP分析骨干路由器配置。经过这些核心路由器的IP流量的主体是TCP流量，特别是HTTP通信。UDP所占据的比例要少得多，至于ICMP则少得可怜。我很惊奇地发现，这些路由器对流量缺乏任何控制，只是采用了一些最低限度的过滤器，以防止某些形式的对这些路由器本身的未授权访问。但是，当我建议对核心路由器的配置进行更改，侧重于保护这家ISP的顾客时，这下轮到公司的领导感到惊奇了，他们立即告诉我此事绝不可行。

两种思维在这里发生了冲突。ISP并不想冒着减少它们的核心路由器吞吐量的风险，如果它们使用了任何类型的具有实质意义的包过滤器，就会发生这种情况。总之，ISP的业务是销售带宽，顾客把它看成是吞吐量。当系统把数据包从A点移动到B点时，就不得不花费额外的时间决定如何处理每个数据包，因此路由器的行为和最终的吞吐量会受到负面影响。

而且，此时不论是ISP还是它的顾客都无法忍受任何负面效果。管理层可以理解可能会发生针对它们的路由器的攻击，但他们愿意等待，在发生这种情况时再对它进行处理。他们觉得在没有出现问题的时候花钱是件浪费的事情，虽然未来出现问题时所需要的花费可能大于现在积极预防问题发生所需要的开销。此外，对顾客的攻击则不在他们的考虑范围。

相反，站在我的角度，尽管当时还没有发生广泛的DDoS攻击事件（事实上，当时还不存在DDoS这个词），但我还是意识到了不仅针对ISP的服务器而且针对背后的顾客的网络资源饿死型攻击的可能性。我知道对顾客的攻击将很难被诊断，并且难以快速作出反应，但是我完全无法说服ISP。事实上，我不得不从商业的角度理解他们的想法。他们对自己的系统不需要进一步采取安全措施的理由多少是符合逻辑的。

（Randy V.Sabett所编写的第12章也讨论了安全是成本而不是收益的问题。）

在大规模的DDoS攻击之后的某个时刻，我有幸被邀请参加在白宫的椭圆办公室所举行的圆桌会议，所坐的位置距离克林顿总统只有几个座位之遥。这个会议的目的是讨论政府和行业如何应对最近的DDoS攻击以及接下来应该做些什么？

在会议上，我再次感到吃惊。商业部门的领导层所表达的主要担心是这些攻击可能会导致政府介入，并对它们的行业进行调控。看上去，他们实际上对于理解或处理当前的技术问题并没有什么兴趣。

我开始渐渐明白，ISP固守一种概念，他们觉得政府在这些事物上的干预很可能会对他们的收益产生负面影响。他们与我在前几个月所遇到的那家拒绝在它们的核心路由器上采用包过滤器的大型ISP具有相同的概念：安全需要花钱，并且只能预防未来潜在的损失。他们从来没有从安全也可能制造收益这个角度去思考问题。

在会议之后，我重新约见了几个月前打过交道的那家大型ISP的执行官。我告诉他，我理解他为什么做出这样的安全决策，并要求他真心回答后来在我脑海里萦绕的一个问题。我要求他假设我并不是站在安全的角度问这个问题的。反之，假设我已经指出ISP可以协商承诺访问速率（Commited Access Rate），根据这些新的确定性更好地规划利用率，并最终使每台关键服务器服务更多的顾客。而且，他们可以使用这个方案为他们所销售的新类型的服务提供不同的记账和报告功能。过滤和测量可以防止用户不合理地占用带宽，但任何客户端所发现有用的网路被封锁或变慢是可以通过不同服务水平来满意解决的。但是，作为回报，这种过滤可以极大地降低外部恶意因素对带宽的不正当占用。我的问题是——这会不会是一个更好的方法？

他的回答是一声响亮的“是”，因为这家公司把这个方法看成是一个机会，认识到它可能带来的更多收益，而不仅仅是把它看成是与安全姿态相关联的日常开支。

我从这个事情中认识到，我（包括安全领域的绝大多数实干家）受到“安全只是它自己的事情，它并没有被看成是一个不同目标的副产品”这个功能锁定的困扰。正如经常被事实所证明的那样，构建高效和具有良好定义的需求也可以增强安全。

[1]“Clinton fights hackers,with a hacker，”CNN,February 15，2000（http://web.archive.org/web20070915152644/http://archives.cnn.com/2000/TECH/computing/02/15/hacker.security/）.

1.3.3　降低成本与未来收益：能源实例

我的工作有一部分涉及详细检查各家电力公司的后端控制系统，有时候还包括石油公司的后端系统。我评估了它们与其他系统和网络的相互联系是如何被保护和跟踪的。令人吃惊的是，石油和电力行业虽然使用了相似的系统和协议，但它们在配置和安全方面的操作和运行存在极大的差别。

换种礼貌的说法，电力公司的网络是一团糟。电力控制系统和网络可以通过公共的Internet访问。用于通用的系统由许多任务所共享，文字处理和其他日常工作与那些关键的功能混杂在一起，后者应该局限于专用的系统内部，以防止操作的潜在干扰和破坏。在好几个地方，系统和网络的布局随心所欲，根本没有考虑过优化的甚至是准确的操作。实现者在完成一项任务之后就转移到另一项工作。许多电力控制网络、电力信息网络和公司LAN没有配备防火墙或阻塞点。从安全的角度出发，所有这些因素结合在一起，可能导致恶意入侵者潜入后大搞破坏，包括操纵或破坏电力的生产和传输过程所使用的物理组件。

相反，我所观察的几个近海石油系统虽然也利用了类似的SCADA系统，却采取了不同的配置和操作方式。电力控制和信息网络严格与公司的LAN分享。大多数关键系统进行了正确的设置，使它们的结果和状态由一个管理系统进行处理，然后用一种类似二极管的风格把信息推向更高的分析系统。简洁和高效的网络图导致物理世界中的SCADA和DCS系统可以被清晰、整洁地实现，包括对访问的限制，有效地提高了安全性。在许多场合，组件是自定义的系统，被设计和配置用来执行特定的功能（[1]）。

电力和石油公司之间的反差令我感到好奇和困惑。仿佛是命运的安排，我的职位使我能够与电力公司、石油公司和政府部门的高级技术人员一起开会讨论这个主题。

在会议中，首先令我感到惊奇的是来自电力系统以及电力系统监管和结算组织的人们，他们并没有反驳我对他们的网络和系统的不良（或者说完全不存在的）安全所作出的论断。这令我感到吃惊，因为电力公司在公开场合总是否认它们存在任何网络系统风险。在会议中，他们指出了一些已经正确地实现了安全的例子，但他们承认这些例子只是例外，并不是普遍现象。

我的第二个惊奇来自石油公司，他们表示根本没有站在安全的角度设计他们的系统。尽管安全非常重要，但它并不是正确配置系统的商业推动力。主要的推动力来自它们的直接竞争对手的压力。

如果A公司在关键组件操作的效率能够比B公司高出5%，经过一段时间之后，增加的操作能力或减少的开销可以使A公司赚得大笔的钱。提高这种效率的例子包括：

·系统的强制分离，防止关键系统由于管理和报告请求所执行的查询而增加额外的延迟。

·利用特殊用途的专门完成特定任务的系统替换通用目的的非优化系统。

这些效率改进措施同时也改善了安全。第一个措施在网络和系统中创建了强大、干净和可实施的边界。第二个措施使系统的表面区域变小，从而增加了攻击的难度。

可实施的网络和系统边界的效果显而易见，但更小的表面区域这个情况则值得稍加解释。想象一个具有默认配置的通用目的的系统。默认配置可能已经有几个配置并正在运行的服务，以及许多正在执行的协助用户处理的后台进程。这就允许用最大数量的设置对系统进行部署，并最大限度地减少重新配置的数量。系统的生产商希望这样的系统具有更广的功能，因为这样可以使安装更为方便。

但是，这并不意味着默认设置对于消费者主体都是优化的，只不过是可以接受而已。在默认设置中，每个正在运行的服务都是一个攻击表面，可能会遭到攻击。与此类似的是客户应用程序可能由于来自被盗用或虚假的服务器的恶意输入而被盗用。系统所运行的服务和客户应用程序越多，系统的攻击表面就越大，被远程或本地盗用的可能性也就越大。

虽然具有较大的攻击表面不是件好事情，但是石油公司所关心的基本缺点实际上是系统的性能不够理想。对于每个正在运行的程序，包括服务器所提供的服务以及本地应用程序，处理时间是由内核和CPU贡献的。如果有许多正在运行的应用程序，系统就不得不为它们划分时间片，这是一种本身就会消耗资源的内核活动。

但是，如果正在运行的应用程序数量很少，那么每个应用程序可以拥有更多数量的CPU时间片，实现更佳的性能。对系统进行瘦身的一种简单方法是删除多余的服务和应用程序，对系统进行优化，使它尽可能以最专一的方式运行。另一种方法是系统部署为专门实现特定的功能，不提供运行不相关任务的功能。这些策略在我已经检查过的近海石油公司中使用，以便最大限度地提高性能，并因此增加了利润。

为什么电力企业不采取与石油公司相同的做法呢？首先，电力公司是受控制的垄断企业。这个行业并不存在什么竞争，因此它们就没有动力设计优化和严格结构化的环境。

人们可能忍不住假设如果取消对电力行业的控制，把电力公司放在竞争环境中会不会提高它们的效率并因此提高它们的安全性（按照与石油公司相同的路径）。但是，事态的发展恰好相反。当电力公司放松管制时，它们意识到需要采取削减成本的措施以保持竞争力。结果，它们的第一个步骤就是削减劳动力。它们让更少的人维护和操作相同数量的本地和远程系统（常常通过远程访问技术），把注意力集中在每天都要进行的操作，而不是高瞻远瞩地看到长期的需要。这通常是提高效率或安全的不佳方案。

石油公司的故事证实了我在1.3.2一节中对ISP所作的评论。大多数组织把安全看成是一种令人不快的成本，至少到目前为止它们大都是这样想的。按照这种方式实现的安全很可能是不充分或糟糕的。但是，如果有的公司将网络和系统的功能进行优化和流水化，以实现特定的商业目的，那么它们常常可以以副产品的形式实现优质的安全。同样，安全专业人员可以把克服他们对安全的功能锁定作为一个崇高的目标，直到觉得安全本身值得花费大量的资金，而不是把安全看成是一种偶然的副产品。

[1]值得注意的是，我只分析了那里的一小部分石油和电子系统。这里所归纳的区别是出于比较的目的，以说明功能锁定以及它是如何影响公司对安全的看法的。石油行业也有部分企业存在配置不正确的系统和环境，这和所有大型行业一样。类似，可能有些地方的电子企业具有良好配置的电力信息和控制网络。

1.4　小结

在本章中，我提供了一些经典的安全失败的例子，并在脱离工具、实践和个人决策的情况下对它们进行跟踪，以探索如何进行思考的基本原则。我们可以采用与自然倾向相反的更明智方式应用我们的资源，以提高安全性。

·我们可以保证最初的决定并不会影响创造性的思维，以克服习得性无助和无从选择。

·我们可以寻找不同人群的输入并强制自己试图推翻自己的假设，以克服确认陷阱。

·我们可以通过寻求工具的替代用法并通过其他路径来实现目标，以克服功能锁定。所有这些都需要实践。但是对它们进行实践的机会每天都会出现。如果更多的人致力于此，这种常常称为非比寻常的方法将越来越不稀奇。

第2章　无线网络：社会工程的沃土

如今，许多人都已经听说过关于无线设备安全的问题。从2000年最初发布Wi-Fi时，无线设备安全就已经是许多安全专家所关注的领域了。早在2001年，有线等效保密（Wired Equivalent Privacy,WEP）访问协议就被发现存在严重的缺陷。虽然设计这个协议的目的就是为了阻止非法用户访问无线设备，但是攻击者在几分钟之内就可以绕过它的安全保护机制。随着2003年Wi-Fi网络安全存取（Wi-Fi Protected Access,WPA）协议的公布，无线设备的安全性已经得到了极大的提高。尽管如此，大多数偏执的系统管理员仍然对它的安全性表示怀疑。尤其是在发现了WPA的几个新的漏洞之后，他们就更加确信这一点了。虽然它的危险程度不及WEP，但这足以让许多管理员认为他们的看法是正确的。

虽然仍然有人对无线网络的安全性持怀疑态度，但大多数人在看到无线技术给生活所带来的便利之后，都接受和认可了这项技术。比如，给百货公司的员工人手配备一台手持设备，那么他们就能够直接与机构的服务器联系，快速高效地完成一些与库存有关的工作。这可以节省大量的时间并提高客户的服务满意度。Wi-Fi也给市政公共事业注入了新的活力，从咖啡馆到公园都有它的身影。遗憾的是，人们对一些针对Wi-Fi的攻击场景仍然不是十分清楚，这种攻击将会引发企业和个人信息被盗的风潮。

本章首先介绍我作为一个专业的安全研究人员是如何探索无线安全缺陷的，并简单地描述它们所造成的威胁。接着，我继续讨论Wi-Fi的现状以及它是如何破坏安全的。

2.1　轻松赚钱

这是一个日常攻击场景。假设你在美国的一个主要机场稍作停留之后准备启程时，把目光转向登机监视器寻找自己的入口时，发现每位旅客都在哀叹：“飞机又延误了。”这时，你已经成为众多“难民”的一员，将在机场的舒适安静环境中度过接下来的6个小时。这时你的视线离开监视器，四处搜寻可用的电源插座，以挽救快要没电的笔记本电池。我经历过很多次这样的搜寻，每次都是慢慢地在整个区域到处查找，比如每排座位的后面或者是某根杆子的背面。你可能会注意到，寻找这种隐秘插座的人在路过时，似乎是在盯着你的脚下，却又做的不是太明显。我觉得这类似于穴居人在寻找火种。每个人都想拥有它，但只有少数人能够找到，而一旦你找到了，那么你会极具占有意识。事实上，在不止一个场合，当有人走近时，我都会哼一声，然后锤打胸口以示强势。

现在，假设你是找到插座的成功人士，然后打开笔记本，将电源插到插座上，并立即开始搜索无线接入点。现在，大多数机场、宾馆、咖啡馆甚至是公园都提供了无线接入设备。你只需打开笔记本电脑，点击无线接入图标，就会弹出一个或多个可供选择的接入点。当你浏览接入点列表时，看到了一个标题为“T-Mobile”的接入点。原来，这个机场使用的是这个接入设备，因此你毫不犹豫地选择了它。几秒钟之后，你打开了一个网页浏览器。此时显示的不是你的主页，而是T-Mobile的页面，它让你选择是使用已有的T-Mobile账号登录还是创建一个新的账号。

由于你并没有账号，所以点击创建了一个新的账号，这时会发现所需价格是9.99美元一天。虽然这个价格不是太恐怖，但你刚才注意到还有其他的无线接入点，所以你决定看看其中是否碰巧有免费的。你再次点击无线图标，然后看到列表中还有其他三个接入点。其中有两个是加了锁并且需要正确的密钥才能连接，但有一个标题为WiFly的是开放的。然后你选择了WiFly，这一次重定向到的是WiFly的登录页面，上面给出的价格仅为1.99美元。让你感到高兴的是，你刚刚节省了8美元，这时你拿出信用卡，填写在线表单，然后点击提交，瞧，你现在正在浏览互联网。

别的也没有什么事可做，所以你决定通过在线网页接口来检查你的电子邮件。输入网站的网址后敲回车。突然一个错误消息弹出，告诉你网站的安全证书有问题。只有在浏览需要加密访问的网站时才会用到安全证书。这时你才会意识到这个网站是使用加密会话的，因为网站链接是以https://开头的，而不是http://。

此外，你还会注意到浏览器的状态栏上有一个闭合的锁，这表示该页面也是加密的。但是，错误消息表明这个网站的安全证书不是由可信任的认证机构颁发的，另外你所访问的网站和证书不符。

现在你可以选择关闭这个页面或者继续浏览下去。稍做考虑，你想可能是自己访问错了，所以选择关闭该页面。然后你打开了一个新的浏览器窗口又试了一次，结果弹出了同样的错误。此时你可能意识到页面是正确无误的，但确实又有什么地方出了问题。你可能会想这也许和你在机场所用的服务提供商有关，所以选择继续浏览下去。页面出来了，而且看起来很正常，没有什么问题，所以你就登录进去并查看电子邮件。接着你又浏览了几个小时，中间除了又跳出来几次同样的错误消息之外，其他一切看起来都正常。

最后你的飞机来了，收拾好笔记本，把电源插座留给附近其他正在耐心等待的“穴居人”，然后到达了最终目的地。

几周过去，你回到了家里，然后去支付账单。当你打开信用卡账单时，大吃一惊，先前信用卡里面还有好几百美元，现在已经被刷爆了。想了一下，然后你去问你的妻子是否去疯狂购物了。让你感到放心的是，她确实没有去，但这些消费账单依旧还在。当然，你已经完全忘记了，在从机场返回的那一天，你选择了一个廉价的无线接入公司，并向其提供了信用卡信息。遗憾的是，原来这个便宜的无线接入公司的确是一个身份窃贼（identity thief）。

2.1.1　设置攻击

在工作之余，有人雇我去布置安全陷阱以用于测试。我已经在整个美国多次实施了这种特别的攻击。每一次我都能收集到信用卡信息。虽然骗局看似复杂，但让人如此担忧的情况却可以很容易的实施。

在我去布置攻击的场地之前，我会先制作像WiFly这样的一个虚构的公司登录页面，这个页面看上去很可信，而且外观绝对专业。它还提供了一个用于填写信用卡信息的表单。

到了场地之后，我打开一个普通的笔记本电脑并购买了该地提供的真正的互联网接入服务。如果是在没有互联网提供商的地方，我就会用我的手机来进行连接。即使网络速度会很慢，但这没有关系，因为当受害者在使用接入服务时，就已经被骗了。

接着，我给笔记本安装上一个无线路由设备。根据地方的大小，我会设置比较大的天线来覆盖尽可能广的范围。

最后，我运行我写的一个程序，截获从不知情的受害者电脑上发送到无线路由设备、再发送到我的电脑上的数据包，然后再把这些数据发送到真正的我所购买的互联网上。万事俱备，此时，我就像是一只等待苍蝇的蜘蛛。

最终，受害者出现。他们选择了低价的无线接入点，点击了之前设计的网页，并提交了他们的信用卡信息，至此，他们就成了另一个身份窃贼的囊中之物了。

2.1.2　隐私的聚宝盆

很明显，获取信用卡信息对身份窃贼来说是有用的，但是这类攻击还有更多的用途。

还记得浏览器弹出的关于证书的安全警告吗？之所以出现警告是因为那些加密的网络数据并不是在用户认为的最终目的地进行解密的，而是在我的笔记本上进行解密的。换句话说，使用安全的、加密的连接是因为这种操作确实需要，只是用了我的证书来进行加密，当然我可以轻易地解密了。类似于中间人攻击，我能够对用户的数据进行解密，记录下任何数据，然后再重新加密并传送给最终目的地。我可以记录用户名、密码、电子邮件信息以及其他的可能敏感信息，而受害者却认为这些信息会安全地发送给可信的最终接收方。

即使是一小部分个人网络数据都可能会导致一次危害巨大的身份攻击。举个例子，比如我只得到了你的电子邮件账号，现在我就能够浏览所有你接收到的电子邮件信息。想想那些所有通过电子邮件来获得的令人疯狂的信息吧。

如果你不用基于Web的电子邮件服务，那么你可能会认为在你的电子邮件账号和网页账号之间存在一个安全的屏障。但是，大多数网站都提供了“忘记密码”的选项。只需要提供你的email地址，他们就会把密码发给你。通过在一个人的足够长的邮件信息中查找泄露秘密的字符串，通常能够得到任何东西的用户名和密码，包括网银账号。

一个高水平的身份窃贼明白通向王国之路有很多。有时需要从几个不同的源来收集数据。像解决一个谜语一样，你得到的每一份数据都能够用来获取另一份数据。在某些测试中，我也会布置一个什么页面也没有的无线接入点。此时，我暂时放弃了嗅探用户的信用卡信息的机会，但却吸引了更多的用户，他们认为这个接入点更加安全，因为他们不需要提供诸如信用卡信息之类的敏感信息。这种测试的唯一目的就是记录下都有哪些数据流过。几个小时里我就记录到了从用户名和密码到在线购物信息，如姓名、地址和信用卡信息等。

通常，住在公寓里的人都希望不用自己购买互联网接入服务就能访问网络，这时他们就会连接到发现的开放的无线接入设备，而且认为这可能是邻居的接入点。他们认为这是免费的午餐，实际上他们每天都被身份窃贼所欺骗，身份窃贼布设这些接入点的目的就是为了欺骗别人。

正因为无线接入点如此普遍并且已成为日常景观的一部分，所以大多数人都没有想这些无线接入点从哪来。在购物商场里，你会相当肯定地认为从一家珠宝店里买的一块看起来不错的手表是没有问题。但是，如果你正在街上开着车，看到一个在他的车外叫卖手表的人，你可能就不会做出同样的判断了。这很容易看出两者之间的区别，知道什么看起来安全，哪些又看起来像一个陷阱。但是对于无线网络，你根本不知道是谁在提供。即使名称叫做T-Mobile而且登录页面也很像真的T-Mobile登录页面，但你又怎样知道这个特殊的页面真的是由T-Mobile运营的呢？任何能够部署无线接入点的人都可以随意设置访问页面。

2.1.3　Web安全的基本缺陷：不要相信可信系统

我的攻击之所以能成功的主要原因之一在于用户和服务器对安全证书的处理都存在问题。大多数人在浏览互联网时都不太在意安全警告。这主要有两个原因。

一方面，一些人根本不明白这些警告的意思。我想用我父母的例子来说明。他们和世界上其他到达退休年龄的老年人一样，他们是聪明的人，非常乐于接受互联网这一新鲜事物。但是，他们从没有接受过关于互联网及其风险的专业训练，也没有在有专门的工程师负责教会员工的公司里工作。他们只是自己体验整个互联网，也只有一般的能够应付在线支付和查看股票证券信息的知识。类似于数字证书这样的东西对他们来讲就像是质子加速器对他们的意义一样。

另一方面，我发现也有一些精通技术的人不但懂得数字证书，还了解中间人攻击。有人认为这类人从不会中这样的陷阱，但恰恰相反，即使是这些人也很容易上当受骗而成为受害者。与我什么都不懂的父母所不同的是，这些专家受骗的原因是他们太了解这些知识了，以至于能够为自己失误做出合理的解释。

例如，当安全警告跳出来时，他们首先认为是这因为WiFly网站的管理员没有更新已过期的证书而导致的。或者他们认为由于这个网站是由咖啡馆或宾馆提供的，而配置这个网站的人没有经过良好的技术训练，所以没有正确地配置网站的证书。除此之外，在企业内网中你也会经常碰到证书过期的情况，而这时公司的信息技术人员会告诉员工只要忽略安全警告，然后继续使用网站就可以了。这类事件让普通用户认为证书是不重要的，而那些明白证书重要性的高级用户则对他们没有关注的地方一点都不敏感。

在完成攻击之后，我和受害者交谈时，受害者对他们的行为的普遍答案都是根本就没有关注所有弹出来的安全警告。他们大多数似乎都是被操作系统搞烦了，因为有些操作系统每天都会给你提示许多的安全警告（最糟的是微软的Vista操作系统），这使得安全警告变成了白色噪音。

2.1.4　建立无线信任

尽管使用无线接入存在风险，但它们显然又是非常方便的，而且我也不建议所有人都停止使用它们。人们需要的是在不降低使用频率的前提下，找出减少风险的方法。

用户进行自我防范的最有效的办法是多注意安全警告。如果你在使用无线接入服务的时候，收到了关于安全证书存在问题的警告，那么你需要立即停止操作。虽然这也有可能是因为某些网站管理员的操作错误或者是没有更新证书所导致的，但继续操作下去的风险仍然非常大。

当然，这也是要求企业对证书进行良好管理的理由之一。数字证书是企业能够让最终用户对网站的安全持有信心的少数保障之一。如果连证书都没有维护好，那么很容易导致别人对其他方面的安全产生怀疑。

如果某个要求输入信用卡账号，此时你可以采用一种比较简单的方法来验证这个网站合法性，即提交一个假的信用卡账号。如果这个网站是合法的，那么它会检查信用卡账号并且告诉你交易失败。如果是一个恶意的网站，那么它不会作检查，无论你输入什么它都接受。这种方法并不保证一定有效，但有总比没有好。

最后一个建议是尽量不要使用公用的接入点来进行个人交易。咖啡馆也许并不是最好的登录并检查你的银行账号收支的场所。在机场登录并进行股票交易可能会受到一些不好的影响。作为一个经常出差的旅行者，我自己也知道有些时候你别无选择，并且可能也摆脱不了使用你所找到的无线接入服务来传输个人信息的情形。如果发生这种情况，而且也稍微注意到你输入了敏感的信息，那么一旦你回到了一个可信的地方，就要立即修改刚才使用无线接入服务时的登录账号。当然，这也不是说你已经是一名受害者了，但这么做没有什么坏处。

2.1.5　采用可靠的解决方案

虽然时刻注意你的个人信息安全确实可以起到一定的作用，但却不能真正消除这类攻击所带来的危险。为实现这一目标，业界需要解决信任关系。当我打开无线接入软件并搜索可用的无线接入点时，我需要确信一个叫做“T-Mobile”的无线路由确实是属于T-Mobile公司的。

要做到这一点，接入点和客户端之间必须能够交换可以对两者进行鉴别的核查信息。虽然在不同的系统中协调实现这种相互信任比较困难，但这种技术本身确实非常容易，而且已经出现很多年了。用于网站的证书技术同样也可以用于无线接入设备。

这种解决方案的工作流程大致是这样的。用户首先打开无线客户端并收到了一份可用的无线接入点列表。然后客户端会检查对应于每台设备的数字证书。每个证书都包含显示出来的名字、公司名称、硬件标识符以及签发证书的认证机构。由于是无线客户端列出这些设备的，所以能够指明他们是否合法（见图2-1）。

有许多建立认证中心（Certificate Authority）的方法，我所想象的最容易的办法是采用和Web认证相同的认证机制。使用一套已经建立起来的机制会让这个特殊领域的开发变得更加容易，而且利用已经部署的基础设施可减少大量的重复开发。

图　2-1　假设经过认证的无线路由

我也意识到有些恶意的无线路由可能会使用伪造的证书，而且一些用户可能从未注意到合法的和非法的接入点之间的区别。但是当对人们进行这种解决方案的培训越来越多以及客户端软件会尽可能正确地指引人们时，这类攻击的风险性将大为降低。

2.2　无线也疯狂

在描绘了前沿的无线攻击技术以及如何进行相应的防范之后，我们再回过头来讨论众所周知的Wi-Fi安全问题以及使其成为日常威胁的社会条件。

习惯于以以太网技术为基础的局域网网络用户和管理员很难理解无线的相对不可控性。局域网使用的是非常不安全的协议（嗅探和篡改、系统伪装和实施拒绝服务攻击都是很常见的），但是有线电缆的物理限制性能够缓解一些安全问题，你不可能在网线内部部署一个恶意的系统。而无线技术故意去掉了我们习以为常的物理安全层，使得网络流量会泄露出去，甚至能够跨域有形的围墙和围栏之类的物理界限。

如果我在本章开始提到的，管理员（假设他们使用部署任何安全措施时都足够细致）一开始是采用WEP来保证接入点的安全。WEP的主要问题是黑客能够很容易地嗅探到在空气中传输的数据包，并且能够提取密钥进行解密。甚至初级黑客都能够借助于一些工具来实施这种攻击。

后来又引进了WPA技术来解决WEP问题，即解决从嗅探到的数据包中快速提取密钥的漏洞。这时，无线工程师们都很高兴。

当然，喜悦是短暂的，又有人发现仍然能够提取用于WPA预共享密钥（Pre-Shared Key,PSK）的密码短语。WPA协议首先传输的是交换包，即所谓的握手机制，它能够验证客户端和接入点。由于数据是加密的，所以即便是在无线路由附近对传输的数据包进行监控并记录整个握手过程，也没有人能够推导出密钥进而破坏整个系统。但是，黑客可以记录下这些数据，然后采用暴力破译密码的方法来进行攻击，即从一张单词列表中对每个单词进行穷举，以找到和记录数据相匹配的。

黑客要成功地实施这种攻击，需要满足几个条件。首先，黑客要必须能够从接入点接收到无线信号。我见过很多公司把这作为主要的防御点。有些时候，考虑到在墙外接收到的无线信号会很弱，管理员会把接入点安装到非常隐蔽的地方。确实，这样做的话，普通笔记本电脑中的无线设备很难再捕捉到无线信号了，但是对于那些坚定的黑客来说，他们往往都拥有非常强大的天线以接收任何弱小的信号源。

其次，黑客需要从客户端和无线接入点开始会话时就进行监控。监控会话建立后的数据包没有什么用处。虽然听起来这个要求大大减少了黑客获取初始握手信息的机会，但实际情况是这根本算不上什么。事实证明，已经出现了大量的能够终止无线会话的工具，它们能够迷惑客户端，并发送一条终止会话的命令。一旦连接断开，客户端一般会尝试重新连接。这时黑客就能够对数据包进行监控并获取初始握手信息了。

最后一条是密码短语本身的强度。在我的测试中已经出现了像tootired和bicycles这样简单的密码。密码破译软件在几分钟之内就能够找到这些密码。一个WPA密码短语可以由8到63个字符组成，但是我发现大多数人一般都只用8个或者9个字符。当然，最好的密码短语是63个字符，它可以混合字母、数字和标点符号。那么为什么没有一个管理员这么做呢？

最主要的原因是不管你为接入点设置了什么样的密码短语，每台电脑在连接的时候都需要输入这一密码。对于一个真正疯狂的密码的支持成本将成为一个噩梦，因为人们会输错字符或完全忘记了密码。所以，取而代之的是，管理员走向了另一个极端，即使用尽可能简单的密码。虽然没有完美的解决方法，但我建议可以采取皆大欢喜的折中方法。比如“This1was900yearsold！”这样的密码既便于记忆，又比标准字典中的单词更难破译。

2.2.1　无线侧信道

担心无线接入点被伪造是一回事，但是负责保证网络安全的系统管理员还有更多方面需要考虑。

一些企业很久之前就认为浏览互联网网站对他们的网络造成了很大的风险，因此完全屏蔽了网络浏览。过去这种办法看起来很有效，但是最近随着开放的无线接入点遍地开花，威胁再次显露出来。许多用户发现他们可以在工作的时候带着一个USB无线设备或者使用笔记本电脑中的无线设备来登录到临近公司的无线网络。

随着大约五年前“战争粉迹”（warchalking）（[1]）的出现，开始流行黑客们使用别人的无线接入服务。这个术语是指黑客在有开放的无线网络的场所留下来的标记。在有黑客看到这种标记时，就知道能够使用笔记本来连接这里的无线网络了。

随着无线技术越来越流行，即使是一些没有什么技术的用户也开始使用这种方法了。这时管理员所面临的问题就变成了这些用户根本就不清楚他们使用新发现的接入点会给自己公司的网络所带来的潜在的危险。

任何绕过自己所在公司的安全基础设施，利用其他设备来访问互联网的用户，都把自己的安全交给其他公司所实现的安全机制来保证。然而，从无线网络是任何人都可以连接的这一点就可以看出这家公司的安全措施并不好。可能已经被本公司的入口所屏蔽的病毒和蠕虫此时就有了新的传播途径，它们会从这个无线接入点进入企业网络。

一些用户把他们的笔记本电脑或台式电脑通过有线网络连接到他们公司的本地局域网络，同时又通过无线连接到其他公司的网络。这种设计会在两个网络之间创建一个潜在的通道，它能够直接破坏更为安全的企业的整个内部网络，使大部分安全预防措施都形同虚设。

黑客们也同样知道企业员工是如何利用开放的无线连接来访问互联网的。出于这个原因，黑客就会在一些敏感的场所附近部署伪造的无线接入点来试图获得企业信息。

他们的攻击相当简单。首先，在某个建筑物中设置一个能够访问互联网的无线接入点。大多数情况下，这相当容易实施。因为许多小公司没有对网络进行真正的控制，而且也不知道员工什么时候安装了这种设备。接着，使用修改的天线来增强无线信号的强度使之覆盖尽可能广的范围。然后，写一个小程序用来监控无线设备上的所有活动。一旦有活动发生，就会通知黑客。此时，黑客就会着手攻击那些连接到无线网络上的电脑。如果某台电脑有漏洞，黑客仍会让它继续使用互联网，同时却静悄悄地获取这台机器的访问权限。通常情况下，间谍软件和木马就会登录到这些用户的电脑上了。

一旦系统被攻陷，那么黑客就能够获取任何类型的信息。既连接到公司的内部网络，又连接到黑客的接入点的系统简直就是一块金子。虽然在初次访问用户网络的时候会出现一些路由问题，但即便是最低层次的黑客都能够在几分钟内解决这一问题。

那么企业的内部网络监控此时又会如何呢？取决于所采取的安全机制，他们可能从不知道网络中正在发生些什么，也许直到破坏已经存在许久之后才会发现。

[1]在墙角或墙面用粉笔标上一系列的符号（面朝外的半月形），以指出附近的无线接入点。

2.2.2　无线接入点自身如何

如果我提到TJX，你会想到什么？如果你经常关注过去两年的主流新闻，你可能首先想到的是这家公司曾被盗过许多信用卡账号。这件事在2006年12月才被人们发现，黑客攻入了他们的网络并不断地下载了至少四千五百七十万个信用卡账号，而且据推测这个数据更接近于两亿（TJX事件在第3章中有详细介绍，作者是Elizabeth A.Nichols）。专家预测TJX公司需要花费1亿美元左右才能弥补此事带来的后遗症，在这个时候，越来越多的证据表明，更多的公司每天都面临着同样的安全缺陷：未加保护的无线接入点。在2007年11月，Motorola公司提供安全和无线网络监控服务的AirDefense部门发布了一项关于全国范围内的3000个商场的无线设备安全研究报告。据该报告指出，25%的无线设备根本没有使用任何加密措施，剩下的1/4仍然还在使用老的、具有安全漏洞的WEP连接协议。

在TJX案例被广泛宣传一年之后仍然还有如此之多的安全隐患，这是非常可怕的。1/4的商场比TJX的安全措施更薄弱，剩下的1/4的安全措施和TJX公司的一样容易绕过。

决定使用无线技术所带来的方便的企业除了清楚地知道它所具有的风险，还要不懈地做好无线设备的安全防护措施。TJX在初次部署无线路由的时候，也实施了当时存在的安全机制。但遗憾的是，这种安全机制很快就过时了。假如这家公司稍微花此时间来更新部署更为安全的WPA机制，很可能我们大多数人都不会听说过TJX。

2.3　无线仍然是未来

从酒店和机场到企业办公大楼和超市，无线接入的需求仍然在持续增长。和互联网一样，更多的安全风险将会暴露出来。企业、管理员，甚至是普通用户对这些安全威胁的重视仍然是个问题。使用开放的无线接入点是有风险的，用户需要明白这一点并做出相应的举措。除此之外，如果没有做好无线接入点的安全保护将会导致更加严重的危害，这正如TJX公司的案例。如果你打算采用新技术，那么必须考虑到所有的后果。

第3章　美丽的安全度量指标

当你可以对自己所论及的东西进行度量，并可以用数字表达它时，你就对它有了相当程度的了解。当你无法对它进行度量，无法用数字表达它时，说明你对它缺乏了解，也就无法达到满意的程度。它可能是知识的起点，但你觉得很难把它推进到科学的状态。

——William Thomson,Kelvin勋爵，1883

界定现代和过去的革命性思想就是对风险的掌握；未来并不是上帝的一时兴趣，人类在自然界面前并不是全然被动的。在人类找到办法跨越这个边界之前，未来仍然是过去的一面镜子，它是在预测未来事件方面占据垄断地位的巫师和占卜者的独有领地。

——Peter Bernstein，1996

本章开始时的两段引语捕捉到了测量之美的本质：度量指标。Kelvin勋爵的说法是没有度量指标就不能成为科学。Peter Bernstein的说法与风险有关，它在概念上是与安全相关的。Bernstein博士表示，度量指标可以使你不再成为过去的囚徒，更不必依赖占卜者的胡说八道。一般情况下，这正是科学的关键目标。

由于自身的这些原因，度量指标在概念层次上是美丽的。它在实践中又是怎样的呢？具体地说，度量指标在保护个体、公司、国家和全球利益上的应用是怎么样的呢？特别是当它们的利益依赖于信息产业（IT）基础设施的时候。本章的目标是探索应用安全度量指标的美丽之处。首先，我将使用医学领域作为类比。接着，我将转移到本书所涵盖的领域：计算机安全，并通过实例来讨论度量指标。为此，我将分析一些广为人知并且危害严重的安全事件。其中一个例子是预防ATM欺诈这个基本领域。另两个例子则是众所周知的事件，涉及两家单独的上市公司：巴林银行和TJX。

3.1　安全度量指标的类比：健康

医学研究通过度量指标提高了人类健康的科学性和实践性。和安全一样，健康也是无形的。我们可以感觉到它，但不能触摸到它。和安全一样，健康也是为了实现某些缺乏的东西，即由于精神上或身体上的不适而寻求健康监护，就像由于机密性、完整性或可用性不足而寻求安全一样。许多安全专业人员把这些特征（不可触摸和“纠正负面因素”）作为IT安全难以找到一种定量的、严格的、可解析的方法的原因。但是，医学研究却是一个闪亮的反面例子。因此，对医学领域所应用的一些度量指标的观察有助于我们通过例子来讨论安全的度量指标。

让我们从观察大的问题开始，也就是美丽的安全度量指标应该实现什么目标：

1）我的安全状况怎么样？

2）我目前的状况是不是比去年同期有所改善？

3）我是不是花费了恰当数量的金钱、精力和时间？

4）我该如何与同等类型的人进行比较？

5）我可能会遇到什么情况？

第5个问题表示最近可能发生的任何事件（通常是负面的）。

前4个问题最初是由Daniel E.Geer,Jr博士作为对安全度量指标的挑战而提出的。他是一位非常优秀的安全“度量专家”（[1]），他的博士学位是从流行病学专业获得的。在流行病学中，度量指标被认为是公共健康监护研究的基石。第5个问题是我自己添加的。Geer博士表示，他最初是在一家大型的公共金融服务公司的CFO那里听到第1-4个问题的。在回顾这段经历时，他描述说当时人们对这些无法顺口回答的问题颇为不屑。显然，需要提供更多的证据证明安全是一门需要度量指标的学科，才能使它获得应有的尊重。把前面的安全问题映射医学领域后，我们会发现两者几乎没有什么差别。而且，和安全一样，人的健康也是受许多因素影响的复杂函数。这些因素包括：威胁（例如病菌和疾病）、潜在风险（例如基因或环境条件）以及它们的联合影响。和IT安全一样，预防性健康监护的目标是确定并实现一些措施，减少威胁利用一个或多个潜在风险的可能性。和IT安全操作一样，医学的目标也是确认威胁，当一个威胁利用一个潜在风险发动攻击时尽量减少它的影响。

3.1.1　不合理的期待

在医学和IT安全中，前面那些问题是没有办法回答的。用科学术语表示，它们是不规范的（这已经是客气的说法了）。第一个问题是关于定义的。健康或安全的定义是什么呢？如果一个人的指甲边上长了倒刺，是不是就可以认为他是不健康的？或者用类似的说法，如果一台服务器没有打补丁，是不是就可以断定整个公司的IT基础设施就是不安全的？

第二个问题涉及环境。那里怎么会长倒刺，它是不是很麻烦呢？病人的病史或其他体征是不是能够提示什么？他是不是经常咬指甲？如果倒刺长在一个小提琴演奏家的手上，它的影响显然与长在一个与音乐无关的办公人员手上截然不同。为了对一项IT资产中的一个漏洞进行评估，显然应该考虑漏洞的危险程度、该资产的使用情况以及连贯性（或通用性）。

第三个问题是不确定性。倒刺也存在极为微小的导致坏疽、截肢或死亡的概率。或者按类似的说法，一个漏洞也存在因为被攻击而导致灾难性损失或声誉尽损的微小概率。大多数人对倒刺以及相关的风险比较了解，知道怎样正确的治疗。反之，许多高管对IT资产中的漏洞并没有清醒的认识。他们把期待的目光投向CISO和CIO，而后者只会让他们失望。

为什么会这样？答案是影响因素有一部分与文化有关，有一部分是由于缺乏科学的严谨所致。在克服这些影响因素方面，医学研究做得要比IT安全研究好得多。

[1]度量专家这个词并不是现实中存在的，是我和其他人所创建的，用来表示通过执行量化分析来确认需求、模型以及与度量指标相关联的可视化的人。

3.1.2　数据透明性

首先让我们观察文化。医学社区具有强大并且体制化的数据共享机制，对于想学习更多的知识并传播他们的研究结果的研究人员而言，这是非常关键的先决条件。我们可以这样，如果发现了一个可能会导致死亡或患病的紧急健康威胁，将会发生什么情况？调查人员将会被紧急召集起来，对来自不同机构不同专业的专家们展开问询。所涉及的专家包括：主治医师、咨询顾问、护士、看护人、疾病控制预防中心（CDC）的免疫专家、保险公司、药物生产商和医院。他们的发现将会受到媒体和政客的检阅，得到医疗服务人员深入讨论，并被编入医学教材和其他职业教育出版物中，快速、准确地引起大众的注意。

反之，我们可以注意操作ATM网络的银行的行为。20世纪80年代后期，纽约银行发现在ATM收据上打印顾客的完整账号会为窃贼提供帮助，导致银行和顾客的重大经济损失。窃贼可以站在ATM机的黄线边上，通过观察手工输入获取合法顾客的PIN码。然后，窃贼可以使用PIN码和账号（来自被丢弃的打印收据）进行偷窃。许多年以后，非纽约的银行仍然在ATM收据上打印账号，有很多是因为没有意识到这种做法的危险性。至少在1992年的时候，英国还有一家银行因为继续采用这种做法而遭到媒体的指责。虽然公开承认自己的行为缺陷有点颜面无光，但它会带来不可否认的好处，使其他银行能够立即终止这种过时和不安全的做法。

政府和管理机构深深懂得数据共享的价值。美国国土安全部（DHS）要求像金融服务、电信、电力和制造这样的行业建立信息共享和分析中心。从2002年早期加利福尼亚的SB 1386法案开始，州法律要求在顾客的私人被侵害时必须向顾客报告，现在实行这项法律的州已经扩展到38个。这就产生了一个称为丢失数据库（DataLossDB）的珍贵在线数据库，安全研究人员已经开始从中挖掘数据丢失的规模和发生率的度量指标（按照行业、类型和来源分类）。具体地说，就是可以通过根据准确和（相对）完整的真实数据计算产生的度量指标来描述侵害的规模和发展趋势以及内部犯罪和外部犯罪的比例。

3.1.3　合理的度量指标

用于衡量健康合理的医学方法向患者提供了一个合理的指导方针，使他们可以评估自己的健康状况，同时又保持合理的预期。尽管病人会问：“我的健康程度如何？”但他们知道这实际上是一个无法回答的问题，多数医生只会给出一个相对谨慎的回答，例如“目前看来一切正常”。如果病人还想究根问底，医生可能会再加上一些套话，例如“您的生命体征非常正常。您的所有测试结果都是正常的。您并没有超重，但您最好能够戒烟。”他们只是提供了一个总结，列出了一些不包含权重的正面或负面事实。这些事实都直接或间接来自这些度量指标。在有些情况下，如果病人坚持询问，他们会使用像胆固醇和骨质疏松这种专门用来应付门外汉的医学术语。

重要的是，医生不会试图向病人提供一个全面周到的回答。这个列表常常按照器官系统进行组织：心血管、肺、皮肤、神经系统等。每个系统具有相关的专家，病人可以有的放矢，寻求深入的测试和诊断。

同样重要的是，病人觉得这样的答案是可以接受的。和Geer博士所参加会议的CFO不同，他们并不期望快速、明确的结果。

而且，医学领域还发展了令人印象深刻的各种度量指标，值得安全度量专家们学习。

首先，医学研究需要收集“病人历史”、“生命体征”和“基本生物计量”，以便展开与健康有关的讨论。病人历史提供了广泛的基本上下文，一般由一组答案为“是”或“否”的问题所表示。例如：“您是否得过麻疹？”“您的父母或兄弟姐妹是否患有高血压？”对于大多数人而言，医学生命体征包括体温、血压、脉搏和呼吸频率。基本生物计量包括身高、身重和疼痛。对新生儿而言，设计了一组称为阿普加评分的特殊生命体征。这个度量指标是在1952年由小儿麻醉专家Virginia Apgar所发明的。这是一种简单、可重复的方法，可以快速、准确地评估新生儿在出生时的健康状况。新生儿在出生一分钟后和五分钟后根据5个标准进行评估：外观、脉搏、应激反应、活动和呼吸。阿普加评分是为5个标准分别打0分、1分或2分，然后计算总分所得到的。完美的评分是10分。除了如何为每个标准打分的指南之外，还有解释最终得分的指南。超过7的评分被认为是正常的。低于3的评分被认为是非常危险的。

其次，医学研究更侧重于相对度量指标，而不是绝对度量指标。前面所提到的生物计量指标“疼痛”就是一个很好的例子。有趣的是，疼痛是按照等级0～5或0～10进行测量的，其中0表示不疼痛，5或10表示病人曾经感受过的最剧烈疼痛。它被认为是一种良好的做法，使医生允许病人挑选一种需要使用的等级（0～5或0～10）。同样重要的是，这个度量指标是相对于病人的以前体验进行测量的。它并没有绝对指标。由于一个特定的病人无法体验其他人的疼痛，因此这个方法只是一个知觉选项。

再次，医学研究引进了“足够好”的概念。正面的生命体征和生物计量指标对于表示一个人是否健康而言既不是必要条件也不是充分条件。一个具有良好评分的人可能患了病，另一个评分很差的人很可能却是健康的。但是，后面这种情况是不寻常的。按照“足够好”的精神，对生命体征和生物计量指标的测量仍然被看成是“最佳实践”。而且，通过统计分析，医学研究提供了不同层次的信心，能够大致确定一个异常情况发生的可能性。最后，也可能最为重要的是，医学研究人员把他们所制定的指导方针建立在过去几十年所收集的大量病人数据的基础之上。医学生命体征的值已经通过实验确定，并通过分析数据得到证实。这种数据之所以可用的原因是：1）病人乐意接受测量；2）病人同意共享这些测量结果。对自由共享的病人数据的分析允许研究人员通过分布来确定正常值，描述个体值偏离正常值的可能性并解释这种区别。另外，他们可以确定来自不同人群的值，例如新生儿、儿童、青少年、成人和老年人。通过分析还可以确定对正常值的其他影响，例如性别、国籍、种族、最近活动或生活方式以及是否超重等。注意，单独的病人数据绝不会被泄露给医生或其他病人，实际所提供的只是经过分析的度量指标，例如平均值、百分比范围、最高值、最低值和标准差等。

从文化上分析，安全专业人员需要模拟医学专业人员，改变工作方式。安全必须开发一个生命体征系统和通常接受的度量指标，能够被理解并且做到“足够好”。这些度量指标并不需要是明确的或绝对的。它们可以是相对的，有时甚至可以是主观的。它们允许出现假阳性或假阴性，但出现的概率必须非常低，能够被接受，并且是可以测量的。IT安全专业人员不但必须定期测量数据，而且还要共享数据，而不是把它们当做秘密而秘而不宣。如果他们不作改变，像加利福尼亚SB 1386这样的法案就会强制他们这样做。有了这些变化之后，安全研究人员可以用严格的科学来支持他们的分析。只有到了这个时候，他们才能解放自己，增加别人对他们的信任，而不像以前人们眼中的占卜家和算命先生一样。

3.2　安全度量指标的实例

度量指标可以为医学从业人员提供帮助，它为个体或人群健康的量化提供了一个框架，并为如何与非专业人群进行交流提供了指导方针。度量指标可不可以在安全领域发挥同样的作用呢？如前所述，安全领域缺乏自愿的数据共享。但在有些时候，当后果足够可怕时，消息就难以被捂住，接踵而来的调查结果就会被公开。在本节中，我们将分析两个灾难性的安全事故，并讨论有效的安全度量指标计划是如何缓解甚至消除灾难和损失的。

3.2.1　巴林银行：内部侵害

首先让我们观察侵害可能导致的最可怕后果：破产。这个侵害实际上是由一个叫Nick Leeson的人所犯下的一连串错误行为所导致的。这个过程持续了4年，最终导致了巴林银行的破产，并在1995年以1英镑的价格卖给了ING集团。

玩家

巴林银行是英国最古老的商业银行，成立于1762年。它具有非常悠久和光辉的历史，路易斯安那并购案、拿破仑战争、加拿大太平洋铁路的修筑以及伊利运河的开挖等重大历史事件都留下了它的身影。英国政府通过巴林银行清算它在美国以及其他地方的财产，为第二次世界大战期间的战事筹集资金。黛安娜王妃就是巴林家族的一位成员的曾孙女。巴林银行还是伊丽沙白女王的私人理财银行。

Nick Leeson出生于1967年，是一位泥水匠工人的儿子。他的生活轨迹是从穷光蛋到富翁，再从富翁到穷光蛋的过程，很可能还会恢复到小富的状态。他在学校里没有通过最终的数字考试，在没有几门课程合格的情况下离开了学校。尽管如此，他还是设法找到了一个银行职员的工作，辗转了几家银行之后，最后在20世纪90年代初期来到了巴林银行。一开始他只是从事期货和期权的结算工作，但到了巴林银行之后不久，他申请并获准来到远东，帮助解决巴林银行在雅加达的混乱期货和期权结算任务。他的工作成效非常显著。1992年，他被任命为巴林期货（新加坡）有限公司（BSS）的总经理，负责雇用交易员以及期货期权结算工作人员，并在SIMEX（现在的新加坡国际金融交易所）启动一个未来的交易操作。尽管交易并不是他的职责，但他很快通过了必要的考试，开始在SIMEX与他的交易员队伍一起执行交易。结果，在侵害过程时，他扮演了三个角色：总经理、交易员和期货和期权结算的实际领导（因为他的过去经历）。对于任何交易操作而言，职权的分离是一项基本原则。但在BSS，这个原则却是不存在的。巴林银行直到4年以后才发现这个事实，但此时已经为时过晚。

怎么发生的

Leeson和他的交易员队伍有权执行两种类型的交易：外部客户和其他巴林银行业务单位的期货和期权交易；利用SIMEX和日本的大阪证券交易所（OSE）的期货交易价差的价格套利。

按官方的说法，巴林银行（Leeson作为它的指定代表）采纳了一种称为“空头跨式套利”的交易策略。它涉及销售（看空）有些股票、利率、指数或其他基础工具的购入期权和卖出期权（在Leeson的案子中是Nikkei 255指数）。为了便于讨论，假设把一个公开交易股票的一定数量的股数作为基础工具。

如果读者不熟悉期权的概念，可以先了解一些定义。期权有两种类型：卖出期权和购入期权。卖出和购入都是双方（卖方和买方）之间的契约。

卖出期权提供了卖出的权利。卖出期权的购买者可以自己决定是否在未来的某个时间（T）以协议价格（S）售出一定数量的股票（N）。卖出期权的出售者承诺在未来的某个时间（T）以协议价格（S）购买一定数量的股票（N）。对于一个卖出期权，N、S和T的价值对于该卖出期权的购买者和出售者而言都是相同的。期权（Qput）的价格是购买者为履行契约（即期权）支付给出售者的金额。

购入在本质上是卖出的映射。购入期权的购买者可以决定在未来的某个时间以协议价格购买一定数量的股票。购入期权的出售者承诺在未来的一个时间以协议价格出售一定数量的股票。和卖出期权一样，参数N、S和T对于购入期权的购买者和出售者而言具有相同的价值。期权的价格是购入者支付给出售者的金额。

空头跨式套利是一种交易策略，交易员（例如Nick Leeson）出售匹配的卖出和购入期权。注意，多头跨式套利表示交易员购买匹配的卖出和买入期权。对于这两种交易策略，可以创建一个模型，把交易利润（Y）用N、S、T以及当前每股股票价格（X）的函数来表示。由于Nick被授权执行空头跨式套利策略，因此我们详细观察什么是空头跨式套利。

一天，Nick卖出了两个期权：到期时间为T、协议价格为S的N股的卖出和购入期权。Nick由于卖出他的两个期权而从购入者那里得到：

Q=Qcall+Qput

因此，Nick的起始利润相当于Q。稍后从我们的分析中可以看到，这是Nick可以得到的最理想结果。当日期为T+1天，当这两个期权到期时，会出现3种可能的情况：

情况1：X=S（当前股票价格X等于时间T的协议价格S）

如果当前价格和协议价格相同，则两个期权都不会被执行，Nick将不必购买或出售任何股票。他可以保有利润Q，享受胜利的喜悦。

情况2：S＜X（当前股票价格X大于时间T的协议价格S）

在这种情况下，购入期权的持有人（拥有按价格S低价购入股票的权利）将会执行这个期权。卖出期权的持有人（拥有按价格S出售股票的权利）将不会执行这个期权，因为他可以按更高的当前价格出售。因此，Nick必须按价格X购买N股，并按价格S出售N股，这样总体利润相当于：

Q-N*（X-S）

只要N*（X-S）小于Q,Nick仍然是赚钱的。显而易见，如果N或（X-S）非常大，就会导致Nick陷入亏空。

情况3：X＜S（当前股票价格X小于时间T的协议价格S）

在这种情况下，卖出期权的持有人（拥有按价格S高价出售股票的权利）将会执行这个期权。购入期权的持有人（拥有按价格S购入股票的权利）将不会执行这个期权，此外他可以按更低的价格购入股票。因此，Nick必须按价格S购买N股并按X出售N股，这样总体利润相当于：

Q-N*（S-X）

只要N*（S-X）小于Q,Nick仍然能够赚钱。显而易见，如果N或（S-X）非常大，就会导致Nick陷入亏空。

我们可以把利润设置为0，通过解前面所显示的方程式来计算当前股票价格X在时间T的保本值：

0=Q-N*（S-X）当S＞X

0=Q-N*（X-S）当X＞S

产生：

X=S-Q/N当X＜S

X=S+Q/N当X＞S

为了更好地进行总结，空头跨式套利的模型可以用图3-1所示的图表示。这张图显示了y轴所表示的利润根据x轴所反映的当前股票价格发生变化。

图　3-1　显示空头跨式套利模型：利润Y在期权到期后用当前价格X的函数表示

这并不是一张很完善的图。如果市场的动荡不大，这种策略将会产生（有限的）利润。但是如果市场较为动荡，就可能蒙受巨大的损失。如图3-1所示，如果N的值非常大或者X和S的差非常大（价格变动很大），利润就会趋向负值。

差不多就在刚开始时进行交易的时候，Leeson就开始在Nikkei 255指数期货、Nikkei股票期权和日本政府债券上进行了未授权的投机活动。利用他对期货和期权结算部门员工的影响力，他在正确的账户上报告他的授权套利交易的利润，在一个旧的错误账户上隐藏它的未授权活动和损失（账号88888）。这个账户是他以前在雅加达的期货和期权结算项目的复制品。表3-1[1]显示了描述这个情况的一些金融度量指标，所有的值都用百万英镑（GBP）为单位。

直到1995年1月，Leeson的主要问题还是技术不够熟练并且做事肆无忌惮。但在1995年1月17日，坏运气降临到他头上。日本神户大地震导致Nikkei 225平均指数跳水。证券市场的动荡持续了一个多月。谨慎的交易员在这种情况下应该尽量控制自己的损失，但Leeson采取了相反的做法，他开始对Nikkei 225指数的期权采取了积极进取的购入策略，如图3-2所示[2]。

图　3-2　Leeson在危机期间的购入

x轴反映了时间，恰好从1995年1月17日神户地震之前开始，直到巴林银行于1995年2月27日进入接管状态。左边的y轴和带有触角的深灰柱形显示了Nikkei 225在大阪证交所的最高均价、最低均价和收盘均价。在地震之前，Nikkei 225指数的交易价格波动在19 000和19 500之间。在神户地震之后，交易价格猛然下探，然后在17 400和18 800之间大幅波动。在空头跨式套利模型中，这意味着X和S之差非常大。

图3-2中的浅灰柱是以百万美元（$M）为规模绘制的，表示Leeson所交易的巴林银行的多头期权位置。这个规模从最低的大约30亿美元到最高的大约200亿美元，其走势恰好与左边表示Nikkei指数（从16 500到20 000）的y轴相反。每个柱形都标上了数字，表示用百万美元表示的相关值。因此，每个柱形的高度反映了Leeson在这段时期内所承诺的在未来日期以一个指定价格购买的Nikkei指数股数。Leeson在这段时间内的最大承诺购买金额是200亿美元，是巴林银行当时大约6.15亿美元的报告资产的32倍。在空头跨式套利模型中，这意味着非常巨大的N值。

结局是非常的迅速和痛苦，并且很快传播了开来。

当然，本节中的数据和图表所反映的是金融度量指标，而不是安全度量指标。美丽的安全度量指标的一个关键特征多少与金融度量指标存在联系，至少需要对业务的执行进行一些有意义的测量。能不能采用什么安全度量指标，在事情变得无法收拾之前捕捉问题并发出警告呢？我们首先分析哪里出了问题，然后观察一个“如果……怎么样”的场景，即巴林银行如果采用了一个度量指标计划将会怎么样。

哪里出了问题

在回顾前面所描述的事件所处的上下文环境以及它的先后顺序时，我们可以作出下列这些评论：

·Leeson在目标系统的特权上并没有一个清晰定义的角色。事实上，他同时扮演了三个角色：交易员、期货和期权结算管理人和总经理。

·定义、实行、授权和执行政策的职责没有分离。

·Leeson执行期货期权结算和交易功能所需要的访问和授权并没有正式的审核过程，尤其是当他获取对IT应用程序的访问时。

·不存在相关的控制，保证当Leeson的责任和功能发生了变化时进行审查。

适当的安全度量的一个重要角色就是进行监视并对控制的执行进行测量。在巴林银行的例子中，许多关键的控制都不存在。在3.2.2一节中，我们将吸取医学研究上的一些灵感，显示如何使用安全度量指标来检测是否缺乏关键的控制，并对它们的有效性和效率进行监视。

巴林银行：“如果……怎么样”

如前所述，医学研究定义了一组事实，医生可以根据这些事实，对他首次遇到或者以前没什么了解的病人作出评估。第一个步骤是查看病人历史，这也是医生们经常采用的做法。病人历史是一系列的简单问题，提供了病人状态的概况。把话题转回到公司（具体地说，巴林银行），与病人历史相关的适当类比又是什么呢？答案是“安全历史”。这是一系列的高层次问题，提供了一个机构为了保护IT资产的安全所实现控制的基本特征。下面是一些示例问题：

·机构是否具有正式成文的信息安全策略和流程？

·是否要求员工遵循这些策略和流程？

·机构是否具有身份和访问管理流程，支配用户访问和授权的管理？

·是否在IT资产的访问授权人和IT资产的访问人之间实现权责分离？

·当员工的工作或责任发生变动时，是否执行一个正式的审核流程？

·系统和应用程序的活动是否由类似日志文件这样的机制进行指导？

·是否定期对活动日志进行分析？

安全历史的问题数量可以多达数百个。和病人历史不同，一个机构可能需要花费几天甚至几个星期才能搞清安全历史，但它们的原则是相同的。它强调了高层次的问题，目的是提供一个宏观计划，作为未来调查的路标，并最终成为目标性更强的度量指标的集合。

和2008年一样，IT安全市场开始开发安全历史的指导方针。在医学领域，存在大量建议使用的病人历史问题。但在IT安全空间，只有极少数的机构或结构从事这项工作。但是，事情正在发生变化，目前已经出现一些提供某些需求的机构：

·支付卡行业（PCI）发布了一组需求，要求它的成员相信公司将会适当地保护顾客的信用卡数据。

·Santa Fe Group联同BITS创建了金融机构共享评估计划，增强评估公司的IT安全姿态的有效性并提高其效率。

·像Unified Compliance Framework（http://www.unifiedcompliance.com）这样的公司已经制定了规范，用来协调由制度、标准、最佳实践以及其他方式所构成的需求。这些积极的做法很有潜力产生一个层次的一致“安全历史”，与当前能够为医生提供很大帮助的病人历史非常相似。

如果有人向巴林银行提问这些示例（毫无疑问是有针对性的）问题，他们的回答肯定也是振振有词的。但是，我们可以很合理地推测到巴林银行在英国的高级管理层并没有意识到远东地区所进行的BSS操作有多么可怕。事实上，如果他们进行了询问，那么Leeson很可能在填写调查表时撒谎。反映人的所想所说的主观性问题必须得到坚实的事实和数据的支持。因此，和病人历史一样，这种类型的安全历史必须通过度量指标来增强它的说服力。历史只是一些简易的信息，用于引导进一步的深入调查应该从什么地方开始。

考虑下面这个“如果……怎么样”的场景：假设巴林银行决定获取一个共享评估安全评分，会产生什么效果呢？如果巴林银行为了维护与诸如Visa和American Express这样的支付卡公司的友好关系而进行强制的PCI审核又会产生什么效果呢？

显然，巴林银行立即就会发现对交易和期货期权结算IT系统的访问控制非常松懈。审核者可能会发现缺乏中央化的控制。他们可能会发现对员工的授权没有常规的审核，尤其是当员工的工作发生变动时。根据这些发现，巴林银行就会得到一个很低的共享评估分数，这将影响他们的声誉，他们很可能会失去处理支付卡数据的能力，因此将会导致收益的明显损失。

最后两个业务冲击很可能会引起巴林银行高级管理层的注意，也许会驱使他们启动一个项目来处理这个问题。但是，他们应该在这方面进行多少投入，并且应该怎样采取一种现实的目标或成功措施呢？很容易想象，他们会提出下面的问题：

·“我们的竞争对手是怎么做的？”

·“他们的身份和访问管理控制的复杂程度如何？”

和健康监护不同，IT安全同行在这方面并不能提供什么帮助。把个人的医疗数据用于研究会导致当事人存在类似的风险，可能会以几种形式变为现实：更高的医疗保险费用或医疗保险不支付这种疾病，难以找到工作，信用问题，如果他们感染到别人可能会引发诉讼等。但是，尽管存在这种风险，人们还是共享了他们的数据。为什么？一个原因是人们相信他们的数据在被发送给研究人员之前会进行适当的匿名化。为了处理与公司安全相关的担忧，Internet安全中心（CIS）[3]公布了一个跨企业的基准度量指标项目。为了响应这个项目，我的公司PlexLogic发布了一项服务，称为MetricsCenter，可以匿名地收集计算CIS基准度量指标所需要的数据。MetricsCenter技术利用了匿名研究的成果，它在很大程度上受到了医学研究的隐私需求的启发[4]。

当然，和巴林银行在20世纪90年代前期的做法一样，许多公司并没有收集度量指标数据，因此没有什么可报告的。这有点像有些人从来不去看医生一样，显然是不明智的。

结果，巴林银行的管理层无法使用度量指标（其他公司的或自己的）来定义他们的“处理”的规模和范围。由于别无选择（他们需要并期望良好的安全评分），巴林银行必须启动一个内部项目，“治疗”他们的“患病的”的反凭直觉的身份访问管理控制。假设这次他们决定实现处理程序的同时实现一个度量指标计划。

好消息是巴林银行的项目可以极大地从度量指标中获益，并且他们的实现完全是在内部可捕捉资源的控制之下，特别是他们并不担心其他公司收集和共享这些数据。

巴林银行：一些安全度量指标

因此，在这个“如果……怎么样”场景中，我们假设巴林银行对他们的项目在改进身份和访问管理控制上的效果非常满意。我们进一步假设他们已经采取了一些安全度量指标，并忙于收集、分析和报告结果。注意：虽然访问管理上的改进要推进到远在新加坡的BSS业务单位是比较慢的，Leeson很可能还能自由自在地执行他的交易。但他的好日子快到头了。让我们观察一些很可能暴露他的越权行为的基本度量指标：

覆盖率度量指标

这个度量指标对解决方案的实现深入程度以及部署的不一致程度进行量化。下面是一些以巴林银行为实施对象的例子：

·应用程序或基本服务的百分比，例如电子邮件、网站或共享存储，它们的访问是由一个授权系统所管理的。这个值越接近100%，控制就越完全地集中到一个或少数几个管理良好的地方，因此增强了责任性。为了计算这个度量指标，可以比较下面这两个列表：一个是来自授权系统的，另一个是从配置文件和目标应用程序或服务的活动日志编译而来的。这两个列表的重叠百分比就是这个度量指标的测量方法。在Leeson的案子中，这个度量指标将会暴露应用程序所识别的用于授权访问但并不受授权系统正式管理的秘密账户的存在。

·登录账户明确链接到合法的员工或契约人的百分比。如果这个度量指标不是100%，就应该进行仔细检查，并进行最终的调整。无法链接到当前员工或契约人的一大类账户一般都是被解雇的员工，可以肯定有些人对他们的前雇主不会持有积极的态度，并且几乎肯定会有一些“内幕”消息。同样，这个度量指标是通过比较两个列表计算结果产生的：第一个列表来自授权访问控制系统，第二个列表来自公司的人事资源或员工花名册。这两个列表的重叠程度就是这个度量指标的值。这个度量指标可以发现Leeson使用了一位前任员工的账户或一个虚拟账户以获得未授权的访问。

·账户或员工的访问和授权每年、每季度至少正式审核一次或从来不进行审核的百分比。账户或员工从来不进行审核的百分比应该尽可能接近于零。这个度量指标常常来自保存已执行的审核记录的服务票据系统。它的结果是通过把已执行的审核账户数量除以活动账户的总数产生的。Leeson和他的账户很可能从来没有经过审核，因此可能出现最坏的后果。Leeson在他完成新加坡的第一个项目之后就获得了职位提升，但他的访问资格证书从来没有被审核过。和其他度量指标一样，这个指标将会提示Leeson的状态是未经审核的。

·账户组重叠的百分比。在访问控制系统中，定义共享相同的授权的账户组是常见的做法。例如，一个称为期权交易员的组可能被定义为拥有访问交易应用程序所需的授权，另一个称为交易管理的组可能被定义为拥有负责买家、卖家、中介和其他方之间的货币交换所需的授权。期权交易员组和交易管理组之间的组重叠的百分比在理想情况下应该为零。如果位于交集内的所有账户最近（例如，在一个季度内）已经被审核，这个度量指标不太会发生警告。在Leeson的案子中，他的账户将位于这两个组的交集中，并且在超过12个季度的时期内没有经过任何审核。

与测试体温或脉搏相似，这些度量指标都不难理解，并且都不需要复杂的数学计算。另外，它们很可能并不是非常明确的。总之，任何公司在这些度量指标上都很难实现100%的完美，就像一个体温或脉搏并不完全标准的人可以找出正当的理由来解释这种偏离（例如，他们常常跑了十层楼梯）。另外，这些度量指标是从单个时间点所得出的值，它们只提供了一个机构的状态的一个快照。经过经常和反复的计算，这些测量的时间序列可以形成有价值的数据集，可以支持进一步的、可能更复杂的分析。

时间序列的度量指标基于一组根据定期或不定期的时间间隔而获取的值。时间序列反映了系统随时间而发生的动态改变，显示了系统的状态是在改善还是下降以及改善或下降的速度。例如，如果我们每月或每季度收集前面所描述的所有度量指标，那么将会看到什么。在一个像巴林银行这样的机构内部的一个业务单位所得到的度量指标很可能可以预告其他业务单位展开这些新控制的期望速度。也许，我们甚至可以推算需要多长时间能够发现和终止Leeson对期货和期权结算的非授权访问。

投资度量指标

类似，如果跟踪与一个用于改善访问控制的计划相关的成本，对于前面所描述的任何（或所有）度量指标，都可以得出每单位改进的成本。如果开销率是个常量，并且一个度量指标逼近它的目标（例如100%）的速度开始慢下来，每单位改进的成本就会上升。使用预报模型，可以推算实现其他控制目标（例如95%、96%等）时将耗费的成本。当然，在Leeson的案子中，很容易（凭借事后诸葛）实现只需要很少的开销就能使BSS的身份和访问管理系统得到有效的控制。

处理效果度量指标

为了得出处理效果度量指标，可以运行一个时间序列度量指针，对“处理”前后的效果进行比较。我们可以使用基本的统计分析进行无效假设的测试，在这个例子中就是指任何被检测到的变化仅仅是由于几率的原因，“处理”没有产生任何区别。

例如，假设在启动针对一个指定业务单位的访问控制项目之前，巴林银行决定收集一些与访问相关的安全事件的度量指标。像安全事件之间的平均时间（MTBSI）或修补安全事件的平均时间（MTTRSI）这样的度量指标来显示被观察的访问安全事件的频率和影响。这些基本值（时间序列“之前”）可以与部署了访问控制之后（时间序列之后））的相同指标进行比较。是不是有所改进呢？改进的幅度大不大（还是纯粹由于几率所致）？统计上的显著结果将使我们确信这些改进是由于部署了访问控制所致。

通过时间序列“之前”和“之后”的数据，巴林银行就得到了铁的事实，可以用它来评估处理方案的有效性。另外，它可以向审核者提供坚实的数据，因此可以通过一个初始的审核或者清除一个负面的发现。随着时间的过去，将会收集越来越多的结果，因此通过统计所得到的信心会进一步增强，对处理效果的量化也会进一步的完善。最后（由于数据的缺乏，这可以仅仅是个猜测），他们可能会避免这个最终导致巴林银行破产的重大访问控制事件。

收集测试数据、计算度量指标并对结果进行分析是没有问题的，但是如果不在正确的时间把这些发现与正确的对象进行交流，使后者可以做出更好的决策，那么这些活动就没有什么价值。对于巴林银行而言，很容易断定高层管理层很可能没有阅读与访问控制事件相关的详细报告，或者说并不了解我们刚刚所描述的详细技术度量指标。他们所看到的很可能只是一个包含基本区域列表的圆盘，上面用一些颜色代码标记了各个区域，例如用红色表示差，用黄色表示警告，用绿色表示没问题。负责每个区域的经理将根据他们对我刚刚所描述的度量指标的理解设置颜色代码。

当经理们负责跟踪和报告安全漏洞时，他们面临很艰险的选择。报告问题是一把双刃剑，它既可以带来潜在的收益，因为上级可能会调拨专款解决这个问题。它也可能对声誉造成损害，因为上级可能会追究为什么会发生这种问题。这种声誉损害实际上类似一些（但不是全部）健康监护的情况。使用度量指标可以避免这种两难的局面，因为它要求经理与他们的上司共享信息，上司可以通过与其他经理进行比较，独立地评估经理们的工作实绩。具有讽刺意味的是，上司可能会拒绝与部门或公司之外的人共享这些信息，因为他们害怕被拿来与同一个市场系统的同级人物进行比较。

当然，对于BSS业务单位或远东公司而言，Nick Leeson的账户以及在诸如前面所描述的度量指标上的评分至少得用黄色表示。如果长时间维持黄色而没有改进，将会引发领导层进行某种类型的调查，除非有理由说明Leeson的账户已经进行了审核并证明没有发现什么问题。

可以确定，任何类型的审核都可以震慑或终止Leeson的未授权活动。另外，这样的审核并不需要对复杂的金融衍生品、套利或高层的数字模型有深入的了解。

[1]来源：银行监管调查委员会对巴林银行破产情况的报告，由英国下议院和皇家文书局所要求，1995年。

[2]参见http://www.asiafunclub.com/japan/finance/stock_market/nikkei_1995.shtml以及Zang Peter G的《巴林银行破产和金融衍生品》一文，发表于1995年的《世界科学》，第137页。

[3]详见http://www.cisecurity.org。

[4]详见http://www.plexlogic.com and http://www.metricscenter.org。

3.2.2　TJX：外部侵害

在本章的第二个安全度量指标实例中，让我们观察有史以来（截止到2008年）最大的支付卡诈骗案件。这是一个最近才发生的案子。事实上，在本章写作之时（2008年10月）与这个案子有关的事件和信息仍在不断地披露中。由于这个案子离现在太近，因此很难断言哪些事情已经被证实、哪些事情还没有被证实。下面的有些描述来源于司法部门未经证实的指控以及与这个案子密切相关的人们的陈述。但是，这并不会减少我所总结出的这个教训的价值，因为我通过这个案子说明了安全度量指标的价值。

侵害行为相信是从2005年1月在明尼苏达州靠近圣保罗市的一家Marshalls折扣服装店外的一个停车场开始的。侵害行为持续了18个月，直到TJX于2006年12月采取了行动。实际上，并不是TJX首先检测到信用卡和其他顾客的私人数据被从它们的网络上被大量泄露的。信用卡持有人和发行银行早在2005年11月就开始发现在他们的信用卡账户上所发生的奇怪交易。直到2006年12月，TJX才对此引起警觉，并雇用了审计专家展开调查。尽管TJX并没有因为此次侵害行为而破产，但公司为了彻底平息这个事件可能被迫在接下来的五年里花费10亿美元，包括聘请的顾问费用、IT安全的升级、法律费用以及向支付卡公司和银行的潜在赔付支出。受影响的个人以及与TJX有业务往来的合伙伙伴所涉及的开销需要很长时间才能确定。

玩家

TJX公司2007年的营收超过170亿美元，是最大的平价服装和家居时尚零售连锁店。这家公司拥有8个子公司、2500家商店和13万名员工。TJX当前在美国的子公司包括T.J.、Maxx、Marshalls、HomeGood和A.J.Wright。

2008年夏天，有11个人被指控不仅盗窃了T.J.Maxx和Marshalls，而且还涉及另8家美国零售商：Office Max、Barnes＆Noble、Sports Authority、Boston Market、DSW Shoe Warehouse、Dave＆Buster's、Forever 21和B.J.'s Wholesale Club。其中10人已经被拘禁，另1人在逃。被指控的主犯是Albert Gonzalez，27岁，佛罗里达州迈阿密人。其他罪犯分别来自爱沙尼亚、乌克兰、白俄罗斯和美国。

当Gonzalez被捕时，他已经拥有了一支格洛克27手枪、一辆2006款宝马车、位于佛罗里达的一座公寓以及165万美元的现金。他高中毕业、单身，与父母生活在一起。2003年，Gonzalez在新泽西州因为欺诈指控而被捕，后来被美国特勤局雇用，协助调查一起重大的计算机黑客事件。2004年，他被公认为协助开设了一项留言板操作，作为购买和出售信用卡号码以及其他个人数据的中介。随后，他被特勤局指派参与TJX案子。他的任务是充当线人，把他从黑客那里所获得的消息透露给特勤局。但是，现在特勤局确信他是个双面线人，同时还把他从特勤局所获悉的消息透露给黑客。据称，Gonzalez在特勤局的顾问工作使他每个月可以领取6000美元的薪水。

怎么发生的

首先让我们看一下事件是如何暴露于公众视野之中的。下面的时间线是波士顿环球报在2007年10月25日发布的（[1]）：

2007年1月17日：TJX公司声称它的计算机系统遭到未授权的入侵，可能导致顾客的贷记卡和借记卡数据被盗用，所涉及的数据最早包括2003年的。公司表示它确认并没有对数据使用错误。

2007年1月18日：信用卡公司、银行和顾客开始报告存储在TJX系统中的贷记卡和借记卡账号被欺诈使用。有几千张卡被取消。

2007年2月21日：TJX报告黑客可能早在2005年就获取了对它的计算机的控制，比之前所想的早了一年。

2007年3月28日：TJX公布被黑客所盗用的贷记卡和借记卡号码的数量为4570万，但它表示其中有75%已经过期或者数据被屏蔽。

2007年7月：佛罗里达警方从几个人的认罪答辩中得知他们使用来自TJX的信用卡号码，用代金卡和电子消费券从Wal-Mart和Sam's Club购买了价值800万美元的商品。

2007年8月14日，在一份公司整理的档案中，TJX推算由于这次数据侵害所导致的总体支出为2亿5600万美元。

2007年9月21日，TJX与受侵害影响的顾客达成了一个临时和解协议，向受害人提供公司代金券，并计划举行为期三天的“顾客满意”销售。这个和解协议后来进行了修订，受害人可以选择领取代金券或现金。

2007年9月25日：加拿大保密部门指责TJX没有采取有效的安全措施保护顾客的信息。

2007年10月23日：在银行向TJX所指控的一个案子的法庭档案中显示受盗窃事件影响的账号总数高达9400万。

这条时间线显示了此次侵害行为所造成的影响不断地上升，不管是受影响的用户数量还是随之发生的诉讼（为了维护自己的权益）。2007年10月的时候，犯罪分子还没有被捕获，但是已经在美国的几个州和另外几个国家里展开了涉及多家执行机构的积极调查。直到几乎一年之后的2008年8月，才公布了罪犯已被逮捕以及相关的犯罪事实。

下面是一条从技术角度所看到的时间线：

1999年：无线LAN诞生：IEEE 802.11b完成并被批准。

2001年：对无线通信的WEP保护的劫持和破解被证实存在。

2002年4月：IEEE 802.1X批准使用可扩展认证协议（EAP）处理端口层次的安全。这个协议工作于RADIUS服务器，已经得到了广泛的商业用途。它的目标是对有线网络端口进行标准化，但被发现同样适用于无线网络。

2002年7月：IEEE 802.11b安全脆弱性被广泛公布，并附带详细的获取企业级的强度的建议。

2004年6月：IEEE 802.11i被批准为无线IEEE 802.11标准的修正案，以提供安全机制的标准规范，用于取代以前的安全规范。

2005年6月：TJX仍然在零售商店中广泛地使用WEP保护。

现在，司法部门宣称Gonzalez和其他犯罪分子使用了一种流行的称为驾驶攻击（wardriving）的低成本技术，用于发现开放的无线网络。驾驶攻击就是在配备了PC和优质天线的汽车里对公共区域进行巡查。天线可以发现无线网络。PC运行无线客户软件，如果该网络具有开放的访问就试图对它进行连接。如果这个网络实现了某种形式的访问安全，就记录被传输的数据包流。如果它的访问安全是脆弱的，就可以通过适当的软件对数据包流进行分析，提取入侵需要使用的所有信息。一旦成功侵入，黑客就可以访问该网络连接的顾客数据库，开始收集信用卡号码和其他个人数据。

为了理解它的详细工作原理，让我们观察与无线网络有关的一些关键事实。我们还将提供“一致性良好行为”，它们是由不同的行业小组和标准化组织所开发的建议：

·IEEE 802.11b标准指定了一个服务集标识符（SSID），用于区分无线接入点（独立的网络）。例如，一家称为Good Night’s Rest的宾馆为了方便起见可能把它的WAP命名为GoodNR。默认模式下的WAP操作每隔几秒广播这个SSID，称为“Beacon帧”。这种做法使授权用户很容易发现并连接到网络，而且它同样为非授权用户提供了方便。

一致性良好行为：公司应该把所有的SSID的值设置为比较含糊。像“TJX-Miami”这样的设置并不是很好的选择。另外，最好关掉SSID广播。

·为了对希望连接到网络的用户进行认证，IEEE 802b指定了一个称为无线等效加密（WEP）的协议。大多数WAP配备了一个默认设置，用于开放式的认证，意味着WEP是被禁用的，因此对希望进行连接的客户不会进行认证。

一致性良好行为：公司应该修改默认设置进行认证，不允许所有客户的开放式访问。

·当WEP被启用时，每个单独的客户用一个密钥进行配置，由它的相关WAP所共享。想要访问网络的客户首先向WAP发送一个关联请求，WAP对这个请求作出响应，向客户发送一个包含挑战文本的字符串。然后客户采用一种称为RC4的算法对挑战文本进行加密并把它发送回WAP。WAP根据客户是否正确地对挑战文本进行了加密而决定是否通过客户的认证。

一致性良好行为：WEP的密钥管理是一个噩梦。为了修改共享密钥，每个WAP和客户的配置必须进行更新。

·WEP协议允许未授权的听众捕捉认证方程式的3个变量的其中2个：挑战文本和RC4加密的响应。只要有足够的样本，黑客可以使用嵌入到免费下载的软件中的广为人知的算法破解共享密钥。

一致性良好行为：WEP并没有对无线网络提供充分的保护。实现强大的无线安全的行为包括：

—关闭WEP

—选择含糊的SSID

—采用路由器和防火墙隔离无线子网

—使用802.1X进行密钥管理

显然，和其他遭到侵害的零售商一样，TJX并没有遵循早在2002年7月就已经颁布的指导方针。黑客们利用了这个事实，获取了对公司网络的访问。结果，他们可以通过商店的网络获得顾客的信息，包括通过无线传输的员工资格证书。黑客们使用员工资格证书，访问商店的服务器以及TJX位于马萨诸塞州总部存储了大量的顾客数据的服务器。

哪里出错了

与由于访问管理过程的松懈而导致内部侵害行为的巴林银行的遭遇不同，TJX的侵害行为是由外部攻击者利用网络和系统管理过程的松懈而发起的。有关的调查仍在进行之中，据相关人士透露，外部攻击者发现了使用默认配置的WAP、使用了通俗易懂的SSID和开放式访问的WAP以及被配置为使用WEP的WAP。这些WAP是黑客们伸手可及的果实，他们只需要难以置信的微小投资就可以获得数量惊人的有价值数据。

因此，站在事后的角度，TJX到底哪里做错了？技术上的松懈是很容易列出的：

·不充分的网络配置控制

·不充分的网络使用监管

·不充分的服务器配置和使用监视

·不充分的数据保护，尤其是数据的外流

当然，在更深层次上，技术上的失败是由于管理上的错位所致：

·忽视或不关心基本的安全行为，例如密码保护；

·忽视无线网络的操作，没有遵循行业警告，并且不关注与常见漏洞有关的新闻；

·缺乏统一的策略，也缺乏与远程网站的通信，未能获取推荐的策略；

·在报告出现了问题时拒绝寻找问题的证据；

·在所有时间内缺乏对安全的基本关注，包括安装、员工培训以及接下来的运营期间。这些时期都应该进行监管。

无线标准委员会和生产商也对这个问题负有一定的责任，但这个问题过于广泛，无法在这篇文章内深入讨论。

说明

并不仅仅是TJX应该受到指责。其他九家零售公司被相同的窃贼利用相同的黑客技巧所入侵，也是利用了他们的IT系统的相同安全漏洞。

TJX：“如果……怎么样”

如果TJX使用了有效的感应系统，能够持续监视数以百万计的交易以及在他们的IT基础设施中存储和传输的高达万亿字节（很可能达到）的数据，情况又会怎么样呢？如果他们能够定期对遥测数据（没人可以直接理解）进行汇总，并把它包含在一些度量指标集之中，覆盖聚合标准的某个区域，以便进行定期的审查，情况又会怎么样呢？医学研究已经产生了许多有效的传感系统，并产生大量的数据，同时可供研究人员和医生所使用。这当然是可能的。

在医学领域，成功创建“优质传感系统（包括数据共享）”的关键是直接与病人打交道的医生需要密切与研究人员进行协作，而后者又需要与生产这些传感系统的厂商进行协作。这样，当一个医学诊断工具投入使用后，它不仅可以感应数据，而且可以对它进行汇总，提供有关如何使用这些结果的特定指导方针。

例如，我们可以考虑执行骨密度或DEXA扫描用于诊断骨质疏松症的设备。首先，生产商提供相关的信息，说明这个设备用来做什么？怎么操作？应该在什么时候使用以及如何对结果进行分析。另外，医学研究人员也提供各种信息，例如骨质疏松症的风险因素、各种治疗方法（例如节食、敷药等）、当前的研究（包括医学试验）、支持组织、参考资料以及其他产品。DEXA扫描结果常常用显示病人骨骼的颜色图片表示，上面的颜色表示骨密度。这些构成了一个完整的数据包。

根据安全产品的当前状态，人们可以获取软件以及描述其相关技术和如何操作的手册。包含了推荐用法和事件重要性的论文可以作为承载诊断和解释的主要载体。对于许多安全厂商（以及它们的投资人）而言，这却是他们对产品最不关心的部分。事实上，这个领域的系统管理员也很少阅读和理解这些文档。位于传感系统或软件平台内部的专利技术往往是终端用户完全无法理解的，这些技术使自己蒙上一层神秘的面纱，无形之中抬高了自己的身价。特别是当新产品刚出来的时候，里面常常包含了大量革新的技术。一般而言，安全厂商缺乏发布完整解决方案的专业知识，因此在他们的产品发布和用户能够有效地使用产品之间留下了一个落差。

安全度量指标具有消除其中一些落差的潜力。度量指标通过提供定义，指定原始（传感系统）数据的来源以及如何解释结果，显著地增加现有安全管理产品的价值。SecurityMetrics.org和MetricsCenter.org联合倡议的Security Metrics Catalog项目的目标就是提供一个中央化的、可搜索的度量指标定义的仓库（[2]）。

下面让我们讨论一些可能对TJX有用的安全度量指标。

TJX：一些安全度量指标

和其他计算机问题不同，数据受侵害的问题向我们提供了独特的机会观察一些公共的度量指标，包括全球级别和本地级别。全球级别的度量指标蕴含了整个行业的经验，而本地级别的度量指标体现了单独企业的经验。

全球级度量指标。全球级度量指标需要全球的数据。加利福尼亚的SB 1386强制法案以及类似的法律要求从2003年7月起报告影响个人私密信息的公共侵害。由一组志愿者所组成的开放软件基金会使用媒体报道作为他们的主要信息来源，追踪公开报道的安全事件。他们的结果以数据丢失数据库（DataLossDB）仓库的形式出现在ttrition网站上（[3]）。这个数据库实际上只是一个逗号分隔的文件，很容易用大多数统计软件进行分析。2008年7月15日，开放安全基金会从Attrition.org那里接手了为DataLossDB提供主机服务的责任。DataLossDB将成为一个由社区驱动的项目，它的新地址是http://datalossdb.org。

我们可以使用DataLossDB作为来源，得到一个简单的度量指标“每季度事件数量”，并生成如图3-3所示的时间序列图。

图　3-3　强制报告第一年内的数据泄露事情发生率

在过去的几年中，强制报告数据泄露的范围和规模在2003年中期仅包括加利福尼亚。在过去的五年中，实行这个法规的州数量不断增加中。显然，被报告的数据泄露数量的趋势是在不断增加中，但并没有迅速膨胀。毫无疑问，2005年间有一些早期增长归因于一些新的强制法规。以前不想泄露这类事件的人们现在因为法律的规定而不得不这样做。在图3-4中，我们将“放大”原图，以观察每月的数据，从2005年开始。

每月数据显示了相当程度的波动性。如果我们画一条直线（使用最小正方形模型）进行观察，可以看到趋势是略微增长。我们可以使用这样的直线对将来进行投影，当然不能追求很精确。图中的度量指标（虚线的斜率和截距）清晰地显示了事件的频率是在不断地增加，因此很容易得出结论，就是情况变得越来越糟糕，而不是越来越好。

TJX的侵害是由外贼所发起的。之前人们广泛相信内部侵害的增长速度要远远高于外部侵害。DataLossDB数据允许我们用真实的事实和数据调查这个问题。由于DataLossDB在每个侵害事件上都加上了“内部”或“外部”的标签，因此我们可以进行新的测量，它与上一个图相似，但是把已发生的事件分解为两个子集。这个计算产生了图3-5的图，也是从2005年开始。

图　3-4　最近几年的数据泄露发生率

图　3-5　数据泄露事件的分解

实心的深灰色圆圈表示内部侵害的数量，实心的淡灰色圆圈表示外部侵害的数量。黑色的圆环只是复制了两者的总和，它和前一张趋势图一样。深灰、淡灰和黑色虚线分别符合外部、内部和总侵害频率。很容易看出，外部侵害的数量不断增加中，而内部侵害的数量基本是一条水平的直线。因此，总侵害次数的增加实际上完全是由于外部侵害的增加所致，至少根据DataLossDB的数据所得出的结论是这样的。

侵害的另一个因素是它所造成的后果。每次事件被盗用的数据量可以深入到什么程度呢？Attrition志愿者对每次侵害的记录很好地提供了侵害后果的测量，即受侵害影响的总数。这个值的名称是TotalAffected。当然，对于大多数事件而言，这只是遭到侵害的公司能够提供的最接近估计，但总比没有要好得多。另外，我们也不需要追求特别的精确。因此，图3-6用箱线图反映了一些“分布”度量指标，TotalAffected由log_10y-axis来表示，它对从2005开始直到2008年8月11日为止每一年的侵害总数进行了缩放。事实上，由于本书写作时2008年还没有结束，因此有人觉得不该把2008年放在这张图中。我们的理由是最近的年份总是最有价值的。但是需要注意，这一年的数据实际上只反映了七个半月。

图　3-6　数据侵害的严重性比较

中位数是log_10（TotalAffected）经过半年的观察后的值，它可能偏高或偏低。上四分位数和下四分位数分别近似Q1和Q3的观察。上边缘和下边缘涵盖了围绕两个分布的中位数的95%的置信区间的值。在这个例子中，一个分布覆盖了一年的报告侵害数，因此图3-6显示了4个分布，分别表示2005年、2006年、2007年和2008年（只有7.5个月）。在所有的分布中，高度的重叠表示每次侵害受影响的个体数量根据两个数量级而变化（1og_10垂直轴上的上边缘和下边缘），从低端的5 000到高端的100 000。

确定TotalAffected值与发现一个侵害行为所需时间的灵敏度是非常有趣的。在TJX的案子中，这个时间的长度大约是18个月。根据DataLossDB的数据，TJX侵害的TotalAffected值是9400万。注意图中标记为“异常值”的点就表示这个TJX侵害[4]。遗憾的是，DataLossDB并没有追踪表示发现侵害所需时间的值。

本地级度量指标。在离开TJX的话题之前，我们非常简单地讨论一下值得进行跟踪的本地度量指标，重点是在内部操作上。也许对这些度量指标的定期审查可以激励TJX的IT安全操作检测并修复一些导致我们所说的受到侵害的漏洞。

在TJX的IT基础设施内部，漏洞、补丁管理和配置管理系统都是积极的安全系统的例子，它们的目标是发现并修补弱点，不给黑客利用它们的机会。这些系统在执行任务时会创建大量的数据，可以用来得出一些度量指标，能够体现出它们实现目标的程度。下面是两种类型的度量指标例子：

扫描覆盖率

公司网络实际上被审核了多少？一般情况下，这个度量指标用一个百分比表示，并且常常被分解为地点、所属业务单位、设备类型、漏洞类型或严重性。显然，它的目标是尽可能地实现100%，即完全覆盖。

看上去，TJX的WAP覆盖率度量指标是完全缺乏的。TJX忽略了对它们的所有无线接入点的配置审核。

公司可以使用的各种工具也会使监视复杂化。由于专业化的缘故，安全管理产品常常只覆盖了一个类型的目标，例如路由器、WAP、服务器甚至是运行某个特定操作系统的服务器。2002年，无线接入点常常在企业扫描人员在它们的配置中发现漏洞之前就推向了市场。TJX很可能就是这种落差的受害者。当然，这并不是说它们不该知道得更多。

修补平均时间（MTTR）

在IT基础设施中，消除一个已经被发现（按照广泛的定义）的缺陷需要花费多长时间（按平均算）呢？这个度量指标用某些时间单位表示：小时、天、周等。一般而言，这个度量指标的值越低，黑客能够利用这个漏洞的时间就越短。

尽管这个度量指针显然很有价值，可以作为风险的主要指示标志，但它需要几个前提条件。首先，需要安装传感系统，检测像漏洞、不良的配置、未修补的软件这样的缺陷。其次，当一个漏洞被发现时，必须保存记录，跟踪它被发现的时间以及它被有效修补的时间。我们不清楚TJX是否存在任何检测已部署的无线接入点的机制。就算有，它们的配置也肯定大有问题。因此，它们从来不曾被修补，所以MTTR相当于无限长。

如果无法防止一个侵害行为，接着能够采用的最好策略就尽快检测到它。TJX花了18个月，直到数十万张信用卡号码出现疑问时才发现问题。有什么样的度量指标可以帮助它们呢？

盗窃信用卡号码的窃贼留下了大量的作案痕迹：

在对外的网络流量中的敏感数据（例如信用卡号码或SSN）

这是很容易检测到的，通过现在可用的称为数据使用监视器的商业工具可以在几分钟之内就阻断它们。

无法解释的账户活动

黑客从授权用户那边截获账户资格证书之后，就会登录到账户、搜索数据库、提取数据、创建文件并执行文件传输，把他们所窃取的果实转移到他们所控制的服务器。所有这些活动都可以被数据库管理工具所检测，应用程序日志常常也可以记录这些活动。

对数据使用监视器、数据库管理系统和应用程序日志所收集的数据进行聚合量化的度量指标将会反映黑客为了列集和导出他们所盗窃的材料所需要的活动量级。这些数量在任何时间序列中都会显得特别突出。

[1]详见http://www.boston.com/business/globe/articles/2007/10/25/tjx_timeline。

[2]详见http://www.securitymetrics.org和http://www.metricscenter.org。

[3]详见http://datalossdb.com以及DataLossDB数据库。

[4]log_10（94 000 000）≈7.97。

3.2.3　其他公共数据来源

最后，值得注意的是公共威胁信息的其他公共数据来源正在不断增加。这是个很大的进展，不应该被遗忘：

Symantec DeepSight威胁管理服务[1]

通过捕捉和处理来自路由器、防火墙、IDS和其他设备的日志数据，提供了全球安全威胁的全景视图。它提供了有限的度量指标，通过地址、严重性和类型等进行聚合。例如，http://eval.symantec.com/mktginfo/enterprise/white_papers/b-whitepaper_internet_security_threat_report_xiii_04-2008.en-us.pdf提供了2008年7月的一个示例报告。

iDefense Labs（http://labs.idefense.com/）

VeriSign的一个附属机构。iDefense Labs监视威胁和黑客活动，并把它的分析结果报告给订阅者和公众。

SANS Internet Storm Center（http://isc.sans.org/）

这是个免费和开放的服务，使用一个称为DShield的平台，允许防火墙的用户共享入侵信息。Internet Storm网站提取简单的度量指标，并公布确认了被攻击最多的端口、最大的攻击来源的报告以及国家报告等。

其他感兴趣的来源还包括：[2]

·http://www.gocsi.com提供了行业调查的结果。

·http://antiphishing.org提供了与钓鱼、恶意软件和数据盗窃有关的数据和统计数字。

·http://nvd.nist.gov提供了与漏洞有关的数据和统计数字。

·http://www.commtouch.com/Site/ResearchLab/statistics.asp for spam data and statistics

·http://www.postini.com/stats提供了与垃圾邮件有关的数据和统计数字。

·Symantec的Internet安全威胁报告（ISTR），它是人工发布的。

·http://www.webroot.com提供了与间谍软件有关的数据和统计数字。

·http://www.counterpane.com for polity violations与友好违规（例如IDS、IPS等）有关的内容。

·http://www.qualys.com/research/rnd/vulnlaws对漏洞的流行、持续和半衰期进行了有趣的分析。

[1]详见http://www.symantec.com/business/services/overview.jsp?pcid=hosted_services＆pvid=deepsight_early_warning_services。

[2]非常感谢D.E.Geer,Jr博士，他提供了其中许多资源的链接。

3.3　小结

安全度量指标是把IT安全转变为科学而不是艺术的关键前提条件。我在本章中所讨论的主要思路可以归纳为：

度量可以显示差别

度量指标可以更好地引起警觉并帮助作出更好的决策。它们通过把一个结构应用于复杂情况的分析，可以检测出被破坏的进程或异常的活动。当执行官和经理了解这方面的知识时，他们通常可以确认一个解决方案，并在它实施时对它的效果进行监督。在巴林银行和TJX的例子中，只要事先存在一个度量程序提供关键的透明性，就可以显著地避免或缓解严重的后果。

需要量体裁衣

度量指标必须处理关键利益的情况，并反映与它们所测量的情况有关的高度领域知识。在讨论“安全度量指标实例”时，我们花费了大量的时间讨论业务环境，显示了度量指标通过帮助避免灾难而显示它们的美丽。

种瓜得瓜，种豆得豆

如果公司并不共享与它们的安全成功或失败相关的更多数据，IT安全管理将注定成为像占卜家或巫师这样的迷信，缺乏可信度。在讨论“安全度量指标实例”时，事实上我们可以用来计算和展示内部度量指标的数据非常少。由志愿者所发起的DataLossDB项目很好地填补了这个空白。但是，如果对应的投资没有增加，那么我们无法期待它的范围和规模会进一步的扩大。

另一方面，如果存在安全数据共享文化，IT安全管理学科将会得到成长，成为一门科学，并产生大量的度量指标，帮助决策制定者在作为日益增长的数字世界的基础设施中进行投资。

安全度量是跨学科的追求

从其他学科（例如医学）中可以学到很多东西。安全度量专家必须熟悉IT安全、数字和统计学、量化信息的可视化、可伸缩软件的实现以及度量指标将服务的业务。

安全度量指标还处于婴儿期，按我的想法它还将蹒跚多年。本章描述了我认为要想让它成长所需要的相关内容。在本章临近结束时，我们用一两句话总结本章的内容。首先，我们关注一门更为成熟并与IT安全具有许多相似之处的学科（医学）的相对成功，然后把我们所学到的东西应用到一些值得注意的IT安全失败实例。

第4章　安全漏洞的地下经济

来自NetCraft公司的最新统计数字表明，目前Internet上共有185，497，213个网站。尽管网站的绝对数量由于最近的经济危机有所下滑，但Internet在2008年中期每天大约增加130，000个网站！全球Internet用户数量预计很快将突破5亿。Internet正迅速成为现代经济中最引人注目的市场之一。

毫不奇怪，就像现实世界一样，Internet也正在培育最大的地下经济之一。

正如我们所推测的那样，地下网络产业只有一个主要目标，那就是赚钱。这个经济领域的活动者们使出浑身解数，动用了大量的数字武器（包括流氓软件、肉鸡和垃圾邮件等）帮助他们实现这个目标。

与绝大多数场合的行为都受到法律和规定制约的现实世界不同，无论从哪点看，支配数字世界的法律并没有良好的定义，实施也不甚得力。结果，地下网络产业不断繁荣。近几年来，网络攻击逐渐从临时起意的个人攻击行为发展成为大规模的、有组织的犯罪。2007年的CSI/FBI研究提供了下面这些统计数字：

·每年由于计算机犯罪而造成的经济损失从2004年的$168 000增加到2006年的$350 424。这个数字是2004年以来三年的新高。

·金融欺诈首次超越病毒攻击，成为造成经济损失的最大的网络犯罪原因。

毫无疑问，地下网络产业已经成为遍及全球的繁荣经济，热衷于此道的活动者可以从他们的活动和投资中获得可观的收益。由于这个市场的组织性日益加强并且变得越来越成熟，这使它朝着自动化和优化的方向发展，并最终从犯罪活动中获取更高的收益。

那么，这种地下经济是怎么运作的呢？各个活动者是如何相互合作进行非法活动的呢？本章展示了我对这些话题进行调查后所作出的高层次的结果。

在本章的最后，我将提出几个建议，可以瓦解地下网络或者缓解它的破坏性效果。但是，我并不是专业执法人员，因此寻找解决方案并不是我应该扮演的角色。反之，我希望能够引发公开的讨论，使整个社区行动起来，共同遏制地下网络经济的上升势头。

4.1　地下网络的组成和基础设施

与听上去的感觉相反，地下网络是一个相当繁盛的社区，每一分钟都在发生各种类型的合作和交易。这个社区的成员很少单独行事，不同的成员交换财富（例如数据和恶意软件）以实现双赢或者缩短发动攻击的时间是极为常见的做法。

地下网络由各种各样的活动者所组成，下面对它们进行了粗略的概括：

恶意软件生产者

当前所售卖的许多恶意软件具有很好的产品质量：功能强大、易于使用、效果明显。专业的恶意软件作者所采用的产品周期与合法的软件生产者所采用的产品周期相似，包括测试、发布以及经常的更新。

资源商

这些活动者通过出售计算资源或人力资源赚取利润。计算资源常常来自于受感染计算机所变成的肉鸡，它可以远程执行特定的命令，并作为攻击的一部分。人力资源表示实际的黑客，他们分布于全球的各个角落，等待活动的机会。资源商的存在取决于大量存在的肉鸡的利用能力，这样他们就有动力寻找更多的肉鸡资源。他们的主要使命是创建、维护和扩张肉鸡。

信息商

信息商通过出售有价值的信息而盈利，例如顾客数据、银行账户和安全漏洞。因此，他们的主要目标是收集更多的这类信息。有时候，信息商也是恶意软件产品的顾客，花钱购买窃取信息的恶意软件。2008年2月，安全公司Finjan报告有一个包含了超过8700个FTP账户信息的数据库正在挂牌出售。如果这些信息被恶意使用，可能会导致信任领域的大量盗用事件。具有有价值的信用卡信息的人被称为“卡人”。

犯罪分子、诈骗者和攻击发动者

它们是地下经济的最终消费者。他们花钱购买资源、恶意软件和信息，并发动各种攻击，例如金融欺诈、分布式拒绝服务（DDoS）以及其他犯罪行为。

出纳员

地下网络中的这个活动者持有合法的银行账户，但是用于向诈骗者提供服务，接受和流通来自这些账户的资金。出纳员的任务是支出现金，把钱发送给诈骗者（常常通过Western Union）。出纳员一般会收到可观的经济回报。

根据上面的描述，这些类型的地下网络犯罪分子常常组成了一种连锁关系，每个活动者是其他活动者的客户或供应商。另外，一个人常常可以扮演多种角色：恶意软件生产者可以出售通过发布恶意软件所获取的有价值信息，资源商可以使用恶意软件，使肉鸡一直可以被访问。如果必要，不同的活动者可以交易他们的财富以实现双赢。

地下经济的一个有趣发展趋势是采用传统的商业策略，形成了许多生产商和消费者之间的长期业务关系。例如，一个恶意软件生产者在发布他的代码之前会进行深入的测试，并经常发布更新和补丁，以满足顾客的需求。不论从哪点看，地下经济都是一项严肃的产业。稍后我们将会看到，地下网络犯罪分子甚至创建了一种建立在人口统计学基础上的广告模式！

4.1.1　地下通信基础设施

Internet在线聊天（IRC）网络是地下网络进行通信的经典并容易理解的方法。全球范围内廉价主机服务的大量普及使得设计专业管理的IRC网络变得极为容易。如果一个网络被执法部门所监视，犯罪分子可以很方便地转换到另一个网络。当一个被称为Shadowcrew的臭名昭著的网络被美国特情局所破获时，它大约有4000个成员，每天进行着大量的失窃个人数据交易行当。

IRC成员常常采取措施隐藏他们的身份。在一个像Shadowcrew和BoA Factory这样的网站中，成员们常常使用匿名代理或虚拟专用网（VPN）避免被跟踪。

这个地下市场和它的成员也是社交网络的积极参与者。公共论坛提供了一种良好的方式寻找潜在的商业伙伴。许多成员具有良好的交易声誉。如果在交易时出现了欺诈行为，可能会在公开的交易渠道对欺诈者进行不同程度的指责，那么欺诈者在声誉上的损失将会严重影响他以后的交易活动。

4.1.2　攻击基础设施

地下网络中的不同活动者使用各种工具和机制获取信息、搜集资源和发动攻击。除了瞄准一个特定漏洞或一个特定系统的一次性漏洞利用和攻击（在后面的内容中将深入讨论这个话题）之外，许多攻击常常涉及到肉鸡的使用。

攻击者通过引诱未引起警觉的用户下载恶意代码来创建肉鸡，通过肉鸡服务器的命令把用户的计算机变成肉鸡。在安装了肉鸡之后，被感染的肉鸡计算机与肉鸡服务器进行联系，并下载其他组件或者获取最新的命令，例如发动拒绝服务攻击或者发送垃圾邮件。有了这种动态的控制和命令基础设施之后，肉鸡的主人可以用几分钟的时间把大量的计算资源从Internet的一个角落移动到另一个角落。应该注意到，控制服务器本身可能并不是静态的。肉鸡已经从一种静态的控制基础设施演变为一种对等的结构，以实现容错和逃避检测。当一台服务器被检测并被阻塞之后，另一台服务器可以介入并接管前者的任务。控制服务器在被盗用的计算机上运行或者通过代理运行也是常见的做法，这样肉鸡主人的身份就很难被发现。

肉鸡常常通过与它们的创建者的公共IRC服务器使用相同的方法进行通信。但是，最近我们看到了一些肉鸡的触角伸到了P2P、HTTPS、SMTP和其他协议。通过这些实时的通信基础设施，肉鸡服务器向肉鸡发送指令、漏洞和代码修改。因此，肉鸡可以接受指令启动垃圾邮件发送、DDoS、数据偷窃、钓鱼或点击式欺诈攻击。肉鸡已经成为计算机犯罪领域中功能最丰富的攻击载体之一。

4.2　回报

根据Aegenis Group（http://www.aegenis.com/）的报告，2007-2008期间一张支付卡的黑市价值的估计售价大约在$4和$6之间。具有价格标签的支付卡磁条数据的售价在$25和$35之间，取决于信用限制以及支付卡的类型。足够开通银行账户的完整信息（包括生日、住址和社会保障号码）的售价大约是$200到$300。

黑市上还常常出售像驾驶证号码、社会保障卡以及PayPal或eBay账户这样的私人数据。驾驶证或出生证明的售价可达$100，PayPal和eBay账户也可以卖到$5到$10。

因此，一个能够利用一个未打补丁的漏洞从任何地方窃取信息的恶意软件根据效果可以卖到$20 000到$40 000。肉鸡装备软件（例如漏洞利用和肉鸡代理代码）在黑市上的售价大约是$5 000到$10 000。

私人数据和数据盗窃恶意软件日益上升的黑市价值创造了专注于交易金融信息的犯罪行业（即前面所提到的信息商）。TJX和Hannaford Brothers的事件仅仅是冰山的一角，问题的主体还没有被公众所理解。在接下来的几节中，我们将探索地下网络的数据搜集游戏，并探讨他们怎样把这些数据变成利润丰厚的生意。

4.2.1　数据交换

下面是一段被捕捉的IRC谈话的片断，对话发生在一位信息商和一位消费者之间：

＜A＞出售拥有几百个cvv2、银行账号和金融机构识别码的在线商店的管理员密码。我接收第一方支付（WU/E-Gold）。另外，希望用cvv2交易（WEBSITE）账户。

这位信息商可能通过黑客的手段获取了一家在线商店的信用卡和支票账户信息，更有可能是从一位实际侵入该商店的黑客手中购买了这些信息。购买和出售金融信息仍然是这个地下市场排名第一的活动。被盗用的信息在被使用之前常常会被转手数次。

值得引起警惕的是，为什么会有如此众多的“完整”私人信息挂牌出售呢？这类信息几乎包括了个人身份的所有方面，也就是申请账户、通过简单的网络认证以及在线购买商品所需要的所有信息。下面是地下交易渠道的一个被捕捉到的广告（实际细节被隐藏）：

＜A＞完整信息出售

＜A＞姓名：John Smith

＜A＞地址1：XXX S Middlefield Road

＜A＞城市：XXX

＜A＞州：CA

＜A＞邮政编码：XXXXX

＜A＞国籍：美国

＜A＞生日：04/07/19XX

＜A＞社会保障号码：XXX-XX-5398

＜A＞母亲的姓：Jones

＜A＞驾驶证的号码：XXXX24766

＜A＞驾驶证所在的州：CA

＜A＞信用卡号码：XXXXXXXXXXXX2134

＜A＞信用卡品牌：Visa

＜A＞过期日期：10/2010

＜A＞CVV号码：178

＜A＞信用卡银行名称：Citibank

＜A＞私密问题1：你的第一辆汽车的型号和品牌是什么？

＜A＞私密问题1的答案：Geo,Prism

＜A＞私密问题2：你的第一个宠物的名字叫什么？

＜A＞私密问题2的答案：Sabrina

可以看到，处理这些信息的人可以轻松地认定信息所有人的身份。像基于知识的认证（KBA）这样使用私密问题的机制对于保护被窃信息是没有用处的。

4.2.2　信息来源

信息商是从什么地方获取这些数据的呢？它的一些来源包括：

金融机构

金融机构是非常有吸引力的目标，因此它们保存了诈骗者进行金融犯罪时所需要的所有信息。由于这个原因，在线银行网站经常受到攻击。犯罪分子伺机寻找“漏洞”，窃取Web服务器后端的顾客数据。

商店

许多零售商（不论是网店还是实体店）在保护安全和数据隐私方面措施不力，因此是私人金融数据窥视者所关注的重要目标。TJX和Hannaford Brothers所发生的数据泄漏就是因为安全措施不得力所致。

个人持卡人

用户桌面上的间谍软件、键盘记录器和跟踪软件（pharmware）是泄漏私人数据的另一通道。

钓鱼

钓鱼网站伪装成合法的商业网站，引诱用户泄漏像登录ID和密码这样的私密信息。钓鱼仍然是一个广泛存在的安全威胁，尤其那些对计算机不太熟悉的用户。

4.2.3　攻击向量

当前的地下网络玩家使用许多攻击方法搜集数据。我将列出其中一些较为著名的方法。但是还有许多已经被观察到的神秘手法超出了本书的范围。

利用网站的漏洞

易受攻击的网站（特别是金融机构的网站和在线电子商务的网站）常常是通向有价值数据的直接通道。由于运行软件的Web服务器发布SQL命令提取和修改内部数据库（例如敏感的顾客信息），因此成功的SQL注入式攻击可以欺骗Web服务器把任意的SQL命令传递给数据库，提取它所选择的数据。

最近，一家著名的女性服装商店受到他们的Web应用程序防火墙生产商的通知，他们的Web应用程序中的SQL注入错误可能导致他们的整个顾客数据库被盗用，包括信用卡号码、PIN码和地址。

当前，致力于Web应用程序安全的厂商通过扫描发现许多（而不是一例两例）针对现有Web应用程序的漏洞所进行的SQL注入攻击，这是司空见惯的事情。随着Web 2.0的问世以及它仍然显得神秘的安全代码开发方式，我们对于许多Web应用程序很容易受到数据窃贼的攻击不应该感到奇怪。

有组织的犯罪组织很早就认识到，数字数据的盗窃对于他们而言是一个巨大的金矿。我们知道有些组织提供了自动化方法和手工方法对Internet进行持续扫描，寻找易受攻击的网站。

恶意软件

当今的许多Internet犯罪可以追溯到某种形式的恶意软件。例如，安装在用户计算机上的间谍软件可以窃取硬盘上的私密信息，例如社会保障号码、信用卡信息和银行账户信息。注入式IFrame是恶意软件的一种形式，它一般寄生于服务器上，可以捕捉用户的登录信息以及浏览器和服务器之间的其他用户通信信息。创建肉鸡的恶意软件安装在用户的计算机上之后经常会被唤醒，参与用户所不知情的肉鸡活动。

当前散布恶意软件最流行的方法是通过Web。当浏览Web的用户接触到被散布的恶意软件或者恶意站点时，可能会导致他们的计算机遭到恶意软件的注入。这类注入在很多情况下并没有任何视觉线索，如果没有特殊的检测工具，很难发现它们。

一个令人忧心的趋势是，我们发现越来越多合法的网站无意识地参与到恶意软件的传播中。注入网站的恶意软件（例如前面所提到的注入式iFrame）可以透明地把用户的浏览器重定向到包含恶意软件的第三方站点。根据著名搜索引擎的报告，排名前一百万名的网站（根据该搜索引擎的页面排名算法）中，有超过6000个网站在某个方面被列为是“恶意”的。它们有许多都是合法的网站，只是在某个时刻被盗用。社交网络站点和高容量的电子商务站点已经成为恶意软件散布的热点。

根据Symantec在2007年的报告，每天发现的新的恶意软件实例高达1950个！图4-1显示了2005年到2007年新的恶意软件的校正增长率，来源于Sophos、Symantec和Panda Labs所报告的数字。在这张图中，三家厂商中最保守的Sophos报告最近两年新的恶意软件的年增长率超过100%。Panda Labs报告恶意软件从2006年到2007年的增长率达到了令人震惊的800%。

图　4-1　恶意软件程序的估计（校正）增长率

大量的增长来自于同种恶意软件的变型数量的增加。也就是说，一次写成的恶意软件可以呈现为多种形式，以逃避签名检测。事实上，当前恶意软件生产者发布恶意软件的频率以及恶意软件对自身进行变形的方法导致基于签名的检测方式几乎毫无意义。

由社会垃圾邮件所推动的钓鱼威胁

垃圾邮件在Web上产生了更大的钓鱼威胁。今天的许多垃圾邮件本身并不携带实际的恶意软件，而是诱使用户访问钓鱼网站或者布满恶意软件的网站。

另一种可见的趋势是定向垃圾邮件攻击的增加，也就是向一组对某个特殊领域怀有兴趣的接收者发送经过特别筛选的垃圾信息。例如，向老年市民发送节省处方药的信息以及向股民发送热点股市信息是常见的做法。这种针对性的垃圾邮件的成功率要高得多，它使钓鱼成为一种充满生机的攻击方法。

在过去的几年里，反垃圾邮件技术取得了显著的进展。但是，Internet上的垃圾邮件的绝对数量自2005年以来还是增加了一倍。这个事实凸显了许多反垃圾邮件系统所存在的限制。

4.2.4　洗钱游戏

使地下网络经济切实可行的一个重要步骤是把金融欺诈的成果变成实际可用的现金。这并不是一个无足轻重的步骤，它涉及从合法的金融机构提取现金。

地下网络最有价值的财富之一就是所谓的“掩护”账户，可以安全地通过它进行转账和取款。这种账户往往是前面所讨论的出纳员的，它是可以分得一杯羹的参与者所拥有的合法账户。

假设黑客Johnny具有20位美国银行顾客的完整账户信息。Johnny可以创建一个银行转账，从这些（他可以访问的）被盗用的账户转账到Betty所拥有的一个美国银行账户中。Betty就是为Johnny提供服务的出纳员。然后，Betty来到一家本地银行，取出她的账户中的所有金额。她把Johnny的存款的50%电汇给一个预先确定的位置，由Johnny收取。剩下的50%则归自己所有。

出纳员需要承担不小的风险。有经验的出纳员很少只使用一个账户，而是常常拥有好几个用虚假身份证明开设的不同账户。好的出纳员常常可以要求分享很大一块市场利润。没有掩护账户和出纳员的参与，地下经济只具有学术研究的价值。

4.3　如何对抗日益增长的地下网络经济

公司在保护它们的数据免遭计算机犯罪方面是否已经做得足够？有些人持否定的看法。从经济学的角度，与数据盗用相关的成本包括私有成本（公司内部的成本）和外部成本（其他实体由于盗用而被迫支付的成本）。问题在于在传统的成本模型中，例如在总体拥有成本（TCO）中，很少考虑外部成本的因素。因此，保护技术上的投资很少能够满足数据盗用的真实成本。对于这个社区而言，现在是时候面对这些成本，寻找替代解决方案了，甚至可以寻求在传统上被认为成本太高而被否决的解决方案。下面提供了一些代替解决方案的思路。

4.3.1　降低数据的价值

诈骗者把目标对准数据的一个重要原因是这些数据具有价值。如果我们降低数据的价值，岂不是可以降低诈骗者窃取数据的动机？

降低数据价值的一种方法是限制数据的使用途径。以信用卡为例，如果发行银行降低基本信用额度，使用户难以获取高信用额度的信用卡，就可以极大地降低诈骗者窃取信用卡的欲望，从而减少数据失窃事件的发生。

显然，这个方法与透支消费的做法背道而驰。但是，如果说最近的信用市场的崩溃教会了我们什么，那就是在扩展信用额度之前应该发出警告。当数据失窃事件发生得越来越频繁并且保护数据的成本越来越高时，金融机构总有一天会重新评估数据背后的价值。为什么不从现在就做起呢？

4.3.2　信息的权限分离

大多数安全系统的一个常见缺陷是把权限的授予（认证和授权，以便进行像购买这样的活动）与唯一地用于标识客户信息的所有权相混淆。姓名和出生日期是标识身份的个人信息，可能会被窃取。信用卡号对于犯罪分子而言具有类似的价值，尽管它们除了一些信用历史数据之外能够提供的信息很有限。认证和授权不应该需要敏感的身份信息的实际所有权。

假设一个卡号是人脸螺旋几何的单向散列码，并且使用某种类型的PIN码的支付卡。只有当面部扫描和PIN码证实了卡号之后才能授权进行交易，否则这张卡就没有用处。如果我们采用真正的难以绕过的认证过程，那么甚至可以公布每个人的身份信息，而不必对它产生任何担忧。

4.3.3　构建动力/回报结构

今天，服从是采纳安全技术的一个重要动力。本书的其他几位作者在本书中讨论了这个问题。但是，服从的中心思想在于惩罚：如果不服从规定，必须付出一定的代价。这种结构不会为良好的行为提供回报，因此它的动力性不足。

回报型结构对于提高性能的正面影响众所周知。也许现在是时候让信息安全社区停止完全依赖服从，开始研究如何通过回报良好行为来提高总体的数据保护能力。这种回报应该包括组织的内部行为以及社会上各个组织之间的行为。

4.3.4　为数据责任建立评估和声誉体系

就像公司为环境所采取的“绿色环保”措施一样，我们需要一种类似的方法评估公司在数据处理操作方面的成熟度。就像绿色环保措施可以帮助公司提高社会声誉一样，良好的数据安全声誉也可以提高顾客的忠诚度，提升商业伙伴之间的信任度。也许这种评估和声誉框架会驱使一些公司把一些外部成本考虑为公司的内部成本，只要这种做法可以帮助它们赢得更好的声誉。

显然，实现这些建议需要转变思路。在某些情况下，需要完全推翻原来的基础设施，这可能需要昂贵的投入。但是，如果我们不是大刀阔斧地改变问题的处理方式，而是心怀侥幸，觉得已经很久没有看到像Hannaford Brothers和TJX事件这样的安全灾难了，那么灾难随时就会降临到我们头上。

4.4　小结

在现实世界中，无法指望一个组织或一个公司单枪匹马就能与有组织的犯罪进行对抗。这个道理也适用于虚拟世界。消除地下网络经济并不是一个小组、一个机构甚至一个国家能够办到的事情。它需要许多实体和机构的合作，包括用户、研究人员、安全机构、执法部门以及商业协会和政府所组织的力量。

第5章　美丽的交易：重新思考电子商务的安全

信息安全始终是电子商务的最大障碍之一。那些竭尽全力思考各种不同的新方法以保证电子商务系统和应用程序的安全的人们知道信息安全首先是从数据开始的。总而言之，我们努力保护的东西就是信息。

电子商务安全所面临的最大挑战之一就是设计出实用的方法来保证支付交易数据的安全。对于大量不同的应用程序而言，这个概念涉及许多不同的东西。但是为了简单起见，我们把注意力集中在信用卡数据上，例如账号、安全码、CV2码、PIN码、磁条数据、到期日期和发行日期。此外，还将包括我们觉得能够使这个过程变得更加安全的其他数据，例如身份认证和交易授权。

让我们观察信用卡信息可能出现失败的地方。当一位消费者在不涉及信用卡的情况下通过他的借记或贷记账户完成一次购买时，不管是在线交易还是像电话购买这样的离线交易，他都向商家提供这些数据，以证明他拥有购买这种商品的资源或信用。这些数据通过支付网关、后端应用程序、收单银行的网络和系统、发行银行和信用卡相关网络，在各种不同的系统之间传输。有些传输发生在商家环境中，有些传输则是在商家环境之外发生。

有些商家（合作伙伴）可能代表其他商家转售商品，而另一些商家（配套商）则把来自不同供应商和中间商的商品和服务包装在一起。当前，这意味着数据必须通过所有这些服务供应商和二级经销商的系统传递，增加了敏感的支付数据的流通次数（见图5-1）。最终，在安全性进一步被削弱之后，这类包含了遗留应用程序和操作系统的网络和系统难以保证支付数据的安全。

图　5-1　信用卡数据的扩散

但是，如果我们采取另一种方法又会怎么样呢？如果我们扔掉当前对电子付款和电子商务的许多假设，并假设商家并不一定要存储数据，又会出现什么情况呢？如果我们干脆不把敏感信息发送给商家又会怎么样呢？我们可以看到，保证这类数据安全性的一个主要困难是确认它所到达的所有地点。可是，如果这个问题无关紧要或者并不关键，情况又会怎么样呢？

5.1　解构商业

为了重新思考电子商务的安全性，我们首先必须检查当前有哪些安全措施。当前的安全模型存在一些基本缺陷，并且受到大量范围极广但最终却非必要的假设的制约。目前所采取的最佳安全实践方法是发布一系列的补丁亡羊补牢，把它作为深入的安全策略的组成部分。尽管这些安全实践在通常意义上有助于保护数据，但它们并没有把注意力集中在支付系统的真正问题上。

作为一种产业，我们花费了大量的时间和金钱跟踪这类数据，通过加密转换数据，并在存储和传输数据时对它们进行保护。所有这些措施都是为一个缺乏安全性的模型进行弥补。围绕支付卡行业数据安全标准（Payment Card Industry’s Data Security Standard）已经创建了一个完整的行业，以满足商家和服务供应商的需求。但这是为什么呢？在电子商务和零售行业的许多安全专业人员眼里，这些数据已经成为皇冠上的明珠。

分析安全环境

根本问题在于持卡人的数据成为共享的秘密。正如我们所看到的那样，即使只是为了完成单个交易，这种秘密也常常被许多参与方所共享。由于安全依赖于这些参与方所采取的安全控制的最小公分母，因此在账户的生命期内，发生数据泄漏几乎是不可避免的。Visa公司在其2008年第3季度的收入报告中表示，Visa品牌（包括Visa、Interlink、Plus和Electron）的年度交易总额增长了11%，从86.5亿美元增加到95.9亿美元。当我们分析数据的泄露途径时，这个信息向我们提供了一些启示。Visa是最大的信用卡品牌，但它只是许多信用卡品牌之一。每个交易很可能要经历多个商业系统、支付网关、服务供应商、实现系统、银行网络和信用卡网络。秘密被这么多方所共享，实在是太糟糕了！所有这些问题结合在一起，再加上这些共享秘密的复杂性，商家或服务供应商很容易产生几个理由，在完成初始的交易之后存储像账号这样的信息：

定期收费

许多商家所提供的服务要求每周、每月、每季度或每年定期收费。为了在同一个账户上定期收取费用，只要消费者的身份不变，商家就需要存储敏感的支付信息。

退款

为了进行退款，商家必须存储它的服务或商品所收费的账户。作为一种预防欺诈的措施，许多收单银行要求商家和服务供应商把退款打到与原先收费时相同的账户上。

方便消费者

消费者常常选择把他们的账户信息存储在经常购买商品的商家那里。这与我们以后所讨论的消费者动机一致。对于许多消费者而言，方便使用比规避风险更重要。

5.2　微弱的改良尝试

为了解决与多组人群共享秘密相关联的问题，银行卡协会建立了安全码（CV2）的概念，这是打印在信用卡上的3位或4位数字。它作为身份认证的准二级因素，试图证明信用卡确实是购买者本人所有。

系统的这种修补方案存在两个弱点。首先，安全码本身又成为另一个共享秘密。商家和服务供应商在处理安全码时会采用特定的规则，但是这样就需要再次依赖购买路径中最脆弱的一环。其次，当前并不是所有的银行都支持这个码，也不是在所有情况下都需要这个码。这意味着商家和收单银行即使遇到安全码检验失败的情况，也很难产生拒绝消费者购买的动力。因此，安全码对于反欺诈系统的改进收效甚微。

虽然CV2试图认证消费者的身份，但它对商家的认证仍然缺乏手段。消费者怎么知道商家是合法的呢？是什么因素妨碍消费者的社会交往或者导致他们的支付数据被泄露呢？银行卡协会和第三方的支付供应商已经创建了额外的安全程序，可以在不必出示银行卡的情况下认证和批准支付。我们可以分析当前所存在的一些更常见的过程，来评估哪些措施可以发挥作用以及哪些领域可能存在缺陷。

5.2.1　3D安全

3D安全是Visa所创建的一种基于XML的协议，用于认证消费者是否为信用卡的主人。Visa、MasterCard和JCB International都采纳了这个协议，它们所采用的名称分别是Verified by Visa、MasterCard SecureCode和J/Secure。

3D安全交易

3D安全这个名称表示这个安全模型所涉及的3个域。第1个域是需方（收单方），一般是电子商务的商家或者收单银行。商家使用顺应性软件向发行银行（第2个域，或称发行方）请求一个调用，证实账户持有人（第3个域）已经在3D安全卡程序中进行了登记。如果持卡人已经登记，发行银行就通过商家网页上的一个框架直接从账户持有人那里收集PIN码或密码。这种认证协议是以XML文档的形式通过安全套接字层（SSL）传输的。SSL保证账户持有人的PIN码或密码的机密性以及主机和客户机的真实性。另外，商家或收单银行并不知道PIN码或密码，这些数据只有账户持有人以及他的发行银行才知道。这个过程虽然描述起来颇为复杂，但它实际上是我们所熟悉的一种第三方信任场景：

1）消费者连接到商家的网站，做出购买选择，并输入信用卡的详细信息。

2）商家服务器插件（MPI）把主账号（PAN）发送给目录服务器。

3）目录服务器查询访问控制服务器（ACS），确定是否可以进行主账号的身份认证（或认证尝试的证明）。如果不存在适当的访问控制服务器，目录服务器就为这个MPI创建一个响应，并在步骤5中继续处理。

4）访问控制服务器向目录服务器作出响应。

5）目录服务器把访问控制服务器的响应（或者是它自己的响应）发送给MPI。如果认证或认证尝试证明均不存在，3D安全处理便结束。如果需要，商家、需方或支付处理器可能会递交一个传统的认证请求。

6）MPI向访问控制服务器发送一个付款人认证请求（PAR）。

7）访问控制服务器接收这个付款人认证请求。

8）访问控制服务器通过可用于主账号（PAN）的过程对消费者进行身份认证，这个过程可能涉及密码、芯片、PIN码等。

另外，访问控制服务器可能会生成一个认证尝试的证明。

9）接着，访问控制服务器用适当的值对付款人认证响应消息进行格式化，并对它进行签署。

10）访问控制服务器向MPI返回付款人认证响应，并对这个响应进行缓存，用于以后的交易。访问控制服务器可以把选中的数据发送给一个认证历史服务器（AHS）。

11）MPI接收付款人认证响应。

12）MPI对付款人认证响应的签名进行验证（执行验证或者把消息传递给一个独立的验证服务器）。

13）商家继续与需方进行认证交换。

在步骤13之后，需方通过一个像VisaNet这样的认证系统与发行方进行认证，然后把结果返回给商家。

3D安全的评估

3D安全在用户和账户的认证方面具有一些出色的优点，例如它在商家一端付出了很大的努力，保证它已经针对信用卡品牌进行了注册。它使用了顺应性支付软件，通过收单银行维护一个活动的、合法的商家账户。但是，它在实践中被证明是不堪使用的。

3D安全一直存在的一个问题是用户对被“钓鱼”的担心。一方面，有些钓鱼者会模仿它的行为。另一方面，有些用户会把真正的3D安全交易当成是钓鱼手段而拒绝交易。这与PIN认证多少有些不合常规的本质有关，因为请求是从一个与商家不同的域中发出的，事实上消费者可能无法识别发行银行和DNS名称。

3D安全所存在的一个更基本的问题与前文所描述的CV2码一样，它对于所有的交易并不是强迫进行的。因此消费者的账号仍然是珍贵的共享秘密。当PAN在各种不同的商家、支付系统和供应商系统之间传输时，必须采用相同的预警措施，以保护主账号。

事先授权要求设置商家、银行和消费者之间的协议，因此具有明显的开销。尽管这种开销相对于5.2.2小节所分析的SET协议而言明显要小得多，但它仍然不是理想的方案。

除了3D安全的一些消费者对钓鱼抱怨这一问题之外，3D安全还存在一个责任问题。支持3D安全的信用卡品牌表示他们并不会因为通过3D安全而发生的欺诈交易要求商家承担责任。这对于商家实现3D安全而言是个很大的激励，因为它将把寻求欺诈交易证据的责任转移到消费者头上，而消费者以前在这个领域很少需要承担责任。由于3D安全并非万无一失，因此对于持卡人而言并非万事大吉。从本质上说，如果遭受了钓鱼攻击，它还是需要负有责任的。

5.2.2　安全电子交易

安全电子交易（SET）是Visa和MasterCard于1996年开发的一个协议，用于保证通过像Internet这样的不安全网络所进行的信用卡交易的安全。SET使用X.509证书和扩展以及公钥加密技术，确认电子商务交易中每一方的身份以及传输数据，同时维护数据的机密性。对于消费者的账户而言，SET独一无二的绑定算法代替了一个临时证书，使在线的商家完全不需要访问这种敏感信息。交易的每一方要求通过证书授权（CA）进行预注册，这样信用卡发行方可以先进行必要的审核，然后再批准商家执行电子商务交易。然后，它对交易的所有参与方进行认证。

在消费者端，SET创建订单信息的散列值以及支付信息。支付信息与订单信息的签名散列一起被发送回银行。消费者端的软件还把订单信息发给具有支付信息签名散列的商家。持卡人和商家创建了对等的散列，当它们从银行或支付网关接收到散列值时对它们进行比较。

这个协议为交易提供了一些不同的保护：

·它在用CA进行注册时对参与初始交易的所有各方进行认证。

·它在消费者、商家和支付网关之间交换证书，在交易时执行额外的认证。

·像账号这样的敏感数据按照“有必要知道”的原则只由消费者和银行共享，使商家不能存储或传输这个信息。

SET交易

下面显示了一个交易所要求的事件序列：

1）消费者从银行那里获得一个支持电子支付和SET的信用卡账号。

2）消费者接收一个由银行签署的X.509 v3数字证书。

3）消费者进行订购。

4）每个商家具有自己的证书，它把这个证书发送给消费者，使消费者通过软件可以验证它是一家合法的商店。

5）订单和支付信息被发送。

6）商家请求发行银行进行支付认证。

7）商家确认订单。

8）商家的货物发送给消费者或者为消费者提供服务。

9）商家请求支付。

SET的评估

遗憾的是，由于海量的公钥基础设施（PKI）所涉及的开销以及SET所要求的注册过程，它绝不可能被广泛采纳。就电子商务市场的规模而言，采用这种技术的管理复杂性也是无法忍受的。

5.2.3　单用途和多用途虚拟卡

保证持卡人安全的一个最近趋势是通过虚拟卡。PayPal、MBNA和Citi是这个市场较有竞争力的公司。

虚拟卡用于在不出示信用卡的情况下像常规信用卡一样进行交易，并且商家的处理方式也和使用常规信用卡一样。事实上，商家甚至并未意识到卡是虚拟的，因此商家系统把它当成是其他信用卡账号一样处理。

虚拟卡的工作原理

每家供应商实现虚拟卡的方式稍有不同，但基本上可以归纳为两种类型：单用途虚拟卡和多用途虚拟卡。这两种类型的虚拟卡通常都是根据持卡人的请求“即时”生成的。一个现有信用卡账户的持有人向虚拟卡供应商请求一张虚拟卡，在一个特定的电子商务站点使用。供应商向该账户持有人提供一个虚拟卡号，包括一个过期日期和一个CV2安全码。

单用途卡支持有限金额的单次交易。这种卡一般会在几星期甚至更短的时间内过期，并且只能用于指定的商家。因此，即使信息丢失或者被盗，它很快将会失效，所以它对攻击者并无价值可言。

多用途卡也是由许多虚拟卡供应商提供的。这种卡适合于定期收费的情况，例如支付月账单。多用途卡仍然有许多与单用途卡相同的安全特性，例如只对单个商家有效，并规定了月支付限额。如果一张多用途虚拟卡丢失或被盗，攻击者只能在这个商家使用该卡，并且只能使用通过认证的定期收费金额，这样就大大降低了被欺诈的可能性。

5.2.4　破灭的动机

信息安全的一个常见经济问题涉及破灭的动机。动机是任何需要进行多方处理的系统的一个关键因素，尤其是那些依赖人们自由地选择做他们认为是“正确的事情”的系统。如果没有适当的动机，一般将会导致失败。为了对导致失败（市场失败）的外部压力进行调整，金融系统采用了两种方法：

规程

政府或行业协会制定了一系列的规则，以处理像垄断、污染、产品质量问题以及缺乏信息安全等市场失败行为。规程的形式包括医疗保险可携性和责任法案（HIPAA）、金融服务现代化法案（GLBA）、Sarbanes-Oxley法案（SOX）等。

责任

法律机制要求对商品的损坏或丢失负责的人或团体来赔偿损失（经常是经济上的）。以信息安全为例，如果个人或公司未采取合理的预防措施保护信息的安全，可能会被判定对此负责。

现在让我们看看为什么信用卡“安全市场”会导致市场失败。按照我们的金融模型，必须检查这个市场的主要参与方（消费者、商家、服务供应商、收单银行、发行银行和银行卡协会）的当前动机。

消费者

人们常常以为消费者会小心地保护自己的信用卡，因为当信用卡被滥用时，他们是损失最大的一方。但是，由于控制这些客观因素的一些规程的存在，事实并非如此。在美国，对消费者的账户所发生的任何欺诈行为，消费者本人只承担前50美元的损失。一般情况下，为了保障消费者的基本权益，发行银行还会放弃这50美元。

因此，虽然绝大多数消费者表达了保护他们的账号、安全码和过期时间的愿望，但在实际购买中，消费者很难产生保护这些信息的动机。这样的动机即使存在也是非金融性的，例如节省时间以及避免信用卡被盗用所带来的麻烦。

相比较而言，消费者对借记卡具有较强的保护动机，因为消费者在这方面受规程保护的程度不如信用卡。另外，借记卡常常直接与消费者的支票和存款账户相关联，如果借记卡的安全出了问题，很可能立即导致消费者的直接经济损失。

商家和服务供应商

在现有的模型中，如果交易出了问题，商家的损失实际上并不小。持卡人的数据如果被盗用，就可能会导致同时与规程和责任相关联的后果。商家受到银行卡协会（支付卡行业，PCI）的约束，这个协会制定了商家在处理数据（包括容纳数据的系统和传输数据的网络）时必须遵守的安全标准。违反这个标准的商家会同时受到银行卡协会在经济上和操作上的处罚。经济处罚通常针对收单银行进行评估，收单银行再将罚款递交给商家。商家也会被判定负有责任并被发行银行起诉，使银行能够补偿由于信用卡被盗用而付出的成本，包括重新发卡的成本。

商家还要承担在它们的环境中（使用3D安全时除外）使用欺诈性信用卡进行购物的责任。商家必须采用一些欺诈预防系统，以保证信用卡是合法的并且是由持卡人所支配的。如果商家最终接受了一张欺诈性的信用卡，发行银行就会进行退款，向消费者的账户退回款项。商家如果已经处理了交易并提供了产品或服务，就不得不自行承担与该交易相关联的损失。

尽管商家具有大量动机保护这些信息，但它们对购买过程并没有足够的控制，无法有效地实现目标。如前所述，这些数据必须通过多个系统传递，其中包括商家无法直接控制的系统。我们还看到许多商家在交易完成很久之后仍然保留其中一些数据，这就进一步增加了风险。

服务供应商也受到PCI数据安全标准（DSS）的约束。根据PCI安全委员会的说法，服务供应商的定义是：

服务供应商是一个业务实体，既不是支付卡品牌成员，也不是直接参与处理、存储、传输以及交换业务数据和持卡人信息或两者的商家。它还包括为商家、服务供应商、控制或影响持卡人数据安全性的成员提供服务的公司。例如，提供托管防火墙、IDS和其他服务的托管服务供应商、主机服务商和其他实体。仅提供通信链路而不能访问通信链路的应用层的电信公司除外。

许多适用于商家的规则同样适用于服务供应商，后者具有类似的处罚和责任。但是，它们的动机却颇不相同，因为它们并不直接与消费者进行接触。也就是说，在业务对业务的空间中，对品牌的损害仍然可能是一个很重要的因素。

收单银行和发行银行

收单（商家）银行和发行银行是受到严重约束的实体，它们对信息保护的需求远远超出了支付卡行业的预期。发行银行还具有代表交易中的消费者的额外动机。这通常意味着它不仅要关注数据的保护，而且常常要向消费者倡导数据安全。商家银行受到许多金融法规和交换的约束。它通常作为中间人或过手人，因此也牵涉到与商家和服务供应商相关的处罚。当一位商家或它的其中一位服务供应商相信发生了盗用事件时，收单银行将把银行卡协会所评估的任何罚款递交给商家和服务供应商，因为收单银行直接管理与商家的关系。

银行卡协会

银行卡协会预防欺诈的主要动机在于维护自己的品牌。简而言之，如果一个特定品牌被过度盗用，会给自己的形象抹黑，从而降低它在网络上的使用量。对银行卡协会而言，与银行卡盗用相关的经济后果并不存在。银行卡协会主要是希望消费者在使用它们的银行卡时能够感觉到安全。PCI DSS是由几家银行卡品牌组合了它们的安全程序后形成的，目的是为了自律和保护它们的品牌。

谁是主导者呢

总之，交易的每个参与者都有一定的动机希望保护这些数据（具有讽刺意味的是，消费者的动机反而最小）。但是，动机与控制力显然并不平衡。也就是说，没有任何一方可以完全控制数据的保护，也没有任何一方的动机与控制共享的秘密在流经各个环境时能够得到保护方面相当。

简而言之，当前系统具有太多的参与方需要知道这些共享秘密，但它们又没有足够的动机期待这些信息在它们的生命期内保持机密。把图5-1所显示的每个交易的参与方数量乘以一张信用卡的生命期内所经历的交易总数，可以看到共享秘密的接触方可以高达数千个。

5.3　重塑电子商务：新的安全模型

回顾了电子商务交易的当前安全模型之后，我们对它的优点和弱点有了充分的了解。现在，我建议用一种更优雅的方式观察电子商务的安全，使银行卡账户信息对攻击者而言没有什么利用价值，从而为消费者提供安全保障。

在不出示银行卡的情况下执行借记或贷记交易时，本质上共有7个基本需求，以保证交易的安全性，同时不会影响消费者和商家对系统的使用。

5.3.1　需求1：消费者必须通过认证

第1个需求是对消费者进行身份认证，确认他们的合法身份。如果消费者John被绑定到信用账户John123，首先需要确定他就是消费者John本人。这种认证由谁来执行最为合适呢？最合适的认证方式又是什么呢？

身份认证最好是由持卡人账户的管理者来执行，在大多数情况下是发行银行。这样做的理由如下：发行方已经拥有这些信息，因此让发行方存储这些信息并不会增加系统失败的风险。另外，发行方在认证的专业知识和流程方面投资了大量的资源，能够正确地执行认证。最后，发行者的数量要远远少于其他角色。

这种认证可以通过结合3个经典的认证因素来完成：

·消费者所知道的东西，例如密码。

·消费者所拥有的东西，例如令牌或证书。

·消费者本身所代表的东西，例如生物统计数据。

稍后我们将会看到，后面两个因素可能会给海量的分布和管理增加复杂性。

5.3.2　需求2：商家必须通过认证

第2个需求是对商家进行认证。这可以使消费者相信商家是合法的，能够提供消费者所订购的商品或服务。与第1个需求相似，对商家进行认证的最适合的参与方是商家账户的管理者，在大多数情况下是收单银行。

好了，我们已经完成了对消费者和商家的认证，现在又遇到了一个难题。发行方和收单方已经执行了认证，但交易是在消费者和商家之间启动的。怎么才能共享认证信息，使交易中的两个主要角色能够证实彼此的身份？这就需要发行方和收单方通过一种安全的方式传递确认信息。当我详细解释自己所倡导的流程时，将会深入讨论这个问题。

5.3.3　需求3：交易必须经过授权

接下来就是第3个需求：交易本身必须经过授权。到目前为止，我们已经确定了消费者John确实是本人，而且绑定到消费者John的账户。我们已经证实了商家Vencer Corp是一家合法的商家，并绑定到一家收单银行。现在，我们需要知道消费者John是否被授权进行这次购买。

幸运的是，在电子商务中，如果消费者已经正确地通过了认证，那么第3个需求不会有什么问题。发行银行可以根据所有现有系统中的账户状态、信用限制等状况确认交易。因此，如果需求1已经比较完美（说得容易），就可以防止入侵者的欺诈（常常称为“恶意欺诈”）。

必须承认，电子商务系统可能会遇到所谓的“友好欺诈”（这是一个颇显矛盾的术语），就是说欺诈来自商家想与之进行交易的人。当消费者在进行购买之后变卦了或者拒绝某些合法的收费时，就发生了友好欺诈。在交易中检测友好欺诈要比检测恶意欺诈更为困难。通过像数字签名这样的方式可以把友好欺诈控制在可以接受的程度，不过这个话题已经超出了本章的范围。

5.3.4　需求4：认证数据不应被认证方和被认证方之外的其他各方所共享

安全电子商务交易的第4个需求是不能把认证数据泄露给被认证方（消费者或商家）和负责执行认证的一方之外的其他各方。在一个典型的不出示银行卡的交易中，共有4个不同的实体。消费者（1）应该由发行银行（2）进行认证，而商家（3）由它的收单银行（4）进行认证。这里的诀窍是所有各方所共享的是表示认证成功或失败的证明，而不是共享实际的证书。

与当前广泛共享秘密的模型不同，这种新模型必须能够在不共享证书的情况下共享消费者和商家的认证状态。如果实现了这个目标，即使商家泄露了消费者的认证状态信息，对消费者的信用账户也不会产生任何影响。消费者可以依赖发行银行系统的安全性，而不是依赖每次购买所涉及商家的安全性。

商家一般是由一个供应商系统认证的，供应商是收单银行。由于收单银行对商家进行认证，因此在这个模型中，消费者在交易之前共享这个认证状态信息。我建议这个过程应该由收单方和发行方通过银行卡网络进行处理，后面会给出解释。

5.3.5　需求5：过程不能完全依赖共享秘密

第5个需求简单地重复了本章的中心思想。如果商家或服务供应商被盗用，这个新模型也不应该遭到破坏。前面所讨论的虚拟卡账号是满足这个需求的好方法，但它必须被统一使用。如果账号只是单次有效或者只具有有限的用途，即使商家或服务供应商被盗用也不会破坏整体的电子商务认证模型。

5.3.6　需求6：认证应该是可移植的（不受硬件或协议所限）

可移植性是必须实现的，因为当前存在太多的基础设施和太多的系统，不可能进行如此巨量的重新部署或翻新。我们所创建的模型必须允许消费者和商家在现有的支付框架和网络中进行认证。

尽管SET提供了一组健壮的安全控制，满足到目前为止所提及的大多数需求，但这个领域仍然存在很大的短缺。对于当前的支付流程而言，增加PKI基础设施和流程的额外开销显得过于庞大了。

5.3.7　需求7：数据和交易的机密性和完整性必须得到维护

第7个也是最后一个需求是维护数据和交易的机密性和完整性。这个需求是众所周知的，这个新模型必须认真处理这个问题。它所涉及的东西包括所有的认证数据、交易和订购数据。此外，它还维护这些数据所需要的任何数据。这个需求必须得到满足，因为这些数据是通过网络传输的，并且在系统中随时可能会被存储。

5.4　新模型

考虑前面几节的需求，我提议在不出示银行卡的交易中使用下面这个支付模型。

1）消费者通过一种公共的格式向商家和发行银行同时发送订购信息。

2）当商家从消费者那里接收到订购信息时，商家通过它的收单银行进行认证，并用一种单向的散列形式向银行发送订购信息。

3）成功通过认证之后，收单银行对表示订购信息的散列值进行签名，并通过银行卡网络把这个值发送给发行银行。

4）发行银行验证收单银行的签名，并为消费者所发送的订购信息创建一个单向的散列值。然后，它把这两个散列值进行比较，如果两者匹配，就确认这次订购。

5）如果发行银行成功地验证了收单银行的签名和消费者的订单信息，发行银行就向消费者发送一个限额与消费者的订购信息相符的虚拟卡号。

6）消费者使用一个虚拟卡账户向商家提交支付信息。

图5-2描述了这些步骤。

这个相对较为简单的六步骤过程满足上面所提到的所有需求，可以防止处理交易的任何系统盗用消费者的账户。尽管上面这些概念对于信息安全领域的人们而言并没有什么新意，但以前并没有协同使用这些概念来保护现代的电子商务交易。我相信这种方法的简易性使它成为一种美丽的方案，并且具备满足当前环境的可伸缩性。通过结合一系列的现有安全特性和过程，我们可以从根本上改变整体模型，创建一种简单、安全和美丽的混合体。

图　5-2　信用交易的新模型

第6章　捍卫在线广告：新狂野西部的盗匪和警察

最新的计算机安全问题报告揭示了一个令人震惊的事实，那就是大量的网络安全问题来源于在线广告。广告是网站生存的基石，它并不向终端用户收费，因此在网络上遍地开花。但是，广告的安全问题也是普遍存在的，从推销流氓反间谍软件的恶意链接横幅广告到点击欺诈软件、间谍软件和恶意广告软件，在线广告所存在的安全问题是令人震惊的。

在过去的5年里，我发现过数百起在线广告诈骗事件，受害的用户多达数千人，更不用说网络上那些深受其害的顶级商家了。本章对我所发现的一些问题进行总结，并说明了用户和广告客户应该如何保护自己。

6.1　对用户的攻击

用户是在线广告攻击的最早受害者，一般也是最直接的受害者。从欺骗性的弹出式广告到训练有素的浏览器漏洞攻击，用户所付出的直接成本就是不得不使用清理工具。本节将观察一些对用户进行攻击的罪魁祸首。

6.1.1　充满漏洞的横幅广告

2004年3月，包装式间谍软件推广人Sanford Wallace发现了一种在未经许可的情况下在用户的计算机上安装软件的方法。Wallace利用安全漏洞（Windows、Internet Explorer或用户计算机上的其他软件中的缺陷），在未经用户许可的情况下控制用户计算机。早先的入侵者必须诱使用户点击一个可执行文件或打开一个感染病毒的文档，这些伎俩是用户已经学会避开的。但是，Wallace的新方法可以在用户仅仅浏览了一个网站（这是我们每天无数次所做的事情）的情况下就完全接管用户计算机的控制权。

Wallace向一位合作者发送了一封电子邮件，汇报了这个成果：[1]

发件人：Sanford Wallace,masterwebfanclub@aol.com

收件人：Jared Lansky,jared@optintrade.com

主题：我成功了

日期：2004年3月6日

我想出了一种方法，在不需要任何用户交互的情况下安装一个exe文件。现在是我们随心所欲赚钱的时候了。

当Wallace利用这种漏洞攻击获得对用户计算机的控制之后，就可以安装他所希望的任何软件。许多厂商花钱请Wallace在用户的计算机上安装它们的软件。因此，Wallace在用户的PC上填满了用于跟踪用户行为、显示弹出式广告的程序，使用户的浏览器由于挤满多余的工具栏而混乱不堪。

但Wallace仍然面临一个问题：为了利用安全漏洞接管用户的计算机，Wallace首先需要让用户访问他的漏洞加载程序。怎么办？Jared Lansky作出回答：他从横幅广告网络购买了广告。用户只要在一个普通的网站上查看了一个广告，就有可能感染Wallace的不受欢迎软件。

但是购买广告仍然存在很大的困难。很少有网站直接向广告客户出售广告。反之，大多数网站通过广告网络公司来出售它们的广告空间。一旦某个广告网络公司意识到发生了这种情况时，它可能会暴跳如雷。总之，漏洞可能会摧毁一家广告网络公司在它的合作网站中的声誉。

Wallace和Lansky提出了一个由两部分组成的计划来避免检测。首先，他们在不容易引起广告网络公司注意的周末时间运行漏洞加载程序：

发件人：Sanford Wallace,masterwebfanclub@aol.com

收件人：Jared Lansky,jared@optintrade.com

主题：策略

我偷偷摸摸地实施自己的计划……从今天到周日——在每个人不注意的时候

其次，如果广告网络公司注意到了这种情况，Lansky就会声称对此一无所知。例如，Lansky是这样回复Bob Regular（广告网络公司Cydoor的副总）的抱怨的：

发件人：Jared Lansky,jared@optintrade.com

收件人：Bob Regular,bob@cydoor.com

主题：回复：请终止OptinTrade Online Pharmacy——违反协议

嗨！Bob，药品推广是一个使用了新的代码集的广告程序。在测试的时候，它并不会产生弹出窗口也不会修改主页，因此我批准它在您那儿运行。我也不清楚为什么会发生这种情况……

通过这种策略，Wallace和Lansky感染了数以千计的计算机，实际数量可能远远不止（诉讼档案并没有显示细节）。但是联邦贸易委员会（FTC）最终弄清了事实，立案并要求赔偿非法所得超过4百万美元[2]。遗憾的是，Wallace和Lansky的策略只是冰山的一角。

2004年后期，英国IT新闻网站The Register[3]受到一个在用户计算机上安装恶意软件的漏洞的攻击。这种恶意软件显示弹出窗口、跟踪用户的详细行为、甚至把用户的计算机变成转发垃圾邮件的“肉鸡”。我恰好对这个漏洞进行了测试，发现它至少安装在十几个不同的程序中，涉及数百个文件和数千个注册项[4]。在2005和2006年所出现的其他类似的漏洞攻击也使用了类似的策略，以降低越来越大的负载[5]。

访问一个熟悉和受信任的网站可能导致用户的计算机变成肉鸡，用户甚至没有意识到这个受到自己信任的网站已经成为恶意软件工厂。

利用漏洞所进行的软件安装并没有很好的防范手段：FTC多次禁止未经审查的安装[6]，消费者的集体诉讼证实了未经审查的安装是“对私人财产的侵犯”，是对消费者使用自己的私有物品（在这里是计算机）的干涉[7]。因此，基于漏洞的安装怎么才能逍遥法外呢？一种方法是继续采用Wallace的只在周末钻空子的方法：我已经发现漏洞攻击只在工作时间之外发生，或者只攻击特定区域的用户（一般是远离广告网络公司可能进行调查和采取措施的地方）。

更常见的情况是，许多基于漏洞的安装程序使用了中间媒介试图逃避责任。在Wallance与Lansky的伙伴关系的基础上，通过各种中间媒介所构成的关系链常常包括很多公司、伙伴和附属机构。它们所采取的手法很可能是由一位同谋者负责购买横幅广告，一位负责运行漏洞攻击，另一位负责开发软件，还有一位则设法通过这个软件赚钱。一旦暴露，它们便互相推诿。例如，购买横幅广告的公司声称它并没有运行漏洞攻击，而开发软件的公司会出示一份合同，合同上注明了软件的发行商承诺将会征得用户的许可。将焦点分散之后，这些合谋者希望广告网络公司、监管机构或保护消费者权益的律师无法找出罪魁祸首。

类似这样的攻击仍在继续，但已经显著减少了。为什么会发生这种变化？Windows XP Service Pack 2在基于漏洞的安装方面向用户提供了更好的保护，尤其是在Internet Explorer上安装了SP2补丁的情况下。另外，运行漏洞攻击的经济动力也开始枯竭：监管机构和消费者起诉了一些向大量基于漏洞的安装程序提供赞助的“恶意广告软件”商家，使这些广告客户开始犹豫是否值得通过这种不受欢迎的弹出广告进行产品宣传。对于能力出众的用户，有可能自己发现这些漏洞攻击。使用VMware Workstation创建一个新鲜的虚拟机，并安装一个具有漏洞的操作系统（例如，未安装任何Service Pack的Windows XP）。浏览网页并观察不同寻常的磁盘或网络行为。最好再运行一个包嗅探器（例如免费的Wireshark）以及一个或多个更改跟踪器。（我喜欢用InCtrl进行完全扫描，用HijackThis进行快速分析。）如果计算机遇到了一个漏洞攻击，可以检查包嗅探器的日志，推断发生了什么情况并且知道它是怎么发生的。然后，联系网站、广告网络公司和其他中间组织，帮助阻止漏洞攻击。我发现在浏览娱乐网站（游戏、歌词、BitTorrent下载的链接）时更容易受到漏洞攻击。但是，从原则上说，漏洞攻击可能发生在任何地方。总之，当虚拟机受到感染之后，可以使用VMware的“恢复”功能把虚拟机恢复为干净的感染前状态。

[1]这些邮件是在FTC诉讼时被发现的，被FTC v.Seismic Entertainment,Inc.等所引用。No.04-377-JD，2004 U.S.Dist.LEXIS 22788（D.N.H.2004）。

[2]FTC v.Seismic Entertainment,Inc.等。No.04-377-JD，2004 U.S.Dist.LEXIS 22788（D.N.H.2004）。

[3]“Bofra Exploit Hits Our Ad Serving Supplier.”The Register.November 21，2004。http://www.theregister.co.uk/2004/11/21/register_adserver_attack/。

[4]Edelman,Benjamin，“Who Profits from Security Holes?”November 18，2004。http://www.benedelman.org/news/111804-1.html。

[5]Edelman,Benjamin.“Spyware Installation Methods”。http://www.benedelman.org/spyware/installations/。

[6]参见FTC v.Seismic Entertainment,Inc.等。另参见Matter of Zango,Inc.f/k/a 180Solutions,Inc.等。FTC File No.052 3130。另参见Matter of DirectRevenue LLC,et al.FTC File No.052 3131。

[7]Sotelo v.Direct Revenue.384 F.Supp.2d 1219（N.D.Ill.2005）。

6.1.2　恶意链接广告

运气不好的用户有时候会遇到从网页弹出的横幅广告。从某些方面看，这类广告看上去与弹出式广告相似，它在用户已经打开的浏览器窗口之外又打开一个新的浏览器窗口。但这些所谓的“恶意链接”弹出式广告并没有得到底层网页的授权。相反，网站一般倾向于销售普通的横幅广告，而不是弹出式的横幅广告。而且，在这些恶意链接弹出式广告从网页中弹出时一般会充满用户的整个屏幕，而标准的弹出式广告通常要小得多。更糟的是，有时候这些恶意链接广告会把用户的整个浏览器进行重定向，完全替换了用户所请求的页面，使用户只能看到这些不请自来的广告。

如图6-1所示，恶意链接广告一般用于推销反间谍软件，它们声称用户的计算机已经感染了间谍软件，尽管它们并没有确切的证据表明用户的计算机与正常的有何不同。例如，这张屏幕截图声称它正在“扫描filevw80.ocx”，尽管在我的测试PC上并不存在这样的文件。事实上，这种弹出式网页只是个网页而已，并不是货真价实的扫描程序，根本扫描不出任何问题。而且，“正在扫描”框只是广告的一部分，看上去逼真的按钮和图形只是障眼法而已。突然弹出的中断信息加上逼真的用户界面，再加上过度渲染的感染“警告”，这些广告就可以有效地吸引用户的注意力，并说服他们购买这种软件。

图　6-1　虚假的扫描广告

恶意链接广告想方设法避开网站和广告网络公司的检测。在无害状态下查看一个恶意链接广告时，它看上去和普通的Flash广告并无不同，就是一个横幅，或许还有有限的动画。但是，当时间吻合时，这个恶意链接广告就使用Flash ActionScript弹出恶意广告帧。它是怎么做的呢？ActionScript常常使用混淆代码，它使用一个桩过程，通过对一段令人费解的信息进行解码以获取命令。在Web服务器登记的其他ActionScript程序可以查询用户的IP地址，并决定广告的行为。这些系统甚至可以检测到使用远程代理从遥远的地址测试广告行为的审查者。具体地说，ActionScript让一个广告根据用户PC的时钟获取时间。如果用户PC显示了某一时间，但实际时间却是与用户的表面IP地址相关联的另一个时间，广告服务器就可以推断出这个“用户”是一个通过代理连接的测试者，这样广告服务器就可以恢复到无害的横幅模式。

针对这些攻击的最佳防御是全面地保护客户的设备。以典型的横幅广告为例，Flash被认为只显示图片和动画，偶尔显示视频。为什么要允许区区一个广告使用如此丰富的代码？在默认情况下，广告所具有的权限应该非常有限，但Flash倾向于向广告提供令人吃惊的自由空间。遗憾的是，一种新的安全模型要求对Flash架构进行根本性的改变，这意味着Adobe到目前为止所采取的动作显示了它对这类防御的兴趣非常有限。

幸运的是，分散式的开发提供了两种可行的办法。首先，Flash的“AllowScriptAccess”标签允许广告网络公司禁用广告脚本，以杜绝被隐藏的恶意广告（[1]）。广告网络公司只需要把这个属性放在用于在页面中嵌入Flash横幅广告的PARAM或EMBED标签中。有了这个属性之后，广告就无法调用外部代码，因此极大地减少了攻击行为。有些广告客户可能坚持它们的丰富多媒体需要运行外部脚本。但是，有些在负责行为上被证明具有很高可信度的广告客户可以说服广告网络公司允许它们的广告运行脚本，同时标准广告客户或知名度较小的广告客户则获得有限的许可，与它们的有限声誉相对应。

其次，广告网络公司可以测试广告以发现不良角色。这个领域最负盛名的产品是Right Media的Media Guard，它使用自动化系统，使用不同位置的计算机对广告进行检查。这需要很大的工作量，Right Media花了很大成本创建了这项严肃的自动化技术。但是，这个系统使Right Media对它的广告行为充满了自信。随着越来越多的广告网络公司通过Right Media的平台交换库存，这种自信是极为关键的。现在，某搜索引擎提供了类似的测试，同时在广告客户的页面和自然搜索结果（OSR）中标出漏洞。但是其他广告网络公司，尤其是那些较小的网络，仍然缺乏有效的工具捕捉恶意广告或者将它们搜索出来。如果广告网络公司无力对它们的广告客户进行合理评估而导致用户陷入混乱，这实在是一件非常糟糕的事。

[1]“Using AllowScriptAccess to Control Outbound Scripting from Macromedia Flash.”http://kb.adobe.com/selfservice/viewContent.do?externalId=tn_16494。

6.1.3　欺骗式广告

30年前，《大英百科全书》是由经销商上门推销的。站在消费者的角度，这些推销商最大的优点就是对工作比较执著。《大英百科全书》的推销商们知道消费者将会拒绝他们的推销，因此他们借助一些计策来吸引消费者的注意力。在后来的一起诉讼中，一位法官是这样描述这些策略的：

一种用于进入潜在顾客家门的策略是通过广告调研分析调查表。这张调查表使推销商可以隐藏自己的身份，被误认为是致力于广告调研的调查人员。如果潜在顾客质疑他们的调查人员角色，他们就出示广告部主任所提供的一封通函，从而增强了调查表的欺骗性。这些调查表被推销商随手丢弃，根本不会对它们做任何针对性的分析。

FTC（美国联邦贸易委员会）成功地禁止了《大英百科全书》推销商的做法[1]，尽管《大英百科全书》千方百计在联邦最高法院为自己辩护[2]。FTC下令禁止“欺骗性上门推销”，它的原则是不允许推销人员通过他对消费者所说的第一句话给对方留下虚假的印象。这种欺骗的影响可能非常严重，使顾客后来无法作出公正的判断。把这个规则应用于《大英百科全书》的推销策略后，如果推销人员一开始声称自己是调查人员，就算消费者“同意”购买百科全书，这种买卖也是无效的，即使推销人员最终承认了自己的真正角色。

让我们把目光转回到现在。很容易看出，在线横幅广告与FTC的1976年《大英百科全书》案例非常相似。一组低劣的广告要求消费回答一个粗浅的问题（例如，“你喜欢George Bush吗”，见图6-2）或者要求消费者认出一个名人（例如，“这是谁的嘴唇”，见图6-3）。尽管提出了问题，但广告客户实际上并不关心用户是不是喜欢George Bush或者认出了哪个人，因为这类广告并不是进行市场调查。反之，就像《大英百科全书》的推销商声称进行“调查”只是为了引起消费者的注意一样，这些广告只是想让用户点击一个选项，使广告客户可以向用户展示一个完全不相关的推销页面。

图　6-2　吸引眼球的问题

图　6-3　吸引眼球的测验

但是，“欺骗式上门”广告并不局限于调查类型的横幅广告。大量的其他广告也采用了虚假的声明。一个广告可能会许诺“获取免费的手机铃声”，但实际上每月收费$9.99，这和“免费”显然是大相径庭（见图6-4和图6-5）。或者考虑一个许诺“免费信用报告”的广告，尽管它所导向的服务既不是免费的也不是信用报告。还有一些虚假广告试图销售那些到处可见的不收费软件。可以试试搜索“Firefox”、“Skype”或“Winzip”，读者将会对那些广告试图对那些流行且免费的软件收费而感到目瞪口呆（见图6-6）。

图　6-4　实际上并不免费的“免费”手机铃声

这些骗局向已经面世几十年的消费者保护法律提出了挑战。首先，并不是协议中的每个条款都是可以合法执行的。具体地说，实质性的条款必须清晰醒目，而不是用很小的字体含糊其辞[3]（例如，FTC成功地禁止了承诺“98%脱脂”冰淇淋但在脚注中承认它们的产品的脂肪含量并不低的广告。[4]但是，在线市场营销人员常常认为他们可以一边承诺“免费手机铃声”或“免费的iPod”，一边用很小的字体承认还存在“适用细节”。

图　6-5　“无限期免费”的手机铃声实际收费$7.99

幸运的是，管理阶层已经注意到这些情况。例如，FTC对ValueClick声称的“参与调查，赢取免费PS3”和“免费选择您的等离子电视机”等欺骗式横幅广告和弹出广告进行了处罚。FTC发现，要想收到这些承诺的商品，必须要完成极为复杂并且几乎不可能完成的各种注册和其他手续。在此基础上，FTC最终要求ValueClick支付290万美元的罚款，并停止这种做法[5]。佛罗里达的首席检察官对欺骗式免费手机铃声广告也持类似的不赞成看法，包括起诉广告网络公司，要求它们为自己的系统中所出现的欺骗式广告负责[6]。私人律师也伸出了援手，例如起诉一些搜索引擎巨头肆意显示虚假广告[7]。2008年7月，欧盟消费者保护专署报告超过80%的手机铃声网站使用了不准确的标价、误导性的条件或不正确的合同信息，而且这些都是建立在合法活动的基础之上[8]。新的欺诈广告每天都在出没，有远见的用户会比较警觉。如果广告所提供的条件太好，他们就会觉得难以相信。一旦产生怀疑，用户可以快速搜索这个网站的名称，也许会发现其他人的抱怨。如果用户想得到自动帮助，可以访问McAfee SiteAdvisor [9]，它专门用于检测和标记欺诈行为[10]。

图　6-6　对Skype和Winzip收费

欺骗式广告真的只是“计算机安全”的事情吗？虽然安全专家可以处理其中的一些骗招，但负责保护消费者的律师至少可以提供一些帮助。但是，对于受骗的用户而言，这种区别并不重要。保护用户避免许多安全风险的良好判断同样可以帮助用户减少欺骗式广告所造成的危害。

[1]Encyclopædia Britannica,Inc.87 F.T.C.421（1976）。

[2]Encyclopædia Britannica,Inc.445 U.S.934（1980）。

[3]“FTC Advertising Enforcement：Disclosures in Advertising.”http://www.ftc.gov/bcp/workshops/disclosures/cases/index.html。

[4]Häagen-Dazs Co.119 F.T.C.762（1995）。

[5]ValueClick,Inc.Case No.CV08-01711 MMM（RZx）（C.Dist.Cal.2008）。

[6]“CyberFraud Task Force.”Office of the Attorney General of Florida.http://myfloridalegal.com/。

[7]Goddard v.Google,Inc.Case No.108CV111658（Cal.Super.Ct.2008）。

[8]“EU Crackdown of Ringtone Scams.”EUROPA Memo/08/516.July 17，2008。

[9]“SiteAdvisor FAQs.”http://www.siteadvisor.com/press/faqs.html。

[10]Disclosure：I am an advisor to McAfee SiteAdvisor.John Viega,editor of this volume,previously led SiteAdvisor’s data team。

6.2　广告客户也是受害者

对于在线广告所造成的问题，人们很容易对广告客户进行指责。不管怎样，广告客户所投入的资金才使这个系统得以运作。另外，如果能更仔细地设计和购买广告，广告客户可以大大地减少不受欢迎的广告实践所造成的危害。

但是在有些情况下，我相信广告客户也是受害者：他们被广告网络公司过度收费，并且得到的结果也与他们所预料的大相径庭。

6.2.1　虚假的印象

2006年，我检查了一个称为Hula Direct的在线广告代理商。Hula旗下的Global-store、Inqwire和Venus123网站由于巨量的广告而令人震惊：它们在单个页面上至少显示6个不同的横幅广告，有时候甚至更多。而且，Hula常常把一些横幅广告堆在其他横幅广告的前面。结果，许多“背后”的横幅广告根本看不见，而被后来的广告所覆盖。如果Hula的网站只是在有价值的内容之间插入横幅广告，用户可能会谅解这种广告轰炸。但是，Hula的网站事实上很少有实质性的内容，只是一个接一个的广告。广告专家称这些页面是“横幅农场”：就像一个农场只种植一种庄稼，一眼望去看不到其他东西一样。横幅农场仅仅就是广告而已。

行业报告显示，Hula的广告客户一般根据每千人的印象成本（CPM）来购买广告的。也就是说，它们为每个据称看到过广告的人付一笔钱（尽管是非常小的费用，一般只有几分之一美分）。这种支付结构使广告客户面临一个显而易见的风险：如果一个像Hula这样的网站同时显示它们的广告，导致广告客户很可能为那些没有被用户注意到、没有点击到或没有实效的广告而付钱。总而言之，一个除了横幅广告之外什么也没有的页面不太可能吸引用户的注意力，因此这个页面上的广告引起用户注意的希望非常地渺茫。至于那些藏在后面的、基本上被其他广告所掩盖的广告，根本就起不到广告的效果（见图6-7）。

图　6-7　虚假印象实例：一个大幅Circuit City弹出广告覆盖了其他更多的横幅广告

Hula的横幅农场显示了对广告客户进行过度收费的两种方法。首先，Hula通过用户从未请求的弹出式页面加载广告。因此，Hula不仅可以声称显示了许多广告，而且Hula通过从间谍软件、恶意广告软件或低质量的网站所购买的廉价弹出式窗口显示这些广告。其次，Hula把它的广告设计为每隔9秒重新加载一次。广告客户希望自己的广告在用户浏览页面时一直出现在实际的页面上，结果却是在Hula发布的横幅农场上出现了9秒[1]。同时，其他诈骗者在横幅农场诈骗方面又玩出新花样，包括在每个1×1像素的微小窗口中加载横幅广告，使诈骗者在实际上并没有显示任何广告的情况下获取丰厚的回报。毫无疑问，广泛采用CPM的广告客户很容易受到诈骗。如果其他条件相同，采用这种支付方式的广告客户所获得的广告效果就不如采用其他支付方式的广告客户。

[1]Edelman,Benjamin.“Banner Farms in the Crosshairs.”June 12，2006.http://www.benedelman.org/news/061206-1.html。

6.2.2　避开容易受骗的CPM广告

对于不满意CPM的广告客户，在线广告销售商提供了另两种基本方式。首先，广告客户可以根据CPC（每点击成本）付费，只有当用户点击了广告时才需要付费。另外，广告客户可以根据CPA（每行动成本）付费，只有当广告客户实际卖出东西时才需要付钱。初看上去，这两种方式都没有CPM容易受骗。也许它们确实更好一点，但经验显示它们同样会受到欺骗。

玩弄CPC广告

CPC广告是搜索引擎的基石，从本质上说是Google的所有利润的来源。在最好的情况下，CPC使广告客户能够找到正在考虑购物的顾客，这很可能是一种非常有效的市场策略。但是，CPC广告策略也存在令人吃惊的过度付费问题。

考虑一个独立的网站发布者，他与一家CPC广告销售商达成了一种合作关系。在这种合约中，CPC广告销售商向该发布者支付一笔费用，一般是广告客户依据用户在该发布者的网站上点击了一个广告而支付的钱的某个预定百分比。这使发行者很自然地产生尽可能地多报告点击次数的动力。怎么做？有些发行者告诉它们的用户点击这些广告。（“支持我的网站：请点击下面的广告之一”）其他发行者使用JavaScript使用户的浏览器“点击”一个广告，而不管用户是否想要访问这个广告[1]。大多数技艺高超的流氓发行者使用肉鸡类型的代码，使被感染的PC加载各种CPC广告，这些都是在用户并没有请求广告的情况下发生的，甚至是在用户根本没有看到广告客户的网站的情况下发生的[2]。

CPC广告容易受到这类攻击，因为它的底层跟踪方法非常容易被愚弄。就像CPM广告网络无法方便地确定一个用户是否实际看到了一个广告一样，CPC广告销售商也无法分辨一次广告“点击”是由于一位真正的用户把他的鼠标移动到这个广告上并按下鼠标左键所导致的，还是由于其他类型虚假点击所导致的。总之，CPC广告销售商根据发送给搜索引擎的Web服务器所携带发行者ID的特殊编码HTTP请求跟踪点击的。但是，流氓发行者可以轻松地在用户并没有点击任何东西的情况下伪造这类请求。在发展中国家，由承包人整天点击广告以便从对应的网站收取费用也是一种成本很低的服务[3]。

对于关注这个问题的CPC网络而言，一个很容易想到的办法是拒绝合作伙伴，而只在自己的网站上显示广告。例如，在2008年中期，Microsoft只在Live.com、Microsoft的网站以及由受信任的顶级伙伴所运行的网站上显示广告。但是这个策略限制了CPC网络的到达范围，减少了广告客户的网络流量。反之，Google选择了相反的策略：在2008年第二季度，Google向它的伙伴支付了14.7亿美元购买它们的网站上的广告位置[4]。这么一大笔钱使Google增加了它的广告的到达范围，使它的服务对潜在的广告客户更有吸引力。毫不奇怪的是，Microsoft也在2008年7月坦白地宣布它将更广泛地开拓合作伙伴[5]。

CPC广告销售者声称它们具有健壮的方式确认无效的点击，包括对IP地址、重复点击和其他模式的无效点击[6]进行监控。但是，很难让人对他们的方法充满信心。CPC销售商怎么才能知道哪些东西是他们无法捕捉的呢？CPC销售商如何检测一个见多识广的肉鸡每个月在每台PC上只点击几次广告呢？如何应对把欺诈性点击与真正的点击混合在一起并伪造正确HTTP头部的做法呢？更有甚者，有些欺诈性点击会在每个广告客户的网站上浏览几个页面，这种情况又该怎么对付呢？这些虚假点击可以很方便地与真正的点击混淆在一起，使合作伙伴可以无风险地要求额外的费用。

2005和2006产生了一系列的集体诉讼，认为顶级搜索引擎Yahoo！等公司在防止点击欺诈、保护广告客户的账户方面无所作为。最终的和解补偿金超过了1亿美元，尽管广告客户的实际损失可能远远不到这个数。（如果一个广告客户未提交索赔申请，就无法分享这些和解金）。同时，搜索引擎商们发布了为自己辩解的报告，否认点击欺诈是重大的问题（注26）。但是，根本的安全缺陷并没有得到改变。

为了保护自己免受点击欺诈的伤害，广告客户们有时雇用第三方的点击欺诈检测服务对它们的Web服务器的日志文件进行审核，以搜索可疑的活动[7]。Google通过公开的响应，对其中一些服务的分析误差进行了抨击[8]。（很容易预测的是，Google掩盖了由于自己向这些服务所提供的数据有限而导致它们存在缺陷的程度）。Google甚至尚不清楚点击欺诈检测服务是否可以检测到那些来源于肉鸡的包含点击后浏览的点击欺诈。不过，虽然点击欺诈检测服务确实存在缺陷，但它们至少能够找到一些明显的点击欺诈，可以对搜索引擎的过度收费进行部分纠正。

增加CPA成本

每行动成本（CPA）广告看上去是最不容易受到欺诈的，这种广告只有当用户在广告客户的网站上进行一次购买时才支付广告费用。从某种重要的意义上，CPA降低了广告客户的风险：如果没有用户看到广告、没有用户点击广告或者没有用户在点击广告后进行了购买，广告客户就不需要支付广告费用。因此，像LinkShare和Commission Junction tout CPA这样的附属网络可以作为其他类型的在线广告的低风险替代品[9]。

但是，CPA距离对欺诈免疫还差得很远。假设大家都知道一个商家采用立即支付佣金的方式，例如在订单产生的当天支付。一位合谋者可以通过攻击者的CPA链接进行购买，然后退还商品并要求全额退款。这样，由于商家立即向攻击者支付佣金，因此虽然发生了退款，但攻击者还是拿到了佣金。另外，如果被拒绝的信用卡或者退款是在几天后才知道的，攻击者有可能在商家注意到这种情况前骗走了佣金。看上去这种事情很难大规模地发生，但事实上这类诈骗还是时有出现（现在比过去要少一些）。

在一个更精密的方案中，攻击者设计一个网页或横幅广告，自动调用CPA链接，不需要用户采取任何行动点击这个链接。如果用户以后恰好从对应的商家进行了购买，攻击者就可以得到佣金。把这个方案应用于Amazon、eBay等Web上最大的商家后，这些“填满的cookie”可以产生大量的佣金支出，因为即使没有广告的鼓励，仍然有非常多的用户在这些顶级商业网站上进行购买[10]。所有的迹象表明，问题的数量还是相当可观的。例如，eBay认为这样的欺诈非常严重，值得联邦立法。eBay于2008月8月对Digital Point Systems及其他公司的诉讼就是例子。在诉讼中，eBay要求共同被告们强制在用户的计算上安装eBay的跟踪cookie，因此如果没有提供真正的市场服务或者没有通过任何方式增加eBay的市场影响力，就无法从eBay获取佣金[11]。

在一种特别复杂的CPA方案中，攻击者首先在用户的计算机上安装跟踪软件，即间谍软件（或者用更礼貌的说法，恶意广告软件）。然后，这种跟踪软件监视用户的浏览活动并调用CPA链接最大限度地获取佣金。假设一位攻击者注意到一个用户正在浏览Dell网站的笔记本电脑网页，这位攻击者可以打开一个指向Dell的CPA链接，这样如果用户从Dell网站进行了购买，这位攻击者就可以获得2%的佣金。（不管怎么说，到哪里去寻找比刚刚浏览了Dell新产品的用户更好的Dell顾客呢？）对于Dell而言，事情看上去很正常：所有迹象显示，一位用户点击了一个CPA链接并进行了购买。但是，Dell并没有意识到用户实际上并没有点击这个链接。反之，是间谍软件打开了这个链接。Dell没办法知道用户实际上肯定会购买笔记本电脑，这意味着它所支付的2%佣金是完全没有必要并且很浪费。遗憾的是，Dell无法知道事实真相，因为Dell无法确认“点击”实际上来自间谍软件而不是用户的点击，并且Dell无法推断出即使它不支付CPA佣金，用户仍然会进行购买。

在过去的4年里，我发现了上百个使用这些伎俩的假冒CPA附属网络[12]。欺骗者的数量最终超过了我手工记录这些事件的能力，因此我在去年创建了一个自动化系统自动发现这类事件。我的软件在一组感染了间谍软件的虚拟机上浏览Web，监测任何意料之外的CPA“点击”事件，并在日志和屏幕捕捉视频中保存它的发现[13]。现在，我常常能够每天找到几个新的欺骗者，并且我并不是唯一注意到这个现象的人。例如，在一个针对ValueClick的集体诉讼中，商家们寻求退回它们不该支付的佣金[14]。最近声明的和解方案是被告拿出100万美元，作为退回给商家的佣金以及由于攻击者改写网络佣金cookie而遭受佣金损失的合法附属网络的补偿。

[1]Edelman,Benjamin.“The Spyware-Click-Fraud Connection-and Yahoo’s Role Revisited.”April 4，2006.http://www.benedelman.org/news/040406-1.html。

[2]Daswani,Neil and Stoppelman,Michael.“The Anatomy of Clickbot.A.”Proceedings of the First Workshop on Hot Topics in Understanding Botnets.2007。

[3]Vidyasagar,N.“India’s Secret Army of Online Ad‘Clickers.’”The Times of India.May 3，2004。

[4]Google Form 10-Q,Q2 2008。

[5]Microsoft adCenter Publisher Program（http://advertising.microsoft.com/publisher）。

[6]例如，参见“How Does Google Detect Invalid Clicks?”https://adwords.google.com/support/bin/answer.py?answer=6114＆topic=10625。

[7]例如，参见Click Defense（http://www.clickdefense.com）和Click Forensics（http://www.clickforensics.com）。

[8]“How Fictitious Clicks Occur in Third-Party Click Fraud Audit Reports.”August 8，2006.http://www.google.com/adwords/ReportonThird-PartyClickFraudAuditing.pdf。

[9]例如，参见“pay affiliates only when a sale……is completed”within“LinkShare-Affiliate Information.”http://www.linkshare.com/affiliates/affiliates.shtml。

[10]例如，参见Edelman,Benjamin.“CPA Advertising Fraud：Forced Clicks and Invisible Windows。”http://www.benedelman.org/news/100708-1.html.2008年10月7日。

[11]eBay,Inc.v.Digital Point Solutions,No.5：08-cv-04052-PVT（N.D.Cal.complaint filed Aug.25，2008）。

[12]参见Edelman,Benjamin.“Auditing Spyware Advertising Fraud：Wasted Spending at VistaPrint.”http://www.benedelman.org/news/093008-1.html.September 30，2008.另参见“Spyware Still Cheating Merchants and Legitimate Affiliates.”http://www.benedelman.org/news/052107-1.html。2007年5月21日。另参见“How Affiliate Programs Fund Spyware.”2005年9月14日。http://www.benedelman.org/news/091405-1.html。

[13]Edelman,Benjamin.“Introducing the Automatic Spyware Advertising Tester.”http://www.benedelman.org/news/052107-2.html.2007年5月21日。U.S.patent pending。

[14]Settlement Recovery Center v.ValueClick.Cen.Dis.Calif.No.2：07-cv-02638-FMC-CTx。

6.2.3　广告客户为何不奋起反击

根据几年来对在线广告的审核经验，我感觉许多广告客户并没有把欺诈放在很重要的位置。当然也存在例外，许多小型或中小广告客户无法承受欺诈所造成的损失。有些大公司对公平和道德的做法具有普遍的承诺。也就是说，虽然大公司因为欺诈而产生了很大的损失，但它们倾向于默默承受。

市场人士常常把广告欺诈看成是一种不可避免的商业成本。对我来说，这显然是本末倒置。谁说欺诈应该得到回报，为什么要把它看成是一种商业成本呢？通过提高对欺诈响应的有效性（寻找低成本的方式确定并预防），商家可以把“不可避免的”损失转化为一条更大的底线，从而赢得真正的竞争优势。然而，这种情况并不是经常发生，我相信这在很大程度上是由于广告客户的工作人员动力不足所致。

在购买在线广告的公司内部，个体员工的动力可能会阻碍保护公司免受广告欺诈。例如，有一家广告购买商认识到公司的其中一个供应商向公司多收取了好几千美元的费用。按说，这家公司应该立即追究这个事情，很可能把它提交给公司法律顾问或其他管理人员。但是在向上追查时，这家广告购买商将不可避免地承认它以前的失败，事实上它向供应商多支付了几个月才发现这种欺诈行为。我与广告购买商的交谈使我确信这些令人尴尬的效果是真实存在的。广告购买商觉得自己之前就该了解此事，并且它们会在本能上责备自己。或者不管如何，它们总是试图平息错误的事态。

同样严重的是，有些广告客户通过某些途径对广告购买商进行补偿，致使广告购买商缺少追究欺诈的动力。假设公司把在线广告总花费的10%支付给一家广告购买商，这家广告购买商很难拒绝来自虚假来源的广告。对于公司在那里所花费的每一美元，这家广告购买商将得到10美分的回报。这种补偿比局外人所想象的要普遍得多。许多广告客户就是通过这种方式向它们的外部广告代理提供补偿的。

对于内部广告购买商，这个动机甚至可能更加明显。例如，一位CPA程序经理（“广告联盟经理”）所得到的报酬可能是5万美元加上CPA程序年度增长额的20%。如果一个程序的毛收入从50万美元增加到80万美元，这位经理可以得到非常可观的6万美元的奖金。但是，如果这位经理发现了5万美元的欺诈金额，他的奖金就将减少1万美元。即使对于最具奉献精神的员工，这也是一剂难以下咽的苦药。毫不奇怪的是，许多广告联盟程序甚至无法拒绝即使是最臭名昭著的罪犯。

类似的动机问题也困扰着广告网络公司对它们的伙伴、下属机构和经营者的监管。如果一家广告网络公司承认它的一部分流量是欺诈性的，它就需要在商家所收取的费用中扣除相关的部分。反之，如果隐藏或否认这个问题，广告网络公司所收取的费用就更高。大的问题无法永远隐瞒，但是通过拖延或低调处理欺诈事件，广告网络公司可以增加短期的收益。

6.2.4　其他采购环境的教训：在线采购的特殊挑战

当大型公司采购供应品时，它们一般成立具有健壮的内部控制的采购部门。考虑一下顶级生产商对供应链的监管方式：经过精心培训的工作人员所组成的采购部门对候选生产商进行评估，以证实它们是合法和诚信的商家。在线广告行业的许多公司已经发展了规模更大的供应网络。例如，至少有几十万家网站向Google的AdSense服务销售广告库存。但是，广告客户和广告网络缺乏任何可参照的严密方法对发行者进行评估。

事实上，在线广告的两个特点使在线广告的采购比其他环境的采购更为艰难。首先，广告媒体倾向于它们将在哪里刊登广告持保密态度。由于担心竞争对手撬走它们的顶级合作伙伴，因此广告网络公司把它们的发行者名单作为机密。但是这样一来，从Google AdSense或ValueClick Media处进行购买的广告客户就无法知道有哪些网站刊登了它们的广告。因此，广告客户就没有办法通过手工检查合作伙伴的网站来证实它们的正当性和合法性。

而且，即使有一家发行者由于违反了某个可施行的规则而被捕获，也仍然很难防止它用一个新名字和URL重起炉灶。因此，即使在线广告客户即使设法检测到了不法分子，但是仍然对它们无计可施。在我的论文“通过优化的延迟付款策略阻止在线广告欺诈”（[1]）中，我建议了另外一种方法：延迟付款2到4个月，这样如果发行者的恶行被发现，它将面临经济上的损失。但是，到目前为止，大多数广告客户仍然采用快速付款的方法，使发行者进行欺诈时遭到处罚的风险很小。而且，广告客户仍然把快速付款作为一项优点，而没有意识到太快的付款将会导致它们在发现欺诈时仍然无能为力。

[1]Edelman,Benjamin.“Deterring Online Advertising Fraud Through Optimal Payment in Arrears.”2008年2月19日。http://ssrn.com/abstract=1095262。

6.3　创建在线广告的责任制

在线广告仍然是“狂野西部”，用户常常会遇到他们不该信任的广告，公司常常会过度地支付广告费用，而没有收到承诺的回报。但是，事情并不一定要这样发展。公共机构开始有所作为，提醒广告客户和广告网络公司，长远的消费者保护规则同样适用于在线广告。并且，随着广告客户变得越来越老练，它们将越来越不能容忍那些无法确认得到的服务的不透明收费。

同时，在线广告经济为那些具有跨学科兴趣并结合了软件工程师的创造性、侦探的敏锐嗅觉和公仆的奉献精神的人们提供了令人兴奋的机遇。好好工作，抓住那些坏蛋们！

第7章　PGP信任网络的演变

Jon Callas

当PGP（Pretty Good Privacy）于1991年问世时，人们才开始使用以前只有重要的政府部门才能使用的强大加密技术。

对于人权组织和世界上关注隐私问题的其他用户而言，PGP带来了新的机会，但它同时也带来了一些容易被误解的法律问题，稍后我们将讨论这些话题。

PGP最重要的特点之一就是它为从未谋面的人们（因此完全没有机会交换安全密钥）之间的安全联系提供了解决方案。这个解决方案很快赢得了“信任网络（Web of Trust）”的绰号，这个名称非常准确地描述了这个系统的操作方式。

PGP的信任机制与它早期发布时相比已经演变了很多。我们有必要研究这个信任模型的采用原因以及PGP为了提供更好的健壮性而进行演变的方式。

信任网络还提供了一个有趣的历史角度，因为它是一种早期的对等设计，毫无疑问是最早的社交网络之一。

关于PGP以及实用的公钥加密技术的论著已经有很多。但是，令人沮丧的是，有许多论著包含了大量不准确的内容。在本章中，我们的目标是描述PGP信任模型和它的实现、标准化和用途。我们还将在历史和政治的语境中讨论这些内容。

7.1　PGP和OpenPGP

PGP是软件，OpenPGP是标准和协议。PGP还是PGP公司所拥有的一个注册商标（并包括相关的术语“Pretty Good Privacy”和“Pretty Good”）。

在PGP成为协议之前，它只是个软件。这个概念非常重要，因为目前我们使用“PGP”这个术语表示PGP公司所开发的软件。这个软件实现了一些标准和协议，其中包括OpenPGP。另外，PGP是OpenPGP标准的许多实现的其中一种。但是，OpenPGP标准的许多东西都来自PGP，因此并没有严格定义的术语区分OpenPGP之前的软件和OpenPGP之前的协议。

OpenPGP协议的核心是根据IETF RFC 4880和RFC 3156定义的。早期的PGP协议是由RFC 1991定义的。因此，我们将用PGP表示早期的软件，用RFC 1991表示早期的协议。

7.2　信任、验证和授权

信任是一个很广阔的概念。在许多上下文环境中，它意味着许多东西。在本章中，我们将使用狭窄的、严格的定义：信任就是决定一个密钥是否合法的机制。如果一个密钥确实是一个声称拥有它的人所拥有的，它就是合法的。换句话说，如果与密钥相关的描述信息是准确的，这个密钥就是合法的。我们告诉PGP谁是我们所信任的密钥介绍人，然后PGP告诉我们哪些密钥是合法的。

因此，如果一个密钥声称它的主人就是拥有电子邮件地址“Alice”alice@example.com的人，并且这个信息是准确的，那么这个密钥就是合法的（或准确的）。如果Bob因为Charlie对这个密钥进行了签名而相信它是合法的，那么Bob就认为Alice的密钥是合法的，因为他信任Charlie。

让我们从下面这个定义开始展开讨论：

如果一个密钥确实为一个声称拥有它的人所拥有，它就是合法的。

然后我们转移到“信任”的定义：

“信任”是决定一个密钥是否合法的机制。

大多数人会混淆信任和合法这两个术语，即便像我们这种最了解信任网络的人有时也会犯错。合法只是一个评分，只能用于确定一个密钥的名称是否准确。信任是一种关系，可以帮助我们确定是否合法。

我们告诉PGP谁是我们所信任的密钥介绍人，然后PGP告诉我们哪些密钥是合法的。它根据密钥签名人以及我们对密钥签名人的信任程度为每个密钥设置一个分数。（PGP把签名人称为介绍人，稍后将介绍其中的原因。）

例如，如果Bob相信Charlie所签名的密钥是合法的，那么Charlie可以签名Alice的密钥并且Bob将会信任这个密钥，因为他信任Charlie。

信任模型是一种通用方案，对信任进行形式化，使计算机可以自动进行信任计算。下面我们讨论信任模型的基础知识。

7.2.1　直接信任

直接信任是最简单易懂的信任类型。在直接信任中，Bob信任一个认证是Alice的，因为Alice把它交给了Bob。这是最佳的信任模型，简单到用不着想个名称来描述它。它不仅是最简单的信任模型，而且也是最值得信任的模型。

人们随时都在使用直接信任，例如在他们的电子邮件或商业信用卡上加上一个OpenPGP密钥指纹（它本身是一个OpenPGP密钥的一个散列值）。甚至还有更简单的，如果我把自己的密钥通过电子邮件发送给你，我们就在使用直接信任。你将信任这个密钥是合法的，因为如果出了错，我只会伤害到我自己。

在直接信任系统中，常常由一个证书保存这个证书本身所签名的密钥。换句话说，它是自签名的。自签名非常实用，因为它提供了一致的检查。我们知道，一个自签名的证书要么完全正确，要么完全不正确。

直接信任的唯一问题是它无法迅速得到扩展，无法适合Internet的规模。但这并不意味着它是不实用的，只能说它的伸缩性不够强。

7.2.2　层次式信任

层次式信任也非常简单明了。在层次式信任中，我们信任一个证书合法的原因是因为它是由一个我们所信任的人签名的，就像前面所举的例子一样。Bob认为Alice的证书是合法的，因为它是由Charlie签名的。Bob相信Charlie得到了授权，可以进行正确的签名。有许多公司提供了类似Charlie这样的角色的业务。例如，GoDaddy和VeriSign就是两家受到广泛信任的认证中心（CA）。它们受到信任的原因是它们在创建证书时所遵循的做法。它们所采用的有些做法是使用一些数量较少的密钥，对信任进行扩展。这些密钥本身保存在根证书中（它们是自签名的证书）。根证书中的密钥对其他证书中的密钥进行签名。这个过程可以经过几次扩展，一直延伸到我们所感兴趣的实际证书。

为了对证书进行验证，我们可以跟踪一个证书链。Alice的证书是由一个证书所签名的，后者又是由另一个证书所签名的，而最后这个证书又是由Jack所创建的证书所签名的。许多大型公司、政府部门等都拥有自己所创建和维护的层次式信任。

在网络上，SSL连接所使用的X.509类型的证书就使用了层次式的信任。如果我们访问Amazon.com，它的Web服务器拥有一个通过层次式信任所签名的证书，最终可以回溯到我们所信任的一个根证书。这些商业的认证中心一般使用两层或三层的深度。

读者可能会问：“我们怎么才能信任根证书呢？”答案是：直接信任。Web浏览器内置了一组根证书。我们可以把它们看成是合法的，因为它们是我们所安装的软件的组成部分。这是一个重要的概念：层次式信任最终是从直接信任扩展而来的。

层次式信任简单明了，但具有一个明显的风险。如果授权方发生了错误，那么这个错误所造成的影响也将非常大。这个问题有过一个臭名昭著的例子，Microsoft在它的代码签名系统Authenticode中使用了一个层次式信任系统。这个层次结构是由VeriSign所维护的。几年前，一些歹徒说服VeriSign，相信他们是Microsoft的员工，但实际上并不是。VeriSign向不是Microsoft公司的人发布了Microsft代码签名证书。幸运的是，骗局很快被揭穿。如果他们拿到不正确发布的证书而逍遥法外，不管它们最终落到谁的手里，都可以用一个Microsoft密钥对软件进行签名。站在系统的角度，这种存在缺陷的软件就是Microsoft的软件。

7.2.3　累积式信任

层次式信任的缺点是层次是脆弱的，这就导致了最后一种重要信任模型的诞生，它就是累积式信任。累积式信任参考了一些因素，根据这些因素决定证书是否合法。

累积式信任来自于对层次式信任所进行的检查。在理想化的世界中，当一切事物都表现正常时，层次式信任是最理想的。在现实世界中，我们一直在使用层次式信任。护照、签证、驾驶证、身份证、信用卡甚至员工的资格证书都是通过层次式信任系统发挥作用的。

但是，安全人员最感兴趣的事情并不是一切都正常时会发生什么，而是一旦出错时会发生什么。层次式信任是脆弱的。由于它们的脆弱性，所以很自然地产生了一个趋势，就是限制层次的范围。例如，在一般情况下，护照和驾驶证并不是统一的。政府文档很少成为信用文档。由于层次是脆弱的，因此不同的层次之间往往互不通用。能够把你带到工作场所的标识卡并不能把你送上飞机，尽管它们之间实际上存在着信任链！雇主信任层次中的标识卡通过身份文档在银行和税务系统都进行了备案。至少从理论上说，它们可以被联系在一起。但是，它们并没有被联系起来，因为层次之间缺乏信任。

“我能看一下两种形式的ID吗？”这个问题表达了最简单的累积式信任系统。两种形式的ID就是一种累积式信任系统。它背后的安全设想是：两条信任路径同时出现错误的可能性要比一条信任路径出现错误的可能性低得多。这种思路就是依赖方至少要收集一条信任路径，使它成为累积式信任。这种累积可以使用不同类型的信任。例如，当你向一位商家提供信用卡时，商家可能会要求同时看一下你的驾驶证，这就是累积式信任。注意，信用卡是金融证书，而驾驶证则类似于身份证书。但是，商家从这两种证书中累积了足够的信任，然后再完成交易。

注意，累积式信任可以同时包含直接信任和层次式信任。设置一个包含直接信任和层次式信任的累积式信任系统是非常容易的。我们还可以把层次式信任看成是一种特殊类型的累积式信任，它将累积一条信任路径。直接信任也可以看成是特殊形式的层次式信任和累积式信任。

在公钥基础设施中，最广泛使用的累积式信任系统是PGP信任网络。但是，交叉认证和桥认证中心A即使还不能说是累积式系统，至少也是非常接近于累积式信任的。

定义

尽管本章中的大多数术语都是安全领域中常用的，但在这里提供部分术语的定义还是非常实用的，可以帮助我们保持思维的同步。PGP术语是有意为口语化设计的，它的目标就是成为与技术术语不同的常用语言。下面让我们定义其中一些术语，并讨论与PGP相关的术语：

PKI公钥基础设施。PKI是一组用于创建、发布、维护公钥和证书的技术和机制。证书（Certificate）

证书是一种数据结构，它包含了一个公钥、一些信息和一个签名，声明这个密钥和这些信息是相配的。身份证书是最常见的。它们把一个名称与一个公钥（电子邮件地址只是某种类型的名称）绑定在一起。属性证书把密钥持有人的相关信息与这个密钥绑定在一起。例如，一个表示“这个证书的持有人的年龄超过21岁”的证书就是一个属性证书。

认证（Certification）

认证就是用一个数字签名对信息和密钥所进行的单个绑定。X.509证书包含了一个单独的认证。OpenPGP证书可能包含一些认证。

认证（因此也包括证书）是认证中心（CA）在创建用于绑定密钥和相关信息的签名时创建的。

密钥（Key）

OpenPGP的术语，其他PKI称之为证书。Whitfield Diffie创建这个术语的原因是它比“证书”（Certificate）更容易发音、输入和理解。一般人对密钥有直观的认识，知道它是用来做什么的。目前，这个术语在有些语境上可能会造成混淆。一个OpenPGP密钥（证书）可能包含许多认证，事实上许多相关的公钥都是由它自己的上下文环境和策略所支配的。

密钥环（Keyring）

密钥的集合。它可能是一位用户所维护的私有数据库，也可能由一些用户所共享，例如下一个术语所提到的密钥服务器。

密钥服务器（Keyserver）

一部通过网络共享大量集合密钥的字典。人们可以把密钥上传到服务器并把它当成可靠的存储工具。通过密钥服务器，可以使所有与密钥相关的信息保持最新。

签名（Sign）

OpenPGP使用口语化的“签名”表示其他PKI所称的“批准”。用OpenPGP的行话来说，就是Alice对Bob的密钥进行了签名（而不是批准）。

介绍人

创建证书的密钥。因此，介绍人是OpenPGP用来表示认证中心的术语。和密钥这个术语一样，Whitfield Diffie创造这个术语的原因使它更加口语化，比“认证中心”这个术语更容易理解。

7.2.4　基本的PGP信任网络

对于熟悉其他公钥基础设施（PKI）的人们而言，PGP的一个特点使它与基本的层次式信任系统有所不同，而它的所有一切都是从这个特点所衍生而来的。在PGP信任模型中，所有的用户同时也是证书授权人。当然，并不是所有的授权人都具有平等的地位。Alice可以批准Bob，但这并不意味着Fran应该接受这个认证。这就是为什么PGP使用介绍人这个术语而不使用授权人的原因。人们具有一种自然的心理倾向，觉得授权人是可以信任的，但是对介绍人持很现实的怀疑态度。

“Phil，这是Bob。”Alice这样进行介绍。根据Phil对Alice的信任程度，他将会作出一个决定。Phil可以完全信任介绍人Alice，并接受为Bob为合法的。他也可以等待，直到Jon表示“嗨！Phil，你还没有碰见Bob吗”这时他才完全接受Bob是合法的。

有一个常见的错误概念是：由于信任网络是相对的，因此所有的参考系都是同等有效的，可以不存在可识别的授权人和有组织的使用。这样的思考方式只会混淆体系结构和实现。许多实现和部署把一组介绍递交给终端用户，由他们使用，就像接受软件中所植入的根证书一样。

但是，为了理解它的工作方式，让我们观察图7-1，它显示了信任网络以及它的各个方面。这张图中的每个节点是一个密钥环中的一个密钥。每个箭头表示一个认证签名。从Alice到Bob的箭头表示Alice已经签名或批准了Bob的密钥。

在图7-1的顶部，标签为You（你）（[1]）的密钥是主人的密钥。它是个三环圆形，表示它是完全受信任的，意味着它所签名的任何密钥都是合法的。我们也可以说它是隐式受信任的，因为You同时包含了私钥和公钥。图中的一些箭头表示You对属于Alice、Bob、Carol、Dan、Eli和Fran的密钥进行了签名。

图　7-1　PGP信任网络

图中还有一些孤立的签名，它们是不包含在这个密钥环中的虚引用。例如，有个You并未签名的密钥对Eli的密钥进行了签名。

图中用灰色填充的密钥是合法的密钥，我们可以认为它们是准确的，因为信任网络的累积式系统经过计算认为它们是合法的。累积信任的工作方式如下：部分信任的介绍人在图中用一个双环圆表示的密钥表示，它们的签名为一个点的积分。完全信任的介绍人在图中用一个三环圆表示的密钥表示，它们的签名为两个点的积分。如果一个密钥从介绍人那里所累积的签名达到两个点或更多，它就被认为是合法的。积分只有一个点或者没有点的密钥不会被认为是合法的。

为了观察一些例子，让我们注意一些特定的子图：

·Alice和Bob分别对Hal进行了签名。因此，Hal是合法的，但Hal并不是受信任的。

·Gil被Alice所签名，但是由于Alice只是部分信任的，因此Gil的密钥并不是合法的。

·Carol签名了一些同样位于这个密钥环中的密钥，但是它们都不是合法的。有趣的是，Ken签署了Phil，后者是合法的，但它之所以合法并不是因为Ken对它进行了签名。

·Dan、Eli和Fran具有密集的密钥。You对每个密钥都进行了签名。另外，它们都是完全信任的介绍人。Eli对Fran的密钥签名是冗余的，因为You直接批准了它。或者说，You的直接信任是冗余的，因为Eli对它进行了签名。

·Nan、Olivia和Mary是合法的，因为Fran对它们进行了签名。如果向上追踪到它们中的任何一个，可以看到两条合法路径，一条包含Eli，另一条是从Fran到You的直接路径。

·You认为Jon是合法的，因为Dan对它的密钥进行了签名，You也可以完全信任它。因此，Phil和Quentin是合法的。You所发现的由Jon进行签名的任何密钥也是合法的。对于其他完全信任的介绍人而言，这当然也是正确的。但是，Jon并不是You直接信任的。Dan和Jon的组合非常像层次式认证中心。

·另外，注意Quentin虽然有两条向上路径，但是其中一条经过Nan，它是合法的但并不是受信任的。因此，虽然Nan具有两条合法的向上路径，但Quentin只有一条。

·Ron具有3个签名，但没有一个来自You所信任的人。

·Sal是由两个虚密钥签名的，它们表示未连接到这个信任网络的人。

现在，我们可以很方便地通过这张图区分合法和信任这两个很容易混淆的概念。合法是节点（圆）的特性，而信任是节点之间的边的特性。我们可以通过信任路径确定是否合法。

[1]You并不是人名，但是为了讨论方便，下面有时把它当做人名处理。

7.2.5　最早的信任网络的毛边

早期版本的PGP基本信任网络与累积式信任系统非常相似。但是，它在体系结构和语义上存在一些毛边。在后期版本的PGP中，我们修正了这些毛边。不过，首先还是让我们回顾一下这些毛边。

超级合法

在图7-1中，Fran是个特殊的密钥，因为它的积分达到了4，其中2个是由You所签名的，另2个是由Eli所签名的。信任网络对这种超级合法的密钥并没有提供特殊的待遇，但在直觉上Fran应该享有一定的特权才是。对于Fran和Jon都进行了签名的密钥，应该也有一些特殊待遇。我们还需要把这种直觉转变为一种实用的机制。

签名密钥的社会影响

尽管如此，如果要为一个自己所不喜欢的人的密钥进行签名，许多人会感到不太乐意。站在社会的角度，分配信任也可能带来麻烦。仍然以图7-1为例，假设你在生活中非常信任Carol（她可能是你最好的朋友），但是你可能无法信任她能够正确地对密钥进行签名，这是由于她的信任本质所决定的。类似，Fran可能是个无聊的家伙，喜欢吹毛求疵，你无法忍受和她呆在一个房间里。但是，尽管她是个讨厌的家伙，但是你可能不得不让她成为一个完全信任的介绍人。

这个事实强调了我们在本章之初定义信任时所提到的一个要点：信任网络中的信任是一种特殊的信任，它局限于验证密钥的范围之内，而并不是现实世界的信任。

然而，对某个人的密钥进行签名可能是个非常个人的决定。许多人在这方面的感觉非常强烈。信任网络的优点之一就是个人之间的接触正是PGP的精神之一。但是，它存在一个很简单的弱点，就是人与人之间所谓的信任很容易受到情绪的影响。图7-2所示的漫画[1]即说明了这个道理。对于许多人而言，允许一个密钥完全是个人的私事。

图　7-2　负责任的行为

信任网络很自然产生的一个相关属性是密钥的签名会获取一定的威望。它们就像亲笔签名一样，具有一定的社会价值。

在这种社会价值中，有些是可以简单操作的。如果你好不容易找到几个自己重视的人对自己的密钥进行了签名[2]然后又得到一个新的密钥，就不得不再次获取所有这些签名。许多人保留密钥的时间要比密钥应该被保存的时间更长，因此这些密钥是由相应的社交圈子中具有影响力的人物所签名的，或者他们与密钥持有人之间的关系比较密切。

即使是自己的密钥，这个基本问题仍然存在。目前还没有办法把你的1998年的密钥自动前滚为2008年的密钥。如果旧密钥对新密钥进行了签名（或者反之），就相当于创造了信任模型所支持的一种手工前滚，但它并不是自动化的，它需要人们同时拥有这两个密钥。信任网络的社会影响是它最基本也是最顽固的毛边。它们在没有安全资格证书的情况下创建了一个社交网络。它们使安全资格证书成为一种滑稽和私人化的东西。但是，当它们变得滑稽并且私人化时，它们就获得了情感价值。我们对自己网站上的SSL证书并没有什么感觉，但是对PGP密钥的审核就涉及到对人际关系的审查。

[1]来自http://imgs.xkcd.com/comics/responsible_behavior.png,Randall Munroe，已获准使用。

[2]这里再次出现了社会价值问题！为什么你要从你所重视的人那里获取签名而不是从那些能够作出正确签名的人那里获取呢？

7.3　PGP和加密的历史

遗憾的是，在技术社区中流传着大量与PGP有关的荒诞故事。因此，了解它的真实历史并理解信任网络的技术选择是非常重要的。下面，我们将讲述它的一些历史[1]。

7.3.1　早期的PGP

20世纪80年代，当Phil还是一位致力于核武器冻结运动的和平主义者时，他就开始从事PGP的一些早期设计。当时的世界与现在大不一样：当时的美国总统是里根，克里姆林宫的掌权人是勃列日涅夫，美国联邦应急管理局（FEMA）向老百姓们灌输紧急疏散计划，数以百万计的人们害怕这个世界将会不可避免地陷入核战争的泥淖。上百万的美国人在中央公园游行，呼吁全球和平。

1984年，正是在这样的政治气候下，Phil看到了开发一种新概念（后来演变成PGP）的需要，不仅可以保护海外的人权，还可以保护国内的草根政治组织。他开始着手PGP的早期设计，但是更为急迫的和平运动事件拖延了他的开发工作，直到数年之后才有所进展。Phil在1991年编写了PGP的第一个实用版本。他在美国国会讨论要求所有具有“陷阱门”的通信设备和服务允许政府部门的反犯罪和反恐活动[2]之后发布了PGP。

尽管国会的讨论并没有形成最终的法律，但Phil还是推出了PGP 1.0，当时存在着各种野路子的强加密技术，而且都是合法的。用一种比喻的说法，PGP的明确设计目标之一就是在牲口棚的大门关闭之前把马群赶出牲口棚。

尽管当时还存在其他信息加密系统，例如引人注目的隐私增强邮件（PEM）[3]，但它们的控制过于紧密，不适合大规模的部署[4]。

PGP 1.0吸引了许多人的注意，但它还存在很大的缺陷。具体地说，它使用了Phil所设计的Bass-O-Matic解码器，而Eli Biham发现这种解码器存在密码上的缺陷。因此，Phil和Hal Finney、Peter Gutmann和Branko Lancaster经过改良之后推出了PGP 2.0。

PGP 2.0用Xuejia Lai和James Massey所设计的国际数据加密算法（IDEA）解码器代替了Bass-O-Matic解码器。它还用一种与RFC 1991相似的形式引入了PGP信任模型。

PGP 2抓住了许多人想在一种特定的、不受限制的环境中使用强加密技术的想法。它成为了一种草根现象。

[1]PGP的源代码可以从http://www.pgp.com/downloads/sourcecode下载。它不仅包括PGP桌面的源代码，而且包括PGP命令行源代码和PGP统一GPL修改的源代码。我们还可以从http://www.pgp.com/company/pgpassurance.html找到PGP在保证和特殊生成需要的策略。

[2]这是1991年的美国参议院草案266。它最终没有获准成为法律。

[3]PEM最早是由John Linn和Steve Kent在RFC 1113、RFC 1114和RFC 1115定义的。它们在RFC1421、RFC 1422、RFC 1423和RFC 1424中由David Balenson和Burt Kaliski进行了修订。

[4]Jon Callas当时在DEC公司，并拥有一个PEM证书。获取PEM证书需要一个公证人，并把他的护照通过信函发送给认证中心。他和他和同事从PEM转移到了PGP，因为要拿到PEM证书，至少需要一个月的时间，而且没有护照的人根本没办法申请证书。

7.3.2　专利和输出问题

尽管从加密的角度而言PGP 2非常强大，但是它也存在一些其他问题，涉及专利和输出控制。IDEA解码器是申请了专利的，专利拥有者Ascom-Tech AG授权非商业用户免费使用这个解码器，但商业用户却没有获得同样的授权。MIT在美国申请了RSA公钥算法的专利，并授权给了RSA Data Security Incorporated（RSADSI）。

1986年，Phil与RSADSI的首席执行官Jim Bidzos会晤，讨论了将RSA算法授权给免费软件的问题。会议的结果是Phil相信只要软件的售价为零（它是真正的免费软件），他就可以在软件中使用RSA算法。而Bidzos的思路恰好相反，他认为只要软件能够卖钱，他就能提取专利费用。接下来就是持续数年的对这个协议的分歧。PGP在法律上所遇到的困难有部分原因是RSA算法只在美国申请了专利，RAS专利的实际拥有者MIT有意向PGP软件提供方便。

最终的结果是RSADSI创建了一个称为RSAREF的库，用于免费软件和共享软件。PGP软件接着发布了2.5版本，在美国使用RSAREF，但它破坏了与以前所有版本的兼容性。这意味着PGP存在几个版本：

·PGP软件2.5之前的版本，未来将不会再对它进行开发和提供支持。

·PGP软件的“国际”版本，它完全是在美国之外开发的（由于输出控制的原因），它使用了RSA算法的原先实现。

·

PGP软件的“RSAREF”版本，它是在美国开发的，使用了RSA算法的RSAREF实现。在所有这些版本中，IDEA解码器对于非商业用途是免费的，但商业用途就没有这个待遇。

知识产权上的争议仍在继续，Public Key Partners于1992年向美国海关进行了投诉，抱怨Phil在没有得到适当授权的情况下输出密码技术（即PGP程序）并通过Internet传播。接下来就是臭名昭著的对Phil和PGP软件的调查。这个调查一直持续到1996年1月才被取消。

关于Zimmermann事件，存在许多错误概念，我们可以进行纠正。

Phil成了犯罪调查的目标，但并没有被指控。无论是他还是其他任何人，都没有接到与PGP有关的任何犯罪指控。没有任何诉讼发生，没有任何除了调查之外的其他法律行动，也没有任何人因为此事而坐牢。调查所覆盖的是PGP软件本身的实际发布，而不是PGP队伍的开发行为。PGP开发队伍由美国、新西兰和欧洲的开发人员组成。在国际范围内开发软件的行为从来没有成为此次调查的目标。

美国政府（具体地说，是国家安全局（NSA）并没有针对软件的存在而启动调查。一种常见的误解是政府因为不喜欢这种海量发布的加密技术而启动了这个调查。美国海关针对Jim Bidzos的投诉而进行了调查。尽管NSA也卷入了这个事件，但他们显然并没有启动调查，而且他们取消这个调查也发挥了一定的作用[1]。

[1]当时美国国家安全局的副局长William Crowell在与Jon和Phil的私人通信时表示，他在调查开始之前曾受到过咨询，并认为调查是允许的，但他也施加了压力要求取消调查，并认为自己在调查的成功取消方面做出了努力。后来，司法部门的其他人也声称在做出取消调查决定上有过作用。

7.3.3　密码战争

20世纪90年代的政治环境有个重要的组成部分就是所谓的密码战争。密码战争是密码技术在社会中的角色也被破坏殆尽。

直到1997年，密码技术仍被国际条约视为战争武器。免费和开放的密码系统在美国被视为军火，在法国则被禁止。它在世界上其他国家的地位也是混杂不一。1997年，美国对密码技术进行了重新分类，把它作为一种双重用途的项目。法国在1999年几乎开放了所有的加密技术，对加密技术的输出控制在2000年得到了极大的放松。尽管国际社会对恐怖主义日益关注，但这种放松政策仍在继续。

为了结束美国对密码输出的控制，PGP所做的一个努力是以书面印刷的形式公布了它的完整源代码，这是因为印刷书籍是不受输出控制的。1995年，首先出版的是《PGP Source Code and Internals》（MIT Press出版）。后来，Pretty Good Privacy公司在一套更高级的书中出版了PGP的源代码。这套书专门介绍一些经过优化后用于C源代码的光学字符识别（OCR）的软件工具。这样，输出数量不受限制的密码技术源代码变得极为方便，使输出控制变得没有意义，实行输出控制的政治规章和期望暗示被要求广泛应用强大的密码技术。

今天，政府对密码技术的态度几乎发生了180度的改变。这是与隐私、数据管理和公司管理有关的国内和国际法律不断变化所导致的结果。

1990年，关于密码技术的文化态度可以描述为：你为什么需要它？你非得隐藏些什么吗？20年后，文化态度转变为：你为什么不要它？难道你不理解保护自己数据的重要性吗？

密码战争的明确历史以及公众对密码技术的文化态度的转变并没有太多的书面材料可供参考。但是，Steven Levy的《Crypto：How the Code Rebels Beat the Government Saving Privacy in the Digital Age》（Penguin）可以作为了解这些知识的起点。

7.3.4　从PGP 3到OpenPGP

在PGP 2的状态变得平静之后，Phil伙同Derek Atkins和Colin Plumnb开始致力于PGP软件的新版本PGP 3。PGP 3包含了对RFC 1991协议的一些改进，包括：

·在一个PGP密钥中增加了对多个公钥对的支持。具体地说，它的设计提倡分离签名和加密密钥，作为一种促进密钥使用的方法。

·支持DSA公钥签名以及用于公钥加密的ElGamal。

·提供了对支持对称加密的CAST5和Triple DES的支持。

·在Hans Dobbertin在MD5散列函数的压缩函数中发现了伪冲突之后，用SHA-1替换了MD5散列函数（参见7.6节）。

1996年，Phil携同私人投资者以及曾经生产过商业版本的PGP软件的Viacrpt公司创建了Pretty Good Privacy公司。由于Viacrypt已经用产品名PGP 4发布了RFC 1991系统的发行版本，因此PGP 3密码系统在发行时被命名为PGP 5。

当时，PGP软件开发队伍的成员开始倡议PGP 3协议的一个轮廓，他们通俗地称为Unencumbered PGP，通过一组参数避免使用受到知识产权保护的算法。开发队伍将Unencumbered PGP提交给IETF，作为RFC 1991的继承者。它后来成为了OpenPGP。

7.4　对最初信任网络的改进

尽管本章前面所描述的模型是经典的并且广为人知，但很少有人知道后期版本的PGP所增加的许多额外特性和增强功能。当我们认识到基本模型所存在的限制之后，我们增加了这些元素，以增强它的伸缩性，抚平它的毛边。

对信任网络的主要改进所涉及的领域包括撤销（或其他原因所导致的使密钥变得无效）、伸缩性问题以及密钥的过期签名所导致的膨胀。本节将讨论其中的每个领域，并介绍其他一些有趣的改进。

7.4.1　撤销

所有的PKI都需要一种撤销证书的方法。人是一种容易犯错的生物，他们有时候会失去对计算机和密钥的控制，系统可能会丢失或被劫持。因此，在密钥过期之前，必须有一种方法能够使它们失效。

在诸如X.509这样的层次式PKI中，撤销从理论上来说是非常简单的（尽管仍然难以普及）。中心授权机构采用与发布密钥授权相同的渠道发布撤销信息。

基本的撤销模型

7.3节所描述的最初PGP信任网络提供了两种用于撤销的机制：

密钥撤销

如果有人失去了对他的密钥的控制，他必须能够撤销所有的一切。他不能依赖对他的密钥进行签名的每个人撤销他们的签名。这种类型的撤销和前面所讨论的一样，本身也是一种类型的签名。（在PGP中，签名是传递信任消息的基本形式。）用这种密钥撤销签名就使它的所有证书变得无效。

签名撤销

密钥可以创建一个签名，发布另一个签名不再合法。例如，如果Alice发现Bob欺骗她对他的密钥进行签名，她可以撤销她为这个密钥提供的证书。如果Charlie也对这个密钥进行了签名但并没有撤销他的签名，其他人可能会信任Charlie，并继续接受Bob的密钥为合法的。注意，这种形式的撤销对于数据签名和证书签名都是适用的。

密钥的撤销和过期

密钥的撤销对于所有的PKI而言都是不完善的，它也许是公钥密码系统中最尖锐的毛边。尽管PGP模型非常灵活，并避免了一些撤销上的陷阱（例如证书撤销列表的规模可能会趋向于无法控制），但它仍然存在与撤销有关的问题，下面列举了其中一些问题：

·没有一种机制能够发布脱离密钥本身的带外（out-of-band）撤销信息。如果Alice撤销了她的密钥，她可以把撤销签名放在一台密钥服务器上，但是没有办法把这个机制广播到每个拥有她的密钥的人。

·没有办法撤销一个丢失的密钥。对于PGP新用户而言，创建一个密钥，然后把它上传到一个密钥服务器上，接着很快忘了保护密钥的口令并不是新鲜的事情。丢失的密钥会一直处于未使用状态，直到它过期。

·撤销（也包括过期）在超出时间后再解释它们的含义时应该不再有意义。例如，Alice在2002年创建了一个将在2006年过期的密钥。假设她在2003年对一个文档进行了签名，并在2005年撤销了她的密钥。在2008年时，我们该怎么看待这个签名呢？显然，在2003年的时候，这个签名是没有问题的，但是这个签名在她撤销了自己的密钥后是不是仍然合法呢？

当然，具体地说，它在很大程度上取决于她为什么要撤销自己的密钥。如果她是由于创建了一个新密钥而撤销了原密钥，那么这个签名仍然是没有问题的。如果她是因为发现有窃贼窃取了自己的笔记本电脑并抄写了一些不良支票，这个签名就应该被认为是伪造的。实际上我们无法区分它是否来自一个伪造的、倒填日期的签名[1]。

这些困难意味着密钥是极少被撤销的，除非是密钥丢失这种显而易见的情况。至少在理论上，当一个密钥被新的密钥所取代或者由于其他原因不再使用时，应该能够很方便地撤销它，并且也应该撤销。但是，撤销常常被认为是发生了最坏的情况所致，也就是密钥被盗用。因此，用户很少撤销密钥。

避免撤销的一种方式是使用过期。PGP密钥的过期时间保存在一个自签名中。这种做法增加了一些额外的灵活性，因为密钥持有人可以简单地通过创建一个自签名来修改过期时间。因此，过期也是可以撤销的。遗憾的是，人们很少使用过期这个功能。

尽管我们接下来将讨论一些改进措施，但撤销仍然是个难题。

指定撤销人

指定撤销人是一种辅助密钥，它的权利是为一个主密钥生成撤销签名。来自指定撤销人的撤销是合法的，就像来自主密钥本身的撤销一样。

假设你指定Dan作为自己的指定撤销人。在指定之后，你的密钥中就存在一个自签名，它表示Dan是你的撤销人。另外，这个签名本身也被标记为不可撤销的签名。撤销人的指定必须是不可撤销的，不然在发生真正的密钥盗用时，盗用人就可以撤销这个撤销人[2]。

指定撤销人并不一定是个活动的密钥。例如，你可以创建一个专门作为自己的撤销人的密钥，然后把它存储在一个离线的保险箱时，直到需要的时候才把它从保险箱里拿出来。指定撤销人这个特性极大地缓解了丢失私人密钥、忘记口令和密钥被盗用这些事件所造成的困扰。每个人都应该为自己的密钥定义一个指定撤销人。

指定撤销人最初是在PGP 3中出现的，它也是OpenPGP的组成部分。

新鲜

新鲜是另一种管理过期和撤销的方法。新鲜的系统使用标准的过期和撤销，但它不再强调撤销的重要性强于过期（参见7.6节中的Rivest）。

假设一个密钥持有人创建了一个将在两周后过期的密钥，但他每周又重新创建了一个过期签名。这个密钥持有人单方面地创建了一个新鲜的系统。他的撤销需求被最小化了，因为他的密钥的任何特定拷贝都会在两周或更短的时间内过期。在许多情况下，新鲜允许持有人彻底忽略撤销，完全依赖于过期。

密钥签名人也可以使用新鲜的系统，避免对他们的签名进行撤销。

但是要注意，新鲜要求签名人不断地更新他们的签名。在代替签名时，它还需要更严格的政策。

新鲜的OpenPGP系统最早是在2003年Jon的文章“Improving Message Security With a Self-Assembling PKI”中进行描述的（参见7.6节）。

撤销的原因

如前所述，撤销密钥的原因有很多，但在信任网络的最初实现中，只考虑了最戏剧性的原因。从OpenPGP开始，可以对撤销签名进行注释，表示撤销的原因是密钥被停止使用、密钥被盗用或者这个密钥已经被一个新密钥所取代。这种注释甚至可以包含能够供人阅读的字符串。

[1]这个特定的问题可以通过一个第三方的公证人签名或邮戳签名来解决。如果出现了问题的文档具有另一个来自公证人或邮戳服务的签名，则这个表示Alice在2003年进行了签名的额外签名在此时仍然是合法的。但是，事实上并没有人采用这样的做法。

[2]不可撤销的签名是在指定撤销人的同时创建的，因为由这个特性创建了这个需要。

7.4.2　伸缩性问题

基本形式的信任网络在低端具有非常出色的伸缩性，它可以扩展到几千人。在一个机构内部，它甚至可以扩展到几十万人。

但是大型的、不连接的人群网络可能难以使用基本的信任网络，因为人群之间相互了解的路径很少。当信任网络包含了更多的具有相对较少的边节点时，寻找信任路径就变得非常困难。

信任网络最适用于具有某种联系的人群。它并不太适合像Internet这样的大型的普及网络。但是，目前存在两个补救办法，现代的社交网络重建了最适合信任网络的小型网络。Internet作为一个整体可能不适合信任网络，但对于我们大部分人所使用的Internet，它可能非常适合。

另外，认识直接信任的力量也是非常重要的。如果Alice与Zeke之间没有连接，那么她总是可以要求他向她发送他的密钥。

扩展介绍人

扩展介绍人又称辅助介绍人，通过把信任网络扩展到多级体系，完善它的伸缩机制。基本的信任网络已经支持多级层次结构，但是树中的每个签名节点必须是个特定的信任。扩展介绍人可以自动把介绍人介绍到一个指定的深度。

再次观察图7-1。假设Dan是个深度为1的辅助介绍人签名。通过这个信任签名，Jon的证书不需要其他签名就自动成为一个完全信任的介绍人。对于Dan所签名的其他任何密钥，情况也是如此。

注意，这样一来Dan就拥有很大的权力，与第一级委托的根证书授权相同。在现实世界中，很少向个人提供如此大的权力，但是我们可以考虑把一个机构分解为几个部门：如果该机构的一个顶层密钥得到一个辅助介绍人签名，并且这个密钥对部门的密钥进行签名，这些部门就自动信任彼此之间的密钥，而不需要进一步的交互。

扩展介绍人最初是作为OpenPGP的一部分出现的。

授权密钥

尽管扩展介绍人允许把一个密钥作为整个社区的一个信任点，但它作为伸缩性问题的解决方案仍然作用有限，它只适用于具有相互之间存在一些关系的本地级别。最初的信任网络成功地在没有授权的情况下无中生有地创建了一个可用的PKI。

一种范围更广的伸缩性机制是我们最近对PGP信任网络所添加的特性之一。Jon在“Improving Message Security With a Self-Assembling PKI”一文中首先描述的授权密钥的概念可以帮助创建一个完全分布式的PKI。

授权密钥的概念是这样的：有些证书或密钥可能被预设为真的，这并不是因为它们是从密码层次上继承而来，而是因为它们来自一个适当的授权，比如一个Internet域。例如，如果我们想对一个由alice@example.com所标识的密钥进行加密，就可以接受example.com的授权，即便它与我们并没有共同的信任祖先。

从安全的角度而言，这并不是没有争议的。如果example.com是伪造的，我们就会面临许多问题。另外，最终DNSSEC将使用数字签名保护DNS域，这可以使授权密钥得到更安全的密码保护。授权密钥依赖现实的网络：尽管DNS在小规模时很容易被破坏，但破坏大规模的DNS非常困难。即使最近在DNS基础设施上所发现的一些问题也说明DNS是个合理的信任点。

授权密钥解决了一个重要的伸缩性问题：机构必须能够管理它自己的PKI。解决这个问题的另一种方法是使用十几个甚至上百个层次（X.509证书就采用了这种做法），但这种替代方法自身就存在伸缩性问题。

授权密钥的概念也是DKIM电子邮件认证协议的一个不可分割的组成部分（参见7.6节的Allman等）。

7.4.3　签名的膨胀和困扰

PGP软件系统一般根据密钥对签名进行组合，它是处理签名最容易的方法，通常能够做到名实相符。但是，这种方法仍然可能导致三种我们不希望看到的场景：

替换签名

如果一个新签名替换了一个旧签名，这个旧签名应该被丢弃。例如，假设Alice在2007年为Bob的密钥进行了签名，过期时间为2008年。接着，在2008年，在第一个签名过期之后，她再次为Bob的密钥签名，过期时间为2009年。显然，2008年的签名应该替换2007年的签名。但是我们并不是很清楚一个签名是怎么成为另一个旧签名的后继者的。而且，在基本的信任网络中，我们也不清楚被替换的签名应该被删除还是继续保存在历史记录中。

过期的签名

考虑前一个例子，假设在Alice的第一个签名过期之后，她并没有创建一个新签名。我们是不是应该从Bob的密钥中删除这个签名？过期的签名不再包含在信任计算中，那么为什么不删除它呢？基本的信任网络并没有处理这个问题。一般而言，信任网络的实现方案采取了懒惰的方法，它只是简单地保留所有的过期签名。

不想要的签名

假设Bob不希望Alice对他的密钥进行签名。这可能事出有因，也许是因为Bob对Alice丝毫不了解（[1]），也许是因为Bob看不起Alice，不想与她有任何瓜葛。不管怎么说，这是Bob的密钥，他有权持有这样的想法。

另外还存在两个相关的问题。在Dudley/Snidely案子中，Dudley出于公共利益的原因想在Snidely的密钥上签名。很容易理解，Snidely不喜欢这样。因此，在Snidely对他自己的意愿的控制和公众利益之间就存在冲突。如果没有办法通过某种基于价值的策略进行密钥的签名，就很难调解这种冲突。这种策略正是我们想方设法实现的。

第二个问题是基本的信任网络并没有提供对签名人的认证。例如，假设Dudley对Snidely的密钥进行了签名，而这个密钥并不是来自Dudley，而是来自Boris Badenov（另一部卡通片中的一个坏人），这个人创建虚假的Dudley密钥的原因纯粹是为了激怒Snidely。

我们把最后这种情况称为签名骚扰。当我们开发OpenPGP的时候，在内部讨论了签名骚扰，但是没有把任何讨论内容公之于众，因为骚扰很容易进行，但是极难防备。那一代的密钥服务器所采用的密钥组合策略很容易产生这种形式的签名滥用。

随着时间的推移，我们建立了一些新特性，用于处理这些问题。

可导出的签名

签名膨胀的一个主要原因是密钥的导入机制。如果Alice想要导入Phil的密钥并认为它是合法的，她很可能需要亲自对它进行签名。如果她不小心（或有意）把Phil的密钥发回给一台密钥服务器，它的上面就会有她的签名。

防止这个问题的一个好办法是区分哪些签名出于公共目的，哪些签名出于私人目的（例如信任计算）。前者是可导出的，后者是不可导出的（[2]）。

当一个软件实现导出一个密钥或把它发送到自己的控制范围之外时，它会排除所有不可导出的签名。另外，当一个软件实现导入一个密钥时，它也会排除不可导出的签名。

这就允许信任网络区分哪些认证是用于大众的，哪些认证只是为了让个人创建密码安全的合法密钥集。

可导出的签名最初是在PGP 3中出现的，它也是OpenPGP的组成部分。

密钥编辑策略

为了防止签名膨胀，OpenPGP引入了两种密钥编辑策略，密钥的主人可以向密钥服务器声明自己的策略。

·指示服务器只允许那些由密钥主人所授权的编辑

·指示服务器用一份新拷贝替换密钥的旧拷贝，而不是简单地组合所有的密钥签名这两个策略使密钥的主人能够控制他的每个密钥上的签名。另外，第一个策略允许组合签名，但只有在主人的控制下才会发生。这两个策略一起为密钥的主人提供了对密钥的完全控制。

除了这些策略之外，还可以在密钥中嵌入一个URI，声明密钥的确定性拷贝位于什么地方。这个位置可以是一台密钥服务器，也可以是一个通过HTTP或FTP所访问的文件。这个URI允许密钥的主人把用户定向到这个密钥的定义性拷贝。这个确定性拷贝还可以帮助密钥的主人发布撤销信息。如果这个URI指向一台密钥服务器，那么它还可以帮助进行签名撤销。

所有这些策略最初是作为OpenPGP的组成部分而出现的，它们被实现为一个自签名的属性。因此，对于密钥上的每个用户ID，可以使用不同的策略。

[1]如果我们发现自己有密钥上有自己一无所知的人的签名，无疑会感到愤怒。

[2]“公共”和“私有”可能是更好的术语。“不可导出”有点拗口，不过它们都是正确的术语。

7.4.4　证书内偏好

尽管从严格意义上说OpenPGP并不是信任模型的组成部分，但它所引入的一个重要特性允许它在一个密钥的自签名中存储一些用户偏好。例如，存储一些与对称密码、压缩函数和散列函数有关的选项。

OpenPGP允许用户在一个包含每个选项的有序值列表中声明他们的偏好，这个列表被放在用户的自签名中。

它提供了一些人们所需要的特性，其中有些与信任网络相关。这种选项列表是用户选项经过认证的存储，允许OpenPGP的两种实现对不同用户的不同思路进行解析。

例如，我们假设Alice喜欢红色、蓝色和紫色，而Bob喜欢紫色、绿色、白色和蓝色。他们都喜欢蓝色和紫色，但喜欢的程度却几乎恰好相反。OpenPGP机制允许他们使用自己喜欢的方式。Alice可以使用紫色，因为它也是Bob的偏好。她也可以使用蓝色，因为它是自己的偏好。反正只要保证两者之间存在交集就可以了。OpenPGP有一组基本的强制算法，保证这种互操作性。

这些证书内偏好非常实用，因此X.509标准正着手添加自己的证书内偏好。

7.4.5　PGP全球目录

PGP全球目录是经过修订的基于LDAP的密钥服务器，通过更好的认证和密钥编辑对以前版本密钥服务器进行了完善。它的工作原理如下：

·全球目录要求通过电子邮件回路，对向它提交的密钥进行认证。全球目录向所有提供给它的电子邮件地址发送一封包含认证URL的电子邮件。只有当全球目录被一个认证URL重新访问时，它才会把密钥放在这个目录中。这个过程类似于网络博客、邮件列表和社交网络服务器所发出的确认邮件，以防止在没有得到当事人同意的情况下与其签约。

·全球目录允许一个电子邮件地址的主人通过电子邮件回路认证删除该电子邮件的一个密钥，这个过程与添加一个密钥相似。

·全球目录只允许每个电子邮件地址有一个密钥。这并不是密钥膨胀的完美解决方案，因为在许多情况下密钥持有人在一个特定的电子邮件上可以拥有多个合法的密钥。但是，在这种情况下，又会产生另一个问题，就是第三方如何决定哪个密钥应该被使用。最旧的？最新的？还是两者都使用？只列出一个密钥非常清楚地解决了这个问题，尽管这个解决方案并不能让所有人都满意。

·全球目录要求每半年对密钥进行重新验证。它向每个密钥发送一封更新电子邮件，如果邮件没有得到响应，对应的密钥就从全球目录中删除。

·全球目录实现了前面所描述的OpenPGP密钥编辑策略。

·全球目录用它自己的验证密钥对它所包含的每个密钥进行签名。这些签名使用两周间隔的新鲜期策略。因此，如果一个用户删除或撤销了他的密钥，它在全球目录中最多保持2个星期的有效。这就允许全球目录放弃自己的撤销机制。

这些操作策略完善了全局目录的准确性。用户知道全球目录中的每个密钥都通过全球目录与密钥的主人在此前六个月的某个时候的电子邮件交换进行了认证。

注意，有些策略仍然存在滥用的风险。例如，Boris Badenov仍然可以定期更新Snidely Whiplash的密钥。这类问题可以由抑制这类请求的适当软件解决，也可以通过类似反垃圾邮件软件通常采用的反骚扰机制来解决。

全局目录是对OpenPGP社区所存在的一些思路进行强化，包括：

·Robot CA，由Phil Zimmermann首先提出，由Seth Schoen撰文描述，并由Kyle Hasselbacher细化和实现（参见7.6节）。

·前面所描述的自组装PKI，由Jon Callas和Will Price所设计。

·现有的OpenPGP密钥服务器。

·由诸如Mailman（http://www.list.org）和GNU邮件列表管理器所使用的电子邮件回路认证。

7.4.6　可变信任评分

密码专家Ueli Maurer（参见7.6节）开发了PGP信任网络的一个扩展，允许信任分配的连续伸缩性。在他的模型中，通常的PGP伸缩性可以是0、1/2、1。如果累积了足够的信任，总量达到1的时候，就授权为合法。但是，他允许把介绍人的值设置为任何分数。例如，我们可以让一个介绍人的值为0.9，另一个介绍人的值是0.15。

7.5　未来研究的有趣领域

信任网络允许数以千计的人们在不必实际会面的情况下通过Internet建立联系，从而增强了人际关系和国际关系。它还提供了迷人的研究环境，可以致力于研究人际关系的创建以及声誉的存在。这些研究成果可以作为下一代的研究和编码的起点。

7.5.1　超级合法

在本章前面的图7-1中，Fran是个特殊的密钥，因为她的得分达到了4，其中2分来自You的签名，另外2分来自Eli的签名。信任网络不支持超级合法密钥，但在直觉上Fran应该享有一些特殊待遇。对于Fran和Jon都进行了签名的密钥，也应该有些特别之处。我们还没有把这种直觉转变为一种实用的机制。

7.5.2　社交网络和流量分析

许多人已经注意到信任网络形成了一个社交网络，这是一种泛化的定向图，显示了人与人之间可以通过他们的密钥和签名的连接。我们可以分析这种图的社交连接信息。

如果某个人同意一种理论，认为签名应该是中性的，甚至同意对一个“敌意”的密钥进行签名（例如Dudley对Snidely的密钥进行了签名），人们就无法根据一个密钥签名的存在而推断两个人之间存在个人关系。而且，PGP存在“密钥签名方”，就是一些人聚集在一起，集中批准彼此的密钥，从而淡化了社交网络的语义。

Neal McBurnett（见7.6节）分析了信任网络图的网络结构。他在图中对路径长度、连接性、规模级别和其他特性进行了研究。

Mark Reiter和Stuart Stubblebine创建了PATHSERVER（见7.6节），这是一种评估密钥之间的多签名路径的方法。

这些分析确实受到了信任网络的启发，并且是从信任网络引申而来的，但我们必须注意到它们与信任网络是正交的。信任网络的一个不可分割的特性是它由观点所组成的。它可以被认为是相对的，因为信任网络中不存在天生就特别有价值或者比其他更为重要的参考系。信任网络的信任部分完全依赖于用户特定的信任标记以及密钥持有人在密钥上所设的权重。密钥网络是个有趣的对象，我们相信它必然有自己的用途，并且能够对信任网络的使用提供帮助，但它是信任网络的一个正交结构。

信任网络的定向图提供了与网络中的人们有关的信息，虽然这些信息仍然有待于进一步的研究和辩论。

7.6　参考资料

E.Allman、J.Callas、M.Libbey、J.Fenton和M.Thomas。《DomainKeys Identified Mail（DKIM）Signatures,RFC 4871》，http://www.ietf.org/rfc/rfc4871.txt。

D.Atkins、W.Stallings和P.R.Zimmermann。《PGP Message Exchange Formats,RFC 1991》，http://www.ietf.org/rfc/rfc1991.txt。

J.Callas。《Improving Message Security With a Self-Assembling PKI》在第2届Annual PKI Research Workshop,Gaithersburg,MD上提出，2003年4月。

J.Callas、L.Donnerhacke、H.Finney、D.Shaw和R.Thayer。《OpenPGP Message Format,RFC 4880》，http://www.ietf.org/rfc/rfc4880.txt。

J.Callas、L.Donnerhacke、H.Finney和R.Thayer。《OpenPGP Message Format,RFC 2440》，http://www.ietf.org/rfc/rfc2440.txt。

H.Dobbertin。《Cryptanalysis of MD5 Compress》，在Internet上的声明，1996年。

M.Elkins、D.Del Torto、R.Levien和T.Roessler。《MIME Security with OpenPGP》，http://www.ietf.org/rfc/rfc3156.txt。

C.Ellison。《SPKI Requirements,RFC 2692》，1999年9月，http://www.ietf.org/rfc/rfc2692.txt。

C.Ellison、B.Frantz、B.Lampson和R.Rivest。《SPKI Certificate Theory,RFC 2693》，1999年9月，http://www.ietf.org/rfc/rfc2693.txt。

S.Garfinkel。《PGP：Pretty Good Privacy》（http://oreilly.com/catalog/9781565920989/index.html）。O'Reilly，1995.

J.Gordon。《The Alice and Bob After Dinner Speech》，1984年4月在苏黎世的研讨会上提出，http://downlode.org/etext/alicebob.html。

K.Hasselbacher。《ROBOT CA》，http://www.toehold.com/robotca.

K.Hasselbacher。《Robot CA：toward zero-UI crypto》，http://www.kuro5hin.org/story/2002/11/18/135727/66.

S.Levy。《Crypto：How the Code Rebels Beat the Government——Saving Privacy in the Digital Age》Diane Pub Co.，2003.

U.Maurer。《Modeling a Public-Key Infrastructure》，在1996年欧洲关于计算机安全（ESORICS’96）的专题报告会上提出。Springer-Verlag计算机科学的演讲稿，1996年9月，第1146卷，第325-350页，http://citeseer.ist.psu.edu/maurer96modelling.html。

N.McBurnett。《PGP Web of Trust Statistics》，http://bcn.boulder.co.us/～neal/pgpstat。

M.Reiter和S.Stubblebine。《Path independence for authentication in large-scale systems》，在“第4届ACM计算机和通信安全会议”上提出，瑞士苏黎世，1997年4月，第57-66页。http://stubblebine.com/97ccs.pdf。

M.Reiter和S.Stubblebine。Resilient Authentication Using Path Independence》，IEEE Transactions on Computers，第47卷，第12号，1998年12月。

R.Rivest。《Can We Eliminate Certificate Revocation Lists?》，在Financial Cryptography’98上提出，Springer计算机科学演讲稿，第1465号（Rafael Hirschfeld,ed），1998年2月，第178-183页。

B.Schneier。《Applied Cryptography：Protocols,Algorithms,and Source Code in C》，第2版，John Wiley＆Sons，1996年。

S.Schoen。《Casual PKI and making e-mail encryption easy》，http://www.advogato.org/article/391.html.

P.R.Zimmermann。《The Official PGP User's Guide》，The MIT Press，1995年。

P.R.Zimmermann。《PGP：Source Code and Internals》，The MIT Press，1997年。

第8章　开源Honeyclient：先发制人的客户端漏洞检测

当前，客户端软件的漏洞受到攻击的频率不断升高。根据2004年9月的一项调查，Dell公司估计90%的Windows个人计算机都至少潜伏了一个间谍程序。根据公共漏洞和暴露（CVE）数据库（http://cve.mitre.org）所提供的数据，Microsoft的Internet Explorer浏览器在过去的六个月中至少发现了50个漏洞。通过攻击客户端软件的漏洞，攻击者可以感染和控制那些受防火墙或其他措施的保护而无法访问的系统。

众所周知，当一台受害的计算机被劫持之后，攻击者可以利用客户端的漏洞进行许多其他恶意活动。攻击者可以盗窃有价值的信息，例如用户的在线银行资格证书等。攻击者可以劫持受害计算机，并把它添加到不断增长的肉鸡网络中。在肉鸡网络中，每个肉鸡都可以成为分布式拒绝服务（DDoS）攻击或恶意软件发布系统的组成部分。

攻击者是怎么利用客户端软件的漏洞呢？早在2002年，一篇标题为《How to Own the Internet In Your Spare Time》[1]的论文设想了一些可能出现的烦人场景：一种具有传染性的蠕虫攻击同时把目标瞄准了服务器和客户的漏洞。首先，攻击方使用典型的Web服务器安全缺陷（例如缓冲区溢出或SQL注入）上传恶意代码，当一个目标浏览器访问这个网站时就会下载这些代码。然后，被下载的代码就可以攻击浏览器客户的漏洞。

今天，攻击者把目标瞄准客户软件的原因是许多这样的软件应用程序是由那些没有经过安全软件训练的人们所开发的。另外，使用客户软件的人群要远远多于使用服务器软件的人群，并且大多数人只是很普通的Internet用户，很可能没有进行安全更新。相比之下，服务器管理员一般受过专门的技术训练，会经常对服务器软件进行安全更新。

即使用户足够明智，经常更新他们的操作系统并且很勤奋地运行反病毒产品的最新签名，大多数通过客户软件实现攻击的恶意软件仍然能够成功入侵用户的系统，而且不会被反病毒软件所检查。反病毒软件生产商很难跟上它们的步伐。

漏洞攻击产业

当我思索客户端攻击的风险的时候，我查看了诸如CVE和SecurityFocus（（http://www.securityfocus.com/bid）这样的漏洞数据库。我意识到安全行业在服务器漏洞的确认和修补方面远远领先于客户应用程序漏洞的确认和修补。事实上，也就是最近5到10年，我们才开始理解客户端的漏洞和攻击。我们有大量的东西要追赶！

使事态更为严重的是，各种把攻击客户端的漏洞作为它们的商业策略的“产业”如雨后春笋般不断冒出。根据InformationWeek所提供的信息（[2]），一家叫做Iframedollars.biz的俄罗斯公司开发了一个单线漏洞攻击工具，并把它放在市场上向Web服务器管理员推广。管理员下载这个漏洞攻击工具，把它安装在他们的Web服务器上，当每个Web浏览器访问了服务器并受到漏洞攻击代码的感染后，就可以向Iframedollars.biz收取6美分。

我必须承认对这样的单线漏洞攻击非常有兴趣。因此我访问了Iframedollars.biz，看看可以从什么地方下载它，结果发现为了获得这个漏洞攻击工具，我需要向Iframedollars.biz发送一些信息，包括我的姓名、住址和信用卡号码。这是相当滑稽的，你能放心把自己的个人信息交给这样的人吗？我不太清楚会有多少Web管理员同意把这些敏感信息发送给Iframedollars.biz。当然，我放弃了获得这个漏洞攻击工具的机会。

8.1　进入Honeyclient

在网络安全管理中，安全分析师对漏洞攻击的响应质量常常取决于他现在所掌握的与漏洞和攻击有关的知识。这些知识可用于创建入侵检测系统（IDS）签名，或用于主动修补存在漏洞的系统。honeypot是一种流行的用于获取高级知识的工具。它是一种专用的系统，故意成为新攻击的目标，当一次漏洞攻击获得成功时获取与此相关的详细信息。

但是，honeypot是消极的设备，到目前为止所有公开的honeypot系统仅限于发现对服务器软件的攻击。当前有大量的漏洞是在客户软件中发现的，例如Web浏览器或电子邮件客户。由于现有的honeypot系统无法检测对这些漏洞的攻击，因此我们需要一种新技术，能够驱动客户软件，使它们能够检测新的漏洞攻击。这个概念就是Honeyclient。

Honeyclient系统把具有潜在漏洞的客户软件（例如Web浏览器）暴露在潜在的恶意网站面前，然后监视系统的行为，观察有无攻击行为发生。Honeyclient模仿连接的客户端，在正常模式下作为蜘蛛或肉鸡，持续地访问随机的网站。这种客户程序很可能在一个沙盒内运行，由后者监视它的行为，观察它是否偏离了正常操作的边界。例如，我们可以对来自Honeyclient的请求进行监视，观察Honeyclient的请求是否开始偏离良好请求的状态（例如，这个Honeyclient系统不再写入到一个可执行文件）。如果Honeyclient开始表现出异常行为，我们就知道它已经被感染。另外，这个监视进程还可以检测恶意或被劫持的Web服务器。

传统的Honeypot是消极的，除非攻击者恰好对它们进行了攻击，否则它们无法检测到恶意行为。因此，Honeyclient具有能够搜寻可疑远程系统的优点。但是，它们各有适合自己的领地：Honeypot适合检测服务器端的漏洞攻击，Honeyclient适合检测客户端的漏洞攻击。

[1]S.Staniford,V.Paxson，和N.Weaver。How to Own the Internet In Your Spare Time。http://www.icir.org/vern/papers/cdc-usenix-sec02（2008年9月4日访问）。

[2]InformationWeek关于Iframedollars.biz的文章：http://www.informationweek.com/news/security/vulnerabilities/showArticle.jhtml?articleID=163701736。

8.2　世界上第一个开源Honeyclient简介

2004年，我开始设计一个Honeyclient原型。我所作出的最早决定之一就是开放这个原型的源代码。我希望这种做法可以激励安全社区的其他人把Honeyclient看成是一个技术领域，并且会有一些人对这个项目作出贡献。

为了检测漏洞攻击，我对客户软件所发生的改变进行了全面的检查，就像Tripwire（http://sourceforge.net/projects/tripwire）对服务器所做的一样。我通过记录文件的MD5散列和枚举Windows的注册表键，为Honeyclient设置了一条基线。在访问了每一个可疑网站之后，我开始寻找原先的基线和新的快照之间的变化。

我已经决定从一个Windows Honeyclient主机开始，因为普通用户的计算机一般都使用这个操作系统。

我还选择了Microsoft Internet Explorer（IE）作为访问可疑网站的浏览器。虽然可能存在专门针对其他操作系统或浏览器的漏洞攻击，我的Honeyclinet可能无法检测到它们，但是我只能选择一种操作系统和一种浏览器，因此应该选择用户使用量最大的。它们也是恶意软件开发人员最可能选择的系统，将它们暴露在漏洞攻击之下能够为最多的人群提供帮助。

真正关键的决定是让一个真实的Web浏览器访问每个站点而不是通过像wget这样的工具下载文件来模拟浏览器的行为。这种模拟方法是无法让我所希望发现的恶意软件的隐藏效果现形的。实际上，Honeyclient软件在遇到一个恶意网站时必须被劫持，这样才能收集并分析恶意软件，以便更好地理解最近的攻击线路。利用这些信息，我可以向用户社区警告这些不良网站以及潜在的感染后果。如果Honeyclinet只是模拟Web浏览器，它并不会被劫持，因为它并不存在攻击所瞄准的底层系统。

Honeyclient的操作步骤类似下面这样：

1）创建一条由MD5散列值和注册键值所组成的基准线。

2）Honeyclient主机调用iexplore.exe进程，启动Internet Explorer（IE）。

3）打开一个以前创建的URL列表文件，然后从这个文件中逐个读取URL。

4）驱动IE访问指定的URL。

5）当URL被访问时，就在系统上创建MD5散列值和注册键值的一个快照。

6）如果步骤5的快照与步骤1所创建的基准线不符，Honeyclient就认为发生了一个可疑的事件，它设置一个标志以警告用户。

7）如果快照与基准线相匹配，就对网站进行爬行（递归地访问每个链接），直到这个网站上不再有其他链接可以访问。

8）当一个网站被完全爬行之后，Honeyclient就从URL列表中提取下一个外部链接，并再次执行上述的过程。

最初的Honeyclient原型也是用一种驱动器——代理体系结构开发的（见图8-1）。驱动器代码负责创建一条初始的基准线并调用Internet Explorer浏览器，而代理则负责创建套接字以传递来自浏览器的HTTP请求。驱动器还负责对系统的状态进行检查。

2005年6月，在蒙特利尔的RECon会议上，Honeyclient的最初原型首次在公众面前亮相[1]。观众们显示了极大的兴趣，并在会议之后向我提出了许多问题。除了介绍Honeyclient原型之外，我还被问到了许多相关的问题。

这项技术是伟大的，但最终的军备竞赛会怎么样呢？攻击者会不会通过网络缺陷、着色URL或robot.txt文件设置Honeyclient的检测器呢？对于Honeyclient无法自动“点击”的Flash网站又该怎么办呢？我知道未来的开发中需要改进的一个地方就是使Honeyclient能够检测嵌入到JavaScript应用程序中的链接。

图　8-1　Honeyclient的第一代开源原型

稍后，我们将会看到Honeyclient操作是如何受到这些类型的反检测措施的影响的。

[1]有关Honeyclient在RECon会议上的展示请参见http://2005.recon.cx/recon 2005/papers/Kathy_Wang/Wang-Honeyclients-RECON.pdf。

8.3　第二代Honeyclient

2005年夏末，Honeyclient项目得到了MITRE公司的资助，我们的新项目队伍（[1]）开始致力于更高级的Honeyclient。我们希望保持部分的最初原型，主要是基准线完整性检查系统。但是，我们还想添加全新的特性，例如在虚拟机中运行Honeyclient。一直运行最初的Honeyclient，直到它被劫持，然后对操作系统进行重新镜像以准备下一次的被攻击，这个过程是非常乏味的。另外，如果我们想快速保持被劫持的操作系统用于以后的攻击证据分析，就需要实现Honeyclient的虚拟化。我们的队伍想要实现快速、易用并且具有强大伸缩性的系统。当我们创建下一代Honeyclient原型时，这些想法始终在我们的脑海中盘旋。

我们所作出的最初决策之一是对新的Honeyclient体系结构实现模块化。这就允许我们开发可选的插件，可以根据用户的目标方便地启用或禁用。模块化便于项目队伍之外的贡献者开发可以与现有的Honeyclient代码一起工作的插件。我们还决定根据GNU公共许可v2对这个第二代Honeyclient进行授权。我始终是开放源代码的热心支持者，回归社区总是非常好的方法，它可以鼓励其他人创建自己的开源Honeyclient原型。

另一个重要决定是采用客户——服务器模型，其中Honeyclient的客户（或代理）端位于Honeyclient虚拟主机上，而服务器（或管理者）端位于容纳Honeyclient虚拟主机的物理服务器上。服务器控制Honeyclient的高级操作，包括克隆和挂起虚拟机、检测Honeyclient系统的变化、与外部Honeyclient数据库进行通信以及捕捉Honeyclient和远程服务器之间流量的完整数据包以记录日志。这个新的体系结构几乎是对旧版本的完全重写。我们还更改了术语，把“驱动器——代理”更改为“客户——服务器”。更改术语的部分原因是我们觉得客户-服务器更好地反映了这一代的Honeyclient能够实现的主要概念之一：分布式Honeyclient。我们队伍的重量级开发人员Darien Kindlund就负责实现这个基于虚拟机的Honeyclient模型。

站在体系结构的角度，这个新的Honeyclient原型是个虚拟机（VM），它把一个本地运行的目标应用程序驱动到一个或多个远程资源（见图8-2）。在这个体系结构中，Honeyclient的结构如下所示：

·运行于VMware[2]虚拟机中。

·运行Microsoft Windows[3]操作系统的一个变型。

·配置为当系统管理员启动系统之后自动登录。

·在登录时，自动执行称为HoneyClient：Agent的Perl模块，作为一个daemon运行于一个Cygwin[4]环境中，并开始Web访问。

图　8-2　Honeyclient体系结构

我们选择VMware服务器进行虚拟化，因为它对所有的用户都是免费的，我们并不想加重开源社区用户的负担，不想让他们购买软件才能运行我们的Honeyclient原型。尽管VMware并不是开放源代码的，但我们发现它比开放源代码的虚拟化项目更适合我们的需要。它还可以运行于许多操作系统上。本章后面的“Honeyclient的未来”将解释为什么选择Linux作为主机操作系统。

Honeyclient系统被实现为一系列的Perl模块，在Windows XP平台上执行。这些Perl模块驱动一个Microsoft Internet Explorer浏览器，并自动爬行到特定的网站。驱动器脚本向Honeyclient提供了一个URL作为起点之后，这个模拟的Internet Explorer浏览器就抓取这个网站上的每个URL，并依次地点击每个链接，同时由驱动器脚本提取链接，用于将来的爬行。

Honeyclient Perl服务器检查文件和注册表键的完整性。甚至在Honeyclient第一次连接到一台远程服务器之前，完整性检查模块就开始在Honeyclient主机上监视文件、注册表键值和相关的进程信息。在Honeyclient访问每个站点之后，完整性检查模块把文件校验和、注册表的键值与已知的良好列表进行比较，观察敏感的系统文件或注册表键是否被修改。如果发现了修改，服务器就把这个URL标记为潜在恶意的，并保存在爬行这个URL时生成的所有网站流量记录，然后提醒分析师对这个网站进一步进行检查。

Honeyclient原型安装在一个虚拟机环境（当前是VMware）中，以便把系统恢复到一个已知的良好状态。为了进一步减少风险，我们的物理主机操作系统是Linux。由于Honeyclient的虚拟主机是Windows，出于前面所解释的原因，部署一个不同的操作系统作为物理主机有助于限制从虚拟主机到物理主机的连环失败的风险。当我们收集并分析负载数据时，可以更好地理解Honeyclient原型的限制，并不断对它进行修改以提高它的性能。

漏洞攻击常常通过垃圾邮件和拒绝服务这样的攻击方式登录到客户系统中，因此我们希望Honeyclient不要被攻击者当做发动其他攻击的跳板。因此，我们的设计的另一块内容是一个启用了防火墙的路由器，与Honeyclient一样运行在一台虚拟主机上。当这个防火墙路由器在内部网络和DMZ网络之间进行传递时，会过滤Honeyclient流量。当Honeyclient被驱动到一个远程资源时，这个防火墙被配置为允许Honeyclient访问处理从远程资源提取所有内容所需要的最小资源集。Honeywall（https://projects.honeynet.org/honeywall）是Honeynet项目的另一个开源软件，它很好地满足了我们的需要，因此开发队伍的成员JD Durick把它集成到我们的体系结构中。

如果主机系统把一个Honeyclient驱动到http://www.cnn.com页面，这个防火墙将允许Honeyclient与通过TCP端口80映射到http://www.cnn.com域的所有Web服务器（包括任何可能容纳外部内联内容的其他服务器，例如来自http://www.doubleclick.net外部链接的广告）进行联系。当防火墙批准Honeyclient进行访问时，Honeyclient就接到信号转到http://www.cnn.com。这个防火墙允许我们保护Honeyclient，防止它被攻击之后作为其他攻击的跳板。如果攻击者试图从Honeyclient主机发动电子邮件或DDoS攻击，这些行动将被防火墙所阻塞。

尽管我们一开始利用了Honeywall，但我们很快意识到通过这个防火墙的限制性外出流量防止了Honeyclient主机被劫持的可能。例如，如果一个网页使用了第三方的横幅式广告，漏洞攻击潜伏在一台不同的服务器上，那么Honeyclient将会错过这个攻击，因为防火墙将会阻塞对后面这个服务器域的访问。这个场景是我们需要考虑的权衡因素。让自己被盗用以捕捉更多的漏洞攻击更重要呢？还是限制成为肉鸡的机会更为重要？这是每个Honeyclient用户需要作出的一个决定。

在Honeynet项目中，除了利用Honeywall之外，我们还集成了来自Capture-HPC[5] Honeyclient的完整性检查算法。最初，我们的服务器只是简单地检查我们的Honeyclient系统上的文件和注册表键的基准线变化。稍后，我们决定对体系结构进行更改，实时检查文件、注册表键和系统进程的变化。Capture的完整性检查代码符合我们的标准，因此我们的开发队伍中的成员Xeno Kovah与Christian Seifert一起把这个完整性检查功能集成到Honeyclient中。

[1]http://www.honeyclient.org/trac/wiki/about上列出了Honeyclient项目队伍以及它的捐助人。

[2]VMware公司的网址是http://www.vmware.com。

[3]Windows XP的网址是http://www.microsoft.com/windowsxp。

[4]Cygwin工具可以从http://www.cygwin.com获得。

[5]Capture-HPC Honeyclient可参见https://projects.honeynet.org/capture-hpc，这个项目在https://projects.honeynet.org/capture-hpc/wiki/AboutCapture上有描述。

8.4　Honeyclient的操作结果

我们的第2代原型在2006年已经完全可以投入操作。虽然开发Honeyclient框架是个必要的步骤，与第2代原型一起成长本身也是一种学习体验。我们必须解决的第一个障碍是这个新原型所产生的假阳性。

8.4.1　Windows XP的透明活动

例如，如果我们访问一个外语网站，IE浏览器将弹出一个窗口，询问是否需要安装这个网站所用的特定语言的语言包。在这个过程中，我们注意到有6个Windows XP注册表键按照下面的方式进行了修改：

HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\IntelliForms（增加）

HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\International（修改）

HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\International\CpMRU（增加）

HKEY_USERS\S.+\Software\Microsoft\Internet Explorer\IntelliForms（增加）

HKEY_USERS\S.+\Software\Microsoft\Internet Explorer\International（修改）

HKEY_USERS\S.+\Software\Microsoft\Internet Explorer\International\CpMRU（增加）

由于我们在浏览一些没有问题的外语网站时同样会出现上面这些结果，因此我们知道这些修改并不是恶意行为的提示。此时，我们决定把这些没有问题的操作添加到我们的白名单中。另一个有趣的结果是在我们访问基于SSL的URL时发生的。下面这些文件在Windows XP环境中被反复修改：

Cab1.tmp,Cab2.tmp，……Cabx.tmp

Tar1.tmp,Tar2.tmp，……Tarx.tmp

创建这种白名单始终是一种有趣的体验。开始测试已知的良好网站之前，我们并不知道在Windows XP环境内部的正常操作过程中会发生多少修改。即使我们在文件和注册表键中看到了所有这些修改，也并不意味着我们知道一个文件或注册表键的修改是否是恶意的。例如，当我们访问一个情况不明的网站时遇到一个文件中的下面这个修改时：

C：\WINDOWS\fla1.tmp

这个文件是正常的还是恶意的？当我们在百度上搜索这个文件名时，看上去即使是反病毒研究人员对这个文件是否“良好”也存在疑问。正如这个文件的名称所提示的那样，它可能与Flash Player相关联，但它也可能与系统中我们不需要的东西相关联。一般而言，不仅在正常（善意）操作时会产生大量的活动，而且有许多操作是相当的复杂。我们的项目网站列出了当前的白名单。

在生成白名单时所花费的大量时间使我们意识到反病毒研究社区很少把自己的成果与公众共享。因此，当我们设法确认假阳性时，我们把自己的结果与公众共享，使每个人都可以理解对于一种特定的操作系统（例如Windows XP）而言什么是“正常”行为。

不可思议的是，即使是熟练的安全研究人员也觉得确认系统上的一个状态改变是否为恶意事件是件非常困难的事。如果经验丰富的研究人员也难以区别好坏，我们怎么能够期望Internet上的其他用户能够知道得更多呢？这也是目前客户端的漏洞攻击非常普遍的原因之一。

8.4.2　Honeyclient数据的存储和关联

在早期开发第1代原型的时候，我意识到Honeyclient技术背后的真正价值之一是漏洞数据库和它可以生成的被动持网站。这个数据库将存储与每个Honeyclient劫持相关联的信息，包括文件系统的改变、注册表键的修改以及远程Web服务器被Honeyclient访问时发生的进程执行。这个数据库记录了当这些系统变化发生时所访问的URL，并包括一个时间戳，标记这些事件是什么时候发生的。开发队伍的成员Matt Briggs接手了实现漏洞攻击数据库的任务。

在开发漏洞攻击数据库的过程中，我们意识到可能需要存储与漏洞攻击本身并无关联的其他信息，例如被劫持映像的VMware标识号。

我们还希望能够在这个数据库中存储Honeyclient所驱动的应用程序的相关信息。例如，用IE 6和IE 7访问一个网页时很可能会得到不同的结果。（我们的原型现在也支持Mozilla Firefox浏览器。）使用基于Windows XP的Honeyclient和基于Windows Vista的Honeyclient很可能也会得到不同的结果。我不想进一步展开到其他可能性，例如驱动不是基于HTTP的应用程序。这个数据库允许我们存储所有与不同的环境和结果相关的信息，并允许我们对不同OS环境中驱动不同应用程序所产生的结果进行关联。

简而言之，我们的数据库不断成长。它不仅仅是个漏洞攻击数据库，还应该被称为Honeyclient数据库。

这向我们提供了另一个理由，为什么采用模块化的Honeyclient体系结构是个很好的思路。由于我们按照模块进行开发，因此可以允许用户决定把哪个应用程序发送到Internet上。

8.5　漏洞攻击的分析

现在，让我们讨论在操作Honeyclient时所发现的一些有趣的恶意软件，并讨论在检测它们时所遇到的困难。

大多数恶意软件都受到经济利益的驱动。因此，我们看到大量新的恶意软件变型为游戏木马，把用户的游戏账户信息发送到攻击者的计算机内。我们经常看到的另一种类型的恶意软件是银行木马，它允许攻击者访问受害者的在线银行账户资格证书。此外我们甚至看到过政治利益驱动的恶意软件，攻击者通过在用户的桌面上安装和（或）显示HTML文件，以便宣传他们的政治主张。

在我们所看到过的恶意软件中，其中最有趣的也许是那个能够检测到我们正在使用VMware、然后在几秒内关闭访客操作系统的恶意软件。大量的恶意软件能够检测像VMware这样的虚拟平台。是否还存在其他的Honeyclient实现，能够利用不同的虚拟化平台？这个问题将在8.6节讨论。

当我在过去几年里从总体上探索客户端漏洞攻击时，我很清楚地意识到攻击者都是极端的机会主义者。当前客户端漏洞攻击的主要目标是Microsoft的IE 6浏览器，但这个情况可能随市场而变化。许多人问我，是不是改用Mozilla Firefox浏览器更好一点。我的回答差不多总是类似的：“确实如此。但是要记住，如果世界上的每个人都使用Firefox，就会出现大量把目标瞄准Firefox浏览器的漏洞攻击。”换句话说，不要以为使用Firefox就可以免受这类攻击。用户仍然需要保持一点老练，不要随便点击来路不明的链接。

对反病毒软件有什么看法呢？直到我们开始用Honeyclient捕捉大量的恶意软件的时候，我们才真正意识到反病毒行业所面临的任务是多么的艰巨。首先，大多数反病毒产品是以签名为基础的，这意味着反病毒公司要隔离每个攻击，并找到一个嵌入的字节字符串集合，能够唯一地标识这个攻击。由于它们的产品是基于签名的，因此反病毒厂商需要在某个层次上理解攻击的原理，以便检测到攻击。开发一个签名可能非常简单，就是针对恶意软件运行一个字符串工具，提取它们之中都出现的特定唯一的一个字符串。开发一个签名也可能非常复杂，涉及许多复杂的搜索。但是，签名的开发并不是本章的焦点，如果读者对这个话题感兴趣，可以参阅其他关于这个话题的书籍[1]。

要为每个新的攻击都开发签名并跟上所有这些攻击的步伐是极为困难的。注意，我在这里所说的是新的恶意软件变型，而不仅仅是新的漏洞攻击。每天都有数以千计的新恶意软件变型诞生，而反病毒公司所拥有的资源数量却是有限的。因此，我们用Honeyclient所捕捉的大量恶意软件在主要的反病毒公司那里并没有得到很好的检测。被积压下来的有待开发的签名数量极为庞大，有时候导致反病毒公司在发现新的恶意软件之后又过了几周才实际开发它的签名。这样，它们的用户就没有得到及时的保护。更有甚者，有些恶意软件一直就没有开发出有用的签名。

我们的项目所缺乏的东西之一就是专业的恶意软件分析队伍。当前我们并不拥有这些资源。当我们用Honeyclient捕捉恶意软件时，我们通过文件系统修改、注册表键修改和进程执行的形式得到了实用的信息。我们甚至提供了一个模块，当攻击发生时它在Honeyclient和远程Web服务器之间捕捉完整的网络流量包。但是，我们的原型并不是自动化的恶意软件分析工具。这意味着我们无法自动对被捕捉的恶意软件进行逆向工程，以确定与这种恶意软件相关的更多信息。当我们捕捉到新的恶意软件时，我们能够采取的方式是使用诸如VirusTotal.com（http://www.virustotal.com）这样的工具扫描恶意软件。VirusTotal使我们能够对Honeyclient所捕捉的恶意软件有个大概的了解。VirusTotal在后端管理反病毒扫描工具，这些扫描工具都用最新的检测签名进行了更新。图8-3显示了一种典型的通过VirusTotal接口对恶意软件所进行的扫描。

我们可以看到，只有极少数的反病毒产品对这个文件产生了怀疑。显然，这个恶意软件被隐蔽了。隐蔽是恶意软件作者所使用的一种流行技巧，用于绕过反病毒扫描，避免安全研究人员对恶意软件本身展开进一步的分析。这就是为什么图中有些反病毒厂商把这个恶意软件加上了“Pakes”名称的标签。这就意味着这个恶意软件被隐蔽，反病毒扫描程序无法使它现形或者对这个恶意软件进行解包。

图　8-3　VirusTotal对恶意软件扫描的输出

[1]我特别喜欢的一本书是Peter Szor的《The Art of Computer Virus Research and Defense》（Symantec Press）。

8.6　当前Honeyclient实现的限制

Honeyclient的检测并不是完美的。在信息安全领域，没有任何解决方案能够及时防止100%的攻击。如果你遇到有人在这方面夸下海口，你就要提高警惕。在操作我们的Honeyclient原型时，我们发现了它的一些限制。如果一个恶意网站在针对Web浏览器客户发动攻击之前插入一个延迟，Honeyclient就无法检测到这个攻击。这是由Honeyclient的操作方式所决定的，在网页内容被渲染并且没有检测到可疑的修改后，Honeyclient就会转移到下一个URL。

许多网站提供了横幅式广告，我们知道有些攻击者购买了横幅广告空间并把恶意代码嵌入到活动广告中。活动广告基于JavaScript或Flash技术，并不需要用户点击广告就能显示动态内容。因此，用户的计算机可能被这类活动广告利用Web浏览器的漏洞而被劫持。任何网站都可能包含这类广告，包括我们熟悉并信任的流行网站。我们的Honeyclient原型能够检测这些恶意广告，而且我们在操作时已经检测到了这些广告。但是，如果我们和其他用户同时访问一个网站，我们所看到的广告可能和其他用户所看到的不同。因此，当恶意软件嵌入在循环播放的广告中时，Honeyclient就难以检测。当前我们致力于更精确地检测网页中导致攻击的特定组件。如果能够做到这一点，就可能会帮助我们确认哪些广告公司更容易把广告租给恶意方。

当我们测试第2代Honeyclient原型时，我们遇到了大量需要用户交互和确认才能执行和劫持计算机的恶意软件。这种类型的恶意软件可能是可执行（.exe）格式，点击这种.exe链接将会导致Windows IE浏览器询问用户是否允许安装或下载这个.exe文件。对于这种询问，我们很难（至少在当前）开发出一种自动的“确定”或“下载”点击机制。因此，我们意识到Honeyclient的主要目标是检测由下载所驱动的恶意软件，而不是经过用户的允许才能执行的恶意软件。由下载所驱动的恶意软件并不是所有客户端攻击的主体，但它们更为微妙（一般的用户很难阻止），在未来的数年中，它们将会进一步流行。

8.7　相关的工作

尽管Honeyclient相对于honeypot、反病毒和入侵检测系统（IDS）而言是一项相对较新的技术，但当前至少出现了好几个独立的Honeyclient项目。各种不同的Honeyclient的开发目的各不相同，但大多数把注意力集中在检测恶意网站上。

当我开始着手第1代的开源Honeyclient时，Microsoft也在开发Honeyclient（他们称之为Honeymonkey）。Microsoft和我各自平行地开发Honeyclient，直到我完成了第1个原型时才知道Microsoft也在开发Honeyclient，对方也是如此。遗憾的是，我无法从Microsoft那里获得Honeymonkey的内部信息，Honeymonkey并没有提供太多与实现细节有关的信息[1]。作为研究人员，我认为不管怎么强调与研究同一种技术的其他人交流的重要性也不为过。有些与我有过交谈的人们对这个问题感到疑惑。一方面，如果他们分享自己的思路，也许可以会发现其他对这个想法有所考虑的人，这样有助于对自己原先的思路进行优化。另一方面，他们担心一旦披露了自己的想法，其他人可能会剽窃他们的思路。但我相信很少会有人抱着这种异想天开的想法，甚至不认为世界上真正会有这样做的人。到了最后，抱有保密想法的人最终会发现其他人也抱着同样的想法，甚至比他们走得更远！即使某个人首先想到了什么，他也可能失去申请有些专利的机会，因为他不想与任何人交流而导致无法完善自己的思路。

许多反病毒公司操作软件的方式与Honeyclient相似，主要是为了收集恶意软件以便后面的签名开发。在大多数情况下，这些反病毒公司更倾向于讨论恶意软件特征而不是Honeyclient技术。我和Websense的Dan Hubbard谈论过他们所操作的Honeyclient。Robert Danford也是一位Honeyclient研究人员，他实现了Pezzonavante Honeyclient[2]。通过以前与Robert的谈话，我知道他的主要精力放在收集恶意软件，在更可能短的时间内收集尽可能多的URL。这是一种有效的方法，但它牺牲了确定导致攻击的特定URL的准确性。

2005年下半年，Dublin City大学的Aidan Lynch和Daragh Murray在我原先的开放源代码的Honeyclient的基础上开发了一个新的扩展。Lynch和Murray的扩展允许用户使用Outlook收取电子邮件URL，并把它们发送回Honeyclient。这就允许Honeyclient用户使用电子邮件作为URL的另一来源。他们还添加了一个特性，允许对新衍生的进程进行完整性检查。他们的源代码当前位于我们的4台服务器之一[3]。

Thorsten Holz通过他在German Honeynet项目上的工作接触了Honeyclient技术。Honeynet项目把Honeyclient称为“客户端的honeypot”，这主要是因为他们是honeypot技术的先驱者之一。由于传统的honeypot是服务器端的，因此他们将选择“客户端”这个术语表示他们的其他honeypot。但是，通过过去几年里与许多人的交谈，我注意到很多人把Honeyclient和honeypot混为一谈，尽管这两项技术所处理的领域并不相同。因此，在描述Honeyclient时加上“honeypot”这个词很可能并不合适。

2006年，通过Thorsten Holz，我们的Honeyclient项目队伍开始与Christian Seifert以及他的Capture客户端Honeyclient队伍展开讨论。Capture的设计目标是成为一种高交互性的Honeyclient，这点和我们相似。从本质上说，它作为一种善意的工具包安装在Honeyclient系统中，在Honeyclient启动后开始监视文件、注册表键和进程的修改。这样做的好处有很多，其中最重要的一个好处是通过Capture的完整性检查，我们能够减少检查系统修改所需要的时间，从3到4分钟减少到不到5秒。

由于我们的Honeyclient项目与Capure项目共享一个公共的GPLv2许可，因此我们能够相互利用对方的代码，而不涉及法律问题。我们能够开发一个包装器脚本，通过模块调用Capture。Capture代码也添加了我在前面所提到的进程检查特性。

从那时开始，还出现了其他Honeyclient实现[4]，尤其是在低交互端。前面所提到的实现都是高交互的Honeyclient，实际上把Web浏览器作为一个进程予以驱动。低交互Honeyclient模仿了Web浏览器，而真正的浏览器不被感染。低交互Honeyclient的例子包括Niels Provos所开发的SpyBye（http://spybye.org）。Google也采用了Honeyclient操作，用于寻求不良网站并根据这些URL创建黑名单。这个黑名单是Google安全浏览API（http://code.google.com/apis/safebrowsing）的组成部分，当前它已被集成到Mozilla Firefox浏览器中。最近，Jose Nazario发布了PhoneyC（http://svn.mwcollect.org/log/phoneyc），它的重心在于自动浏览器脚本的现形和分析。

[1]关于Microsoft的HoneyMonkey项目的讨论可访问http://research.microsoft.com/HoneyMonkey。

[2]关于Pezzonavante的描述可见http://handlers.dshield.org/rdanford/pub/Honeyclients_Danford_SANSfire06.pdf。

[3]源代码位于http://www.synacklabs.net/honeyclient/email-honeyclient.zip。

[4]参见Wikipedia关于Honeyclient的条目：http://en.wikipedia.org/wiki/Honeyclient。

8.8　Honeyclient的未来

当前Internet上的网站已超过2亿4千万个（而且这个数字正在不断膨胀），没有任何组织可以用Honeyclient技术覆盖所有的网站。为了更好地与破坏计算机和窃取数据的攻击者展开斗争，我们需要团结在一起，相互吸收对方的经验。

我们应该预见到，未来Honeyclient会采取一种SETI@home（“在家里搜索外星智能”）方法，也就是每个Honeyclient能够处理它自己的数据并把它发送回一个中央数据仓库，由后者对数据进行更有效的关联。这可以帮助我们确认目标域：例如，A公司的员工被B公司当做攻击目标，以劫持某个人的计算机并窃取公司的文档。

现在，我们主要支持Web浏览器，但我的兴趣在于寻找一种对等（P2P）的Honeyclient。P2P网络存储了大量的恶意软件，驱动一个P2P应用程序可以成为一种非常有趣的寻找新的恶意软件的方式。我们可能在未来实际开发一个P2P Honeyclient。当前，我们的时间都被寻找新的和有趣的Web的恶意软件所占据。

如果一般用户所做的事情只是打开浏览器并在Internet上寻找一些信息，那么他们不应该生活在恐惧之中。我们已经看到许多用户由于安装了基于签名的反病毒扫描程序而认为自己是完全安全的，不会受到攻击。我们应该帮助保护这些用户，其中有许多用户实际上已经遭到了攻击，并不是因为访问了一个“隐蔽”的网站，而是因为他们访问了一个以为安全但实际上包含了恶意广告的网站。

在IT安全领域，对客户端漏洞攻击的理解要比对服务器端的理解大约滞后5年。作为一种产业，我们在最近才开始更好地理解客户端的漏洞攻击是怎么发生的。显然，还有大量的工作需要完成，我相信Honeyclient技术将成为这个过程中的一个重要组成部分。共享代码和结果将是一个关键的前进步骤。

第9章　未来的安全齿轮和杠杆

如果不改变思维模式，就无法解决我们用当前的思维模式所创建的问题。

——阿尔伯特・爱因斯坦

信息安全并不仅仅和技术有关，它依次关系到人、流程和技术。更准确地说，它把人、流程和技术结合起来，使人和整个系统可以作出明智的决策。在专门讨论安全管理技术未来的一章中，首先用一句话把技术的身份牢牢地固定在第三名的位置，初看上去有些奇怪。但是，我觉得定好这个基调是非常重要的，它可以为本章剩余部分的讨论提供合适的上下文环境。

这并不意味着我贬低技术在安全中所扮演的角色。我坚定地认为，我们在信息技术革命的道路上才刚刚起步，信息技术革命影响我们的生活方式是我们很难想象的，更不用说作出预测了。人们很容易对未来主义的思维嗤之以鼻。许多人仍然对20世纪七八十年代描述的将来自已认路的气垫车能够在早上自动送我们上班，并且当我们下班回家后，像人一样的笨重机器人会为我们调好鸡尾酒之类的预测嘲笑不已。但是，根本性的技术突破就在眼前，它们将闪现未来技术发展的火花。

其中的一个火花让我更坚定地认为我们尚处于一条指数级技术曲线的起点，它就是编程语言的发展以及用人类无法匹敌的速度对自身进行完善的人工智能。稍微考虑一下：编程语言本身可以创建更好的语言，而后者又可以创建更好的语言，接下来以此类推。软件可以通过自我学习，对自身进行重新编程，使自己变得更好，还能设计和解决那些我们根本无法想象该怎么解决的问题（假设我们知道这类问题的存在）。许多人质疑医学伦理研究，强烈抗议可能改变地球的人类克隆。但是，他们很可能把目光瞄准了错误的问题。

当然，信息安全以及它与技术的关系可以追溯到远古。埃及人在石碑上镌刻了令人费解的象形文字，斯巴达人使用一种称为密码棒（缠在树枝上的皮革）的工具传达军事计划，罗马时代的恺撒密码更是记录在许多学校的教科书上。许多历史学家把第二次世界大战的胜利直接归功于Bletchley Park的密码破解者，他们破解了著名的Enigma密电码。但是，即使是这个导致第二次世界大战结束并永远改变了历史的重大事件，相比即将发生的变革仍然显然无足轻重。

Donald Davies在1970年所发明的包交换网络永远地改变了世界，以前互不相关的计算机之间突然可以对话了，这就为以前彼此隔绝的计算能力开创了新的可能性。虽然几乎一个世纪之前的电报网络已经唤醒了人们通过电子连接的星球的梦想，但是直到20世纪70、80和90年代，人们才开始用铜缆和光纤技术确确实实地把世界连接在一起。今天，网络的发展已经进入了一个新阶段。物理位置之间不再是用双绞铜缆连接在一起，而是通过面向服务的架构（SOA）把软件应用程序和服务连接在一起。同样重要的是，它把人们连接成一个复杂的社交网络，创建了一种全新的人际关系。

虽然对未来进行预测是件愚蠢的事情，但我常常发现自己在思考信息安全的未来趋势，思考信息安全对未来的潜在影响，这是两件明显不同却又密切相关的事情。简而言之，我相信未来的信息安全将与现在相对原始的操作方式大为不同。

与电子游戏、金融投资或医学研究软件相比，当前人们可以使用的安全工具和技术可以用“原始”来形容。大量的现代多玩家游戏建立在复杂的物理引擎的基础上，模拟现实世界的运动，利用了能够提供与人相似的交互的高级人工智能引擎，并且同时把几百甚至几千个玩家连接在一起，构成一个高度复杂的虚拟世界。支撑投资银行的金融管理软件对来自公共和私人来源的海量数据集合执行“超级快速”的计算，并根据数以万亿计的数据创建先进的预测模型[1]。医学研究系统分析遗传疾病的复杂DNA模式，预测整个人口继承基因特性的遗传概率。

与此形成鲜明对比的是，本应肩负起保护数十亿美元的财富，确保行业秘密的安全免遭商业间谍的窃取，隐藏军事计划使它远离全球恐怖分子的肮脏触角等职责的信息安全管理计划往往是由Excel电子表格、Word文档、自行编制的脚本、备忘笺、电子邮件系统、咖啡杯壁上所张贴的备注以及走廊上的谈话这样杂七杂八的方式完成的，其原始程度不亚于第二次世界大战时用于破译密码的Rube Goldberg机（英国人的叫法是Heath Robinson机）。因此，当我们继续看到前所未有的安全风险管理危机，安全人员感觉他们是在黑暗中摸索时，就丝毫不会感到奇怪了。如果信息安全要跟上形势（确实需要如此），人、流程和信息安全技术（这是本章的焦点所在）就需要有所发展。安全专家们今天所窃笑的好莱坞式的安全在明天将成为现实。

我对自己能否在塑造未来的安全技术中扮演一个角色而感到悲观。对我来说，这涉及被我称为“明天的安全齿轮”的定义和创建。本章将讨论我认为将对安全行业产生重要影响的技术趋势，并探索了如何对它们进行整合，创建信息安全风险管理系统，帮助我们更快、更好、更经济地完成任务。我们可以通过无数种方法对技术进行分解，但高级的技术最终要实现这三个目标，那就是更快、更高、更经济。

我把本章的内容划分为几个核心话题：

·“云计算和Web服务：这里是单机”

·“结合人、流程和技术：业务流程管理的潜力”

·“社交网络：当人们开始通信时，大变革来临了”

·“信息安全经济：超级运算和网络新规则”

·“长尾变型平台：为什么未来会如此改变？”

在正式介绍之前，我们分享Upton Sinclair的一句妙语，Al Gore在他那部关注气候变化的纪录片《An Inconvenient Truth》中巧妙地引用了这句话。我在公众场合进行演讲时，常常把这句话放在幻灯片中：

如果一个人就靠不理解某样东西领取薪水，就很难指望他理解这样东西。

挑剔的听众会追问为什么把一些老生常谈的、略带偏见的想法和思路当成是事实灌输给他们。坦白地说，写作本书的时候我供职于Microsoft。我所在的工作小组“连接信息安全组（Connetect Information Security Group）”长期专注于推动前面所讨论的这些主题。本章所阐述的只是我个人的观点（或许是我的偏见），但是这个小组的工作成果取决于未来的措施与本章所阐述的想法的接近程度！

9.1　云计算和Web服务：这里是单机

文明的进步常常是通过增加不假思索就可执行的重要操作而实现的。

——Alfred North Whitehead《数字入门》（1911）

当前，媒体大量渲染“云计算”的威力。至少最近五年内，计算机行业对Web服务表示了极大的兴奋。Web服务的范围从软件即服务（SaaS）到各种基于Web的API以及面向服务的架构（SOA）。

事实上，云计算只不过是从硬件系统或用户那里所抽象出来的计算基础设施（包括存储、处理能力或应用程序主机服务）。就像我们把照片上传到Flickr之后并不知道它们存储在什么地方一样，我们可以通过一个能够自由地在它所选择的任何系统上运行的服务运营整个商业。因此，软件的部分或全部是在“云中”运行的。系统组件和系统用户并不需要知道（坦白地说，也并不介意）实际的计算机位于什么地方或者数据实际存储在什么地方。他们所关心的只是系统的功能，而不是使系统可以运行的基础设施，就像一般的电话用户并不关注线路交换的细节或者信号传输时所经过的电缆类型一样，他们只关心怎么和对方通话。

尽管云计算只是其他类型的在线服务和主机服务的一种自然扩展，但它在全球网络的发展历史中却具有极端重要的作用。云计算可以使计算能力大众化，可以被所有背景的软件创建者所分享，向他们提供能够把思路转变为现实的超级计算机。有些人可能觉得这仿佛是回到了所有用户分享大型机调度时间的老日子，似乎云计算也没有什么新意。但是，这种想法是错误的，关键在于当今这个特别时期，任何能够访问Interent的人都可以使用超级计算机。

Web服务是基于标准的架构，并提供独立于运行它们的基础设施的资源（一般是不相干的应用程序功能片断）。Web服务允许很多网站通过提供基于标准格式的功能和公共API，把它们的应用程序和数据经济地集成在一起。SOA是紧密相关的Web服务的集合，提供了各种功能的集合。SOA是一种催化剂，允许把强大的计算基础设施与软件功能、数据和用户连接在一起（在以前的时代，这意味着“用线连起来”）。

我并不属于那种轻视云计算或Web服务的影响力的怀疑者。我相信它们将会带来模式的改变，从根本上改变我们思考和使用Internet的方式。它对其他任何领域的影响都不如安全领域那样深刻，并且它在安全社区引发了争议。

9.1.1　创建者和破坏者

安全社区的人们可以分为两种主要的类型：

·建设者通常把玻璃杯看成是半满的。虽然他们认识到漏洞的严重性以及当前做法所存在的危险，但他们通常持乐观态度，相信通过改进他们能够改变的状态，使世界变得更加美好。

·破坏者通常把玻璃杯看成是半空的，常常持非常悲观的态度。如果你经常聆听他们的观点，就会奇怪为什么Internet到现在还没有崩溃，为什么我们的银行账户上的钱还没有被偷光。这种悲观态度导致他们把展现脆弱和失败的行为艺术带入到当前的方法中。

每隔几年所发生的一件大事都会使安全社区的人们分裂为两个阵营。显然，我认为自己属于建设者。

海量计算能力的虚拟数字云，加上将它吸进吐出的虚拟管道（Web服务），引发了破坏者通过几十年的经验所建立的怀疑。当你掠过安全“守旧派”的冰冷水面时，很可能会看到他们得意的嘲笑和世故的眼神，以及轻蔑的断言“你无法保护你无法控制的东西”，“你无法修补并不属于你的数据中心”，甚至是愤世嫉俗的呐喊“你为什么要信任别人能够做好像安全这样重要的事情呢？”

我听到过所有这些论断，当然也难以辩驳。对于云计算中的主机服务和数据处理，这些确实也算是合理的批评意见。但是，破坏者们只是把标准的理由应用于旧式的技术，他们忘记了历史上人类的一个关键特性。那就是当优点超出缺点之后，事情总是能够获得成功。拥有大量需求的可伸缩资源的经济优势、访问几乎不受限计算资源的技术优势和服务行业（从提供商品的餐馆到银行）的良好文档趋势，使云计算机的优点远远超出了它的缺点。

我深深地理解破坏者所持有的一个理由，它也是我在自己的职业生涯的某个时期曾经萌发的想法。2002年，我加入了一个叫做Foundstone的漏洞管理公司（现在归属McAfee旗下）。这家公司出售一种网络漏洞扫描程序。它以客户应用程序的形式按照传统模式运行，把所有的数据本地存储在顾客的系统中。我们的主要竞争者是一家称为Qualys的公司，对方在自己的系统中以服务的形式提供了一个网络扫描程序，把数据集中存储在他们的设备中。我们把托管安全数据列为一个巨大的风险，成功地说服顾客购买我们的产品。坦率地说，我们促成了FUD（恐惧、不确定和怀疑）。当时大多数顾客都同意这一点，并且它也成为我们赢取利润的关键差别，并帮助我们把公司卖给McAfee。我在Foundstone期间的工作是得到回报最多的。但是当我回头审视时，我仍然觉得卖掉公司的时机可以说是掌握得令人难以置信地准确。过了没几年，风向就完全变了。我们发现越来越多的顾客不仅开始接受在线模式，而且主动要求这种模式。

当然，对于一般的消费者，情况也是如此。超过500万的WordPress博客用户已经用无形的双手进行了投票，他们通过在线的方式撰写博客。估计全球10%的终端用户Internet流量来自于主机托管的、基于Web的电子邮件，例如Yahoo！Mail、Gmail和Live Mail。Microsoft则在称为Azure的云操作系统上进行了巨大的投资，并创建了庞大的数据中心基础设施，为软件和服务提供主机服务。Amazon已经租掉了自己的部分基础设施，以便支配在线零售空间。

[1]当然，有人可以辩称2008年的信用恐慌应该可以被预测。失误的原因应该是提供给程序员的先决条件，而不是程序的先进性有问题。

9.1.2　云计算和Web服务是拯救方案

安全专家应该被问到的问题并不是“云计算和Web服务可以做得安全吗？”，而是“我们如何在这种新方法中实现安全？”或者换种更机智的说法，我们应该思考：“如何把自己的优势融入这种模式？”

好消息是在Web服务和云计算中实现安全并不像人们想象的那么困难。这个初看上去非常吓人的任务实际上只需要对模式进行改变就可以完成。向我们提供服务的公司还必须保证我们的安全，并不仅仅是合法就行了。

为了让读者理解这些新服务对于安全而言是个福音而不是威胁，让我们把注意力集中在一个现实世界的场景。圣诞节期间，我在家里安装了一套全新的Windows Home Server。突然之间，我们都沉浸在数字世界里：我们可以通过Xbox在卧室的电视里上观看数以千计的孩子照片和视频。散布在家里的六台计算机和一台中央服务器放在一块，整个家庭感觉被连接在一起。和中央服务器放在一块当然很好，但是如果我们遇到了抢劫，有人偷走了我们的PC和服务器，那情况可就糟糕了。

现在，让我们进入Web服务和云计算的新世界。为了缓解灾难性系统损失的风险，我为家庭服务器编写了一个简单的插件（参见9.5节），它利用了Amazon的Web服务平台。系统根据设定的间隔把我所选择的目录复制到服务器并通过Web服务连接到Amazon的S3（简单存储系统）云基础设施。服务器把我所选择的数据的一个备份拷贝发送到云中。我利用了Web服务的WS-Security规范（以及一些其他规范），保证数据被加密，不会由于传输而被篡改。我使用了一个X.509数字证书，保证通信对象是Amazon。为了进一步保护数据，我先在本地对数据进行加密，然后再发送给Amazon，这样即使Amazon遭到黑客入侵，我的数据也不会被暴露或者被更改。整个解决方案只花了30分钟就搞定，这得感谢Internet上的一些可复用的开放源代码。

因此，在其他人的服务器上存储自己的私人数据初看上去令人不太放心。但是，经过深思熟虑并把众所周知的实践应用于新模式后，我们将会认识到这种改变并不像我们一开始想象的那么剧烈。守旧派所持的“你无法控制位于自己控制范围之外的物理服务器的安全”的说法也许是正确的，但我们的解决方案是把安全应用于系统中的不同地点。

另外值得指出的是，使用其他计算机的服务这个概念并不是新东西。我们每天都在使用其他人的服务器所提供的DNS服务。当我们克服了破坏性技术一开始所带来的震惊之后（先不考虑破坏者的权限），我们就可以把思路转移到对云计算和Web服务的重要讨论中，并接受这些新技术可以做到的事情。

9.1.3　新曙光

在本章的“结合人、流程和技术：业务流程管理的潜力”一节中，我讨论了超级信息处理（supercrunching），这个术语用于描述对大量数据集合进行海量分析以推导出结论的过程。我认为超级信息处理在未来系统中将扮演重要的角色。当前，我们不得不接受局部存储和局部处理所带来的限制，这种限制超出了我们的想象。如果我们学会在Interent连接的服务所允许的规模巨大的空间里解决问题，就可以取得新的成功。

这个原则可以在安全监控方面取得收益，使我们不必为如何保持云之外的安全以及如何把云转变为安全革新的来源之类的问题所困扰。

事件日志可以提供令人难以置信的大量证据信息，允许我们重新构建一个事件。问题可以简单到如哪位用户重设了一个特定的账户密码，也可以复杂——如哪个系统过程读取了用户的标记。当然，目前存在许多日志分析工具，甚至出现了一整类的安全工具，称为安全事件管理器（SEM），但它们远远没有达到超级信息处理的功能。当前的工具运行于标准的服务器上，由相当标准的硬件执行相对原始的分析。

几年前，我还为自己参与创建了一个系统而自豪。当时，我供职于San Francisco的一家大型金融服务公司。这个系统的数据存储量高达TB级，并且使用了Sun公司的一些功能强大的硬件。我们认为这个系统是最前沿的，当时确实如此！时至今日，如果我们使用新的连接计算模型，照样可以使用这种级别的计算能力和存储容量。这个计算模型允许我们存储巨量的安全监控数据以便进行分析，并使用巨大的处理能力执行复杂的分析。

接着，我们可以从大量的数据集合中查找模式并推断它们的含义，对安全事件作出预测而不是对它们作出反应。我们需要正确地理解这个结论：我们将能够根据一个特定的事件预测第三方事件的发生概率。这就允许我们提供上下文敏感的安全或者作出与合理措施相关的决策，以阻止麻烦的发生。

在本章的“社交网络：当人们开始通信时，大变革发生了”一节中，我讨论了社交网络。当人们开始高效地与他人进行通信时，社交网络将会对安全产生深远的影响。与同伴共享前面所提到的日志文件并对大量的数据集合进行分析，并作出更准确的预测。

另外值得注意的是，云服务独立于任何参与方，因此可以是中立的，可以作为公正的推动因素。长期以来，公司们已经可以划分它们的网络，允许受信任的第三方的有限访问，或者提供一个双方均可访问的代理工具。对于利润丰厚并且具有重要意义的身份管理领域的大量竞争者而言，这种做法并没有被丢弃。像OpenID和Windows Live ID这样的身份管理服务是通过云计算进行操作的，允许它们跨域绑定用户。

9.2　结合人、流程和技术：业务流程管理的潜力

事实上，几乎每家公司都会想方设法为它们的员工提供一些工具，但通过这些工具所收获的价值上的巨大差别取决于公司如何审视自身，思考它们的业务流程、思考它们的业务怎么改变、思考它们的项目管理以及顾客的反馈。它们的计划周期可能与以前截然不同。

——比尔・盖茨

纽约时报专栏作家Thomas Friedman在2005年写了一本非常优秀的书《The World is Flat》（Farrar、Straus and Giroux出版）。在这本书中，他对外包革命进行了探索，从印度的呼叫中心和中国的纳税申报表格处理到澳大利亚的放射照相术分析。现在，我生活在Friedman所描写的平面世界中。事实上，当我写作本书时，我正坐在前往海德班德的飞机上，拜访我的开发小组的部分成员。我的当前小组的工作基地分布于美国（Redmond）、欧洲（伦敦和慕尼黑）、印度（海德班德）和中国（北京）。当前，很多媒体在关注中国和印度的熟练劳动力队伍的壮大，感叹他们向西方世界所提供的商品和服务。但是，当我们回溯历史时，就会觉得这个现象并没有什么特别之处。Friedman的著作指出在世界历史上，每隔500年就会完成一次经济实力的转变，而这种转变总是受到贸易的推动。那么，是什么促进了贸易的增长呢？很简单：连接和通信。从中国的丝绸之路到铺满硅谷的暗光纤，联系供应和需求、交换商品和服务的基本原则仍然兴旺发达。我们所感兴趣的东西（Friedman接下来所阐述的）是当今世界的工作流软件已经成为了一个关键的“校正机”，这意味着通过Internet传递电子数据的能力允许加速这些特定的全球市场的转变。工作流软件，或更准确的说法，业务流程管理（BPM）软件是工作流设计、编排、商业规则引擎和商业活动监控工具的结合，它将极大地改变我们审视现代商业软件的安全的方式以及采用信息安全管理本身的方式。

9.2.1　发散型世界的发散型安全

在平面的世界中，劳动力是分散的。它并不像工业革命一样把工人们聚集在办公室或工厂中，而是根据项目进行组合，在很多情况下通过Internet把个体组织成队伍。

许多安全原则建立在实体办公室或者物理或虚拟网络的基础上。有些技术（例如流行的文件共享协议，如公用Internet文件系统[CIFS]和基于LAN的同步协议，例如地址解析协议[ARP]）理所当然地接受了这个本地环境。但是，当任务、消息和数据穿越松散耦合的节点网络时，这个基础就显得不合时宜了。

它的效果类似于全球商业的效果，它使得在繁华街道两侧租赁店面房或者在繁忙的港口或火车站开设银行的优势变得荡然无存。任务是通过复杂的业务规则引擎传递的，例如确定呼叫中心的一条消息应该被交换到印度还是中国，或者确定一种特定商品最便宜的供应商是否还有库存。

BPM软件极大地改变了供应链的组成，还可以根据动态商业条件对供应链进行动态的重新配置。涉及许多公司的商业交易可能只需要几毫秒就可完成，也可能需要耗时数年方能结束。当技术演变时，业务流程一般都会脱胎换骨，以发现新的服务。这种工作方式的复杂性和影响力只会有增无减。

当然，对于信息安全而言，这带来了引人注目的新挑战。在几千年的历史中，人类出现过许多与物理位置相关联的安全。他们在山峰上建筑具有高大城墙的城堡，并挖壕沟包围自己。他们在门上和文件柜上安装了物理装置的办公室里工作，并把他们的钱放在银行保险柜里（对于现在而言显得新奇），对在他们的办公室或数据中心中的档案（包括计算机中所保存的）加锁。实现这个概念的Internet安全使用防火墙和包过滤器，在穿越公共网关时检查网络流量。

今天，当公司们努力处理不断发展的商业模型时，像Jericho Forum这样的小组是“反边缘化”思路的翘楚。现在的公司很少是由坐在同一物理位置并受相同的规则约束的全职员工所组成。现在的公司往往是员工、商业伙伴、外包公司、临时承包商（有时又称“临时销售商”）以及其他可以想到的各种实体所组成。他们可以位于北京、班加罗尔、曼哈顿和菲律宾。他们受到不同法律、文化、政治的制约，当然还包括真实和假想的安全紧急情况。公司的防火墙对于保护记录在公司网络中的PC不再是必需的，偶尔玩玩星际争霸也无伤大雅。事实上，公司网络这个概念本身也受到应用的侵蚀，它们要求公司的网络通过面向服务的体系结构由Web服务和消息系统与外部世界进行连接。

当一家公司的知识产权和业务数据穿越各种不同的边界，进入它们的安全控制无法掌控的系统时，就进入了一个包含许多问题和复杂性的全新世界。我们不再有任何自信，不敢确定对自己的安全计划非常有效的安全控制对于位于遥远的印度乡村的代理机构是否依然有效。当来自保加利亚的肉鸡被激活，安装了密钥记录器，并改变了系统流程的完整性时，要求生产商安装最新反恶意软件的纸面合同是没有办法让人感觉安心的。没有比“只有当联系最少时，安全才是没问题的”这句话更适合现在这种场景了。系统架构师必须在“最虚弱的联系确实非常虚弱”的假设下进行工作。

9.2.2　BPM作为多站点安全的指导方针

尽管存在这些明显的顾虑，我相信BPM所涵盖的技术和业务技巧在未来的信息安全管理中将扮演一个非常关键的角色。

例如，如果我们检查当前漏洞管理的常见信息安全流程，就可以很容易地想象这样一个世界：一个具有伸缩性的系统定义了业务流程，把它的各个部分的包袱分给能够更快、更好、更经济地做好它们的人或公司。如果我们分解一个典型的漏洞管理流程，可以把它想象成一系列的步骤（当然，这里只是为了简单举例起见），例如漏洞研究的分析、公司自身数据的分析以及用于确定风险的系统，另外还包括像漏洞修补这样的最终管理动作。

今天，许多公司把漏洞研究这样的任务外包给像iDefense（现在是一家VeriSign公司）或Secunia这样的公司，它们通过XML输入数据，并由公司的分析师所使用。当安全BPM软件（以及支持它的全球网络）出现时，公司不再局限于把这个步骤外包给单个公司，寄希望于这家公司拥有执行正确分析的必要技巧，而是可以外包给一个全球性的分析师网络。BPM软件能够把一个任务分配给一个在某项特定的隐蔽技术（世界上最精通黑客系统X的人，或者理解语言Y的人）上具有跟踪记录的分析师，或者分配给一家能够在一个特定的时间段内返回分析结果的公司。分析师可以躺在马尔代夫的沙滩上，也可以坐在伦敦的办公室里。他们分布在世界上的哪个角落是没什么关系的，除非工作时间和时区是个决定性的标准。

业务规则引擎可以分析资产管理系统，我并决定收取一个来自旧金山的分析查询，并把它路由到中国，使它连夜就可以完成处理，在第二天清晨就为公司的分析师们提供答案。

对安全研究的业务流程的相同基本改变很可能扩展到智能输入动力安全系统，例如反病毒引擎、入侵检测系统和代码审查扫描器。BPM软件可以帮助新的业务模型，对业务流程进行细分，更快、更好、更经济地产生最终解决方案。对于我们所接受的许多安全技术而言，这很可能带来使用方式上的巨大转变，与来自发行机制的内容进行分离。将来，由于BPM软件所提供的安全性，分析师可以选择最好的反病毒引擎和最好的分析输入，但它们很可能并不是来自同一个厂商。

当Nicholas Carr编写《Does IT Matter》（[1]）时，他认为技术需要重新定位，对业务提供支持而不是改变业务。BPM并不是高深莫测的东西（尽管它在未来可能包含一些复杂的东西），但它所做的事情仅仅是：对技术重新定位以支持业务。除了通过开启新市场对信息安全提供了极大的改进之外，BPM还可以通过它对安全背后的科学的演变效果，产生更强大的变化。业务流程管理计划（BPMI）（http://www.bpmi.org）定义了有效的BPM计划的5个原则。这些原则与信息安全无关，但可以被看成是强大的催化剂。下面列出了每个原则以及它们对安全的最重要潜在效果：

1）理解流程、创建流程的文档

安全效果：实现结构化的、有效的信息安全计划

2）理解度量指标和目标

安全效果：理解成功标准并跟踪它们的有效性

3）模型和自动化流程

安全效果：提高效率、减少成本

4）理解操作、实现控制

安全效果：数据的快速、准确顺应和审核（可见性）

5）优化和提高

安全效果：事半功倍

安全效果：减少成本

换而言之，如果我们理解并记录了流程、安全效果和目标，建立流程的模型并使之自动化，理解并实现了流程，优化和改进了流程，就可以实现一个结构化和有效的信息安全计划，理解成功标准、跟踪有效性、提高效率和减少成本、并快速准确的顺应和审核数据，最终在安全方面达到事半功倍的效果。这个意义是非常重大的！

在信息安全中应用BPM的话题可以用整整一本书来描述。当我们思考业务流程的建模、编排、业务规则设计和业务活动建模时，如果不讨论BPM技术对模拟的潜在影响，无疑是个重大的疏忽。当我们理解了一个流程，包括它的活动、过程流和业务规则时，就拥有了一幅强大的描述应该怎样工作的蓝图。当我们从现实世界的流程编排中捕捉业务活动数据时，就拥有了与它的实际工作相关的强大数据。

模拟向我们提供了改变约束并对结果进行模拟的能力，而不需要花费巨量的资源和时间。我们可以用一个安全事故响应流程为例，它用模拟软件预测一定数量或一定类型的事件的结果。我们也可以根据与流程有关的事实，对成功或失败作出预测，然后改变实际业务的约束，获得更好的结果。模拟可以发生在实时，帮助避免人们很可能无法预测的情况。我相信业务流程的模拟将成为一项强大的技术，允许公司更好、更快、更经济地完成任务。接下来，让我们考虑在连接到社交网络之后BPM可以实现的各种可能。

[1]Does IT Matter?，Nicholas Carr,Harvard Business School Press，2004.

9.3　社交网络：当人们开始通信时，大变革发生了

人类在学习他人经验方面具有独到之处（在所有动物中），在表现自己的不情愿方面也是表现突出。

——Douglas Adams

在大海中的一个晚上，C.S.Forester的系列小说中的虚构人物Horatio Hornblower被大副叫醒。大副惊慌失措地告诉他，航道正前方大约20英里处发现有一艘船的灯光，并且对方拒绝移动。Horatio来到甲板上，通过无线电通讯向这艘船发出命令，要求对方立即右转舵20度。对方的操作员拒绝了这个命令，并愤怒地告诉一等男爵Horatio，应该立即右转舵20度是他才对。Horatio Hornblower听了之后暴跳如雷，决定利用自己的职权和船的规模令对方屈服。他向对方宣称，自己是船长，正驾驶着一艘巨型军舰。对方顿时安静下来，沉默一会儿之后平静地告诉Hornblower船长，他所驾驶的才是最大的船，看到的是在怪石嶙峋的悬崖上方的一座灯塔。

我每次讲述这个故事时，它都提醒我人类的通信是件多么麻烦的事。我们都有原罪，但通信对于我们所做的每件事情都是至关重要的。当人们进行通信时，他们就会产生共同兴趣并建立某种联系。

社交网络被认为是Web 2.0（这个是中性术语，描述了下一代的Internet应用程序）的核心。它实际上并没有什么新东西。历史上，人们以种族、宗教和社会或经济价值为纽带，以部落、宗族和社区的形式生活。在任何时候，当社会组织能够更方便地进行联系时，就会有大的变革发生。贸易增加了，思想被传播，新的社会、政治和经济模型形成。

我在2 0 0 1年意外地启动了一个称为开放We b应用程序安全项目（O WA S P）（http://www.owasp.org）的社交网络。我们最初使用了一个电子邮件发布列表和一个静态的网站，用于通信和合作。在前两年，这个项目以稳定的步调前进着。但是，2003年后期随着wiki的出现，任何人都可以方便地进行合作，这个项目才得到了脱胎换骨的发展。今天，OWASP的工作受到联邦贸易委员会、美国标准协会以及引发热议的支付卡行业数据安全标准（PCI-DSS）的推荐。自OWASP成立以来，我学到了很多有价值的生活教训，但是其中最为重要的就是人们所使用的社交网络技术的类型（[1]）。

9.3.1　社交网络的艺术状态和潜力

今天，人们常常认为Facebook和MySpace是前沿的社交网络软件，它们可以把具有某种共同纽带的人们聚焦在一起。和以前相比，世界上的人们可以更方便地进行接触。毫无疑问，这些网站得到了迅速的繁荣，拥有千百万级的订阅者（它们都拥有超过2.5亿的用户），并得到了媒体的宠爱。但是，根据我的观察，它们的绝大部分用户把时间花在和朋友们打打招呼或者在他们的数字水族馆里放几条鱼之类的事情上。对于像我这样的老家伙，就有点像我们祖辈眼里的摇滚乐。我对这些不以为然，但我已经承认自己正在变老。

同样有趣的是社交网络在像Second Life这样的虚拟世界的形成。Second Life具有它们自己的虚拟经济，包括通胀封顶的货币系统，把现实世界的货币交换为虚拟货币的能力以及繁荣的虚拟房地产市场，因此它吸引了许多像IBM这样的大公司的兴趣。最近，研究人员能够通过心灵传输的方式把阿凡达从一个虚拟世界转移到另一个虚拟世界。像OpenSocial API这样的创举表明了社交网络的互操作性正在不断发展。网络的网络很快将会出现！

事实上，像这样的社交平台能够为公司提供的东西甚少，更不用说对安全专业人员了。但是，情况将会发生变化。当情况发生变化时，它对信息安全的影响将会是非常明显。

如果说今天的社交网络是人与人的网络，那么未来的社交网络可能是业务与业务的网络。几年来，纽约的投资银行、美国的政府部门和其实行业组织已经通过小型的私有社交网络共享与安全有关的统计数据。情报社区和各级警察部门使敏感的数据“只有需要的人才知道”。在全球范围内所形成的分布式网络具有更为特定的用途。但是从宏观上说，业务与业务的社交网络是非常有限的。信息安全很少被认为是竞争性的优点（事实上，对遭受严重数据泄露事件的公司的股价走势研究显示，股价与数据泄露的相关性极低，因此有人可能会认为这并不是缺点），大多数商业都认识到合作的优势远远超过了它的预想缺点或真实缺点。

这仍然产生了一个问题“为什么社交网络没有更加流行？”我认为在很大程度上是由于缺乏合适的软件。我们知道，人们很难在得不到帮助的情况下进行在线通信。这一点可以用当OWASP转移到wiki时所发生的化学反应为例。我们可以总结出一个理论，当开发或引进了一种正确类型的社交网络技术时，就会发生我们在历史上所看到的类似行为。

[1]我不能把wiki的创办归功于自己（Jeff Williams才有此资格）。相反，我实际上对它持反对意见，因此我觉得它是非结构化，将会导致内容质量的下降。我得到了一个很好的教训。

9.3.2　安全行业的社交网络

安全行业的未来是很难预测的，但是如果我们把当前成功的社交网络现象运用到安全行业，就可以推断出一些实用的场景。

例如，从eBay所奉行的可靠性原则出发，这家成功的拍卖网站把全球范围内的个人销售者链接到个人购买者。它的成功部分建立在声誉经济的基础之上，社区对销售者的回馈在很大程度上决定了购买者对它的信任程度。

现在，想象一个eBay类型的处理信息安全的业务对业务的网站。它可以列出“所需要的安全服务”广告，允许公司提供服务契约和匹配标准，允许服务供应商根据公共评价以及通过为类似的客户所执行的类似工作所获得的资格证书进行竞标。

eBay类型的系统不仅作为直接连接购买者和销售者的中介，而且具有提供其他参与方可以交易的市场信息中心数据的潜力。以安全软件为例，应该为下一项大型安全源代码扫描技术支付多少钱以便对自己的1200万行代码进行检查呢？将来，我们可能咨询全球数据库，看看与自己相似的其他公司平均支付了多少。

为了让这些类型的社交网络获得成功，它们必须在一个“收费服务”模型上进行操作。虽然从一开始输入足够的数据促使它们采用这种模型有点困难，但我毫不怀疑这样的网络必定会出现。东欧和中国的病毒编写者长期以来一直通过旧式的公告板进行技术交易，我们开始看到漏洞交易甚至漏洞拍卖网站的萌芽。为什么白道就不能像黑道一样利用同样强大的网络呢？

实际上，安全技术疯狂者的大型社交网络已经存在。他们一般使用电子邮件发布列表，这是一种令人惊异的老掉牙的通信方式，但它非常有效。

在2008年的黑帽大会上，一位名叫Dan Kaminsky的来自美国的“研究人员”声称他将要讨论一个严重的DNS漏洞的细节。在接下来的几天时间内，会议上充斥着像“Daily Dave”这样的邮件列表，大家都在猜测这个漏洞到底是什么，甚至要求共享代码。我们可以想象一下，如果安全工程师可以共享通过测试和实验所产生的经验数据和结果，那这该是一个多么强大的专业社交网络啊！

9.3.3　数字中的安全

社交网络并不仅仅是把需要交易的人们连接在一起。它是众包（crowdsourcing）的媒介，也就是利用众人的智慧对信息作出预测。它也可以在未来的安全市场中扮演一个有趣的角色。

下面提供一个简单的众包例子。某个星期五，我的小组中的某个人发出了一张简单的包含提问的电子表格。它是在美国东海岸接近中午的时候发送的，因此当时欧洲是星期五临近傍晚的时候，而在我们的海德拉巴办公室则已经是晚上。小组中的大部分人都在雷德蒙德，因此都在上班途中。在印度，大部分英国人习惯在星期五晚上喝啤酒、吃咖喱食品，而大多数印度人则在外面欢庆他们的生活。下面是对接下来的50分钟里发生的事情所做的统计：

·请求是从美国东部时间上午11点07分开始的

·人群的规模超过70，分布在多个不同时区

·由于时差，活动的聆听人群很可能少于30人

·参与者从8人开始

·问题总数=30

·刚开始时未解决的问题=21

·总回答数=35

·完成所有问题所消耗的时间=50分钟

到目前为止，人类的大脑仍然是功能最强大的计算机。当我们创建一个由这种强大的计算机所组成的分布式网络时，我们可以期望得到良好的结果。网络可以为共享非竞争性信息的对等组织提供支持，它可能需要发展一小部分程序。为了使社交网络能够真正对安全行业产生作用，它将需要开发一些受欢迎的应用程序。我的收入来源于对信息和基准值的大量搜集、共享和分析。参与者可以进行与软件成本、公司的漏洞、威胁配置文件相关的服务质量的基准评估。它的潜在范围巨大，如图9-1的MeMe图所示（来源于一个安全行业）。

图　9-1　安全专业人员的社交网络的可能性

9.4　信息安全经济：超级数据解析和网络新规则

20世纪90年代后期，在伦敦的一家投资银行的服务器办公室里，Curphey把头转向他的朋友Jeff Cave并问道：“什么时候？”Cave回答道：“哥们，太简单了，就是空间中的距离单位。”

场景1：你正在河边野餐，突然发现有人在水里痛苦地挣扎。你跳入水中，把他拉回岸边。附近的镇长对你的义举大加赞赏，并向你颁发了一枚奖章。然后，你回到河边继续野餐。几分钟后，你又看到第二个人在水中挣扎。你再次进行救援，并且又因此得到了一枚奖章。又过了几分钟，又有第三个人出现在水中，你继续救援，又得到了第三枚奖章。一整天的时候，这样的事情反复发生。到太阳落山的时候，你收获了大量的奖章和荣誉。你在当地成了一位英雄！当然，在自己的思维深处，你可能会透出一丝疑虑，觉得自己应该去上游探个究竟，看看为什么会不断有人落水。但是，如果你这样做了，你那天可能就得不到这么多的荣誉了。

场景2：现在你是一位软件测试员。你发现了一个缺陷，你的经理就在附近，并向你授予“缺陷发现者”的称号。几分钟后，你又找到了一个缺陷，紧接着又是一个……当天下班的时候，你收获了大量的“缺陷发现者”的称号，所有的同事都向你表示祝贺。你成了英雄！当然，你可能会细心思考怎样才能防止这些缺陷进入到系统中，但是你还是选择了不作为。不管怎么说，缺陷预防者所收获的荣誉肯定远远比不上缺陷捕捉者。简单概括：要想得到什么，就要衡量什么。B.F.Skinner在50年前就告诉我们，老鼠和人都趋向于进行能够获得回报的活动。这个道理到现在仍然是正确的。当开发人员发现上级用一种衡量指标对他们进行评估时，他们就会非常努力地采取措施，并使自己的表现达到这个衡量指标的要求，即使这些活动实际上对项目并没有帮助。如果你手下的测试人员发现你最看重的是发现缺陷，他们就会成为称职的缺陷发现者。如果你不重视预防，就不会有人做这种事情。对于其他许多安全学科而言，情况也是这样的，例如跟踪事件、漏洞和入侵。

信息安全的度量指标和措施在最近几年已经成为一个流行话题，尽管它并不是什么新话题。虽然Peter Drucker的著名格言“如果你无法衡量它，你就没法管理它”听上去就像是老生常谈，但仍常常被优秀的安全管理人员挂在嘴边。

确定安全实践的投资回报率（ROI）已经成为某种类型的圣杯，安全专家们试图通过它来吸引公司管理层（他们都阅读过Drucker的著作或者从同伴那里吸引了类似的想法）的注意力。ROI的问题在于安全就像在金矿里挖金子一样，不同的淘金人所收获的成果可能有天壤之别（有些人挖了一年之后成为百万富翁，有些人挖了20年还是一贫如洗），但是他们的成功无法归功于他们所使用的工具。与铲子和推土机相关的度量指标只会令人感到困惑。简而言之，ROI这种衡量方法在计算高风险的投资类型时非常麻烦。按照我的想法，当前只有工具推销商才会把安全的投资回报率放在嘴边。

我曾经对度量指标寄予厚望，但我觉得如今的狂热反而会妨碍我们对那些微妙事物的理解。相比度量指标或衡量指标，我更倾向于采用经济学这个词。我认为信息安全经济学将作为一门学科出现，它将对我们管理风险的方式产生深远的影响。

信息安全经济学表示对那些在微观和宏观层次上影响安全的各种因素和关系能够理解。这个话题具有极大的深度，可以产生意义深远的影响。如果我们把当今安全行业的最佳实践与金融或保险市场的复杂超级数据解析相比较，可能会产生幻觉。这些相同的高级技术和技巧可以应用于新的安全学科，并产生同等有效的结果。

最近，我在Ian Ayres的著作《Super Crunchers：Why Thinking-by-Numbers Is the New Way to Be Smart》（Bantam Press）中读到了一个非常精彩的故事。这本书讲述了一位叫做Orley Ashenfelter的葡萄酒经济学家的故事。Ashenfelter是普林斯顿的一位统计专家，他非常喜爱葡萄酒，但他被葡萄酒的估值和品级活动中出现的浮华所困惑，就像我也被如今风险管理的浮华所迷惑一样。20世纪80年代，葡萄酒评论家们主宰了这个市场，他们根据自己的声誉和鉴赏力对葡萄酒的质量作出预测，很少考虑其他因素。反之，Ashenfelter在法国波尔多地区进行了细心调研，开发了一种关于葡萄酒质量的静态模型。他的模型根据收获季节之前的那个冬天的平均降雨量（雨水使葡萄更丰满）以及收获季节期间的平均日照量（光照使葡萄成熟）建立的，采用了一个简单的公式：

质量=12.145+（0.00117*冬季降雨量）

+（0.0614*生长季节的平均温度）

（0.00386*收获季节降雨量）

当然，他遭到了统治这个行业的葡萄酒评论家们的斥责和讽刺。但是，连续几年产生有价值的结果之后，他的方法被广泛接受，并成为葡萄酒的重要估值标准。事实上，在19世纪晚期法国所进行的一次葡萄酒普查中，也是采用了相同的技巧。

显然，理解了影响结果的各种因素之后，我们就可以建立经济模型。同一个原则（根据科学应用健全的经济模型）也可以应用于许多信息安全领域。

对于安全领域而言，Orley Ashenfelter的工作最有价值的部分是他所提供的信息的时间。大多数葡萄酒至少必须贮藏18个月才能品味以确定它的质量。这对于葡萄园和投资商而言都是个很大的问题。但是，通过Ashenfelter的公式，可以在葡萄收获时就预测葡萄酒的质量。这个方法可以应用于安全领域，回答诸如“如果我让软件开发人员接受数量为X的培训，它对最终系统的安全会产生什么效果？”或“如果我们在一个叫做Hackistanovia并且具有下面这些因素的国家部署这个系统，最终的系统会有什么特征？”这样的问题。当然，只有当我们像Ashenfelter一样已经拥有大量的前期数据可以进行解析时，才能够使用这种类型的安全经济学。当前阶段正是度量指标方兴未艾的时候，这个阶段将会产生大量的数据，未来的超级数据解析阶段就可以对这些数据进行分析。但是，社交网络很可能成为催化剂，促使参与者共享数据，创建海量的数据仓库，可以供我们分析信息和建立模型。安全经济学可能会提供一个平台，公司可以借此证明各种类型的安全成本-效益比率，并产生一种具有竞争力的实现方法。

9.5　长尾变型的平台：未来为什么会截然不同

“平台”是一个可以进行编程的系统，因此可以由外部开发人员所定制，从而满足难以尽数的各种需求和利基，这可能是平台的最初开发者完全没有想到的，更不用说花时间去适应了。

——Marc Andreessen,Netscape的创始人

2004年10月，大受欢迎的《Wired》杂志的主编Chris Anderson写了一篇关于技术经济学的文章（[1]）。这篇文章还衍生了一本书《The Long Tail》（Hyperion），它试图解释数字时代的经济现象，并对未来产品和服务战略的机遇发表了一些观点。

这个理论提示产品和服务的分布曲线改变为倾斜的，把需求的很大一部分集中在“长尾”：许多不同的产品或服务各自分享一小块消费者。在许多行业中，除了那些被认为是支柱的产品和服务之外，还有一些产品和服务虽然比较细碎，但汇合起来的需求总量却非常庞大，因此称之为“利基”。以前，致力于利基产品和服务的公司被认为是不经济的，包括iTunes、WordPress、YouTube、Facebook和其他许多Internet经济趋势。

在本质上，我相信信息安全是一个长尾市场，我提供了3个标准来支持这个说法：

·每个业务具有多个流程。

·不同业务中具有相似名称的流程实际上可能是高度自定义的（没有两个业务是相同的）。

·许多流程对于小众用户而言是唯一的。

为了理解长尾理论对信息安全行业可能产生的影响，我们可以观察其他长尾市场以及驱动变化的三个关键推动力。

9.5.1　生产工具的大众化

很久以前，我就不再阅读流行的技术媒体（尤其是安全媒体）上的文章。我感觉这些期刊编写文章的主要目的是为了卖出更多的广告，而博客作家写文章主要是为了让人们阅读。这是一个微妙但重要的区别。如果我阅读媒体上的文章，我所看到的是所谓“内幕人士”的评论。通常，他们告诉记者们想要听到的东西，让他们的名字公布于众。他们是我不愿意信任并听取想法的人。我阅读博客，因为我希望聆听别人表达真实的想法。当然，这个趋势在新经济中变得越来越普遍，并且在信息安全领域将变得越来越重要。行业的核心参与者的意见要比旁观者更为可靠，因为他们掌握更多的相关知识，并且不会偏离方向。

就像博客工具使发表变得大众化以及GarageBand（一种数码音乐创作软件）使音乐创作变得大众化一样，也有一些工具使信息安全变得大众化。事实上，博客已经产生了明显的效果，允许数以千计的安全专业人士提供想法和数据。

意义最为深远的转变是从工具到平台的演变。按照软件的术语，平台是一个可以重新编程的系统，因此可以被外部开发人员和用户所定制，用来满足难以计数的需要和利基。它远远超出了平台最初开发者的想象，更不用说挤出时间去适应。（这是Andreessen在本节之初的引语部分的观点）。当Google提供一个用于访问它的搜索和地图功能的API时，它就把这些服务推到了一个新层次的应用。Facebook为应用程序所提供的插件也是相同的道理。

正如我将在9.5.2节所描述的那样，安全平台将允许人们创建工具，解决他们所面临的问题。

当我们讨论平台时，我们当然需要小心谨慎。任何术语只要具有产生更多销售的潜力，就都会被媒体所劫持，它的本质常常会被冲淡。有相当多的工具被宣传为安全平台，但很少是名副其实的。

[1]http://www.wired.com/wired/archive/12.10/tail.html。

9.5.2　发行渠道的大众化

安全信息并不缺乏，它有大量的邮递列表、BBS（是的，我已经老了）、博客和社区网站存在，另外还有许多专业文章。技术也并不缺乏，不管是开源技术还是商业技术。但在，使信息极大丰富的如今经济是iTunes和Amazon.com获得成功的关键原因之一。它们的成功很大程度上归功于它们能够聚合大量的数据并对它们进行过滤，向用户提供适合他们需要的结果。在一个模糊了传统上称为“专业出品”和“业余创作”边界的世界中，过滤和秩序变得至关重要。它在本质上是一种更好的信息发布模型。

在其他长尾市场中，发行渠道大众化的另一个特征是微观数据解析，这是一种只向每位用户发布他所需要产品的市场策略。微观数据解析还促进了新渠道的应用，使它为顾客所接受。

《The Long Tail》使用了音乐作为例子。几十年前，音乐只是通过CD发行的。如今，音乐的发行渠道还包括在线下载、手机铃声和重混音材料等。

和软件行业的其余部分一样，安全领域已经提供了一种类型的灵活性：我们可以在自己的系统中安装监视工具，或者将它们外包。在未来的世界里，安全用户还想要混合的供应。他们可能要求来自生产商A的最佳扫描引擎与生产商B的最佳签名集合进行组合。按照逻辑关系的术语，顾客们寻求“And”，而不是“Or”。

安全工具的底层考虑可谓众口难调。与我有过交谈的几乎所有公司安全人士都重复了这个论调，分享他们对80/20规则的理解：工具的80%行为满足了我们的需要，但我们无法离开未满足要求的20%，这20%是我们剩余80%的痛苦来源！

让我们以软件的威胁建模工具为例。未来巨大吸引力的关键在于支持所有类型的威胁建模方法学，包括用户自己所作的调整。在自己与漏洞有关的数据上叠加地理数据，并用其他人的可视化工具处理这种混合数据可以帮助我们发现复杂虚拟世界的热点，使我们见树又见林。这些类型的重叠可以帮助我们根据业务性能数据更好地做出风险决策。在一个臭名昭著的信噪比极差的行业中，我们将会看到一些能够更快、更好、更有效地产生高质量信号的工具出现在我们面前。

9.5.3　连接供应和需求

下一代将会发生的最大转变也许是如何把人、流程和技术连接在一起。搜索、本体（信息架构）和社区都将扮演重要的角色。

《Long Tail》的建议是：人们将会告诉你他们喜欢什么以及不喜欢什么，因此不要试图去预测，只要进行评估和响应就可以了。推荐、复核和分级是所谓的声誉经济的关键组成部分。这些过滤器可以帮助人们找到想要的东西，并用一种适用于具体情形的方法来表示它们。

当前很少有信息安全工具尝试提供适用于具体情况的信息。我们可能会看到一些把自己的特定功能与声誉机制相结合的工具。寻找一个潜在漏洞的代码审查工作可以将它与众包建议进行匹配，而后者本身又被众人所分级，并提供像“发现这个漏洞的人中有50%还具有漏洞X”这样的上下文信息。评估和分级可以帮助我们把大量的信息供应与需求联系在一起。

为了对安全工具大众化中的三个趋势进行总结，我相信安全领域将会出现真正的能够连接人、流程和技术的平台。它们的驱动因素包括用于生产的工具的大众化、用于发行工作的大众化以及供应和需求之间的联系。没有任何两个业务是相同的，真正的安全平台能够适应最初的设计者从来不曾预料到的问题。

9.6　小结

我非常荣幸在伦敦大学皇家哈洛威学校的信息安全组（http://isg.rhul.ac.uk）接受教育。他们是这个行业中最棒的。《达・芬奇密码》的读者会注意到它正是书中法国密码破解者Sophie Neveu读书时所在的学校。

在我为Microsoft工作前的几年，信息安全组的Fred Piper教授要我提供一些意见，在英国计算机协会发言时应该说些什么。他向我提出了一个直截了当的问题：“如果Windows从第一天开始就在安全方面表现非常出色，Microsoft是不是还会这么成功？”此时，我想请读者回顾我在本章之初所引用的Upton Sinclair的名言，它引发了一个与安全在信息技术发展的全景中所扮演角色有关的有趣想法。

在一次会议上，我用幻灯片演示了这样一段文本“安全的重要性不如性能，性能的重要性不如功能”，然后又演示了另外一段文本“操作的安全只是一项业务支持功能，要克服自尊，接受这个事实”。为此，我被指责为不重视安全。当然，作为一名安全专家，我绝不会贬低安全的重要性。相反，我理解专家所肩负的用户需求的压力，理解怎样才能使我们的解决方案发挥作用。

在本章之初，我曾提及对未来作出预测是愚蠢的。但是，我希望读者能够同意我的看法，未来几年是对安全进行思考的有趣时刻，甚至是对安全发生影响和进行塑形的有趣时刻。我希望，当我几年后重新回顾这篇文章以及我的博客时，会因为它们与20世纪70年代的电影中的室内调酒机器人有异曲同工之处而感到讪笑不已。我相信，随着技术的不断发展，会创建出更好的技术，正确的东西将会真正出现。

技术的发展既可以用于武装也可以用于裁减军备，即可以用于增加人口也可以用于减少人口，既可以用于攻击全球社区也可以用于防卫全球社区，一切皆有可能。我在本章中所讨论的话题，包括业务流程管理、数字解析和静态建模、可视化以及长尾技术，它们都为未来的安全管理系统提供了丰富的土壤，将在历史记录中写下厚重的一笔。最起码，我痛恨平庸，而今天的安全管理系统最多只能用平庸来形容，所以我期望未来会有重大的改变。

9.7　致谢

本章献给我的母亲Margaret Curphey，她于2004年在法国南部的家中因癫痫而去世。在我成长的岁月里（坦率地说，我有大量的时间不务正业），她总是鼓励我想得长远一点，并帮助我认识到只要专心致志，生命中没有什么是无法实现的。她做出了许多牺牲，使我最终在信息安全领域取得了一席之地（尽管她在晚年不再抱有希望）。她总是说，眼光远大总是好事情，但还必须放手实干。当然，我还需要向我的妻子Cara以及年轻的小黑客Jack、Hana以及Gabe表示谢意，感谢他们的持续支持和耐心。

第10章　安全设计

“美就是真，真即美”，这是我们所有人在世上所知道的和需要知道的。

——《希腊古瓮颂》，约翰济慈

美并不是浮于表面的。真正的美是对一个人、事物或系统的所有方面的写照。对于安全来说，美体现在其简洁而又优雅的设计上，它是在系统设计初期就把安全作为一个重要的目标来考虑的产物。在设计合理的系统中，安全是不可或缺的一部分，它贯穿于系统的设计、构建和测试过程；它又是轻量级的，适应性强，能够让整个系统在面临不断变化的需求时仍然保持灵活。如果把安全作为马后炮来对待，或者是在独立于整个系统的设计需求之外进行开发，那么它经常会是丑陋和死板的。

在我的职业生涯中，我比较注重信息安全和整个系统的开发理念，也积累了一些经验。一是我在美国宇航局的兰利研究中心（NASA’s Langley Research Center）积累的。二是来自于我在软件质量、可靠性、可用性和安全方面的四年工作经历，一开始是在Reliable软件科技公司（现在叫做Cigital公司），后来是在Bell实验室的软件科技中心任副主席。我的亲身经历以及我有幸与之一起工作的优秀的研发团队都告诉我，安全和所有其他的重要“资产”（即质量、可靠性、有效性，可维护性和可用性）都是高度相关的，都能够以讲求效益的方式来实现。在国家宇航局的早期经历让我明白哪些是没用的以及为什么不起作用。在Cigital,Bell实验室以及后来第二次在国家宇航局的工作经历帮助我发展和完善了一套能够按时、按预算交付的高质量安全系统设计策略。

国家宇航局和朗讯公司都有一种错误的观念，认为他们所开发和部署的系统都是“封闭的”，或者是处于高度控制之下的网络架构。而事实完全不同。计算机网络和全球互联网的快速发展从根本上改变了系统互联和访问的方式。实际上，许多系统管理员所认为的“封闭”网络都是与学术研究网互联的，而这些网络的安全策略和访问控制机制大相径庭。网络的互联给攻击者提供了新的访问点，也扩大了系统的整体攻击面。

这种误解给整个安全策略造成了大片的盲点，也滋长了仅靠周边安全（perimeter security）就足够保护系统的论点。遗憾的是，如果连对所谓的周边都没有一个清楚的认识和定义，就更别谈能够拥有一种行之有效的基于周边的安全策略了。由于拥有已知的，处于良好监控之下的周边的观念都不精确，那么系统就会暴露在没有防范设计的安全危险之中。周边防护模型常常被比作为“M＆M”模型，或者是“外强中干”的模型。一旦绕过或者攻破了周边防护系统，那么内部系统的软件和硬件设施都不再安全了，它们也无法自我保护。

10.1　无意义的指标

在20世纪90年代初，当时我正在国家宇航局工作，局里来了一位新上任的管理员。他来自一家私营企业，负责改进局里所有项目的性能。他的目标很简单：降低项目成本，缩短项目交付时间，并且提高最终系统的整体质量。虽然我非常赞同这些目标，但不久就表明了这个计划考虑得还不是很周全。

这个新的方案关注的是交付“更好，更快，更廉价”的程序。当有人指出安全才是人类宇宙飞行和航空程序的关键时，方案就调整为“更好，更快，更廉价，更安全”。宇航局的普通阶层员工们又加上了一句，“任选其二”。

为什么这么幽默？很简单：这个方案认为所有关键的系统特性——质量、进度、成本、保密——都是各自独立的，而目前正在使用的项目各个方面都非常薄弱，所以可以同时实施改进。更重要的是，其他的关键系统质量特性都被推到一边，比如安全、可操作性和可维护性。从资源投入和关注程度上来说，这个方案的焦点都没有放在安全上，就是因为新的威胁正在出现，所以必须迅速采取行动。

我也承认现有的宇航局系统开发过程尚存在可改进的空间。这并不是在指责现有的项目。所有的项目都能够从不断改进的程序中获益。但是，“更好，更快，更廉价，更安全”这个方案是建立在未经验证的关于问题产生的根本原因的假想之上的，反而忘掉了开发一个高效的程序所需的核心项目分析过程。指标计划和相应的全面质量管理（Total Quality Management,TQM）还是不错的，但是方向却错了。对于更好，更快，更廉价，更安全来说，还没有一个清楚的基准线，而且大多数情况下，制定的指标都是有直接冲突的。人类飞行（包括航天和航空两者）是一项需要艰苦奋斗的事业。安全性的加强并不总是与缩短交付日期和降低整个项目的成本这两项指标相符的。

用于支持这个方案所建立的指标是非常烟囱式的。例如，其中有对成本和预算进行评估比较的详细指标，但却没有采用基本的挣值管理（Earned Value Management,EVM）概念，所以这个指标只是一种单维评估，而与成本、进度和规模相关的指标却没有集成或关联进来。成本指标是基于花费的预算数目，而不是获得的价值。比如，如果一个项目经理为了争取更多的测试和集成时间而加大了某个关键步骤的投入，那么这个指标就指出该项目超出了预算。即使在事先对成本做了估算的情况下，这种情形也有可能发生，只不过比计划提前了一些。更为重要的是，这个“单调的”指标非常容易受影响，如果一个项目延长了某个关键步骤的进程，而减少了一些未预见到的步骤的投入，那么这种延迟就掩盖了预算风险。在预计支出达到之前，指标一直显示是绿的，但它可能会突然就变成红的。

质量和安全指标也存在类似的缺点。他们没有说明在质量、速度、成本缩减或者保密之中安全所扮演的角色。项目中的最小安全指标只解决了最终组件和系统周边防护的安全。质量和安全没有在组件级别进行测试。由于缺少单元测试和系统整合测试，所以一些关键问题被忽略，而只能在最终系统测试，或者更糟的情况是在系统正在运行的时候，才能暴露出来。

根本没有足够好、足够快、足够廉价或者足够安全的概念。指标是二进制级别的，而且常常不是在单元或者组件级别上收集到的。对整个计划和项目的成功规划也没有做清楚的定义。花费了大量的能量，定义和筹划了许多单调的、单维的指标，而明确的结果却很少。我惊恐地看着我们的麻烦越来越多，却依然是治标不治本。因此我对“为指标而指标”这种做法大为反感。

大约是在世纪之交，起初是在Reliable软件科技公司，后来是在朗讯的Bell实验室，我有机会花了一段时间来关注安全、质量、可靠性和可支持性问题。在“互联网爆炸”的绝佳时机，许多公司都把关注投向了市场营销机遇或者是如何能让自己的产品拥有与众不同的特性。这种单一的关注意在占有市场份额，而且大都使公司迈向了大规模的“首次公开招股集资”（Initial Public Offering,IPO）。遗憾的是，许多公司都不觉得核心“特性”（即质量、可靠性、有效性、可维护性、安全性和可用性）是市场区分因素，而是把重点放在领先于竞争者并在市场上占有先机这上面。健壮的系统架构、安全性、设计、开发、测试和部署常被视为影响产品快速进入市场的不利因素。虽然有学术与业界的研究都表明这种方法的隐患，但是加快市场占有率的策略还是居主导地位。关键的战略决策是由把握市场机遇来驱动的，而不是由牢固的工程实践。

在科技泡沫破灭之后，市场上那些设计不当、粗制滥造的产品对广泛的IT界的影响就很明显了。受此危害的公司没有办法来清除他们的产品。他们没有什么办法或者财政资源对系统和应用程序重新进行设计。到现在，从目光短浅的“市场投放”（TTM,time to market）关注中恢复已经持续了六年，而且还没有完成。一些公司已经着手于对产品进行重新架构设计进行投资，目的是给自己的产品加上改进的标志。而另一些仍然在投放那些基于失败的设计和开发的产品。这些系统都存在侵蚀产品安全的深层次架构和程序设计缺陷。

10.2　市场还是质量

Bell实验室在计算机科学研究领域取得了令人骄傲的杰出成就，但是实验室中研发出来的软件开发最佳方案没有被坚持运用到朗讯的产品开发小组中。导致安全漏洞的设计、开发和测试方案同样也是导致系统性能低下，可靠性低以及质量差的原因。这些教训仍然没有被吸取，而是精心地摆放在书架上用来收集灰尘。我们只是“痛定思痛”，却没有“吸取教训”。

不当的项目管理和薄弱的系统开发过程使得软件的功能不断膨胀，需求也逐渐发生改变，同时也让关键的架构、设计、开发和测试从核心产品要素中分离出去。产品应该以质量和安全为主，然后再扩展功能，而项目小组却没有这么做，他们试图交付能够满足所有人的所有需求的繁杂的产品。由于很多都是这种“满足所有需求”的情况，所以许多产品都忽略了核心市场的需求。在附加功能的驱使之下，某些原本旨在解决一些简单问题的产品已经变成了研究项目，只知道一味地追求不成熟的技术，提供复杂而又难以维护的解决方案。在某些情况下，我们允许并积极鼓励采用简便的方案来解决一些常见的问题，而且方案可以在多个产品小组之间得以复用，但是许多项目小组还是决定开发自己的解决方案。不断增长的代码数量让系统的设计、开发、测试、维护以及安全变得更加复杂，代价也越来越高。

在我到了Bell实验室不久之后，我有机会领导了一个负责解决目前产品中存在的质量和安全问题的小组。该项目的问题陈述简洁清楚，即“为了市场竞争，朗讯需要改进软件质量，生产效率和可预测性”。

朗讯刚刚完成了一项内部调查，让几个软件开发小组对企业的目标进行评价。五个目标都有“高”、“中”、“低”三种优先级（见图10-1）。安全性根本就没有出现在这个目标之中。这是一个很关键的问题，安全性甚至都没有作为一个重要的企业目标出现在图中。几乎所有的调查对象都高度关注市场投放（TTM）。生产效率（time to volume）的关注度较低，而它是直接影响企业效益的。TTM占据主导关注和调查显示出的可靠性与性能设计不足相互印证。质量、安全性、可靠性、可用性、生产效率和企业效益之间错综复杂的联系还没被认识清楚，也没有有效的管理。高度相关的属性要么看做各自独立的，要么就完全被忽略。

图　10-1　朗讯的企业目标

现实中，在达到适当的质量和安全程度之前，不应该就从单一的一个产品过渡到大规模的量产。如果你这样做了，那么就会直接影响整个产品的开发和支持成本，延长盈利时间。在朗讯的几个案例中，对一些产品草率地进行了量产以迎合TMM目标。这些最终产品将会有很高的保修和服务成本，不但减少了企业利润，而且产品中的质量、可靠性和安全性问题也损坏了朗讯一贯的出品高质量产品的声誉。

对于任何项目或者产品开发计划而言，决定哪些属性需要评估，以及它们之间的相关性和每个属性的可接受级别是非常重要的。项目小组必须拥有一致的成功观念。成功标准必须是实际可行的，而且是综合考虑了项目的所有目标。了解每种指标的可接受程度以对其进行协调、综合是很重要的。

该项调查同时评估了调查对象对朗讯目前的一些关键产品领域的性能与确保项目成功实施所需的性能之间的差距的看法。再次让我感到惊讶，这项调查又没有明确地提到安全性。收集到的数据包含了多个方面，而且可靠性、指标、系统性能、培训、早期的需求开发、管理和测试都存在很大的不足。很明显，需要一个全面的计划来解决这一系列系统问题，我们不能只改进其中的某个“特性”而不解决与其相关的其他根源。

除此之外，软件工程研究所（Software Engineering Institute,SEI）的评估表明大部分（超过60%）朗讯的软件开发小组在过程的成熟度和有效性方面的表现都处于最低的层次。内部分析和检讨指出需要对部署标准的指标、过程以及在初次发布之前改进产品的“特性”方面大加重视。

针对调查发现的结果，一个跨企业的小组提出了一种端到端的计划，这项计划旨在采用一套相互协调的软件设计和开发最佳实践来提高软件质量。图10-2给出了该计划的关键最佳实践要素：“特性”设计，测试和配置管理（Configuration Management,CM）设计以及标准基础因素的调度。

图　10-2　最佳实践要素

“特性”设计关注的是性能与负载，可靠性与有效性、适用性，可用性和安全性。测试和配置管理设计关注的是测试计划、测试自动化和每天的编译与测试。标准基础因素为项目管理、标准操作指标以及培训。

这个小组推荐了一种根据财政季度来划分的含有四个实验项目的先导模型：可以在第一季度开始第一个项目，根据项目提交人对这一阶段的成果的复核确定是否成功，来决定是否在第二季度开始第二个项目。在这两个季度都成功的情况下，可以开始下一个实验项目，当然，这也是在项目提交人复核成功的前提下。

在计划开始之前，需要有一个先驱启动小组对项目小组进行一次轻量级的自我评估。启动小组直接和项目小组一同工作，提供技术专家来支持重点的项目改进计划并把详细的知识介绍给项目组。整个小组开发并实现详细的项目计划，同时也与项目一起维护发现的问题与解决方案之间的“所有关系”。这种合作关系非常好。它使得项目组全身心地投入项目开发中，并且时刻肩负着整个项目成功的责任。

该小组规划了一套高层次的开发周期，以便于理解最佳方案并在实施它们的时候所采取的合理的项目周期阶段之间的依赖关系。这个开发周期的三个阶段是挖掘、执行和部署。图10-3给出了“特性”设计中的性能与负载方面的依赖关系，图10-4给出了可靠性与有效性方面的依赖关系。

这个计划是非常成功的，它将注意力放在了改进朗讯的产品所需的关键行为和最佳实践这两者身上，并且恰当得从安全性、成本与利润、客户满意度和及时性这几个方面对其进行调整。

图　10-3　最佳实践依赖关系：性能与负载

图　10-4　最佳实践依赖关系：可靠性与有效性

10.3　符合准则的系统开发周期的作用

在2003年1月我回到国家宇航局之后，我有幸参加了一个重新设计国家宇航局非任务（即一般用途）范围网络的项目。这个项目受到了两位挑剔的高级领导的强烈支持，他们明白采用强壮的系统开发周期方法的价值，以确保新的设计安全、健壮、便于扩展。我和当前网络的设计者Betsy Edwards共同领导这个项目。我们组织了一个跨部门团队，包括杰出的网络架构师Jim McCabe，厉害的项目经理John McDougle，和一个由安全专家、最终用户、网络架构师、网络运营工程师和网络测试工程师组成的小组。

John McDougle负责让团队始终保持符合准则的设计过程（如图10-5所示），在这个过程中，我们针对整套指标对设计进行评估并给出建议。最终的设计与现有的网络架构大相径庭，但是所有的设计决策都清楚地加以描述，也都能够在系统需求上被找到。最终系统按照预算和进度进行部署，它比所代替的网络提供了10倍的带宽，而月运营费却少了很多。John很棒地完成了维护所有支持该项目的准则的任务，这些准则包括安全性、运营以及管理方面的事情，它们贯穿于整个周期。

图　10-5　NASA项目管理过程（[1]）

Jim McCabe领导了整个团队完成详尽的需求分析，优化和管理过程。最终形成的需求文档不仅仅是功能需求（即带宽、延迟等），也是所有针对最终系统的非功能性操作需求（即安全性、可维护性、可扩性）。在开发周期的每一阶段都会对需求进行复核和更新。项目团队使用详尽的模型对其他设计选择进行评测，而且在每次复核的时候都对非功能性需求进行评估。在每次复核的时候，我们也对安全性进行评估，我们的策略和最终实现也融为最终系统设计的一部分。

在这个项目中，我所学到的比较重要的经验之一是风险管理工具的使用。John采用了一个5×5的矩阵（见图10-6）来跟踪项目中的风险。这个矩阵的一个坐标轴表示发生的可能性，另一个坐标轴表示相应的后果。在每次项目会议中，我们都会将John指出的风险增加到项目风险日志中并对其进行分析。所有的风险都被做了实时的跟踪，直到它们被消除。这个矩阵创造了一种非常强大的可视化图形，方便了项目提交人和复核人员对项目风险进行统筹，而不管是什么类型的风险。

图　10-6　5×5风险管理矩阵

这个项目是我职业生涯中最成功的项目之一。这份成功直接归功于团队成员（这是个全明星队）的努力以及John和Jim的杰出领导。团队所采用的准则同样是项目成功的直接功臣。项目小组在项目开始之前就制定了一套明确的需求，包括安全性、可靠性、有效性、带宽、延迟、可用性和可维护性。包括安全性在内的所有“特性”都在系统设计和可选项分析阶段都已解决，而且所有的需求都划分了优先顺序。最终形成的系统简洁优美。没有额外的层或组件偏离设计或者阻碍系统来适应不断变化的需求和安全威胁。

我在商务部时采用了同样的5×5风险模型来评估信息安全风险和个人身份信息（PII,Personally Identifiable Information）丢失所带来的风险。我们用两个坐标轴来衡量PII损失，一个坐标轴为保护信息所采取的控制措施（由高到低），另一个坐标轴为假设信息被盗所带来的身份窃取风险（由低到高）。这种方法能够让我们制定协调的、基于风险的决定以响应PII的丢失。在对处于设计中的系统进行安全评估和安全审计的过程中，我们也采用了这个模型。这个模型能够使我们清楚地认识到特定的风险并且决定是减少还是接受风险。极少存在完美的系统，因此大多数情况下，你最终必须决定（我希望这是一个慎重的决定）容忍某种程度的残留风险。当越来越多的团队转向基于风险的安全策略时，明确的风险识别和管理策略就成为了成功的关键因素。

[1]本图改编自NASA项目管理过程（NPG7120）和NASA系统工程手册（SP-6105）。

10.4　结论：安全之美是系统之美的象征

我发现那种“快速而又随意（quick and dirty）”的方法仍然用于安全领域，对此非常关心。看来我们并没有从20世纪90年代末的网络泡沫破灭（dotcom bust）中吸取教训。我们只是记下了这件事，然后把它装订起来放到书架上来收集灰尘。正如我之前所说的，我们只是痛定思痛，却没有真正的吸取教训。只是记录这些教训不太好，我们必须吸取它们以期改进。我们还没有制定基于风险的安全计划来解决在系统开发周期各个阶段中的安全问题。除非我们作出这种改变，才会取得有限的成功。

曾有人问我为什么“丑陋的”安全不好。这个问题让我感到很失望。在问了这个问题之后，我们应该制定一种成熟的准则。这让我们明白，对社会而言，我们面前的改进之路还有很长。我对这个问题的答案很简单：丑陋的方案是一块块零碎的，没有集成到一个完整的系统设计中。系统的安全要素偏离了整个系统开发周期，并导致最终的系统脆弱、难以操作和维护。和在系统中设计安全性不同的是，我们试图测试系统的安全性。由于安全层并不是系统设计的一部分，如何安排系统和安全要素的集成点就变得很困难。这种“快速而又随意”的方法在质量、可靠性和可用性方面存在25年的失败历史，我们又为什么要期待它能用于安全性方面？

打个简单的比方，把法拉利430 Scuderia的设计和当前流行的自我改装车相比。很明显法拉利430的整个设计集成了影响车辆（即系统）系统性能的所有方面。这辆车很优美，简洁的车身线条，平衡极佳的轻量级底盘。自我改装车就与之相反，它的各个部件设计互相独立，也是分别购买的，然后由最终用户在基本的车身框架上进行搭建。许多情况下，这种车的各个部件之间并不能很好地协调工作，而且还会对系统性能带来负面的影响。对于法利拉来说，整个车辆都是作为一个系统整体进行设计的，速度、操纵、重量和强度之间相当平衡。

我很确定你肯定在想法拉利的造价比改装车要多太多了，而且如果你在改装车上也花这么多的钱，就能得到同样的效果。我不同意这种论断，以我的经验来看，优良的设计不依赖于价钱。考虑到我们所保护的公共（知识产权）和私人财产（个人身份信息），如果我们没有对安全方面作适当的调查，那么我们就是对客户和消费者不负责任。我们有尽最大可能交付最好的产品和服务的责任，在由于没有适当地处理信息安全问题而带来的风险面前，是时候重视和明确安全要素了。

我非常相信，一个应用于所有项目的设计优良、实现合理的系统开发周期能够在改进安全性方面发挥重要的作用。已经被证明的是，系统开发周期这种方法能够提高团队实施项目和服务的能力。这种生命周期提供了一种轻量级的框架来实现明确的决策制定、反馈和风险管理。在生命周期的初期，它使得项目的各个关键参与方积极参与，并且提供了处理周期下一阶段的明确的标准集。这种方法使得最终系统的所有属性在整个设计和开发过程中都得到了完美地解决。它使得我们能够将安全编织到整个项目方案的结构中。我见过性价比很高的优美的系统，也见过成本很高的“丑陋”的系统。优良的设计准则本质上并不是昂贵的，而且大多数情况下，它还能够减少系统的整体设计、开发和运营成本。

我在NASA、Reliable软件科技公司和Bell实验室的工作经历是很有益的，它帮助我形成了一个完整的构建“特性”的生命周期的观念，也助我完善了管理项目和安全风险的策略。我非常敬重曾与我一起工作过的人，感谢他们一直帮助我不断学习进步。这些机构的研究和咨询团队都很有天赋，时刻致力于提高系统性能的各个方面。

最后，安全之美并不是一个孤立的标志。对一个可靠性、有效性或可用性都很“低劣”的系统来说，怎么去说它的安全很美？安全之美的关键是让整个系统的设计、开发和部署都很美。

第11章　促使公司思考：未来的软件安全吗

为了实现软件的安全之美，需要一种与当前截然不同的业务模型。事实上，当前商业软件的安全状态是相当糟糕的，不断有令人尴尬的漏洞和实际攻击被公布于众，开发人员常常因为修补和发布补丁而手忙脚乱，顾客也不断地被告诫要对软件进行日常的检查和维护。

解决方案是在商业软件的开发中融入顾客对安全控制的需求。商业软件开发公司的目前业务模型是想方设法满足显式的顾客需要，但是并不包括像安全这样的隐式需求。历史的经验显示，软件供应商在发布核心功能以满足顾客的上市需求方面非常在行。但是，消除安全漏洞从来没有成为显式需求。能不能采用一种方式，把安全添加到需求模型中，让顾客和软件供应商都从中获益呢？

本章讲述了一家公司努力改变采购和开发软件这一常规模式的故事。这个故事的结局还没有完成，但是对那些有兴趣提高它们对软件安全漏洞攻击的抵抗力的机构而言，这个故事到目前为止的进程可能会对它们产生一些帮助。

11.1　隐式的需求也可能非常强大

软件的购买与其他类型的购买存在很大的区别。但是，为了更好地理解采用一种不同的软件购买方法的需要，在其他面向顾客的商业中考虑下面这些顾客需求场景是非常有益的。我鼓励读者认真思考，如果你是消费者，在下面的场景中将会怎么做：

1）你来到一家麦当劳，订了一个巨无霸并付了钱。当你坐下来准备享受这个汉堡时却发现它已经冰凉了。你打算怎么做呢？

2）你在Amazon.com购买了一位知名作家的一本畅销书，并选择了隔夜交货的发货方式，因为你迫不及待地想看到这本书。收到快递，打开包装后，却发现缺失了16页，并且所有书页的边角都皱巴巴的，包括封面也是一样。你下一步打算采取什么行动呢？

3）你从Best Buy上花125美元购买了一个能够进行光滑照片编辑和打印的专业桌面打印软件包，并把它安装在一台家用计算机上，以便编辑和管理自己在许多有趣的旅游经历中所拍摄的私人照片。在完成安装并进行了一些基本的测试之后，你对软件的功能感到非常满意。你把自己以及其他至亲好友经过编辑的照片上传到自己的Facebook首页和Flickr网站，并把它们与自己的朋友分享。几个月之后，你在一个博客中看到，你花了125美元所购买的软件版本包含了一个安全漏洞，它允许你与共享这些文件的任何人看到这些照片在编辑之前的原始版本，但这些原始材料却是你不想让别人看到的。你会作出怎么样的响应呢？

现在我敢打赌，在场景1，你将会带着冰冷的巨无霸回到柜台，要求换个火热的。并且我相信，十有八九你可以实现自己的愿望。如果麦当劳在意服务质量和顾客的满意度，那么在每年售出的5.5亿个巨无霸中，顾客手里拿到冰凉汉堡的概率是非常小的。我很疑惑会有多少人在购买巨无霸时会明确提出拿给他们的汉堡必须是火热的。这是个隐式需求，但这个需求却是经理们热切地想要实现的。在购买汉堡时，没有必要为了保证自己所买的汉堡是热的而向柜台提出自己想买的是“热的巨无霸”。

在场景2中，你和Amazon.com的销售代表会很快取得一致意见，认为缺少16页是个严重的缺陷。我相信只要Amazon.com在乎优质顾客服务的声誉，只须把事情和他们说清楚，你将会在一天左右收到一本页数齐全的新书，并且不必另外支付快递费用。在这个场景中，你显然并不需要在买书的时候说明所有的书页必须是齐全的。Amazon.com的销售代表不会借口你在购买时没有指定书页必须齐全而推搪。同样，在购书时隐式地包含了所有的书页都必须齐全这个需求。由于书商没有办法通过提供不完整的书页来赢利，因此在这个行业中，为每一本书提供完整的书页是必然的做法。

但是在场景3，你很可能无法获取同样满意的结果。尽管安全可能是你在购买软件时的隐式需求之一，但它肯定不是显式需求。而且，遗憾的是，在开发软件时很少会在需求中考虑安全的问题。大多数商业软件没有付出多大努力确认安全漏洞并在开发过程中对它们进行处理。

正如我在本章开始时所说的那样，满足显式需求和隐式需求之间的落差是非常巨大的。商业软件的主要注意力集中在满足顾客的上市时间需求的功能上，并且它们在这个方面确实做得很好。它们所提供的新特性和新功能使它们的产品相比其他商业软件开发商的产品更有优势，从而鼓励顾客购买它们的新软件或更新旧的软件包。

显然，在隐式需求中省略了对安全的考虑并不意味着顾客不在意安全或者不明白它的重要性。但是出于两个原因，顾客并没有显式地指定安全需求：

1）没有标准的方法评估软件的安全。

2）随着威胁全景的变化，软件的漏洞也在不断地演变。

对于顾客而言，确定一个巨无霸是否够热是件很简单的事情。但是，确定一个软件产品是否存在安全漏洞却需要不同层次的技术能力和表达能力。在开发软件时，并没有一种像使用温度计测量巨无霸的温度这样的标准方法确定软件是否存在漏洞或者是否还存在更安全的开发方法。

11.2　公司为什么需要安全的软件

我曾经为几家高度规范的金融服务公司工作，它们的核心业务操作极度依赖软件的可靠性。其中一家公司（用虚构名称Acme表示）付出了很大的努力，在数年的时间内投入了可观的资金，并要求CIO级别的高度介入，以减少公司的安全风险。在本章的剩余部分，我将详细描述这家公司是如何启动、运行和扩展这些工作的。

在Acme开始实施现在所描述的这个长期项目之前，它从来没有在软件安全改善做过任何尝试。但是，它具有令人印象深刻的质量记录，并投入了资金，在系统开发时采用了一种连贯的方法，并在卡耐基梅隆・大学的能力成熟度模型集成（CMMI）过程中达到了第3级的状态。为了解决安全问题，Acme考虑引入一些安全漏洞检测工具，在一种“概念可靠”的模式下应用于被选中的项目。

当公司启动一个安全软件计划时，这是它们经常采用的一种方法。

但是，Acme的信息安全主管官员（这是真事）阅读了一本书（由Addison-Wesley所出版的《Software Security：Building Security In》），这本讲述软件安全的书是由知名作家Gary McGraw所编写的。Gary清楚地指出，软件质量并不仅仅是指确认并消除软件代码中的缺陷的能力。事实上，超过50%的软件漏洞是由于体系结构的缺陷所导致的，而不是由于代码本身所致。我还从以前的软件开发经历中认识到，在软件生命周期的早期确认和解决软件缺陷的成本效率要远远高于当代码投入生产后再修补缺陷。我相信强调在软件生命周期的早期确认和修补缺陷是创建更安全软件的经济动力的最佳方法，这可能也是CIO们最欣赏的方法。

在我以前的软件开发经历中，我认识到鼓励软件开发人员把安全漏洞看成是软件缺陷是件很困难的事。它不仅要求一个全面的教育计划，而且对管理行为的改变要采用有组织的开发技巧。我决定把注意力放在教导开发人员创建安全的代码，而不是在代码完成开发、测试和实现之后把大量的金钱花在渗透测试服务上。

11.2.1　如何制订安全计划

实现这个雄心勃勃的计划要求Acme的高管、开发人员以及最终提供软件的外部公司在态度上发生一系列的转变。本节描述了启动这个计划的步骤。

选择一个焦点，并通过管理获得成功

改革家没法一下完成对所有东西的修正。我不得不挑选软件中的一个区域，并努力在这个区域实现可衡量的安全改进。幸运的是，由于现代软件对漏洞的关注，这个任务变得非常简单，并且它与最高风险暴露是一致的。

和早期Internet蠕虫和美丽莎病毒所带来的麻烦不同，现代的软件漏洞攻击涉及经济利益。和任何商业企业一样，现代的黑客始终在寻求只要付出最少的努力就可以赢得最佳经济效益的方法。他们把目光瞄准了基于Web的漏洞攻击。一方面，Web应用程序以及它们所运行的软件平台存在许多缺陷，入侵者可以利用这些缺陷进入Web服务器，并最终入侵终端用户的计算机。结果，数百万的未经训练的计算机用户对Web应用程序的广泛使用为那些把目光瞄准数据盗窃的入侵者提供了丰厚的回报。

毫不奇怪的是，安全威胁数据中的最新趋势清楚地显示了黑客的攻击从网络外围设备（路由器、交换机、防火墙等）到Web应用层的转移。Web应用程序之所以成为目标，是因为这个领域的数据偷窃能够实现最大的经济回报。

关于黑客对Web应用程序的最常见攻击技巧的详细信息，可以访问http://www.owasp.org。这个网站显示了最常见的10个基于Web的漏洞攻击，其中排名第一的是跨站脚本（cross-site scripting）：

当应用程序接收用户所提供的数据并在未进行验证或者未对内容进行编码的情况下就把它发送给一个Web浏览器时，很可能导致跨站脚本攻击。跨站脚本允许攻击者在受害者的浏览器中执行脚本，劫持用户的会话、破坏网站，并且很可能把蠕虫带到网站中。在Acme，我调整了初始的安全焦点。我向主席和COO提供了来自OWASP的Top 10列表和Gartner Group的行业趋势数据（75%的黑客攻击发生在应用层），它们清楚地显示了Web应用程序的漏洞攻击风险。McAfee最近发布了调查结果，显示大约三分之二的美国青少年的母亲对她们的孩子的在线安全的关注并不亚于对酒后驾驶和服用毒品的关注。显示Web漏洞在安全漏洞攻击中所占的重大比例对于说服金融服务执行官投资安全项目相当有力。大多数机构对Web应用程序的依赖性也在不断增加，以便更好地为顾客服务，提供更高的效率。行业监管机构对于解决应用程序的安全也起到了推动作用。Acme的主席清楚地理解检测软件漏洞的价值，并明白软件开发生命周期的早期检测漏洞是非常容易的，它的修补成本要比后期再检测出漏洞要少得多。

设置格式化的安全质量过程

假设我们开始把安全集成到Acme的开发周期中，首先的工作涉及各种形式的格式化和标准化，它们提供了未来工作所需要的度量指标和教育材料。

我们创建了一个来自CLASP（全面轻量级应用程序安全流程）的关键考核标准列表，它是由John Viega开发并被OWASP采纳的一种用于开发安全软件的方法学。我们把它集成到我们的软件开发生命周期（SDLC）中，并通过适当的控制来实行它。例如，每个项目开始时所要求的一个考核标准是确认应用程序的信任边界的文档：它所操纵的是什么数据？谁可以合法地访问这些数据？这个文档根据应用程序所处理的数据分类，帮助定义了应用程序的安全需求。

格式化的另一项内容是把每个新的软件架构提交给安全机构进行严密的审核，这个软件架构必须在得到批准之后才能进入下一阶段的开发。采用了标准安全架构的项目只需少量必需的审核就可以进入到下一阶段。

程序员也必须得到支持，以满足安全标准的要求。为此，Acme选择了一种静态代码分析工具，并要求所有的Java和C++代码用这个工具按照特定的质量门槛进行分析，然后才把代码移交质量保证队伍。我们选择这个工具的一个关键标准是它所提供的高质量上下文帮助，它允许缺乏相关培训的开发人员能够轻松地确认和理解安全漏洞。

开发人员培训

在说服公司领导实施这个安全计划后，我面临第二个重要的挑战：鼓励软件开发人员在开发过程中接受并使用这个静态分析工具。为了更有效地达到这个目的，我选择了能力最强、最受尊重的开发人员，说服他们的队伍领导人允许他们接受一个特定的教育课程，并向他们灌输与安全有关的内容。完成课程并通过考试的开发人员便被确认成为一个新建立的由顶级开发人员所组成的社区的成员，并接受明确的责任，向他们的同事灌输与安全有关的信息。

第一批课程包括了18位由各自队伍领导人所提名的顶级开发人员。教育的内容根据一种称为CLASP的现有软件开发方法学，按照Acme的环境进行了自定义的设计。Acme通过在教室讲课的形式实施这种教育，然后转移到现场以加深理解。整个教育计划由外面的专业软件安全顾问公司负责。有些公司（例如Cigital和Aspect Security）专门就是协助企业实现软件安全计划。

当安全过程真正扎根时

在安全课程的第三天，发生了一件有趣的事情。一位最有影响力之一的、所开发的一个Web应用程序架构被多个已经投入生产的Acme Web应用程序所使用的Web开发人员突然从椅子上跳起来，急匆匆地夺门而出，一边含糊不清地表达了需要对自己的架构做一些测试的急迫需要。教师、协调员和我相信，有一位最好的开发人员已经离开了教室，开始实施控制损害的计划。

第二天，这位开发人员回到教室，安静地坐了下来。教师迫不及待地向他追问一切事情是否已经搞定。这位开发人员站了起来，向教师和全体人员解释：“昨天，我认识到自己为几个产品级应用程序所开发的Web应用程序架构存在一个重大的安全漏洞，可能被外部黑客通过跨站脚本轻松地入侵。我用静态代码分析器对代码进行了检查，我最大的担心得到了证实，所以我昨晚忙了一宿，修正了代码。我用静态代码分析器挽救了代码，在代码提交测试之前消除了关键的漏洞。测试将在今晚进行，我现在可以松一口气了。”从这个时候开始，Acme的开发人员很少拒绝对这个静态代码分析工具的使用。说服开发人员的努力取得了成功。

11.2.2　修正问题

我需要解决的下一问题是与队伍领导人关于处理在静态漏洞分析中所找到的缺陷的重要性的争论。有些项目没有达到可以接受的门槛，被退回到开发队伍。队伍领导人因此面临花费更多时间和金钱修正安全缺陷的需要，这可能会推延项目的发布计划，并会让项目超出预算。

我预感到了这些额外的开销，并试图缓解队伍领导人对这个问题的顾虑。之前我已经与Acme的CIO有过协商，在年度的培训、工具集成和修补需求中增加了10-15%的生产率指数。随后，CIO和我与商业利益相关方进行了会面，获得了他们为了安全而增加时间和预算的许诺。

但是也有一些队伍领导人并不合作。他们不愿意放松调度或者使用生产率指标来弥补为了修补已确认的漏洞的时间增长需求。他们更倾向于为创建功能而分配时间和预算，而不是为了改进他们的队伍所编写代码的漏洞风险评分。事实上，队伍领导人自信地以为安全漏洞并不是缺陷，它是可以推迟到未来的完善或未来的项目。Acme通过两个重要的调整来处理这个问题。

CIO所作出的第一个关键决定是推翻之前的协议，取消了15%的生产率指标，纯粹把它当成一种削减软件开发预算的方式，消除软件生命周期所有阶段的某一层次的意外事件。我刚开始是害怕这个决定的，因为它限制了我用来鼓励项目领导人确定和修正安全漏洞的杠杆。但是，结果却证明这个决定实际上增加了大家对安全计划的遵循性。通过静态分析测试成为开发队伍必须完成的任务，他们感觉到了在软件生命周期的早期消除缺陷的需要，以避免它们的产品在最后时刻由于这个原因而被退回。

第二个决定是实现一个详细的报告框架。在这个框架中，每个月都把关键的性能度量指标（例如，每行代码出现一个高风险漏洞的比例）由队伍领导人、经理和CIO共享。静态代码分析器所产生的漏洞信息按照项目、资产和机构的层次进行总结，并由所有的利益承担人共享。经过一段时间，开发领导人把注意力集中到会增加他们风险评分的问题上，并实质性地与其他队伍展开竞争，以实现更好的结果。这对CIO并没有什么损失，他将毫不犹豫地把开发领导人召集到他的办公室，要求他们根据每月报告回答与SQL注入漏洞有关的问题。

对安全控制的进一步抵制逐渐消失。尽管仍然有一些Acme开发人员和队伍领导人相信安全控制是个负担，使他们束手束脚。但经过一段时间之后，他们都接受了它们，把它们作为核心需求。

后来，Acme增加了由外部服务公司所运行的针对它的核心控制的动态测试和人工渗透测试。这家公司花费了大量的时间把漏洞信息的报告集成到一个模型中，与Acme的责任模型进行链接。这就在整个机构的所有层次上增强了所需要的行为。最初由18位开发人员所组成的核心队伍在一个为期三年的时间框架内增加到超过70人。我们还创建了一个格式化过程，向队伍领导人提供某些被选中的开发人员的性能反馈，帮助开发人员进行安全训练，并实现提升和轮转，这对于他们的职业发展是非常有益的。

11.2.3　把安全计划扩展到外包

到目前为止所描述的步骤是在Acme的主要开发点进行的，密切的监视和个人接触可以增强对规则的学习。但是，有些开发是由外部服务供应商进行的，他们不可能得到公司内部所具有的密切控制。因此，Acme为外部开发设计了一些特定的控制。它们都基于一些核心控制：

·外部开发人员必须按照相同的核心控制（例如分析工具）把他们的代码与Acme所开发的代码集成。

·Acme对远程访问体系结构提出了一些标准，包括特定的控制。

·对开发环境的访问将受到Acme的监视和严格控制，限制对核心开发服务器和工具的访问。

在这个软件安全计划的前几年，由于使用成本低廉的外国承包人所创建的富有吸引力的经济环境，Acme的海外开发数量有所增长。这导致了新的安全问题的出现。例如，海外厂商的员工可能会访问超出他们的项目需要的源代码。Acme增加了一些选择性的控制，监视和限制对文件和开发服务器的访问。

11.3　对现有的软件进行安全化

有了一个相对较为成熟的应用程序安全计划之后，Acme决定解决现成的商业软件（简称COTS）的安全控制问题。和大多数公司一样，Acme在业务和工作站软件方面对外购软件的依赖性不断增加，我们无法在软件实现之前进行对它们安全审查。

在Acme内部，很多人对于商业软件是否能够采用任何安全控制也存在大量的怀疑态度。IT领导人很不情愿对任何诸如评估和选择软件包这样需要增加包袱或时间的事情提供支持。Acme的业务领导人并不确定软件公司在开发过程中对顾客的安全控制是如何作出反应的。

但是，我热情地推动至少在部分商业软件应用程序中采用安全控制。我认识到软件在商业基础设施中扮演了越来越重要的角色，并且经济因素驱使Acme这样的公司越来越依赖于从商业软件开发商那里购买现成软件。我相信，经过一段时间之后，很可能会出现与应用程序的安全有关的规章要求。Acme需要未雨绸缪，提前采取安全控制措施，以避免在面临新的规章制度时猝不及防。例如，2008年4月，美国货币监理署（OCC）公布了它们所监管的金融服务公司在软件安全需求方面的全面指导意见。这是美国第一个明确与软件安全有关的全面规章要求。对于那些在软件安全方面还没有制订成熟计划的公司而言，采纳和实现一个完全遵循OCC需求的安全计划是件非常艰巨的任务。创建一个完全顺应性方案的成本是非常巨大的，何况还有时间和资源方面的压力。另外，我们可以实现的任何控制都表示对现状的巨大改进，因为目前没有办法确认使用外购软件的安全风险，也无法对供应商施加影响。按照我的观点，商业软件只不过是IT供应链中要求进行适当控制的一环而已。

当我向安全委员会描述了商业软件控制的概念时，他们进行了激烈的辩论。他们最终同意在一个“概念可靠”的项目中尝试这个新过程。我定义了这个项目的评估标准，并许诺把结果提交给委员会。

我认识到软件公司很可能会抵制这个新的需要，并且商业利益承担人也很难接受软件交付上的额外延迟。因此，我想出了一个办法，在作出购买决定之前首先与软件开发商进行协商，要求软件开发商必须提供一些材料，证明对它们的软件开发过程施加了有效的安全控制。

这个要求反映了Acme准备与其他请求向Acme获取类似信息的顾客所共享的信息。Acme在软件开发生命周期的需求、设计、开发和测试阶段都能拿出一点证明进行了安全控制的材料。图11-1显示了Acme所要求的材料。图中的4个框分别表示常规的瀑布开发模型中的一个核心阶段，由每个框顶部的一个标签所标识。每个框的底部列出了我们所使用的漏洞检查工具和服务的类型。

简而言之，Acme列出了一个常规的软件开发过程以及用于质量控制和安全检查的工具和服务类型，并要求开发商提供类似的信息。

图　11-1　Acme的安全控制，与显示给顾客的一样

另外，我决定在一家开发了一种二进制格式扫描代码的革新方法的年轻公司测试我们的一个新服务。在本书中，我称之为Verify。这个新方法并不需要访问源代码，这对于致力保护自己的知识产权的商业软件开发商而言就扫清了一个重大障碍。Acme向它的供应商提供了一种方法，只要使用Verify，就可以代替前面所提到的与4个开发阶段有关的信息。如果开发商选择使用Verify,Acme就使用它扫描它们的最新发行软件的二进制格式。软件开发商接收详细的扫描结果，这样他们就可以根据风险以及风险的优先级进行漏洞的确认和修补。然后，我可以根据Acme的需求影响它们的优先级。Verify服务通过一个一致性的框架提供风险评分，这样Acme能够为每个软件产品或每种软件产品建立某所需的风险评分。因此，被检测到的漏洞的级别可以与其他软件产品、第三方所开发的应用程序以及Acme内部所开发的系统进行比较。

我的概念可靠项目包括了三家中等大小的软件公司。其中两家开发商抵制了提供安全控制证明的请求，极为勉强地同意了使用Verify服务。第三家公司恰巧被另一家金融服务公司要求使用Verify服务作为自己的开发商评估过程的组成部分。在Verify所发现的漏洞中，这家软件开发商的实际评分要比它的竞争对手更低。但是，它还是被这家金融服务公司选中了，因为它承诺解决这些漏洞并完善软件。这家软件公司也使用了Verify，并通过一致的方式把这项服务集成到它的安全控制中，最终提高了它的风险评分以及它的软件的顺应性。其他几家公司，包括Acme，则要求这家软件公司提供漏洞汇总信息。我们要求进行安全控制的这三家软件开发商的最终结果从总体上说是积极的。其中两家公司对漏洞信息提供了良好的响应，并根据这些结果对它们的修补工作的优先级进行了适当的安排。其中一家公司在一家金融服务公司使用风险评分作为一个评价标准的单独评估中实际上所得到的风险评分低于它的竞争对手，但这家开发商对漏洞风险信息的响应非常积极和有效，因此还是得到了这笔生意，并且它在后来提高了Acme所要求的风险评分。

Acme并不期望完美的风险评分，它更在意开发商证明自己随着时间的发展能够改进它们的安全控制的意愿。

第三家开发商得到的漏洞评分相对较低，然后开始质疑这项二进制扫描技术的准确性。这家公司在金融服务行业组织BITS所提出的公共软件标准（Common Criteria for Software）进行了不少投资。这是一种严格的文档实践，适用于劳动力密集的控制和漏洞，并且其价值直接绑定到被审查的特定软件版本。Acme同意这家开发商用这个标准作为安全控制的证明，但同时鼓励对方使用Verify。接下来与开发商的对话提供了开发商环境内部对安全控制的有效评估，这使得在Acme环境中采用这家开发商的产品可能导致的安全风险也得到了更好的理解。

概念可靠项目证明了Acme提前一步为所有未来的业务应用程序的采购实行安全需求所获得的足够成功。我分析了Acme当前所提供的桌面应用程序的现有库存，并选择那些我们在延续年度许可之前应该进行安全评估的产品。开发商管理办公室与每家开发商接触，解释新的安全需求，并向它们提供满足这些需求的选择。

同样，Acme决定只把新的安全需求应用于外购软件的一部分——业务应用程序软件，通过渐进的方法提高软件的安全。这种软件一般是基于Web的，具有最高的漏洞攻击风险。我们的计划是对安全评估进行改进，直到我们觉得它是成熟的，然后把类似的需求应用于Acme所购买的所有软件。

11.4　分析：如何使世界上的软件更安全

软件，不管是商业采购的软件还是IT机构内部开发的软件，都将渗透到全球经济的核心基础设施中。今天，完全与软件无关的商业产品或服务可以说是少之又少。我们所乘坐的飞机的设计、生产、操作和维护流程均依赖于软件功能。我们每天离不开的食品、汽车和其他商品也都离不开软件。但是，尽管对软件的依赖如此强烈，公司和消费者对于通过控制安全漏洞创建适合软件的过程的理解还是极为有限的。

11.4.1　最好的软件开发人员创建了具有漏洞的代码

设计满足功能并且具有一致行为的软件始终是个巨大的挑战。大多数软件开发人员都明白，不管他们的天赋有多高，还是没有办法在软件的设计和开发过程中保证万无一失。

就安全而言，Acme明白设计和开发漏洞有限且风险很低的软件是非常困难的。不管怎么说，我们自己已经开发了多年的软件。但是，通过对软件开发过程的简单回顾，可以清楚地看到，执行代码审查使软件开发人员能够在软件生命周期内确认和修正软件缺陷是极为必要的。Acme相信软件代码在开发过程中必须像质量保证测试周期一样测试安全漏洞，并采用多重控制。这个需求对于商业软件开发人员、海外应用程序外包服务供应商和内部IT机构均适用。

最优秀的软件开发人员创建了功能最出色的软件，却可能由于代码中的漏洞而导致受到攻击。软件开发人员在创建功能代码时往往非常在行，但是恶意黑客在破坏应用程序时所采用的技巧是独特的，很难教给软件开发人员。

幸运的是，工具和服务的改进使我们可以在软件开发生命周期的早期检测到漏洞，并在修补成本急剧上升之前就对它们进行修补。Acme所选择的分析工具提供了上下文敏感的帮助，它们除了确认需要立即修补的缺陷之外，还教会了开发人员许多东西。

大量的证据显示，使用一致的、可重复的过程并结合事先确立的实践原则能够增加工程或生产获得成功的概率。为了设计并创建具有有限的安全漏洞的软件，这个道理也是适用的。顾客或商业用户可以并且应该要求在软件开发生命周期内采用良好的做法消除与安全有关的软件缺陷。

基于两个基本原因，Acme理解在软件开发过程中实行安全控制的需要：

·安全缺陷很可能在设计时就在安全和授权体系结构中、在实际软件代码中以及在支持应用程序的设备的配置设置中被发现，因此，在不同阶段实行多重控制是非常重要的。

·在软件生命周期的早期确认和修补漏洞的成本比较低。

Acme已经在设计自己的程序时使用了这些原则，并且在我们对软件开发商所提出的需求中也包括了这些原则。这就是为什么Acme相信要求开发商在软件开发方法中提供安全控制的证明能够有效地鼓励软件开发商对控制的效果进行联合评估，更好地理解商业软件的内在风险。

11.4.2　Microsoft领先一步

当我们向软件开发商提出安全需求时，Acme所接到的最常见的抱怨之一是最大的商业软件供应商之一（这家公司恰好位于华盛顿州的雷德蒙德市）并不期待遵循Acme的需求，这具体取决于开发商的市场规模。

但具有讽刺意味的是，Microsoft是为数不多的在软件开发过程中就处理安全问题的几家商业软件供应商之一。他们在通过安全控制更新他们的软件开发生命周期这一方面进行了大量的投入，并且目前在开发过程中拥有最成熟的安全控制之一，并被其他公司所效仿。熟悉Microsoft产品的顾客可能会对他们每天发布的大量补丁留下深刻的印象，但事实上Microsoft认真地倾听顾客对安全的需求，并不断地通过改进以满足这些需要。

许多年前，他们就在SDLC中引入了安全控制。Michael Howard是倡导提高软件安全的先驱者之一，他编写了几本关于软件安全的书籍。Microsoft最近向公众发布了它们的软件开发生命周期（SDL第3.2版），以提高行业的软件安全实践。显然，他们在产品中完善软件漏洞的能力在近几年得到了显著的提高。

与此形成鲜明对照的是，Acme发现其他几家大型软件公司在要求它们证明在开发过程中采取了安全控制时却摆出了抗拒的姿态。

11.4.3　软件开发商给了我们想要的，却不是我们需要的

遗憾的是，公司和消费者从来没有真正把没有安全漏洞的软件认定为首选软件，消费者的购买习惯也从来没有意识到安全对于成功是至关重要的。各个机构所发布的大量软件需求方案说明书（RFP）并没有指出在软件开发过程中进行控制的内容，以发现和修补安全缺陷。事实上，大多数安全漏洞是由研究人员而不是消费者发现的。

软件开发商的响应是不断地发布软件补丁，尽可能快地解决已知的安全漏洞。Acme意识到，把当今商业软件产品中不断增加的安全漏洞的责任推给软件开发商并不会产生有效的结果，可以说是偏离了要害。软件开发商在很大程度上已经习惯了向顾客提供他们想要的东西：在基于开放标准的平台上发布的不断增长的功能。它们的商业模型反映了向市场快速投放新功能的定式，只有精通此道才能同时满足消费者和华尔街的期望值。Acme试图更改这种定式，把安全需求注入商业软件采购的软件评估标准中。其他公司会不会遵循这个需求？这个有待观察。消费者会不会调整他们的行为？也许他们已经调整了。我们看到了为消费者提供具有更高安全层次的软件产品的日渐增长的需求。

Acme意识到通过改变消费者的行为来减少安全风险的做法超出了自己的能力范围。我们把注意力集中于在自己的软件采购过程中实行安全需求。Acme的安全计划在这方面是否成功需要几年的时间才能够评估。

幸运的是，Acme对于改进软件开发世界的努力被映射到一个完全不同和更大的规模。有一家每年软件采购金额高达数十亿美元的大型机构在近几年调整了它的软件和硬件采购流程，包含了对安全设置的需求。当然，我所说的就是美国联邦政府，它在2006年初成功地谈判了一组特定的安全设置，并将其应用于它所购买的所有通过Microsoft Windows和其他Microsoft软件进行配置的Win/Tel工作站。

2005年，与Microsoft达成一致之后，美国空军的CIO成功地实现了安全配置标准，按照自己所想要的设置对工作站和Windows进行配置，然后在类似配置的工作站上测试软件补丁，然后才发行它们。这个计划相当成功，因此美国政府对它的所有机构都实现了相同的配置需求。例如，农业部在2007年12月12日所发布的终端工作站标准中指定了安全需求。这是部门级的标准，实现了安全控制的一致应用，并指导所有工作站的采购。这个标准包含了下面这些需求：

·补丁管理

·反病毒和反间谍软件

·操作系统的安全补丁

·漏洞扫描

·磁盘加密

·多重身份认证

对于实施了全面软件安全计划的公司，已经看到了黑暗尽头的曙光。Acme已经有连续5个月在它超过600名开发人员所支持的所有项目中没有找到任何高风险的漏洞了。Acme还完成了分析，显示了在软件生命周期的早期消除安全漏洞使公司的生产率提高了11%，在代码投入生产之后节省了大量的修补工作。Acme的软件安全计划减少了风险，提高了生产率，使开发人员把注意力集中在更有价值的活动中。每个采购软件的机构，不管是大型的还是小型的，都有机会把相同类型的逻辑应用于采购过程，要求开发商证明它们通过三种常见类型的漏洞检测控制（静态、动态和手工）来遵循安全需求。

如果我们把证明实行了这些类型的安全控制作为采购过程中的一个需求，久而久之，软件开发商、集成商和外包商将会作出响应，在这些控制上进行投入，并把结果与预期的顾客进行分享，以证明它们相比其他公司具有竞争优势。这是件非常美的事情！

第12章　信息安全律师来了

俗话说：万变不离其宗（换得越勤，变得越少）（[1]）。在信息安全领域，技术变革的步伐极为迅速。一旦“好人们”跟上队伍，“坏人们”就会加速前进，采取新的攻击手段。但是在信息安全领域，有一种说法总是对的，那就是“换得越勤，变得越少”。

安全专业人员总是在处理那些广为人知并且是体系性的问题，包括不良的用户实践、充满缺陷的软件，并且在国家一级上缺乏有力的领导（至少美国是这样，到目前为止仍然采用以市场为驱动的方法）。这些问题的广泛性，包含常见要素的事件频发以及文化影响的深度决定了它们将继续存在，或许司法的介入会使事情发生变化。事实上，信息技术和法律已经发生了碰撞，并且将更频繁地继续产生联系。在本章中，我将讲述一些逸闻和原则，希望能够帮助读者理解法律和信息安全之间的交流的积极潜力。

尽管读者可以跳过本章，因为它是由一位律师所写的。但是，读者应该先考虑到下面这些因素：首先，我以前曾经是位加密技术工程师，因此理解技术和法律的相互关系。其次，信息安全和法律的交集可以很自然地用一个人对其他人的义务这个概念来形容。在历史上，义务（以及对义务的违背）是通过民事法律解决的，尤其是那些由于疏忽所导致的事件。尽管技术改变了许多东西（并且会改变越来越多的东西！），但对于在信息安全领域中感觉受到伤害的人们来说，基本的道理还是一样的。具有保护他人的义务的实体由于未尽到这个义务导致受害方造成了损失。简而言之，这就是信息安全场景下与失职有关的法律原则。稍后，我们将花更多的时间讨论失职的问题。

本书提供了大量与原则有关的例子，许多作者认为原则是美丽安全的核心思维。按照我的观点，有4个基本概念影响到这个领域的所有问题：文化、平衡、通信和正确做事。尽管数据安全问题已经吸引了大量的注意力，并且已经有许多与信息安全有关的法律得到通过，但信息的丢失以及数据安全的失败等问题仍然时有发生。许多人疑惑怎么会允许这类事情发生。对于那些相信法律的力量能够带来“美丽安全”的人们而言，这种情况令他们尤感困惑。如果我接受“更好的安全需要改变现状”这个假设，那么从立法的角度，应该做些什么来改变现状呢？

站在务实的角度，安全领域的一个能够反映基本原理的变化就是建立一个安全系统，然后把责任人的思维从“安全是事后诸葛”改变为“安全集成到我们的日常工作中”。

在本章中，我将从法律的角度讨论文化、平衡、通信和正确做事的概念，并分析各自的职责。为了解释这些概念，我将公开自己的经验，并分享被我深情地称为“信息安全律师疯狂小乐队”的小组讨论意见。

12.1　文化

在信息安全领域，我还没有遇到过任何怀疑组织文化是否将会影响信息安全成败的人。因此，一个组织的领导层对安全的立场直接影响到企业的文化，并最终影响它的安全实践。把这个概念进一步深化，与安全相关的地区或国家文化将依赖于这个地区的领导层。因此，我首先列举一个为这个领域的国家领导层（第44届总统班子的网络安全委员会）所提出的特定建议。

战略和国际研究中心（CSIS）在2007年下半年成立了第44届总统班子网络安全委员会。这个委员会的领导层包括Jim Langevin和Mike McCaul（两人均是众议院国家安全委员会内部负责正在浮现的威胁、网络安全以及科学和技术的下属委员会的成员）、Scott Charney（Trustworthy Computing、Microsoft Corporation的副总裁）和已退休的美国空军中将Harry Raduege（Center for Network Innovation,Deloitte＆Touche LLP的主席）。这个委员会的目标是提出一个与党派无关的草案，实现全面的策略，完善联邦系统和关键基础设施的网络安全。

在我作为这个委员会的委员参与这项活动期间，我非常惊奇地发现，美国并没有一种协调覆盖整个网络空间活动的网络主义。由于本节所讨论的是文化，读者可能会疑惑为什么我首先从讨论主义入手。但是，我们可以很自然地得出结论，如果整个联邦没有一个统一的主义和相关的网络安全策略，安全文化的发展将是极为困难的。与这个观点一致的是，这个委员会的成员们出现在2008年9月众议院国家网络安全下属委员会的听证会上。委员们证实了美国缺乏一致的、可操作的国家策略来处理安全网络空间所面临的日益严峻的挑战。

美国在历史上的不同时期产生过各种主义。十九世纪初，门罗主义表明了美国对美洲其余地区的政治发展策略，并确定了欧洲在西半球的角色。它的三个主要主张是：1）划分美洲和欧洲的势力范围；2）去殖民化；3）互不干涉。这些主张清晰地表明了新成立的美国与欧洲专制帝国的决裂。

第二次世界大战之后，杜鲁门主义确立了美国在接下来的几十年里所奉行的外交政策。杜鲁门主义声称“支持自由的人们免受极权国家的武装侵略或外部压力是美国必须恪守的原则”。这个政策改变了美国长期奉行的隔离主义原则，确立了美国处理第二次世界大战后事务的基础，并预示了美国在接下来的六十年里对世界政治的干预。近来，我们还看到了卡特宣言，它确立了美国在必要的情况下可以动用武力保卫美国在波斯湾地区的国家利益的政策。我们在2009年很明显地看到了这个主义的一些后果。

考虑到这些主义的广阔范围以及Internet对社会生活的全面渗透（政府、公司和个人），一个网络主义加上相关的策略将难以置信地帮助建立一种安全文化。委员会的其中一个建议是督促美国政府在这个领域扮演强大的领导角色：a）、在政府内部建立国家网络空间办公室（NOC）；b）、发展一种全面的主义和策略，保卫网络空间的安全。

委员会指出网络空间安全已经成为国家最需要关注的问题之一（2008年总统竞选双方都提出了这个观点），它还提供了其他几个建议，包括：

·竭全国之力，开发一个针对网络空间的全面国家安全策略，创建美国网络安全综合计划（CNCI）。

·通过一种平衡的方法管理网络空间，既避免强制规定，又避免过度依赖市场力量。

·需要一种更健壮的身份认证机制访问关键的基础设施组成部分。

·创建公共和私人的咨询组织，以支持NOC。

·与私营部门建立合作关系，专注于关键的基础设施和协作活动，预防网络空间事故并对这类事故作出响应。

尽管这个委员会所作的努力的长期效果尚有待观察，但这个委员会与以前的做法的一个关键区别在于它强调可操作的提案。每个提案的目标是提供一些建议，从而使总统和行政部门可以采取切实可行的行动。

[1]Alphonse Karr,Les Guêpes（1849）。这个短语可以更直白地翻译为“换得越勤，变得越少。”但是更常被形容为我在这里，唱着摇滚歌手Rush的专辑Hemispheres中的歌曲“Circumstances”。在某种程度上，信息安全的某些方面与Rush在Circumstances中所描述的世界相似：“现在我已经得到了一些理解/对我们所看到的唯一世界/我只能梦想的事情/已经变为了现实。”

12.2　平衡

涉及责任和失职的法律领域包括法律分析、风险评估和违责风险，必要时还涉及平衡。信息安全中的平衡常常趋向于把注意力集中在影响整体安全演算的竞争因素。和其他风险评估领域（例如人寿保险或医疗保险，它们可以通过保险分析数据计算出强有力的结论）不同，信息安全无法根据大量的历史信息得出结论。从法律的角度考虑安全时，这个特点就尤为明显。

然而，我们仍然可以采取一种整体的、积极的（在某些情况下）方法。在本节中，我将提供两个在利益竞争者之间实现平衡法案的例子，它们分别是美国律师协会所提出的数字签名指南以及加利福尼亚数据隐私法案（a.k.a SB 1386）。本节的最后将讨论信息安全程序可以实现的投资回报率（ROI），以得出一个更全面的结论。

12.2.1　数字签名指南

我在信息安全领域所遇到的第一个与法律有关的平衡概念出现在20世纪90年代初期，当时我正从一位加密技术工程师转行成为一名律师。从事法律行业后，我加入了美国律师协会（ABA）内部的信息安全委员会（ISC）[1]。令我惊异的是，这个小组并没有思想陈腐、观念固执的律师，而是由一群充满活力的律师、技术人员和商业人士所组成的（还曾经出现过几位国际代表和政府代表）。我们所接受的一个有趣挑战是在公钥基础设施（PKI）开始流行之后制订适当的权责平衡。计算机领域中很少有像PKI这样依赖信任和长期保证的。证书只对认证中心（CA）通过签署证书的人有效。许多人开始偷工减料，在很少甚至没有得到技术和法律支持的情况下完全依赖几个信任实体作决定，这就与这个流程相去甚远。知道了数字签名领域的潜在政府规则之后，ISC决定起草数字签名指南（DSG）[2]，它的子标题对它的范围进行了总结：“证书授权和安全电子商务的法律基础架构”。考虑到PKI涉及当时许多法律未知的领域，因此我们有很多的基础工作要做。DSG不仅要成为以前从未出现过的明确法律原则，而且一项新生的技术也需要这样的规则。

DSG是在70位委员会成员的合作下产生的。经过数年的努力，这个小组明确了标准PKI模型中每个利益相关方的责任和义务：认证中心（CA）、证书申请者和证书信任方。在工作期间，ISC确立了许多术语的定义，并起草了一组基本原则，包括协议的变型、可预见的证书依赖性以及利益相关方之间的信任关系。

当基本原则和定义完成之后，每个利益相关方（CA、证书申请书和证书信任方）都经过了仔细的解析和检查。委员会内部的各个下属小组为每个实体起草了原则，并为每个原则提供了评论。当一个下属小组完成了一个草案之后，它就由整个委员会进行讨论。虽然这个流程有时候显得有些笨拙，但它最终产生了一个简洁的文档，描述了我们通过精心思考而制订的原则。这样的结果只有在一个非常坚定的领导队伍的带领下，并在一群非常优秀的人所组成的跨学科小组努力地工作下才有可能实现。

[1]http://www.abanet.org/scitech/ec/isc。

[2]美国律师协会、信息安全委员会、科学和技术法律部门的《数字签名指南》，可访问http://www.abanet.org/scitech/ec/isc/digital_signature.html（Aug.1，1996）。

12.2.2　加利福尼亚数据隐私法

尽管不像开发DSG那么积极，但加利福尼亚数据隐私法也提供了寻求竞争方利益平衡的一个好例子。加利福尼亚数据隐私法（又称SB 1386，作为民法的第1798.29、1798.82和1798.84条获得通过）对任何“拥有或批准包含个人信息的计算机化数据”的“在加利福尼亚从事商业活动的商业组织”提出了要求。但是，这部法律把个人信息相当狭窄地定义为个人的名字、中间字母以及姓氏与他的账户信息的组合。2008年的修正案扩展了这个定义，包含了医疗信息。

SB 1386幕后的故事则少为人知。但是，它的主体文本仅仅经过一次起草会议就成为法律的执行版本，我想这个过程肯定非常有趣和刺激。我只要描述时间阶段，读者就会理解为什么会起草这些文本以及它们是如何起草的。我们首先来简单讨论一下立法机构的历史（或者说，我们事实上很可能不想知道或者不需要知道的所有与起草法律有关的事情）。当一项法律草案被引入时，它一般会经过一个由某个委员会所进行的“标记”过程，然后在立法机构内进行辩论。通常一个草案在成为法律之前需要经过几次不同的修订，包括在两院的立法机构之间取得妥协。在许多情况下，最终的法律将与最初所引入的草案较为相似。

但是，这部加利福尼亚法律的有趣之处在于该法律通过时所使用的语言与最初的草案没有相似之处。事实上，这部法律经过了完全的重写。这部法律的最初草案[1]的焦点主要集中在对州立机构所拥有的公共记录的访问以及这些机构对个人信息的泄露所施加的压力，以遵循加利福尼亚早期有关州内隐私政策的法律。

在2002年3月的一组修正案之后，司法委员会在2002年6月的一个修正案中对这个草案的文本进行了完全的重写，把焦点集中在州立机构和州内商业组织对数据泄露的通知上[2]。在一个分析表决前草案的报告中，院务委员会提到了3月和6月之间所发生的两个促使他们完全重写这个草案的特定事件。

首先，这个报告讨论了“Stephen P.Teale数据中心的事故，几十万工人的个人金融信息落入计算机黑客的手中”。其次，它提到了一个私营部门的事故，“未获授权的个人使用正常情况下由Ford Motor Credit所掌握的访问码访问了Experian的13000名消费者的完整信用历史，包括姓名、地址、社会保障号码、母系信息和信用卡账户的细节”（[3]）。

根据这些事故，委员会继续指出：

有太多这类事件并没有被报道。它们是怎么被悄无声息地处理的呢？公司或机构对于泄露机密之类的尴尬事件讳莫若深，它们可能害怕自己的不良信息安全事件被公布于众而影响公司的生意，所以不愿意通知受到影响的人。信用卡发行者、电话公司和Internet服务供应商以及州和地方官员“处理”消费者的个人信息和金融信息被未获授权者非法访问的事故时，常常自行吸收由于欺诈所导致的损失，把它作为一种“顾客服务”成本，不会通知顾客他的账户遭受了未授权使用。

顾客需要知道什么时候在他们的账户上出现了未授权的活动，或者什么时候未授权的人访问了他们的敏感信息，以便采取适当的步骤保护他们的金融健康。

为了平衡消费者和商业组织的竞争性利益，委员会最终采取了一些妥协。最终的法律规定了在美国范围内必须对数据泄露进行及时通知，最终平衡了消费者对他们的个人信息被泄露的知情权以及作出这种通知的机构和商业组织所受到的负面效果。例如，这个草案的早期提案要求这种通知是“立即的”。根据涉及数据泄露的现实调查，这个“立即的”要求被删除。由于担心通知大量人群的成本问题，所以在起草过程中增加了一个替代的通知条款。

[1]参见草案编号：SB 1386，引入的草案文本，详见http://www.leginfo.ca.gov/pub/01-02/bill/sen/sb_1351-1400/sb_1386_bill_20020212_introduced.html（February 12，2002）。

[2]参见草案编号：SB 1386，修改案文本，详见http://www.leginfo.ca.gov/pub/01-02/bill/sen/sb_1351-1400/sb_1386_bill_20020606_amended_asm.html（June 6，2002）。

[3]http://www.leginfo.ca.gov/pub/01-02/bill/sen/sb_1351-1400/sb_1386_cfa_20020617_141710_asm_comm.html。

12.2.3　安全的投资回报率

每家公司不可避免地需要把它在安全领域的义务与其他许多竞争性利益进行平衡。在“我们是否应该购买一个价值1万美元的箱子，以应付可能出现也可能不出现的威胁？”和“我们是否应该购买一台价值1万美元的服务器，使我们的交易处理效率提高20%？”之间作出选择的事情是极为常见的。单纯站在投资回报率的角度，进行安全投资往往缺乏说服力。

因此，即使对于最老练的安全专家，投资回报率这个词仍然会戳到他们的痛处。每个曾被要求调整安全预算或者特定的安全开支的CIO和CISO都知道信息安全存在一个无可逃避的问题，那就是收益很低或者没有直接的回报（这个问题在Peiter“Mudge”Zatko所编写的第1章“心理上的安全陷阱”中进行了详细的解释）。许多专家把信息安全比作一项保险政策：如果一切运行正常，你甚至不会意识到自己已经得到了回报。

现在，信息安全在许多IT部门的优先级列表中竞争着最顶层的位置（因此争取更多的预算）。2008年对信息技术认证专业人员的调查结果显示，信息安全管理已经成为排名第一的技术推动力[1]。但是，在调整信息安全的开支时，这可能是“律师可以帮助你”的罕见场合之一。为了理解我说的意思，我们首先需要讨论一只驳船、一只拖船、纽约港和一条绳子。

让我们回到1944年1月的一个意义重大的日子，停泊在纽约港的Anna Carroll号驳船因为绳子断了而发生漂移，在驳船船主（管理人）不在场的情况下造成了一些损失。在确定驳船主人的责任时，Learned Hand法官提出了B＜P×L这个公式，并表示“如果概率是用P表示，伤害是L表示，负担用B表示，责任取决于B是否小于L乘以P，即B是否小于PL。[2]”放在信息安全的环境中，它可以转换为下面的分析：如果一个机构为了防止信息安全失败或错误所付出的开销小于该失败出现的概率乘以它可能导致的损失，这个机构就应该严肃考虑承担这个开销（或者采取其他合理的方法）。

把这个原理映射到积极务实的信息安全中，B＜P×L这个简单的记法可以为信息安全预算提供一个强而力的论据。公司可以在威胁和风险评估（TRA）中对所有的威胁进行观察，并确定与每个威胁相关的两件事情：根据这个威胁所发生的将会实际影响业务的攻击的概率以及攻击可能造成的损失，对它的安全预算做一个非常粗略的估计。这些数字可以作为预算的起点，然后可以计算安全投资回报率（ROSI）。如果我们把B＜P×L这个计算看成是风险暴露，并确定一个特定的解决方案可以在多大程度上缓解这个风险，就可以演算出ROSI。下面这个公式提供了至少一种方法：[3]

ROSI=（（Risk Exposure x Percent of Risk Mitigated）

-Solution Cost）/Solution Cost

另一个需要考虑的因素是：公司在安全上的投资实际上可以（至少是间接地）在确定责任时取得回报。至少一个在法庭审理的数据泄露案件中，法官特别注意到公司已经采取了步骤实现合理的安全措施。具体地说，法官表示：“法庭认为，原告Guin并没有提供足够的证据，事实调查人无法据此断定被告Brazos未遵循GLB法案……Brazo已经编写了安全策略、当前风险评估报告，并采取了适当的安全措施，满足了GLB法案对保护顾客私人信息的要求。”[4]

我们希望读者永远不要出现在由于数据泄露而导致的诉讼名单中，不必面临是否采取了适当的安全控制相关的问题。但是，如果在数据泄露之前已经准备了ROSI分析，那么它可以成为一个实用的工具。即使在那些并不是这么极端的情况下，它也可以作为一个有力的回复，反驳那些把烦人的专家把信息安全比作一项保险政策的不当比喻的人。ROSI显示了在安全领域同样存在投资回报率。

[1]参见AICPA 2008 Top Technology Initiatives，详见http://infotech.aicpa.org/Resources/Top+Technology+Initiatives/2008+Top+10+Technology+Initiatives/2008+Top+Technologies+and+Honorable+Mentions.htm。

[2]United States v.Carroll Towing Co.，159 F.2d 169（2d.Cir.1947）。

[3]Wes Sonnenreich、Jason Albanese和Bruce Stout，“Return On Security Investment（ROSI）-A Practical Quantitative Model”，Journal of Research and Practice in Information Technology,Vol.38，No.1，at p.55（2006年2月）。详见http://www.securemark.us/downloads/ROSI-Practical_Model-20050406.pdf。

[4]Guin v.Brazos Higher Ed.Service Corp.，U.S.Dist.Court,Minnesota,Civ.No.05-668（February 7，2006）。

12.3　通信

与信息安全相关的通信可以非常简单地表述为：技术狂需要律师，律师也需要技术狂。它已经在政策中得到证实（通常是通过劣质计算机产品和不良法律决策的负面例子），它在商业背景中的确是真理。

12.3.1　技术狂为何需要律师

技术狂从律师那里可以学到的东西包括信息安全领域的法律，它可以提供本章的“平衡”一节所提到的平衡，并可以发展得更为精密。在早期，州和国家级别都通过了反黑客法律。这个领域的联邦法律包括计算机欺诈和滥用法案（CFAA）[1]、电子通信隐藏法案（ECPA）[2]和其他刑法。与之相同的是，所有的州都通过了它们自己的反黑客法律。

虽然这些被动性的法律在信息安全领域扮演（并将继续扮演）着重要的角色，但是从整体观察安全的角度，它们也有一些缺点。首先，它们只专注于整体安全范畴的一个参与方：做错事情的一方。其次，它们专注于安全时间轴的事后部分。具体地说，所有专注于黑客的法律只有在发生了某个坏事时才会被触发。因此，它们对于未来的网络攻击只起威慑的作用。

如前所述，在立法领域的一个与安全相关的重大进展中，加利福尼亚成为美国第一个通过数据泄露通知法律的州。由于商业和信息的跨边界效应，因此它成为一个全国性有效的法律。随着SB 1386的通过，加利福尼亚把信息安全提升到一个所谓“C套件”（CEO、CFO、CIO、COO等）的有意识的层次。

认识到事情还存在另外一边时（积极的一边），法律上的一个近期趋势（同样发端于加利福尼亚）是专注于不良事件之前的活动。AB 1950[3]要求批准使用加利福尼亚居民的未加密个人信息的商业组织实现和维护合理的安全过程的实践，保护个人信息不受未授权的访问、破坏、修改或泄露。

在联邦层次上，管理条例已经成为实现安全的最强驱动力之一。信息安全法律和条例的通过可以看成是影响信息安全最重要的趋势之一，也可以看成是导致条例等于安全这个危险错觉的原因。这把双刃剑一旦挥舞起来，可能会导致没有根据的故步自封。尽管在实现信息安全计划时管理条例显然是一个必须考虑到的动力，但它应该是结果，而不是目标。我们可以想象一下SOX、HIPAA、GLBA和数据泄露法律之前的年代。大量的非管理性指导来源和一些信息安全标准提供了大多数信息安全专业人员所需要的指导方针。但是，各种事件的发生导致了前面所述的各项法律的通过。

站在“玻璃杯是半满的”的这个角度，法律已经提供了一个优雅的框架，可以根据这个框架创建合理的安全程序。在受到规定制约的行业领域（例如健康监护和金融服务）中，和以前的状态相比，各种规定已经有力地提高了安全。对于这些领域之外的公司，要求执行数据泄露通知和合理的安全措施的州法律使信息安全走到了最前沿。因此，规定可以看成是使许多机构把注意力集中到安全上所需要的催化剂。因此，一般认为需要更多的规定。

但是，这个观点的批评者指出，如果公司对于提升它们的安全水平持漫不经心的态度，它们也不会把那些要求加强安全的规定太当一回事。而且，规定会成为一种门槛，许多企业的目标就是寻求达到这个最低层次，没有理由或动力超越这个最低层次。

而且，站在“玻璃杯是半空的”这个角度，有些人觉得这些法律是不成功的。部分由于条件的含糊性以及征税的原因，它们并没有提供许多倡议者所喜欢的那种更紧密的信息安全。如果这些法律是成功的，确实起到了很大的作用，那么在过去的几年里，我们不应该遇到这么多的数据泄露事件，身份数据盗窃和公司数据盗窃事件也不该日益增加。因此，至少在最低限度，我们不应该对目前的规定进行补充。在完美的世界中，应该摆脱这些毫无作用的规定。

由于安全破坏的影响波及个人、商业和国家的安全（这是法律无法忽略的影响），因此并没有人主张不把规定作为信息安全全景的一部分。我们应该把注意力集中在规定的实现方式，使管理条例（到任何特定的组织所面临的程度）成为一种无足轻重的副产品，而不是一个可怕的负担。

接着，我将回到前面的观点：管理条例怎样才能成为结果而不是目标？如果一个机构寻求对自身进行适当的保护，就应该采用务实的安全方法。

务实的安全方法有助于认识维护安全网络的正确价值，理解不实行充分安全措施的后果，并在机构内部自上而下地实现信息安全。务实的安全方法可以有效地创建一种安全文化。《联邦判决指导条例》对服从和道德也存在类似的务实认识。“为了实现有效的服从和道德计划”要求一个机构实现一个“经过合理设计、实现和实行的，一般情况下可以有效地预防和检测犯罪行为。如果无法预防或检测即时的违法情况，并不意味着它在一般意义上不具备预防和检测犯罪行为的效果”的计划（[4]）。如果认真执行务实的安全方针，它事实上可以发挥作用：大多数成功的信息安全计划在C层次的领导层开始就自上而下地支持它们。当上层的管理提供了必要的领导能力并创建信息安全计划的动机时，就很容易在实现这个计划的人群中创建一种安全文化。

因此，我们需要拓宽思路，让管理层支持我们的信息安全计划，包括得到法律队伍的支持。我们积极编写处理管理、技术和物理安全措施的信息安全策略，并确保得到律师的同意。得到管理层的支持之后，就可以实现适当的内部控制和合理的安全措施。然后，我们就要准备提供数据泄露通知（希望永远不需要这样做）。但是，我们之所以满怀希望地做这些工作是因为我们的管理层和法律队伍信任我们，并授权我们放手大干，而不是简单地规定我们必须这样做。

我的一个好朋友花了42年时间研究同一部法律，他曾经说过：“律师常常是错误的，但绝不会存疑”。但是，不管律师的想法如何，他都需要技术狂的协助。相对法律的缓慢发展，技术发展的步伐无疑是极为迅速的。另外，技术的复杂性仍然在不断增加。因此，一位优秀的技术律师具有一项意义重大的任务，那就是跟上技术发展的步伐，并确定如何把法律应用于这些不断变化的复杂性。实现这个目标的唯一方式就是与技术社区的人们进行良好的沟通和交流。

律师可能会在许多途径中遇到技术问题。例如，客户可能需要批准使用一些新技术或者可能具有一种独有的商业模型，需要一些开创性的合同。客户可能需要保护他的智力财产（IP），或者确定他是否侵犯了其他人的IP。

而且，律师作为技术消费者，可能决定使用一种新产品或者可能要求为客户做这些事情。不管是哪种情况，能够请教技术专家都是不可估量的财富，尤其对于那些独有的和不寻常的技术。

[1]18 U.S.C.§1030 et seq。

[2]18 U.S.C.§2510 et seq

[3]参见http://info.sen.ca.gov/pub/03-04/bill/asm/ab_1901-1950/ab_1950_bill_20040929_chaptered.pdf。

[4]2007 Federal Sentencing Guideline Manual,Chapter 8，Sentencing of Organizations，§8B2.1“Effective Compliance and Ethics Program，”详见http://www.ussc.gov/2007guid/tabconchapt8.htm。

12.3.2　来自顶层的推动力，通过合作实现

我的一位客户曾经打电话给我，表示公司的管理层下达了一个通知，说是需要实现一种联合认证解决方案，并且要求马上完成。我弄清了原委，这家金融机构有许多客户认识到实行一种联合认证程序所带来的丰厚回报。它不仅可以降低系统的成本，还可以增加安全性，部分原因是客户打算升级硬件、采用一种基于口令的解决方案，并对传统的用户名和密码组合进行补充，创建一种双因素的认证系统。

由于这个项目一开始就得到了管理层的支持，因此我们很容易组织起足够的人力起草这项政策。为了处理技术、法律、政策和业务的组合问题，在我的队伍中引进了一个由内部人员组成的多学科的跨部门小组，以处理相关的法律问题。

我们每周举行电话会议，讨论上一周的工作进展，并讨论下一周需要处理的突出问题以及每个功能小组（或它们的组合）需要覆盖的活动。尽管初看上去这种做法加重了管理负担，但是定期举行电话会议极大地提高了生产率，事实上这项任务完成得相当顺利。

12.3.3　数据泄露小虎队

早期的网络攻击常常是技术平平的个人使用众所周知的“工具”针对网站发动攻击（即所谓的“脚本小子”）。在最近几年，由于电子技术日益增加的复杂性，攻击已经变得越来越复杂。（其他章节讨论了这个话题，尤其是Chenxi Wang所著的第4章以及Ben Edelman所著的第6章，它们都集中讨论了与安全攻击相关的经济。）

由于攻击可能非常复杂，并且是多方位的，如果法律队伍能够随时得到技术专家的协助，那么调查数据泄露事件可以起到立竿见影的效果。在本节中，我将描述一个近期的客户案例。在这个案子中，我们需要动用一个跨功能的小组才能充分理解数据泄露是如何发生的（包括接下来该如何处理这个事件）。这个“小虎队”由公司的几位技术成员、总顾问、三位外部律师、内部市场和公关人员、法律公司所雇用的执行这项调查的技术部分的第三方法律队伍组成。

这样的小虎队并不存在管理负担，甚至不需要正式的名分，尽管在大型机构中名分可能大有帮助。简而言之，安全小虎队由精心挑选的表示关键的公司利益（在我们的案子中，还包括局外人的利益）的人所组成，他们的职责是安全意识和倡议、发展和完善安全策略、主动的安全活动、调查任何安全泄密事件，并处理与安全策略相关联的千丝万缕的问题。

在这个案例中，事实上我们在发生数据泄露的第二天开始每天都举行会议（有时候一天要开好几次会），这是为了向其他各方提供所需要的背景，以更快地获得进展。为了提高会议的效率，我们利用了一个开放式问题列表，描述了每个问题，标示了特定问题的“主人”，并指定该问题的预定解决日期、问题的优先级以及它的当前状态。

这里可以得出两个实际上非常重要的经验。首先，让律师参与数据泄露的调查可以获得非同寻常的好处。例如，我们可以声称调查期间所产生一些影响判决（以及任何相关的通信）的材料受到律师-当事人特权的保护。于是这些材料在进入与数据泄露相关的审理之前可以得到保护（例如，如果发现了一个主要的漏洞或者不服从规定的活动）。类似地，如果法律顾问像我们一样雇用了一家外部的安全或法律公司帮助调查，外部公司在安全审核期间所产生的像草案报告和其他结果等可能影响判决的材料也可以要求得到律师-当事人特权的保护。

开放式问题列表的第二个经验是设置每个特定问题的负责人，并规定一个特定的到期时间可以极大地加快问题的解决速度。授权人们开展行动并给他们规定一个到期时间可以清除大量的借口和障碍，就像大多数以目标为导向的项目一样。

12.4　正确做事

在公开场合发言中，我常常要求听众考虑“遵守规定等于安全”或“安全等于遵守规定”这两种说法。当与会者表示“这是捉弄人的问题吗？”对我进行诘难时，我回答说不是，但它们在某种程度上是反问句，答案都是“不”。

当然，服从不等于安全。我们知道在大量的案例中，公司或机构遵循相关的标准或要求，但在安全领域仍然存在缺陷。例如，几个被泄密的机构，包括CardSystems、TJX和Hannaford在遭遇泄密之前已经通过了它们的PCI DSS审核。

类似，并不能因为一家公司是安全的，就能推断出它是遵守规定的。在一个假设的例子中，一个实体从技术的角度可能是非常安全的，但它可能并不遵循一个特定的安全标准或指南。

在结束本章时，我想重复自己的观点，安全应该是有计划地实现，也就是本人所讨论的把目光瞄准在“正确做事”的文化、平衡和通信上。在某种程度上，这个词听去更像是理想主义，而不是务实态度。但是，我看到过一些机构实现了这三者的结合，因此它是可以实现的。一般而言，在信息安全领域，“正确做事”意味着不仅要考虑大量的技术因素，还需要考虑许多策略和流程问题。在后面两个领域，常常会出现法律问题，这时候法律队伍（由机构内的律师和外部顾问组成）就可以大显身手。

具体地说，从事安全工作的人们与律师之间的无缝关系可以扫除困扰我们的大量问题。更通俗地说，也许不再是万变不离其宗，而且可以实现真正的变化，完善整体的安全环境，这是不是很美呢？

第13章　美丽的日志处理

有一句广为人知的格言“知识就是力量”，但是我们应该从哪里去获取我们所负责的包括计算机、网络设备、应用程序框架、SOA Web基础设施甚至是目前还没有发明的未来组件在内的信息技术（IT）组件的知识呢？这类信息最丰富的来源尽管总是可供使用，但常常不被人们所注意。这种来源就是系统和应用程序所产生的日志和审计跟踪。通过日志和审计跟踪以及相关的警告，信息系统常常可以提示我们什么东西出了差错，甚至允许我们把目光投向未来，并告诉我们什么东西将会出现差错。

日志可能还会揭露更多的脆弱性，例如在我们在控制中对规章制度的顺应性方面的不足。它们甚至会对IT的管理产生重要影响，并可能通过扩展影响公司的管理。因此，它们的影响范围甚至会超出IT领域。

但是，有许多日志常常只包含了数据（有时候是垃圾数据）而没有包含信息。我们需要付出额外的努力（有时候甚至付出需要巨大的努力）对数据进行过滤，才能把它变成与IT和业务相关的可以付诸行动的实用信息。

13.1　安全法律和标准中的日志

在一个非常高的层次上，我们可以把日志等同于责任。这并不是一个新思路，而是可以回溯到1983年首次发行的广受推崇的橙色书（《Department of Defense Trusted Computer System Evaluation Criteria》）。在“基本需求（Fundamental Requirements）”一节，我们找到了对日志的需求：

需求4——责任（accountability）——审核信息必须有选择地保存和保护，使影响安全的活动可以被追踪到责任方。受信任的系统必须能够在一个审核日志中记录安全相关事件的发生。

维基百科把“责任”定义为：

责任是一个道德概念，它具有几个含义，常常作为像负责、可应答、实施、问责、义务以及其他与责任期望相关联的术语概念的同义词（[1]）。

在机构内部，还有其他许多与责任有关的机制，但在IT领域日志是最为常见的。如果IT机构无法实现责任，那么它的业务也无法实现责任。因此，如果对日志抱无所谓的态度，相当于对责任也抱无所谓的态度。所有的机构都应该明白这个道理。

同时，日志对于规章制度也有着无可估量的价值。许多最近的美国法律制订了相关的条款，要求对日志进行审核和处理。其中几个最重要的法律是健康保险携带和责任法案（HIPAA）、Gramm-Leach-Bliley金融服务业现代化法案（GLBA）和Sarbanes-Oxley法案（SOX）。

例如，对HIPAA的安全规则小节164.306、164.308和164.312所列出的安全需求和规范进行详细分析后可以推断出与审核该日志有关的条款。具体地说，164.312（b）小节“审核保留”覆盖了患者被保护的健康信息（PHI）的系统审核、日志和监视控制。类似地，GLBA的第501小节、SOX的第404小节和其他条款间接规定了审核日志的收集和审查。跨越各种系统和应用程序的中央化事件日志以及它的分析和报告，共同提供了证明机构所实现的安全控制的存在性和有效性的信息。这些做法也可以帮助确认和减少机构的安全弱点和漏洞所造成的影响，并提供修补方案。随着其他标准（例如PCI DSS、ISO2700x、ITIL和COBIT）成为必将出现的新规定的基础，日志对于遵循规章制度的重要性只会越来越增加。

[1]我注意到负责的定义是在维基百科的“Accountability”项提供的（http://en.wikipedia.org/wiki/Accountability），最近访问是在2009年1月10日。

13.2　聚焦日志

由于与日志有关的正规课程极为缺乏，那么该怎么讲述日志文件和审核记录的例子呢？我们可以根据产生日志的来源对日志文件进行分类，因为它通常粗略地确定了它们所包含的信息类型。例如，Unix、Linux和Windows系统所产生的日志文件与Cisco、Nortel和Lucent所生产的路由器、交换机和其他网络设备所产生的网络设备日志显然是不同的。类似地，防火墙、入侵检测和预防系统和消息安全设备所产生的安全设备日志与系统日志和网络日志都截然不同。

事实上，不同的安全系统在日志所记录的内容和格式方面存在很大的区别。从简单地记录可疑的IP地址到想方设法捕捉所有的网络流量，安全日志可能存储数量惊人的数据，有些内容具有相关性，有些内容则是无关紧要的，甚至有些内容对于判断当时的情况具有欺骗性。

日志术语

日志领域所使用的许多术语（包括日志的定义本身！）具有含糊、误导或多重含义。在有些情况下，这些术语是从其他学科“借用”的，或者不同的人有不同的用法。对于已经难以航行的海面，这种含糊性只会把水进一步搅混。下面是我们在本章中使用的一些术语的定义：

日志消息（有时简称消息）或日志记录对系统、网络或应用程序上所观察的一些活动或一个事件的记录。日志或日志文件存储在一个文件中的一条或多条日志消息的集合。这种文件可以是文本文件、二进制格式的文件、数据库或其他存储媒介中的文件。

13.3　什么时候日志是极为珍贵的

日志被认为是调查的关键部分，事实上常常是需要观察的第一手数据。日志一旦被记录之后就不能通过正常的系统使用途径进行修改，这意味着它们可以作为永久记录（至少在日志被保留的情况下）。因此，它们对系统上的其他数据提供了准确的补充，因为前者更容易被修改或破坏。（前提是管理员遵循了建议，把日志记录到从Internet断开的系统，使它们很难被破坏。）

由于日志在每条记录上都提供了时间戳，因此它们提供了事件的时间顺序，不仅显示了发生了什么，还显示了它们是在什么时候发生的以及是按什么顺序发生的（[1]）。

另外，记录在专用的日志收集器主机的日志提供了与最初来源隔离的证据来源。如果最初来源上的信息的准确性被质疑（例如入侵者可能会修改或删除日志），那么这种隔离的信息来源可能被认为是更可靠的。来自不同来源甚至不同站点的日志可以相互提供证据，增强每种数据来源的准确性。

另外，日志可以巩固在法庭调查期间所收集的其他证据。一个事件的重建常常并不是根据一段信息甚至一个信息来源，而是根据各种来源的数据：系统中的文件和时间戳、用户命令历史、网络数据和日志。偶尔，日志可能会反驳其他证据，这意味着其他来源的信息本身可能已经被破坏（例如，被攻击者破坏）。当一台主机被入侵后，远程记录在其他系统上的日志可能是唯一可靠的信息来源。

正如本章后面所解释的那样，有时候日志中的证据是间接的或不完整的。例如，一条日志记录可能显示一个特定的活动，但并没有记录是谁进行了这个活动。例如，过程记账日志显示了用户已经运行了什么命令，但没有记录这些命令的参数。因此，我们并不总能依赖日志作为唯一的信息来源。

[1]记住前面所提到的修正日志时间的难度。

13.4　日志所面临的困难

根据我们在日志的格式、语法和含义中所发现的混乱，日志的分析甚至将简单的日志收集和保留以便未来之用也存在许多独特的困难。我们将回顾其中的一些挑战，然后讲述一个在调查中使用日志数据的有代表性的故事。

太多的数据

这通常是在考虑日志时所面临的第一个困难。数百道防火墙（对于大型环境而言并非罕见）和数千个桌面应用程序每天可能会产生数以百万计的记录。即使没有其他原因，仅仅是日益增长的带宽和连接性，都会导致每天的日志容量越来越大。

日志消息的巨大容量导致日志的分析需要大量的时间和计算资源。在一个几G字节的文件上，即使简单地使用Unix grep工具（它逐行查找文件中的字符串）也需要10分钟甚至更长的时间。像数据挖掘这种分析，可能需要几小时甚至几天才能处理完如此容量的数据。

不充分的数据

这个问题正好与前一个问题相反。对事故或事件响应的处理可能由于应用程序或安全设备无法记录重要的数据或由于管理员没有预料到需要收集这样的数据而受到阻碍。这个挑战常常是由于日志保留时间太短所导致的。

信息的发布方式不佳

这与前一个问题有点相似。许多日志只是没有正确的信息或者需要花费很大的力气才能从日志中发掘出正确的信息。例如，有些电子邮件系统在不同的日志消息甚至不同的文件中记录已发送的电子邮件和已接收的电子邮件，导致很难得出正确的消息序列或者很难把一条邮件消息与它的响应相关联。

有些日志正好缺少我们感兴趣的关键数据片断。一个有名的例子是没有提示试图进行登录的用户账户的登录失败信息。

假阳性

这个问题常见于网络入侵检测系统（NIDS），它会浪费管理员的时间，并隐藏可能提示真正问题的更为重要的信息。除了假阳性之外（由正常的事件所触发），管理员可能因为处理错误的警报（可能是恶意的但不会对目标造成伤害）而应接不暇。

很难获得的数据

由于政治上或技术上的原因，分析数据的人常常无法得到他所需要的数据，从而削弱了日志管理的功能。另一个比较罕见的原因是由于遗留软件或硬件导致数据难以获得。例如，获得大型机的审核记录往往是非常困难的。

冗余和不一致的数据

冗余数据来自记录同一个事件的多个设备，由于它们记录这个事件的方式不同，可能会造成混淆。对于日志分析而言，这就增加了一个额外的步骤，因为需要执行数据的“去重复”，删除那些“表述相同东西”的记录。

日志并没有统一的标准。大多数应用程序在记录日志时采用了它们的创建者所开发的格式（有些显得颇有创意，有些只能用精神错乱来形容），从而导致了很大的分析困难。日志可能采用花样百出的格式，可能通过不同的端口记录，有时候看上去不一样，但实际上说的是同一件事情，有时候看上去相同，但说的是不同的事情。例如，下面的字符串都提示成功地访问了一个系统，尽管这些系统可能各不相同：

login

logon

log on

access granted

password accepted

除了消息的多样性之外，许多消息还非常含糊。许多系统并没有提供如何产生或解释日志的消息目录。在极端的情况下，由个人程序员作出关于与日志有关的决定，例如格式、语法和消息的内容，常常导致日志完全无法理解，因此出现了一句名言“日志分析是一门艺术”。

另外，除了处理二进制日志之外，还需要处理文本日志，有些文本日志格式散漫，很难进行解析或者从文本转换为结构化的数据。

多样化的IT环境

有多少人只使用一个平台、一段网络、一种安全设备类型以及一个生产商的产品呢？显然不多，大多数公司使用来自多个生产商的多种类型的设备。

多样化的IT环境加深了前面所提到的问题，而且还引入了自身的问题。例如，更特殊的文件格式需要被理解和处理，以获得重点。容量会超出控制，NIDS会被它们所监测的东西混淆，自定义的应用程序日志会使本来就很复杂的问题雪上加霜。

在下面这个调查日志使用的案例研究中，我们将演示这些困难，并将讨论其他的一些困难。

13.5　案例研究：瘫痪服务器的背后

本节的这个例子来源于作者所领导的几个调查。作者把它们组合在一起，在一个很小的空间里提供了几个概念性有趣说明。

13.5.1　事故的架构和环境

出现问题的公司是一家中等规模的在线零售商。这家公司理解网络和主机安全的价值，因为它的业务需要依赖可靠、安全的在线交易。它的内部网络和DMZ设置在设计时就考虑了安全性，得到了最新安全技术的保护。DMZ是一种堡垒网络，用一道防火墙将DMZ与充满恶意攻击者的Internet进行隔离，并使用另一道防火墙把内部网络与DMZ和Internet攻击隔离开来（DMZ到内部网络的所有连接都被阻塞）。

这家公司在防火墙内部部署了一个网络入侵保护系统（IPS），把内部网络与外部网络进行隔离。在DMZ中，公司收集了网络服务器的标准集合：Web、电子邮件和一个专门用于支持一些长时间运行操作的遗留FTP服务器，它是旧时代所留下来的。一些像DNS这样的网络服务则是从外部供应商采购的。

13.5.2　被观察的事件

星期一早晨，这家公司的支持小组接到了一位试图从FTP服务器下载一个大型ZIP文件的现场工作人员的警报。他报告说，他的浏览器在试图连接到公司的FTP服务器时“超时”。由于无法从内部网络通过安全的shell远程登录到FTP服务器，支持小组的一位成员走进了服务器房间，结果发现这台机器已经瘫痪，无法启动。原因很简单，它的操作系统不见了。

此时，公司的事故响应计划被触发。由于安全小组很早就提出这台FTP服务器应该退休，由一些更安全的文件传输方法来代替。这个事故在“FTP的棺材上钉上了最后一颗钉子”，这台服务器顺理成章地不再被使用。但是，安全小组被告知需要完成一项调查，防止其他关键的网络服务中断。注意，这个时候我们并不知道系统的瘫痪到底是由于恶意攻击所致还是由于其他长期存在的原因所致。

13.5.3　调查开始

因此，这次调查的主要目的是找出服务器瘫痪的原因。如果它是由于恶意攻击所致，就需要采取措施保护其他系统服务器，防止这类事件再次发生。调查的主要证据片断是服务器的磁盘驱动器。没有任何现场证人，因为机器是在没人注意的情况下运行时突然瘫痪的，内存中的内容或其他现场数据都丢失了。

但是，我们从防火墙和IPS以及其他DMZ系统中得到了一组日志文件，它们是由一个日志管理系统所收集的。我们本该为日志管理系统从FTP服务器收集到日志而感到高兴，但由于疏忽之故，这台FTP服务器并没有启用远程日志。因此，我们无法从这台FTP服务器本身获取第一手的攻击信息。

我们从审查流量日志模式开始着手调查。

首先，通过对来自网络防火墙的防火墙日志数据进行分析，我们发现有人在距离事故几个小时之前对公司的外部可见IP地址进行了探查。这个人试图连接到DMZ中的多台服务器。当然，所有这些尝试都没有成功，并且被日志所记录。

下面是向我们提供了试图访问所有面向外部系统的快速尝试的证据的防火墙日志记录。（本节中的所有日志记录中的所有IP地址都进行了处理，位于局域网10.10.0.0/16的范围之内。）

Oct 1 13：36：56：%PIX-2-106001：Inbound TCP connection denied from\10.10.7.196　/41031 to 10.10.15.21/135 flags SYN on interface outside

Oct 1 13：36：57：%PIX-2-106001：Inbound TCP connection denied from\10.10.7.196　/41031 to 10.10.15.21/80 flags SYN on interface outside

Oct 1 13：36：58：%PIX-2-106001：Inbound TCP connection denied from\10.10.7.196　/41031 to 10.10.15.21/443 flags SYN on interface outside

Oct 1 13：37：15：%PIX-2-106002：udp connection denied by outbound list\1 src 10.10.7.196 3156 dest 10.10.175.7 53

最后，他连接到这台FTP服务器，如下面这条日志记录所示：

Oct 1 13：36：59：%PIX-6-302001：Built inbound TCP connection 11258524\for faddr 10.10.7.196/3904 gaddr 10.10.15.16/21 laddr 10.10.16.120.122/21

在获得访问之后，攻击者最终把一个文件上传到这台FTP服务器，如下面这条防火墙日志记录所示：

Oct 1 14：03：30 2008 11：10：49：%PIX-6-303002：10.10.7.196 Stored\10.10.15.66：rollup.tar.gz

我们怀疑这个文件rollup.tar.gz包含了一个恶意软件工具包，这个怀疑后来被一项更完整的调查证实。

最后一条记录是另一个令我们颇感郁闷的惊奇。如果不允许来自DMZ的任何连接，攻击者如何进入系统呢？公司的系统管理队伍展开了调查，令人不快的事实浮出了水面：这台FTP服务器具有一个全局可写入的目录，允许顾客上传日志文件以进行故障排除。在许多经典的FTP服务器上，不受限制的匿名上传是可以的。匿名用户可以把文件上传到一个称为“incoming”的目录，并且这个目录的设置很可能采用了最不安全的方式：匿名用户可以读取其他人所上传的任何文件。这就导致了一个风险，FTP服务器可能被匿名的外部方用来存储和交换它们所劫持的软件。

13.5.4　使数据起死回生

分析了网络日志之后，现在是时候在硬盘驱动器上寻找一些证据了。我们决定寻找日志文件的碎片（最初位于/var/log），证实攻击的本质并了解其他细节。调查小组从系统消息日志、网络访问日志和FTP传输日志中找到了下面这些日志片断（幸运的是，这台FTP服务器记录了所有的传输）：

Oct 1 00：08：25 ftp ftpd[27651]：ANONYMOUS FTP

LOGIN FROM 10.10.7.196[10.10.7.196]，mozilla@

Oct 1 00：17：19 ftp ftpd[27649]：lost connection to 10.10.7.196[10.10.7.196]

Oct 1 00：17：19 ftp ftpd[27649]：FTP session closed

Oct 1 02：21：57 ftp ftpd[27703]：ANONYMOUS FTP LOGIN FROM

10.10.7.196　[10.10.7.196]，mozilla@

Oct 1 02：29：45 ftp ftpd[27731]：ANONYMOUS FTP LOGIN FROM

10.10.7.196　[192.168.2.3]，x@

Oct 1 02：30：04 ftp ftpd[27731]：Can't connect to a mailserver.

Oct 1 02：30：07 ftp ftpd[27731]：FTP session closed

（此时，细心的读者可能会注意到我在前面所提到的难题之一：FTP服务器和防火墙日志之间的时间戳并没有同步。）

这个序列提示了攻击者首先用一个浏览器进行查找（留下了标准痕迹mozilla@）。接着，可以推测攻击者运行了漏洞攻击（password x@）。这行消息显示了攻击者对邮件服务器的试图访问也存在预兆。

另外，从硬盘驱动器的FTP日志上得到了下面的信息：

Oct 1 00：08：25 ftp xinetd[921]：START：ftp pid=27692 from=10.10.7.196

Oct 1 00：17：19 ftp xinetd[921]：EXIT：ftp pid=27692 duration=255（sec）

由于公司的外部防火墙规则，从这个FTP服务器到攻击者计算机的所有下载都失败了。但是，此时攻击者已经通过漏洞攻击获得了一个“root shell”权限。

13.5.5　小结

从这次事故可以得出两个结论：首先，这台服务器的确由外部人士用一台位于10.10.7.196（地址经过了处理）的计算机成功入侵；其次，攻击者设法把一些文件放在受害主机上。

总之，这个事故告诉我们，尽管日志存在许多漏洞缺陷，但它在调查事故原因时仍然能够发挥极大的作用。即使删除，它们也常常能够取回。

13.6　未来的日志

目前还显得比较简陋的日志文件在未来将会怎么演变，从而继续在系统管理和安全中扮演关键角色呢？

13.6.1　来源的扩大化

首先，我们应该考虑扩大日志来源的范围。以前的日志来源仅限于防火墙和IDS日志，接着扩展到服务器，现在已经扩展到所有类型的日志来源：数据库、Web服务器、应用程序等。

几年前，所有称职的防火墙或网络管理员至少会扫一眼由早期的PIX或Checkpoint路由器所记录的连接日志的简单总结。事实上，对于日志管理厂商而言，防火墙日志分析在早期提供了大量的商机。许多防火墙用一种syslog格式记录日志，幸运的是这种格式很容易收集和查看。

在下一个阶段，尽管系统管理员总是知道在发生问题时去查看日志，但是直到最近对服务器上的大量操作系统日志进行分析才变成现实。例如，从所有关键的（许多是非关键的）Windows服务器收集日志长期以来由于缺乏像LASSO这样的无人日志收集工具而难以进行。另一方面，Unix服务器的日志分析由于syslog记录中日志内容的格式不统一而受到严重的影响。

通过邮件服务器日志所进行的电子邮件跟踪也同样处于衰退状态。人们只有在出现了问题（电子邮件失败）甚至发生恐怖的错误（外人访问了你的日志）的时候才会关注邮件日志。由于缺乏本地中央化，并且在某种程度上由于使用了复杂的日志格式，导致电子邮件的日志分析进展缓慢。

数据库日志很可能直到去年才成为大多数IT人群关注的对象。事实上，IT人群一直以不用打开DBMS所提供的扩展日志和数据访问的审核功能而津津乐道，但是这个情况现在正在发生变化！预计Oracle、MS SQL、DB2和MySQL都将提供出色的日志功能，只要知道怎么启用这个功能就可以了（并且知道怎么处理它所收集的最终数据）。

接下来是什么？过去生活在自己世界中的Web应用程序和大型企业应用程序框架。但是，人们最终开始认识到这些来源的数据对内部攻击、内部数据盗窃和其他受信任的访问滥用提供了独特的洞察力。预计会有越来越多的这类日志加入到日志管理解决方案。桌面日志分析也不会是太遥远的事情。

在更遥远的未来，各种深奥的日志来源将会继续被添加进来。自定义的应用程序、物理传感器和其他许多不常见的设备和软件被“监听”！

因此，我们观察到人们一般会首先注意防火墙日志，然后是服务器日志，然后是其他电子邮件和Web日志，然后是数据库日志（这是最近才出现的），最终才是其他应用程序日志甚至是非IT的日志来源。

13.6.2　未来的日志分析和管理工具

在本章最后时，让我们想象未来理想的日志管理和分析应用程序，它们将帮助解决前面所描述的各种困难，满足我们所提出的各种需要。

理想的日志管理工具将具有下面这些功能：

日志配置

应用程序将会设法寻找所有可能的日志来源（系统、设备、应用程序等），然后对它们启用正确类型的日志，遵循一个向它所提供的高级策略。就目前而言，这要求这个工具拥有“上帝的能量”，远远超出了当前产品的能力。

日志收集

应用程序将收集它通过安全的方式找到的所有日志（而不需要使用任何具有风险的超级用户访问权限），并且对网络和系统的影响要非常小。就目前而言，这是不可能的。

日志标准

日志管理工具将能够使用我希望在未来采纳的日志标准。当今在日志标准化方面所付出的越来越多的努力（例如MITRE的公共事件表达式CEE）首先将导致日志标准的创建，最终导致它们被采纳。这可能需要几年的时间，但是对将会混乱不堪的日志世界实现部分的有序。

日志存储

只要有需要，并且允许同时通过原始格式和抽象的、丰富的格式访问日志，应用程序就可以安全地按照最初的格式存储日志。这在当前是不可能的，除非舍得在存储硬件上花大本钱。

日志分析

理想的应用程序将能够观察所有类型的日志，即使是以前未知的日志、来自标准来源和自定义日志来源的日志，并告诉用户根据他的需要应该知道他的环境中的哪些内容。什么被破坏了？什么遭到了黑客？在哪里？对规章制度或政策有什么违反？什么将会很快遭到破坏？谁在干这种事情？这种分析将会促进自动化的操作、实时的通知、长期的历史分析和顺应性相关的分析（稍后讨论）。未来类似人工智能系统的开发可能会把这个愿望变为现实。

信息表现

日志管理工具将会过滤数据、信息以及由分析组件所生成的结论，并用一种与用户的角色一致的方式表现它们，不管用户的角色是操作员、分析师、工程师还是执行官。高级的、交互式可视化的、基于文本的并且带有向下挖掘功能的数据表现形式将可应用于所有的日志来源。未来的日志可视化工具不仅可以表现“漂亮”的图画，而且适合它们的用户的任务，不论是操作员还是管理人员。用户也可以根据他们的愿望以及需要完成的任务对数据表现形式进行自定义，包括信息感知风格。这可能需要许多深入理解日志执著的用户接口设计人员的共同努力。

自动化

日志管理工具将能够采取有限的自动化操作，解决已发现和已证实的问题，并且为用户提出指导意见，使他们知道如果不适合全自动模式时应该采取什么行动。它所产生的响应包括全自动操作、协助操作（“点此修正问题”）、显示详细的修正指南。它的输出将包括一个按优先级排列的已发现条项的“怎么做”列表以及相关的建议操作。这个愿望的实现还非常的遥远。

顺应性

审核者还可以直接使用日志管理工具，使用规章制度特定的内容以及所有收集的数据，验证或证明与相关规章制度的顺应性。日志管理工具还可以发现数据收集与用户需要遵循的特定规章制度之间的落差。同样，这个功能需要“上帝的能量”，而且可能永远不会开发出来（但我们肯定可以尝试）。

13.7　小结

正如我在本章中所证明的那样，日志对于完成调查以及更好地遵循规章制度非常实用。尽管存在我所描述的困难，但日志处理确实是“美丽的”。至少，它可以为发现隐藏在事故背后的真相这个美丽目标提供服务。

首先，如果我们展望未来的日志世界，我们将会看到更多的日志：更大的容量、更多的日志来源和更丰富的样式。其次，我们将看到对日志信息的更多需求以及对这类数据的更多应用。再次，我们希望看到更好的工具来处理日志。最后，今天的日志标准化努力将会奏效并使日志世界变得更好。尽管这对于许多人而言只是梦想，但本章的作者却在努力使它变为现实。

但是在与日志有关的难题“改善”之前，接下来几年情况很可能会“恶化”。观察缓慢出现的日志标准化行动和快速增长的新日志类型（例如各种应用程序所记录的消息日志）和新日志用户这两种趋势之间的竞争是非常有趣的。开发像CEE这样的可信任的实用日志标准将需要几年的时间，并且需要很多人的努力。标准的采纳也是一个很大的困难，因为存在太多的遗留日志系统。

另一方面，对规章制度的遵循要求以及事故调查需要将会驱动当今不断增长的跨平台和跨应用程序的日志。在安全社区向开发人员提供有效地安全实用地增加日志功能的方法意见前，开发人员就被要求“添加日志”。我相信日志在未来将会更加统一，但我不能断定这个愿望什么时候才会变成现实。

第14章　事件检测：寻找剩余的68%

2003年1月25日，星期六午夜时分，Internet上发生了灾难性的事件，席卷全球各大数据中心、公司和家庭的数十万台计算机遭到了密集的攻击。蠕虫利用Microsoft SQL Server的一个已知漏洞发动攻击，而这个漏洞最早是在六个月前也就是2002年7月24日被报道的。

根据我们在Symantec对那个安静的晚上的观察，我们的安全操作中心的分析师们像平常一样工作着，对来自全球范围的数以百计的顾客所报告的安全事件进行分析，寻找网络攻击的迹象。但是，平静的水面上突然掀起了风暴，在数分钟之内，我们的分析师们就接到了数以万计的警报。按照分析师的观点，被监控的入侵检测系统同时警告我们出现了“SQL缓冲区溢出”，同时被监控的防火墙也检测到发生在1434/udp端口的密集流量。对于那些允许1434/udp端口穿越它们的防火墙的机构而言，它们的内部基础设施在短短数小时之内就被蠕虫所攻克，在蠕虫不断扩散的同时导致了拒绝服务攻击。对于那些阻塞了1434/udp端口的公司而言，当星期一早晨来临时，当许多在笔记本电脑上安装了MS SQL的用户把他们的计算机接入公司网络时，他们不知不觉地帮助蠕虫穿越了公司的外围防御，直接向内部发动攻击。

这就是我们对SQL Slammer的观察，这是Internet上的一次范围极广的通过Microsoft SQL的一个已知漏洞所发动的攻击，几乎使Internet临时瘫痪。

我们在Symantec的托管安全服务（Managed Security Services）部门已经有了10年的工作经验，遇到过大量的安全事件。虽然Slammer对IT基础设施的打击是极为致命的，但它却非常容易被检测。它所利用的漏洞是已知的，因此攻击很快得到确认，蠕虫的侵略性扩散方式保证了每个安全监控装置都会被触发。

虽然Slammer很容易被检测，但是当前许多网络攻击的手法会变得越来越狡猾。攻击的目的已经从入侵系统使自己名扬天下变成了通过网络犯罪攫取经济收益，其中隐蔽性和匿名性成了最重要的特征。随着攻击意图的复杂性和后果的严重性不断增加，攻击者使用了更微妙的越来越难以检测的攻击方式，在攻击结束时最多只有日志文件中留下不起眼的痕迹，并且伪装成正常的流量。

毫无疑问，对于保护他们的信息资产免受网络攻击的安全从业人员而言，他们所面临的挑战从来没有如此巨大。我们的看法是，许多安全从业人员并没有意识到他们所操作的事件检测程序最多只能发现在他们的网络中所发生的32%[1]的事件。本章探讨了传统事件检测所存在的一些局限以及它们对安全监测程序的效果的不利影响，并讨论了一些用于创建富有弹性的事件检测模型的技术和技巧的应用，从而发现其余的68%。

14.1　一个常见起点

Anton Chuvakin所著的《Beautiful Log Handling》一书的第13章描述了与日志管理相关的挑战以及在调查、调整和管理中使用日志的重要性。在本章中，我们将专注于如何最大限度地对日志进行挖掘，以帮助事件的检测。大多数安全从业人员通过分析专门用于检测的技术所产生的日志来检测网络攻击。这些技术称为入侵检测系统或入侵预防系统，从现在开始我们称之为入侵检测和预防（IDP）系统。这些技术把网络活动与提示出现了攻击尝试或其他未授权活动的模式进行比较。通过这种做法，IDP帮助安全从业人员把数以TB计的潜在问题缩小为少量的真正出现的安全事件。这看上去很简单，那么难度又在什么地方呢？现在，让我们观察一些证明IDP所存在的内在限制的例子。

在SQL Slammer事件期间，这个蠕虫露面仅仅几个小时之后就出现了专门针对它的签名[2]。这个签名非常有效地触发了对那些具有该蠕虫扩散的独特字节模式的数据包的警告，因此对于被感染的主机的检测具有很高的可信度。这个消息听上去令人鼓舞，但是在Slammer的高峰时，大多数公司检测到数以百万计的来自Internet的感染企图，其中绝大多数是没有什么价值的，仅仅是噪声而已，不会对顾客造成实际的伤害。

在任何时刻，Internet上都会发生不计其数的事件，但其中只有极少数需要立即作出响应，以减轻它对业务所造成的风险。确认这些少数事件需要非常特定和耗时的行动，它是任何有效的安全监测程序的主要目标。以Slammer蠕虫为例，意识到这种蠕虫在Internet上的快速感染率当然是比较有趣的，但这种数据对于实现信息资产保护这个目的并没有什么用处。在这种情况下，需要付诸行动的事件就是警告系统管理员他们的环境中有些遭到攻击的主机正试图向其他系统进行扩散的事件。

对于Slammer和其他不计其数的类似蠕虫而言，一种有效的检测需要付诸行动的事件的技巧是根据方向性对检测进行重新聚焦。下面是前提条件：尽管任何网络都会频繁遭到攻击，但大多数攻击都是不成功的。通过寻找自己的环境中开始进行带外攻击并需要立即采取措施以解决问题的主机，就可以非常快速地找到遭到入侵的系统。初看上去这似乎并不直观。确认带内攻击并主动防御入侵不是更好的方法吗？从理论上说确实如此，但是要考虑到在许多情况下从攻击开始到入侵成功只需要几秒的时间，任何非自动化的内联阻塞响应根本无济于事。因此，把注意力集中在带外事件可以提高我们的效率，因为我们只要处理真正的事件就可以了。这种技巧不仅对于蠕虫有效，而且适用于各种类型的恶意软件和其他攻击。为了允许根据方向性进行监测，我们可以对警告进行过滤或者对签名进行重新配置，只报告在自己的网络中进行扩散的事件。经过微小的调整之后，安全从业人员可以把每次遇到攻击尝试时都会发出警告的签名转变为可靠地检测需要付诸行动的安全事件的签名。

安全从业人员还使用了许多其他技巧对来自IDP技术的结果进行调整，最大限度地提高效率。但是，根据我们的经验，即使采用最有效的技巧，IDP所能产生的最好结果就是能够发现在自己的网络上所发生的32%的安全事件。虽然许多安全从业人员相信他们使用了IDP技术有效地对他们的环境进行着监测。但是，他们还需要使用其他技术和技巧，寻找其他68%的安全事件。

[1]根据我们对世界上数百个企业的监测，我们对得到验证的事件进行了检查，确定了哪些发现归功于对它们的检测。根据我们的分析，被确认的事件中只有32%来自于IDP的警告。

[2]虽然大部分厂商在蠕虫露面之后很快推出了它的签名，但许多IDP在Slammer出现之前就已经有了通用的SQL缓冲区溢出签名，可以进行早期的检测。

14.2　改进与上下文相关的检测

一种有效的改进检测功能的技巧是增加与上下文相关的信息，帮助验证是否可以对攻击不予理会。我们可以观察来自IDP的另一个事件的例子。如果我们有一台公共Web服务器，我们可以确保它每天对漏洞进行扫描，甚至更勤快。大多数IDP拥有数以千计的签名，能够检测不同的攻击。下面是一些Snort签名，一般是在Web服务器遭到攻击时被触发的：

WEB-IIS ISAPI.ida attempt

WEB-IIS cmd.exe access

WEB-IIS msadcs.dll access

这些签名是否都是应该付诸行动的事件？也许是。这些签名可能提示一次失败的攻击尝试，但是也可能表示一次对Web服务器的成功入侵。问题是，根据这些签名，失败的尝试和成功的入侵看上去可能是完全一样的。我们需要更多的信息来判断攻击是否成功。通过结合方向性的考量，如果Web服务器开始启动对Internet上其他主机的攻击，就可以证实入侵是成功的。但是，如果攻击者的痕迹更为微妙，我们可能需要寻找其他提示入侵成功的蛛丝马迹。

一个稳定的上下文信息来源是防火墙或路由器的日志。这些日志一般记录了一台主机是什么时候以什么方式连接到另一台主机。例如，主机A通过TCP端口80到主机B的连接很可能表示主机A访问主机B上的Web服务器。另外，取决于流量日志的来源，我们还可能看到在会话中所传输的字节、会话被接受还是被拒绝、NAT转换以及其他信息片段。准确地知道在出现了问题的主机之间还发生了什么对于确定攻击的本质是极为关键的，它可以提供成功入侵的证据。例如，我们可以想象一下，有一次攻击被IDP和防火墙所记录，日志里留下了下面的内容：

1）IDP根据多个被触发的签名，记录了主机A通过TCP端口80对主机B发起攻击。

2）防火墙把这次流量记录为主机A通过TCP端口80连接到主机B。

3）防火墙接着记录了主机B通过TCP端口21（TCP）连接到主机A，下载一个工具包或肉鸡恶意代码。

IDP设备不太可能对第3个连接发出警告，因为这次流量看上去是极为普通的。但是，当这个连接紧随这次攻击之后发生时，就强有力地证明了这次攻击是成功的，因为在入侵之后发生了连接到攻击者的FTP会话。对于经常受到攻击的Web服务器而言，分析师如果只参考IDP警告，就经常需要猜测攻击是否成功。攻击之后的流量是攻击痕迹的一个关键部分，可以轻易地把一个预想的假阳性或者低优先级的事件转变为确定的入侵。简单地通过在IDP日志中增加额外的与上下文有关的信息，可以把检测率大约提高40%。

14.2.1　用流量分析提高覆盖率

IDP的另一个重大局限性是它们只能找到已经存在签名的攻击。新的威胁和漏洞以令人惊异的速度不断增长，常常要求对签名进行更新才能保证检测。根据Symantec的Internet安全威胁报告（[1]），Symantec在2007年的下半年共报告了499，811例新的恶意代码，比2006年上半年增加了567%。对于每个新威胁而言，从它在Internet上的初次露面到出现能够检测到它的存在的签名之间可能存在一段时间。即使是最优秀的IDP技术，这段窗口期的长度也可以持续几小时甚至几周。以W32.Gaobot蠕虫为例，2004年共编写并释放了这个蠕虫的几十种变型。如果发动攻击时所使用的是IDP的签名并不匹配的变型，结果会怎么样呢？如果我们完全依赖IDP进行检测，可能会对许多攻击视而不见。除了根据签名观察单独的数据包之外，如果我们对一段时间内主机之间的交互情况进行分析又会有什么效果呢？如果我们能够用一种足够实时的方式对日志进行组织，有些常见的攻击模型就会清晰地浮现在我们面前。例如，每个被Slammer感染的系统很容易辨认，因为它试图在一个非常短的时期内通过1434/udp端口连接到数以千计的其他主机。由于两个原因，这种行为被认为是不正常的：1）主机一般不会通过1434/udp端口连接到大量的其他主机。2）通过1434/udp端口的连接频繁非常高。通过分析我们所保护的主机在一段时期内相互之间的连接情况，很可能会发现不需要依赖已知签名的可疑流量。例如，考虑下面这些情况：

不是非常可疑

主机A通过TCP端口25（SMTP）连接到主机B

更加可疑

主机A通过TCP端口25（SMTP）连接到1000台不同的主机

第1个例子看不出有什么恶意，很可能表示一台主机试图连接到一台邮件服务器以发送邮件。但是，第2个例子可能表示截然不同的事件。如果主机A是一台邮件服务器，这很可能也是正常的活动，因为邮件服务器频繁向Internet上的其他主机发送电子邮件属于预期行为。但是，如果主机A并不是邮件服务器，这个行为就变得非常可疑，很可能表示使用SMTP作为感染向其他主机进行自身扩散的恶意代码。让我们观察另外一个例子：

其他信息主机A通过TCP端口25（SMTP）连接到100万台不同的主机

首先，我们重新分析主机A是一台邮件服务器的情形。这可能表示主机A发送了过量的合法邮件。另一方面，如果邮件服务器除了为企业发送正常的邮件之外还被垃圾邮件发送者所劫持，向世界上数以百万计的人们发送垃圾邮件，情况又会怎么样呢？为了警告类似这样的事件，我们还必须把流量作为检测标准的一部分。由于每台服务器和每个企业可能具有不同层次的活动量，因此用一个固定的门槛表示预期的连接很难产生良好的效果。

但是，一种可以采取的技巧是为主机A在一段时间内的正常邮件活动设立一条基线，并确定它的合理变化范围。假设我们确定主机A在30天内平均每天建立50万个连接，标准差为24小时的窗口期内10万个连接。如果在一个后续的24小时窗口期内，我们检测到主机A建立了100万个连接，这个统计数字提示这个连接量远远超过了平均值，要求进一步的检测。虽然这个技巧会出现假阳性，因为流量出现异常波动的可能性也是存在的。但是，它可以检测出那些用其他方式检测将会忽略的事件。我们发现这个技巧对于检测那些垃圾邮件发送者所使用或者受到某种类型的DOS攻击错误配置的邮件服务器非常有效。

IDP在具有详细的签名时可以实现精确的检测，但这个特性也可能使它们对那些同等有效的事件检测方式视而不见。反之，像上面这样通过检查网络流量寻找不同寻常的流量模式，完全可以根据攻击者与网络上其他主机的交互对攻击者进行有效的检测，而不必考虑是否能够看到实际的攻击。这个技巧具有什么样的价值呢？考虑到恶意负载尝试在入侵成功之后所采取的最常见活动之一就是搜索其他存在漏洞的主机，光凭这个技巧就可以使企业内部的攻击检测率增加大约66%。

[1]Symantec的Internet安全威胁报告，第13卷（2008年4月）。

14.2.2　对监测列表进行综合分析

在过去的几年里，网络攻击的动机已经从获取名声转变为通过窃取机密信息攫取经济利益。黑客们保护自己身份的隐蔽性并感染大量的系统以进行非法交易为目标，创建了由被恶意软件感染的系统所组成的网络，称为肉鸡网络，或简称为肉鸡。一个肉鸡可以由Internet上数百台、数千台甚至数万台属于未察觉的家庭用户和公司的计算机所组成。它们一般都是在访问受感染的网站时由于客户端的漏洞受到攻击而被黑客秘密接管控制的。每台被接管的计算机都称为肉鸡，或称为“僵尸”。它们都被安排与“母舰”系统（称为肉鸡命令和控制服务器）中的一个或几个IP地址进行接触，通过一个独特的通信渠道接收后所发出的命令。这种命令可能简单如在受感染的主机上搜索信用卡信息，也可能复杂如针对一台目标Web服务器并发地发动分布式拒绝服务攻击。

今天，肉鸡网络已经成为Internet上的一个重大威胁：2007年下半年，Symantec确认受到肉鸡感染的计算机数量超过500万台，连接到超过4000台肉鸡命令和控制服务器。为了强调这个问题的严重性，在Symantec的托管安全服务操作中，我们所检测到的超过50%的付诸行动的事件存在肉鸡感染的痕迹。

检测肉鸡所面临的一个挑战是，除了经常与肉鸡命令和控制服务器进行回调通信之外，许多受感染的主机可能不会产生其他任何可疑的行为，直到几天甚至几个月后发出一个命令。虽然有些更为常见的、广泛传播的肉鸡感染主机网络有公开的签名可用于检测，但大多数未知的肉鸡网络却是IDP无法检测的。如果没有签名，可以使用什么技巧检测肉鸡感染呢？熟悉了肉鸡命令和控制服务器之后，可以揭露这些付诸行动的事件。不管是否存在其他任何痕迹或签名，光凭自己网络中的一台主机连接到Internet上的一个肉鸡命令和控制服务器就应该可以确定系统很可能遭到了入侵。

对于Internet上已知与垃圾邮件、钓鱼、端口扫描或任何其他恶意活动相关联的系统，这个原则也是适用的。如果能够知道Internet上那些恶意系统或地址名，无疑会有很大的帮助。作为托管安全服务供应商，我们的工作由于能够看到大范围的恶意攻击而获益良多。只要我们的一家客户能够根据一次事件就把相应的IP地址标识为存在恶意，其他所有客户也能从中受益。这个适用于Internet安全的“邻居监测程序”可以快速确定犯罪分子的存在。这个概念非常简单明了，但它的功能却是非常强大的。只要简单地在发现与已知的恶意IP地址进行连接的时候对路由器或防火墙发出警告，应该能够找到自己的网络上最危险和最隐蔽的病根。

创建和维护这样的监测列表既不是一项简单也不是一项精确的活动。肉鸡命令和控制服务器可以在一天被分配给一个系统，第二天被分配给另一个完全不同的系统。一个具有高风险的IP地址很可能是个并不知情的公司或家庭用户的受害计算机，只是作为向下一台主机发出攻击的中继。虽然大多数安全从业人员并没有创建一个全面的监视列表的有机功能，但这样的列表作为商业产品或者在公开领域中存在[1]。取决于智能输入和流量日志来源的可靠性和及时性[2]，可以很轻松地把检测率大约提高80%。

[1]公开的监视列表的例子可见http://www.spamhaus.org和http://cbl.abuseat.org/。

[2]有些企业在选择像HTTP这样的带外网络交通时选择了禁用日志。显然，如果你的企业采用了这种做法，很可能无法发现通过这些端口所进行的任何连接。

14.3　使用主机日志增强洞察力

通过网络设备（路由器、防火墙和IDP）对企业进行监测具有很大的优势。由于网络设备的良好覆盖率，分析师能够监测企业的大多数主机（如果不是全部的话）。但是，除了前面所描述的基于网络分析的局限性之外，还存在进一步的限制：网络警告无法提示系统是如何受到攻击影响的。例如，账户是否被成功地创建或访问？文件是否被修改或下载？关键的服务是不是失败？

主机可以利用3项关键技术，通过日志提供对安全事件的洞察力。这3项技术分别是操作系统、服务器应用程序和安全技术。在理论上，利用操作系统日志的概念听起来很重要，但遗憾的是，这些日志本身的价值非常有限。日志一般是有局限性的，系统中的大多数日志用于记录一组有限的活动以及预期的条件和错误。考虑到大多数漏洞会显式地绕过操作系统内部的一些控制，我们可以合理地预计它的日志记录肯定是不完整的，甚至根本就没有被记录。

反之，有些服务器的应用程序，尤其是像数据库和Web服务器这样基于网络的应用程序，倾向于记录所有来自网络和主机的请求。它们可以捕捉被传递的详细参数以及被执行的命令，并提供丰富的上下文信息，有助于发现安全事件。服务器应用程序日志的困难在于如何高效率地从企业内部收集所有主机的日志。在那些由许多不同操作系统组成的大型企业网络中，从中心开始收集应用程序日志是件很困难的工作，在很多情况下并不会被当做优先完成的任务。

在网络中，还存在一些专门设计用于保护主机免受恶意代码和入侵攻击以及检测所有攻击尝试的安全技术。最全面的技术组合了反病毒、个人防火墙、反间谍软件、反垃圾邮件和主机IPS功能。一般情况下，这些技术首先把注意力集中在预防上，然后才是检测。因此，虽然日志中确实有更多的价值，但它们在攻击期间哪些将被记录哪些将不被记录方面还是存在一些局限，并且不同的生产商所提供的功能可能各不相同。由于大多数企业已经部署了反病毒软件并且有许多企业主动更新到更统一的终端安全解决方案，因此在分析时收集和包含这些日志具有确定的价值。它们不仅可以提供更准确的分析，而且填充了IDP基础设施内部由于网络位置而存在的检测空隙。

因此，有了额外的主机日志后，是不是打算得到100%呢？也许不能，但它比本章之初所说的32%显然要高得多。

创建富有弹性的检测模型

即使是最优秀的安全从业人员也很容易陷入一个陷阱，就是在检测安全事件时过于依赖一种技术或一种日志来源。在我们对全球数百家大型公司的安全管理经历中，我们与无数的安全官员有过谈话。我们发现有许多官员对他们的安全监控没有实现预想的结果而倍感挫折，但他们的监控计划仅限于在整个企业内部部署IDP传感器。我们发现许多安全队伍在他们的系统被入侵时浑然不觉，这是由于安全体系结构的不完整，被大量的假阳性警报所淹没，没有办法强调真正的攻击。如前所述，只使用IDP技术进行监测的方法当然可以产生积极结果，但它很可能只捕捉32%的事件。对于有些公司而言，由于预算的限制或者它们的信息资产的性质并不是任务关键的，因此只找到三分之一的安全事件的风险是它们愿意接受的。但是，对于大多数公司而言，如果它们的事件检测功能只能检测出数量非常有限的攻击，这是不可接受的，尤其是在它们的网络包含了员工或顾客的敏感数据的情况下。

如果一家公司的目标是真正缓解与网络攻击相关的风险，它必须拓宽对网络上所发生事件的洞察力。利用前面所描述的部分或全部技巧，可以为事件检测打下一个坚实的基础。这些技巧都非常有效地强调了一些关键的攻击特征，例如无法解释的流量、连接到已知的恶意地址或者表示入侵的明显签名。但是，如果孤立地使用每种技巧，可能无法实现有效的检测。通过结合检测不同来源的日志，可以提供一个事件的不同视角，极大地增强检测真正事件的能力。例如，让我们重新回顾与主机入侵相关联的攻击提示（见图14-1）。

图　14-1　入侵的证据

图中所显示的提示包括：

1）IDP根据已知签名检测到来自Internet的攻击尝试。由于IDP的警告并没有提供足够的细节提示攻击是否成功，因此这个提示只表示试图发起攻击。

2）防火墙记录了返回到发起攻击的主机的一个FTP会话的回应流量。这个额外的上下文信息非常可信地证实了第一次攻击是成功的。

3）主机的日志显示了大约在同时，注册表键HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run的字符串值被添加。它导致了恶意代码在系统启动时被执行。由于注册表键的修改是极为频繁的，因此修改的时机是极为关键的。与提示1和提示2同时发生的注册表键修改能够提示攻击是成功的。

4）被劫持的主机连接到其他系统文件的共享，进行恶意代码的扩散或搜索信用卡信息。这些连接尝试只被防火墙日志所记录。对主机的历史进行快速检查可以显示主机存在异常情况，因此这是另一个高度可信的提示，表示系统已经被劫持。

5）带外攻击尝试肯定不会是预期的或正常的主机行为。这是第3个高度可信的提示，表示系统已经被劫持。

6）与已知的命令和控制服务器通过高端口的UDP连接也相当有力地证明了系统已经被劫持。

7）取决于流量的规模，电子商业站点本身的流量过多并不表示恶意，但是当它与前面的提示相关联时，很可能提示系统正参与一次分布式拒绝服务攻击。

通过结合这些不同片段的证据，毫无疑问可以断定主机已经被成功入侵。在这个例子中，当我们从不同的角度进行分析时，可以从这个事件所展现的各方面的可疑活动中得出结论，但我们并不总是能够得到这样的机会。虽然有些攻击将会触发每个完整性检查工具，但有些攻击在设计时就注重了隐蔽性，但用一种检测技巧很可能无法发现。如果图14-1的漏洞攻击由于不存在可用的签名而没有被IDP所检测出来，那么情况又会怎么样呢？提示1和提示5将没有效果。另外，提示2在第1次攻击尝试时增加了上下文信息，此时它也无法发挥作用。即使在攻击完成了一半时，仍然由于签名不到位而无法发现攻击的痕迹。但是，我们还是拥有足够的信息来证实这次攻击。

通过利用一组范围极广的检测技巧，我们创建了一个富有高度弹性的事件检测模型。在这里，我们使用“弹性”这个词描述了能够发现针对我们的环境所发起的绝大多数攻击的事件检测模型。如果有一次攻击没有被一种技巧检测出来，它很可能会被其他一个或几个技巧成功检测出来。弹性的概念虽然很简单，但令人吃惊的是，它是一个被许多安全从业人员在设计检测功能时所忽略的特性。事件检测模型缺乏弹性所存在的危险是常常会无意识地忽略有些事件，而往往在自己的信息资产受到真正的损害之后才会发现检测的覆盖率上的空隙。下面是一些真实的事件例子，它们可以测试出安全从业人员在事件检测模型上的弹性：

·攻击者通过一种新的还不存在匹配签名的攻击或其变种对主机发起攻击。

·签名常常触发假阳性。

·由于最近的一次网络变化，以前能够监视带内和带外流量的IDP现在只能监测带外流量。

·网络管理员不小心关闭了端口映射，使IDP对所有的网络流量都熟视无睹。

·用于记录防火墙流量日志的服务器对于网络不再可用。

·由于防火墙的性能限制，带外防火墙被配置为不记录日志。

·已知恶意IP地址的监视列表已经过期。

所有这些潜在的空隙都会影响我们观察被攻击的完整图像。事实上，我们应该假设在绝大多数时间内只能看到攻击的部分痕迹。看到攻击痕迹的不同部分的能力越全面，发现事件的可能性也就越大，即使是在不太理想的情况下。

除了把各种检测功能结合为一个整体事件之外，许多企业还发现把受影响主机的资产信息与事件结合起来分析也是非常有效的。让我们回顾一个例子，这次攻击的痕迹要比以前所描述的攻击不明显得多（见图14-2）。

图　14-2　更微妙的入侵证据

在图14-2中，攻击痕迹不太明显，因为3个提示可能导致我们作出2个截然不同的结论：可能是一台主机试图扩散恶意代码，也可能是一台邮件服务器执行预期的操作。让我们详细观察每个攻击提示：

1）IDP对多个签名发出警告。这取决于哪个IDP被触发，邮件服务器触发各种不同的，可信度不高的签名从而产生假阳性的情况并不少见。对于这个例子而言，我们假设这些签名觉得它是可疑的，但不足以将它确认为一个事件。

2）这个提示强调了主机试图通过25/tcp端口（SMTP）与多个其他主机进行连接。实际的目标主机序列可以提示真相。如果这台主机按递增顺序连接到不同的IP地址，这很可能是恶意代码在搜索其他邮件服务器，因为它一般并不是通过这种方式连接到其他服务器。试图连接到看上去随机的IP地址的邮件服务器是非确定性的，因为它可能是正常邮件操作的结果，也可能是由于恶意代码随机地连接到主机所致。因此，这个提示可能非常有价值，也可能毫无用处。我们假设它试图进行随机连接，因此它是一个可信度不高的提示。

3）最后一个提示是在25/tcp端口（SMTP）的增长流量。虽然这个行为很可疑，但邮件服务器的负载变化幅度可能非常大。除非观察到的流量显著地超过了正常的基线，否则分析师很难根据这个提示断定它是一个需要付诸行动的事件。

在这种情况下，临界点并不在于系统正在做什么，而是系统是什么，即它是数据库服务器还是邮件服务器。如果与信息资产的属性进行结合，那么可信度不高的提示也会变成可信度很高的提示。

有许多不同的信息资产属性对于检测安全事件具有类似的作用。有些实用的信息资产属性的例子包括：

主机的操作系统

它可以从当前的配置管理数据库或者通过最近的漏洞扫描获取。例如，假设服务器是基于Unix的，如果它被基于Microsoft漏洞的攻击所瞄准，那么入侵成功的机会为零。

主机上所展现的漏洞

为了让攻击获得成功，系统必须没有针对这个漏洞攻击打上补丁，因此存在漏洞。漏洞信息提供了良好的上下文环境，但是只有在最近执行了漏洞扫描后才具有价值，因为陈旧的信息可能导致虚假的结论。另外，漏洞扫描本身也可能出现假阳性和假阴性，这可能会使分析复杂化，甚至带来误导。

主机的业务或网络功能

网络管理服务器将会发送和接收大量的SNMP流量，邮件服务器将在SMTP上进行通讯。这些信息不仅对于理解预期的网络流量减少假阳性率是极为关键的，而且可以帮助业务管理人员理解与关键的业务功能相关联的系统。

主机上所存储的数据类型

主机是否包含信用卡信息、社会保障卡号码、病人健康信息、知识产权或者日常的工作文件和电子邮件对于开发安全架构和保护策略是极为重要的。虽然对于事件检测并不是特别关键，但这些关键性相对次要的系统可能会降低未来调查的门槛。另一方面，如果一个作为PIC DSS审核组成部分的系统遭到入侵，可能会对规章制度的遵循以及客户的风险带来巨大的影响。

DHCP契约日志

许多攻击痕迹都围绕着IP地址。大多数企业为除了服务器之外的所有计算机分配动态地址，因此对一个影响192.168.5.4的事件作出响应可能是没有用处的，因为这台物理主机现在可能使用了一个不同的IP地址。如果能够访问DHCP日志，我们就可以查找MAC地址，它对于每个物理网络接口都是唯一的。

每个属性都为事件的评估增加了一个判断依据，最终可以增加判断事件是否应该付诸行动的可信度。它们还提供了与主机的业务功能有关的上下文信息，允许进行快速的商业影响评估，以便作出适当的响应。

14.4　小结

尽管IDP为执行事件检测提供了基础，但它们具有许多内在的限制，需要使用其他技术或技巧实现更全面的覆盖率。在本章中，我们了解了各种不同数据来源和观察点对于提高检测率的重要性。最重要的是，富有弹性的事件检测模型允许使用一组范围很广的技术和技巧，使那些能够躲过一种或几种检测技术的事件能够被其他技术所捕捉。富有弹性的事件检测模型还保证了在监测基础设施的某个部分的不可避免的失败（不管是由于人的错误、应用程序错误还是网络错误）时将得到其他检测功能的弥补。有了正常的数据来源和检测技巧，我们应该能够创建一种富有弹性和健壮的安全监测程序，能够找到网络中的大多数安全事件。

第15章　无需真实数据就能出色完成工作

最大的隐私破坏都是由于数据窃贼偷取企业或政府的数据库内容所造成的。设想一个能够完成实际工作却又不含有任何有用信息的数据库。例如有一台能够回答问题的服务器，这些问题都是一些关于你所购买的物品、你的下周四日程安排、你喜爱的电影以及其他无数的类似琐碎问题。这种情形和许多其他连接到互联网的数据库类似，但是如果有人偷偷地穿过防火墙，破译了密码，或者发现了其他得到机器上超级用户控制权限的方法，那么他将找不到任何有用的东西。即使是邪恶的黑客或者忍者偷偷地进入摆放服务器的机房并且把硬盘挂载到取证分析仪上，也找不到任何有用的信息。

像这样的数据库看起来不太可能。一个并不清楚下周四将会发生什么事情的数据库如何来回答关于下周四日程安排这样的问题呢？某个地方一定藏有数据，对不对？

也有人建议用次优的解决方案来保护敏感数据。但是即使这些敏感数据是在某个电子安全设备之中，而这个设备位于一座网络堡垒内部的一间虚拟的石砌小屋之中，在堡垒之外是一条不可越过的软件护城河，河里全是能够溶解任何数据的数字酸液，数据也依然存在而且对某个足够聪明以致能够伪装成特权用户的人来说，仍然是有安全危险的。在本章中，我所提出的解决方案比较独特。我承认数据是存在的，但是以只对最终用户有用而对其他任何人都没有用的形式存在。数据在发送给服务器之前由最终用户或者客户端对其进行混乱，当客户端取回服务器的返回数据时再进行解码。虽然数据仍留在数据库中，但除了正确的所有者之外，对其他任何人都是不可理解的，也几乎是没用的。这种方法与简单地使用大多数数据库中都内置的加密函数对数据库进行加密有点不同。数据库加密是由数据库本身处理的，在存储和检索终端都存在可读版本的数据。数据可能是以加密的形式存在的，但是数据库能够对其进行解密，然后进行检索并给出结果。如果服务器知道整个过程，那么所有能够攻入服务器或者网络的人都可以获取这些数据。

这种解决方案的技巧在于不要把数据库看做是一个什么都能看到的预言机，而把它当做一个对所有经过它的信息流一无所知的数学交换网络。然后选择性地对数据进行伪装并作半透明式的存储，这样敏感的数据就隐藏起来了，而非敏感信息仍然保持明文。换句话说，不要把数据库看做是一个受保护的堡垒，而把它看做是一个冗长的、不可攻击的、到处都是分散数据的透明云彩。

这种方法不需要任何高深的数据理论或者复杂的计算机科学知识。人们已经很好地掌握了对信息进行加密和解密的工具，这些工具能够有效地在现代CPU上运行，而且相对比较稳定。35年的公开研究给我们提供了一个广泛的可选加密函数范围，也提供了各种机制用于创造对其存储的信息一无所者的数据库。

这个系统也存在一些局限和缺陷。如果数据库真的什么都不知道，那么它无法向程序员和依赖这些数据的人提供同样可信的基础以及像一般数据库那样的稳定性。如果有人忘记了她的密码，即解密数据库中所有数据的钥匙，那么这些数据就会永远无法还原了。如叶芝所言，再也保不住中心（[1]），因为中心什么也不知道。

这种方法有价值吗？它确实有价值，而且比大多数预想的还要大。当我第一次开始研究这个想法的时候，一个朋友这样说：“这个主意不错。但是没有任何实际意义。如果互联网想要读取我们的思想，那么它就要时刻掌握我们的动机。”我不这么认为。接下来进行测试。我曾根据这个想法大致搭建了一个类似于亚马逊（Amazon）这样的大型在线商城，我发现这个商城能够提供所有最好的在线商城所具有的功能，而不保存任何个人信息。此时，只需以半透明的形式来存储数据，即一部分是可读的，另一部分是不透明的。

15.1　数据半透明化的工作原理

这种商店看起来会是什么样的？解释它的基本设计模式的最好的办法是通过一个例子来说明。表15-1给出了一个传统的商城中所用的数据库表。

这些都是售货员和制定营销决策的人所需要的信息。他们需要知道卖出了什么以及卖给了谁。

人们对这些数据库各有不同的本能反应。有些人很喜欢他们，而且把这种效益看做是社会的一种网络收入（Brin 1998）。其他有些人担心这些信息可能会被用作其他目的。我们有保护人们电影租赁记录的法律，因为这种数据在Robert Bork的一次参议院听证会上成为了焦点（Etzioni 1999）。随着时间和习俗的改变，没有人知道将来这些信息会包含什么。吸烟曾经十分风行，而且也是一种可接受的按下生命终止键的方法。现如今，一些公司会因为抽烟而解雇员工以减少医疗成本。未来的社会会怎样处理你目前的购物记录呢？

一家服装店可以通过宣布隐私保护条约来缓解消费者的顾虑，并且将顾客的消费记录采用某种万无一失的策略保存起来，但仍然还会发生隐私泄露的情况。这种情况有故意为之的（Wald 2004，Anon.2003，Shenon 2003），也有无心的（Wald 2007，Zeller 2006，Pfanner 2007，除此之外还有很多），但是即使你采用安全的堡垒模型也不可能阻止这种情况的发生。

半透明化方法可以解决这个问题，将某几列使用一种单向散列函数来进行混乱，比如SHA256。SHA256是一种著名的散列函数，它是不可逆的。就是说，如果有一个字符串x，你可以很容易的得到SHA256（x），但是如果你手上是SHA256（x）的值，却不可能得到字符串x [2]。如果对客户的姓名进行散列，那么原来的明文姓名就可以用BASE64（SHA256（姓名））的256位计算结果来替代。结果类似于表15-2[3]。

销售部仍然可以从数据库中得到卖出了多少条卡其裤。市场部仍然能够从中寻找客户的消费模式，比如ick93LKJ0dkK这个顾客同时买了一条裤子和一件衬衫。但是，他的名字却是经过混淆的。

单向函数的价值在于把隐藏信息和让信息始终是可靠的这两种看起来不相容的目标组合在了一起。也就是说，James Morrison可以将他的名字的散列值以ick93LKJ0dkK这种形式交给商店，而商店也能够知道数据库中相应的记录是属于他的，但是却无法知道他的真实姓名。这种方法能够用于一些担保条款中，或者是常用的购物程序以及自动建议工具中。能够根据顾客已往购买记录来计算出顾客消费建议的网站同样也能够根据数字化名ick93LKJ0dkK来计算。

当James Morrison使用这些功能时，他可以提供他的名字以查看这些计算结果。他不需要记住ick93LKJ0dkK这个字符串，因为他电脑上的软件能够通过计算BASE64（SHA256（姓名））来重新得到这个值。在这之后，ick93LKJ0dkK就成为一个正规的姓名。

混乱后的表中原始数据没有多大用。一个疯狂的毒品犯不可能只花几个小时就能从一个半透明化的药店数据库中找出麻醉处方（Meier 2007，Harris 2004，Butterfield 2001）[4]。但这种方案还存在一些缺陷。如果James Morrison的邻居恰好在这家商店工作，那么这个邻居仍然能够查出James Morrison买了哪些东西，只需将名字正确的拼写出来就可以了。这个不足可以通过要求James Morrison记住一个秘密口令来消除，即由SHA256（姓名+口令）来计算出化名。这是一个很好的解决办法，一旦James Morrison忘掉了这个口令，那么所有的记录都将销毁[5]。这也是为什么这种级别的安全最适合于那些不太正式的应用程序的原因，比如在线商城。对于价值较高的记录而言，这种方法可能没有什么意义，比如银行的存储证书。

[1]Pace Yeats,the center cannot hold，叶芝，爱尔兰著名诗人，这句诗取自于他的《基督降临》（The Second Coming）一诗。

[2]用户需要密切关注对SHA256这类函数的攻击研究情况，已有许多类似的函数被证明远没有达到完美的单向。例如，对诸如SHA256这类的普通散列函数使用HMAC机制进行封装以提高安全性也是很有意义的。

[3]256位用BASE64来表示通常是42又3分之2个字符。可以用BASE64（SHA256（姓名））的前n个字符来保存原来的空格字符。用更短的唯一字符串来表示也可以，这是一个目标。

[4]药店有记录药卖给哪个人的要求，这种要求限制了半透明化方法的应用。但是也可以在本地分店使用半透明化的数据库，而在总部保存真实姓名。

[5]由拼错了密钥或者是忘记密钥所导致的错误可以用多种技术来限制，比如删除空格、全部转换成大写字母，或者使用读音码（SOUNDEX Code）。其他的解决方案在《半透明化的数据库》中有详细的介绍（见15.6一节）。

15.2　一个现实的例子

半透明化的数据库可以通过多种方式来节省系统管理员的时间和精力，免除对他们所管理的数据的担忧。假设有一个记录了保姆工作日程安排的数据库，家长们可以进行搜索以查看在某个特定的晚上哪位保姆有空。它提供了一种减少家长和保姆之间进行多次麻烦的电话联系的服务。我构建了一个这样的系统以研究我的想法的价值。这个系统要求向客户分发一种牢靠的散列函数。中央服务器不需要做任何的代码修改，因为经过散列处理的个人信息也是字符串，它能够代替姓名。

这些保姆的信息在安全方面一直存在一些没有解决的麻烦。这些信息被用于恶意目的的潜在风险很大，而且一旦如此，造成的损失也是相当严重的，但是没有人对这种系统做出大的经济支持以解决这个问题。这些保姆不像美国政府，他们没有钱来维护这种系统以保护“核发射码”。

这么多的信息都集中在一块地方，而且只有一个管理员的密码来保护。半透明化数据库能够降低这些信息的危险。与存储每个保姆的姓名所不同的是，工作日程表能够以化名的形式来进行存储，如表15-3所示。

那么家长们又如何来访问这种日程表？由保姆来选择哪些人有访问的权限，这是通过计算SHA256（家长的姓名+家长的密码）来实现的。然后他们会在一张类似于图15-4所示的访问表中添加一条记录。

家长们可以通过输入他们的姓名和密码来登录以生成他们的别名。然后他们可以查询这个访问表来找出保姆姓名的别名。这就使得他们能够查询日程表以查看是否有合适的保姆。如果保姆没有将某位家长的别名放到第二张表中，那么这位家长就是被拒之门外了。如此，保姆的信息就处理控制之中。

家长们怎样知道到底是哪个保姆？第三列中包含了使用家长的姓名与密码进行加密的保姆姓名。这样的话，每一行保姆的真实姓名就可以由该行中相应的家长的姓名与密码进行解密。在这种情况下，第二列的别名就可以由SHA256（家长的姓名+家长的密码）来计算得出，之前也说过，密钥的计算稍有不同，即SHA256（家长的密码+家长的姓名）。如此，家长们只需要有一对信息（姓名和密码）来计算他们的别名和密钥，但是没人能够从公开的别名中猜测出秘密密钥。

15.3　为便利而存储的个人数据

我的系统最大的好处在于保护了商店为了方便而记录的数据。许多商店，不管是实体店还是在线商城，都会将顾客的送货地址、信用卡和其他个人信息保存到一些文件中以避免每次购物时都重新输入这些信息的麻烦。

保护这些信息的一种方案是把传统的加密技术和单向散列函数组合起来使用，然后利用单向散列函数对姓名和密码进行计算以得出密钥。即对地址和信用卡号用SHA256（密码+姓名+填充字符）计算得到的密钥进行加密，其中填充字符是某种额外添加的随机值，以确保生成的密钥不同于用来替代姓名的数字别名SHA256（密码+姓名）。

在商品货物离开商场之前，商家需要一直保持跟踪顾客的送货地址。然后他们就可以将该数据删除，只保留加密的版本，直到顾客再次光顾。

15.4　如何权衡

将数据库转换成一堆难以理解的二进制数据究竟有没有意义？以下是我所认为的这种方法所具有的几点优势：

更低的安全成本

这些二进制数据应该对任何拥有管理权限的人都没有用处。这种系统甚至更易于将整个数据库都分发给几个网站，因为每个单独的网站所拥有的信息较少。如果数据是不可理解的，那么泄密就不再是问题。

更低的计算成本

服务器不需要对数据进行加密或者使用SSL会话协议。而客户端却拥有足够多的计算周期来消耗，所以使用客户端来代替。

更有效的数据库操作

这也许是一种次要的效果，但是一个好的单向散列函数应该让数据库密钥均匀分布于散列空间。如果你使用一些类似于姓名之类的字符串，那么你将会被那些常见的单词和姓名所纠缠，以至于让数据库索引变得更慢。

但是，使用这种方法也需要付出一些代价：

没有负责恢复的超级用户

如果服务器管理员没有权限来读取数据，那么他也没有修改数据的权限。中心不能主动为之使用半透明化数据库的商店无法给用户提供服务，除非用户提供了他的别名才能启动服务。商店是不可能发出像这样的电子邮件通知的，“你曾买了A、B和C，所以我们想你也许会喜欢correlation_function（A,B，C）。”但有些人认为这恰恰是它的一种功能，它能减少垃圾邮件。

客户端变成了目标

如果服务器是中立的，那么黑客就会转而攻击客户端，他们会利用强大的密钥嗅探器，社会工程和其他的恶意软件等手段。这时候用户也许就不是一名好的系统管理员，他们可能不擅长阻止这种行为。但是，攻击一个单独的客户端所产生的危害要远小于攻击一个中央服务器。

加密了的数据更加脆弱

对于姓名来讲，一个简单的拼写错误是可以恢复的，但是对于SHA256（x）来说，一位的变化都会使结果产生巨大的不同（[1]）。

这些考虑说明这种办法最适合于保护那些比较温和的敏感数据，即使这些数据丢失了也不会产生真正的后果。一位丢失了访问某家商店购物历史权限的顾客也不会产生多么严重的不便。但是，一位不能访病历的病人可能就比较麻烦了。

这种方法的最好的用途之一是用于多层次数据库。医院可以把病人的记录原封不动的保存在一个中央服务器中，同时也可以使用半透明化的方法将数据发送给研究人员以做统计分析。研究人员仍然能够使用真实的数据而不必担心隐私保护的问题（Schneier 2007）。

[1]在《Translucent Databases》中介绍了几种比较好的减少这种危险的方法，在某些情况下，还能消除这种危险。

15.5　进一步深入

单向散列函数只是解决数据保护问题的开始。由密码学家发明的其他传统的数学工具都能够以同样的方法用于隐私保护。例如，隐写术的应用能够将一个隐藏的数据层和真正的层相混合，有效地创建了一种双重数据库。只有拥有正确的隐写层解密密钥的用户才能提取第二层的数据。公钥加密方案也能够创建一种单向函数，但是只要拥有私钥的人都可以对数据库进行解密。在《Translucent Databases》（参见15.6节）中对这些方案都一一作了详述。

数字堡垒是传统的存储敏感信息的方法。那些最好的数据库公司里有很多天才，他们给数据加上了层层坚固无比的保护层，有访问级别、安全模型和许多其他的角色管理方案以允许正确查询，并阻止可疑的行为。这些“不可攻破的”系统有它们的地位，但它们一般都比较复杂，昂贵而脆弱。最好的方法就是不保存任何真实的数据，而让用户自己去掌握他们的信息的命运。

15.6　参考资料

佚名，“Betraying One’s Passengers，”《纽约时报》，2003年9月23日。

David Brin，《The Transparent Society》，New York：Basic Books，1998.

Fox Butterfield，“Theft of Painkiller Reflects Its Popularity on the Street，”《纽约时报》，2001年7月7日。

Amitai Etzioni，“Privacy Ain't Dead Yet，”《纽约时报》，1999年4月6日。

Gardiner Harris，“Tiny Antennas to Keep Tabs on U.S.Drugs，”《纽约时报》，2004年11月15日。

Barry Meier，“3 Executives Spared Prison in OxyContin Case，”《纽约时报》，2007年7月21日。

Eric Pfanner，“Data Leak in Britain Affects 25 Million，”《纽约时报》，2007年7月22日。

Philip Shenon、John Schwartz，“JetBlue Target Of Inquiries By 2 Agencies，”《纽约时报》，2003年9月23日。

Bruce Schneier，“Why‘Anonymous'Data Sometimes Isn't，”《在线》，2007年12月13日。http://www.wired.com/politics/security/commentary/securitymatters/2007/12/securitymatters_1213.

Matthew L.Wald，“U.S.Calls Release of JetBlue Data Improper，”《纽约时报》，2004年2月21日。

Matthew L.Wald，“Randi A.J.v.Long Is.Surgi-Center,No.2005-04976.”纽约中级上诉法院，2007年9月25日。

Peter Wayner，《Translucent Databases》，Flyzone，2003。http://www.wayner.org/books/td/.

Tom Zeller,Jr.“U.S.Settles With Company on Leak of Consumers'Data，”《纽约时报》，2006年1月27日。

第16章　铸造新词：PC安全剧场

天空中阴云密布，大地上笼罩着不安的气氛。盗贼们在大路上肆无忌惮地游荡，路边的每棵大树后面都隐藏着张牙舞爪的怪兽，巫师一边念着咒语一边把护身符交给行人，声称能够向他们提供保护。我们的主角相信灵符具有强大的威力，能够神奇地使自己变成金刚不坏之身，战胜前进路上的一切艰难险阻。

但是，我们的主角受到了欺骗。目不识丁的农民们在他身边喋喋不休地重复着护身符小贩的胡扯。但他很快将会发现，这些咒语和魔法完全不能代替铠甲利剑以及头脑里的良好计划。

尽管这看上去像是鬼怪小说的开头，但它却反映了计算机安全的现状。

问题并不在于我们用于保护自己的计算机的解决方案的质量。实事求是地说，当前的许多安全解决方案是非常出色的，是由具有奉献精神、经验丰富并且天赋非凡的人们所创建的。但是，当我们观察安全的总体状态时，目前这些成果更像是错误的方向和魔法师的符咒，而不是负责的有效策略。

我们需要的是一种新的安全策略，能够更好地利用当前的工具，并指导开发新的工具。古代的炼金士和药剂师在化学和医学上留下了许多有价值的发现，但他们的深刻见解只有在现代科学观得到发展之后才被证实是有效的。对于当前大量的反病毒和反间谍软件产品、防火墙、沙盒等工具，情况也是这样的。除非我们采用了一种新的安全方法，否则这些常见的工具在不远的将来会变以无能为力，从长远来说也是偏离了方向（对于大部分而言）。我们的研究和开发通过虚拟和现实（持续的）系统的管理，通过使用人工智能（AI）检测反常行为，通过加速的反病毒产品的开发，开启了新的保证安全的可能性。本章讨论了一些驱使我们转向这种解决方案的问题，并介绍了我们当前在一个称为Returnil的产品上所做的工作。

16.1　攻击不断增加，防御不断倒退

为了理解我们现在处于什么位置上，我们必须回顾安全市场的主体是在什么地方偏离轨道的。我们并不需要追溯到20世纪70年代第一个真正的病毒出现的时候，也不需要回望80年代针对日益流行的IBM兼容PC所设计的新的恶意软件的时候，而只需要回顾90年代末期Internet泡沫繁盛的年代。当时有大量的专业知识投入到网络防御的开发之中，但是战争的天平还是向垃圾邮件发送者和间谍软件发行者倾斜。

16.1.1　在Internet的传送带上

链条的第一个环节是反病毒厂商之间的区别的消失。直到20世纪90年代中期之前，恶意软件的传播主要通过在一起工作或者相互熟悉的人们之间交换被感染的文件。因此，它在不同的地理位置之间的传播不会非常快速，这就导致了许多小型的、区域特定的解决方案供应商的出现。

随着Internet的不断普及以及恶意软件通过电子邮件地址的传播不断增加，显然反病毒厂商需要扩展它们的视野，处理“其他人的问题”。虽然许多大型供应商仍然抵制这种变化，但是那些率先改变思维的厂商由于大肆宣传它们的检测功能而收获了丰硕的成果。这个转变导致了一段时期的联合，较大的公司开始合并或者收购竞争对手，或者购买竞争对手的技术和OEM许可。从实用的角度出发，这也是快速获取大型样本数据库最为简单明了的方式，避免花费大量的时间使用传统的方式获取样本数据，可以直接复制自己的竞争对手已经完成的研究。

因此，这个行业被“越大越安全”的市场信息所改变。由于媒体对恶意程序开始关注，这个行业也随之繁盛。有些读者可能还记得有些攻击所造成的焦虑，例如Pakistani Flu（1986）、Jerusalem（1987）、Morris（1988）、Michelangelo（199）、Melissa（1999）、VBS/Loveletter——“I Love you”（2000）、Code Red（2001）、Nimda（2001）、Klez（2001）、Blaster（2003）、Slammer（2003）、Sobig（2003）、Sober（2003）、MyDoom（2004）等[1]。每个新发现的病毒都会使媒体产生日益增长的沮丧感，反病毒行业利用这个趋势成功地在人们心中产生恐慌，促使顾客购买它们的产品。

有趣的是，另一个助长人们对网络攻击的恐惧感的关键因素与Internet没有任何关系，但它深深地印在每个人的脑海里，这就是2001年9月11日。不论读者本人有没有受到这场悲剧的影响，它毫无疑问增加了人们的恐惧感。结果，人们对所有类型的安全都更加重视了。

尽管消费者安全领域的联合直到今天仍在继续，但它的紧迫性已经被削弱，因为各个厂商不再投资必要的时间和金钱开发独有的数据库。现在，它们在专业性方面展开竞争，声称自己把注意力集中在某种特定类型的攻击上（银行木马、游戏木马、恶意程序工具包、间谍软件、键盘记录器、垃圾邮件/钓鱼、恶意网站等）。这些区别实质上常常并不十分重要，但是却产生了蛊惑人心的广告效果。

即使随着特性的增强，新的检测技术的出现以及反病毒服务所增加的复杂性，我们可能会震惊地发现反病毒解决方案的整体效果其实是在不断地下降。最近对随机PC人口的研究显示，尽管运行了主流的反病毒产品，但仍有六分之一的用户很明显受到了恶意软件的感染，这就证明了现在的安全技术虽然很实用，但绝对是不够充分的。

相同的研究显示这个问题变得越来越糟糕。反病毒产品在检测它们所要针对的攻击类型时的成功率从2006年的平均50%下降到2007年的30%。

为了公平起见，大多数研究（如果说不是全部）由于使用了有限的样本集合而存在缺陷，无法表示所有可能的恶意软件样本的真正数量。我们不应该严肃地使用精确的数字，但这个结果仍然提示了值得警惕的趋势。不管统计数据的真实性如何，这些研究显然并没有为当前的方法增强信心。稍后，我们将观察反病毒和反间谍软件产品的失败原因。

[1]参见http://en.wikipedia.org/wiki/Timeline_of_notable_computer_viruses_and_worms。

16.1.2　不正当行为的回报

链条的下一个环节直接来自Internet泡沫，投资者将大把大把的金钱投资在那些思路与Internet哪怕只有极微弱联系的人的身上，差不多相当于街上有个陌生人走到你身边，低声说：“嗨！我有个不可思议的想法，能够让您赚钱……”

这个灾难的其中一个幸存者是广告行业，这个行业在热情拥抱Internet时找到了金矿。它们的圣杯就是有针对性的广告。他们认识到了数字网络在记录和筛选数据方面所具有的无可比拟的带来机遇的能力。因此，他们奋力投身于发掘用户账户并跟踪他们的冲浪习惯的运动中，创建与行为、偏好、位置等因素有关的统计数据。然后，他们向其他人出售这些信息，后者根据这些现实世界的数据设计具有针对性的广告。

他们的技巧取得了引人瞩目的成功，获得了巨大的利润，从而驱使人们进一步开辟从信息集合中获取利益的途径。最初适度的针对式广告是免费软件的弹出式广告，提示用户支付费用或者提供网站广告。随后，捆绑式广告组件变得越来越复杂，它们跟踪用户的冲浪习惯以及其他所有可以通过挖掘主机得到的相关信息。

在恶意软件编写者发现可以通过这些被利润蒙住了双眼、希望自己的内容出现在用户的计算机上而不管它们是通过什么途径到达那儿从而使广告商赚钱之前，这些广告最多只不过浪费用户的时间罢了。赚钱的动力使恶意软件编写者振作起精神，应用他们的丰富知识欺诈性地安装广告组件。这使他们能够从那些极端愿意把自己的金钱投入到不管用什么方法，只要能够提高它们的广告安装率的合法广告商那里并获取相应的回报。这种由最先进的犯罪活动所带来的新收入来源促进了一些变化，导致了今天所使用的相当有效的行业级恶意软件发布技术。（参见Chenxi Wang所著的第4章和Ben Edelman所著的第6章。）

16.1.3　暴徒的响应

人们并没有预料到主流广告和恶意欺诈的结合，这种情况经过一段时间之后才被发现。不同的独立研究人员和网络管理人员小组注意到了这类欺诈的存在，成立了专门的新闻组（后来是在线论坛），讨论阻止和消除它们的方法。

注意到这些程序所造成的私密性和安全性问题与反病毒公司声称要与之作斗争的活动非常相似之后，研究人员和管理员开始向反病毒公司施压，要求它们采取行动对抗欺诈型的针对式广告。但是成名的公司却对这个请求缩手缩脚，它们把入侵式广告活动仅仅看成是一种流氓式商业活动，而不是恶意攻击。

隐蔽性和安全性社区的活动家和热心人士开始收集在他们的技术论坛上的分散信息，目标是帮助他们的成员找出攻击的来源。这些信息一开始毫无条理，没什么实用价值。可以实际检测和消除恶意软件的指令非常稀少，并局限于手工执行的步骤。渐渐地，包含了不太完整的指导意见的研究报告慢慢变得常见起来。很遗憾的是，这些论坛还包含了一些不正确的甚至是危险的建议。

迈向反间谍软件措施的实用实现方案的第一步是由GRC.com的Steve Gibson迈出的，他和许多人付出了很大的努力，向公共和媒体宣传间谍软件的危害，并开发了第一个称为OptOut的清理工具。这个工具可以处理一些当前已知的广告攻击。这个工具在设计时主要考虑了概念可靠性，因此它的效果相对有限，主要是为了推动其他人采取下一个步骤开发真正的商业隐私解决方案。

在发布OptOut之后不久，德国的一位曾经参与过这些讨论的开发人员接过了接力棒，开发了一种更为健壮的可以像商业的反病毒程序一样定期更新的工具。这个工具在更新时并不是通过重写来处理每个新发现的广告程序或威胁。他认识到（这个社区的其他人也抱有同样的观念）——强调反病毒方法（对新攻击所进行紧急的、持续进行的搜索，并频繁发布可以处理它们的补丁）最终将会失败。但是，由于当前的技术限制，研究人员接受了这个模型，因为这是他们唯一可以用来转变以满足紧密和日益增长的需要的实用工具。

这样，一个新的以解决间谍软件所造成的威胁为己任的行业诞生了。具有讽刺意味的是，隐私社区和反病毒服务用来让公众意识到它们的问题的推动力是同一种恐惧信息（包括真实的和设想的）。新产品吸引了越来越多的媒体的注意，推动了反间谍软件的成长，从一家单独的公司发展为今天的反病毒甚至防火墙来解决问题的有机组成部分。

这个变化为反病毒公司又赢得了一线生机，甚至开启了新一轮联合的大门。现在已经出现了大量独特的恶意软件签名数据库，反病毒软件公司想把它们融入到自己的产品中。明白了病毒和间谍软件之间明显的相似性之后，它们走到一起只是时间问题。

这有什么不对劲的地方吗？它的极大成功抑制了开发新的惯用法以及处理它们的产品中明显缺陷的迫切动力。只要公司能够保持竞争优势，能够通过具有吸引力的方式扩展供应并增加收入，在发展研究和创建方面总是动机滞后于实干。这些因素加上它们所面临的激烈竞争环境，将会抑制它们在战略上任何真正的改变。

简单地进行历史总结，尽管越来越多的安全专家充满热情和作出令人称赞的贡献，但过去15年里反病毒和反间谍软件行业的主要特征就是穷举式集合以及对现有恶意软件的分析。通过一种“越大越好”的方法，它们的产品吸收了竞争产品的优点并对研究人员设计的最初产品做了创新，增加了它们的复杂性，以处理具有创造性的新威胁。现在我们探讨，为什么我们已经遇到了这种方法的瓶颈，为什么它已经成为一种不充分的方法。

16.2　揭穿假象

在本节中，我们将回顾每种类型的安全解决方案，观察它是如何保护计算机的以及它会在什么地方失败。

16.2.1　严格审查：传统的和更新的反病毒扫描

第一个解决方案是传统的、基于签名的反病毒/反间谍软件过滤器。这种程序把文件与一个定期更新的不良内容的“黑名单”进行比较。如果这个文件位于这个列表中，过滤器将根据黑名单的指令阻止或删除这个文件。任何没有出现在黑名单上的东西都被认为是良好的。

黑名单方法的演变

事实上，当人们连接到Internet之后，黑名单方法就变得过时了，因为它允许恶意软件跨越地理边界，在人们的眼皮底下传播。

反病毒软件供应商毫无疑问很早就意识到了签名方法最多只是一种短期的解决方案，最终将无法提供先进的可靠保护。为了使用这种方法提供保护，生产商首先必须掌握恶意软件的样本。接着，它必须对它的签名进行更新，以便正确地确认和删除目标内容。

问题在于研究人员只有在恶意软件被发布之后才能隔离并查看它的样本，以前有时要滞后几个月甚至几年。到目前为止所发现的基于Windows的最危险的恶意程序工具包之一的Rustock.C就是这方面的一个好例子，它在Internet上肆虐了超过一年之后才被发现、分析并添加到检测签名中。即使是每日的更新也无法向生产商提供足够的时间寻找、分析和发布针对新恶意软件的防护措施，因此用户容易受到未知攻击的伤害（所谓的0 day攻击）。

根据这些描述，一种很合理的结论是，研究人员所寻找的是恶意软件的旧版本，其他恶意开发人员和“用户”有足够的时间来应付。每种恶意攻击很快改变为某种全新的东西，或者把它的有些功能融入到其他东西中。

而且，尽管反病毒公司所维护的开发队伍的人数可能高达几百，但他们所积压的恶意软件的确认和签名生成任务也在不断地增加。他们每天从他们的“honeypot”网络和其他来源接收到数以万计的新的怀疑项。简而言之，寻找恶意软件的成本不断上升，而反恶意软件产品相对于恶意软件总数的检测率却在下降。

人们很早就意识到，基于签名的扫描系统虽然在过去被证明是行之有效的，但是由于恶意软件的巨大数量以及它的极快变化率，它注定只是一种单纯防御，尤其是最近出现行业化的、有组织的恶意软件开发，使恶意软件的总量只过了一年（2007）就增加了100%。下一个解决方案是前一种的一个变型，在它的基础上增加了试探法。这种方法拓宽了防护程序的有效性，但它有一个非常遗憾的趋势就是会增加假阳性率，就是把良好的文件看成是不良文件。这种方法并不会使用户更加安全，而是会使他麻痹（多次叫喊“狼来了！”的结果）或者增加他的恐惧。前一种情况使系统对攻击大开方便之门，因为警告会被忽略。第二种情况会导致用户寻求更多的护身符，因为不管广告是怎么宣传的，实际上并没有一种扫描方法能够100%地检测到所有的恶意软件。

白名单方法

另一种方法是不再依赖黑名单的可靠性，而是把注意力集中在哪些东西实际上是没有问题的，这种方法称为白名单。对于反病毒厂商来说，这应该是非常简单的，只要简单地禁止任何新的或未知的东西就可以了。

实现这种方法的一个途径是使用反可执行文件。正如它的名称所提示的那样，这种防御措施阻止程序被运行或激活。这种策略相当于Windows Vista用户经常看到的恼人对话框，迫使他们在申请一些日常管理活动后点击一个按钮。

常规的智慧告诉我们，如果一个恶意文件无法被激活，它就无法感染我们的系统。但是，为了实现正确的功能，完全依赖于维护一个干净的白名单。如果用户犯了错误，允许一个恶意文件添加到白名单中，事态就严重了，保护措施就将失败。

更加烦人的是反可执行文件无法区分真正良好的程序和被劫持而运行了恶意内容的良好程序。如果出现了这种情况，事态就会再度恶化，用户将会失去保护。

基于主机的入侵预防系统

这种方法的一种变型是使用基于主机的入侵预防系统（HIPS）。从概念上说，HIPS结合了免签名反病毒程序、防火墙、行为控制和沙盒（将在后面描述）。尽管大型机构通常在专门设备上运行功能强大的监测软件，对它们的网络中的每台主机进行审查，但HIPS应用程序实际上栖息在用户的计算机（主机）中。

这种程序根据它所学习或者编制的一组规则，寻找恶意行为或可疑行为。遗憾的是，良好程序和恶意程序常常展现出相似的行为，因此HIPS必须确定这些行为背后的实际意图。这是它的致命缺陷，因为这种程序必须依赖用户来确定什么是好的，什么是坏的。一旦用户作出了错误的决定，就会和反可执行文件一样，对他的保护将会遭到破坏。另外，这些程序极端地消耗资源，会对系统的性能和用户的体验造成不良影响。

反病毒和反间谍软件所执行的扫描是一种令人绝望和震惊的入侵式方法，即使在它们工作良好的时候也是如此。用户经常会抱怨它们会导致性能下降，尤其是在应用程序启动的时候。这些产品所要求的与操作系统的集成度导致了它们自身的脆弱性，软件中的缺陷甚至会引入新的攻击向量。

在一定程度上，安全是一种简单功能。一种策略越是复杂，它遭到失败的可能性也就越大。我们相信终端用户并不需要多重扫描、高级防火墙、进程监视、过滤器和阻塞器（它们是现代的反病毒系统的常见元素）才能获得安全。

应用人工智能

在我们的Returnil公司（http://www.returnil.com），我们试图通过行为分析、人工智能和机器学习来简化恶意软件的确认以提高效率。Returnil与一个虚拟环境共同运行，它还拥有我们将在16.2.2节所描述的一个创新特性。

分析是通过我们的Advanced Monitor System（AMS）系统执行的，它阻止任何表现出我们之前定义为恶意行为的进程（换而言之，这是某种形式的黑名单），并把具有不可识别行为的进程报告给中心AI系统。因此，任何新的或者与以前存在本质不同的行为都由AI系统进行处理。

我们的AI知识库把提交给它的进程分为三种类型：良好、恶意或不确定。不确定的进程继续由AI系统的学习引擎进行监视，进一步进行归类。在极少数情况下，把它提交给研究人员作出最终决定。假阳性和假阴性结果继续导向反馈循环，以完善AI学习引擎。

16.2.2　沙盒和虚拟化：新的银弹

当用户认识到反病毒软件的入侵本质后，他们会关注当前的技术，为自己留条后路。他们表示：“如果我无法用反病毒产品杀死恶意软件，我可不可以至少将它隔离，使它不伤害我的计算机？”

答案既是肯定的，又是否定的。就像沙盒这个名称所提示的那样，它允许用户把一个未知的或潜在有害的应用程序放在一个“气泡”中，使它不再与文件和系统中的其他敏感部分进行接触，同时由安全程序密切监视它是否有恶意行为。我们可以把它看成是把应用程序放在一个孤岛上，使我们可以控制对它的所有访问。

这本身是个安全的想法，但它在实际应用中却很容易遭到失败。用户的错误和恶意软件中的特定设计元素都会引起“泄漏”，导致被隔离的“孤岛”和计算机的“大陆”之间的有限连接。沙盒还和到目前为止所描述的其他解决方案一样具有一个显著的后果，就是对“气泡”内的程序的性能和操作系统的总体性能都会产生影响。但是，这种后果并不像传统的虚拟化那样极端。

虚拟机、主机和宾客

有些人建议使用传统的、成熟的虚拟化作为安全工具。这个概念本身很简单，但它需要大量的精力和成本才能正确地工作。最终，它无法提供任何真正的保护。大多数人听到过或知道像VMware这样的程序，它是对实际“主机”的计算机内部的“客端”运行一台真正计算机的一个模拟。事实上，这些类型的程序经常被恶意软件研究人员所使用，在各种不同的操作系统中测试恶意内容，使他们并不需要安装和使用一台真正的计算机，后者经常需要照看，以便在每个测试会话时对它进行重置。

但是，这并不意味着真正的计算机受到了保护或者是安全的。用户错误地把一个受病毒感染的应用程序植入到他的真正（主机）计算机中是相对容易发生的事情，这样病毒就可以使用共享文件夹从客端操作系统转移到主机操作系统。如果客端操作系统允许连接，也可能通过Internet感染。

虚拟化同样会占据大量的资源，因为我们需要对诸如可用的RAM和硬盘空间一样对一些东西进行划分。这可能会严重影响宾客操作系统的性能，如果我们试图在主机操作系统上以本地的方式运行其他程序，那么将会严重主机操作系统的性能。另外，我们仍然需要使用与真正的计算机所使用的相同安全应用程序，它们将会消耗更多的资源。

安全特定的虚拟化

我们并没有在可视化技术的顶部拼凑一个安全环境用于其他目的，而是在Returnil上开发了一个精简版的虚拟化解决方案，直接处理关键的系统文件与日常用户行为的隔离。当用户的系统启动时，我们就锁定“现实世界”的系统不被修改，创建一个虚拟系统展现给用户。对用户而言，系统看上去和其他任何Windows系统没什么两样。从理论上说，用户甚至不必知道系统正在沙盒中运行。让用户在虚拟沙盒中运行时偶尔会出现的一个副作用就是系统作为整体受到磁盘的碎片或损耗的影响较小。

在默认情况下，虚拟系统是一份新鲜的拷贝，与之前的会话没什么改变。但是用户可以选择在虚拟系统（例如My Computer、Favorites、System logfiles）中保存系统文件夹，这样用户可以访问他在重启时真正想要保存的数据。而且，我们还向用户提供了一种把文件保存到真实环境的机制。本章“桌面安全的更佳实践”一节将描述这个过程的安全性。

如果前面所描述的AMS监测软件（在16.2.1节）检测到不良行为，它立即自动重启虚拟系统，通常这是个非常快速的操作，不会给用户带来很大的不便。即使恶意内容出现在用户在重启时想要保存的一个文件夹中，导致这些内容存在危险的注册表也修改只存储在虚拟系统的注册表中，因此会在重启时会清除。用户并不需要作出他们所运行的程序是否安全这样不合情理的决定。

图16-1描述了我们的监测系统和虚拟化沙盒之间的交互。

图　16-1　Returnil用于捕捉恶意软件的过程

Returnil中被保存文件的安全性

如前所述，我们的虚拟环境允许用户把文件保存到真实环境中。当他们执行这种操作时，我们应用了一种全面的扫描，同时使用传统的签名数据库和行为来分析检测恶意软件。大多数反病毒和反间谍软件程序会在用户试图完成工作时对系统进行持续全面的扫描，包括所有正在运行的进程、PC注册表和磁盘的关键区域。这些软件按照一定的间隔花费一小时甚至更多的时间扫描所有的文件。这是一个巨大的并且极为消耗时间的任务，涉及数十万的对象。Returnil通过在一个动态的基础上使用相对轻量级的行为检测，绕过了所有这些开销，只对保存到真实环境的个别文件展开全面扫描。当我们实际只需要扫描一个对象时，为什么要对10万个对象进行扫描呢？

16.3　桌面安全的更佳实践

计算机用户可以采取什么措施提高当前不满意的安全状态呢？

首先需要接受一个现实，世上并不存在银弹或全能的灵符能够使你的计算机免受任何恶意软件感染的侵害。我们建议了两个指导原则：

·安全是为了评估和降低风险，而不是让入侵变得不可能。

·越简单的方案往往是越有效的。

一个相关并且众所周知的深度防御原则建议混合使用多种解决方案。

这并不意味着用户必须求助于“越多越好”的资源耗竭、入侵式和恼人的HIPS产品或沙盒方法。尽管这些产品对专家在某种特定的环境下具有很大的价值，但是对于许多复杂配置并不知情的普通用户而言，这些方法并不适用。

真正的神奇来自于作出正确的决定并选择出场阵容，使安全链中的每个环节结合起来形成一个坚固的整体并具有最小的开销。换句话说，使用一种智能的层次式方法，把注意力集中在最可能遇到的威胁。我们相信Returnil对人工智能和虚拟化的组合使用是一个良好的起点。另外，还应该确保具有可靠的备份和紧急恢复过程。

尽管可供选择的程序数量非常惊人，每种都有自己独特的利弊。但是，如果采取符合逻辑的思路，缩小选择范围并不是一件困难的事情。

第一个步骤是确定自己的目标。接着，可以选择在自己的环境中能够最好地实现目标的程序。一般情况下，需要覆盖3个主要范畴：

预防

这可以通过防火墙、在不同的分区隔离程序和数据存储、注意软件的定期安全更新、电子邮件过滤器、Internet内容建议、家长控制、用户账户控制（即保证用户的有限权限）、可执行文件和驱动器控制、策略限制和安全实践（常称为安全冲浪或安全HEX）。尽管“通过隐藏实现安全”被研究人员所轻视，但是通过不合常规的命令约定对敏感文件和数据进行伪装也不会有什么害处。

检测、分析和消除

这仍然是坚固的、一致的反恶意软件解决方案的天下。坚持使用一种前沿的产品，因为它们现在都可以处理病毒、木马、蠕虫、恶意程序工具包、间谍软件和其他委婉地称为“潜在的不需要或有害的应用程序（大多是指广告）”。

治愈

虚拟化、数据备份或复制以及紧急恢复提供了从被感染状态（包括其他常见的用户错误，例如删除了关键的文件）恢复的方法。后者并不总是为人所知，它是由镜像解决方案所组成，每隔一定的间隔创建硬盘驱动器的一个“图像”，之后可以恢复这个镜像，通过简单的重写磁盘消除所有恶意软件。

16.4　小结

计算机安全行业和媒体已经联合起来创建了一个环境，恐惧、误导信息、有缺陷的比较和侵略性的广告在公众面前肆意泛滥。这个行业宣称提供了我们所需要的保护，但它们的许诺并没有成为现实。

只要访问任何安全或隐私论坛，总会看到大多数“讨论”涉及帮助成员们确定和消除恶意或广告内容，然后就觉得这样就使他们受到了保护。事实上，过了一段时间以后，这样的话题成为这些社区最主要的任务。这个行业并没有解决恶意软件问题，而是助长了这种风气，直到它排挤了以前与基本隐私和安全问题有关的建设性话题。

简而言之，要按照与处理个人安全相同的方式处理PC安全。注意你的环境，确保自己所使用的工具是否正常，注意你所经过的地方，并对紧急情况有所准备。对自己所使用的工具进行简化，对安全策略进行配置，为问题的每一片段都提供最高层次的保护，在尽量不与其他片段重叠的情况下实现对核心竞争力的保护。

例如，如果你正在度假，那么你不会在订好一家旅馆的同时带上帐篷，以防旅馆的屋顶漏雨。如果真的出现了这种情况，那么你会换个房间或者干脆换家旅馆。类似，如果你在后台已经运行了一个反恶意软件程序，为什么还要用一个带有间谍软件检测功能的防火墙呢？如果你判断一个解决方案是不充分的，你可以简单地换一个能够更好地满足需要的其他解决方案。

安全并不神秘，并不需要变得很复杂才会显现效果。你可以做个深呼吸并开动自己的脑筋。和本章开始时我们的主角一样，你所需要的是一套优质盔甲（一个简单的软件防火墙、虚拟化和紧急镜像）、一把锐利的宝剑（检出率很高的反病毒扫描程序）和一个简单有效的计划，以便处理自己的旅途中很可能遇到的威胁。

附录　作者简介

Ed Bellis目前是Orbitz Worldwide的副总裁兼首席信息安全官（Chief Information Security Officer,CISO），负责所有信息和电子资产的保护和安全，并负责Orbitz Worldwide广大国际业务单位之间的顺应性和行为规范。这些资产包括Orbitz、CheapTickets、eBookers、Away.com、HotelClub、RatesToGo、AsiaHotels和Orbitz for Business。

Ed在信息安全和技术方面具有超过16年的经验，他曾经为几家财富500强公司保护信息资产。在加入Orbitz之前，Ed担任美国银行全球企业和投资银行部门负责企业信息安全副总裁。他的资历还包括在Ernst＆Young、福特汽车公司、计算机科学公司和Young＆Rubicam公司担任安全技术和管理角色。

Ed是国际注册信息安全专家（CISSP）、注册信息安全管理专家（CISM）和注册支付卡行业安全管理专家（CPISM），也是ISM社区的一位撰稿人，并且也是支付安全专业人员社团（Society of Payment Security Professionals）的顾问成员。他还经常在北美和欧洲的各种信息安全事件上发表言论。他曾经在开放式Web应用程序安全项目（OWASP）、系统管理系统学院（MIS Institute）、CXO Media、信息技术专业人员协会（the Association of Information Technology Professionals）、技术执行官俱乐部（the Technology Executives Club）和美国商业旅行协会（the National Business Travel Association）等场合发言。读者可以通过他的博客http://cleartext.wordpress.com了解他的更多著作。

Jon Callas是位安全专家和加密专家，他是PGP公司的创始人和首席技术官。他曾经为几家公司工作，包括Counterpane Internet Security、苹果（Apple）公司和数字设备公司（DEC）。他同时还是一些安全协议的共同起草人，这些安全协议包括OpenPGP、DKIM、ZRTP和Signed Syslog。

Anton Chuvakin博士是Qualys的PCI顺应性解决方案的指挥者，他负责定义产品的路线图、创建产品需求、市场定位以及与关键伙伴的合作。

Anton也是一位知名的安全专家和作者。他参与编写了《Security Warrior》（http://oreilly.com/catalog/9780596005450/index.html）（O’Reilly），并为《Know Your Enemy,Second Edition》（Addison-Wesley）、《Information Security Management Handbook》（Auerbach）、《Hacker’s Challenge 3》（McGraw-Hill）、《PCI Compliance》（Syngress）、《OSSEC HIDS》（Syngress）以及其他著作有所贡献。Anton还在业余时间通过他的博客http://www.securitywarrior.org发表了许多关于各种安全主题的论文。他最近的演讲地包括美国、英国、新加坡、西班牙、加拿大、波兰、俄罗斯等国家。

Anton是从LogLogic跳槽到Qualys的。在LogLogic期间，他持有日志布道者的头衔，他的使命是向全世界传播日志在安全、顺应性和操作上的重要性。就职LogLogic之前，Anton在一家安全信息管理公司工作，扮演战略性产品管理角色。

Anton拥有Stony Brook大学的博士学位。他的个人网站是http://www.chuvakin.org。

Mark Curphey毕业于伦敦大学皇家哈洛威学院，20世纪90年代中期以成人学生的身份获得了信息安全专业的硕士学位。皇家哈洛威学院最近因密码学校而闻名，这是因为在畅销书《达・芬奇密码》中，密码专家Sophie Neveu就是在皇家哈洛威学院接受教育的。Mart曾经在伦敦的几家投资银行工作，从事各种技术项目，包括PKI设计、Windows NT安全、策略开发和单点登录系统。之后，他在亚特兰大创办了一个顾问小组，在Internet Security Systems（现在是IBM的一个部门）开展安全评估活动。

2000年下半年，他接受了Charles Schwab的一个工作，创建和管理全球软件安全计划。其中，他负责保证所有商业应用程序的安全，保护数千亿美元的顾客投资。在这段时间内，他开创了OWASP，也就是开放式Web应用程序安全项目。

2003年，他加入了一家名为Foundstone的新兴小公司，并把他从Schwab所学到的经验传授给其他财富1000强公司。2004年10月，这家公司由McAfee公司兼并，Mark随之加入了McAfee公司的管理层，直接向董事长汇报工作。

2005年，Mark接受了Visual Developer Security的Microsoft MVP奖项，以便表彰他的社区工作在推进软件安全自律方面所作出的贡献。2006年11月，他离开Foundstone回到欧洲，花了一年时间认真思考信息安全管理平台的设计。一年之后，他加入了Microsoft任职总监和产品组总经理，目的是使这个平台变为现实。当前，他虽然在英国有一所房子，但是经常在飞往雷德蒙德的飞机上生活！

Brian Dunphy是Symantec公司托管安全服务机构的资深运营总监，负责全球800余家公司7×24小时的入侵监测，其中包括92家财富500强公司。任职Symantec之前，他担任Riptech（2002年8月由Symantec收购）的托管安全服务部门的分析运营总监。他利用自己从政府部门获得的经验，重新创建了一支一流的安全监测队伍，为Riptech的2001年Internet安全威胁报告做了许多幕后分析工作。这是最早根据经验记录Internet攻击状态所进行的分析。

在就职Riptech之前，Brian是美国空军的一位官员，曾担任美国国防部计算机紧急响应小组（DOD-CERT）的事故响应领导人。他以这个身份领导了许多引人注目的美国国防部事件的技术调查、响应和对策，包括Solar Sunrise和Moonlight Maze。他与情报部门、联邦执法机构以及其他全球响应队伍有着密切的工作联系。作为Solar Sunrise的结果，Brian为1998年计算机网络防御联合任务部队的创建作出了贡献，并为它的前两年运营提供了技术指导。他还创建了下一代称为Centaur的入侵检测功能的骨架，通过国防部的未分类网络，向美国国防部提供了Internet攻击的可见性。

在过去几年里，Brian频频在国际上就各种不同的安全话题发表意见，包括安全威胁的状态、确认攻击的技术、美国公司所面临的风险和挑战。另外，他是Symantec公司的主要发言人之一，就安全问题的爆发与公众进行关键信息的交流。他拥有卡耐基・梅隆大学的电子和计算机工程学士学位，是一位注册信息系统安全专家（CISSP）。

Benjamin Edelman是哈佛商业学院的副教授。他的研究集中在市场设计，尤其是电子市场和Internet广告。他最近的工作包括对以点击为基础的广告拍卖的替代结构的收益进行比较、对早期不充分的拍卖系统所导致的损失进行量化。他还对有些在线广告机制的稳定性和真实性进行了分析，并设计了一个模拟招标环境，根据经验对招标策略进行评估。

在他的研究过程中，Ben发现了在线广告欺诈的所有行为，从不可见的横幅广告到点击欺诈和间谍软件。他在自己的网站上经常宣传一些主要的广告诈骗手段，并协助广告网络和广告商防止这类诈骗。

Fernando Francisco在信息安全和技术方面拥有超过10年的经验，曾在几家主要的PC安全公司担负高级执行官职务。他是在F-Secure开始他的信息安全生涯的，这是芬兰的一家Internet安全公司。接着，他成为Lavasoft Ad-Aware队伍的早期成员之一，是它的策略和业务发展部门的副总裁。他和他的队伍创建了Ad-Aware，它是世界上下载、使用和发布最多的PC安全产品之一。最近，他曾担任一家英国安全软件公司Prevx负责经销渠道的副总裁。凭借他把新的创新安全产品引入市场的丰富经验以及快速获得市场渗透的能力，Fernando与他人合伙创建了PC安全公司Returnil，并且担负了这家公司的CEO。Grant Geyer目前是Symantec公司的托管安全服务（MSS）机构的副总裁，负责该业务的策略、运营、服务交付和损益。Symantec公司的MSS通过全世界的安全运营中心向超过800家的大型企业客户提供高度引擎化的、远程交付的可伸缩解决方案，为它的客户管理数以千计的设备。

Grant此前是Symantec MSS负责北美和亚太地区的资深总监。通过这个角色，他负责向Symantec MSS顾客提供全天候的保护，免受网络安全威胁的伤害。他在澳大利亚的悉尼启动了安全运管中心，帮助实现ISO 27001和SAS 70第II类认证。在Symantec公司期间，Grant还启动了一些项目，发行日志管理服务和肉鸡警觉保护服务、托管威胁分析服务，把Symantec的DeepSight早期警告服务与MSS集成，并集成了漏洞数据，以便提供全面保护。

在加入Symantec之前，Grant担任了一家托管安全服务供应商Riptech负责业务开发的副总裁，开发并管理与公司的主要服务供应商和技术联盟伙伴的关系。在就职Riptech之前，他担任Lucent Technologies的资深顾问，指导公司为一些行业的全球客户建设各种大型网络基础设施以及操作预约的运营。在加入Lucent之前，他在美国陆军担任了几年的军事情报官员，领导了一些关键的情报任务小组。他在美国西点军校以优异的成绩获得了计算机科学系的学士学位，并在巴尔的摩的马里兰大学获得了工程管理硕士学位。

John McManus博士是专业服务公司Watermark的首席信息官（Chief Information Officer,CIO）。John领导了Watermark的信息技术体系结构的定义和开发，以保证它满足企业和客户的需要。

John是在担任了NASA和商务部的副CIO和首席技术官之后加盟Watermark的。在加入NASA之前，他在位于新泽西州莫雷山市的Lucent Technologies的贝尔实验室的软件技术中心担任副总裁。John还在Cigital公司担任过负责研究的副总裁，他随后加入了NASA的Langley研究中心从事飞行模拟的研究、开发和管理工作。

John的博士和硕士学位是从威廉玛丽学院的计算机科学系获得的，他的学士学位来自兰道夫-梅肯学院。

Elizabeth（Betsy）A.Nichols是一位连续创业者，她利用数字知识开发卫星任务优化、工业过程控制、战争游戏、经济模型创建、企业系统和网络管理以及最近的安全度量指标的解决方案。2007年，她和她的丈夫Joseph创建了PlexLogic LLC，这家公司使用软件作为服务交付模型，提供了市场急需的度量指标。她是公司的CTO和工程副总裁。

在创办PlexLogic之前，Betsy合作创办了另两家软件公司，担任CTO和工程副总裁。第一家公司是Digital Analysis Corporation（DAC），用于实现网络和系统管理软件。后来Legent公司收购DAC，而Legnet又在20世纪90年代中期Internet繁盛时被Computer Associates（CA）收购，当时正是Unix、Web和网络兴起的时候。Betsy成为CA的旗舰产品Unicenter的两位主要架构师之一。DAC技术变成了Unicenter的实时代理基础设施。在Betsy任职CA期间，Unicenter的年度收益从5千万美元增长到超过30亿美元。她的第二个公司ClearPoint Metrics是在2000年成立的，这是第一家专门实现安全度量指标软件产品的公司。

Betsy是五本关于微处理器编程和接口教材的合著者，她还在专业刊物和院校期刊上发表了大量的文章。最近，她主管了几家MetriCon Workshop，并为Andrew Jaquith的著作《Security Metrics：Replacing Fear,Uncertainty,and Doubt》（Addison-Wesley）作出了贡献。

Betsy毕业于瓦萨学院，后来又获得了杜克大学的数学博士学位。她的丈夫Joseph Nichols也是一位数学家，他们是合伙创办人和合著者。她有两个儿子，当前分别在加利福尼亚理工学院和麻省理工学院读书。

Andy Oram是O’Reilly Media的一位编辑。Andy从1992年开始就是这家公司的员工，他当前的专业领域是开放源代码、编程和软件工程。他在O’Reilly所做的工作包括美国所出版的第一本关于Linux的书、2001年超级畅销书《Peer-to-Peer》（http://oreilly.com/catalog/9780596001100/）和2007年超级畅销书《Beautiful Code》（http://oreilly.com/catalog/9780596510046/）。他的编程功底和系统管理技能大部分是自学的。Andy常常为O’Reilly Network（http://community.oreilly.com）和其他出版物撰写文章，内容包括与Internet相关的策略问题、影响技术革新的趋势及其对社会的影响。他的个人网站是http://www.praxagora.com/andyo。

Jim Routh是一位注册信息安全管理专家（CISM），他作为一名从业人员、管理顾问和全球金融服务公司的技术职能和信息安全职能的领导人，在信息技术和信息安全领域拥有超过20年的经验。

他当前是Depository Trust＆Clearing公司（DTCC）的管理总监和首席信息安全官。Jim在他的岗位上根据风险管理的最佳实践以及COBIT和ISO 27001标准，设计并实现了一个企业级的信息安全计划。他实现了一个创新的信息安全风险评估过程和一个用于软件开发的安全计划，被认为是该行业的领头人。2007年，Jim当选为美国东北地区年度信息安全风云人物。

在加入DTCC之前，他在American Express Advisors的U.S.卡业务部门的风险管理小组内领导一个顾客信息管理职能，为American Express Financial的制度服务和投资管理业务开展信息技术功能，成为American Express的第一位CISO，并领导了GLBA特定的控制的企业级实现，并在北美的两个不同的银行实体内进行了实践。他还担任了十几家主要金融服务公司的信息技术管理顾问，时间长达12年以上。

Jim是金融服务信息共享和分析中心（FS-ISAC）以及华尔街技术协会的董事会成员。在FS-ISAC，他是2009年程序委员会主席以及产品和服务委员会主席。他还是BITS的安全和风险管理委员会的舵手级委员会成员，同时还是BITS的共享评估程序的执行咨询委员会成员。他也是安全行业和金融市场协会（SIFMA）的风险管理委员会成员。他还是Archer用户组的当值主席。

Randy V.Sabett是位法学博士和国际注册信息安全专家，他是华盛顿特区Sonnenschein Nath＆Rosenthal LLP办公室的一位合伙人，也是Internet、通信和数据保护实践组以及Venture技术组的成员。他为客户提供的顾问服务涵盖信息安全、隐私、IT许可和专利、处理像公钥基础设施（PKI）这样的问题，以及数字和电子签名、联合身份、HIPAA、GrammLeach-Bliley、Sarbanes-Oxley、州和联邦信息安全法律、身份盗窃和安全漏洞等。Randy是美国第44届政府的网络安全委员会的特派员，并被认为是2007版和2008版的Chambers USA负责隐私和数据安全（Privacy＆Data Security）的领导人，这是美国顶级业务律师的标志。美国律师协会的《IP Law and Business》杂志认为他是排名前50的商业律师，并将其列入国际商业律师名人录。

在美国律师协会内部，他是科学和技术法律部门委员会的成员之一。他还曾担任这个部门的信息安全委员会共同主席，并在2002～2004年期间担任该委员会的共同副主席。在这个委员会中，他还担任了《Information Security：A Legal,Business,and Technical Handbook》（2004）和《Digital Signature Guidelines》（1996）这两本书的编辑，并且是2003年PKI评估指南（PAG）的共同起草人之一。

Randy是信息系统安全协会（ISSA）的活跃成员，并为这个协会开办了一个每月专栏。他还是马里兰、弗吉尼亚、哥伦比亚特区和美国专利和商标办公室（USPTO）的律师协会会员。他以副教授的身份在乔治・华盛顿大学讲授信息策略，并且是应用网络安全学院的教研组成员。他还加入了许多信息安全新兴公司的咨询委员会。

Randy还在信息安全市场拥有几年的工程经验，他的工作领域是有效噪声消除。他还作为加密工程师为美国国家安全局工作。他拥有两项美国专利，其中一项是在信息安全领域（美国专利号6 981 149），另一项是在有效噪声消除领域（美国专利号5 440 642）。Jim Stickley是TraceSecurity股份公司战略运营部的CTO和副总裁，也是该公司的创始人之一。作为一名富有声誉的安全专家，他为数以千计的金融机构、财富100强公司、健康监护机构、法律公司、保险公司和政府机构等提供安全服务。他是《The Truth AboutIdentity Theft》（FT Press）的作者，并为《Time Magazine》、《BusinessWeek》、《Fortune Magazine》、《The New York Times》和许多特定行业的出版物（例如《PC Magazine》和《Security Focus》）撰写专稿。他还曾经在NBC的《晚间新闻》、CNN的《新闻晚报》和一些CNBC节目（包括Big Idea和Business Nation）亮相，并多次在NBC的《今日秀》节目中出场。他攻破过全国范围内超到1000台的物理安全设备，并通过被窃取的身份控制数十亿美元。

在没有工作任务的时候，Jim继续作为发言人，在美国和其他国家进行了数百场安全相关的内部预映、会议、研讨班和论坛上发言，涉及的话题从身份盗窃到全国范围的网络攻击。他的个人网站http://www.stickleyonsecurity.com以安全提示和最新警告闻名。

John Viega是McAfee公司的SaaS业务部门的CTO，这是他在McAfee公司的第二个职位。此前，他在成立Stonewall Software并担任CEO之后作为公司的首席安全架构师。这家公司的目标是创建更快、更好、更经济的反病毒技术。John还是Secure Software（现在是Fortify的一个部门）的创始人。

John是许多涉及安全问题书籍的作者，包括《Building Secure Software》（Addison-Wesley）、《Network Security with OpenSSL》（http://oreilly.com/catalog/9780596002701/index.html）（O'Reilly）和即将出版的《Myths of Security》（http://oreilly.com/catalog/9780596523022/index.html）（O'Reilly）。他负责许多软件安全工具，也是GNU邮递列表管理器Mailman的最初作者。他在IEEE和IETF完成了大量的标准化工作。他与他人合作发明了GCM，这是一种美国国家标准和技术协会（NIST）已经标准化的加密算法。John也是一位积极的咨询师，为几家安全公司提供服务，其中包括Fortify和Bit9。他拥有弗吉尼亚大学的学士和硕士学位。

Chenxi Wang博士是Forrester的安全和风险管理研究队伍的成员，经常在研究和行业会议中担任主题发言人。最近，Chenxi成为OWASP北美和亚洲会议的特邀发言人。

在加入Forrester之前，Chenxi是风险管理服务供应商KSR股份公司的首席科学家。在此之前，她担任卡耐基・梅隆大学计算机科学系的副教授。在卡耐基・梅隆大学，Chenxi领导了一些大型研究项目，包括美国国防部和国家科学基金会所赞助的研究项目。Chenxi为联邦贸易委员会、HP实验室、Lucent和一些风险投资公司提供顾问服务。在职业生涯的早期，她在Citibank的公司信息安全办公室（CISO）担任助理研究员，这是美国非常早的CISO委任。

Chenxi拥有弗吉尼亚大学计算机科学专业的博士学位。她的博士论文获得了ACM Samuel Alexander奖。

Kathy Wang是AlphaDetect股份公司的共同创始人和首席技术官。在成立AlphaDetect之前，她是MITRE公司的信息安全工程师。

2004年，Kathy开始研究和开发一个开放源代码的honeyclient原型，并在2005年6月的一次会议上发布和展示。在此之后，Honeyclient项目在MITRE公司通过研究赞助的形式得到了资金，并且Kathy成为这个项目的主要研究人员，向一组工程员提供指导，开发一个更加有效的honeyclient原型。Kathy过去参与的项目包括Morph，这是一个操作系统指纹障眼工具。

在加入MITRE之前，Kathy先后就职于Counterpane、密歇根大学的计算机辅助工程网络（CAEN）和DEC公司。Kathy毕业于密歇根大学安娜堡分校，获得了电子工程学士和硕士学位。她在许多国际会议上展示她的工作成果，包括RSA、DEFCON、AusCERT、ToorCon和RECON。另外，她还被一些恶意软件检测专家组邀请为主持人。

Peter Wayner是15本书的作者，其中包括《Translucent Databases》（CreateSpace）。这本书对作者在本书中的第3章中所描述的思路进行了扩展。《Disappearing Cryptography》（Morgan Kaufmann）对速记式密码进行了探索。《Free for All》（HarperBusiness）对开放源代码的历史进行了简短介绍。他常常就这些话题提供咨询，并针对如何实现这些思路进行授课。他居住在巴尔蒂默，经常为《The New York Times》、《InfoWorld》和其他主流出版物撰写文章。

Michael Wood具有非常独特的工作经历，他不但活跃在与恶意软件作斗争的前线，同时又在后方的会议室里运筹帷幄。他的洞察力来自超过10年的在线隐私和安全倡导，期间他花费了无数小时作为志愿者帮助技术支持论坛的人们。他是启动Lavasoft（Ad-Aware的创建者）的最初队伍的成员之一，Lavasoft后来成为一个完整的产业。Michael当前是Returnil负责产品管理的副总裁，指导Returnil产品的开发，同时与顾客和安全社区密切接触，提供尖端的PC安全解决方案。

Peiter“Mudge”Zatko也许是著名的黑客之一。他告诉美国参议院，他可以在30分钟之内让Internet瘫痪。他作为权利内容专家在美国参议院政府事务委员会上就政府系统提供证词，并作为权利内容专家在美国众议院就网络犯罪的立法提供证词。他当前是BBN技术公司负责国家情报研究与应用的技术指导。

Mudge在ACM和CORE/CQRE期刊以及USENIX Security期刊发表文章。他在美国国防部的许多部门讲授攻击性网络战争技巧和策略课程，最近成为卡耐基・梅隆大学的客座教授。作为称为“The L0pht”的黑客智囊团的领导人，Mudge是L0phtCrack的发明人。L0phtCrack是一种行业标准的Microsoft密码审核工具。他还是其他几个广受欢迎的软件安全解决方案的发明人。

美国国家安全委员会、总统办公厅认为Mudge是总统服务计划奖学金获得成功的重要贡献者。他还被认为在CIA的关键国家安全任务中作出了贡献。他是USS McCampbell（DDG-85）的荣誉板条拥有者。他的使命始终如一：在宇宙中凿个洞。

Philip Zimmermann是Pretty Good Privacy（PGP）的创建者，PGP是一种电子邮件加密软件包。尽管缺乏经费、没有任何领取报酬的员工、背后也没有提供支持的公司，并且受到政府指控的耻辱，但是PGP还是成为世界上使用最广泛的电子邮件加密软件。

在美国政府于1996年初取消了对他的调查之后，Zimmermann成立了PGP股份公司。这家公司在1997年12月被Network Associates股份公司（NAI）收购。他以高级研究员的身份在这家公司呆了3年。2002年8月，一家新的称为PGP的公司从NAI手中收购了PGP,Zimmermann随之成为这家公司的特殊指导和顾问。

Zimmermann目前就密码学方面为一些公司和行业组织提供咨询，并且是斯坦福大学法学院互联网与社会研究中心的研究员。他是无线USB标准的加密密钥一致性协议的主要设计者。他最近的项目是Zfone，目标是为Net提供安全的电话服务。

Zimmermann由于在密码学中的先驱工作，接受过非常多的技术和人文奖章。2008年，《PC World》提名他为近50年最优秀的50名技术远见者之一。2003年，他被列入Heinz Nixdorf MuseumsForum名人堂。2001年，他被列入CRN行业名人堂。1999年，他从隐私国际（Privacy International）获得了Louis Brandeis奖。1998年，他接受了《Secure Computing Magazine》的终身成就奖。1996年，他因为促进了技术的负责应用而从计算机社会责任专家联盟获得了Norbert Wiener奖。1995年，《新闻周刊》提名Zimmermann为“网络50杰”之一，也就是互联网史上最有影响力的50个人之一。

Zimmermann于1978年在佛罗里达大西洋大学获得计算机学士学位。目前，他是国际密码学研究协会、美国计算机协会和程序设计自由联盟的成员。他担任科学通信与国家安全项目的圆桌协会成员，这是美国全国研究委员会与战略和国际研究中心的一个合作项目。

O'Reilly Media,Inc.介绍

为了满足读者对网络和软件技术知识的迫切需求，世界著名计算机图书出版机构O'Reilly Media,Inc.授权机械工业出版社，翻译出版一批该公司久负盛名的英文经典技术专著。

O'Reilly Media,Inc.是世界上在Unix、X、Internet和其他开放系统图书领域具有领导地位的出版公司，同时也是联机出版的先锋。

从最畅销的The Whole Internet User,s Guide＆Catalog（被纽约公共图书馆评为20世纪最重要的50本书之一）到GNN（最早的Internet门户和商业网站），再到WebSite（第一个桌面PC的Web服务器软件），O'Reilly Media,Inc.一直处于Internet发展的最前沿。

许多书店的反馈表明，O'Reilly Media,Inc.是最稳定的计算机图书出版商——每一本书都一版再版。与大多数计算机图书出版商相比，O'Reilly Media,Inc.具有深厚的计算机专业背景，这使得O'Reilly Media,Inc.形成了一个非常不同于其他出版商的出版方针。O'Reilly Media,Inc.所有的编辑人员以前都是程序员，或者是顶尖级的技术专家。O'Reilly Media,Inc.还有许多固定的作者群体——他们本身是相关领域的技术专家、咨询专家，而现在编写著作，O'Reilly Media,Inc.依靠他们及时地推出图书。因为O'Reilly Media,Inc.紧密地与计算机业界联系着，所以O'Reilly Media,Inc.知道市场上真正需要什么图书。

译者序

我一直对数据挖掘很感兴趣，尤其是通过对海量、抽象甚至枯燥的数据进行挖掘分析后，利用数据可视化工具展现出来的那种绚丽多彩、富含意蕴的数据之美更是令我痴迷、叹为观止。本书涉及领域很广，各领域的精英们向我们娓娓道来相关领域的数据信息系统的架构的设计，包括Yahoo！的云存储架构、Deep Web数据抓取、Facebook的信息平台、自然语言处理、“凤凰号”火星探测器的图像数据处理、探索数据生命的DNA漫谈，甚至是Radiohead视频的制作、旧金山的次贷危机等。

阅读完本书之后，我自己的一个很大的收获是对于自己比较了解的领域，如云存储、Deep Web、NLP等有了进一步的理解和实践指导，而对于那些完全不熟悉的领域，如探索数据生命、火星探测器、制作Radiohead视频等则更是开阔了视野，不但对数据有了新的认识，而且激发了思考问题的一些新的思维方式。

这本书令我很感怀的另一方面是，我发现这些“数据科学家”在兢兢业业构建平台处理数据的过程中，虽然遇到了很多困难和挑战，但是却依然如此坚持、执着地探索数据之美。在翻译本书过程中，这种激情不仅激励着我完成这本书的翻译，同时也激励着我在生活、工作中要有毅力和恒心。而纵观我身边的阿里巴巴云计算的同事们——这些“阿里数据科学家”们，也无一不是那种永远充满着激情致力于我们的“飞天”梦想！

这是我翻译的第一本书，很感激机械工业出版社华章公司编辑陈冀康先生慷慨地引我入门，并且对因为我前段时期项目开发非常紧张而导致翻译进度几乎停滞的宽容和理解表示深深感激。感谢所有其他为本书付出努力的人们。

由于时间和精力有限，本书的疏漏、错误之处在所难免，还望各位读者不吝批评指正。

李妹芳

2010年6月26日

前言

当我们第一次接触为《代码之美》编写“续集”的想法时，这次是关于数据也就是这本书，我们觉得这个想法令人兴奋且很有挑战性。现在收集、可视化和处理数据涉及每个专业领域和日常生活的诸多方面，一个大数据集在范围上将是难以想象的广泛。因此，我们联系了一组相当多样化的群体，这些人的工作让我们钦佩。当他们中的大多数都同意撰稿时，我们感到异常兴奋。

这本书就是我们努力的结果，我们希望它能够展示数据处理工作可以多么的广泛（和美丽）。在本书中，你将了解从和政府协作到和火星登陆器一起工作的各个方面；你将了解如何使用统计程序、制作可视化应用、混合Radiohead视频；你将看到地图、DNA和一些我们真正只能称之为“数据哲学”的内容。

本书的版权收益贡献给知识共享组织(Ceative Commons)和阳光基金会(te Sunlight Foundation)，它们致力于通过解放数据使世界变得更美好。我们希望你将会考虑你和数据亲身“邂逅”的经历如何塑造了世界。

本书的组织方式

本书的章节贯彻一条较为松散的曲线：从数据收集到数据存储、组织、检索、可视化及最后的数据分析。

第1章：在数据中观察生活。作者Nathan Yau着眼于在新兴的个人数据收集领域的两个项目背后的动机和挑战。

第2章：美丽的人们：设计数据收集方法时牢记用户。Jonathan Follett和Matthew Holm讨论了在Web上向人们收集数据时，信任、说服和测试的重要性。

第3章：火星上的嵌入式图像数据处理。J.M.Hughes分析了设计在太空旅行下能够正常工作的数据处理系统所面临的挑战。

第4章：PNUTShell中的云存储设计。Brian F.Cooper、Raghu Ramakrishnan和Utkarsh Srivastava描述了雅虎所设计的软件系统，该系统将其全球分布式数据中心转换为支持现代Web应用的通用存储平台。

第5章：信息平台和数据科学家的兴起。Jeff Hammerbacher以Facebook的数据团队的历史演化作为特例，追溯了信息处理工具以及驱动这些工具的人们的演化。

第6章：照片档案的地理之美。Jason Dykes和Jo Wood吸引人们注意一个志愿者组织收集的彩色可视化空间数据的普及性及其力量。

第7章：数据发现数据。Jeff Jonas和Lisa Sokol阐述了思考数据的新方式，为了完全管理这些数据，很多人需要采用这种方式。

第8章：实时的可移动数据。Jud Valeski深入分析了Web上实时的分布式社会和定位数据当前存在的局限，讨论了解决该问题的一个可能方案。

第9章：探寻Deep Web。Alon Halevy和Jayant Madhavan描述了G公司开发的用于搜索当前“受困”于Web表单之后的数据的工具。

第10章：构建Radiohead的“House of Cards”。Aaron Koblin和Valdean Klump讲述了一个涉及激光、编程和“骑在巴士背上”的惊险故事，故事以一个获奖音乐视频结束。

第11章：都市数据可视化。Michal Migurski详细描述了释放和美化一些我们身边的最重要的数据的过程。

第12章：Sense.us的设计。Jeffrey Heer重塑了作为社会空间的数据可视化，并使用这种新视角来探索历时150年的美国人口普查数据。

第13章：数据所做不到的。Coco Krumme关注于证明人们在很多方面误解和误用数据的实验性工作。

第14章：自然语言语料库数据。Peter Norvig通过从Web上获取的1兆规模的自然语言词汇语料数据，带领读者走进一些令人回味的实践。

第15章：数据中的生命：DNA漫谈。Matt Wood和Ben Blackburne描述了数据之美，即DNA和创造、捕捉和处理数据需要的大量基础设施。

第16章：美化真实世界中的数据。Jean-Claude Bradley、Rajarshi Guha、Andrew Lang、Pierre Lindenbaum、Cameron Neylon、Antony Williams和Egon Willighagen展示了“众包”(cowdsourcing)和高度透明的结合如何提高了药物发现的研究。

第17章：数据浅析：探索形形色色的社会定型。Brendan O'Connor和Lukas Biewald展示了当让人们匿名对其他人的图片进行打分时所表现出来的关联和模式。

第18章：旧金山海湾之殇：次贷危机的影响。Hadley Wickham、Deborah F.Swayne和David Poole通过使用开源软件和公共数据资源，带领读者走进对近年来旧金山海湾地区的住房危机的详尽研究。

第19章：美丽的政治数据。Andrew Gelman、Jonathan P.Kastellec和Yair Ghitza展示了统计和数据可视化工具是如何帮助我们加深对社会进行组织的政治进程的理解。

第20章：连接数据。Toby Segaran探索了对Web上可获取的大量的数据集进行连接的挑战性和可能性。

本书使用的体例

本书遵循以下字体体例：

斜体(Ialic)

表示新的术语、URL、Email地址、文件名和文件扩展名。

等宽字体(Cnstant width)

用于程序清单以及段落中的程序单元如变量或函数名称、数据库、数据类型、环境变量、声明和关键字。

等宽粗体字(Cnstant width bold)

显示命令或者其他由用户输入的文本。

等宽斜体字(Cnstant width italic)

表示必须根据用户提供的值或者由上下文决定的值进行替代的文本。

使用本书的样例代码

本书是为了帮助你完成工作。通常来说，你可以在你的程序和文档中使用本书的代码。除非你使用了本书的大量代码，否则你无需联系我们获取许可。例如，写一个程序用到本书的几段代码不需要获得许可，销售和分发O'Reilly丛书的代码需要获得许可；引用本书的样例代码来解决一个问题不需要获得许可，使用本书的大量代码到你的产品文档中需要获得许可。

我们不要求你（引用本书时）给出出处，但是如果你这么做，我们对此表示感谢。出处通常包含标题、作者、出版社和ISBN。例如：“Beautiful Data,edited by Toby Segaran and Jeff Hammerbacher.Copyright 2009 O'Reilly Media,Inc.，978-0-596-15711-1.”。

如果你觉得你对本书样例代码的使用超出了这里给出的许可范围，请与我们联系：permissions@oreilly.com。

联系方式

如果您对本书有任何意见和问题，请联系出版社：

美国：

O'Reilly Media,Inc.

1005 Gravenstein Highway North

Sebastopol,CA 95472

中国：

北京市西城区西直门南大街2号成铭大厦C座807室（100035）

奥莱利技术咨询（北京）有限公司

O'Reilly的每一本书都有专属网站，你可以在那找到关于本书的相关信息，包括勘误列表、示例代码以及其他的信息。本书的网站地址是：

http://www. oreilly.com/catalog/9780596157111/

对于本书的评论和技术性的问题，请发送电子邮件到：

bookquestions@oreilly. com

关于本书的更多信息、会议、资料中心和网站，请访问以下网站：

http://www. oreilly.com

http://www. oreilly.com.cn

第1章　在数据中观察生活

NathanYau

在不远的过去，Web就是关于共享、广播和分发。但是潮流在变：Web已经走进了个人的世界。每个月，新的应用如雨后春笋般诞生，这些应用可以追踪、监测和分析人们的行为习惯，使他们更好地了解自己和周围的世界。人们可以对饮食习惯、运动、上网时间、性生活、每月的自行车旅行、睡觉、情绪和资产进行在线跟踪。如果你对自己生活中的某一部分感兴趣，很可能存在某种应用可以对它进行跟踪。

当然，个人数据收集不是什么新鲜事。在20世纪30年代，英国的社会研究小组Mass Observation，收集了关于日常生活的各个方面的数据——如胡须、眉毛、驾驶员的叫喊和手势，以及人们在战争纪念碑前的行为，这些都是为了对英国有更好的理解。然而，数据收集方法从1930年开始已经有了提高；现在不再仅仅是一支笔和便条纸，或者是一个手工的计数器。数据可以通过手机和手持电脑自动收集，因此每天都可以持续地上传数据和信息流到服务器、数据库和所谓的数据仓库。

随着数据收集技术的进步，数据流也发展为比Mass Observation[1]参与者报告的计算计数要强大得多的技术。数据可以实时修改，因此，人们期望获取最新的信息。

但是，只是简单地为人们提供数以GB计的数据是不够的。并非所有人都是统计学家或者计算机科学家，而且也不是每个人都愿意对大数据集的数据进行筛选过滤。这是我们在个人数据收集上经常面临的问题。

虽然收集和返回的数据类型可能已经随着时间变化，但是个人的需求是不变的。也就是说，那些收集关于自己和他们周围的数据的个人，他们还是会收集这些数据，以获得对流动的数据的信息有更好的理解。绝大多数时候，我们不是追求数据本身；我们感兴趣的是数据的真正含义。这是个微小的区别，却是非常重要的一点。这个需求要求系统能够处理个人数据流，高效准确地处理这些数据，把这些信息通过易于理解且有用的方式分发给非专业人员。我们想要的远远不只是一个电子表格的数据，我们想要的是隐含在这些数据中的故事。

构建这样的系统要求同时在分析和审美上作出严谨的设计思考。这在我们实现PEIR(Personal Environmental Impact Report，个人环境影响报告）中是非常重要的。PEIR允许人们在微观层面观察自己如何影响环境以及环境如何影响自己；your.flowingdata(YFD)是一个正在开发的项目，允许用户通过Twitter来收集自己的数据，后者是一个微博客服务。

在PEIR项目中，我是其前端开发工程师，主要工作是负责用户界面(U)和数据可视化。对于YFD，我是唯一参与者，因此我的职责可能有些不同，但我还是集中于研究可视化方面。虽然PEIR和YFD在数据类型、收集和处理上区别甚大，但是它们的目标是相似的。PEIR和YFD都是为个人提供信息；但为个人提供信息又都不是它们的最终目标。相反地，它们都是为了激发人们的兴趣，了解即自己每天做出的抉择如何为生活带来重大影响，然后开始探讨个人数据。在对PEIR和YFD有了简单的背景知识后，我将带着这些想法，讨论个人数据收集、存储和分析。然后我将深入探讨PEIR和YFD数据可视化背后的设计过程，通常这些统称为个人数据可视化。最后，我们想向人们显示他们自己的个人数据之美。

个人环境影响报告(PIR)

PEIR是由加州大学洛杉矶分校的嵌入式网络传感中心(Cnter for Embedded Networked Sensing)开发的，或者更准确地说，是由城市传感小组(Uban Sensing group)开发。我们重点利用日常的移动技术（如手机）来收集关于周围和自己的数据，因此人们可以对如何与身边的事物进行交互有更好的理解。例如，DietSense是一个在线服务，它允许人们自我监测饮食选择以及进一步向饮食专家咨询；Family Dynamics帮助家庭和生活教练记录一个家庭每日交互的关键特征，如户外驻扎和家庭聚餐；Walkability帮助居民和行人，提倡通过观察发表他们对于附近的步行适宜性和与公共交通的联系的看法。[2]所有这些项目使得人们可以只通过手机参与到社区活动中。我们利用手机内置的传感器，如摄像头、GPS和加速度计来收集数据，通过这些设备来提供信息。

PEIR采用了类似的原则。例如某个用户下载一个名为Campaignr的很小的软件到他的手机，该软件可以在后台运行。随着该用户每日的活动——比如在田径道慢跑、开车上下班、去杂货店买点东西，手机每2分钟就会上传GPS数据到PEIR的中央服务器，包括纬度、经度、高度、速率和时间。我们利用该数据来预估个人对环境的影响和排放。不需要环境污染传感器；相反地，我们使用在很多手机上已经具备的功能——GPS，然后传递这些包含“环境”信息的数据（如天气）来构建环境模型。最后，我们对环境影响和排放数据进行数据可视化。这个阶段的挑战是向人们传达数据中绝大部分人都不熟悉的含义。每周释放1000千克的碳意味着什么？1000千克是很多还是很少？我们需要谨记我们的用户和目标，因为用户和目标驱动着系统设计，从可视化直到数据收集和存储。

[1]Mass Observation是一个英国社会研究组织。它通过500名未经过训练的志愿者的观察来记录英国人的日常生活。请参考http：//en.wikipedia.org/wiki/Mass_Observation。

[2]CENS城市感知，具体参见http：//urban.cens.ucla.edu/。

your.flowingdata(YFD)

PEIR使用的是运行在后台的定制软件，YFD则要求用户主动通过Twitter输入数据。Twitter是一个微博客服务，它提出了一个简单的问题：您现在在做什么？人们可以张贴，或者更确切地说，可以通过桌面应用、邮件、即时通信告诉别人他们正在做的事；而且最重要的是（就YFD而言）SMS，它意味着人们可以通过手机和别人交流。

YFD利用了Twitter的普遍性，因此人们可以在任何地方通过Twitter发送SMS消息来交流个人数据。用户目前可以跟踪饮食习惯、体重、睡眠、情绪，以及简单地通过发布特定形式的消息来跟踪他们什么时候上洗手间。像PEIR一样，YFD向用户表明的是：这些小事可以对我们的生活方式产生深远的影响。在设计过程中，我们依然考虑用户体验。什么将会激励用户频繁地手工输入数据？我们如何使得数据收集对于用户来说尽可能地简单？一旦数据被记录下来，我们应该如何和用户交流？最后，我将从数据收集开始谈起。

个人数据收集

个人数据收集和科学数据收集在一定程度上有所区别。个人数据收集通常较为不正式，而且不是发生在具备控制条件下的实验室里。人们收集现实世界中的数据，而在现实世界中，存在干扰、网络连接故障或者访问受限的计算机。用户不一定是数据专家，所以如果出了问题（这通常是不可避免的），他们可能不知道如何调整来解决问题。

因此，从用户角度，我们需要使得数据收集变得尽可能简单。它应该是无干扰的、直观的且易于访问的，这样数据收集才更有可能成为日常生活的一部分。

使数据收集成为日常生活

这是我选择Twitter作为YFD数据代理的主要原因，它可以通过电话或者计算机连到数据库。Twitter允许用户通过一些不同的渠道发布Twitter消息(teet)。用户可以通过移动电话发布Twitter消息，由于这一点，用户可以在任何地方通过电话发送SMS消息记录数据，这意味着人们可以随时记录实际情况，而不需要等到他们可以访问计算机时才开始记录。如果需要等待，人们很可能会忘记。因此，易于访问获取是很关键的。

人们可以通过E-mail而不是Twitter来完成一些相似的事情，因为很多手机允许人们给E-mail发送SMS，而这实际上正是YFD的最初实现方式。然而，我们回到数据收集作为日常生活的一个基本部分。成百上千万的人们已经在频繁地使用Twitter，由于这一点，其挑战性已经被部分削弱了。人们确实也在很频繁地使用E-mail，而且比起Twitter，人们也有可能更习惯于使用E-mail。但是这二者在本质上还是很有区别。在Twitter上，人们发布自己正在做的事，每天更新好多次信息。Twitter最初被创建也仅仅是因为这个原因。某个人可能正在吃三明治、出去散心或者在看电影。成千上万的人每天发布这类Twitter消息。而另一方面，E-mail本身就意味着其主要是一些更有价值的信息。绝大多数人不会给朋友发E-mail，告诉朋友他们正在看一个电视节目——尤其不会每天或者每个小时告诉朋友们这些事情。

通过利用Twitter得到的这种发布频率，我们期望它能够用于数据收集。我尝试着使得在YFD上记录数据和在Twitter上的感觉一样。举个例子，如果有个人吃意大利香肠的三明治，他发送了一条消息：“吃了意大利香肠三明治”(ae salami sandwich)。通过这种方式，数据收集就变成了交谈方式。用户不需要学习像SQL一样新的语言。相反地，他们只需要记住关键字及其对应值。在前一个例子中，关键字是“ate”，值是“salami sandwich”。为了跟踪睡眠，用户简单地发送一个关键字：睡觉时是“goodnight”，醒来时是“gmorning”。

在某些方面，用PEIR频繁发布信息的挑战要比在YFD上小一些。因为PEIR是在后台自动收集数据，用户只需要通过点击几下按钮，就可以启动其手机上的软件。开发那种软件本身存在一定的困难，但那就是另一回事了。

异步数据收集

对于PEIR和YFD，我们发现异步数据收集实际上是必要的。当人们遇到一些感兴趣的事情发生后，他们会希望输入和上传数据。在YFD，人们希望能够给他们的Twitter消息增加时间戳，而PEIR用户想要手工上传GPS数据。

正如之前所述，创建YFD的初始想法是人们在遇到一些事情时，会希望输入数据来记录它们。这是使用Twitter的好处和目的。然而，很多人没有通过手机使用Twitter，因此他们需要等到可以用电脑时才登录Twitter。即使是那些确实给Twitter发送SMS消息的用户通常也会忘记记录数据；有些人只是想在每天结束前输入所有的数据。

不用说，YFD现在支持时间戳。数据入口的语义尽可能地贴近交谈仍然很重要。为了实现这个目标，用户可以给他们的Twitter消息添加时间戳，比如“早上6点吃烤鸡和土豆”(ae roast chicken and potatoes at 6：00pm)或者“晚上11点晚安”(godnight at 23：00）。时间戳语义上只是简单地在Twitter消息的最后添加一个“at hh:mm”。我还发现同时支持标准格式和军事格式的时间戳是有用的。最后，当用户输入一个时间戳，YFD将会记录该时间的最近出现频度，因此在先前的“goodnight”例子中，YFD会输入昨天晚上的时间点。

PEIR的初始设计也只是为了“当时”(i the moment)的数据收集。正如前述，Campaignr运行在用户的手机上，周期性把GPS数据（每20分钟上传一次）上传到中心服务器。这对一个每天运行PEIR的用户来说，他本身只需要很少的付出，就可以收集成千上万的数据点。一旦在手机上安装PEIR应用，用户只是简单地按动几下按钮就可以启动应用。然而，几乎从一开始，我们就发现不能依赖100%可达的网络连接，因为几乎总有地区是服务无法覆盖的。最简单虽然也是最幼稚的做法是只有当手机可以上网时才收集和上传数据。这种做法的代价是我们可能会失去大量的数据。相反地，我们通过缓存把数据存储在手机的本地内存，直到手机可以重新连上网络。我们还提供了另一个方案来收集数据，而不需要同步上传这些数据。

这种做法的一个不足之处在于，期望人们在事件发生时收集数据是不合理的。人们会忘记收集数据或者在当时不方便收集数据。在以上任何情况下，提供用户在后期也能够输入数据的功能是很重要的，这一点又反过来影响了数据流的下一步设计。

数据存储

对于YFD和PEIR，很重要的一点是应该记住：一旦数据被收集了，我们应该如何处理这些数据。通常情况下，数据库机制和数据库模式都是在一念之间设计的，研究人员在处理过程中会渐渐后悔，其原因或者是因为他们当时的决策使现在的数据处理变得很困难，或者是因为数据库不具有扩展性。YFD在这方面的选择不是特别困难。我们在其他项目中使用MySQL，而YFD通常只涉及较简单的插入和选择语句，因此很容易搭建系统。此外，数据是人工输入的——而不是像PEIR那样连续上传，因此数据库表的大小在开发早期不是问题。主要的考虑是当增加新的数据字段时，我希望能够扩展数据库模式，因此在创建数据库模式时考虑到这一点。

另一方面，PEIR需要更细心的数据库开发。我们每几分钟就执行成千上万的基于地理的计算，因此我们采用了PostGIS，为PostgreSQL数据库增加地理对象支持。虽然MySQL提供了GIS和空间扩展，我们认为从PEIR的需求考虑，把PostGIS和PostgreSQL结合在一起是更健壮的方案。

以上的描述可能过于简化了我们的数据库设计过程，这里我需要谈一点背景。我们的团队是由10个左右的研究生组成，每个人都有自己的研究方向。正如你所料，每个人也都致力于PEIR的不同模块的工作。这种分工方式极大地影响了我们的工作方式。PEIR的数据最初是非常分散的。我们没有使用统一的数据库模式；在需要多个数据库时，就创建多个数据库，而且并没有遵循任何特定的设计模式。无论谁在PEIR的早中期加入PEIR项目，他都会对去哪里查找数据、查找什么数据以及应该和谁联系感到困惑不已。我这么说是因为我自己正是在这个时候加入PEIR。为了减轻数据分散带来的问题，我们最终冻结了所有的开发工作。一个在PEIR的各个模块都做过开发的同事，很灵活地把每个人的代码和数据库表结合起来。本次代码和模式的凝合（c onsolidation)在用户体验开发之前完成是非常必要的，而且这一点在后期中越来越明显。现在回想起来，在早期对数据存储方面付出更多的努力来进行精心设计是很值得的，而这一点也正是研究生研究的本质。

协调和代码凝合对于YFD不是问题，因为YFD只有我一个开发人员。我可以很容易改变数据库模式、用户接口和数据收集机制。我采用了Django，一个基于Python语言的Web框架，它采用了MVC(Model-View-Control，模型-视图-控制器）模式，支持敏捷高效开发。但是，我确实需要每件事都自己完成。因为我们团队的每个小组成员在统计、计算机科学、工程、GIS和环境科学上的多样性，PEIR可以完成更多的功能——最显著的是在数据处理领域，这在下一节将会介绍。因此，和一个庞大的团队一起开发，必然有其一定的优势和不足。

数据处理

数据处理是个人数据收集系统最重要、最基础的部分，这部分几乎都不为用户所见，而且用户也不感兴趣。用户通常是对数据处理的结果更感兴趣。这就是YFD面临的情况。相反地，PEIR用户可以通过观察他们的数据是如何处理的而从中受益，而且这反过来也影响他们诠释数据表示对环境带来的影响和排放的方式。

PEIR的分析组件包含一系列服务端的处理步骤，这包括GPS数据到对影响和排放的预估。更确切地说，我们可以把PEIR的处理过程分解为四个独立的阶段[1]。

1.跟踪纠正和标注：只要可能，我们通过预估技术，如利用道路网进行地图匹配和构建宗地数据(prcel data)来对易于引起错误、抽样定位的地理位置踪迹进行纠正和标注。因为这些纠正和标注是估计值，它们确实包含一定的不确定性。

2.活动和地理位置分类：通过使用Web服务，纠正和标注的数据会自动分类为移动的或者静止的，从而可以为某人在某天的模型输出提供一级完善。其数据也基于停滞时间，划分为不同的旅途行程。

3.上下文预估：把正确和分类的地理位置数据作为基于Web的信息资源的输入，这些信息资源包括天气、路面状况和驾驶员行为的汇总。

4.排放和影响计算：最后，把细粒度、分类和衍生的数据作为地理空间数据集合和微观环境模型的输入，这些输入反过来又可以用于提供个人的个性化估计值。

虽然PEIR的重点还是研究这四个步骤的处理过程的结果，但是我们最终发现用户想要对如何估计影响和排放这些方面有更多的了解。因此，我们提供每块数据处理过程的细节，比如在高速公路上花费的时间比、用户旅游地方周边的天气情况。我们还包含了对每个提供的尺度的详细解释。在这种情况下，预估过程的透明性允许用户了解他们的行为对环境是如何产生影响和排放的，而不仅仅是了解他们对周边地区造成的污染有多么大或多么小。当然，也存在如信息过载的情况，因此我们对于应该展示多少信息量很慎重。这些方面将在下一节做更多的探讨。

[1]PEIR,http：//peir.cens.ucla.edu

数据可视化

一旦数据被收集、上传和处理，用户需要能够访问、评估和浏览他们的数据。YFD和PEIR的主要设计目标是使得个人数据对于非专业人员也可以理解。数据必须通过可关联的方式来展示；它需要具有人性化。通常情况下，我们会过于陷入统计图形和图表之中。这些图形和图表非常有用，但是同时我们也希望用户能够参与进来，这样他们可以因为感兴趣而留下来，继续收集数据，不断地回到站点来衡量他们所跟踪的任何事情的进展。用户必须理解的一点是：数据是关于他们自己的信息，而且反映的是他们在日常生活中做出的选择。

我想把数据可视化视为一个故事。故事的主角是用户。我们可以采取两种方式来讲述这个故事。一个关于图形和图表的故事可能读起来很像一本教科书，但是，一个包含场景、关系、交互、模式和解释的故事读起来像是一部小说。这并不是说其中一个比另一个更好。有很多有趣的教科书，同时也有很多（如果不是更多）庸俗的小说。当我们对个人数据作可视化分析时，想要的是介于教科书和小说之间的“故事”。我们想要展示事实，但我们也想提供场景，比如什么人、什么时间、什么地点以及结果数字的原因是什么。我们追求的是情感。数据通常是枯燥无味的，但只有当我们以枯燥的方式来展现时它们才会如此。

PEIR

对于PEIR，我们面临的挑战是如何展示科学数据——“碳”影响、高密度的颗粒物排放以及对敏感地区（如医院和学校）造成的影响。影响和排放并没有成为人们每天谈资的一部分。绝大多数人并不知道一天释放1000千克的碳到底是很多还是很少。排放一小时高密度的颗粒物是否正常？这些问题都成为PEIR可视化设计的考虑因素。但是，必须记住一点，虽然结果数据不是可以马上被理解，但是它们都是通过地理位置数据衍生出来，而地理位置数据本身是非常直观的。可能很少有如一个人在物理空间的地理位置这样易于被立即理解的数据。因此，我们使用地图作为数据可视化的锚点，从地图开始着手。

映射基于地理位置的数据

基于地理位置的数据驱动着PEIR系统，因此交互式的地图是用户界面的核心。我们最初使用GMap API，但很快就由于灵活性原因采取了另一个方案——采用Modest Maps取而代之。Modest Maps是基于“贴图”(tlebased)的地图的展现和交互库，它以Flash方式展现，通过ActionScript 3.0实现。Modest Maps提供了一组核心特性，如放大缩小，而且设计师和开发人员可以很容易地定制展现方式。Modest Maps的实现方式可以容易地转换地图“贴图”，这些地图“贴图”可以是微软的，定制的或者以上全部包含。我们可以自由地调整颜色、布局和全局样式，这些带来了良好的设计实践和有用的可视化展现。而且由于其灵活性，我们可以在地图上结合或者补充自己的可视化展现方式。毕竟，我们不想把自己仅仅受限于地图，Modest Maps为我们提供了这种灵活性。

通过视觉提示(vsual cue)进行实验

在决定最终的映射机制之前，我们实验了多种不同的方式来呈现PEIR数据。在设计过程中，我们考虑了几种参数：

·如何使用户能够马上和各种旅行轨迹进行交互而不会把地图弄得很混乱？

·如何能够同时呈现静态（用户处于悠闲状态）和运动（用户在运动）的数据块？

·如何呈现从所有四个微观环境模型中获取的数据值？

·应该采用什么颜色来表示GPS轨迹、影响和排放？

·如何把重点转移到实际的数据上，而不是底层的地图“贴图”？

映射多元地理位置轨迹

在设计过程的早期阶段，我们把GPS轨迹以用户通常看见的地理位置轨迹的方式进行映射：只是简单地一条线从一个点到达另一个点。在考虑微观环境模型的值之前采用这种方式，因此地图是通过使用Modest Maps和OpenStreetMap的“贴图”数据的方式做的简单实现。其GPS轨迹是单一色的，只唯一表示地理位置信息；在轨迹的最后有个圆圈(crcle)，因此用户可以知道旅程是从哪里开始，然后在哪里结束。

这在某个范围内是可行的，但是很快我们就需要对更多的数据进行可视化，因此我们改变了呈现方式——基于影响和排放值来对轨迹进行着色。其着色方案采用了五种不同深度的红色。碳影响的级别越高，其红色就越深。同样地，碳影响级别越低的旅程，其红色就越浅。

这种呈现方式的隐含意思是用户对环境的影响越大，该旅程在地图上的显示就越突出。这种实现方式的问题是在地图的轨迹并不明显（见图1-1）。我们尝试过使用更鲜亮的颜色，但是鲜亮的颜色和地图上现存的颜色交杂会显得比较混乱。虽然我们希望轨迹显示上能够很明显，但是我们不希望使用户产生视觉疲劳。为了解决这个问题，我们尝试了一种不同的映射机制，该机制采用的方式是在地图上的所有旅程都用单色显示，但是使用了一些圆圈对影响和排放进行加密。所有的轨迹都是白色的，模型值通过圆圈实现可视化表示，这些圆圈在旅程的终点展现，其大小不一。模型值越大，圆圈的面积就越大；反之，模型值越小，圆圈的面积就越小。但是这种设计策略很快就被淘汰了。

图　1-1：我们在地图上对不同的视觉提示进行实验，通过影响和排放值来最佳展示地理数据。以上展示了我们初始设计的三次迭代。最左边的地图显示根据碳影响所呈现的彩色GPS轨迹；对于中间的地图，我们通过单色的圆圈面(aea circle)来表示影响；对于右边的地图，我们结合了GPS数据来呈现用户什么时候有空，然后重新采用单色的颜色编码方案（见彩图1）

只在轨迹的终点（通过圆圈）来表示值，这种显示方式的问题之一是用户会误认为圆圈意味着每次旅程的终点发生了一些事情。但是，实际上并不是这样。地图本应该展示的是在旅程的全部过程中发生了一些事情。

在你旅程的任何地方都释放碳，而不是收集起这些碳然后再在终点被释放。我们切换回原来的对旅程进行颜色编码的方式，删除表示模型值的大小不一的圆圈面。在设计过程的这一方面，我们现在有两种类型的GPS数据：动态的和静止的。动态的GPS数据意味着用户在运动，不论是徒步行走还是驾车行驶；静态的数据表示用户没有在运动。她可能坐在桌子上或者被交通拥堵困住。为了展示静态的数据块，我们并没有完全抛弃在地图上使用圆圈面的方式。圆圈越大，意味着历时周期越长；相反地，圆圈越小，意味着历时周期越短。和旅程相似，也是通过线条来表示的，圆面相应地进行着色。例如，如果用户选择通过颜色来标示颗粒物排放值，在高速公路上静止的数据块会显示为颜色鲜艳的圆圈。

然而，我们还是面临着与之前相同的问题：试着使轨迹能够在地图上突出显示而且不会和地图已有的颜色相冲突。我们已经试着为这些轨迹采用不同的颜色机制，但是尚未尝试改变实际地图的色调。Trulia Snapshot是为房地产做地图的公司，受到它的启发，我们用灰色来表示地图“贴图”，对换颜色过滤器，这样把原来颜色很浅的地图元素的颜色变深，反之亦然。更具体地说，地形原本是浅色的，那么现在它应该是深灰色的；而马路原本是深色的，现在改为浅灰色。这种深色调的地图能够突出显示浅色的轨迹，而且由于地图是灰度模式图，颜色冲突变少了（见图1-2）。用户不需要费很大的劲儿就能把他们的数据从道路和地形地貌中区分出来。Modest Maps提供了这种灵活性。

图　1-2：在当前的映射机制中，我们采用颜色过滤器来高亮显示数据。地图仅仅是提供上下文信息。链接的直方图显示映射后的数据的影响和排放分布。当用户通过滚动条看直方图的某一栏，相应的GPS数据会在地图上高亮显示（见彩图2）

选择一种颜色机制

一旦我们把地图“贴图”通过深色背景显示，而把旅程通过浅色前景显示，我们确定了需要使用什么颜色。这一点很重要，因为用户会识别一些颜色表示某些特定事件。例如，红色通常表示停止或者前方有危险，而绿色则意味着进展或者增长，尤其是站在环境的立场上看。

另外很重要的一点是不要使用太多颜色对比非常分明的色彩。采用颜色很不相似的不连续的数据则意味着是分类数据。但是，模型值有连续的范围。因此，我们采用有稍微渐变的颜色。在早期的设计版本中，我们尝试使用不同深度的绿色色阶来表示。用户评论说因为绿色通常意味着好的方面或者是对环境友好，通过绿色来表示影响和排放程度很高会显得很怪异。于是，我们采用了不同色阶的绿色和黄色组合来取代。从低值到高值，我们增量式地把颜色分别从绿色切换到黄色。把影响或者排放值为零的旅程显示为白色。

使旅程有交互性

用户一次可以潜在地映射成百上千的旅程，提供关于旅游习惯、影响和排放的一个总体概要，但是用户也需要查看每个旅程的细节。仅仅把一个旅程映射到地图上是不够的。用户需要能够和各次旅程进行交互，从而能够清楚自己相关旅程的环境信息。

当用户在PEIR地图上通过滚动条来查看旅程，该旅程会高亮显示，而所有其他的旅程都会变得暗淡，和背景融合起来但又不是完全消失。更确切地说，对感兴趣的旅程的透明度减少了，而其他旅程的模糊度变成原来的5倍。Cabspotting是对旧金山的出租车活动做的可视化地图，是它启发了我们实现该效果。当用户在地图上点击一个旅程，该旅程的日志会自动滚动到用户感兴趣的旅程上。再一次强调，该设计的目标是为了给用户提供尽可能多的环境信息，而且不会显得整个屏幕很杂乱。

当然，这些特征只在某个范围内处理多个旅程。例如，如果在一个密度高的地区有成百上千的长途旅行，这些旅程会由于杂乱而变得难以分析导航。因而我们结合用户提供的元数据如标签和分类，期望提高这一领域。

呈现分布

PEIR在地图右侧提供直方图来显示对选择的路程的影响和排放分布。有四个直方图，每个直方图都表示一个微观环境模型。每当用户从旅程日志中选择旅程时，直方图就会自动更新。如果在绝大多数情况下，旅程在影响和排放上的值很高，直方图就会向右倾斜；类似地，如果在绝大多数情况下，旅程在影响和排放上的值很低，那么直方图就会向左倾斜。

我们最初认为直方图会是有用的，因为它们在统计上应用得如此广泛，但实际情况并非如此。直方图给人带来的更多是困惑而不是对事物的洞悉力。虽然只有很小的测试组认为它们是有用的，绝大多数人会认为横轴是时间，而纵轴是影响或排放值。人们似乎对随着时间变化的模式更感兴趣，而不是对全局分布。因此，我们转为采用基于时间的条形图（见图1-3）。用户能够看到它们随着时间的推移造成的影响和排放的连续值序列，而且可以按周浏览。

图　1-3：实践证明：时间序列条形图比基于值的直方图更有效

分享个人数据

PEIR允许用户和他们的Facebook朋友互相分享自己的影响和排放值，这种方式可以作为另一种比较有价值的方式。正是通过分享，我们可以绕过轴线的绝对范围解释，而是把重点放在相对值上，这样可以进一步帮助我们进行推导。虽然1000千克的碳看起来可能很多，但是和其他用户进行比较后，可以改变这种误解。和其他的Facebook朋友相比，我们的PEIR Facebook应用显示了用户在Facebook的个人信息的汇总值（见图1-4）。

图　1-4：PEIR的Facebook应用允许用户分享他们对影响和排放值的发现，以及与朋友的进行比较（见彩图3）

PEIR Facebook应用显示了用户的影响和排放值的条状图，以及他或她朋友的影响和排放值的平均值。该应用也显示了这些值的全局排序。影响或者排放值越低的用户，排名越高。图标也提供了更多的环境信息。如果影响很高，则会显示一个带有散发着很多浓烟的烟囱图标；如果影响值低，则会显示一条溪流，溪流里倒映着蔚蓝的天空。

把注意力重新转回PEIR的用户界面，除了个人信息以外，用户也有一个网页。该网页显示上周用户影响和排放值的排序，也显示了用户好友的排序。其目的是为了鼓励用户减少影响和排放，提高排名，同时也鼓励他们的朋友这么做。虽然通过千克或小时为单位表示的影响和排放的实际值可能最初是不清晰的，但排序的用处可以立竿见影。当用户追求更高的排序，从PEIR微观环境模型值来说也意味着更多，当打视频游戏也采用相同的方式时，其分值也开始有了意义。

读者应该注意到其中没有分享任何GPS数据。我们对于隐私数据非常严谨，采取了很多措施使得某些数据是私有的，这就是只有影响和排放的汇总值在网页中显示的原因。

YFD

PEIR处理的是一些不是立即相关的数据，而YFD则刚好相反。YFD帮助用户跟踪每天日常对话的数据。和PEIR一样，YFD的目标是使得一些生活中的小事更加明显。正是小的选择反而产生了巨大的影响，可视化可以证明这一点。

我们回到之前提及的一个挑战。我们希望用户能够频繁地用Twitter对话，把个人的数据收集转为他们每天的Twitter日常习惯。数据收集背后的动机是什么？为什么用户要跟踪他们自己的饮食或者睡眠习惯？可能有些人想要减肥是为了在异性面前可以更自信，或者是想获得更多的睡眠是为了不在办公桌上睡着。但是，可能有另一个用户想增加体重，因为她生病时体重减轻了，或者是她睡得太多，每天起床经常感觉有气无力。其他用户可能仅仅是好奇。很明显的一点是，无论其动机是什么，每个人对于个人数据收集都有他自己的理由。作为对用户的提醒，YFD重点突出了该动机，因为不论一个人正在尝试的饮食和睡眠系统如何，除非他们自身真正希望改变，否则人们是不会有变化的。注意图1-5的屏幕中间通过大号字显示的个人动机。

需要注意的另外一点是：每个跟踪页面的最上方显示了最近最经常发生的事情。这么做有以下目的：首先，每当用户发送一个Twitter消息数据，就会被更新，因此每当用户登录到YFD时，就可以看见自己的状态。其次，我们不希望用户使用起来，体验和Twitter本身差别太大，还是进一步使用户把YFD的Twi tter消息作为Twitter的日常习惯。最后，YFD的设计方案绝大部分是源于PEIR的经验。用户似乎期望的是基于时间的可视化，因此绝大多数的YFD可视化就是基于时间来呈现。

有一个特例是对于感觉和情绪(felings and emotions)的跟踪（见图1-6）。正如每个人都会告诉你，情绪是无比复杂的。如何量化幸福、伤心或者焦虑？把情绪分解为图形和数字看起来是不对的，因此，我们采用有序的标签云图(srted tag cloud)取而代之。后者在某种程度上，让人感觉更有生命。出现频度更高的情绪比频度低的在呈现上字体格式更大。YFD跟踪器在开发的早期都是模块化的，但是我确实计划最终结合所有的跟踪器，把YFD当作用户生活的仪表盘。感觉跟踪器会在所有跟踪器的中间。最后，我们做的每件事都是由我们的感觉或者我们想要什么样的感觉来驱动的。

图　1-5：人们为不同原因跟踪他们的体重和饮食。YFD把这些动机作为用户界面的焦点（见彩图4）

图　1-6：用户还可以跟踪他们的感觉。不像其他的YFD跟踪器，情绪页面不包含任何的图形图表。选择单词“云”(coud)是为了提供更多的有生命的情感可视化

要点

数据可视化通常全部都是关于数据分析和技术结果，但实际上不需要如此，尤其对于个人数据收集。人们收集自己的数据并不一定是为了追求实际数据。他们主要是对结果蕴涵的信息以及如何使用自己的数据来提高自己感兴趣。为了实现这一点，人们在可视化过程中需要观察的不仅仅是数据，他们更需要观察自己。生活很复杂，数据表示生活，用户希望能够通过数据在某种程度上能够了解这种复杂性。但这并不意味着我们需要简化数据或者信息。相反地，我们使用数据可视化来教授知识，吸引用户的兴趣。一旦用户产生了兴趣，我们可以为用户提供某种方式，从而可以更深入地钻研、探索他们的数据；或者更确切地说，探索和理解其在数据中的生活。这依赖于统计学家、计算机科学家和设计师来合理地讲述其中的故事。

如何参与

PEIR和YFD目前只能通过邀请的方式加入但是如果你希望参与，请访问我们的网站，分别是http：//peir.cens.ucla.edu和http：//your.flowingdata.com。此外，如果你想和PEIR研究组一起协作结合新的模型、策略或数据可视化，或者如果你对于如何提高YFD有自己的见解，我们都非常期望听到你的意见。

第2章　美丽的人们：设计数据收集方法时牢记用户

Jonathan Follett和Matthew Holm

简介：用户共鸣正当其时

始终牢记受众的期望和需求。这个指导着用户体验(uer experience,UX)设计领域的原则，有目共睹——足够引起任何创造新的、创新性的数字技术或者正在改进现有系统的任何专业人员的注意。“当然有人在用该产品！”虽然以用户为中心的设计过程存在很大益处，比如增强了产品可用性和客户满意度，减少了800服务呼叫，但是这个看似简单的建议人们并非总是遵守，尤其是当涉及数据收集方面时。

什么是用户体验

用户体验是一个新兴的、跨学科的领域，主要集中在设计易于理解和使用的产品和服务上。它主要关注于使得系统能够适应并服务于用户，而不是使用户适应并服务于系统（见图2-1）。用户体验专业人员可以包含以下方面的从业人员和研究人员：可视化设计、交互设计、信息架构、用户界面设计和可用性。用户体验领域与人为因素以及人机交互密切相关，它和人种学以及心理学也有一定联系：用户体验专家是用户的倡导者。通常，用户体验设计技术应用于计算机桌面软件和分布式Web软件。不过，支持者可能会更广泛地使用这一术语，用其描述任何复杂的设计经验，比如博物馆展览或者零售店的用户访问设计。

把用户体验的最佳实践应用于数据收集的好处

当涉及数据收集时，用户体验设计尤为重要。数据（最宝贵的数字资源）来自于人们及其行动。因此，设计人员和开发人员应该一直考虑这些人，不仅仅是考虑他们想要收集的数据。收集在线人们的数据的关键方法当然是通过令人厌恶的表单。对于业务来说，没有比表单更有价值的方式；而对于参与者而言，也没有比表单更令人厌烦和无聊的方式。

作为用户体验的从业者，我们经常处理借助Web表单从大量的受众那里收集来的数据。正如我们所见，优雅的视觉设计的表单一次次极大地帮助了数据收集。对于任何表单设计项目，其所提出的挑战是：虽然数据收集比较简单，但要收集到有用的数据却可能比登天还难。表单设计很重要（见图2-1），它会直接影响到你获取数据的质量：设计良好的表单可以收集到更准确和更相关的数据。

图　2-1：用户体验设计过程不是把受众的需求作为事后反思，而是在设计阶段解决处理受众需求、业务需求和技术可行性

那么，到底是什么驱动人们去填写表单、创造我们需要的数据？作为设计开发人员，我们如何才能促使他们更切实、有效、准确地完成这件事？

我们将分析一个案例研究，用来说明一个通过用户体验的最佳实践和原则来提高自主自愿的问卷回答完成率的简单的表单设计例子。

项目：关于一个新奢侈品的用户调查

该项目是为一个营销咨询公司Urban Wallace Associates做网上调查，其目的是探测消费者对一种新的奢侈品的兴趣。（为了保密，在本章中，我们不得不改变调查问题内容的一些细节。）被调查的群体与该产品的最终零售受众群体是同一群体：年龄在55～75岁的有钱人。

电子邮件调查不是我们的客户的第一选择。Urban Wallace Associates已经尝试过对目标群体进行电话调查。“通常情况下，我们打电话时，大约35%是录音电话，”UWA主席Roger Urban说，“但是在有钱人群体中，有超过80%是录音电话。即使有人接电话，接电话的人也通常是管家！”

无法通过电话获得令人满意的目标受众的回答，我们的客户转而使用电子邮件。他们选择这种通信方式的原因是，对于富裕的群体，电子邮件是普遍使用的工具。虽然电子邮件面临其自身的“守门员”(gtekeeper)（即自动垃圾邮件过滤）但很少有人雇佣别人为自己读取邮件。即便是有钱人，他们也仍然是亲自打开自己的电子邮件。

Urban Wallace Associates争取到一个电子邮件营销公司为他们生成和预审收件人列表，并提供投放和跟踪外放信息。我们公司的任务是设计和构建该调查需要的“着陆页”(lnding page，即访问的起始页面），当收件人点击邮件正文的一个链接，该页面就会在他的浏览器打开，然后收集信息到数据库。我们在这项调查中的首要任务是维护并保持问卷调查的Web页面的令人愉悦的氛围，因而受访者能够更愿意填写表单。我们的另外一个任务是为客户创建一个简单的页面，这样当有数据进来时，他可以审查实时的报告结果。

数据收集面临的特殊挑战

数据收集提出了一些特殊的挑战，包括可访问性(acessibility)、信任和用户动机。以下各节将讨论这些问题如何影响了我们的设计。

可访问性挑战

倡导Web的可访问性（设计使得网页和网站对于有特殊需要的人和残疾人仍然是有用的）通常情况下，设计一个具备可访问性的站点同时也应是创建一个对于每个人都更易用的网站。这并不只是该案例下的理论想法，因为目标受众是接近退休或者已经退休的成员，与年龄有关的视力障碍将成为真正需要关注的问题。约72%的美国人在45岁左右时报告存在视力障碍。

关于年龄问题的另一方面（由于担心表现出歧视性，很少被谈及）是老年人使用计算机和互联网的人数较少，而且不像那些和计算机一起成长的年轻人那样能够对它们驾轻就熟。（收入越高的个人，通常使用计算机和网络越多，因而，这种和年龄相关的影响在我们的受众群组中被削弱了。）被一个设计不清晰的调查所僵住的受访者不太可能会提供准确的信息——或者更确切地说，根本无法完成这项调查。就我们而言，对于所有的这类项目，回顾那最基本格言是值得的：了解你的受众。

直觉的挑战

可访问性是个功能上的问题（受访者如果不能读懂一个调查，就无法完成该调查问卷），我们的项目面临着在本质上更情感化的其他挑战，而且依赖于受访者如何理解提问者及其问题。

建立信任

Internet用户很清楚在线透露信息可能会有严重的后果，包含增长的垃圾信息、电话诱惑和垃圾邮件，以及欺骗和身份盗窃。因此，对这些想做网络市场调研者来说，建立信任是很重要的因素。即使对产品和调查的答复整体非常肯定（我们将在后面详细介绍），如果问一些参与者为什么对产品不感兴趣，有如下的回答：

“不信任你们公司”

“从未听说过该产品的提供者”

“不相信该产品声称的任何方面”

“买不起……不信任……好得令人难以置信，因此很可能不是真的。关于该产品，请不要再联系我！”

这些受访者的答复，说明了为建立网络信任，我们还必须走很长的路。更重要的是，对于这个项目，明确要求我们不能销售任何东西——我们是在做调研。我们的客户Roger Urban一开始就告诉我们：“我不想听任何像是搞销售的话。”明确地提供关于Urban Wallace Associates的信息是很必要的，这样人们就知道是什么样的公司在对他们做问卷调查；而且明确地指出我们不会收集他们的个人资料，而且也不会再骚扰他们。其中唯一的障碍是，我们的客户调研需要知道每个答复者在美国哪个州居住。因此我们需要探索出一种合适的方式，在没有违背我们正在尝试建立的信任的宗旨的情况下捕获该信息。

调查的“度”

避免受访者不积极参与是我们所面临的最大困扰之一。客户和我们需要在早期达成一致，保持调查页面只通过一个屏幕就能够完全显示。如果需要多个屏幕显示，不但需要受访者有更多的耐心，而且可能需要他们执行一些额外操作（比如点击“下一个问题”按钮）。一个调查在任何时候需要受访者多执行一步操作，就相当于“邀请”他决定这额外的付出是不值得的，然后选择放弃。此外，我们希望在任何情况下，调查从始至终都应该避免恐吓受访者。多个屏幕或者在一个屏幕上看起来有太多问题，这些都增加了受访者中途退出调查的可能性。

准确的数据收集

在本次调查的设计阶段，我们考虑的一个特别重要的问题是收集的数据必须尽可能准确——可能该数据只是一个显而易见的表述，但要保证准确性，还是很困难。我们的表单设计需要参与者诚实的回答，而且其回答不受其他因素影响，比如说，潜意识地希望能够让提问者高兴（这是这类调研的“通病”）。收集人们的意见这方面的数据和收集信息之间的区别是，后者可能本质上更偏重于管理实施，比如配送地址；配送地址可以很容易验证，但观点意见这类数据，本身就是主观的，通常不是那么可靠。虽然在本章中不会深入涉及设计民意调查和评测结果数据这门科学，但是我们将会讨论为鼓励受访者填写准确的答案，我们团队所采用的一些语言以及做出的其他选择。

动机

最后，虽然我们已经谈到了如何让受访者有可能使用表单，以及关于以下方面的诸多问题：让受访者信任我们、参与调查；避免问题太多“吓跑”了受访者；确定我们没有潜意识地影响他们的回答；以及我们之前尚未提及的一点，而且可能是任何调查最重要的部分：受访者为什么要参与？对于这类调研，不存在任何参与的受益动机，不像Amazon的Mechanical Turk一样的在线论坛，对于后者，用户可以在空闲时间完成任务，而且每项任务可以获取几美元或者几美分。然而，当对方没有明显的利益时，如何说服一个人花时间来回答你的问题？

设计解决方案

我们已经谈到了对于数据收集项目的一些内在的陷阱，在后面几章将分析我们设计的具体细节，包括排版、Web浏览器的兼容性和动态表单元素。

设计哲学

当我们是为了获取受访者的答复而进行设计时，从用户角度来制定问题是很重要的。项目设计时很容易因为技术限制而陷入其中，从而是为计算机设计而不是为用户。但是表单数据是由用户主动生成的（而不是通过传感器或其他输入被动生成），而且要求参与者决定如何以及是否回答你的问题。因此，以何种方式来收集参与者的数据至关重要。我们为该项目设计Web表单时，重点在于对调研参与者和客户商业目标的动机进行权衡。客户的主要商业目标是：收集数据，决定目标受众是否对购买一款新的奢侈品感兴趣——与以用户为中心的设计理念一致。通过把个人同时置于顾问和潜在的未来客户这两种核心角色中，业务目标为以用户为中心的设计决策提供了强有力的理由。

以下是我们在制定设计决策时遵循的一些指南：

尊重用户

为了使设计在整个过程中保持以人为本，需要了解考虑用户的情绪反应。为了说服他们参加，我们首先必须对他们表示尊重。他们不是傻瓜，而是我们的潜在客户。我们本能上都清楚这些，但令人惊讶的是，我们会多么容易忘记该原则。如果我们对用户表示尊重，自然地就会希望构建的数字产品对他们来说是可访问的、可用的且易于理解。这种观点影响了我们做出的每个选择，包括从语言、布局直至技术。

展现真实的人们

对于时间节奏要求很快或者预算受限的项目，我们通常没有资源基于目标市场研究来塑造一个完整的用户个人信息或角色，或者在用户的工作环境中观察他们。在这些情况下，采取简单的“游击队”式的用户体验技术使用户产生共鸣，其指导性的设计决策是想想一个我们认识的真实的人，我们真正地想要帮助他。我们已经有一些这样的角色替身来指导我们思考，包括年迈的父母、一些很了解其先前情况的商业伙伴。当然，通过数字产品去想象这些人只是设计的第一步。既然对他们很了解，我们也可以在设计的初期测试时取得他们当中一些人的帮助。

最终，人们会调整自己的行为，如果有必要，那么他们的行为几乎可以和任何设计保持一致。用户体验的目的是优化这些设计，使得人们会期望使用某种产品或服务，而且可以不需要调整他们的行为，就能够更方便、快捷地使用这些产品和服务。

表单布局设计

通常来说，不论表单做得多么漂亮，它都不太可能会达到取悦用户的水准。设计师没有“圣杯”使人们对填写表单充满激情。但是表单的审美艺术还是很重要：清晰的信息、可视化的设计可以明晰地引导用户看哪些方面，鼓励他们对表单填写坚持到最后而不是中途放弃任务，从而减轻用户的无聊厌倦情绪。好的表单设计不会把用户注意力吸引到自己本身，表单应该几乎是不可见的，永远信守其主要目的——从人们那里收集准确的信息。虽然表单设计需要给人感觉既舒服又专业，但在绝大多数情况下，和其他形式的Web页面相比，经过合理的视觉处理的表单会给人感觉既含蓄又实用。表单视觉设计只能通过它如何有效地促使用户完成任务这个指标来判断。对于这个项目，我们在设计上的努力主要集中在排版、页面布局和交互设计这三个领域。

Web表单排版和可访问性

通常来说，年纪较大的读者看较小的东西很吃力，而且调查参与者也不会“慷慨”到为了阅读一个表单，甘愿让自己的眼睛受累。由于调查项目的目标受众是年龄较大（55～75岁）的群体，我们清楚整体的易读性将是个问题。

我们采用Sans Serif字体类型Arial（类似于微软推出的骨干字体Helvetica)，它对几乎100%的Web浏览器都是标准的，我们对标题和正文的字体大小分别设置为20像素(pxel)和14像素。虽然字体大会造成整个页面稍长，但由于其带来的易读性方面的改善，该代价是值得的。

行间距不是很紧，且采用左对齐的方式。每行长度大约是85个字符。对于大多数文本，我们设置了高对比度——白底黑字的显示方式，这种设计也是出于易读性的考虑。虽然我们确实可以在策略上使用鲜艳的颜色对页面进行高亮显示，而且突出显示主要标题，但是我们并没有依赖这种方式来为用户提供额外信息。我们这样做的原因是，大约有7%～8%的男性受众是某种类型的色盲。

给人们一些空间

没有“呼吸”空间、设计过于紧凑的表单必定会吓坏用户。因此，在布局时留一些开放空间很重要。

在我们的调查中，表单的第一部分包括对该奢侈产品的文本描述，在该文本描述中，我们请求参与者对它进行阅读和评估。由于阅读Web页面的读者的注意力能够集中的时间很短，而且他们倾向于略过一部分文本而不是通读。因此，遵从Web写作的最佳实践，我们把250个单词的产品描述切分为包含多个标题的子部分，即抽取出关键点，并把这些点划分成几个易于理解的不同分块（见图2-2）。

图　2-2：为易读性而设计（见彩图5）

适应于不同的浏览器，并测试兼容性

为了确保表单对于受众是可用的，我们设计了表单页面，这样对于从800个像素到更多像素的不同大小的屏幕，用户都可以很容易地查看该表单。为了实现这一点，我们把表单放在浏览器中间，使用中性的灰色背景来填充由于宽频显示器引起的左右空白剩余空间，而且保证表单看起来不会显得空洞和“飘忽不定”。为了确保主流表单看起来不错且能够正常工作，我们还在所有的主流Web浏览器上进行测试，包括传统的IE6浏览器。

交互设计考虑：动态表单长度

动态表单可以“舒缓”需要回答很多问题带来的“重击”。使用JavaScript或者其他方法允许表单基于用户输入，做一些微妙的修改或加长，从而营造一种柔和的展现方式（见图2-3和图2-4）。这些技术允许我们对以下两方面做出权衡，一方面不会由于表单太长而吓坏了用户，另一方面不会使用户因为在表单长度方面感到被“欺骗”而动怒。

对于我们的项目，实际上是根据用户回答的每个问题来构建了整个调查。我们使用了一段简单的JavaScript脚本，确保每个新问题都是基于前一个问题的回答。这种解决方案的思想来自于我们当时为之工作的另一个Web站点。那个项目是为设计者创建组合站点，我们使用JavaScript来隐藏和显示不同项目的细节，使得用户一眼就可以理解设计者的所有作品，然后对感兴趣的领域进行深入研究，而且都不需要离开该站点的首页。这种思想不但没有把用户淹没在过多的信息中，同时还使得这些信息能够很快地被用户所访问。我们在设计该调研时，一直牢记着该想法。以下是我们使用的代码：

＜script language="JavaScript"＞

//This finds the word"Yes"in an input value and displays the designated

hiddenElement

(o hides it if"Yes"is not found)

function switchem(switchElement,hiddenElement){

if(switchElement.value.search（"Yes"）＞-1）

document.getElementById(hiddenElement).style.display=''；

else

document.getElementById(hiddenElement).style.display='none'；

}

＜/script＞

＜script language="JavaScript"＞

//This finds the word"No"in an input value and displays the designated

hiddenElement

(o hides it if"No"is not found)

function switchem2(sitchElement,hiddenElement){

if(switchElement.value.search（"No"）＞-1）

document.getElementById(hiddenElement).style.display=''；

else

document.getElementById(hiddenElement).style.display='none'；

}

＜/script＞

图　2-3：调查开始只有3个问题（见彩图6）

……

＜li id="survey1"class="surveynum"＞How interested would you be to purchase this

kind of product?

＜p＞＜b＞Would you say that you：＜/b＞＜/p＞

＜ul class="nobullet"＞

＜li＞＜input

onclick="switchem(this，'survey2'）；switchem2(tis，'survey3'）；document.

get Element ById（'surveytextarea'）.value=''"type="radio"name="q1"value="Yes，

Definitely would purchase"＞Definitely would purchase＜/li＞

＜li＞＜input

onclick="switchem(this，'survey2'）；switchem2(tis，'survey3'）；document.

getElementById（'surveytextarea'）.value=''"type="radio"name="q1"value="Yes,Probably

would purchase"＞Probably would purchase＜/li＞

＜li＞＜input

onclick="switchem(this，'survey2'）；switchem2(tis，'survey3'）；document.

getElementById（'surveytextarea'）.value=''"type="radio"name="q1"value="Yes,Might or

might not purchase"＞Might or might not purchase＜/li＞

＜li＞＜input

图　2-4：基于用户输入，调查可能会扩展到6个问题（见彩图7）

onclick="switchem(this，'survey2'）；switchem2(tis，'survey3'）；document.

getElementById（'q2a'）.checked=false；document.getElementById（'q2b'）.

checked=false；document.getElementById（'q2c'）.checked=false；document.

getElementById（'q2d'）.checked=false；document.getElementById（'q2e'）.

checked=false；document.getElementById（'q2f'）.checked=false；

document.getElementById（'q2g'）.checked=false"type="radio"name="q1"value="No，

Probably would not purchase"＞Probably would not purchase＜/li＞

＜li＞＜input

onclick="switchem(this，'survey2'）；switchem2(tis，'survey3'）；document.

getElementById（'q2a'）.checked=false；document.getElementById（'q2b'）.

checked=false；document.getElementById（'q2c'）.checked=false；document.

getElementById（'q2d'）.checked=false；document.getElementById（'q2e'）.

checked=false；document.getElementById（'q2f'）.checked=false；

document.getElementById（'q2g'）.checked=false"type="radio"name="q1"value="No，

Definitely would not purchase"＞Definitely would not purchase＜/li＞

＜/ul＞

＜/li＞

＜li id="survey2"style="display:none"class="surveynum"＞Which of the following are

extremely important to you?

＜p＞(Ceck up to 3 responses)＜/p＞

＜ul class="nobullet"＞

＜li＞＜input type="checkbox"name="q2"id="q2a"value="The price of the product"＞

The price of the product＜/li＞

＜li＞＜input type="checkbox"name="q2"id="q2b"value="The product's

lifetime guarantee"＞The product's lifetime guarantee＜/li＞

＜li＞＜input type="checkbox"name="q2"id="q2c"value="The quality of

the craftsmanship"＞The quality of the craftsmanship＜/li＞

＜li＞＜input type="checkbox"name="q2"id="q2d"value="The fact that it can be

customized to my taste"＞The fact that it can be customized to my taste＜/li＞

＜li＞＜input type="checkbox"name="q2"id="q2e"value="The prestige of

owning the product"＞The prestige of owning the product＜/li＞

＜li＞＜input type="checkbox"name="q2"id="q2f"value="The safety features"＞The

safety features＜/li＞

＜li＞＜input type="checkbox"name="q2"id="q2g"value="The on-call customer

service guarantee"＞The on-call customer service guarantee＜/li＞

＜/ul＞

＜/li＞

＜li id="survey3"style="display:none"class="surveynum"＞Why are you not interested

in this product?

＜ul class="nobullet"＞

＜li＞＜textarea id="surveytextarea"name="q3"＞＜/textarea＞＜/li＞

＜/ul＞

＜/li＞

结果是在采用5分制的问题1的问答中，若受访者选择其中三个正面答案中的任意一个，都会显示一个清单来帮助我们更好地识别该受访者喜欢该产品的什么方面（见图2-5）。相反地，若受访者选择两个负面答案中的任意一个，都会显示一个文本区，在该文本区中，该受访者可以准确地解释为什么他不喜欢这款产品（见图2-6）。

随着编程不断进展，以上这些都是小儿科，几乎不值一提。但是站在用户角度其所带来的影响是微妙且有力的。它意味着我们可以通过对话的方式“倾听”和“答复”用户的输入。它也意味着由于表单长度造成的心理影响是很低的，因为用户最初面对的只是3个问题的调查。该调查潜在地可以扩展到6个问题，但是这些都是在用户的“着陆页”发生的，而且不需要用户离开页面，因而避免了强迫用户点击类似于“下一页”的按钮。

图　2-5：调查细节——当用户对问题1回答“Yes”时（见彩图8）

图　2-6：调查细节——当用户对问题1回答“No”时（见彩图9）

设计信任

为了尝试与受访者建立信任，我们确实做了一些具体的实践，暗示受访者这是一次合法的调查，而不是惩罚“苦旅”。首先，我们在Web调查页面的最上方很显著地展示了客户的公司标识(lgo)。该标识本身又链接到Urban Wallace Associates的主站的“关于我们”(Aout Us)页面，因此参与者可以很清楚他们在和谁进行沟通。此外，我们把调查页面放在客户的主站的子域名，而不是什么第三方提供商。

正如之前所述，我们的客户所要做的调查研究需要每个受访者在美国有居住权。但是，由于我们告诉了每个受访者“我们不收集任何个人身份信息”，所以询问受访者居住在哪里会显得很不适宜。对于该问题的解决策略是自动记录受访者的IP地址，这样可以满足获取美国地理位置的需求，而且没有违背受访者的隐私。毕竟，用户的IP数据在他或她访问任何Web站点的任何时候都被记录下来；而且在绝大多数情况下，IP数据只能用于确定用户的网络服务提供商(IP)所在的城市，否则该网络服务提供商就是匿名的。

我们后来购买了一份廉价的IP与州的映射关系数据。根据这份数据，我们可以知道收集到的每个IP地址在美国的哪个州。虽然我们可以通过在页面嵌入脚本来访问该数据库的方式实现在收集数据的过程中实时地对数字做地理信息匹配，但考虑到所面临的实际情况，我们还是选择了半自动化匹配方式。在开始阶段，项目预算和时间表不能保证能够购买额外的服务器资源来处理该任务。但更重要的是，从用户角度考虑，进行实时匹配所带来的延迟将不可避免地成为调查过程的一部分。

虽然对我们来说，能够立即获取最终的数据可能会更方便，但它也会给我们的用户带来额外的不便。当设计数据收集时，为了满足用户需求，重要的是要考虑在调查过程中，服务器首先必须完成哪些任务，而哪些任务是可以推延到数据收集之后再完成。不要要求用户——为了你——去做或者去发现你自己可以做的事。

以上所有这些都把我们带回到本章的中心，也是建立信任的最终和最核心方面：尊重受访者。通过表示你对受访者以及她付出的时间和智慧的尊重，通过交谈对话的方式和她互动（虽然事实上所有的调查问卷都是通过对机器进行预设置来完成的），而且向她显示你一直在“倾听”她的回答（例如，不要一遍又一遍地问受访者几乎相同的问题，这会显得你没有注意听她最初的回答），这样，你将增加信任，鼓励真实的回答，而且避免受访者“溜号”。

为精准的数据收集而设计

这种讨论方式似乎显得有点“亲昵”，尤其是对于只与收集来的那些“生硬”的数据打交道的人们，而不是与生成这些数据的人们打交道。但是，所有这些以用户为中心的关注都不仅仅是礼貌问题，它对于我们实际获取到的数据的可靠性也是至关重要的。Roger Urban的公司专长于通过当面沟通、邮件、电话和类似于本案例的电子邮件调查。“对于这样的调查，”他说，“你是在处理非常少量的数据，因此数据的质量是非常重要的。”换句话说，如果仅仅是基于几百个人的回答来做出重大决策，那么这些回答最好是“优秀的”。

但是，“优秀的”的回答并不意味着是肯定的回答。毕竟，这是调研，和科学家们一样，我们想要评估的是实际情况（对于这款产品，用户是否会很关注其价格？安全性是否是他们最关心的？他们实际上对我们的服务是否满意？），并查看我们的假设错在哪里。“说服技巧对于调研是个灾难。”Roger Urban如是说。人们为了使提问者高兴，通常会下意识地通过回答他们认为是提问者所期望的答案。

无论是“隐式”地还是“显式”地采用说服技巧，都将会使得你的调研数据结果有偏差。“如果你想要一份人为地非常肯定的数据，”Urban说，“我每次都可以获取给你。”但是，如果你是在做真正的商业决策或者政治决策，这种数据又有何用？

动机

在数据收集期间，你不能使用说服技巧；而在调研最初时，你又确实需要说服受访者参与。既然没有金钱，那么对于那些受访者，他们的动机会是什么呢？

“应该总是有一些‘好处’”，Roger Urban说，“即使该‘好处’仅仅是‘表达你的观点’”。人类是有趣的动物，生冷僵硬的金钱可能并不能驱使我们去做某些事情，反而是一些更“朦胧”的“好处”可能会驱使我们去做，比如一些很得体的“恭维”。我们都希望自己被认为是专家；为了证实我们的观点是正确的，可能就足以说服我们花时间和陌生人交流。另一方面，通过参与我们可能可以获取一些“内幕信息”、看得更远，这些想法也会诱惑我们去和陌生人交流。举个例子，参与一项可以一睹Apple的下一个i-gadget设计的调查，有哪个技术痴迷者会对此不热衷呢？

对于这个项目，我们知道目标客户是年纪较大的受众群体。为了使邮件接收者参与调查，初始邮件的语言设置是很重要的，我们团队在邮件中征求了受访者的专业知识。在我们的第一封邮件中，我们对同样多的接收者测试了两种不同的标题：

“您可以为后代塑造**产品（产品信息被删除）的形象。”

“我们正在寻求‘经验之声’。”

结果是对于第一个标题，虽然赋予受访者权利来操纵未来的方向，相比之下，显然被证明了显得更短暂易逝和过于无私（毕竟，它意味着其利益只是给后代，而不是受访者本人）；而第二个则勾起人们的自我主义，把她们的年龄变成积极的一面（“经验”）。对于第一个标题，有12.90%的接收者收到邮件后点击它，这些人当中，有16.22%完成了调查。而对于第二个标题，有14.04%的接收者收到邮件后点击它，这些人当中，有29.5%的人完成了调查。当两周后开始第二次调查时，所有消息的标题都是“经验之声”，有27.68%的接收者打开邮件，这些人当中，有33.16%的人完成了调查。第二次邮件也发给了那些第一次没有打开邮件的人们。(Eail调查的一个秘密是：给相同的邮件列表发送邮件，第二次发送通常会和第一次发送收到同样多的答复。）

用户体验哲学上值得记住的另一方面是：对一切都进行测试。在这种情况下，甚至是对你的测试方法进行测试！当你有时间和资源时，要对不同的副本、不同的布局和不同的交互设计类型进行测试，而且这些都是基于和实际用户进行的测试。

报告即时数据结果

在我们的项目中，一个特殊的考虑是结果数据的接收者，即客户，也会成为系统的用户，他们和那些调查参与者有着显著不同的需求。

由于该项目时间紧迫，客户为了确定该产品是否普遍受欢迎，需要快速看到调查结果。对于这种应用背景，我们的解决方案是采用一个HTML页面显示数据，客户可以访问该页面，其数据通过最少的格式化进行简单排序。即时、原始的调查结果首先通过邮件列表来排序（其中每个标题都发送给两个邮件列表，年龄段分别是在55～64岁和65～75岁），然后是通过人们对第一个问题，即是否对该产品感兴趣所做出的Yes/No回答。需要说服调查参与者来参与调查并完成表单填写，而和这些参与者所不同的是，客户希望的是能够尽快看到生成的数据结果。对于客户，速度和对即时结果的即时访问比其他任何因素都要重要。因此，客户在用户体验上反映了以下这些优先级（见图2-7）。

图　2-7：在这个即时数据报告屏幕上，随着数据的流入，客户可以看到调查结果

当然，最后交付的不是这种原始数据的展示。在项目完成时，我们向客户展示的是完全可排序的Excel表格，这些表格包含我们收集到的所有数据（总共包含8个邮件列表，通过两次批量发送的方式），包括在调查时还没有生成的关于所在美国的哪个州的地理位置数据。

结论和反思

最后，我们付出的这些努力都是值得的吗？这仅仅是一个Web表单，不是吗？人们每天都在填写数百万的表单。有些人可能会认为我们根本就不需要考虑如何设计——创建一个可用的Web表单的问题早已经解决了，而且是一劳永逸的。但是你永远都不应该低估在解决最普通的设计问题上所缺乏的努力，尤其是在线的设计问题。现在绝大多数的表单和20世纪90年代的表单相比，并没有太大区别。

此外，如果有一件事是一名优秀的设计师应该清楚的，尤其是对于一名遵循用户体验规范的设计师，那就是不存在“一刀切”式的解决方案。为你的用户群体采取定制方式几乎总是会提高用户体验，而且是针对数据收集这种类型来说。

对于我们的客户的情况，从结果上看，付出的努力是值得的。我们了解到，该电子邮件营销公司以前的推广活动中，通常打开其邮件的概率在1%～2%，而我们的邮件打开概率高达4%。对于打开的邮件，通常完成点击的概率在5%～7%，而我们的则高达21%。最重要的是，对于这些执行点击操作，跳到Web页面，继续操作（例如完成表单）的概率是2%～5%；而对于我们的设计，其完成概率是29%（见图2-8）。

图　2-8：我们的调查答复概率要显著高于正常情况的答复概率，这归功于更好的总体用户体验

当然，这次调查比该公司之前的邮件调查收效好得多，但是也存在其他可能的原因。可能是因为该产品比该公司之前为其他产品或话题开展的调查更加吸引人，而且可能是该产品给人们带来的兴奋之情使得更多人能够参与到最后；也可能是预选的接收者比通常的要准确得多，而且这些接收者特别适合该产品；甚至可能还存在年龄上的偏见，认为是年龄在起作用——年纪大的计算机用户比年轻的用户更有可能会打开邮件、阅读这些邮件、点击进入、完成调查，而年轻的用户是否可能更精明，而且对于不请自来的邮件更加谨慎？我们不了解对于该主题的任何研究，但它是一种可能性。实际上，虽然我们不能完全排除这些解释中的任何一个，但是电子邮件公司为我们这次调查所做的似乎并没有和他们之前为其他几百个调查所做的有什么区别。因此，很有可能可以下这样一个结论：该项目的成功和我们的表单设计有关系。

顺便说一下，虽然这和该调查的设计没有什么关系，不过也许你还是会对此感兴趣，那就是受访者对该产品本身的反馈非常积极正面。虽然客户告诉我们的是如果有10%的正面答复概率（即对第一个调查问题的回答是“Yes”），就可以推出该产品；而结果是超过16%的受访者潜在地对于购买该产品颇感兴趣。这究竟是一款什么样的产品呢？遗憾的是，保密协议禁止我们对本次调查透露更多信息。

如果你想“亲眼目睹”这款产品，你只能期望能够参与到下一次的电子邮件调查中。不要过快地把这些电子邮件扔到垃圾箱中；至少，你可以学到关于表单设计的一些好的或者差的方面。

第3章　火星上的嵌入式图像数据处理

J. M.Hughes

摘要

太空飞行器是独特的工程项目，其受到的约束和要满足的需求是地球上的任何物体所不具备的。它们必须能够承受极端恶劣的气温极限、完全的真空空间和强烈的辐射，而且其重量必须足够轻，以便可以用火箭将它们载入太空并发送到其目的地。太空飞行器是应用了极简主义的一项“运动”：只有完成任务所必需的（设备），没有丝毫多余。所有方面的设计都将测试以下几个方面：必要性、重量和成本。在发射之前，一切都要经过测试、再测试，包括太空飞行器的“大脑”——嵌入式计算机系统及其上面运行的软件。本章将概述凤凰号火星着陆探测器(Poenix lander)的图像处理软件如何获取、存储、处理图像数据以及最终如何再把图像发送回地球。

简介

人们在设计和编程实现一套嵌入式系统时，会受到很多约束条件。这些约束包括处理器速度、执行期限、允许中断的延迟时间以及内存约束等。由于太空飞行的使命，这些约束条件会变得很严酷。通常，在太空飞行器上的计算机只使用非常昂贵的能够抗辐射的内存。它的中央处理器(CU)，通常也是经过专门设计的设备，因为它需要承受高能量宇宙射线的破坏性影响。从商业标准来看，其CPU运行速度并不快，而只是典型的抗辐射电子设备。这其中的权衡是速度和被一个星际粒子直接击中后还能不断运转的能力。例如，一台典型的PC机里的双核CPU将在太空中无法长时间正常运转（同理，该PC机的其他大部分电子设备也不会正常运转）。

还有一些科学目标，它们驱动着软件的功能和性能需求。所有需求必须满足太空飞行器的计算环境的约束条件。经过无数次的权衡抉择之后，最终产品必须能够运转，而且在飞行任务期间不能出现致命的错误。因为，对于机器人太空飞行器，任何错误都足以导致飞行任务非正常终止，所以，在点燃火箭、飞向蓝天之前，有必要保证所有的方面都是正确的。

在2008年5月25日，凤凰号火星探测器安全降落在火星的北极地区。图3-1是一位艺术家所画的凤凰号着陆效果图。和巡游在火星赤道附近相对温暖的地区的探测器不同，凤凰号探测器是位于贫瘠荒岛上的一个静止的探测器，荒岛上气候寒冷，大气压力相当于在地球上约10万英尺的高度时的压力。火星上稀薄的大气的主要成分也是二氧化碳。虽然算不上是一个理想的度假胜地，但却是一个寻找古代固态水的好地方。

图　3-1：凤凰号火星探测器在火星上的着陆效果图（图像来源：美国航空航天局和喷气推进实验室NASA/JPL，见彩图10）

该探测器的任务是寻找可以证明水存在的直接证据——可能在火星表面下以冰的形态存在（顺便提一下，探测器找到了冰），以及能够说明火星曾经提供了适合生命的栖息地的证据。由于飞行器的着陆点的位置靠近极地，它的寿命是有限的；当火星的冬季来临时，凤凰号几乎肯定会结束“生命”。在高纬度的着陆点，该探测器在一个完全黑暗的、被固态二氧化碳所覆盖的极寒冷的（-90℃或者更冷）冬天下存活的机会是非常非常渺茫的。

我是凤凰号探测器的成像软件的首席软件工程师。在本章中，我将分享在设计凤凰号火星探测器的成像飞行软件的各种各样的数据处理策略的过程中所做的一些思考。因为该软件负责处理在火星表面的所有成像相关的任务，又具备“飞行软件”的使命，所以在喷气推进实验室/美国航天局(JL/NASA)的术语中，该成像软件又称为“成像飞行软件”。

对于凤凰号火星着陆探测器，其面临的挑战是同时捕获并处理四个感光元件(carge-coupled device,CCD)成像器的数据（类似于普通的数码相机的功能），而且所有这些操作都是在探测器的主计算机预分配的非常有限的内存中完成。不仅如此，图像可能还需要在传输回地球之前使用一个或几个不同的压缩方法进行压缩。此外，一些最终的数据产品（即图像）需要被细分为不同的分段，每个分段都有自己序列化的数字数据头，这种方式可以满足高效存储在太空飞行器的闪存中，并且当数据包从火星传到地球时，可以减少数据包丢失等原因所造成的数据量的丢失。最终生成的嵌入式代码，在凤凰号火星着陆探测器正常运转的生命周期内，获取并处理了超过25000张的图像。

一些背景

在我们深入了解数据处理之前，最好简单地介绍一下“戏剧的主角”：成像器（也称为照相机）和太空飞行器的计算机。

凤凰号火星着陆探测器的主计算机的CPU是一个RAD6000，其最大时钟率为20MHz，另外它也能够以较低的时钟率来运行，以节省电池能量。这其中没有什么尖端的技术，只是一台防辐射的、把内存和闪存都混合在VME电路板的第一代PowerPC。CPU完成处理着陆任务后，它的主要功能是和地球通信（用专业术语来说，就是“上行和下行”，接下来将会详细介绍），监测太空飞行器的健康状况，通过从地球上发送的各种命令来协调各种科学设备的运行活动。它采用了WindRiver公司的VxWorks实时操作系统(ROS)，以及太空飞行器承包商Lockheed Martin公司提供的各种扩展软件。所有的飞行软件都是用C语言实现的，代码编写遵循一组特定的规范。

“上行”和“下行”

从太空飞行任务的专业角度来看，术语“上行”和“下行”指的是在地面和太空飞行器的控制器之间传送数据或命令。“上行”指的是传送命令或者数据到太空飞行器。“下行”发生在太空飞行器向地面发送回数据。

正如生活中的许多事情，指向飞行器顶部的天线并点击“Push To Talk”（对话）按钮，从来就不是一件很简单的事情。“上行”的数据或者命令必须首先通过审核，甚至是通过一些模拟来确保一切都准确无误。然后，命令和数据将会传递给命令控制器，当存在“上行”任务时，该控制器将会调度该任务（或者用太空术语来说，即“辐射”(rdiated)）。最后，这些数据进入NASA的Deep Space Network(DSN)通信系统，并从太空中辐射出来。但这并非最后一步，因为对于凤凰号探测器，它还必须通过绕火星轨道飞行的人造卫星进行中转，因为凤凰号探测器本身无法直接和地球通信。当该人造卫星运行到火星水平线时，凤凰号探测器将会监听新的“上行”数据。

“下行”也一样错综复杂。同样地，需要人造卫星作为中继，从凤凰号探测器接收数据，然后把这些数据传送回在地球上的NASA的其中一个DSN接收天线。然后，将会对这些数据进行各种处理和中转操作，直到把它们最终保存在JPL。如果是图像数据，JPL的Mission图像处理实验室将会对这些图像重新组合，提供给Arizona大学的科学运作中心的那些热切期待数据的科学研究团队。

凤凰号火星着陆探测器包含三个主要的照相机来完成表面科学成像：立体表面成像器(Sereo Surface Imager,SSI，有两个CCD感光元件）、机械臂成像器(Rbotic Arm Camera,RAC，只有一个CCD感光元件）、晶体管光学显微镜(MCA Optical Microscope,OM)成像器（同样，有一个和RAC相同的CCD)。图3-2显示了立体表面成像器SSI的飞行模型，图3-3显示了连接到机械臂的机械臂成像器RAC。光学显微镜放置在晶体管装置的内部，它本身看起来像是着陆器甲板表面上的一个黑盒子。

设计时所面临的挑战是设计一种方式，用以完成以下几个步骤的处理：从每个照相机上下载图像数据；把数据保存在一个预分配的内存空间；对数据进行处理，删除已知的像素点缺陷；修剪或者扩展图像；根据命令行参数进行压缩；然后对图像进行切片和切块(sice-and-dice)，把图像切分成各个数据包，把这些数据包传送给主机的“下行”(dwnlink)管理任务，从而传输回地球。

在立体表面成像器SSI中，每1024×1024像素的CCD感光元件能够生成2MB的数据，或者是100万个12位像素的数据。因为SSI是一台真正的立体照相机，它的成像器通常叫做“眼睛”。而且，SSI看起来确实有点像老式的机器人脑袋。机械臂成像器RAC和晶体管光学显微镜成像器OM每个都包含一个512×256像素的CCD感光元件成像器，每个成像器生成131072（或262144字节）个像素的数据（从现在开始，对于这两台成像器，我都将称之为RAC/OM，因为从成像软件角度出发，它们是完全一样的成像器）。实际上，CCD成像器的每个像素只占用12位的空间；标准内存的一个“字”长是16位。如何处理剩余的4个未使用的位，在设计阶段引发了一些有趣的讨论，这些讨论我将在下章中讨论。SSI、RAC和OM生成的所有图像都是单色的，没有彩色的图像。彩色图片是由在地球上使用过滤器或者特殊的照明技术，获取到独立的图像进行合成而来的。

图　3-2：立体表面成像器（图像来源：Arizona大学/NASA/JPL，见彩图11）

必须指出的是，虽然成像软件控制OM CCD来获取图像，但该软件和晶体管(MCA)设备、光学显微镜本身的电子机械控制并没有关系。后二者的控制是由JPL的MECA团队实现的一个独立的实时任务来负责处理的。

虽然以当前消费者的数字照相机的标准来看，凤凰号火星着陆探测器的100万像素并不算高，但是其使用的CCD成像器是为科学成像而专门定制的。照相机中的每个CCD感光元件设备都花费了数万美元，而且生产制造的数量有限。这些设备可靠、健壮而且精确，而且每台CCD都进行了全面地测试，而且为敏感度、噪音和缺陷等做了像素级别(pxel-by-pixel)的特性定制。正是这种特殊级别的特性化以及CCD生成的参考数据，把科学的CCD设备和消费者使用的普通照相机区别开。精确的特性化使得研究人员对图像数据能够精确地表现照相机所捕获的镜头充满自信。但是它也是影响CCD代价的主要因素。

图　3-3：机械臂照相机(Rbotic Arm Camera，图像来源：Arizona大学/Max Planck/NASA/JPL)

数据是否打包

对于任何高度受限的嵌入式系统，其软件需要同时满足操作需求和执行环境的约束条件。正如人们可能预期的那样，这些约束条件往往并非总是互补的，因此在设计实现过程中，需要做出一些权衡决策。SSI和RAC/OM照相机都使用了12位的转换或像素数据，这带来了我们的首次主要的权衡决策：数据打包。对于二进制数据的概述，请参阅后面的“二进制数据”部分。

在任务的早期设计阶段，大家产生了对12位像素数据进行打包的想法，还引发了一些有趣的争论。由于只有有限的内存可用于图像数据存储，对12位像素的数据进行打包存储在16位的内存空间中，这个想法还是很吸引人的。这里的“打包”，指的是对这些12位像素的数据进行连续存储，在这些位中间不“浪费”任何空间——实际上就是忽略16位的内存边界限制。但是更高效的数据存储换来的代价是处理时间变长（解包、移位和重新打包）。一个数字图像就是一个数组（该图像需要作为一维数组处理还是二维数组处理，依赖于需要在它上面完成哪些操作），因此，在图像上的任何操作需要利用对数组的一个或多个算法循环来处理其图像数据。我们期望通过RAD6000处理的数据量非常大，因此，即便是在20MHz的时钟频率下，即计算每个CPU循环时间，该速度也将是慢得让人无法接受。

最后，我们决定“浪费”一些内存空间，把12位的像素存放在16位的内存空间，使得处理简单，避免占用任何不必要的CPU资源。这个决定也是由以下早期的想法所驱动，即通过就地执行所有的图像处理和压缩操作，避免使用多个大的处理缓存或者结果数组。如果对数据打包，将会使得就地执行这些操作变得异常困难，而且最终代码也将会变得过于复杂，导致整个我们想要实现的就地处理的想法都变得不可行。

二进制数据

数据即信息。在计算机系统中，它是通过数值来表示的，因为计算机CPU所处理的就是数值。数据能够表示文本，在该文本中，每个字符有一个唯一的数值，或者通过对每个像素或者图片元素进行编码来表示图像，在一个图像中，由一个数值表示它的强度、颜色以及这二者特征的组合。给定一个合适的数值编码方案，计算机可以处理一个人可以想象到的任何类型的数据，包括音频、电位、文本、图像甚至是定义猫和狗区别的一组不同特征。但是不论数值表示的是什么数据，对计算机而言，它们都仅仅是数字。我们提供规则来说明该数字应该如何编码、处理、显示和解释。

数据有不同的大小值，该值取决于其表示的是什么。举个例子，一扇窗户或者一个防盗报警系统的门锁只需要1位（或一个二进制数）来表示其可能的两种状态：开或关，即0或1。为了表示一个英语字母或者发音，需要100个左右的数字，每个数字通过8位的数据来表示。现代计算机通过二进制形式工作，因此8位可以表示从0～255的任何一个数字（如11001000_2=200_10）。在很多计算机中，有更合适的形式来表示二进制数值，如典型的八进制或者十六进制。对于16位或32位的CPU，正如凤凰号火星着陆探测器所采用的，内存可以通过16位的字长方式来高效访问。想要访问低于16位（如8位）的字长方式，其效率实际上可能会很低，因此对于大于8位而又小于16位的数据，通常都是以16位字长的内存方式来存储，有几位没有使用。这正是凤凰号火星着陆探测器所处理数据的方式，因为CCD成像器的电子设备每个像素生成的是12位的数据，而飞行器的内存是通过16位或32位的存储方式组织的。

三个任务

对于凤凰号火星着陆探测器所采用的VxWorks实时操作系统环境，实际上确实没有什么方面是和普通的计算机用户可能会想到的某个项目相同。不在磁盘上加载任何东西（不存在磁盘），而且当系统初次启动时，计算机所做的每件事都被加载到内存中。该实时操作系统实际上只是一个大程序，包含很多几乎同时在运行的较小的子程序。这些较小的子程序又称为任务或线程，基于以下因素来执行：资源的可达性，如时间事件或者I/O（输入/输出）设备；这些子程序在更大范围中被分配的优先级（优先级高的任务比优先级低的任务能够获取更多的机会运行）。

一开始就显而易见，对于表面图像处理，至少需要完成两项任务，每台照相机完成一项任务。SSI成像器和RAC/OM成像器有很大区别，它们包含不同的命令集和操作特征。SSI使用所有新的控制器硬件，并且结合了和那些用于Mars Rovers（译注：火星巡游器）完全一样的CCD成像器。RAC和OM成像器最初是由德国的Max Planck研究所建立的，而且已经应用了一段时间（其中一个最初设计应用于Huygens探测器，在Titan着陆）。RAC/OM控制器硬件实际上是命运多舛的Mars'98任务中的飞行器备用设备，当其飞行器引擎在火星表面上方几百英尺处不正常终止时，该控制器也结束了其“悲惨的命运”。但是从每个照相机获取的数据仍然需要处理、压缩，然后“下行”（向地面传输），而且这些操作并不依赖于物理数据源。图像数据的每个像素都是12位，这些数据中真正有区别的是几何信息（高度和宽度），以及最终需要处理的图像数据量。

虽然太空飞行器集成团队(Lckheed Mar-tin)期望保持科学设备的任务数量是最少的，在早期就显而易见，在两个照相机任务中都重复相同的图片压缩和“下行”功能并没有多大意义。由于程序的存储空间有限，这种重复操作会浪费资源；因此，对代码做必要的修改，然后验证这些修改可以达到事半功倍。最后，我们达成的一致意见是使用三个任务：一个任务处理SSI，另一个任务处理RAC和OM照相机（这两个任务不能同时执行，因为接口硬件不允许它们同时执行），第三个任务将作为共享资源，利用由两台照相机的控制任务所生成的数据，执行图像压缩和“下行”处理，因而它是异步运行的。因此，虽然SSI和RAC/OM任务通过控制电子设备来获取图像数据、控制照相机的内部温度、执行运动控制，第三个任务仅仅完成图像数据处理和“下行”操作。

图像处理任务被称为ICS,ICS代表图像压缩子系统(Iage Compression Sub-System)，虽然该子系统最终不仅仅只是压缩图像数据。图3-4中显示了三个任务的流程框图以及它们之间的通信。

使用三个任务的决策确实增加了系统的复杂性，但它也减少了需要的程序存储空间。作为额外的补充，有第三个独立的任务使得改变一个特定的ICS处理功能变简单得多，然后通过使用真正的照相机硬件，对从任何一个成像任务获取的数据或者是采用模拟的图像数据源，对这些功能进行测试。这些测试，对于后期对项目做更多的扩展压缩测试是极有好处的；当超过15000个的测试图像通过ICS被连续处理，同时使用自动化测试验证其操作。

ICS也包含两种资源模块，它包含静态内存管理（也称为“槽管理”）和图像处理（取样、子图定位、像素缺陷纠正等）。这些实际上并非ICS任务的一部分，而是作为线程安全的伪程序库来支持两个照相机控制任务以及ICS任务。实际情况是由于该设备软件所面临的架构约束，把这块代码放在任何其他地方都不合适，因此，最终把它放在ICS。

图　3-4：图像飞行软件任务

对图像切槽

本章之前提到过，每个设备任务可以分配到的内存数量是有限的，但究竟有限到什么程度，可能说出来会让有些人大吃一惊，因为现在对于一台桌面台式机，有500M或者1G的内存已经很普遍了。分配给SSI和RAC/OM用于图像数据存储任务的初始内存差230K是10M（确切地说是10255360个字节）。大家讨论过，在太空飞行器着陆后，可以增加其内存分配。这意味着任何内存管理机制都必须很灵活，而这也是设计的基本要求。默认的存储机制需要至少处理四个SSI图片（或者两对，每个“眼睛”包含一个图片），而且至少四张RAC/OM图片，这些图片都存储在相同的内存空间中。这种零散的大小分配方式意味着至少在初始阶段，不可能“挤进”超过四张的全尺寸大小的SSI图像。

在嵌入式系统中，使用动态内存分配通常被认为是一个非常糟糕的想法。为了避免内存碎片、内存泄露、空指针、莫名其妙的程序崩溃以及可能完全无法完成任务，飞行软件编码规范禁止使用动态内存分配（C的malloc函数及其家族）。这意味着不论分配给成像软件的内存有多小，它都需要自己在该内存范围内管理图像数据，而且软件必须健壮、可靠。

为了满足以上需求，成像软件的解决方案是在启动时，使用一组函数作为内存管理器，用来为ICS预分配内存。内存管理器是图像数据处理的核心组件。为了避免冲突，采用阻塞信号来控制三个成像任务间的共享访问（实际上，在太空飞行器软件中的任何一个任务都可能使用共享内存，但实际情况是只有照相机使用共享内存）。

静态内存分配被划分成“槽”，它可能大到足以容纳一个全尺寸大小的SSI图像，或者是较小的RAC/OM图像。图3-5显示了ICS图像存储空间的默认组织方式。

图　3-5：默认的图像“槽”分配

这只是一种可能的配置方式，每种类型的槽的个数可以通过接收从地面传输上来的命令而随时改变。

每个图像还包含一个包含头数据的关联结构。图像头数据记录了以下信息，如定义生成图像的照相机的代码、曝光时间、选择的图像处理选项、图像维度、可能使用的光学过滤器以及图像如何压缩（如果采用了压缩方法）。部分的头数据是由生成图像的设备任务生成的，其余部分是由ICS在把图像数据发送给太空处理器“下行”的处理程序之前生成的。由于头数据并非图像数据本身，它存储在一个独立的内存槽，直到需要执行“下行”操作时才使用。每个图像槽和其关联的头数据需要通过协力的方式进行跟踪和处理。

如图3-6所示，内存管理器实际上只是一组函数集合，这些函数在一个结构数组上执行操作。内存槽的当前状态是保存在数组中，这些数组实际上构成了一个物理内存空间及其内容的动态模型。

图　3-6：内存槽管理数组

其中一个数组包含图像数据的结构体，每个图像槽对应一个结构体。该结构体通过C语言类型定义如下：

typedef struct{

uint16_t slot_status；　/**＜Owned or unowned　*/

int16_t slot_owner；　/**＜-1 if slot is unowned　*/

int16_t slot_size；　/**＜either RAC/OM or SSI sized　*/

uint16_t*slot_address；　/**＜address of data space of slot　*/

}ics_img_slot_entry_t；

第二个数组包含指向头数据入口的结构体，其定义如下：

typedef struct{

uint16_t slot_status；　/**＜Owned or unowned　*/

int16_t slot_owner；　/**＜-1 if unowned　*/

int16_t img_id；　/**＜associated image data slot number　*/

uint16_t hdr_data[ICS_HDR_SLOT_SZ]；/**＜array for header data　*/

}ics_hdr_slot_entry_t；

注意：图像的头结构体包含该图像ID的入口。这一点是很有必要的，因为槽可以以任意次序进行分配和释放，而且不能保证一个图像槽入口的索引和它原来关联的头数据槽的索引是同一个索引。我们采用图像ID把图像和头数据入口绑定在一起，而不是依赖于数组中的索引偏移来保证图像和头数据入口是同步的。

由于可以动态重新配置图像的槽分配，内存管理器可以适应于特定的任务。一方面，如果为火星上的某天制定的计划（或者称一个Sol)和通过SSI成像相关，那么我们就可以通过槽配置来最小化RAC/MECA的大小分配，即设置为默认的配置。另一方面，如果该计划和很多的RAC或OM图像相关，那么我们可以对内存进行配置，使得它能够处理上限39个、较小尺寸的图像。

传递图像：三个任务间的通信

一台照相机的图像数据刷新会通过其中一个照相机任务写入槽中。在执行完任何必要的像素纠正和子图定位操作之后，该照相机任务会通知ICS有新的图像可以处理。然后ICS根据输入的命令，对该槽中的图像执行就地压缩（不论是有损的还是无损的），然后对图像数据进行打包，传递给“下行”处理程序。只有完成了“下行”处理，该槽才会被释放，供新的图像使用。从图像曝光到为SSI照相机传送图像的事件顺序如图3-7所示。

图　3-7：图像获取和传送序列

如图3-6所示，整个事件的次序是根据图像槽的可达性而定的。如果无法获取槽，照相机任务将会等待一段可配置的时间，使得ICS可以完成压缩和图像“下行”处理，这样就有一个图像槽变得可用。如果ICS在那段时间内不释放槽，该照相机任务就会给地面的操作员生成一条错误信息，直接把该图像保存在照相机中（因为实在没有其他地方可以保存该图像）。

一旦一个照相机设备任务获取一个槽，它在把该槽交给ICS之前，就“拥有”该槽，而交给ICS之后，ICS就拥有该槽。拥有权的验证是基于槽管理器在初始化该槽时所分配给它的槽ID（即ID数值）、图像ID代码和照相机设备任务ID。当某个任务把一个槽交给ICS时，ICS会验证这些ID和之前记录下来的该图像槽的ID是否匹配。

正如之前所述，ICS是异步方式运行，且和任意一个照相机任务并没有密切关系。可以通过利用VxWorks的内置消息队列系统，并且在内存槽管理器中使用共享函数，把ICS和照相机关联起来。图3-8显示了这些步骤的消息序列图(MC)表示方式，这些步骤包括从照相机获取数据到最终把它们传输给“下行”程序。

使用内部消息队列（S/C FSW操作系统管道，代表太空飞行器的飞行软件），这将允许任意一个照相机任务使用和执行从地面“上行”的命令相同的机制，并向ICS发布命令。该命令会保存在队列中，直到ICS完成了其当前的任务。只要有可用的槽来存储图像数据，照相机就会继续获取图片，ICS将轮流检索和处理这些数据，直到队列为空，而不需要考虑照相机当前的工作状态。

注意：图3-8没有显示在成像过程中的错误检查。总之，错误检查和出错处理的代码行数大概和实际上执行处理或者处理该数据的代码的行数相同。在设计生命周期的早期就采用了失效模型效果关键性分析(Filure Mode,Effects,and Criticality Analysis,FMECA)技术，该技术在软件实现和出错处理能力上具有指导作用。

ICS对数据流进行序列化，但是使用图像槽和命令消息队列允许一组图像可以连续快速地（相对而言）获取。这也意味着对于图像获取，存在一定的时间间隙(tming margin)，这减少了由于等待图像“下行”处理，操作被挂起的几率。早期的测试命令序列证明了可以执行如下操作：制作短篇“电影”（这么说是因为从SSI照相机下载一张图片大约需要6秒），或者使用不同焦距的RAC或者OM生成大的（30多张图像）数据集合。

获取图片：图像下载和处理

从图像曝光到最终传送给ICS，这期间发生了很多事情。系统中的每个照相机都有自己的控制电子设备来处理命令和把从CCD获取的模拟信号转换为12位的数字值，然后把数据存储在硬件缓冲区中，直到飞行软件可以把数据下载到图像槽。

图　3-8：图像获取、处理和“下行”处理的MSC表示

以上所有这些都是在防辐射的可编程门阵列设备中的嵌入式逻辑控制之下发生的。

照相机一旦获取到一张图像，并把它写入图像槽，就会对该图像执行各种方式的处理，每种处理都是在图像槽的约束下就地执行。由于没有额外大的（图像尺寸）缓冲区可以供处理过程或者保存结果数据使用，因此，只有几个小的缓冲区来保存必要的中间结果。使用就地处理方式是设计成像软件的关键因素，它允许三个任务在整个系统中只占用很少的内存。图3-9显示了对于凤凰号火星着陆探测器成像飞行软件，多缓冲区的处理方式和单缓冲区（即槽）的就地设计策略之间的区别。

我们采取就地单缓冲区处理方式，是为了满足图像处理需求，并且占用的内存仍然在预分配给照相机的内存范围之内，在早期设计时采用的一个权衡。虽然这种设计处理方式，确实满足内存需求，其不足之处是无法提供“undo”操作。如图3-10所示，如果在图像处理时出现错误，那么会导致或者整个图像丢失，或者是返回的是部分错乱的图像。

图　3-9：多数据缓冲区和单数据缓冲区

图　3-10：就地数据处理

就地数据处理方式的处理算法是应用于图像槽中的全程数据处理，包括读数据、处理数据及写回结果数据。有些算法，如像素纠正算法，并没有改变图像的几何信息，而只是基于已知一些“坏”像素的一个“上行”表，简单地对单个像素值进行修改（一个像素有可能是“坏”像素有两个原因：一是其没有相邻的像素那么敏感，二是它可能过于敏感）。在空间环境中，人们认为奇特的宇宙射线可能会穿透CCD上的一个像素，导致该像素有缺陷。其他的操作，比如图像子图定位，从原始图像中抽取一块区域，把该区域写回到槽中，然后相应地调整高度和宽度参数。抽样技术采用了一种数学均值计算的方式，通过把像素按4、9或16分组的方式，每个组生成的是单一的结果像素，从而减少图像大小。结果像素在图像大小上相应地缩小了1/2、1/3或者1/4；而最小化“梯度步长”的处理方式，通常是在当图像通过子抽样技术，只保留每第2、第3或者第4个像素，而抛弃其他像素来达到减小图像大小。这种操作也把修改后的结果数据写回到图像槽中，并相应地调整了其几何参数。

当一个成像任务完成了根据命令执行的处理操作，它就会向ICS发送一条消息（如之前所述），然后执行消息队列中的下一条命令。如果有可用的图像槽，该任务会执行获取另一个图像的处理操作。

图像压缩

正如由机器人完成的任务生成的数据非常宝贵，需要返回这些数据的通信带宽也是非常宝贵的。对于较小的图像，比如那些通过子图定位或者抽样操作，图片大小已经减少了，因此直接执行“下行”操作而不做压缩处理是可行的。更大的图像，比如全尺寸大小的SSI图像，“下行”操作会消耗很多带宽，因此在这种情况下，通常采用压缩方法来解决。

ICS采用像素映射和扩展，提供了两种压缩和减少图像大小的方式。对于某个特定的图片，采用哪种压缩或减少图像大小方式，主要依赖于图像需要达到的保真程度，高保真被认为是图像的一个必要方面。在一些情况下，每个像素8位就足够了；而在其他一些情况下，JPEG压缩本身造成的图像保真损失是可以接受的；而对于一些情况，图像需要保持尽可能高的保真，则可以采用无损压缩的方式。

在ICS内部，一台JPEG压缩器采用所有的整数算术计算和就地操作，提供所谓的“有损”压缩方式。JPEG被认为是有损的，因为其压缩过程丢失了部分图像数据。JPEG可以通过命令，对图像数据实现不同程度的压缩。最终代码是松散式地基于Mars'98使命的JPEG压缩器；虽然凤凰号火星着陆探测器的ICS的实现只采用了其部分原始代码。原始的JPEG压缩器使用的是浮点数乘以全尺寸大小的图像数组作为缓存，并采用动态内存分配方式。对于这种方式如何在飞行软件上正常工作，我仍然感到很困惑，不过它确实能够正常工作。在压缩代码中使用浮点数来表示像素数据，这也意味着对于每个图像，比起16位整数的原始图像表示方式，浮点数占用了其四倍的内存空间。

第二种压缩方式，也称为Rice无损压缩(Rce Lossless)或者Rice压缩，采用了由Jet Propulsion实验室的Robert Rice开发的一种算法。该Rice算法可以对图像数据实现几乎2：1的压缩效果，且没有数据损失。而JPEG算法在压缩过程中丢失了部分数据。Rice压缩方法也是在图像槽中就地对图像进行压缩。

两种无压缩的缩小图像大小技术或者采用查询表，把12位的像素值映射到8位的像素值，或者采用位缩小技术，对像素数据向右移动4位，生成一个每个像素8位的图像。JPEG和Rice压缩函数都接受12位或者8位的图像数据。

是否采用有损的JPEG压缩方式，通常需要对各种因素进行权衡，比如数据需要达到的精确度、可用的带宽大小、太空飞行器的主机有多少可用的“下行”存储空间、有多少时间可用于执行压缩（之前提过，RAD6000的最高速度是20MHz，因此压缩1M像素的图像数据可能需要超过1秒钟）。

当使用JPEG压缩方式时，采用的压缩比率是通过命令行参数决定，该命令行参数指定结果数据在最差情况下的压缩比率。换句话说，ICS不是指定一个“质量”因子（该因子通常表示JPEG压缩方法需要达到的压缩级别），而是使用一个可扩展因子，自动计算得出需要的压缩级别。这是基于全局“图像熵”(iage entropy)的“快查”(qick-look)分析。图像熵是图像“繁忙”程度的估计值，熵值越高的图像（在亮度上有很多细节和变化，如有很多鹅卵石的地面，阴影对比分明）将需要更高的压缩设置来满足最终的目标图像大小限制。对于熵值低的图像，比如飘着一些云彩的Martian天空，没有太多的细节和变化，因而只要较少的压缩量就能满足目标图像大小限制。

JPEG压缩算法扩展因子也用于把原始图像划分成不同的分段。这些分段一次一个地传给JPEG压缩器，其输出被写回到图像的槽。在执行“下行”操作之前，图像槽的最终结果是一组小的、自包含的JPEG图像，这些图像的全部大小不大于由命令行指定压缩比率的原始图像大小。

Rice压缩器包含它自己的嵌入式分段方法，而且其“下行”操作是简单地通过读出压缩数据，这些压缩数据通过小的分包方式，分包大小适合太空飞行器主机的闪存大小。查询表和减少位的方法的输出也是简单地读出闪存大小的包，用于“下行”操作。

“下行”或一切都从这里向下传输

太空飞行器的飞行软件处理过程的最后一步是传送图像给“下行”管理器。一些科学设备可以简单地把数据传给“下行”程序，完成处理；但是，由于海量的数据以及采用分包的方式，结果是ICS本身需要完成很多的预处理操作。

对于JPEG数据，这意味着需要分别单独处理每次的图像压缩分段。一个序列的第一个和最后一个分段通常包含一个全尺寸大小的图像数据头。中间分段的头数据大小较小，包含一个图像ID号和序列号。由于每个分段都是从图像槽读取数据，所以采用了头数据。采用滑动窗口的方式向外读取数据，使得分段在以闪存大小分包组合时，可以对分段执行端对端(ed-to-end)地打包。反过来，它允许“下行”处理器最大化使用临时闪存存储空间，因为如果一部分图像对应的分段的熵值很小，那么一些压缩的分段可能比其他的分段小。实际上，压缩的分段大小区别很大是很常见的，因此对这些分段执行端对端地打包避免了浪费飞船上任何的闪存资源。

因为数据是由唯一识别的分段组成的，“下行”过程丢失一个分包并不会导致整个图像毫无用处。在JPL，地面上的重构和解压缩软件能够识别出丢失了哪个分段，然后简单地把丢失的那部分图像以全0像素数据来填充。如果后期获取到丢失的那部分数据（这是可能的，因为考虑到数据的“下行”过程是非常曲折的），那么这部分数据就可以用来填充之前丢失的那部分像素数据。

一旦发送数据给“下行”处理器，ICS就完成任务，并且释放该图像数据的插槽。这整个过程（从图像曝光到完成发送给“下行”程序）需要3～10分钟，这依赖于CPU速度和其他额外必定发生的成像处理，如自动曝光和寻找光线（这两者本身就非常复杂，所以我们不在这里讨论）。

结束语

该设备软件所完成的任务要远远多于照相和处理图像数据。它也管理SSI的三（自由）度运动控制，以及RAC中的聚焦和视角。RAC还支持多色的LED，包括红、蓝和绿，照亮任何可能在机械臂范围内的视角，然后生成彩色图片。SSI和RAC都结合了活动的热控制，这或者是通过使用特定的加热器，或者是故意拖延一个步进电机来达到自我加热。所有这些操作都包含错误检查和容错恢复。总之，该设备软件任务非常“繁忙”。如果让我重新设计该软件，我希望改变的主要方面是，照相机使用的是它们自己的嵌入式处理器，而不是依赖于太空飞行器的CPU。这样会使所有的操作都变简单得多。除了这点，我总觉得对于每个设备任务，“挤进”了太多的操作。换句话说，热控制对于每个照相机本应该是一个独立的任务。这样就可以极大地减少每个任务的复杂度，虽然其代价是增加全局任务间通信的复杂度。然而，在刚开始时，没有足够的理由来这么做，因此当有一些新的加热需求需要对设计做出调整时，该设计已经很固定了（没有冻结，只是非常不灵活）。

最后，对于选择“心跳”检查方式，我真的是很不支持。我原先不知道命令消息队列是为了进行“心跳”检查。这种“心跳”检查方式对设备任务提出了需求，要求每个任务能够放弃任何它们正在做的事，通过“ping”消息定时检查命令消息队列。我相信更好的方式应该是让设备注册一个太空飞行器飞行软件回调函数，该函数可通过异步方式，检查连续更新的计数器变量的值。如果在一定时间后，该值没有更新，则该设备任务很可能已经崩溃。当时确实对这种实现方式有很多争议，最后采用ping消息的方式，仅仅是因为这是一种通用的方式，而且已有的测试系统就是这么设计处理的。因此，即使系统不是为了需要花费好几分钟来处理的海量图像数据而设计的，但是该设计方式也不会发生改变。

凤凰号火星着陆探测器的SSI和RAC/OM成像软件在设计、实现和测试上有很多工作要做，而且在最后，它完成了在其使命的整个生命周期中应该完成的任务。图3-11是该太空飞行器在火星上的第一天，即Sol 1，从SSI返回的第一批图像之一(S001EDN896308958_10D28R1M1）。

图　3-11：从Sol 1的SSI返回的图像（图像来源：NASA/JPL/Arizona大学）

更多关于凤凰号火星着陆探测器

如果你想对凤凰号火星着陆探测器的使命了解更多，可以查看以下几个主要来源：

Arizona大学的Phoenix网站：·http：//phoenix.lpl.arizona.edu

Jet Propulsion Laboratory(JPL实验室）的Phoenix网站：·http：//www.jpl.nasa.gov/news/phoenix/main.php

NASA的Phoenix网站：·http：//www.nasa.gov/mission_pages/phoenix/main/index.html

在JPL,MIPL为各种任务做了很多图像处理，你可以通过以下站点了解更多：

JPL的Mission图像处理实验室：·http：//www-mipl.jpl.nasa.gov/

如果你想对RAD6000 CPU、图像处理或嵌入式系统了解更多，可以查阅Wikipedia:http：//www.wikipedia.org。

第4章　PNUTShell中的云存储设计

Brian F. Cooper、Raghu Ramakrishnan和Utkarsh Srivastava

简介

雅虎公司运营着世界上最受欢迎的多个网站，每月的访问用户有5亿之多。这些Web站点的后台数据系统存储了种类庞杂的数据，如用户信息、照片、餐馆点评、博客博文以及大量其他数据。雅虎开发部署了多套成熟、稳定的数据库体系来支持它的站点，同时这些架构也为用户提供低延迟的数据访问方式来快速加载页面。

但是，这些系统存在着一些严重的局限性。首先，系统扩容困难。为了进行系统扩容，通常需要数月的时间来规划和重组数据，并且在扩容过程中，相关应用的服务质量会受到影响。此外，有些系统可支持的扩展性具有上限值，即使增加硬件支持也无济于事。其次，很多早期设计的系统，其设计思想就是基于单一的数据中心。雅虎已经发展为全球性的品牌，其广泛的基础用户遍及全世界。为了提供良好的用户体验，系统需要复制数据到离用户近的数据中心，这样当用户访问时，页面就可以快速加载。由于数据库系统本身并没有将全球性数据复制作为内建的功能提供，每个应用需要自己构建数据复制，这导致复杂的应用逻辑和薄弱的数据基础。部署一个大规模的、在不同地理空间上都存在数据副本的数据库架构需要很大投入，因此，在这样的架构上，很难快速地开发新的应用或在已有的应用上增加新的特性。

PNUTS系统致力于支持雅虎的Web站点和应用平台，并解决上述局限(Coper等2008）。在设计上，它通过云存储方式，可以有效地处理租户应用(tnant applications)中复杂的读写工作，并且支持本地全球数据复制。和很多其他的分布式系统类似，PNUTS通过水平数据分区的方式，将数据分布到一组存储服务器阵列上，从而实现高性能和可扩展性。该系统的核心不是复杂的分析和决策支持工作，而是从最初设计就把两个属性作为首要特征：

向外扩展(sale-out)

系统通过分区将数据分布式存储到多台服务器上，扩充系统容量犹如增加新的服务器一样容易，且系统可以平滑地将负载分配到新的服务器上。

地理空间上的数据复制(go-replication)

在PNUTS系统中，数据可以自动地复制到全世界。一旦开发者告诉系统在哪个区域[1](clos，即数据中心）复制数据，系统本身就会自动地完成诸多细节操作，包括故障处理（如机器、链接甚至整个区域）的一些细节操作。

PNUTS系统还设置了一些其他的目标，尤其是，我们希望应用开发者能够专注于应用逻辑本身，而不是操作数据库的具体细节。因此该系统对数据库采取托管方式，并为开发者提供一个简洁易用的API来存储和访问这些数据，而不需要他们自己对一大堆参数进行调优。由于系统采取托管方式，在功能实现上，它能够尽可能达到自我维护。

尽管这些目标都很重要，而且构建一个能够向外扩展且易于全球性数据复制的数据库系统是对公司最有吸引力和直接价值的。随着开始系统设计，我们清楚地发现很多长期普及和使用的数据库系统机制需要重新考虑(Rmakrishnan和Gehrke 2002）。

实现向外扩展和地理空间上数据复制的核心思想是只进行简单廉价的同步操作，而在后台异步式地实现所有代价高的操作。举个例子，当一个加州的用户想用一个关键字来标注一张照片，她必定不愿意等待该系统把该标签提交到在新加坡的标签数据库副本（从加州到新加坡的网络延迟可能高达1秒）。然而，她还是希望她的新加坡朋友能够看见该标签，因此需要在后台对新加坡的数据库副本完成快速（几秒内）、可靠的异步更新。

另一个如何发挥异步作用的例子是诸如聚集查询和连接操作，它们通常需要在多台服务器上执行数据操作。随着系统规模扩大、负荷加重，这些服务器出现响应变慢或者崩溃的概率也随之增加，因此会增加请求延迟。为了解决该问题，我们可以维护实体化视图(mterialized views)来重组这些基础数据，这样一些预先设定的复杂查询可以通过只访问一台服务器来获得响应。和数据库副本相似，若同步更新每个视图，在执行写操作时非常缓慢，因此我们的策略是采取异步方式更新这些视图。

本章的剩余部分会深入地剖析重点研究向外扩展和地理空间上的数据复制这两大首要特征的涵义。我们将通过一个例子来阐明主要问题、说明基本方法、讨论一些问题及扩展；然后，将PNUTS和其他方法作比较。本章的讨论重点是设计理念，而不是系统架构或实现细节，并且为了在全局方案中突出某些选择，本章涵盖了一些当前系统版本不包含的特性。

[1]区域设施，或者称数据中心。雅虎在全世界运营着大量的数据中心。

更新数据

用户和Web站点的交互操作会导致数据库的不断更新。因此，我们要解决的首要挑战是为每个更新提供良好的性能和操作一致性的前提下，如何支持大量的更新操作。

面临挑战

假设我们要构建一个社交网络站点，系统中的每个用户都有一条用户信息记录，该记录包含用户名、爱好等。用户Alice可能在全世界都有朋友，她的朋友想看她的个人信息，这些读取请求操作有严格的低延迟要求。为此，系统必须保证Alice的用户信息记录（其他人的也类似）在全球都有副本，这样她的朋友们就可以访问该记录的本地副本。现在，社交网络的一个特征是用户可以通过自由的文本定制来更新自己的状态。比如，Alice可能想修改她的状态为“忙碌，电话中”(Bsy on the phone)，过后又修改为“有空，要聊天吗？”(Of the phone,anybody wanna chat?）。当Alice更改她的状态时，系统需要把这些更改操作写入数据库中她的用户信息记录，这样她的朋友们才可以看到。用户信息记录表见表4-1。注意，为了支持Web应用的不断升级，必须提供一个灵活的数据库模式和松散耦合的数据组织方式，而不是每条记录的每个字段都有相应值，并且增加新的字段代价必须小。

我们该如何更新Alice的用户信息记录？从标准数据库角度看，为了实现该更新操作的原子性，我们需要执行以下步骤：打开一个事务；对所有的副本执行写操作；向所有的副本发送提交信息并关闭该事务。这种方法和标准数据库的ACID[1]模式一致，保证所有的副本都能够正确地更新到新状态。即使是非ACID模式的数据库，如G公司的BigTable(Chang等2006），采用了类似的方法来同步更新数据的所有副本。但是，当存在地理空间上的数据复制时，该方法效率很低。当Alice输入她的状态信息并点击“OK”，由于需要等待分散在各地的数据中心提交该事务，她可能需要等待很长时间才能加载响应页面。此外，为了保证原子性，在事务处理过程中，系统需要对Alice的状态加排他锁，这意味着其他用户可能长时间无法看到Alice的状态。

由于当地理空间上存在不同副本时，原子事务的代价很高，很多Web数据库采用了“尽最大努力”的方式(bst-effort approach)：先将更新写到一份数据中，然后异步地将更新操作传播(popagate)到其他的副本。模拟事务操作时，不执行加锁及验证。这种方法，犹如其名——“尽最大努力”，存在很多困难。即使系统能够保证在所有的副本上执行更新，也不能保证数据库的不同副本最终都保持一致性。比如，Alice首先将她的状态更新为“Busy”，这样在美国西海岸(wst coast)的数据中心需要执行写操作，如表4-2所示。

然后，Alice把她的状态更新为“Off the phone”，但由于网络干扰，该更新操作误在美国东海岸(est coast)的数据副本上执行，如表4-3所示。

由于更新传播是异步的，一个可能的事件序列如下：在美国东海岸的数据副本上首先执行“Off the phone”更新操作，然后执行“Busy”更新操作。于是，通过有线传播，更新操作如表4-4所示。

在美国东海岸，“Busy”的状态覆盖了“Off the phone”状态；而在美国西海岸，“Off the phone”状态覆盖了“Busy”状态，这导致如表4-5所示的状态不一致性。

根据读取数据副本的不同，Alice的好友看到的Alice的状态也是不同的，而且该不一致性会一直存在，直到Alice再次改变她的状态。

为了解决上述问题，一些Web规模(wb-scale)的数据存储实现了最终一致性策(eentual consistency)，如上述的不正常现象可能暂时发生，数据库最终将会解决该不一致性，并保证所有的数据副本都有相同值。该策略是某些系统的核心，如Amazon的Web服务S3。最终一致性策略通常使用如闲话(gssip)和反熵(ati-entropy)的技术来实现。但是，即使数据库最终会收敛到某个值，也很难预测它最终的收敛结果。因为没有全局时钟来序列化所有的更新操作，数据库难以识别Alice的最终状态更新是“Busy”还是“Off the phone”，因此最后有可能会收敛为“Busy”状态。这样，正当Alice准备和好友聊天，她的所有好友都认为她是忙碌的，且在Alice再次更新她的状态前，这种不正常现象会一直存在。

我们的方法

强一致性策略（如ACID事务）存在扩展上的局限性，弱一致性策略（如尽最大努力策略或最终一致性策略）可能存在不正常现象，我们提出介于二者之间的一种策略，即时间轴一致(tmeline consistency)策略：所有的数据副本将通过相同的时间轴执行更新操作，而且该更新和数据库上执行的更新顺序相同。该时间轴如图4-1所示。因此，数据库将在所有的数据副本上收敛为相同的结果值，且该值是该应用在数据库上执行的最近一次更新操作之后的值。

图　4-1：Alice状态的更新操作时间轴

时间轴一致性是这样实现的：由一台主备份服务器(mster copy，以下简称主备份）执行所有的更新操作，然后将变化异步传播到其他备份服务器来实现的。该主备份序列化所有的更新操作，并确保为每个更新指定一个序列号。即使在更新的异步传播中存在瞬间的故障或误序，该序列号的次序是和需要在所有副本上执行的更新次序一致的。我们选择每条记录一个主备份是因为雅虎的很多应用依赖于同一张数据表，该表中不同的记录对应不同的用户，每个用户都有不同的使用模式(uage patterns)。当然，有可能为主备份性(mstership)选择其他的粒度，比如每个记录分区（如基于关键字）有一个主备份。

即使是在同一张表，不同记录的主备份也可能分布在不同的服务器上。在以上例子中，Alice住在美国西海岸，并在美国西海岸的数据副本有一条主备份记录；而她的好友Bob住在新加坡，其主备份记录是在亚洲地区的副本。记录的主备份性是作为记录的元数据域(mtadata field)存储在该记录本身，如表4-6所示。

主备份似乎和我们的原则——只应该同步执行代价低的操作不符。如果Alice去纽约旅行，并在那里更新她的状态，由于她的个人信息记录的主备份在美国西海岸，她必须等待系统把她的更新操作提交到美国西海岸；这种高延迟的跨州更新操作正是我们应该尽量避免的。由于用户转变使用模式（如Alice的旅行），这种跨数据中心的写操作确实会偶尔发生，但几率很小。我们分析了对雅虎的用户数据库的更新操作，发现记录更新在包含主备份的数据中心上执行的概率占85%。当然，Alice可能迁移到美国东海岸或者欧洲，由于其记录的主备份仍然在美国西海岸，她执行的写操作就不再是在本地执行的。我们的系统会追踪一条记录更新的源起，为了保证绝大部分的写操作还是在本地执行，系统会根据长期性访问模式(acess pattern)的转变来修改主备份性。（我们将在下一章更详细地讨论主备份性。）

当应用读取一条记录，它通常会读取该记录的本地副本。除非该副本标记为主备份，否则其数据可能已经过期了。该应用可以从时间轴识别其记录实例是和某个版本的记录一致，但是它无法从记录本身识别该记录是否为最新版本。如果该应用要求记录必须是最新版本，系统允许它发送最新读(u-to-date read)请求操作，该请求会被提交到主备份来获取记录的最新备份。执行最新读操作代价很高，而通常情况下读本地副本（可能数据已经过期）的代价很低，这和系统的设计原则也是一致的。幸运的是，Web应用通常对数据时效性要求较低，能够接受过期数据。如果Alice更新了她的状态，但她的好友Bob没有马上看到，而是过后不久看到其新状态，也是可以接受的。

应用可以执行的另一类读操作是“临界读”(citical read)，可以保证从用户的角度来讲，数据只随时间向前移动。假设有这样的情况，Alice改变了她的头像（表示一个用户的图片），Bob可能会看Alice的信息页面（导致对数据库的一次读操作），并看到了Alice的新头像。然后，Bob可能会刷新该页面，但由于网络问题，该刷新操作被重定向到没有执行Alice头像更新的副本上执行。结果是Bob将看到的数据比他刚看到的版本更老。为了避免应用程序执行这些操作引起的这种不正常现象，数据库为读操作调用返回记录时会附上该记录的一个版本号。该版本号可以保存在Bob的会话状态(sssion state)或者其浏览器cookie中。当Bob刷新Alice的信息页面时，他上次读到的版本号将和本次请求一同发送，数据库会保证返回记录的版本不会老于该版本。这可能需要把版本号和请求提交给主备份。指定版本号的读操作被称为“临界读”，任何版本号等于指定版本号或者更新的副本，其返回的结果数据都是可以接受的。对于先更新再读取数据库的用户，该技巧尤其有用。以Alice自己为例，当她更新完头像后，如果有任何页面还显示其旧头像，她一定会感到很困惑。因此，当她执行了某个动作需要更新数据库（如更新头像）时，应用程序可以使用“临界读”机制来保证页面都不会显示她的旧数据。

我们也支持“测试并设置”(tst-and-set)操作，该操作使得写操作必须依赖于当前读操作的版本和之前看到的版本（其版本号作为参数传递给“测试并设置”请求）相同。从传统的数据库系统考虑，这是ACID事务的一个特例，它限于单记录，使用的是乐观并发控制策略(otimistic concurrency control)。

主备份性的其他方面

我们采用了各种技术来保证即使在工作负荷变化、失效情况下，都能够低延迟地、平滑地执行读写操作。

举个例子，正如我们之前所述，该系统实现的是记录级(rcord-level)的主备份性。如果一条记录的很多写操作来源于某个数据中心而不是当前的主备份，该记录的主备份性将被及时地转移到该数据中心，且随后的写操作将在该数据中心本地执行。此外，转移主备份性代价很低且系统可以自动执行，因而系统可以快速地适应工作负荷的变化。

系统还实现了即使在存储单元失效的情况下，还能够不间断地执行读写操作的机制。当某个存储单元失效，会（手工或自动）执行对该存储单元的覆盖操作(oerride)，并指示另一个数据中心取代失效的存储单元，（为以该存储单元作为主备份的记录）执行写操作。系统会采取措施（细节不在这里赘述），确保根据在失效的存储单元已执行的更新操作执行顺序覆盖操作。这样，当其他的数据中心开始取代失效的存储单元执行更新操作时，还能够保证时间轴一致性。

在PNUTS中，所有读写请求会通过路由层来定位到一条记录相应的数据备份（可能是主备份）。该间接层(lvel of indirection)是提供不间断的系统可用性(uinterrupted system availability)的关键。即使当一个存储单元失效而其数据恢复到另一个存储单元，或者根据使用模式的变化转移记录的主备份，这些变化对于上层应用也是透明的，应用依然可以连接到路由器，享受不间断的系统可用性，其请求可以无缝地路由到适当的数据中心。

支持有序数据

PNUTS系统架构上同时支持散列分区和范围分区的数据。我们称散列版的数据库为YDHT，即雅虎分布式散列表(Yhoo!Distributed Hash Table)；称有序表版的数据库为YDOT，即雅虎分布式有序表(Yhoo!Distributed Ordered Table)。绝大多数系统不关心数据的组织方式。但是，有个重要的方面对物理数据的组织方式是敏感的。尤其是采用散列表方式组织的数据，往往可以把负载很均衡地分散到不同的服务器。如果数据是有序的，部分更频繁访问的关键字空间(ky space)将会出现“热点”(htpot)效应。比如，如果状态更新是通过时间排序，那么最近的更新是用户最感兴趣的，则包含在该段时间范围末的数据分区的服务器负荷会最重。在不影响系统向外扩展下，我们不允许“热点”效应持续。

逻辑有序的数据实际上是存储在物理相连的记录分区上，但是当分区无序组织时，这些数据就可能存储在多台物理服务器上。我们可以根据负载，动态移动分区来解决“热点”效应问题。如果一台服务器上有多个热分区(ht partition)，可以把这些分区移到负荷较低的服务器上。此外，我们还可以动态划分分区，这样某个热分区上的负载可以分散到几台服务器上[2]。这种在存储单元间移动和划分分区的策略和先前提到的改变一条记录的主备份性机制是不同的：对于前者，改变记录的主备份会导致在一台服务器发起的更新延迟，但通常不影响给定记录分区记录的累积读写操作负荷。需要划分和移动分区的一个特例是当我们想要更新或插入大量的记录。在这种情况下，如果不采取特殊措施，就会发生只向几台服务器发送大批量更新操作，从而导致服务器负载不均衡。因此，需要理解更新操作是如何分布在关键字空间(ky space)，必要的时候，可以为后期的一连串更新操作预先划分和移动分区(Slberstein等2008）。

我们把应用和物理数据组织细节相分离。对于单记录的读写操作，使用路由层可以使应用免于受分区移动和划分的影响。对于范围扫描，我们需要提供进一步抽象：假设我们需要扫描年龄在21～30岁的所有注册用户，执行该查询可能意味着在一台服务器上需要扫描包含几千条记录的数据分区，在另一台服务器上扫描另一个分区等。每个分区有几千条记录，由于它们在磁盘上是顺序存储的，因此可以快速扫描。我们希望这些分区的移动和划分对于应用是透明的。解决该问题的一个巧妙方式是借鉴迭代器(ierator)的思想：当应用执行扫描时，我们返回一组记录，然后准备好下一组记录，触发应用继续扫描。因此，当应用执行完一次批处理，开始请求更多的数据时，可以把这些请求转换到包含下一组记录分区的新存储服务器上执行。

牺牲一致性，换取可用性

时间轴一致性处理一般问题效率高，且语义简洁，但它也并不完美。有时，整个数据中心崩溃（如断电）或者不可用（如断网），那么以该数据中心作为主备份的记录将不可写。这种情况暴露了已有的在一致性、可用性和分区容忍性(prtition tolerance)上的平衡问题(tade-off)：在所有情况下，这三者属性只能保证其中的两个。由于数据库是全球性的，分区必然存在且不会造成大的影响，因此实际上我们只需要处理一致性和可用性间的平衡。如果一个数据中心断网了，可能有些新的更新操作还没有传播到其他的数据副本，我们可以通过两种方式保证一致性：一是为了保持一致性，在该数据中心可用前，不允许执行数据更新；二是为了保持可用性，要违背时间轴一致性原则，允许一些更新操作应用到非主备份的记录。

系统为应用提供了基于表级粒度(pr-table basis)来选择相应策略。如果对于某个特定的表，可用性对于该应用的优先级比一致性高，那么当一个数据中心断网时，系统会临时转换该表中任何不可用记录的主备份性到其他的数据中心。该决策牺牲时间轴一致性，有效地支持了可用性，如图4-2中的例子所示。当恢复完一个不可用的数据中心，系统会自动协调任何有冲突的更新记录，并通知应用这些冲突信息。该协调保证了即使不能满足时间轴一致性，数据库在任何数据备份上都能收敛到相同的值。另一方面，对于应用而言，如果一致性优先级比可用性高，则会保持时间轴一致性而不会传递主备份性，这样，一些写操作执行会失败。

对于某些操作，一致性和可用性间的平衡较易于把握。例如，假设有个关于投票的应用，用户可以对各种问题进行投票（如“你最喜欢的颜色是什么？”），投票结果作为计数存储在数据库上。计数操作（如增加）是可交换的(cmmutative)，因此，在不破坏时间轴一致性的情况下，甚至可以应用于非主备份副本上。一般来说，复制机制可以在不同副本间传递记录的新版本，但是对于可交换操作，我们实际上是传递操作（如增加）而不是结果值。那么，主备份在任何时间接收到操作（不论是在正常的数据操作期间还是在某个数据中心失效后），它都可以执行该操作，而不需要担心操作是否乱序。该策略的一个局限是可交换和非可交换的操作不可以混合：因为我们无法知道如何合理地对一个计数值的增加操作和重写操作进行排序，所以当记录插入后，禁止在任何时刻对其计数器设值。

图　4-2：西部数据中心断网，更新时间轴产生交叉(frks)

PNUTS的另一个扩展是允许对多条记录执行更新操作。我们主要研究基于记录级的时间轴一致性的原因是很多Web应用负载涉及在某一时刻对单条记录的更新操作。但是，有时更新多条记录更合适。例如，在社会网络应用中，可能存在双向好友链接：如果Alice和Bob是好友，那么Alice会出现在Bob的好友列表，同样，Bob也会出现在Alice的好友列表。因此，当Alice和Bob成为好友时，我们需要更新两条记录。因为系统不提供ACID事务，所以无法保证该更新是原子性的。但是，我们可以采取绑定式写操作(bndled writes)：当对数据库执行一次调用时，应用可以请求两个写操作，数据库会保证两个写操作最终都会执行。为了实现这点，系统对请求的写操作写日志，如果初始执行失败，系统会重试这两个写操作，直到它们都成功。该方法保证了记录级的时间轴一致性，由于重试可以异步执行，这也保证了系统的性能。

总之，时间轴一致性为记录更新如何传播提供了简单的语义支持(smantics)，以及为应用如何牺牲读延迟来换取流通性(crrency)提供了灵活性。但是，它并不支持一般的ACID事务，尤其是执行多条记录的读写操作事务。

[1]事务具有原子性、一致性、隔离性（不受其他并发事务影响）和持久性。

[2]细心的读者可能会发现，如果所有的更新影响包含该时间范围内的分区，划分该分区并不能解决问题，需要一些其他方法，如通过组合关键字如用户名和时间进行排序。

复杂查询

随着Web应用更加复杂有趣，这些应用需要以新的不同方式从数据库中检索和组合信息。下一步，我们将探索如何在大规模系统上支持这些查询。

面临挑战

系统对只需要一条或者几条记录的查询做了优化。尤其是，系统可以通过主键来查询记录；只要知道Alice的用户名，就可以直接确定哪个分区包含她的信息记录，加载页面时读取该记录。另外，系统可以采用散列分区表或范围分区表来存储数据。对于范围分区表，可以通过主键的排序范围做范围扫描。例如，系统可能以每个连接一条记录的方式存储Alice的好友列表，其中每个连接的主键是Alice和她好友的用户ID对（见表4-7）。

对于范围分区表，所有以“Alice”为前缀的记录会聚集在一起，因此，只需要小范围扫描就可以检索到这些相关记录。

现在假设我们需要给社交网络站点添加另一个功能。在该站点中，用户可以发布照片，对其他人的照片发表评论。Alice可能想对Bob、Charles和Dave的照片发表评论。当页面显示一张照片，我们希望它也能够显示和该照片相关的一组用户评论，同时还想为Alice显示她对其他人的照片做出的一组评论。评论表的主键设定为(PotoID,CommentID)，并以有序的YDOT表的方式存储（见表4-8），因此对相同照片的所有评论会聚集在一起，可以通过范围查询来检索。

系统如何收集Alice做出的所有评论？它需要对Alice的信息记录（以username为主键）和评论记录（以username做外键）做连接查询。由于向外扩展的系统架构，数据分区跨越很多服务器，因此连接操作需要访问很多服务器。该操作一方面需要连接多台服务器，另一方面一条查询占用大量的服务器资源（导致其他的请求响应变慢），因此代价很高，增加了请求延迟。

在向外扩展的系统中，另一种代价高的操作是聚集(goup-by-aggregate)查询。假定用户指定了爱好，系统想要计算每种爱好的用户数，从而可以向Alice展示哪种爱好是最受欢迎的。这种查询需要扫描所有的数据并保存计数值。这种表扫描操作会给系统带来巨大的负荷，而且不能够同步执行，因此Alice的页面需要非常长的时间才能加载。

这些例子说明了虽然单点查询和范围扫描可以快速执行，但是代价更高的连接和聚集查询操作不能够同步执行。

我们的方法

处理代价高的操作，核心原则是异步执行，但是代价高的查询实际上不能够以这种方式处理；我们不希望需要Alice不断返回检查获取其所有评论的异步查询是否已经执行完成。

实例化视图可以通过异步式构建(Aarawal等2009），当Alice登录时，她可以快速（同步）地查询该视图[1]。虽然和基本表的数据相比，异步式构建的视图可能已经过期，但由于应用本来就需要处理过期的副本，所以处理过期的视图数据通常是可以接受的。事实上，我们把实例化视图作为一种特殊的副本，该副本可以复制数据和转换数据。通过与更新副本相同的机制来更新视图，系统保证了视图和复制的基本表数据，有相似的可靠性和一致性保证，而不需要再设计和实现另一个机制。

虽然视图是在后台维护，但是我们还是希望尽量减少它的代价。如果维护视图占用很多系统资源，或者它会影响同步的读写请求（增加每个查询的延迟），或者会被分配很少的资源，运行很慢，这样视图数据会严重过期而导致视图可能毫无用处。因此，我们必须探索使视图维护高效的策略。比如对于先例，系统想给Alice显示她对其他人的照片做出的所有评论。系统将创建一个实例化视图，该视图通过外键（评论员的username)而不是主键对评论数据进行重组聚合。那么，Alice做出的所有评论会聚合在一起，我们还可以给视图添加Alice的信息记录，以她的username为主键，因此Alice的信息和评论可以聚合在一起。主键/外键连接操作如同扫描并连接以“Alice”为前缀的视图记录一样简单。结果如表4-9所示。

注意，我们并没有在视图中做信息和评论记录的预连接操作，只是通过定位需要连接的记录，使得连接的维护代价低。当需要对基本表记录做更新操作时，我们只需要更新一条视图记录，即使该视图将和很多其他记录做连接操作。

我们如何在同一个表存储信息和评论记录？在传统的数据库，因为两种记录有不同的模式(shema)，所以这样会很难存储。但是，PNUTS的一个核心特征是能够表示灵活的数据库模式。相同表中不同的记录可以有不同的属性组。这个特性对于Web应用特别有用，因为Web数据通常稀疏松散，一个销售商品的数据库根据商品的种类，会有不同的属性（如color、weight、RAM和flavor)。灵活的数据模式对于实现实例化视图连接也是非常重要的，这样我们可以定位来自不同表的连接记录。

异步式视图方法也有助于响应其他类型的查询。聚集查询可以通过实例化视图预先分组(pe-group)，甚至可以预先聚合(pe-ag gregate)数据，因此可以有效地响应查询。甚至有诸如选择非主键属性的“简单”查询，这种查询通过实例化视图方式处理最有效。假设需要查询住在加州Sunnyvale的所有用户。因为用户表的主键是username，该查询通常需要执行代价高的表扫描操作。但是，我们可以采用实例化视图在用户表的“location”域构建索引，把该索引存储到有序的YDOT表，然后在“Sunnyvale,California”的索引记录上执行范围扫描来获取结果，从而响应查询请求（见表4-10）。

如其他系统中的实例化视图，只要我们事先知道需要什么查询，就可以有效地创建视图。对于提供Web服务的应用，查询通常是根据事先确定的模板，在运行时指定参数（如地理位置或用户名）。因此，应用开发者可以预先知道哪些查询很复杂，需要实例化视图。为了即时请求存储在PNUTS中的数据，开发者需要使用PNUTS插件从系统中获取数据，并把数据保存到运行着Hadoop的计算网格。Hadoop是MapReduce的开源实现。当有一些不同的机制来处理复杂的查询，实现有助于有效执行查询的查询计划器是非常有用的。查询计划器可以减轻应用开发者的负担，它可以写声明性查询(dclarative queries)，而不需要考虑这些查询将如何执行。但是，对于如PNUTS的大规模系统，有效的查询计划器需要复杂的统计收集、负载监测、网络监测和一系列其他机制来保证查询计划器对于系统的所有可能瓶颈有足够的信息来制定最有效的查询计划。

[1]在PNUTS系统的生产版本中，还不包含实例化视图。

和其他系统的比较

当我们开始思考PNUTS时，G公司和Amazon已经宣布了两个大规模可扩展性数据库系统，微软也将对外宣布。当我们开始设计时，我们仔细研究了这些系统，思考它们的思想是否对我们有用。这些系统的一些想法对我们有影响，但我们还是决定构建一个新的系统，其架构在很多方面和这些系统不同。我们现在先来研究这些系统，并讨论为什么决定不采用它们的设计理念。

G公司的BigTable

BigTable(Chang等2006）是为了支持G公司的很多Web应用而设计的系统。该系统基于水平方式将“大表”(bg table)切分为很多小表(tblet)，并把这些小表分散到不同的服务器上。支持可扩展性的基本方法以及一些其他特性，比如灵活的模式和有序存储，与我们采用的方法很相似。但是，我们的设计和BigTable在某些方面有区别。

首要区别是数据复制方式不同。BigTable是构建在G公司的文件系统GFS上(Gemawat等2003）。GFS通过同步更新三个数据备份到三台不同的服务器上的方式来处理数据备份。这种方法在一个数据中心可以工作良好，因为一个数据中心的服务器间的延迟很短。但是，在三个不同的、分散的数据中心同步更新三台服务器代价很高。Alice可能需要等待很长的时间，然后其状态才能更新，尤其是如果她的好友通过带宽很低的网络来访问数据中心。为了支持跨数据中心的数据复制，我们提出了时间轴一致性模式，以及与主备份性、负载平衡和失效处理相关的机制。

BigTable和GFS在数据库服务器和文件系统间存在分离，而PNUTS不采用该分离。GFS最初是为需要对大文件进行扫描的应用设计和优化的（如MapReduce)。BigTable通过保留每个记录的版本历史和GFS关联，并把文件以SSTables的文件格式压缩来减少存储空间。这意味着对于记录的读和更新操作，数据需要采用这种压缩格式解码和编码。此外，GFS的特征是面向扫描，这有助于BigTable处理面向列的扫描应用（如“返回所有用户的所有地理位置信息”）。与之相反，我们的主要应用是需要读和更新一个小范围记录内的一条记录的一个版本。因此，我们的策略是把数据作为完整的记录，以B树形式组织保存在磁盘上。这种方法对快速定位、就地更新由主键识别的各条记录做了优化。

PNUTS和BigTable在其他方面还有不同。比如，PNUTS对于一个应用支持多个表，而不是一个大表，且对于散列表和有序表都支持。MegaStore是继BigTable之后(Frman等，2008）添加了事务、索引和更丰富的API，但仍然遵循BigTable中的基本架构原则。

Amazon的Dynamo

Dynamo(DeCandia等2007）是Amazon最近构建的应用于大规模数据负荷的系统，是和我们的高可用性、大规模数据存储的目标最相近的一个系统(Dnamo中称记录为对象）。Dynamo通过允许应用向任何副本写数据提供了写操作可用性，它通过Gossip协议（稍后会细述）向其他副本延缓传播(lzily propagate)这些更新操作。

为了解决网速慢和易于失效问题，Dynamo和PNUTS一致，它们都采用了延缓传播更新策略(lzily propagate updates)。但是，PNUTS的复制策略与之有很大区别。在Gossip（闲话）协议中，更新操作会传播到随机选定的副本，该副本又把该操作继续传播到其他随机选定的副本。这种随机性是该协议提供的概率性保证的基础，它保证了绝大多数副本可以相对快速更新。然而，在我们的应用环境中，随机性必然是次优的。比如Alice执行状态更新操作，其数据是在美国西海岸的数据中心。若采用Gossip方式，该操作可能随机传播到新加坡的副本，然后又随机传播到Texas的副本，接着又随机传播到东京的副本。该更新操作在传播地域上跨越了三次太平洋，如果采用一种更确定性方式(dterministic approach)，只需要传播该操作（和其他更新操作）一次，可以节约紧缺的跨太平洋主干带宽。此外，Gossip要求传播数据的副本预先知道在其他哪个数据中心的哪台服务器上有副本，因此很难为了负载均衡或数据恢复，在不同服务器间传送数据。

PNUTS和Dynamo的另一个关键区别是一致性协议。Gossip协议的本质决定了它是最终一致性模型：所有的数据副本最终将保持一致，但是在更新传播期间，副本可能是不一致的。尤其是，副本可能会存在“无效”状态。举个例子，Alice把她的状态从“Sleeping”改为“Busy”，再把地理位置信息从“Home”改为“Work”。由于更新的次序，从Alice的角度出发，唯一有效的记录状态是(Seeping,Home)、(Bsy,Home)和(Bsy,Work)。在最终一致性原则下，如果两个更新是在不同的数据副本上执行的，有些副本可能会首先接收到改为“Work”的更新，这样，这些副本将临时显示(Seeping,Work)状态。如果Alice的老板看到这种状态，Alice可能就有麻烦了。对依赖于在一条记录上以合理的次序执行多次更新的应用，它需要比最终一致性更严格的执行次序保证。虽然时间轴一致性模式允许副本已经过期，但即使是过期的副本也应该是和某个合理的更新次序一致的版本。

PNUTS和Dynamo还有很多其他区别：Dynamo只支持散列表，不支持有序表，PNUTS二者都支持；和Dynamo相比，PNUTS支持更灵活的数据到服务器的映射机制，可以提高负载均衡和数据恢复（尤其是对于有序表，可能存在不可预测的“热点”）。除了DynamO'Amazon还提供其他的存储系统：S3用于存储数据块；SimpleDB用于在结构化、包含索引的数据上执行复杂查询。虽然SimpleDB提供了更丰富的API，但是它需要应用实现某种数据分区策略，使得每个分区都有固定的大小限制。因此，一个分区内的数据增长是受限的。

微软的Azure SDS

作为Azure服务提供的一部分，微软构建了大规模的SQL Server服务器，称为SQL数据服务(SL Data Service或SDS，参见http：//hadoop.apache.org)。SDS的核心是通过水平分区达到系统的可扩展性。SDS的一个良好的特性是它通过扩展数据索引，并以SQL Server作为查询处理引擎，提高了查询能力。然而，SDS通过严格的分区来实现其查询表达能力：如果应用创建自己的分区，则无法简单地进行重分区。因此，虽然可以对一个数据分区发出丰富的查询请求，如果某个数据分区数据量增长或者访问很多，那么系统无法简单或自动地通过划分分区来解决“热点”效应。我们的策略是通过表抽象层，使得为加载和恢复数据而做的数据分区或者改变这些分区对于上层应用是透明的。虽然PNUTS的查询模式表达能力不如SDS(PNUTS不支持跨分区的复杂查询），但是我们还在继续探索增强PNUTS查询功能（如前面描述的通过视图的方式）的策略。

PNUTS和SDS的另一个区别是PNUTS系统把地理复制作为内建的主要特征。至少对于SDS的第一版本，其工作负荷是期望全部在一个数据中心，只有当主副本全部崩溃时，才会使用远程备份。我们希望Alice在新加坡、柏林和里约热内卢（巴西）的好友都能够有他们自己本地的Alice更新的首要备份。

其他相关系统

和我们的系统类似，一些需要扩展性和灵活性的其他公司，也构建了很多其他系统。Facebook构建了Cassandra系统(Lkshman等2008），它是一个P2P(Peer-to-Peer，对等网络）的数据存储，其数据模型类似于BigTable，架构类似于Dynamo，系统只提供最终一致性。

Sharded数据库（如Flickr[Pattishall]和Facebook[Sobel 2008]使用的MySQL的分区(sarding)策略）通过把数据分区到很多服务器上来提供可扩展性。但是，Sharding系统并没有提供我们所期望的扩展灵活性或全球数据复制特征。和SimpleDB类似，Sharded数据库的数据需要预分区。此外，该数据库只有一个副本可以是主备份，执行写操作。在PNUTS中，不同数据中心的所有副本都可以执行写操作（虽然是对不同的记录）。

雅虎的其他系统

PNUTS是雅虎构建的众多云系统中的一个。在数据管理中，还包含云计算的其他两个方面，但是研究的问题和PNUTS不同。Hadoop是MapReduce框架(Dan和Ghemawat2007）的开源实现(htp：//hadoop.apache.org)，它提供了在大数据文件上的大规模并行分析处理。Hadoop的文件系统HDFS，对扫描操作做了优化，因为MapReduce主要是处理面向扫描的操作。与之相反，PNUTS主要是处理单条记录读写操作。另一个系统MObStor是为了存储和服务大数据对象如图片和视频而设计的。MObStor的目标是为静态对象提供低延迟检索和低代价存储。因为很多应用需要结合记录存储、数据分析和对象检索，我们正在探索无缝地集成这三个系统(Coper等2009）。

结论

当启动PNUTS项目时，我们希望该系统能够无缝地扩展到跨域多个州的成千上万的服务器。构建这样的系统不仅需要巧妙的工程技术，也要求我们在数据库很多已有的领域继续探索。虽然放弃ACID特性是相对轻松的决策，但是我们很快发现需要提出其他的方案来取代它，因此我们提出了时间轴一致性模型。虽然该模型在设计上相对简单，但处理复杂的极端案例(crner cases)，提出有效的实现机制以及构建应用用例模型都需要反复深入的思考。值得一提的是，刚开始我们的客户和我们自己都不太关心该系统只支持简单的查询语言的不足。但是，随着开发者开始试着在PNUTS上构建真正的应用，我们意识到该系统不能处理的小部分复杂查询将影响该系统被采用。如果我们不提出解决这些问题的机制，那么开发者就需要采用一些复杂的变通方案，在应用内部实现代价高的操作（如嵌套连接）或者是频繁地将数据导出到其他的索引来支持他们应用的工作负荷。

该领域是云数据管理的新兴领域，这在很多其他可选系统设计的构建和部署中也体现出来。我们希望PNUTS的思想有助于我们更接近构建易于管理、广泛应用、多租户(mltitenanted)云数据库系统的目标，为应用提供弹性、高效、全球可用的和无比健壮的数据后台。

致谢

PNUTS是雅虎很多员工共同努力的结果。工程方面的负责人是P.P.S.Narayan和Chuck Neerdaels。该项目的其他研究员包括Adam Silberstein和Rodrigo Fonseca。Brad McMillen和Pat Quaid帮助构建PNUTS架构并把它用于雅虎的云计算服务提供中。该系统的其他设计和开发人员包括Phil Bohannon、Ramana Yerneni、Daniel Weaver、Michael Bigby、Nicholas Puz、Hans-Arno Jacobsen、Bryan Call和Andrew Feng。

参考文献

[1]Azure Services Platform. http：//www.microsoft.com/azure/.

[2]Hadoop. http：//hadoop.apache.org.

[3]Agrawal,P.，A. Silberstein,B.F.Cooper,U.Srivastava,and R.Ramakrishnan."Asynchronous View Maintenance for VLSD Databases."In SIGMOD，2009.

[4]Chang,F. et al."Bigtable:A distributed storage system for structured data."In OSDI，2006.

[5]Cooper,B. F.，E.Baldeschwieler,R.Fonseca,J.J.Kistler,P.P.S.Narayan,Chuck Neerdaels,Toby Negrin,Raghu Ramakrishnan,Adam Silberstein,Utkarsh Srivastava,and Raymie Stata."Building a cloud for Yahoo！"IEEE Data Engineering Bulletin，32（1）：36-43，2009.

[6]Cooper,B. F.，R.Ramakrishnan,U.Srivastava,A.Silberstein,P.Bohannon,H.-A.Jacobsen,N.Puz,D.Weaver,and R.Yerneni."PNUTS:Yahoo！'s hosted data serving platform."In VLDB，2008.

[7]Dean,J. and S.Ghemawat."MapReduce:Simplified data processing on large clusters."In OSDI，2004.

[8]DeCandia,G. et al."Dynamo:Amazon's highly available key-value store."In SOSP，2007.

[9]Furman,J. J.，J.S.Karlsson,J.-M.Leon,A.Lloyd,S.Newman and P.Zeyliger."Megastore:A Scalable Data System for User Facing Applications."In SIGMOD，2008.

[10]Ghemawat,S.，H. Gobioff,and S.-T.Leung."The Google File System."In SOSP，2003.[11]Lakshman,A.，P.Malik,and K.Ranganathan."Cassandra:A Structured Storage System on a P2P Network."In SIGMOD，2008.

[12]Pattishall,D. V."Federation at Flickr:Doing Billions of Queries Per Day."http：//www.scribd.com/doc/2592098/DVPmysqlucFederation-at-Flickr-Doing-Billions-of-Queries-Per-Day.

[13]Ramakrishnan,R. and J.Gehrke.Database Management Systems.McGraw-Hill,New York,NY，2002.

[14]Silberstein,A.，B. F.Cooper,U.Srivastava,E.Vee,R.Yerneni,and R.Ramakrishnan."Efficient bulk insertion into a distributed ordered table."In SIGMOD，2008.

[15]Sobel,J."Scaling out."Facebook Engineering Blog,August 2008.

第5章　信息平台和数据科学家的兴起

Jeff Hammerbacher

图书馆和大脑

在我17岁时，我丢掉了在印第安纳州Fort Wayne的Scott杂货铺的出纳员工作。在我上大学前仅仅两个月里，我看到了没有工作带来的机遇。我没有告诉父母自己被解雇了。每天下午，我依然穿着出纳员的工作服离开家：黑色裤子、黑色皮鞋、白色衬衫，还有罩衫。在父母看来，我这身穿着是为严谨的账单审查工作准备，实际上，我是要在公共图书馆看10个小时的书。

所有好奇心强的人都想知道大脑是如何工作的，17岁的我更是超乎寻常的好奇。我在图书馆里学习大脑如何工作、休息和重建。除了使我们保持平衡、调整体温、不时地眨眨眼，大脑还摄取、处理和生成大量的信息。我们对周围的环境产生无意识的条件反射，养成短期口头禅和肢体特征，做出择偶和教育的长期计划。大脑令人感兴趣的不仅仅是它对感官数据做出反应的能力，而是作为信息库，生成计划和创建新的信息。我很想知道它是如何工作的。

然而，大脑的特点是其存储的信息只在一个人身上。为了从很多大脑收集信息，我们建造了图书馆。为了今后的利用，图书馆科学领域已经为图书馆的信息存储发展了众多的技术。关于该课题的一个有趣的读本是Alex Wright的《Glut》(Jseph Henry出版）。除了为今后检索存储信息，图书馆在创建新信息方面也起了重大作用。正如哲学家Daniel Dennett所说的：“学者即是以图书馆的方式创建另一个图书馆”（a scholar is just a library's way of making another library)。

图书馆和大脑是信息平台的两个例子。它们是组织进行摄取、处理和生成信息的场所，它们加速了从经验数据中学习的过程。当我在2006年加入Facebook时，很自然地开始构建了一个信息平台。因为Facebook用户数量剧增，我们团队构建的系统最终需要管理几十亿兆字节(ptabyte,PB)[1]的数据。在本章中，我将详细阐述在构建Facebook信息平台遇到的挑战，以及在开源软件上构建解决方案过程中汲取的教训。我还会概述在利用信息构建数据密集型产品和服务，并且帮助整个企业制定、实现目标过程中，数据科学家所起到的重大作用。在整个过程中，我还会描述一些其他企业在过去几十年里如何构建信息平台来处理这些问题。

在开始介绍前，首先需要指出的是我去图书馆而不是去杂货店的计划还是很快泡汤了。自在地读了几天后，某天晚上我从图书馆出来时，却找不到车子了。对我来说，在那时丢车很平常，但是停车场是空的，所以我知道出事了；最后是母亲发现了我的伎俩，把我的车拖走了。在走回家的漫长路上，我在心里学会了一课：应该带着怀疑的态度来考虑自己的策略，另外，不要和母亲斗智。

[1]1PB(petabyte)=1024TB，1TB(terabyte)=1024GB

Facebook具有了“自知之明”

在2005年9月，Facebook首次向非大学生公开，允许高中生注册账号。忠实的用户愤怒了，但Facebook团队认为这是为网站做出的正常方向。那么它该如何证明它的方案是正确的呢？

此外，在几乎所有可登录Facebook网站的学校中，Facebook已经渗入学生当中，但还是在有部分学校中，该网站一直不受青睐。和那些更成功的网络相比，这些落后的网络对于Facebook有什么区别呢？Facebook团队应该如何做才能激励他们的成功？

当我在2006年2月参加Facebook面试时，他们正积极地期望找到这些问题的答案。我曾在大学学习数学，在华尔街工作近一年，工作内容是构建模型来预测利率、价格复杂的衍生产品和对冲抵押贷款池；有一定编程经验，GPA成绩“暗淡”。虽然我的背景可能不太理想，但是Facebook却给了我研究科学家的职位。

几乎同时，Facebook聘用了一个报告分析主管。该主管在解决问题方面的经验远远超过我。我们和另外一个工程师一起，开始着手构建一个数据收集和存储平台，以便找到我们产品以上问题的答案。

我们第一个尝试是构建一个离线信息库，其涉及两个方面：一是用Python脚本把查询分发到Facebook的MySQL服务器层，二是采用C++实现守护进程，实时地处理事件日志。当脚本可以如期运行，我们每天收集大约10GB的数据。我后来明白系统的这部分通常称为“ETL”过程，即抽取、转换和加载。

Python脚本和C++守护进程从Facebook的数据源系统中抽取数据，然后这些数据又被加载到MySQL数据库用于离线查询。我们在包含这些数据的MySQL上又运行了一些脚本和查询，对数据进行聚集，以便得到更有用的表现方式。这种用于决策支持的离线数据库即“数据仓库”(Dta Warehouse)。

最后，通过简单的PHP脚本把数据从离线的MySQL数据库抽取出来，向内部用户展示收集到的信息摘要(smmary)。这是我们第一次可以回答网站特性对用户行为的影响。早期通过以下几种渠道分析最大化增长：登出用户的默认页面的布局、邀请来源、Email联系方式导入器的设计。除了以上分析，我们开始通过历史数据开发简单的产品，包括对赞助商成员特性进行聚集的内部项目。实践证明，该项目很受品牌广告商欢迎。

我那时没有意识到，实际上，通过ETL框架、数据仓库和内部控制台(dshboard)，我们已经构建了一个简单的“商业智能”(Bsiness Intelligence)系统。

商业智能系统

在1958年IBM系统期刊的一篇论文中，Hans Peter Luhn描述了一个系统，该系统基于每个用户“动作点”(ation points)的“兴趣信息”(iterest profiles)，将文件通过“选择性传播”(slective dissemination)到动作点上。作者描绘了令人震撼的预测科学理论。该文章的标题是“一个商业智能系统”，它是“商业智能”这一术语在现代环境中的第一次应用。

除了实时信息传播，该系统还可以进行“信息检索”，即搜索，可以在全文资料库中执行。Luhn强调动作点，突出了信息处理角色在完成任务中的重要性。换句话说，仅仅收集并聚集数据是不够的；由于利用数据进行推断的洞察力很重要，企业必须提高能力来完成艰巨的任务。他还提出“报告人”需要周期性地筛选数据，有选择地将信息移到需要的动作点上。

从Luhn的论文发表之后，商业智能领域在过去五年得到不断发展，“商业智能”这个术语和结构化数据的管理更加紧密相关。今天，经典的商业智能系统是由ETL框架和商业智能工具组成：ETL有规律地把一组数据源中的数据抽取出来并加载到数据仓库中；商业分析师利用商业智能工具在该数据仓库上生成报告供内部使用。那么，我们是如何从Luhn的愿景走到今天？

在1970年，E.F.Codd首先提出了关系模型；在20世纪70年代中期，IBM拥有了关系数据库管理系统(RBMS)的工作原型。RDBMS极大地促进了构建面向用户的应用，到了20世纪80年代早期，这些应用已经非常普遍。

在1983年，Teradata出售了第一个关系数据库，该数据库是专门为Wells Fargo公司的决策支持而设计。几年后，在1986年，Ralph Kimball成立了Red Brick Systems公司，面向相同的市场构建数据库。以后，应用开发商就使用Teradata和Red Brick系统来开发解决方案，但是直到1991年，在数据仓库方面的首篇经典论文才发表。

Bill Inmon的《Building the Data Warehouse》(Wley出版社）一书，对数据仓库设计做了连贯清晰地阐述，它包含构建数据仓库的详细方法和最佳实践。Inmon提倡先仔细研究已有的数据源和商业目标，再构建企业数据模型。

在1995年，随着Inmon的书越来越受欢迎，数据仓库在企业数据中心内大量繁衍，数据仓库机构TDWI(The Data Warehouse Institute)诞生了。TDWI组织了有关会议和报告，在阐述和传播数据仓库方面的知识上，其依然是个核心力量。在斯坦福大学启动它的WHIPS研究机构那年，数据仓库在学术领域开始传播。

1996年，Ralph Kimball发布了《The Data Warehouse Toolkit》(Wley出版社）一书，对Inmon理论进行了挑战。Kimball提出了实现数据仓库涅(nrvana)的不同方式，首先摒弃了企业数据模型。Kimball认为不同的企业单位应该构建他们自己的数据“集市”(mrts)，这些集市可以通过“总线”(bs)相连。此外，Kimball提倡使用多维建模，而不是使用范化的数据模型，前者的关系数据模型可以通过少量人工干预来适应很多数据仓库实现中的特定的工作负荷。

随着数据仓库的增长，通常情况下，商业分析家想要快速地操作少量的数据子集，通常这些数据是通过几个“维度”参数来描述。基于这些观察，微软的一组研究人员，包括Jim Gray，在1997年推出了CUBE操作符。该新的操作符允许在小的、多维的数据集上进行快速查询。

维度模型和CUBE操作符都意味着虽然关系模型在构建面向用户的应用方面获得了成功，但它可能并不是构建信息平台的最佳方案。此外，文档(dcument)和动作点(ation point)是Luhn关于商业智能系统提议的核心，而不是表。另一方面，整个工程师团队在构建关系数据处理系统中具备出色的专业知识。

在分析了一些背景知识后，我们再一起回到关于Facebook的探讨。

数据仓库的消亡和重起

在Facebook，我们不断地向MySQL数据仓库加载更多的数据，执行更多的查询。我们只执行过为动态网站提供服务的数据库查询，对于在数据仓库上执行一条查询所需运行的时间之长，令我们都很惊讶。在和一些经验丰富的数据仓库专家讨论后，我明白了由于查询复杂性、数据量大或者两者兼备，查询执行几个小时甚至几天都是很正常的。

有一天，我们的数据库数据接近100万兆（1TB)，mysqld守护进程突然中断了。在诊断了一段时间后，我们试着重启数据库，熬了一宿，直到重启操作开始执行，我们才回家。

当我次日早上回去工作时，数据库还处在恢复阶段。由于数据被很多客户端修改，为了获得一致性视图，数据库服务器维护了一张持久性列表，该列表包含了所有的修改操作，称为“重做日志”(rdo log)或者“预写日志”(wite-ahead log)。如果数据库服务器被粗暴地终止和重启，它将会从重做日志中读取最近的写操作日志，加快恢复速度。由于我们的数据仓库系统规模很大，MySQL数据库恢复需要一定的时间。在那次崩溃之后，我们花了三天时间才重新拥有可以正常工作的数据仓库。

我们那时决定把数据仓库迁移到Oracle上，Oracle数据库软件对于管理大数据集有更好的支持。我们还购买了一些昂贵的高密度存储服务器和一个强大的Sun服务器来运行新的数据仓库。

在我们把程序从MySQL迁移到Oracle的过程中，我充分体验到所谓的标准关系数据库在实现上还是存在许多差别。每个数据库的大批量导入和导出工具使用完全不同的机制。此外，每个数据库支持的SQL语句很不一样，这迫使我们不得不重写很多查询。更糟的是，Oracle上的Python客户端库还不正式且有些bug，因此遇到问题时，我们需要直接和开发人员联系。

经过几周的辛苦努力，我们重写的脚本在新的Oracle平台上可以运行了。夜间执行的程序运行正常，我们很兴奋地去尝试Oracle“生态系统”的其他工具。尤其是，Oracle有个名为Oracle仓库构建器(Oacle Warehouse Builder,OWB)的ETL工具，我们期望用它替代自己手工写的Python脚本。但是，该软件不支持我们需要的数据源规模：在那时，通过每天晚上收集数据，Facebook已经有几万个MySQL数据库。虽然连Oracle都无法帮助我们解决在ETL方面遇到的可扩展性挑战，但是我们很高兴拥有一个能够正常运行的包含几百万兆(T)数据的数据仓库。

然后，我们打开点击流日志：仅仅第一天，在Oracle数据库上就发送了400GB的无结构化数据。我们再次对使用数据仓库表示怀疑。

超越数据仓库

根据互联网数据中心(Iternet Data Center,IDC)显示，数字世界在2011年将超过1800exabyte（1exabyte=2^16byte)。这么庞大的数据将无法通过关系数据库来管理。因此，对数据库管理系统存在很紧迫的需求，要求该系统能够从无结构化数据和结构化数据中抽取信息，但是人们对如何进展没有达成一致。

特别是，自然语言数据量丰富，信息量大，但是数据仓库管理不善。自然语言和其他无结构化数据通常在文档库和语音记录中获取，为了管理这些数据，企业组织开始超越了数据仓库供应商的产品，探索很多新的领域，包括企业搜索。

虽然很多搜索公司构建了很多工具来收集很多超链接文档即万维网(WW)，一些企业搜索公司选择重点研究内部文档集的管理。Autonomy公司成立于1996年，由一些剑桥大学的研究人员组成，他们充分利用贝叶斯推导算法(Byesian inference algorithms)来帮助定位重要的文档。Fast Search and Transfer(FAST)公司于1997年在挪威成立，其核心技术是更直接的关键字搜索和排序。两年后，Endeca公司成立了，其核心是研究使用结构化的元数据遍历文档集，该技术即“分面搜索”(fcted search)技术。G公司看到了其在搜索领域专长的机遇，在2000年推出了企业搜索设施。

在短短的几年内，企业搜索已经成长为拥有几十亿美元的市场，该市场和数据仓库市场几乎完全分离。Endeca拥有一些处理更传统的商业智能的工具，一些数据库供应商为系统引入了文本挖掘能力，但是对结构化的和无结构化的企业数据进行管理仍然尚未实现一个完善的、集成的解决方案。

企业搜索和数据仓库都是技术解决方案，这些方案是为了最大化利用企业的信息资源来改进性能。早在1944年，MIT教授Kurt Lewin提出了“行为研究”(ation research)的螺旋式框架，其每个螺旋阶段是由计划、行为以及关于行为结果的事实发现组成的循环。对该问题更现代的方法是Peter Senge的“学习型组织”(Larning Organization)概念，其思想在他的书《The fifth Discipline》(Boadway Business出版）中做了详细阐述。这两种管理理论在很大程度上都依赖于企业组织根据先前收集到的行为信息做出反思并适应的能力。从这个角度来说，信息平台是一个学习型组织用于摄取、处理和生成实现螺旋式行为研究的必要信息的基础设施。

讨论完结构化和无结构化的数据管理，让我们一起回到Facebook的故事。

“猎豹”和“大象”[1]

从第一天开始对Facebook的点击流写日志起，到现在我们已经收集了超过400GB的数据。对该数据集的加载、索引和聚集操作对Oracle数据库的负载很重。虽然做了很多优化操作，但是我们还是无法在24小时内完成对一天的点击流的聚集操作。很显然，我们需要把日志文件聚集到数据库外，只在数据库中保存摘要信息(smmary information)供后期查询。

幸运的是，一个来自某大型网站的顶尖工程师加入了我们团队，他有过处理大规模Web点击流的经验。仅仅几周的时间，该工程师就构建了一个名为Cheetah（猎豹）的并发日志处理系统，该系统能够在两个小时内处理一天的点击流。这实在太让人振奋了。

但是，Cheetah存在一些不足：首先，在处理完点击流数据后，原始数据还是以归档方式保存(achival storage)，不能够被再次查询。此外，Cheetah是从一个共享的NetApp归档数据中获取点击流数据，而NetApp归档数据的读带宽受限。每个日志文件的“模式”(shema)是嵌入在处理脚本中，而不是保存为可查询格式。我们没有收集进程信息，而是通过Unix基础工具cron来调用Cheetah任务，因此无法应用复杂的加载共享逻辑。最重要的是，Cheetah不是开源的。我们团队很小，资源有限，无法分配更多的资源来开发、维护和给新用户培训使用Cheetah系统。

Apache的Hadoop项目，由Doug Cutting和Mike Cafarella于2005年末启动，是我们取代Cheetah的最佳选择。以Doug的孩子的玩具大象命名，Hadoop项目的目标是实现遵从Apache 2.0许可的G公司的分布式文件系统和MapReduce技术。雅虎在2006年1月聘用了Doug Cutting，并投入了大量的工程资源来开发Hadoop。在2006年4月，该软件使用188台服务器，能够在47小时内，对1.9T的数据进行排序。虽然Hadoop的设计在很多方面优于Cheetah，但它在那时还太慢了，不能够满足我们的需求。在2008年4月，Hadoop用910台服务器，可以在209秒内对1T的数据进行排序。由于Hadoop性能的改进，我说服了运行组团队利用60台Web服务器和3台500GB的SATA驱动器，开始在Facebook第一次部署Hadoop集群。

在最开始，我们通过流方式在Hadoop和Cheetah中都导入一部分日志。Hadoop增强的编程能力加上其能够查询历史数据，从而推动了一些其他有趣的项目。其中一个应用是对所有Facebook用户交互的有向对进行打分来确定这些用户的亲密程度；这个分数可以被用于搜索和新闻订阅的排序。过了一段时间，我们把所有的Cheetah工作流都迁移到Hadoop上，废弃了前者。后来，事务数据库收集程序也都迁移到了Hadoop。

有了Hadoop,Facebook的基础设施可以支持对无结构化和结构化的数据的大规模分析。随着平台扩展为每天几百TB的数据规模，可以执行成千上万个任务，我们发现由于现在系统能够存储和检索的数据规模很大，我们可以构建新的应用，探索新问题的答案。

当Facebook向所有的用户开放注册，用户数在一些国家增长迅猛。但是在那时，我们无法根据国家执行点击流粒度分析。自从有了Hadoop集群，我们可以通过加载所有的历史访问日志到Hadoop，写一些简单的MapReduce任务来重新分析Facebook在一些国家，如加拿大和挪威，增长迅猛的原因。

Facebook的用户每天都有几百万的半公开(smi-public)的对话。据一次内部估算，留言板的数据量是博客的10倍！但是，这些对话的内容还是无法进行访问用来数据分析。在2007年，一个对语言学和统计学有强烈兴趣的暑期实习生Roddy Lindsay加入了数据组。通过Hadoop,Roddy能够独立构建一个强大的趋势分析系统，该系统名为Lexicon，每天晚上能够处理TB级别的留言板数据。可以通过URL:http：//facebook.com/lexicon查看结果。

在为Facebook应用构建信誉积分系统时，我们证明了把不同系统的数据存储到相同的存储库中会导致严重的问题。在2007年5月启动了Facebook平台后不久，我们的用户就被“淹没”在添加应用的请求中。我们很快意识到需要添加一个工具来识别有用的应用和用户认为是spam（垃圾）的应用。通过收集API服务器的数据、用户信息以及来自网站本身的行为数据，系统能够构建一个模型对应用进行打分，这使得系统可以分发我们认为对用户最有用的应用邀请。

[1]猎豹和大象在此采用了借代的修辞方法。猎豹(ceetah)指的是Facebook的The Cheetah日志处理系统，大象(eephant)指代的是Hadoop项目，具体参见下文。

不合理的数据有效性

在最近的文章中，G公司研究员发表了三部曲，提炼出他们在尝试解决机器学习中的一些最困难的挑战的心得。当讨论到语音识别和机器翻译，他们认为“简单的模型和大量的数据总是胜过在少量数据上构建的复杂(eaborate)模型。”我不想和他们的发现争论，但是，当然存在某些领域，更复杂的模型会更成功。但是基于G公司研究员的经验，确实存在大量问题，更多的数据和更简单的模型对它们会更有效。

在Facebook,Hadoop是我们探索不合理的数据有效性的工具。比如，当把Facebook网站翻译为其他语言时，我们试着征集那些母语为特定语言的用户，以便帮助我们完成翻译任务。我们的一个数据科学家Cameron Marlow，对所有的Wikipedia进行了爬虫，对每种语言构建了特征三元频率计数(caracter trigram frequency counts)。使用这些频率计数，他构建了一个简单的分类器，该分类器可以通过识别用户的一组留言来确定他的母语。使用该分类器，我们可以通过有针对性方式，积极地征集用户加入我们的翻译项目中。Facebook和G公司在很多应用中都用自然语言数据，详情请参看本书第14章Peter Norvig对于该课题的探讨。

G公司的观点指出了对现代商业智能系统的第三条变革：除了在一个系统中管理结构化和无结构化的数据，这些商业智能系统必须能够扩展到可以存储足够多的数据，使得可以采取“简单模型，大量数据”的方法来实践机器学习。

新工具和应用研究

在Facebook，绝大部分Hadoop集群的早期用户都是渴望追求新兴技术的工程师。为了使企业的更多人可以访问信息，我们在Hadoop上构建了一个数据仓库框架，并称为Hive。Hive的查询语言类似于SQL，支持嵌入MapReduce逻辑、表分区、抽样和处理任意序列化数据的能力。最后一个特征至关重要，因为收集到Hadoop的数据在结构上不断变化；允许用户指定自己的序列化模式，可以使我们把为数据指定结构问题转为把数据加载到Hive。此外，我们还实现了一个简单的用户界面(uer interface,UI)来构建Hive查询，名为Hipal。使用这些新的工具，市场、产品管理、销售和客户服务的非工程师都能够在几TB的数据上自己执行查询。经过几个月的内部使用后，在Apache 2.0许可下，Hive成为Hadoop的官方子系统，现在仍然在积极地开发中。

除了Hive，我们构建了分享图表和图形的门户Argus（受IBM的Many Eyes项目启发）、工作流管理系统Databee、用Python写MapReduce脚本的框架PyHive、为终端用户提供结构化数据服务的存储系统Cassandra（现在作为开源，在Apache孵化器中）。

随着这些新系统的稳定，我们最终构建了由单一Hadoop集群管理的多层模式(mliple tiers)的数据。企业中的所有数据，包括应用日志、事务数据库和Web爬虫，都以原始数据格式，定期收集到Hadoop分布式文件系统中(HFS)。夜间执行的几万个Databee进程将把一部分数据转化为结构化格式，把它放入由Hive管理的HDFS文件目录中。在Hive中执行下一步聚集操作，用来生成Argus服务报表。此外，在HDFS内，在自己的home目录下维护“沙盒”(sndboxes)的工程师可以运行原型(pototype)任务。

目前，Hadoop包含了将近25亿兆（2.5PB)的数据，而且以每天15TB的数量级增加。每天都有3000个以上的MapReduce任务在运行，处理55TB的数据。为了适应这些运行在集群上的任务的不同优先级，我们构建了作业调度器，实现在多个队列上的资源共享。除了支持内部和外部的报表、a/b测试管道和很多不同的数据密集型产品和服务，Facebook的Hadoop集群可以实现一些有趣的应用研究项目。

由数据科学家Itamar Rosenn和Cameron Marlow主持的一个纵向研究项目(lngitudinal study)用于预测长期的用户参与的最重要的因素是什么。我们使用信息平台来选择一些用户的样本，删除游离点，并对参与度的不同尺度使用一些最小角度回归技术(last-angle regression)来生成大量的特性。有些特性能够通过Hadoop生成，包含计算好友网络密度的各种尺度和基于信息特性的用户范围。

另一个探索激励新用户贡献内容的动机的内部研究，在2009年CHI会议的论文“Feed Me:Motivating Newcomer Contribution in Social Network Sites”中描述。Facebook数据组的一个更新的研究是查看信息流是如何在Facebook的社会图中流动，该研究的标题为“Gesundheit!Modeling Contagion through Facebook News Feed”，已被2009 ICWSM会议接收。

在Facebook，每天收集证据、测试假设、构建应用和使用共享的信息平台生成新的洞察。而在Facebook之外，其他公司也同时构建了类似的系统。

MAD技术和Cosmos

2009年VLDB会议的“MAD Skills:New Analysis Practices for Big Data”一文详细描述了Fox Interactive Media(FIM)公司的分析环境。结合Hadoop和Greenplum数据库系统，该团队在FIM中构建了我们很熟悉的数据处理平台，但其工作和我们在Facebook的工作是独立的。

该文章的标题谈及了FIM平台的三个方面：磁性(Mgnetic)、灵活(Aile)和有深度(Dep)。“磁性”表示存储该企业的所有数据，而不仅仅是适合企业数据模型的结构化数据。同样的思路，一个“灵活”的平台需要能够优雅地处理模式变化，使得分析员能够直接在数据上工作，或者根据需求对数据模型不断演化。“有深度”表示在数据上执行更复杂的统计分析实践。

在FIM环境中，在单Greenplum数据库内，数据分为呈现(saging)、生产、报表和沙箱四种模式，和之前描述的在Facebook中Hadoop内的多层模式很相似。

微软单独发表了其数据管理栈(dta management stack)的细节。在标题为“Dryad:Distributed Sata-Parallel Programs from Sequential Building Blocks”和“SCOPE:Easy and Efficient Parallel Processing of Massive Data Sets”两篇文章中，微软描述了和我们在Facebook构建的极为相似的信息平台。它的基础设施包含分布式文件系统Cosmos和并行数据处理系统Dryad，它还发明了类似SQL的查询语言SCOPE。

三个团队在完全独立的技术团队工作，却开发演化了处理大规模数据的相似平台。这是怎么回事呢？通过把特定结构的需求与存储数据的能力以及为数据检索创造API进行分离，大规模网站的存储系统看起来更像数据空间(dtaspace)而不是数据库。

作为数据空间的信息平台

在雅虎、Quantcast和Last.fm公司，也有类似的几十亿兆规模(ptabyte-scale)的数据平台。这些平台不完全是数据仓库，因为它们通常不使用关系数据库或者任何数据仓库建模技术。它们也并不完全是企业搜索系统，因为只对一部分的数据构建索引，而且它们对外开放了更丰富的API。除了传统的数据分析工作，它们通常用于构建产品和服务。

与大脑和图书馆类似，这些共享的数据处理平台为公司摄取、处理和生成信息提供了场所，幸运的话，它们还可以加速企业从经验数据中学习的步伐。

在数据库社区，研究议程已经开始从纯关系数据管理过渡到在大数据集上存储和查询的更经典的系统，称为“数据空间”。在论文“From Databases to Dataspaces:A New Abstraction for Information Management”(htp：//www.eecs.berkeley.edu/～franklin/Papers/dataspaceSR.pdf)中，作者强调了存储系统必须支持所有数据格式，并提供一些API供数据访问，这些API是基于存储系统对数据的理解不断演化的。

我们之前描述的信息平台是数据空间的真实例子：用单一存储系统管理企业中各个部分的所有的结构化和无结构化的几十亿兆的数据，该系统需要给工程师、分析员和报告员提供各种数据访问API。由于在工业上这些系统大量存在，我期待数据库社区继续探索数据空间的理论基础和实践意义。

信息平台是构建学习型组织的基础设施的关键部分。人们在加速学习和利用信息平台过程中，开始扮演了数据科学家(te Data Scientist)的角色。

数据科学家

在最近的访谈中，G公司首席经济学家Hal Varian强调了员工需要能够从之前描述的信息平台中抽取信息。正如Varian所言：“找到能够为一些变得普遍且廉价的东西提供稀缺、互补的服务。那么，是什么变得普遍且廉价？数据。是什么与数据相辅相成？分析。”

在Facebook，我们发现传统的头衔如商业分析师、统计学家、工程师和研究科学家都不能确切地定义我们团队的角色。该角色的工作是变化多样的：在任意给定的一天，团队的一个成员可以用Python实现一个多阶段的处理管道流、设计假设检验、用工具R在数据样本上执行回归测试、在Hadoop上为数据密集型产品或服务设计和实现算法，或者把我们分析的结果以清晰简洁的方式展示给企业的其他成员。为了掌握完成这多方面任务需要的技术，我们创造了“数据科学家”这种角色。

在金融服务领域已经构建了历史市场行为的大数据存储作为该领域的数据科学家，即数据分析专家(Qants)，来开发新模型的实验场。在工业以外，我发现在很多科学领域，研究生扮演着数据科学家的角色。Facebook数据组团队的其中一员曾在生物信息实验室工作过，在那里他构建过数据管道流，并做类似的离线数据分析。在CERN，著名的Large Hadron Collider生成大量的数据，这些数据是由一群追求突破的研究生精心收集和钻研的。

最近新出的书如Davenport和Harris合著的《Competing on Analytics》（哈佛商学院出版社，2007），Baker的《The Numerati》(Hughton Mifflin Harcourt，2008）以及Ayres的《Super Crunchers》(Bntam，2008）都强调了在跨工业中数据科学家的重要性，他们在促进企业基于收集到的信息做出改进发挥了至关重要的作用。和研究社区在数据空间的调研一起，数据科学家在今后几年需要进一步的定义。通过更好的阐明数据科学家角色，我们可以建设培训课程、制定广告层次、组织会议、写书以及为任何被认可的行业做补充。在这个过程中，可行的数据科学家组织将会不断扩展，用来满足飞速增殖的数据平台上不断增长的专业“领航员”需求，进一步加速跨企业的学习过程。

结论

当面对在Facebook构建一个信息平台的挑战时，我发现观察别人是如何跨越时间和问题领域来解决相同的问题是很有帮助的。作为工程师，我最初的做法是通过已有可得的技术作为指导，这在现在看来显得有点目光短浅。最大的挑战是一直致力于研究构建“学习型组织”的基础平台和人员构成(hman component)这个大的问题，而不是某些特定的技术系统，如数据仓库或企业搜索系统。

我确信构建信息平台采用的硬件和软件将会迅速演化，并且数据科学家需要掌握的技术也将以同样的速度变化。保持致力于加速学习过程的目标对于企业组织和科学都有帮助。未来属于数据科学家！

第6章　照片档案的地理之美

Jason Dykes和JoWood

照片可以是美丽的。把能够捕捉精彩瞬间、点燃激情并能激发崇高的东西仅仅看做是数据，这是对照片的一种贬低。然而一旦以数字形式存储，处理相片的二进制数据可以与处理任何其他的数值型数据流一样。但是我们可以对这些照片做进一步处理：收集在一起、排列整理、添加描述标注，由此我们可以创建出一个场景，并且一个新的美好的东西也由此而生。这种美虽源于组成它的所有照片，但却又远远超过了每张照片的所有美丽汇总。

在本章中，我们将探索以地理的视角审视一组照片时，所能发现的美。我们将通过地图和其他图形来对地理特性进行可视化检查。这里的“地理”指的是允许我们把一些东西同某个地方(pace)或位置(lcation)（这是两个截然不同的概念）关联在一起的信息。当我们处理数据时，有很多相关的地理信息。有些人估计大约有80%的数据是地理数据(McEachren和Kraak 2001）。地理信息可能是直接以经纬度坐标的形式进行记录，或者是这个地方的邮政编码、名称或一些其他的信息。这种地理数据将是组织、过滤和解释数据的有用的方式。记录不断增长的、巨大的数据集中的地理信息，可能会为关于某个地方的观点分析提供格外有用的信息资源。

可以通过很多途径把地理信息和数据进行关联。这个途径可能是数据收集过程的组成部分（例如，卫星遥感）；也可能产生于查询和解释数据的过程中（例如，以G公司本地搜索为代表的一些地理位置相关的服务）；也可能产生于更先进复杂的时空分析过程中，它是“直觉构建”(Snsemaking)过程的一部分，且当前的地理可视化分析正是其典型代表(Adrienko等2008）。在这里，我们采用在收集过程中已经通过特定的定位方式取得了其地理信息的数据作为起始点数据，但这些起始点数据也包含了一些额外的、地点不明晰的描述。地理档案包含了超过100万的带有精确的经纬度的照片。这些照片的经纬度有的是通过带GPS功能的设备如iPhone获得的，有的则是由一些人为的方式在地图上手工定位了他们的照片。此外，这些照片的地理信息也有可能是它们的拥有者以非格式化的文本形式提供描述，或者可能是以邻近地区的名字给照片命名，或者是对照片捕捉到的特征或者活动进行描述。这其中有一定的复杂性和精妙性，而且我们应该明白，当我们试图通过可视化方式来加强对位置和地点描述之间相互作用的知识时，美丽将“油然而生”。

当我们编写优美的代码时，通常情况下都有很具体的目的，比如对列表进行排序，求解线性方程组，或者完成傅里叶变换。代码之美源于达成目的(Klawa 2007）。当处理美丽的数据时，我们的目标往往不是非常明确。分析数据是科学探索的一个重要环节，而且可以带给人们更深的洞察力，并且测试假设情况和验证先前的理论。美丽的数据值得探索。它包含的模式、结构和异常情况虽然不能立即显现出来，但是在进行更深的深度挖掘时，它就显现出来。我们在工作中通过可视化方式探索数据过程中，形成了两个长期的传统。为了进行信息通信以及支持知识发现，制图学已经发展了强大的技术来通过可视化方式表示地理数据。几个世纪以来，它已经成功地将开拓创新、科学活力和更多主观的设计和批判技巧相结合。地图本身可以与它们所描述的对象同样美丽。信息可视化涵盖了数据的可视化探索过程，该过程可能与地理信息毫不相关，而只是通过设计和图形、图表以及相关的交互行为。在本章中，我们将报告一些融合了制图学和信息可视化的基础原理的探索地理数据之美的方法。

数据之美：Geograph项目

我们认为Geograph项目[1]是优美的数据集合，这里有很多方面的原因。描述不列颠群岛(Bitish Isles)的地理的地理对象和标注照片的档案正在快速增长，它是有价值的数据源、在线社区、游戏和乡村探索驱动的完美结合，无论是宏观美还是细节美，都可以让人产生无限遐想。

Geograph项目最初由Gary Rogers提出，后来由Paul Dixon、Barry Hunter和不断发展壮大的各个版面的版主团队提供支持，它旨在收集英国和爱尔兰的每一平方公里上的“具有代表性的图片和信息”(Gograph 2009）。到本文写作为止，该项目已经有超过8500个贡献者，共收集记录了120多万张图片，涵盖了英国244000平方公里国土面积的90%以上。图6-1给出了一个例子。该数字无疑会随着时间不断增长——当你正阅读本章时，英国的乡村正在“地理化”。

图　6-1：Aberuchill附近的小路。远处是Bioran Dalchonzie。这是Geograph项目的网站出现的第100万张图片(htp：//www.geograph.org.uk/photo/1006884）。图片版权归Dr.Richard Murray所有，由Creative Commons License(http：//creativecommons.org/licenses/by-sa/2.0/）授权使用（见彩图12）

Geograph项目的参与贡献者可以自由地选择他们的典型风景，但是地理特征应该能够表达1km的网格范围内的典型的人文和物质地理特征：

想象一下，在地理课上，一个小孩希望弄清楚一个特定的网格范围内的人文和物质地理特征实际上看起来是什么样子，当他看着地图时，他可能会找到对他有帮助的东西(Hwgood等2007）。

地理的很多方面蕴含美：

创造一个有目的地选择并标注的开放图像库的目标是简单性（想法）、复杂性（处·理，其过程也是包含着几丝美丽）、实用性（资源）的“动人”组合。为了维护该集合并使得它可用，推动该项目所付出的组织工作和努力是令人钦佩和印象深刻的。

·通过“自下而上”(bttom-up)、“集体力量”(cllective effort)的方式生成档案，以及使用技术和个人力量来实现这些目标，这些方式中无不蕴涵着美丽。在协作式的“以人作为传感器采集信息”(ctizens as sensors)(Godchild 2007），该项目本质上的集体之美，是基于不考虑个人利益的共同理解和广泛合作，这一点值得一提。

·通过引人入胜、易于理解和激动人心的Web站点来实现和展现该想法，该网站还提供了很多种方式来访问信息，这些也体现了项目在艺术和技术上的品质。

根据地理信息生成的不同尺度的地图，提供了生成该档案集合的各种过程的洞察·力，这些也蕴涵着艺术品质（见图6-2）。这些包含很多富于创造性的制图表示和提供访问档案集合的大量信息和更新的交互特征，比如参与贡献者以及其贡献的照片的地理密度和分布地图。

个人贡献的照片作为人文和物质景观的表示，是由那些居住和游览图片所描述的地·方的人们所决定的，这些照片具有艺术魅力。Geograph项目社区将这种魅力“正式化”，因为“地理年度”(te geograph of the year,GOTY)的候选图片是通过每周从贡献的图片中选出来的。

图　6-2：Norfolk海岸的地理马赛克图。每个图片都是根据其地理位置进行映射，表示了1平方公里的景观。图片由Creative Commons License(http：//creativecommons.org/licenses/by-sa/2.0）授权使用（见彩图13）

Geograph项目在某种程度上是典型的广泛的用户生成的描述地理的数据集合，这些数据正变得可以访问，而这种种特征使得Geograph项目格外显著。存在很多真正的机遇来探索Geograph项目的数据，并通过这些数据孕育新的知识，这些知识可能会进一步加强Geograph数据之美。我们在本章的后面部分将举出一些例子，用来描述在档案里集中展现一些地理视觉探索。

[1]关于该项目的更多信息，可参考其Web站点：http：//www.geograph.org.uk/。

可视化、美丽和树形图

在我们深入探索Geograph项目档案本身之前，一些有助于探索档案集的动机和可视化技术是值得考虑的。传统制图绝大多数集中于可以通过传统媒体进行再制造的静态产品，这一点是可以理解的。在过去15年中，我们的绝大多数工作是交互式的——吸引数字技术来重新考虑地图的本质和角色，并且把它们作为可响应的图像方式，目的是为探索而查询(Fsher 1998）。我们的目标是保证在交互平滑性过程中的艺术品质，以及通过开发富信息的景象和激发思考和探索的动态行为所获得的满足。然而，我们最近的一些工作已经重新重视数据密度，重新集中于最根本的制图设计决策，这些决策和那些高效利用空间生成布局和符号学（数据编码）相关。制图工作的一部分是由硬件进步所驱动，这些进步使得处理和显示大的数据密集型图片变得更可行。实际上，该工作响应了以新的有效、优雅的方式来图形化表示各种更大的数据集合的需求；由于我们遵从了Tufte的建议“在小空间展现很多数字”(Tfte 1983），因而这些数据变得更易于访问。

可视化数据探索之美在哪里

我们把美丽看做和一些激励相关的主观品质，该品质可以带来积极的感性体验。在可视化过程中，通常都是开发者或设计者觉得美丽。为了更全面地审视艺术（如Kosara 2007），适当地号召项目社区正式地对可视化作品提出批评意见。但是，在一个可用的知识体系开发出来之前，我们在开发优雅的图形上还是依赖于广泛的原则和经验法则。以上这些很多方面都已经应用于《Beautiful Code》(Oam和Wilson 2007），而且有助于数据可视化应用。例如，Brian Kernighan(Kernighan 2007）指出优雅的代码的特征，包括紧凑、优雅、高效和实用，而且通过“理想情况下，代码可以在单个页面显示”来非正式地量化紧凑性。Yukihiro Matsumoto认为让人难以理解的代码是不优雅的，而且应用该标准来开发Ruby编程语言(Mtsumoto 2007）。但是“难以理解”和复杂是不同的，不该混淆在一起。根据我们的评判标准，一个易于理解的简单图形但表示的数据信息量非常少谈不上优雅。相反地，美丽的数据可视化是通过易于理解的方式来表示复杂的东西——可能通过重点研究该数据的某几个方面或者突出某些特殊方面。这可能和Kernighan的原则一致：努力达到在单个“页面”上完成（数据可视化）的目标。我们的情况是通过以下几个方面来达到这个目标：努力扩展和综合已有的制图和信息可视化方法，从而高效地利用空间显示多个图片（在空间和其他方面的）关系；力求通过足够紧凑的方式，使得在一个页面或者屏幕可以完全显示，而且可以很优雅地同时展示全局结构(Gstalt)和局部细节（即时需求的细节）；基于任何特定的数据集上的需求，努力设计和开发易于理解和有用的图形，使得真正的用户能够理解和使用图形来解决已知的信息需求。

Tufte（1983）提出了“数据/墨水”比率的想法——这是一个启发式算法，鼓励平面设计师评估一个直接用于表示数据的页面的墨水比例。该比例越高，使用图形符号化就越高效，而且其表示的信息就越有深度。这种形式和功能指标可能有助于数据图像之美。相似地，我们可以考虑数据/地理位置比率，即一个图形元素在一个页面的位置程度反映了它所表示的数据的特征。传统的地形图在这方面表现很好，因为一个符号在页面上的位置通常可以确定其所代表的地理位置。在这方面，很多信息图形可能不是很高效，如一些地图（比如统计地图和电路图）。我们认为空间的有效利用是优雅的数据可视化的重要方面，因为它支持地理（或者其他）模式的可视化发现过程。在包含地理特征的大量的数据集情况下，比如大量的志愿者贡献的数据集合，空间的有效利用就变得更加切实重要。简而言之，在高效使用和展现空间上蕴涵着美——尤其是为了展现地理。

使树形图变美：从地理角度出发

树形图是层次的填充空间式(sace-filling)展现方式(Sneiderman 1992），如图6-3所示。正如很多美丽的想法一样，树形图基于一个优雅简单的思想。把一个数据项表示成一个矩形。如果该数据项本身包含一些其他数据项（任意层次的特征定义）的集合，这些数据项的每个都是通过一个更小的矩形来表示，这些小矩形落在“父”矩形的内部。反过来说，这些小矩形本身也可以包含更小的“子”矩形等。这些矩形在排列上可以填充整个图形空间，而没有任何缝隙。每个矩形或节点可以根据其所表示的数据的一些特征来调整大小；还可以根据数据对它进行着色或者有意义地标注。所有的矩形都是可见的，它们不会交叠。这些矩形在表示的紧凑性上具有一定的优雅性（单个着色和标注的矩形可以同时显示某些数据的三个或者更多独立的特征）。每个节点（一个矩形）的简单的几何图形本身就表示大量的数据集，因为一个树形图可以同时显示的节点数目几乎和一个屏幕上的所有像素一样多。由于层次的语义包含关系是直接表示为树形图中的几何包含关系（父节点包含子节点），这种方式让人感觉更优雅。

我们认为采用树形图来表示大量的地理和专题分类记录信息具备潜在可能性，而且我们发现通过构建新的分类这种方式探索Geograph数据集中的海量记录数是可能的。

然而，由于很多原因，树形图广受人们的批判。滑稽的是，人们批判的是树形图的艺术性(Cwthon和vande Moere 2007），但是我们认为这是功能实现上的问题，而不是设计本身。

更值得注意的是，在树形图中任意放置节点可以显著降低“数据/位置”的比例。绝大多数现有的树形图布局算法定位这些节点是为了最大化它们的“高/宽”的比例（使长方形尽可能接近方正，对于审美和大小比较任务是很重要的），而且为了提高可阅读性（最大化水平上的线性连续性）。很少有人会关心如何使用图形的位置来表示该数据的某些方面特征。因此，树形图包含线性不一致性以及节点位置的任意放置（见图6-3）。这些对立面创建了制图学和统计图形的最佳实践，其中平面上的位置被认为是表示关系信息的最主要方式(Brtin 1983）。在树形图内的节点的任意位置都无法利用“认知地理第一定律”(Fbrikant等2002），该定律认为不同物体之间距离越近，就越相似。

图　6-3：两个简单的树形图。（左图）通过传统的方块化(suarified)布局方式（按序着色）放置的20个有序节点。注意没有使用前后一致的位置来表示有序的1～20序列。（右图）空间树形图，其节点是根据它们相应的地理位置（按范围着色）来放置的

我们认为在一个二维树形图中，根据数据中的一维次序或者二维次序，对所有层次的节点进行排序是可行的(Wod和Dykes 2008）。通过这种处理方式，我们解决了和树形图关联的一个关键问题，即主要的信息载体维度，通过一维（或者多维）的数据维度对它们进行映射，但这并没有达到充分利用的效果。简而言之，我们在树形图内部使用空间来表示一维上有序的或者二维上按空间排列的数据。

Geograph在使用条款上的观点

“地方”(pace)的概念是很复杂的，不能简单地通过经纬度来很好地描述。地方不仅仅是一个位置，而且它也说明了创建一个地方的感觉所包含的特征本质。它可以依赖于无形的、主观的甚至有时对立的特征，这些特征传统上无法通过数字数据集来很好地进行展示。志愿者收集的或社区参与者贡献的地理信息，如可以通过Geograph项目获取的地方的个人描述，为我们提供了新的多角度方式来访问信息。这些可能会反映很多观点，而且当我们努力通过更有效的方式来描述一个地方时，促使我们开始考虑对该地方的其他想法。

Ross Purves和Alistair Edwardes在Zurich大学做研究时，一直使用Geograph项目的数据作为描述地方的参考来源。他们的最终目标是通过为描述地理的数字图像自动添加索引项，用来改进信息检索，这些数字图形和某个地方的流行观点相关，如“山脉”、“偏远”或“徒步旅行”。他们的工作涉及验证先前的研究，并通过比较Geograph项目的数据和当前为了描述地方所付出的努力来形成新的观点，以及分析在地理描述中的术语共现度(Ewardes和Purves 2007）。

在学术上流行的方法涉及识别所描述地方的基层或场景类型。这些对地方的综合描述已经通过传统地人为主观测试的方式推导出。这些描述之间很难协调，而且通常参与的人数很少，使得人们很难对结果进行泛化或者重新做那些实验。Edwardes和Purves评估了Geograph项目的参与者所使用的方式，对场景类型如山地、丘陵、山谷、河流、岩石、湖泊、峡谷、悬崖、海洋、洞穴进行排序，发现和参与者的研究中报告的术语以及这些术语使用程度有很大关联(Ewardes和Purves 2007）。

随着数据集中所使用的术语在一定程度上得到了验证，我们共同确定探索自然、结构和Geograph项目数据三者之间的一些关系的地理特征存在着机遇。特别地，我们期望能够理解以下几个方面之间的关系：照片内容、照片位置以及这些照片提供者的标注中记录的关于某个地方的文本描述。可视化方式看起来很合适，而且树形图技术使我们能够探索Geograph项目的以上这些特征。

以下的例子记录了一些方式，这些方式中的空间树形图和其他图形被用于探索Geograph项目，因为我们开发了该项目数据集的共享知识库，用来表达我们对地方的描述的理解。

表示术语分类

Geograph项目档案在2008年4月进行了处理，当时大约为75万的图片提供了标题和文本描述来说明其地理信息。我们重点研究与六个基础层次相关的图片，或者通过Edwardes和Purves的分析，被认为特别有意思的场景：海滩、乡村、城市、公园、山脉和丘陵。对于这些场景类型，我们为每个在以下三个不同方面选择了最受欢迎的描述词：活动（主要是动词）、元素（主要是名词）和品质（主要是形容词）。这种方式产生了六种场景类型的术语共现分类，每种场景包括三个方面，每个方面包含和场景类型相关的很多描述符。树形图反映了对于选定的包含一个流行描述符的场景类型，其每个共现度所包含的节点的分类，并展示了在Geograph项目所使用的描述的一些结构。图6-4所表示的树形图采用了有序的方块化布局算法来优化节点的形状和位置一致性(Wod和Dykes 2008）。叶子节点（各个子图）是单位大小。

图　6-4：对于六种选定的场景类型，其出现在地理标题和评论的描述的树形图。节点大小表示术语的出现频率。颜色突出了包含一种继承随机方案的场景类型/刻面(fcet)/描述符分类。布局采用“有序的方块化”方法来维持节点之间的方块形状（见彩图14）

图6-4中的每个节点都采用一种继承随机方案来对场景类型进行着色，而且孩子节点（刻面、描述符和地理科学本身）继承了这种着色方案，只有微小的颜色差别。虽然颜色本身并没有独立的涵义，但是采用这种着色方案来突出这种分类方式的层次结构。这种布局和颜色编码的结合有助于我们通过展现结构和鼓励视觉对比来探索数据。例如，我们发现丘陵(hll)这个词比公园、乡村、城市、沙滩和山脉这些词更流行，因为它在树形图中占了更大的区域面积。在这些场景类型中，元素刻面始终比品质或活动更为流行。活动方面和公园相关性特别强。描述符“公路”在四种场景类型中都居首要地位，而不是“沙滩”或者“山脉”。对于丘陵和山脉，描述符如山谷和小路与丘陵一起使用得更频繁，虽然相对而言，这些描述和山脉更相关。而湖泊和山脉一起使用得更频繁。小径是对与丘陵相关的流行的描述符，而不是山脉。这种数据密集的信息之美的一个重要方面是很多其他关系是同时显示，而且可能是同样被报道。我们可以通过远远多于1000个的文字来继续描述这些图片。

基于对各种Geograph的图形数据的理解上的经验，我们这里已经采取了一系列的设计决策。一个有用的可选方案是采用“切片和切块”的布局算法。其结果是一个马赛克图，使得其内的每个场景类型和刻面的大小比例更易于关联在一起，因为是通过长度而不是区域来进行比较（在图6-5上方）。但是，把生成的节点拉长，意味着不同描述之间的标注和大小估计会变得更加困难。一个折中的方案是应用有序的方块化算法来对叶子节点进行排列（在图6-5底部）。通过这种在不同的分类级别所做的布局和颜色实验，有助于我们突出和探索数据集中的各种显著的“品质”。

图　6-5：对于六种选定的场景类型，其出现在地理标题和评论的描述的树形图。节点大小表示术语出现频率。颜色突出了包含一种继承的随机方案的场景类型/刻面/描述符分类。布局采用“切片和切块”的方法来增强维度（上图）间的比较和“切片和切块/有序的方块化”方法来改进标注的可读性，如下部的图（见彩图15）

通过颜色表示绝对地理位置

虽然图6-4和图6-5中的树形图提供了如何描述地方的一些信息，但是它们几乎没有提到该地方与地理位置之间的关系。我们探索了一系列的方式给树形图增加地理位置信息。第一种方式是采用颜色来提供大不列颠群岛之内的绝对地理位置信息。Geograph数据的地理位置信息是通过“东向”(esting)和“北向”(nrthing)坐标组的投影来存储的，它们记录了该地理位置到英国国家电网(Bitish National Grid)起源地区的东部和北部距离。

我们面临的挑战是通过一个颜色来表示照片地理位置的两个维度（东向和北向），该颜色能够把该照片和其他不同地区的照片区别开。绝大多数的颜色空间是通过三个部分（如红色、绿色和蓝色，或者色调、饱和度和亮度）定义的，因此只选择两个部分来表示一组坐标是有问题的。此外，绝大多数颜色方案在主观感觉上就是不一致的；换句话说，两种颜色之间在色彩板上存在固定的视觉差距。因此，我们决定采用CIELab颜色模型，其提供了一种在感官上感觉更一致的色阶方案。通过色彩空间的a和b两个分量表示每个照片的“东向”和“北向”位置，我们能够生成一个颜色地图，其中西南地区是橙色的，东南地区是绿色的，东北地区是蓝色的，而西北地区是紫色的。中部地区趋向于褐色，两个节点间的颜色相似度说明了它们所表示的照片的地理位置的相似度。通过这种方式对图6-6有序的方块化树形图中的节点（和图6-4不同）进行着色。

图　6-6：“有序的方块化”树形图，其颜色是通过CIELab颜色模型的一种色彩空间来显示绝对的地理位置，在该空间中，主观感觉到的颜色区别和地理位置的差异紧密相关（见彩图16）

从这个观点来看，地理位置对地方的描述带来的影响是显而易见的，这一点可能鼓励我们从空间角度来探索Geograph项目的数据档案。例如，小径、顶峰和石冢展示了高山草地不同的地理特征；白垩、古冢和交界点则表示丘陵的不同地理特征；而山脉、沙滩、乡村有不同的地理位置特征；在沙滩内的活动和品质有显著不同的地理特征。地理位置和地方之间相关的一些复杂性也很明显。

通过空间树形图表示相对地理位置

使用颜色来表示地理位置有一定的艺术之美，并为地理位置-地方(lcation-place)的关系提供了一定的洞察力，但是其在有效性上具有一定的局限性。特别地，它要求用户为颜色和地理位置的对应关系分配内存。图6-6显示的树形图也没有以任何有意义的方式来使用节点位置。因此，我们可以把每个照片的地理位置映射到树形图中的节点位置，这样北部的照片会出现在每个封闭的节点空间的上方，西部的照片出现在左上方等。由于树形图会通过非交叠矩形来填充空间，我们无法提供精确的地理位置空间映射，但是这种布局方式确实说明了节点的相对位置，因而增加了“数据/地理位置”的比率。如果我们对探索地理位置方面的地方描述符感兴趣，则可以采用CIELab着色方案来突出绝对的地理位置，或者在保留很强的制图隐式表现前提下，用颜色表示数据的一些其他方面（比如术语的重要性）。图6-7保留了有序的方块化布局方式来进行术语分类，但是根据地理位置，对每个描述符的节点进行重新排列。

在该图中，山脉的粉色、紫色和褐色显示了该术语是用于北部和西部，虽然如围栏、垫木、凹地和黑色这类术语和这种模式不一致，但是它们也显示了自己的地理特征。沙滩的生动形象的色彩显示了这种场景类型周围的海岸的自然特性，而城市更柔和的颜色则表示它是中央地带。

图6-8通过对所有节点使用空间排序方法，更进一步加强了显示效果。它显示了选定的六种场景类型的地理和分类特征。

表示地理位置位移

虽然在分类结构中，我们可以识别出一些空间模式，而为了使节点能够完全镶嵌于树形图空间中，节点的显示和真实的地理位置的偏移程度是不确定的。CIELab的着色方案可以提供这种偏移的表示；注意图6-7中的沙滩的品质和活动方面的不同颜色，或者在城市/元素/舞厅和小山/元素/农场之间的不一致性。我们可以做出进一步的改变，但是这种改变是通过说明在地理上，一张照片或者一组照片如何在镶嵌过程中从地理坐标转换过来。为了实现这点，我们遵循Skupin和Fabrikant（2003）的建议，他们认为认知上似是而非的制图需要采用合适的方法来表达这种位置错误。

图　6-7：“有序的方块化”树形图，通过CIELab色彩空间的颜色显示绝对的地理位置。包含描述符节点的叶子节点，使用空间有序算法，通过地理位置做关联排序（见彩图17）

图6-8在树形图上叠加了一组线条集合。这些线条把每个节点的树形图位置连接到其地理位置-线条越长，位移越大。例如，海滩上与品质和活动方面相关的位移向量，尽管它们在空间树形图中是并列的，但是这些位移向量证实了我们之前指出的不同地理特征。创建这些线条背后的设计目标是为树形图提供额外的空间环境，而同时保持能够探索术语分类。图6-8中，粗线条用于显示场景类型的位移，而不是刻面的位移。因此，在显示地理位移时，使用的是非常细的线条，因为在树形图中，在地理上的照片位置和叶子节点的位置之间可以描绘成千上万的线条。图6-9给出了一个示例，在该例子中，位移向量在其节点位置终端比其地理位置终端弯曲度更大。这有助于突出任何的节点空间聚类，正如图6-9给出的在多数情况下的例子，也提供了对位移趋势的总体概览。

这种术语分类以及相对和绝对地理位置的并发视图，允许我们考虑丘陵上的小径、顶峰和石冢，同时把它们与白垩、古冢和交界点区别开。

这种空间安排可能会吸引我们注意新的关系。举个例子，马路、小路和小径表示类似的功能，但是当和丘陵一起使用时，它们分别有不同的地理特征；钓鱼和曲棍球是乡村中不同地理位置的活动；比较乡村中的礼拜堂和大教堂，或者山上的高尔夫球场和公园的高尔夫球场。图6-10显示了海滩上的一些元素的特写，这些特写帮助我们从其他方面中发现以下几点：通过潮汐的积聚场所形成的英国海岸线的的各个方面；南部地区突出了海滩内的路径和悬崖；而东南部地区突出了与海滩/元素/海港相关的特定的地理集群，以及西北中部地区突出了海滩/元素/海港。

图　6-8：对于六种选定的场景类型，其出现在地理标题和评论的描述的空间树形图。节点大小表示词频，颜色表示使用CIELab方案的绝对空间地理位置。位移向量表示非叶子节点的绝对地理位置如场景类型、刻面和描述符（见彩图18）

发现之美

我们考虑的是大规模分类数据集的空间上有序的空间树形图，该树形图的“数据/地理位置比率”很高，在审美上令人赏心悦目，而且把本章中提供的数字作为描述数据之美的备选。但是，美丽的事物应该是清晰、可用的，而且最终必须是令人满意且优雅。Matsumoto（2007）通过计算机代码环境表达了这种观点，坚信优美的代码必须具备可读性。但是，正如地图使得复杂的地理数据以适合目的的方式获得了可读性，这种方式可以决定很多空间关系，实现如航海、地理比较和模式检测的功能。因此，我们的目标是通过图形的可读性，使得Geograph项目的语言具有地理分析意义。我们能读懂Geograph数据吗？我们认为Geograph项目中的图形以及其他的图形意味着我们在诠释现有的、丰富的、美丽的地理数据集上有了不断深入发展，而且使用通过分析确定的描述和地理位置之间的各种关系获取知识，根据获取的这些知识采取措施。

图　6-9：对于六种选定的场景类型，其出现在地理标题和评论的词汇的空间树形图。位移向量表示非叶子节点的绝对地理位置（共现词），并提供了关于位移的空间聚类和空间趋势的信息来满足树形图的空间填充目标（见彩图19）

这些场景视图中描述的空间关系依赖于宽高比和叶子节点的平均地理位置。我们可能会对比这里所描述的更精确的地理位置感兴趣，而且数据密集的树形图对于为更传统的映射选择候选的刻面和描述符一直都很有用。通过树形图推导出的词汇共现图能够帮助我们确定在术语使用上可量化的空间区别。举个例子，我们发现在南部和西南部地区，山谷和丘陵共现的频率比期望的还高，而在北部地区，顶峰和丘陵一起使用更频繁。而且当使用Geograph项目的数据中其他可选的描述方式时，这些关系在不同程度上一直存在。它们可能会反映在选择需要记录的场景的特定方面有偏好：区域地理特征、语言区别、在特定地方贡献图片的个人偏好，或者以上这些特征的结合。不论怎么理解，我们对树形图的理解，由于其他地方的启发，引起了我们对一些其他地方的地理特征的兴趣。同时需要注意的是这里使用了一些较生硬的词汇来描绘乡村。这些趋势可能并非是有意识的偏见，而可能是和为Geograph项目选择一些令人赏心悦目的景观方面相关——这意味着我们对这些数据的理解使我们更加坚信处理的数据之美。我们打算继续探索和分析这种关于地图和更正规的空间地理术语。用户选定的描述性术语的地理特征的以上及其他方面，有助于描述地方的“本体”特征的发展。

图　6-10：在beach基础区块中出现的对选定元素的描述符在地理标题和评论中出现的术语的空间树形图。位置向量表示图6-9中放大部分的叶子节点的绝对地理位置（见彩图20）

我们的图形和探索是不全面的。我们正在研究由社区参与者贡献的地理信息中存在的系统偏见所造成的影响，并提出策略来减轻这些影响。我们正在制定标识符，用来描述视觉设计空间和交互式应用，通过这些标识符可以做出进一步的探索。我们尚未考虑在Geograph项目中所确定的在地理上不同的关系是否会随着时间推移而依然保持一致。然而，这里所描述的方法帮助我们获取知识，克服当考虑规模、结构、多样性和复杂性时会面临的各种不利条件。但是，除了居住在某个地方的大量的志愿者共同努力外，谁还能更好地帮助我们了解这个地方？此外，除了通过精心设计的多元图形来揭示结构和帮助发现，如何能更好地探索在描述范围之内的某个地方的意义？

反思和结论

我们认为数据之美在于其深度。当先前隐藏着的结构和模式开始展现出来时，美丽就显现出来。这些模式激发了人们对数据提出新的想法和质疑。数据激发人们、鼓励探索并为探索者提供洞察力。只要存在空间数据，就可能呈现地理之美。

美丽的数据激励人们实现优雅的数据可视化策略，正如它也激励探索并回报那些探索者。可视化特别适合于探索地理模式，正如好几个世纪的制图学所证明的。

我们这里给出的例子说明了我们可以如何使用美丽的数据，如Geograph项目的数据的地理特征，从以一个视图的方式到使用为信息管理和检索的应用场景的描述来描述一个地方。复杂、数据密集且优雅的图形是该过程的重要部分。

我们广泛地认为美丽是一个实体的特征，它给人们提供快乐、意义或满足。对于数据及其表示，Geograph项目的各个方面都包含了这些特征品质。我们对包含高度的“数据/地理位置”比率，通过空间填充图形的Geograph项目的某些方面所做的可视化，是富于创新、有创意的，而且具备解决问题的基础。它有助于沟通同事间的工作，并开辟新的分析途径。我们将及时发布人们对大不列颠群岛的人文和自然地理这些数据密集的描述，作为我们所创造的最美丽的数据。

致谢

我们对Barry Hunter和Geograph British Isles(http：//www.geograph.org.uk/credits/2008-04-31）的贡献参与者表示衷心感谢，他们的工作基于Creative Commons Attribution-ShareAlike 2.5 License(http：//creativecommons.org/licenses/by-sa/2.5/）可以获取。

我们对Ross Purves和Ross Purves在Geograph项目中的工作付出也深表谢意。该项目是TRIPOD项目的一部分，由欧盟委员会支持，合同号是045335。

参考文献

[1]AndrienkO'G.，N. AndrienkO'J.Dykes,S.Fabrikant,and M.Wachowicz.2008."Geovisualization of dynamics,movement and change:key issues and developing approaches in visualization research."Information Visualization,v.7：173-180.

[2]Bertin,J. 1983.Semiology of graphics(W.J.Berg,trans.）.Madison:University of Wisconsin Press.(Oiginal work published 1973.）

[3]Cawthon,N.，and A. vande Moere.2007."The Effect of Aesthetic on the Usability of Data Visualization，"Proceedings of the 11th International Conference Information Visualization,IEEE Computer Society Washington,DC：637-648.

[4]Edwardes,A.，and R. Purves.2007."A theoretical grounding for semantic descriptions of place."M.Ware and G.Taylor(eds.）.LNCS:Proceedings of 7th Intl,Workshop on Web and Wireless GIS,W2GIS：106-120.

[5]Fabrikant,S.，M. RuoccO'R.Middleton,D.MontellO'and C.Jörgensen.2002."The first law of cognitive geography:Distance and similarity in semantic space."Proceedings of GIScience 2002：31-33.

[6]Fisher,P. F.1998."Is GIS hidebound by the legacy of cartography?"The Cartographic Journal,v.35：5-9.

[7]Geograph."Geograph British Isles-photograph every grid square."http：//www. geograph.co.uk/(acessed April 9，2009）.

[8]Goodchild,M. 2007."Citizens as sensors:the world of volunteered geography."GeoJournal,v.69：211-221.

[9]Hawgood,D.，D. Dunford,R.Farrow,B.Hunter,and P.Mayes."Geograph or supplemental."http：//www.geograph.org.uk/article/Geograph-or-supplemental/(acessed April 9，2009）.

[10]Kernighan,B. W.2007."A Regular Expression Matcher，"in Beautiful Code:Leading Programmers Explain How They Think,ed.Andy Oram and Greg Wilson，1-9.Sebastopol,CA:O'Reilly.

[11]Kosara,R. 2007"Visualization Criticism-The Missing Link Between Information Visualization and Art."Proceedings of the 11th International Conference Information Visualization,IEEE Computer Society Washington,DC：631-636.

[12]MacEachren,A. M.，and M.J.Kraak.2001."Research challenges in geovisualization."Cartography and Geographic Information Science,v.28：3-12.

[13]MatsumotO'Y. 2007."Treating Code as an Essay，"in Beautiful Code:Leading Programmers Explain How They Think,ed.Andy Oram and Greg Wilson，477-481.Sebastopol,CA:O'Reilly.

[14]Oram,A. and G.Wilson,eds.2007.Beautiful Code:Leading Programmers Explain How They Think.Sebastopol,CA:O'Reilly.

[15]Shneiderman,B. 1992."Tree visualization with tree-maps：2-d space-filling approach."ACM Transactions on Graphics(TOG)，v.11：92-99.

[16]Skupin,A. and S.Fabrikant.2003."Spatialization Methods:A Cartographic Research Agenda for Non-geographic Information Visualization."Cartography and Geographic Information Science,v.30：99-119.

[17]Tufte,E. R.1983.The Visual Display of Quantitative Information(First Edition).Cheshire,CT:Graphics Press.

[18]Wood,J. and J.Dykes.2008."Spatially Ordered Treemaps."IEEE Transactions on Visualization and Computer Graphics,v.14：1348-1355.

第7章　数据发现数据

Jeff Jonas和Lisa Sokol

简介

新一代的“智能”信息管理系统将不会依赖于用户向计算机提出一些异想天开的问题；相反地，它们会自动确定新观察到的现象是否可以表示有足够理由去做出某种响应，如给用户或系统发送关于潜在隐患或者危险的自动通知。

一个组织最多只会和它的所有成员的洞察力总和一样有智慧。这些智慧源于观察——通过各种企业系统收集到的观察，如客户登记系统、财务会计系统和薪水系统。每一次交易，企业组织都可以从中学到一些东西。因为在某个时刻获知了一些信息，才有了一些机遇来弄清该新数据的涵义并做出适当的响应，实际上这也是一种义务。举个例子，客户记录的当前地址变化是否表示该客户和你的最重要的50个客户之一相关？如果一个企业组织不能评估新的数据点如何实时地与其历史数据相关，那么该组织将会错失采取行动的机会。

当“数据可以发现数据”时，存在着通过洞察力找到用户的机遇。

数据如何发现数据是关于数据可发现性的声明，是对已有的信息和新的数据进行定位和关联所能达到的程度。可发现性要求能够召回相关的历史数据，这样新到达的数据可以定位到该位置，这和每个拼图游戏的方式相似，它们是通过其他相关的正在进行中的谜题来评估的。每个新的谜题都是增量式地构建在已知的方面，每个给定点在时间上和不断发展的拼图相关。通常来说，新的信息虽然对于构建全局大图很重要，但是它们自己本身并没有带来新的至关重要的信息。（相反地，有些信息可能会改变谜题结构，这种改变导致产生报警——结果发现是由于某个拼图放置错位）。正是在这个时刻，当新的谜题能够重新改变图像的结构，发现就产生了。实时发现不需要用户三思而后行并在恰当的时候提出正确的问题。

那些无法切换到“数据发现数据”模式的组织，其竞争力更差且效率将会更低。

实时发现的好处

立足于“数据发现数据”的高级信息管理系统，不会依赖于用户向计算机凭空提出正确的、相关的和即时的问题。虽然该技术会引发新的策略探讨，如允许哪些数据去发现哪些数据以及什么级别的相关度应该通知谁，以下是“数据发现数据”系统可以做什么的一些例子：

客户便利

在酒店辗转反侧一宿后，客人在早上7点时终于决定打电话要求晚点退房，并预约了中午的电话叫醒服务功能。客人刚沉睡不久，服务员无意敲门准备打扫房间，于是发生了令人不愉快的事情。遗憾的是，全世界的酒店旅客，这种骚扰经常发生。当数据发现数据，延迟退房和叫醒电话需要和服务员调度信息保持一致。这种“数据发现数据”的事件会触发一条自动的文本信息，通知服务员在下午2点前不要打扫该房间。

客户服务

某个用户对于一本即将出版的新书感兴趣，他在Amazon搜索该书标题，但是没有找到。该用户决定每个月都检查一下，直到该书出版。遗憾的是，该用户下一次检查时，发现该书不但已售空，而且需要重新预订，等待第二次印刷。如果能够做到数据发现数据，一旦该书开始销售，该数据检查点就会发现该用户的最初请求，自动给用户发送邮件，告诉他该书已经开始销售了。

加强儿童安全

有位妈妈想要确保她的小孩在步行去学校的路上是安全的。这位妈妈可能会搜索该社区的Web站点，确保没有登记的犯罪分子居住在孩子去上学的路上经过的小区。为了确定在孩子上学途中的街道上是否增加了一条新的犯罪分子地址，妈妈会每天都去上网检查一下Web站点吗？如果使用“数据发现数据”模式，在孩子上学路上一旦有新犯罪分子在某个小区登记住址，这种新的数据会自动链接到先前的查询。这位妈妈就可以立即知晓相关的发现。

越过“划分”，进行探索

有些部门使用“划分”，故意把数据隔离起来。隔离数据有助于防止高度敏感的数据意外泄露出去。尽管新的政策授权对信息共享，但是对高度分类的数据的传统保护措施，阻碍了部门发现两个“划分”正在处理相同的主题或者在主题上有交叉。其中一个例子可能是某个机关正在执行反恐，而另一个正在做反毒工作。部门有数百个“划分”，而一个人要在另一个“划分”定位相关数据是很不现实的，因为他永远都不知道其他人会有什么信息。如果数据发现数据，当有一条记录加入到反毒部门的数据库中时，如果该记录和反恐部门相关（如涉及同一个人的数据），系统就会立即发送通知给相关人士。

赌桌上的舞弊

这是一个真实的故事，坏人发现了好人的数据——导致一个意外的发现并产生令人惊喜的结果。

在20世纪90年代中期，美国很多新的行政管辖区承认了河船赌博(rverboat gaming)是合法的，路易斯安那州就是其中之一。一个新的游戏管辖区所面临的挑战之一是缺少一批有丰富游戏经验的当地雇员候选人。因此，新的管辖区必须培养一批有广泛的专业工作类别的当地劳工，岗位包括从交易员到监察室业务员。路易斯安那州的Bossier市就是这样的一个社区，它需要从无赌场业务过渡到赌场河船业务。

今天和任何其他赌场的任何其他日子没有什么区别。交易员正在监视着赌场的玩家。该楼层的主管正在监视着交易员和玩家。赌场经理正在监视着楼层主管、交易员和玩家。而监察室正在监视着每个人——甚至是该赌场的执行官。监察室有责任监视游戏的交易情况，不仅仅是为了保护赌场，而且也是为了保护客户。监察室同时聚焦于游戏的交易以及捕获其他犯罪行为的证据。例如，抢钱包的凶徒属于违法行为，因为他干扰了用户体验。

监察是最后一道防线。

在一间非常小的监察室里，安装着数百部摄像机（在拉斯维加斯的大型赌场有数千部）。一面墙上可能安装着20、30甚至40部的监视器，看起来像是在做犯罪现场调查(Cime Scene Investigation,CSI)。因此，就这么几个操作员审查进入房间的权限，怎么可能有意义呢？答案就是：绊网（暗线）和周密的浏览。

绊网(Tipwire)有很多种形式，包括从热线上的一条建议到一个楼层的主管要求监察室评价一个客户的打牌行为，例如，确定她是否在对桌牌计数[1]。这一天，监察业务员在随意查看——即执行所谓的随机审查——从一个表到另一个表，简单看一下，检查是否有不正常的，然后继续查看下一个表。

等一等！那是什么？简直不可能！悉心观察的监察业务员刚刚观察到一个玩家公然在轮盘赌桌(rulette table)上作弊。今天所发现的这个骗局称为“逾期投注”(pst posting)——某个玩家在轮盘已经停止滚动，停留到某个数字后才下赌注。“逾期投注”发生在当玩家注意到滚球已经停止到某个数字（如32），发现这个结果后，马上押注一个逾期的、必赢的赌注。“逾期投注”和桌牌计数不同，后者是合法的但是不鼓励，而前者实际上是舞弊，而且绝对是不合法的。

今天很特别的原因是那张桌子只有一个交易员和一个玩家。通常，“逾期投注”舞弊方式一般会涉及很多玩家——这些玩家一起努力阻止交易员监察到逾期投注的发生。这种团体活动可能会涉及同一张桌子的两个或者更多的“玩家”，他们看起来似乎完全不相关（比如，表现得彼此都不认识对方）。等到骰子滚到某个数字不动了，一个玩家（如一个看起来性情很好的妇人）会沿着整个桌子假装要下赌注。交易员当然会说：“女士，您下的赌注太晚了。”但是，她的这个下注对必输的号已经使该桌的其他成员（如一个看起来像摇滚队的、留着莫霍克发型的花花公子）对必赢的号下了一个迟到的赌注——而且避免了被交易员所发现。

安保人员得到通知，警卫小组也调动起来。该玩家遭受逮捕和拘留。怎么会发生这样的事呢？交易员显然很尴尬。在新的行政管辖中，这名雇员还是一名游戏新手，而且确实只受到过课堂培训。交易员清楚地表示，对于这种类型的诈骗，她只是听说过而已。然后，她说安全报警今天确实做得很不错。她非常尴尬，而且很快保证，在她以后的监察期间，再也不会发生这样的事了。

输入“数据发现数据”。

当骗子被逮捕时，他必须证明自己的身份。今天的骗子和任意某天没有什么区别，介绍了他的名字、地址、电话号码，以及一些其他信息。这种信息收集是通过一个标准的表单，包含一个签名（如果他们能得到），然后，数据进入企业安全逮捕处理系统。

当然，这个骗子和交易员的姓氏不同，否则事情就会变得非常简单。当然，该骗子的地址和这位交易员的地址也不一样。但是，他的电话号码恰巧就是该交易员求职应聘时用的电话号码。正在这个时刻，逮捕信息发送给逮捕处理系统，该系统是实现数据发现数据的次级系统[2]，它找到这个至关重要的发现，并生成一条即时报警信息：“5764号员工和44-00321号被逮捕人有相同的电话号码！”

长话短说，交易员受到对质、开始招供，于是该交易员和骗子一起被交给执法机关接受起诉。

需要明确说明的是，用户并没有收集之前作弊诈骗者的身份属性（如名字、地址和电话号码），并尝试搜索大范围的业务系统（称为联合搜索）。求职者、员工、会员俱乐部和逮捕处理系统等，这些系统甚至不能通过地址或电话号码来搜索——它们不是为搜索而设计的。

在“数据发现数据”的环境中，用户不需要主动搜索或者提出相关的问题。安全部门的用户在系统中输入他们学会的东西，然后通过其他企业信息资产来评估这种新的信息。发现新的信息需要和已有的数据相关，而且这种关系满足预定义的兴趣条件：有个“坏人”和某个员工相关。因为坏人和该员工共享一个电话号码（所以这个坏人认识这个员工），这样就满足该兴趣条件，并触发一条报警信息。如果发现这个骗子逮捕相关的数据和三年前某个宾馆的一个顾客相关，这种不满足兴趣条件的发现不会生成报警。

为了说清楚这个观点，我们现在假定这个“逾期投注”者提供的电话号码和这位交易员的电话号码没有任何关系。在这种情况下，就不会生成任何报警信息。交易员可能会受到某些质疑，但是显然没有足够的理由来起诉她。可能公司的安全部门会调查这个交易员，或者雇佣一个私人侦探来调查这两个人是否是朋友？谁知道呢？

但是，如果他们真的有关系呢？要是电话号码不匹配，系统认为他们没有关系，那么发生什么事呢？交易员继续从事交易，时间继续推移。如果六个月以后，交易员在她的人事系统中更改了她的家庭住址，而且她的新信息和那个骗子的地址完全相符，企业组织如何能够知道这件事呢？实际上，没有一个企业组织会发现这一点，除非它能够做到“数据发现数据”。一旦新的信息能够和之前已经结束的案件关联起来，该系统就可以检测到报警条件：坏人和某个员工相关。

顺便说一下，报警并不意味着真的有犯罪活动。但报警确实为企业组织集中有限的调查资源发挥了重大作用——在这种情况下，基于足够的信息条件，给更相关的可能事件报警。

其他例子是：在这个轮渡赌场行业，还发现和一个市场营销人员相关的骗局，这个营销人员提出了一个系统攻击、返还现金比赛的想法，（在营销活动中，你玩得越多，就会获得越多的积分，而且这些积分可以兑换现金），而结果是那些现金都是返回给他的室友。而在另一个案例中，某个人举办“一天一辆车”活动抽奖，系统发现中奖的人“碰巧”是他的妹妹（只是姓氏不同）抽中了该车，这两个家庭成员在现场时演得好像彼此从来都不认识对方。

通过“数据发现数据”的策略，以上三个骗局最后都真相暴露——所有这三个骗局都是在90天之内就被查出个水落石出！

数据发现数据非常重要的原因是因为信息到达的时间顺序是不确定的。想当然地认为事件是有序的系统和处理方案有一个致命的缺陷：无序的事件可能会提供企业组织足够的信息，但从来都没有基于这些信息真正采取措施。这一点在后面会谈及更多。

一个在美国有几千个连锁店的大零售商分析了它的历史数据，很震惊地发现它的每一千名雇员当中，有两名实际上曾经在该店偷窃过，并被逮捕。更糟糕的是，这些员工所被抓到的店和雇佣他的店是同一家店。虽然数据表现方式不同，一旦企业有了这些证据，就一分钟都不能耽搁。该连锁店需要立刻意识到这一点。

解决无序的数据点的传统解决方法十分繁琐。企业安全部门应该如何利用新的数据，使这些数据能够揭露一个员工可能曾经就是偷窃者？何时在人力资源系统中更新一条员工记录会导致企业安全部门重新评估它之前所有的调查呢？这种重新评估该如何结构化，这样当在内部调查数据库中，有新的或者更新的记录和员工相关时，企业才不会错过这些信息？一种策略是周期性地测试调查的数据库和整个员工数据库。另一种策略是企业安全部门定期重新调查每个员工。但是这两种策略都会错过一些重要的发现，因为定时（时间）是至关重要的。

即使是完美的算法，当它在基于完全重新设计的业务系统上运行时（如人力资源系统和内部调查系统），它依然会错过一些可发现的事件。如果为了确定两个人是同一个人或者相关所需要的数据存在于一个完全不相关的系统中，那么又会发生什么事呢？举个例子，想象从一个不相关的系统中得到了第三条记录，比如会员俱乐部登记系统，它反映了家庭地址和家庭电话号码之间先前未知的链接。什么样的企业系统会用于检测这种被我们称之为“非显著关系”(nnobvious relationship)的条件？

数据发现数据，包括非显著关系条件，要求人们首先解决“企业的可发现性”问题。

[1]值得注意的是，赌场监督的作用是提供情报。他们报告自己的观察和发现，但是没有执行力；执行力是安全部门所发挥的作用。

[2]这种被称为“无明显的关系意识”(Nn-Obvious Relationship Awareness,NORA)技术，由Systems Research＆Development，即SRD公司（由Jeff Jonas成立）为拉斯维加斯的游戏行业开发。SRD公司从那以后被IBM收购，现在是IBM的实体分析部门的一部分。这里有一些额外的信息——IEEE论文：Threat＆Fraud Intelligence-Las Vegas Style(http：//jeffjonas.typepad.com/jeff_jonas/2006/11/ieee_paper_thre.html)。

企业的可发现性

当企业中有新的信息到达时，不论是来了一个新员工、员工的某条记录更新、剽窃者信息或者是会员俱乐部登记，人们需要记住其他什么组织数据和这个信息相关。

企业发现的一个常见办法是采用“联合搜索”技术，把查询传递给每个相关的业务系统。正如我们所证明的，这种方式对于数据发现数据系统并不适用。可发现性，尤其是在大规模、实时的环境中，目录是必需的。

联合搜索不是万能的

企业组织有众多的业务系统，每个系统都有自己专用的业务功能、定制的信息结构、分析和报告。二级数据聚合是很常见的，而且包含如数据仓库、业务数据存储和数据集市。数据聚合存在无数的信息孤岛，每个孤岛都和特定的任务或功能相关。

传统的联合搜索系统涉及用户查询每个孤岛的数据库，获取相关内容。更复杂的联合搜索系统使用智能中间件，为数据库的每个查询做代理；智能中间件是一个模型，在该模型中，中间件通过自动结合各种信息来处理查询，并对查询的结果进行编制，返回集成的结果集给查询者。

在需要信息的时候，联合搜索“即时”收集跨孤岛的数据。虽然这种类型的联合搜索在一些场景下是可用的，但是无法很好地适合于高性能的企业可发现性，后者需要根据数据发现数据来生成。

有两种主要的原因使得联合搜索无法扩展：

现有的系统通常不包含高效定位一条记录所需的必要的索引。如支付系统通常会包·含预先构建的索引（对数据定义指针），从而有助于在员工号码、纳税ID号码和名字进行查询。支付系统很少会包含高效的方式来定位地址或者电话号码上的记录。假设新发现的赌博诈骗者暴露了他的地址和电话号码。我们的支付系统保留所有员工的电话号码和地址记录（毕竟它需要发送支票）。但是该支付系统无法简单地生成（如果它可以）员工的列表，这些员工和之前提到的赌博诈骗有相同的地址或电话号码。即使在支付系统中为员工的电话号码中创建了一条索引，但是它仍然无法使人们在相同的系统中定位紧急联系电话号码。如果我们无法对赌博诈骗者的验证信息和所有相关的员工数据进行比较，我们还是错过了发现员工和诈骗者之间的联系。

如果某个字段不在索引中，在数据库中定位一条记录的方法称为“表扫描”。在表扫描过程中，对搜索的值和表中每条记录的值进行比较——从数据库中的第一条记录开始比较，到第二条，等等。因此，数据库越大，每次搜索花费的时间越长，因而主服务器系统上的计算压力就越大。

更糟糕的是，联合搜索需要递归处理（某些情况下需要重复一些步骤），这对于分·布式查询环境简直是一场噩梦。假设你执行一条联合查询，是为了发现和某个特定的人相关的企业记录——也就是说，根据某个特定的人的名字和出生日期开始查询。如果联合查询返回这个人的一些新的属性，如一些地址和电话号码，那么你就了解了一些新的东西。为了更全面地了解，需要最大化利用所了解到的关于这个人的新数据，也就是说，初始化另一个企业范围的联合查询，防止基于这些新的数据点，定位额外的数据。因此，如果第二次的联合查询发现了另一个地址，拼写名字的多种方式，以及一两个别名，那么又会发生什么情况呢？更具体全面地说，每次了解一些能够发现先前丢失的记录的知识，发现过程必须执行另一个企业范围的联合查询。

下面这个真实的案例强调了这一点。某个企业组织（一个商业实体，不是政府）有2000多个数据库。用户查询被定向到这些数据库中来收集相关的记录。为优化搜索过程而设计的优秀的中间件产品需要花很多年的时间，以及几百万美元的成本。这种智能的系统会识别哪个数据库应该处理哪个查询，确定合理的数据库访问方式，把查询同时传播给所有相关的集合，并汇集了解到的知识。但是，这种联合搜索方式永远都无法克服一个严重的设计缺陷：每次发现了解一些知识（如一个别名或者一个新的电话号码），优秀的中间件必须重新把这些查询发给很多数据库。这种递归过程运行在大规模的计算机集群中，最终需要定制为每8分钟停止处理。注意下一次的递归查询可能最终会发现一条重要记录。

但是，需要8分钟！这意味着用户或者系统只能等着，无法执行任何操作，因为在这段期间内无法获取到任何答案。然而，在数据发现数据的情况下，数据是个问题。这意味着每当有新的数据到达，在处理这些数据之前会有最多高达8分钟的延迟。想象一下，在整个企业网络，每秒提交成百上千的联合查询的规模下，通过无数的业务系统递归执行，使用联合查询这种方式是多么得不切实际！

如果以上提到的因素还不具有足够的吸引力，联合搜索的致命一击是所有必须搜索的系统在物理上必须是可访问的，而不是处于维护或者备份状态，或者处在周期性的半夜或者月末的批处理过程中。当然，连接性也必须是完全可运作的。考虑这些需求以及由成百上千的涉及建筑、时区和大陆的系统组成的组织。联合搜索无法支持“数据发现数据”的任务，因为它无法大规模地基于企业的发现力来发布数据。

目录：无价之宝

想想一个图书馆。把图书馆的地板、走廊和书架想象成存储信息的孤岛。有价值的信息被搁置一边，等待着我们去发现。人们不需要为了寻找特定的一本书而走遍图书馆大厅。相反地，人们会使用卡片分类目录，在主题、标题和作者上执行交叉索引来帮助查找相关的文档。目录、索引和分类目录基本上指的是相同的东西：都是用于定位其他东西。

定位器的例子包括图书馆的卡片目录、电话号码目录和eBay等。对于每种定位器，目录相当于定位器服务：当对它们提供一个或者多个查询项时，这些定位器会返回引用信息（指针）。在图书馆，卡片分类目录是为了促进高效的企业搜索的一个特定目录，它为用户提供指向文档的特定的指针（如：使用Dewey十进制系统）。当提供给用户一个指针后，就变成“联合搜索”。注意（有用的）联合搜索和（无用的）联合搜索之间的区别。

G公司的搜索也不是扫描整个地球来获取搜索结果；相反，它是搜索G公司创建的一个特定目录，然后把搜索结果指向真正文档的指针返回给查询者。

企业范围的可发现性的唯一可扩展解决方案是使用目录。这并不奇怪。因此，一个特定的目录是最根本的部分，它允许“数据发现数据”系统有规模地处理可发现性和实时地确定相关性。

不是所有的目录都是等同的。传统的“上下文无关”的目录和能够累计并持久的上下文的目录之间存在很大的区别。上下文相关的目录使得数据发现数据可以通过非常令人意外（不显著）的、实时、大量的方式，而且效率非常高效。

上下文无关的目录是最常见的目录类型：每个文档的索引和所有其他文档不相关。换句话说，新的企业交易（文档）更新目录，并不关心该交易（索引的元数据）和其他交易之间的关系。上下文无关的目录是为了给用户提供最基本的定位文档的能力而设计的（如所有和“Billy the Kid”相关的书）。

语义上一致[1]的目录是那些试着利用同义词的目录，使用不同的单词来表示相同的东西。这意味着用户想要寻找一个东西（如“Billy the Kid”）应该能够找到其他“相同”的东西（如“William Antrim”，他的一个别名）。当一个新报告的实体引用了之前观察到的实体，语义一致的目录能够做出识别。包含语义一致的数据的目录可以看成是图书馆的一个卡片文件，这有一个很大区别：和类似的实体相关的卡片被胶合在一起。这意味着如果一个搜索定位到了一张卡片，“作为奖励”，它不需要付出额外的努力就可以发现所有其他相关的卡片。最显著的是，在这些胶合在一起的图书馆卡片中，有些甚至不包含被搜索时的原始数据项。

坦白地说，这一切看起来很神奇。当试着去发现该企业对于某个Email地址的了解程度时，查询者会发现和该Email地址相关的记录以及关于这个人在企业中的其他记录——举个例子，某个会员俱乐部，虽然实际上它的记录中从来都没有包含Email地址。语义上协调身份的算法（如人们或者组织）有时被认为是身份识别[2]。确定多个实体什么时候是相同的实体的算法需要非常深入细致的讨论，因而超出了本章讨论的范围。

语义一致且“有关系意识”(rlationship-aware)的目录是提供了更高程度的上下文的一种目录类型，它通过允许用户发现额外文档，如那些和私人相关的关联（如Billy theKid就是William Antrim)。可能知道实际上确实有William Antrim可能也是很重要的，而且这个人碰巧是Billy the Kid的继父。对这些达到可视化的方式是在脑海里画一张图书馆卡片目录，其中一些卡片如前所述，已经通过胶带系在一起，还有一些线条连接到一些卡片捆绑(bndle)到其他卡片以及其他卡片捆绑。人们可以搜索任意术语的某个集合，并定位一个卡片捆绑，并且在那时弄清楚该捆绑是如何和其他捆绑关联（相关）的。有些关联的线条比其他线条粗，这表明它们关联关系更强。通过一条线找到另一个卡片捆绑，人们可以马上看到是什么线条连接到下一个捆绑。通过这种方式，人们可以观察到这些卡片捆绑分离的程度，如Kevin Bacon所提出的六度分割理论。

当思考“数据发现数据”系统时，必须记住当有新的交易发生时，可能是把当前已经存在的实体增加到上下文中，那么就有可能会发生由于捆绑和线条的重新组织，新的信息改变了图片的大小。

持久性上下文(Prsistent context)是个术语，用于表示一致的、“有关系意识”的当前状态。从本质上讲，它指的是不断变化和越来越复杂的当前状态。持久性上下文是保存了事物间相关性，优于在联合搜索系统中用到的“即时”性上下文(Jst-in-time context)。

持久性上下文（语义上一致且“有关系意识”的路径）能够促使高性能的发现、流语境化以及实时检测相关性。同样值得注意的是，如果相关性检测可以在获取数据时发生，其计算代价是最低的。从这方面来看，图书管理员（负责把新数据补到目录中）是第一个发现新到来的数据和待发布的数据是否有足够的相关性。

意识的溪流胜过沸腾的海洋。想象这一点的一种清晰的方式是想象一个企业组织有4 exabytes（1 exabytes=2^16字节）的历史数据，每分钟接收5TB的数据。你觉得他们是在周末启动一个进程，用来发现发生的事吗？在整个地球上，可能没有足够的计算机或者能源来做这件事。（注意：规模上设想的行为在设计非常高效的系统时，被证明是非常有效的。）

[1]定义为“当两个东西描述上虽然不同，但实质相同，能够做出识别”。

[2]不要把身份识别和类似的有时被称为匹配/归并或归并/清洗的一组方法混淆。更多关于它们之间的区别，可参考文章“Entity Resolution Systems vs.Match Merge/Merge Purge/List De-duplication Systems”(htp：//jeffjonas.typepad.com/jeff_jonas/2007/09/entity-resoluti.html)。

相关性：什么是重要的以及对谁重要

现在，有大量的数据涌向我们。花点儿时间来评估在你周围环境的所有视野和声音：背景是计算机的嗡嗡声，音乐的播放，电视上的漫画以及孩子们想要偷窃彼此的玩具。人类本身有自己内部的相关性检测算法，可以评估所有展现的数据，并且当某些观察使我们注意并且可能做出某种反应时，会提醒我们。就如忽略小孩们以及他们的吵架，直到该吵架上升到暴力威胁。正是某个小孩的一个动作，提醒我们是时候开始出面调解。

人们如何使用技术来计算相关性，以及什么时候允许它自动注册一个报警是至关重要的。在技术方面，目标是一种状态，在该状态中，报警队列中的下一项是审查时最重要的下一个项目。没有理由产生的报警比现有的可用于应付它们的资源还多。例如，风险评估引擎生成的报警数必须被配置为和某个人的个人风险、编制和反应能力相适应。如果资源增加了，人们可以提高报警的灵敏度。

各个组件及特殊考虑

智能的“数据发现数据”的环境必须包含8个最基础的组成部分：

·观察的存在和可用性；

·能够从观察中提取特征并对特征进行分类；

·能够有效地发现相关的历史背景；

·能够对新的观察做出判断（相同或相关）；

·当新的观察推翻先前的判断时，能够做出识别；

·能够积累和坚持主张；

·能够识别相关性/洞察力如何形成；

·能够通知相应实体的洞察力。

观察的存在和可用性

如果没有数据，人们就不可能会理解观察；而如果有数据，为了使这些数据某天有潜在的利用价值，那它就必须通过一些传感系统来“感应”（收集）。而且即使数据被收集，人们为了还有一线希望能够理解它，必须能够访问它。

能够从观察中提取特征并对特征进行分类

为了避免不必要的争论，我们说一粒沙子包含的用于抽取和分类的特征太少。沙粒往往具有相同的颜色、大小、重量、形状等。因此，缺少可区别的特征后期将会妨碍人们识别相同的数据（语义一致）。其要点是，为了把数据放到语境中，人们必须能够抽取和分类它的关键特征。当地址信息包含在一个列中，而姓名在另一个列中时，结构化数据是非常容易的。无结构化的数据，如新闻报纸和博客，花费的时间更多；提取正确的名字和地址是非常具有挑战性的，它通常被称为实体提取。从视频中提取特征，如为汽车拍照的读者，可以在某些情况下进行。

长话短说，人们如果想要把观察结合到上下文中，必须能够对观察的主要特征进行提取和特征分类。

能够有效地发现相关的历史背景

随着新的观察到达，抽取和分类的关键特征被用于查询上下文相关的历史数据（我们称之为持久性上下文），用来发现这些数据是否适合。为了能够实时发生，支持大体积的数据流，这种发现需求必须是极度快速的。

能够对新的观察做出判断（相同或相关）

当有新的观察根据历史数据语境化(cntextualized)时，该算法必须能够做出以下断言之一：1）新的实体，是其类型的首例（如一个新人）；2）已知的实体（如是我们已知的关于某人的观察），在这种情况下新的实体通过已有实体“分解”；3）该实体（新的实体或者是已知的实体）如何和其他实体相关。

因此，存在这样的观点，正如人们把拼图玩具拼在一起，其结论是没有什么可以做的。正是这时，人们抛弃了当前项，不管它曾经在什么位置，而且把注意力转移到下一个拼图模块（观察）。注意：可能你的最后一次判断是错误的，但是关于这一点，你不太可能会发现它，除非有新的拼图模块暴露出了这个潜在的错误。

当新的观察推翻先前的判断时，能够做出识别

有时一个观察包含新的信息，证明之前的判断必须完全推翻。可能这种新的信息证明之前认为不相同的实体实际上是相同的实体。相反地，一个新的观察可能会暴露人们之前认为相同的实体，而现在坚信它们一点都不相同（例如，新的数据点说明了我们数据库中的两个Bob Joneses不是同一个人，而实际上一个是大三的，一个是大四的）。

使用新的观察推翻先前的判断是语义一致算法最复杂的方面之一。但是如果没有这个关键特征，数据库会随着时间流逝，和真实情况越来越不相符。它的缺点是使用周期性的数据库重载来纠正这种问题。而且对于非常大的数据集，很显然这会带来可扩展性噩梦。

能够积累和坚持主张

当完成断言过程——换句话说，当新的观察被规约为断言（新的、相同的或者相关的实体），而且新的观察是用于弥补和之前相同的断言——新学到的知识必须放到数据库中，这样下一次交易可以从新的知识中受益。在某些方面，这感觉上很像最基本的增量学习系统。

能够识别相关性/洞察力如何形成

只有当新的观察应用于历史数据，这样无法保证更多的计算，该系统该“扪心自问”，“我是否学会了一些真正有用的东西？”，它和一个人不断地查看在每次放上一个拼图模块时会有什么发现很相似。

我们需要做的工作包含检测预定义的兴趣模式。例如，发现一个好人是否认识一个坏人是相关的，或者某个人每天的现金交易是否超过1万美元。

但是，新的相关性参数可以基于外部过程来设置，它可能包含人类洞察力或者次要的模式发现/数据挖掘引擎。

能够通知相应实体的洞察力

当检测到某方面的洞察，应该通知谁或者哪个系统？在我们现有的实现中，这是小事，因为每个相关性规则（比如，如果一个可能的主顾是我们的最高的50个客户的一位很亲密的同事），以及传播规律（也就是说，发送一条友好信息给赌场主人）是同时建立的。

隐私考虑

智能系统与那些可以执行“数据发现数据”的其他系统一样，要求非常密切地关注隐私权和保护公民自由。如何构建和部署这些下一代系统，以及如何使用管理策略（包括责任和监督）都值得人们密切关注和热烈讨论。一些核心问题包括：定义什么样的数据需要为可发现性构建索引，如何把这些数据整合在一起（比如，什么是关系？），什么是相关性，应该向谁揭示什么样的相关性，谁可以搜索索引，系统如何对未授权使用进行监视，以及如何检测错误和纠正错误。

幸运的是，基于目录的模型有很多关于加强隐私保护的很好的特征，包括：

·敦促多方之间分享更多的数据被传送更少的信息到更少的地方（卡片目录）所取代。

·可以通过一致的方式，对人们的搜索操作以及找到的结果以日志形式记录下来，这样有助于更好地实施责任制和监督制[1]。

·不同方之间的信息共享已经减少为只需要分享他们需要知道的东西（通过直分享必须分享的信息使得分享变少了）。

·可以使索引“匿名制”(aonymized)，这意味着无意的暴露甚至是那些索引中数据量很小的元数据的机会也大大降低[2]。

[1]防篡改的日志记录通常也被称为不可变的审计日志。Markle Foundation在关于国家安全，尤其是非透明的政府系统方面，发表了一篇有趣的论文，可以浏览：http：//www.markle.org/downloadable_assets/nstf_IAL_020906.pdf。

[2]更多关于“匿名制”的目录，可以在Jeff Jonas和John Karat所著的《Emergent Information Technologies and Enabling Policies for Counter-Terrorism》（编辑Robert L.Popp和John Yen，2006年Wiley-IEEE Press出版）的“Anonymized Semantic Directories.A Privacy-Enhancing Architecture for Enterprise Discovery”一章中找到(htp：//www.wiley.com/WileyCDA/WileyTitle/productCd-0471776157.html)。

结束语

“数据发现数据”系统决定了新的观察是如何和已知的事务相关，并检测出值得特别注意的相关性/洞察力。这种洞察力通常实时发生，因此能够做出实时反映。

这一新兴的技术将最终把一个组织和其他组织通过一系列的企业利益区别开，包括更好的识别机遇到风险估计。

数据发现数据很可能会成为下一个里程碑，下一代的高级分析机制会从中受益。这种新的模式将不可避免地带来更智能的系统的兴起，甚至可能还会推动“认知计算”(cgnitive computing)的发展。

第8章　实时的可移动数据

JudValeski

简介

长期以来，应用数据一直被“禁锢”。最初，这些数据被局限在世界各地“孤立”运行的系统中。CAT-5以太网络、IP、路由器、DNS和Sockets诞生之后，机器之间开始能够互相连接。机器间的互通使得数据可以在应用程序之间自由传输，最终这些数据也变得更加有意思。数据超出了其原始的应用程序范围，开始进入各种各样的新场景之中。一夜之间，随着互联网的兴起，客户端-服务器计算得到了迅速发展，那些“孤立”的计算机突然之间互相连接在了一起。以HTTP做为通信媒介和文本作为语言，数据“流动”了起来。

在过去大约15年里，主要的数据流可分为两类：色情和普通的消费商务（包括通过代理的广告），二者都很好地遵从20世纪90年代初期兴起的传统的由“Web浏览器”以及相应的服务器所构成的数据传送模型。最近，第三类数据——社交数据开始兴起。人们开始把自己的社交移到网络上，这创造了新的数据类型，以及新的不完全遵循基于浏览器模式的访问需求。社交数据就在这里，而且对它的需求正当其时。2009年1月14日的PEW报告《Adults and Social Network Websites》指出，18～34岁的在线成年人，50%以上有自己的社交网络个人信息[1]。

在本章中，我将讨论这一演变过程以及消费趋势信息即社交数据的内在问题的当前解决方案。虽然我们已经经历了跨越式发展，但这过程中丢失了一些基本的编程原则：事件驱动的体系结构和数据结构的可预测性。通常，当前问题的解决方案就在眼前，而且“奥卡姆剃刀”(Ocam's razor)理论也表明这些解决方法通常很简单。真正通过180角度HTTP转换并采用POST方法，“实时”可以正式成为社交数据词汇的一部分。作为中介，Gnip(http：//www.gnip.com)在异构社交数据和消费应用之间架起了便捷的桥梁。

[1]http：//www.pewinternet.org/Reports/2009/Adults-and-Social-Network-Websites.aspx

前沿技术

当前的社交数据被“扭曲”来适应那些与最终用户所喜欢的行为需求并不完全匹配的传输和展现方式。当一个平台是经过很长时间逐步建立起来时，往往会发生这种情况；人们发现新的方式来利用平台，即使有时必须为此做出某些牺牲。详细说明当前框架的主要方面是描述这种“扭曲”了的杠杆作用的最佳方式。

传输

有很多方法可以把两个Socket连接起来，并在它们之中移动数据。但是，在可预见的将来，只存在一个可能：HTTP协议。总存在特定的情况，可以使用更高效的协议来优化该特定情况下的数据传输，但是HTTP协议仍然是无处不在的，虽然它比任何其他东西给地球和卫星的防火墙带来的漏洞都多。因此，这就是HTTP协议。如果有什么事不能通过HTTP协议来完成，那这件事就需要重新考虑一下了。为了说明这一点，我们具体看一下消费电子设备的情况。这些产品很多都很酷，但是只有那些很棒的设备才包含HTTP客户端（如iPhone或者一些基于Android的设备）。真正令人惊叹的设备甚至包含了HTTP服务器(Nkia的S60上可以运行HTTP服务器）。正如任何完善的规则都有例外，HTTP协议也不例外。它是无状态、重量级的，当你需要以快速、有状态形式完成一些任务时，这两个特征可能会成为障碍。以下协议是这些例外的例子。

可扩展消息处理现场协议(XPP)

AOL的即时通信产品是该领域在实时社会通信产品上的首次亮相，并且发展异常迅猛。人们想“聊天”。不久，人们就想通过P2P进行聊天，或者通过其他服务，XMPP就这样诞生了。开始的时候，作为一个标准的传输框架和用于描述社会交互、展现数据的标准格式，XMPP被拼凑成了一种通用的消息框架，使用范围广泛，包括从IM到通用消息工具PubSub。在2008年中期，Cisco收购了Jabber.com(XMPP安装商业化的领袖），网络路由器也将很快装配很小的、烧制了专用的XMPP处理程序的硅芯片，供全世界使用。

BitTorrent

当你的用例是高度定制时，最好能够把数据处理流程调成“最优”。砍掉这些琐碎的东西和多余的载荷，使得任意的客户端和服务器之间的数据传输通信只适用于客户端和服务器紧密耦合并且协作良好的情况。BitTorrent在分布式的一组节点上来回移动大量的二进制数据块的速度是异常惊人的，这是对高度定制的用例进行优化的一个很好的例子。

私有权/P2P

HTTP协议的一大不足是每个有效的请求/响应对包含了与之前相同的数据头集合，即使这对操作可能是某个特定的客户端和服务器的第n对的交互操作。如果能够在初始阶段管理好这些交互，使得数据通道除了位操作之外其他什么都不做，那岂不是更智能？确实如此。这个原因以及很多其他原因，驱动着“点到点”(Pint-2-Point或Peer-2-Peer)协议的很多实现，这些实现每天传输着大量的数据。

选择HTTP协议作为传输方式，这将带来一个问题：它对应的格式是什么？当然，如果一个传输协议在过去几年中已经变得无处不在，这个过程中基于其传输的数据也必然已经被“净化”了。

格式

XML为进一步实现数据格式化打下了基础。数据应该基于哪一个层次格式来通信的问题基本上已经解决了。现在对由XML衍生出的数据格式的讨论是所有一切讨论的起始。虽然现在有很多开发人员觉得采用XML格式是理所当然的，但是值得注意的是这种数据格式非常臃肿。XML基于文本的格式，在定义上非常有描述性，因而它通常不是用于通信的最佳格式。每当我参与现代协议和格式的讨论时，我就想起AOL专有的FDO模型。FDO是二进制的、紧凑的以比特存储的协议/格式结合，为300波特的现代连接进行了优化。幸运的是，网络连接已经发展到今天，宽带速度已经成为规范。

如上所述，正如HTTP协议一样，XML的劣势也是它的优势。如果我们在过去15年学到了些东西，那便是无处不在的可读的协议和格式——我真的是这么认为。一种格式越是被人们所理解，它就越可以进一步走向创新之路。当调试程序的唯一方式是通过解密工具把数据转换成用大脑可以理解的东西时，人类创造性和通信之间就开始存在严重的障碍。“成功的数据”可以为程序员所理解，而“失败的数据”只能通过计算机来处理。注意：有效载荷格式和元数据格式（通常是XML)之间存在区别。高清晰的视频数据（如最近的高清晰的轰动电影）占用了大量的物理空间，而且人们无法阅读它；我把这种数据称为有效载荷格式。描述性的信息，比如视频的元数据，是相对比较紧凑的。在网络中，通过有效载荷格式传输胜过元数据传输（只要问问你最喜欢的网络服务提供商它占多少比例就知道了），但是这种格式的数据在被计算机解码之前，相对显得呆板无趣。

接口

接口(AI)使事情变得开始有趣。一旦每个人都认识到大量的HTTP事务像传统的API调用一样在网络上无处不在，REST协议为我们把Web作为大分布式应用奠定了基础。Roy Fielding是HTTP1.1协议的贡献者之一，他教会我们如何像表示状态转换(Rpresentational State Transfer,REST)那样描述事物的思想框架。

开发者终于可以停止挣扎于使用CORBA和DCOM模式来把他们的逻辑和数据分布到联网的计算机上，转而借助于标准的HTTP和基于文本的工具来实现应用程序间在传统的Web应用层以下进行交互。

这导致你能够想到的几乎所有We b应用的API呈爆炸式发展，如图8-1所示。API成为每个产品经理必有的东西。这既是件好事，也是件坏事。从好的方面来看，成千上万的产品公开它们的数据，全世界都能够访问。Programmable Web(http：//www.programmableweb.com/）记录了好几百个这样的产品，而且随时可以使用。从坏的方面来看，很多API很仓促地开发出来，没有考虑到它们可能会如何被使用。其结果是由于API调用程序根据性能或者功能特征，带来从WAN到各种可能满足或可能不满足你的期望的不同终端范围的API调用高潮。很少有Web API提供服务层协议(Srvice-Level Agreement,SLA)，如果真的有这样的API，其状况通常是很悲惨的。

图　8-1：在过去几年公用的API数量增长情况，数据来源于Programmable Web网站

为了使某个应用的数据和功能能够通过API获取，不论是真正的还是假想的需求，都可以在网络上创造一些有趣的挑战。

如果查看今天的API在网络上是如何使用的，那么你会发现它们看起来是随意传输数据，且没有严格的SLA。在规模上，我们第一次有软件可以非常有规律地和其他软件进行集成。这种集成方式称为“混搭服务”(mshups)，因为它们把数据从各个服务中“混搭”在一起。越来越多的终端用户期望数据在他们的社交应用能够具有实时性，但是支持实时数据传输的基础设施还远远不够丰富。在实现实时应用时，软件工程师无限复杂的创新性已经为应用提供实时通知产生了很多不同的模型（如“Comet”、“Web Hooks”，以及各种“PubSub”消息通知系统）。这些模型其中有些行之有效，有些则没有。不管你的方法是什么，解决方案在根本上没有真正的“魔术”；相反，饱受考验的框架通常都是在“幕后”一直发挥着作用。

在应用软件中有两个基本的时间流处理(tme-flow-processing)原语(Pimitives)：轮询(Plling)和事件(Eents)。应用程序根据时间来执行，而用户交互的应用程序需要用户或者另一个服务的输入。该输入通过轮询或者事件方式在应用中执行。值得注意的是你可以通过以上任意一种模式来实现，在计算机课堂上应该讲解到使用它们的一些绝妙技巧，我不在这里赘述。这里重要的是定义关于应用程序如何收集它的相关输入的更高层的概念。使用例子来说明表达这两种模型最简单。

轮询

轮询是通过应用程序不断询问中断是否有变化来实现的；如果检测到变化，程序会执行定义好的动作。一个常用的类比是酒吧服务员。想象一家酒吧，有1个服务员和10个顾客。如果这个服务员是轮询模式的服务员，他会不断地询问每个顾客他们是否要喝酒。在整个酒吧询问“要喝一杯吗？”“要喝一杯吗？”“要喝一杯吗？”“要喝一杯吗？”等。不时地，其中一个被询问的顾客会回答“来一杯”，则该服务员就先不询问了，给顾客倒一杯酒。轮询的方式实现起来美丽简单，但是通常这对于客户端和服务器都是非常低效（正如类比所示）。

速率控制[1]

提供API为开发者进行访问的方式来展示你的在线服务，通常可以很容易实现；而基于应用程序构建一些框架并把数据连接起来就不是那么容易了。但是，一旦把API开放出来，你就无法控制这些API将会如何被其他应用程序使用。对程序的完全控制已经保护代码运行了几千年的时间。因此，构建一个大规模的“Web API”（即“Web服务”）需要付出慎重的考虑、熟练的管理和业务操作。

如果有人幸运地构建出一个令开发者很感兴趣的API，因此其轮询负载将变得很大，在应用上处理负荷增长的最简单的方式是通过IP或包头、通信层的访问，从而限制API的使用。虽然这种处理方式解决了伸缩性问题，但是它中断了为取悦用户而集成这个API的程序的处理流程。开发者不希望在他们的应用中构建“节流层”(Trottling Layer)，他们只是希望执行习惯性的调用，使得系统能够“工作即可”。实际上，在系统中解决数据访问问题需要花费的精力比基于IP的速率限制还要多（即“节流”），但是我们通常无法投入足够的时间来调研真正的解决方案。一些社交数据的应用已经投入精力来解决这个问题，他们的API能够承载高负荷访问（如Digg)。但是绝大多数应用还不具备这一特点。

做正确的事

然而，包含大量社交数据的流行的社交应用程序在基础设施上不应该要求有庞大的资金投入，也不必是具有很强横向扩展性的应用。这些方面都会抑制产品的创新，耗费大量的资金。相反地，利用饱经时间考验的“事件”原语，简单地说是通过有序方式。大量规则说明了访问社交数据的90%以上的调用是不必要的。绝大多数时候，这些数据甚至是无法访问的。仍然以之前的酒吧服务员为例，当顾客回答“不喝酒”时，会由于被服务员不断地询问相同的问题而开始变得不耐烦。

为了更新某个用户拍的照片，不断地询问Flickr是一种资源浪费。相反地，如果当用户更新了他的照片，Flickr系统通知你的应用程序来调用，这种处理方式就会更高效，对用户更重要以及更及时。

每加仑0英里的效率

我将使用众所周知的“Flickr,Friendfeed的例子”来强调在获取社交数据时，使用轮询方式的低效性。总体来说，社交数据是由用户生成的。这意味着依赖于利用社交数据的社交应用是基于人们的行为，而不是更容易预测和管理的计算机的行为。当我们开发的软件的价值完全是人类活动的某个功能时，事情开始变得有趣。这个例子很有趣，因为发布者(Fickr)和消费者(Fiendfeed)都对用户的笔记留言进行比较，这能够对通过轮询方式异步传输的“外部调用”(ofbox)的本质问题产生有价值的洞察。

在2008年7月，Even Henshaw-Plath和Kellan Elliot-McCrea在俄勒冈州波特兰(Prtland)市组织的OSCON(Open Source Convention,O'Reilly的开源集会）会议上发表了演讲“Beyond REST?”(htp：//www.slideshare.net/kellan/beyond-rest)。在该演讲中，他们透露为了确定Friendfeed的4.6万的用户账号在最近24小时内是否上传相片，Friendfeed每天会请求Flickr的API 290万次。而它的4.6万的用户，其中仅仅7000个用户在这期间内会访问Flickr，可能会上传照片。

一条著名的社交应用规则，称为“1-9-90”规则，在过去几年中开始兴起。其要旨是，如果你考虑社交应用100%的用户基础，其中1%的用户对核心数据有贡献（如“上传一张照片”），9%的用户参与贡献数据（如“把一张照片标记为最喜欢的”），而90%的用户仅仅浏览这些数据（如“视图照片”）。如果保守地把该规则应用于Flickr/Friendfeed范例中，则可以说明在某一天有7000个用户访问了Flickr，有700个用户会上传照片。这意味着Friendfeed请求Flickr接口290万次，只是为了知道他们的700个用户确实做了某些更新。这种方式“命中”的最高比率是0.02%，即4000次请求发现1条更新。

把Flickr/Friendfeed例子和酒吧服务员的类比结合起来，意味着轮询方式的酒吧服务员为了使得顾客需要酒时可以实时得到，需要询问这位顾客4000次“要喝一杯吗？”，他才会回答1次“来一杯”。

一方面，应用程序调用本地的“内部调用”(O-box)API来“吸纳”这种低效性。另一方面，应用程序执行Web API调用的效率也非常低下。如果你根据当前所有的社交应用来考虑这个例子，其代价确实会非常令人震惊。

事件驱动型的软件可以驱动应用程序达到实时性，而社交数据在本质上就需要实时通信。通过在线银行软件查看某张支票账户清除了我的支票账户的操作，“拖延”一两天才能看到是可以容忍的；但是一个年轻人要看到他最好的朋友在春天拍摄的照片的请求，需要被立即处理。

事件

事件驱动型的架构和轮询驱动型不同，它采取了不同的解决方案。和酒吧服务员不断地询问顾客们是否需要喝酒不同，这种模型的酒吧服务员仅仅站在吧台后面，等待顾客告诉说他们想要喝酒。虽然事件处理通常更有效，但是考虑到具体的“事件”概念本身，它确实要求一些额外的代价。事件需要被消除、捕获；事件处理框架必须存在，这导致了事件处理方式的复杂性。轮询方式可以简单地通过线性、过程化的循环来完成，而这种简单性也正是它被过度滥用的一个关键原因。

从侏罗纪时期开始，轮询和事件这两种执行流规范奠定了软件开发的基础。客户端和服务器软件都依赖这两种模式来控制执行流以及应用的行为。操作系统通过恰当地运用它们，提供流畅的用户输入和交互体验。作为一个开发人员，你选择哪一种模型通常取决于你所使用的开发语言，或者你正在使用的库或框架的功能。虽然这两者的性能特征在本地、内部API调用的应用中通常区别不大，但是当执行远程、外部API调用的操作时，其性能特征可能导致应用程序瘫痪。注意，当处理用户界面和图形渲染时，本地性能的区别对于应用是至关重要的。

一旦程序员开始大量注入远程API调用到本地应用程序，轮询和事件处理方式之间的区别变得非常明显。本地I/O调用和远程I/O调用之间的区别呈指数增长。所有通过磁盘、内存和芯片接口来加快应用程序的工作变得毫无意义。图8-2说明了本地数据访问和远程数据访问之间的I/O性能的相对区别。为了简单起见，我已经隐含地说明了固有的IP连接断开延迟，并且作为带宽/吞吐量功能来说明这一点。然而，如果你真正考虑实际的协议交互作为全部数据传输代价的功能，延迟问题会变得更加严重。

图　8-2：文件描述符在各种连接类型上的操作的相对吞吐量

正如你所看到的，差异非常明显。在远程数据访问次数远多于本地数据访问的极端情况下，轮询和事件模式在驱动应用时所产生的延迟的差别会更加明显。当你的软件为了采取某些措施而需要知道在网络中某个服务是否发生变化时，你往往没有足够的时间来执行低效的轮询。如果酒吧服务员问了4000次的“要喝一杯吗？”，却只能得到顾客1次的肯定回答，当服务员需要问遍城镇里的所有顾客时，浪费在查询上的时间都要爆炸了。轮询和事件之间各有千秋，但是只有一种模型在Web上易于实现：轮询。基于HTTP/REST的客户端-服务器编程从来没有为Web应用开发提供正式的事件驱动框架。结果导致Web充满了低时效性的社交数据驱动的应用，社交数据的时效性特征让这一问题更为突出。如果我的朋友明天可能在城镇，我可不想自己知道时为时已晚。我们希望应用能够“实时”就像构建在要倒塌的扑克牌堆砌的房子那样，而我们看到墙变得越来越高。

HTML 5事件

值得注意的是HTML5规范突出了事件模型和在高层上支持双向通信功能的“Web Socket”框架。XMLHttpRequest已经把GUI Web应用提升到一个新的层次，我猜想“Web Sockets”将对今后几年的Web应用开发产生重大影响。允许基于浏览器的应用以更类似于socket的方式来访问数据必将带来好的结果。

WAN规模的事件

分布式事件处理和通知不是一个新问题，而它的解决方案也不是。事实上，企业空间已经生成了无比强大和高效的“消息总线”的解决方案，尽管这是专利的。Tibco公司作为企业消息总线解决方案的领先提供商而广为人知。然而，企业消息总线的关注重点在于它们传输的数据的临界值，而非通过各种各样的连接堆栈方能实现与多样化的终端间进行连接。股票交易需要确保传输成功并保护隐私。要达到这种程度需要付出较高的代价，该代价转化为了高额的牌照费，但Internet上，这一转化进行得不是很好。社交数据是通过对终端用户免费的应用创建和消费的。因此，各企业公司提供的昂贵的、私有的和难以集成的消息总线解决方案在Web上不受欢迎。

利用生成社交数据的技术来提供事件驱动通知机制的解决方案，通常正是基于该系统已有的技术。可以使用HTTP POST方式来推动整个WAN范围内任意服务之间的事件，使用标准格式(XL)，在执行控制流过程中的延迟可以被关闭。Jeff Lindsay在宣传HTTP POST的事件传输模型时给出了一个生动的比喻，并由此获得了绰号“Web Hooks”(htp：//blog.webhooks.org/）。再强调一次，HTTP不是最优化的传输事件协议，XML也不是最优格式，但是这二者提供了最丰富、实用的机制来解决服务器之间实时通信的问题。

如果社交数据变得更加隐蔽，有可能发生以下两件事之一：一是我们作为消费者，将继续忍受，而我们的行为模式和期望将会更符合低级模型；二是我们停止使用该产品，因为它们的使用价值已经下降到不足以抵消使用所需要忍受的烦恼。前者有很多先例(Bta与VHS)，因为我们通常接受我们想要解决的问题的非最佳解决方案。后者每天都发生，因为发展迅猛的工业和产品通常由于时间和最后的优化因素而崩溃。在一个框架变成标准之前，中间的事件网关可以通过众多协议和格式代为转发事件和数据的方式，起到连通数据发布者和阅读者的桥梁作用。如果事件机制的实现可以做到成本低且通用，它将可以消除社交数据的破坏性的延迟。“Web规模”的实时社交数据的普遍应用依赖于事件驱动模型。

Gnip项目[2]是基于以下理念成立的：围绕社交数据消费的事件驱动架构是提供实时访问模式的唯一方式。然而，由于轮询方式还会存在，Gnip仍然选择轮询方式作为社交数据交互的首选模型。

单一的API集成通常简单明了。然而，当你要集成很多不同的终端时，便开始出现效率低下的问题。Gnip为它的终端数据消费者实现了多API集成的工作，这些API最后只需要通过唯一点就可以完成集成Gnip。

[1]在计算机网络中，速率控制常用在一个网络接口上，用于控制发送和接收数据的通信速率。流量小于或等于指定速率的数据会被发送，而流量若是超过了指定速率，则被丢弃或延迟发送。

[2]想了解更多信息，可访问其网站：http：//www.gnip.com/。

社交数据规范化

假定我们通过HTTP POST来处理广域网范围内的事件的方式——Web Hooks——解决了数据访问延迟问题。这种方式只是解决了通用的API访问问题，而不是数据本身的多样化特征问题。XML提供了数据的结构化特性，但是没有考虑到通用性。当今的社交数据聚集应用和它们要集成的各种类型的社交数据的API都碰上了“一次性理解”的难题。理解从一个特定API获取到的数据的结构需要付出的代价很高——太高了。虽然多路复用网关协议已经存在了很长时间，但是通常对于加强来自稀疏数据源的通用的、规范化的数据，只存在严格的XML转换翻译器。遗憾的是，严格的数据解析方式很少能够正常工作，因为实际上各种服务集创建的数据是非常多样化的。人们对标准、编码和转义序列的理解都是不同的。此外，创建XML用于消费的软件不可避免地存在bug，这导致输出格式简陋，对其使用变得进一步复杂化。

我们从严格的HTML解析Web浏览器中明白，遗憾的是，标准并不能使那些和标准保持一致的软件能够准确无误地解析这些非完美的数据格式。实际情况是标准有不同的解析方式，软件存在bug，而且最强大的事实标准是用户已经正在做的事。人类的强大永远都不能被否认。

如果你曾经花一些时间从各种社交数据源看这些数据，你会注意到这些数据变得看起来都差不多。虽然要解释的通用性对人类而言很明确，但对机器来说却很困难；需要人工编辑指南给出从数据集A到数据集B的映射。

从两种不同的“社交标签”服务考虑以下两个XML例子。虽然它们显然都是XML，但是其“书签”的表示方式差别迥异，而且它们都为终端用户提供相似的服务。

来自Delicious网站：

＜item＞

＜title＞Fractals derived from Newton-Raphson iteration＜/title＞

＜pubDate＞Mon，19 Jan 200920：02：05+0000＜/pubDate＞

＜guid isPermaLink="false"＞http：//delicious.com/url/7549fded443f#joe＜/guid＞

＜link＞http：//www.chiark.greenend.org.uk/～sgtatham/newton/＜/link＞

＜dc:creator＞iacovibus＜/dc:creator＞

＜comments＞http：//delicious.com/url/7549fded443f＜/comments＞

＜wfw:commentRss＞http：//feeds.delicious.com/v2/rss/url/a＜/wfw:commentRss＞

＜source url="http：//feeds.delicious.com/v2/rss/joe"＞joe's bookmarks＜/source＞

＜category domain="http：//delicious.com/joe/"＞mathematics＜/category＞

＜category domain="http：//delicious.com/joe/"＞newton-raphson＜/category＞

＜category domain="http：//delicious.com/joe/"＞fractals＜/category＞

＜category domain="http：//delicious.com/joe/"＞iteration＜/category＞

＜/item＞

来自givealink.org网站：

＜item＞

＜title＞Bus slams into shop houses after driver collapses behind wheel＜/title＞

＜link＞http：//www.thaivisa.com/forum/Bus-Slams-Shop-Coll-t198228.html＜/link＞

＜description＞Bus slams into shop collapses behind wheel＜/description＞

＜/item＞

当今，有成千上万的服务通过API和Feeds暴露其用户生成的内容(Uer-Generated Content,UGC)，对其结构和内容进行规范化，这样开发人员可以期望通用性优先级变得更高。DiSo项目是把整个API范围内相关成员引入该方面的主要催化剂，用于“净化”更多可使用的社交数据参见http：//diso-project.org/wiki/activity-streams。

作为数据生成者和数据使用者之间的媒体，Gnip处于一个得天独厚的地位来把社交数据的意义翻译和规范化到传统的理解和结构中。利用整个行业范围内需要更易于消费的社交数据提供集体输入和知识，Gnip作为“漏斗”，接收的是很多不同的协议和格式，而输出的是一致、异构的数据，并提供了对数据更便捷的访问方式。

数据的商业价值

关于开源软件是好是坏的争论基本结束。企业何时对其软件或者是部分软件进行开源，何时不能开源，有很清晰的界限。总体来说，如果你的软件享有“独一无二”的知识产权，把你和外界的其他方面分离开，你应该考虑不把它开源。否则，它就可以作为开源，这样开源社区可以在代码中涵盖其经验和专业知识，为全世界作出贡献。遗憾的是，软件开源而言的数据相对决策框架还不够成熟。API的爆炸使得数据发布商措手不及，因此，他们对数据价值的理解变得糊涂得一文不值。

一些传统的内容发布商已经强化了其数据的价值。提供免费的周刊来提供内容，包含广告商支持内容（评论）和产品（杂志）的生成。一些贸易杂志收取订阅金，不加入广告模式。Internet上的传统的媒体/内容发布商主要采取广告模式来支持他们的数据（内容）的分布。然而，通过API访问社交数据没有成熟的模型。提供商是否需要为数据本身的访问收费，还是一切都免费？除非你是一个“freegan”（反消费者），否则以上问题没有一个有明确的答案。除非谈及的数据存在内在价值，否则对其评价变得非常困难和曲折。如果你作为数据使用者，为了购买微博客消息，是否应该花费0.01美元？这个消息是否应该获得许可，或者访问方式是否应该被租用？数据项的价值和对它在转换处理之间的区别开始变得很明显。用户在发布商的服务上真正去生成内容，是否应该获得其中收益的一部分？从刚开始，谁是真正的数据拥有者？正如你所见的，这里充斥着很多这样的问题，它们没有明显的答案。

当出现了社交数据，在该行业中，一些发布商会声称他们的数据是无价的，而另一些则认为数据是免费的。数据使用者已经潜意识地相信绝大多数的数据是免费的。因此，如果真的对社交数据进行标价，他们便会发展成价值链切实的一部分。

公共的与私有的

有些数据被认为是公共的，而其他数据则被认为是私有的。公共和私有这二者之间的区别是通过提供数据的服务来定义的，而且通常是通过给定服务的《服务条款》来概括的。虽然对这二者的社会理解及其本身是很有意思的话题，但是我在这部分将考虑访问这二者的技术上的一些隐式问题。

让我们从两个当中更简单的公共数据说起。访问公共数据相对来说比较简单。今天绝大多数的数据API是通过未授权的REST接口来访问的。这意味着在网络上它们和其他的URL并没有什么区别，它们可以很容易被任意用户和应用程序利用。访问这些API终端需要很少的身份验证形式，如果有的话，而且对API用户的授权通常是通过不透明的方式在幕后实现处理的。两种更丰富的验证方式是使用HTTP Basic-Auth和嵌入在URL中的“API关键字”。这两种方式都允许API服务基于身份验证证书来控制对数据和API功能的访问。令人惊讶的是，这两种方式和HT TP都能工作很好。不考虑对公共数据的API访问是否需要任何身份验证，底层的社交数据在绝大多数情况下仍然是“公共的”。

私有数据带来了很多麻烦。由于越来越多的应用开始利用通常被认为“私有”的其他应用的数据，这需要控制终端用户的访问。数据提供商还使用各种技术，在访问权限控制中叠加上自己的观点意见。这些技术都使得私有数据的聚集访问方式变得异常复杂。

保证终端用户数据在存储角度是受到保护的，这仅仅是挑战的一部分。存储冗余和加密技术允许应用“蓬勃发展”，通常不需要考虑用户数据的丢失和折中方案。实际上，用户对应用程序很放心，为了允许更深层的API集成，他们免费提交用户名和密码到第三方应用程序。这种做法实际上非常不安全，但是它说明了用户为了获得他们期望的功能，甘愿牺牲其隐私信息。有两种有效可选的方案用于分享私人登录证书：OAuth和Blind URL。

OAuth(http：//oauth. net/）提供了简单的解决方案来允许各种服务之间进行交互，所有这些交互都给终端用户在他们的信息上提供了最终的权限控制权，而不需要和第三方透露他们的用户名和密码。交互程序把用户交给其期望的集成点，询问用户是否允许集成服务所请求的访问权限控制。如果终端用户赞成该交互，两个服务之间就共享一个令牌(Tken)，而且用户也可以选择撤销服务的访问权限许可。

Blind URL是在服务和用户之间共享信息的更简单的方式，但是它们在技术上还不够“安全”，因此它们通常都没有什么可信度。Flickr的“Guest Pass”共享功能能够巧妙地利用盲目的URL来允许用户和那些没有Flickr账号的用户分享“私有”的照片，因此，这些用户不属于Flickr用户的“朋友”或者“家族”。Blind URL为了使用服务来获取URL的使用许可，它不需要用户做出任何额外的工作，但是它们从技术上能够猜测出来，这会带来私有信息泄露的隐患。

Gnip当前只处理公共的数据。对于公共的和私有的边界访问控制必须提供给终端用户，而且当讨论中间基础设施时，它并非总是很清晰的。例如，你不知道也不关心公司采用什么样的通信路由服务信用卡来保证金融交易，而对于谁能够访问你的假期照片的访问权限的控制却是至关重要的。Gnip正在努力在公共数据和私有数据之间画线，以及它最终如何为私有数据提供如公有数据一样的服务支持。

结束语：通过Gnip思考

每天“汹涌”的数据“浪潮”把更多的异构API“拍上海岸”，而在这些API中，必须保证数据的一致性。作为面向消息的中间件服务，Gnip公司承诺使用中间数据代理的方式来“传送Web数据”。这意味着该数据发布商期望通过很多内部的传输协议来分享这些数据，Gnip从发布商获取规范化的数据，并实时地传送给Gnip用户（从消息接收到重播时间延迟低于60秒）。不考虑内部的数据格式不一致，Gnip把数据“清洗”并“规范化”成标准的格式作为服务提供给Gnip用户。其结果是只使用Gnip的单点集成，就可以为其他应用提供一致的、格式化的数据。

你可以通过Gnip一个接口来访问很多的社交数据API，且都是“实时”地。Gnip的框架主要采用之前所述的事件模型，为了促进网络和应用之间总体上更高效的数据流。然而，它也支持轮询方式。把基于轮询的应用切换到基于事件的模式来最大化利用Gnip提供的规范化的数据的优点，而通过手工处理将很麻烦。如果不是同步，那么完全采取尚待完善的发布/订阅框架和格式一致性标准，采取中间代理是很有必要的。

当前的基础设施在推进事情进展方面已经做了很多工作，而且网上贸易已经蓬勃发展。然而，不断改变用例和终端用户需求使得和当前框架保持一致会使我们受到一些局限。实时社交数据的需求要求在构建我们的应用的底层控制流上做出修改。我很期望看到基于事件的架构可以扩展到整个Web。

第9章　探寻Deep Web

Alon Halevy和Jayant Madhaven

什么是Deep Web

“Deep Web”（深网）指的是隐藏在HTML表单后的Web内容。为了获取这些内容，用户必须提交包含有效值的表单。例如图9-1的商店定位表单，搜索邮编为94043的商店，可以得到一个包含了商店列表的Web页面。该结果页面即为Deep Web的Web页面的一个例子。

图　9-1：Borders商店定位表单和提交特定表单得到的Deep Web结果页（见彩图21）

Deep Web被公认为是搜索引擎在覆盖率上存在的一个很大空白。这是因为搜索引擎通过Web爬虫(cawler)来发现页面，并在索引中包含这些页面，从传统意义上来说，这些Web爬虫仅仅依赖于Web页面的超链接来发现新的Web内容。它们缺乏自动提交表单的能力，因此在表单后的Web页面无法被搜索引擎索引。包含表单的页面通常对表单后的页面内容包含很少；因此，对于普通的用户，只有当他们已经知道相应的HTML表单或者搜索引擎以某种方式将他们引导至该表单，这些用户才能够发现这些Deep Web内容。事实上，Deep Web（也称“不可见网络”(Ivisible Web)或者“暗网”(Hdden Web)）的名字正是基于以下观察——Web用户无法通过搜索引擎简单地访问这些内容。

很多文章认为Deep Web拥有的数据比现在可搜索的WWW要多得多(Brgman 2001，He 2007，Raghavan 2001）。我们最近的研究(Mdhavan 2007）估计，存在几千万包含潜在有用的deep-web内容的HTML表单，而且Deep Web涵盖的范围跨越了每个可以想象的领域。这些流行的领域包括二手车销售、房地产清单、公寓租赁、求职、产品和食品食谱。有很多表单可以访问政府或者公共部门信息，比如法律法规、法院裁决、环境报告等。还有些表单可以让用户搜索更加深层的内容，如绿荫树、去公园的打车费、马雕像等。

当考虑Deep Web时，我们必须记住HTML表单适用于Web中的各种任务，但不是所有表单都可以访问deep-web内容。例如，要求输入用户名和密码的登录表单、提交用户输入到论坛和博客的反馈表单、执行购买的购物车等，这些表单需要获取用户的私有信息或者导致后台状态改变，因此不属于Deep Web的一部分。相反，我们主要考虑那些允许用户匿名搜索信息的表单。

由于deep-web内容的本质特征和数量规模，很自然地，搜索引擎会希望在它们的Web索引中包含这些内容。这样，搜索引擎才可以向用户展现这些新的内容，反之亦然。因此，无论在学术领域还是工业领域，人们对如何提供访问deep-web内容的问题都很感兴趣。但是，很大一部分的研究和技术将重点放在了如何在某些狭窄的领域来解决这个问题。这种做法最显著代表是垂直搜索引擎，每个都是专注于某个狭窄领域的内容。例如，二手车搜索和求职搜索网站允许用户从单一门户网站搜索很多底层的站点。这种策略虽然可以访问一部分Deep Web内容，但是其可达的范围非常有限，并且忽略了大量不适合于这些狭义领域的基于表单的网站。

我们的目标是为通用搜索引擎用户提供Deep Web访问方式。从搜索引擎的角度，我们希望包含很大规模的Deep Web内容，即能够访问尽可能多的上千万的HTML表单。因此，我们需要一个解决方案可以在所有可能的语言和领域工作，而且不需要人工监督，即一个完全自动化的策略。从deep-web站点主机(hst)角度考虑，该策略不应该过度使用站点宿主的资源；换句话说，该策略只应该驱动那些真正相关的用户流量到该网站。

在这一章，我们将概述一个满足上述标准的解决方案。该方案名称是探寻(srfacing)，对于每个HTML表单，预计算一组查询，这组查询可能能够从深层站点中返回有用的内容。从每个预计算的查询中汇集URL，再把这些URL插入到搜索引擎的索引。从高层上看，我们的方法解决了两个挑战：一是在向一个表单提交查询时，决定需要填写哪些表单输入框；二是找到合适的值来填写这些输入框。在核心上，该方法依赖于通过智能选择的样本查询来探测HTML表单，然后分析检索到的Web页面的区别。该策略虽然简单优雅，却是非常高效的，而且可以有效地为Web搜索用户提供很大一部分Deep Web内容。我们的搜寻策略可以应用于Web中任何使用get方法（在本章后面会概述）的HTML表单。这主要是为了排除一些我们不希望爬虫去抓取的表单，比如需要用户信息的表单或者是产品购买结果表单，这些表单通常使用post方法。

我们的deep-web探寻系统已经部署到G公司的搜索引擎上。该系统成功地抓取了几百个领域下的数百万个站点。目前，在G公司的网站上，每秒有超过1000个的查询，在首页结果中能够看到一条来自Deep Web的搜寻结果。总体来说，搜索引擎用户发现这些结果和普通的搜索结果一样有用。关于该解决方案更详细的探讨和实验分析请参考(Mdhavan 2008）和(Mdhavan 2009）。

在本章的剩余部分，我们首先探讨访问deep-web站点的其他可选方案；其次描述思考该问题的概念模型；然后描述预测一个表单中有用的输入框（以及输入框组合）以及预测适合于这些输入框所需的合适值的策略；最后总结并评论我们在方案部署方面的经验。

提供Deep Web访问的其他可选方案

访问deep-web内容有两种通用的方法。第一种方法是为特定领域（如汽车、书或者房地产）创建中间协调器(mdiator)，这种方法被垂直搜索引擎广泛采用。在这种方法中，我们先创建一个主(mster)表单（即作为中间协调器），然后在每个表单和该中间协调器之间创建语义映射。对于每个中间协调器上的查询，基于一些预计算的表单摘要信息选择相关的底层表单；通过语义映射在每个表单上构建查询；接着从每个选择的表单中返回内容，然后组合这些内容呈现给用户。从高层上看，该方法和现代提供购物对比的门户网站的实现思想很相似，这些门户网站通过使用Web服务，返回很多底层站点的供应信息。

由于垂直搜索专注于一个领域内的同类表单的收集，因此这种方法可以适合于垂直搜索，但它不适合于通用的搜索引擎。首先，构建和维护很多不同的中间协调器和映射的人工代价很高。其次，识别对搜索引擎关键字查询最相关的表单是非常具有挑战性的。只有小部分的表单需要识别，否则，底层表单接收到的用户流量可能超过了它们的处理能力。为了实现相关表单的识别，极端情况下，表单的摘要信息可能需要和底层内容几乎一样大。最后，即更基础的原因是，Web上的数据可以是方方面面，无法给领域的边界做出清晰的定义。因而，为Web创建一个中间协调器将是一项宏伟的挑战，而且该中间协调器需要支持100多种语言。因此，当目标是覆盖很多领域上的大量表单时，这种方法是不可行的。

第二种方法是探寻，为任何感兴趣的HTML表单预先计算最相关的表单提交。每个表单提交生成一个唯一的URL，该URL可以像任何其他的HTML页面一样被搜索引擎索引。这种方法能够充分利用现有的搜索引擎基础设施。此外，它可以将deep-web页面无缝地融合到Web搜索中，也就是说，这些deep-web页面可以被直接插入到结果页的排序列表中来响应搜索查询请求。此外，如果用户基于页面摘要(sippet)认为搜索结果是相关的，当点击该搜索结果时，就可以给deep-web内容带来用户流量。

实现探寻方法[1]主要面临两个挑战。第一，HTML表单通常含有多个输入框，因此，对所有输入框的所有可能值的整个笛卡儿积进行枚举的简单策略，会导致生成海量的URL。Web爬虫需要抓取每个生成的URL，抓取太多的URL会耗尽Web爬虫的资源，而且很可能会对这些HTML表单的宿主Web服务器带来无法承受的负荷。此外，当笛卡儿积非常大时，很可能会有大量的结果页面是空的，因而从索引角度考虑，这些结果页是无用的。例如，在Cars.com上的某个查询表单有5个输入框，这些输入框的笛卡儿积生成的URL超过2.4亿个，而实际上该网站只有65辆车待售(htp：//www.cars.com)。毫不奇怪，对于这些URL，大量的表单提交将无法得到任何记录，因此对于搜索引擎是无用的。

第二，HTML表单通常包含文本输入框，而且可能需要输入合适的值才能够返回检索结果。文本输入框可以有两种类型：一是接受任意输入值的通用输入框，如关键字搜索框，二是只接受某个特定集合中的值键入的输入框，图9-1所示的是商店定位的邮政编码输入框。

本章的剩余部分所描述的解决方案采用如下策略来解决这些挑战。首先，我们设计了称之为“信息度测试”(iformativeness test)的测评方式，用来评估查询模板，即表单输入框的组合。对于任何模板，我们通过不同集合的值来探测表单中的模板里的输入框，并且检查我们得到的HTML页面之间是否有显著不同。生成的结果页面显著不同的模板被认为是作为探寻的好的候选模板。其次，为了识别适合于探寻的查询模板，我们设计了一个算法，它可以高效地遍历查询模板的空间。该算法权衡了生成更少的URL和达到网站内容的高覆盖率。最后，我们设计了算法来预测文本输入框的合适的输入值。该算法扩展了之前为通用文本输入选择关键字的算法(Brbosa 2004，Ntoulas 2005），采用富信息量测试方式来表示从通用的数据类型中识别键入的输入项的值，比如邮政编码、价格和日期。

HTML表单处理的基础

这里我们只是简要地概述了表单处理。更多的信息可以通过HTML表单规范来获取(htp：//www.w3.org/TR/html401/interact/forms.html)。

HTML表单是由form标签定义的（如例9-1）。参数action表示当表单提交时，需要执行查询处理的服务器。表单可以有若干个输入控件，每个控件通过input标签定义。输入控件可以有很多类型，其中最重要的是文本框(txt box)、下拉选择框(slect menu，通过分离的select标签定义）、复选框(ceckbox)、单选按钮(rdio button)和提交按钮(sbmit button)。每个输入项都有一个name，它通常不是用户可以直接在HTML页面上看到的名字。用户可以选择以下方式选择输入值：在文本框中输入任意的关键字，或在下拉选择框、复选框和单选按钮中选择预定义的选项。此外，存在隐藏输入框(hdden input)，它的值是固定的且对于和表单交互的用户是不可见的。隐藏输入框通常用于为表单提交提供一些额外的上下文信息（如该表单的来源站点）。在这一章中，我们集中研究表单中的选择按钮和文本输入框。复选框、单选框的处理方式和下拉选择框相同。

例9-1：包含让用户搜索工作的表单的HTML代码

＜form action="http：//jobs.com/find"method="get"＞

＜input type="hidden"name="src"value="hp"/＞

Keywords：＜input type="text"name="kw"/＞

State：＜select name="st"＞＜option value="Any"/＞＜option value="AK"/＞

＜option value="AL"/＞……＜/select＞

Sort By：＜select name="sort"＞＜option value="salary"/＞

＜option value="startdate"/＞……＜/select＞

＜input type="submit"name="s"value="go"/＞

＜/form＞

当提交一个表单时，浏览器会通过get或者post方法，发送包含输入值的HTTP请求到服务器。如果采用get方法，参数会附加到action参数值后，在HTTP请求中，作为URL的一部分（如例9-1的http：//jobs.com/fnd?src=hp＆kw=chef＆st=Any＆sort=salary＆s=go)。如果采用post方法，参数会在HTTP请求体(bdy)中发送，URL仅仅包含action（如例9-1的http：//jobs.com/find)。因此，使用get方法从表单中得到的URL是唯一的（依赖于提交的参数值），但采用post方法得到的URL不是唯一的。

因为搜索引擎是基于Web页面的URL来识别它们，从post得到的结果页面无法区分URL，因此不能直接插入到搜索引擎的索引中。此外，按照HTML规范，当表单提交导致状态变化或者产生副作用(sde effect)（如购物车、旅行预定和登录），必须使用post表单。正如之前描述的，这样的站点通常本质上没有信息。由于这些原因，我们把注意力限制在get表单，这种表单可以生成更适合于Web搜索的内容。

任何需要输入个人信息的表单都必须去除，举个例子，通过过滤任何包含密码输入框以及任何如用户名、登录等通常都和个人信息相关的单词。同样，只是简单地记录用户反馈或者评论的表单也可以通过以下方式去除：忽略那些包含有文本区域输入框的表单。

最后一点，我们提出的方案无法处理Javascript事件（正如本章所描述的）。表单和输入控件可以包含onselect、onclick和onsubmit属性，这些属性可以包含任意的Javascript代码。处理这些事件意味着要模拟Javascript在所有可能事件下的执行。原则上，我们可以利用Javascript引擎如SpiderMonkey和V8（见本章的“参考文献”），通过利用Web浏览器为我们执行表单提交，从而扩展我们的算法来处理这些表单。但是这种模拟的处理代价很高，因而其挑战在于能够快速地识别那些很可能会生成deep-web内容的Javascript表单，而且可以把这些deep-web内容添加到搜索引擎索引中，也就是说，最后的那个Web页面有一个诸如get请求一样的“幂等URL”(iempotent URL)。

查询和查询模板

我们可以把表单理解为允许用户向后台数据库提交查询的接口。每个表单提交都是一个查询，这个查询的输入是表单的输入框中提供的值，返回的是数据库的记录子集。该查询属于由表单输入项和其值约束的限制性语言(rstricted language)。此外，表单站点的内容在查询之前是未知的。因此，为探寻选择表单提交的问题实质上是在内容未知的数据库中的限制性语言中选择一组查询。

选择正确的一组查询的挑战在于表单输入项的模糊性质。尤其是，输入项可以有两种类型。第一，选择输入项是在数据库记录上增加选择条件，如例9-1中的kw（在求职描述中的关键字(kyword)）和st（状态(sate)）。选择输入项的值可以从一个预定义的列表（通过选择菜单）中获取，或者在文本输入项中输入。文本输入项可能只接受特定类型的值，但其类型我们通常是无法知道的。选择输入项通常可以分配一个可以匹配数据库中所有记录的通配符值。对于选择菜单，该通配符必须是该菜单的一个选项，如输入状态有Any这个选项值；对于文本输入项，其通配符是空字符串。

第二，存在表示层输入项，它们不影响记录的选择，而只是控制表示方面，比如结果页面的排序或HTML页面布局，如例9-1中的排序。对选择输入项和表示层输入项进行区分是探寻算法需要解决的一个挑战。

形式上，假设我们需要使用SQL来提交查询。我们可以把表单站点的内容建模为只包含一个单表D的数据库，该表包含m个属性。每个表单提交可以看做一个如下查询：select*from D where P，其中P表示通过选择输入项来表示的选择谓词。

例如，假设例9-1的表单是用于在job表Jobs(position、city、state、desc)上提交查询，提交请求返回在美国加州的厨师岗位的检索，其对应的查询应该是：select*from Jobs where state='CA'and desc like'%chef%'。注意一点，我们这里假定在例9-1的其他输入项是表示层输入项。

探寻的问题本质上是选择一组较优的查询（表单提交）。但是，对每个表单提交的属性进行推测是不现实甚至不可行的。因为一个表单可能对应几百万种不同方式的表单提交，分别测试每个表单提交可能会耗尽底层站点的资源。为了推测表单提交的集合，我们定义了查询模板。查询模板指定表单中输入项的一个子集作为绑定输入项(bnding input)，其余的是自由输入项(fee input)。通过给绑定输入项赋以不同的值，可以生成多个表单提交。以SQL查询为例，查询模板可以简洁地表示表单中的所有查询：select*from D where P'，其中P'只包含表单中绑定输入项的选择谓词。绑定输入项的个数即模板的维度。表9-1显示了例9-1表单查询模板的三个实例。

注意，在实践中，为了生成有效的表单提交，必须给模板中的自由输入项分配值。理想情况下，我们希望不要为模板的SQL查询增加任何额外的选择条件。对于文本输入，我们可以分配空字符串；对于选择菜单，我们可以分配菜单的默认值，期望该值可以用通配符表示。我们注意到，为了减少用户交互操作的麻烦，绝大部分的表单都支持通配符表示。

探寻deep-web站点的问题现在可以划分为两个子问题：

·选择一组合适的查询模板；

·为绑定输入项选择合适的输入值，即采用实际值来初始化查询模板。对于一个选择框，可以使用该选择框的所有选项；但是对于文本输入框，必须预测输入值，而且我们不能假定对于需要考虑的那些值所在的领域存在先验知识。

为了在说明上尽可能简单，我们假设初始化输入项的一组值对于绑定输入项的所有模板都是一样的。但是，实际上有些输入可能是相关的。例如，一个输入项（如cityName)的值可能依赖于选定的另一个输入项（如state)的值，或者多个输入项（如salaryMax和salaryMin)可能会约束相同的底层属性。

选择输入项组合

我们首先描述如何判断表单中的输入项和输入项组合对于探寻是有用的。现在，我们假定表单中的每个输入项的值都是已知的，也就是说，把文本输入项和选择菜单都统一对待。对文本输入项中的值进行选择的问题将在第10章探讨。

正如所看到的，我们可以把探寻问题建模为选择一组合适的查询模板。在选择一组正确的模板中，从识别我们需要追求的一些目标开始。正如之前所述，简单地枚举所有可能的输入值的笛卡儿积的策略既浪费（给Web爬虫和表单的宿主服务器施加负载负担），又不必要（因为有大量的URL可能不包含数据记录）。

因为我们想尽可能多地发现deep-web内容，很自然地，其处理策略的一个目标是最大化底层数据库的覆盖率（即返回记录的所有数量），同时限制表单提交的总数量。但是，由于我们生成的页面需要添加到搜索引擎的索引中，因此我们也必须解决一些其他的问题。

首先，最佳的策略可能是确定一些能够包含大量结果的表单提交。但是，有大量结果的Web页面并不一定最适合添加到搜索引擎索引中。举个例子，对一个二手车网站，只有一个包含所有待售的Honda汽车的页面是没有用的；相反，如果它有多个页面，包含Honda的每个模型或者不同价格范围则更理想。因此，我们希望生成在每个页面中既不包含太少也不包含太多记录的URL。

其次，虽然一个搜索引擎的主索引很大，它还是远远不够存储从Deep Web抽取的所有页面。由于搜索引擎最重要的目标是根据用户的查询将用户引导到相关的Web站点，我们更希望拥有来自很多站点的丰富且重要的内容。从某种意义上说，生成的URL个数不需要是完整的，但必须足够好以便将流量引导到底层站点。

再次，对于Web站点探寻来说，实际上不需要追求百分百的覆盖率；为一个站点的那些内容丰富的页面建立Web索引就足够了。定期执行的Web爬虫最终会抓到通过探寻得到的页面的超链接（如对于相同的查询，链接到更多的结果，或者链接到相关查询的结果），因此，最终该网站的覆盖率会增长。

总之，为了实现只通过在每个站点提交少量的查询就能够达到较好的（可能是不完整的）覆盖率，我们的目标是为几百万个不同的表单选择查询，而且通过探寻将获取的页面作为搜索引擎索引的好的候选页面。

查询模板的质量

我们可以把以上列出的每个目标作为查询模板的标准。例如，我们绝对不想要一个包含表现层输入项作为绑定输入项的模板。这种模板返回的结果也可以简单地通过一个不包含表现层输入项的模板来提交查询，从而获得返回结果，而且这种处理方式，通常可以生成更少的URL。

包含很多维度的模板通常不是我们想要的，因为它们生成太多的URL，这些URL中有很多无法返回结果。然而，维度越多的模板能够保证返回越多的记录。

维度较小的模板通常是我们更想要的，因为它们生成较少的URL，但是也有可能这些查询每个都将返回太多的结果。正如之前所述，有太多记录的页面并不适合作为索引。此外，站点可能对在每个页面上显示的实际记录有限制，因此减少了返回的实际记录的数量。

因此，我们期望模板1）不包含任何绑定的表现层输入项；2）不包含维度太多或者太少的URL。从直观上说，模板的维度应该依赖于底层的数据库大小。

现在我们定义一个测试来进一步说明之前所述的标准。在我们描述之前，先举个例子来说明这个想法。如例9-1表单的两个模板，模板T1只有一个绑定输入项st(State)；模板T2只有一个绑定输入项sort(Sort by)，现在考虑这两个模板生成的表单提交集合（如表9-2所示）。

我们注意到模板T1的每个提交都返回不同状态下的job列表。因此，返回的记录不同，从而结果Web页面内容将会很不一样。我们称这种生成的Web页面内容很不同的模板为富信息量模板。反之，因为没有关键字，T2中的每个提交返回的记录都是相同的（所有job)我们称这种模板为非富信息量模板。从本质上讲，我们的目标是选择富信息量模板，去除非富信息量模板。

因此，我们可以基于从表单提交生成的Web页面的内容区分度(dstinctness)来评价模板。我们通过基于由模板生成的Web页面内容的相似性来对这些页面进行聚类，从而估计该模板生成的不同的Web页面的数量。

如果和表单提交的数量相比，不同的Web页面的数量很小，很可能是如下情况：1）该模板包含表现层输入项，因此多个页面的集合本质上有相同的记录；2）模板维度相对于底层数据库来说太多，因此产生大量的无记录页面，不同页面之间都很相似；3）模板（或者表单）有问题导致出现错误结果页面，这些错误结果页面也很相似。如果模板不存在这种情况，但是还是生成很相似的内容，那可能是搜索引擎索引的边界值，因此不可能对搜索引擎查询有任何影响。

富信息量测试

我们考虑对于给定模板生成的URL，并且下载这些Web页面的内容。我们计算从每个提交得到的Web页面内容的签名，当一个模板生成的Web页面内容计算得到的签名个数远远少于可能的提交个数时，则认为该模板是非富信息量的。

富信息量是根据阈值τ来定义的，该阈值可以通过实验确定。假设Sd是不同签名的集合，St是所有签名的集合。那么，当|Sd|/|St|＞τ时，表示T是富信息量模板。

相比之下，关于内容签名更具体的细节不是很重要，我们列举了该函数需要的一些重要属性。首先，该签名必须对HTML格式是透明的，因为表现层输入项通常只是改变Web页面的布局。其次，该签名必须和词项的排序无关，因为结果重排是很普通的表现层操作。再次，该签名必须能够忽略页面间的细微区别；因为区别的来源通常是广告，尤其是商业网站，更是如此。这些广告通常在页面边缘处展现。它们增加了页面的文本内容但是并不反映返回结果的内容，因此必须被过滤掉。这些签名不应该包含输入项的值本身。一个二手车搜索网站，如果在邮编为94107的区域没有红色的Honda Civic车出售，则很可能会有这样的错误信息“在94107区域没有红色Honda Civic汽车出售！”(N search results for Red Honda Civic in 94107！）。类似的，对于以{color make model zip}为查询项的结果页面，会有大量的“No search results for{color make model}in{zip}”的结果页。这些页面的唯一区别是搜索词不同，不排除搜索词的签名很可能会把这些页面当做不同的页面，因而认为相应的模板是富信息量的。

在实践中，可能不需要分析一个模板生成的所有提交的内容；而只是需要测试所有可能提交的一个足够大的样本集合就够了。

搜索富信息量查询模板

我们的目标是搜索表单中的富信息量查询模板。我们可以采取一个非常简单的策略来考虑表单中所有可能的模板，并对每个模板应用富信息量测试。但是，假设一个表单包含n个输入项，存在2n-1个可能的模板，那么对每个可能的模板进行测试的计算代价很高而且不必要，因此，可以采取增量式的策略而不是遍历所有模板空间，并且只测试那些可能是富信息量的模板。

我们的策略是从单一的绑定输入项模板开始，通过自下而上的方式搜索模板空间。这种策略的基本思路是认为模板的信息量很可能依赖于它扩展的模板，也就是说，它有额外的绑定输入项。如果模板T有维度k，而且其扩展的所有维度小于k（即维度为k-1）的模板都是非富信息量，那么T不可能是富信息量模板。

我们通过所有维度为1的模板开始，测试每个候选模板的信息量。如果有维度为1的模板被认为是富信息量模板，我们就给该模板维度加1，也就是说，构建包含绑定输入项超集的维度为2的模板。

因此，候选模板必须至少包含一个富信息量扩展模板（但该候选模板其本身并不一定是富信息量）。然后测试每个新的候选模板来确定该新模板是否为富信息量。从维度为2的富信息量模板，我们继续以相同的方式构建维度为3的候选模板，如此反复操作。如果某个维度不包含富信息量模板，程序就终止。

注意：当增加一个模板时，考虑到所有的候选输入项。我们可以选择更积极的策略，只考虑富信息量的输入项，即其相应的维度为1的模板是富信息量的。但是，我们相信，在实践上，这种策略会（错误地）忽略一些富信息量模板。也有很多表单，包含一个主要输入项用于返回结果，而其他的输入项实际上只是作为修饰。例如，一个包含make和color输入项的表单，如果对于make输入项选择默认值，而只选择color这一输入项时，则无法返回结果记录。因此，只有绑定输入项color的表单是非富信息量，而包含make和color的则是富信息量。

一旦搜索终止，我们可以把所有富信息量模板生成的URL增加到搜索引擎的索引中。对于搜索的调整有很多可以完善的地方。例如，我们发现永远都不需要考虑多于三个绑定输入项的模板，或者是生成很多提交的模板。它们不可能是富信息量模板，因此可以很容易地删除这些模板。不需要分析由一个模板生成的所有URL；通常来说，考虑一个足够大的样本就够了。

实验分析表明，我们的算法生成的URL远远少于其他更简单的可选策略。特别地，我们发现生成的URL数量比不采用富信息量测试的最佳的启发式算法少了两个数量级。我们还发现该方法可以高效地为表单确定富信息量模板——在分析过程中，无论测试的模板个数还是下载的模板提交总数在数量上都很小。重要的是，我们发现生成的URL依赖于底层数据库的大小，而不是表单中输入项的个数。

预测输入项值

大量的HTML表单包含文本输入项。此外，有些包含选择菜单的表单必须有文本输入项值才能够返回检索结果。

我们注意到文本输入项通常用于两种方式。第一，通用的输入项实际上可以接受任何合理的值，在输入项输入的查询词可以返回在后台数据库中包含该词的所有文档。这种情况的通常例子是通过标题或者作者搜索书籍。第二，存在一些键入的输入项(tped input)。这种输入项的值只能是一个定义良好的有限集或者数据类型（如邮政编码），或者属于一些连续的但定义良好的数据类型（如日期或价格）。在固定格式的文本框中输入无效的值通常会导致错误页面，因此识别正确的数据类型很重要。在通用的文本输入框中输入不好的关键字仍然可以返回一些结果，因此挑战在于可以识别关键字的一个有限集合，从该集合中可以抽取很多不同的结果页面。

文本输入项的两种类型可以分别对待。在接下来的部分，我们首先描述一个算法为通用的输入项生成关键字，然后考虑键入的输入项。

通用的文本输入项

在我们描述如何为通用的输入项识别好的候选关键字前，先来考虑（并驳回）一个可能的备选方案。可以想象，我们本可以在各种领域把涉及的查询词列表作为文本输入项值，并把每个文本输入项值和最适合的查询词列表进行匹配。但是，我们很快发现这种处理方式会有太多的概念和领域。而且，对于通用的输入项，即使我们在相同领域符合相同概念的两个分离的表单中识别输入项，相同集合的关键字也不一定在两个站点上都能正常工作。最佳的关键字通常是属于特定站点的(ste-specific)。由于我们的目标是扩展到几百万的表单和多种语言，我们需要一种简单、高效和完全自动化的技术。

我们采用迭代式探测的方法：从高层上，我们指定候选关键字的初始种子集合作为文本框的输入值，构建查询模板，把该文本框作为单一绑定输入框。生成相应的表单提交，下载相应的Web页面内容，从结果文档中抽取额外的关键字。然后，我们用这些抽取的关键字更新文本框的候选值；重复该过程直到无法进一步抽取关键字或者达到了某个候选的停止条件，如有足够多的候选关键字。在终止时，选择候选关键字子集作为文本框的输入值集合。

迭代式探测在过去作为从文本数据库中检索文档的方法而提出(Brbosa 2004，Callan 2001，Ipeirotis 2002，Ntoulas 2005）。但是，这些方法的目标是实现特定站点的最大覆盖。因此，它们采用了和站点相关的(ste-aware)技术，这些方法无法应用于所有的领域。

在高层上，我们定制了如下迭代式的探测方式：

为了确定文本输入框实际上是否是一个通用输入框，在第一次迭代时，使用初始候·选集合在模板上执行富信息量测试。结果表示，通用的文本输入框很可能会被认为是富信息量模板，而其他的输入框则是非富信息量。

·为了选择候选值的种子集合，我们分析了包含该表单的Web页面的内容。我们通过识别与页面内容最相关的单词来选择页面的单词。任何合理的查询词打分方法，如流行的TF-IDF方法(Slton 1983），都可用于选择表单页面的几个最佳查询词。

为了在每次迭代结束时选择新的候选值，我们考虑为模板分析的所有表单提交页面·上找到的所有选择词集合。排除在很多页面上都包含的选择词，因为这些选择词出现在每个页面上，很可能是HTML样本模板的一部分。我们还排除只出现在一个页面上的选择词，因为这些选择词很可能是没有意义的或者是具有特殊含义，不能代表该表单所表示的站点的内容。

为了对文本输入框选择最终输入值集合，考虑从表单页面或提交页面抽取的所有候·选值，根据这些值检索到最丰富的内容的能力来选择这些值（通过分析从表单提交得到的页面内容）。

我们注意到，为每个文本输入框设置单一的关键字个数的最大值的方式是不合理的，因为表单站点的结果内容的区别可能是几个、几十个到几百万个。我们采用back-off的策略来解决该问题。从每个表单一个小的最大值开始，在这过程中，我们评估了这些生成的URL对搜索引擎流量会带来的影响。如果受到影响的查询个数很高，那么我们就为该表单提高关键字个数限制，然后重启探测进程。

实验分析表明，这里概述的迭代式探测方法对于通用输入框选择输入项值是有效的。相应的表单提交可以展现底层数据库大量的记录个数。有趣的是，我们发现相同表单的文本输入项和选择菜单通常能够展现底层数据的不同部分。我们还可以构建Web爬虫，通过系统生成的URL，能够随时间展现更多的deep-web内容。

键入的文本输入项

实践表明，存在相对较少的类型，如果能够识别出来，可以用于索引很多领域，因此出现在很多表单中。例如，邮政编码在很多领域被作为输入项，包括店铺定位、二手车、公共记录和房地产。类似的，日期通常在很多领域作为输入项，如事件和文档档案。我们基于两个想法来利用以上的观察。首先，只有键入适当值，键入的文本输入框才可以生成合理的结果页面。通过这个想法，我们使用已知值为流行类型构建富信息量测试。我们考虑有限的和连续的类型。对于有限的类型（如美国的邮政编码和州的缩写），使用已知值的部分样本进行富信息量测试。对于连续的类型，可以根据不同规模次序，测试使用均匀分布值的集合。我们可以使用这样的输入项名称列表，通过手工提供或者随时间进行学习（参见[Doan 2001]），来选择候选输入项，应用于富信息量测试。

[1]即决定提交查询表单时，需要填写哪些输入框，并找到合适的值填写这些输入框。

结论

我们描述了探寻Deep Web内容的方法，使得搜索引擎可以访问这些内容。该系统最重要的要求是它必须是完全自动化的（因此可以扩展到整个Web)，而且可以从任何语言、任何领域来检索内容。有意思的是，这些严格的要求激发我们探索出一个相对简单优雅的解决方案，从而表明简单性通常是解决复杂问题的关键。

今后探寻Deep Web的工作有很多方向。特别地，可以识别表单中存在的特定类型来扩展爬虫的覆盖度。例如，各个域的组合之间通常是相互关联的（如MinPrice和MaxPrice)，输入有效的精心选择的值的组合可以探寻到更多的页面。

参考文献

[1]Barbosa,L. and J.Freire."Siphoning Hidden-Web Data through Keyword-Based Interfaces."SBBD 2004：309-321.

[2]Bergman,M. K."The Deep Web:Surfacing Hidden Value."Journal of Electronic Publishing，2001.

[3]Callan,J. P.and M.E.Connell."Query-based sampling of text databases."ACM Transactions on Information Systems，19（2）：97-130，2001.

[4]Doan,A.，P. Domingos,and A.Y.Halevy."Reconciling Schemas of Disparate Data Sources:A Machine-Learning Approach."SIGMOD Conference 2001：509-520.

[5]"Forms in HTML documents."http：//www. w3.org/TR/html401/interact/forms.html.

[6]He,B.，M. Patel,Z.Zhang,and K.C.-C.Chang."Accessing the Deep Web:A survey."Communications of the ACM，50（5）：95-101，2007.

[7]Ipeirotis,P. G.and L.Gravano."Distributed Search over the Hidden Web:Hierarchical Database Sampling and Selection."VLDB 2002：394-405.

[8]Madhavan,J.，L. Afanasiev,L.Antova,and A.Y.Halevy."Harnessing the Deep Web:Present and Future."CIDR 2009.

[9]Madhavan,J.，S. Jeffery,S.Cohen,X.Dong,D.KO'C.Yu,and A.Y.Halevy."Web-scale Data Integration:You can only afford to Pay As You Go."CIDR 2007.

[10]Madhavan,J.，D. KO'L.Kot,V.Ganapathy,A.Rasmussen,and A.Y.Halevy."Google's Deep-Web Crawl."PVLDB 1（2）：1241-1252（2008）.

[11]Ntoulas,A.，P. Zerfos,and J.Cho."Downloading textual hidden web content through keyword queries."JCDL 2005：100-109.

[12]Raghavan,S. and H.Garcia-Molina."Crawling the Hidden Web."VLDB 2001：129-138.

[13]Salton,G. and M.J.McGill.Introduction to Modern Information Retrieval.New York:McGraw-Hill，1983.

[14]SpiderMonkey(JavaScript-C）Engine,http：//www. mozilla.org/js/spidermonkey/.

[15]V8 JavaScript Engine,http：//code. google.com/p/v8/.

第10章　构建Radiohead的“House of Cards”

Aaron Koblin和Valdean Klump

这是一个关于获格莱美(Gammy)奖提名的乐队Radiohead[1]的音乐视频“House of Cards”是如何完全根据数据创建出来的故事。在你阅读本章之前，你应该先观看这个视频。该视频权威资料在Gcode项目页面(htp：//code.google.com/radiohead)上可以找到。在该网站上你还可以找到一些其他的资源，包括我们用于构建该视频的数据样本，一个允许你通过3D模式查看数据的Flash应用，一些你可以用来创建自己的可视化，以及一个“Making of”的代码视频。当然首先需要找出这些代码。

这一切是如何开始的

2007年9月，我收到一封来自James Frost的电子邮件，询问我是否对基于数据来创造一个音乐视频感兴趣。James是一个天才视频导演，他为Coldplay、Norah Jones、Pearl Jam以及很多其他流行的艺术家做了很多工作。他看过我的“飞行模式”(Fight Patterns)项目（见图10-1）——使用空中交通的GPS数据对商业飞行模式和密度进行可视化——而且希望碰面并讨论制作一个可视化音乐视频。

图　10-1：来自“飞行模式”的静态图片（2005）（见彩图22）

两个月后，James、他的制片人Justin Glorieux和我在洛杉矶的咖啡厅会面，我们讨论了一些想法。我向他们展示了自己做过的一些项目以及我认为有助于可视化制作的一些技术。我们讨论了使用Proccessing编程语言的一些可能性，该语言已经广泛应用于数据可视化。最终通过这种方式为Interpol乐队制作了“Rest My Chemistry”视频，该视频在2008年3月发行。如果你从来没有用过Processing语言，我强烈建议你访问它的网站：http：//processing.org/，并找出相关代码。就我所知，Processing对于艺术家、设计人员或者任何对动态数据可视化感兴趣的人而言，都是最佳的编程语言。

我们讨论的另一种可能是对激光传感器收集的数据进行可视化。我在加州大学洛杉矶分校(ULA)的嵌入式网络传感(CNS)中心做一个项目时，第一次接触了这种技术。CENS使用激光来检测光线是如何穿透森林的覆盖的，我被渲染后的图片所蕴含的内在美深深地震撼了。James看完一些例子后，同意了我的建议，而且使用激光来制作影片的想法给他留下了深刻的印象。他问：“你的意思是不用摄像机来拍摄视频？不用视频就可以拍摄视频？”他马上意识到可以做一件从来没有做过的事情。在那之后不久，他以这种想法开始制作Radiohead的音乐视频。

希望你能够了解该影片是如何制作的并从中找到自己的工作灵感。在这一章，我将首先讨论我们捕捉数据所使用的工具。然后，我将谈到数据本身即视频拍摄，以及后期的数据处理。最后，我们一起来看我在GCode网站上提供的数据可视化代码，并讨论你可以自己如何发挥它。

[1]Radiohead是一支来自牛津大学的乐队，通常中文翻译为“电台司令”，实际上是指收音机粉丝（因此本书对该词不做翻译，保留原文），乐队由Thom Yorke（主唱，吉他手）、Ed O'Brien（主唱，吉他手）、Jonny Greenwood（吉他手）、Colin Greenwood（贝司）和Phil Selway（鼓手）组成，是20世纪90年代初受华丽壮美的竞技/舞台摇滚所影响的另类乐队。

数据捕捉设备

如果没有一些精密复杂的数据捕捉设备，“House of Cards”视频就不可能成功。当你观看视频，你会注意到有很多场景，从静态的郊区风景到歌手Thom Yorke所唱的动态点云(pint clouds)场景。为了同时捕捉对Thom的歌唱和风景相结合的特写，我们需要使用两种不同的设备：Velodyne公司的Lidar（激光雷达）和Geometric Informatics公司的可视化系统。

Velodyne公司的Lidar

Velodyne是坐落在美国加州的San Jose南部的一家公司，由两个人来运营，他们业余时间会参加机器人大赛如Battle Bots和Robot Wars竞赛。该公司生产扬声器、立体音响设备和强大的激光扫描设备，包括我们用于捕捉“House of Cards”视频所需要用到的风景和派对场景的HDL-64E Lidar（激光雷达设备）。HDL-64E真正声名显赫的原因是它被成功地应用于2007年DARPA城市挑战赛(Uban Challenge)的一些交通工具中，包括获得冠军的团队，成功地把它应用于环境和地形来提高视觉力。在某些情况下，它是这些交通工具的唯一视觉系统。

Velodyne公司的HDL-64E Lidar是一个扫描仪，包含64个激光发射器和64个激光检测器。它围绕一个圆圈旋转，以360°水平方向和26.8°垂直方向旋转收集数据，每秒可以扫描超过100万个数据点，收集到接近5MB的原始数据。默认情况下，Lidar以600RPM（10Hz)的速率旋转，可以通过系统计算机的序列化端口发送文本命令来调整速率，速率范围介于300～900RPM。在扫描静态风景时，为了达到最佳分辨率，我们采用了其最高设置900RPM。

Lidar的可达范围随环境的反射率而变化。例如，沥青路面可以拍摄50米以内的范围，而汽车和树叶（它们的反射率更高）可以达到120米。Lidar最小的可达范围是3英尺，如果更近，那么光线反射回检测器太快，以致无法计量。

发射-检测设备被划分成两组，每组包含32个激光斜岸(Lser bank)，如图10-2所示。上斜岸指向斜上方，与仰角的上半角方向一致；换句话说，它扫描了Lidar的上半个垂直视图域。与此相反，下斜岸扫描了仰角的下半个视角。通常上斜岸仰角更高，因此对于处在远处的一个物体，上斜岸的激光脉冲可以到达的位置范围比下斜岸的要大。为了在更长的距离获取好的分辨率，上斜岸的激光被触发的次数是下斜岸的被触发次数的三倍。从视频的静态图片中可以看到这种方式对我们的数据所产生的影响，如图10-3所示。

Lidar通过发射一束光（即激光束）并测量该光束返回的光量来获取一个数据点。随着设备的运行，这64个发射器每个都释放持续5ns的光脉冲；然后使用透镜对脉冲进行聚焦，再用平面镜把光线反射到外界环境中。如果该光线在外界环境中照射到某些物体，会有一小部分光线反射回Lidar。这种反射回的光线会穿过激光接收透镜和太阳光紫外线过滤器。太阳光紫外线过滤器能够限制太阳所带来的光量；如果没有这个过滤器，自然太阳光线会降低系统的灵敏度，从而给数据带来很多噪音数据。

图　10-2：Velodyne公司的Lidar（图像来源于Velodyne公司）

图　10-3：派对场景的一张静态图片，通过Velodyne的Lidar拍摄。注意该图片上方的分辨率更高，这是由于在上斜岸的激光的触发速率更快（见彩图23）

发射回来的光线经过太阳光过滤器后，接收透镜会把返回的光线聚焦于名为Avalanche Photodiode(APD)的光检测装置上，该装置生成和接收的光线强度相当的输出信号。将APD的输出信号进行放大并把模拟信号转换成数字信号。然后把数据发送给数字信号处理器，该处理器决定返回信号的时间。脉冲的强度和返回时间构成了一个数据点。正如我之前所说，HDL-64E模型每秒创建了超过100万的数据点，即每秒超过5MB的原始数据。

等数据创建完成后，传感器通过标准的100BaseT以太网端口把结果数据输出给用户。数据以等于旋转速率的帧速率（600RPM=10Hz帧速率）不断地从以太网端口输出。以太网数据包中记录了每个发射器-检测器的距离、密度和角度相关的数据。然后通过以太网包捕获程序捕获这些数据，而在我们这个项目中，将会把捕获的数据保存到硬盘上。

Geometric Informatics公司

Lidar是对户外景色进行可视化的神奇工具。它可以根据距离检测一个镍币的大小，在较大的空间中工作良好，但是对于人脸的轮廓和细节的识别，它还不够完善。为了对Thom Yorke的歌唱进行特写，我们需要一些其他的东西。我们需要有更好的视觉。

当谈到如何近距离拍摄特写时，我突然想起在2005年SIGGRAPH会议上发现的叫做Geometric Informatics公司的设备。（题外话：如果你有任何很酷的数据可视化技术，请到每个会议上展示这些技术，我有可能会参加这些会议。谢谢。）Geometric Informatics公司在该会议上有个展台，演示其称为GeoVideo的系统。

GeoVideo是一个实时动作捕捉系统，它尤其适合捕捉人脸的几何图形。在近距离拍摄时，它的效果明显优于Lidar系统，它能够检测到0.2mm的数据点，而Lidar只能检测到2cm的数据点。有了GeoVideo，我们能够捕捉到Thom Yorke唱歌时的具体细节。你在视频开始看到的点云数据(pint cloud data)就是通过GeoVideo捕捉的。

如果你认为图10-4所绘的图看起来挺简单，那是因为该设备外表看起来确实没有很多特别的地方。系统看起来像一个米色的箱子，每面有个胶卷，有两个透镜。一个透镜把一个区域的光线投影到箱子前面，而另一个透镜则捕捉该数据。光线区域包含60万个直角的网格，该网格在效果上形成一个即时等高线图，投影到传感器的前面。然后，传感器读取每个直角点作为一个数据点，然后把这些原始数据以每秒54MB的速率，不加任何改动地输出到一台计算机上。传感器每秒可以捕捉180帧。

采用GeoVideo的方法把光线区域投影到物体上的优点是，无论传感器前面需要被投影的是什么东西，都不需要在其上画上一些网格，不需要穿着用于动作捕捉的“衣服”，也不需要坐在一个包含参照物标记的绿色屏幕前。光线投影创造了一个即时、可移植的参照图，其使用之简单达到了令人难以置信的程度。

图　10-4：Geometric Informatics系统（图像来源：Geometric Informatics公司，见彩图24）

GeoVideo系统也支持纹理映射，这意味着它不仅可以捕获数据点，而且可以捕捉这些数据点之间的纹理。综合而言，它可以达到对物体或者人脸进行精确的3D表现的效果。但是对于“House of Cards”视频，我们决定放弃纹理捕捉，而只使用其数据点。即使如此，我们还是进行了大量采样。其结果是你在视频开场看到的Thom Yorke的数字化的“点云”。该图像看起来和Thom Yorke并非完全一致，而是像个数字化的头像或者精灵——至少，我觉得是这样。看了两个版本的数据——包含纹理的和不包含纹理的——我认为不使用纹理的那个版本看起来更有趣。有了纹理，他看起来有点像视频游戏中的一个角色。有时抛弃一些数据会使可视化效果更美丽。

两种数据捕捉系统的优点

值得一提的是，自从为这部影片工作后，我开始对在同一个项目上使用多个数据捕捉系统变得很感兴趣。混合技术在创造力上可以产生事半功倍的效果。我们本来可以只使用Lidar系统或者GeoVideo系统来拍摄整部影片，但是我很高兴我们采取了不同的方式。这可能使项目更复杂，但是它使我们变得更具有创造性。对于我们需要捕捉的东西，它也给我们带来了更多的灵活性。

在寻找设备方面，你也可以从我的经验中获取一些教训。第一，如果要为数据可视化寻找设备，那么要找遍每个地方。有很多令人兴奋的传感技术被开发出来但却从未使用过。如果你准备启动一个可视化项目，首先在贸易展览上或者你所在地的大学做一些在线调研。确认是否有新的、你未曾考虑过的方式可以捕捉数据。不同的设备可能会给你的工作增加新的主题或者显示出你之前没有看到的数据。总之，尽力去寻找可以给人震撼的可视化技术。

第二个结论是一个警告：如果你只使用一种设备，那么你的工作看起来可能仅仅是该设备的演示。如果我们只使用Lidar来创造“House of Cards”视频，我有点怀疑该视频是不是会成为“Lidar视频”。通过同时使用GeoVideo系统和Lidar系统，最终产品不会被贴上某个产品的标签。单一工具是无法定义该作品的。混合使用两种数据捕获系统使得该视频后面的故事变得更有趣。

数据

在我讨论拍摄之前，我想先展示一些样本数据。这些数据其实非常简单。以下是我从Thom Yorke的歌唱数据中任意抽取的三个数据点。它们在文件2067.csv中，它是该视频的第1067帧。因为每秒包含30帧，而且第一个帧是1001.csv，这些数据点可以在视频播放约36秒时看到。你可以在GCode网站上看到这个数据文件，以及一些其他的帧：

70.05，162.48，-79.32，122

70.23，165.26，-78.82，112

70.46，168.00，-77.55，95

这些数据以（x,y,z）的强度格式表示。我们捕捉到的所有数据最终都转化为这种格式。

（x,y,z）值是相对测距。GeoVideo系统和Lidar系统相似，包含（0，0，0）点，基于该点构建所有其他点。因此，70.46是指该点在x轴距离0点是70.46个单元距离。你可以任意扩展这些点。其强度范围是从0（0%白色）到256（100%白色）。

在GCode网站，你可以找到Thom歌唱的2000帧有效数据文件，它们组成刚刚超过1分钟的视频。也可以访问音频数据。我们还包含了两个静态景观的有效数据文件：城市及其尽头的小路。它们在可视程序所在的压缩文件HoC_DataApplications_v1.0.zip中。

捕捉数据，即“拍摄”

2008年5月，我们在佛罗里达州的Palm Beach县花了一个周末还多的时间采集“House of Cards”视频的点数据。Radiohead那时正在那里巡演，而且他们为了减少巡演时的旅行，想要拍摄音乐视频。想要了解该产品的幕后花絮，请在GCode网站检索本章最开始处提到的“Making Of”视频。

户外Lidar拍摄

我们到达佛罗里达州之后所做的第一件事就是在制片小组租的一辆旧面包车的后面安装上Lidar。我们使用该面包车来捕捉在视频中看到的静态的景观数据，如城市及其尽头的小路。

不像DARPA城市挑战赛的汽车，我们没有把Lidar放到汽车的上方，而是把它倾斜了90°度，然后装到面包车的后面。这意味着激光可以垂直扫描环境。如果对这种拍摄方式感到困惑，你可以想象一个灯塔倾斜在一边，粘在车的后面，像一个管道的尾巴。这意味着激光器从街道旋转扫描到天空，然后再回来。每分钟这样反复旋转900次。

我们这么做是因为它能够提供非常高分辨率的区域扫描。而且事实上，在后期处理中，我们从64条激光中只分离出一条激光，这是因为它们都非常有效地扫描了同样的东西。随着面包车向前行驶，激光每次旋转都扫描了环境的唯一独特部分。

图10-5的景观就是通过这种技术捕捉的。面包车径直行驶在街道的中央。你是否发现街道上的线和街道本身垂直？这是因为Lidar是挂在背面，而且是朝下的。你可能还会注意到在公寓塔楼边上有曲线，这是由于面包车的行驶和Lidar的旋转角度交叠在一起。

图　10-5：Velodyne Lidar的一个激光器捕捉到的数据（见彩图25）

拍摄过程进行得非常顺利。制片组已经找好了位置，因此我们径直开到每个场景，并顺序扫描它们。当我们到达了想要扫描的地方，我们就会放慢速度到10mph左右，司机会尽量把车开得平稳，然后我们就开始记录。

和摄像机不同，Lidar不会启动和停止。相反，它总是运转着，不断地旋转和输出数据。因此，我们不需要打开或关闭它，而只需要知道什么时候开始记录数据。当要开始记录数据时，我们的导演助理Larry Zience会以对Velodyne的现场工程师Rick Yoder喊“开动电脑”作为指示，Rick Yoder就会开始收集数据点。（这对于一些制片组成员而言，显得有些可笑，因为通常导演会说“开动摄像机”。）然后，Rick点击他的笔记本电脑上的一个按键，Lidar数据就开始输出到他的硬盘上。当Larry喊“停”时，我们就停止记录。

后来，Rick发给我一封信，是关于和制片组一起工作的感受：

我内心的一个感受就是这打破了一位摄影师要找到合适的镜头角度是非常困难的习惯看法。传统的摄像导演不习惯于从一堆扫描中构建模型，然后在后期处理时操纵相机的角度。他们会说：“我们想要把相机从这里移到那里，然后向上移动大约20英尺，而镜头保持约45°。”我们会说：“好极了。我们只需要把扫描器放在场景中间，你就可以在后期处理中这么做。”

图10-6显示了另一处景观。注意电线为什么会是弯弯曲曲？这是一个很简单的原因：面包车在不平滑的路上上下颠簸产生的自然的曲线。通常，这些“错误”可以通过陀螺仪、加速器以及其他精密设备来避免。在我们这种情况下，我们想要这样的“错误”。不但处理起来代价更低、操作更简单，而且它使得数据也更有趣（至少我这样认为）。完美是值得赞美的目标，但是它并非总是最具有创造性。

图　10-6：Velodyne Lidar捕捉到的另一幅景观图片（见彩图26）

室内Lidar拍摄

我们还把Lidar用于室内拍摄影片背景。它被用于捕捉视频中3：30和3：55时刻的派对场景。和景观场景不同，我们在这部分拍摄中使用了所有的64个Lidar激光器的数据，而不是仅仅一个。这是由于派对场景是动态的——Lidar每次旋转，点数据都会随之变化——这意味着这些点数据在视频的每个帧都有变化。因此，你看见场景中的人们正在运动。在这部分拍摄中，我们使用了正常水平放置的Lidar，这是数据以水平线方式显示的原因。

为了创建派对场景，我们从邻近的学校招募了一些电影专业的学生。有些学生精心打扮了一番，认为他们会出现在Radiohead视频中，这是他们展现自己的机会；他们几乎没有意识到，事实上我们真正需要的只是他们的身体形态。抱歉，伙计们！

如果你数一下图10-7所示图片的水平线，你会发现有64条。注意为什么图片的上半部分更亮？那是由于Lidar上方的32个激光器比下方的触发得更快。正如我之前所述，Lidar这种构建方式是由于它通常扫描很大的地形空间，要到达水平升起的地方需要更高的分辨率。

图　10-7：派对场景的一个静态图片，Velodyne Lidar捕捉，数据有64条线，每条线由Lidar的一个激光器生成（见彩图27）

遗憾的是，Lidar的分辨率很低，每个数据点约有2cm，这也是图片看起来很模糊的原因。在我看来，这一点反倒给视频增添了一丝韵味。派对通常是由那些不太认识的人组成的，而这种可视化方式能反映出这种隔离的“味道”。然而，Lidar的低分辨率无法满足对Thom Yorke拍特写。因此，我们使用GeoVideo系统。

室内GeoVideo拍摄

Thom Yorke的点云图、他的“爱人”（由Lauren Maher扮演，在视频的1：05时刻你第一次看到她）以及一些其他场景都是通过Geometric Informatics公司的GeoVideo系统采集的。

GeoVideo系统能够实现令人震撼的现实主义。如果你看了Geometric Informatics公司的网站上的演示视频，你会发现其图片的质量比我们视频中的点云要高得多。它的可视化也没有我们视频中遇到的干扰和错误。

我们的视频质量低的原因是我们故意这么做。导演James Frost并不想要一个完美的Thom Yorke的视觉化身，他想要看到的是Thom Yorke深情、缥缈的视觉幻影（见图10-8）。当观看视频的开场时，在我眼里，他不是Thom Yorker这个人，而是这位歌手的灵魂。我们在机器里看见的是幻影。

图　10-8：歌手Thom Yorke在Radiohead视频中的一张静态图片（见彩图28）

可视化中数据质量的低下和频繁错误，也使得获取数据看起来很困难。这种似是而非的困难增强了故事性。一张清晰的图片无法表达我们想要表达的意思。

在后期数据处理中，我们没有对数据中的干扰进行干预；相反地，我们为数据集创建干扰。制片公司购买了很多道具来打破现场的数据，包括把一些很小的镜子粘到一面有机玻璃上，在扫描器前洒一些羽毛，在Thom前把水倒在有机玻璃上来制造“流水”景象。最后，用镜子把数据干扰成无组织的方式效果最佳；羽毛在数据干扰上的效果差强人意；而水吸收光线，在数据集中只是生成一些空白点。

室内拍摄花了大约10个小时的时间，包含GeoVideo和Lidar。对于所有包含Thom的场景，为了防止数据丢失，我们很小心地把数据存储到了多个硬盘上，因为担心这些数据万一丢失，我们可能就无法再拍摄出同样的效果了。

处理数据

所有的数据都捕捉完成后，开始了数据处理工作。我们做的第一件事是把原始的Lidar数据发送到510 Systems公司，一家在加州伯克利的工程公司，对处理这种类型的数据有很多经验。该公司把该项目分配给其内部的Lidar数据大师Pierre-Yves Droz。他在把Lidar数据转换为可使用格式领域是专家。

我们把数据以DVD方式邮寄给Pierre之后，他为我们做了两件事情。

首先，对于景观场景，他把一个激光器和其他63个激光器分离开，为这个激光器的点创建了一个数据集。其次，他把所有的原始Lidar数据，包括动态派对场景数据，都转换为包含（x,y,z）和强度的很多单点数据。

其次，为了对原始数据进行转换，Pierre需要知道每组Lidar激光发射器和检测器的精确位置和方向。计算信息是由Velodyne公司提供的，对于每个Lidar单元，其参数都不一样。Pierre还使用面包车的速度来帮助计算Lidar在真实世界中旋转时运动有多快。

最后，我们提供给510 Systems公司大约4GB的原始数据，却生成了接近50GB处理后的.obj格式的文本数据。

后期数据处理

我们一处理完数据，就把它交给The Syndicate公司，在加州Santa Monica的一个视觉效果工作室。它把场景渲染成二维，并添加了粒子流效果，在视频开始约1分钟左右，你可以看到该效果。

The Syndicate工作室的粒子专家Brandon Davis致力于这个项目。他发给我一封邮件，告诉我为什么这个项目很不寻常：

一开始，从可视化角度考虑，Radiohead项目有很多不寻常的可能性。有了生动的数据集合，你就会充满奇怪的“矛盾”：依赖于视觉的数据在视觉上却可以看做独立的。这真的是“第二景象”，能够带一个人欣赏其看到的，并从不同角度来享受这场视觉盛宴，暴露景象的差距。这打开了一些真正独特的图像大门。

Brandon Davis继续描述他是如何处理“蒸发”效果，你将会在视频中注意到这些效果：

客户想要随着时间而退化的数据集，似乎这些点正在被一场虚拟的风刮走。从刚开始，我们看的就是两种显著不同的数据类型——环境的静态Lidar点云和歌手Thom Yorke的动态生动的点云。我们认为后者操纵起来会更有挑战性。静态的数据集相对更容易操纵，因为所有你需要做的是随着时间取代这些点，因此我们知道选择性地触发受重力场影响的部分数据集不会太困难，从而创造出一种建筑物和树都随着喧嚣的巨浪一起蒸发的效果。正如我们所预测的，在静态数据集上生成该效果只是把跳跃的边框移动到点云，在一帧帧的基础上完成剪辑测试。如果一个点在某个特定帧的内部，就把该点传给粒子系统，这样它可以自由地通过噪音区域，否则它就保持静止状态。这是非常有交互性的，容易控制，且基本上迭代迅速。

然而，Thom歌唱的动态点云却是另一回事。Brandon继续说：

从我的角度看，动态点云是该项目的最大挑战。虽然你可以使用和静态数据集一样的方法，但是动态数据集有其自身特有的问题：它会刷新每个帧！只要你发布一个点，并把它传给一个粒子系统，原始的源点就会反弹到新的位置，基本上重新生成每个帧。因此，虽然我可以指定数据点在他的鼻子上落下，然后再飞走，随后的每个帧都有一个新的点来取代飞走的那个点。

最后，The Syndicate找到了一个简单的方法，使用2D面具增加动态点云的衰减效果：它在3D点云上给面具添加了一层吹走的粒子。这意味着不存在像静态景观那样完美的一对一的粒子衰变，但是我认为这些变化是无法察觉的。

当我咨询导演James为什么他在视频初始时增加了粒子衰变的效果，他说：

“House of Cards”本质上是一首爱情歌曲，和爱情或友情一样，在那里有关系破裂。它们不仅仅停止，而是有一系列的事件活动，一种催化剂导致它们终止或衰落。对于视频，我想在更大范围内探索这个主题。在我看来，在生活中我们对于周围的世界知之甚少。我的想法是基础设施可以在我们周围大规模地崩溃，但是由于我们没有感受到其影响，我们作为人类已经达到对什么都满不在乎的地步。但是，最终该事件会产生连锁效应，而且最终会在我们个人层面上带来影响。金融机构正在发生的事就是一个最佳的例子；一家银行倒闭了，但是除非你把钱存在那里，否则你就不会去关心它。然后你的钱所在的银行也倒闭了。

当完成了所有的后期处理，编辑委员Nicholas Wayman Harris和我们一起编辑了剪辑。视频终于完成了。

发布视频

“House of Cards”视频是在G公司网站上首映的第一个音乐视频，它在2008年7月11日发布。G公司站点包含了一些该视频的数据，因此你可以创建自己的可视化以及3D数据可视化工具。G公司的创新实验室开发了这个站点。

该可视化工具是由我和朋友Aaron Meyers一起用Flash开发的。它允许观看者在播放视频期间实时旋转云点数据。对我来说，这正是数据美丽之处。Flash应用允许你实时从任意视角查看视频，这是传统的视频记录所不能及的。你可能旋转Thom Yorke的脸庞，这样他从远处望着你，有效地把持住他的脸作为你的面具，你就可以透过他的眼睛。在我看来，这种效果非常强大。它使得音乐视频在某种方式上变得有形，我怀疑很多人都没有过这种体验。

我们还发布了一些数据（把数据开源）以及通过Processing编程语言开发的视频创建工具。然后，我们鼓励人们下载数据来创建他们自己的视频。

我想为视频创建工具分享源代码，以便展示通过Processing创建你自己的视频时多么简单。以下是输出Thom Yorke歌唱的帧的代码：

import processing.opengl.*；

int frameCounter=1；　//Declare a variable to store which frame we're dealing with

void setup（）{　//Here we set up the program

size（1024，768，OPENGL)；　//This is the render size.We'll use OpenGL to draw as

//fast as possible

//frameRate（30）；　//Uncomment to watch the animation at 30 frames per second.

strokeWeight（1）；　//Draw lines at a width of 1，for now.

}

void draw（）{　//Here we state the things we're going to do every frame

background（0）；　//We'll use a black background

translate(width/2，height/2）；　//The data has 0，0，0 at the center and we want to

//draw that point at the center of our screen

translate（-150，-150）；　//Let's adjust our center slightly

scale（2）；//Let's draw things bigger

//rotateY(frameCounter/50.0f）；　//If uncommented,this makes the data rotate over

//time

//rotateY(mouseX/150.0）；　//If uncommented,this uses the mouse's horizontal

//location to adjust the rotation

String[]raw=loadStrings(frameCounter+".csv"）；//Here we load the current frame

//data into an array

for(int i=0；i＜raw.length；i++）{　//Now we loop through each line of the

//raw data

String[]thisLine=split(raw[i]，'，'）；　//For each line we're going separate

//each parameter

float x=float(thisLine[0]）；　//Now we make a decimal variable for each

//parameter

float y=float(thisLine[1]）；

float z=float(thisLine[2]）；

int intensity=int(thisLine[3]）；

stroke(intensity*1.1，intensity*1.6，200，255）；//We set the color of each point to

//correspond to the data's

//intensity value

line(x,y,z,x+1，y+1，z+1）；　//Here we draw a little line for each point；this

//is much faster than a more complex object and//we'll be drawing a lot of them

}

frameCounter++；//Add one to the frame variable to keep track of what frame we're

//currently on

if(frameCounter＞2101）{　//If we get to the end of the data we'll exit the

//program

exit（）；

println（"done"）；

}

//saveFrame（"renderedFrames/"+frameCounter+".tga"）；　//This would be a way to save out

//a frame

//*remember you're saving files to your harddrive！*

}

它没有数据那么美（毕竟本书不是《代码之美》），但是它的效果很好。正如代码所示，它允许你从头到尾观看Thom Yorke的歌唱，但是有一些修改，你可以定制这个过程。以下是两个修改实例，基于之前的代码做出注释。第一个修改实例是：

rotateY(frameCounter/50.0f）；

把绘图函数的起始部分的这行代码取消注释，将会导致Thom的脸随着帧的增加而向y轴旋转。

第二个修改实例是：

rotateY(mouseX/150.0）；

把绘图函数起始部分的这行代码取消注释，允许你对鼠标函数进行旋转。你现在可以随着帧的输出移动Thom的脸。

我相信你能够想到一些其他方面来修改。很多人做了我从来没有想到的事情，这正是我期望的。对所有的帧进行渲染（把最后一行取消注释）并放在一起生成一个视频，就可以制作类似QuickTime Pro、Final Cut或After Effects这样的节目了。其他人创建的一些视频令人印象深刻。在Youtube的“House of Cards”组(htp：//www.youtube.com/group/houseofcards)上看到这些视频。

这一切真的都非常简单；只需要在开始实践时有一些美丽的数据。

结束语

在写本章时，我有7个想法，在你通过美丽的方式来捕获和表示自己的数据时可能会觉得有用：

1.通过新的方式看普通的东西会使它变得很特别。

你不需要扫描月亮、热带岛屿或者一位流行模特来获取美丽的数据。通过新的方式来观察普通的东西可以产生相同的效应。对于“House of Cards”视频来说，我们扫描了一个人的脸以及一些郊区建筑。通过新的方式来观察这些平凡的东西，以及通过新的可视化技术，我们可以使事情变得有趣。

2.讲一个故事。

显然，如果你有一首令人震撼的歌，它对你的工作会有帮助；但是，也尽可能地根据你的数据讲述故事。给这些数据增加一些含义要比仅仅展示数据更加生动。

3.使用多种可视化技术比单一技术会更有趣。

正如我之前所述，“House of Cards”视频是通过多种技术如Velodyne Lidar和GeoVideo系统，以及粒子衰变后期处理效果而变得更加强大。如果我们所做的所有都是可视化原始Lidar数据，那么它就不会很有趣。

4.想想数据，而不是真实世界。

当我们给有机玻璃板增加一些镜面，在Thom唱歌时把它移到他脸前时，我们没有想在视频相机里它会是什么样子。数据即是产品。当你看一些你想可视化的东西时，想想你可以从中获取的数据。

换句话说，尝试使一件很离奇的事情变成现实，然后感受这种现实。这会使你的故事古怪离奇。音乐视频通常描述离奇的一面。问问自己，你该如何操作数据使得它更稀奇、有趣和与众不同？

5.不需要使用全部数据。

我们使用GeoVideo系统来扫描Thom Yorke生成的数据超出我们想要的。通过大范围抽样，我们生成了更有趣的数据点。我们想要的不是一张照片。

6.把数据开源。

通过使得其他人可以拥有这些数据并创建他们自己的“House of Cards”视频版本，使得每个人可以创建自己认为最美丽的视频。每个人总是对于什么是最好的有自己的观点。让人们“纵容”自己的观点。如果你见到他们的作品，你会感到高兴且满足，你可能永远都不知道，有些人做了一些意想不到的事情。这是件好事。已经有超过10万人从GCode网站下载了数据并创建了一些伟大的视频。

7.和Radiohead乐队一起工作。

可能显得有点不够严肃，但是毫无疑问，我们很幸运能够和世界上最有创造性的乐队一起工作。而且不仅仅是他们，而是整个团队。只有和一些无比卓越的天才一起合作才使得这个视频变成可能，包括James Frost，以及Velodyne、Geometric Informatics、510 Systems和The Syndicate的员工。和这些比你更具智慧的人合作，你的项目会受益良多。

第11章　都市数据可视化

Michal Migurski

引言

数据因何而美丽，它又来自哪里

美丽的数据有趣、有用、公开且免费。数据必须使它的收集者、观众或者某个机构能够对它感兴趣。它必须对那些感兴趣收集和维护这些数据的人有用，可以帮助他们理解自己所处的环境。当数据是公开、免费、允许查看且可以对其自由辩论时，它是最美丽的。

这是一个关于奥克兰的Crimespotting项目(htp：//oakland.crimespotting.org)的故事，它是位于旧金山市的Stamen Design设计公司(htp：//stamen.com)的一个研究项目。Crimespotting项目（见图11-1）是为了响应当前奥克兰警察局的犯罪报告的一个应用——CrimeWatch(http：//gismaps.oaklandnet.com/crimewatch/）而开发的。正如很多其他项目那样，Crimespotting在最初构想时并没有具体的目标；它的产生是由挫折引起的，借助于基础、技术性的研究逐渐成熟起来，并最终因一起给人们带来创伤性的犯罪事件而变得众所周知，因为该犯罪事件引起了全国对该城市的广泛关注。Crimespotting项目的发展看起来经历了典型的项目发展曲线：经过有目标的酝酿，通常会发展演化成为成熟的信息化项目。该项目成为Stamen设计公司的顾问Ben Cerveny所谓的“信息化”的典型例子：世界上的数据正一点点地移动到互联网，并与某个开源工具链和方法论相互接触和碰撞。

图　11-1：Stamen Design设计公司的一个研究型项目——奥克兰市的Crimespotting项目标志

这个故事包含三个部分。首先，我们获得了奥克兰警察局的核心数据，将其抽取出来转化为更适合于“切片混合”操作的某种格式。其次，我们创建了一个动态网站，对数据进行公开，以便本地的居民可以找到并使用这些数据。最后，我们通过观察这个网站如何发挥作用，回顾最开始的设想，并答复公众的反馈意见。

背景

将现代的发布在线数据的方式应用于犯罪举报并不是什么新主意。当前对犯罪的关注可以追溯到记者/开发人员Adrian Holovaty在2005年研究的Chicago Crime项目(htp：//chicagocrime.org)。Chicago Crime项目是GMap混搭的一个早期例子，它利用来自其他站点的代码和数据打造了一个卓越的网站。在这个案例中，尚未文档化的GMap API(http：//code.google.com/apis/maps/）被看做发布芝加哥警察局的犯罪报告信息的基础组件。该警察局本身的网站是由文本驱动的，它刚好包括了每个报告的街道地址或十字路口。应用服务程序Holovaty在夜间收集报告数据，把它们发布到动态的、可缩放或“滑动”地图上。该服务作为早期G公司2005年在线映射的最佳实践，别出机杼，是个非常绝妙的方法技巧。GMap从原来的局限于只是画图和展示静态图片，转换成为可以展示不断滚动、可以无限回应的地理数据平台。

Chicago Crime项目不是唯一的。几乎是在同一期间，开发者Paul Rademacher开发了Housing Maps(http：//housingmaps.com)项目，它类似于Chicago Crime项目，是公寓出租数据和可视化浏览的组合体。在其之前一年，Michael Frumin和Jonah Perretti在New York艺术基金会Eyebeam创建了FundRace项目(htp：//fundrace.org)，实现了2004年总统竞选政治捐款数据的可视化。Rich Gibson、Schuyler Erle和Jo Walsh合著出版的《Mapping Hacks》（O'Reilly出版社）一书，是一本关于新的基于Web制图的技术指南。针对这些事件，开展了很多在线活动，因为在地理信息上下文中，新的信息化数据集正在大刀阔斧地修改并重新发布。这一切所产生的更广泛的影响改变了人们对地图的期望：人们现在期望有新的、深层次的多规模交互方式；作家Steven Johnson称之为“长距离变焦”(Te Long Zoom)，一种包含游戏、电影和其他媒体的新视野。

GMap对于Web地图领域的影响是不可估量的，其中有两个原因。首先，GMap做出了采用了图片拼接的方法来发布地图的决策。这使得它可以不用重新渲染全部图片，减少了对服务器的压力，因而能够采用视觉上更复杂的制图技术。其他地图需要效果非常显著，才有资格与其竞争。其次，这个决策也意味着最后的可视化效果将变成由用户端的浏览器通过动态组合的方式完成。最近，Jesse James Garrett提出了一种关于浏览器的新兴技术——Ajax（异步JavaScript和XML)，这一术语后来变得尤为重要。Ajax技术说明了现代的浏览器已经有足够高的质量，且当它与标准足够兼容之后，人们开始重新开始对动态HTML和JavaScript感兴趣。采用这种方式后，原始数据的传输从制图展示中独立了出来，在一个单独的频道中完成。而在此之前，其采用的方式是在服务器端完成地图的生成、地址查找和确定方位等处理之后，将所有的数据打包成一张图片并将整张图片作为返回结果展示给Web访问者。

把地图分块、接口和数据组装放在客户端完成，这一点使得小开放团队创建非常复杂的基于地理的浏览应用变成可能，这也正是在我们和Web四年交互过程中所看到的。

所有这一切构成了从奥克兰现有的、获得商业授权的犯罪地图产品CrimeWatch中抽取可管理的、可重用的数据的努力的主要背景。它最初是由简单的技术好奇心所驱动，以及圣诞节的很多空闲时间无意中所带来的“背后一击”。

解决棘手问题

奥克兰的CrimeWatch是一个提供犯罪报告信息的应用，以图片的形式提供犯罪报告相关的原始制图和卡通式的图标。CrimeWatch为便于数据展示进行了专门优化，而且侧重于预测用户需求而不是提供原始材料。应用的用户体验是通过“向导小工具”(Wzard)来通知的，它是一个用户界面，提供给用户一系列的对话框，它们生成了一系列的步骤，通过特定的序列执行任务。CrimeWatch应用的必要步骤如下：

1.事件：选择事件的类型（一种或者多种）；

2.地点：在地址附近搜索，在可管理的范围内；或者是某个特征附近，如学校或者公园；

3.时间：在多长的时间范围内进行搜索。

CrimeWatch通过静态图片来响应，该图片以图表的形式显示各个报告。

点击这些图标可以获取更多的信息。第一次激发了对CrimeWatch的兴趣是在我开始思考某种方式用来颠倒服务端完成归并过程；从静态的图片开始，通过附加的地理信息来抽取犯罪报告信息：使用和那些其他地理软件系统兼容的经纬度值，通常被称为地理定位(golocation)。这种简单的识别问题很容易理解，而且在视觉特征抽取方面已经存在着很好的技术。

首先，我们需要获取一张图片进行处理。这过程实际上比看起来要复杂得多，我们必须跳过一系列的“钩子”(hok)，“说服”服务器生成一张犯罪报告图片。CrimeWatch在服务器端保存了会话状态，因此有必要通过一个假的虚拟用户来模拟一套完整的向导互动模式：通过表单接受服务条款，然后通过交互向导继续完成各个步骤，在这些过程中保存HTTP Cookies和令牌(Tken)，然后对非标准的HTTP重定向进行正确地响应。重构这些步骤过程，从而生成有用的犯罪报告图像是该项目面临的第一个难题。幸运的是，客户端的HTTP代理软件Charles(http：//charlesproxy.com/）和Mozilla插件LiveHTTPHeaders(http：//livehttpheaders.mozdev.org/）简化了这种重构过程。解释中间HTML页面本身是通过使用网页刮屏(pge-scraping)库的方法，如Leonard Richardson的BeautifulSoup库(htp：//www.crummy.com/software/BeautifulSoup/）。BeautifulSoup库是为了使通常看到的HTML“标签”变得有意义，纠正了某些通用的问题如不正确的嵌入式标签或者部分标记，而且它允许我们读取HTML表格和JavaScript脚本命令，这些构成了一个完整的客户/服务器会话。

可以使用简单的Unix命令行工具如Shell脚本和网页访问工具cURL(http：//curl.netmirror.org/）来模拟第1版的刮屏过程。其关键是仔细检查浏览器和服务器之间的HTTP连接，寻找暴露出来的信息可以帮助重新构建交互。查看URL中包含的CGI变量和Post方式请求体是重构的第一步，它可以准确地表示接受了使用条件后的初始的会话连接。基于会话的应用如CrimeWatch大量地使用了Cookie来保存客户端的状态，因此，利用HTTP库把Cookie打成Jar包是必需的。CrimeWatch还大量依赖于客户端的JavaScript框架，而不仅仅是简单的表单提交；此外，它还使用了额外的状态变量，这样中间响应页面必须通过HTML解析器和正则表达式才能够搜索隐藏在页面脚本中的详细信息。最后，由于很多这种老一代的Web应用是在跨浏览器的动态HTML被开发者普遍应用之前出现的，因此“欺骗”User-Agent头数据，假装是IE浏览器或者Mozilla火狐浏览器绕过User-Agent头数据通常是必要的；其他浏览器会显示不兼容报警信息，不返回任何数据。

在这个过程结束时，只剩下中等大小的图片位图，期望能够包含可识别的犯罪报告图标。通过第一轮抽取每个图标的像素位置很简单，但是过程很慢；对于图像中的每个可能的位置，把它的像素颜色和已知的图标做比较，一旦差异小于某个给定的阈值，就显示正的匹配度值。因为我们是在噪音相对较少的后台来处理预先指定的图标，这实际上是一种完全的“防弹”方法。CrimeWatch中使用的犯罪图标是独一无二的，而且即使部分被其他图标或者图片特征所屏蔽，但是还是可以很容易识别出来。我采用的检测这些图片的工具是NumPy(http：//numpy.scipy.org/），它是一个久经考验、功能强大的Python数组操作库。图11-2和图11-3显示了CrimeWatch的部分样本图片，突出显示了程序可识别的图标。

图　11-2：CrimeWatch的样本数据显示了盗窃、毒品、抢劫、盗窃车辆等犯罪活动（见彩图29）

图　11-3：CrimeWatch相同的样本数据，突出显示了程序可识别的图标（见彩图30）

遗憾的是，蛮力方法在普通的CPU上很慢，但是通过对所需处理的各种地图的一些简单分析，也许可以对其速度进行一些优化。例如，很多犯罪报告图标有很显著的特征颜色：通常货币形状的绿色袋子表示盗窃，蓝色拳击手套表示普通伤害。一种简单的预处理步骤是通过代价较低的图片扫描来找到与其中一个期望的颜色接近的像素的位置，这种方式可以极大地降低代价高的全图标匹配的位置数目。图11-4用白色正好显示了图11-2的红色部分，它表示严重伤害可能发生的场所，可以看到有红色的拳击手套。

图　11-4：CrimeWatch的相同的样本图片，红色的部分用白色显示是为了更清晰地突出红色的拳击手套图标（表示严重伤害）

一个较为复杂的预处理步骤是通过一系列的扫描来搜索临近的几种特征图标颜色。例如，盗窃是通过黑白相间的破碎的玻璃小图标来显示的。一张经典的地图上包含很多的黑色和白色，但只有图标和文本会黑白相间。我们会找到所有和这两种颜色很相近的像素，并把高代价的搜索区域降低限制为只包含有限的候选像素。

每次犯罪报告的地理定位过程都需要根据在渲染地图中检测到的像素位置来确定位置。为了实现这一点，对于给定的一组输入，CrimeWatch总是返回可预测大小和位置的地图是有帮助的。例如，3号警务服务区域(Plice Service Area)的地图需要总是覆盖相同的区域，而不用考虑当时实际的犯罪报告是集中于区域的某个角落还是扩散到所有地方。CrimeWatch提供的地图包含的底层地理特征如街道或海岸线，总是在相同的地方。对于每一种可能的地理布局方式，需要人工定位三个大间距的已知参照点。道路交叉口对于这点非常合适，因为在CrimeWatch上可以很容易地定位它们的（x,y）像素坐标，而且通过Simon Willison的GetLatLon应用(htp：//getlatlon.com)，也可以很容易地定位道路交叉口的地理位置（经纬度）。给奥克兰的6个警务服务区添加三个参照点相当于手工创建了18个已知的地理定位。该步骤只需要做一次：在每个服务区，所有能在地图上找到的图标，都可以利用简单的线性算法，通过和已知的参照点比较，计算出它们的地理位置。图11-5显示了商业区邮政编码的地图，它包含选定的三个地理参照点。定位了这三个点之后，就可以三角测量地图上的任何其他点的地理位置。

图　11-5：奥克兰商业区地图，为三角测量所显示的三个参照点（见彩图31）

唯一剩下的一件事就是模拟用户点击每个犯罪图标，收集关于犯罪报告的进一步详细信息，比如案例编号、每天的日期和时间以及简单的文本描述。其生成的最终产品是每天包含大约100个报告的数据库。该步骤面临的一个挑战是确定一个独特报告是由哪些构成的。我采用移动窗口来收集报告，这意味着每个报告都会被收集多次，而多个独立的报告可以通过警察局提供的唯一案例编号覆盖成一个。最后，我们使用了一组案例编号和文本描述，它们足够覆盖数据收集中绝大部分的不一致性。

实现上述功能的代码运行于事件驱动的网络引擎Twisted Python(http：//www.twistedmatrix.com/）上。采用这种引擎使得打开并保持一个长时间持续运行、使用CrimeWatch提供服务的模拟浏览器的会话变成可能。有了这些代码库，可以把脆弱的程序转变成每天晚上连续运行和收集数据的应用程序，而且结果数据也可以以表单方式公开展示。我们相信对于奥克兰市的居民而言，表单方式显示要比采用CrimeWatch显示显得更为友好。

晚上收集这些数据为最初8个月的收集和实验奠定了基础。每天晚上，我们会在13种类型的犯罪和6个警务服务区的全部组合上运行一个网页刮屏工具(Pge Scraper)。由于CrimeWatch的一次性设计，每个个人报告需要有它自己的请求/响应循环。我们还为每个步骤增加了很多延迟——在这个过程中，每两个步骤之间高达一分钟或者更多的延迟——因此不会由于过多的请求而使CrimeWatch服务器负载过高。在午夜之后，将会启动单一进程运行，而且该程序通常可以持续6个小时或者更多。

一般来说，错误还是时常发生。CrimeWatch服务程序常常会变得非常“不理智”，勉强生成一个没有空间、时间或类型约束的地图，比如奥克兰过去三个月所有地区的所有犯罪。我们没有可靠的方式来检测这种情况，而且我们的数据库所报告的地理位置经常错乱。

在这个案例中，我们觉得偶尔的错误报告只是为了改进数据库浏览工具而付出的很小的代价。在2007年前半年，我们继续收集数据，通过可视化表示或者出版发表技术，周期性地发布一些小的实验。

公开数据

2007年8月2日，《Oakland Post》记者兼主编Chauncey Bailey在光天化日下被刺杀了，距离我所在的商业区的公寓仅隔几个区。虽然这个尚未公开的案例看起来是被当前Bailey正在调查的一群罪犯分子的政治谋杀的一个案例，但是它重新聚焦了全国对奥克兰暴力犯罪的关注。几乎同时，《Oakland Tribune》的Sean Connelly和Katy Newton发表了获奖作品《Not Just A Number》，它是一张奥克兰杀人犯的交互图(htp：//www.bayareanewsgroup.com/multimedia/iba/njn/）。Connelly和Newton对于该城市的谋杀统计幕后的故事尤其感兴趣。在《Not Just A Number》地图中，绝大部分的受害者先是通过面部照片来识别，它还努力联系受害者活着的家人、朋友和邻居在新闻报道中露面留名。我们对于提供一个服务来提供铁的事实和当前数据来补充这些故事非常感兴趣。

我们的预算包括三个人、两周的敏捷开发工作量：我们把收集的数据转化为可用于Web的服务，Stamen交互设计师Tom Carden使用Flash开发了可融入式的可视化接口，而富于创意的负责人Eric Rodenbeck预见了这种可视化方向及其伴随而至的语言所产生的效果。

发布信息优先级最高的是要显示所有方面。Crimespotting网站的主页是一张地图（见图11-6），而且该地图显示了从上周开始的所有犯罪报告。地图包含西奥克兰和商业区的绝大多数地方，包含图标化的Lake Merritt，可用于视觉识别。熟悉的“滑动地图”(Sippy Map)的移动和缩放控制使得可以迅速地获取城镇的其他地方的信息，西北部是Berkeley/Emeryville，东北部是富饶的山脉和Piedmont，东南部是San Leandro及其以外地区。这种表示方式与奥克兰城市的官方网站City of Oakland发布的当前已有的通过向导的方式形成了鲜明的对比。现有的应用需要一些关于奥克兰的先验知识，而且它假定参观者寻找的是特定地方的犯罪信息，Crimespotting项目的“滑动地图”不需要先验知识或者特定的搜索议程表，相反地，它支持的是更有探索性的、更曲折的搜索行为。Peter Morville描述的是一种新兴的概念——“可寻性”(fndability)，它描述了在信息空间中进行定位的方法以及借助接口和描述使得数据实现自我描述的方法。动态的基于Web的地图在过去四年中经历了很多改变升级。在2005年，一家报纸网站采用GMap做试验，发现被测人员不知道他们可以拖动地图；现在像《New York Times》这样的组织会定期地把信息设计和表现方式在线公示。有了犯罪数据库，我们觉得通过创造数据优先的用户接口，使数据更具有“可寻性”是很重要的。数据首先意味着通过大范围的可视化概要表示是可能的，并且通过类型、时间或地理特征缩小了搜索结果范围。我们实现了“香型小工具”(Sented Widgets)，由加州大学伯克利分校的研究人员Wesley Willett、Jeffrey Heer和Maneesh Agrawala在2007年嵌入式可视化方面的论文(htp：//vis.berkeley.edu/papers/scented_widgets/2007-ScentedWidgets-InfoVis.pdf)中提出：

图　11-6：奥克兰Crimespotting项目的主页显示了从上周开始的犯罪报告地图（见彩图32）

虽然有效的信息“气息”线索可能是基于底层的信息内容（如在Web超链接的文本描述了链接文档的内容，这就是“气息”），其他线索可能涉及各种形式的元数据，包括使用模式。在物理世界，我们通常以浏览的方式对待其他人的活动。当发现一群人聚集在一起的时候，我们可能走过去以便观察确定引起大家兴趣的根源到底是什么。或者，我们可能故意避开人群或者陈腐的途径，选择“人迹罕至”的方式来探索鲜为人知的兴趣点。在信息空间环境中，这种社会性的浏览会把我们的注意力吸引到兴趣的热点或者尚未探索的区域。

Crimespotting地图左下角的主接口的时间选择器界面显示了报告的犯罪随着时间变化的条形图（见图11-7），虽然在右下层类型选择器包含了谨慎的工具提示信息，显示当前选择时间范围的每种报告类型的总数（见图11-8）。这两种选择器都包含双重功能：过滤和反馈。特别是，时间选择器是受到博客统计包Measure Map(http：//measuremap.com/）的一个类似功能启发，Measure Map是Adaptive Path公司的Jeffrey Veen设计的，后来变成G公司的分析产品。而反过来，Measure Map的时间滑动轴是受Flickr的界面特征所启发的，因此，坦白说，这实际上是模仿。我们自己加强的是不同条形栏的颜色区分，并显示已经包含的数据的日期（深色）和不包含的数据的日期（浅色）。

图　11-7：Crimespotting的主地图上的时间选择器界面（见彩图33）

图　11-8：类型选择器显示了在选定时间范围内的每种报告类型的总数（见彩图34）

在图11-8中，有一个水平滑动轴显示所有东西，而它就是信息过载。我们已经介绍了一种可视化报告类型过滤的方式，它是受到苹果公司的Max OS X系统偏好的聚光灯特征所启发：当在地图中存在特定的犯罪报告，或者通过鼠标在类型选择器上多滞留几秒钟，界面颜色就变深，使得地图中高亮显示的区域围绕匹配该类型的报告。抢劫可能会由于不同的犯罪类型被覆盖，导致无法在地图上显示，但是可以通过聚焦显示方式展现和访问它。

设计过程中的意外收获使我们更清楚地理解了数据专业化的哪些方面可以成为应用界面产品。考虑到Flash在视觉特技方面相当成熟，我们选择它作为实现环境，而且及时实现了我们自己必须实现的滑动地图交互代码，而不是依赖于很多已有可选的JavaScript库，如OpenLayers(http：//www.openlayers.org)。

平移、缩放地图交互看起来是可以应用于其他项目的一个有用的功能点，因此在犯罪数据显示的早期工作成果是生成了独立的通过BSD授权的软件库Modest Maps(http：//www.modestmaps.com/）。Modest Maps在数据显示和交互隐喻之间的功能有一个清晰的突破，而且它把地图相关的代码库独立出来，这有助于很多不相关的项目的敏捷开发，这些项目有一些源于Stamen，但是很多是来自于外界的设计和开发。

第二优先级是为我们收集的数据生成一个公开、可共享的地址空间。通常来说，在Crimespotting项目中只有一些URL“气息”：

·地图视图http：//oakland.crimespotting.org，更大的视图在http：//oakland.crimespotting.org/map。

·报告列表视图，如http：//oakland.crimespotting.org/crimes,http：//oakland.crimespotting.org/crimes/Robbery,http：//oakland.crimespotting.org/crimes/2009-01-09和http：//oakland.crimespotting.org/crimes/2009-01-09/Robbery。

·个人报告视图，如http：//oakland.crimespotting.org/crime/2009-01-09/Robbery/113569。

·巡警视图，如http：//oakland.crimespotting.org/beat/04X。

绝大多数URL是在把它和内容关联起来之前设计的。特别地，这些URL需要和设计师Matt Biddulph在2005年发表的演讲“Designing Data For Reuse”(htp：//www.hackdiary.com/slides/xtech2005/）的思想一致：可读性、建设性、易于破解、不透明、永恒和经典。我们包含地址的一个层次结构，它在大声朗读时富有韵律：“1月9日抢劫”，“04X巡警”等。其中存在潜在的二义性——例如时间在前“/犯罪/2009-01-09/抢劫”和类型在前“/犯罪/抢劫/2009-01-09”或者单数形式的“/犯罪(cime)/抢劫”和复数形式的“/多重犯罪(cimes)/抢劫”——我们把这些HTTP请求重定向到它们自己相应的表单。该重定向通过确保一个人给出的某天的盗窃清单和另一个人给出的能够完全匹配，这样URL就变得更具有共享性。通过个人给出的URL，其不足之处在于它的地址结尾存在一个数值关键字。PostgreSQL开发人员Josh Berkus非常憎恶这种关键字，在他的“Primary Keyvil”的系列文章中对此做了详细的描述(htp：//it.toolbox.com/blogs/database-soup/primary-keyvil-part-i-7327）：

没有多久（大约两个月），我们就发现使用“id”作为唯一的不重复字段存在严重问题。我们在日历安排上有很多的听证会，它们具有相同的摘要、在同一天或者在同一个地方发生。这两个听证会是相同的还是不同的？我们无法确定。问题的本质是自动id列不包含其关联记录的信息，而且不包含该记录的任何信息。记录本身可能是重复的，也可能是唯一的，而如果有人无知删除了外键约束，重复的记录就丢失了。

我们包含这些关键字的理由是奥克兰警察局保存记录的方式相当松散。虽然每个报告都有一个案例编号，但这些编号通常是在不同报告之间共享，而且看起来把一组个人的指控连接为单个更广泛的事故。一个极端的例子是编号为08-056061的案例(htp：//oakland.crimespotting.org/crime/2008-08-01/murder/93014），它关联了9个在2008年8月某个晚上发生的谋杀、盗窃和严重伤害报告。我们已经使用案例编号和文本描述（如“ASSAULT W/SEMIAUTOMATIC FIREARM ON PEACE OFFICER/FIREFIGHTER”）作为唯一标识符来解决这个问题，但这种方式导致URL太长，可读性很低，因此使用数值ID来代替。

对URL的这种关注的结果是把在线的犯罪信息转化为社会对象。在CrimeWatch上，引用报告需要对要采取的动作使用过程化的描述：采用向导小工具(Wzard)，可以选择这个，按住那个，点击这里等；找到关于某个特定报告的特定信息需要大约10多次的独立点击。URL公开之后，其本身也是犯罪信息的完整描述。Leonard Richardson认为地址或者统一资源标识符URI是导致WWW在20世纪取代了其他流行的网络协议的主流技术。在Richardson 2008年的精彩演讲“Justice Will Take Us Millions Of Intricate Moves”(htp：//www.crummy.com/writing/speaking/2008-QCon/）中，他认为Web主要有三种技术构成：定位资源的URI协议、传输资源的HTTP协议和使客户端软件能够理解并知道如何处理资源的HTML。这三者都是至关重要的组成部分。特别地，URI设计受到人们的广泛关注，但实际上是包含链接和表单的又老又慢的HTML使得相互连接的Web真正变成可能。HTML是对Roy T.Fielding在2000年博士论文中提出的“表示状态传输(Rpresentational State Transfer,REST)作为架构方式”(htp：//www.ics.uci.edu/～fielding/pubs/dissertation/top.htm)想法的至关重要的解释。只要可能，我们就遵循这些想法，保持Crimespotting的交互的Falsh展示部分严格基于基础的、1993年的Web页面的格式。我们的API为Flash、RSS和Atom的Feed读者提供XML格式的数据，为电子表格提供CSV格式的数据；所有核心的使用信息方式组成了一套完整的API。

2007年8月发布的产品中囊括了这里描述的所有概念，同时依赖于CrimeWatch在夜间执行的代价很高的刮页操作。我们确定市政府肯定有人最终会注意到这一点并对它表示不满，但是八个月来系统一直运行很顺利，这让我们产生了一种安全的错觉。

重新回顾

发布后不久，我们的网页刮屏程序的运行负荷开始超出能够容忍的底线。即使使用通用的浏览器，似乎不论等待多长时间，都无法访问CrimeWatch网站。城市信息技术部相关人员的谈话，暗示我们一旦公众注意到这些访问问题，该网站就会不受欢迎。他们提供了通过官方访问这些数据的一些线索，但是他们那种作风的方式处理很慢，没有任何即时收效。我们很遗憾地关闭了该网站，花了几个月考虑优化程序以及改进策略，使它能够重新运转起来。在那时，我们尝试了两种思路来优化该系统，同时还开发实现了一个新功能。对系统的两种优化最终都未成功，而新开发的功能则对外发布了。这次修改过程的一个产出是生成更集中、实用的最终数据展现方式。

在思考如何最佳表示本地犯罪的影响时，我和Adam Greenfield的谈话激发了这种想法：“暴力是发生在某个地方的一种力。”在周边社区的谋杀或者抢劫所带来的长期影响可以被看做一个环（见图11-9）。这种想法的在我的大脑中产生的初始模型是一个空间-时间半球，球的空间半径约0.25里，时间半径是一周。这种可视化显示方式将是：从一个点开始，随着时间轴逐渐靠近精确的事件发生时间而变大为一个有色圆。Greenfild建议将半球转换为一对圆锥：犯罪的时间点是两个圆锥的共同的顶点，在时间轴上以这个点为起点向前和向后各有一个“光锥”(lght cone)。其视觉显示效果将是一个大的散开的圆圈随着逐渐接近事件发生的精确时间而逐渐清晰化，并最终汇聚为一个微小的点。这种展现方式的理念可能更适合于展示对象的随机属性。犯罪的潜在可能性可能很广，甚至可能涵盖整个社区。随着事件的曝光，这种潜在的危害逐渐聚焦为一个点-即某个邻居成为受害者，然后随着消息传播开来，个人安全感也消失了。

图　11-9：设想的一起谋杀或抢劫对周边社区的长期影响的两种方式

我们生成了很多交互式地图来探索这种以圆锥作为隐喻的方式，并发现了一些有趣的现象。其中一个现象是某些类型的报告存在依赖于其实施方式的唯一视觉签名。卖淫就是其中的一个特例。在之前的案例中，我们展现的绝大多数报告都是事件驱动的-受害者给我们打电话，而卖淫则是由警察局的决策和安排的严打所驱动的。通常，在前端展示上，关于卖淫方面，好几周都是风平浪静，而后会突发产生关于San Pablo Avenue或International Boulevard的迅猛的、翻天覆地的信息。遗憾的是，这种圆锥式的展示方式太怪异了，以致在主要的网站上无法使用。而在地图上，通过时间遍历的方法非常新颖，而且对我们来说，使报告展示和时间的关系控制尽量明朗是很重要的。圆锥式展示方式将还需要进一步试验。

在我们关闭网站时期，我们考虑到另一种可能的改进是采用分布式的网页刮屏方式(pge scraping)。正常的收集过程容易受到中断的原因是所有请求都来源于相同的网络地址，这使得这些请求在需要时很容易被阻塞。我们试验了一种分布式的模型，它是火狐浏览器的一个现成的插件，主要通过JavaScript控制。我们希望有足够多理解技术的访问者会愿意下载浏览器搜索框，帮助收集数据。这种方式，和通常的网站使用方式截然不同，发到CrimeWatch服务器的请求将会在每天不定时地被分散到很多访问IP地址中。这种处理过程的优点是在最后增加了人工错误纠正。中间件刮页过程的最后页面中包含了用户帮助收集的所有报告的总体概述，还可能标注某些匹配为不正确的。

在这期间开发的一个新功能是巡警区专用(bat-specific)页面，比如在商业地区和Lake Merritt地区之间的商业和居住区域：http：//oakland.crimespotting.org/beat/04X（见图11-10）。当我们最初开发该服务时，我们有意识地决定忽略在CrimeWatch中出现的管理部门。警务服务区域、城市委员会区、邮递区、巡警区对我们而言都只是对定位的一个干扰。在发布后，我们很快明白了我们对巡警区的理解是错误的。用户告诉我们公民和警察局间的沟通主要的渠道是巡警区的警官，这些警官各自有特定的巡查区域且会定期与当地社区的居民开会沟通。报告按照巡警区进行划分是非常重要的，因为它匹配了任何给定的警官关注的区域和责任。另外，巡警区的边界通常是该城市的明显的物理特征，主要街道、小溪、高速公路和铁路都是作为社区的自我标识的关键特征。巡警区页面显示的是该区域的静态全局概要地图，以及对于习惯于电子表格软件的非技术人员来说，它是比较有用的一部分API。在这个功能中，我们收到的最终的反馈是非常宝贵的。居民说：“我们下周有Beat1X NCPC（社区犯罪预防委员会）会议……我可以比那些老警察(OD)准备得更充分……到目前为止，他们几乎没有将统计数据分享给我们。”

火狐浏览器插件及其关联的Web服务控制器完成后，正在计划做一些有限的、实验性的首次展示的时候，奥克兰城市官方报纸Ci ty of Oakland告诉我们，他们将每天晚上给我们提供一个包含合适的街道地址和岔路口的完整的全城范围的犯罪报告信息列表。从2008年1月开始，一直持续到今天，由于市政创建者以及与我们一起工作的数据管理员帮助，我们的数据收集过程已经从冗余、易错的事件驱动方式转化为敏捷的处理方式。最终，在奥克兰CTO Bob Glaze、项目经理Ahsan Baig和City的Julian Ware和Andrew Wang的帮助下，我们获得了在夜间生成的官方犯罪报告数据的Microsoft Excel电子表格。改进后的系统和之前的犹如天壤之别：原来需要几个小时的数据收集和处理，现在只需几分钟。定位信息也变得更加真实可靠，地图信息详细到了街区级别，突出街道名称和叉路口特征，取代了原有的彩色图标。

结束语

我们是否成功地维护了和最初提出的“美的理念”(te ideals of beauty)一致的数据服务？Crimespotting项目的犯罪报告数据生动有趣，而且定期激励关注的居民发邮件，并支持邮件告警订阅服务的人数达到几百人。犯罪对任何城市居民，都是一项严重的问题，而它对于像奥克兰这样因犯罪事件而昭著的城市来说，犯罪对于人们的生活影响尤其重要。我们发布的数据有用吗？

图　11-10：巡警区专用页面允许居民为在当地巡逻的警官提供反馈（见彩图35）

我们定期收到来自使用新闻订阅的居民的来信，并且向那些居住在事件发生地的居民发送邮件告警或者为其推荐新的居住社区。我们是否足够自由或者公开？

所有的网站信息都是可以通过适合于很多种大范围的技术专业性的形式获得，从简单的每日邮件订阅或者电子表单到更复杂的新闻Feed订阅方式或者基于XML的API。该项目是有收获的、成功的，它创造了我们认为对本地居民最有利的数据服务。

通过创建并继续维持奥克兰的Crimespotting项目，我们在社会和政治方面学到了重要的一课。为了满足居民相互关联、相互通信的期望，城市和政府信息正逐渐迁移到网络上。随之而来的是关于“数据是公共产品”的理念，正如FortiusOne公司总裁Sean Go rman 2009年早期在一个重要的博客上发布的帖子所解释的(htp：//blog.fortiusone.com/2009/01/28/data-is-the-public-good-data-is-the-infrastructure-data-is-the-stimulus/）。为“专业消费者”(posumer)提供的数据可视化和分析工具的增长使得数据变得更加重要，因而可预测的、值得信任的网上原始数据，则更倾向于过于受束缚的基于Web的用户界面。同时，人们也逐渐精心设计关于发布原始数据的新兴的实践和约定，而且在很多情况下，为了把注意力转移到问题的形式或者可达性上，那些感兴趣的、有能力的非项目成员的介入是必需的。事实胜于雄辩，而主动为公众的利益而公开和改进数据所做出的改变是最有效的。

第12章　Sense.us的设计

Jeffrey Heer

必须承认的是，我不信任美丽的数据，至少在没有上下文的情况下，我是不信任它的。第二次世界大战之前，荷兰政府收集了详细的民事记录，登记备案了荷兰居民的人口统计信息。这种做法的出发点很好，收集人们的注册信息来为政府服务管理提供信息。然而，德国入侵后，这些数据被用于有效地锁定少数族裔(Coes 2006）。1940年前荷兰还有大约14万的犹太人，而其中只有大约3.5万人幸存了下来。

虽然这种例子也许有点极端，但对我来说，这种发人深省的故事说明了一个基本的道理：数据之“美”取决于这些数据如何为人所用。数据蕴涵着帮助人们改进对事物的理解，并做出更优决策的潜能，因此数据是“美丽”的。要让数据实现其价值，需要做到收集并保护正确的数据，让合适的用户访问并理解使用它们。AOL公布的匿名性不足的查询搜索数据所造成的骚动是近期关于数据保护失败的一个实例。

幸运的是，绝大多数例子并不像这两个故事这么悲惨。实际上，更普遍的现象是数据都被浪费了：把数据收集并存储在数据仓库中——有时付出的基础设施代价很高——但还是尚未被充分利用。对于公司和政府，不断衰竭的数据意味着机会的错失以及很低的投资回报率。数据的价值和人们从中抽取出有价值信息并用它来指导实际行动的能力成正比。

然而有些自相矛盾的是，一些数据集拥有的“美丽”（或者潜在美）要高于其他数据。显然，选择收集什么数据以及如何设计数据存储的基础设施、模式、访问机制决定了在避免数据给人们带来伤害的同时，激发数据给人们带来信息和启发的潜能。但是，攀越美丽巅峰的“最后一里”(lst mile)是人类-信息相互交互的问题，即为了支持对数据的分析和通信，人们应该如何展示和探索数据。

本章将介绍一个交互式可视化应用的案例研究，以帮助培养人们实际应用美丽的数据：sense.us的设计，它是一个对美国的150年人口普查数据进行协同式探索和理解(snse-making)的Web应用。我将谈到建立庞大的、政府部门收集的数据集的步骤——美国人口普查——而且通过一组交互可视化方式使所有用户都可以访问这些数据集。我还将描述我们设计的分享和讨论机制，该机制的设计目的是吸引数据的浏览者们在对社会的理解和思考方面组成一个群体。我们的目标是通过培养集体数据分析来实现数据的潜在之美。

可视化和社会数据分析

可视化通常用于构建数据的涵义来帮助理解这些数据，探索这些数据并交流结果发现。为了处理可视化信息，人类神经系统的很大一部分已经得到进化，在人类大脑中，超过70%的神经末梢和40%的皮质在视觉处理中相互关联(Wre 2004）。可视化设计充分利用了这种视觉处理系统的能力，使人们可以更容易理解并发现蕴藏于数据中的趋势、模式和异常信息。

需要注意的是，这不是手工制作一个“花哨的图形”的问题。通常，一个简单的表格或条形图(sns 3D虚饰frills和镜面高光）能提供很好的演示效果。这其中的特技在于为现有的数据和任务选择合适的视觉展现形式。

一个富有启发性的例子是Anscombe的“四重检测”(Qartet)，它采用由统计学家Francis Anscombe收集的四类数据集来说明数据可视化的重要性(Ascombe 1973）。使用通用的统计学描述方式，这四类数据集看起来似乎完全一样（见图12-1）。但是，对这些数据的绘图展示就可以马上揭示出这些数据集之间的差异。

还有其他作家详细地描述了有效的视觉设计是如何辅助人们理解、沟通和制定决策。Tufte（1997）提出了著名的论断，如果当时工程师创造了更好的对火箭缺陷数据的视觉描述，挑战者号航天飞机的灾难可能可以避免（虽然该论断也饱受争议，见Robison等2002）。

可视化研究人员列出了“视觉变量”(vsual variables)的空间（如位置、长度、面积、形状和色彩）这些变量可以用于在可视化显示中对数据编码(Brtin 1967，Cart等1999）。他们还研究了当这些视觉变量应用于不同类型的数据，如范围的（名字）、顺序的（基于排序有序的）和定量的（数值的）数据时，人们对其解码可以达到的精确程度。举个例子，如在条形栏或散点图中所使用的空间位置，有助于对这些数据的每一种类型进行解码；当采用色彩对不同的标签进行分类时，它的区分度很高，而用于描述数值时，它的区分度则很低。

图　12-1：Anscombe的“四重检测”(Qartet)，包含统计学上相似的数据集来说明如何利用可视化帮助人们理解

本着这种思想，绝大多数可视化集中于研究可视化应用的感性和认知方面，通常是在单用户交互系统的上下文中。然而，在实践中，可视化分析通常是一个社会过程。人们可能对于如何理解数据有不同的见解，可能会提供上下文知识来帮助加深理解。由于参与者达成共识，他们可以从同龄中互相学习。此外，有些数据集很大，由一个人完全深入探索是不可能的。这意味着为了全面支持有意义的决策(snse-making)，可视化应该也支持社会交互。

基于这些观点，我和同事Martin Watternberg以及IBM研究院的Fernanda Viégas开始调查用于可视化数据的用户界面如何可以更好地促进“可视化的社交生活”。我们开始启动研究项目，设计和实现网站sense.us，致力于形成人口统计数据的团体探索。该网站提供了一套美国过去150年的人口统计数据的可视化的解决方案，以及促进面向团体的数据分析的协作机制。

在本章的剩余部分，我将分享sense.us网站的设计过程，包括：我们如何选择和处理数据，开发一套可视化方案，并设计协作特征来促进社交数据分析。最后，观察人们如何通过该系统协作构建数据。

数据

Martin、Fernanda和我以研究人员的心态开始做这个项目：我们希望能够理解在可视化分析过程中，应该如何最好地支持社会交互。我们对数据集的选择不是预先确定的。但是，很显然，好的数据领域的数据集将会满足某些特定的属性：我们想要大规模的、真实世界的数据集，它们和普通的大众相关，而且数据丰富，可以保证很多不同的分析。根据这些标准，人口普查统计的数据看起来很理想。我还一直对促使人口统计数据更公开、可访问感兴趣：我相信这是重要的一个方面，通过它我们可以更好地理解自己和历史。

我通过美国人口普查局的网站(htp：//www.census.gov)开始搜索数据。事实证明，这种方式收效甚微。人口普查局在各种聚集层次提供了很多不同维度的数据集（如通过邮政编码、都市区域），但是只在最近几年人口普查才有丰富的数据。我还意识到自己在该网站搜索数据时不够集中深入。人口普查数据在过去几十年如何收集和建模的来龙去脉，还亟待进一步了解。例如，人口普查局在收集数据过程中使用的问题以及涉及的范围在过去几十年在不断变化，意味着即使该网站包含某个人每年的数据，它也并不能保证这些数据能够很容易具有比较性。

一般情况下，人们在对某个数据领域有了最基本的熟练程度之前，不应该一下子扎进可视化设计。因此，我的下一个步骤是访问各个领域的专家们：加州大学伯克利分校的社会学和人口学部门的同事，那时我是该部门的研究生。通过和他们的讨论，我对人口普查是如何工作以及人口学家使用哪些数据源研究人口有了更深的理解。在这个过程中，有人为我介绍了一个宝贵的资源：由明尼苏达大学人口中心(htp：//www.ipums.org)维护的综合公用微观数据序列(Itegrated Public Use Microdata Series,IPUMS)数据库。IPUMS-USA数据库包含了1850～2000年的美国人口普查数据。其普查周期是10年，只有1890年例外，没有普查数据；在1921年这些数据记录在商业大厦(Cmmerce Building)被大火摧毁。IPUMS数据包含了每10年的代表性样本数据，占这10年的人口普查记录的总样本数据的1%或5%。每条记录代表人口样本的某个特征人物。在某些情况下，有某些特征的人物和家庭被多次代表，因而记录需要和不同的权值关联。

IPUMS-USA数据库特别吸引人之处在于IPUMS项目为所有的人口统计学变量开发了统一的代码和文档，这有助于随着时间分析变化。这种“和谐化”是一种宝贵的服务，使得能够进行比较性分析，而且通过扩展，可以获取一些新的洞察。然而，把分离的数据应用于共享模式的过程不可避免地引入了人工干预，该问题在后面还会出现。

诚如之前所述，IPUMS-USA数据库包含413个人口统计学变量，其涉及的范围从普通的领域如性别、年龄、种族、婚姻状态和职业，直到多少家庭包含洗衣机、烘干机、抽水马桶和电视（见图12-2）。在很多情况下，一些变量只在某几年有数据记录，而在其他年份，这些变量完全没有被测量。

图　12-2：在IPUMS-USA数据库中可访问的人口统计学变量的摘要

IPUMS项目的宗旨是“只用它从善，不作恶”(ue it for good,never for evil)。幸运的是，在实施该箴言的过程中，超出了人们在下载数据摘要时必须点击的复选框。为了保护个人隐私，有些数据的可访问性受到了限制。举个例子，不包含宗教从属关系，而且详细的人口统计数据的可访问性也受到很大限制，尤其是对于人口密度低的地区。

我们决定采用IPUMS-USA作为sense.us应用的主要数据来源。使用IPUMS Web界面，我们首先选择了1850～2000年的样本数据（不包括1890年），然后选择了需要抽取的一组变量。绝大多数变量只是所有人口统计年份的一个子集。为了对长期的变化进行可视化探索，我们选择了一组变量，它们至少在一个世纪内都是可访问的。这组变量由22个变量组成，包括年龄、性别、婚姻状态、出生地（美国某个州/地区或者其他国家）、职业、种族、学校出勤率以及地理区域。由于隐私约束，地理数据仅仅局限于粗粒度的区域如新英格兰和美国西海岸。结果数据的抽取是把520MB的Gzip文件解压缩为3.3GB的文本文件。

接下来，我不会赘述后续发生的详细细节。数据处理、清洗以及后续的导入操作是直接但繁琐的过程，最终生成了包含抽取出来的可查询形式的人口统计数据，保存在MySQL数据库中。为了帮助分析，我们使用星型模式来组织数据(htp：//en.wikipedia.org/wiki/Star_schema)：把人口统计的评测方法存储到一张大的事实维表(fct table)，该维表的每一列表示一个人口统计学变量，通过键值表示变量的范围值。维度表集合存储了每个人口统计变量采用的键值的文本标签和描述。

这种组合方式提供了指导探索性分析的基础。我们通过发布查询生成数据摘要，这些查询通过选定的维度“向上钻取”(rlled up)数据。举个例子，我们可以通过对所有其他变量的样本人数进行求和，分离出年龄、性别和婚姻状态的关系。简而言之，我们拥有探索数据和原型系统可视化的基础。

可视化

给定人口统计数据的大小和范围，我们早就意识到把单一的可视化设计应用于所有的数据可能会导致某些灾难。只要有可能，我们就希望把数据转换成最简单的形式，用于支持广泛的分析。由于我们的设计需要面对广泛的受众，我们创建了一个可视化数据集合的方法，它表示选定数据的分片。在本质上，我们希望可视化有用，同时，它又尽可能地简单。

因此，我们的设计哲学需要理清哪一种数据集合可能是最大的兴趣点，而哪一种可视化设计和交互技术可以最佳支持对这些维度的积极探索。为了实现这一点，我们开始同时探索数据本身和可视化设计空间。

在设计一个界面来帮助别人探索数据之前，我希望首先能够确保数据本身足够有趣，使得别人愿意去探索它。我在探索中采用了很多种方法，包括SQL查询、Excel和可视化系统。数据探索最有用的工具是数据库可视化系统Tableau。使用Tableau工具，人们可以把数据库的各个字段通过拖拽方式映射到视觉编码中；然后，应用查询该数据库，对结果进行可视化（披露一下，我是Tableau软件的顾问）。因此，我们可以对很多不同方法构建可视化原型系统（见图12-3）。我们生成了一个庞大的数据可视化集合，和同事分享该集合，收集他们的反馈。使用真实数据快速评估可视化方法的能力为我们节省了很多实验时间，使得能够为最后的系统设计保留开发精力。

在探索数据过程中，我记录下了有趣的趋势、模式和发现的游离点。在某些情况下，有意思的故事是在很多维度的组合下发现的。例如，通过年龄和性别对婚姻状态的统计结果说明了女性初次结婚的平均年龄随着时间在增长，在过去一个世纪大约增长了5年。在其他情况下，根据单一数据类型描绘的曲线随着时间推移，揭示了很多有意思的故事，如农民工在劳动力市场中的盛衰以及全球不同的移民浪潮。一般来说，我发现对数据进行转换是很有用的，可任意把数据看做绝对人口数量，或者看做是十年内人口普查比例。

图　12-3：使用Tableau工具构建的显示几十年间婚姻状态分布的原型系统可视化（见彩图36）

设计考虑

有了原型系统，Martin、Fernanda和我为sense.us应用协作设计了交互式可视化。在设计中，我们首先简单列出了设计中的一些考虑。

培养个人相关性

如果没有人关心数据，就没有人会去探索它。我们假设熟悉的维度如地理和时间，使得用户能够快速地在数据和表单叙述中他们自己（或者和他们相似的人们）。假设我们可以访问的地理数据是有限的，我们集中于随着时间的变化来表示数据。我们还尝试使用交互技术，使用户可以快速找到他们感兴趣的数据记录，如特定的职业或国家起源。

提供有效的视觉编码

自然而然，我们希望使用视觉编码来帮助理解数据。在某些情况下，这种工作任务直截了当：如果想要检验两个数值之间的关联，我将很难找到比散点图更好的表示方式。在这种情况下，我们需要做出很多折中权衡。

对随着时间变化进行可视化的一个通用方法是使用线条图。但是，在一个线条图中显示200多个职业会导致封闭的线条之间非常混乱。取而代之，我们选择堆栈图(sacked graph)，它们可以把一个图叠加到另一个图上，从而可以对多条时间序列视觉上求和（见图12-4和图12-5）。我们的这种选择是受到Martin的婴儿名向导图(Bby Name Voyager)可视化的影响，婴儿名向导图是关于婴儿名字受欢迎程度的堆栈图，在网上变得超乎寻常地受欢迎(Wttenberg和Kriss 2006）。堆栈图可以清晰地显示聚集模式，并且可以很优雅地支持交互过滤，但是其代价模糊了个人倾向——对于某个倾向的理解是由栈序列的曲线带来的偏见生成的。相反地，我们确定点击一条序列可以过滤出对它的显示，因此，通过这种方式，其趋势可以很容易被隔离出来进行显示。

进一步地，我们遵循了构建的文化约定，通过很多观察者所熟悉的方式对数据进行视觉编码（比如，用蓝色表示男孩，粉红色表示女孩）。当考虑如何在很多人口统计学变量间对交互进行可视化，而不是试图发明一些完全新颖的东西，我们采取的是对人口统计学家已经普遍使用的图形类型——人口金字塔进行扩展。

使每种展示方式都与众不同

在某些情况下，我们使用了相同的可视化类型来显示不同的数据类型。例如，对于职业和出生地数据，我们都使用了堆栈图。然而，我们希望每一种展示方式在视觉上都与众不同，这样用户就可以一眼识别出它们。结果是，我们为每一种可视化都构建了唯一的彩板。

支持直觉探索

为了培养交互探索，我们希望使得接口操作尽可能地简单。对于堆栈图，我们让用户输入搜索关键字对感兴趣的项进行查询。在其他情况下，我们提供了一组下拉菜单对维度进行选择或者过滤。我们还包含了控件来选择绝对人数计数和范化的比例。更高级的控制可能实现，而且我们发现提供该级别的控制能够促使通过整洁易用的接口所做的一系列探索。

积极参与玩耍

除了培养个人相关性，我们希望和该系统的交互有趣好玩。因此，我们致力于采用优雅高效的数据表示方式。我们设计了交互技术和动画转换技术来提高响应和动态性效果，但是我们不想过度滥用这种文体特征。

我们希望增强而不是骚扰数据探索。我们不断改变动画形式和计时方式，直到设计上“感觉良好”。持续1秒钟的动画提供了不需要放慢分析的过程，观看者就可以跟上的视觉转换过程。

可视化设计

首先通过参与自己的数据探索，我们可以确定自己最感兴趣的数据维度。为了设计一组人口统计数据的可视化解决方案，我们把这些观察应用到设计考虑中。

工作向导

“工作向导”(Jb Voyager)是一个堆栈图，显示了美国在过去150年间（见图12-4）的劳动力的组成部分。每条序列曲线表示一种职业，通过性别进一步细分：蓝色代表男性，粉色代表女性。用户可以通过点击一条序列曲线，只显示其相应的职位，或者通过输入关键字查询来过滤掉与查询不匹配的职位。我们还包含了下拉菜单，通过性别进行过滤；在绝对人数计数和劳动力比例之间进行视图转换。这些操作支持对聚集趋势（如第二次世界大战后涌入劳动力市场的女性）和个人模式（如机动车工程师的盛衰）的探索。

图　12-4：工作向导可视化：左图显示在过去150年间劳动力组成概要视图，右图显示过滤后农民工的比例的视图（见彩图37）

我们很快意识到仅仅通过性别对序列曲线进行着色是不够的。当我们过滤了视图，仅仅显示男性或者女性，要区分不同的序列曲线变得很困难。一个解决方案是通过任意方式改变色彩饱和度，使得感官上能够区分不同的序列曲线。Martin提出了一种很有技巧的变换方式：不是随意改变颜色，而是通过有意义的、数据驱动的方式来实现。因此，我们根据每个职业的社会经济指数分值来改变其对应的颜色饱和度。因此，平均收入越高的序列曲线，其颜色越深。在实践中，这种编码方式工作良好，不需要为显示增加误导或者无意义的视觉特征，而提高了不同职业的识别度。

出生地向导

“出生地向导”(Brthplace Voyager)和工作向导的设计相似，但是它显示的是每个人口统计年份的美国居民的出生地（见图12-5）。其记录的出生地或者是美国某个州和地区或者是其他国家。交互式控件可以通过查询关键字或不同大陆位置进行过滤，只显示绝对计数和比率。

数据可视化支持对全世界（如过去来自欧洲的移民浪潮以及当前来自拉丁美洲的移民浪潮）和美国国内（如每个州的居民的变化比率）的移民趋势的探索。

图　12-5：出生地向导可视化：左图显示在过去150年间出生地的分布概要，右图显示过滤后欧洲移民的总数的视图（见彩图38）

在出生地向导中，我们遇到了和之前相似的着色问题。在这种情况下，我们根据大陆位置来分配色彩，为美国的各个州增添一些额外的色彩。然后，我们实验了改变色彩饱和度的不同方法，直到我们可以使用在该州出生的人口总数或者所有时间片断的国家作为后备数据。

美国人口统计的国家地图和散点图

虽然工作向导和出生地向导的时间线在设计上都是使观察者以历史叙述的方式进行参与，但是我们希望能够包含更加传统的视图。我们提供了一个着色的美国国家地图，显示每个州的人口统计变量的分布。在2000～2005年（如图12-6的左图所示），在国家人口变化的标注地图中，人们可以看到西南部地区人口得到显著增长，而North Dakota的人口却在不断下降。我们还提供了散点图的表示方式来检测不同变量之间的潜在关联。用户可以把人口统计变量映射到坐标轴的x坐标和y坐标，圆圈的面积表示整个美国。例如，人们可能会注意到全国家庭收入和零售额之间的关联（如图12-6的右图所示）。这些视图的备份数据包括我们从美国人口统计局网站上下载的额外的统计数据。

人口金字塔

最复杂的可视化是交互式的人口金字塔，它是为了同时帮助探索多个人口统计学变量而设计的（见图12-7）。人口金字塔（有时称之为“年龄-性别”金字塔），是由同事介绍的关于人口统计学的一种图形。金字塔被一条垂直曲线划分为两半：一条曲线表示男性，另一条表示女性。y轴表示年龄，通常以5年或10年作为一组进行分组；x轴表示某个年龄段的总人数，男性的人数值朝一个方向增长，而女性的人数值朝相反方向增长（如图12-7的左图所示）。该金字塔形状传递了人口动态信息：险峻的金字塔暗示了其死亡率比圆柱形的更高。

图　12-6：（左图）交互式国家地图显示每个州从2000～2005年的人口变化，（右图）美国散点图显示了平均家庭收入（x轴）和零售额（y轴），新罕布什尔州和特拉华州有最高的零售额（见彩图39）

我们创建了一个交互式金字塔，除了年龄和性别，它还结合以下人口统计变量：地理区域、种族、婚姻状态、学校出席率和收入水平。该金字塔的两边默认通过性别来划分数据。我们放宽了这种限制，增加了下拉菜单，通过该菜单，用户可以选择一个人口统计学变量，把该变量对应的（x,y）两个值映射到金字塔的两条边上。比如，用户通过把美国西部海岸放到金字塔一边，而把新英格兰放在另一边的方式来观察地理区域。

我们还引入了彩图编码菜单：选择一个人口统计变量把金字塔的两条边转换为描述变量值的流行率(pevalence)的堆栈图。例如，用户可以以学校出勤率进行统计，用于查看学校人口出勤率分段情况（如图12-7的右图所示）。我们为每个变量选择一个与众不同的彩板，只要可能就依赖于已有的文化约定（如蓝色=男性，粉色=女性），使用在线工具ColorBrewer(http：//colorbrewer.org)来确定其他情况的颜色选择。我们使用灰色带表示数据丢失或者未知值。

图　12-7：人口金字塔可视化：（左图）2000年的每个年龄组的男性和女性的总人口数量的比较，（右图）2000年学校出席人数的分布（标注重点突出了成人教育的流行率）（见彩图40）金字塔下方的时间线使得可以在整个人口统计期间进行时间探索，而回放特征使金字塔可以随时间动态变化。举个例子，随着时间对人口变化的动画显示显著突出了战后期间

社会出现的婴儿高潮。由于多层次色彩和泡沫式动画的结合，用户亲切地把我们的人口金字塔重命名为“Georgia O'Keefe的熔岩灯”(lva lamp)。

最后，我们支持四种尺度来衡量数据：人口总数、10年内人口比率、（金字塔）图面内(pnel)比率（当金字塔两边不协调时有用）和年龄组比率（为了探索不同年龄间的比率区别）。我们发现每一种衡量尺度都有助于揭示某些特定的故事。例如，年龄组比率显示说明了年纪大的男人比年纪大的女人更容易结婚，推测原因是女人平均活得更长，因而成为了寡妇。

实现细节

我们把每一种可视化作为Java Applet(Java应用小程序）实现，这样我们可以把它嵌入到Web页面。选择Java而不是Flash，一部分是由于性能原因，但更重要的是当时有现成的Java可视化框架。堆栈图和人口金字塔是通过开源工具包Prefuse(http：//prefuse.org)构建的。每一种可视化是通过从人口统计数据库中抽取出来的纯文本文件来支持的。对于人口金字塔，我们为每种可能的人口统计变量组合生成了一个文件，预计算数据的所有相关投影。这种方式避免了在服务器端处理数据的需求，生成方便管理的存储历史：虽然刚开始数据库包含3GB多的数据，在最后部署时数据减少到仅仅稍多于3MB！当然，这种方法确实存在其局限性：它阻碍了用户探索新的人口统计变量组合，并且使得要求服务器端处理数据的可视化变得复杂。

协作

因而，我们创建了sense.us网站，它通过协作分析机制对可视化进行耦合（见图12-8）。左图是可视化Applet（见图12-8中的a）和标注工具（见图12-8中的b）。右图提供了图形化的书签线索（见图12-8中的c），提供了用户保存的访问路径以及显示和当前视图相关评论的讨论区域（见图12-8中的d和e）。我们通过一组协作特征来增强可视化，这一点在后面会详细描述：视图分享、双向链接讨论、图形化标注、书签线索和以评论列表和用户行为个人档案生成的社会导航。

视图分享

当围绕可视化协作时，我们认为参与者看到的必须是相同的视觉环境，才能够为其他人的行为和评论打下基础。为此，sense.us提供了对视图添加书签的机制。我们试着通过把应用绑定到传统的Web书签上，使它透明化。浏览器地址栏总是显示链接到可视化的当前状态的一条URL，该URL由一组过滤、导航和视觉编码等参数进行定义。随着可视化视图的变化，URL也随之更新来显示当前状态（见图12-8中的f）；用户可以在任意时刻剪切、粘贴一条链接到当前视图，因而简化了通过Email、博客或者即时通信来共享视图的过程。为了遵循用户期望，把浏览器的前进和后退按钮绑定到可视化状态，这种方式可以很方便地导航到过去浏览的视图。

图　12-8：sense.us协作式可视化系统：a）交互式可视化应用小程序，图形化标注出当前选定的评论。该可视化是按照性别划分的美国劳动力人数的叠加式时间序列可视化。该图显示了军事职位的劳动力比率；b）一组图形化标注工具；c）保存的视图书签索引；d）添加评论的文本入口区域，可以拖动书签到文本区域，为评论中的视图增加链接；e）在当前视图添加相同主题的评论；f）该应用当前状态对应的URL。该URL随着可视化状态变化自动更新（见彩图41）

双向链接讨论

为了设置可视化相关话题，我们创建了称之为“双向链接讨论”(dublylinked discussion)的技术。该技术起始于一个独立的讨论页面，在这里用户可以为某种特定的可视化状态（或视图）添加评论。评论显示在Web页面的右侧，通过线性的讨论话题进行组合（见图12-8中的e）。每条评论显示了话题主题、评论文本、作者全名以及评论时间。点击一条评论可以把可视化切换到书签状态，表示该评论员已看过的视图。

用户可以通过开始一个新的话题或者回复已有的话题来增加评论。当点击“新的评论”或“回复”链接，该站点就会显示一个文本编辑器，可以通过它添加评论（见图12-8中的d），图形化标注工具（后面讨论）也变成可操作模式。在提交时，评论文本和任何标注都会被发送给服务器，并且评论列表会被更新。

以上描述的界面基于把评论生成可视化的链接。系统还提供了另一个方向的链接：从可视化到讨论。因为用户可以改变可视化中的参数和视图，他们可能会无意中改变了另一个用户已经添加评论的视图。发生这种情况时，相关的评论会自动出现在右侧面板。我们的直觉是这种“双向链接”讨论界面结合了独立的和嵌套的讨论的各个方面，有助于为可视化奠定基础，并促使可视化本身变成一种社交场所。

我们很快意识到书签机制不足以支持双向链接讨论。为了观察从一种可视状态链接到该视图的所有评论所面临的挑战，让我们以图12-8的可视化为例。当用户在最上层搜索框输入“military”（军事）（见图12-8中的f），他看到所有以“military”字符串开头的职位标题。而该用户如果只输入“mili”，他看到的是所有以“mili”开头的标题——但是其结果是和之前完全相同的职位信息。不同的参数设置生成不同的URL结果，但是提供完全相同的可视化视图。从更广义上说，参数设置可能不包含一对一的可视化状态映射。因此，为了对视图添加评论，我们需要一种索引机制，它可以识别相同的可视化状态，虽然其参数表现形式不同。

我们通过区分两种类型的参数来解决索引问题：过滤参数和视图参数。过滤参数确定哪些数据元素在显示中可见。我们不是直接索引过滤参数，而是通过记录哪些数据项当前可见来索引应用的过滤状态，因此可以捕获不同的过滤参数来最终生成相同的过滤状态的情况。而视图参数则是调整视觉映射方式，如选择范化的或绝对的坐标轴尺度。我们当前的系统直接索引了视图参数。书签机制同时基于过滤状态和视图参数，通过计算概率唯一的SHA-1散列值实现这两种参数的索引。通过将散列值作为键值，即可检索当前可视化状态的评论。

通过图形化标注来指向标识

在物理协作中，人们通常通过言谈举止，尤其是指向某个物体来表示这个物体，交谈该话题。在分布式、异步Web上下文中，图形化标注可以发挥相似的通信作用。我们假设图形化标注对于指向行为和有趣的评论都将非常重要。为了给评论增加图形化元素或者指向感兴趣的某个特征，用户可以使用绘图工具（见图12-8中的b）来标注评论的视图。这些工具允许在可视化视图中绘制自由格式的笔墨、线条、形状和文本。这些工具和那些表示工具如Microsoft PowerPoint相似，目的是充分利用用户对于系统的熟练程度。包含标注的评论是通过在评论列表中作者名字的左侧显示一个小图标来表示（见图12-8中的e）。当鼠标悬停在一条标注的评论上时，该评论将以黄色突出显示，并呈手形光标显示。因此，点击包含标注的评论区域会显示标注，页面亮度变暗，使得评论区域变得透明。再次点击评论（或者点击不同的评论）将会删除当前的标注和高亮。

图形化标注的形式是在可视化上绘制矢量图形。当提交一条新的评论时，浏览器从可视化应用小程序请求当前标注（如果有的话）。该标注是以XML格式保存，然后通过gzip压缩，以Base64字符串表示方式编码，然后传给浏览器。如果后期从服务器检索到评论，编码过的标注就以JavaScript变量的形式保存在浏览器中。当用户请求显示某条标注，该编码过的标注就会传递给应用小程序，然后应用小程序对其解码并绘图。

我们称这种方法为几何学标注，类似于在可视化表现方式上覆盖了一层“乙酸纤维层”，它和数据相关的标注不同，后者直接和底层数据关联。我们选择实现自由格式的标注机制，因此，我们可以在无约束方式下，首先研究指向行为。这种方式除了为我们提供自由表达方式，几何学标注还包含技术优势：它允许对整个可视化重用相同的标注系统，简化实现并维持一致的用户体验。

收集和链接视图

在数据分析中，通过观察数据，对不同方式进行比较是很常见的。而且，（看图）讲故事已经在可视化的社交应用中发挥了重要的作用。绘画比较和讲述故事都需要能够把多个视图书签嵌入到一个评论中。

为了支持这种多视图评论和叙述方式，我们创建了“书签索引”(bokmark trail)向导小工具。书签索引在功能上类似购物车：用户通过导航访问站点，她可以点击专门的“添加视图”链接，把当前视图添加到书签的图形列表（见图12-8中的c）。任意个数的可视化书签都可以增加到一条索引书签中。可以命名并保存一条书签线索，使得其他用户可以访问。

当做出包含多个视图的链接的评论时，书签线索向导小工具还可以作为短期的存储机制。从书签线索中拖动一个缩略图，把它放入一个文本区域，为书签视图创建一条超链接；然后，用户可以直接在文本编辑器中编辑或删除链接文本。当鼠标悬浮在链接文本上时，显示该链接视图的技巧提示小图标。

意识和社会导航

术语“社会导航”(Scial Navigation)指的是基于别人的行为或建议进行导航的倾向。在Web上，为了提供额外的导航选项，这种导航方式可以通过显示其他用户的使用历史来实现。我们设计了sense.us应用来支持通过评论列表和显示用户最近行为的个人档案页面的社会导航。评论列表提供了该系统用户做出的所有评论的一个可搜索集合，可以对该列表过滤，从而集中于单方面的可视化（见图12-9）。评论列表页面包括文本和每个评论的可视化状态的小图标头像。悬停在小图标上可以生成技巧小提示，图像也变大了。点击评论链接会把用户带到该评论的可视化状态，显示该评论中包含的任意标注。发表评论的作者名字链接到他的个人信息页面，该页面包括他的五个最新评论话题，以及五条最新保存的书签线索。这种视图还标识了从用户发表的上一条评论来开始，该话题的评论数量，这种方式允许用户监测他们所参与讨论的话题的相关动态。

虽然可以有更复杂细致的社会导航机制，但是我们希望仅仅通过这些基本的选项来观察系统的使用方式。我们对观察数据驱动的探索和社会导航之间的相互影响特别感兴趣。当用户探索数据时悄悄检索讨论，可以为探索过程引入潜在可能相关的谈话。同时，评论列表和最近话题动态可能有助于用户找到感兴趣的视图，使得社交活动成为数据探索的催化剂。

图　12-9：sense.us评论列表页面，评论列表显示可视化的所有评论，提供链到评论的可视化视图的链接（见彩图42）

不唐突的协作

我们还遵从计算机支持的协作领域的通用设计指南：协作式特征不应该妨碍个人使用。因此，我们默认上并没有给出视图标注。相反地，可视化的评论显示在屏幕右侧不显眼的地方，而图形化标注则是根据用户需求，“随需显示”。

“向导”和“偷窥”

在完成数据获取、设计和系统实现步骤后，我们的网站就可以运行，准备好让用户“现场试验”。本着一组用户研究的想法，我们部署了系统，观察人们对系统会做出什么反应，产生什么样的见解，以及我们应该如何改进改站点。

我们邀请了30名用户到我们的实验室，用来观察他们如何使用sense.us探索数据。每个人可以看到其他参与者对该站点的贡献。我们还在IBM企业内网现成部署了sense.us，这样公司内所有员工都可以访问。从这些研究中，我们调查了人们如何参与可视化，以及协作特征如何给人们的探索带来影响。下一步，我将总结观察到的一些最有意思的使用模式。

寻找模式

绝大多数用户的第一反应是参与“寻宝游戏”(savenger hunts)，获取有趣好玩的观察，通常是由个人所处环境所驱动。例如，用户会搜索他们自己或朋友或家庭成员的工作，或者查看他们祖先的出生地数据。在探索过程中，人们通常会做出评论，记录他们认为最有意思的趋势。

举个例子，参与者注意到在1930年酒吧服务员的人数减少为零，发表评论说其原因是由于禁酒令。有人发现在19世纪从加拿大移民的（美国）人口比率上升到最高值，然后稳步下降，就咨询是什么原因导致了这种趋势。而另一个用户注意到在大萧条后股票经纪人数骤减，留下了如图12-10所示的视觉评论。

用户还通过人口金字塔发现了有趣的趋势。例如，用户探索了婚姻状态随时间的变化（见图12-11）。绿色带和紫色带意味着分离和离婚的流行率，该比率在1960年后显著上升。一名用户调研了学校出席率，发表评论说从1960年后，成人教育显著增长（如图12-7的右图所示）。

图　12-10：股票经纪人的标注视图，其评论意思是“大萧条‘杀死’了很多经纪人”（见彩图43）

图　12-11：人口金字塔显示在1940年（左图）和2000年（右图）每个年龄组的婚姻状态分布（见彩图44）

另一种情况是，一个用户把金字塔的两边映射到大西洋中部地区（即纽约、宾夕法尼亚州和新泽西州）和西海岸地区的人口（见图12-12）。在1850年，“淘金热”年代，西海岸地区的人口和东海岸地区有很明显的区别，前者主要是年青和中年男性。90年之后，两个地区的人口统计则更接近对齐，虽然有用户提出西海岸地区人口在比统计的晚10年后才开始平滑。

图　12-12：人口金字塔比较了在1850年（左图）和1940年（右图）西海岸和大西洋中部地区的人口（见彩图45）

比起特定的视图，有些用户则对重现模式更感兴趣。一个用户对探索历史上男性主导职场的职业生涯感兴趣，但是发现在后半个世纪，女性的人数不断增加。用户系统性地探索数据，把视图保存在书签线索中，然后命名为“女性的崛起”，并和其他人分享。

类似地，一个更有数学思想的用户对于工作波动模式感兴趣，他创建了一条曲线，显示工作波动重现概率。另一个用户搜索被科技所取代的职位，如银行出纳员和电话操作员。每一种情况，其结果都是通过多视图所“蜿蜒”出来的一次旅行或者一个故事。

使一切变得有意义

用户观察了数据，他们通常会张贴问题寻找解释，或者假设该数据可能导致产生某种趋势。很多这种问题和假设吸引了其他用户的回复，开始了对社会理解的一个循环过程。在我们的现场部署中，一个用户对散点图评论咨询为什么新罕布什尔州有这么高的人均零售额（见图12-6）。另一个用户指出新罕布什尔州没有征零售税，而拥有美国第二大零售业的特拉华州也没有征零售税。照这种方式，讨论通常涉及引入可视化中不包含的上下文信息。比如用户迭代式构建了一条事件时间线来标注军事建设（见图12-8），而另一个用户标注了引入义务教育的教师图。

社交数据分析的一个例子是牙医在劳动力比率中的盛衰以及复苏（图12-13）。第一个评论指出了这种趋势，然后咨询其发生的原因。一条作为独立的话题回复该问题的主题为“和氟化作用有关？所以上升……和溺爱小孩，给他们吃了很多糖有关？”；而另一条回复主题是“由于预防性的牙医行业变得更有效，牙医寻找继续工作的方式（如很多人现在每年看两次牙，而十几年前每年只看一次）”。然而，最有说服力的，可能是包含链接到不同观点的评论，同时显示牙医和牙科技术。因为牙医人数比例已经下降了，而牙医人数又得到了显著增长，意味着在该领域内的专业性。针对这条评论，另一个用户问：“我想知道如果学校学牙医的费用变得太昂贵以至于人们根本不敢想，或者当他们上技校后，牙医从业费用很高，那牙医数量会减少吗？”视觉数据分析、历史知识和个人轶事都对理解过程(snsemaking process)有影响，阐明了塑造该数据的各种因素。

图　12-13：标注的工作向导视图（左图）突出显示了1930年后牙医人数的减少，（右图）由于牙医人员排序的上升，牙医从业人数得到增长（见彩图46）

评论的另一个作用是帮助解释数据，尤其对于数据集合中模糊的涵义或异常现象。对于IPUMS项目，虽然我们很努力工作，但是丢失数据以及标签晦涩的问题依然存在。为了比较所有人口统计普查年份的数据，必须形成共享的分类机制。以职位数据为例，使用的是20世纪50年代的模式。该模式不包含一些现代社会的职位如计算机程序员，而且一些标签含义不明。

一个很显著的职业被标记为“技工”(Oerative)，泛指主要包含有技术的劳动力。这个术语对于用户几乎没有意义，其中一个用户问：“技工到底是啥？”其他用户回复同样不解或者提出某种理解，如“我肯定它指的是工厂工人”。另一个人同意，指出工人的数量很大，技工的盛衰时间似乎和工厂的机器操作员的盛衰一致。

照这种方式，用户集体参与数据验证和解疑中，通常在数据中“种”下了“标记贴”(sgnposts)来帮助其他人理解。总的来说，大约16%的评论涉及数据命名、分类和收集问题。

人群中“冲浪”

我们观察到绝大多数最初是由他们自己的兴趣或者在概述中找到的兴趣项所驱动来探索数据的（如“噢！看看这些可怜的农民是怎么消亡的”）。最后，用户将没有更多的想法或者厌倦了探索。这时候，我们观察的每个用户都是不再探索可视化，而是去探索评论列表。有些用户认为这么做他们可以更快地发现有意思的观点。表达这种意思的评论包括：“我相信其他人发现了一些更有趣的东西”和“我需要站在别人的肩膀上”。

其他试验者对于他们知道的某些人或者发现其他人所调查的东西感兴趣。一个用户说：“我觉得自己就像数据偷窥者。我真的很喜欢看别人都在搜索什么。”在数据驱动的探索和社会导航之间切换很常见：通过评论列表发现的视图通常可以点燃新的兴趣点，并激励更多的可视化过程中的数据分析。在经过一些探索之后，参与者习惯性地回到列表寻求更多的兴趣灵感。因此，我们观察到在数据驱动的探索和社会导航之间正面的反馈循环：探寻社交活动有助于促进探索新的分析问题。换句话说，用户可以很流畅地在“向导”和“偷窥”两个角色之间切换。

结论

基于sense.us项目成果，我们发现交互式可视化和对社会的理解可以帮助用户更丰富地探索数据集。然而，作为研究原型系统，sense.us网站从未对外公布过。相反地，我在IBM的同事基于sense.us，发布了Many-Eyes.com站点：一个公共网站，用户可以上传他们自己的数据集，使用各种交互式可视化组件对数据可视化，参与在线讨论或者在该站点外部的博客和wiki中嵌入可视化视图。

本着同样的精神，Web服务如Swivel.com和Data360.org，以及商业产品如Spotfire Decision Site Posters和Tableau Server，都允许用户在Web上发布可视化帖子，吸引其他人参与社交数据分析过程。和较大的Web规模的社交计算的运动同时，仍然存在如何促进和支持社交形式的数据探索。关于如何集成数据分析和社交活动的很多有趣的研究问题仍然亟待解决。开发问题包括设计更好的社会导航提示、更丰富的标注技巧和结合用户的观察、问题和假设到推理分析故事中的新方法。

虽然我们在sense.us网站上看到的分析形式本质上是解释性的，但是该系统有明显的教育价值，而且用户反馈说使用sense.us既让人开心又能够获取信息。此外，用户生成的很多观察、问题和假设引起了专业分析师的跟进分析。数据可访问表示与社会交互相结合，有助于人们把数据转换为对社会更丰富的理解。

我发现这一切真的很美！

参考文献

[1]Anscombe,Francis J.（1973）."Graphs in Statistical Analysis."American Statistician，27，17-21.

[2]Bertin,Jacques.（1967）. Sémiologie Graphique,Gauthier-Villars.English translation by W.J Berg as Semiology of Graphics,University of Wisconsin Press，1983.

[3]Card,Stuart K.，Ben Shneiderman,and Jock D. Mackinlay.（1999）.Readings in Information Visualization:Using Vision To Think,Morgan-Kaufmann.

[4]Cleveland,William S. and Robert McGill.（1985）."Graphical Perception and Graphical Methods for Analyzing Scientific Data."Science，229（4716），828-833.

[5]Croes,Marnix.（2006）."The Holocaust in the Netherlands and the Rate of Jewish Survival."Holocaust and Genocide Studies，20（3），474-499.

[6]Robison,Wade,Roger Boisjoly,David Hoeker,and Stefan Young.（2002）."Representation and Misrepresentation:Tufte and the Morton Thiokol Engineers on the Challenger."Science and Engineering Ethics，8，59-81.

[7]Tufte,Edward R.（1997）. Visual Explanations:Images and Quantities,Evidence and Narrative,Graphics Press.

[8]Ware,Colin.（2004）. Information Visualization:Perception for Design,Second Edition,Morgan-Kaufmann.

[9]Wattenberg,Martin and Jesse Kriss.（2006）."Designing for Social Data Analysis."IEEE Transactions on Visualization and Computer Graphics，12（4），549-557.

第13章　数据所做不到的

Coco Krumme

数据做了很多事情：它使我们能够区分科学与迷信，重复与随机。在过去几十年中，科学家们收集、整理和存储数据的能力已经有了长足的发展。从医学的决策制定到软饮料的市场营销到供应链的管理，我们更多地是依靠事实而不是直觉。我们依靠数据驱动这一切。

但是，数据无法驱动一切。在刚刚过去的20世纪，心理学家对人们对任何事物的解释接近理性式的稳重这一理论已经“颇有微词”。实际上，我们在解释信息的时候通常是有倾向性的。此外，真实世界并不是和骰子游戏的概率一样简单。相反地，个人必须从观察到的经验模式中提取可能性。

本章是关于存在于数据和分析之间的近似和偏见的讨论。它描述了一组新的实验和工具来帮助我们更有效地利用数据，而且从医学、消费和金融决策领域不断增加的文献中抽取一些例子。

假设我请你从图13-1中识别出与众不同的一只鸭子。

答案很简单且可以立即给出：人类的眼睛可以从一群白天鹅中识别出丑小鸭。但是，对于计算机，其解决方案就没就有这么简单：对于绝大多数情况，图像识别软件远落后于人类的视觉系统。

图　13-1：人类视觉系统善于识别出与众不同的鸭子

我们的大脑能够快速地对新事物和旧事物进行对比。我们善于进行类比和扩展：将手套、毛衣和滑雪者关联在一起；卡通老鼠和卡通熊是相似的；卡通熊和真正的熊相似；后两类的相似性本身很不一样。

即使你抹去上下文，我们的视觉系统仍然非常擅长挑选模式。以图13-2为例：你将如何把这些数据分成两组？

图　13-2：我们可以构建模型来区分两组数据集（见彩图47）

从添加维度和数据点开始，该任务如果由人来做，将会逐渐变得太复杂；而计算机则可以较轻松地解析该数据。你或许可以在两个点集之间画出一条近似曲线，但是计算机能够构建一个模型进而以最精确的方式划分数据。

现在，看看图13-3的图表模式，它显示了三个主要制造商在2005年1～10月这段时间的股票价格。观察以下图表，在阅读下一段前给出答案：基于给定的模式，你预期这三只股票中哪一只股票在2006年会涨？

你是选择了第三只股票吗？因为它看起来最后在涨。那么，你错了。事实上，如果你选择第一只或者第二只股票，你也一样错了。没有哪只股票会上涨，也没有哪只会下跌。实际上，这三个图表甚至并不能表示股票价格。它们是随机生成的：垃圾数据——可能有人会如是说。但是一旦相信这些图表属于某个公司（在本例中指制造业），就会产生各种各样的猜测。即使有真正的图表，除非你对某个公司或者行业有专业知识，否则很难基于一只股票过去10个月的业绩预测它将来的走势。

图　13-3：三只股票（a、b和c）在2005年的业绩（见彩图48）

从噪音数据捏造故事的倾向有时被称为“叙述谬误”(nrrative fallacy)。即使你对问题持怀疑态度（或者读得太快），当我们调查一些拔尖的MBA学生——其中有些应聘金融业的工作——相同的问题，他们很确定地表达对这些“股票”走势的信心。有些人说会涨，有些人说会跌。当为这些图表补充一些随机生成的“新闻剪辑”，并随机和某个图表放在一起时，这些学生更坚定了自己之前的预测——这些可能显示了人们根据数据给自己讲一个好故事的能力(Kumme，待发表）。（再看一下财经记者写的一些俏皮话：“对失业率上升的恐惧导致道琼斯指数下跌100点。”）

如果说人类善于观察模式，则人类同时也是编造关于统计学故事的大师。如果我们知道数据来自哪里及其涵义，这个问题就不那么严重；如果我们面对的是来自很多数据源的证据和高赌注的产出时，这个问题便可能会是灾难性的。

最后一个例子，在探讨问题之前，想一想你的期望。把过去的结论应用于当前分析的倾向被称为“确认偏见”(cnfirmation bias)(Lrd等1979）。（想一想你认识的某些人，他们只是为了选出一些语句来进一步肯定自己的世界观而阅读。）这在处理数据时也是真实存在的现象。观察表明，科学家更偏向于坚持过去的假设，有时在面临着对过去假设否定的压倒性的证据面前亦是如此(Jng 2006）。同样，股票投资获得收益的投资商会根据经验舒缓地释放神经素多巴胺，帮助他们对市场的行为做出决定(L和Repin 2002）。

我们暂时假定你既不是科学家也不是股票投资商，而只是一个渺小的调查员，对以下两个投资方案选项做出选择：

·选项A：100%收益7400美元

·选项B：75%可能收益10000美元，25%没有任何收益

这里没有什么技巧：你可以看到每种结果的期望效益。你会选择哪一种呢？

现在，给出以下两种选项，你又会选择哪一种呢？

·选项C：100%损失7400美元

·选项D：75%可能亏损10000美元，25%没有亏损

绝大多数人选择A和D。这里出现了不对称的情况：我们往往在下跌时更愿意冒险，而在上涨时则更保守。也就是说，稳赚比赚得更多但可能不赚更吸引人，但是当需要支付一定资金时，我们宁愿选择可能亏损更多可能不亏的情况。此外，这种投资组合并没有得到最大回报：如果我们只是投机性地考虑收益和损失，我们应该选择B+C的组合方式，因为其总期望收益是1000美元，而A+D组合方式的总期望收益是-1000美元（也就是说，比起A+D组合方式，B+C组合方式总期望收益高出2000美元）。

Daniel Kahneman和Amos Tversky在20世纪70年代首先做了该实验，它揭示了人们通常都不是以概率的方式思考问题：相反，我们想象和每一种单一收益相关的感情收益。

事实上，在很多重要的方面，我们并不是以自己所假定的方式来处理数据。

何时数据无法驱动

前面的内容指出了人们在分析数据时存在的一些认知偏见。本文的剩余部分讨论数据所不能做的，也就是说，其衡量和解释本身可以转换数据的各种方式。本文不是一篇关于“谎言、天杀的谎言和统计”的论文：我们知道数据可以被故意混淆；本文的重点是它如何会被无意混淆。特别是在如下情况：

·我们使用数据的方式不够准确

·我们采用已知的偏见方式处理数据

虽然以下例子重点研究医学和财务数据，但是它们或多或少可以扩展。“数据”用于表示任意从经验、观察和实验中积累的原始的事实。

数据并非越多越好

统计是一门表示和近似的科学。我们捕获或者观察一个系统越多，就越能真实地表示它。一篇入门性的文章往往会强调：随着你增加样本大小，置信区间就减少，而没有丧失任何置信度。换句话说，更多的数据可以帮助你控制误差边际（见图13-4）。

图　13-4：正态分布（见彩图49）

教科书上一个不错的真相。在游离世界外，需要审查一些假设。首先，数据是如何分布的？它是否是正态的？例如，很多财务数据的分布偏离常态。医学数据（例如，特征表达）通常更多的是呈高斯分布，但是演变并不总是符合中心极限定理。

如果数据不是正态分布，更多的数据将不会减少你期望的误差边际。Karl Popper描述了我们如何使用数据来回答问题上的偏态：

虽然无论有多少个与假设一致的结果都无法证明该假设是正确的，但是只要存在一个反面结果，就可以推翻该假设。更多的数据只是增加了必然性边际，而一个实例就可以推翻一个世纪的信仰。

其次，“误报率”(flse positive)和“漏报率”(flse negative)的代价相同吗？即使你的数据是（或者看起来）是正态分布的，你对不同结果的兴趣可能也是不对称的。

例如，无法检测到威胁生命的疾病的误差所付出的成本可能比错误的诊断更高。在这种情况下，（通过减少“漏报率”）提高诊断正确性的数据比通过大量数据来减少“误报率”更有用。

更多的数据并不容易

数据并不一定需要大规模。信息时代一个陈腐的“箴言”是：处理10TB的数据和处理10比特的数据一样简单，而制作100亿个向导小工具要比制作10个更昂贵。

在某些情况下，清洁和处理数据的代价不低。当需要人工肉眼校验，比如阅读X射线或者问卷上的手写编码数据时，尤其如此。依据“红桃皇后”(Rd Queen)效应[1]，性能更好的计算机及其愈来愈强的收集数据的能力驱动了（而且被驱动）开发新的工具和采用新的方式来解析和使用数据。

也存在包含更多信息的认知成本。我们是选择超市的拥挤还是“401(k)条款”（401(k)plans)[2]，研究表明随着选项的增加，决策需要花费的时间越来越多，我们变得更可能不做任何选择而直接放弃，而且对于自己做出的选择的满意度也降低了(Ienger和Lepper 2000）。

最后，一个微妙的成本是：更多的数据会使我们变得盲目而看不到其他的可能性，尤其当我们需要收集并整理数据时。很难想象，看到更多的数据意味着更好地支持一个假设——确认偏差和抽样问题的必然结果之前已经讨论过了。

数据不会自我解释

人们做出了种种解释。你可能已经听说，相关性(crrelation)和因果关系(cusality)是“聚头冤家”(srange bedfellows)。给定两个统计上密切相关的变量，因果关系既可以变大也可以变小。统计学家喜欢滥用相关性（更不用提很多博客了），比如对现代世界传统价值观的衰落有所不满的老太太。

记者是这种统计“不满”的首要主导因素。例如，《华尔街日报》最近的一篇文章指出(sellenbarger 2008），由于婚前同居离婚的概率更高，未婚夫妇为了提高婚后呆在一起的概率，应该避免婚前同居。该项研究没有任何迹象表示其中存在因果关系，而这个记者基于“数据”，给情侣们提供了她自己的建议。

用因果关系替代相关性不需要如此明显。当开展一项科学研究项目时，存在以下假设：如果发现事物间存在相关性，就意味着它们存在因果关系，否则其关系就是未知的。否则，为什么要去回答研究问题呢：大规模地搜索没有因果关系的相关性属于偶然性计算，不是科学。即使是所谓的大数据，科学依然是一个很强的由假设驱动的过程。

经验研究的局限不是有道理的服输，只是小心谨慎地推进研究发现，不受因果关系影响。基于数据创造故事是人之常情：研究是不断地修改故事，使它变得合理。

仅仅是答案的数据是不好的

描述性统计信息会隐藏细节信息。例如，图13-5的图表显示了看起来显著不同的四种分布，但其均值和方差却是相同的。描述性统计学的两大支柱——均值和方差——提供的关于分布的信息很少(Ascombe 1973）。

图　13-5：Anscombe的“四重奏”(qartet)：每组数据集都有相同的均值和方差

当使用数据来制定决策时，我们倾向于认为数据分布只是为了最后的一个答案。我们可能需要基于二选一来制定决策——美国应该宣战吗？FDA（美国食品药品管理局）应该批准这种药物吗？预计谁会赢得竞选？（或者是做一个总结性陈述）美国人有多富有？在今后5年，地球的气候将会如何？——这些都是基于不确定性数据。即使报告了方差，重要的还是决策。

人们在思考时，考虑的是结果，而不是分布。以个人财务决策为例：我应该在股票、债券和现金上投资多少？即使过去的财务业绩可能预测将来的回报（即使财务顾问法律上需要承认这一点，事实上也并非如此）——也就是说，即使我们知道了分布的状态——我们还存在很多可选择的风险和回报组合，以及在这些分布范围内的很多可能的结果。给定一个风险水平，一个人退休时可能很富有也可能很贫穷，同时想象这几种情况很难（人们倾向于假设一个平均情况，或者有时是最佳的情况，即所谓的“规划性谬误”(panning fallacy)）。

一个决策学科学家团队创建了一个有趣的工具来帮助投资者理解结果分布的固有的概率范围（见图13-6）。参与者可以调整100个“单位概率”来形成一条分布曲线。举个例子，他们可以把所有单位都放在75%的薪金的位置，或者把这些单位平均分发到很多不同的比例水平。然后，点击“开始”(g)，观察这些单位一个个逐渐随机消失。最后留下的一项是“结果”(otcome)(Gldstein等2008）。因此，风险水平不是一条模糊的分布曲线，而是一组（这里是100个）具有相同发生概率的曲线。

图　13-6：Goldstein等开发的工具帮助人们理解作为一组结果的分布（见彩图50）

生物学家Stephen Jay Gould通过和结果等同的描述统计进一步阐明了这个问题。“中间产物不是想要的信息”(Te Median is not the Message)是Gould对癌症诊断和“只能活8个月”的警告的反应。关于癌症的文章基于“预先定义的一组环境”揭示了向右偏分布——也就是在过去治疗情况的假设上，寿命很长的幸存者的一个很长的故事。声称“8个月”的生存期具有很大的局限性，正如Gould优雅地给出兽性的统计特征：

进化生物学家知道自然变异本身是唯一不可改变的本质。变异是残酷的现实，而不是对集中趋势的不完美的衡量尺度。均值和中位数是抽象化的。

数据不能预测

建立模型（来预测明天的天气，2012年超级杯的结果或者世界500强的命运）是一门很吸引人的艺术。事实上，科学最重要的探险——解释我们周围的世界的一个重要扩展，正试着去了解世界的未来。

在某些领域，即受控的物质世界宇宙，有可能预测几乎确定的结果。未来的结果可以高保真地从过去事件的发展轨迹中得出：水加热时会变成气体；物体在真空中会以每秒9.8米的加速度下降；如果动物的心脏停止跳动，它就死了。从认识论上看，Popper的“可否认性”(flsifiability)理论没有实际意义，但是如果之前三个假设是真实的，那么它可以引导出健全的社会生活。

在确定性较低的领域，比如人类或物理行为，模型是帮助解释模式的重要工具。但是，当我们过于渴望让数据说话时，可能会导致生成过拟合模型。

以发现多普勒径向速度为例（该过程我仅有表面简单的理解：基本上，明亮的星星使得查看行星变得很难，因此天文学家识别了多普勒变化的各种组合，它们只有在行星绕过恒星时才会出现）。很难检测模型的灵敏度，而且只有15个观察，其数据可能非常适合图13-7的正弦曲线(G等2004）！

当模型过拟合时，它就失去了预测能力。此外，如果我们喜欢接受任何最适合已有数据的模型，而不关心其复杂性或灵敏性，那么我们就会犯一些错误。首先，忘记数据的因果关系，对它有损害，过度调试的模型无法说明任何东西。

其次，我们忘记数据（或者数据集合）可能是有限的，而世界本身可以改变。以试图预测200年前的世界气候问题为例。存在一些关键证据，长期以来还保持很高的分辨率，即化石记录和冰核中的全球气温数据。气候学家还可以推断出当地气温和来自日记和树的年轮的沉淀，但是其精度水平差别悬殊：18世纪的暴风玻璃和20世纪带GPS的气象气球不同。而且，谁能够知道相同组合的交互在21世纪驱动的气候事件和20世纪会相同呢？

同样，1914年的福特汽车公司和1975年不一样，和今天的福特也不一样，但是很多金融模型假定市场的最后一个周期的动态性也将会解释其未来的业绩（而且，模型对要考虑的相对时间周期做出了非常不同的假设）。因此，风险分析模型可能在很多时候只是偏离（可接受程度）一点点，但是当发生“未预期”的情况时，它可能就会被完全打破（例如房地产市场崩溃）。

图　13-7：外星识别模型

好的科学家了解坏模型的危险，但是被一个看起来特别好的模型所“诱惑”也不难。举个例子，2005年报告的过拟合模型的一个单位的经验（它当时是正确的，但是事后看来，它还差得很远）：

当一种新的模型方法大大提高了能量时，人们对它产生一定的怀疑是合适的。这种新方法通常是拟合数据收集的结果，而不是实际的底层行为关系。此外，这些问题在事后看来很显然，但是在我们当时有的数据清理过程中，并没有出现这些问题……最好的情况是，过度拟合会给新模型带来不必要的复杂性或者用户会诋毁该模型。最坏的情况是，它导致投资组合的风险评估的系统错误。

看看吧！

存在很多令人信服的理由来建立预测模型，包括探索场景和阐明假设；一个优秀的例子，参考Joshua Epstein 2008年发表的文章“为什么建模？”

概率不是直观的

这是构建统计时的另一个招人喜欢的“鞭笞”(fogging horse)策略，而且是出于善意的。统计学家不知疲倦地设计可爱的游戏，证明一个看似常识性的答案不正确的概率，而且条件概率和联合概率不是直观的。当数学家们和医生们被这些游戏愚弄时，这些统计学家便会格外高兴。

给定一个美国城市，100万居民当中大约有1000名（或0.1%）居民携带艾滋病毒阳性。一种新的艾滋病毒测试，包含1%的误诊率：每一百次中会有一次，它会将一个艾滋病毒阴性的人误诊为艾滋病毒携带者；反之亦然。

假设有人参加了测试，并被诊断携带艾滋病毒阳性。那么，他患有艾滋病毒的概率是多大呢？

很多人会回答说他患有艾滋病毒的概率是99%，因为测试的误诊率是1%。实际上，因为患有该疾病的人口比例非常小，任何人患有该疾病的概率，即使被诊断患有，也只有9.9%。（99.9万的艾滋病毒阴性的居民，有9990个会被诊断为携带艾滋病毒，而其中只有990个真正有艾滋病毒。对于结果是携带艾滋病毒的诊断，实际上真正有艾滋病毒的概率是990/9990或9.9%。）

医生给出的诊断错误率很高，至少是值得怀疑的。

在很多情况下，先验不会消失。当使用数据来回答问题时，我们不知道应该排除哪些证据，以及如何权衡哪些证据更重要。Daniel Kahneman——一个不知疲倦的概念命名者——把这种现象命名为“基准概率谬误”(bse rate fallacy)。

概率事件不是直观的

不但概率论很难把握，每个概率事件也很飘忽不定。当缺乏因果关系来把一个事件绑定到一组结果集上时，人们依赖过去的观察来估计概率。而且观察通常是以偏见方式收集的（尤其当通过经验囊括，而通过实验也很平常），并且很难记录、调试、加权、保存和查询。

真实世界并不创建随机变量

最开始，地球是没有形状的，而且没有东西。然后Fisher说：“让我们有z分数和变异数。”于是存在了z分数。Fisher看到回归很好，于是他把有意义的和无意义的区分开来。统计学的发明看起来是如此重大的创新，但是它们很难记住，因为它们不是自然规律。人可以想象另一个宇宙，那里设置了统计意义的标准阈值（被任意设置，正如在我们宇宙里）为p=0.01或p=0.06，而不是当前的p=0.05。想想那些已经被批准或拒绝的毒品、环境变量和健康影响之间的错误关联，你在汽车保险上花费的大量金钱！

在没有Fisherian的世界，不存在类似于独立随机变量的东西。实际上，很多东西关联度都很高。目前，实验控制相互依赖性是可能的，但是独立性就很难保证。正如我们最近所知，假设离散事件（如购房贷款人按揭拖欠）相互独立可能是错误的，但却有很多理论基于这种假设（如可交易的金融产品融入了贷款款项）不一定是错误的。

预测市场和群体决策制定过程能够非常出色地工作——在某些情况下，优于一组专家的估计。然而，当信息瀑布和相互依赖关系进入系统时，它们已经证明被打破了(Bkhchandani等1998）。

数据不是独立的

在现实世界的决策制定中，数据有很多种形式。信息很少会被清理并打包成一个格式良好的电子表格或“矩阵文件”(mtrix file)；取而代之，我们通常需要基于主观以及量化信息来制定决策。

举个例子，是否制定在线把钱借给别人的决策（为了盈利，作为建立贷款市场的一部分）。我和同事使用一组来自于P2P(peer-to-peer)平台Prosper.com的35万贷款数据集，对资金借贷和偿还进行了分析，其结果表明不论有多少个模型（混合模型、神经网络、决策树和回归）能够预测谁能够获得贷款以及谁能够准时偿还的准确率只有75%。大量的数据——包括该网络的每个人有100多种个人财务健康指标——都可以根据现有算法来计算，但是哪个申请者可以获得资格以及哪个无法得到资助还是难以预测。通过量化主观特征，模型可以得到部分完善。当一个人决定是否把钱借给网络的某个成员时，出借人（和银行不同）考虑了很多“软”因素：借款人的目的声明、公司形象、拼写、语法以及其他个人资料信息。为了把其中一些特征纳入我们的模型，我们请人工（来自Amazon的Mechanical Turk)对Prosper.com网站的成员图像进行编码，首先是内容——该图像是否描述一个人、家庭、车等——然后是“诚信”评分：即该问题的答案，“你会借钱给这个人吗？”

但是模型仍然存在不足：社会因素对贷款的动态性有着意想不到的作用。和我们的假设相反，贷款给别人的决定不是独立的。而证据表明，竞标存在一些“羊群效应”：出借人学其他出借人，而且随着一个贷款上的竞标增加，单位时间的竞标也加速。

即使考虑这些情况以及其他的社会因素，很多出借人还是制定了次优的贷款决定。Proper网站在理论上是一个接近完美的市场：几乎所有的人都可以访问该站点的API，并重复我们的分析。但是出借人对于非常冒险的投资，还是只得到非常低的回报：给定统计的期望回报，很差的投资赌注的数量多得惊人。即使有可靠的信息（而且主观数据代理人谦逊温和），决策通常不是根据数据直接指定的，反过来，数据只能够从一定程度上解释人们的决策。

数据受到观察者的影响

最后，即使在可靠的因果关系是可能的情况下，如果数据是由Fisher和Bayes（如果该学生如此虔诚）的一个明智的学生老老实实地收集和建模，并由该学生负责改变并验证其模型（而且对模型结果仍然持怀疑态度），那么很多认知偏见会模糊人们的思考。在真实世界中，我们最好能够以概率性假设方式来操作。

正如统计学家常常在他们的博客上表示不满，行为经济学家在各自的编年史上都臭名昭著。如之前提到的叙述性谬误、确定偏见、选择悖论、冒险、基础概率谬误以及夸张的贴现。心理学家已经索引了很多其他的，包括从“锚定”（过度依赖于最近单点数据来制定决策）到“Lake Wobegon效应”（超出一半的人认为他们高于平均水平）。

随着这些影响被更好地记录，我们可以开发工具并利用直觉来帮助采用数据的票面价值（我的一部分工作重点为金融决策开发工具）。从某种意义上来说，解决办法很简单：如果你不理解数据的局限性，那么数据并没有太多价值。

[1]Lewis Carroll的《爱丽丝梦游仙境》中的“红桃皇后”说“为了呆在原地，你需要尽力奔跑”。该思想被用于描述一个系统，由于外部压力的竞争，必须不断地和竞争者共同演化发展。

[2]401(k)条款指的是20世纪80年代初制定的美国《国内税收法》的第401条第k项条款，是由雇员和雇主共同缴费建立起来的养老保险制度。

结束语

不用多说，我们在一个丰富的数据时代生活。很多东西便宜松垮。进化过程已经使我们能够注意到环境的显著变化——众所周知的老虎和海啸——识别老虎的脸，描述海啸以帮助记忆。但是我们缺乏基础设施来收集和排序大量的异构数据集。我们有哪些基础设施必须慎重利用：我们可以通过理解概率和概率的极限来开始更好地利用数据，而且对帮助阐释世界的认知偏见持审慎态度。

观察者眼里的数据真的可以很美！

参考文献

[1]Anscombe,F. J."Graphs in Statistical Analysis."The American Statistician,vol.27，no.1(Fbruary 1973），pp.17-21.

[2]Bikhchandani,S. et al.（1998）."Learning from the Behavior of Others:Conformity,Fads,and Informational Cascades，"Journal of Economic Perspectives,vol.12，issue 3，pp.151-170.

[3]Dwyer,D. W.2005."Examples of overfitting encountered when building private firm default prediction models"(ww.moodyskmv.com/research/files/wp/Overfitting_Private_Firm_Models.pdf).

[4]Epstein,Joshua M.（2008）."Why Model?". Journal of Artificial Societies and Social Simulation 11（4）12.

[5]Ge,J. et al.，2004，"All Sky Extrasolar Planet Searches with Multi-Object Dispersed Fixeddelay Interferometer in Optical and near-IR."Proc.SPIE，5492，711.

[6]Goldstein,Daniel G. et al.（2008）."Choosing Outcomes Versus Choosing Products:Consumer-Focused Retirement Investment Advice."Journal of Consumer Research，35(Otober)，440-456.

[7]Gould,S. J."The Median is not the Message."Discover Magazine，1985.

[8]Iyengar,S. S.and M.R.Lepper."When choice is demotivating:can one desire too much of a good thing?"Journal of Personality and Social Psychology，2000，vol.79，no.6，995-1006.

[9]Jeng,M."A selected history of expectation bias in physics."American Journal of Physics，2006.

[10]Kahneman,Daniel and Amos Tversky（1979）."Prospect Theory:An Analysis of Decision under Risk，"Econometrica,XLVII（1979），263-291.

[11]Krumme,C."Telling tales:the effects of narrative creation on decision-making with data，"working paper.

[12]LO'A. W.and D.V.Repin."The Psychophysiology of Real-Time Financial Risk Processing"Journal of Cognitive Neuroscience,April 1，2002，vol.14，no.3，323-339.

[13]Lord,C. G.et al.（1979）."Biased assimilation and attitude polarization:The effects of prior theories on

[14]subsequently considered evidence."Journal of Personality and Social Psychology，37，2098-2109. Shellenbarger,Sue.Wall Street Journal"Work and Family"column,March 22，2008.

第14章　自然语言语料库数据

Peter Norvig

本书大部分阐述的是“波德莱尔(Budelaire)意义”上的数据之美：“一切美好和崇高的都是理性和计算的结果。”本章的数据是“梭罗(Toreau)意义”上的数据之美：“人们总是被最平凡的演讲之美打动。”本章要阐述的数据是“最平凡的演讲”：取自公开的Web页面中的总长度达1MB英语单词的数据。这些数据涵盖了Web所有的“陈词滥调”，既有拼写和语法错误、哈哈大笑(Lugh Out Loud,LOL)的猫、踢踢滚滚(Rckrolling)，也包含马克·吐温、狄更斯、奥斯汀和几百万其他作家的作品集。

G公司的Thorsten Brants和Alex Franz于2006年发布了1MB的单词数据，你可以在语言数据联盟(Lnguistic Data Consortium)上可以获取(htp：//tinyurl.com/ngrams)。该数据集通过计算每个单词的出现次数，依照每二词(to-word)、三词、四词和五词序列对原始文本求和。举个例子，“the”这个单词出现了230亿次（占了1MB单词的2.2%），该词是最常用的单词。单词“rebating”出现了12750次（占百万分之一），还有“fnuny”（显然是“funny”的误拼）。在三词序列中，“Find all posts”出现了1300万次（占0.001%），“each of the”出现频率与之相似，但都低于出现了1亿次的“All Rights Reserved”（占0.01%）。以下是三词序列的一个摘录：

outraged many African 63

outraged many Americans 203

outraged many Christians 56

outraged many Iraqis 58

outraged many Muslims 74

outraged many Pakistanis 124

outraged many Republicans 50

outraged many Turks 390

outraged many by 86

outraged many in 685

outraged many liberal 67

outraged many local 44

outraged many members 61

outraged many of 489

outraged many people 444

outraged many scientists 90

从摘录可见，土耳其人是最易产生不满的组织（来自网络，根据当时收集的数据），共和党和自由党人有时会产生不满，而民主党和保守党则很少有不满。

为什么我说数据很美并不乏味？单个计数都是乏味的。但是这些计数的聚集——几亿个计数则是美丽的，因为它具有很多含义。计数的聚集不再仅仅是英语，而是关于说英语的人们的世界。数据是美丽的，因为它表示了很多值得表达的东西。

对于这些数据，在观察可以做什么之前，我们需要弄清如何探讨(tlk the talk)——学习一些术语。文本的集合称为语料库。我们把语料库看做token序列——单词和标点符号。每个不同的token称为类型(tpe)，因此文本“Run,Lola Run”包含四个token（逗号也作为一个token)，但只有三种类型。类型的集合称为词汇。G公司语料库包含1MB的token，1300万种类型。英语在词典里出现的单词大约只有100万，但是语料库包含这样的类型，如“www.njstatelib.org”、“+170.002”、“1.5GHz/512MB/60GB”和“Abrahamovich”。但是绝大多数的类型是很少见的，最通用的10个类型占几乎1/3的token，最通用的1000个占超过2/3，而最通用的10万个占98%。

1-token序列是一元，2-token序列是二元，n-token序列是N元。P代表概率，如P(the)=.022，表示token“the”的概率是0.022或者2.2%，如果用W表示token序列，那么W3表示第三个token，而W1：3表示从第一个到第三个token。P(Wi=the|Wi-1=of)是token“the”的条件概率，表示在先验token“of”下的概率。

G公司语料库的一些细节：出现次数少于200次的单词作为未知类型，以符号＜UNK＞表示。丢弃出现次数少于40次的N元。该策略减少了打字（错误）的影响，使得数据集只有24G（压缩后）。最后，语料库的每个句子都以特殊的字符＜S＞开始，＜/S＞结束。

我们现在来看看可以利用该数据完成的一些任务。

分词

考虑中文文本“浮法像蝴蝶”，它的英语翻译是“float like a butterfly”。它包含5个字符，但是字符间没有空格，因此中文阅读者需要执行分词：决定单词的边界位置。英语阅读者通常不需要执行分词，因为单词间存在空格。但是，一些文本，如URL，通常不包含空格，但是有时输入错误，会留个空格；搜索引擎或文本处理程序如何纠正这样的错误呢？

比如英语文本“choosespain.com”，这是个网站，希望你选择西班牙作为旅行终点站。但是如果你把名字切分错误，你会得到这样的分词“chooses pain”，这个名字就不那么吸引人了。人工阅读可以通过几年的经验来做出正确的决策；但是把这种经验编码为计算机算法将是一项难以完成的任务。但是我们可以采取一种捷径，其效率之好让人惊讶：在二元表里查询每个词汇。我们发现“choose Spain”出现了3210次，而“chooses pain”在表里一次都没有出现（这意味着它在1MB的语料库中出现次数少于40次）。因此，“choose Spain”出现至少80次，可以作为正确的分割。

假如我们需要解释短语“insufficientnumbers”的含义，如果我们把单词的大小写加起来，其计数是：

insufficient numbers 20751

in sufficient numbers 32378

“In sufficient numbers”的出现频率高于“insufficient numbers”，但是这并不具备很强的说服力。这种情况令人沮丧：我们可以猜测，但是无法确定。对于类似这种不确定问题，没有方法能够确定地给出正确的值，我们还没有能够确保答案正确的完备模型，实际上人类专家也没有一个完备的模型，可以不同意该答案。但是，解决不确定问题有个确定的解决方案。

1.定义概率模型。我们无法定义所有的因子（语义的、语法的、词汇的和社会的）使得选择“choose Spain”对于一个域名是一个更佳的候选。但是我们可以定义一个简化的模型，从而能得到近似概率。对于“choose Spain”这种简短的候选，我们可以在语料库数据中查找n元，并使用该概率。对于更长的候选，我们需要切分为多个部分，组合这些部分得到结果值。核心是我们定义语言模型——在该语言中所有字符串的概率分布——从语料库数据中学习该模型参数，然后使用该模型来定义每个候选的概率。

2.枚举候选项。我们无法确定“insufficient numbers”还是“in sufficient numbers”更可能是所期望的短语，但是我们可以确定它们都是候选分割，如“in suffi cient numb ers”也是候选项，但“hello world”不是有效的候选项。在该步骤中，我们不做判断，只是枚举可能的候选项——如果可以，列出所有的候选项，或者列出仔细选择的一个样本。

3.选择最可能的候选项。对每个候选项应用语言模型来获得它的概率，选择概率最高的是选项。

如果你更习惯于数学等式表达，如下所示：

best=argmaxc∈candidates P(c)

或者，如果你更习惯于计算机代码表达，则如下：

best=max(c in candidates,key=P）

我们把这种方法用于分割中。定义一个函数、分段，把不包含空格的字符串作为输入，返回最佳分段的单词列表。

＞＞＞segment（'choosespain'）

['choose'，'spain']

我们从步骤1——概率语言模型开始。单词序列的概率是每个单词概率的乘积，假设单词的上下文是：所有上述单词。等式表达如下：

P(W1：n）=Πk=1：nP(Wk|W1：k-1）

我们没有数据来精确地计算该等式。因此我们可以使用一个更小的上下文来近似计算。由于数据序列大于五元，我们就采用五元文法模型，因此N元文法的概率即给定前缀四个单词（不是前缀所有单词）的每个单词的乘积。

五元文法模型存在三个难题。第一，五元数据大约30G，因此无法全部装载到内存中。第二，很多五元计数将为0，我们需要一些回退(bcking off)策略，使用更短的序列来估计五元文法的概率。第三，候选项的搜索空间将会很大，因为依赖性扩展到四个单词。这三个难题花些时间都是可控的。但是，我们首先来考虑解决这三个问题的一个更简单的语言模型：一元模型，其序列概率即每个单词自身的概率乘积。在一元模型中，每个单词的概率和其他单词无关。

P(W1：n）=Πk=1：nP(Wk)

对'wheninrome'进行分割，考虑这些候选项，如when in rome，然后计算P(when)×P(in)×P(rome)。如果该候选项的乘积高于任何其他候选项，那么它就是最佳答案。

n个字符的字符串包含2n-1个不同的分割方式（字符间有n-1个位置，每个位置都可以作为分割边界）。因此，字符串'wheninthecourseofhumaneventsitbecomesnecessary'包含35T的分割方式。但是我确定你可以在几秒钟内找到合适的分割方式；显然，你不可能枚举所有的候选类型。你可能浏览了“w”、“wh”和“whe”，并作为不可能的单词放弃它们，而接受“when”作为可能的分割方式。然后继续剩余部分，并找到它们的最佳分段方式。一旦我们简化假设每个单词和其他的单词是不一样的，那意味着我们不需要考虑单词的所有组合。

以上简单地给我们描述了segment函数：考虑把文本划分为一个起始单词和剩余文本的所有方式（可以任意限制单词最大长度，如L=20个字母）。对于每种可能的划分，找到对剩余文本分词的最佳方式。在所有的候选项中，乘积P(first)×P(remaining)值最高的即为最佳方案。

这里我们用一个表格来说明这个问题。表格中包含了第一个单词的所有候选、该单词的概率、剩余单词的最佳分词概率以及所有的概率（即第一个单词和剩余单词的概率的乘积）。我们发现，以“when”开始的分词概率是第二个最佳候选的5万倍。

我们可以通过几行Python代码来实现分词：

@memo

def segment(text)：

"Return a list of words that is the best segmentation of text."

if not text:return[]

candidates=（[first]+segment(rem)for first,rem in splits(text)）

return max(candidates,key=Pwords)

def splits(text,L=20）：

"Return a list of all possible(first,rem)pairs,len(first)＜=L."

return[(txt[：i+1]，text[i+1：]）

for i in range(min(len(text)，L））]

def Pwords(words)：

"The Naive Bayes probability of a sequence of words."

return product(Pw(w)for win words)

这是整个程序——包含三个较小的省略部分：product是把数字列表进行乘积的工具函数；memo是一个装饰器(dcorator)，对函数product的结果进行缓存，因而这些结果就不需要重新计算；Pw通过询问单元计数数据来估计一个单词的概率。

没有装饰器memo，对一个包含了n个字符的文本段的调用会导致对该段的2n次递归调用；有了memo，它就只执行n次调用——memo使得调用变成一个高效的动态编程算法。对于n次调用，每个都需要考虑O(L)次的分片，对每个分片乘以O(n)的概率来估算每个分片的代价，因此整个算法代价是O(n^2L）。

对于P w，我们从一个数据文件读取单元计数值。如果一个单词在语料库中出现，它的估计概率是Count(word)/N，其中N是语料库的大小。实际上，我没有采用1300万类型的单元数据文件，而是创建了vocab_common，它有几方面特性：（1）大小写不敏感，因此“the”、“The”和“THE”的计数值都加在一起，作为一个词条的“the”；（2）词条是由字母表示的，而不是数字或者标点符号（因此“+170.002”不能作为词条，同样“can't”也不能；（3）100万单词中最通用的1/3单词。

Pw唯一较难处理的部分是当一个单词不在语料库中时如何处理更为合适。即使是在包含了10000亿个单词的语料库中，这种情况有时也会发生，因此把概率当做0返回是错误的。但是它应该为多少呢？语料库中的token个数N，几乎有10000亿，vocab_common中最不经常出现单词有12711个。因此之前没有看见的单词，其概率应该在0～12711/N。不是所有未见到的单词的概率都是一样的：一个有20个字母的随机序列是单词的概率要小于一个有6个字母的随机序列。我们将为概率分布定义一个类Pdist，它加载一个内容是(ky,count)对的数据文件。默认情况下，一个未知单词的概率是1/N，但是Pdist的每个实例可以提供一个自定义函数对默认值进行重载(oerride)。为了避免对于很长的单词概率太高，因此我们确定（相当随意）了一个起始概率10/N，对于候选单词中的每个字母，以因子10来递减。我们定义Pw为一个如下的Pdist：

class Pdist(dict)：

"A probability distribution estimated from counts in datafile."

def__init__(slf,data,N=None,missingfn=None)：

for key,count in data：

self[key]=self.get(key，0）+int(count)

self.N=float(N or sum(self.itervalues（）））

self.missingfn=missingfn or(lambda k,N：1./N）

def__call__(slf,key)：

if key in self:return self[key]/self.N

else:return self.missingfn(key,self.N）

def datafile(name,sep='\t'）：

"Read key,value pairs from file."

for line in file(name)：

yield line.split(sep)

def avoid_long_words(word,N）：

"Estimate the probability of an unknown word."

return 10./（N*10**len(word)）

N=1024908267229##Number of tokens in corpus

Pw=Pdist(datafile（'vocab_common'），N,avoid_long_words)）

注意，Pw[w]是单词w的原始计数，而Pw(w)是它的概率。本章描述的所有程序都能够通过http：//norvig.com/ngrams获得。

因此，该模型分割的效果如何呢？以下是一些实例：

＞＞＞segment（'choosespain'）

['choose'，'spain']

＞＞＞segment（'thisisatest'）

['this'，'is'，'a'，'test']

＞＞＞segment（'wheninthecourseofhumaneventsitbecomesnecessary'）

['when'，'in'，'the'，'course'，'of'，'human'，'events'，'it'，'becomes'，'necessary']

＞＞＞segment（'whorepresents'）

['who'，'represents']

＞＞＞segment（'expertsexchange'）

['experts'，'exchange']

＞＞＞segment（'speedofart'）

['speed'，'of'，'art']

＞＞＞segment（'nowisthetimeforallgood'）

['now'，'is'，'the'，'time'，'for'，'all'，'good']

＞＞＞segment（'itisatruthuniversallyacknowledged'）

['it'，'is'，'a'，'truth'，'universally'，'acknowledged']

＞＞＞segment（'itwasabrightcolddayinaprilandtheclockswerestrikingthirteen'）

['it'，'was'，'a'，'bright'，'cold'，'day'，'in'，'april'，'and'，'the'，'clocks'，

'were'，'striking'，'thirteen']

＞＞＞segment（'itwasthebestoftimesitwastheworstoftimesitwastheageofwisdomitwastheage

offoolishness'）

['it'，'was'，'the'，'best'，'of'，'times'，'it'，'was'，'the'，'worst'，'of'，'times'，

'it'，'was'，'the'，'age'，'of'，'wisdom'，'it'，'was'，'the'，'age'，'of'，

'foolishness']

＞＞＞segment（'asgregorsamsaawokeonemorningfromuneasydreamshefoundhimselftransformed

inhisbedintoagiganticinsect'）

['as'，'gregor'，'samsa'，'awoke'，'one'，'morning'，'from'，'uneasy'，'dreams'，'he'，

'found'，'himself'，'transformed'，'in'，'his'，'bed'，'into'，'a'，'gigantic'，

'insect']

＞＞＞segment（'inaholeinthegroundtherelivedahobbitnotanastydirtywetholefilledwiththe

endsofwormsandanoozysmellnoryetadrybaresandyholewithnothinginittositdownonortoeat

itwasahobbitholeandthatmeanscomfort'）

['in'，'a'，'hole'，'in'，'the'，'ground'，'there'，'lived'，'a'，'hobbit'，'not'，'a'，

'nasty'，'dirty'，'wet'，'hole'，'filled'，'with'，'the'，'ends'，'of'，'worms'，'and'，

'an'，'oozy'，'smell'，'nor'，'yet'，'a'，'dry'，'bare'，'sandy'，'hole'，'with'，

'nothing'，'in'，'it'，'to'，'sitdown'，'on'，'or'，'to'，'eat'，'it'，'was'，'a'，

'hobbit'，'hole'，'and'，'that'，'means'，'comfort']

＞＞＞segment（'faroutintheunchartedbackwatersoftheunfashionableendofthewesternspiral

armofthegalaxyliesasmallunregardedyellowsun'）

['far'，'out'，'in'，'the'，'uncharted'，'backwaters'，'of'，'the'，'unfashionable'，

'end'，'of'，'the'，'western'，'spiral'，'arm'，'of'，'the'，'galaxy'，'lies'，'a'，

'small'，'un'，'regarded'，'yellow'，'sun']

看到程序正确地对一些生僻的单词如“Samsa”和“oozy”进行分割，你可能会很高兴。对于“Samsa”在10000亿个单词中出现了42000次，“oozy”出现了13000次，你应该不会太惊讶。分割的整体结果看起来不错，但是有两个错误：'un'、'regarded'应该是一个单词，而'sitdown'应该是两个单词。尽管如此，分词的准确率是157/159=98.7%，还是不错的。

第一个错误是由于“unregarded”在我们的三十多万单词的词汇里（它在所有单词词汇中的位置是1005493，计数值是7557）。如果我们把它放入词汇中，我们发现之前的分词是正确的。

＞＞＞Pw['unregarded']=7557

＞＞＞segment（'faroutintheunchartedbackwatersoftheunfashionableendofthewesternspiral

armofthegalaxyliesasmallunregardedyellowsun'）

['far'，'out'，'in'，'the'，'uncharted'，'backwaters'，'of'，'the'，'unfashionable'，'end'，'of'，'the'，'western'，'spiral'，'arm'，'of'，'the'，'galaxy'，'lies'，'a'，'small'，'unregarded'，'yellow'，'sun']

这并没有证明我们解决了问题：我们需要放回所有其他的干扰单词，而不仅仅是我们需要的那个；而且我们需要返回所有的测试案例，确保增加其他的单词并没有混淆了任何其他的结果。

第二个错误是虽然“sit”和“down”是频繁词（概率分别为0.003%和0.04%），但是这两个单词的概率乘积刚好略小于“sitdown”这个单词本身的概率。根据二元计数，两个单词序列“sit down”的概率比其分别的概率乘积约大100倍。我们可以通过对二元建模来解决这个问题；也就是说，考虑每个单词的概率，给定前一个单词的概率是：

P(W1：n）=Πk=1：nP(Wk|Wk-1）

当然，整个二元单词表无法全部装载到内存中。如果我们只保留出现100000次以上的二元单词，结果就有250000多的入口项，这也无法装载到内存中。然而，我们可以通过Count(sit down)/Count(sit)来估计概率P(down|sit)。如果一个二元单词在表中不出现，我们就通过单元值来计算。在一个单词的前序单词给定的情况下，我们可以定义这个单词的条件概率cPw如下：

def cPw(word,prev)：

"The conditional probability P(word|previous-word)."

try：

return P2w[prev+''+word]/float(Pw[prev]）

except KeyError：

return Pw(word)

P2w=Pdist(datafile（'count2w'），N）

（细心的人会注意到cPw不是概率分布，因为对于一个给定的先验单词，所有单词的概率之和会超过1。这种方法的技术名称是“愚蠢回退”(supid backoff)，但其实际效果很不错，因此我们不用担心。）我们现在来比较包含先验单词“to”、“sitdown”和“sit down”的概率：

＞＞＞cPw（'sit'，'to'）*cPw（'down'，'sit'）/cPw（'sitdown'，'to'）

1698.0002330199263

我们发现“sit down”比“sitdown”的概率高1698倍，因为“sit down”是一个高频的二元项，而且“to sit”词频很高，而“to sitdown”词频不高。

这看起来振奋人心：让我们用二元模型来实现新版的分词。但要实现这种新版分词，我们还需要解决另外两个问题：

1.当给一个由n个单词构成的序列增加一个新的单词时，它调用Pwords来对这n+1个概率值进行乘法运算。但是分词时，原有的n个概率值已经做过一次运算处理了。将每个元素的运算结果也就是概率值保存起来，这样添加一个单词的时候就只需要另外执行一次运算，因此分词也将更为高效。

2.可能存在算术运算向下溢出问题。如果我们把Pwords应用于一个出现了61次“blah”之后的序列，我们将得到5.2×10-321的概率，而如果再加上一个“blah”，其概率为0.0。我们能够表示的最小正浮点数是4.9×10-324；任何小于该值的概率都置为0.0。为了避免向下溢出，最简单的解决方案是对这些数值使用对数运算而非直接进行乘法运算。

我们将定义“segment2”（分词2），它和分词有三个方面的区别：第一，它使用了条件二元语言模型，cPw，而不是单元模型Pw。第二，函数特征是不同的。“segment2”除了需要把“文本”作为一个参数传递给函数之外，还需要将这段文本的一个单词也传递给函数。在句子的开始，前一个单词是句子的起始标记＜S＞；其返回值也不只是一个单词的序列，而是包含了两项内容：分词的概率和单词序列。我们返回概率，这样可以把它保存起来（通过记录的装饰器memo)，不需要重新计算；这解决了问题：（1）的效率低问题。合并函数接受四个输入：第一个单词、剩下的单词以及它们分别的概率。合并函数会把第一个单词添加到其他单词后面，并对所有的概率值进行乘法运算。但为了解决问题（2）[1]我们引入了第三个区别：不对概率值直接进行乘法运算而是对概率的对数执行加法运算。

以下“segment2”的代码：

from math import log10

@memo

def segment2(txt,prev='＜S＞'）：

"Return(log P(words)，words)，where words is the best segmentation."

if not text:return 0.0，[]

candidates=[combine(log10(cw(first,prev)），first,segment2(rm,first)）

for first,rem in splits(text)]

return max(candidates)

def combine(Pfirst,first，(Pem,rem)）：

"Combine first and rem results into one(probability,words)pair."

return Pfirst+Prem，[first]+rem

segment2执行了O(nL)次的递归调用，每次调用需要考虑O(L)次的切分，因此整个算法的复杂度是O(nL^2）。实际上，这就是Viterbi算法，memo隐含地创建Viterbi表。

segment2正确地对“sit down”例子进行分割，第一个版本中能正确切分的例子，在这个版本中也仍然是正确的。但这两个版本对“unregarded”的分词都不正确。

我们能否在性能上有所提升？很有可能。我们可以对未知单词创建一个更精确的模型；可以组合更多的数据，或者在单元数据或者二元数据中保留更多的入口项，或者添加三元数据。

[1]乘法运算的向下溢出问题。

密码

我们所面临的第二个挑战是对加密的消息进行解密。我们首先一起来看看替换加密，即使用一个字母替换另一个字母。对其中替换关系的描述称为密钥，可以通过26个字母的字符串来表示它：第一个字母替换“a”，第二个字母替换“b”等。以下是通过替换密码密钥对消息进行加密(Pthon的库函数maketrans和translate可以实现大部分功能）：

def encode(msg,key)：

"Encode a message with a substitution cipher."

return msg.translate(string.maketrans(ul(alphabet)，ul(key)））

def ul(text)：return text.upper（）+text.lower（）

alphabet='abcdefghijklmnopqrstuvwxyz'

可能所有代码中最简单的就是移位加密，它是一种替换加密方法，在这种加密方法中，消息中的每个字母都由其后面的第n个字母进行替换。如果n=1，那么“b”替换“a”，“c”替换“b”……直到“a”替换“z”。移位加密也被称为凯撒(Cesar)加密法；它们在公元前50多年很先进。变换(sift)函数使用移位加密的方法进行编码：

def shift(msg,n=13）：

"Encode a message with a shift(Caesar)cipher."

return encode(msg,alphabet[n：]+alphabet[：n]）

我们如下使用该函数：

＞＞＞shift（'Listen,do you want to know a secret?'）

'Yvfgra,qb lbh jnag gb xabj n frperg?'

＞＞＞shift（'HAL 9000 xyz'，1）

'IBM 9000 yza'

在不知道密钥的情况下进行解密，我们遵循和分段相同的方法：定义一个模型（我们将继续采用单元单词概率方式），枚举候选项并选择最有可能的。由于只需要考虑26种候选移位，所以我们可以尝试全部候选移位。

为了实现这一点，我们定义函数logPwords，它和函数Pwords相似，但是它返回的是概率的自然对数值，其输入可以是一个很长的字符串，也可以是一个单词序列：

def logPwords(words)：

"The Naive Bayes probability of a string or sequence of words."

if isinstance(words,str)：words=allwords(words)

return sum(log10(P(w)）for w in words)

def allwords(text)：

"Return a list of alphabetic words in text,lowercase."

return re.findall（'[a-z]+'，text.lower（））

现在我们可以枚举所有候选项进行解码并挑选最有可能的：

def decode_shift(msg)：

"Find the best decoding of a message encoded with a shift cipher."

candidates=[shift(msg,n）for n in range(len(alphabet)）]

return max(candidates,key=logPwords)

我们可以通过测试来检查这种方式是否能够工作：

＞＞＞decode_shift（'Yvfgra,qb lbh jnag gb xabj n frperg?'）

'Listen,do you want to know a secret?'

这一切都太简单了。为了查看为什么简单，观察以下26个候选项，以及它们的概率对数值：

Yvfgra,qb lbh jnag gb xabj n frperg?-84

Zwghsb,rc mci kobh hc ybck o gsqfsh?-83

Axhitc,sd ndj lpci id zcdl p htrgti?-83

Byijud,te oek mqdj je adem q iushuj?-77

Czjkve,uf pfl nrek kf befn r jvtivk?-85

Daklwf,vg qgm osfl lg cfgo s kwujwl?-91

Eblmxg,wh rhn ptgm mh dghp t lxvkxm?-84

Fcmnyh,xi sio quhn ni ehiq u mywlyn?-84

Gdnozi,yj tjp rvio oj fijr v nzxmzo?-86

Heopaj,zk ukq swjp pk gjks w oaynap?-93

Ifpqbk,al vlr txkq ql hklt x pbzobq?-84

Jgqrcl,bm wms uylr rm ilmu y qcapcr?-76

Khrsdm,cn xnt vzms sn jmnv z rdbqds?-92

Listen,do you want to know a secret?-25

MjtufO'ep zpv xbou up lopx b tfdsfu?-89

Nkuvgp,fq aqw ycpv vq mpqy c ugetgv?-87

Olvwhq,gr brx zdqw wr nqrz d vhfuhw?-85

Pmwxir,hs csy aerx xs orsa e wigvix?-77

Qnxyjs,it dtz bfsy yt pstb f xjhwjy?-83

Royzkt,ju eua cgtz zu qtuc g ykixkz?-85

Spzalu,kv fvb dhua av ruvd h zljyla?-85

Tqabmv,lw gwc eivb bw svwe i amkzmb?-84

Urbcnw,mx hxd fjwc cx twxf j bnlanc?-92

Vscdox,ny iye gkxd dy uxyg k combod?-84

Wtdepy,oz jzf hlye ez vyzh l dpncpe?-91

Xuefqz,pa kag imzf fa wzai m eqodqf?-83

当你扫描列表时，刚好一条线很明显，类似英语；而Pwords和我们的直觉一致，假设该行的概率的对数值是-25（即10-25），它的可能性是任何其他候选项的10^50倍多。

代码编写者通过消除单词之间的标点符号、空格和大小写区别，使得解码者的工作变得更加困难。采用这种方式，解码者无法从简短的单词如“I”、“a”和“the”中得到任何线索，也无法猜测省略符号（'）之后的字符应该是“s”还是“t”。函数shift2是一种加密方法，它删除了非字符的数据，把所有的字符都转换成小写，然后再应用变换加密方法：

def shift2(mg,n=13）：

"Encode with a shift(Caesar)cipher,yielding only letters[a-z]."

return shift(just_letters(msg)，n）

def just_letters(text)：

"Lowercase text and remove all characters except[a-z]."

return re.sub（'[^a-z]'，''，text.lower（））

以下是破译这段代码的一种方式，通过枚举每个候选项，对每个候选项分段，并选择概率值最高的那个分段：

def decode_shift2(mg)：

"Decode a message encoded with a shift cipher,with no spaces."

candidates=[segment2(sift(msg,n））for n in range(len(alphabet)）]

p,words=max(candidates)

return''.join(words)

我们一起来看它是如何工作的：

＞＞＞shift2（'Listen,do you want to know a secret?'）

'yvfgraqblbhjnaggbxabjnfrperg'

＞＞＞decode_shift2（'yvfgraqblbhjnaggbxabjnfrperg'）

'listen do you want to know a secret'

＞＞＞decode_shift2(sift2（'Rosebud'））

'rosebud'

＞＞＞decode_shift2(sift2（"Is it safe?"））

'is it safe'

＞＞＞decode_shift2(sift2（"What's the frequency,Kenneth?"））

'whats the frequency kenneth'

＞＞＞msg='General Kenobi:Years agO'you served my father in the Clone

Wars；now he begs you to help him in his struggle against the Empire.'

＞＞＞decode_shift2(sift2(mg)）

'general kenobi years ago you served my father in the clone wars now he

begs you to help him in his struggle against the empire'

这还是太简单了。我们一起来看看通用的密码替换方案，在这种方案中，任何字母都可以被其他字母替换。现在我们无法继续枚举所有的概率，因为总共有26！（约4×10^26）种密钥，而不再仅仅是26种。Simon Singh（编码鼻祖）编写的《The Code Book》提供了五种策略（我们自己在后面增加了第六种）来破解密码：

1.字母一元模型频率。匹配消息中的常用字母到英语中的常用字母（如“e”），匹配不常用字母到不常用字母（如“z”）。

2.双字字母分析。加密消息中的双字字母，在消息解密后还是双字字母。考虑最生僻和最常用的双字字母。

3.找常见的单词如“the”、“and”和“of”。单字母单词通常是“a”或“I”。

4.可能的话，获取一个由你处理的消息类型所组成的词频表。如军事消息使用军事术语等。

5.猜测一个单词或者短语。例如，如果你觉得消息可能包含“your faithful servant”，就大胆猜测它。

6.使用单词模式。例如，加密单词“abbccddedf”很可能表示“bookkeeper”，因为在词典库中没有其他单词满足这种模式。

对于单词间不包含空格的消息，策略3和策略6都不适合。策略1和策略2每种只包含26种概率，而且似乎目标受众是有一定记忆和计算能力的分析人员，而不是一个计算机程序。策略4和策略5是为了某个特殊用途，而不是通用的解码方式。看起来我们只能依赖于自己提出的策略6。但是我们知道应该如何实现该方法。

第一，定义一个概率模型：我们可以采用处理移位加密时所用的方式来评估候选项：对文本分段，计算单词的概率。但是考虑该方法的第二个步骤，刚开始几个（或者几千个）候选项可能是很差的候选。在开始探索时，我们没有任何类似于单词的东西，因此在刚开始就对文本分段的意义不大。然而，我们可能（或者只是碰巧）对一行的几个字母进行解码，生成有意义的结果。因此，我们使用N元文法模型而不是单词来构建语言模型。我们应该采用字母二元文法模型？还是三元文法模型？抑或五元文法模型？我选择字母三元文法模型是因为它是能够表示常用词（策略3）的最短的文法模型。我通过一个单词二元字母数据文件（不能只看词汇文件，因为需要考虑单词边际间的字母三元文法模型），对三元字母（删去了空格和标点符号）进行计数，生成数据文件count_3l。它总共包含26^3=17576个三元字母组合。以下是出现频率最高和最低的10个三元字母组合：

the 2.763%fzq 0.0000004%

ing 1.471%jvq 0.0000004%

and 1.462%jnq 0.0000004%

ion 1.343%zqh 0.0000004%

tio 1.101%jqx 0.0000003%

ent 1.074%jwq 0.0000003%

for 0.884%jqy 0.0000003%

ati 0.852%zqy 0.0000003%

ter 0.728%jzq 0.0000002%

ate 0.672%zgq 0.0000002%

三元字母组合的概率计算如下：

def logP3letters(text)：

"The log-probability of text using a letter 3-gram model."

return sum(log10（P3l(g)）for g in ngrams(text，3））

P3l=Pdist(datafile（'count_3l'））

P2l=Pdist(datafile（'count_2l'））##We'll need it later

第二，枚举候选项：我们无法考虑全部的4×10^26种可能的密钥，而似乎没有什么方法可以像文本分段那样，系统性地消除非最优候选项。这种情况下，需要本地搜索策略，如“爬山算法”(hll climbing)。假设你想要到达最高峰，但是没有地图。通过爬山算法的策略，你可以从随机位置x开始，向相邻位置走一步。如果该位置比之前位置高，继续从那里开始“爬山”；如果不是，则考虑x的另一个相邻位置。当然，如果你从地球上随机一个位置开始步行上山，你可能无法到达珠穆朗玛峰的顶部。更有可能的是，你到达了某个地方的小山头，或者困在一个平原里到处走。因此，我们为爬山算法增加了随机次数的重试。在完成这些步骤后，我们开始从一个新的随机位置从头开始。

以下是通用的爬山算法。它包含起始位置x，我们正试着优化的函数f，生成一个位置的相邻位置的函数neighbors，以及最多采取的步骤。（如果变量调试是真实的，它会输出最佳的位置x及其分值）。

def hillclimb(x,f,neighbors,steps=10000）：

"Search for an x that miximizes f(x)，considering neighbors(x)."

fx=f(x)

neighborhood=iter(neighbors(x)）

for i in range(steps)：

x2=neighborhood.next（）

fx2=f(x2）

if fx2＞=fx：

x,fx=x2，fx2

neighborhood=iter(neighbors(x)）

if debugging:print'hillclimb：'，x,int(fx)

return x

debugging=False

为了使用爬山算法来解码，我们需要指定参数。我们将要搜索的位置是纯文本（解码后的）消息。我们将试着最大化三元字母组词频，因此，设置函数f=logP3letters。从根据随机的密钥解码生成的x位置开始，做随机次数的重试，但是当从每次重试中收集候选项时，我们会根据segment2，选择最佳的一个候选项，而不是根据函数logP3letters：

def decode_subst(msg,steps=4000，restarts=20）：

"Decode a substitution cipher with random restart hillclimbing."

msg=cat(allwords(msg)）

candidates=[hillclimb(encode(msg,key=cat(shuffled(alphabet)）），

logP3letters,neighboring_msgs,steps)

for_in range(restarts)]

p,words=max(segment2(c)for c in candidates)

return''.join(words)

def shuffled(seq)：

"Return a randomly shuffled copy of the input sequence."

seq=list(seq)

random.shuffle(seq)

return seq

cat=''.join

现在，我们需要定义相邻消息函数neighboring_msgs，它生成下一步要尝试的消息的解码。我们首先尝试“修复”不可能的二元字母组。举个例子，最不频繁出现的二元字母组合“jq”的概率是0.0001%，它比最频繁出现的二元字母组合“in”和“th”的概率低5万倍。因此，如果我们在消息中看到“jq”，我们试着把“j”和其他每一个字母进行交换，同样也交换“q”。如果一个交换生成更频繁的二元字母组，那么我们生成由这次交换得到的消息。在遍历20次最不可能出现的二元字母组“修复”后，我们考虑随机交换：

def neighboring_msgs(msg)：

"Generate nearby keys,hopefully better ones."

def swap(a,b）：return msg.translate(string.maketrans(a+b,b+a））

for bigram in heapq.nsmallest（20，set(ngrams(msg，2）），P2l）：

b1，b2=bigram

for c in alphabet：

if b1==b2：

ifP2l(c+c）＞P2l(bigram)：yieldswap(c,b1）

else：

if P2l(c+b2）＞P2l(bigram)：yield swap(c,b1）

if P2l(b1+c）＞P2l(bigram)：yield swap(c,b2）

while True：

yield swap(random.choice(alphabet)，random.choice(alphabet)）

我们一起来看看它的效果如何。我们将从Robert Raynard的书《Secret Code Breaker》(Sith和Daniel；见http：//secretcodebreaker.com)中抽取一些密码来尝试这种策略。首先是一则“热身”的加密消息：

＞＞＞msg='DSDRO XFIJV DIYSB ANQAL TAIMX VBDMB GASSA QRTRT CGGXJ MMTQC IPJSB AQPDR

SDIMS DUAMB CQCMS AQDRS DMRJN SBAGC IYTCY ASBCS MQXKS CICGX RSRCQ ACOGA SJPAS

AQHDI ASBAK GCDIS AWSJN CMDKB AQHAR RCYAE'

＞＞＞decode_subst(msg)

'it is by knowing the frequency which letters usually occur and other distinctive

characteristics of the language that crypt analysts are able to determine the

plain text of a cipher message j'

解码后的消息，除了“crypt analyst”本应该是一个单词，其他都是正确的。（这个词不在我们的词库中，但是它在1300亿的单词库中）。注意最后一个字符（加密文本的“E”）是为了使所有分块都是五个字母一组而增加的。

以下是来自第一次世界大战时德国间谍Baron August Schluga的真正的密文：

＞＞＞msg='NKDIF SERLJ MIBFK FKDLV NQIBR HLCJU KFTFL KSTEN YQNDQ NTTEB TTENM QLJFS

NOSUM MLQTL CTENC QNKRE BTTBR HKLQT ELCBQ QBSFS KLTML SSFAI NLKBR RLUKT LCJUK

FTFLK FKSUC CFRFN KRYXB'

＞＞＞decode_subst(msg)

'english complaining over lack of munitions they regret that the promised support of

the french attack north of arras is not possible on account of munition insufficiency wa

以下是在1992年，美国国家安全委员会(KB)向美国前中央情报局(CA)主席Aldrich Ames发送的密文，Aldrich Ames在1994年被判为间谍：

＞＞＞msg='CNLGV QVELH WTTAI LEHOT WEQVP CEBTQ FJNPP EDMFM LFCYF SQFSP NDHQF OEUTN

PPTPP CTDQN IFSQD TWHTN HHLFJ OLFSD HQFED HEGNQ TWVNQ HTNHH LFJWE BBITS PTHDT

XQQFO EUTYF SLFJE DEFDN IFSQG NLNGN PCTTQ EDOED FGQFI TLXNI'

＞＞＞decode_subst(msg)

'march third week bridge with smile to pass info from you to us and to give

assessment about new dead drop ground to indicate what dead drop will be used next

to give your opinion about caracas meeting in october xab'

以下是在1943年德国的U-Boat命令的密文被截获并且被解码，从而挽救了联合战舰护航队：

msg='WLJIU JYBRK PWFPF IJQSK PWRSS WEPTM MJRBS BJIRA BASPP IHBGP RWMWQ SOPSV PPIMJ

BISUF WIFOT HWBIS WBIQW FBJRB GPILP PXLPM SAJQQ PMJQS RJASW LSBLW GBHMJ

QSWIL PXWOL'

＞＞＞decode_subst(msg)

'a cony ov is headed northeast take up positions fifteen miles apart between point

yd and bu maintain radio silence except for reports of tactical importance x abc'

解码后的消息混淆了“y”和“x”。分析人员会意识到“conyov”应该是单词“convoy”，因此“point yd”应该是“point vd”。我们的程序从未考虑到这种可能性，因为正确文本的三元字母组合概率比这里结果所显示的要低。通过发明一种更好的打分函数，它不会陷于局部最优中，我们可能可以解决这个问题。或者我们可以增加第二层次的爬山搜索：通过第一次搜索生成的候选项，以segment2作为打分函数来简单搜索。我们将把它作为练习留给读者。

拼写纠正

我们的最后一个任务是拼写纠正：给定一个输入的单词w，确定单词c是其最可能要表达的单词。举个例子，如果w是“acomodation”，c应该是“accommodation”。（如果w是“the”，那么c也应该是“the”。）

根据标准的方法，我们想要选择c，使得其条件概率P(c|w）值最大。但是如何定义该概率却不简单。假设w=“thew”，那么c的一个候选项是“the”——它是最常见的单词，我们可以想象是打字员的手指输入“e”后不小心按到“w”。另一个候选项是“thaw”——一个较常见的单词（虽然出现概率比“the”低3万倍），而且用一个原因取代另一个很常见。其他的候选项包括“thew”本身（表示肌肉或者肌腱的模糊术语），“threw”和一种姓氏“Thwe”。我们应该选择哪一种？看起来我们合并了两种因素：c本身的概率是多大？w是c的输入错误，或者是拼读错误，或者是其他某种形式的误拼，它们的概率有多大？有人可能会认为我们将通过某种特殊(a hoc)方式来结合这些因素，但是实际上存在一个数学公式——贝叶斯理论——精确地告诉我们如何组合这些因素来找到最佳的候选项：

argmaxc P(c|w）=argmaxc P(w|c）P(c)

这里，P(c)表示c是期望的结果单词的概率，称为语言模型；P(w|c）表示用户期望获取c会输入w的概率，称为错误模型或者噪音渠道模型。（其想法是用户希望输入c，但是由于一些噪音或静态数据，改成了输入w。）遗憾的是，从我们拥有的语料数据来看，对该模型进行估计并不容易——语料库没有提供信息表示哪些单词是其他单词的拼写错误。

我们可以通过更多的数据来解决这个问题：一个错误拼写列表。Roger Mitton在http：//www.dcs.bbk.ac.uk/～ROGER/corpora.html维护了一个约1万条（c,w）词对的数据语料库。但是我们不能只是从该数据中查找P(w=thew|c=thaw)；若只有4万条例子，那么看到相同的词对的概率很低。当数据很稀疏时，需要对它进行范化。可以通过忽略相同的字母、“th”和“w”来进行范化，计算P(w=e|c=a），即期望输入字母“a”却输入“e”的概率。由于存在如“consistent/consistant”和“inseparable/inseperable”这样的错误，把“a”误拼写为“e”是最常见的一种单词误拼。

在以下表中，我们考虑当w=thew时，c的五个候选项。其中一个是thew本身，另外四个表示我们将要考虑的四种单步编辑类型：1）删除字母“thew”中的“w”，生成“the”。2）插入“r”，得到“threw”。以上两种编辑都是考虑编辑其前一个字母。3）用“a”替换“e”，如之前所述。4）对两个相邻字母交换位置，把“ew”交换成“we”。我们称这些单步编辑为编辑距离1；需要两步编辑的候选项是编辑距离2。下表显示单词w和c，编辑w|c，概率P(w|c），概率P(c)，以及二者概率的乘积（为了可读性进行了扩展）。

从表中可以看出，“the”是最可能的纠正结果。P(c)可以和Pw一起计算。对于P(w|c），我们需要创建一个新的函数Pedit，它给出了一步编辑操作的概率，从拼写错误的语料库中估计出来。举个例子，可以计算Pedit（'ew|e'）=0.000007。更复杂编辑定义成单步编辑的连接。例如，为了把“hallow”转换成“hello”，我们把a|e编辑操作和ow|o编辑操作连接起来，因此整个编辑被称为a|e+ow|o（或者ow|o+a|e，在这种情况下（但不总是如此）是相同的）。复杂编辑的概率等于其各个子部分的乘积。

存在一个问题：空编辑Pedit（''）的概率有多大？也就是说，假设期望的单词是c，人们真的输入c而不是其中某种错误的编辑？这依赖于打字员的熟练程度以及是否有检查。很随意地，我假定每20个单词出现一次拼写错误。值得注意的是，如果我假定每50个单词才出现一次错误，那么对于w=“thew”，P(w|c）的概率为0.98.，而不是0.95，而且“thew”将是最可能的结果。

最后，我们可以展示代码了。该代码包含两个高层函数correct和corrections,correct函数返回一个单词的最佳纠正，corrections函数把correct应用到文本中的每个单词，对其邻近字符不做改动。候选项是所有可能的编辑，而最佳候选项是有最高的P(w|c）P(c)分值的候选项。

def corrections(text)：

"Spell-correct all words in text."

return re.sub（'[a-zA-Z]+'，lambda m:correct(m.group（0）），text)

def correct(w)：

"Return the word that is the most likely spell correction of w."candidates=edits(w).items（）

c,edit=max(candidates,key=lambda(c,e）：Pedit(e)*Pw(c)）

return c

P(w|c）是通过函数Pedit来计算的：

def Pedit(edit)：

"The probability of an edit；can be''or'a|b'or'a|b+c|d'."

if edit==''：return（1.-p_spell_error)

return p_spell_error*product(P1edit(e)for e in edit.split（'+'））

p_spell_error=1./20.

P1edit=Pdist(datafile（'count1edit'））##Probabilities of single edits

候选项是通过编辑生成的，即传递一个单词，返回一个{单词：编辑}对的词典dict表示可能的纠正。通常来说，可能对于一个纠正存在多种编辑。（例如，我们通过在“e”或者“l”后面插入“l”后，都可以把“tel”转为“tell”。）我们选择有最高概率的编辑。edits是我们目前为止最复杂的函数。一部分原因是该函数本质上就很复杂，生成4种编辑很复杂。但是一部分是因为我们采取了一些措施使得该函数变得高效。（性能较低但是易于读取的一个版本在http：//norvig.com/spell-correct.html。）如果我们考虑所有的编辑，一个单词如“accommodations”可以生成233166个候选项。但是只有11种候选项可以在词汇库中找到。因此，编辑是通过预计算词汇库中所有单词的所有前缀才能工作。然后，它递归调用edits，把单词分成头部和尾部（代码中的hd和tl)，并保证头部总是在前缀列表中。通过把结果添加到词典库dict中进行收集：

def edits(word,d=2）：

"Return a dict of{correct:edit}pairs within d edits of word."

results={}

def editsR(hd,tl,d,edits)：

def ed(L,R）：return edits+[R+'|'+L]

C=hd+tl

if C in Pw：

e='+'.join(edits)

if C not in results:results[C]=e

else:results[C]=max(results[C]，e,key=Pedit)

if d＜=0：return

extensions=[hd+c for c in alphabet if hd+c in PREFIXES]

p=(h[-1]if hd else'＜'）##previous character

##Insertion

for h in extensions：

editsR(h,tl,d-1，ed(p+h[-1]，p））

if not tl:return

##Deletion

editsR(hd,tl[1：]，d-1，ed(p,p+tl[0]））

for h in extensions：

if h[-1]==tl[0]：##Match

editsR(h,tl[1：]，d,edits)

else：##Replacement

editsR(h,tl[1：]，d-1，ed(h[-1]，tl[0]））

##Transpose

if len(tl)＞=2 and tl[0]！=tl[1]and hd+tl[1]in PREFIXES：

editsR(hd+tl[1]，tl[0]+tl[2：]，d-1，

ed(tl[1]+tl[0]，tl[0：2]））

##Body of edits：

editsR（''，word,d，[]）

return results

PREFIXES=set(w[：i]for w in Pw for i in range(len(w)+1））

以下是使用edits函数的一个例子：

＞＞＞edits（'adiabatic'，2）

{'adiabatic'：''，'diabetic'：'＜a|＜+a|e'，'diabatic'：'＜a|＜'}

以下是正在工作的拼写纠正器：

＞＞＞correct（'vokabulary'）

'vocabulary'

＞＞＞correct（'embracable'）

'embraceable'

＞＞＞corrections（'thiss is a teyst of acommodations for korrections

of mispellings of particuler wurds.'）

'this is a test of acommodations for corrections of mispellings

of particular words.'

15个单词中有13个处理结果正确，但是“acommodations”和“mispellings”处理结果错误。为什么呢？遗憾的是，网络上充满了很多错误的拼写。错误的单词“mispellings”在语料库中出现了18543次，而正确的单词“misspellings”只出现了50万次，这其中的差距不足以带来用一次编辑替换无编辑。“thew”出现了96759次，我怀疑其中绝大多数也是由于拼写错误。

有很多种方式可以改进这个拼写程序。首先，我们可以通过上下文的其他单词来纠正这个单词，因此“they're”本身是正确的，但是当出现在“in they're words”时，就会被纠正为“their”。一个单词二元模型或者三元模型就可以实现它。

我们真的需要清洗语料库中的拼写错误。看看以下这些不同的“misspellings”：

misspellings 432354

mispellings 18543

misspelings 10148

mispelings 3937

语料库中使用的misspellings一词，拼写错误的概率达到了7%。可以有三种方式来解决这个问题。第一种，我们可以获得一个字典单词列表，并且只对字典中的单词纠正错误。但是字典没有列出最新发明的单词和名字。（一种折中方案是把单词变成小写放到字典词汇中，但是允许全部大写的单词不在字典库中。）第二种，我们可以获得一个已经仔细校验过的语料库，可能是来自于高品质的出版社的书籍和期刊。第三种，我们可以纠正语料库中的错误。在使用语料库进行拼写检查之前，先对语料库拼写检查，这看起来就像是“循环论证”(crcular reasoning)，但是可以实现。对于这个应用，我们把编辑距离很小的单词聚在一起。针对每组相似单词，我们检查是否有某个单词的出现频率要远远高于其他单词。如果存在这个单词，我们将检查二元模型（或者三元模型）计数来观察这两个单词和相邻的单词是否有相似的分布。例如，以下是“mispellings”和“misspellings”的四个二元文法模型计数：

mispellings allowed 99

misspellings allowed 2410

mispellings as 50

misspellings as 749

mispellings for 122

misspellings for 11600

mispellings of 7360

misspellings of 16943

这两个单词共享很多相同的二元文法相邻的单词，对于这些单词组合中，总是“misspellings”的出现频率更高，因此很有可能“mispellings”是拼写错误。初级测试表示这种方法效果不错——但是存在一个问题：它需要CPU计算好几百个小时。因此，这种方法适合于在一个工作组上执行计算，而不适合于单机。

这种数据驱动的方法与更传统的软件开发过程相比如何？对于后者，程序员在编码中制定了一些显示规则。为了回答这个问题，我们一起来看ht：//Dig项目的拼写纠正代码，Dig是一款优秀的开源企业内网搜索引擎。给定一个单词，ht：//Dig项目的metaphone算法可以生成一个关键字来表示这个词的发声。举个例子，“tough”和“tuff”都映射到关键字“TF”，因此可以作为互相拼写错误的候选。以下是对于字母“G”的metaphone算法的代码：

case'G'：

/*

*F if in-GH and not B--GH,D--GH，

*-H--GH，-H---GH else dropped if

*-GNED，-GN，-DGE-，-DGI-，-DGY-

*else J if in-GE-，-GI-，-GY-and

*not GG else K

*/

if（（*（n+1）！='G'||vowel（*（n+2）））＆＆

（*（n+1）！='N'||（*（n+1）＆＆

（*（n+2）！='E'||

*（n+3）！='D'）））＆＆

（*（n-1）！='D'||！frontv（*（n+1））））

if(frontv（*（n+1））＆＆*（n+2）！='G'）

key＜＜'J'；

else

key＜＜'K'；

else if（*（n+1）=='H'＆＆！noghf（*（n-3））＆＆

*（n-4）！='H'）

key＜＜'F'；

break；

这块代码正确地把“TOUGH”中的“GH”映射为“F”，但是不会把“BOUGH”中的“GH”映射为“F”。但是这些规则是否正确地捕获了所有的情况？“OUGHT”呢？或者“COUGH”和“HICCOUGH”？我们可以编写测试样例来验证代码的每个分支，但是即使如此，我们还是无法知道哪些方面没有覆盖全。当引入新的单词如“iPhone”，结果会如何呢？显然，手工制定的规则很难扩展和维护。数据驱动方法的很大优势是在数据中包含了很多信息，可以简单地通过增加更多的数据来增加新的知识。另一个优点是，虽然数据是海量的，但代码却很简洁——函数correct只包含50行左右的代码，而ht：//Dig的拼写代码超过1500行。正如伟大的Bill Gate所言：“通过代码行数来衡量编程进展正如通过重量来衡量飞机建造过程。”Gates认为代码行数是一种负担，而不是财富。概率性的数据驱动方法是敏捷开发的终极。

该算法的另一个问题是可移植性。如果我们想要一个拉脱维亚语(Ltvian)的拼写纠正器，该英语metaphone规则几乎毫无用处。把数据驱动的纠正算法移植到另一种语言上，我们需要的是一个大的拉脱维亚语的语料库，而不需要改动代码。

其他任务

以下是通过概率性语言模型处理的一些其他任务。

语言识别

存在Web协议来声明网页的编码语言。实际上至少存在两种协议，一种在HTML，另一种在HTTP。但是有时这两种协议不一致，它们都不表示真实的信息，因此搜索引擎在搜集了一些已知语言的样本页面后，通常通过基于实际页面内容对页面进行分类。任务是写一个这样的分类器。该技术的前沿水平是语言识别的准确率超过99%。

垃圾邮件检测和其他分类任务

据估计，每天发送的垃圾邮件有1000亿封。给定垃圾邮件和非垃圾邮件两个语料库，任务是对未来的消息进行分类。最佳的垃圾邮件分类器包含N元单词模型（包含“10000000.00 will be released”和“our country Nigeria”的消息很可能是垃圾邮件）以及N元字符模型（“v1agra”可能是垃圾邮件）等特征。该任务的前沿技术是通过垃圾邮件拦截方式，其准确率超过99%。一旦你可以对文档分类为垃圾邮件/非垃圾邮件，那么你就可以很容易地进行其他方式的分类，比如紧迫/非紧迫邮件消息，或者对新闻文章的分类，如政治/商业/体育/其他，或者产品反馈的分类如“喜欢/一般/不喜欢”。

作者识别（文体学Stylometry)

语言模型已经被用于识别《Federalist Papers》、莎士比亚的诗和《圣经》的有争议的作者。相似的技术还被用于追踪恐怖组织，在刑法中，用于识别和找到罪犯。该领域还不太成熟，我们还不太确定什么是最佳实践，或者期望概率是多少，虽然2004年竞赛的胜出者的准确率达到71%。竞赛的最佳实践在语言上很简单，但是在统计上非常复杂。

记录文档重组和DNA序列化

在Vernor Vinge的科幻小说《Rainbows End》(Tr Books出版），Librareome项目通过把所有书籍扔到树碎纸机、对碎片拍照和使用计算机算法重新组合这些图片的方式数字记录整个图书馆的信息。在实际生活中，德国政府的E-Puzzler项目正在重新构建4500万页面的记录文档，它们被民主德国秘密警察Stasis所破坏。这两个项目都依赖复杂的计算机视觉技术。但是一旦把图像转化为字符，即可使用语言模型和爬山算法来重新组合那些碎片。可以采用类似的技术来解读生命之语：人类基于组项目使用了称为shotgun序列化技术来重新组合DNA碎片。因此，所谓的“下一代序列化”把更多的负担从Web实验室转移到大规模的并行重组算法。

机器翻译

G公司的N元语料是由机器翻译组的研究人员创建的。把外语(f)翻译成英语(e)和纠正拼写错误的单词很相似。最佳的英语翻译建模如下：

best=argmaxe P(e|f）=argmaxe P(f|e）P(e)

其中，P(e)是英语的语言模型，通过N元语法模型数据进行估计，而P(f|e）是翻译模型，通过双语语料库进行学习：在双语语料库中，一对文档被标记为相互的翻译。虽然高端系统利用了语言学特征，包括很多句子的一部分词和语义解析，但是结果发现翻译需要的绝大部分知识来自于n元文法模型数据。

讨论和结论

我们显示了软件开发方法的力量，它使用大量的数据来解决不确定环境中提出的不确定问题。本章中探讨的是语言数据，但是很多相同的经验也适合于其他数据。

在我们所探索的样例中，程序简洁明了，这是因为使用的概率模型简单。这些样例模型忽略了人类所知道的很多知识——显然，对“choosespain.com”进行分段，我们需要了解很多关于旅游业如何工作以及其他因素的专业知识，但是奇怪的结果在于程序不需要显式表示所有的知识；它隐式地从N元语法模型中获取知识，它表示其他人选择了探讨的方面。在过去，概率模型更复杂，因为它们依赖于更少的数据。

需要强调的是，当缺失数据时的统计复杂的平滑策略。既然有非常大的语料库，那么我们可以使用类似“愚蠢回退”的方法，而不再过于担心平滑模型。

我们在本章研究的程序中的绝大多数复杂性是由于搜索策略。我们看到了三类搜索策略：

穷举式(Ehaustive)

对于移位密码，只有26个候选项，我们可以全部一一测试它们。

保证式(Garanteed)

对于分段策略，存在2n种候选方式，但是很多方式可以被证明是非最优的（在独立假设前提下），不需要对它们进行检查。

启发式(Huristic)

对于全部的替换密码，我们无法保证已经发现了最佳的候选，但是我们可以搜索一个代表子集，使我们可以有较大的机会找到最佳候选。对于很多问题，其绝大部分工作是选择正确的函数进行优化，理解搜索空间的拓扑，发现好的顺序来枚举相邻节点。

如果我们是基于大量的数据构建模型，那么就需要获取“在各种情况下”(i the wild)可能的数据。N元文法计数具备这个属性：我们可以很容易地从Web中收集1万亿条自然文本数据。然而，标签化的拼写纠正不是自然产生的，因此我们在这些数据中只找到了4万个。两种最成功的自然语言应用——机器翻译和语言识别——可以得到大量的语料样例，这绝非偶然。相反，句子的语义解析任务依然大部分还未实现，一部分是由于在解析后的句子中不存在自然存在的大量的语料库。

值得一提的是，我们的概率性数据驱动模型方法——最大化了所有候选项的概率——是合理的数据驱动方法的一个特例——最大化所有候选项的期望效益。举个例子，合理性的拼写纠正程序需要知道存在一些禁忌语，而当用户并不想使用这些禁忌语时，提示这些禁忌语会给用户带来难堪，其负面影响比拼写一个错误单词要严重得多。合理性的程序会考虑一个单词正确或错误的概率，以及提示每个单词所带来的正面或负面价值。不确定问题需要在校验和测试上有很好的纪律约束。为了评估一个不确定问题的解决方案，人们需要把数据分为三个集合：1）训练集合，用于构建概率模型。2）校验集合，开发人员用于评估一些不同的方法，观察哪一种方法打分更高，从新的算法中获取思想。3）测试集合，用于在开发介素时准确地判断该算法对于新的、未访问的数据效果如何。当使用完一次测试集合后，理想情况是抛弃该集合，正如一个老师不能给一个班级的学生进行两次相同的测试。但是在实践中，数据很昂贵，而且在通过支付方式获取新数据和通过某种不会导致模型过度拟合数据的方式重用旧数据之间存在一个折中。注意对独立测试集合的需要对于未知问题本身是固有的，而不是和选择的解决方案类型有关——即使你决定通过特定规则(a hoc rule)而不是概率模型的方式来解决问题，你都应该使用合适的测试方法。

总之，由于网络上可以获取更多的数据，而且随着计算能力的提高，我相信概率性数据驱动方法将会成为解决未知领域的复杂问题的主要策略。

致谢

感谢Darius Bacon、Thorsten Brants、Andy Golding、Mark Paskin、Franco Salvetti和Casey Whitelaw提供评论、纠正和代码相关的建议。

第15章　数据中的生命：DNA漫谈

MattWood和Ben Blackburne

DNA不仅仅是组成生物体的积木，它还是生物体化学组成的数据库。这个数据库由数百万年来芸芸众生的演化所设计实现，它简单、无模式、可容错。在过去的20年中，随着基因手段在现代生物医学研究中所占的比重增大，生物学家的研究重点也开始从单个基因转移到整个基因组上。最近几年来，生物学家一直在研究如何在DNA上读写数据。本书将会讲述与DNA有关的两个话题。其一，DNA本身也是一种对数据进行编码、数字化存储的方式，它的出现要比硬盘早得多。其二是一个与之相关的故事，测定DNA中的数据并确定其含义的浩大工程。

用DNA存储数据

基因组是生命体的数据库。它由DNA分子写成，其副本存储在人体的每一个细胞中（只有少数例外）。这种模式在自然界中普遍存在，即使是最简单的生命形式也不例外。基因组中编码的信息包含制造蛋白质的指令，而这些蛋白质组成的分子机器则运行着细胞内的化学系统。这就是为什么我认为DNA是可容错、冗余的存储形式。

你身体里的几乎每一个细胞中都有一个数据中心，数据中心里存储着这些基因数据库。我们称这个数据中心为细胞核，细胞核中又含有染色体。所有的人都是双倍体，你也不例外。你的每一个染色体都由两个副本构成，分别来自于你的父亲和你的母亲。除了普通的染色体之外，人类还有性染色体。女性有两条X染色体，男性有一条X和一条Y染色体。这些基因数据存储库的基本存储单元是DNA双链，两条链通过神奇的双螺旋结构缠绕在一起，如图15-1所示。

图　15-1：DNA中的一个小片段，由POV-Ray软件从PDB文件1BNA生成，doi：10.2210/pdb1bna/pdb（见彩图51）

DNA的每条链都是由一串碱基构成的。DNA中含有四种碱基——腺嘌呤、鸟嘌呤、胞嘧啶、胸腺嘧啶（或者分别缩写为A、G、C、T）。基因数据库正是编码存于这个四元体系中。在人体中，DNA含有3G（3×10^9）个碱基，分布在两个稍有不同的副本中。

DNA生RNA,RNA生蛋白质

DNA到底是用来做什么的？人类基因组中的大部分似乎并没有直接的作用，尽管它们可能有一些潜在用途。基因组中的2%构成基因，基因的序列决定了蛋白质的构造。DNA是由四进制编码写成的大分子，同样，蛋白质是由二十进制编码写成的稍小的分子。组成蛋白质的序列编码确定了蛋白质的形状，而蛋白质的形状和化学结构又决定了其在细胞的分子机器中担当哪一个齿轮的角色。

和其他文件格式一样，读取和显示DNA序列中所编码的信息也需要特殊的读取装置。和其他种类的文件不一样的是，这个装置是在纳米尺度下在活细胞中工作的，它根据基因中碱基的排列，按照一定的顺序查找各个分子。RNA是DNA的短命表兄，它有一个类似四进制代码的系统。当一个基因被激活时，蛋白质分子机器系统会在RNA中生成该基因的副本。这些RNA副本被转移到蛋白质机器中翻译成蛋白质，每个RNA分子都能用作许多蛋白质分子的模板。

如此看来，DNA和蛋白质形成了一种伙伴关系：蛋白质提供机器装置，在基因中的碱基序列指导下制造新的蛋白质。虽然DNA被认为是“生命的蓝图”，但它也只能在细胞中分子（尤其是蛋白质）已经存在的情况下发挥作用。

通过控制负责激活基因的蛋白质的产生，细胞可以用产生蛋白质的方式对刺激物或者其他需要作出反应。通过这些机制，其他细胞可以通过发送信号分子（比如激素）来控制某个细胞。许多药物也通过这些机制产生作用。

每一个基因都由其周围控制蛋白质产生的序列做了标记（见图15-2）。基因组中编码的一些蛋白质可以选择性地用于这些标记。一些蛋白质会促进基因的表达，而另外一些则会阻止该基因的表达。

图　15-2：图示为一个DNA片段，其中包含一个基因，以及基因周围用来和细胞调控机制相互作用来限制基因表达的区域。这里基因由一个启动子和一个增强子引导，这样基因就被做了标记，使得细胞知道何时应当表达该基因（见彩图52）

使用药物Hack你的DNA数据

让我们来幻想一下：O'Reilly要在“Hacks”系列图书中增加一本《DNA Hacks》——其中包含100种方式来修改、重构或者增强你的基因数据库。那么这些Hack方式的一半差不多都通过阻断或者激励某一类蛋白质的活动来进行，这类蛋白质被称作G蛋白偶联受体（G-protein coupled receptors,GPCR)。例如，胃酸分泌由体内信号作用于一个特殊的GPCR来激发。当信号分子触及GPCR时，将会发生一系列的相互作用，以至会“打开”或“关闭”你的DNA中的基因。例如用来治疗胃溃疡和胃痛的雷尼替丁，通过在信号和GPCR之间横插一脚来阻断信号，从而发挥作用。和在Email客户端中使用智能的hack来过滤垃圾邮件一样（比如过滤包含“V1Agr4”的消息），雷尼替丁就是一个智能的hack，人们通过它来阻止传递“请分泌更多胃酸”信息的信号穿过细胞屏障。

还有一种形式的DNA元数据(mtadata)值得一提。尽管基因组通常情况下被看做一个只读数据库，但是DNA可以因为环境因素被化学修改，导致某些基因被抑制。这些化学变化并不改变基因自身的编码，而是通过提供附加的注释使得细胞机器不再容易转录该基因。研究这些化学修改的学科叫做表观遗传学。对于这些修改在身体组成中所扮演角色的研究现在仍旧处于初级阶段。不过目前已知的是，一些表观遗传学变化会关掉在某个特定细胞中不需要的基因（肌肉细胞需要的基因和肝脏细胞并不相同）。基因可以因为环境因素而被关掉——这似乎毫无疑问地会在幻想中的《DNA Hacks》一书的未来版本中出现，这确实让人激动不已。例如，生长素基因可以因为饥饿而被关掉。更有趣的是，这些变化可能会遗传给孩子，换句话说，我们的生活方式产生的变化可以影响到下一代的基因，这在以前被认为是天方夜谭。

癌症

癌症是一种由于细胞增生失控而产生的疾病。一个细胞的生长和位置是由它的基因控制的，也就是说，它只会以可控的方式分裂。除此之外，人类基因组中包含了很多肿瘤抑制基因，它们可以在细胞不受控增生的时候使细胞消亡。那么癌症是怎么发生的呢？答案就在数据库损坏中。化学致癌物（比如香烟中的）、辐射和病毒都能够使你的DNA产生变化。

当计算机硬盘碰到小损坏的时候，你可能注意不到有什么不同。不过当一定数量的损坏不断积累，程序就会开始崩溃，文件就打不开了。基因组同样如此。损坏足够多之后，基因的控制会产生紊乱，导致不正常表达的发生。如果你能触及正确的基因，那么细胞会开始向它自己发送生长和分裂的信号。其他的基因（比如肿瘤抑制基因）可以被完全关闭，一旦如此，你便在一步步走向癌症。

当然，硬盘在大多数情况下是可靠的。大多数硬盘中都有专门的系统来检测和重映射坏道。硬盘中也包含了检测磁盘损伤并通知用户的系统（比如SMART监控）。为了获得额外的可靠性，你可以通过RAID1阵列来将两个硬盘相互镜像。这样，两个硬盘的内容是相同的，如果一个硬盘出现错误，另一个硬盘便可以替代它并恢复数据。细胞自身也有检测和修复DNA损伤的机制。让我们来回忆一下，DNA分子以双螺旋的形式存在，一条链上的碱基与另一条链上的对应碱基互补配对。如果一条链发生损伤，它可以通过另一条链作为模板进行修复（类似硬盘的RAID1方式）。对于更大损伤的修复，还可以采用其他机制。

如果损伤不能修复，肿瘤凋亡基因就会启动，阻止细胞分裂并最终导致细胞程序性死亡。在理想的情况下，DNA受到损伤的细胞可以得到修复，类似硬盘的SMART机制。涉足过计算机集群（比方说5000个硬盘的规模）的人都会知道硬盘故障是多么的频发。想想人体内有100万亿个细胞，他们大概会奇怪为什么癌症的发病率会比较低。

复制

前文中已经说到，DNA是由缠绕在一起的两条链构成的。两条链是互补的：DNA的四种碱基都有对应的互补碱基（A和G互补，C和T互补），互补碱基出现在互补链的相同位置。沃森和克里克[1]在他们揭示DNA结构的论文中有一段著名的论述：“我们注意到我们的碱基互补配对假说直接给出了一种可行的基因物质复制机制。”

每一个DNA链都可以用作另一个链的模板，用以复制DNA双螺旋。细胞可以分离两条链，将他们分别作为模板，用互补碱基制造新链。在细胞分裂为两个新细胞时，上述过程就会发生（见图15-3）。

图　15-3　使用两个原始DNA链（白色）作为模板制造两个新链（黑色）的DNA复制过程，产生了两个新的双螺旋分子（照片来自http：//genome.gov/glossary.cfm)

破解基因编码

假设未来文明出土了一块21世纪的硬盘。即使未来人能够理解文件系统（如前所述），文本文件在硬盘上最终还是以二进制形式储存的。如果没有将7bit数据转换为一个字母的7bit ASCII码表，信息就会无法解读。

1961年，克里克和布伦纳[2]开始了基因中类似的编码的逆向工程。ASCII将二进制转换为90个左右的字符，每个字符必需7个比特，即2^7=128（实际上还有一位用于奇偶校验）。在DNA中，四进制的碱基代码需要编码21个字母（20个氨基酸和1个终止信号），那么每个字符就需要3个碱基（4^3=64）（见表15-1）。

举例来说，三个碱基ACT编码了苏氨酸。当翻译机器在基因中遇到ACT三联密码子时，就会向制造中的蛋白质分子插入一个苏氨酸。我们可以注意到，密码子的数量（64）比信号（20个氨基酸和一个终止信号）要多。这意味着三联密码子有内建的冗余机制——大多数的氨基酸由不止一个密码子编码。

实际上，对大多数密码子来说，最后一个碱基对编码没有影响或者影响很小。这使得碱基突变（碱基的变化，比如A变为T）对蛋白质造成影响的可能性变得尽可能低，也就保护了蛋白质结构免受可能的碱基损伤影响。

人类基因组在有意外错误并入的情况下已经成长了几十亿年。因此，基因组中充满了没有作用功能的元素以及那些作用功能不明确的元素。虽然“垃圾DNA”这个说法现在已经不再流行了，但是不可否认的是，DNA中大量信息的存在仅仅是因为将它们移出基因组的选择性压力太小了或者根本不存在。

有些元素可以自行在基因组中复制、粘贴。例如，Alu元素，一个大约有300个碱基的碎片，其中包含的信息可以它自己复制为RNA（一种寿命短一些的类DNA分子），然后在其他位置重新复制进入基因组。这样的结果就好像让库布里克(Sanley Kubrick)的《The Shining》中的人物杰克和你一起写本书，书中写着“只工作，不玩耍，聪明杰克也变傻”[3]。Alu元素占到了人类基因组的10%。简单生命体基因组的大小限制了这些元素的增殖，人类基因组中这些元素的存在也就使得人类基因组更为深奥难懂了。

DNA作为数字化存储

进化的力量塑造了基因组。但在达尔文提出进化论的时候，他知道自己碰到了一个问题。常识告诉我们，高个子和矮个子生出的孩子身高会一般，那么“高”和“矮”的遗传信息会不可逆地混合在一起。有性繁殖会让一个物种达到平均水平，但事实上，正如和达尔文同一时代的孟德尔[4]所发现的，基因并没有不可逆地混合在一起。

原因在于DNA的数字化本质。中等个头的子代会有来自父亲和母亲各一套基因拷贝。基因并没有混合在一起，举例来说，序列中没有任何一点有半A半T的状态。所以信息并没有丢失，并且可以原样传递给孙代。复杂的生命得以进化并存在，这就是由DNA所实现的数字化储存。

进化作为算法

我们知道创造细胞中DNA的基本算法是：进化。DNA序列因变异而变化。那么进化是如何塑造DNA？令人吃惊的是，进化（以两代间DNA序列变化定义）的大部分是选择中性的，即进化对生命体没有任何影响。为什么是这样？很明显，在小种群中这种变化的发生有机会以后在种群中占支配地位。对大一些的种群来说，这种变化占支配地位的可能性较小，但是这种变化的数量会相应增加——所以最终中性进化仍然占优势。

这对解释个体间与物种间的差异很重要。中性进化是两个基因组之间任意不同的空模型(nll model)。这意味着，为了证明两个物种的相同基因中存在的差异是有意义的，科学家们一直想证明中性进化的“算法”无法生成两个序列。而这需要估算变化的比率，并确定其是否会快于或慢于中性理论所预计的速度。如果产生了多于预计的变化，则可能是由于这些变化已经为组织提供了功能上的改进，并且这些变化已在种群中迅速蔓延。如果变化比所预计的要慢，那么基因可能足够重要，使得绝大部分变化不再是中性的，然后被进化所拒绝。

人类参考基因组[5]目前被看做表示不同人混合的线性字符串，物种内的巨量差异意味着人类参考基因组可以更适当的被看做包含了种群内不同的图(Gaph)。一个个体的基因组可以被表示为参考图的一个遍历。

[1]即詹姆斯·杜威·沃森(Jmes D.Watson)与佛朗西斯·克里克(Fancis Crick)，此二人因发现DNA分子双螺旋结构于1962年获得诺贝尔生理学或医学奖。

[2]悉尼·布伦纳(Sdney Brenner)，南非生物学家，2002年诺贝尔生理学或医学奖获得者。

[3]“All work and no play makes Jack a dull boy.”是《The Shining》中的原文。

[4]格里哥·孟德尔(Gegor Mendel)，奥地利遗传学家，遗传学的奠基人。他通过豌豆杂交实验，提出了遗传因子（现称基因）及显性性状、隐性性状等重要概念，并阐明其遗传规律，后人称之为孟德尔定律。

[5]Reference Human Genome，即人类基因组计划所测序的基因组。

DNA作为数据源

用编程语言来说，DNA就是一个字符串：

char（3*10^6）human_genome；

人类的完整基因信息由30亿字符组成，即使在最没效率的自制编程语言中也可以很轻松地处理。但是确定这30亿个碱基的确切顺序需要化学、生物信息学和实验室过程的通力合作，当然还需要很多旋转的磁盘。

人类基因组计划第一次将目标设定为对所有这些字符进行测序。全世界很多大型、高产的研究所都将学术竞争放到一边，着手这项可能持续13年花费数十亿美元的任务。他们的目标是制作出一个健壮、精确的人类基因组图谱，并向所有人免费发布。英国、美国和日本的科学家联合组织已于2001年2月在科学文献上成功发表了第一个人类基因组草图。如果不算附加注解和相关数据，基因组本身有10Gb的数据量，这在没有iPod或者U盘的时代是相当大的。然而，随着数据在世界各地的分发拷贝，对储存空间的要求以指数形式增长，因此整体的数据要大得多。科学家着手分析数据，将其用作基因标记与疾病指标的来源，并将其与其他来自老鼠、酵母菌、病菌的基因组对比。这10G数据已经构成了现代生物研究的基础。

现在将时间快进到2008年。人类基因组已经得到了很好的注释(htp：//ensembl.org)，包括老鼠、黑猩猩、鸭嘴兽在内的其他四十多个物种的基因组也被测序了。在威康信托基因园区[1]中摩根楼一角的会议室中Sanger研究所的IT专家、信息学家和科学家正在一起开会讨论DNA测序的下个时代。

若干互相竞争的测序技术平台已经给出了数字，并都期待着去探索基因信息的非富余本质。与已经被最初的人类基因测序测出的碱基对里的长读序列不同，这些短读序列将只有30～50个碱基，而前者则数以千计。但这30～50个碱基足以在特定的染色体上的特定位置进行读取，而这在多达30亿字符中有很高的确定性。数以百万计的碱基迅速使得在毛细血管状的基因序列中计算单个Kb变得逊色不堪，因为短读序列的数据要求的细节已经十分明了。白板上填满了手写的图解过程，那些过程正是运用了荧光碱基的图像，用来确定这些DNA短片段的正确序列。

人们已经讨论过了要支持研究所的科研目标所需的仪器通量指标及仪器数量。白板上的一个数字与其他数字相比，更加引起了人们的注意。在以后六个月中，Sanger研究所将会测序出50Tb的最新数据，是每星期50Tb。

会议上一片寂静。

即使我们有能力支持大数量序列的人类基因测序仪器（100+），同时也拥有可供分析和注解数据的专用的现成硬件，但是整个生物学界也从未处理过具有如此原始信息的数据。

新的测序平台将会大大地超越Sanger研究所现有的大规模数据需求。也就是说，数据需求已经在Sanger研究所15年的历史中飞速呈指数增长，最终达到了由17000个在线硬盘驱动的PB级储存系统。在未来12～18个月内，新的测序技术会产生相当于这15年总和的数据。

果不其然，测序仪器在基因园区里掀起了新一波的浪潮，6个月后，那场会议的寂静被工业史上最大规模信息设施打破了，有服务器农场、存储阵列、软件、数据库和信息管理工具。

量的飞跃

新技术在基因测序中产生了量的飞跃。上一代的仪器需要大量设备来产生1Mb的可用序列。测序一个单一的基因组就要花费好多年并且费用昂贵。

测序一个基因组在比对不同物种的基因组时是很有用的。DNA序列中有生物学意义的部分在进化历史中保持不变，而且找出这些不变的区域可以帮助生物学家识别新基因，新基因则可能在疾病中扮演某种角色。

由于单个Kb的测序很少是完全精确的，在统计学效能上，对同一碱基进行多个相互独立的测序是有切实好处的，不过这样就需要大规模的部署仪器。不过新一代的短读测序仪器每次运行都会读取同一碱基几百万次，可以为结果基因组提供额外的解析度，这对比较基因组很重要。

有了这个额外的解析度，短读测序方法第一次使得构建大量个人基因组成为可能，从而提供了个人基因组数据库的洞察力。将这些基因组进行比较，可以帮助识别出为什么有些人比别人有更大的风险患高血压或者乳腺癌。基因序列中很少一些碱基的差异就能改变这些易患病基因体质；从一个单独的碱基(sngle nucleotide polymorphisms,SNP)到重复单元集合(cllections of repeating units,CNV)，30亿碱基中的这些小变化正是很多疾病状态的关键。通过比对短读技术获取的几百万份拷贝DNA字符串中某些特定位置的碱基，就可以肯定两个体碱基差异是测序错误引起还是真的有微小的核酸变化。通过病例和其他的研究进行额外的注解就可以知道这些变化是不是和疾病有关。

全是碱基

这种基因测序平台的技术实现值得讨论一下。从本质上来说它是线性组装流水线，其中每个DNA都是原材料，计量后在测序仪器中准备参与光化学反应。将DNA剪切为数以百万计的短片段后，再将其复制几百万次，然后附着在一种特殊制备的玻璃片上。带有特殊标记的碱基会堆积在这些制备好的短片短上，并配对在互补碱基上：A对T,C对G。用激光照射时这些碱基会发出不同波长的荧光，然后就可以拍到含有几千个点的图像，每一个点都是一簇发光的DNA碱基（见图15-4）。此时对测序平台的要求就以乘法方式增长了。每张图像都只是百万像素的解析度，采用短读测序时每个碱基要拍四张图像（每个位置的每个碱基一张）。这也仅仅是148张图像。

不过，每张图像都只只能覆盖DNA簇的一部分。DNA会分成330个单独的块进行拍照（48840张图像），然后是8条线（390720张图像）。通常情况下，DNA链是由正向和逆向双向测序（781440张图像）。同时还有测序运行是由激光强度和射流测量产生的元数据。

一台仪器每天可以总计可以产生1Tb数据。

这些数据传给图像分析软件，软件将基于荧光强度通过校准每一系列的图像确定碱基顺序。这样又生成的文件含有各个字符串，质量评分和荧光强度详细信息，同时附带了簇位置和其他相关的元数据。实际上，为了方便下游的分析，这些信息是写成两种文件格式的：fastq和SRF，一种被全世界同事采用的数据格式。

图　15-4：Illumina[2]GA2测序图像，其中每一个点都是由数千个DNA分子组成的簇，在激光下发出荧光

图像分辨率与读取长度的简单增加对数据处理和分析有着巨大的影响。Sanger研究所新测序平台的最初版本可以进行每次36个碱基的短读。100个碱基对的读取长度（比如Sanger研究所研发实验室里运行着的原型机）就会导致图像数目变成三倍（2334320张图像）。在拥有40台仪器的一个平台中（你读到本文的时候可能已经不止这么多了），每个平台的每台仪器总共会产生9300万张图像，每次运行大约持续一星期，总计每周会产生75Tb的数据。

数据是怎么处理的？

[1]Wellcome Trust Genome Campus，位于剑桥南部的Hinxton，是Sanger研究所和欧洲生物信息学研究所的所在地。

[2]Illumina是一家提供DNA测序仪器解决方案的公司。

搏击数据洪流

大规模的部署测序仪器对很多方面是不可或缺的，比如基因数据集建设，支持诸如千人基因组计划[1]和国际癌症基因组协会[2]这样的大规模、全基因组关联研究，这对今后20年的生物研究有着巨大的价值。世界各地的大型基因中心令人钦佩地接受了这个挑战。拿Sanger研究所来说，有超过35个Illumina GA2基因组分析器，运行在位于英国剑桥以南10英里处Hinxton基因园区的高通量设施中。

Sanger研究所的测序平台

测序平台是Sanger研究所的核心服务，对所有Sanger研究所或其他合作机构进行的基因组研究开放。由于测序对设备的要求极高，同时，Sanger研究所也开发了一系列操作工具与流程来满足这种高要求。

项目管理

我们开发了名为Sequencescape的一些列项目管理工具集，可以帮助研究者计划自己的实验，并帮助测序设备管理者计划容量和吞吐量。Sequencescape最初是被内部小组开发维护的。它用Rails语言编写，运行在标准的刀片服务器上，采用MySOL，通过内部网络(htp：//www.sanger.ac.uk/）供用户访问。

当一个新项目需要测序时（可以是单一样品的单次运行，也可以是1G碱基位上万样品的数千次运行），就在Sequencescape上登记，附带提供一些相关的元数据，例如科学理由、预算信息和联系方式。

每一个待测序样本都会以这种方式登记，测序运行的请求提交到设备。这个请求组成了测序平台工作的基本单位，物理DNA样本则被送交实验室小组来进行准备和测序。

准备测序样本是一个复杂的手动过程，其中包括一个不断提纯的实验室流程。物理DNA样本在手动仪器、自动机器和计算机控制组件间，甚至不同实验室之间，从一个试管中移到另一个中。记录一个样品的“生命故事”是很关键的，在评析测序结果、实验室吞吐量以及确定实验流程组件的错误调校或故障时，这些元数据可以导入进来。样本中的DNA如果稍微处理不慎，仪器就不能测序：这是一个费用昂贵并且耗时冗长的问题。通过评估和分析元数据，可以在失败中看到共同性，找出通讯协议偏差、误运转的器械或者错误调校。在不懈的监测和评析中，实验室进程得以继续提供高质量DNA样本以供测序。

生物实验过程，用俏皮的生物术语来说，是一个持续进化的过程。随着科学期刊或者会议中新方法的讨论发布，优化、更新、采用全新方法，或者是彻底推倒重做都是很常见的。新仪器和机器自动化引进使用。在实验室工作流这种高灵活度领域中，保持健壮的原始数据获取是极其困难的。它也很快在高吞吐量环境下变得不可实现，在这个环境中，即使很小低效或者停滞也会导致长久的事物堆积。

毋庸置疑的是，基因测序中病人个体的基因材料也许有限、供应不足，或者保存期限短。制备好的待测序DNA也不能永久保存，而且对于不能再次获得的DNA，实验过程或测序的延迟将会带来灾难性的后果。

为了平衡对有效原始数据集的需求和时间紧迫且高度动态的工作流之间的矛盾，现代的数据获取方法必不可少：我们不想看到软件成为工作流程中的限制步骤。

灵活的数据获取

传统的数据库重构，都是对数据模式(dta schema)进行增、删或者重命名，然而Sanger研究所的高通量生产线使用的数据建模工具集却允许在运行时改变数据描述。当一个实验室协议升级，包括一系列新仪器或者快速方法时，与任务相关的现有原始信息可以在运行时重构。如此帮助实验室更有效率地运作，减轻开发者时间负担，还使得数据集可以与当前实验室流程保持一致。

对大多数部分来说，生产系统中运行的数据库仍然是传统型的，存在于诸如项目、样品和工作流、任务这样关键协议组件的真实世界对象中。不过，元数据也被定为主要对象，表示为描述符(Dscriptors)、值和族(Fmily)对象类。一个族可以有多个描述符，描述符提供数据字段的定义，包括名称、类型、UI元素和是否为必须字段，等等。每一个工作流都可以有效继承一个族的字段。

这就允许我们快速定义和修订新类型的项目、样本和工作流任务。

所有的数据项页面的UI都是使用这些信息动态生成的，用以监测测序平台和实验工作流通量的多个报告工具也是一样动态生成的。族有版本控制，这就使得老项目数据可以持续访问，同时也有迁移和校对工具将旧版本对象升级为新版本。

从计算角度来看，这个过程比我们预想的要快。借助数据表中足够的索引和数据范式化，即使高通量系统高负荷运行时Web应用程序也能迅速反应。由于某些属性是牢牢固定在工作流中的（比如，每个物品都有名称、唯一标识符，以及创建和修改日期），他们可以很容易地重构回主要数据库字段以提供更快速的查询。

这项对于实验室工作流数据模型进行精简的尝试已经取得了很大的成功。优势越来越明显，因为灵活度的优化可以让相同的构架应用于研究所内其他实验室工作流中（首先用在了基因型鉴定中），而只需要很少的额外开发。Sequencescape项目管理和实验室信息管理工具是开源的，可以从http：//www.sanger.ac.uk下载。

仪器和数据管理

在Sanger，整个短读测序流程当做了一系列托管服务来运行，和Web服务定义的界面有通用性。项目管理和实验室信息管理最终让位于仪器管理工具，此工具可以监管、控制整个仪器平台，并尽可能地实现自动运转。

每台测序仪器都连接着一台PC，电脑用来控制仪器并给仪器产生图像数据提供临时存储空间。遗憾的是，尤其是对长一点的测序来说，本地机器的磁盘空间会迅速变得不够存储整个运行的数据，而且桌面PC的计算能力不足以进行数据的图像、序列与质量分析。

因此，数据通过一个10G b线路从仪器上连接的P C传输到更大的存储阵列中：由Lustre[3]管理的400Tb大的EVA存储系统中。这便是一个1000节点的网络集群，此集群承担了原始图像的主要分析和图像校准任务。管理这个规模存储系统问题很多，需要管理员不断干预，供应商不断扩展还有内部数据管理者的监管。这个阵列处理了差不多4个星期的数据；随着分析和最终的碱基识别，仪器中的原始数据相关的仪器运行细节（激光强度、荧光数据等）在删除之前都按需进行了校对并存档。

这种规模的原始测序数据很大，处理费用很高；不过，图像校准、序列上个各个碱基确定之后，每次运行产生大约30Gb序列和质量数据。如果原始数据没有备份（从磁带上恢复数据需3个月），每一个测序运行的原始图像压缩成低质量的JPEG文件，保存在数据库中。尽管不适合分析，如果任何一次运行都需要人工判读来确定是图像问题或者人工制品（油，不太好的DNA集群，甚至指纹都不稀奇），这个数据仍是有用的。

一旦测序数据可用，它会以两种格式被存储到一个高性能的Oracle数据库里。虽然生产系统充分利用了数据库，但是生物信息学工具通常会在文件系统上用普通文件工作。我们确实需要迎合所有的需求，这个存档里大量序列信息也同样通过一个Fuse[4]用户空间文件系统呈现给Sangery研究所内的计算农场。

这项措施出人意料的好。序列数据紧接着通过一系列的质量控制措施传送出去，然后在序列分析集群上运行，检查低测序产出、高层次的未知碱基，或者低复杂序列，而所有的这些都是测序错误的标志。质量控制(Q)结果又被传回到Sequencescape，研究者和合作者会收到有关新数据及其在Fuse资料系统位置的提醒。在测序结果并不具有足够质量，或者如果实验室存在问题的情况下，Sequencescape会自动排队样本进行再处理。

对Sanger研究所的许多项目来说，得到许可之后，数据会对所有人开放：从最初的项目和样本元数据，到实验室的原始信息，以及从每个样本中获得上百万测序数据。这些材料任何感兴趣的人都可以免费下载，包括最新的基因数据草图(htp：//www.ensembl.org/index.html)，各个测序追踪结果(htp：//trace.ensembl.org/），随后几个月，还会有更多的项目的短读序列（请关注http：//www.ebi.ac.uk)。这就为生物医学研究者提供了优秀的资源，同时，也继续了为人类基因组计划提供免费开放数据的光荣传统。

[1]即1000 Genomes Project，此计划将测定世界各地至少1000个人类个体DNA序列，参见http：//1000genomes.org。

[2]即International Cancer Genome Consortium，参见http：//1000genomes.org。

[3]Lustre是一种分布式文件系统，参见http：//lustre.org/。

[4]http：//fuse.sourceforge.net/

DNA的未来

这些新的测序技术和数据作为使用它们的工程的一部分正在为现代生物学的下一个篇章建造基石。运用此数据的管理工具、校对工具和分析工具将会继续进化，因而花费稍微长一些的时间关注DNA的未来，是十分有价值的。

如何成为基因黑客

与其他数据密集型领域相比，基因具有更加伟大的提供开放在线数据仓库的历史——从种类多样的基因浏览和注解工具（例如Enesembl和UCSC)，一直到和基因有关的疾病(HpMap,SNPedia)细节，例如23andMe和Navigenics的私有化的基因服务。这里有足够丰富的资源以供任何人日后成为一个基因黑客。

下下一代

目前，这种公司仅仅提供对基因感兴趣的几点高层抽象。但是创新一如既往地伴随下一代测序仪器和基因分析器的发展而前进着。像Pacific Biometrics和Oxford Nanopore这样的公司正在致力于可以使目前的兆碱基(mgabase)级的读取转换成千兆级(ggabase)，甚至更大。随着吞吐量的提高，相关费用会下降，成本为$5000，甚至是$1000的基因测序，对治疗学基因测序来说，这些已经变得很经济有效，并离我们越来越近。

大数据时代

以上的尝试若有所进展，我们可以肯定一件事情：数据需求正在急剧上升。大数据时代对于基因来说已经到来，这对于现代生物研究也是一样。数据将继续提供极其巨大的约束分析和研究，而且，很明显，越来越多的研究和医院队伍进行内部基因测序，大基因中心的角色将会有所改变。为成千上万的基因提供一个灵活的计算平台，有效率的序列搜寻、排列和汇编工具，加上安全的环境，将会变得至关重要。这也会为它们带来技术实现上的问题，比如隐私以及效率，所有这些都将会在当今生物医学的舞台上成为焦点话题。

尽管这些阻碍也许会带来一些地平线上的未知云团，但DNA的未来必定光明无限。

致谢

作者在此感谢Wellcome Trust对生物医学研究中开放接入和开放数据的不懈支持。

第16章　美化真实世界中的数据

Jean-Claude Bradley、Rajarshi Guha、Andrew Lang、Pierre Lindenbaum、Cameron Neylon、AntonyWilliams和EgonWillighagen

关于真实数据的问题

要在真实世界中搜集“数据之美”，并将其展示给感兴趣的公众，至少有两个问题需要解决。第一个问题：世界本身是嘈杂的。在大多数情况下，重复搜集同一块数据两次会得到不同的结果。这是因为搜集过程不可能完全无误差。温度、压力、湿度、动力源、水或试剂的质量、称量精度的波动，还有人类的误差都会使“正确”结果变得模糊。实验测量的艺术在于如何设计数据搜集过程，使随机误差与操作错误对结果的影响最小化。在最好的情况下，这涉及如何精确优化实验设计，检测误差的大小及来源。在最差的情况下，人们就只有不断重复实验，直到得到他们满意的结果为止。

对于处理误差导致的不确定性，传统实验的做法是进行重复实验并将结果付诸统计分析。重复的例子可以在大多数科学期刊的大多数文章中找到，很多图表中有“典型结果如上所示”。“典型结果”一般意味着“我们获得的最好数据集”。尽管理论上详细的统计分析是一种更严密的方法，但是也可能存在争议并让人误解。医学期刊的评论页中常常对从分析中删除不相关数据的适当方法产生争议。之所以对“典型”结果产生怀疑，对统计方法怀有争论，是由于无法接触原始数据。如果底层数据可得，人们自己便可很简单地进行分析和检查。虽然争议似乎不会因此减少，但是至少人们可以更加知情。

这个问题的第二部分是，一直到最近，印刷期刊的版面限制了可发表的数据总量，如此，便使得支撑论文论点的完整数据及数据分析的发布变得困难或者根本不可能。然而，在出版开始逐渐转向线上的当今世界，这个理由变得牵强了。起码在数据量处于KB级至GB级的研究中，将论点所依赖的完整数据集发布出来是可能的。因此一直有很高的呼声来要求发布完整的数据。不过，这样就产生了如何呈现数据的问题；数据可能不一致，也可能包含错误，但是起码可以显示出所得结论的总体描述。简单地说，问题就是：如何清晰地展示出隐藏在数据表面下的美丽，同时又可以不刻意避免或者隐藏数据表面的瑕疵。

我们认为要成功调和这些相互矛盾的要求，关键在于透明。以尽可能全面的方式提供原始数据，并处理和筛选数据的完整描述，意味着任何用户都可以深度挖掘到他或者她需要的细节。原始数据通常很难或者不可能自然而然地以一种机器可读、可处理的形式发布，所以筛选和提取过程同时也包含了对分类和简化的决策，从而提供清楚明了的数据以作其他用途。这里描述了我们在“美化”一系列开放来源[1]数据时所采取的方法，以一种开放的形式筛选和发布数据，允许任何人将其收为己用。结果表明，以上描述的方法已经使得多个研究人员能够配备一系列可视化和分析的工具，创建一个协作网络来有效地分析结果、提议后续实验，并将结果发布给更广大的读者群，而这皆是传统研究通信所不允许的。

[1]开放来源即crowdsource，指的是一种任何人都可以提交数据和参与各项任务，也可译作“众包”。

提供可以追溯到记录本的原始数据

作为Jean-Claude Bradley教授所指导的一个药物发现研究大计划(Badley 2007）的一部分，我们希望预测各种各样的化合物在非水溶剂（比如乙醇、甲醇等）中的溶解度。Bradley研究组用来合成潜在抗疟疾目标化合物(Badley等2008）所用的Ugi反应主要成分有醛类、羧酸、异腈和基本氨基酸，这些物质的溶解度是最让人感兴趣的。化合物的溶解度是化合物可在某个特定溶剂中溶解的数量。构建并验证一个可以预测溶解度的模型需要大量溶解度数据。出人意料的是，世界上并没有唾手可得的非水溶剂溶剂度数据。因此我们选择开放数据来源，将数据测量开放给想要参与的任何人(htp：//onschallenge.wikispaces.com/）。不过这样就产生了一系列问题。由于任何人都可以提交测量值，我们没有直接的方法来检查测量值的质量。

因此创建数据集的第一阶段需要创建如何测量每一个数据的详细记录。不同贡献数据的测量技术、精度和准度都不一样，但是所有的背景资料都以人类可读形式提供。这种将完整研究记录在实验完成时就发布出来的“彻底分享”方式称为开放记录本科学(htp：//en.wikipedia.org/wiki/Open_Notebook_Science)。这种方式虽然在专业研究者中并不普遍，但是它很符合我们公开一个完整和透明的数据集的愿望。我们利用在Wikispaces托管的wiki(http：//onschallenge.wikispaces.com)来保存实验记录，利用诸如Gdoc和Flickr(http：//flickr.com)等其他服务来保存数据（见图16-1）。

图　16-1：使用免费通用服务来托管实验工作记录及处理过的数据。A）单个实验测量时期的一部分；B）Flickr上实验中所拍照片；C）托管在Gdoc上主要数据仓库的一部分（见彩图53）

从实验描述中提取出数据值的数据库储存在Gdoc的电子表格中，用来生成项目的主要汇总数据。各条测量值一个不漏地发布在此，并附有一条指向原始数据的链接。无论是机读还是人们来阅读数据，这条链接都是很重要的，因为它不仅提供了测量值的出处（即谁给出的断言），还提供了此断言证据的记录。人们可以点击链接来查看测量是如何进行的，机器则可以在必要时下载或者抓取这条记录。

在某种意义上，电子表格是从实验室记录本转换为可以评论和过滤数据的第一步。从技术角度来说，选择Gdoc可能是一种偏好，不过这是基于以下几点要求的：允许我们以原始格式发布和共享数据；有一个实验科学家熟悉的界面并不占用他们额外的工作；由稳定的大公司维护的免费托管服务，可以让世界上的任何人以最小的代价复制这个信息处理模型；最后，可以用强大并灵活的API访问数据。很少有其他的方式可以让普通科学家用熟悉的方式使用、添加、下载原始数据，并且也为其提供强大的底层数据程序访问。

验证开放来源数据

由于数据是由不同的研究人员采用不同的方法搜集的，因此很可能出现矛盾的值。这可能会导致出现异常值或者直接出现相差很大的结果。习惯上，如果没有额外的信息，研究人员只能将每个测量值给以相同的权重或者应用统计方法来剔除异常值。然而，由于我们采用了需要完整记录每个值测量过程的开放记录本方式，每个测量都可以通过记录的信息上下文来进行评估。在若干情况下，这样可以允许熟悉此方法的科学家在测量条件不适当或者忽略一个重要参数的事实基础上，剔除有问题的数据点。

就溶解度来说，事实证明：混合时间和蒸发条件都是重要因素。对硝基苯甲醛在甲醇中溶解度的确定就是一个很好的例子。在五个测量值中，有三个明显低于另外两个（见图16-2；http：//oru.edu/cccda/sl/solubility/ugidata.php?solute=4-nitrobenzaldehyde＆solvent=methanol)。测量方法基于对硝基苯甲醛的甲醇饱和溶液配制，然后蒸发掉甲醇，称量残留物重量。其中的关键就是完全饱和溶液的配制，配制过程通常是在搅拌条件下添加溶质，直到试管中有不再溶解的可见固体。通过检查这些测量实验的详细情况可以发现，测量值较低的三个实验都只是稍作搅拌，而测量值较高的两个实验则搅拌了好几个小时，这说明实验需要长时间搅拌(htp：//usefulchem.blogspot.com/2008/12/mechanical-turk-does-solubility-on.html)。

原始实验记录的可获得性使得所有研究人员都可以发现存在疑问的测量值，也可以从“失败”的实验经历中受益。这就是科学研究的本质。在记录实验细节和高效之间需要达到一个平衡。初始实验的目的通常是帮助研究人员找出需要注意的影响因素。遗憾的是，这些信息通常不会在研究社区内分享。

对于这些存在疑问的结果，通常我们不是直接剔除它们，而是在这些测量值上加上“请勿使用”的标记，并给出原因。这使得其他研究人员可以点击进入原始的实验室记录本页面并自己检查原始数据。错误可能发生在任何地方，甚至包括验证结果的过程中。“错误”的数据在某些用途上也可能是有价值的。完全透明可以让数据的用户来决定在分析中包含什么数据。这也同样会减少（但是不会消除）隐藏错误的风险。这里有一个这种标记的例子，即香草醛在甲醇中溶解度的测量报告(htp：//usefulchem.blogspot.com/2008/11/what-is-solubility-of-vanillin-in.html)。

显然，在我们标记为可疑或者不可信的数值和没有标记的数值之间有一片灰色地带，尽管未被标记的数值在有些问题或者领域还存在争议。一天下来，它们可能会得出科学的判断，同时，也会产生大量分歧。每个案例的实验原始数据都可以被获取与查验，而且电子表格的历史版本同样如此。我们需要保持以下两者的平衡：提供一个有用的数据和每个决断与错误的呈现程度。然而，这个任务是很具有挑战性的。

在线发布数据

我们的目标是让所有的实验记录与处理后的数据可以在线得到。如何在Web中以一种有用的格式发布数据产生了一系列的问题，其中包括标准标识符、可视化工具和数据集成方式的选择。

化学实体的唯一标识符

要让我们的数据有用，使用公认的标准来描述化学实体就很重要。如果不这么做，数据集的集成就会变得很困难或者不可能。在化学中，有些人认为使用CAS注册号[1](htp：//en.wikipedia.org/wiki/CAS_registry_number)标识化学实体很理想。不过，CAS号本质上是私有的，而且不能转化为化学结构，只能用作查询，而且依赖与一个外部的组织来进行编号。我们更倾向于本质上开放的标识符，可以自由地进行交换，并可以同化学连接表[2]双向转换。

IUPAC[3]国际化学标识符(IChI，发音为“INchee”）提供了一个非私有的标准与算法以及相应开源支持软件(htp：//en.wikipedia.org/wiki/Inchi)用于标识字符串的生成及标识符到化学结构逆转换（参见http：//www.qsarworld.com/INCHI1.php的最近讨论）。InChI作为一个标准正在获得软件厂商、出版商和开发者的鼎力支持。InChI算法——即对同一结构生成多个不同InChI的可能性——已经通过标准InChI(Standard InChI)的开发得到了解决。对于某些用途，InChIKey(InChI的一种hash码）已经很实用，可是InChIKey不能转换成结构，而且只能在搜索化学结构的查找表中使用。

SMILES(http：//en. wikipedia.org/wiki/SMILES)是一种标识化合物的常见结构，其字符串编码非常紧凑，并且可以和化学结构双向转换。但是，SMILES算法有多种形式与实现，导致了同一化学实体有不同的SMILES。现在我们在此项工作中使用SMILES是由于其简单且搜索容易。正如本章的“通过唯一标识符与自描述数据格式实现数据集成”所述，将我们的SMILES转换为InChI是可行的，从而我们可以自动将数据集成到由InChI标识的数据网络中。

开放数据与可访问服务提供了广泛的可视化与分析选择

有了一个标准、自由、可访问的原始溶解度数据贮存构架，下一步就是分析数据。分析可以是简单地生成所有已完成测量的汇总，也可以是来自数据衍生模型的复杂统计描述。不论是哪种情况，都有必要通过自动化的方式访问数据以进行处理。采用其他来源的信息来丰富数据也是值得一做的。这反过来又要求主要数据需以通用的标准发布。

电子表格包含了若干列，每一行是一条测量值。溶剂和溶质都采用了两种格式表示：人类可读的通用名及机器可读的SMILES编码。由于选择GDoc作为数据的主要发布形式，为了让数据集发挥最大优势，采用机器和人类可读的两种表示是非常重要的。唯一不需要采用两种表示方式的是采用数值表示的溶解度本身。

前面已经提到，我们决定不从主数据集中剔除有问题的数值。这产生了一个机器可读性的问题，因为并没有通行的方式来表示“这个数字有一点狡猾”。对于这项工作，我们决定在人工判读之后来标记那些被认为是不准确的数据，并给出理由。这使得任何用户在做任何分析时都可以选择采用那些记录，不管是采用人工还是自动的方式。任何进一步的详细分析都需要访问电子表格中的数据。最简单的方式就是把数据导出，比如导出为CSV文件，然后用其他软件来分析。但这样会导致无法获取到最新的数据。G公司的Doc API则可以让创建Web应用变得简单，同时又维持着与“实况”数据的连接。

作为这种应用的示例，我们创建了一个Web服务来让用户查询电子表格中储存的数据并获取表格汇总（见图16-2）。这个基于表单的查询界面为获取相关数据提供了一个快速直观的访问方式。整个页面只由HTML与JavaScript构成，不需要任何的服务器端软件。数据是通过G公司提供的API从GDoc中异步访问的。获取数据之后，就可以进行一系列的计算。在这里，我们要计算溶解度的平均值和标准差，可以用于动态高亮反常数据。不仅原始数据子集的表格输出很有用，可视化技术也可有效地概括查询结果。这个查询应用采用G公司Visualization API生成电子表格中提取数据的柱状图。从电子表格提取数据很容易，生成一系列可视化图表也是很容易的。在我们的案例中，一个显示某化合物在不同溶剂中溶解度的简单柱状图就可以提供结果的简要概括。

本应用的另外一个值得注意的地方是表格中包含化学结构的二维渲染图，渲染图是通过美国Indiana University基于REST的服务提供的。通过在服务URL中传递SMILES代码，可以将化学结构的二维图片插入到任何网页中。同样，这些特性也不需要服务器端软件的支持。分发这种应用变得非常容易，仅仅只需要把HTML页面拷贝到另一个Web服务器上即可。

图　16-2：用于检视溶解度数据的可视化工具。A）采用JavaScript和G公司Doc API生成的一个简单表单输入界面；B）溶解度数值的图形化表示；C）带有二维化学结构渲染图的数据表格输出，可以从http：//toposome.chemistry.drexel.edu/～rguha/jcsol/sol.html访问此服务。请注意：本服务与这里提到的其他服务都是动态的，可能得到与上图所示不同的查询结果（见彩图54）

虽然这只是一个相当简单的应用程序，但它突出了这种解决方案的分布式本质，即把开放数据与具有多个来源的免费可视化方法结合起来。更重要的是，这个系统的分布式本质和可自由获得的数据使得来自不同领域的专家们——测量数据的实验员、制作接口的软件开发者以及制作创建统计模型的计算建模人员——可以更好地将他们的专业技术结合起来。开放数据、开放服务以及支持它们的生态系统的真正前途在于：这种专业技术的结合不需要建立任何正式的合作关系。研究人员可以用一种数据的测量者所想不到的方式来使用数据。不管是进行补充实验，还是做新的分析，或者提供处理数据的新服务，这样做都可以使原始的数据集更有价值，同时加强它周围的生态系统。而且不管什么时候，都可以找到指向原始记录的链接。

通过中心聚合服务集成数据

对于我们主张的方式存在一个合理反对意见，那就是：如果此方式被广泛采用，那么将会导致很多分散、不相连的数据源出现。尽管，从技术上可以利用搜索工具将这些资源汇总起来，但对于研究人员来说，这仍然是一个问题。因为他们通常习惯于使用几种他们喜欢的服务。有人校对的化学信息数据集中，美国化学会的化学文摘服务(Cemical Abstract Service,CAS)是黄金标准。CAS注册集中覆盖了超过四千万种物质(htp：//www.cas.org/newsevents/connections/derivative.html)，其中包含的数据来自出版物、专利、化合物目录以及越来越多的在线数据源，比如ChemSpider(http：//www.chemspider.com/blog/caschemspider-connectivies-and-unintended-collaboration.html)。

ChemSpider是以“构建以结构为中心的化学家社区”为目的为化学家创建的Web资源。有来自超过150个数据源的两千余万不同的化学实体，ChemSpider已经成为了化学家寻找化学实体信息的主要Internet资源之一。每个化合物都有不同类型的信息相互关联起来，这些信息包括不同的标识符（系统命名、商品名、注册号、各语种命名），理化性质的预测值，实验获得的物理、化学与光谱数据来自一系列数据源(htp：//www.chemspider.com/DataSources.aspx)的指向链接。因此ChemSpider可以看做一个指向其他资源且基于结构的链接农场[4]。同时，ChemSpider还提供了一个操作环境，以便用户在线简要或者扩展显示数据。用户可以批注、校对数据，从而删除化学实体的错误关联，并且添加他们自己的信息，包括指向外部资源的链接或者其他数据批注。用户也可以向数据库中储存新化学结构，并为之关联光谱数据、图片，甚至是视频文件。ChemSpider中有很多种搜索功能，包括搜索预测的数据、基于结构或子结构的搜索[5]。

ChemSpider中数据主键通常是分子的通用标识符，如此就为连接其他来源的化学信息提供了一个理想的环境。ChemSpider不仅提供了化学数据搜索的中心资源，同时也是用户的数据银行，其他研究人员就可以很容易地通过它找到我们的数据。尽管一些ChemSpider关联的数据是由在线资源抓取的数据搜集的，但是对这种方式需要小心处理(htp：//www.chemspider.com/blog/care-in-nomenclature-handling-andwhy-visual-inspection-will-remain.html)，更多的数据是在进行了某种形式的人工校对后添加的。

非水溶剂溶解度数据是作为ONS-Solubility[6]项目的一部分添加到ChemSpider中的。目前一些在线可得的数据是作为补充信息和化合物数据中可能有的其他理化数据放在一起的。溶解度数据有一个链接指向实验记录页面，这样既和ChemSpider作为指向原始数据源的链接方式一致又和我们指向原始记录的方式一致。图16-3给出了一个ChemSpider的示例。记录中有了越来越多的非水溶剂溶解度数值，可能是人工测定或者机器测定的，这些数据都会发布出来，其相应的原始来源信息也可以点击可得。

图　16-3：这是ChemSpider条目显示溶解度数据与原始数据链接的例子（见彩图55）

以后会有更多质量与校对程度参差不齐的数据在线发布，可以预想到数据提取过程会自动化。在现在的项目中我们已经在努力利用自动化过程，不过自动化过程可以推广之前有两大困难需要克服。第一个问题是信任问题：哪些来源的数据可以给予充分的信任然后自动提取？对这些数据源应该进行何种程度的人工校对？目前对GDoc电子表格主数据的校对程度是否足够？是否还需要进一步的过滤？对目前的情况来说，我们采取的方式对一个致力于提供化学家可以无条件信任的服务还有差距。随着研究者发布更多的原始数据以及自动数据提取的增多，需要详细讨论数据应该在何时以怎样的方式发布，数据放置在上千白名单中需要怎样的标记。数据发布的认证过程会越来越多，从而为发布的数据提供质量和功能的双重标记。

第二个主要问题是功能问题：关联到ChemSpider的数据集目前来说还不多，在大多数情况下也是稳定不变的，所以人工提取过程尽管需要工作较多但是更实用一些。随着数据集的增多，这种人工过程会变得不可能。将数据作为在线电子表格发布出来对研究组来说很方便，但是并不能映射进ChemSpider或者其他中心汇总服务的数据结构中。ChemSpider是基于微软SQL Server的关系型数据库的。ChemSpidr标准Web页面中显示的理化数据是保存在数据库的数据表中的。在有数以万计数据供应者创建各种数据的世界中，需要一个通用语言来使广泛的数据重用于汇总成为可能。描述化学信息有多种相互竞争的标准。最可以信赖的方式是开发一种方法让数据尽可能以通用的形式展示。然后，就可以开发在不同描述标准间转换的服务。

通过唯一标识符与自描述数据格式实现数据集成

尽管G公司的Doc API提供了为数据集开发分析工具与可视化方法的简单途径，但是问题仍然存在，数据的展示并不是标准格式，连唯一标识符也是如此。那些工具是根据GDoc电子表格为此数据集所写的。简单来说，它们需要人类来理解电子表格的每一列是什么描述符什么数值。尽管利用程序来判断包含SMILES代码的列是可行的，但是机器却不能知道它是溶剂还是溶质，甚至根本就不能判断到底是不是溶解度数据。要实现相互连接数据的希望（即支持ChemSpider和其他服务的自动提取），要以最通用的方式将数据提供给其他研究人员，那么就有必要提供一种遵守语法标准与描述符标准的格式。

资源描述框架(Te Resource Description Framework,RDF)，提供了将数据集以一种公认的机器可读格式发布的方式。有了这种格式，任何信息都可以转换为一条由一个“主语”、一个“谓语”和一个“值”组成的语句。举例来说，下面代码片段中所示代码表明电子表格中找到的对象叫做solute#59，在给出的URL中定义为资源。RDF使用“命名空间”，或者说采用公认概念集来定义“资源”之间的关系，资源可以是由唯一标识符指向的任何对象。这里一共有四个主命名空间。第一个是RDF命名空间本身，它定义了文件是RDF格式的，提供了诸如“由……定义”或者“是一个资源”这样的顶层概念。第二个命名空间是包含数据的电子表格，其本身也是个资源，在这里由命名空间ons定义，其中又包含了某些资源，每个单元格即是一个资源。第三个命名空间是Dublin Core(dc)，包含了诸如名称、作者和版本的概念。第四个命名空间是chem，托管在http：//rdf.openmolecules.net(RON)，用来指定某个单元格定义的分子是被定义为一个资源的。

如前面所说，电子表格有它自己的数据结构，大体上是每一行指向一个测量值。G公司的Doc API可以让一个简单URL就可以引用一个单元格。要将此写入RDF声明中，我们需要描述单元格内容间的关系，比如“2-辛酸”和其他资源间的关系。一个简单关系指出“2-辛酸”由RON的某个资源定义，RON通过简单的HTTP URL引用。有了定义好的单元格内容，就可以使用外部资源来找出关于这个分子更多的信息。解析这个URL就可以获得RON服务定义的更多关于这个分子的RDF语句。类似地，它也可以给出SMILES和分子的名称，这些都是从电子表格中取得的。对我们数据中每条表示分子的记录都无一例外地可以定义一个标准描述让和包括系统命名、InChI和SMILES在内的其他标准定义连接起来。

＜ons:Solute rdf:about="http：//spreadsheet.google.com/……/onto#solute59"＞

＜rdfs:isDefinedBy rdf:resource="http：//rdf.openmolecules.net/?InChI=

1/C8H14O2/c1-2-3-4-5-6-7-8（9）10/h6-7H，2-5H2，1H3，（H，9，10）"/＞

＜chem:inchi＞InChI=1/C8H14O2/c1-2-3-4-5-6-7-8（9）10/h6-7H，2-5H2，1H3，

（H，9，10）＜/chem:inchi＞

＜chem:smiles＞CCCCCC=CC（=O）O＜/chem:smiles＞

＜dc:title＞2-octenoic acid＜/dc:title＞

＜/ons:Solute＞

定义了电子表格中找到的每个化学实体之后，我们就可以将每个测量值类似上述的RDF片短表达。RDF定义了新的测量值，给出了溶剂、溶质、溶解度和测量值所属的实验。由于我们已经用化学表达定义了每个溶质和溶剂的ID，这个测量值信息也可以被其他描述同一分子的RDF文件链接并使用。下面的RDF片段使用XML实体ons作为http：//spreadsheet.google.com/plwwufp30hfq0udnEmRD1aQ/onto#的别名来让XML可读性更好。(masurement179可以扩展为添加了“measurement179”的完整URL。）

＜ons:Measurement RDF:about="measurement179"＞

＜ons:solubility＞0.44244235315106＜/ons:solubility＞

＜ons:solvent RDF:resource="solvent8"/＞

＜ons:solute RDF:resource="solute26"/＞

＜ons:experiment RDF:resource="experiment2"/＞

＜/ons:Measurement＞

这些语句，或者说三元组[7]可以用任何RDF引擎和诸如SPARQL的查询系统读取或者分析。通过使用合适的命名空间，尤其是那些被承认和分享的命名空间，可以生成基本上自描述的数据文件。我们开发了一个解析器(htp：//github.com/egonw/onssolubility/tree/）来生成完整的RDF文档，可以在http：//github.com/egonw/onssolubility/tree/master/ons.solubility.RDF/ons.RDF下载。化学开发工具包(Te Chemistry Development Kit,CDK；参见http：//cdk.sourceforge.net/）用来从SMILES中获得包括InChI在内的分子属性。这是关键一步：将实验特定信息转换为可以有任何支持RDF系统或服务读取的数据文件。这种服务不需要理解如何处理新命名空间的特定概念，但是却可以知道如何处理这些概念所属的类别，从而可以将数据解析为使用相同命名空间的其他资源。

RDF的真正力量在于将多个资源通过链接连接起来（见图16-4）。举例来说，可以将我们的实验数据连接到DBPedia的数据中，DBPedia(http：//dbpedia.org)是一个由RDF呈现的在线信息资源。DBPedia使用名为简单知识组织系统(Smple Knowledge Organization System,SKOS；参见http：//www.w3.org/TR/skos-primer/）的命名空间来引入诸如“类别”的概念。在DBPedia中，不同的溶剂被描述为属于不同的类表，如碳水化合物或者醚类。通过将我们的数据与DBPedia中的RDF语句结合起来，可以对我们的实验数据进行诸如按照不同溶剂类别中的测量值进行查询。之所以是这样可能是因为RON中的资源将某些概念（分子的ID)与我们的数据和DBPedia中的资源链接了起来。尽管DBPedia中可能包含错误的化学命名（只要对象关联了正确的InChI即可）；尽管我们的数据没有溶剂类别的概念；尽管DBPedia对ONS命名空间一无所知，RDF还是做到了。

图　16-4：通过R D F将溶解度测量值和更广阔的数据网连接到一起。RO N(http：//rdf.openmolecules.net)即将DBPedia、Chemical Blogspace和ChEBI（欧洲生物信息学研究所的化学资源）中的记录连接到一起

将此更进一步，可以把我们的实验数据连接到Web上更广阔的天地中去，举例来说，可以利用RON中的RDF识别出讨论某个特定化合物的博客。此RDF包含了指向Chemical blogspace(http：//cb.openmolecules.net/）的链接，同时共享唯一识别符（在这里，InChI以URI的形式得到了使用）。rdf.openmolecules.net连接到了一系列数据源，这样又为不同来源的数据和分析结合在一起铺平了道路。RDF方式的价值在于可以在此图的任何一个地方插入附加数据。共享更多共同的词汇元素可以让数据集成变得更好，不过只要有一个共同的标识符，数据集成就可以开始。

[1]CAS注册号是一种化学物质的人工编号方法，如甲苯的CAS注册号为108-88-3。

[2]化学连接表是在计算机中表示化学分子结构的一类常见格式。其基本原理为储存每个原子信息与原子之间的连接关系。

[3]IUPAC，国际纯粹与应用化学联合会。

[4]即link farm，指本身并无内容，但提供大量链接的网站。

[5]结构搜索指的是以化学拓扑结构为查询对象，采用拓扑结构匹配而非字符串查询来进行化学搜索的方式。

[6]即Open Notebook Science-Solubility，开放记录本科学溶解度项目。

[7]即包括“主语”、“谓语”和“值”的RDF语句。

结束循环：采用可视化技术启发新实验

前面说过，实验取得的数据可以采用很多种方式，从可视化到建模。这些活动都很有用，可以为手头的实际问题提供帮助。不过我们的主要目标是采用建模和分析来启发新实验的设计。随着开放来源努力的扩展，考虑可能的实验与优先度是很重要的，尤其是在最终目标是让更多感兴趣、但不一定是有经验的研究者参与进来的情况下。这种计算上的优先级在很多情况下都是有用的，如资源（经费、材料、时间）有限不能进行所有可能的实验。就溶解度来说，可能有实验员问：“我们已经测试了这些化合物，下一个该做什么？”数据的可视化既可引人注意又可在资源有限的情况下为下一个实验的选择提供帮助。这就建立了实验和计算的良性循环，使得实验者和数据分析者都可以人尽其才。

要确定我们（或者别人）下一步测试哪些化合物，我们需要一种方式来了解我们所测定的化合物在化学空间[1]的位置。然后就可以确定我们的数据在化学空间中所空缺部分的位置，并用此空间内的特定分子来拟合，从而进行这些实验。这需要集成我们数据集中找不到的信息。我们有化合物和溶剂的ID也有溶解度，但是我们不知道分子的特性，也就是它们在化学空间中的位置。要获取此信息，我们需要使用一系列服务创建化学数据的混搭(mshup)。我们提供了CDK(Steinbeck 2006）描述符的一个简易REST接口。类似http：//www.chembiogrid.org/cheminfo/rest/desc/descriptors/c1ccccc1COCC这样的URL可以额外获取包含多个URL的XML文档，其中每个URL又指向一个含有特定描述符数值的XML文档。这里展示的可视化所采用的空间特性包括化合物分子量(M)，预测的油水分配系数(AogP，化合物亲水亲油特性的一种度量），还有计算得到的分子表面积(TSA)。Web服务中还有更多的描述符可供我们使用。

由于这里所有的服务和数据都在开放Web上提供，三方用户就可以利用这些服务和数据来进行可视化。使用GDoc中的数据和Indiana University的Web服务，我们独立开发了一个可视化工具，可以提供所有化合物在某个特定溶剂中溶解度的多维可视化(htp：//oru.edu/cccda/sl/descriptorspace/ds.php)。在图16-5中，X与Y轴都是一个特定的分子描述符，颜色表示化合物的类型，每个点的大小和颜色表示溶解度。另外，把鼠标在点上悬停可以激活一个含有更多详细信息的提示条，其中有结构和溶解度数据。此图清楚地显示了现有数据点所没有占据的化学空间部分（比如图A的左下方）。理论上讲，可以查询ChemSpider这样的数据源来给出空白部分化合物的建议。

要扩展多维展示的能力，我们准备了在Second Life[2](htp：//www.secondlife.com；见图16-6）的3D环境中的可视化工具。和GDoc一样，Second Life也是一个古怪的科学可视化环境。不过，它一样满足了我们对项目其他部分所要求的条件。它通过一个通用的免费程序包为用户提供了一个简单（或者说相对简单）的环境。其他专门开发的可视化工具经常很复杂而且价格昂贵，和它们相比，Second Life显著降低了门槛。从可视化体验的角度来讲，Second Life同时也有很多优点。它可以在图表周围移动，放大缩小，甚至走进图表中，从内部的角度来查看图表。不同的用户也可以同时查看和操作同一图表。从开发者的角度看，Second Life提供了将数据带到Web之外的能力，可以使用前面提到的Web服务，同时也为用户提供了一个可点击界面，可以操作图表或者随着数据点中的链接访问数据源和实验记录。从理论上来说，使用一个完全在浏览器中工作的开源渲染系统可能更好，但是开源系统十分有限，而且没有其他系统提供了像Second Life这样的技术性能、简洁的界面和可用性的组合。从实际应用上来说，这些引人注目的可视化界面已经说明了问题。

图　16-5：化学空间溶解度数据的图表展示。A和B给出了同一数据组在表示不同化学特性的数轴上的两个可视化图表。点的颜色表示化合物的类型（红色的为醛类，蓝色为羧酸类，黄色的为胺类，黑色表示其他），点的大小表示溶解度的值。表C显示了可点击的界面，其中有单个数据点的化合物结构和溶解度值（见彩图56）

很明显，易访问的数据会让计算科学家可以进行一系列分析，不过实验和计算的紧密集成则可使得整体效率更高。虽然很多计算分析需要大量的人工干预，不能转换为自动化的在线服务，不过也有很多简单的分析能转换为可以与一系列平台交互的服务。这样，分析数据来指示实验方向、开发新的应用或者和混搭其他数据和应用都变得更简单了。这些混搭展示出了使用广为人知且易于转换、机器可读标识符的强大之处。此处的SMILES编码是关键的标识符，使用它可以从其他Web服务、数据源或者其他研究者实验数据中获取进一步的数据。在不久的将来，采用可以描述结果的RDF会极大地促进自动集成。

图　16-6：使用Second Life展示多维数据。三个空间轴分别表示三个化学描述符。球的颜色表示化合物的类别（和图16-5的定义一样），球的大小表示在当前溶剂中的溶解度。此可视化图表可以在http：//slurl.com/secondlife/Drexel/165/178/24找到，即Second Life中的Drexel岛（见彩图57）

由于RDF提供了一个基于公认字典的自描述框架，在不知道服务在何处且不了解内部数据结构的情况下，搜索提供感兴趣信息的数据服务也是可能的。现在大多数的混搭服务都是在已知服务、已知数据结构上，通过单个公用键（如地理位置、搜索项、日期）工作的。开放数据Web的对象间关系都是自描述的，其真正希望在于任何人都可以创建任意的混搭服务，其中的数据搜索和信息资源都是过程的一个整体部分。

[1]即Chemical Space，指的是所有稳定化合物所组成的集合。

[2]即第二人生，是由Linden实验室开发的一个基于因特网的虚拟世界游戏。

在开放数据和免费服务下建立数据网络

科学展示和科学交流的艺术很大程度上在于设计可以剔除不准确或者是误导性结果的流程，同时又要提出证据来论证一个人们可以理解的简单解释。科学，可以看成是一个把世界的各部分简化为简单模型的过程。这个问题的一部分是将模型过分简化，而只是为了强调某一论据或者让复杂的系统易于理解。

我们的做法则是提供全部细节让人们直面真实测量的复杂性。通过在创建主数据集时采取尽可能透明的方式筛选数据记录，我们致力于在复杂性产生的问题和对清晰有用数据集的需求间保持平衡。网络存储空间几乎可以零成本获得，而且有着大量优质、免费的托管服务，这使得运行一个公开的研究记录成为了可能。这样就无法再为在文章中出现“数据不再一一列举”找理由了。不过要提供一个完整的记录又产生了几个新问题。

第一个问题是简单的体积问题。研究记录本身往往是由一系列不系统的文本和图像组成。并不存在一个由人类或者机器容易解析的通行标准。在将所记录信息转向为人所用的过程中，提取和过滤是必不可少的。我们选择GDoc电子表格作为已提取数据的主数据源。自记录中提取数据的过程，目前仍然是主观和人为的。电子表格为人类提供了一个自然的界面，在个别实验科学家看来，它同样为网络服务提供了一个有效的数据再加工和展示界面。

我们可以考虑直接从实验报告中抓取数据。使用一些通俗格式和正则表达式分析和转换，从记录的feed中读取数据自动填充电子表格是可能的。我们还没有做到这点，因为我们期待在这个阶段中有人为筛选的过程。随着项目的扩大，到了一定规模这样就不行了。在何种规模引入自动抓取将取决于项目、数据类型，以及目前呈现的数据集对与精度和准度的需求。

数据一旦公开化，它就会对任何有兴趣的研究者敞开大门，而且G公司的Doc API也使得在大范围的服务中利用数据成为了可能。这其中包括可视化或分析服务。这些服务将会依赖于对电子表格数据结构的理解，这意味着工具通常是针对某个特定数据集编写的。但是，即使在这种情况下，有效利用大范围的服务、数据源以及可视化工具来造就高效的数据展示仍是直接可行的，这些展示小到表格和简单图表，大至五维、七维甚至更多维的可点击界面。开放的标准和系统提供了把数据和信息移动到最有效之处的能力。关于真正的开放和自描述数据格式的承诺是了不起的，但是并不现实，即使是像化学这样依赖数据的科学也一样，因为以一个实验者理解的方式把记录恰当地翻译为系统的可机读格式，并要使得编码的计算机或者人类也可以理解，这是科技上和社会科学上都有的困难。这里我们已经展示了把电子表格（那些实验者所熟悉和认可的）的数据转换成RDF，不过其他的格式一样也会很简单的。

这样普遍的数据格式为开创能够综合多源数据的服务提供了可能。包含溶解度信息，或者数据集中其他信息的大范围数据源可以集中进行综合分析。这就使得ChemSpider这样的聚合和连接农场服务成为可能，不仅仅是自动聚合数据，从技术上已经可以实现对多源数据所需校对级别进行判断，并在需要时引入人工校对。这个中心化，反过来提供了一个有价值的索引服务，而这个服务提供了一个在线的中心位置，在那里用户能够搜到他们寻找的数据。我们描述的所有工作的中心主题之一，在于免费托管系统的使用，这个系统可以在为用户避免复杂性负担的前提下提供足够多功能。对大多数的记录、聚合、分析、可视化以及展示步骤来说，这里拥有更加先进、大众或者更加尖端的可用工具。为了记录这次研究，我们本可以使用一个商业电子实验室记录本或者是一个专门设计的在线系统；然而，我们选择了免费的wiki服务。主数据的呈现可以使用一个带有内容管理系统的数据库后端来提供复杂的可视化；但我们选择了一个在线电子表格并利用其JavaScript API来展现一定范围的可视化服务。虽然有很多功能高级和复杂的三维可视化环境，但是我们选择了Second Life。

我们的部分决策理由是费用。我们使用的所有服务器都是免费可用的，并允许本质上无管理的发展过程有成长的空间，也吸引了新的低门槛的合作者。然而，此决策更多的考虑则是运用一个适合目标却不过于复杂的服务。在实验者和理论及分析型专家之间构建一个有效的连接通常都是一个挑战。将电子表格应用为数据源，不仅能够自动或人为地转换成一个有良好描述能力的格式（一个正式的关系数据库），或者是一个自描述且可扩展的格式(RF)，也能简单地直接转换为复杂的可视化，这个应用也阐释了为何将电子表格作为结合点。实验科学家们喜欢并且理解电子表格。计算科学家们也许更倾向于文本格式，或者是可以用代码操作的数据库，类似XML和RDF的格式。把这些用户群集合在一起的关键将是具备能够自动进行格式来回转换的能力。

最后，整个工程的关键是信任和透明度。由于记录需转化为数据，数据需转化为信息，而最终信息会被转化为一个模型或者理论，在以上每一个阶段内容都有可能丢失。那些通常杂乱无章的细节，在更大的图像出现的时候总是被抛在脑后。这是完全合适的。科学，正是一个以允许人们预见未来的方式进行总结观察的过程。正如我们对服务的选择，一个科学模型或者理论如果可以做到通常用最少的时间完成实验，那么它便是有用的。然而，传统上，这个总结的过程是以无法探寻细节为代价的。在网络的世界里，存储是廉价的，在此不再需要赘述。而当前，如何做出选择，在于如何展示隐含的细节，在汇总过程中如何过滤，以及如何保持汇总结果和原始记录间的链接。

这些并不是简单的决定，我们并不会宣称我们百分之百正确。尽管如此，我们相信这个工程能够作为这项尝试的样本。四个月来，一个原本是火车上两人间的讨论已经发展成为一个跨国的数据收集、可视化以及建模尝试的项目，而这些参与者们是实实在在全部共享所有的数据和分析。如果新的研究者感兴趣，那么合作可以容易地得到发展。我们开放的数据和服务已经创建了激发兴趣的新的可视化服务，而且不需要任何实验者自身的直接参与。这些可视化不仅对于实验者是有用的，而且，自身也是极其美丽的。不过，它们只能再现任何人利用我们发布的数据能够做到的一小部分。同时，它们通常提供通向充满瑕疵和缺陷的原始记录的链接，允许任何一个用户评定其有效性和他选择的分辨率下的任意某个数据。

美丽，通常被认为是类似简约或对称的，这是一种用简单的数学描述便可使全貌再现的感觉。而与此相比，现实中的实验数据却大相径庭。那些存在或者有时埋藏于实验数据中的美丽，或许需要大量的过滤才能浮现出来。但是，若真正的美丽存在于理解那些真正于世界最深处发生的事情之中，只要我们可以，那么我们就能够仅仅依据已知的分析来发觉那有限的一点点美丽。通过提供尽可能多的记录，我们可以让其他研究者发现和揭示更多埋藏在表面深处的美丽之路更加平坦。

致谢

作者在此感谢各位所做的努力。感谢Khalid Mirza、Jennifer Hale和Tim Bohinski完成大部分数据搜集工作。感谢Bill Hooker对开放记录本科学的帮助。感谢Submeta和Nature出版集团给予的经费帮助与大力支持。

参考文献

[1]Bradley,Jean-Claude. 2007."Open Notebook Science Using Blogs and Wikis."Available from Nature Precedings,http：//dx.doi.org/10.1038/npre.2007.39.1.

[2]Bradley,Jean-Claude et al. 2008."Optimization of the Ugi reaction using parallel synthesis and automated liquid handling."Journal of Visualized Experiments,http：//dx.doi.org/10.3791/942.

[3]Steinbeck,C. et al.2006.Current Pharmaceutical Design，12，2110-2120.

第17章　数据浅析：探索形形色色的社会定型

Brendan O'Connor和Lukas Biewald

引言

我们如何感知年龄、性别、智商和魅力？我们如何从形形色色匿名的观点中获取洞察力？去年，我和Chris Van Pelt一起构建了网站FaceStat.com，在该站点中，用户可以上传他们自己的照片，以及浏览和评判其他人的照片（见图17-1）。令人惊讶的是，该站点变得很流行。超过10万勇敢的用户上传了他们自己、朋友、亲戚、仇人的照片等，而且收集了关于比如以下几个预定问题的1000多万个评论：

·我看起来多大了？

·你觉得我看起来是否很帅？

·你觉得我和一只中等体型的狗打架能赢吗？

·用一个词描述我。

我们称这些问题为“多元的Hot-or-Not”。

心理学和社会学的研究人员已经广泛地研究了定型(sereotype)以及我们的外表如何影响别人对我们的看法。但是没有人可以访问来自如此多样化群体的如此庞大的数据。该数据比传统的实验研究要丰富得多，但是庞大的数据是否能够弥补对数据的控制力缺乏问题？实际上，真实世界的数据可能是最有启发性的：认为自己在做游戏的人比起一个在心理学入门课程上参与一个调查的大二学生会更诚实。

图　17-1：FaceStat的评判界面（见彩图58）

我们喜欢探索大量的数据集。我们要寻求的是有趣的模式和关联，而不是去验证预想的各种假设。我们不会试图隐瞒或者掩饰杂乱的游离点和缺失值；相反，我们将公开显示我们被迫所做出的各种选择。我们不会对种种定型做出评论或者下有争议的结论——让数据来说话吧。

预处理数据

我们从头开始：像很多网站一样，FaceStat运行在一个SQL数据库上。评判界面记录用户的评论并把它们作为一个三元组(fce ID,attribute,judgment)保存起来。我们做的第一件事是从数据库中抽取1000万条记录。它生成一个如下的文件：

face_id key value

149777 describe serious

18717 trustworthy 3

140467 attractive 2

149777 describe five-head

……

我们对探索不同类型的感知属性之间的关系感兴趣。一个有趣的问题是“我看起来多大了？”第一件要做的事是看人们给出的回复。Unix命令行工具使得快速查看人们的回复直方图变得非常简单。最常见的回复看起来是合理的年龄，但是我们也发现了一个问题：

Look at$cat data.tsv|

age judgments'grep"age"|

values cut-f3|

and count how many times sort|

each value occurs,uniq-c|

and order by this count.sort-nr

以下是这个shell管道的输出。对于每一行，第一个数字是频率计数。第二个字符串是回复值——是人们针对问题“我看起来多大了？”在Web表单中输入的回复。经常出现的情况是用户输入了一个数字，但是有时存在一些问题：

7047219

7002122

6938718

6842317

……

2724\r\n

2717\r\n

2301

2116\r\n

……

1 old enough to know better

1 hopefully over 21

1 e

1??

……

FaceStat已经运行了8个月，而且经历了很多变化，因此数据是在不同的情况下收集的。一些奇怪的网页浏览器会增加行结束空白符\r\n。有时存在bug，用户输入文本形式的回复以及一些其他格式有问题的数据。看看在sort|uniq-c|sort-nr之后，直方图的最底部的数据值，就可以很容易地暴露出数据上存在的bug，因为它们通常会展现为游离点。我们需要写一些正则表达式，它可以清除这类格式有问题的值。

详细描述所有的数据合理性检查和数据清除会显得很繁琐，但是它们是任何数据分析重要的第一步。对于任何人工生成的数据集，必定存在一些杂乱的游离点。比如，有人发现了如何绕过选择哪张脸来评论的随机性，把一张脸标注了100多次“mr.cool”。

除了清除之外，对于该特定数据集的一些重要的决策是：1）如何把多选的回复如“很值得信任”和“不值得信任”映射为一个数值；2）如何把很多人对一张脸的很多回复聚集成一个描述。每张脸在一些不同属性上有大约几百个评价。我们将简单地对数值评价求平均值。（在这种规范下，我们忽略文本描述，在后面会提到。）因此，每张脸包含感觉上的年龄、智商等因素的均值。它看起来如表17-1所示。

总而言之，有成千上万的脸，包含20多种不同的属性。有很多缺失值：询问不同的人不同的问题。有了这些警告，我们可以把数据加载到某个包里进行更详细的分析。如果你想深入了解，我们已经生成了数据子集，在http：//data.doloreslabs.com可以获取有用的代码。

探索数据

有很多不错的数据分析工具。表17-2对一些最常见的工具进行了比较。

我们喜欢使用工具R，它是一个开源的统计和可视化编程环境，包含活跃的并且不断增长的开发社区。它在统计学中作为一种事实标准而产生。为了广泛的数据分析，比起其他数据分析包，我们更喜欢R工具包，因为它的图形库、便捷的索引标注以及一组优秀的复杂统计和社区维护包。你可以看看该软件，在http：//www.r-project.org可以下载，并查看一下本章最后关于它的参考资料。

R提供了很多优秀的工具来查询数据的内在涵义。通过交互式注释：

Load the data＞data=read.delim（"http：//data.doloreslabs.com/face_scores.tsv"，sep="\t"）

and plot.＞plot(data)

给定记录的基本表，R的默认描绘动作是为我们提供每个变量组的散点图矩阵（见图17-2）。一个很突出的方面是其年龄关联看起来很滑稽——最右一列和最下面一行。

图　17-2：脸部数据的初始散点图矩阵

我们需要调查。第一件需要做的事是查看年龄值的分布（见图17-3）。

＞hist(data$age)

图　17-3：脸部数据年龄分布的初始直方图

这看起来不正常。由于存在游离点，x轴已经横向伸长到7000万。我们一起来看包含游离点的年龄值的记录：

Select records with age greater than 100.　＞data[which(data$age＞100），]

在初期，我们清除了非数值的年龄值，但是没有检查过分高的值。现在，最简单的操作方式是删除这些游离点。如果你之前从未使用过一种数据分析语言，注意R的丰富的下标标注，它使得基本的探索和清除变得简单有趣：

Subselect rows with age less than 100.　＞clean_data=data[which(data$age＜100），]

我们再一次检查了直方图（见图17-4）发现我们的绝大多数用户是（或者看起来是）18～30岁，这看起来很合乎情理。

图　17-4：清除后的脸部数据的年龄分布直方图

年龄、魅力和性别

我们想要放大一些观察到的最有趣的属性：年龄、性别和魅力。每当我们用一个表来包含一些有趣的字段，把数据作为一个散点图展现出来通常是非常直观且信息量丰富（见图17-5）：

Draw a scatterplot of age vs.attractiveness，＞plot(d$age,d$attractive，

using gender to define the points'colors.col=ifelse(d$male，'blue'，'deeppink'））

该散点图包含提示作用。举个例子，女人似乎比男人更有魅力。但是很难对任何事情有一个确定的答案，因为成千上万的点描绘时互相交叠在一起。当数据过载时，散点图可能会有误导作用。处理该问题的一种方法是平滑数据，通过画估计的分布图而不是点本身（见图17-6）。我们使用称为核密度估计(krnel density estimation)的标准技术。

Lay out side-by-side plots.＞par(mfrow=c（1，2））

For males and females，＞dm=d[d$male，]；df=d[d$female，]

draw smoothed plots，＞smoothScatter(df$age,df$attractive，

with a color gradient,colramp=colorRampPalette(c（"white"，"deeppink"）），

and aligned axes.ylim=c（0，4））

＞smoothScatter(dm$age,dm$attractive，

colramp=colorRampPalette(c（"white"，"blue"）），ylim=c（0，4））

图　17-5：魅力和年龄关系的散点图，通过性别进行着色（见彩图59）

图　17-6：魅力和年龄关系的平滑散点图，每个性别一个图（见彩图60）

我们还可以试着把它们放到同一张图上（见图17-7）：

＞smoothScatterMult(d$age,d$attractive,d$male,blendFun=bl_burn,colramps=

c(colorRampPalette(c（"white"，"red"）），colorRampPalette(c（"white"，"blue"）），

colorRampPalette(c（"white"，"green"））），pch=""，nrpoints=10000）

图　17-7：魅力和年龄关系的平滑散点图，通过性别着色，在一张图上交叠显示（见彩图61）

这些图形显示了数据的完全分布，但是很难看出其中的模式。举个例子，年龄如何影响魅力？通过计算总结统一和绘制它们更容易看出这一点（见图17-8a）。

For males　＞dm=d[which(d$male)，]

and females，　＞df=d[which(d$female)，]

average across faces　＞male_avg_by_year=by(dm$attractive，

within bins　cut(dm$age,breaks=0：80），mean)

(oe per year)　＞female_avg_by_year=by(df$attractive，

then　cut(df$age,breaks=0：80），mean)

plot them　＞plot(male_avg_by_year,col='blue'）

all together.　＞points(female_avg_by_year,col='deeppink'）

这个图可以开始描述一个故事了，但还是有点难以理解。有些点是成千上万张脸当中的平均年龄值，而有些年龄更大的点来自少量的观察。因此，图的右侧有更多的噪音数据，因为样本更小。

我们给该图增加了两个特征（见图17-8b）。首先，我们计算了95%的置信区间，确保我们不是欺骗自己去看来自于噪音数据的模式。置信区间是通过我们有限的数据来估计可能的均值范围的一种方式。其次，我们将拟合局部加权回归(lcally weighted regression,LOESS)曲线，从而有助于对这种噪音序列数据的聚集模式进行可视化。通常，我们可能会对数据进行线性回归，但是该数据不是线性的，而且看起来不像我们所知道的任何一个函数。局部加权回归函数是拟合任意曲线到数据的一种方法。它基本上是执行代价高的移动平均值。该图还不够完美。图的两边有一些点包含一个或者两个可以计算置信区间的样本。如果你查看图17-4的年龄直方图，这些并不奇怪——看起来超过50岁的人们只占数据集的1.7%。而且，很多置信区间很大，以至于它们的数据点不是很有意义。因此，对于包含很少数据点的区域——特别年轻的和年老的——我们分别使用5年和10年这两种更大的桶。该图看起来噪音数据就少多了（见图17-8c）。

图　17-8：魅力和年龄以及性别的关系的三种迭代绘图：a）平均值在桶内区间的每个年龄；b）每个桶95%置信区间，以及局部加权回归曲线；c）更大的桶，其中数据更稀疏（见彩图62）

一般来说，在所有年龄段中，除了婴儿，女人被认为比男人更有魅力。我们发现婴儿被认为是最有吸引力（魅力）的，但是该吸引力随着年龄增长而下降，直到18岁左右（可能用户对判定青少年为“有魅力”的觉得不太自在？），然后魅力又开始上升，在27岁达到最高值。在那以后，魅力值随着年龄下降，直到50岁，在50岁年龄点，魅力值似乎又开始上升。但是很难确定，因为大于50岁的数据非常稀疏。

当然，对于20岁左右的非文本属性，存在很多关系需要探索。我们可以描绘更多类似于图17-8的图，但是我们能否马上看到所有有趣的交互？我们还是采用两两交互的方法，改变之前描绘的两两结对图。我们不是在每个面板使用散点图，而是显示单一色彩表示属性之间的全局关联。蓝色是正向关联，而红色是负向关联（见图17-9）。

First compute pairwise correlations，　＞cors=cor(d,use='pair'）

and order the attributes to try to　＞ord=order.hclust(cors)

put similar attributes next to each other.　＞cors=cors[ord,ord]

Plot the correlation matrix，　＞image(cors,col=col.corrgram（7））

with axis labels.　＞axis（1，at=seq（0，1，length=nrow(cors)），labels=row.names(cors)）

图　17-9：Pearson关联矩阵，蓝色方块的属性对和上升的斜线是正向关联，而红色方块的属性对和下降的斜线是逆相关（见彩图63）

该绘图蕴涵了很多有趣的关联信息，这些关系值得进一步调查：

·女人被认为比男人更聪明。

·女人被认为更有可能在和狗打架中胜出。

·衣服尺寸和体重只是弱相关。

·女人更有可能被雇佣为安全巡警。

·那些看起来做过整容手术的人被雇佣为安全巡警的概率更小。

值得信任、聪明、有才华、显得年轻、富有和保守之间都两两关联。一条“责任心·轴”？

观察标签

除了所有顺序和数值的数据，我们包含一组自由格式的标签，用户能够输入人的照片。标签范围从描述性的（“雀斑”、“鼻子带环”）到粗俗的（“抱我上床吧”、“肮脏的胸口”）到友好的（“你穿着红色的衣服很漂亮”）到建议性的（“修修头发”、“避免在太阳下暴晒”）到评论性的（“太让人惊叹了！”、“Nancy，够了！”）到卑鄙的（“那个肥胖的朋友”）到没有意义的（“……”、“plokmnjiuhbygvt fcrdxeszwaq”）。通常来说，自由格式的文本数据处理上更复杂。第一件要做的事是检查标签的分布。最常见的标签是什么？

Load our tags　＞face_tags=read.delim（"face_tags.tsv"，sep="\t"，as.is=T）

then count　＞counts=table(face_tags$tag)

and rank them.　＞sorted_counts=sort(counts,decreasing=T）

Show the most common tags.　＞sorted_counts[1：20]

以下表包含了输出结果。

最不常见的标签是什么？

Show the least common tags.　＞tail(sorted_count，20）

查看一些标签会对范化有问题。“cute”和“Cute”是否应该归并为相同的标签？标点符号是否应该全部丢掉？看起来很滑稽的亚洲字体的全角问号（?）是否应该认为和ASCII的问号（?）一样？显然，这取决于应用。只要可能，我们的本能是慎之又慎，保留原始数据的完整性。这保护了信息；举个例子，标签“hot”和“HOT！”肯定包含不同的语义内容。通常，当针对特定的可视化或者分析，对数据进行归并是更容易的，而不是提前猜测所有的需求，并且被强迫撤销所有值钱的范化决策。

标签分布的基本绘图查看的是标签的频率相对于其频率的排序。通常来说，当对单词或者其他词汇项进行计数时，我们看到的是从最频繁出现的单词很快下降到最不频繁出现的单词。在我们的数据，总共有240万的标签，其中有29万个唯一标签。最高的1000个唯一标签出现频率是140万——超过所有标签的一半。而在这些标签的出现频率中，有非常显著的下降趋势。从常用标签表中，我们看到最常见的标签“cute”出现了3.6万次，而第二常见的标签“pretty”出现的频率只有它的一半。

For the top 1，000 tags，　＞s=sorted_counts[1：1000]

draw a plot of their counts.　＞barplot(s)

1935年，语言学家George Zipf观察到词频分布遵循“幂函数定律”(pwer law)，其中第n个单词的频率和（1/ns)成正比，其中s是个常量。和高斯分布不同，该分布包含有限的变量，这使得它不适合于某些统计算法。流行的书如Nassim Nicholas Taleb的《The Black Swan》(Rndom House出版）和Chris Anderson的《The Long Tail》(Hperion出版）使得这些分布分别以“重尾”和“长尾”分布而著名。实际上，我们的数据包含一条很长的长尾：22万个单词，或者词汇的76%只出现一次。

图　17-10：出现频率最高的1000个标签的标签频率

通过在对数空间描绘我们的单词的出现频率图，我们可以查看这些单词出现的频率是否满足幂函数定律分布（见图17-11）：

Plot log ranks　＞log_ranks=log（1：length(sorted_counts)）

against log frequency.　＞plot(log_ranks,log(sorted_counts)）

图　17-11：通过对数排序的标签的对数频率，包含幂函数定律模型的拟合线

幂函数定律分布在对数-对数空间上，看起来应该是线性的：

Fit a model of log count against log rank　＞model=lm(log(sorted_counts)～log_ranks)

and draw it on Figure 17-10.　＞abline(model)

我们发现标签的概率很接近（1/n^80）分布。（如果你觉得它看起来像最佳拟合线，记住所有点的76%是在数据的最右下侧的边上）

如果你在任何文本——报纸、小说、Web页面等上描绘这种对数-对数频率的绘图——它看起来都很相似[1]。当然，当FaceStat的用户写描述标签时，它们参与的是一种语言行为，这种行为和其他类型的人类通信在本质上很相似。

这些标签如何拟合剩余的数据？第一步需要做的是从标签中随机选取样本，在我们已经生成的绘图上绘制（见图17-12）。

图　17-12：基于平滑后的魅力与年龄描绘的标签样本图（见彩图64）

在该图中，深暗色表示政治背景和魅力的密度的全局分布。单词是从分布中随机抽样出来的。蓝色词表示男性标签，粉色词表示女性标签。这么做可以使我们感觉到标签是否和图中的变量一致。数据看起来很合理，标签“平均”显示在图形的中央，而被标记为“接近顶尖”的人显示在“自由/魅力”象限，而标记为“土里土气的”则是在“保守/丑陋”象限。该图可以通过不同的随机种子生成多次，可以查看全局数据的标签分布。

[1]Zipf,George.1935.《The Psychobiology of Language》(MT出版社）参见http：//en.wikipedia.org/wiki/Zipf's_law。

哪些单词具有性别化

很多社会理论学家考虑性别在语言中的不同显示的程度如何。我们的数据集使得可以从单词层次探索这一点：我们可以发现哪个标签是男性脸孔或者女性脸孔的最显著的特征。我们可以统计出对于男性出现最频繁的单词，以及对于女性出现最频繁的单词，但通常来说，这只能获取在任意地方出现频率都很高的单词。一种更好的方式是通过对性别间出现频率的标签进行打分。也就是说，为了确定标签T表示性别G的特征性，看以下表达式：

这个表达式有个缺陷：出现频率很低的标签会带来噪音数据。举个例子，任何只出现一次的标签，它表示的性别就得到满分1（这是由于样本数据量小造成的错误，我们称之为稀疏年龄桶）。解决该问题的一个简单的方法是设置词频阈值。在这种情况下，我们只查看出现次数大于100次的标签。

计算这些分值——用统计学术语来说，它们是条件概率Pr(G|T）的最大可能估计——我们可以获得如下一些最大可能估计值表。

最具有男性特征的单词如下表所示。

最具有女性特征的单词如下表所示：

可能令人惊讶的是这些单词如“handsome”（英俊）、“gamer”（赌徒）、“Bubbly”（活泼的）和“slut”（骚货）是多么地具有性别化特征。它们几乎总是和它们所表示的性别一起出现。

聚类

数据中的人们有哪些典型类型？聚类是找到这种模式的强大的统计方法。聚类算法通过将相似的实例分组在一起，从而把数据点分成几种特征类。聚类有很多方法，但是最流行和简单的方法称为K均值聚类（K-means)方法。在K均值聚类方法中，每个聚合有一个中心点，称为“质心”(cntroid)。在数据中找到一些不同的质心，每个数据点被分配给一个质心。该算法迭代调整聚合，这样使得尽可能多的数据点靠近分配给它们的质心。

在我们的数据集中，每张脸大约有20个数值属性。因此，脸部就是在20维空间的点。K均值聚类方法会把脸孔放到该空间的不同的聚合，试着选择聚合，使得脸孔可以尽可能地和它们所在的聚合的中央相似。

K均值聚类算法不好的一点是你必须选定固定值的聚合个数——“K”。然而，不存在很明显的方式来选择聚合的个数。需要做的最好的事是尝试集中不同的个数，查看出现了什么模式。以下是运行K均值分类算法的一个合理输出：

Preprocess the data，＞norm_data=apply(d，2，function(x){

by changing missing values to the mean,x[is.na(x)]=mean(x,na.rm=TRUE)

and unit-normalizing values,x=（x-mean(x)）/sd(x)

which usually makes k-means work better.x}）

Then run k-means for 5 clusters，＞clus=kmeans(norm_data，5）

and plot attractiveness vs.age，＞plot(d$age,d$attractive，

but color by col=c（"red"，"purple"，"blue"，"orange"，

cluster assignment，"green"，"darkturquoise"）[clus$cluster]，

and have fun with unicode.pch=ifelse(d$male，'\u2642'，'\u2640'））

散点图的点表示脸部，点的颜色和它们被分配的聚合一致。我们将显示在魅力和年龄空间内的脸庞，如我们之前的绘图一样。一些聚合已经可以理解：橙色聚合表示年长的人，紫色看起来是有魅力的年轻人等。

这个绘图只显示数据的二维或者三维，因此没有充分地总结聚类算法，聚类算法完整地比较了20维空间的人脸。这是一些聚类重叠(oerlap)的原因：举个例子，红色和绿色看起来有非常相似的年龄和魅力范围。这些聚合必须通过其他属性区分。

我们通过各种方式来分别查看各个聚合。首先，我们显示聚合的属性权重。它是聚合的质心点的位置，质心点通常被认为是该聚合表示人脸的典型属性。因此，如果你查看图17-13的每个集群的平均点，它会返回年龄和魅力的聚合权重。（我们将显示八个属性；其他属性不重要，因为它们包含太多的缺失值。）其次，我们显示了在该集群中的人脸的最显著的10个特征，通过如之前的性别分析那样的条件概率进行排序。

图　17-13：魅力和年龄的关系，通过集群着色，显示了包含2000个点的子样本（见彩图65）

首先，图17-14显示的是紫色集群。这是一个非常女性化、具有高度魅力值的集群。标签很有趣。它们和属性惊人的相似；事实上，如果覆盖左侧图，你很可能猜出该组的很多属性。标签显示了一致生动的画面——即使我们的k均值聚类算法完全忽略了这个信息！这说明了标签和社会属性有内在的联系。（这一点可能不足为怪。）

图　17-14：集群2

我们在图17-15和图17-16展示了所有集群，包括每个集群中四个最有代表性的脸孔（表示最接近质心点的）。

图　17-15：集群的质心点、标签和例子（见彩图66）

有些集群有直接的解释，而另一些则不是很清晰：

紫色集群

年轻、有魅力的女性。

蓝色集群

丑陋、智商低的男人（“失败者”？）。

图　17-16：集群的质心点、标签和例子（见彩图67）

绿色集群

其他更普通的年轻男人。它的很多标签对于蓝色集群也非常有可能。

红色集群

其他年女性。

橙色集群

较年老的女性。

青绿色集群

较年老的男性。

聚类一方面用于发现数据高纬度的模式或者分组会很有用，因为这些模式和分组无法通过两个维度进行可视化。另一方面，很难验证聚类是否告诉你一些“真实”的东西。存在很多聚类算法和很多参数调整（比如k），不同的调整会生成不同的结果。这项工作是否有用？

集群看起来似乎相当一致，而且对于很多模式具有提示作用。观看那些生动的标签集合互相关联很有趣。而k均值聚类算法可能可以模拟我们的大脑中对人类特征的思考。从质心点和标签集合，我们可以想象表示每个集群的典型的人类特征。

结论

数据表明，一些人持有一些常见的“偏见”，认为女性比男性更有魅力。年龄对于魅力的影响，女性比男性高。社会属性的空间和我们所熟悉的很一致：乡下小伙、父辈、有魅力的年轻女性。但还是有一些潜在的令人惊讶的地方：婴儿最有吸引力、看起来保守的智商更高等。我们还发现一些性别化单词的例子。

我们试着一直继续探索这些有提示性的发现，但是本章不会给出任何特定的结论。相反地，我们想要显示在关于人们的判断的大量、混杂的数据中包含的有意义的模式的富集合的一些例子。更严格的数据收集处理（如谨慎控制的实验）将永远都无法生成如此大量的数据，因此对于后续的实验没有太大作用。

每天我们都通过自己购买的东西、使用的Web站点、搜索的查询、发送的消息和去的地方方方面面地展示自己。不论我们喜欢与否，在人类历史上，这是第一次把所有的数据都精心保存起来。暂不考虑重要的隐私问题，该数据对于社会科学的价值是非常巨大的。通过这种改变用途的信息的混乱，我们将以全新的方式了解自己。

致谢

非常感谢给本章初稿反馈的所有人：J o a n n a G u b m a n、S a s h a G o o d m a n、J e f f Hammerbacher、Mike Love、Will Moffat和Toby Segaran。FaceStat的存在归功于工作努力且杰出的同事Chris Van Pelt。

参考文献

我们上传了部分数据子集以及一些笔记和代码来帮助重现我们的分析，在：http：//data.doloreslabs.com。

如果你对学习建模工具R感兴趣，我们推荐两个Web站点：

Quick-R(http：//statmethods. net)

Robert Kabacoff写的高级概要和主题指南。

RSeek(http：//rseek. org)

Sasha Goodman实现的R文档、包和邮件列表的搜索引擎。

统计工具R的官方Web站点可以通过http：//www.r-project.org访问。如果你对它和其他的数据分析包比较感兴趣，请参见表17-2的很多评论，可以通过http：//anyall.org/blog/?p=421访问。

最受推荐的学习R的书是Peter Dalgaard的《Introductory Statistics with R》(Sringer出版，2008）。

除了统计工具R的核心功能，我们使用的一些附加包包括corrgram、flowCore、gclus、geneplotter、plyr和pixmap。

Trevor Hastie、Robert Tibshirani和Jerome Friedman合著的《The Elements of Statistical Learning》(Sringer出版，2008）包含了聚类、局部加权回归法的优秀的概要总结以及其他机器学习技术。

关于标签部分仅仅涉及统计语言分析最基础的部分。为了了解更多信息，可以参考Christopher Manning和Hinrich Schütze著的《Foundations of Statistical Natural Language Processing》语料语言学一章(MT出版，1999），以及Daniel Jurafsky和James H.Martin著的《Speech and Language Processing》(Pentice Hall出版社，2008）。

对于魅力和年龄的变化关系分析，存在很多更好的方式来估计置信区间。一种方法是部分集中(prtial pooling)；参考Andrew Gelman和Jennifer Hill的《Data Analysis Using Regression and Multilevel/Hierarchical Models》（剑桥大学出版社，2006）的252～258页。我们在本章所做的称为“探索性数据分析”(eploratory data analysis,EDA)——和那些通常在统计方法学课程上教的精心实践的假设检验不同。探索性数据分析被统计学家John Tukey在1977年以之命名（译注：即《Exploratory Data Analysis》）(Adison-Wesley出版）的书中广泛推崇。

我们的创业公司Dolores Labs专注于外包项目：从大量的人中收集人类的任务数据来解决内容控制、信息抽取、Web搜索相关性以及其他领域的实际问题。我们收集、查看并自动分析大量关于人们的判断的数据。你可以查看本章的后续，关于其他主题如性别、颜色和宗教的分析，在博客http：//blog.doloreslabs.com可以获得。

第18章　旧金山海湾区之殇：次贷危机的影响

HadleyWickham、Deborah F. Swayne和David Poole

引言

房地产市场在过去几年得到了媒体的极大关注。大约从2000～2006年，随着价格的飙升，我们既惊奇又焦虑地看着房地产行业。从那时开始，随着信用成为稀缺以及丧失抵押品赎回权的蔓延，房地产业也跌入了冬天。在本章中，我们通过分析从2003～2008年旧金山海湾地区50万的住宅销售来近距离探讨这个“故事”。从一个地区的价格在一个很大价格范围内上升和下降的方式，我们可以学到些什么呢？

我们将从描述数据、如何获取数据以及如何通过重构、转化、清除和增强提升原始数据开始，从而为数据分析做准备。随着分析逐渐深入，我们的大多数发现都会通过图像形式展示。在此过程中，还会介绍一下我们使用的一些工具，这些工具多数都是可以免费获取的。我们的主要工具是R，它提供了统计编程和数据分析环境，在数据分析的各个阶段中都用到了这个工具：获取、清除、分析、诊断和表示。

我们是如何获取数据的

在我们确定对房地产的实际销售数据感兴趣之后，数据搜索过程马上随之展开。数据搜索并不总是成功的，因此当我们发现了《San Francisco Chronicle》杂志生成的每周海湾地区(By Area)的住宅房地产（住房、公寓、托管公寓）的销售情况（在http：//www.sfgate.com/homesales/可访问）之后，我们觉得特别的幸运。当我们得知自己不需要通过解析Web页面来抽取数据，而是已经可以获取计算机可读格式的数据时，我们感觉自己更幸运了。

每个人可读的(HML Web页面）每周总结都是基于如下的文本文件构建的：

rowid：1

county:Alameda County

city:Alameda

newcity：1

zip：94501

street：1220 Broadway

price：$509，000

br：4

lsqft：4420

bsqft：1834

year：1910

每个星期的数据可以使用如下模式的URL获取：http：//www.sfgate.com/c/a/＜year＞/＜month＞/＜day＞/REHS.tbl。这非常方便，因为只需要生成一个从他们开始记录的日期2003/04/27（我们从他们网站的归档页面中找到的）到最近2008/11/16（截止到分析时间）的所有星期天的日期列表。有了这个日期列表，我们生成正确格式的URL列表，通过Unix命令行工具Wget就可以下载它们了。我们之所以选用wget是因为它支持断点续传功能，这样当下载中断时，可以很容易地从上一次下载到的位置继续开始而不是从头开始。

所有的数据都保存在本地计算机上，下一步是把数据转换成标准格式。我们通常使用csv（逗号分隔值，comma-separated values)格式；csv文件很容易生成，而且为各种统计包（包括Excel！）所支持。我们生成了一个csv格式的文件，如下：

county,city,zip,street,price,br,lsqft,bsqft,year,date,datesold

Alameda County,Alameda，94501，1220 Broadway，509000，4，4420，1834，1910，2003-04-27，NA

Alameda County,Alameda，94501，429 Fair Haven Road，504000，4，6300，1411，1964，2003-04-

27，NA

Alameda County,Alameda，94501，2804 Fernside Boulevard，526000，2，4000，1272，1941，2003-

04-27，NA

Alameda County,Alameda，94501，1316 Grove Street，637000，3，2700，1168，1910，2003-04-

27，NA

可能原始的数据格式更适合人们阅读，但是这种csv格式更适合计算机处理。它不但更标准，而且更紧凑（c s v格式的数据大小是45M B，而原始数据格式的数据大小是90MB)。如果你仔细查看样本数据，你可能会注意到有些东西需要解释：NAs。NA代表“不适用”(Nt Applicable)，而标记值R用于表示缺失值。在分析中我们必须考虑缺失值。

解析所有293周的数据并创建结果文件house-sales.csv只需要花费几分钟的时间，该csv文件包含521726个观察和11组变量。调整解析器使得它满足所有的边界情况，这个过程花费了更多的时间：我们需要把价格格式的数据转换成常用的数字（通过删除“$”和点“.”符号），把时间解析成一致的格式，而且为没有在所有的表都出现的字段填充缺失值。

地理编码

当第一次查看数据时，我们认为对所有的436106个唯一地址进行地理编码是非常重要的。也就是说，我们想要为每一个地址关联一个经度和纬度值，这样可以很容易地探索细粒度的空间效果。这是一个很有趣的挑战：你如何对将近50万的地址进行地理编码呢？

我们从查看G公司和Yahoo！提供的Web服务开始。这些服务不适合有两个原因：一是它们对于每天的请求个数有严格的限制；二是使用结果数据时存在繁琐的限制约束。单单请求限制就意味着对所有的地址进行地理编码可能需要花费1个多月的时间，而且其授权将会影响到公开发布结果！进一步调研之后，我们发现一项非常有用的开源服务，USC WebGIS，它是由南加州大学的GIS研究实验室提供的(Gldberg和Wilson 2008）。该服务对于非商业使用是免费的，而且对于结果数据的使用没有什么约束。我们开始使用该服务，不存在每天的使用上限，但是由于速度原因存在一个隐含的上限：我们每天只能够对大约8万条地址进行地理编码，因此为总共40多万条数据进行地理编码花费了5天多的时间。该免费服务的缺点是地理编码的质量不是很好（它只使用了公开可以访问的地址数据），但是该服务的创始人很乐于帮助别人，并且发布了该服务主题的优秀的介绍文档(Gldberg 2008）。

除了经度和纬度，USC WebGIS服务提供的结果中还包含了用来描述结果本身准确程度的一个类别变量：精确地址、邮政编码、区县等。

数据检查

对于每个阶段的数据分析，通常而言，花一些时间来确保数据的准确性是完全必要的，地理编码也不例外。地理编码的错误来自于很多方面：地址中存在拓扑错误，新的建筑通常不会列在公开的数据库中，邮政编码随着时间可能变更。我们在使用USC WebGIS过程中，怀疑该软件可能存在一个bug，因为大量的地址被错误地分配到该州的洛杉矶及其他地区；我们使用另一个免费在线服务http：//gpsvisualizer.com对这些地址进行了重映射。调试过程中，为了识别在旧金山海湾区很远以外的地址，我们使用统计建模工具R来为每个区县以及绝大多数的城镇描绘经度和纬度的简单地图。

San Jose地区的地址提出了一个有趣的地理编码挑战。如一些“城镇”的销售列表，我们找到的所有映射网站都无法识别这些信息，因此我们假定它们是社区的非正式名称：San Jose的北部、南部、东部和西部，Berryessa、Cambrian和一些其他地区。

只要可能，我们就试着纠正任何错误。当无法纠正错误时，我们就使用统计建模工具R的缺失值来表示我们不知道该地区准确的经纬度值。这种处理方式比扔到那些“错误匹配”更好，因为不同目的需要有不同的准确度：当我们以区县或者城市的粒度对数据进行映射时，只要有很接近的地理位置就可以。为经度和纬度使用缺失值确保任何包含可疑的地理编码的地区都会从使用经纬度的分析中删除，但是在其他情况中，都会包含这些缺失值。

分析

为了对房地产市场变化有更广泛的概览，我们从平均销售价格和销售量谈起。因为数据是以周为单位生成的，这是一个自然的时间单位。

图18-1显示了293周数据中每周的平均销售价格和销售量。这里存在一些非常有趣的模式。平均价格的行为非常显著：一直呈上升趋势直到2007年7月，然后是急剧下降，直到现在——房地产市场价格的繁荣和萧条的一个明显的例证。

销售情况则完全不同。大多数年份（尤其是2004年和2005年）显示非常明显的季节效应，夏季中下旬达到高峰，而冬季的销售量则减少。（在这里需要做一个说明，我们使用的这份数据中只有很少一部分提供了真正的截止日期，因此我们使用了在报纸中报道的销售日期，这个日期可能在截止日期的4～6周之后。）一旦我们考虑过去的季节性影响，我们可以看到一些其他方面。从2006年中～2008年初，销售量下降，当然意味着房地产的萧条。但是，在2008年初的销售急剧下降，也预示了冬季销售量的下跌。2008年初的销售增加是怎么回事呢？一个可能是那时房价已经下降了很多，买家在春天到来前购买降价商品。销售量增加的另一种可能是由于有些房子丧失了抵押品赎回权。可能在未来几个月之后，对于给定问题的解释会逐步明朗起来。

这些简单的绘图为进一步探索提出了一些方向。这些模式在所有价格区间对于所有家庭都一样吗？那不同的城市或者是一个城市的社区内会是什么情况呢？为了调查这些问题，我们将遵循相同的过程：我们将通过不同的方式对数据进行分区，比较不同分区的模式。我们将基于房屋价格和物理位置创建分区（从最贵的到最便宜的），都是城市之间和一个城市内的（旧金山）。

通货膨胀的影响

图　18-1：每周的平均价格（上图）和销售量（下图），住房市场繁荣和萧条的明显证据。注意在2008年的销售上升

在进一步分析之前，我们停下来考虑一下通货膨胀。数据是在相对较短的时间周期（大约6年）内收集的，但是我们在思考是否要根据通货膨胀对价格指数做出调整，以便确保2003年支付的价格和2008年支付的价格具有可比性。常用的计算通货膨胀方法是参考消费者价格指数(Cnsumer Price Index,CPI)，它是由劳工统计局生成的，可以访问http：//www.bls.gov/CPI获取。CPI计算经常购买的消费者产品和服务的加权的“桶”的价格。该价格是每个月计算一次，我们将使用西海岸序列(CUR0400SA0序列）为通货膨胀对数据做调整。我们想把所有的值都调整为2003年的价格，因此，我们把每个CPI值除以其在2003年3月相应的价格值。这种操作被称为“相关价格指数化”(Idexing)。它得到的是2003年1美元在每个时间点的相对价值，使得可以很容易看懂图中的通货膨胀效果：值1.1表示从数据起始开始，累积通货膨胀达到10%。图18-2显示基于CPI的通货膨胀衡量尺度以及为通货膨胀调整价格的影响。通货膨胀在过去5年一直稳定增长，我们可以发现根据通货膨胀调整的房价的增长趋势稍微低于未调整的趋势。最后，我们还是决定不按照通货膨胀调整销售价格。住房价格对于CPI有影响，因为它的一个子指数是住房指数，它是出租和“拥有者的等价出租”的尺度。可能有人会说住房价格对于CPI在整个研究期间内有深远的影响。

图　18-2：（上图）通货膨胀，在序列开始时价格相关指数化为1；（下图）以2003年价格为标准根据通货膨胀调整的住房价格（红色）和未调整的价格（蓝色）。没有根据通货膨胀调整价格使得价格上升趋势看起来更陡峭，但是对于价格下降几乎没有任何影响。不包含Monterey、San Benito、San Joaquin和Santa Cruz区县，因为我们只有这些区县的2008年的数据

有了这些基本概况，我们现在开始深入到细节。在本章的后面部分，我们把住房销售划分成几个小的群体，首先是通过价格，然后是通过地理位置。我们对于发现住房市场是否对某些业主群体比其他人带来更多的影响感兴趣。

富者更富，穷者更穷

住房危机给富人带来的影响和穷人带来的影响是相同的吗？该危机的影响是否改善或者恶化这两个群体的相对平等性？在本章中，我们将探索危机如何影响住房价格的分布。一个重要的提醒是我们查看的是洛杉矶海湾区，因此，其住房价格会比该区的其他地方的价格高得多，但是我们仍然期望能够观察到一些相对的不平等性。（注意：在以下部分，我们将频繁地使用单词“住房”(huses)来泛指住宅房地产的所有领域：住房、别墅、公寓等。）

我们先计算每个月的价格十分位数。该十分位数分别是住房价格下降10%、20%、30%、40%、50%、60%、70%、80%和90%。这是对每月价格分布的简明摘要：不是像我们之前所做的那样查看平均价格，而是通过9个数值来总结价格的完整分布。（我们没有显示最低价格或者最高价格的曲线，因为这些价格太不稳定。）

图18-3显示了这些十分位数如何随着时间变化。最高行是第九个十等分，价格低于住房价格的90%；最低行是第一个十等分，价格仅仅比住房价格低10%。中间行是平均值，该价格是住房价格的一半。线条颜色从深变浅，表示从最贵的到最便宜的。每条线有相似的模式，我们可以观察到受2007年的住房泡沫的影响，尤其是价格最高的住房。

图　18-3：每个百分点内的每月平均住房价格。百分点越低，颜色越浅。该绘图很清晰地显示了价格更高的住宅的泡沫的本质，但是对于最低价格区间，它不是很明显

该绘图允许我们比较每个十分位数的绝对值，但是查看相对价格可能更合适：价格如何相应的变化？查看相对价格的一种方式是比较每个十分位数和它的初始值。为了实现这一点，我们对每个十分位数指数化，通过其初始值划分序列，正如我们计算消费价格指数一样。图18-4显示了这些指数。每个十分数起始位1.0，我们可以看到随时间的价格相对变化。该图的一个很有趣的一面是价格更低的住房（浅色线条）所达到的峰值更高更早（2005年中），然后在后期下降更快。（注意深色和浅色线条在2007年早期的位置交替。）最便宜的住房，即在最低的十分数，损失值相当于2003年价值的43%，而最昂贵的住房只损失了9%。比较图18-3和图18-4，我们发现虽然在实际价格中，最大的绝对下降值发生在最贵的住房，但是，最便宜的住房相对损失值是最大的。

图18-5是查看该不平等性的另一种方式。我们把所有的价格都除以住房平均价格。其值表示平均住房价格：值1.2表示价格高出平均价格20%，0.8表示低于平均价格20%。自从2007年年初，虽然价格高的住房形式还很繁荣，相对不平等性已经开始增长。这是否意味着住房高的和住房低的价格差距更大是后继产生危机的前兆？该情况是否暗示了其他危机？这些问题可以进一步研究，但是我们这里没有数据来对它们进行深入探索。

图　18-4：每个十分数内指数化的住房价格（线条颜色越浅，价格越低）萧条期在价格低的住房中首先发生：价值越低的住房的平均价格上升的峰值越高而且越早，下降更快且幅度更大

图　18-5：住房价格相对于中等价格的住房的价格。住房价格上存在的差距从2007年早期开始一直增长

地理区别

本节中我们将探索海湾地区不同城市的住房价格变化。因为我们查看的是平均价格，我们必须注意不要包含只有很少的销售量的城市。我们决定重点放在每周平均销售量至少10套的所有城市。这种方式最后筛选出58个城市（总共是245个城市，占24%），共428415套住房销售量（占销售总量的82%）。

然后，我们计算了以周为单位的平均房价。图18-6显示了这些价格，每个城市使用一条不同的曲线来描绘。统计学家为这种展示方式起了一个很生动的名字：“意大利面条曲线”。很难从一大堆线条中查看任何东西。对这种曲线的一种改进方法是对每条线进行平滑，删除短期的变化，使我们可以重点观察长期趋势。

图　18-6：每个城市的每周平均销售价格。这种类型的绘图通常被称为“意大利面条曲线”。它需要平滑，因为曲线中这种“周到周”之间的变化无法检测出趋势

为了创建平滑的曲线，我们使用了广义的可加模型(GM)，它是对线性模型的范化(Wod 2006）。广义可加模型通过优化权衡切近数据并且线条平滑的方式来拟合线条曲线。实际上，它消除了噪音数据带来的短期影响，并且将重点集中于长期趋势。这正是我们所需要的：我们对每天的或者每周的变化不感兴趣，而只对和住房危机相关的长期变化感兴趣。

图18-7上方显示了平滑化后的结果。这是一个很大的改善。现在我们可以真正看到一些模式！注意该图和第一个图在规模上的重大区别：平滑数据已经删除了一些非常昂贵的住房的销售高峰的描述。我们还会对每个城市指数化，正如我们之前对每个十分数指数化那样：通过相对初始价格进行划分，使得每个城市可以有一个共同的规模，允许我们把重点放在变化上。这一点显示在图18-7下方。

现在还存在很多变化，但是我们可以开始看到增长值模式直到2007年年中，然后后期值开始下降。为了进一步探讨，我们需要分别查看这些城市，如图18-8所示。该曲线图占了很大空间，但是它提供了很多额外的信息，因此是值得的。我们可以挑选出一些有趣的模式：伯克利和旧金山显示的高峰值较低，而下降值较少；而Mountain View的独特之处在于它的住房价格一点都没有下降。其他城市如Oakley、Vallejo和San Pablo，则是大起大落，高峰值很高，而跌落值也很大。

回想一下我们之前关于San Jose城市的讨论，我们注意到，原始数据本身描述了San Jose城市的很多社区。由于这一点，有时相同的地址被分配给多个社区，但是该数据还表明这些社区具有鲜明的特点。Berryessa、东San Jose、北San Jose和南San Jose有相似的曲线，呈现出很鲜明的高峰和一样鲜明的下降；而另一方面，Cambrian、San Jose和西San Jose则没有出现这么大幅度的下降。

经过进一步调查，我们发现一个使得城市之间相互区别的主要特征：在繁荣期价格之间的区别以及它们最近的下降幅度。我们创建了一个新的变量，称为“价格回落”，它表示在2006年2月（繁荣期的顶峰）和2008年11月（写本章内容时的“低谷”）之间的平均价格的相对下降。图18-9通过新的变量对城市进行分组。这些城市的划分是随机的，但是你可以看到每组城市如何遵循相似的模式：繁荣越鼎盛，崩溃越惨烈。这意味着这单个数字可以很好地总结住房危机的繁荣和萧条方面。

图　18-7：平滑后的每周销售价格，每条曲线表示一个城市（上图）。下图底部的曲线进行了指数化来显示价格的变化比例。模式开始出现了

我们已经确定城市具有不同的模式，但是还不知道产生这种现象的可能原因。地理模式，如图18-10所示，没有显示任何特别突出的，除了受灾最严重的地区通常是旧金山市的北部和东部。这并不能提供有力的说服力，因此我们需要寻找更多的数据，可以帮助我们获得更深入的理解。

人口普查信息

美国人口普查局提供了在县、市两个级别的最新调查的人口统计信息。quickfacts网站（比如http：//quickfacts.census.gov/qfd/states/06/0649670.html)显示了每个城市的很多有趣的人口统计变量。遗憾的是，城市级别的数据无法简单地通过下载获取，但是我们可以采用脚本（正如那些我们使用的销售数据）来收集人口统计信息，并把它转化为csv格式。此外，在人口普查数据和销售数据中，城市的定义有一点区别，因此我们从全部58个城市中只匹配了46个。人口统计数据没有覆盖我们选择的一些城市，因为这些城市的人口低于某些截断值（阈值）；而另一方面正如我们之前讨论San Jose城市时所指出的，一些住房数据称之为“城市”的，实际上是更大城市范围内的社区。

图　18-8：每个城市的销售价格图，已经进行了平滑化和指数化。这些曲线和图18-7中下方的图的各条交叠的曲线（意大利面条曲线）完全相同。San Pablo的曲线显示住房市场的繁荣和萧条；伯克利的曲线显示变化幅度更小；Mountain View则是唯一一个价格还在上升的城市

图　18-9：根据城镇的价格回落对曲线进行分组图。图中左上方的城镇价格下降最大（在0.8和1之间，或80%打破100%）；图中右下方的城镇(Muntain View)是唯一价格没有下降的。每个组内的模式是相似的，表明单个数字提供了很有用的方式把城市划分成组

图　18-10：价格回落的地理分布。降价最严重的城镇是北部和东部。单个环表示Mountain View的地理位置，它是唯一销售价格继续增长的城市

一看人口统计变量就能发现影响最严重的城市有大量的婴儿和小孩，家庭成员更多，学士学位更少，交通时间更长。更重要的是，这些城市的平均收入也更低，这可能是导致其他关系的主要因素。图18-11包含了三个散点图，说明了住房价格回落和收入、大学生比例以及交通时间的关系。价格回落和交通之间的关联度很低，但是注意交通时间最长（超过35分钟）的所有城市在价格上有特别大的回落。住房危机看起来在更贫困的地区，受到的冲击影响更大。

县级人口普查数据包含比城市数据更多的变量，因此我们分析了县级数据来进一步解释住房危机。图18-12中上方的图显示，对于我们有销售数据的任何县，描绘了住房单元个数的变化比例（从2000～2006年）和2008年的平均销售价格比较。在最近住房价值和新建筑数据之间存在很强的负面关系。换句话说，最近几年绝大多数的建筑繁荣发生在更贫困的社区，正如我们之前所述，有些地区其后继的价格疲软是最严重的。特别是San Joaquin县，它的城镇价格都很低，从最近几年到现在为止，有更多的新建筑。应该注意的是，这些城市中，有些我们没有很多的销售数据（比如San Benito和Santa Cruz)，但是这些关系的全局的本质还是非常清晰的。图18-12中下方的图更进一步说明了这个效果，它也显示了住房单元从2000～2006年的比例变化，但是它是和县级的2005年人均收入比较，从人口普查数据中获得。我们注意到该图和前一个图存在相似性，而且它进一步说明了新建筑的密度在相对不富裕的地区更大，即使将统计的聚集级别从城市扩大到县级时，仍然具有上述特征。

显然，价格和收入是强正相关，从图18-11的城市等级上可以观察到。

根据《New York Times》的一篇文章(MKinley 2007），Stockton城市，San Joaquin省最大的城市之一，到2007年夏天，已经是美国丧失抵押品赎回权比例最高的城市了。

遗憾的是，我们没有Stockton城市在2008年之前的销售数据，但是直到2008年，看起来它仍然是该地区持续低迷的主要代表。Stockton城市的人口在过去10年增长迅速，为了逃避海湾地区过热的房地产市场，上下班的人们住的地方越来越偏远。这有助于解释之前提出的新建筑，而且也把我们的观察和上下班时间关联起来。该文章还列举了Modesto和Merced城市，在中央山谷(Cntral Valley)的两个城镇，在当时都位列全国丧失抵押品赎回权数量最高的十大城市之中。

探索旧金山

探索了城市之间的区别，我们开始更详细地查看单个城市。旧金山是个很显然的选择：在我们包含的数据中它是最大的城市，也是我们最熟悉的城市，而且它包含一些标志性的特征，可以很容易为别人所识别。我们通过抽取旧金山的所有地址来开始探索，这些地址通过非常高的精确度进行编码，为我们总共提供25377个地址。我们创建了一个简单的经纬度散点图，如图18-13所示。

图　18-11：从上到下，住房价格的下降（价格回落）和平均收入、大学生比例和平均交通时间的关系

图　18-12：新的构建与最近价格（上图）和个人收入（下图）的关系，数据以县为单位聚集。住房单位个数在价格较低和人均收入较低的住房城市相对增长最大

对于城市的居住地区，该图给了我们非常详尽的画面。我们可以看到街道方向、海滨边界和公园。一些地区的视图，如市中心，显得更散落，因为这些地方住宅区较少。（在本章中，我们将避免用简写词“房子”，因为很显然很多销售的住房指的是公寓。）

该绘图的一个问题是我们无法看到每个特定位置的销售数量。图18-14显示了试着捕获信息的两种尝试。在上图，我们显示的是冒泡图，其地理位置和销售数量成正比。现在，我们得到和商业区非常不同的视图：那里有很高的销售量。仔细查看这些数据可以发现这些公寓建筑有几百套的公寓。在下图，我们把旧金山分成经度和纬度0.005平方米的方块，计算每个方块内的住房数量。这给了我们更高层次的视图，显示主要的家庭所在的地区。

图　18-13：（上图）对于数据中的每个住宅销售都画出了一个小点，它给我们非常好的旧金山布局的感觉；（下图）为了比较，显示的是一张旧金山的街道地图，来自http：//openstreetmap.com（见彩图68）

使用相同的分块方式，我们计算了住房价格的均值和变化系数。变化系数是通过除以平均值计算出来的标准差。我们这里使用它是因为$100000的变化在当住房价格很低时要比住房价格高相对更重要。

图　18-14：住宅销售数量的地理分布。（上图）该图和之前的绘图相似，但是点的大小和每个唯一位置的销售数量成正比。这大大改变了图片，因为城市大公寓的复杂性现在开始凸显出来。（下图）在更高层聚集的销售显示：经度和纬度被划分到少数的分块，每个分块的销售数量都被计数，作为分块的颜色显示

图18-15显示了这两种求和统计的地理分布。我们可以看到Presidio城市边界和该城市的南海岸最昂贵的住房。在西南地区看起来有个高峰：这就是富裕的St.Francis Wood地区，在旧金山州立大学附近。变化系数存在一个有趣的地理趋势：似乎价格朝着西北地区增长。

图　18-15：使用和之前图相同的经度和纬度分块方法；均值（上图）和变化系数（下图）是使用相同的灰色调来计算和显示的

结论

我们从多个角度查看了数据，观察到的是相同的东西：住房危机在更贫困地区相对伤害更深。繁荣和萧条对于价格低的住房打击更早更大；平均收入较低的城市高峰值更高，下降更多，落差也更大。很多繁荣景象和新建筑有关，绝大多数的目标是低端市场。

很多新建成的住宅远离旧金山市区，在较不发达的地区，居民平均收入较低、孩子更多、上下班时间更长。虽然价格上最大的绝对下降发生在高端，但从跌下的价格相对于房子本身的价值所占比例的角度考虑，较便宜的住房损失更大。

自从房地产泡沫成为头条新闻，所有这一切都和我们所学到的关于次级抵押贷款一致。很多信誉很低的人也获得了抵押贷款，初始每个月支付很少。当这些支付款项不断增长时，他们就无法支付了，违约和丧失抵押品赎回权的数量开始上升。我们之前猜测过2008年销售的增长可能和丧失抵押品赎回权有关，而且下一步有趣的步骤是定位出丧失抵押品赎回权的数据，并结合我们的销售数据一起调研。

我们使用了相对简单的统计方法如索引、计算位数、平滑和分块来探索大的、复杂的数据集合。我们从广泛的摘要开始，然后深入探索细节，但是我们还只是触及表面。如果你对我们的数据感兴趣并希望进一步了解我们的工作细节，或者尝试一些你的想法，你可以在git库：https：//github.com/hadley/sfhousing检出所有数据和代码。我们写的所有代码（R、Perl和shell脚本）都是运行在开源软件上，因此，任何人都可以复制这些工作成果，而不需要购买昂贵的软件。再生性的原则(Gntleman和Temple Lang 2007）在科学实验室非常重要，在这里也很重要：如果我们犯了一个错误，你可以发现它、解决它并观察对结论的影响。

通过这次实践工作，我们获得了从住房数据中抽取、探索、分析并最终得出有用的洞察见解的快乐。此外，我们希望关于该策略、方法和技巧的通用的说明能够被证明对于那些和我们志同道合一起工作和从真实数据中学习的人有所帮助。

参考文献

[1]Gentleman,Robert and Duncan Temple Lang. 2007."Statistical analyses and reproducible research."Journal of Computational and Graphical Statistics，16（1）：1-23.

[2]Goldberg,Daniel W. 2008."A geocoding best practices guide."Technical report,GIS Research Laboratory,University of Southern California.http：//www.naaccr.org/filesystem/pdf/Geocoding_Best_Practices.pdf.

[3]Goldberg,D. W.，and J.P.Wilson.2008.USC WebGIS Services.https：//webgis.usc.edu.Last accessed December 2008.

[4]McKinley,Jesse."From housing to haven to foreclosure leader."New York Times. August 13，2007.

[5]Wood,Simon. 2006.Generalized Additive Models:An Introduction with R.Boca Raton,FL:Chapman＆Hall/CRC.

第19章　美丽的政治数据

Andrew Gelman、Jonathan P. Kastellec和Yair Ghitza

历史上最早的一些关于数据分析的例子涉及政治和政府；甚至单词“统计”(satistics)也显示了数据收集“为了国家”以及“和国家”之间的关系。一些统计学先驱，包括Playfair、Laplace和Galton，投入了很大努力来设计和分析公共数据。在20世纪，统计学和Gallup投票、经济和军事组织（五年计划等），甚至是像Svengali一样的政治顾问（1964年的小说《The 480》的一个角色，作者还写了《The Ugly American》、《Fail-Safe》以及其他冷战时期的最佳畅销书）。最近，电视观众已经开始习惯于彩色的地图以及最新的按照地区和人口分片的民意调查和选举结果。还有后面更复杂成熟的杂志《USA Today》、《New York Times》和博客网站FiveThirtyEight.com。

本章提供了一些例子，在这些例子中数据可视化可以有助于加深我们对政治的理解，还带有做出每种选择涉及的因素的讨论。在本章中，我们的重点是使用图形进行研究和演示。

我们尝试使用以下模板：

·“图X显示了……”

·“该图中的每个点（或每条线）表示……”

·“这些不同的图形表示……”

·“在做图前，我们做了……但是它效果不好，因为……”

·“一个自然的延伸是……”

我们并没有形成一套完整的统计图理论——最接近于此的尝试是把对图形显示的探索和检查统计模型的拟合度关联起来(Glman 2003）——但是我们希望这一小段内容对读者自己的理解有所帮助。我们认为这些图形不仅仅是漂亮的手工杰作，而是觉得它们是帮助我们领悟真正的美丽的工具。

我们使用工作中的例子来进行说明，不是因为我们的图形特别漂亮，而是因为在这些例子中，我们知道每个图形背后的故事。

实例1：重新划分选区和党派偏好

图19-1显示了重新划分选区对党派偏好产生的影响的估计（重新画线将可以选举出立法议员的区区分开来）。图中的每个点表示一个州议会选举年（如1972年的Missouri)，其纵轴和横轴显示在那次选举和两年前上一次选举中的党派偏好。

图　19-1：选区重新划分对党派偏好的影响。每个符号表示一个州的选举年，点表示对照组（没有重新划分的年份）以及与不同的重新划分类型一致的其他符号。正如这些拟合线所示，“先验”值比“后验”值的对照组处理（重新划分）情况更具有预测性。处理的显著效果是把党派偏好的期望值置为0，而通过普通的方法无法发现这种效果，普通的方法是拟合模型，假定处理的和对照案例存在并发的回归模型。该图只能显示数据中的关键模式

“党派偏好”，正如这里所定义的，是衡量考虑了投票份额后，选举系统在多大程度上更有利于民主党或共和党。简单地说，党派偏好是民主党在议院立法中期望赢得的议席份额，如果它们的平均值是50%的选票。偏好通常是在-5%～5%，意味着在一个州立法中赢得一般选票的党派将会赢得45%～55%的议席。

图中的小圆点表示没有重新划分选区的“对照”情况，更大的符号对应不同类型的重新划分，这里我们一律视为“被处理”的情况。选举每两年举行一次，而重新划分选区通常每10年发生一次，因此绝大多数的数据点是对照情况。对照情况在衡量前后的关联要远远大于被处理的情况。两组中的斜线区别应该是不足为怪。在没有重新划分选区的对照情况下，州立法几乎没有变化，因此党派偏好可能和之前的选举没有太多变化。相反地，当重新描绘立法区时，将会出现更大更不可预测的变化。为了观察这种效果，在处理中对变量进行建模是非常重要的。

对于民主党从重新划分中获取党派偏好的最简单的方法是划出“区”线，这样在他们所在的每个区都赢得60%的选举权，和共和党的加起来，他们就赢得了接近100%的选举权。然而，这种操纵方式（“不公正的选区划分”）可能在实际情况中是不可行的，即使限制所有区包含相同的人口和区之间的毗连，在美国法院诉讼中，这种潜在的恶意“不公正的选区划分”可能会被推翻。

图19-1是美丽的，因为在我们画出这张图之前(Glman和King 1994），党派内对于重新划分选区的讨论集中于本党是否可以获得较大的利益，以及是否会削弱选举制度的竞争性（因为描绘区域线条的立法院可以试着为自己和同事保留“安全座位”）。

在我们第一次尝试使用该数据来为选区重新划分的后果建模时，我们采用了没有交互的线性回归模型进行拟合，因此这样做就完全错过了最有价值的内容。最终在将数据和拟合回归线都绘出之后，我们才注意到重点并采用了一个更为合适的模型拟合。

我们的图表显示重新划分的主要结果是降低了党派偏好的波动幅度（而且也使得选举系统更好地反映选民，但这是没有显示在这里的另一个图形的主题。）

实例2：估计的时间序列

图19-2说明了经典的逻辑回归（一种预测“是/否”结果的标准统计方法）方法的一个问题，以及如何使用所谓的弱信息化的贝叶斯方法来解决该问题。对于从1952～2000年每次美国总统选举的投票数据，我们为每年的数据拟合了一个分离的逻辑回归模型，预测给定种族、收入和一些其他变量的共和党人的投票选择。

在每个小图中，每个点代表一个逻辑回归系数，垂直线条表示估计的不确定性程度。点序列显示了每种选择的单独的估计，图形中的两行显示了种族和收入的估计系数的时间序列。（为了简单起见，我们没有将其他的系数列在这里。）图中的最左边一列的两个图形使用的是经典的估计，而最右侧的两列则采用的是两种不同的贝叶斯估计（在这个例子中，它们生成的答案基本相同）。

图19-2的估计结果看起来不错，除了1964年是一个完全的分隔点，那一年所有的黑人都支持民主党。因此，种族系数的估计值是负无穷大——也就是说，其推论是那一年黑人选举共和党的概率是0%。1964年共和党确实不受黑人选民支持（那年共和党候选人是Barry Goldwater，他曾经反对《the Civil Rights Act》），因此他们得到的黑人选票肯定为0。这种回归的目的，正如几乎所有的调查分析一样，是为了得出关于普通人群的结论，而不仅仅是调查一个小样本，因此，我们不能仅仅满足于经典的负无穷大的估计结果。（图19-2左栏显示的估计实际上不是无限的，那是因为用于拟合模型的软件是迭代的，在达到一定程度之后就停止了。）

图　19-2：左栏显示了在一个逻辑回归中的两个预测器的估计的系数值（标准误差±1），预测共和党人会选举民主党候选人作为总统的概率，对美国选举研究中心提供的从1952～2000年的每次选举的数据进行分别拟合。数值变量收入（初始值规模是1-5）首先被集中起来，然后通过除以两种标准差重新调整规模。1964年存在一个完全分割点（没有一个非洲裔的选民支持共和党候选人Barry Goldwater)，导致那一年预测器的系数估计值为-1。（该估计的有限值和标准误差是通过多次使用统计建模工具R中的glm函数进行迭代确定的，直到程序终止。）其他两列显示了对于相同模型，使用不同的“弱信息”先验分布的贝叶斯估计。贝叶斯推论解决了1964年由于完全分割点导致的预测器系数估计值为-1的问题，而对其他年份的估计则没有太多效果

本图以及其他和它类似的图的优点在于，它的严格并行性（1990年Tufte和1967年Bertin提出的“小倍数”思想）允许读者——以及该图的创建者——一次性做出很多比较。

贝叶斯方法，正如图19-2的最右侧两栏所示，为1964年的黑人选民系数生成了一个合理的值——低于1952～2000年中的任何一年，而且有更大的不确定性边界，但不是无限的。在解决该问题时，贝叶斯程序并没有混合其他年份或者模型中的其他变量的系数估计（正如图中第二行的收入系数所示）。

该图谈不上漂亮，但是它说明了一条重要且通用的原则，即图形化不仅仅只是为原始数据做的。统计学中常见的实践是在一张表中显示这种结果，但是好的图形可以通过更少的空间显示更多的信息(Glman等2002）。

从我们的角度考虑，以图形的方式展示参数估计有助于向别人表达我们的方法的有效性，同时也可以进一步证实我们的估计序列是合理的，而将估计系数放到一个表格中的方式（或者更经典的计算机输出的很长的序列）将无法达到这种效果。

实例3：年龄和选举

紧随奥巴马的历史性选举之后，产生了一个年轻选民在获胜联盟中发挥很大作用的推测。选举后民意测验数据显示奥巴马在年轻人当中特别受欢迎，但是这真的很有新闻价值吗？举个例子，政治顾问Mark Penn在《New York Times》的网站上写道：“显然，绝大多数年轻人都选奥巴马，但是他们也非常支持John Kerry。”Penn的看法是正确的吗？

和通常一样，做出比较的最清晰的方式是使用图形。图19-3显示了结果有四个版本：第一个版本是我们在选举夜做出的基本图形（从CNN网站上得到的选后民意调查数据），然后一个学生在Web上注意到了我们的图形，并发布了一份升级版；然后我们发布了自己的包含了更广时间序列的图形。在每个这种图形中，点和线连接，点表示共和党候选人在最近几次选举中在四个不同年龄组中每个分组所占的两党选票的比例。显然2008年是不同的，因此Mark Penn错了——这是权威人士只看数字不看宏观形势的案例。这就是所有图形的共同优势：一次性显示所有细节和模式。

要了解实际的更为宏观的状况，还需要大量的研究，而且我们并不认为这些揭示了一些简单模式的图形从任何角度可以代替有关随着年龄模式和选举的时间推移的变化模式的更严肃的研究。

图　19-3：几张显示最近几届美国总统选举的选票年龄纬度的分布模式图

左上图是我们的第一次尝试，基于即时的选后民意调查数据，在选举之夜生成的。右上图是Hober Short创建的，Hober Short是一个学生，他在Web上看到了我们的图，自己做了一个，x轴是选举时间。左下图是基于Short的图的“简洁版”，把所有四个年龄分组直接在图中的线条上标注出来。所有这些图显示了2008年相对于在其之前的两次选举的重大变化。最后，右下图把数据扩展到1988年，显示了1996年Bill Clinton也很受年轻人支持——和奥巴马一样，他也是一个年轻的民主党，对手是年老的共和党——但是没有达到奥巴马在2008年受欢迎的程度。

这些图显示了在制作即使是最简单可行的图形时的选择。正如在很多的政治背景中，最大的收入来自于组合额外的数据——在这种情况下，比较2008年和其之前的年份，比较年轻的选举人和那些年老的选举人，以及比较三个其他年龄组和另一个（在最后比较中缺乏变化性，成为特别重点研究年轻人的选票趋势的动力）。

此外，我们通过重点研究民主党而不是共和党的选票（由于奥巴马在年轻选民中很受关注，这种方式更合适）以及给图形添加更有描述性的标题来改进图形。

实例4：关于最高法院被提名人的公众舆论和参议院选票

美国参议院做出的决议很少可以像确认或者拒绝最高法院提名人的投票一样向公众公开。但是，很多州选票的结果，比如开支法案或者修改法规，在详细的过程中是不明确或者模糊的，但最高法院提名表决的结果是很明显的：或者确认该提名人，允许她在国家的最高法院任职，或者拒绝该提名人，迫使总统提名另一个候选人(Kstellec等2008）。参议员在投票时是否遵从国家级别的公众意见？

图19-4通过把州级别的公众对九名最高法院法官被提名人的意见和参议员们对他们的投票结果之间的关系放到一张图中的方式初步回答了该问题。在每个图形中，曲线显示了一个参议员投赞成票的概率相对于参议员所在州的公众意见的变化规律。黑色实线是拟合的逻辑回归的估计曲线，而灰色线条集群描绘了这种估计的不确定性。散列标记（或者称“小地毯图片”）表示赞成（“1”）和反对（“0”）提名人的选票，而在每个图的右下角的数字表示该被提名人的总票数。底部的图把所有的被提名人都放到一起。我们通过为每个被提名人增加均值的支持度，使得图形曲线平铺开来，渐次向下。

图　19-4：州内公众的意见和该州的参议员在最高法院被提名人投票之间的关联关系。对于每个被提名人，黑色线条描绘的是一个州的参议员投赞成票的概率相对该州的公众舆论的逻辑回归曲线。浅灰色线条描绘了在这些估计中的不确定性。散列标识表示对被提名人的赞成（“1”）和反对（“0”）的选票，而在每个图的右下角的数字表示参议院的总票数。最下面的图把所有的提名人结合在了一起。该图的美丽在于它将原始数据和简单的推理模型融合在了一张图中

该图表明虽然在不同被提名人之间有区别，但公众舆论和投赞成票之间通常成正比关系。毫不奇怪，拥有一边倒的赞成选票的被提名人存在更大的不确定性。同时，“所有被提名人”的图形显示，通常来说，随着州内公众对提名人的支持的增长，该州的参议员更有可能会投赞成票。（这种关系依然成立，即使一个人控制了唱名投票的其他预测因子，如被提名人的能力以及参议员和被提名人之间的观念差距。）

该图之美在于它把原始数据和简单的推理之美结合起来。通常来说，二元关系是通过表格形式显示的。在这个例子中，这么做需要9个关联系数或者回归系数，以及9个回归模型的标准错误，这会使得表格很丑陋，使它难以对公众舆论和为每个被提名人投票之间的关系进行可视化，而且对于不同被提名人之间的比较也变得困难。我们在图中包含的真正数字（我们通过不显眼的方式，不会转移对图形本身的注意力）是唱名投票差值，它易于理解而且为读者反映出每次提名竞争的激烈程度。最后，如图19-2所示，使用小乘数允许读者马上做出一些比较，避免在一张图中出现信息过载。

实例5：宾夕法尼亚州的本地党派

1986年，政治战略家詹姆斯·卡维尔，后来主持克林顿的首次总统竞选，认为宾夕法尼亚州是Paoli和Penn Hills以及它们之间的Alabama。Paoli是费城的一个郊区，Penn Hills是匹兹堡的一个郊区，因此卡维尔指的是这两个长期“摇摆州”(sing state)的市中心是民主党的堡垒，而该州剩下的农村地区是共和党的支持区。

卡维尔的话意味着公众和最高级别的政治专家广泛存在的把国家分成“红区”和“蓝区”的想法。对于绝大多数熟悉21世纪竞选的美国人来说，关于最近美国政治的最深刻的印象之一是从2000～2004年无处不在的选举地图，以北方和西海岸是“蓝区”州，南方和中心地区为“红区”州为特征。虽然当选为总统的奥巴马坚持我们不是一个“红区”州和“蓝区”州的集合，但是这种根深蒂固的形象很难被克服。

图19-5展示了卡维尔对宾夕法尼亚州的描述以及查看地理党派的不同方式，基于新的和令人兴奋的数据类型以及丰富的可视化技术。地图下侧显示了通过2004年总统大选公布的数据对宾夕法尼亚州的不同地区着色，蓝色表示对民主党候选人John Kerry的支持度更高，红色表示对共和党候选人George W.Bush的支持度更高，紫色表示居于二者之间。通过使用连续的红色-紫色-蓝色模式，而不是更普通的红色实线或蓝色实线来表示每个县的胜出者，我们可以更好地对整个州范围内党派的变化程度进行可视化。

该地图的最上层——分散的圆柱面——显示了在该州随机选取的4000个注册的投票人样例的本地化的党派。本地化党派是衡量民主党或者共和党在每个社区的集中程度。特别地，它是定义为居住在1公里范围内支持民主党的人们的比例。每个圆柱面都是基于在投票人的住宅为中心，以1公里范围内为界限，因此复制该党派衡量方式的区域。蓝色圆柱面表示更支持民主党的地区——这次考虑个人层次的注册——红色圆柱面表示更支持共和党的地区，而紫色表示介于二者之间的区域。

图　19-5：宾夕法尼亚州的地理党派。基础层显示了根据2004年总统竞选公布的数据对宾夕法尼亚州进行的分块着色显示，蓝色表示对民主党候选人John Kerry的支持度更高，红色表示对共和党候选人George W.Bush的支持度更高，而紫色分区表示介于这二者之间。分散的柱面图表示对于该州4000个随机注册的投票人的本地党派，定义为居住在1公里范围内支持民主党的人们的概率。每个圆柱面都是基于在投票人的住宅为中心，1公里范围内为界限，因此复制该党派衡量方式的区域。蓝色圆柱面表示更支持民主党的地区——这次考虑个人层次的注册——红色圆柱面表示更支持共和党的地区，而紫色表示介于二者之间的区域。该图之美在于它显示了在国家级别、州级别，甚至是县级别，红区和蓝区的思想观念的复杂性（见彩图69）

该图之美在于它显示了在国家级别、州级别，甚至是县级别，红区和蓝区的思想观念的复杂性。虽然有时很容易想起红色的州和蓝色的州，但是该图说明了在社区级别（甚至是个人级别）还有紫色。在费城之外，该州最大的城市，你可以发现一些红色的社区。相反地，即使是在该州中部最红的县，仍然存在紫色和蓝色区域。

该图之美还在于它证明了我们通常持有的信念可能受到挑战，以及我们的理解可以通过认真地数据分析和可视化进一步加深。该图使用了Catalist提供的数据，Catalist是一家公司，它维护所有在美国的选举年龄的个人的国家数据库。正如详细的和大规模的数据源变得越来越容易访问，多层可视化技术可以帮助我们使用数据来了解我们周围的世界。

结论

政治数据越来越容易获取，而且在媒体和Web上越来越多地被描绘和分享。在研究领域，政治科学期刊的文章开始利用图形技术来发现和展示结果。从NationMaster.com到Name Voyager(http：//www.babynamewizard.com/voyager)的在线工具变得越来越易于使用，这些工具可以导出数据，如Hans Rosling的TED演讲变得非常受欢迎。我们期望统计可视化在政治分析上变得更重要和更广泛。

参考文献

[1]Bertin,J.（1967）. Semiology of Graphics.Translated by W.J.Berg（1983）.Madison:University of Wisconsin Press.

[2]Gelman,A.（2003）."A Bayesian formulation of exploratory data analysis and goodnessof-fit testing."International Statistical Review 71，369-382.

[3]Gelman,A.，A. Jakulin,M.G.Pittau,and Y.S.Su（2008）."A weakly informative default prior distribution for logistic and other regression models."Annals of Applied Statistics,to appear.

[4]Gelman,A. and G.King（1994）."Enhancing democracy through legislative redistricting."American Political Science Review 88，541-559.

[5]Gelman,A.，C. Pasarica,and R.Dodhia（2002）.“Let's practice what we preach:turning tables into graphs.”American Statistician 56，121-130.

[6]Kastellec,J.，J. Lax,and J.Phillips（2008）.“Public opinion and Senate confirmation of Supreme Court nominees.”Technical report,Department of Political Science,Columbia University.

[7]Tufte,E. R.（1990）.Envisioning Information.Cheshire,CT:Graphics Press.

第20章　连接数据

Toby Segaran

每年，人们都能发明数十种新的或改进的统计和机器学习技术来梳理各种数据集。这些技术几乎无一例外地拥有如下共同点：假定存在一个干净的数据集，该数据集包含现有任务需要的所有信息，而这样的数据集在现实世界中通常是无法找到的。正如亚马逊的前首席科学家Andreas Weigend所言：“人们总是问‘何种伟大的技术可以应用在这个数据集上’，而实际上他们应该问的是‘能够获取到的最好的数据集是什么’”。

同时，科学家们每天通过研究和实验生成TB量级的数据，并把它们放到网上；全世界的各地政府允许下载他们在行政中收集的数据；而用户生成的内容的“繁衍”创造了大量的餐馆数据库、科幻小说以及街道的地理位置，而在此之前根本就没有全面的数据。因此，很多数据是可以获取的，但是除了极少数的专家善于利用这些数据外，其他人很少使用——对于其他所有人来说，不能利用这些数据是很让人遗憾的，如果充分利用，那么一篇文章的一页或者两页，其价值可能会高出10多倍。

我相信当前数据“牧羊人”的最大挑战和机遇在于连接不同的数据集，以便创建新的数据集进行分析，并且充分利用数据的繁衍性，已经研究出来的新技术以及可用的令人不可思议的硬件资源。自从数据库产生之后，数据集成一直是一个问题。但是对于研究人员或者好奇的个人可以获取的潜在相关的数据量比原来大了好几千倍——该问题已经从企业范围转变为普遍存在的。

对我来说，这是个重大、棘手的问题，它几乎涉及我的职业生涯的方方面面。因此，在本章中，我不会讨论某个项目，而是要采用不同于本书中多数章节所采用的方式，转而探讨我从多年的项目中汲取的经验。

实际上到底存在哪些公共数据

在Freebase(http：//www.freebase.com)中工作时，我研究了数以百计的数据集，这些数据集本身很有趣，但是当作为其他数据集的增强和提供上下文时，则变得更有趣。这些数据集来自非营利机构、政府、公司和基层的努力。以下给出了数据集来源的一个大列表（但是数据样本很小），以飨读者：

责任政治中心(htp：//opensecrets.org)公布了美国的政治候选人收到的个人捐款的数量。

·很多国家提供在线的人口普查数据。在美国，你可以从http：//www.census.gov网站上下载人口普查数据。

·Geonames(http：//www. geonames.org)数据库包含了世界各地著名地方的经度、纬度、包含的内容以及级别。

·美国证券交易委员会(htp：//sec.gov)提供在美国证券交易所上市的所有公司的可下载的财经数据。

·如环境保护局(htp：//epa.gov)这样的机构提供关于某些地方的环境污染以及产生这些污染的设施的可下载的信息。

·一个特别有用的资源是商标数据库(htp：//uspto.gov)，可以用它来发现哪些公司拥有哪些品牌，这些品牌名称的卖点是什么以及更重要的和所有不同品牌相关的艺术。

·很多社会网络允许下载很多信息子集，包括关系和其他的如位置之类的信息。

·几乎和每个可消费产品相关的营养信息（卡路里、脂肪量等）都可以从美国农业部获取(htp：//usda.gov)。

·国家生物技术信息中心(NBI；http：//ncbi.nlm.nih.gov)发布了很多包含了与遗传和医疗信息相关的数据库，包括Genbank、Pubmed、基因和dbSNP数据库。

·许多城市或州的卫生部门发布了关于餐饮饭馆检查的数据，它是关于城市中包含哪些餐饮饭馆以及它们是否干净的一个很好的免费数据源。

·很多机构比如联邦医疗保险(htp：//medicare.gov)和美国食品和药物管理局(htp：//www.fda.gov)提供了大量的可下载的关于药品的用途、成本和用法的数据。

·在线留言板通常提及公司、产品和场所以及可以从中挖掘情绪和人际关系的文本。你会发现，虽然这些资源很多来自完全不同的地方，但是它们谈论的却是非常相似的东西。这是我所探索的问题的本质——当两个数据库存储相同的东西时，如何识别？正如您将在本章的剩余部分所看到的，这是一个很难的问题，但是解决这个问题可以挖掘带来很多令人兴奋的各种可能性。

连接数据的可能性

早在20世纪80年代，我看了一部名叫《Wall Street》的电影，有一个场面始终给我非常深刻的印象：由Charlie Sheen扮演的一个年轻的股票经纪人给他后来的导师（由Michael Douglas扮演）提供了一份非常有先见之明的股票情报。在该情报被证实非常精确之后，Douglas告诉Sheen说他知道该公司工会的主席是Sheen的父亲。这意味着Douglas手下的研究人员可以发现人们和公司的关系，而在那时，它促使我思考数据连接在一起可以做什么事情。

当然，在以上场景下，这听起来让人有点毛骨悚然，但这正是像证券和交易委员会(Scurities and Exchange Commission,SEC)这样的机构为了发现欺诈和内幕交易，通过手工所做的研究。暂时抛开个人隐私问题和如家庭关系这样的个人数据，而是考虑如果几百个公共数据源能够结合在一起，我们可以发现不同事物之间的关系，那将会发生什么事情呢？我们会发现什么？

以下是一些即兴的想法来启发你。你很可能对实施这些想法没有任何兴趣，但是希望这些想法可以引导你产生自己的关于数据连接的想法。

使用商标数据，我们可以确定哪些公司为不同的品牌服务，我们可能会结合美国农业部(Uited States Department of Agriculture,USDA)的营养学数据，以便确定哪个公司生产最多的含糖饮料。我们还可以对商标数据进行商标标识分类，查看卡通是否更经常被用于销售高热量的产品。我们还引入更多的数据，使用美国环境保护局(EA)的数据来查看不同地方企业的污染量，以及这些数据和公司产品的销售情况的相互关联关系。

把地理数据库如Geonames和社交网络结合起来，我们可以判断人们的地理位置和距离对他们成为朋友的可能性所产生的影响。把这些影响关系和人口普查数据结合起来可以告诉我们人们成为朋友是否受到地理位置或者人口统计的影响（小城镇的人们是否关系更密切？未婚率高的人口是否与更多的社交网络使用相关？）。在政治方面，我们可以把证券交易委员会的哪个公司属于哪个行业的数据和责任政治中心(Cnter for Responsible Politics,CRP)提供的政治贡献相关的数据结合起来。这可以帮助我们确定哪个行业给哪个政治党派捐赠最多。图20-1显示了一些饼图，用于证明这种特定方式的数据混搭。

图　20-1：证券交易委员会的数据和责任政治中心提供的政治贡献相关的数据的饼图（见彩图70）

我甚至还没有谈及股票价格和留言板上的情绪分析之间的关系，试着把遗传和药物数据结合起来，或者判断在收入低的小区的餐馆是否卫生更差（根据健康检查员的报告），但是这只会使你对不同数据源连接起来后带来的可能性有略微的了解。遗憾的是，数据集之间的自动连接很复杂，甚至接近不可能。在上个例子中，我们获取证券交易委员会的数据，它根据名字列出各个公司，但是为了在证券交易委员会找到公司，我们需要中心索引关键字(Cntral Index Key,CIK)。更进一步，像Exxon Mobile这样的公司通常不是分类为“能源”公司，而是更具体的“石油勘探”公司，因此，为了发现所有的能源公司，我们需要建立哪个公司属于哪个行业子集的层次分类。

企业内部

我们认为对Web上的数据源进行整合是一个很大的挑战，但是这种挑战的缩影却是无处不在。通常，大公司内部有一些数据库存储了相同的数据项，但是这些数据库之间却无法进行查询或者员工都无法知道他们感兴趣的数据就存储在与他们一起工作的某个同事所维护的数据库里，这些问题让人震惊（至少当我注意到这个问题时）。这个问题通常被称为“信息仓库问题”，指出信息被清晰地分隔开来，而且绝大多数情况下是不可访问的——像仓库里的一粒稻谷（我一直认为隐喻可以达到涵义延伸的效果）。

当我在生物技术行业工作，与很多制药公司探讨它们的数据集成方案时，这个问题显得非常突出。在很多情况下，公司的管理结构被分为医疗领域（重点研究疾病家族）。这些领域的人们可能采用特定的目标蛋白质集来查找药品或者寻找遗传标记来预测某种药物是否有效，他们所有的时间都在执行成本昂贵的实验，在这些基因、蛋白质及其化合物上构建大的知识集。

同时，公司其他部门的同事，或者可能提早完成项目的研究人员，通常研究或者已经研究了相同的或者相似的基因、蛋白质及其化合物，因此可能错过了一些重要的深刻的见解和重复的实验。

想想为什么会有这样的问题：即使假定所有人都同意了某种模式和查询机制，也不能保证人们会使用相同的术语来描述他们的实验。应该使用什么字段以及如何搜索？比如“肺癌”实验而另一个人却把它描述为“腺癌”。很多工作组都已经尝试创建受控的词汇和固定的模式使得更容易发现实验，但是目前为止没有一个人完全真正破解这个问题。

生物技术实际上走在了前列，因为它至少已经明确了该问题，而且做出了严谨的工业范畴内的努力来解决这个问题。而另一方面，最近美国的银行投资的失败问题则很突出，没有人知道他们的股票经纪人的立场是什么，而股票经纪人自己也无法知道他们是否对房间里的某个人持反对立场。如果数据连接太少是一个问题，那么连接不应该连接的事物则会成为更大的问题。举个例子，政府已经做出了一些万众瞩目的案例，比如错误地确定某些人是恐怖分子或者拒绝某人登机仅仅因为他们和已知的被怀疑的人有相同的名字。

这些当然不是局限于大公司或者特定行业的信息。即使是小公司，也存在使客户和员工资料结合起来的问题。

连接数据的障碍

希望你已经相信把不同数据源的数据整合起来有很大优势。但是存在一些原因，使得人们并没有做这件事……

展现问题

试图连接数据集所面临的最基本的问题可能是很多数据存储在非常不灵活的结构中。首先，在科学和商业中，有惊人数据量的数据就存储在Excel表单，保存在人们本地的计算机中，其他人无法访问，而且不是为整合而做出的设计。

即使在数据库可以访问的公司，数据通常是存储在关系数据库中，很多都是预先定义适合最初设计时认为重要的模式。图20-2显示了餐馆数据的关系模式的一个简单例子。这对于大的、可预测的数据集很好，因为当配置调整很少时，关系数据库的性能非常卓越，但是当应用需要新的数据类型、新的字段或者需要经常添加新的关系时，这些都对关系数据库提出了难题。

我见过人们通过很多种方式来解决这个问题，但是有两种方式很突出，主要是因为这两种解决方法刚好相反。传统的解决方式是不断地重构数据库，创建新表，给已有表增加新的字段，这个过程可能代价很高，很容易犯错误且很慢。（由于篇幅问题，我在这里就不再赘述其他例子，但是每当我谈到这个问题时，总有很多人点头。）这种解决方式使得模式变得越来越复杂，最终使得整个模式变得过于复杂，难以可视化和理解。

我见过的另一种解决方式是简单地构建一个非常基础的数据库模式，它可以支持任何类型的数据。通常的实现方式是有一个实体表、一个关系表以及如图20-3所示的UML图。

图　20-2：餐馆数据的关系数据库模式

图　20-3：基础的数据库模式

这种做法有个优点，开发人员和数据加载人员可以随时给数据增加新的关系。一般来说，它把数据表示成图，而不是一组表。图20-4显示了以图形表示的餐馆，以及人们日后可以很容易增加的额外数据。

图　20-4：餐馆数据的“图形数据库”，可扩展

遗憾的是，关系数据库不是为了以这种方式来存储数据而设计的，而在该模式上做任何有用的查询将需要很多自连接操作，因此会变得非常慢。

由于这是一个非常通用的模式，因此最近几年出现了很多商业的和开源的“图形数据库”，专门为了存储这种数据而设计。以下是一些例子：

Sesame(http：//openrdf. org)

一个开源的图形数据库，由荷兰的一家软件公司Aduna维护。

Jena(http：//jena. sourceforge.net)

另一个开源的图形数据库，由Hewlett Packard开发。

AllegroGraph(http：//agraph. franz.com)

它是一个特征更丰富的商业图形数据库，由Franz开发。

Neo4J(http：//neo4j. org)

它是一个有商业授权的开源图形数据库。

虽然它们代表了在数据建模和开发实践上的转型，当从多个数据源中连接数据时，图形数据库变得更加灵活。由于这个原因，这两种数据库最近获得了很多关注，因为人们开始考虑“复杂度扩展”以及规模扩展。遗憾的是，这个属于我们连接数据时面临的最简单的问题——已经变得越来越糟糕。

共享名词和共享动词

假定你有一个非常优雅的图形或者一个完全合适的关系模式来合并两个数据库，你如何知道哪些数据项真的相互匹配？比如可口可乐公司，一个数据库可能是“Coca-Cola”，而另一个是“The Coca-Cola Company”，或者更简单的“Coke”（但是这些都和“The Coca-Cola Bottling Company”没有关系，它是独立区分开的）。

更糟糕的是，人们在命名各个字段或者对象属性时，几乎没有任何一致性可言。在一个数据库中，一个餐馆地址可能通过address字段表示，而另一个数据库可能通过location字段表示。我们如何识别一个数据库的location字段在作为餐馆的一个属性时，表示的是地址，但是当location用于描述基因序列时，表示的是在染色体中的位置。在实践中，这通常是一个人工识别过程，但是如果我们期望构建一个系统，可以很容易整合成千上万的数据，我们需要找到一些方式来消除整合过程中需要涉及的人工操作。

人们已经尝试了很多种办法来解决这些命名问题。在语义Web社区产生了“链接开源数据”，人们鼓励其他人通过标准的通用资源标识符(UI)来表示特定的对象（如一部电影、一个人或者一个餐馆），因此当两个人探讨相同的事物时，所有人都可以理解。人们已经为一组本体(otologies)进行标准化，这些本体描述了应该使用哪些字段来描述在所有情况下的餐馆或电影这类事物。

然而，到目前为止，同意使用相同的URI来表示事物以及采用相同的本体论来描述事物的组织在所有的免费数据库中只占了非常小的比例，而且几乎没有覆盖任何公司的私有数据库。在很多情况下，即使那些试着参与链接的开源数据，当前也并没有使用相同的URI来表示显然是相同的事物。这意味着，对于像我们这样试着连接数据集，我们将需要设计自动确定两种事物相同的方式。

同一事物的不同名字

和很多人一样，当我第一次试着连接数据集时，我想到了一个不错的一级假设，确定两个事物相同的最佳方式是它们有相同的名字。我甚至认为通过使用字符串距离和子串匹配就能够很有技巧地解决像“Coca-cola”和“The coca-cola company”这样的问题。这在很多情况下适用，但是在最有意思的情况下通常失败了——而且是你最感兴趣的那些情况。

我在试着结合维基百科的电影数据和Netflix网站的电影数据时遇上的一个简单的例子是《Prêt-à-Porter》。维基百科用该名字来描述这部电影（虽然实际上它是一部美国电影），但是在Netflix上，则是用英文名字《Ready to Wear》来描述。在你认为我们应该先把名字翻译成英语，再进行字符串比较时，首先应该注意的一点是存在很多电影，同一个语言有多个名称，如《B.U.S.T.E.D.》和《Everybody Loves Sunshine》，或者《Point of No Return》和《The Assassin》——这些电影或者是在不同国家有不同的名字或者包含和发布标题不同的工作标题。

因此，如果我们不依赖于搜索相似的字符串，我们如何匹配电影？原则很简单，而细节却非常困难（是很多个人和学术研究的主题）。举个例子，我们有两部电影，都是在1994年发布的，都是由Robert Altman导演，Julia Roberts和Sophia Lauren主演：它们有可能会是不同的电影吗？正如实际情况所示，只有一部电影满足这些特征，因此包含这些属性的任何电影——不考虑它的名字——一定是相同的电影。在本章后面我将探讨在实践中是如何发现这一点。

顺便说一下，字符串距离匹配很不适合电影匹配。《Ghostbusters》和《Ghostbusters 2》是非常相似的字符串，但是它们是两部不同的电影，你可以很容易发现一部在1984年上映而另一部在1989年上映。甚至无法假定电影标题后面的数字表示的是一系列电影——《The Madness of George III》和《The Madness of King George》实际上指的是同一部电影。

同一名字的不同事物

没有意识到两种事物实际上相同通常会造成很多麻烦，生成一些副本，必要的话可以在事后解决。更严重的后果是由于两个不同事物有相同的名字或者一些其他不足以确定“身份”的属性而导致不正确地确定它们是相同的事物。这种方式更危险的原因是一旦数据库中的事物不正确地合并在一起，在不需要太多人工介入的情况下，我们就没有很简单的方式来解决掉这些问题。

在威斯康星州（美国）有7个城镇名字叫“富兰克林”，其中只有一个城镇有沃尔玛。至少有四本书的名字为《City of God》。至少有50个名叫“John Smith”的人有足够的知名度出现在维基百科上。抛开考虑错误的飞行黑名单，即使两个事物具有相同的名字，很显然也不足以证明它们是相同的，尤其对于人们的名字而言。虽然对于人来说，存在唯一关键字，如在美国是社会保险号码，但是这些信息几乎从来不会出现在公众可访问的数据库上；对人的唯一识别符通常需要某种程度的保护。

除了在非常封闭的集合（比如国家名字）或者是非常罕见的名字（我想到了“Toby Segaran”），强烈建议不要仅仅基于名字来合并事物，应该使用额外的信息来设计算法，以便确定两条不同的记录确实是同一个事物。

我觉得必须指出已经付出很多努力来为某些特定事物创建经典的标识符，但是这些标识符由于隐私问题从未被成功地应用于人。在美国，我们有社会保险号码，很多政府部门和信誉机构用它来追踪我们，但是人们告诉我们不应该把这些信息和别人分享，因此我们当然不会把它放到公共数据库中以防别人可以更容易地把他们的数据和我们的数据链接起来。因此，我们永远都处于几十亿的人们有相似的名字而没有办法识别出他们。

可能的解决方案

在通常情况下，能够意识到这是一个悬而未解的问题很重要，人们尝试了一些在某些特定情况下可以工作的想法。这些方法，有的是个“死胡同”，但是有的当继续发展时，看起来像是可以在广泛的数据集上工作。

匹配多个字段

在第7章中，Jeff Jonas描述了一个假设的场景，可以通过结合名字和地址，发现某个员工是个小偷。在那种情况下，名字和地址的组合足以表明两种不同的记录实际上表示同一个人。Jeff可以很快地指出他遇到这样的情况，一个“Patrick Smith”和另一个“Patricia Smith”有相同的地址，而且都包含“Pat Smith”，因此如果你不注意，很容易陷入其他很明显的规则的迷宫中。

这说明了在数据集中匹配数据项时可用的最基本常见的方式：选择一组参数，创建一组固定的规则来告诉你事物间是否匹配。举个例子，“两个人是否有相同的名字和地址？”或者“两部电影是否有相同的名字并且是在同一年发布的？”

这种方法在很多情况下都适用，但是它存在一些缺点。首先，它需要开发人员识别字段和匹配规则。这种方式可能会非常乏味，因为当他们发现根据基本的名字/发布年份规则，《Prêt-à-Porter》和《Ready to Wear》不匹配时，他们需要发明另一种规则，如“两部电影在同一年发布，导演相同，而且至少有一个演员相同。”

另一个问题是它需要这些字段本身具有很高的一致性。如果我们没有演员的全名怎么办呢？如果在其中一个数据库丢失了某些电影的年份信息，那又该如何处理？最后，因为我们是选择特定的字段，然后生成平面记录，这种方式并没有充分利用全部网络的数据——全网数据潜在地可能为我们提供更多关于身份的信息。

集体调解

我相信充分利用全网数据是解决匹配问题的关键。这种思想体现在称为“集体调解”(cllective reconciliation)或“集体实体决议”(cllective entity resolution)。有关这方面的详细讨论，我建议阅读Indrajit Bhattacharya的博士论文，你可以在http：//www.lib.umd.edu/drum/handle/1903/4241获取该论文。

在本节中，我将从高层次介绍集体调解的涵义。这些算法的实现细节根据你所处理的特定类型的数据会有很大区别，而且超出了本章的讨论范围，但是我希望一个高层次的概要会帮助你去试验以及使你阅读别人在该主题上所做的工作会更容易。

首先，考虑如下情况，我们有两个电影数据集，两个数据集包含的信息略有不同。从这两个数据集抽取两份数据片段并进行图形化表示，如图20-5所示。我们已经确定仅仅基于名字进行匹配是不明智的，因此我们无法仅仅通过名字来确定两个对象是一样的。但是，我们相信图A中的节点node10和图B中的节点node22可能是一样的，因为它们的名字都是“Julia Roberts”，图A中的节点node12和图B中的节点node27可能是一致的，因为它们都是命名为“Ready to Wear”。

技巧在于我们在两个图中的数据项有潜在的匹配，而且这些数据项相互间有一定的连接——潜在匹配的“Julia Roberts”节点和潜在匹配的“Ready to Wear”之间有连接。这条连接为这二者的匹配提供了更多的证据。这种匹配关系是否是最终结论取决于很多可能性假设，如有多个名叫Julia Roberts的女主角在名为《Ready to Wear》的电影中，但是在该案例情况下，我们可以认为这些关联匹配是正确的。

这些关联的实现方式有很多区别，但是主流技术称为“消息传递”(mssage-passing)。通常，图A中的节点node12知道它可能和图B中的节点node27一样，查看图B中的所有连接，它给图A中的邻居节点发送了消息。连接所有演员的消息可能是“你可能和节点node22或节点node25一样”。而节点node10接收到这条消息后，意识到“实际上，我已经认为我可能和节点node22一样”。同理，节点node10告诉节点node12它可能的所有电影。图20-6显示了可能的过程。

你很可能明白了为什么这被称为“集体调解”，而不是拼合记录，我们实际上是想要一次性把所有东西都归并起来，而且节点之间可以互相提供信息是否需要归并。当然，这只是一个很小的例子，但是考虑一下图20-7所示的更复杂的一项。

图　20-5：两个不同的电影数据集的数据片段

在该图中，名字为“Ready to Wear”的节点并不匹配，而且我们甚至没有一个节点命名为node10！我们如何知道它是Julia Roberts？但是，我们已经稍微扩展了网络范围，包含一些其他Julia Roberts为明星的电影，幸运的是，“消息传递”机制可以通过很多迭代来完成。可能你明白了“消息传递”算法的作用？以下是基本的思想：

图　20-6：节点间的消息传递

1.节点node11决定它和节点node23有可能匹配，因为它们的名字相同；

2.相似地，节点node15认为它和节点node9有可能匹配（如果我们有更多关于Julia Roberts的电影数据，就可以继续更多这样的推测了）；

3.节点node11发送了一条消息给节点node10：“你可能和节点node22或者节点node24匹配”；

4.类似，节点node15发送了一条消息给节点node10：“你可能和节点node22或节点node25匹配”；

5.节点node10在收到了所有这些消息后，下定结论认为很可能是节点node22，因为那是它所共有的消息；

6.注意，节点node10已经确定了自己所匹配的节点，因而可以发送一条消息到所有它所连接的节点（包括它收到消息的节点），给出消息它所认为的它们可能和哪些节点相匹配；

7.节点node12收到了节点node10和节点node19的消息：“你可能和节点node23匹配”，因此它最后选择和节点node23匹配。

虽然只有一个演员名字相同，但是我们已经确定《Prêt-à-Porter》和《Ready to Wear》是同一部电影。

图　20-7：一个更复杂的归并问题

注意我们是如何利用全网的事实来确定最终的匹配？这就是集体调解的精髓，以及它如此强大的原因。这一观点可以继续扩展：在某些实验中，我发现你可以仅仅使用电影的发布年份来连接两个电影和演员的数据集。某个明星在特定12个年份出演的电影，以及和在8个特定年份另一个不同的明星共同出演的事实就足以唯一性地确定这两个人的身份。

当然，其中的实现和数学细节可能非常复杂，但那些不在本章的讨论范围之内。实现这种技术的可定制性版本就留个读者作为非常有益的练习了。

结论

到目前为止，多数人都意识到几乎每个领域都越来越依赖于数据分析的进步。虽然科学主要是依赖于从很少的观察中构建的理论，未来看起来似乎需要收集和挖掘好几百万个衡量尺度；虽然零售业公司主要依赖于“趋势观察者”提供的洞察力，但是现在很多人相信他们应该销售的东西已经淹没在大量收集的数据中了。

不是花费更大代价独立构建更大的数据集，我相信未来在于利用别人生成的大量数据，把它和我们自己生成的数据进行结合和混合。不论这些数据是否来自我们自己的企业内部、非营利结构或者公共领域，都可以通过重用和连接数据来节省很多开支。希望本章能够启发你找到更好的方法来实现这些。

附录　作者简介

Ben Blackburne是Wellcome Trust Sanger Institute的序列分析和汇编研究组的博士后。

Jean-Claude Bradley是Drexel大学化学专业的副教授以及艺术和科学学院的网络学习协调员。他带领UsefulChem项目，该项目在2005年夏天启动，目标是通过实时地把所有的研究工作发表到公开博客、wiki和其他Web页面，使得科学过程尽可能透明化。Jean-Claude发明术语“Open Notebook Science”以便和更严格的“Open Science”等其他方式区别开。他教大学生组织化学课程，其绝大部分课程内容都能够通过公开博客、wiki、游戏和音频视频来自由获取。他在组织化学专业获得博士学位，在合成和机械化学、基因治疗、纳米技术和科学知识管理领域发表过论文和获得专利。

Lukas Biewald是Dolores实验室的创始人和CEO，该公司致力于使“开放来源”(cowdsourcing)变得容易简单。Dolores实验室的博客(htp：//blog.doloreslabs.com)充满了有趣的“开放来源”技术和数据可视化实验。在创办Dolores实验室之前，他是Powerset公司的资深科学家；在加入Powerset之前，他构建了Yahoo!Japan的搜索引擎排序算法。他在斯坦福大学获得了数学学士和计算机硕士学位，他在人工智能实验室工作，并发表了两篇有关机器学习应用的论文。他的个人网站是：http：//lukasbiewald.com/。此外，Lukas还是个专家级别的围棋手。

Brian Cooper是雅虎研究院的首席科学家。在加入雅虎之前，他是Georgia Tech的助理教授。在那之前，他在斯坦福大学获得了博士学位。他的研究兴趣是构建分布式系统，尤其是通过数据库方式管理和处理数据的分布式系统。在雅虎，他的工作是构建庞大的分布式数据存储和处理系统。早期，他的工作方向是自适应的P2P系统、分布式流事件处理、可靠的分布式存档数据存储和XML索引。

Jason Dykes从20世纪90年代早期就一直设计和开发用于探索的交互式空间接口。他使用了一系列灵活的技术进行快速开发，包括使用Tcl/Tk、SVG/JavaScript和Processing来开发创新的软件应用和显示地理结构的创新视图。作为伦敦城市大学(htp：//gicentre.org)的资深讲师，1990年他在牛津大学获得地理专业学士学位，2000年在Leicester大学获得博士学位。Jason是在地理可视化方面的国际制图联委会(Iternational Cartographic Association Commission)的联合主席。

Jonathan Follett是Hot Knife Design公司的总裁和首席创意官。他在国际上发表了关于用户体验、信息设计和虚拟团队方面的作品。他在《A List Apart》、《Digital Web》和《UXmatters》上都发表过作品，给Boston区的技术组织做Web相关的主题讲座。他的文章被翻译为中文、印度语、葡萄牙语、俄语和西班牙语。Jon的虚拟设计作品获得多个美国图形设计奖、一个Horizon交互奖和其他业界的认可。

Andrew Gelman是哥伦比亚大学的统计学和政治科学教授。他最近出版的书有《Data Analysis Using Regression and Multilevel/Hierarchical Models》（剑桥大学出版社）、《Red State,Blue State,Rich State,Poor State:Why Americans Vote the Way They Do》（普林斯顿大学出版社）和《A Quantitative Tour of the Social Sciences》（剑桥大学出版社）。

Yair Ghitza是哥伦比亚大学的政治学专业博士生，致力于研究美国政治学和量化方法。他之前在政治分析公司工作，包括Catalist公司和Copernicus Analytics公司。

Rajarshi Guha是NIH化学基因组中心的科学家，研究高吞吐量屏蔽问题的各个方面。在这之前，他是Indiana University信息学院的访问学者。在过去几年，他研究了化学信息学和计算药物发现，范围从QSAR建模和算法开发到工具箱的软件工程和部署化学信息方法和模型的Web服务基础设施。

Alon Halevy他领导一个结构化数据管理研究组。在这之前，他是西雅图华盛顿大学计算机专业的教授。在1999年，Halevy博士是Nimble Technology公司的创始人之一，该公司是第一个在企业信息集成空间领域的公司；在2004年，他成立了Transformic公司，为Deep Web发明搜索引擎，该公司后被G公司收购。Halevy博士是Computing Machinery组织的会员，在2000年获得为科学家和工程师颁发的Presidential Early Career Award(PECASE)；他是Sloan会员（1999～2000）。他发表了150多篇技术论文。他于1993年在斯坦福大学获得了计算机专业的博士学位。

Jeff Hammerbacher是Cloudera公司的产品副总裁和首席科学家。在加入Cloudera之前，Jeff是Accel Partners公司的一个企业家。在加入Accel之前，他在Facebook构思、创建和领导了数据组。在Facebook，数据组是负责驱动很多统计和机器学习应用，以及构建为大数据集支持这些任务的基础平台。该数据组发表了一些学术论文和两个开源项目：Hive是在Hadoop上构建的离线分析系统；Cassandra是在P2P网络上的结构化存储系统。在加入Facebook前，Jeff是华尔街的定量分析师。Jeff在哈佛大学获得数学专业的学士学位。

Jeffrey Heer是斯坦福大学计算机专业的助理教授，他主要研究人机交互、交互可视化和社会计算。他的工作成果包含探索数据的创新性可视化技术，简化可视化创建和定制软件工具以及为最大化利用多个分析员的洞察力的协作分析系统。他是开源可视化工具箱prefuse和flare的作者，这两个工具当前被可视化研究组织和很多公司采用。在过去几年里，他也曾就职于Xerox PARC、IBM Research、Microsoft Research和Tableau Software。他在加州大学伯克利分校获得计算机专业的学士、硕士和博士学位。

Matthew Holm是波斯顿的Hot Knife Design公司的咨询创造主管，工作主要涉及公司的战略、HTML/CSS的开发以及CMS驱动的Web站点。Matt当前是Oregon的人机交互论坛的副总裁(te Oregon chapter of the Association of Computing Machinery's Special Interest Group on Computer-Human Interaction,CHIFOO)。除了在线网络领域的工作，Matt还是专业的儿童书籍作者和解说员。他的作品图片小说《Babymouse》(Rndom House出版社）备受赞誉且获得奖项，目前已经印刷超过100万册。

J. M.Hughes是一位嵌入式系统和软件工程师，对实时控制、数据采集和图像处理特别感兴趣。从2003～2007年，他负责设计、实现和测试火星着陆器凤凰号的表面成像软件。他当前致力于多波长的激光干扰仪系统的电子和控制软件，该系统可以为NASA项目验证望远镜镜片分割。他和妻子、女儿一起住在亚利桑那州的Tucson。

Jeff Jonas是IBM首席科学家、IBM实体分析组(IM Entity Analytics Group)和杰出工程师。IBM实体分析组是基于系统研究和开发的技术成立的，由Jonas在1984年创办，2005年被IBM收购。Jonas的博客是http：//jeffjonas.typepad.com。

Jonathan P. Kastellec是普林斯顿大学的政治学教授。他在《Journal of Law》、《Economics＆Organization》、《Journal of Empirical Legal Studies》和《Perspective on Politics》发表过论文。

Valdean Klump目前居住在旧金山，是一名作家，在一家创意实验室工作。

Aaron Koblin是一名来自旧金山的艺术家，他的可视化数据项目Flight Patterns、The Sheep Market和Ten Thousand Cents都广为人知。他是视频“House of Cards”的技术主任，当前是一家创意实验室的设计技术带领人。

Coco Krumme是MIT Media Lab的研究生。她同时也为在旧金山的Metaweb Technologies公司工作。

Andrew Lang是Oral Roberts大学的数学教授。他的博士研究领域是弯曲时空的量子理论。在该领域他还很活跃，同时他也喜欢参与跨项目的协作工作，范围从篮球自由投篮建模到在冲击力下的旋转宇宙飞船的稳定性。他当前的研究兴趣包含多维数据可视化、科学和科幻小说的关系、技术和形而上学自然主义在认识论上的区别。

Pierre Lindenbaum 2000年获得病毒学的博士学位，当时他研究virushost交互。后来他转行研究生物信息学，在French National Center of Genotyping工作一年后，他在2001年加入了French startup Integragen。他现在作为生物信息学家，在巴黎的遗传研究中心Fondation Jean Dausset-CEPH工作。

Jayant Madhavan是一名资深软件工程师，是Deep Web爬虫项目计划的技术带领人。在这之前，他是Tranformic公司（2005年被G公司收购）的首席架构师，Tranformic公司为Deep Web发明了搜索引擎。Madhavan博士2005年在华盛顿大学获得计算机专业博士学位。

Michal Migurski是Stamen Design公司的合伙人，同时是该公司的技术和研究方面的带领人。他从1995年开始就一直构建Web，专于处理大规模的、振奋人心的数据集，以及这些数据和大量的客户端的用户进行通信和分发的方式。他公开为学术和工业领域做一些主题讲座，积极参加各种开源开发项目，积极维护Web博客：http：//mike.teczno.com。他在加州大学伯克利分校获得学位。

Cameron Neylon是在跨领域工作的生物物理学家，著名的开源研究实践和改进的数据管理的倡导者。他当前作为生物分子科学的资深科学家，在Science and Technology Facilities Council(STFC)的ISIS Neutron Scattering facility工作。在学术研究上，他经常在科学Web技术和成功的（和不成功的）通用和设定设计工具的应用方面发表论文或进行演讲。

Peter Norvig他是AAAI和ACM的会员，合著了《Artificial Intelligence:A Modern Approach》(Pentice Hall出版社）一书，这是在人工智能领域的一流的教材。在这之前，他是NASA计算机科学的负责人、USC和Berkeley的教师。

Brendan O'Connor是机器学习和自然语言处理的研究员。他是Dolores实验室的科学顾问，先前作为相关性技术工程师在Powerset公司工作。他从斯坦福大学获得符号系统学的学士和硕士学位，目前回到学术领域，作为研究生去卡内基梅隆大学就读。他的博客名为“Artificial Intelligence and Social Science”，可以访问：http：//anyall.org/blog。

David Poole在AT＆T实验室的统计研究部门工作，目前是美国统计协会(Aerican Statistical Association)的统计计算部门秘书和财务主任。他在大规模的数据挖掘方面很有经验，如通信工程的客户呼叫数据分析和欺诈识别。

Raghu Ramakrishnan是雅虎Audience and Cloud Computing的首席科学家，是一名研究员。他在数据库系统的工作——主要研究数据挖掘、查询分析和Web规模的数据管理——影响了商业数据库系统的查询优化和SQL：1999的窗口函数的设计。他的Birch聚类算法论文，获得了SIGMOD 10-Year Test-of-Time奖，他还著有被广泛采用的教材《Database Management Systems》（和Johannes Gehrke合著；McGraw-Hill出版社）。他是ACM SIGMOD的主席，ACM和IEEE的成员。

Toby Segaran是O'Reilly的两本很受欢迎的书：《Programming Collective Intelligence》和最近出版的《Programming the Semantic Web》的作者。他当前在Metaweb工作，在那里他开发了大规模的协调算法，目的是为所有其他公共数据库创建包含共享关键字的免费数据库。在加入Metaweb之前，他成立了生物技术软件公司，2003年被Genstruct收购，Genstruct是系统生物公司。Toby在MIT获得了计算机学士学位，现在和妻子Brooke一起居住在旧金山。在他的博客上你可以读到更多关于他的文章和数据实验：http：//blog.kiwitobes.com。

Lisa Sokol当前是IBM全球商务服务组的顾问，专门做实体分析。她的主要研究领域是帮助执法人和情报组织发现深埋在大数据集中的“可采取措施的信息”(Ationable Information)。她架构了大量的系统来检测评估相对于欺诈、恐怖主义、反间谍和犯罪活动的威胁风险。她还帮助开拓了一些技术的应用，如数据挖掘、文本挖掘和机器翻译，用来探索共享情报环境中的可访问信息。Sokol博士在这些领域发表了无数篇论文。她在Massachusetts大学获得操作研究的博士学位。

Utkarsh Srivastava是雅虎研究院的资深研究科学家。他的主要研究领域是构建系统来解决大规模数据管理系统。在开发PNUTS之前，他在开发Pig项目中发挥了重要作用，Pig是在Hadoop之上的描述查询语言。他在斯坦福大学获得了博士学位，在那里他的研究工作是流数据的查询处理和一些查询优化问题。

Deborah Swayne在AT＆T实验室的统计研究部门工作，是美国统计协会的成员，统计图形学的ASA部门的前主席。她是已经被广泛使用的多维数据可视化软件ggobi的作者之一。

Jud Valeski是Gnip公司的CTO和创始人之一。Gnip公司致力于开发数据便携式软件。从客户端用户接触的产品到大规模后台基础设施项目，Jud工作了20多年，并乐在其中。他在IBM、Netscape、onebox.com、AOL和me.dium加入过工程师、产品和M＆A团队。在发布的由几千万的用户使用的产品中，Jud发挥了重要的作用。

Hadley Wickham是Rice大学的助理教授，研究兴趣是开发工具（包括计算和认知两个方面）使得数据准备、可视化和分析变得简单。他开发了15个统计工具R的工具包，在2006年，由于在ggplot软件上的工作以及重塑了统计工具R的工具包，Hadley获得了统计计算的John Chambers Award。

Antony Williams是ChemZoo公司的总裁，提供ChemSpider服务，为化学家提供在线免费访问服务，目的是为科学家构建以结构为中心的组织。他在商业科学软件领域的Advanced Chemistry Development(ACD/Labs)当了10多年的首席科学官，任期中监督产品开发、市场和销售团队。他是个成就很高的NMR光谱学家，发表了100多篇论文。他曾经是Eastman Kodak Company的NMR技术带头人，在学术领域和政府研究所都工作过。他最近热衷于提供ChemSpider服务，为大众提供化学相关的信息和软件服务。

Egon Willighagen是瑞典Uppsala大学的科学家，主要研究制药生命科学领域的数据分析。他主要研究设计统计方法的开发和生物分子化学计量学和蛋白质化学计量学。他是Chemistry Development Kit和Metware的发布主管，在其他化学信息学开源项目工作了10多年，其中两个项目是Jmol和Bioclipse。

Jo Wood是伦敦城市大学giCentre(http：//gicentre.org)地理信息学的读者。他从1990年就一直致力于山水地貌的分析，在GIS上发表了《LandSerf》，用于进行表面视觉探索。作为地理学家和程序员，他在过去几乎10年的时间中一直在用Java开发软件为数据分析问题找到地理可视化的解决方案。他的著作有《Java Programming for Spatial Sciences》(CC)。当不用计算机分析地形地貌时，他经常骑车兜风。

Matt Wood是Wellcome Trust Sanger Institute的产品软件的负责人，在那里他负责驱动该研究所的世界级的序列设备的软件。

Nathan Yau是加州大学洛杉矶分校的统计学博士生，在加州大学伯克利分校获得了电子工程和计算科学的学士学位。他的主要研究兴趣包括数据可视化、自我监督以及数据本身和物理世界如何纠织在一起。深受暑期《New York Times》的图形编辑实习生工作的启发，Yau维护了一个领先的统计学和数据可视化博客：FlowingData(http：//flowingdata.com/）。该博客主要涉及设计家、统计学家和计算机科学家如何使用数据帮助我们做出更好的决策。

彩图　1（图1-1）　我们在地图上对不同的视觉提示进行实验，通过影响和排放值来最佳展示地理数据。以上展示了我们初始设计的三次迭代。最左边的地图显示根据碳影响所呈现的彩色GPS轨迹；对于中间的地图，我们通过单色的圆圈面(aea circle)来表示影响；对于右边的地图，我们结合了GPS数据来呈现用户什么时候有空，然后重新采用单色的颜色编码方案

彩图　2（图1-2）　在当前的映射机制中，我们采用颜色过滤器来高亮显示数据。地图仅仅是提供上下文信息。链接的直方图显示映射后的数据的影响和排放值分布。当用户通过滚动条看直方图的某一栏，相应的GPS数据会在地图上高亮显示

彩图　3（图1-4）　PEIR的Facebook应用允许用户分享他们对影响和排放值的发现，以及与朋友的进行比较

彩图　4（图1-5）　人们为不同原因跟踪他们的体重和饮食。YFD把这些动机作为用户界面的焦点

彩图　5（图2-2）　为易读性而设计

彩图　6（图2-3）　调查开始只有3个问题

彩图　7（图2-4）　基于用户输入，调查可能会扩展到6个问题

彩图　8（图2-5）　调查细节——当用户对问题1回答“Yes”时

彩图　9（图2-6）　调查细节——当用户对问题1回答“No”时

彩图　10（图3-1）　凤凰号火星探测器在火星上的着陆效果图（图像来源：美国航空航天局和喷气推进实验室NASA/JPL)

彩图　11（图3-2）　立体表面成像器（图像来源：Arizona大学/NASA/JPL)

彩图　12（图6-1）　Aberuchill附近的小路。远处是Bioran Dalchonzie。这是Geograph项目的网站出现的第100万张图片(htp：//www.geograph.org.uk/photo/1006884）。图片版权归Dr.Richard Murray所有，由Creative Commons License(http：//creativecommons.org/licenses/by-sa/2.0/）授权使用

彩图　13（图6-2）　Norfolk海岸的地理马赛克图。每个图片都是根据其地理位置进行映射，表示了1平方公里的景观。图片由Creative Commons License(http：//creativecommons.org/licenses/by-sa/2.0）授权使用

彩图　14（图6-4）　对于六种选定的场景类型，其出现在地理标题和评论的描述的树形图。节点大小表示术语的出现频率。颜色突出了包含一种继承随机方案的场景类型/刻面(fcet)/描述符分类。布局采用“有序的方块化”方法来维持节点之间的方块形状

彩图　15（图6-5）　对于六种选定的场景类型，其出现在地理标题和评论的描述的树形图。节点大小表示术语出现频率。颜色突出了包含一种继承的随机方案的场景类型/刻面/描述符分类。布局采用“切片和切块”的方法来增强维度（上图）间的比较和“切片和切块/有序的方块化”方法来改进标注的可读性，如下部的图

彩图　16（图6-6）　“有序的方块化”树形图，其颜色是通过CIELab颜色模型的一种色彩空间来显示绝对的地理位置，在该空间中，主观感觉到的颜色区别和地理位置的差异紧密相关

彩图　18（图6-8）　对于六种选定的场景类型，其出现在地理标题和评论的描述的空间树形图。节点大小表示词频，颜色表示使用CIELab方案的绝对空间地理位置。位移向量表示非叶子节点的绝对地理位置如场景类型、刻面和描述符

彩图　17（图6-7）　“有序的方块化”树形图，通过CIELab色彩空间的颜色显示绝对的地理位置。包含描述符节点的叶子节点，使用空间有序算法，通过地理位置做关联排序

彩图　19（图6-9）　对于六种选定的场景类型，其出现在地理标题和评论的词汇的空间树形图。位移向量表示非叶子节点的绝对地理位置（共现词），并提供了关于位移的空间聚类和空间趋势的信息来满足树形图的空间填充目标

彩图　20（图6-10）　在beach基础区块中出现的对选定元素的描述符在地理标题和评论中出现的术语的空间树形图。位置向量表示图6-9中放大部分的叶子节点的绝对地理位置

彩图　21（图9-1）　Borders商店定位表单和提交特定表单得到的Deep Web结果页

彩图　22（图10-1）　来自“飞行模式”的静态图片（2005）

彩图　23（图10-3）　派对场景的一张静态图片，通过Velodyne的Lidar拍摄。注意该图片上方的分辨率更高，这是由于在上斜岸的激光的触发速率更快

彩图　24（图10-4）　Geometric Informatics系统（图像来源：Geometric Informatics公司）

彩图　25（图10-5）　Velodyne Lidar的一个激光器捕捉到的数据

彩图　26（图10-6）　Velodyne Lidar捕捉到的另一幅景观图片

彩图　27（图10-7）　派对场景的一个静态图片，Velodyne Lidar捕捉，数据有64条线，每条线由Lidar的一个激光器生成

彩图　28（图10-8）　歌手Thom Yorke在Radiohead视频中的一张静态图片

彩图　29（图11-2）　CrimeWatch的样本数据显示了盗窃、毒品、抢劫、盗窃车辆等犯罪活动

彩图　30（图11-3）　CrimeWatch相同的样本数据，突出显示了程序可识别的图标

彩图　31（图11-5）　奥克兰商业区地图，为三角测量所显示的三个参照点

彩图　32（图11-6）　奥克兰Crimespotting项目的主页显示了从上周开始的犯罪报告地图

彩图　33（图11-7）　Crimespotting的主地图上的时间选择器界面

彩图　34（图11-8）　类型选择器显示了在选定时间范围内的每种报告类型的总数

彩图　35（图11-10）　巡警区专用页面允许居民为在当地巡逻的警官提供反馈

彩图　36（图12-3）　使用Tableau工具构建的显示几十年间婚姻状态分布的原型系统可视化

彩图　37（图12-4）　工作向导可视化：左图显示在过去150年间劳动力组成概要视图，右图显示过滤后农民工的比例的视图

彩图　38（图12-5）　出生地向导可视化：左图显示在过去150年间出生地的分布概要，右图显示过滤后欧洲移民的总数的视图

彩图　39（图12-6）　（左图）交互式国家地图显示每个州2000～2005年的人口变化，（右图）美国散点图显示了平均家庭收入（x轴）和零售额（y轴），新罕布什尔州和特拉华州有最高的零售额

彩图　40（图12-7）　人口金字塔可视化：（左图）2000年的每个年龄组的男性和女性的总人口数量的比较，（右图）2000年学校出席人数的分布（标注重点突出了成人教育的流行率）

彩图　41（图12-8）　sense.us协作式可视化系统：a）交互式可视化应用小程序，图形化标注出当前选定的评论。该可视化是按照性别划分的美国劳动力人数的叠加式时间序列可视化。该图显示了军事职位的劳动力比率；b）一组图形化标注工具；c）保存的视图书签索引；d）添加评论的文本入口区域。可以拖动书签到文本区域，为评论中的视图增加链接；e）在当前视图添加相同主题的评论；f）该应用当前状态对应的URL。该URL随着可视化状态变化自动更新

彩图　42（图12-9）　sense.us评论列表页面，评论列表显示可视化的所有评论，提供链到评论的可视化视图的链接

彩图　43（图12-10）　股票经纪人的标注视图；其评论意思是“大萧条‘杀死’了很多经纪人”

彩图　44（图12-11）　人口金字塔显示在1940年（左图）和2000年（右图）每个年龄组的婚姻状态分布

彩图　45（图12-12）　人口金字塔比较了在1850年（左图）和1940年（右图）西海岸和大西洋中部地区的人口

彩图　46（图12-13）　标注的工作向导视图（左图）突出显示了1930年后牙医人数的减少，（右图）由于牙医人员排序的上升，牙医从业人数得到增长

彩图　47（图13-2）　我们可以构建模型来区分两组数据集

彩图　48（图13-3）　三只股票（a、b和c）在2005年的业绩

彩图　49（图13-4）　正态分布

彩图　50（图13-6）　Goldstein等开发的工具帮助人们理解作为一组结果的分布

彩图　51（图15-1）　DNA中的一个小片段，由POV-Ray软件从PDB文件1BNA生成，doi：10.2210/pdb1bna/pdb

彩图　52（图15-2）　图示为一个DNA片段，其中包含一个基因，以及基因周围用来和细胞调控机制相互作用来限制基因表达的区域。这里基因由一个启动子和一个增强子引导，这样基因就被做了标记，使得细胞知道何时应当表达该基因

彩图　53（图16-1）　使用免费通用服务来托管实验工作记录及处理过的数据。A）单个实验测量时期的一部分；B）Flickr上实验中所拍照片；C）托管在Gdoc上主要数据仓库的一部分

彩图　54（图16-2）　用于检视溶解度数据的可视化工具。A）采用JavaScript和G公司Doc API生成的一个简单表单输入界面；B）溶解度数值的图形化表示；C）带有二维化学结构渲染图的数据表格输出。可以从http：//toposome.chemistry.drexel.edu/～rguha/jcsol/sol.html访问此服务。请注意：本服务与这里提到的其他服务都是动态的，可能得到与上图所示不同的查询结果

彩图　55（图16-3）　这是ChemSpider条目显示溶解度数据与原始数据链接的例子

彩图　56（图16-5）　化学空间溶解度数据的图表展示。A和B给出了同一数据组在表示不同化学特性的数轴上的两个可视化图表。点的颜色表示化合物的类型（红色的为醛类，蓝色为羧酸类，黄色的为胺类，黑色表示其他），点的大小表示溶解度的值。C显示了可点击的界面，其中有单个数据点的化合物结构和溶解度值

彩图　57（图16-6）　使用Second Life展示多维数据。三个空间轴分别表示三个化学描述符。球的颜色表示化合物的类别（和图16-5的定义一样），球的大小表示在当前溶剂中的溶解度。此可视化图表可以在http：//slurl.com/secondlife/Drexel/165/178/24找到，即Second Life中的Drexel岛

彩图　58（图17-1）　FaceStat的评判界面

彩图　59（图17-5）　魅力和年龄关系的散点图，通过性别进行着色

彩图　60（图17-6）　魅力和年龄关系的平滑散点图，每个性别一个图

彩图　61（图17-7）　魅力和年龄关系的平滑散点图，通过性别着色，在一张图上交叠显示

彩图　62（图17-8）　魅力和年龄以及性别的关系的三种迭代绘图：a）平均值在桶内区间的每个年龄；b）每个桶95%置信区间，以及局部加权回归曲线；c）更大的桶，其中数据更稀疏

彩图　63（图17-9）　Pearson关联矩阵，蓝色方块的属性对和上升的斜线是正向关联，而红色方块的属性对和下降的斜线是逆相关

彩图　64（图17-12）　基于平滑后的魅力与年龄描绘的标签样本图

彩图　65（图17-13）　魅力和年龄的关系，通过集群着色，显示了包含2000个点的子样本

彩图　66（图17-15）　集群的质心点、标签和例子

彩图　67（图17-16）　集群的质心点、标签和例子

彩图　68（图18-13）　（上图）对于数据中的每个住宅销售都画出了一个小点，它给我们非常好的旧金山布局的感觉；（下图）为了比较，显示的是一张旧金山的街道地图，来自http：//openstreetmap.com

彩图　69（图19-5）　宾夕法尼亚州的地理党派。基础层显示了根据2004年总统竞选公布的数据对宾夕法尼亚州进行的分块着色显示，蓝色表示对民主党候选人John Kerry的支持度更高，红色表示对共和党候选人George W.Bush的支持度更高，而紫色分区表示介于这二者之间。分散的柱面图表示对于该州4000个随机注册的投票人的本地党派，定义为居住在1公里范围内支持民主党的人们的概率。每个圆柱面都是基于在投票人的住宅为中心，1公里范围内为界限，因此复制该党派衡量方式的区域。蓝色圆柱面表示更支持民主党的地区——这次考虑个人层次的注册——红色圆柱面表示更支持共和党的地区，而紫色表示介于二者之间的区域。该图之美在于它显示了在国家级别、州级别，甚至是县级别，红区和蓝区的思想观念的复杂性

彩图　70（图20-1）　证券交易委员会的数据和责任政治中心提供的政治贡献相关的数据的饼图