حسورة. عالية الأداء GraphQL لبنية خادم SQL

مرحبا يا هبر! أقدم لكم ترجمة مقالة "معمارية GraphQL عالية الأداء لمحرك SQL" .

هذه ترجمة لمقال حول كيفية هيكله داخليًا وما هي التحسينات والحلول المعمارية التي يحملها Hasura - خادم GraphQL عالي الأداء وخفيف الوزن ، والذي يعمل كطبقة بين تطبيق الويب الخاص بك وقاعدة بيانات PostgreSQL.

يسمح لك بإنشاء مخطط GraphQL بناءً على قاعدة بيانات موجودة أو إنشاء مخطط جديد. وهو يدعم اشتراكات GraphQL من الصندوق استنادًا إلى مشغلات Postgres ، والتحكم الديناميكي في الوصول ، والتوليد التلقائي للصلات ، ويحل مشكلة طلبات N + 1 (التجميع) وأكثر من ذلك بكثير.


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

{ album (where: {year: {_eq: 2018}}) { title tracks { id title } } } 

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

طلب دورة الحياة


يمرّ طلب مُرسَل إلى Hasura عبر المراحل التالية:

  1. جلسات الاستلام : يقع الطلب في البوابة التي تتحقق من المفتاح (إن وجد) ويضيف رؤوسًا مختلفة ، على سبيل المثال ، المعرف ودور المستخدم.
  2. طلبات التحليل : يتلقى Hasura الطلب ، ويحلل الرؤوس للحصول على معلومات حول المستخدم ، وينشئ GraphQL AST بناءً على نص الطلب.
  3. التحقق من الطلبات : يتم إجراء فحص لمعرفة ما إذا كان الطلب صحيحًا من الناحية الدلالية ، ثم يتم تطبيق حقوق الوصول المقابلة لدور المستخدم.
  4. تنفيذ الاستعلام : يتم تحويل الاستعلام إلى SQL وإرساله إلى Postgres.
  5. توليد الاستجابة : تتم معالجة نتيجة استعلام SQL وإرسالها إلى العميل ( يمكن للبوابة استخدام gzip إذا لزم الأمر ).

الأهداف


المتطلبات تقريبًا كما يلي:

  1. يجب أن تضيف حزمة HTTP الحد الأدنى من النفقات العامة وأن تكون قادرة على معالجة العديد من الطلبات المتزامنة لزيادة الإنتاجية.
  2. إنشاء SQL سريع من استعلام GraphQL.
  3. يجب أن يكون استعلام SQL الذي تم إنشاؤه فعالاً لـ Postgres.
  4. يجب أن يتم تمرير نتيجة استعلام SQL بشكل فعال من Postgres.

معالجة استعلام GraphQL


هناك عدة طرق للحصول على البيانات المطلوبة لاستعلام GraphQL:

محللات تقليدية


عادةً ما ينطوي تنفيذ استعلامات GraphQL على استدعاء محلل لكل حقل.
في طلب المثال ، نحصل على الألبومات التي تم إصدارها في عام 2018 ، وبعد ذلك نطلب كل منها المسارات المقابلة لها - وهي مشكلة كلاسيكية لطلبات N + 1. يزداد عدد الاستعلامات بشكل كبير مع زيادة عمق الاستعلام.

الطلبات المقدمة من Postgres ستكون:

 SELECT id,title FROM album WHERE year = 2018; 

سيعيد هذا الطلب جميع الألبومات إلينا. افترض أن عدد الألبومات التي أرجعها الطلب يساوي N. ثم نقوم بتنفيذ الطلب التالي لكل ألبوم:

 SELECT id,title FROM tracks WHERE album_id = <album-id> 

في المجموع ، يمكنك الحصول على استعلامات N + 1 للحصول على جميع البيانات اللازمة.

طلبات التجميع


تم تصميم أدوات مثل dataloader لحل مشكلة طلبات N + 1 باستخدام التجميع. لم يعد عدد استعلامات SQL للبيانات المضمنة يعتمد على حجم العينة الأولية ، لأن الآن يؤثر على عدد العقد في استعلام GraphQL. في هذه الحالة ، يلزم تقديم طلبين إلى Postgres للحصول على البيانات المطلوبة:

نحصل على ألبومات:

 SELECT id,title FROM album WHERE year = 2018 

نحصل على المسارات للألبومات التي تلقيناها في الطلب السابق:

 SELECT id, title FROM tracks WHERE album_id IN {the list of album ids} 

في المجموع ، تم تلقي 2 استفسار. لقد تجنبنا تنفيذ استعلامات SQL على المسارات لكل ألبوم فردي ؛ وبدلاً من ذلك ، استخدمنا عامل WHERE للحصول على جميع المسارات الضرورية في استعلام واحد في وقت واحد.

ينضم


تم تصميم Dataloader للعمل مع مصادر بيانات مختلفة ولا يسمح باستغلال قدرات مصدر معين. في حالتنا ، Postgres هو مصدر البيانات الوحيد ، ومثل جميع قواعد البيانات العلائقية ، فإنه يوفر القدرة على جمع البيانات من جداول متعددة باستخدام استعلام واحد باستخدام عامل تشغيل JOIN. يمكننا تحديد جميع الجداول المطلوبة لاستعلام GraphQL وإنشاء استعلام SQL واحد باستخدام JOINs للحصول على جميع البيانات. اتضح أنه يمكن الحصول على البيانات اللازمة لأي استعلام GraphQL باستخدام استعلام SQL واحد. يتم تحويل هذه البيانات قبل إرسالها إلى العميل.

مثل هذا الطلب:

 SELECT album.id as album_id, album.title as album_title, track.id as track_id, track.title as track_title FROM album LEFT OUTER JOIN track ON (album.id = track.album_id) WHERE album.year = 2018 

سوف تعيد لنا مثل هذه البيانات:

 album_id, album_title, track_id, track_title 1, Album1, 1, track1 1, Album1, 2, track2 2, Album2, NULL, NULL 

ثم سيتم تحويلها إلى JSON وإرسالها إلى العميل:

 [ { "title" : "Album1", "tracks": [ {"id" : 1, "title": "track1"}, {"id" : 2, "title": "track2"} ] }, { "title" : "Album2", "tracks" : [] } ] 

تحسين توليد الاستجابة


وجدنا أن معظم الوقت في معالجة الاستعلام يتم إنفاقه على وظيفة تحويل نتيجة استعلام SQL إلى JSON.

بعد عدة محاولات لتحسين هذه الوظيفة بطرق مختلفة ، قررنا نقلها إلى Postgres. أضاف Postgres 9.4 (الذي تم إصداره في وقت الإصدار الأول لـ Hasura تقريبًا ) ميزة لتجميع JSON التي ساعدتنا في القيام بعملنا. بعد هذا التحسين ، بدأت استعلامات SQL تبدو كما يلي:

 SELECT json_agg(r.*) FROM ( SELECT album.title as title, json_agg(track.*) as tracks FROM album LEFT OUTER JOIN track ON (album.id = track.album_id) WHERE album.year = 2018 GROUP BY album.id ) r 

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

البيانات المعدة


يمكن أن تكون استعلامات SQL التي تم إنشاؤها كبيرة ومعقدة تمامًا اعتمادًا على مستوى تداخل الاستعلام وشروط الاستخدام. عادة ، تحتوي تطبيقات الويب على مجموعة من الاستعلامات التي يتم تنفيذها بشكل متكرر بمعلمات مختلفة. على سبيل المثال ، يجب تنفيذ الاستعلام السابق لعام 2017 ، بدلاً من 2018. تعتبر العبارات المعدة هي الأنسب للحالات التي يوجد فيها استعلام SQL معقد متكرر يتم فيه تغيير المعلمات فقط.

لنفترض أن هذا الاستعلام تم تنفيذه لأول مرة:

 { album (where: {year: {_eq: 2018}}) { title tracks { id title } } } 

نقوم بإنشاء عبارة معدة لاستعلام SQL بدلاً من تنفيذه:

 PREPARE prep_1 AS SELECT json_agg(r.*) FROM ( SELECT album.title as title, json_agg(track.*) as tracks FROM album LEFT OUTER JOIN track ON (album.id = track.album_id) WHERE album.year = $1 GROUP BY album. 

بعد ذلك نقوم بتنفيذها على الفور:

 EXECUTE prep_1('2018'); 

عندما تحتاج إلى تنفيذ استعلام GraphQL لعام 2017 ، فإننا ببساطة نسمي نفس العبارة المعدة بحجة مختلفة:

 EXECUTE prep_1('2017'); 

هذا يعطي زيادة حوالي 10-20٪ في السرعة حسب درجة تعقيد استعلام GraphQL.

هاسكل


يعمل هاسكل بشكل جيد لعدة أسباب:


في النهاية


تؤدي جميع التحسينات المذكورة أعلاه إلى مزايا أداء خطيرة للغاية:



في الواقع ، يسمح انخفاض استهلاك الذاكرة والتأخيرات الضئيلة مقارنة بالمكالمات المباشرة إلى PostgreSQL في معظم الحالات باستبدال ORM في الواجهة الخلفية بمكالمات إلى GraphQL API.

المعايير:

منصة الاختبار:

  1. كمبيوتر محمول مع 8 جيجابايت رام و i7
  2. Postgres يعمل على نفس الكمبيوتر
  3. wrk ، تم استخدامه كأداة مقارنة ولأنواع مختلفة من الطلبات حاولنا "زيادة" rps
  4. مثيل واحد من Hasura GraphQL Engine
  5. حجم تجمع الاتصال: 50
  6. مجموعة البيانات : شينوك


طلب 1: track_media_some

 query tracks_media_some { tracks (where: {composer: {_eq: "Kurt Cobain"}}){ id name album { id title } media_type { name } }} 

  • الطلبات بالثانية: 1375 ريال / ثانية
  • تأخير: 17.5 مللي ثانية
  • وحدة المعالجة المركزية: ~ 30٪
  • ذاكرة الوصول العشوائي: ~ 30 ميجابايت (Hasura) + 90 ميجابايت (Postgres)

طلب 2: track_media_all

 query tracks_media_all { tracks { id name media_type { name } }} 

  • الطلبات في الثانية: 410 طلب / ثانية
  • تأخير: 59 مللي ثانية
  • وحدة المعالجة المركزية: ~ 100٪
  • ذاكرة الوصول العشوائي: ~ 30 ميجابايت (Hasura) + 130 ميجابايت (Postgres)

طلب 3: album_tracks_genre_some

 query albums_tracks_genre_some { albums (where: {artist_id: {_eq: 127}}) { id title tracks { id name genre { name } } }} 

  • الطلبات في الثانية: 1029 طلب / ثانية
  • تأخير: 24 مللي ثانية
  • وحدة المعالجة المركزية: ~ 30٪
  • ذاكرة الوصول العشوائي: ~ 30 ميجابايت (Hasura) + 90 ميجابايت (Postgres)

طلب 4: album_tracks_genre_all

 query albums_tracks_genre_all { albums { id title tracks { id name genre { name } } } 

  • الطلبات في الثانية: 328 طلب / ثانية
  • تأخير: 73 مللي ثانية
  • وحدة المعالجة المركزية: 100٪
  • ذاكرة الوصول العشوائي: ~ 30 ميجابايت (Hasura) + 130 ميجابايت (Postgres)

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


All Articles