【问题标题】:Distributed lock manager for PythonPython 的分布式锁管理器
【发布时间】:2014-05-27 01:10:57
【问题描述】:

我有一堆具有多个实例的服务器访问对每秒请求有硬性限制的资源。

我需要一种机制来锁定所有正在运行的服务器和实例对该资源的访问。

我在github上找到了一个restful分布式锁管理器:https://github.com/thefab/restful-distributed-lock-manager

不幸的是,似乎有一分钟。锁定时间为 1 秒,相对不可靠。在多次测试中,解锁 1 秒锁需要 1 到 3 秒。

有没有经过我可以用于此目的的 python 接口经过良好测试的东西?

编辑:我需要能在 1 秒内自动解锁的东西。在我的代码中,锁永远不会被释放。

【问题讨论】:

标签: python python-2.7 locking distributed-lock


【解决方案1】:

您的要求似乎非常具体。我会考虑编写一个简单的锁服务器,然后使用一个在创建时获取锁的类来实现锁客户端,然后在超出范围时删除锁。

class Lock(object):
    def __init__(self,resource):
        print "Lock acquired for",resource
        # Connect to lock server and acquire resource

    def __del__(self):
        print "Lock released"
        # Connect to lock server and unlock resource if locked

def callWithLock(resource,call,*args,**kwargs):
    lock = Lock(resource)
    return call( *args, **kwargs )

def test( asdf, something="Else" ):
    return asdf + " " + something

if __name__ == "__main__":
    import sys
    print "Calling test:",callWithLock( "resource.test", test, sys.argv[0] )

样本输出

$ python locktest.py 
Calling test: Lock acquired for resource.test
Lock released
locktest.py Else

【讨论】:

  • 使用__del__ 来释放你的锁是一个非常糟糕的主意。您无法保证 Python 在对象超出范围后调用该方法的速度有多快,尤其是当它是引用循环的一部分时。相反,您应该使用上下文管理器协议(__enter____exit__)和with 语句。此外,如果没有任何尝试实际实现锁定服务器或您用# Connect to lock server cmets 掩盖的客户端代码,我不确定这个答案有多大用处。我怀疑 Jay 知道如何使用锁服务器,但他没有!
  • 好吧,我怀疑他会不会找人为他写一本。我的解决方案是他自己动手。不过,您对使用上下文管理器原型的建议是正确的,谢谢! :)
【解决方案2】:

我的第一个想法是使用 Redis。但是还有更多很棒的工具,有些甚至更轻,所以我的解决方案建立在 zmq 之上。因此,您不必运行 Redis,运行小型 Python 脚本就足够了。

需求审核

在描述解决方案之前,让我回顾一下您的要求。

  • 对某个资源的请求数限制为固定时间段内的请求数。

  • 自动解锁

  • 资源(自动)解锁时间应少于 1 秒。

  • 它应该被分发。我会假设,你的意思是多个分布式服务器消耗一些资源应该能够,并且只有一个储物柜服务很好(更多关于它的结论)

概念

限制时隙内的请求数

时隙可以是一秒、更多秒或更短的时间。唯一的限制是 Python 中时间测量的精度。

如果您的资源具有每秒定义的硬限制,则应使用时隙 1.0

监控每个时隙的请求数,直到下一个开始

第一次请求访问您的资源,设置下一个时间段的开始时间并初始化请求计数器。

对于每个请求,增加请求计数器(针对当前时隙)并允许请求,除非您已达到当前时隙中允许的最大请求数。

使用带有 REQ/REP 的 zmq 服务

您的消费服务器可能分布在更多计算机上。要提供对 LockerServer 的访问,您将使用 zmq。

示例代码

zmqlocker.py:

import time
import zmq

class Locker():
    def __init__(self, max_requests=1, in_seconds=1.0):
        self.max_requests = max_requests
        self.in_seconds = in_seconds
        self.requests = 0
        now = time.time()
        self.next_slot = now + in_seconds

    def __iter__(self):
        return self

    def next(self):
        now = time.time()
        if now > self.next_slot:
            self.requests = 0
            self.next_slot = now + self.in_seconds
        if self.requests < self.max_requests:
            self.requests += 1
            return "go"
        else:
            return "sorry"


class LockerServer():
    def __init__(self, max_requests=1, in_seconds=1.0, url="tcp://*:7777"):
        locker=Locker(max_requests, in_seconds)
        cnt = zmq.Context()
        sck = cnt.socket(zmq.REP)
        sck.bind(url)
        while True:
            msg = sck.recv()
            sck.send(locker.next())

class LockerClient():
    def __init__(self, url="tcp://localhost:7777"):
        cnt = zmq.Context()
        self.sck = cnt.socket(zmq.REQ)
        self.sck.connect(url)
    def next(self):
        self.sck.send("let me go")
        return self.sck.recv()

运行你的服务器:

run_server.py:

from zmqlocker import LockerServer

svr = LockerServer(max_requests=5, in_seconds=0.8)

从命令行:

$ python run_server.py

这将开始在本地主机上的默认端口 7777 上提供储物柜服务。

运行您的客户端

run_client.py:

from zmqlocker import LockerClient
import time

locker_cli = LockerClient()

for i in xrange(100):
    print time.time(), locker_cli.next()
    time.sleep(0.1)

从命令行:

$ python run_client.py

您将看到“go”、“go”、“sorry”...打印的响应。

尝试运行更多客户端。

一点压力测试

您可以先启动客户端,然后再启动服务器。客户端会一直阻塞,直到服务器启动,然后才会愉快地运行。

结论

  • 满足所述要求
    • 请求数量有限
    • 无需解锁,只要有下一个时隙,就允许更多请求
    • Lo​​ckerService 可通过网络或本地套接字使用。
  • 应该靠谱,zmq是成熟的方案,python代码比较简单
  • 它不需要所有参与者的时间同步
  • 性能会很好

另一方面,您可能会发现,您的资源限制并不像您想象的那么可预测,因此请准备好使用参数以找到适当的平衡,并始终为这方面的例外情况做好准备。

还有一些空间可以优化提供“锁” - 例如。如果 locker 用完了允许的请求,但当前的时间段已经几乎完成,您可以考虑用“sorry”稍等片刻,然后在几分之一秒后提供“go”。

将其扩展到真正的分布式锁管理器

通过“分布式”,我们还可以理解多个储物柜服务器一起运行。这更难做到,但也是可能的。 zmq 允许非常轻松地连接到多个 url,因此客户端可以非常轻松地连接到多个储物柜服务器。有一个问题,如何协调储物柜服务器不允许对您的资源进行过多请求。 zmq 允许服务器间通信。一种模型可能是,每个储物柜服务器将在 PUB/SUB 上发布每个提供的“go”。所有其他储物柜服务器都将被订阅,并使用每个“go”来增加它们的本地请求计数器(稍微修改逻辑)。

【讨论】:

    【解决方案3】:

    对于我的集群,我将 ZooKeeper 与 python-kazoo 库一起用于队列和锁。

    为您的目的修改了 kazoo api 文档中的示例: http://kazoo.readthedocs.org/en/latest/api/recipe/lock.html

    zk = KazooClient()
    lock = zk.Lock("/lockpath", "my-identifier")
    if lock.acquire(timeout=1):
    
       code here
    
       lock.release()
    

    但我记得,ZooKeeper 至少需要三个节点。

    【讨论】:

    • 您的解决方案是否控制请求率?
    • @JanVlcinsky 不,此代码不控制请求率。为了控制请求率,最好将所有带锁的代码放在类方法中并用装饰器包装它。装饰器会存储一些请求给zookeeper节点,并在达到限制时释放锁。
    【解决方案4】:

    分布式锁管理器 Taooka http://taooka.com 的 TTL 精度达到纳秒级。但它只有 Golang 客户端库。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-07-04
      • 1970-01-01
      • 1970-01-01
      • 2022-05-13
      • 1970-01-01
      • 2010-10-14
      • 2021-08-18
      相关资源
      最近更新 更多