译者 | 布加迪
审校 | 孙淑娟
每个与客户产生共鸣的技术型组织最终都会遇到扩展问题。扩展产品和组织对您的流程和基础架构提出了新的要求。本文着重介绍了我们公司如何应对基础架构扩展方面的诸多挑战之一:使用Spring和Spring Data对Postgres数据库实现可扩展写入。
随着用户群越来越庞大,我们开始遇到一些性能问题,主要是受到我们的上游Postgres 数据库的制约。我们的RPS(每秒请求)在短短几个月内就从<50增加到了超过180,我们开始遇到SQL连接超时、连接断开和延迟显著增加等问题。这导致客户体验下降,这是不可接受的。
因此,我们着手研究如何消除这些Postgres瓶颈。我们很快意识到耗费太多的周期进行数据库写入,这阻塞了系统。对Postgres的每次写入都是一次调用,这意味着如果我们想将50行保存到数据库中,每行将调用1次,而不是执行一次SQL调用来保存所有这50行!
根本原因:在Hibernate中使用IDENTITY生成ID值
为什么我们无法进行批量更新?事实证明,问题与我们如何使用Hibernate为数据库中的实体生成标识符值(即主键)有关。
我们使用的方法需要从IDENTITY列检索值,新实体插入数据库时,Hibernate动态维护这些列。我们针对新资源写入数据库是在没有指定id(主键)的情况下完成的,改而使用GenerationType.IDENTITY。
这是我们的Spring实体的样子:
Kotlin
@Entity
@Table(name = "entity")
data class Entity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
val metadata: String,
) : TenantEntity()
采用这种策略后,使用ORM来创建和更新现有资源显得非常简单:
- 如果没有传递id,会创建一个新行。
- 如果传递了id,会更新现有行。
是不是听起来很简单?我们也是这么想的!而且似乎效果良好,直到后来我们意识到使用IDENTITY带来了严重的性能问题。这种策略的缺点是批量更新不起作用。
这给我们带来了一个大问题,因为我们的所有实体都使用IDENTITY标识符值生成。对于每个现有的表及对应的实体,我们必须将策略从IDENTITY换成支持批量插入语句的不同策略。
从IDENTITY迁移到基于序列的ID生成
我们研究可用于支持批处理的实体的其他生成类型后,遇到了Hibernate基于序列的标识符值生成。这个策略得到底层数据库序列的支持。Hibernate从序列中请求下一个可用的id,为资源获取新的id。
虽然该策略的底层机制超出了本文的讨论范围,但结论是,这种基于序列的策略将为我们实现批量插入。
现在我们需要弄清楚如何从现有的IDENTITY策略迁移到基于序列的新方法。
进一步调查后,我们意识到现有的表已经有一个Postgres序列。所以如果我们有一个这样定义的表:
SQL
CREATE TABLE IF NOT EXISTS entity (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
...
)
将创建一个名为entity_id_seq的序列!
您可以运行以下SQL命令来检查序列是否存在:
SELECT
*
FROM
pg_sequence
WHERE
seqrelid = 'entity_id_seq'::regclass;
由于我们能够轻松访问Postgres表的序列,因此可以进行非常本地化的更改,改而使用基于序列的策略来生成id。
对于每个实体,我们只需更改几行代码即可解决性能瓶颈。更新后的实体如下所示:
Kotlin
private const val TABLE = "entity"
private const val SEQUENCE = "${TABLE}_id_seq"
(name = TABLE)
data class Entity(
(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
(name = SEQUENCE, sequenceName = SEQUENCE, allocationSize = 50)
(name = "id")
val id: Long? = null,
val metadata: String,
) : TenantEntity()
AllocationSize和序列增量大小
这里需要说明的一点是,Hibernate中的allocationSize属性需要与Postgres中底层序列的增量大小相同。
这是为了让Hibernate和底层序列在它们拥有的id方面“同步”。这还可以防止多台服务器写入到同一个表的分布式架构出现任何问题。
默认情况下,Postgres序列的增量大小为1。我们写了一个非常快速的迁移来更改它,以便与我们的allocationSize匹配:
ALTER SEQUENCE entity_id_seq INCREMENT 50;
现在,Hibernate只需要进行1次调用,即可获取每50次插入的id列表。
它也只需要1次调用即可插入这50行。
以下是我们从这个问题中得出的总结:
- 如使用Hibernate,尽快开始使用基于数据库序列的身份值生成,尤其是在您预见到写入次数会增加的情况下。
- 保持allocationSize和底层Postgres序列增量大小参数相同,避免id冲突,并支持分布式系统。
最后,这是我们实施该更改后RPS从近180变成约90的屏幕截图。
原文标题:Scalable Writes to Postgres With Spring,作者:Aditya Bansal