Courier: ترحيل Dropbox إلى gRPC



ملاحظة المترجم


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

  • استخدام لغة Protocol Buffers لوصف تفاعل الخدمات ؛
  • إنشاء رمز البرنامج استنادًا إلى البروتوكول الموضح لـ 11 لغة مختلفة لكل من جزء العميل وجزء الخادم ؛
  • تنفيذ إذن بين المكونات المتفاعلة ؛
  • استخدام كل من التفاعل متزامن وغير متزامن.

بدا لي gRPC إطارًا ممتعًا للغاية ، وكنت مهتمًا بالتعرف على تجربة Dropbox الحقيقية في بناء نظام قائم عليه. تحتوي المقالة على الكثير من التفاصيل المتعلقة باستخدام التشفير ، وإنشاء نظام موثوق ومُلاحظ ومنتج ، وعملية الترحيل من حل RPC القديم إلى الحل الجديد.

إخلاء المسؤولية
لا تحتوي المقالة الأصلية على وصف gRPC ، وقد لا تبدو بعض النقاط واضحة لك. إذا لم تكن معتادًا على gRPC أو الأُطُر المشابهة الأخرى (على سبيل المثال ، Apache Thrift) ، فإنني أوصيك أولاً بالتعرف على الأفكار الرئيسية (ستكون كافية لقراءة مقالتين صغيرتين من الموقع الرسمي: "ما هي gRPC؟" و "مفاهيم gRPC" ).

بفضل أليكسي إيفانوف الملقب SaveTheRbtz لكتابة المقال الأصلي والمساعدة في ترجمة الأماكن الصعبة.

تقوم Dropbox بإدارة العديد من الخدمات المكتوبة بلغات مختلفة وتخدم ملايين الطلبات في الثانية. في مركز الهندسة المعمارية الموجهة نحو الخدمات لدينا ، Courier ، إطار عمل قائم على تكلفة النقرة. في عملية تطويره ، تعلمنا الكثير عن القابلية للتوسعة gRPC وتحسين الأداء والانتقال من نظام RPC السابق.

ملاحظة: يحتوي المنشور على مقتطفات برمجية لـ Python و Go. نحن نستخدم أيضا الصدأ وجافا.

الطريق إلى gRPC


Courier ليس إطار Dropbox RPC الأول. حتى قبل أن نبدأ في تقسيم نظام Python المتجانس إلى خدمات منفصلة ، كنا بحاجة إلى أساس موثوق لتبادل البيانات بين الخدمات - خاصة وأن اختيار إطار عمل سيكون له عواقب طويلة الأجل.

قبل ذلك ، جربت Dropbox أطر عمل RPC مختلفة. أولاً ، كان لدينا بروتوكول فردي للتسلسل اليدوي وإلغاء التسلسل. تستخدم بعض الخدمات ، مثل التسجيل المستند إلى Scribe ، Apache Thrift . في الوقت نفسه ، كان إطار عمل RPC الرئيسي هو بروتوكول HTTP / 1.1 مع تسلسل الرسائل باستخدام Protobuf.

إنشاء إطار عمل ، اخترنا من بين عدة خيارات. يمكننا تقديم Swagger (المعروف الآن باسم OpenAPI ) في إطار عمل RPC القديم ، أو تقديم معيار جديد ، أو إنشاء إطار عمل يستند إلى Thrift أو gRPC. كانت الحجة الرئيسية لصالح gRPC هي إمكانية استخدام protobufs الموجودة مسبقًا. أيضًا ، كان تعدد إرسال HTTP / 2 ونقل البيانات في اتجاهين مفيدًا لمهامنا.

ملاحظة: إذا كانت fbthrift موجودة في تلك اللحظة ، فربما نلقي نظرة فاحصة على حلول Thrift.

ما يجلب الساعي إلى gRPC


Courier ليس بروتوكول RPC؛ إنها وسيلة لدمج gRPC في البنية التحتية الحالية. كان من المفترض أن يكون الإطار متوافقًا مع أدوات الاستيقان والترخيص واكتشاف الخدمة ، بالإضافة إلى جمع الإحصاءات وتسجيلها وتتبعها. لذلك أنشأنا Courier.

رغم أننا في بعض الحالات نستخدم Bandaid كوكيل gRPC ، فإن معظم خدماتنا تتواصل مباشرة مع بعضها البعض لتقليل تأثير RPC على الكمون.

