کش چند لایه. تنها راه نجات!

چند وقت پیش یک پروژه داشتم که یکی از end point هاش نیاز به یک سری محاسبات داشت. اینطور که به ازای هر درخواستی که سمت سرور میومد باید حدود ۱۰۰۰ درخواست get به کش میزد و بعد از انجام محاسبات (‌ که خود محاسبات زیاد سنگین نبودن ) و محاسبه مقادیر جدید همون مقدار درخواست set به کش بزنه. در آخر یک Worker دیگه هر چند وقت یک بار اطلاعات کش رو دریافت میکنه و توی دیتابیس ذخیره میکنه.من زیاد وارد جزئیات فنی پروژه نمیشم ، یکم خلاصه ترش میکنم. فرض کنید یک api با http میخوایم که توی هر درخواست ۱۰۰۰ درخواست set به کش بزنه و بعدش اون مقادیر رو بخونه. زیاد وارد جزئیات persist بودن دیتا ها هم نمیشیم چون اون خودش یک چالش دیگه هست.نکته دومی که هست میخوایم پاسخ اونقدر سریع باشه که اپ ما بتونه به بیش از ۳۰۰۰ هزار ریکوئست برثانیه پاسخ بده.ردیس سریع هست ، ولی نه به اندازه کافیبیاید یکم بیشتر با Redis آشنا بشیم. ردیس یک دیتابیس ذخیره دیتا به صورت key value هست که مقدار ها رو توی مموری ذخیره میکنه. این موضوع که دیتا ها توی مموری ذخیره میشن یعنی نسبت به دیتابیس هایی که دیتا رو مستقیم روی هارد درایو و .. ذخیره میکنن سریع تره. خلاصه بگم که اگه نیازه یک دیتایی رو از دیتابیس بگیرید ، و میدونید مثلا هر چند دقیقه یک بار اون مقدار ثابت هست جای اینکه هر ریکوئست به دیتابیس وصل شه و مقدار رو بگیره یک بار دیتا رو توی ردیس میریزیم و اون چند دقیقه دیتا رو از ردیس میگیریم. این موضوع باعث میشه اپلیکیشن ما خیلی سریع تر بشه در نتیجه با یک سرور یکسان میتونیم به ریکوئست های بیشتری در یک ثانیه پاسخ بدیم.نکته دومی که هست و ردیس رو با بقیه سیستم های کشینگ متمایز میکنه دیتاتایپ های ذخیره سازیش هست. که شامل string , list , set , hash , sorted set میشه. برای مثال من نیاز داشتم توی این پروژه یک سری از دیتا ها sort شده ذخیره بشن که sorted set به این موضوع کمک کرد. اما ردیس چقدر سریع هست؟همون طوری که گفتم ردیس نسبت به دیتابیس های مثلا SQL خیلی سریعه. ما ترجیح میدیم که اگر امکان داره برای دریافت یک دیتا به ردیس ریکوئست بزنیم تا MySQL. اما باید در نظر بگیرید ردیس هم داره یک سری امکانات در کنار سرعتش هم میده. یعنی یک Trade Off هست. فقط برای سرعت خالص ساخته نشده. با استفاده از redis-benchmark روی سیستمتون میتونید ببینید که بنچ مارک ردیستون چطوره.$ redis-benchmark -n 100000 -t set,get -q SET: 129870.13 requests per secondGET: 125000.00 requests per secondهمون طوری که میبینید ردیس روی سیستم من میتونه حدود ۱۰۰ هزار ریکوئست بر ثانیه داشته باشه! خیلی زیاده! اما توجه کنید هر ریکوئست قرار بود ۲ هزار درخواست به ردیس بزنم. یعنی در ثانیه اند پوینت من میتونه در ثانیه 100000/2000=50 ریکوئست رو هندل کنه. ۵۰ ریکوئست کجا ۳۰۰۰ کجا! بیاید این رو توی عمل ببینیم. ( برای نوشتن بنچ مارک ها از زبون go استفاده میکنم‌ ، بخصوص وقتی سرعت مهمه :)‌)func Test01() func(c echo.Context) error { rdb := redis.NewClient(&redis.Options{ Addr: &quotlocalhost:6379&quot, Password: &quot&quot, // no password set DB: 0, // use default DB }) return func(c echo.Context) error { iteration := 1000 // set for i := 0; i < iteration; i++ { key := fmt.Sprintf(&quotkey_%d&quot, i) rdb.Set(c.Request().Context(), key, i, 100*time.Second) } // get for i := 0; i < iteration; i++ { key := fmt.Sprintf(&quotkey_%d&quot, i) rdb.Get(c.Request().Context(), key) } return c.String(http.StatusOK, &quotDone&quot) } }همون طوری که توی کد میبینید اول به دیتابیس ردیس وصل میشیم. توی هر درخواست هم ۱۰۰۰ مقدار توی کش میریزیم و بعدش همون ۱۰۰۰ تا رو میخونیم. حالا با آپاچی بنچ مارک بهش ۱۰۰ ریکوئست برثانیه در مجموع ۱۰۰۰ ریکوئست بزنیم ببینیم چطور جواب میده.$ ab -n 1000 -c 100 http://localhost:1323/ Requests per second: 41.73 [#/sec] (mean) Time per request: 2396.329 [ms] (mean) Time per request: 23.963 [ms] (mean, across all concurrent requests) Percentage of the requests served within a certain time (ms) 50% 2419ms 66% 2447ms 75% 2482ms 80% 2493ms 90% 2505ms 95% 2513ms 98% 2519ms 99% 2526ms 100% 2552 (longest request)عددا همونطوری هستن که حدس میزدیم. سیستممون تونست حدود ۴۰ ریکوئست برثانیه هندل کنه.یه وقت هایی نیاز به این امکانات ردیس نداریم ، ما نیاز داریم value یک key در سریع ترین زمان ممکن به ما داده بشه. برای این کار باید یک لایه جدید از کش اضافه کنیم. خودمون یک سیستم کش با Map بسازیم!وقتی توی مصاحبه های کاری و ... با مپ کار میکنیم میگیم دسترسی به value یک key در Map پیچیدگی زمانی o(1) داره. کلا بنظر خیلی سریع میاد. برای پیاده سازی لایه جدید کشمون یک پکیج جدید میسازم که یک تایپ جدید به اسم MyCache تعریف میکنه.type MyCache struct { db map[string]int localLock sync.Mutex } func NewMyCache() *MyCache { return &MyCache{db: map[string]int{}} } func (p *MyCache) Set(key string, val int) { p.localLock.Lock() defer p.localLock.Unlock() p.db[key] = val } func (p *MyCache) Get(key string) int { p.localLock.Lock() defer p.localLock.Unlock() return p.db[key] }همون طوری که از کد هم معلومه یک Map داریم که بهش مقدار اضافه میشه یا مقدارش دریافت میشه. چیز پیچیده ایی نیست. ببینیم حالا این کد ما چطور جواب میده: Requests per second: 1764.61 [#/sec] (mean) Time per request: 56.670 [ms] (mean) Time per request: 0.567 [ms] (mean, across all concurrent requests) Percentage of the requests served within a certain time (ms) 50% 56ms 66% 60ms 75% 62ms 80% 64ms 90% 70ms 95% 78ms 98% 87ms 99% 92ms 100% 100 (longest request)پیشرفت زیادی کردیم! از ۴۰ ریکوئست برثانیه رسیدیم به ۱۷۰۰ ریکوئست برثانیه. من در این مرحله بنظرم به آخر خط رسیده بودم. مگه میشه از مپ سریع تر شد؟ تا اینکه این مقاله رو خوندم.خلاصه بخوام بگم ( حتما اون مقاله رو مطالعه کنید ) شما وقتی Map دارید Garbage Collector توی فاز های مارک و اسکن تک تک آیتم های مپ تون رو بررسی میکنه. این موضوع وقتی که دیتای زیادی دارید میتونه توی پرفورمنس کش‌تون تاثیر بذاره. توی این مقاله که هدفشون رسیدن به یک سیستم کش به شدت سریع بوده خیلی نکات دیگه ایی رو مطرح میکنه و در نهایت به پکیج ایی که ارائه دادن یعنی BigCache میرسه. بیاید حالا جای مپ از BigCache استفاده کنیم. ببینیم چه اتفاقی میوفته.cache, _ := bigcache.NewBigCache(bigcache.DefaultConfig(10 * time.Minute)) return func(c echo.Context) error { iteration := 1000 // set for i := 0; i < iteration; i++ { key := fmt.Sprintf(&quotkey_%d&quot, i) cache.Set(key, []byte(strconv.Itoa(i))) } // get for i := 0; i < iteration; i++ { key := fmt.Sprintf(&quotkey_%d&quot, i) cache.Get(key) } return c.String(http.StatusOK, &quotDone&quot) }و بنچ مارک :Requests per second: 3225.35 [#/sec] (mean) Time per request: 31.004 [ms] (mean) Time per request: 0.310 [ms] (mean, across all concurrent requests) Percentage of the requests served within a certain time (ms) 50% 27 66% 31 75% 34 80% 37 90% 43 95% 49 98% 54 99% 60 100% 95 (longest request)اوه اوه! رسیدیم به ۳۲۰۰ ریکوئست برثانیه. یعنی حدود ۲ برابر سریع تر از Map. تعداد ریکوئست های کانکارنت رو به ۱۰۰۰ ریکوئست و تعداد ریکوئست های کل رو هم به ۱۰۰۰۰ افزایش دادم. توی بنچ مارک باز هم ۲ برابر سریع تر از مپ بود.خلاصه و نتیجه گیری این مقاله به هیچ وجه برای این نیست نشون بده ردیس به درد نمیخوره! توی این پروژه من خیلی جا ها نیاز داشتم ( و گزینه ایی بهتر از ردیس نبود ) که از ردیس استفاده کنم.ردیس رو میشه Scale کرد. دیتا های ردیس Persist هستن. ردیس دیتاتایپ های مختلف با کلی امکانات خفن داره. هدف این مقاله این بود نشون بده که چندین تکنولوژی کنار هم توی جای درستشون میتونن چقدر به افزایش سرعت کمک کنن در نتیجه روی یک سرور یکسان بتونیم لود بیشتری رو هندل کنیم.