NATS JetStream durable crawler: nối lại, giao lại và khóa item
NATS JetStream durable crawler là cách dùng durable consumer của JetStream làm hàng đợi URL, để worker mất kết nối rồi nối lại vẫn đi tiếp đúng chỗ. Trạng thái nằm ở phía server, không nằm trong worker. ProxyVN chạy thư viện nats-py thật trên một máy chủ mô phỏng tự viết, thử ngắt worker, giao lại, nak, term, rồi ghi bảng số lần giao theo từng item.
NATS JetStream durable crawler: mục tiêu và phạm vi không lặp
Phần này chốt bài toán, đầu ra và giới hạn.
Bài toán cần giải
Một nhóm có vài worker tải trang chạy trên máy rẻ, thường xuyên khởi động lại. Khi worker chết, họ không muốn mất vị trí đang làm và không muốn đọc lại từ đầu. Họ cần một hàng đợi nhớ hộ: item nào xong, item nào đang dở, item nào đã thử mấy lần.
Đầu ra nghiệm thu
Đầu ra là lab mất consumer rồi nối lại, kèm bảng delivery attempt theo khóa item. Bảng đó cho thấy item nào được giao một lần, item nào được giao lại sau khi hết AckWait. Phần code gồm khai báo stream, durable consumer và vòng lặp worker.
Ranh giới với bài RabbitMQ và Bluesky
Bài RabbitMQ publisher confirms nói về confirm và ack theo giao thức AMQP. Bài này nói về stream và trạng thái consumer của JetStream. Chữ Jetstream trong bài Proxy Bluesky là dịch vụ khác của AT Protocol, trùng tên, không liên quan tới NATS. Bài này không dùng dữ liệu Bluesky.
Giới hạn của lab
Cần nói rõ ngay. Lab không chạy nats-server thật. Máy lab không tải được máy chủ từ bên ngoài, nên ProxyVN viết một máy chủ nhỏ bằng Python, nói giao thức văn bản của NATS và trả lời các subject $JS.API cơ bản. Nó có stream trong bộ nhớ, durable consumer, AckWait, MaxDeliver, bốn kiểu trả lời và khử trùng theo Nats-Msg-Id.
Thứ gì thật, thứ gì giả
Thư viện nats-py 2.16.0 là thật, nói qua socket thật, tự tạo inbox, tự phân tích subject ack thành metadata. Ngắt kết nối cũng là thật. Còn quy tắc giao lại do ProxyVN viết theo tài liệu. Máy chủ mô phỏng không có đĩa, không có cluster, không có RAFT, không có giới hạn stream.
Đọc kết quả thế nào cho đúng
Lab kiểm tra mã worker phản ứng đúng với tín hiệu mà tài liệu mô tả. Nó không chứng minh nats-server thật lưu bền, cũng không đo tốc độ. Mã lỗi và thông báo do máy chủ mô phỏng trả, ví dụ khi tạo consumer thứ hai, cũng là văn bản ProxyVN đặt, nên đừng so từng chữ với máy chủ thật.
Ai nên đọc bài này
Người chạy crawler phân tán và đang cân nhắc NATS. Người đã có NATS cho nhắn tin và muốn tận dụng thêm cho hàng đợi. Người muốn hiểu vì sao tên durable lại quan trọng tới vậy.
Dùng durable consumer cho URL work queue trong NATS JetStream durable crawler
Phần này tóm tắt các khái niệm từ tài liệu chính thức.
Stream và consumer
Stream là nơi lưu thông điệp theo subject. Consumer là con trỏ đọc trên stream, có trạng thái riêng. Worker không giữ vị trí đọc. Vị trí, danh sách đang chờ ack, số lần giao đều nằm ở consumer trên server. Đây là lý do worker chết mà công việc không mất dấu.
Durable nghĩa là gì
Một consumer durable có tên và tồn tại độc lập với kết nối của client. Tài liệu về pull consumer dùng ví dụ bind vào durable tên shipping rồi fetch từng lô. Client đi rồi quay lại với đúng tên đó sẽ nhận tiếp từ chỗ dở.
Pull hợp với crawler hơn push
Với pull, worker chỉ lấy khi rảnh, nên tốc độ tải tự quyết định tốc độ lấy. Thêm worker là thêm một tiến trình gọi fetch trên cùng durable, không cần cấu hình gì thêm. Worker chậm thì lấy ít, không có chuyện bị dồn việc tới tràn bộ nhớ.
Ba chính sách giữ thông điệp

