Mühendislik Blogu - Prexy: süper hızlı arama ve değiştirme kuralları

İlk mühendislik blogumuza hoş geldiniz. Diğer bloglardan alışık olduğunuzdan biraz daha teknik olabilir, ancak herkes için anlaşılabilir olması için elimizden geleni yaptık. Bu makalede, Clonable bünyesinde ikame kurallarını uygulamak için kullanılan yeni bir yazılım parçası olan Prexy hakkında konuşacağız.

Arka plan

As a user of Clonable, you may already be familiar with the substitution rule functionality. You can use these search and replace rules to exchange pieces of text or code with a self-selected variant. For example, you can use this to replace an API key or an analytics ID so you can do different analysis for your original site and your translated clones. Internally, these same rules are also used for replacing your original domain name to your clone's domain name and a number of other things.

Since the very first version of Clonable, this functionality has never been updated, which in itself is not surprising, because it did its job just fine. Still, improvements were possible, both in terms of performance and usability. Once in a while, we Clonable take a piece of our product that hasn't been spent for a long time to start improving it. For example, last year we overhauled all of our data infrastructure to make it faster, more scalable, and more fault-tolerant, and before that we also rebuilt the URL translation functionality from the ground up. This quarter it was the turn of the substitution rules.

Eski durum ve sınırlamalar

Substitution rules are applied in Clonable only at the very last stage, just before sending the response back to the client. In the first version of Clonable, we therefore chose to implement this within the NGINX web server using replace-filter-nginx-module, a module created by OpenResty. The module uses sregex as its streaming regex engine, also created by OpenResty. The streaming aspect is important in this case because we don't want to load each response fully into memory. If we did, a number of large responses could cause NGINX to crash or crash because of insufficient memory availability. A second important feature of sregex, is that it can process multiple lines in parallel. This is because each clone already has some standard rules and, in addition, sometimes some rules added specifically for the clone. If these could not be handled in parallel, we would still need to buffer the entire response, because we would need to be able to match it again after the first rule for the second rule.

Ancak geçen yıl giderek artan bir şekilde garip performans sorunlarıyla karşılaştık. Bu sorunlar genellikle kısa süreliydi, ancak aşırı durumlarda bir sayfanın yanıt süresini saniyelerce artırabiliyordu. Böyle bir artıştan sonra sayfa genellikle tekrar normal bir şekilde yükleniyor ve bu da sorunun yeniden üretilmesini zorlaştırıyordu. Bu konuda daha fazla hata ayıklamak için, tüm yanıtlara Clonable-Timings başlığını ekledik ve bu da çeviri sürecinin hangi adımının bu kadar zaman aldığını kabaca belirlememizi sağladı. Bu, neredeyse tüm durumlarda, yukarı akışın hızlı bir şekilde yanıt verdiğini ve çevirinin de oldukça sorunsuz olduğunu ortaya koydu. Bununla birlikte, çevirinin tamamlanması ile isteğin tamamen tamamlanması arasında bir boşluk vardı ve bu bize sorunun ikame kurallarıyla ilgili olabileceğine dair bir ipucu verdi.

NGINX'in eşzamanlılık modeli

Ancak, bu bize bu gecikmelerin neden sadece ara sıra meydana geldiği ve sunucular kapasitelerinin yarısına bile yüklenmediğinde bunun neden olduğu sorusuna henüz bir cevap vermedi. Bu fenomeni daha iyi anlamak için NGINX'in iş yüklerini nasıl işlediğine biraz daha derinlemesine bakmamız gerekiyor.

NGINX, bir ana süreç ve birden fazla işçi süreç içeren bir işçi mimarisine sahiptir. Bağlantılar, aynı anda birden fazla isteği işlemek için işçiler arasında dağıtılır. Ancak bu yapının da bir dezavantajı vardır: bir çalışan bir talebi işlemekle meşgulken, aynı çalışana atanan diğer talepler de beklemek zorunda kalır. Bu durum, diğer çalışanların yapacak bir işi olmasa bile, ağır bir talebin birden fazla talep için gecikmelere neden olmasına neden olabilir. Bu etki aşağıdaki resimde açıkça görülebilir. Bu görüntünün oluşturulması sırasında, 10 farklı bağlantıda bir stres testi çalıştırılmaktadır. Görüntü, 3 çalışanın meşgul olduğunu ve dördüncü çalışanın neredeyse hiçbir şey yapmadığını açıkça göstermektedir.

Prexy zamanı

Darboğazın açıkça görülebilmesiyle birlikte, bunu iyileştirmek için bir plan oluşturduk. Bu projeye RegEx ve Proxy kelimelerinin birleşiminden oluşan Prexy adı verildi. Nihai sonuç için 3 ana gereksinim vardı:

  1. Mevcut kurulumun yerine geçecek bir ürün olmalıdır. Mevcut kurulumlarda bir şeyleri bozmaktan kaçınmak için değiştirme kuralları aynı şekilde uygulanmalıdır.

  2. Çözümün çok fazla bellek kullanmasını önlemek için ikame kuralları akış halinde olmalıdır.

  3. Yeni çözüm mevcut çözümden daha hızlı olmalıdır.

Öncelikle, istekleri verimli bir şekilde işleyebilecek güçlü bir çok iş parçacıklı çalışma zamanı aramamız gerekiyordu. Birkaç seçeneği değerlendirdikten sonra, Rust programlama dili için Tokyo çalışma zamanını seçtik. Rust, C++ gibi diğer düşük seviyeli dillerin güvenlik riskleri olmadan hızlı programlar yazmanıza olanak tanımasıyla biliniyor. Tokyo çalışma zamanı, ağ uygulamaları için üretilmiş esnek bir asenkron çalışma zamanıdır. Bizim için en önemli özelliklerinden biri iş çalma özelliğine sahip olmasıdır. Bu, NGINX'in aksine, bir isteğin bir çalışana bağlı olmadığı, ancak yapacak hiçbir şeyi olmayan bir çalışanın başka bir çalışandan iş "çalabileceği" anlamına gelir. Bu şekilde sunucularımızın kaynakları daha iyi kullanılabilir.

İlk prototip

Temel teknolojileri seçtikten sonra, bu projenin bize ne kadar hız kazandıracağını ve buna değip değmeyeceğini kabaca tahmin etmek için bir ilk prototip oluşturmaya karar verdik. Regex motoru (kuralları gerçekten işleyen ve uygulayan kısım) için ilk uygulama olarak standart regex kütüphanesini (ya da Rust terminolojisindeki adıyla crate) kullandık. Bu motor, sregex gibi, geri izleme yapmaz, bu da ReDoS sorunu riskinin daha az olduğu anlamına gelir. Bir ReDoS'ta, bir düzenli ifade öyle bir girdiyle karşılaşır ki girdiyi değerlendirmek için gereken süre katlanarak artar. Bu durum performans açısından felaket olabilir ve cloudflare'in büyük bir kesinti yaşamasına neden olmuştur. Bu yüzden bizim için kullandığımız motorun geri izleme yapmaması önemlidir.

Bu regex motorunu kullanarak bir ilk prototip oluşturduk. Bu prototipte sabit kodlanmış kurallar kullanıldı ve regex motoru tüm yanıtı akış yerine arabelleğe aldı, ancak bu ilk performans testi için yeterliydi.

Bu regex motorunu kullanarak bir ilk prototip oluşturduk. Bu prototipte sabit kodlanmış kurallar kullanıldı ve regex motoru tüm yanıtı akış yerine arabelleğe aldı, ancak bu ilk performans testi için yeterliydi. Test kurulumumuz iki sanal makineden oluşuyordu ve bir sanal makine hem eski çözümün hem de yeni çözümün çalışabileceği bir kurulum çalıştırıyordu. Bu şekilde iki yöntem arasında kolayca karşılaştırma yapabildik. Diğer sunucu, kaynak sunucu olarak görev yapan ve bir test dosyası sunan NGINX'i çalıştırdı. Aynı sunucuda yükü oluşturduğumuz wrk aracı da çalışıyordu. Saf veri için yük oluşturucuyu ayrı bir sanal makinede çalıştırmayı tercih ettiğinizden, bu tamamen optimum değildir, ancak bu sanal makine, NGINX ve wrk'nin birbiriyle etkileşime girmemesi için yeterli kaynağa sahipti.

İlk test sonuçları çok açıktı: Prexy tek bir isteği işlerken yaklaşık 22 kat daha hızlıydı (aşağıdaki resme bakın). Bu, projeye son yeşil ışığı yaktı, çünkü bu kadar büyük bir farkla, işlevselliğin tamamlanmasıyla ortaya çıkabilecek bazı performans düşüşlerini absorbe etmek için yeterli alan vardı.

İlk testlerden sonra, derlenmiş düzenli ifadelerin önbelleğe alınması gibi bir dizi belirgin optimizasyon uyguladık. Ayrıca yukarı akışa bağlantıları yeniden kullanmak için daha verimli bir yol uyguladık. Bu sayede yaklaşık %20'lik bir hız kazanımı elde ettik. Daha sonra her iki çözümü de sunucuya mümkün olduğunca çok istek göndererek yoğun yük altında test ettik. Aradaki fark yine açıkça görüldü ve Prexy eski çözümden yaklaşık 25 kat daha yüksek bir verim elde etti.

Akış regex motoru

Bu projenin gerekliliklerinden biri, kullanılan regex motorunun akışlı olmasıydı. Prototipin regex oluşturucusu böyle değil, bu nedenle bu alanda ek çalışma yapılması gerekiyordu. Bununla birlikte, regex sandığının oldukça optimize edilmiş olduğu ortaya çıktı, bu nedenle bu uygulamayı akış motorumuz için temel almaya karar verdik. Bir regex motoru aracılığıyla veri akışı yaparken, akılda tutulması gereken birkaç önemli nokta vardır. İlk olarak, hangi verinin hala geleceğini bilemezsiniz. Bu nedenle, kısmi eşleşmelerle çalışmaya başlamanız gerekir: henüz tamamlanmamış, ancak 1 veya daha fazla karakterle eşleşen eşleşmeler. Tam bir eşleşme bulurken, üst üste binen kısmi eşleşme olup olmadığını kontrol etmeniz gerekir, çünkü bunlar da eşleşme olabilir (ve üst üste binme durumunda daha uzun bir eşleşme daha kısa bir eşleşmeye göre kazanır). Dolayısıyla, bir eşleşmeyi işlemeye ancak o anda mevcut olan tüm kısmi eşleşmelerin tam bir eşleşme olmadığı ortaya çıkarsa başlayabilirsiniz.

Diğer optimizasyonlar

Beklendiği gibi, regex motorunu yeniden inşa etmenin Prexy'nin performansı üzerinde olumsuz bir etkisi oldu. Bununla birlikte, eski çözüme kıyasla hala çok fazla marj vardı ve daha fazla optimizasyon ekleyerek, regex motorunun yeniden oluşturulmasından önce bile daha iyi performans elde ettik. Uyguladığımız optimizasyonlardan biri, bir kuralın belirli bir dosyaya uygulanıp uygulanmayacağını hatırlamaktı. CSS ve JS dosyaları gibi statik varlıklar neredeyse hiç değişmez ve bu nedenle, belirli bir dosyada hiçbir zaman eşleşmediğinde her seferinde bir kuralı çalıştırmak işe yaramaz. Bunun bir alternatifi, değiştirmelerin sonucunu önbelleğe almaktı, ancak bu stratejinin dezavantajı, tüm dosyaları depolamak için çok fazla bellek gerektirmesidir. Hangi kuralların uygulanacağını hatırlayarak, bilgileri bir bitmap olarak depolayarak dosya başına yalnızca birkaç bayta ihtiyacımız var.

Buna ek olarak, regex motorundaki optimizasyonlardan tam olarak yararlanıyoruz; örneğin, değiştirme işleminde kullanılmadıklarında yakalama gruplarını tutmuyoruz. Sonuç olarak, regex motorunun yalnızca tüm eşleşmenin başlangıcını ve sonunu hatırlaması gerekir, bu da iş tasarrufu sağlar. Ayrıca adlandırılmış yakalama gruplarını da devre dışı bıraktık, çünkü bu akış varyantında oldukça karmaşıktı ve eski çözüm bunu da desteklemediğinden, mutlaka gerekli değildi. Tüm bu optimizasyonlar sonuçta aşağıdaki performansı sağladı:

Nihai sonuç

After testing Prexy extensively to ensure that its behavior was indeed the same as the old solution, we began rolling it out incrementally. Over a period of about a week, we activated Prexy for each clone. In our monitoring, the timing of activation was often easy to see. A decrease of about 50% in response times was not rare to see. Minor differences of about 10% were also seen with other customers who had very few substitution rules. Our own website became about 30% faster (~50ms).

Genel altyapımız genelinde, Prexy'yi kullanıma sunduktan sonra aşağıdaki farklılıkları görüyoruz.

  • 33 daha az RAM tüketimi

  • 20 daha az CPU kullanımı

  • 10-50 daha hızlı yanıtlar

Sonuç olarak Prexy'nin başarılı bir proje olduğunu söyleyebiliriz. Eski bir NGINX modülünü değiştirerek, artık tüm müşterilerimiz için net ve ölçülebilir bir fark yaratan daha yeni ve daha verimli teknikler kullanabiliyoruz. Gelecekte, Clonable'ı hem yeni özellikler hem de mevcut işlevselliği geliştirme açısından geliştirmeye devam edeceğiz.


Bu mühendislik blogunu okuduğunuz için teşekkür ederiz. Ürünümüzle ilgili bunun gibi teknik bilgileri daha sık almak isterseniz bize bildirin. Bu proje için heyecanlandınız mı? O zaman iş ilanları sayfamıza göz atın :)

Clonable 'ı bugün ücretsiz olarak kullanmaya başlayın

Tek bir platformdan uluslararası web sitelerini merkezi olarak yönetmenin yollarını keşfedin.

Kullanıcı deneyiminizi iyileştirmek, web sitesi trafiğini analiz etmek ve hizmetlerimizi geliştirmek amacıyla çerezler kullanıyoruz.

Çerez politikamızı okuyun