如何设计日活的流计算
之前在Redis上构建了基于Bitmap索引的存储结构,在Openresty上实现了一个简易的查询引擎,
这里再思考一下数据的来源、采集、实时接入等问题。
日活数据的来源
日活数据最大的可能来源就是浏览日志、登录日志等,数据的内容一般包括时间戳、UID、PageId等,
一般网站或者App统计日活是需要区分模块和功能的,类似友盟、TalkingData等公司的产品,以一个
SDK或者Agent的方式采集和发送数据。
数据接收
客户端采集到的数据会定期发送回来,接收端并没有复杂的业务处理逻辑,基本上就是format。
当接收端收到数据做了基本上的合法性校验之后,可以选择写日志或者发送到队列中(比如Kafka)。
Openresty加Lua可以完成这部分的工作,性能会非常强悍,我比较倾向直接写本地Log,
然后批量发送至Kafka。
关于维度信息的计算
因为我们是要做维度数据统计的,所以在数据处理过程中就需要为访问日志数据添加维度的信息。
维度信息大体可以分为两大类,一类是固定维度,一类是变动维度。固定维度就类似性别、年龄等,
变动维度可以是所在的地理位置等信息。维度的数据经常是需要进行编码的,就是将string转id。
客户端每次上传的数据中是冗余维度信息,还是在流计算中去获取转化丰富数据的维度,这个逻辑
通常就是getOrCreate,如果是有一个Mysql的表来存储这种数据,再加一个Redis的Cache,这种
中心化的设计在数据流量大的时候必然是瓶颈,所以要适当考虑在客户端做一些工作。比如,在客户
端启动时,进行一些基本维度的转化工作,并且将这些转化的数据进行保存,在定期提交数据时,
主动将数据的string转id的工作在客户端完成。当然这样的分散逻辑也导致了维护成本,当你需要进行
一些数据的整理和升级时,就会要跟客户端打交道了,需要设计一下数据的Version和避免过于集中的
数据更新导致后端压力过大。
关于时间窗口与聚合
无论你选择了哪一种流计算框架,都会提供一个Timewindow的功能,提供基本的Map、Reduce功能。
如果我们的浏览访问数据存在极大的聚合利益,可以压缩几倍或者十几倍,就值得一做。
如果考虑到数据的修复和数据Delay等问题,一定要设计一个增量补全的逻辑,比如有put和incr操作。
之前看过一些文章有在Hbase上加协处理器来完成的,当然我们的Redis+Lua也是可以的。
数据的输出结果尽量不要直接写存储,可以再打回Kafka中,做多订阅的处理。Kafka的客户端可以把
单条输出转化为批量,这种设计比较容易规避流计算的一些迭代输出单条的麻烦。
维度数据的存储
每个用户的每次访问都带有20个维度信息,这种沉重的冗余信息会让你的存储不堪重负的。
因为我们的目标是日活,也就是以用户为主体。当某个用户已经被处理过一次了,那么他的固定维度
数据最好就不用再处理了,每次只处理变动信息就好了,表的行数相对比较稳定,跟总用户数有关。
如果只处理日活相对比较简单,如果想要按照时间做区分,比如每小时一个列,相当于行转列。
UID | Gender | Age | 00:00 | 01:00 | 02:00 | 03:00 |
---|---|---|---|---|---|---|
1 | F | 12 | 1 | 1 | 1 | 1 |
2 | M | 13 | 0 | 0 | 0 | 0 |
3 | F | 10 | 1 | 1 | 1 | 1 |
4 | M | 19 | 0 | 0 | 0 | 0 |
水平扩展
如果单机Redis不能存下所有的数据,就需要有一个分布式的方案,因为我们的计算是列级别的,
所以同一个Uid的所有列必须在一台Redis实例中,于是我们的分布式方案只能按照Uid来分了。
因为我们的UID是使用16K、64K的Block块来划分的,所以我们只要对Block块进行离散就好了。
计算引擎的改动也会很小,这里就是一个简单的MapReduce的过程。