从光大证券的软件设计缺陷想到的。

8月16中国股市出现名震历史的乌龙事件,导致该事件的原因今天被证监会调查后,确定是软件系统的设计问题:

光大证券自营的策略交易系统存在程序调用错误、额度控制失效等设计缺陷,并被连锁触发,导致生成巨量市价委托订单,累计申报买入234亿元,实际成交72.7亿元。

根据这条信息我个人瞎胡乱猜测一下出错场景:

交易员发出买单以后,由于锁问题,无论是语言内存锁或数据库锁,导致买入状态没有更新,或者交易员点按“买入”按钮时,没有响应,不由自主多按了几下,结果,这些命令恰恰全部被计算机系统都接受,只是因为锁堵塞,没有及时处理,没有及时更新状态,等系统稍微空闲,这些被锁住的交易全部被执行。

这个问题有点类似“重复提交”问题,打个简单比喻,在本论坛发言,按发布键后,没有反应,或者查询主题列表没有看到,然后就再多按一次,结果同一个贴重复发表了两次。

个人观点:出现这种问题其实真正体现了用计算机提供的锁等ACID机制并不能真正解决业务上的高一致性。

有一篇文章:弱一致性在现实世界中到处存在 http://www.jdon.com/43246

过去,所有的一切都是写在纸上的假象,至少过去几十年一直如此,即时一致性是IT人发明的,他们纸上谈兵地认为:不同组织不同地方的数据如果没有即时一致性(高一致性)几乎不可能的。

其实,现实生活中也并不是不存在高一致性,只是可能比较少,这里我想提出一个新观点:高一致性是个好东西,但是好东西是不是一定是缺省配置呢?比如有钱是个好东西,那么是不是让每个人一生下来都有很多钱呢?实际相反,每个人生下来缺省是没有钱的,赤条条降生。

既然高一致性是好东西,当然不能随便缺省配置,但是实际中却相反,当我们使用关系数据库,或者使用事务机制等ACID时,这些机制都缺省让我们的程序变成高一致性,高一致性不是稀罕物,可以无偿获得,那么无疑结果是,当系统负载增加时,每个无偿得到高一致性的处理程序都要付出代价,那就是堵塞。

相反,如果我们缺省是弱一致性,对于需要高一致性的业务,我们通过业务分析会辨识发现,然后给予充分的业务重视和业务设计,那么也许能够将计算机提供ACID服帖地为我们业务高一致性服务。

在DDD分析需求过程中,这个问题得到高度重视,以DDD书籍中订单为案例,该订单有一个额度限制,也就是所有订单下条目Item总额不能超过订单设定的额度限制,这个限制其实是一种逻辑一致性限制,而且必须是实时高一致性的,因为每增加一个订单条目,都必须检查这一逻辑一致是否被破坏,如果是,立即不能加入。

比如Oder限制是1000元,订购了三件商品,总金额是1200元,超过了1000元限制,那么这个订单是无法生成的,这属于即时一致性检查。

这种检查我们采取什么方式实现呢?传统是采取锁的方法,将Oder这个对象或表锁住,因为在统计总金额时是不允许其他人有操作的,当你锁住这个对象或表时,堵塞的可能性就会到来。

如何突破这种锁方式的限制呢?那就是Actor模型,也就是说,Oder检查自己总金额,不用在被外界访问时被锁住,而是永远无法被外界锁住可能,因为Actor模型无法被外界直接使用方法调用,只能象发QQ消息一样与外界交互。

具体可见这个贴:事件驱动编程:
http://www.jdon.com/45436

所以,对于这起事件,我个人观点是:这次光大事件问题核心直指系统连锁,应该是状态锁问题,不是内存锁,就是数据库锁等出错,依赖计算机提供的acid等锁机制其实不是百分百安全,只有用业务方法解决业务问题才是根本解决之道。
[该贴被banq于2013-08-18 22:05修改过]

想到再补充一点:

交易员发出了几次买入指令,这是用户的意图,也就是说,用户的意图是分批买入50ETF,而计算机系统在执行这个意图时出错。

这里可能还存在一个潜在思路问题,没有区分清楚用户意图的接受和用户意图的执行这两个动作。

用户意图首先必须由领域模型接受,领域模型必须综合自身的逻辑限制,比如额度限制,对用户意图进行判断是否可执行?如果用户几次买入50ETF的意图会破坏领域中逻辑限制,如额度限制,领域模型有权决定不再执行意图指令。

而我们传统架构实则都没有区分这两种,而是在执行意图过程中再进行逻辑判断,比如额度限制,如果执行过程本身被锁住,自身形成了一个封闭边界,只能对自己内部进行逻辑审核,而用户再发出一个同样的意图,就不能跨两个意图进行综合判断了。

打个比喻,每个省都有自己的监察部门,但是这个监察只能审核自己省内部的,如果两个省范同样错误就没有办法跨省联合监察,这必须更高一层监察部门实现。

而我们区分用户意图接受和执行两个动作,实际就是在跟高层面对用户指令进行监督检查。

参考见:http://www.jdon.com/45385

分析得很好。
我觉得“锁”这个东西跟人的思维习惯相关——先锁住处理完后其他人再进来,好比火车站检票口平时人少大家一个一个来,到春运时一堆人拥到哪里那个叫悲催,慢慢检票,但我们的系统可等不了那么长的时间“检票”,锁是锁住了但大量数据拥挤而来形成的阻塞问题不可忽视,到底用不用锁,什么时候用锁,一定花时间要推敲,碾磨后再使用。如果这个系统用disuptor来处理,又会怎样呢?

1)买入是插入操作还是更新操作?如果是更新操作,在聚合根上设计乐观锁就搞定了应该,不至于出现重复买入的情况;但是,即便是update操作,要是不是因为并发的问题,用乐观锁还解决不了呢,呵呵
2)如果是插入操作,那服务器端领域模型内如何判断是否重复买入?也就是聚合的不变性是什么?
3)那个系统没有在客户端做基本的防止重复提交的判断?比如按钮灰掉。

我觉得要谈解决方案,首先要搞清楚它的业务到底是什么,买入操作做了什么操作,这个操作有哪些业务不变性规则?这点没搞清楚,没法谈具体解决方案了。
[该贴被tangxuehua于2013-08-20 13:36修改过]

他们的好像是生成订单,和处理订单是分开的。
现在生成了好多,而处理的少。
金额发生了不同。
这个系统只有10万元,不能处理高频的交易,只能说是上面的决策问题,为了省钱。

嗯,即时互动性强的主题,总能从Banq这听到新鲜即时的资讯,不错。

2013-08-20 14:46 "@xianghx
"的内容
他们的好像是生成订单,和处理订单是分开的 ...

订单生成和订单执行两个过程是分开的,倒是符合我前面谈到的用户意图的接受与执行分开的方式。

用户每发出一笔买入指令意图,实际生成一个订单,而订单执行是指将更新应用状态,比如购物订单的执行将是减少库存,这个商品库存减少了,被订购了,如同转账中,将100元从A转到B,那么首先检查A是否有100元余额,如果有,扣除这100元准备用于转账,这相当于订单执行,如果订单最终未执行,还需要归还这100元为正常余额。

更新应用系统状态是系统最容易出错的地方,因为这里存在并发写的可能,无论是采取内存锁或数据库锁,都可能引发堵塞,导致同一笔交易反复执行从而产生虚假交易。

采取Disruptor的LMAX架构也是一种证券金融高频交易系统,使买卖双方撮合,其基本也分为订单生成和订单执行,不过其在订单执行时采取了两个Disruptor+一个中央逻辑处理器,这个中央逻辑处理器类似Actor模型,不受外界干扰,能独立完成逻辑计算,不会象光大事件中那样在订单执行环节出错,导致大量虚假订单执行。


[该贴被banq于2013-08-25 09:25修改过]
[该贴被banq于2013-08-25 09:27修改过]

调查真相出来了:
证监会调查乌龙指事件解密:光大前后四次篡改程序

事实上,光大证券策略投资部在外购软件后,自己又开发了订单生成系统。这个系统包括订单生成系统和订单执行系统两部分,均存在严重的程序设计错误。

其中,订单生成系统中ETF套利模块的“重下”功能(用于未成交股票的重新申报),设计时错误地将“买入个股函数”写成“买入ETF一篮子股票函数”。

“重下功能”用于未成交股票的重新申报,这个功能从未实盘启用,严重的程序错误未被发现。

而8月16日上午,光大证券交易员进行了三组180ETF申赎套利,前两组顺利完成。11时02分,交易员发起第三组交易。

11时05分08秒,交易员想尝试使用“重下”功能对第三组交易涉及的171只权重股票买入订单中未能成交的24只股票进行自动补单,便向程序员请教,程序员在交易员的电脑上演示并按下“重下”按钮,存在严重错误的程序被启动,补单买入24只股票被执行为“买入24组ETF一篮子股票”,并报送至订单执行系统。