Chọn cách giữ
Trang Retention policies mô tả ba loại. Limits giữ tới khi chạm giới hạn. Interest giữ tới khi mọi consumer quan tâm đã ack. WorkQueue giữ tới khi một consumer ack, ack đầu tiên xóa thông điệp cho tất cả.
Chính sách |
Khi nào thông điệp bị xóa |
Hợp với |
|---|---|---|
Limits |
Khi chạm giới hạn tuổi hoặc dung lượng |
Nhật ký, dữ liệu đọc lại nhiều lần |
Interest |
Khi mọi consumer quan tâm đã ack |
Nhiều hệ thống cùng đọc |
WorkQueue |
Khi một consumer ack |
Hàng đợi việc, mỗi việc một worker |
Chọn WorkQueue cho URL
Mỗi URL là một việc cho đúng một worker. Tài liệu xếp hàng đợi việc vào WorkQueue. Stream tự cạn khi làm xong, không cần dọn. Nếu anh em muốn giữ lịch sử để chạy lại, tách làm hai: stream Limits lưu sự kiện gốc, stream WorkQueue chứa việc cần làm.
Bốn kiểu trả lời
Trang Ack responses and redelivery nêu bốn kiểu. Ack là xong. Nak là lỗi, giao lại. Term là bỏ hẳn, không giao cho ai nữa. In-progress là còn đang làm, đặt lại bộ đếm AckWait. Ba kiểu đầu là kết thúc, kiểu thứ tư chỉ kéo dài thời gian.
Trả lời |
Hàm nats-py |
Dùng khi |
|---|---|---|
ack |
msg.ack() |
Đã lưu kết quả |
nak |
msg.nak() |
Lỗi tạm, proxy hỏng, website 503 |
term |
msg.term() |
URL bị cấm, sai định dạng, không thử nữa |
in-progress |
msg.in_progress() |
Trang chậm, còn đang tải |
AckWait
Tài liệu nêu AckWait mặc định 30 giây. Thông điệp được giao nằm trong danh sách chờ, đồng hồ chạy. Hết giờ không có trả lời, server coi như worker đã dừng và giao lại. Server không phân biệt được worker chết với worker chậm, nó chỉ biết chưa có ack.
MaxAckPending
Trang Scaling a consumer nêu MaxAckPending là trần số thông điệp đã giao mà chưa ack, mặc định 1000, dùng chung cho cả consumer. Đặt thấp quá thì worker ngồi chơi. Đặt cao quá thì khi nhiều worker chết, một lượng lớn task cùng giao lại một lúc.
Giao ít nhất một lần
Tài liệu gọi vòng ack và giao lại là một trong những phần tạo nên at-least-once. Trang về worker cũng nói thẳng cùng một việc có thể chạy hai lần, nên phải khóa side effect theo mã item. Worker phải chịu được trùng, đó là điều kiện của mô hình này.
Ack thường và double ack
Tài liệu Delivery and acknowledgment nêu ack thường là gửi rồi đi, không chờ xác nhận. Nếu ack mất trên đường, server sẽ giao lại. Double ack chờ server ghi nhận rồi mới coi là xong. Với crawl có thao tác ghi đè được, ack thường là đủ.
Chọn stream subject và durable consumer của NATS JetStream durable crawler
Phần này là mã khai báo, chạy thật bằng nats-py.
Khai báo stream
Subject nên có cấu trúc để lọc được về sau. Lab dùng crawl.url theo sau là tên nguồn. Dấu lớn hơn ở cuối khớp mọi token phía sau.
import nats
from nats.js.api import ConsumerConfig, AckPolicy, RetentionPolicy, StreamConfig
nc = await nats.connect("nats://127.0.0.1:9476", allow_reconnect=False)
js = nc.jetstream()
await js.add_stream(StreamConfig(
name="CRAWL",
subjects=["crawl.url.>"],
retention=RetentionPolicy.WORK_QUEUE,
))
Cổng 9476 là của lab, NATS mặc định dùng 4222. Trên server thật anh em chỉ đổi địa chỉ.
Publish có mã item
Trang Publishing giải thích vì sao cần khử trùng: publish lỗi không có nghĩa là chưa lưu, vì timeout có thể xảy ra sau khi server đã lưu. Publisher đặt mã item vào header Nats-Msg-Id, gửi lại cùng mã trong cửa sổ khử trùng thì server không thêm bản mới.
a = await js.publish(
"crawl.url.shop",
json.dumps({"item": "p1", "url": "http://s/p1"}).encode(),
headers={"Nats-Msg-Id": "p1"},
)
Lab gửi năm item, rồi gửi lại p1. Lần gửi lại nhận PubAck có duplicate bằng True và seq là 1, đúng số của bản gốc như tài liệu mô tả, stream không có bản thứ sáu.
PubAck khác publish thường
Publish qua js.publish chờ PubAck từ server. Tài liệu gọi PubAck là bằng chứng duy nhất cho việc thông điệp đã lưu. Publish thẳng bằng nc.publish thì không biết gì. Crawler nên luôn dùng js.publish cho URL. Nếu PubAck không về, gửi lại cùng Nats-Msg-Id là an toàn trong cửa sổ khử trùng.
Khai báo durable consumer
Bốn tham số quan trọng nhất của lab nằm ở đây. Lab dùng AckWait 1 giây để đo nhanh, ngoài đời phải dài hơn thời gian tải một trang.
cfg = ConsumerConfig(
durable_name="fetchers",
ack_policy=AckPolicy.EXPLICIT,
ack_wait=1.0,
max_deliver=3,
max_ack_pending=10,
)
await js.add_consumer("CRAWL", cfg)
sub = await js.pull_subscribe("crawl.url.>", durable="fetchers", stream="CRAWL")
Tên durable nên đặt theo công việc, không theo máy. Mọi worker cùng việc dùng chung một tên, đó là cách chia tải.
Đặt tên subject theo nguồn
Subject có cấu trúc giúp sau này tách luồng mà không phải đổi publisher. Ví dụ crawl.url.shop và crawl.url.news vẫn vào cùng stream CRAWL. Khi một nguồn cần tốc độ thấp hơn, anh em tách riêng nguồn đó thành stream hoặc consumer có filter. Đừng nhét URL vào subject, vì subject không phải chỗ chứa dữ liệu và ký tự trong URL dễ phá cấu trúc token.
Giới hạn tốc độ theo website
JetStream điều tiết theo khả năng lấy việc của worker, không biết gì về website đích. Nếu mười worker cùng lấy URL của một host, host đó nhận mười request song song. Giới hạn theo host phải nằm trong worker hoặc một bộ đếm dùng chung. Nguồn nhỏ nên có một consumer riêng với ít worker, cách này đơn giản nhất.
Vòng lặp worker
Worker gọi fetch theo lô, xử lý từng thông điệp, trả lời từng cái. Đây là ví dụ triển khai dựa trên mã lab, thêm phân loại lỗi.
while True:
try:
msgs = await sub.fetch(10, timeout=5)
except nats.errors.TimeoutError:
continue
for m in msgs:
item = json.loads(m.data)
try:
await crawl_and_upsert(item)
await m.ack()
except TemporaryError:
await m.nak()
except ForbiddenUrl:
await m.term()
Hết việc thì fetch ném TimeoutError, lab ghi nhận đúng lỗi đó ở ca hàng rỗng. Đừng coi nó là sự cố, chỉ cần quay lại vòng lặp.
Fetch bao nhiêu mỗi lần
Lô lớn giảm số lượt hỏi server, nhưng worker chết thì cả lô đó phải chờ hết AckWait. Với tải trang, mỗi item mất vài giây, lô nhỏ là hợp lý. Nguyên tắc: thời gian xử lý cả lô phải nhỏ hơn AckWait, nếu không phần cuối lô sẽ hết hạn trước khi tới lượt.
Đọc metadata của thông điệp
Mỗi thông điệp có m.metadata do client tách ra từ subject trả lời. Hai trường hữu ích nhất là sequence.stream và num_delivered. Trường thứ hai cho biết đây là lần giao thứ mấy, dùng để quyết định thử tiếp hay bỏ.
md = m.metadata
print(item["item"], md.sequence.stream, md.num_delivered)
Dòng này chính là nguồn của bảng delivery attempt ở phần sau.
Cấu hình ack policy và delivery attempt trong NATS JetStream durable crawler
Phần này đi qua các ca lỗi không liên quan tới mất kết nối.
URL luôn lỗi
Lab tạo stream BAD có hai item, consumer có max_deliver bằng 3. Item hong luôn bị nak, item tot được ack. Thứ tự giao như sau.
[('hong', 1), ('hong', 2), ('hong', 3), ('tot', 1), 'het']
stream con 1, ack_pending 0
Item hong được giao ba lần rồi dừng. Trong máy chủ mô phỏng, nak đưa nó về đầu hàng nên item tot phải chờ. Thứ tự ở server thật có thể khác, đừng dựa vào nó.
Thông điệp hết MaxDeliver không tự biến mất
Điểm dễ bỏ sót nằm ở dòng thứ hai của output. Stream vẫn còn một thông điệp, tức item hong nằm đó, không ai nhận nữa. Trong mô phỏng là vậy, và anh em phải kiểm chứng trên server thật. Dù hành vi cụ thể ra sao, crawler cần tự ghi nhận URL đã hết lượt, đừng chờ nó tự xuất hiện ở đâu đó.
Không để hết lượt một cách âm thầm
Cách làm chắc tay: khi num_delivered chạm ngưỡng, worker tự chuyển item sang subject lỗi rồi term. Như vậy item có chỗ để xem lại và không tiêu thụ thêm tài nguyên.
if m.metadata.num_delivered >= 3:
await js.publish("crawl.dead." + source, m.data)
await m.term()
Đoạn này là ví dụ triển khai, chưa chạy trong lab. Điều kiện cần là có một stream khác nhận crawl.dead.
Nak ngay hay nak có trễ
Tài liệu nêu nak thường giao lại ngay, và điều đó hiếm khi là thứ anh em muốn. Lỗi tạm thường kéo dài vài giây tới vài phút, giao lại tức thì chỉ đốt lượt thử. Hãy dùng nak có độ trễ, trong nats-py là tham số delay của hàm nak. Lab chỉ thử nak thường.
Term dùng khi nào
Term dành cho item không bao giờ xử lý được: URL bị robots cấm, định dạng sai, website trả 410. Lab gửi một item, gọi term, rồi fetch tiếp trong 0,7 giây, không nhận lại. Tài liệu nêu term loại khỏi danh sách chờ như ack, nhưng công việc chưa hề được làm, nên nhớ ghi lại lý do ở kho của mình.
Task dài và in-progress