كان من المهم بالنسبة لنا تقليل مقدار الشفرة الروتينية التي يجب كتابتها. نظرًا لأن Courier بمثابة إطار عام لتطوير الخدمات ، فإنه يحتوي على ميزات يحتاجها الجميع. يتم تمكين معظمها افتراضيًا ويمكن التحكم فيها بواسطة وسيطات سطر الأوامر ، ويتم التحقق من بعضها باستخدام مربع اختيار.

الأمان: هوية الخدمة ومصادقة TLS المتبادلة


تنفذ شركة Courier آلية تحديد الخدمة القياسية الخاصة بنا. يتم تعيين شهادة TLS فردية لكل خادم وعميل صادرة عن سلطة إصدار الشهادات الخاصة بنا. الشهادة المعرف الشخصي المشفر ، والتي يتم استخدامها للمصادقة المتبادلة - يتحقق الخادم من العميل ، يتحقق العميل من الخادم.

في TLS ، حيث نتحكم في كلا جانبي الاتصال ، أدخلنا قيودًا صارمة. تتطلب جميع RPCs الداخلية تشفير PFS . الإصدار المطلوب من TLS هو 1.2 وما فوق. حددنا أيضًا عدد الخوارزميات المتماثلة وغير المتماثلة ، مفضلين ECDHE-ECDSA-AES128-GCM-SHA256 .

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

خدمة الهوية هي معرّف عمومي لـ ACL ، وحدود السرعة ، والإحصاءات ، إلخ. بالإضافة إلى ذلك ، فهي آمنة بشكل تشفير.

فيما يلي مثال لتكوين ACL والحد الأقصى للسرعة المستخدم في خدمة التعرف على الأنماط البصرية :

limits:  dropbox_engine_ocr:    # All RPC methods.    default:      max_concurrency: 32      queue_timeout_ms: 1000      rate_acls:        # OCR clients are unlimited.        ocr: -1        # Nobody else gets to talk to us.        authenticated: 0        unauthenticated: 0 



نحن نفكر في إمكانية التبديل إلى تنسيق SVID (المستند الذي تم التحقق منه بشكل تشفير SPIFFE ) ، والذي سيساعد على دمج إطارنا مع العديد من المشاريع مفتوحة المصدر.

الملاحظة: الإحصاءات والتتبع


باستخدام معرف واحد فقط ، يمكنك بسهولة العثور على السجلات والإحصائيات وملفات التتبع والبيانات الأخرى حول Courier.



أثناء إنشاء التعليمات البرمجية ، تتم إضافة مجموعة الإحصائيات لكل خدمة ولكل طريقة من جانب العميل ومن جانب الخادم. يتم تقسيم إحصائيات جانب الخادم بواسطة معرف العميل. في التكوين القياسي ، سوف تتلقى بيانات مفصلة عن الحمل والأخطاء ووقت التأخير لكل خدمة باستخدام Courier.



تتضمن إحصائيات Courier بيانات حول التوفر والكمون من جانب العميل ، وكذلك عن عدد الطلبات وحجم قائمة الانتظار على جانب الخادم. هناك رسوم بيانية أخرى مفيدة ، ولا سيما الرسوم البيانية لوقت الاستجابة لكل طريقة ووقت مصافحة TLS لكل عميل.

تتمثل إحدى مزايا إنشاء الكود لدينا في إمكانية التهيئة الثابتة لهياكل البيانات ، مثل الرسوم البيانية والرسوم البيانية التتبعية. هذا يقلل من تأثير الأداء.



قام نظام RPC القديم بتوزيع request_id فقط على واجهة برمجة التطبيقات. هذا جعل من الممكن الجمع بين البيانات من سجلات الخدمات المختلفة. في Courier ، قدمنا ​​API بناءً على مجموعة فرعية من مواصفات OpenTracing . لقد كتبنا مكتباتنا الخاصة من جانب العميل ، وعلى جانب الخادم ، قمنا بتطبيق حل قائم على Cassandra و Jaeger .



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

الموثوقية: المواعيد النهائية وانقطاع الاتصال


يوفر Courier مكانًا مركزيًا لتنفيذ وظائف العميل الشائعة (على سبيل المثال ، المهلات) بلغات مختلفة. لقد قمنا تدريجياً بإضافة العديد من الميزات ، استنادًا إلى نتائج تحليل "بعد وفاته" للمشاكل الناشئة.

المواعيد النهائية


كل طلب gRPC له موعد نهائي للإشارة إلى مهلة العميل. نظرًا لأن روتين Courier يقوم تلقائيًا بتوزيع البيانات التعريفية المعروفة ، يتم نقل الموعد النهائي للطلب خارج API. ضمن هذه العملية ، تتلقى المواعيد النهائية عرضًا أصليًا. على سبيل المثال ، في Go ، يتم تمثيلهم بنتيجة السياق .Context من أسلوب WithDeadline .

في الواقع ، تمكنا من إصلاح فئات كاملة من مشاكل الموثوقية من خلال إجبار المهندسين على تحديد مواعيد نهائية في تحديد الخدمات المناسبة.

هذا النهج يتجاوز حتى RPC. على سبيل المثال ، يقوم ORM MySQL لدينا بتسلسل سياق RPC مع الموعد النهائي في تعليق استعلام SQL. وكيل SQL لدينا يمكن تحليل التعليقات و "قتل" الاستعلامات عند حدوث الموعد النهائي. وكعلاوة عند تصحيح أخطاء مكالمات قاعدة البيانات ، لدينا استعلام SQL ملزم لاستعلام RPC معين.

قطع الاتصال


هناك مشكلة شائعة أخرى واجهها عملاء نظام RPC السابق وهي تنفيذ خوارزمية التأخير الفردي والتقلبات عند الطلب المتكرر.

لقد حاولنا إيجاد حل ذكي لمشكلة الانفصال في Courier ، بدءًا من تنفيذ المخزن المؤقت LIFO (الأخير في الخروج أولاً) بين الخدمة ومجموعة المهام.



في حالة وجود حمل زائد ، سيتم قطع LIFO تلقائيًا. قائمة الانتظار ، وهو أمر مهم ، يقتصر ليس فقط من حيث الحجم ، ولكن أيضا حسب الوقت (يمكن أن يقضي الطلب في قائمة الانتظار فقط وقت معين).

ناقص LIFO - تغيير ترتيب معالجة الطلبات. إذا كنت تريد الاحتفاظ بالترتيب الأصلي ، استخدم CoDel . هناك ، أيضًا ، إمكانية قطع الاتصال ، وسيظل ترتيب معالجة الطلبات كما هو.



الاستبطان: تصحيح نقاط النهاية


على الرغم من أن نقاط النهاية لتصحيح الأخطاء ليست جزءًا من Courier مباشرة ، إلا أنها تستخدم على نطاق واسع في جميع أنحاء Dropbox وهي مفيدة جدًا بحيث لا يمكن ذكرها.

لأسباب أمنية ، يمكنك فتحها على منفذ منفصل أو على مقبس Unix (للتحكم في الوصول باستخدام أذونات الملفات). يجب أيضًا مراعاة مصادقة TLS المتبادلة ، والتي سيتعين على المطورين تقديم شهاداتهم للوصول إلى نقاط النهاية (لا يقتصر الأمر على القراءة فقط).

التنفيذ


تعد القدرة على تحليل حالة الخدمة أثناء تشغيلها مفيدة للغاية لتصحيح الأخطاء. على سبيل المثال ، يمكن الوصول إلى ملفات تعريف الذاكرة الديناميكية ووحدة المعالجة المركزية من خلال نقاط النهاية HTTP أو gRPC .

نحن نخطط لاستخدام هذه الفرصة في إجراء التحقق من الكناري - لأتمتة البحث عن الفرق بين الإصدارات القديمة والجديدة من الشفرة.

تتيح نقاط النهاية تعديل حالة الخدمة في وقت التشغيل. على وجه الخصوص ، يمكن للخدمات المستندة إلى Golang تكوين GCPercent بشكل حيوي.

المكتبة


قد يكون التصدير التلقائي للبيانات الخاصة بالمكتبة كنقطة نهاية RPC مفيدًا لمطوري المكتبة. على سبيل المثال ، يمكن لمكتبة malloc تفريغ الإحصاءات الداخلية في ملف تفريغ . مثال آخر: يمكن لنقطة النهاية لتصحيح الأخطاء تغيير مستوى تسجيل الخدمة على الطاير.

RPC


بالطبع ، استكشاف الأخطاء وإصلاحها في البروتوكولات المشفرة والمشفرة ليس بالأمر السهل. لذلك ، يعد تقديم أكبر عدد ممكن من الأدوات على مستوى RPC فكرة جيدة. أحد الأمثلة على API الاستبطاني هو حل Channelz .

