我们如何在2020年发明开发,调试和交付数据库更改的过程

在院子里是2020年,您已经习惯于听到背景噪音:“ Kubernetes就是答案!”,“微服务!”,“服务网格!”,“安全政策!” 周围的每个人都在走向光明的未来。



对于数据库,我们公司比在应用程序中采用更为保守的方法。数据库不是在Kubernetes中旋转,而是在硬件或虚拟机中旋转。我们拥有完善的付款处理数据库更改流程,其中包括许多自动检查,大量审核以及由DBA参与的发布。这种情况下涉及的支票和人员数量会对上市时间产生负面影响。另一方面,它经过调试,可以使您可靠地对生产进行更改,从而最大程度地减少了损坏某物的可能性。如果出现故障,则说明维修过程中已经包括了合适的人员。这种方法使公司的主要服务工作更加稳定。



我们在PostgreSQL上为微服务启动了大多数新的关系数据库。Oracle的一种经过微调的过程虽然健壮,但却为小型数据库带来了不必要的复杂性。没有人愿意将艰难的过程从过去拖到光明的未来。没有人提前开始为这一光明的未来而努力。结果,我们缺乏标准和raznozhopitsu。 如果您想知道这导致了什么问题以及我们如何解决这些问题,欢迎与我们联系。











我们解决的问题



没有统一的版本控制标准



最好的情况是,这些是DDL SQL文件,位于带有微服务的存储库中db目录中的某个位置。如果这只是数据库的当前状态,而在测试和生产上有所不同,并且没有数据库模式的参考脚本,那将是非常糟糕的。



在调试过程中,我们破坏了测试基础



“我现在要稍微动摇测试数据库,不要再惊慌了”-并调试了测试数据库上新编写的架构更改代码。有时会花费很长时间,并且所有这些时间测试电路都无法正常工作。



同时,测试电路可能会中断其他微服务与微服务进行交互的部分,开发人员已经破坏了该基础。



DAO方法未包含在测试中,未在CI中验证



在开发和调试时,通过将外部句柄拉到上方几层来调用DAO方法。这揭示了业务逻辑的整个场景,而不是微服务和数据库之间的特定交互。



无法保证将来不会崩溃。微服务的质量和可维护性受到影响。



媒体的非同构



如果变更环以不同的方式交付给测试和生产,那么您将无法确定它是否可以正常工作。尤其是在测试中实际进行开发和调试时。



可以在开发人员或应用程序的帐户下创建测试中的对象。授予是随机交出的,通常授予所有特权。按照“我在日志中看到错误-我给予补助”的原则向应用程序发放补助。赠款经常在发布时被遗忘。有时,在发布之后,烟雾测试不能涵盖所有新功能,并且缺少授权不会立即触发。



重而脆弱的轧制过程



进入生产阶段是手动完成的,但与Oracle的过程类似,需要DBA,发行经理的批准,并由发行工程师进行前滚。



这减慢了发布速度。并且在出现问题的情况下,这会增加停机时间,使开发人员对数据库的访问复杂化。exec.sql和rollback.sql脚本通常没有在测试中进行测试,因为没有针对非Oracle的补丁设置标准,并且测试一直在进行。



因此,碰巧开发人员根本不需要此过程即可将更改推送到非关键服务。



你怎么做才能好



在Docker容器中的本地数据库上调试



对于某些人来说,本文中介绍的所有技术解决方案似乎都是显而易见的。但是出于某种原因,我每年都看到有人热情地踩着同样的耙子。



您不通过ssh进入测试服务器来编写和调试应用程序代码,对吗?我发现在测试数据库实例上开发和调试数据库代码是荒谬的。有例外,碰巧很难在本地提升数据库。但是通常,如果我们谈论的是轻量级的和非遗留的东西,那么就不难在本地提高基础并持续地在基础上进行所有迁移。作为回报,您将获得一个稳定的实例,该实例不会被其他开发人员所困扰,您也不会失去访问权限,并且您拥有开发所需的权限。



这是一个建立本地数据库有多么容易的示例:



让我们编写一个两行的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表,其中记录了已泵入的变更集。

自动计算需要滚动到数据库的变更集的数量。



有一个Maven和Gradle插件,能够从需要集成到数据库中的多个变更集生成脚本。



将数据库迁移系统集成到应用程序启动阶段



