于 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,其中的 port
和 bind-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_certificate
和 ssl_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 上的。
爲什麽換服務器
實際上如果只是想切換服務端的話,也並不需要做那麽多備份然後搬遷整個主機。這麽做的原因包含:可以丟棄掉某些我可能之前沒有發覺的惡意文件;可以趁機升級系統内核;最後是遷移到 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,對於用戶端小綠鎖上的文字都是一樣的)。這是我以往混淆的地方,以此參考。