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(),调用这个方法,返回是否限制流量。

class BaseRateLimiter(object):

  __metaclass__ = abc.ABCMeta

  @abc.abstractmethod
  def __init__(self, rate):
    self.rate = rate

  @abc.abstractmethod
  def acquire(self, count):
    return

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

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

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

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

class ThreadingRateLimiter(BaseRateLimiter):

  def __init__(self, rate):
    BaseRateLimiter.__init__(self, rate)
    self.queue = Queue.Queue()
    threading.Thread(target=self._clear_queue).start()

  def acquire(self, count=1):
    self.queue.put(1, block=False)
    return self.queue.qsize() < self.rate

  def _clear_queue(self):
    while 1:
      time.sleep(1)
      self.queue.queue.clear()

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

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

class DistributeRateLimiter(BaseRateLimiter):

  def __init__(self, rate, cache):
    BaseRateLimiter.__init__(self, rate)
    self.cache = cache

  def acquire(self, count=1, expire=3, key=None, callback=None):
    try:
      if isinstance(self.cache, Cache):
        return self.cache.fetchToken(rate=self.rate, count=count, expire=expire, key=key)
    except Exception, ex:
      return True

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

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

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

cache抽象类

class Cache(object):

  __metaclass__ = abc.ABCMeta

  @abc.abstractmethod
  def __init__(self):
    self.key = "DEFAULT"
    self.namespace = "RATELIMITER"

  @abc.abstractmethod
  def fetchToken(self, rate, key=None):
    return

给出一个redis的实现RedisTokenCache

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

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

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

class RedisTokenCache(Cache):

  def __init__(self, host, port, db=0, password=None, max_connections=None):
    Cache.__init__(self)
    self.redis = redis.Redis(
      connection_pool=
        redis.ConnectionPool(
          host=host, port=port, db=db,
          password=password,
          max_connections=max_connections
        ))

  def fetchToken(self, rate=100, count=1, expire=3, key=None):
    date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    key = ":".join([self.namespace, key if key else self.key, date])
    try:
      current = self.redis.get(key)
      if int(current if current else "0") > rate:
        raise Exception("to many requests in current second: %s" % date)
      else:
        with self.redis.pipeline() as p:
          p.multi()
          p.incr(key, count)
          p.expire(key, int(expire if expire else "3"))
          p.execute()
          return True
    except Exception, ex:
      return False

多线程场景下测试代码

limiter = ThreadingRateLimiter(rate=10000)

def job():
  while 1:
    if not limiter.acquire():
      print '限流'
    else:
      print '正常'

threads = [threading.Thread(target=job) for i in range(10)]
for thread in threads:
  thread.start()

分布式场景下测试代码

token_cache = RedisTokenCache(host='10.93.84.53', port=6379, password='bigdata123')
limiter = DistributeRateLimiter(rate=10000, cache=token_cache)
r = redis.Redis(connection_pool=redis.ConnectionPool(host='10.93.84.53', port=6379, password='bigdata123'))

def job():
  while 1:
    if not limiter.acquire():
      print '限流'
    else:
      print '正常'

threads = [multiprocessing.Process(target=job) for i in range(10)]
for thread in threads:
  thread.start()

可以自行跑一下。

