软件架构场景之—— 数据收集:高频数据收集请求如何不影响主业务?

业务背景

因业务快速发展,某天公司的日活用户高达 500 万,基于现有业务模式,业务侧要求我们根据用户的行为做埋点,旨在记录用户在特定页面的所有行为、开展数据分析与第三方进行费用结算,另外可以对用户的行为进行推送业务等

当然,在数据埋点的过程中,业务侧还要求在后台能准实时查询用户行为数据及统计报表,为了让你更加容易理解后续方案的设计思路,把真实业务场景中的数据结构进行了相关简化(真实的业务场景数据结构更加复杂)。首先,我们需要收集的原始数据结构如下表所示

指标备注
IMEI用户设备的IMEI
定位点经纬度
用户 id123
目标 id每个页面/按钮/banner 都有唯一识别的id
目标类型页面、按钮、banner等
事件动作点击、进入、跳出等
From URL来源 URL
Current URL当前 URL
TO URL去向 URL
动作时间触发这个动作的时间
进入时间进入该页面的时间
跳出时间跳出该页面的时间
......

通过以上数据结构,在后台查询原始数据时,业务侧不仅可以以城市(根据经纬度换算)、性别(需要从业务表中抽取)、年龄(需要从业务表抽取)、目标类型、目标 ID、事件动作等作为查询条件实时查看用户行为数据,还可以以时间(天/周/月/年)、性别、年龄等维度实时查看每个目标 ID 的总点击数、平均点击次数、每个页面的转化率等统计报表数据等等

为了实现费用结算这个需求,需要收集的数据结构如下表所示

字段备注
日期结算的日期
目标 id原始数据中的目标 id,比如页面 id、按钮 id 、banner id
点击人数有多少人点击了目标,1人点多次算1次
点击人次有多少人次点击,1人点多次算多次
费用目标 id 当天的收费总计

 

技术选型思路

根据以上业务场景,提炼出了 6 点业务需求,并针对业务需求梳理了技术选型相关思路

  1. 原始数据海量: 对于这点,初步考虑使用 HBase 进行持久化

  2. 对于埋点记录的请求响应要快: 埋点记录服务会把原始埋点记录存放在一个缓冲的地方,以此保证响应快速。关于这点有好几个缓存方案,下面展开讨论

  3. 可通过后台查询原始数据: 如果使用 HBase 直接作为查询引擎,查询速度太慢了,所以我们还需要使用 ES 来保存查询页面上作为查询条件的字段和活动 id

  4. 各种统计报表的需求: 关于数据可视化工具也有很多选择,比如 Kibana、Grafana 等,考虑使用过程的灵活性,最终选择自己设计功能

  5. 能根据埋点日志生成费用结算数据: 我们将费用结算数据保存在 MySQL 中

  6. 需要一个框架将缓存中的数据进行处理,并保存到 ES、HBase 和 MySQL 中。 因为业务有准实时查询的需求,所以我们需要使用实时处理工具。目前,市面上流行的实时处理工具主要分为 Storm、Spark Streaming、Apache Flink 这三种,下面展开说明

仔细观察这张架构图,你会发现图上还有 2 个地方打了问号,这是为什么呢?这就涉及我们接下来需要讨论的 4 个问题

1.使用什么技术保存埋点数据的第一现场?

市面上关于快速保存埋点数据的技术主要分为 Redis、Kafka、本地日志这三种,在上面的业务场景中,最终选择了本地日志

Redis 跟 Kafka 到底哪里不好,为什么你没使用呢?

先来说说 Redis 的 AOF 机制,Redis 的 AOF 机制会持久化保存 Redis 所有的操作记录,用于服务器宕机后数据还原,那 Redis 什么时候将 AOF 落盘呢?

在 Redis 中存在一个 AOF 配置:appendfsync,如果 appendfsync 配置成 everysec,AOF 每秒落盘一次,不过这种配置方式有可能会丢失 1 秒的数据。如果 appendfsync 配置成 always,每次操作请求的记录都落盘后再返回成功信息给客户端,不过这种配置方式系统性能就会很慢。因为对于埋点记录的请求要求响应快,所以我们没有选择 Redis

再讨论下Kafka 的技术方案,Kafka 的冗余设计是每个分区都有多个副本,其中一个副本是 Leader,其他副本都是 Follower,Leader 主要负责处理所有的读写请求,并同步数据给其他 Follower,Kafka 什么时候将数据从 Leader 同步给 Follower 呢 ?

Kafka 的 producer configs 中也有个 acks 配置,它的配置方式分为三种

  1. acks=0:不等 Leader 将数据落到日志,Kafka 直接返回完成信号给客户端。这种方式虽然响应快,但数据持久化没有保障,数据如果没有落到本地日志,系统就会出现宕机,导致数据丢失

  2. acks=1:等 Leader 将数据落到本地日志,但是不等 Follower 同步数据,Kafka 就直接返回完成信号给客户端

  3. acks=all:等 Leader 将数据落到日志,且等 min.insync.replicas 个 Follower 都同步数据后,Kafka 再返回完成信号给客户端。这种配置方式虽然数据有保证,但响应慢

如果我们想保证数据的可靠性,必然需要牺牲系统性能,那有没有一个方案可以性能+可靠性同时兼得呢?有的,所以最终决定把埋点数据保存到本地日志中

2.使用什么技术(ES、HBase、MySQL)把缓冲的数据搬到持久化层?

最简单的方式是通过 Logstash 直接把日志文件中的数据搬运到 ES,但是问题来了,业务侧要求存放 ES 中的记录包含城市、性别、年龄等原始数据(这些字段需要调用业务系统的数据进行抽取),而这些原始数据日志文件中并没有,所以我们并没有选择 Logstash

如果要坚持通过 Logstash 把日志文件的数据搬运到 ES,分享 3 种实现方式

  1. 自定义 filter: 先在 Logstash 自定义的 filter 里封装业务数据,再保存到 ES。因 Logstash 自定义的 filter 是使用 Ruby 语言编写的,也就是说我们需要使用其他语言编写业务逻辑,因此 Logstash 自定义 filter 的方案被我们 pass 了

  2. 修改客户端的埋点逻辑: 每次记录埋点的数据发送到服务端之前,我们先在客户端将业务的相关字段提取出来再上传到服务端。这个方法也直接被业务端 pass 了,理由是后期业务侧每更新一次后台查询条件,我们就需要重新发一次版,实在太麻烦了

  3. 修改埋点服务端的逻辑: 每次服务端在记录埋点的数据发送到日志文件之前,我们先从数据库获取业务字段组合埋点记录。这个方法也被服务端 pass 了,因为这种操作会直接影响每个请求的效率,间接影响用户体验

另外,我们没选择 Logstash 还有 2 点原因

  • 日志文件中的数据需要同时输出 ES 和 Hbase 两个输出源,因 Logstash 的多输出源基于同一个 pipeline,如果 1 个输出源出错了,另 1 个输出源也会出错,两者之间会互相影响

  • MySQL 中需要生成费用结算数据,而费用结算数据需要通过分析埋点的数据动态来计算,显然 Logstash 并不适合这样的业务场景,因为 filter 可以改变每条数据某些字段的值

在上面的业务场景中,最终决定引入了一个计算框架了,此时整个解决方案的架构图如下

这个方案中就是先通过 Logstash 把日志文件搬运到 MQ 中,再通过实时计算框架处理 MQ 中的数据,最后保存处理转换出来的数据到持久层中

实际上,引入实时计算框架是为了在原始的埋点数据中填充业务数据,并统计埋点数据生成费用结算数据,最后分别保存到持久层中

关于 Logstash 的注意点,需要重点强调下

Logstash 系统是通过 Ruby 语言编写的,资源消耗大,所以官方又推出了一个轻量的 Filebeat。我们可以使用 Filebeat 收集数据,再通过 Logstash 进行数据过滤。如果你不想使用 Logstash 的强大过滤功能,你可以直接使用 Filebeat 收集日志数据发送给 Kafka。但问题又来了,Filebeat 是使用轮询的方式采集文件变动,存在一定(有时候很大)延时,不像 Logstash 可直接监听文件变动,所以最终我们选择继续使用 Logstash。(因为我们扛得住资源的消耗,有钱就是这么任性)

 

3.为什么使用 Kafka?

Kafka 是 LinkedIn 推出的开源消息中间件,它天生是为收集日志而设计,且它具备超高的吞吐量和数据量的扩展性,号称无限堆积,根据LinkedIn官方说法,他们使用 3 台便宜的机器部署 Kafka,就能每秒写入 2 百万条记录

Kafka 的存储结构中每个 Topic 分区相当于 1 个巨型文件,而每个巨型文件又是由多个 segment 小文件组成。其中,Producer 负责对该巨型文件进行“顺序写”,Consumer 负责对该文件进行“顺序读”

可以把 Kafka 的存储架构简单理解为 Kafka 写数据通过追加数据到文件尾实现顺序写,读取数据时直接从文件中读,好处是读操作不会阻塞写操作,这也是吞吐量大的原因

理论上只要磁盘空间足够,Kafka 可以实现消息无限堆积,因此它特别适合处理日志收集这种场景,可见我们选择使用 Kafka 是有一定理论依据

 

4.使用什么技术把 Kafka 的数据搬运到持久化层?

为了把 Kafka 的数据搬运到持久层,需要使用一个分布式实时计算框架,原因有 2 点

  1. 数据量特别大,为此我们需要使用一个处理框架将上亿的埋点数据每天进行快速分析和处理(且必须使用多个节点并发处理才来得及),再存放到 ES、HBase 和 MySQL 中,即大数据计算,因此它有分布式计算的诉求

  2. 业务要求实时查询统计报表数据,因此我们需要一个实时计算框架处理埋点数据

目前,市面上流行的分布式实时计算框架有 3 种:Storm、Spark Stream、Apache Flink,到底使用哪个好呢?

Apache Flink,不仅因为它性能强,还因为它的容错机制能保证每条数据仅仅处理 1 次,且它有时间窗口处理功能

关于流处理、容错机制、时间窗口这三个概念,这里具体展开说明一下

在流处理这个过程中,往往会引发一系列的问题,比如一条消息处理过程中,如果系统出现故障该怎么办?你会重试吗?如果重试会不会出现重复处理?如果不重试,消息是否会丢失?你能保证每条消息最多或最少处理几次?

在不同流处理框架中采取不同的容错机制,它们也就保证了不一样的一致性

  1. At-Most-Once : 至多一次,表示一条消息不管后续处理成功与否只会被消费处理一次,存在数据丢失可能

  2. Exactly-Once : 精确一次,表示一条消息从其消费到后续的处理成功,只会发生一次

  3. At-Least-Once : 至少一次,表示一条消息从消费到后续的处理成功,可能会发生多次,存在重复消费的可能

Exactly-Once 无疑是最优的选择,因为在正常的业务场景中,一般只要求消息处理一次。而 Apache Flink 的容错机制就可以保证所有消息只处理 1 次(Exactly-Once)的一致性,还能保证系统安全性能,所以很多人最终都使用它

Apache Flink 的时间窗口计算功能,以下是 Apache Flink 的一个代码示例,它把每个小时里发生事件的用户聚合在一个列表中

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime);
// alternatively:
// env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime);
// env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

DataStream<MyEvent> stream = env.addSource(new FlinkKafkaConsumer09<MyEvent>(topic, schema, props));

stream
    .keyBy( (event) -> event.getUser() )
    .timeWindow(Time.hours(1))
    .reduce( (a, b) -> a.add(b) )
    .addSink(...);

日志中事件发生的时间有可能与计算框架处理消息的时间不一致。假定实时计算框架收到消息的时间是 2 秒后,比如有一条消息,这个事件发生的时间是 6:30,因你接收到消息后处理的时间延后了 2 秒,即变成了 6:32,因此当你计算 6:01-6:30 的数据和,这条消息并不会计算在 6:01-6:30 范围内,这就不符合实际的业务需求了

在实际业务场景中,如果需要按照时间窗口统计数据,我们往往是根据消息的事件时间来计算。而 Apache Flink 的特性恰恰是基于消息的事件时间,而不是基于计算框架的处理时间,这也是它的另一个撒手锏

整个架构设计方案如下图所示

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值