1. 程式人生 > >從PHP客戶端看MongoDB通訊協議

從PHP客戶端看MongoDB通訊協議

MongoDB?的 PHP 客戶端有一個?MongoCursor?類,它是用於獲取一次查詢結果集的控制代碼(或者叫遊標),這個簡單的取資料操作,內部實現其實不是那麼簡單。本文就通過對 MongoCursor 類一些操作進行分析,向大家揭開 MongoDB 客戶端伺服器通訊的一些內部細節。

getNext與網路請求

通常來說,每一次find操作都會返回一個MongoCursor物件,在這個物件上呼叫getNext方法,就能夠獲得一條結果資料。迴圈呼叫getNext方法就能獲取多條資料。下面我們就來看看其內部取資料的具體邏輯。

首先我們用最簡單的方法來生成一個MongoCursor物件:

$m = new Mongo();
$collection = $m->demoDb->demoCollection;
$cursor = $collection->find();

當我們呼叫 find 方法的時候,會生成一個 MongoCursor 物件,而這時候只是生成一個記憶體中的物件而已,並不會把我們的 find 查詢傳送到服務端,因為在生成 MongoCursor 物件後,我們還可能對它做一些其它操作,比如 sort,limit 等等。這就對查詢條件進行了改變。

那什麼時候 PHP 會對 MongoDB 發起 find 的網路請求呢,是在 MongoCursor 呼叫 getNext 方法的時候。比如我們在上面程式碼的基礎上,再執行 sort 和 getNext 兩個方法:

$cursor->sort( array( 'name' => 1 ) );
$result = $cursor->getNext();

這時候第二行程式碼就會觸發 find 的網路請求,具體請求的內容如下圖,下圖是對這次請求的二進位制協議進行解析後的資料結構展示:

從上面圖中我們可以看到,Number to Return 欄位是0,MongoDB 協議裡0表示不做限制,獲取全部資料。所以這一次的 find 操作會把所有這個 collection 中的所有資料都拿到。而我們呼叫一次 getNext 實際上只拿到一條資料。那是不是說我們每調一次 getNext,PHP 就會進行一次網路請求獲取一條資料呢?結果當然是否定的,這樣效率未免也太低了。那好,那是不是 PHP 在第一次呼叫 getNext 就把所有資料拿回來,存在記憶體中,然後後續的 getNext 呼叫都在本地記憶體裡取就行了呢?結果還是否定的,這樣資料量大點 PHP 就容易被暴菊了吧。

所以事實上是怎麼做的呢?我們來看下面一張圖:

圖上的 Number Returned 的值是101,也就是說 MongoDB 給我們返回了101條資料,這個101實際上就是伺服器預設的 batchSize 大小。也就是說在沒有指定返回多少條的情況下,會預設返回101條資料。這101條資料會存在 PHP 的記憶體中,這樣後續的100次 getNext 呼叫,都不會再進行網路請求,而是直接從記憶體中返回資料。

如果我們在上面的 getNext 後再進行下面的呼叫。

// skip the other 100 docs
for ($i = 0; $i < 100; $i++) { $cursor->getNext(); }
// request document 102:
$result = $cursor->getNext();

上面先迴圈呼叫了100次 getNext,記憶體中的101項資料就都已經被取光了,然後當我們再次呼叫 getNext 去獲取第102條資料的時候,PHP 記憶體中已經沒有資料可以提供了,這時候又會再發起一次向 MongoDB 伺服器的請求,去獲取更多的資料。客戶端這次會發起如下請求:

這次我們看到,請求的碼變成了 Get More。也就是在上次的基礎上獲取更多資料。這時候實際 MongoDB 不會再按一個特定的條數返回資料,而是按一個特定的大小,目前是4M,也就是說,這一次,MongoDB 會返回最多4M的資料。對上面的請求,MongoDB 的返回如下:

這次返回結果中,標識了是從第101條開始,共返回了34673條資料。大小是4194378,正好是4M。

設定batchSize

上面我們說了,MongoDB 預設的 batchSize 是101條,這個條數實際上我們可以通過客戶端來設定的。在 PHP 中,通過 batchSize 函式來進行設定。比如我們用下面命令設定 batchSize 為25:

$cursor = $collection->find()->sort( array( 'name' => 1 ) );
$cursor->batchSize(25);
$result = $cursor->getNext();

上面程式碼呼叫了一次 getNext,按上面講到的,會一次性批量取N條資料回客戶端。上面程式碼執行時產生的網路請求如下:

我們可以看到,Number to Return被設定為了25。

