前言

6月10-11日,2017年SDCC峰会在深圳举行。为期两天的会议邀请业内顶尖的架构师和数据技术专家分享干货实料。来自腾讯TEG架构平台部的Jerome以及数据平台部的Boyce作为演讲嘉宾,分别发表主题为“基于空闲资源的弹性计算实践”以及“StreamSQL实时计算平台的挑战及解决方案”的演讲。本文为演讲者现场PPT及演讲稿整理编辑。

大会介绍

SDCC 2017·深圳站,拥有互联网应用架构实战峰会大数据技术实战峰会两大峰会,秉承干货实料的内容原则,邀请业内顶尖的架构师和数据技术专家,共话高可用/高并发/高性能的系统架构设计、分布式缓存服务、Web App前端架构、消息引擎架构、弹性计算、大数据平台构建、优化提升大数据平台的各项性能、Spark部署实践、企业流平台实践,以及实现应用大数据支持业务创新发展等核心话题,旨在通过来自国内一线互联网公司的实践案例,为开发者提供一个最有价值的高效技术交流平台。

演讲主题:基于空闲资源的弹性计算实践

演讲嘉宾:TEG架构平台部 Jerome

微信,QQ,空间等用户每天上传了海量图片及视频,图片上传下载时需压缩,视频播放前需转码;AI热潮兴起后,围棋,游戏等对弈数据的生成需要大量的计算能力;计算成本逐步成为不可承受之重。同时由于公司业务的多样化,难以均衡用满各类资源;现网服务器主要承载在线业务,有明显的波峰波谷效应;同时设备购买,裁撤,流转形成了大量的短期空闲设备,公司整体资源利用并不充分,故架平虚拟化团队建设了弹性计算平台,致力于挖掘复用现网的空闲资源,以满足当前对海量计算能力的需求。

但是,复用现网的空闲资源主要存在2个挑战:

首先,容易影响现网在线业务的服务质量。计算业务可能与在线业务共享cpu执行单元,L1,L2,L3 cache,内存,磁盘,网络等,哪一个环节没控制好,都会影响在线业务的服务质量,比如仅仅L3 cache的冲突,便可能使得计算性能下降60%以上。

其次, 挖掘出来的弹性资源本身难以用好,如上图所示,弹性资源非常多样,资源规格不一,可用端口不同等,且资源非常易变,比如资源份额(quota)随在线业务负载变化可能动态调整,影响了在线业务时会立即被清除,业务用好这些资源很难。

为解决上述挑战,我们设计了弹性计算技术架构下图所示,其中:

接入层:负责提供服务化接口,包括服务访问,服务配置,镜像管理等;调度层:通过名字服务屏蔽多样易变资源,实现负载均衡,扩缩容调度,故障调度,错峰调度,灰度变更等能力;节点层:实现资源隔离,冲突检测,容器管理监控等机制,供上层使用。

下面针对关键挑战详细描述技术解决方案。

为保障在线业务的容量,首先要做好业务间合理混搭,如下图所示,消耗CPU资源多,但网络带宽少的,尽量混搭到消耗网络带宽多但CPU空闲的,实现混搭关键点在于提炼合理的性能模型,因为现网业务资源需求差异大,服务器硬件资源规格也不统一,性能模型要能抽象这种差异,用最简单的公式表达出性能特点,弹性计算平台首先通过cpu相对模型来识别是否适合混搭,比如万兆服务器每核配比带宽73M/s,A业务1核跑满消耗100M/s,B业务1核跑满消耗40M/s,那A与B适合1:1混搭;在此基础上再通过规格模型来隔离计算业务,比如用C4-8-100,表示分配给容器业务4核CPU,8GB内存,100GB磁盘。分核心时会综合考虑是否通过超线程共享物理核,设置合理的软硬配额,由于当前内核协议栈对网络及磁盘IO的隔离并不理想,我们主要是通过业务性能模型匹配解决网络及磁盘冲突的问题。

在线业务有明显的波峰波谷效应,如下图所示,在波谷时有更多的资源容量可让出供给给计算使用,所以我们也广泛应用了错峰调度,在线业务波谷时,比如0点到6点,让出更多的资源给离线计算型业务使用,比如围棋AI的对局演绎。

解决在线业务资源容量保障后,业务计算延时的影响主要来源于指令执行延时:

  1. 计算型指令由硬件执行,时间基本固定(温度影响带来的差异可忽略不计);

  2. 控制型(主要跳转等)指令主要由业务逻辑本身决定,业务逻辑没优化好时频繁分支预测失败可能造成延时增加;

  3. 访存型指令:这块由于cpu cache结构,可能造成延时的波动,如下表所示,一级cache访问延时与内存相差了40倍以上,共享时主要影响的是访存指令的延时。

为监控业务计算延时,我们引入了CPI(cycles-per-instruction)监控,简单来说,CPI = CPU周期数/CPU指令数,CPI放映了业务整体的指令执行延时,如下图所示,通过历史数据可提炼合理的CPI标准值及方差值,监控当前实际值,对比模型来判断业务计算延时是否正常。这个值与业务指令模型(主要是访存指令的比例等)及CPU型号相关,故需区分业务模块及CPU型号建模。

CPI延时变化时,反应了真实的指令执行延时增加,但指令延时的增加不一定对业务有实际影响,所以监控的关键点在于怎么确认实际业务影响,解决误报的问题,我们主要是通过三种办法来解决:

  1. 对接业务延时监控,但业务延时监控精度可能不一致,且可能无便捷的数据访问接口,故这种方案只能在重点业务上采用;

  2. 增加确认值,比如监控cpu cache miss的次数及计算型业务的cpu利用率,利用这些值来反推计算业务是否影响了在线业务质量;

  3. 消除监控噪点,比如连续3个点以上延时增加才真正告警处理。

检测到CPI异常时,我们会先通过本地动态调整计算业务CPU配额减轻影响,效果不明显时,才会将计算业务调度至其它服务器,先本地调度有利于于避免瞬时的计算毛刺造成频繁的分布式调度。

前面通过资源隔离解决了在线业务计算容量保障问题,通过CPI监控及调度解决了计算质量保障的问题,剩下的便是在线业务调度延时保障了,如下图所示。由于Linux是非抢占内核,默认情况下在线业务获取时间片的时间是无保障的,混搭上计算型业务后,调度延时更不可控,为解决这个问题,我们在内核层面实现了业务优先级调度。

我们通过设置不同docker容器cpu.share的值,将业务优先级传导至内核,指导内核调度,比如默认1024代表普通优先级,4096代表高优先级,3代表低优先级,配置了4096的容器,可抢占配置为3的容器的时间片。应用优先级调度后,在线业务的毛刺(这里为平均延时10倍)点,对比优化前缩减了32.6%,基本与不共享时持平。

CPU,网络IO及磁盘IO是可压缩资源,共享时可能使得在线业务延时增加,内存是不可压缩资源,如果冲突,可能导致在线业务或关键系统进程被OOM,而导致业务异常;为解决这个问题,平台与内核结合一起研发了OOM优先级调度,如下图所示:

平台主要解决可预测,持续性的OOM调度,比如平台收到内存不足预警通知时,如果近期内存一直持续上升,会立即调走计算型业务;内核主要解决不可预测,突发性的OOM调度,比如计算型业务或在线业务收到异常业务请求,导致内存突然暴涨时,Kill掉低优先级的计算型业务容器,并给平台发送事件通知,平台做清理善后。

由于弹性资源的多样易变,短期难以成为通用的计算平台,故结合实际需求选择了图片压缩,视频转码,AI计算,日志计算4个场景,该4个场景总结起来的共同点是单核流量小于10M/s,且业务平台能容忍节点的变动或失效,如下图所示。

对于无计算状态的业务,比如图片压缩,弹性计算平台提供服务化接口,接管计算节点的扩缩容,对于有状态的计算,比如视频转码切片,AI计算中间数据缓存,日志计算map/reduce模型等,则提供API接口,让业务自行发起扩缩容等调度。弹性资源由于其特殊性难以保障所有接入业务的计算质量,故我们区分优先级,提供差异化服务,优先保障在线计算型服务质量。

现网弹性资源的多样及易变主要来源于3点:

  1. 可弹性资源规格不一样,比如有些服务器可复用2核,有的可复用4核;

  2. 硬件性能有差异,如下表所示,最好的cpu与最差的cpu性能可差距一倍;

  3. 计算业务容器可用配额(quota)会随着在线业务负载变化,甚至被销毁。

