中文分詞 mmseg nginx 模塊開發

一、nginx模塊開發

Nginx 是一款高性能web服務器,因此,工作業務中需要藉助nginx強大的網絡服務功能,往往需要開發和定製相應的 Nginx 模塊滿足業務需求。
基本上作爲第三方開發者最可能開發的就是三種類型的模塊,即handler,filter和load-balancer。Handler模塊就是接受來自客戶端的請求併產生輸出的模塊,也是我們使用最多的一個模塊。有關nginx模塊開發的入門資料,作者十分推薦淘寶寫的文檔, 點我,點我。相信看完這個,對nginx模塊開發會有一定的認識。要想對nginx源碼有個認識也可以參考作者之前學習nginx寫的源碼學習文檔 nginx源碼分析

二、mmseg算法

關於中文分詞 參考之前寫的jieba分詞源碼分析 jieba中文分詞,有關mmseg分詞算法見上一個博客介紹的 mmseg分詞算法及實現
mmseg的主要目錄結構如下:

mmseg
├── build : 生成*.so 動態鏈接庫
├── data :詞典數據
└── src : 實現源碼
    └── util : 字符串處理通用庫

代碼採用c++11實現(g++ version >= 4.8 is recommended),已有詳細註釋,源碼。
https://github.com/ustcdane/mmseg

mmseg實現起來採用了{.h, .cpp} and {.hpp} 。Mmseg.hpp格式的代碼在開發nginx模塊時 好處是方便使用,無需鏈接,直接包含.hpp文件即可。此外有的時候需要向以動態庫的方式提供給使用人員,並只想暴露一定的接口時,此時分詞算法mmseg採用{.h, .cpp} 文件格式組成代碼,這樣可以把這部分代碼編譯成*.so的動態鏈接庫,mmseg算法也支持生成動態鏈接庫,進入目錄build , 裏面有已經寫好的Makefile文件 直接make 即可生成 libmmseg.so的 動態鏈接庫文件,從而方便在其它程序中調用。

三、mmseg nginx模塊開發

nignx HTTP 模塊開發網上也有很多例子,比較有名的就是nginx_hello 模塊,有興趣的先去網上找找這個例子看看,自己動手編譯下。

下面介紹mmseg nginx模塊開發的源碼組織形式:

nginx_mmseg/ # mmseg nginx 模塊源碼目錄
├── config # config文件,用來指導nginx生成makefile文件
├── Makefile_example # nginx objs/Makefile 樣例
│   ├── Makefile.bk.hpp #mmseg無鏈接的 Makefile
│   └── Makefile.bk.so # 動態鏈接庫形式的Makefile
├── mmseg # mmseg源碼
│   ├── build # 生成動態鏈接庫
│   ├── data
│   ├── LICENSE
│   ├── main.cpp
│   ├── README.md
│   └── src
├── ngx_http_handle_interface.cpp #  處理 HTTP請求的主要函數
├── ngx_http_handle_interface.h
├── ngx_http_mmseg_module.c
├── ngx_http_mmseg_module.h # nginx 模塊配置相關
├── README.md
└── tags

下面介紹mmseg nginx模塊開發的一些重要知識:

nginx模塊開發
可以通過參考資料中瞭解 nginx模塊的基本結構,包括:模塊配置結構,模塊配置指令,模塊上下文結構,模塊的定義,handler模塊的基本結構等等,這部分內容參見源碼{ngx_http_mmseg_module.h ngx_http_mmseg_module.c}。
其中,handler模塊必須提供一個真正的處理函數,這個函數負責對來自客戶端請求的真正處理。這個函數的處理,既可以選擇自己直接生成內容,也可以選擇拒絕處理,由後續的handler去進行處理,或者是選擇丟給後續的filter進行處理。來看一下這個函數的原型申明。typedef ngx_int_t (*ngx_http_handler_pt)(ngx_http_request_t *r);r是http請求。 該函數處理成功返回NGX_OK,處理髮生錯誤返回NGX_ERROR,拒絕處理(留給後續的handler進行處理)返回NGX_DECLINE。 返回NGX_OK也就代表給客戶端的響應已經生成好了,否則返回NGX_ERROR就發生錯誤了。在我們的項目中這個處理回調函數是ngx_http_mmseg_module.c中的 static ngx_int_t ngx_http_mmseg_handler(ngx_http_request_t *r)。
當回調這個函數時,表示 Nginx 收到了 HTTP 請求,並且 HTTP 的 header 數據已經被解析完畢的時候(對HTTP不是很瞭解的話,可以參考 這篇HTTP講解的blog。)。
一般情況下我們只處理HTTP的GET和POST請求,下面分別介紹下這兩種情況:

  • GET
    GET 請求通常是只需要 header 數據即可,不需要 body 數據。 所以當 GET 請求過來的時候,我們只需要 在調用 ngx_http_handle_interface.cpp 中的 int ngx_http_do_get(ngx_http_request_t *r)函數處理,這個函數其實很簡單,先獲得請求ngx_http_request_t的配置信息,並解析request的參數信息(即獲得要分詞的句子),對分詞句子進行轉碼後調用我們的mmseg算法進行分詞,並把結果寫入 ngx_chain_t out,再通過 ngx_http_output_filter(r, &out)函數將結果發送給客戶端即可,這樣就完成了get請求的過程。
  • POST
    處理 POST 請求時,不僅需要 HTTP 的 header,也需要 body 數據, body 數據大小是通過 header 裏面的 content-length 長度指定。 處理post 方法時注意使用ngx_http_read_client_request_body(r, ngx_http_do_post),來看下 ngx_http_read_client_request_body方法的原型:
    ngx_int_t
    ngx_http_read_client_request_body(ngx_http_request_t *r,ngx_http_client_body_handler_pt post_handler);
    參數r就是要處理的請求,post_handler則是body接收完成後的回調方法。在worker進程中,調用ngx_http_read_client_request_body是不會阻塞的,要麼讀完socket上的buffer發現不完整立刻返回,等待下一次EPOLLIN事件,要麼就是讀完body了(調用 recv 函數去接收數據。 並將該數據累加起來,當累計的數據量大於等於 content-length 時,代表該請求的 body 數據已經被接收完畢),調用用戶定義的post_handler方法去處理body。因此,我們需要註冊一個回調函數(post_handler)來告訴 nginx, 當 body 數據接收完畢之時,就是調用我 這個回調函數。 這個回調函數在源碼是ngx_http_handle_interface.cpp 中的 void ngx_http_do_post(ngx_http_request_t *r)函數。

