GNL 0.17: erken aşamada ve hızlı ilerliyor — API'ler sürümden sürüme değişebilirBize katıl

GNL
← Blog

Bir Postgres'e bir milyon ajan çalıştırması koyup her ekranı ölçtük

GNL ekibiperformancepostgresmeasurementgdpr

Mesaj göndermek 20 ms civarında kaldı. Bir kişiyi silmek 90 saniye, onay kutusu 23 saniye sürdü; yeni bir thread depodaki bütün çalıştırmaları okudu. Yavaş durumların hepsinin sebebi aynıydı, çözümün türü de.

Sonuç#

Tek bir Postgres'i bir milyon çalıştırmayla doldurduk ve bir uygulamanın ya da Studio'nun yaptığı her işlemi ölçtük. Çoğu zaten sabitti. Şunlar değildi:

İşlemÖnceSonra
Bir kişiyi silmek (yaklaşık 500 çalıştırma)90 sn, 425.966 sorgu7–16 sn, 24.630 sorgu
Bir kişinin çalıştırmalarını listelemek (6 çalıştırması olan biri)16 sn1 ms
Bir thread'i çalıştırmalarıyla birlikte silmek15 sn45 ms
Studio onay kutusu (bekleyen 167 çalıştırma)23,3 sn, 20.335 ifade75 ms, 335 ifade
Kesin kullanım toplamı, tek organizasyon17,0 sn10 ms
Studio canlı güncelleme, 2 saniyelik bir tur22,8 sn; tek açık sekmede veritabanı %73 meşgul1–3 ms; veritabanı %0,07–0,17 meşgul
Yeni bir thread'de ilk çalıştırma (30.000 çalıştırma)70 sn14 ms
Bütün çalıştırma listesini okumak (30.000 çalıştırma)71 sn432 ms
Thread kayıtlarını yazan yükseltme adımı (1M çalıştırma)1,4 GB bellek+97 MB

İki satır bir milyonda değil, 30.000 çalıştırmada ölçüldü: eski kod o boyutta bile bir dakikayı geçiyordu.

Mesaj göndermek değişmedi: önce de sonra da yaklaşık 20 ms ve 90–96 ifade. Düzeltmeler okuma yolundaki işi kaldırdı, yazma yoluna iş eklemedi.

Nasıl ölçtük#

Depo bir Postgres 16'ydı: 1.000.005 çalıştırma, 8,2 milyon günlük satırı, 200.000 thread ve 2 milyon mesaj, toplam 5,3 GB. Gerçek, beş çalıştırmalık bir thread'i SQL ile bir milyona ulaşana kadar kopyalayarak kurduk.

Sonra her işlemi birkaç kez çalıştırıp iki sayı kaydettik: süre ve kaç ifade gönderdiği. Sayı, süre kadar önemli. Her çalıştırma için bir sorgu gönderen bir işlem, test veritabanında hızlı, gerçek veritabanında yavaştır. Bunu önce sayı gösterir.

Listenin çoğu zaten düzdü: thread oluşturmak 1 sorgu, thread'leri listelemek 1,6 ms, bir thread'in mesajlarını okumak 0,5 ms, bir çalıştırmayı okumak 0,3 ms, bir Studio sayfası 0,8 ms. Onlar bu yazıda yok.

Yeni bir thread depodaki bütün çalıştırmaları okuyordu#

Belirti. Yeni bir thread'deki ilk çalıştırma, depo büyüdükçe yavaşladı: 1.000 çalıştırmada 72 ms, 10.000'de 7,3 sn, 30.000'de 70 sn. Thread'siz bir çalıştırma birkaç milisaniye sürüyordu.

Sebep. Bir çalıştırma bir thread'e katılmadan önce GNL thread'in kime ait olduğuna bakar; böylece kimse başkasının thread'ine yazamaz. Cevap thread'in sahip kaydından gelir. Yeni bir thread'in henüz kaydı yoktur. Bu yüzden kontrol, o thread'de daha önce çalıştırma var mı diye bütün çalıştırma listesini sayfa sayfa geziyordu. Giriş yapmamış biriyle başlayan bir thread'in hiç kaydı olmuyordu; yani GNL o thread'in varlığını da bu gezintiyle öğreniyordu. Gezintinin kendisi de karesel büyüyordu: sayfalar LIMIT … OFFSET n ile okunuyordu ve Postgres atlanan satırların hepsini önce oluşturuyordu. 15.000. satırdaki sayfa 132 ms, ilk sayfa 3 ms sürüyordu.

Çözüm. Kullanıcısız yeni bir thread artık küçük bir işaret yazar. Eski thread'lerin hepsi kaydını ya da işaretini alınca depo kendini tamam diye işaretler. Tamam olan bir depoda kayıt da işaret de yoksa thread yenidir: iki nokta okuması. Depo tamam olana kadar eski gezinti yerinde kalır; yani doğru cevap hiçbir zaman yükseltmeye bağlı değildir.

Çalıştırma listeleri de satır saymayı bıraktı. Bir sayfa artık, verdiği son çalıştırmanın zamanından ve kimliğinden hemen sonra başlar; zaten var olan indeksle. 30.000 çalıştırmada yeni thread'deki ilk çalıştırma: 70 sn → 14 ms. 30.000 çalıştırmanın hepsini okumak: 71 sn → 432 ms.

Satır numarasıyla sayfalama ikinci bir hatayı da gizliyordu. İki sayfa arasında silinen bir çalıştırma, sonraki bütün satırları bir geri kaydırıyordu; sonraki sayfa da bir çalıştırmayı atlıyordu. Yeni imleç bunu her depoda düzeltti.

Bir kişiyi silmek herkesi okuyordu#

Belirti. Yaklaşık 500 çalıştırması olan bir kişiyi silmek 425.966 sorgu gönderdi ve 90 saniye sürdü. Bu sorguların 404.674'ü başka insanların thread sahip kayıtlarını okuyordu.

Sebep. Bir çalıştırmanın kişisi ve thread'i yalnızca çalıştırmanın girdi kaydının içinde duruyordu. Silme, kişinin çalıştırmalarını bulmak için bütün çalıştırmaları geziyordu. Sahip olduğu thread'leri bulmak için de depodaki her thread'in sahip kaydını, 200.000 tanesini, tek tek açıyordu.

İlk çözüm. Kişi başına bir dizin: kişinin sahip olduğu her çalıştırma ve thread için bir anahtar, gösterdiği kayıttan önce yazılır. Bir silme: 24.630 sorgu, 7–16 sn; çoğu silmelerin kendisi.

Dikkat
Açık. Sonra bir denetim, dizinin neyi kaçırdığını buldu. Dizini motor yazıyordu; ama hâlâ önceki sürümde çalışan bir süreç ya da başka bir yazıcı yazmıyordu. Onların çalıştırmaları dizinde görünmüyordu, silme de onları geride bırakıyordu. Hızlıydı ama silmenin tek vaadini bozuyordu: kişiden geriye hiçbir şey kalmaz.

Son çözüm. Dizini kaldırdık. Postgres ve Sqlite artık bir çalıştırmanın kişisini ve thread'ini satırında sütun olarak tutuyor; depo bunları kayıtla aynı işlemde dolduruyor. Sütunları boş olan bir satır her seferinde kaydından okunur ve kaydına göre karar verilir. Sütunları bilmeyen bir yazıcı bir kaydı yeniden yazarsa, bir veritabanı tetikleyicisi sütunları boşa döndürür; o satır yine kaydından karara bağlanır.

1.000.020 çalıştırmalık bir kopyada bir silme 5,2 sn ve 21.112 ifade sürdü; dizinin süresiyle aşağı yukarı aynı, ama açık yok. Aynı sütunlar iki ekranı daha ucuzlattı: bir kişinin çalıştırmaları 16 sn → 1 ms, bir thread'i çalıştırmalarıyla silmek 15 sn → 45 ms.

Studio ekranları birkaç satır göstermek için bir milyon satır okuyordu#

Belirti. Onay kutusu bekleyen 167 çalıştırma gösteriyordu. Bunları bulmak için bütün çalıştırma listesini okuyup süzüyordu: 23,3 sn, 20.335 ifade. Kesin kullanım toplamı da her çalıştırmayı okuyordu: tek organizasyon için 17 sn. En kötüsü canlı güncellemeydi. Bir organizasyon var olduğu sürece her 2 saniyelik tur 22,8 sn süren tam bir tarama yapıyordu. Tek Studio sekmesi açıkken veritabanı %73, üç sekmeyle %94 meşguldü.

Sebep. Üçü de aynı soruyu soruyordu: hangi çalıştırmalar açık ya da değişti? Cevap çalıştırma satırındaydı ama onu kapsayan bir indeks yoktu. Cevaplamanın tek yolu her satırı okumaktı.

Çözüm. Kısmi bir indeks yalnızca tamamlanmamış çalıştırmaları tutar. Burada bu, bir milyonda 167 satırdı. Onay kutusu onu okur: 75 ms. Kesin toplam açık çalıştırmaları ve indeksli tek bir sayımı okur: bir organizasyon için 10 ms, bütün depo için 72 ms; toplamlar öncekiyle aynı. Canlı güncelleme son turdan beri değişen çalıştırmaları, güncellenme zamanı indeksinden ister: tur başına 1–3 ms, veritabanı %0,07–0,17 meşgul; bir ya da üç sekmeyle, boştayken de saniyede 20 yazmada da.

Önce daha ucuz bir toplam denedik: kullanım sayacı artı açık çalıştırmalar. Farklı bir sayı verdi; çünkü sayaç, kullanım kaydetmeden biten çalıştırmaları kaçırıyor, silinenleri de tutuyor. Bu yüzden toplam kesin bir sayım olarak kaldı.

Yükseltme de aynı biçimdeydi#

Eski bir depo, yeni kontrolün dayandığı thread kayıtlarını yazmak için bir kerelik bir adıma ihtiyaç duyar. Adımın ilk sürümü her çalıştırma özetini bellekte tutuyordu: bir milyon çalıştırmada 1,4 GB. Artık her seferinde bir sayfa okuyor ve thread başına küçük bir kayıt tutuyor: bir milyon çalıştırma için +97 MB ve 141 sn.

Tek bir komut: gnl upgrade. Her adımı ve ne yaptığını listeler; hepsini ya da yalnızca adını verdiklerinizi çalıştırır. --dry-run hiçbir şey yazmaz.

Hâlâ açık olanlar#

Bunlar ölçüldü, henüz düzeltilmedi:

  • Bir mesaj Postgres'te yaklaşık 89 ifade tutuyor. Ağ üzerindeki bir veritabanında her biri bir gidiş-dönüş demek. Nereye gittiklerini saydık; bazıları birleştirilebilir. Henüz yapmadık.
  • Bellek her turda bütün thread'i okuyor, sonra yalnızca son birkaç mesajı tutuyor. Bir mesaj 10 mesajlık thread'de 25,4 ms, 1.000'de 32,0 ms, 10.000'de 57,4 ms sürdü.
  • Eski bir depo ancak yükseltmeden sonra hızlı. Bir milyon eski çalıştırmayı yeni sütunlara yerleştirmek 5,8 dakika sürdü. O zamana kadar kişiye göre aramalar eski çalıştırmaları kayıtlarından okumaya devam ediyor.
  • Canlı güncelleme silinen bir çalıştırmayı duyurmuyor. Bir sonraki liste çekimi onu düşürüyor.

Bundan ne çıkardık#

Yavaş ekranların hepsi aynı biçimdeydi: birkaç çalıştırma hakkında bir soru, hepsi okunarak cevaplanıyordu. Küçük bir test veritabanı bunu gizler. 30.000 çalıştırmada 70 sn süren yeni thread kontrolü, 1.000 çalıştırmada 72 ms sürüyordu.

O yüzden ifadeleri sayın, çalışmayı beklediğiniz boyutta. Bir şey yavaşsa gezintiyi hızlandırmayın. Cevabı bir indeksin ulaşabileceği yere koyun, veriyle aynı işlemde yazın ve yeni yolun kefil olamadığı veri için yavaş yolu yerinde bırakın.

İpucu
Silmenin nasıl çalıştığı ve neyi kaldırdığı: GDPR silme. GNL'nin hangi depolarda çalıştığı: depo adaptörleri.