في حالة تنظيم الخدمات المصغرة للتطبيق ، يرتكز العمل الجوهري على آليات التواصل المتكامل للخدمات الصغرى. علاوة على ذلك ، يجب أن يكون هذا التكامل متسامحًا مع الخطأ ، مع درجة عالية من التوفر.
في حلولنا ، نستخدم التكامل مع Kafka و gRPC و RabbitMQ.
في هذه المقالة ، سوف نشارك تجربتنا في تجميع RabbitMQ ، الذي يتم استضافة عقده على Kubernetes.

قبل الإصدار 3.7 من RabbitMQ ، لم يكن تجميعه في K8S مهمة تافهة للغاية ، مع العديد من الاختراقات والحلول غير الجميلة للغاية. في الإصدار 3.6 ، تم استخدام البرنامج المساعد autocluster من RabbitMQ Community. وفي 3.7 ظهرت Kubernetes Peer Discovery Backend. إنه مدمج بواسطة المكون الإضافي في التوصيل الأساسي لـ RabbitMQ ولا يتطلب تجميعًا وتثبيتًا منفصلين.
سنصف التكوين النهائي ككل ، مع التعليق على ما يحدث.
نظريا
يحتوي البرنامج المساعد على
مستودع على جيثب ، حيث يوجد
مثال للاستخدام الأساسي .
هذا المثال غير مخصص للإنتاج ، وهو موضح بوضوح في وصفه ، علاوة على ذلك ، تم ضبط بعض الإعدادات فيه على عكس منطق الاستخدام في المنتج. أيضًا ، في المثال ، لم يتم ذكر استمرار التخزين على الإطلاق ، لذلك في أي حالة طارئة ستتحول مجموعتنا إلى زيلك.
عمليا
الآن سنخبرك بما واجهته بنفسك وكيفية تثبيت وتكوين RabbitMQ.
دعونا نصف تكوينات جميع أجزاء RabbitMQ كخدمة في K8s. سنوضح على الفور أننا قمنا بتثبيت RabbitMQ في K8s كمجموعة StatefulSet. في كل عقدة من مجموعة K8s ، ستعمل نسخة واحدة من RabbitMQ دائمًا (عقدة واحدة في تكوين الكتلة الكلاسيكية). سنقوم أيضًا بتثبيت لوحة تحكم RabbitMQ في K8s وإتاحة الوصول إلى هذه اللوحة خارج المجموعة.
الحقوق والأدوار:
rabbitmq_rbac.yaml--- apiVersion: v1 kind: ServiceAccount metadata: name: rabbitmq --- kind: Role apiVersion: rbac.authorization.k8s.io/v1beta1 metadata: name: endpoint-reader rules: - apiGroups: [""] resources: ["endpoints"] verbs: ["get"] --- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1beta1 metadata: name: endpoint-reader subjects: - kind: ServiceAccount name: rabbitmq roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: endpoint-reader
حقوق الوصول لـ RabbitMQ مأخوذة بالكامل من المثال ، لا توجد تغييرات مطلوبة فيها. ننشئ حساب ServiceAccount لمجموعتنا ونمنحه أذونات قراءة لنقاط نهاية K8s.
التخزين المستمر:
rabbitmq_pv.yaml kind: PersistentVolume apiVersion: v1 metadata: name: rabbitmq-data-sigma labels: type: local annotations: volume.alpha.kubernetes.io/storage-class: rabbitmq-data-sigma spec: storageClassName: rabbitmq-data-sigma capacity: storage: 10Gi accessModes: - ReadWriteMany persistentVolumeReclaimPolicy: Recycle hostPath: path: "/opt/rabbitmq-data-sigma"
هنا أخذنا أبسط حالة التخزين الدائم - hostPath (مجلد عادي على كل عقدة K8s) ، ولكن يمكنك استخدام أي من أنواع عديدة من وحدات التخزين المستمرة التي تدعمها K8s.
rabbitmq_pvc.yaml kind: PersistentVolumeClaim apiVersion: v1 metadata: name: rabbitmq-data spec: storageClassName: rabbitmq-data-sigma accessModes: - ReadWriteMany resources: requests: storage: 10Gi
إنشاء مطالبة حجم على وحدة التخزين التي تم إنشاؤها في الخطوة السابقة. سيتم استخدام هذه المطالبة بعد ذلك في StatefulSet كمخزن بيانات مستمر.
الخدمات:
rabbitmq_service.yaml kind: Service apiVersion: v1 metadata: name: rabbitmq-internal labels: app: rabbitmq spec: clusterIP: None ports: - name: http protocol: TCP port: 15672 - name: amqp protocol: TCP port: 5672 selector: app: rabbitmq
ننشئ خدمة داخلية بدون رأس يعمل من خلالها البرنامج المساعد Peer Discovery.
rabbitmq_service_ext.yaml kind: Service apiVersion: v1 metadata: name: rabbitmq labels: app: rabbitmq type: LoadBalancer spec: type: NodePort ports: - name: http protocol: TCP port: 15672 targetPort: 15672 nodePort: 31673 - name: amqp protocol: TCP port: 5672 targetPort: 5672 nodePort: 30673 selector: app: rabbitmq
للتطبيقات في K8s للعمل مع مجموعتنا ، نقوم بإنشاء خدمة موازن.
نظرًا لأننا بحاجة إلى الوصول إلى مجموعة RabbitMQ خارج K8s ، فإننا نمرر عبر NodePort. سيكون RabbitMQ متاحًا عند الوصول إلى أي عقدة في مجموعة K8s على المنافذ 31673 و 30673. في العمل الحقيقي ، ليست هناك حاجة كبيرة لذلك. مسألة راحة استخدام لوحة إدارة RabbitMQ.
عند إنشاء خدمة من نوع NodePort في K8s ، يتم أيضًا إنشاء خدمة من نوع ClusterIP بشكل ضمني لتقديمها. لذلك ، ستتمكن التطبيقات في K8s التي تحتاج إلى العمل مع RabbitMQ من الوصول إلى المجموعة في
amqp: // rabbitmq: 5672التكوين:
rabbitmq_configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: rabbitmq-config data: enabled_plugins: | [rabbitmq_management,rabbitmq_peer_discovery_k8s]. rabbitmq.conf: | cluster_formation.peer_discovery_backend = rabbit_peer_discovery_k8s cluster_formation.k8s.host = kubernetes.default.svc.cluster.local cluster_formation.k8s.port = 443 ### cluster_formation.k8s.address_type = ip cluster_formation.k8s.address_type = hostname cluster_formation.node_cleanup.interval = 10 cluster_formation.node_cleanup.only_log_warning = true cluster_partition_handling = autoheal queue_master_locator=min-masters cluster_formation.randomized_startup_delay_range.min = 0 cluster_formation.randomized_startup_delay_range.max = 2 cluster_formation.k8s.service_name = rabbitmq-internal cluster_formation.k8s.hostname_suffix = .rabbitmq-internal.our-namespace.svc.cluster.local
نقوم بإنشاء ملفات تكوين RabbitMQ. السحر الرئيسي.
enabled_plugins: | [rabbitmq_management,rabbitmq_peer_discovery_k8s].
أضف المكونات الإضافية الضرورية إلى الإضافات المسموح بتنزيلها. الآن يمكننا استخدام اكتشاف الأقران التلقائي في K8S.
cluster_formation.peer_discovery_backend = rabbit_peer_discovery_k8s
نكشف البرنامج المساعد الضروري كخلفية لاكتشاف النظراء.
cluster_formation.k8s.host = kubernetes.default.svc.cluster.local cluster_formation.k8s.port = 443
حدد العنوان والمنفذ اللذين يمكنك من خلالهما الوصول إلى kubernetes apiserver. هنا يمكنك تحديد عنوان IP مباشرة ، ولكن سيكون أجمل من القيام بذلك.
في مساحة الاسم الافتراضية ، عادةً ما يتم إنشاء خدمة باسم kubernetes يؤدي إلى k8-apiserver. في خيارات تثبيت K8S المختلفة ، قد تختلف مساحة الاسم واسم الخدمة والمنفذ. إذا كان هناك شيء ما في تثبيت معين مختلفًا ، فأنت بحاجة إلى إصلاحه وفقًا لذلك.
على سبيل المثال ، نواجه حقيقة أنه في بعض المجموعات تكون الخدمة على المنفذ 443 ، وفي البعض الآخر على 6443. سيكون من الممكن أن نفهم أن هناك خطأ ما في سجلات بدء RabbitMQ ، فإن وقت الاتصال بالعنوان المحدد هنا موضح بوضوح هناك.
### cluster_formation.k8s.address_type = ip cluster_formation.k8s.address_type = hostname
بشكل افتراضي ، حدد المثال نوع عنوان عقدة نظام المجموعة RabbitMQ حسب عنوان IP. ولكن عند إعادة تشغيل الكود ، تحصل على عنوان IP جديد في كل مرة. مفاجأة! الكتلة تموت في عذاب.
قم بتغيير العنوان إلى اسم المضيف. تضمن لنا StatefulSet عدم ثبات اسم المضيف خلال دورة حياة StatefulSet بأكملها ، وهو ما يناسبنا تمامًا.
cluster_formation.node_cleanup.interval = 10 cluster_formation.node_cleanup.only_log_warning = true
عندما نفقد إحدى العقد ، نفترض أنها ستتعافى عاجلاً أم آجلاً ، نقوم بتعطيل الحذف الذاتي من خلال مجموعة من العقد التي يتعذر الوصول إليها. في هذه الحالة ، بمجرد عودة العقدة عبر الإنترنت ، ستدخل الكتلة دون أن تفقد حالتها السابقة.
cluster_partition_handling = autoheal
تحدد هذه المعلمة إجراءات الكتلة في حالة فقدان النصاب. هنا تحتاج فقط لقراءة
الوثائق حول هذا الموضوع وتفهم بنفسك ما هو أقرب إلى حالة استخدام محددة.
queue_master_locator=min-masters
تحديد اختيار المعالج لقوائم الانتظار الجديدة. باستخدام هذا الإعداد ، سيحدد المعالج العقدة بأقل عدد من قوائم الانتظار ، لذلك سيتم توزيع قوائم الانتظار بالتساوي عبر عقد الكتلة.
cluster_formation.k8s.service_name = rabbitmq-internal
نقوم بتسمية خدمة K8s بدون رأس (التي أنشأناها سابقًا) والتي من خلالها ستتواصل عقد RabbitMQ مع بعضها البعض.
cluster_formation.k8s.hostname_suffix = .rabbitmq-internal.our-namespace.svc.cluster.local
شيء مهم للعنونة في كتلة هو اسم المضيف. يتم تشكيل FQDN لموقد K8s كاسم قصير (rabbitmq-0 ، rabbitmq-1) + لاحقة (جزء المجال). هنا نشير إلى هذه اللاحقة. في K8S ، يبدو
. <اسم الخدمة>. <اسم مساحة الاسم> .svc.cluster.localيحل kube-dns أسماء النموذج rabbitmq-0.rabbitmq-internal.our-namespace.svc.cluster.local في عنوان IP لجراب معين دون أي تكوين إضافي ، مما يجعل كل سحر التجميع حسب اسم المضيف ممكنًا.
تكوين StatefulSet RabbitMQ:
rabbitmq_statefulset.yaml apiVersion: apps/v1beta1 kind: StatefulSet metadata: name: rabbitmq spec: serviceName: rabbitmq-internal replicas: 3 template: metadata: labels: app: rabbitmq annotations: scheduler.alpha.kubernetes.io/affinity: > { "podAntiAffinity": { "requiredDuringSchedulingIgnoredDuringExecution": [{ "labelSelector": { "matchExpressions": [{ "key": "app", "operator": "In", "values": ["rabbitmq"] }] }, "topologyKey": "kubernetes.io/hostname" }] } } spec: serviceAccountName: rabbitmq terminationGracePeriodSeconds: 10 containers: - name: rabbitmq-k8s image: rabbitmq:3.7 volumeMounts: - name: config-volume mountPath: /etc/rabbitmq - name: rabbitmq-data mountPath: /var/lib/rabbitmq/mnesia ports: - name: http protocol: TCP containerPort: 15672 - name: amqp protocol: TCP containerPort: 5672 livenessProbe: exec: command: ["rabbitmqctl", "status"] initialDelaySeconds: 60 periodSeconds: 10 timeoutSeconds: 10 readinessProbe: exec: command: ["rabbitmqctl", "status"] initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 10 imagePullPolicy: Always env: - name: MY_POD_IP valueFrom: fieldRef: fieldPath: status.podIP - name: HOSTNAME valueFrom: fieldRef: fieldPath: metadata.name - name: NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: RABBITMQ_USE_LONGNAME value: "true" - name: RABBITMQ_NODENAME value: "rabbit@$(HOSTNAME).rabbitmq-internal.$(NAMESPACE).svc.cluster.local" - name: K8S_SERVICE_NAME value: "rabbitmq-internal" - name: RABBITMQ_ERLANG_COOKIE value: "mycookie" volumes: - name: config-volume configMap: name: rabbitmq-config items: - key: rabbitmq.conf path: rabbitmq.conf - key: enabled_plugins path: enabled_plugins - name: rabbitmq-data persistentVolumeClaim: claimName: rabbitmq-data
في الواقع ، StatefulSet نفسها. نلاحظ نقاط مثيرة للاهتمام.
serviceName: rabbitmq-internal
نكتب اسم الخدمة بدون رأس التي تتواصل من خلالها القرون في StatefulSet.
replicas: 3
قم بتعيين عدد النسخ المتماثلة في الكتلة. في بلدنا ، يساوي عدد عقد العمل K8s.
annotations: scheduler.alpha.kubernetes.io/affinity: > { "podAntiAffinity": { "requiredDuringSchedulingIgnoredDuringExecution": [{ "labelSelector": { "matchExpressions": [{ "key": "app", "operator": "In", "values": ["rabbitmq"] }] }, "topologyKey": "kubernetes.io/hostname" }] } }
عندما تسقط إحدى عقد K8s ، تسعى مجموعة الحالة إلى الحفاظ على عدد الحالات في المجموعة ، وبالتالي ، فإنها تخلق عدة مداخن على نفس عقدة K8s. هذا السلوك غير مرغوب فيه تمامًا ، ومن حيث المبدأ ، لا معنى له. لذلك ، نحن ننشئ قاعدة مضادة للألفة لمجموعات الموقد من مجموعة الحالة. نحن نجعل القاعدة صعبة (مطلوبة) حتى لا يتمكن مجدول kube من كسرها عند تخطيط القرون.
الجوهر بسيط: ممنوع على المجدول وضع (داخل مساحة الاسم) أكثر من جراب مع
التطبيق: علامة rabbitmq على كل عقدة. نحن نميز
العقد بقيمة تسمية
kubernetes.io/hostname . الآن ، إذا كان عدد عقد K8S العاملة لسبب ما أقل من العدد المطلوب من النسخ المتماثلة في StatefulSet ، فلن يتم إنشاء نسخ متماثلة جديدة حتى تظهر عقدة مجانية مرة أخرى.
serviceAccountName: rabbitmq
نسجل ServiceAccount ، التي تعمل بموجبها القرون.
image: rabbitmq:3.7
صورة RabbitMQ قياسية تمامًا ويتم التقاطها من مركز عامل الميناء ، ولا تتطلب أي إعادة بناء ومراجعة للملف.
- name: rabbitmq-data mountPath: /var/lib/rabbitmq/mnesia
يتم تخزين البيانات المستمرة من RabbitMQ في / var / lib / rabbitmq / mnesia. نقوم هنا بتثبيت مطالبتنا المستمرة بشأن وحدة التخزين في هذا المجلد بحيث تكون آمنة وسليمة عند إعادة تشغيل المداخن / العقد أو حتى StatefulSet بأكملها ، وتكون البيانات (كلتا الخدمتين ، بما في ذلك حول المجموعة المجمعة وبيانات المستخدم) آمنة وسليمة. هناك بعض الأمثلة حيث يكون المجلد / var / lib / rabbitmq / بأكمله ثابتًا. توصلنا إلى استنتاج مفاده أن هذه ليست أفضل فكرة ، لأنه في نفس الوقت يبدأ تذكر جميع المعلومات التي حددتها تكوينات الأرنب. أي ، من أجل تغيير شيء ما في ملف التكوين ، تحتاج إلى تنظيف التخزين المستمر ، وهو أمر غير مريح للغاية في التشغيل.
- name: HOSTNAME valueFrom: fieldRef: fieldPath: metadata.name - name: NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: RABBITMQ_USE_LONGNAME value: "true" - name: RABBITMQ_NODENAME value: "rabbit@$(HOSTNAME).rabbitmq-internal.$(NAMESPACE).svc.cluster.local"
مع هذه المجموعة من متغيرات البيئة ، نطلب أولاً من RabbitMQ استخدام اسم FQDN كمعرف لأعضاء الكتلة ، وثانيًا ، نقوم بتعيين تنسيق هذا الاسم. تم وصف التنسيق سابقًا عند تحليل التكوين.
- name: K8S_SERVICE_NAME value: "rabbitmq-internal"
اسم الخدمة بدون رأس للتواصل بين أعضاء المجموعة.
- name: RABBITMQ_ERLANG_COOKIE value: "mycookie"
يجب أن تكون محتويات Erlang Cookie هي نفسها في جميع عقد المجموعة ، تحتاج إلى تسجيل قيمتك الخاصة. لا يمكن أن تدخل العقدة التي تحتوي على ملف تعريف ارتباط مختلف إلى المجموعة.
volumes: - name: rabbitmq-data persistentVolumeClaim: claimName: rabbitmq-data
تحديد وحدة التخزين المعينة من مطالبة وحدة التخزين المستمرة التي تم إنشاؤها سابقًا.
هذا هو المكان الذي انتهينا فيه من الإعداد في K8s. والنتيجة هي مجموعة RabbitMQ ، التي توزع قوائم الانتظار بالتساوي بين العقد ومقاومة للمشاكل في بيئة وقت التشغيل.

