"لقد زاد Kubernetes زمن الوصول بمقدار 10 مرات": من المسؤول عن ذلك؟

ملحوظة. ترجمة.: هذا المقال الذي كتبه جالو نافارو، الذي يشغل منصب مهندس البرمجيات الرئيسي في شركة Adevinta الأوروبية، هو "تحقيق" رائع ومفيد في مجال عمليات البنية التحتية. تم توسيع عنوانه الأصلي قليلاً في الترجمة لسبب يوضحه المؤلف في البداية.

"لقد زاد Kubernetes زمن الوصول بمقدار 10 مرات": من المسؤول عن ذلك؟

ملاحظة من المؤلف: يبدو مثل هذا المنصب ينجذب اهتمام أكثر بكثير مما كان متوقعا. مازلت أتلقى تعليقات غاضبة مفادها أن عنوان المقال مضلل وأن بعض القراء يشعرون بالحزن. أنا أفهم أسباب ما يحدث، لذلك، على الرغم من خطر تدمير المؤامرات بأكملها، أريد أن أخبرك على الفور عن موضوع هذه المقالة. الشيء الغريب الذي رأيته أثناء ترحيل الفرق إلى Kubernetes هو أنه كلما ظهرت مشكلة (مثل زيادة زمن الوصول بعد الترحيل)، فإن أول ما يتم إلقاء اللوم عليه هو Kubernetes، ولكن بعد ذلك اتضح أن المنسق ليس هو المسؤول حقًا لوم. تحكي هذه المقالة عن إحدى هذه الحالات. يكرر اسمه علامة تعجب أحد المطورين لدينا (سترى لاحقًا أن Kubernetes لا علاقة له به). لن تجد أي اكتشافات مفاجئة حول Kubernetes هنا، ولكن يمكنك أن تتوقع بعض الدروس الجيدة حول الأنظمة المعقدة.

منذ بضعة أسابيع، كان فريقي يقوم بترحيل خدمة صغيرة واحدة إلى منصة أساسية تتضمن CI/CD، ووقت تشغيل قائم على Kubernetes، ومقاييس، وغيرها من الأشياء الجيدة. كانت هذه الخطوة ذات طبيعة تجريبية: لقد خططنا لاتخاذها كأساس ونقل ما يقرب من 150 خدمة أخرى في الأشهر المقبلة. جميعهم مسؤولون عن تشغيل بعض أكبر المنصات عبر الإنترنت في إسبانيا (Infojobs، Fotocasa، وما إلى ذلك).

بعد أن قمنا بنشر التطبيق على Kubernetes وأعدنا توجيه بعض حركة المرور إليه، كانت تنتظرنا مفاجأة مثيرة للقلق. تأخير (وقت الإستجابة) كانت الطلبات في Kubernetes أعلى بعشر مرات منها في EC10. بشكل عام، كان من الضروري إما إيجاد حل لهذه المشكلة، أو التخلي عن ترحيل الخدمة الصغيرة (وربما المشروع بأكمله).

لماذا يكون زمن الاستجابة في Kubernetes أعلى بكثير منه في EC2؟

للعثور على عنق الزجاجة، قمنا بجمع المقاييس على طول مسار الطلب بأكمله. بنيتنا بسيطة: طلبات وكلاء بوابة API (Zuul) لمثيلات الخدمة الصغيرة في EC2 أو Kubernetes. في Kubernetes نستخدم NGINX Ingress Controller، والواجهات الخلفية هي كائنات عادية مثل قابل للفتح مع تطبيق JVM على منصة Spring.

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

يبدو أن المشكلة مرتبطة بزمن الاستجابة الأولي في الواجهة الخلفية (لقد حددت منطقة المشكلة على الرسم البياني بـ "xx"). في EC2، استغرقت استجابة التطبيق حوالي 20 مللي ثانية. في Kubernetes، زاد زمن الوصول إلى 100-200 مللي ثانية.

لقد رفضنا بسرعة المشتبه بهم المحتملين المرتبطين بتغيير وقت التشغيل. يظل إصدار JVM كما هو. لم يكن لمشاكل النقل بالحاويات أي علاقة بها: كان التطبيق يعمل بالفعل بنجاح في حاويات على EC2. تحميل؟ لكننا لاحظنا فترات استجابة عالية حتى عند طلب واحد في الثانية. يمكن أيضًا إهمال فترات التوقف المؤقت لجمع القمامة.

تساءل أحد مسؤولي 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) لمزيد من التحليل في يريشارك.

لم يكن هناك أي شيء مريب بشأن استعلامات 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 Instance Metadata في كل طلب.

الفرضية 2: المكالمات غير الضرورية إلى AWS

كلا نقطتي النهاية تنتمي إلى واجهة برمجة تطبيقات البيانات الوصفية لمثيلات AWS. تستخدم خدمتنا الصغيرة هذه الخدمة أثناء تشغيل 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 للعمل مع مثيلات EC2، وليس مع Kubernetes. ومن ناحية أخرى، لم نرغب في تغيير واجهة التطبيق. لهذا استخدمنا كيام - أداة تسمح للمستخدمين (المهندسين الذين ينشرون التطبيقات إلى مجموعة)، باستخدام الوكلاء في كل عقدة من عقد Kubernetes، بتعيين أدوار IAM للحاويات الموجودة في القرون كما لو كانت مثيلات EC2. يعترض KIAM المكالمات الواردة إلى خدمة AWS Instance Metadata ويعالجها من ذاكرة التخزين المؤقت الخاصة به، بعد أن استلمها مسبقًا من AWS. من وجهة نظر التطبيق، لا شيء يتغير.

تقوم KIAM بتزويد القرون بشهادات قصيرة الأجل. وهذا أمر منطقي بالنظر إلى أن متوسط ​​عمر الكبسولة أقصر من مثيل EC2. فترة الصلاحية الافتراضية للشهادات يساوي نفس 15 دقيقة.

ونتيجة لذلك، إذا قمت بتراكب القيمتين الافتراضيتين فوق بعضهما البعض، فستنشأ مشكلة. تنتهي صلاحية كل شهادة مقدمة للطلب بعد 15 دقيقة. ومع ذلك، تفرض AWS Java SDK تجديد أي شهادة يتبقى لها أقل من 15 دقيقة قبل تاريخ انتهاء صلاحيتها.

ونتيجة لذلك، يتم فرض تجديد الشهادة المؤقتة مع كل طلب، مما يستلزم إجراء عدة استدعاءات لواجهة برمجة تطبيقات 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

إضافة تعليق