DBA: سعياً وراء الأقفال الطائرة

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

  • للوهلة الأولى ، القاعدة تعمل
  • لا يتم استنفاد موارد الخادم
  • ... لكن بعض الطلبات "تبطئ"

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

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

ما لم يتم إدخال سجل قصير: لكن دعونا نحاول الإمساك بها!
process ... still waiting for ...


نقبض على القفل في السجل


ولكن حتى يظهر هذا الخط على الأقل في السجل ، يجب تكوين الخادم بشكل صحيح:
log_lock_waits = 'on'
... ووضع حد أدنى لتحليلنا:
deadlock_timeout = '100ms'

عند تمكين log_lock_waitsالمعلمة ، تحدد هذه المعلمة أيضًا بعد أي وقت ستتم كتابة الرسائل المتعلقة بانتظار الحظر إلى سجل الخادم. إذا كنت تحاول التحقيق في التأخيرات التي تسببها الأقفال ، فمن المنطقي تقليلها مقارنة بالقيمة المعتادة deadlock_timeout.
سيتمزق كل طريق مسدود ليس لديه الوقت "لفك الارتباط" خلال هذا الوقت. وإليك الأقفال "المعتادة" - الملقاة في السجل عن طريق عدة إدخالات:
2019-03-27 00:06:46.026 MSK [38162:84/166010018] [inside.tensor.ru] [local] csr-inside-bl10:10081: MergeRequest LOG: process 38162 still waiting for ExclusiveLock on advisory lock [225382138,225386226,141586103,2] after 100.047 ms

2019-03-27 00:06:46.026 MSK [38162:84/166010018] [inside.tensor.ru] [local] csr-inside-bl10:10081: MergeRequest DETAIL: Process holding the lock: 38154. Wait queue: 38162.

2019-03-27 00:06:46.026 MSK [38162:84/166010018] [inside.tensor.ru] [local] csr-inside-bl10:10081: MergeRequest CONTEXT: SQL statement "SELECT pg_advisory_xact_lock( '""'::regclass::oid::integer, 141586103::integer )"
PL/pgSQL function inline_code_block line 2 at PERFORM


2019-03-27 00:06:46.026 MSK [38162:84/166010018] [inside.tensor.ru] [local] csr-inside-bl10:10081: MergeRequest STATEMENT: DO $$ BEGIN
PERFORM pg_advisory_xact_lock( '""'::regclass::oid::integer, 141586103::integer );
EXCEPTION
WHEN query_canceled THEN RETURN;
END; $$;



2019-03-27 00:06:46.077 MSK [38162:84/166010018] [inside.tensor.ru] [local] csr-inside-bl10:10081: MergeRequest LOG: process 38162 acquired ExclusiveLock on advisory lock [225382138,225386226,141586103,2] after 150.741 ms
يوضح هذا المثال أنه كان لدينا 50 مللي ثانية فقط من لحظة ظهور القفل في السجل حتى لحظة حله. فقط في هذا الفاصل الزمني يمكننا الحصول على بعض المعلومات حوله على الأقل ، وكلما استطعنا القيام بذلك بشكل أسرع ، كلما أمكننا تحليل المزيد من الأقفال.

هنا أهم المعلومات بالنسبة لنا هي PID للعملية التي تنتظر القفل. يمكننا أن نقارن المعلومات بين السجل وحالة قاعدة البيانات. وفي الوقت نفسه لمعرفة مدى مشكلة قفل معين - أي كم "علق".

للقيام بذلك ، نحتاج فقط إلى اثنين من RegExp في LOGسجل المحتوى :

const RE_lock_detect = /^process (\d+) still waiting for (.*) on (.*) after (\d+\.\d{3}) ms(?: at character \d+)?$/; //    
const RE_lock_acquire = /^process (\d+) acquired (.*) on (.*) after (\d+\.\d{3}) ms(?: at character \d+)?$/; //   

نصل إلى السجل


في الواقع ، يبقى قليلاً - لأخذ السجل ، وتعلم كيفية مراقبته والاستجابة وفقًا لذلك.

لدينا بالفعل كل شيء لهذا - محلل سجل عبر الإنترنت واتصال قاعدة بيانات تم تنشيطه مسبقًا ، والذي تحدثت عنه في مقالة حول نظام مراقبة PostgreSQL :


ولكن إذا لم يكن لديك أي نظام مشابه تم نشره ، فلا بأس ، سنفعل كل شيء "الحق من وحدة التحكم"!

إذا كان الإصدار المثبت من PostgreSQL 10 والإصدارات الأحدث محظوظًا ، لأنه ظهرت وظيفة pg_current_logfile () ، والتي تعطي صراحة اسم ملف السجل الحالي. سيكون الحصول عليه من وحدة التحكم سهلاً بما يكفي:

psql -U postgres postgres -t -A -c "SELECT CASE WHEN substr(current_setting('log_directory'), 1, 1) <> '/' THEN current_setting('data_directory') ELSE '' END || '/' || pg_current_logfile()"

إذا كانت النسخة أصغر فجأة - فسيظهر كل شيء ، ولكن أكثر تعقيدًا:

ps uw -U postgres \
  | grep [l]ogger \
  | awk '{print "/proc/"$2"/fd"}' \
  | xargs ls -l \
  | grep `cd; psql -U postgres postgres -t -A -c "SELECT CASE WHEN substr(current_setting('log_directory'), 1, 1) = '/' THEN current_setting('log_directory') ELSE current_setting('data_directory') || '/' || current_setting('log_directory') END"` \
  | awk '{print $NF}'

tail -fنحصل على اسم الملف الكامل ، الذي نعطيه وسنتلقى المظهر هناك 'still waiting for'.

حسنًا ، بمجرد ظهور شيء في هذا الموضوع ، نسميه ...

طلب تحليل


للأسف ، ليس الهدف من الحظر في السجل دائمًا هو المورد الذي تسبب في التعارض. على سبيل المثال ، إذا حاولت إنشاء فهرس يحمل نفس الاسم بالتوازي ، فلن يكون هناك شيء مثل هذا في السجل still waiting for ExclusiveLock on transaction 12345.

لذلك ، سيكون علينا إزالة حالة جميع الأقفال من قاعدة البيانات للعملية التي تهمنا. والمعلومات فقط حول زوج من العمليات (الذين يضعون القفل / من يتوقع ذلك) ليست كافية ، لأنه غالبًا ما تكون هناك "سلسلة" من التوقعات بشأن الموارد المختلفة بين المعاملات:

tx1 [resA] -> tx2 [resA]
tx2 [resB] -> tx3 [resB]
...

الكلاسيكية: "جد اللفت ، جدة الجد ، حفيدة الجد ، ..."

لذلك ، سنكتب طلبًا يخبرنا بالضبط من أصبح السبب الجذري للمشاكل على طول سلسلة الأقفال بأكملها لـ PID محدد:

WITH RECURSIVE lm AS (
-- lock modes
  SELECT
    rn
  , CASE
      WHEN lm <> 'Share' THEN row_number() OVER(PARTITION BY lm <> 'Share' ORDER BY rn)
    END rx
  , lm || 'Lock' lm
  FROM (
    SELECT
      row_number() OVER() rn
    , lm
    FROM
      unnest(
        ARRAY[
          'AccessShare'
        , 'RowShare'
        , 'RowExclusive'
        , 'ShareUpdateExclusive'
        , 'Share'
        , 'ShareRowExclusive'
        , 'Exclusive'
        , 'AccessExclusive'
        ]
      ) T(lm)
  ) T
)
-- lock types
, lt AS (
  SELECT
    row_number() OVER() rn
  , lt
  FROM
    unnest(
      ARRAY[
        'relation'
      , 'extend'
      , 'page'
      , 'tuple'
      , 'transactionid'
      , 'virtualxid'
      , 'object'
      , 'userlock'
      , 'advisory'
      , ''
      ]
    ) T(lt)
)
-- lock modes conflicts
, lmx AS (
  SELECT
    lr.rn lrn
  , lr.rx lrx
  , lr.lm lr
  , ld.rn ldn
  , ld.rx ldx
  , ld.lm ld
  FROM
    lm lr
  JOIN
    lm ld ON
      ld.rx > (SELECT max(rx) FROM lm) - lr.rx OR
      (
        (lr.rx = ld.rx) IS NULL AND
        (lr.rx, ld.rx) IS DISTINCT FROM (NULL, NULL) AND
        ld.rn >= (SELECT max(rn) FROM lm) - lr.rn
      )
)
-- locked targets/pids
, lcx AS (
  SELECT DISTINCT
    (lr.locktype, lr.database, lr.relation, lr.page, lr.tuple, lr.virtualxid, lr.transactionid::text::bigint, lr.classid, lr.objid, lr.objsubid) target
  , ld.pid  ldp
  , ld.mode ldm
  , lr.pid  lrp
  , lr.mode lrm
  FROM
    pg_locks ld
  JOIN
    pg_locks lr ON
      lr.pid <> ld.pid AND
      (lr.locktype, lr.database, lr.relation, lr.page, lr.tuple, lr.virtualxid, lr.transactionid::text::bigint, lr.classid, lr.objid, lr.objsubid) IS NOT DISTINCT FROM (ld.locktype, ld.database, ld.relation, ld.page, ld.tuple, ld.virtualxid, ld.transactionid::text::bigint, ld.classid, ld.objid, ld.objsubid) AND
      (lr.granted, ld.granted) = (TRUE, FALSE)
  JOIN
    lmx ON
      (lmx.lr, lmx.ld) = (lr.mode, ld.mode)
  WHERE
    (ld.pid, ld.granted) = ($1::integer, FALSE) -- PID
)
SELECT
  lc.locktype "type"
, CASE lc.locktype
    WHEN 'relation'      THEN ARRAY[relation::text]
    WHEN 'extend'        THEN ARRAY[relation::text]
    WHEN 'page'          THEN ARRAY[relation, page]::text[]
    WHEN 'tuple'         THEN ARRAY[relation, page, tuple]::text[]
    WHEN 'transactionid' THEN ARRAY[transactionid::text]
    WHEN 'virtualxid'    THEN regexp_split_to_array(virtualxid::text, '/')
    WHEN 'object'        THEN ARRAY[classid, objid, objsubid]::text[]
    WHEN 'userlock'      THEN ARRAY[classid::text]
    WHEN 'advisory'      THEN ARRAY[classid, objid, objsubid]::text[]
  END target
, lc.pid = lcx.ldp "locked"
, lc.pid
, regexp_replace(lc.mode, 'Lock$', '') "mode"
, lc.granted
, (lc.locktype, lc.database, lc.relation, lc.page, lc.tuple, lc.virtualxid, lc.transactionid::text::bigint, lc.classid, lc.objid, lc.objsubid) IS NOT DISTINCT FROM lcx.target "conflict"
FROM
  lcx
JOIN
  pg_locks lc ON
    lc.pid IN (lcx.ldp, lcx.lrp);

المشكلة رقم 1 - بداية بطيئة


كما ترى في المثال من السجل ، بجوار السطر الذي يشير إلى حدوث قفل ( LOG) ، هناك 3 خطوط أخرى ( DETAIL, CONTEXT, STATEMENT) تشير إلى معلومات موسعة.

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

للتخلص من هذه التأخيرات ، قمنا بالتبديل إلى معالجة دفق سجلات القفل. أي ، ننتقل الآن إلى قاعدة البيانات المستهدفة بمجرد فرز السجل الأول ، وأدركنا أنه القفل الموصوف هناك.

المشكلة رقم 2 - طلب بطيء


ولكن مع ذلك ، لم يكن لدى بعض الأقفال وقت للتحليل. بدأوا في التعمق - ووجدوا أنه في بعض الخوادم يتم تنفيذ طلب التحليل الخاص بنا لمدة 15-20 مللي ثانية !

اتضح أن السبب شائع - إجمالي عدد الأقفال النشطة على القاعدة ، والتي تصل إلى عدة آلاف:



pg_locks


هنا يجب ملاحظة أن الأقفال في PostgreSQL ليست "مخزنة" في أي مكان ، ولكن يتم تقديمها فقط في طريقة عرض النظام pg_locks ، وهو انعكاس مباشر للوظيفة pg_lock_status. وفي طلبنا نقرأ منه ثلاث مرات ! في هذه الأثناء ، نقرأ هذه الآلاف من حالات الأقفال ، ونتجاهل تلك التي لا تتعلق بـ PID الخاص بنا ، وتمكن القفل نفسه من "حل" ...

كان الحل هو "تجسيد" حالة pg_locks في CTE - بعد ذلك يمكنك تشغيله عدة مرات كما تشاء ، إنه بالفعل لم يتغير. علاوة على ذلك ، بعد الجلوس على الخوارزمية ، كان من الممكن تقليل كل شيء إلى اثنين فقط من عمليات المسح.

ديناميات مفرطة


