Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
zhenlanghuo opened this issue Jul 27, 2023 · 26 comments

Comments

@zhenlanghuo
Copy link

zhenlanghuo commented Jul 27, 2023

我发现了 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 了就返回失败,这样重试的逻辑才正确

@zhenlanghuo
Copy link
Author

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

@zhenlanghuo zhenlanghuo changed the title 关于 dtm-examples 中的 http_workflow_tcc_barrier 例子的问题 http_workflow_tcc_barrier 某种异常情况下,子事务 rollback 之后又会走到 commit 步骤 Jul 28, 2023
@yedf2
Copy link
Contributor

yedf2 commented Aug 10, 2023

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

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

@zhenlanghuo
Copy link
Author

zhenlanghuo commented Aug 10, 2023

  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
Copy link
Contributor

yedf2 commented Aug 11, 2023

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

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

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

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

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

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

@zhenlanghuo
Copy link
Author

zhenlanghuo commented Aug 22, 2023

是的,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
Copy link
Contributor

yedf2 commented Aug 22, 2023

TccBTransInTry失败了(金币不足)

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

@zhenlanghuo
Copy link
Author

TccBTransInTry失败了(金币不足)

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

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

image image

@yedf2
Copy link
Contributor

yedf2 commented Aug 22, 2023

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

@zhenlanghuo
Copy link
Author

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

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

@relxet
Copy link

relxet commented Sep 3, 2023

是的,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,就不会出现你说的这种情况

@zhenlanghuo
Copy link
Author

是的,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
Copy link

relxet commented Sep 3, 2023

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

@relxet
Copy link

relxet commented Sep 3, 2023

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

@zhenlanghuo
Copy link
Author

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

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

@relxet

@zhenlanghuo
Copy link
Author

zhenlanghuo commented Sep 3, 2023

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

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

@relxet

@relxet
Copy link

relxet commented Sep 3, 2023

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

@zhenlanghuo
Copy link
Author

zhenlanghuo commented Sep 3, 2023

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

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

@relxet
Copy link

relxet commented Sep 3, 2023

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

@zhenlanghuo
Copy link
Author

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

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

@relxet
Copy link

relxet commented Sep 3, 2023

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

@zhenlanghuo
Copy link
Author

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

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

@relxet
Copy link

relxet commented Sep 3, 2023

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

@zhenlanghuo
Copy link
Author

这跟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
Copy link
Contributor

yedf2 commented Sep 3, 2023

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

@zhenlanghuo
Copy link
Author

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

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

@zhenlanghuo
Copy link
Author

zhenlanghuo commented Sep 6, 2023

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants