多線程C調用python api的陷阱

衆所周知,用腳本語言編寫的服務(wsgi接口)都需要一個server容器,常見的如php的php-fpm, lightd等。python中一般是用的uwsgi,uwsgi是在wsgi的基礎上的一種新的協議,可以用來部署python等腳本程序的運行。然而在不熟悉uwsgi的代碼架構和c調用python的api情況下進行開發可能會遇到一些意想不到的問題。


我們先看一段代碼,下面這段代碼是用的Flask框架,每次請求的時候會把COUNT的值先減一再加一,最後再乘二。如果請求50次,其最終的結果應該是2的50次冪。


from flask import Flask, request

COUNT = 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'


17179869184
34359738368
68719476736
137438953472
274877906944
549755813888
1099511627776
2199023255552
4398046511104
8796093022208
17592186044416
35184372088832
70368744177664
140737488355328
281474976710656
562949953421312
1125899906842624


這是直接執行50次index函數得到的最後幾行的結果,可得結果爲2的50次冪。

 536870912
 1073741824
2147483648
4294967296
8589934592
17179869184
34359738368
 68719476736
137438953472
274877906944
549755813888
1099511627776
 2199023255552
 4398046511104
8796093022208
17592186044416
 35184372088832
70368744177664
 140737488355328
281474976710656
5629499534213121125899906842624

這是通過ab測試,用多個併發訪問/test_uwsgi接口50次得到的最後幾行的結果。可以看出最終的結果肯定是個異常數字。爲什麼程序在uwsgi中運行時的運行結果會出現異常呢?

其實大家通過閱讀這個簡單的例子就可以發現,這種例子一般都是用來演示多線程共享數據同步問題的時候,如果不加鎖會暴露問題的例子。下面的代碼我們就在修改共享資源COUNT的時候加上互斥鎖,看看有沒有什麼變化。

from flask import Flask, request
import threading

mutex = 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 sharedareas
	uwsgi_sharedareas_init();

	// setup queue
	if (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 available
	if (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 segments
	uwsgi_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 INITIALIZATION
	return 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 it
		uwsgi.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 worker
	uwsgi_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 here
	exit(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_loop
		if (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中的錯誤,反而是一直在懷疑被調用方的接口性能問題。其實這個團隊在交流上確實也存在一些問題,爭論基本上靠喊,靠搶話,不是就是論事而是經常人身攻擊。每當有工程師的方案確實在理,確實證明是可用的時候,反對的人也寧死不妥協。。。這大概就是像新浪這樣臃腫老舊缺少活力的大公司的通病吧(一點吐槽,熟人請無視)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章