bigywater.tech
数据编码与压缩 · 22

场景二:Redis 里的任务体与页面缓存,小对象怎么压

爬虫的任务队列和页面缓存都放在 Redis 里,内存是钱。任务体该用什么格式,压不压缩,打包多大,缓存的页面要不要压缩?这篇在本机独立的 Redis 上逐项量内存、吞吐和网络流量。

392.9 对 52.8 字节
队列里一个任务占的 Redis 内存:JSON 与 JSON + zstd-3 + 字典
68.7 对 14.9 MiB
缓存 5000 个页面:原样存放与 zstd-3 + 字典
265.8 对 36.1 字节
一个任务占的内存:每元素 1 个任务与每元素 50 个任务(整体 zstd-3)

任务体

实测 · 本机执行输出
  任务数 100000;紧凑 JSON 平均 376 字节

任务里很多内容是每个任务都相同的(UA、语言、referrer)。直接把常量字段拿出任务体,JSON 就从 392.9 降到 183.1 字节/任务;换 Protobuf 是 265.8;JSON 加 zstd 字典是 52.8。字典能把这些重复统统吃掉。

吞吐

实测 · 本机执行输出
  编码                                 平均载荷   Redis 内存增量     每任务内存  其中 Redis 自身开销      网络入流量
  JSON                              376.1       37.5MiB    392.9B         16.8B     38.7MiB
  JSON(常量字段不进任务体)                   176.1       17.5MiB    183.1B          7.1B     19.7MiB
  MessagePack                       332.1       32.7MiB    343.3B         11.3B     34.5MiB
  Protobuf                          258.3       25.4MiB    265.8B          7.5B     27.5MiB
  pickle(只在可信环境用)                   389.1       39.3MiB    412.0B         22.9B     40.0MiB
  JSON + zstd-3                     258.2       25.4MiB    265.8B          7.6B     27.5MiB
  JSON + zstd-3 + 字典                 50.3        5.0MiB     52.8B          2.6B      7.6MiB
  Protobuf + zstd-3 + 字典             51.2        5.1MiB     53.8B          2.6B      7.7MiB
  JSON + lz4                        326.3       32.7MiB    343.3B         17.0B     34.0MiB

MessagePack 和 Protobuf 的编码解码比 JSON 快;压缩加字典的 JSON 入队 128789 个/秒,和纯 JSON(138308)接近。在 Python 里瓶颈是编码解码和 Redis 往返,不是压缩本身。

打包

实测 · 本机执行输出
  JSON                           入队   138308 个/秒;出队 + 解码   153853 个/秒
  MessagePack                    入队   214950 个/秒;出队 + 解码   199171 个/秒
  Protobuf                       入队   178133 个/秒;出队 + 解码   238389 个/秒
  pickle(只在可信环境用)                入队   195311 个/秒;出队 + 解码   194645 个/秒
  JSON + zstd-3                  入队    94576 个/秒;出队 + 解码   126820 个/秒
  JSON + zstd-3 + 字典             入队   128789 个/秒;出队 + 解码   151041 个/秒
  Protobuf + zstd-3 + 字典         入队   154402 个/秒;出队 + 解码   216982 个/秒
  JSON + lz4                     入队   125481 个/秒;出队 + 解码   144472 个/秒

把 N 个任务打包成一个元素再压缩,每任务内存随 N 增大迅速下降(1 个 265.8,10 个 63.4,50 个 36.1,200 个 31.0 字节)。但租约、重试、去重都以元素为粒度,崩溃时要重做整包(见第 66 篇)。

缓存页面

实测 · 本机执行输出
      每元素任务数     元素数   Redis 内存增量     每任务内存
           1  100000       25.4MiB    265.8B
          10   10000        6.0MiB     63.4B
          50    2000        3.4MiB     36.1B
         200     500        3.0MiB     31.0B
  (元素里装的任务越多,每个任务的内存越小,但一个 worker 取走一个元素就是取走 N 个任务,崩溃时要重新入队 N 个;租约、重试、去重都以元素为粒度)

13 KiB 的页面原样存 68.7 MiB(5000 页),lz4 29.6 MiB,zstd-3 20.5 MiB,zstd-3 加字典 14.9 MiB;读取加解压的吞吐 lz4 最快,zstd 约 46000/秒,brotli q5 只有 24219/秒。缓存适合 zstd-3 或加字典,生成一次读很多次。

Redis 自身开销

实测 · 本机执行输出
  存储方式               平均值大小    Redis 内存增量         写入        读取 + 解压
  原样                 13907        68.7MiB    41961/秒       59061/秒 (解压校验通过)
  lz4                 5862        29.6MiB    73115/秒       73856/秒 (解压校验通过)
  zstd-3              4013        20.5MiB    75343/秒       46750/秒 (解压校验通过)
  zstd-3 + 字典         2903        14.9MiB    91079/秒       46016/秒 (解压校验通过)
  brotli q5           3891        19.8MiB    83252/秒       24219/秒 (解压校验通过)

同样 300 字节,列表元素额外占 16.9 字节,单独的 key 67.3,哈希 field 71.3。把小对象放进列表比单独的 key 省内存。

什么时候别压

实测 · 本机执行输出
  列表元素 RPUSH q           每个元素共占  316.9 字节(载荷 300 字节,额外  16.9 字节)
  单独的 key SET k:i        每个元素共占  367.3 字节(载荷 300 字节,额外  67.3 字节)
  哈希 field HSET h        每个元素共占  371.3 字节(载荷 300 字节,额外  71.3 字节)
  集合成员 SADD s            每个元素共占  341.1 字节(载荷 300 字节,额外  41.1 字节)

任务体已经很小(比如只有 url),压缩的收益有限,而每个任务加字典后要保证 worker 和队列里的字典 ID 一致,版本升级时队列里还有旧字典压的任务,要继续能解(见第 17 篇)。

分步演示

队列与缓存
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8(系统 Python,自带 compression.zstd)/ zstandard 0.25.0 / lz4 4.4.5 / zstd、lz4、brotli、protoc 命令行(Homebrew);msgpack、protobuf、snappy 等第三方库临时装在临时目录里,用完删除;所有数据都是本机脚本生成的合成数据(固定随机种子),没有使用任何真实网站或用户的数据;计时用 time.perf_counter,取多次中位数,数字每次运行会有小幅抖动。redis-server 8.10.2 独立实例(只监听 127.0.0.1 的 9700 以上端口,关闭持久化,实验后关闭;没有碰本机已有的 Redis)、redis-py 8.0.1;任务体由固定种子合成(UA、语言、referrer 是常量,与真实爬虫的任务体相比重复度偏高,字典的收益是上界);Redis 内存用 INFO memory 的 used_memory 差值(包含分配开销);吞吐是 3 次取中间值,单线程 pipeline 每批 500;网络入流量用 INFO stats 的 total_net_input_bytes
    • 没有验证:真实爬虫任务的重复度;列表以外的结构(sorted set 的优先队列、Streams)的内存开销;Redis 开启 AOF/RDB 持久化后的情况;复制到副本时的网络字节;大 value 对 Redis 单线程延迟的影响(第 67 篇看过大 key 的删除);客户端压缩与 Redis 模块(如 RedisJSON)的比较
    京ICP备2026067502号-1京公网安备11010502063487号