对于数据库,我们公司比在应用程序中采用更为保守的方法。数据库不是在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:
1_dcl.sql:
2_dml_refs.sql:
Fixtures. dev
3_dml_dev.sql:
rollback.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。
当应用程序上下文启动时,需要这种机制来滚动迁移。
例如:
- 在测试数据库上,修补程序集001、002、003。
- 鼓舞者开发了补丁集004、005,并且没有将应用程序部署到测试中。
- 部署到测试。补丁集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))
}
在验证管道中运行这些测试有两个相关的任务:
- 构建代理可能正忙于标准PostgreSQL端口5432或任何静态端口。您永远都不会知道,在测试完成后,没有人将容器和底座一起取出。
- 第二项任务:完成测试后,需要将容器熄灭。
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中的标准管道引入测试和自动发布补丁集。管道是拥有数据库的微服务的子级。
- 我们不在团队城市中存储数据库中的积分。而且我们不关心虚拟构建者的访问。
我知道对于许多人来说这不是一个启示。但是,既然您已经阅读完,我们将很乐意在评论中分享您的经验。