那是2005年6月的一个晴天,看着为之奋斗两年的新订单系统在生产环境上线,我们精神无比振奋。我们的合作伙伴开始发送订单,监视系统也告诉我们一切工作正常。一个小时之后,我们的COO给战略合作伙伴发了一封邮件,告诉他们可以将订单发送到新系统了。五分钟之后,一台服务器宕掉了,一分钟后,又有两台服务器瘫痪。客户开始打电话过来,那时我们明白,我们将有一段时间见不着太阳了。
原本意在提高战略伙伴订单利润率的系统就这样崩溃了。抓狂的COO不得不再次给战略伙伴发去邮件,不过这次是让他们使用旧系统。奇怪的是,尽管我们有后备服务器,但是来自战略伙伴的少量订单就击垮了一台服务器。能供大量一般合作伙伴使用的系统却偏偏应付不了为数不多的战略合作伙伴。
这是一个关于我们所犯的错,我们如何改正错误,以及最后顺利完成项目的故事。
“最佳实践” 远远不够
尽管我们设计系统时翻阅了众多供应商所提供的最佳实践文档,使用了无状态的请求处理逻辑、分层结构、分层部署、分离OLTP 和OLAP服务器,但是从没有人告诉我们这个系统将要应对不同类型的伸缩性问题。在2003年,我们设计了和系统效率有关的关键部分。在2004年,我们经受住了负载和压力测试的考验。因此,我们都信心满满地以为我们将各方面都覆盖到了。
通过筛选审查服务器日志和监视系统的事件,我们发现来自战略伙伴的订单和一般合作伙伴的订单有着很大的不同。一般合作伙伴一次订购几百件物品,战略伙伴一次发来的订单却有成千上万行。请求的数据量甚至可以达到数百兆字节。我们的消息基础设施和对象/关系映射代码都从未承受过如此负载的测试。服务器核心为了反序列化所有这些XML数据承受了前所未有的考验,处理单个请求就能消耗掉半G内存。数据库锁的占用时间达到了几分钟而不是毫秒级。当线程超时后,垃圾回收机制开始疯狂地回收内存,这更加损害了系统的可用性。
我们做的第一件事就是在性能测试实验室再现现实中的场景。每当我们一遍一遍的测试而系统一次又一次的崩溃时,我们面面相觑,都不敢相信。我不断告诉自己:“我们做了书上说的每一件事,怎么会是这样?”
事实上,这是我工作中遇到的第一家真正给你时间和预算让你一切都照着书本去做的公司。我们没有任何的借口。可当书本远远不够解决问题时,你能做些什么呢?
不同类型的伸缩性
最后发现,每秒的请求数仅仅是可伸缩性的一方面而已。我们经历痛苦找到的其它方面还包括:
消息的大小
每个请求的CPU利用率
每个请求的内存利用率
每个请求的IO(和网络)利用率
每个请求的总处理时间
消息的大小看似对其它的各个方面都有很大的影响。当消息增大时,它会占用更多的CPU时间来反序列化,消耗更多的内存来保存结果数据,更多的网络带宽和IO来进行数据库读写操作,所有这些加起来就会影响总的处理时间。然而,即使是像给一个合作伙伴的所有待处理订单打折这样的小请求,也会因所处理数据量不同而受到影响。
我们检查了所有的东西,没有一个可以把问题解决。除非我们使大消息变小,问题始终会存在。这是我们对话的片断:
Dan: “二进制序列化或许对更少数量的战略伙伴有用。”
Barry: “不好,他们之间总共有五种互不兼容的平台。”
Sasha: “而且那也不会对内存和IO有多大的帮助。”
Me: “试试压缩怎么样?那样会减轻消息底层的负载。”
Dan: “那样会使CPU的负担更重。”
Sasha: “还要我再说一次内存和IO吗?”
Barry: “请求/响应好像在这里并不管用。”
Me: “你知道我多喜欢发布/订阅,但我也看不出来在这里怎么用得上。”
可是当我们深入探究消息模式的核心时,我们偶然发现了解决方案。
真实世界是面向消息的
最让我们惊奇的是解决方案对于一般的合作伙伴和战略合作伙伴都适用,而且都显著提高了两者的性能。不仅如此,它还加快了订单的周转时间从而提升了存货管理的能力。这是连我们自己都没有想到的。
事实上,解决方案相当直接——与之前的一条“创建订单信息”不同,合作伙伴可以随着时间动态地发送给我们多条“订单信息”,关键字是:(合作伙伴id,采购订单编号)。当该采购订单编号的所有条目完成后,他们可以发来一个“完成”标志为真的“订单信息”。这是有状态的交互。
你知道,合作伙伴几乎总有一个采购部门来发出订单。这些订单是随着时间逐步添加,直到最后“完成”并发送给我们。我们的解决方案使合作伙伴的采购系统在生成订单的同时发送给我们那些部分、非完的订单信息。他们可以修改已发出的订单信息或者取消掉订单的某部分,无需了解我们系统中的订单号(它由一个现有的ERP来管理)。事实上,在我们收到表示订单已完成的信息之前,我们根本不会去调用ERP来处理订单。
当我们收到“订单信息”时,我们会返回一个“订单状态已改变”消息。如果他们系统在他们认定的合理时间段内没有收到响应,他们可以再发一次之前的消息。换句话说,我们要保证消息是幂等的。这意味着,如果合作伙伴想对产品SKU(Stock Keeping Unit,库存单元)作任何更改,都必须重新发送该SKU的所有行(包含各种各样的选项和配置)——实际上没有多大的数据。
幂等消息指的是这样一种消息,无论其被系统处理多少次,效果也跟被系统处理一次一样。
这给性能带来了极大的影响——我们不再需要为了使消息不丢失而对其进行持久化。不再总是向磁盘写大量消息,我们的应用协议使合作伙伴的系统为我们管理交互状态——只需在他们的系统中稍稍增加一些复杂性。