etcd مقابل PostgreSQL

Jinhua Luo

March 17, 2023

Technology

الخلفية التاريخية

PostgreSQL

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

يوفر PostgreSQL دعمًا قويًا لكل من معالجة التحليل عبر الإنترنت (OLAP) ومعالجة المعاملات عبر الإنترنت (OLTP)، ويتمتع بقدرات استعلام SQL قوية ومجموعة واسعة من الامتدادات التي تسمح له بتلبية جميع الاحتياجات التجارية تقريبًا. نتيجة لذلك، حظي باهتمام متزايد في السنوات الأخيرة. في الواقع، تتيح قابلية التوسع والأداء العالي لـ PostgreSQL تكرار وظائف أي نوع آخر من قواعد البيانات تقريبًا.

هندسة PostgreSQL مصدر الصورة (بموجب اتفاقية CC 3.0 BY-SA): https://en.wikibooks.org/wiki/PostgreSQL/Architecture

etcd

كيف نشأت etcd، وما هي المشكلة التي تحلها؟

في عام 2013، طور فريق Startup CoreOS منتجًا يسمى Container Linux. إنه نظام تشغيل مفتوح المصدر وخفيف الوزن يعطي الأولوية لأتمتة ونشر خدمات التطبيقات بسرعة. يتطلب Container Linux تشغيل التطبيقات في حاويات ويوفر حلًا لإدارة المجموعات، مما يجعل من السهل على المستخدمين إدارة الخدمات كما لو كانت على جهاز واحد.

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

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

تحتاج خدمة التنسيق بشكل مثالي إلى تحقيق الأهداف الخمسة التالية:

  1. توفر عالي مع نسخ متعددة من البيانات
  2. اتساق البيانات مع التحقق من الإصدار بين النسخ
  3. سعة تخزين صغيرة: يجب أن تخزن خدمة التنسيق فقط معلومات تكوين البيانات الوصفية الحرجة للخدمات والعقد التي تنتمي إلى تكوين مستوى التحكم، بدلاً من البيانات المتعلقة بالمستخدم. هذا النهج يقلل من الحاجة إلى تجزئة البيانات للتخزين ويتجنب التصميم المفرط.
  4. وظائف CRUD (إنشاء، قراءة، تحديث، وحذف)، بالإضافة إلى آلية للاستماع إلى تغييرات البيانات. يجب أن تخزن معلومات حالة الخدمات، وعند حدوث تغييرات أو شذوذ في الخدمات، يجب أن تدفع حدث التغيير بسرعة إلى مستوى التحكم. هذا يساعد في تحسين توفر الخدمة ويقلل من الحمل الزائد غير الضروري على أداء خدمة التنسيق.
  5. سهولة التشغيل: يجب أن تكون خدمة التنسيق سهلة التشغيل والصيانة واستكشاف الأخطاء وإصلاحها. واجهة سهلة الاستخدام يمكن أن تقلل من خطر الأخطاء، وتخفض تكاليف الصيانة، وتقلل من وقت التوقف.

من منظور نظرية CAP، تنتمي etcd إلى نظام CP (الاتساق والتسامح مع التجزئة). هندسة etcd

كمكون مركزي في مجموعة Kubernetes، يستخدم kube-apiserver etcd كتخزين أساسي.

من ناحية، يتم استخدام etcd للاستمرارية في إنشاء كائنات الموارد في مجموعة k8s. ومن ناحية أخرى، فإن آلية مراقبة البيانات في etcd هي التي تقود عمل Informer في المجموعة بأكملها، مما يتيح التنسيق المستمر للحاويات.

لذلك، من الناحية الفنية، الأسباب الأساسية لاستخدام Kubernetes لـ etcd هي:

  • تمت كتابة etcd بلغة Go، وهي متوافقة مع مكدس تقنية k8s، وتستهلك موارد قليلة، وسهلة النشر للغاية.
  • توفر etcd اتساقًا قويًا، ومراقبة، وعقود إيجار، وغيرها من الميزات التي تعتبر اعتمادات أساسية لـ k8s.

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

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

نموذج البيانات

لدى قواعد البيانات المختلفة نماذج بيانات مختلفة تقدمها للمستخدمين، وهذا العامل يحدد مدى ملاءمة قاعدة البيانات للسيناريوهات المختلفة.