وحتى لو نظرة فاحصة قليلا في الاستعلام أعلاه، يمكننا أن نرى أن CTE lmو lmxليس بأي حال من الأحوال مرتبطة البيانات، ولكن ببساطة حساب دائما نفس "ثابت" تعريف مصفوفة من الصراع بين وسائل منع . لذا دعنا نضعها كجودة VALUES.

المشكلة رقم 3 - الجيران


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

ونتيجة لذلك ، التقطنا PID الأول ، وذهبنا إلى القاعدة المستهدفة (ولكي لا نفرط في التحميل ، نحتفظ باتصال واحد فقط) ، وقمنا بالتحليل ، وعادوا بالنتيجة ، وأخذوا PID التالي ... لكن الحياة لا تستحق ذلك على القاعدة ، وقفل PID # 2 ذهب بالفعل!

في الواقع ، لماذا نحتاج إلى الذهاب إلى قاعدة البيانات لكل PID بشكل منفصل ، إذا ما زلنا نقوم بتصفية الأقفال من القائمة العامة في الطلب؟ دعنا نأخذ على الفور القائمة الكاملة للعمليات المتضاربة مباشرة من قاعدة البيانات ونوزع أقفالها عبر جميع معرّفات PID التي سقطت أثناء تنفيذ الطلب:

WITH lm(ld, lr) AS (
  VALUES
    ('AccessShareLock', '{AccessExclusiveLock}'::text[])
  , ('RowShareLock', '{ExclusiveLock,AccessExclusiveLock}'::text[])
  , ('RowExclusiveLock', '{ShareLock,ShareRowExclusiveLock,ExclusiveLock,AccessExclusiveLock}'::text[])
  , ('ShareUpdateExclusiveLock', '{ShareUpdateExclusiveLock,ShareLock,ShareRowExclusiveLock,ExclusiveLock,AccessExclusiveLock}'::text[])
  , ('ShareLock', '{RowExclusiveLock,ShareUpdateExclusiveLock,ShareRowExclusiveLock,ExclusiveLock,AccessExclusiveLock}'::text[])
  , ('ShareRowExclusiveLock', '{RowExclusiveLock,ShareUpdateExclusiveLock,ShareLock,ShareRowExclusiveLock,ExclusiveLock,AccessExclusiveLock}'::text[])
  , ('ExclusiveLock', '{RowShareLock,RowExclusiveLock,ShareUpdateExclusiveLock,ShareLock,ShareRowExclusiveLock,ExclusiveLock,AccessExclusiveLock}'::text[])
  , ('AccessExclusiveLock', '{AccessShareLock,RowShareLock,RowExclusiveLock,ShareUpdateExclusiveLock,ShareLock,ShareRowExclusiveLock,ExclusiveLock,AccessExclusiveLock}'::text[])
)
, locks AS (
  SELECT
    (
      locktype
    , database
    , relation
    , page
    , tuple
    , virtualxid
    , transactionid::text::bigint
    , classid
    , objid
    , objsubid
    ) target
  , *
  FROM
    pg_locks
)
, ld AS (
  SELECT
    *
  FROM
    locks
  WHERE
    NOT granted
)
, lr AS (
  SELECT
    *
  FROM
    locks
  WHERE
    target::text = ANY(ARRAY(
      SELECT DISTINCT
        target::text
      FROM
        ld
    )) AND
    granted
)
, lcx AS (
  SELECT DISTINCT
    lr.target
  , ld.pid ldp
  , ld.mode ldm
  , lr.pid lrp
  , lr.mode lrm
  FROM
    ld
  JOIN
    lr
      ON lr.pid <> ld.pid AND
        lr.target IS NOT DISTINCT FROM ld.target
)
SELECT
  lc.locktype "type"
, CASE lc.locktype
    WHEN 'relation' THEN
      ARRAY[relation::text]
    WHEN 'extend' THEN
      ARRAY[relation::text]
    WHEN 'page' THEN
      ARRAY[relation, page]::text[]
    WHEN 'tuple' THEN
      ARRAY[relation, page, tuple]::text[]
    WHEN 'transactionid' THEN
      ARRAY[transactionid::text]
    WHEN 'virtualxid' THEN
      regexp_split_to_array(virtualxid::text, '/')
    WHEN 'object' THEN
      ARRAY[classid, objid, objsubid]::text[]
    WHEN 'userlock' THEN
      ARRAY[classid::text]
    WHEN 'advisory' THEN
      ARRAY[classid, objid, objsubid]::text[]
  END target
