什么是海量数据?
所谓的海量数据从字面上理解就是数据多到已经用大海来形容了,它指的就是数据量太大,无法在较短时间内迅速解决,无法一次性装入内存。
少量数据处理方案
对于在内存中可以一次性快速处理的少量数据,我们有很多方式对数据进行处理。
-
数据排序:通过快排、归并排序、桶排序等等多种方式都可以进行处理;
-
数据查询:通过遍历、二分查找等方式,或者通过构建 hash 表、红黑树、位图等数据结构实现快速查询;
-
求 TOPK:
- 对于静态数据,可以使用排序后遍历,或者使用快排的思想快速查找分区点等方式进行处理;
- 对于动态数据,可以借助堆这种数据结构实现快速查找 TOPK;当然,静态数据也能使用堆的方式求 TOPK;
-
两组数据去重/找重:
- 对一组数据构建 hash 表,另一组数据基于这组 hash 表实现去重/找重;
- 先对两组数据分别进行排序,然后使用双指针(分别指向一组数据)的方式进行逐个遍历,实现去重/找重;
对于少量数据的处理,因为当前数据量较小,可以一次性加载到内存中进行处理,所以对于这种数据的处理,我们可以找到很多种方式对数据进行处理。
海量数据处理面临的问题
我们要想对海量数据实现排序、查询、求 TOPK、去重等操作,我们没法直接把数据一次性加载到内存中,然后一次性进行处理,因为海量数据往往面临以下两个问题:
- 单台机器内存不够;
- 单台机器对数据的处理速度过慢。
海量数据处理的核心思想
基于海量数据处理面临的上述两个问题,我们可以很容易想到一些对于海量数据进行处理的方案:
- 不必把数据一次性加载到内存中,而是通过分批处理的方式,把外存中的数据加载到内存中进行处理;
- 单机内存存不下,那么可以扩展为多机,对于外存中的海量数据,把数据分片到不同的机器中,用多机内存进行处理;
- 对于单机对数据处理速度慢的问题,可以通过多机并行计算的方式进行并行处理,提升整体的处理速度。
海量数据处理的一些常见案例及对应处理方案
排序问题
案例:给 10 GB 的订单文件进行排序,排序条件是订单的总金额。
首先需要判断,当前内存中能否一次性处理这 10 GB 的文件?如果能处理的话,直接在内存中通过排序的方式进行处理即可。
如果内存中无法一次性处理 10 GB 大小的文件,我们应该如何处理?
我们假设内存大小是 1.5 GB ,我们每次从 10 GB 的文件中读取 1 GB (预留 0.5 GB 的空间,用来申请临时变量等使用)的文件到内存中;
我们在内存中对这 1 GB 的文件排好序后,将排好序的 1 GB 文件写到外存中,然后释放内存空间;
接着继续往下处理第 2 个 1 GB、第三个 1 GB ......依次类推,最终可以得到 10 个 1 GB 的小文件,每个小文件内部都是有序的。
接下来,我们需要将这 10 个各自有序的 1 GB 小文件合并成一个大的 10 GB 的有序文件;
这里的处理思路也非常简单,我们可以从每个小文件中取最小的一个值,放入内存中 size 为 10 的数组中,找到数组中最小的值,写 10 GB 的新文件(也可以是覆盖原来的 10 GB 文件)第一个位置;
然后从内存中删除这个最小值,并在该值对应的 1 GB 有序小文件中读取第二个值放到数组中,重复之前的操作;
最终我们即可将 10 个 1 GB 的有序文件整合成 1 个 10 GB 的有序文件。
针对上面这种情况,其实我们还可以继续做一些优化:
(1)IO 操作是比较耗时的,所以我们每次不仅仅只是从外存中读取 1 个值处理,可以在读取时给每个小文件添加一层缓存,大小 100 MB ,读取时优先从缓存中获取,缓存读完后再触发下一次 IO 操作从文件中读取数据;
(2)同样的思路,在写回文件时,也可以使用缓存来减少 IO 操作,提高效率;
(3)上述问题在单机的情况下,我们顺序将 10 GB 的文件读取为 10 个 1 GB 的小文件,然后用多路归并的方式合并;在多机情况下,假设我们有 10 台机器,那么每台机器可以读取 1 GB 的文件,同时进行排序,再按照上面的思路进行后续的整合,可以进一步提高效率。
对于上述问题,我们使用了多路归并的思想进行了处理,实际情况中,如果订单金额最小最大值我们是已知的,且分布较为均匀的话,我们还可以使用如下的方式进行处理:
假设订单金额在 0-999 元的范围内,顺序读取 10 GB 文件,按照金额大小,将订单拆分到不同的桶中;
之后,按序将每个小文件读取到内存中排好序,然后写回到 10 GB 的大文件中;由于不同小文件之间金额有大小关系,所以顺序写回的10 个小文件写好后整个大文件就是有序的。
查询问题
案例:判断当前登录人 IP 地址是否在 IP 地址白名单内,已知 IP 地址白名单现有 10 亿个 IP 地址。
单机处理方式:
这个问题首先我们已知 IP 地址可以用 32 位的整数来表示,占用 4 个字节;对于 10 亿个 IP 地址,总共占用空间大约 4 GB;假设我们内存空间是 1 GB,那么是否可以在内存中直接判断登录人 IP 是否在这 10 亿个 IP 地址中呢?
首先考虑可以支持快速查找的数据结构: hash 表、红黑树,发现这种数据结构无法在 1 GB 的内存地址存储 4 GB 的数据;考虑到节省空间,可以使用位图来进行处理:
IP 地址表示为 32 位整数,int 类型数字范围最大可以在 0~2^32-1,所以存储 int 类型数字,我们总共需要 2^32 个二进制位来进行存储;8 个二进制位为1个字节,所以 2^32 / 8 个字节,可得大约需要 500 M 空间的位图来存储。
位图构建:扫描 IP 地址白名单文件,找到 IP 地址对应 int 类型数字所在的位图中的二进制位的位置,将对应二进制位置为1;
实时查询:通过当前登录人 IP,找到位图中对应的位置,判断当前位是否为 1 即可处理。
多机处理方式:
对于白名单服务,我们也可以考虑使用多机进行处理,降低单机的压力。
假设我们有 4 台机器,每台机器有 1 GB 的内存,我们需要处理这 10 亿个 IP 地址( 500 MB )。我们首先需要扫描外存中的白名单文件,对每个 IP 地址求 MD5(确保分布均匀),再对 4 求余,然后将该条白名单 IP 存储对应的机器内存中(存储形式按照内存是否富余可以考虑 hash 表、红黑树、位图等方式)。在校验当前登录人 IP 时,我们也对当前登录人 IP 求 MD5,再对 4 求余,然后找到对应的机器,再在该机器上判断是否在白名单内即可。
TOPK 问题
案例:对 100 GB 搜索关键字⽂件,统计其中出现频率最高的 TOP 100 的关键词
针对这类问题,我们首先假设内存足够大,可以直接一次性加载处理的情况,我们应该如何处理?
(1)首先统计每个关键词出现的频率,可以用如下方法
- 方法1:先进行排序,然后顺序扫描,边扫描边统计;
- 方法2:使用 hash 表进行频率统计
(2)然后我们可以使用堆的方式,按照频率统计出现频率 TOP 100 的关键词。
简单的代码实现如下:
ini
public String[] top100(String[] keywords) {
if (keywords == null) return null;
// hash表统计频率
Map<String, Integer> map = new HashMap<>();
for (String keyword : keywords) {
map.put(keyword, map.getOrDefault(keyword, 0) + 1);
}
// 小顶堆保留频率最大的top100
PriorityQueue<String> heap = new PriorityQueue<>(100, (o1, o2) -> map.get(o1) - map.get(o2));
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String keyword = entry.getKey();
int frequency = entry.getValue();
if (heap.size() < 100) {
heap.offer(keyword);
} else if (frequency > map.get(heap.peek())) {
heap.poll();
heap.offer(keyword);
}
}
// 结果输出
String[] res = new String[100];
int i = 0;
while (!heap.isEmpty()) {
res[i++] = heap.poll();
}
return res;
}
实际上,100 GB 数据无法无法一次性加载到内存中,对于海量数据的频率 topK ,我们按如下方式进行处理:
(1)首先还是需要对每个关键词的频率进行统计,统计关键词频率的方式跟内存中统计一样,都可以使用排序后遍历和 hash 表统计的方式。
a. 排序后遍历的方式较为简单,首先对于海量数据排序,我们可以使用之前提到的海量数据排序问题的处理方式,得到一个有序的关键词文件;之后我们顺序扫描有序文件中的关键词到内存中,并记录同一关键字连续出现的个数,统计每个关键词的形式,之后以 key-val 的形式({"关键字":"出现次数"})存储到外存中形成一份关键词频率文件。
b. 对于 100 GB 海量数据关键词频率的统计,还可以使用 hash 表的方式:
假设机器有 15 GB 的空间,我们对每个关键词求 MD5,然后对 10 取余的方式进行数据分片,这样余数就落到 0-9 的范围内,就可以把 1 个大文件拆分为 10 个小文件,编号为 0.txt~9.txt;
我们可以先把 0.txt 加载到内存中,然后使用 hash 表来进行频率统计,统计后生成 key-val 的频率统计小文件;
之后依次读取 1.txt~9.txt,这样就生成 10 个存储 key-val 的频率统计小文件。
(2)统计完频率以后,我们需要读取频率文件,然后获取频率最高的 top100 关键词。这里依然可以使用堆的方式,读取外存中的 key-val 到内存中,然后通过容量为 100 的小顶堆获取频率最高的 top100 关键词。
去重/找重问题
案例:有两个文件 0.txt 和 1.txt,各有 50 亿条 URL,每条占内存为 64 字节,单机内存限制 4 GB,需要找出这两个文件中相同的 URL
我们首先考虑内存足够大,可以一次性将所有数据加载到内存中的情况,如何处理两个文件中找到重复 URL 的问题,有两种方式可以作为参考:
(1)方式1:先对两个文件各自进行排序,然后使用双指针的方式,a 指针指向文件 0.txt 开头,b 指针指向文件 1.txt 开头,对比指针位置是否相等,相等就将重复值取出保存;不相等的情况下,如果 a 指针的值小于 b 指针的值,那么 a 指针后移一位,否则 b 指针后移一位,可参考如下代码:
ini
public List<Integer> findDuplicate(int[] arr1, int[] arr2) {
if (arr1 == null || arr2 == null) return null;
// 先对两个数组进行排序
Arrays.sort(arr1);
Arrays.sort(arr2);
// 重复数字结果
List<Integer> res = new ArrayList<>();
// a、b双指针
int a = 0, b = 0;
while (a < arr1.length && b < arr2.length) {
// 数组内部重复的值只统计一次
if (a < arr1.length -1 && arr1[a] == arr1[a + 1]) {
a++;
continue;
}
if (b < arr2.length -1 && arr2[b] == arr2[b + 1]) {
b++;
continue;
}
// 相同的值取出
if (arr1[a] == arr2[b]) {
res.add(arr1[a]);
a++;
b++;
} else if (arr1[a] < arr2[b]) {
a++;
} else {
b++;
}
}
return res;
}
(2)方式2:先对一个文件构建 hash 表,然后遍历另一个文件,通过 hash 表来找到重复的 URL 。
对于海量数据而言,仍然可以使用上面的两种方式来进行处理:
(1)方式1:排序+双指针
先对两个文件 0.txt 和 1.txt 进行排序,具体方案可以参考上文排序问题里面的案例;
然后使用 a 、 b 两个指针,分别指向 0.txt 和 1.txt 文件的第一个值,读取到内存中,如果值重复就写到新的文件 2.txt 中,不重复就按照上面内存中少量数据处理方式后移较小的指针,直到其中某个文件被读取完即可;
这里可以优化为:每次从外存中读取 100 MB 的数据进行处理,减少 IO 操作。
(2)方式2:hash 表找重
构建 hash 表的过程需要在内存中进行,一次性加载 0.txt 和 1.txt 到内存中是不可行的,通过前面案例的启发,很容易想到使用分片的方式:
扫描 0.txt 和 1.txt 对 URL 求 MD5,然后对 20 求余(这里只是举例,具体分片多少个需要根据内存的实际情况),将 0.txt 和 1.txt 中余数为 0 的 URL 放到内存中,然后通过 hash 表的方式找到重复的元素,写入新文件;
扫描 0.txt 和 1.txt 对 URL 求 MD5,然后对 20 求余(这里只是举例,具体分片多少个需要根据实际情况),将 0.txt 和 1.txt 中余数为 1 的 URL 放到内存中,然后通过 hash 表的方式找到重复的元素,写入新文件;
以此类推......
最终我们会扫描 20 次,并将所有重复的 URL 写到新文件中。
这种方式虽然可以实现找重,但是扫描了 20 次文件,所以可以使用多机的方式进行处理,我们使用 20 台机器,把余数为 0~19 的分别放到不同的机器上进行处理,这样只需要扫描 1 次,极大提高了处理效率。
总结
对于海量数据处理问题,在实际情况中,我们可以先考虑单机内存足够处理的情况下需要采用何种方式;
当我们找到单机内存充足情况的处理方案以后,再通过一些海量数据的通用处理手段,例如:外存分批读取、分片、多机并行处理等方式,最终达到成功处理海量数据的目标。
推荐阅读
招贤纳士
政采云技术团队(Zero),Base 杭州,一个富有激情和技术匠心精神的成长型团队。规模 500 人左右,在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。
如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊......如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com
微信公众号
文章同步发布,政采云技术团队公众号,欢迎关注