前端全棧之路–搭建生產環境的linux+nodejs+express的web伺服器

eric_bin發表於2019-02-16

前言小序

以前我是個純前端,就是很純的那種。切切圖,寫寫html、css佈局;到後來寫js,封裝外掛、元件;再後來公司沒人力了,又要寫後臺,當時聽說”PHP是世界上最好的語言…“,還學了php,會寫一些php後臺和myslq。後來還是因為公司沒人了,又當起了運維,當時給某專案某司搭了個windows server+Apache+php+mysql的伺服器。(因為當時他們要求在他們的windows server 2012內部伺服器上搞專案)。

但作為一個前端,或者以前端自居的人來說內心還是覺得“javascript才是世界上最好的語言…^^”,於是後來轉到javascript和nodejs的一條龍體系。之前自己的伺服器有用過百度雲的應用引擎bae,簡單來說就是,百度搭建好的Linux和nodejs環境,直接git或者svn版本bae的倉庫就可以進行專案開發,不需要理會伺服器的搭建和管理。可以直接專注開始做業務層的開發,但是bae應用引擎的一些限制,比如臨時生成的檔案再新版本釋出後會被清除等。不能滿足需要了,後面開始自己從頭搭建伺服器。從寫前端,寫些後臺,還搞起了伺服器,當運維……雖然主要工作是前端開發,但勉強也算被逼成了個全棧的概念了吧,寫這篇文章主要是對之前從事開發路上的總結和記錄吧。並以一個實際線上生產的伺服器例子為記錄藍本。本篇主要總結的是從什麼都沒有,到一個可進行基於nodejs環境進行業務層開發的過程,不涉及業務邏輯層如專案結構那些的東西。

好了,廢話不多說了,進入正題。

系統架構層面的東西

從零開始,從無到有

  1. 系統環境:阿里雲 Linux centOS 6.8 64位

  2. node環境:node 6.11.1

  3. express:4.x

  4. 版本控制:git 1.7.1

不同於開發環境node配置,生產環境還有許多不同於開發環境的nodejs環境的問題,不僅僅是安裝node後,命令列執行node app.js就可以開啟一個node的web伺服器。下面將從最開始什麼都沒有說起,搭一個node生產伺服器,可能會遇到的問題。

以下若有涉及本地開發環境的描述,環境為:mac os 10.12.3

1. Linux伺服器

一般用於生產環境的現在大部分都使用Linux作為伺服器的系統環境,雖然window server也可以,但處理node環境,優選linux系統。
Linux有很多版本,CentOS、Ubuntu、Debian等,具體選什麼版本,自行百度啦,不多說了。百度雲,阿里雲,騰訊雲這些雲服務商也不多說了,具體用哪家的產品,自行看著選吧(土豪或者土豪公司自己買伺服器機房的請忽略這句話)。
這裡給個參考文章,CentOS、Ubuntu、Debian三個linux比較異同

本篇例項使用:阿里雲ECS Linux centOS 6.8 64位

2. Linux使用者建立

伺服器有了後,就要開始進行什麼裝軟體,調配置的環境搭建了。可以直接在控制檯伺服器例項,點遠端連線,開啟一個web版的linux shell的視窗,與linux伺服器進行互動。如果需要登入linux管理的人少或者就一個,可以直接用root使用者進行操作。但出於規範和安全的考慮,這裡我們建立一個使用者,用於linux管理。並賦予可執行sudo的管理員許可權。

建立eric使用者(這裡自行使用名稱,eric是事例名稱),預設使用系統根目錄/home下,生成eric目錄,/home/eric即為eric的使用者根目錄

# useradd eric

然後賦予使用者sudo許可權,開啟/etc/sudoers檔案,找到
root ALL=(ALL) ALL
可以新增一行
eric ALL=(ALL) ALL或者不需要密碼執行 eric ALL=(ALL) NOPASSWD: ALL
這樣使用者即可擁有sudo許可權。

但是建議將建立的使用者,新增到wheel使用者組,而不是直接新增使用者許可權(wheel使用者組是linux預設的具有超級管理員許可權的分組)
並將# %wheel ALL=(ALL) NOPASSWD: ALL前面#號去掉,同樣使使用者擁有sudo許可權,這樣會更好一些。

3. ssh遠端登入的設定

雖然可以通過web版的linux shell的視窗,也可以通過本地命令列終端通過ssh密碼登入,但為了規範與安全和方便管理,我們使用本地命令列終端,通過ssh金鑰對進行遠端與linux互動。一般雲伺服器預設安裝好了ssh,就不需要另行安裝了,直接用就行了。

先在web版的linux shell登入linux伺服器,切換到eric使用者,執行

建立.ssh目錄

mkdir .ssh

/home/eric/.ssh目錄下生成金鑰對檔案,預設檔名為私鑰:id_rsa,公鑰:id_rsa.pub

ssh-keygen -t rsa

建立authorized_keys檔案,儲存使用者公鑰

touch authorized_keys

將使用者公鑰拷貝到authorized_keys檔案,若有多個,每一個另起一行。

cp id_rsa.pub >> authorized_keys

注:關於金鑰對當然也可以直接在雲服務控制檯UI介面根據雲服務指引生成。

然後私鑰下發給使用者,id_rsa放在使用者本機的使用者目錄的~/.ssh/id_rsa

這裡有一個問題,就是如果本機要用ssh登入多個ssh連線,比如既要登github,又要登linux伺服器,一個id_rsa檔案會出問題,因為都預設使用這個名稱的檔案為私鑰檔名。這樣的話需要在.ssh目錄下,建立名稱為config的檔案,重新命名私鑰檔案為:比如阿里linux eric使用者的id_rsa重新命名為id_rsa_aliyun_eric,github下的bbb使用者的私鑰id_rsa
重新命名為:id_rsa_github_bbb
config檔案可以配置如下

# 阿里雲eric使用者ssh配置
Host 119.23.xx.xxx
HostName 119.23.xx.xxx
User eric
IdentityFile ~/.ssh/id_rsa_aliyun_eric

# github的ssh配置
Host github
HostName github.com
User bbb
IdentityFile ~/.ssh/id_rsa_github_bbb

這樣一臺機可以實現多處ssh登入了。

4. nodejs的安裝

安裝nodejs有幾種方案,

  1. 通過 yum install nodejs

  2. 通過linux進行Source code編譯安裝

  3. 通過下載編譯好的nodejs原始碼壓縮包免編譯安裝

通過yum install安裝主要就是nodejs版本的問題,不能指定版本安裝。
source code編譯安裝比較麻煩,這裡直接下載編譯好的原始碼包,解壓安裝特定版本的nodejs

wget --no-ckeck-certificate https://nodejs.org/dist/v6.11.1/node-v6.11.1-linux-x64.tar.xz

並解壓安裝到/opt目錄下

tar -zxvf /home/eric/node-v6.11.1-linux-x64.tar.xz -C /opt/

然後通過軟連結node和npm到/usr/local/bin,使node可以在任何環境目錄下執行

ln -s /opt/node-v6.11.1-linux-x64/bin/node /usr/local/bin/node
ln -s /opt/node-v6.11.1-linux-x64/bin/npm /usr/local/bin/npm

最後執行node -v,npm -v有版本顯示說明安裝成功。

5. nodejs建立http伺服器

nodejs環境安裝好後,進行以下http服務的測試

 #進入使用者目錄
 cd ~
 #建立www目錄作為web服務根目錄
 mkdir www
 #進入www目錄建立server.js作為nodejs的http伺服器檔案
 touch server.js

作為測試用nodejs官網的事例程式碼測試下先

const http = require(`http`);

const hostname = `127.0.0.1`;
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader(`Content-Type`, `text/plain`);
  res.end(`Hello World
`);
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

執行node server.js後http服務執行成功,說明nodejs環境已可以順利搭建nodejs的http伺服器

6. 使用git作為版本控制系統

一般linux雲伺服器都預設裝有git,沒有的自行安裝。
如果你想使用github做為git倉庫,大可以使用github,通過github設定將提交的程式碼釋出到linux伺服器的www目錄。(當然私有倉庫要收費)
我們這裡以自建git倉庫為例,說下git倉庫的建立和版本釋出的問題

 #進入使用者根目錄
 cd ~
 mkdir gitrepo
 #建立git倉庫
 cd gitrepo
 mkdir test.git
 cd test.git
 #執行git初始化指令建立空倉庫
 git init --bare

此時在test.git下會有一個.git的資料夾,說明名為test的git倉庫建立完成。(當然還可以建立一個linux使用者git來專門處理git相關的管理,再此我僅以一個eric單一linux使用者來管理,這也是考慮到以後沒有那麼多管理員實際管理的情況,避免linux使用者切換的繁瑣)

最後進入www目錄進行倉庫克隆

cd /home/eric/www
git clone /home/eric/gitrepo/test.git

這樣作為釋出目錄的www目錄正式有了版本控制

7. git本地提交push遠端後,通過hooks鉤子的post-receive自動部署到www

當我們原生程式碼上傳提交後,不能每次都進入www目錄進行人工pull操作,這樣實在不是一個好方式。用什麼方式,當我們本地提交後,git會自動幫我們部署到www目標目錄。就是hooks,也叫鉤子。即git遠端倉庫接受到提交的指令後自動執行的指令碼檔案。

hooks目錄下建立名稱為post-receive的檔案(這個檔案可以理解為git倉庫在收到本地push到本遠端倉庫之後要執行的指令碼)
指令碼事例如下

#!/bin/sh

unset GIT_DIR
DeployPath=/home/eric/www

echo "==============================================="
cd $DeployPath
echo "Starting publish"

git pull origin master

time=`date`
echo "Publish success at time: $time."
echo "================================================"

其實指令碼就是進入到www目錄,然後執行pull操作,只不過是指令碼執行,而不是人工。當然這個是最簡易的版本,隨後我們要討論下,一些根nodejs特殊性的提交細節。

8. 使用yarn替代npm作為nodejs,express的包管理工具

在搭建express架構的nodejs伺服器時,通常我們使用npm管理package.json的install安裝和管理。
但經過實踐考慮,後來經過自我研究決定使用yarn代替npm。主要考慮一下兩方面原因。

  1. npm的速度比較慢

  2. npm的版本依賴不好控制

Yarn 是 Facebook, Google, Exponent 和 Tilde 開發的一款新的 JavaScript 包管理工具。
具體yarn和npm的異同和優劣請自行百度,這裡就不再贅述了,直接說說使用問題。

yarn的安裝:
參照nodejs的原始碼安裝過程,參照官網事例通過軟鏈yarn到/usr/local/bin
不通過yum install的原因是,yum源的版本比較舊,直接yum安裝的都是比較舊的版本,想安裝最新的版本或特定版本,可以參照官網進行原始碼安裝或者編譯安裝。

執行yarn --version顯示版本號,說明yarn安裝成功。
基本的express目錄結構如下

--bin
    www
--public
--routes
--views
  app.js
  package.json

在根據package.json執行yarn install後目錄結構根目錄多了一個yarn.lock的包依賴的鎖定檔案(這個是yarn特有的包管理的依賴管理檔案)
install後,目錄結構類似如下

--bin
    www
--public
--node_modules
--routes
--views
  app.js
  package.json
  yarn.lock

至此應該說,express框架下的node服務算是比較像樣子了,基本可以使用了。

9. 使用pm2作為node http服務的程式管理器

我們都知道使用node server.js會在命令列直接開啟nodejs的web服務,但這樣命令列當前視窗會被阻塞。想輸入其他指令還得退出或者另起視窗。而且如果檔案修改,需要停止後重新載入執行一次才回生效。開發環境還好,但這顯然不是一個正式生產環境所能容忍的。所以我選擇了pm2作為node程式管理的工具(類似的還有forever,不過對比後沒有pm2出色)。

首先安裝pm2(一個小問題:由於我是原始碼安裝npm install -g後模組全域性的會安裝在/opt/nodejs/bin下,同樣要軟鏈到/usr/local/bin,或者新增環境變數,才能全域性使用)

npm install -g pm2
#使用pm2開啟node程式
pm2 start server.js

使用也是很簡單直接pm2 start檔案即可。更多指令詳情可以看看官方文件,寫的非常清楚了。

pm2同樣可以以配置檔案啟動,這裡為了以後便於管理,我們使用啟動配置檔案的形式啟動。pm2預設的配置檔案啟動的檔名為ecosystem.config.js

我簡單配置的啟動指令碼如下,僅供參考:

module.exports = {
  apps : [
    {
      //general
      name      : `node-web`,
      script    : `bin/www`, //啟動執行的初始指令碼

      //advanced
      watch     : [`appsback`,`routes`,`ecosystem.config.js`,`server.js`],//監聽檔案變化
      ignore_watch: [`node_modules`,`apps`,`static`],//忽略監聽的資料夾
      max_memory_restart: `800M`,//記憶體達到多少會自動restart
      env: {
        COMMON_VARIABLE: `true`
      },
      env_production : {
        NODE_ENV: `production`
      },

      //log file
      log_date_format: `YYYY-MM-DD HH:mm:ss Z`,//日誌格式

      //control
      min_uptime: 3000,
      listen_timeout: 3000,
      kill_timeout: 5000,
      max_restarts: 5,
    }
  ]
};

pm2一個比較好的地方是,可以監聽檔案變化,即檔案發生變化後node的http服務會過載而且是0秒過載。這才是一個生產環境應該有的操作…而不是停止後重新載入,對於線上環境基本是不允許的。

這樣只要執行pm2 start ecosystem.config.js就可以開啟一個node的http服務程式啦。

至此我們的express專案的目錄結構大致變化成這樣:

--bin
    www
--public
--node_modules
--routes
--views
  ecosystem.config.js
  server.js
  package.json
  yarn.lock

10. package.json變化導致的自動化部署的問題

我們知道如果本地package.json變化,需要執行npm install或者yarn install安裝模組,但提交後伺服器端怎麼辦呢?一個比較笨的方法就是,每次提交後若package.json變化就進入node的web根目錄即www目錄手動執行npm install或者yarn install。但這種方式顯然不夠智慧,怎麼辦呢….
既然package.json變化必須npm install才能使node http重啟後找到相應模組。於是我有以下兩種形式的思考

  1. 不監聽package.json檔案變化。即每次提交不管package.json有沒有變化,都刪除node_modules資料夾,執行yarn install對package.json依賴重新安裝

  2. 監聽package.json變化,即只有package.json變化才去執行yarn install安裝。

顯然第一種更方便簡單一些,不需要多餘的監聽,減少了伺服器的一些配置。(現在想想這其實就是百度雲bae的策略,他的官方文件介紹就是每次提交不管package.json有沒有變化,都刪除node_modules再裝一遍)

但我想的是第二種顯然更好更合理的,因為如果package.json沒有變化那刪除node_modules再裝一遍顯然是一步浪費資源的操作。剩下的問題就成了

  1. 如何監聽package.json變化

  2. 變化後何時以及怎樣去執行npm install或者update模組。

