bitmap基础介绍+holo实现离线UV计算

bitmap

  • 基础介绍
  • bitmaping 数据结构
  • bitmap计算算子集成
  • 二阶段分布式计算:
  • RoaringBitmap构造方案
    • 分桶方案
    • 建序方案
  • holo官网 离线UV计算
    • 创建用户映射表
    • 创建聚合结果表
    • 更新用户映射表和聚合结果表
    • 更新聚合结果表
    • UV、PV查询

基础介绍

RoaringBitmap主要为了解决UV指标计算的问题。旨在建立一种可以多维分析的精准UV数据模型,并且可以低成本地实现交并差等集合运算UV指标。

PV和UV指标一直是各类业务中广泛存在并且重点关注的实时指标。其中对于PV指标而言,由于其具备可加性,因此当对维度组合或者时间维度进行上卷时,可以直接求和得出我们所要的累计结果。但是去重指标UV则具备不可加性,他是一种对UID去重计数的指标,如果在维度上卷时直接求和会导致结果偏大。即UV指标一旦定制化生成,就很难具备再计算的能力,需要用户事先计算好。例如。

  • 模型包含每天的UV,但是要算多天的去重UV,则需要从特定时间点重新开始算。
  • 模型包含每个城市的UV,但是要算全国的去重UV,则需要去掉城市维度再次聚合计算。

根据历史经验,去重指标有许多解决方案可供设计。主要分为两类,一类是前置计算方案,通过Count Distinct将UV预聚合后存储起来,查询时直接路由到对应UV指标。一类是后置计算方案,将user_id转变成可去重的结构,在查询时通过集合运算,算出UV指标。

  • 前置计算方案,即最常用的Count Distinct算子,统计时根据uid是否存在来决定计次,通常基于DWD模型或者UID粒度的DWS模型,生成UV指标存储在ADM模型中。
  • 后置计算方案,通常将user_id存储在可去重结构,可去重结构包括非精准去重的HyperLogLog与ThetaSketch,以及精准去重的RoaringBitmap。

bitmaping 数据结构

RoaringBitmap是效压缩位图,采取的是2^ 32位(4294967296)的Bitmap,可以存储[0,2^32 -1]区间范围的用户编号。但是RoaringBitmap会对这个2^32bit的Bitmap做很多压缩操作,将Bitmap尽可能地压缩在很小的存储量级。其核心思想是:

  1. 对于每个用户编号k,会被划分成二进制的高16位(k / 2^16)和低16位(k mod 2^16)。
  2. 其中高16位称为共享有效位,又可称为分桶号。属于roaring bitmap的一级索引,总计最多可包含2^16=65536个桶。共享有效位只存储一份,可以由多个编号共用,因此很大程度上减少了空间消耗。
  3. 每一个桶由一个Container 来存放一个数值的低16位。其中container是RBM新创造的概念,其核心目标是为了更高效地压缩和存储数据。Container 总共包括三种数据结构: Array Container ,Bitmap Container和RunContainer。Array Container 存放稀疏的数据,Bitmap Container 存放稠密的数据。此外,如果Array Container和Bitmap Container可以用行程编码压缩,就会替换成RunContainer存储。
    在这里插入图片描述
  • Array Container:使用short数组存储低16位,元素排序后放入short数组中。没有数据压缩机制,在数据稀疏场景存储效率高。
  • Bitmap Container:long数组存储低16位,数据内容对应long类型的bit位,数据稠密存储效率很高。如:1,5,6表达成一个long值为00110001.
  • Run Container:低16位使用short数组存储,将连续数据值存储为[起始点,连续个数的格式]。在数据连续性好的场景存储效率高。例如数据[11,12,13,14,15]将存储为两个short数值[11,4]。

在存储效率方面,数据量离散且小于4096时使用array最优,在数据量大且无规律时bitmap最优,在数据连续性比较好的情况下,RunContainer的存储效率最优。

时间复杂度方面Bitmap时间复杂度为O(1)高于Array和Run存储的nlog(n)。

bitmap计算算子集成

hologres本身是兼容postgres开源生态,pg版本的roaringbitmap插件通过简单适配,很容易集成在hologres中。roaringbitmap插件:https://github.com/zeromax007/gpdb-roaringbitmap。

holo中roaringbitmap函数使用文档 :https://help.aliyun.com/zh/hologres/user-guide/roaring-bitmap-functions?spm=a2c4g.11186623.0.0.df1b5791jxHiiU

典型计算算子:
在这里插入图片描述

二阶段分布式计算:

数据的分布键按桶号和bitmap高16位打散到hologres各个计算节点。在进行交并差集计算过程中,由于各个节点之间数据完全独立,每个节点可以单独进行计算,并将计算结果直接汇总到master节点计算进行聚合。整个计算过程是一个二阶段计算过程,完全没有数据shuffle, 整体计算非常高效。
在这里插入图片描述

RoaringBitmap构造方案

RoaringBitmap使用过程中,主要遇到的问题,就是如何将user_id存入到RoaringBitmap数据结构中。因为RoaringBitmap不能像HyperLogLog那样,可以直接将user_id存入Bitmap数据结构中。为了能将亿级的编号存入Bitmap中,探索过如下两种方案。

分桶方案

主要解决:数据量过多

分桶方案采取分治的思想,即将一个大用户集切成多个桶,每个桶的量级足以存到42亿的Bitmap中,并且桶中的UID互斥,没有重合的UID。

而分桶的方案包括很多,例如可以采取前几位分桶,或者后几位分桶,只要能够保证剩下的几位都可以存在Bitmap里面。在实时场景中,由于要考虑Explorer的机器数,因此既需要保障每个桶内的UV量级均匀的同时,也需要保障每台机器在存储桶的个数也是均匀的。如下图所示,但是由于ODPS没有存储机器数的概念,因此其分桶的个数可以更加灵活,只需要保障每个分桶下的UID数是相同的即可。

在这里插入图片描述
此外,为了防止数据条数膨胀太多,我们默认会采取分10个桶。如果UID量级比较大,用户也可以选择性地增加分桶个数。为了将UID均匀地划分到10个桶上面,我们采取取轮询分桶的方式,即对于第1个编号放在第1个桶里,第2个编号放在第2个桶里,不断轮询分配,直到第11个编号,又重新从第1个桶开始划分。因此最终的分桶方法为。

例如将2088022931508105按10个桶划分,则其分桶号和用户编号为
分桶号 = 93150810 % 10 = 0
用户编号 = concat(reverse(022),93150810 / 10 ) = 2209315081
分10个桶,等同于将UID的倒数第二位作为分桶号,剩下的有效位重排后作为编号。

分桶方案的优势

  • 不需要依赖外部表的输入,纯粹根据支付宝ID的特性生成新的编号,且分桶数相同的表可以相互做关联。
  • 在ODPS中,存储空间大小与分桶个数关系不大,即不同的分桶个数下表的存储空间的大小都差不多。但是分桶个数与bitmap聚合算子使用的机器数呈正相关,即分桶的个数越多,则ODPS reduce时的线程数,或者Explorer查询和存储的机器数也会越多,可以充分利用分布式集群的资源,提高查询效率。

分桶方案的局限性

  • 只能适用于支付宝ID,其余字符串类型的基数统计无法使用。
  • 分桶后会存在数据膨胀,由于分桶个数最少为10,因此至少表的数据会膨胀10倍。在部分场景对数据条数有限制时,需要限制聚合表维度组合的个数。
  • 在下游消费时,需要二次聚合,有一定的使用成本。

建序方案

建序方案也是当时实时场景中最早探索的方案,并且在一些开源的RoaringBitmap技术分享中,也是广泛使用此类方案的。例如《Flink+Hologres精准去重》 、《Hologres使用Flink+RoaringBitmap实现实时UV计算》 等。

其核心思想在于,在存储用户ID编号时,会先从一张【用户映射表】中获取用户新的编号,然后再将新的编号存储到RoaringBitmap中。【用户映射表】需要覆盖所有用户,如果用户不存在则将新的用户append至表中,并新增一个唯一编号
在这里插入图片描述

对于映射表的设计,首先为了保证每个新增用户的新编号是唯一的,可以采取自增主键的方式,并存储在新字段user_no中,此外,为了兼容分桶方案的编号,建序表保留了两个字段:bucket_no和bucket_user_no,分别作为分,分桶号和分桶后的用户编号。

建序方案的优势

  • 不需要分桶,也不会产生数据膨胀。
  • 在下游查询时,单次聚合即可,消费成本更低。
  • 可以对任意字符串都生成对应的编号,因此非支付宝ID的数据也可以存储bitmap算基数。

建序方案的局限性

  • 无法利用分布式集群的资源,由于仅会有一个桶,因此如果采用Explorer进行加速,不会进行计算下推,只能使用一台Explorer机器,查询性能会大打折扣。
  • 用户映射表需要高保链路来保障,首先映射表本身上游的写入任务不能发生延迟,防止下游消费的UID没有生成编号的情况,否则会导致结果不精准。其次映射表会被大量任务依赖,因此对查询请求量要求也更高。

holo官网 离线UV计算

  1. 创建一张用户明细表,用于存放业务所有维度的明细数据。
  2. 创建一张历史用户映射表,存放历史每个访问过的用户ID(uid)和对应的int32数值,其中int32主要是Serial类型,便于与明细表做用户uid映射。说明RoaringBitmap类型要求用户ID必须是32位int类型且越稠密越好(用户ID最好连续),而常见的业务系统或者埋点中的用户ID很多是字符串类型,因此使用uid_mapping类型构建一张用户映射表。用户映射表利用Hologres的Serial类型(自增的32位int)来实现用户映射的自动管理和稳定映射。
  3. 把T+1(上一天)的明细表和历史用户映射表做Inner Join得到基础维度表。
  4. 根据业务逻辑,将基础维度表按照最细粒度基础维度group by,把上一天的所有数据根据最大的查询维度聚合出的uid结果放入RoaringBitmap中,并存放在聚合结果表(每天百万条)。
  5. 按照查询维度查询聚合结果表,对其中关键的RoaringBitmap字段做or运算进行去重后并统计基数,即可得出对应用户数UV,计数条数即可计算得出PV,达到亚秒级查询。
    在这里插入图片描述

创建用户映射表

创建名称为uid_mapping的用户映射表,用于映射uid到32位INT类型,其DDL如下所示。

RoaringBitmap类型要求用户ID必须是32位int类型且越稠密越好(用户ID最好连续),而常见的业务系统或者埋点中的用户ID很多是字符串类型,因此使用uid_mapping类型构建一张映射表。映射表利用Hologres的Serial类型(自增的32位int)来实现用户映射的自动管理和稳定映射。

说明: 该表在本例每天批量写入场景,可为行存表也可为列存表,没有太大区别。如需要做实时数据(例如和Flink联用),需要是行存表,以提高Flink维表实时JOIN的QPS。

BEGIN;
CREATE TABLE public.zc_uid_mapping (uid text NOT NULL,uid_int32 serial,PRIMARY KEY (uid));--将uid设为clustering_key和distribution_key便于快速查找其对应的int32值
CALL set_table_property('public.zc_uid_mapping', 'clustering_key', 'uid');
CALL set_table_property('public.zc_uid_mapping', 'distribution_key', 'uid');
CALL set_table_property('public.zc_uid_mapping', 'orientation', 'row');
COMMIT;

创建聚合结果表

创建名称为dws_app的聚合结果表,用于存放RoaringBitmap聚合后的结果,其DDL如下所示。
基础维度为之后进行查询计算pv和uv的最细维度,这里以country、 prov、 city为例构建基础维表。

begin;
create table dws_dau_app(client_type text,ipv text,ds text NOT NULL,  --日期字段uid32_bitmap roaringbitmap, -- UV计算pv integer, -- PV计算primary key(client_type,ipv,ds)--查询维度和时间作为主键,防止重复插入数据
);
CALL set_table_property('public.dws_dau_app', 'orientation', 'column');
--clustering_key和event_time_column设为日期字段,便于过滤
CALL set_table_property('public.dws_dau_app', 'clustering_key', 'ds');
CALL set_table_property('public.dws_dau_app', 'event_time_column', 'ds');
--distribution_key设为group by字段
CALL set_table_property('public.dws_app', 'distribution_key', 'client_type,ipv,ds');
end;

更新用户映射表和聚合结果表

更新用户映射表每天从上一天的uid中找出新客户(用户映射表uid_mapping中没有的uid)插入到用户映射表中,命令如下。

WITH
-- 其中ymd = '20210329'表示上一天的数据user_ids AS ( SELECT imeisi FROM xxx WHERE ds = '20231119' AND imeisi is not null GROUP BY imeisi ),new_ids AS ( SELECT user_ids.imeisi FROM user_ids LEFT JOIN zc_uid_mapping ON (user_ids.imeisi = zc_uid_mapping.uid) WHERE zc_uid_mapping.uid IS NULL )
INSERT INTO zc_uid_mapping SELECT  new_ids.imeisi
FROM    new_ids

更新聚合结果表

更新完用户映射表后,将数据做聚合运算后插入聚合结果表,主要步骤如下。
undefined 通过明细表Inner Join用户映射表,得到上一天的聚合条件和对应的uid_int32。
undefined 按照聚合条件做聚合运算后插入RoaringBitmap聚合结果表,作为上一天的聚合结果。
undefined 每天只需进行一次聚合,存放一份数据,数据条数等于UV的量。明细表每天几亿的增量,在聚合结果表每天只需存放百万级数据。
插入数据至聚合结果表命令如下。

WITH aggregation_src AS
( SELECT client_type,is_ipv as ipv, uid_int32 FROM (SELECT imeisi,client_type,is_ipvfrom xxx  t1WHERE t1.ds = '20231119' and t1.imeisi is not null)t1INNER JOIN zc_uid_mapping t2ON t1.imeisi = t2.uid   
)
INSERT INTO dws_dau_app 
SELECT client_type,ipv,'20231119' ds,RB_BUILD_AGG(uid_int32),COUNT(1)
FROM    aggregation_src
GROUP BY client_type,ipv
;

UV、PV查询

查询时,从dws_app聚合结果表中按照查询维度做聚合计算,查询Bitmap基数,得出Group By条件下的用户数,命令如下。

-- 多天去重
SELECT  client_type,RB_CARDINALITY(RB_OR_AGG(uid32_bitmap)) AS uv-- ,rb_or_cardinality_agg(uid32_bitmap),sum(pv) AS pv
FROM    dws_dau_app
WHERE   ds in ('20231119','20231120')
GROUP BY client_type;-- 等价于
SELECT ds,client_type,count(distinct imeisi) 
FROM  xxx 
WHERE ds in ('20231119','20231120') 
GROUP by ds,client_typeSELECT  client_type,ds   ,RB_OR_AGG(uid32_bitmap),RB_CARDINALITY(RB_OR_AGG(uid32_bitmap)) AS uv,sum(pv) AS pv
FROM    dws_dau_app
WHERE   ds in ('20231119','20231120')
AND client_type = 'travel'
GROUP BY client_type,ds
;
-- 等价于
SELECT ds,client_type,count(distinct imeisi) 
FROM  xxx 
WHERE ds in ('20231119','20231120') 
GROUP by ds,client_type--  两天同端的来访去重
SELECT client_type,RB_CARDINALITY(RB_AND_AGG(xx))
FROM 
(SELECT  client_type,ds   ,RB_OR_AGG(uid32_bitmap) xx,RB_CARDINALITY(RB_OR_AGG(uid32_bitmap)) AS uv,sum(pv) AS pvFROM    dws_dau_appWHERE   ds in ('20231119','20231120')GROUP BY client_type,ds
)t
GROUP BY client_type
;-- 等价于
SELECT t1.client_type,count(distinct t1.imeisi) 
FROM  xxx t1
INNER JOIN xxx t2
ON t1.imeisi = t2.imeisi
AND t1.client_type = t2.client_type
WHERE t1.ds = '20231119'
AND t2.ds = '20231120'
GROUP by t1.client_type

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/155452.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第十一章 目标检测中的NMS

精度提升 众所周知,非极大值抑制NMS是目标检测常用的后处理算法,用于剔除冗余检测框,本文将对可以提升精度的各种NMS方法及其变体进行阶段性总结。 总体概要: 对NMS进行分类,大致可分为以下六种,这里是依…

训练跳跃(青蛙跳台阶),剑指offer,力扣

目录 题目地址: 题目: 青蛙跳台阶问题 我们直接看题解吧: 相似题目,斐波那契数列: 解题方法: 难度分析: 审题目事例提示: 解题思路: 代码实现: 小鸡识补充 题…

手机弱网测试工具:Charles

我们在测试app的时候,需要测试弱网情况下的一些场景,那么使用Charles如何设置弱网呢,请看以下步骤: 前提条件: 手机和电脑要在同一局域网内 Charles连接手机抓包 一、打开Charles,点击代理,…

如何搭建测试环境?一文解决你所有疑惑!

什么是测试环境 测试环境,指为了完成软件测试工作所必需的计算机硬件、软件、网络设备、历史数据的总称,简而言之,测试环境硬件软件网络数据准备测试工具。 硬件:指测试必需的服务器、客户端、网络连接等辅助设备。 软件&#…

Java 省考试院自学考试考籍管理系统

1) 项目简介 考籍管理系统是省考试院自学考试管理系统的一部分,包括考生考籍档案管理、考生免考管理、课程顶替、考籍转入转出管理、毕业管理和日志管理等功能模块。该项目的建设方便和加强了省考试院对自学考试考籍的一系列管理操作,社会效应明显。…

React函数组件状态Hook—useState《进阶-对象数组》

