背景
由于业务发展,需要开发一套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 | def authenticate_credentials(self, key): |
以上代码片段来自官网文件:rest_framework.authentication.py
中的一段。
这是Token
认证方式与数据库交互部分的源码。这里的数据库如果不作配置,默认会是Sqlite3
,由于我做了配置,这里指的是MySQL
。通过这段代码,可以看到,Token
认证方式,读取的是MySQL
数据库,也就是说,每来一次请求,都会读取一次MySQL
数据库进行匹配认证,也就是说,极限的Django Web
性能取决于MySQL
数据库的读写性能。对于一个对性能有较高要求的后台Web程序而言,这里就是相当不合理的。理由如下:
- 由于我的在线服务数据读取来自于
Redis
缓存数据库,如果说认证部分的极限性能取决于MySQL的读写性能的话,那么我的Redis数据库会完全没用了。也就是说,虽然你后面是一片海洋(单机版Redis的读写性能应该在4W以上),但是前面却是一根细细的水管,这是不合理的。
因此我需要想办法修改这段代码。直接修改源码不太好,正确的做法应该是在我自己的代码中继承这个认证类,然后定义我自己的认证类,来做认证。并且我需要将其改为:一个请求过来,第一次会查MqSQL数据库,然后会将结果缓存至Redis数据库中,如果第二次,同样是该请求过来,则直接查询缓存,这样可以大大提高查询效率,增加Web的QPS。
优化两步曲
正是因为有了以上的想法,下面开始着手进行代码优化。
首先需要定义我自己的认证类,并且该类需要继承至原来的认证类,代码如下:
1 | class MyTokenAuthentication(TokenAuthentication): |
以上代码继承了原来的TokenAuthentication
类,并且重载了方法authenticate_credentials
,该段代码可单独放在一个auth.py
里面以方便被引入。
然后再Web请求处理逻辑代码处,指定使用自己写的这个类来进行认证,如下:1
2
3
4
5
6
7from 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 | REST_FRAMEWORK = { |
以上代码片段取自于settings.py
如果我在Views中指定了认证类,其真正起作用的会是Views中指定的认证类。
总结
虽然对Django
进行了优化,但是由于Python语言的特性(存在全局锁),其RESTFul API
的性能依然无法达到其本身数据库读写的峰值,再怎么优化,基于Python语言构建的Web服务总有其本身的性能瓶颈所在,并且无法真正利用机器的CPU多核资源。因此如果对性能有较高要求(例如要求QPS达到10W+)的网站或是API服务,最好采用Go语言。在一个项目最开始的技术选型是很重要的,会减少很多无用功和无用的折腾,这也间接说明了,在一个项目中,一个好的架构师的重要性。
【版权声明】
本文首发于戚名钰的博客,欢迎转载,但是必须保留本文的署名戚名钰(包含链接)。如您有任何商业合作或者授权方面的协商,请给我留言:qimingyu.security@foxmail.com
欢迎关注我的微信公众号:科技锐新