مستوى التطبيق


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

تحسين الأداء


من خلال توسيع إطار gRPC الخاص بنا إلى النطاق المطلوب ، وجدنا العديد من الاختناقات الخاصة بـ Dropbox.

استهلاك الموارد لمصادقة TLS


في الخدمات التي تخدم العديد من العلاقات ، نتيجة لمصادقة TLS ، يمكن أن يكون حمل وحدة المعالجة المركزية المدمجة خطيرًا للغاية (خاصة عند إعادة تشغيل خدمة شائعة).

لتحسين الأداء عند التوقيع ، استبدلنا أزواج المفاتيح RSA-2048 بـ ECDSA P-256. فيما يلي أمثلة على أدائها (ملاحظة: مع RSA ، يكون التحقق من التوقيع أسرع).

RSA:

 ~/c0d3/boringssl bazel run -- //:bssl speed -filter 'RSA 2048' Did ... RSA 2048 signing operations in ..............  (1527.9 ops/sec) Did ... RSA 2048 verify (same key) operations in .... (37066.4 ops/sec) Did ... RSA 2048 verify (fresh key) operations in ... (25887.6 ops/sec) 

ECDSA:

 ~/c0d3/boringssl bazel run -- //:bssl speed -filter 'ECDSA P-256' Did ... ECDSA P-256 signing operations in ... (40410.9 ops/sec) Did ... ECDSA P-256 verify operations in .... (17037.5 ops/sec) 

نظرًا لأن التحقق باستخدام RSA-2048 أسرع بثلاث مرات تقريبًا من ECDSA P-256 ، يمكنك اختيار RSA لشهادات الجذر والنهاية لزيادة سرعة التشغيل. ولكن من وجهة نظر الأمان ، ليس كل شيء بهذه البساطة: ستقوم ببناء سلاسل من بدائل التشفير المختلفة ، وبالتالي ، سيكون مستوى معلمات الأمان الناتجة هو الأدنى. وإذا كنت ترغب في تحسين الأداء ، فإننا لا نوصي باستخدام شهادات الإصدار RSA-4096 (والإصدارات الأحدث) كشهادات الجذر والنهاية.

لقد وجدنا أيضًا أن اختيار مكتبة TLS (وأعلام الترجمة) له تأثير كبير على كل من الأداء والأمان. قارن ، على سبيل المثال ، بناء LibreSSL على نظام MacOS X Mojave مع OpenSSL المكتوب ذاتيًا على نفس الجهاز.

LibreSSL 2.6.4:

 ~ openssl speed rsa2048 LibreSSL 2.6.4 ...                 sign verify sign/s verify/s rsa 2048 bits 0.032491s 0.001505s     30.8 664.3 

OpenSSL 1.1.1a:

  ~ openssl speed rsa2048 OpenSSL 1.1.1a  20 Nov 2018 ...                 sign verify sign/s verify/s rsa 2048 bits 0.000992s 0.000029s   1208.0 34454.8 

ومع ذلك ، فإن أسرع طريقة لإنشاء مصافحة TLS هي عدم إنشائها على الإطلاق! لقد قمنا بتضمين دعم لاستئناف الجلسة في gRPC-core و gRPC-python ، وبالتالي تقليل الحمل على وحدة المعالجة المركزية أثناء النشر.

التشفير غير مكلفة


يعتقد الكثيرون خطأ أن التشفير مكلف. في الواقع ، حتى أبسط أجهزة الكمبيوتر الحديثة تؤدي تشفيرًا متماثلًا على الفور تقريبًا. يستطيع المعالج القياسي تشفير البيانات والمصادقة عليها بسرعة 40 جيجابت / ثانية لكل قلب:

 ~/c0d3/boringssl bazel run -- //:bssl speed -filter 'AES' Did ... AES-128-GCM (8192 bytes) seal operations in ... 4534.4 MB/s 

ومع ذلك ، لا يزال يتعين علينا تهيئة gRPC لكتل ​​الذاكرة الخاصة بنا ، والتي تعمل بسرعة 50 جيجابت / ثانية. وجدنا أنه إذا كانت سرعة التشفير مساوية تقريبًا لسرعة النسخ ، فمن المهم تقليل عدد عمليات memcpy. بالإضافة إلى ذلك ، لقد أجرينا بعض التغييرات على gRPC نفسها.

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

