报错信息是 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 内存都没解决。
1
sagaxu 247 天前
qs: 15
Queueditems=50 |
2
codegenerator 247 天前
预算多少?资深架构师应战
|
3
sujin190 247 天前
这个 LockTakeAsync 看下底层实现也就一个 setnx 指令,这请求量 redis 层面不可能超时吧,所以超时肯定是软件这边的问题了,线程池这么高不合理,要么是 redis 连接管理问题要么是 await 调度有死锁了吧,而且怎么记得 Task.Delay(1);似乎是延时 1 毫秒吧,这么短不合理也没必要吧,也很容易导致死锁和并发异常什么的,好歹延时个 50 毫秒吧
话说以用户 ID 加锁,那么客户端批量是统一用户的? redis 的加锁 req 和 resp 协议加锁确实费劲,要不换个其它加锁服务试试? 300tps 要用 256GB 服务器内存真豪啊!!! |
4
drymonfidelia OP @sujin190 我也觉得是这个 1ms delay 不合理,但是延时 50ms ,500 订单就浪费了至少 25 秒,没办法实现 10 秒内完成下单
每个客户端同时只能登录一个用户,所以以用户 ID 加锁没问题 |
5
drymonfidelia OP 慢日志排查过,是空的
|
6
sujin190 247 天前
@drymonfidelia #4 看流出流量没有,流入只有 75 字节,IOCP 空闲状态但是线程池很高,估计不是连接管理有啥异常就是 await 调度异常了,想要解决只能慢慢加日志压测调试了,Sub/Pub 也算一种可行方案了吧,只不过实现起来就麻烦多了
不知道你们部署环境是啥,不介意加新服务的话,可以考虑下我们做的原子操作服务来提供加锁 https://github.com/snower/slock.git 有.net 的 sdk ,只需要换一下你这个加锁的逻辑就好,因为全异步的通信协议,所以不需要循环服务会主动异步通知,我们也在生产环境用很多年了,只是加锁没遇到啥问题,就你这请求量大概需要 100M 内存吧,也支持集群高可用模式 |
7
sujin190 247 天前
.net sdk 在这 https://github.com/snower/slock4net
|
8
drymonfidelia OP @sujin190 谢谢,我看一下
|
9
javalaw2010 246 天前
我瞎说啊,盲猜,会不是是有定时的 redis 备份比如 BGSAVE 命令导致主进程阻塞了,从而导致了客户端超时
|
10
drymonfidelia OP @javalaw2010 没有,redis 里面全是锁和缓存,丢了重启就好,不需要备份
|
11
lsk569937453 246 天前
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) |
12
keakon 246 天前
根据你的业务逻辑重新实现一遍呗,弄懂屎山反而更难。
|
13
chenqh 246 天前
你这个 lock release 在哪里?
|
14
drymonfidelia OP @lsk569937453 是,但是我需要确保前一个锁释放到加下一个锁的间隔尽可能最短。目前我的想法是如果没加锁成功,Sub 一个锁名称的 Channel ,每个锁释放就 Pub 一条消息,Sub 收到消息就再尝试加锁
|
15
drymonfidelia OP |
16
drymonfidelia OP @drymonfidelia #14 但是不知道这样弄性能会不会更差,一条 SET 命令能解决的问题我要 SUB 几百次
|
17
dynastysea 246 天前
这就是不用云服务的缺点,上云了直接甩工单给云厂商就行了
|
18
chenqh 246 天前
|
19
drymonfidelia OP @chenqh 每个任务结束后都要释放锁才能进入下一个任务,我觉得问题出在频繁加锁上。
|
20
chenqh 246 天前
@drymonfidelia 感觉几乎无解了,你这个就是加需要加上去的,
比如第一版需求是: 做个下单任务。OK ,做完了 然后第二版需求是: 同一个 userid ,同一时间只能下一单,可能是为了方便算什么东西,能怎么办呢,在外面套个 lock 咯。 现在你第三版需求: 加了 lock 性能不行了啊。你来提升下性能。。。 反正就我这种菜 B 脑袋想不出来。 |
21
chenqh 246 天前
@drymonfidelia 你要不加个 log 看一下 lock 的耗时时间?
|
22
sagaxu 246 天前
“客户端一次多少个订单就多少并发调用 API 提交”
如果都是同一个 userId ,或者有锁其它共同的 id ,并发高的时候,可能会出现饥饿的情况,每次 unlock 都被别的 lock 抢去了,如果下单性能稳定,超时时长改成 10 秒,从头饿到尾也轮到了 |
23
dynastysea 246 天前
@chenqh 因为作者分析不出原因,我的意思是这种也可以叫云厂商的帮忙协助定位
|
24
chenqh 246 天前
@dynastysea 云厂商还帮忙做这个的吗?我无知了。
|
25
lsk569937453 246 天前
1.不要换成 Sub/Pub,性能上不会有提升,架构更复杂了。
2.提高 redis 的超时时间。 redis 的 client 超时时间可能是全局线程池繁忙导致的,也可以是 redis 服务端导致的。redis 可以加一下监控,看有没有大 key/热 key 查询。 |
26
chunworkhard 246 天前
学习下
|
27
drymonfidelia OP @sagaxu 但是这个 timeout 似乎是 redis 被卡死了,一次加锁尝试一直没返回结果
|
28
drymonfidelia OP @lsk569937453 redis 服务端不是单线程运行的么,客户端从上面的报错信息看线程池还有很大空闲
|
29
lsk569937453 246 天前
|
30
8355 246 天前
@dynastysea #23 这是应用问题,是代码写成这样,遇到屎山问题我没脸提工单。我怕人家让我提供代码我截出来丢老脸。
|
31
edward1987 246 天前
delay + random(1,20)试试? 可以少试 10 倍请求,随机过后不容易有空闲或堵塞
|
32
sagaxu 246 天前
300 个并发,每个并发每秒 1000 次请求,你算算看 QPS
|
33
gaogang 246 天前
循环里面 delay 的带短了吧
拿 redis 锁之前 加个本地锁 应该会好点 |
34
drymonfidelia OP @sagaxu 不能直接乘吧,拿到锁的请求就不会继续申请锁了
|
35
drymonfidelia OP @drymonfidelia 另外 1ms 也不一定能拿到锁
|
36
i8086 246 天前
这个错误信息,没什么问题,毕竟都用了异步 IOCP 也是空闲。
如果有监控且是单机 redis ,那就查查 redis 当时的连接数是不是爆了,首行提示 Timeout awaiting response 。 |
37
8355 246 天前
我猜测了一下你的代码上下文,通过首行业务加并发锁,并且在执行到最后一个业务的时候内部有一个解锁指令。
加了个 180 秒的锁,执行到最后应该执行 del 解锁。 可以把你的订单内容,产品+数量之类的编个字符串算个 hash 加入到 rediskey 中,这样的话相同的产品并发下单才会触发锁,而不同产品下单不会受到影响。 这样改的前提是你需要把整个执行流程看一遍确认只有 2 个位置(也有可能只有一个)有锁操作,如果其他位置有锁检查一定要再看下代码逻辑。 我觉得这是最低成本改动,不牵扯到原有逻辑,可以通过搜索去检查该 key 的应用次数。 |
38
sagaxu 246 天前
@drymonfidelia 300 个只有一个拿到了锁,其它的都拿不到啊
|
39
sunjiayao 246 天前
加锁和解锁的地方都加下日志看看 应该是死锁了
|
40
zhy0216 246 天前 via Android
增加重试时间啊 1ms 这瓶颈是 cpu 了 你加内存什么用
redis 单线程还不能利用多核优势 |
42
antli 246 天前
考虑到此信息,我们强烈建议客户将 IOCP 和辅助角色线程的最小配置值设置为大于默认值。 我们无法提供有关此值应是多少的通用指导,因为一个应用程序的合适值对于另一个应用程序可能会太高或太低。 此设置还可能会影响复杂应用程序其他部分的性能,因此每个客户需要按照其特定需求来微调此设置。 开始时设置为 200 或 300 会比较好,随后可进行测试并根据需要进行调整。
如何配置此设置: 建议使用 global.asax.cs 中的 ThreadPool.SetMinThreads (...) 方法,以编程方式更改此设置。 例如: C#复制 private readonly int minThreads = 200; void Application_Start(object sender, EventArgs e) { // Code that runs on application startup AreaRegistration.RegisterAllAreas(); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); ThreadPool.SetMinThreads(minThreads, minThreads); } 备注 此 方法指定的值是全局设置,将影响整个 AppDomain 。 例如,如果已有 4 核计算机,并想要在运行时将 minWorkerThreads 和 minIoThreads 设置为 50 (每个 CPU ),请使用 ThreadPool.SetMinThreads(200, 200)。 还可以使用 Machine.config 中 <processModel> 配置元素下的 minIoThreads 或 minWorkerThreads 配置设置来指定最小线程设置。Machine.config 通常位于 %SystemRoot%\Microsoft.NET\Framework\[versionNumber]\CONFIG\。 不建议以这种方式设置最小线程数,因为这是系统范围设置。 备注 此配置元素中指定的值是按核心设置。 例如,如果使用 4 核计算机,并且希望 minIoThreads 设置在运行时为 200 ,则使用 <processModel minIoThreads="50"/>。 |
43
wccc 246 天前
还是修改锁的实现, 可重入
|
44
rnv 246 天前
是不是惊群了,每次锁空闲会唤起一大批在等待的,但只有一个拿到了锁
|
45
zhuisui 246 天前
setex 作为一个原子操作,兼顾读写,消耗较大。
300 个线程 1ms 一次,那就是 30w qps ,超时也正常。 基于这个思路改善肯定没问题。 |
46
abccccabc 246 天前
```作用是确保同一用户只有一个订单未写入数据库```
这句话怎么怪怪的,用户的订单不是都应该写入数据库吗? |
47
EmbraceQWQ 246 天前
如果确定是卡死了 redis ,业务要求就是如此的话,感觉要么增大等待时间,锁的粒度是不是也可以变小一点例如上面提到了 hash ,不知道上集群会不会有改善,要么就上队列来搞
|
48
drymonfidelia OP |
49
timy007 246 天前
有使用 StackExchange.Redis.Extensions 这个包吗? 有个话把 poolSize 改成 1 试试。
https://www.cnblogs.com/cmt/p/16405164.html |
50
keakon 246 天前
看上去是并发拿锁的太多了,都在轮询。你考虑下常规的锁实现:先获取自旋锁,不成功就进入内核等待。
比如先 LockTakeAsync ,不成功就 brpop 一个 key ,拿到这个 key 或超时再尝试下次 LockTakeAsync 。 完成订单的线程除了释放 LockTakeAsync 的锁,还需要 rpush 这个 key ,用来唤醒一个客户端。 |
51
chenqh 246 天前
你要不 sleep 个随机数,sleep 肯定有问题的。
|
52
popvlovs 246 天前
还有个不是办法的办法,如果你们能在 load-balancer 里自定义一个按 user-id hash 的策略,那可以考虑把分布式锁干掉
|
53
xinzhanghello 246 天前
看下 jstack ,看下主、副、pub/sub 线程卡在哪里? refer: https://mp.weixin.qq.com/s/t040fhPDPzQ3EeZo1_yp8A
|
54
sighforever 246 天前
@drymonfidelia delay 50 你觉得长,可以先试试 10 ,5, 3 哪怕是 2 都减少了一半的并发啊
|
55
abccccabc 246 天前
|
56
drymonfidelia OP @abccccabc 6.0.16 ,不敢更新
|
57
EscYezi 246 天前 via iPhone
是不是连接池耗尽了,循环等待过程中一直持有连接没归还?
|
58
drymonfidelia OP @antli 以前就考虑过 minThreads 的问题,把 minThreads 调大了,没解决,后面调到非常大,像上面的错误消息里的那样,还是不行
|
59
drymonfidelia OP @EscYezi 看错误消息,连接池应该还是有很多空闲的
|
60
testcgd 246 天前 via Android
感觉是锁的实现问题,你把 delay 改下,等待一次时间翻倍,最多等 50ms
|
61
sryanyuan 246 天前
先抓包确定是否是 redis 慢了还是应用问题 假设是 redis 分析 cpu 使用然后 pprof 去打热点
|
62
harleyliao 246 天前
EVAL 执行了什么逻辑?
|
63
sampeng 246 天前
nodejs 啊?那正常。99%是你连接池使用问题
|
64
drymonfidelia OP @sampeng 不是 node ,是 .net
|
65
drymonfidelia OP @testcgd delay 的方案有一点效果,但是一次性订单少的情况变得更慢了
|
66
tg2312 245 天前
既然是应用的性能问题不是 redis 的性能问题,说明瓶颈在应用。又基于风险考虑,不想大改,而且内存都加到 256 了,说明也不怎么顾虑硬件资源,有没有可能直接把应用横向扩展,搞些硬件再部署几个应用。
|
67
drymonfidelia OP @tg2312 问题是应用没考虑到多机部署的情况 横向部署肯定要大改
|