Encoding / Decoding

Atalarin encoding'i


Programlar genelde veriyi 2 sekilde kullanir:

1. In-memory: bilgisayar hafizasinda list, dictionary, ya da custom class olarak tutulut. Bu veri yapilari direk olarak CPU tarafindan erisilmek ve manipule edilbilmek uzere optimize edilmis olurlar. 

2. Byte array: Datayi diske yazmak istedigimizde ya da network uzerinden transfer etmek istedigimizde, bir sekilde byte dizesi haline getirmemiz gerekir. 

Iste hafizada duran verilen byte dizisi haline getirilmesi islemine encoding (marshalling ya da seializasyon da denilir) adi veilir. Byte arayindan tekrar hafiza tiplerine donustumek de decoding olarak adlandirilir. (benze sekilde Unmarshalling veya deseializasyon da denilebili).

Peki hafizada tutulan verilei ne sekilde serialize edebiliriz?

1. Dil destekli encoding
Mesela python icin pickle, Java icin java.io.Serializable, Ruby icin marshall formatlarinda dil tarafindan desteklenen encoding olanaklari vardir. Bir python class'ini ya da arrayini kolayca pickle'a cevirip, diske yazabiliriz. Daha sonra bu pickle'i tekrardan okuyup, hafizaya yine ayni Python class ya da arrayini almis oluruz.

Ancak bu dil-destekli encoding yonteminin bazi poblemlei var:

1. En basta o dile bagimli kaliyorsunuz. Python'da pickle ile yazdiginiz bir datayi Scala/Spark  ile okumaya kalktiginizda zorlukla cekebiliyorsunuz. 

2. Decoding asamasinda, encoding kutuphanesi (pickle mesela), dinamik olaak classlar olusturacaktir. Bu da aslinda bir guvenlik sorunu teskil eder. Kotu niyetli kisiler, pickle formatini taklit ederek sizin uygulamaniza yabanci kod sokabilirle ve calistirabilirler.  (Surada guzel bir ornek var)

3. Bu gibi dil destekli encoding yaklasimlarinda genelde versiyonlama, geri ve ileri uyumluluk pek dusunulmez. Bu acidan ileri/geri uyumluluk ve sema evrimi gibi konular pek desteklenmez. Ki pickle bunlarin hicbirini desteklemez ornegin. 

4.Efficiency. Bu kutuphaneler genelde cok optimize degildir. Ornek olarak Java'nin serializasyon kutuphanesi kotu performansi ile unludur. Bu acidan zaten bircok spark projeci Kryo serializasyon kutuphanesini kullanir. 

2. Textual Formatlar

Tamam, o zaman dilden bagimsiz ve bircok dil tarafindan desteklenen formatlara gozatabiliriz. XML, Json ve CSV encok bilinen textual (insan tarafindan okunabilir) formatlardir. 

Ama gidisattan da anlasilabilecegi gibi, bunlarin da problemleri bulunmaktadir:

1. Sayilarin encode edilmesi buyuk bir problem. Ornegin XML ve CSV zaten string ve sayi arasinda bile bir ayrim yapmaz. Json gene stringleri kesme isaretiyle ayiriyor hadi tamam ama mesela integer ile long tiplerini ayirdetmek imkansiz. 

2. JSON ve XML binary tipini desteklemiyor. Insanlar bunu asabilmek icin, herhangi bir binary stringi alip base64 encode edip o sekilde string olarak kullaniyorlar. Bu ise yarasa da, hacky bir yontem ve de data buyuklugunu artiriyor. 

3. XML sema tanimlamarini destekliyor ama JSON bu konuda cok geride. Genelde sema tanimlamasi kullanilmiyor. CSV zaten komple semasiz. 


3. Binary Encoding

Bir ustte bahsettigimiz ortak formatlar, birkac farkli organizasyon arasinda veri paylasimi icin ideal gibi gozukuyor. Ne de olsa insna tarafindan okunabilir ve de iki organizasyon sema uzerinde hem fikir ise efficinecy bir noktaya kadar gozardi edilebilir.

Ama kendi organizasyonumuz icerisinde (yani kimseyle hem fikir olmak zorunda degiliz) kendi kabul ettigimiz daha kompakt formatlar ile calisabiliriz. Kucuk verilerde gozardi edilebilir bir performans artisi olsa da eger buyuk veriler ile calisiyotsak cok buyuk bir kazanc elde etmis oluruz. 

Json, XML'e gore daha az verbose olsa da yine de verileri represente etmek icin ekstradan bitsuru karakter kullanirlar. Binary custom bir format ile kiyaslandiginda cok gereksiz data-size sismesi oluyor diyebiliriz. 

