مراقب باش کد ات یک کار یکسان رو چند بار انجام نده!

CPU spikes! yikes!بیاید یک سناریو رو در نظر بگیریم. ما یک سیستم ساده داریم که یک دیتابیس سیکوئلی داره، یک سری کوئری سنگین هم داریم و تصمیم میگیریم که جوابش رو برای یک مدت زمانی کش کنیم. تا اینجا همه چیز خوبه. تست میکنیم جواب میده ، میبریم روی Production بازم خوبه. شب میخوابیم و صبح بیدار میشیم با تصویر بالا روبرو میشیم! یه زمان هایی سی پی یو میچسبه به سقف. علت چیه؟دوباره تحلیل کنیم که چه کد ایی زدیم.کدی که ما زدیم احتمالا اینطوره. اول یک شرط داریم چک میکنه دیتا توی کش هست یا نه. اگه نبود میریم کوئری میزنیم به دیتابیس و دیتا رو به کش اضافه میکنیم. اما یک حالت رو در نظر بگیرید:سرویس ما لود اش رفته بالا‌، حدود ۱۰۰ کاربر همزمان درخواست HTTP به اون اند پوینت ما میزنن. چه اتفاقی میوفته ؟ کاربر اول میره میبینه توی کش دیتا نیست ، میگه خب حله من میرم دیتا رو از دیتابیس بیارم و بریزم تو کش. حالا این فرایند ۱۰۰ میلی ثانیه طول میکشه. توی این پنجره ۱۰۰ میلی ثانیه ایی ۵۰ تا کاربر دیگه درخواست HTTP میزنن. چه اتفاقی میوفته؟ تمام اون ۵۰ درخواست هم می بینن چیزی توی کش نیست و شروع میکنن به provide کردن اون دیتا از دیتابیس. توی اون پنجره ۱۰۰ میلی‌ثانیه ایی سی‌پی‌یو میچسبونیم به سقف.این مثال جاهای مختلف کد میتونه وجود داشته باشه، پیدا کردن این موارد توی یک کدبیس بزرگ میتونه سخت باشه، اما باید مشخص کنیم منابع کند ما چیا هستن. میتونه خوندن یک فایل استاتیک باشه‌، درخواست های HTTP یکسان ، یک کوئری یکسان به دیتابیس و ...حالا فرض کنید متوجه شدیم که کجای کد مشکل ایجاد میکنه و باعث اون اسپایک ها توی سی‌پی‌یو میشه. چطور حلش کنیم؟ ( مسئله Thundering Herd )همون طوری که گفتم، فقط یک بار انجامش بدهمن با زبان go کار میکنم، راه حلی که دارم از کتابخانه ایی برای این زبان استفاده میکنه اما یقیناً مشابه این کتابخانه برای زبان های دیگه هم باید باشه. روش های دیگه ایی هم هستن که مستقل از زبان باشن.توی این مقاله از یکی از مهندسین اینستاگرام با روش Promises این مشکل رو حل میکنه که خیلی جالبه. اما فعلا ما توی یک اسکیل معمولی به قضیه نگاه میکنیم.گوگل سال ۲۰۱۳ یک کتابخانه به اسم groupcache معرفی کرده که علاوه بر این مشکل ( Thundering Herd ) یک سری مشکل های دیگه رو هم حل میکنه. کلا کتابخانه ی جذابیه. اما نکته ایی که هست توی خودش داره از singleflight استفاده میکنه. این کتاب‌خانه یک duplicate function call suppression mechanism ارائه میده. یعنی توابع مشابه ایی که همزمان دارن اجرا میشن یک بار اجرا شن و اون جواب به بقیه call ها داده بشه.cacheData, err := cache.Get(&quotgetData&quot) var result string if err != nil { result = GetData() cache.Set(&quotgetData&quot, []byte(result)) } else { result = string(cacheData) } return c.String(http.StatusOK, result)کد بالا رو در نظر بگیرید. ما یک کار خیلی ساده انجام دادیم! گفتیم که مقدار getData رو از cache به من بده. اگه مقدار وجود داشت که نمایشش میده و اگه مقدار نبود تابع GetData رو صدا میزنه و بعد دیتایی که گرفته رو توی کش میریزه و در نهایت مقدار رو برمیگردونه. تابع GetData هم یک تابع ساده هست ( که مثلا وصل میشه به دیتابیس یا ... و حدود ۱۰۰ میلی ثانیه طول میکشه‌ )من یک سری متریک اضافه کردم که بررسی کنم تابع GetData چند بار صدا زده میشه؟ اگر یک درخواست جدید بیاد توی بازه ۱۰۰ میلی‌ثانیه ایی که جواب رو پیدا میکنه درخواست جدیدی بیاد دوباره صدا زده میشه. (دقیقا Thundering herd ایی که صحبتش رو کرده بودیم)حالا چطور مشکل رو رفع کنیم؟ اول یک resourceGroup با singleflight میسازم. var resourceGroup singleflight.Group بعد اون رو پاس میدم به route ایی که قراره دیتا توش پرواید بشه. ( نکته ایی که هست اینه تمام goroutine ها باید به یک resourceGroup یکسان دسترسی داشته باشن.حالا میتونیم متد Do رو صدا بزنیم. اینطور که یک کلید بهش میدیم و یک تابع. خودش تابع رو اجرا میکنه اما مراقب این هست اگر یک گوروتین دیگه ایی خواست تابعی صدا بزنه اگه کلیدش مشابه این کلید بود صدا زده نشه ، بلکه منتظر میزنه اون بار اولی که تابع صدا زده شده به اتمام برسه و جواب رو به بقیه روتین ها میده.func HomeRoute( cache *bigcache.BigCache) func(c echo.Context) error { var resourceGroup singleflight.Group return func(c echo.Context) error { cacheData, err := cache.Get(&quotgetData&quot) var result string if err != nil { v, _, _ := resourceGroup.Do(&quotgetData&quot, func() (interface{}, error) { return GetData(), nil }) result = v.(string) cache.Set(&quotgetData&quot, []byte(result)) } else { result = string(cacheData) } return c.String(http.StatusOK, result) } }خب کد ما تقریبا همون کد قبلیه با یک تفاوت. ریکوئست اول میاد و میبینه دیتا توی کش نیست ، میاد متد Do رو صدا میزنه ، resourceGroup می‌بینه که قبلا کسی Do ی getData رو صدا نزده پس میاد اون تابع ایی که بهش دادیم رو صدا میزنه. حالا ریکوئست دوم ( یا چندین ریکوئست بعدی ) میاد و Do رو صدا میزنه. اما resourceGroup میبینه که قبلا اجرا شده این تابع ، منتظر میمونه که ریکوئست اول مقدار رو provide کنه.وقتی ریکوئست اول مقدار رو فراهم کرد resourceGroup مقدار رو هم به ریکوئست اول میده هم ریکوئست دوم و مقدار توی کش هم ذخیره میشه. از این به بعد بقیه ریکوئست ها مقدار رو از کش میگیرن.در نتیجه اگر ۱۰۰ ریکوئست همزمان بیاد جای اینکه ۱۰۰ بار به دیتابیس ریکوئست بزنیم یک بار میزنیم که اون اسپایک های سی پی یو رو از بین میبره.مثال پیچیده تر! چندین سرویس داریم با زبان های مختلفچیزی که تا الان صحبت کردم یک مثال خیلی ساده بود. یه وقت هایی پیش میاد ما چندین سرویس داریم که همزمان از یک کش استفاده میکنن. میتونن زبان های مختلف باشن و خلاصه نتونیم از singleflight استفاده کنیم. اگه همزمان به دو تا سرویس درخواست بره همزمان هر دو میرن که دیتا رو از سرویسی که دیتا رو ارائه میدن دریافت کنن. برای این حالت چیکار میکنیم؟ انشالله مقاله بعدی D: