游戏数据分析的架构及调优

系统架构

数据处理系统的架构和流程,最基本的框架流程如下图所示:

 

每天,来自各个项目的数据每时每刻都在打入数据分析系统。这些数据通过数据采集环节,被初步整理成一定的格式,然后录入集中的数据存储系统中。当然,我们系统呈献给用户的是各种统计指标。所谓统计指标,即对原始数据的聚合,形成一定的统计结论。所以,我们需要有查询系统,基于海量的数据进行。在我们的系统中,各个部分的技术选型如下:

 

紫色的部分即为各个部分的技术实现

客户的应用数据,通过REST API打入数据处理系统中。这部分我们考虑使用PHP来实现,主要考虑其实现简单,便于水平扩展;处理速度也可以接受。

Rest API接收到的原始数据,在进入存储之前,还需要进一步整理,主要有两方面的原因:一是一些不合理的日志需要筛选出去;二是我们在这一步,有时候需要把接收到的数据转换成更基本的数据。一个例子是应用程序可能会打入一个应用加载事件visit。那么,我们收到visit的时候,还需要判断这个用户是不是第一次出现;以及用户加载的时候的一些refer信息,等等。所以,这一步的逻辑比Rest API要复杂,做这些逻辑判断所需要的信息也更多,因此我们选用java开发了这一模块。

我们收集到的大量数据,有两方面的信息:一是事件信息,记录发生过的事件:用户A在某个时刻登录了一次;用户B在另一个时刻进行了一次购买,等等。这种事件信息占了系统所存储信息的大部分,而且绝大部分是不断新增的数据。我们采用HBase来存储这部分信息,一方面,HBase对于批量的写入性能很好;另一方面,在rowkey设计合理的情况下,其索引和查询性能也不错。除事件信息外,还有用户的属性信息,比如用户A所在地区位于中国,用户B首次充值的时间为2013年1月1日,等等。这种信息的访问特点是插入和更新都比较多;在查询的时候,既要按照主键(用户ID)来查询,也要按照属性值(例如前面提到的充值时间)来查询。这就要求同一个表有多列的索引能力,所以,我们目前采用了MySQL来存储用户属性信息。

当数据都导入了以后,我们就需要一种系统,具有从HBase和MySQL这种异构存储之上进行查询计算的能力。基于原始数据的聚合计算是一个相对来说比较耗资源的过程,在去年,我们主要使用MapReduce进行这个计算过程,当计算完成后,把结果保存起来,用于展示。这样做的限制在于,系统所展示的数据只能是预先算好的;用户无法动态地对报告进行修改。基于这个问题,我们很早就开始关注交互式查询技术的进展。今年,分布式环境下的SQL查询系统成为一个发展中的热点技术,有若干个这样的系统陆续浮出水面。目前来看,Drill属于发展较晚的一个项目,但其开放、可扩展的架构能很好的满足我们的需求。所以,我们围绕Drill,构建了我们的SQL查询系统。这样,任何能通过SQL表达的数据报告,都可以通过我们的系统来实现了。现在,用户可以通过界面构建任何他想要看到的报告,然后就能在比较短的时间内得到他想要的结果。

上面提到,聚合计算是一个耗资源的过程,即使是现在我们采用了基于drill的查询系统缩短了响应时间,计算后的结果也需要保存下来,以应对用户的查询。我们使用Redis来保存我们的计算结果,主要考虑Redis优秀的随机查询性能。

作为系统的展示层,我们使用了RoR+backbone+coffeescript的组合。这个层面选择的自由度就比较大了,coffeescript应该说是作为python爱好者的首选,用来配合backbone构建富客户端程序;RoR用来完成各种界面操作的Rest API以支持客户端的运行。总体感觉,这个组合还不错,开发起来也快,同时界面表现也不错。

相关调优

理论上来说,性能调优是一件永无止境的工作。在大数据的背景下,似乎很多问题都可以通过加机器来解决,调优的重要性看起来更低了。但事实上,调优是我们的重要工作之一,原因有以下几点:首先,作为小公司,资源毕竟有限,在集群环境下,调优的成果也具有集群效应;其次,由于我们的系统基于交互式查询技术,那么系统各个环节性能的提升无论对于数据响应的速度还是数据生效的实时性,都会有直接的影响。

所以,我们在系统的各个环节、各个层面都进行了调优。如果用一句废话来解释的话,数据处理系统的特点是(顾名思义)需要处理的数据有很多,所以调优的核心目标只有一个:在尽量短的时间内,使用尽量少的资源(内存,I/O访问,CPU),处理尽量多的数据。

下面,我们按照模块来做划分,简单介绍一下我们在优化方面所作的努力,囊括数据导入、存储、查询、缓存和前端多个部分。

数据导入部分——主要的优化点在于java程序的优化和对于数据库访问的优化(by刘熊)

关于Thrift连接池的优化

在前文中提到,我们在数据导入的时候需要判断一个用户是否为新出现的用户;同时,我们还需要对字符串类型的ID做一定的映射,转化为整型的内部表示。我们开发了专门的服务(称之为ID Service)来处理这种ID之间的转换和新ID的识别工作。出于性能和开发效率的考虑,我们采用Apache的Thrift RPC框架来快速搭建ID Service。不过,Thrift框架的Java客户端本身存在一个不足,就是不支持连接池。为了减少连接创建销毁的开销,我们自己实现了客户端连接池。

关于内存容器类优化

ID Service的后端存储用的是MySQL。为了减轻对MySQL服务器的压力和提升获取内部整型ID的速度,我们采用本地内存来缓存用户原始字符串ID和内部整型ID的对应关系。用户的原始字符串ID,哈希成整数后作为关键字来查找对应的内部整型ID。显然,一个整型到整型的HashMap能保存这种对应关系。考虑到JDK中HashMap的实现相对来说较占内存,我们使用HPPC中的Primitive Collections来替代。采用这种内存缓存后,经统计,缓存命中率能达到99%. 如果对HPPC中OpenHashMap的操作只用到get/put/remove,那么在每次重新哈希时不用打乱关键字顺序,减少重新哈希的计算时间。

关于MySQL Connector/J的优化

每天,我们需要从MySQL中查询各个项目两个月以来的活跃用户。对于某些项目,活跃用户数上千万。默认情况下,Connector/J一次查询会先缓存满足查询条件的所有数据。如果机器内存使用紧张,而某次查询需要缓存的数据量很大,就有可能导致OOM。Connector/J支持逐行返回数据(通过enableStreamingResults来启用),这样,就能大大减轻短时间内对内存的压力。

我们使用LOAD DATA LOCAL INFILE将用户属性信息批量地导入MySQL。刚开始时,我们把待导入的数据先写到文件中,然后让LOAD DATA LOCAL INFILE语句去读这个文件。后来得知,并不需要写入文件,Connector/J支持从输入流中读取待导入的数据(通过setLocalInfileInputStream方法)。这样,批量导入数据就能从内存中读取了,避免了磁盘IO开销。经粗略估计,速度能提升10倍以上。

关于HBase客户端的优化

至于HBase批量导入数据,我们在客户端这边做的优化主要是关闭WAL、调节写缓冲区大小和手动刷新缓冲区。

存储部分——主要的优化点在于对HBase和HDFS的参数调整,以及访问方式的调整(by刘熊&王宇飞)

我们在HBase参数调优上花了一些时间,主要在如下几个方面作了调整:

  • GC:影响HBase性能的GC参数主要是堆的大小、新生代的大小和垃圾回收器类型。这方面的资料网上较多,前人的经验较多,但不存在万能的最优参数组合,实践中还是需要根据自己的系统线上的实际表现来不断尝试和优化。
  • 数据压缩:HBase支持启用数据压缩,能节省很多存储空间,但是同时会带来一定的读写性能开销。目前我们线上使用的仍然是0.94系列的版本,在0.96以及0.98版本中,HBase的存储引擎有非常大的改进,比如引入了新的prefix tree编码方式,以及更新的文件结构,等等。这些改动对于性能提升的潜力巨大,我们也十分期待。
  • 手动major compaction:由于执行major compaction对HBase的响应速度有明显影响,所以我们关闭了自动major compaction,而改在集群较空闲时执行。
  • HDFS的本地读模式可以让HBase的RegionServer进程在合适的时候不与本机的DataNode进程通讯,而直接从文件系统读取数据。我们打开了本地读模式,提升读性能,好在现在本地读模式已经默认启用了。
  • 启用bloom filter,提升读性能。
  • 调节scan cache大小,提升scan性能。

