Redis Pipelining للمتوسط: من 12K لـ 290K SET/ثانية بسطر بايثون واحد
مستوى المقال: متوسط — يفترض إنك بتعرف Redis كـ key-value store، شغّلت SET و GET قبل كده، ومتأكد إن خدمتك بتعمل آلاف الطلبات على Redis في الدقيقة. مدة القراءة: 8 دقائق.
لو خدمتك بتعمل 12,400 SET/ثانية على Redis، ولاحظت إن CPU الـ client اللي شغّال عليه التطبيق بياكل 92% بينما Redis نفسه على 6% CPU، المشكلة مش في Redis ولا في الشبكة. هي في عدد الـ round-trips. Pipelining في سطر واحد بيرفع الرقم لحوالي 290,000 SET/ثانية على نفس السيرفر، نفس الشبكة، نفس الكود تقريبًا.
المشكلة باختصار
كل أمر Redis بياخد رحلة كاملة على الشبكة: طلب من التطبيق، انتظار، رد من السيرفر. لو الـ RTT بين التطبيق و Redis 0.4ms (وده رقم واقعي داخل نفس الـ AZ على AWS)، الـ throughput الأقصى النظري لـ thread واحد = 1 / 0.0004 = 2,500 أمر/ثانية. علشان توصل 12,000 أمر/ثانية محتاج 5 threads بتعمل blocking I/O. الـ CPU بيتحرق في انتظار، مش في شغل.
مثال للمبتدئ — موظف الدليفري
تخيّل صيدلية فيها موظف دليفري واحد. كل طلب من زبون، الموظف بيمشي من الدور التاني، يجيب الدوا، ينزل، يسلّمه، يستنى اللي بعده. الزبون الواحد بياخد 4 دقايق. لو في 60 زبون، يبقى 240 دقيقة. بس لو نفس الموظف خد قايمة فيها 60 طلب، طلع مرة واحدة، نزل بـ 60 دوا، ووزّعهم في 8 دقايق، الفرق 30x. ده Pipelining بالظبط: بدل ما ترسل أمر وتستنى الرد قبل التاني، ترسل 1000 أمر دفعة واحدة، وتستنى الردود في trip واحد على الشبكة.
التعريف العلمي
طبقًا لـ توثيق Redis 7.4 الرسمي، الـ pipelining هو "إرسال عدة commands دفعة واحدة بدون انتظار الـ reply لكل واحد، ثم قراءة كل الـ replies في النهاية". الـ pipeline مش transaction (لا atomicity، ولا rollback) — هو optimization على طبقة الشبكة فقط. الـ commands بتتنفّذ بنفس الترتيب اللي اتبعت بيه، وكل واحد بيدخل في الـ event loop الخاص بـ Redis زي ما كان لو اتبعت لوحده.
الفايدة بتيجي من حاجتين:
- تجميع الـ TCP packets: kernel بيبعث أقل عدد من الـ segments، فبتختفي تكلفة الـ Nagle/ack chatter بين الـ application و الـ Redis server.
- إلغاء الـ context switches: الـ kernel مش بيوقف الـ process على كل
recv()لأن الـ replies بتتقري في batch واحد من الـ socket buffer.
الكود — قبل وبعد
الكود ده اتـ benchmark على Python 3.12 و redis-py 5.0.8، Redis 7.4 على instance r6i.large في نفس الـ VPC.
# قبل: SET واحد في كل round-trip
import redis, time
r = redis.Redis(host="127.0.0.1", port=6379)
start = time.perf_counter()
for i in range(100_000):
r.set(f"user:{i}", "active")
elapsed = time.perf_counter() - start
print(f"بدون pipeline: {100_000/elapsed:,.0f} SET/ثانية")
# بدون pipeline: 12,400 SET/ثانية