لینک کوتاه مطلب : https://hsgar.com/?p=5477

چگونه یک مشکل عملکرد 7 ساله را در اکسیر کشف کردیم | توسط ییمینگ چن

در این پست، بررسی خواهیم کرد که چگونه در تلاش‌های خود برای بهینه‌سازی یک سرویس داخلی، به مشکل عملکردی برخوردیم که هفت سال است در پایگاه کد اکسیر وجود دارد. برای شروع، داشتن زمینه ای در مورد مشکلی که سعی در حل آن داشتیم مفید است. Tubi یک پلت فرم استریم ویدیو با میلیون ها کاربر است که هر روز فیلم ها و برنامه های تلویزیونی را به صورت رایگان پخش می کنند. هر بار که کاربر دکمه پخش را می‌زند، پخش‌کننده ویدیو با یک سرویس داخلی تماس API برقرار می‌کند که مانیفست پخش‌کننده ویدیو را از S3 واکشی می‌کند، برخی تغییرات را بر اساس پارامترهای درخواست انجام می‌دهد و فایل را به کاربر برمی‌گرداند. نکته دیگری که باید به آن توجه داشت این است که برخی از این مانیفست ها بزرگتر از بقیه هستند و/یا ممکن است نیاز به پردازش بیشتری داشته باشند و بسیاری از پردازش ها با استفاده از عبارات منظم انجام می شود.

معیارهای داخلی ما نشان داد که p99 برای این سرویس حدود 400 میلی‌ثانیه معلق است. با توجه به اینکه دستکاری ها نسبتاً ساده بودند، ما گمان کردیم که گلوگاه درخواست شبکه S3 باشد. فرضیه ای که ما با معیارهای محلی تأیید کردیم.

از آنجایی که این فراخوانی API در مسیر داغ قرار داشت، می‌خواستیم آن را بیشتر بهینه کنیم، بنابراین برای افزودن یک لایه کش به سراغ Redis رفتیم.

افسوس، زمانی که ما این لایه کش را به حدود 50 درصد از ترافیک خود اضافه کردیم، p99 آنطور که انتظار داشتیم پایین نیامد. در عوض، حدود 100 میلی‌ثانیه افزایش یافت. این البته آن چیزی نبود که ما انتظار داشتیم، بنابراین شروع به حفاری عمیق‌تر کردیم.

ما ابتدا حدس زدیم که افزایش p99 ناشی از افزایش بار CPU است. یک جزئیات مهم در پیاده‌سازی کش ما این است که قبل از ذخیره/بارگیری از Redis یک مرحله فشرده‌سازی/فشرده‌کردن اضافی اضافه کردیم. به طور خاص، ما استفاده می کنیم :erlang.term_to_binary/2با :compressed گزینه زمانی که به Redis و می نویسیم :erlang.binary_to_term/2 وقتی می خوانیم این تکنیکی است که در گذشته با موفقیت از آن استفاده کرده ایم. این عملیات هر دو هزینه انرژی CPU دارند. و پس از فعال شدن لایه کش، استفاده از CPU از 30% به ~40% افزایش یافت. اما حتی با استفاده از CPU بالاتر، با توجه به مدل همزمانی قدرتمند BEAM، ما انتظار نداشتیم که p99 تا این حد (100 میلی ثانیه) بالا برود. بنابراین ما عمیق تر حفاری کردیم.

سپس متوجه شدیم که اگرچه تأخیر p99 و تأخیر متوسط ​​افزایش یافته است، تأخیر متوسط ​​(p50) کاهش یافته است. این بدان معناست که با این لایه کش که به تازگی اضافه شده است، آنچه قبلاً یک درخواست سریع بود سریعتر شد، اما آنچه قبلاً یک درخواست آهسته بود کندتر شد. این نتیجه گیری نشان داد که گلوگاه واقعی برای این درخواست های کند ممکن است درخواست شبکه S3 نباشد، بلکه چیز دیگری باشد.

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

از این گزارش می توان دریافت که 89.60 درصد از زمان CPU صرف این امر شده است Regex.precompile_replacement/1 عملکرد. و در گزارش پروفایل هر فرآیند، Regex.precompile_replacement/1 همیشه زمان برترین عملکرد CPU بود و 30٪ -90٪ زمان CPU را می گرفت. با خواندن کد منبع اکسیر، متوجه شدیم که این تابع فراخوانی شده است Regex.replace/4، که تابعی بود که برای اصلاح فایل مانیفست استفاده کردیم.

بنابراین، این توضیح داد که چرا آنچه قبلاً یک درخواست سریع بود سریع‌تر شد، اما درخواست‌های کند کندتر شدند. الگوی رایج در تمام درخواست‌های سریع قبلی این بود که تماس نمی‌گرفتند Regex.replace/4 بنابراین گلوگاه آنها در واقع درخواست شبکه S3 بود. اما برای درخواست های آهسته، گلوگاه آنها در واقع این بود Regex.replace/4 زنگ زدن. لایه حافظه نهان زمان درخواست شبکه S3 را کاهش داد اما قدرت CPU بیشتری را مصرف کرد (فشرده کردن/فشرده کردن) که باعث شد Regex.replace/4 بیشتر طول می کشد، بنابراین درخواست های آهسته کندتر می شوند.

با دانستن اینکه Regex.replace/4 در واقع گلوگاه برای کندترین درخواست های ما بود، ما دوباره شروع به بهبود عملکرد این نقطه پایانی کردیم. توجه داشته باشید که در این مرحله، ما متوجه نشدیم و تصور نکردیم که مشکلات عملکرد واقعی در واقع در درون ما نهفته است Regex.replace/4. بنابراین ما قصد داشتیم تابعی را که فراخوانی می کند بازنویسی کنیم Regex.replace/4 برای تغییر محتوای فایل

ما با تنظیم یک اسکریپت معیار شروع کردیم Benchee به عنوان اولین گام برای بهبود عملکرد این عملکرد. با این اسکریپت بنچمارک، ما توانستیم به طور مداوم یک p99 با پردازش 100 میلی ثانیه برای بزرگترین فایل مانیفست که در مجموعه آزمایشی خود داشتیم، دریافت کنیم. به طور تصادفی، فایلی که ما انتخاب کردیم هیچ خطی نداشت که باید جایگزین شود. انتظار این بود که کد پردازش ما خیلی سریع فایل را بخواند. در عوض، هنوز حدود 100 میلی‌ثانیه در داخل آن صرف شده است Regex.replace/4 بدون تعویض چیزی بنابراین یک بهبود طبیعی به ذهن ما رسید: اضافه کردن a Regex.match?/2 قبل از تماس بررسی کنید Regex.replace/4. فقط با انجام این کار، عملکرد این تابع را تقریباً 100 میلی ثانیه برای این فایل ویژه بهبود بخشیم. و برای فایل هایی که 50٪ خطوط دارند باید جایگزین شوند، عملکرد ~50 میلی ثانیه افزایش یافته است.

در این مرحله، بهینه سازی ما تقریباً انجام شد. ما قصد داشتیم از چیزهای شیک تر استفاده کنیم nimble_parsec برای تجزیه و اصلاح محتوای فایل. اما با توجه به اینکه عملکرد پس از اضافه کردن این بی اهمیت است Regex.match?/2 پیش‌چک به اندازه کافی خوب بود، به نظر می‌رسد این بهینه‌سازی بیش از حد است.

اما این مانع از این نشد که بیشتر سوال کنیم: چرا اکسیر این بهینه سازی بی اهمیت را در خود ندارد؟ انجام داد :re.replace در ارلنگ re ماژول هم همین مشکل رو داره؟ بنابراین ما نسخه دیگری را با استفاده از Erlang به اسکریپت بنچمارک خود اضافه کردیم re مدول. نتیجه تقریباً به همان سرعتی بود که داشتن یک Regex.match?/2 از پیش بررسی کنید. یعنی اکسیر Regex.replace/4 وقتی چیزی برای تعویض وجود نداشت، مشکل عملکرد داشت.

با جستجو در کد منبع Elixir، متوجه شدیم که Elixir این کار را کرده است قبل از تعویض، بررسی کنید که آیا مطابقت دارد یا خیر. اما قبل از این چک، اکسیر به نام Regex.precompile_replacement/1 عملکرد، که منجر به این کار محاسباتی سنگین زمانی شد که چیزی برای جایگزینی وجود نداشت. (این با نتیجه نمایه سازی ما مطابقت داشت eprof.)

از طرفی ارلنگ re مدول این را ندارد precompile_replacement/1 منطق. بنابراین وقتی هیچ مسابقه ای وجود نداشت، :re.replace به عنوان یک تابع noop عمل کرد.

پس از تأیید اینکه این واقعاً یک مشکل اکسیر است، ما یک PR برای رفع آن ارسال کردیم: سرعت دادن Regex.replace/4 وقتی هیچ مطابقتی وجود ندارد توسط dsdshcym · درخواست کشش #10500 · elixir-lang/elixir.

(توجه شد که این روابط عمومی در 10 دقیقه ادغام شد که بسیار چشمگیر بود.)

اگرچه رفع نهایی بی اهمیت بود، اما ما هنوز از این فرآیند جالب اشکال زدایی چیزهای زیادی یاد گرفتیم.

  1. نمایه با داده های واقعی در شرایط واقعی
    اولین درس این است که همیشه با داده های واقعی نمایه کنید. ما ابتدا این نقطه پایانی را با برخی داده‌های بی‌اهمیت در ماشین محلی خود محک زدیم، که فرض ما را ثابت کرد که درخواست شبکه S3 گلوگاه است. اما درخواست های دنیای واقعی پیچیده تر از این بود. همانطور که در بالا آموختیم، دو نوع درخواست وجود داشت، CPU فشرده و I/O فشرده. اگر نمی دویدیم eprof در یک سرور واقعی، ما نتوانستیم علت اصلی این مشکل عملکرد را شناسایی کنیم.
  2. در بدترین حالت، گلوگاه واقعی را شناسایی کنید
    مشابه درس اول، هنگام محک زدن، باید بدترین ورودی را شناسایی کنیم و در مقابل آن معیار قرار دهیم. ما از برخی ورودی‌های بی‌اهمیت در زمانی که در محلی خود محک زدیم استفاده کردیم، که منجر به نتیجه‌گیری مغرضانه و گمراه‌کننده شد. جابجایی به بزرگترین فایل مانیفست به ما کمک کرد تا علت اصلی را سریعتر شناسایی کنیم.
  3. به منبع باز کمک کنید
    با چرخش به عنوان کلیک- طعمه، این Regex.replace/4 موضوع عملکرد از ابتدای کار یعنی 7 سال پیش به اکسیر وارد شده است. من تعجب کردم که چرا این باگ قبلا توسط دیگران پیدا نشد. حدس من این است که هیچ کس استفاده نمی کند Regex.replace/4 در این مقیاس (تماس Regex.replace/4 هزاران بار در خطوط بدون مسابقه در یک چرخه درخواست). شاید این مشکل برای دیگران کمتر باشد، سپس رفع این نوع مشکل نادر در بالادست اهمیت بیشتری دارد تا دیگران در آینده از این لبه مورد آسیب نبینند. به علاوه، مشارکت در Elixir بهترین تجربه منبع باز بوده است که یک توسعه‌دهنده می‌تواند درخواست کند: 10 دقیقه و کد ما به صورت اصلی در می‌آید، باورنکردنی!

اگر کار بر روی اکسیر در مقیاس شما را هیجان زده می کند، پس به ما در توبی بپیوندید، جایی که از Elixir برای تقویت محتوای استودیو زنده و درخواستی به ده ها میلیون کاربر استفاده می کنیم.

لینک منبع

ارسال یک پاسخ

آدرس ایمیل شما منتشر نخواهد شد.