不知道何时忽然发现我已经稳定运行了近半年的 sec-news ( http://wiki.ioin.in )忽然变得特别慢,为跳转效率我也是尝试了不少方法,好比加缓存。我使用了一个叫 flask-cache 的缓存: https://pythonhosted.org/Flask-Cache/ ,很好用的 cache 。php
特别喜欢 python 的一点就是,修饰器(@Decorator )的存在,让不少功能变得简单。 flask-cache 里有一种 cache 方式叫 Memoization ,它能够简单地用 Decorator 的方式放在任意函数上。根据函数参数的值,来缓存函数的结果。python
class Person(db.Model): @cache.memoize(50) def has_membership(self, role_id): return Group.query.filter_by(user=self, role_id=role_id).count() >= 1
上面是文档里给出的一个 example ,其缓存了 has_membership 函数,当咱们调用 has_membership(1)的时候,就缓存下 50 秒这个函数的返回值。那么下次再调用 has_membership(1)的时候,就会直接返回缓存的结果,但若是你调用 has_membership(2),就是另外一个缓存了。mysql
我将 flask-cache 加到 flask 的 view 里,这样就能够缓存整个页面了。nginx
可是,缓存永远不是解决效率问题的根本方法,解决问题是找到根本缘由。我仔细分析了个人 sec-news ,我认为之前使用的 mongodb 数据库,是致使整个网站运行慢的缘由。git
也的确,我设计 mongodb 的概念和之前设计 mysql 的概念彻底不一样,我设计了这样一个集合:redis
Rsssql
idmongodb
url数据库
titlejson
posts (array)
这个集合用来存储 Rss 数据,好比 http://www.leavesongs.com/rss.php ,这是一个订阅 Rss 。这个订阅的内容,其实就是它的文章( posts ),个人订阅列表中有几个 Rss ,其中包含的文章已经超过 1000 篇,也就是 posts 数组大小已经超过 1000 ,且数组中每篇文章我都保存了文章的标题和内容。
因此其实当咱们没有设计好 ORM 的状况下,提取出这个 Rss 集合,将占用大量内存,致使 Sec-news 总体速度变慢。
这是我以为影响网站效率的最大缘由。备份数据后,我删掉了全部文章的内容,再次测试,结果也同样,速度并无变快。
我开始怀疑架构问题,我开始怀疑是 mongodb 哪里有坑被我踩中了。这种问题对于半吊子开发我来讲,实在是难以发现,难以解决。但在电脑维修界,有著名的『万金油定律』——重启、重装、换电脑。既然解决不了问题,不如用简单点的办法规避问题。
我如今的位置可能位于重启到重装这条路上,在替换一些数据(重启)的状况下并不能解决效率问题,那么我就须要思考『重装』的问题了。所谓的重装,也就是换掉 mongodb 。
sec-news 在开发的时候就已经作到了 MVR ( Model - View - Route ),代码耦合性也比较低,但实际上替换数据库的过程仍是须要重构大量代码,主要缘由就是 mongodb->mysql 是一场 Nosql 到 Sql 的转变,基础架构须要调整。
不过总代码量也不大,整个 view + model 也只有 700 行代码左右,须要改动的部分不超过 200 行。重构过程还改进了不少功能、用户体验方面的问题(主要是后台)。
重构后的 sec-news 仍是用 ORM ,我在 peewee 和 sqlalchemy 中选择了后者,由于 flask-sqlalchemy 是一个比较成熟的搭配,在实际开发中我比较看重稳定性,虽然我的感受 peewee 更『酷』。
除了替换数据库。细节上还有一处改进:我将 flask 原生的 client-side-session 换成了一个叫"flask-session"的 server-side-session 的插件,以规避前段时间本身发现的『验证码绕过漏洞』。 flask-session 储存在 redis 中,我喜欢 redis 赛过 memcache ,缘由是 memcache 所拥有的功能 redis 都有,但 redis 所拥有的功能 memcache 并不必定有,因此我通常都不用 memcache 。
另外,我实现了后台多用户权限控制,其实提及来也比较简单:
def check_role(request_role): def do_check(role_array): def check(func): @functools.wraps(func) def do_function(*args, **kwargs): if flask.session.get("user_id") > 0: if flask.session.get("role") in role_array: return func(*args, **kwargs) else: return permission_deny(*args, **kwargs) else: return flask.redirect(flask.url_for("login")) return do_function return check return do_check(request_role) @app.route('/admin') @check_role(["admin", "user"]) def admin(): #show administrator index page @app.route('/admin/add') @check_role(["admin"]) def add(): #add a new administrator
再次感谢 python 的 Decorator ,我用一个简单的 check_role 函数便可实现权限控制。好比 admin 函数,能够容许 user 、 admin 两个角色访问,而 add 函数就只容许 admin 角色访问,假设既不是 user 也不是 admin ,就直接跳到 login 页面。
Decorator 也是我迟迟放不下 python 的缘由,假设 php 里也加入这个语法糖,那我保准不会用 python 写网站了,不少方面仍是 php 更方便。
在 Route 方面,我也作了一些改进。由于 mongodb 的默认索引_id 是一个 24 位 hash 值,不容易被用户猜到,而 mysql 的主键一般是一个 AUTO_INCREMENT 的数字,好事者只须要编写一个脚本便可遍历个人全部文章,我不喜欢这样。
我用了 hashids 这个库,将 int 类型的 id 转换成了一个 hashids ,好事者猜不到这个字符串,也就没法遍历个人文章了。(固然能够写爬虫爬取,但这和遍历有本质区别)
重构用了大概一天半,传到原来的服务器上,发现……这 TM 仍是同样慢啊……我真是错怪 mongodb 了,我给你赔罪!
那么如今,『重装』这条路也死了,并无解决问题。
最后也就只剩『换电脑』了,我一咬牙一跺脚买了一台阿里云青岛的服务器(按流量计费,算下来仍是不贵的,一个月 50RMB 左右)。这时候我基本上已经心力交瘁了,只想尽快把问题解决我好干别的。
我用最快的速度部署好服务器:
apt-get update apt-get install nginx mysql-server mysql-client redis-server libjpeg-dev git clone xxx pip install -r requirements.txt pip install gunicorn supervisor
直接安默认的,能用就行。由于服务器带的 ubuntu14 没有 systemd ,我就选择用 supervisor 管理个人 gunicorn 服务, nginx 简单配了一下就了好了, mysql 最开始也直接用 root 帐号。
服务器移到国内,还有一个问题就是域名,个人 leavesongs.com 是没有备案的,因此新的 sec-news 域名不能再用这个子域名了。还好本身手上刚备案了一个新域名,我就直接用新域名下的子域名做为 sec-news 的域名。
那么老域名的"遗产"怎么办?
如上图,有些网站还保留着个人老域名下的连接,我想尽可能保持一切不变。因而我从老数据库导出了一个 json 格式的对象:_id : url ,在老 vps 上作了个简单的转发:
location ^~ /url/ { rewrite ^/url/(.*)$ /old.php?hash=$1 last; } location = /old.php { fastcgi_pass unix:/var/run/php5-fpm.sock; fastcgi_index index.php; include fastcgi.conf; } location / { rewrite ^/(.*) http://wiki.ioin.in/$1 permanent; }
将全部 /url/开头的连接转发到 old.php 里处理,其余连接就直接 301 到新域名下。那么 old.php 就专门处理之前_id 是 24 位 hash 的连接:
<?php $old_data = json_decode(file_get_contents('olddata.txt'), TRUE); $hash = isset($_GET['hash']) ? $_GET['hash'] : ""; if($hash && array_key_exists($hash, $old_data)) { header('Location: ' . $old_data[$hash]); } else { header('Location: http://wiki.ioin.in/url/' . $hash); }
这样就能保证之前的连接所有可以访问,新连接直接跳转到新域名。
后面有空闲时间又慢慢优化了许多地方,找到几个小伙伴一块儿更新一些好文章, sec-news 正式复活了。
但愿我此次重构之路对你们的开发有启发,也欢迎你们订阅 Sec-News 的 RSS ,主页: http://wiki.ioin.in ,订阅: http://wiki.ioin.in/atom
分享几张重构后后台的截图: