在項目中發現經常有這種需求,需要加載一些大的固定的格式化數據,比如對戰中的一些技能數據,物品等。這些數據都是只讀數據,並且可能會比較大,目前來看大約有上萬條復雜數據,如果serialize的話,純文本有20M左右。嘗試過直接放一個array在php文件裡,結果發現require這個文件很耗時,可能會花費幾十ms的時間,並且這個時候io很重,因為需要加載幾十m數據到內存;另外去調研了一下sqlite,這個東西還算比較靠譜,但問題在於,比如寫操作函數,使用起來很不爽;於是就產生了自己寫一個擴展的想法。於是折騰之旅就此展開。
一開始想的是,直接在MINIT裡調用zend_execute_script方法來加載一個php文件,返回一個zval來存儲到全局變量裡。結果後來仔細一琢磨發現根本就是妄想。原因在於MINIT的時候php的vm還沒初始化完,不可能讓你調用zend_execute_script方法,並且這個方法也不會返回一個zval,要想拿到zval必須從EG中去拿,很麻煩。
於是轉換思路,嘗試用unserialize/serialize,結果發現,php_var_unserialize在MINIT階段果然是可以調用的。於是開搞,調用這個方法得到一個zval,然後存在全局變量裡,在get方法裡返回這個zval。寫完之後,在測試的時候杯具的發現,只要調用就會core呀。於是查文檔,自己思考,最終發現PHP_RSHUTDOWN_FUNCTION函數裡會將所有非pealloc分配的的變量給清除。因此在MINIT階段還正常的數據,到了Request階段已經被free了。
於是再查文檔,發現php裡提供了pealloc這類函數來提供persistent的數據分配。於是再轉換思路,將全局變量裡的hashtable用pealloc來分配,並且將hastable設置成persistent的(謝天謝地php的hashtable還要存代碼和vm,因此有這個功能)。但是杯具的是php_unserialize只會返回一個zval,你根本無法控制它是否是persistent的。沒辦法,只能調用zend_hash_copy來做了。寫完之後再測試,發現還是core,這就不明白了,為啥呢?中午吃飯的時候,突然想到,可能是淺拷貝的問題,zend_hash_copy提供了一個copy函數而我沒有設置它。再加上深拷貝函數之後再測試,發現果然可以用了,使用起來很清爽。 www.2cto.com
接下來進行測試發現,內存使用率不能忍,一個20m的數據文件加載到內存,需要大約100m左右的內存,如果有100個php-cgi進程,那就多要10G內存,這根本不能忍。於是設想,可以用共享內存來解決這個問題,反正這部分數據只要能讀就行了。php-cgi的主進程負責MINIT操作,子進程只要讀這部分數據就行了。但是很麻煩的是,php裡沒提供任何讓用戶維護內存的接口,於是只能一個函數一個函數的扒了。
仔細看了一下php的hashtable實現,發現比較復雜,而且關鍵用到了realloc函數,這個太讓人無語了,總不能我也寫一個內存管理吧。目前只用共享內存實現了一個簡單的線程分配內存的函數,從共享內存上依次往後分配空間。但是還好,這部分功能是resize功能根本不需要。因為我的目標是將php_var_unserialize裡得到的zval拷貝到共享內存而已,而大小我明顯已經知道了。並且也不需要updatea功能,因為是全新的copy。最終弄完之後,發現可以使用了,內存使用率果然降了。
接下來進行壓力測試,突然發現又開始core了,這根本不能忍呀,為啥呀?根據core文件,發現是裡面的hashtable的refcount降到0了。於是各種測試,發現單線程情況下是ok的,只有多線程大壓力情況下會掛。於是想到refcount是會被修改的,而且多線程修改的話,必須可能被改亂。那怎麼辦呢?總不能加鎖吧。
後來仔細想了一下,突然想到只要我每次在返回這個zval裡將頂層的zval的refcount修改為大於php-cgi進程數的值,那即使會被改亂也沒啥問題,因為根本不會改到0。於是修改了之後再測試,發現果然靠譜了。
到此,整個問題基本解決。但是還有另外一個問題,在重啟Php-cgi時還是會core,原因是,當時把正在使用的一些變量給強制寫成0了。其實共享內存的正確用法是,一個進程來寫,另外的進程來讀,但是我這個應用裡將共享內存當做絕對地址在使用,因此不可能在一個地方寫,在其他地方讀,除非shmat裡的第二個參數修改為固定值,但是這個就需要對進程的地址分配有充分的了解,知道哪些內存根本不可能被使用。不過這個應該還好,因為Php-cgi進程有內存上限,所以應該可以找到一塊內存在php-cgi運行過程中無法被用到的。不過具體的情況得接下來具體研究一下。
作者 無心雲