番外篇:初探 Nginx

montroyal_blur

于 2020 年 9 月 7 日。

這是一片技術博客,但由於本網站的目的是爲了藝術和娛樂,故放在番外篇了。

值得慶賀的是,現在我的個人頁面已經有五周年(又不到一個月)了。本著生活在於折騰的原則,我又(很早就)起了遷移本網站服務器的念頭。於是今天凌晨我暫時放棄了原來使用的 AWS 機器,轉向 Azure 虛擬機。另一個更重要的長時間縈繞我心裡的結便是由老舊的 Apache Httpd (LAMP 技術棧) 遷往 Nginx (LEMP 技術棧)。實際上我本來思考過使用 FEMP 棧,即用 FreeBSD 代替 Linux,而且我也有前者的使用和工作經驗,但是考量了一圈互聯網上的資源不多,不知道會掉入什麽樣的坑,於是最後還是決定以後有時間了再解決吧。

在這個文章裡我先簡述我是如何遷移一個 WordPress 子域名多站點(即 example.com 和 *.example.com 具有獨立的 WP 安裝)頁面並使之運行在 Nginx (同時也爲我自己留一個參考),再討論一下爲何做如此的遷移。所幸這個過程并不複雜。

第一步 - 備份原數據

這一步與是否是 httpd 沒有關係。假設用戶已經在主機上安裝好了 WordPress (廢話),那麽備份數據僅是拷貝一下文件夾和數據庫罷了。按照推薦的做法,我們應該先關閉所有 WP 插件,不過因爲個人比較懶,只關閉了影響大的插件,諸如:

  • Jetpack by WordPress.com
  • Yoast SEO
  • Site Kit by Google
  • W3 Total Cache (卸載)
  • WP Clean Up
  • WPS Hide Login

至此關閉之後 pic.cleoold.com 就無法正常運作了(笑)。其次由於頁面使用了 Cloudflare (吐槽一下 Firefox 無法登錄 CF 網站),需要在那裡開啓 Dev mode (消除緩存)和關閉 HTTPS。

網站本身重要的只有 /var/www/html/ 文件夾(在我系統上的缺省配置),其包含了所有的 WP 程式和公共的資源,另一個是 phpMysql 中的一個數據庫。爲了省事,我直接將整個硬盤的内容先打包起來:

$ sudo tar -cvpzf backup.tar.gz --exclude=/backup.tar.gz --one-file-system /

會在當前目錄生成一個壓縮包。如果只要複製網站文件夾,把最後的 "/" 替換成路徑即可。這樣網站數據就複製好了。

然後導出數據庫和用戶。這裡假設 WP 所使用的數據庫名為 wp_db,(本地)數據庫用戶名爲 wp_user。如果忘了,這個配置可以在 /var/www/html/wp-config.php 中看到。首先在備份之前登錄 mysql,看一看數據庫的摸樣:

% mysql
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| wp_db              |
| ...                |
+--------------------+
mysql> select User, Host from mysql.user;
+------------------+-----------+
| User             | Host      |
+------------------+-----------+
| wp_user          | localhost |
| ...              | ...       |
+------------------+-----------+

我省略掉了無關的内容。可以看到數據庫目前有這兩個和 WP 有關的内容。以後在恢復備份后,可以再查詢一遍看看是否和當前的數據庫模樣一致。

先使用自帶的 mysqldump 工具導出 wp_db 這一個數據庫:

$ sudo mysqldump -u root -p wp_db > backup_wp_db.sql

然後再“備份”數據庫用戶。我使用的是 pt-show-grants 工具,使用此工具,可以給予它一個已存在的數據庫用戶名,打印一條 SQL 命令用來創建一個完全相同的新用戶。

$ sudo apt install percona-toolkit
$ pt-show-grants --only wp_user

把運行結果拷貝到別的地方。程序的輸出應該類似於:

