Zach Tellman - Everything Will Flow

Zach Tellman - Everything Will Flow — ClojureTV

ClojureTV

AI-assisted summary of Zach Tellman - Everything Will Flow — original source. Check the original for full context.

Source published:

Key ideas

  • Unbounded in-memory queues are fundamentally broken because production systems are open systems where arrival rates cannot be controlled, inevitably leading to catastrophic out-of-memory crashes. (12:34)
  • Scaling out worker capacity and arrival rates proportionally does not prevent system failure; it merely tightens response-time variance and masks instability right up until catastrophic collapse. (9:46)
  • Buffers do not decouple execution time—an unbuffered rendezvous channel already does that; their true purpose is serving as shock absorbers that mask the latency of pausing and resuming backpressure. (15:13)
  • Architectures must centralize drop or reject policies at application edges while preserving upstream backpressure throughout internal pipelines to prevent silent data loss or cascading outages. (17:42)

Detailed notes

## Background Published on YouTube by ClojureTV on April 22, 2015, this talk features Zach Tellman examining the operational complexities of queue-driven architectures. According to the video description, queues unlock problem spaces unreachable by pure immutable primitives, but naive implementations in libraries like core.async risk building brittle systems, making an understanding of queueing theory essential for practical Clojure engineering.

## TL;DR Tellman demonstrates that modern software is layered with queues coordinating execution across thread pools, operating systems, and network boundaries. Through mathematical simulations, he proves that open systems face abrupt, rocket-like latency collapses when traffic spikes, and that adding consumers merely masks this variance until catastrophic failure occurs. To prevent memory exhaustion, architectures must replace unbounded queues with backpressure, explicit rejection, or dropping policies at application boundaries. Using case studies from high-throughput services and real-time chat servers, he shows how to protect systems using durable disk queues, token-bucket throttling, instrumented thread pools, and stream topology inspection.

## Atmosphere & style The presentation feels like a sobering, battle-hardened masterclass delivered with deadpan pragmatism and intellectual clarity. Tellman builds an engaging narrative arc that transitions from theoretical probability distributions to live simulation spectrograms and painful real-world post-mortems of cascading cloud outages. His delivery is steady, unpretentious, and urgent, creating an atmosphere that strips away architectural wishful thinking to urge developers out of willful ignorance and into rigorous systems engineering.

## Bulletin - [12:34](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=754s) Unbounded in-memory queues are fundamentally broken because production systems are open systems where arrival rates cannot be controlled, inevitably leading to catastrophic out-of-memory crashes. - [09:46](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=586s) Scaling out worker capacity and arrival rates proportionally does not prevent system failure; it merely tightens response-time variance and masks instability right up until catastrophic collapse. - [15:13](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=913s) Buffers do not decouple execution time—an unbuffered rendezvous channel already does that; their true purpose is serving as shock absorbers that mask the latency of pausing and resuming backpressure. - [17:42](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1062s) Architectures must centralize drop or reject policies at application edges while preserving upstream backpressure throughout internal pipelines to prevent silent data loss or cascading outages.

## Key takeaways - [00:00](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=0s) Queues fundamentally orchestrate side effects by separating what needs to happen from when it happens, moving beyond pure, immutable functional transformations. - [01:04](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=64s) Modern execution stacks nest roughly half a dozen queues between application channels, thread pool blocking queues, OS run queues, and hardware CPU cores. - [03:32](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=212s) Open systems are inherently unstable compared to closed systems because producers generate requests independently without waiting for consumer availability. - [08:50](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=530s) Queues under growing load experience a rocket-like latency spike where queuing delay completely dominates total execution time. - [09:46](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=586s) Adding consumers narrows latency variance and creates an illusion of safety, but multi-consumer systems still fail at the exact same load threshold as single consumers. - [12:34](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=754s) When arrival rates outstrip processing capacity, systems have only three fundamental options: drop, reject, or apply backpressure. - [15:13](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=913s) Buffers are unnecessary for temporal decoupling; an unbuffered channel safely coordinates handoffs, while buffers exist primarily to amortize backpressure signaling delays. - [17:42](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1062s) Chaining buffered queues in series compounds and magnifies downstream latency, making end-to-end performance unpredictable. - [22:10](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1330s) Relying on implicit in-memory buffering in third-party clients during downstream outages cascades quickly into JVM out-of-memory crashes across entire clusters. - [23:55](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1435s) Decoupling network persistence by appending directly to local disk queues isolates external storage outages and keeps ingestion endpoints available. - [26:49](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1609s) Instrumenting thread pools with quantile distributions rather than scalar averages is essential to expose tail latency and task rejection spikes under load. - [31:25](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1885s) Chat rooms and pub-sub systems require size-bounded output buffers with disconnection timeouts to prevent slow subscribers from stalling shared distribution streams. - [35:37](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=2137s) Inbound flooding should be countered via transport-level TCP backpressure combined with token-bucket burst limits rather than complex disconnection logic. - [37:18](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=2238s) Dynamic asynchronous architectures require stream-level introspection to track shifting consumer topologies and avoid willful operational blindness. - [39:20](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=2360s) All queues are physically bounded by host memory; robust software requires explicit edge policies, universal backpressure in middleware, and deep instrumentation.

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=0s) Queues, Side Effects, and the Clojure Ecosystem

Queues appear deceptively elementary on the surface: a producer enqueues messages, a consumer dequeues them—usually, though not always, in FIFO order—and that is essentially the whole mechanism. However, an often-overlooked property of queues is their inherently side-effecting nature. When an enqueue operation completes, the caller receives only an acknowledgment that the item was accepted rather than a pure transformation of the input data. The data placed in a queue exists to describe an action, making queues fundamentally a mechanism for orchestrating side effects.

Because of this focus on side effects and state transitions, queues were long relegated to the sidelines of the Clojure community. Developers historically relied on standard Java concurrency utilities, wrapping or abstracting over them whenever possible rather than treating them as first-class primitives. That changed dramatically with the introduction of `core.async`, which brought queueing and asynchronous coordination straight into mainstream Clojure application architecture. Zach Tellman argues that embracing queues is essential for real-world software engineering:

> “we of course are in the business of making computers do things right we don't just raise the ambient temperature of the room”

[01:04](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=64s) Queues All the Way Down: Core.async, Thread Pools, and Hardware

Examining the machinery beneath modern concurrency primitives reveals layers of nested queues. A single `core.async` channel does not contain just one queue; it manages three separate ones: a buffer that stores the payload messages, a puts queue where producers wait when attempting to insert into a full buffer, and a takes queue where consumers wait when attempting to extract from an empty buffer. Even after a producer successfully delivers a message to a consumer, the handoff is still incomplete. The consumer attaches a callback to execute work against that message, and running that callback requires dispatching it to a thread pool.

A Java thread pool inserts another queue—a `BlockingQueue`—between tasks requesting execution and the worker threads servicing them. By default, thread pools scale dynamically: when a caller attempts to place work on a full queue, the pool instantiates a new worker thread until reaching its configured ceiling. If that limit is exceeded, the pool abruptly throws a `RejectedExecutionException`. Internally, the `BlockingQueue` mirrors `core.async`: it holds a message buffer alongside two condition queues named `notFull` and `notEmpty`. These conditions do not transmit data; they simply provide parking locations where blocked threads release their locks and sleep until a single thread is signaled to wake up. Further down the operating system stack, software threads queue on the kernel scheduler before finally waiting to execute on physical hardware threads and CPU cores.

```mermaid flowchart TD accTitle: Layers of Nested Queues accDescr: Diagram showing how a message travels across channel queues, thread pools, and OS hardware queues.

subgraph Channel["core.async Channel"] Puts["Puts Queue"] Buf["Message Buffer"] Takes["Takes Queue"] end

subgraph ThreadPool["Java Thread Pool"] BQ["BlockingQueue\n(notFull / notEmpty)"] Workers["Worker Threads"] end

subgraph OS["Operating System & Hardware"] Sched["OS Run Queue"] Cores["CPU Cores"] end

Puts --> Buf Buf --> Takes Takes --> BQ BQ --> Workers Workers --> Sched Sched --> Cores ```

Between high-level application logic and the underlying silicon, roughly half a dozen distinct queues coordinate execution. Tellman emphasizes that this omnipresence stems from a core operational benefit:

> “and the reason that cues are so ubiquitous is because they separate what we want to have happen uh from when it happens and this is really valuable because most of the time when we write code we don't know what else is going on”

- `core.async` channels rely on three internal queues: a message buffer, a waiting-producers queue (`puts`), and a waiting-consumers queue (`takes`). - Java thread pools expand when their underlying `BlockingQueue` fills up, raising a `RejectedExecutionException` once thread limits are breached. - Java condition queues (`notFull` and `notEmpty`) coordinate thread parking and single-thread wakeups without passing data payloads. - Software threads are queued once more by the OS scheduler before mapping onto physical processor cores.

[03:32](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=212s) Queuing Theory and Closed vs. Open Systems

Because queues mediate nearly every layer of modern software execution, understanding how they behave, degrade, and fail under heavy load is critical. Queuing theory provides the formal mathematical foundation for these questions, though it belongs primarily to the domain of statistics rather than everyday software engineering. Approaching the discipline requires filtering theoretical statistical proofs down to practical mental models that inform operational intuition. For developers seeking to explore the field further, Tellman recommends the academic literature—noting that while the best available textbooks are roughly 90% pure probability theory with only sporadic mentions of computing systems, the insights gained can profoundly shape architectural instincts.

A foundational architectural distinction within queuing theory is the boundary separating closed systems from open systems:

- **Closed Systems**: A producer must wait for the consumer to finish processing the current unit of work before producing another. Classic examples include a REPL—which must read input, evaluate it, and print output before prompting the user again—or an individual browsing a website, where the user must receive the rendered page before clicking another link. Because control yields cooperatively back and forth, closed systems are inherently stable and straightforward to reason about. - **Open Systems**: Producers emit requests without any central coordination or awareness of consumer availability. Incoming tasks arrive continuously regardless of whether the processing workers are occupied.

Tellman explains that open systems are the primary source of failure in real-world infrastructure:

> “and closed systems are uh certainly a thing that we deal with and they are as it turns out fairly easy to reason about but many of the things that give us trouble and many of the systems that we build are open systems where there is no coordination where when a request comes in we're processing it it's almost a given that a request is to come in before we're ready”

[06:01](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=361s) Simulating Open Systems: Arrival Patterns and Latency Spectrograms

Production metrics such as Amazon Web Services dashboard graphs contain too many confounding variables to illustrate the core mathematical dynamics of open systems. To isolate these behaviors, Tellman constructs a software simulation. In this model, request arrival times follow an exponential distribution, where the interval between successive arrivals varies stochastically. Empirically, exponential distributions accurately reflect uncoordinated human behaviors, such as people lining up at bank tellers, visiting food trucks, or competing for shared public resources. Even if an endpoint sustains an average rate of 1,000 queries per second, requests do not arrive at neat, uniform 1-millisecond intervals; instead, they arrive in unpredictable bursts that pile up on one another, separated by intermittent idle periods.

While arrival intervals match an exponential distribution, task execution complexity follows a Pareto distribution. This power-law distribution models varying tail lengths (comparable to the familiar 80/20 rule), allowing the simulation to modulate the proportion of unusually complex, expensive operations entering the pipeline. To mirror operational degradation—such as increasing disk saturation or neighbor contention on a multi-tenant cloud virtual machine—the simulation slowly widens the tail over time, shifting simple tasks toward heavier computation.

To capture the resulting system dynamics across time, Tellman uses a spectrogram:

- **Vertical Axis**: Displays end-to-end task latency—the complete duration spanning arrival, queue dwell time, processing, and final completion—plotted on a logarithmic scale to keep extreme tail outliers visible alongside baseline throughput as instability sets in. - **Horizontal Axis**: Tracks the progressive widening of the execution-time distribution, moving smoothly from a thin tail where almost all tasks are trivial to a fat tail where complex outliers become common. - **Heat Map Color Density**: Visualizes probability density, starting on the left with a sharp, intensely colored red band indicating that the vast majority of tasks complete within a tight, predictable latency window before degradations begin to disrupt the system.

[08:50](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=530s) The Rocket-Like Failure Mode of a Growing Queue

Zach Tellman explains how a queue in an open system destabilizes as load creeps up. Looking at the simulation data, about two-thirds of the way through the timeline, the system suddenly jumps to an entirely new equilibrium. On the chart, the red dots scatter several orders of magnitude above where they hovered previously. While this new state is technically an equilibrium and not yet completely out of control, everything operates significantly slower. Eventually, as load continues to pile on, the response times shoot upward like a rocket.

```mermaid flowchart LR accTitle: Unbounded Queue Degradation accDescr: Three progressive stages of an unbounded queue moving from low latency to memory exhaustion. subgraph Stable["Normal Load"] A["Low Latency<br/>Tight Distribution"] end subgraph Saturated["Intermediate Shift"] B["High Latency Equilibrium<br/>Queue Wait Dominates"] end subgraph Collapse["Runaway Failure"] C["Rocketing Latency<br/>OOM Crash"] end A --> B --> C ```

Tellman underscores that this exponential jump is the hallmark of a queue in crisis: “This is an equilibrium we're not out of control yet but everything is significantly significantly slower than it was before and then as it continues we see that it gets more and more out of control until it just sort of takes off like a rocket and this is what happens when a queue grows out of control right this is a system in crisis this is what will cause our system to run out of memory and crash.” Additionally, the distribution of latencies flattens from a tight spread into a very thin, uniform band. Because requests spend almost all their time waiting behind other backlog items rather than being processed, queuing delay dominates total execution time, averaging everything out into a consistent yet painfully slow response profile.

[09:46](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=586s) Scaling Consumers, Variance, and the Illusion of Safety

When systems run out of capacity, the instinctive engineering response is simply to add more machines or worker threads. Adding a second consumer initially allows the system to keep up, delaying the flare-up until near the end of the run. Adding two more consumers makes the system appear virtually unfazed, remaining well within safe operating bounds. However, Tellman shifts to a far less obvious experiment: what happens when consumer capacity and incoming throughput are scaled up together by the exact same proportion? Comparing a 1x arrival rate with 1 consumer, a 4x rate with 4 consumers, and a 16x rate with 16 consumers reveals a startling reality: every single configuration fails at the exact same point in time.

The visual shape of the failure leading up to that cliff edge, however, is radically different. In a 1-consumer setup, an unusually expensive task completely ties up the single worker, causing all subsequent work to pile up immediately behind it. With 4 consumers, an expensive outlier ties up only 25% of the capacity, allowing the remaining three consumers to continue draining the queue and making forward progress. In a 16-consumer setup, it requires an extraordinarily rare confluence of multiple concurrent expensive events to saturate all workers at once. Consequently, the 16-consumer system maintains clean, sub-millisecond response times almost right up to the exact moment it falls off a cliff. Tellman warns that this deceptive dynamic perfectly mirrors modern architectures:

> “And I will point out that this is the best sort of analog for the systems that we build we have multiple machines we have multiple threads and so we can absolutely have a system which is responding in submillisecond time right up to the point where it becomes completely unstable right it happens pretty much all at once.”

The key takeaways from this behavior include: - Production systems are almost always open systems, making them difficult to reason about during traffic spikes. - Long-tail, complex tasks—not the mean or median complexity—are what cause queues to saturate and trigger cascade failures. - Engineers must closely track response time outliers because downstream variance directly dictates whether upstream consumers remain stable or collapse. - Adding consumers does not prevent an open system from being overwhelmed; it merely narrows variance and masks instability until sudden, catastrophic failure occurs.

[12:34](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=754s) Why Unbounded Queues Are Broken: Drop, Reject, or Pause

Because arrival rates will inevitably outstrip capacity at some point, Tellman delivers the central warning of his talk:

> “And so the real lesson here and the one thing that I hope that you take away from this is that unbounded queues are fundamentally broken because it puts the correctness of your system in somebody else's hands right you have no control over how fast the data is showing up and so that means that someone can intentionally or unintentionally cause your system to break and so we cannot have our strategy for dealing with too much data to hold on to it and hope for the best we have to you know do something a little bit more proactive.”

When excess volume overwhelms capacity, systems have only three fundamental options:

| Strategy | Definition | Typical Applications & Drawbacks | |:--- |:--- |:--- | | **Drop** | Discard the incoming data silently without informing the sender. | Valid in telemetry or sensor systems where newer samples render older values obsolete, or in best-effort protocols like StatsD over UDP. Drawback: network crises trigger drops precisely when accurate monitoring is needed most. | | **Reject** | Discard the incoming data and notify the sender that it was refused. | The standard approach at the web layer (e.g., HTTP `503 Service Unavailable`, Twitter's classic "fail whale"). Necessary because servers rarely possess the application context or identity required to process the user later. | | **Pause (Backpressure)** | Signal upstream producers to stop sending until capacity frees up. | The ideal strategy for libraries, frameworks, and internal pipelines. Avoids making application-specific drop/reject guesses by delegating timing control to upstream callers. |

Tellman points out that developers often rely on dropping data far too readily. In monitoring infrastructure using StatsD over UDP, metrics packets are fired blindly with a prayer that they arrive. Yet under heavy network distress, packet loss spikes—meaning accurate telemetry vanishes precisely when operators need it most. Conversely, while rejecting data via HTTP 503 errors or fail whales is unpleasant, application servers lack the state to do anything more graceful; an overloaded e-commerce store cannot tell an anonymous shopper that it will email them once the checkout cart becomes available.

[15:13](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=913s) Backpressure in core.async and the Real Purpose of Buffers

When building lower-level libraries or general-purpose frameworks rather than edge web applications, developers cannot predict whether dropping or rejecting tasks is acceptable. Instead, they must exert backpressure to pause incoming work, deferring operational choices to upstream callers who understand the broader business domain. This architectural necessity explains why Clojure's `core.async` employs three distinct queues internally rather than just a single storage line: it maintains a distinct puts buffer to track operations on which backpressure has been exerted. When a producer attempts to write to a saturated channel, the runtime registers the pending put, holds the execution, and notifies the producer only once consumer capacity frees up.

```mermaid flowchart LR accTitle: Core.async Channel Structure accDescr: Architecture of an unbuffered core.async channel showing puts and takes synchronization. subgraph Channel["Unbuffered core.async Channel"] P["Puts Buffer<br/>(Suspended Producers)"] T["Takes Buffer<br/>(Waiting Consumers)"] end Prod["Upstream Producer"] -->|"put! / >!"| P P -.->|"Direct Handoff<br/>(Rendezvous)"| T T -->|"take! / <!"| Cons["Downstream Consumer"] ```

Queues decouple *what* needs to be done from *when* it is done, allowing bounded hardware resources to handle potentially unbounded external demand. However, Tellman emphasizes that a buffer—the storage container holding pending items—is completely unnecessary to achieve this decoupling:

> “And these are all sort of saying the same thing but the crucial point I want to make is that the buffer right the thing that holds on to the messages is unnecessary for any of these properties right in fact we could just as easily have a channel which has just the puts and the takes just waits to sort of pass the message off once it's available and in fact in core.async this is the default channel configuration it has no buffer.”

An unbuffered channel acts as a closed system. Because an unbuffered channel forces an explicit rendezvous where the producer yields control until a consumer physically accepts the value, it prevents runaway queue growth.

If unbuffered channels provide safety and buffers inherently introduce latency, why use buffers at all? Tellman explains that asserting and releasing backpressure carries a non-trivial physical cost: - Restarting a paused thread requires copying stack frames back into working memory. - Resuming paused TCP connections requires round-trip network signaling before window sizes ramp traffic back up. - Re-engaging paused disk or database readers incurs storage seek and lookup latencies.

The idle time spent waiting between a consumer signaling readiness and the upstream producer actually delivering the next task harms aggregate throughput. Buffers exist not as limitless holding pens for excessive demand, but as shock absorbers that hide backpressure signaling delays and keep workers continuously supplied with work.

[17:42](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1062s) Planning for Excess Data, Backpressure, and the Limits of Buffers

Zach Tellman explains that smoothing out throughput into a higher, stable rate always comes at a direct cost to system latency. Because bursts and surges of volume cannot be avoided in real-world software, engineering for overload must be deliberate rather than accidental. When an influx occurs, a system must possess a clear strategy for coping with excess traffic. Wherever possible, backpressure represents the cleanest, most neutral recourse available: it allows a component simply to signal to its caller that it cannot accept further data, leaving the responsibility of how to handle the rejection to the caller. That signal can travel back up the call chain all the way to the boundary of the application, where edge-level logic can uniformly decide whether to drop packets, shed load, or reject requests, ensuring policy remains centralized rather than scattered indiscriminately through internal components.

While buffering can smooth throughput, its hidden cost remains inflated latency. Consequently, buffers should be introduced sparingly and only when strictly required. Tellman cautions strongly against composing buffered queues in series, as chained buffers tend to compound and magnify downstream delays and unpredictability. Looking at the wider Clojure ecosystem, he observes a troubling disconnect between these queueing principles and actual implementations. While libraries such as `core.async` implement backpressure internally with great care, trouble surfaces when they are connected to underlying networking transports or server interfaces where traffic volume cannot be controlled:

> “And wherever possible we should use backpressure, because that's the most neutral choice that we can make, right? That just kind of tells someone else, ‘I can't handle this data, you figure out what that means.’”

```mermaid flowchart LR accTitle: The Golden Chain of Backpressure accDescr: Incoming network data flows forward while backpressure signals flow in reverse to edge rejection logic. subgraph Edge["Periphery / Edge"] Client["Client / Network Traffic"] Reject["Centralized Rejection / Dropping Logic"] end subgraph App["Application Core"] Q1["Ingest / Unbuffered Pipeline"] Worker["Worker / Downstream Processor"] end

Client --> Q1 Q1 --> Worker Worker -.->|"Backpressure (can't handle data)"| Q1 Q1 -.->|"Propagates back"| Reject Reject -.->|"Reject / Shed Load"| Client ```

- In some technologies, omitting backpressure is a protocol failure: for example, the Java EE WebSocket specification completely lacks a mechanism for backpressure, offering developers inside servlet containers no standard means to slow upstream data rates. - In other instances, authors fail to link queues to transports due to oversight, never having suffered the cascading failures that occur when the "golden chain of backpressure" is broken. - Consumers of Clojure networking and async libraries must critically audit dependencies to ensure foundational plumbing will withstand production spikes rather than forcing complete architectural rewrites under load.

---

[20:16](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1216s) Designing "Maw": The High-Throughput Ingestion Endpoint

To demonstrate these challenges in practice, Tellman presents "Maw," an internal service designed as an omnivorous endpoint built to ingest arbitrary payloads delivered by external partners and customers. Maw’s operational model is conceptually direct: capture enormous volumes of incoming data, perform light processing, and persist the records for future batch workloads. To minimize operational friction on complex infrastructure, Maw was architected on top of Amazon Web Services (AWS), using an Elastic Load Balancer (ELB) to fan incoming network traffic across multiple nodes, and targeting Amazon S3 as a reliable, highly available, replicated storage destination.

The application’s constraints dictated a very specific architecture. First, it had to scale horizontally to meet massive traffic expansion: while Maw originally handled roughly 40,000 queries per second, its peak volume reached approximately 300,000 queries per second within 18 months. Second, it had to remain exceptionally low-maintenance; Tellman notes that if Maw requires more than a single day of maintenance per month, something has gone terribly wrong. Third, ingestion did not need to operate in real-time—it simply required that all collected records landed securely on S3 by the end of the day. Finally, Maw benefited from a forgiving reliability metric: data utility scaled strictly linearly with volume:

> “And the most important property is that the loss of the data is more or less directly proportional to loss utility. If we lose 1% of the data, we lose about 1% of the utility.”

- Unlike transactional domains like payment processing—where a 1% data drop destroys the system's baseline usefulness—losing 1% of Maw's telemetry input simply reduces downstream analytical utility by roughly 1%, greatly simplifying trade-offs. - The service was designed so traffic increases would not force continuous ground-up redesigns of the application tier.

---

[22:10](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1330s) The S3 Outage and the Cascading Failure of Unbounded Buffers

Before Maw, Tellman built an ingestion system consuming the Twitter "garden hose" (the official stream providing 10% of the Twitter fire hose), which generated a modest 600 tweets per second. That earlier pipeline passed an HTTP client stream into a standard Hadoop S3 client to write records to Amazon S3 without incident. When bootstrapping Maw's prototype for an anticipated 20,000 entries per second, he reused that exact architecture, merely placing an HTTP web server in front of the Hadoop S3 client.

This design concealed a fatal vulnerability: Amazon S3 occasionally experiences transient blips during which it temporarily stops accepting write operations. When S3 stalled, the Hadoop S3 client attempted to survive the outage by silently buffering all incoming records in memory, expecting the connection to clear so it could flush the backlog. At Maw's incoming volume, this unconstrained in-memory buffering quickly consumed all available JVM heap space:

> “And so we ran out of memory, and the system fell over, and then all of the traffic that that machine was handling fell over to the other machines, which themselves fell over, and then the entire system was dead.”

- In-memory buffering had likely triggered during the Twitter garden hose integration as well, but because 600 tweets per second generated a minimal footprint during brief S3 pauses, the queue flushed silently before memory ran out. - The failure exposed the danger of invisible unbounded buffers: the Hadoop client provided no status metrics, making it impossible to diagnose backlogs until the JVM crashed from an out-of-memory error.

---

[23:55](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1435s) Decoupling Ingestion with Durable Disks and Explicit Metrics

Reflecting on the outage, Tellman realized he had burdened the intake layer with an unnecessary guarantee: forcing an synchronous round-trip write over the network into S3’s replicated storage before acknowledging the payload. All that ingestion actually required was moving data out of volatile process memory into a durable, local destination. Writing records to the machine’s local disk offered far greater immediate availability than S3, despite the operational trade-off that a sudden hardware loss would destroy the un-flushed data on that host.

To decouple ingestion from network persistence, Tellman authored two open-source libraries: `durable-queue` (a simple, disk-backed queue that has processed roughly 3 to 4 trillion events) and `s3-journal` (a buffer built on `durable-queue` that commits items to disk before asynchronously uploading them to S3). Furthermore, to eliminate the blind spots of the earlier client, both libraries expose fine-grained metrics for health monitoring, reporting enqueue counts, successful uploads, in-memory file slabs, pending records, and retry attempts:

> “These are all things that are absolutely critical because we cannot prove properties about our system, right? The system is too complicated. We can only look and see what happens in as much as the metrics that we have created for ourselves allow us to do that.”

```mermaid flowchart LR accTitle: Maw Ingestion Pipeline with Durable Disk Queue accDescr: HTTP requests hit the Aleph/Netty server, write directly to a local durable queue, and are uploaded asynchronously to S3. subgraph Host["Single Maw Node"] HTTP["Aleph / Netty HTTP Server"] DQ["Local Disk (durable-queue)"] Journal["Upload Loop (s3-journal)"] end Remote["Amazon S3"]

HTTP -->|"1. Fast append"| DQ DQ -->|"2. Read batches"| Journal Journal -->|"3. Asynchronous flush"| Remote ```

- Writing to local storage eliminated the downstream latency of S3 replication hops from the critical ingest path. - Splitting the ingest path into an HTTP-to-disk write step and an independent disk-to-S3 upload loop isolated S3 downtime from request acceptance. - On the ingestion boundary, the Aleph HTTP server (backed by Netty) faced an additional constraint: it must return fast HTTP acknowledgments to upstream senders, because clients throttle their transmission rates if upstream latency spikes.

[26:49](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1609s) Netty's Threading Model and Instrumented Thread Pools

Zach Tellman explains that Netty has a very particular threading model: whenever a connection opens, it is assigned to a thread, and multiple connections are assigned to that same thread. Consequently, if any single connection blocks on that thread, it implicitly blocks every other connection sharing it, dragging along requests that never chose to be affected. The standard remedy is placing another thread pool behind Netty to distribute work and prevent one misbehaving connection from causing others to back up. However, metrics gathered at the Ring handler level often diverge significantly from actual response times—the true time from packet entry to packet exit. Tellman points out that thread pools themselves are notoriously under-instrumented: they act as an invisible boundary where a queue exists, tasks wait an unknown amount of time, and work completes at an unspecified moment.

```mermaid flowchart TD accTitle: Netty Threading Model and Backing Thread Pool accDescr: Incoming network connections map to shared Netty I/O worker threads, which forward tasks through an instrumented thread pool queue to isolated worker threads. subgraph NettyLayer["Netty I/O Layer"] C1["Connection 1"] --> T1["Shared Netty Thread"] C2["Connection 2 (slow)"] --> T1 end subgraph PoolLayer["Instrumented Execution Pool"] T1 --> Q["Thread Pool Queue"] Q --> W1["Worker Thread A"] Q --> W2["Worker Thread B"] end ```

To combat this blindness, Tellman wrote a library that includes an instrumented thread pool tracking core operational statistics. Showing a JSON endpoint from live Factual servers, he examines task rejection, completion, and arrival rates. Crucially, none of these values are simple scalars; they are quantile distributions because uncoordinated client requests spike and drop dynamically over time. When capacity planning, these tails matter: the difference between the median and the 99.9th percentile for arrival and completion rates is roughly a 3x spread. Even though task rejections happen infrequently, the metrics show that the system does drop data under peak capacity constraints, making rejection tracking an absolute necessity.

- **Quantiles over scalars:** Task arrival, completion, and rejection must be measured across quantiles because uneven arrival bursts produce a 3x spread between the median and the 99.9th percentile. - **Queue vs. task latency:** Task latency is a superset measuring the entire lifecycle (queue wait plus execution time), whereas queue latency isolates time spent waiting in line. - **Sampled metrics artifact:** In the sampled statistics, the 99.9th percentile of queue latency occasionally reads higher than that of task latency because the samples capture slightly different operational windows. - **Unbuffered queue delays:** Even though the monitored queue has an unbuffered length of zero, traversing it can still take up to 2 milliseconds due to lower-level buffered queues underlying the Java execution runtime.

[29:44](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1784s) Measurement Limits and System Tooling

Having visibility into queues and thread pools gives operators a clearer picture, yet Tellman admits it remains an approximation. The instrumented thread pool does not inspect the deep internals of Netty, meaning that if a massive divergence ever appeared between internal metrics and external measurements, engineers would still have to dig deeper. But having partial visibility beats willful ignorance:

> “There’s always something more to measure, but the idea is that we just don’t want to be sort of willfully blind and close our eyes and pretend that everything’s okay.”

Understanding what constitutes a completed task is vital when architecting systems, Tellman argues, because an accumulation of incomplete tasks forces an architecture to apply backpressure, drop packets, or reject input outright. Moving the goalposts to complete work earlier provides enormous leverage. When selecting engineering tools, raw performance benchmarks only show that software can run fast under artificial conditions; operators should always prioritize tools that actively report their own runtime speed in production. Tellman reminds the audience that certain system variables will always escape direct control—such as a mobile user driving into a tunnel and dropping their connection—yet from the user's perspective, that hiatus represents the service's latency.

[31:25](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1885s) Building a Robust Chat Server with Manifold

To demonstrate these dynamics in practice, Tellman introduces a classic scenario: a multi-user chat server where participants enter a room and broadcast messages to everyone else. Using his library `manifold`—chosen because of its familiarity and useful stream operators, though `core.async` or alternative stream libraries could serve the same role—he defines an event bus implementing a publish-subscribe model. Publishing to a topic sends the message to all active subscribers; if no one is subscribed, the message becomes a no-op that evaporates into the ether. A basic chat handler extracts the requested room from an incoming connection, publishes that connection's messages to the room stream, and pipes the room's stream back out to the connection.

```mermaid flowchart LR accTitle: Multi-user Chat Room Pipeline with Backpressure Timeout accDescr: Incoming user messages are published to an event bus room, buffered, and piped into each client connection with a drop timeout. U1["Client 1 (Publisher)"] -->|"publish"| EB["Manifold Event Bus Room"] EB -->|"pipe"| B["Aggregate Size Buffer (limit 1000)"] B -->|"connect (10s timeout)"| U2["Client 2 (Subscriber)"] ```

This naive implementation breaks down quickly under real-world conditions. Because publishing requires delivering the message to every subscriber in the room, a single slow consumer delays message distribution for all other users. To prevent one bad connection from destabilizing the chat room, Tellman explains how to add a buffer between the room's stream and the client's output connection. Rather than bounding the buffer by message count—which risks instability since a message could range from a single word to a novel—the buffer limits aggregate message byte size (such as 1,000 units). However, a buffer merely postpones failure instead of preventing it. Ultimately, the system must drop unresponsive consumers:

> “The only thing that we really can do is once someone is slow to receive our messages, is unable to receive them, we kick them out.”

Using Manifold's `connect` function, the server pipes the stream into the network sink with an explicit timeout. If a subscriber's buffer remains full and fails to accept a message for 10 seconds, the connection terminates automatically, forcing them to reconnect.

[34:42](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=2082s) Protecting the System from Bad Actors and Floods

Solving the slow-consumer problem immediately exposes a reciprocal failure mode: if a publisher sends messages at an extreme rate, they can fill everyone else's buffers simultaneously and trigger mass disconnections across the room. Whether caused by malicious intent or naive client code, the underlying problem remains identical, and Tellman emphasizes that engineers cannot afford to care about the user's motive.

> “It’s your job to make sure that you don’t get knocked over, that, you know, the many don’t suffer because of the actions of the few and so you really need to think about this carefully.”

To defend against floods and prevent a few active clients from taking down the entire room, Tellman explains that incoming connections must be strictly throttled—for example, limiting a client's stream to a rate of only one message per second.

[35:37](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=2137s) Boundary Conditions: Throttling, Credits, and Robustness

Zach Tellman explains how inbound throttling differs from outbound disconnect logic. On the inbound side, a system does not need complex disconnection logic when dealing with a peer that sends traffic too aggressively. Instead, the server can rely on transport-level backpressure: it uses TCP backpressure to simply refuse additional data, telling the client that it will not accept the message. If refusing the incoming packets causes the client to fail, Zach notes, “that's their problem.”

```mermaid flowchart LR accTitle: Inbound backpressure and token-bucket throttling accDescr: Incoming TCP messages accumulate up to a 10-second burst credit or face backpressure. subgraph Inbound["Client Connection"] Client["Inbound Client"] -->|"Sends messages"| TCP["TCP Backpressure"] end subgraph Throttling["Rate Limiting Policy"] TCP -->|"Permitted flow"| Credits{"Credits Available?\n(Max 10s burst)"} Credits -->|"Yes"| Accept["Process Message\n(1 msg/sec rate)"] Credits -->|"No credits"| Stalled["Push back via TCP"] end ```

Moving into the specifics of rate limiting, Zach addresses the nuances of throttling rules. A naive rule enforcing exactly one message per second can feel irritating and unnatural to human users. For instance, if a participant sits quietly in a chat room for ten minutes, it is reasonable to ask whether they must still be limited to one message a second or if they should be allowed to send ten minutes' worth of messages in a burst. Both behaviors are valid design choices in throttling schemes. In his example, he configures a token-bucket approach where users accrue credits while idle: they can accumulate up to 10 seconds’ worth of credit. Thus, a silent user can send an immediate burst of 10 messages, but no more.

Defining these concrete rules strips away the illusion of an idealized, generic architecture. Zach points out that the example becomes “a little bit less platonic than it was before” because real-world resilience demands explicit trade-offs. Engineers must define the maximum memory allocated to each connection, determine acceptable client latency timeouts, and fix the inbound message rates. By moving beyond hand-waving abstractions where everyone projects their own ideal chat room onto the design, the architecture becomes precise and grounded. As Zach emphasizes:

> “This is what you need to do, right? These are the steps that you need to take. And I want to emphasize also that it's not hard to do this, right? You have to think a little bit about what these boundary conditions should be, but this is what's required for us to be able to make these robust systems, for us to be able to ride out the behavior of the outside world.”

[37:18](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=2238s) Topology Visibility and Dynamic Queue Systems

Zach shifts focus to a major architectural hazard in asynchronous systems: managing dynamic queue cardinality. In simpler architectures, an application might rely on one or two static queues. A fixed cardinality makes operational observability straightforward because engineers can attach permanent graphs and monitor queue depth directly. By contrast, an application like a chat room generates an untold number of queues that dynamically appear, disconnect, and reconfigure as users join and leave. Standard queue monitoring tools fail in this environment because the underlying system is constantly changing.

```mermaid flowchart TD accTitle: Manifold topology tracking between streams accDescr: Downstream inspection enables tracing flow across transient queue topologies. subgraph Topology["Dynamic Manifold Streams"] Source["Source Stream"] -->|"connect / consume"| DownstreamA["Downstream Stream A"] Source -->|"connect / consume"| DownstreamB["Downstream Stream B"] end subgraph Inspection["Observability"] Source -.->|"inspect topology"| Meta["Topology Markers\n('What streams are downstream?')"] end ```

Zach highlights that this dynamic churn presents a serious visibility problem in libraries like `core.async`:

> “If you think about what this would look like for core.async, you have all these go routines that are sort of like shoving messages in every direction, but you can't look at it and sort of apprehend it as a whole.”

To tackle this weakness in his library, Manifold, Zach introduced stream-level introspection. Operations such as `connect` or `consume` register internal markers that record a stream’s connections. Any stream can be inspected at runtime to reveal its immediate downstream consumers, creating an inspectable graph of the running pipeline. Zach acknowledges that visualizing these graphs remains an unsolved problem: early attempts using Graphviz quickly turned into unreadable, unscalable hairballs even for moderately complex topologies. Determining whether the solution is a better visualization engine or offline event logging to disk remains an open area for community exploration, but he stresses that Clojure developers cannot simply cross their fingers and hope transient asynchronous architectures behave.

[39:20](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=2360s) Core Principles of Queues and Closing Thoughts

Zach summarizes the core lessons of the talk into three operational principles for building resilient queue-driven software:

1. **Unbounded queues do not exist.** Any seemingly unbounded queue is physically bounded by host memory or another shared resource, and crossing that hidden boundary crashes the entire application. 2. **Handle overload at the edges, preserve backpressure in the middle.** Application boundaries must have explicit shedding, throttling, or backpressure policies for handling excess traffic. Conversely, internal, reusable components must never arbitrarily drop data; they should propagate backpressure upstream to allow higher-level callers to make informed trade-offs. 3. **Demand metrics everywhere.** Developers must insist on deep instrumentation across all stages of execution, continuously seeking visibility into internal queues, even when components attempt to obscure their operational state.

As Zach finishes, he reflects on the responsibility developers bear when designing systems:

> “And when we build an application it should always account for what happens when it receives too much data, but where we're not at the sort of boundaries of application, we're trying to write something which is more general or less opinionated or can be reused elsewhere, we should always use back pressure, right? We should always defer to someone else who knows better than us what to do.”

Concluding right as his allotted speaking time expires at 40 minutes, Zach invites the audience to continue the discussion and ask follow-up questions out in the hallway.

## Notable quotes > [17:42](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1062s) And wherever possible we should use backpressure, because that's the most neutral choice that we can make, right? That just kind of tells someone else, ‘I can't handle this data, you figure out what that means. — Zach Tellman

> [29:44](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1784s) There’s always something more to measure, but the idea is that we just don’t want to be sort of willfully blind and close our eyes and pretend that everything’s okay. — Zach Tellman

## Mentioned Zach Tellman, Clojure, core.async, Java, Amazon Web Services, Amazon S3, Elastic Load Balancer, Twitter, StatsD, UDP, HTTP, TCP, Hadoop, Maw, durable-queue, s3-journal, Aleph, Netty, Ring, Factual, Manifold, Graphviz

## Video description Queues, it seems, are back in fashion. This is exciting, as queues let us explore problem spaces that were unreachable by pure, immutable primitives.

But queues, and the execution models that drive them, are very different than what we're used to. Naive use of libraries like core.async will create brittle, unpredictable systems that fall over when we least expect.

This talk will cover the basics of queueing theory, and its practical application in Clojure systems large and small.

[Source](https://www.youtube.com/watch?v=1bNOO3xxMc0)

中文简报

Zach Tellman:万物皆流 Tellman指出,现代软件系统层层叠叠地布满了队列,用于协调线程池、操作系统和网络边界之间的执行。通过数学模拟,他证明了开放系统在流量突增时会遭遇断崖式、如火箭般蹿升的延迟崩溃,而增加消费者仅仅是掩盖了这种方差,直到灾难性故障爆发。为防止内存耗尽,架构必须在应用边界用背压、显式拒绝或丢弃策略来取代无界队列。结合高吞吐服务和实时聊天服务器的案例研究,他展示了如何利用持久化磁盘队列、令牌桶限流、带监控指标的线程池以及流拓扑检查来保护系统。 无界内存队列在本质上是不可行的,因为生产系统属于开放系统,到达率无法受控,这必然会导致灾难性的内存溢出崩溃。 (12:34) 成比例地扩容工作节点容量和到达率并不能防止系统崩溃;它只是收窄了响应时间的方差,并在灾难性崩溃发生前掩盖了不稳定性。 (9:46) 缓冲区并不能解耦执行时间——无缓冲的汇合通道(rendezvous channel)就已经能做到这一点;缓冲区的真正用途是充当减震器,以掩盖暂停和恢复背压时的延迟。 (15:13) 系统架构必须将丢弃或拒绝策略集中在应用边缘,同时在内部流水线中始终保持向上游传递背压,以防止静默数据丢失或级联故障。 (17:42)

Reading summary

Zach Tellman argues that unbounded queues are an illusion that inevitably precipitates catastrophic system collapse under uncoordinated load. Building resilient software requires treating queues as physical resources, enforcing strict backpressure across internal pipelines, shedding overload at application boundaries, and instrumenting latency distributions beyond simple averages. The Hidden Instability of Open Systems (3:32) Queuing theory differentiates between stable closed systems—where producers wait for consumers to finish, as in a REPL—and open systems, where external requests arrive independently without central coordination. Modeling open systems with exponential arrival distributions and Pareto-distributed task runtimes reveals that queue saturation is driven by long-tail execution outliers rather than median complexity. When load mounts, an open queue experiences a non-linear tipping point: latency jumps orders of magnitude to an intermediate state where queuing delay dominates processing time, before rocketing upward into memory exhaustion. Tellman demonstrates that adding worker threads or consumer nodes does not prevent saturation when throughput and capacity scale proportionally. Comparing 1 worker under 1x load against 16 workers under 16x load, both configurations fail at the exact same threshold. However, while a single consumer exhibits early latency degradation when blocked by a slow task, 16 consumers mask degradation by absorbing rare outliers across available workers. This dynamic creates a dangerous illusion of safety, maintaining sub-millisecond latencies right until the entire system abruptly collapses. Backpressure and the Real Purpose of Buffers (12:34) Because external arrival rates will eventually exceed processing capacity, unbounded queues surrender system stability to outside traffic. Overloaded architectures have only three recourses: drop incoming data silently (viable for transient telemetry), reject it with an error notification like an HTTP 503, or pause incoming flow using backpressure. For internal libraries and middleware, Tellman contends that backpressure is the only neutral choice, as intermediate components lack the business context to decide which requests can be safely discarded. While queues decouple what work is executed from when it occurs, storage buffers are not mathematically required for that decoupling. Unbuffered channels—such as the default rendezvous channels in Clojure's core.async—force producers to wait for consumers, functioning as stable closed systems. Buffers exist strictly as shock absorbers to mitigate the physical costs of asserting backpressure, such as thread rescheduling, TCP window renegotiation, and disk seek overhead. Because buffers inherently inflate end-to-end latency, chaining buffered queues in series compounds downstream delays. Decoupling Volatile Storage in Ingestion Pipelines (20:16) Tellman illustrates the failure modes of implicit buffering using "Maw," an ingestion endpoint designed to scale from 40,000 to 300,000 requests per second while targeting Amazon S3. An earlier prototype pipeline crashed during a transient S3 outage because an underlying Hadoop S3 client quietly buffered incoming HTTP records in volatile JVM heap memory. The uninstrumented in-memory backlog triggered out-of-memory errors that knocked over nodes sequentially in a cascading failure across the cluster. To eliminate this vulnerability, Tellman created durable-queue and s3-journal, shifting the ingestion boundary from remote network confirmation to local disk persistence. By immediately committing batches to local disk before acknowledging HTTP requests, the ingest tier decoupled front-end throughput from S3 replication latencies and outages. This architecture accepted the trade-off that local hardware loss could discard un-flushed batches, an acceptable compromise because Maw's data utility degraded proportionally to volume loss. Edge Enforcement and Dynamic Topology Observability (26:49) Protecting systems requires explicit boundary rules rather than idealized abstractions. In an asynchronous chat service built with Manifold, outbound connections must prevent slow consumers from exhausting room memory by introducing aggregate byte-limited buffers paired with explicit disconnect timeouts (such as kicking unresponsive clients after 10 seconds). On inbound streams, servers must avoid runaway memory growth by exerting TCP backpressure and enforcing token-bucket rate limits, such as granting a 10-second burst credit before constraining throughput to one message per second. Maintaining system health also depends on deep runtime visibility. Netty connection-sharing models can cause a single blocked task to halt unrelated requests on the same I/O thread, requiring instrumented thread pools that report quantile distributions. Because arrival and completion rates exhibit up to a 3x spread between median and 99.9th percentiles, scalar metrics obscure operational stress. Furthermore, in systems where dynamic queues continuously spawn and terminate, Manifold introduces runtime topology markers to inspect downstream streams, addressing the visibility deficit found in complex asynchronous architectures. Tellman concludes that because infinite queues cannot exist in physical hardware, systems must be architected around explicit limits: internal plumbing must faithfully transmit backpressure upstream, while edge interfaces enforce deliberate load-shedding, rejection, or throttling policies. A lingering architectural challenge remains the effective visualization of transient, highly dynamic queue topologies without generating unreadable graphs.

精炼阅读摘要

Zach Tellman指出,现实中的软件系统大多属于缺乏协调的开放系统,所谓无界队列本质上只是由物理内存硬性限制的定时炸弹;构建高韧性系统必须依靠边界反压、防御性限流以及全局可观测性来主动应对过载。 多层嵌套队列与开放系统的失控模式 (1:04) 从高级并发原语到底层硬件,现代系统由多层队列嵌套而成。以Clojure的core.async通道为例,其内部维护着消息缓冲区、挂起生产者的puts队列与挂起消费者的takes队列;当消费者将回调指派给Java线程池时,工作任务又会进入BlockingQueue,随后经由操作系统调度队列分发至CPU核心。Tellman认为队列的核心价值在于将“期望的操作”与“执行的时机”解耦,但这种解耦在开放系统中极易引发危机。闭式系统(如REPL或单用户网页浏览)由生产者与消费者交替控制,具有内在稳定性;开放系统则持续接收无协调的外部请求,任务到达间隔符合指数分布,执行复杂度则服从帕累托分布。 水平扩展与缓冲区的安全假象 (8:50) Tellman通过仿真证明,当长尾重型任务比例上升时,队列延迟会突变至高位平衡,并在系统彻底失去控制前像火箭一样呈指数级飙升。面对这种过载,常见的工程直觉是横向增加工作线程或机器。然而,当输入流量与消费者数量同比例扩展时(例如1x速率对1个消费者、4x速率对4个消费者、16x速率对16个消费者),系统发生崩溃的时间节点完全一致。增加消费者仅通过分摊耗时任务掩盖了延迟方差,使系统在断崖式崩溃前持续维持亚毫秒级的低延迟假象。Tellman进一步强调,缓冲区并非实现生产消费解耦的必要组件,其本质是吸收反压恢复物理开销(如线程唤醒、TCP窗口重开、磁盘寻道)的减震器;若串联堆叠缓冲队列,不仅无法防止过载,反而会显著放大整体延迟并引发内存耗尽。 过载应对策略与中间件反压链条 (12:34) 系统处理过量数据只有静默丢弃(Drop)、明确拒绝(Reject)与反压暂停(Pause)三种策略。静默丢弃(如基于UDP的StatsD监控)存在致命缺陷,网络拥堵时正是最需要监控的时刻,指标数据反而会率先丢失;HTTP 503等明确拒绝是Web边界的常用方案,但边缘网关通常缺乏业务状态来延迟兑现请求。Tellman主张,通用库、框架和内部管线必须坚持“黄金反压链”(Golden Chain of Backpressure):当下游无法承载时,应将压力逐级回推至系统边缘,由边缘统一执行限流或熔断。然而,生态中常出现反压断裂,例如Java EE WebSocket规范未定义反压机制,导致底层网络传输与上层队列失联,极易引发级联崩溃。 实战架构设计:持久化解耦与指标度量 (20:16) 在数据摄取系统“Maw”的演进中,吞吐量从初始的每秒4万次请求激增至18个月后的30万次。早期原型采用Hadoop客户端直接写入Amazon S3,但在S3发生偶发性写入停顿时,客户端在内存中无限制缓冲请求,导致JVM因内存耗尽而崩溃。Tellman随后开发了开源库durable-queue与s3-journal,将摄取链路重构为“本地磁盘快速写入”与“异步批量上传S3”两个独立循环,解除对外部存储网络往返的强依赖。针对Netty单线程服务多连接容易相互阻塞的问题,Tellman利用自制监控工具将线程池队列公开为JSON接口。实测数据显示,未缓冲队列在高负载下仍可能因底层运行时队列产生最高2毫秒的等待,且任务到达与完成速率的99.9分位数是中位数的约3倍,证实了系统容量规划必须依赖分位数而非标量均值。 边界条件收敛与动态拓扑可见性 (31:25) Tellman以基于Manifold构建的多人聊天室说明如何落地具体边界条件。为防止单个慢连接阻塞全员,服务端在客户端输出流前设置了按字节大小(如1000单位)限制的缓冲区,并在写入超时达10秒时强制切断慢速消费者。针对上行洪水攻击,服务端利用TCP反压直接抑制发送端,并结合令牌桶算法允许最多累积10秒信用额度的突发消息。此外,针对流式系统运行时动态生成海量队列导致的“黑盒”问题,Manifold引入了下游流拓扑检查机制。尽管拓扑图形可视化仍面临“依赖乱麻”(hairballs)的挑战,但Tellman坚称必须保持对队列生命周期的内省监控,绝不能忽视动态拓扑的运行状态。 Tellman最后总结出高韧性软件的三项核心原则:世上不存在真正的无界队列;过载应在边界处明确处理,中间组件则必须严格传递反压;所有执行管道都必须实施深度度量。关于复杂异步拓扑的可视化与追踪,目前仍是尚未完全解决的开放性课题。

Detailed notes — zh

## Background 该视频由 ClojureTV 频道于 2015 年 4 月 22 日在 YouTube 发布,主讲人为 Zach Tellman。页面描述指出,队列能够帮助开发者探索纯不可变原语无法触及的领域,但其执行模型与常规模式大相径庭,若单纯或幼稚地使用 core.async 等库极易导致脆弱脆弱的系统意外崩溃,本场演讲旨在介绍排队论的基础理论及其在 Clojure 系统中的落地实践。 ## TL;DR Zach Tellman 深入剖析了队列作为“副作用”的本质及其在现代多层运行时(从 core.async、JVM 线程池到操作系统与硬件)中的广泛存在。他结合排队论指出,在请求随时可能涌入的开放系统中,多消费者横向扩容无法延缓崩溃的发生,反而会掩盖长尾任务与极端延迟的前兆抖动,因此无界队列在本质上是不可取的。演讲系统地探讨了过载处理的三种策略(丢弃、拒绝与背压),指明缓冲区存在的物理意义是对冲背压恢复的开销,并以真实生产系统 Maw 的解耦改造以及基于 Manifold 的聊天室广播防雪崩设计为例,阐释了如何在系统边缘制定防护策略并持续追求深层可观测性指标。 ## Atmosphere & style 整场演讲充满务实、严谨且略带黑色幽默的工程技术氛围。Zach Tellman 以低调沉稳、娓娓道来的叙述风格,逐步拆解现代软件架构中看似美好却暗藏杀机的各种抽象假象。从数学分布推导到血淋淋的线上级联故障,观众仿佛置身于一次深度的事故复盘与架构思辨现场,既能体会到直面复杂系统脆弱性时的冷峻清醒,又能感受到工程师用指标和边界防御化解失控时的笃定与从容。 ## Bulletin - [11:40](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=700s) 无界队列在物理上并不存在,本质上受限于系统内存等底层资源;将数据无底线缓存在内存中等同于将系统生存权拱手让人,极易诱发致命的 OOM 级联崩溃。 - [08:50](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=530s) 成比例增加消费者和吞吐量(如 16x 速率搭配 16 个消费者)无法延缓系统崩溃的物理时间点,反而会抹平早期的延迟抖动,掩盖系统濒临崩溃的前兆假象。 - [15:19](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=919s) 队列解耦和削峰填谷的本质并不依赖内部缓冲区,无缓冲队列天然构成闭式系统;缓冲区的唯一存在价值是对冲背压挂起与唤醒时的物理延迟开销(如线程上下文切换、TCP 窗口重建)。 - [39:20](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=2360s) 高并发系统应对过载的黄金法则是:仅在系统与外部世界的绝对边界执行拒绝或丢弃策略,而在通用中间件与内部调用链路上必须坚决透传背压。 ## Key takeaways - [00:00](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=0s) 队列处理的是动作(副作用)而非不可变数据变换,其核心价值在于解耦“期望执行什么”与“何时执行”。 - [01:04](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=64s) 现代运行时在代码与硬件之间隔着深达五六层的队列链条(包括 core.async 通道、线程池阻塞队列、条件队列、OS 调度队列及物理核心)。 - [04:38](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=278s) 封闭系统通过互相让渡控制权自然限流,而开放系统缺乏全局协调,并发请求随机会重叠涌入,是生产环境失控的主要来源。 - [06:01](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=361s) 开放系统的请求到达通常符合指数分布成簇突发,而任务耗时符合帕累托分布的厚尾效应,长尾离群值是压垮并发能力的根本推手。 - [08:50](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=530s) 当系统跨过过载临界点,排队延迟会彻底主导任务生命周期,导致延迟分布从离散方差收敛为均质且极度缓慢的狭长带。 - [08:50](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=530s) 增加消费者数量虽然能在早期分摊长尾复杂任务并维持亚毫秒级响应,但多消费者系统在崩溃前夕毫无征兆抖动,形成极具欺骗性的架构幻觉。 - [11:40](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=700s) 面对超载只有丢弃、拒绝与暂停(背压)三种策略;UDP 尽力而为的丢弃模式存在残酷讽刺——在网络最危机、最需要指标时反而丢包最严重。 - [15:19](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=919s) 通用组件绝不应武断丢弃或拒绝数据,必须利用背压将决策权延后让渡给更了解业务场景的调用方。 - [17:42](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1062s) 缓冲区是以延迟为代价换取平稳吞吐,其存在的真正目的是避免消费者在背压解除的物理真空期内陷入饥饿;严禁随意嵌套串联多个缓冲队列。 - [22:50](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1370s) Maw 系统早期因 AWS S3 故障导致内存无界队列积压引发 OOM 级联崩溃,后续通过 durable-queue 本地磁盘队列与 S3 异步上传实现解耦改造。 - [26:49](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1609s) 仅在 Ring Handler 中埋点会产生度量盲区,必须使用具备可观测性的线程池来精确测量任务在队列中的隐式排队耗时。 - [28:07](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1687s) 到达率与完成率的中位数与 P99.9 尾部分位数常存在数倍差距,且即使长度为 0 的无缓冲队列也会因底层环境产生毫秒级排队延迟。 - [30:11](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1811s) 工具的主动自省与度量指标能力远比脱离现实的理论跑分更有价值,重新定义“完成”的语义边界能显著减轻排队积压。 - [33:12](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1992s) 在 Pub/Sub 广播模型中,单个消费缓慢的客户端会拖垮全场,设置投递超时并强行断开是隔离慢消费者的终极手段。 - [35:37](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=2137s) 剔除慢消费者的机制可能会被恶意高速发送方武器化,因此必须在入站处配合节流(如支持突发信贷的令牌桶)以抵御外部洪泛。 - [37:18](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=2238s) 动态多租户流系统缺乏固定队列基数,传统监控失效,需要依赖运行时拓扑追踪机制与图快照来透视数据流向。 ## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=0s) 队列的本质:副作用与 Clojure 社区的视角变化 演讲开篇探讨了队列(Queue)这一概念。表面上看,队列十分简单直观:生产者负责将消息入队(enqueue),消费者负责将消息出队(dequeue),通常遵循先进先出(FIFO)的顺序。然而,一个不太明显的本质在于,入队操作本身就是一个副作用(side effect)。我们从入队操作中拿到的仅仅是“消息已加入”的确认,返回值并不是输入数据的某种纯函数映射,数据本身代表的也仅仅是一个待执行的副作用动作。 ```mermaid flowchart LR accTitle: 典型队列的基本架构与副作用流动 accDescr: 生产者将消息推入队列并仅接收副作用确认,消费者从中出队并执行具体操作。 subgraph S1["入队阶段"] P["生产者 (Producer)"] -->|"入队 (副作用确认)"| Q["队列缓冲区 (Queue Buffer)"] end subgraph S2["出队阶段"] Q -->|"出队 (通常按序)"| C["消费者 (Consumer)"] C -->|"执行动作"| E["执行端点 (Action)"] end ``` 正因如此,Clojure 社区历来更倾向于处理纯不可变数据,队列曾长期被边缘化。开发者大多直接使用 Java 提供的队列,能抽象封装就尽量封装,很少正面探讨它。然而,随着 `core.async` 库的引入,队列重新回到了 Clojure 社区的核心技术讨论中。Zach Tellman 认为这是件好事,并幽默地指出,程序员的工作毕竟是让计算机做实实在在的事,而“不仅仅是在提高房间的环境温度(ambient temperature)”。 - 队列的核心机制:生产者入队、消费者出队,处理的是动作(actions)而非纯粹的数据变换。 - Clojure 过去对待队列的态度:由于其本质属于副作用范畴,社区以往偏好封装隐藏,直至 `core.async` 引起普遍关注。 - Zach Tellman 强调系统实际落地的意义:“we of course are in the business of making computers do things right we don't just raise the ambient temperature of the room” ### [01:04](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=64s) 现代运行时的队列层叠:从 core.async 到底层硬件 剥开表象会发现,一个普通的 `core.async` 通道(channel)内部实际上同时包含三个队列: 1. 存放消息的缓冲区(buffer); 2. 供生产者等待的“puts 队列”(当缓冲区填满时在此阻塞等待); 3. 供消费者等待的“takes 队列”(当缓冲区为空时在此挂起等待)。 但这只是冰山一角。当消息成功由生产者传递给消费者后,事情并没有结束。消费者通常提供一个回调函数(callback)来指明后续处理逻辑,而这个回调必须被提交到线程池(thread pool)中运行。在 Java 中,线程池内部又自带一个阻塞队列(BlockingQueue),夹在提交任务的客户端与执行任务的线程之间。 ```mermaid flowchart TD accTitle: 请求从应用层穿透到硬件核心所经过的多层队列 accDescr: 展示 core.async 通道、Java 线程池、条件队列、OS 调度与 CPU 硬件线程之间的六层队列链条。 subgraph L1["应用并发层 (core.async)"] A["Puts 队列 / Buffer / Takes 队列"] end subgraph L2["运行时管理层 (Java / JVM)"] B["线程池任务队列 (BlockingQueue)"] C["条件挂起队列 (not-full / not-empty)"] end subgraph L3["底层执行层 (OS 与硬件)"] D["操作系统调度队列 (OS Scheduler)"] E["CPU 硬件核心 / 硬件线程"] end A --> B B --> C C --> D D --> E ``` Java 线程池的大小通常是动态的。当阻塞队列堆满且有新任务尝试加入时,线程池会创建新线程;如果已达到设定的最大线程上限,就会直接抛出著名的 `RejectedExecutionException` 异常。Java 的 `BlockingQueue` 底层原理与 `core.async` 如出一辙:它有一个存放消息的缓冲区,以及两个由条件变量(Condition)支撑的等待队列——`not-full` 与 `not-empty`。当缓冲区满时,生产者线程会释放持有的锁并挂起到 `not-full` 条件队列,等待缓冲被消费后唤醒单个线程。再往深处看,JVM 线程本身还需要依赖操作系统层面的就绪与调度队列,而操作系统的软件线程最终又必须排队调度到物理 CPU 核心与硬件线程上。 - 在开发者期望计算机执行某项任务与该任务实际落地之间,中间往往隔着五六层队列在进行中介协调。 - 线程池容量达到极限并拒绝任务是开发高并发系统必须时刻警惕的隐患:“which case it will throw a reject execution exception which is something that we should always be aware of when using a thread pole” ### [03:03](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=183s) 排队论的介入:为什么队列无处不在? 队列在现代系统中之所以无处不在,核心价值在于解耦了“期望执行什么”与“何时执行”。业务代码执行时往往无法预知系统全貌——不知道当前是否有其他并发请求在涌入、不知道请求到达的时序,也难以动态权衡优先级;因此,最佳做法就是通过队列宣告“希望在某个时刻执行该操作”。 然而,这种无处不在的解耦也意味着我们必须搞清楚队列是如何运作、如何过载以及如何崩溃的。这就引出了排队论(Queueing Theory)。排队论更偏向于纯统计学,而非纯软件工程领域。Zach Tellman 自谦并非排队论科班学者,但作为一个多年来深耕分布式系统、并多次下功夫钻研排队论的工程师,他希望分享那些真正能为系统设计提供直觉指导的核心思想。对于想深入学习的听众,他推荐了一本虽然包含 90% 纯数学统计、但偶尔会结合计算机系统进行阐述的经典著作。 - 队列解耦时序:“The reason that cues are so ubiquitous is because they separate what we want to have happen uh from when it happens” - 学习排队论的目的不是死记公式,而是建立应对高并发、过载与系统退化的直觉模型。 ### [04:38](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=278s) 封闭系统与开放系统 排队论中至关重要的一个分类是封闭系统(Closed System)与开放系统(Open System): | 系统类型 | 核心行为特征 | 常见真实案例 | 建模与推理难度 | |:--- |:--- |:--- |:--- | | **封闭系统 (Closed System)** | 生产者发出一个请求后,必须等待消费者处理完成并返回,才会生产下一个请求;双方相互让渡控制权。 | 交互式 REPL 循环(读取、求值、打印后才接受下一次输入);单个用户浏览网页(点击链接后等待页面加载完毕才点下一页)。 | 容易建模与推理。 | | **开放系统 (Open System)** | 请求产生与服务消费完全独立,各方缺乏全局协调;正在处理旧请求时,新请求随时可能并发涌入。 | 大多数分布式 Web 服务、微服务后端、云端请求接口。 | 极易发生排队拥堵,行为难以预测,容易失效。 | 现实中困扰工程师的生产级事故,绝大部分发生在开放系统中。在开放系统中,请求随时会发生重叠与冲突,因缺乏全局节流,系统在过载边缘的响应特性变得极其复杂。 - 开放系统的失控常态:“where there is no coordination where when a request comes in we're processing it it's almost a given that a request is to come in before we're ready” ### [06:01](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=361s) 开放系统模拟:指数到达、帕累托任务与频谱图 为了直观揭示开放系统在过载时的行为特征(单看 AWS 监控图表往往由于维度过多而掩盖本质),Zach Tellman 建立了一个仿真模型: 1. **到达间隔(Inter-arrival Times)**:采用指数分布(Exponential Distribution)。大量实证研究表明,指数分布能极好地拟合非协同场景下的并发资源竞争,如银行柜台、街头餐车排队,以及网络请求。这意味着即使系统平均吞吐为每秒 1000 次查询(1000 QPS),请求也绝不会均匀地每隔 1 毫秒到达,而是呈现成簇突发(bursts)并在突发后出现短暂的空白等待。 2. **任务复杂度(Task Complexity)**:采用帕累托分布(Pareto Distribution,即著名的 80/20 法则)。任务的计算耗时不能简单用指数分布概括,而是具有厚尾效应(heavy tail)。通过调节长尾的胖瘦程度(fat tail),模拟极端耗时异常任务(outliers)的出现频率。在真实环境中,磁盘 I/O 拥堵或同宿主机其他虚拟机的资源争抢,都会细微地拉厚这个尾部,让任务变得更慢。 ```mermaid flowchart LR accTitle: 开放系统仿真模型设定 accDescr: 展现请求到达的指数分布特性、任务复杂度的帕累托厚尾,以及通过频谱图观察延迟的整体流程。 A["请求到达<br/>(指数分布:成簇突发/非匀速)"] --> B["任务消耗时长<br/>(帕累托分布:长尾效应)"] B --> C["系统表现观测<br/>(对数尺度延迟频谱图/热力图)"] ``` 3. **可视化方案(Spectrogram 延迟频谱图)**: - **纵轴(Y 轴)**:端到端延迟(从任务到达、排队、被消费到最终完成的总时间),由于系统在走向不稳定时延迟会急剧飙升,因此使用无具体刻度的对数刻度(Logarithmic Scale)展示相对变化趋势。 - **横轴(X 轴)**:帕累托分布从“瘦尾”(绝大多数任务极简单、仅极少数复杂)逐步过渡到“胖尾”(高复杂度任务占比增加)的过程。 - **热力图(Heat Map)**:颜色深浅表示延迟分布的概率密度。在初始瘦尾状态下,左侧呈现一条极窄且明亮的红带,表明绝大多数任务都能在短时间内稳定完成;然而随着尾部渐胖,系统延迟分布开始出现显著的扩散与失控迹象。 ### [08:50](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=530s) 队列失控的临界点与多消费者假象 随着负载上升,延迟并不会线性温和地增加,而是会在跨过某个临界点时突然跃升至全新的平衡态。图表上的红色数据点会在极短时间内散布到比先前高出数个数量级的区间。此时系统虽然勉强维持着平衡,还没有彻底失去控制,但所有处理速度已经显著慢了下来。紧接着,如果压力持续,队列便会“像火箭一样升空(takes off like a rocket)”,整个系统旋即陷入危机,迅速耗尽内存并引发致命的崩溃(OOM)。在图表形态上,原本离散分布的任务耗时最终会聚集成一条极其狭窄平整的细带,这并非因为处理变得高效稳定,而是因为任务在队列中漫长等待的时间已经彻底压倒了真正的计算时间,所有任务的生命周期都被均质化为极其缓慢的排队延时。 面对容量耗尽与处理滞后,工程中最直觉的做法就是横向扩容——增加消费端机器或消费者线程。如果仅仅向原有系统追加消费者,确实能在短时间内消化队列,将延迟重新压回安全水位;但当面临更现实的场景,即吞吐量成比例增长时,系统的表现却呈现出一种极具欺骗性的残酷规律。若对比三组不同规模的系统配置:1 倍请求速率搭配 1 个消费者、4 倍速率搭配 4 个消费者、16 倍速率搭配 16 个消费者,这三者在遭遇超额峰值时,实际上是在完全相同的时刻彻底倒下的。增加机器并没有从本质上拔高系统应对极端过载的防线。 导致这种现象的根源在于长尾任务(long-tail tasks)与计算复杂度的不对称分布。在单消费者的体系中,一旦进来一个极度耗时的复杂任务,唯一的消费者会被其完全锁死,后续所有请求瞬间在身后积压成山。而在拥有 4 个消费者的系统中,一个长尾任务只能卡住 25% 的处理能力,其余 3 个消费者依然能够处理常规任务、推动进度;到了拥有 16 个消费者的系统中,唯有极端罕见的低概率事件——多个长尾任务同时抵达——才会将所有计算资源吞噬殆尽。这也导致了最危险的架构幻觉:16 个消费者的系统在崩溃的前一秒,响应时间依然能够保持在惊人的亚毫秒级,外观近乎完美,然而崩溃却是在毫无缓冲征兆的情况下在一瞬间突然暴发。这正是现代多机器、多线程架构最典型的写照。 - 随着排队等待耗时彻底主导计算耗时,任务响应时间分布从离散方差收敛为一条狭长水平带,呈现出表面上高度一致、实质上极其缓慢的病态特征。 - 成比例提升吞吐量与消费者数量(如 1x/1 对比 16x/16)无法延缓崩溃的绝对时间,但会极大地掩盖系统濒临崩溃时的前兆抖动。 - 复杂任务与长尾离群值的汇聚效应是压垮队列并发能力的直接推手。 --- ### [11:40](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=700s) 长尾任务的破坏力与无界队列的原罪 生产环境中真正让人焦头烂额的往往是开式系统(open systems),而主导开式系统生死的并不是均值或中位数复杂度,恰恰是处于长尾边缘的极端任务。工程师不能只盯住平均响应时间自欺欺人,必须极度审慎地监控那些异常复杂的离群值。这种监控不仅关乎下游服务的健康,更因为自身系统所产生的延迟毛刺与离群响应,会直接化为上游依赖方眼中的致命毒药,决定了整条链路的生死存亡。虽然增加消费者能够抹平系统早期的方差,但只要流量在短时间内突破极限,崩溃就是不可避免的物理法则。 ```mermaid flowchart TD accTitle: 面对过载时的三种数据处置策略 accDescr: 面对超出处理能力的数据时,系统可采取丢弃、拒绝或背压暂停三种策略。 subgraph Ingress["数据流入"] Traffic["突发过量流量"] end subgraph Strategies["系统过载处置策略"] Drop["策略 1: 丢弃 (Drop)<br/>• 传感器/遥测新数据覆盖旧数据<br/>• UDP 尽力而为(危机时最易丢包)"] Reject["策略 2: 拒绝 (Reject)<br/>• 返回 HTTP 503 / Fail Whale<br/>• 明确告知上游无法处理"] Pause["策略 3: 暂停 / 背压 (Pause / Backpressure)<br/>• 向上游施加阻力<br/>• 将决策权延后让渡给业务层"] end Traffic --> Drop Traffic --> Reject Traffic --> Pause ``` 基于这一必然性,Zach Tellman 给出了整场演讲中最核心的警示:“无界队列从根本上是坏掉的,因为它把系统的正确性完全交托到了别人手中(unbounded queues are fundamentally broken because it puts the correctness of your system in somebody else's hands)。”在开式系统中,服务端对外部数据的到达速率没有任何控制权,任何人都可以有意或无意地用海量请求将系统打垮。将“把所有数据死死捏在内存里,然后祈求好运发生”作为应对过载的策略,无异于自杀。当数据量超出负荷时,架构层面能采取的主动策略只有且仅有三种: 1. **丢弃(Drop)**:直接把数据扔掉。 2. **拒绝(Reject)**:把数据扔掉的同时明确通知发送方“我把它丢了”。 3. **暂停(Pause)/ 背压(Backpressure)**:通知发送方“我现在吃不下了”,迫使对方停止发送,直到系统具备处理能力再通知继续。 丢弃策略适用于极窄的应用边界,即新数据的到达会直接让旧数据失去意义,例如某些传感器监控或遥测链路。另一种丢弃场景则是“尽力而为(best-effort)”的场景,比如 StatsD 等监控工具基于 UDP 传输数据,把包抛出去就听天由命。Zach Tellman 犀利地指出,工程师往往滥用了这种模式,而残酷的讽刺在于:UDP 丢包率与网络处于危机状态的概率呈绝对正相关,“这意味着当你最迫切需要精准监控指标的时候,你反而最不可能拿到它”。至于拒绝策略,则是应用层最常见的手段,典型表现为网站过载时弹出的 HTTP 503 错误或早年 Twitter 著名的“宕机鲸(Fail Whale)”。在应用顶层面对汹涌请求时,服务并没有足够的上下文去做优雅的转圜,正如电商平台无法给未登录的访客发邮件说“稍后再来结账”一样,只能冷酷拒绝。 - 均值与中位数会掩盖致命隐患,排队系统中唯有离群值能真正解释灾难的演进。 - 无界队列剥夺了系统掌控自身生命周期的权利,将崩溃的主动权拱手让给调用方。 - 拒绝策略是应用层缺乏足够交互手段时的无奈下策,绝非理想设计,但系统必须具备明确拒绝的能力。 --- ### [15:19](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=919s) 暂停策略、core.async 与缓冲区的真正意义 当脱离特定的业务应用层,转而去编写通用的基础库、框架或中间件时,处理过载的最佳方式绝不是武断地决定丢弃或拒绝数据,因为通用组件根本无法知晓具体业务中丢弃数据是否可被接受,也不清楚该如何有意义地回绝客户端。此时唯一理性的做法是实施“暂停”,即通过背压(Backpressure)将选择权向上推迟给更懂业务场景的调用者。闭式系统天然具备这种协作控制,而对于异步工具库而言,必须从架构上显式设计这种协调机制。 这正是 Clojure 的 `core.async` 内部设计了三条队列而非一条单一队列的原因:除了存储正常数据的通道外,它还专门维护了一个用于存放写操作请求的 `puts` 缓冲区。当通道满载、背压生效时,写入操作不会被抛弃,而是被挂起并压入 `puts` 队列,系统明确告诉生产者“我正在处理,但现在还轮不到你”;一旦消费者完成读取、通道腾出空位,运行时便会唤醒这些被挂起的写请求,通知其可以继续推进。从抽象角度看,队列的作用是解耦“做什么”与“何时做”,用极其有限的物理资源去桥接潜在无限的需求波动,抚平供需双方在速率上的不可预测性。 然而,Zach Tellman 强调了一个极具颠覆性的关键论断:**对于实现上述所有解耦与削峰填谷的特性而言,作为中间存储介质的缓冲区(Buffer)在本质上是完全不需要的**。完全可以设计一个仅有 `puts` 和 `takes` 挂起队列、中间没有承载任何多余消息缓冲区的通道;在 `core.async` 中,通道的默认行为恰恰就是无缓冲通道(unbuffered channel / rendezvous)。无缓冲通道是一个纯粹的闭式系统(closed system),发送方必须等到接收方真正把消息接过去才能返回,实现了控制权的严格交接。 既然无缓冲通道如此干净且具备天然的闭式安全性,为什么软件系统中依然广泛存在缓冲区?其核心原因在于**背压的施加与解除从来都不是零成本的**。当为了执行背压而挂起一个操作时,恢复它必须付出高昂的系统开销: 1. 重启一个休眠线程,需要将调用栈重新拷回内存; 2. 恢复由于接收窗口归零而停滞的 TCP 连接,需要向网络发送确认报文,并等待拥塞窗口重新慢启动爬坡; 3. 从磁盘或数据库恢复拉取,必须等待物理 I/O 的漫长往返。 从发出“我准备好干活了”的信号,到上游真正把下一批数据送达之间所耗费的真空期,会直接让消费者处于饥饿状态,进而不可逆转地重创系统的整体吞吐量。缓冲区的唯一真正价值,就是为了吸收这部分切换开销,为背压的解除争取时间,从而保证消费管道始终被喂饱、维持最大吞吐。 - `core.async` 内部依赖专门的 `puts` 与 `takes` 挂起队列来实现精细的背压与控制权协调。 - 队列的核心价值(解耦时间与空间、用有限资源处理无限需求)在数学和逻辑上并不依赖内部 Buffer,无缓冲队列天然构成自律的闭式系统。 - 缓冲区的存在不是为了延迟数据,而是为了对冲背压挂起与唤醒时的物理延迟代价(线程唤醒、TCP 拥塞窗口重建、I/O 调度),防止因消费者饥饿导致系统吞吐量雪崩。 ### [17:42](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1062s) 背压、缓冲区的权衡与 Clojure 生态的断链 在系统设计中,利用缓冲区虽然可以使吞吐量更加平稳并维持在较高水平,但这必然是以牺牲系统延迟为代价的。面对海量数据涌入的现实,开发者必须提前规划好应对策略,因为数据过载在分布式环境中是无法避免的。在处理过载时,最理想的手段是在任何可能的地方引入背压(Back Pressure)。背压是一种最中立的处理策略,它只是单纯地下达通知:“我现在无法处理这些数据,具体如何应对由你决定。”这种信号可以沿着调用链一直向上传递至应用程序的最外围边缘,最终由边缘集中决定是拒绝请求、丢弃数据还是执行其他应对逻辑。这种决策逻辑必须保持集中化,绝不能零散地随处涂抹在代码各处。 虽然缓冲区带来了恒定的吞吐表现,但鉴于其对延迟的负面影响,应当仅在绝对必要之处使用,并且在实践中应极力避免将多个带缓冲的队列串联组合。因为每额外增加一级缓冲区,都会成倍放大下一级队列的延迟与波动效应。审视当下的 Clojure 生态系统,许多实践并未严格遵循这些准则。例如,尽管 `core.async` 本身完美实现了背压机制,但当它与某些网络通信机制结合时,往往缺乏正确的对接方式。这种问题有时源于外部标准的局限——比如 Java EE 的 WebSocket 规范中完全没有背压机制,导致在 Servlet 容器中运行相关服务时无解;但在其他情况下,这纯粹是库作者的疏忽,作者可能从未经历过生产环境下的背压崩溃,未意识到保持“背压黄金链条”完整的重要性。 面对这种情况,类库的使用者需要更加挑剔,在选型时提出更严苛的问题,确保底层架构不是建立在摇摇欲坠的基础之上。Zach Tellman 强调,这并非要贬低任何类库作者的贡献,而是为了确保生态系统能够经受住极端规模系统的检验: > “And so we as consumers of the libraries I think need to be more discerning, we need to ask the tough questions when we look at this sort of stuff and make sure that we're not building on top of shaky foundations.” - 应对过载的原则:在整个调用链中尽可能透传背压,并将拒绝或丢弃数据的决策集中在应用边界。 - 缓冲队列组合的风险:严禁随意将多个缓冲队列嵌套连接,避免各层缓冲放大延迟与排队效应。 - 生态现状的断层:Java EE WebSocket 规范缺乏背压支持;部分网络适配层在接入 `core.async` 时打破了背压链条。 --- ### [20:16](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1216s) 数据吞吐系统 Maw 的设计要求与早期隐患 为了更具体地说明这些理论如何在实际架构中落地,Zach Tellman 分享了他负责维护的一个名为 Maw 的开源系统。Maw 是一个“杂食性”的接收端点,其核心任务是吞噬合作伙伴和客户发送的所有数据。该系统的基本流程非常简单:接收海量输入数据,进行少量基础处理,然后将数据持久化存储以供后续批处理使用。为了避免在系统复杂性上过度投入精力,该系统构建在 AWS 之上,前端使用 Elastic Load Balancer (ELB) 将任意规模的流量分发给多台机器,后端则将数据写入高可用且具备副本机制的对象存储服务 S3。 该系统具备几项非常苛刻但特殊的业务属性:首先,它必须具备水平扩展能力。刚上线时系统的请求量约为每秒 40,000 次查询(QPS),而在一年半之后的高峰期已经达到了 300,000 QPS。其次,系统必须保持极低的维护成本。因为该系统只是 Zach Tellman 负责的众多项目之一,如果一个月内花在它上面的维护时间超过一天,就说明系统设计出现了严重缺陷。此外,它并非实时系统,只要数据能在当天结束前顺利落入 S3 即可。最关键的一项特征是:数据丢失与业务效用损失成正比。若丢失 1% 的数据,系统只承受约 1% 的价值损失,这与支付交易等丢失 1% 数据就会彻底瘫痪的系统完全不同。 在此之前,团队曾构建过一套摄取 Twitter “花园水管”(Gardenhose,即 Twitter 全部推文数据流的 10%,约为 600 条推文/秒)的系统。当时他们简单地把 HTTP 客户端流通过 Hadoop S3 客户端直接写入 S3,运行非常平稳。在开发 Maw 的初版时,Zach Tellman 沿用了这套思路:在前端搭建 Web 服务器,直接通过 Hadoop S3 客户端将约 20,000 条/秒的记录写入 S3。然而,这一设计为后续的系统灾难埋下了严重的隐患。 - **吞吐规模**:初始阶段承载 40,000 QPS,一年半内峰值跃升至 300,000 QPS。 - **运维目标**:系统必须做到极低维护开销,每月投入运维的时间不能超过 1 天。 - **业务容忍度**:数据丢失量与效用损失呈线性关系,允许在极端情况下牺牲极小比例的数据以保全整体可用性。 --- ### [22:50](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1370s) S3 写入故障、级联崩溃与解耦改造(Durable Queue 和 S3 Journal) 在初版 Maw 投入运行后,一个未曾预料的问题暴露了出来:AWS S3 会偶尔出现短暂的不可用状态,在短时间内完全停止接受写入请求。当 S3 停止写入时,隐藏在 Hadoop S3 客户端内部的内存无界队列开始持续囤积数据,寄希望于在连接恢复后一口气将数据刷入 S3。然而流入的数据速率实在太快,瞬间耗尽了机器的堆内存,导致单个节点发生 OOM 崩溃。该节点宕机后,原本由其承担的流量被负载均衡器自动切换到集群内的其他节点,随之引发多米诺骨牌效应,导致所有机器相继内存耗尽,整个系统全军覆没。 回顾这次事故,Zach Tellman 意识到在早期的 Twitter 客户端中必然也发生过类似现象,只是当时流量较小、重试成功而未暴露。事故的核心在于:系统将“数据接收成功”与“数据成功写入 S3”强行耦合在了一个步骤内,而写入 S3 需要跨越网络跳步并完成远程副本同步。事实上,系统当下唯一真正需要保证的,仅仅是将数据持久化存放到机器内存之外的安全介质上。本地磁盘的可用性远高于 S3,虽然单机硬件损坏会导致少量未同步数据丢失,但鉴于 Maw 的效用损失特征,这种小概率数据损失是完全可接受的。 为了彻底解耦这套链路,Zach Tellman 开发了两个开源库:`durable-queue`(基于磁盘持久化的轻量队列)和在其之上构建的 `s3-journal`。改造后的架构将处理划分为两个阶段:前端 HTTP 服务器收到数据后只负责将其快速写入本地磁盘队列即返回;另一侧则由独立的生产消费循环异步从磁盘读取数据并上传至 S3。此外,由于原先 Hadoop 客户端的一大缺陷是内部状态不可见,新架构为 `s3-journal` 和底层的持久化队列注入了极其完备的度量指标(Metrics),能够实时监控入队量、上传成功量、内存驻留的文件切片(Slabs)数量、待办任务以及重试次数。Zach Tellman 强调,通过指标感知系统健康是运维不可预测系统的基石: > “These are all things that are absolutely critical, because we cannot prove properties about our system, right? The system is too complicated, we can only look and see what happens uh in as much as the metrics that we have created for ourselves allow us to do that.” ```mermaid flowchart LR accTitle: Maw 系统解耦与双阶段持久化架构 accDescr: 展示 HTTP 接入层写入本地磁盘队列,再由后台工作进程将数据异步持久化至 S3 的两阶段结构。 subgraph Phase1["阶段一:接收与本地持久化"] A["外部流量 / 合作伙伴"] -->|"HTTP 请求"| B["Aleph / Netty HTTP 服务器"] B -->|"同步快速写入"| C[("durable-queue<br/>本地磁盘队列")] B -.->|"立即返回 200 OK"| A end subgraph Phase2["阶段二:异步上传至 S3"] C -->|"消费就绪数据"| D["S3 Journal 上传进程"] D -->|"批量写入"| E["AWS S3 对象存储"] end ``` - **开源组件支撑**: - `durable-queue`:基于磁盘的高吞吐持久化队列,在生产环境中已稳定经受了 3 至 4 万亿条事件的检验。 - `s3-journal`:基于 `durable-queue` 构建的双阶段同步组件,将本地持久化与远程批处理上传完全解耦。 - **可观测性指标(Metrics)**:监控驻留内存的 Slab 数量、已入队与已上传记录差值、挂起任务数及网络重试次数,消除黑盒状态。 - **摄取端压力**:HTTP 摄取端(基于 Aleph 与 Netty)必须极快响应,若不能迅速向数据提供方返回确认,上游客户端便会主动降级、缩减推送的数据量,违背了系统尽量收集所有数据的初衷。 ### [26:49](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1609s) 隐藏的线程池与度量指标偏差 在 Netty 等底层框架中,通常采用非常特殊的线程模型:每当一个新连接建立时,它就会被分配给某个特定的线程,而单个线程往往同时负责处理许多个连接。这种机制潜藏着一个严重风险:一旦其中某一个连接霸占了该线程(例如在该线程上执行阻塞操作),就会隐式地导致该线程上承载的所有其他连接一同被阻塞,而那些连接本身并未参与这一阻塞决策。为了解决这一问题,通用的做法是在其后方引入另一个线程池来分摊负载,确保没有任何单个恶意或异常连接能够拖垮其他正常的请求。 ```mermaid flowchart TD accTitle: Netty线程模型与后端隔离线程池 accDescr: 展示单个连接阻塞导致同线程其他连接被连带阻塞,以及通过后端线程池隔离故障的结构。 subgraph NettyLayer["Netty 线程模型 (事件循环)"] direction TB C1["客户端连接 1 (阻塞行为)"] --> T1["Netty 线程 A"] C2["客户端连接 2 (正常)"] --> T1 C3["客户端连接 3 (正常)"] --> T2["Netty 线程 B"] end subgraph Failure["隐式连带故障"] T1 -.->|"连带挂起"| C2 end subgraph BackendLayer["后端隔离线程池 (Instrumented Thread Pool)"] direction TB T1 --> TP["任务工作队列"] TP --> W1["Worker 线程 1"] TP --> W2["Worker 线程 2"] end ``` 然而,这也带来了度量上的巨大盲区。如果仅仅在 Ring 处理函数(Ring Handler)中埋点测量响应时间,所得到的数据与真实的客户端响应时间相差甚远。真实耗时应涵盖数据包从到达网卡到响应包完全发出的全生命周期,但因为线程池本身缺乏良好的度量机制,它们在系统内部就像一个透明的黑盒——任务被放进队列,在里面等待一段时间,然后不知何时才被最终执行。Zach Tellman 为此编写了一个工具库,提供可度量(instrumented)的线程池,用于精确跟踪相关的各项统计数据。 - 在标准的 Ring Handler 中记录的响应耗时严重失真,遗漏了在线程池队列中静默排队的大量时间。 - 线程池通常缺乏良好的可观测性,任务在队列中的滞留时间对上层应用几乎是不可见的。 - Zach Tellman 编写的库在运行服务上暴露了 JSON 格式的度量端点,用于全面监控队列与任务的各项运行时指标。 ### [28:07](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1687s) 分位数统计、队列延迟与容量规划 在分析服务暴露的监控指标时,单纯依赖标量或平均值是极具误导性的。任务拒绝率(rejection rate)、完成率(completion rate)和到达率(arrival rate)必须使用分位数(quantiles)来表达。由于无序且未经协调的请求会突发性地集中到达或完全中断,系统的负载本身就充满尖峰。监控数据显示,到达率与完成率在 50% 中位数与 99.9% 分位数之间存在约 3 倍的差距,容量规划必须基于这类尾部分位数进行权衡。同时,指标清晰地揭示出系统因容量耗尽而偶尔拒绝任务,尽管概率极低,但这正是容量治理中必须持续追踪的关键点。 ```mermaid flowchart LR accTitle: 任务端到端耗时分解 accDescr: 说明任务总耗时(Task Latency)如何由排队耗时(Queue Latency)和实际计算耗时(Execution Latency)组成。 subgraph TaskLatency["任务总耗时 (Task Latency)"] direction LR QL["排队耗时 (Queue Latency)"] --> EL["实际执行耗时 (Execution)"] end ``` 在耗时度量上,任务耗时(task latency)是排队耗时(queue latency)的超集,前者代表任务在队列中等待加上实际执行的总耗时,后者则仅测量在队列中排队的时间。在采样统计下,甚至可能出现队列耗时的 99.9% 分位数高于任务耗时 99.9% 分位数的奇特现象,这源于统计采样偏差,说明指标并非百分之百高保真,但已足够支撑系统洞察。更具启示性的是,尽管该系统配置了一个长度始终为 0 的无缓冲队列(unbuffered queue / SynchronousQueue),请求穿透该队列依然可能花费多达 2 毫秒。这说明在应用层无缓冲队列的底层,Java 执行模型乃至操作系统中仍潜藏着大量有缓冲队列。Zach Tellman 警告道:“有深度的指标能让我们不再故意视而不见、闭上双眼假装一切安好。” - 中位数(Median)与 99.9% 分位数(P99.9)在到达率与完成率上存在高达 3 倍的差距,直接决定了系统的容量预估边界。 - 零长度的无缓冲队列在 P99.9 场景下依然观测到 2 毫秒的穿透排队延迟,暴露出底层 JVM 与运行环境中的多层隐藏缓冲。 - 不能将生产者到消费者的任务交接视为无耗时的瞬时操作,测量必须涵盖任务入队与流转的真实开销。 ### [30:11](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1811s) 性能基准测试与盲区认知 构建系统时,明确“完成”(completion)的语义边界至关重要。如果大量任务处于未完成状态,系统就必须被迫应用背压(backpressure)、丢弃数据或直接拒绝请求;反之,若能合理重新定义完成的标准,让状态闭环变得更容易,将极大提升系统的稳定性。在技术选型方面,不能盲目崇拜跑分基准。基准测试固然能证明工具具备高速运行的潜力,但那些脱离实际的人造测试假设很难在复杂现实中成立。因此,能够主动暴露内部运行状态、提供度量能力的工具,远比单纯宣传原始吞吐量的黑盒工具更有价值。 与此同时,开发者必须清醒认识到系统外部永远存在无法被度量的不可控现实。例如,移动端用户在使用 Web 应用时驾车进入隧道,导致连接中断数秒。虽然服务后端无法阻止这种网络物理断连,但对于客户端来说,这几秒的无响应正是他们所感知到的全部系统延迟。Zach Tellman 指出:“对于工具的选择,原始性能固然好,但你应该始终偏向于那些真正能告诉你它到底有多快的工具,因为那些非常生硬的人造基准测试中的假设,并不一定会映射到现实中。” - “完成”的定义决定了背压触发的阈值;如果能降低任务“完成”的判定成本,就能极大地缓解排队积压。 - 工具的透明度与自省能力(可观测性)优先于脱离实际场景的理论基准测试成绩。 - 真实感知延迟往往受制于网络环境等外部因素,系统不可能做到全链路绝对监控,但必须分清内部可控与外部感知的界限。 ### [31:25](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1885s) 聊天室场景与 Manifold 事件总线 为了具体阐述流系统中的队列与背压问题,Zach Tellman 以 Node.js 等技术栈中最常见的玩具级案例——广播聊天室(Chat Server)为例。在该场景中,用户加入房间并实时交流,任何人的发言都会向房间内的所有人广播。Zach Tellman 选用自己编写的 Manifold 库进行演示,尽管 core.async 或其他流抽象库也能实现类似功能,但 Manifold 提供了简洁且适宜该场景的高阶操作符。 Manifold 中的核心抽象之一是事件总线(Event Bus),它完美实现了发布-订阅(Pub/Sub)模式。生产者可以向特定主题(Topic)发布消息,该主题下的所有订阅者都会接收到这条消息;若当前无订阅者,操作则等同于空操作(noop),消息直接丢弃消散。在此基础上,聊天室的核心连接处理逻辑(Chat Handler)可以极度精炼: ```mermaid flowchart LR accTitle: 聊天室 Pub/Sub 与连接交互流 accDescr: 说明客户端输入流发布至房间总线,以及房间总线流回填至客户端连接输出的过程。 subgraph ClientSession["单个客户端连接"] direction TB CIn["连接输入 (Inbound Stream)"] COut["连接输出 (Outbound Stream)"] end subgraph EventBus["Manifold Event Bus (房间主题)"] direction TB Topic["Room Topic"] end CIn -->|"publish"| Topic Topic -->|"connect / pipe"| COut ``` 当新连接接入时,系统首先解析其目标房间,将该连接输入流中的每一条消息发布到对应房间主题;同时,系统连接到该房间主题,将房间内广播的所有消息管道传输(pipe)回客户端的输出流中。这虽然用几行代码实现了 IRC 的核心逻辑,但其设计极度脆弱:因为广播发布操作要求将消息投递给所有订阅者,一旦房间内出现哪怕一个接收缓慢的客户端,发布操作就会被挂起,进而拖慢整个房间内的所有人。 - Manifold 的 Event Bus 在无订阅者时执行 noop 操作,避免了资源无端泄漏。 - 原始的广播模型缺乏隔离,发布操作依赖于所有消费者的即时响应,单个慢消费者的阻塞会直接波及全局。 - 极简的流对接(piping)虽然代码紧凑优雅,但若不显式处理消费速率不均衡问题,系统将面临灾难性的连带脆弱性。 ### [33:12](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=1992s) 慢消费隔离、超时淘汰与防恶意洪泛 为了防止房间内单个消费缓慢的“不良参与者”破坏整体系统的稳定性,通常直觉的做法是在房间到客户端的管道间引入缓冲队列(Buffer),以吸收瞬时波动。但仅仅以消息数量作为缓冲区容量是不严谨的,因为单条消息可能仅有一个单词,也可能是一整部长篇小说;更为稳妥的方式是基于消息字节大小设置上限(例如累计 1000 字节)。然而,增加缓冲区只是在向后拖延故障爆发的时间,并没有从根本上改变系统终将因消费不及时而崩溃的本质。 ```mermaid flowchart TD accTitle: 慢消费者超时淘汰与速率节流 accDescr: 展示消息缓冲、超时强行剔除慢客户端以及在连接入口增加速率限制的防护链路。 subgraph InboundDefense["入口防护 (发布端)"] RawIn["原始消息输入"] --> Throttle["throttle (1 条/秒)"] Throttle --> Bus["发布至房间总线"] end subgraph OutboundDefense["下发防护 (消费端)"] Bus --> Buf["大小限制缓冲 (如 1000 字节)"] Buf --> ConnectTimeout{"投递是否超时\n(如 10 秒缓冲区持续满载)?"} ConnectTimeout -- "正常接收" --> Client["客户端接收"] ConnectTimeout -- "超时未接" --> Kick["强制断开连接并注销"] end ``` 在不可逆的慢消费面前,系统唯一的解法是在超时后将其强行踢出。Manifold 的 `connect` 操作支持定义下游超时,若尝试向下游发送消息并在长达数秒(例如 10 秒)内因其缓冲区满载而无法投递,服务端将直接关闭该连接并注销所有资源。Zach Tellman 强调说:“这是我们唯一能做的事情,再也没有其他任何办法能将这个人从他自己的迟缓中拯救出来。” 然而,踢出慢消费者的机制却引发了一个致命的次生攻击场景:一旦有用户发送消息过快,瞬间撑爆所有正常人的缓冲区,导致整个房间的所有人都因超时被系统踢出。无论这种洪泛行为是源于恶意攻击还是无意识的程序错误,服务提供者都必须承担防御责任。系统的底线必须是:保护绝大多数正常用户不受极少数异常行为的侵害。因此,必须在消息发布的入口处增加节流机制(throttle),例如严格限制每个连接每秒只能发送一条消息,从而在物理源头上封堵压垮全场广播的漏洞。 - 缓冲区不论基于数量还是字节大小,都只是推迟了背压导致的故障,无法杜绝慢消费者带来的系统淤塞。 - 解决顽固慢消费者的终极手段是设置强行投递超时(Timeout Kick),超时即单方面切断套接字连接。 - 针对慢消费者的踢出机制可能会被高速发送者武器化,诱发“一人发狂、全员被踢”的雪崩,必须在前置入口处通过 `throttle` 施加严格的发送速率限制。 - Zach Tellman 总结道:“确保你的系统能够抵御来自外部世界的冲击,是你的职责所在……确保多数人不会因为极少数人的行为而遭受连带之害。” ### [35:37](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=2137s) 速率限制机制与系统边界设计 在处理入站连接时,系统并不一定需要与出站场景相同的断开连接(disconnect)逻辑。如果某个客户端发送消息过于频繁,系统可以直接利用 TCP 背压(TCP back pressure)机制,拒绝读取新数据并告知客户端“不,我们现在不接收”;如果这导致客户端自身崩溃,那就是客户端自己的问题。针对消息限流(throttling),单纯将速率限制死在“每秒一条消息”在实际体验中可能会让人感到恼火。例如,假设某个用户静默了 10 分钟,他们之后依然只能每秒发一条消息,还是可以一口气发送相当于 10 分钟积累量的消息?这两种方式都属于限流讨论的范畴。 ```mermaid flowchart LR accTitle: 令牌桶限流与突发信贷模型 accDescr: 展示用户静默期积累信贷(最高10秒)并在突发流量时消耗信贷的过程。 subgraph Bucket["信贷/令牌桶 (最大容量 10s)"] direction TB Tokens["积累信贷: 0 ~ 10 秒"] end Client["客户端消息流"] -->|"请求发送"| Gate{"是否有可用信贷?"} Gate -->|"是 (扣除信贷)"| System["接收消息并路由"] Gate -->|"否 (达到上限)"| Backpressure["触发 TCP 背压 (拒绝接收)"] ``` 为了兼顾这两种场景,可以在限流器中设定一个允许积累的最大信贷(credits)上限——例如相当于 10 秒的额度。如果一个客户端连续静默 10 秒,它随后便可以瞬间突发发送 10 条消息,但绝不能超出这个限制。通过引入这些具体的数值与约束,原本抽象、普适的系统概念逐渐落地成一个具有清晰边界条件的实体。构建高可用系统的核心正是做出这些权衡:“我们必须决定愿意为每个人分配多大的内存空间、愿意等待他们多久,以及允许别人向我们发送多少消息。” - **TCP 背压应对恶意或过快发送方**:无需在应用层主动编写复杂的踢人断开逻辑,直接通过暂停读取利用底层 TCP 窗口填满自然施加反压。 - **突发信贷(Burst Credits)设计**:将限流速率设定为 1 条/秒的同时,允许最高累积 10 秒的突发额度,避免合法用户的间歇性发言受到生硬压制。 - **走出抽象乌托邦**:系统设计不能停留在通用的理论模型上,必须明确为每个连接分配的内存上限、超时等待阈值与请求速率配额。 > “And so all of a sudden we've gone from this thing which was a little bit handwavy a little bit vague very general, everybody could sort of project their particular chat room onto it, and now this is a very precise chat room right, maybe not a very good one, but it's certainly precise in terms of what it does.” —— Zach Tellman --- ### [37:18](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=2238s) 动态拓扑的监控难题与 Manifold 的拓扑追踪 聊天室这类系统与其他只有一两个固定队列的简单系统存在本质差异。在固定基数(fixed cardinality)系统中,队列数量是明确的,运维人员可以为每个队列绘制图表、挂载监控指标并清晰地理解其运行状态。然而在聊天室中,队列的数量难以计数,且处于持续不断创建、销毁与重构的动态流动之中。传统的“观察单个队列”的监控方式彻底失效,当底层拓扑实时发生变化时,开发者极难从宏观上审视整个系统的全貌。 在 `core.async` 中也普遍存在类似痛点:数以千计的 Go 协程(go blocks)在各个方向上互相推挤消息,系统外部的人根本无法一览全局。为了缓解此问题,Zach Tellman 在自己的异步库 Manifold 中尝试了一套拓扑感知机制。在 Manifold 中,开发者可以查询任意一个数据流(stream),检查其下游连接(downstream)了哪些流。这是因为在调用 `connect` 或 `consume` 等方法时,Manifold 都会在内部拓扑结构中打上标记,从而赋予开发者一定程度的拓扑追踪能力。 尽管可以追踪拓扑,但如何直观地呈现这些数据仍是一个挑战。Zach 提到他早前曾尝试使用 Graphviz 渲染可视化架构图,但很快发现该方案完全无法应对规模化场景,只要拓扑稍微复杂一点,生成的关系图就会混乱到难以辨识。未来的方向可能是探索更优秀的专用可视化工具,或者定期将拓扑快照转储到磁盘供后续离线分析。 - **固定队列 vs 动态流**:固定队列基数允许建立静态 Dashboard 指标体系;而高并发多租户系统下的队列处于动态重组状态,传统队列级监控无法反映系统全貌。 - **`core.async` 的不透明性**:协程与通道错综复杂地相互交织传递数据,缺乏整体把握和拓扑审视工具。 - **Manifold 的拓扑标记机制**:通过流操作 API(如 `connect`、`consume`)自动维护内部下游有向图结构,支持运行时向上或向下探测数据流向。 - **可视化伸缩性瓶颈**:简单的 Graphviz 自动布局在面对复杂的动态流拓扑时极易失效,需要更强大的拓扑追踪与磁盘快照分析方案。 --- ### [39:20](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=2360s) 队列理论的三大核心原则与总结 构建健壮的 Clojure 系统不能依赖侥幸心理。Zach 对队列及异步系统设计的核心教训进行了终结性梳理,提炼出构建弹性系统必须恪守的三大基本法则: 1. **世界上不存在真正的无界队列(Unbounded Queues)**:任何所谓的无界队列本质上都被物理内存或某些固定的共享系统资源所严格限制。一旦数据溢出耗尽这些底层硬件资源,整个系统就会彻底崩溃。 2. **在系统边缘做好防爆,在非边缘层坚持传递背压**:当应用直面不可控的外部流量边界时,必须主动制定过载应对策略(如抛弃消息、断开连接或报错);但在组件内部、中间件或编写可复用的通用代码时,严禁自行拍脑袋丢弃数据,而必须始终使用背压,将决策权上抛给更了解业务全貌的调用方。 3. **在所有地方坚持索取监控指标(Metrics)**:即使某些内部状态处于视野盲区,也必须对指标保持极高的诉求,永远清晰地知道如何向下深挖一层以排查隐患。 演讲在精准卡点耗尽时间时落下帷幕,Zach 宣布正式演讲结束,并将剩余的互动移步至场外走廊进行深入讨论。 > “The things we learned are that unbounded queues aren't unbounded, right, they're bounded by memory or some other sort of fixed shared resource that will cause our system to fall over once that's exceeded.” —— Zach Tellman > “Where we're not at the sort of boundaries of application, we're trying to write something which is more general or less opinionated or can be reused elsewhere, we should always use back pressure, right, we should always defer to someone else who knows better than us what to do.” —— Zach Tellman ## Notable quotes > [03:03](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=183s) The reason that cues are so ubiquitous is because they separate what we want to have happen uh from when it happens — Zach Tellman > [11:40](https://www.youtube.com/watch?v=1bNOO3xxMc0&t=700s) unbounded queues are fundamentally broken because it puts the correctness of your system in somebody else's hands — Zach Tellman ## Mentioned Zach Tellman, Clojure, core.async, Java, JVM, BlockingQueue, AWS, AWS S3, Elastic Load Balancer, Hadoop, Twitter, Maw, durable-queue, s3-journal, Netty, Aleph, Ring, Manifold, StatsD, Graphviz, Node.js ## Video description Queues, it seems, are back in fashion. This is exciting, as queues let us explore problem spaces that were unreachable by pure, immutable primitives. But queues, and the execution models that drive them, are very different than what we're used to. Naive use of libraries like core.async will create brittle, unpredictable systems that fall over when we least expect. This talk will cover the basics of queueing theory, and its practical application in Clojure systems large and small. [Source](https://www.youtube.com/watch?v=1bNOO3xxMc0)

Source quotes

[3:03] Zach Tellman: The reason that cues are so ubiquitous is because they separate what we want to have happen uh from when it happens [11:40] Zach Tellman: unbounded queues are fundamentally broken because it puts the correctness of your system in somebody else's hands

Source quotes

[17:42] Zach Tellman: And wherever possible we should use backpressure, because that's the most neutral choice that we can make, right? That just kind of tells someone else, ‘I can't handle this data, you figure out what that means. [29:44] Zach Tellman: There’s always something more to measure, but the idea is that we just don’t want to be sort of willfully blind and close our eyes and pretend that everything’s okay.

Original transcript

[0:00] so we're here to talk about [0:01] cues uh and you know some of you may [0:05] have wondered uh why uh cues really are [0:09] pretty simple right you have a producer [0:11] that Ines messages you have a consumer [0:13] that dqes messages typically but not [0:15] always in the order that they were added [0:18] and that's about it uh but something [0:21] that is maybe non obvious is that in [0:23] addition to the active ining being a [0:26] side effect right we get back to just [0:27] confirmation that was added because what [0:29] we're getting back is not a function of [0:31] the data uh the data itself can only [0:33] describe a side effect right Q's are a [0:35] way of dealing with actions and for this [0:37] reason I think in the closure Community [0:39] it's always been a bit of a sideline [0:41] right you know Java provides us with [0:43] cues we use them occasionally we [0:44] abstract over them where possible but we [0:46] don't really uh deal with them headon or [0:49] at least not until we had core async [0:52] which brought it sort of more into the [0:54] central conversation which I think is a [0:56] good thing because we of course are in [0:58] the business of making computers do [0:59] things right we don't just raise the [1:01] ambient temperature of the room and so [1:04] looking under the covers uh a coring [1:06] channel has not one but three cues it [1:08] has a buffer that holds the messages but [1:10] it also has a puts que where producers [1:13] that are trying to add to a full buffer [1:15] will wait and it takes CU where [1:16] consumers that are trying to take [1:17] messages from an empty buffer will [1:21] wait but once we've sort of Traverse [1:24] this once the producer has successfully [1:25] handed off a message to the consumer uh [1:28] that's not the end of the story because [1:29] the consumer has provided a call back [1:31] wherein it says what it wants to do with [1:33] the message and that call back has to be [1:34] run on a thread pole and a thread pole [1:37] in Java has another que that sits [1:39] between the people who want something uh [1:41] to be done and the cues that can do it [1:43] and typically the size of a thread poool [1:46] is dynamic and what causes a thread pool [1:47] to grow is uh trying to add something to [1:50] the block and queue and finding that the [1:51] queue is full at which point a new [1:53] thread will be spun up unless we have [1:55] the maximum number of threads whatever [1:56] we Define that to be in which case it [1:58] will throw a reject execution exception [2:01] which is something that we should always [2:02] be aware of when using a thread pole and [2:05] the blocking que under the covers looks [2:06] a great deal like core async it has a [2:08] buffer that holds the messages and it [2:10] has these things called conditions or [2:12] condition cues that are called not full [2:14] and not empty respectively uh unlike a [2:16] typical queue though it's just a place [2:18] for Threads to park themselves there's [2:19] no actual conveyance of information and [2:21] so when a producer comes in and finds a [2:23] buffer to be full it will release all [2:25] the locks and will park itself at the [2:26] not full condition and way to be [2:28] notified that the buffer is not full [2:30] and uh when it's notified only one of [2:32] the threads that are waiting there will [2:33] actually uh be woken [2:36] up and the conditions uh are buried [2:39] somewhere inside of the jvm [2:40] implementation I'm not quite sure how [2:42] they work but these are a way for [2:43] Threads to sort of coordinate amongst [2:44] each other but even those threads on the [2:46] OS level have other another cue which in [2:48] which they are sort of scheduled and [2:50] even those sort of software threads have [2:52] to be queed onto the hardware threads or [2:54] the cores on our machines and so between [2:57] us and everything we want a computer to [2:58] do there's probably about half half a [2:59] dozen Q's uh just sort of intermediating [3:03] that and so cu's are ubiquitous they're [3:06] everywhere and it sort of behooves us on [3:08] some level to understand them and the [3:10] reason that cues are so ubiquitous is [3:12] because they separate what we want to [3:15] have happen uh from when it happens and [3:17] this is really valuable because most of [3:19] the time when we write code we don't [3:21] know what else is going on we don't know [3:22] the context in which it's running we [3:23] don't know what other requests are [3:24] showing up at what point they're showing [3:26] up uh what the sort of relative priority [3:28] is and so the best thing that we can do [3:29] is just say that at some point we'd like [3:31] this to [3:32] happen and sort of because again there [3:36] of this ubiquity it behooves us to [3:38] understand how cues work how they fail [3:40] what happens when they get overloaded [3:42] and uh luckily there is a thing called [3:44] queuing Theory which unfortunately is [3:47] more a branch of Statistics than it is a [3:50] branch of anything related to software [3:51] specifically and I I stand in front of [3:53] you not as an expert in queing theory [3:54] this is a slightly snarky slide which I [3:57] can't quite back up but uh I also stand [4:00] before you as someone who has worked [4:01] with systems a lot and so I've also made [4:04] a couple of pretty honest efforts to [4:06] wrap my head around queuing Theory and [4:07] what I'm presenting to you is the things [4:09] that seemed to intersect the things that [4:11] made sense to me that sort of informed [4:13] my intuition for building with systems [4:15] and if any of you at the end of this [4:17] talk are interested in want to learn [4:18] more uh this is by far the best book [4:20] that I found uh it's still about 90% [4:22] pure statistics but it does mention [4:24] computer systems occasionally and uh if [4:29] you read this uh what sort of resonates [4:30] for you what sort of fuels your [4:32] intuition may very well be different and [4:33] so I really do encourage you to not take [4:35] my word as the final word on the subject [4:38] but with that said uh one of the key [4:41] distinctions in queuing theory is [4:43] between closed and open systems a closed [4:45] system is where as we produce something [4:47] we must wait for the consumer to [4:49] complete it before uh producing [4:51] something else and we're all very [4:52] familiar with an example of this which [4:54] is a reppel right uh if it reads in our [4:56] input it must evaluate and print the [4:58] result before looping back another [5:01] example of a closed system is a single [5:02] person browsing a website they receive a [5:04] page they click on the link and they [5:06] must wait for the next page to load [5:07] before they can click again and so [5:09] naturally they're sort of seeding [5:11] control to each other and closed systems [5:14] are uh certainly a thing that we deal [5:16] with and they are as it turns out fairly [5:18] easy to reason about but many of the [5:19] things that give us trouble and many of [5:21] the systems that we build are open [5:23] systems where there is no coordination [5:25] where when a request comes in we're [5:27] processing it it's almost a given that a [5:28] request is to come in before we're [5:31] ready [5:33] and I'm going to talk about some sort of [5:36] more concrete examples of open systems [5:38] but it's a little bit hard for me to [5:40] show you AWS graphs or something else [5:42] and really demonstrate the properties of [5:44] the system there are too many sort of [5:46] Dimensions to capture there and so I'm [5:48] going to first show you a [5:50] simulation of a uh an open system to [5:53] sort of demonstrate the uh more abstract [5:56] properties and then move on to something [5:57] a little bit more concrete [6:01] so in this simulation uh the producers [6:05] are uh exponentially distributed which [6:07] means that the time between each arrival [6:09] follows an exponential [6:10] distribution and this empirically has [6:13] been shown to be a pretty good model it [6:15] models how often people show up to Banks [6:17] or to food trucks or to uh any sort of [6:21] other place where there are people [6:22] competing for resources without any sort [6:24] of coordination with respect to each [6:26] other uh but the complexity of the T [6:29] masks uh is not very well modeled by an [6:32] exponential distribution and I'm showing [6:33] you here a graph that looks quite a lot [6:35] like the previous one but uh the par [6:37] distribution uh follows this sort of uh [6:40] configurability of how fat the tail is [6:43] right if you've heard of the 8020 rule [6:44] that is a particular sort of ratio [6:46] between the uh sort of top end of the [6:49] distribution and the bottom end and we [6:51] are able to sort of control that to to [6:53] determine how often these outliers come [6:55] in how often we get unusually complex [6:58] tasks and so I'm going to have a [7:00] simulation where uh tasks are sort of [7:03] arriving according to this exponential [7:04] distribution which means that even if [7:06] we're getting for instance 1,000 queries [7:08] per second they're not showing in up at [7:10] regular 1 millisecond intervals right [7:12] they'll sort of pile up on top of each [7:13] other and there'll be these long periods [7:15] where nothing happens uh the tasks [7:18] however are going to be uh par [7:19] distributed which means that we have [7:20] this sort of very thin tail and over [7:22] time it's going to grow a little bit [7:23] fatter and if he wants to have sort of a [7:26] reason why this might happen it might be [7:27] that your disc is a little bit more [7:28] underload or the VM uh that you're is [7:31] sharing the machine with is taking up a [7:33] little bit more and so your machine is [7:34] just a little bit slower it's a subtle [7:35] change but in this simulation uh the [7:40] best way that I have to sort of [7:41] demonstrate what happens is using what's [7:42] called a spectrogram and I don't know [7:44] how many of you have ever seen something [7:45] like this so I'll take some time to sort [7:47] of describe what's going on here uh the [7:50] vertical axis shows the latency which is [7:53] the time that elapses between a task [7:55] coming in it traversing the queue it [7:57] being consumed and completed uh and [8:00] we're showing this on a logarithmic [8:01] scale without any sort of numbers by the [8:03] way because everything here is made up [8:04] but it's really just to show sort of the [8:06] relative uh Behavior but uh on a [8:08] logarithmic scale because towards the [8:10] end uh as the system sort of becomes [8:12] unstable uh it's the only way to make [8:14] everything visible next to each other on [8:17] the horizontal axis we have the movement [8:19] of sort of the distribution from a thin [8:21] tail right where most of our tasks are [8:22] very easy and only a few are more [8:24] complicated to a fat tail where [8:27] proportionately more of them are [8:28] complicated but again this is a very [8:30] subtle change but we see that at the [8:32] left side right there is this very tight [8:34] distribution because the heat map here [8:36] represents the probability that uh any [8:39] one of our tasks took the most amount of [8:40] time and there's this very thin red [8:41] strip that represents where most of our [8:42] tasks are that most of our tasks are [8:44] completing in a reasonable amount of [8:45] time but as it begins uh to kind of get [8:48] more and more complex we see that it [8:50] starts to creep up and about 2third of [8:52] the way through we have uh this sort of [8:55] jump to a new equilibrium all the red [8:56] dots are sort of scattered several [8:58] orders of magnitude above where it was [9:00] before this is an equilibrium we're not [9:01] out of control yet but everything is [9:03] significant significantly slower than it [9:05] was before and then as it continues we [9:08] see that it gets more and more out of [9:09] control until it just sort of takes off [9:11] like a rocket and this is uh what [9:14] happens when a q grows out of control [9:16] right this is a system in crisis uh this [9:18] is what will cause our system to run out [9:20] of memory and [9:21] crash and you'll also notice that uh [9:23] whereas before the distribution was very [9:25] sort of tightly bound now it's this this [9:27] very sort of thin strip and this is [9:28] because [9:29] uh the time being spent on the Queue the [9:31] time that you're waiting behind all the [9:32] other tasks that are queed up dominates [9:34] your time and all these are sort of [9:36] averaging out right we now have actually [9:38] a very uh consistent but slow uh time [9:41] that's being taken for each of these [9:42] tasks and so we know when a system is [9:46] out of capacity when is uh things are [9:48] running too slowly we just add more [9:50] machines and this does exactly what you [9:51] would expect here we add an additional [9:53] consumer and we basically keep up we can [9:55] see towards the end that it begins to [9:57] flare up and maybe if this sort of trend [9:59] continued it would also fall over but [10:01] not [10:04] yet and again you know if we add even [10:06] two more machines uh it's barely phase [10:09] right it's uh Bas it's well within sort [10:11] of its operating uh [10:13] abilities a less obvious thing though is [10:16] what happens when we have additional [10:19] consumers but increase the incoming rate [10:21] by a commensurate amount so here we have [10:23] sort of a one x rate and one consumer [10:25] then we have a 4 x rate and four [10:26] consumers and 16 and 16 [10:29] and the key thing here is that all of [10:31] these have failed at exactly the same [10:33] time right this is in no way uh [10:35] increasing sort of our ability to handle [10:37] volume but what they look like as they [10:40] lead up to failure is very different and [10:43] sort of conceptually why this is is that [10:45] every time that an expensive task comes [10:46] in uh in the case where you only have [10:49] one consumer it's focused entirely on [10:51] that right and things are starting to [10:53] kind of pile up behind it right these [10:55] longtail tasks these complex tasks uh [10:57] completely our ability to keep [10:59] up but when you have four consumers only [11:01] three out of the four consumers uh can [11:03] sort of continue but we still are able [11:05] to make some forward progress and [11:06] likewise with the 16 it we have to have [11:09] a very unusual Confluence of events come [11:11] in for us to be completely uh sort of [11:13] taken up with these uh longtail tasks uh [11:17] but you'll notice that uh in the case of [11:19] the 16 it looks very very good almost up [11:22] until the point where it falls over and [11:24] I will point out that this is the best [11:26] sort of uh analog for the systems that [11:28] we build we have multiple machines we [11:29] have multiple threads and so we can [11:31] absolutely have a system which is [11:32] responding in submillisecond time right [11:34] up to the point where it becomes [11:36] completely unstable right uh it happens [11:38] pretty much all at [11:40] once and so what we've learned here is [11:43] that uh your systems are typically open [11:45] ones or at least the ones that you will [11:47] struggle to sort of understand as they [11:49] run in production and it's the long [11:51] taale of the of your tasks that cause [11:54] your system to kind of fill up right and [11:56] so we can't just look at what the mean [11:59] or median complexity we have to be very [12:01] aware of what the unusually complex [12:03] tasks look like and track that very very [12:06] closely and this is important not just [12:08] because of we us looking at the [12:10] complexity of things that are Downstream [12:11] of us but also because the uh outliers [12:14] in our own response time are what will [12:16] make the systems that are relying Upon [12:17] Us stable or unstable and so using more [12:20] consumers will reduce our variance right [12:22] up until the point where the system [12:24] actually falls over which is inevitable [12:26] right there is no way that we can [12:27] protect against this data at some point [12:29] will outstrip our ability if only for a [12:31] short period of time and so the real [12:34] lesson here and the one thing that I [12:35] hope that you take away from this is [12:37] that unbounded cues are fundamentally [12:40] broken because it puts the correctness [12:42] of your system in somebody else's hands [12:44] right you have no control over how fast [12:46] the data is showing up and so that means [12:49] that someone can uh intentionally or [12:51] unintentionally cause your system to [12:53] break and so we cannot have our strategy [12:56] for dealing with too much data to hold [12:57] on to it and hope for the best [12:59] we have to you know do something a [13:00] little bit more proactive and so when we [13:04] look at this problem what happens when [13:05] too much data shows up we really have [13:07] only three strategies we can drop the [13:09] data on the floor we can reject the data [13:12] which is to say drop the data on the [13:13] floor and let them know that we dropped [13:15] it or we can pause the data which is to [13:17] say back to whoever's giving us the data [13:19] I can't handle this right now and then [13:21] at some point in the future say now I [13:23] can and so dropping the data is actually [13:26] a valid strategy for a certain subset of [13:28] problems where the newer data makes the [13:30] older data obsolete once we get a new [13:32] piece of data we can drop anything else [13:34] we had before it doesn't matter anymore [13:36] and this would be true for some uh cases [13:38] of telemetry uh some sensor systems it's [13:41] a generally a pretty narrow uh set of [13:43] problems uh the other reason we can drop [13:45] the data is if we just don't care if [13:47] it's sort of best effort right we'd like [13:49] to have the data if we possibly could [13:50] but if we don't then oh well uh I think [13:53] that we do this a little bit too readily [13:55] honestly uh for instance you can see [13:57] that uh mon in systems like stats D use [14:00] UDP which is again a sort of best effort [14:02] delivery you fire it and sort of just [14:04] pray that it gets there and you'll see [14:06] this sort of correlation between uh [14:09] situations where the network is in [14:10] crisis uh and the sort of likeliness of [14:13] this data being dropped which means that [14:14] when you most need accurate data we're [14:16] least likely to get it and so this [14:18] certainly is a thing that we can do but [14:20] we should often if we can sort of search [14:22] for a better [14:23] approach rejecting the data is pretty [14:26] much uh what you see when you go to an [14:28] overloaded website and returns a 503 or [14:30] when Twitter back in the day has [14:31] returned a fail whale or any of these [14:33] other sorts of things this is the most [14:35] common thing to do at the application [14:36] Level because when we have too many [14:38] requests we don't have enough [14:40] information to do something more [14:41] graceful than that right we can't email [14:43] the customer uh saying oh well you can't [14:46] check out your cart now but we'll let [14:47] you know once you can right we don't [14:48] even know who they are we have no means [14:50] to figure out who they [14:54] are and so this is far from ideal [14:56] ideally we would never reject anything [14:58] especially if you know you know [14:58] someone's going to pay us money for it [15:00] but uh we need to have a an [15:02] understanding that this is inevitable we [15:03] need to understand like what we're going [15:05] to do in this situation and make that as [15:07] uh palatable as we [15:09] can uh but elsewhere right except when [15:13] we're not sort of uh writing code at the [15:15] application Level when we're trying to [15:16] write a more General sort of thing uh [15:19] what we want to do is we want to pause [15:20] the data or exert back [15:22] pressure and a closed systems and this [15:24] is sort of implicit right we have this [15:26] sort of seating of control between the [15:27] producer and the consumer that's of [15:28] built into it but when we're writing a [15:30] library or a framework or something like [15:32] that we don't know what is appropriate [15:34] we don't know if dropping data is [15:35] appropriate we don't know uh how to [15:37] reject the data in a meaningful way and [15:39] so the best thing that we can do is just [15:41] sort of defer the choice to someone [15:43] who's upstream and understands the [15:45] application better than we [15:47] do and so this is why core async has uh [15:50] three cues instead of just one it has [15:52] the puts buffer for things which have [15:55] had back pressure exerted on them where [15:56] we've told them we're working on it but [15:59] we can't quite get to you and then we'll [16:00] eventually be able to notify them it's [16:02] okay we took it you can [16:04] continue and so when we look at [16:07] cues as I said you know this they sort [16:09] of separate the what from the when they [16:11] also sort of allow us to deal with [16:12] potentially unbounded demand using very [16:15] bounded resources uh and they serve as [16:17] sort of interface between systems where [16:19] the demand and the supply may be sort of [16:21] mismatched or we can't really predict [16:23] what the relative proportion of each [16:24] will be and these are all sort of saying [16:26] the same thing but the crucial point I [16:28] want to make is that the buffer right [16:30] the thing that holds on to the messages [16:32] is unnecessary for any of these [16:33] properties right in fact we could just [16:36] as easily have a uh Channel which has [16:40] just the puts and the takes just weights [16:42] to sort of pass uh the message off once [16:44] it's available and in fact in core async [16:46] this is the default Channel [16:47] configuration it has no [16:50] buffer and the reason that this is good [16:52] is because an unbuffered queue is a [16:54] closed system right there is that sort [16:56] of implicit seating of control we only [16:58] proceed once someone else has actually [16:59] taken it off our [17:01] hands and so you may wonder you know why [17:04] on Earth would we even have buffers [17:06] right buffers seem only to be a thing [17:07] that introduced latency and the answer [17:10] to this is that back pressure right [17:12] enacting back back pressure and [17:13] retracting back pressure is not free uh [17:16] if we want to restart a thread that we [17:17] stopped we have to copy the stack back [17:18] into memory if we want to restart TCB [17:21] traffic we stopped we have to send a [17:22] message over the network and wait for [17:23] the uh rate of traffic to ramp back up [17:26] if we're reading off the disc or a [17:27] database we have to go actually do that [17:29] and the time that elapses between us [17:31] saying we're ready and someone actually [17:33] giving us something to work on is uh [17:35] hurting our overall throughput our [17:36] actual rate of handling the data and so [17:39] that's what buffers are good for right [17:40] buffers allow us to make sure that our [17:42] throughput is more sort of stable and as [17:46] going at just a higher rate and that's [17:48] of course at the expense of latency in [17:50] our [17:51] systems and so we should always plan for [17:54] too much data right this is something [17:56] that is inevitable and we need to know [17:58] what our sort of response to this will [17:59] be and wherever possible we should use [18:01] back pressure because that's the most [18:02] neutral choice that we can make right [18:04] that just kind of tells someone else I [18:06] can't handle this data you figure out [18:07] what that means and that might sort of [18:09] you know flow up on a chain all the way [18:11] to the periphery of our application [18:12] before before something says oh I guess [18:14] I have to reject or I have to drop or I [18:15] have to do whatever is appropriate in [18:17] this situation but that logic should be [18:19] centralized it should not be sort of [18:21] sprinkled [18:22] throughout and buffers are useful in [18:24] that they give us this sort of constant [18:26] throughput but it's at the expensive l [18:28] and so we should only use them where [18:30] absolutely necessary and we should in [18:32] practice avoid composing buffered cues [18:35] wherever possible because each [18:36] additional buffer tends to sort of [18:38] magnify the results of the [18:41] next and so you know as we've been going [18:44] through this we've had some things that [18:46] uh we've decided or I've told you and uh [18:49] it's worth taking a moment to sort of [18:51] compare these guidelines to the current [18:54] state of the closure [18:57] ecosystem [19:01] um a lot of the attempts to kind of take [19:04] something like corac sync which [19:05] absolutely sort of uh it implements back [19:08] pressure and everything flawlessly but [19:10] then it is hooked up to some sort of [19:11] networking mechanism right something [19:13] where again we have no control over how [19:15] much data is coming in and it doesn't [19:18] hook it up properly and in some cases [19:20] this is unavoidable for instance the [19:21] Java e spec for web sockets has no [19:23] mechanism for back pressure I don't know [19:25] why but it doesn't and so when you are [19:28] trying to use this in a Ser container or [19:30] something like that uh there's no real [19:33] way to solve this problem but in other [19:34] cases it's just sort of an oversight [19:36] right the this particular author has [19:38] never been bit by this problem doesn't [19:39] see sort of how important it is to make [19:41] these sorts of connections to not break [19:42] the golden chain of back pressure and so [19:46] we as consumers of the libraries I think [19:48] need to be more Discerning we need to [19:50] ask the tough questions when we look at [19:52] this sort of stuff and make sure that [19:53] we're not building on top of shaky [19:55] foundations and this is not to call out [19:57] any of the author who have made a [19:58] library which many of which are you know [20:01] used to good effect within our community [20:03] but I think that we want to make sure [20:04] that our ecosystem can stand up to any [20:07] scale of system right that we are not [20:09] forced to rebuild from scratch once we [20:11] hit the point of failure which can [20:13] happen just all of a sudden [20:16] right and so a uh concrete example of [20:20] this uh is an open system that I work on [20:23] which is called Ma and Ma is sort of [20:25] this omnivorous Endo that just absorbs [20:27] any data that our uh Partners or [20:30] customers send to us and so the general [20:33] uh idea looks something like this we [20:35] have a large amount of data coming in we [20:37] need to do a little bit of processing to [20:38] it and then we need to persist it [20:39] somewhere for batch processing in the [20:41] future and because I didn't want to have [20:43] to work very hard on the hard parts of [20:46] this uh I decided to do this on AWS [20:48] which gives us the elastic load balancer [20:50] which is a place you can sort of Point [20:51] arbitary amounts of traffic and have it [20:53] spread it across a bunch of machines and [20:55] I wanted to persist to S3 which is a [20:57] replic at data store which is highly [21:00] available and is able to kind of you [21:02] know deal with whatever volumes of data [21:03] that we want to write to it and the [21:05] system has some pretty uh particular [21:09] properties right it has to be [21:10] horizontally scalable because we don't [21:11] know uh how many how much day we're [21:14] going to need right when it first uh was [21:16] started up we were getting about 40,000 [21:18] quaries per second we're up to about [21:19] 300,000 at Peak uh about a year and a [21:21] half later and so uh I wanted to be able [21:25] to have that just kind of work without [21:26] me having to go back and sort of rethink [21:28] the entire thing and it must be low [21:30] maintenance this is far from the only [21:31] thing that I'm responsible for if it [21:33] takes up more than a day out of a month [21:35] of my time then something's gone [21:36] horribly wrong uh it's not real time uh [21:39] we don't really care about the data [21:41] getting into S3 in any particular uh [21:43] rapid Pace as long as it's sort of there [21:45] by the end of the day uh and the most [21:48] important property is that the loss of [21:50] the data is more or less directly [21:53] proportional to loss utility if we lose [21:55] 1% of the data we lose about 1% of the [21:57] utility and this is uh very different [21:59] than if we were doing say Payment [22:01] Processing we're losing 1% of the data [22:02] would make us useless right and so this [22:05] is a very particular property which [22:06] makes my job a lot easier and [22:10] so uh in sort of previously we had uh [22:13] set up a system that absorbed the [22:15] Twitter garden hose which is 10% of the [22:17] Twitter fire hose uh [22:19] and this is the official term uh and uh [22:23] that ended up being about 600 tweets per [22:25] second and so I had you know kind of [22:28] something together which took an HTTP [22:29] client stream and then put it through [22:31] the Hadoop S3 client which was something [22:33] that we had on hand and wrote it to S3 [22:35] and this worked very well and so in my [22:37] sort of initial let's just get something [22:39] up and running I adapted that and put a [22:42] web server in front of it and then wrote [22:45] the entries which I uh assumed to be [22:47] about 20,000 entries per second uh [22:50] through the Hadoop client but uh what I [22:53] was unaware of what I had not really [22:55] been tracking with the uh the Twitter [22:56] client was that occasionally S3 becomes [22:59] uh unavailable right for whatever reason [23:02] I still don't really know why but it [23:03] just stops accepting rights for a bit [23:06] and what happens in this case is that [23:07] the uh Hadoop S3 client sort of just [23:09] held on to everything uh hoping that you [23:12] know eventually everything would sort of [23:13] work itself out and it' be able to flush [23:15] all the accumulated data but in fact it [23:18] uh was getting stuff at too rapid a pace [23:20] and so we ran out of memory and the [23:21] system fell over and then all of the uh [23:24] traffic that that machine was handling [23:25] fell over to the other machines which [23:27] themselves fell over and then the entire [23:28] system was [23:31] dead and um you know to be clear this [23:35] was almost certainly happening with the [23:36] Twitter client before because uh I don't [23:40] see why they would behave any [23:41] differently and in fact I'm sure that [23:42] many times it held onto the data and was [23:44] able to flush it out right but this is [23:46] sort of invisible to me this was not a [23:48] failure mode I was thinking very [23:49] carefully about uh until I saw it and [23:52] had to deal with it and so uh when I [23:55] sort of thought about this a little bit [23:56] more clearly though I realized that I [23:58] was making my job a lot harder than it [24:00] had to be because I was treating the [24:03] handling of this data as getting it all [24:05] the way into S3 right which again is [24:07] sort of over a network hop requires it [24:09] to be replicated and everything else and [24:11] really all I wanted was for it to be [24:13] somewhere that was not in memory uh I [24:15] could write it to the local disc for [24:17] instance which is much more highly [24:18] available as it turns out than S3 uh and [24:22] obviously this has some uh shortcomings [24:24] right we uh in this case if we lose the [24:26] machine we lose the data but again like [24:29] this is something that for this [24:30] particular use case I was okay [24:32] with and so uh I end up writing two [24:35] libraries both of which are open source [24:38] uh one of which is called durable queue [24:39] which is exactly what it sounds like [24:40] it's a dis backed que uh very simple [24:43] very uh sort of well tested you know [24:46] it's had about uh I think at this point [24:48] 3 to four trillion events flowing [24:50] through it uh and then likewise S3 [24:52] Journal which is built on top of uh the [24:55] durable queue right it will write each [24:57] of the things that in tends to upload S3 [24:58] to dis and then on the other side have a [25:00] consumer which is uploading it as it's [25:02] able and so the uh system that I end up [25:06] building looks like this right and so [25:08] now there are actually two stages the H [25:11] server is only concerned with the act of [25:13] getting it onto some persisted store and [25:15] then there is a different uh sort of [25:17] producer and consumer a different Loop [25:19] which is uploading that to S3 and of [25:21] course the fact that I've simply made my [25:23] job easier doesn't mean that it's always [25:25] going to work and so I need to make sure [25:27] that everything is working and so one of [25:29] the flaws arguably of the Hado client [25:32] wasn't that it just had an unbounded [25:33] queue it's that it was invisible right [25:35] we don't know what's going on inside of [25:37] it we only know by inferring that once [25:40] our system fails it was because our [25:41] memory has sort of ballooned out of [25:43] control and so S3 Journal uh and the [25:46] underlying Q both have uh metrics right [25:49] you just you can go and poll it and say [25:50] what is your status in this case how [25:52] many things have I in queed you and how [25:54] much have you successfully uploaded and [25:56] the que underneath has even more stuff [25:58] which says you know how many slabs which [26:00] are the sort of files that it's writing [26:01] to do you have currently resident in [26:03] memory how many things are pending how [26:05] many things have you had to [26:06] retry and so this is crucial right this [26:09] allows us to say what is the health of [26:10] our system it allows us to quantify how [26:13] much we might have lost when a machine [26:14] goes down these are all things that are [26:17] absolutely critical because we cannot [26:19] prove properties about our system right [26:21] the system is too complicated we can [26:23] only look and see what happens uh in as [26:26] much as the metrics that we have created [26:27] for ourselves allow us to do [26:28] that but on the far side uh of this on [26:31] the HTTP intake side uh there is the [26:34] additional problem that we need to [26:35] respond to everyone who's sending us [26:37] data quickly tell them yes we've [26:39] accepted your data because otherwise [26:40] they will scale back the volume of data [26:42] that we're sending us and we're trying [26:43] to get everything that we possibly can [26:45] and so uh underneath the uh Alf HD [26:48] server it uses netti and netti has a [26:49] very particular threading model where [26:51] every time a connection is opened it's [26:52] assigned to a thread and many [26:54] connections are assigned to a given [26:56] thread which means that if any uh [26:58] connection takes up that thread right [27:00] blocks on it or something that in [27:02] implicitly blocks a bunch of other [27:03] connections that didn't really want to [27:05] be included in that decision and so the [27:07] way that you deal with this is that you [27:09] put another thread pole behind it right [27:11] that sort of goes and spreads this out [27:13] and make sure that no one connection uh [27:15] acting badly can cause other things to [27:18] back up and when we look at the metrics [27:21] here uh if we put something in our ring [27:23] Handler that's looking at our response [27:24] time it actually is uh fairly far off [27:27] from the uh actual response time right [27:29] the thing that uh is actually the packet [27:31] coming into the packet going out and [27:33] this is because thread pools are [27:35] themselves not very well instrumented [27:37] right they're they're kind of this [27:38] invisible thing there's a queue we spend [27:39] some time on it and then it happens [27:41] eventually who knows when and so there's [27:44] a library that I wrote [27:47] which has a bunch of stuff um in it but [27:51] one of them is an instrumented thread [27:52] Poole right something that actually [27:53] tracks all the relative statistics and [27:55] so when I go uh to any of the ma servers [27:58] that are running there's a Json endpoint [28:00] that looks like this and I'm going to [28:01] focus in on uh two aspects of this uh [28:05] one of which is [28:07] the uh sort of measuring the task [28:09] rejection completion and arrival rate [28:11] and notice that none of these are just [28:12] simple scalar values they're all [28:14] quantiles because again the rate of [28:17] arrival is going to be sort of spiking [28:19] and going down as as sort of different [28:22] requests which are not coordinated show [28:23] up at the same time or not at all and so [28:26] you'll notice that the arrival and [28:28] completion rate uh the difference [28:29] between the median and the 99.9th [28:31] percentile is about a 3X difference and [28:33] so when we talk about capacity planning [28:35] right we have to you know think about [28:36] that likewise we are in fact sometimes [28:39] rejecting uh data because we don't have [28:41] the capacity to handle it it seems to [28:42] only happen very infrequently but this [28:44] is something that we should absolutely [28:45] be [28:47] tracking uh there's also the task [28:50] latency and the Q latency where the task [28:52] latency is a superet it represents the [28:54] entire time spent on the Queue and [28:55] working on the task and the Q latency [28:57] represents just the time spent on the Q [28:59] you'll notice uh that the 99.9 [29:02] percentile for the Q is actually higher [29:04] than that for the task and that's [29:04] because these are sampled uh statistics [29:07] right uh sometimes the what the Q is [29:10] looking at and what the task is looking [29:11] at are going to be slightly different [29:12] and so this is imperfect right it's not [29:14] a perfect High Fidelity view of our [29:15] system but it is good enough and you'll [29:18] also notice that the Q is zero uh in [29:21] length all the time because it is [29:22] actually an unbuffered q and yet somehow [29:24] we're spending 2 milliseconds sometimes [29:26] trying to Traverse it and this is [29:27] because of course underneath our Q which [29:29] is unbuffered there are many cues which [29:30] are buffered right all the things that [29:32] sit underneath uh the execution model of [29:34] java and so again we need to measure [29:37] that we need to be aware of these things [29:38] and not just treat this as some sort of [29:40] uh instantaneous process of handing off [29:42] between producer and [29:44] consumer and so we have metrics now [29:47] right we have uh something that gives us [29:50] a little bit more visibility in the [29:51] system it obviously doesn't actually [29:52] capture everything it doesn't look very [29:54] deep into netti and if we ever saw a [29:56] huge discrepancy between the response [29:58] times that we were seeing and the [29:59] response times being reported outside of [30:01] our system we would have to dig into [30:03] that right there's always something more [30:04] to measure but the idea is that we just [30:06] don't want to be sort of willfully blind [30:07] and close our eyes and pretend that [30:09] everything's [30:11] okay and so it's really crucial when [30:14] we're building these systems that we [30:15] understand what it means to complete [30:16] something right because too many things [30:18] that are incomplete means that we need [30:20] to apply back pressure or drop data or [30:22] reject it and so if we can sort of move [30:24] the gold post and make that easier that [30:26] is a huge win [30:27] and you should be picky about your tools [30:29] you should make sure that they uh don't [30:30] sort of subvert your assumptions of what [30:32] should be happening or uh what you know [30:35] needs to happen in these sorts of [30:36] systems and you know when choosing a [30:39] tool right raw performance is good these [30:41] benchmarks they tell us something which [30:42] is probably if I use this right it'll be [30:44] fast but you should always prefer [30:46] something which actually tells you how [30:48] fast it is because the assumptions that [30:50] are from these sorts of very contrived [30:51] benchmarks will not necessarily uh make [30:54] their way into [30:55] reality and your you're never going to [30:57] be able to measure all of your system [30:59] there're always going to be things that [30:59] are outside of your control someone [31:01] who's accessing your website from a [31:02] mobile connection right might go into a [31:04] tunnel and lose their connection for X [31:06] number of seconds and that's clearly not [31:09] something that you can uh account for [31:11] that you can sort of prevent but that is [31:14] what they're perceiving your response [31:15] time to be and whether or not that [31:16] matters whether or not that's something [31:17] that you're trying to kind of deal with [31:20] is very dependent on your application [31:22] but there are always going to be things [31:23] that you were unaware [31:25] of and so I'm going to uh talk about [31:30] another example here which is a sort of [31:32] a common uh very toy use case that you [31:35] see with these sorts of things using [31:36] node.js or something like that which is [31:38] a chat server right you join a room [31:40] there are a bunch of people there [31:41] they're talking to each other every [31:43] messag is broadcast out and I'm going to [31:45] use a library called manifold uh not [31:47] because it's sufficient really uh better [31:50] at this than anything else but it's [31:51] because something that I know and it has [31:52] some nice uh operators that uh are sort [31:55] of useful in the situation but you you [31:57] could do something very similar with cor [31:58] sync or any other number of uh stream [32:01] abstraction [32:02] libraries and in [32:05] manifold there's something called an [32:07] event bus which is something that just [32:08] kind of is for the publisher subscriber [32:10] model right we can publish out to a [32:12] topic any subscribers on that topic will [32:14] receive the message if there are no [32:15] subscribers then it's a noop it just [32:17] sort of vanishes into The [32:18] Ether and when we have a chat Handler [32:21] when we actually get some connecting we [32:23] uh find out what room they want to join [32:26] and then we take take every message from [32:28] that connection and we publish it out to [32:30] the room that they've selected right [32:32] very straightforward we're just taking [32:33] everything and throwing it through [32:34] another [32:36] stream and likewise we connect to the [32:38] room and take everything that's been [32:40] said in that room and pipe it back into [32:42] the connection and so this is a chat [32:44] Handler right this this is uh IRC in its [32:47] Essence right but there are some [32:49] problems here uh one of which is that if [32:52] any of our consumers anybody on the chat [32:54] uh room is slow to receive a message [32:57] that slows down everybody because the [32:59] act of publishing means it has to go to [33:00] everyone and so we want to uh sort of [33:05] control this we want to make sure that [33:06] one bad actor doesn't make the entire [33:08] system sort of [33:09] destabilize and so when we're looking at [33:12] uh this connection right the taking all [33:14] the things from the room and just kind [33:16] of piping them over to uh this client [33:19] whoever they are uh maybe we want to add [33:21] a buffer right uh be able to write out a [33:23] few of these sorts of uh you know [33:26] instabil here maybe if they're just a [33:28] little bit slow we can make it so that [33:30] that's not too visible to everybody else [33:32] uh and of course measuring this in [33:33] messages may not be the right thing [33:35] because who knows how big a message is [33:36] it might be one word it might be a novel [33:38] and so a better thing would be to have [33:40] some sort of metric right something that [33:41] tells us how big the message is and then [33:43] put an upper bound on the aggregate size [33:45] of all the messages in this case just a [33:48] thousand but even that doesn't [33:50] necessarily prevent us from arriving at [33:52] the same Pro uh sort of same failure [33:54] mode right it just defers it puts it off [33:56] a little bit further than into the [33:57] future and so the only thing that we [33:58] really can do is once someone is slow to [34:02] receive our messages is unable to [34:03] receive them we kick them out and so the [34:06] connect uh method here takes a message [34:09] source and pipes all of it the messages [34:11] from it into a message sync and there's [34:13] an optional thing here to define a [34:15] timeout which says if I've been trying [34:17] to give you a message for a x number [34:19] milliseconds and I can't do that I'm [34:22] just going to close you and wipe my [34:23] hands with the whole thing and so in [34:26] this case we say if we've been trying [34:27] for 10 seconds to send you a message and [34:30] you haven't received it yet your [34:31] Buffer's been full for that entire time [34:33] uh we're done you're gone you can just [34:35] reconnect and this is really again the [34:37] only thing we can do this there's there [34:39] is no other thing that we can do to save [34:41] this person from [34:42] themselves but this actually uh creates [34:45] another problem which is if someone is [34:47] publishing messages too fast they can [34:49] overwhelm everyone and get everybody [34:51] kicked right uh and you know again this [34:54] may be because someone's a jerk it may [34:55] be because someone's sort of naive [34:57] uh you can't really differentiate [34:59] between the [35:00] two [35:04] uh but but this is I I mean you know [35:06] it's it's it's funny but it's also like [35:09] it's your job to make sure that your [35:11] system is robust to the rest of the [35:14] world whatever that means whatever sort [35:15] of motivations you ascribe to that right [35:17] it's your job to make sure that you [35:19] don't get knocked over that uh you know [35:22] the many don't suffer because of the [35:23] actions of the few and so you really [35:26] need to think about this carefully and [35:27] so in this particular case uh we can [35:31] throttle the connection right we can say [35:33] you're only allowed to send us one [35:34] message a second and note here that we [35:37] don't actually need to have the same [35:38] sort of Disconnect logic because if [35:40] someone's sending us messages too [35:41] rapidly we'll just keep on using TCB [35:42] back pressure and saying nope we're not [35:44] taking it and if that causes them to [35:46] fail that's their [35:48] problem but in this case uh th [35:50] throttling at one message a second may [35:52] seem a little bit uh irritating because [35:55] maybe you know what happen happens if [35:57] someone has been silent for 10 minutes [35:58] right uh are they still only able to [36:00] send one message a second can they send [36:01] 10 minutes worth of messages all at once [36:04] I mean these are sort of both [36:05] possibilities when we talk about [36:07] throttling and so uh we able here to [36:09] sort of specify the uh maximum amount of [36:12] credits that they're able to acrew in [36:14] this case 10 seconds Worth right they're [36:16] able to uh if they're silent for 10 [36:17] seconds they can send 10 messages all at [36:19] once but they can't send more and so [36:23] when we look at this right this sort of [36:25] example it's it's a little bit less uh [36:28] platonic than it was before right it's a [36:30] little bit less uh General because we've [36:32] had to make all these decisions we've [36:34] had to decide what is the maximum amount [36:36] of memory that we're willing to give to [36:37] each of these people uh how long are we [36:39] willing to wait on them how much uh how [36:41] many messages can people send us right [36:43] and so all of a sudden we've gone from [36:44] this thing which was a little bit [36:45] handwavy a little bit vague very general [36:48] everybody could sort of project their [36:49] particular chat room onto it and now [36:51] this is a very precise chat room right [36:53] maybe not a very good one but it's [36:55] certainly precise in terms of what it [36:57] does and this is what you need to do [37:00] right these are the steps that you need [37:01] to take and I want to emphasize also [37:04] that it's not hard to do this right you [37:05] have to think a little bit about what [37:07] these boundary conditions should be but [37:09] this is what's required for uh us to be [37:13] able to make these robust systems for us [37:15] to be able to write out the behavior of [37:17] the outside [37:18] world and I want to make one additional [37:21] point about the uh sort of chat room [37:23] example because all the other examples [37:25] I've talked about has had a Q or like [37:27] two qes that are there and we we have a [37:29] fixed cardinality which means that we [37:31] can monitor them have graphs associated [37:33] with them basically look at them and [37:35] understand their behavior very [37:36] particularly but with a chat room [37:38] example we have sort of an Untold number [37:39] of cues the cues are constantly [37:41] disappearing and reappearing in new [37:42] configurations and so the sort of [37:45] typical monitoring thing which is to [37:47] look at all the individual cues uh is [37:49] not sufficient and it becomes very hard [37:52] for us to understand the system as a [37:53] whole because the system is constantly [37:55] changing out from underneath us and so I [37:57] think that this is a real problem if you [37:58] uh think about what this would look like [37:59] for core async uh you know you have all [38:03] these go routines that are sort of like [38:04] shoving messages in every direction but [38:06] you can't look at it and sort of [38:07] apprehend it as a whole and I think that [38:10] this is a weakness at least for certain [38:12] classes of applications and so in [38:14] manifold uh I've played around with this [38:16] a little bit and one of the things you [38:17] can do is you can look at any stream and [38:19] ask it what streams are Downstream of [38:21] you right what are you feeding into and [38:23] this is because all these methods the [38:24] connect method or the consume method [38:26] actually make a marker uh update this [38:29] sort of internal topology and this means [38:31] that we can to some degree right trace [38:33] the system look at each of the streams [38:35] understand how they're feeding into each [38:36] other uh it's not clear to me exactly [38:38] how this should be represented in uh [38:41] some of my earlier attempts of this I [38:43] had uh graph viz visualizations but I've [38:45] come to the conclusion that that doesn't [38:47] scale very well right it doesn't take a [38:48] very complex topology to make that sort [38:50] of not well understood and so maybe the [38:53] right thing is to come up with a better [38:54] visualization tool maybe it's to dump [38:56] into dis for you know examination later [38:59] I think that this is something which is [39:00] a sort of uh Rich topic and ripe for uh [39:04] sort of exploration but I think that in [39:07] again for in order for closure to become [39:08] an ecosystem where we can build these [39:09] sorts of systems that we don't just sort [39:12] of you know cross our fingers and hope [39:13] they don't fail like this is necessary [39:16] we need to answer these questions we [39:17] need to have good answers for these [39:20] questions and so we talked about cues [39:23] and the things we learned are that [39:24] unbounded cues aren't unbounded right [39:26] they're bounded by memory or some other [39:28] sort of fixed shared resource that will [39:30] cause our system to fall over once [39:32] that's [39:33] succeeded and when we build an [39:35] application it should always account for [39:37] what happens when it receives too much [39:39] data but where we're not at the sort of [39:41] boundaries of application we're trying [39:42] to write something which is more general [39:43] or less opinionated or can be reused [39:45] elsewhere we should always use back [39:48] pressure right we should always defer to [39:50] someone else who knows better than us [39:51] what to do and we should demand metrics [39:54] everywhere right even if we don't [39:55] actually get them even if there always [39:57] things that are a little bit out of [39:58] sight we should always be asking for [40:00] more and we should always know how we [40:01] can go one step deeper and with that uh [40:05] I think we're pretty much exactly out of [40:06] time but uh I can take questions in the [40:11] hallway