查询部分——主要的优化点在于如何提高查询的IO性能,以及优化查询过程本身(by王宇飞)

Ad-hoc查询机制 

Direct Scanner

对hbase的数据扫描的方式我们做了一些改变,没有使用传统的方式通过hbase原生提供的client去获取数据,而是设计了一个direct scanner的组件来替代hbase client。

采用hbase client的方式会通过region server去扫描hdfs存放的数据文件(hfile),之后和region server本身内存中(memstore)数据做合并,按照row key的顺序按批次传给client。如果数据量小没有明显性能问题,比如扫描一两百万行,但通常我们场景会实时扫描上千万行的数据,这个时候就有明显的性能问题了。主要是hdfs上的数据多经过了一层region server再传给client,多增加了一次序列化和网络传输的开销。

Direct scanner的设计就是为了优化这个问题,我们在client的进程中直接扫描hbase在hdfs上的数据,这个数据占大部分。只从region server中获取少部分存在于内存中的数据。最后在direct scanner中控制两部分数据的合并和更新覆盖,版本控制等逻辑。

对两种方式的扫描我们做了性能测试对比,发现数据量越大direct scanner的优势越明显。当扫描3000w行数据时,采用hbase client的方式要用到60s左右,而direct scanner只用29s。

采样

由于我们本身提供的是一个实时查询的服务,这就要求从查询处理到结果返回给用户的时间要尽可能地短,过长时间的等待对用户的体验会大打折扣。但很多时候用户所需要的统计结果本身涉及到查询的数据量就很大,全部扫描的话光是磁盘io的耗费时间用户可能就不能接受。例如,之前提到扫描3000w行hbase数据direct scanner扫描就需要29s左右,这里还没有算上物理计划的执行时间。在实际查询中,一个统计指标所涉及到的扫描量可能远不止3000w行。

针对这个问题,我们的出发点是想能不能扫描尽量少的数据就能得到相对准确的结果。如果可以,会大大减少不必要的磁盘io和cpu处理。通过对游戏玩家的行为分析,我们发现大部分事件发生次数和玩家本身有一个比较一致的分布。所以我们通过对玩家的抽样扫描,来预估最终的统计结果。抽样所涉及到的玩家越多,结果也就越准确。所以在实际的查询中,我们采取逐步扫描的方式,每轮都是对一部分玩家采样,直到结果满足误差可以控制在一定范围内为止。

给一个实际的测试结果可能更形象一些,下面是我们扫描某个项目一天发生visit事件的人数和事件次数结果,分别用采样和不采样的方式:

不采样: 人数:3187632 次数:82686362 耗时:12.53s

采样(只扫描1/256的用户): 人数:3150336 次数:82599680 耗时:0.49s

可以看到,人数的误差在1.17%,次数的误差在0.1%,而查询耗时从12.52s减少到了0.49s。

合并与物理计划执行方式的改进

logical plan的合并可以说是根据我们的查询业务场景做的一个比较有意思的创新。由于我们前后台交互是异步请求模式,前台提交的查询请求转化为logical plan在后台的队列里排队等待处理。而这些排队的logical plan如果一个一个的执行可能会涉及到大量对磁盘的重复扫描和计算。

于是我们想有没有方法一次性批量的去执行这些logical plan,把相同的io扫描和计算合并掉。基于这个目的,我们采取的方案是对一系列的logical plan做合并,把涉及到重复磁盘扫描的操作符合并到一起。这样多个不同的logical plan经过合并后形成的大logical plan极大地增强了我们系统的吞吐量。

在有了合并后的logical plan后,我们对drill原生的物理执行框架又做了进一步改进。Drill本身的物理执行方式是单线程从DAG的root节点逐步向child节点pull数据,这样对于大plan的执行就利用不到服务器多核的优势,处理起来就很慢。所以我们把每个physical operator节点改造成独立的一个线程worker,它本身处理完后就把结果push给自身的parent节点。每个physical operator节点有一个数据队列来接收child节点所传递过来的数据。通过这种多线程push的方式,充分利用了cpu的多核能力,加快了大plan的处理速度。

缓存部分——主要的优化点是如何考虑缓存的粒度,以提高内存利用率(by武子竞)

对缓存结果的读取和存放主要为Get/Set操作,在低延迟、允许少量数据丢失的情况下,架构上,结果缓存采用Redis shard的集群方式,这样可以保证每个Redis实例占用较小的系统资源。同时对Redis的save进行配置,可以灵活掌控数据保存至硬盘的策略。Redis的硬盘存储文件dump.rdb的结构对外开放,也使得扩展集群规模,迁移数据变得相对简便,另外使用Redis作为结果缓存,而不用memcached的另一个重要原因是Redis提供了丰富的数据结构,如Hash,这对缩减整个结果集的Keys的数量也至关重要。

由于每一个查询结果都有对应的key,因此如果用户定义的查询粒度非常细小,就会导致key的数量暴增,因此在缓存的键的设计上,我们选择了折中的粒度,如果是更细粒度的结果,则存储到Hash结构中,这样确保了在时间上可接受的前提下,占用内存比String更小。使用hash的另一个好处是,redis引入了zipmap(SmallHash),可以比正常的HashMap节省一些元数据的存储开销,在我们的hash中,entry的数量不大而且相对固定,因此调整Redis的hash-max-zipmap-entries和hash-max-zipmap-value配置可以找到性能和空间的平衡点。

前端——主要是如何使用各种工具来达到更好的交互体验(by王长历)

采用web app方式设计:用ror做后台的rest服务,页面交互用了backbone.js这个前端MVC框架,除了报表数据的更新,其他的交互都在浏览器端完成。另外,我们用coffeescript来替代javascript,css方面采用bootstrap框架,部署工具用的是capistrano。我们也用到了一些很不错的ruby gem,如利用resque用来做异步调用服务,用private_pub做消息订阅服务(及时把系统通知推到前端)等等。

相关工具

工具的进步使得小团队的配合变得更加容易。我们的代码均托管在GitHub( @XingCloud)上,同时使用Basecamp来记录项目中的备忘资源和协调任务。自动化运维方面,开源社区也有非常多的选择,比如我们使用Ansible进行配置管理,使用Fabric来做集群的自动化升级。

关于未来

数据处理问题三个比较主要的技术环节是:如何整理数据、如何存储数据和如何查询数据。在这三个方面,开源社区目前都处于比较快的进步当中:数据整理方面,各种流式处理框架越来越成熟;存储方面,新的存储格式的探索还在不断进行,老的存储系统比如HBase也有很多新的特性在规划当中;查询方面,包括Drill的各种查询引擎也在快速发展之中。

针对业务场景我们做了大量的优化工作,原因在于目前还没有一个特别适合且易用的开源数据仓库方案,能够让我们很方便的“开箱即用”。有理由相信,随着开源技术的不断进步,数据处理的门槛将会越来越低,更多的人和组织有能力处理更多的数据;同时新的问题和新的解决方案也会不断出现。作为一名技术人员,能够投身于这一技术潮流中,是一件令人激动的事情。

穆黎森,现智明星通CTO。曾就职于搜狗,后参加多次创业公司。关注服务器性能优化,关注分布式存储技术,关注分布式查询技术。 

Got master's degree in CS from Tsinghua Univ. Served as engineer in sogou.com after graduation. Participated in multiple startup projects, and now entitled as CTO at elex-tech.com, an international game developing and publishing company. Mainly interested in distributed computation and distributed storage system.(责编/仲浩)

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值