, lc.pid = lcx.ldp as locked
, lc.pid
, regexp_replace(lc.mode, 'Lock$', '') "mode"
, lc.granted
, lc.target IS NOT DISTINCT FROM lcx.target as conflict
FROM
  lcx
JOIN
  locks lc
    ON lc.pid IN (lcx.ldp, lcx.lrp);

الآن لدينا الوقت لتحليل حتى الأقفال الموجودة فقط 1-2 مللي ثانية بعد الدخول إلى السجل.

حلل هذا


في الواقع ، لماذا حاولنا طويلا؟ وماذا يعطينا هذا نتيجة؟ ..

type     | target                  | locked | pid    | mode        | granted | conflict
---------------------------------------------------------------------------------------
relation | {225388639}             | f      | 216547 | AccessShare | t       | f
relation | {423576026}             | f      | 216547 | AccessShare | t       | f
advisory | {225386226,194303167,2} | t      |  24964 | Exclusive   | f       | t
relation | {341894815}             | t      |  24964 | AccessShare | t       | f
relation | {416441672}             | f      | 216547 | AccessShare | t       | f
relation | {225964322}             | f      | 216547 | AccessShare | t       | f
...

في هذه اللوحة ، تكون القيم targetذات النوع مفيدة لنا relation، لأن كل قيمة مثل OID لجدول أو فهرس . الآن لم يتبق سوى القليل جدًا - لمعرفة كيفية استجواب قاعدة بيانات OID التي لا تزال غير معروفة لنا ، من أجل ترجمتها إلى شكل يمكن قراءته بواسطة الإنسان:

SELECT $1::oid::regclass;

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



ولكن عن هذا - في مقال آخر.

"وحدها"


في هذه الأثناء ، سنجمع النسخة الكاملة من "المراقبة" ، والتي ستزيل تلقائيًا حالة الأقفال لأرشيفنا بمجرد ظهور شيء مريب في السجل:

ps uw -U postgres \
  | grep [l]ogger \
  | awk '{print "/proc/"$2"/fd"}' \
  | xargs ls -l \
  | grep `cd; psql -U postgres postgres -t -A -c "SELECT CASE WHEN substr(current_setting('log_directory'), 1, 1) = '/' THEN current_setting('log_directory') ELSE current_setting('data_directory') || '/' || current_setting('log_directory') END"` \
  | awk '{print $NF}' \
  | xargs -l -I{} tail -f {} \
  | stdbuf -o0 grep 'still waiting for' \
  | xargs -l -I{} date '+%Y%m%d-%H%M%S.%N' \
  | xargs -l -I{} psql -U postgres postgres -f detect-lock.sql -o {}-lock.log

وبجانب detect-lock.sqlوضع الطلب الأخير. تشغيل - واحصل على مجموعة من الملفات مع المحتوى المطلوب في الداخل:

# cat 20200526-181433.331839375-lock.log
     type      |     target     | locked |  pid   |     mode     | granted | conflict
---------------+----------------+--------+--------+--------------+---------+----------
 relation      | {279588430}    | t      | 136325 | RowExclusive | t       | f
 relation      | {279588422}    | t      | 136325 | RowExclusive | t       | f
 relation      | {17157}        | t      | 136325 | RowExclusive | t       | f
 virtualxid    | {110,12171420} | t      | 136325 | Exclusive    | t       | f
 relation      | {279588430}    | f      |  39339 | RowExclusive | t       | f
 relation      | {279588422}    | f      |  39339 | RowExclusive | t       | f
 relation      | {17157}        | f      |  39339 | RowExclusive | t       | f
 virtualxid    | {360,11744113} | f      |  39339 | Exclusive    | t       | f
 virtualxid    | {638,4806358}  | t      |  80553 | Exclusive    | t       | f
 advisory      | {1,1,2}        | t      |  80553 | Exclusive    | f       | t
 advisory      | {1,1,2}        | f      |  80258 | Exclusive    | t       | t
 transactionid | {3852607686}   | t      | 136325 | Exclusive    | t       | f
 extend        | {279588422}    | t      | 136325 | Exclusive    | f       | t
 extend        | {279588422}    | f      |  39339 | Exclusive    | t       | t
 transactionid | {3852607712}   | f      |  39339 | Exclusive    | t       | f
(15 rows)

حسنًا ، إذن - اجلس وحلل ...

All Articles