مهران علم بیگی - زمستان ۱۴۰۴


مفهوم Over-fetching به وضعیتی گفته می‌شود که یک API داده‌ای بیش از نیاز واقعی کلاینت برمی‌گرداند. این مشکل به‌طور کلاسیک در APIهایی با معماری REST دیده می‌شود، زیرا در REST سرور تعیین می‌کند چه ساختاری از داده برای هر endpoint بازگردانده شود و کلاینت کنترلی روی انتخاب فیلدها ندارد. برای مثال اگر کلاینت فقط نام یک کاربر را با دانستن id او نیاز داشته باشد، با فراخوانی endpointای مثل ‎/user/1‎ معمولاً کل آبجکت کاربر شامل ایمیل، شماره تماس، آدرس، تاریخ تولد و وضعیت فعال بودن نیز بازگردانده می‌شود، در حالی که این داده‌های اضافی نه‌تنها استفاده نمی‌شوند، بلکه باعث افزایش حجم پاسخ، مصرف پهنای باند و پردازش غیرضروری می‌شوند. علت ریشه‌ای این مشکل در REST این است که endpointها ثابت و resource-based هستند و پاسخ‌ها از پیش توسط سرور طراحی شده‌اند.

معماری GraphQL برای حل مستقیم این مشکل معرفی شد. در GraphQL کلاینت مشخص می‌کند دقیقاً چه فیلدهایی را نیاز دارد و سرور فقط همان‌ها را برمی‌گرداند. در نتیجه over-fetching و حتی under-fetching حذف می‌شود، چون پاسخ دقیقاً مطابق با query تعریف‌شده توسط کلاینت است. این تفاوت ناشی از تغییر فلسفه معماری است؛ REST حول resource می‌چرخد، در حالی که GraphQL حول query و نیاز واقعی کلاینت طراحی شده است.

در معماری gRPC مسئله از زاویه‌ای متفاوت حل می‌شود. gRPC که مخفف Google Remote Procedure Call است، یک فریم‌ورک ارتباطی high-performance محسوب می‌شود که اجازه می‌دهد یک سرویس، توابع سرویس دیگر را به‌گونه‌ای صدا بزند که گویی یک تابع لوکال است. برخلاف REST که مبتنی بر JSON و HTTP/1.1 است، gRPC از داده‌های باینری و پروتکل Protobuf برای serialization استفاده می‌کند و روی HTTP/2 سوار می‌شود. این انتخاب باعث کاهش شدید حجم پیام‌ها، سرعت بالاتر پردازش و ارتباط کارآمدتر بین سرویس‌ها می‌شود.

معماری gRPC تنها به مدل request-response محدود نیست و چهار الگوی ارتباطی را پشتیبانی می‌کند: حالت Unary که مشابه request-response کلاسیک است، Server Streaming که در آن کلاینت یک درخواست می‌فرستد و سرور چند پاسخ پشت‌سرهم ارسال می‌کند، Client Streaming که کلاینت داده را به‌صورت chunkهای متوالی می‌فرستد و در نهایت یک پاسخ دریافت می‌کند، و Bidirectional Streaming که ارتباطی دوطرفه و مداوم بین کلاینت و سرور برقرار می‌کند. این مدل‌ها gRPC را برای سیستم‌های real-time و microserviceهای پیچیده بسیار مناسب می‌سازند.

دلیل اینکه gRPC برای معماری Microservices گزینه‌ای ایده‌آل محسوب می‌شود، مجموعه‌ای از ویژگی‌هاست: performance بسیار بالا، داشتن قرارداد مشخص، strongly typed بودن، پشتیبانی از streaming و مستقل بودن از زبان برنامه‌نویسی. یک سرویس نوشته‌شده با Go می‌تواند بدون هیچ وابستگی خاصی سرویس Python را صدا بزند، زیرا ارتباط آن‌ها مبتنی بر باینری و یک قرارداد مشترک است، نه وابستگی زبانی.

هسته‌ی این استقلال، مفهوم قرارداد در gRPC است. gRPC به‌جای وابستگی به زبان، به یک قرارداد مستقل از زبان متکی است که با IDL یا Interface Definition Language تعریف می‌شود. این قرارداد در قالب فایل‌های ‎.proto‎ نوشته می‌شود که نه Go هستند، نه Python و نه Java، بلکه یک تعریف خالص از سرویس‌ها، متدها و ساختار داده‌ها محسوب می‌شوند. بر اساس همین قرارداد، gRPC برای هر زبان به‌صورت خودکار کد تولید می‌کند و stubهای لازم را می‌سازد، به‌طوری که در Go متدی مثل client.GetUser و در Python متدی متناظر با همان امضا وجود دارد، اما هر دو دقیقاً یک قرارداد، یک شماره فیلد و یک نوع داده را پیاده‌سازی می‌کنند. در نهایت تمام زبان‌ها داده را با Protobuf و روی HTTP/2 منتقل می‌کنند و هیچ‌کدام نیازی به شناخت زبان طرف مقابل ندارند.

معماری RPC به‌صورت کلاسیک به معنای Remote Procedure Call است، یعنی صدا زدن یک تابع روی یک ماشین دیگر به‌گونه‌ای که انگار تابع لوکال است. این مفهوم شامل شبکه، serialization، لایه انتقال و deserialization می‌شود. RPC را می‌توان یک معماری دانست، اما با REST تفاوت ماهوی دارد؛ REST یک معماری resource-oriented است، در حالی که RPC procedure-oriented بوده و تمرکز آن روی عملیات و متدهاست نه روی resourceها. RPCهای قدیمی مثل Java RMI یا .NET Remoting زبان‌وابسته بودند، چون فرمت داده و پروتکل استاندارد بین‌زبانی نداشتند. gRPC با سه تصمیم کلیدی این مشکل را حل کرد: تعریف قرارداد مستقل از زبان، تولید کد خودکار برای هر زبان، و استفاده از پروتکل استاندارد HTTP/2 به‌همراه Protobuf.

در مقابل، REST ذاتاً stateless است، معمولاً بر پایه HTTP/1.1 پیاده‌سازی می‌شود و از مفاهیمی مثل URI، URL و عملیات CRUD استفاده می‌کند. محدودیت‌های HTTP/1.1 نقش مهمی در ضعف‌های REST سنتی دارند. در HTTP/1.1 درخواست‌ها به‌صورت خطی پردازش می‌شوند و تا پاسخ یک درخواست نیاید، درخواست بعدی عملاً بلاک می‌شود که به آن Head-of-Line Blocking گفته می‌شود. برای دور زدن این مشکل، مرورگرها مجبور به باز کردن چندین اتصال TCP موازی می‌شوند که هزینه‌ی handshake، مصرف حافظه و latency را افزایش می‌دهد. علاوه بر این، headerها در هر درخواست به‌صورت کامل و تکراری ارسال می‌شوند و امکان server push نیز وجود ندارد.

متد HTTP/2 این مشکلات را در سطح پروتکل حل کرد. مهم‌ترین تغییر آن multiplexing است؛ یعنی تمام درخواست‌ها و پاسخ‌ها روی یک اتصال TCP واحد و در قالب streamهای مستقل و همزمان منتقل می‌شوند، بدون بلاک شدن. HTTP/2 مبتنی بر binary framing است که پردازش سریع‌تر و خطای کمتر نسبت به متن دارد. همچنین با HPACK، headerها فشرده می‌شوند و فقط تغییرات ارسال می‌گردند. قابلیت server push نیز به سرور اجازه می‌دهد منابع موردنیاز کلاینت را قبل از درخواست صریح ارسال کند. در نتیجه، به‌جای چندین اتصال TCP با latency بالا، تنها یک اتصال با handshake واحد و سرعت بیشتر برقرار می‌شود. در یک مثال واقعی مثل بارگذاری یک صفحه وب با HTML، CSS و JavaScript، HTTP/1.1 مجبور به ارسال درخواست‌های پشت‌سرهم است، در حالی که HTTP/2 همه‌ی این منابع را به‌صورت همزمان و روی یک connection منتقل می‌کند که نتیجه‌ی آن عملکرد به‌مراتب بهتر است.

منظور از Handshake چیست؟

مبحث Handshake به فرآیندی گفته می‌شود که در ابتدای برقراری یک ارتباط شبکه‌ای بین دو طرف انجام می‌شود تا قبل از شروع تبادل داده‌ی اصلی، هر دو سمت روی نحوه‌ی ارتباط به توافق برسند. در این مرحله، کلاینت و سرور یک‌سری پیام اولیه رد و بدل می‌کنند تا مشخص شود آیا می‌توانند با هم صحبت کنند، از چه پروتکلی استفاده کنند، چه پارامترهایی فعال باشد و آیا ارتباط امن است یا نه. تا زمانی که handshake کامل نشود، هیچ داده‌ی واقعی و کاربردی منتقل نمی‌شود.

در ساده‌ترین حالت، مثل TCP handshake، این فرآیند شامل سه مرحله است. ابتدا کلاینت یک پیام SYN به سرور می‌فرستد و اعلام می‌کند که قصد برقراری ارتباط دارد. سرور در پاسخ، پیام SYN-ACK را ارسال می‌کند که هم دریافت درخواست را تأیید می‌کند و هم آمادگی خود را برای ارتباط اعلام می‌کند. در نهایت کلاینت با ارسال ACK اتصال را نهایی می‌کند. بعد از این سه پیام، اتصال TCP برقرار شده و دو طرف می‌توانند شروع به ارسال داده کنند. این رفت‌وبرگشت اولیه هزینه‌ی زمانی دارد که به آن handshake cost گفته می‌شود.

اگر ارتباط امن باشد، مثلاً در HTTPS، علاوه بر TCP handshake، یک TLS handshake هم انجام می‌شود. در TLS handshake دو طرف روی نسخه‌ی پروتکل امنیتی، الگوریتم‌های رمزنگاری، کلیدها و گواهی‌های دیجیتال به توافق می‌رسند. این مرحله پیام‌های بیشتری نسبت به TCP دارد و زمان بیشتری هم مصرف می‌کند، اما نتیجه‌ی آن یک کانال امن است که داده‌ها به‌صورت رمزنگاری‌شده منتقل می‌شوند.