DEV Community

TianZhen
TianZhen

Posted on • Originally published at youth2009.org on

Job queue rollback

背景

我们的异步队列是Tasktiger,一个背后有商业公司支持的开源项目。

之前是Pyres,一个Resqueçš„Pythonå®žçŽ°ï¼Œå› ä¸ºGithub在使用Resque,Instagram在使用Pyres,我们应该可以用一段时间。

后来Pyres长久不再维护,积累了一些bug以及性能问题让我们不得不选择更换一个异步队列系统,期初考虑过Celery,那是一个很成熟很强大的队列系统,以至于我很难一下子搞清它。

问题

在使用Tasktiger后有时会遇到已经处理完毕的task又重新被enqueue的情况,如果所有task都是幂等的(Idempotent),那这也不是问题,第二次执行taskä¼šå› ä¸ºä¹‹å‰å¤„ç†è¿‡è€Œä¸è‡³äºŽå†åšä¸€æ¬¡ã€‚ç„¶è€Œå¹¶ä¸æ˜¯æ‰€æœ‰taskéƒ½å®Œæˆäº†å¹‚ç­‰æ”¹é€ ï¼Œæ‰€ä»¥è¿™å¯¹äºŽæˆ‘ä»¬æ¥è¯´æ˜¯ä¸ªä¸¥é‡é—®é¢˜ï¼Œä½†å½“æ—¶å¹¶ä¸èƒ½ç¨³å®šé‡çŽ°ã€‚

探索

怀疑过许多地方,后来发现在queue redis上执行BGREWRITEAOF就能避免这个问题,于是把原先一个月执行一次的AOFé‡å†™æ“ä½œå˜æˆäº†æ¯å¤©æ‰§è¡Œä¸€æ¬¡ï¼Œå¤§æ¦‚åœ¨å¤‡ä»½æ•°æ®ä¹‹å‰ï¼Œå› ä¸ºå¤‡ä»½éœ€è¦åœæœºï¼Œå¦‚æžœä¸åœ¨è¿™ä¹‹å‰åšï¼Œå¯èƒ½å¤‡ä»½å¯¼è‡´é‡å¯æœåŠ¡å°±ä¼šè®©å¾ˆå¤šå·²ç»å¤„ç†è¿‡çš„task重复处理,例如可能会给会员发多条短信和微信提醒。


å°±åœ¨æ˜¨å¤©ï¼Œå› ä¸ºé‡å¯queue redis导致很多task重复处理,这个问题终究没有被解决,于是花了大约2å¤©æ—¶é—´ï¼Œæ‰¾åˆ°äº†å¯¼è‡´é—®é¢˜çš„æ ¹æºã€‚

之前猜测是不是Tasktiger哪里没写好导致,毕竟这个项目在Github上只有不到500个star,而redisæœ‰å‡ ä¸‡ï¼Œå¹¿å¤§ç”¨æˆ·åŸºç¡€æ›´å®¹æ˜“å‘çŽ°å¥‡æ€ªbug,而在Tasktiger上遇到某个bugï¼Œä¹Ÿè®¸ä½ å°±æ˜¯ä¸–ç•Œä¸Šç¬¬ä¸€ä¸ªé‡åˆ°çš„ã€‚åŸºäºŽè¿™ä¸ªå‡è®¾æˆ‘çœ‹äº†å¾ˆå¤šæºä»£ç ï¼Œæ²¡æœ‰æ‰¾åˆ°å¯é è¯æ®ï¼Œè€Œä¸”åŸºäºŽä¸€ä¸ªç®€å•äº‹å®žï¼šä¸€ä¸ªtask从最开始不存在,到出现在Tasktigerä¸­ï¼Œåˆ°å¤„ç†å®Œè¢«åˆ é™¤ï¼Œå¦‚æžœæ˜¯Tasktigerçš„é—®é¢˜ï¼Œé‚£å®ƒä»Žå“ªé‡Œå¾—åˆ°æ•°æ®æž„é€ å‡ºä¸€ä¸ªtask呢?难道是redis的问题吗?

queue redis一般都会启用AOF,所有修改数据的命令和值被记录到AOF中,等redis重启时,会执行AOFæ–‡ä»¶ä¸­çš„å‘½ä»¤ï¼Œè¿™æ ·rediså°±æœ‰äº†å’Œé‡å¯å‰ä¸€æ¨¡ä¸€æ ·çš„æ•°æ®ã€‚åœ¨AOF中记录着某个task从出现到消失的全过程,难道是redis没完成loadå°±æŽ¥å—æœåŠ¡äº†ï¼Ÿä»Žæ—¥å¿—ä¸Šçœ‹å¹¶ä¸æ˜¯è¿™æ ·ï¼Œåœ¨æ²¡load完执行操作会得到异常。

看了下生产环境的queue状态,基本没有正在处理的task,停掉queue redis,下载AOF文件,在本地让redis启动并load这份AOF,不启动Tasktiger的worker,只静静地看queue的状态,发现load完后有很多task被重新放回了queue,此时如果有worker,一定会重复执行。

到这里难道能确定是redis的问题吗?redisçš„AOF并不能让rediså›žåˆ°ä¹‹å‰çš„çŠ¶æ€ï¼Ÿå¦‚æžœæ˜¯è¿™æ ·çš„è¯redis怎么会被广泛应用?

于是找了一个task,从AOF中看看Tasktiger是如何处理它的。

处理过程没发现异常,只是Tasktiger用了很多lua脚本操作数据,例如有个脚本是:当一个keyä¸åœ¨ç»™å®šçš„æŸå‡ ä¸ªzsetä¸­æ—¶åˆ é™¤è¿™ä¸ªkeyã€‚è¿™ä¸ªæ“ä½œä¸å¤æ‚ï¼Œä½†ä½ æ— æ³•åªç”¨rediså®Œæˆï¼Œå› ä¸ºredisåªæ˜¯ä¸€ä¸ªæ•°æ®å­˜å‚¨ï¼Œæ— æ³•å¤„ç†å¤æ‚é€»è¾‘ï¼Œæ‰€ä»¥åªèƒ½ä½¿ç”¨lua完成,当然很多语都可以完成这个逻辑,只不过redis支持执行lua脚本,算作是对redis操作的补充。

å› ä¸ºæœ‰äº†ä¸€ä¸ªå…·ä½“task,查看和它相关的所有操作后,发现除非是在某一步lua脚本未执行时才会导致这个task被放在active queue中而不是被移除。

äºŽæ˜¯æˆ‘æž„é€ äº†ä¸€ä»½AOFï¼Œæœ€å¼€å§‹çš„éƒ¨åˆ†æ˜¯ä¸€å †SCRIPT LOAD指令,把Tasktiger需要的脚本都load到redisä¸­ï¼Œè¿™æ ·åŽç»­æ“ä½œæ‰å¯ä»¥ä½¿ç”¨EVALSHAï¼Œè¿™ä»½æž„é€ å¥½çš„AOF也通过了redis-check-aof检查,被load后再次查看queue状态,竟然正常了,和停redis之前一致!看来问题出在执行这些脚本的地方。

在redis中执行lua脚本有两种做法,第一种是使用EVALï¼Œè¿™ç§åšæ³•æ˜¯æŠŠè„šæœ¬å†…å®¹ä¸€å¹¶è´´ä¸Šï¼Œæ¯æ¬¡æ‰§è¡Œéƒ½éœ€è¦è¿™æ ·ï¼Œå¦‚æžœæ€»æ˜¯é‡å¤æ‰§è¡ŒæŸä¸ªç‰¹å®šè„šæœ¬å°±ä¼šå› ä¸ºä¼ è¾“äº†å¤§é‡è„šæœ¬å†…å®¹è€Œä½¿å¾—æ•ˆçŽ‡ä¸å¤Ÿé«˜ï¼ŒäºŽæ˜¯æœ‰äº†ç¬¬äºŒç§æ‰§è¡Œæ–¹æ³•––EVALSHA,使用这种需要脚本的SHA1,当执行SCRIPT LOAD后,redis返回的内容就是脚本的SHA1,load过的脚本在手工flush前或者redis重启前都是有效的。SCRIPT LOAD在BGREWRITEAOFåŽå¹¶ä¸ä¼šå­˜åœ¨ï¼Œè™½ç„¶ä½ è¿˜æ˜¯å¯ä»¥é€šè¿‡EVALSHA调用脚本。

那么这两种执行方法对于AOF文件记录内容有差别吗?没有,执行EVALSHA也会被记录成EVALï¼Œå¦‚æžœä½ åœ¨AOF中发现有EVALSHA那就奇怪了。在生产环境下载的AOF中,有不少EVALSHA,为何没有被记录成EVALï¼Ÿå› ä¸ºè¿™äº›EVALSHA并不是redis执行的,而是Tasktigerçš„pipeline

å¯¼è‡´å‡ºçŽ°å›žæ»šé—®é¢˜çš„åŽŸå› æ˜¯ï¼šBGREWRITEAOF后,由于SCRIPT LOAD被去掉,redis重启后如果有使用EVALSHAè°ƒç”¨è„šæœ¬çš„åœ°æ–¹ä¼šå‡ºé”™ï¼Œå› ä¸ºé‚£ä¸ªè„šæœ¬å¹¶æ²¡æœ‰è¢«redis load

不是说EVALSHA会被翻译成EVAL放在AOF中吗?是的,不过Tasktiger自己实现了一个redis pipeline,这也是个lua脚本,在这个lua脚本中是需要调用其他lua脚本的。调用方法包含两种,大多数都是EVALSHA, 由于redis禁止在lua中使用redis.call执行EVALSHA调用另一个脚本(并不是所有redis命令都可以在redis.call使用) ,Tasktiger使用了其他方法来调用,算是一种hack。

由于redisçš„é™åˆ¶åŠ ä¸ŠTasktigerçš„hack,导致 在BGREWRITEAOF后,如果有pipeline脚本的调用发生,下次重启redis会使得之前做过的task被重新放回queueï¼Œå› ä¸ºç§»é™¤æ“ä½œçš„è„šæœ¬è¿˜æ²¡æœ‰load,所以移除失败了,看起来就像是task被重新放回queue

目前我们针对这个问题的解决方法是:勤快点BGREWRITEAOF,每小时一次,重启redis之前再做一次

Top comments (0)