BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

智能服务契约带来的巨大伸缩性

| 作者 Udi Dahan 关注 3 他的粉丝 ,译者 黄璜 关注 0 他的粉丝 发布于 2008年5月14日. 估计阅读时间: 19 分钟 | ArchSummit北京2018 共同探讨机器学习、信息安全、微服务治理的关键点

那是2005年6月的一个晴天,看着为之奋斗两年的新订单系统在生产环境上线,我们精神无比振奋。我们的合作伙伴开始发送订单,监视系统也告诉我们一切工作正常。一个小时之后,我们的COO给战略合作伙伴发了一封邮件,告诉他们可以将订单发送到新系统了。五分钟之后,一台服务器宕掉了,一分钟后,又有两台服务器瘫痪。客户开始打电话过来,那时我们明白,我们将有一段时间见不着太阳了。

原本意在提高战略伙伴订单利润率的系统就这样崩溃了。抓狂的COO不得不再次给战略伙伴发去邮件,不过这次是让他们使用旧系统。奇怪的是,尽管我们有后备服务器,但是来自战略伙伴的少量订单就击垮了一台服务器。能供大量一般合作伙伴使用的系统却偏偏应付不了为数不多的战略合作伙伴。

这是一个关于我们所犯的错,我们如何改正错误,以及最后顺利完成项目的故事。

“最佳实践” 远远不够

尽管我们设计系统时翻阅了众多供应商所提供的最佳实践文档,使用了无状态的请求处理逻辑、分层结构、分层部署、分离OLTP 和OLAP服务器,但是从没有人告诉我们这个系统将要应对不同类型的伸缩性问题。在2003年,我们设计了和系统效率有关的关键部分。在2004年,我们经受住了负载和压力测试的考验。因此,我们都信心满满地以为我们将各方面都覆盖到了。

通过筛选审查服务器日志和监视系统的事件,我们发现来自战略伙伴的订单和一般合作伙伴的订单有着很大的不同。一般合作伙伴一次订购几百件物品,战略伙伴一次发来的订单却有成千上万行。请求的数据量甚至可以达到数百兆字节。我们的消息基础设施和对象/关系映射代码都从未承受过如此负载的测试。服务器核心为了反序列化所有这些XML数据承受了前所未有的考验,处理单个请求就能消耗掉半G内存。数据库锁的占用时间达到了几分钟而不是毫秒级。当线程超时后,垃圾回收机制开始疯狂地回收内存,这更加损害了系统的可用性。

我们做的第一件事就是在性能测试实验室再现现实中的场景。每当我们一遍一遍的测试而系统一次又一次的崩溃时,我们面面相觑,都不敢相信。我不断告诉自己:“我们做了书上说的每一件事,怎么会是这样?”

事实上,这是我工作中遇到的第一家真正给你时间和预算让你一切都照着书本去做的公司。我们没有任何的借口。可当书本远远不够解决问题时,你能做些什么呢?

不同类型的伸缩性

最后发现,每秒的请求数仅仅是可伸缩性的一方面而已。我们经历痛苦找到的其它方面还包括:

  1. 消息的大小
  2. 每个请求的CPU利用率
  3. 每个请求的内存利用率
  4. 每个请求的IO(和网络)利用率
  5. 每个请求的总处理时间

消息的大小看似对其它的各个方面都有很大的影响。当消息增大时,它会占用更多的CPU时间来反序列化,消耗更多的内存来保存结果数据,更多的网络带宽和IO来进行数据库读写操作,所有这些加起来就会影响总的处理时间。然而,即使是像给一个合作伙伴的所有待处理订单打折这样的小请求,也会因所处理数据量不同而受到影响。

我们检查了所有的东西,没有一个可以把问题解决。除非我们使大消息变小,问题始终会存在。这是我们对话的片断:

Dan: “二进制序列化或许对更少数量的战略伙伴有用。”

Barry: “不好,他们之间总共有五种互不兼容的平台。”

Sasha: “而且那也不会对内存和IO有多大的帮助。”

Me: “试试压缩怎么样?那样会减轻消息底层的负载。”

Dan: “那样会使CPU的负担更重。”

Sasha: “还要我再说一次内存和IO吗?”

Barry: “请求/响应好像在这里并不管用。”

Me: “你知道我多喜欢发布/订阅,但我也看不出来在这里怎么用得上。”

可是当我们深入探究消息模式的核心时,我们偶然发现了解决方案。

真实世界是面向消息的

最让我们惊奇的是解决方案对于一般的合作伙伴和战略合作伙伴都适用,而且都显著提高了两者的性能。不仅如此,它还加快了订单的周转时间从而提升了存货管理的能力。这是连我们自己都没有想到的。

事实上,解决方案相当直接——与之前的一条“创建订单信息”不同,合作伙伴可以随着时间动态地发送给我们多条“订单信息”,关键字是:(合作伙伴id,采购订单编号)。当该采购订单编号的所有条目完成后,他们可以发来一个“完成”标志为真的“订单信息”。这是有状态的交互。

你知道,合作伙伴几乎总有一个采购部门来发出订单。这些订单是随着时间逐步添加,直到最后“完成”并发送给我们。我们的解决方案使合作伙伴的采购系统在生成订单的同时发送给我们那些部分、非完的订单信息。他们可以修改已发出的订单信息或者取消掉订单的某部分,无需了解我们系统中的订单号(它由一个现有的ERP来管理)。事实上,在我们收到表示订单已完成的信息之前,我们根本不会去调用ERP来处理订单。

当我们收到“订单信息”时,我们会返回一个“订单状态已改变”消息。如果他们系统在他们认定的合理时间段内没有收到响应,他们可以再发一次之前的消息。换句话说,我们要保证消息是幂等的。这意味着,如果合作伙伴想对产品SKU(Stock Keeping Unit,库存单元)作任何更改,都必须重新发送该SKU的所有行(包含各种各样的选项和配置)——实际上没有多大的数据。

幂等消息指的是这样一种消息,无论其被系统处理多少次,效果也跟被系统处理一次一样。

这给性能带来了极大的影响——我们不再需要为了使消息不丢失而对其进行持久化。不再总是向磁盘写大量消息,我们的应用协议使合作伙伴的系统为我们管理交互状态——只需在他们的系统中稍稍增加一些复杂性。

模式变化带来的伸缩性影响

在我们和战略伙伴在新“版本”(没人敢说系统重写了)订单系统上进行的合作过程中,我们发现“订单消息”的预期大小在几千行条目的数量级——和来自一般合作伙伴的订单差不多。随着消息大小减小,我们也看到了其它各方面的伸缩性得以提升——CPU、IO、还有内存利用率都相应下降。

一旦每个请求的资源利用率得以下降,延迟也得以明显下降,但是吞吐率却比我们预期的更高。这是因为巨型消息“扰乱了”更小的请求使用的资源。随着时间增加,数据库连接池被处理大消息的线程占据,效果上形成了对那些小消息服务线程的“拒绝服务”,导致这些线程超时。

系统中,仍有其它方面对我们造成影响,靠扩容数据和减小消息大小也无法解决。比如说,为待处理的合作伙伴订单打折,象这样类似的请求必须寻求对象/关系设计之外的解决办法加以实现。那种请求处理逻辑更易用基于集合的逻辑表达——SQL。与把所有数据读进内存,循环并改变它,最后再把所有东西存回数据库的方式不同,可以使用以下这样的一个简单SQL语句:

UPDATE PendingOrders SET Discount=@Discount WHERE PartnerId = @PartnerId

效果是惊人的——更快的响应时间和更好的稳定性。

显式状态管理分类

新旧版本系统设计之间最引人注意的区别就是消息处理逻辑是“有状态的”,这正是所有供应商所警告的。Martin Fowler的一些文章使我们明白在数据库中保存状态并不会神奇的使得系统获得伸缩性——这只使得数据库成为了瓶颈,而让数据库供应商卖出了更多的许可证。我们的新设计显式地处理了多消息处理的状态;我们用“saga”来描述逻辑与状态的结合。显式地状态管理使我们能选择最合适的状态存储技术。

对于一般合作伙伴,我们要高速地读写许多小对象,但是这些对象的生命周期相对比较短,所以我们决定用内存中的、分布式缓存产品以保证状态的高可用性。对于战略合作伙伴,状态可能达到几百兆字节,而且会按几周甚至几月为周期缓慢地演变。我们最后使用了数据库,但是它是映射到直接存取设备的开源数据库,而不是使用SAN的OLTP数据库。不用XML存储的订单数据在给我们能进行二进制序列化好处的同时又不用损失互操作性。

