PostgreSQL
在 PostgreSQL 上,一个数据库保存服务存储的一切:它的日志、快照、定时器,以及它的检查点、租约、失败台账和预留。日志每次提交一条自动提交的语句;负载高时,节点把一个分区的追加合成一条语句。本页连接一个服务、介绍这些表,并确定连接池的大小。
连接服务
把 wow-postgres 和 HikariCP 放到类路径上,并设置 spring.datasource.url 以及 spring.datasource.username、spring.datasource.password:宿主用它们建一个带连接池的 DataSource,并选择 PostgreSQL。HikariCP 自己的设置放在 spring.datasource.hikari.* 下,例如 maximum-pool-size。连接池在第一次取连接时才打开。没有设置 URL 时,宿主不连接任何数据库。
自己定义了 DataSource Bean 的应用保留它;引入了 Spring Boot JDBC 模块(spring-boot-starter-jdbc)的应用,使用 Spring Boot 用同样的属性建的那个。eShop 的服务只在它们的 postgres profile 中设置这些属性:
spring:
config:
activate:
on-profile: postgres
datasource:
url: jdbc:postgresql://localhost:5432/eshop_ordering
username: postgres
password: postgreswow.backend: postgres 直接指明后端,而不是交给应用拥有的 Bean 来决定。每个服务有自己的数据库;两个服务从不共用一个。
表
存储在第一次使用时创建它们的表。表名由上下文和聚合类型推出,所以人或查询服务可以直接读取一个聚合的数据:
| 表 | 保存 |
|---|---|
wow_log__<context>__<aggregate> | 一个聚合类型的日志:每条已提交的流一行,它的事实放在列里(版本、命令、请求、作用域、因果、操作人,访问属性为 jsonb,未盖章时为 null),以及它的事件 |
wow_log__<context>__partition | 上下文日志的每个分区一行计数器 |
wow_log_aggregate | 每个上下文的日志有哪些聚合表 |
wow_snapshot__<context>__<aggregate> | 每个聚合一行:它最新的快照,状态和访问属性为 jsonb,直接在原处查询 |
wow_snapshot_table | 每张快照表保存的状态修订号 |
wow_timer__<context>__<aggregate> | 一种聚合类型的定时命令,每个定时器一行,位于其属主的分区 |
wow_timer_aggregate | 每个上下文有哪些定时器表;到期读取在一条语句中合并它们 |
wow_checkpoint、wow_lease、wow_lease_member | 每个订阅进行到哪里,以及哪个节点拥有哪个分区 |
wow_failure_ledger | 停放的聚合 |
wow_reservation__<context>__<aggregate> | 一种类型的聚合预留的唯一键:每个(租户、命名空间、键)一行,带着它的所有者 |
wow_format | 每个结构的格式版本 |
日志表的键是位置(partition、sequence)、版本(aggregate_id、tenant_id、version)和请求(tenant_id、request_id):数据库本身就会拒绝同一版本或同一请求的第二次提交。
格式。 每个结构在创建之前先在 wow_format 中声明它的格式版本。以另一个版本记录的结构,或者存在却没有记录的结构,都会被拒绝:一个 Wow 构建从不读写它不认识的布局。同一格式内新增的列,会在下次检查时加到已有的表上。
管理 schema
wow.ddl 决定存储怎样对待它们的表:
wow.ddl | 作用 |
|---|---|
auto(默认) | 创建缺失的东西 |
validate | 什么都不创建;缺少表或表是另一种格式时拒绝启动 |
none | 什么都不创建,也不检查 |
数据库账户不能建表的部署,先用一个能建表的账户运行一次,或者自己应用 schema,然后以 validate 运行。wow.stores.<name>.ddl 为单个存储设置它。
聚合声明的查询索引(@QueryIndex)来自一次 auto 下的启动:它用 CREATE INDEX CONCURRENTLY 建立每个索引,并把规划定义的指纹记为索引的注释。validate 检查这条注释,所以手工建立的同名索引(没有这条注释)或早先定义的索引会被拒绝,下一次 auto 下的启动会重建它。改变索引规划方式的版本会把声明的索引重建一次。
服务端设置
TCP keepalive。 节点不关连接就死掉时,它的连接对应的后端进程仍然开着,其中未结束的事务还持有它的锁。设置服务端的 keepalive,让服务器在一分钟左右察觉并结束它们:
ALTER SYSTEM SET tcp_keepalives_idle = 30;
ALTER SYSTEM SET tcp_keepalives_interval = 10;
ALTER SYSTEM SET tcp_keepalives_count = 3;
SELECT pg_reload_conf();默认值跟随操作系统,通常要静默两小时才发第一次探测。
Wow 在事务内设置的上限。 存储开启的每个事务,开头都有一批 SET LOCAL,与事务的第一条语句在同一次往返中发送(第一条语句若是批量写入,则先单独发送),由连接的超时(wow.postgres.operation-timeout,默认 30 秒)推导,没有单独的配置项:
| 设置 | 取值 | 作用 |
|---|---|---|
statement_timeout、lock_timeout | 超时减 1 秒,至少 1 秒:默认 29 秒 | 服务器在客户端放弃之前取消语句(57014),连接得以保留 |
idle_in_transaction_session_timeout | 超时的两倍:默认 60 秒 | 服务器结束节点留下未结束的事务,包括崩溃节点的事务 |
查询的事务改为使用自己的预算,并以只读方式开启。追加每次是一条自动提交的语句,不设服务端上限。SET LOCAL 随事务结束,所以应用的连接池上什么也不会留下。
建表步骤。 存储在创建任何东西之前先读系统目录,所以 schema 已是最新时不取表锁。必须执行的语句等锁最多 5 秒,最多尝试 5 次(含第一次),退避从半秒开始;之后启动失败,并指出表和持有锁的会话。同时启动的节点一次只有一个执行同一张表的步骤。
确定连接池的大小
每个节点为它的追加、订阅、定时器和租约持有几个连接,为它的查询持有更多。连接池耗尽时追加就会等待,所以给查询一个单独的连接池:
- 定义第二个
DataSourceBean,并把它指定为存储的query-data-source:快照查询和events查询就从它取连接,绝不会占用追加正在等待的连接。它可以指向只读副本。 - 没有单独的连接池时,查询与追加共用连接池,节点把查询限制在它的四分之一,至少 2 个。追加变慢或等待连接时降低这个上限,恢复后再提高,最多到连接池的一半(配置项)。
按数据库计算连接数:连接池大小乘以服务的节点数,再加一个连接池,留给替换崩溃节点的新节点,因为崩溃节点的连接在上面的 keepalive 结束它们之前仍然开着,再加上迁移和运维人员使用的连接,要在 PostgreSQL 的 max_connections 之内。
存储借出的每个连接等待回应最多 wow.postgres.operation-timeout(默认 30 秒,单个存储的 operation-timeout(wow.stores.<name>.operation-timeout)可覆盖),建表类语句最多 5 分钟:在故障切换中或代理之后丢失的回应会让调用以暂时性错误失败并换掉这个连接,而不是一直挂住。连接池的驱动已设了更短的 socketTimeout 时保留它。服务器按同一个超时为 Wow 的每个事务设上限(服务端设置)。
多个数据库
各聚合类型把数据放在不同数据库里的服务,命名它的存储实例,并把每个类型放到其中一个上:
wow:
stores:
orders:
type: postgres
data-source: ordersDataSource
archive:
type: postgres
data-source: archiveDataSource
log:
store: orders
snapshot:
stores: [orders]
aggregates:
"[shop.invoice]":
log:
store: archive每个存储运行在它指定的 DataSource Bean 上,两个日志存储不能共用一个数据库。放到另一个存储上的聚合类型,把它的日志、它的订阅的检查点、它的失败台账、定时器和预留都放在那里;租约仍留在默认的日志存储上。
备份
日志是唯一的事实:快照、读模型和数仓都从它重建。把日志表与检查点、租约、台账、预留和定时器一起,作为一次一致的数据库备份来备份。