"Kubernetes تاخیر را 10 برابر افزایش داد": چه کسی در این امر مقصر است؟

توجه داشته باشید. ترجمه: این مقاله توسط گالو ناوارو که سمت مهندس نرم افزار اصلی در شرکت اروپایی Adevinta را بر عهده دارد، یک "تحقیق" جذاب و آموزنده در زمینه عملیات زیرساخت است. عنوان اصلی آن به دلیلی که نویسنده در همان ابتدا توضیح می دهد در ترجمه کمی بسط داده شده است.

"Kubernetes تاخیر را 10 برابر افزایش داد": چه کسی در این امر مقصر است؟

یادداشت از نویسنده: شبیه این پست است جذب کرد توجه بسیار بیشتر از حد انتظار من هنوز از نظراتی عصبانی می شوم که عنوان مقاله گمراه کننده است و برخی از خوانندگان ناراحت هستند. من دلایل آنچه اتفاق می افتد را درک می کنم، بنابراین، با وجود خطر از بین بردن کل فتنه، می خواهم بلافاصله به شما بگویم که این مقاله در مورد چیست. نکته جالبی که در هنگام مهاجرت تیم ها به Kubernetes دیده ام این است که هر زمان مشکلی پیش می آید (مانند افزایش تاخیر پس از مهاجرت)، اولین چیزی که مورد سرزنش قرار می گیرد کوبرنتیس است، اما بعد معلوم می شود که ارکستراتور واقعاً این کار را نمی کند. سرزنش کردن این مقاله در مورد یکی از این موارد می گوید. نام آن تکرار تعجب یکی از توسعه دهندگان ما است (بعداً خواهید دید که Kubernetes هیچ ربطی به آن ندارد). در اینجا هیچ افشاگری شگفت انگیزی در مورد Kubernetes پیدا نخواهید کرد، اما می توانید منتظر چند درس خوب در مورد سیستم های پیچیده باشید.

چند هفته پیش، تیم من در حال انتقال یک میکروسرویس به یک پلتفرم اصلی بود که شامل CI/CD، زمان اجرا مبتنی بر Kubernetes، متریک‌ها و موارد دیگر بود. این حرکت ماهیت آزمایشی داشت: ما برنامه ریزی کردیم که آن را به عنوان مبنایی در نظر بگیریم و در ماه های آینده تقریباً 150 سرویس دیگر را منتقل کنیم. همه آنها مسئول عملکرد برخی از بزرگترین پلتفرم های آنلاین در اسپانیا (Infojobs، Fotocasa و غیره) هستند.

پس از اینکه برنامه را در Kubernetes مستقر کردیم و مقداری ترافیک را به آن هدایت کردیم، یک شگفتی نگران کننده در انتظار ما بود. تاخیر انداختن (تاخیر) درخواست ها در Kubernetes 10 برابر بیشتر از EC2 بود. به طور کلی، یا باید راه حلی برای این مشکل پیدا کرد، یا از مهاجرت میکروسرویس (و احتمالاً کل پروژه) صرف نظر کرد.

چرا تأخیر در Kubernetes بسیار بیشتر از EC2 است؟

برای یافتن گلوگاه، معیارهایی را در کل مسیر درخواست جمع آوری کردیم. معماری ما ساده است: یک دروازه API (Zuul) درخواست ها را به نمونه های میکروسرویس در EC2 یا Kubernetes پراکسی می کند. در Kubernetes ما از NGINX Ingress Controller استفاده می کنیم و backendها اشیاء معمولی هستند گسترش با یک برنامه JVM در پلتفرم Spring.

                                  EC2
                            +---------------+
                            |  +---------+  |
                            |  |         |  |
                       +-------> BACKEND |  |
                       |    |  |         |  |
                       |    |  +---------+  |                   
                       |    +---------------+
             +------+  |
Public       |      |  |
      -------> ZUUL +--+
traffic      |      |  |              Kubernetes
             +------+  |    +-----------------------------+
                       |    |  +-------+      +---------+ |
                       |    |  |       |  xx  |         | |
                       +-------> NGINX +------> BACKEND | |
                            |  |       |  xx  |         | |
                            |  +-------+      +---------+ |
                            +-----------------------------+

به نظر می رسید مشکل مربوط به تاخیر اولیه در باطن باشد (من منطقه مشکل را در نمودار به عنوان "xx" علامت گذاری کردم). در EC2، پاسخ برنامه حدود 20 میلی ثانیه طول کشید. در Kubernetes، تأخیر به 100-200 میلی ثانیه افزایش یافت.

