Redis 几乎每小时都出现大量超时,求助

65 天前
 drymonfidelia

报错信息是 Timeout awaiting response (outbound=1KiB, inbound=0KiB, 5728ms elapsed, timeout is 5000ms), command=SET, next: EVAL, inst: 0, qu: 0, qs: 15, aw: False, bw: SpinningDown, rs: ReadAsync, ws: Idle, in: 76, in-pipe: 0, out-pipe: 0, serverEndpoint: 127.0.0.1:6379, mc: 1/1/0, mgr: 10 of 10 available, clientName: AppProductionEnvServer1(SE.Redis- v2.5.43.42402), IOCP: (Busy=0,Free=10000,Min=9000,Max=10000), WORKER: (Busy=236,Free=32531,Min=10000,Max=32767), POOL: (Threads=236,Queueditems=50,Completeditems=8751117254), v: 2.5.43.42402 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts)

出错的时候看了下也就三百多并发,比较怀疑是下面这段代码引起的:

   while (!await redis.GetDatabase().LockTakeAsync($"PlaceOrder:{user.UserId}", "1", TimeSpan.FromSeconds(180)))
        {
            await Task.Delay(1);
        }

作用是确保同一用户只有一个订单未写入数据库(系统下单逻辑涉及几十个函数,全是一些莫名奇妙的判断逻辑,混淆后可读性大幅提升的那种(当然是开玩笑的)),屎山作者已经跑路了,没人能看懂他代码,一个用户下多个订单数据会混乱。更牛逼的这套系统除了性能极差,运行 3 年没出错一次。一次调用 API 只能下一单,客户端随硬件交付,已经写死了,不能更新,然后客户端一次多少个订单就多少并发调用 API 提交,没有队列功能。目前要求 500 订单 10 秒内全部下单完成返回订单号(单独提交的话每个订单 0.01 秒左右能写入完数据拿到订单号)。

预分配订单号行不通,不运行一遍这部分屎山代码不能确定这个订单能不能提交,返回订单号就代表这个订单提交成功了,不能取消。目前打算改造成 Sub/Pub ,不知道能不能提升性能,或者 V 友有没有更好的改造方案?只要能让这屎山跑起来就行,代码多脏都没关系,改动需要尽可能小,不能把系统改炸。目前加硬件到 256GB 内存都没解决。

5798 次点击
所在节点    Redis
67 条回复
sagaxu
65 天前
qs: 15
Queueditems=50
codegenerator
65 天前
预算多少?资深架构师应战
sujin190
65 天前
这个 LockTakeAsync 看下底层实现也就一个 setnx 指令,这请求量 redis 层面不可能超时吧,所以超时肯定是软件这边的问题了,线程池这么高不合理,要么是 redis 连接管理问题要么是 await 调度有死锁了吧,而且怎么记得 Task.Delay(1);似乎是延时 1 毫秒吧,这么短不合理也没必要吧,也很容易导致死锁和并发异常什么的,好歹延时个 50 毫秒吧

话说以用户 ID 加锁,那么客户端批量是统一用户的? redis 的加锁 req 和 resp 协议加锁确实费劲,要不换个其它加锁服务试试?

300tps 要用 256GB 服务器内存真豪啊!!!
drymonfidelia
65 天前
@sujin190 我也觉得是这个 1ms delay 不合理,但是延时 50ms ,500 订单就浪费了至少 25 秒,没办法实现 10 秒内完成下单
每个客户端同时只能登录一个用户,所以以用户 ID 加锁没问题
drymonfidelia
65 天前
慢日志排查过,是空的
sujin190
65 天前
@drymonfidelia #4 看流出流量没有,流入只有 75 字节,IOCP 空闲状态但是线程池很高,估计不是连接管理有啥异常就是 await 调度异常了,想要解决只能慢慢加日志压测调试了,Sub/Pub 也算一种可行方案了吧,只不过实现起来就麻烦多了

不知道你们部署环境是啥,不介意加新服务的话,可以考虑下我们做的原子操作服务来提供加锁

https://github.com/snower/slock.git

有.net 的 sdk ,只需要换一下你这个加锁的逻辑就好,因为全异步的通信协议,所以不需要循环服务会主动异步通知,我们也在生产环境用很多年了,只是加锁没遇到啥问题,就你这请求量大概需要 100M 内存吧,也支持集群高可用模式
sujin190
65 天前
drymonfidelia
65 天前
@sujin190 谢谢,我看一下
javalaw2010
65 天前
我瞎说啊,盲猜,会不是是有定时的 redis 备份比如 BGSAVE 命令导致主进程阻塞了,从而导致了客户端超时
drymonfidelia
65 天前
@javalaw2010 没有,redis 里面全是锁和缓存,丢了重启就好,不需要备份
lsk569937453
65 天前
await redis.GetDatabase().LockTakeAsync($"PlaceOrder:{user.UserId}", "1", TimeSpan.FromSeconds(180))

你这个是获取 redis 锁吧。如果方法返回的 true ,则获取到锁;反之,则循环获取锁。在循环里等待 1ms ,这个是不是太短了。

https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.delay?view=net-8.0#system-threading-tasks-task-delay(system-int32)
keakon
65 天前
根据你的业务逻辑重新实现一遍呗,弄懂屎山反而更难。
chenqh
65 天前
你这个 lock release 在哪里?
drymonfidelia
65 天前
@lsk569937453 是,但是我需要确保前一个锁释放到加下一个锁的间隔尽可能最短。目前我的想法是如果没加锁成功,Sub 一个锁名称的 Channel ,每个锁释放就 Pub 一条消息,Sub 收到消息就再尝试加锁
drymonfidelia
65 天前
@keakon 当时的需求文档已经没了。我觉得这么混乱的逻辑他能弄到没问题还是有点水平的
@chenqh 在下单结束的时候
drymonfidelia
65 天前
@drymonfidelia #14 但是不知道这样弄性能会不会更差,一条 SET 命令能解决的问题我要 SUB 几百次
dynastysea
65 天前
这就是不用云服务的缺点,上云了直接甩工单给云厂商就行了
chenqh
65 天前
@dynastysea 这和云有什么关系这明显

是 lock 之后,按顺序执行任务 500 个超过 10S 了,无解吧
drymonfidelia
65 天前
@chenqh 每个任务结束后都要释放锁才能进入下一个任务,我觉得问题出在频繁加锁上。
chenqh
65 天前
@drymonfidelia 感觉几乎无解了,你这个就是加需要加上去的,

比如第一版需求是: 做个下单任务。OK ,做完了

然后第二版需求是: 同一个 userid ,同一时间只能下一单,可能是为了方便算什么东西,能怎么办呢,在外面套个 lock 咯。

现在你第三版需求: 加了 lock 性能不行了啊。你来提升下性能。。。

反正就我这种菜 B 脑袋想不出来。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1034586

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX