Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building a Distributed Message Log from Scratch

Building a Distributed Message Log from Scratch

Apache Kafka has shown that the log is a powerful abstraction for data-intensive applications. It can play a key role in managing data and distributing it across the enterprise efficiently. Vital to any data plane is not just performance, but availability and scalability. In this session, we examine what a distributed log is, how it works, and how it can achieve these goals. Specifically, we'll discuss lessons learned while building NATS Streaming, a reliable messaging layer built on NATS that provides similar semantics. We'll cover core components like leader election, data replication, log persistence, and message delivery. Come learn about distributed systems!

Tyler Treat

November 04, 2017
Tweet

More Decks by Tyler Treat

Other Decks in Programming

Transcript

  1. - Messaging Nerd @ Apcera - Working on nats.io -

    Distributed systems - bravenewgeek.com Tyler Treat
  2. - The Log
 -> What?
 -> Why? - Implementation
 ->

    Storage mechanics
 -> Data-replication techniques
 -> Scaling message delivery
 -> Trade-offs and lessons learned Outline
  3. The purpose of this talk is to learn…
 -> a

    bit about the internals of a log abstraction. -> how it can achieve these goals. -> some applied distributed systems theory.
  4. You will probably never need to build something like this

    yourself, but it helps to know how it works.
  5. Some first principles… Storage Mechanics • The log is an

    ordered, immutable sequence of messages • Messages are atomic (meaning they can’t be broken up) • The log has a notion of message retention based on some policies (time, number of messages, bytes, etc.) • The log can be played back from any arbitrary position • The log is stored on disk • Sequential disk access is fast* • OS page cache means sequential access often avoids disk
  6. avg-cpu: %user %nice %system %iowait %steal %idle 13.53 0.00 11.28

    0.00 0.00 75.19 Device: tps Blk_read/s Blk_wrtn/s Blk_read Blk_wrtn xvda 0.00 0.00 0.00 0 0 iostat
  7. Storage Mechanics log segment 3 file log segment 0 file

    0 1 2 3 4 5 0 1 2 0 1 2 index segment 0 file index segment 3 file
  8. Questions:
 -> How do we ensure continuity of reads/writes? ->

    How do we replicate data? -> How do we ensure replicas are consistent? -> How do we keep things fast? -> How do we ensure data is durable?
  9. Questions:
 -> How do we ensure continuity of reads/writes? ->

    How do we replicate data? -> How do we ensure replicas are consistent? -> How do we keep things fast? -> How do we ensure data is durable?
  10. Questions:
 -> How do we ensure continuity of reads/writes? ->

    How do we replicate data? -> How do we ensure replicas are consistent? -> How do we keep things fast? -> How do we ensure data is durable?
  11. Data-Replication Techniques 1. Gossip/multicast protocols Epidemic broadcast trees, bimodal multicast,

    SWIM, HyParView, NeEM
 2. Consensus protocols 2PC/3PC, Paxos, Raft, Zab, chain replication
  12. Questions:
 -> How do we ensure continuity of reads/writes? ->

    How do we replicate data? -> How do we ensure replicas are consistent? -> How do we keep things fast? -> How do we ensure data is durable?
  13. Data-Replication Techniques 1. Gossip/multicast protocols Epidemic broadcast trees, bimodal multicast,

    SWIM, HyParView, NeEM
 2. Consensus protocols 2PC/3PC, Paxos, Raft, Zab, chain replication
  14. Consensus-Based Replication 1. Designate a leader 2. Replicate by either:


    a) waiting for all replicas
 —or— b) waiting for a quorum of replicas
  15. Pros Cons All Replicas Tolerates f failures with f+1 replicas

    Latency pegged to slowest replica Quorum Hides delay from a slow replica Tolerates f failures with 2f+1 replicas Consensus-Based Replication
  16. Replication in Kafka 1. Select a leader 2. Maintain in-sync

    replica set (ISR) (initially every replica) 3. Leader writes messages to write-ahead log (WAL) 4. Leader commits messages when all replicas in ISR ack 5. Leader maintains high-water mark (HW) of last committed message 6. Piggyback HW on replica fetch responses which replicas periodically checkpoint to disk
  17. 0 1 2 3 4 5 b1 (leader) 0 1

    2 3 4 HW: 3 0 1 2 3 HW: 3 HW: 3 b2 (follower) b3 (follower) ISR: {b1, b2, b3} writes Replication in Kafka
  18. 0 1 2 3 4 5 b1 (leader) 0 1

    2 3 4 HW: 3 0 1 2 3 HW: 3 HW: 3 b2 (follower) b3 (follower) ISR: {b1, b2, b3} writes Leader fails
  19. 0 1 2 3 4 5 b1 (leader) 0 1

    2 3 4 HW: 3 0 1 2 3 HW: 3 HW: 3 b2 (follower) b3 (follower) ISR: {b1, b2, b3} writes Leader fails
  20. 0 1 2 3 4 5 b1 (leader) 0 1

    2 3 4 HW: 3 0 1 2 3 HW: 3 HW: 3 b2 (follower) b3 (follower) ISR: {b1, b2, b3} writes Leader fails
  21. 0 1 2 3 HW: 3 0 1 2 3

    HW: 3 b2 (leader) b3 (follower) ISR: {b2, b3} writes Leader fails
  22. 0 1 2 3 4 5 b1 (leader) 0 1

    2 3 4 HW: 3 0 1 2 3 HW: 3 HW: 3 b2 (follower) b3 (follower) ISR: {b1, b2, b3} writes Follower fails
  23. 0 1 2 3 4 5 b1 (leader) 0 1

    2 3 4 HW: 3 0 1 2 3 HW: 3 HW: 3 b2 (follower) b3 (follower) ISR: {b1, b2, b3} writes Follower fails
  24. 0 1 2 3 4 5 b1 (leader) 0 1

    2 3 4 HW: 3 0 1 2 3 HW: 3 HW: 3 b2 (follower) b3 (follower) ISR: {b1, b2, b3} writes Follower fails replica.lag.time.max.ms
  25. 0 1 2 3 4 5 b1 (leader) HW: 3

    0 1 2 3 HW: 3 b3 (follower) ISR: {b1, b3} writes Follower fails replica.lag.time.max.ms
  26. 0 1 2 3 4 5 b1 (leader) 0 1

    2 3 4 HW: 3 0 1 2 3 HW: 3 HW: 3 b2 (follower) b3 (follower) ISR: {b1, b2, b3} writes Follower temporarily
 partitioned
  27. Follower temporarily
 partitioned 0 1 2 3 4 5 b1

    (leader) 0 1 2 3 4 HW: 3 0 1 2 3 HW: 3 HW: 3 b2 (follower) b3 (follower) ISR: {b1, b2, b3} writes
  28. Follower temporarily
 partitioned 0 1 2 3 4 5 b1

    (leader) 0 1 2 3 4 HW: 3 0 1 2 3 HW: 3 HW: 3 b2 (follower) b3 (follower) ISR: {b1, b2, b3} writes replica.lag.time.max.ms
  29. Follower temporarily
 partitioned 0 1 2 3 4 5 b1

    (leader) 0 1 2 3 4 HW: 3 0 1 2 3 HW: 3 HW: 3 b2 (follower) b3 (follower) ISR: {b1, b2} writes replica.lag.time.max.ms
  30. Follower temporarily
 partitioned 0 1 2 3 4 5 b1

    (leader) 0 1 2 3 4 HW: 5 0 1 2 3 HW: 5 HW: 3 b2 (follower) b3 (follower) ISR: {b1, b2} writes 5
  31. Follower temporarily
 partitioned 0 1 2 3 4 5 b1

    (leader) 0 1 2 3 4 HW: 5 0 1 2 3 HW: 5 HW: 3 b2 (follower) b3 (follower) ISR: {b1, b2} writes 5
  32. Follower temporarily
 partitioned 0 1 2 3 4 5 b1

    (leader) 0 1 2 3 4 HW: 5 0 1 2 3 HW: 5 HW: 4 b2 (follower) b3 (follower) ISR: {b1, b2} writes 5 4
  33. Follower temporarily
 partitioned 0 1 2 3 4 5 b1

    (leader) 0 1 2 3 4 HW: 5 0 1 2 3 HW: 5 HW: 5 b2 (follower) b3 (follower) ISR: {b1, b2} writes 5 4 5
  34. Follower temporarily
 partitioned 0 1 2 3 4 5 b1

    (leader) 0 1 2 3 4 HW: 5 0 1 2 3 HW: 5 HW: 5 b2 (follower) b3 (follower) ISR: {b1, b2, b3} writes 5 4 5
  35. Replication in NATS Streaming 1. Metadata Raft group replicates client

    state
 2. Separate Raft group per topic replicates messages and subscriptions
 3. Conceptually, two logs: Raft log and message log
  36. Scaling Raft With a single topic, one node is elected

    leader and it heartbeats messages to followers
  37. Scaling Raft Technique 1: run a fixed number of Raft

    groups and use a consistent hash to map a topic to a group.
  38. Scaling Raft Technique 2: run an entire node’s worth of

    topics as a single group using a layer on top of Raft. https://www.cockroachlabs.com/blog/scaling-raft
  39. Dual Writes msg 1 msg 2 sub msg 3 Raft

    msg 1 msg 2 Store committed
  40. Dual Writes msg 1 msg 2 sub msg 3 add

    peer msg 4 Raft msg 1 msg 2 msg 3 Store committed
  41. Dual Writes msg 1 msg 2 sub msg 3 add

    peer msg 4 Raft msg 1 msg 2 msg 3 Store committed
  42. Dual Writes msg 1 msg 2 sub msg 3 add

    peer msg 4 Raft msg 1 msg 2 msg 3 msg 4 Store commit
  43. Dual Writes msg 1 msg 2 sub msg 3 add

    peer msg 4 Raft msg 1 msg 2 msg 3 msg 4 Store 0 1 2 3 4 5 0 1 2 3 physical offset logical offset
  44. Dual Writes msg 1 msg 2 sub msg 3 add

    peer msg 4 Raft msg 1 msg 2 Index 0 1 2 3 4 5 0 1 2 3 physical offset logical offset msg 3 msg 4
  45. Questions:
 -> How do we ensure continuity of reads/writes? ->

    How do we replicate data? -> How do we ensure replicas are consistent? -> How do we keep things fast? -> How do we ensure data is durable?
  46. Performance 1. Publisher acks 
 -> broker acks on commit

    (slow but safe)
 -> broker acks on local log append (fast but unsafe)
 -> publisher doesn’t wait for ack (fast but unsafe) 
 2. Don’t fsync, rely on replication for durability
 3. Keep disk access sequential and maximize zero-copy reads
 4. Batch aggressively
  47. Questions:
 -> How do we ensure continuity of reads/writes? ->

    How do we replicate data? -> How do we ensure replicas are consistent? -> How do we keep things fast? -> How do we ensure data is durable?
  48. Durability 1. Quorum guarantees durability
 -> Comes for free with

    Raft
 -> In Kafka, need to configure min.insync.replicas and acks, e.g.
 topic with replication factor 3, min.insync.replicas=2, and
 acks=all
 2. Disable unclean leader elections
 3. At odds with availability,
 i.e. no quorum == no reads/writes
  49. caches databases indexes writes writes writes writes Topic: purchases Topic:

    inventory Accounts A-M Accounts N-Z SKUs A-M SKUs N-Z
  50. High Fan-out 1. Observation: with an immutable log, there are

    no stale/phantom reads
 2. This should make it “easy” (in theory) to scale to a large number of consumers (e.g. hundreds of thousands of IoT/edge devices)
 3. With Raft, we can use “non-voters” to act as read replicas and load balance consumers
  51. Push vs. Pull • In Kafka, consumers pull data from

    brokers • In NATS Streaming, brokers push data to consumers • Pros/cons to both:
 -> With push we need flow control; implicit in pull
 -> Need to make decisions about optimizing for
 latency vs. throughput
 -> Thick vs. thin client and API ergonomics
  52. Bookkeeping • Two ways to track position in the log:


    -> Have the server track it for consumers
 -> Have consumers track it
 • Trade-off between API simplicity and performance/server complexity
 • Also, consumers might not have stable storage (e.g. IoT device, ephemeral container, etc.)
 • Can we split the difference?
  53. Offset Storage • Can store offsets themselves in the log

    (in Kafka, originally had to store them in ZooKeeper)
 • Clients periodically checkpoint offset to log
 • Use log compaction to retain only latest offsets
 • On recovery, fetch latest offset from log
  54. Offset Storage bob-foo-0
 11 alice-foo-0
 15 Offsets 0 1 2

    3 bob-foo-1
 20 bob-foo-0
 18 4 bob-foo-0
 21
  55. Offset Storage bob-foo-0
 11 alice-foo-0
 15 Offsets 0 1 2

    3 bob-foo-1
 20 bob-foo-0
 18 4 bob-foo-0
 21
  56. Offset Storage Advantages:
 -> Fault-tolerant
 -> Consistent reads
 -> High

    write throughput (unlike ZooKeeper)
 -> Reuses existing structures, so less server
 complexity
  57. Competing Goals 1. Performance
 -> Easy to make something fast

    that’s not fault-tolerant or scalable
 -> Simplicity of mechanism makes this easier
 -> Simplicity of “UX” makes this harder 2. Scalability (and fault-tolerance)
 -> Scalability and FT are at odds with simplicity
 -> Cannot be an afterthought—needs to be designed from day 1 3. Simplicity (“UX”)
 -> Simplicity of mechanism shifts complexity elsewhere (e.g. client)
 -> Easy to let server handle complexity; hard when that needs to be
 distributed and consistent while still being fast
  58. Availability vs. Consistency • CAP theorem • Consistency requires quorum

    which hinders availability and performance • Minimize what you need to replicate
  59. Trade-offs and Lessons Learned 1. Competing goals 2. Availability vs.

    Consistency 3. Aim for simplicity 4. Lean on existing work
  60. Trade-offs and Lessons Learned 1. Competing goals 2. Availability vs.

    Consistency 3. Aim for simplicity 4. Lean on existing work 5. There are probably edge cases for which you haven’t written tests
  61. There are many failure modes, and you can only write

    so many tests.
 
 Formal methods and property-based/ generative testing can help.
  62. Trade-offs and Lessons Learned 1. Competing goals 2. Availability vs.

    Consistency 3. Aim for simplicity 4. Lean on existing work 5. There are probably edge cases for which you haven’t written tests 6. Be honest with your users
  63. Don’t try to be everything to everyone. Be explicit about

    design decisions, trade- offs, guarantees, defaults, etc.