数据变多不是错觉:网络把行为都记下来了,而且记下来的多数是文字、图片、声音。 这些非结构化数据又占地方又压不动,还塞不进表的某一列。 体积这件事要真算:同一条记录,三种存法差三倍;压过之后差距还会反转。
这是全书第一篇。不假设你搭过集群,只要求你会四则运算。 教材第 1 章用两节铺背景:大数据和分布式处理、非结构化数据与机器学习。 本篇把这两节合成一条线:数据为什么多、多在哪、多到什么程度就得换机器。 后面第 3 章的分布式、第 5 章的收集,都是被这一篇里的数字逼出来的。
一、数据不是凭空冒出来的
二十年前,一家店一天卖多少东西,记在账本上。一个人一天走多少路,没人记。 现在不一样了。你点一下、划一下、停几秒,都变成一条记录发出去。 不是数据变重要了,是记录这件事变得不要钱了。 教材说的「网络普及带来数据增加」,说的就是这个:传感器、手机、网页、监控、刷卡机, 全在往一个方向写数据。
所以大数据的第一句大实话是:它的量是被「顺手记下来」撑起来的,不是被「认真统计」堆起来的。 这就决定了它的样子——杂、脏、格式不统一。认真统计的表是干净的,顺手记下来的东西不是。
二、多出来的部分是文字、图片和声音
老数据库擅长的是「数」:金额、数量、日期。一行一行,整整齐齐,每一列都有类型。 后来进来的东西不是这样。 客服聊天记录、商品评价、监控视频、录音、扫描件——它们没有列,也没有固定的长度。 这类东西统称非结构化数据。它有两个和老数据完全不同的脾气:一是体积大,二是不能直接算。
「不能直接算」才是最要命的。你可以对金额求和,但你不能对一段话求和。要算它,得先把它变成数。 把文字变成数,就是教材 1.2 节提到的机器学习要干的事:分词、提特征、算相似度。 所以「非结构化数据多」和「机器学习火」是同一件事的两面。
三、先算一笔体积账
演示里造了 20 万条订单,每条四个字段,用三种方式存:CSV 文本、JSON 行、紧凑结构体。 结果:结构体每条 16 字节,CSV 20 字节,JSON 48 字节。 JSON 是结构体的三倍,多出来的全是字段名和花括号。 这里只有四个短字段。 真业务表二三十个字段、字段名又长, JSON 相对结构体的倍数还要继续拉大——体积的浪费是按字段数线性叠加的。
不过别急着下结论。演示的第三段做了压缩,结论翻了个面。
四、压过之后,文本反超了
把三份数据各自 zlib 压一遍:CSV 剩下 24%,JSON 剩下 17%,结构体剩下 35%。 不压缩时结构体最省,压过之后 CSV 反而比结构体更小。 为什么会这样?压缩算法吃的是重复。CSV 里那些逗号、「,99.0」这样的片段反复出现,建个字典就压没了; 结构体是紧凑二进制,本来就没有冗余,算法几乎无从下手。
这是本课第一个反直觉的结论,值得记牢:「原始体积」和「落盘体积」是两笔账。讨论存储成本时, 先问一句「压不压」。 第四段更狠:一张 100KB 的随机图片,压完 100041 字节——不但没小,还多了 41 字节, 那是压缩头自己的开销。 文字能压是因为有规律,照片压不动是因为每个像素都是噪声。 非结构化数据「贵」,就贵在这儿:又大,又压不动。
五、多到什么程度就得换机器
演示最后算了一笔:一条日志 2KB,一天一亿条就是 190GB。 一天还好。一个月 5.59TB,一年 67.99TB。一块 8TB 的硬盘只装得下 42 天。 而这才是一种日志。真实系统里几十种数据源同时在写,单机的天花板不是算力,是盘位。
这就是分布式处理被逼出来的原因。 第 3 章要讲的三件事——分布式存储、分布式计算、分布式网络——解决的都是「一台不够用」。 但先别跳到那儿。下一篇先看系统整体长什么样:数据从进来、到用上,中间要过几道手。
六、动手 Demo:同一条记录,三种存法,三种体积
造 20 万条订单,分别存成 CSV、JSON、结构体,比字节数。 再压一遍,看结论怎么翻面。 最后换成一天一亿条日志的真实规模,算要多少硬盘。
import struct, zlib
rows = [(100000 + i, 5000 + (i * 37) % 900, 1 + i % 9,
99.0 + (i % 400)) for i in range(200000)]
b_csv = ''.join('%d,%d,%d,%.1f\n' % r for r in rows).encode()
b_json = ''.join('{"order":%d,"user":%d,"qty":%d,"amt":%.1f}\n'
% r for r in rows).encode()
st = struct.Struct('<iiif') # 小端,三个 int 一个 float
b_bin = b''.join(st.pack(*r) for r in rows)
for name, b in (('CSV', b_csv), ('JSON', b_json), ('结构体', b_bin)):
c = zlib.compress(b, 6)
print('%-6s %8d 字节 压后 %8d %.0f%%'
% (name, len(b), len(c), 100.0 * len(c) / len(b)))
实跑输出:
--------------------------------------------------------------------
数据从哪来:三种存法,体积差多少
--------------------------------------------------------------------
① 先造 20 万条订单
--------------------------------------------------------------------
条数:200000
第一行:(100000, 5000, 1, 99.0)
造数据花了 0.1119 秒
一行订单就四个数。业务系统里它是四条记录,在这里它是四个格子。
② 同一批数据,三种存法
--------------------------------------------------------------------
| 存法 | 总字节 | 每条 | 写入耗时 |
|---|---:|---:|---:|
| CSV 文本 | 3999500 | 20.0 | 0.165 秒 |
| JSON 行 | 9799500 | 49.0 | 0.188 秒 |
| 结构体 | 3200000 | 16.0 | 0.088 秒 |
结构体一条只占 16 字节:三个整数各 4 字节,一个单精度浮点 4 字节。
字段名省了、逗号省了、小数点也省了。
JSON 一条 48 字节,是结构体的 3.1 倍——多出来的全是字段名和花括号。
这里只有四个短字段,真业务表二三十个字段,倍数还要更大。
③ 再压一压
--------------------------------------------------------------------
| 存法 | 压缩前 | 压缩后 | 剩下 |
|---|---:|---:|---:|
| CSV 文本 | 3.81 MB | 953.2 KB | 24% |
| JSON 行 | 9.35 MB | 1.59 MB | 17% |
| 结构体 | 3.05 MB | 1.07 MB | 35% |
这里出了个反直觉的结果:不压缩时结构体最省,压过之后 CSV 反而更小。
原因是压缩算法专吃重复。CSV 里那一堆逗号和「,99.0」的重复片段,字典一建就没了;
结构体本来就没有冗余,算法几乎无从下手。
所以「存得省不省」要看最后落在盘上的是哪一份。直接存裸结构体省,存压缩后的文本更省——这是两笔账。
④ 非结构化数据:更占地方,还压不动
--------------------------------------------------------------------
一段客服文字:720 字节 -> 压缩后 131 字节
一张 100KB 的图:100000 字节 -> 压缩后 100041 字节
| 类型 | 原始 | 压缩后 | 剩下 |
|---|---:|---:|---:|
| 重复的中文句子 | 720 字节 | 131 字节 | 18% |
| 一张照片 | 97.7 KB | 97.7 KB | 100% |
文字有规律,压缩算法能认出重复;
照片的每个像素都是随机噪声,压完几乎没变小——你看那 41 字节,是压缩头自己的开销。
贵在两处:占地方,而且没法直接塞进一张表的某一列——你要先把它变成数,才能拿去算。
⑤ 换成真实规模算一算
--------------------------------------------------------------------
一条日志 2048 字节,一天一亿条就是 190.73 GB
一个月(30 天):5.59 TB
一年(365 天):67.99 TB
一块 8TB 硬盘能装 42 天。
看到问题了:一天 190GB 单机还扛得住,一年 68TB 就不行了。
而且这才是一种日志,真实系统里有几十种数据源同时在写。
这就是为什么单机数据库不够用:不是算不动,是装不下。
装不下就得拆到多台机器上——分布式处理是被数据量逼出来的。
第一段的数字要看清单位:结构体每条 16 字节,是因为三个 int 各 4 字节、一个单精度 float 4 字节, 一点空隙都没留。 CSV 每条 20 字节,只比结构体多 4 字节——因为有小数点、逗号和换行。 JSON 每条 48 字节,多出来的四十多字节全是字段名。 第三段那个反转是本篇最该记住的地方:压缩之后 CSV 反超结构体。 所以「哪种格式省地方」这个问题,不问清「压不压」就没法回答。 第四段用的 os.urandom 生成的是真随机字节,压不动。 换成一张真照片也压不动多少,原理一样:像素之间没有可预测的重复。
30 秒实验
- 把 N 从 200000 改成 2000,重跑一遍,看三种存法的「每条字节」变没变。 (应该几乎不变——它是个固定开销,跟数据量无关。)
- 把金额改成保留两位小数(
%.2f),再跑,看 CSV 每条多几个字节。 - 把
zlib.compress(b, 6)的 6 改成 1 和 9,各跑一次,看压缩率和耗时怎么变。
常见误区
- 「数据多就是好事。」数据多只带来成本。 值钱的是能从里面问出答案,不是硬盘里躺了多少 TB。
- 「非结构化数据就是没法处理的数据。」不是没法处理,是要先转成数。 图片转成向量、文字转成词频,都能算。
- 「换成二进制格式一定更省空间。」演示里压缩过之后 CSV 反而更小。 没有脱离场景的「最优格式」,只有「在这套压缩算法下谁更小」。
自测三问(点开对答案)
1. 同样 20 万条记录,JSON 每条 48 字节、结构体每条 16 字节。为什么 JSON 会大三倍? —— 因为 JSON 把字段名(order、user、qty、amt)和花括号引号都写进了每一条里。 字段名在四十多条里重复出现,只是文本上看不出来。 2. 既然结构体最紧凑,为什么压缩之后反而比 CSV 大? —— 压缩算法吃的是重复。CSV 里逗号、小数点、相近的数值凑在一起,字典一建就压掉大半; 结构体是紧凑二进制,没有可压的冗余,所以「原始小」不等于「压完还小」。 3. 一张 100KB 的图片压完变成 100041 字节,说明压缩算法坏了吗? —— 没有。随机噪声没有可压缩的规律,算法只能原样存,再多加几十字节的头。 这恰恰说明**「压不动」本身就是一种信息**:这份数据没有冗余。下一篇看系统的整体结构:数据从进来、到用上,中间要走哪三段。
- ☐ (本篇完,下一篇 1.2 大数据系统的三段路)