我正在参加「掘金·启航计划」
上一篇文章我们解决写操作对数据库的压力,但是如果是长期频繁插入数据,那么这个方案并不能解决这个问题。这篇文章将会解决这个问题。
一、案例
A公司有一个面向全国的大型社区团购APP,日访问量在80万以上,用户下单量16万单以上,运营中心需要根据用户在页面上的停留时间、用户所在位置来进行数据分析,以便调整不同商品的营销策略,并且还需要查看不同商品每天的点击人数、点击人次和下单人数。 这是一个看似很简单的需求,貌似就只是设置一个埋点,记录一下用户在商品详情页停留时长、用户当前所在位置以及在商品详情页的操作信息,但是对于一个日访问量在80万以上,用户下单量在16万以上的网站来说,长期不断的写入数据势必会造成数据库压力剧增。根据这个需求,我们分析出如下两张表:
- 商品操作原始数据表
| 字段 | 说明 |
|---|---|
| 用户位置 | |
| 用户ID | |
| 商品ID | |
| 停留时长 | 离开详情页时间减去进入详情页时间,离开详情页也有可能是跳转到了下单页面,也有可能是直接跳出了详情页 |
| 是否下单 |
- 商品操作综合数据表
| 字段 | 说明 |
|---|---|
| 商品ID | |
| 点击人数 | 每个用户点击N次都算作一次 |
| 点击人次 | 每个用户点击N次算N次 |
| 日期 | |
| 下单次数 |
二、解决方案思路
上一小节我们简单的描述了需求以及简要的涉及了数据表,这一小节我们将会从业务需求和技术问题两方面进行探讨实现思路。
2.1 实现问题
详细的业务需求如下
- 海量的原始数据初始化 根据前面的文章,以及当前案例的情况,我们选择了 HBase 对原始数据进行持久化。
- 快速响应埋点数据 埋点数据会存储在缓存层中,这样就可以实现快速响应,后面的内容我们将详细讲解。
- 查询原始数据 因为将 HBase 作为查询引擎的话,查询速度会很慢,因此我们需要结合 Elasticsearch 使用,Elasticsearch 中将保存查询条件和商品ID。
- 运营部门需要灵活的查看统计报表 虽然第三方的数据统计报表很多,但是运营部门要求可以灵活的查看统计报表,因此我们可以自己来设计并开发。
- 根据埋点获取到的商品操作原属数据,生成商品操作综合数据表 将商品操作综合数据存入数据库中。
- 将缓存中的数据存储在 HBase、Elasticsearch 和数据库中 这个案例不要求很高的实时性,因此我们需要从 Storm、Spark Streaming和Flink中选择一种使用就可以了,下面的内容我将会详细讲解如何选择。
2.2 技术问题
2.2.1 使用什么技术保存埋点数据?
目前保存埋点数据的技术一共三种:Redis、Kafka和本地日志记录,下面我就来说一下这三种技术在案例中如何选型:
- Redis: AOF机制将所有数据持久化保存起来,每当服务器发生宕机,并再次启动后会将数据还原,但是这里有个问题。Redis 什么时候会将 AOF 落盘。如果我们将 AOF的配置项 appebdfsync 设置为 everysec 的话就代表一秒钟进行一次落盘,那么当服务器宕机后,就有可能丢失一秒内的数据,如果设置为 always的话,就代表每次操作请求都将会进行落盘,虽然这种方式可以保证数据几乎不会丢失,但是会造成系统运行很慢,这也违反了 2.1 节所说的快速响应埋点数据的要求。
- Kafka: Kafka中存在冗余设计,每个分区都有多个副本,其中的一个副本是主副本,其他的副本都是从副本,主副本负责全部读写请求的请求并将数据同步到从副本中。 和Redis 一样,我们需要设置主副本什么时候将数据同步到从副本中。在 Kafka 中我们可以在 Producer 中配置 acks ,acks 有三个值:0、1、all。 0:不管主副本是否已经将数据落入日志,就直接将完成信号返回给客户端,这种方式虽然响应很快,但是数据持久化无法得到保证。 1:主副本将数据落入日志,但不管从数据是否已经同步到数据,就直接将完成信号返回给客户端。 all:主副本将数据落入日志并完全同步给从副本后,才将完成信号返回给客户端,虽然可以保证数据基本不会丢失,但是响应速度很慢。 经过分析,Redis和Kafka在使用过程中无法既保证数据可靠性又能保证性能,要解决这两个问题,我们就只能使用本地日志记录了。
2.2.2 使用什么技术将本地日志存储到持久化层中
一般来说,我们经常用的技术是 Logstash,使用它将日志迁移到 Elasticsearch 中,但是我们的案例需要在 Elasticsearch 中存储搜索条件,这些搜索条件在日志中并不存在,因此我们无法直接使用这两个技术的组合。如果必须使用这种两个组合的话我们有两种解决方案:
- 自定义过滤器: 在Logstash的自定义过滤器里加入业务数据,然后再保存到 Elasticsearch 里,这里要注意的是 Logstash 的过滤器是使用的是 Ruby 语言编写的,也就是说我们至少要掌握 Ruby 语言的基础。
- 修改埋点逻辑: 修改埋点逻辑可以分为两种,一种是修改客户端逻辑,一种是修改服务端逻辑,下面们来说一下。 修改客户端逻辑:每次将埋点数据发送到服务端之前,在客户端中将相关字段提取出来放入埋点数据中,再发送给服务端。但是这个方案存在一个问题,每次查询条件更新都需要更新APP程序。 修改服务端埋点逻辑:每次客户端将埋点数据发送到服务器后,都先从数据库中将相关业务字段查询出来并加入埋点数据中后再将埋点数据存入本地日志记录中。这个方案也存在一个问题,每次都要从数据库中查询业务数据并加入到埋点数据中,这样会影响每个请求的响应速度。 但是,在我们的案例中,是无法直接使用Logstash的,这是因为日志文件需要同时向数据库和Elasticsearch中写入文件,Logstash 向多个目标输出时使用的是同一个管道,那么这就会出现如果一个目标发生错误,其他的目标即使没有人任何问题,也会出错。同时我们需要向数据库中存入商品操作综合数据,这是一个需要进行一定计算后才能存入数据库的数据,因此我们不能直接使用Logstash ,需要在项目中引入计算框架。思路是这样的,首先我们将日志通过 Logstash 迁移到MQ中,然后再使用计算框架处理MQ中的数据,最后将计算的记过保存在持久化层中。这里引入计算框架是为了给埋点数据中增加业务数据,计算商品操作综合数据,并存入持久化层中。
2.2.3 为什么选择Kafka
上一小节中我们提到了MQ,那么我们的项目里可以使用 Kafka,为什么要使用Kafka呢?首先它出来的目的就是为了日志收集,并且它的吞吐量和数据扩展两超高,几乎可以做到无限堆积,是其他MQ无法比拟的。Kafka的吞吐量之所以很高,是因为在它的存储结构中,每个Topic分区可以被看作一个巨型文件,每个巨型文件又是由很多个Segment文件做成,并且Producer负责对巨型文件进行顺序写,Consumer负责对巨型文件顺序读。简单的说就是写数据时kafka将数据追加到巨型文件末尾,读数据时直接从文件中读取,这样读操作不会阻塞写操作,从而提高了吞吐量。
2.2.4 用什么技术将Kafka中的数据迁移到持久化层
这里我们需要使用前面所提到的分布式实时计算框架,这时因为数据量巨大,需要有一个框架将数据快速分析并处理,然后再存储到持久化层中。 目前流行的持久化框架有三种:Storm、Spark Stream和Flink。这三个都可以使用,但是就目前而言使用比较多的是Flink,这是因为它性能强劲,流处理的容错机制能保证每条数据只能被处理一次,而且还有时间窗口处理功能。
- 流处理的容错机制: 在流处理过程中,有时会出现一条消息在处理过程中系统出现了故障,那么我们要么重试,要么什么都不处理。基于这个问题,大部分流处理框架中就引入了不同的容错机制,主要有三种:
- 至多一次(At Most Once):一条消息不管处理是否成功,只能被消费一次,因此这种机制存在数据丢失的可能;
- 精确一次(Exactly Once):一条消息从消费到处理成功之会发生一次;
- 至少一次(At Last Once):一条消息从消费到处理成功可能会发生很多次,这种机制存在数据重复消费的可能。 综上所述,精确一次是最好的选择,这也是Flin所使用的容错机制,既能保证消息只被消费一次,也能保证系统的安全性。
- 时间窗口 日志中记录的事件发生事件可能和甲酸框架处理消息的时间不一样,Flink的特性是基于事件发生时间,而不是基于计算框架的处理时间来计算数据的。
2.3 思路总结
这个方案的流程如下:
- 服务端记录所有的请求数据,并存入本地日志文件中;
- 使用Logstash手机数据,从日志文件中获取原始日志数据后存放在Kafka中;
- 使用Flink从Kafka中获取数据,并进行加过处理,然后存放在持久化层中;
- 当用户查询数据时,从Elatcsearch中获取到商品ID,然后通过商品ID在HBase中查询出详细数据;
- 数据库中存放组合加工后的商品操作综合数据,当有查询时直接从数据库中查询即可。
三、总结
这篇文章主要讲解了技术选型的问题,涉及到了具体的技术,读者可以根据本文的内容,在后续的实际开发中去探索每个项目的技术选型。