【版務】省錢之道.三(Node + PHP~)


春天是總想開始做點什麼的季節。

眨眼間搬家到 WordPress 也兩年了,那時也正值春天時節呢——對我來說春天就是埋首 program 的季節,因為想做點什麼,

腦筋就會變得活躍,而活躍的我進入了工作模式,對我來說寫 program 就是工作……

準確一點說腦電波長期處於 beta 波狀態?

所以 eventually 我也會跑去做 program 的,即使原本我明明是想寫文跟弄魚缸。well 魚缸是有生命的東西,所以還是需要 attention 的,於是就是沒生命的寫文被我擱置下來……



前情提要一下。

本站自從搬家到 WordPress 以來,本站使用的便是 AWS EC2 虛擬主機來架設 web server,這項服務首年免費,之後便要付費了。軟件方面用的是家傳戶曉的 WordPress,以及背後的 LAMP stack——即 Linux + Apache + MySQL + PHP。

而……以本站的流量,實在不太需要一台強勁的主機,所以我選了支援 LAMP stack 的機款當中最便宜最便宜一款,叫 t3a.nano。

這台機器的 spec,也沒什麼不好,就是 RAM 比較少,只有 512 MB。而為了讓它順利運行,我當時也花了不少工夫。詳見這篇文章:

而時至 2023 年中旬,AWS 宣佈 Public IPv4 地址將會在 2024 年 2 月起收費,為了不多付費,我把主機搬到了 IPv6 之上運作,詳見這篇文章:

而我在這裡要為上文作點補充。

雖然成功搬到了 IPv6 運作,也拔走了 Elastic IP,但是我的主機原來選綁定了一個 Public IPv4 地址……這是無法拔走的,所以我結果又要上來重練一遍。

所以在 3 月收到 AWS 帳單時是嚇了一跳,也花了我半天的時間,才又把主機複製一次,生出了另一台不綁定任何 IPv4 地址的主機,再把 DNS 指到新的主機之上。

詳細步驟用文字描述:

  1. 登入 AWS console -> Services -> EC2
  2. 找目前運作中的 EC2 Instance -> 右鍵 -> Image and Templates -> Create Image
    (可選 No Reboot 讓來源 Instance 維持運作)
  3. 在左邊欄找 AMI,將會顯示一個剛生的 AMI,在 AMI 上按右鍵 -> Create Instance from Image
  4. (省錢看這步)
    在 Network Settings 配置上按 Edit,在 Auto-assign public IP 一欄選 Disable
  5. 選好其他設定後,開機

另外是解決無法 IPv6 連接 ftp、ssh 等服務,可以在家裡的電腦安裝 Cloudflare WARP:https://one.one.one.one/

安裝後之啟動,家裡上網便多了 IPv6 地址了~



okay,處理完前情留下的東西了。

而今天是省錢系列第三彈——本站終於導入了 Node.JS,(有望)可以加快載入速度!給點掌聲,啪啪啦啪啪啦啪……

Node.JS 與 PHP

  • 先說 PHP,它於 1995 年面世,同時是 programming 語言,也是一個 runtime software
    作為 LAMP stack ——即 Linux + Apache + MySQL + PHP 的重要一員,PHP 負責處理的是網站的動態內容
    例如這個 blog,我出了新文章,所以載入時就多了一篇文章在主頁
    我並不需要修改任何 source code,只需要在 WordPress 發佈即可
  • Node.js 則是另一套 runtime software,於 2009 年面世
    它使用 javascript 引擎,因此在 server side 也可以使用 javascript
    更重要的是它擁有 event-driven 以及 non-blocking I/O 的特性,這使得 node 比 PHP 更適合同時處理多個連線要求
    ……
    詳細原理我也是不太懂,我只知道新就是好

由於 WordPress 是基於 LAMP stack 運行的,所以我無可避免地需要在我的主機上安裝它們的了。

我也沒神到可以把 WordPress 改成用 node 運作,但是我卻找到了一個很有趣的 package,叫 php-server,可以用 nodejs 呼叫 php 運行?

……

咦?可以一試喔……

它的原理很簡單,示範 code 也非常簡潔易明。只需要用 Node.js 執行以下幾句 code,電腦上便多了一個用 node 運行的 php server。

import phpServer from 'php-server';
const server = await phpServer({
    base: "/home/user/wordpress", // 這裡指向 wordpress 的 folder
    port: 81,
    hostname: "localhost",
    binary: "/usr/bin/php-7.2.33", // 這裡指向 php binary 檔案的所在地
});
export default server

把以上 code save 到 wordpress.js,再在包含的資料夾裡運行 npm initnpm install php-server 以及 node wordpress.js 就大概能跑得動的了~

打開瀏覽器去 http://localhost:81,就理應能看見用 node 執行的 wordpress 網站了。

是說它依然是用到了 php 的主程式,但是在 web request/response 層面,由 apache 處理變成了由 node 處理,所以理應更適合處理多個 request?

(我之後放棄了這個做法,改回用 apache,後述)



可是本站又不是只有一個 domain,本站至少需要 3 個 sub domain 來運作的呢:

  • 青色的天空:https://www.qingsky.hk
  • 青 . 小說:https://novel.qingsky.hk,以及
  • Glassnote 2.0:https://glassnote.qingsky.hk

但是 Web server 卻只能用一個 80 或 443 port。(其實只能用一個 443)……怎麼辦呢?

不用怕,我們可以把 php server 掛在數個不同的 port,例如 81、82、82;再寫一個 proxy,放在 port 80 運行,然後把不同 sub domain 的 request 分流去不同的 server 啊~

例如這樣:

// Credit: ChatGPT
const http = require('http');
const httpProxy = require('http-proxy');

// 開啟我們剛寫好的 php wordpress server
const wpServer = await import("./wordpress")

// 把 sub domain 指向 localhost 上的不同 port 位
const routingRules = {
  'www.qingsky.hk': 'http://127.0.0.1:81',
  'novel.qingsky.hk': 'http://127.0.0.1:82',
  ...
};

const proxy = httpProxy.createProxyServer({});
const server = http.createServer((req, res) => {
  const requestedDomain = req.headers.host.split(':')[0];
  const targetServer = routingRules[requestedDomain];
  if (targetServer) {
      // 把 request 指向 target server 並回傳
      proxy.web(req, res, {
        target: targetServer,
      })
  }  
});

// 在 port 80 開啟 Web server
server.listen(80, "::", () => {
  console.log('Reverse proxy server is running on port 80');
});


嘛,就是這麼簡單。

而事實上這個 node proxy 還可以做更多事情。例如是偵查到 request 是圖片或其他媒體檔案,可以直接回傳檔案內容,而不用經過整個 wordpress engine,從而減輕 php server 的負擔。

修改後的 code 是這樣子:

// Credit: ChatGPT
const http = require('http');
const httpProxy = require('http-proxy');
const fs = require('fs');
const mime = require('mime-types');

const wpServer = await import("./wordpress")

const routingRules = {
  'www.qingsky.hk': 'http://127.0.0.1:81',
  'novel.qingsky.hk': 'http://127.0.0.1:82',
  ...
};

const proxy = httpProxy.createProxyServer({});
const server = http.createServer((req, res) => {
  const requestedDomain = req.headers.host.split(':')[0];
  const targetServer = routingRules[requestedDomain];
  // 偵測檔案是否圖片/影片
  if (req.url.match(/(\.jpg|\.gif|\.png|\.jpeg|\.mov|\.mp4)$/i)) {
    const filePath =  __dirname + req.url
    if (fs.existsSync(filePath)) {
      // 如找到檔案,先讀取其 mime/type 並加入 header
      const readStream = fs.createReadStream(filePath);
      const contentType = mime.lookup(filePath)
      res.setHeader('Content-Type', contentType);
      let error = false
      // 回傳 filestream
      readStream.pipe(res);
      readStream.on('error', (err) => {
        res.statusCode = 500;
        res.end('Internal Server Error: ' + err);
        error = true
      });
      // 如無錯誤就跳出,如有錯誤,例如找不到檔案,則交由 php server 處理
      if (!error)
        return
    }
  }
  if (targetServer) {
      proxy.web(req, res, {
        target: targetServer,
      })
  }  
});

server.listen(80, "::", () => {
  console.log('Reverse proxy server is running on port 80');
});


……

可是等等。

Node.JS 在執行時會佔用 command line 啊。一旦離開了 ssh shell 它不就停止運作了嗎?

是的,所以接下來我們要把這個 script 註冊成為 system service。

在 ssh 裡輸入以下 command:

  • cd /etc/systemd/system
  • sudo vi node-php.service 並輸入以下內容
    [Unit]
    Description=node-php
    After=lampp.service
    [Service]
    WorkingDirectory=/home/use/node-php #這裡是你的 node script folder
    Type=simple
    ExecStart=/usr/bin/node run index.js #前端輸入 NodeJS 的安裝位置
    Restart=always
    [Install]
    WantedBy=multi-user.target

    然後按 esc->:wq 儲存並結束
  • sudo systemctl enable node-php.service
  • sudo systemctl start node-php

這樣電腦就會在每次開機時開啟 node-php,也會在它不幸掛掉之後立刻重新開啟了~

嘛,就這樣,我把一個原先運行在 php 的 WordPress 成功搬到 Node.JS 上運行了!哈……

接觸 NodeJS 讓我非常後悔,後悔為什麼這麼晚才學。因為 php 跟 apache 的 setup 是如此複雜,但沒想到數句 NodeJS 的 code 居然就能做到我原先花很多時間 setup 的事情……

到這裡也不得不給 credit ChatGPT,因為上述這些 code 都是出自它手筆的,我只負責修改至符合本站運作,以及加入中文 comment 而已。



well 事情也有點太順利了……

到這裡為止我大概只用了兩三個小時就已經做好,並放到 aws 上做實驗呢。

但現實可沒那麼簡單。我發現了一個非常重大的問題——Node.JS 版本的 wordpress 無法進入 WordPress 後台!一進入後台的網址便沒有回應……

而且不止這個,還記得我的 t3a.nano 只有 512 MB RAM 嗎?……是的,數天實驗下來,我發現用 node 運行的 wordpress 會非常容易當機,隔兩三小時後再進來只會顯示空白一片。

(我的實驗都使用 dev.qingsky.hk 進行,應該不會影響到主站運作?)

所以,我又把 wordpress 等 php server 又改回用 apache 運行。今次用 http-vhost.conf。

  • 修改 /opt/lampp/httpd.conf
    Listen 80 前加 # 號
    再把 #Include etc/extra/httpd-vhosts.conf 一句前的 # 號拿掉
    儲存
  • 修改 /opt/lampp/extra/httpd-vhosts.conf,加入以下內容
    Listen 81
    <VirtualHost *:81>
    DocumentRoot "/user/wordpress"
    <Directory "/user/wordpress">
    AllowOverride All
    Require all granted
    </Directory>
    </VirtualHost>
  • /opt/lampp/lampp reload
    重新載入 lampp

我們把 apache 的 80 port 關掉,因為要留給我們的 node-php proxy server。把 WordPress 搬到 81 port 運作,也把另外數個不同 domain 搬到各自的 port 位之上。

但是我保留了使用 NodeJS 作分流,以及回傳圖片、影片等檔案內容的 proxy server。


所以,就是這樣了~

有比較快嗎?至少現在個人感覺良好吧~現在暫時就希望它沒事,至少我可以歇息一會。


P.S.

其實我還做了一件事,就是進到 wordpress/wp-include/load.php 裡,把 is_ssl() 改成任何時候都 return true;

嘛,這是要補救我用了 http://localhost:80/ 來當 node proxy server 地址的缺陷,因為外部的 request 進來時即便使用 https,但經 localhost:80 轉介給 localhost:81 時並不會保留 https 的前綴呢。

嘛,這個其實放著不管也不成大問題,但是本站使用的 ajax load、service worker 等 client side javascript 對 http 跟 s 混搭的內容很敏感,稍微不對就會失效,所以就暫時把它硬改成 true 就算了。


P.P.S.

我也不是沒試過 upgrade wordpress 或 php,但是成本效益並不高。等下年春天有興致再來弄吧。


青鳥

2024年3月


Google 提供的廣告