ngx_http_request_t
一個http請求,包含請求行、請求頭、請求體、響應行、響應頭、響應體。nginx中代表http請求的數據結構是ngx_http_request_t, ngx_http_request_s是nginx中非常重要的一個結構體,貫穿於htpp請求處理的整個過程中。其初始化過程爲:函數ngx_epoll_process_events { src/event/modules/ngx_epoll_module.c }接收到網絡IO事件EPOLLIN後(即socket上有數據可讀),調用了這個回調方法ngx_http_init_request {src/http/ngx_http_request.c。ngx_http_init_request開始處理這個事件,首先它把基本的ngx_http_request_t變量(nginx HTTP框架中由始至終用到的)初始化,並從內存池中爲每個連接ngx_http_request_t * r 分配一個內存池,你可以經常看到 ngx_palloc(r->pool, …) or ngx_pcalloc (r->pool, …)之類的內存分配,其中ngx_pcalloc對分配的內存進行置零。 通過 ngx_palloc or ngx_pcalloc 分配出來的內存不需要手動回收。 因爲該 r->pool 這個內存池是每個連接創建一個內存池。 當該連接斷開的時候,該內存池會被整個釋放掉。所以不需要擔心內存泄露的問題。有關ngx_http_request_t結構體詳解 可以看源碼或者上網查下,這方面的資料也挺多的。

四、nginx_mmseg 安裝編譯

需要先下載 pcre軟件包,假設下載了pcre-8.37 並放在user目錄下。
下載源碼
git clone https://github.com/ustcdane/nginx_mmseg.git /user/nginx_mmseg
configure
進入nginx目錄,cd nginx-1.8.0,運行configure生成Makefile文件,Makefile文件在objs目錄下:
./configure –prefix=/user/nginx-1.8.0/bin –add-module=/user/nginx_mmseg/ –with-pcre=/user/pcre-8.37
解釋下各個意義:
—prefix=nginx的運行目錄,還有—add-module=自己的module目錄,—with-pcre=pcre目錄。
修改Makefile
因爲 nginx_mmseg 是 C++ 源碼,所以作爲 nginx 模塊編譯的時候需要 修改 obj/Makefile,
修改後的Makefile一定要備份,因爲重新configure或者make clean都會把這個makefile給刪掉,這樣心血就白費了。
1. 首先把CC=cc改爲CC=gcc,然後加入CXX=g++,並把LINK指定爲CXX
CXXFLAGS=$(CFLAGS) -std=c++11 -g
2. 在ALL_INCS中添加你用到的.h文件的路徑
3. 在ADDON_DEPS中添加你的模塊需要依賴的.h文件(不需要扯到mmseg的.h or .hpp)
4. 在objs目錄下添加你的module下的.c或者.cpp文件生成的.o文件
5. 在LINK後面也加上這些.o
6. 加上這些.o文件所需要的編譯選項,cpp用CXX編譯,c用CC編譯,然後再make就完事了。把objs目錄下的nginx可執行文件拷貝到運行目錄下/user/nginx-1.8.0/bin,然後修改bin/conf的nginx.conf文件,添加我們需要的words_path和charFreq_path參數,即詞典的路徑,如下面的代碼所示,執行bin/nginx就能提供mmseg的HTTP服務了。

  location / { 
            root   html;
            index  index.html index.htm;
        }   
    location /test {
            words_path /user/nginx_mmseg/mmseg/data/words.dic;
            charFreq_path /user/nginx_mmseg/mmseg/data/chars.dic; 
        }   

有不清楚的可以參考目錄下 Makefile_example 文件 Makefile.bk.hpp和 Makefile.bk.so,
這是把mmseg算法兩種形式供nginx模塊調用的方式:Makefile.bk.hpp是在模塊開發時直接包含分詞算法Mmseg.hpp 無需額外的鏈接;Makefile.bk.so是把分詞算法mmseg包裝成.so的動態鏈接庫形式供nginx模塊調用。兩個 Makefile不同之處在於$(LINK) 的時候,.so在LINK時的寫法是:
-L$(MMSEG_PATH)/../build -lmmseg -lpthread -lcrypt /search/daniel/pcre-8.37/.libs/l ibpcre.a -lz

注意 在使用動態鏈接庫 .so會出現如下情況:
./nginx: error while loading shared libraries: libmmseg.so: cannot open shared object file: No such file or directory。

這是因爲nginx 找不到我們的動態鏈接庫 libmmseg.so,因此需要在路徑LD_LIBRARY_PATH中添加,方法如下:
在 /etc/profile 中添加
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/user/nginx_mmseg/mmseg/build/
然後 source /etc/profile 即可。

測試

  • GET
    curl “http://127.0.0.1/test?data=研究生命起源”
    結果:研究 生命 起源
    瀏覽器打開上述鏈接也可以,瀏覽器的頁面編碼設置爲 utf-8 。
  • POST
    curl –data “研究生命起源” “http://127.0.0.1/test
    結果:研究 生命 起源
    具體源碼註釋見:
    github

細心的童鞋,注意到了上面mmseg基於nginx的HTTP模塊開發,每個worker進程都會加載數據,當數據量比較大,顯然太費內存了,又由於Linux系統進程fork時採用了copy on write的技術,因此,只要master進程加載了數據,master進程fork的worker就會共享這些數據,所以寫了一個基於mater加載數據的例子,詳情見: github [data_load_on_master]
在nginx_mmseg_new/mmseg/src/Mmseg.cpp 中函數load加載數據階段,爲了測試大數據添加了如下代碼:

#ifdef DEBUG_LEVEL // 內存佔用測試

    typedef struct test_ {
            long long l1;
            long long l2;
            long long l3;
            long long l4;
            long long l5;
    }test_;
    mmsegSpace::MMSeg::test_* p_memory_test = new test_[100000000];
    memset(p_memory_test, 0, sizeof(test_)*100000000);
#endif

效果如下圖:
內存佔用
通過圖中可以看出來, 機器實際內存16GB。結合上面的內存佔用測試代碼我們也能計算出來:p_memory_test 所佔內存爲:(100000000*5*8)/(1024*1024)=3814MB=3.72G,加上分詞字典、詞頻所佔內存符合top所示3.771GB,但top命令顯示1個master和8個worker各佔用了3.771GB的內存空間(),這樣算的話nginx大概佔用了9*3.7=33.94GB,顯然不符實際情況,因爲我的機器內存才16GB!所以可見cow生效了!!,詳情見代碼。
當master申請的內存超過機器內存一半時,會出現nginx [emerg] fork() failed (12: Cannot allocate memory,這是由於系統級的限制,當所申請內存超過機器內存一半時會出現這個錯誤,需要修改文件/etc/sysctl.conf 中的 overcommit_memory值,其取值有0,1,2對應的含義如下:

  • 0表示內核將檢查是否有足夠的可用內存供應用進程使用;如果有足夠的可用內存,內存申請允許;否則,內存申請失敗,並把錯誤返回給應用進程。
  • 1表示內核允許分配所有的物理內存,而不管當前的內存狀態如何。
  • 2 表示內核允許分配超過所有物理內存和交換空間總和的內存

利用如下三種方法之一修改overcommit_memory值:
vim /etc/sysctl.conf 添加 vm.overcommit_memory=1
sysctl vm.overcommit_memory=1
echo 1 > /proc/sys/vm/overcommit_memory

在我的機器(16GB內存)上實驗結果如下:
big


參考

  1. http://blog.csdn.net/russell_tao/article/details/5637451
  2. http://tengine.taobao.org/book/chapter_03.html#handler
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章