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

تدور قفل
على عكس الأقفال "الثقيلة" العادية ، يتم استخدام أقفال أخف وزنا وأرخص (من حيث الحمل) لحماية الهياكل في ذاكرة الوصول العشوائي المشتركة.
أبسط منهم
الأقفال تدور أو
spinlocks . وهي مصممة لالتقاط لفترة قصيرة جدًا (عدة تعليمات للمعالج) وحماية أقسام الذاكرة الفردية من التغييرات المتزامنة.
يتم تطبيق أقفال الدوران بناءً على تعليمات ذرية من المعالج ، مثل المقارنة والمبادلة. أنها تدعم وضع حصري واحد. إذا كان القفل مشغولاً ، فعملية الانتظار تنتظر انتظارًا نشطًا - يتكرر الأمر ("يدور" في الحلقة ، وبالتالي الاسم) حتى يتم تنفيذه بنجاح. هذا أمر منطقي ، حيث يتم استخدام أقفال الدوران عندما يكون احتمال الصراع منخفضًا للغاية.
لا توفر أقفال الدوران الكشف عن حالة توقف تام (يراقب مطورو PostgreSQL هذا) ولا يقدمون أي أدوات مراقبة. إلى حد كبير ، فإن الشيء الوحيد الذي يمكننا القيام به مع أقفال الدوران هو معرفة وجودها.
أقفال الضوء
يأتي بعد ذلك
الأقفال الخفيفة المزعومة (الأقفال الخفيفة ، الأقفال).
يتم التقاطها للوقت القصير الذي تستغرقه للعمل مع بنية البيانات (على سبيل المثال ، جدول التجزئة أو قائمة المؤشرات). كقاعدة عامة ، لا يتم قفل قفل الضوء لفترة طويلة ، ولكن في بعض الحالات ، يحمي قفل الضوء عمليات الإدخال / الإخراج ، لذلك من حيث المبدأ ، يمكن أن يكون الوقت كبيرًا.
يتم دعم وضعين: حصري (لتغيير البيانات) ومشاركته (للقراءة فقط). على هذا النحو ، لا توجد قائمة انتظار للانتظار: إذا كانت هناك عدة عمليات تنتظر إصدار القفل ، فستتمكن إحداها من الوصول بشكل عشوائي أو أكثر. في الأنظمة التي تتمتع بدرجة عالية من التوازي والحمل الثقيل ، يمكن أن يؤدي ذلك إلى تأثيرات غير سارة (انظر ، على سبيل المثال ،
المناقشة ).
لم يتم توفير آلية للتحقق من الجمود ، وهذا لا يزال على ضمير مطوري النواة. ومع ذلك ، تحتوي أقفال الإضاءة على أدوات مراقبة ، وبالتالي ، على عكس أقفال الدوران ، يمكن "رؤيتها" (بعد فترة وجيزة سأظهر لك كيف).
مقطع العازلة
هناك نوع آخر من القفل الذي ناقشناه بالفعل في المقالة حول
ذاكرة التخزين المؤقت المخزن المؤقت وهو
تثبيت العازلة .
مع وجود مخزن مؤقت ثابت ، يمكنك تنفيذ العديد من الإجراءات ، بما في ذلك تغيير البيانات ، ولكن بشرط أن هذه التغييرات لن تكون مرئية للعمليات الأخرى بسبب الإصدارات المتعددة. وهذا يعني ، يمكنك إضافة سطر جديد إلى الصفحة ، ولكن لا يمكنك استبدال الصفحة في المخزن المؤقت بسطر آخر.
إذا تم إعاقة العملية عن طريق الربط ، فعادةً ما تتخطى مثل هذا المخزن المؤقت وتختار أخرى. ولكن في بعض الحالات ، عندما يكون هذا المخزن المؤقت المحدد مطلوبًا ، فإن قوائم الانتظار العملية تغفو - يستيقظ النظام عند إزالة الرابط.
توقعات التوحيد متاحة للمراقبة.
مثال: ذاكرة التخزين المؤقت المخزن المؤقت

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

مثال آخر: سجل المخازن المؤقتة.
بالنسبة لذاكرة التخزين المؤقت للمجلة ، يتم أيضًا استخدام جدول التجزئة الذي يحتوي على تعيين الصفحات إلى المخازن المؤقتة. بخلاف ذاكرة التخزين المؤقت المخزن المؤقت ، يتم حماية جدول التجزئة هذا بواسطة القفل الخفيف الوحيد في WALBufMappingLock ، نظرًا لأن حجم ذاكرة التخزين المؤقت للمجلة يكون أصغر (عادة ما يكون 1/32 من ذاكرة التخزين المؤقت المخزن المؤقت) ويكون الوصول إلى المخازن المؤقتة أكثر انسيابية.
تتم حماية صفحات الكتابة على القرص بواسطة قفل WALWriteLock خفيف الوزن بحيث يمكن إجراء عملية واحدة فقط في كل مرة.
لإنشاء إدخال دفتر يومية ، يجب أن تحجز العملية أولاً مسافة على صفحة WAL. للقيام بذلك ، فإنه يلتقط قفل موضع إدراج قفل الدوران. بعد حجز مكان ، تقوم العملية بنسخ محتويات سجلها إلى المكان المحدد. يمكن إجراء النسخ من خلال عدة عمليات في نفس الوقت ، والذي يتم حماية السجل من خلال شريحة مكونة من 8 أقفال سهلة لإدخال القفل (يجب أن تلتقط العملية
أيًا منها).
لا يُظهر الشكل جميع الأقفال المتعلقة بسجل السجل المسبق ، ولكن هذا والمثال السابق يجب أن يعطيا فكرة عن استخدام الأقفال في ذاكرة الوصول العشوائي.
رصد التوقعات
بدءًا من PostgreSQL 9.6 ، يتم تضمين أدوات مراقبة الانتظار في طريقة العرض pg_stat_activity. عندما لا تتمكن العملية (النظام أو الصيانة) من أداء عملها وتنتظر شيئًا ما ، يمكن رؤية هذا التوقع في طريقة العرض: يشير العمود wait_event_type إلى نوع التوقع ، ويشير العمود wait_event إلى اسم توقع محدد.
ضع في اعتبارك أن طريقة العرض تظهر فقط تلك التوقعات التي يتم التعامل معها بشكل مناسب في التعليمات البرمجية المصدر. إذا لم تظهر طريقة العرض التوقع ، فهذا لا يعني عمومًا احتمال 100٪ أن العملية لا تتوقع أي شيء حقًا.
لسوء الحظ ، فإن المعلومات الوحيدة المتاحة حول التوقعات هي المعلومات
الحالية . لا توجد إحصاءات محفوظة. الطريقة الوحيدة للحصول على صورة للتوقعات بمرور الوقت هي
أخذ عينات من طريقة العرض في فاصل زمني محدد. لا توجد وسائل مدمجة لهذا الغرض ، لكن يمكنك استخدام الامتدادات ، على سبيل المثال ،
pg_wait_sampling .
من الضروري مراعاة الطبيعة الاحتمالية لأخذ العينات. للحصول على صورة أكثر أو أقل موثوقية ، يجب أن يكون عدد القياسات كبيرًا بدرجة كافية. قد لا يعطي أخذ العينات بتردد منخفض صورة يمكن الاعتماد عليها ، وستؤدي زيادة التردد إلى زيادة في الحمل. للسبب نفسه ، أخذ العينات غير مجدية لتحليل جلسات قصيرة الأجل.
كل التوقعات يمكن تقسيمها إلى عدة أنواع.
تشكل الأقفال المدروسة فئة كبيرة:
- في انتظار تأمين الكائنات (قيمة التأمين في العمود wait_event_type) ؛
- انتظار الأقفال الخفيفة (LWLock) ؛
- انتظار المخزن المؤقت مثبت (BufferPin).
لكن العمليات يمكن أن تتوقع أحداثًا أخرى:
- تحدث توقعات I / O (IO) عندما تحتاج العملية إلى كتابة البيانات أو قراءتها ؛
- يمكن للعملية انتظار البيانات الضرورية للعمل من العميل (العميل) أو من عملية أخرى (IPC) ؛
- يمكن للإضافات تسجيل توقعاتهم المحددة (ملحق).
هناك حالات عندما لا تؤدي العملية ببساطة عملاً مفيدًا. هذه الفئة تشمل:
- انتظار العمليات الخلفية في حلقة الرئيسي (النشاط) ؛
- في انتظار مؤقت (مهلة).
كقاعدة عامة ، هذه التوقعات "طبيعية" ولا تتحدث عن أي مشاكل.
يتبع نوع التوقع اسم التوقع المحدد. يمكن العثور على الجدول الكامل
في الوثائق .
إذا لم يتم تحديد اسم انتظار ، فلن تكون العملية في حالة انتظار. يجب اعتبار مثل هذا الوقت في
عداد المفقودين ، لأنه في الواقع ليس من المعروف ما يحدث بالضبط في هذه اللحظة.
ومع ذلك ، فقد حان الوقت للنظر.
=> SELECT pid, backend_type, wait_event_type, wait_event FROM pg_stat_activity;
pid | backend_type | wait_event_type | wait_event -------+------------------------------+-----------------+--------------------- 28739 | logical replication launcher | Activity | LogicalLauncherMain 28736 | autovacuum launcher | Activity | AutoVacuumMain 28963 | client backend | | 28734 | background writer | Activity | BgWriterMain 28733 | checkpointer | Activity | CheckpointerMain 28735 | walwriter | Activity | WalWriterMain (6 rows)
يمكن ملاحظة أن جميع عمليات خدمة الخلفية "تعبث". تشير القيم الفارغة في wait_event_type و wait_event إلى أن العملية لا تتوقع أي شيء - في حالتنا ، عملية التقديم مشغولة في تنفيذ الطلب.
أخذ العينات
للحصول على صورة كاملة أو أقل للتوقعات باستخدام أخذ العينات ، نستخدم امتداد
pg_wait_sampling . يجب أن يتم تجميعها من شفرة المصدر ؛ سوف أغفل هذا الجزء. ثم نقوم بتسجيل المكتبة في المعلمة
Shared_preload_libraries وإعادة تشغيل الخادم.
=> ALTER SYSTEM SET shared_preload_libraries = 'pg_wait_sampling';
student$ sudo pg_ctlcluster 11 main restart
الآن تثبيت التمديد في قاعدة البيانات.
=> CREATE EXTENSION pg_wait_sampling;
يسمح لك الملحق بعرض محفوظات التوقع ، والتي يتم تخزينها في مخزن مؤقت دائري. ولكن الشيء الأكثر إثارة للاهتمام هو رؤية ملف تعريف التوقعات - الإحصاءات المتراكمة طوال فترة العمل.
إليك ما سنراه خلال بضع ثوانٍ:
=> SELECT * FROM pg_wait_sampling_profile;
pid | event_type | event | queryid | count -------+------------+---------------------+---------+------- 29074 | Activity | LogicalLauncherMain | 0 | 220 29070 | Activity | WalWriterMain | 0 | 220 29071 | Activity | AutoVacuumMain | 0 | 219 29069 | Activity | BgWriterMain | 0 | 220 29111 | Client | ClientRead | 0 | 3 29068 | Activity | CheckpointerMain | 0 | 220 (6 rows)
نظرًا لعدم حدوث أي شيء منذ بدء تشغيل الخادم ، فإن التوقعات الرئيسية هي من نوع النشاط (عمليات الخدمة تنتظر حتى يظهر العمل) والعميل (ينتظر psql للمستخدم لإرسال طلب).
باستخدام الإعدادات الافتراضية (
pg_wait_sampling.profile_period المعلمة) ، تكون فترة أخذ العينات 10 مللي ثانية ، أي ، يتم حفظ القيم 100 مرة في الثانية. لذلك ، لتقدير مدة الانتظار بالثواني ، يجب تقسيم قيمة العدد على 100.
لفهم توقعات العملية التي تنتمي إليها ، نضيف طريقة العرض pg_stat_activity إلى الطلب:
=> SELECT p.pid, a.backend_type, a.application_name AS app, p.event_type, p.event, p.count FROM pg_wait_sampling_profile p LEFT JOIN pg_stat_activity a ON p.pid = a.pid ORDER BY p.pid, p.count DESC;
pid | backend_type | app | event_type | event | count -------+------------------------------+------+------------+----------------------+------- 29068 | checkpointer | | Activity | CheckpointerMain | 222 29069 | background writer | | Activity | BgWriterMain | 222 29070 | walwriter | | Activity | WalWriterMain | 222 29071 | autovacuum launcher | | Activity | AutoVacuumMain | 221 29074 | logical replication launcher | | Activity | LogicalLauncherMain | 222 29111 | client backend | psql | Client | ClientRead | 4 29111 | client backend | psql | IPC | MessageQueueInternal | 1 (7 rows)
دعونا تحميل مع pgbench ونرى كيف تتغير الصورة.
student$ pgbench -i test
نقوم بإعادة تعيين ملف التعريف الذي تم جمعه إلى صفر ونجري الاختبار لمدة 30 ثانية في عملية منفصلة.
=> SELECT pg_wait_sampling_reset_profile();
student$ pgbench -T 30 test
يجب إكمال الطلب قبل اكتمال عملية pgbench:
=> SELECT p.pid, a.backend_type, a.application_name AS app, p.event_type, p.event, p.count FROM pg_wait_sampling_profile p LEFT JOIN pg_stat_activity a ON p.pid = a.pid WHERE a.application_name = 'pgbench' ORDER BY p.pid, p.count DESC;
pid | backend_type | app | event_type | event | count -------+----------------+---------+------------+------------+------- 29148 | client backend | pgbench | IO | WALWrite | 8 29148 | client backend | pgbench | Client | ClientRead | 1 (2 rows)
بطبيعة الحال ، فإن توقعات عملية pgbench ستكون مختلفة بعض الشيء اعتمادًا على النظام المحدد. في حالتنا ، من المحتمل جدًا تقديم انتظار إدخال سجل (IO / WALWrite) ، لكن في معظم الوقت لم تتوقف العملية ، ولكنها فعلت شيئًا مفيدًا.
أقفال الضوء
يجب أن تتذكر دائمًا أن عدم وجود أي توقعات عند أخذ العينات لا يعني عدم وجود توقعات. إذا كانت الفترة أقصر من فترة أخذ العينات (مائة ثانية في مثالنا) ، فلن تتمكن ببساطة من الوقوع في العينة.
لذلك ، لم تظهر أقفال الإضاءة في ملف التعريف - ولكنها ستظهر إذا قمت بجمع البيانات لفترة طويلة. لضمان إلقاء نظرة عليها ، يمكنك إبطاء نظام الملفات بشكل مصطنع ، على سبيل المثال ، استخدام مشروع
slowfs المبني على نظام الملفات
FUSE .
هذا هو ما يمكننا رؤيته في نفس الاختبار إذا استغرقت أي عملية إدخال / إخراج 1/10 من الثانية.
=> SELECT pg_wait_sampling_reset_profile();
student$ pgbench -T 30 test
=> SELECT p.pid, a.backend_type, a.application_name AS app, p.event_type, p.event, p.count FROM pg_wait_sampling_profile p LEFT JOIN pg_stat_activity a ON p.pid = a.pid WHERE a.application_name = 'pgbench' ORDER BY p.pid, p.count DESC;
pid | backend_type | app | event_type | event | count -------+----------------+---------+------------+----------------+------- 29240 | client backend | pgbench | IO | WALWrite | 1445 29240 | client backend | pgbench | LWLock | WALWriteLock | 803 29240 | client backend | pgbench | IO | DataFileExtend | 20 (3 rows)
الآن ، يرتبط التوقع الرئيسي لعملية pgbench بـ I / O ، أو بالأحرى ، إدخال سجل يتم تنفيذه في وضع متزامن مع كل التزام. بما أنه (كما هو موضح في المثال أعلاه) ، فإن كتابة سجل إلى القرص محمي بواسطة قفل المصباح WALWriteLock ، فإن هذا القفل موجود أيضًا في الملف الشخصي - أردنا أن ننظر إليه.
مقطع العازلة
لرؤية تثبيت المخزن المؤقت ، نستفيد من حقيقة أن المؤشرات المفتوحة تحمل الدبوس بحيث تكون قراءة السطر التالي أسرع.
نبدأ المعاملة ونفتح المؤشر ونختار صفًا واحدًا.
=> BEGIN; => DECLARE c CURSOR FOR SELECT * FROM pgbench_history; => FETCH c;
tid | bid | aid | delta | mtime | filler -----+-----+-------+-------+----------------------------+-------- 9 | 1 | 35092 | 477 | 2019-09-04 16:16:18.596564 | (1 row)
تحقق من تثبيت المخزن المؤقت (pinning_backends):
=> SELECT * FROM pg_buffercache WHERE relfilenode = pg_relation_filenode('pgbench_history') AND relforknumber = 0 \gx
-[ RECORD 1 ]----+------ bufferid | 190 relfilenode | 47050 reltablespace | 1663 reldatabase | 16386 relforknumber | 0 relblocknumber | 0 isdirty | t usagecount | 1 pinning_backends | 1 <-- 1
الآن سوف نقوم
بمسح الجدول:
| => SELECT pg_backend_pid();
| pg_backend_pid | ---------------- | 29367 | (1 row)
| => VACUUM VERBOSE pgbench_history;
| INFO: vacuuming "public.pgbench_history" | INFO: "pgbench_history": found 0 removable, 0 nonremovable row versions in 1 out of 1 pages | DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 732651 | There were 0 unused item pointers.
| Skipped 1 page due to buffer pins, 0 frozen pages.
| 0 pages are entirely empty. | CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s. | VACUUM
كما نرى ، تم تخطي الصفحة (تم تخطي صفحة واحدة بسبب المسامير المؤقتة). في الواقع ، لا يمكن للتنظيف معالجته ، لأنه يُحظر حذف إصدارات الصفوف فعليًا من صفحة في مخزن مؤقت مثبت. لكن التنظيف لن ينتظر - ستتم معالجة الصفحة في المرة القادمة.
والآن سنقوم بعملية
التنظيف بالتجميد :
| => VACUUM FREEZE VERBOSE pgbench_history;
من خلال التجميد المطلوب بوضوح ، لا يمكنك تخطي صفحة واحدة غير محددة في خريطة التجميد - وإلا فإنه من المستحيل تقليل الحد الأقصى لسن المعاملات غير المجمدة في pg_class.relfrozenxid. لذلك ، توقف التنظيف حتى يغلق المؤشر.
=> SELECT age(relfrozenxid) FROM pg_class WHERE oid = 'pgbench_history'::regclass;
age ----- 27 (1 row)
=> COMMIT;
| INFO: aggressively vacuuming "public.pgbench_history" | INFO: "pgbench_history": found 0 removable, 26 nonremovable row versions in 1 out of 1 pages | DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 732651 | There were 0 unused item pointers.
| Skipped 0 pages due to buffer pins, 0 frozen pages.
| 0 pages are entirely empty. | CPU: user: 0.00 s, system: 0.00 s, elapsed: 3.01 s. | VACUUM
=> SELECT age(relfrozenxid) FROM pg_class WHERE oid = 'pgbench_history'::regclass;
age ----- 0 (1 row)
حسنًا ، دعنا ننظر إلى ملف تعريف التوقعات لجلسة psql الثانية التي تم فيها تنفيذ أوامر VACUUM:
=> SELECT p.pid, a.backend_type, a.application_name AS app, p.event_type, p.event, p.count FROM pg_wait_sampling_profile p LEFT JOIN pg_stat_activity a ON p.pid = a.pid WHERE p.pid = 29367 ORDER BY p.pid, p.count DESC;
pid | backend_type | app | event_type | event | count -------+----------------+------+------------+------------+------- 29367 | client backend | psql | BufferPin | BufferPin | 294 29367 | client backend | psql | Client | ClientRead | 10 (2 rows)
يشير نوع الانتظار BufferPin إلى أن التدفق كان ينتظر تحرير المخزن المؤقت.
على هذا سنفترض أننا أكملنا الأقفال. شكرا لكم جميعا على اهتمامكم والتعليقات!