眾所周知,用指令碼語言編寫的服務(wsgi介面)都需要一個server容器,常見的如php的php-fpm, lightd等。python中一般是用的uwsgi,uwsgi是在wsgi的基礎上的一種新的協議,可以用來部署python等指令碼程式的運行。然而在不熟悉uwsgi的代碼架構和c調用python的api情況下進行開發可能會遇到一些意想不到的問題。
我們先看一段代碼,下面這段代碼是用的Flask架構,每次請求的時候會把COUNT的值先減一再加一,最後再乘二。如果請求50次,其最終的結果應該是2的50次冪。
from flask import Flask, requestCOUNT = 1 app = Flask(__name__)@app.route('/test_uwsgi')def index(): global COUNT COUNT=COUNT-1 COUNT=COUNT+1 COUNT=COUNT*2 print COUNT return 'OK'
17179869184343597383686871947673613743895347227487790694454975581388810995116277762199023255552439804651110487960930222081759218604441635184372088832703687441776641407374883553282814749767106565629499534213121125899906842624
這是直接執行50次index函數得到的最後幾行的結果,可得結果為2的50次冪。
536870912 10737418242147483648429496729685899345921717986918434359738368 687194767361374389534722748779069445497558138881099511627776 2199023255552 4398046511104879609302220817592186044416 3518437208883270368744177664 1407374883553282814749767106565629499534213121125899906842624
這是通過ab測試,用多個並發訪問/test_uwsgi介面50次得到的最後幾行的結果。可以看出最終的結果肯定是個異常數字。為什麼程式在uwsgi中運行時的運行結果會出現異常呢。
其實大家通過閱讀這個簡單的例子就可以發現,這種例子一般都是用來示範多線程共用資料同步問題的時候,如果不加鎖會暴露問題的例子。下面的代碼我們就在修改共用資源COUNT的時候加上互斥鎖,看看有沒有什麼變化。
from flask import Flask, requestimport threadingmutex = threading.Lock()COUNT = 1 app = Flask(__name__)@app.route('/test_uwsgi')def index(): global COUNT global mutex mutex.acquire() COUNT=COUNT-1 COUNT=COUNT+1 COUNT=COUNT*2 print COUNT mutex.release() return 'OK'
上面的代碼也是放到uwsgi的容器裡面運行,通過http介面多個並發訪問50次,得到的結果是正確的。但是這是為什麼呢。在我們原來的python代碼中並沒有寫任何涉及多進程的操作,雖然uwsgi在設定檔中開啟了多個線程可以並發的處理請求,但是按筆者原來的理解,不是應該每個線程執行自己獨立的Python解譯器嗎。每個線程在運行python指令碼的時候的資料不應該是隔離的嗎。
為了弄明白上面的問題,我們不得不研究研究uwsgi及其server架構中的結構和設計。
UWSGI是在python中廣泛使用的一個伺服器應用程式容器,類似於php上常見的wsgi協議的伺服器應用程式容器,如mod-php、php-fpm、lightd等。uwsgi協議是在原有的wsgi協議之上新增了一套uwsgi的協議。
通過研讀uwsgi的源碼(core/uwsgi.c core/loop.c core/init.c core/master_util.c core/util.c),可以知道uwsgi的server設計,採用的是UNX書中介紹歸納的伺服器程式設計範式8,暨TCP預先建立線程伺服器程式,每個線程各自accept。
int main(int argc, char *argv[], char *envp[]) {uwsgi_setup(argc, argv, envp);return uwsgi_run();}void uwsgi_setup(int argc, char *argv[], char *envp[]) {int i;struct utsname uuts;.........設定和初始化各種資源,這裡就省略了,有興趣的自己看看......//最主要的是這行uwsgi_start((void *) uwsgi.argv);}int uwsgi_start(void *v_argv) {......簡化摘要一些主要的代碼......... 題外話,這裡是建立一個多線程的共用記憶體空間,後面uwsgi_setup_workers的時候會用到。因為uwsgi有一個master進程,可以監測各個子進程的狀態,所以需要一塊匿名共用記憶體// initialize sharedareasuwsgi_sharedareas_init();// setup queueif (uwsgi.queue_size > 0) {uwsgi_init_queue();}... 這裡很重要,uwsgi.p是一個介面,uwsgi中部署的app在這裡初始化(在uwsgi中,部署的APP需要所對應語言的外掛程式,如python就用python外掛程式)後面也會看到,實際上uwsgi所執行的python代碼,其所有模組的import都在這裡執行// initialize request plugin only if workers or master are availableif (uwsgi.sockets || uwsgi.master_process || uwsgi.no_server || uwsgi.command_mode || uwsgi.loop) {for (i = 0; i < 256; i++) {if (uwsgi.p[i]->init) {uwsgi.p[i]->init();}}}// again check for workers/sockets...if (uwsgi.sockets || uwsgi.master_process || uwsgi.no_server || uwsgi.command_mode || uwsgi.loop) {for (i = 0; i < 256; i++) {if (uwsgi.p[i]->post_init) {uwsgi.p[i]->post_init();}}}。。。這裡主要是設定各個worker的共用記憶體空間// initialize workers/master shared memory segmentsuwsgi_setup_workers();// here we spawn the workers...if (!uwsgi.status.is_cheap) {if (uwsgi.cheaper && uwsgi.cheaper_count) {int nproc = uwsgi.cheaper_initial;if (!nproc)nproc = uwsgi.cheaper_count;for (i = 1; i <= uwsgi.numproc; i++) {if (i <= nproc) {if (uwsgi_respawn_worker(i))break;uwsgi.respawn_delta = uwsgi_now();}else {uwsgi.workers[i].cheaped = 1;}}}else {for (i = 2 - uwsgi.master_process; i < uwsgi.numproc + 1; i++) {。。。這裡就是根據我們設定的進程數,去fork子進程if (uwsgi_respawn_worker(i))break;uwsgi.respawn_delta = uwsgi_now();}}}// END OF INITIALIZATIONreturn 0;}int uwsgi_respawn_worker(int wid) {。。。主要是這行代碼,fork子進程,裡面就不跟了pid_t pid = uwsgi_fork(uwsgi.workers[wid].name);if (pid == 0) {signal(SIGWINCH, worker_wakeup);signal(SIGTSTP, worker_wakeup);uwsgi.mywid = wid;uwsgi.mypid = getpid();// pid is updated by the master//uwsgi.workers[uwsgi.mywid].pid = uwsgi.mypid;// OVERENGINEERING (just to be safe)uwsgi.workers[uwsgi.mywid].id = uwsgi.mywid;/* uwsgi.workers[uwsgi.mywid].harakiri = 0; uwsgi.workers[uwsgi.mywid].user_harakiri = 0; uwsgi.workers[uwsgi.mywid].rss_size = 0; uwsgi.workers[uwsgi.mywid].vsz_size = 0; */// do not reset worker counters on reload !!!//uwsgi.workers[uwsgi.mywid].requests = 0;// ...but maintain a delta counter (yes this is racy in multithread)//uwsgi.workers[uwsgi.mywid].delta_requests = 0;//uwsgi.workers[uwsgi.mywid].failed_requests = 0;//uwsgi.workers[uwsgi.mywid].respawn_count++;//uwsgi.workers[uwsgi.mywid].last_spawn = uwsgi.current_time;}else if (pid < 1) {uwsgi_error("fork()");}else {// the pid is set only in the master, as the worker should never use ituwsgi.workers[wid].pid = pid;if (respawns > 0) {uwsgi_log("Respawned uWSGI worker %d (new pid: %d)\n", wid, (int) pid);}else {uwsgi_log("spawned uWSGI worker %d (pid: %d, cores: %d)\n", wid, pid, uwsgi.cores);}}return 0;}int uwsgi_run() {。。。也是撿重要的摘抄一些如果pid是master,就執行master_loop如果pid是worker,就執行uwsgi_worker_run// !!! from now on, we could be in the master or in a worker !!!if (getpid() == masterpid && uwsgi.master_process == 1) {(void) master_loop(uwsgi.argv, uwsgi.environ);}//from now on the process is a real workeruwsgi_worker_run();// never here_exit(0);}void uwsgi_worker_run() {int i;if (uwsgi.lazy || uwsgi.lazy_apps) {uwsgi_init_all_apps();}uwsgi_ignition();// never hereexit(0);}void uwsgi_ignition() {if (uwsgi.loop) {void (*u_loop) (void) = uwsgi_get_loop(uwsgi.loop);if (!u_loop) {uwsgi_log("unavailable loop engine !!!\n");exit(1);}if (uwsgi.mywid == 1) {uwsgi_log("*** running %s loop engine [addr:%p] ***\n", uwsgi.loop, u_loop);}u_loop();uwsgi_log("your loop engine died. R.I.P.\n");}else {。。。子進程的迴圈體,一般是用simple_loopif (uwsgi.async < 1) {simple_loop();}else {async_loop();}}// end of the process...end_me(0);}。。。一直到這裡,在子進程的loop裡面才開始建立接收處理request請求的線程線程的執行函數simple_loop_run也是一個迴圈,基本上都是常規步奏,accept,receive, response...,後面就不繼續追下去了在reciev接到請求的資料後,會通過python_call的方法調用python指令碼的wsgi函數,處理這個請求void simple_loop() {uwsgi_loop_cores_run(simple_loop_run);}void uwsgi_loop_cores_run(void *(*func) (void *)) {int i;for (i = 1; i < uwsgi.threads; i++) {long j = i;pthread_create(&uwsgi.workers[uwsgi.mywid].cores[i].thread_id, &uwsgi.threads_attr, func, (void *) j);}long y = 0;func((void *) y);}
簡單來說,就是uwsgi中執行python指令碼和直接運行python指令碼是不同的。uwsgi執行python指令碼是通過調用python c api的方法,首先通過調用api載入python指令碼中的module,這時候,像最開始的執行個體代碼一樣module import中的相關代碼會被執行,所有的全域變數在進程中被建立和初始化。然後uwsgi建立線程,開始處理請求調用python api(python_call),執行python指令碼中處理請求的函數(wsgi介面),因為module import線上程建立之前已經執行了,所以之前在進程中的共用資料線上程中是可以訪問的。這裡就是需要我們著重注意的,在訪問這些線程間共用資料的時候需要加鎖,或者在編寫python指令碼的時候盡量少用全域變數而多用單例模式,避免不必要的采坑。
其實上面所述只是對我遇到問題的一個簡化,為了協助大家弄明白uwsgi多線程執行python wsgi介面的相關問題。我所遇到的問題是在處理請求的函數中,調用了一個在全域中建立的gearman client,這個client庫不是安全執行緒的,使用中也沒有加鎖。當請求的並發比較大的時候,gearman client這個庫就會報出一些串連的異常。
由於GIL的存在,Python的多線程並不能充分的利用多核的優勢。在實際項目中,我們常常使用多進程來取代多線程的方法,來實現一些需要並發的業務。然而,進程的開銷畢竟比較大,並且設計進程間的資料同步,處理序間通訊等操作相對線程來說十分的複雜,也是開發中的一個痛點,我們經常為了效能而放棄使用python轉而使用c多線程的方案,但是確實降低了代碼的可維護性,增加了代碼的成本。
在瞭解了uwsgi的多線程結構之後,其實我們也可以學習其通過多線程調用python c api的方法,使用c的線程調用python的業務代碼,將需要共用的資料放在c線程建立之前進行module import。將c多線程的部分提取出來成為一個架構,工程師只需要書寫python的業務代碼並在注意在使用共用資料的時候加鎖。架構部分交由專業的有經驗的c/python工程師進行維護,這樣在不犧牲代碼生產效率的前提下,提升了程式的效能。
後記:其實這個問題並不是十分的複雜,暴露的問題是對於uwsgi的代碼結構,和python c api以及c調用python的方法和相關概念等不是非常的熟練,暴露了自己知識體系中的短板。由於那幾天同時在開發幾個需求,沒有對問題進行詳細的測試,沒有仔細的分析和尋找Trackback中的錯誤,反而是一直在懷疑被呼叫者的介面效能問題。其實這個團隊在交流上確實也存在一些問題,爭論基本上靠喊,靠搶話,不是就是論事而是經常人身攻擊。每當有工程師的方案確實在理,確實證明是可用的時候,反對的人也寧死不妥協。。。這大概就是像新浪這樣臃腫老舊缺少活力的大公司的通病吧(一點吐槽,熟人請無視)。