它可以是迁移控制系统以及构建应用程序的框架的任何适配器。在许多框架中,它与ORM捆绑在一起。例如Ruby-Rails,Yii2,Nest.JS。



当应用程序上下文启动时,需要这种机制来滚动迁移。

例如:



  1. 在测试数据库上,修补程序集001、002、003。
  2. 鼓舞者开发了补丁集004、005,并且没有将应用程序部署到测试中。
  3. 部署到测试。补丁集004、005即将推出。


如果它们不滚动,则应用程序无法启动。滚动更新不会杀死旧的Pod。



我们的堆栈是JVM + Spring,我们没有使用ORM。因此,我们需要Spring-Liquibase集成



我们公司有一项重要的安全要求:应用程序的用户应具有一组有限的授予,并且绝对不应具有架构所有者级别的访问权限。使用Spring-Liquibase,可以代表架构所有者用户滚动迁移。在这种情况下,应用程序应用程序级别的连接池无法访问Liquibase数据源。因此,该应用程序将无法从架构所有者用户获得访问权限。



Application-testing.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"




CI阶段的DAO测试验证



我们公司有一个CI阶段-验证。在此阶段,将检查更改是否符合内部质量标准。对于微服务,这通常是一次轻量级运行,以检查代码样式,并进行错误检查,单元测试运行和带有上下文提升的应用程序启动。现在,在验证阶段,您可以检查数据库迁移以及应用程序DAO层与数据库的交互。



通过数据库提升容器并滚动补丁集,取决于工作机器的功能和补丁集的数量,Spring上下文的启动时间将增加1.5-10秒。



这些并不是真正的单元测试,而是将应用程序的DAO层与数据库集成在一起的测试。

通过将数据库称为微服务的一部分,我们说它正在测试一个微服务的两个部分的集成。没有外部依赖性。因此,这些测试是稳定的,可以在验证阶段运行。他们确定了微服务和数据库合同,为将来的改进提供了保证。



这也是调试DAO的便捷方法。我们无需调用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 库解决了这两个任务它使用现有的docker映像启动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过程。我们拥有一种经过实践检验的真正的团队城市方法。看来,另一篇文章的原因在哪里?



这是有原因的。除了经过实践检验的方法外,大公司还存在无聊的问题。



  • 没有足够的团队城市建设者适合所有人。
  • 许可证要花钱。
  • 通过带有configs和puppet的存储库,以老式方式完成buildagent虚拟机的设置。
  • 必须以老式的方式查看从构建者到目标网络的访问。
  • 用于将更改滚动到数据库的登录密码也以老式方式存储。


在所有这些“老式方式”中,问题是-每个人都在走向光明的未来,并得到了Legacy的支持……您知道。它可以正常工作。不起作用-我们待会再处理。有一天 今天不行。



假设您已经在光明的未来深陷其中,并且已经拥有Kubernetes基础架构。甚至有机会生成另一个微服务,该微服务将立即在此基础结构中启动,获取必要的配置和机密,具有必要的访问权限并在服务网格基础结构中注册。普通开发人员无需拥有OPS角色的人就能获得所有这些幸福。我们记得在Kubernetes中有一种Job工作负载,仅用于某种​​服务工作。好吧,我们开车在Kotlin + Spring-Liquibase上开发了一个应用程序,试图在kubera的JVM上尽可能多地重用公司现有的基础架构以用于微服务。



让我们重用以下方面:



  • 项目的产生。
  • 部署。
  • 交付配置和机密信息。
  • 访问。
  • 记录日志并将日志传送到ELK。


我们得到了这样的管道:可点击









现在我们有



  • 变更集版本控制。
  • 我们检查它们的可行性更新→回滚。
  • 为DAO编写测试。有时我们甚至遵循TDD:我们使用测试运行DAO调试。测试是在TestContainers中新建立的数据库上执行的。
  • 在标准端口上本地运行docker数据库。我们正在调试,查看数据库中剩余的内容。如有必要,我们可以手动管理本地数据库。
  • 我们以与微服务类似的方式,通过Teamcity中的标准管道引入测试和自动发布补丁集。管道是拥有数据库的微服务的子级。
  • 我们不在团队城市中存储数据库中的积分。而且我们不关心虚拟构建者的访问。


我知道对于许多人来说这不是一个启示。但是,既然您已经阅读完,我们将很乐意在评论中分享您的经验。



All Articles