這段時間在作一個nginx + uwsgi + python的項目,有個需求是須要在服務運行過程當中能夠改變配置並生效,能夠理解爲熱重載. 以前這些配置都是寫死在項目的配置文件中的基礎配置,通常就是python項目中的config.py文件. 如今配置變動使用了開源的apollo做爲管理端,須要python使用client對接apollo.html
先看一份常見的python後臺使用uwsgi的配置:java
test@python:~/app$ cat uwsgi.ini [uwsgi] module = app wsgi-file = app.py master = true processes = 4 # 多個work進程 enable-threads = true # 容許啓動多線程 #lazy-apps = true # 後面再說 http = :3000 die-on-term = true pidfile = ./uwsgi.pid chdir = /home/test/app disable-logging = true log-maxsize = 5000000 daemonize = /home/test/app/log.log
這裏給出python代碼的demo app.py:python
from flask import Flask, jsonify, request from apollo import Config cf = Config("test", "application") print("----------key-----------") print(cf.SQLALCHEMY_TRACK_MODIFICATIONS) # 嘗試獲取一些配置 print(cf.LOG_NAME) print("----------key-----------") app = Flask(__name__) @app.route('/') def hello_world(): key = request.values.get('key') new = getattr(cf, key) # 嘗試實時獲取配置 return jsonify({'data': new, 'apo': cf.apo.get_value(key), "my": cf.SQLALCHEMY_POOL_SIZE}) application = app # for uwsgi.ini if __name__ == "__main__": app.run(port=5000)
再看看這個配置啓動後的效果:mysql
test@python:~/app$ ps -ef|grep uwsgi.ini test 16224 1 0 14:36 ? 00:00:00 uwsgi --ini uwsgi.ini test 16225 16224 0 14:36 ? 00:00:00 uwsgi --ini uwsgi.ini test 16226 16224 0 14:36 ? 00:00:00 uwsgi --ini uwsgi.ini test 16227 16224 0 14:36 ? 00:00:00 uwsgi --ini uwsgi.ini test 16228 16224 0 14:36 ? 00:00:00 uwsgi --ini uwsgi.ini test 16229 16224 0 14:36 ? 00:00:00 uwsgi --ini uwsgi.ini test 16378 15998 0 14:39 pts/48 00:00:00 grep --color=auto uwsgi.ini
每次在apollo後臺變動配置時明明配置的localfile本地文件已經變動可是進程中的cache就是沒變...查看了apollo開源說明中推薦的三種python client,發現實現方式都是大同小異,主要就是啓動守護線程長連接pull服務端的接口,服務端有變動時接口就能訪問通,進而觸發這個守護線程的動做去更新cache和localfile,上面說了localfile已經有了更新的動做爲啥cache沒被更新呢? 帶着疑問去看了這三個開源庫的issues,而後發現uwsgi+django項目中配置的apollo, 不能獲取最新apollo數據 嗯,看來是通病了...linux
翻了下其餘語言上沒啥相似問題,那會不會是python的特點,先來個手動多進程試試:nginx
1. 執行python app.py 2. 修改app.py中的端口號 3. 執行python app.py 4. 重複2,3 5. 注意看打印的日誌 6. 試着訪問下設置的端口 curl "127.0.0.1:3000" 7. 修改apollo的配置 8. 看看日誌,再執行curl "127.0.0.1:3000",看看獲取的配置是否是最新的.
而後發現沒啥問題啊,每一個實例都能訪問到最新的,日誌中都打印了更新cache和localfile的日誌.那麼就排除了python的問題,聚焦到uwsgi的配置上看看吧,網上搜的話比較凌亂,通常搜官方文檔好了,如這裏Python/WSGI應用快速入門,而後就會看到左邊有個關於Python線程的注意事項嗯,難道是我沒加enable-threads = true致使的? 立馬加上試試,效果仍是不行,那繼續看文檔吧,翻看目錄直到看到這句 優雅重載的藝術,下面摘抄文檔中的一些關鍵語句:git
這是uWSGI項目具備爭議的選擇之一。 默認狀況下,uWSGI在第一個進程中加載整個應用,而後在加載完應用以後,會屢次 fork() 本身。這是常見的Unix模式,它可能會大大減小應用的內存使用,容許不少好玩的技巧,而在一些語言上,可能會讓帶給你不少煩惱。 儘管它的名聲如此,可是uWSGI是做爲一個Perl應用服務器 (它不叫作 uWSGI,而且它也並不開源) 誕生的,而在Perl的世界裏,preforking通常是一種受到祝福的方式。 然而,對於許多其餘的語言、平臺和框架來講,這並非真的,所以,在開始處理uWSGI以前,你應該選擇在你的棧中如何管理 fork() 。 而從「優雅重載」的角度來看,preforking極大的提升了速度:只加載你的應用一次,而生成額外的worker將會很是快。避免棧中的每一個worker都訪問磁盤會下降啓動時間,特別是對於那些花費大量時間訪問磁盤以查找模塊的框架或者語言。 不幸的是,每當你的修改代碼時,preforking方法迫使你重載整個棧,而不是隻重載worker。 除此以外,你的應用可能須要preforking,或者因爲其開發的方式,可能徹底因其崩潰。 取而代之的是,lazy-apps模式會每一個worker加載你的應用一次。它將須要大約O(n)次加載 (其中,n是worker數),很是有可能會消耗更多內存,但會運行在一個更加一致乾淨的環境中。 記住:lazy-apps與lazy不一樣,前者只是指示 uWSGI對於每一個worker加載應用一次,然後者更具侵略性些 (通常不提倡),由於它改變了大量的內部默認行爲。
看來是默認配置致使了多進程多線程狀況下,uwsgi加載完後第一個完整的work後,剩下processes中配置的work都是經過fork來的,看看uwsgi的啓動日誌也會發現的確只加載了一個app,每次操做也只有一個守護線程在監聽和打印日誌,那爲啥fork來就不是完整的服務了呢,這就要說到unix fork的原理和實現了.github
0. fork()函數用於從一個已經存在的進程內建立一個新的進程,新的進程稱爲「子進程」,相應地稱建立子進程的進程爲「父進程」。使用fork()函數獲得的子進程是父進程的複製品,子進程徹底複製了父進程的資源,包括進程上下文、代碼區、數據區、堆區、棧區、內存信息、打開文件的文件描述符、信號處理函數、進程優先級、進程組號、當前工做目錄、根目錄、資源限制和控制終端等信息,而子進程與父進程的區別有進程號、資源使用狀況和計時器等。 1. 普通的函數調用,調用一次,返回一次,可是fork()調用一次,返回兩次。由於操做系統自動把當前進程(父進程)複製了一份(子進程),而後分別在父進程和子進程內返回。 2. 子進程永遠返回0,父進程返回子進程的ID。 3. 一個父進程能夠fork()出不少個子進程。所以,父進程要記下每一個子進程的ID,而子進程只須要調用getppid()就能夠拿到父進程的id。getpid()能夠拿到當前進程id 4. 父進程、子進程執行順序沒有規律,徹底取決於操做系統的調度算法。 5. 若是父進程有多個線程會不會複製父進程的多個線程呢?其實子進程建立出來時只有一個線程,就是調用fork()函數的那個線程。
也就是說 uwsgi fork進程(不區分進程和線程)的時候只會把當前正在執行的app線程複製一份,而不會把隨app線程初始化過程當中產生的守護線程apollo-client也fork一份,那麼解決起來就簡單了,配置下lazy-apps = true就能夠了,每次fork都是一個真正完整的app進程包含了app線程和apollo-client線程.若是我還沒說清楚的話,能夠參考這裏
謹慎使用多線程中的forkfork多線程進程時的坑(轉)redis
那麼天然就想到既然cache是每一個進程獨立的,那就乾脆去掉cache使用localfile,也很簡單粗暴是能夠完成多進程共享配置的功能,每次訪問配置都作下文件IO操做,這裏不是什麼訪問量大的服務的話能夠這麼操做,下面再說說其餘方案.算法
重構apollo client中線程中的cache緩存的存儲方式,好比切換爲redis,一樣是IO操做比每次都http直接查詢apollo配置接口要好些,要是是遠程redis-server那網絡延時也不可忽略,進而考慮本地redis或者使用uWSGI緩存框架
使用緩存API,在應用中訪問緩存 你能夠經過使用緩存API,訪問你的實例或者遠程實例中的各類緩存。目前,公開了如下函數 (每一個語言對其的命名可能與標準有點不一樣): cache_get(key[,cache]) cache_set(key,value[,expires,cache]) cache_update(key,value[,expires,cache]) cache_exists(key[,cache]) cache_del(key[,cache]) cache_clear([cache]) 若是調用該緩存API的語言/平臺區分了字符串和字節 (例如Python 3和Java),那麼你必須假設鍵是字符串,而值是字節 (或者在java之下,是字節數組)。不然,鍵和值都是無特定編碼的字符串,由於在內部,緩存值和緩存鍵都是簡單的二進制blob。 expires 參數 (默認爲0,表示禁用) 是對象失效的秒數 (並當未設置 purge_lru 的時候,由緩存清道夫移除,見下) cache 參數是所謂的「魔法標識符」,它的語法是
好了,到這裏這個問題到此解決了一半. 爲何說一半呢,由於這些配置都是普通配置並非相似mysql,redis的配置信息,這些配置不會再修改配置後從新生成實例,也就無法使用最新的mysql或redis配置,那麼怎麼辦呢? 下面說說重載服務.
如何優化的重啓服務?
再守護線程的監聽函數最後建加上回調,回調命令函數的實現以下,pid_path是uwsgi啓動後生成的pid文件地址.簡單粗暴但有效.
# 重載uwsgi def relaod_uwsgi(pid_path): """選用方案1""" print("------------relaod_uwsgi---------------") val = os.system('uwsgi --reload {}'.format(pid_path)) print(val) if val: print("重啓可能遇到了問題...")
py-auto-reload argument: 必需參數 parser: uwsgi_opt_set_int flags: UWSGI_OPT_THREADS|UWSGI_OPT_MASTER help: 監控python模塊mtime來觸發重載 (只在開發時使用) py-autoreload argument: 必需參數 parser: uwsgi_opt_set_int flags: UWSGI_OPT_THREADS|UWSGI_OPT_MASTER help: 監控python模塊mtime來觸發重載 (只在開發時使用) python-auto-reload argument: 必需參數 parser: uwsgi_opt_set_int flags: UWSGI_OPT_THREADS|UWSGI_OPT_MASTER help: 監控python模塊mtime來觸發重載 (只在開發時使用) python-autoreload argument: 必需參數 parser: uwsgi_opt_set_int flags: UWSGI_OPT_THREADS|UWSGI_OPT_MASTER help: 監控python模塊mtime來觸發重載 (只在開發時使用) py-auto-reload-ignore argument: 必需參數 parser: uwsgi_opt_add_string_list flags: UWSGI_OPT_THREADS|UWSGI_OPT_MASTER help: 自動重載掃描期間,忽略指定的模塊 (能夠屢次指定)
這些配置是監控特定文件來重載uwsgi服務的,那麼咱們只要改下localfile的名字爲py結尾,那差很少也是沒問題的.
最後想說點私貨,人類不可能想象出超越意識範圍內的東西,好比作夢,夢中的東西確定都是平時生活中雞零狗碎的拼湊和假裝,代碼也是.創新也是.
這裏整理了一個採坑後貢獻出來的python client demo,主要代碼是apollo-client-python中的,我在改了裏面的http請求使用requests,而後作了點淺淺的封裝.歡迎你們star!
這篇隨記也歸檔到了這裏python-mini,也歡迎歡迎你們star!
請用腦子想一想,試着將顯示的配置調整以適應你的需求,或者建立新的配置。 每一個應用和系統都是彼此之間不一樣的。 做出選擇以前請進行實驗。
上面那句不是我說的,是uwsgi文檔說的.