📊 第5章 大数据的收集
📄 一句话概括

队列是缓冲:容量 1 时生产者被堵 1.51 秒,容量 100 时堵 0.00 秒。 但总耗时没变,还是 2.25 秒——队列能解耦快慢,不能凭空变快。 真正的解法是加消费者:1 个 10.40 秒、2 个 5.30 秒、4 个 2.77 秒。

📝 本章和前面几章的关系

上一篇讲批量的取数。这一篇讲流的落地方式:队列。 教材 5.7、5.8、5.9 三节:分布式队列与流处理、流数据收集中的分布式队列、生产者、 分布式队列和消费者。 三节讲的是一件事:上游和下游速度不一样,中间放个能堆东西的地方。

一、没有队列会怎样

演示第一段:生产者每 0.01 秒产一条,消费者每 0.05 秒收一条,队列容量 1。 总共 40 条,跑了 2.25 秒。其中生产者有 1.51 秒是堵在 put 上的。 意思很直白:上游产得快,下游跟不上,上游就得停下来等。

这叫背压。它是好事——不会丢数据;也是坏事——上游被拖慢。 真实系统里,上游可能是一个接口、一个传感器、另一个服务。让它停下来等,往往意味着用户那边卡住了。 所以几乎所有的数据管道中间都会放一个队列。

二、队列的作用是解耦,不是加速

演示第二段:同样速率,容量改成 100。 生产者不再被堵(0.00 秒),它一口气把 40 条塞进队列就走了,只花 0.40 秒。 但总耗时还是 2.25 秒——消费者该多慢还是多慢。

这是最容易误解的一点:队列只是把「等待」从生产者挪到了队列里。它改变的是「谁在等」, 不是「等多久」。 所以加队列能让上下游各自按自己的节奏跑,但不能让整体变快。 整体速度永远受最慢那一段限制——这就是第 3.1 篇讲的瓶颈。 第三段验证了这一点:跑 200 条、容量 100,前 100 条塞得很痛快,塞满之后生产者又开始等(2.87 秒)。 总耗时 10.40 秒。

三、真正的解法是加消费者

演示第四段:消费者从 1 个加到 2 个、4 个。 总耗时 10.40 秒 → 5.30 秒 → 2.77 秒。差不多是 1 : 1/2 : 1/3.75。 这才叫扩容——增加处理能力,不是增加等待的地方。

分布式队列(Kafka、RabbitMQ 那类)的价值就在这儿:它可以有几十个消费者同时从里面取, 而且分布在不同机器上。 一个队列能挂多少消费者,取决于它怎么分区。Kafka 的并发上限就是分区数——一个分区只能被一个消费者读。 所以要提并发,先加分区。而分区一多,顺序就没法全局保证了。

四、顺序和并发是一对矛盾

如果所有消费者都从同一个队列里抢,谁抢到算谁的——顺序就乱了。 要顺序,就得按 key 分区:同一个 key 的消息进同一个分区,同一个分区只让一个消费者处理。 比如按用户 ID 分区:同一个用户的订单一定是按顺序处理的,不同用户之间可以并行。

这个设计叫「分区内有序」。它是绝大多数流系统的默认承诺——不要指望全局有序,那是拿并发换来的。 选分区键是流系统设计里最关键的决策之一。 选得不好,会出现「热点分区」:某个 key 的消息特别多,那一个消费者累死,其他闲着。 这跟第 3.3 篇讲的分片倾斜是同一件事,只是发生在消息上。

五、动手 Demo:容量 1、容量 100、四个消费者,各快多少

一个生产者产、一个消费者收,先看队列容量对上游阻塞的影响。 再看数据量超过容量之后会怎样。 最后加消费者,看总耗时怎么降。

import queue, threading, time

def producer(q, n, gap):
    for i in range(n):
        q.put(i)          # 队列满的时候会阻塞在这里
        time.sleep(gap)

def consumer(q, cost, stop):
    while True:
        try:
            q.get(timeout=0.2)
        except queue.Empty:
            if stop[0]:
                break
            continue
        time.sleep(cost)

q = queue.Queue(maxsize=1)     # 换成 100 再跑一遍

实跑输出:

--------------------------------------------------------------------
队列是缓冲:生产者不用等消费者
--------------------------------------------------------------------

  ① 先跑一个没有缓冲的
  --------------------------------------------------------------------
  1 个生产者每 0.01 秒产一条,1 个消费者每 0.05 秒收一条。
  队列容量 1(相当于没有缓冲),一共 40 条。
  总耗时 2.25 秒,生产者有 1.51 秒是堵在 put 上的
  生产者产得快(0.01 秒一条),消费者慢(0.05 秒一条)。队列只有 1 格,塞满了就得停下来等。
  生产者的速度被消费者的速度绑住了。
  这叫背压,是好事也是坏事:好处是不会丢数据,坏处是上游被拖慢。

  ② 换成有缓冲的
  --------------------------------------------------------------------
  同样的速率,队列容量改成 100。
  总耗时 2.25 秒,生产者有 0.00 秒是堵在 put 上的

| 队列容量 | 总耗时 | 生产者被堵的时间 |
|---|---:|---:|
| 1(无缓冲) | 2.25 秒 | 1.51 秒 |
| 100 | 2.25 秒 | 0.00 秒 |

  生产者不再被堵——它一口气把 40 条全塞进队列就走了,总共只花 0.40 秒(40 × 0.01)。
  但总耗时没变,还是被消费者拖着。队列能解耦「快慢」,不能凭空变快。
  这句话很重要:加队列只是把等待的地方从「生产者」挪到了「队列里」,消费能力不够,队列迟早被填满。

  ③ 队列被填满之后会怎样
  --------------------------------------------------------------------
  还是 0.01 对 0.05,容量 100,但这次要跑 200 条。
  总耗时 10.40 秒,生产者被堵了 2.87 秒
  消费者要 200 × 0.05 = 10 秒,生产者只要 2 秒
  前 100 条塞得很痛快,塞满之后生产者就开始等了。队列只是把「马上等」变成「过一会儿等」。

  ④ 加消费者才是解法
  --------------------------------------------------------------------

| 消费者个数 | 总耗时 |
|---|---:|
| 1 个 | 10.40 秒 |
| 2 个 | 5.30 秒 |
| 4 个 | 2.77 秒 |

  消费者从 1 个加到 2 个,时间减半;加到 4 个,接近四分之一。
  这才叫扩容——增加处理能力,不是增加等待的地方。
  分布式队列(Kafka 那类)的价值就在这儿:它可以有几十个消费者同时从里面取,
  而且消费者可以在不同的机器上。
  但它也有代价:消费者一多,顺序就没法保证了。
  要顺序就得按 key 分区,同一个 key 只让一个消费者处理。

第一段那个 1.51 秒很说明问题:整个 2.25 秒里,三分之二的时间生产者是被堵住的。 它不是在干活,是在等。 第二段「总耗时没变」是这一篇的核心。 很多人加队列是为了「变快」,其实队列解决的是「不被拖住」,不是「变快」。 第三段验证了「队列只是一次性缓冲」:200 条塞进 100 格,塞满之后该等还是等。 最后那张表里 4 个消费者是 2.77 秒,不是理论上的 2.6 秒——因为线程调度、锁、队列本身都有开销。 并行永远不会是线性的。

30 秒实验

  1. 把消费者 cost 从 0.05 改成 0.02(比生产者快),看生产者还堵不堵。
  2. 把队列容量从 100 改成 10000,重跑 200 条,看生产者被堵的时间变成多少。
  3. 把消费者个数改成 8,看总耗时还降不降——为什么会有下限?

常见误区

  1. 「加个队列就能解决问题。」队列只是缓冲。消费能力不提升,队列迟早被填满。
  2. 「消费者越多越快。」到一定程度会被 CPU、锁、队列本身限制住,而且顺序没法保证。
  3. 「消息队列保证顺序。」多数系统只保证分区内有序。 要全局有序,只能让一个消费者读,等于放弃并发。
自测三问(点开对答案) 1. 队列容量从 1 改成 100,为什么总耗时没变? —— 因为总耗时由最慢的那一段(消费者)决定。队列只是让生产者不用等着,消费者的速度一点没变。 2. 为什么加了 4 个消费者,速度不是严格的 4 倍? —— 线程调度、锁竞争、队列操作本身都有开销。 而且消费者之间会抢同一把锁,并行度越高,这些开销占比越大。 3. Kafka 的并发上限由什么决定? —— 分区数。一个分区同一时刻只能被一个消费者读,所以最多只能有「分区数」个消费者同时工作。

下一篇讲数据结构会变:字段改名、加字段、改语义,下游怎么办。

相关笔记