
السلوك غير المتوقع للتطبيق فيما يتعلق بالعمل مع قاعدة البيانات يؤدي إلى حرب بين DBA والمطورين: DBA يصرخ: "التطبيق الخاص بك يسقط قاعدة البيانات" ، والمطورين - "لكن كل شيء يعمل من قبل!" والأسوأ من ذلك كله ، أن DBA والمطورين لا يستطيعون مساعدة بعضهم البعض: البعض لا يعرفون عن الفروق الدقيقة في التطبيق وبرنامج التشغيل ، والبعض الآخر لا يعرفون الميزات المتعلقة بالبنية التحتية. سيكون من الرائع تجنب مثل هذا الموقف.
عليك أن تفهم ، فغالبًا لا يكفي البحث من خلال go-database-sql.org . من الأفضل تسليح نفسك بتجربة الآخرين. حتى أفضل إذا كانت تجربة اكتسبها الدم وخسر المال.
اسمي Ryabinkov Artemy وهذه المقالة هي ترجمة مجانية لتقريري من مؤتمر Saints HighLoad 2019 .
الأدوات
يمكنك العثور على الحد الأدنى من المعلومات الضرورية حول كيفية العمل مع Go مع أي قاعدة بيانات تشبه SQL في go-database-sql.org . إذا لم تكن قد قرأته ، فاقرأه.
sqlx
في رأيي ، قوة الذهاب هي البساطة. ويتم التعبير عن ذلك ، على سبيل المثال ، لأنه من المعتاد بالنسبة لـ Go كتابة استعلامات في SQL عارية (ORM ليس تكريماً). هذه ميزة ومصدر لصعوبات إضافية.
لذلك ، مع أخذ حزمة لغة database/sql
القياسية ، ستحتاج إلى توسيع واجهاتها. بمجرد حدوث ذلك ، ألقِ نظرة على github.com/jmoiron/sqlx . اسمح لي أن أريك بعض الأمثلة عن كيف يمكن لهذا التمديد تبسيط حياتك.
باستخدام StructScan يلغي الحاجة إلى نقل البيانات يدويا من الأعمدة إلى خصائص الهيكل.
type Place struct { Country string City sql.NullString TelephoneCode int `db:"telcode"` } var p Place err = rows.StructScan(&p)
يتيح لك استخدام NamedQuery استخدام خصائص البنية كعناصر نائبة في استعلام.
p := Place{Country: "South Africa"} sql := `.. WHERE country=:country` rows, err := db.NamedQuery(sql, p)
يتيح لك استخدام Get and Select التخلص من الحاجة إلى كتابة حلقات يدويًا تحصل على خطوط من قاعدة البيانات.
var p Place var pp []Place
السائقين
database/sql
هي مجموعة من الواجهات للعمل مع قاعدة البيانات ، و sqlx
هو امتدادها. لكي تعمل هذه الواجهات ، فإنها تحتاج إلى تنفيذ. السائقين مسؤولون عن التنفيذ.
السائقين الأكثر شعبية:
- github.com/lib/pq -
pure Go Postgres driver for database/sql.
ظل برنامج التشغيل هذا هو المعيار الافتراضي لفترة طويلة. لكن اليوم فقد أهميته ولا يتم تطويره بواسطة المؤلف. - github.com/jackc/pgx -
PostgreSQL driver and toolkit for Go.
من الأفضل اليوم اختيار هذه الأداة.
github.com/jackc/pgx - هذا هو برنامج التشغيل الذي تريد استخدامه. لماذا؟
- بدعم بنشاط وتطويرها .
- يمكن أن يكون أكثر إنتاجية إذا تم استخدامه دون واجهات
database/sql
. - دعم أكثر من 60 نوعًا من PostgreSQL التي ينفذها
PostgreSQL
خارج معيار SQL
. - القدرة على تنفيذ تسجيل ما يحدث داخل السائق بشكل مريح.
pgx
على أخطاء يمكن قراءتها من قِبل الإنسان ، بينما يرمي lib/pq
فقط بنوبات الهلع. إذا لم تصاب بالذعر ، فسوف يتعطل البرنامج. ( يجب عدم استخدام الذعر في Go ، وهذا ليس هو نفسه الاستثناء. )- مع
pgx
، لدينا القدرة على تكوين كل اتصال بشكل مستقل. - هناك دعم لبروتوكول
PostgreSQL
للنسخ المتماثل المنطقي .
4KB
عادة ، نكتب هذه الحلقة للحصول على البيانات من قاعدة البيانات:
rows, err := s.db.QueryContext(ctx, sql) for rows.Next() { err = rows.Scan(...) }
داخل برنامج التشغيل ، نحصل على البيانات عن طريق تخزينها في مخزن مؤقت 4 كيلوبايت . rows.Next()
يولد رحلة شبكة ويملأ المخزن المؤقت. إذا لم يكن المخزن المؤقت كافيًا ، فسنذهب إلى الشبكة للحصول على البيانات المتبقية. المزيد من زيارات الشبكة - سرعة معالجة أقل. من ناحية أخرى ، نظرًا لأن حد المخزن المؤقت هو 4 كيلو بايت ، فلننسى ذاكرة العملية بالكامل.
لكن ، بالطبع ، أرغب في إلغاء فك وحدة التخزين المؤقتة إلى الحد الأقصى من أجل تقليل عدد الطلبات على الشبكة وتقليل زمن الوصول إلى الخدمة الذي نقدمه. نضيف هذه الفرصة ونحاول معرفة التسارع المتوقع في الاختبارات الاصطناعية :
$ go test -v -run=XXX -bench=. -benchmem goos: linux goarch: amd64 pkg: github.com/furdarius/pgxexperiments/bufsize BenchmarkBufferSize/4KB 5 315763978 ns/op 53112832 B/op 12967 allocs/op BenchmarkBufferSize/8KB 5 300140961 ns/op 53082521 B/op 6479 allocs/op BenchmarkBufferSize/16KB 5 298477972 ns/op 52910489 B/op 3229 allocs/op BenchmarkBufferSize/1MB 5 299602670 ns/op 52848230 B/op 50 allocs/op PASS ok github.com/furdarius/pgxexperiments/bufsize 10.964s
يمكن ملاحظة أنه لا يوجد فرق كبير في سرعة المعالجة. لماذا هذا
اتضح أننا مقيدون بحجم المخزن المؤقت لإرسال البيانات داخل بوستجرس نفسها. يحتوي هذا المخزن المؤقت حجم ثابت 8 كيلو بايت . باستخدام strace
يمكنك رؤية أن نظام التشغيل يُرجع 8192
بايت في استدعاء نظام القراءة . و tcpdump
يؤكد هذا مع حجم الحزم.
توم لين ( أحد المطورين الأساسيين لنواة Postgres ) يعلق مثل هذا:
تقليديًا ، على الأقل ، كان هذا هو حجم المخازن المؤقتة للأنابيب في أجهزة Unix ، لذلك من حيث المبدأ ، يعد هذا الحجم الأكثر حجمًا لإرسال البيانات عبر مأخذ يونيكس.
يعتقد Andres Freund ( مطور Postgres من EnterpriseDB ) أن المخزن المؤقت 8 كيلو بايت ليس هو أفضل خيار للتنفيذ حتى الآن ، وتحتاج إلى اختبار السلوك بأحجام مختلفة وبتكوين مختلف للمقبس.
يجب أن نتذكر أيضًا أن PgBouncer يحتوي أيضًا على مخزن مؤقت ويمكن تكوين pkt_buf
المعلمة pkt_buf
.
OIDs
ميزة أخرى لبرنامج التشغيل pgx ( v3 ): لكل اتصال ، يجعل طلب قاعدة البيانات للحصول على معلومات حول معرف الكائن ( OID ).
تمت إضافة هذه المعرفات إلى Postgres لتحديد الكائنات الداخلية بشكل فريد : الصفوف والجداول والوظائف ، إلخ.
يستخدم برنامج التشغيل معرفة OIDs
لفهم أي عمود قاعدة البيانات إلى أي لغة بدائية لإضافة البيانات. لهذا ، يدعم pgx
مثل هذا الجدول ( المفتاح هو اسم النوع ، والقيمة هي معرف الكائن )
map[string]Value{ "_aclitem": 2, "_bool": 3, "_int4": 4, "_int8": 55, ... }
يؤدي هذا التطبيق إلى حقيقة أن برنامج التشغيل لكل اتصال ثابت مع قاعدة البيانات يقدم حوالي ثلاثة طلبات لتشكيل جدول Object ID
. في الوضع العادي لتشغيل قاعدة البيانات والتطبيق ، يسمح لك تجمع الاتصال في Go بعدم إنشاء اتصالات جديدة إلى قاعدة البيانات. ولكن عند أدنى تدهور في قاعدة البيانات ، يتم استنفاد مجموعة الاتصالات على جانب التطبيق ويزيد عدد الاتصالات التي تم إنشاؤها في وقت الوحدة زيادة كبيرة. طلبات OIDs
ثقيلة جدًا ، ونتيجة لذلك ، يمكن لبرنامج التشغيل إحضار قاعدة البيانات إلى حالة حرجة.
هذه هي اللحظة التي تم فيها صب هذه الطلبات على إحدى قواعد البيانات الخاصة بنا:

