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 裡邊收邊寫回資料, 會有錯誤訊息, 後來就改成收完全部資料才開始運算和寫回資料。

2010年8月6日 星期五

增加自定的 logging 的級別

參考 logging.info() 和 logging.root.info() 後, 總算弄出 loggin.detail(message), 用來記錄 DETAIL 級別的 log, DETAIL 是我自訂的級別, 重要性介於 DEBUG 和 INFO 之間。不確定這樣是不是最好的寫法, 總之有弄成功啦。

def _detail(msg, *args, **kwargs):
    """
    Log a message with severity 'DETAIL' on the root logger.
    """
    if len(logging.root.handlers) == 0:
        logging.basicConfig()

    if logging.root.manager.disable >= logging.DETAIL:
        return

    if logging.DETAIL >= logging.root.getEffectiveLevel():
        apply(logging.root._log, (logging.DETAIL, msg, args), kwargs)

def add_detail_to_logging():
    """
    Add a customized level between INFO and DEBUG.
    """
    logging.DETAIL = 15
    logging.addLevelName(logging.DETAIL, 'DETAIL')
    logging.detail = _detail

2010年8月3日 星期二

安裝 MySQL-python

先說裝法:

  1. sudo vi  /etc/apt/sources.list: 加入 hardy-security 列表:
    deb http://security.ubuntu.com/ubuntu hardy-security main restricted
    deb-src http://security.ubuntu.com/ubuntu hardy-security main restricted
    deb http://security.ubuntu.com/ubuntu hardy-security universe
    deb-src http://security.ubuntu.com/ubuntu hardy-security universe
    deb http://security.ubuntu.com/ubuntu hardy-security multiverse
    deb-src http://security.ubuntu.com/ubuntu hardy-security multiverse
    
  2. sudo aptitude update
  3. sudo aptitude install libmysqlclient-dev
  4. (enter virtualenv)
  5. sudo pip install MySQL-python
若不用 virtualenv, 第 4, 5 步就改為 sudo easy_install MySQL-python。

用 pip 裝 MySQL-python 時, 出現「EnvironmentError: mysql_config not found」的錯誤訊息, 摸索一陣子後才發覺 mysql_config 是個程式, 要裝 libmysqlclient-dev 才會有 (用 apt-file search mysql_config 查出來的)。結果用 aptitude install libmysqlclient-dev 卻說找不到。查了官網 package 列表, 才發覺它是放在 hardy-security 裡。在 /etc/apt/sources.list 裡加上 hardy-security 的位置, aptitude update 後, 就能安裝 libmysqlclient-dev 了。裝好 libmysqlclient-dev 後, 再執行 pip install MySQL-python 就 OK 了。

自從會用 apt-file 後, 看安裝錯誤訊息說少什麼, 就用 apt-file search FILE 找一下該檔放在那個 package 裡, 安裝軟體還滿順利的。apt-file 真是太好用了。

mysql server 連線問題

剛發覺可以從 mysql server 本機連 localhost, 但無法從別台機器連到 mysql server, 會出現 "Can't connect to MySQL server on X.X.X.X" 的錯誤訊息。查了一下, 這篇提到解法。將 /etc/mysql/my.cnf 的 "bind-address       = 127.0.0.1" 註解掉, 就可以從別台機器連了。看來 Ubuntu 上 mysql 預設是只能從本機連。

在 Fedora 下裝 id-utils

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