قنوات بيانات عالية الكمون (BDP)


ملاحظة المترجم: يستخدم العنوان الفرعي الأصلي مصطلح "تأخير عرض النطاق الترددي" ، والذي لا يحتوي على ترجمة ثابتة إلى اللغة الروسية.

تتضمن شبكة العمود الفقري Dropbox العديد من مراكز البيانات . في بعض الأحيان ، يجب على العقد الموجودة في مناطق مختلفة الاتصال عبر RPC ، على سبيل المثال ، للنسخ المتماثل. عند استخدام TCP ، تكون نواة النظام مسؤولة عن الحد من كمية البيانات المرسلة في اتصال معين (داخل / proc / sys / net / ipv4 / tcp_ {r، w} mem ) ، على الرغم من أن gRPC المستندة إلى HTTP / 2 لها أداة خاصة بها التحكم في التدفق. يقتصر الحد الأقصى لـ BDP في grpc-go بدقة على 16 ميغابايت ، مما قد يؤدي إلى اختناق.

net.erver Golang أو grpc.Server


في البداية ، في Go Go ، دعمنا HTTP / 1.1 و gRPC مع net.Server واحد. كان الحل منطقيًا فيما يتعلق بالحفاظ على رمز البرنامج ، لكنه لم ينجح تمامًا على الإطلاق. يؤدي توزيع HTTP / 1.1 و gRPC عبر الخوادم وترحيل gRPC إلى grpc.Server إلى تحسين عرض النطاق الترددي Courier واستخدام الذاكرة بشكل ملحوظ.

golang / protobuf أو gogo / protobuf


يمكن أن يؤدي التبديل إلى gRPC إلى زيادة تكلفة التنظيم والتنظيم. بالنسبة إلى Go code ، تمكنا من تقليل حمل وحدة المعالجة المركزية (CPU) على خوادم Courier بشكل كبير عن طريق التبديل إلى gogo / protobuf .

كما هو الحال دائمًا ، كان الانتقال إلى gogo / protobuf مصحوبًا ببعض المخاوف ، ولكن إذا قمت بتحديد الوظائف بشكل معقول ، فلا ينبغي أن تكون هناك مشاكل.

تفاصيل التنفيذ


في هذا القسم ، سنخترق أعمق في جهاز Courier ، وسننظر في مخططات protobuf وأمثلة من الرذائل من لغات مختلفة. جميع الأمثلة مأخوذة من خدمة الاختبار ، والتي استخدمناها أثناء اختبار تكامل Courier.

وصف الخدمة


ألقِ نظرة على مقتطف من تعريف خدمة الاختبار:

 service Test {   option (rpc_core.service_default_deadline_ms) = 1000;   rpc UnaryUnary(TestRequest) returns (TestResponse) {       option (rpc_core.method_default_deadline_ms) = 5000;   }   rpc UnaryStream(TestRequest) returns (stream TestResponse) {       option (rpc_core.method_no_deadline) = true;   }   ... } 

كما ذكر أعلاه ، مطلوب موعد نهائي لجميع أساليب Courier. باستخدام الخيار التالي ، يمكنك تعيين الموعد النهائي للخدمة بأكملها:

 option (rpc_core.service_default_deadline_ms) = 1000; 

في الوقت نفسه ، يمكن ضبط كل طريقة على الموعد النهائي الخاص بها ، وإلغاء الموعد النهائي للخدمة بأكملها (إن وجد):

 option (rpc_core.method_default_deadline_ms) = 5000; 

في حالات نادرة عندما لا يكون الموعد النهائي منطقيًا (على سبيل المثال ، عند تتبع أحد الموارد) ، يمكن للمطور تعطيله:

 option (rpc_core.method_no_deadline) = true; 

بالإضافة إلى ذلك ، يجب أن يحتوي وصف الخدمة على وثائق API مفصلة ، وربما مع أمثلة الاستخدام.

كعب الجيل


لتوفير قدر أكبر من المرونة ، يولد Courier كعب الروتين الخاص به دون الاعتماد على وظيفة الاعتراض التي توفرها gRPC (باستثناء Java ، حيث تتمتع API المعترضة بقدرة كافية). دعونا نقارن بذراتنا مع بذرات Golang القياسية.