15 معاملة في الدقيقة في الوضع العادي ، قفزة تصل إلى 6500 معاملة خلال التدهور.
ما يجب القيام به
أولاً وقبل كل شيء ، قم بتحديد حجم حمام السباحة الخاص بك من أعلى.
بالنسبة database/sql
يمكن القيام بذلك باستخدام وظيفة DB.SetMaxOpenConns . إذا كنت تتخلى عن واجهات database/sql
وتستخدم pgx.ConnPool
( تجمع الاتصال الذي ينفذه برنامج التشغيل نفسه ) ، فيمكنك تحديد MaxConnections
( الافتراضي هو 5 ).
بالمناسبة ، عند استخدام pgx.ConnPool
سيقوم برنامج التشغيل بإعادة استخدام معلومات حول OIDs
المستلمة ولن يقدم طلبات إلى قاعدة البيانات لكل اتصال جديد.
إذا كنت لا ترغب في رفض database/sql
، فيمكنك تخزين معلومات عن OIDs
بنفسك.
github.com/jackc/pgx/stdlib.OpenDB(pgx.ConnConfig{ CustomConnInfo: func(c *pgx.Conn) (*pgtype.ConnInfo, error) { cachedOids =
هذه طريقة عمل ، لكن استخدامها قد يكون خطيرًا في ظل شرطين:
- يمكنك استخدام التعداد أو أنواع المجال في بوستجرس ؛
- إذا فشل المعالج ، يمكنك تبديل التطبيق إلى النسخة المتماثلة التي يتم سكبها بواسطة النسخ المتماثل المنطقي.
يؤدي تنفيذ هذه الشروط إلى حقيقة أن OIDs
المخزنة مؤقتًا تصبح غير صالحة. لكننا لن نكون قادرين على تنظيفها ، لأننا لا نعرف لحظة التحول إلى قاعدة جديدة.
في عالم Postgres
، عادةً ما يتم استخدام النسخ المتماثل الفعلي لتنظيم التوافر العالي ، الذي ينسخ مثيلات قاعدة البيانات شيئًا فشيئًا ، لذلك نادراً ما تظهر مشكلات التخزين المؤقت OIDs
في البرية. ( ولكن من الأفضل أن تتأكد من طريقة DBA الخاصة بك كيف يعمل وضع الاستعداد لديك ).
في الإصدار الرئيسي التالي من برنامج تشغيل pgx
- الإصدار pgx
، لن تكون هناك حملات OIDs
. الآن سيعتمد برنامج التشغيل فقط على قائمة OIDs
في التعليمات البرمجية. بالنسبة للأنواع المخصصة ، ستحتاج إلى التحكم في إلغاء التسلسل في جانب التطبيق الخاص بك: سوف يتخلى برنامج التشغيل ببساطة عن جزء من الذاكرة كصفيف من وحدات البايت.
تسجيل ومراقبة
ستساعد المراقبة والتسجيل في ملاحظة المشكلات قبل تعطل القاعدة.
database/sql
طريقة DB.Stats () . ستعطيك لقطة الحالة التي تم إرجاعها فكرة عما يحدث داخل السائق.
type DBStats struct { MaxOpenConnections int
إذا كنت تستخدم التجمع في pgx
مباشرةً ، pgx
طريقة ConnPool.Stat () معلومات مماثلة:
type ConnPoolStat struct { MaxConnections int CurrentConnections int AvailableConnections int }
التسجيل مهم بنفس القدر ، ويتيح لك pgx
القيام بذلك. يقبل برنامج التشغيل واجهة Logger
، من خلال تطبيق ، والتي ستحصل على جميع الأحداث التي تحدث داخل برنامج التشغيل.
type Logger interface {
على الأرجح ، ليس عليك حتى تنفيذ هذه الواجهة بنفسك. في pgx
خارج الصندوق ، توجد مجموعة من المحولات الخاصة بأكثر أجهزة التسجيل شعبية ، على سبيل المثال ، uber-go / zap ، sirupsen / logrus ، rs / zerolog .
البنية التحتية
دائمًا تقريبًا عند العمل مع Postgres
ستستخدم أداة تجمع الاتصالات ، وستكون PgBouncer ( أو odyssey - إذا كنت Yandex ).
لماذا هذا ، يمكنك أن تقرأ في المقالة الممتازة brandur.org/postgres-connections . باختصار ، عندما يتجاوز عدد العملاء 100 ، تبدأ سرعة معالجة الطلبات في الانخفاض. يحدث هذا بسبب ميزات تنفيذ بوستجرس نفسها: إطلاق عملية منفصلة لكل اتصال ، وآلية إزالة اللقطات واستخدام الذاكرة المشتركة للتفاعل - كل هذا يؤثر.
هنا هو المعيار من مختلف تطبيقات تجمع الاتصالات:

وعرض النطاق الترددي القياسي مع وبدون PgBouncer.

نتيجة لذلك ، ستبدو البنية الأساسية الخاصة بك كما يلي:

حيث Server
هو العملية التي تعالج طلبات المستخدم. هذه العملية تدور في kubernetes
في 3 نسخ ( على الأقل ). بشكل منفصل ، على خادم الحديد ، هناك Postgres
، التي تغطيها PgBouncer'
. PgBouncer
نفسه ذو ترابط واحد ، لذلك نطلق العديد من الحراس ، حركة المرور التي PgBouncer
خلالها باستخدام HAProxy
. نتيجة لذلك ، نحصل على سلسلة تنفيذ الاستعلام هذه في قاعدة البيانات: → HAProxy → PgBouncer → Postgres
.
يمكن أن يعمل PgBouncer
في ثلاثة أوضاع:
- تجمع الجلسة - لكل جلسة ، يتم إصدار اتصال واحد وتعيين لها طوال فترة الحياة.
- تجمع المعاملات - يعيش الاتصال أثناء تشغيل المعاملة. بمجرد اكتمال المعاملة ، يأخذ
PgBouncer
هذا الاتصال PgBouncer
إلى معاملة أخرى. يسمح هذا الوضع بالتخلص جيدًا من المركبات. - تجميع البيانات - وضع الإهمال . تم إنشاؤه فقط لدعم PL / Proxy .
يمكنك رؤية مصفوفة الخصائص المتوفرة في كل وضع. نختار تجمّع المعاملات ، لكن له قيودًا على العمل مع Prepared Statements
.
تجمع المعاملات + البيانات المعدة
دعنا نتخيل أننا نريد إعداد طلب ثم تنفيذه. في مرحلة ما ، نبدأ معاملة نرسل فيها طلب تحضير ، ونحصل على معرّف الطلب المعد من قاعدة البيانات.

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

في وضع " تجمع المعاملات" ، يمكن تنفيذ معاملتين في اتصالات مختلفة ، لكن معرف البيان صالح فقط داخل اتصال واحد. نحصل على prepared statement does not exist
خطأ عند محاولة تنفيذ طلب.
الأكثر رضاءًا: نظرًا لأنه أثناء التطوير واختبار الحمل صغير ، فإن PgBouncer
يصدر في كثير من الأحيان نفس الاتصال ويعمل كل شيء بشكل صحيح. ولكن بمجرد طرحنا على المنتج ، تبدأ الطلبات في الانخفاض مع وجود خطأ.
الآن ابحث عن Prepared Statements
في هذا الكود:
sql := `select * from places where city = ?` rows, err := s.db.Query(sql, city)
لن تراه! سيحدث تحضير الاستعلام ضمنًا داخل Query()
. في الوقت نفسه ، سيحدث إعداد وتنفيذ الطلب في معاملات مختلفة وسنتلقى بالكامل كل ما وصفته أعلاه.
ما يجب القيام به
الخيار الأول والأسهل هو تبديل PgBouncer
إلى Session pooling
. يتم تخصيص اتصال واحد للجلسة ، وتبدأ جميع المعاملات في هذا الصدد وتعمل الطلبات المعدة بشكل صحيح. ولكن في هذا الوضع ، فإن كفاءة استخدام المركبات تترك الكثير مما هو مرغوب فيه. لذلك ، لا يعتبر هذا الخيار.
الخيار الثاني هو إعداد طلب من جانب العميل . لا أريد القيام بذلك لسببين:
- نقاط ضعف SQL المحتملة. قد ينسى المطور أو يهرب بشكل غير صحيح.
- الهروب من معلمات الاستعلام في كل مرة يجب أن تكتب بيديك.
خيار آخر هو التفاف كل طلب في معاملة بشكل صريح . بعد كل شيء ، ما دامت المعاملة حية ، PgBouncer
لا تلتقط الاتصال. يعمل هذا ، ولكن بالإضافة إلى الفعل في الشفرة الخاصة بنا ، نحصل أيضًا على المزيد من مكالمات الشبكة: البدء ، الإعداد ، التنفيذ ، الالتزام. إجمالي 4 مكالمات شبكة لكل طلب. الكمون ينمو.
لكنني أريدها بأمان وراحة وكفاءة. وهناك مثل هذا الخيار! يمكنك إخبار برنامج التشغيل بشكل صريح أنك تريد استخدام وضع Simple Query . في هذا الوضع ، لن يكون هناك إعداد وسيتم تمرير الطلب بالكامل في مكالمة شبكة واحدة. في هذه الحالة ، سيقوم برنامج التشغيل بعمل التدريع لكل من المعلمات نفسها ( يجب تنشيط standard_conforming_strings
على المستوى الأساسي أو عند إنشاء اتصال ).
cfg := pgx.ConnConfig{ ... RuntimeParams: map[string]string{ "standard_conforming_strings": "on", }, PreferSimpleProtocol: true, }
إلغاء الطلبات
تتعلق المشكلات التالية بإلغاء الطلبات من جانب التطبيق.
ألق نظرة على هذا الرمز. أين هي المزالق؟
rows, err := s.db.QueryContext(ctx, ...)
Go لديه طريقة للتحكم في تدفق تنفيذ البرنامج - context.Context . في هذا الرمز ، نقوم بتمرير ctx
برنامج التشغيل بحيث عندما يتم إغلاق السياق ، يلغي برنامج التشغيل الطلب على مستوى قاعدة البيانات.
في الوقت نفسه ، من المتوقع أن نوفر الموارد عن طريق إلغاء الطلبات التي لا ينتظرها أحد. ولكن عندما يتم PgBouncer
الطلب PgBouncer
يرسل PgBouncer
الإصدار 1.7 معلومات إلى الاتصال بأن هذا الاتصال جاهز للاستخدام ، وبعد ذلك PgBouncer
إلى التجمع. هذا السلوك من PgBouncer'
هو تضليل برنامج التشغيل ، والذي عند إرسال الطلب التالي ، يتلقى ReadyForQuery
فورًا استجابة لذلك. في النهاية ، نلاحظ أخطاء ReadyForQuery غير المتوقعة .
بدءًا من PgBouncer
الإصدار 1.8 ، تم إصلاح هذا السلوك. استخدم الإصدار الحالي من PgBouncer
.
وعلى الرغم من ذلك ، في هذه الحالة ، ستختفي الأخطاء - سيبقى سلوك مثير للاهتمام. في بعض الحالات ، قد يتلقى تطبيقنا إجابات ليس لطلبه ، ولكن للطلب المجاور (الشيء الرئيسي هو أن الطلبات تتوافق مع نوع وترتيب البيانات المطلوبة). هذا هو ، على سبيل المثال ، للاستعلام where user_id = 2
، استجابة الاستعلام where user_id = 42
سيتم إرجاعها. ويرجع ذلك إلى معالجة طلبات الإلغاء على مستويات مختلفة: على مستوى تجمع السائقين وتجمع الحارس.
تأجيل الإلغاء
لإلغاء الطلب ، نحتاج إلى إنشاء اتصال جديد بقاعدة البيانات وطلب الإلغاء. Postgres
ينشئ عملية منفصلة لكل اتصال. نرسل أمرًا لإلغاء الطلب الحالي في عملية محددة. للقيام بذلك ، قم بإنشاء اتصال جديد وفيه نقل معرف العملية (PID) الذي يهمنا. ولكن في الوقت الذي ينتقل فيه أمر الإلغاء إلى القاعدة ، فقد ينتهي الطلب الملغى من تلقاء نفسه.

سيقوم Postgres
بتنفيذ الأمر وإلغاء الطلب الحالي في العملية المحددة. لكن الطلب الحالي لن يكون الطلب الذي أردنا إلغاؤه في البداية. نظرًا لهذا السلوك عند العمل مع Postgres
مع PgBouncer
عدم إلغاء الطلب على مستوى برنامج التشغيل. للقيام بذلك ، يمكنك تعيين CustomCancel
، والتي لن تلغي الطلب ، حتى لو context.Context
استخدام context.Context
.
cfg := pgx.ConnConfig{ ... CustomCancel: func(_ *pgx.Conn) error { return nil }, }
Postgres المرجعية
بدلاً من الاستنتاجات ، قررت إنشاء قائمة مرجعية للعمل مع بوستجرس. هذا من شأنه أن يساعد المادة تنسجم مع رأسي.
- استخدم github.com/jackc/pgx كسائق للعمل مع Postgres.
- الحد من حجم تجمع الاتصال من أعلاه.
- ذاكرة التخزين المؤقت
OIDs
أو استخدام pgx.ConnPool إذا كنت تعمل مع pgx
الإصدار 3. - جمع المقاييس من تجمع الاتصال باستخدام DB.Stats () أو ConnPool.Stat () .
- سجل ما يحدث في السائق.
- استخدم وضع الاستعلام البسيط لتجنب مشاكل في إعداد الاستعلام في وضع معاملات
PgBouncer
. - تحديث
PgBouncer
إلى أحدث إصدار. - كن حذرا مع إلغاء الطلبات من التطبيق.