说明:

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

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • python分布式环境下的限流器的示例

    项目中用到了限流,受限于一些实现方式上的东西,手撕了一个简单的服务端限流器. 服务端限流和客户端限流的区别,简单来说就是: 1)服务端限流 对接口请求进行限流,限制的是单位时间内请求的数量,目的是通过有损来换取高可用. 例如我们的场景是,有一个服务接收请求,处理之后,将数据bulk到Elasticsearch中进行索引存储,bulk索引是一个很耗费资源的操作,如果遭遇到请求流量激增,可能会压垮Elasticsearch(队列阻塞,内存激增),所以需要对流量的峰值做一个限制. 2)客户端限流 限制

  • python在linux环境下安装skimage的示例代码

    一.执行代码 yum install xz-devel yum install python-backports-lzma pip3 install scikit-image pip3 install backports.lzma 二.修改文件 修改文件路径 /usr/local/python3/lib/python3.6/lzma.py 修改文件部分内容 将 from _lzma import * from _lzma import _encode_filter_properties, _de

  • Linux集群/分布式环境下session处理的五种策略详解

    前言 我们一般在搭建完集群环境后,不得不考虑的一个问题就是用户访问产生的session如何处理.如果不做任何处理的话,用户将出现频繁登录的现象,比如集群中存在A.B两台服务器,用户在第一次访问网站时,Nginx通过其负载均衡机制将用户请求转发到A服务器,这时A服务器就会给用户创建一个Session.当用户第二次发送请求时,Nginx将其负载均衡到B服务器,而这时候B服务器并不存在Session,所以就会将用户踢到登录页面.这将大大降低用户体验度,导致用户的流失,这种情况是项目绝不应该出现的. 我

  • Python交互环境下实现输入代码

    Iamlaosong文 Python交互环境的提示符是">>>",命令行模式下输入python命令就可以进入这个交互环境进行交互会话. 在windows中,除了在shell窗口中输入python,也可以通过启动IDLE的主窗口或者通过从python的start按钮菜单的菜单选项中选择"Python(command line)"来开始类似的交互会话. 在MS-DOS和Windows系统中输入Ctrl+Z退出.如果在IDLE GUI中,也可以输入Ctr

  • Mac中Python 3环境下安装scrapy的方法教程

    前言 最近抽空想学习一下python的爬虫框架scrapy,在mac下安装的时候遇到了问题,逐一解决了问题,分享一下,话不多说了,来一起看看详细的介绍吧. 步骤如下: 1. 从官网 下载最新版本Python 3.6.3(本地快速下载安装:http://www.jb51.net/softs/583651.html) # 在Mac上Python3环境下安装scrapy 2. 安装 Python3 在终端输入python3出现下面的内容表示安装成功 ➜ ~ python3 Python 3.6.3 (

  • ASP 环境下 VBS 事件应用 示例代码

    <% Class TopicModel  Public OnView  Public Function Load(id)   IF Not(IsEvent(OnView)) Then    OnView(1)   End IF  End Function  Function IsEvent(evnet)   IsEvent = IsEmpty(evnet)  End Function End Class Sub UpdateViewCounter(value)  Response.Write(&qu

  • Python交互环境下打印和输入函数的实例内容

    需求: 1.在控制台显示一个"欢迎你...." 2.在控制台显示一个"输入您的姓名:" 3.显示"你好,xx"(xx是第二步输入的字符串信息) 技能: 1.打印函数:print() 2.接收控制台输入信息的函数:input() 3.变量的使用:使用变量接受控制台输入信息 示例: >>> print('欢迎你...') 欢迎你... >>> uname=input('请输入您的姓名:') 请输入您的姓名:bobo

  • Linux下搭建Spark 的 Python 编程环境的方法

    Spark编程环境 Spark 可以独立安装使用,也可以和Hadoop 一起安装使用.在安装 Spark 之前,首先确保你的电脑上已经安装了 Java 8 或者更高的版本. Spark 安装 访问 Spark 下载页面 ,并选择最新版本的 Spark 直接下载,当前的最新版本是 2.4.2 .下载好之后需要解压缩到安装文件夹中,看自己的喜好,我们是安装到了 /opt 目录下. tar -xzf spark-2.4.2-bin-hadoop2.7.tgz mv spark-2.4.2-bin-ha

  • python数字图像处理环境安装与配置过程示例

    目录 引言 一.需要的安装包 二.下载并安装 anaconda 三.简单测试 四.skimage包的子模块 引言 一提到数字图像处理编程,可能大多数人就会想到matlab,但matlab也有自身的缺点: 1.不开源,价格贵 2.软件容量大.一般3G以上,高版本甚至达5G以上. 3.只能做研究,不易转化成软件. 因此,我们这里使用python这个脚本语言来进行数字图像处理. 要使用python,必须先安装python,一般是2.7版本以上,不管是在windows系统,还是linux系统,安装都是非

  • Python环境下安装使用异步任务队列包Celery的基础教程

    1.简介 celery(芹菜)是一个异步任务队列/基于分布式消息传递的作业队列.它侧重于实时操作,但对调度支持也很好. celery用于生产系统每天处理数以百万计的任务. celery是用Python编写的,但该协议可以在任何语言实现.它也可以与其他语言通过webhooks实现. 建议的消息代理RabbitMQ的,但提供有限支持Redis, Beanstalk, MongoDB, CouchDB, ,和数据库(使用SQLAlchemy的或Django的 ORM) . celery是易于集成Dja

随机推荐