Mục lục
- Mục Tiêu Bài Học
- Bài Toán: Singleton Task Trong Cụm
- Cơ Chế Leader Election Với Redis Lock
- Implementation: LeaderElection Class
- TTL Và Renewal Interval
- Use Case 1: Singleton Scheduler
- Use Case 2: Cleanup & Maintenance Job
- Use Case 3: Per-Partition Consumer
- Vấn Đề Split Brain
- Renewal Failure — Mất Leadership
- Graceful Step-Down
- Consensus System Là Alternative
- Monitoring Leader Election
- Anti-patterns & Best Practices
- Tổng Kết & Quiz
Mục Tiêu Bài Học
- Hiểu tại sao cần leader election khi scale app lên nhiều instance.
- Nắm cơ chế acquire lock = leader, renewal giữ leadership, lock expire = failover.
- Implement
LeaderElectionvới Lua atomic renew, graceful step-down. - Nhận biết và xử lý split brain.
- Biết khi nào nên dùng Redis leader election và khi nào cần consensus system.
Bài Toán: Singleton Task Trong Cụm
App được deploy với 3 instance (pod, container, server). Mỗi instance có một goroutine hoặc thread chạy cron job lúc 2:00 AM để gửi email báo cáo ngày. Không có cơ chế điều phối, cả 3 instance đều chạy job → email được gửi 3 lần. Tương tự với cleanup job xóa dữ liệu cũ: nếu 3 instance cùng chạy, xóa trùng lặp hoặc xung đột.
Một số task điển hình yêu cầu singleton (chỉ 1 instance chạy tại một thời điểm):
- Scheduler/cron job: gửi email, push notification, aggregate stats.
- Cleanup job: xóa record hết hạn, purge log cũ, archive data.
- Report generation: tạo báo cáo tốn CPU/IO, không nên chạy song song.
- Partition consumer: mỗi partition của message queue có đúng 1 consumer.
Yêu cầu với cơ chế điều phối:
- Tại mọi thời điểm, nhiều nhất 1 instance chạy task (exclusive execution).
- Khi instance giữ quyền chạy bị chết, instance khác phải tự động tiếp nhận (failover) — không cần can thiệp thủ công.
- Failover phải đủ nhanh để task không bị delay quá lâu.
Đây là bài toán leader election: trong cụm N instance, chọn 1 instance làm leader để chạy task đặc biệt. Khi leader chết, bầu leader mới.
Cơ Chế Leader Election Với Redis Lock
Leader election dùng Redis lock hoạt động theo nguyên tắc sau:
- Mỗi instance cố acquire một lock chung (
SET NX EX). Instance nào acquire được → là leader. - Leader chạy task, đồng thời renew lock định kỳ (reset TTL) để giữ leadership.
- Instance không acquire được → là follower. Follower retry acquire định kỳ.
- Khi leader chết (crash, bị kill, mất kết nối Redis) → lock không được renew → TTL expire → lock biến mất.
- Follower acquire thành công → trở thành leader mới.
Sơ đồ trạng thái của một instance:
┌─────────────────────────────────────────┐
│ │
acquire() renew() OK
│ │
▼ │
┌──────────────┐ acquire() ┌──────────────────────┐
│ FOLLOWER │ ─────────────▶ │ LEADER │
│ (waiting) │ NX success │ (running task) │
└──────────────┘ └──────────────────────┘
▲ │
│ renew() fail
│ (lock mất)
└─────────────────────────────────┘
Renew lock nghĩa là reset TTL về giá trị ban đầu, chỉ khi lock vẫn thuộc về instance hiện tại (check ownership). Dùng Lua script để check + PEXPIRE trong một bước atomic:
-- Renew: chỉ PEXPIRE nếu value khớp (vẫn là owner)
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('PEXPIRE', KEYS[1], ARGV[2])
end
return 0
Nếu lock đã bị acquire bởi instance khác (sau khi expire), Lua script trả về 0 — instance nhận biết mình đã mất leadership.
Implementation: LeaderElection Class
Class đầy đủ bằng Python với redis-py:
import socket
import uuid
import threading
import time
import logging
logger = logging.getLogger(__name__)
class LeaderElection:
"""
Leader election dùng Redis lock với renewal.
TTL: thời gian lock tự expire nếu không được renew (giây).
Renew định kỳ mỗi TTL/3 để giữ leadership.
Failover time (worst case): TTL giây sau khi leader chết.
"""
def __init__(self, redis_client, name: str, ttl: int = 15):
self.redis = redis_client
self.key = f"leader:{name}"
# ID duy nhất per-instance, per-process: hostname + random suffix
self.id = f"{socket.gethostname()}:{uuid.uuid4().hex[:12]}"
self.ttl = ttl
self.is_leader = False
# Lua script: atomic check owner + PEXPIRE
self._renew_lua = redis_client.register_script("""
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('PEXPIRE', KEYS[1], ARGV[2])
end
return 0
""")
def try_become_leader(self) -> bool:
"""
Cố acquire leadership.
Trả True nếu acquire thành công (trở thành leader).
"""
acquired = self.redis.set(self.key, self.id, nx=True, ex=self.ttl)
if acquired:
self.is_leader = True
logger.info("Acquired leadership: key=%s id=%s", self.key, self.id)
return bool(acquired)
def renew(self) -> bool:
"""
Renew lock nếu vẫn là owner.
Trả False nếu mất leadership (lock expire hoặc instance khác đã acquire).
"""
result = self._renew_lua(
keys=[self.key],
args=[self.id, self.ttl * 1000] # PEXPIRE dùng milliseconds
)
if not result:
if self.is_leader:
logger.warning("Lost leadership: key=%s id=%s", self.key, self.id)
self.is_leader = False
return bool(result)
def step_down(self):
"""
Graceful release: xóa lock (với ownership check) để follower lên ngay,
không cần chờ TTL expire.
"""
if not self.is_leader:
return
release_script = """
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
end
return 0
"""
result = self.redis.eval(release_script, 1, self.key, self.id)
self.is_leader = False
if result:
logger.info("Stepped down gracefully: key=%s id=%s", self.key, self.id)
else:
logger.warning("Step-down: lock was already gone: key=%s", self.key)
def run(self, on_elected, on_revoked, on_follower=None):
"""
Vòng lặp chính:
- Nếu là leader: renew, nếu renew fail thì gọi on_revoked.
- Nếu là follower: thử acquire, nếu thành công thì gọi on_elected.
- Ngủ TTL/3 giữa mỗi vòng.
"""
interval = self.ttl / 3
while True:
try:
if self.is_leader:
if self.renew():
on_elected()
else:
on_revoked()
else:
if self.try_become_leader():
on_elected()
elif on_follower:
on_follower()
except Exception:
logger.exception("Error in leader election loop")
time.sleep(interval)
Vài điểm cần lưu ý trong implementation này:
self.idkết hợp hostname + UUID hex để đảm bảo unique kể cả khi 2 instance chạy trên cùng máy (vd Docker container).PEXPIREtrong Lua nhận milliseconds, nên phải nhânttl * 1000.on_electedđược gọi mỗi renewal interval khi là leader — nên đây là nơi kiểm tra và chạy task, không phải callback một lần.try/excepttrong vòng lặp tránh exception không xử lý làm tắt thread.
TTL Và Renewal Interval
Hai thông số quan trọng nhất của leader election:
- TTL: thời gian lock tự expire nếu không được renew. Quyết định thời gian failover.
- Renewal interval: tần suất leader gọi renew. Phải nhỏ hơn TTL nhiều để lock không expire trước renew kế tiếp.
Quy tắc thực tế: renewal interval = TTL / 3. Ví dụ TTL = 15s → renew mỗi 5s. Với cách này, lock có thể bỏ lỡ tới 2 lần renew liên tiếp mà vẫn chưa expire — bộ đệm an toàn cho các trường hợp network blip ngắn.
| TTL | Renewal interval | Failover time (worst case) | Renew calls/phút |
|---|---|---|---|
| 10s | ~3.3s | 10s | ~18 |
| 15s | 5s | 15s | 12 |
| 30s | 10s | 30s | 6 |
| 60s | 20s | 60s | 3 |
Trade-off:
- TTL nhỏ: failover nhanh (leader chết → follower lên sau ≤ TTL), nhưng renew nhiều hơn và dễ bị flapping nếu mạng không ổn định.
- TTL lớn: ít renew, ổn định hơn với mạng kém, nhưng khi leader crash task bị gián đoạn lâu hơn.
Đối với scheduler, failover 15–30s thường chấp nhận được. Với task cần phản hồi nhanh hơn, TTL 10s. Không nên để TTL dưới 5s trừ khi network rất ổn định — risk flapping cao.
Lỗi thường gặp: đặt renewal interval ≥ TTL. Ví dụ TTL = 10s, sleep 10s giữa các lần renew → lock expire đúng lúc chuẩn bị renew, leader tự mất quyền dù đang chạy bình thường.
Use Case 1: Singleton Scheduler
App có 3 pod, mỗi pod cần chạy scheduler kiểm tra và trigger due jobs mỗi 5 giây — nhưng chỉ 1 pod làm việc này tại một thời điểm.
import redis
r = redis.Redis(host="redis", port=6379, decode_responses=True)
election = LeaderElection(r, name="scheduler", ttl=15)
def on_elected():
# Được gọi mỗi renewal interval khi là leader
run_due_jobs()
def on_revoked():
# Mất leadership — log để theo dõi
logger.warning("Scheduler lost leadership, stepping back to follower")
def main():
# Chạy trong background thread hoặc process
election.run(on_elected=on_elected, on_revoked=on_revoked)
def run_due_jobs():
"""
Kiểm tra và chạy các job đến hạn.
Hàm này phải idempotent: chạy 2 lần cùng lúc không gây lỗi.
"""
now = time.time()
due = fetch_jobs_due_before(now)
for job in due:
try:
execute_job(job)
except Exception:
logger.exception("Job failed: %s", job.id)
Lưu ý thiết kế quan trọng: run_due_jobs() phải idempotent. Trong kịch bản split brain (mục 9), hai instance có thể cùng gọi hàm này. Nếu mỗi job được đánh dấu "claimed" bằng atomic operation trước khi chạy (vd UPDATE ... WHERE status='pending' RETURNING id), chạy 2 lần không sinh ra side-effect kép.
Use Case 2: Cleanup & Maintenance Job
Daily cleanup xóa record hết hạn. Task chạy một lần mỗi ngày, tốn vài phút.
import datetime
CLEANUP_INTERVAL = 86400 # 24 giờ, tính bằng giây
cleanup_election = LeaderElection(r, name="daily-cleanup", ttl=30)
last_cleanup_date = None
def on_cleanup_elected():
global last_cleanup_date
today = datetime.date.today()
if last_cleanup_date == today:
return # Đã chạy hôm nay rồi, skip
logger.info("Starting daily cleanup")
run_cleanup()
last_cleanup_date = today
logger.info("Daily cleanup done")
def run_cleanup():
"""
Xóa record hết hạn. Dùng DELETE WHERE ... LIMIT batch_size
để tránh lock table quá lâu.
"""
batch_size = 1000
deleted = 1
while deleted > 0:
deleted = db.execute(
"DELETE FROM events WHERE expired_at < NOW() LIMIT %s",
(batch_size,)
)
time.sleep(0.1) # throttle, tránh hammer DB
Cleanup leader dùng TTL lớn hơn (30s) vì task chạy lâu. Follower không làm gì trong lúc leader cleanup — không cần on_follower callback. Khi leader chết giữa chừng, follower lên và chạy lại từ đầu — cần cleanup idempotent (xóa lần 2 không sao, DB không còn record hết hạn để xóa nữa).
Use Case 3: Per-Partition Consumer
Một message queue có 4 partition. Cần 4 consumer, mỗi partition đúng 1 consumer để duy trì message ordering. Khi consumer crash, partition phải được tiếp nhận bởi instance khác.
Tạo leader election độc lập cho từng partition:
NUM_PARTITIONS = 4
elections = [
LeaderElection(r, name=f"partition-{i}", ttl=15)
for i in range(NUM_PARTITIONS)
]
def consume_partition(partition_id: int, election: LeaderElection):
def on_elected():
# Instance này là consumer của partition_id
messages = fetch_messages(partition_id, batch_size=100)
for msg in messages:
process_message(msg)
ack_message(msg)
def on_revoked():
logger.info("Lost consumer role for partition %d", partition_id)
election.run(on_elected=on_elected, on_revoked=on_revoked)
# Khởi chạy trong các thread riêng biệt
threads = []
for i, elec in enumerate(elections):
t = threading.Thread(
target=consume_partition,
args=(i, elec),
daemon=True
)
t.start()
threads.append(t)
Trong cụm 4 instance, mỗi instance có thể là leader của nhiều partition cùng lúc (nếu một số instance chết). Leader election per-partition cho phép N instance chia tải N partition linh hoạt.
Vấn Đề Split Brain
Split brain là tình huống 2 instance đều tin mình là leader và cùng chạy task. Kịch bản dẫn đến split brain với Redis lock:
- Instance A đang là leader, đang giữa critical section.
- A bị pause (GC stop-the-world, VM snapshot, OS scheduling) trong thời gian > TTL.
- Lock của A expire vì không được renew.
- Instance B acquire lock, trở thành leader mới, bắt đầu chạy task.
- A tỉnh dậy sau khi pause, không biết mình đã mất lock — tiếp tục task.
- Cả A và B cùng chạy task → side-effect xảy ra 2 lần.
Đây là vấn đề đã đề cập ở bài 44 (lock expiration và process pause). Mitigation cho leader election:
-
Task phải idempotent: chạy 2 lần không sinh side-effect kép. Ví dụ scheduler claim job bằng
UPDATE ... WHERE status = 'pending' AND NOT EXISTS (SELECT 1 FROM processing WHERE job_id = ...)— atomic, chỉ 1 instance claim được. - Fencing token cho task có side-effect: resource (DB, object storage) nhận fencing token từ lock và từ chối request từ token đã bị invalidate. Xem bài 46.
-
Kiểm tra renew trước mỗi bước quan trọng: trong task dài, gọi
renew()trước các bước có side-effect. Nếu renew trả False, dừng ngay. - Consensus system thay Redis: Zookeeper, etcd có strong consistency — không có split brain. Dùng khi correctness quan trọng hơn đơn giản.
Redis lock + leader election phù hợp cho efficiency: giảm duplicate, tiết kiệm tài nguyên, không phải zero. Task phải thiết kế chịu được duplicate thỉnh thoảng xảy ra.
Renewal Failure — Mất Leadership
Khi renew() trả về False, instance đã mất leadership. Điều này xảy ra khi:
- Lock đã expire (instance bị pause quá lâu, mạng gián đoạn dài).
- Instance khác đã acquire lock sau khi expire.
Hành động bắt buộc: dừng task ngay lập tức khi renew fail. Tiếp tục chạy sau khi mất lock dẫn đến split brain.
def on_elected():
"""
Được gọi mỗi renewal interval.
Hàm này nên kiểm tra trước khi làm side-effect lớn.
"""
# Với task ngắn (vài ms): chạy ngay
run_quick_task()
def on_elected_long_task():
"""
Với task dài: renew từng bước, dừng nếu mất leadership.
"""
# Bước 1
data = fetch_data()
# Kiểm tra leadership trước bước có side-effect
if not election.renew():
logger.warning("Lost leadership before step 2, aborting")
return
# Bước 2 (có side-effect)
persist_results(data)
if not election.renew():
logger.warning("Lost leadership before step 3, aborting")
return
# Bước 3
send_notifications(data)
Nếu task có bước không thể bỏ dở giữa chừng (vd database transaction), hãy thiết kế sao cho transaction đó atomic và idempotent — nếu chạy lại từ đầu cũng không gây lỗi.
Graceful Step-Down
Khi instance shutdown bình thường (rolling deploy, scale down, restart), nếu instance đó là leader thì follower phải chờ tới khi TTL expire trước khi lên được leader mới. Với TTL = 15s, downtime của task là 15s.
Graceful step-down: leader chủ động release lock khi nhận tín hiệu shutdown, follower acquire ngay lập tức (không chờ TTL). Downtime gần như bằng 0.
import signal
import sys
election = LeaderElection(r, name="scheduler", ttl=15)
def shutdown_handler(signum, frame):
logger.info("Received shutdown signal, stepping down")
election.step_down()
sys.exit(0)
# Đăng ký handler cho SIGTERM (Kubernetes gửi khi pod bị terminate)
signal.signal(signal.SIGTERM, shutdown_handler)
signal.signal(signal.SIGINT, shutdown_handler)
Trong Kubernetes, pod nhận SIGTERM khi bị terminate (rolling update, scale down). Xử lý SIGTERM + step_down đảm bảo leadership chuyển giao nhanh trong quá trình deploy mà không cần TTL lớn.
Lưu ý: step_down() dùng Lua script check-and-delete (giống bài 45) để không xóa nhầm lock của instance khác nếu leadership đã được chuyển giao trước đó.
Consensus System Là Alternative
Redis single-node lock không có strong consistency. Trong điều kiện network partition hoặc process pause, split brain có thể xảy ra. Nếu task yêu cầu correctness tuyệt đối (không thể sai dù thỉnh thoảng), nên dùng consensus system:
- Zookeeper: tạo ephemeral sequential znode. Instance nào tạo được znode với sequence number nhỏ nhất là leader. Khi leader disconnect, znode tự động xóa → Zookeeper thông báo → follower nhỏ nhất kế tiếp lên leader. Đảm bảo không split brain nhờ consensus protocol (ZAB).
-
etcd: cung cấp lease API và election API (
concurrency.Electiontrong clientv3). Leader campaign để acquire, follower observe. Nếu leader mất lease, follower tự động tiếp quản. - Consul: dùng session + leader key, tích hợp health check để phát hiện instance chết nhanh hơn TTL.
So sánh:
| Tiêu chí | Redis lock | Zookeeper / etcd |
|---|---|---|
| Độ phức tạp setup | Thấp (Redis đã có) | Cao (thêm dependency) |
| Split brain | Có thể xảy ra | Không (consensus) |
| Failover speed | Phụ thuộc TTL | Thường nhanh hơn (session timeout) |
| Phù hợp với | Efficiency, task idempotent | Correctness critical |
Nếu hệ thống đã dùng Redis và task có thể thiết kế idempotent, Redis leader election là lựa chọn hợp lý. Không cần thêm Zookeeper chỉ để chạy cron job nếu cron job đó có thể chạy 2 lần mà không hại gì.
Monitoring Leader Election
Các metric cần theo dõi:
-
Who is leader: mỗi instance expose gauge
is_leader{name="scheduler"}= 1 nếu là leader, 0 nếu follower. Tổng giá trị của tất cả instance phải bằng 1. > 1 nghĩa là split brain; = 0 nghĩa là no leader. - Leadership change rate: đếm số lần acquire thành công per minute. Flapping (liên tục acquire/lose) là dấu hiệu TTL quá ngắn hoặc mạng không ổn định.
- Failover time: thời gian từ khi leader chết đến khi follower acquire được lock. Nên nhỏ hơn TTL một chút (follower retry ngay khi lock biến mất).
- Renewal failure rate: số lần renew trả 0 per instance per minute. Tăng đột biến → instance đang bị GC pause dài hoặc mạng chậm.
Alert quan trọng:
- Không có leader nào trong > 2 × TTL: cụm có vấn đề, không instance nào acquire được.
- Có > 1 leader cùng lúc: split brain đang xảy ra.
- Leadership thay đổi > N lần / phút: flapping.
from prometheus_client import Gauge, Counter
is_leader_gauge = Gauge(
"leader_election_is_leader",
"1 if this instance is currently leader",
["name"]
)
leadership_acquired_total = Counter(
"leader_election_acquired_total",
"Total number of times this instance acquired leadership",
["name"]
)
renewal_failure_total = Counter(
"leader_election_renewal_failure_total",
"Total number of renewal failures",
["name"]
)
# Trong LeaderElection.try_become_leader():
# if acquired: leadership_acquired_total.labels(name=name).inc()
# Trong LeaderElection.renew():
# is_leader_gauge.labels(name=name).set(1 if result else 0)
# if not result: renewal_failure_total.labels(name=name).inc()
Anti-patterns & Best Practices
Anti-patterns
- Renewal interval ≥ TTL: lock expire trước renew tiếp theo → leader liên tục mất và acquire lại (flapping).
- Tiếp tục task sau renew fail: khi
renew()trả False, instance vẫn tiếp tục chạy → split brain. - Task không idempotent với Redis leader election: gửi email, charge payment không idempotent + split brain = side-effect kép. Cần idempotency ở tầng task hoặc dùng consensus.
- Không graceful step-down: leader restart mà không release lock → downtime task bằng TTL mỗi lần deploy.
- TTL quá lớn: TTL = 5 phút nghĩa là khi leader crash, task bị gián đoạn 5 phút.
- Dùng Redis leader election cho correctness-critical task: financial transaction, data migration không thể chạy 2 lần → nên dùng consensus hoặc fencing token.
- Instance ID không đủ unique: hostname thuần (không thêm UUID) có thể trùng trong một số môi trường containerized → ownership check sai.
Best Practices
- TTL = 10–30s cho phần lớn use case; renewal interval = TTL / 3.
- Task của leader phải idempotent — thiết kế trước khi implement election.
- Dừng task ngay khi
renew()trả False. - Graceful step-down khi nhận SIGTERM.
- Monitor:
is_leadergauge, renewal failure rate, leadership change rate. - Alert khi no leader > 2 × TTL hoặc leader count > 1.
- Correctness critical → Zookeeper hoặc etcd thay Redis.
- Renewal dùng Lua atomic (check owner + PEXPIRE) — không dùng SET NX thuần để renew.
Tổng Kết & Quiz
Tổng kết
- Leader election giải quyết bài toán singleton task trong cụm N instance: scheduler, cleanup, per-partition consumer.
- Cơ chế:
SET NX EXacquire + Lua atomic renew + TTL expire = failover tự động. - Failover time worst case = TTL. Renewal interval = TTL / 3 là quy tắc thực tế.
- Split brain xảy ra khi leader bị pause > TTL. Mitigation chính: task idempotent, dừng ngay khi renew fail.
- Graceful step-down (release lock khi shutdown) giảm downtime khi deploy.
- Redis election phù hợp cho efficiency; correctness-critical → Zookeeper, etcd.
- Monitor: is_leader gauge, renewal failure, leadership change rate.
Quiz
- Nếu TTL = 20s và renewal interval = 15s, điều gì xảy ra? Kể tên ít nhất 1 scenario cụ thể.
- Lua script renew kiểm tra owner trước khi PEXPIRE. Tại sao kiểm tra này cần thiết? Nếu bỏ đi (PEXPIRE trực tiếp không check), hậu quả là gì?
- Instance A đang là leader, renew trả 0. A nên làm gì ngay lập tức? Tại sao không nên tiếp tục task vài giây rồi mới dừng?
- Mô tả sequence of events dẫn đến split brain với Redis leader election. Tại sao consensus system như etcd không bị vấn đề này?
- Graceful step-down dùng Lua check-and-delete thay vì DEL trực tiếp. Cho ví dụ một kịch bản mà DEL trực tiếp gây lỗi.
Đáp án gợi ý
- TTL = 20s, renewal interval = 15s: mỗi lần renew reset TTL về 20s, nhưng nếu một lần renew bị delay hoặc fail, đến lần renew kế tiếp (15s sau) lock chỉ còn 5s TTL — rất dễ expire trước khi renew kịp. Scenario: leader bị delay network 5s → renew thất bại → 5s sau lock expire → follower lên. Leader tỉnh dậy và renew → lock đã là của follower, renew trả 0 → nhận biết mất leadership. Nhưng nếu delay đúng lúc logic check, có thể bỏ sót dẫn đến split brain ngắn.
- Nếu bỏ check owner, PEXPIRE sẽ reset TTL của bất kỳ lock nào đang tồn tại, dù lock đã được acquire bởi instance khác. Kịch bản: A pause > TTL → lock expire → B acquire → A tỉnh dậy, gọi PEXPIRE (không check) → reset TTL của B's lock → B tiếp tục là leader nhưng TTL vừa bị reset bởi A. Vô hại trong trường hợp này, nhưng nếu A cũng tự đặt lại
is_leader = True, split brain xuất hiện. - A phải dừng task ngay lập tức và set
is_leader = False. Không nên delay vì: renew trả 0 nghĩa là B đã acquire lock. Nếu A tiếp tục chạy thêm vài giây, cả A và B cùng chạy task (split brain). "Vài giây" đủ để A gửi email, charge payment, hay xóa record. - Sequence: leader A bị GC pause dài > TTL → lock expire → follower B acquire → B là leader mới, bắt đầu task → A tỉnh dậy, chưa gọi renew lại, vẫn tin
is_leader=True→ A tiếp tục task → split brain. etcd dùng consensus (Raft): lease chỉ được gia hạn nếu leader vẫn kết nối với majority nodes. Nếu A bị network partition hoặc pause, etcd revoke lease A ngay → A nhận biết mất leadership qua watch event trước khi tỉnh dậy làm điều nguy hiểm. - Kịch bản: A là leader với TTL = 15s. A bị pause 16s → lock expire → B acquire. B đang là leader. A tỉnh dậy, gọi
step_down()với DEL trực tiếp → DEL keyleader:scheduler→ xóa lock của B → B mất leadership dù đang chạy bình thường → cả hệ thống không có leader trong khoảng thời gian cho đến khi B hoặc C acquire lại. Với check-and-delete Lua: A's token không khớp value của lock (đang là B's token) → Lua trả 0, không xóa → B tiếp tục là leader an toàn.
Bài tiếp theo
Bài 50 xét distributed semaphore — giới hạn N concurrent worker thay vì chỉ 1 như leader election.