المفاتيح والقيم مقابل SQL

نموذج بيانات المفاتيح والقيم هو نموذج شائع في NoSQL، والذي تتبناه etcd أيضًا. كيف يقارن هذا النموذج بـ SQL وما هي ميزاته؟

أولاً، دعونا نلقي نظرة على SQL.

تحتفظ قواعد البيانات العلائقية بالبيانات في جداول وتوفر طريقة فعالة وبديهية ومرنة لتخزين المعلومات المنظمة والوصول إليها.

الجدول، المعروف أيضًا باسم العلاقة، يتكون من أعمدة تحتوي على فئة واحدة أو أكثر من البيانات، وصفوف، المعروفة أيضًا باسم سجلات الجدول، التي تتضمن مجموعة من البيانات التي تحدد الفئات. تسترد التطبيقات البيانات باستخدام الاستعلامات التي تستخدم عمليات مثل "project" لتحديد السمات، و"select" لتحديد الصفوف، و"join" لدمج العلاقات. تم تطوير النموذج العلائقي لإدارة قواعد البيانات في عام 1970 بواسطة إدجار كود، عالم الكمبيوتر في IBM.

قاعدة بيانات علائقية

مصدر الصورة (بموجب اتفاقية CC 3.0 BY-SA): https://en.wikipedia.org/wiki/Associative_entity

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

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

قام PostgreSQL بتوسيع SQL مع العديد من الامتدادات، مما جعله لغة كاملة تورينغ. هذا يعني أن SQL يمكنه تنفيذ أي عملية معقدة، مما يسهل تنفيذ منطق معالجة البيانات بالكامل على جانب الخادم.

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

بشكل عام، يبسط هيكل المفاتيح والقيم في etcd SQL وهو أكثر ملاءمة وبديهية للمهمة المحددة لإدارة التكوين.

MVCC (التحكم في التزامن متعدد الإصدارات)

MVCC هي ميزة أساسية لإصدار البيانات في إدارة التكوين. تتيح:

  • استعلام البيانات التاريخية
  • تحديد عمر البيانات من خلال مقارنة الإصدارات
  • مراقبة البيانات، والتي تتطلب الإصدار لتمكين الإشعارات التزايدية

كل من etcd وPostgreSQL لديهما MVCC، ولكن ما هي الاختلافات بينهما؟

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

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

يختلف تنفيذ MVCC في PostgreSQL عن etcd في أنه لا يركز على توفير أرقام إصدار متزايدة، ولكن على تنفيذ المعاملات ومستويات العزل المختلفة بشكل شفاف للمستخدم. MVCC هي آلية قفل متفائلة تمكن من التحديثات المتزامنة. يحتوي كل صف في الجدول على سجل معرف المعاملة، مع xmin يمثل معرف المعاملة لإنشاء الصف وxmax يمثل معرف المعاملة لتحديث الصف.

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

لعرض مثال، يرجى الرجوع إلى الرابط التالي: https://devcenter.heroku.com/articles/postgresql-concurrency

لسوء الحظ، لا يمكن استخدام معرفات المعاملات للتحكم في إصدار بيانات التكوين في PostgreSQL لعدة أسباب:

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

نتيجة لذلك، يتطلب PostgreSQL طرقًا بديلة للتحكم في إصدار بيانات التكوين حيث لا يوجد دعم مدمج.

واجهة العميل

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

لقد أثبتت واجهات برمجة التطبيقات kv/watch/lease الخاصة بـ etcd أنها بارعة بشكل خاص في إدارة التكوينات. ومع ذلك، كيف يمكن تنفيذ هذه الواجهات في PostgreSQL؟

لسوء الحظ، لا يوفر PostgreSQL دعمًا مدمجًا لهذه الواجهات، ويجب التغليف لتنفيذها. لتحليل تنفيذها، سنفحص مشروع pg_watch_demo الذي طورته بنفسي: pg_watch_demo.

gRPC/HTTP مقابل TCP

