WIUNIX ACADEMY
پیام تکراری در RabbitMQ؛ تفاوت تأیید انتشار و پایان پردازش
صفحهٔ 2 از 2؛ مراحل قبلی و پیشنیازها را پیش از ادامه مرور کن.
آموزش متناسب با محیط تو
کجا اجرا میکنی؟
سیستمعامل و نسخه را انتخاب کن تا مراحل، دستورها و راهحلها برای همان محیط نمایش داده شوند.
آموزش متناسب با مقادیر تو
تنظیمات این آموزش
مقادیر نمونه را برای محیط خودت تنظیم کن؛ متن و دستورهای مرتبط با هم تغییر میکنند. رمز، کلید خصوصی و توکن وارد نکن. مقادیر فقط در همین زبانه نگهداری میشوند و به سرور یا لینک اشتراکگذاری فرستاده نمیشوند. پیش از اجرا، DNS، گواهیها و سیاست شبکهٔ واقعی را بررسی کن.
آیپی، دامنه یا پورت مثالها با محیط تو فرق دارد؟ دکمهٔ زیر را باز کن، مقادیر را تغییر بده و «اعمال در آموزش» را بزن؛ سپس دستورهای بهروزشده را کپی کن.
تنظیم مقادیر برای محیط من8 مقدار قابلتغییر
مثال چگونه فاصلهٔ ثبت نتیجه تا ACK را نشان میدهد؟
فایل کامل را ذخیره کنید. برنامه فایل پایگاهدادهٔ SQLite یا صف موجود را نمیپذیرد. ابتدا پیامی با mandatory به مسیری میفرستد که صفی ندارد و انتظار دارد این وضعیت به کلاینت گزارش شود؛ چون publisher ACK بهتنهایی مسیریابی موفق را ثابت نمیکند. سپس یک پیام آزمایشیِ قابلمسیریابی را با تأیید انتشار میفرستد.
برنامه در نخستین دریافت پیام یک ردیف با شناسهٔ یکتا در SQLite ثبت میکند و عمداً بدون ACK اتصال را میبندد. برنامه در دریافت دوبارهٔ پیام همان پیام را دوباره میگیرد، میبیند ردیف قبلاً ثبت شده، محتوایش را تطبیق میدهد و ACK میفرستد. این ردیف فقط جای نتیجهٔ یک کار آزمایشی است؛ پرداخت، ارسال ایمیل یا تراکنش بین چند سرویس را شبیهسازی نمیکند.
کلاینت آزمایشی با Pika نسخهٔ ۱٫۳ یا بالاترِ نصبشده
import getpass, os, sqlite3, ssl, uuid, time
from pathlib import Path
import pika
root = Path('/home/academy/rabbitmq-probe-20261028')
ledger = root / 'effect-ledger.sqlite'
fd = os.open(ledger, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
os.close(fd)
db = sqlite3.connect(ledger)
db.execute('PRAGMA synchronous=FULL')
db.execute('CREATE TABLE effect(message_id TEXT PRIMARY KEY, body BLOB NOT NULL)')
db.commit()
password = getpass.getpass('Staging RabbitMQ password: ')
tls = ssl.create_default_context(cafile='/etc/ssl/certs/academy-rabbitmq-ca.pem')
def connect():
return pika.BlockingConnection(pika.ConnectionParameters(
host='rabbitmq.example.com', port=5671, virtual_host='academy_lab',
credentials=pika.PlainCredentials('academy_probe', password),
ssl_options=pika.SSLOptions(tls, 'rabbitmq.example.com'),
heartbeat=30, blocked_connection_timeout=10, socket_timeout=5,
connection_attempts=1))
queue = 'academy_ack_probe_20261028'
message_id = 'academy_effect_20261028'
conn = connect()
ch = conn.channel()
try:
ch.queue_declare(queue=queue, passive=True)
except pika.exceptions.ChannelClosedByBroker as exc:
if exc.reply_code != 404:
raise
ch = conn.channel()
else:
raise RuntimeError('Queue already exists: stop; do not reuse it')
ch.queue_declare(queue=queue, durable=True,
arguments={'x-queue-type':'quorum', 'x-quorum-initial-group-size':3})
ch.confirm_delivery()
props = pika.BasicProperties(delivery_mode=2, message_id=message_id,
content_type='text/plain')
try:
ch.basic_publish(exchange='', routing_key='absent_' + uuid.uuid4().hex,
body=b'negative-route-probe', properties=props, mandatory=True)
except pika.exceptions.UnroutableError:
print('PASS: unroutable mandatory publish reported')
else:
raise RuntimeError('Missing negative routing result')
ch.basic_publish(exchange='', routing_key=queue, body=b'academy-synthetic-effect',
properties=props, mandatory=True)
print('PASS: routed publish confirmed')
conn.close()
for attempt in (1,2):
conn = connect()
ch = conn.channel()
deadline = time.monotonic() + 10
while True:
method, properties, body = ch.basic_get(queue=queue, auto_ack=False)
if method is not None or time.monotonic() >= deadline:
break
conn.process_data_events(time_limit=0.1)
time.sleep(0.1)
if method is None or properties.message_id != message_id or body != b'academy-synthetic-effect':
raise RuntimeError('Unexpected delivery: preserve state and inspect')
if attempt == 2 and not method.redelivered:
raise RuntimeError('Expected redelivery after unacknowledged close')
with db:
db.execute('INSERT OR IGNORE INTO effect VALUES (?,?)',(message_id,body))
stored = db.execute('SELECT body FROM effect WHERE message_id=?',(message_id,)).fetchone()
if stored[0] != body:
raise RuntimeError('Message-ID collision with different effect')
if attempt == 1:
print('PASS: synthetic effect committed; closing without ACK')
else:
ch.basic_ack(delivery_tag=method.delivery_tag)
print('PASS: redelivery deduplicated, then ACK sent')
conn.close()
assert db.execute('SELECT count(*) FROM effect').fetchone()[0] == 1
db.close()
print('PASS: exactly one synthetic ledger row; not an exactly-once service claim')
قدم بعدی
مثال را یک بار و با مهلت زمانی اجرا کنید
اجرا یک صف quorum تازه و یک فایل SQLite برای همین آزمایش میسازد. در پایان عادی باید نتیجهٔ پیام بدون مسیر، تأیید انتشار، ثبت نتیجه پیش از ACK، تحویل دوباره و وجود تنها یک ردیف را ببینید.
اگر زمان تمام شد یا اتصال قطع شد، بدون بررسی دوباره اجرا نکنید. ممکن است broker پیام را پذیرفته باشد. خروج۱۲۴ فقط میگوید مهلت بیرونی تمام شده؛ دربارهٔ رسیدن یا نرسیدن پیام حکم نمیدهد. صف، فایل و شناسه را برای بررسی نگه دارید.
کلاینت آزمایشی با Pika نسخهٔ ۱٫۳ یا بالاترِ نصبشده
timeout 60s python3 '/home/academy/rabbitmq-probe-20261028/confirm-ack-probe.py'همهٔ پیامهای PASS، از جمله تحویل دوباره و وجود یک ردیف در SQLite، دیده میشوند.
قدم بعدی
وضعیت همان صف را از broker بخوانید
درخواست سه عضو هنگام declaration فقط درخواست است، نه شاهد آنلاینبودن سه replica. روی گرهٔ آزمایشی اعضای واقعی همین صف و وضعیت آنها را ببینید و مطمئن شوید اکثریت لازم در دسترس است.
پس از بستهشدن مصرفکننده، تعداد پیام آماده و پیام تحویلشدهٔ بیACK باید روی صفر قرار بگیرد. برای اطمینان از پایان مصرف، شمارندههای خود RabbitMQ را هم بخوانید؛ فقط چاپ پیام در کلاینت یا ارسال ACK کافی نیست. اگر شمارنده هنوز تغییر میکند، پیش از نتیجهگیری برای تثبیت وضعیت صبر و دوباره بررسی کنید.
گرهٔ کلاستر سهعضوی RabbitMQ آزمایشی
rabbitmq-queues quorum_status --vhost 'academy_lab' 'academy_ack_probe_20261028'
rabbitmqctl list_queues -p 'academy_lab' name type messages_ready messages_unacknowledged consumersاعضای واقعی صف ثبت شدهاند و شمارندهٔ پیام آماده و بیACK پس از پایان مصرف، صفر است.
تأیید نرسیده یعنی نتیجه نامعلوم است
وقتی نوشتن روی socket انجام شده ولی پاسخ نرسیده، نه میتوانید دریافت را قطعی بدانید و نه پیام را قطعاً گمشده فرض کنید. برنامهٔ فرستنده باید کار را در outbox در انتظار نگه دارد، نتیجهٔ confirm یا return را رسیدگی کند و اگر سیاست اجازهٔ تکرار میدهد، همان شناسهٔ منطقی را حفظ کند.
RabbitMQ پیام معمولی را صرفاً به خاطر یکسانبودن message_id حذف تکرار نمیکند. مصرفکننده باید نشانگر انجام کار و خودِ نتیجه را در یک تراکنش مشترک و ماندگار ثبت کند. اگر کار، اثر بیرونی مثل پرداخت دارد، سرویس مقصد هم به سازوکار جلوگیری از تکرار نیاز دارد.
خطا را مشخص کنید؛ حفاظت را برای عبور از آن کم نکنید
برای خطای TLS، CA، نام گواهی و ساعت را بررسی کنید؛ insecure کردن اتصال راه تشخیص نیست. PRECONDITION_FAILED ممکن است نشان دهد نام صف از قبل با تنظیم دیگری وجود دارد؛ آن صف را حفظ کنید. کمبود منابع یا نبودن اکثریت اعضا هم میتواند انتشار را معطل یا ناموفق کند.
ACK باید با delivery tag همان کانالی فرستاده شود که پیام را تحویل داده است. پیام مشکلدار نیز به تعداد تلاش محدود و سیاست روشن برای کنارگذاشتن و بررسی نیاز دارد؛ برگرداندن بیپایان آن به صف میتواند یک حلقهٔ پرهزینه بسازد.
نتیجه را ثبت کنید و صف را برای تمیزشدن نمودار خالی نکنید
در این تمرین گرهای خاموش نشده و قطع شبکهای شبیهسازی نشده است؛ نتیجه را بهعنوان تأیید failover گزارش نکنید. عضویت صف، شناسهٔ پیام، خروجی، هش SQLite و شمارندههای پایان را طبق تصمیم مسئول محیط آزمایش نگه دارید.
پس از بازبینی، پاکسازی فقط باید همان صف و پوشهٔ تازه را هدف بگیرد. این مقاله دستور purge عمومی یا حذف بازگشتی یک مسیر حدسی نمیدهد.
پشتوانهٔ این آموزش
منابع رسمی
وضعیت بررسی هر محیط بالای صفحه مشخص است؛ بررسی مستندات به معنی اجرای عملی همهٔ نسخهها نیست.
- RabbitMQ: صفهای quorum
- RabbitMQ: تأیید انتشار و تأیید پردازش
- RabbitMQ: ابزار مدیریت صف
- Pika: آداپتور همگام Blocking
- RabbitMQ: قابلیت اطمینان
- سازگاری و مسیر نصب RabbitMQ / Debian, Ubuntu
- سازگاری و مسیر نصب RabbitMQ / RPM
- Python: محیط مجزای اجرای برنامه
- Red Hat: بررسی خطاهای دسترسی SELinux
- Red Hat: بررسی و مدیریت firewalld
- Red Hat: نسخههای Python و محیط اجرای برنامه
- Red Hat: نسخههای Python و محیط اجرای برنامه