ما به سرعت مظنونین احتمالی مربوط به تغییر زمان اجرا را اخراج کردیم. نسخه JVM به همان صورت باقی می ماند. مشکلات کانتینرسازی نیز ربطی به آن نداشت: برنامه قبلاً با موفقیت در کانتینرهای EC2 اجرا می شد. بارگذاری؟ اما ما تاخیرهای بالایی را حتی در 1 درخواست در ثانیه مشاهده کردیم. مکث برای جمع آوری زباله نیز می تواند نادیده گرفته شود.

یکی از مدیران Kubernetes ما تعجب کرد که آیا برنامه دارای وابستگی های خارجی است زیرا پرس و جوهای DNS مشکلات مشابهی را در گذشته ایجاد کرده بودند.

فرضیه 1: وضوح نام DNS

برای هر درخواست، برنامه ما یک تا سه بار به یک نمونه AWS Elasticsearch در دامنه ای مانند دسترسی پیدا می کند. elastic.spain.adevinta.com. داخل ظروف ما یک پوسته وجود دارد، بنابراین ما می توانیم بررسی کنیم که آیا جستجوی یک دامنه واقعاً زمان زیادی می برد.

جستارهای DNS از کانتینر:

[root@be-851c76f696-alf8z /]# while true; do dig "elastic.spain.adevinta.com" | grep time; sleep 2; done
;; Query time: 22 msec
;; Query time: 22 msec
;; Query time: 29 msec
;; Query time: 21 msec
;; Query time: 28 msec
;; Query time: 43 msec
;; Query time: 39 msec

درخواست های مشابه از یکی از نمونه های EC2 که در آن برنامه در حال اجرا است:

bash-4.4# while true; do dig "elastic.spain.adevinta.com" | grep time; sleep 2; done
;; Query time: 77 msec
;; Query time: 0 msec
;; Query time: 0 msec
;; Query time: 0 msec
;; Query time: 0 msec

با توجه به اینکه جستجو حدود 30 میلی ثانیه طول کشید، مشخص شد که وضوح DNS هنگام دسترسی به Elasticsearch واقعاً به افزایش تأخیر کمک می کند.

با این حال، این به دو دلیل عجیب بود:

  1. ما در حال حاضر تعداد زیادی برنامه Kubernetes داریم که با منابع AWS ارتباط برقرار می کنند بدون اینکه از تاخیر زیاد رنج ببرند. دلیل هر چه باشد، به طور خاص به این مورد مربوط می شود.
  2. ما می دانیم که JVM ذخیره سازی DNS درون حافظه را انجام می دهد. در تصاویر ما، مقدار TTL در نوشته شده است $JAVA_HOME/jre/lib/security/java.security و روی 10 ثانیه تنظیم کنید: networkaddress.cache.ttl = 10. به عبارت دیگر، JVM باید تمام کوئری های DNS را به مدت 10 ثانیه کش کند.

برای تایید فرضیه اول، تصمیم گرفتیم برای مدتی تماس DNS را متوقف کنیم و ببینیم آیا مشکل برطرف شده است یا خیر. ابتدا تصمیم گرفتیم برنامه را مجدداً پیکربندی کنیم تا مستقیماً با Elasticsearch از طریق آدرس IP ارتباط برقرار کند نه از طریق نام دامنه. این به تغییرات کد و استقرار جدید نیاز دارد، بنابراین ما به سادگی دامنه را به آدرس IP آن نگاشت کردیم /etc/hosts:

34.55.5.111 elastic.spain.adevinta.com

اکنون ظرف تقریباً بلافاصله یک IP دریافت کرد. این منجر به بهبودهایی شد، اما ما فقط اندکی به سطوح تاخیر مورد انتظار نزدیکتر بودیم. اگرچه رزولوشن DNS زمان زیادی طول کشید، اما دلیل واقعی هنوز از ما دور مانده است.

تشخیص از طریق شبکه

تصمیم گرفتیم با استفاده از کانتینر ترافیک را تجزیه و تحلیل کنیم tcpdumpبرای دیدن اینکه دقیقا چه اتفاقی در شبکه می افتد:

[root@be-851c76f696-alf8z /]# tcpdump -leni any -w capture.pcap

سپس چندین درخواست فرستادیم و ضبط آنها را دانلود کردیم (kubectl cp my-service:/capture.pcap capture.pcap) برای تحلیل بیشتر در Wireshark.

هیچ چیز مشکوکی در مورد جستارهای DNS وجود نداشت (به جز یک چیز کوچک که بعداً در مورد آن صحبت خواهم کرد). اما در نحوه رسیدگی سرویس ما به هر درخواست عجیب و غریب خاصی وجود داشت. در زیر یک اسکرین شات از عکس گرفته شده است که نشان می دهد درخواست قبل از شروع پاسخ پذیرفته شده است:

"Kubernetes تاخیر را 10 برابر افزایش داد": چه کسی در این امر مقصر است؟

شماره بسته ها در ستون اول نشان داده شده است. برای وضوح، من جریان های مختلف TCP را با رنگ کدگذاری کرده ام.

جریان سبز که با بسته 328 شروع می شود نشان می دهد که چگونه مشتری (172.17.22.150) یک اتصال TCP را به کانتینر (172.17.36.147) برقرار کرد. پس از دست دادن اولیه (328-330)، بسته 331 آورده شد HTTP GET /v1/.. - یک درخواست ورودی به خدمات ما. کل فرآیند 1 میلی ثانیه طول کشید.

جریان خاکستری (از بسته 339) نشان می دهد که سرویس ما یک درخواست HTTP را به نمونه Elasticsearch ارسال کرده است (هیچ دست دادن TCP وجود ندارد زیرا از یک اتصال موجود استفاده می کند). این 18 میلی‌ثانیه طول کشید.

تا اینجا همه چیز خوب است و زمان ها تقریباً با تأخیرهای مورد انتظار مطابقت دارد (20-30 میلی ثانیه وقتی از مشتری اندازه گیری می شود).

با این حال، بخش آبی 86 میلی‌ثانیه طول می‌کشد. در آن چه خبر است؟ با بسته 333، سرویس ما یک درخواست HTTP GET به آن ارسال کرد /latest/meta-data/iam/security-credentialsو بلافاصله پس از آن، از طریق همان اتصال TCP، یک درخواست GET دیگر به /latest/meta-data/iam/security-credentials/arn:...

ما متوجه شدیم که این با هر درخواست در طول ردیابی تکرار می شود. وضوح DNS در کانتینرهای ما در واقع کمی کندتر است (توضیحات این پدیده بسیار جالب است، اما من آن را برای یک مقاله جداگانه ذخیره می کنم). معلوم شد که علت تاخیرهای طولانی تماس با سرویس فراداده نمونه AWS در هر درخواست بوده است.

فرضیه 2: تماس های غیر ضروری به AWS

هر دو نقطه پایانی متعلق به AWS Instance Metadata API. میکروسرویس ما هنگام اجرای Elasticsearch از این سرویس استفاده می کند. هر دو تماس بخشی از فرآیند مجوز اولیه هستند. نقطه پایانی که در اولین درخواست به آن دسترسی پیدا می‌کند، نقش IAM مرتبط با نمونه را صادر می‌کند.

/ # curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
arn:aws:iam::<account_id>:role/some_role

درخواست دوم از نقطه پایانی دوم مجوزهای موقت برای این نمونه می خواهد:

/ # curl http://169.254.169.254/latest/meta-data/iam/security-credentials/arn:aws:iam::<account_id>:role/some_role`
{
    "Code" : "Success",
    "LastUpdated" : "2012-04-26T16:39:16Z",
    "Type" : "AWS-HMAC",
    "AccessKeyId" : "ASIAIOSFODNN7EXAMPLE",
    "SecretAccessKey" : "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    "Token" : "token",
    "Expiration" : "2017-05-17T15:09:54Z"
}

مشتری می تواند برای مدت کوتاهی از آنها استفاده کند و باید به طور دوره ای گواهی های جدید (قبل از آنها) دریافت کند Expiration). مدل ساده است: AWS به دلایل امنیتی مکررا کلیدهای موقت را می چرخاند، اما کلاینت ها می توانند آن ها را برای چند دقیقه ذخیره کنند تا جریمه عملکرد مرتبط با دریافت گواهی های جدید را جبران کنند.

AWS Java SDK باید مسئولیت سازماندهی این فرآیند را بر عهده بگیرد، اما به دلایلی این اتفاق نمی افتد.

پس از جستجوی مشکلات در GitHub، با مشکلی مواجه شدیم #1921. او به ما کمک کرد تا مسیری را که در آن «کاوش کنیم» بیشتر تعیین کنیم.

AWS SDK گواهی ها را زمانی که یکی از شرایط زیر رخ می دهد به روز می کند:

  • تاریخ انقضا (Expiration) افتادن در EXPIRATION_THRESHOLD، به مدت 15 دقیقه کدگذاری شده است.
  • زمان بیشتری از آخرین تلاش برای تمدید گواهی ها گذشته است REFRESH_THRESHOLD، به مدت 60 دقیقه کدگذاری شده است.

برای دیدن تاریخ انقضای واقعی گواهی هایی که دریافت می کنیم، دستورات cURL بالا را هم از کانتینر و هم از نمونه EC2 اجرا کردیم. مدت اعتبار گواهی دریافت شده از ظرف بسیار کوتاهتر بود: دقیقاً 15 دقیقه.

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

چرا مدت اعتبار گواهینامه ها کوتاه شده است؟

AWS Instance Metadata برای کار با نمونه های EC2 طراحی شده است نه Kubernetes. از طرف دیگر، ما نمی خواستیم رابط کاربری را تغییر دهیم. برای این استفاده کردیم کیام - ابزاری که با استفاده از عوامل روی هر گره Kubernetes، به کاربران (مهندسینی که برنامه‌ها را در یک خوشه مستقر می‌کنند) اجازه می‌دهد تا نقش‌های IAM را به کانتینرهایی در پادها اختصاص دهند که گویی نمونه‌های EC2 هستند. KIAM تماس‌های سرویس فراداده نمونه AWS را رهگیری می‌کند و آنها را از حافظه پنهان خود پردازش می‌کند، زیرا قبلاً آنها را از AWS دریافت کرده است. از نظر کاربردی، هیچ چیز تغییر نمی کند.

KIAM گواهینامه های کوتاه مدت را به غلاف ها ارائه می کند. با توجه به اینکه میانگین طول عمر یک غلاف کوتاهتر از یک نمونه EC2 است، منطقی است. مدت اعتبار پیش فرض گواهی ها برابر با همان 15 دقیقه.

در نتیجه، اگر هر دو مقدار پیش فرض را روی هم قرار دهید، مشکلی ایجاد می شود. هر گواهی ارائه شده به یک برنامه پس از 15 دقیقه منقضی می شود. با این حال، AWS Java SDK هر گواهی را که کمتر از 15 دقیقه تا تاریخ انقضای آن باقی مانده است، تمدید می کند.

در نتیجه، گواهی موقت مجبور می شود با هر درخواست تجدید شود، که مستلزم چند تماس با API AWS است و منجر به افزایش قابل توجه تاخیر می شود. در AWS Java SDK ما پیدا کردیم درخواست ویژگی، که به مشکل مشابهی اشاره می کند.

راه حل ساده بود. ما به سادگی KIAM را برای درخواست گواهینامه هایی با دوره اعتبار طولانی تر پیکربندی کردیم. هنگامی که این اتفاق افتاد، درخواست‌ها بدون مشارکت سرویس AWS Metadata شروع به جریان یافتن کردند و تأخیر حتی به سطوح پایین‌تری نسبت به EC2 کاهش یافت.

یافته ها

بر اساس تجربه ما در مورد مهاجرت، یکی از رایج ترین منابع مشکلات، اشکالات در Kubernetes یا سایر عناصر پلت فرم نیست. همچنین هیچ نقص اساسی در میکروسرویس هایی که ما انتقال می دهیم برطرف نمی کند. مشکلات اغلب به این دلیل به وجود می آیند که ما عناصر مختلف را کنار هم قرار می دهیم.

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

در مورد ما، تأخیر بالا نتیجه اشکالات یا تصمیمات بد در Kubernetes، KIAM، AWS Java SDK، یا میکروسرویس ما نبوده است. این نتیجه ترکیب دو تنظیمات پیش فرض مستقل بود: یکی در KIAM و دیگری در AWS Java SDK. به طور جداگانه هر دو پارامتر معنی دارند: سیاست تمدید گواهی فعال در AWS Java SDK و مدت اعتبار کوتاه گواهینامه ها در KAIM. اما وقتی آنها را کنار هم قرار دهید، نتایج غیرقابل پیش بینی می شوند. لازم نیست دو راه حل مستقل و منطقی هنگام ترکیب منطقی باشند.

PS از مترجم

می توانید در مورد معماری ابزار KIAM برای ادغام AWS IAM با Kubernetes اطلاعات بیشتری کسب کنید. این مقاله از سازندگان آن

همچنین در وبلاگ ما بخوانید:

منبع: www.habr.com

اضافه کردن نظر