Còn đang làm
Lab đặt AckWait 0,5 giây, worker gọi in_progress mỗi 0,3 giây trong 1,2 giây. Thông tin consumer lúc đó báo ack_pending 1, redelivered 0, tức chưa bị giao lại dù đã quá thời hạn gốc. Khi ngừng gọi, 0,7 giây sau thông điệp được giao lại với số lần là 2.
Ack muộn của worker cũ
Sau khi bản giao lại đã ack, lab cho worker cũ ack muộn trên thông điệp cũ. Phía client không báo lỗi gì, vì ack thường không chờ xác nhận. Nghĩa là worker cũ tưởng mình làm xong và không biết việc đã có người khác làm. Kho dữ liệu vì vậy cần ghi đè được.
Chọn AckWait cho crawler
AckWait phải lớn hơn thời gian tải điển hình cộng thời gian lưu. Nếu thời gian tải dao động mạnh, đừng đặt theo trường hợp xấu nhất, vì worker chết thì item nằm chờ đúng khoảng đó. Hãy đặt theo mức thông thường và gọi in_progress cho trang chậm. Quy tắc này giống cách gia hạn visibility timeout của SQS.
Tham số |
Lab |
Gợi ý ban đầu |
Lý do |
|---|---|---|---|
ack_wait |
0,5 đến 1 giây |
Lớn hơn timeout tải |
Tránh giao lại khi đang làm |
max_deliver |
3 |
3 đến 5 |
Đủ cho lỗi tạm, không quay mãi |
max_ack_pending |
10 |
Số worker nhân lô fetch |
Đủ việc, không ôm quá nhiều |
Lô fetch |
3 đến 4 |
Nhỏ |
Worker chết thì ít item phải chờ |
Cột gợi ý là quan điểm của ProxyVN, chưa đo trên hệ thống thật.
Dừng consumer rồi nối lại đúng durable name trong NATS JetStream durable crawler
Phần này là tài sản chính của bài: mất worker, nối lại, và đọc trạng thái.
Kịch bản
Stream có năm item. Worker 1 lấy ba item, ack một, rồi đóng kết nối mà không trả lời hai item còn lại. Sau 1,2 giây, tức hơn AckWait, worker 2 nối lại với cùng tên durable.
Trạng thái trước khi ngắt
Ngay sau khi ack p1, thông tin consumer báo ack_pending 2, pending 2. Nghĩa là hai item đang được giữ bởi worker 1, và hai item p4, p5 chưa giao cho ai.
[('p1', 1, 1), ('p2', 2, 1), ('p3', 3, 1)]
info ack_pending 2 pending 2 redelivered 0
Mỗi bộ ba trong output là mã item, số thứ tự trong stream, và số lần giao.
Trạng thái sau khi nối lại

Nối lại đúng tên
Sau khi hết AckWait, consumer báo ack_pending 0, pending 4, delivered 3. Hai item đang dở đã quay về hàng chờ. Vị trí delivered vẫn là 3, nghĩa là consumer vẫn nhớ chỗ đã đi tới, không đọc lại từ đầu.
info ack_pending 0 pending 4 redelivered 0 delivered 3
('p2', 2, 2)
('p3', 3, 2)
('p4', 4, 1)
('p5', 5, 1)
Worker 2 không cần biết gì về worker 1. Chỉ cần đúng tên durable là nhận đúng phần việc còn lại.
Bảng delivery attempt theo khóa item
Đây là đầu ra nghiệm thu chính. Cột khóa item là mã anh em tự đặt, cột số lần giao lấy từ metadata. Worker nên ghi bảng này vào kho riêng để tra cứu.
Khóa item |
Stream seq |
Worker nhận lần cuối |
Số lần giao |
Kết thúc |
|---|---|---|---|---|
p1 |
1 |
Worker 1 |
1 |
ack |
p2 |
2 |
Worker 2 |
2 |
ack |
p3 |
3 |
Worker 2 |
2 |
ack |
p4 |
4 |
Worker 2 |
1 |
ack |
p5 |
5 |
Worker 2 |
1 |
ack |
Sai tên durable thì sao
Lab gọi consumer_info với tên fetcherz. Client nhận NotFoundError với mã 404 và thông báo consumer not found. Nếu mã gọi add_consumer với tên sai, sẽ có một consumer mới, và trên WorkQueue chuyện đó không đơn giản, xem phần dưới. Tên durable là khóa của trạng thái, gõ sai là mất dấu.
Consumer thứ hai trên WorkQueue
Lab thử tạo consumer khác không có filter trên cùng stream WorkQueue. Máy chủ mô phỏng từ chối với BadRequestError. Quy tắc này ProxyVN viết theo hiểu biết về nats-server, trang retention đọc được chưa nêu chi tiết, nên anh em thử lại trên server thật. Với WorkQueue, mỗi thông điệp chỉ thuộc về một consumer.
Vì sao worker không nên tự giữ vị trí
Một số crawler tự ghi con trỏ vào tệp. Với durable consumer, việc đó thừa và nguy hiểm, vì hai nguồn trạng thái sẽ lệch nhau. Worker chỉ nên giữ kết quả công việc. Câu hỏi đang ở đâu thì hỏi server qua consumer_info.
Khoảng trống khi worker chết
Từ lúc worker chết tới lúc hết AckWait, item đang dở nằm im. Server không biết worker đã chết, như tài liệu nhấn mạnh. Vì vậy AckWait dài làm phục hồi chậm hơn, đây là cái giá phải trả. Trang về worker mô tả đúng tình huống này: item ở trạng thái đang xử lý tới khi hết giờ.
Dừng worker tử tế
Với lần triển khai có kế hoạch, worker nên ngừng fetch, làm xong lô hiện tại, trả lời hết rồi mới đóng. Phần còn dở nếu có thì gọi nak để giao lại ngay, khỏi chờ AckWait. Cách này giúp thời gian đứng của hàng đợi gần như bằng không khi cập nhật mã.
Đối soát pending, ack, redelivery và khóa item của NATS JetStream durable crawler
Phần này nối trạng thái consumer với dữ liệu thật trong kho.
Ba con số cần xem

Đọc ba con số
Thông tin consumer có ba số đáng theo dõi. num_pending là số chưa giao. num_ack_pending là số đang làm. num_redelivered là số thông điệp đã từng giao lại. Trong ca nối lại, num_redelivered của máy chủ mô phỏng cập nhật theo lần giao, nên lúc vừa hết AckWait vẫn là 0.
Tín hiệu |
Ý nghĩa |
Việc cần làm |
|---|---|---|
num_pending tăng mãi |
Worker không theo kịp |
Thêm worker |
num_ack_pending chạm trần |
Worker giữ việc quá lâu |
Xem trang chậm, xem MaxAckPending |
num_redelivered tăng nhanh |
Worker chết hoặc AckWait ngắn |
Tăng AckWait hoặc gọi in_progress |
Khóa item là gì
Khóa item là mã nghiệp vụ, ví dụ nguồn cộng mã sản phẩm. Nó khác stream sequence, vì publisher gửi lại ngoài cửa sổ khử trùng thì sequence mới nhưng khóa cũ. Mọi thao tác ghi vào kho dùng khóa này, không dùng sequence.
Ghi kết quả chịu được trùng
Ví dụ dưới là câu upsert theo khóa item. Trang tải lần hai ghi đè lên bản cũ, không tạo dòng mới.
INSERT INTO page(item_key, url, body_hash, attempts, fetched_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(item_key) DO UPDATE SET
body_hash = excluded.body_hash,
attempts = excluded.attempts,
fetched_at = excluded.fetched_at;
Cột attempts lấy từ num_delivered, nên bảng delivery attempt có sẵn trong kho. Câu lệnh là ví dụ triển khai, chưa chạy trong lab.
Đối soát đầu cuối
Cuối mỗi lượt, so tập khóa item đã publish với tập khóa có trong kho và tập khóa ở subject lỗi. Chênh lệch là item bị bỏ sót. Lab không chạy phép đối soát này ở quy mô lớn, nhưng bảng năm item ở trên là dạng thu nhỏ của nó.
Một URL, nhiều lượt crawl
Crawl định kỳ làm một URL xuất hiện nhiều lần theo thời gian. Khóa item vì thế có hai phần: khóa nội dung và lượt chạy. Bảng kết quả dùng khóa nội dung để giữ bản mới nhất, bảng lịch sử dùng cả hai phần. Nhầm hai tầng này, lượt sau sẽ bị coi là trùng và bỏ qua.
Đo thời gian từ lúc đẩy tới lúc xong
Publisher nên ghi thời điểm vào thân thông điệp. Worker so với thời điểm lưu kết quả để có độ trễ đầu cuối. Con số này nói được nhiều hơn độ sâu hàng đợi, vì nó tính cả thời gian chờ giao lại. Tăng đều trong nhiều giờ nghĩa là thiếu worker, tăng theo từng đợt thường là worker chết.
Cửa sổ khử trùng có giới hạn
Nats-Msg-Id chỉ khử trùng trong một khoảng thời gian cấu hình trên stream. Gửi lại URL sau khoảng đó sẽ thành thông điệp mới. Máy chủ mô phỏng giữ mã mãi mãi, nên lab không tái lập được ca này. Với crawl định kỳ, đưa số lượt vào mã, ví dụ p1 ghép với ngày chạy.
Proxy trong vòng lặp worker
Proxy nằm trong hàm crawl_and_upsert, không liên quan tới stream. Lỗi proxy nên đi nhánh nak có trễ, vì đó là lỗi tạm. Với website được phép thu thập, có lúc cần IP tại Việt Nam để nhận đúng nội dung trong nước. Khi đó anh em có thể mua proxy tốc độ cao tại trang chủ ProxyVN. Proxy không thay được AckWait và không chống được trùng. Vai trò của nó trong thu thập dữ liệu có ở bài Web Scraping proxy là gì, cách ghi chuỗi cấu hình ở bài định dạng proxy.
Phân loại lỗi trước khi trả lời
Worker cần một bảng quyết định rõ ràng, nếu không mọi lỗi sẽ thành nak. Bảng dưới là gợi ý của ProxyVN.
Lỗi |
Trả lời |
Ghi chú |
|---|---|---|
Timeout mạng, proxy lỗi |
nak có trễ |
Lỗi tạm |
Website 429, 503 |
nak có trễ dài |
Tôn trọng tín hiệu giảm tốc |
Website 404, 410 |
ack, ghi trạng thái |
Kết quả hợp lệ, không phải lỗi |
robots cấm, URL ngoài phạm vi |
term |
Không thử lại |
Lỗi phân tích nội dung |
Chuyển subject lỗi |
Giữ để xem sau |
Đầu ra nghiệm thu của NATS JetStream durable crawler
Phần này gom tiêu chí và ghi rõ phần chưa thử.
Danh sách kiểm tra
Hệ thống đạt khi mọi dòng dưới đây đúng.
- Stream dùng WorkQueue và subject có cấu trúc theo nguồn.
- Publisher dùng js.publish kèm Nats-Msg-Id ổn định.
- Consumer có ack_policy explicit, ack_wait, max_deliver và max_ack_pending rõ ràng.
- Mọi worker cùng việc dùng chung một tên durable.
- Kết quả ghi đè được theo khóa item.
- Có đường đi cho item hết lượt.
- Có theo dõi ba con số của consumer.
Kịch bản phá hỏng nên chạy trên server thật
Trên nats-server thật, chạy lại bốn ca: kill worker giữa lô, khởi động lại server khi còn pending, nối lại với đúng tên, và cho URL luôn lỗi. Sau mỗi ca so bảng delivery attempt với bảng trong bài. Chỗ nào khác, tin server thật.
Những gì lab chưa chạy
Chưa chạy nats-server thật. Chưa có lưu file, chưa có cluster, chưa có nhân bản stream, chưa có cửa sổ khử trùng có hạn, chưa có nak có trễ, chưa có BackOff, chưa đo thông lượng. Thông báo lỗi và thứ tự giao lại trong bài là của máy chủ mô phỏng.
Từ hàng đợi Redis chuyển sang
Nhiều crawler bắt đầu bằng danh sách Redis: LPUSH rồi BRPOP. Cách đó không có khái niệm đang làm, nên worker chết là mất item. Khi chuyển sang durable consumer, phần phải sửa nhiều nhất là thêm trả lời sau khi lưu và làm cho lưu chịu được trùng. Phần publish gần như đổi một dòng.
Trình tự triển khai
Làm theo thứ tự này để mỗi bước đều kiểm tra được.
- Dựng stream và consumer trên môi trường thử.
- Đẩy một lô nhỏ URL có mã, xác nhận PubAck.
- Chạy một worker, kill giữa lô, đọc lại consumer_info.
- Đổi lưu kết quả sang ghi đè theo khóa item.
- Thêm phân loại lỗi và đường đi cho item hết lượt.
- Tăng số worker, theo dõi ba con số trong một tuần rồi mới chỉnh AckWait.
Lưu file hay lưu bộ nhớ
Stream của crawler nên dùng lưu file, ví dụ tài liệu dùng cờ storage file khi tạo stream. Lưu bộ nhớ thì khởi động lại server là mất hàng đợi. Số bản sao tùy số node. Lab không kiểm chứng phần này, nên anh em đọc thêm tài liệu về nhân bản trước khi chạy thật.
Giới hạn stream là lưới an toàn
Tài liệu nhắc giới hạn stream vẫn áp dụng với WorkQueue và đóng vai trò chặn cuối. Nếu worker ngừng hẳn, stream không thể phình vô tận. Hãy đặt giới hạn dung lượng hoặc tuổi, và cảnh báo trước khi chạm ngưỡng, vì chạm giới hạn có nghĩa là URL bắt đầu bị loại hoặc bị từ chối.
NATS JetStream durable crawler: câu hỏi thường gặp
Các câu dưới đây gom những điểm anh em hay hỏi.
Worker chết thì task có mất không?
Không mất, nếu chưa ack. Sau AckWait, server giao lại cho worker khác trên cùng durable. Lab thấy p2 và p3 được giao lần hai cho worker mới.
Nối lại thì có đọc lại từ đầu không?
Không, miễn là đúng tên durable. Trạng thái nằm ở consumer trên server. Lab thấy delivered vẫn là 3 sau khi worker cũ đi.
Có bảo đảm mỗi task chạy đúng một lần không?
Không. Tài liệu mô tả giao ít nhất một lần. Worker chậm hoặc ack bị mất đều có thể gây chạy hai lần, nên phải ghi đè được theo khóa item.
Dùng ack, nak hay term khi website trả 404?
Với crawler, 404 là kết quả hợp lệ. Ghi trạng thái rồi ack. Term dành cho item không nên thử nữa nhưng cũng không có kết quả, ví dụ URL bị cấm.
Vì sao lab không dùng nats-server thật?
Máy lab không tải được máy chủ từ bên ngoài. Vì vậy bài dùng máy chủ mô phỏng tự viết. Client nats-py là thật, còn quy tắc giao lại lấy từ tài liệu, nên cần kiểm lại trên server thật.
NATS JetStream có liên quan tới Jetstream của Bluesky không?
Không. Hai thứ trùng tên. Bài này chỉ nói về NATS.
NATS JetStream durable crawler và những điều cần nhớ
NATS JetStream durable crawler giữ trạng thái ở consumer trên server, nên worker chết rồi nối lại đúng tên durable sẽ nhận tiếp phần dở. AckWait quyết định tốc độ phục hồi, còn khóa item và thao tác ghi đè lo chuyện trùng. Proxy.vn - Nhà cung cấp dịch vụ proxy chất lượng hàng đầu Việt Nam khuyên anh em chạy lại NATS JetStream durable crawler trên server thật.