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

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

  •  
  •   drymonfidelia · 11 天前 · 4963 次点击

    报错信息是 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  
       11 天前
    qs: 15
    Queueditems=50
    codegenerator
        2
    codegenerator  
       11 天前
    预算多少?资深架构师应战
    sujin190
        3
    sujin190  
       11 天前
    这个 LockTakeAsync 看下底层实现也就一个 setnx 指令,这请求量 redis 层面不可能超时吧,所以超时肯定是软件这边的问题了,线程池这么高不合理,要么是 redis 连接管理问题要么是 await 调度有死锁了吧,而且怎么记得 Task.Delay(1);似乎是延时 1 毫秒吧,这么短不合理也没必要吧,也很容易导致死锁和并发异常什么的,好歹延时个 50 毫秒吧

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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