هذا هو ما يبدو عليه كعب روتين خادم gRPC الافتراضي:

 func _Test_UnaryUnary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {       in := new(TestRequest)       if err := dec(in); err != nil {               return nil, err       }       if interceptor == nil {               return srv.(TestServer).UnaryUnary(ctx, in)       }       info := &grpc.UnaryServerInfo{               Server: srv,               FullMethod: "/test.Test/UnaryUnary",       }       handler := func(ctx context.Context, req interface{}) (interface{}, error) {               return srv.(TestServer).UnaryUnary(ctx, req.(*TestRequest))       }       return interceptor(ctx, in, info, handler) } 

تتم جميع عمليات المعالجة في الداخل: فك تشفير protobuf ، وإطلاق interceptors (انظر متغير interceptor في الكود) ، وإطلاق معالج UnaryUnary.

الآن نلقي نظرة على بذرة ساعي:

 func _Test_UnaryUnary_dbxHandler(       srv interface{},       ctx context.Context,       dec func(interface{}) error,       interceptor grpc.UnaryServerInterceptor) (       interface{},       error) {       defer processor.PanicHandler()       impl := srv.(*dbxTestServerImpl)       metadata := impl.testUnaryUnaryMetadata       ctx = metadata.SetupContext(ctx)       clientId = client_info.ClientId(ctx)       stats := metadata.StatsMap.GetOrCreatePerClientStats(clientId)       stats.TotalCount.Inc()       req := &processor.UnaryUnaryRequest{               Srv: srv,               Ctx: ctx,               Dec: dec,               Interceptor: interceptor,               RpcStats: stats,               Metadata: metadata,               FullMethodPath: "/test.Test/UnaryUnary",               Req: &test.TestRequest{},               Handler: impl._UnaryUnary_internalHandler,               ClientId: clientId,               EnqueueTime: time.Now(),       }       metadata.WorkPool.Process(req).Wait()       return req.Resp, req.Err } 

يوجد الكثير من التعليمات البرمجية هنا ، لذلك دعونا نحللها.

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

 defer processor.PanicHandler() 

سبب آخر لتشغيل معالج الذعر الخاص بنا هو التأكد من تعطل التطبيق في حالة حدوث خطأ. سيتجاهل معالج HTTP golang / net القياسي في هذه الحالة المشكلة ويستمر في تقديم الطلبات الجديدة (حتى التالفة وغير المتسقة).

بعد ذلك نقوم بتمرير السياق ، ونعيد تحديد القيم بناءً على البيانات الأولية للطلب الوارد:

 ctx = metadata.SetupContext(ctx) clientId = client_info.ClientId(ctx) 

نقوم أيضًا بإنشاء (وذاكرة التخزين المؤقت لمزيد من الكفاءة) إحصائيات العميل من جانب الخادم لتجميع أكثر تفصيلاً:

 stats := metadata.StatsMap.GetOrCreatePerClientStats(clientId) 

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

بعد ذلك ، نقوم بإنشاء هيكل طلب ، ونقله إلى تجمع المهام وانتظار التنفيذ:

 req := &processor.UnaryUnaryRequest{       Srv:        srv,       Ctx:        ctx,       Dec:        dec,       Interceptor:    interceptor,       RpcStats:       stats,       Metadata:       metadata,       ... } metadata.WorkPool.Process(req).Wait() 

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

لاحظ أن مكتبة gRPC تدعم واجهة TAP ، التي تسمح لك باعتراض الطلبات بسرعة هائلة. توفر الواجهة البنية التحتية لبناء محددات السرعة الفعالة بأقل استهلاك للموارد.

رموز خطأ محددة للتطبيقات المختلفة


يتيح مولد أداة كعب الروتين للمطورين أيضًا تعيين رموز خطأ خاصة بالتطبيق باستخدام خيارات خاصة:

 enum ErrorCode { option (rpc_core.rpc_error) = true; UNKNOWN = 0; NOT_FOUND = 1 [(rpc_core.grpc_code)="NOT_FOUND"]; ALREADY_EXISTS = 2 [(rpc_core.grpc_code)="ALREADY_EXISTS"]; ... STALE_READ = 7 [(rpc_core.grpc_code)="UNAVAILABLE"]; SHUTTING_DOWN = 8 [(rpc_core.grpc_code)="CANCELLED"]; } 

