如何构建一个交易系统(十一)
天下武功唯快不破,那么对于交易系统更是如此,如何达到这个快,可以提升单机性能,提高运算速度和效率, 可以横向集群扩展增加吞吐量和并行能力。尽可能将数据放在内存, 在内存中实现计算是一种行之有效的方法,IMDG(In Memory Data Grid), 是此篇我们将要探讨的内容。
概述
需求
对于一个交易系统,在交易时段,需要非常精确快速提供下面几点服务:
- 保存和计算所有用户的资金信息
- 可用现金
- 占用保证金
- 账户浮动盈亏
- 实时聚合计算,规则引擎触发
- 保存和计算用户持仓
- 仓位变更
- 实时浮动盈亏计算
- 实时消息推送
- 用户在线状态
- 订阅价格信息
- 持仓仓位信息
基本条件
我们需要这样一个工具(或者服务)提供:
- 缓存
- 内存中索引
- 高可用性
- 易扩展性
- 落地方案
- 流式计算
- 内存运算
当建立一个大型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 数据; 其实按照价格传播的快慢可以分几大类型价格:
- 匹配中心, 这个是最新最快价格放在节点的本地 hashmap 中
- 内部计算的价格,通过redis 发送给PNL, Account 节点
- 历史数据,比如分时,小时这个是最后价格,通过KAFKA 到Market 节点进行聚合和存储
在考察了一堆的时序数据库,比如最近当红炸子鸡InfluxDB,但是如何处理OHLC,特别在你有10来个价格档位情况下没有看到合适的处理方法,同时InfluxDB 开源不支持集群了。 通过简单的评估, 比如平均有200产品同时开市,每个每秒平均报价5次, 有10种K线聚合情况下, 每秒其实也就 200x5x10 ~= 10000 次操作, 如果完全在内存中还是绰绰有余,DONE!
所以很多时候解决方案,没有你想像那么复杂, 计算机没有你想像那么快,也没有你想像那么不堪,只有试过方知可不可。
Market 数据部分同时涉及,报价往客户端实时发送, 这里涉及内容比较多,下篇再说。
上面涉及的各模块之间的交互,其实不完全借助IMDG完成,KAFKA在内部消息通道中占有很大一块比重,将分独立篇幅叙述。