龙空技术网

新东方技术实践之Django throttle限流模块

闪念基因 136

前言:

现时小伙伴们对“django功能模块”大约比较讲究,小伙伴们都需要了解一些“django功能模块”的相关知识。那么小编在网上汇集了一些对于“django功能模块””的相关知识,希望姐妹们能喜欢,大家快快来学习一下吧!

此篇将基于后端服务进行实践,并针对Django RestFramework框架的相关源码进行语义剖析和流程解读。

软件包版本

本次测试了两个新旧两个后端版本,均可兼容使用:

软件

旧版

新版

python

3.8.16

3.11.2

django

3.0.5

4.1.5

djangorestframework

3.11.0

3.14.0

附源码阅读顺序:

1.python3.11/site-packages/rest_framework/throttling.py

2.python3.11/site-packages/rest_framework/views.py

3.python3.11/site-packages/rest_framework/exceptions.py

一、简易实例代码

假设有个安全需求,要求在某敏感接口设置限流,每个认证用户访问不得超过每分钟30次,否则输出超限等待提示,并执行钉钉告警。先画一个简易的请求步骤流程图:

配置方面很简单,先在settings.py的REST_FRAMEWORK定义全局默认限流触发条件(此处意为服务端收到认证用户请求达到 30次/分钟 即触发):

接着自定义UserDisabledRateThrottle类用于限流处置,继承自UserRateThrottle类:

接着在接口对应的视图类重写关键字属性throttle_classes,指向刚刚自定义的UserDisabledRateThrottle类:

至此限流代码就已完成,下面跟踪一下Django RestFramework底层是如何实现限流功能的。

二、源码剖析(rest_framework/throttling.py)

前面自定义的UserDisabledRateThrottle类继承了UserRateThrottle类,故从这里入手 打开UserRateThrottle源码,看到其继承自SimpleRateThrottle类,其中get_cache_key()是在子类必须要实现的方法,使用scope+ident(即应用范围+UID)将cache_format拼接(生成访问缓存cache_key):

查看SimpleRateThrottle,里面定义了cache_format和需要子类实现的方法get_cache_key(),这里可以看到之所以必须要子类实现,是因为scope和ident都是字符串变量(%代入):

继续向下看,核心方法allow_request()中调用了这个get_cache_key()生成缓存key,并算出是否要触发限流(返回布尔值):

有几个参数很重要,key(缓存的键),history(访问历史),duration(持续时间),num_requests(请求数量),下面一一说明:key是通过self.get_cache_key()获取的,这就是为什么继承SimpleRateThrottle类就必须要实现重写get_cache_key()的原因;history就是上面图中的cache.get返回的这个数据列表,因为符合FIFO原则,可以把它当成队列结构;duration和num_requests是在parse_rate设置的,是从settings.py的DEFAULT_THROTTLE_RATES中split分割出来的固定数值。本文中DEFAULT_THROTTLE_RATES为“30/minute”,所以num_requests为30(单位:次),duration为60(单位:秒)。

了解完参数,接下来看执行赋值的两个重要判断逻辑:第一,128~129行,请求过期时(self.history[-1] <= self.now - self.duration)则删除(self.history.pop())历史记录的对应数据。这里用pop()是因为最新请求在cache中是插入队首的(见138~140行的self.history.insert(0, self.now)),所以pop会删除最老的数据(见上面的cache.get结果)。第二,130~132行,当history的length大于num_requests时,访问频次到达限流阈值,触发throttle_failure限流逻辑(本次重写的正是此部分)返回False;否则插入缓存队列,返回True,继续APIView的后续操作。

可以测试下cache_key的生成过程,当throttle对应的接口未触发限流时,每次请求会在数据库cachetable表中更新cache_key(在settings.py中设置的是CACHE_BACKEND = 'db://cachetable')

通过SQL语句查看库中的cache_key值:(py3) mbp:src $ ./manage.py dbshellmysql> desc cachetable;+-----------+--------------+------+-----+---------+-------+| Field     | Type         | Null | Key | Default | Extra |+-----------+--------------+------+-----+---------+-------+| cache_key | varchar(255) | NO   | PRI | NULL    |       || value     | longtext     | NO   |     | NULL    |       || expires   | datetime(6)  | NO   | MUL | NULL    |       |+-----------+--------------+------+-----+---------+-------+3 rows in set (0.00 sec)访问一下接口,缓存里出现一条数据:mysql> select * from cachetable where cache_key like '%throttle_%';+----------------------+----------------------------------+----------------------------+| cache_key            | value                            | expires                    |+----------------------+----------------------------------+----------------------------+| :1:throttle_user_274 | gAWVDQAAAAAAAABdlEdB2PZuE43BZWEu | 2023-02-01 08:55:38.000000 |+----------------------+----------------------------------+----------------------------+1 row in set (0.00 sec)通过代码获取value值:(py3) mbp:src $ ./manage.py shell_plus# 设置datatable为cache>>> from django.core.cache import cache as default_cache>>> cache = default_cache# 设置key值为上面的cache_key值:>>> key = "throttle_user_274"# 查看用来测试的用户user_id:>>> User.objects.filter(username='admin').first().id274# 注意:我在访问接口时使用的用户是admin,此时查到的admin的id是274,与上面cache_key中的274吻合# scope的值:UserRateThrottle()中已将其设置为"user"# ident的值:用户admin的user_id,即为274# 将上述2个值作为变量代入 cache_format = 'throttle_%(scope)s_%(ident)s',则get_cache_key()结果值为"throttle_user_274",与上面的cache_key值结果相符获取访问的1次接口的对应value:>>> cache.get(key, [])[1675212878.2149289]再访问3次接口,列表数据内容变成4条(从队头插入):>>> cache.get(key, [])[1675212918.3631868, 1675212917.43252, 1675212915.0148559, 1675212878.2149289]

综上所述,throttling.py的核心方法是allow_request(),它实现了用于限流的cache_key和触发逻辑。

三、源码剖析(rest_framework/views.py)

接着看下APIviews里是在哪里、如何调用及捕获allow_request的。再次回到视图类,这里继承了ReadOnlyModelViewSet方法:

打开 rest_framework/viewsets.py 文件,发现如下继承关系:ReadOnlyModelViewSet -> GenericViewSet -> generics.GenericAPIView:

这里generics.GenericAPIView(位置rest_framework/generics.py)又继承自API视图基类views.APIView:

来到views.APIView(位置rest_framework/views.py),它封装了throttle的常用关键字属性(还包含了authentication_classes、permission_classes等常用模块),这里相当于一个钩子:

APIView类在初始化方法中会调用check_throttles()函数:

关键点来了,check_throttles()函数用到了前面提到的限流核心方法allow_request(),当它返回布尔值为False(到达限流阈值)时,throttle_durations列表非空,继而调用self.throttle()模块:

之后self.throttle()会主动抛出一个限流异常:

限流异常捕获类(位置rest_framework/exceptions.py)定义了拼接文案和返回码:

到此,限流的整体核心代码就都关联起来了。

四、测试限流

代码上线后开始测试,频繁访问接口会触发限流,直到下一个统计周期前,页面都将返回如下(返回文案与上图拼接的一致):

验证下日志里的请求返回码(对应 Throttled类中 status_code = status.HTTP_429_TOO_MANY_REQUESTS):

触发后的自定义逻辑(发送钉钉机器人告警)正常执行:

限流功能测试完成,符合预期处置结果。

总结

回顾下刚刚的限流代码,整体流程见下图:

所以实现一个限流功能只需三步:1.设置限流阈值 2.编写处置逻辑 3.应用到视图类

源码方面,有以下几个重点文件:

1.throttling.py文件:实现了访问频次统计的cache_key结构及限流判断逻辑;

2.views.py文件:实现了用户视图基类的限流钩子;

3.exceptions.py文件:捕获限流异常并生成文案。

作者:钟仕骏

来源:微信公众号:新东方技术

出处:

标签: #django功能模块