وفر الكثير من المال بكميات كبيرة في PostgreSQL

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

يتعلق الأمر بإعدادات TOAST ومحاذاة البيانات . "في المتوسط" ، لن توفر هذه الطرق الكثير من الموارد ، ولكن دون أي تعديل على رمز التطبيق.


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

الأسرار الصغيرة للبيانات الضخمة


وفقًا لملف تعريف خدمتنا ، يتلقى بانتظام حزمًا نصية من السجلات .

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

دعونا نلقي نظرة على هيكل أحد الجداول التي نكتب فيها البيانات "الأولية" - وهذا هو النص الأصلي من إدخال السجل:

CREATE TABLE rawdata_orig(
  pack -- PK
    uuid NOT NULL
, recno -- PK
    smallint NOT NULL
, dt --  
    date
, data --  
    text
, PRIMARY KEY(pack, recno)
);

مثل هذه اللوحة النموذجية (مقسمة بالفعل ، بالطبع ، فهي قالب قسم) ، حيث يكون النص هو الأهم. في بعض الأحيان ضخمة للغاية.

تذكر أن الحجم "المادي" لسجل واحد في PG لا يمكن أن يشغل أكثر من صفحة واحدة من البيانات ، ولكن الحجم "المنطقي" يختلف تمامًا. لكتابة قيمة حجم (varchar / text / bytea) في الحقل ، يتم استخدام تقنية TOAST :
يستخدم PostgreSQL حجم صفحة ثابت (عادة 8 كيلوبايت) ، ولا يسمح للصفوف بالامتداد عدة صفحات. لذلك ، من المستحيل تخزين قيم الحقول الكبيرة جدًا بشكل مباشر. للتغلب على هذا القيد ، يتم ضغط قيم الحقول الكبيرة و / أو تقسيمها إلى عدة خطوط مادية. يحدث هذا دون أن يلاحظه المستخدم ويؤثر بشكل طفيف على معظم كود الخادم. تُعرف هذه الطريقة باسم توست ...

في الواقع ، بالنسبة لكل جدول يحتوي على حقول "يحتمل أن تكون كبيرة" ، يتم إنشاء جدول مزدوج تلقائيًا مع "تشريح" كل سجل "كبير" في مقاطع 2 كيلوبايت:

TOAST(
  chunk_id
    integer
, chunk_seq
    integer
, chunk_data
    bytea
, PRIMARY KEY(chunk_id, chunk_seq)
);

أي إذا كان علينا كتابة صف بقيمة "كبيرة" data، فإن السجل الحقيقي لن يحدث فقط في الجدول الرئيسي و PK الخاص به ، ولكن أيضًا في TOAST و PK الخاص به .

تقليل تأثير التوست


لكن معظم السجلات هنا لا تزال غير كبيرة ، يجب أن تتناسب مع 8 كيلو بايت - كيف يمكنك حفظ هذا؟ ..

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

ALTER TABLE rawdata_orig ALTER COLUMN data SET STORAGE MAIN;

كيفية تقييم التأثير


نظرًا لأن تدفق البيانات يتغير كل يوم ، لا يمكننا مقارنة الأرقام المطلقة ، ولكن من الناحية النسبية ، كلما كانت النسبة التي سجلناها في TOAST أصغر ، كان ذلك أفضل. ولكن هناك خطر - فكلما زاد حجمنا "الفعلي" لكل سجل فردي ، أصبح الفهرس "أوسع" ، لأنه يجب علينا تغطية المزيد من صفحات البيانات.

القسم قبل التغييرات :
heap  = 37GB (39%)
TOAST = 54GB (57%)
PK    =  4GB ( 4%)

القسم بعد التغييرات :
heap  = 37GB (67%)
TOAST = 16GB (29%)
PK    =  2GB ( 4%)

في الواقع ، بدأنا في الكتابة في TOAST مرتين أقل ، مما أدى إلى تفريغ القرص ليس فقط ، ولكن أيضًا وحدة المعالجة المركزية:



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

الذين يعيشون على PostgreSQL 11 يعيشون بشكل جيد


بعد الترقية إلى PG11 ، قررنا الاستمرار في "توليف" TOAST ولاحظنا أنه بدءًا من هذا الإصدار ، أصبحت المعلمة متاحة للتهيئة toast_tuple_target:
يتم تشغيل كود معالجة TOAST فقط عندما تكون قيمة الصف التي سيتم تخزينها في الجدول أكبر من TOAST_TUPLE_THRESHOLD بايت (عادة 2 كيلوبايت). سيضغط رمز TOAST و / أو ينقل قيم الحقول خارج الجدول حتى تكون قيمة الصف أقل من TOAST_TUPLE_TARGET بايت (متغيرة ، عادةً 2 كيلوبايت أيضًا) أو يصبح من المستحيل تقليل الحجم.
قررنا أن البيانات التي لدينا عادة ما تكون إما "قصيرة جدًا" أو "طويلة جدًا" على الفور ، لذلك قررنا أن نقتصر على أدنى قيمة ممكنة:

ALTER TABLE rawplan_orig SET (toast_tuple_target = 128);

دعونا نرى كيف أثرت الإعدادات الجديدة على تحميل القرص بعد الترحيل:


ليس سيئا! تم تقليل متوسط قائمة الانتظار لقرص ما يقرب من 1.5 مرة ، و "شغل" القرص - بنسبة 20 في المئة! ولكن ربما هذا أثر بطريقة أو بأخرى على وحدة المعالجة المركزية؟


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

من تغيير الموقف ، المجموع ... يتغير!


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

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

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

لنبدأ بالنماذج الاصطناعية:

SELECT pg_column_size(ROW(
  '0000-0000-0000-0000-0000-0000-0000-0000'::uuid
, 0::smallint
, '2019-01-01'::date
));
-- 48 

SELECT pg_column_size(ROW(
  '2019-01-01'::date
, '0000-0000-0000-0000-0000-0000-0000-0000'::uuid
, 0::smallint
));
-- 46 

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

من الناحية النظرية ، كل شيء على ما يرام ويمكنك إعادة ترتيب الحقول كما تريد. دعنا نتحقق من البيانات الحقيقية على مثال أحد الجداول ، حيث يستغرق القسم اليومي منها 10-15 جيجابايت.

هيكل المصدر:

CREATE TABLE public.plan_20190220
(
--  from table plan:  pack uuid NOT NULL,
--  from table plan:  recno smallint NOT NULL,
--  from table plan:  host uuid,
--  from table plan:  ts timestamp with time zone,
--  from table plan:  exectime numeric(32,3),
--  from table plan:  duration numeric(32,3),
--  from table plan:  bufint bigint,
--  from table plan:  bufmem bigint,
--  from table plan:  bufdsk bigint,
--  from table plan:  apn uuid,
--  from table plan:  ptr uuid,
--  from table plan:  dt date,
  CONSTRAINT plan_20190220_pkey PRIMARY KEY (pack, recno),
  CONSTRAINT chck_ptr CHECK (ptr IS NOT NULL),
  CONSTRAINT plan_20190220_dt_check CHECK (dt = '2019-02-20'::date)
)
INHERITS (public.plan)

القسم بعد تغيير ترتيب الأعمدة هو نفس الحقل بالضبط ، يختلف الترتيب فقط :

CREATE TABLE public.plan_20190221
(
--  from table plan:  dt date NOT NULL,
--  from table plan:  ts timestamp with time zone,
--  from table plan:  pack uuid NOT NULL,
--  from table plan:  recno smallint NOT NULL,
--  from table plan:  host uuid,
--  from table plan:  apn uuid,
--  from table plan:  ptr uuid,
--  from table plan:  bufint bigint,
--  from table plan:  bufmem bigint,
--  from table plan:  bufdsk bigint,
--  from table plan:  exectime numeric(32,3),
--  from table plan:  duration numeric(32,3),
  CONSTRAINT plan_20190221_pkey PRIMARY KEY (pack, recno),
  CONSTRAINT chck_ptr CHECK (ptr IS NOT NULL),
  CONSTRAINT plan_20190221_dt_check CHECK (dt = '2019-02-21'::date)
)
INHERITS (public.plan)

يتم تحديد الحجم الإجمالي للقسم من خلال عدد "الحقائق" ويعتمد فقط على العمليات الخارجية ، لذلك نقسم حجم الكومة ( pg_relation_size) على عدد السجلات فيه - أي نحصل على متوسط ​​حجم السجل المخزن الفعلي :


ناقص 6٪ من الحجم ممتاز!

لكن كل شيء ، بالطبع ، ليس وردياً - لأنه في المؤشرات لا يمكننا تغيير ترتيب الحقول ، وبالتالي "بشكل عام" ( pg_total_relation_size) ...


... بعد كل شيء ، وفروا 1.5 ٪ هنا ، دون تغيير سطر واحد من التعليمات البرمجية. نعم نعم!



ألاحظ أن الترتيب أعلاه للحقول ليس حقيقة أن الأمثل. لأن بعض الكتل الميدانية لا تريد "تمزيقها" بالفعل لأسباب جمالية - على سبيل المثال ، زوج (pack, recno)، وهو PK لهذا الجدول.

بشكل عام ، يعد تعريف الترتيب الميداني "الأدنى" مهمة "شاملة" بسيطة إلى حد ما. لذلك ، يمكنك الحصول على نتائج أفضل لبياناتك - جرّبها!

All Articles