تغيير مخطط جداول PostgreSQL بدون أقفال طويلة. محاضرة ياندكس

إذا تم تنفيذ العديد من العمليات في نفس الوقت لتغيير مخطط قاعدة البيانات ، فلن تعمل الخدمة بشكل صحيح على التسجيل. أوضح المطور Vladimir Kolyasinsky ما هي العمليات في PostgreSQL التي تتطلب أقفال طويلة الأجل وكيف يضمن فريق Yandex.Connect أن تكون الخدمة قابلة للكتابة تقريبًا خلال هذه العمليات. بالإضافة إلى ذلك ، سوف تتعلم عن مكتبة Django ، المصممة لأتمتة جزء من العمليات الموصوفة.


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

- مساء الخير جميعا! اسمي فلاديمير ، لقد عملت في ياندكس منذ خمس سنوات. خلال العامين الماضيين ، قمت بتطوير الخدمات والخدمات الداخلية للمؤسسات.

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

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



أقوم حاليًا بتطوير "مصمم النماذج" و "ويكي". المكدس المستخدم هو بشكل رئيسي خدمات مكتوبة في Python من الإصدارين الثاني والثالث ؛ جانغو 1.9-1.11. كقاعدة بيانات ، معظمها هو PostgreSQL. إنه أيضًا كرفس مع MongoDB و SQS كوسطاء. كل هذا يعمل في دوكر.

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

اليوم سنتحدث عن كيفية تعاملنا مع مثل هذه المواقف وكيف نحقق توفرًا كبيرًا في خدمات القراءة والكتابة.

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

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

العمليات التي تتطلب قفل طويل:



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

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

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

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

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





إنشاء فهرس. الأمر بسيط للغاية هنا ، ويمكن إنشاؤه باستخدام الكلمة الرئيسية CONCURRENTLY. ما هو الفرق؟ ستستغرق هذه العملية مزيدًا من الوقت ، حيث لن يتم تنفيذ أي عملية مرور ، لكن سيتم تنفيذ العديد من التمريرات عبر الجدول ، وستنتظر أيضًا إكمال جميع العمليات الحالية التي يمكن أن تغير الفهرس. وقد يفشل أيضًا - على سبيل المثال ، في حالة انتهاك فهرس فريد عند إنشاء فهرس فريد. بعد ذلك ، سيتم وضع علامة على الفهرس على أنه غير صالح ، ويجب حذفه وإعادة إنشائه. لا ينصح باستخدام الأمر REINDEX ، لأنه يعمل بنفس طريقة CREATE INDEX العادية ، أي أنه سيغلق الجدول للكتابة.

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



دعنا ننظر إلى إضافة عمود جديد بقيمة افتراضية. فيما يلي عملية قياسية يتم تنفيذها عندما نريد تنفيذ مثل هذا الأمر ، بما في ذلك تنفيذ Django لهذه العملية.

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

بعد قيامنا بتحديث جميع البيانات ، يبقى فقط تنفيذ SET NOT NULL إذا أنشأنا عمود NOT NULL. إذا لم ننشئ ، فلا تفعل ذلك. بهذه الطريقة يمكنك تجنب الكتابة فوق الجدول عند إجراء هذا النوع من التغيير.

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



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

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



حول إضافة عمود فريد. يؤخذ قفل في وقت الخلق. يمكن تجنبه إذا كنت تعرف أن التفرد في PostgreSQL مضمون من خلال بناء فهرس فريد. يمكننا نحن أنفسنا بناء الفهرس الفريد المطلوب باستخدام CONCURRENTLY. وبعد بناء هذا الفهرس ، قم بإنشاء CONSTRAINT باستخدام هذا الفهرس. بعد ذلك ، سيختفي تعريف الفهرس الأولي من الجدول ، والنتيجة التي سيوضحها تعريف الجدول لن تختلف بعد إجراء هاتين العمليتين.



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

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

العمليات التي تعمل في البداية بسرعة في PostgreSQL ولا تتطلب أقفال طويلة:



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

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

كذلك ، فإن التشغيل السريع لإعداد SET NOT NULL ، هنا يستغرق وقتًا أطول بقليل مما هو موضح سابقًا ، حوالي بضع ثوانٍ لكل جدول من 30 مليون سجل. هذه المرة يمكن أيضا تجنبها إذا كانت مهمة.

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



فيما يتعلق بإضافة عمود NOT NULL. لتجنب الحجب أثناء التحقق من الصحة ، يمكنك تنفيذ الطريقة المذكورة سابقًا - إضافة CONSTRAINT المطابقة لـ CHECK (العمود غير فارغ) NOT VALID ، والتحقق من صحتها باستخدام أمر منفصل.

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



يعد حذف البيانات في PostgreSQL عمومًا عملية سريعة ، حيث لا يتم حذف البيانات على الفور ، يتم تمييز العمود فقط في سمات الجدول ، وسيتم حذف البيانات بالفعل فقط بعد بداية الفراغ التالي.



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

تمت كتابة المكتبة لأتمتة بعض التقنيات التي تمت مناقشتها سابقًا لتجنب الأقفال الطويلة على الطاولة أثناء عمليات الترحيل هذه. تعمل مع Django منذ الإصدار 1.8 إلى 2.1 ضمناً ، وبيثون من 2.7 إلى 3.7 ضمناً.

فيما يتعلق بالميزات الحالية للمكتبة ، فإن هذا يضيف عمودًا ذو قيمة افتراضية بدون أقفال ، لاغية أو لا ، مما يؤدي إلى إنشاء فهرس CONCURRENTLY ، بالإضافة إلى القدرة على إعادة التشغيل عند تعطله. في تطبيق Django القياسي ، إذا أضفنا عمودًا ذو قيمة افتراضية ، فسيكون الجدول مغلقًا ، وإذا كان حجمه كبيرًا ، فقد يستغرق الأمر 40 دقيقة من تجربتي. الجدول مغلق ، وهذا كل شيء ، انتظر حتى يتم نسخ التغييرات وإجرائها. مرت 30 دقيقة - اكتشفوا خطأ الاتصال بقاعدة البيانات ، وسقوط الترحيل ، والتغييرات ليست ملتزمة ، وعليك أن تبدأ من جديد ، انتظر 40 دقيقة مرة أخرى ، وحظر الجدول مرة أخرى لهذا الوقت.


جيثب لينك

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



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



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

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

ثم يتم تعيين SET NOT NULL في العمود ، وسيتم حذف القيمة الافتراضية ، لتكرار سلوك Django ، الذي يخزن القيمة الافتراضية ليس في قاعدة البيانات ، ولكن على مستوى المنطق الخاص به في التعليمات البرمجية.

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



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

بشكل عام ، يتم تطوير المكتبة ، ونحن نقبل طلبات التجمع. من يهتم - الانضمام.

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

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

تجدر الإشارة إلى أنه حتى العمليات السريعة في PostgreSQL يمكن أن تتسبب في تباطؤ الخدمة والأخطاء بسبب طريقة عمل قائمة انتظار القفل في PostgreSQL.

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

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

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

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


All Articles