Bu acidan JSON ve XML'in de binary versiyonlari cikmistir. Ama yine de , kendi baslarina bir sema barindirmadiklari icin, her kayitta column isimlerini de tutmalari gerekir. Yani bildigimiz json dosyasini alip binary'e cevirmek gibi. Bu durumda yine gereksiz karakter kullanimi olmaktadir. Ornegin asagidaki users arrayinda, userName bilgisi her kayit icin bulunmak zorundadir.

{
    users: [
        {userName: "lombakSehidi", userId: 123},
        {userName: "kkSerafettin", userId: 234},
         ...
    ]
}


4. Thrift ve Protobuf 

Thrify, facebook ve Protobuf Google tarafindan gelistirilmis binary encoding kutuphaneleridir. 

Ikisi de, daha en basta, encode edilecek data icin birer sema olusturulmasini gerektirir. Ornek olarak, Thrift icin Thrift Interface Definition Language ile sema tanimlamasi soyle yapilabilir:

struct Person {
    1: required string userName,
    2: optional i64 userId,
    3: optional list<string> friends
}

Protobuf ile ayni semayi soyle tanimlayabiliriz:

message Person {
    required string user_name = 1;
    optional int64 user_id = 2;
    repeated string friends = 3;
}

Bu sekilde sema tanimlamasini yapmanin bircok faydasi var. Bir tanesi, otomatik tooling. Hem Thrift, hem de Protobuf, bu sema tanimlamalarina bakarak istediginiz dilde kod uretebiliyor. Bu sayede istediginiz dilde verileri encode/decode edebiliyorsunuz. Bu da bircok olanaga kapi aciyor. Tek bir sema tanimlamasi ile farkli dillerde yazilmis bircok proje  ayni verileri okuyup yazabiliyor. Hatta kendi aralarinda iletisim kurabiliyor (gRPC vb., gelecegiz.).

Iki sema tanimlamasinda da birtakim rakamlar goruyoruz. Bunlara field tag adi veriliyor. Data encode edildikten sonra da, field isimleri yerine (userName gibi), bu field taglar ile eslestiriliyor. Hem daha kisa, hem de ileride field isimlerini degistirme durumunda karisiklik cikmiyor.


Schema Evolution

Semalar zaman icerisinde degisecektir. Bu degisime schema evolution adi verilmektedir. Ornegin userName ismini memberName olarak degistirdigimizde semayi evolve etmis oluyoruz. Bu durumda field name yerine field tag kullanildigi icin eski sema ile yazilmis olan encoded veriler de problemsiz olarak okunabilecektir. 

Semaua yeni alanlar (field) eklenebilir. Bu durumda eski semaya gore okuma yapan bir client, data iceriisnde fazladan olan (yeni eklenen) alanlar yuzunden problem yasamaz. Cunku her alan icin kac byte skip edilecegi belirtilmistir. Bu durumda o alani sadece okuyup gececektir. Bu sayede forward compatibility saglanmis olur. Yani eski semaya gore yazilmis olan kod, yeni semayla yazilmis datalari da okuyabilir. 

Benzer sekilde, yeni semaya gore yazilmis bir kod, eski datalari okuyabilmelidir. Buna da backward compatibility adi verilir. Buradaki tek onemli nokta, yeni eklnene alanin required bir alan olamayacagidir. Cunku bu durumda eski semaya gore yazilmis datalar bu alani icermeyecegi icin, olay patlar. 

Bir alani silmek de eklemek ile benzer implikasyonlara sahip. Required bir alan silinemez, bu durumda eski sema ile yazilmis kod patlar. 


Avro

Avro da Thrift ve Protobuf gibi bir binary encoding kutuphanesidir. Yukaridaki semayi tanimlamak istersek:

record Person {
    string userName;
    union { null, long } userId = null;
    array<string> friends;

Burada dikkatimizi ceken sey, field tag olmamasi. Ayrica, encode edilmis datada alanlar icin veri tipi ve ne kadar byte skip edilmesi gerektigi de belirtilmez. Sadece encode edilmis alanlar yan yana concatenate edilir. Bu sayede, ayni veriyi encode ettiklerinde en kucuk veriyi Avro uretmektedir. Yani ucu arasinda en compact olani. 

Bu durumda Avro bu datayi nasil parse ediyor? Semaya bakarak. Yani sema sadece tek bir kez tanimlaniyor ve data icerisinde sema ile ilgili bir bilgi yer almiyor. Bu da demek oluyor ki, semada en ufak bir uyumsuzluk, (mesela bir alanin tipinin degistirilmesi), datayi okunamaz hale getirecektir.

Peki bu durumda schema evolution nasil saglanacak?


Writer's schema ve reader's schema

Avro ile bir datayi encode edip yazmak istedigimizde, var olan guncel mevcut semayi kullanacaktir. Buna writers schema diyebiliriz. 

Benzer sekilde bir uygulama Avro ile bir datayi okuyup decode etmke istediginde de kendi bildigi, o datanin semasi reader's schema olarak adlandirilir. 

Bu ikisi, ayri uygulamanin farkli versiyonlari olabilir. Yani writer program, reader programdan 2 versiyon oncesi olabilir ve aralarinda sema farkli bulunabilir. (Seman evolve olmus ablacim senin).

Sema evolve olmus ise. Avro, iki semayi yan yana koyarak bir resolution cikarmaya calisir. Yani olayi cozmeye calisir. Eger bu iki sema compatible ise, resolution basarili olacaktir ve datalar okunabilir. Ama degilse, olay patlar.

Ornek olarak alanlarin yeri degismis olabilir. Bu durumda Avro, alanlari isimden match ederek bu durumu cozecektir. Ya da, reader schema'sinda default degeri olan bir alan eklenmisse, ve bu alan okunan data icerisinde yok ise, belirtilen default deger ile doldurulacaktir, 

Esas sorun su ki, biz writer's schema'yi nereden bilecegiz? Her record icin semayi datanin icerisine gomemeyiz ki bu tum kazanimlari goturecektir. Bu sorunun cevabi, Avro'nun hangi context'te kullanildiginda sakli:

1. Milyonlarca record iceren buyuk dosyalar (Hadoop use case): Milyonlarca record ayni semaya sahip oldugu icin, buyuk dosyanin basinda sema sadece tek bir kere tanimlanir. 

2. Bagimsiz olarak yazilmis database recordlari: Burada tum kayitlar ayni versiyona sahiptir diyemeyiz. Bu durumda, her bir kayit icin bir versiyon numarasi verilip, her bir versioyna karsilik gelen semayi da ayri bir tablo icerisinde tutabiliriz. 

3. Kayitlari network uzerinden gondermek: Bu durumda sadece connection basinda taraflar semalari exchange ederek bir agreement olusturulur. Daha sonra connection suresince bu semalara sadik kalinir. (ornek olarak Avro serialization kullanan Avro RPC protokolu verilebilir).

Genelde Avro kullanilan yerlerde, versiyon numaralarina karsilik gelen semalari tutmak adettendir. 


Dinamik Sema Uretimi

Avro'nu bir farki, data icerisinde field tag icermemesi demistik. Bu, otomatik olarak sema uretilmesi gereken durumlarda cok isimize yarar. Peki neden otomatik olarak sema uretmek isteyelim ki?

Bir kullanim alani, elimizde buyuk bir database var. Bunu bir dosyaya dump etmek istiyoruz. Daha once bahsettigimiz sikintilardan dolayi da textual encoding (XML, Json, CSV...) kullanmak istemiyoruz. 

Yani binary bir encoding olsun isriyoruz. Ama bu durumda oncelikle sema olusturmamiz gerekir. DB'deki tablo yapisina bakarak otomatik olarak bir Avro sema tanimlamasi olsuturulabilir. 

Ancak esas mesele, database tablolari degistigi zaman, ornegin tabloya yeni bir alan eklendigi zaman, Avro olusturulacak olan semada schema evolution'u dusunmek zorunda kalmayacaktir. Mevcut tablo yapisina uygun semayi uretir gecer. Gerisi schema resolution asamasinda Avro tarafindan halledilir.

Anca Thrift ve Protobuf field tag kullandigi icin, database tablolarindaki degisiklikler bir insan tarafindan yeni semaya yansitilmalidir. Yeni eklenen column'lar icin yeni field taglar verilmeli, bir column adi degismis ise, field tagi ayni kalacak sekilde yeni sema uretilmeli gibi gicik durumlar ortaya cikabilir. Hepsi de bir sekilde otomatize edilebilir ancak hata potansiyeli her zaman mevcut.


Bu yazimizda programlar verileri nasil en efektif sekilde okuyup/yazabilir/gonderebilir bunu inceledik. 

Gorusmek uzere, stay hungry, stay foolish, stay healthy :)




 

Yorumlar

Bu blogdaki popüler yayınlar

Python'da Multithreading ve Multiprocessing

Threat Modeling 1