إذا كانت إحدى عقد نظام المجموعة غير متوفرة ، فسوف يتوقف الوصول إلى قوائم الانتظار الموجودة عليها ، وسيستمر كل شيء آخر في العمل. بمجرد عودة العقدة إلى التشغيل ، ستعود إلى الكتلة ، وستصبح قوائم الانتظار التي كانت تعمل فيها رئيسية عاملة مرة أخرى ، مع الحفاظ على جميع البيانات الموجودة فيها (إذا لم ينكسر التخزين الدائم ، بالطبع). كل هذه العمليات تلقائية بالكامل ولا تتطلب تدخلًا.
المكافأة: تخصيص HA
كان أحد المشاريع فارق بسيط. بدت المتطلبات انعكاساً كاملاً لجميع البيانات الواردة في المجموعة. يعد ذلك ضروريًا حتى في حالة تشغيل عقدة نظام مجموعة واحدة على الأقل ، يستمر كل شيء في العمل من وجهة نظر التطبيق. هذه اللحظة لا علاقة لها بـ K8s ، فنحن نصفها ببساطة بأنها طريقة صغيرة.
لتمكين HA الكامل ، تحتاج إلى إنشاء سياسة في لوحة معلومات RabbitMQ في علامة التبويب
المسؤول -> السياسات . الاسم عشوائي ، النمط فارغ (جميع قوائم الانتظار) ، في التعريفات تضيف معلمتين:
ha-mode: all ،
ha-sync-mode: automatic .


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

اقرأ المزيد عن HA في RabbitMQ
هناكتب مفيدة:
حظا سعيدا!