يتبع PostgreSQL بنية متعددة العمليات، حيث يتعامل كل عملية مع اتصال TCP واحد فقط في كل مرة. يستخدم بروتوكولًا مخصصًا لتقديم الوظائف عبر استعلامات SQL ويتبع نموذج تفاعل طلب-استجابة (مشابه لـ HTTP/1.1، الذي يتعامل مع طلب واحد فقط في كل مرة ويتطلب التوصيل لمعالجة طلبات متعددة في وقت واحد). ومع ذلك، نظرًا لاستهلاك الموارد العالي والكفاءة النسبية المنخفضة، فإن وكيل تجمع الاتصالات (مثل pgbouncer) ضروري لتحسين الأداء، خاصة في السيناريوهات ذات QPS العالية.

من ناحية أخرى، تم تصميم etcd على بنية متعددة الروتينات في Golang ويوفر واجهتين سهلتي الاستخدام: gRPC وRESTful. هذه الواجهات سهلة التكامل وفعالة من حيث استهلاك الموارد. بالإضافة إلى ذلك، يمكن لكل اتصال gRPC التعامل مع استعلامات متزامنة متعددة، مما يضمن الأداء الأمثل.

تعريف البيانات

etcd

message KeyValue {
  bytes key = 1;
  // رقم الإصدار عند إنشاء المفتاح
  int64 create_revision = 2;
  // رقم الإصدار عند آخر تعديل للمفتاح
  int64 mod_revision = 3;
  // عداد متزايد يزيد كل مرة يتم فيها تحديث المفتاح.
  // يتم إعادة تعيين هذا العداد إلى الصفر عند حذف المفتاح، ويستخدم كعلامة قبر.
  int64 version = 4;
  bytes value = 5;
  // كائن الإيجار المستخدم من قبل المفتاح لـ TTL. إذا كانت القيمة 0، فلا يوجد TTL.
  int64 lease = 6;
}

PostgreSQL

يحتاج PostgreSQL إلى استخدام جدول لمحاكاة مساحة البيانات العالمية لـ etcd:

CREATE TABLE IF NOT EXISTS config (
  key text,
  value text,
  -- يعادل `create_revision` و `mod_revision`
  -- هنا، يتم استخدام نوع تسلسل متزايد كبير لمحاكاة الإصدار
  revision bigserial,
  -- علامة قبر
  tombstone boolean NOT NULL DEFAULT false,
  -- فهرس مركب، البحث بالمفتاح أولاً، ثم بالإصدار
  primary key(key, revision)
);

get

etcd

واجهة برمجة التطبيقات get الخاصة بـ etcd لديها مجموعة واسعة من المعلمات:

  • استعلامات النطاق، على سبيل المثال، تعيين key كـ /abc و range_end كـ /abd سوف يسترد جميع أزواج المفاتيح والقيم مع /abc كبادئة.
  • استعلامات تاريخية، تحديد revision أو نطاق mod_revision.
  • فرز وتحديد عدد النتائج المرجعة.
message RangeRequest {
  ...
  bytes key = 1;
  // استعلامات النطاق
  bytes range_end = 2;
  int64 limit = 3;
  // استعلامات تاريخية
  int64 revision = 4;
  // الفرز
  SortOrder sort_order = 5;
  SortTarget sort_target = 6;
  bool serializable = 7;
  bool keys_only = 8;
  bool count_only = 9;
  // استعلامات تاريخية
  int64 min_mod_revision = 10;
  int64 max_mod_revision = 11;
  int64 min_create_revision = 12;
  int64 max_create_revision = 13;
}

PostgreSQL

يمكن لـ PostgreSQL تنفيذ وظيفة get الخاصة بـ etcd عبر SQL، وحتى تقديم وظائف أكثر تعقيدًا. نظرًا لأن SQL نفسه لغة وليس واجهة معلمات ثابتة، فهو متعدد الاستخدامات للغاية. هنا نعرض مثالًا بسيطًا لاسترداد أحدث زوج مفتاح-قيمة. نظرًا لأن المفتاح الأساسي هو فهرس مركب، يمكن البحث بسرعة بالنطاق، مما يؤدي إلى استرجاع عالي السرعة.

CREATE FUNCTION get1(kk text)
RETURNS table(r bigint, k text, v text, c bigint) AS $$
    SELECT revision, key, value, create_time
    FROM config
    where key = kk and tombstone = false
    ORDER BY key, revision desc
    limit 1
$$ LANGUAGE sql;

put

etcd

message PutRequest {
  bytes key = 1;
  bytes value = 2;
  int64 lease = 3;
  // ما إذا كان يجب الرد ببيانات زوج المفتاح-القيمة قبل التحديث من طلب `Put` هذا.
  bool prev_kv = 4;
  bool ignore_value = 5;
  bool ignore_lease = 6;
}

PostgreSQL

تمامًا كما في etcd، لا يقوم PostgreSQL بتنفيذ التغييرات في المكان. بدلاً من ذلك، يتم إدخال صف جديد، ويتم تعيين إصدار جديد له.

CREATE FUNCTION set(k text, v text) RETURNS bigint AS $$
  insert into config(key, value) values(k, v) returning revision;
$$ LANGUAGE SQL;

delete

etcd

message DeleteRangeRequest {
  bytes key = 1;
  bytes range_end = 2;
  bool prev_kv = 3;
}

PostgreSQL

مشابه لـ etcd، لا يقوم الحذف في PostgreSQL بتعديل البيانات في المكان. بدلاً من ذلك، يتم إدخال صف جديد مع تعيين حقل tombstone إلى true للإشارة إلى أنه علامة قبر.

CREATE FUNCTION del(k text) RETURNS bigint AS $$
  insert into config(key, tombstone) values(k, true) returning revision;
$$ LANGUAGE SQL;

watch

etcd

message WatchCreateRequest {
  bytes key = 1;
  // تحديد نطاق المفاتيح للمراقبة
  bytes range_end = 2;
  // الإصدار الأولي للمراقبة
  int64 start_revision = 3;
  ...
}

message WatchResponse {
  ResponseHeader header = 1;
  ...
  // للكفاءة، يمكن إرجاع أحداث متعددة
  repeated mvccpb.Event events = 11;
}

PostgreSQL

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

-- دالة المحفز لتوزيع أحداث put/delete
CREATE FUNCTION notify_config_change() RETURNS TRIGGER AS $$
DECLARE
  data json;
  channel text;
  is_channel_exist boolean;
BEGIN
  IF (TG_OP = 'INSERT') THEN
    -- استخدام JSON للتشفير
    data = row_to_json(NEW);
    -- استخراج اسم القناة للتوزيع من المفتاح
    channel = (SELECT SUBSTRING(NEW.key, '/(.*)/'));
    -- إذا كان التطبيق يراقب القناة، قم بإرسال حدث من خلالها
    is_channel_exist = NOT pg_try_advisory_lock(9080);
    IF is_channel_exist THEN
        PERFORM pg_notify(channel, data::text);
    ELSE
        PERFORM pg_advisory_unlock(9080);
    END IF;
  END IF;
  RETURN NULL; -- يتم تجاهل النتيجة لأن هذا محفز AFTER
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER notify_config_change
AFTER INSERT ON config
FOR EACH ROW EXECUTE FUNCTION notify_config_change();

نظرًا لأن وظيفة watch مغلفة، يجب على تطبيقات العميل أيضًا تنفيذ المنطق المقابل. باستخدام Golang كمثال، يجب اتخاذ الخطوات التالية:

  1. بدء الاستماع: عند بدء الاستماع، سيتم تخزين جميع بيانات notify مؤقتًا على مستوى PostgreSQL وقناة Golang.
  2. استرداد جميع البيانات باستخدام get_all(key_prefix, revision): تقوم هذه الوظيفة بقراءة جميع البيانات الموجودة بدءًا من الإصدار المحدد. لكل مفتاح، سيتم إرجاع بيانات أحدث إصدار فقط، مع إزالة أي بيانات محذوفة تلقائيًا. إذا لم يتم تحديد الإصدار، فإنه يعيد أحدث البيانات لجميع المفاتيح مع key_prefix المحدد.
  3. مراقبة البيانات الجديدة، بما في ذلك أي إشعارات قد تكون مخزنة بين الخطوة الأولى والثانية، لتجنب فقدان أي بيانات جديدة قد تحدث خلال هذه النافذة الزمنية. تجاهل أي إصدارات تمت قراءتها بالفعل في الخطوة الثانية.
