敏捷、架构和凌晨五点的产品问题

我非常喜欢敏捷宣言的第一句话,“通过开发软件和帮助他人开发软件,我们正在发现开发软件更好的方法”。它点出了我们一直以来所采取的共同协作之道。伴随着我们不断的学习和探索,敏捷方法也在持续进化。更为重要的是,敏捷方法和实践,是我们从开发人员纷繁芜杂的实际日常工作中积累、提炼出来的,而不可能来自理论研究的象牙塔。

\

如何构建满足需求并且能随机应变的软件?关于这个话题,我们从敏捷方法中受益良多。大概最有影响的技术就是单元测试了。单元测试强调隔离接受测试的各个单元,而接口(通过总是存在争议的mocks, stubs, 和fakes)为待测试的每个单元提供了良好的可分离性。

\

问题出在软件系统各个单元之间的连接或缺口上,它们也许衍生自设计阶段,也许存在于待测的系统单元之间。但此类问题并不新鲜。各自功能完备的软件单元,所构成整体系统的功能不一定完备;正是由于上述原因,才出现了功能测试这个敏捷实践。 

\

然而,当你想要开发在现实生活中实际运作的软件时,功能测试便显露出了不足之处。功能测试只能告诉你“当软件的所有部分都按照软件需求规约运作时,会得到什么样的结果”。没错,你可以强迫一个系统或子系统返回一个错误响应,但原来的规定中还是要对这个错误有对应说明!如果你正要调用一个EJB的远程方法,该方法要么返回“true”或“false”,要么抛出异常;这都没有问题。但没有功能测试会要求它返回一个“purple”或其它什么。也不可能有什么功能测试能使这个方法永远暂停,或每秒返回一个字节。

\

在《Release it》一书中,我重复提到的主题之一就是:每一个对其他系统的调用必定会在某一天试图把你的应用干掉,毫无例外!通常这是由软件规约中没有描述的行为所导致的。一旦发生类似情况,就必须追溯到抽象层,深入到“并发用户”、“会话”、甚至是“连接”中去,找出问题根源所在。

\

接下来我要讲述一个当时无法解释的恼人问题。看看到底为什么会发生这样的状况。

\

凌晨5点问题

\

软件在发布之前都必须通过QA测试,而其生命周期是存在于实际生产环境中的。事实上它总是在实际生产环境中出现无法解决的致命问题。这就是我想说的问题。我的腿并不瘸,我也不对Vicodin上瘾,但除此之外,我有点象Gregory House医生。当大流量事务系统宕机时,我所得到的案例,按照常理是无法解释的。你在电视剧里看不到这样的案例,但再次声明一下,我可做不出像Hugh Laurie那种性感的、愤世嫉俗的、鄙夷不屑的表情。(译者注:上述语句的出处,请看美剧《House. M. D.》)

\

House医生认为发生问题的通常原因,要么是毒品,要么是感染,除非出现某个症状显示可能是其它理由。而面对我的案例,我总是先怀疑系统彼此之间的整合处。在引发系统崩溃的众多原因中,整合处引发的问题是最多的,其数量甚至超出了拙劣代码所造成的问题个数。我甚至敢于声明,在应用系统中,那些整合处迟早会因某些原因而出现故障。有可能是因为拒绝连接,也有可能因为返回了不完整的响应。有可能当你想得到XML时,它却返回了HTML。有可能速度实在是太慢,甚至根本就不响应。

\

我认为数据库调用是整合处问题的一个特殊案例。数据库的复杂程度非常之高,但很多开发人员只是觉得它们应该可以正常工作。随便发送给数据库任何超级复杂的SQL语句,它都会返回整齐的结果集;开发人员认为这是理所当然的。多数情况下,他们更多的考虑在SQLConnection的清晰抽象之下,具体发生了什么。

\

不要被错误的安全感所麻痹,对数据库的任何操作都可能因为发生问题而被挂起,这些操作不仅仅局限于造成死锁的insert和update,或是来源于存储过程。我曾经看到一个案例:一个不允许任何人进行update操作的只读数据库,由于对其进行循环查询,最终还是出现了问题。

\\

快去找急救推车!这个网站不行了!

\
每天的同一时间宕机
\

在我所参与过的网站中,有一个网站所发生的问题非常诡异:几乎每天早上5点,它都会宕机。发生问题时,有大约30多个不同的应用实例正在运行;某种原因使这30多个实例全部死掉,并且是在5分钟之内(通过我们的URL pinger产生的数据解析而得)。重启应用服务器后,问题解决;也就是说,是由于那短短5分钟内发生的事情导致网站宕机。不幸的是,每天的流量都是从这时候开始攀升。从午夜到早上5点,每小时只有100个连接,但当东海岸的人开始上线后,这个数字迅速上升(美国东部时间比中部时间早一小时)。正当人们要开始认真使用网站的时候,重启所有的应用服务器并不能算是一种理想的解决方案。

\

这个问题发生的第三天,我从一个发生问题的应用服务器中取出了其中堆积的线程。在服务实例启动和运行后,所有的请求处理线程都被阻塞在Oracle的JDBC库中,更明确的说,是发生在对OCI进行调用时。(我们使用的是瘦客户端驱动,因为它具有出色的故障转移特性。)实际上,当消除了那些试图调用同一个同步方法的线程后,我发现活动线程们都在进行低层socket的读写调用。 

\

接下来要做的就是使用tcpdump 和ethereal(Ethereal现在被称为Wireshark)。可是采取措施之后,成效微乎其微。从应用服务器向数据库服务器发送一些数据包后,却得不到任何响应,也没有任何信息从数据库发送到应用服务器。但通过监控发现数据库是正常工作的,没有任何死锁,运行队列为空,而且几乎没有I/O操作。

\

抓取数据包

\
从抽象到具体
\

使用抽象的方式可以让你的表达变得更加简明扼要。直接讨论如何从一个URL中得到文档的过程,要比从初始连接、数据打包、应答、接收窗口等等这些冗长乏味的细节入手要容易得多。即使有了高层抽象,当出现问题的时候,我们还是需要抽丝剥茧,深入这些抽象内部的底层细节,寻找发生错误的原因,以利于后面的修正。在网络环境中,要进行问题诊断或是性能调优,数据包抓取工具是我们追根寻源的唯一工具。  

\

tcpdump是UNIX系统下从网络上抓取数据包的常见工具。当在“混淆(promiscuous)”模式下运行时,它会通知网卡 (NIC)接收所有经过它的数据包,甚至是那些发送到其它计算机的数据包。在数据中心,NIC几乎肯定是连接到分配给某个虚拟局域网(VLAN)的交换机端口上(switch port)。此时,交换机保证了发往该虚拟局域网内所有地址的全部数据包都被NIC所接收。这是一个很重要的安全措施,因为它可以防止外部不怀好意的人做坏事,比如探测网络中他所感兴趣的信息之类。

\

Wireshark 具有探测器(sniffer)和协议分析器(protocol analyzer)的功能。 它不但可以象tcpdump一样探测数据包,而且还可以为我们解开数据包。Wireshark在过去曾经有过一些或轻或重的安全纰漏。有些时候,恶意软件或是已经中招的计算机会向网络发送经过“特殊”处理的数据包,这个包会触发缓冲区溢出,并执行攻击者设定的恶意代码。像其他数据包抓取工具一样,Wireshark必须使用root的权限,才能运行“混淆(promiscuous )”模式,因此上述漏洞会使得攻击者在网络管理员的机器上获得root权限。 

\

除了安全问题以外,Wireshark还是一个很笨重的GUI程序。在UNIX系统上,它需要一些X库,而这些库在headless系统上是不会安装的。在任何配置的计算机上运行Wireshark,它都会占用很多内存和CPU来解析和显示数据包。在生产环境中使用的服务器上,不应安装Wireshark以避免其形成负担。由于上述原因,最好以非交互的方式使用tcpdump来完成数据包的抓取,并将其保存在文件中,然后将抓取文件移动到非生产环境中进行分析。

\

下面的截屏图显示了在我的家庭网络上捕获的一些数据包。第一个包显示了一个从我的无线网桥发送到我的有线modem的一个ARP请求,看起来这好像是个问题。第二个包很奇怪:是发送到Google的HTTP查询,请求一个URL,链接为/safebrowsing/lookup,还带了一些参数。接下来的两个数据包显示了一个DNS查询和响应,其主机名为“michaelnygard.dyndns.org”。第五、六、七数据包是建立TCP连接的三段式握手。我们可以跟踪web浏览器和服务器之间的整个会话。在数据包追踪下面的方格中,展示了第二个数据包中的层次结构,这个结构由TCP/IP堆栈围绕HTTP请求所创建;外层是一个包含了IP数据包的以太网数据包,IP数据包中又包含了一个TCP数据包。最后,TCP数据包的有效负载是一个HTTP 请求。整个数据包的准确字节数在第三个方格中。

\

dec5dbd4e360118e3d38ba9ec08afec1.jpg

\

对这类问题,我强烈推荐在身边放一本Kozierok的《The TCP/IP Guide》或者W. Richard Steven的《TCP/IP Illustrated》。

\

重复与偏执

\

理解灾难背后的根本原因

\

到了这时候,我们不得不重启应用服务器。我们的第一要务是恢复服务;然后在不破坏服务品质协议(SLA)的前提下,做一些力所能及的数据收集工作。更深层次的研究只能等灾难再次发生才能进行了。没有人怀疑问题会再次出现。

\

确实如些,第二天早上,灾难再一次降临了。应用服务器死的硬硬的,像绷紧的鼓皮,一堆线程挂在JDBC driver上,整个情况一团糟。这次,我可以看到数据库网络上的流量情况。竟然毫无动静!防火墙另一侧的数据库服务器上没有发生任何流量。根据这个最大的线索,我做出一个假设。通过对应用服务器资源池处理类的反编译,验证了我的假设似乎是有道理的。

\

我提到过socket连接是一个抽象。他们作为网络一端的计算机内存中的一个对象存在。一旦建立之后,一个TCP连接对象可以一直存在好几天,即使没有收到任何数据包。只要两个计算机在内存中保存了socket的状态,这个“连接”就依然有效。路由可以改变,物理链路可能发生断路并重新连接。这都没关系。只要两端的计算机认为它还在,这个“连接”就一直保有在内存中。

\

有一次,所有的一切都可以正常运转。那些日子里,有些偏执的家伙破坏了整个网络运行所基于的理论与实践。没错,我是在说防火墙出了问题。 

\

防火墙只不过是个特别的路由器。它将数据包从一些物理端口路由到另一些物理端口。在每个防火墙内部,一整套访问控制列表定义了允许连接的规则。这些规则看起来就象“IP地址从192.0.2.0到192.0.2.24的这些计算机,可以向192.168.1.199的80端口发起连接”。当防火墙发现一个进来的SYN数据包时,它会根据那些规则进行检查。这个数据包可能被批准(被路由到目标网络),也可能被拒绝(TCP重置数据包并发回),也可能被忽略(扔掉数据包而且不做任何响应)。如果连接被批准,那么防火墙在它自己的内部表中做一个标记,例如“192.0.2.98:32770 已连接到192.168.1.199:80”。此后的数据包只要匹配这个标记,就可以在防火墙的网络中进行路由。

\

到目前为止一切顺利。那么这与那些早上5点把我叫醒的电话有什么联系呢?

\

问题关键在于防火墙内部建立的那个连接表。它是有限的,所以它不许无限期的连接,尽管TCP本身允许无限期连接。除了记录连接的端点之外,防火墙也记录了“最后到达数据包”的抵达时间。如果很长时间不在连接中传递数据包的话,防火墙会认为端点已经无效,并从列表中删除这个连接。TCP的设计却不这么高明。也没有什么第三方来告诉网络的两个端点,连接已经被停掉了。尽管没有传递数据包,这两个端点还是认为它们之间的连接有效,并且可以一直持续下去。

\ 8406cbba2815297f536fa336091a3c49.png

打那儿开始,由于socket端口的半开状态,从任何一端的socket端口进行读写操作,都不会引起TCP重置或报错。相反,TCP/IP堆栈发送这些数据包,等待ACK应答信号;如果等不到,就再次发送。忠实的堆栈一次又一次地重建连接,而防火墙不断地把发送过来的数据包扔到一边去,而且不返回“ICMP目标不可达”这样的消息(攻击者可以通过欺骗源地址来侦测活动连接)。我所使用的Linux系统,运行在2.6内核上,它把自己tcp_retries2的默认值设置为15,这样,在TCP/IP通知socket库之前就会有一个20分钟的时间间隔。我们当时使用的HP-UX服务器的超时时限设置为30分钟。因此发生问题的应用所发出的socket写操作可以阻塞30分钟!socket读操作遇到的情况更加糟糕,它会被永远阻塞下去!

\

