Kafka Internals & Operations — Hiểu Sâu Để Vận Hành Đúng
Kafka không chỉ là "message queue nhanh". Đằng sau performance của nó là một loạt quyết định thiết kế cực kỳ thú vị: log-structured storage, zero-copy I/O, partition-level ordering, và consumer group protocol. Hiểu những thứ này giúp bạn không chỉ dùng Kafka mà còn debug được khi Kafka misbehave và tune đúng khi cần throughput cao.
1. Log-Structured Storage — Tại Sao Kafka Nhanh
Sequential I/O là chìa khóa
Hầu hết message broker truyền thống (ActiveMQ, RabbitMQ với persistence) dùng random I/O: mỗi message được ghi vào một vị trí ngẫu nhiên trên disk, giống như bạn vừa ghi chỗ này vừa xóa chỗ kia trong một cuốn sổ. HDD chịu không nổi cái này (seek time), SSD thì đỡ hơn nhưng vẫn tốn.
Kafka làm ngược lại hoàn toàn: append-only log. Mọi message đều được ghi tuần tự vào cuối file. Disk I/O pattern này gần như tương đương với memory I/O về throughput — HDD sequential write có thể đạt 100-200 MB/s, random write chỉ 1-2 MB/s. Đây là lý do chính tại sao Kafka có thể handle millions of messages/second trên hardware thông thường.
Partition Log (append-only):
+----------+----------+----------+----------+
| offset 0 | offset 1 | offset 2 | offset 3 | ---> append tiếp
+----------+----------+----------+----------+
^ ^
oldest newest
(có thể (active
đã delete) segment)
Mỗi partition là một directory chứa các segment files:
/kafka-data/my-topic-0/
00000000000000000000.log # segment 1
00000000000000000000.index # offset -> file position index
00000000000000000000.timeindex # timestamp -> offset index
00000000000000012345.log # segment 2 (active)
00000000000000012345.index
Zero-Copy với sendfile()
Khi consumer request data, thay vì copy data từ disk → kernel buffer → user space → socket buffer, Kafka dùng Linux sendfile() syscall để copy thẳng từ page cache → socket buffer mà không đi qua user space:
Traditional path (4 copies):
Disk --> Kernel Page Cache --> User Space Buffer --> Kernel Socket Buffer --> NIC
Zero-copy path (2 copies):
Disk --> Kernel Page Cache --> Kernel Socket Buffer --> NIC
(sendfile() skips user space)
Đây gọi là zero-copy — không phải zero copy thật sự (vẫn có 2 copies) mà là zero copy qua user space. Kết quả: CPU overhead giảm drastically, throughput tăng 2-4x so với truyền thống.
Page Cache — Đừng Chiếm Hết RAM Cho JVM
Kafka tận dụng OS page cache thay vì tự quản lý memory cache. Khi bạn đọc một segment file, OS cache nó trong page cache. Consumer đọc lần sau không cần xuống disk.
Hệ quả quan trọng cho ops:
- Kafka broker JVM heap nên để nhỏ (6-8 GB max), phần còn lại để OS dùng làm page cache
- Producer mới thường đọc lại ngay message vừa ghi → hit page cache 100% → latency cực thấp
- Consumer đọc historical data (lag cao) → page cache miss → I/O spike
Nếu bạn thấy consumer lag đột nhiên tăng cao sau khi consumer restart, đó thường là do page cache đã bị evict và consumer phải đọc từ disk lạnh.
2. Partition & Replication Internals
Leader Election Per Partition
Đây là điểm nhiều người nhầm: Kafka không bầu leader per broker, mà per partition. Một broker có thể là leader cho partition 0 của topic A, nhưng là follower cho partition 1 của cùng topic đó.
Broker 1 Broker 2 Broker 3
+-----------+ +-----------+ +-----------+
| topic-A | | topic-A | | topic-A |
| part-0 [L]| | part-0 [F]| | part-0 [F]|
| part-1 [F]| | part-1 [L]| | part-1 [F]|
| part-2 [F]| | part-2 [F]| | part-2 [L]|
+-----------+ +-----------+ +-----------+
[L] = Leader, [F] = Follower
Benefit: load phân bổ đều giữa các brokers. Tất cả producer/consumer đều giao tiếp với leader của từng partition, không phải một single leader cho cả topic.
ISR — In-Sync Replicas
ISR là tập hợp các replicas đang theo kịp leader (không lag quá replica.lag.time.max.ms, mặc định 30 giây).
Khi nào replica bị kick khỏi ISR:
- Follower không fetch từ leader trong
replica.lag.time.max.msms - Follower fetch nhưng lag quá nhiều message (trước đây có
replica.lag.max.messages, giờ đã removed) - Follower bị network partition hoặc crash
Normal state (ISR = {0,1,2}):
Leader (0) ----replicate---> Follower (1)
----replicate---> Follower (2)
ISR: [0, 1, 2]
Follower 2 bị slow:
Leader (0) ----replicate---> Follower (1) [in ISR]
----replicate---> Follower (2) [lagging...]
After lag.time.max.ms:
ISR: [0, 1] <-- Follower 2 kicked out
Hậu quả khi replica bị kick khỏi ISR:
- Nếu broker chứa leader chết, chỉ các replicas trong ISR mới được bầu làm leader mới
- Nếu ISR chỉ còn 1 (leader), và leader chết → nếu
unclean.leader.election.enable=falsethì partition unavailable cho đến khi broker cũ recover
acks — Trade-off Thực Tế
acks=0: Fire and forget
Producer ------> Broker (ghi xuống buffer, không confirm)
<------ Không có response
Throughput: MAX | Durability: NONE | Use case: metrics, logs không quan trọng
acks=1: Leader acknowledge
Producer ------> Leader (ghi vào leader log)
<------ ACK (không đợi followers)
Throughput: HIGH | Durability: MEDIUM | Risk: leader chết trước khi replicate
Use case: non-critical events
acks=all (hoặc acks=-1): All ISR acknowledge
Producer ------> Leader ------> Follower 1
------> Follower 2
<------ ACK (sau khi tất cả ISR confirm)
Throughput: LOWER | Durability: HIGH | Use case: payment, orders
Config thực tế cho critical data:
# Producer config
acks=all
retries=2147483647 # Retry "forever" (Integer.MAX_VALUE)
max.in.flight.requests.per.connection=5 # Với idempotent=true, an toàn tăng lên 5
enable.idempotence=true
min.insync.replicas — Tại Sao Cần Set
min.insync.replicas là số ISR replicas tối thiểu phải có để producer với acks=all được phép ghi. Nếu ISR ít hơn số này, producer nhận NotEnoughReplicasException.
Công thức: ISR_count >= min.insync.replicas để ghi được, và replication_factor >= min.insync.replicas + 1 để chịu được ít nhất 1 broker failure.
Ví dụ với replication.factor=3:
min.insync.replicas=2:
- Normal (ISR=3): OK, chịu được 1 broker failure
- 1 broker down (ISR=2): OK, vẫn đủ ISR
- 2 broker down (ISR=1): FAIL, producer nhận exception
--> Tolerate: 1 failure
min.insync.replicas=1 (default):
- Ngay cả khi chỉ còn leader trong ISR, vẫn ghi được
- Durability thực sự = acks=1 (chỉ leader có data)
--> Đừng dùng với acks=all nếu bạn quan tâm đến durability
Topic-level config override:
# Khi tạo topic hoặc alter topic config
min.insync.replicas=2
3. Log Compaction vs Log Retention
Kafka có hai cơ chế xử lý data cũ:
Retention by Time/Size (Delete policy)
# Retention theo thời gian (mặc định 7 ngày)
log.retention.hours=168
log.retention.ms=604800000
# Retention theo size (per partition)
log.retention.bytes=107374182400 # 100 GB per partition
# Kích thước mỗi segment file
log.segment.bytes=1073741824 # 1 GB
Khi segment đủ cũ hoặc partition vượt size limit, Kafka xóa toàn bộ segment (không xóa từng message). Đây là lý do bạn không bao giờ nên set log.retention.bytes quá nhỏ trên partition có segment lớn.
Log Compaction — Kafka Như Một Key-Value Store
Log compaction giữ lại message mới nhất cho mỗi key, xóa các message cũ hơn có cùng key. Use cases:
- Changelog topics (Kafka Streams internal state): giữ latest state per key
- CDC (Change Data Capture): latest row state per primary key
__consumer_offsetstopic: Kafka tự dùng compaction ở đây
Before compaction (cleanup.policy=compact):
offset: 0 key=user-1 value={"name":"Alice"}
offset: 1 key=user-2 value={"name":"Bob"}
offset: 2 key=user-1 value={"name":"Alice Smith"} <-- cùng key
offset: 3 key=user-3 value={"name":"Charlie"}
offset: 4 key=user-2 value={"name":"Robert"} <-- cùng key
After compaction:
offset: 2 key=user-1 value={"name":"Alice Smith"} <-- latest
offset: 3 key=user-3 value={"name":"Charlie"}
offset: 4 key=user-2 value={"name":"Robert"} <-- latest
Tombstone message: gửi message với key nhưng value=null để "xóa" key khỏi compacted log. Kafka giữ tombstone trong delete.retention.ms trước khi xóa hẳn, để consumer kịp nhận "deletion event".
Compaction Gotchas — Không Phải Real-Time
Log compaction không xảy ra ngay lập tức. Cleaner thread chạy background:
log.cleaner.min.cleanable.ratio=0.5 # Compact khi 50% log là "dirty" (có duplicate keys)
log.cleaner.min.compaction.lag.ms=0 # Thời gian tối thiểu message tồn tại trước khi compact
log.cleaner.max.compaction.lag.ms= # Default: không giới hạn (có thể lag dài)
Implication quan trọng: Nếu bạn dùng compacted topic như một key-value store và cần đọc "latest state" ngay sau khi write, bạn có thể đọc phải stale data nếu cleaner chưa chạy. Đây là gotcha kinh điển trong Kafka Streams.
Kết hợp cả hai:
# Compaction + Delete (giữ latest mỗi key, nhưng xóa sau 7 ngày)
cleanup.policy=compact,delete
log.retention.ms=604800000
4. Consumer Group Protocol
Consumer Group Rebalance — Khi Nào Trigger
Rebalance là quá trình Kafka phân phối lại partitions giữa các consumers trong một group. Trigger khi:
- Consumer mới join group
- Consumer rời group (crash, shutdown, hoặc quá 30s không poll)
- Topic thêm partition mới
- Consumer subscribe thêm topic mới
Before rebalance (3 consumers, 6 partitions):
Consumer-A: [part-0, part-1]
Consumer-B: [part-2, part-3]
Consumer-C: [part-4, part-5]
Consumer-C crashes → Rebalance:
STOP-THE-WORLD (Eager rebalance):
1. Tất cả consumers revoke tất cả partitions
2. Re-join group
3. Phân phối lại:
Consumer-A: [part-0, part-1, part-4]
Consumer-B: [part-2, part-3, part-5]
Trong thời gian này: không consumer nào xử lý → LAG SPIKE
Trong production, lag spike trong rebalance có thể kéo dài 30-60 giây nếu không tune đúng.
Eager vs Cooperative Rebalance
Eager (default trước Kafka 2.4): Stop-the-world. Tất cả consumers drop partitions, re-join, nhận lại partitions mới. Gây downtime.
Cooperative Incremental (từ Kafka 2.4+): Chỉ revoke các partitions cần di chuyển, không phải tất cả. Consumers tiếp tục xử lý partitions không bị ảnh hưởng.
// Enable Cooperative Rebalance
props.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,
CooperativeStickyAssignor.class.getName());
Cooperative rebalance khi Consumer-C crash:
Round 1: Broker thông báo partitions [part-4, part-5] cần di chuyển
Consumer-A và B revoke ONLY part-4, part-5
Consumer-A, B vẫn xử lý [part-0,1] và [part-2,3]
Round 2: Phân phối part-4 và part-5 lại
Consumer-A: [part-0, part-1, part-4]
Consumer-B: [part-2, part-3, part-5]
max.poll.interval.ms vs session.timeout.ms
Đây là hai config hay bị nhầm lẫn nhất:
session.timeout.ms (default: 45000ms):
- Broker-side timeout
- Nếu broker không nhận heartbeat từ consumer trong khoảng thời gian này
→ Broker coi consumer chết → trigger rebalance
- Heartbeat gửi bởi background thread riêng (không phải poll thread)
- Set quá thấp → spurious rebalances khi consumer bận xử lý
- Set quá cao → chậm phát hiện consumer thật sự chết
max.poll.interval.ms (default: 300000ms = 5 phút):
- Client-side timeout
- Nếu khoảng cách giữa 2 lần poll() vượt quá giá trị này
→ Consumer tự rời group → trigger rebalance
- Dùng để detect consumer bị stuck trong processing loop
- Tăng nếu message processing time dài (batch jobs, external API calls)
Timeline:
t=0: Consumer poll() → nhận 100 messages
t=0-t=50s: Consumer xử lý (slow processing)
t=45s: session.timeout expires → nhưng heartbeat thread vẫn chạy → KHÔNG rebalance
t=300s: max.poll.interval expires → consumer bị coi là dead → rebalance!
Tuning cho slow processors:
# Consumer config
max.poll.interval.ms=600000 # 10 phút nếu processing thực sự chậm
max.poll.records=50 # Reduce batch size để xử lý nhanh hơn
session.timeout.ms=30000
heartbeat.interval.ms=10000 # Phải < session.timeout.ms / 3
Consumer Lag Monitoring
Consumer Lag = Latest Offset (log-end-offset) - Committed Offset (consumer position)
Topic: orders, Partition 0:
Log end offset: 1000
Consumer committed: 950
Lag: 50 messages
Tool monitoring:
# kafka-consumer-groups.sh
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
--group my-consumer-group --describe
# Output:
# GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG
# my-group orders 0 950 1000 50
# my-group orders 1 1200 1200 0
Prometheus metrics (JMX hoặc Kafka exporter):
# kafka_consumer_group_lag{group="my-group", topic="orders", partition="0"} 50
Alert rule thực tế:
# Prometheus AlertManager rule
- alert: KafkaConsumerLagHigh
expr: kafka_consumer_group_lag > 10000
for: 5m
labels:
severity: warning
annotations:
summary: "Consumer group {{ $labels.group }} lag {{ $value }} on {{ $labels.topic }}"
Burrow (LinkedIn's open-source lag evaluator) tốt hơn threshold-based lag alerting vì nó evaluate lag trend (đang tăng hay đang giảm) thay vì chỉ compare với threshold tĩnh.
5. Producer Tuning Thực Chiến
batch.size và linger.ms — Throughput vs Latency
Producer batch messages trước khi gửi. Hai config điều khiển batching:
batch.size=65536 # Max bytes per batch (default 16KB, tăng lên 64KB hoặc 128KB cho throughput)
linger.ms=10 # Đợi tối đa 10ms để fill batch trước khi gửi
linger.ms=0 (default):
Producer → gửi ngay khi có message → nhiều request nhỏ → throughput thấp, latency thấp
linger.ms=10:
t=0ms: message 1 vào buffer
t=3ms: message 2 vào buffer
t=7ms: message 3 vào buffer
t=10ms: batch full (hoặc timeout) → gửi cùng 1 request → throughput cao hơn
Trade-off:
linger.ms=0: tốt cho latency-sensitive (payment processing, real-time recommendations)linger.ms=5-20: tốt cho throughput (event streaming, logs, metrics)batch.sizelớn hơn giảm overhead per request, nhưng tốn memory buffer
Compression
compression.type=lz4 # Recommended cho most cases
# Options: none, gzip, snappy, lz4, zstd
So sánh thực tế:
none: Baseline. Không tốn CPU, dùng nhiều bandwidth + disk.
gzip: Compress tốt nhất (~70-80%), CPU cao, latency cao.
Dùng khi bandwidth là bottleneck (cross-DC replication).
snappy: Compress tốt (~50-60%), CPU thấp, được Google thiết kế cho speed.
lz4: Compress vừa (~50%), CPU cực thấp, speed cao nhất.
--> KHUYẾN NGHỊ cho hầu hết use cases
zstd: Compress tốt (~70%), CPU vừa, Kafka 2.1+.
--> Tốt khi muốn balance compression ratio và speed
Compression xảy ra ở producer, decompress ở consumer. Broker lưu data đã compressed (không decompress). Một gotcha: nếu broker cần reassign batches (acks=all, recompressing), có thể ảnh hưởng CPU.
Idempotent Producer — Exactly-Once At Producer Level
Without idempotence:
Producer → Broker (message sent, network timeout)
Producer → Broker (retry) → Broker nhận duplicate!
Với enable.idempotence=true:
- Mỗi producer có Producer ID (PID) unique, assigned bởi broker
- Mỗi message có Sequence Number tăng dần per partition
- Broker deduplicate dựa trên (PID, partition, sequence number)
- Đảm bảo exactly-once delivery within a producer session
enable.idempotence=true
acks=all # Bắt buộc với idempotence
retries=2147483647
max.in.flight.requests.per.connection=5 # Tối đa 5 in-flight, an toàn với idempotence
Lưu ý: Idempotence chỉ bảo vệ trong một producer session. Nếu producer restart, PID mới được assign. Cho cross-session exactly-once, cần Kafka Transactions.
6. KRaft vs ZooKeeper
ZooKeeper là Bottleneck Như Thế Nào
ZooKeeper lưu cluster metadata (broker registration, topic configs, partition assignments, ISR lists). Mọi thay đổi metadata đều phải qua ZooKeeper:
Với ZooKeeper (pre-KRaft):
+-------------+
| ZooKeeper | <-- single point of coordination
| Ensemble |
+-------------+
/ | \
Broker1 Broker2 Broker3
(Controller)
Vấn đề:
- Metadata scalability: ZooKeeper không thể handle cluster với hàng triệu partitions efficiently (Kafka 2.x bottleneck ở ~200k partitions)
- Controller failover chậm: Controller mới phải load toàn bộ metadata từ ZooKeeper → có thể mất 30-60 giây
- Operational complexity: 2 distributed systems để vận hành (Kafka + ZooKeeper)
- Split-brain risk: ZooKeeper partition có thể gây inconsistency
KRaft (Kafka Raft) — GA từ Kafka 3.3
KRaft loại bỏ ZooKeeper hoàn toàn. Metadata được lưu trong một internal Kafka topic (__cluster_metadata), quản lý bởi Quorum Controller dùng Raft consensus:
KRaft architecture:
+----------+ +----------+ +----------+
| Broker1 | | Broker2 | | Broker3 |
| (Ctrlr) | | (Ctrlr) | | |
+----------+ +----------+ +----------+
| | |
+----Raft Consensus Quorum------+
(cho __cluster_metadata)
Benefits:
- Controller failover < 1 giây (metadata đã replicated via Raft)
- Support hàng triệu partitions (tested với 10 triệu partitions)
- Chỉ cần vận hành 1 distributed system
Migration từ ZK → KRaft:
# Kafka 3.x: Migration mode available
# 1. Upgrade brokers lên Kafka 3.x
# 2. Chạy migration tool để copy metadata từ ZK sang KRaft
# 3. Enable KRaft mode trên controllers
# 4. Remove ZooKeeper
# Với deployment mới (Kafka 3.3+): dùng KRaft từ đầu
process.roles=broker,controller # Hoặc tách riêng broker và controller nodes
node.id=1
controller.quorum.voters=1@controller1:9093,2@controller2:9093,3@controller3:9093
Kết luận thực tế: Nếu bạn deploy Kafka mới từ 2024 trở đi, dùng KRaft. Không cần học ZooKeeper internals nữa — nó đang được deprecate.
7. Operational Concerns
JVM Heap Sizing
Broker memory = JVM Heap + OS Page Cache + OS overhead
Rule of thumb:
- JVM Heap: 6-8 GB (đủ cho GC, metadata, connections)
- Phần còn lại: Page Cache (càng nhiều càng tốt)
Ví dụ server 64 GB RAM:
- JVM Heap: 6 GB (-Xmx6g -Xms6g)
- Page Cache available: ~56 GB
- GC: G1GC (mặc định Kafka), không dùng CMS
Đừng set JVM heap lớn hơn 8 GB:
- Larger heap → longer GC pauses → heartbeat miss → rebalance
- Page cache ít hơn → nhiều disk I/O hơn → throughput thấp
# Kafka start script
export KAFKA_HEAP_OPTS="-Xmx6g -Xms6g"
export KAFKA_JVM_PERFORMANCE_OPTS="-server -XX:+UseG1GC \
-XX:MaxGCPauseMillis=20 -XX:InitiatingHeapOccupancyPercent=35"
Disk Throughput là Bottleneck Chính
Kafka throughput gần như hoàn toàn phụ thuộc vào disk:
- Dùng multiple disks (JBOD — Just a Bunch Of Disks), mỗi disk handle một số partitions
- Không dùng RAID (Kafka tự xử lý replication), RAID-5 đặc biệt tệ (write amplification)
- NVMe SSD >> SATA SSD >> HDD cho production Kafka
# Nhiều disk paths cho JBOD
log.dirs=/data1/kafka,/data2/kafka,/data3/kafka
Kafka sẽ round-robin phân phối partitions giữa các disks. Mỗi disk độc lập → aggregate throughput tăng tuyến tính.
Partitioning Strategy — Bao Nhiêu Partitions Là Đủ?
Rule of thumb từ Confluent:
Partitions per topic = max(target throughput / consumer throughput,
target throughput / producer throughput)
Ví dụ:
- Target: 1 GB/s
- Consumer throughput: 50 MB/s per instance
- Partitions cần: 1000/50 = 20 partitions
Giới hạn thực tế:
- Mỗi partition = 1 open file handle trên broker (file descriptor)
- Mỗi partition = 1 thread trên producer (khi gửi)
- Rebalance time tỷ lệ thuận với số partitions
- Đừng over-partition: 1000 partitions/broker là reasonable max
Over-partitioning là anti-pattern phổ biến. Bắt đầu ít, tăng sau — Kafka hỗ trợ tăng partitions nhưng không giảm.
# Tăng partitions (không giảm được)
kafka-topics.sh --alter --topic my-topic \
--partitions 20 \
--bootstrap-server localhost:9092
Replication Flow — ASCII Diagram
Producer write flow với acks=all, ISR=[leader, f1, f2]:
Producer
|
| ProduceRequest (acks=all)
v
Leader Broker
|
|-- append to local log
|
|-- sends to Follower 1 ------> Follower 1
| |-- append to local log
| |-- FetchResponse (ack)
| v
|<----- f1 acked --------------
|
|-- sends to Follower 2 ------> Follower 2
| |-- append to local log
| |-- FetchResponse (ack)
| v
|<----- f2 acked --------------
|
v
ProduceResponse (success) -------> Producer
Nếu f2 không ack trong replica.lag.time.max.ms:
→ f2 bị kick khỏi ISR
→ ISR = [leader, f1]
→ ProduceResponse thành công khi chỉ f1 ack (nếu min.insync.replicas=2)
Mental Model — Checklist Trước Khi Production
Durability checklist:
-
replication.factor >= 3cho critical topics -
min.insync.replicas = replication.factor - 1(thường = 2) -
acks=allở producer cho critical data -
enable.idempotence=trueở producer -
unclean.leader.election.enable=false(default Kafka 3.x)
Performance checklist:
- JVM heap <= 8 GB, phần còn lại cho page cache
- Multiple disks (JBOD), không RAID
-
compression.type=lz4hoặczstdở producer - Tune
batch.sizevàlinger.mstheo use case (throughput vs latency) -
max.poll.recordsphù hợp với processing time
Consumer checklist:
- Dùng
CooperativeStickyAssignorđể tránh stop-the-world rebalance -
max.poll.interval.msđủ lớn cho processing time thực tế - Consumer lag monitoring với alerting
- Idempotent consumer logic (xử lý at-least-once delivery)
Ops checklist:
- KRaft cho deployment mới (không cần ZooKeeper)
- Số partitions: đủ dùng, không over-partition
- Log retention đặt phù hợp với storage capacity
- Monitoring: broker disk usage, network I/O, ISR shrink events, unclean elections
Red flags cần điều tra ngay:
- ISR shrink events tăng → network issue hoặc slow brokers
- Consumer lag tăng liên tục → consumer bị stuck hoặc producer throughput tăng đột biến
- Frequent rebalances → consumer crash loop hoặc
max.poll.interval.msquá thấp - Disk usage > 80% → tăng retention delete hoặc thêm disk
Tài Liệu Tham Khảo
- Kafka Documentation — Configuration
- Confluent: Optimizing Kafka Throughput
- Jay Kreps: The Log — What every software engineer should know about real-time data
- Kafka Improvement Proposal: KRaft (KIP-500)
- Sách: "Kafka: The Definitive Guide" (2nd Edition) — Narkhede, Shapira, Palino