محمد حسین حاجی وندی
محمد حسین حاجی وندی
خواندن ۹ دقیقه·۲ سال پیش

مقایسه ی Apache Thrift و Protocol Buffers و Apache Avro

این مقاله که در واقع ترجمه و ترکیب چنتا مقاله است که اخیرا خوندم سعی کردم تفاوت هرکدوم از پروتوکل های سریالایز کردن داده ها رو توضیح بدم و تمرکزم هم بیشتر روی سازگاری رو به جلو یا forward compatibility و سازگاری رو به عقب یا backward compatibility بوده است.
تصویر کدگذاری شده
تصویر کدگذاری شده

برای ذخیره سازی یا انتقال داده ها در شبکه راه حل های مختلفی پیشنهاد می شود:

  1. استفاده از امکانات زبان برنامه نویسی برای سریالایز کردن داده مثل serialization در Java یا marshal زبان Ruby یا pickle برای Python
  2. استفاده از فرمت های متنی مثل JSON یا XML
  3. استفاده از فرمت های باینری BSON یا BJSON و …
  4. استفاده از پروتوکل هایی مثل Apache Thrift و Protocol Buffers یا Apache Avro

هرکدام از این روش ها معایب و مزایای خاص خود را دارد. در صورتی که از امکانات زبان برنامه نویسی بخواهید استفاده کنید در این صورت موقع خواندن داده ، مجبورید از همان زبان برنامه نویسی استفاده کنید.

استفاده از JSON وXML با اینکه خیلی پر طرفدار است و تقریبا همه ی زبان های برنامه نویسی از آنها پشتیبانی میکنند، معایبی هم دارند. از جمله اینکه ابهامات زیادی حول سریالایز کردن اعداد در این فرمت ها وجود دارد. مثلا در XML یا CSV هیچ تفاوتی بین اعداد و رشته ها وجود ندارد یا در JSON نمی توانید تفکیکی بین اعداد صحیح و اعشار قائل شوید یا مثلا دقت خاصی را روی اعداد اعشاری اعمال کنید. (منظور این است که در فرآیند سریالایز و دی سریالایز کردن این اتفاق بیفتد چاره ی کار این است که در کد برنامه این موارد را در نظر بگیرید ولی این باعث پیچیدگی کد می شود!)

همچنین اگر داده ای که می خواهید سریالایز کنید زیاد باشد استفاده از فرمت های متنی باعث کندی و سربار حافظه می شود.برای برطرف شدن این مشکل راه حل سوم پیشنهاد می شود.

به جای اینکه داده ها را به صورت متنی ذخیره کنید آن ها را به صورت باینری ذخیره کنید. اما استفاده از فرمت های باینری هم نمی تواند شما را راضی کند چرا؟ چون هیچ اسکیما و مستندی ندارد که شما بتوانید بر اساس آن اشیا و کلاس ها را بسازید. مخصوصا اگر از زبان های برنامه نویسی نوع ایستا استفاده می کنید. همچنین درست است که فرم باینری JSON حجم کمتری از فرم متنی در حافظه می گیرد اما هنوز هم می توان با حذف نام فیلد ها که بسیار تکرار می شود حافظه ی کمتری اشغال کرد.

پس به سراغ استفاده از پروتکل هایی مثل Apache Thrift و Protocol Buffers یا Apache Avro می رویم. این پروتکل ها برای توصیف داده ها از زبان توصیف اسکیما استفاده می کنند. همچنین داده ها را به صورت باینری در حافظه ذخیره می کنند.طوری که در مصرف حافظه بهینه باشد و امکانات چند سکویی را برای سریالایز کردن و دسریالایز کردن داده ها در اختیار کاربران قرار می دهند.

اما سوال اصلی اینجاست که کدام پروتوکل بهترین انتخاب است؟

در ادامه ی این مقاله نگاهی نزدیک تر به هر یک از این سه پروتکل می اندازیم و ویژگی هایشان را باهم مقایسه می کنیم.

معرفی Apache Thrift

Apache Thrift ابتدا در فیسبوک ایجاد شد و هدف از ساخت آن نه فقط فراهم آوردن ویژگی هایی که گفته شد برای سریالایز کردن داده ها بلکه توسعه ی یک چارچوب کامل RPC بود.برای همین Apache Thrift مجموعه ای از پروتکل ها را در اختیار توسعه دهنده ها قرار می دهد. مثلا 2 نوع متفاوت کدگزاری JSON دارد و 3 نوع پروتوکل کدگزاری باینری دارد البته پروتکل DenseProtocol فقط برای زبان ++C پیاده سازی شده است. همه ی پروتکل های کدگذاری از یک زبان مشترک به نام Thrift IDL برای تعریف اسکیما استفاده می کنند مثلا اسکیمای Person به صورت زیر تعریف می شود:

struct Person { 1: string userName, 2: optional i64 favouriteNumber, 3: list<string> interests }

