zoukankan      html  css  js  c++  java
  • python分布式环境下的限流器

    项目中用到了限流,受限于一些实现方式上的东西,手撕了一个简单的服务端限流器。

    服务端限流和客户端限流的区别,简单来说就是:

    1)服务端限流

    对接口请求进行限流,限制的是单位时间内请求的数量,目的是通过有损来换取高可用。

    例如我们的场景是,有一个服务接收请求,处理之后,将数据bulk到Elasticsearch中进行索引存储,bulk索引是一个很耗费资源的操作,如果遭遇到请求流量激增,可能会压垮Elasticsearch(队列阻塞,内存激增),所以需要对流量的峰值做一个限制。

    2)客户端限流

    限制的是客户端进行访问的次数。

    例如,线程池就是一个天然的限流器。限制了并发个数max_connection,多了的就放到缓冲队列里排队,排队搁不下了>queue_size就扔掉。

    本文是服务端限流器。

    我这个限流器的优点:

    • 1)简单
    • 2)管事

    缺点:

    • 1)不能做到平滑限流
      例如大家尝尝说的令牌桶算法和漏桶算法(我感觉这两个算法本质上都是一个事情)可以实现平滑限流。
      什么是平滑限流?举个栗子,我们要限制5秒钟内访问数不超过1000,平滑限流能做到,每秒200个,5秒钟不超过1000,很平衡;非平滑限流可能,在第一秒就访问了1000次,之后的4秒钟全部限制住。
    • 2)不灵活

       只实现了秒级的限流。

    支持两个场景:

    1)对于单进程多线程场景(使用线程安全的Queue做全局变量)

    这种场景下,只部署了一个实例,对这个实例进行限流。在生产环境中用的很少。

    2)对于多进程分布式场景(使用redis做全局变量)

    多实例部署,一般来说生产环境,都是这样的使用场景。

    在这样的场景下,需要对流量进行整体的把控。例如,user服务部署了三个实例,对外暴露query接口,要做的是对接口级的流量限制,也就是对query这个接口整体允许多大的峰值,而不去关心到底负载到哪个实例。

    题外话,这个可以通过nginx做。

    下面说一下限流器的实现吧。

    1、接口BaseRateLimiter

    按照我的思路,先定义一个接口,也可以叫抽象类。

    初始化的时候,要配置rate,限流器的限速。

    提供一个抽象方法,acquire(),调用这个方法,返回是否限制流量。

    1. class BaseRateLimiter(object):
    2.  
    3. __metaclass__ = abc.ABCMeta
    4.  
    5. @abc.abstractmethod
    6. def __init__(self, rate):
    7. self.rate = rate
    8.  
    9. @abc.abstractmethod
    10. def acquire(self, count):
    11. return

    2、单进程多线程场景的限流ThreadingRateLimiter

    继承BaseRateLimiter抽象类,使用线程安全的Queue作为全局变量,来消除竞态影响。

    后台有个进程每秒钟清空一次queue;

    当请求来了,调用acquire函数,queue incr一次,如果大于限速了,就返回限制。否则就允许访问。

    1. class ThreadingRateLimiter(BaseRateLimiter):
    2.  
    3. def __init__(self, rate):
    4. BaseRateLimiter.__init__(self, rate)
    5. self.queue = Queue.Queue()
    6. threading.Thread(target=self._clear_queue).start()
    7.  
    8. def acquire(self, count=1):
    9. self.queue.put(1, block=False)
    10. return self.queue.qsize() < self.rate
    11.  
    12. def _clear_queue(self):
    13. while 1:
    14. time.sleep(1)
    15. self.queue.queue.clear()

    2、分布式场景下的限流DistributeRateLimiter

    继承BaseRateLimiter抽象类,使用外部存储作为共享变量,外部存储的访问方式为cache。

    1. class DistributeRateLimiter(BaseRateLimiter):
    2.  
    3. def __init__(self, rate, cache):
    4. BaseRateLimiter.__init__(self, rate)
    5. self.cache = cache
    6.  
    7. def acquire(self, count=1, expire=3, key=None, callback=None):
    8. try:
    9. if isinstance(self.cache, Cache):
    10. return self.cache.fetchToken(rate=self.rate, count=count, expire=expire, key=key)
    11. except Exception, ex:
    12. return True

    为了解耦和灵活性,我们实现了Cache类。提供一个抽象方法getToken()

    如果你使用redis的话,你就继承Cache抽象类,实现通过redis获取令牌的方法。

    如果使用mysql的话,你就继承Cache抽象类,实现通过mysql获取令牌的方法。

    cache方法

    1. class Cache(object):
    2.  
    3. __metaclass__ = abc.ABCMeta
    4.  
    5. @abc.abstractmethod
    6. def __init__(self):
    7. self.key = "DEFAULT"
    8. self.namespace = "RATELIMITER"
    9.  
    10. @abc.abstractmethod
    11. def fetchToken(self, rate, key=None):
    12. return

    给出一个redis的实现RedisTokenCache

    每秒钟创建一个key,并且对请求进行计数incr,当这一秒的计数值已经超过了限速rate,就拿不到token了,也就是限制流量。

    对每秒钟创建出的key,让他超时expire。保证key不会持续占用存储空间。

    没有什么难点,这里使用redis事务,保证incr和expire能同时执行成功。

    1. class RedisTokenCache(Cache):
    2.  
    3. def __init__(self, host, port, db=0, password=None, max_connections=None):
    4. Cache.__init__(self)
    5. self.redis = redis.Redis(
    6. connection_pool=
    7. redis.ConnectionPool(
    8. host=host, port=port, db=db,
    9. password=password,
    10. max_connections=max_connections
    11. ))
    12.  
    13. def fetchToken(self, rate=100, count=1, expire=3, key=None):
    14. date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    15. key = ":".join([self.namespace, key if key else self.key, date])
    16. try:
    17. current = self.redis.get(key)
    18. ") > rate:
    19. raise Exception("to many requests in current second: %s" % date)
    20. else:
    21. with self.redis.pipeline() as p:
    22. p.multi()
    23. p.incr(key, count)
    24. p.expire(key, int(expire "))
    25. p.execute()
    26. return True
    27. except Exception, ex:
    28. return False

    多线程场景下测试代码

    1. limiter = ThreadingRateLimiter(rate=10000)
    2.  
    3. def job():
    4. while 1:
    5. if not limiter.acquire():
    6. print '限流'
    7. else:
    8. print '正常'
    9.  
    10. threads = [threading.Thread(target=job) for i in range(10)]
    11. for thread in threads:
    12. thread.start()

    分布式场景下测试代码

    1. token_cache = RedisTokenCache(host='10.93.84.53', port=6379, password='bigdata123')
    2. limiter = DistributeRateLimiter(rate=10000, cache=token_cache)
    3. r = redis.Redis(connection_pool=redis.ConnectionPool(host='10.93.84.53', port=6379, password='bigdata123'))
    4.  
    5. def job():
    6. while 1:
    7. if not limiter.acquire():
    8. print '限流'
    9. else:
    10. print '正常'
    11.  
    12. threads = [multiprocessing.Process(target=job) for i in range(10)]
    13. for thread in threads:
    14. thread.start()

    可以自行跑一下。

    说明:

    我这里的限速都是秒级别的,例如限制每秒400次请求。有可能出现这一秒的前100ms,就来了400次请求,后900ms就全部限制住了。也就是不能平滑限流。

    不过如果你后台的逻辑有队列,或者线程池这样的缓冲,这个不平滑的影响其实不大。

  • 相关阅读:
    程序员如何利用空闲时间挣零花钱
    常见的数据交互之跳转页面传值
    一个能让cin和scanf 一样快的方法:
    HDU 4901 DP
    POJ 2823 线段树 Or 单调队列
    POJ 3264 线段树 ST
    POJ 3468 线段树+状压
    POJ 2777 线段树
    QQ 临时会话+图标 HTML代码
    POJ 1463 Strategic game
  • 原文地址:https://www.cnblogs.com/ExMan/p/12622644.html
Copyright © 2011-2022 走看看