Django的认证性能优化

背景

由于业务发展,需要开发一套RESTFul API接口。在技术选型上,最开始采用的是最为轻量级的Bottle框架进行开发,后来由于要解决与数据库交互的时候有频繁短连接的问题,同时希望功能稍微多一些(例如管理后台),因此决定选用Django这种稍微重一点的Web框架进行开发,同时加上Django REST framework来做RESTFul API接口。

存在的问题

最终开发出来的Django数据输出平台,去掉了框架自带的Sqlite3数据库,其后台所依赖的数据库和作用分别如下:

  • Redis:在线服务采用的数据库
  • Cassandra:一般性详细查询采用的数据库
  • Mysql:Django自身认证所用的数据库

但是由于需要让其达到QPS 1W以上的响应,开发完之后需要对代码进行优化。

部署上的优化

采用Nginx+Gunicorn+Django进行部署,其中Nginx在最前面,Gunicorn是Web服务器,Django是Web应用。为了做负载均衡,Nginx将请求分别映射到三台服务器上,其中每台服务器开了多个进程。

但是经过部署上的优化之后,虽然QPS有所提高,但一直还是提不起来。所以得继续找原因。

认证模块的优化

问题的发现

为了验证真实性能,首先去掉所有的中间处理环节,所有的请求过来之后均直接返回,这样压测会得到该架构下的极限Web性能,暂定为性能A。然后加上所有的中间处理逻辑代码,再进行压测,这样会得到真实情况下的Web性能,暂定为性能B。

经过测试,发现性能A和新能B相差并不大,然后这就让我很奇怪了,这说明性能提不上去并不是因为我的代码处理逻辑的问题,而是其本身框架性能的瓶颈问题,也就是Django框架其本身的高并发QPS性能有问题,这需要我从源码级别去修改代码。

问题的进一步思考

对于压测得到的性能A,也就是Django Web框架其自身的极限性能为A,我从这里开始去思考:一条请求进来,什么也没干,直接返回了,为什么这里会这么慢,存在瓶颈?后来,仔细思考,原来一条请求进来,是经过了Django REST framework来认证的,也就是说一条请求,经过一次认证,然后到达逻辑处理部分,什么也没做,直接返回给用户。唯一值得怀疑的就是这里的认证导致慢。于是看了认证部分的源码,如下所示:

1
2
3
4
5
6
7
8
9
def authenticate_credentials(self, key):
model = self.get_model()
try:
token = model.objects.select_related('user').get(key=key)
except model.DoesNotExist:
raise exceptions.AuthenticationFailed(_('Invalid token.'))
if not token.user.is_active:
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
return (token.user, token)

以上代码片段来自官网文件:rest_framework.authentication.py中的一段。

这是Token认证方式与数据库交互部分的源码。这里的数据库如果不作配置,默认会是Sqlite3,由于我做了配置,这里指的是MySQL。通过这段代码,可以看到,Token认证方式,读取的是MySQL数据库,也就是说,每来一次请求,都会读取一次MySQL数据库进行匹配认证,也就是说,极限的Django Web性能取决于MySQL数据库的读写性能。对于一个对性能有较高要求的后台Web程序而言,这里就是相当不合理的。理由如下:

  • 由于我的在线服务数据读取来自于Redis缓存数据库,如果说认证部分的极限性能取决于MySQL的读写性能的话,那么我的Redis数据库会完全没用了。也就是说,虽然你后面是一片海洋(单机版Redis的读写性能应该在4W以上),但是前面却是一根细细的水管,这是不合理的。

因此我需要想办法修改这段代码。直接修改源码不太好,正确的做法应该是在我自己的代码中继承这个认证类,然后定义我自己的认证类,来做认证。并且我需要将其改为:一个请求过来,第一次会查MqSQL数据库,然后会将结果缓存至Redis数据库中,如果第二次,同样是该请求过来,则直接查询缓存,这样可以大大提高查询效率,增加Web的QPS。

优化两步曲

正是因为有了以上的想法,下面开始着手进行代码优化。
首先需要定义我自己的认证类,并且该类需要继承至原来的认证类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyTokenAuthentication(TokenAuthentication):
def authenticate_credentials(self, key):
# get cache
token_cache = "token" + key
token_all = cache.get(token_cache)
if token_all:
return (token_all.user, token_all)
model = self.get_model()
try:
token = model.objects.select_related('user').get(key=key)
except model.DoesNotExist:
raise exceptions.AuthenticationFailed(_('Invalid token.'))
if not token.user.is_active:
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
# set cache
if token:
token_cache = 'token' + key
cache.set(token_cache, token)
return (token.user, token)

以上代码继承了原来的TokenAuthentication类,并且重载了方法authenticate_credentials,该段代码可单独放在一个auth.py里面以方便被引入。

然后再Web请求处理逻辑代码处,指定使用自己写的这个类来进行认证,如下:

1
2
3
4
5
6
7
from auth import MyTokenAuthentication
class MyView(APIView):
# you only need to pass one of the three certifications, that you will pass
authentication_classes = (MyTokenAuthentication, SessionAuthentication, BasicAuthentication)
def get(self, request):
result = {"code":0, "result":"OK"}
return Response(result)

以上代码在认证类的地方,写了三种类别,分别是自己定义的Token认证类Session认证以及普通用户名密码认证。含义是:外面过来的请求,只要符合其中的任意一个,即通过了认证。

以上就完成了基于自己认证类的Django Web的性能优化。值得指出的是,在Django模块的设置里面,也可以设置其默认的认证类,如果在全局设置中设置了采用的默认认证类,可以在特有的Views模块中,不指定其采用的认证类。如果同时在默认处和Views模块处指定了其使用的认证类,Views模块中的会优先级会高于设置中的。例如,我虽然在设置中指定采用官方的认证方式,代码如下:

1
2
3
4
5
6
7
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
)
}

以上代码片段取自于settings.py

如果我在Views中指定了认证类,其真正起作用的会是Views中指定的认证类。

总结

虽然对Django进行了优化,但是由于Python语言的特性(存在全局锁),其RESTFul API的性能依然无法达到其本身数据库读写的峰值,再怎么优化,基于Python语言构建的Web服务总有其本身的性能瓶颈所在,并且无法真正利用机器的CPU多核资源。因此如果对性能有较高要求(例如要求QPS达到10W+)的网站或是API服务,最好采用Go语言。在一个项目最开始的技术选型是很重要的,会减少很多无用功和无用的折腾,这也间接说明了,在一个项目中,一个好的架构师的重要性。


【版权声明】
本文首发于戚名钰的博客,欢迎转载,但是必须保留本文的署名戚名钰(包含链接)。如您有任何商业合作或者授权方面的协商,请给我留言:qimingyu.security@foxmail.com
欢迎关注我的微信公众号:科技锐新

kejiruixin

本文永久链接:http://qimingyu.github.io/2018/05/11/Django的认证性能优化/

坚持原创技术分享,您的支持将鼓励我继续创作!

热评文章