Apache Thrift برای ذخیره کردن داده ها به هر یک از فیلد ها یک تگ اختصاص می دهد و هر تگ و داده ی مربوط به آن به صورت باینری در حاظفه ذخیره می شود که این امکان را به پارسر می دهد که در صورتی که فیلد ناشناخته بود از روی آن عبور کند.

کدگذاری BinaryProtocol خیلی سرراست است اما نسبتا حافظه را به هدر میدهد. برای ذخیره کردن مثال Person در حافظه 59 بایت فضا اشغال می کند.

BinaryProtocol مثال داده ی کدگذاری شد تحت پروتکل
BinaryProtocol مثال داده ی کدگذاری شد تحت پروتکل

کدگذاری CompactProtocol تقریبا مشابه BinaryProtocol اما چون از اعداد صحیح با طول متغیر و bit packing استفاده می کند حجم کمتری از حافظه را اشغال می کند.

CompactProtocol داده ی کدگذاری شد تحت پروتکل
CompactProtocol داده ی کدگذاری شد تحت پروتکل


پروتوکل بافرز یا پروتوباف

لوگوی گوگل پروتوباف
لوگوی گوگل پروتوباف


پروتکل بافر فرمت داده ای متن باز و چند سکویی است که توسط گوگل توسعه پیدا کرده است. مانند Thrift زبان توصیف اسکیمای مختص خودش را دارد و برای مثال Person به صورت زیر است:

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

وقتی که مثال بالا در حاظفه ذخیره شود فقط 33 بایت حافظه اشغال می کند:

کدگذاری پروتوباف
کدگذاری پروتوباف

برخلاف Thrift پروتکل بافر از لیست پشتیبانی نمی کند. برای تعریف فیلد های تکرار پذیر در اسکیما از کلمه ی repeated استفاده می شود. بین فیلد های required و optional و repeated فرقی در پروتوباف وجود ندارد یعنی شما می توانید یک فیلد را از optional به required تغییر بدهید فقط ممکن است موقع پارس کردن داده به خطای موقع اجرا برخورد کنید. مثلا زمانی که ارسال کننده تصور کند فیلد مذکور optional است اما دریافت کننده ی داده فیلد را required ببیند.

فیلد optional بدون مقدار یا فیلد repeated بدون هیچ مقداری در داده ی رمزگذاری شده آورده نمی شود. در این صورت ممکن است توسعه دهنده ی اسکیما بخواهد آن را حذف کند فقط نکته ای که در اینجا وجود دارد این است که بهتر است از شماره ی تگ آن استفاده نشود چون ممکن است داده ی ذخیره شده ای وجود داشته باشد که دارای تگی باشد که حذف شده است و استفاده از همان تگ برای یک فیلد دیگر امن نیست.

اگر شما بخواهید فیلد جدیدی به اسکیما اضافه کنید از منظر سازگاری با کد های قدیمی چه اتفاقی می افتد؟

خوشبختانه مهندسانی که این قرارداد ها را طراحی کردن دقیقا به سازگاری قدیم و جدید توجه کردند. مثلا در پروتوباف اگر فیلد جدید با تگ جدید به اسکیما اضافه شود. پارسر قدیمی آن فیلد را نادیده می گیرد. پارسر قدیمی نمی داند که آن فیلد چیست اما با توجه به اینکه بایت اول آن فیلد نمایانگر اندازه ی داده ی مرتبط با همان فیلد است تشخیص می دهد که از روی چند بایت بعدی باید پرش کند.با توجه به اینکه پروتوباف و Thrift از شماره ی تگ برای ذخیره کردن داده ها استفاده می کنند شما میتوانید نام فیلد را در اسکیما تغییر دهید.

و اما Apache Avro

توسعه ی Apache Avro از سال 2009 به عنوان یکی از زیر پروژه های Apache Hadoop شروع شد. Avro برای توصیف ساختارِ داده از اسکیما استفاده میکند و دارای دو زبان توصیف اسکیما است یکی برای راحتی ویرایش که Avro IDL نام دارد دیگری هم بر اساس JSON است.

لوگوی پروژه ی Apache Avro
لوگوی پروژه ی Apache Avro


مثال Person در زبان Avro IDL به صورت زیر است:

record Person { string userName; union { null, long } favouriteNumber; array<string> interests; }

بازنمایی همین مثال به صورت JSON به شکل زیر می باشد:

{ &quottype&quot: &quotrecord&quot, &quotname&quot: &quotPerson&quot, &quotfields&quot: [ {&quotname&quot: &quotuserName&quot, &quottype&quot: &quotstring&quot}, {&quotname&quot: &quotfavouriteNumber&quot, &quottype&quot: [&quotnull&quot, &quotlong&quot]}, {&quotname&quot: &quotinterests&quot, &quottype&quot: {&quottype&quot: &quotarray&quot, &quotitems&quot: &quotstring&quot}} ] }

وقتی در حافظه ذخیره شود تنها در 32 بایت فضا اشغال می کند.

مثال داده ی کدگذاری شد در Avro
مثال داده ی کدگذاری شد در Avro

در Avro هر فیلد شامل یک بایت اندازه وچند بایت داده است.هیچ چیزی وجود ندارد که مشخص کند یک فیلد string است هم زمان می تواند عدد صحیح یا یک ساختار دیگر باشد. برخلاف پروتوباف یا Thrift هیچ تگی درکار نیست. تنها راهی که بتوان داده را به درستی پارس کرد داشتن اسکیماست. برای خواندن داده دقیقا به همان نسخه ای از اسکیما نیاز است که نویسنده داده را کدگذاری کرده است.

در Avro هر فیلدی یکی پس از دیگری می آید و با توجه به اینکه هیچی تگی درکار نیست پارسر نمیتواند تشخصی دهد که کدام فیلد اختیاری است که از روی آن پرش کند. تنها راهی که وجود دارد استفاده از چیزی شبیه به این موقع تعریف اسکیما است:

union { null, long }

در این صورت پارسر با رسیدن به null می فهمد که باید از آن فیلد پرش کرد.

چه نکاتی را هنگام تغییر در اسکیما در Avro باید رعایت کرد؟

  • وقتی می خواهید یک مقدار جدید به union اضافه کنید ابتدا نسخه ی همه ی reader ها را به روز رسانی کنید سپس با استفاده از writer داده جدید تولید کنید
  • شما می توانید ترتیب فیلد ها را در اسکیما عوض کنید. فیلد ها بر اساس ترتیبی که در اسکیما نوشته می شود کدگذاری شده و خوانده می شود.
  • چون فیلد ها بر اساس نامشان با هم مطابقت پیدا می کنند تغییر نام فیلد ها باید با احتیاط انجام شود. اول باید نام فیلد ها در اسکیمای خواننده ها به روزرسانی شود درحالی که نام های قدیمی به عنوان نام مستعار باقی بمانند. سپس نام های جدید در اسکیمای نویسنده اعمال شود.
  • درصورتی که فیلد جدید به اسکیما افزوده می شود از نوع union باشد که قبلا معرفی شد و مقدار پیشفرض هم داشته باشد زیرا درصوتی که بخواهد داده ی قدیمی هم بخواند فیلد جدید را با مقدار پیشفرض پر کند.
  • با توجه به اینکه تغییرات زیادی در اسکیما ممکن است رخ بدهد بهتر است با داشتن یک مخزن اسکیما نسخه های مختلف اسکیما در دسترس سرویس ها قرار بگیرد.

یکی از مزایای Avro نسبت به پروتوباف و Thrift این است که هنگام ذخیره ی رکورد در حافظه شماره ی تگ در کار نیست اما این چه اهمیتی دارد؟ چون اسکیمای Avro را میتوان به صورت پویا تولید کرد.مثلا فرض کنید که شما می خواهید داده های پایگاه داده را منتقل کنید و برای اینکه در حافظه صرفه جویی کنید نمی خواهید از فرمت های متنی مثل CSV, JOSNو… استفاده کنید و می خواهید از Avro استفاده کنید. به راحتی می توانید اسکیمای Avro را در قالب JOSN از اسکیمای پایگاه داده تولید کنید و داده های پایگاه داده را به فرمت باینری تبدیل کنید. حالا اگر اسکیمای پایگاه داده تغییر کند اسکیمای جدید Avro جایگزین می شود، بدون اینکه تغییری در فرآیند استخراج داده از پایگاه داده رخ دهد.درمقایسه با استفاده از پروتوباف یا Thrift برای همین منظور هر بار که پایگاه داده تغییر می کرد، لازم است شماره ی تگ جدید به صورت دستی تخصیص داده شود.

جمع بندی

پروتکل های کدگذاری داده ها با این هدف طراحی شدند که هم در میزان مصرف حاظفه صرفه جویی شود هم با دراختیار قرار دادن امکانات مختلف به توسعه دهندگان اطمینان بدهند که در آینده اگر تغییراتی در اسکیما رخ داد، می توانند داده های قدیمی خود را هم بدون مشکل بخوانند. اگر چه راحتی استفاده از این امکانات در ابزار های مختلف متفاوت است. همانطور که در این مقاله اشاره شد استفاده از Thrift و پروتوباف در مقابل Avro سرراست تر است اما برخی مواقع نیازمند پویایی بیشتری هستیم که Avro در اختیار توسعه دهندگان قرار می دهد.



منابع مورد استفاده برای نوشتن این مقاله به شرح زیر است:

  1. https://martin.kleppmann.com/2012/12/05/schema-evolution-in-avro-protocol-buffers-thrift.html
  2. http://radar.oreilly.com/2014/11/the-problem-of-managing-schemas.html
  3. https://www.confluent.io/en-gb/blog/avro-kafka-data/



مهندس نرم افزار
شاید از این پست‌ها خوشتان بیاید