Yazar Liu Zhiyong Compile
Düzenle Xiaozhi
Python çöp toplama (Çöp Toplama, GC) mekanizmasını kapatarak (kullanılmayan verileri geri alarak ve serbest bırakarak belleği geri alma), Instagram'ın performansı% 10 artırılabilir. Evet, doğru duydunuz! GC'yi devre dışı bırakarak, bellek ayak izini azaltabilir ve CPU LLC önbellek isabet oranını artırabiliriz. Nedenini öğrenmek istiyorsanız, gelin ve Chenyang Wu ve Min Ni tarafından yazılan makaleyi okuyun.
Yazar Chenyang Wu, Instagram'da bir yazılım mühendisi ve Min Ni, Instagram'da bir teknik yöneticidir.
Web sunucusunu nasıl yönetiriz
Instagram'ın web sunucusu çok işlemli bir modda Django üzerinde çalışır. Ana işlem, gelen kullanıcı isteklerini almak için düzinelerce çalışan işlem oluşturmaya çatallar. Uygulama sunucusu için, ana işlem ile çalışan süreç arasındaki bellek paylaşımından yararlanmak için ön uç modu ile uWSGI kullanıyoruz.
Django sunucusunun OOM'a çalışmasını önlemek için, ana uWSGI işlemi, RSS belleği eşiği aştığında çalışan işlemi yeniden başlatmak için bir mekanizma sağlar.
Hafızayı anlamak
Ana süreç tarafından oluşturulduktan sonra RSS belleğinin neden hızla büyüdüğünü incelemeye başladık. Bir gözlem, RSS belleği 250MB'de başlasa bile, paylaşılan hafızasının çok hızlı bir şekilde düştüğüdür: birkaç saniye içinde 250MB'den yaklaşık 140MB'ye (paylaşılan belleğin boyutu / proc / PID / smaps'den okunabilir). Buradaki sayılar sıkıcı çünkü sürekli değişiyorlar, ancak paylaşılan bellek atmalarının ölçeği ilginç: toplam belleğin yaklaşık 1 / 3'ü. Daha sonra, paylaşılan hafızanın neden işçi neslinin başlangıcında her sürecin özel hafızası haline geldiğini anlamak istiyoruz.
Teorimiz: kopya okuma
Linux çekirdeği, fork sürecini optimize etmek için kullanılan Copy-on-Write (CoW) adlı bir mekanizmaya sahiptir. Alt süreç, her bellek sayfasını kendi üst süreciyle paylaşarak başlar. Yalnızca sayfa yazılırken alt bellek alanına kopyalanan bir sayfa (ayrıntılar için Wikipedia'daki Copy_on_Write girişine bakın).
Ancak Python'da, referans sayımı nedeniyle işler ilginçleşir. Bir Python nesnesini her okuduğumuzda, yorumlayıcı referans sayısını artıracaktır, bu da esasen temeldeki veri yapısına bir yazmadır. Bu, CoW'a yol açtı. Bu nedenle, Python ile, Okumaya Kopyala (CoR) yapıyoruz!
O halde soru şudur: Yazarken değişmez nesneleri (kod nesneleri gibi) kopyalıyor muyuz? PyCodeObject'in gerçekten de PyObject'in bir "alt sınıfı" olduğu göz önüne alındığında, cevap açıktır: evet. İlk düşüncemiz PyCodeObject için referans sayımını devre dışı bırakmaktır.
Deneme 1: Kod nesnelerinin referans sayımını devre dışı bırakın
Instagram'da önce basit şeyler yapıyoruz. Bunun bir deney olduğunu düşünerek, CPython yorumlayıcısında bazı küçük değişiklikler yaptık, referans sayısının kod nesnesini değiştirmediğini doğruladık ve ardından CPython'u üretim sunucularımızdan birine uyguladık.
Sonuçlar hayal kırıklığı yaratıyor çünkü paylaşılan bellekte bir değişiklik yok. Sebebini bulmaya çalıştığımızda, analizin doğru olup olmadığını kanıtlayacak güvenilir bir gösterge olmadığını ve paylaşılan bellek ile kod nesnelerinin kopyaları arasındaki ilişkiyi kanıtlayamayacağını fark ettik. Açıkçası, burada bir şeyler eksik. Bundan kazanılan deneyim şudur: Kullanmadan önce teorinizi kanıtlayın.
Sayfa hatalarını analiz edin
Google'da Copy-on-Write hakkında bilgi aradığımızda, Copy-on-Write'ın sistemdeki sayfa hataları ile ilgili olduğunu öğrendik. Her CoW, işlemde bir sayfa hatasını tetikler. Linux ile birlikte gelen Perf aracı, sayfa hataları dahil olmak üzere donanım / yazılım sistemi olaylarının günlüğe kaydedilmesine izin verir ve hatta yığın izleri sağlayabilir!
Bu yüzden bir prod sunucusu çalıştırdık, sunucuyu yeniden başlattık, çatallanmasını bekledik, bir çalışan işlemin PID'sini aldık ve sonra aşağıdaki komutu çalıştırdık:
performans kaydı -e sayfa-hataları -g -p < PID >Yığın izleme sırasında bir sayfa hatası oluştuğunda ne olacağını görmek için yeni bir fikrimiz var.
Sonuç beklenmedikti ve kod nesnesi kopyalanmadı. En büyük şüpheli, gcmodule.c'ye ait olan ve çöp toplama tetiklendiğinde çağrılan Collect'tir. CPython'da GC'nin çalışma prensibini okuduktan sonra, aşağıdaki teoriyi bulduk:
Eşiğe bağlı olarak CPython'un GC'sini belirleyici olarak tetikleyin. Varsayılan eşik çok düşük olduğundan çok erken bir aşamada başladı. Nesnelerin nesle bağlı bir listesini tutar ve GC sırasında bağlantılı liste karıştırılır. Bağlantılı liste yapısı nesnenin kendisiyle birlikte mevcut olduğundan (ob_refcount gibi), bu nesnelerin bağlantılı listede yeniden yazılması sayfanın CoW olmasına neden olur, bu da talihsiz bir yan etki.
Deneme 2: GC'yi devre dışı bırakmayı deneyin
GC bizi bıçakladığından beri, devre dışı bırakın!
Bootstrap komut dosyamız bir gc.disable çağrısı ekledi ve ardından sunucuyu yeniden başlattı. Sunucuyu yeniden başlattık ama maalesef! Perf'e tekrar bakarsak, gc.collect'in hala çağrıldığını ve hafızanın hala kopyalandığını göreceğiz. GDB'nin bazı hata ayıklamalarını kullanarak, onu geri yüklemek için gc.enable adlı üçüncü taraf bir kitaplığın (msgpack) kullanıldığını ve bu nedenle gc.disable'ın önyükleme sırasında temizlendiğini gördük.
Msgpack'i yamalamak, yapmamız gereken son şeydir, çünkü bu, gelecekte diğer kütüphanelerin de aynısını yapacağını fark etmediğimiz anlamına gelir. Öncelikle, GS'yi devre dışı bırakmanın gerçekten çok yardımcı olduğunu doğrulamamız gerekiyor. Cevap gcmodule.c içindedir. Gc.disable'a alternatif olarak gc.set_threshold (0) yaptık. Bu sefer hiçbir kitaplık geri yüklenmedi.
Bu şekilde, her bir çalışan işlemin paylaşılan belleğini başarıyla 140 MB'tan 225 MB'a yükselttik ve ana bilgisayardaki her makinenin toplam bellek kullanımını 8 GB azalttık. Bu, tüm Django kümesi için% 25 bellek tasarrufu sağlar. Böylesine geniş bir baş alanıyla, daha fazla işlem çalıştırabilir veya daha yüksek bir RSS bellek eşiğiyle çalıştırabiliriz. Aslında, bu tür iyileştirmeler Django katmanının verimini% 10'dan fazla artırdı.
Deneme 3: GC tamamen yasaklanmalıdır
Bir dizi ayarı denedikten sonra, daha büyük bir ölçekte denemeye karar verdik: kümeler. Geri bildirim oldukça hızlıydı çünkü GC devre dışı bırakıldıktan sonra web sunucusunu yeniden başlatmak o kadar yavaşladı ki sürekli dağıtımımız kesintiye uğradı. Genellikle yeniden başlatmak 10 saniyeden az sürer, ancak GC devre dışı bırakıldıktan sonra bazen 60 saniyeden fazla sürer.
Bu hatayı yeniden üretmek çok zahmetli çünkü deterministik değil. Çok sayıda deneyden sonra, en üstte gerçek bir yeniden tepe gösterilir. Bu olduğunda, ana bilgisayardaki kullanılabilir bellek neredeyse sıfıra düşer ve geri atlayarak tüm önbelleği boşaltmaya zorlar. Daha sonra tüm kodun / verilerin diskten okunması gerektiğinde (DSK% 100), her şey yavaştır.
Python'un yorumlayıcıyı kapatmadan önce son GC'yi yapması garip geliyor, bu da kısa sürede bellek kullanımında büyük bir sıçramaya neden olacak. Dahası, önce bunu kanıtlamak ve sonra nasıl doğru şekilde idare edileceğini bulmak istiyorum. Bu nedenle, uWSGI'nin python eklentisindeki Py_Finalize çağrısını yorumladım ve sorun ortadan kalktı.
Ama belli ki, Py_Finalize'ı yasaklayamayız. Bir sürü önemli temizlememiz olduğu için, buna bağlı atexit kancalarını kullanmamız gerekiyor. Son olarak, yaptığımız şey, GC'yi tamamen devre dışı bırakmak için CPython'a bir çalışma zamanı bayrağı eklemekti.
Sonunda, bu uygulamayı daha geniş bir ölçekte genişletmeye başladık. Bundan sonra tüm kümede denedik, ancak sürekli dağıtım tekrar kesintiye uğradı. Ancak, bu sefer yalnızca eski CPU modeli (Sandybridge) makinesinde kesintiye uğradı ve yeniden üretilmesi daha da zordu. Alınan ders: daha eski müşterileri / eski modelleri daha fazla test edin çünkü kesintiye uğrama olasılığı daha yüksektir.
Sürekli dağıtımımız oldukça hızlı bir süreç olduğundan, olanları gerçekten yakalamak için kullanıma sunma komutunun üstüne ayrı bir şey ekledim. Bu şekilde, önbelleğin gerçekten düşük olduğu bir anı yakalayabiliriz. Tüm uWSGI işlemleri çok sayıda MINFLT'yi (küçük sayfa hataları) tetikler.
Perf ile elde edilen özet üzerinden bir kez daha Py_Finalize'yi görüyoruz. Kapatma sırasında, son GC'ye ek olarak, Python, tür nesnelerini yok etme ve modülleri kaldırma gibi bir dizi temizleme işlemi yapar. Bu, paylaşılan belleğe tekrar zarar verir.
Deneme 4: GC'yi kapatmanın son adımı: temizleme yok
Neden temizlemeye ihtiyacımız var? Bu süreç ölecek ve başka bir yedek alacağız. Gerçekten önemsediğimiz şey, uygulamanın ateşleme kancasını temizlemek. Python'un temizlenmesine gelince, bunu yapmak zorunda değiliz. Önyükleme betiğinin sonu:
Bu gerçeğe dayanarak atexit işlevi kayıt defterinin ters sırasına göre çalışır. Atexit işlevi diğer temizlemeleri tamamlar ve ardından son adımın geçerli işleminden çıkmak için os._exit (0) öğesini çağırır.
Bu iki hattın değişmesiyle nihayet tüm kümenin tanıtımını tamamladık. Bellek eşiğini dikkatlice ayarladıktan sonra,% 10'luk bir genel performans artışı elde ettik!
Hadi gözden geçirelim
Bu performans iyileştirmesini gözden geçirirken iki sorumuz var.
Öncelikle, çöp toplama yoksa, tüm bellek ayırma serbest bırakılmayacağı için Python belleği patlamaz mı? (Unutmayın, Python belleğinde gerçek bir yığın yoktur, çünkü tüm nesneler öbek üzerinde tahsis edilmiştir.)
Neyse ki bu doğru değil. Python'da nesneleri serbest bırakmanın ana mekanizması hala referans saymadır. Bir nesnenin referansı kaldırıldığında (Py_DECREF olarak adlandırılır), Python çalışma zamanı her zaman referans sayısının sıfıra düşüp düşmediğini kontrol eder. Bu durumda, nesnenin serbest bırakıcısı çağrılacaktır. Çöp toplamanın temel amacı, referans sayımı çalışmadığında referans döngüsünü kırmaktır.
Mola kazancı
İkinci soru: Kazanç nereden geliyor?
GC'yi devre dışı bırakmanın kazancı iki katına çıkar:
Belleğe bağlı sunucu üretimi için daha fazla iş süreci oluşturmak veya CPU'ya bağlı sunucular tarafından oluşturulan iş programlarının yenileme hızını azaltmak amacıyla her sunucu için yaklaşık 8 GB RAM serbest bıraktık;
Döngü başına CPU talimatları (IPC) yaklaşık% 10 arttıkça, CPU verimi de artar.
GC devre dışı bırakıldığında, esas olarak IPC'deki% 10'luk bir artışa bağlı olarak, önbellek kaçırma oranı% 2 ila% 3 düşer. Bir CPU önbelleğini kaçırmanın maliyeti çok yüksektir çünkü CPU işlem hattını geciktirir. CPU önbellek isabet oranındaki küçük iyileştirmeler genellikle IPC'yi önemli ölçüde iyileştirebilir. Daha az CoW ile, farklı sanal adreslere sahip daha fazla CPU önbellek satırı (farklı iş süreçlerinde) aynı fiziksel bellek adresini işaret eder ve bu da daha iyi bir önbellek isabet oranıyla sonuçlanır.
Her bileşenin beklendiği gibi çalışmadığını görebiliriz ve bazen sonuçlar çok şaşırtıcı olabilir. Bu yüzden kazmaya ve etrafa bakmaya devam edin, işlerin nasıl yürüdüğüne şaşıracaksınız!
Bugünün TavsiyesiOkumak için aşağıdaki resme tıklayın
Tao Kardeş: 29 aylık Ali'ye döndüğüm ve kariyerimle ilgili 6 düşüncem