声明

本节内容全部整理自简书 如何构建一个交易系统 系列,目的是为了大概学习一下流程。

本节不做过多个人的理解和展开,后续将围绕其中的各个点进行展开。

交易系统

比起下面我们要介绍的交易系统,一个真正的交易系统复杂程度将超过数个数量级, 交易系统作为一个比较成熟,也是最早电子化的系统。

估计比现在的大部分大家接触的系统都要早, 他的模型、 理论基础也非常健全; 整个产业非常成熟,提供基础服务的非常之多;大部分的人或许多多少少有点交易的经历, 但是对于整个交易系统后台怎么运作,可能不太熟悉。

这个系列的文章, 将会从一个 IT 角度解析如何构建一个交易系统, 过程中将交叉结合业务逻辑和基本的架构设计的原则, 寄希望能够以通俗易懂的方式, 给大家揭秘整个交易系统的运作原理。

同时这也试图探讨了一些在互联网思维下一个交易系统如何突破原有的障碍, 做到去中心、扁平化、真正意义上面的普惠金融的方法和心得。 

何为交易系统?

在任何一个终端,当你输入某个买、卖指令后,后面是如何运作的?

有本书 《The Trade Lifecycle: Behind the Scenes of the Trading Process》, 在摩根工作的时候曾有人大力推荐, 当时粗略扫过一遍, 理解不是太深入, 只挑选了和自己工作交集比较多的地方:OTC 和 后交易(Post Trade) 部分东西了解了; 里面介绍的金融产品非常丰富, 超出了我们大多数人接触和理解的范围, 但是对于一个交易的生命周期, 在本书中得到淋漓尽致的描述,而且非常通俗易懂, 同时还有些许金融八卦, 有兴趣的同学可以了解下。

当自己试图去构建一个真正的交易系统时候, 翻尽网上资料,收获不多, 倒是引来了一堆google,某度的大量广告的推荐, 都是某某交易软件怎么牛X!-_-!;

但是最后找到了一个很不错的 CMEGroup 的资料库

开放透明详细, 还有视频, 实属不易。

在迷茫中找到了一丝光明。

一个互联网思维下的交易系统, 当然和一个部署在银行或者一个算法交易平台后面的系统, 不太一样,在时效上会稍逊一筹; 但是在扩展上面将会便捷强大得多; 他本身致力于服务于万千被拒之于在现有交易平台、系统之外的人们, 目标是任何一个能连上网的人都可以交易。

任何一种产品,金融非金融的,只要有价格波动的均可交易; 这个让我回想起很多OTC 产品,听起来都让人摸不着脑袋, 比如天气里面的温度雨水等; 但是他们都是一些非常优质的标的:公开、透明、唯一且权威。

所以某种意义上任何一种符合这样属性的产品, 都可以称为金融产品—当然需要有波动, 越频繁越好! 

比如上海二号线人民广场4号口每天的人流量。 如果这个作为一个数字进行交易, 很有可能变成一个非常炙手可热的产品。

所以某种意义上, 金融产品和菜场里面的菜,百货市场里面的衣服、电器没有本质差别。

如此一来一个交易系统, 不就是一个淘宝系统吗?

嗯, 差不离,基本挺像,但是有些特征非常具有挑战是淘宝没有的(后续再表述)。 所以总体来说设计、实现一个交易系统其实并不十分难, 但是这个是让人听起来挺高级的玩具。关键他可以为自己所用,真正来源于生活,最终服务你生活日常。

言归正传, 这个系列的文章将分为下面几个部分(可能后面会修改调整)来探讨, 一个是对团队埋头捣鼓做些总结, 另外抛砖引玉,寻找更多志同道合同学共勉学习。

专题

交易系统是什么: 交易系统主要功能和特点。

交易系统几大模块: 参考 CMEGroup, 剔除不需要的留下最核心的东西。

交易系统需求: 不管采用何种底层技术,一个交易系统在技术上比较满足条件。

技术架构选择: 围绕需求, 如何选择技术框架, 这里以 JAVA 语言下平台为主。

搭建交易系统: 着手开始设计,分模块,列接口。

如何测试: 压力压力压力, 正确性。

准备上线: 终于可以解脱了?吗?

展望: 算法交易, AI 其他

这将是一个初级的交易系统, 不能保证在真实的环境中能够非常平稳的工作, 同时由于部分工作的问题, 内容细节多有删减和调整。 

服务模块

这篇文章主要参考 CMEGroup 里面公开的服务模块, 当然最终我们的系统里面有些部分会简化或者丰富,先30000英尺高空俯视他们系统提供了哪些服务 (链接):

服务模块

清分 Clearing

最下面是clearing 中文翻译过来叫清分, 这里有必要区分几个名词:清分;清算;结算, 知乎了一把:

清分  Clearing =  记账

对交易数据依据机构和交易类型进行分类汇总,并计算结算金额的过程。

清算 Settlement = 算账

指根据清分结果对交易数据进行净额轧差和提交并完成资金划拨的全过程。

结算  Settlement of Accounts = 转账

指完成客户账户间资金划拨的全过程。

记账从字面上理解非常明了, 但是在交易系统里面的记账, 是个非常复杂有挑战性的模块, 由于交易频次非常之高, 保证 transaction 一致性会是个挑战。

这个模块也是整个交易平台的基础、核心之一, 如果钱都没有记对、算对, 其他部分就皮之不存毛将焉附;

CMEGroup 里面说了他们 clearing 系统每年处理金额大概在 $1,000 trillion(万亿),这个是流水,应该是合同的面值,也会包含杠杆部分; 但是这个也是非常牛掰的量。

万分之一, 足够一个公司活下来了。

交易平台

大部分人的注意点在这里, 这里也是占整个交易系统非常大部分的模块。

匹配系统

根据不同的匹配算法,撮合成交订单, 最简单的匹配的逻辑其实很容易理解, 买卖单子按照价格分别排两个列表, 买价(BID)从高到底(买价越高越容易成交),卖价(ASK)的从低到高(卖价越低越容易成交)。

然后入砌俄罗斯方块,买价格大于等于卖价格,这两个单子就匹配好了。

比如市场现行情如下, S&P500, 最高买价(BID), 2041.00; 最低卖价(ASK)2041.25, 点差0.25(记得,还有上文说的做市商), 如下你现在下个 在2041.25 买 100 的委单。  

撮合

ps: 这个一般在股票,期货等软件中很常见。这个过程一般称之为【撮合报价】,一般应该还是上清所,CFETS 这些国有的机构完成。其他人只是接入接口而已。

刚好有卖价在2041.25的单子150个,当然这150个有好几个人下的, 从上到下,依次成交,直到最终100个单子全部被匹配掉,还剩下面的50个。

匹配系统, 其实只干了这一件事情; 同时匹配系统需要把价格广播出去, 在我们这里, 价格没有因为此单影响, 最终还是, 2041.00 / 2041.25; 如果用户买了200个, 那么价格就会调整到, 2041.00 / 2041.50; 这个时候可能更多人买, BID 价格会继续往上涨。

或者流动性供应商注入更多的 BID 单子, 最终点差稳定一定范围内, 而价格根据买卖双反的博弈,往上或者下移动, 大家可以参考市场深度图, 可以看到买卖双方那方更强。

如下面的市场深度图可以看出那方更强:

深度图

价格

从匹配系统出来的价格就是我们日常说的价格, 还有五档数据, 历史数据, K线整合等等, 就是另外一个模块,专门负责价格的聚合整理和广播出去。 

下单

负责接收你买卖指令的地方, 在发送到匹配引擎前, 需要做风险检查, 比如资金充沛与否,是否在黑名单, 账号是否被锁定等等。 

ps: 诚如作者所言,这个过程看起来很高端,实际和你去电商买商品是一样的。区别可能是电商的商品价格相对固定。

风控

风险的监控和管理,在开市的时间, 特别当海量的单子涌入系统, 用户的持仓,现金,占用保证金,浮动盈亏等都在不停的变化, 对风险的实时监控是个非常大的挑战。

前置机(Front-end)

嗯, 最终到我们用户可能接触到的地方, 这里控制用户入口, 用户 session 会话的管理,输入订单,取消,查询订单信息, 查看账户持仓比等等信息。

规则引擎

这里的我们所说的规整是关于交易的, 有个专有的名词: 算法交易、量化交易(Quant);

ps: 量化一般是在股票、债券、基金、期货等金融领域,一般的支付公司很少有这个概念。

一听就是个非常高大上的名称, 特别在集合当今热门大数据和AI; 提供这样服务的公司更是如雨后春笋遍地开花, 这可能的确也是未来的交易方向, 据说现在排名靠前的基金投资公司, 大部分的投资决策是靠计算机规则驱动。

这也不难想像, 在过去, 主要靠“老司机”喊单, 哎, 金叉,或者死叉发生了,大家快加仓或者减仓!到某个压力位了, 大家注意了等等;  MACD, RSI, KDJ, BOLL, 能上的指标和参数都给上了。

整个交易页面看起来如蜘蛛网一般,让人感觉上好像很牛掰; 但是靠 “老司机” 喊单, 一双肉眼观察还是慢了, 老司机也不能记得所有的参数指标, 记得的历史价格范围有限,有时候还眼花,或者有个人情绪; 这个时候计算机就派上用场了;于是乎人们把这些经验电子化, 建立相关的计算模型;然后把某种产品的过去几年历史数据都跑一把, 看能否跑赢大盘,不断调整自己的模型, 最终验证自己的方法盈利相当很不错,然后动真格了, 拿真金白银操练, 完事了最后还是亏? 

历史是惊人的相似, 但是推动历史的却是黑天鹅事件。

有个比较有意思的模型, 每次投资都在上次基础上双倍投, 按照理论只要你钱够多, 只要最后一次中了, 这样你就可以绝对赚, 但是这个只存在想象的世界里面, 事实证明是错误的,首先你没有无限多资金;现在很多的模型,已经远远超过一般人能够理解的范围, 都是些研究金融,数学,甚至物理的博士搞出来; 日常我们接触比较多的止损, 其实是一种最最简单的模型, 当价格触及设定的止损线, 就立即在市场上面下一个市价单,这个其实就是个自动化交易的过程,以后你也可以跟别人说你也在搞量化交易!

结合现在的大数据和AI, 量化交易想象的空间将非常之大, 如果能将这些金融工具能够以非常便捷、低成本, 低门槛的带给我们云云个人投资者,对于投资领域无疑如普罗米修斯之火, 达到真正意思上的金融面前人人平等。

ps:这段时间在读一般书《生命 3.0》,其实里面也提到了普罗米修斯这个 AI,也许未来的金融战场,会变成 AI 与 AI 的战斗。

后交易(POST TRADE)

传统意义上, 这部分可能针对 OTC 比较多, 由于OTC交易的复杂性, POST 阶段有非常多的事情需要处理, 对比校验, 确认,再确认等。

投行后面一般都有一个庞大的团队负责处理这部分的工作, 所以他们现在更趋向于推荐 STP Trade,  笔者在摩根时候的团队花费很大部分精力处理这部分的工作, 聚合每个trade, NEW, CONFIRM, SETTLE, MATURE 等不同阶段花费的时间。

从而可以找出,哪个步骤有瓶颈, 这里影响到公司不少一部分的利润。 

其实整个交易的生命周期, 海量的数据被生成, 在过去,这些数据也许只能作为一个备份,放在哪里, 现如今大数据处理日趋成熟, 对于这部分数据的挖掘,兴许能够发现很多意想不到的结果!

下面系列,将结合我们的业务, 更多谈到技术相关的部分, 业务的部分本人也是班门弄斧, 略懂一二, 大部分的见解也是管中窥豹, 理解可能多有偏见和不足, 欢迎拍砖!

交易系统需求

一个交易系统需要满足那些要求?

交易系统和其他系统,既有共性,又有很多的不一样地方; 不像其他领域需求比较不确定、多变,对于金融行业,很多业务背景其实几十年,甚至上百年来都没有改变;

很久以前没有电脑时候大家是这样玩的:

竞价

可以想象大家济济一堂,熙熙攘攘,吵吵闹闹,对着黑板数字口若悬河,眉飞色舞;和现在对着k线大谈一通其实没有什么差别, 只不过过去人与人更近点。

但凡能够打动人类灵魂深处的东西, 比如*黄**赌***毒,都能激发人类的无限的想象力和创造力,比如现时流行的视频社交,其实玩线视频最溜的,技术最为先进的,是当今世界上最大不可描述网站某por**hub; 据说最近在玩AI,已经远远超过很多纸上谈兵的研究机构。

交易系统也是,这样一个充满荷尔蒙,总是让人蠢蠢欲动,欲罢不能的产业,更是在玩家,庄家,服务提供者们的共同努力下,将技术的运用推到了极致;互联网和移动化,更多的参与者涌入,如何做好这个国民手游,是个非常挑战而又有意义的事情。

安全/高可用

对于金融系统来说, 这是个硬性指标,安全可能分不同类别, 比如企业的内部环境,金融系统一般有自己专有网络环境, 对于办公室场所上班行为也有非常严格的控制,毕竟往往堡垒都是从内部攻破。这些都有相应的标准、规范和法律, 基本有专门的团队负责这块东西。 

和我们交易相关联的部分, 很大部分是数据和平台的安全, 平台的安全,信息交互的通道安全否, 平台是否稳定, 数据存储安全否,防篡改, 是否有冗余备份,防止丢失。

对于数据通道存储安全,使用加密算法,现在一般都比较成熟, 业有规范的行业标准。 

对于平台的安全也就是灾备, 现在的金融系统, 最主流的灾备技术是两地三中心(即生产数据中心、同城灾备中心、异地灾备中心)。

参考下阿里高可用技术架构: 第一个是做了同城的双活,第二个做了异地只读及冷备,第三个是做了异地多活,这块, 基本专门系统团队维护, 对于我们设计开发的团队, 接触不多。

上面的文章, 可能更多的谈到实现的过程, 没有具体实现的细节, 说到这里,其实阿里云上有专门针对金融系统的解决方案, 这里人家也是拿出看家的本领来分享,能够做到异地多活还是非常牛掰的,保证最终一致性可能是退而求其次的方案, 参考CAP 理论。

翻了翻还有不少: 写事情真不能100%完美退而求其次也不错:《异地多活设计辣么难?其实是你想多了!》 ;服务降级等在应用层中控制也是不错方案。 《面向业务的立体化高可用架构设计》;总之条条大道通罗马,八仙过海各有神通, 不拘一格,只要能解决问题的方法就是好方法、对的方法。 

保证高可用性,另外一个监控运维也是必不可少的步骤,毕竟三分代码,七分运维。总之成功的项目和每个部门、每个环节都密不可分。成功的项目都是类似的, 不成功的项目是千差万别的。

我们这里将主要分享的是, 具体的应用层面, 如何实现数据一致性, 下面会有专门的章节讲解,在应用层面如何使用 eventsource 等设计模式,保证数据一致性,和可追溯。对数据的冗余,更多会使用分片 partition 和 replication; 如何动态的调整不同节点之间的balance, 整个cluster集群的维护。 

性能

天下武功,唯快不破;对于交易系统,快代表这先人一步得到消息, 也就意味这更多、更佳的交易机会,和更好的盈利;所以人对交易系统延误的忍受会被远远的放大,可能你在刷淘宝的时候多1~2秒延迟你还能忍受,但是在交易系统,就像有句台词:我分分秒秒几百万的盈亏。 这有点夸张, 但是确实很现实, 特别在高风险、高杠杆化的交易中, 分秒就是盈亏的分界线; 所以性能除了我们上面说到的高可用性外,在单节点上如何实现性能最大化是在设计中必须要考虑的。 现有交易系统大部分基于一些更底层更高效的语言(c/c++), 但是语言始终是一门工具。能否达到预期的性能需求, 还是要看具体的设计和实现。 

这里演示的系统, 基础开发语言是java,可能熟悉编程语言的人怀疑java 能否胜任, 但是对于一个企业的开发, 有诸多的因素需要考量:成本,效率,投资回报率等等,其实对于性能的需求,同高可用性需求一样, 大家有诸多理解上的误区, 这也同大家对质量的理解一样, 质量是越高越好吗?

 答案不是, 是能满足你的质量就是最好的质量。你用一次性塑料袋去买捆菜, 非得要这个袋子镀金的,上面有印有精美的花纹的, 那是不现实的。 同样对于性能的需求,对于一个目标是服务于每个能上网的民众; 在如今基础上你也许很难达到毫秒级别,即时能够达到, 付出的成本也许大大超出你的承受范围,所以做生意或者做人一样都得有点妥协,这是么有原则性吗?不是!这恰恰是最好的原则; 如果我就需要100%高可用性,所有单子都需要在1毫秒内成交--这个才是绝对不能的!

于是乎, 达到恰好是资源、能力范围内能够达到,最好的、最大满足性能需求的设计,便是当下最优的设计,不要over design 才是对你投资人老板最好的负责!

除了 java 是基础的语言, 也有必不可少的中间件:缓存、 内存网格、消息等, 下篇章节将一一细述。

PS:  对于某些细节过分的追求,可能是大部分技术出生的工科男的一大优点也是一大通病, 项目中最大的风险固然是时间成本不够,还有一个潜在的隐形杀手是项目镀金!一个项目必须100%完美才能上马?非的花费大部分时间去优化一个1%性能提升的模块? 不是的。特别对于IT项目,等到完美的那一刻, 也许这个项目永无上线之日。 

一点细节

在记下来介绍具体细节前, 有必要轰炸一些名称, 这些名称和以往的架构设计方式稍有不同,特别如果没有接触过这些概念, 首次理解和使用起来将会比较烧脑。 

  1. DDD: Domain-driven Design,领域驱动设计

  2. CQRS: Command Query Responsibility Segregation, 命令查询分离。

  3. EventSource: 事件源

  4. CAP

下面只能做粗略的介绍这些概念, 他们每个都有独立成书来深入阐述的, 当你联系实际逐渐理解其中的微妙之处, 在实践应用中便会产生共鸣,自有亮点和可取之处,但是需要集合自己领域特征、业务背景和团队的素质,公司的组织资产,不可一味强求。

DDD: 领域驱动设计

DDD

DDD: Domain-driven Design,领域驱动设计; 在软件设计领域有各种各样的pattern; 领域驱动设计, 也是一种设计的套路;Domain 领域; Design 设计; Driven 驱动;DDD 理念提出者是 Eric Evans, 可以参考他的书 《Domain-Driven Design: Tackling Complexity in the Heart of Software》; 中文亦已出版 《领域驱动设计 软件核心复杂性应对之道》, 上面的截图也是来自此书。 

领域(Domain)

领域是一个比较大的概念, 我们构造的这个系统是解决那个领域的问题, 比如一个航空管理系统, 一个咖啡馆,一个银行系统, 一个保险销售系统,一个在线电子商务系统, 一个交易系统等等。

一个系统可能跨不同的领域, 比如我们零售系统,他势必要和货运(快递)系统, 报价系统, 和推荐系统等由紧密的联系。 

某个领域就是为了解决这个行业的痛点, 比如我们要做一个去中心化的交易系统,就是为了解决,现在的交易系统, 费用高,效率低,透明度低,门槛高等。

我们就是需要让所有的人, 可以随时, 随地,随心的交易全球的金融产品。 这个就是我们定位的领域。

模型(Model)

“A useful approximation to the problem at hand.” – Gerry Sussman

比如我们所一个 Person 人的模型, 其实他不是一个真正的人, 他仅仅是代表一个人的模型,他不具备一个人的所有属性(AI将来说不定);可能在我们问题的上下文中(context)中,我们不需要一个人的所有的属性, 我们不关注这个人血型、身高等等; 不同领域中,对同一个对象可能塑造不同的模型, 这个看你的领域的关注点和切入点, 比如最简单的 员工 模型, 在人力资源部, 和销售部门肯定有不同的意义。 

上下文(Context) 这个是个比较重要的概念, 解决问题都离不开上下文。

领域模型(Domain model)

为这个领域建立的模型

设计(Design) &  驱动 (Driven)

DDD中的设计主要指领域模型的设计, 强调领域模型是整个系统的核心,领域模型也是整个系统的核心价值所在。每一个领域,都有一个对应的领域模型,领域模型能够很好的帮我们解决复杂的业务问题。

领域模型包含了领域和代码实现,确保了最终的代码实现就一定是解决了领域中的核心问题。

因为:

1)领域驱动领域模型设计;

2)领域模型驱动代码实现。

我们只要保证领域模型的设计是正确的,就能确定领域模型可以解决领域中的核心问题;同理,我们只要保证代码实现是严格按照领域模型的意图来落地的,那就能保证最后出来的代码能够解决领域的核心问题的。这个思路,和传统的分析、设计、编码这几个阶段被割裂(并且每个阶段的产物也不同)的软件开发方法学形成鲜明的对比。

DDD中,我们总是以领域为边界,分析领域中的核心问题(核心关注点),然后设计对应的领域模型,再通过领域模型驱动代码实现。而像数据库设计、持久化技术等这些都不是DDD的核心,而是外围的东西。

领域驱动设计(DDD)告诉我们的最大价值我觉得是:当我们要开发一个系统时,应该尽量先把领域模型想清楚,然后再开始动手编码,这样的系统后期才会很好维护; 但是在实际的操作中非常难,很多人很难把系统的边界分得很清楚, 其实有时候能分清楚,用不用DDD 倒无所谓了。  概括起来DDD 有一下几个特点:

  1. 领域就是问题域,有边界,领域中有很多问题;

  2. 任何一个系统要解决的那个大问题都对应一个领域;

  3. 通过建立领域模型来解决领域中的核心问题,模型驱动的思想;

  4. 领域建模的目标针对我们在领域中所关心的问题,即只针对核心关注点,而不是整个领域中的所有问题;

  5. 领域模型在设计时应考虑一定的抽象性、通用性,以及复用价值;

  6. 通过领域模型驱动代码的实现,确保代码让领域模型落地,代码最终能解决问题;

  7. 领域模型是系统的核心,是领域内的业务的直接沉淀,具有非常大的业务价值;

  8. 技术架构设计或数据存储等是在领域模型的外围,帮助领域模型进行落地;

ps: DDD 这本书我几年前看过,虽说也有些收获,但是感觉实践起来有些难度。毕竟国内员工水平稂莠不齐,也喜欢追求所谓的“敏捷”。

CQRS

命令查询的责任分离Command Query Responsibility Segregation (简称CQRS)模式是一种架构体系模式,能够使改变模型的状态的命令和模型状态的查询实现分离。

这属于DDD应用领域的一个模式,主要解决DDD在数据库报表输出上处理方式。

Greg Young 是提出来此概念,布道者人之一,  Eric Evans on How Technology Influences DDD 。

下图来自 Martin Fowler CQRS

CQRS

命令(Command)

所有对于领域模型状态的修改都必须通过命令完成。

命令一般都是一个形象化的动词标识。比如 DepositMoneyToAccountCommand; 顾名思义就是往一个account 上面充钱。 如果这个命令成功, 他必然涉及到核心 领域模型的 状态转换。比如这个账号上面金额增加或者减少,

事件(Event)

事件标识一个已经在领域发生了的状态改变。注意这个事件是历史的,以及发生完了, 所以是永远不能改变的。这个我们再下面的事件源中将会再说一把。

一般以过去词代替, 比如MoneyDepositedToAccountEvent;  无论命令还是事件, 都需要遵循immutable原则; 这个比较直观, 既然已经发出的命令, 和产生的事件, 就不应该再改变, 保持他们的不变性,会给你整体架构设计带来意想不到的便捷,但是一旦打破由会带来无尽的烦恼。 

查询(Query)

Greg Young 把领域模型分为两种:状态校验,以及状态转换,维持当前状态的一个视图, Command 和Event 对于前面, 那么查询, 主要针对 Domain 状态查看。 

在传统意义上, 我们设计一个系统,对于数据的操作, 无非CRUD, 就是下面的模型:

Query

对于简单的应用, 这个已经非常够用,但是随着业务的复杂度增加,往往更多的过程和信息需要披露, 我们需要知道整个状态变化的历程, 同时需要回溯到某个点的某个状态。

CQRS 主要在分离这个词上面, 当今应用程序产生如此复杂性的原因之一在于对贫血领域模型的大量使用,组成这种模型的实体只包含数据,由与之分离的服务负责处理逻辑。

另一个原因在于对数据进行读取与变更时使用了相同的接口。

缺乏读写分离性是真正的问题所在,正确的方式是将数据的查询视为一种完全不同的关注点, 实践应用中对于数据的读写往往有下面一些特征:

  1. 对数据的读取往往比写入频繁得多

  2. 在读取数据时,我们通常会获取大量数据,或是一个数据列表。与之相比,对数据的写入通常只影响一个单一的聚合。

  3. 从用户的角度来看,数据的读取应当表现出比写入更高的性能。对于用户来说,在进行数据变更时产生一些拖慢的现象更易于接受。

事件源(EventSource)

涉及到 DDD & CQRS 必然会谈到事件源, 其实事件源,是一个很好的设计方式,如果你熟悉RDBMS的transaction log 设计方式,可能对此感同身受, 事件源设计方式,就是把系统所有发生的事件都存储起来。

事件源设计方式好处:

  1. 可以把系统回溯到以前任何一个状态(debug, diagnose, support 等等)

  2. 有整个系统的历史, 方便audit , 追踪分析

  3. 多种下游可以消费

  4. 便于扩展, 系统之间协调等

事件源, 不是一定非得配合CQRS 使用, 对于大部分系统可以非常便捷的引入,特别对于现在大数据分析追踪,事件源将非常有帮助。

CAP

Consistency(一致性):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)

Availability(可用性):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)

Partition tolerance(分区容错性):两个复制系统之间,如果发生了计划之外的网络连接问题,对于这种情况,有一套容错性设计来保证。

CAP 理论,任何分布式系统只可同时满足二点,没法三者兼顾,架构师不要将精力浪费在如何设计能满足三者的完美分布式系统,而是应该进行取舍。

CAP

大家比较熟悉的一般是针对我们关系数据库的ACID模型,满足高可用性和强一致性,也就是上面的CA部分,而对于分布式事务一般采用2PC(two-phase commit),比如J2EE 中的JTA来实现, 但是2PC is the anti-scalability pattern (Pat Helland) 是反可伸缩模式;而对于web 2.0 时代,对于数据强一致性需求没有那么突出, 部分牺牲高强一致性, 换取可用性或可靠性,于是有了BASE模型:

  1. 基本可用(Basically Available)

  2. 软状态(Soft state)

  3. 最终一致(Eventually consistent):两个复制系统之间,如果发生了计划之外的网络连接问题,对于这种情况,有一套容错性设计来保证

当有网络分区情况下,也就是分布式系统中,你不能又要有完美一致性和100%的可用性,只能这两者选择一个。在单机系统中,你则需要在一致性和延迟性latency之间权衡

当然,牺牲一致性,并不是完全不管数据的一致性,否则数据是混乱的,那么系统可用性再高分布式再好也没有了价值。牺牲一致性,只是不再要求关系型数 据库中的强一致性,而是只要系统能达到最终一致性即可,考虑到客户体验,这个最终一致的时间窗口,要尽可能的对用户透明,也就是需要保障“用户感知到的一致性”。通常是通过数据的多份异步复制来实现系统的高可用和数据的最终一致性的,“用户感知到的一致性”的时间窗口则 取决于数据复制到一致状态的时间。

最终一致性(eventually consistent)强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。 亚马逊首席技术官Werner Vogels在于2008年发表的一篇文章中对最终一致性进行了非常详细的介绍。

最终一致性;可以分为从客户端和服务端两个不同的视角。从客户端来看,一致性主要指的是多并发访问更新过的数据如何获取的问题。从服务端来看,则 是更新如何复制分布到整个系统,以保证数据最终一致。一致性是因为有并发读写才有的问题,因此在理解一致性的问题时,一定要注意结合考虑并发读写的场景。

在实际工程实践中,最终一致性存在以下五类主要变种:

因果一致性。如果进程A通知进程B它已更新了一个数据项,那么进程B的后续访问将返回更新后的值,且一次写入 将保证取代前一次写入。与进程A无因果关系的进程C的访问遵守一般的最终一致性规则。

“读己之所写(read-your-writes)”一致性。当进程A自己更新一个数据项之后,它总是访问到 更新过的值,绝不会看到旧值。这是因果一致性模型的一个特例。

会话(Session)一致性。这是上一个模型的实用版本,它把访问存储系统的进程放到会话的上下文中。只要 会话还存在,系统就保证“读己之所写”一致性。如果由于某些失败情形令会话终止,就要建立新的会话,而且系统的保证不会延续到新的会话。

单调(Monotonic)读一致性。如果进程已经看到过数据对象的某个值,那么任何后续访问都不会返回在那 个值之前的值。

单调写一致性。系统保证来自同一个进程的写操作顺序执行。要是系统不能保证这种程度的一致性,就非常难以编程 了。

上述最终一致性的不同方式可以进行组合,例如单调读一致性和读己之所写一致性就可以组合实现。并且从实践的角度来看,这两者的组合,读取自己更新的数据,和一旦读取到最新的版本不会再读取旧版本,对于此架构上的程序开发来说,会少很多额外的烦恼。

从服务端角度,如何尽快将更新后的数据分布到整个系统,降低达到最终一致性的时间窗口,是提高系统的可用度和用户体验非常重要的方面。

对于分布式数据系统(参考鸽笼原理/抽屉原理):

N — 数据复制的份数

W — 更新数据是需要保证写完成的节点数

R — 读取数据的时候需要读取的节点数

如果W+R>N,写的节点和读的节点重叠,则是强一致性。例如对于典型的一主一备同步复制的关系型数据库,N=2,W=2,R=1,则不管读 的是主库还是备库的数据,都是一致的。

如果W+R<=N,则是弱一致性。例如对于一主一备异步复制的关系型数据库,N=2,W=1,R=1,则如果读的是备库,就可能无法读取主库 已经更新过的数据,所以是弱一致性。

对于分布式系统,为了保证高可用性,一般设置N>=3。不同的N,W,R组合,是在可用性和一致性之间取一个平衡,以适应不同的应用场景。

如果N=W,R=1,任何一个写节点失效,都会导致写失败,因此可用性会降低,但是由于数据分布的N个节点是同步写入的,因此可以保证强一致性。

如果N=R,W=1,只需要一个节点写入成功即可,写性能和可用性都比较高。但是读取其他节点的进程可能不能获取更新后的数据,因此是弱一致性。这种情况 下,如果W<(N+1)/2,并且写入的节点不重叠的话,则会存在写冲突

我们的问题

我们需要搭建一个分布式去中心化的交易系统, 所以高可用性,对于我们非常重要, 同时他又是个交易系统,一致性也是必不可少, 这个系统需要可以说需要满足上面的 CAP 所有的选项, 但是理论和证实, 同时满足三样是不太可能的;于是我们得做些取舍,我们的平台首先是面向移动端平台,受众是年轻一代手机端用户, 大家可以在任何一个场景,都可以随意掏出自己的手机, 进行买卖全球的金融产品。 

高可用性, 高扩展性, 最终一致性, 成为我们系统必要的需求。

在综合权衡各方的利害后, 我们采用DDD设计方式。

怎么实现

其实对于上面所说的设计方式, 没有约定俗成的规范, 具体的实现在于和自己业务场景, 组织环境有关, 同时理论创建者本人也建议大家不需要必须套具体的模版和框架。

可以说DDD本身的意义就在于, 更好的和你的业务模式融合,一最自然的方式, 那么我们这里还是选择了一些开源的框架, 而没有去自己造轮子。 

  1. Axonframework 提供DDD基本的结构, 当然这里我们做了很多的取舍以提升性能

  2. KAFKA 作为消息中间件,系统的事件由KAFKA分发出去

  3. APACHE Ignite 作为内存计算网格

  4. Redis作为不同模块之间交互渠道,放些公用小的reference 信息

下面将分析, 几大业务模块, 然后就是上面几个框架在我们使用中的使用。

框架使用

根据上篇DDD 的思想方法, 首先我们需要分清系统的边界, 相同 Domain 的放一起。

分解业务模块可以遵从软件设计的思想,保持高内聚、低耦合; 相同的业务在同一个 Domain 中处理,按照DDD设计的思想: 一个Domain 内业务应该只有一个Aggregate Root(聚合根) 对外提供服务, 内部可有多个子Aggregate Root,但是必须通过这个主的Aggregate Root 对外提供服务接口, 我们选择使用的CQRS 框架 AxonFramework,作为我们的架构基础, 所以下面的若干章节将结合框架的特征和我们的业务特点分解我们的业务模块。

概述

可参考我们的 《如何构建一个交易系统(三)》 的背景; 一个交易系统大概可分为, front–>middle–>back-end office, 性能要求几乎是呈梯减态势, 前端机直面前段流量的冲击, 所以必须高可用性、高响应,高吞吐量; 中端做进一步的校验,和信息的丰富; 后端系统, 做分析, 报表, 整理,风控,现金流等等。

Book

首先从最核心的 book 系统开始, 用户打的单; 流动性供应商报单, 这些所有单子, 最终汇聚到 book 系统。

快速的下单、撤单管理订单是一个book 系统必需满足的条件。 book 系统把收到买单、卖单,按照价格的高低, 排好序, 触发匹配成交。 book domain 需要提供基本的对外服务包含:

  1. 下单(市价,委托,止损)

  2. 撤单

Book Domain 需要传递的信息(Event 方式)

  1. 单子状态(进入book列表,部分成交,成交, 拒绝,取消,取消失败,超时,出错等等)

  2. 当前 book 买卖价格(最高买, 最低卖),以多档形式(1档,5档)传递出去,报告,或者为pnl 所用等。

表面看来 book 业务非常简单明了, 从代码层面看, 就是两棵树(Tree数据结构), 每次打单往两个树上面添枝加叶, 如果最高的买价,高于最低的卖价格, 那么两棵树都会被削掉顶端,直到平衡。

Tree

Booker Aggregate Root 同时还有自身的状态需要维护, 比如临时关闭 booker,还有部分参考和调整参数:

比如报价停顿多长时间, 可以判断 book 有异常,进而停止接收单子; 没有足够流动性如何处理,是失效还是拒绝等, 需要设定price banding?

如果价格出现特殊波动。

要保证 book 系统高并发、 高吞吐,一般做法把这些数据结构放置内存中做处理; 但是对于 booker 系统有个无法避免的限制, 单个产品的 book 无法分布到两台机器上面; 所以最大限度是一个 book 占用一台机器,后续只能竖向扩展、优化单机的性能,或者 book 算法优化。

全部放置内存如何保障高可用性?

这里需要借助于 EventSource 里面的概念(下面有独立章节讲解), 每个进入book 的命令(下、取消单)都会触发一个book 的状态转变, 这些转换以 Event 方式落地, 而整个Aggregate Root 状态的变更, 又会定期以snapshot方式落地, 这样可以保证即使节点崩溃, 也可以以最快的速度恢复到最后一致状态, 参考log 或者git 工作的方式,每次修改都有相应的 check-in list, 可以按照需要恢复到任何一个历史版本, 这个也是Eventsource 带来的好处, 其实这也是现实世界非常自然的处理方式。

对于 book 的每次增删改查, 需要保证线程安全,具体如何保证, 下面的章节 Disruptor 的运用会详细描述。

需要说明book 外层有层薄的前置机(front)功能, 主要检查账号的可用性, 当然这里不能直接hit 数据库, 基本的信息都会保存在内存中,后续会有详细描述Apache Ignite 如何运用。

Portfolio

Portfolio 其实叫account 系统更通俗易懂点, 但是他又不是传统意义上的account 系统, 他不提供用户角色,权限,登录,名称修改等功能, 所以命名成portfolio 可能更合适, 这里只保存中性的信息:

  1. 可用现金

  2. 占用保证金

  3. 用户持仓

可能大家问,用户浮动盈亏呢?

这个我们单独撇出来, 因为这部分模块也是遵循 CQRS 设计模式, 每个账号对应一个Aggregate Root,对于高频的价格波动,由于每次涉及 Aggregate Root 状态变动, 和关联事件的落地,所以 CQRS 无法实现高性能运算,AxonFramework 试图借助AKKA persistence 功能优化Eventstore 部分性能瓶颈,本来打算参与他们的Bata测试, 但是事情不了了之, 价格的波动不属于 domain 东西, 所以没有放在 portfolio domain 中, 有独立的query 端模块负责这部分工作。

一个Portfolio Domain 主要接收的command 有:

  1. 充钱 deposit

  2. 取款 withdraw

  3. 仓位变动

充钱 & 取款

这个涉及 aggregate root 可用现金的支配

仓位的变动

用户建仓, 减仓, 也就是 book 中的成交信息会传递到 Portfolio Domain 中,引起Portfolio 聚合根的状态变更, 这个是 Portfolio 接收最多的状态变化请求。 每个订单的匹配都会涉及两个账号的仓位信息的变更。订单的成交会导致:

  1. 仓位的增加或者减少

  2. 仓位平均价格变化

  3. PNL 的产生

仓位增加

同方向仓位变化, 比如同样做空、做多, 涉及:

  • 仓位的累加

  • 平均价格调整

  • 保证金累加

仓位减少

反方向仓位变化, 比如当前多单, 做了个空单, 或者平仓:

  • 仓位累减, 净仓可能变成反向或者继续保持原来方向

  • 仓位价格, 保持原仓位方向, 平均价格不变;反方向,反方向价格

  • 保证金减少

  • PNL 生成

  • 可用现金调整

Portfolio 状态的变更,同样以Event 方式传递出去; 以供下游系统使用可包括: Floating PNL 运算, margin call 触发等。

采用Eventsource 的设计方式,集群replication,partition 方式保证单点失败后能够以最快的速度恢复,保证状态的一致性。

Query

每个Aggregate Root Command 一般配合一个Query端使用, 外围状态查询不会直接抵达 command 端, 同时command 端的信息都是尽量无状态、normalization 化; 所以不适合直接供客户查询使用。

从 aggregate root 中生成的 event 通过 ESB, 感兴趣的模块可以选择监听接受, 也就是我们的Query 端, query 端,可以进一步对数据进行丰富、demoralization、缓存、落地到查询数据库。

Query

Order

