مزرعه پنبه سلیب


سلیب پس از برگزاری موفقیت‌آمیز مجموعه مسابقات کداستار، دوباره با ممزواد (Mamzavad)، عنصر فتنه‌ی کوئرا به مشکلات جدی‌ای بر میخورد و از آن‌جایی که قدرت ممزواد در کوئرا خیلی بیشتر از سلیب است، او ناچار می‌شود تا برای اتمام درگیری در کوئرا، پس از اتمام مسابقات کداستار به مزرعه پنبه‌اش پناه برده و به کشت و برداشت پنبه مشغول شود!

از آن‌جایی که سلیب علاوه بر فردی بسیار بانظم بودن، به اسکریپت‌های Lua و همچنین ردیس کلاستر (Redis Cluster) بسیار علاقه‌مند است، از شما که تنها فرد مورد اعتماد باقی مانده برای او هستید، می‌خواهد تا مدیریت کشت و برداشت پنبه در مزرعه‌اش را با استفاده از دو Endpoint، /get و /set که توسط یک سرویس OpenResty و اسکریپت‌های Lua پیاده‌سازی خواهند شد، مدیریت کنید. وظیفه شما در این سوال پیاده‌سازی بخشی از این سیستم مدیریت پنبه‌های سلیب با استفاده از اسکریپت‌های Lua، کانفیگ فایل nginx.conf و نوشتن فایل docker-compose.yml برای راه‌اندازی سرویس‌های مورد نیاز مزرعه پنبه می‌باشد.

تصویر سوال پنجم

پنبه‌های سلیب به صورت مقادیر کلید-مقدار هستند که شما باید آن‌ها را در یک ردیس کلاستر با ۳ عدد نود (Node) ذخیره‌سازی کنید. سلیب می‌خواهد تا برای بالانس لود در نود‌های ردیسی، هر بار کلید‌های پنبه‌هایش را با استفاده از الگوریتم هش FNV-1a هش کرده و به یکی از شاردهای shard1, shard2, shard3 اختصاص دهد که هر کدام از ۳ نود ردیسی (یا Replica آن‌ها) می‌باشد.

پروژه اولیه🔗

برای دانلود پروژه‌ی اولیه روی این لینک کلیک کنید.

ساختار فایل‌ها
├── docker-compose.yml
├── nginx
│   ├── lua
│   └── nginx.conf
Plain text

برای راه‌اندازی پروژه، از یک فایل docker-compose.yml استفاده خواهد شد، که شامل چند سرویس مختلف برای مدیریت داده‌های پنبه‌های مزرعه سلیب با استفاده از Redis و OpenResty می‌باشد.

جزئیات پروژه🔗

پیاده‌سازی فایل کانفیگ nginx.confو اسکریپت‌های Lua، منطق مزرعه پنبه!

در این سوال، شما وظیفه دارید تا با استفاده از OpenResty، که یک سیستم مبتنی بر Nginx با پشتیبانی از Lua پیاده‌سازی کنید. این سیستم باید شامل پیکربندی کامل سرور Nginx در مسیر nginx/nginx.conf و همچنین اسکریپت‌های Lua در مسیر پوشه nginx/lua باشد که منطق مورد نیاز برای مدیریت درخواست‌ها و ارتباط با کلاستر ردیسی را اجرا می‌کنند.

  • لازم است فایل پیکربندی nginx.conf را به‌صورت کامل بنویسید. این فایل باید شامل تنظیمات مربوط به بارگذاری Lua، تعریف مسیرهای مورد نیاز (مانند /get و /set)، و پیکربندی منابع اشتراکی (مثل lua_shared_dict) باشد. توجه داشته باشید که تنها دو مسیر (Route) get و set مجاز خواهند بود و Nginx باید به ازای باقی مسیر‌ها خطای 404 نمایش دهد.

  • اسکریپت‌های مربوط به منطق برنامه‌نویسی باید در مسیر nginx/lua/ قرار داده شوند. این اسکریپت‌ها ممکن است شامل ماژول‌هایی برای موارد زیر باشند، البته توجه داشته باشید که مدیریت چگونگی نحوه‌ پیاده‌سازی اجزای مختلف بر عهده‌ی شما می‌باشد:

  • اتصال به کلاستر ردیس (شامل مدیریت Shard، Fallback و Health check)

  • اعمال محدودیت نرخ (Rate limiting)

  • خواندن و نوشتن کلیدها با استفاده از GET و SET

  • تولید پاسخ در قالب JSON و هندل کردن خطاها

ساختار پروژه شما باید به گونه‌ای باشد که امکان اجرای کامل سیستم با راه‌اندازی Nginx فراهم شود. توجه داشته باشید که تمامی منطق سمت سرور باید به کمک Lua و از طریق OpenResty اجرا شود و اجازه استفاده از سرورهای خارجی یا زبان‌های دیگر وجود ندارد.

در ادامه به بررسی هر کدام از موارد ذکر شده به صورت مفصل‌تر همراه با مثال خواهیم پرداخت.

پیاده‌سازی فایل docker-compose.yml، زیرساخت مزرعه پنبه!

سرویس OpenResty🔗

با استفاده از ایمیج زیر یک سرویس با نام openresty ایجاد کنید:

registry.gitlab.com/qio/standard/openresty:1.19.9.1-5-alpine-fat
Plain text
  • این سرویس برای اجرای کدهای Lua و مدیریت درخواست‌ها با استفاده از Nginx و OpenResty استفاده خواهد شد.

  • پورت 8080 باید از میزبان به کانتینر متصل شود.

  • فایل کانفیگ Nginx در مسیر nginx/nginx.conf و اسکریپت‌های پیاده‌سازی شده در پوشه nginx/lua را به مسیر‌های مرتبط در سرویس openresty متصل کنید.

  • این سرویس به تمامی نود‌های کلاستر ردیسی وابسته است!

سرویس‌های Redis🔗

مجموعاً ۶ سرویس Redis در این پروژه تعریف شده‌اند:

  • سه نود Redis اصلی (redis1, redis2, redis3)

  • سه نود Redis به عنوان Replica (redis1r, redis2r, redis3r)

همه‌ی این سرویس‌ها از ایمیج زیر استفاده می‌کنند:

registry.gitlab.com/qio/standard/redis:latest
Plain text
  • این سرویس‌ها برای ذخیره‌سازی داده‌ها در یک ساختار توزیع‌شده Redis استفاده می‌شوند.

نکاتی در مورد پیاده‌سازی فایل docker-compose.yml🔗

  • توجه: شما اجازه‌ی build کردن یک ایمیج جدید نخواهید داشت و باید از ایمیج‌های استاندارد کوئرا استفاده کنید.

  • توجه: نام کانتینر‌ها و سرویس‌ها باید دقیقا عبارات گفته شده در متن سوال باشند.

  • توجه: سیستم داوری کوئرا به‌صورت خودکار فایل docker-compose.yml را با کامند up اجرا می‌کند. شما نیازی به کد یا اسکریپتی برای اجرای این کار ندارید.

  • توجه: ورژن docker-compose.yml باید 3.3 باشد.

  • توجه: تنها فایل‌ docker-compose.yml شما در سیستم داوری مورد پذیرش قرار خواهد گرفت و سایر تغییرات در سایر فایل‌ها بی‌تاثیر خواهند بود. همچنین شما باید حتما تنها از ایمیج‌های معرفی شده در متن سوال استفاده کنید. استفاده از سایر ایمیج‌ها باعث عدم داوری ارسال شما خواهد شد.

extensionFromNamedocker-compose.yml
version: "3.3"

# Do not forget that the only available 
# openresty image is accessible with the following url:
# registry.gitlab.com/qio/standard/openresty:1.19.9.1-5-alpine-fat

# Do not forget that the only available 
# redis image is accessible with the following url:
# registry.gitlab.com/qio/standard/redis:latest
YAML
پیاده‌سازی Setter برای کشت پنبه!

در این بخش، سلیب قرار است تا امکان ذخیره مقدار (Value) جدید پنبه برای یک کلید (Key) دلخواه را فراهم کند. ابتدا بررسی می‌شود آیا کلید (key) و مقدار (value) در Query String درخواست وجود دارد یا خیر. سپس با استفاده از تابع هش گفته شده، مشخص می‌شود کلید در کدام شارد (Shard) کلاستر ردیسی (Redis Cluster) قرار دارد. شارد هر کلید keykey به این صورت مشخص خواهد شد:

FNV1a(key)mod3+1FNV-1a(key) mod 3 + 1

  • طبق رابطه بالا، پس از محاسبه هش کلید key باقی‌مانده آن را بر ۳ محاسبه کرده و یکی شیفت می‌دهیم تا شارد (Shard) نهایی‌اش محاسبه شود.

پس از محاسبه شارد کلید مورد نظر و افزودن مقدار آن کلید در نود مورد مشخص شده (یا Replica ی آن)، در صورتی که این ذخیره‌سازی موفق بود، خروجی باید یک JSON شامل وضعیت (status) موفقیت (OKکلید (keyمقدار (value) ذخیره‌شده و نام شارد (shard) باشد.

اگر مقدار یا کلید ارسال نشده باشند، باید خطایی با کد 400 به همراه پیام مناسب نمایش داده شود. اگر عملیات SET شکست بخورد (مثلاً ردیسی در دسترس نباشد)، باید خطای 500 به همراه پیام مناسب بازگردانده می‌شود.

همانطور که پیش‌تر توضیح داده شد، در مسیر /set، درخواست‌هایی برای ثبت مقدار (value) برای یک کلید (key) خاص ارسال می‌شوند. این بخش به طور کلی شامل مراحل زیر است:

مراحل پیاده‌سازی🔗

  • بررسی وجود key و value در Query String.

  • محاسبه هش FNV-1a روی key برای تعیین شارد کلید (یکی از shard1, shard2, shard3).

  • تلاش برای اتصال به نود اصلی (Primary Node) ردیس مربوطه.

  • تلاش برای اتصال به نود Replica ی شارد مورد نظر در صورت در دسترس نبود نود اصلی، مثلا در صورتی که شارد کلیدی shard1 باشد، سرویس OpenResty باید سعی کند تا به نود ردیسی redis1 متصل شود و در صورت شکست، به نود Replica ی آن یعنی redis1r متصل شود.

  • ذخیره‌سازی مقدار مورد نظر برای کلید در دیتابیس ردیسی

  • بازگرداندن JSON با کد 200 شامل اطلاعات key, value, shard, status.

مثال‌ها🔗

درخواست‌های موفق برای ثبت مقدار جدید🔗

extensionFromNameterminal
curl "http://localhost:8080/set?key=key3&value=value3"
Bash
extensionFromNameoutput
{
  "key": "key3",
  "value": "value3",
  "status": "OK",
  "shard": "shard1"
}
JSON
extensionFromNameterminal
curl "http://localhost:8080/set?key=key1&value=value1"
Bash
extensionFromNameoutput
{
  "key": "key2",
  "value": "value2",
  "status": "OK",
  "shard": "shard2"
}
JSON
extensionFromNameterminal
curl "http://localhost:8080/set?key=key6&value=value6"
Bash
extensionFromNameoutput
{
  "key": "key6",
  "value": "value6",
  "status": "OK",
  "shard": "shard3"
}
JSON

درخواست‌های ‌نا‌موفق برای ثبت مقدار جدید، بدون کلید و مقدار🔗

extensionFromNameterminal
curl "http://localhost:8080/set?value=blue"
Bash
extensionFromNameoutput
{
  "error": "Missing 'key'"
}
JSON
extensionFromNameterminal
curl "http://localhost:8080/set?key=color"
Bash
extensionFromNameoutput
{
  "error": "Missing 'value'"
}
JSON

درخواست‌ ‌نا‌موفق برای ثبت مقدار جدید، در دسترس نبودن همزمان نود اصلی و Replica🔗

extensionFromNameterminal
curl "http://localhost:8080/set?key=color&value=green"
Bash
extensionFromNameoutput
{
  "error": "Redis unavailable"
}
JSON
توضیحات کد وضعیت خروجی نمونه
ارسال درست مقدار و کلید 200 { "key": "key3", "value": "value3", "status": "OK", "shard": "shard1" }
ارسال بدون key 400 { "error": "Missing 'key'" }
ارسال بدون value 400 { "error": "Missing 'value'" }
عدم دسترسی به نود اصلی و replica 500 { "error": "Redis unavailable" }
پیاده‌سازی Getter برای برداشت پنبه!

در این بخش، سلیب باید قادر باشد مقدار ذخیره‌شده‌ای را با استفاده از کلید (key) از کلاستر ردیس دریافت کند. ابتدا بررسی می‌شود آیا کلید در Query String درخواست وجود دارد یا خیر. سپس با استفاده از تابع هش گفته شده، مشخص می‌شود این کلید در کدام شارد (Shard) از کلاستر ردیسی قرار دارد. برای تعیین شارد، از رابطه زیر استفاده می‌شود:

FNV1a(key)mod3+1FNV-1a(key) mod 3 + 1

  • پس از محاسبه هش key، باقی‌مانده تقسیم بر ۳ محاسبه شده و با ۱ جمع می‌شود تا شماره شارد (shard1, shard2, shard3) مشخص گردد.

  • تلاش می‌شود تا به نود اصلی (Primary) شارد مربوطه متصل شویم.

  • اگر نود اصلی در دسترس نبود، اتصال به Replica صورت می‌گیرد.

  • سپس دستور GET روی کلید اجرا می‌شود.

  • اگر مقدار یافت نشد (null)، فیلد found باید مقدار false داشته باشد.

  • در صورت موفقیت، خروجی شامل مقادیر key, value, found, shard در قالب JSON و کد وضعیت 200 خواهد بود.

  • در صورت ارسال نشدن کلید، کد خطای 400 با پیام مناسب بازگردانده می‌شود.

  • در صورت عدم در دسترس بودن هیچ نودی، کد 500 به همراه پیام مناسب بازگردانده می‌شود.

مراحل پیاده‌سازی🔗

  • بررسی وجود key در Query String.

  • محاسبه هش FNV-1a روی key برای تعیین شارد کلید

  • تلاش برای اتصال به نود اصلی شارد مربوطه.

  • تلاش برای اتصال به Replica در صورت عدم موفقیت در اتصال به نود اصلی.

  • دریافت مقدار کلید از دیتابیس ردیسی

  • بازگرداندن نتیجه در قالب JSON همراه با وضعیت 200.

مثال‌ها🔗

درخواست‌های موفق برای دریافت مقدار کلید🔗

extensionFromNameterminal
curl "http://localhost:8080/get?key=key1"
Bash
extensionFromNameoutput
{
  "key": "key1",
  "found": true,
  "value": "value1",
  "shard": "shard2"
}
JSON
extensionFromNameterminal
curl "http://localhost:8080/get?key=key6"
Bash
extensionFromNameoutput
{
  "key": "key6",
  "found": true,
  "value": "value6",
  "shard": "shard3"
}
JSON

درخواست‌ ناموفق بدون کلید🔗

extensionFromNameterminal
curl "http://localhost:8080/get"
Bash
extensionFromNameoutput
{
  "error": "Missing 'key'"
}
JSON

درخواست برای کلید ناموجود🔗

extensionFromNameterminal
curl "http://localhost:8080/get?key=unknown"
Bash
extensionFromNameoutput
{
  "key": "unknown",
  "found": false,
  "value": null,
  "shard": "shard2"
}
JSON

درخواست‌ ‌ناموفق به دلیل عدم دسترسی به ردیس🔗

extensionFromNameterminal
curl "http://localhost:8080/get?key=key9"
Bash
extensionFromNameoutput
{
  "error": "Redis unavailable"
}
JSON
توضیحات کد وضعیت خروجی نمونه
دریافت مقدار موجود 200 { "key": "key1", "found": true, "value": "value1", "shard": "shard2" }
عدم ارسال کلید 400 { "error": "Missing 'key'" }
دریافت کلیدی که وجود ندارد 200 { "key": "unknown", "found": false, "value": null, "shard": "shard3" }
عدم دسترسی به نود اصلی و replica 500 { "error": "Redis unavailable" }
پیاده‌سازی Rate Limiter، برای محافظت مزرعه!

برای محافظت از منابع سیستم در برابر حملات و بار بیش از حد، سلیب باید یک محدود‌کننده نرخ درخواست (Rate Limiter) بر پایه‌ی IP کلاینت پیاده‌سازی کند.

مراحل پیاده‌سازی🔗

  • در هر بازه ۶۰ ثانیه‌ای، حداکثر ۶۰ درخواست از یک IP مجاز است.

  • اگر تعداد خطا‌ها از این حد بیشتر شود، کاربر باید با خطای 429 Too Many Requests مواجه شده و از ادامه فعالیت او برای مدت زمان ۱۵ ثانیه‌ای، جلوگیری به عمل آید.

مثال‌ها🔗

درخواست بیش از حد مجاز🔗

extensionFromNameterminal
curl "http://localhost:8080/get?key=key1"
Bash
extensionFromNameoutput
{
  "error": "Rate limit exceeded"
}
JSON
توضیحات کد وضعیت خروجی نمونه
درخواست در محدودۀ مجاز 200 { "key": "test", "value": "val", "status": "OK", "shard": "shard1" }
عبور از حد مجاز درخواست 429 { "error": "Rate limit exceeded" }
پیاده‌سازی Fallback، برای پشتیبانی مزرعه!

برای اطمینان از پایداری بالا و جلوگیری از قطع خدمات در زمان از کار افتادن برخی نودهای ردیسی، سلیب باید از مکانیزم (Fallback) استفاده کند.

  • در زمان پردازش درخواست، اگر نود اصلی سالم باشد، به آن متصل می‌شویم. در غیر این‌صورت، به نود Replica متصل می‌شویم.
  • اگر هیچ‌کدام در دسترس نبودند، پاسخ با خطای 500 و پیام Redis unavailable بازگردانده می‌شود.

مراحل پیاده‌سازی🔗

  • بررسی سلامت و دسترس پذیری نود اصلی
  • اتصال به نود اصلی در صورت سالم بودن
  • اتصال به نود Replica در صورت ناسالم بودن نود اصلی.
  • در صورت در دسترس نبودن هر دو، بازگرداندن خطای 500.

مثال‌ها🔗

عدم دسترسی به نود اصلی و Replica🔗

extensionFromNameterminal
curl "http://localhost:8080/get?key=key4"
Bash
extensionFromNameoutput
{
  "error": "Redis unavailable"
}
JSON
توضیحات کد وضعیت خروجی نمونه
نود اصلی سالم و قابل اتصال 200 { "key": "key5", "found": true, "value": "value5", "shard": "shard3" }
اتصال مستقیم به replica 200 { "key": "key6", "found": true, "value": "value6", "shard": "shard2" }
عدم دسترسی به هر دو نود 500 { "error": "Redis unavailable" }

آن‌چه باید آپلود کنید🔗

  • توجه: پس از اعمال تغییرات، کل پروژه را Zip کرده و آپلود کنید. همانند پروژه اولیه در فایل زیپ شده نباید کد در پوشه‌ی دیگری قرار بگیرد در غیر این صورت سیستم داوری فایل را شناسایی نکرده و نمره‌ای دریافت نخواهید کرد.

  • توجه: تنها فایل‌هایی که در ساختار پروژه مشخص شده‌اند، در سیستم داوری مورد پذیرش قرار خواهد گرفت و سایر تغییرات در سایر فایل‌ها بی‌تاثیر خواهند بود.