如果我們再迴圈執行getNext函式25次,加上上面程式碼一共執行26次,那麼因為第一次只返回了25條記錄,所以第26次呼叫getNext函式時會再一次觸發網路請求。請求體如下:

由於我們設定了 batchSize 為25,所以這一次要求返回的也只有25條。服務端返回的資料也就只有25條。

使用limit

除了 batchSize 函式以外,還有一個方法可以控制每次網路請求批量返回的記錄條數,那就是在 MongoCursor 上呼叫 limit 函式,直接設定需要獲取的記錄條數。

比如下面程式碼,我們通過設定 limit 查詢前50000條記錄:

$cursor = $c->find()->sort( array( 'name' => 1 ) );
$cursor->limit( 50000 );
$res = $cursor->getNext();

上面程式碼會發出下面的請求

我們看到,要求返回的數目是50000條,那麼MongoDB伺服器是不是就乖乖返回50000條資料了呢。讓我們直接來看一下具體的返回資料包

很遺憾,MongoDB 服務端只返回了34678條,而不是我們理想中的50000條,其實原因也很簡單,從 Message Length 的值就能看出來,因為目前請求包已經達到4M大小了,這個上限無法逾越。所以只能返回34678條資料了。

而同時,客戶端在收到返回的資料包時,發現只有34678條資料,不夠自己要求的50000條,還差 50000 – 34678 = 15322 條,所以會再發起一次請求,要求伺服器返回剩餘的15322條記錄。如下:

batchSize 和 limit 相組合

有時候我們可能會需要取很多條資料,比如上面的,通過設定limit為50000來獲取50000條資料,而取這50000條資料的獲取可能會超出我們設定的 MongoCursor 的 timeout 限制,丟擲 Cursor 超時的異常。這時候我們可以在設定 limit 的同時,設定 batchSize 來控制每兩次請求伺服器的時間間隔。以免由於獲取大量資料導致的 MongoCursor 超時。

比如下面的例子裡,我們要獲取128條資料,但是通過設定 batchSize 來控制每次只從伺服器取回50條。這樣在後續的 getNext 呼叫中,就會發生三次網路請求,分別請求數目是50條,50條,28條。

$cursor = $c->find()->sort( array( 'name' => 1 ) );
$cursor->limit( 128 )->batchSize( 50 );
$res = $cursor->getNext();
// retrieve the other 127 documents that we still want
for ($i = 0; $i < 127; $i++) { $cursor->getNext(); }

關於 batchSize 函式的小問題

上面我們說了通過設定 batchSize 來控制客戶端與 MongoDB 伺服器的資料交換。但是這裡有一個特例,當 batchSize 被設定為1,或者是負數時,MongoDB 只會返回第一次請求的資料包,然後直接關閉掉這個連線。也就是說,如果我們執行下面的命令:

$cursor = $c->find()->sort( array( 'name' => 1 ) );
$cursor->batchSize( 1 )->limit( 10 );
$cursor->getNext();
var_dump( $cursor->getNext() );

會發現最後一個 var_dump 打出來的總是 NULL。因為每一次按 batchSize 的設定只返回了1條資料,然後連線就關閉了。

而我們只需要稍做修改,將 batchSize 改成2,情況就大為不同

$cursor = $c->find()->sort( array( 'name' => 1 ) );
$cursor->batchSize( 2 )->limit( 10 );
$cursor->getNext(); // item 1
$cursor->getNext(); // item 2
var_dump( $cursor->getNext() ); // item 3

可以看到,雖然第一次網路返回包被設定只返回兩條資料,但是每三次調 getNext 時還是返回資料了,也就是說還是從伺服器第二次獲取到資料了。

—-20121102更新—-

我們試試 batchSize 為 -2 會發生什麼:

可以看到傳送出去的請求裡制定的是-2,說明負數的真正含義由服務端解釋。
返回的結果如下:

Cursor ID 為 0 說明沒有cursor了,也就是不能通過cursor來獲取其他的資料了。

實際上,通過上面的實驗結果,我們已經大致對 MongoDB 客戶端伺服器通訊協議有了大致的瞭解,更詳細的內容我們可以直接在 MongoDB 官方文件中找到(Mongo Wire Protocal

總結五點:
1. BatchSize告訴服務端每次打包返回的紀錄條數
2. 打包返回的紀錄加起來不超過4MB
3. limit是指客戶端想要(嘗試)獲取的記錄數
4. 預設情況下,不限制limit的話,第一批返回的文件個數是101
5. batchSize 為負數或為1是,會銷燬cursor,沒有cursor就沒有後續資料