func watch(l *pq.Listener) {
    for {
        select {
        case n := <-l.Notify:
            if n == nil {
                log.Println("listener reconnected")
                log.Printf("get all routes from rev %d including tombstones...\n", latestRev)
   // عند إعادة الاتصال، استئناف الإرسال بناءً على الإصدار قبل الانفصال.
                str := fmt.Sprintf(`select * from get_all_from_rev_with_stale('/routes/', %d)`, latestRev)
                rows, err := db.Query(str)
                ...
                continue
            }
            ...
            // الحفاظ على حالة تسجل أحدث إصدار تم استلامه
            updateRoute(cfg)
        case <-time.After(15 * time.Second):
            log.Println("Received no events for 15 seconds, checking connection")
            go func() {
                // إذا لم يتم استلام أي أحداث لفترة طويلة، تحقق من صحة الاتصال
                if err := l.Ping(); err != nil {
                    log.Println("listener ping error: ", err)
                }
            }()
        }
    }
}

log.Println("get all routes...")
// عند التهيئة، يجب على التطبيق الحصول على جميع أزواج المفاتيح والقيم الحالية ثم مراقبة التحديثات بشكل تزايدي من خلال watch
rows, err := db.Query(`select * from get_all('/routes/')`)
...
go watch(listener)

transaction

etcd

معاملات etcd هي مجموعة من العمليات المتعددة مع فحوصات شرطية، ويتم تأكيد التعديلات التي تم إجراؤها بواسطة المعاملة بشكل ذري.

message TxnRequest {
  // تحديد شرط تنفيذ المعاملة
  repeated Compare compare = 1;
  // العمليات التي سيتم تنفيذها إذا تم استيفاء الشرط
  repeated RequestOp success = 2;
  // العمليات التي سيتم تنفيذها إذا لم يتم استيفاء الشرط
  repeated RequestOp failure = 3;
}

PostgreSQL

يسمح الأمر DO في PostgreSQL بتنفيذ أي أمر، بما في ذلك الإجراءات المخزنة. يدعم لغات متعددة، بما في ذلك اللغات المدمجة مثل PL/pgSQL وPython. باستخدام هذه اللغات، يمكن تنفيذ أي أحكام شرطية، وحلقات، وغيرها من منطق التحكم، مما يجعلها أكثر تنوعًا من etcd.

DO LANGUAGE plpgsql $$
DECLARE
     n_plugins int;
BEGIN
    SELECT COUNT(1) INTO n_plugins FROM get_all('/plugins/');
    IF n_plugins = 0 THEN
       perform set('/routes/1', 'foobar');
       perform set('/upstream/1', 'foobar');
       ...
    ELSE
       ...
    END IF;
END;
$$;

lease

etcd

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

message LeaseGrantRequest {
  // TTL للإيجار
  int64 TTL = 1;
  int64 ID = 2;
}

// تجديد الإيجار
message LeaseKeepAliveRequest {
  int64 ID = 1;
}

message PutRequest {
  bytes key = 1;
  bytes value = 2;
  // معرف الإيجار، يستخدم لتنفيذ TTL
  int64 lease = 3;
  ...
}

PostgreSQL

  • في PostgreSQL، يمكن الحفاظ على الإيجار من خلال مفتاح أجنبي. عند الاستعلام، إذا كان هناك كائن إيجار مرتبط قد انتهت صلاحيته، فإنه يعتبر علامة قبر.
  • تقوم طلبات Keepalive بتحديث الطابع الزمني last_keepalive في جدول الإيجار.
CREATE TABLE IF NOT EXISTS config (
  key text,
  value text,
  ...
  -- استخدام مفتاح أجنبي لتحديد كائن الإيجار المرتبط.
  lease int64 references lease(id),
);

CREATE TABLE IF NOT EXISTS lease (
  id text,
  ttl int,
  last_keepalive timestamp;
);

مقارنة الأداء

يحتاج PostgreSQL إلى محاكاة واجهات برمجة التطبيقات المختلفة لـ etcd من خلال التغليف. إذن كيف هو أداؤه؟ هنا نتائج اختبار بسيط:https://github.com/kingluo/pg_watch_demo#benchmark.

etcd_vs_postgres

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

