V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
soulomoon
V2EX  ›  Python

最近写了一个 Python 的 redis rate limiter,在多线程和多进程踩到了大坑,向万能的 v2er 求助 onz

  •  
  •   soulomoon · 2018-03-13 20:37:35 +08:00 · 4005 次点击
    这是一个创建于 2447 天前的主题,其中的信息可能已经有所发展或是发生改变。

    soulomoon/python-throttle 在假如多线程和多进程测试前一切都是好好的, 感觉好开心(^・ω・^ )
    但是加入多线程,多进程测试后发现出现了 race condition😢
    但是写的时候我已经好好地去尝试规避这个问题了,找不到原因😫
    race condition 的问题出现在 test_limiter 中(;´༎ຶД༎ຶ`)

    IMAGE ALT TEXT HERE

    第 1 条附言  ·  2018-03-13 21:11:13 +08:00
        def add_key(self, key, expired):
            """use lua script to avoid race condition"""
            multiply = self.redis.register_script(self.lua_incr)
            return multiply([key, expired])
    

    出现问题的地方在这儿0 - 0

    第 2 条附言  ·  2018-03-13 21:58:59 +08:00

    test 不好意思 这个才是fixed windows的测试, 实际上,两种implementation都fail了

    8 条回复    2018-03-14 01:28:21 +08:00
    holyghost
        1
    holyghost  
       2018-03-13 21:29:09 +08:00 via iPhone   ❤️ 1
    手机上看的,看测试大概了解了思路,不成熟的想法

    如果你真要这么做,用 incr 保证线程安全
    另外,你没有考虑到滑动窗口的问题
    最后 token bucket 了解下?
    soulomoon
        2
    soulomoon  
    OP
       2018-03-13 22:02:43 +08:00
    @holyghost
    不好意思 我图放错了 0 0。
    这里我有两种 implementaion, 一个用 ordered set, 一个用 incr 写在 luascript,redis 官方是这么推荐的 0。
    都用 expire。
    token bucket 要单开 put token 的进程吧?
    soulomoon
        3
    soulomoon  
    OP
       2018-03-13 22:06:44 +08:00
    ```python
    lua_incr = """
    local current
    current = redis.call("incr",KEYS[1])
    if tonumber(current) == 1 then
    redis.call("expire",KEYS[1],KEYS[2])
    end
    return current-1
    """
    ```
    这个是我改过的 lua script,原版的在[这里]( https://redis.io/commands/incr)
    holyghost
        4
    holyghost  
       2018-03-13 22:18:54 +08:00
    @soulomoon

    我的理解,incr 不需要放在 lua script 里面来保证单线程吧? incr 本身是有返回值的, 比较下返回值和 threshold 应该就可以了

    另外,粗略算的话,token bucket 不需要另外开进程,可以在消耗时顺便添加 token (当然了,这种实现需要带时间戳)
    soulomoon
        5
    soulomoon  
    OP
       2018-03-13 22:44:48 +08:00
    incr 放进去是为了和 expire 一起,保证 key 不会因为没有 set expire 而 leak @holyghost,现在我怀疑 redis-py run script 的特点,因为是通过 register 到远端,然后再通过 sha1 作为 key 执行,可能是多个 instance 同时执行了同一 script,获取到了相同的返回值。。。
    还有一个 sliding log 的 implementaion,pipeline 看 redis-py 的简介 0 0 理论上是一个 multi exce 的行为,0 0, 也是 fail。这样的:
    def add_key(self, key, expired):
    """use ordered set for counting keys get_set manner
    """
    now = time.time()
    with self.redis.pipeline() as session:
    session.zremrangebyscore(key, 0, now - expired)
    session.zrange(key, 0, -1)
    session.zadd(key, now, uuid.uuid4().hex)
    session.expire(key, expired)
    result = session.execute()
    return len(result[1])
    soulomoon
        6
    soulomoon  
    OP
       2018-03-13 22:45:45 +08:00
    *多个 instance 接受了同一个 script 的执行结果。。
    soulomoon
        7
    soulomoon  
    OP
       2018-03-14 00:35:01 +08:00
    发现即使是简单如
    def add_key(self, key, expired):
    return self.redis.incr(key)
    也会有同样的问题

    who's to blame....
    soulomoon
        8
    soulomoon  
    OP
       2018-03-14 01:28:21 +08:00
    发现问题了, 是我写 unittest 的问题,interleaving 多个不同时限的 limiter 到相同的 key 中,当然会出现不同的结果 ozn
    好开心解决了,原来不是我 implementaion 的问题是我的测试的问题,看来要好好学习测试, 最后谢谢 @holyghost 的回答。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2726 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 06:43 · PVG 14:43 · LAX 22:43 · JFK 01:43
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.