一開始想到已經在用的工具pm2有監聽功能,但pm2監聽只會產生restart的重啟執行,不具有執行某一特定回撥函式或指令碼的功能(官方文件也說到了這一點)。所以只能想別的解決方案。
後來有想用使用linux系統自帶的inotify監聽檔案變化,另外開啟一個監聽指令碼時刻監聽www目錄下package.json的變化。但最後經過思考決定在git的hooks鉤子post-receive指令碼處理這個事情,這樣可以省掉一個系統監聽的指令碼檔案。

最後的解決方案就變成了:在post-receive指令碼判斷package.json檔案有麼有變化,並在git pull後執行install,然後過載pm2的node http服務程式,一氣呵成。

最後post-receive指令碼修改為以下形式供參考:

#!/bin/sh

unset GIT_DIR
DeployPath=/home/eric/www

echo "==============================================="
cd $DeployPath
echo "Starting publish"

last_modify_time=`stat package.json | grep "Modify"`
echo "t1:$last_modify_time"
git pull origin master

cur_modify_time=`stat package.json | grep "Modify"`
echo "t2:$cur_modify_time"

if [ "$last_modify_time" != "$cur_modify_time" ]; then
  echo "package.json changed"
  rm -rf node_modules
  yarn install
  pm2 restart ecosystem.config.js
else
  echo "package.json not changed"
fi

time=`date`
echo "Publish success at time: $time."
echo "================================================"

我這裡是通過判斷pull前後的package.json檔案的最後修改時間是否有產生變化,來確定package.json本次push過來的是否有修改。
若變化了則執行刪除node_modules再yarn install安裝,最後pm2重啟。若沒有變化,則可以根本不執行以上的操作,省去了執行以上步驟帶來的資源消耗。

當然如果再精細一點可以通過解析package.json裡的依賴dependence的具體變化,是增加了模組還是刪除了模組,還是修改了某一項依賴的版本作出install還是update還是delete的具體操作,而不用所用都重新刪除再安裝。不過這樣對於json解析和npm或者yarn的增刪改查操作過於繁瑣,這個方式被我pass掉了,直接刪了重灌。由於yarn具有安裝過的模組快取,所以yarn isntall帶來的時間上的問題基本可以忽略,速度非常快。這也是為什麼我提倡用yarn替代npm的原因之一。

11. linux重啟後的node web服務自啟動

我們可以重啟linux後,進入linux,進到/home/eric/www目錄下,再次手動pm2 start ecosystem.config.js啟動node的http服務。
雖然linux重啟並不是一件很經常發生的事情,但這種手動需要做的事,當然可以交給linux的開機自啟動指令碼完成。

我們可以自己在,/etc/rc.d/rc.local目錄下自建shell指令碼。這裡我們使用pm2給我們提供的功能,直接建立自啟動指令碼。
根據官網:

pm2 startup

直接根據提示資訊,建立開機自啟的startup script開機自啟動指令碼,非常方便。

12. 非root使用者使用非80埠對映80埠的問題

預設情況下Linux的1024以下埠是隻有root使用者才有許可權佔用,我們的apache,nginx,nodejs等等程式如果想要用普通使用者如eric來佔用80埠的話就會丟擲Permission denied:80的許可權異常。

那我們只能使用8080這樣的較大的埠號,但在url上要帶著埠號這樣可不好。這裡我使用的是iptables進行的埠轉發,即將8080這樣的nodejs的http服務所用埠,轉到80埠上,node服務雖然是8080埠,但使用者url上不用帶埠,直接用預設的80埠,也可以直接轉發到8080的node伺服器處理了。

使用root使用者執行

iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080

完了不要忘了儲存和重啟服務service iptables saveservice iptables restart

另外還可以使用nginx作為反向代理,作埠轉發。

================================================================================

至此,一個nodejs環境的http伺服器,從無到有就建立了起來,有http服務git版本控制自動化部署pm2程式管理,並且可以在生產環境使用的nodejs express的web伺服器。房子蓋好了,至於具體的業務邏輯,就在目錄下盡情的搞裝修吧。

相關文章