این مقاله که در واقع ترجمه و ترکیب چنتا مقاله است که اخیرا خوندم سعی کردم تفاوت هرکدوم از پروتوکل های سریالایز کردن داده ها رو توضیح بدم و تمرکزم هم بیشتر روی سازگاری رو به جلو یا forward compatibility و سازگاری رو به عقب یا backward compatibility بوده است.
برای ذخیره سازی یا انتقال داده ها در شبکه راه حل های مختلفی پیشنهاد می شود:
هرکدام از این روش ها معایب و مزایای خاص خود را دارد. در صورتی که از امکانات زبان برنامه نویسی بخواهید استفاده کنید در این صورت موقع خواندن داده ، مجبورید از همان زبان برنامه نویسی استفاده کنید.
استفاده از JSON وXML با اینکه خیلی پر طرفدار است و تقریبا همه ی زبان های برنامه نویسی از آنها پشتیبانی میکنند، معایبی هم دارند. از جمله اینکه ابهامات زیادی حول سریالایز کردن اعداد در این فرمت ها وجود دارد. مثلا در XML یا CSV هیچ تفاوتی بین اعداد و رشته ها وجود ندارد یا در JSON نمی توانید تفکیکی بین اعداد صحیح و اعشار قائل شوید یا مثلا دقت خاصی را روی اعداد اعشاری اعمال کنید. (منظور این است که در فرآیند سریالایز و دی سریالایز کردن این اتفاق بیفتد چاره ی کار این است که در کد برنامه این موارد را در نظر بگیرید ولی این باعث پیچیدگی کد می شود!)
همچنین اگر داده ای که می خواهید سریالایز کنید زیاد باشد استفاده از فرمت های متنی باعث کندی و سربار حافظه می شود.برای برطرف شدن این مشکل راه حل سوم پیشنهاد می شود.
به جای اینکه داده ها را به صورت متنی ذخیره کنید آن ها را به صورت باینری ذخیره کنید. اما استفاده از فرمت های باینری هم نمی تواند شما را راضی کند چرا؟ چون هیچ اسکیما و مستندی ندارد که شما بتوانید بر اساس آن اشیا و کلاس ها را بسازید. مخصوصا اگر از زبان های برنامه نویسی نوع ایستا استفاده می کنید. همچنین درست است که فرم باینری JSON حجم کمتری از فرم متنی در حافظه می گیرد اما هنوز هم می توان با حذف نام فیلد ها که بسیار تکرار می شود حافظه ی کمتری اشغال کرد.
پس به سراغ استفاده از پروتکل هایی مثل Apache Thrift و Protocol Buffers یا Apache Avro می رویم. این پروتکل ها برای توصیف داده ها از زبان توصیف اسکیما استفاده می کنند. همچنین داده ها را به صورت باینری در حافظه ذخیره می کنند.طوری که در مصرف حافظه بهینه باشد و امکانات چند سکویی را برای سریالایز کردن و دسریالایز کردن داده ها در اختیار کاربران قرار می دهند.
اما سوال اصلی اینجاست که کدام پروتوکل بهترین انتخاب است؟
در ادامه ی این مقاله نگاهی نزدیک تر به هر یک از این سه پروتکل می اندازیم و ویژگی هایشان را باهم مقایسه می کنیم.
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 بایت فضا اشغال می کند.
کدگذاری CompactProtocol تقریبا مشابه BinaryProtocol اما چون از اعداد صحیح با طول متغیر و bit packing استفاده می کند حجم کمتری از حافظه را اشغال می کند.
پروتکل بافر فرمت داده ای متن باز و چند سکویی است که توسط گوگل توسعه پیدا کرده است. مانند 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 از سال 2009 به عنوان یکی از زیر پروژه های Apache Hadoop شروع شد. Avro برای توصیف ساختارِ داده از اسکیما استفاده میکند و دارای دو زبان توصیف اسکیما است یکی برای راحتی ویرایش که Avro IDL نام دارد دیگری هم بر اساس JSON است.
مثال Person در زبان Avro IDL به صورت زیر است:
record Person { string userName; union { null, long } favouriteNumber; array<string> interests; }
بازنمایی همین مثال به صورت JSON به شکل زیر می باشد:
{ "type": "record", "name": "Person", "fields": [ {"name": "userName", "type": "string"}, {"name": "favouriteNumber", "type": ["null", "long"]}, {"name": "interests", "type": {"type": "array", "items": "string"}} ] }
وقتی در حافظه ذخیره شود تنها در 32 بایت فضا اشغال می کند.
در Avro هر فیلد شامل یک بایت اندازه وچند بایت داده است.هیچ چیزی وجود ندارد که مشخص کند یک فیلد string است هم زمان می تواند عدد صحیح یا یک ساختار دیگر باشد. برخلاف پروتوباف یا Thrift هیچ تگی درکار نیست. تنها راهی که بتوان داده را به درستی پارس کرد داشتن اسکیماست. برای خواندن داده دقیقا به همان نسخه ای از اسکیما نیاز است که نویسنده داده را کدگذاری کرده است.
در Avro هر فیلدی یکی پس از دیگری می آید و با توجه به اینکه هیچی تگی درکار نیست پارسر نمیتواند تشخصی دهد که کدام فیلد اختیاری است که از روی آن پرش کند. تنها راهی که وجود دارد استفاده از چیزی شبیه به این موقع تعریف اسکیما است:
union { null, long }
در این صورت پارسر با رسیدن به null می فهمد که باید از آن فیلد پرش کرد.
چه نکاتی را هنگام تغییر در اسکیما در Avro باید رعایت کرد؟
یکی از مزایای Avro نسبت به پروتوباف و Thrift این است که هنگام ذخیره ی رکورد در حافظه شماره ی تگ در کار نیست اما این چه اهمیتی دارد؟ چون اسکیمای Avro را میتوان به صورت پویا تولید کرد.مثلا فرض کنید که شما می خواهید داده های پایگاه داده را منتقل کنید و برای اینکه در حافظه صرفه جویی کنید نمی خواهید از فرمت های متنی مثل CSV, JOSNو… استفاده کنید و می خواهید از Avro استفاده کنید. به راحتی می توانید اسکیمای Avro را در قالب JOSN از اسکیمای پایگاه داده تولید کنید و داده های پایگاه داده را به فرمت باینری تبدیل کنید. حالا اگر اسکیمای پایگاه داده تغییر کند اسکیمای جدید Avro جایگزین می شود، بدون اینکه تغییری در فرآیند استخراج داده از پایگاه داده رخ دهد.درمقایسه با استفاده از پروتوباف یا Thrift برای همین منظور هر بار که پایگاه داده تغییر می کرد، لازم است شماره ی تگ جدید به صورت دستی تخصیص داده شود.
پروتکل های کدگذاری داده ها با این هدف طراحی شدند که هم در میزان مصرف حاظفه صرفه جویی شود هم با دراختیار قرار دادن امکانات مختلف به توسعه دهندگان اطمینان بدهند که در آینده اگر تغییراتی در اسکیما رخ داد، می توانند داده های قدیمی خود را هم بدون مشکل بخوانند. اگر چه راحتی استفاده از این امکانات در ابزار های مختلف متفاوت است. همانطور که در این مقاله اشاره شد استفاده از Thrift و پروتوباف در مقابل Avro سرراست تر است اما برخی مواقع نیازمند پویایی بیشتری هستیم که Avro در اختیار توسعه دهندگان قرار می دهد.
منابع مورد استفاده برای نوشتن این مقاله به شرح زیر است: