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

news2024/11/17 16:35:49

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_ipv
        from xxx  t1
        WHERE t1.ds = '20231119' and t1.imeisi is not null
    )t1
    INNER JOIN zc_uid_mapping t2
    ON 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_type


SELECT  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 pv
    FROM    dws_dau_app
    WHERE   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.coloradmin.cn/o/1233955.html

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

相关文章

网站为什么一定要安装SSL证书

随着互联网的普及和发展,网络安全问题日益凸显。在这个信息爆炸的时代,保护用户隐私和数据安全已经成为各大网站和企业的首要任务。而SSL证书作为一种网络安全技术,已经成为网站必备的安全工具。那么,为什么网站一定要安装SSL证书…

DeepStream--测试TrafficCamNet检测模型

模型地址:https://catalog.ngc.nvidia.com/orgs/nvidia/teams/tao/models/trafficcamnet/version 目前模型是nvidia的加密格式etlt。 nvinfer的配置 [property] gpu-id0 net-scale-factor0.0039215697906911373 tlt-model-keytlt_encode tlt-encoded-modeltraffic…

01【SpringBoot快速入门、yml语法、自动配置、整合框架】

目录 一、SpringBoot简介 1.1 Spring优缺点 1.1.1 Spring的优点 1.1.2 Spring的缺点 1.2 SpringBoot的概述 1.2.1 SpringBoot概述 1.2.2 SpringBoot的核心功能 二、SpringBoot快速入门 2.1 创建Maven工程 2.2 添加起步依赖 2.3 编写Controller 2.4 编写SpringBoot引…

软件需求的三大层次,逐层细化的注意事项

需求逐层分解和转化是一个持续优化的过程,在这个过程中,我们需要明确软件需求的三大层次,从而帮助项目团队理解组织或客户的高层目标和期望,满足用户的期望和需求,有助于产品的系统设计和开发。 一、软件需求三大层次 …

每日一题(LeetCode)----链表--两两交换链表中的节点

每日一题(LeetCode)----链表–两两交换链表中的节点 1.题目([24. 两两交换链表中的节点](https://leetcode.cn/problems/spiral-matrix/)) 给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内…

JavaWeb开发——文件上传

1 简介 文件上传:将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程 文件上传涉及到两部分:前端程序 服务端程序 前端程序 【三要素】:① 需要定义一个form 表单,且表单里需定义一个类型为“ …

Qt专栏3—Qt项目创建Hello World

setp1 打开软件 双击Qt Creator 11.0.3 (Community),打进入软件界面 step2 创建项目 点击创建项目 step3 选择模板 选着Application(Qt)->Qt Widgets Application setp4 设置项目 名称中填入项目号名,创建路径中填入项目保存位…

Git 简介及使用(1)

目录 一、在 Linux 环境中安装 Git 1. 先检查当前服务器中是否有 Git(如果有显示如下图) 2. 安装Git 3. 然后重复第一步:查看 Git 的版本信息即可 二、Git 的初始化及配置 1. 创建目录 2. 对仓库进行初始化 3. 新增两个配置项&#xff08…

Linux CentOS 8(DNS的配置与管理)

Linux CentOS 8(DNS的配置与管理) 目录 一、DNS相关知识1.1 DNS简介1.2 DNS的解析原理1.3 DNS解析 二、DNS服务器部署2.1 不使用chroot模式启动DNS2.2 使用chroot模式DNS 三、DNS配置文件详解3.1 主配文件详解3.2 区域数据库文件详解 四、项目实施4.1 主…

电脑便签工具推荐哪个?电脑上好用的便签软件使用哪一款

对于职场办公人士来讲,一款好用的电脑便签工具可以给日常工作带来极大的便利,如果您的日常工作离不开电脑工具,您就会知晓电脑便签工具在日常工作中的重要,电脑便签通常以一个小的窗口呈现在电脑桌面上,记录一些工作中…

Java修仙记之记录一次与前端女修士论道的经历

文章开始之前,想跟我念一句:福生无量天尊,无量寿佛,阿弥陀佛 第一场论道:id更新之争 一个天气明朗的下午,前端的小美女长发姐告诉我:嘿,小后端,你的代码报错了 我答道&am…

【Java】java | CacheManager | redisCacheManager

一、说明 1、查询增加缓存,使用Cacheable注解 2、项目中已经用到了ehcache,现在需求是两个都用 二、备份配置 1、redisConfig增加代码 Bean("redisCacheManage")Primarypublic CacheManager redisCacheManager(RedisConnectionFactory fact…

iptables的一次修复日志

iptables的一次修复日志 搭建配置wireguard后,使用内网连接设备十分方便,我采用的是星型连接,即每个节点都连接到中心节点,但是突然发生了重启wg后中心节点不转发流量的问题,即每个接入的节点只能与中心节点连接&…

No appropriate protocol -- Mysql

DataGrip连接mysql报以下异常信息: javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate) The following required algorithms might be disabled: SSLv3, TLSv1, TLSv1.1, RC4, DES, MD5wi…

java-jdbc快速入门

文章目录 简介快速入门 简介 JDBC就是使用Java语言操作关系数据库的一套APIJava DataBase Connectivity 快速入门 -- mysql 中准备工作 create database if not exists my_db; use my_db; create table account(id int,name varchar(20),money int ); insert into account v…

Threejs_09 gltf模型的引入(效果初现)

本节使用到的图片、素材、gltf文件,都是老陈打码的原素材 支持!!!!!!!!!!!!!!!&#x…

Thinkphp-商城项目之oss文件上传及web端直传

4.3头像上传 一般商城网站都会把文件上传到第三方云,例如阿里云(oss),腾讯云(cos),当然如果公司有足够的实力,可以自己部署一台文件服务器,用于文件的保存。 头像上传一般是用户在用户中心上传的,后台管理…

个人如何进行深度复盘?这6大高效的复盘模型,让你的年终总结如虎添翼!

一年之计在于春,一日之计在于晨,而一年的收获与成长,在于这个年终的深度复盘。自我复盘,是对过去一年生活、工作、学习的反思和总结,能帮助我们提炼经验,发现不足,规划未来,以便更好…

DataFunSummit:2023年数据基础架构峰会-核心PPT资料下载

一、峰会简介 正如From、Join、排序等是SQL的基本算子,存储与计算是也是数据架构中数据生产与消费的基本算子,对于数据架构之下的技术栈层级,我们可将其定义为数据基础架构。 数据存储技术在适应大数据时代的规模需求基础之上,持…