今天给大家带来大数据实时计算的架构设计,文章版权归属系统和算法研发工程师刘汉生所有。
1 实时计算简介
1.1 应用场景
谈起实时计算,一般我们都会首先去比较实时计算和离线计算的区别。
离线计算:批量获取数据、批量传输数据、周期性批量计算数据、数据展示;代表技术:Sqoop批量导入数据、HDFS批量存储数据、MapReduce批量计算数据、Hive批量计算数据、Azkaban/oozie任务调度。
实时(流式)计算:数据实时产生、数据实时传输、数据实时计算、实时展示;代表技术:Flume实时获取数据、Kafka/metaq实时数据存储、Storm/Jstorm/spark streaming实时数据计算、Redis实时结果缓存、mysql持久化存储。
一句话总结:实时计算就是将源源不断产生的数据实时收集并实时计算,尽可能快的得到计算结果。从电信运营商角度来看,实时计算主要有以下四个应用场景:
用户感知:实时预测用户感受:“哪些用户网络不畅或视频卡顿?
安全预警:实时感知网络安全态势,快速响应输出威胁情报,完成资产精准防护。
精准营销:实时感知用户兴趣变化、环境/位置变化、商家优惠策略变化,从而实现精准营销
运维监控:实时感知每台机器、每个业务的运行状态,实现秒级监控告警
1.2 实时计算的挑战与架构
面对海量实时流数据,为保证高可用与低延时,开发过程中主要面临以下几个挑战:
数据聚合:数据以日志、数据库文件等多种形式多种格式散落在各台业务服务器上,如何做到高效聚合?
数据复用:同一份数据可能承载在多份数据业务,如何实现多份业务的并发执行并保证数据的一致性?如何去承载洪峰流量?
数据计算:实现业务的实时计算,如何做到数据无丢失、高容错性?
数据存储:实时流计算会频繁读写操作数据库,选择什么样的方式,才能保证高频读写效率?
数据展现:计算汇聚后的结果怎样能高效展现,让数据说话,让运维者迅速读懂计算情况?
面临这么多棘手的问题,我们很难重头开始一步步做起,可以借助业界较为成熟的中间件,结合我们的业务场景进行定制化开发。目前较具有代表性的实时计算框架有以下几个:
Flume(数据聚合):Flume是Cloudera提供的一个高可用的,高可靠的,分布式的海量日志采集、聚合和传输的系统,解决数据聚合的问题。
Kafka(数据分发):Apache Kafka是一个开源消息系统,由Scala写成。是由Apache软件基金会开发的一个开源消息系统项目
Storm(实时计算):用来实时处理数据,特点:低延迟、高可用、分布式、可扩展、数据不丢失。提供简单容易理解的接口,便于开发。
Redis(k-v数据库):redis是一个key-value存储系统。
Echarts(图表呈现):ECharts,一个使用 JavaScript 实现的开源可视化库,可以流畅的运行在 PC 和移动设备上,兼容当前绝大部分浏览器,底层依赖矢量图形库 ZRender,提供直观,交互丰富,可高度个性化定制的数据可视化图表。
2 实时计算组件详解
2.1 flume框架
2.1.1 flume概述
Flume是一个分布式、可靠、和高可用的海量日志采集、聚合和传输的系统。
Flume可以采集文件,socket数据包等各种形式源数据,又可以将采集到的数据输出到HDFS、hbase、hive、kafka等众多外部存储系统中
一般的采集需求,通过对flume的简单配置即可实现
Flume针对特殊场景也具备良好的自定义扩展能力,因此,flume可以适用于大部分的日常数据采集场景
运行机制
Flume分布式系统中最核心的角色是agent,flume采集系统就是由一个个agent所连接起来形成每一个agent相当于一个数据传递员 ,内部有三个组件:
Source:采集源,用于跟数据源对接,以获取数据
Sink:下沉地,采集数据的传送目的,用于往下一级agent传递数据或者往最终存储系统传递数据
Channel:angent内部的数据传输通道,用于从source将数据传递到sink
2.2 消息队列与kafka
2.2.1 消息队列
以一个业务系统为例,介绍消息队列在日常开发的应用。传统做法如下图所示,在网络流量日志汇聚以后,立刻调用质差识别模块实时监测现网运行情况。
该做法在实际应用中有以下几个问题:
耦合强,采集系统与处理系统之间互相调用,模块间耦合性太强;
响应慢,需要识别系统处理完成后,再返回给客户端,即使用户并不需要立刻知道结果;
并发低,一般识别系统并发上限较小,很难应对突发尖峰流量的冲击。
请求先入消息队列,而不是由业务处理系统直接处理,做了一次缓冲,极大地减少了业务处理系统的压力;
每个子系统对于消息的处理方式可以更为灵活,可以选择收到消息时就处理,可以选择定时处理,也可以划分时间段按不同处理速度处理;
发送者和接收者间没有依赖性,发送者发送消息之后,不管有没有接收者在运行,都不会影响到发送者下次发送消息。
Kafka是一个分布式消息队列:生产者、消费者的功能。它提供了类似于JMS的特性,但是在设计实现上完全不同,此外它并不是JMS规范的实现。Kafka对消息保存时根据Topic进行归类,发送消息者称为Producer,消息接受者称为Consumer,此外kafka集群有多个kafka实例组成,每个实例(server)成为broker。
无论是kafka集群,还是producer和consumer都依赖于zookeeper集群保存一些meta信息,来保证系统可用性,其核心组件有以下几个:
Broker:(可以把Broker理解为Kafka的服务器)缓存代理,Kafka 集群中的一台或多台服务器统称为 broker。
Producer:生产者,用来向Kafka中发送数据(record)。
Consumer:消费者,用来读取Kafka中的数据(record)。
Consumer Group :消费者组,可以并行消费Topic中partition的消息。
Partition:每个topic可以有一个或多个partition(分区)。分区是在物理层面上的,不同的分区对应着不同的数据文件。
Topic:用来区分不同类型信息的主题。
注意:每个partition都是一个有序并且不可变的消息记录集合。当新的数据写入时,就被追加到partition的末尾。在每个partition中,每条消息都会被分配一个顺序的唯一标识,这个标识被称为offset,即偏移量。注意,Kafka只保证在同一个partition内部消息是有序的,在不同partition之间,并不能保证消息有序
2.3 流式计算storm
2.3.1 storm概述
Storm是一个分布式实时流式计算平台
水平扩展:通过加机器、提高并发数就提高处理能力
自动容错:自动处理进程、机器、网络异常
实时:数据不写磁盘,延迟低(毫秒级)
流式:不断有数据流入、处理、流出
开源:twitter开源,社区很活跃
2.3.2 storm编程模型
Spout:从外部获取数据,输出原始Tuple
Tuple:数据处理单元,一个Tuple由多个字段组成
Stream:持续的Tuple流
Bolt:接收Spout/Bolt输出的Tuple,处理,输出新Tuple
Topology:一个应用的spout, bolt, grouping组合
2.3.3 storm核心组件
nimbus:集群的master,负责管理supervisor、调度topology
supervisor:负责运行topology的worker
worker:负责实际的计算和网络通信
Task:worker中每一个spout/bolt的线程称为一个task. 在storm0.8之后,task不再与物理线程对应,不同spout/bolt的task可能会共享一个物理线程,该线程称为executor。
2.4 redis
2.4.1 redis组件
Redis 是一个高性能的key-value数据库。有以下三个特点:
支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
支持数据的备份,即master-slave模式的数据备份。
2.4.2 redis数据类型
字符串类型:最基本的存储类型,可以存储任何字符串,包括二进制数字、JSON对象和图片
散列类型:存储了字段和字段值的映射,适合存储对象
列表类型:存储一个有序的字符串列表,常用操作是向两端添加元素(类比双向链表)
集合类型:常用操作向集合中加入或删除元素、判断元素是否存在
有序集合类型:常用于计算topN
2.5 数据呈现
以echarts、D3为代表的开源数据可视化工具,包括折线图、雷达图、热力图、3D地图等多种成型的展示方法,而且已精简到只要用指定的json就可以调试,非常方便!
3.实践遇到的问题
3.1 redis脏读,统计结果遗漏
我们在使用redis的时候发现,实时计算一天物联网活跃终端的数目经常会小于离线计算的结果,经过不断排查,最终定位到redis读写的问题中。redis读取修改后再写入redis,希望每次访问都递增变量value的值,但在并发情况下,存在多个进程都读取到了一样的初始值,然后都加1,最后写回Redis,这种情况就会统计数据比实际的少。
经过调研,我们发现了有以下两个解决办法:
(1)基于redis的分布式锁的方法
分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。以基于Redis分布式的锁为例进行介绍。设计的分布式锁具备下面四个条件:
互斥性:在任意时刻,只有一个客户端能持有锁。
不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
具有容错性:只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
解铃还须系铃人:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解开。
存在的问题:加锁保证了数据安全,但严重削弱了实时流数据处理能力,有性能瓶颈。加锁的方式能够完美解决redis脏读的问题,但是加锁导致同一时刻只能有一个线程进行写操作,大大削弱了整个任务的性能。同时,当某个子任务加锁后程序出错后未释放锁需要等待较长时间的延迟才能释放锁,导致数据积压或者数据延时。
(2) 按企业名分发处理,利用对企业名hash取模的方法,将每个企业名数据只分发到一个线程处理
由于我们做的是对每个企业的总数统计,如果对每个企业计数都在一个线程中,单个线程顺序插入更新,就不会出现脏读的情况。由于一般kafka的一个或多个partition一般会对应下游的一个消费线程,因此我们可以考虑在flume数据分发到kafka时就有选择的进行分发。Flume写入kafka我们选择了按照企业名分发partition,解决了线程的问题。解决了线程问题后,发现会出现部分part太不均衡的情况,针对top5企业(占比40%)进行了计算规则的人工调整,保证各part的基本均衡。
存在的问题:负载不太均衡,可能有的进程/线程空闲时间较长
3.2 因某个bolt处理数据量太大,导致集群性能不足
举个例子:如果计算每个地市的总流量,一种做法是直接根据地市对数据进行分片,然后直接汇总。但是由于某些大城市的流量特别大,从而导致处理这些地区Bolt的压力特别大,从而大幅度延缓了整个Toplogy的处理性能。以下图业务为例,我们执行shuffle操作后,ACDE的业务都较小,而B的业务很大,各个Bolt严重不均衡。我们使用的解决办法是先按小区级别汇总,然后再按地市级别去做二次汇总,这样基本保证了每个bolt的计算量均衡,但是随之而来带来了额外的性能开销(我们进行了两次计算,先算小区再算地市)。所以说我们日常做优化时可以选择曲线方针,某些操作看似增加了业务计算量,实际提高了整体的性能。
向大家简要介绍了实时计算的架构和我们在实际开发中遇到的问题与解决的办法。最后想和大家分享的是:实时计算开发在不同业务中总会出现各种各样的问题,习惯与“脏数据”共事,有条不紊的追根溯源,是大数据开发必备的品质!