Saga —— 好处与挑战

由于新消息契约使合作伙伴给我们发送很多带有同一个采购订单编号的消息,因此我们系统需要能区分哪些是针对已存在的订单处理saga,哪些需要创建新的 saga。于是,我们需要一种按客户id和采购订单编号来查询saga持久化机制的方法。这一需求使得一些流行的分布式缓存技术出局,因为它们只允许通过 id查询,但是一些高端解决方案刚好满足我们的需要。

“saga”这个术语于1987年由关系数据库社区创造,用于描述一种处理长生命周期事务的风格。Saga放弃全局的原子性和隔离性,将事务过程处理为多重的、小粒度的ACID事务的序列。

尽管一开始我们团队里有人对转换到一种新的编程模式有些许不安,但他们很快就看到这与常规消息处理实质上是一样的。当消息进来,它的数据被用于从存储容器中查询一些对象(saga),对象的方法被调用,对象的一些状态被改变了,一些消息被发出,最后对象再被存进存储容器。唯一的区别在于saga所管理的不是存储在主数据库或者ERP系统里的数据,而是系统交互时产生的临时数据。当saga收到“完成”标记为真的订单消息时,它会调用ERP系统读取所有它所积累的数据,并发消息告诉合作伙伴的系统它们的订单状态已由“收到(received)”改为“接收(accept)”。

在开发过程中,我们逐渐意识到,在我们收到“订单消息”时,除了合作伙伴系统需要收到回应以外,公司内还有其它系统也有有兴趣了解这个订单,甚至在其完成之前。因此我们开始发布“订单状态改变消息”给那些有兴趣知道这些消息的系统。

发布/订阅提高了企业范围的效率

第一个感兴趣的部门是订单执行部——特别是遇到那些“突击任务”时。公司近来面临的一大挑战就是向提出紧急任务的客户提供更好的服务,而问题最终归于订单的执行。你知道,即使能够按时将所有产品准备好,但是准备数量刚好而又合适的交通工具几乎从来不可能。一些产品需要被冷藏,一些需要泡沫包裹。现在你已经明白了我的意思,仅仅在手边配备足够的人手来处理事情都是个问题。

既然执行系统预先知道了订单的出货日期和需要哪些产品,他们可以提前作出计划以配备足够的人手,足够的冷藏车,以及按时完成任务所需要的一切。

紧随执行系统的是库存管理——知道将接收订单的具体产品使得他们可以提前准备好库存,并能在需要时敦促供应商提前送货。

负载测试的结果

当我们用之前失败的测试所纪录的消息来测试新系统的时候,我们发现了一些很有意思的事情。

首先,在我们第一次运行测试的时候,我们的测试工程师错误地设置了消息基础设施使得消息可以被乱序处理。尽管我们对性能表现很满意,但我们还是把日志和数据检查了个遍,看看到底是不是真的正确了。即使我们知道幂等消息可以被处理任意多次,但我们之前从未想过处理的次序问题。为了确保万无一失,我们又进行了一轮代码审查,专门针对于消息的次序。最后,我们又做了更多的功能测试,直到我们确信可以放松次序约束条件。

其次,我们发现响应时间随着一段时间的测试慢慢地下降,即使这些测试只是一遍又一遍的重复同样的测试用例。当注意到后面的测试用例的CPU利用率跟之前的一样之后,我们把关注的焦点转向了数据库的锁机制。当我们中有人发现saga表中的记录超过百万行时,问题应刃而解。当时我们面面相觑,有人低声咕哝:“我们有没有说当saga完成后要把它们删掉呢?”,这正是原因。稍加修改了saga持久化机制之后,响应时间平滑多了。

合作伙伴的观点

当然,为了使用新系统,合作方的系统也需要做一些修改。你可以想像,在我们的设计得以通过之前,很多合作伙伴都进行了技术层面和COO级别的咨询。我们的一般合作伙伴对作出改变显得有一点苦恼,但最终促成了“创建订单消息”到“订单消息”的转变并保持了下去。

