PHP 的 Web 運行原理 ( 2 ) - 非阻塞 I/O 之 Reactor 模式
php
Lastmod: 2019-12-15

前篇: PHP 的 Web 運行原理 ( 1 )

上面一篇文章中,我們有提到兩種 php 的 web 運行模式moduelfast_cgi模式,它們在某種情況下,都會有些問題,而我們這篇文章就是要來理解是碰到什麼問題,然後又是如何解決呢 ?

  • Reactor 模式想解決的問題
  • Reactor 模式原理
  • Reactor 的使用注意事項

Reactor 模式想解決的問題

使用 moduel 與 fast_cgi 模式 的 web server 模式基本上會有兩個問題存在。

1. 高併發請求,會爆 !

如下面這張圖一樣,它每一個 http 請求都需要使用一個 process 或是 thread 來進行處理,而每一台機器的 process 與 thread 的數量都有限制,且操作系統進行 process 或 thread 上文文切換時非常耗的資源。

2. 服務如果是大量 I/O 操作會很浪費資源 !

ex. 讀 db 或 redis 啥的

主要耗資源的地方在於,每個 process 開啟後,大部份的時間者是在等待 I/O 的處理,而 CPU 都是閒在那。

上面兩個是看到的現象,而真正的問題點在於 :

為什麼每個請求都需要開啟一個 process 或 thread 來處理呢 ?

主要的原因在於,在 linux 底層中,我們使用了阻塞 I/O 方法,來讀取 http 傳送過來的資料。如下範例程式碼。socket网络编程中read与recv

data = read(sockfd); // 這裡會阻塞整個 process
handler(data);

它就會一直停在那一行 read 等待資料進來,也就是說整個 process 為了監聽 socket 有沒有資料,就卡在那了。

這也是為什麼每一條 http(I/O) 請求,都需要啟一個 process 或 thread 來處理某條 socket 的原因。

備註: 每一個 http 請求基本上就是會建立一條 socket,而 sockfd 又代表此 socket 的 File descriptor,不熟悉網路的友人可以用上面這幾個關鍵字來查詢。

Reactor 模式原理

基本上高併發 I/O 這個問題,目前比較常用的解法為 :

Reactor 模式

reactor 模式可以幫助我們建立非阻塞 I/O 模式,也就是不需要開多個 thread 或 process 來處理 I/O 操作。

它的概念如下圖 :

上圖架構中,最重要的就是 I/O 多路復用 (Multiplexing)這部份,基本上它是系統底層所提供的功能,它可以幫助我們監控所有有註冊的 socket,當它有事件進來以後,就會由指定的 handler 來進行處理。

下面為 reactor 模式的概念碼。而事實上這就是 nodejs 中我們常聽到的 event loop 的機制。

但這裡要注意,epoll_wait 本身是一個阻塞方法,也就是說執行它,整個 process 會被卡住。有寫過 nodejs 的人在這裡應該會有疑問,等等會解答。

while(true){
    events = epoll_wait();  // 這裡會取得到 I/O 讀取資料的事件資訊 ex. read
    for (int i=0; i < events.size(); i++){
        handler(events[i]);
    }
}

備註 : I/O 多路復用 (Multiplexing) 不同的平台有不同的實作,epoll(linux)、kqueue(Mac)、IOCP(Window)。

Multiplexing 與 Handler 是不同的 process 嗎 ? 還是相同的 ?

答案是都可以。

像 nodejs 就是屬於 multiplexing 與 handler 都是在同一個 process 運行,而 php swoole 則是屬於 multiplexing 與 handler 在不同 process 運行。

Multiplexing 不是需要一個 process 一直監聽所有 socket 嗎 ? 那為什麼 nodejs 可以單個 process 同時做到監聽與處理呢 ?

嗯之前我也有這個疑問。

當初我的想法是如下概念碼一樣,它會在 epoll_wait 阻塞住整個 process 來監聽,然後在有事件時,丟給某個 thread 或其它 process 的 handler 來處理。

while(true){
   events = epoll_wait(); // 它會一直停在這裡,等某個 socket 有事件進來。
   for (int i=0; i < socket.size(); i++){
        handler(events[i])
   }
}

epoll_wait 的確是阻塞 I/O 操作沒錯,但是它事實上有提供一個參數 timeout,也就是如果這段時間沒有資料,它就會回傳個 null 或啥 -1 的,反正就是和你說沒資料進來,而這時你就可以繼續往下處理。

備註: 可以拉到最下看看 libvu 所提供的 timeout 計算方式。

while(true){
   events = epoll_wait(timeout); // 注意 timeout 。
   for (int i=0; i < sockets.size(); i++){
        handler(sockets[i])
   }

   // 取出接下來 event queue 中要處理的工作。
   worker = queue.poll();
   handler(worker);
}

Reactor 模式的使用注意

下面為 reactor 模式的概念碼。

while(true){
    events = epoll_wait();  // 這裡會取得到 I/O 讀取資料的事件資訊 ex. read
    for (int i=0; i < events.size(); i++){
        handler(events[i]);
    }
}

假設我們在 handler 執行一段 cpu 密集工作的話會如何呢 ?

嗯就是會整個 process 卡住,也就是說除非計算完成,不然什麼事情都不能做。

所以別忘了 reactor 的使用重點 :

reactor 是用來解決 I/O 密集的模式,但無法處理 CPU 密集的工作

所以在基本上要先理解清除你的系統是大部份的工作才能決定要什麼模式來處理。

  • CPU 密集處理 => multi process 或 multi thread 模式。
  • I/O 密集處理 => reactor 模式

結論

本篇文章中我們說到以下幾個重點:

Reactor 模式想解決的問題

大量 I/O 操作的情況,會導致傳統的 multi process web 模式倒站問題。

Reactor 架構

重點就是I/O 多路復用有了它我們才能一個 process 監控多個 socket。

Reactor 的使用注意

在 reactor 模式的系統下,不要執行大量 cpu 運算的工作,會導致整個 process 卡住,大量 cpu 運算請另開 process 或 thread 來處理。

參考資料

libuv 的 timeout 計算參考

下面為 libuv 的 timeout 計算的方法,

libuv timeout 原始碼

int uv_backend_timeout(const uv_loop_t* loop) {
  if (loop->stop_flag != 0)
    return 0;

  if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop))
    return 0;

  if (!QUEUE_EMPTY(&loop->idle_handles))
    return 0;

  if (!QUEUE_EMPTY(&loop->pending_queue))
    return 0;

  if (loop->closing_handles)
    return 0;

  return uv__next_timeout(loop);
}

而下面為 uv__next_timeout 的源始碼。

int uv__next_timeout(const uv_loop_t* loop) {
  const struct heap_node* heap_node;
  const uv_timer_t* handle;
  uint64_t diff;

  heap_node = heap_min(timer_heap(loop));
  if (heap_node == NULL)
    return -1; /* block indefinitely */

  handle = container_of(heap_node, uv_timer_t, heap_node);
  if (handle->timeout <= loop->time)
    return 0;

  diff = handle->timeout - loop->time;
  if (diff > INT_MAX)
    diff = INT_MAX;

  return (int) diff;
}
comments powered by Disqus