Redis pipelining, istemcinin birden fazla komutu her birinin yanıtını beklemeden art arda sunucuya göndermesi, yanıtları ise sonradan toplu olarak okuması tekniğidir. Böylece her komut için ayrı ayrı ödenen ağ gidiş dönüş süresi (RTT) bir kez ödenir; sunucu tarafında da okuma ve yazma sistem çağrılarının sayısı azaldığı için saniyede işlenen komut sayısı belirgin biçimde artar. Pipelining Redis'e özel bir komut değildir, istemcinin protokolü kullanış biçimidir ve Redis bu davranışı ilk sürümlerinden beri destekler.
Bu yazıda istek/yanıt modelinin neden yavaşladığını, pipelining'in bunu nasıl çözdüğünü, bellek tarafındaki bedelini, transaction ve Lua betikleriyle farkını, Python ve Node.js istemcilerinde nasıl kullanıldığını ve büyük veri setlerini redis-cli --pipe ile nasıl yükleyeceğinizi resmî belgelere dayanarak anlatıyoruz. Redis'in ne olduğunu ve kurulumunu Redis nedir yazımızda bulabilirsiniz; serinin tamamı için Redis öğrenme rehberi sayfasına bakın.
İstek/yanıt modeli ve RTT sorunu

Redis, istemci-sunucu modelinde çalışan bir TCP sunucusudur ve istek/yanıt (request/response) protokolü kullanır. Olağan akışta istemci bir komut gönderir, ardından soketten yanıtı okumak için genellikle bekler; sunucu komutu işler ve yanıtı geri yollar. Dört INCR komutu art arda gönderildiğinde akış şöyledir:
İstemci: INCR X
Sunucu: 1
İstemci: INCR X
Sunucu: 2
İstemci: INCR X
Sunucu: 3
İstemci: INCR X
Sunucu: 4Her satır çifti arasında paketlerin istemciden sunucuya gidip yanıtın geri gelmesi gerekir. Bu süreye RTT (Round Trip Time) denir. Bağlantı aynı makinedeki loopback arabirimi üzerinden kurulmuşsa RTT genellikle milisaniyenin altındadır; internet üzerinden, arada çok sayıda ağ düğümü bulunan bir bağlantıda ise çok daha uzundur. Belgelerdeki örnek bu farkı çarpıcı biçimde gösterir: RTT 250 milisaniye olan yavaş bir bağlantıda, sunucu saniyede yüz bin isteği işleyebilecek güçte olsa bile istemci saniyede en fazla dört istek tamamlayabilir. Çünkü darboğaz sunucunun hızı değil, her komutun ardından beklenen ağ gecikmesidir.
Loopback arabiriminde gecikme küçük olsa da, binlerce yazma işlemini tek tek yapan bir uygulamada bu küçük süreler toplanır. Bir listeye çok sayıda öğe eklemek ya da veritabanını çok sayıda anahtarla doldurmak bu sorunun en sık görüldüğü iki durumdur.
Pipelining nasıl çalışır?
Bir istek/yanıt sunucusu, istemci eski yanıtları henüz okumamış olsa bile yeni istekleri işleyebilecek biçimde tasarlanabilir. Redis böyle çalışır: istemci birden fazla komutu yanıt beklemeden gönderir, sunucu bunları sırayla işler ve yanıtları aynı sırayla geri yazar, istemci de en sonda yanıtların hepsini okur. Belgelerdeki en yalın örnek, ham netcat aracıyla üç PING komutunu tek seferde göndermektir:
$ (printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONGBurada RTT bedeli üç komut için yalnızca bir kez ödenir. Aynı dört INCR örneği pipelining ile gönderildiğinde akış da değişir: önce dört komut peş peşe sunucuya gider, ardından dört yanıt peş peşe döner.
İstemci: INCR X
İstemci: INCR X
İstemci: INCR X
İstemci: INCR X
Sunucu: 1
Sunucu: 2
Sunucu: 3
Sunucu: 4Önemli bir ayrıntı: yanıtların sırası, komutların gönderiliş sırasıyla aynıdır. İstemci kütüphaneleri bu sayede her yanıtı doğru komutla eşleştirebilir ve size genellikle komut sırasına göre dizilmiş bir sonuç listesi verir.
Bellek bedeli: yanıtlar sunucuda kuyruğa alınır
Pipelining bedava değildir. İstemci yanıtları okumadan komut göndermeye devam ettikçe sunucu bu yanıtları bellekte bekletmek zorunda kalır. Belgeler bu yüzden çok sayıda komutu tek bir dev pipeline yerine makul büyüklükte gruplar halinde göndermeyi önerir; örnek olarak verilen değer 10 bin komuttur. Önce 10 bin komut gönderilir, yanıtlar okunur, ardından sonraki 10 bin komuta geçilir. Belgelere göre bu yöntemle hız neredeyse aynı kalır, buna karşılık ek bellek kullanımı en fazla bu 10 bin komutun yanıtlarını tutacak kadar olur.
Kazanç yalnızca RTT değildir: sistem çağrıları
Pipelining'in faydası ağ gecikmesini azaltmakla sınırlı kalmaz; aynı sunucunun saniyede işleyebildiği işlem sayısını da büyük ölçüde artırır. Nedeni şudur: pipelining olmadan her komutu sunmak, veri yapısına erişmek ve yanıtı üretmek açısından çok ucuzdur, fakat soket giriş çıkışı açısından pahalıdır. Her komut için read() ve write() sistem çağrıları yapılır; bu da kullanıcı alanından çekirdek alanına geçiş, yani bağlam değiştirme demektir ve belgelerin ifadesiyle ciddi bir hız cezasıdır.
Pipelining kullanıldığında çok sayıda komut genellikle tek bir read() çağrısıyla okunur ve çok sayıda yanıt tek bir write() çağrısıyla gönderilir. Belgelerdeki grafiğe göre saniyedeki toplam sorgu sayısı pipeline uzadıkça başta neredeyse doğrusal biçimde artar ve sonunda pipelining kullanılmayan durumun yaklaşık on katına ulaşır. Belgedeki Ruby örneğinde, loopback üzerinde 10 bin PING komutu pipelining olmadan yaklaşık 1,19 saniye, pipelining ile yaklaşık 0,25 saniye sürmüş ve yazar bunu beş katlık bir iyileşme olarak aktarmıştır. Bu değerler belgedeki tek bir makinede alınmış ölçümlerdir; kendi ortamınızda farklı sonuçlar görmeniz doğaldır, ancak yönün aynı olmasını bekleyebilirsiniz.
Pipeline, transaction ve Lua betiği arasındaki fark

Pipelining sık sık MULTI/EXEC ile karıştırılır. İkisi de birden fazla komutu toplu gönderir, ama verdikleri garanti farklıdır. Pipeline yalnızca bir iletim tekniğidir: komutlar gönderiliş sırasıyla çalışır, fakat Lua belgelerinin de vurguladığı gibi aralarına başka istemcilerin komutları girebilir. Transaction ise kuyruktaki komutları tek bir yalıtılmış işlem olarak sırayla çalıştırır; ortasında başka bir istemcinin komutu işlenmez. Lua betiği de atomik çalışır ve ek olarak sunucu tarafında okuma, hesaplama ve yazma adımlarını tek seferde yapabilir.
| Yöntem | Ne sağlar | Araya başka komut girer mi | Okuyup karar verebilir mi |
|---|---|---|---|
| Pipeline | Daha az RTT ve daha az sistem çağrısı | Evet | Hayır, yanıtlar en sonda okunur |
| MULTI/EXEC | Kuyruktaki komutların yalıtılmış ve sıralı çalışması | Hayır | Hayır, kuyruktaki komutlar yanıt döndürmez |
| Lua betiği (EVAL) | Atomik çalışma ve sunucu tarafı mantık | Hayır | Evet, betik içinde |
Belgeler bu ayrımı şöyle özetler: "oku, hesapla, yaz" türündeki işlemlerde pipelining yardımcı olamaz, çünkü istemcinin yazma komutunu gönderebilmesi için önce okuma komutunun yanıtını alması gerekir. Böyle durumlarda işi sunucuda yapan bir betik hem gecikmeyi hem de yarış koşulunu ortadan kaldırır. Ayrıntılar için MULTI, EXEC ve WATCH yazımıza ve Lua betikleri yazımıza bakın.
Pipeline içinde EVAL veya EVALSHA göndermek de mümkündür. Burada bir tuzak vardır: EVALSHA, betik sunucunun önbelleğinde yoksa NOSCRIPT hatası döndürür ve pipeline içindeki komutların arasına başka istemcilerin komutları girebildiği için bu hatayı pipeline ortasında düzgün biçimde ele almak mümkün olmaz. Belgeler bu yüzden pipeline öncesinde SCRIPT LOAD ile betiğin yüklenmesini önerir; istemci kütüphanelerinin pipeline bağlamında parametreli düz EVAL kullanmaya dönmesi gerektiğini de not eder.
İstemci kütüphanelerinde pipeline
Çoğu Redis istemcisi pipelining'i destekler ve size protokol ayrıntılarıyla uğraşmadan komutları toplayıp tek seferde gönderen bir nesne sunar. Aşağıdaki iki örnek resmî istemci sayfalarından uyarlanmıştır.
Python (redis-py)
redis-py'da önce pipeline() ile bir pipeline nesnesi oluşturulur. Bu nesnenin set(), get() gibi yöntemleri komutu hemen çalıştırmaz, arabelleğe ekler ve yine pipeline nesnesini döndürür; bu yüzden çağrıları zincirleyebilirsiniz. Komutlar ancak execute() çağrıldığında gönderilir ve bu yöntem tüm sonuçları komut sırasıyla bir liste olarak döndürür. Dikkat edilmesi gereken nokta, redis-py pipeline'ının varsayılan olarak transaction gibi çalışmasıdır; saf pipelining istiyorsanız transaction=False parametresini vermelisiniz.
import redis
r = redis.Redis(decode_responses=True)
pipe = r.pipeline(transaction=False)
pipe.set("koltuk:0", "#0").set("koltuk:1", "#1")
pipe.get("koltuk:0")
sonuclar = pipe.execute() # her komutun sonucu, gönderiliş sırasıylaBurada sonuclar listesi üç eleman içerir: iki SET komutunun ve bir GET komutunun sonuçları, bu sırayla. redis-py ile bağlantı ve diğer ayrıntılar için redis-py yazımıza bakın.
Node.js (node-redis)
node-redis, olay döngüsünün aynı "tick" içinde çalışan komutları kendiliğinden pipeline'a alır. Bunu sağlamanın en kolay yolu komutları Promise.all() içinde çağırmaktır. Belgedeki örnek ve gösterilen çıktısı:
await Promise.all([
client.set('seat:0', '#0'),
client.set('seat:1', '#1'),
client.set('seat:2', '#2'),
]).then((results) =>{
console.log(results);
// >>> ['OK', 'OK', 'OK']
});İkinci yol multi() ile bir nesne oluşturup komutları ona eklemek ve sonunda execAsPipeline() çağırmaktır. Belgelere göre iki yaklaşım neredeyse eşdeğerdir; fark bağlantı kesildiğinde ortaya çıkar. Bağlantı yeniden kurulduğunda Promise.all() ile oluşan pipeline kaldığı yerden devam eder, multi() ile oluşturulan pipeline ise çalışmamış kalan komutları atar.
await client.multi()
.set('seat:3', '#3')
.set('seat:4', '#4')
.set('seat:5', '#5')
.execAsPipeline()
.then((results) => {
console.log(results);
// >>> ['OK', 'OK', 'OK']
});Go istemcisindeki otomatik pipelining ve diğer dillerin karşılıkları için node-redis yazımıza ve go-redis yazımıza göz atabilirsiniz.
Cluster ortamında pipeline
Tek sunuculu (cluster kapalı) bir Redis'te pipeline içindeki komutlar herhangi bir anahtara dokunabilir. Redis Open Source cluster modunda ise çok anahtarlı işlemler tablosuna göre pipeline'lar tek bir hash slot ile sınırlıdır. Yani farklı slot'lara düşen anahtarları aynı pipeline'da kullanmak istiyorsanız bunları istemcinin nasıl yönlendirdiğine dikkat etmeniz ya da anahtarları hash tag ile aynı slota toplamanız gerekir. Hash slot, hash tag ve CROSSSLOT hatası Redis Cluster yazımızda anlatılıyor.
Büyük veri yükleme: redis-cli --pipe

Mevcut büyük bir veri setini Redis'e aktarmak (bulk loading) pipelining'in uç bir örneğidir. Belgeler bu iş için sıradan bir istemciyle komutları tek tek göndermenin iyi bir fikir olmadığını belirtir: her komut için RTT ödenir, pipeline kullanılsa bile en yüksek hız için yanıtları okurken aynı anda yeni komut yazmak gerekir ve bunu verimli yapabilen istemci sayısı azdır. Önerilen yol, eklenecek veriyi ham Redis protokolü biçiminde bir metin dosyasına yazmak ve bu dosyayı redis-cli aracının pipe moduyla sunucuya aktarmaktır.
cat data.txt | redis-cli --pipeBelgedeki bir milyon komutluk örnekte bu komutun çıktısı şöyledir; son satır hata sayısını ve alınan yanıt sayısını verir:
All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 1000000Eskiden aynı iş netcat ile yapılırdı, fakat netcat verinin ne zaman tamamen aktarıldığını bilemez ve hataları denetleyemez. Pipe modu bu iki eksikliği giderir; ayrıca redis-cli yalnızca sunucudan gelen hataları standart çıktıya yönlendirir. Yardım metninde --pipe-timeout seçeneği de yer alır: tüm veri gönderildikten sonra belirtilen saniye içinde yanıt gelmezse işlem hatayla sonlanır. Varsayılan değer 30 saniyedir, 0 vermek sonsuza kadar beklemek anlamına gelir.
Redis protokolünü üretmek
Dosyaya yazacağınız biçim çok basittir. Her komut, argüman sayısını belirten * satırıyla başlar; ardından her argüman için bayt cinsinden uzunluğu veren bir $ satırı ve argümanın kendisi gelir. Satırlar \r\n ile biter. Örneğin SET key value komutu "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n" olarak yazılır. Belgeler bu işi yapan bir Ruby fonksiyonu verir; aynı mantığı Python ile şöyle kurabilirsiniz:
def resp(*parcalar):
cikti = "*%d\r\n" % len(parcalar)
for parca in parcalar:
metin = str(parca)
cikti += "$%d\r\n%s\r\n" % (len(metin.encode("utf-8")), metin)
return cikti
print(repr(resp("SET", "mykey", "Hello World!")))
# '*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$12\r\nHello World!\r\n'
print(repr(resp("SET", "şehir", "İzmir")))
# '*3\r\n$3\r\nSET\r\n$6\r\nşehir\r\n$6\r\nİzmir\r\n'İkinci satırdaki sonuç küçük ama önemli bir ayrıntıyı gösterir: $ satırındaki uzunluk karakter sayısı değil bayt sayısıdır. "şehir" beş karakterdir, ancak UTF-8 ile altı bayt tutar; uzunluğu karakter olarak yazarsanız sunucu argümanı yanlış keser. Bu yüzden fonksiyon uzunluğu encode("utf-8") sonrası hesaplıyor. Belgedeki örnekle aynı 1000 anahtarı (Key0 ile Value0'dan Key999 ile Value999'a kadar) üreten betik ve aktarım komutu şöyledir:
import sys
def resp(*parcalar):
cikti = "*%d\r\n" % len(parcalar)
for parca in parcalar:
metin = str(parca)
cikti += "$%d\r\n%s\r\n" % (len(metin.encode("utf-8")), metin)
return cikti
for n in range(1000):
sys.stdout.buffer.write(resp("SET", "Key%d" % n, "Value%d" % n).encode("utf-8"))$ python proto.py | redis-cli --pipeÇıktıyı sys.stdout.buffer üzerinden bayt olarak yazmak, bazı işletim sistemlerinde metin modunun satır sonlarını değiştirmesini önler. Belgedeki 1000 komutluk örnekte redis-cli çıktısı errors: 0, replies: 1000 satırıyla biter; yanıt sayısı gönderdiğiniz komut sayısıyla aynı olmalıdır.
Pipe modu arka planda ne yapar?
redis-cli --pipe veriyi olabildiğince hızlı gönderirken aynı anda gelen yanıtları okuyup ayrıştırır. Standart girdide okunacak veri kalmadığında 20 baytlık rastgele bir dizeyle özel bir ECHO komutu gönderir. Bunun gönderilen son komut olduğundan emin olduğu için, aynı 20 baytı içeren yanıt geldiğinde aktarımın bittiğini anlar ve başarıyla çıkar. Böylece gönderdiği protokolü ayrıştırmasına gerek kalmaz; yalnızca yanıtları sayarak kaç komutun aktarıldığını raporlar. Diğer redis-cli seçenekleri için redis-cli kullanımı yazımıza bakın.
Kıyaslama yaparken düşülen tuzak: meşgul döngü
Belgelerin ek bölümü sık yapılan bir hatayı açıklar: aynı makinede, bir saniye boyunca döngü içinde art arda SET çağıran bir kıyaslama neden loopback üzerinde bile yavaştır? Çünkü süreçler sürekli çalışmaz, onları çekirdek zamanlayıcısı çalıştırır. Kıyaslama programı yanıtı okuyup yeni komutu yazdığında komut loopback arabelleğine düşer; sunucunun onu okuyabilmesi için zamanlayıcının sunucu sürecini çalıştırması gerekir ve bu döngü her komutta tekrarlanır. Sonuçta loopback da ağa benzer bir gecikme içerir. Belgeler bu tür meşgul döngü kıyaslamalarından kaçınmayı önerir; performans ölçecekseniz pipelining'in etkisini de hesaba katan, gerçek iş yükünüze benzeyen bir test kurgulayın.
Sık yapılan hatalar
- Pipeline'ı transaction sanmak: pipeline atomiklik vermez, araya başka istemcilerin komutları girebilir. Yalıtım gerekiyorsa MULTI/EXEC ya da betik kullanın.
- Her şeyi tek pipeline'a koymak: yanıtlar sunucuda bekler ve bellek tüketir. Belgelerdeki örnek gibi binlerle ifade edilen gruplar halinde gönderin.
- Bir yanıta bağlı kararı pipeline içinde vermeye çalışmak: okumanın sonucunu bilmeden yazma yapılamaz; bu iş için Lua betiği ya da WATCH kullanın.
- Pipeline içinde önceden yüklenmemiş bir betik için EVALSHA göndermek: NOSCRIPT hatası pipeline ortasında ele alınamaz. Önce SCRIPT LOAD ile yükleyin.
- redis-py'da pipeline'ın varsayılan olarak transaction olduğunu unutmak: saf pipelining için transaction=False verin.
- Protokol üretirken uzunluğu karakter olarak hesaplamak: $ satırı bayt sayısını ister; Türkçe karakterlerde fark ortaya çıkar.
Sık Sorulan Sorular
Pipelining için Redis'in belirli bir sürümü gerekir mi?
Hayır. Belgelere göre Redis pipelining'i ilk günlerinden beri destekler, bu yüzden hangi sürümü çalıştırırsanız çalıştırın kullanabilirsiniz. Gereken tek şey istemci kütüphanenizin bu özelliği sunmasıdır; çoğu istemci sunar.
Pipeline içindeki bir komut hata verirse diğerleri çalışır mı?
Pipeline, komutları sunucuya tek tek göndermenin hızlandırılmış hâlidir; her komut kendi başına çalışır ve kendi yanıtını döndürür. Hatalı bir komut yalnızca kendi yerinde hata yanıtı üretir. İstemci kütüphanenizin bu hatayı nasıl sunduğuna (istisna mı, listede hata nesnesi mi) kendi belgesinden bakın.
Pipeline mi kullanmalıyım, Lua betiği mi?
Birbirinden bağımsız çok sayıda komutu hızlıca göndermek istiyorsanız pipeline yeterlidir. Bir değeri okuyup ona göre yazmanız gerekiyorsa, ya da işlemin araya başka komut girmeden atomik çalışması şartsa Lua betiği veya Redis Functions daha uygundur.
Toplu yükleme için en hızlı yöntem hangisi?
Belgelerin önerdiği yöntem, veriyi Redis protokolü biçiminde bir dosyaya üretmek ve redis-cli --pipe ile aktarmaktır. Bu mod hem hızlıdır hem de sonunda hata ve yanıt sayısını raporlar.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: Redis öğrenme rehberi · MULTI, EXEC ve WATCH · Redis Lua betikleri · redis-cli kullanımı · Redis Cluster ve hash slot · redis-py kullanımı