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

不知道何時忽然發現我已經穩定運行了近半年的 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 的域名。

那麼老域名的"遺產"怎麼辦?

400f4ee7jw1ezz0rwroe1j21320w2qdu.jpg

如上圖,有些網站還保留着個人老域名下的連接,我想盡可能保持一切不變。因而我從老數據庫導出了一個 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

分享幾張重構後後臺的截圖:

400f4ee7jw1ezz0t4jcjhj21kw0snwjr.jpg

400f4ee7jw1ezz0tb88i4j21ku0ksgp2.jpg

400f4ee7jw1ezz0tjdo64j21jm0tuwqu.jpg

400f4ee7jw1ezz14hna7yj21k40x4gs0.jpg

400f4ee7jw1ezz0u0b601j212c0jkgn3.jpg

400f4ee7jw1ezz0u7s1byj212e0py0ul.jpg

相關文章
相關標籤/搜索