V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
drymonfidelia
V2EX  ›  Redis

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

  •  
  •   drymonfidelia · 2024-04-22 13:35:22 +08:00 · 9377 次点击
    这是一个创建于 388 天前的主题,其中的信息可能已经有所发展或是发生改变。

    报错信息是 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 内存都没解决。

    67 条回复    2024-04-23 16:36:08 +08:00
    sagaxu
        1
    sagaxu  
       2024-04-22 13:59:19 +08:00
    qs: 15
    Queueditems=50
    codegenerator
        2
    codegenerator  
       2024-04-22 14:10:37 +08:00
    预算多少?资深架构师应战
    sujin190
        3
    sujin190  
       2024-04-22 14:10:46 +08:00
    这个 LockTakeAsync 看下底层实现也就一个 setnx 指令,这请求量 redis 层面不可能超时吧,所以超时肯定是软件这边的问题了,线程池这么高不合理,要么是 redis 连接管理问题要么是 await 调度有死锁了吧,而且怎么记得 Task.Delay(1);似乎是延时 1 毫秒吧,这么短不合理也没必要吧,也很容易导致死锁和并发异常什么的,好歹延时个 50 毫秒吧

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

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

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

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

    有.net 的 sdk ,只需要换一下你这个加锁的逻辑就好,因为全异步的通信协议,所以不需要循环服务会主动异步通知,我们也在生产环境用很多年了,只是加锁没遇到啥问题,就你这请求量大概需要 100M 内存吧,也支持集群高可用模式
    sujin190
        7
    sujin190  
       2024-04-22 14:32:49 +08:00
    drymonfidelia
        8
    drymonfidelia  
    OP
       2024-04-22 14:37:44 +08:00
    @sujin190 谢谢,我看一下
    javalaw2010
        9
    javalaw2010  
       2024-04-22 14:45:43 +08:00
    我瞎说啊,盲猜,会不是是有定时的 redis 备份比如 BGSAVE 命令导致主进程阻塞了,从而导致了客户端超时
    drymonfidelia
        10
    drymonfidelia  
    OP
       2024-04-22 14:50:41 +08:00
    @javalaw2010 没有,redis 里面全是锁和缓存,丢了重启就好,不需要备份
    lsk569937453
        11
    lsk569937453  
       2024-04-22 14:53:28 +08:00
    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
        12
    keakon  
       2024-04-22 14:55:27 +08:00
    根据你的业务逻辑重新实现一遍呗,弄懂屎山反而更难。
    chenqh
        13
    chenqh  
       2024-04-22 14:56:20 +08:00
    你这个 lock release 在哪里?
    drymonfidelia
        14
    drymonfidelia  
    OP
       2024-04-22 14:57:01 +08:00
    @lsk569937453 是,但是我需要确保前一个锁释放到加下一个锁的间隔尽可能最短。目前我的想法是如果没加锁成功,Sub 一个锁名称的 Channel ,每个锁释放就 Pub 一条消息,Sub 收到消息就再尝试加锁
    drymonfidelia
        15
    drymonfidelia  
    OP
       2024-04-22 14:59:26 +08:00
    @keakon 当时的需求文档已经没了。我觉得这么混乱的逻辑他能弄到没问题还是有点水平的
    @chenqh 在下单结束的时候
    drymonfidelia
        16
    drymonfidelia  
    OP
       2024-04-22 15:00:24 +08:00
    @drymonfidelia #14 但是不知道这样弄性能会不会更差,一条 SET 命令能解决的问题我要 SUB 几百次
    dynastysea
        17
    dynastysea  
       2024-04-22 15:02:47 +08:00
    这就是不用云服务的缺点,上云了直接甩工单给云厂商就行了
    chenqh
        18
    chenqh  
       2024-04-22 15:05:40 +08:00
    @dynastysea 这和云有什么关系这明显

    是 lock 之后,按顺序执行任务 500 个超过 10S 了,无解吧
    drymonfidelia
        19
    drymonfidelia  
    OP
       2024-04-22 15:06:52 +08:00
    @chenqh 每个任务结束后都要释放锁才能进入下一个任务,我觉得问题出在频繁加锁上。
    chenqh
        20
    chenqh  
       2024-04-22 15:09:06 +08:00
    @drymonfidelia 感觉几乎无解了,你这个就是加需要加上去的,

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

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

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

    反正就我这种菜 B 脑袋想不出来。
    chenqh
        21
    chenqh  
       2024-04-22 15:10:04 +08:00
    @drymonfidelia 你要不加个 log 看一下 lock 的耗时时间?
    sagaxu
        22
    sagaxu  
       2024-04-22 15:10:58 +08:00
    “客户端一次多少个订单就多少并发调用 API 提交”

    如果都是同一个 userId ,或者有锁其它共同的 id ,并发高的时候,可能会出现饥饿的情况,每次 unlock 都被别的 lock 抢去了,如果下单性能稳定,超时时长改成 10 秒,从头饿到尾也轮到了
    dynastysea
        23
    dynastysea  
       2024-04-22 15:12:29 +08:00
    @chenqh 因为作者分析不出原因,我的意思是这种也可以叫云厂商的帮忙协助定位
    chenqh
        24
    chenqh  
       2024-04-22 15:13:57 +08:00
    @dynastysea 云厂商还帮忙做这个的吗?我无知了。
    lsk569937453
        25
    lsk569937453  
       2024-04-22 15:16:58 +08:00
    1.不要换成 Sub/Pub,性能上不会有提升,架构更复杂了。
    2.提高 redis 的超时时间。

    redis 的 client 超时时间可能是全局线程池繁忙导致的,也可以是 redis 服务端导致的。redis 可以加一下监控,看有没有大 key/热 key 查询。
    chunworkhard
        26
    chunworkhard  
       2024-04-22 15:18:15 +08:00
    学习下
    drymonfidelia
        27
    drymonfidelia  
    OP
       2024-04-22 15:18:52 +08:00
    @sagaxu 但是这个 timeout 似乎是 redis 被卡死了,一次加锁尝试一直没返回结果
    drymonfidelia
        28
    drymonfidelia  
    OP
       2024-04-22 15:22:56 +08:00
    @lsk569937453 redis 服务端不是单线程运行的么,客户端从上面的报错信息看线程池还有很大空闲
    lsk569937453
        29
    lsk569937453  
       2024-04-22 15:31:54 +08:00
    @drymonfidelia 客户端从上面的报错信息看线程池还有很大空闲

    你的报错信息展示的都是.net 的线程池信息,并没有 redis 服务端的监控信息。
    8355
        30
    8355  
       2024-04-22 15:33:53 +08:00
    @dynastysea #23 这是应用问题,是代码写成这样,遇到屎山问题我没脸提工单。我怕人家让我提供代码我截出来丢老脸。
    edward1987
        31
    edward1987  
       2024-04-22 15:33:58 +08:00
    delay + random(1,20)试试? 可以少试 10 倍请求,随机过后不容易有空闲或堵塞
    sagaxu
        32
    sagaxu  
       2024-04-22 15:34:44 +08:00
    300 个并发,每个并发每秒 1000 次请求,你算算看 QPS
    gaogang
        33
    gaogang  
       2024-04-22 15:43:22 +08:00
    循环里面 delay 的带短了吧
    拿 redis 锁之前 加个本地锁 应该会好点
    drymonfidelia
        34
    drymonfidelia  
    OP
       2024-04-22 15:44:35 +08:00 via iPhone
    @sagaxu 不能直接乘吧,拿到锁的请求就不会继续申请锁了
    drymonfidelia
        35
    drymonfidelia  
    OP
       2024-04-22 15:45:00 +08:00 via iPhone
    @drymonfidelia 另外 1ms 也不一定能拿到锁
    i8086
        36
    i8086  
       2024-04-22 15:45:33 +08:00
    这个错误信息,没什么问题,毕竟都用了异步 IOCP 也是空闲。

    如果有监控且是单机 redis ,那就查查 redis 当时的连接数是不是爆了,首行提示 Timeout awaiting response 。
    8355
        37
    8355  
       2024-04-22 15:46:38 +08:00
    我猜测了一下你的代码上下文,通过首行业务加并发锁,并且在执行到最后一个业务的时候内部有一个解锁指令。
    加了个 180 秒的锁,执行到最后应该执行 del 解锁。

    可以把你的订单内容,产品+数量之类的编个字符串算个 hash 加入到 rediskey 中,这样的话相同的产品并发下单才会触发锁,而不同产品下单不会受到影响。

    这样改的前提是你需要把整个执行流程看一遍确认只有 2 个位置(也有可能只有一个)有锁操作,如果其他位置有锁检查一定要再看下代码逻辑。
    我觉得这是最低成本改动,不牵扯到原有逻辑,可以通过搜索去检查该 key 的应用次数。
    sagaxu
        38
    sagaxu  
       2024-04-22 15:50:00 +08:00
    @drymonfidelia 300 个只有一个拿到了锁,其它的都拿不到啊
    sunjiayao
        39
    sunjiayao  
       2024-04-22 15:53:09 +08:00
    加锁和解锁的地方都加下日志看看 应该是死锁了
    zhy0216
        40
    zhy0216  
       2024-04-22 16:00:12 +08:00 via Android
    增加重试时间啊 1ms 这瓶颈是 cpu 了 你加内存什么用
    redis 单线程还不能利用多核优势
    antli
        42
    antli  
       2024-04-22 16:07:00 +08:00
    考虑到此信息,我们强烈建议客户将 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"/>。
    wccc
        43
    wccc  
       2024-04-22 16:10:31 +08:00
    还是修改锁的实现, 可重入
    rnv
        44
    rnv  
       2024-04-22 16:12:23 +08:00
    是不是惊群了,每次锁空闲会唤起一大批在等待的,但只有一个拿到了锁
    zhuisui
        45
    zhuisui  
       2024-04-22 16:15:03 +08:00
    setex 作为一个原子操作,兼顾读写,消耗较大。
    300 个线程 1ms 一次,那就是 30w qps ,超时也正常。
    基于这个思路改善肯定没问题。
    abccccabc
        46
    abccccabc  
       2024-04-22 16:41:04 +08:00
    ```作用是确保同一用户只有一个订单未写入数据库```

    这句话怎么怪怪的,用户的订单不是都应该写入数据库吗?
    EmbraceQWQ
        47
    EmbraceQWQ  
       2024-04-22 16:44:46 +08:00
    如果确定是卡死了 redis ,业务要求就是如此的话,感觉要么增大等待时间,锁的粒度是不是也可以变小一点例如上面提到了 hash ,不知道上集群会不会有改善,要么就上队列来搞
    drymonfidelia
        48
    drymonfidelia  
    OP
       2024-04-22 16:45:19 +08:00
    @abccccabc 就是确保同一用户只有一个订单正在写入数据库的意思
    @8355 我看到他的代码后一个订单有引用前一个订单的数据,同时下多个订单确实会出问题。逻辑实在太混乱了,有几处我都看不懂,实在不敢改
    timy007
        49
    timy007  
       2024-04-22 16:55:21 +08:00
    有使用 StackExchange.Redis.Extensions 这个包吗? 有个话把 poolSize 改成 1 试试。
    https://www.cnblogs.com/cmt/p/16405164.html
    keakon
        50
    keakon  
       2024-04-22 17:20:28 +08:00
    看上去是并发拿锁的太多了,都在轮询。你考虑下常规的锁实现:先获取自旋锁,不成功就进入内核等待。

    比如先 LockTakeAsync ,不成功就 brpop 一个 key ,拿到这个 key 或超时再尝试下次 LockTakeAsync 。
    完成订单的线程除了释放 LockTakeAsync 的锁,还需要 rpush 这个 key ,用来唤醒一个客户端。
    chenqh
        51
    chenqh  
       2024-04-22 18:00:17 +08:00
    你要不 sleep 个随机数,sleep 肯定有问题的。
    popvlovs
        52
    popvlovs  
       2024-04-22 18:08:26 +08:00
    还有个不是办法的办法,如果你们能在 load-balancer 里自定义一个按 user-id hash 的策略,那可以考虑把分布式锁干掉
    xinzhanghello
        53
    xinzhanghello  
       2024-04-22 19:47:33 +08:00
    看下 jstack ,看下主、副、pub/sub 线程卡在哪里? refer: https://mp.weixin.qq.com/s/t040fhPDPzQ3EeZo1_yp8A
    sighforever
        54
    sighforever  
       2024-04-22 21:43:31 +08:00
    @drymonfidelia delay 50 你觉得长,可以先试试 10 ,5, 3 哪怕是 2 都减少了一半的并发啊
    abccccabc
        55
    abccccabc  
       2024-04-22 22:37:26 +08:00
    @drymonfidelia

    redis 是什么版本?据说最新的 redis 版本可以利用多 cpu 特性。
    drymonfidelia
        56
    drymonfidelia  
    OP
       2024-04-22 22:40:19 +08:00
    @abccccabc 6.0.16 ,不敢更新
    EscYezi
        57
    EscYezi  
       2024-04-23 00:31:17 +08:00 via iPhone
    是不是连接池耗尽了,循环等待过程中一直持有连接没归还?
    drymonfidelia
        58
    drymonfidelia  
    OP
       2024-04-23 00:34:54 +08:00
    @antli 以前就考虑过 minThreads 的问题,把 minThreads 调大了,没解决,后面调到非常大,像上面的错误消息里的那样,还是不行
    drymonfidelia
        59
    drymonfidelia  
    OP
       2024-04-23 00:35:26 +08:00
    @EscYezi 看错误消息,连接池应该还是有很多空闲的
    testcgd
        60
    testcgd  
       2024-04-23 09:28:50 +08:00 via Android
    感觉是锁的实现问题,你把 delay 改下,等待一次时间翻倍,最多等 50ms
    sryanyuan
        61
    sryanyuan  
       2024-04-23 09:51:49 +08:00
    先抓包确定是否是 redis 慢了还是应用问题 假设是 redis 分析 cpu 使用然后 pprof 去打热点
    harleyliao
        62
    harleyliao  
       2024-04-23 10:38:21 +08:00
    EVAL 执行了什么逻辑?
    sampeng
        63
    sampeng  
       2024-04-23 11:08:32 +08:00
    nodejs 啊?那正常。99%是你连接池使用问题
    drymonfidelia
        64
    drymonfidelia  
    OP
       2024-04-23 11:09:33 +08:00 via iPhone
    @sampeng 不是 node ,是 .net
    drymonfidelia
        65
    drymonfidelia  
    OP
       2024-04-23 11:11:02 +08:00 via iPhone
    @testcgd delay 的方案有一点效果,但是一次性订单少的情况变得更慢了
    tg2312
        66
    tg2312  
       2024-04-23 16:22:51 +08:00
    既然是应用的性能问题不是 redis 的性能问题,说明瓶颈在应用。又基于风险考虑,不想大改,而且内存都加到 256 了,说明也不怎么顾虑硬件资源,有没有可能直接把应用横向扩展,搞些硬件再部署几个应用。
    drymonfidelia
        67
    drymonfidelia  
    OP
       2024-04-23 16:36:08 +08:00
    @tg2312 问题是应用没考虑到多机部署的情况 横向部署肯定要大改
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3350 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 04:29 · PVG 12:29 · LAX 21:29 · JFK 00:29
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.