WordPress 備份與恢復記錄
很久以前就想要好好地重整一下我的 blog 了,剛好趁著搬家重灌系統的機會,好好地整理一番。那時的目標有
- 真正的版本控制
- 資料庫瘦身
- 升級後台編輯用的 FCKeditor
- 與 Sidebar Widget 相容
- IE 下的畫面極為慘烈
其中,「升級後台編輯用的 FCKeditor」已經在《WordPress 編輯器升級到 FCKeditor 2.5.x》時搞定,theme 的問題暫時先不解決,所以剩下「真正的版本控制」與「資料庫瘦身」兩個目標。
真正的版本控制
以前的作法是,用 Subversion 管理我的 WordPress 目錄,不過只是簡單的直接 checkout 出 WordPress 程式。雖然升級或更換版本很方便,svn update 或 svn switch 一下即可,不過自己在 local 端,因應自己的喜好而做的更改,就沒辦法有好的版本控制機制,加以管理了。
也就是說,我希望在能夠隨時將 WordPress 主網站的更新同步回來之外,還能夠保有自己因喜好而產生的一些小改變。如果 WordPress 主程式和我自己的改變,是存放在同一個 subversion repository 裡,那這一切就可以做到。
簡單講就是,如上圖,我希望能夠隨時 incrementally 從 WordPress 主 subversion repository 將最新的修正,mirror 到自己電腦上的 subversion repository 裡,成為一條 mirror branch。然後,又能夠有自己的一條 local branch,儲存自己的喜好,同時又能夠隨時視需要,將 mirror branch 裡的東西,merge 進我的 local branch 裡。
換句話說,我需要某種 subversion repository mirroring 的機制,且目的地端不能夠是 read-only,否則我就沒辦法擁有自己的 branch。
目前可以用的工具,有這三種:Subversion 原廠的 svnsync、SVN::Mirror 與 SVK。
使用 SVN::Mirror 複製 wordpress repository
Subversion 原廠的 svnsync 只能夠 mirror 出 read-only repository,目的端是來源端的完整複製,拿來做備份很好用,但不符合我的需求。SVK 有用到 SVN::Mirror,所以我先試試 SVN::Mirror。
SVN::Mirror 有附一個叫 svm 的工具,可以讓我們直接在 command line 呼叫 SVN::Mirror 做事情,這樣就不需要寫程式了。假設目的端的 subversion repository 在 /svn/wp,使用 svm 的方法如下:
SHELL> env SVMREPOS=/svn/wp svm init /mirror http://svn.automattic.com/wordpress/ SHELL> env SVMREPOS=/svn/wp svm sync /mirror
這樣就搞定了。/svn/wp 不需要是一個「乾淨」的 repository,只要 file:///svn/wp/mirror 目錄不存在即可。
接著,我就可以用 svn copy 製造自己的 local branch:
SHELL> svn copy file:///svn/wp/mirror file:///svn/wp/local
然後任何自己做的修改,就直接 commit 到 file:///svn/wp/local,想到時,就再 svm sync 一下,然後用 svn merge 把 file:///svn/wp/mirror 裡的新的東西,merge 到 file:///svn/wp/local 裡即可。
使用 SVK 複製 wordpress repository
用 svn log 翻閱利用 SVN::Mirror 所做出來的 mirror branch,大致如下:
SHELL> svn log --limit 2 file:///svn/wp/mirror ------------------------------------------------------------------------ r6353 | ryan | 2007-12-04 08:40:00 +0800 (二, 04 12 2007) | 1 line Avoid unnecesary call to get_userdata in get_permalink function. Props xknown. fixes #5414 ------------------------------------------------------------------------ r6352 | ryan | 2007-12-04 08:19:10 +0800 (二, 04 12 2007) | 1 line Don't save page and attachemtn uris to page_uris and page_attachment_uris. This is not needed. Add an option to use wildcard page rewrite rules instead of per-page rules. see #3614 -----------------------------------------------------------------------
我們會發現,雖然 commit author/message 等資訊都還存在,可是所對應的來源端 revision number 遺失了,例如上例中的 r6353,在 wordpress repository 裡實際上是 r6352。如果說 mirror 目的端是個乾淨且完整複製的 repository,目的端的 revision number 就會剛好是來源端的 revision number,故不會有遺失的問題[1]。但很可惜的是,我需要在 local repository 裡建立 local branch,並 commit 進自己的改變,故一定會造成 revision number 無法對應的情況。
因此,我只好祭出 SVK 來,改用 SVK 來進行 mirror,步驟大致如下:
# 建立 local subversion repository。
SHELL> svnadmin create /svn/wp
# 製造 svn-sync 專用的 svk depot。
SHELL> svk depotmap sync ~/.svn-sync
# 設定將 /sync/wp/remote 映射到 wordpress 的 subversion repository URL。
SHELL> svk mirror /sync/wp/remote http://svn.automattic.com/wordpress
# 設定將 /sync/wp/local 映射到 local repository 的 URL。
SHELL> svk mirror /sync/wp/local file:///svn/wp/mirror
# 將 wordpress 的 changesets 複製到 svk depot 裡。
SHELL> svk sync /sync/wp/remote
# 利用 smerge 進行複製,將 svk depot 裡的 wordpress changesets 複製到 local
# subversion repository 裡的 mirror branch。
# 之後的複製,都不必再加上 --baseless,只有第一次做的時候才需要。
SHELL> svk smerge --incremental --log --baseless \
/sync/wp/remote /sync/wp/local
# 之後的複製,只需要做 svk sync 與 svk smerge 即可,且不必再加上 --baseless
# 參數,只有第一次做的時候才需要。
SHELL> svk sync /sync/wp/remote
SHELL> svk smerge --incremental --log /sync/wp/remote /sync/wp/local
這樣複製之後,原本在 http://svn.automattic.com/wordpress 的 changesets,就會通通被複製到 file:///svn/wp/mirror 裡。若是我們翻閱 file:///svn/wp/mirror 的 log,可以看到,log 裡完整地保留了,原始 wordpress repository 的 revision number,以及作為中介轉送站的 svk depot 的 revision number,如下:
r6301 | jeffhung | 2007-11-22 08:48:33 +0800 (四, 22 11 2007) | 3 lines r12502@dev (orig r6300): ryan | 2007-10-31 11:59:18 +0800 wpdb::set_prefix(). fixes #5287 ------------------------------------------------------------------------ r6300 | jeffhung | 2007-11-22 08:48:31 +0800 (四, 22 11 2007) | 3 lines r12501@dev (orig r6299): ryan | 2007-10-31 11:53:32 +0800 AJAX for link category add and delete. Props mdawaffe. fixes #5291
如範例裡的第一筆,r6301 是 local 端的 repository 的 revision number,r12502 是負責中介轉運的 SVK depot 的 revision number,而 r6300 則是官方 wordpress repository 的 revision number,ryan 則是該筆 changeset 的作者。如此一來,當有需要追蹤問題時,就可以清楚地找出對應的原始 revision number,從 wordpress 官方的角度,與 wordpress community 討論了。
迴避 SVK 的 memory leak 問題
不過,在使用 SVK 轉送 wordpress repository 內容時,卻碰到了 memory leak 的問題。
值此時,Wordpress 官方 repository 共有 6352 個 revisions,最後跑 svk smerge 時,使用的記憶體量會飆到 500MB 左右[2],然後停在 r1862 左右,不斷地印出 perl in free(): warning: page is already free 的訊息,只能用 Ctrl-C 跳開:
SHELL> cd ~/tmp/svk-mess-usage SHELL> svn-newrepo.sh --layout mirror wp-svn Layout (mirror): | /local | /mirror 送交修訂版 1. SHELL> svk depotmap --init wp-svk wp-svk SHELL> svk mirror /wp-svk/remote http://svn.automattic.com/wordpress SHELL> svk sync /wp-svk/remote SHELL> svk mirror /wp-svk/local file://`pwd`/wp-svn SHELL> svk smerge -I -l -B /wp-svk/remote /wp-svk/local ... perl in free(): warning: page is already free perl in free(): warning: page is already free perl in free():Killed Syncing file://`pwd`/wp-svn Retrieving log information from 1862 to 1862 Committed revision 8205 from revision 1862. SHELL>
這種大量使用導致記憶體飆增,最後陷入 perl in free(): warning: page is already free 無窮迴圈的問題,b6s 也回報過:#26826 - perl in free(): warning: page is already free。
為了迴避這個問題,我修改上述的步驟,將 sync/smerge 拆成很多次做,一次只做 100 個 revisions。如此一來,每次記憶體用量僅約 20MB,不會碰到 perl in free(): warning: page is already free 無窮迴圈的問題,雖然耗時較久,但最後確實將 6352 個 revisions 完整搬動完畢。
我使用的 script 如下,歡迎使用:
#!/bin/sh -e
# ---------------------------------------------------------------------------
# Copyright (c) 2007, Jeff Hung
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#.
# - Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# - Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# - Neither the name of the copyright holders nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# ----------------------------------------------------------------------------
svn_sync_via_svk()
{
local name="$1"; shift;
local from_url="$1"; shift;
local to_url="$1"; shift;
local svk_depot_name="$1"; shift;
local svk_depot_path="$1"; shift;
local svk_sync_limit="$1"; shift;
# Ensure that $svk_depot_path doesn't contain '~'.
svk_depot_path=`realpath "$svk_depot_path"`;
echo "[INFO] Setting up SVK transporter at $svk_depot,";
echo "[INFO] | using SVK depot '$svk_depot_name' at '$svk_depot_path',";
echo "[INFO] | which can be later accessed via 'svk depot /$svk_depot_name'.";
svk depotmap "$svk_depot_name" "$svk_depot_path";
echo "[INFO] Setting mirror from $from_url to /$svk_depot_name/$name/remote.";
svk mirror "/$svk_depot_name/$name/remote" "$arg_from_url";
echo "[INFO] Setting mirror from $arg_to_url to /$svk_depot_name/$name/local.";
svk mirror "/$svk_depot_name/$name/local" "$arg_to_url";
from_url_head=`\
svn info --xml "$arg_from_url" \
| xml sel --text --template --value-of //entry/@revision \
`;
echo "[INFO] Head revision of $arg_from_url is r$from_url_head.";
from_url_rev_beg=0;
from_url_rev_end="$opt_svk_sync_limit";
while [ $from_url_rev_beg -lt $from_url_head ]; do
echo "[INFO] Syncing from -r$from_url_rev_beg:$from_url_rev_end $arg_from_url.";
if [ $from_url_rev_end -le $from_url_head ]; then
svk sync --torev $from_url_rev_end "/$svk_depot_name/$name/remote";
else
svk sync "/$svk_depot_name/$name/remote";
fi;
if [ $from_url_rev_beg -eq 0 ]; then
svk smerge --incremental --log --baseless \
"/$svk_depot_name/$name/remote" \
"/$svk_depot_name/$name/local" \
;
fi;
svk sync "/$svk_depot_name/$name/local";
from_url_rev_beg=`expr $from_url_rev_end + 1`;
from_url_rev_end=`expr $from_url_rev_beg + $opt_svk_sync_limit - 1`;
done;
echo "[INFO] Done.";
}
svn_sync_via_svk $@;
其中,xml 指令來自於 XMLStarlet 這個套件,讓我得以很方便地直接在 command-line 剖析加了 --xml 選項的 svn 指令輸出,這樣就避免了 locale 對直接的 svn 指令輸出的影響。
另外,$to_url 這個 local repository 要自行建立,且必須先具備 /mirror 與 /local 兩個目錄。
AUTO_INCREMENT 不見了
有了完美的 source code 來源,安裝好全新的 wordpress,把plugins/、themes/ 目錄與資料庫倒回去,修改好 wp-config.php 之後,理論上我的 blog 應該就恢復了,可是我卻發現,發表的新文章,卻不見蹤影,直接進 mysql 裡看才發現,新文章的 ID 居然是 0,用 DESCRIBE wp_posts 查才知道,wp_posts.ID 的 AUTO_INCREMENT 不見了。
我的 wordpress 資料庫備份,當初是用這樣的指令備份的:
mysqldump --skip-opt \
--skip-quote-names \
--complete-insert \
--add-drop-table \
--add-locks \
--lock-tables \
--user <user> \
--password \
<wp-dbname> \
> backup-wordpress.sql
打開 backup-wordpress.sql 發現,裡面所有的 CREATE TABLE 指令,該有 AUTO_INCREMENT 的地方,都不見了,因此知道,問題出在當初 dump 資料庫的指令有問題。
首先,先解決錯誤的 dump 檔。因為翻遍了 MySQL 文件,我還是找不出,怎樣用 ALTER TABLE 直接把 AUTO_INCREMENT 加上去,因此只好直接修改 backup-wordpress.sql,將該有 AUTO_INCREMENT 的地方手動編輯加上去,然後重新建立空的資料庫,把 backup-wordpress.sql 匯入。
接著,man mysqldump 發現,還有一個參數叫做 --create-options:
o --create-options Include all MySQL-specific table options in the CREATE TABLE statements.
因為 AUTO_INCREMENT 是 MySQL 特有的選項[3],因此如果沒有加上 --create-options 的話,mysqldump 就不會把 AUTO_INCREMENT 也 dump 出來。因此,我的 wordpress 資料庫備份方法,應該改為:
mysqldump --skip-opt \
--skip-quote-names \
--complete-insert \
--add-drop-table \
--add-locks \
--lock-tables \
--create-options \
--user <user> \
--password \
<wp-dbname> \
> backup-wordpress.sql
如此才能確保,資料庫的所有內容,被完整的備份下來。
資料庫瘦身
WordPress 很不好的一點就是,很多 plugins 會直接在資料庫裡面新增 table,甚至是修改原有的 wordpress 的 tables,增加了新的 column。因此,倘若有一天,某個修改了資料庫 schema 的 plugin 不再被使用之後,資料庫裡就會留下一堆「垃圾」,且沒有好的方法可以清除。
雖然說,就算是垃圾,其實量也還不大,但有時候就是會讓我感到不爽,想要清乾淨。
我最初始的想法是,利用 wordpress 的 export 功能,將資料庫裡的資料,export 出來另存成一個 XML 檔,假設叫做 wordpress-export.xml。然後,用 wordpress 的安裝功能,建立全新的空資料庫,然後再用 import 功能,將 wordpress-export.xml 匯入新的 wordpress。如此一來,資料庫裡的資料,確實非常乾淨,僅剩下真的需要留在資料庫裡的資料。
不過,接下來我就發現,所有連往自己 blog 的文章連結,全部都亂掉了。好比連往 A 頁的鍊結,點下去卻跑到 B 頁了。仔細觀察後發現,這是因為我的 permalink 的設定,與 AUTO_INCREMENT 的關係。我的 permalink 的設定如下:
/articles/%author%/%post_id%/
因為這是我個人的 blog,所以 %author% 一定是 jeffhung,所以,整個 permalink 可以用來辨別這到底是哪一篇文章的欄位,就只剩下 %post_id%,而 %post_id% 對應到資料庫裡的 wp_posts.ID 這個欄位,是設定成 AUTO_INCREMENT 的。因此,當經過 export 再 import 之後,wp_posts.ID 會重新經過 AUTO_INCREMENT 的作用,重新產生索引值。由於 export 出來的文章順序,並不以 wp_posts.ID 排序,且刪除文章也會造成 wp_posts.ID 變成不連續。是故,當重新 AUTO_INCREMENT 之後,文章的 wp_posts.ID 就變了。可是文章裡的 <a href="..."> 鍊結不會改變,於是就牛頭不對馬嘴了。
解決方法初步想來,有兩種:
- 修改 wordpress 的 import 程式,把
wordpress-export.xml檔裡,每篇文章所記載的<wp:post-id>值,納入INSERT指令。由於AUTO_INCREMENT是當INSERT INTO時卻沒有該欄位時才發揮作用,因此不會造成wp_posts.ID錯亂。 - 分析
wordpress-export.xml,利用<wp:post_name>的值,與 import 之後的資料庫裡的wp_posts.post_name欄位,來解析出原始%post_id%與新的wp_posts.ID的對應表,然後利用我在這篇《Kramer 與 get-recent-comments 的問題》提到的方法,進去改所有的文章內文,修正連往自己 blog 的所有連結。最後,修改 permalinks 格式,把這個對應表 hard-code 進程式,當遇到舊格式時,就查表轉到新 permalinks。
前者雖然需要改 wordpress 的程式,但只有一點點,而且可以保證 %post_id% 不變,所以應該是比較好的方式。而後者就麻煩很多,而且因為 %post_id% 的改變,必須要改變 permalinks 格式,以便接受來自外站,循著舊 permalink 過來的尋訪。
不過,我早就想改 permalinks 格式了,所以就試了一下第二個方法。
修改 permalink 的方法
我想要把 permalink 改成下面這個樣子:
/articles/%post_name%/
沒有年月日,只有 %post_name%,也就是 wordpress 編輯畫面裡,Post Slug 那個欄位的值,相當地簡潔,乾淨。對我來說,blog 的 log 成份並不大,反而比較像是一種偏文章性質的 wiki 型的個人出版媒體。事實上,Thinker 的 GinGin 系統的那種「形式」,比較符合我理想中的樣子。因此,僅使用具有「意義」的 %post_name% 作為 permalink 的組成元素,很合我意。
雖然理論上應該要將原來的 permalink 前置的 /articles 改變,好比變成 /posts,因為這將是前端據以判斷是哪種版本的 permalink 的重要依據,但是因為我原來的 permalink 還有一個永遠是 jeffhung 的 %author% 存在,所以可以利用這個來判定新舊版本,故 permalink 仍然可以用 /articles 作為前置字串。
修改原始文章內容,將舊 permalinks 代換成新 permalinks 不難,只要先從資料庫裡,抓出「(舊) %post_id% 對應 %post_name%」的對應表,等到 wordpress-export.xml 匯入新灌好的 wordpress 之後,再從資料庫裡,抓出「%post_name% 對應(新) %post_id%」的對應表,兩者合併,就可以做出「舊 %post_id% 對應新 %post_id%」的對應表。
有了對應表之後,我們需要讓舊 permalinks 自動轉換到新 permalinks 的機制。也就是說,若 user 用舊的 permalinks 來連,就要自動用 HTTP status code 301 Moved Permanently 轉址到新的 permalinks。Permalinks Moved Permanently 這個 wordpress plugin 正可以這麼做。
然而 Permalinks Moved Permanently 卻沒能發揮作用,翻 code 發現,似乎 Permalinks Moved Permanently 能夠應付任何 permalinks 格式的組合,就是不能處理 %post_id%,故我的舊 permalinks 她看不懂。又因為其所掛載的 template_redirect 這個 action,太過於「前面」,很多關於這個頁面的值,都尚未載入[4],因此一時之間,我找不到方法修正這個問題。
還好,最後我發現,$_SERVER['REQUEST_URI'] 還可以用,因為舊 permalinks 的確實格是我知道,所以可以利用 regular expression 將舊的 %post_id% 擷取出來,繼而查表產生新的 permalinks。
至此,修改 permalinks 可能會碰到的技術問題,應該都解決了。不過,最後我還是沒有修改 permalinks,因為整個程序太過複雜,而我又急著將 blog 恢復上線,因此就暫時放棄了這個計畫。反正,過不久又將要換硬體,到時再來弄吧。
沒有對應 <VirtualHost> 設定的 request
最後,我終於知道了,若 http request 要連的 host,沒有對應的 <VirtualHost> 設定時,並不會如之前我想當然爾地,改連 global 設定 (/usr/local/www/data),而是會對應到 httpd.conf 裡第一個出現的 <VirtualHost>。因此,我們需要有一組 dummy <VirtualHost> 擺在第一筆,以便攔截沒有對應 <VirtualHost> 的 http request。
因為搞不清楚這個機制,所以一度讓 feed大亂,張冠李戴地誤植我的文章到其他 blog 的 feed 裡。下次真的要記得看清楚設定檔裡的範例啊。
- 如果是這種情況,那使用
svnsync即可,不需要動用到SVN::Mirror,不僅效率會好一些,而且要安裝的軟體更少。 ↩ - 這台 FreeBSD 6.2 P4-2.4G 的機器,實體記憶體才 512MB。 ↩
- 據說
AUTO_INCREMENT有很大的問題,如果可以的話,最好使用SEQUENCE。大家來投靠 PostgreSQL 吧。 ↩ - 因為看不懂 URL 了,所以不可能知道要連的是哪一樣,故關於該頁面的許多值,也不可能載入。 ↩




4 Comments
可以給每篇文章加入 old_slug ,內容則是各篇文章的 post_id ,這樣 WordPress 就會自動用 301 redirect 過去。
old_slug 在 wp_postmeta 這張表裡。
bcse,
真是感謝,這可真是密技了。
Jeff Hung
您對中文輸入法(IME)有興趣嗎?
如果有﹐請給我電郵回覆。謝謝。
香港Chionk
雖然我是寫PHP的。。
咋就是看不懂你後半段在寫啥 。。
Post a Comment