作者简介:罗意,广发证券 IT 中后台系统架构师。2013 年初加入腾讯,主要负责腾讯微博的转发评论的逻辑层和存储层,后期负责微博后台的基础组件研发。2016 年加入广发证券信息技术部,主要负责行情、交易等中后台业务的系统架构设计和研发工作。在高性能、高可用后台系统架构设计方面经验丰富。目前专注于金融 IT 系统、FinTech 等相关技术的应用,关注互联网技术和金融系统的深度融合。
目录
-
证券行情和交易系统服务架构
-
如何做到高并发、高性能?
-
如何做到高质量推送?
-
如何做到高可用、可扩展?
-
遇到的挑战点
广发证券是最早在自研系统中使用Go语言的券商之一,我们大概有接近3年的使用经验。现在广发自研的分布式系统以及一些高性能的系统或者对性能优要求的系统,基本上都是用Go实现的。
我们团队使用Go语言自研的系统有:
1、行情云系统,要求高性能。
2、分布式交易中台系统,要求安全、稳定。
今天主要和大家交流这两个基于Go改造的系统。
证券行情和交易系统的特点
• 行情用户连接并发峰值高
• 行情数据时效性要求高
• 行情数据推送流量大
• 行情指标计算量大
• 安全、稳定、快速
行情和交易系统有各自的特点:
1、行情用户连接并发数高。9点到11点半开盘期间,用户在线时间比较长,而且是长链接。
2、行情数据时效性要求高。用户要求我们的行情数据更新速度要尽量快,来自于交易所(深交所、上交所)的行情数据,大概是3秒一个更新数据快照,要求券商系统尽快将交易所的最新行情数据送达用户终端。我们要采用的是行情数据由后台主动推送的方式。
3、行情数据推送流量大。广发证券作为第一梯队券商有约1800万用户,牛市时最高同时在线约50-80万。一个用户平均订阅自选股约10个,这样同时对外的行情推送量就很大,约200万左右QPS,实际是100万QPS左右。高峰时刻在9点30开盘和13点开盘的那会儿,流量非常大。
4、行情数据指标的计算量大。从交易所快照行情数据到行情指标数据,需要经过大量计算工作。以十种K线计算为例,证券数约2W,每天开市4小时,每3秒刷新一次行情,共需计算7.2亿次。再加上实时,分时,市盈率,涨跌幅,委比,委差等十几项指标计算,每日计算量在10亿级别以上。
5、主管部门对于券商管控比较严。交易系统的安全、稳定就显得非常重要的。虽然我们用互联网思维解决这些问题,但还是需要有适应性来适应券商的环境,不能做得像互联网那么灵活,比如说用户无感升级,比如说扩容,服务器和网络成本。其实针对灵活性、成本、安全、稳定等指标会有一些平衡性的取舍。一旦有一个用户下单没有成功或者下错单了,下单没成功我们系统需要尽快让用户知道是否成功与否,而不应去做重试了,以避免系统下错单。再者就是交易速度要快,在牛市的时候,很多券商下不了单,也就是处理速度和稳定性存在问题的。我们需要在交易速度快的同时也进行过载保护等措施。
1.证券行情和交易系统服务架构
广发行情云系统从外包转向自研,我们做的事情是改变后台设计架构,以适当高并发、高推送流量的稳定可靠的系统要求。首先,我们对整个行情和交易业务进行梳理拆分:
** 一是横向拆分,**借鉴于微服务或分布式相关理念来进行业务拆分。依据功能完整、数据同构、职责单一的原则进行横向拆分为实时行情、K线行情、分时行情、分笔行情、板块行情、资金流向、代码链、鉴权服务、交易服务、期权期货、基金债券和Lv2行情。
二是纵向拆分,依据合理分层,剥离逻辑和存储的原则将行情云系统进行纵向拆分为接入网关、读逻辑层、数据存储层、写逻辑层、行情转码层。将交易系统拆分为接入层、逻辑层、柜台接入层、柜台交易层。
证券行情和交易系统服务架构图图片
交易系统架构
我们首先从用户侧来看,再从行情源侧(深交所行情、上交所行情)来看。用户侧需要一个高并发、高性能的接入层,用于屏蔽大规模用户连接,将用户请求通过长连接转到对应的业务逻辑模块。接入层同时可以针对逻辑层的具体业务逻辑模块做负载均衡、路由容灾、服务发现。接入层的设计指标是一个进程在四核心、1G内存的情况下支持5万连接数、10万QPS(行情小包),一个请求包大概是200字节左右。我们需要考虑以下问题:
1、接入层如何提供50万左右的同时在线连接数?绝大多数的TCP连接数来自于APP,Web用户使用WS协议连接的。
2、接入层如何支持不同的接入协议(tcp/ws/http)。
读逻辑层有分时、K线、资金流向、实时等行情模块,只需要与接入层进行通信,而无须接触每个用户连接,这样读逻辑层就可以专注于处理自己的业务逻辑。
要实现推送大规模的行情数据,需要一个推送系统来管理用户对自选股的订阅,从而实现给某个用户推送他关心的行情数据,同时管理用户连接与接入层进程、接入机器的映射关系。推送系统从写逻辑层订阅全量标准行情数据,然后再按用户的订阅关系推送出去。
数据层,主要用于存储行情数据。最开始的方案使用Redis存储,读写很高频,3秒一个快照,主Redis是要不停写,高并发量,备redis不停的同步,容易造成redis读取性能差的问题。同时因为自建机房和合规的原因,我们不能架设大规模的redis集群,所以我们最后采用了性能好、但开发难度大一些的文件存储行情的方式。
写逻辑层,负责解码深交所、上交所的行情源数据。交易所过来的数据是源数据,而我们给用户展示的是标准化的行情数据,如分时、K线、分笔和资金流向等数据。
周边支撑系统,包括HttpDNS、服务发现、负载均衡、监控告警、日志服务等。
此外本架构方案在行情服务稳定运行后,也应用于交易系统。交易系统有加密和安全的要求,一是对通信数据进行加密,交易数据需要进行严格加密,二是对用户权限、登录态进行安全校验,此外还需要提供验证码、流量控制等措施。
横向拆分交易系统逻辑层,可分为股票的查持仓、委托买卖,基金查询和申购,OTC(场内交易),融资融券等等。逻辑模块采用读写分离方案,比如:有些用户不断刷持仓查询,但不一定会下单,读写分离方案可以做到委托下单与查询接口相互独立。柜台接入层,承接恒生柜台、金证柜台、顶点柜台的统一接入功能,将不同柜台的接入与业务逻辑进行解耦。
**
接入层是解决高并发和高性能问题的关键,我们采用TCP接入来承接广发证券体系内最大流量的行情服务。
首先要解决的问题是管理高并发的TCP连接,一个进程如何做到同时5万TCP连接,总体QPS 10万?其实用常规的Go来写,那就是写一个Socket监听,每来一个连接甩出一个读协程,再甩出一个写的协程,中间可能还需要一个协调控制的协程,再去处理这个事情。这样是否可行?我们最开始就是这样做,但是测试结果是可以达到 1万个TCP连接,2~3万QPS,再往上增加压力就跑不动了。所以这种方式很难达到我们的设计要求,我们需要进行改进。
第一、对于10万级别的协程数,Go是很难调度的,基本上是超过了它的调度能力,一旦GC使用程序进入短暂停顿,就会堆积大量的任务,GC过后就是大爆发的时候,这时候很容易出问题。用户连接管理和业务协程池是平等关系,所以5万连接产生的10万协程和N个业务协程(常数级别)是平等关系,所以10万协程应该作为一个整体参与到N个业务协程来分配CPU的调度,我们要设法让这10万协程变为一个整体。
第二,各个模块的职能要单一和专注,有converger进程专门进行accept用户连接,有netcgo模块专门进行用户管理,有业务逻辑模块专门进行请求转发,同时有推送逻辑进行推送数据的转发,tcpClient专门负责与业务系统进行连接和转发数据,这样将业务模块的请求收敛为一个长连接上来了,这样可以更高效的处理数据。
我们做自定义协议设计需要考虑如下问题:
我们为了更高效的传输行情小包的数据,增加了一个handshake的过程,这样可以极大压缩业务请求所需携带的包头信息,提高传输效率。同时,我们将接入协议和内部协议分离,这样内部模块协议升级和发布不会影响前端用户请求。
这是我们上线之前针对接入层做的性能测试。Client ->接入层->server,server进行echo回射。
当时的监控图如下,大概跑了一个多小时。
![](https://s4.51cto.com/images/blog/202104/20/90bba05ea2f0c7a7f0f7e190ad7834ee.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=)
3.如何做到高质量推送?
推送系统主要职责是管理用户订阅行情,推送行情数据。PushManager负责和接入层通信,管理用户的订阅请求,同时将用户和接入层进程ID进行映射,将映射关关系发送到push Proxy,由Push proxy进行UDP推送行情数据。对于同一个接入进程上的用户推送数据,PushProxy会进行合并,这样能更高效的进行推送。
4.如何做到高可用、可扩展?
4.1 服务发现
业务模块要做到无限可扩展,需要实现服务发现和负载均衡机制。我们的服务发现方案是采用的开源组件Consul来实现的,基于docker容器化部署。比如对于K线业务模块,取名为kline.gf.com,该服务部署在2台机器的集群里,每台机器部署2个进程,每台机器都有一个registrator来实现自动注册服务,这样在consul里kline.gf.com会对应四个IP:PORT对标识的进程。客户端需要调用时,会从consul拿到4个IP:PORT,然后和他们建立4条连接,接下来就可以把请求包发到对应的进程上去。那么这四个进程怎么路由?这就是负载均衡要解决的事情了。
4.2 动态负载均衡组件
这个组件和服务发现结合,从服务发现那里拿来4条IP:PORT记录,业务请求过来时如何分配请求?业务模块需要传入kline.gf.com调用GET API,负载均衡组件会依据权重返回一个质量比较好的连接,然后业务模块就可以发送请求包了。业务逻辑收到回包后需要调用report接口上报调用情况。负载均衡组件会依据调用成功次数、业务耗时、连接情况等信息对某一个连接进行计分,然后依据计分结果选择最优连接。连接情况是实时进行反馈,所以该负载均衡方案能在几十毫秒内就能将down的进程移除调度,能在秒级将新增的进程加入到调度。
4.3 解码与转发系统
从交易所层面来看,解码怎么做到高可用?因为VDE是上交所或者深交所的代理服务,你可以理解为把行情快照转发到这里,部署到我们的机房。我们其实部署了三套解码,一个是做容灾,挂了一套,还有其他的。交易所来的时候已经打上时间戳,repeater可以针对两路行情数据进行冗余去重。
4.4 数据监控与告警
如果要说高可用,不仅仅考虑负载均衡、异地部署、集群服务、数据冗余等,监控和告警也是非常重要的。业务进程调用监控上报API上报指标,Api的协程每10秒汇总一次通过UDP上报到本机房的monitor,monitor通过Tcp方式将监控数据转发给influxDB,用户可以通过Grafana在Web上查看监控报表。
5.遇到的挑战点
1、Goroutine调度没有优先级之分
在一些高并发场景下,大量请求Goroutine和其他工作Goroutine数量级差别较大,他们之前调度是没有优先级之分,所以有时会导致调度不合理。
2、10万级别Goroutine调度不均衡
如果一个进程Goroutine数量是10^3 级别,Go的问题不大。如果是10^4级别,Go的调度不一定高效。10^4级别的Goroutine数量调度不均衡。
3、大量Goroutine对象创建和销毁,GC影响系统响应速度
大量Goroutine时,GC影响特别明显,因为一旦停顿一下,就会阻塞很多任务,一旦放开就会像洪水一样过来。
4、cgo通信机制的低效
接入进程Converger专门用来accept连接,然后通过UnixDomain发送到Epooll管理器。Epoll管理连接通过一个Goroutine拉起,常驻在进程内,所以收敛了Goroutine的数量。
用户频繁上下线,需要频繁的分配内存,我们设计5万个连接,一次性创建5万个Hash桶放在那里,用户上来时获取一个内存对象,用户下线后将回收内存对象,打上free标记。
我们通过开辟一个无锁队列,C语言和GO语共享这个队列地址,通过内存通信来代替CGO通信。
接入层要负责很多业务,下面又有不同的分布式模块。业务模块之间不能互相影响,业务逻辑模块之间比较好隔离,那就是不同的系统、不同模块,使用不同的集群来部署。接入层是一个要对多种业务,我们在内部通过不同的命令字、不同的业务模块使用不同的Chan,这样的好处是不同的业务转发不会相互影响。比如代码链是比较重的业务,实时业务是比较轻的,这样他们就可以有不同的优先级和处理方式。Chan还有一个好处是削峰填谷,有可能某一时间请求量比较多大,像浪一样打上来。对于你系统来说,这种浪是不友好的,对于一个程序来说这种浪会触发边界值。你有Chan以后,一旦这种浪打过来,会先存储在chan里,然后空闲时慢慢处理,所以会有平滑的作用,业务系统看起来是相对平滑的曲线,不会有浪那样的感觉。
编程SDK集成负载均衡、服务发现和异步化session,以及一致性的协议封包和心跳机制,同时管理动态的业务连接池。开发者只需关注业务逻辑,每一个程序都能高并发和高可用,同时解决了分布式系统模块间的通信协作问题。
有疑问加站长微信联系(非本文作者)