Fəsil 04 · 9 dəq oxu

Encoding və evolution

Encoding və evolution

Fəsil 2-də data model-lərin, Fəsil 3-də isə onların diskdə necə saxlandığının üzərində dayandıq. Hər ikisində bir gizli fərziyyə var idi: schema sabitdir. Real sistemlərdə belə deyil. Tələblər dəyişir, yeni field-lər əlavə olunur, köhnələri atılır və bu dəyişikliklər kodu da, data-nı da əhatə edir. Çətinlik ondadır ki, kod və data eyni vaxtda dəyişmir.

Konkret vəziyyəti götürək: istifadəçi profilinə pronouns field-i əlavə edirsiniz. Server-side kod rolling upgrade ilə yenilənir: on node-dan əvvəlcə biri, sonra qalanları. Bir neçə dəqiqə ərzində köhnə və yeni kod eyni anda işləyir. Mobil client-lərin yarısı isə aylarla köhnə versiyada qalacaq. Database-də isə beş il əvvəl yazılmış qeydlər var. Bu fəsil həmin uyğunluğu, yəni data-nın necə encode olunduğunu və fərqli versiyaların bir-birini necə başa düşdüyünü izah edir.

İki istiqamətdə uyğunluq

Kod və data-nın birlikdə yaşaması üçün iki ayrı xüsusiyyət lazımdır:

  • Backward compatibility: yeni kod köhnə kodun yazdığı data-nı oxuya bilir. Bunu təmin etmək nisbətən asandır, çünki yeni kodun müəllifi köhnə formatı tanıyır.
  • Forward compatibility: köhnə kod yeni kodun yazdığı data-nı oxuya bilir. Bu çətindir, çünki köhnə kod gələcəkdə əlavə olunacaq field-ləri bilmir və onları sadəcə ötürməyi bacarmalıdır.

Rolling upgrade hər iki istiqaməti tələb edir: yeni node köhnə node-un yazdığını oxuyur, köhnə node isə yeni node-un yazdığını. Bu xüsusiyyət olmadan "hamısını eyni anda söndür, hamısını eyni anda qaldır" formasına düşür, bu da Fəsil 1-dəki maintainability və məqsədləri ilə ziddiyyət təşkil edir.

Encoding nədir

Proqram data ilə iki təsvirdə işləyir. Yaddaşda data struct, list, map və pointer-lərlə saxlanılır: CPU üçün optimallaşdırılmış forma. Faylə yazanda və ya şəbəkəyə göndərəndə isə onu özünü təsvir edən byte ardıcıllığına çevirmək lazımdır, çünki pointer-in başqa prosesdə mənası yoxdur. Bu çevrilmə encoding və ya serialization, əksi isə decoding adlanır.

Hansı encoding-i seçmək isə uyğunluq sualının əsas hissəsidir.

Dilə bağlı formatlar

Hər dilin öz built-in serialization-u var: Java-da Serializable, Python-da pickle, Ruby-də Marshal. Əlverişlidir, çünki bir sətir kodla obyekt byte-lara çevrilir. Amma bir neçə ciddi problemi var:

  • Dilə bağlılıq. Data-nı başqa dildə yazılmış sistem oxuya bilmir. Beş il sonra sistemi yenidən yazmaq istəsəniz, data-nın formatı sizi köhnə dildə saxlayır.
  • Təhlükəsizlik. Obyekti bərpa etmək üçün decoder ixtiyari class-ları instantiate etməlidir. Bu, remote code execution üçün klassik hücum vektorudur.
  • Versiyalaşdırma düşünülməyib. Backward və forward compatibility çox vaxt təsadüfidir.
  • Performans. Java-nın built-in serialization-u yavaşlığı və böyük ölçüsü ilə məşhurdur.

Praktik qayda: dilə bağlı formatı yalnız qısa ömürlü, bir proses daxilində qalan data üçün istifadə edin.

JSON, XML və CSV

Dillərarası standart formatlardan ən geniş yayılanları JSON, XML və CSV-dir. Hamısı mətn əsaslıdır, insan tərəfindən oxuna bilir və hər yerdə dəstəklənir. Amma hər birinin incə problemləri var:

  • Rəqəmlər. XML və CSV-də rəqəmlə rəqəmlərdən ibarət string arasında fərq yoxdur. JSON rəqəmi string-dən ayırır, amma integer ilə float-ı ayırmır və göstərmir. 2^53-dən böyük integer-lər JavaScript-də itkisiz parse olunmur. Twitter bunun üçün tweet id-sini həm rəqəm, həm də string kimi iki dəfə göndərirdi.
  • Binary data. JSON və XML Unicode string-i dəstəkləyir, byte ardıcıllığını yox. Həll Base64-dür, bu isə ölçünü 33% artırır.
  • Schema. XML Schema və JSON Schema mövcuddur, amma mürəkkəbdir və çox vaxt istifadə olunmur. Schema olmayanda data-nın şərhi koda yazılır.
  • CSV ən zəifidir: nə schema, nə tip, vergül və sətir keçidi ilə bağlı escaping qaydaları isə hər implementation-da fərqlidir.

Buna baxmayaraq JSON organizasiyalar arasında mübadilə üçün de-fakto standart olaraq qalır: uyğunlaşmaq bəzən optimal formatdan vacibdir.

Binary JSON variantları

MessagePack, BSON və oxşar formatlar JSON-u binary şəklə salır: tip-ləri byte-la işarələyir, string uzunluğunu prefix kimi yazır. Əvvəlki {"userName": "Martin", "favoriteNumber": 1337, "interests": ["daydreaming", "hacking"]} nümunəsi JSON-da 81 byte, MessagePack-də 66 byte tutur. Qənaət var, amma kiçikdir: field adları hələ də hər qeyddə təkrarlanır, çünki schema yoxdur. Bu qədər qazanc üçün insan tərəfindən oxunmağı itirmək nadir hallarda dəyər.

Thrift və Protocol Buffers

Thrift və Protocol Buffers schema tələb edən binary encoding kitabxanalarıdır. Schema interface definition language ilə yazılır:

message Person {
  required string user_name       = 1;
  optional int64  favorite_number = 2;
  repeated string interests       = 3;
}

Əsas ideya field tag-lərdir. Encode olunmuş data-da field adı yoxdur, yalnız 1, 2, 3 kimi rəqəmlər var. Eyni nümunə 33 byte tutur: field adlarının təkrarı yoxdur, tip bir neçə bit-ə sığır. Code generator schema-dan hər dil üçün class-lar yaradır, bu da compile-time yoxlamanı mümkün edir.

Schema evolution qaydaları

Tag-lər uyğunluğun açarıdır. Qaydalar belədir:

  • Field əlavə etmək. Yeni field yeni tag alır. Köhnə kod tanımadığı tag-i görəndə onu ötürür, bunun üçün tip annotasiyası neçə byte keçməli olduğunu bildirir. Bu, forward compatibility-dir. Yeni kod köhnə data-da yeni tag-i tapmayanda field-i boş sayır, bu da backward compatibility-dir. Bir şərtlə: yeni field required ola bilməz, çünki köhnə data onu təmin etmir.
  • Field silmək. Əksi: yalnız optional field-i silmək olar və silinmiş tag nömrəsi heç vaxt yenidən istifadə edilməməlidir, çünki köhnə data-da həmin tag hələ də mövcuddur.
  • Tip dəyişmək. Mümkündür, amma itkili ola bilər: 32-bit integer-i 64-bit-ə dəyişəndə yeni kod köhnə data-nı sıfırlarla doldurur, köhnə kod isə yeni 64-bit dəyəri kəsir.
  • Field adını dəyişmək. Adlar encode olunmadığı üçün sərbəstdir. Dəyişməz olan tag-dir.

Avro

Avro Hadoop ekosistemində Thrift-in uyğun gəlmədiyi hallar üçün yaranıb və fərqli bir fərziyyədən çıxış edir: encode olunmuş data-da nə field adı, nə də tag var. Yalnız dəyərlər ardıcıl düzülür. Eyni nümunə 32 byte-dır. Decoder dəyərlərin hansı field-ə aid olduğunu yalnız schema-ya baxaraq bilir, buna görə data-nı oxuyan tərəf onun hansı schema ilə yazıldığını dəqiq bilməlidir.

Writer's schema və reader's schema

Avro-nun həlli iki schema-nı ayırmaqdır:

  • Writer's schema: data encode olunanda istifadə olunan schema.
  • Reader's schema: decode edən kodun gözlədiyi schema.

Onların eyni olması tələb olunmur, yalnız uyğun olması. Decoder iki schema-nı yan-yana qoyub schema resolution aparır: field-ləri ada görə uyğunlaşdırır, reader-in gözləmədiyi field-ləri ötürür, writer-in yazmadığı field-ləri isə default dəyərlə doldurur. Field-lərin sırası fərqli ola bilər. Uyğunluq qaydası sadədir: yalnız default dəyəri olan field əlavə etmək və ya silmək olar.

Writer's schema-nı necə bilmək isə kontekstdən asılıdır: böyük faylın başında bir dəfə yazılır, database-də hər qeyd schema versiyası nömrəsi ilə saxlanılır, şəbəkə bağlantısında isə handshake zamanı razılaşdırılır. Versiya nömrələrini schema-nın özünə çevirən xidmət schema registry adlanır: Kafka ilə işləyən sistemlərdə geniş istifadə olunur.

Dinamik yaradılan schema-lar

Tag nömrələrinin olmaması Avro-nun əsas üstünlüyünü verir: schema-nı avtomatik yaratmaq rahatdır. Relational cədvəli Avro faylına export edəndə hər sütun field-ə çevrilir. Sütun əlavə olunanda yeni schema yaranır və köhnə fayllar hələ də oxunur, çünki uyğunlaşdırma ada görədir. Thrift və Protocol Buffers-da isə tag-ləri əl ilə təyin etmək və sütun silinəndə onları yenidən istifadə etməmək lazım gəlir.

Schema-ların dəyəri

Binary schema formatlarının JSON qarşısında üstünlükləri qənaətdən daha böyükdür:

  • Field adları olmadığı üçün daha kompaktdırlar.
  • Schema özü sənəddir və kod ilə sinxron qalmağa məcburdur, çünki decode etmək üçün lazımdır.
  • Schema versiyalarını saxlamaq backward və forward compatibility-ni əvvəl yoxlamağa imkan verir.
  • Statik tipli dillərdə generated code compile zamanı yoxlama verir.

Bütün bunlar relational database-lərin schema-on-write ideyasına yaxındır, Fəsil 2-dəki schema-on-read-in çevikliyi isə default dəyərlər və optional field-lər vasitəsilə qorunur.

Dataflow: data prosesdən prosesə necə keçir

Encoding uyğunluğu üç fərqli kontekstdə lazımdır və hər birində çətinlik fərqlidir.

Database vasitəsilə

Database-ə yazan proses encoder, oxuyan proses isə decoder-dir, hətta eyni proses olsa belə: "keçmişdəki sən"dən "gələcəkdəki sən"ə mesaj göndərirsən. Data kodun ömründən uzun yaşayır: beş il əvvəlki qeyd hələ də originaldır, kod isə bu müddətdə onlarla dəfə dəyişib. Database-lər adətən sətirə toxunmadan schema dəyişikliyini qəbul edir: yeni sütun default NULL olur və köhnə sətirlər oxunanda doldurulur.

Bir gizli tələ var. Köhnə kod yeni kodun yazdığı qeydi oxuyur, model obyektinə çevirir, bir field-i dəyişir və geri yazır. Tanımadığı field bu dövrədə itə bilər. Encoding format bunu dəstəkləsə də, application kodu həmin field-i saxladığından əmin olmaq lazımdır.

Arxivləşdirmə, məsələn snapshot çıxarmaq, isə sadədir: bütün data bir anda ən yeni schema ilə encode olunur və immutable qalır. Avro bu halda yaxşı seçimdir.

Service-lər vasitəsilə

Client-server və service-to-service ünsiyyətdə ən geniş yanaşma : HTTP-nin öz mexanizmlərindən istifadə, URL ilə resurs identifikasiyası, JSON body. Alternativ -dir: remote çağırışı lokal funksiya çağırışına bənzətmək cəhdi. Bu bənzətmə zahirən cəlbedicidir, amma fundamental olaraq yanlışdır:

  • Lokal çağırış ya nəticə, ya exception qaytarır. Şəbəkə sorğusu isə cavabsız qala bilər və nə baş verdiyini bilməzsiniz.
  • sonra təkrar cəhd etmək əməliyyatı iki dəfə icra edə bilər. Bunun üçün lazımdır, Fəsil 11-də bu mövzuya qayıdacağıq.
  • Gecikmə lokal çağırışın milyonlarla qatıdır və hər dəfə fərqlidir.
  • Parametrlər pointer deyil, byte-lardır: böyük obyektləri ötürmək bahadır.
  • İki tərəf fərqli dillərdə ola bilər və tip sistemləri üst-üstə düşmür.

Müasir framework-ləri, məsələn , bu fərqi gizlətmir: çağırış asinxron future qaytarır, streaming dəstəklənir, service discovery var. Binary encoding JSON-dan xeyli səmərəlidir, amma debugging rahatlığı, hər dildə hazır client-lər və HTTP infrastrukturu ilə uyğunluğu onu public API-lər üçün default saxlayır. Praktik bölgü: organizasiya daxilində , kənara .

Uyğunluq baxımından ağlabatan fərziyyə belədir: server-lər əvvəl, client-lər sonra yenilənir, deməli request-lər üçün backward, response-lar üçün forward compatibility lazımdır. API versiyalaşdırması, məsələn URL-də /v2/ və ya header-də versiya, bunu açıq idarə etmək üçündür. Client-i sizə aid olmayan public API-də köhnə versiya illərlə yaşaya bilər.

Message passing vasitəsilə

Üçüncü yol asinxron mesajlardır: producer mesajı -ə göndərir, consumer onu sonradan alır. ilə müqayisədə üstünlükləri:

  • Broker buffer kimi işləyir: consumer çökəndə və ya yükləndikdə mesajlar yığılır, itmir.
  • Çökən proses mesajı yenidən çatdırmaqla bərpa olunur.
  • Producer consumer-in ünvanını və sayını bilmir: bir mesajı bir neçə consumer ala bilər.
  • Producer cavab gözləmir, yəni loosely dizayn alınır.

Broker mesajın məzmununu şərh etmir, sadəcə byte ötürür, ona görə istənilən encoding işləyir. Uyğunluq qaydaları isə eynidir: producer və consumer müstəqil olunur və schema evolution üçün yuxarıdakı qaydalar tətbiq olunur. Consumer mesajı başqa topic-ə yenidən publish edirsə, database-dəki eyni tələ qayıdır: naməlum field-lər qorunmalıdır.

Actor model bu ideyanı bir prosesin daxilinə gətirir: hər actor-un öz state-i var və yalnız mesajlarla ünsiyyət qurur. Distributed actor framework-ləri, məsələn Akka və Orleans, eyni abstraksiyanı cluster-ə yayır. Rolling upgrade burada da eyni sualı qaldırır: actor-lar arası mesajların encoding-i backward və forward compatible olmalıdır.

Fəslin nəticəsi

Encoding seçimi görünüşdə texniki detaldır, əslində sistemin təkamül qabiliyyətini müəyyən edir. Schema əsaslı binary formatlar kompakt və uyğunluğu yoxlanıla bilən olduğuna görə organizasiya daxili dataflow üçün yaxşı default-dur, JSON isə kənar interfeyslər üçün qalır. Hər halda forward compatibility üçün naməlum field-ləri qorumaq, backward compatibility üçün default dəyərlər vermək lazımdır. Növbəti fəsillərdə data bir neçə maşına yayılır və hər mesaj bu qaydalara tabe olacaq.

Tapşırıqlar

  1. Aşağıdakı Protocol Buffers schema-sında favorite_number-i int32-dən int64-ə dəyişmək, interests-i silmək və email adlı required field əlavə etmək planlaşdırılır. Hər dəyişiklik üçün backward və forward compatibility-nin pozulub-pozulmadığını, pozulursa köhnə və ya yeni kodda konkret nə baş verəcəyini yazın.
  2. Sizin service-iniz başqa komandanın JSON API-sindən oxuyub nəticəni öz database-inə yazır. Həmin API-yə yeni field əlavə olunanda sizin sistemin hansı nöqtələrində data itə bilər? Hər nöqtə üçün qoruyucu tədbir təklif edin.
  3. Kafka topic-ində Avro ilə encode olunmuş event-lər var. Consumer-lərin hamısı eyni vaxtda yenilənmir. Schema registry-nin uyğunluq rejimini backward, forward və ya full seçməyin producer və consumer sırasına necə təsir etdiyini izah edin.