يتم نشر كل من أخطاء gRPC والتطبيق داخل الخدمة ، وعند حدود واجهة برمجة التطبيقات ، يتم استبدال جميع الأخطاء بـ UNKNOWN. بفضل هذا ، يمكننا تجنب نقل المشكلة إلى خدمات أخرى ، مما قد يؤدي إلى تغيير في دلالاتها.

بيثون التغييرات


تضيف عناصر كعب بايثون معلمة سياق صريحة لجميع معالجات Courier:

 from dropbox.context import Context from dropbox.proto.test.service_pb2 import (       TestRequest,       TestResponse, ) from typing_extensions import Protocol class TestCourierClient(Protocol):   def UnaryUnary(           self,           ctx, # type: Context           request, # type: TestRequest           ):       # type: (...) -> TestResponse       ... 

في البداية بدا الأمر غريبًا ، ولكن مع مرور الوقت ، اعتاد المطورون على التعبير عن ctx تمامًا مثلما اعتادوا على الذات .

يرجى ملاحظة أن كعب الروتين يتم كتابته بالكامل من أجل mypy ، والذي يتم تعويضه أثناء إعادة التوطين الرئيسية. بالإضافة إلى ذلك ، يتم تبسيط التكامل مع بعض IDEs (مثل PyCharm).

متابعة لمتابعة اتجاه الكتابة الثابتة ، نضيف التعليقات التوضيحية غير الصحيحة إلى البروتوكولات نفسها:

 class TestMessage(Message):   field: int   def __init__(self,       field : Optional[int] = ...,       ) -> None: ...   @staticmethod   def FromString(s: bytes) -> TestMessage: ... 

ستتجنب هذه التعليقات التوضيحية العديد من الأخطاء الشائعة ، مثل تعيين قيمة بلا إلى حقل نوع السلسلة ، على سبيل المثال .

هذا الرمز متاح هنا .

عملية الهجرة


إن إنشاء مكدس RPC جديد ليس بالمهمة السهلة ، لكنه لا يقف بجوار عملية الانتقال الكامل إليها ، إذا نظرت من وجهة نظر تعقيد التشغيل. لذلك ، حاولنا أن نجعل من السهل على المطورين التبديل من RPC القديم إلى Courier. نظرًا لأن الهجرة غالباً ما تكون مصحوبة بأخطاء ، فقد قررنا تنفيذها على مراحل.

الخطوة 0: تجميد RPC القديم


بادئ ذي بدء ، جمدنا RPC القديم حتى لا نطلق النار على هدف متحرك. كما دفع الناس للتبديل إلى Courier ، لأن جميع الميزات الجديدة مثل التتبع كانت متاحة فقط في الخدمات على Courier.

الخطوة 1: واجهة مشتركة ل RPC القديمة والساعي


بدأنا بتحديد واجهة مشتركة لـ RPC و Courier القديمة. كان من المفترض أن يكون توليد الشفرة الخاص بنا للتأكد من أن كلا إصداري الروتين يتوافق مع هذه الواجهة:

 type TestServer interface {  UnaryUnary(     ctx context.Context,     req *test.TestRequest) (     *test.TestResponse,     error)  ... } 

الخطوة 2: الترحيل إلى الواجهة الجديدة


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

يمكن ترحيل الخدمات البسيطة مع عدد قليل من الطرق والحق في ارتكاب الأخطاء في وقت واحد ، دون الانتباه إلى تحذيراتنا.

الخطوة 3: ترحيل العملاء إلى RPC Courier


أثناء عملية الترحيل ، بدأنا في وقت واحد في إطلاق الخوادم القديمة والجديدة على منافذ مختلفة من نفس الجهاز. تم تبديل تطبيق RPC من جانب العميل عن طريق تغيير سطر واحد:

 class MyClient(object): def __init__(self): -   self.client = LegacyRPCClient('myservice') +   self.client = CourierRPCClient('myservice') 

يرجى ملاحظة أنه باستخدام هذا النموذج ، يمكنك نقل عميل واحد في كل مرة ، بدءًا من العملاء ذوي المستوى الأدنى من اتفاقية مستوى الخدمة.

الخطوة 4: التنظيف


, , RPC ( ). — .

الاستنتاجات


, Courier — RPC-, , Dropbox.

, Courier:

  1. — . .
  2. — , .
  3. , . Codegen.
  4. . , , . , : .
  5. RPC- — , . . .


Courier, gRPC , , , .

gRPC Python , C++ Python Rust . ALTS TLS- (, ).

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


All Articles