كيف اخترعنا عملية تطوير وتصحيح وتسليم تغييرات قاعدة البيانات في عام 2020

إنه عام 2020 في الفناء وأنت معتاد بالفعل على سماع ضوضاء الخلفية: "Kubernetes هي الإجابة!" ، "Microservices!" ، "شبكة الخدمة!" ، "سياسات سيسوريتي!" الجميع يركض نحو مستقبل مشرق.



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



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











المشاكل التي قمنا بحلها



لا توجد معايير إصدار موحدة



في أفضل الأحوال ، هذه هي ملفات DDL SQL الموجودة في مكان ما في دليل db في المستودع مع الخدمة المصغرة. إنه أمر سيئ للغاية إذا كانت هذه مجرد الحالة الحالية لقاعدة البيانات ، مختلفة في الاختبار والإنتاج ، ولا توجد نصوص برمجية مرجعية لمخطط قاعدة البيانات.



أثناء التصحيح ، نحطم قاعدة الاختبار



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



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



لا تغطي الاختبارات طرق DAO ، ولم يتم التحقق من صحتها في CI



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



ليس هناك ما يضمن عدم انهيار أي شيء في المستقبل. تعاني جودة الخدمة المصغرة وقابليتها للصيانة.



عدم تماثل الوسائط



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



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



عملية ثقيلة وهشة لدحرجة الإنتاج



تم بدء الإنتاج يدويًا ، ولكن عن طريق القياس مع عملية Oracle ، من خلال موافقة DBA ، ومديري الإصدار ، والمضي قدمًا من قبل مهندسي الإصدار.



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



لذلك ، يحدث أن يقوم المطورون بإدخال التغييرات على الخدمات غير الهامة دون هذه العملية على الإطلاق.



كيف يمكنك أن تفعل لتكون جيدًا



التصحيح على قاعدة بيانات محلية في حاوية عامل إرساء



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



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



إليك مثال عن مدى سهولة إنشاء قاعدة بيانات محلية:



لنكتب Dockerfile من سطرين:



FROM postgres:12.3
ADD init.sql /docker-entrypoint-initdb.d/


في init.sql ، نقوم بإنشاء قاعدة بيانات "نظيفة" ، والتي نتوقع الحصول عليها في كل من الاختبار والإنتاج. يجب أن تحتوي على:



  • مالك المخطط والمخطط نفسه.
  • مستخدم التطبيق مع منحة لاستخدام المخطط.
  • الإضافات المطلوبة


مثال على Init.sql
create role my_awesome_service
with login password *** NOSUPERUSER inherit CREATEDB CREATEROLE NOREPLICATION;
create tablespace my_awesome_service owner my_awesome_service location '/u01/postgres/my_awesome_service_data';
create schema my_awesome_service authorization my_awesome_service;
grant all on schema my_awesome_service to my_awesome_service;
grant usage on schema my_awesome_service to my_awesome_service;
alter role my_awesome_service set search_path to my_awesome_service,pg_catalog, public;

create user my_awesome_service_app with LOGIN password *** NOSUPERUSER inherit NOREPLICATION;
grant usage on schema my_awesome_service to my_awesome_service_app;

create extension if not exists "uuid-ossp";




للراحة ، يمكنك إضافة مهمة db إلى Makefile ، والتي ستبدأ (إعادة) الحاوية بالقاعدة وتبرز المنفذ للاتصال:



db:
    docker container rm -f my_awesome_service_db || true
    docker build -t my_awesome_service_db docker/db/.
    docker run -d --name my_awesome_service_db -p 5433:5432 my_awesome_service_db


يتغير تعيين الإصدار مع معيار الصناعة



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



بشكل عام ، أنت بحاجة إلى التحكم. أنظمة الهجرة هي مجرد تحكم.

لن ندخل في مقارنة بين أنظمة إصدارات مخطط قاعدة البيانات المختلفة. FlyWay vs Liquibase ليس موضوع هذه المقالة. اخترنا Liquibase.



