2011年3月28日 星期一

試證 Plurk 是目前台灣最大的 micro blog

最近看了《學會思考,你贏定了!:45個讓你站穩立場的論證法則》, 試著練習用較嚴謹的程序論證「Plurk 是目前台灣最大的 micro blog」。試了以後, 發覺即使是這麼「明顯的事實」, 論證起來, 還真不是普通的難。

  • 一開始我搜 plurk market share taiwan, 或是類似的關鍵字, 加減 "twitter" 或是幫 "market share" 加雙引號, 結果找到 2009 年末和創辦人的訪談, 文中有引用 InsightXplorer 的資料, 不過畢竟是 2009 年年末的資料, 和現在差了一年多。
  • 後來查 Wikipedia 介紹 Plurk, 裡面有引用 Alexa 說明 Plurk 近半的流量來自於台灣, 但這不表示 Plurk 是台灣最大的 micro blog。
  • 直接看 Alexa 統計台灣的 top site, 會發現 Plurk 是第 28 大的網站, 在它之前沒有其它 micro blog, 這應該算有力的證據。這裡有個隱藏的前提, 我認為 Facebook 不算是 micro blog。還有不確定 Alexa 的排序方式。
  • 看 Double Click 統計台灣的 top site 得出類似的結論, Plurk 是第 31 大的站, 在它之前沒有其它 micro blog。Double Click 看起來是用不重覆的拜訪者來排序。
  • 再來用 Google Trend 查 台灣區搜尋 plurk 和 twitter 的比例, 可以說明 Plurk 比 Twitter 紅, 不過這不表示 Plurk 是最紅的 micro blog, 只能說他在台灣比 Twitter 紅, 算是較弱的證據。但相對於 Alexa 用流量 (推測) 和Double Click 用不重覆拜訪者, 搜尋關鍵字是另一種具代表性的指標。
綜合以上結果, 可以比較明確地說「Plurk 是目前台灣最大的 micro blog」。雖然還有會員數等其它資料, 應該也能做為論證依據, 不過查到這裡我已經能說服自己了,  就懶得查下去了。
仔細想想, 這個論證的目標, 本身並不夠明確, 沒定義是指什麼方面「最大」, 是會員數還是 page view。一個嚴謹的論證程序, 真是困難啊。

2011年3月26日 星期六

Same origin policy 和 JSONP

Wikipedia 上有清楚的範例說明 Same origin policy, 瀏覽器端的 javascript 只能存取 protocol、domain name、port 一致下的檔案。若沒有這個限制的話 [*1], 網站 A 就能透過使用者取用網站 B 的內容, 後果不堪設想。但是, 現今做網站常需要整合其它網站的內容, 像是在自己的網站用 Flickr 的相片, 用 Google Map 的地圖等, 必須要有方式安全地避開 Same origin policy。
備註 1: 我自己測的結果, 用 ajax 連到其它網域要東西, 對方的 server 還是會收到請求, 並傳回 200, 只是 ajax 不會得到結果, 我猜應該是瀏覽器自己擋掉拿回來的結果。所以, 即使有 same origin policy, 還是有可能讓使用者狂連其它網站, 浪費別人的資源。但也會留下 referral log 就是了。
《JavaScript 抓取跨網域外的資 清楚的說明避開 Same origin policy 的兩個作法
  • 透過自己網站的 proxy script, 就沒有 same origin policy 的限制。也就是說, 自己先寫好一個 proxy script (用 PHP / CGI,  什麼都可以), 要取用其它網域的檔案時, 先連到自家的 proxy script, 透過它連向目標 URL, 再直接傳回收到的結果。缺點是會增加 latency, 還有增加自家網站的負擔。
  • html tag 沒有跨網域的限制 (不然連載入其它網站的圖和 CSS 都很麻煩), 可以用 <script type="text/javascript" src="http://OTHER-DOMAIN/some.js"></script> 載入執行另一個網域的 javascript。
由於 HTML 可以動態加入元件, 這讓使用 script tag 的作法有更多彈性, 可以動態載入其它 domain 的 javascript, 執行裡面的內容, 也方便控制執行的順序。若對方的 javascript 知道自己目前的內容 (DOM、javascript), 就可以搭起兩個網域的資料。於是有了 JSONP 的想法: OTHER-DOMAIN 提供一個 URL, 這個 URL 接受一個參數 callback, 表示 OUR-DOMAIN 寫好的函式名稱。OTHER-DOMAIN 則傳回 CALLBACK({...}) 的程式碼, 用 script tag 載入後會自動執行, 結果就是用 OUR-DOMAIN 的 CALLBACK 接受 OTHER-DOMAIN 「傳回」的 JSON 物件 ( 即 {...} ), 達到跨網域的需求。必要時 OTHER-DOMAIN 也可以多產生一些程式碼, 做些 CALLBACK 的前處理和後處理。
Wikipedia 有詳細的 JSONP 例子, 另外這裡這裡也有不錯的例子, 觀察 server side (即 OTHER-DOMAIN) 和 client side (即 OUR-DOMAIN) 的原始碼和中間產生的 code, 就能明白它運作的機制。
使用 JSONP 的代價就是, 使用者得相信提供 JSONP API 的網站不會亂搞, 偷產生惡意程式碼, 這有點像接受外部的輸入, 再執行 eval 一樣, 方便歸方便, 卻有一定的風險。另外 jQuery 有提供 JSONP 的執行方式, 可以省掉自己產生 script tag 執行, 再砍掉它的步驟。

2011年3月24日 星期四

MySQL table cache 和 open table 的效能影響

最近同事跑程式時, 發現做大量 join update 時, 偶而會很慢, 但是 flush table 後就恢復正常。依官網所言, flush tables 做兩件事
  • 清 query cache (等同於 reset query cache)
  • 關掉所有打開的 table
經同事測試, 執行 reset query cache 無效, 剩下的原因只剩關 table。這讓我百思不得其解, 不就是開關檔, 怎麼會有效能差異? 後來經 Tib 提醒, 用 "show global status like '%open%'" 一看, 發覺相當地不得了。table_cache 預設值是 64, 開啟 MySQL server 至今 (應該不到一個月), 竟然開了一百多萬次 table, hit rate 是 0%  (可以用 mysqltuner.pl 摘要 show status 的結果)。
研究一下官網的 How MySQL Opens and Closes Tables 後 (下面留言也很有幫助), 得到以下的心得:
  • 當 table_cache 沒東西或新增 temporary table 時, 會增加 Opened_tables, 這個變數存的是 MySQL server 開啟至今, 開啟 table 的次數。
  • show tables 或操作 information_schema 會一口氣開一堆檔案, 若用到多個 database, 大家都來個幾下 show tables, 很快就會塞爆 table_cache, 所以不能設太小
  • 但若 table_cache 大於等於 open_files_limit, MySQL 執行一會兒後會停住。查看 /var/log/syslog 後發現 errno 24, 結果是超出能開檔的數量限制, 讓 MySQL 無法做事, 因為 table_cache 占掉所有能開檔的數量, 結果要再存取別的 table 時, 卻無法再開檔了。將 open_files_limit 設得比 table_cache 大, 就沒事了
  • table_cache 也不能設太大, 照這篇的說法, cache 滿的時候, 找 expired record 會很花時間
調大 table_cache 後, 程式有順利跑完, 但原本就是偶而會發生的事, 所以還要再多觀察一陣子, 才能確定是否有解掉問題。下次再發生時, 再來觀察 status 裡和開關 table 有關的記錄, 確定是否有關。

btw, Set ulimit parameters on ubuntu 說明如何改變 ulimit 的限制。照 MySQL 官網的說法, 改 my.cnf 應該能提高 open_files_limit, 若不行的話, 再來試看看改變 Ubuntu ulimit 開檔的預設值 (1024)。不過那篇文章提到要先設定 PAM 再重開機, server 不方便隨便重開測試, 先備忘吧。

2011-04-09 更新

試過改 my.cnf 無效, 改成修改 /etc/init.d/mysql , 將 /usr/bin/mysqld_safe 加上 --open-files-limit=N 後, 就 OK 了。

2011年3月14日 星期一

MySQL 交集的寫法

一直以為自己寫過這個記錄, 今天來這裡查卻找不到, 真是太神祕了。

參照這篇的作法, 用 UNION ALL + GROUP BY + HAVING COUNT(*) = 2 就可以做到交集的效果。
SELECT * FROM (
             SELECT DISTINCT col1 FROM t1 WHERE...
             UNION ALL
             SELECT DISTINCT col1 FROM t1 WHERE...
) AS tbl
GROUP BY tbl.col1
HAVING COUNT(*) = 2
附帶一提, 聯集自然是用 UNION, 差集可以用 sub query
SELECT * FROM T1 WHERE ... AND id NOT IN (SELECT id FROM T2 WHERE  ...)

2011年3月13日 星期日

MySQL 大量寫入的加速技巧 (bulk insert)

官網 Speed of INSERT Statements 詳細地解釋各種加速方式和它背後做的運算。結合個人經驗, 在這裡摘要相關訊息:
  • INSERT 時會依序做 connect、send query、parse query、insert record、update index。雖然可以用 prepared statement 減少 parse query 的時間, 最耗時間的應該還是 update index, 省時的關鍵在於如何減少 update index。
  • 有兩個方向可以達到這個目的: 減少 insert 次數或是減少 update index 次數。
