標籤:blog http io ar os 使用 sp for on
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 docsfor ($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 wantfor ($i = 0; $i < 127; $i++) { $cursor->getNext(); }
關於 batchSize 函數的小問題
上面我們說了通過設定 batchSiz e來控制用戶端與 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 2var_dump( $cursor->getNext() ); // item 3
可以看到,雖然第一次網路返回包被設定只返回兩條資料,但是每三次調 getNext 時還是返回資料了,也就是說還是從伺服器第二次擷取到資料了。
實際上,通過上面的實驗結果,我們已經大致對 MongoDB 用戶端伺服器通訊協定有了大致的瞭解,更詳細的內容我們可以直接在 MongoDB 官方文檔中找到(Mongo Wire Protocal)
從PHP用戶端看MongoDB通訊協定(轉)