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 快啊?

在 Fedora 下裝 id-utils

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