dtm-labs/dtm

http_workflow_tcc_barrier 某种异常情况下,子事务 rollback 之后又会走到 commit 步骤

zhenlanghuo opened this issue · 26 comments

我发现了 dtm-examples http_workflow_tcc_barrier 例子中的一个异常情况的问题

复现步骤如下

  1. 执行 go run main.go http_workflow_tcc_barrier
  2. TccBTransOutTry 执行成功 (branch_id 01)
  3. 这个时候branch_id 01在 barrier 表中有一条记录为 image
  4. TccBTransInTry 执行失败 (brand_id 02)
  5. 紧接着 workflow 流程会执行 branch_id 01 的 rollback 操作,这个时候branch_id 01在 barrier 表中有两条记录 image
  6. 这个时候停掉程序模拟程序崩溃,branch_id 01的回滚结果甚至都没有注册到dtm服务器
  7. 然后修改 http_workflow_tcc_barrier 的代码:1) 修改gid为上一次执行的gid;2) 设置 TccBTransInTry 执行成功
  8. 执行 go run main.go http_workflow_tcc_barrier,快速模拟dtm发现全局事务超时回调业务服务器重跑事务的过程额
  9. 这个时候,TccBTransOutTry 返回执行成功 (实际没有执行,barrier给拦住了,但是没有返回错误)
  10. 然后 TccBTransInTry 执行成功,这个时候 brand_id 02 在 barrier 表中有一条记录 image
  11. 紧接着 workflow 流程会执行 branch_id 02 的 commit 操作,这个时候 branch_id 02 在 barrier 表中有两条记录 image
  12. 再紧接着 workflow 流程会执行 branch_id 01 的 commit 操作,这个时候 branch_id 01 在 barrier 表中有三条记录 image

也就是 TccBTransOut 分支在 rollback 的情况下,没有重新try的情况,又走到了 commit 的状态,这显然是不正确的

我认为 barrier 需要判断 try 操作之前是执行成功还是被 rollback 了,如果是执行成功就返回成功,如果是被 rollback 了就返回失败,这样重试的逻辑才正确

dtm-examples 的go mod如下

module github.com/dtm-labs/dtm-examples

go 1.15

require (
github.com/dtm-labs/client v1.15.1
github.com/gin-gonic/gin v1.7.7
github.com/go-redis/redis/v8 v8.11.5
github.com/go-resty/resty/v2 v2.7.0
github.com/go-sql-driver/mysql v1.6.0
github.com/lib/pq v1.10.3
github.com/lithammer/shortuuid/v3 v3.0.7
go.mongodb.org/mongo-driver v1.9.1
google.golang.org/grpc v1.47.0
google.golang.org/protobuf v1.28.0
gorm.io/driver/mysql v1.0.3
gorm.io/driver/postgres v1.2.1
gorm.io/gorm v1.22.2
)

// replace github.com/dtm-labs/client/dtmcli => /Users/wangxi/dtm/dtmcli

// replace github.com/dtm-labs/client/dtmgrpc => /Users/wangxi/dtm/dtmgrpc

yedf2 commented
  1. TccBTransInTry 执行失败 (brand_id 02)
  2. 然后修改 http_workflow_tcc_barrier 的代码:1) 修改gid为上一次执行的gid;2) 设置 TccBTransInTry 执行成功

作为分布式应用,需要做到幂等,这个是基本要求。4和7的行为不是幂等行为,因此属于你的业务程序设计问题,而不是dtm的行为。如果业务不幂等,无论dtm怎么做,都无法保证数据一致

  1. TccBTransInTry 执行失败 (brand_id 02)
  2. 然后修改 http_workflow_tcc_barrier 的代码:1) 修改gid为上一次执行的gid;2) 设置 TccBTransInTry 执行成功

作为分布式应用,需要做到幂等,这个是基本要求。4和7的行为不是幂等行为,因此属于你的业务程序设计问题,而不是dtm的行为。如果业务不幂等,无论dtm怎么做,都无法保证数据一致

@yedf2

  1. 使用了barrier不应该就是由barrier来保证幂等吗
  2. 4和7的行为为什么不是幂等,4的失败可能是网络超时失败,实际执行成功的,所以再次执行,7返回成功,这样不是幂等吗
  3. 子事务都回滚了,再次执行子事务的try,返回成功,难道这是正确的吗
yedf2 commented

使用了barrier不应该就是由barrier来保证幂等吗

是的,barrier可以保证幂等,但是你在barrier记录失败之后,又设置TccBTransInTry 执行成功,这就矛盾了,barrier记录失败之后,永远不再会返回成功了

4和7的行为为什么不是幂等,4的失败可能是网络超时失败,实际执行成功的,所以再次执行,7返回成功,这样不是幂等吗

网络超时失败,跟子事务失败回滚完全不同,dtm会重试网络失败,而不是把事务分支记录为失败,

子事务都回滚了,再次执行子事务的try,返回成功,难道这是正确的吗

这个行为不是dtm自发行为,是中间错误的修改了子事务的逻辑造成的

是的,barrier可以保证幂等,但是你在barrier记录失败之后,又设置TccBTransInTry 执行成功,这就矛盾了,barrier记录失败之后,永远不再会返回成功了

假设 TccBTransInTry 是一个扣金币的动作,TccBTransOutTry 成功了,TccBTransInTry失败了(金币不足),然后TccBTransOutRollback,在submit全局事务之前,程序崩溃了(用debug调试运行,在submit全局事务之前退出)。之后重启程序,dtm服务器那边resume,这个时候会重新调用 TccBTransOutTry , barrier会直接返回成功(不会真正执行TccBTransOutTry),TccBTransInTry 此时也成功(此时金币足够了),之后就会执行 TccBTransOutCommit 和 TccBTransInCommit,相当于 TccBTransIn既执行了rollback也执行了commit

image

通过以上的方式手动模拟金币不足返回错误的情况

@yedf2

yedf2 commented

TccBTransInTry失败了(金币不足)

这个事情发生了之后,返回ResultFailure,就不会再返回成功了,即使金币足够了,也只会返回barrier保存的失败

TccBTransInTry失败了(金币不足)

这个事情发生了之后,返回ResultFailure,就不会再返回成功了,即使金币足够了,也只会返回barrier保存的失败

@yedf2 不啊,返回ResultFailure之后,barrier不会保存失败,是rollback的

image image
yedf2 commented

这样吧,你给个复现了问题的可运行的代码例子,而不是你手动的debug改数据,这样描述的问题最清晰最准确,就能够弄清楚怎么回事了

这样吧,你给个复现了问题的可运行的代码例子,而不是你手动的debug改数据,这样描述的问题最清晰最准确,就能够弄清楚怎么回事了

@yedf2 我在dtm-examples上写了一下可以复现问题的代码例子,但是没有权限push分支(从main分支拉出来的分支),无法跟你这边交流

relxet commented

是的,barrier可以保证幂等,但是你在barrier记录失败之后,又设置TccBTransInTry 执行成功,这就矛盾了,barrier记录失败之后,永远不再会返回成功了

假设 TccBTransInTry 是一个扣金币的动作,TccBTransOutTry 成功了,TccBTransInTry失败了(金币不足),然后TccBTransOutRollback,在submit全局事务之前,程序崩溃了(用debug调试运行,在submit全局事务之前退出)。之后重启程序,dtm服务器那边resume,这个时候会重新调用 TccBTransOutTry , barrier会直接返回成功(不会真正执行TccBTransOutTry),TccBTransInTry 此时也成功(此时金币足够了),之后就会执行 TccBTransOutCommit 和 TccBTransInCommit,相当于 TccBTransIn既执行了rollback也执行了commit

image 通过以上的方式手动模拟金币不足返回错误的情况

@yedf2

barrier和业务逻辑用同一个sql.Tx,就不会出现你说的这种情况

是的,barrier可以保证幂等,但是你在barrier记录失败之后,又设置TccBTransInTry 执行成功,这就矛盾了,barrier记录失败之后,永远不再会返回成功了

假设 TccBTransInTry 是一个扣金币的动作,TccBTransOutTry 成功了,TccBTransInTry失败了(金币不足),然后TccBTransOutRollback,在submit全局事务之前,程序崩溃了(用debug调试运行,在submit全局事务之前退出)。之后重启程序,dtm服务器那边resume,这个时候会重新调用 TccBTransOutTry , barrier会直接返回成功(不会真正执行TccBTransOutTry),TccBTransInTry 此时也成功(此时金币足够了),之后就会执行 TccBTransOutCommit 和 TccBTransInCommit,相当于 TccBTransIn既执行了rollback也执行了commit
image
通过以上的方式手动模拟金币不足返回错误的情况
@yedf2

barrier和业务逻辑用同一个sql.Tx,就不会出现你说的这种情况

@relxet 这个写法 barrier和业务逻辑 就是 用同一个sql.Tx 呀

relxet commented

TccBTransOutRollback 在崩溃前,执行成功了没有?如果成功了,dtm恢复以后是直接走rollback流程,而不是像你说的,重新从try再走一遍流程

relxet commented

而且try是客户端触发的,try再执行一次,就是一个新的事务

TccBTransOutRollback 在崩溃前,执行成功了没有?如果成功了,dtm恢复以后是直接走rollback流程,而不是像你说的,重新从try再走一遍流程

TccBTransOutRollback执行成功之后,业务服务器崩溃,全局事务未到终态,dtm服务器会不断得调用resume来重试,业务服务器恢复之后,resume在业务服务器中执行的顺序是从头开始(用之前的全局事务id),调用TccBTransOutTry,此时虽然该全局事务下该事务分支已经有了rollback的记录,但是barrier是返回的成功,所以接下来会继续执行TccBTransInTry,如果这个时候TccBTransInTry执行成功,就会进入到commit的流程

@relxet

而且try是客户端触发的,try再执行一次,就是一个新的事务

resume的话,不是新的事务,是之前的全局事务

@relxet

relxet commented

你搞错了,try是ap端直接call的,tm不会去call try,何来从头开始。ap重新call try,就要注册一个新的gid,就是一个新事物了。

你搞错了,try是ap端直接call的,tm不会去call try,何来从头开始。ap重新call try,就要注册一个新的gid,就是一个新事物了。

@relxet
你有尝试过resume吗,resume是带着原来的全局事务gid,重新跑的
我说的重新跑,是指在ap端重新跑

relxet commented

你一个事务分支的rollback都执行过了,还要让ap从try再跑一遍?你这流程就不是dtm的流程。你就要让dtm把当前这个事务rollback完,ap端重新发起一次新的事务,而不是原来的事务重新从try跑一遍。

你一个事务分支的rollback都执行过了,还要让ap从try再跑一遍?你这流程就不是dtm的流程。你就要让dtm把当前这个事务rollback完,ap端重新发起一次新的事务,而不是原来的事务重新从try跑一遍。

@relxet
这个是workflow这个模式的执行机制啊,不是我定的啊,你有看懂workflow的执行机制吗

relxet commented

tcc协议中定义为commit和rollback必须成功,你不能重新从try跑一遍事务

tcc协议中定义为commit和rollback必须成功,你不能重新从try跑一遍事务

@relxet 你先好好去看下workflow是怎么跑的,如果中间出错了,是怎么重试的

relxet commented

这跟workflow没关系,workflow也是几个协议的组合,saga的补偿操作,Tcc的Confirm/Cancel操作,协议上规定,不能允许失败。

这跟workflow没关系,workflow也是几个协议的组合,saga的补偿操作,Tcc的Confirm/Cancel操作,协议上规定,不能允许失败。

@relxet

  1. 我这里说的就是workflow模式下执行tcc在某些异常场景下有问题,这个问题跟workflow的执行机制有关系,你撇开workflow来说tcc,有啥用
  2. dtm通过resume接口访问业务服务器,让业务服务器继续完成全局事务,官网上是这么写的
image

而我说的业务服务器会重新跑一遍,workflow的process函数就是这么干的,会用原来的gid重新调用TccBTransOutTry,TccBTransOutTry接口通过barrier做幂等、空补偿、防悬挂,barrier在分支事务存在rollback记录的情况下,再次调用try会返回成功,之后就导致我说的情况,所以这里面是更多的是workflow模式的重试机制或者是barrier的返回问题
image

yedf2 commented

@zhenlanghuo 你不需要push到dtm-examples下面,你只需要在你自己的github账号下面弄好了例子和说明,我就能够直接下载复现问题了

@zhenlanghuo 你不需要push到dtm-examples下面,你只需要在你自己的github账号下面弄好了例子和说明,我就能够直接下载复现问题了

@yedf2
我这边找到原因了,之前dtm-examples引用的dtm的版本是v1.15.1,这个版本workflow存在问题:Workflow.getProgress返回的[]*dtmgpb.DtmProgress永远是一个空数组,具体原因是生成的DtmProgressesReply结构体的json tag的字段名是大写开头的,这个在最新版本中已经修复了

@relxet
我这边找到原因了,之前dtm-examples引用的dtm的版本是v1.15.1,这个版本workflow存在问题,导致wf.progresses没有正确设置,所以每次resume都会重新调用TccBTransOutTry

image 执行分支事务的任何阶段,都会调用wf.recordedDoInner接口,这个接口会检查一下该阶段是否已经有保存执行结果,v1.15.1的dtm由于上述的问题,导致每次都会获取不到结果,都需要重新执行