金融大数据革命-eXtremeDB金融数据库
-基于矢量的统计函数的流水操作实现内存分析
简介
对于当今自动化资本市场的底层软件来说,其关键任务是管理与交易和报价相关的市场数据,其中包括用于算法交易、风险管理以及订单匹配与执行的应用系统。降低上述系统时延能够获得竞争的优势,因此业界迫切期待能够有效提高市场数据管理速度的技术。
市场数据通常以时间序列的形式出现,也就是在一段时间内对某个值的重复计量。数据库管理系统(DBMS)发展出专业的技术来加快对时间序列数据的处理,包括对这类数据基于列的处理技术。本白皮书详细介绍了McObjecteXtremeDB金融版数据库系统实施的基于列的数据处理方法。包括在采用基于列和基于行的混合数据处理的数据库设计,eXtremeDB金融版如何实施列处理;此外重点介绍了eXtremeDB金融版为降低时延而设计的一个主要特性:基于矢量的统计函数库,该函数库可以在数据序列(列)上执行。通过最大限度地提高相关市场数据载入到一级/二级CPU缓存中的比例,基于列处理如何能够提升性能;以及通过减少在一级/二级缓存和DRAM之间开销巨大的通讯,基于矢量统计函数的流水操作如何能够最大限度地提升性能。
纵列数据和“序列”数据类型
关系数据库管理系统使用表这个概念对数据进行逻辑存储,表由行和列组成。面向对象的数据库管理系统和诸如eXtremeDB金融版等“对象友好型”数据库系统使用对象的类来代替表,但是从概念上来说,类和表相同,为了简单起见,本白皮书统一使用“表”。
传统(关系和面向对象)的数据库管理系统是面向行的,因此它们基于行对存储的数据进行操作。数据库系统将行组织成数据库页面,数据库页面是数据库管理系统输入/输出的基本单位。当数据库管理系统读取数据时,无论是从数据库的缓存还是持久性介质进行读取,都需要将一个数据库页面转移到CPU的一级/二级缓存中。例如,假设表如下面的图 1所示,数据库页面大小为4096字节,一个页面可以容纳约145行数据。该图相当于两个数据库页面。图中列出的行号仅限于说明的目的。
行号 | 股票代码 | 开盘价 | 收盘价 | 成交量 | 日期 |
1 |
IBM |
204 |
205 |
200000 |
20120410 |
2 |
IBM |
202 |
203 |
150000 |
20120411 |
3 |
IBM |
204 |
205 |
300000 |
20120412 |
… |
… |
… |
… |
… |
… |
145 |
IBM |
206 |
202 |
400000 |
20120927 |
150 |
IBM |
202 |
203 |
200000 |
20120928 |
151 |
IBM |
203 |
202 |
200000 |
20120929 |
152 |
IBM |
202 |
205 |
400000 |
20120930 |
… |
… |
… |
… |
… |
… |
290 |
IBM |
210 |
209 |
150000 |
20130316 |
图 1.
基于行处理的方法存在的问题是,对于时间序列数据的分析(即下文的市场数据分析),特定操作很可能只对这些列的某个子集感兴趣。例如,许多金融计算可能只使用其中一个字段比如收盘价(图 1中的“收盘价”列)。面向行的处理会获取整个页面,并将其装入到CPU缓存中,这就包括了同该操作无关的“股票代码”、“开盘价”、“交易量”和“日期”等字段。换句话说,由于实际使用的数据只是全部获取数据的一部分,因此浪费了大量带宽。计算中使用“收盘价”列,但是却将“股票代码”、“开盘价”、“交易量”和“日期”列都转移到一级/二级缓存中,而这仅仅是因为传统面向行数据库管理系统所采用的数据组织方式。
这造成了同操作无关的数据对缓存的“大量占用”,而这个问题可以通过基于列的数据库来解决,顾名思义,这种方案将数据按列装入到一级/二级缓存中。在上述例子中,如果采用列优先的方法,可以将“收盘价”这列的数据装入到数据库页面中, 直到达到页面大小的限制。在将此页面移入CPU缓存时,与基于行处理的方法相比,每次转移都能够将更多的“收盘价”列数据转移到一级/二级缓存中。转移带宽的使用效率更高,需要的获取次数更少,因此显著缩短了时延。
然而,虽然基于列处理能够加快对数据列的分析处理,但是在其它情况下(例如要求对多列进行操作的“常规”数据处理),基于行管理通常更快。事实上,许多资本市场应用程序管理的数据,既有适合按列处理的数据,也有适合按行处理的数据。
利用eXtremeDB金融版,开发人员可以灵活实施基于列和基于行的数据处理,并且能够在一个表中使用将这两种模式相结合的混合解决方案。它是通过独有的“序列序列”数据类型实现的这一功能。使用eXtremeDB金融版的C/C++开发人员先创建一个文本文件格式的数据库架构(设计),然后使用McObject的mcocomp工具对其进行编译,得到头文件和数据库词典,以便在相应的应用程序中调用。数据库schema声明了表不同列的数据类型。例如,下面的架构创建了Security表(类),其中一列为固定长度的字符串类型,五列为可变长度的字符串类型,还有九列被声明为序列序列(sequence)数据类型:
class Security
{
char<16> Id;
string ID;
string Ex;
string Descr;
string SIC;
string Cu;
sequence<time asc> TradeTimeStamp;
sequence<float> TradePrice;
sequence<uint4> TradeSize;
sequence<time asc> DateStamp;
sequence<float> Open;
sequence<float> Close;
sequence<uint4> Volume;
sequence<time asc> AskTimeStamp;
sequence<float> AskPrice;
sequence<uint4> AskSize;
sequence<time asc> BidTimeStamp;
sequence<float> BidPrice;
sequence<uint4> BidSize;
};
在上面的表中,char和string是标量数据类型,这种数据类型只能容纳一个元素。与此相对,sequence是一种无限数组,可以容纳多个元素。从概念上说,可以将两个或多个sequence类型的数据视为时间序列数据。在上述例子中,可以将四组sequence数据视为四个时间序列数据。某些编程语言支持矢量运算,但通常要求将矢量驻留在内存中,因此限制了它们的实用性。(如果需要运算的矢量非常大和/或可用的系统内存非常小,那么系统可能无法完成该运算,或者会降低系统性能,因为操作系统不得不在物理内存和虚拟内存之间多次数据交换。)此外,对于基于矢量的语言,下面的表达式
result = a * b / c
必须先得到(实例化)运算a * b的中间结果(另一个矢量),才能执行下一个运算(将该矢量除以矢量c)。假设,矢量a和b含有1亿个元素,每个元素是一个4字节的无符号整数(4亿字节),则必须得到第三个包含4亿元素的中间矢量的中间结果(然后将每个元素除以矢量c的元素,得到最终的结果矢量)。毫无疑问,4亿字节的矢量无法装入CPU的缓存中,因此不得不在内存(DRAM)中进行创建,从而需要在CPU和DRAM之间进行大量数据移动,造成了时延。
eXtremeDB金融版采用C语言编写,eXtremeDB用户通常以C或类似语言(C++、Java、C#、Python)进行编程。这些语言都不属于矢量编程语言,但是可以使用C语言实现矢量(数组)处理,而eXtremeDB金融版提供了包括超过150函数的函数库,可用于对sequence数据进行基于矢量的统计分析。
为了克服矢量编程语言要求将整个矢量驻留在内存中的限制。eXtremeDB金融版实施了一个名为区块(tile)的专用数据库页面类型。该页面类型用于将序列数据装入到一级/二级缓存中。McObject使用术语“对矢量元素按区块处理”来描述eXtremeDB金融版对时间序列数据的处理,利用该产品基于矢量的统计函数按序列处理所存储的数据。对于上述包含“开盘价”、“收盘价”、“成交量”和“日期”元素的表来说,可以只将需要处理的“收盘价”数据的区块装入CPU缓存,如图2所示。
英文 | 中文 |
Close | 收盘价 |
图 2.
在eXtremeDB金融版中,sequence类型的字段可以与所有其他支持的数据类型一起在表中使用;该数据库系统“知道”以列优先的方式处理时间序列数据,并且对其他类型的数据基于行处理,如下面的图3所示。
英文 | 中文 |
Symbol | 股票代码 |
Exchange | 交易所 |
Open | 开盘价 |
Close | 收盘价 |
Volume/Vol | 交易量 |
Date | 日期 |
Row | 行 |
图 3.
基于矢量的统计函数的流水操作(Pipelining)
基于矢量的统计函数的流水操作是eXtremeDB金融版的一项重要编程技术。该技术能够提升处理时间序列数据(例如市场数据)的性能。采用流水操作带来的一项重要改变是,它能够让处理过程中使用的中间结果集保留在CPU缓存中,而不必像其他数据库系统(包括面向列的数据库管理系统以及矢量/矩阵语言)要求的那样作为临时表输出。将中间结果保留在一级/二级缓存中能够消除在CPU缓存和主内存之间通过快速通道互联(Quick Path Interconnect,QPI)或前端总线(Front Side Bus,FSB)进行反复数据传输而产生的时延。
下面使用以C和SQL数据库编程语言编写的eXtremeDB金融版的基于矢量的统计函数作为例子,介绍流水操作的工作原理。假设应用程序需要计算某只股票的5天移动平均值和21天移动平均值,并且检测较快移动平均值(5天移动平均值)位于较慢移动平均值(21天移动平均值)上方或下方的点。交易员可能会要求实现这种功能,他/她可能会将位于上方看做进入市场点(买入或卖空信号),并且将位于下方看做市场退出点(卖出或平仓信号)。
这种处理可以由下列C代码实现,其中使用了eXtremeDB金融版基于矢量的统计函数库中的四个函数以及历史收盘价序列作为输入:
mco_seq_window_agg_avg_double(&5day,&close, 5);
mco_seq_window_agg_avg_double(&21day,&close, 21);
mco_seq_sub_double(&delta, &5day,&21day);
mco_seq_cross_double(&crosspoints,&delta, 1));
mco_seq_map_double(&crossprices,&crosspoints, &close));
1. 第一个函数“mco_seq_window_agg_avg_double”对“&close”引用的输入收盘价序列执行计算,生成在特定时间段(在本例中时间段为五天)元素的平均值。计算结果(即5天移动平均值的序列)被一种称为迭代器的特殊类型的数据对象引用。迭代器提供了指向序列元素的指针(引用),这里创建了一个迭代器“&5day”来引用5天移动平均值。请注意,虽然在本代码示例中,迭代器“&5day”是后续统计函数的输入,而且也将它当作数据库中的序列进行处理,但是实际上并没有将它实例化为输出,也没有将其传输到主内存或存储中。相反,在程序执行期间它一直位于CPU的一级/二级缓存中。
2. 同一个函数“mco_seq_window_agg_avg_double”在“&close”引用的收盘价序列上执行计算,并且返回引用21天移动平均值的迭代器“&21day”。
3. 基于矢量的函数“mco_seq_sub_double”对创建的迭代器执行计算,从对应的“&5day”元素减去“&21day”元素,得到一个新的迭代器“&delta”,顾名思义,它表示5天移动平均值和21天移动平均值的差。
4. 函数“mco_seq_cross_double”将迭代器“&delta”作为输入,并且通过检查“&delta”元素与零相交(从正到负或从负到正)的点(元素位置)找到交叉点。然后返回“&crosspoints”。
5. 最后一个函数“mco_seq_map_double”,将交叉点映射为收盘价原始序列。换句话说,它返回5天移动平均值和21天移动平均值相交处的收盘价序列。
通过利用eXtremeDB金融版基于矢量的统计函数作为SELECT语句的参数,下列SQL代码可以实现相同的功能:
select seq_map(ClosePrice,
seq_cross(seq_sub(seq_window_agg_avg(ClosePrice,5),
seq_window_agg_avg(ClosePrice, 21)),1))
from Security;
1. 两次调用“seq_window_agg_avg”在“ClosePrice”收盘价序列上执行计算,得到5天移动平均值和21天移动平均值。请注意,在SQL中,迭代器位于SQL引擎内部,因此迭代器没有出现在代码中。
2. 函数“seq_sub”从5天移动平均值中减去21天移动平均值;
3. 将上述结果作为第四个函数“seq_cross”的“输入”,找到5天移动平均值与21天移动平均值相交的位置。
4. 最后,函数“seq_map”将交叉点映射到原始的“ClosePrice”序列,返回两种移动平均值相交位置的收盘价。
在上述两个SQL和C代码示例中,由于在操作开始阶段只将收盘价装入到一级/二级缓存中进行处理,因此性能得到了大幅提升。即使所处理的数据来自一个表的多个列(如上面的图3所示),也是如此。而且,在最大限度地降低CPU缓存和内存之间的数据传输方面,甚至能够获得更大的改善。由于这种方案无需在一级/二级缓存外的主内存中创建、填充以及查询临时表,而这对于使用其他数据库系统和基于矢量的编程语言来管理所处理的中间结果来说是必不可少的。
之所以能够获得如此巨大的性能提升,是因为eXtremeDB“对矢量元素的按区块处理”。具体来说,每次调用“mco_seq_window_agg_avg_double()”都将对一个输入数据区块进行处理,每次都会产生一个区块输出,并将该输出传递给“mco_seq_sub_double()”,相应地,也会产生一个输出区块,并将其作为输入传递给mco_seq_cross_double(),再产生一个输出区块,并将其传递给mco_seq_map_double()。在最后一个函数mco_seq_map_double()使用完输入后,整个过程将从头开始,产生其他处理的新区块。
相比之下,如果使用传统SQL数据库管理系统来实现上述任务(找到5天移动平均值和21天移动平均值的交叉点),那么开发人员需要首先创建三个临时表:
CREATE TEMP TABLE mavg ( 5day float, 21dayfloat );
CREATE TEMP TABLE sub ( delta float );
CREATE TEMP TABLE crosses ( Price float );
接下来计算5天移动平均值和21天移动平均值,并且将结果填充到表“mavg”(此代码假设该数据库系统支持用户定义的函数):
INSERT INTO mavg SELECT MovingAvg(ClosePrice, 5 ), MovingAvg ( ClosePrice, 21 ) FROM Security;
然后,需要将两种移动平均值相减的结果填充到临时表“sub”中:
INSERT INTO sub SELECT 5day – 21day FROMmavg;
“普通”SQL语言能够进行的操作到此为止。开发人员必须编写代码(这里给出的是C代码)来获得交叉点的位置:
// create two arrays to hold the positionsof crosses over/under zero
int overs[100];
int unders[100];
// indexes into the two arrays above
int j = 0, k = 0;
rc = SQLExecDirect( “SELECT delta FROM sub”);
rc = FETCH FIRST into “PrevDelta”
for( pos = 0; rc == S_OKAY; pos++ ) {
FETCH NEXT into “ThisDelta”
if ThisDelta < PrevDelta AND ThisDelta< 0 AND PrevDelta > 0
unders[j++] = pos;
else if ThisDelta > PrevDelta andThisDelta > 0 and PrevDelta < 0
overs[k++] = pos;
PrevDelta = ThisDelta
}
接下来需要获得收盘价,并且将其插入到临时表“crosses”中,插入的位置是之前确认为交叉点的位置:
rc = SQLExecDirect( “SELECT ClosePrice fromSecurity” );
int count = 0;
j = 0, k= 0;
while( rc == S_OKAY ) {
if( count == overs[j] {
INSERT INTO crosses VALUES ( ClosePrice );
j++;
} else if( count == unders[k] ) {
INSERT INTO crosses VALUES (ClosePrice );
k++;
} else {
; // do nothing, this ClosePrice was not across over or under
}
count++;
}
现在,临时表“crosses”中包含了5天移动平均值和21天移动平均值交叉位置的收盘价(但是没有方向信息:从负到正或从正到负。计算这类信息是上述eXtremeDB金融版SQL代码的一个“额外”功能;如果使用传统SQL数据库管理系统实现这一功能,需要添加额外的代码)。
显然,使用传统SQL数据库管理系统需要大量额外的代码。但更重要的是,必须对处理中使用的所有临时(瞬态)结果进行“实例化”,也就是要在一级/二级CPU缓存外创建结果作为输出。在传统SQL方案中,第一个SELECT语句查询数据库的收盘价表(“ClosePrice”),然后填充一个新的临时表。这需要将收盘价数据收集到一个页面或多个页面中,通过位于CPU缓存和RAM之间的快速通道互联(QPI)或前端总线,将数据从数据存储的ClosePrice列装入到CPU缓存中进行处理,然后再回写(从相反方向)到临时表。在填充第二个和第三个临时表时会重复这一过程,最后,还必须再次读取第三个临时表,将结果从RAM再次装入到一级/二级缓存中,并检查交叉点。
请注意,由于消除了基于行布局固有的浪费,因此纵列SQL数据能够获得一定程度的效率提升。但是,如果不采用流水操作,由于反复通过QPI或FSB传输数据而产生的开销将非常巨大,因为QPI或FSB的速度通常只是一级/二级缓存的几分之一。这类传输产生的时延乘以反复移动某个数据集需要的页面数。换句话说,使用SQL数据库管理系统处理产生的时延比使用流水操作产生的时延要多不止3-4倍。将达到3-4倍再乘以需要通过QPI/FSB传输的页面数,而流水操作能够完全避免对中间结果的传输。
性能和eXtremeDB金融版
在测试中,eXtremeDB金融版的“序列(sequence)”数据类型和基于矢量的统计函数的流水操作能够实现什么样的性能?最近,McObject会同合作伙伴Kove公司和Fultech Consulting公司进行了一项风险管理概念验证(POC),以应对日益增长的在紧迫时间范围内进行综合性风险分析的严苛需求。这项概念验证使用10年历史风险值(value-at-risk,VaR)模型对50,000个投资组合进行衡量,每个投资组合含有3,278个仓位。目标处理时间设定为10分钟,反映出对当天风险分析甚至实时风险分析应用程序类别日益增长的需求。
实际上,eXtremeDB金融版不到8.5分钟就完成了这种对市场数据的大型处理。它在约13.5 TB的原始市场数据上执行了约4,280亿次计算。通过数据库规范化(主机服务器为四台4插槽Dell R910,每个插槽可容纳一枚8核CPU,总计128个核心。每台服务器含有1024 GB的RAM)将数据减少为Kove®XPD™ L2存储上的3.5 TB。这个结果相当于以每秒进行约9.5亿次计算的速度进行连续金融模型处理,每小时处理65 TB(请点击此处了解有关这个概念验证的更多信息)。
设计这个概念验证时我们充分考虑了全新的监管环境,具体来讲,是在Dodd Frank法案、巴塞尔协议III(Basel III)以及要求交易公司更密切地监控交易对方风险的其他改革下出现的法规要求。此外,资本市场的激烈竞争和技术挑战也促使业界迫切需要更加完善的实时数据库解决方案。市场数据不断激增,纽约股票交易所每天都会产生超过五亿个交易数据,在交易高峰期这一数字甚至会超过二十亿。资本市场技术必须消化、整理和分析更多数据——并且利用日益复杂的应用程序和算法来进行更深入的处理。此外,面向实时金融系统的数据库管理系统必须利用功能越来越强大的多核硬件和高性能网络。
简而言之,数据库管理系统一定不能成为处理的瓶颈,通过多种新方法能够实现这一目标,比如内存存储、市场数据的面向列的布局以及针对CPU缓存优化的统计分析函数。eXtremeDB金融版将上述优势融为一体:它是一款内存数据库系统(支持可选的持续性存储),而且,如上所述,支持对时间序列数据进行纵列处理,同时保留了在其他情况下更高效的基于行的处理方式。
此外,eXtremeDB金融版是一款功能完备的数据库系统,可提供支持ACID(原子性、一致性、独立性和持久性)属性的事务,以确保数据完整性;数据库定义语言;多线程并发访问;数据库索引(B-树、KD-树和哈希等);内建的集群和高可用性特性、行业标准的SQL、ODBC和JDBC接口,以及本机C、C++、Java、.NET和Python API;超过13年的丰富经验以及在要求严苛的应用环境中部署了数千万个成功案例。
最后,eXtremeDB金融版提供了一些真正独有的性能提升特性,才从资本市场应用程序中脱颖而出。其中一项是,执行路径非常短,代码开销仅有约150K。通过减少每个数据库操作所需的CPU周期数并且提高操作所需指令位于一级/二级CPU缓存的可能性,这种短路径的特点能够有效加快代码的执行。另一项独有的强大特性是本文介绍的流水处理。这项编程技术基于eXtremeDB金融版对市场数据的基于列处理方式而实现,通过确保市场数据序列必要时置于一级/二级CPU缓存中可加快处理。流水操作技术能够最大限度地降低时延,为处于技术前沿的金融系统实现竞争优势。