最佳解: 一次搞定
  • LOAD DATA INFILE 最快。這不難理解, 但我沒試過就是了。
  • INSERT ... SELECT ... 也是好方法, 類似 LOAD DATA INFILE, 只是資料來源是 MySQL 本身。讓 MySQL 自己一次搞定取資料和寫資料的操作, MySQL 有最多的彈性決定處理資料的順序。
次佳解: 減少 insert 次數
  • INSERT ... VALUES ... (ref: 在該頁搜 values): 一次寫入多筆資料自然能減少 update index 次數, 不過資料太多時可能會超出 SQL 長度限制, 看到相關錯誤訊息時, 記得改 my.cnf 調高長度。我自己覺得一次寫過多筆應該會出問題 (單次操作用過大的記憶體, 不會是好事), 通常都一次寫入個一萬筆或十萬筆。實測的感覺兩者沒差太多, 百筆以下會比較慢。
次佳解: 減少 update index 次數
  • LOCK TABLES 後, MySQL 就不會急著 update index, 而會等 UNLOCK TABLES 後再一次更新 index。若不在意其它 thread 會用到這個 table, 用 LOCK TABLES 可在多次 insert 的情況下省下不少 update index 的時間。
  • DISABLE KEYS 和 ENABLE KEYS (ref: 在該頁搜這兩個 keyword): 這是 MyISAM 才有的功能, 並且只能用在 non-unique index, 因為 insert 時需要檢查 unique constraint, 不能暫時關掉。實測後效果並不好, 雖然 insert 時有省下時間, 但是連同 enable keys 時重建 index 的時間, 整體來說反而變慢。
其它相關的選擇
  • INSERT ... ON DUPLICATE KEY UPDATE: 這個語法相當威, 寫入資料, 發覺違反 unique constraint 後, 再執行指定的更新操作 (合併資料) 。批次寫入常會遇到一些小狀況, 用這招省了不少事。若改拆成批次 insert 和批次 update (用 join update), 相對來說會慢上不少。
  • 先 drop unique constraint、drop indexes 再寫入, 寫完再重建 unique constraint、index: 這方法反而是最慢的。問題出在 MyISAM 在修改 schema 時會重建 data file 或 index file, 若有三個 index, 寫完資料後下三次 CREATE INDEX 的 SQL, 結果 MySQL 會重建三次 index file, 每次都會讀出全部資料, 加好新 index, 再寫入新的檔案, 移掉舊檔。附帶一提, 上面的 DISABLE KEYS 和 ENABLE KEYS 比這個作法快, 因為 ENABLE KEYS 後是一次重建全部 non-unique indexes, 不是有幾個 non-unique indexes 就重建幾次檔案。
