暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

【译】对于乐观锁和透明故障转移的重试逻辑

原创 2022-06-07
876

原文地址:Retry logic for optimistic locking and transparent failover
原文作者:Franck Pachot

下面是一个小样例,用于演示必须实现的重试逻辑,以便处理数据库中的事务冲突或透明故障转移错误。目标是能够在笔记本电脑上进行一些实验,可惜,没有更好的解决方案。

配置 PostgreSQL 数据库

启动本地 PostgreSQL:

docker run --name pg -e POSTGRES_PASSWORD=franck -p 5432:5432 -d postgres

创建一个表(franck),插入10行数据:

PGUSER=postgres PGPASSWORD=franck PGHOST=localhost PGPORT=5432  psql -e <<SQL
create table franck(id int primary key, val int);
insert into franck select generate_series(1,10),0;
SQL
create table franck(id int primary key, val int)
CREATE TABLE

insert into franck select generate_series(1,10),0;
INSERT 0 10

获取YBDemo

我使用一个非常简单的程序在线程中运行查询,
下载YBDemo.jar,此jar包中包括PostgreSQL和YugabyteDB JDBC驱动程序以及Hikari.properties参数文件:

curl -Ls https://github.com/FranckPachot/ybdemo/releases/download/v0.0.1/YBDemo-0.0.1-SNAPSHOT-jar-with-dependencies.jar > YBDemo.jar
curl -Ls https://jdbc.postgresql.org/download/postgresql-42.3.2.jar > postgresql.jar

我在 Hikary.properties参数文件中设置了连接参数:

cat > hikari.properties <<'INI'
dataSourceClassName=org.postgresql.ds.PGSimpleDataSource
dataSource.url=jdbc:postgresql://localhost:5432/postgres?user=postgres&password=franck
INI

使用for循环,开启5个线程,对表的每一行进行更新操作。

for i in {1..5} ; do
 echo "update franck set val=val+1 returning val+1"
done | java -jar YBDemo.jar

这是一个非常简单的示例,更新fracnk表的每一行,使值val=val+1,从多个线程运行,以显示事务冲突。当然,如果您的应用程序中有此内容,也可以使用自己的程序。但实验室的目标是分析简单的事情,以便充分理解。

Read committed

在PostgreSQL 中的默认隔离级别是 Read Commited(提交读)。您通过我的程序可以快速检查是否是提交读:

java -jar YBDemo.jar <<< "select current_setting('transaction_isolation')"

因此,如果您运行下面程序至少3个线程,您将看到一些事务冲突错误:

update franck set val=val+1 returning val+1#

以下为冲突报错

 Thread-2      5 ms: 17735
 Thread-0      5 ms: 17736
 Thread-1      5 ms: 17737

update franck set val=val+1 returning val+1
Error in thread  Thread-0   1016 ms SQLSTATE(40P01) - retry 0/10
org.postgresql.util.PSQLException: ERROR: deadlock detected
  Detail: Process 180 waits for ShareLock on transaction 20923; blocked by process 182.
Process 182 waits for ShareLock on transaction 20926; blocked by process 180.
  Hint: See server log for query details.
  Where: while rechecking updated tuple (11,5) in relation "franck"
 wait in thread  Thread-0     15 ms after   0 retries

update franck set val=val+1 returning val+1
Error in thread  Thread-1   1005 ms SQLSTATE(40P01) - retry 0/10
org.postgresql.util.PSQLException: ERROR: deadlock detected
  Detail: Process 181 waits for ShareLock on transaction 20989; blocked by process 182.
Process 182 waits for ShareLock on transaction 20988; blocked by process 181.
  Hint: See server log for query details.
  Where: while rechecking updated tuple (9,21) in relation "franck"
 wait in thread  Thread-1     10 ms after   0 retries

我运行此示例是为了清楚地表明,即使在 Read Commit (提交读)隔离级别,使用一个简单的 SQL 语句时,也可能发生事务冲突,并且应用程序必须使用重试逻辑来处理它。
为什么选择重试逻辑?因为您不想停止应用程序,也不想忽略不成功的事务。顺便说一句,这是我的YBDemo.java的主要目标。

因此,无论隔离级别如何,SQLSTATE 40P01 (deadlock_detected) 都必须由高可用性应用程序重试。

Serializable

在Serializable隔离级别中,可以预期会出现更多的事务冲突,因为可以保证整个事务的读/写稳定性。

在参数文件hikari.properties中添加以下参数,以设置每个事务的隔离级别:

cat >> hikari.properties <<'INI'
connectionInitSql= set default_transaction_isolation=serializable;
INI

再次运行以下程序:

for i in {1..5} ; do
 echo "update franck set val=val+1 returning val+1"
done | java -jar YBDemo.jar

可以看到像下面一样的错误。

 Thread-3      2 ms: 153703
 Thread-3      2 ms: 153704
 Thread-3      2 ms: 153705
 wait in thread  Thread-0   2567 ms after   8 retries
 Thread-3      2 ms: 153706

update franck set val=val+1 returning val+1
Error in thread  Thread-0      2 ms SQLSTATE(40001) - retry 9/10
org.postgresql.util.PSQLException: ERROR: could not serialize access due to concurrent update
 Thread-3      3 ms: 153707
 Thread-3      2 ms: 153708
 Thread-3      2 ms: 153709
 Thread-3      2 ms: 153710

This one was at the limit because my YBDemo.java fails above 10 retries. This happened quickly:
在YBDemo.java中设置了10次失败重试,程序很快就结束了。

Error in thread  Thread-2      2 ms SQLSTATE(40001) - retry 0/10
 wait in thread  Thread-2     10 ms after   0 retries
Error in thread  Thread-2      2 ms SQLSTATE(40001) - retry 1/10
 wait in thread  Thread-2     29 ms after   1 retries
Error in thread  Thread-2      2 ms SQLSTATE(40001) - retry 2/10
 wait in thread  Thread-2     41 ms after   2 retries
Error in thread  Thread-2      2 ms SQLSTATE(40001) - retry 3/10
 wait in thread  Thread-2     86 ms after   3 retries
Error in thread  Thread-2      2 ms SQLSTATE(40001) - retry 4/10
 wait in thread  Thread-2    164 ms after   4 retries
Error in thread  Thread-2      1 ms SQLSTATE(40001) - retry 5/10
 wait in thread  Thread-2    328 ms after   5 retries
Error in thread  Thread-2      1 ms SQLSTATE(40001) - retry 6/10
 wait in thread  Thread-2    641 ms after   6 retries
Error in thread  Thread-2      3 ms SQLSTATE(40001) - retry 7/10
 wait in thread  Thread-2   1289 ms after   7 retries
Error in thread  Thread-2      4 ms SQLSTATE(40001) - retry 8/10
 wait in thread  Thread-2   2567 ms after   8 retries
Error in thread  Thread-2      3 ms SQLSTATE(40001) - retry 9/10
 wait in thread  Thread-2   5125 ms after   9 retries
Error in thread  Thread-2      2 ms SQLSTATE(40001) - retry 10/10

我使用exponential backoff(指数补偿、指数退避)方式来重试,你可以在代码中看到怎样实现。最后一个等待了5秒钟。

YugabyteDB

在分布式数据库中,由乐观锁的规则造成的重试的错误,可能由于多种原因而发生。在电脑上,使用docker-compose工具,使用yb-lab参数文件,启动3个服务组成一个分布式集群。

image.png

我创建了演示表(docker yb-tserver-0服务重定向本地端口为:5433 ):

psql -p 5433 -U yugabyte -d yugabyte <<SQL
create table franck(id int primary key, val int);
insert into franck select generate_series(1,10),0;
SQL

并为其设置连接参数:

cat > hikari.properties <<INI
dataSourceClassName=com.yugabyte.ysql.YBClusterAwareDataSource
dataSource.url=jdbc:yugabytedb://localhost:5433/yugabyte?user=yugabyte&load-balance=false
connectionInitSql= set default_transaction_isolation=serializable;
INI

当然,我看到一些 serialization错误,但未达到最大重试次数:

 Thread-3     17 ms: 1474
 Thread-3     17 ms: 1475
 Thread-3     17 ms: 1476
 Thread-3     17 ms: 1477
 Thread-3     18 ms: 1478
 Thread-3     17 ms: 1479
 Thread-3     18 ms: 1480
 Thread-3     17 ms: 1481

update franck set val=val+1 returning val+1
Error in thread  Thread-0   6541 ms SQLSTATE(40001) - retry 1/10
com.yugabyte.util.PSQLException: ERROR: Operation failed. Try again.: [Operation failed. Try again. (yb/tablet/running_transaction.cc:456): Transaction aborted: 9ced2fc8-06d2-490c-b400-732e53013a56 (pgsql error 40001)]
 Thread-3     18 ms: 1482
 wait in thread  Thread-0     21 ms after   1 retries
 Thread-3     18 ms: 1483
 Thread-0     20 ms: 1484
 Thread-0     16 ms: 1485
 Thread-0     18 ms: 1486
 Thread-0     20 ms: 1487
 Thread-0     19 ms: 1488
 Thread-0     19 ms: 1489
 Thread-0     18 ms: 1490
 Thread-0     17 ms: 1491

运行一小时后,我得到了两个线程,达到9次重试。

因此,当使用Serializable隔离级别时,错误SQLSTATE 40001 (serialization_failure)必须进行重试,并应针对您的系统调整最大重试次数(以及重试之间的等待时间)。

高可用性

在上一个运行时,使用docker-compose的启停功能,对yb-tserver-2服务进行重启,等待几秒钟后(因为在停止节点中的主节点的服务后,必须有一个从节点被选为新的主节点)。但仍然只显示 SQLSTATE 40001。

但是,在之前的测试中,我只连接到yb-tserver-0。集群负载均衡功能是,当连接到一个节点后,当我连接的节点关闭时,应用程序将会连接到其他节点来继续提供服务(这要归功于连接池和集群感知 JDBC 驱动程序)。暂时性错误也可以由应用程序处理。

 Thread-0     22 ms: 59648^M
 Thread-0     22 ms: 59649^M
 Thread-0     29 ms: 59650^M
53171 [Thread-3] WARN com.zaxxer.hikari.pool.ProxyConnection - HikariPool-1 - Connection com.yugabyte.jdbc.PgConnection@61336f01 marked as broken because of SQLSTATE(08006), ErrorCode(0)^M
53171 [Thread-0] WARN com.zaxxer.hikari.pool.ProxyConnection - HikariPool-1 - Connection com.yugabyte.jdbc.PgConnection@3b373f9c marked as broken because of SQLSTATE(08006), ErrorCode(0)^M
53171 [Thread-2] WARN com.zaxxer.hikari.pool.ProxyConnection - HikariPool-1 - Connection com.yugabyte.jdbc.PgConnection@2abffd27 marked as broken because of SQLSTATE(08006), ErrorCode(0)^M
53171 [Thread-4] WARN com.zaxxer.hikari.pool.ProxyConnection - HikariPool-1 - Connection com.yugabyte.jdbc.PgConnection@39961dd8 marked as broken because of SQLSTATE(08006), ErrorCode(0)^M
53171 [Thread-1] WARN com.zaxxer.hikari.pool.ProxyConnection - HikariPool-1 - Connection com.yugabyte.jdbc.PgConnection@683b2ab4 marked as broken because of SQLSTATE(08006), ErrorCode(0)^M
com.yugabyte.util.PSQLException: An I/O error occurred while sending to the backend.^M
  at com.yugabyte.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:382)^M
  at com.yugabyte.jdbc.PgStatement.executeInternal(PgStatement.java:490)^M
  at com.yugabyte.jdbc.PgStatement.execute(PgStatement.java:408)^M
  at com.yugabyte.jdbc.PgStatement.executeWithFlags(PgStatement.java:329)^M
  at com.yugabyte.jdbc.PgStatement.executeCachedSql(PgStatement.java:315)^M
  at com.yugabyte.jdbc.PgStatement.executeWithFlags(PgStatement.java:291)^M
  at com.yugabyte.jdbc.PgStatement.executeQuery(PgStatement.java:243)^M
  at com.zaxxer.hikari.pool.ProxyStatement.executeQuery(ProxyStatement.java:110)^M
  at com.zaxxer.hikari.pool.HikariProxyStatement.executeQuery(HikariProxyStatement.java)^M
  at YBDemo.run(YBDemo.java:38)^M\

错误 SQLSTATE 08006 (connection_failure)可能被重试。无需等待,因为在 YugabyteDB数据库中,其他节点立即可用。

在日志中也可以看到这些错误信息:

yb-lab-yb-demo-write-1  | Error in thread  Thread-1   1589ms SQLSTATE(XX000) - retry 0/5
yb-lab-yb-demo-write-1  | com.yugabyte.util.PSQLException: ERROR: no owned sequence found
yb-lab-yb-demo-write-1  |   Where: Catalog Version Mismatch: A DDL occurred while processing this query. Try again.

当测试不同的故障转移情况时,我决定重试“XX000”。

重新连接还可能获得一个 SQLTransientConnectionException,该异常也会重试,而无需等待,因为连接池已经具有超时设置

很难提供一些通用代码来处理这个问题。如果这么简单,这将在数据库或驱动程序中自动进行。YugabyteDB已经出现过很多冲突自动重试的情况。但某些情况必须由应用程序处理:

  • 您是否执行了一些非事务性操作,这些操作有待完成两次的风险(例如:发送电子邮件、通知…)
  • 您确定在提交更改之前收到连接错误,还是需要在重试之前进行检查?

总结

任何 SQL 数据库(因为它是共享的,并且保证 ACID 属性以避免数据损坏)都可能引发事务冲突。某些情况可以自动重试。它们的唯一后果是延迟较高,但考虑到它发生的可能性较低,因此在需要扩展场景下,它比悲观锁定更好。对于数据库不知道的应用程序逻辑,必须重试其他一些情况,尤其是如果没有将所有数据库操作封装到存储过程中。

最后修改时间:2022-06-07 10:17:23
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论