CREATE USER IF NOT EXISTS 'wp_user'@'localhost';
ALTER USER 'wp_user'@'localhost' IDENTIFIED WITH 'mysql_foo' AS '*foobarbaz' REQUIRE NONE PASSWORD EXPIRE DEFAULT ACCOUNT UNLOCK;
GRANT ALL PRIVILEGES ON *.* TO 'wp_user'@'localhost';

這樣備份工作就做好了:兩個壓縮包,一個文本文件。實際上這些步驟是我常用的備份脚本。在安裝好新主機后,把這些文件拷貝到那裡去。

第二步 - 安裝服務端,還原環境

假設我們已經經過九九八十一難,找到了一個新主機,并且把域名解析指向到了這個新主機。我這裡使用的是 Ubuntu 20.04 LTS。系統安裝完畢后再安裝一些常用小工具例如 curl, python3.8, tmux, htop 等。

簡單的說起,先還原 mysql。首先安裝數據庫程序:

$ sudo apt update
$ sudo apt install mysql-server
$ sudo mysql_secure_installation

後者是一個 CLI 工具,用來對原生安裝做一些安全性調整。其中會問道是否開啓 VALIDATE PASSWORD,視情況看自己懶不懶;還有一項是 DISABLE REMOTE ACCESS,我這裡就 yes 了。順帶一提,在我的系統上,mysql 的默認配置文件在 /etc/mysql/mysql.conf.d/mysqld.cnf,其中的 portbind-address 是可能需要注意的地方。將地址設定爲 127.0.0.1 就表示僅允許本機登錄,與上一個句子效果相同。

還原數據庫備份很簡單:

$ sudo mysql -u root -p -e "create database wp_db";
$ sudo mysql -u root -p wp_db < backup_wp_db.sql

對於還原用戶,先進入 mysql shell,再輸入執行剛才 pt-show-grants 生成的命令,就完成了。

再來安裝 php。安裝這些包(可能需要也有可能不需要,在安裝完成后可以前往工具 -> 網站狀態(應該是?)裡查看缺了哪些):

$ sudo apt install php-fpm php-common php-mbstring php-gd php-intl php-xml php-mysql php-curl php-zip php-imagick

我系統現在安裝的是 php7.4-fpm。注意這裡安裝的包沒有 php,後者會自動安裝 apache httpd。

再找到 php-fpm 配置文件,

$ sudo vim /etc/php/7.4/fpm/php.ini

找到如下行,設置成如下樣子:

cgi.fix_pathinfo=0 ; disable cgi.fix_pathinfo
upload_max_filesize = 300M
post_max_size = 300M

再重啓 php-fpm

$ systemctl restart php7.4-fpm.service

如果不小心安裝了 httpd,而且沒法卸載,則需要停止他:

$ sudo systemctl disable apache2 # or httpd
$ sudo systemctl stop apache2
$ sudo systemctl mask apache2

最後安裝 Nginx:

$ sudo apt install nginx

(可選)這還附帶了 ufw 作爲一個防火墻,可以如此默認配置它:

$ sudo ufw enable
$ sudo ufw default allow outgoing
$ sudo ufw allow ssh
$ sudo ufw allow 'Nginx HTTP'
$ sudo ufw allow 'Nginx HTTPS'
$ sudo ufw status # print status

我使用的雲服務器面板的安全設置,所以這個就忽略了。

服務器自動打開。此時在 /var/www/html/ 下會多出一個默認的 html 文件。在瀏覽器中輸入網址/IP 就可以看到 Nginx 的歡迎頁面。

現在把提前複製好的 WP 文件夾覆蓋到這裡,并且修復權限:

$ sudo tar -xvzf backup.tar.gz var/www/html/ -C /
$ sudo chown -R www-data /var/www/html/
$ sudo chgrp -R www-data /var/www/html/

此時再次打開網站,應該還是原來的默認頁。

第三步 - 配置 Nginx

Nginx 的配置文件夾在 /etc/nginx/。其中 nginx.conf 包含全局配置,sites-available 文件夾包含單個服務器的設置,把這裡面的文件軟連接到 sites-enabled 文件夾就表示設置啓用了。這裡使用 php-fpm 來處理 Nginx 的鏈接,我來給出我的設置文件,這是基於默認提供的配置文件修改的:

/etc/nginx/nginx.conf

user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
        worker_connections 768;
        # multi_accept on;
}

http {
        ##
        # Basic Settings
        ##

        sendfile on;
        #tcp_nopush on;
        #tcp_nodelay on;
        keepalive_timeout 3;
        types_hash_max_size 2048;
        # server_tokens off;

        # server_names_hash_bucket_size 64;
        # server_name_in_redirect off;

        include /etc/nginx/mime.types;
        default_type application/octet-stream;

        #php max upload limit cannot be larger than this
        client_max_body_size 300m;

        index index.php index.html index.htm;

        ##
        # SSL Settings
        ##

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
        ssl_prefer_server_ciphers on;

        ##
        # Logging Settings
        ##

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        ##
        # Gzip Settings
        ##

        #gzip on;

        # gzip_vary on;
        # gzip_proxied any;
        # gzip_comp_level 6;
        # gzip_buffers 16 8k;
        # gzip_http_version 1.1;
        # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

        ##
        # Virtual Host Configs
        ##

        #include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;
}


#mail {
#       ...
#}

/etc/nginx/global/restrictions.conf

# Global restrictions configuration file.
# Designed to be included in any server {} block.
location = /favicon.ico {
    log_not_found off;
    access_log off;
}

location = /robots.txt {
    allow all;
    log_not_found off;
    access_log off;
}

# Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).
# Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban)
location ~ /\. {
    deny all;
}

# Deny access to any files with a .php extension in the uploads directory
# Works in sub-directory installs and also in multisite network
# Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban)
location ~* /(?:uploads|files)/.*\.php$ {
    deny all;
}

/etc/nginx/sites-available/default.conf

##
# You should look at the following URL's in order to grasp a solid understanding
# ...

map $http_host $blogid {
        default       -999;
        #Ref: https://wordpress.org/extend/plugins/nginx-helper/
        #include /var/www/wordpress/wp-content/plugins/nginx-helper/map.conf ;
}

# Default server configuration
#
server {
        listen 80 default_server;
        listen [::]:80 default_server;

        # SSL configuration
        #
        listen 443 ssl http2;
        listen [::]:443 ssl http2;
        #
        # Note: You should disable gzip for SSL traffic.
        # See: https://bugs.debian.org/773332
        #...
        ssl_certificate /etc/ssl/example.com.pem;
        ssl_certificate_key /etc/ssl/example.com.key;

        root /var/www/html;

        # Add index.php to the list if you are using PHP
        index index.php index.html index.htm index.nginx-debian.html;

        error_page 403 /403.html;

        server_name example.com *.example.com;

        include global/restrictions.conf;

        location / {
                try_files $uri $uri/ /index.php?$args;
        }

        location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
                expires max;
                log_not_found off;
        }

        # pass PHP scripts to FastCGI server
        #
        location ~ \.php$ {
                include snippets/fastcgi-php.conf;

                # With php-fpm (or other unix sockets):
                fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
                # With php-cgi (or other tcp sockets):
        #       fastcgi_pass 127.0.0.1:9000;
        }
        #WPMU Files
        location ~ ^/files/(.*)$ {
                try_files /wp-content/blogs.dir/$blogid/$uri /wp-includes/ms-files.php?file=$1 ;
                access_log off; log_not_found off;      expires max;
        }

        #WPMU x-sendfile to avoid php readfile()
        location ^~ /blogs.dir {
                internal;
                alias /var/www/example.com/htdocs/wp-content/blogs.dir;
                access_log off;     log_not_found off;      expires max;
        }
}

這是三個文件。再次重申一下,這是 WP 子域名多站點的配置。對於其他配置,可以參考官網上的提示(文末有鏈接)。