多個 client 同時寫入的其它選擇
    • INSERT DELAYED 讓 client 送完資料立即結束操作, 可提高各 client 減少的時間, 也能集中寫入資料, 一次寫入到硬碟。但是, 整體來說 overhead 較高, 若是單一 client 寫入的情況下, 反而會更慢。我沒有實際用過。
    最後, 遇到效能問題時, 一定要在自己的環境實測才準, 了解概念和實測同等重要。

    查詢網路封包經過的地理位置

    不知是否是日本地震的影響, 用 Hinet 連一些國外網站速度超慢的。剛聽 stephon 說, Windows 下可以用 WinMTR 之類的工具查經過的 IP。再來用些 IP-to-Geo 的服務, 可以得知封包經過的地理位置。這類服務滿多的, google "ip to geo" 有很多選擇, 像是 IP2Location™ Live Product Demo

    備註: HiNet Traceroute 提供網頁介面從 Hinet 機器發出 traceroute。

    2011年3月12日 星期六

    隨筆心得: 專注地增加經驗


    ( 整理 plurk 上的隨筆: http://www.plurk.com/p/b41fav )

    今天更深刻體會到, 沒有相關經驗, 很難理解別人消化後的體悟。厲害的人經驗不見得比別人多, 但觸類旁通的能力強, 對於自身或週圍人的經驗思考得較透徹, 可以彌補不足的經驗, 加速成長。

    增加經驗的不二法門, 就是投入更多時間實戰, 以前我多少會想找捷徑, 看過杜書伍說他發覺唯一的「捷徑」就是同樣的日子裡, 投入更多的時間, 於是我就看開了, 不再想辦法找捷徑, 穩紮穩打地練功。

    另外, 同樣的時間裡, 鑽深一件事會比多做幾件事來得有效, 以前也在兩者間游移不定, 經過多年自身不成功的經驗後, 外加看到杜書伍得到相似的結論後, 我改成盡量專注在相似的事上, 兩年下來, 效果愈來愈明顯。

    將第一個體系挖深後, 就能用更快的速度熟悉第二個體系, 於是能更快地觸類旁通, 更快地融合體悟出其它事。很多東西背後的原理差不多, 技術的領域之間特別明顯, 但技術外的領域之間也是如此。

    比方說在做技術決策前要先了解「context」, 不止在討論技術時有用, 日常生活解決問題也有用, 待人接物也有用。

    瀏覽器切換上下頁的行為以及 AJAX

    記錄一下觀察到的東西, 可能有錯。

    瀏覽器在切換 URL 時會存下對應的檔案 (HTML、CSS、etc), 在瀏覽器按上下頁時, 瀏覽器會重新 render 該頁, 不會重送 HTTP request 取得內容。這表示:
    • 會重新執行 javascript code 
    • 會另外觸發一些特定的 javascript event  (有改變 URL)
    對 AJAX 來說, 這造成一個問題, 因為 AJAX 沒有改變 URL, 自然無法直覺地切換上下頁。但有個還不錯的偷吃步作法, 利用瀏覽器處理 URL 中「#」的行為做出切換頁面的效果。在 URL 中有「#」的情況下, 瀏覽器會記錄更改的 URL, 並且不會送出 HTTP request, 所以可以按上下頁切換。比較特別的是, 瀏覽器不會重新 render 該頁, 因為「#」原本的用意是跳到同一頁面內的不同位置, 重新 render 會拖慢速度。於是有人利用這點, 配合改變 URL 觸發的 javascript event, 包了函式庫, 用來管理 AJAX 切換頁面的效果。

    以 jQuery Address 為例, 概念很簡單: 將 AJAX 的參數記在「#」後面, 註冊所有會換頁的事件, 在送出 AJAX 前先改變 URL, 改變「#」後的內容, 這樣會觸發事件, 接著再統一在改變 URL 觸發的事件內從「#」後取回參數, 看要做什麼事。由於改變「#」後的內容, 瀏覽器不會送 HTTP request, 不用擔心為了記錄 URL 而多送 HTTP request。看看範例程式碼和玩玩範例, 會比較快理解它的行為。

    看起來相當完美, 唯一的小缺憾是, 切換上下頁時會觸發 URL 改變的事件, javascript 的對應行為就是執行我們原本寫的 handler, 於是會重送出 HTTP request (AJAX), 這違反我們原本對切換上下頁的認知, 它不該多送 HTTP request。若要克服這個缺點, 就得用 javascript 實作 cache 機制, 先查看看是否已取得這組參數對應的內容, 是否沒有過期的顧慮, 沒有的話就不用送 HTTP request。我想一般來說不會寫到那麼麻煩, 若回上頁要重送 HTTP request, 就讓它重送吧, 程式會比較好維護。

    備註

    2012-03-06 更新

    javascript - Updating address bar with new URL without hash or reloading the page 提到 HTML5 的新 api: window.history.pushState(), 在 jQuery Address 裡也有用到它。若想自己另外實作這個功能不想套 library 的架構, 可考慮直接用它, 現今支援 HTML5 的瀏覽器又更多了。

    2011年3月4日 星期五

    apache2 rotate log 的注意事項

    apache2 沒有收到 SIGHUP 就關掉 log 檔重開這回事, 所以若手動改 log 檔, apache2 仍會寫入同一個檔案。Ubuntu 上 logrotate 的作法是, 在 rotate apache2 的 log 檔後, 會要求 apache2 reload config, 藉此讓 apache2 關掉重開 log 檔, 好讀到新的檔名。可從 logrotate 設定 apache2 的內容看出這件事 (/etc/logrotate.d/apache2):
    /var/log/apache2/*.log {
            weekly
            missingok
            rotate 52
            compress
            delaycompress
            notifempty
            create 640 root adm
            sharedscripts
            postrotate
                    if [ -f "`. /etc/apache2/envvars ; echo ${APACHE_PID_FILE:-/var/run/apache2.pid}`" ]; then
                            /etc/init.d/apache2 reload > /dev/null
                    fi
            endscript
    }
    

    但若自己架的一些 Web 服務需要「暖機」的程序的話 (比方說在 CGI process 的記憶體內暫存一些前處理的資料), logrotate 作法會拖慢 log rotation 後第一批連進來的使用者。

    apache2 有提供 piped logs 來避免 apache2 reload。概念是透過外部程式來 rotate log, 而 apache2 也會知道發生 log rotation, 藉此重開檔案。官網有提到, 除搭配內建的 rotatelogs 外, cronolog 是更彈性的選擇, 在《Apache log rotate 的另一個選擇:cronolog》有詳細的教學。另外要注意的是, cronolog 不像 logrotate 有提供設權限的機制, 自己要另外跑個 crontab 將群組改為 adm, 權限設成 640。

    在 Fedora 下裝 id-utils

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