سلیب پس از برگزاری موفقیتآمیز مجموعه مسابقات کداستار، دوباره با ممزواد (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
برای راهاندازی پروژه، از یک فایل docker-compose.yml
استفاده خواهد شد، که شامل چند سرویس مختلف برای مدیریت دادههای پنبههای مزرعه سلیب با استفاده از Redis و OpenResty میباشد.
جزئیات پروژه
پیادهسازی فایل کانفیگ nginx.conf
و اسکریپتهای Lua، منطق مزرعه پنبه!
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
، زیرساخت مزرعه پنبه!
docker-compose.yml
، زیرساخت مزرعه پنبه!سرویس OpenResty
با استفاده از ایمیج زیر یک سرویس با نام openresty
ایجاد کنید:
registry.gitlab.com/qio/standard/openresty:1.19.9.1-5-alpine-fat
-
این سرویس برای اجرای کدهای 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
- این سرویسها برای ذخیرهسازی دادهها در یک ساختار توزیعشده Redis استفاده میشوند.
نکاتی در مورد پیادهسازی فایل docker-compose.yml
-
توجه: شما اجازهی build کردن یک ایمیج جدید نخواهید داشت و باید از ایمیجهای استاندارد کوئرا استفاده کنید.
-
توجه: نام کانتینرها و سرویسها باید دقیقا عبارات گفته شده در متن سوال باشند.
-
توجه: سیستم داوری کوئرا بهصورت خودکار فایل
docker-compose.yml
را با کامندup
اجرا میکند. شما نیازی به کد یا اسکریپتی برای اجرای این کار ندارید. -
توجه: ورژن
docker-compose.yml
باید3.3
باشد. -
توجه: تنها فایل
docker-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
پیادهسازی Setter برای کشت پنبه!
در این بخش، سلیب قرار است تا امکان ذخیره مقدار (Value) جدید پنبه برای یک کلید (Key) دلخواه را فراهم کند. ابتدا بررسی میشود آیا کلید (key
) و مقدار (value
) در Query String درخواست وجود دارد یا خیر. سپس با استفاده از تابع هش گفته شده، مشخص میشود کلید در کدام شارد (Shard) کلاستر ردیسی (Redis Cluster) قرار دارد. شارد هر کلید $key$ به این صورت مشخص خواهد شد:
$$FNV-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
.
مثالها
درخواستهای موفق برای ثبت مقدار جدید
curl "http://localhost:8080/set?key=key3&value=value3"
{
"key": "key3",
"value": "value3",
"status": "OK",
"shard": "shard1"
}
curl "http://localhost:8080/set?key=key1&value=value1"
{
"key": "key2",
"value": "value2",
"status": "OK",
"shard": "shard2"
}
curl "http://localhost:8080/set?key=key6&value=value6"
{
"key": "key6",
"value": "value6",
"status": "OK",
"shard": "shard3"
}
درخواستهای ناموفق برای ثبت مقدار جدید، بدون کلید و مقدار
curl "http://localhost:8080/set?value=blue"
{
"error": "Missing 'key'"
}
curl "http://localhost:8080/set?key=color"
{
"error": "Missing 'value'"
}
درخواست ناموفق برای ثبت مقدار جدید، در دسترس نبودن همزمان نود اصلی و Replica
curl "http://localhost:8080/set?key=color&value=green"
{
"error": "Redis unavailable"
}
توضیحات | کد وضعیت | خروجی نمونه |
---|---|---|
ارسال درست مقدار و کلید | 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) از کلاستر ردیسی قرار دارد. برای تعیین شارد، از رابطه زیر استفاده میشود:
$$FNV-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
.
مثالها
درخواستهای موفق برای دریافت مقدار کلید
curl "http://localhost:8080/get?key=key1"
{
"key": "key1",
"found": true,
"value": "value1",
"shard": "shard2"
}
curl "http://localhost:8080/get?key=key6"
{
"key": "key6",
"found": true,
"value": "value6",
"shard": "shard3"
}
درخواست ناموفق بدون کلید
curl "http://localhost:8080/get"
{
"error": "Missing 'key'"
}
درخواست برای کلید ناموجود
curl "http://localhost:8080/get?key=unknown"
{
"key": "unknown",
"found": false,
"value": null,
"shard": "shard2"
}
درخواست ناموفق به دلیل عدم دسترسی به ردیس
curl "http://localhost:8080/get?key=key9"
{
"error": "Redis unavailable"
}
توضیحات | کد وضعیت | خروجی نمونه |
---|---|---|
دریافت مقدار موجود | 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
مواجه شده و از ادامه فعالیت او برای مدت زمان ۱۵ ثانیهای، جلوگیری به عمل آید.
مثالها
درخواست بیش از حد مجاز
curl "http://localhost:8080/get?key=key1"
{
"error": "Rate limit exceeded"
}
توضیحات | کد وضعیت | خروجی نمونه |
---|---|---|
درخواست در محدودۀ مجاز | 200 |
{ "key": "test", "value": "val", "status": "OK", "shard": "shard1" } |
عبور از حد مجاز درخواست | 429 |
{ "error": "Rate limit exceeded" } |
پیادهسازی Fallback، برای پشتیبانی مزرعه!
برای اطمینان از پایداری بالا و جلوگیری از قطع خدمات در زمان از کار افتادن برخی نودهای ردیسی، سلیب باید از مکانیزم (Fallback) استفاده کند.
- در زمان پردازش درخواست، اگر نود اصلی سالم باشد، به آن متصل میشویم. در غیر اینصورت، به نود Replica متصل میشویم.
- اگر هیچکدام در دسترس نبودند، پاسخ با خطای
500
و پیامRedis unavailable
بازگردانده میشود.
مراحل پیادهسازی
- بررسی سلامت و دسترس پذیری نود اصلی
- اتصال به نود اصلی در صورت سالم بودن
- اتصال به نود Replica در صورت ناسالم بودن نود اصلی.
- در صورت در دسترس نبودن هر دو، بازگرداندن خطای
500
.
مثالها
عدم دسترسی به نود اصلی و Replica
curl "http://localhost:8080/get?key=key4"
{
"error": "Redis unavailable"
}
توضیحات | کد وضعیت | خروجی نمونه |
---|---|---|
نود اصلی سالم و قابل اتصال | 200 |
{ "key": "key5", "found": true, "value": "value5", "shard": "shard3" } |
اتصال مستقیم به replica |
200 |
{ "key": "key6", "found": true, "value": "value6", "shard": "shard2" } |
عدم دسترسی به هر دو نود | 500 |
{ "error": "Redis unavailable" } |
آنچه باید آپلود کنید
-
توجه: پس از اعمال تغییرات، کل پروژه را Zip کرده و آپلود کنید. همانند پروژه اولیه در فایل زیپ شده نباید کد در پوشهی دیگری قرار بگیرد در غیر این صورت سیستم داوری فایل را شناسایی نکرده و نمرهای دریافت نخواهید کرد.
-
توجه: تنها فایلهایی که در ساختار پروژه مشخص شدهاند، در سیستم داوری مورد پذیرش قرار خواهد گرفت و سایر تغییرات در سایر فایلها بیتاثیر خواهند بود.
ارسال پاسخ برای این سؤال