2010年12月31日 星期五

自制彈性的 bash prompt + 解決問題的流程

最近開了幾個 branch 要切來切去, 每次還要下「hg branch」確認狀態, 有點麻煩。就找了一下有沒有人提供啥方案, 這種常見的痛苦, 應該早有人解掉了。結果找到 Prompt Extension, 完全符合我的需求, 官網文件很清楚, 設定也簡單。

滿足需求後想知道它怎麼做的, 關鍵在於如何每次執行 hg 指令就檢查並更新 prompt。只有用 bash 提供的 API 才能正確地更新 prompt, 其它手段應該都是無效的。翻了一下 Prompt Extension 原始碼, 發覺它並沒有 hook 掉 hg 的各個指令。於是我猜是 hg update 本來就會換 prompt, 改看 mercurial 原始碼。看了一陣子覺得不像, 於是回頭交叉比對看看, 是誰改了 prompt。

試了關掉 prompt / 打開 prompt / 不同 hg 指令, 最後發覺....... bash 每執行完一個指令就會重設 prompt 啊!! 從 Prompt Extension 學到的是, 設 PS1 時可以搭配單引號內加函式, 這樣每次更新 PS1 時, 可用函式視情況顯示不同的訊息。之前都用雙引號, 所以只有一次的效果。

換句話說, Prompt Extension 做的事是:
  • 提供設 PS1 的範例。
  • 提供一個 hg 指令來 parse 格式化字串, 藉以顯示不同的 repository 訊息。
總結此次解題的成果:
  • 判斷問題有解: 成功。
  • 背景知識: 知道只有 bash 提供 API 才有機會, 算是成功。
  • 猜測解決的大概方向: 失敗。明明有了正確的知識, 結果卻先猜 Prompt Extension 解的。
  • 第一次修正猜測方向: 失敗。一廂情願地猜到 mercurial 身上, 急著跳下去解, 卻沒先確認這個假設。
  • 回頭驗證假設: 成功。搭配背景知識, 找到正確的原因。
一開始猜的方向錯誤, 繞了一圈才找到做法。幸好我有提早回頭確認一開始的假設, 省了一些冤枉路。愈來愈熟悉「假設 -> 驗證」的樹狀解題流程, 漸漸抓到根本的解題方法, 再來就是增加經驗, 累積背景知識了。這種系統化的解題流程應該是大學時要學會的事, 結果到 26 歲才開竅, 唉呀呀。

2010年12月30日 星期四

mysql 取出最後 insert row 的 id

官網所言, 有設 auto-increment 的情況, 在 insert 完後, 可用「SELECT LAST_INSERT_ID();」取出最後新增的 row 的 id。測試結果不同 mysql client 各自記各自的 last insert id, 不用擔心 race condition。我原本誤以為這個 function 會取出全域的結果, 結果是我多慮了。

這篇提到 Python 可用 cursor.lastrowid 取出, 不過我試了沒效。所以就自己手工多下「SELECT LAST_INSERT_ID();」取資料了。

用 sort 依兩欄數字排序

我是用 Ubuntu 8.04, 附上測的結果

$ cat in
77      1
7       11
7       3
8       2
7       1
$ sort in
7       1
7       11
7       3
77      1
8       2
$ sort -n in
7       1
7       11
7       3
8       2
77      1
$ sort -n -k1,2 in
7       1
7       11
7       3
8       2
77      1
$ sort -n -k 1,2 in
7       1
7       11
7       3
8       2
77      1
$ sort -n -k1 -k2 in
7       1
7       3
7       11
8       2
77      1

結論: 不要寫成 "sort -n -k1,2", 要寫成 "sort -n -k1 -k2", man page 明明是寫前者啊...., 我是亂試試出來的。

2010年12月26日 星期日

用 mysqladmin 觀察細部變化

大致上熟悉 explain、profiling、show processlist 等的用法, 但還不熟用 mysqladmin 觀察更細部的變化。網路上的文件提到用 mysqladmin extended -r -i10 觀察 10 秒內各細部資訊的相對變化, 但項目太多, 很難觀察。

後來想注意到可以用 grep 幫忙過濾。比方說只關心是否用到寫入硬碟的暫存表, 就加個 "grep Created_tmp_disk_tables":
mysqladmin -uUSER -pPASSWORD extended -r -i10 | grep Created_tmp_disk_tables
若不知要觀察啥 (我目前的情況...), 至少可以先去掉沒變化的項目:
mysqladmin -uUSER -pPASSWORD extended -r -i10 | grep -v "| 0   "
還在摸索怎麼用較適當。

登出其它台 Gmail / Google Account

不知道怎麼登出其它台的 Google Account, 不過登出 Gmail 有同等效果
  1. 進 Gmail
  2. 中間下方有個「Last account activity: X hours ago on this computer. Details」, 點 Details
  3. 點「Sign out all other sessions」

2010年12月24日 星期五

Scale out, HA, backup (for mysql)

前一篇經 DK 說明, 發覺我沒有細分需求和方法。混在一起寫變得很亂。

Scale out

目的:
  • 加機器就能應付成長的流量 (ref.)。
作法:
  • Replication 可以應付大量讀取、少量寫入的情況。
  • Sharding (partition) 可以應付大量寫入的情況 (各 server 應付不同段的資料)。

High availability

目的:
  • 網站能持續提供服務。
作法:
  • Master-Slave Replication: 寫入密集的情況下, Master 和 Slave 的資料有時間差,  掛掉 Master 後需要處理資料不同步的情況。
  • MMM 或 DRBD 可以確保資料同步。MMM 不需暖機, 服務中斷時間最短。 DRBD 較易設定, 但需要暖機時間。

Backup

目的: 
  • 可以找回舊資料。
作法:
  • Replication 可以做到「差不多即時」備份, 但若誤砍資料, slave 上的資料也「差不多即時」飛了。
  • 使用 replication 後, 可以在 slave 上固定間隔時間備份資料, 降低對網站效能的影響, 且不用停止服務。
  • 備份方式分為 logical 和 raw backup 兩種類型, 各有優缺點。搭配支援 snapshot 的 file system 用 raw backup, 時間和空間成本都很划算。
  • DB 本身要搭配 crash-safe 的方案 (如 MySQL + InnoDB), 確保 server 掛掉時, 資料有保持一致。
  • 針對 InnoDB 做 logical backup 的話, 用 XtraBackup 較快。
  • 一定要測 restore, 我半信半疑的測了一下, 馬上發現少備份使用者帳號的 DB ......

結論

Scale out、HA、backup 是三種不同的需求, 各自有不同的實踐手段。接下來會先:
  • 做 logical backup: 易於實作, 資料量小時沒什麼缺點。
  • 做 replication: 協助 backup 和為 scale out 讀取做準備。
  • 將 DB 操作包在自己寫的 lib 裡: 之後需要 scale out 時比較好套。
剩下的東西, 待之後有更深的需求再來細讀吧。

參考資料

2010年12月23日 星期四

PNG 教戰守則

DK 那看到的, Does PNG work everywhere?

重點:
  • 需要全彩但不用半透明的話, 用 JPEG
  • 用 PNG8, PNG24 太大了, GIF 也比 PNG8大
  • 用最佳化工具壓 PNG: find . -name '*.png' -print0 | xargs -P4 -0 -n1 pngout
  • 看不懂半透明部份的說明, 需要時再細看吧

2010年12月22日 星期三

Scale out, HA, backup

Scale out、high availability、backup 三者是不同的事, 之前對這幾個詞很陌生, 備忘一下它們的差別。
  • Replication 可以同時 scale out 讀取的操作和當作讀取的 HA, 但不是 backup。舉例來說, 不小心刪錯資料, slave 上的資料也一起飛了。所以 backup 要另外做。
  • Replication 無法保證寫入的 HA, 因為 master 和 slave 之間會因延遲同步而少資料。
  • 針對寫入操作的 HA, DRBD 看起來是最穩且易於實作的 方案。但 DRBD 沒有附帶 scale out, 備份機就是待機狀態。
  • MMM 可用作寫入的 HA, 備份機可充當讀取的 replication。
  • Sharding (partition) 用來 scale out 寫入的操作, 沒包含 HA。
結論是 backup 要分開規劃, scale out 和 HA 也要分開規劃。寫入量不大的話, 可以先用 replication 擋著。反之, 要用 sharding 來 scale out, 用 DRBD 或 MMM 做 HA。

順便記一下 backup 的心得:
  • Backup 分為 raw backup 和 logical backup, 各有利弊。若 file system 有支援 snapshot, 在 slave 上做 raw backup, 不管是執行時間還是占用的空間, 都挺划算的。切記要搭配 crash-safe 的方案, 如 MySQL + InnoDB。
  • 針對 InnoDB 做 logical backup 的話, 用 XtraBackup 較快。
  • 一定要測 restore, 我半信半疑的測了一下, 馬上發現少備份使用者帳號的 DB ......
大概有個概念了, 接下來先做 replication 和 backup, 將 DB 操作包在自己寫的 lib 裡, 待需要 scale out 時比較好套。之後有更深的需求再來細讀吧。

參考資料

2010年12月16日 星期四

varchar 與 text 的效能差異, 以及 order by 的運算方式

今天和同事 S 說明 MEMORY engine 512 bytes 限制時, 被問到用 varchar 超過 512 bytes 的話, 和 text 有何差別。想了一會兒才想到使用 text 會讓 mysql 用比較慢的方式排序, 在這裡小記一下。

在官網 7.3.1.11. ORDER BY Optimization 裡寫得很清楚, 使用 order by 或 group by 時, MySQL 可能會用到 filesort (可用 explain 看 extra 欄位確認)。filesort 有兩種版本:
  • original filesort: 取出符合條件的 rows, 只留 order by 裡的欄位和 row position。排好後再回頭一個個依 row position 取回需要的欄位。換句話說, 會從硬碟讀兩次同樣的資料。
  • modified filesort: 取出符合條件的 rows, 將需要的欄位和 order by 裡的欄位一起排。只會從硬碟讀一次資料。
    舉例來說, 若是 select a from table order by b:
    • original filesort: 先用 b 排序, 再依排好的結果取出各 row 的 a, 所以會有一堆 random access。
    • modified filesort: 排序 (b, a ), 排好後就是最後結果。
      mysql 會盡量用 modified filesort, 以減少重覆讀硬碟 (更何況第二次還是 random access。) 但發生以下其中一個情況, mysql 會選用 original filesort:
      • 用到 text 或 blob 時。
      • 單筆資料 (即上面範例的 (b, a)) 超過 max_length_for_sort_data, 單位為 bytes。
      除了避免使用第一種的 filesort, 說明文件最後有提供幾個最佳化的作法, 可以調一些參數。

      2010-12-16 更新

      original filesort 不見得會比 modified filesort 慢, 今天就踏到雷, 調大 max_length_for_sort_data 讓 mysql 使用 modified filesort 反而慢兩倍多, 原因如文件上所言, 單一 row 變大, buffer 能放 row 的數量變少, 增加排序的次數 (show status like 'Sort_merge_passes')。用 profiling 看則發現 Sorting result 的執行時間變長。

      由於我用來排序的欄位就超過 512 bytes, 一定得將暫存 table 寫到硬碟。順便試了用 ram disk 上的 tmpdir, 結果的確省了一半多的時間, 原以為會會瞬殺的說。

      另外 How fast can you sort data with MySQL ?Impact of the sort buffer size in MySQL 指出 sort_buffer_size 不是愈大愈好, 使用時記得測看看。結論是, 無論如何, benchmark 和 profiling 都是必要的。



      2010年12月11日 星期六

      javascript non-blocking download 的原理

      試了 head.js 的測試頁, 覺得很有意思, 為啥用 head.js 可以快這麼多, 就去看了 head.js 的原始碼, 看起來是 scriptTag 這個函式負責載入檔案。但是我不明白為何這樣可以加快載入速度, 畢竟瀏覽器的連線數量沒變, 為何用同樣的瀏覽器 (我用 Chrome 試的), 速度卻差這麼多。

      在網路上查了一陣子, 看到 jnlin 寫的《Head JS : The only script in your HEAD》, 得知關鍵在於瀏覽器如何處理 tag <script>, 接著就查到這篇: 《What is a non-blocking script?》, 才明白前因後果。

      摘要如下:
      • 只有一個 thread 在執行 javascript。這意味著 UI rendering 和其它 js 操作都是同一個 thread 負責的。 
      • 載入 script tag 時, 瀏覽器怕 script 裡有 js code 會改變 UI, 所以會等 script 載入完並執行完才繼續 rendering。
      • 相對來說, 下載 javascript 檔比執行 javascript 慢得多。若寫程式的人知道這段 script 不會影響之後的 rendering, 有機會將下載的時間切開, 省去下載時間。
      • 做法有兩種: 第一種是動態地增加 script tag, 於是在讀入網頁時, rendering 不會被 script tag 卡住。第二種是用 HTML5 的語法 async, 目前只有 Firefox 3.6 有支援。
      結論是 javascript 是單 thread, 無法平行化執行, 但可以平行下載檔案 (視最大連線數)。並且有機制讓下載檔案不會擋住網頁 rendering, 但程式設計師得自己處理 javascript 相依性 (執行順序)。head.js 提供了不錯的介面解決這問題, 不過也有些小細節要程式設計師自行處理。head.js 的相關說明見附錄, 我有興趣的是為啥它可以加快網頁載入速度 (實際上只有 rendering 完主頁的速度, 不是整體時間)。
      head.js 的說明:
      另外這裡有各家瀏覽器支援網路連線的情況, 滑鼠移到各分欄上會有說明, 可以得知一些細節。HTTP 1.1 規定同一網域最多兩個連線, 結果只有 IE6 遵守, 還被記為缺點......。

      2010年12月10日 星期五

      刪除大量檔案的方法

      當一個目錄內有上百萬、千萬個檔案時, rm -rf 會慢到很糟糕的地步, 而且砍檔案時會吃掉一堆 IO, 做其它事只要一開檔、關檔, 就會頓一下。

      上網查了各種說法, 要驗證何者的說法最合理, 需要讀 rm 的原始碼和 benchmark, 所以就不提了。有人提到一次 rm 多個檔案會比每個檔案都執行一次 rm 快, 減少 fork 和 execute 的時間, 這到是很合理。不確定在要刪除目錄的情況下, rm -rf DIR 和批次砍掉 DIR 下的檔案, 何者較快。

      後來發現以前寫的 《/bin/rm: argument list too long 的解法和原因》 滿有用的, 我可以用 find + xargs 只砍部份檔案, 分多次砍完。這個作法的另一好處是, 可以在沒人用的時候放著讓它砍部份檔案 (像是先砍 a*, 再砍 b*, ...), 方便分階段處理, 只要每次砍檔案的時間不長, 不會和人使用的時間重疊到, 就沒問題了。

      2010年12月9日 星期四

      自動防護惡意 ssh / apache2 連線的方法

      備忘一下, 有空再來試。

      通用軟體
      • ban2fail
      apache2
      • mod_evasive
      • mod_security (Debian / Ubuntu 似乎因 license 的原因, 沒有內建 package)
      ssh
      • sshguard
      • denyhost
      Btw, 一但找到一個相關的軟體名稱後, 用它到 stackoverflow.com 裡搜, 就會找到不少相關軟體。利用比較性質的文章來找相關套件, 似乎是還不錯的作法。

      2010年12月5日 星期日

      atime, noatime 和 relatime

      摘要讀這篇 Difference between noatime and relatime mount options 的心得:

      • atime: 會記錄最後讀取時間, 換句話說, 即使只讀資料, 也會寫入硬碟。
      • noatime: 別記錄讀取時間, 會造成部份程式失常。比方說郵件閱讀軟體得比對最後讀取和更改時間, 若前者比後者小, 表示還有信件未讀。
      • relatime: 只有當最後讀取時間比最後更改時間早的時候, 才記錄最後讀取時間。這解決郵件軟體的需求。
      結論: 用 relatime 安全又可提昇效能。不過我看 Ubuntu 8.04 灌好的時候就已設 relatime 了, 所以沒啥要改的。

      2010年12月2日 星期四

      更改 mysql conf 的注意事項

      mysql 的設定檔有太多讀入的位置, google 一下 "mysql default config path" 會看到一堆文章, 由此可見這問題多令人困擾。

      《High Performance MySQL 2e》提供一個不錯的找法:
      $ /usr/sbin/mysqld --verbose --help | grep -A 1 'Default options'
      
      我在 Ubuntu 8.04 上的輸出結果如下:
      Default options are read from the following files in the given order:
      /etc/mysql/my.cnf ~/.my.cnf /usr/etc/my.cnf
      
      另外要注意 App Armor 是否允許 mysql 讀它的設定檔。我參考書上的建議, 另外開一個 repository, 將系統相關設定檔放到 repository 裡, 再用 soft link 指回去 (例如: ln -s my-repository/mysql /etc/mysql)。結果啟動 mysql 後, 設定檔毫無作用, 也沒看到任何錯誤訊息。

      更改 /etc/apparmor.d/usr.sbin.mysqld 讓 mysql 可以讀設定檔的實體位置後, 就解決這問題了。

      2010年11月30日 星期二

      避免 MySQL 使用 temporary table on disk

      How MySQL Uses Internal Temporary Tables 解釋的滿清楚的, 使用 explain 可以注意 extra 欄位有無「Using temporary」, 有的話 MySQL 會用 temporary table。temporary table 可能是存在記憶體裡的 MEMORY engine, 或是存在硬碟上的 MyISAM engine。執行 SQL 時可以用「show processlist」看 state, 若有出現「Copying to tmp table on disk」就中獎了, 速度會變很慢。

      除不能用 TEXT、BLOB、單一欄位不超過 512 bytes 等注意事項外, 還有兩個參數會影響到是否使用硬碟存 temporary table:
      • max_heap_table_size: in-memory table 的上限 (即 MEMORY engine 的上限)。
      • tmp_table_size: in-memory temporary table 的上限 (即用 MEMORY engine 當 temporary table 的上限)。
      若 temporary table 需要大於 min(max_heap_table_size, tmp_table_size), 就會用硬碟存。

      題外話, 大部份 MySQL 的疑問都能很快地從官網文件找到答案, MySQL 官方文件真不錯啊。《High Performance MySQL 2e》也照順序整理了不少有用資訊, 對照兩者學了不少東西。

      2010年11月27日 星期六

      Web benchmark tool

      提到 web benchmark 大家第一個提到 ab, 超簡單上手, 但只能連同一頁。到 stackoverflow 查了一下, 注意到 JMeter,  Siege Pylot。最多人推 JMeter, 不過看起來有點複雜, 所以先選 Siege 來試。

      Siege 很簡單,  試一下馬上就能上手:
      1. 讀 Siege 官網幾個 link
      2. 用 siege.config 產生 ~/.siegerc
      3. 讀設定檔裡的詳細註解
      4. 寫好 urls.txt 檔
      Siege 可以設定要依序讀 urls.txt 的網址, 還是隨機挑。這樣的簡單設定已可滿足一些常用模式。若想模擬熱門網址較多人連, 大可重覆多放幾次。若需要測使用者的功能, 要在 ~/.siegerc 裡填 login-url, 設定檔裡有範例。Siege 會在進行測試前先連一次 login-url。2.69 版後支援用不同帳號登入。我一開始用 Ubuntu 8.04 包的 Siege 2.66, 結果 login-url 無效, 改用最新的 2.70 就 OK 了。

      注意 Django 1.2 開始有 CSRF middleware, Siege 無法用 POST 的方式登入。我另外寫了一個用 GET 登入的網址, 自己用 auth.login 登入。反正別放到 production server 就好了。

      即使這種測試無法反應真實情況, 有測的話可以抓到一些明顯的錯誤, 像是開太多 WSGI processes, 卻沒提高 MySQL max-connections, 結果負載高時會 MySQL 會發生 "Too many connections" 的錯誤。初步使用上覺得挺不錯的, 接著要規劃一些情境來測試複雜的情況。

      PHP framework 緩慢的可能原因

      先聲明我不熟 PHP, 也很久沒寫 PHP 了, 這篇的解讀可能有誤。

      看了 PHP 作者寫的 Simple is Hard 以及聽 DK 說明, 才終於明白為啥大家說 PHP framework 很慢, 畢竟 scripting language 寫的 framework 滿天飛, 為啥 PHP 的情況比較不同。

      剛從 PHP 換到 Rails / Django 時, 不習慣設好 production server 後就無法一存檔就重讀程式。現在才知道為了這個方便的功能, 付出過高的代價。為了能夠一存檔就執行到新的程式, 有幾種簡單的作法:
      • 每次都重讀程式碼並重編譯。代價是非常慢, 又要讀檔又要編譯程式。使用 opcode cache 可減輕這問題, 確保只編譯一次程式。
      • 每次都要檢查檔案修改時間, 才知道是否需要重編譯程式。
      第二點乍看並不嚴重, benchmark 單頁 PHP 看起來也沒問題, 但使用 framework 時卻不是這麼一回事。即使是寫個 Hello, world 的頁面, framework 仍會載入不少 script, 被載入的 script 會再載入其它 script, 一大串相依性造成的結果, 就是載入一堆檔案。若檔案只會載入一次, 似乎也不是大問題, 但別忘了需要檢查檔案最後修改時間, 結果就是讀個簡單頁面也會有大量的 system call, 而 system call 是很昂貴的。
      相關佐證可以參見 Simple is Hard, 難怪投影片裡不斷強調相依性的結構圖還有執行 stat 相關函式的次數。讓我想到 Joel 寫的 《抽象滲漏法則》, 在框架之上, 很難查覺底層的問題。前陣子也才因 NFS server 負擔過重, 讓我以為是我的 vim 出錯, 開關檔變得超慢的。

      其它可能的原因還有 PHP 的 OO 設計不良, 或是一些早期設計留下的包袱, 常用 PHP 的人應該比較清楚, 我沒仔細研究。

      2010年11月21日 星期日

      South migrate --list 和 db 內資料不符的原因

      常遇到這個問題, 最近找到兩個原因:
      • 有設 PYTHONPATH, 執行到別個 repository 的 mange.py, 用到別組 settings, 拿到另一個 db 的 migration history。在 A 目錄設完 PYTHONPATH 做些事, 再跑去別的目錄做事, 容易發生到這個問題。
      • code 沒有更新到最新版。 South 會以目前 app/migrations 目錄下的 py 檔為準, 若在 A 目錄新增 migration M, 跑完 migration; 接著到 B 目錄執行 "migrate --list", 即使 A、B 的 settings.py 一樣, 結果不會看到剛才新加的 migration M。讀 South 的原始碼後才明白這件事。
      第一個問題無解, 像 PYTHONPATH 這樣的環境變數有其便利之處, 但出錯時卻都很難查覺, 踏到好幾次不同的雷。第二個問題可以提供細心的警告訊息, 說明 migration history 內的資料和目前目錄下的 script 不一致, 算小問題啦。

      網站定位對使用性的影響

      前陣子讀《Don't make me think》提到, 網站設計的最高原則就是 --- 別讓使用者思考如何用, 這意味著網站頁面必須有明確的進入點, 頁面上任何元件都要能自我解讀。所以, 複雜的產品通常不是好主意。

      今天剛好看到YUI3設計中的激進和妥協裡的評論提到:
      前端開發這個行業的產生時間本來就很短,相比傳統的企業級的應用軟件開發是相當的初級的,本質原因是因為web開發的複雜程度遠不如ERP,我在實習的時候參加過一些ERP的開發,光一個訂單管理的小模塊所需要的報表就有成百上千個……我們可以說J2EE艱辛慘淡,但絕不會死,而且會在更加專業和尖端的企業開發中有著更加旺盛的生命力。
      因此框架一定是在業務複雜到一定程度後的必然選擇,這是無可迴避的。 問題是,web產品的使用者不是受過良好培訓的業務員、不是企業管理者、不是操作員和工程師,而是千千萬萬傻乎乎的初等網民,網民水平不提升,我們甚至不敢做出太複雜的產品,那麼……
      讓我有這樣的想法, 公司內部用的網站是個工具, 公司不用擔心使用者看不懂怎麼用而拒絕使用, 公司可以藉由「教育使用者」而大幅減少網站設計的難度和開發時間。另一個相似的例子是, 網站只是協助服務的工具, 使用者不會因網站難用而拒用這個服務。比方說年代售票難用到爆炸, 但若只有這個購票方式時, 也只好繼續使用。而年代售票唯一要確定的事, 是線上刷卡這段有做好, 不能算錯帳。只要這點沒出包, 那怕大刺刺地寫著「請等個數十秒」的訊息, 使用者也得乖乖等待。

      但若網站本身就是核心服務, 像是相簿、Blog、書籤、搜尋、入口網站這類服務, 就要仔細分析和改進使用性。這算是從另一個角度來解讀網站使用性的重要程度吧, 世上沒有絕對重要的事, 什麼事都要看情況決定。

      2010年11月16日 星期二

      Python 處理時間的方法

      基於效率考量, 我決定在 mysql 中用 unix timestamp 的方式表示時間。另一方面, 加減時間也比較直覺, 都是秒數。這裡簡記 timestamp 和字串互轉的方式。

      timestamp 轉為字串

      In [1]: import time
      In [2]: import datetime
      In [3]: t = time.time()
      In [4]: # 透過 datetime
      In [5]: datetime.datetime.fromtimestamp(t).strftime('%Y-%m-%d %H:%M:%S')
      Out[5]: '2010-11-16 20:10:58'
      In [6]: # 透過 time tuple
      In [7]: time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(t))
      Out[7]: '2010-11-16 20:10:58'
      

      字串轉為 timestamp

      In [8]: # 透過 datetime
      In [9]: d = datetime.datetime.strptime('2010-11-16 20:10:58', '%Y-%m-%d %H:%M:%S')
      In [10]: time.mktime(d.timetuple()) + 1e-6 * d.microsecond
      Out[10]: 1289909458.0
      In [11]: # 透過 time tuple
      In [12]: time.mktime(time.strptime('2010-11-16 20:10:58', '%Y-%m-%d %H:%M:%S'))
      Out[12]: 1289909458.0
      

      參考資料

      備註

      2015/01/05 更新

      2015/08/28 更新

      Parse email 時間欄位的方法:

      >>> from email.utils import parsedate_tz, mktime_tz, formatdate
      >>> import time
      >>> date = 'Tue, 28 Aug 2012 02:49:13 -0500'
      >>> tt = parsedate_tz(date)
      >>> timestamp = mktime_tz(tt)
      >>> print formatdate(timestamp)
      Tue, 28 Aug 2012 07:49:13 -0000
      

      或試看看萬能版 parser, 不需指定時間格式: python-dateutil

      2010年11月15日 星期一

      MyISAM 的 data cache 和 covering index

      最近下 SQL 都有加 sql_no_cache, 今天測一個會取出 5,000 ~ 10,000 筆資料的 SQL, 觀察到一個有趣的事:
      • 第一次執行時, Sending data 花了 2.457162s, 其它狀態沒花到什麼時間。
      • 第二次執行時, Sending data 卻只花了 0.154124s
      才想到說, 這該不會就是 OS 的檔案 cache 吧? 不知道 cache 了什麼, 大幅減低讀硬碟的時間。真相如何, 只有待回頭惡補 OS 相關知識, 或深入研究 Linux 底層運作方式時才會明白吧。
      但只不過個幾千筆資料, 沒道理要花到 0.15 秒, 於是將 WHERE 條件內的欄位和 SELECT 後要取出的欄位都加進 index, 也就是用 covering index, 結果 Sending data 只花了  0.011197s。

      解決一個瓶頸後, 立即觀察到下一個瓶頸, 剛好也能用 covering index 解決。建好 covering index, 再用 load index 預先載入 index 到記憶體裡, 簡單的組合技可以提昇不少速度。相較於 MEMORY ENGINE (+heap index), 這個作法支援 range query, 又不用擔心 MySQL server 重開後要重建資料, 用起來滿方便的。

      備註: 用 explain 看 SQL, 在 extra 的地方有出現「using index」就表示成功地用了 covering index。

      2010年11月14日 星期日

      手動更新 MyISAM 檔案的注意事項

      MyISAM 的好處之一是能跨 OS 直接執行, 只要複製檔案即可 (InnoDB 不能這麼做)。配合 "rsync -av --delete db1/ db2/", 就能從檔案系統的層級快速同步 database db1 到 db2, 方便開發。但複製檔案後沒有重開 MySQL 的話, 有可能會有以下問題:
      • 更新資料後可能會因 query cache 而取到舊資料, 執行 "reset query cache" 可解決這問題。保險起見, 用  "flush tables with read lock; unlock tables;"  更穩。
      • 可能會找不到新加的欄位, 原因可能是 INFORMATION_SCHEMA 沒有更新, 文件第一頁就說這個 database 是唯讀的, 不能更新。可以用 "flush tables with read lock; unlock tables;" 可解決 (強迫關掉所有 table, 下次用時就會重讀硬碟了)。
      另一種可怕的錯誤是, 不小心在 mysql 目錄下放了不對的檔案, 比方說 rsync 時不小心少打 "/", 變成在 database 目錄下放另一個目錄, 而不是放入 MySQL 的檔案 (如 *.frm, *.MYD, *.MYI)。這種情況下做任何 SQL 都會有 warning, 有些 framework / lib 會在發現 warning 時直接丟 exception 出來, 結果就是其它程式跑出不知所云的錯誤訊息, 很難除錯。

      2010-11-16 更新

      今天中另一個雷, MEMORY engine 在硬碟上只有 frm 檔用來存 schema, 資料和 index 都在記憶體裡, 別傻傻的 rsync db1 到 db2 後, 就以為兩個資料庫內容一模一樣啊。

      2010-11-22 更新

      《High performance MySQL 2e》p146 "Speeding up alter table" 提到不少非正規更新檔案的方法, 可以學到一些小技巧, 對於自己開發用的資料庫來說, 頗實用的。

      mysql profile 的相關指令

      簡記一些相關心得
      • 用 SQL_NO_CACHE 強迫 MySQL 別從 query cache 取資料, 方便 profiling 或 benchmark。
      • SHOW PROFILES 說明用 profile 的方法。注意時間花在那個階段, 才明白是 I/O bound 或 CPU bound, 或其它預料之外的情況。
      • 用 show profile all 可看到各階段詳細資料, The INFORMATION_SCHEMA PROFILING Table 說明詳細資料的各欄位的意思。

      2010年11月9日 星期二

      mysql select 花了過多時間在 statistics 階段

      今天遇到的神祕現象, Statistics bottleneck on large table 這篇講得超清楚的, 包含問題和他試過的各種無效方案。我的情境和他差不多, 單純地從一個 table 用 primary key 取出一筆資料, 結果花了 0.02s, 用 profiling 看才發覺都花在 statistics 上, 但 analyze table、用 force index 等方法都無效, 實在是太詭異了。

      最後是 drop 掉某個暫時沒用到的 index, 結果就好了。剛好我有好幾個之前實驗用的 index 和兩組一模一樣的 database, 在另一個 database 的同一 table 裡 drop 掉另一個 index, 結果也修正這個問題。無法理解啊。

      Btw, 上網查沒看到 statistics 的詳細說明, 印象中 High Performance MySQL 2e 好像有提到過, 不過翻書沒查到什麼東西。印象中說是依 static statistics 和 dynamic statistics 決定如何執行 query。反到在官網 SHOW PROFILES 裡看到, profiling 是企業版沒有的功能。雖說沒有仔細解釋各 state 的含意, 沒有 profiling, 企業版怎麼抓瓶頸啊? 還是有更好的替代工具?

      2010年11月8日 星期一

      2010 OWASP Top 10 網站潛在漏洞摘要

      這類知識看過沒持續用, 沒多久就會忘光, 來寫個簡單摘要協助記憶。官網有完整的說明和例子, 這裡簡單地用我自己的方式說一次:

      1. Injection: 攻擊者利用 GET / POST 在參數裡塞入部份程式, 取得不被允許的資料。如常見的 SQL Injection。解法就是別相信使用者傳進來的東西, 限制傳入東西的型別和可能的文字模式。
      2. XSS: 誤執行攻擊者傳來的程式。比方說 textarea 就容易混入這類程式。別傻傻的直接引入使用者傳上來的 html, 最好只充許純文字, 或限制 html tag 的功能。
      3. Broken authentication and session management: 如網站傻傻的將 session id 放到 URL 上, 讓使用者有機會不小心外洩 session id。
      4. Insecure direct object reference: 沒有在存取資料時再多做權限檢查。攻擊者在登入後, 可以傳特定參數取得他不能取得的資料。比方說 alice 登入後在需要傳 user id 的地方改傳 bob 卻能取得 bob 的資料。
      5. CSRF: B 網站偷塞會連向 A 網站更新資料的操作 (如塞在 img src 裡), 若使用者登入 A 網站後沒登出, 接著連到 B 網站就中招了。解法是 A 網站要確保任何會更新資料的操作, 在產生輸入的頁面時先塞一段密文, 在更新資料時檢查是否有這段密文, 確保使用者真的從 A 網站送出請求。
      6. Security misconfiguration: 網站留一堆後門給攻擊者用, 像是可以由外連上管理者介面 (如 phpMyAdmin), 或是可以瀏覽網站目錄, 下載 source code 之類的。還有用別人的 framework / lib, 明明說有 security patch 卻不更新。
      7. Insecure cryptographic storage: 加密的 key 和加密的資料存在一起, 有防和沒防一樣。password 檔沒加 salt (之前寫過的文章: 為啥要用 salt)。
      8. Failure to restrict URL access: 沒有在載入頁面時檢查權限。比方說只有查使用者是否有登入, 並隱藏使用者沒權限執行的頁面。一但攻擊者知道那些 URL, 就能登入後直接連上去使用。
      9. Insufficient Transport Layer Protection: 輸入密碼或重要資料 (如卡號) 時沒透過加密連線, 攻擊者可透過監聽網路封包的方式取得這些資料。或是一般使用的情況取得 session id。另外有用 SSL 但沒設好 certificate, 造成惡意攻擊網站有機會偽裝成自家網站, 騙取輸入資料。
      10. Unvalidated Redirects and Forwards: 隨便在參數提供 redirect 不小心被當作攻擊的打手, 使用者沒注意看就點了別人貼 A 站的網址, 卻被 redirect 到 B 站去。或是自己的站被用來導到其它頁面, 卻沒有在進入頁面時檢查權限, 於是 redirect 被當作任意門使用, 能進入不被允許的頁面。
      上面的問題的防範方式有幾個共通點:

      • 使用者都是來亂的 (咦?), 別相信他們輸入的資料。
      • 所有操作都要在最後一層確實把關, 前面把關都只是做表面工夫, 真正要避免問題, 要在最後一關守好。
      • 別圖個方便亂開洞給別人攻擊。

      Btw, 好的 web framework 會幫忙處理一些事, 像 Django 1.2 就強迫使用防範 CSRF 的機制, 除非使用者自己耍蠢硬用 GET 更新資料, 才會中招。使用者密碼也有用 salt, 減少開發者犯錯的機會。

      2010年11月7日 星期日

      Nagle's Algorithm 和 Delayed ACK 的問題

      測 httplib2 POST 的時候發現 httplib2 實作造成的問題 (Issue 91), 就順著討論往下查原因, 還滿有趣的。

      The trouble with the Nagle algorithm 簡單的解釋 Nagle's Algorithm 和 Delayed ACK 的目的和作法, 這篇文末兩段 "Delayed ACK" 和 "Nagle's Algorithm" 有更詳細的解釋。看懂後再回頭看 httplib2 Issue 91 的逐步說明, 終於明白為啥 Nagle's Algorithm 的作者說 write-write-read 會造成問題, 而 httplib2 剛好在 POST 的情況下就是 write-write-read (送 head, 送 body, 讀 response)。

      在這裡整理一下讀到的重點:

      TCP 的 ACK

      用 TCP 傳資料時, 收到任何封包後都會回傳一個 ACK, 表示有收到該封包 (會有個流水號對應是收到那個封包)。

      Delayed ACK

      若收到資料的一端會馬上會回傳資料, 那就不用急著回傳 ACK, 可以等要回傳資料時, 再一起送回 ACK, 藉此少送一個封包。像 ssh 連線時, 每收到 client 端送來的按鍵, 都會將螢幕上的變化送回去。

      但 TCP 不會知道 application layer 會不會立即回傳資料, 所以它只能先猜「會立即回傳」而先不回傳 ACK, 等個一陣子 (500ms?) 都沒有回傳資料的話, 再回傳 ACK。除此之外, 還有個例外規則, 一但收到第二個封包, 立即回傳這兩個封包的 ACK。

      Nagle's Algorithm

      收到送資料的請求時, 不會立即送出資料, 而是等下列兩個條件之一發生時, 才送出資料:
      • 送出的資料可以塞滿一個封包 (避免送出太多小封包, 浪費頻寬)。
      • 收到 ACK (表示之前的資料已成功送達, 此時不送也是讓頻寬空著)。

      兩者的衝突

      假設要送出的封包都不大。Nagle's Algorithm 會等到收到 ACK 後, 才會送出下一個封包。
      • 若操作是 write-read-write-read, 不會有問題, 因為對方的 Delayed ACK 猜中了, ACK 成功地搭回傳資料的便車送回來。
      • 但若是 write-write-read, 送完第一個封包後, 不會送出第二個封包, 因為沒有滿足 Nagle's Algorithm 的兩個條件。對方等個一段時間才會送出 ACK, 於是造成不必要的時間負擔。
      如 Issue 91 第二則留言說的, 理想的解法是將 head 和 body 合在一個封包送出。關掉 client 端的 Nagle's Algorithm 可以避開這問題, 但卻不會避免送出一堆小封包。若 client 端不小心寫成送出一堆小封包, 會降低效率。

      備註

      如 Issue 91 作者所言, httplib2 只有在重用同一 connection 時才會有 write-write-read 卡住的問題。不知為何每次都用新的 connection 的話, 就沒有這個問題。得實際追踪送出封包和回 ACK 的時機, 才能明白為何用新的 connection 沒這問題。也許 Delayed ACK 或 Nagle's Algorithm 的運作方式不如上面所言那般單純。

      2010年11月6日 星期六

      Http connection 相關基本知識

      HttpKeepAlive 提了些有用的知識:
      • 在 Http 1.0 裡, server 和 client 都需要明確送出 "Connection: Keep-Alive" 才會有 keep-alive。
      • Http 1.1 預設使用 keep-alive, 不需另外送 "Connection: Keep-Alive"。若要取消 keep-alive, 要明確送出 "Connection: close"。
      • 即使有 keep-alive, Http 仍是 stateless protocol。keep-alive 可以擔保的只有減少重建 connection 的負擔。
      Python 的 httplib2 不需設定直接支援 keep-alive。trace code 後看到 Http 物件有個 self.connections: key 是 scheme (HTTP 或 HTTPS) 和 HOST、PORT, value 是 connection 物件。

      用 netstat 可明確看出是否有重覆使用 connection。在有 keep-alive 的情況下, 開關 connection 連個 1000 次後, 若 "netstat | grep PORT | wc -l" 顯示 1000, 表示沒有重用到 connection; 反之, 顯示 1 表示有重覆使用。不確定在沒有 keep-alive 的情況下, 是不是也會顯示 1。或是直接到 httplib2、httplib 裡, 在 connect 的函式裡塞 log, 更能確保是否只有連線一次。

      用 POST 的情況下, 新版 httplib2 比 0.6.0 快很多

      httplib2 剛好在 0.6.0 後沒幾版, 修正了 Issue 91: Slow performance with multiple requests over keep-alive HTTP connection。用同一 connection 持續用 POST 傳資料的情況下, 速度有大幅差距。從 patch 提供的簡單測試 script 可看出快了 100 倍 (應該說, 原本慢得不合理)。留言裡有提到這個解法仍有缺點, 我不懂 networking, 沒仔細研究留言提到的問題的影響層面, 在我的環境下測試有 40 倍的差距。

      另外詭異的一點是, 我用 POST 竟然比用 GET 快, 再來看看自己那裡寫錯了吧。還是同樣的 "get" 操作, GET 不見得比 POST 快啊?

      2010年10月25日 星期一

      lsof: list open files

      剛才要重裝 nose 卻一直跑出這樣的錯誤訊息:
      OSError: [Errno 16] Device or resource busy: '/some-path/lib/python2.5/site-packages/nose/plugins/.nfs0000000001d8a00f000005c0'

      查到這篇說是 NFS 的安全措施, 可以用 lsof 檢查那些 process 開啟目標檔案。用 lsof 找出相關 process, 將它們清光後就 OK 啦。

      2010年10月23日 星期六

      avi 轉 flv和播放 flv 的方法

      經同事 R 和同事 M 指點後, 順利地做好這件事, 備忘一下。

      名詞解釋

      • flv: flash video 格式, 比 avi 小很多。網路上的影片多為 flv 格式。
      • swf: flash 執行檔? 能播放動畫或執行程式。

      avi 轉 flv

      我不清楚別人交給我的 avi 檔是什麼格式 (印象中 MS 的 avi 有不同種類?), 我試了三四個軟體, 最後用 SUPER © 轉成功了。不過這軟體超難下載的, 而且參數太多令人無所適從, 幸好 《好用的影音轉檔軟體--SUPER ©》 解釋的相當清楚。照著網站步驟下載軟體, 照文章內捷圖選參數, 看起來還算 OK。

      播放 flv

      要用網頁載入 swf, 再透過 swf 播放 flv。依 Wikipedia 所言, 99% 電腦能執行 swf 檔, 應該不用擔心不能播 swf 的問題。

      FLV Player 官網下載 FLVPlayer.swf, 再依官網提供的工具產生對應的 html code。下面是一個簡化的範例:
      <object type="application/x-shockwave-flash" width="400" height="350"
              wmode="transparent" data="FLVPlayer.swf?file=X.flv&autoStart=true">
          <param name="movie" value="FLVPlayer.swf?file=X.flv&autoStart=true" />
          <param name="wmode" value="transparent" />
      </object>
      

      將上文的 X 改成你放 flv 檔的位置, 還有確定 FLVPlayer.swf 有和網頁放到同樣位置即可。

      2010年10月19日 星期二

      小心瓶頸就在資安軟體

      這是一個很囧的心得。

      今天下午發覺有個簡單的操作竟然有時候要花一秒鐘, 做了許多交叉比對, 發覺以下驚人的跡象:
      • 同一份程式、同一份資料庫, 連用 apache 跑的服務時, 有時要花一秒; 但連 django development server 時卻是瞬殺。
      • log 顯示, 不管連那種 web server, 我的程式都只花不到 0.05s。
      • 用 Chrome Developer Tool 的 Resource 觀看, 慢的時候, 大部份時間是 waiting。
      • 用 VNC 連到 server, 再從本機連 apache, 也是瞬殺。
      • 從我的電腦或同事的電腦連 apache 都是有時會延遲, 我的情況比較頻繁。
      • 同樣的連結點第二次時會變快, 但過一陣子再點似乎又會變慢。
      • 我主要是測 AJAX + GET。
      綜合以上線索, 我下了一個很腦殘的推論: apache 有鬼!! 不知是 apache 讀檔案還是做些奇怪的事, 或是 mod_wsgi 做奇怪的事, 造成延遲。也許 apache 有奇妙的 cache, 造成同一連結點第二次時會變快。
      和來和同事 R 討論後, 他問了一個問題: 「該不會是是資安軟體阻擋吧?」 (*1) 於是同事 P 在 server 隨便開個奇怪的 port, 將該 port 的封包轉到本機的 port 80。我再試的結果, 速度就變正常了!!
      事後回顧發覺自己的推論太腦殘了, apache + 80 和 django development server + some port, 我卻忽略了 port 的事, 只看到 web server 的差異。差點要去找 apache 或 mod_wsgi profiling 的方法, 幸好有先和同事閒聊, 省了一堆工, 也放下心裡一塊大石頭。

      註 *1: 資安軟體會攔截對外連的 URL, 阻止連某些網站。沒想到連對內也有攔截.....。

      2010年10月18日 星期一

      profile Django 的方法 + 關掉 I18N 減少 template rendering 的時間

      今天發現 template rendering 吃掉不少時間, views 和 db 等操作還可以自己寫個簡單的 profile function 記錄, template 就不方便啦。查了一下, 看到 "Speeding up built in Django Templates" 提到, 可以參考 "Profiling Django" 的說明 profile 全部東西, 該篇有提到用 django-command-extensions 的 runprofileserver 來 profile Django development server, 試了一下, 真不錯用, 之後再來試試 WSGI 的部份。

      profile 的結果, 和作者差不多, 卡在 I18N 做了一堆工, 我不確定 stringformat 是否有效, 至少我胡亂在很多地方加上去沒啥效果, 後來直接在 settings.py 裡將 I18N 設為 False, template rendering 的時間就從 0.4s 降到 0.25s。不過 django/utils/importlib.py:18(import_module)、django/utils/translation/__init__.py:23(delayed_loader)、django/utils/formats.py:10(get_format_modules) 還是占了一堆時間。程式碼看起來像只會執行一次的東西, Django runserver 大概是每次都重讀全部 modules 才會吃這麼多時間吧, 之後再來看是否用 WSGI 時也會如此。

      Ubuntu 安裝 mtop 的問題

      裝一些相關 package 時, 會炸在安裝 mtop 的部份, 錯誤訊息如下:
      Setting up mtop (0.6.6-1.1) ...
      ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO)
      ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO)
      dpkg: error processing mtop (--configure):
       subprocess post-installation script returned error exit status 1
      Errors were encountered while processing:
       mtop
      

      查了一下才發覺真算是 bug (我用 8.04, 不過其它版也有這問題), 有一個很爛的解法, 就是先將 mysql root 密碼設成空字串, 裝完再設回來......。做法如連結裡寫的, 用有權限的帳號進 mysql client 後, 打:
      SET PASSWORD FOR root@localhost=PASSWORD('');
      再執行 "aptitude install mtop", 就搞定啦。裝完記得馬上設回 mysql root password。

      2010年10月17日 星期日

      Ubuntu 上 mysql 參考設定檔的位置

      apt-file 真是不錯用, 用它找到 my-huge.cnf 的位置:
      $ apt-file search my-huge.cnf
      mysql-server-5.0: /usr/share/doc/mysql-server-5.0/examples/my-huge.cnf.gz
      
      mysql 有提供幾組預設設定檔, 以現今電腦的設備, my-huge.cnf 假設的規格都算小吧, 更別提系統裝好的預設值。

      2010年10月12日 星期二

      讓 Firefox 在同一網站同時登錄多個帳號

      目前我有兩個情況會有這種需求:
      • 同一網站, 要測不同使用者互動。
      • 同一個 domain, 同時在不同 port 跑不同版網頁, 需要同時登錄比較差異, 但 cookie 是以 URL 不含 port 當 key, 所以無法同時登錄不同 port 的網頁。
      平時我用一個 FF、一個 Chrome 外加一個 Chrome 無痕式視窗搞定, 但最近 javascript 有 bug, 在修好它以前, 相當於廢掉兩個登入額度。只好回頭看看怎麼用 FF 同時登入。
      眾人指點後, 查了一下資料, 找到解法:
      1. 關掉 firefox, 改用「開始」-> 「執行」, 打入 "firefox -P" 啟動 profile manager
      2. 建立另外一個 profile "dev", 關掉 profile manager
      3. 執行指令 "firefox -P default -no-remote"
      4. 執行指令 "firefox -P dev -no-remote" , 這樣就同時有兩個 FF 互不干擾
      新弄的 dev 就和剛裝好的 FF 一樣, 設定都是預設值, plugin 是空的。可以用 FEBE 輕鬆重裝原有的 plugin, 或是用 MozBackup 「還原」 profile, 將原有設定複製到新 profile。我還沒試過就是了, 我改的東西很少, 在新的 profile 裡重裝 FireBug 方便開發就夠了。其它的用原有 profile 執行的 FF 即可。

      2010年10月11日 星期一

      簡單的 mock 使用情境

      今天在寫新功能時, 又陷入小小的苦惱。我要寫一個小函式, 這個函式會依參數從資料庫取出資料, 並依參數決定做那些操作, 再傳回操作完的結果。函式本身需求明確不複雜, 但有不少組合情況要處理好, 有 unit test 比較保險。

      在開始 TDD 前, 我得先決定如何準備 fixture (輸入資料)。大概有兩種選擇:
      1. 規劃好目標函式的測試情境, 準備測試用的資料庫, 塞對應的資料進去。
      2. 使用 Mock (我用 pymox)
      配合 Django TestCase 和 ORM 操作, 塞入測試資料已簡單許多, 不用自己產生一個同 schema 空的資料庫, 並在每個 test case 前先清空資料, 也不用自己下 SQL 填資料, 所以實作門檻已低了不少。但是仍有幾個問題:
      1. 當測試資料有 relation 時, 要多塞不少相關資料。
      2. 要規劃清楚那些案例需要那些資料。規劃完後, 也需要寫註解畫個表格之類的, 不然很難了解測試資料內容。即使畫了表格, 也不太直覺易懂。
      3. 日後發現新 bug 或改功能時, 不容易修改這堆測試資料。
      最讓我受挫的是, 寫完後很難閱讀和維護, 測試案例一多, 根本沒興趣慢慢讀輸入資料的細節。其它像是和外部資源綁在一起、實作目標函式時得先寫相依函式等議題, 在這個例子裡到是沒那麼嚴重。
      這回我改試著用 Mock, 將目標函式的介面寫成大概如下的形式:
      def target_function(arg1, arg2, getter=db.some_function):
          ...
      
      db 是我另開的模組, 用來放包著 SQL 操作的函式。getter 指向真正用的函式, 測試時則視測試案例傳不同的 mock objects。
      使用 Mock 的話, 準備測試資料變得很容易, 而且直接寫明目標函式如何和這些資料互動, 不用看資料庫的資料想像它轉換後的資料。要加測試案例時簡單許多, 不用思考如何在不同表格組出前置資料, 再用這些資料經由資料庫操作轉成中間資料給目標函式用。整體評估下來, 為了測試而多加參數 getter 是很划算的設計, 在註解裡註明一下 getter 的用意即可。

      用 Mock 帶來的問題則是:
      1. 視使用的方式, 對目標函式的實作細節需有不同深度的理解。像是 getter 被呼叫幾次, 傳入那些參數。若程式有部份是別人寫的, 需要另外時間讀原本已被隱藏的細節。 
      2. 目標函式改變使用 getter 的方式時, 也需更新準備 mock 的測試程式。
      3. 無法保證目標函式正確, 實際使用目標函式時會有正確成果, 因為 getter 有可能出錯。導致有可能要另測 getter。相對來說, 直接存取資料庫的例子, 不測 getter 的風險較小。
      換個角度來看, 測試程式使用 Mock 相當於切開和資料庫的連繫, 卻增加和 getter 操作的連繫。

      2010年10月8日 星期五

      django template tag 傳參數 + 使用 template 的方法

      之前眼殘一直沒看到, 還埋怨 Django template 不人性。上 news groups 查了一下相關問題, 發覺都沒有人有這種困擾, 又回頭看文件, 才發現自己眼殘。

      template tag 有兩種模式, 一種是先 parse, 產生 node, 再 render 回傳字串。雖然語法較彈性, 用起來有點複雜, 而且在 python code 裡產生 html 字串, 而不是在 template 裡嵌入變數, 較不容易維護。 另一種用法是 inclusions tag, 就和用 template 的 include 差不多, 不過變數的來源有兩種。

      一種是用傳參數的, 這正是我需要的, 抽出重覆的 template codes, 傳變數用在不同地方。另一種是不接收參數, 直接用 context 當全域變數取出 template 內的東西。但這樣就無法在一個 template 裡使用多次。

      2010年10月2日 星期六

      MySQL varchar 長度和效能問題

      看《High Performance MySQL 2e》 p242 才發現, 可怕的效能問題。varchar 在 disk 上只存必要的空間, 但丟到記憶體後卻當作 char 來存。MySQL 內部處理字串操作時, 是用固定大小的 buffer, 所以 utf-8 會強迫占用 3 bytes, char(10) 會占用 30 bytes。雖然 varchar 在硬碟上只存必要的空間, 但在記憶體裡卻會被當作 char 看待。原文如下:

      MySQL uses fixed-size buffers internally for many string operations, so it must allocate enough space to accommodate the maximum possible lenght. For example, a CHAR(10) encoded with UTF-8 requires 30 bytes to store, even if the actual string contains no so-called wide characters. Variable-length fields (VARCHAR, TEXT) do not suffer from this on disk, but in-memory temporary tables used for processing and sorting queries will always allocate the maximum length needed.

      MEMORY engine 用 varchar 等同於 char, MySQL 暫存表格似乎是用 MEMORY engine, 無法用 MEMORY engine 時會改用 MyISAM 存在硬碟上。若有某個 column 或 select 結果用超過 512 bytes, 該 table 或 select 結果不能存在 memory 裡。

      綜合以上所言, 結果就是 varchar + utf-8 長度設超過 170 的話, 就無法存在 memory 裡。別以為用 varchar 就可以亂設最長長度啊。

      另外, 即使 varchar 可以設長度到 65536, index 時只能取前 1000 bytes (不是 chars), varchar 過長時, 建 index 會出現錯誤, 而要求明確指示 index length。若自己建 index 到也還好, 但用 Django 還是 South 時會噴掉, 沒辦法自動產生正確的 SQL 建過長 varchar 的 index。

      補充一下不能存在 memory 裡有多糟, 這表示 SQL 有用到 order by 或 group by 時, MySQL 得不斷用硬碟暫存結果。以 order by 為例, 假設有 column A, B, 其中 A 能放在 memory, B 不行。即使用 column A 排序, 排完後還得一筆筆依排序結果的順序從硬碟取出 column B 的資料, 那會是非常的慢。詳細的說明見《ORDER BY Optimization》裡 "The original filesort" 和  "The modified filesort" 的說明。

      2010年9月30日 星期四

      用 Ubuntu 將 mysql database 搬到 ramdisk 上

      改自同事 P 的筆記:
      $ sudo -u mysql cp -rp /var/lib/mysql/DATABASE /var/lib/mysql/DATABASE_tmp  # backup the original one.
      $ sudo -u mysql mv /var/lib/mysql/DATABASE /dev/shm/  # move DATABASE to shared memory.
      $ sudo -u mysql ln -s /dev/shm/DATABASE /var/lib/mysql/DATABASE
      $ sudo vim /etc/apparmor.d/usr.sbin.mysqld  # Add /dev/shm/DATABASE to the allow list
      $ sudo /etc/init.d/apparmor restart 
      

      關鍵在於 apparmor 的部份, 沒做的話, 在存取 DATABASE 時, 會出現 "Can't read ... (errno: 13)" 的錯誤訊息。

      不過試了幾次將資料庫丟到 ramdisk 上, 好像都沒有變快過。如果要 scan 整個表格的話, 在 ramdisk 上似乎也沒變快。

      網路上有人提到有 MEMORY engine 為啥會想放到 ramdisk, 每個人有不同的理由, 我的動機則是表格有欄位超過 512 bytes 無法用 MEMORY engine

      2010年9月27日 星期一

      Ubuntu 限制 process 的記憶體用量

      http://stackoverflow.com/http://serverfault.com/ 找半天, 只看到大家說跑程式前記得先用 ulimit, 卻沒看到要怎麼設定使用者的預設值。最後只好在 /etc/profile 裡加上:
      ulimit -v 10000000  # At most 10G
      

      用 help ulimit 可看到說明, ulimit -a 可以看到目前的限制和單位。

      2010年9月25日 星期六

      寫 alias 切換路徑到 virtualenv 相關目錄

      用 virtualenv 時常會需要切入 site-packages 或 src 內做些事, 久了就覺得很麻煩。雖然 virtualenvwrapper 有提供相關指令, 但又不想為了這件事而多裝 virtualenvwrapper, 它的其它功能對我來說幫助不大, 加上我自己有個 script, 用來安裝開發用的 virtualenv, 懶得整合那個 script 和 virtualenvwrapper 的路徑 (也就是指定裝到 ~/.virtualenvs), 所以決定自己寫切換路徑的 alias。

      Python 沒提供堪用的單行寫法, 但為此回頭學 awk, sed 也太麻煩了。想想還是回頭寫 Ruby 吧, 相較於 Perl, 至少較熟一些 Ruby 。

      alias cdve='cd $(echo $PATH | ruby -ne '"'"'puts split(":")[0].split("/")[0..-2].join("/")'"'"')'
      alias cdvesrc='cd $(echo $PATH | ruby -ne '"'"'puts split(":")[0].split("/")[0..-2].join("/")'"'"')/src'
      alias cdvesite='cd $(echo $PATH | ruby -ne '"'"'puts split(":")[0].split("/")[0..-2].join("/")'"'"')/lib/python2.5/site-packages/'
      

      如上所示, 進入 virtualenv 後, 打 cdve / cdvesrc / cdvesite 會切到 virtualen 的根目錄、src、site-packages。

      其中最難搞的是在單引號內用單引號, 參考 《BASH, escaping single-quotes inside of single-quoted strings》, 用 "'" 來表示單引號, 總算是搞定了。關鍵在於用到單引號時就換成 "'", 但字串間不能有空白, 結果就變成超難懂的 bash code 了。

      django 自行新增使用者的作法以及為何密碼檔裡要加 salt

      寫測試程式時需要先建個測試帳號, 為了確保任何人取得原始碼後能直接跑成功測試, 我在測試碼執行時檢查是否已建立測試帳號, 若沒有則自己建一個。

      單純地新增 User 放入 username 和 password 是行不通的, 順著命令 createsuperuser 看原始碼, 發覺正確的寫法如下:
      try:
          user = models.User.objects.get(username='test')
      except models.User.DoesNotExist, e:
          print 'Test account doest not exist. Now create it.'
          user = models.User(username='test', is_active=True)
          user.set_password('testtest')
          user.save()
      

      好奇之下看了一下 set_password 怎麼寫的 (./contrib/auth/models.py):
      def set_password(self, raw_password):
          import random
          algo = 'sha1'
          salt = get_hexdigest(algo, str(random.random()), str(random.random()))[:5]
          hsh = get_hexdigest(algo, salt, raw_password)
          self.password = '%s$%s$%s' % (algo, salt, hsh)
      

      可以看到 Django Auth 用 sha1 和 salt 產生加密後的字串。於是查了一下 salt 的功效, 這回才真的明白它的用處。

      Wikipedia 上舉了幾個情境:
      • 在沒用 salt, 且用單字當密碼的情況, 一但密文被取得後, 攻擊者只要查事先建好的表即可馬上找出原始密碼。表的 key 是 sha1 加密的結果, value 是原碼。
      • 同上, 改用 Rainbow table 建一般性的表, 而不是單純用字典檔。題外話, Rainbow table 的概念滿有趣的, 用到機率和時間空間取捨的技巧。
      所以, 若加密時有用 salt, 攻擊者得另外取得 salt 才能開始攻擊。即使 salt 也存在密文裡, 攻擊者也得照「規矩」來試密碼, 不能用事先建好的表。
      若整份密碼檔用同一份 salt, 攻擊者可以在取得 salt 後開始建表, 之後整份密文就可以用同一份表查出結果。但是當每個使用者的 salt 都不同時, 攻擊者只好每個密文都全試一次, 無法建表加速破解。換句話說, 破解單一特定使用者密碼的時間一樣, 但用多份 salt 讓破解所有人密碼的時間大增。
      當然, 若使用者的密碼較長, 或是有摻雜攻擊者建表時沒用到的字元 (如攻擊者只用字母和數字, 使用者的密碼有含符號), 那加不加 salt 也沒差。不過任何當過系統管理員的人都知道, 絕大多數使用者的密碼都是很簡單的..., 簡單地加個 salt 可以降低不少風險, 讓攻擊者要花更多時間取得明文, 發覺密碼檔外洩時, 可以減少損失。

      2010年9月21日 星期二

      移除 ~/.ssh/known_hosts 裡特定的 host key

      若 known_hosts 不是純文字檔的話, 就得透過指令刪 host key:
      $ ssh-keygen -R HOST
      大概是被 BBS 影響的緣故, 寫一行文會有種罪惡感, 而忍不住想多打幾句話...。

      rsync 來源目錄語法的差異

      當目標是目錄或不存在時, src 有加 '/' 時表示產生一樣的目錄結構, 沒加時表示將 src 複製到目標目錄下
      $ ls src/
      file
      
      $ rsync -av --delete src/ dest
      $ ls dest/
      file
      
      $ rsync -av --delete src dest2
      $ ls dest2
      src/
      

      2010年9月7日 星期二

      用 DVCS 來記錄系統設定檔

      抱著實驗性的心態試用一陣子, 每當要改設定檔時, 就先在當地建個 mercurial repository。目前在這些地方建了 repository:
      • /etc/apache2/conf.d/
      • /etc/mysql/
      • /etc/apt/
      一開始的動機是方便知會其它管系統的人我做了什麼修改。後來發現其它好處:
      • 可以明確知道這次到底改了什麼。改壞了也不用擔心。
      • 可以知道過去改了什麼, 弄新機器時, 輕鬆地弄出一樣的設定。或是只想取部份設定也沒問題, 之前的 changeset 都寫得很清楚, 簡單一句話的註解加上 diff, 就很清楚了。
      在這個例子裡 DVCS 實在是太方便了。減少設定和管理 VCS 伺服器的負擔。一行 hg init 就是一個新的 repository。反正備份也是整個目錄一起備份, 不用另費心思備份 repository meta data。
      不知專業系統管理員是怎麼協同合作管理設定檔的, 目前覺得這樣滿不錯的。

      2010-11-07 更新

      High performance MySQL 建議將所有系統檔集中管理放到 VCS 裡, 再用 symbolic link 連過去, 感覺上是更好的做法。換句話說, /etc/apache2/conf.d、/etc/memcachd.conf 等都是 link, 本體放在 /home/config/ 裡, 而 /home/config/ 存在 VCS 裡。

      2010年8月31日 星期二

      保持小而頻繁的 commit

      一年半前我有個疑問, 究竟要寫成多個小 commit, 還是一個個完整子工作的大 commit。實際運作一陣子後, 我發覺答案很明顯, 小的 commit 才有用處。我現在甚至不明白為啥當時會為這問題困惑...

      小的 commit 有以下的好處 (指令用 mercurial 表示):
      • 發覺某個功能掛掉時, 可以用二元搜尋法快速找到改爛的那版, 接著因為 commit 很小, 很快就能找出是那段 (甚至是那行) 程式造成問題。我已用這招在五分鐘內找出兩個數十、數百版前的錯誤。看到原因後發覺, 若不用版本管理系統, 我大概花很久都不見得能找到問題。
      • 當同伴有時間時, 他們可以容易地 code review, 每個修改都很簡單易懂。
      • 當同伴沒時間時, 他們可以容易地先用 log 濾出需要仔細看的 commit, 並只看一小部份和他相關的程式 (hgtk log 超方便的!)。
      • 寫程式難免會被中斷或忽然亂了方寸, 每做完一件小事就 commit, 確保自己一直都能掌握狀況。真的亂掉不知如何除錯時, 可以先將目前更新存起來 (hg shelve 或 hg revert), 再一個個加回去, 釐清目前狀況。這招數度解救我於混亂之中。
      • 隨時能放心地嘗試, 一看 diff 就知道目前做了那些事。commit 前看 diff 也能很快找出漏砍的除錯碼 (hgtk commit 是大家的好朋友!)。
      另外, 用 DVCS (如 git、mercurial) 再附帶一些好處:
      • 不用擔心自己頻繁的小 commit 造成 build fail, 讓大家共用的 repository 毀了。做個一陣子確定告一個小段落且不會炸到別人, 再 push 這段時間做的一系列小 commits。當然, 每個 commit 本來就該是完整的工作, 且可以正常編譯和通過所有測試。但考量到效率, 小 commit 可以先用較粗略的測試 (如只跑 smoke test suite, 或目前修改部份的 unit test)。push 前再跑完整測試。
      • DAG 可以輕易看出大家開發的支線。但像 SVN 那樣一直線的記錄, 就無法看出各個人開發的脈絡。可能自己的 commit 變 11、14、16 成一個完整的工作, 中間卻搜了別人的 12、13、14。
      • 任何人都可以輕易地在自己的 repository 上做實驗以熟悉版本管理系統。只要打個 hg init, 立即擁有自己的世界!
      不過 DVCS 也不是什麼都好, 像 DVCS 就沒有 lock。若成員眾多, 又會存圖片、音樂這種無法合併 (merge) 的檔案格式, 沒有 lock 就沒辦法避免兩個人在改同一份檔案。
      要能完整地發揮小 commit 的功用, 需要一些好工具的協助。我直到有這樣的需求後, 才明白為什麼大家會需要這些指令:
      • hg shelve: 將目前 working directory 的東西存到暫存的 patch 裡。於是 working directory 就和 repository 內的資料一致。自己做到一部份, 和同事討論需要拿他的程式時, 就可以 hg shelve; hg fetch; hg unshelve。就算 unshelve 失敗, 看一下產生的 reject 檔 (也是 diff 格式), 很快就能將它塞回正確位置。頻繁地 fetch / shelve, 可以減少 merge conflict。另外自己也常做到一半發覺得先改另一個東西, 這時就先 hg shelve。改完另一個東西並 commit 後, 再 hg unshelve。若習慣重構再加功能的話, 這招超好用的。
      • hunk selection: 我是用 hgtk 做 hunk selection。一個 hunk 是指一個修改的檔案裡的一段 diff。hunk selection 就是只 commit 目前更新的部份結果。通常寫寫會手賤順手改一些和目前項目無關的小東西, 像是命之前漏改的小錯、加個共用的 helper function。改了都改了, 為了保持小的 commit 還要還原再分多次一一 commit 太累了。這時就能用 hunk selection 將同性質的部份修改 commit, 就能保有小的 commit 又不用改變原本開發流程。
      另外為了方便追蹤 log, 以下幾件事務必切成獨立的 commit:
      • 搬函式、類別的位置
      • 修正程式縮排
      • 改名稱
      這些修改一次會動到一堆程式。若又混到其它修改, 看的人會很難分辨那些是必須看的修改。千萬不要又縮排又改名字, 或是搬位置順便加幾行新功能。反之, 拆成獨立的 commit, 一看註解 "Refactoring: indent X.", 大概瞄一下覺得沒問題就不用細看了。
      以上的討論漏了如何寫好的  commit log。目前我只知道要先寫一行摘要, 若有必要, 再空一行, 接著寫完整描述。這個第一行寫得好, 其他人就能快速地過跟上進度。還在摸索中, 現在覺得像 mercurial 那樣開頭加個 X: 或 (X) 表示和 X 元件相關挺不錯的。
      最後附上《Coding Horror: Check In Early, Check In Often》, 這篇幫我解決一些疑惑, 強化一些觀念。

      2010年8月29日 星期日

      mysql 快速批次更新資料的方法

      一直忘了記, 來簡記一下。

      若要更新表格 target 裡部份資料欄位 name 的值的話, 假設 id 是 target 的 primary key, 做法如下:
      1. CREATE TEMPORARY TABLE temp ... ENGINE = MEMORY;
      2. INERT INTO temp ... VALUES ..., ...;
      3. UPDATE target, temp SET target.name = temp.name WHERE target.id = temp.id;
      先開一個暫存用的 table, 記得 storage engine 選 MEMORY, 存在記憶體待會兒塞入資料較快。欄位只要有和 target 一模一樣的 value 和 id 即可。前者存更新的值, 後者是待會做 join update 時用的。接著將要更新的值塞入 temp, 記得用批次塞入的方法。最後就是將兩個 table join 起來再更新對應的資料。
      這個作法的好處是, 當需要更新十萬筆資料時, 只要三個 SQL 就搞定。反之, 若下十萬次 SQL 更新, 會因需要連線多次而慢很多。用一個 SQL 更新全部資料, 也可能減少存取硬碟的次數。
      用 TEMPORARY 建立暫存表格的好處是, 這個表格只有這個 connection 看得到, 也不用擔心衝到名稱 (若原本有 temp, temp 會被暫時隱藏起來)。待 connection 結束後會自動丟掉暫存表格。

      2010年8月23日 星期一

      nosetests 的用法

      將之前寫過的東西複製過來備忘。


      安裝方式: easy_install nose。
      寫 unittest 時,管理 test suit 是件很瑣碎又易犯錯的事,相信很多人會想說,能不能跑個程式,自行搜集目錄下全部的測試碼並自動執行。沒錯,大家的心聲 nose 聽到了!這裡直接用例子說明 nose 的使用方式,詳細說明請見《An Extended Introduction to the nose Unit Testing Framework 》
      • 執行目前目錄下所有測試:
        1
        
        nosetests
      • 只執行 package PKG 下的 module MOD 內的測試:
        1
        
        nosetests PKG.MOD
      • 只執行 package PKG 下的 module MOD 內的 test case CLS 的測試 (注意 test case 前接得是冒號):
        1
        
        nosetests PKG.MOD:CLS
      • 執行目前目錄下所有測試並附上子目錄 pkg1、pkg2 的 Code coverage 資訊:
        1
        
        nosetests --with-coverage --cover-package=pkg1,pkg2 --cover-erase
      • 不要執行 slow_test.py:
        1
        
        nosetests -e slow_test.py
      • 使用四個 CPU 平行執行測試:
        1
        
        nosetests --processes=4
      • 只執行上回失敗的測試:
        1
        
        nosetests --failed
      –with-coverage 需要先裝 coverage;–process 得另裝 package multiprocessing ( easy_install multiprocessing ),相關說明詳見 Multiprocess: parallel testing
      另外,若要讓 nose 跳過物件 A 的測試,就在程式裡寫上
      1
      
      A.__test__ = False
      比方若不想測模組 mod,就在 mod.py 裡寫上
      1
      
      __test__ = False

      2010年8月17日 星期二

      Python 強迫釋放記憶體的解法

      悲劇發生的情境:
      你寫了個 python 程式, 它先做些事吃掉個 10G 記憶體, 但隨後用不到這 10G。然後它 fork 成 4 個 processes 處理一些事。結果用 htop 一看, 發現有 4 + 1 個 processes 各吃掉 10G 記憶體。幸好 Linux  有用 copy-on-write, 所以系統顯示總共只用掉 10G。但在 5 個 processes 忙碌做事的途中, 別人的程式也在搶記憶體, 造成 OS 判斷錯誤把這幾個 processes 之一搬到 swap (或是誤把別人搬到 swap), 結果整個系統噴了, 大家都慢到做不完事。
      解法見《How can I explicitly free memory in Python?》, 結論是無法強迫 python 釋放記憶體, 別再相信 gc.collect() 有效這類不實謠言, 至少我試了沒有效。只好用最根本的解法, fork 一個 subprocess 讓它去做那吃掉 10G 的事, 做完掛掉後, OS 自然會回收那 10G, 就和大自然生生不息的循環一樣。由於一開始執行的 process 本身沒吃掉任何記憶體, 之後 fork 4 個 processes 自然沒有占記憶體的問題。

      聽起來頗麻煩的, 好消息是, 自 python 2.6 版起有了 multiprocessing, 2.4 和 2.5 版也有 backport, 只要改用 multiprocessing 執行原來吃記憶體的函式即可。若需要讀函式的回傳值, 參考這裡的說明, 即可搞定。

      2010年8月13日 星期五

      Ubuntu 複製大檔案讓系統變慢的解法和原因

      複製大檔案後, 系統會變得超慢, 明明 htop 顯示沒有執行 application, 卻會顯示全部 CPU (我用的機器是四核心) 都被 kernel 占住 (htop 用紅色表示)。而且 ram 明明沒滿, 卻用掉大量的 swap。查了一下後發現這已是很舊的「bug」, 仍未被解掉, 參見 Ubuntu bug 226784 365775

      《Linux: how to explicitly unswap everything possible?》 提到關掉 swap 可以讓 kernel 釋放 swap, 好讓 application 回頭用 ram。所以先打 "swapoff -a", 用 htop 會看到 swap 上限先縮為目前的使用量, 而非系統的設定值。接著以極緩慢的速度縮小, 最後到零。這時就能再打 "swapon -a" 重開 swap。這時仍會看到 kernel 占掉全部 CPU, 不過等個一陣子 (大概十分鐘吧) , 系統就恢復正常了。

      為了避免再度發生這問題, 查了一下相關資料, 才發現這不太算是 bug。原因出在 linux kernel 決定是否要將 ram 裡的資料搬到 swap 的策略。詳細的說明見《Patrick Lauer: The mistery of swappiness》《swapping behavior》 (後面那篇挺好笑的, 推薦一看!), 大意是當 application 和 kernel cache 用的記憶體量過高時, 即使 ram 仍有空間, kernel 會參考 swappiness 的值, 決定要捨棄 cache 還是將 application 的記憶體搬到 swap。swappiness 的值介於 0 ~ 100 之間, 值愈高, kernel 愈會將 application 用的記憶體搬到 swap。悲慘的是, 預設值一直都是 60, 但現在的電腦有很大的 ram (早期 512MB vs. 現在最少 4GB), 用 60 的結果是, 明明還有很多空間可用, 一搬大檔案, file cache 吃掉過多記憶體, kernel 就將 application 用的記憶體搬到 swap, 於是整個系統進入莫明奇妙緩慢的狀態。

      若要避免這問題再度發生, 參照這篇的說明修改即可:
      • sysctl vm.swappiness  # 顯示目前的 swappiness 的值
      • sudo sysctl vm.swappiness=5  # 將目前的設定改為 5
      • sudo vi /etc/sysctl.conf: 加入這行 "vm.swappiness=5"  # 開機後自動設為 5
      實測後, 改了 swappiness 後就沒有發生 swap 暴增, 然後 kernel 占掉全部 CPU 的情況了, 可喜可賀!! 有文章提到不建議設為 0, 稍微留些 cache 方便檔案操作。另外, 就算設為 0, kernel 只是盡量不將 application 用的記憶體搬到 swap, 待 ram 真的完全被用光時, kernel 還是得強迫擠出空間給 cache 用, 到時系統也會變慢。

      2010年8月12日 星期四

      Python 從 MySQL 資料庫取出整個表格的快速作法

      處理大量資料時, 發覺資料愈大, python 取出 mysql 資料愈慢, 變慢的比例並非線性的, 推測是 client 吃掉太大的記憶體而拖慢速度。

      但是用 mysql client 直接寫入檔案, 以及 python 讀檔的速度都不慢。另外, mysql 有提供兩種方式從 server 端取回查詢結果, 有機會用 sequential 讀資料的方式加速。想說來比看看這三種寫法。

      我從一個有近三千萬筆資料的表格取出單欄資訊, 型別為 varchar。各個方法只測一次, 記憶體用量是用 htop 大概看的, 別太在意。結果如下:

      methodtimememory (G)
      mysql client + read file (all)1m8s~0.64
      mysql client + read file (sequential)0m57s~0
      python api (store)13m40s~3.7
      python api (use + fetchall)>30m>0.9
      python api (use + fetchmany)1m9s~0

      上欄各個方法說明如下, 所有 python 操作都有轉 unicode:
      • mysql client: 直接用 mysql client (系統指令) 寫入檔案 。
      • read file (all): 從檔案讀入全部內容到一個 list。
      • read file (sequential): 從檔案一行一行讀入內容。
      • python api (store): 用 MySQLdb 預設的連線方式, 會用 mysql server 提供的 api mysql_store_result() 取回全部內容到 client 端。
      • python api (use + fetchall): 改用 mysql server 提供的 api mysql_use_result() 一筆筆取回到 client 端 (透過 SSCursor 使用 mysql_use_result() )。
      • python api (use + fetchmany): 同上, 改設 size = 10000, 一次取回 10000 筆以減少取資料次數。
      從結果來看, 先用 mysql client 取出資料寫到檔案, 再用 python 讀檔案比 python api 預設的方式快了十倍左右。其中 mysql client 花了 20 秒, 兩個讀檔方法分別用了 48 秒和 37 秒。

      用 mysql_use_result() 配合 fetchmany 會變快不少, 如同文件所言, 處理大量資料時要改用 SSCursor。速度和先用 mysql client 寫入檔案再讀檔相當。但要配合 fetchmany() 設大一點的 size 才有用, 用 fetchall() 會一筆筆取資料, 反而因連線次數過多而更慢。附帶一提, Django 的 connection 是用預設的 cursor, 也就是採用 mysql_store_result() 取資料, 若想用 mysql_use_result() 只好自己另開 connection 了。

      ps. 以前有比較「預設 cursor 的呼叫一次 fetchall()」和「多次 fetchmany()」。結果用 fetchmany() 不會變快, 原因是資料已取回到 client 端, 再來用 fetchall() 或 fetchmany() 也只是從 python list 取出 list 或 sub-list 而已。 用 fetchmany 搭配 mysql_use_result() 才會真的變快。

      SSCursor + fetchmany 的參考程式如下:

      import MySQLdb
      from MySQLdb.cursors import SSCursor
      
      if __name__ == '__main__':
          conn = MySQLdb.connect(host='localhost',
                                 db='mydb',
                                 user='fcamel',
                                 passwd='*********',
                                 charset='utf8')
          cursor = conn.cursor(cursorclass=SSCursor)
      
          sql = 'SELECT some_column FROM some_table'
          cursor.execute(sql)
      
          try:
              while True:
                  rows = cursor.fetchmany(size=10000)
                  if not rows:
                      break
                  # process your rows here.
          finally:  
              # 要記得關 cursor、connection, 
              # 不然用 SSCursor 時會出現 warning
              cursor.close()
              conn.close()
      

      2010-09-11 更新

      看了 High Performance MySQL 2e 後才知道, 原來 MySQL 的協定是半雙工, 也就是說, 同一時間 server 和 client 只能有一方送資料。使用 mysql_use_result 時在收完全部資料前, client 不能送資料給 server。難怪之前我在一個 process 裡邊收邊寫回資料, 會有錯誤訊息, 後來就改成收完全部資料才開始運算和寫回資料。

      在 Fedora 下裝 id-utils

      Fedora 似乎因為執行檔撞名,而沒有提供 id-utils 的套件 ,但這是使用 gj 的必要套件,只好自己編。從官網抓好 tarball ,解開來編譯 (./configure && make)就是了。 但編譯後會遇到錯誤: ./stdio.h:10...