我们采用了CL5名字服务来屏蔽弹性资源的多样易变性,如下图所示,用户只看到服务名字,无须关心背后绑定的资源及权重动态变化,扩缩容,冲突调度,错峰调度,灰度调度时修改资源的绑定,权重调度修改资源的权重,如下表所示:

权重的设置如下表所示,综合考虑资源规格(核心数),资源能力(性能基准值),可用配额(quota)等设置总权重,并记录单核权重作为负载均衡参考。

使用好弹性资源,仍然需要业务了解弹性资源本身,并做适配处理,比如和弹性计算平台API集成,协调可用端口等,使用门槛依然较高,为解决这个问题,我们提供了云函数使用接口,如下图所示,类比S3存储,数据以文件为载体,用户上传下载数据无须关心S3分布,容灾,扩容等;计算以函数为载体,用户提交函数后无须了解函数执行背后的资源调度,容灾,扩缩容等,可更专注于业务逻辑创新。

在建设弹性计算平台实践过程中,我们有一些经验教训,在这里和大家分享下。

故事1:A业务利用率扩缩容阈值设置不合理,高峰期保留大量资源没充分利用;B业务设置很合理,高峰期确没资源扩容了。---让用户自身做策略,难以达到整体最优,造成业务间资源利用不均衡,老实人反而容易吃亏;故事2:平台默认打开自动扩缩容,自动调配资源;A业务对自动扩缩容机制不知情,发起了版本变更,造成现网多版本共存。---平台来做策略,难以了解完整业务流程,可能影响业务的可用性。

在提供机制或策略的选择上我们有过反复,对于平台方,最理想的是将策略控制起来,以达到整体资源调度的最优化,但这里需要一个前提,平台能够收拢所有业务变更入口,如果做不到闭环,只能优先保障业务可用性,平台提供机制,由业务方自身实现策略,平台方通过其它手段,比如定期公布低负载业务,推动业务方主动关注资源高效利用。

扩缩容的目标在于将计算型业务维持在合理的负载,以实现质量和成本的均衡,但如果业务负载不均衡,扩缩容难以达到预期的效果,如下图所示:

当业务不均时,同计算业务下不同实例表现为个别实例负载高,以图片压缩为例,出现此场景的一般由于收到大图,此时扩容不能缓解高负载,反而容易导致更多实例空闲;当资源性能不均时,同计算业务下不同实例表现为部分实例负载高,出现的原因可能由于CPU性能或在线业务负载差异,此时如果以整体平均负载扩容容易导致部分实例高负载,以前50%实例平均负载扩容,容易导致另一部分实例低负载。

最完美的状态是同计算业务下各实例负载波动上下波动不超过5%,此时在扩缩容调度下,整体能保持在较高的负载而不影响服务质量,但这个需要业务和平台方共同努力才能实现:

对业务方来说,要实现用户请求轻重分离,发送给同业务模块的请求,后台消耗的资源需基本一致,以图片压缩为例,需要业务方做好大小图分离;对平台方来说,需要更综合更动态的权重,为计算实例设置权重时,需综合考虑资源规格,硬件性能基准值,动态可用配额等多方面因素。

弹性计算依赖底层提供资源隔离,优先级调度等机制,底层的稳定性会影响整个平台的稳定性,且修复代价很大,在弹性计算早期,如下图所示,为了避免平台建设打扰到正常业务运营,规避机房间穿越流量,在容器存储选型上,我们选择了loopback+devicemapper方案,因为此方案无须格式化磁盘。使用之前已了解到社区对此方案的应用存在很多问题,所以打齐了补丁,并采用灰度部署的方式以及时发现并解决问题,在规模不大时,问题并不突出,但上1w台规模,经常现网出现dm设备ioutil 100%, cpu100%等问题,有时需重启才能修复,影响了在线业务的体验。由于loopback+dm机制实现比较复杂,我们对短期内完全修复信心不足,不得已花大代价重新格式化,切换至实现上更简单的XFS+Overlay方案。

从这个例子,我们得到的经验是,底层技术要选择最简单,主流,被大家广为认可的,而不是存侥幸心理选择在当前条件下最容易实现的方案。另外由于底层故障修复代价过大,在规模上线前,最好配备热补丁修复能力,以在底层故障出现时低成本的修复问题。

感谢大家参加腾讯弹性计算的分享,希望通过此分享能抛砖引玉,能引发一些大家对资源高效利用的思考和实践,当前整个行业大概6%~12%的CPU平均利用率,有较大的提升空间,怎么去提升利用效率,减少对能源的浪费,应该是我们每一位IT从业人员的职责,欢迎大家一起探讨,今后一起提升做到更好。

演讲主题:StreamSql实时计算平台的挑战及解决方案

演讲嘉宾:TEG数据平台部 Boyce

大家好,我叫周建军,我的分享的主题是StreamSql实时计算平台的挑战及解决方案。本次分享主要分为三个部分,第一部分是介绍一下实时计算的背景及传统实时计算任务开发存在的问题。第二块介绍一下我们StreamSQL的平台这块是介绍我们的平台是做什么的及怎么做的。第三块介绍一下StreamSQL的平台设计过程中遇到的几个挑战及我们的解决方案。

Ok,首先让我们先看一下实时计算的背景。我相信实时计算的概念大家应该都比较了解,为什么需求实时计算,主要是因为在一些特殊应用场景下数据是有时效性的,数据的价值随着时间的推移在逐步消失,所有需要实时计算来对这些数据进行快速的分析处理。下面有一个典型的实时计算应用案例:

上面这张图是一个游戏玩家的历史战绩,如果玩过这块游戏的朋友可能比较清楚:当玩家在打排位赛时,如果连续失败三次以上,系统的匹配时间就会稍微延长。这是为什么呢?

因为对于我们游戏产品而言为了避免用户的流失,系统需要一个实时任务来实时感知玩家的感受,然后为其提供个性化的运营策略。比如对于连续失败、受挫的用户系统需要为他们实时调整匹配策略或降低操作难度。这就需要有一个任务来实时分析游戏产品实时上报各个玩家的数据。Ok,有了这么一个实时计算任务的需求,我们如何去开发一个实时计算任务呢?

当前实时计算任务开发模式可能有两种:

第一种开发模式是数据使用者(像数据分析师或产品运营人员)自己开发实时计算任务,这就要求他们不仅要了解业务逻辑,还需要熟悉复杂的实时计算框架和编程语言比如storm,在实时任务运行过程中还需要关注各种运行指标。对应这种开发模式可能会存在以下问题,第一问题是对于数据使用人员来说学习门槛比较高;第二个问题是指标格式不统一,因为不同的开发者会根据自己的个人爱好方式输出指标格式,这样在指标采集后还需要做一次指标格式的标准化才能做为后续统一的指标分析。第三个是资源使用不可控,不同层次的开发者通过编程自己分配系统资源很容易引入很多不稳定因素,比如异常日志么有收敛造磁盘IO过高的问题。磁盘IO过高不但会导致自己任务异常还会影响其他的任务。另外一点如果内存使用不合理导致内存溢出或泄露也会导致实时任务无法正常运行。

第二种是数据使用者将业务逻辑告知后台开发者,由后台开发者理解业务逻辑后再统一的开发出实时计算任务,这种开发模式存在一定沟通成本,后台人员可能对业务逻辑存在理解偏差导致开发的实时任务不能满足需求。以上的种种问题都会影响实时计算任务的开发周期,给数据使用带来种种烦恼。

我们都知道SQL是一种标准化的数据分析语言,不管是数据分析师还是产品运营人员对SQL使用都没有障碍。为了减少用户的烦恼,我们推出了StreamSQL实时计算平台。下面我来介绍一下我们的平台是做什么的,以及是怎么做的。

StreamSQL实时计算平台就是提供以SQL描述用户的实时计算逻辑,然后将SQL转换成Storm上的一个实时任务。在计算过程中StreamSQL任务实时的从tdbank消费数据(这里的tdbank是腾讯的实时数据接入系统,同时承载了消息中间件的功能),通过计算之后将结果分发到第三方存储系统。StreamSQL平台目前在腾讯内部的运营情况,每天的计算规模都是万亿级别,支持秒级的数据延迟。每天消费的数据在4万亿条以上,计算总数是在2.4万亿条,每天参与计算的总数据量有700TB以上,接入的业务覆盖到微信、qq音乐、腾讯游戏、手机管家、腾讯云等。StreamSQL从2013年项目启动,发展至今快尽4个年头,目前已经成为腾讯内部比较成熟实时计算。

接下来我将从StreamSQL平台的易用性、稳定性和功能特性三个方面做一下讲解。

在平台易用性方面:

第一,StreamSQL 是类SQL逻辑描述。我们的平台提供通过配置SQL脚本就能完成实时统计需求,让用户只专注于业务逻辑的开发实现,无需关心复杂的计算框架和计算节点之间的数据交互,大大提高了业务需求的开发效率。在SQL语法层面,StreamSQL是基于Hive SQL语法扩展,在hive SQL的基础上加入实时计算特性的语法结构改造形成的,整个语法结构和hive非常类似。具有hive基础的数据分析人员可以轻松上手,学习门槛极低。

第二,StreamSQL自带丰富的指标类型和统一的指标格式,当任务出现问题是系统能快速的告警及定位。通常情况下一个实时计算任务会运行在不同的物理节点,每个物理节点中都有各自复杂的计算逻辑,当任务出现异常时系统需要做出快速告警和问题定位。StreamSQL系统对实时任务全链路中的每个节点都提供了丰富指标采集。从数据源输入到各个计算算子再到计算结果输出,整个计算链路的每个模块都采集了计算的数据量大小、数据条数、计算平均耗时、最大耗时等指标数据。当任务出现异常时系统能快速的发现和定位。

在稳定性方面我们会经常听到某个任务前几天一直很稳定怎么就突然挂了呢?实时任务能长时间稳定运行时业务方的一个非常朴素的需求。我们总结出影响系统稳定性的因素主要由两个:

第一个是资源不足,当某个数据洪峰到来之时系统没有足够资源可用,这样极有可能出现内存溢、计算任务崩溃的问题。StreamSQL结合Storm failed机制增加过载保护策略从而防止数据洪峰对计算任务的冲击,同时通过指标监控触发告警机制以便于用户对该任务的资源进行扩容;

第二个是外部环境导致计算任务的不稳定,由于StreamSQL的计算任务会与很多第三方存储系统通信,如果某个存储系统异常系统会触发重试丢弃策略以保障任务的其他计算链路不受影响。比如实时任务将不同的计算结果分别分发到hbase和mysql,如果结果写入mysql异常则重发重试,重试失败则丢弃计算结果。整个计算链路的其他计算分支不受影响。

在平台的功能方面,StreamSQL在数据类型层面除了支持简单的数字、日期、字符串类型外还支持复杂的Map 、Array、和Json这些复杂的数据类型,让用户开发实时计算任务是有更加广阔的选择空间。

在函数层面StreamSQL集成hive的绝大多数函数,除了通用字符串函数、数值函数、日期处理函数、聚合统计函数之外,我们还实现了一些内建函数,比如集合的合并函数、集合的转换函数等。

传统的SQL无法表达实时数据统计的语义,为了支持实时数据统计的特性StreamSQL在传统SQL的基础上做了一些语意的扩展以丰富StreamSQL表达能力。

下面重点看一下我们的StreamSQL是如何扩展SQL表达能力的。我们知道传统的SQL在做统计计算时处理的数据对象是有边界的,也就是说在任务提交时要处理那些数据是明确的,而StreamSQL处理的数据对象是没有边界的流数据,这就为聚合统计带来困难。为了能对流数据做聚合统计我们抽象出窗口的概念,将流数据按照时间维度划分到不同的窗口,同一个窗口的数据集就可以做聚合统计了。

目前StreamSQL平台支持三种类型的窗口:

第一种普通聚合窗口,该窗口是将流数据按照时间维度划分,对于同一个时间段范围内的数据进行聚合统计,等待时间到达窗口的右边界就输出计算结果。

第二种累加窗口,累加窗口是对几个聚合窗口的数据进行连续累计统计,算的数据是针对从累加窗口起始直到当前聚合窗口的聚合值,每个聚合窗口时间结束就输出一次累加统计结果,等待到达累加窗口的时间边界再重新统计;

第三种滑动窗口是对最近的几个聚合窗口的数据进行统计。聚合窗口和滑动窗口都必须以普通窗口为基础,其大小为普通聚合窗口的整数倍。举个例子: 以下需要做三个窗口统计,第一每10分钟进行一次数据统计不同app应用通过qq 的登录人数,第二每10分钟统计一次当前这1小时累计登录人数,第三每10分钟统计一次最近连续30分钟的登录人数。我们的SQL中使用COORDINATE BY指定依据某个时间字段来划分时间窗口,使用WITH AGGR INTERVAL指定普通聚合窗口的大小,这里是10分钟,对应的统计函数是count(qq);累加窗口是使用WITH ACCU INTERVAL来指定窗口大小,这里的累加窗口大小是1个小时,对应的统计函数是count(qq ACCU);滑动窗口是使用WITH SW INTERVAL 指定,滑动窗口的大小是30分钟,对应的统计函数是count(qq SW) ;从聚合窗口和滑动窗口的粒度上可以看出它们都是普通窗口的整数倍。

另外一个对SQL表达能力的扩展是通过表达式实现的。StreamSQL新增for each表达式用于对map\array这种复杂的数据类型遍历处理。

有时候用户想需要某种计算逻辑,但是SQL中没有对应计算函数该怎么办?为了解决这种问题我们推出了Execute表达式,Execute表达式支持用户在表达式内部自定义变量,然后通过代码片段来实现自定义逻辑,然后通过emit输出结果,整个过程相当于用于通过SQL脚本实现自定义函数。

讲了这么多StreamSQL系统的优点,下面重点来看下我们的平台架构,从架构图上可以对StreamSQL平台有一个整体的了解。这个架构图有两种不同颜色的箭头,绿色的箭头代表从指令下发路径,黄色的箭头代表数据流向路径。

首先从指令下发的维度看整个平台分为三个部分,最上层web ui和Stream api是StreamSQL平台的接入层,它们站在系统的最前端负责实时任务的接入;中间层是一些管理模块,负责任务管理、权限管控、元数据管理和任务的测试预编译管理等;底层是StreamSQL平台的核心层,核心层包含以下几个模块:第一个是SQL解析模块,它负责SQL转换为Storm的topology,这个转换过程分为四个步骤:第一步使用antrl先将SQL转换抽象语法,这一步主要是做SQL语法检查,检查SQL语法是否满足antlr语法文件的定义;第二步是将抽象语法树结合配置文件转换为逻辑执行计划,在此过程中会做一些配置的校验,比如校验SQL中涉及的表或字段是否存在,类型是否正确等;第三步是将逻辑执行计划转换为物理执行计划,这一过程是将不同的计算逻辑划分到storm的不同组件上;第四步是根据配置的worker个数及task个数将物理执行计划组装为拓扑。opterator算子模块是系统抽象出算子的具体实现。SQL的解析过程和算子的实现都比较抽象,后面会结合具体实例进一步讲解。UDF模块负责系统内建函数的注册和实现。数据的输入输出模块主要负责系统支持从哪里消费数据以及将计算结果存储到哪些地方,该模块是插件化的管理模式,当系统需要新增某种存储类型时只需添加相应的插件模块即可。配置信息管理和监控指标上报模块是负责任务配置信息及监控指标的管理。

从数据流向维度看StreamSQL平台是源源不断地从数据源中消费数据,经过计算后将结果分发各种存储系统。传统SQL任务式运行是有生命周期的,从任务的提交到计算结果返回,整个任务就结束了;StreamSQL是一个个持续运行的任务,除非人为终止,否则不会停止。在这个过程中驱动整个计算任务不断运转的就是源源不断的流数据。

传统SQL中所有的查询统计都与表相关,表在传统的SQL中是一个非常核心的元素,StreamSQL也不例外,在StreamSQL设计过程中的一个重要设计理念就是认为一切数据皆属于表。尽管目前非结构化数据技术快速发展,nosql理念被越来越多的人所倡导,但是我们仍然认为结构化数据是数据分析和数据挖掘领域的主流,表不过是具有相同结构的一组数据的集合,这与数据的存储类型没有关系,不管其存储时消息中间件,或是kv系统,还是普通db,只要具有固定的数据结构,我们都可以把它抽象为表。