注意到 ssl_certificatessl_certificate_key 選項,它們是 SSL 證書的鑰匙文件。我在服務端使用的是 Cloudflare 的 wildcard 證書,同時能應付多個子域名,故而把這兩個選項置此。在獲得 SSL 證書的 .pem 和 .key 之後,把它們拷貝在配置中的這兩個地方就可以了。

最後再執行配置文件格式檢查,就可以重載 Nginx,見到效果了。

$ sudo nginx -t
$ sudo systemctl reload nginx

此時再打開網站,就能看到原來的 WP 頁面。接下來的工作就是登錄,啓用插件,測試發文章了。

爲什麽從 Apache 遷到 Nginx

引用網絡上常見的原因:apache 老舊,nginx 相比“較新”。在使用 LAMP 的時候,每次用戶訪問都會導致系統創建一個新進程(也就代表一堆新内存,一個新 php 解釋器)來處理。這種同步處理請求的方法資源占用較多,對於這個網站運行的小低配主機來説,非常災難,完全沒有半點抗并發能力,個位數就會死機。而 nginx 使用異步的事件隊列,也就不需要那麽多資源占用,輕裝上陣。有人説性能瓶頸實際上在 php(-fpm) 和 mysql 上,對此我的回答是,無論是速度慢還是不穩定,總比死機了之後無法重啓要好一下吧。其次是現在很多網站都使用 nginx,爲了追上時代潮流也要跟進。據説 WordPress 官網就是運行在 nginx 上的。

500 errors
這是我第一次在瀏覽器中同時訪問 httpd 服務器 100 次的盛況。有時 apache 進程會占滿内存和 swap

爲什麽換服務器

實際上如果只是想切換服務端的話,也並不需要做那麽多備份然後搬遷整個主機。這麽做的原因包含:可以丟棄掉某些我可能之前沒有發覺的惡意文件;可以趁機升級系統内核;最後是遷移到 Azure 之後,不説別的,我發現它的控制面板(特別是安卓 app)比他的同行順眼,易用好幾倍。我現在使用著很多微軟產品,多一個何嘗不可。

後記

更改域名

有的用戶想在搬遷的過程中同時更改域名,比如如果想在本地測試的話,可以把 example.com 和 *.example.com 更改成 localhost 和 *.localhost (當然修改 hosts 也可)。這時候需要修改兩個地方:wp-config.php 中和域名有關的常量,例如 DOMAIN_CURRENT_SITE,以及數據庫中的相關行。對於 WP 數據庫中的内容,又分爲兩部分:WP 站點設定中的網站地址(這些可以在網站設置中更改)和文章中的超鏈接。如果想更改全部,比較有效(沒有遺漏)的方式是在整個數據庫裡直接查找全部關於 example.com 的字符串并且替換成新的域名。我使用的是 Search-Replace-DB,相關的使用方法和警告都在那裡有所提及。

Cloudflare SSL

很多人使用 CF 是爲了它的免費 wildcard SSL。和網站挂了 CF 顯示的頁面一樣,當用戶使用頁面時,服務器會和 CF 的服務器通信,而後 CF 再和用戶的電腦鏈接。這個過程中 SSL 的運作和普通的不套 CDN 的時候不同:因爲有三個終端,兩次數據傳送,所以 HTTPS 傳輸也有兩次。自己的服務器和 CF 通信時會使用一個服務器端的 SSL 證書(稱之爲 origin certificate),CF 在和用戶通信的時候會使用另一個 CF 官方的 SSL 證書。前者需要的證書需要自己在 nginx 裡設置,而後者需要的證書由 CF 負責,不需要自己操作。在上面提到過的 SSL 證書就是前者(CF 提供了能夠在服務端使用的證書——也可以選擇不用,而去使用自簽名或 Let's Encrypt,對於用戶端小綠鎖上的文字都是一樣的)。這是我以往混淆的地方,以此參考。

參考

發表迴響