当战略伙伴的技术团队看过我们的示例代码后,我们看到他们脸上流露出欣喜的表情。“你不会以为我们喜欢发那些庞大的消息吧?这个方案同样将会节省我们的内存和CPU-Bound的序列化。”他们甚至愿意打开防火墙的一个端口来获取订单在我们公司所经历的各个阶段的状态。

部属到生产环境就显得有一点麻烦了。当我们引入新系统的时候,合作伙伴必须使得新旧两套系统并行上线工作。这可以理解,我们的COO在整个过程中一直处于紧张不安的状态,然而,除了一些小问题,整个过程相当成功。

我们一直观察之前的那些健康指标——CPU、内存、IO、数据锁,足足几个小时,大气都不敢出。接下来的日子里运营组和开发组的人员都一直监视这些统计数据以期找到末日将至的迹象,但什么也没有。响应时间比我们之前的观察快了两倍,三个月之后旧系统开始了退役的过程。

从2005年的夏天以来,已过去了两年了。我们头上的乌云最终散去了。COO实际上并不在乎系统技术上那些令人惊奇的能力,他太过于专注从我们战略伙伴那里所带来的利润增长了。

经验教训

这个项目对现在的很多最佳实践来说是一次真正考验。我们盲目地寄希望于供应商的产品和技术,却砸了自己的脚。尽管一直都紧记性能和伸缩性,但是我们从未考虑到大数据量对系统的深层次影响。以我们全部加起来的聪明头脑,也只参透了“不成熟的优化是一切错误的源泉”这个真理的一部分。完整的阐述是:

“我们应该忘记那些小的效率,大概百分之九十七的时候都是这样:不成熟的优化是一切错误的源泉。”
           ——Tony Hoare爵士

我们这里没有“小的效率”,也没有随之而来的重新设计(比如“优化”)。通过改变我们的服务契约并引入有状态的交互,使得我们能够管理那些和系统性能息息相关的状态。我们从未想到能有这种级别的控制权,通过选择特定的技术不仅促成了一个高性能系统,也获得了一个有成本效益的解决方案。

我最大的收获是:可伸缩性不是是或不是这样一个简单的问题。除了并发用户数量和在线服务器的数量之外,可伸缩性还是一个多维的成本函数。对于某个响应时间的需求,峰值和均值请求的比率,消息大小,格式,每个请求的内存工作集大小,每个请求的CPU/IO利用率,一个解决方案需要花多大的代价?对于战略伙伴有意义的技术选型对一般合作伙伴又不具成本效益。始终站在业务的角度来得出结论——当他们发现成本(前端和进行中)盖过产生的回报的时候他们就可能改变性能需求。

参考阅读

关于作者

Udi Dahan是软件简化主义者,获得了微软公司颁发的令人羡慕的解决方案架构最有价值专家大奖,该活动已运作了三年。Udi是一个互联技术顾问,专注于微软的WCF,WF,Oslo等技术。他为全世界的客户提供培训,指导以及高端架构咨询服务,特别是面向服务的,可伸展的安全的.NET架构设计。

Udi是国际.NET协会欧洲宣讲局的成员,国际软件架构组织的作者、培训师以及其架构培训委员会的创始人。同时他也是Dr.Dobb所赞助的web服务,SOA&XML专家。

Udi曾在诸如TechEd USA,SD Best Practices,DevTeach Canada,Prio Germany,Oredev Sweden,TechEd Barcelona,QCon London,以及TechEd Israel等一系列国际会议上发表演讲,其主题覆盖了持久性服务,持久化域模型,多线程偶尔互联的客户端等深入而任务关键的领域。

可以通过Udi的主页www.UdiDahan.com来联系他。

查看英文原文Spectacular Scalability with Smart Service Contracts


译者简介:黄璜,毕业于重庆邮电大学计算机学院。现从事软件开发工作,供职于成都ISSC,主要负责Java Web开发,熟悉struts,spring,ibatis,关注语义网,SOA,云计算等领域。个人主页:http://www.chinacomputing.org, 联系方式huangh@cn.ibm.com。参与InfoQ中文站内容建设,请邮件至china-editorial@infoq.com

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

好文章、好例子 by 冯 希顺

受教了,从头到尾看一遍感觉像自己都亲身经历了一遍似的,呵呵。

Re: 好文章、好例子 by Zheng Can

深有同感,呵呵~~

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

2 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT