JavaScript'te Karmaşık Nesneleri Serileştirme
Yayınlanan: 2022-03-11Web Sitesi Performansı ve Veri Önbelleğe Alma
Modern web siteleri genellikle veritabanları ve üçüncü taraf API'ler dahil olmak üzere bir dizi farklı konumdan veri alır. Örneğin, bir kullanıcının kimliğini doğrularken, bir web sitesi veritabanından kullanıcı kaydını arayabilir ve ardından API çağrıları aracılığıyla bazı harici hizmetlerden gelen verilerle bunu süsleyebilir. Veritabanı sorguları için disk erişimi ve API çağrıları için internet gidiş dönüşleri gibi bu veri kaynaklarına yapılan pahalı çağrıları en aza indirmek, hızlı ve duyarlı bir siteyi sürdürmek için çok önemlidir. Veri önbelleğe alma, bunu başarmak için kullanılan yaygın bir optimizasyon tekniğidir.
İşlemler çalışma verilerini bellekte saklar. Bir web sunucusu tek bir işlemde çalışıyorsa (Node.js/Express gibi), bu veriler aynı işlemde çalışan bir bellek önbelleği kullanılarak kolayca önbelleğe alınabilir. Ancak, yük dengeli web sunucuları birden çok işlemi kapsar ve tek bir işlemle çalışırken bile, sunucu yeniden başlatıldığında önbelleğin devam etmesini isteyebilirsiniz. Bu, verilerin bir şekilde serileştirilmesi ve önbellekten okunduğunda seri durumdan çıkarılması gerektiği anlamına gelen Redis gibi işlem dışı bir önbelleğe alma çözümünü gerektirir.
Serileştirme ve seri durumdan çıkarma, C# gibi statik olarak yazılan dillerde elde edilmesi nispeten kolaydır. Ancak JavaScript'in dinamik doğası, sorunu biraz daha zorlaştırıyor. ECMAScript 6 (ES6) sınıfları tanıtırken, bu sınıflardaki (ve türlerindeki) alanlar, başlatılıncaya kadar tanımlanmaz - bu, sınıf somutlaştırıldığında olmayabilir - ve alanların ve işlevlerin dönüş türleri tanımlanmamıştır. tamamen şemada. Dahası, sınıfın yapısı çalışma zamanında kolayca değiştirilebilir—alanlar eklenebilir veya kaldırılabilir, türler değiştirilebilir, vb. Bu, C#'ta yansıma kullanılarak mümkün olsa da, yansıma o dilin "karanlık sanatlarını" temsil eder ve geliştiriciler, işlevselliği bozmasını bekler.
Birkaç yıl önce Toptal çekirdek ekibinde çalışırken bu sorunla karşılaştım. Ekiplerimiz için hızlı olması gereken çevik bir gösterge panosu oluşturuyorduk; aksi takdirde geliştiriciler ve ürün sahipleri bunu kullanmaz. Bir dizi kaynaktan veri topladık: iş takip sistemimiz, proje yönetim aracımız ve bir veri tabanı. Site Node.js/Express'te oluşturuldu ve bu veri kaynaklarına yapılan çağrıları en aza indirmek için bir bellek önbelleğimiz vardı. Bununla birlikte, hızlı, yinelemeli geliştirme sürecimiz, günde birkaç kez konuşlandırmamız (ve dolayısıyla yeniden başlatmamız), önbelleği geçersiz kılmamız ve dolayısıyla birçok avantajını kaybetmemiz anlamına geliyordu.
Açık bir çözüm, Redis gibi işlem dışı bir önbellekti. Ancak, biraz araştırmadan sonra JavaScript için iyi bir serileştirme kitaplığı olmadığını buldum. Yerleşik JSON.stringify/JSON.parse yöntemleri, orijinal sınıfların prototiplerindeki tüm işlevleri kaybederek nesne türünün verilerini döndürür. Bu, seri durumdan çıkarılmış nesnelerin uygulamamız içinde "yerinde" kullanılamayacağı anlamına geliyordu, bu nedenle alternatif bir tasarımla çalışmak için önemli ölçüde yeniden düzenleme gerektirecekti.
Kütüphane Gereksinimleri
JavaScript'te rastgele verilerin serileştirilmesini ve seri durumdan çıkarılmasını desteklemek için, seri durumdan çıkarılmış temsiller ve birbirinin yerine kullanılabilen orijinaller ile, aşağıdaki özelliklere sahip bir serileştirme kitaplığına ihtiyacımız vardı:
- Seri durumdan çıkarılmış temsiller, orijinal nesnelerle aynı prototipe (işlevler, alıcılar, ayarlayıcılar) sahip olmalıdır.
- Kitaplık, iç içe geçmiş nesnelerin prototipleri doğru şekilde ayarlanmış olarak iç içe karmaşıklık türlerini (diziler ve haritalar dahil) desteklemelidir.
- Aynı nesneleri birden çok kez seri hale getirmek ve seri durumdan çıkarmak mümkün olmalıdır - süreç bağımsız olmalıdır.
- Serileştirme formatı, TCP üzerinden kolayca iletilebilir ve Redis veya benzer bir hizmet kullanılarak depolanabilir olmalıdır.
- Bir sınıfı seri hale getirilebilir olarak işaretlemek için minimum kod değişikliği gerekli olmalıdır.
- Kütüphane rutinleri hızlı olmalıdır.
- İdeal olarak, bir tür eşleme/sürüm oluşturma yoluyla bir sınıfın eski sürümlerinin seri durumdan çıkarılmasını desteklemenin bir yolu olmalıdır.
uygulama
Bu boşluğu kapatmak için JavaScript için genel amaçlı bir serileştirme kitaplığı olan Tanagra.js yazmaya karar verdim. Kütüphanenin adı, Enterprise mürettebatının dili anlaşılmaz olan gizemli bir uzaylı ırkı ile iletişim kurmayı öğrenmesi gereken Star Trek: The Next Generation'ın en sevdiğim bölümlerinden birine atıfta bulunuyor. Bu serileştirme kitaplığı, bu tür sorunları önlemek için yaygın veri biçimlerini destekler.
Tanagra.js basit ve hafif olacak şekilde tasarlanmıştır ve şu anda Node.js'yi (tarayıcıda test edilmemiştir, ancak teorik olarak çalışması gerekir) ve ES6 sınıflarını (Haritalar dahil) destekler. Ana uygulama JSON'u destekler ve deneysel bir sürüm Google Protokol Tamponlarını destekler. Kitaplık, deneysel özelliklere, Babel aktarmaya veya TypeScript'e bağımlı olmaksızın yalnızca standart JavaScript (şu anda ES6 ve Node.js ile test edilmiştir) gerektirir.
Serileştirilebilir sınıflar, sınıf dışa aktarıldığında bir yöntem çağrısı ile bu şekilde işaretlenir:
module.exports = serializable(Foo, myUniqueSerialisationKey)
Yöntem, yapıcıyı durduran ve benzersiz bir tanımlayıcı enjekte eden sınıfa bir proxy döndürür. (Belirtilmezse, bu varsayılan olarak sınıf adını alır.) Bu anahtar, verilerin geri kalanıyla serileştirilir ve sınıf ayrıca onu statik bir alan olarak gösterir. Sınıf herhangi bir iç içe tür içeriyorsa (yani, serileştirilmesi gereken türlere sahip üyeler), bunlar da yöntem çağrısında belirtilir:
module.exports = serializable(Foo, [Bar, Baz], myUniqueSerialisationKey)
(Sınıfın önceki sürümleri için iç içe türler de benzer şekilde belirtilebilir, böylece örneğin, bir Foo1'i serileştirirseniz, bir Foo2'ye seri hale getirilebilir.)
Serileştirme sırasında kitaplık, sınıfların anahtarlarının küresel bir haritasını yinelemeli olarak oluşturur ve seriyi kaldırma sırasında bunu kullanır. (Unutmayın, anahtar, verilerin geri kalanıyla serileştirilir.) "Üst düzey" sınıfın türünü bilmek için, kitaplık bunun seri durumdan çıkarma çağrısında belirtilmesini gerektirir:
const foo = decodeEntity(serializedFoo, Foo)
Deneysel bir otomatik eşleme kitaplığı, modül ağacında gezinir ve eşlemeleri sınıf adlarından oluşturur, ancak bu yalnızca benzersiz olarak adlandırılmış sınıflar için çalışır.

Proje Düzeni
Proje birkaç modüle ayrılmıştır:
- tanagra-core - sınıfları serileştirilebilir olarak işaretleme işlevi de dahil olmak üzere farklı serileştirme biçimlerinin gerektirdiği ortak işlevsellik
- tanagra-json - verileri JSON biçiminde seri hale getirir
- tanagra-protobuf - verileri Google protobuffers biçimine serileştirir (deneysel)
- tanagra-protobuf-redis-cache - serileştirilmiş protobuf'ları Redis'te depolamak için bir yardımcı kitaplık
- tanagra-auto-mapper - bir sınıf haritası oluşturmak için modül ağacında Node.js'de yürür, yani kullanıcının seri durumdan çıkarılacak türü (deneysel) belirtmesi gerekmez.
Kitaplığın ABD yazımını kullandığını unutmayın.
Örnek Kullanım
Aşağıdaki örnek, seri hale getirilebilir bir sınıf bildirir ve onu seri hale getirmek/seri hale getirmek için tanagra-json modülünü kullanır:
const serializable = require('tanagra-core').serializable class Foo { constructor(bar, baz1, baz2, fooBar1, fooBar2) { this.someNumber = 123 this.someString = 'hello, world!' this.bar = bar // a complex object with a prototype this.bazArray = [baz1, baz2] this.fooBarMap = new Map([ ['a', fooBar1], ['b', fooBar2] ]) } } // Mark class `Foo` as serializable and containing sub-types `Bar`, `Baz` and `FooBar` module.exports = serializable(Foo, [Bar, Baz, FooBar]) ... const json = require('tanagra-json') json.init() // or: // require('tanagra-protobuf') // await json.init() const foo = new Foo(bar, baz) const encoded = json.encodeEntity(foo) ... const decoded = json.decodeEntity(encoded, Foo)
Verim
İki serileştiricinin ( JSON serileştirici ve deneysel protobuf serileştirici) performansını bir kontrolle (yerel JSON.parse ve JSON.stringify) karşılaştırdım. Her biri ile toplam 10 deneme yaptım.
Bunu, Ubuntu 17.10 çalıştıran 32Gb belleğe sahip 2017 Dell XPS15 dizüstü bilgisayarımda test ettim.
Aşağıdaki iç içe nesneyi seri hale getirdim:
foo: { "string": "Hello foo", "number": 123123, "bars": [ { "string": "Complex Bar 1", "date": "2019-01-09T18:22:25.663Z", "baz": { "string": "Simple Baz", "number": 456456, "map": Map { 'a' => 1, 'b' => 2, 'c' => 2 } } }, { "string": "Complex Bar 2", "date": "2019-01-09T18:22:25.663Z", "baz": { "string": "Simple Baz", "number": 456456, "map": Map { 'a' => 1, 'b' => 2, 'c' => 2 } } } ], "bazs": Map { 'baz1' => Baz { string: 'baz1', number: 111, map: Map { 'a' => 1, 'b' => 2, 'c' => 2 } }, 'baz2' => Baz { string: 'baz2', number: 222, map: Map { 'a' => 1, 'b' => 2, 'c' => 2 } }, 'baz3' => Baz { string: 'baz3', number: 333, map: Map { 'a' => 1, 'b' => 2, 'c' => 2 } } }, }
Yazma Performansı
seri hale getirme yöntemi | Ave. inc. ilk deneme (ms) | StDev. inc. ilk deneme (ms) | Cad. Örn. ilk deneme (ms) | StDev. eski. ilk deneme (ms) |
JSON | 0.115 | 0.0903 | 0.0879 | 0.0256 |
Google Protobuf'ları | 2.00 | 2.748 | 1.13 | 0.278 |
Kontrol grubu | 0.0155 | 0.00726 | 0.0139 | 0.00570 |
Okumak
seri hale getirme yöntemi | Ave. inc. ilk deneme (ms) | StDev. inc. ilk deneme (ms) | Cad. Örn. ilk deneme (ms) | StDev. eski. ilk deneme (ms) |
JSON | 0.133 | 0.102 | 0.104 | 0.0429 |
Google Protobuf'ları | 2.62 | 1.12 | 2.28 | 0.364 |
Kontrol grubu | 0.0135 | 0.00729 | 0.0115 | 0.00390 |
Özet
JSON serileştirici, yerel serileştirmeden yaklaşık 6-7 kat daha yavaştır. Deneysel protobufs serileştiricisi, JSON serileştiricisinden yaklaşık 13 kat veya yerel serileştirmeden 100 kat daha yavaştır.
Ek olarak, her seri hale getirici içindeki şema/yapısal bilgilerin dahili olarak önbelleğe alınmasının performans üzerinde açıkça bir etkisi vardır. JSON serileştiricisi için ilk yazma, ortalamadan yaklaşık dört kat daha yavaştır. Protobuf serileştirici için dokuz kat daha yavaştır. Bu nedenle, meta verileri önceden önbelleğe alınmış nesneleri yazmak, her iki kitaplıkta da çok daha hızlıdır.
Aynı etki okumalar için de gözlendi. JSON kitaplığı için, ilk okuma ortalamadan yaklaşık dört kat daha yavaştır ve protobuf kitaplığı için yaklaşık iki buçuk kat daha yavaştır.
Protobuf serileştiricinin performans sorunları, hala deneysel aşamada olduğu anlamına gelir ve bunu yalnızca bir nedenden dolayı biçime ihtiyacınız varsa tavsiye ederim. Bununla birlikte, format JSON'dan çok daha ters olduğundan ve bu nedenle kablo üzerinden göndermek için daha iyi olduğundan, biraz zaman ayırmaya değer. Stack Exchange, dahili önbelleğe alma için biçimi kullanır.
JSON serileştirici açıkça çok daha performanslıdır, ancak yine de yerel uygulamadan önemli ölçüde daha yavaştır. Küçük nesne ağaçları için bu fark önemli değildir (50ms'lik bir isteğin üzerine birkaç milisaniye, sitenizin performansını bozmaz), ancak bu, aşırı büyük nesne ağaçları için bir sorun haline gelebilir ve benim geliştirme önceliklerimden biridir.
Yol Haritası
Kütüphane hala beta aşamasındadır. JSON serileştirici oldukça iyi test edilmiş ve kararlıdır. İşte önümüzdeki birkaç ayın yol haritası:
- Her iki serileştirici için performans iyileştirmeleri
- ES6 öncesi JavaScript için daha iyi destek
- ES-Next dekoratörleri için destek
Karmaşık, iç içe nesne verilerini serileştirmeyi ve orijinal türüne seri hale getirmeyi destekleyen başka bir JavaScript kitaplığı bilmiyorum. Kitaplıktan yararlanacak bir işlev uyguluyorsanız, lütfen deneyin, geri bildiriminizle iletişime geçin ve katkıda bulunmayı düşünün.
Proje ana sayfası
GitHub deposu