Amazon EBS (Elastic Block Store)

Amazon EC2 一直都沒有保證在上面的磁碟資料不會掉,這使得 MySQL 必須透過定時備份到 S3 以及即時將 binlog 備份到非 Amazon 的站台以確保資料的安全性。

之前的解法是將資料放到 Amazon SimpleDB 上,但 SimpleDB 的空間有限制 (還在 limited beta),而且會有被綁在 Amazon 上,如果想要搬出來會找不到 open source 的替代方案。另外一個問題是熟悉度的問題,大家還是比較偏好用 MySQL 這類的 RDBMS。

這個限制終於在 Amazon 推出 EBS (Elastic Block Store) 解決了:你可以在 EC2 上開一個磁碟空間給 MySQL 用!

這個空間確保資料的持久性外,還可以 snaphost 到 S3 上。第一次 snapshot 會是 full backup,後面都是 incremental backup。單一 EBS 的空間可以從 1GB 到 1TB,而目前 beta 階段一個帳號可以開 20 個 EBS,也就是最多可以吃到 20TB。磁碟會是 block level,也就是說你可以在上面跑任何你想要的 filesystem,或者利用 stripe 提高效率。

目前計費的方式,除了 storage 的費用外,每百萬次 I/O 會收 USD$0.10,snapshot 的費用是依照 S3 使用的量計算。不過這些都有可能會改變,可以參考 EBS 的網頁。

參考:Amazon EBS (Elastic Block Store) - Bring Us Your Data

這次 PIXNET 前後台的一些整理

趁著在家養傷 (腳痛) 寫程式的時候,順便把這陣子 PIXNET 在重新改寫的部份紀錄下來,從底層與 OS 比較有關的、PHP 的,以及 Web UI 的部份。

FreeBSD 的 NFS client 的效能並不好,在這次 PIXNET 前後台大改版前,我這幾天重新跑數據看目前舊系統的架構,可以看出來 PHP code 放到 NFS 上面所吃的 system CPU resource 比 userland CPU resource 還多:(這是其中一台 blog 主機的 CPU usage,用 Munin 畫出來的圖,中間斷掉那段是我在改 Munin 的設定...)

從圖上可以看出這台跑 blog 的主機有 4 Logical CPUs,但卻有很多 idle time。這是因為 NFS 量更大時會不穩定,所以我們無法使用更高。這次改版把所有的 code 都放到 local disk 上,應該會有很大的改進。

MySQL 還是用 Linux 比較好,同一台機器 (Xeon E5405 + 16GB RAM + 10KRPM*2) 分別跑過 FreeBSD 7-STABLE、8-CURRENT (20080812)、Linux 2.6。在 FreeBSD 上用 UFS2 (包括 noatime + async 與 noatime + softupdate 都有測試) 一直都是 I/O bound,而在 Linux 上用 XFS 一直都很順。這是用 MySQL slave 跑 real traffic 而非模擬測試。

新的資料庫系統還是用 Linux 平台,然後引入 DRBDInnoDB 達到 High Availibility。不過我想在這陣子忙完後測 MMM,以他的實做方式看起來會比 DRBD 好。(但 DRBD 開發比較久,資料比較豐富,也比較容易找到穩定的設定)

PHP 的部份仍然使用 Apache 2.2 + mod_fastcgi 與 PHP。不過這次不跑 event mode,而是跑 worker (threading mode)。在正確的設定下,APC 的 cache 是整台機器共用,使用的記憶體更省。上面是 blog2 (worker MPM),下面是 blog5 (event MPM) 的記憶體使用量:(附註:我覺得 event 應該也可以做到同樣的事情,這次換 worker 是因為種種機緣 XD)

其中 FreeBSD 上可用記憶體空間的意義可以參考 Inactive memory 這篇,並不是只有 Free 代表可用空間。

靜態檔案在重新建立架構時,儘量拆開到其他 domain 上,除了可以 pipeline download,也可以節省使用者送 cookie 的頻寬,這部份的伺服器改用 nginx,因為要用他的 gzip on the fly 讓下載的量減少。我們用到的 javascript framework 盡量都塞進這個系統裡。

PHP 的部份,這次是賭了一把,選擇 Zend Framework 實做前後台,而且完完全全使用 ORM framework 處理資料。以目前的量去推算,看起來應該是沒問題,不過還沒上線前誰都不敢說,換燈管的 CTO 還為此先準備另外的機器,如果真的不行就用先暫時用機器海換出來...

我們用到的部份包括 Zend_ControllerZend_ViewZend_Feed。(不,我們沒有用 Zend_Db,而是有個撞到腦袋的人寫了 Pix_Table_Cluster...)

不過 Zend_Controller 並不好用,沒有針對開發者的想法發展 (另外一種說法是,沒有針對 PHP 語言特性實做簡潔有力的語法),所以之後可能會自己開發 Pix_Controller。舉兩個例子說明:

  • 要抓參數可以用 $this->getRequest()->getParam() 抓,也可以用 $this->_getParam() 抓。我可以理解後面是前面的捷徑,但如果 implements ArrayAccess 不是更好嗎?$this->getRequest() 抓出來的 object 可以直接 $obj['blog'] 抓出 blog 這個變數。
  • 如果我想把 /foo/bar/12/34/56/78 拆開,我必須用 Route 做,然後再用 getParam 抓到參數。或是在 barAction 裡面直接自己拆開。我個人比較偏好的方法是先去找 bar_12_34_56_78_Action,沒有再一路往上找,最後會是 bar_Action('12', '34', '56', '78'),這樣寫 code 才會方便。
  • 對 Helper 的使用相當不方便,但這個部份還沒有仔細想要怎麼做才會方便。

另外一個沒有用的是 Zend_Form,原因在於 Zend_Form 的預設值會使得客製化很困難。所以我們自己開發了 Pix_Form,只產生個別的元件,而不產生整體的 Form,所以你可以拿到一個版面後套版進去用。但仍然可以用 Pix_Form validate。

Deploy PHP code 的事情,目前是用 rsync 做,但 rsync 的效率並不高 (不過目前是夠用了),只能加減請 coder 擔待點。前陣子找了不少這類軟體在測試,像是 csync2,不過他同步的方式與期望了方式有落差,也許改變流程,配合 csync2 的方式做,或者是不改變流程,自己從頭幹一個出來?

Web UI 的部份這次改寫時直接強迫大家裝 Html Validator 直接檢查。我對 XHTML 1.0 沒有什麼好感 (事實上這次改版大多都是用 HTML 4.01),但至少不要有 <div> 不對稱這類 browser 會亂猜一通的問題。這樣才不用在奇怪的 DOM tree 裡面操作。

在政策上,javascript 全部用 jQuery,目前的 javascript code 幾乎都是 jQuery 寫出來的。Unobtrusive Javascript 目前只是個理想,有很多地方還是得直接處理。

另外就是 CDN 的事情,有機會再說 orz

FreeBSD 上的 MySQL 6.0

年初的時候 MySQL 6.0 把 libevent 匯入 source tree 裡,用 libevent 處理 connection pooling (之前似乎是 poll?)。

FreeBSD 剛把 MySQL 6.0 納入 ports 裡 (databases/mysql60-server),可以用 WITH_THREAD_POOL=yes 把這個功能打開。

理論上,使用 libevent 後在 Web 端用 persistent connect 連 MySQL 應該不會影響到效能。

附上年初時看到的資料:MySQL 6.0, Libevent

關於 MySQL 的 --with-fast-mutexes

ports/125616: Add --with-fast-mutexes to databases/mysql51-server」這個 PR 中所提到的 --with-fast-mutex 在 ale@ commit 後,他也順便問是否有變快 (或是變慢)。我回了他一封信,MyISAM 在十個 clients 的模擬測試下有快一些,大約 6%。

不過這是 Super Smack 測試的結果,如果沒有其他比較好的工具,拿來當參考還可以,把結果當作事實就有點偏頗了。我打算複製一份 real data 到其他空機器上,再測試一次看看。

下面這是 Super Smack 測試的結果,最後面的單位是 qps,愈高愈好。

(without fast mutexes)
$ repeat 10 super-smack -D/tmp \
  /usr/local/share/super-smack/select-key.smack \
  10 2000 | grep ...
select_index    40000   0       0       56503.40
select_index    40000   0       0       55638.24
select_index    40000   0       0       55750.29
select_index    40000   0       0       56229.37
select_index    40000   0       0       55770.81
select_index    40000   0       0       56083.44
select_index    40000   0       0       56411.05
select_index    40000   1       0       55156.36
select_index    40000   0       0       56268.21
select_index    40000   0       0       55059.18

(with fast mutexes)
$ repeat 10 super-smack -D/tmp \
  /usr/local/share/super-smack/select-key.smack \
  10 2000 | grep ...
select_index    40000   0       0       59652.25
select_index    40000   0       0       58040.66
select_index    40000   0       0       57937.60
select_index    40000   0       0       58511.95
select_index    40000   0       0       57319.65
select_index    40000   0       0       58308.02
select_index    40000   0       0       58514.35
select_index    40000   0       0       58493.64
select_index    40000   0       0       57937.01
select_index    40000   0       0       59348.24

The first one is 55887 qps (avg), the second one is 58406.3 qps.

MySQL 5.1.26

5.1.26-rc 放出來了:。這個版本會是 5.1 最後一個 RC (Release Candicate),下一個就會是 Production Ready (GA) 的版本了。

不過 Paul McCullagh ( 的開發者) 寫了一篇「」,裡面發現他已經把 PBXT 改到本身已經不是瓶頸,所以反過來開始看 MySQL 的問題...

裡面有很多很有趣的惡搞 (像是他覺得,如果在他的環境裡如果拿掉某些 POSIX mutex lock 不會遇到問題,他就先拔掉 lock 然後看效能可以提昇多少,如果不能拿掉,他就用自己改寫的 spinlock 或是其他方式替代),除此之外,文章裡還有一些數據可以看,可以看出他一步一步改善所走過的路,以及還有哪些地方值得改善。

最後改出來的成果相當輝煌,改到有 60% 的時間會因為在等網路另一邊的 query 進來。

在 comment 有人有提到可以用 --with-fast-mutexes,會把 POSIX mutex lock 換成 spinlock,不知道效果怎麼樣... (我發現 裡的版本沒有支援這個選項,我送了個 PR,用 Ports 的人可以更方便的設定這個選項:)

MySQL 的 ORDER BY RAND() 的替代方案

這篇 所提到 的「」這篇文章裡為了找到 ORDER BY RAND() 的替代方案,花了不少功夫解釋。

(PS:我跟 同屬 的一員,其中本篇文章所提到的 主機目前也放在 PIXNET 的機房裡)

最原始的想法是:

SELECT MAX(id) FROM table;
### 在 application 取一個 1 到 id 中間的值
SELECT * FROM table WHERE id = ...;

但這篇文章要探討的是如何在 裡全部做完,所以重點放在如何在 MySQL 裡取得 randid。

首先是透過 RAND() 幫忙:

SELECT RAND() * MAX(id) FROM random;

然後發現有小數點,所以用 CEIL() 變成:

SELECT CEIL(RAND() * MAX(id)) FROM random;

但這個 SQL query 效率不太好:如果有 1M rows,就會跑了 1M 次。所以利用 subquery 改寫成:

SELECT CEIL(RAND() * (SELECT MAX(id) FROM random));

EXPLAIN 檢查看起來不錯,所以包成 subquery 拉出隨機選出的 row:

SELECT name FROM random WHERE id = (SELECT CEIL(RAND() * (SELECT MAX(id) FROM random)));

結果發現速度不佳,用 EXPLAIN 檢查發現是因為 subquery optimization 被取消。所以改用其他的方法,像是利用 temporily table 與 JOIN

SELECT name FROM random JOIN (SELECT CEIL(RAND() * (SELECT MAX(id) FROM random)) AS id) AS r2 USING(id);

速度沒什麼問題,用 EXPLAIN 檢查看起來也都 ok 了,所以我們要處理 id 不連續的情況,也就是有「洞」的狀態,所以取比這個 randid 大的第一個 row:

SELECT name FROM random AS r1 JOIN (SELECT (RAND() * (SELECT MAX(id) FROM random)) AS id) AS r2 WHERE r1.id >= r2.id ORDER BY r1.id ASC LIMIT 1;

至於更後面為了要做到在 non-uniform distribution 下的 ORDER BY RAND() 效果所花的功夫太大 (像是透過 trigger 產生某個 uniform 欄位,然後就可以用那個欄位用上面的方法處理),一般人應該用不到。需要的人就麻煩自己去看了 :p

MySQL 調整

昨天晚上幫「測試」,發現速度卡在 的 CPU bound,先用 丟在背景跑,再用 抓幾個比較明顯的 slow query,補了幾刀 INDEX 後,速度快了不少,不過還是不太滿意。

印象中 MySQL 除了可以紀錄 slow query 外,還可以紀錄沒用到 INDEX 的 SQL query,花了不少時間才找到。這些指令是可以線上改,不需要重開 (如果你堅持要改設定檔重開也 ok),不過請不要在 production 的機器上開,以免 SQL query 寫的很爛,產生大量的 log:

mysql> SET GLOBAL log_queries_not_using_indexes = 1;
Query OK, 0 rows affected (0.00 sec)
mysql> SET GLOBAL slow_query_log = 1;
Query OK, 0 rows affected (0.00 sec)

參考:

PS:這只是告訴你問題在哪裡,而非解決的方法。要知道為什麼會慢,你需要讀不少資料,像是 這類的書籍,以及網路上 MySQL 資料庫長輩們的討論。

Update:翻到 這篇,可以檢查過度 index 時造成效能降低的問題 :p

MySQL UDF (User-defined function) 與 memcached

以往 + 的作法是由 application 端「拉」資料後再塞到 memcached 上,這會產生幾個問題:

  • 資料不同步:MySQL 上的資料更新了,但 memcached 裡的 cache 因為還沒過期而尚未更新。
  • 第一次的 Race Condition:同時有很多 client 向 memcached 要一份目前還不存在的 cache,這時候這些 client 都會跑到 MySQL 要資料再放一份到 memcached 上。

這兩個問題都有在 MySQL UDF + memcached 出來之前都有解法:前者可以在更新 MySQL 時順便更新 memcached 裡的資料;後者可以靠 memcached lock 的技巧避免突然有大量 Query 造成後端 MySQL 負荷過重。這兩個方法需要 application (client) 配合,不是很完美,但在實際應用上還算堪用。(一個很簡單的場景:如果公司內同時使用 ,那麼就必須維護這兩個 library)

除了 client 自己推資料到 memcached 上,也有人研究,當 MySQL 上的資料更新時,由 MySQL server 自動到 memcached 上更新資料,也就是把資料「推」到 memcached 上的方法。資料更新時的觸發動作可以用 做到,而寫入到 memcached 的部份透過強者的 處理:

btw, 也用 libmemcache 寫過類似的東西: 這篇的下方 (「MySQL and memcached」的部份)