React函数组件状态-state 对象 state state 中可以保存任意类型的 JavaScript 值,包括对象。但是,你不应该直接修改存放在 React state 中的对象。相反,当你想要更新⼀个对象时,你需要创建⼀个新的对象(或者将其拷⻉⼀…

股票指标信息(六)

6-指标信息 文章目录 6-指标信息一. 展示股票的K线图数据,用于数据统计二. 展示股票指标数据,使用Java处理,集合形式展示三. 展示股票目前的最新的指标数据信息四. 展示股票指标数据,某一个属性使用Java处理五. 展示股票的指标数据,用于 Echarts 页面数据统计六. 展示股票指标数…

【开题报告】基于uni-app的汽车租赁app的设计与实现

1.项目背景及意义 项目背景: 随着人们生活水平的提高,汽车租赁服务在城市中变得越来越普及。传统的租车方式存在一些问题,比如租车流程繁琐、费用不透明、选择有限等。因此,开发一款基于uni-app的汽车租赁app成为了满足用户需求…

MAX/MSP SDK学习05:A_GIMME方法

今天终于将A_GIMME方法部分的描述看懂了,上周因为太赶时间加上这文档很抽象一直没看懂。也就那么一回事,记录一下。 A_GIMME方法用于接收多个参数: #include "ext.h" // standard Max include, always required #include "…

vue3拖拽排序 使用 vuedraggable

vue.draggable.next vue3 拖拽排序 vue.draggable.next 下载 pnpm add vuedraggablenext使用 <script lang"ts" setup> import { reactive } from "vue"; import draggable from "vuedraggable";const list reactive([{id: 1,name: &…

在Uni-app中实现计时器效果

本文将介绍如何在Uni-app中使用Vue.js的计时器功能实现一个简单的计时器效果。 首先&#xff0c;我们需要创建一个包含计时器的组件。以下是一个基本的计时器组件示例&#xff1a; <template><div class"timer"><p>{{ formatTime }}</p><…

Android 12.0 默认授予应用权限

Android 12.0 默认授予应用权限 最近接到客户需求提到每当首次点开某个应用时都会弹出申请权限的弹窗&#xff0c;操作起来感觉很麻烦&#xff0c;需要将指定的这个应用默认授予权限&#xff0c;具体修改参照如下&#xff1a; frameworks/base/services/core/java/com/androi…

uniapp 微信小程序如何实现多个item列表的分享

以下代码是某个循环里面的item <button class"cu-btn" style"background-color: transparent;padding: 0;"open-type"share" :data-tree"item.treeId" :data-project"item.projectId"v-if"typeId1 && userI…

阿里云3M固定带宽服务器速度快吗?是否够用?

阿里云服务器3M带宽下载速度是384KB/秒&#xff0c;上传速度是1280KB/s&#xff08;折合1.25M/秒&#xff09;&#xff0c;3M固定带宽够用吗&#xff1f;对于一般流量不是太大的个人博客、企业官网、论坛社区、小型电商网站或搭建个人学习环境或测试环境是完全够用的&#xff0…

Spring AOP用法(待完善)

Cglib实现AOP // 切所有方法Testpublic void cglib1() {UserService target new UserService();// 通过cglib实现AOPEnhancer enhancer new Enhancer();enhancer.setSuperclass(UserService.class);// 定义额外逻辑&#xff0c;也就是代理逻辑// o:代理对象; method:被代…

mysql 怎么做定时备份 / mysql 备份 / sql文件导出

在MySQL数据库中&#xff0c;你可以使用不同的方法来定时备份数据库。以下是其中的一种方法&#xff0c;使用Linux系统中的cron任务和mysqldump命令来创建定时备份&#xff1a; 创建备份脚本&#xff1a; 首先&#xff0c;创建一个脚本文件&#xff0c;比如backup_script.sh&am…

【ceph】ceph集群的故障域是怎么快速修改导入导出

本站以分享各种运维经验和运维所需要的技能为主 《python零基础入门》&#xff1a;python零基础入门学习 《python运维脚本》&#xff1a; python运维脚本实践 《shell》&#xff1a;shell学习 《terraform》持续更新中&#xff1a;terraform_Aws学习零基础入门到最佳实战 《k8…

RedisConnectionFactory is required已解决!!!!

1.起因&#x1f936;&#x1f936;&#x1f936;&#x1f936; redis搭建完成后&#xff0c;准备启动主程序&#xff0c;异常兴奋&#xff0c;结果报错了&#xff01;&#xff01;&#xff01;&#xff01; 2.究竟是何原因 &#x1f62d;&#x1f62d;&#x1f62d;&#x1f…

关于在3dsmax中制作的模型导入UE后尺寸大小不对的问题

现象 在3dsmax中的基本单位为毫米 在UE中基本单位是厘米 我在3dsmax中创建一个长宽高均为1000mm的方块 然后导入到UE中的世界坐标原点 方块向X轴正方向移动100个单位100cm1000mm&#xff0c;按理来说&#xff0c;新方块的此时应该和旧方块是贴着的&#xff0c;但是现象确是两者…