信息安全聚合 Sec-News 的重构之路 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
phithon
V2EX    Python

信息安全聚合 Sec-News 的重构之路

  •  
  •   phithon
    phith0n 2016-01-14 14:45:03 +08:00 3173 次点击
    这是一个创建于 3558 天前的主题,其中的信息可能已经有所发展或是发生改变。

    不知道什么时候突然发现我已经稳定运行了近半年的 sec-news ( http://wiki.ioin.in )突然变得特别慢,为跳转效率我也是尝试了很多方法,比如加缓存。我使用了一个叫 flask-cache 的缓存: https://pythonhosted.org/Flask-Cache/ ,很好用的 cache 。

    特别喜欢 python 的一点就是,修饰器(@Decorator )的存在,让很多功能变得简单。flask-cache 里有一种 cache 方式叫 Memoization ,它可以简单地用 Decorator 的方式放在任意函数上。根据函数参数的值,来缓存函数的结果。

    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),就是另一个缓存了。

    我将 flask-cache 加到 flask 的 view 里,这样就可以缓存整个页面了。

    但是,缓存永远不是解决效率问题的根本方法,解决问题是找到根本原因。我仔细分析了我的 sec-news ,我认为以前使用的 mongodb 数据库,是导致整个网站运行慢的原因。

    也的确,我设计 mongodb 的概念和以前设计 mysql 的概念完全不同,我设计了这样一个集合:

    Rss

    • id
    • url
    • title
    • 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

    分享几张重构后后台的截图:

    6 条回复    2016-01-14 17:04:08 +08:00
    firebroo
        1
    firebroo  
       2016-01-14 15:18:57 +08:00
    pp 牛。
    est
        2
    est  
       2016-01-14 15:21:10 +08:00
    最后居然没广告链接。于是又从头仔细看了一遍。
    Braid
        3
    Braid  
       2016-01-14 15:21:35 +08:00
    pp 牛。
    Braid
        4
    Braid  
       2016-01-14 15:22:00 +08:00
    观摩 p 牛
    l0wkey
        5
    l0wkey  
       2016-01-14 15:26:08 +08:00
    @est 哈哈哈,顶部有
    phithon
        6
    phithon  
    OP
       2016-01-14 17:04:08 +08:00
    @est 失误了,我应该放在最底下!
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1595 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 31ms UTC 16:24 PVG 00:24 LAX 09:24 JFK 12:24
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86