واجهات GraphQL

تحسين الأداء في GraphQL

20 دقيقة الدرس 22 من 35

تحديات أداء GraphQL

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

تحليل تعقيد الاستعلام

حلل وحدد تعقيد الاستعلام لمنع العمليات كثيفة الموارد:

const { createComplexityLimitRule } = require('graphql-validation-complexity'); const complexityLimit = createComplexityLimitRule(1000, { scalarCost: 1, objectCost: 10, listFactor: 20, introspectionListFactor: 2, // حساب التعقيد المخصص onCost: (cost) => { console.log('تكلفة الاستعلام:', cost); }, // تنسيق رسالة الخطأ المخصصة formatErrorMessage: (cost) => { return `الاستعلام معقد جدًا: ${cost}. الحد الأقصى للتعقيد المسموح به: 1000`; } }); const server = new ApolloServer({ typeDefs, resolvers, validationRules: [complexityLimit] });
ملاحظة: يتم حساب تعقيد الاستعلام عادةً عن طريق تعيين تكاليف لأنواع الحقول المختلفة. تحتوي الحقول العددية على تكاليف أقل، بينما القوائم والكائنات المتداخلة لها تكاليف أعلى.

تحديد العمق

منع الاستعلامات المتداخلة بشكل مفرط التي يمكن أن تسبب مشاكل في الأداء:

const depthLimit = require('graphql-depth-limit'); const server = new ApolloServer({ typeDefs, resolvers, validationRules: [depthLimit(5)], // عمق أقصى 5 مستويات // معالجة الأخطاء المخصصة formatError: (error) => { if (error.message.includes('exceeds maximum operation depth')) { return new Error('الاستعلام متداخل بعمق كبير. يرجى تبسيط استعلامك.'); } return error; } }); // سيتم رفض هذا الاستعلام (العمق > 5): // query { // user { // العمق 1 // posts { // العمق 2 // comments { // العمق 3 // author { // العمق 4 // posts { // العمق 5 // tags { // العمق 6 - مرفوض // name // } // } // } // } // } // } // }

DataLoader - التجميع والتخزين المؤقت

يحل DataLoader مشكلة استعلام N+1 عن طريق تجميع طلبات قاعدة البيانات وتخزينها مؤقتًا:

const DataLoader = require('dataloader'); // إنشاء مثيل DataLoader const userLoader = new DataLoader(async (userIds) => { console.log('تحميل المستخدمين المجمعين:', userIds); // استعلام قاعدة بيانات واحد لجميع معرفات المستخدمين const users = await User.findAll({ where: { id: userIds } }); // إرجاع المستخدمين بنفس ترتيب المعرفات المطلوبة const userMap = new Map(users.map(user => [user.id, user])); return userIds.map(id => userMap.get(id)); }); // المحللات باستخدام DataLoader const resolvers = { Post: { author: (post, args, { loaders }) => { // DataLoader يجمع ويخزن مؤقتًا تلقائيًا return loaders.user.load(post.authorId); } }, Comment: { author: (comment, args, { loaders }) => { // إذا تم تحميل المستخدم بالفعل، يعود من التخزين المؤقت return loaders.user.load(comment.authorId); } } }; // دالة السياق لإنشاء المحملات لكل طلب const server = new ApolloServer({ typeDefs, resolvers, context: () => ({ loaders: { user: new DataLoader(batchLoadUsers), post: new DataLoader(batchLoadPosts) } }) }); // بدون DataLoader: استعلامات N+1 // الاستعلام 1: SELECT * FROM posts // الاستعلام 2: SELECT * FROM users WHERE id = 1 // الاستعلام 3: SELECT * FROM users WHERE id = 2 // ... (استعلام واحد لكل منشور) // مع DataLoader: استعلامان // الاستعلام 1: SELECT * FROM posts // الاستعلام 2: SELECT * FROM users WHERE id IN (1, 2, 3, ...)
نصيحة: قم دائمًا بإنشاء مثيلات DataLoader جديدة لكل طلب (في دالة السياق) لمنع تخزين البيانات مؤقتًا عبر مستخدمين أو طلبات مختلفة.

القائمة البيضاء للاستعلامات (الاستعلامات الدائمة)

قصر التنفيذ على الاستعلامات المعتمدة مسبقًا لتعزيز الأمان والأداء:

const { ApolloServer } = require('apollo-server'); // مخزن تجزئات الاستعلامات المعتمدة const persistedQueries = { 'abc123hash': ` query GetUser($id: ID!) { user(id: $id) { id name email } } `, 'def456hash': ` query GetPosts { posts { id title author { name } } } ` }; const server = new ApolloServer({ typeDefs, resolvers, plugins: [{ async requestDidStart() { return { async didResolveOperation(context) { const { queryHash, query } = context.request; // السماح فقط بالاستعلامات الدائمة if (!queryHash || !persistedQueries[queryHash]) { throw new Error('يُسمح فقط بالاستعلامات الدائمة'); } // استبدال الاستعلام بالنسخة الدائمة context.request.query = persistedQueries[queryHash]; } }; } }] }); // العميل يرسل تجزئة الاستعلام بدلاً من الاستعلام الكامل // POST /graphql // { // "queryHash": "abc123hash", // "variables": { "id": "1" } // }

الاستعلامات الدائمة التلقائية (APQ)

دع العملاء يرسلون تجزئات الاستعلام مع العودة التلقائية إلى الاستعلامات الكاملة:

const { ApolloServer } = require('apollo-server'); const server = new ApolloServer({ typeDefs, resolvers, // تمكين الاستعلامات الدائمة التلقائية persistedQueries: { cache: new Map(), // استخدم Redis في الإنتاج ttl: 900, // 15 دقيقة } }); // التدفق: // 1. العميل يرسل تجزئة الاستعلام // 2. إذا لم يكن لدى الخادم، يُرجع خطأ // 3. العميل يرسل الاستعلام الكامل + التجزئة // 4. الخادم يخزن وينفذ // 5. الطلبات المستقبلية تحتاج فقط إلى التجزئة
ملاحظة: في الإنتاج، استخدم ذاكرة تخزين مؤقت موزعة مثل Redis بدلاً من Map في الذاكرة لدعم مثيلات الخادم المتعددة.

تتبع Apollo Server

تمكين مقاييس الأداء التفصيلية والتتبع:

const { ApolloServer } = require('apollo-server'); const { ApolloServerPluginInlineTrace } = require('apollo-server-core'); const server = new ApolloServer({ typeDefs, resolvers, plugins: [ // التتبع المضمن لـ Apollo Studio ApolloServerPluginInlineTrace(), // مكون التتبع المخصص { async requestDidStart() { const startTime = Date.now(); return { async willSendResponse(context) { const duration = Date.now() - startTime; console.log(`تم تنفيذ الاستعلام في ${duration}ms`); // تسجيل الاستعلامات البطيئة if (duration > 1000) { console.warn('تم اكتشاف استعلام بطيء:', { query: context.request.query, variables: context.request.variables, duration }); } } }; } } ] }); // الاستجابة تتضمن بيانات التتبع // { // "data": { ... }, // "extensions": { // "tracing": { // "version": 1, // "startTime": "2026-02-16T10:00:00.000Z", // "endTime": "2026-02-16T10:00:00.123Z", // "duration": 123456789, // "execution": { // "resolvers": [ // { // "path": ["user"], // "parentType": "Query", // "fieldName": "user", // "returnType": "User", // "startOffset": 1234567, // "duration": 45678901 // } // ] // } // } // } // }

التخزين المؤقت للاستجابة

قم بتخزين استجابات الاستعلام بالكامل مؤقتًا للطلبات المتكررة:

const { ApolloServer } = require('apollo-server'); const responseCachePlugin = require('apollo-server-plugin-response-cache'); const server = new ApolloServer({ typeDefs, resolvers, plugins: [ responseCachePlugin({ // وقت البقاء الافتراضي للتخزين المؤقت sessionId: (context) => context.userId || null, // إنشاء مفتاح التخزين المؤقت المخصص generateCacheKey: (context) => { const { request, userId } = context; return `${userId || 'anonymous'}:${request.operationName}:${JSON.stringify(request.variables)}`; } }) ], // استخدام التخزين المؤقت الخارجي (يوصى بـ Redis) cache: new RedisCache({ host: 'localhost', port: 6379 }) }); // المخطط مع تلميحات التخزين المؤقت const typeDefs = gql` type Query { # التخزين المؤقت لمدة 60 ثانية، ذاكرة تخزين مؤقت عامة posts: [Post!]! @cacheControl(maxAge: 60, scope: PUBLIC) # التخزين المؤقت لمدة 300 ثانية، ذاكرة تخزين مؤقت خاصة لكل مستخدم me: User @cacheControl(maxAge: 300, scope: PRIVATE) # لا تخزين مؤقت latestPrice: Float @cacheControl(maxAge: 0) } type Post { id: ID! title: String! # وراثة التحكم في التخزين المؤقت من الأب أو استخدام 30 ثانية author: User @cacheControl(maxAge: 30, inheritMaxAge: true) } `;
تحذير: كن حذرًا عند تخزين الاستجابات التي تحتوي على بيانات خاصة بالمستخدم أو حساسة مؤقتًا. استخدم دائمًا scope: PRIVATE للمحتوى المخصص.

مراقبة الأداء على مستوى الحقل

تتبع أداء المحللات الفردية:

const { GraphQLExtension } = require('graphql-extensions'); class PerformanceExtension extends GraphQLExtension { willResolveField(source, args, context, info) { const startTime = Date.now(); return (error, result) => { const duration = Date.now() - startTime; // تسجيل المحللات البطيئة if (duration > 100) { console.warn('محلل بطيء:', { field: info.fieldName, type: info.parentType.name, duration: `${duration}ms` }); } // تتبع المقاييس context.metrics = context.metrics || {}; context.metrics[`${info.parentType.name}.${info.fieldName}`] = duration; }; } } const server = new ApolloServer({ typeDefs, resolvers, extensions: [() => new PerformanceExtension()] });

أفضل ممارسات الترقيم

نفذ ترقيمًا فعالًا لتجنب تحميل مجموعات البيانات الكبيرة:

// الترقيم المستند إلى المؤشر (موصى به) const typeDefs = gql` type Query { posts(first: Int!, after: String): PostConnection! } type PostConnection { edges: [PostEdge!]! pageInfo: PageInfo! } type PostEdge { cursor: String! node: Post! } type PageInfo { hasNextPage: Boolean! endCursor: String } `; const resolvers = { Query: { posts: async (parent, { first, after }) => { const limit = Math.min(first, 100); // حد أقصى عند 100 const offset = after ? parseInt(Buffer.from(after, 'base64').toString()) : 0; const posts = await Post.findAll({ limit: limit + 1, // احصل على واحد إضافي للتحقق من hasNextPage offset }); const hasNextPage = posts.length > limit; const edges = posts.slice(0, limit).map((post, index) => ({ cursor: Buffer.from((offset + index).toString()).toString('base64'), node: post })); return { edges, pageInfo: { hasNextPage, endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null } }; } } };
تمرين: قم بتحسين خادم GraphQL مع المتطلبات التالية:
  1. تنفيذ DataLoader لتجميع استعلامات المستخدم والمنشورات
  2. إضافة تحديد لتعقيد الاستعلام (أقصى تعقيد 500)
  3. تعيين الحد الأقصى لعمق الاستعلام إلى 4 مستويات
  4. تمكين التخزين المؤقت للاستجابة مع TTL 60 ثانية للمنشورات العامة
  5. إضافة مراقبة الأداء التي تسجل الاستعلامات التي تستغرق أكثر من 200 ملي ثانية
اختبر باستعلام متداخل معقد وتحقق من أن التجميع يعمل بشكل صحيح.