📊 第1章 大数据分析概述
📄 一句话概括

数据变多不是错觉:网络把行为都记下来了,而且记下来的多数是文字、图片、声音。 这些非结构化数据又占地方又压不动,还塞不进表的某一列。 体积这件事要真算:同一条记录,三种存法差三倍;压过之后差距还会反转。

📝 本章和前面几章的关系

这是全书第一篇。不假设你搭过集群,只要求你会四则运算。 教材第 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 秒实验

  1. 把 N 从 200000 改成 2000,重跑一遍,看三种存法的「每条字节」变没变。 (应该几乎不变——它是个固定开销,跟数据量无关。)
  2. 把金额改成保留两位小数(%.2f),再跑,看 CSV 每条多几个字节。
  3. 把 zlib.compress(b, 6) 的 6 改成 1 和 9,各跑一次,看压缩率和耗时怎么变。

常见误区

  1. 「数据多就是好事。」数据多只带来成本。 值钱的是能从里面问出答案,不是硬盘里躺了多少 TB。
  2. 「非结构化数据就是没法处理的数据。」不是没法处理,是要先转成数。 图片转成向量、文字转成词频,都能算。
  3. 「换成二进制格式一定更省空间。」演示里压缩过之后 CSV 反而更小。 没有脱离场景的「最优格式」,只有「在这套压缩算法下谁更小」。
自测三问(点开对答案) 1. 同样 20 万条记录,JSON 每条 48 字节、结构体每条 16 字节。为什么 JSON 会大三倍? —— 因为 JSON 把字段名(order、user、qty、amt)和花括号引号都写进了每一条里。 字段名在四十多条里重复出现,只是文本上看不出来。 2. 既然结构体最紧凑,为什么压缩之后反而比 CSV 大? —— 压缩算法吃的是重复。CSV 里逗号、小数点、相近的数值凑在一起,字典一建就压掉大半; 结构体是紧凑二进制,没有可压的冗余,所以「原始小」不等于「压完还小」。 3. 一张 100KB 的图片压完变成 100041 字节,说明压缩算法坏了吗? —— 没有。随机噪声没有可压缩的规律,算法只能原样存,再多加几十字节的头。 这恰恰说明**「压不动」本身就是一种信息**:这份数据没有冗余。

下一篇看系统的整体结构:数据从进来、到用上,中间要走哪三段。

相关笔记