财经行市API架构优化实践

财经行情API架构优化实践

        从3月份到5月份一直在做行情API的架构优化,在这个过程中遇到了很多问题,也尝试了很多的解决方案,但都没有寻找到一个最佳的方案,最后基于API的特点自己实现了一个基于共享内存的key-value存储touchdb。这里,和大家一起分享一下。

1. 行情API及旧架构介绍

        先简单介绍一下行情API,让大家对这个应用的特性、需求有一个足够的了解,才能清楚了解后续的优化过程。

        行情API主要就是解决股票的实时报价问题,比如股票的价格、涨跌幅、成交量以及一些实时排行榜。由于整个财经系统的股票实时数据在5s内全部刷新,所以5s内API的读写量还是很大的,大概最多的时候有20000+w/5s,而正常情况下700000r/min,最多时可以达到1500000r/min,也就是说平均11000r/s - 25000r/s,因为我们有9台前端API服务器,所以落到单台服务器的访问量1200r/s - 2700r/s。通过上面的描述可以知道API具有高并发、大访问量的特点。

        下面看一下旧版API的结构,基于Tokyo Cabinet(后文简称tc),前端通过自己开发的nginx模块解析请求提供服务。

财经行市API架构优化实践

        对这个架构简单描述一下,首先这个架构具有可扩展性,在访问量高时,通过增加服务器就可以解决。采用的pub/sub实现一对多通信,Apiserver是pub端,在接收到上游数据之后向所有的下游Apiclient(Apiserver和Apiclient在两台不同的服务器)publish数据,Apiclient是一个java进程在接收到数据后会向tc中写入数据。在所有机器的最前端是一个负载均衡层,nginx接收到请求后由自己实现的nginx模块解析并从tc中查询,然后返回给客户端。

        这种架构具有高并发、大访问量以及可扩展的特点,但是它也有一个bug,这个bug曾经多次导致我们先上服务受到影响。先看一下对于tc的读写,涉及到两个进程,一个是Apiclient,一个是nginx。由于tc实际上就是一个文件,这个bug就是多个进程对tc数据文件的并发读写导致的,tc的数据文件头有一部分信息是放在共享内存中,比如hash桶列表等,但是有一个空闲内存池是没有放到共享内存中的,所以这部分数据在多进程并发读写时会产生不一致。因为nginx模块只在启动时去加载tc文件,而在nginx运行时,apiclient不停的去修改tc文件,导致不一致的情况,所以经常通过nginx读取的内容是空的,或者损坏的数据。

        我的后续的优化工作主要就是在保证性能的情况下,解决这个bug,下面具体看一下各种优化方案的尝试。

2. 架构优化

        在优化过程中,尝试了很多方案,主要有:

1. nginx + ngx_lua + redis

        2. nginx + tmpfs

        3. nodejs

        4. 基于共享内存的k-v存储touchdb

        前3中方案都各有特点和优势,但都不能达到最佳的情况,最后自己实现了基于共享内存的k-v存储touchdb,除了可维护性外,在各方面都是最佳的。下面来看看这几种方案。

(1)nginx + ngx_lua + redis

财经行市API架构优化实践

        这种架构中,以redis作为数据存储(key-value形式),Apiclient进程同样去订阅Apiserver,得到数据后写入redis。而前面依然是采用nginx(nginx在高并发及低资源消耗方面绝对是不二选择),这里是通过ngx_lua(关于ngx_lua的使用可以参见 使用ngx_lua构建高并发应用1, 2)实现的解析请求和读redis,由于需要同一请求处理多个股票的情况,所以这里采用的redis pipeline的方式(试过mget,但是会有bug,在多个key的情况会有某几个key的内容为空的现象)。

        这种架构在没上线时,性能测试得到的效果很不错,只是比基于tc的架构差一些,但是可维护性要提高不少。但是上线之后,由于我门线上API服务器的内存比较小只有2G,最大3G,而在高并发时,redis很吃内存,并且一直不释放。内存不够时,redis会使用swap,我们知道redis只有在所有数据都在内存中时,性能才是最好的,使用swap会导致apiclient写不进去,同时前面的nginx读也会很慢。

        我曾经做过测试,用一个nodejs去读redis,并且提供web服务,这时拿20000的并发去压,请求20000次,单个请求的响应内容大小是130kb,在压测的过程中redis的内存从30MB飙升到2.7GB,并且测试完redis的内存也一直不释放,此时redis的碎片率高达1000+,我们知道redis本身是允许内存碎片的存在的,但是这种碎片率是绝对不能容忍的。有没有懂redis的同学,解释一下呢??

        对这种架构小结一下,由于我们前段服务器的内存过小,所以会导致redis使用swap,所以在加大内存后这种架构还是可行的。

(2)nginx + tmpfs

财经行市API架构优化实践

        在上一种架构中,我们看到了瓶颈就在redis,并且在已有的所有nosql产品中已经找不到能够替代redis的,所以我们大胆的设想,将架构中的redis去掉,把所有的数据放到内存中,后续几种方案都是以这个为出发点的。

        这里我们采用了tmpfs,简单点说tmpfs就是把一块内存挂载到文件系统,这样我们就可以像读写文件一样来操作内存。知道这一点后,可以API架构简化成,以tmpfs作为数据存储,对于每一个股票在tmpfs中都有一个文件存储它,文件名就是股票代码。Apiclient接收到数据后直接写文件,前面nginx以静态文件的方式提供服务。

        通过架构图,我们可以明显的发现这个和tc的方案有着相同的问题,就是文件的多进程并发读写问题,所以nginx读到的内容经常是空的,因为此时apiclient正在写这个文件。还有一个问题就是,打开的文件描述符特别多,在测试时必须设置ulimit -n 100000才能正常运行。

(3) nodejs

        nodejs就是服务器端js引擎,用于网络编程,比如实现一个http服务器或者socket服务器。由于js本身的异步、基于事件的特性可以很好的描述网络事件,同时nodejs是构建在google v8引擎之上的,而v8号称是全宇宙最快的解释器,所以nodejs具有很高的性能。

财经行市API架构优化实践

        和nginx类似,依然采用master+worker的进程模型,所有的数据以js map的形式存储在worker进程内,master进程负责订阅Apiserver,得到数据后分发到所有的worker进程,这里采用的是nodejs原生的分发机制。worker得到数据后put到map中,同时服务http请求的响应。

        这种架构在测试时,无论是性能还是资源消耗上都要高前两种方案,只是比tc的方案略差。但是上线测试之后,出现很多问题:

        a. 数据延迟。表现在两方面,一是所有数据整体延迟,比如之前从apiserver到worker需要3s,这种架构则需要5s。二是部分worker的数据延迟,比如4个worker中的3个数据时最新,另一个worker却是旧的,对于客户端来说,单个Api对外的数据时不一致的,会影响到线上服务。

        b. cpu负载高。基本上系统负载在3-4,cpu idle维持在0,而其他方案的系统负载在1以下。

        c. 上下文切换频繁。上下文切换次数在40000-50000之间,其他的方案都在20000-30000。

        通过上面总结,可以知道问题出现在master和worker之间的进程通信,也就是数据分发。所以下面采用redis的pub/sub来替换nodejs本身的分发机制。

财经行市API架构优化实践

        采用这种架构后,数据延迟的现象不存在了,但是cpu负载高和上下文切换频繁的情况依旧存在。

(4)基于共享内存的k-v存储touchdb

        这里暂时不对touchdb的实现做介绍,后面会专门写一篇文章详细描述。touchdb采用c实现,主要的特点有:

        a. 基于共享内存,对所有读写touchdb的进程来说,读写操作都是基于内存的。

        b. 持久化存储。

        c. 基于API的特点,只允许一个进程写,但可以多个进程读,这样可以保证无锁,提高并发度。

        并将touchdb封装成nodejs、python以及nginx的模块,下面看一下第一种基于touchdb的架构:

财经行市API架构优化实践

        实际上,touchdb是被映射到master和worker进程的地址空间内的,但是架构图不好描述,这里依然沿用前面的形式。这里touchdb实际上是承担了master到worker进程的多进程通信,不过不再是基于管道的,而是采用共享内存,所以性能要比nodejs原生的方式高效。

        改用此架构后,上下文切换的次数下降到正常情况,只有20000-30000,下降了40%-50%,但是cpu负载依然很高。我猜测,这可能是nodejs本身的问题,由于nodejs要把所有的操作都改成是异步的,所以内部实现了线程池来模拟异步。在高并发时,这些线程会耗费大量的cpu。

        在否定nodejs的方案后,我又尝试了一下基于python的方案,这里使用tornado框架。与nodejs类似,tornado也是实现非阻塞网络通信的,都是基于epoll的。

财经行市API架构优化实践

        基本上nodejs的类似,只是将前面的nodejs的worker进程换成tornado。这种架构在高并发、大访问量的情况下,tornado会抛出一堆的socket错误,可能本身python虚拟机的性能还是赶不上v8吧。在尝试完上述两种方案均不成功的情况下,不得不使出杀手锏,直接使用nginx c模块。

财经行市API架构优化实践

        由于采用的c模块,并且本身touchdb的性能就不赖,所以这种架构是所有尝试的方案中最好的,无论是在性能还是资源消耗方面。下面看一下各种方案的测试数据:

        a. 响应内容大小150KB+, ab -n 20000 -c 20000,20000并发请求20000次。

        b. 虚拟机,4核E5640 2.67GHz,4G内存。

方案 QPS
TC + nginx C module 1300+
Redis + lua + nginx 900+
nodejs 1100+
touchdb + nginx C module 1400+
   

        从测试数据中可以看出,基于touchdb的方案是最好的,并且在测试时最高可以达到1670qps/s,而单个nginx worker进程的内存只有100MB,还有50MB是使用的共享内存,即touchdb使用的内存。

        上面就是行情API架构优化的过程,学到了很多东西,尤其是自己实现了touchdb,后面会详细描述一下touchdb的实现,并把它开源。