نحن نسخة:



  • DDL- بنية كائنات قاعدة البيانات (إنشاء جدول).
  • محتوى DML لجداول البحث (إدراج ، تحديث).
  • منح DCL لتطبيقات UZ (منح حدد ، أدخل في ...).


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



مثال التصحيح SQL
0_ddl.sql:

create table my_awesome_service.ref_customer_type
(
    customer_type_code    	varchar not null,
    customer_type_description varchar not null,
    constraint ref_customer_type_pk primary key (customer_type_code)
);
 
alter table my_awesome_service.ref_customer_type
    add constraint customer_type_code_ck check ( (customer_type_code)::text = upper((customer_type_code)::text) );


1_dcl.sql:



grant select on all tables in schema my_awesome_service to ru_svc_qw_my_awesome_service_app;
grant insert, update on my_awesome_service.some_entity to ru_svc_qw_my_awesome_service_app;


2_dml_refs.sql:



insert into my_awesome_service.ref_customer_type (customer_type_code, customer_type_description)
values ('INDIVIDUAL', '. ');
insert into my_awesome_service.ref_customer_type (customer_type_code, customer_type_description)
values ('LEGAL_ENTITY', '. ');
insert into my_awesome_service.ref_customer_type (customer_type_code, customer_type_description)
values ('FOREIGN_AGENCY', ' . ');


Fixtures. dev

3_dml_dev.sql:



insert into my_awesome_service.some_entity_state (state_type_code, state_data, some_entity_id)
values ('BINDING_IN_PROGRESS', '{}', 1);


rollback.sql:



drop table my_awesome_service.ref_customer_type;




مثال على Changeset.yaml
databaseChangeLog:
 - changeSet:
     id: 1
     author: "mr.awesome"
     changes:
       - sqlFile:
           path: db/changesets/001_init/0_ddl.sql
       - sqlFile:
           path: db/changesets/001_init/1_dcl.sql
       - sqlFile:
           path: db/changesets/001_init/2_dml_refs.sql
     rollback:
       sqlFile:
         path: db/changesets/001_init/rollback.sql
 - changeSet:
     id: 2
     author: "mr.awesome"
     context: dev
     changes:
       - sqlFile:
           path: db/changesets/001_init/3_dml_dev.sql




يقوم Liquibase بإنشاء جدول databasechangelog في قاعدة البيانات ، حيث يلاحظ التغييرات التي تم ضخها.

يقوم تلقائيًا بحساب عدد مجموعات التغييرات التي تحتاجها للتشغيل في قاعدة البيانات.



هناك مكون إضافي مافن و gradle مع القدرة على إنشاء نص برمجي من عدة تغييرات يجب إدخالها في قاعدة البيانات.



دمج نظام ترحيل قاعدة البيانات في مرحلة إطلاق التطبيق



يمكن أن يكون أي محول لنظام التحكم في الترحيل والإطار الذي تم بناء تطبيقك عليه. مع العديد من الأطر ، تأتي مجمعة مع ORM. على سبيل المثال Ruby-On-Rails و Yii2 و Nest.JS.



هذه الآلية مطلوبة لبدء عمليات الترحيل عند بدء سياق التطبيق.

على سبيل المثال:



  1. في قاعدة بيانات الاختبار ، مجموعات التصحيح 001 و 002 و 003.
  2. طور صانع المذبحة مجموعات التصحيح 004 و 005 ولم ينشر التطبيق للاختبار.
  3. انشر للاختبار. يتم طرح مجموعات التصحيح 004 و 005.


إذا لم يتم تشغيلها ، فلن يبدأ التطبيق. التحديث المتداول لا يقتل القرون القديمة.



مكدسنا هو JVM + Spring ونحن لا نستخدم ORM. لذلك ، كنا بحاجة إلى تكامل Spring-Liquibase .



لدينا مطلب أمان مهم في شركتنا: يجب أن يكون لدى مستخدم التطبيق مجموعة محدودة من المنح وبالتأكيد يجب ألا يكون لديه وصول على مستوى مالك المخطط. باستخدام Spring-Liquibase ، من الممكن تدوير عمليات الترحيل نيابة عن مستخدم مالك المخطط. في هذه الحالة ، لا يمتلك تجمع الاتصال الخاص بمستوى التطبيق حق الوصول إلى Liquibase DataSource. لذلك ، لن يحصل التطبيق على حق الوصول من مستخدم مالك المخطط.



تطبيق test.yaml مثال
spring:
  liquibase:
    enabled: true
    database-change-log-lock-table: "databasechangeloglock"
    database-change-log-table: "databasechangelog"
    user: ${secret.liquibase.user:}
    password: ${secret.liquibase.password:}
    url: "jdbc:postgresql://my.test.db:5432/my_awesome_service?currentSchema=my_awesome_service"




تتحقق اختبارات DAO في مرحلة CI



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



يؤدي رفع حاوية بقاعدة بيانات ومجموعات رقع متدرجة إلى زيادة وقت بدء سياق الربيع بمقدار 1.5-10 ثانية ، اعتمادًا على قوة آلة العمل وعدد مجموعات التصحيح.



هذه ليست اختبارات وحدة حقًا ، إنها اختبارات لدمج طبقة DAO للتطبيق مع قاعدة البيانات.

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



إنها أيضًا طريقة سهلة لتصحيح أخطاء DAOs. بدلاً من استدعاء RestController ، لمحاكاة سلوك المستخدم في بعض سيناريوهات الأعمال ، نقوم على الفور باستدعاء DAO مع الوسائط المطلوبة.



مثال اختبار DAO
@Test
@Transactional
@Rollback
fun `create cheque positive flow`() {
      jdbcTemplate.update(
       "insert into my_awesome_service.some_entity(inn, registration_source_code)" +
               "values (:inn, 'QIWICOM') returning some_entity_id",
       MapSqlParameterSource().addValue("inn", "526317984689")
   )
   val insertedCheque = chequeDao.addCheque(cheque)
   val resultCheque = jdbcTemplate.queryForObject(
       "select cheque_id from my_awesome_service.cheque " +
               "order by cheque_id desc limit 1", MapSqlParameterSource(), Long::class.java
   )
   Assert.assertTrue(insertedCheque.isRight())
   Assert.assertEquals(insertedCheque, Right(resultCheque))
}




توجد مهمتان مرتبطتان بإجراء هذه الاختبارات في مسار التحقق:



  1. يمكن أن يكون وكيل الإنشاء مشغولاً بمنفذ PostgreSQL القياسي 5432 أو أي منفذ ثابت. أنت لا تعرف أبدًا ، لم يقم شخص ما بإخماد الحاوية بالقاعدة بعد اكتمال الاختبارات.
  2. من هذه المهمة الثانية: تحتاج إلى إطفاء الحاوية بعد اكتمال الاختبارات.


مكتبة TestContainers تحل هاتين المهمتين . يستخدم صورة عامل الإرساء الموجودة لإحضار حاوية قاعدة البيانات في حالة init.sql.



مثال على استخدام TestContainers
@TestConfiguration
public class DatabaseConfiguration {

   @Bean
   GenericContainer postgreSQLContainer() {
       GenericContainer container = new GenericContainer("my_awesome_service_db")
               .withExposedPorts(5432);

       container.start();
       return container;
   }

   @Bean
   @Primary
   public DataSource onlineDbPoolDataSource(GenericContainer postgreSQLContainer) {
       return DataSourceBuilder.create()
               .driverClassName("org.postgresql.Driver")
               .url("jdbc:postgresql://localhost:"
                       + postgreSQLContainer.getMappedPort(5432)
                       + "/postgres")
               .username("my_awesome_service_app")
               .password("my_awesome_service_app_pwd")
               .build();
   }
    
   @Bean
   @LiquibaseDataSource
   public DataSource liquibaseDataSource(GenericContainer postgreSQLContainer) {
       return DataSourceBuilder.create()
               .driverClassName("org.postgresql.Driver")
               .url("jdbc:postgresql://localhost:"
                       + postgreSQLContainer.getMappedPort(5432)
                       + "/postgres")
               .username("my_awesome_service")
               .password("my_awesome_service_app_pwd")
               .build();
   }




مع التطور والتصحيح برزت. نحتاج الآن إلى تسليم التغييرات على مخطط قاعدة البيانات للإنتاج.



Kubernetes هو الجواب! ماذا كان سؤالك؟



لذلك ، تحتاج إلى أتمتة بعض عمليات CI / CD. لدينا نهج مدينة فريق مجرب وحقيقي. يبدو ، أين سبب وجود مقال آخر؟



وهناك سبب. بالإضافة إلى النهج المجرب والصحيح ، هناك أيضًا مشاكل مملة لشركة كبيرة.



  • لا يوجد عدد كافٍ من بناة مدينة الفريق للجميع.
  • الترخيص يكلف المال.
  • تتم إعدادات الأجهزة الافتراضية buildagent بالطريقة القديمة ، من خلال المستودعات مع التكوينات والدمية.
  • يجب نشر الوصول من المنشئين إلى الشبكات المستهدفة بالطريقة القديمة.
  • يتم أيضًا تخزين عمليات تسجيل الدخول وكلمات المرور الخاصة بسحب التغييرات إلى قاعدة البيانات بالطريقة القديمة.


وفي كل هذا "بالطريقة القديمة" تكمن المشكلة - الجميع يركضون إلى مستقبل مشرق ، ودعم من Legacy ... كما تعلمون. إنه يعمل وحسنا. لا يعمل - سنتعامل معه لاحقًا. في يوم ما. ليس اليوم.



لنفترض أنك بالفعل في ركبتيك في المستقبل المشرق ولديك بالفعل بنية تحتية Kubernetes. هناك أيضًا فرصة لإنشاء خدمة مصغرة أخرى ، والتي ستبدأ على الفور في هذه البنية التحتية ، وتلتقط التهيئة والأسرار اللازمة ، وتتمتع بالوصول اللازم ، والتسجيل في البنية التحتية لشبكة الخدمة. وكل هذه السعادة يمكن الحصول عليها من قبل مطور عادي ، دون إشراك شخص له دور * OPS. نتذكر أنه في Kubernetes يوجد نوع من عبء العمل ، مخصص فقط لنوع من أعمال الخدمة. حسنًا ، سافرنا لتقديم تطبيق على Kotlin + Spring-Liquibase ، في محاولة لإعادة استخدام أكبر قدر ممكن من البنية التحتية للخدمات الصغيرة في الشركة على JVM في kubera.



دعنا نعيد استخدام الجوانب التالية:



  • جيل المشروع.
  • نشر.
  • تسليم التكوينات والأسرار.
  • التمكن من.
  • التسجيل وتسليم السجلات إلى ELK.


نحصل على مثل هذا الخط : قابل للنقر









لدينا الآن



  • تغيير الإصدار.
  • نتحقق منها لتحديث الجدوى → التراجع.
  • اختبارات الكتابة لـ DAO. في بعض الأحيان نتبع TDD: نقوم بتشغيل تصحيح أخطاء DAO باستخدام الاختبارات. يتم إجراء الاختبارات على قاعدة بيانات حديثة الظهور في TestContainers.
  • قم بتشغيل قاعدة بيانات عامل الإرساء محليًا على منفذ قياسي. نحن نصحح الأخطاء وننظر إلى ما تبقى في قاعدة البيانات. إذا لزم الأمر ، يمكننا إدارة قاعدة البيانات المحلية يدويًا.
  • ندخل في مجموعات التصحيحات الاختبارية والإصدار التلقائي بخط أنابيب قياسي في Teamcity ، عن طريق القياس مع الخدمات المصغرة. إن خط الأنابيب هو تابع للخدمة المصغرة التي تمتلك قاعدة البيانات.
  • لا نقوم بتخزين أرصدة من قاعدة البيانات في مدينة الفريق. ونحن لا نهتم بالوصول من البناة الافتراضيين.


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



All Articles