عملکرد بالا یکی از الزامات کلیدی هنگام کار با داده های بزرگ است. در بخش بارگذاری داده در Sberbank، ما تقریباً تمام تراکنشها را به داده ابری مبتنی بر Hadoop خود پمپ میکنیم و بنابراین با جریانهای واقعاً بزرگ اطلاعات سروکار داریم. طبیعتاً ما همیشه به دنبال راههایی برای بهبود عملکرد هستیم و اکنون میخواهیم به شما بگوییم که چگونه توانستیم RegionServer HBase و کلاینت HDFS را وصله کنیم که به لطف آن توانستیم سرعت عملیات خواندن را به میزان قابل توجهی افزایش دهیم.
با این حال، قبل از رفتن به اصل پیشرفت ها، ارزش دارد در مورد محدودیت هایی صحبت کنیم که در اصل، اگر روی هارد دیسک بنشینید، نمی توان آنها را دور زد.
چرا خواندن HDD و سریع تصادفی ناسازگار است
همانطور که می دانید، HBase و بسیاری از پایگاه های داده دیگر، داده ها را در بلوک هایی با اندازه چند ده کیلوبایت ذخیره می کنند. به طور پیش فرض حدود 64 کیلوبایت است. حالا بیایید تصور کنیم که فقط باید 100 بایت دریافت کنیم و از HBase می خواهیم که این داده ها را با استفاده از یک کلید خاص به ما بدهد. از آنجایی که اندازه بلوک در HFiles 64 کیلوبایت است، درخواست 640 برابر (فقط یک دقیقه!) بزرگتر از نیاز خواهد بود.
در مرحله بعد، از آنجایی که درخواست از طریق HDFS و مکانیسم ذخیره ابرداده آن میگذرد ShortCircuitCache (که امکان دسترسی مستقیم به فایل ها را فراهم می کند)، این منجر به خواندن 1 مگابایت از دیسک می شود. با این حال، این را می توان با پارامتر تنظیم کرد dfs.client.read.shortcircuit.buffer.size و در بسیاری از موارد کاهش این مقدار به عنوان مثال به 126 کیلوبایت منطقی است.
فرض کنید این کار را انجام می دهیم، اما علاوه بر این، زمانی که شروع به خواندن داده ها از طریق api جاوا می کنیم، مانند توابعی مانند FileChannel.read و از سیستم عامل می خواهیم که مقدار مشخص شده داده را بخواند، "فقط در مورد" 2 برابر بیشتر می خواند. ، یعنی 256 کیلوبایت در مورد ما. این به این دلیل است که جاوا راه آسانی برای تنظیم پرچم FADV_RANDOM برای جلوگیری از این رفتار ندارد.
در نتیجه، برای دریافت 100 بایت، 2600 برابر بیشتر در زیر هود خوانده می شود. به نظر می رسد که راه حل واضح است، بیایید اندازه بلوک را به یک کیلوبایت کاهش دهیم، پرچم ذکر شده را تنظیم کنیم و شتاب روشنگری عالی به دست آوریم. اما مشکل اینجاست که با کاهش 2 برابری اندازه بلوک، تعداد بایت های خوانده شده در واحد زمان را نیز 2 برابر کاهش می دهیم.
مقداری سود از تنظیم پرچم FADV_RANDOM به دست می آید، اما فقط با چند رشته ای بالا و با اندازه بلوک 128 کیلوبایت، اما این حداکثر چند ده درصد است:
آزمایشها بر روی 100 فایل، هر کدام 1 گیگابایت و در 10 هارد دیسک انجام شد.
بیایید محاسبه کنیم که اصولاً با این سرعت روی چه چیزی می توانیم حساب کنیم:
فرض کنید از 10 دیسک با سرعت 280 مگابایت بر ثانیه می خوانیم، یعنی. 3 میلیون بار 100 بایت. اما همانطور که به یاد داریم، داده های مورد نیاز ما 2600 برابر کمتر از آنچه خوانده می شود است. بنابراین 3 میلیون را بر 2600 تقسیم می کنیم و بدست می آوریم 1100 رکورد در ثانیه
افسرده است، اینطور نیست؟ این طبیعت است دسترسی تصادفی دسترسی به داده ها در هارد دیسک - صرف نظر از اندازه بلوک. این محدودیت فیزیکی دسترسی تصادفی است و هیچ پایگاه دادهای نمیتواند در چنین شرایطی مقدار بیشتری را محدود کند.
پس چگونه پایگاه داده ها به سرعت بسیار بالاتری دست می یابند؟ برای پاسخ به این سوال، بیایید به آنچه در تصویر زیر رخ می دهد نگاه کنیم:
در اینجا می بینیم که برای چند دقیقه اول سرعت واقعاً حدود هزار رکورد در ثانیه است. با این حال، بیشتر، با توجه به این واقعیت که بسیار بیشتر از آنچه خواسته شده خوانده می شود، داده ها در buff/cache سیستم عامل (لینوکس) قرار می گیرند و سرعت به 60 هزار در ثانیه افزایش می یابد.
بنابراین، در ادامه ما فقط با تسریع دسترسی به داده هایی که در حافظه پنهان سیستم عامل یا در دستگاه های ذخیره سازی SSD/NVMe با سرعت دسترسی قابل مقایسه قرار دارند، سروکار خواهیم داشت.
در مورد ما، ما تست هایی را روی یک میز 4 سرور انجام خواهیم داد که هزینه هر کدام به شرح زیر است:
CPU: Xeon E5-2680 v4 @ 2.40GHz 64 Thread.
حافظه: 730 گیگابایت.
نسخه جاوا: 1.8.0_111
و در اینجا نکته کلیدی میزان داده های جداول است که باید خوانده شوند. واقعیت این است که اگر دادهها را از جدولی بخوانید که به طور کامل در حافظه پنهان HBase قرار دارد، حتی از buff/cache سیستم عامل نیز نمیتوان آن را خواند. زیرا HBase به طور پیش فرض 40 درصد از حافظه را به ساختاری به نام BlockCache اختصاص می دهد. اساساً این یک ConcurrentHashMap است، که در آن کلید نام فایل + offset بلوک است و مقدار دادههای واقعی در این آفست است.
بنابراین، هنگام خواندن فقط از این ساختار، ما
به عنوان مثال، در مورد ما، حجم BlockCache در یک RS حدود 12 گیگابایت است. ما دو RS را روی یک گره فرود آوردیم، یعنی. 96 گیگابایت برای BlockCache در همه گره ها اختصاص داده شده است. و چندین برابر بیشتر داده است، به عنوان مثال، اجازه دهید 4 جدول، هر کدام 130 منطقه، که در آن فایل ها 800 مگابایت حجم دارند، با FAST_DIFF فشرده شده اند، یعنی. در مجموع 410 گیگابایت (این داده خالص است، یعنی بدون در نظر گرفتن ضریب تکرار).
بنابراین، BlockCache تنها حدود 23٪ از کل حجم داده است و این بسیار به شرایط واقعی چیزی که BigData نامیده می شود نزدیک است. و اینجاست که سرگرمی شروع می شود - زیرا بدیهی است که هرچه تعداد بازدیدهای حافظه پنهان کمتر باشد، عملکرد بدتر است. از این گذشته ، اگر از دست بدهید ، باید کارهای زیادی انجام دهید - یعنی. به فراخوانی توابع سیستم بروید. با این حال، نمی توان از این امر اجتناب کرد، بنابراین بیایید به یک جنبه کاملاً متفاوت نگاه کنیم - چه اتفاقی برای داده های داخل حافظه پنهان می افتد؟
بیایید وضعیت را ساده کنیم و فرض کنیم که یک کش داریم که فقط با 1 شیء مناسب است. در اینجا نمونهای از اتفاقی است که وقتی میخواهیم با حجم دادهای 3 برابر بزرگتر از حافظه پنهان کار کنیم، اتفاق میافتد، باید:
1. بلوک 1 را در کش قرار دهید
2. بلوک 1 را از کش حذف کنید
3. بلوک 2 را در کش قرار دهید
4. بلوک 2 را از کش حذف کنید
5. بلوک 3 را در کش قرار دهید
5 عمل تکمیل شد! با این حال، این وضعیت را نمی توان عادی نامید؛ در واقع، ما HBase را مجبور به انجام یکسری کارهای کاملاً بی فایده می کنیم. دائماً دادهها را از حافظه پنهان سیستمعامل میخواند، آنها را در BlockCache قرار میدهد و تقریباً بلافاصله آنها را بیرون میاندازد زیرا بخش جدیدی از دادهها وارد شده است. انیمیشن ابتدای پست اصل مشکل را نشان می دهد - جمع آوری زباله از مقیاس خارج می شود ، فضا گرم می شود ، گرتا کوچک در سوئد دور و گرم در حال ناراحتی است. و ما افراد IT واقعاً وقتی بچهها غمگین هستند دوست نداریم، بنابراین شروع به فکر کردن به این میکنیم که در مورد آن چه کاری میتوانیم انجام دهیم.
اگر همه بلوکها را نه، بلکه فقط درصد معینی از آنها را در حافظه پنهان قرار دهید، تا کش سرریز نشود، چه؟ بیایید با اضافه کردن چند خط کد به ابتدای تابع برای قرار دادن داده ها در BlockCache شروع کنیم:
public void cacheBlock(BlockCacheKey cacheKey, Cacheable buf, boolean inMemory) {
if (cacheDataBlockPercent != 100 && buf.getBlockType().isData()) {
if (cacheKey.getOffset() % 100 >= cacheDataBlockPercent) {
return;
}
}
...
نکته در اینجا این است: offset موقعیت بلوک در فایل است و آخرین ارقام آن به طور تصادفی و به طور مساوی از 00 تا 99 توزیع شده است. بنابراین، ما فقط از مواردی که در محدوده مورد نیاز ما قرار می گیرند صرف نظر می کنیم.
به عنوان مثال، cacheDataBlockPercent = 20 را تنظیم کنید و ببینید چه اتفاقی می افتد:
نتیجه مشهود است. در نمودارهای زیر، مشخص میشود که چرا چنین شتابی رخ داده است - ما بدون انجام کار سیزیفی قرار دادن دادهها در حافظه پنهان، منابع GC زیادی را صرفهجویی میکنیم تا بلافاصله آنها را در زهکشی سگهای مریخی بیاندازیم:
در همان زمان، استفاده از CPU افزایش می یابد، اما بسیار کمتر از بهره وری است:
همچنین شایان ذکر است که بلوک های ذخیره شده در BlockCache متفاوت هستند. بیشتر، حدود 95 درصد، خود داده است. و بقیه متادیتا هستند، مانند فیلترهای بلوم یا LEAF_INDEX و
بنابراین در کد یک شرط چک می بینیم buf.getBlockType().isData() و به لطف این متا، در هر صورت آن را در حافظه پنهان می گذاریم.
حالا بیایید بار را افزایش دهیم و یکباره ویژگی را کمی سفت کنیم. در آزمایش اول، درصد برش را 20= کردیم و از BlockCache اندکی استفاده نشد. حالا بیایید آن را روی 23٪ تنظیم کنیم و هر 100 دقیقه 5 رشته اضافه کنیم تا ببینیم اشباع در چه نقطه ای رخ می دهد:
در اینجا می بینیم که نسخه اصلی تقریباً بلافاصله با حدود 100 هزار درخواست در ثانیه به سقف می رسد. در حالی که پچ تا 300 هزار شتاب می دهد. در عین حال، واضح است که شتاب بیشتر دیگر آنقدر «رایگان» نیست؛ استفاده از CPU نیز در حال افزایش است.
با این حال، این یک راه حل بسیار زیبا نیست، زیرا ما از قبل نمی دانیم که چند درصد از بلوک ها باید در حافظه پنهان شوند، این به نمایه بار بستگی دارد. بنابراین، مکانیزمی برای تنظیم خودکار این پارامتر بسته به فعالیت عملیات خواندن پیادهسازی شد.
سه گزینه برای کنترل این مورد اضافه شده است:
hbase.lru.cache.heavy.eviction.count.limit — تعیین میکند قبل از شروع استفاده از بهینهسازی (یعنی پرش از بلوکها) فرآیند حذف دادهها از حافظه پنهان چند بار اجرا شود. به طور پیش فرض برابر با MAX_INT = 2147483647 است و در واقع به این معنی است که این ویژگی هرگز با این مقدار شروع به کار نخواهد کرد. زیرا روند تخلیه هر 5 - 10 ثانیه شروع می شود (بستگی به بار دارد) و 2147483647 * 10 / 60 / 60 / 24 / 365 = 680 سال. با این حال، ما میتوانیم این پارامتر را روی 0 تنظیم کنیم و این ویژگی را بلافاصله پس از راهاندازی کار کنیم.
با این حال، یک بار در این پارامتر نیز وجود دارد. اگر بار ما به گونهای باشد که خواندن کوتاهمدت (مثلاً در روز) و خواندن طولانیمدت (شب) دائماً در هم قرار میگیرد، میتوانیم مطمئن شویم که این ویژگی تنها زمانی روشن میشود که عملیات خواندن طولانی در حال انجام است.
به عنوان مثال، می دانیم که خواندن های کوتاه مدت معمولاً حدود 1 دقیقه طول می کشد. نیازی به شروع به بیرون ریختن بلوک ها نیست، حافظه نهان وقت ندارد که قدیمی شود و سپس می توانیم این پارامتر را برابر با 10 قرار دهیم. این منجر به این واقعیت می شود که بهینه سازی فقط زمانی شروع به کار می کند که طولانی مدت اصطلاح خواندن فعال آغاز شده است، یعنی. در 100 ثانیه بنابراین، اگر ما یک خواندن کوتاه مدت داشته باشیم، تمام بلوک ها به حافظه پنهان می روند و در دسترس خواهند بود (به جز مواردی که توسط الگوریتم استاندارد خارج می شوند). و هنگامی که خواندن طولانی مدت انجام می دهیم، این ویژگی روشن می شود و عملکرد بسیار بالاتری خواهیم داشت.
hbase.lru.cache.heavy.eviction.mb.size.limit — تعداد مگابایتی را که می خواهیم در کش (و البته بیرون کردن) در 10 ثانیه قرار دهیم را تنظیم می کند. ویژگی سعی می کند به این مقدار برسد و آن را حفظ کند. نکته اینجاست: اگر ما گیگابایت را به حافظه کش بریزیم، باید گیگابایت را بیرون بیاوریم، و این، همانطور که در بالا دیدیم، بسیار گران است. با این حال، نباید سعی کنید آن را خیلی کوچک تنظیم کنید، زیرا این کار باعث میشود حالت پرش بلوک زودتر از موعد خارج شود. برای سرورهای قدرتمند (حدود 20-40 هسته فیزیکی)، بهینه است که حدود 300-400 مگابایت تنظیم شود. برای طبقه متوسط (~10 هسته) 200-300 مگابایت. برای سیستم های ضعیف (2-5 هسته) 50-100 مگابایت ممکن است نرمال باشد (بر روی این ها تست نشده است).
بیایید ببینیم چگونه این کار می کند: فرض کنید hbase.lru.cache.heavy.eviction.mb.size.limit = 500 تنظیم کرده ایم، نوعی بار (خواندن) وجود دارد و سپس هر 10 ثانیه محاسبه می کنیم که چند بایت بود. اخراج با استفاده از فرمول:
سربار = مجموع بایت های آزاد شده (MB) * 100 / Limit (MB) - 100;
اگر در واقع 2000 مگابایت تخلیه شد، سربار برابر است با:
2000 * 100 / 500 - 100 = 300٪
الگوریتمها سعی میکنند بیش از چند ده درصد را حفظ نکنند، بنابراین این ویژگی درصد بلوکهای کش را کاهش میدهد و در نتیجه مکانیزم تنظیم خودکار را پیادهسازی میکند.
با این حال، اگر بار کاهش یابد، فرض کنید فقط 200 مگابایت تخلیه می شود و سربار منفی می شود (به اصطلاح overshooting):
200 * 100 / 500 - 100 = -60٪
برعکس، این ویژگی درصد بلوک های کش را تا زمانی که Overhead مثبت شود افزایش می دهد.
در زیر مثالی از این که چگونه این در داده های واقعی به نظر می رسد آورده شده است. نیازی به تلاش برای رسیدن به صفر نیست، غیرممکن است. زمانی که حدود 0 تا 30 درصد باشد بسیار خوب است، این به جلوگیری از خروج زودهنگام از حالت بهینه سازی در طول موج های کوتاه مدت کمک می کند.
hbase.lru.cache.heavy.eviction.overhead.coefficient - تعیین می کند که ما چقدر سریع می خواهیم به نتیجه برسیم. اگر مطمئن باشیم که خواندن های ما اکثرا طولانی هستند و نمی خواهیم منتظر بمانیم، می توانیم این نسبت را افزایش دهیم و سریعتر کارایی بالایی داشته باشیم.
به عنوان مثال، ما این ضریب را 0.01 = تنظیم می کنیم. این بدان معنی است که سربار (به بالا مراجعه کنید) در این عدد در نتیجه حاصل ضرب می شود و درصد بلوک های کش کاهش می یابد. فرض کنید سربار = 300٪ و ضریب = 0.01، سپس درصد بلوک های کش 3٪ کاهش می یابد.
منطق مشابه "Backpressure" نیز برای مقادیر منفی سربار (بیش از حد) اجرا می شود. از آنجایی که نوسانات کوتاه مدت در حجم خواندن و خروج همیشه امکان پذیر است، این مکانیسم به شما امکان می دهد از خروج زودهنگام از حالت بهینه سازی جلوگیری کنید. Backpressure منطق معکوس دارد: هر چه overshooting قویتر باشد، بلاکهای بیشتری در حافظه پنهان ذخیره میشوند.
کد پیاده سازی
LruBlockCache cache = this.cache.get();
if (cache == null) {
break;
}
freedSumMb += cache.evict()/1024/1024;
/*
* Sometimes we are reading more data than can fit into BlockCache
* and it is the cause a high rate of evictions.
* This in turn leads to heavy Garbage Collector works.
* So a lot of blocks put into BlockCache but never read,
* but spending a lot of CPU resources.
* Here we will analyze how many bytes were freed and decide
* decide whether the time has come to reduce amount of caching blocks.
* It help avoid put too many blocks into BlockCache
* when evict() works very active and save CPU for other jobs.
* More delails: https://issues.apache.org/jira/browse/HBASE-23887
*/
// First of all we have to control how much time
// has passed since previuos evict() was launched
// This is should be almost the same time (+/- 10s)
// because we get comparable volumes of freed bytes each time.
// 10s because this is default period to run evict() (see above this.wait)
long stopTime = System.currentTimeMillis();
if ((stopTime - startTime) > 1000 * 10 - 1) {
// Here we have to calc what situation we have got.
// We have the limit "hbase.lru.cache.heavy.eviction.bytes.size.limit"
// and can calculte overhead on it.
// We will use this information to decide,
// how to change percent of caching blocks.
freedDataOverheadPercent =
(int) (freedSumMb * 100 / cache.heavyEvictionMbSizeLimit) - 100;
if (freedSumMb > cache.heavyEvictionMbSizeLimit) {
// Now we are in the situation when we are above the limit
// But maybe we are going to ignore it because it will end quite soon
heavyEvictionCount++;
if (heavyEvictionCount > cache.heavyEvictionCountLimit) {
// It is going for a long time and we have to reduce of caching
// blocks now. So we calculate here how many blocks we want to skip.
// It depends on:
// 1. Overhead - if overhead is big we could more aggressive
// reducing amount of caching blocks.
// 2. How fast we want to get the result. If we know that our
// heavy reading for a long time, we don't want to wait and can
// increase the coefficient and get good performance quite soon.
// But if we don't sure we can do it slowly and it could prevent
// premature exit from this mode. So, when the coefficient is
// higher we can get better performance when heavy reading is stable.
// But when reading is changing we can adjust to it and set
// the coefficient to lower value.
int change =
(int) (freedDataOverheadPercent * cache.heavyEvictionOverheadCoefficient);
// But practice shows that 15% of reducing is quite enough.
// We are not greedy (it could lead to premature exit).
change = Math.min(15, change);
change = Math.max(0, change); // I think it will never happen but check for sure
// So this is the key point, here we are reducing % of caching blocks
cache.cacheDataBlockPercent -= change;
// If we go down too deep we have to stop here, 1% any way should be.
cache.cacheDataBlockPercent = Math.max(1, cache.cacheDataBlockPercent);
}
} else {
// Well, we have got overshooting.
// Mayby it is just short-term fluctuation and we can stay in this mode.
// It help avoid permature exit during short-term fluctuation.
// If overshooting less than 90%, we will try to increase the percent of
// caching blocks and hope it is enough.
if (freedSumMb >= cache.heavyEvictionMbSizeLimit * 0.1) {
// Simple logic: more overshooting - more caching blocks (backpressure)
int change = (int) (-freedDataOverheadPercent * 0.1 + 1);
cache.cacheDataBlockPercent += change;
// But it can't be more then 100%, so check it.
cache.cacheDataBlockPercent = Math.min(100, cache.cacheDataBlockPercent);
} else {
// Looks like heavy reading is over.
// Just exit form this mode.
heavyEvictionCount = 0;
cache.cacheDataBlockPercent = 100;
}
}
LOG.info("BlockCache evicted (MB): {}, overhead (%): {}, " +
"heavy eviction counter: {}, " +
"current caching DataBlock (%): {}",
freedSumMb, freedDataOverheadPercent,
heavyEvictionCount, cache.cacheDataBlockPercent);
freedSumMb = 0;
startTime = stopTime;
}
بیایید اکنون با استفاده از یک مثال واقعی به همه اینها نگاه کنیم. ما اسکریپت تست زیر را داریم:
- بیایید شروع به اسکن کنیم (25 رشته، دسته = 100)
- پس از 5 دقیقه، مولتی می شود (25 رشته، دسته = 100)
- بعد از 5 دقیقه، Multi-gets را خاموش کنید (فقط اسکن دوباره باقی می ماند)
ما دو اجرا انجام می دهیم، ابتدا hbase.lru.cache.heavy.eviction.count.limit = 10000 (که در واقع ویژگی را غیرفعال می کند)، و سپس limit = 0 را تنظیم می کنیم (آن را فعال می کند).
در گزارش های زیر می بینیم که چگونه این ویژگی روشن می شود و Overshooting را به 14-71% بازنشانی می کند. هر از چند گاهی بار کاهش می یابد، که Backpressure روشن می شود و HBase دوباره بلوک های بیشتری را ذخیره می کند.
ورود RegionServer
اخراج شده (MB): 0، نسبت 0.0، سربار (%): -100، شمارشگر تخلیه سنگین: 0، حافظه پنهان فعلی DataBlock (%): 100
اخراج شده (MB): 0، نسبت 0.0، سربار (%): -100، شمارشگر تخلیه سنگین: 0، حافظه پنهان فعلی DataBlock (%): 100
اخراج شده (MB): 2170، نسبت 1.09، سربار (%): 985، شمارنده اخراج سنگین: 1، حافظه پنهان فعلی DataBlock (%): 91 <شروع
اخراج شده (MB): 3763، نسبت 1.08، سربار (%): 1781، شمارنده تخلیه سنگین: 2، حافظه پنهان فعلی DataBlock (%): 76
اخراج شده (MB): 3306، نسبت 1.07، سربار (%): 1553، شمارنده تخلیه سنگین: 3، حافظه پنهان فعلی DataBlock (%): 61
اخراج شده (MB): 2508، نسبت 1.06، سربار (%): 1154، شمارنده تخلیه سنگین: 4، حافظه پنهان فعلی DataBlock (%): 50
اخراج شده (MB): 1824، نسبت 1.04، سربار (%): 812، شمارنده تخلیه سنگین: 5، حافظه پنهان فعلی DataBlock (%): 42
اخراج شده (MB): 1482، نسبت 1.03، سربار (%): 641، شمارنده تخلیه سنگین: 6، حافظه پنهان فعلی DataBlock (%): 36
اخراج شده (MB): 1140، نسبت 1.01، سربار (%): 470، شمارنده تخلیه سنگین: 7، حافظه پنهان فعلی DataBlock (%): 32
اخراج شده (MB): 913، نسبت 1.0، سربار (%): 356، شمارنده تخلیه سنگین: 8، حافظه پنهان فعلی DataBlock (%): 29
اخراج شده (MB): 912، نسبت 0.89، سربار (%): 356، شمارنده تخلیه سنگین: 9، حافظه پنهان فعلی DataBlock (%): 26
اخراج شده (MB): 684، نسبت 0.76، سربار (%): 242، شمارنده تخلیه سنگین: 10، حافظه پنهان فعلی DataBlock (%): 24
اخراج شده (MB): 684، نسبت 0.61، سربار (%): 242، شمارنده تخلیه سنگین: 11، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 456، نسبت 0.51، سربار (%): 128، شمارنده تخلیه سنگین: 12، حافظه پنهان فعلی DataBlock (%): 21
اخراج شده (MB): 456، نسبت 0.42، سربار (%): 128، شمارنده تخلیه سنگین: 13، حافظه پنهان فعلی DataBlock (%): 20
اخراج شده (MB): 456، نسبت 0.33، سربار (%): 128، شمارنده تخلیه سنگین: 14، حافظه پنهان فعلی DataBlock (%): 19
اخراج شده (MB): 342، نسبت 0.33، سربار (%): 71، شمارنده تخلیه سنگین: 15، حافظه پنهان فعلی DataBlock (%): 19
اخراج شده (MB): 342، نسبت 0.32، سربار (%): 71، شمارنده تخلیه سنگین: 16، حافظه پنهان فعلی DataBlock (%): 19
اخراج شده (MB): 342، نسبت 0.31، سربار (%): 71، شمارنده تخلیه سنگین: 17، حافظه پنهان فعلی DataBlock (%): 19
اخراج شده (MB): 228، نسبت 0.3، سربار (%): 14، شمارنده تخلیه سنگین: 18، حافظه پنهان فعلی DataBlock (%): 19
اخراج شده (MB): 228، نسبت 0.29، سربار (%): 14، شمارنده تخلیه سنگین: 19، حافظه پنهان فعلی DataBlock (%): 19
اخراج شده (MB): 228، نسبت 0.27، سربار (%): 14، شمارنده تخلیه سنگین: 20، حافظه پنهان فعلی DataBlock (%): 19
اخراج شده (MB): 228، نسبت 0.25، سربار (%): 14، شمارنده تخلیه سنگین: 21، حافظه پنهان فعلی DataBlock (%): 19
اخراج شده (MB): 228، نسبت 0.24، سربار (%): 14، شمارنده تخلیه سنگین: 22، حافظه پنهان فعلی DataBlock (%): 19
اخراج شده (MB): 228، نسبت 0.22، سربار (%): 14، شمارنده تخلیه سنگین: 23، حافظه پنهان فعلی DataBlock (%): 19
اخراج شده (MB): 228، نسبت 0.21، سربار (%): 14، شمارنده تخلیه سنگین: 24، حافظه پنهان فعلی DataBlock (%): 19
اخراج شده (MB): 228، نسبت 0.2، سربار (%): 14، شمارنده تخلیه سنگین: 25، حافظه پنهان فعلی DataBlock (%): 19
اخراج شده (MB): 228، نسبت 0.17، سربار (%): 14، شمارنده تخلیه سنگین: 26، حافظه پنهان فعلی DataBlock (%): 19
اخراج شده (MB): 456، نسبت 0.17، سربار (%): 128، شمارشگر تخلیه سنگین: 27، حافظه پنهان فعلی DataBlock (%): 18 < اضافه می شود (اما جدول یکسان است)
اخراج شده (MB): 456، نسبت 0.15، سربار (%): 128، شمارنده تخلیه سنگین: 28، حافظه پنهان فعلی DataBlock (%): 17
اخراج شده (MB): 342، نسبت 0.13، سربار (%): 71، شمارنده تخلیه سنگین: 29، حافظه پنهان فعلی DataBlock (%): 17
اخراج شده (MB): 342، نسبت 0.11، سربار (%): 71، شمارنده تخلیه سنگین: 30، حافظه پنهان فعلی DataBlock (%): 17
اخراج شده (MB): 342، نسبت 0.09، سربار (%): 71، شمارنده تخلیه سنگین: 31، حافظه پنهان فعلی DataBlock (%): 17
اخراج شده (MB): 228، نسبت 0.08، سربار (%): 14، شمارنده تخلیه سنگین: 32، حافظه پنهان فعلی DataBlock (%): 17
اخراج شده (MB): 228، نسبت 0.07، سربار (%): 14، شمارنده تخلیه سنگین: 33، حافظه پنهان فعلی DataBlock (%): 17
اخراج شده (MB): 228، نسبت 0.06، سربار (%): 14، شمارنده تخلیه سنگین: 34، حافظه پنهان فعلی DataBlock (%): 17
اخراج شده (MB): 228، نسبت 0.05، سربار (%): 14، شمارنده تخلیه سنگین: 35، حافظه پنهان فعلی DataBlock (%): 17
اخراج شده (MB): 228، نسبت 0.05، سربار (%): 14، شمارنده تخلیه سنگین: 36، حافظه پنهان فعلی DataBlock (%): 17
اخراج شده (MB): 228، نسبت 0.04، سربار (%): 14، شمارنده تخلیه سنگین: 37، حافظه پنهان فعلی DataBlock (%): 17
اخراج شده (MB): 109، نسبت 0.04، سربار (%): -46، شمارشگر تخلیه سنگین: 37، ذخیره فعلی DataBlock (%): 22 < فشار برگشتی
اخراج شده (MB): 798، نسبت 0.24، سربار (%): 299، شمارنده تخلیه سنگین: 38، حافظه پنهان فعلی DataBlock (%): 20
اخراج شده (MB): 798، نسبت 0.29، سربار (%): 299، شمارنده تخلیه سنگین: 39، حافظه پنهان فعلی DataBlock (%): 18
اخراج شده (MB): 570، نسبت 0.27، سربار (%): 185، شمارنده تخلیه سنگین: 40، حافظه پنهان فعلی DataBlock (%): 17
اخراج شده (MB): 456، نسبت 0.22، سربار (%): 128، شمارنده تخلیه سنگین: 41، حافظه پنهان فعلی DataBlock (%): 16
اخراج شده (MB): 342، نسبت 0.16، سربار (%): 71، شمارنده تخلیه سنگین: 42، حافظه پنهان فعلی DataBlock (%): 16
اخراج شده (MB): 342، نسبت 0.11، سربار (%): 71، شمارنده تخلیه سنگین: 43، حافظه پنهان فعلی DataBlock (%): 16
اخراج شده (MB): 228، نسبت 0.09، سربار (%): 14، شمارنده تخلیه سنگین: 44، حافظه پنهان فعلی DataBlock (%): 16
اخراج شده (MB): 228، نسبت 0.07، سربار (%): 14، شمارنده تخلیه سنگین: 45، حافظه پنهان فعلی DataBlock (%): 16
اخراج شده (MB): 228، نسبت 0.05، سربار (%): 14، شمارنده تخلیه سنگین: 46، حافظه پنهان فعلی DataBlock (%): 16
اخراج شده (MB): 222، نسبت 0.04، سربار (%): 11، شمارنده تخلیه سنگین: 47، حافظه پنهان فعلی DataBlock (%): 16
اخراج شده (MB): 104، نسبت 0.03، سربار (%): -48، شمارنده تخلیه سنگین: 47، حافظه پنهان فعلی DataBlock (%): 21 < وقفه دریافت می شود
اخراج شده (MB): 684، نسبت 0.2، سربار (%): 242، شمارنده تخلیه سنگین: 48، حافظه پنهان فعلی DataBlock (%): 19
اخراج شده (MB): 570، نسبت 0.23، سربار (%): 185، شمارنده تخلیه سنگین: 49، حافظه پنهان فعلی DataBlock (%): 18
اخراج شده (MB): 342، نسبت 0.22، سربار (%): 71، شمارنده تخلیه سنگین: 50، حافظه پنهان فعلی DataBlock (%): 18
اخراج شده (MB): 228، نسبت 0.21، سربار (%): 14، شمارنده تخلیه سنگین: 51، حافظه پنهان فعلی DataBlock (%): 18
اخراج شده (MB): 228، نسبت 0.2، سربار (%): 14، شمارنده تخلیه سنگین: 52، حافظه پنهان فعلی DataBlock (%): 18
اخراج شده (MB): 228، نسبت 0.18، سربار (%): 14، شمارنده تخلیه سنگین: 53، حافظه پنهان فعلی DataBlock (%): 18
اخراج شده (MB): 228، نسبت 0.16، سربار (%): 14، شمارنده تخلیه سنگین: 54، حافظه پنهان فعلی DataBlock (%): 18
اخراج شده (MB): 228، نسبت 0.14، سربار (%): 14، شمارنده تخلیه سنگین: 55، حافظه پنهان فعلی DataBlock (%): 18
اخراج شده (MB): 112، نسبت 0.14، سربار (%): -44، شمارشگر تخلیه سنگین: 55، ذخیره فعلی DataBlock (%): 23 < فشار برگشتی
اخراج شده (MB): 456، نسبت 0.26، سربار (%): 128، شمارنده تخلیه سنگین: 56، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 342، نسبت 0.31، سربار (%): 71، شمارنده تخلیه سنگین: 57، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 342، نسبت 0.33، سربار (%): 71، شمارنده تخلیه سنگین: 58، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 342، نسبت 0.33، سربار (%): 71، شمارنده تخلیه سنگین: 59، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 342، نسبت 0.33، سربار (%): 71، شمارنده تخلیه سنگین: 60، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 342، نسبت 0.33، سربار (%): 71، شمارنده تخلیه سنگین: 61، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 342، نسبت 0.33، سربار (%): 71، شمارنده تخلیه سنگین: 62، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 342، نسبت 0.33، سربار (%): 71، شمارنده تخلیه سنگین: 63، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 342، نسبت 0.32، سربار (%): 71، شمارنده تخلیه سنگین: 64، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 342، نسبت 0.33، سربار (%): 71، شمارنده تخلیه سنگین: 65، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 342، نسبت 0.33، سربار (%): 71، شمارنده تخلیه سنگین: 66، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 342، نسبت 0.32، سربار (%): 71، شمارنده تخلیه سنگین: 67، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 342، نسبت 0.33، سربار (%): 71، شمارنده تخلیه سنگین: 68، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 342، نسبت 0.32، سربار (%): 71، شمارنده تخلیه سنگین: 69، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 342، نسبت 0.32، سربار (%): 71، شمارنده تخلیه سنگین: 70، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 342، نسبت 0.33، سربار (%): 71، شمارنده تخلیه سنگین: 71، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 342، نسبت 0.33، سربار (%): 71، شمارنده تخلیه سنگین: 72، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 342، نسبت 0.33، سربار (%): 71، شمارنده تخلیه سنگین: 73، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 342، نسبت 0.33، سربار (%): 71، شمارنده تخلیه سنگین: 74، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 342، نسبت 0.33، سربار (%): 71، شمارنده تخلیه سنگین: 75، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 342، نسبت 0.33، سربار (%): 71، شمارنده تخلیه سنگین: 76، حافظه پنهان فعلی DataBlock (%): 22
اخراج شده (MB): 21، نسبت 0.33، سربار (%): -90، شمارشگر تخلیه سنگین: 76، حافظه پنهان فعلی DataBlock (%): 32
اخراج شده (MB): 0، نسبت 0.0، سربار (%): -100، شمارشگر تخلیه سنگین: 0، حافظه پنهان فعلی DataBlock (%): 100
اخراج شده (MB): 0، نسبت 0.0، سربار (%): -100، شمارشگر تخلیه سنگین: 0، حافظه پنهان فعلی DataBlock (%): 100
اسکنها برای نشان دادن همان فرآیند در قالب نموداری از رابطه بین دو بخش حافظه پنهان مورد نیاز بود - تک (جایی که بلوکهایی که قبلاً هرگز درخواست نشده بودند) و چندگانه (دادهها حداقل یک بار "درخواست شده" در اینجا ذخیره میشوند):
و در نهایت عملکرد پارامترها در قالب یک نمودار چگونه است. برای مقایسه، کش در ابتدا به طور کامل خاموش شد، سپس HBase با کش راه اندازی شد و شروع کار بهینه سازی را 5 دقیقه به تاخیر انداخت (30 چرخه تخلیه).
کد کامل را می توان در Pull Request یافت
با این حال، 300 هزار خواندن در ثانیه تمام آن چیزی نیست که می توان در این سخت افزار در این شرایط به دست آورد. واقعیت این است که هنگامی که شما نیاز به دسترسی به داده ها از طریق HDFS دارید، مکانیسم ShortCircuitCache (از این پس SSC نامیده می شود) استفاده می شود که به شما امکان می دهد مستقیماً به داده ها دسترسی داشته باشید و از تعاملات شبکه اجتناب کنید.
پروفایل نشان داد که اگرچه این مکانیسم سود زیادی به همراه دارد، اما در برخی مواقع به گلوگاه تبدیل می شود، زیرا تقریباً تمام عملیات سنگین در داخل یک قفل رخ می دهد که در بیشتر مواقع منجر به مسدود شدن می شود.
با درک این موضوع، متوجه شدیم که می توان با ایجاد آرایه ای از SSC های مستقل، مشکل را دور زد:
private final ShortCircuitCache[] shortCircuitCache;
...
shortCircuitCache = new ShortCircuitCache[this.clientShortCircuitNum];
for (int i = 0; i < this.clientShortCircuitNum; i++)
this.shortCircuitCache[i] = new ShortCircuitCache(…);
و سپس با آنها کار کنید، بدون در نظر گرفتن تقاطع ها در آخرین رقم افست:
public ShortCircuitCache getShortCircuitCache(long idx) {
return shortCircuitCache[(int) (idx % clientShortCircuitNum)];
}
اکنون می توانید آزمایش را شروع کنید. برای این کار فایل ها را از HDFS با یک اپلیکیشن ساده چند رشته ای می خوانیم. پارامترها را تنظیم کنید:
conf.set("dfs.client.read.shortcircuit", "true");
conf.set("dfs.client.read.shortcircuit.buffer.size", "65536"); // по дефолту = 1 МБ и это сильно замедляет чтение, поэтому лучше привести в соответствие к реальным нуждам
conf.set("dfs.client.short.circuit.num", num); // от 1 до 10
و فقط فایل ها را بخوانید:
FSDataInputStream in = fileSystem.open(path);
for (int i = 0; i < count; i++) {
position += 65536;
if (position > 900000000)
position = 0L;
int res = in.read(position, byteBuffer, 0, 65536);
}
این کد در رشته های جداگانه اجرا می شود و تعداد فایل های خوانده شده همزمان (از 10 به 200 - محور افقی) و تعداد کش ها (از 1 به 10 - گرافیک) را افزایش خواهیم داد. محور عمودی شتاب ناشی از افزایش SSC را نسبت به حالتی که فقط یک کش وجود دارد را نشان می دهد.
نحوه خواندن نمودار: زمان اجرا برای 100 هزار خواندن در بلوک های 64 کیلوبایتی با یک کش به 78 ثانیه نیاز دارد. در حالی که با 5 کش 16 ثانیه طول می کشد. آن ها شتاب ~ 5 برابر وجود دارد. همانطور که از نمودار مشاهده می شود، این اثر برای تعداد کمی از خواندن های موازی چندان قابل توجه نیست؛ زمانی که بیش از 50 خواندن نخ وجود داشته باشد، شروع به ایفای نقش قابل توجهی می کند. همچنین افزایش تعداد SSC ها از 6 قابل توجه است. و بالاتر افزایش عملکرد قابل توجهی کمتری را نشان می دهد.
نکته 1: از آنجایی که نتایج آزمایش کاملاً فرار است (به زیر مراجعه کنید)، 3 اجرا انجام شد و مقادیر حاصل به طور میانگین محاسبه شد.
نکته 2: افزایش عملکرد از پیکربندی دسترسی تصادفی یکسان است، اگرچه خود دسترسی کمی کندتر است.
با این حال، لازم به توضیح است که برخلاف مورد HBase، این شتاب همیشه رایگان نیست. در اینجا ما توانایی CPU را برای انجام بیشتر کارها، به جای آویزان کردن روی قفل ها، «قفل» می کنیم.
در اینجا می توانید مشاهده کنید که به طور کلی، افزایش تعداد کش ها باعث افزایش تقریباً متناسبی در استفاده از CPU می شود. با این حال، ترکیب های برنده کمی بیشتر وجود دارد.
به عنوان مثال، اجازه دهید نگاهی دقیق تر به تنظیم SSC = 3 بیندازیم. افزایش عملکرد در محدوده حدود 3.3 برابر است. در زیر نتایج حاصل از هر سه اجرا جداگانه است.
در حالی که مصرف CPU حدود 2.8 برابر افزایش می یابد. تفاوت چندان بزرگ نیست، اما گرتا کوچولو از قبل خوشحال است و ممکن است برای شرکت در مدرسه و درس خواندن وقت داشته باشد.
بنابراین، این امر برای هر ابزاری که از دسترسی انبوه به HDFS استفاده میکند (به عنوان مثال Spark و غیره) تأثیر مثبتی خواهد داشت، مشروط بر اینکه کد برنامه سبک باشد (یعنی دوشاخه در سمت مشتری HDFS باشد) و قدرت CPU رایگان وجود داشته باشد. . برای بررسی، بیایید آزمایش کنیم که استفاده ترکیبی از بهینهسازی BlockCache و تنظیم SSC برای خواندن از HBase چه تأثیری خواهد داشت.
می توان مشاهده کرد که در چنین شرایطی تأثیر به اندازه آزمایش های تصفیه شده (خواندن بدون هیچ گونه پردازش) عالی نیست، اما در اینجا می توان 80K اضافی را فشرده کرد. هر دو بهینه سازی با هم تا 4 برابر سرعت می دهند.
یک روابط عمومی نیز برای این بهینه سازی انجام شد
و در نهایت، مقایسه عملکرد خواندن یک پایگاه داده با ستون گسترده مشابه، Cassandra و HBase جالب بود.
برای انجام این کار، نمونههایی از ابزار استاندارد تست بار YCSB را از دو میزبان (در مجموع 800 رشته) راهاندازی کردیم. در سمت سرور - 4 نمونه از RegionServer و Cassandra در 4 میزبان (نه آنهایی که کلاینت ها در آن اجرا می شوند، برای جلوگیری از نفوذ آنها). قرائت ها از جداول اندازه گرفته شده اند:
HBase - 300 گیگابایت در HDFS (100 گیگابایت داده خالص)
کاساندرا - 250 گیگابایت (ضریب تکرار = 3)
آن ها حجم تقریباً یکسان بود (در HBase کمی بیشتر).
پارامترهای HBase:
dfs.client.short.circuit.num = 5 (بهینه سازی مشتری HDFS)
hbase.lru.cache.heavy.eviction.count.limit = 30 - این بدان معنی است که پچ پس از 30 تخلیه (~5 دقیقه) شروع به کار می کند.
hbase.lru.cache.heavy.eviction.mb.size.limit = 300 - حجم هدف ذخیره سازی و تخلیه
سیاهههای مربوط به YCSB تجزیه و در نمودارهای اکسل کامپایل شدند:
همانطور که می بینید این بهینه سازی ها امکان مقایسه عملکرد این پایگاه های داده در این شرایط و دستیابی به 450 هزار خواندن در ثانیه را فراهم می کند.
ما امیدواریم که این اطلاعات بتواند برای کسی در طول مبارزه هیجان انگیز برای بهره وری مفید باشد.
منبع: www.habr.com