Book 中订单状态信息, Order query, 负责这些数据的丰富, 落地, 供用户查询这些订单信息, 这个模块非常简单, 一般的 order 都是朝生暮死, 对于快频的日内交易来说。

Account

这个才是我们真正用户接触最多的地方, 比起我们aggregate root 中的portfolio, query 中的account 信息多了浮动盈亏属性: 浮动盈亏顾名思义是根据现价不停调整, 在高杠杆的交易系统中, 浮动盈亏的运算涉及到margin call、隔夜费, 和其他风险的计算和处理, 所以尤为重要, 对于spot trade 这部分的困难度将不在一个数量级, 远远没有我们这里的交易系统复杂。

查询端的账号体系提供:

(1)账户基本信息查询

可用资金

占用保证金

浮动盈亏

持仓比

(2)持仓

持仓方向数量品种

浮动盈亏

(3)结算

每单PNL

结算细节

浮动PNL

在开市时间, 用户有持仓, 就需要计算持仓浮动PNL和整个账号的浮动PNL.

如上文所述,在spot trade, 如我们买卖股票(无融资融券),浮动盈亏的计算, 没有特殊的时效需求。

但是对于高杠杆(50/100/200)的交易, 由于涉及margin call,浮动盈亏所以需要近实时的计算。

可以想象如果你有十万用户, 每人十个持仓, 总计100万持仓, 每个产品价格每秒变动3次,同时要在用户在不停调整自己的仓位的情况下聚合每个仓位的浮动盈亏到账号的总浮动盈亏上, 这里的运算和量将会非常的复杂和巨大。

我们采取的是将用户进行partition, 采用多台机分布式运算; 以满足高可扩展和高可用性需求, 浮动盈亏将影响用户进一步加减仓, 和后续的margin call触发, 所以此模块我们借助于 Apache ignite 内存数据网格, 实现在内存中大规模的运算。

ignite 很好的分布式支持, 和简单直观的落地解决方案, 可以很好的支撑我们的业务。 下面将有专门章节讲解Ignite的实践和运用。

MarginCall

MarginCall 一般存在于杠杆交易中(或者融资融券), 相当于你借了别人的钱在投资;如果成功大家皆大欢喜,但是如果投资失利,借贷方为了保证自己利益,会选择撤资。

margin call 在你的保证金,不足以抵消仓位损失的情况下, 将强平你的仓位,资不抵债,破产啦, 参考我前文举例。

浮动盈亏计算的效率, 其实将大大影响margin call 触发的概率,特别对于一些极值的点,没有计算及时将是比较大的风险,对于以此赚取利润这将白白让费好机会,对于以此规避风险可能导致更大的雪崩。

其实 margin call 是控制风险的一种方式, 对绝大部分投资者是一种有效的保护, 杠杆交易可能导致你亏损超过本金,这部分亏空需要你来补偿的, 输掉一套裤子,总比输掉一条胳膊好, 输掉一条胳膊又比输掉一条命好!

避免 margin call 最好的方法是控制仓位比, 勿贪婪。

这部分借助于ignite, 以reactive方法在浮动PNL 运算完, 达到一定benchmark 后, 触发margin call 处理。 由于现实中不同产品报价总是有先后,而价格又一直在波动, 所以很难界定一个合理的时间点。 应此margin call 触发后需要导致新一次重新计算。

隔夜费

在杠杆(融资融券)交易中, 隔夜费需要支付给平台和做市的人, 这个比较好理解, 100块本钱撬动200块的资产, 借的这100块需要付出利息的,不管你亏了还是赚了, 参考房贷等。

对于隔夜费不同人有不同的理解, 理论上一切合理, 但是细节不好推敲, 杠杆不是平台真的给你100块了, 账目上这部分钱甚至不存在, 房子是开发商名下的,开发商是借银行钱盖的, 贷款是你的,从银行贷的, 其实有没有凭空那100万钱?

隔夜费是个EOD job 挑战性不大, 如何做那个点账号snapshot 是有点细节考虑。 一个简单的 ETL, batch job 可以完成这部分的逻辑。

下面的章节将再分具体的模块, 和具体的框架,分享技术上的难易和特点。

一点金融题外话

对冲 (Hedge)

首先先区分下,对冲(Hedge)和套利(Arbitrage)的差异 What is the difference between arbitrage and hedging?:

Hedging involves the concurrent use of more than one bet in opposite directions to limit risk of serious investment loss. Arbitrage is the practice of trading a price difference between more than one market for the same good in an attempt to profit from the imbalance

对冲涉及同时参与两个相反方向的交易,来避免投资中可能出现的严重失利; 而套利是利用两个市场价格之间的不平衡来赚取差价。

比如A市场某个产品X卖价100,但是B市场卖价105,在A市场买入,到B市场卖出,可以无风险套利5块钱, 当然需要出去一些交易手续费等, 这个是套利。

对冲策略的“意义”在于去掉某种我们不想承担的风险!从而只保留我们想要的风险。

可以看到套利是“乐观”主义,而对冲是“悲观”主义--风险永远都在,有的我们是无法避免,唯有合理的防范,方能降低风险的影响。

为什么参与两个完全相反方向的交易的, 这里交易的一般不一样金融产品合约; Options、forward、swaps 其它一系列衍生品合约(Contract)会被用来对冲; 对冲可能导致你的利润减少,但是会大大降低你的风险,记得这点可能对于你下面的理解, 或今后的投资理念有一定的影响。

如果我们对某家公司有强烈的看法(好坏皆可),但是我们并非股市专家,在股市整体走势预测上没有优势,那么我们就可以选择买入股票(假设看好)+ 做空股指以获得纯粹的公司自身经营回报。 

这时回报 = 股票回报 - 整个股市回报(股指)= 自身经营回报。 

风险也变成了只有自身经营风险 -知乎

比如看好 google 股票, 但是对美国整体经济趋势不懂, 可以做多google 股票, 做空美国股指,对冲掉股市的风险 ,如果股市上涨50%, google 上涨70%,可以赚得20%; 如果股市下跌50%,google 下跌30%,我们还是赚得20%。只要我们判断正确,”即google未来会很好” (至少好过大盘),我们就能一直赚取google和大盘指数的差价, 这里有一前提需要强调:你得有某个方面经验,你的判断得非常有把握,知道google 必然好于整体经济,而不是盲目。

对冲的思想也扩展到金融领域的方方面面,我们可以对冲汇率风险,对冲利率风险,对冲某种原材料风险,只要你能想的到就行。但其思路从未变过,就是剥离我们不想要的风险, 其实对于我们日常生活这也是一个非常重要的概念。

详细的描述可以参考知乎里面的解释:对冲策略的意义在于哪里

CFD

上面讲了一大堆概念, 到底和我们要说的下面这个东西有什么关系?

CFD contract for different, 人称差价合约,是一种金融衍生品,Investor和CFD provider就一个标的资产(可以是股票、商品、指数、外汇、钻石、比特币…anything)未来某日的价值和当前价值的差价签订一份合约,到了那一天,标的资产价格上涨了,根据合约,CFD provider给investor支付差价,如果下跌,investor给CFD provider支付差价。

简单一句话CFD 是交一份保险, 在未来某天以当前的价格买入或者卖出某个金融产品,只不过这个行权是没有固定日期的,这样事必给提供方(maker, provider)带来潜在的风险,这份保险费用就是你的隔夜费,可以借此体验套利和对冲的不同意义!

所以CFD 的英文名字很精确,这里只有一个差,所有可以用数字衡量量化的标的都可以用来做合约。 这使他可以标的几乎所有的金融产品。你可以比如以上海明天 2017-11-05 的温度是否超过25度作为一个合约和别人进行交易。

CFD 金融产品的几点特征需要说明下:

  • 你不能拥有标的资产, 比如股票, 你不能享受股票的分红拆息等,但是也少了印花税(stamp duty)

  • 你可以T+0

  • 可以做空做多

  • 带杠杆

  • CFD 非标准化产品

  • 交易时间更长

可以说CFD给喜好逐利的人带来了一片乐园,但是所有的投资工具和方法,都只是一个中性的事物, 不随人的喜怒哀乐而改变,市场对于没有准备或者没有控制力的人说都是异常残酷, 其实人生又何尝不是?

刚刚前文说,对冲概念, CFD 最好是作为对冲工具来使用,比如按照上面的google 的例子,你可能买入持有 google 股票(真的google股东哦!), 然后再对美股指数, 做一个空单,CFD 由于是 Marigin 交易, 所以你只需要你投资额的1/200 或者 1/100的资金来对冲你的投资组合, 这更像一种对你投资的保险,但是以少量代价保护高风险投资资产是一件值当的事情。

特别在如果你的判断出现偏差情况下。

其实这里我想说的和CFD 没有关系,CFD 被很多人误用,这也导致CFD 被很多人诟病,CFD在很多国家没有得到正式的承认。

我要说的是:

金融平等

CFD 是一种能够很好对冲你现有投资资产的工具。

对于绝大部分的中小投资者(散户或者韭菜)来说,能够很好保护自己的资产是件非常难的事情, 投资股票经常被割, 投资房产又不够,加入私募人不要。但是 CPI 不等人,而 CFD 是现有金融市场上面一个为数不多可以用的上的工具。

据全球化最新的报道, 虽然整个世界都在扁平化, 但是整个全球的财富分配,却越来越不均,看似全球的物质、财富都得到了提升, 但是对于富有的阶层,这部分财富增长率更高, 相比较而言,剩下的群体其实被剥夺了愈多。

真正意义上的平等, 不在于打开大门, 放低准入的门槛, 同时得有相关的规则让每个人得到真正意义上的参与。金融世界里面的不平等,在笔者看来是诸多不平等比较突出和显著的一个领域。一部分人与生俱来不能享受更多金融体系带来的实惠。这个让我想起来一本金融书里面的第一句话: Adverse Selection and Moral Hazard,这两点又何尝不是当今社会我们最大的隐形成本?如今信息爆发的时代,看似每个人获得更多的机会,但是没有合理地借助适当的工具、体系和方法过滤去噪提炼,其实弱势者更有可能被欺凌。

金融平等、普惠的时代,必须有一种工具能够很好的捍卫个人金融平等,不管这个工具是否是CFD,但他需要满足:

  • 低门槛, 人人都可以参与

  • 低成本

  • 过程费用

  • 学习理解的成本

  • 高透明度

  • 高自由度

  • 自治, 自我监督和管理,和上面两点相辅相成

至于这个系统是自顶向下,还是自底向上实现, 笔者更趋向于后者,在互联网和移动思潮下, 其实有不止一个行业被这样改造;大到零售、物流、通讯、支付;小到吃饭、聚餐、看个电影、骑个车;这样的趋势将越来越明显,也必将成为主流, 未来的你我都将成为产消一体者, 至于中间的链节, 将被大大的压缩,甚至省去,达到某种意义上的零边际成本。

金融产品某种意义上和其它的产品没有本质的差别,但是金融产品整个环节的成本可能没有被大部分人注意,其实比起其它行业这些成本尤为昂贵,最终这些成本又植入到我们生活的点点滴滴,也许在不久将来基础设施、体系完善,条件允许下人与人之间的金融产品交易、转移未尝不可,这将大大缩小时间成本和加快流通的效率。

其实在金融领域已经有诸多的尝试和探索在这方面的理论和应用的可能性, 希望不久将来能够真正给金融领域带来普罗米修斯之火。

笔者的公司和团队, 也在试图在这个领域有所贡献和建树, 这是个美好而又艰巨的愿景,希望和无数同仁共勉。

JMM & Disruptor 入门介绍

JMM & Disruptor 入门介绍

数据存储

AxonFramework是我们交易系统选择的架构基础, 使用CQRS/EventSource 不拘泥于框架使用,其实不套用任何的框架,自己构建可能有更多的调整和细化的余地, 选用一个框架, 可以加快开发的速度-至少是前期, 但是也有很多框架上面的掣肘, 得失在于自己使用中的权衡,此篇主要讲讲 CQRS 中基础的 domain 中 event storage.

Command vs Query

Axonframework, 现在到3.0 版本, 由 Allard Buijze, 作为主要贡献者, 后面有一个商业化咨询团队支持:AxonIQ 的开源项目, 在JAVA 中是历史比较长的开源CQRS项目, 其实不久前有一个 reveno, 在一些存储落地方面采用更激进的方案, 但是现在好像不怎么维护, 既然要上production, 还是选择比较成熟, 社区比较活跃的开源项目。

Allard Buijze 本人也比较热心, 在社区问的问题基本在3天内能够回答, Axon对分布式支持不是太友好, 包括Event storage 的瓶颈, 有次专门写信给他们团队, 他们还是特意安排了个gotomeeting, 讲解了半个小时,非常热心, 对次Event source AxonIQ 提出了一个 Beta Programmer 试图使用 AKKA persistent Actors 解决, 最终这个项目没有太多的更新, 但是项目时间不等人, 所以我们改造了比较关键的几个模块, 能够更友好的对缓存和分布式支持。

Event Store 的瓶颈

在我们使用Event source 设计方案的时候,不能不讨论后面的落地方案 Event store; Event source 的本质不是只保存一个对象最新的状态,而是到达这个最新状态所有的历程和经过。

这一系列的过程和经历就是一个事件链,当然大家还习惯只在乎现在的状态,现在的状态可以从这系列的事件中得到, 在Event source 习惯称呼这个为projection.

Event source 不是一个新的概念, 由 Martin Fowler 十几年前就提出来了。

但是这个当时只是一个小众的技术, 现如今变成了主流,大概因为这样的理念更适合当下的几大应用:

数据挖掘

大数据, 机器学习,人工智能等告诉我们历史数据非常重要, 里面可能隐藏着宝贵的矿藏,有效的分析利用他们,可以揭示很有商业后面的秘密,这些都基于你积累了这些历史数据, 在传统的应用中,我们可能很少或者没有记录这里历史的数据和转变过程。而Event source 架构下, 需要记录所有历史状态,这样可以恢复到过去的任何一个状态。

在高分布式系统中, 一事件驱动的微服务设计架构已近变得流行, 如果你需要设计一个一事件驱动的微服务系统,他需要一个落地方案,Event source 是个不错的选择。

现在我们的系统需要面临越来越多的监管压力,有些业务需求我们展示系统发生的任何细节,Event source 可以满足这样的需求。

某种意义上来说, event 和其他数据类型没有什么差异,但是作为event source 中的event又有自身的特征。

映入我脑海的第一映象, event store 应该是一个 append-only 的数据库, event 代表过去已经发生的事情,于是他不可以删除和更改,比起CRUD,这样的特征让数据库设计更简单, 这样其实一个复杂成熟的关系数据库有点大材小用,在我们写入event 的时候还是要注意事务, 特别在一系列事件需要在一个事务中处理, 需要保证原子性操作, 还有同一个对象上面的事物, 需要有对象的sequence 序列,保证唯一性; 其实也就是相当于版本号。特别在并发访问的情况下需要保证一致性。

读事件同时有他的特点, 事件有时序性, 也就是每个有一个sequence number, 一般会批量的读取, 或者整个读取,比如在做replay的时候, 最近的数据趋向于更高概率被读到, 比如在从snapshot 恢复状态的时候, 这里时序性非常重要不能打破, 也就是某个聚合跟上面发生的事件, 一定是有先后关系的。

如何界定历史事件和当前事件会有点模糊, 事件更像是一个事件流,当一个聚合跟被创建的时候, 从某个点开始的事件被读出来,加上新进来的事件, 形成一个stream,以供消费,这些事件是连贯的,对于下面的聚合跟, 不会区分是历史的还是刚刚发生的。

扩展性和弹性

比较我们传统的仅仅保存一个对象的最后一个状态, event source需要保存更多的信息:他是保存导致状态变化的所有事件,这些数据将会不断的增长,单纯落地到磁盘上不是一个很大的问题, 挑战是要保证很高的读写效率, 在你的业务量增长后, 保证底层的db 的可扩展性非常重要, 多个节点冗余备份数据是个必要的选择。

Event Source 特有需求

一般Event source都要满足这些需求: 对象状态要做snapshot ,也就是特定的时间点对象状态的保存, 这个点的状态由之前发生所有事件得到最后状态, 做snapshot 不是一个必须的选择, 但是有他可以大大节约你恢复状态的时间,你不需要从头第一个事件来得到最后的状态。

同时在一个大型的系统中, 事件也是不同模块之间交互的桥梁, 这些事件有自己的上下文,所以在设计这些事件的时候要注意粒度, 可能需要在发送前进行,切分,聚合或者转换。

事件存储Event storage选择

更具这些特质我们可以对照现下我们的选择, 确实有很多的选择方案,一个传统的关系数据库, 一个简单的event table,可供选择的, 比如Mysql, Oracle, SQL server 等等。

很多公司开始时候趋向于选择他们, 一个是公司本来就在用, 带入成本低,经验比较丰富, 当扩展性渐渐变成瓶颈后,可能大家发现关系数据库和event source 不是最佳搭配,现在的数据库主要为随机的CRUD, 还有很多高级的sql 语法上面的功能, 但是对于event source这些不是必要的,而去有点多余, 他仅仅是为快速的批量读写事件信息,当今的关系数据库集群的代价还是比较大;而比起后起之秀的 MongoDB, 和ElasticSearch等等天然的集群和易扩展性就逊色不少。

MongoDB

MongoDB 是文档型DB, 可以以集群方式工作,很方便横向扩展,很适合做Event source, 每个事件可以做为一个文档, 但是基本弊端: 事务一致性, MongoDB事务是单个文档范围, 对于多个并发的事件不能保证事务一致性, 需要做些工作,而且对于事件源,需要保证读事件的顺序一致性, 而要做到这一点, MongoDB, 需要添加额外的索引来保证这一切,这些都需要额外的开支,在事件增加的情况下, 会带来更多的性能开销。

Kafka

Kafka, Apache Kafka 是一个高性能、高扩展性在事件驱动领域是比较流行的解决方案, 他既提供消息的pipline, stream, 可提供存储; 但是做为event storage, 还是有点弊端, 一个是消息的存储事件有限制, 默认是168小时, 虽然这个可以该,但是这个不是kafka的本意, 如果滥用这一特征,可能会带来不必要的性能问题, 主要的问题还在, kafka 更是一个消息中间件, 存储消息不是他的本质工作, 而event 需要不定时的读取, 而且这些事件需要关联到某个聚合根(对象),kafka 里面的消息是按照topic 来分, 客户端是按照某个offset来读取,很难按照某个对象来过滤获取这些消息,所以虽然kafka 在存储的量,和事件的吞吐量上面有很多优势,但是还只适合做一个消息的broker, 不能做为一个event source.

Cassandra

Cassandra 同样是Apache下项目, 是一个分布式高可用的DB, 通过在集群中保存多分复制来保证高可用性, 不需要指定特定的master, 这里Cassandra 可以更具用户的配置,来保证读写的一致性程度,一般使用quorum-based一致性来保证读写一致性, Cassandra, 在某种意义上可以满足我们Event source 中的高可用性, 一致性,事件发生的序列一致性, 需要依赖LWT(light weight transaction), LWT 的实现需要4(!) 4x3x2x1次在集群中协调实现写入操作(具体操作步骤笔者也不是了解太多),参考上面的问题,对事务的支持, 特别对多个管理事件的事务支持也有局限性。

可以看到现在市面上的种种解决方案都不能很好的满足我们的event source需求。现如今的这些通用的方式, 需要做适当的裁剪才能为event source 所用,鉴于上面我们描述的event source 的特质, 顺序的读写(append only),并发量大,基于这些特征, AxonIQ 推出来自己的event store 解决方案以保证高效的 store、 snapshot,、读、写、序列保证,AxonIQ event store, 只不过这个是商业的,做为框架外的独立模块。

方案

上面描述主要是AxonIQ, 选择解决方案的理由和过程, 不过我们没有使用他的产品AxonIQ Event store; 但是他们选择的标准和过程有不错的的参考价值。

对于一个交易系统,性能,高可用性,稳定性都是非常核心的元素, 实现性能也就是读写效率, 最好在内存操作; 高可用性需要做parittion 和replication。

内存中读写需要最终还是要落地,这里我们选择了apache 的ignite(下面会有更多的一篇独立讲解)。 过来事件保持一定的replicate(bakcup 2 ) 到cluster几点, 落地由ignite 批量输入到mysql. snapshot 也是以同样的方式。

Apache Ignite

ignite 有自己的落地方案, 不一定需要一个关系数据库, 直接以自己的格式序列化到磁盘中,但是这部分功能缺乏基本的purge,archive,migrate 功能, 需要在商业版中才有, 所以现在只能放到传统的mysql db中。

ignite 天生的良好集群解决方案,可以很好的保证高可用性, 一般设置2~3 replicate指标、可以定制 replicate 策略:比如避免在Neighbors之间保持backup,能很好的保证高可用性; 落地端ignite 可以做update 的collapse, 当然对于event source,这个功能省掉了。

在具体的事件过程中有个比较麻烦的地方, axonframwork, 对于聚合根的状态, 做了个本地的缓存, 这对于性能的提升很有帮助, 但是对于集群节点,一旦集群节点的拓扑图变化掉后, 聚合根可能被shift 到另外一个节点,或者再回来, 这个时候本地的缓存是不可用的, 这个时候其实需要从, 上次snapshot 处,在根据后续事件来恢复, 所以一旦某个聚合根从某个节点shift出去, 必须删除本地的缓存,相当于本地的cache invalidate 掉。

为什么这个聚合根不保存到分布式缓存中呢? 对于更新频繁的聚合根, 比如book, 在每秒有5个左右的报价情况下(mass order 可能50个左右,对应撤单重下单+市场下撤单+上百个产品), 把庞大的聚合根每次更新都写入到分布式缓冲中,不太现实,我们做法是每过1024个版本, 才snapshot 一次(记得LMAX其实是一天做一次),所以仅仅在本地缓存, 是一个很好的权衡,这种情况下,一个聚合根从某个节点 shift出去, 再回来,本地的缓存不一致是比较严重的问题, 那么需要在集群reblance 时候, 根据自身节点partition变动, 清除下本地的缓存, 保证下次回来从event store 中恢复状态。

条条大道通罗马, 任何的技术框架、语言、解决方案等都不会只有一种,他们本质上没有太多的优劣之分, 适合自己的业务场景需要才是最好的, 这里需要权衡团队的接受能力, 学习维护的成本,最重要的还有ROI。

FIX 协议

大部分的系统都不是凭空诞生, 和老系统、模块有千丝万缕的联系,需要和遗留资产交换信息;通道的方式和通讯的格式需要保持一致性和相兼容是个比较大挑战,这期我们主要谈谈一个古老、有点怪怪、又不失时髦的协议 Financial Information eXchange(FIX),金融信息交换协议。

![FIX & FIX 引擎角色](https://upload-images.jianshu.io/upload_images/2842122-8b548064e9bfef59.gif?imageMogr2/auto-orient/strip imageView2/2/w/721/format/webp)

字面上理解他仅仅是个交换的协议,也就是个标准和规范,如我们接触比较多的TCP/IP, Http协议一样,笔者认为他更像XML或者Json,但是多了行业术语。

实现方式和语言不一而同, FIX Engine 基于此标准来实现客户端和服务器端交互, 可以想象成我们的java web server, 和客户端浏览器的角色。

这个协议(标准)诞生于1992, 大概有25年(2017)之久的历史, 基本没有太大的调整和变动(除去由此演化其它协议如FAST Protocal, NYSE、CME Group在用)。

各种协议,FIX 还是多数

历史

FIX-金融信息交换协议的制定是由多个致力于提升其相互间交易流程效率的金融机构和经纪商于1992年共同发起。

这些企业把他们及他们的行业视为一个整体,认为能够从对交易指示,交易指令及交易执行的高效电子数据交换的驱动中获利。FIX由此诞生,一个不受单一实体控制的开放消息标准,一个能够被调整组建适用于任何一个企业的商务需求的协议。

一个经久不衰的协议标准,自然有他闪光地方--抑或迁移的成本太高,总体来说FIX 协议有这些特点:

  • FIX协议是一个免费的开放式通信标准

  • 不是某个金融机构,或者某个中心服务系统

  • “FIX Trading Community™ is the non-profit, industry-driven standards body at the heart of global trading”

  • 一个不受单一实体控制的开放消息标准

  • 一个能够被调整组建适用于任何一个企业的商务需求的协议

共同的语境、语法是沟通的基础, 可以参考巴别塔为哈没有建成,在20几年前,没有当今的交易体量和众多参与者,这些方式还都行得通;但是通过小本子、电话、传真等传递消息,很容易导致错误、丢失、不一致。

为避免混乱及重复使用,于1992年由富达投资和所罗门兄弟为推动股票交易双边通信框架而开发。

(1)就商务流程而言,FIX为机构,中间商,以及其他市场参与者提供了一个减少不必要的电话沟通和琐碎的文档传递方法,为面向特定个体传递高质量的信息提供便利。

(2)FIX为于技术专家提供了一个开放的标准,对他们开发的努力和实践产生了影响,使他们能高效地创建同一个更大范围的参与者之间的联系

(3)FIX可以为卖主提供一条现成的通往行业的信息存取路径,减少了市场营销的难度,增加了潜在的客户群,充分利用现有资产。

FIX 细节(部分)

FIX已经从最初的买方-到-卖方的证券交易中得到发展。

参与方

现在被广泛的用于交易市场,及其它市场参与者,除了证券交易还有:

  • Fix Income

  • List Derivatives

  • FX

生命周期

在一个交易的整个周期

  • Pre-Trade

  • Post-Trade

  • Pre-Settlement

版本也更新了很多:4.0 –>4.1 –> 4.2 –> 4.3 –> 4.4 –> 5.0 ? 6.0

格式

可以说FIX 消息很傻白甜,非常简单就是一个tag=value 的组合。

FIX 消息

如果把SOH(0x01) 换成 或者逗号(,),完全可以道出为个excel 格式。
8=FIX.4.2|9=65|35=A|49=SERVER|56=CLIENT|34=177|52=20090107-18:15:16|98=0|108=30|10=062|

消息类型

(1)Admin Message

主要用于系统维护,session 连接,登录登出等等

logon/logout, Heartbeat, Test Request,

Resend Request, Reject,

Sequence Reset,Gapfilll

(2)Application Message

业务需求的, 我们接触最多的

Application Message可以根据我们trade中不同生命周期提供:

(2.1)Pre-Trade

IOIs, Quotes, News, Email, Market Data, Security Info etc

(2.2)Trade

Single Orders, Basket/List Orders, Multi-leg orders,

Executions, Order Cancel,

Cancel/Replace, Status etc

(2.3)Post Trade

Allocations, Settlement Instructions, Positions Mgmt etc

FIX Engine

FIX 消息的格式、业务背景都知道了,到实现层面上就是一个FIX引擎负责技术的落地,需要:

  • 管理网络链接。

  • Session 层的管理,为application message 出入服务,自动处理 Sequence Reset, Resend Request, Test Request, Logon and Logout 等等消息。

  • 应用层,处理相关的消息。

  • 输出FIX消息,encode。

  • 解析输入FIX消息, decode。

  • 验证消息格式,tag有无,checksum消息破坏否。

  • 消息序列化, 保存到db/mq/file 等。

  • 根据FIX状态模型,重启后恢复session状态。

  • SSL-based 加密。

  • 解压缩等。

FIX 引擎角色

Session

Session 一个FIX会话定义为一个在连接双方间的的带有连续序列号的有序消息双向传输流(A FIX session is defined as a bi-directional stream of ordered messages between two parties within a continuous sequence number series), session是服务器和客户端会话消息交流的上下文, 两种角色:

Initiator :发起者,建立通信连路,通过发送初始Logon消息发起会话的参与方。

Acceptor :接收方 FIX会话的接收方。负责执行第一层次的认证和通过传输Logon消息的确认正式声明连接请求被接受。

这里注意,一个FIX session 可以在多个物理链接中存在(不是并列链接,一个个序列),双方可以在多次重连中使用同一个session,但是不建议这样做, 在onixs 的fix engine 可以使用 #logonAsAcceptor(),#logonAsInitiator()实现session 重用。

每个FIX 消息都需要个sequence number(MsgSeqNum <34> ) 标识,这个sequence 需要在一个session中保持唯一性, 序列号在session启动时候初始化为1, 在消息的不断交互中有序增长,发起端和接收端各自维护一个sequence, 互相协调同步,这个由于发送和接受段的消息数量是不一样的, 比如发起端已经发送了100个消息,但是接受可能到200个消息; 反过来这100个消息就是接收方接受的sequence, 200为接受方发送的消息序列。

一个 session 周期

流程

下面是个最基本的过程:

  • 登录

  • 下单

  • 部分成交

  • 取消单子

  • 取消成功

  • 登出

一个最简单流程

实现

FIX 协议是公开透明通用的, 实现了 FIX Engine的开源、商业解决方案由很多, 开源中比较有名是QuickfixEngine & Fix 8, 而大部分的开源解决方案都缺乏后续的维护, 缺乏必要监控、高可用性解决方案;

所以我们采取的措施:借鉴开源的解决方案,自己重构重写,剔除不必要的功能, 可以通过其它方法进行补偿,以保持FIX Engine 尽量小简单而美。

最终借鉴 FIXIO ,借助Netty底层通讯解决方案,可以非常迅速快捷实现自己的FIX 引擎,下面只挑几个重点的模块点到下。

网路通讯层可以全部由Netty 处理, SSL 用 stunel 处理,这样大大减少引擎层核心代码的数量和逻辑,易于维护扩展debug.

pipeline.addLast("tagDecoder", new DelimiterBasedFrameDecoder(1024, Unpooled.wrappedBuffer(new byte[]{1})));
pipeline.addLast("fixMessageDecoder", new FixMessageDecoder());
pipeline.addLast("fixMessageEncoder", new FixMessageEncoder());
pipeline.addLast("logging", new LoggingHandler("fix", LogLevel.DEBUG));
pipeline.addLast("session", ClientSessionHandler(settingsProvider, authenticationProvider, messageSequenceProvider, getFixApplication())); // handle fix session
pipeline.addLast("testRequest", new TestRequestHandler()); // process test requests
pipeline.addLast(workerGroup, "app", new FixApplication()); // process application events events

基本按照Netty 标准流程下来(这里演示是initiator/client)。

  • 按照SOH, 也就是0x1 进行分割

  • Decoder 区分哪里是开头(BeginString-8),哪个尾(CheckSum-10),组装成bean

  • Encoder 讲发送出去的bean 组装层 SOH 分割风格字符串

  • log 落地

  • 创建session

  • 测试请求, 心跳处理

  • 应用程序部分, 在worker group 中处理业务相关逻辑。

Session创建

(1)channelActive, channel 创建好了:

1.1 准备logon请求

1.2 创建session,包含连个sequence(in,out), 其它基本信息, fix版本, sender, target 等信息。

1.3 将session 放到ChannelHandlerContext 上。

(2)channelInactive, 通道关闭发送logout event

(3)encode, 为out 消息添加 session 中共享的信息,主要是头,事件戳等

(4)decode,进来的消息简单验证, 比如logon 处理, header, sequence检查,主要逻辑还是application 中处理。

(5)exceptionCaught, 异常处理

更多的协议如雨后春笋般涌现出来, 唯独fix 协议的标准和实现方式20多年来没有很多的变更, 大规模的使用也代表他有存在的价值和理由。 对于其它协议有很多可以借鉴的学习的地方。

前十篇,基本的理论、业务、技术都有涉及,后续篇幅,将更多技术方面的东西,比如redis, Kafka, ignite,分布式,高可用性方面的内容。

内存计算

天下武功唯快不破,那么对于交易系统更是如此,如何达到这个快,可以提升单机性能,提高运算速度和效率, 可以横向集群扩展增加吞吐量和并行能力。

尽可能将数据放在内存, 在内存中实现计算是一种行之有效的方法,IMDG(In Memory Data Grid), 是此篇我们将要探讨的内容。

概述

需求

对于一个交易系统,在交易时段,需要非常精确快速提供下面几点服务:

(1)保存和计算所有用户的资金信息

  • 可用现金

  • 占用保证金

  • 账户浮动盈亏

  • 实时聚合计算,规则引擎触发

(2)保存和计算用户持仓

  • 仓位变更

  • 实时浮动盈亏计算

(3)实时消息推送

  • 用户在线状态

  • 订阅价格信息

  • 持仓仓位信息

基本条件

我们需要这样一个工具(或者服务)提供:

  • 缓存

  • 内存中索引

  • 高可用性

  • 易扩展性

  • 落地方案

  • 流式计算

  • 内存运算

延迟分析

当建立一个大型Java应用时,引起性能问题大部分是延迟,在一个分布式Java系统中引起延迟的原因可能有:

  • 从磁盘上加装数据的IO延迟

  • 跨网络加装数据的IO延迟

  • 在分布式锁上的资源争夺

  • 垃圾回收引起的暂停(在JAVA 应用中这个尤为重要)

典型Ping时间是:本地机器是57µs;局域网是300 µs;从伦敦到纽约是100ms;对于1Gb网络,网络数据传输是每秒 25MB – 30MB。对于10GB网络是每秒250MB – 350MB。使用SATA 3.0接口的SSD硬盘数据传输是每秒500-600MB。如果你有1G以上数据需要处理,磁盘延迟会严重影响应用性能。

硬件上最低延迟是内存,典型的内存缓存是每秒3-5 GB,能够随着CPU扩展。如果你有两个处理器,你就能每秒10GB,如果有4CPU就能获得 20GB. 有一个内存基准测试称为STREAM 是测试许多计算机的内存吞吐量,一些在大量CPU帮助下能够实现每秒TB级别的吞吐量。

在内存中存放和管理数据是降低延迟最有效的方法之一,现如今内存的价格大大的下降,现在几十G的上T的服务器非常常见, 使得这样的操作方法的以可行; 内存中保证高可用性, 势必涉及partition 和 replication, 分布式计算、backup, 再平衡等等, 和外围语言的交互,借用ignite 图大概这个样子:

一个内存网格工具需要具备功能

IMDG 提供一个完全基于内存的架构, 理论上可能给你应用提升几倍或者几十倍的性能提升, 可以将上T的数据载入内存, 能够很好满足现如今大数据处理的需求。

IMDG, 直观可以认为是一个分布式的hash map, 你按照key 存储数据, 和传统的系统不一样,key, value 必须是一个string 或者byte数组, 你可以直接用你领域对象作为key 和 value,不需要序列反序列化这些对象将给你的业务处理流程带来大量的编辑, 使用和操作这里map 就和你处理本地的hash map 一样,这个特征也是IMDG, 和IMDB(In-Memory Database)区分, 省去ORM不仅仅省去很多不必要的麻烦,也会有性能上的提升。

IMDG和其他一些产品, 比如NoSql, IMDB, NewSql 数据库,一个现在的区别就是使用集群, IMDG可以按照partition将数据replicate 和均匀的分布在你网络节点中, 所以理论上网络节点越多,能够存储的数据就越多。

在IMDG 使用需要注意点,应该让你的运算尽量靠近数据–类似于Map-Reduce, Hadoop 中思想, 因为在网络世界中,移动计算比移动数据更省事,当整个网络拓扑图稳定后,大部分时间数据是待在固定的节点, 只有在加入,或者有节点退出情况下,产生re-partition 才会导致数据在节点的移动。

Partition & Replication

在我们的运用中有一个必不可少的步骤,就是数据持久化,将数据存储到外部界质,下图的ignite使用方法,这里也可以实现自己的cache storage, 比如通过write/read through保存到数据库。

缓存落地

分布式计算是通过以并行的方式执行来获得高性能、低延迟和线性可扩展。

在集群内的多台计算机上执行分布式计算和数据处理。分布式并行处理是基于在任何集群节点集合上进行计算和执行然后将结果返回实现的。

内存计算

上面是根据我们业务场景, 需要基于内存的解决方案, 最终选择 apache ignite作为IMDG解决方案, 当然业界有很多其他的IMDG 产品比如:

  • Hazelcast

  • Gemfire

  • Coherence 当年还是tangosol 用过

  • Ehcache

  • Terracotta

实践

PNL

PNL分基本两种: 一种是浮动, 一种是结算好的;结算好的比较好理解, 一个买卖周期结束,钱已落袋就是最终的盈亏,这个是固定不变的,另外一种是浮动, 在每个价格跳动都要计算一次, 结算好的PNL在实现方法上面没有太大的挑战, 同时他的TPS 也相对平缓, 但是对于浮动PNL, 特别在像外汇交易中,计算的强度非常的大, 几乎每秒都有3~5个以上的报价, 同时如果用户有大量的持仓。

对系统有运算能力有巨大的挑战。

首先要做的就是将用户的持仓信息载入内存,分布于集群中, 对于计算的频次,也有有区分, 这个根据不用用户的持仓比(占用保证金率), 对于占比高的(70%以上)的当然需要更频繁的计算, 对于低比例比如小于30%,其实这样的仓位非常安全,适当扩大计算的频次风险比较低, 当然也要根据价格波动的区间, 价格波动非常大时候, 需要整体的计算一遍。

一旦订单match后会通过ESB(Kafka,Redis) 推送到PNL节点,由于用户的持仓是多节点集群,在replication 模式下保持3个backup; 每个节点有自己的primary partition, 同时使用Kafka 在publish 时使用同样的partition 算法分发到Kafka topic partition中,这样保证每个集群中节点只consume 自己primary partition 上面的数据, 大大减少了数据在节点间的re-balance; 但是这里需要自己监听持仓cache 上面的EVTS_CACHE_REBALANCE 消息; 这会额外增加节点间的通讯量, 但是基于,一般集群拓扑图稳定后很少变动, 这点损耗还是值得的。

浮动PNL能否正确无误的计算出来, 对于下游一系列的模块正常运作非常重要。

规则引擎

在整个交易系统中, 涉及规则的地方非常多, 简单比如什么时候margin call? Stop 条件的触发, 在开市时间需要实时的触发, 有两种方法, 一种是被动,定时Job去扫描;

另外一种是主动在变动数据上面加上监听条件 reactive callback 回来。

对于margin call,的触发可以使用Job 定时比如1分钟去扫描;

而对于stop 规则 需要使用continuous query 这样的功能在价格和变动后,立刻执行。

微服务

对, 我们这里也用到了时髦的微服务,这里也是借助ignite 的服务网格来注册和发现和部署服务, 只不过我们这里用到 DDD 开发模式, 对于一个聚合根的变动,原则在一个时间点只能在一台机一个线程中处理,所以这里需要做些额外的处理, ignite 可以根据NodeFilter 条件来选择用集群中的某个节点上的服务, 因为可能这个节点有你服务依赖的数据, 而对于我们的DDD,这个是个必要条件, 必须在每个固定的节点,除非这个节点从primary 角色退出; 这里涉及到微服务发现的方式, 可以参考Pattern: Service registry。

我们的做法是使用Client 发现服务的方式,在分两种, 一种是偶尔需要定位某个服务类似ad-hoc, 现用现查,另外一种是和某项服务有长期的关联, 比如一个book 和我们portfolio 管理模块, 这个时候需要注册服务提供节点网络拓扑图改变listener,随时保持保持最新更新, 或者退而求其次,设置一个重试机制,在错误达到一个benchmark, 再重新定位服务。

风控

风险监控, 包含margin call, 某些交易规则触发, 如头寸大小是否超过设定benchmark, 这里借助ignite continuous query + redis , 以 Realtime 处理。

风险还包括 fault detection 这些部分以Near realtime处理,可以借助 ELK、和NEO4J 类似工具来完成。

Market 数据

市场数据, 涉及 realtime 和 history 数据; 其实按照价格传播的快慢可以分几大类型价格:

  1. 匹配中心, 这个是最新最快价格放在节点的本地 hashmap 中

  2. 内部计算的价格,通过redis 发送给PNL, Account 节点

  3. 历史数据,比如分时,小时这个是最后价格,通过KAFKA 到Market 节点进行聚合和存储

在考察了一堆的时序数据库,比如最近当红炸子鸡InfluxDB,但是如何处理OHLC,特别在你有10来个价格档位情况下没有看到合适的处理方法,同时InfluxDB 开源不支持集群了。

通过简单的评估, 比如平均有200产品同时开市,每个每秒平均报价5次, 有10种K线聚合情况下, 每秒其实也就 200x5x10 ~= 10000 次操作, 如果完全在内存中还是绰绰有余,DONE!

所以很多时候解决方案,没有你想像那么复杂, 计算机没有你想像那么快,也没有你想像那么不堪,只有试过方知可不可。

Market 数据部分同时涉及,报价往客户端实时发送, 这里涉及内容比较多,下篇再说。

上面涉及的各模块之间的交互,其实不完全借助IMDG完成,KAFKA在内部消息通道中占有很大一块比重,将分独立篇幅叙述。

编码解码

编码解码,不得不再次搬出这个话题, 编码解码就是程序语言之间交流的语言!

一旦你适应了用粗暴的思考架设了你的设计, 再从头来会很难,一是你没有胆量推倒重来,二是没有时间,三是你没有耐心; 如果,反过来从最粗陋的底层开始, 倒有可能雕琢出更加精致的上层建筑!

撇开一堆高大上的RPC、分布式等等上层建筑,如何编码解码是个最基础的环节,在自己进程内,怎么交流都无障碍, 就好比大家都是中国人,都在说汉语,基本能够理解你说话的意思.

但凡需要跨进程,就需要编码解码你的信息, 不管是同一个还是不同语言; 这就比方你需要和一个吼达不到的人交流,你需要电话,或者书信来传递。

这里首要做的事情就是, 把你要表达的意思,转换成书信、电流信号;如果转换的言不达意,再好的通道也是鸡同鸭讲。

程序之间的交流和任何信息的交流过程其实都一样, 上面说对面讲话如同进程内通讯,其实还是不太精确,任何即使面对面的对话,其实都是有解编码过程,你脑子里面想的电信号,驱动你发音系统产生气流的震动,通过空气传递到对方,对方的听觉系统把震动转换成电流信号,给大脑皮层理解。

概要

我们想要的解编码协议需要满足:

  • 够傻够呆, 字节码打出来,即使你用笔手动算也能算出来

  • 跨语言,小到内部系统之间交流, 大到对外APP, 客户端是个程序给个套路就可以解析

  • 够小,不能臃肿如XML 等之类,但是无需为了个把字节问题影响标准 1

  • 够快,够快才能显示我们的优势,同理不能为了快影响标准 1

满足上面几点的其实很难, 因为大部分不能满足要求 一; 确实如此,这个犹如时间、成本、质量铁三角或者 CAP 一样难以都满足。

其实很简单, 所有的项目、工具、套路最终都是个权衡,权衡和取舍!

首先你不是什么消息都需要交流, 对交流的内容和范围做限制和取舍, 可以很容易满足上面的几点。

大部分的IT 项目其实为了那 20% 甚至 5% 的功能,占据80%以上的代码, 贡献了绝大部分的BUG.

程序因人而生,但是公平的对待程序和自己可以省掉很多的麻烦,需要和外围通讯的消息需要满足:

(1)绝大部分是 primitive 类型: 这个好理解, primitive 所有语言解释套路一致,歧义少!

(2)字段定长

如果实在需要个类似string 呢? 很简单定长, 8/12/16/24/36/48 随便你选, 超过这个呢,对不起,走其他通道。

(3)少group 属性

group 也就是属性是个列表(list), 列表给整个消息带来不确定性。于是少设计列表属性,能拆分就拆分, 不能拆分说明你业务模型有问题。 有人说这样一分其实很简单, 用个基本的分隔符,就可以把消息编解码出来, 你说对了,用个竖线( | ) 或者逗号(,),从笔者角度确实是个非常非常不错的选择,紧凑、好懂,非常完美的满足上面的四点, 而且部分十分微小的消息这样处理非常好用。

经过如此筛选,最终可能上百个消息,最后剩下几十个,最后带group 的只有区区数的过来的几个。

OK 了下面的选择就比较宽泛容易了。

FIX

最初对FIX 我是抗拒的,程序员容易犯的错误,是对一些古老、或者不时髦的东西有偏见,会一直以为最新的东西才是最NB, 最cool, 会了面子上过得去。但是这个协议是如此的简单和健壮,已致他一直被金融界广泛的使用,这里 有介绍,Key+Value+SOH(分隔符,字节码 1 ) 其实就是这个协议的全部;

什么狗日的 hack 帮我省那十来个字节没有的, 比较完美符合标准;

对,其实我们在内部也大胆的使用了FIX 作为通讯协议, 如和 websocket 客户端交流, 我们也使用此风格消息进行交互。

看到 LOG 一窜窜漂亮的K=V 打印出来,有种赏心悦目的感觉!

SBE

SBE Simple Binary Encoding, 听到这个词就有种对眼的感觉, Simple 多谦虚直白的表达方式,中文的SB编码又有点气吞山河如虎的傲娇!

读罢,果然简单直接! 好帅真的一个协议。

SBE设计准则, 我就不一一翻译,直接COPY Jdon 翻译:

Copy-Free:不采取中间缓冲,因为其在多次字节复制中有性能损耗。采取直接与底层缓冲编码与解码,其限制是消息大小不能超过传输缓存的大小,可以进行碎片分段。

Native Type Mapping:copy-free的设计也通过将数据直接编码为底层缓冲的原生类型中得到巨大好处,比如一个64位整数型能直接作为x86_64 MOV汇编指令被编码进入底层缓冲。通过这种原生的类型映射,一个字段能够以类似高阶语言如C++/Java中class类似和struct字段一样高效率访问。

Allocation-Free:对象的分配会导致CPU缓存流失从而降低效率,这些被分配的对象后来得收集并删除,对于Java是使用垃圾回收机制,导致stop-the-world暂停。SBE编码采取flyweight 模式,基于底层缓冲的flyweight窗口直接对消息编码与解码,相应类型的享元是基于消息头部模板id选择的。

Streaming Access:现代内存子系统已成为愈加复杂,该算法能够对性能和一致性有很大帮助,实现最好的性能与最一致的延迟,这是以一种上升顺序方式访问内存的方式实现的,也就是一种流。

Word Aligned Access:当word以非word大小边界访问时,多CPU架构表现出显著性能问题,一个word的起始地址应该是其以字节为单位大小的倍数,64位整数只能从字节地址能被8整除的地方开始,32位整数只能从被4整除的字节地址开始。

简单粗暴, 是我个人对SBE 最直接的感受, 但是好用!

SBE 一个消息(Frame)包含哪些内容:

sbe frame

在HTTP中涉及久了大家基本都记得一些这样的图形, 基本就是个header + payload 设计手法:

WebSocket frame

当然SBE 属于第六层(OSI), 也就是表示层(Presentation Layer), 最终还需要上面的应用也就是你的业务逻辑层来处理这些数据的。

Header 保持简单, 只保留最最基本的信息, 或者你消息路由里面需要的信息, 避免解析整个消息流。

<composite name="messageHeader">
    <type name="blockLength" primitiveType="uint16"/>
    <type name="templateId" primitiveType="uint16"/>
    <type name="schemaId" primitiveType="uint16"/>
    <type name="version" primitiveType="uint16"/>
</composite>

这样的header 占据8个字节的长度, 包括信息:

  • blockLength: payload 的长度

  • templateId: 消息模板, 哪种类型消息, 编码解码需要对象的解码编码器。

  • schemaId: 属于哪个 schema 比如和FIX 哪个兼容

  • version: 整个是你消息的版本, 建议不适用,如果消息变了,本人更趋向创建个新的类,即使加上个V1..N 也可以。

对于最简单的消息:

<sbe:message description="Internal Time Price" id="916" name="Price" semanticType="Price">
        <field id="1" name="product" presence="required" type="VARCHAR12"/>
        <field id="2" name="bid" presence="required" type="double"/>
        <field id="3" name="ask" presence="required" type="double"/>
        <field id="4" name="timestamp" presence="required" type="int64"/>
</sbe:message>

product 是一个12长度的字符串, 然后是一bid 和 ask 报价, 然后是一个epoch 时间戳。

可以看到这个对象的固定长度是 : 36+8 = 44 字节, 固定长度消息。 比如对于bid 字段:

offset 为 header offset 也就是8个字节 + 前面的 product 12, 也就是bid 偏移是 20

编码

buffer.putDouble(offset + 12, value, java.nio.ByteOrder.LITTLE_ENDIAN);

解码

buffer.getDouble(offset + 12, java.nio.ByteOrder.LITTLE_ENDIAN)

是不是好舒服, 用笔都可以算出来。一个协议简单到这个程度也令人咂舌, 好就好在这个协议没有完备的周边设施,个人理解这个是弊端也是好处!弊端不是拿过来就用, 好处是给实现的人留下很多的空间, 而很多的协议都是全家桶, 周边都帮你实现, 一旦出现乱子, 你再去扒拉,非常的艰难。

一旦你适应了用粗暴的思考架设了你的设计, 再从头来很难,一是你没有胆量推倒重来,二是没有时间,三是你没有耐心; 如果,反过来从最粗陋的底层开始, 倒有可能雕琢出更加精致的上层建筑!

实践

上面说的基本就是SBE 全部, 和他的一个GIT版本, 其实这个项目不能说实现,因为SBE 和FIX 一样太简单了, 这个项目仅仅是个TOOL, 生成了最基础的几个语言的 stub, 和你 application 层美好的结合还需要自己去实现!

首先和你底层通讯框架集合起来(RPC, MQ 等), 然后和你的业务bean结合起来。

现在的RPC, 或者MQ 框架都有plugin 进自己的 codec 实现, 好说, 但是和自己业务bean 结合起来就比较麻烦.

你不能decoder/encoder 这些 stub 类往你业务层传啊, 这个薄的一层非常的让人苦恼。

必须要自动代码生成一些伪类, 有这些自动生成的类有:

  • Adapter: 反射封装encoder/decoder get/set 之类

  • Enum: 映射, 这个比较暴力就switch case

  • Stub: 分两种其实如果你需要 Lazy 模式,生成一个业务bean 伪类,覆盖get方法, 对group 这里需要特别对待, 如果是 Eager 模式不需要使用伪类,直接 adapter 中 解码new一个然后 get/set 上去。

  • 反射,由于SBE 是基础XML, 自开发一套annotation, 方便一次扫描出来!

由于以前我们一直使用kryo序列化方式,效率和压缩比还是不错, 但是对其他语言不兼容,换成 SBE 后对于一些小消息,由于有头(部分头不止8字节长度,Domain 消息有29字节长度),还有对于optional 字段其实 SBE 也要占空间, 所以消息的大小没有多大的优势,但是解码编码还是飞快不少!

特别是如果你根据消息的ID 也就是 tempalteId 做路由可以说非常快,记得你的 templateId 固定位置, 直接截取byte 相关位置就可以路由了。

通讯协议

如果你的系统不是采用一个大而全的设计( monolithic ), 不管是现在流行的微服务(microservices)还是其他, 就涉及到进程间通讯。

机器之间的交流和人与人之间的交流一样,诸多的问题就会扑面而来。

简单的交流模型

从一个发送者(客户端)到接受者(服务器),之间会掺和进各种因素, 比如人与人之间交流的噪音,不同文化背景等等,在进程间进行通讯有同样的问题:

  • 通讯的格式

  • 服务的注册和发现

  • 同步还是异步

  • 平衡(Balance)模式

  • 备选方案

所以在万不得已的情况下, 不要选择RPC解决方案, 如需要,一要谨慎选择开放有限的服务, 二是否有其他的解决方案,比如RESTful API 风格, 使用RPC 风格在我们业务中是个比较合适的解决方案, 可以参考以前的文章关于DDD设计风格的架构。

所以在Command 端,可以理解只有POST 请求的 HTTP API, 比较适合用RPC 特别对于并发比较大的应用。 如果集合现有的架构和通讯网络基础架构, 能够以最小的代价引入RPC, 可以说是个不错的选择。

如果将上面一般的信息交流过程,映射到rpc 可以得到下面的图形:

RPC

基本的模块功能也非常的容易理解,但是在实际的操作中还有诸多的细节需要考虑, 现在市面上也有很多成熟的解决方案。

如阿里巴巴的dubbo(开源)、Facebook的thrift(开源)、Google grpc(开源)、Twitter的finagle(开源)等。

其实在我们的系统中,有部分使用 grpc 作为 gateway 转送外围请求到内部系统,但是在一些模块中还是使用了自己定制的rpc框架。

整个框架参考了 luxiaoxun/NettyRpc & fixio - FIX Protocol Support for Netty 和 Dubbo 等设计思想和原理, 在3天左右的工作时间里实现了基础的代码和测试, 带入测试环境。

当然这个架构的设计有大量为本应用定制的元素,不一定适合所有用类。

需求

  • 和 DDD 紧密集合,面向Command 端, Event 自然走MQ走不在讨论范围内。

  • 一套内部的序列化方式

  • 基于Netty

  • 提供服务有限精简,非CRUD 这样的操作(参考DDD设计思想)

  • 无侵入性

参考前面基本介绍DDD 设计基本文章,我们整个交易系统采用了此设计架构,DDD 中的aggregate root 作为触发状态变化的核心元素, 将 domain 中触发状态改变的command 转换成事件分发出去;这一层对外提供操作服务,所以这部分是我们需要改造的部分。

为了维护高可用性和高并发性, 这层首先需要做partition, 但是不能replication, 因为一个aggregate root 在全局中只能有单个线程在运作, 所以其实是带状态的。也就是在同一个时间,一个aggregate root 只能在一台机器,一个线程中被操作。

这让服务器端和客户端协调都带来很棘手的问题, 理论提供一个带parition 的MQ 比如KAFKA 就可以解决这个问题。

这确实是我们第一个版本的实现方案(其实第二个,开始有个其他的rpc 解决方案),能够很好的满足需求, 比如我们对唯一组件的 account id 作为partition 的key将请求分发到kafka(需要保证kafka 的partition 和你节点上面parition算法一致), 服务端监听就可以了:

kafka

有了这个中间层, 可以很好的解耦各系统之间的关联, 多一个中间的跳转有性能上的稍微损耗,而且kafka 没有很好的transaction 支持,但是这个方案可以是很好的 failover backup 方案。

下面的服务的注册和发现, 将提供一个解决方案如何在rpc 下保证服务端和客户端保持一致。

网络通讯层

基于Netty 大部分的问题已近解决, 有先前使用netty 解决其他网络通讯层的问题,所以比较容易解决。

final ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("tagDecoder", new LengthFieldBasedFrameDecoder(4069, 0, 4, 0, 0)); //Size is 4k at most
pipeline.addLast("rpcDecoder", new SBERpcDecoder());
pipeline.addLast("rpcEncoder", new SBERpcEncoder());
pipeline.addLast("pingPong", new PingPongHandler()); // process pingPong
if (application != null) {
    pipeline.addLast("app", application); // process application events events
}

唯一需要定制的就是你的 Decoder 和 Encoder 和你的Application 层用。

由于我们这里使用的是 SBE 序列化方式, 所以这部分是定制的, 这里要注意SBE 默认编码是 Little Endian 还是 Big Endian 编码格式, 是否和Netty 默认冲突。

后面接的是你的Application 层应用,这一次是避免加入RPC 对原有价格的侵入控制点, 我们这里是分发到进程内Command Bus(基于disruptor)。所以这里RPC 必须都是异步调用, 需要根据一个request id(UUID, 或者为一个sequence) 回调—如果需要返回结果的话。 Command Bus 不介意一个Command 是从RPC 还是 MQ 还是 HTTP 请求来。

  • 注 RPC 异步 vs 同步:

同步

异步

业务层

也就是处理Command Business Logic 部分,对于发送过来的command 需要是一个DDD风格的Command:

  1. 包含一个Aggregate Root Identifier, 也就是唯一标示一个物体的, 比如人的account Id, 一个产品的booker Id 等。

  2. 一个全局统一的partition 算法。

  3. 一个标准的SBE 兼容消息

比如一个简单的创建账号的请求(command)

@SBEMessage(id = 800, desc = "Create Portfolio Account")
public class CreatePortfolioCommand {
    private static final long serialVersionUID = -8206695527451683906L;

    @TargetAggregateIdentifier
    @FieldType(index = 999, type = "AccountId", id = 999)
    protected AccountId accountId;

    @FieldType(index = 1, type = "int64", id = 1)
    protected long userId; //this may pass by frontend

    @FieldType(index = 2, type = "double", id = 2)
    protected double credit;

    @FieldType(index = 3, type = "VARCHAR8", id = 3, presence = Presence.OPTIONAL)
    protected String vendor;
}

一个 Message 包含:

short domain;
long timestamp;
long sequence;
int part;
Object payload;
int id;

domain 一个short 标注来自那个domain, 自己知道就可以,理论一个应用domain 个数不会超过百把个,否则太复杂了!

timestamp 消息发送的时间戳

sequence 一个客户端维护的sequence,服务器不检查重复,回调时候用

part: Domain Command 属于那个partition

payload 消息体

id: 消息id 也就是上面SBEMessage 的id

为什么消息上面要带partition, 下面的服务注册和发现 还会再做说明,所以需要一个统一的客户端和服务器端的partition 算法。

为什么要带消息的id?

几个作用:

  1. 路由

  2. 避免服务端和客户端的stub 代码, 无需IDL、XML语言去描述你的服务

  3. 无侵入性

只要在Application 层,套一薄薄层,路由到内部的Command Bus, 但是有个缺点就是内部必须有完善的文档, 知道每个服务器端能接受什么类型的服务,也就是那些类型message Id 支持,否则过来会在Application无情过滤掉。

对应Application 到业务层的Adapter:

@FunctionalInterface
public interface CommandHandler<T> {

    /**
     * Handler for specific command
     *
     * @param command, the payload
     * @param part,      which partition
     * @param sequence,  the sequence like version may the command need to be fail detected
     * @param timestamp, the time message newed
     * @throws Exception
     */
    void handle(final T command,
                final int part,
                final long sequence,
                final long timestamp) throws Exception;
}

服务注册和发现

上面我们反复强调, partition 一致性的重要性,这里可以揭晓为什么这样设计。

DDD设计方式, Domain 无疑是整个架构的核心, 其实一个Object 都可以称为一个Aggregate Root; 维护这个对象状态变更, 比如一个账号对象(Account), 有基本的属性, 比如account Id, cash, credit, position等等, 有对这个对象的不同的操作:充值、取款、加仓、平仓、调整信用度等等, 这些自然的作为一个命令(Command)发送给这个对象。 为了维护这个对象状态一致性, 必须保证单线程操作!这个在单机还容易实现,但是对于集群环境呢?

对 account id 进行partition 分区, 在集群机器上,一个partition只能位于一台机器上。 比如对 account ID 分成32个partition, 理论可以支持最多32台机并行处理对account 的所有请求, 如果现在有两台机器, A & B, 他们分别得到partition:

A: 1,4,8,10,11,13,17,19,20,22,24,27,29
B: 0,2,3,5,6,7,9,12,14,15,16,18,21,23,25,26,30,31

如果错误将一个partition 为0 的账号请求发送到 A 机器上面将导致处理被拒绝。 虽然机器A 可以从Event source 将这个账号replay 出来, 但是由于性能优化, replay 出来的Domain 是做本地缓存的, 同样snapshot 和 event source 也是缓存,这样可能导致 domain 恢复错误。

但是一旦发生re-balance? 比如0 partition 从 A 移动到B, 这个时候会把A 中, partition 为 0 domain 相关所有数据刷入DB, 在 re-balance 结束前, partition 0 上命令可能失败。

这就导致一个问题,服务注册要带上自己的IP+Port+Partitioins.

服务发现端,同样需要根据不同partition 发送到对应的服务器端。

注册和发现服务使用 redis, 服务器端在感知partition 发生变化后, 会刷到redis 一个map 上面(带TTL),然后双方一个消息到topic上, 同时定时刷行当前parition 到redis上面。

客户端直接监听和定时 poll 这个map 就可以:

客户端服务发现

更多服务注册发现方法可以参考 Dubbo 的实现

所以我们这里的实现是个特别的 load balance ,其他一些load balance 可以参考同样Dubbo 的实现

测试

不考虑落地数据, 单机测试可以满足据大部分的性能需求。当然这个离上线还是比较远, 需要考虑re-balance 情况下有parittion 不可用, 所以我们这里还是有failover 方案, 在服务不可得情况下是发送到 mq 中,还待长时间进一步检验。

监控

此篇主要讨论下系统监控相关的东西, 以及我们的IT系统里面的实践。

所有代码都写好,环境搭建好, 该接的都接了,就好像家装修好, 地板铺了,水电煤通了;就拎包入住了, 住了一个月确实不错; 系统运行了三个小时候有余了, 调用API 能通,订单能下, 数据库录入也都正常, 突然某天系统就嗝屁了!

监控可以根据不同的层来区分, 也可以根据时效比如有的需要实时, 而其他可能对实时性需求没有那么高。

系统监控-基础设施层 OS, 网络 etc 服务监控-中间件层 MQ, redis, Mysql,Nginx etc 应用层 根据你业务模型来分 其他等等 监控, 就是不停抽样系统的各项运行指标:

确保都在监控可控范围内, 还需要完善的周边解决方案, 指标不正常后可以自动化走备案。

训练有素的团队快速响应

这样才能有可能保证你的系统健康的运行下去 — 一个系统完美正常的运行下去还有诸多其他的因素, 终极目标可能是一个自动化无人值守的系统 :-)

基础设施的监控

这部分的都有非常成熟的解决方案, 这里不再描述, 可以查询网上很多的解决方案, 比如 Zabbix, Nagios,Pandora 等等

开源工具

这套系统, 如果在云上, 一般都成套的工具帮你实现, 如果自己搭建,需要一个经验比较丰富的运维团队。

业务部分的监控

业务日记的扫描整理可以借鉴ELK 的解决方案。

这里更多注重关键网关上面的性能和效率监控。

监控信息的收集方式无外乎 : 主动push, 或被动的pull; 而收集的机制,一般都是在宿主机器上装相应的agent 汇聚、加工原始的信息到分析的机器上, 比如fileBeat、logstash 等。

这样的做法对现有的应用侵入性少一点, 业务开发人员几乎可以无知这样的基础设施的存在。

我们这里采用还是在代码里面加上锚点, 这样可以达到更粒度的控制, 然后采用主动push 的方式,采用的技术栈:

Dropwizard Metrics 在API, gateway 采集指标, 比如Timer, Counter 等等

InfluxDB 存储时序数据

Grafana 展示

是不是很简单暴力, Dropwizard 采集指标, 这个比较简单,配合InfluxDB Reporter 将指标发送到InfluxDB, 大家可能觉得现在InfluxDB开源部分不在包含集群功能, 有点质疑, 其实你可以将不同模块的指标输送到多个 InfluxDB instance上面去, 在一个不是太复杂的系统, InfluxDB 还是够用的。

InfluxDB 本身包含自己一套的,采集, 聚合和展示套件, 展示这一块我们使用 Grafana。 Grafana 支持从多种数据源导入展示数据, 简单容易上手。

这样一套系统,搭建起来,技术要求没有那么高, 成本也很有限, 使用维护的成本也低, 可谓物美价廉, 当然我们把一些其他的监控, 比如JVM 等也都放上去了。

个人收获

这整个系列可谓是干货慢慢,很多系统其实都是类似的。

一整套完整的系统值得我们学习.

参考资料

如何构建一个交易系统(一)

如何构建一个交易系统(三)

如何构建一个交易系统(四)

如何构建一个交易系统(五)

如何构建一个交易系统(六)

如何构建一个交易系统(七)

如何构建一个交易系统(八)

如何构建一个交易系统(十)

中债登、银行间市场、上清所… 这些机构你能分清几个?