ومع ذلك، لدى PostgreSQL بعض العيوب التي تستحق الذكر:

  • سجل WAL لكل تحديث أكبر، مما يؤدي إلى ضعف كمية I/O القرص مقارنة بـ etcd.
  • يستهلك المزيد من وحدة المعالجة المركزية مقارنة بـ etcd.
  • الإشعارات بناءً على القنوات هي مفهوم على مستوى المعاملة. عند تحديث نفس النوع من الموارد، يتم إرسال التحديث إلى نفس القناة، وتتنافس طلبات التحديث على أقفال الاستبعاد المتبادل، مما يؤدي إلى تسلسل الطلبات. بمعنى آخر، استخدام القنوات لتنفيذ watch سيؤثر على التوازي لعمليات put.

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

التخزين

يتم تحديد الأداء من خلال التخزين الأساسي، وكيفية تخزين البيانات يحدد متطلبات قاعدة البيانات من الذاكرة والقرص والموارد الأخرى.

etcd

مخطط هندسة تخزين etcd:

تخزين etcd

يكتب etcd التحديثات أولاً في سجل الكتابة المسبق (WAL) ويقوم بمسحها إلى القرص لضمان عدم فقدان التحديثات. بمجرد كتابة السجل بنجاح وتأكيده من قبل غالبية العقد، يمكن إرجاع النتائج إلى العميل. يقوم etcd أيضًا بتحديث TreeIndex وBoltDB بشكل غير متزامن.

لتجنب نمو السجل إلى ما لا نهاية، يقوم etcd بشكل دوري بأخذ لقطة للتخزين، ويمكن حذف السجلات السابقة للقطة.

يقوم etcd بفهرسة جميع المفاتيح في الذاكرة (TreeIndex)، وتسجيل معلومات الإصدار لكل مفتاح، ولكنه يحتفظ فقط بمؤشر إلى BoltDB (revision) للقيمة.

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

يستخدم كل من TreeIndex وBoltDB بنية بيانات btree، المعروفة بكفاءتها في البحث والبحث النطاقي.

مخطط بنية TreeIndex:

treeindex etcd

(مصدر الصورة: https://blog.csdn.net/H_L_S/article/details/112691481، مرخص بموجب CC 4.0 BY-SA)

يتم تقسيم كل مفتاح إلى أجيال مختلفة، مع كل حذف يمثل نهاية جيل.

يتكون المؤشر إلى القيمة من عددين صحيحين. العدد الصحيح الأول main هو معرف المعاملة لـ etcd، بينما العدد الصحيح الثاني sub يمثل معرف التحديث لهذا المفتاح داخل تلك المعاملة.

يدعم Boltdb المعاملات واللقطات، ويخزن القيمة المقابلة للإصدار.

boltdb etcd

(مصدر الصورة: https://blog.csdn.net/H_L_S/article/details/112691481، مرخص بموجب CC 4.0 BY-SA)

مثال على كتابة البيانات:

كتابة key="key1", revision=(12,1), value="keyvalue5". لاحظ التغييرات في الأجزاء الحمراء من treeIndex وBoltDB:

put etcd

(مصدر الصورة: https://blog.csdn.net/H_L_S/article/details/112691481، مرخص بموجب CC 4.0 BY-SA)

حذف key="key", revision=(13,1) ينشئ جيلًا جديدًا فارغًا في treeIndex وينشئ قيمة فارغة في BoltDB مع key="13_1t".

هنا، t تعني "علامة قبر". هذا يعني أنه لا يمكنك قراءة علامة القبر لأن المؤشر في treeIndex هو (13,1)، ولكن في BoltDB، هو 13_1t، والذي لا يمكن مطابقته.

delete etcd

(مصدر الصورة: https://blog.csdn.net/H_L_S/article/details/112691481، مرخص بموجب CC 4.0 BY-SA)

من الجدير بالذكر أن etcd تقوم بجدولة كل من القراءات والكتابات إلى BoltDB باستخدام روتين واحد لتقليل I/O القرص العشوائي وتحسين أداء I/O.

PostgreSQL

مخطط هندسة تخزين PostgreSQL:

تخزين PostgreSQL

مشابه لـ etcd، يقوم PostgreSQL بإلحاق التحديثات بملف سجل أولاً، وينتظر حتى يتم مسح السجل بنجاح إلى القرص قبل اعتبار المعاملة مكتملة. في الوقت نفسه، يتم كتابة التحديثات إلى ذاكرة shared_buffer.

shared_buffer هي منطقة ذاكرة مشتركة بين جميع الجداول والف

Tags: