التجربة والخطأ أثناء اختيار وكيل HTTP العكسي

مرحبا بالجميع!

اليوم نريد أن نتحدث عن كيفية حل فريق خدمة حجز الفنادق في Ostrovok.ru لمشكلة نمو الخدمات المصغرة ، والتي تتمثل مهمتها في تبادل المعلومات مع موردينا. حول تجربته يروي undying ، DevOps Team Lead في Ostrovok.ru.


في البداية ، كانت الخدمة الصغيرة صغيرة الحجم وكانت تؤدي الوظائف التالية:

  • قبول طلب من خدمة محلية ؛
  • تقديم طلب إلى شريك ؛
  • تطبيع الاستجابة ؛
  • إرجاع النتيجة إلى خدمة الاستعلام.

ومع ذلك ، بمرور الوقت ، نمت الخدمة مع عدد الشركاء والطلبات المقدمة لهم.

مع نمو الخدمة ، بدأت أنواع مختلفة من المشاكل في الظهور. طرح الموردون المختلفون قواعدهم الخاصة: يحد شخص ما من الحد الأقصى لعدد الاتصالات ، ويقيد شخص ما العملاء بالقوائم البيضاء.

نتيجة لذلك ، كان علينا حل المشاكل التالية:

  • من المستحسن أن يكون لديك عدة عناوين IP خارجية ثابتة حتى تتمكن من توفيرها للشركاء لإضافتها إلى القوائم البيضاء ،
  • لدينا مجموعة واحدة من الوصلات لجميع الموردين بحيث يظل عدد الوصلات عند الحد الأدنى ،
  • إنهاء SSL والحفاظ على keepalive في مكان واحد ، وبالتالي تقليل العبء على الشركاء أنفسهم.

لم يفكروا لفترة طويلة وتساءلوا على الفور عما يجب اختياره: Nginx أو Haproxy.
في البداية ، تأرجح البندول نحو Nginx ، نظرًا لأنني حللت معظم المشكلات المرتبطة بـ HTTP / HTTPS بمساعدتها وكنت راضيًا دائمًا عن النتيجة.

كان المخطط بسيطًا: تم تقديم طلب إلى Proxy Server الجديد على Nginx مع مجال من النموذج <partner_tag>.domain.local ، في Nginx كانت هناك map حيث <partner_tag> تقابل عنوان الشريك. تم أخذ عنوان من map وتم إجراء proxy_pass على هذا العنوان.

فيما يلي مثال map نقوم بتحليل النطاق بها وتحديد المنبع من القائمة:

 ###     : <tag>.domain.local map $http_host $upstream_prefix { default 0; "~^([^\.]+)\." $1; } ###      map $upstream_prefix $upstream_address { include snippet.d/upstreams_map; default http://127.0.0.1:8080; } ###   upstream_host    upstream_address map $upstream_address $upstream_host { default 0; "~^https?://([^:]+)" $1; } 

وهنا يبدو شكل " snippet.d/upstreams_map ":
 “one” “http://one.domain.net”; “two” “https://two.domain.org”; 

هنا لدينا server{} :

 server { listen 80; location / { proxy_http_version 1.1; proxy_pass $upstream_address$request_uri; proxy_set_header Host $upstream_host; proxy_set_header X-Forwarded-For ""; proxy_set_header X-Forwarded-Port ""; proxy_set_header X-Forwarded-Proto ""; } } # service for error handling and logging server { listen 127.0.0.1:8080; location / { return 400; } location /ngx_status/ { stub_status; } } 

كل شيء رائع ، كل شيء يعمل. من الممكن إنهاء هذه المقالة ، إن لم يكن بفارق بسيط.

عند استخدام proxy_pass ، ينتقل الطلب مباشرة إلى العنوان المرغوب ، كقاعدة عامة ، باستخدام بروتوكول HTTP / 1.0 دون keepalive ويغلق فورًا بعد اكتمال الاستجابة. حتى لو قمنا proxy_http_version 1.1 ، فلن يتغير أي شيء دون تغيير المصدر ( proxy_http_version ).

ما يجب القيام به الفكر الأول هو جعل جميع الموردين في المنبع ، حيث سيكون الخادم هو عنوان المورد الذي نحتاجه ، وفي map احتفظ "tag" "upstream_name" .

أضف map أخرى لتحليل المخطط:

 ###     : <tag>.domain.local map $http_host $upstream_prefix { default 0; "~^([^\.]+)\." $1; } ###      map $upstream_prefix $upstream_address { include snippet.d/upstreams_map; default http://127.0.0.1:8080; } ###   upstream_host    upstream_address map $upstream_address $upstream_host { default 0; "~^https?://([^:]+)" $1; } ###   ,       https,    ,    -  http map $upstream_address $upstream_scheme { default "http://"; "~(https?://)" $1; } 

وإنشاء upstreams مع أسماء العلامات:
  upstream one { keepalive 64; server one.domain.com; } upstream two { keepalive 64; server two.domain.net; } 

تم تعديل الخادم نفسه قليلاً ليأخذ في الاعتبار المخطط واستخدام اسم المنبع بدلاً من العنوان:

 server { listen 80; location / { proxy_http_version 1.1; proxy_pass $upstream_scheme$upstream_prefix$request_uri; proxy_set_header Host $upstream_host; proxy_set_header X-Forwarded-For ""; proxy_set_header X-Forwarded-Port ""; proxy_set_header X-Forwarded-Proto ""; } } # service for error handling and logging server { listen 127.0.0.1:8080; location / { return 400; } location /ngx_status/ { stub_status; } } 

عظيم يعمل الحل ، قم بإضافة التوجيه keepalive في كل منبع ، قم بتعيين proxy_http_version 1.1 ، والآن لدينا تجمع اتصال ، وكل شيء يعمل كما يجب.

هذه المرة ، يمكنك بالتأكيد الانتهاء من المقالة والذهاب شرب الشاي. أم لا؟

في الواقع ، بينما نشرب الشاي ، قد يقوم أحد الموردين بتغيير عنوان IP أو مجموعة من العناوين ضمن نفس المجال (مرحبًا ، Amazon) ، وبالتالي قد يسقط أحد الموردين في ذروة حفلة الشاي الخاصة بنا.

حسنا ، ماذا تفعل؟ تتميز Nginx بفارق بسيط: أثناء إعادة التحميل ، يمكنها أن تفيق ​​الخوادم داخل upstream إلى عناوين جديدة وتضع زيارات عليها. بشكل عام ، أيضا الحل. رمي في cron reload nginx كل 5 دقائق والاستمرار في شرب الشاي.

لكن لا يزال الأمر يبدو لي قرارًا جيدًا ، لذلك بدأت أتطلع إلى هابروكسي.

لدى Haproxy القدرة على تحديد dns resolvers dns cache وتكوين dns cache لنظام dns resolvers dns cache . وبالتالي ، ستقوم Haproxy بتحديث dns cache لنظام dns cache إذا انتهت صلاحية الإدخالات الموجودة بها ، وستستبدل العناوين الخاصة بالتيار الرئيسي إذا كانت قد تغيرت.

عظيم! الآن الأمر متروك للإعدادات.

فيما يلي مثال تكوين قصير لـ Haproxy:

 frontend http bind *:80 http-request del-header X-Forwarded-For http-request del-header X-Forwarded-Port http-request del-header X-Forwarded-Proto capture request header Host len 32 capture request header Referer len 128 capture request header User-Agent len 128 acl host_present hdr(host) -m len gt 0 use_backend %[req.hdr(host),lower,field(1,'.')] if host_present default_backend default resolvers dns hold valid 1s timeout retry 100ms nameserver dns1 1.1.1.1:53 backend one http-request set-header Host one.domain.com server one--one.domain.com one.domain.com:80 resolvers dns check backend two http-request set-header Host two.domain.net server two--two.domain.net two.domain.net:443 resolvers dns check ssl verify none check-sni two.domain.net sni str(two.domain.net) 

يبدو أن كل شيء يعمل كما يجب هذه المرة. الشيء الوحيد الذي لا يعجبني في Haproxy هو تعقيد وصف التكوين. تحتاج إلى إنشاء الكثير من النص لإضافة واحد يعمل المنبع. لكن الكسل هو محرك التقدم: إذا كنت لا تريد أن تكتب نفس الشيء ، فاكتب مولدًا.

كان لديّ بالفعل خريطة من Nginx تحتوي على تنسيق "tag" "upstream" ، لذلك قررت أن أعتبرها أساسًا ، وقم بتحليل وإنشاء الواجهة الخلفية لـ haproxy استنادًا إلى هذه القيم.

 #! /usr/bin/env bash haproxy_backend_map_file=./root/etc/haproxy/snippet.d/name_domain_map haproxy_backends_file=./root/etc/haproxy/99_backends.cfg nginx_map_file=./nginx_map while getopts 'n:b:m:' OPT;do case ${OPT} in n) nginx_map_file=${OPTARG} ;; b) haproxy_backends_file=${OPTARG} ;; m) haproxy_backend_map_file=${OPTARG} ;; *) echo 'Usage: ${0} -n [nginx_map_file] -b [haproxy_backends_file] -m [haproxy_backend_map_file]' exit esac done function write_backend(){ local tag=$1 local domain=$2 local port=$3 local server_options="resolvers dns check" [ -n "${4}" ] && local ssl_options="ssl verify none check-sni ${domain} sni str(${domain})" [ -n "${4}" ] && server_options+=" ${ssl_options}" cat >> ${haproxy_backends_file} <<EOF backend ${tag} http-request set-header Host ${domain} server ${tag}--${domain} ${domain}:${port} ${server_options} EOF } :> ${haproxy_backends_file} :> ${haproxy_backend_map_file} while read tag addr;do tag=${tag//\"/} [ -z "${tag:0}" ] && continue [ "${tag:0:1}" == "#" ] && continue IFS=":" read scheme domain port <<<${addr//;} unset IFS domain=${domain//\/} case ${scheme} in http) port=${port:-80} write_backend ${tag} ${domain} ${port} ;; https) port=${port:-443} write_backend ${tag} ${domain} ${port} 1 esac done < <(sort -V ${nginx_map_file}) 

الآن كل ما نحتاج إليه هو إضافة مضيف جديد في nginx_map ، وبدء المولد والحصول على تهيئة haproxy الجاهزة.

هذا ربما كل شيء لهذا اليوم. تشير هذه المقالة إلى المادة التمهيدية وتم تكريسها لمشكلة اختيار الحل وإدماجه في البيئة الحالية.

في المقالة التالية ، سوف أخبركم أكثر عن المخاطر التي واجهناها عند استخدام Haproxy ، والتي تبين أن المقاييس مفيدة لرصدها ، وما ينبغي تحسينه بالضبط في النظام من أجل الحصول على أقصى أداء من الخوادم.

شكرا لكم جميعا على اهتمامكم ، أراك!

Source: https://habr.com/ru/post/ar436992/


All Articles