StreamSQL根据自己的实际情况也定义了四种不同类型的表。第一种是流水表,所有来自TDBank的流式数据都属于流水表,流水表的最大特点是数据在不断变化,流水表数据的计算只针对当前时间的数据进行,最多是针对截止到当前某一段时间所有数据进行的计算。虽然流水表的数据在不断变化,但是它和普通表一样拥有静态的数据结构。第二种是维表,在实际的统计中需要关联一些数据属于维表。我们在与业务沟通是发现在很多时候流水表中的信息是不完全的,在实际的统计中需要关联一些配置信息,之后再进行数据统计,这些配置信息,有时候需要修改,但是通常变化不大,所以系统将这类数据表抽象为维表。目前我们系统支持维表的存储类型有普通mysql和redis。Mysql适用于数据量较小且不会发生更新的维表比如ip与地域的映射表,对应类型为mysql的维表在任务启动时会将数据加载到内存以提高关联的效率。Redis类型维表适维表数据需要实时更新或者维表数据量比较大的场景。第三是结果表,所谓结果表是指计算结果的流向的表,理论上支持各种类型,目前支持tdbank、mysql、hbase、redis、ES等。第四种特殊的表叫着临时表,准确的说流水表并不是真正的表,它是指一段通用的计算逻辑。当后续都需要使用某个相同的计算逻辑时,这段计算逻辑就可以被抽取为临时表。临时表的好处:1)可以使得sql逻辑更加的清晰,减少文本的输入量;2)通常可以优化计算,如果一个相同的查询逻辑被后面多个计算逻辑使用的话使用临时表将这块查询逻辑抽象为临时表可以减少不必要的重复计算。

刚才介绍的各种表大家可能没有一个直观的感受,下面我就以一个春节期间微信红包热地图的实例向大家展现一下各种不同类型的表。微信红包热地图是实时展现春节期间各个省市的微信红包发放情况。当用户发红包是会将系统会将红包信息同步到消息中间件的某个topic,这个topic的数据就抽象为流水表;将流水表的数据通过ip信息关联ip区域映射表然后从中选取红包发送时间、红包金额、uid、省、市等信息为临时表,因为后面按照省市的不同维度进行统计,临时表减少了重复的数据选取操作。这里的维表是ip与区域映射表。结果表就比较简单了,就是计算结果要写入的表,按照省聚合的结果写入一个结果表,按照市聚合的结果写入到另外一个结果表。下游应用按照省市展现数据就从不同的结果表读取数据即可。

至此StreamSQL平台的设计已经基本完成,下面看一下StreamSQL平台开发过程中遇到的几个挑战已经我们对应解决方案。

在实际的实时计算任务中,一个任务会有多条SQL组成,每条SQL又有各种复杂的计算逻辑,那我们系统是如何将这一批复杂的SQL转化为Tstorm上的一个实时任务的呢?首先我们看一下一个实时统计的例子:每分钟输出一次御龙在天和英雄联盟这两款游戏通过qq的登录人数。对于这么一个实时需求对应的SQL描述如下:首先分别从御龙在天和英雄联盟这两款游戏流水表中过滤出状态为登录的数据,抽象出两张临时表,然后将两个临时表的选取结果聚合到utbl表,最后我们按照游戏名称进行聚合统计,窗口大小指定为1分钟。

目前StreamSQL平台根据SQL语法抽象出七种类型的算子。TS输入算子,负责从始化流水表和维表的数据源中拉去数据;SEL选择算子,负责从数据集中选选择需要字段的数据;JOIN关联算子,用于计算过程中按条件关联维表数据;UNION合并算子,用于相同结构数据集的合并;FIL过滤算子用于对数据集按照条件过滤;GBY(聚合,包括MGBY和RGBY两种),用于实现分组聚合操作;FS输出算子,负责将计算结果输出到对应的结果表。

当SQL转换为算子树之后,那这些算子如何划分到Tstorm的不同组件运行呢?这里的划分依据很简单,我们系统会依据GBY算子将算子树进行切分,将MGBY及其之前的算子划分到spout节点,将RGBY及其之后的算子划分到Bolt节点,这样就形成了任务树。在做聚合统计时可以先在spout做一次聚合统计,然后将计算结果发到bolt再对前一次的统计结果做一次聚合,然后输出计算结果。

最后从任务树到执行拓扑的转换就很容易了,系统根据配置worker的个数及task个数将Tstorm的组件组装成计算拓扑。这样SQL的转化过程就结束了。

刚才例子我们提供从算子树到任务树划分的重要节点是GBY算子,那么如果我们的计算逻辑中没有GBY算子又该如何划分呢?在从算子树到任务树的划分过程中StreamSQL有一个划分原则就是尽量减少拓扑的长度。我们先看一个计算拓扑的示例,这个例子是数据通过spout分发,然后经过两个计算逻辑之后将计算结果写到外围存储系统。这里面有大量的中间计算结果需要在不同组件中传输,而这些不同组件可能分布在不同的物理节点。StreamSQL的划分原则是如果计算逻辑能在一个组件中完成就不会拆分到两个组件。如果SQL计算逻辑中没有聚合操作则将所有的算子都放到spout组件中,如果SQL有聚合计算则依据GBY算子切分。

我们这样划分有什么好处呢?

1.实现起来简单,不用考虑各种复杂的算子划分策略。

2.可以减少不必要的网络开销,因为如果计算逻辑都在一个组件中就没有中间计算结果跨物理节点传输的开销。

3.减少数据序列化与反序列化的开销,因为不统节点之间的数据交换必然存在数据的序列化与反序列化,我们的计算逻辑在一个节点内也就避免了这种不必要的开销。

通过我们的算子划分能极大的提高系统的计算性能,这里任务的并行度可以通过增加storm 拓扑task个数或者worker个数来解决。

在实时计算过程中去重统计是一个非常常见的需求,但是当实时任务面临的数据规模超大时就会对系统的存储、网络等带来挑战。接下来我们看一下StreamSQL是如何处理超大规模去重统计问题的。首先我们还是看一个例子:这个例子是实时计算不同区域每个app累加UV,每分钟输出一次统计结果。假设有100app,10个地区。拿到这个需求我们很容易想到使用集合的特性来做去重,这样我们就是有appid+areaid+uid生成key,然后将其存储到kv表,每来一条数据我们就关联KV表判断该key是否存在,如果不存在则计数加1,然后每分钟输出一次计数结果。从计数逻辑上看这个思路是没有问题的,但是考虑到每天100亿以上的访问量可能就暴露问题了。假设我们每个key占用60个字节,那么这100亿个key占用600GB的空间,所以KV表的大小肯定在600GB以上,因为还要考虑集合存储的空间膨胀问题。然后每天对kv系统的请求量也在100亿次以上,平均每秒12W次。对于这种传统方案而言无论在存储空间还是网络请求上都不能满足要求。

那么有没有一种节省存储空间的数据结构用来做去重统计呢?这里我们想到BitMap。比如我们要存储0-7这七个数字,在java中使用byte表示每一个数字,总共消耗8个字节的存储空间,使用bitmap的话只需要使用一个8位的bitmap就可以存储这8个数字了。下面看一下bitmap的结构,其实bitmap就是一个bit数组。从右至左第一个为为1代表0存在,第二位为1代表1存在,第三位为1代表3存在,以此类推,最后计算bitmap中存储1的个数就得出了bitmap存储了多少个不同的数字。

有了这个思路之后我们使用appid+areaid作为key,value为位图数组,使用位图数组存储userId,假设useId是1-1千万不同的数字,那么使用1.192MB空间的位图就能存储这1千万个不同的userId。每来一条消息我们都先在本地去重,每分钟将本地的位图数字和kv表中的位图合并,然后输出一次统计结果。使用这种方案后无论是存储空间还是网络请求数都比较之前的方案好了很多。存储空间降了到之前500分之一,网络请求数降到之前6000分之一。但是bitmap也有自己的问题,那就是当需要统计的数据比较分散时有大量的空间浪费,刚才上述的例子,我们是假设的是useId是1-1千万中间的数字,如果userId是一个随机的八位数,那么所需位图的存储空间就要上升10倍。所以使用bitmap需要过渡依赖与数据的分别情况,bitmap只适用于那些数据比较密集的应用场景。当userId不确定的情况下使用bitmap也并不合适。

在和用户沟通时我们发现用户对UV精确度的要求并不是十分苛刻,如果有少量的误差用户也可以接受。为此我们采用了基于基数统计的HyperLogLogPlus算法来做去重统计。HyperLogLogPlus算法特点是可以用非常小的空间对庞大的数据集做去重统计。还以刚才UV统计为例,KV中的以 appid+areaId 为key,以基数数组为value存储,首先使用HyperLogLogPlus算法的基数数组在本地做去重统计,每分钟和KV中的基数数组合并,并将合并后的结果更新到KV中,每分钟都可以从KV表中获取基数预估的结果也就是累加UV。

使用该方案之后我们的KV存储空间从600G降到45M,对KV的平均请求数从12W/次s降到20次/s。UV统计的精确度只有少量牺牲,大概牺牲在千分之五左右。使用此方案在key的规模增长的情况下KV存储空间基本不变。

文章来源于腾讯云开发者社区,点击查看原文