当我反编译了资源池类以后,我发现它采取了后进先出策略。漫漫长夜之中,网站流量非常小,只需要从资源池中取出一个数据库连接就可以完成工作。下一个请求会用同一个数据库连接。直到流量上来以前,另外三十九个连接全部处于空闲状态。在防火墙中空闲连接超时设置为一个小时,而且三十九个连接空闲时间也超过了一个小时。

\

一旦流量开始升高,另外三十九个连接马上就锁定了。尽管剩余的一个连接还可以生成页面,但它迟早会被某一个线程占用,该线程阻塞在其余资源池的连接上。因此,唯一好使的连接也被阻塞线程占用了,整个网站也就掛了。

\

Dead Connection 起效

\
找到解决方案
\

明白了故障发生的完整过程,我们就该找出解决方案了。我们可以让资源池在提供JDBC Connection前可检查它们的有效性;它可以通过执行类似 SELECT SYSDATE FROM DUAL这样简单的SQL查询来进行检查;可这会使请求处理线程挂起并导致服务器当掉。我们也可以让资源池追踪JDBC Connection的闲置时间,并抛弃那些超过一小时的JDBC 连接。可不幸的是,在这些连接中,有些是用来向数据库服务器发送数据包以通知它Session失效的。结果服务器同样就“挂了”。

\

于是我们开始采取一些非常复杂的措施,其中就包括创建“收割”线程,去查找那些驻留时间过久的连接,并在超时之前把它们干掉。庆幸的是,有一个机灵的DBA想起一件事:Oracle有一个叫作“dead connection detection” 的特性,通过它你可以发现客户端的崩溃时间。将其参数设为“enabled”后,数据库服务器会周期性的发送一个ping数据包到客户端。如果客户端有响应,数据库就知道它仍然是激活的。如果重试几次后客户端还没有响应的话,数据库就认为客户端已经崩溃,并释放该连接所掌握的所有资源。

\

我们不太关心客户端是否崩溃,ping数据包应该可以为当前连接重置防火墙的“最后到达数据包”的抵达时间,以保证连接有效。“Dead connection detection”这一特性会保证连接是激活的,这就可以让我们睡个好觉了。

\

得到的教训是什么?

\

过去我们从没想过要去写一个单元测试,以模拟在TCP/IP协议下数据库调用被无限期挂起的行为。为什么要写呢?甚至更糟的是,这些容易出错的网络、服务器和应用,有无数种方式产生类似的“超出规约”的故障。那我们可以做些什么呢?这是否意味着,在agile的世界中,人们错失了某个实践呢?是否存在某种测试技术或代码实践可以让我们避免这类故障呢?

\

以前没有人能想象程序员会测试自己的代码。二十年前,这样的想法会让人笑掉大牙。而现在,单元测试却是大家所期望的,有时甚至是必须的。越来越多的人甚至用Michael Feather'的定义来识别“遗留代码”,这个定义就是:没有单元测试的代码。也许,有人会发明一种测试技术,可以防止由于最基本的抽象层面发生问题而导致的无数故障。

\

在那之前,我认为我们必须考虑架构问题,甚至在敏捷项目中也一样,以避免类似的错误再发生在我们身边。我们在功能性方面使用设计模式;同样的,为了保证灵活性,我们也要应用设计模式。在《Release It》中,我创建了一套这样的“稳定性模式”。我希望这只是一个开端。

\

关于作者

\

致力于在全国范围内帮助开发人员减轻他们的痛苦。他向遇到的人共同分享他的热情和能量,来帮助他们提高,即使有些人并不情愿。Michael花了将近二十年的时间,来学习如何成为一个关心艺术、质量和工艺的专业程序员。现在他已经是一个具有二十年经验的专业程序员和架构师。其间,他的工作领域涉及美国政府机构、军事、银行、金融、农业和零售业。 除此之外,Michael常常自己开发一些系统。 这些来自现实世界的经验永远改变了他对软件架构和开发的看法。

\

最近,Michael完成了一本书,名为《Release It! Design and Deploy Production-Ready Software》。他认为软件不应该仅仅是通过QA测试,更要在现实世界中长期运行;众多关于如何构建此类软件的想法,都在书中得到了阐述。本书发行以后,它在Amazon的软件开发书籍类别中,名列热销第一名达一个月之久。Michael之前也写了很多文章和评论,并在Comdex会议上演讲,并且是早期Java相关书籍的合著者。

\ 查看英文原文: Agile, Architecture and the 5am Production Problem
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值