前言
最近因为政企部门的工作失误,导致我们的项目差点挂掉,客户意见很大,然后我们只能被动进入007加班状态,忙得嗷嗷叫,直到今天才勉强把项目改完交付,是时候写一个小结。
技术
因为前期需求不明确,数据量不大,人手也不多,所以我直接用Django做了后端,Django自带的admin可以作为管理后台使用,可以很快完成这个需求。
我们的前端有两个,一个数据展示大屏,一个可视化地图。前者使用Vue+ElementUI+DataV实现,后者使用jQuery+百度MapV。
大概的效果如下所示,涉及到数据的部分只能打码,感谢理解~
这个是Django的admin界面,主页是我重新写的
数据展示大屏
可视化地图
技术含量其实不高,但项目在具体实施和落地的过程中,有一些问题和细节,还是有必要记录一下
切换MySQL数据库
开发的时候默认用的SQLite数据库,到了正式环境,需要切换到CS架构的服务器,比如MySQL
我的配置是这样
# 数据库配置
database_config = {
'sqlite3': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
},
'mysql': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'name',
'USER': 'root',
'PASSWORD': 'password',
'HOST': 'mysql' if DOCKER else 'localhost',
'PORT': '3306',
}
}
DATABASES = {'default': database_config['sqlite3']}
这样方便切换不同的数据库,其实还可以把数据库切换用环境变量来控制,docker中使用MySQL,本地开发环境使用sqlite。
MySQL的HOST配置是:'mysql' if DOCKER else 'localhost'
也是为了适配本地环境和docker环境的切换,后续我再封装一下 DjangoStarter 的数据库配置部分,实现前面说的环境变量切换配置。
大量数据导入问题
本项目遇到的第一个麻烦的问题是大量的数据导入
客户提供的数据是Excel格式,大概几百万条吧,我首先使用Python对数据进行预处理,做了一些去重、数据清洗之类的操作,然后导出成JSON文件。
然后在导入Django数据库的时候就遇到了问题,DjangoORM的速度太慢了!
让他跑数据跑了一个晚上,才导入了80w条数据左右,这肯定不行啊,因为被业务部门捅了娄子,项目还有几天的时间就要上线了,要赶!总共数据有几百万呢……
没办法,只能直接上SQL了,掏出Navicat,连上服务器的MySQL数据库,然后把数据直接导入临时表,再用SQL一番折腾,导入到Django生成的表里,这样数据的问题就搞定了。
(当然后续还有一系列的数据问题,前期的数据清洗还是不够的,后面发现的一些数据缺失啥的问题,在赶进度的过程中边处理,做了一些补救措施,最后也还好勉强可以用)
下次数据清洗还是得试一下Pandas这种专业的工具,单靠Python本身不够。
接口缓存
由于数据量太大,有几个需要计算操作的接口是比较慢的,该优化的暂时都优化了(下面或许会写一下DjangoORM的优化),所以只能上缓存了
项目用了我的「DjangoStarter」项目模板,本身集成了Redis支持,所以缓存直接用Redis的就好了。
缓存有两种使用方式,用Django默认的cache_page
装饰器,或者第三方库rest_framework_extensions
。
前者作用于 function view
上,当然 class view
也能用,但是得加 method_decorator
,然后得自己封装一个缓存过期配置。
后者可以使用 rest_framework
的缓存配置,相对来说更方便,只是需要安装一个库。(另外提一点,rest_framework_extensions
这个库还有其他的一些功能,有空再介绍,感兴趣的同学可以探索一下)
最终我选择了第二个,哈哈~
不过我都介绍一下吧,很简单
cache_page 的使用
from django.views.decorators.cache import cache_page
from rest_framework.decorators import api_view
@cache_page(CACHE_TIMEOUT)
@api_view()
def overview(request: Request):
...
其中 CACHE_TIMEOUT
的单位是秒,也可以设置成 None
,这样缓存就永不过期了。
rest_framework_extensions
rest_framework
的缓存配置
REST_FRAMEWORK = {
# 缓存过期时间
'DEFAULT_CACHE_RESPONSE_TIMEOUT': 60 * 60,
}
安装
pip install drf-extensions
使用
常用的两种方式,装饰器和Mixin
装饰器用法
from rest_framework_extensions.cache.decorators import cache_response
class ViewSet(viewsets.ModelViewSet):
...
@cache_response()
def list(self, request, *args, **kwargs):
...
Mixin用法,注意 CacheResponseMixin
要放在 ModelViewSet
的前面
from rest_framework_extensions.cache.mixins import CacheResponseMixin
class ViewSet(CacheResponseMixin, viewsets.ModelViewSet):
...
参考资料
- drf实现常用数据缓存:https://segmentfault.com/a/1190000018170069
响应数据量太大问题
前面那个可视化地图的页面,需要获取几万条人员信息,这个接口一开始没做优化,返回的数据大小有50MB,就很离谱,单纯网络传输就用了40秒,卡的一批。
前端小伙伴反映这个问题后,我查看一下服务器的日志,发现响应时间就达到了5秒,这忍不了啊。一开始是加了缓存,效果显著,响应时间直接压缩到了0.1秒!不过没用,传输时间还是很长。
继续分析,因为是用DjangoStarter的自动代码生成功能实现的接口,所以请求之后会默认返回人员信息的所有字段,但很明显,地图上只需要三个字段:ID、经纬度。
所以我重新写了个 serializer
class BasicPersonSerializer(serializers.ModelSerializer):
class Meta:
model = BasicPerson
fields = ['id', 'address_lng', 'address_lat']
然后在 viewsets
里也重写了 list
方法,用上新定义的这个 serializer
class BasicPersonViewSet(viewsets.ModelViewSet):
...
@cache_response()
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = BasicPersonSerializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = BasicPersonSerializer(queryset, many=True)
return Response(serializer.data)
然后再来测试一下,响应时间69毫秒,数据量变成4MB+,效果很显著!
整个传输时间只需要1.2秒左右~
妙啊,但是我还不满足,既然还有优化空间,那就继续优化。
给接口加个gzip压缩吧~
为图省事,直接上全站压缩
MIDDLEWARE = [
'django.middleware.gzip.GZipMiddleware',
]
搞定
再次请求看看
数据大小被压缩到480kb,传输时间只需要356毫秒!妙啊
这个问题就搞定了。
参考资料:Django使用gzip实现压缩请求 - https://www.pythonf.cn/read/116970
聚合查询
既然用Django了,有了这么方便好用的ORM,就别老想着用什么SQL语句了。
我在这个项目里比较常用的是这几个
- aggregate
- annotate
- values (虽然这个可能不算,但用的很多)
- Count
- Sum
每个函数的具体用法我就不复制粘贴了,看下面的参考资料吧~
参考资料:
- 聚合 | Django 文档 | Django (djangoproject.com)
- 075: 【Django数据库】ORM聚合函数详解-Sum - zheng-weimin - 博客园 (cnblogs.com)
性能优化
Django性能确实有点一言难尽,性能优化也老生常谈了,不过就实际运用而已,我们这也还是在探索之中,因为大部分场景是够用的,没有多高的并发。
不过有个比较慢的地方是展示大屏的数据接口,因为要汇集好几个表的数据进行统计,几十万上百万的数据,有点慢,一开始响应时间需要40秒,这也太离谱了。
肯定是得优化的,优化思路从减少数据库访问次数、合并运算、增加缓存入手,优化完成之后冷启动速度5秒,命中缓存60毫秒内,效果还是可以的。
关于性能优化这块以后还是得继续看看,Django有太多可以优化的地方了……
(或者不行的话直接用.Net Core这种高性能的平台重写?)
部署
部署方面依然是 uwsgi / docker / docker-compose 这套组合,之前用了好多次了,比较稳定,配置文件都是现成的,直接把代码上传服务器 up一下就启动了,非常方便。
对了还需要配置一下nginx,uwsgi是专有协议,需要做个转发,才能使用http访问到。
部署之后关闭debug模式,还需要进入docker容器里,在bash里执行 collectstatics
收集静态文件。
之后要更新的话,只需要在pycharm里配置 commit 的时候顺便deploy,把修改的代码文件提交到服务器,然后修改一下 readme.md
文件(我配置了监听这个文件)即可重启服务。
项目监控
对了,还有一个关键的,项目上线之后,需要监控项目的运行状态
对于Django项目,我用的是sentry来做监控,很好用,集成也很方便,这个sentry我准备后面写篇文章来介绍。
PS:对于.Net Core项目,我用的是.Net专用的ExceptionLess,这个界面很简洁直观,但文档没有sentry详细,不过docker搭建还是很方便的。
可以看我之前的这篇文章:[Vue2.x项目整合ExceptionLess监控](
小结
OK,大概就是这样了,项目也不是到这里就结束了,只是暂告一个段落,接下来看看客户那边有什么新的需求再来继续开发。
Django框架陆陆续续也用了两三年的时间了,虽然应用场景都比较简单,但属于是基本摸清了开发流程和定制的上限,像django-admin这种内置的管理后台,尽管有大量的自定义配置功能,还有simpleUI这种优秀的第三方界面,但他的上限还是摆在那,遇到稍微需要定制化的管理后台需求,还是得自己搞一套,好在用RestFramework写接口实在是方便,接口导出来,套个vue+elementUI的前端,一套后台就搞定了。
如果还需要数据大屏这类可视化功能,我们现在也积累了一些这方面的技术和经验,可以比较快的出产品。
接下来很多新的项目还是优先使用.Net Core技术,从稳定性和性能的方面考虑,我还是更信赖.Net Core。
Django的优势主要还是在于开发效率和背靠Python社区的强大生态,不过性能和部署方便就要稍逊一点点。
总之,都好用,看场景使用~