每特17劃

及時當勉勵 2004/06/07

效能瓶頸下的選擇傾向 — 2017-06-24

效能瓶頸下的選擇傾向

約是在 2014 年前後有了新的體悟,但一直遲未將想法化為文字。今天終於静下心來作這件事了,我想要說明的是,為何在某些效能瓶頸的關頭,我會主張作深度分析優化,而非立即性擴充資源緩解眼前的問題。

舉電商網站經營為例,資料量主要有會員、訂單、商品等資料,大致可初估資料的複雜度以 n^3 的速度在膨脹,因此所需的運算需求量也以這速度跟著成長。這可用下方這示意圖來表示:

https://imgur.com/bBBtC3N

其中X軸為資料量,Y軸為硬體運算量,而運算資源需求則是原點連往 (A)、(B)、(C) 三點的這條曲線。

當網站逐漸成長,終於來到第一個效能瓶頸點 (A) 時,網站又慢又卡、硬體滿載,並已危及正常運作的時候。我們有2個方向的選擇:

《方案一》直接擴充硬體。這可能馬上就解決了(假設是 1 天),相對而言較快速而簡單。

《方案二》分析軟體程式,找出並修正潛在的程式瓶頸。這工程相對耗時長,可能要耗上不少時間(假設是 5-14天),也會有相應的開發成本支出。

因為網站瓶頸會影響營業收入,所以在規避損失的心態下[1],多數人會選擇 《方案一》的路線,直接將資源從 L1 提升到 L2 ,以盡快脫離眼前損失的狀態。接下來發展的狀況大致如下:

1. 很快就擱置了改善軟體效率的念頭 
    1-1. 因為眼前壓力解除了 
    1-2. 有其他更重要的事 ( 應付眼前業務、新功能開發、...)·
    1-3. 非受迫性的額外支出 ( 需要另外撥出預算跟時間 )  

2. 資料量隨時間持續成長,然後又遇到新的瓶頸點 

3. 在瓶頸點再度面臨兩個選擇,但基於 
    3-1. 業務量較先前更大,相同時間的代價損失並上一次更高
    3-2. 營運壓力更大,工作人員比先前更加心有餘而力不足 
    3-3. 先規避眼前立即的損失再說

4. 再度選擇《方案一》,然後重複回到 1. 

最後反覆重覆上述流程,直到獲利跟時間餘裕都被最後吃光,再無法越過某個天花板為止。

那麼,若是一開始在 A 點時,選擇《方案二》的路線會是如何?

從圖中簡單的估計一下 (A)-(P)-(Q)-(R) 的路線,(假設資料量以每個月 1W 的速度成長),雖然一開始多花了 15 天的時間,但遇到資源關卡 L2 的時間(瓶頸點R) 可能約是 5.5 個月之後。相較之下, (A)-(B)-(C) 的路線,一開始只花 1 天,但可能約 2.5 個月就抵達資源關卡 L2 (瓶頸點B),然後不到 2.5 個月,就抵達資源關卡 L3 (瓶頸點C)。

於是,同樣過了 6 個月的時間,面對相同的資料量,兩者不管是在資源耗用度,或是在時間餘裕度上,都形成了此消彼長的差距。而這差異,其實早在第一個瓶頸點 (A) 當下的選擇就決定了。

最早我以為,有很大的可能,會在 (A)-(B)-(C) 的過程中,轉變另一個選擇。但是,當我不只一次遇到,已經發展到 (C) 狀態的苦主們,在討論解法時仍不死心的一再追問說"有沒有更快的方法"的時候,我就深深明白到,人的選擇是有慣性的。一旦選擇《方案一》的路線,除非個性或環境條件有變化,否則就只會一直選擇《方案一》下去,直到被迫不得不改變為止。

同樣的,選擇 《方案二》 的路線,一旦獲得了成功經驗後,也會持續的選擇 《方案二》 的路線下去。唯一不同的是,《方案二》 的路線中,任何一點要切換選擇 《方案一》 相對較容易,所以 《方案二》 比較是一開始選擇較辛苦,但後面的選擇空間是越來越寬裕的路線。

因此,這就是為何在效能瓶頸的問題上,我會主張優先作分析優化,而非立即擴充資源跳過眼前損失的原因。

思考1

"先緩解眼前的問題再說" 有錯嗎?

這没有錯,這是人務實生存的天性,即便我也是如此,而且如果病人已經休克了,還優先處理手指挫傷的部份,也很不合理。所以,危機處理是必要的,我並不否定這件事。

真正的問題點在於,絕大多數的人在眼前的問題緩解後,就將問題給遺忘了。

如果你是個問題緩解後,仍會自發性的長期追蹤跟解決該問題的人,你一點都不需要在意這個問題[2]。但如果你是個問題緩解後,就很快的健忘,然後一再重覆遇到類似問題的人,那麼先忍住待在 "面對眼前問題的損失" 的這個痛苦之中,可能是唯一個能不斷刺激推動你徹底解決問題的唯一動力。

說到這裡,可能有不少人會抗議說:

"痛的是我,又不是你,講的好像很簡單一樣"

是的,完全没錯,這是個困難而且違反直覺的選擇。也正因為真正痛的人不是自己,而是落在作決定跟承擔決定的人身上,所以更需要溝通與說明觀念上的落差。這也是我花這麼多時間寫下這一篇文章的動機之一。

希望讓更多人能正視到,這些簡單決定背後隱藏的長期代價,並理解另一類型的想法為何。而最後不論你認不認同這個想法,最終的判斷跟決定權都還是完全掌握在您的手上(也應該完全承擔),這始終絲毫未變。

如果文中的觀點能帶給你一個新的角度,或是協助你解釋跟說明給另一個人聽,就太好了。 但若是因此在對方未認同下,擅自幫別人作決定而執行的話,我想就不是那麼好了。

也許你是在為別人好,但另一個角度來看,或許也是在迴避"解釋說明跟尋求共識"這條較困難但較長遠的道路。

思考2

另一個近期想通的是,這個想法角度,不只侷限在軟體程式與硬體資源的選擇問題上。 相同的問題也出現在程式開發上的程式架構選擇,開發流程,開發工具,工作方法上… 舉例來說,當我們在努力開發程式時,我們可能會為了跳過程式封裝的限制,而直接複製程式碼片段到我們要用的地方,或是作一些違反常軌的 workaround 。而隨著程式碼片段的複本越來越多,且更新狀態不一致,最後不斷增長的複雜度跟層出不窮的 bug 與斷點,就耗光了程式設計師的腦力跟時間。於是類似的命題或許就會像是:

"要快速拆裝 workaround 完成新功能再說,還是要花更多時間遵循架構,甚至重構改良某些元件?"

其他問題像是: "要直接加班先出貨眼前的積存訂單,還是要花時間研究找出工作流程中的效率問題,以簡省未來的工作時間?" "先把眼前這題型背起來再說,還是要花多一點時間了解題目背後原理,以應變未來不同的題目變化?" …

在這許不同的事件上,都有類似的瓶頸問題性質。

思考3

這個思維不適用於生活中的所有面向,因為人生有限,每個問題都要這樣分析面對太累了,不可能。 但如果是一個反覆發生的問題,且連動到你的生產力或是每日動線上時,或許你可以好好考慮嘗試不一樣的選擇方式。

思考4

> 先求有,再求好

這個大概是兵臨城下,要解然眉之急最常聽到的說辭了。但是常常,求有 之後,幾乎就再也沒有 求好 這回事了。但是兵臨城下之際,確實也只能 先求有 。但對於自我要求較高的人,其實是看在對方願意 求好 或有想徹底解決問題的份上,才投入幫助的。事實,多數結局是令人失望的。

問題在於,是否在一開始就識別有無 求好 的方式? 依我個人經驗,有下列特徵的對象是比較有希望的:

  • 隨身會備有一份重點問題清單
  • 有規律 review 問題的習慣(至少每週)。例如: 主動定期回診
  • 有不定時主動還舊債的習慣

以上,

希望以上幾點分享對大家有幫助。

[1]. 規避損失的傾向參考"展望理論"
[2]. 有趣的是,一個長期追蹤跟解決問題的人,還會遇到上述種種的問題嗎?

Facebook post 2017-06-09 — 2017-06-09

Facebook post 2017-06-09

我注意到幾件事, 一是資料統計內容是針對"工業跟服務業",二是,台灣公司有高薪低報的狀況。 最奇怪的是,為什麼工業是跟服務業的資料混在一起調查???

Comments:

  • Vincent Cheng: 都是壓榨員工的行業
  • Arvin Yeh: 工商普查嗎?
  • 李俊裕: 好像是普查,資料來源是: https://www.stat.gov.tw/ct.asp?xItem=41083&ctNode=2291
    • 李俊裕: 另外,補充我後續進一步了解的狀況 (文長) <br/> 1. 普查的資料大致來源為何? <br/> 我有根據網頁上的電話打去詢問一下 <br/> 他們作的是抽樣調查,方向大概是隨機抽樣廠商,請廠商填寫薪資方面的狀況。 <br/> 然後我問,這要如何確認廠商填的資料的正確性? <br/> 他們有回覆說,會依據"人力運用調查"的資料來作比對,看是否吻合。 <br/> 這讓我注意到,"人力運用調查"的報告,是直接以個人來調查,而不是由資方提供的。所以,"人力運用調查"的內容可能更接近一些,或許有興趣的人可以再進一步看看。 <br/> 另外,我有問,樣本數大概是多少? <br/> 回覆是 1 萬左右。 <br/> 樣本數是夠的。 <br/> 2. 工業跟服務業的資料為何不分開統計? <br/> 收到的回覆大致上是,因為資料會對照"人力運用調查"的資料,而該資料調查時,因個資考量作答沒有限定產業。 <br/> 最後以取總計為主。 <br/> 實際上,分項資料都在主計處跟統計學會相關的網站資料庫可以查得到。 <br/> 只是上述新聞發佈時,著重在總計。 <br/> 3. 我有注意到,這類薪資中位數的報告,其實是在 2016 年底才新推出的 <br/> 先前的資料主要是"xxx年薪資與生產統計年報",而該年報的內容,有不同類產業的分項統計 <br/> 內容蠻完整的,想比較的人可以再自行對照看看。 <br/> 註: 有些對話我記得不是非常清楚,若有出入的話,可以再自行求證看看。 <br/> ==== <br/> 我敘事的內容有點雜亂,但如果細心去觀察思考一下 <br/> 大概就會知道,數據本身的準確性其實是有待商榷的 <br/> 但我並不認為這是公務員的錯,或是素質不好,或是黨派的問題 <br/> 事實上,近年來辦文件跟詢問的狀況,我覺得一線人員的處理態度跟效率,其實遠勝 10~20 年前的 <br/> 甚至比某些銀行還要好,這個進步是非常值得肯定的。 <br/> 所以,問題應該是出在,數據調查跟統計的制度上,還不夠逼近事實跟逼近真相。 <br/> 需要的是修正制度,而不是修理人。 <br/> 要寫這段心得時,心裡是有點掙扎,因為我只想了解事實,並不想旁生爭端或是佔用大家眼球的時間。 <br/> 所以,就請大家不用特意按讚了。但若您也是個想了解事實的人,希望以上的資訊對你有幫助~ <br/> Thanks
  • Nathan Chang: 請問高薪低報帶來的影響是?
    • 李俊裕: 非常好問題。我認為影響大致如下: <br/> 舉例: 假若員工薪資為 5.7W, 但因其他因素在帳面上登記為 5.2W, 此時統計出來的結果就會少個 5000 塊。 <br/> 因市場行情的資訊是不透明也不對稱的(公司可知道員工群體的薪資分佈,但求職者不知道公司的待遇分佈) <br/> 所以求職者能參考的數據是相對有限的,此時薪資的調查報告就會產生一定的錨定作用。 <br/> 若調查報告低估 5000 元的話,那相對的,也會讓求職者的評估跟議價產生 5000 元的落差。 <br/> 而公司方面,可能不小心發現自己公司的敘薪略高於"市場行情",而再嘗試往下調整。 <br/> 幾回合之後,薪資就會朝對受僱者不利的方向傾斜了。 <br/> 註: 相對的,若調查是高估的狀況的話,就會朝對資方不利的方向傾斜。 <br/> 註: 我個人認為公佈調查結果是好的,若資料全面一些會比較好。
    • Nathan Chang: 李俊裕 感謝分享 之前看的太短 覺得可以少繳稅 新創公司又可以留住人才 只有國家吃虧
    • Yuan Chao: 勞工以為可以少付點稅,可是公提的勞健保可是虧大了!
  • Yuan Chao: 這篇的結論有點廠廠,不過幾個評估的點倒是可以檢驗一下… http://soc.thu.edu.tw/2006TSAconference/_notes/2006TSApaper/5-15.pdf
曾經遇過的 Firefox 與 Chrome 的不穩定問題 — 2017-06-04

曾經遇過的 Firefox 與 Chrome 的不穩定問題

記得很久之前,(註:2014-09-02),有個問題一直很困擾我。就是我的筆電,只要開機一段時間,就會在某個時間點就會突然產生 CPU 飆高,風扇一直狂轉的狀況。即使把所有應用程式關閉,仍沒辦法穩定下來,直到我重啟 Xorg 視窗系統,才會恢復正常。這個問題,常常無預警的中斷我的電腦操作,搞得我很痛苦。

我試圖找出問題的根源在那裡,大致將可能的嫌疑犯縮小到 Xorg,Firefox,Chrome,qtile(Window Manager) 這4個主程式。不過,一直沒辦法精確的 reproduce 問題的發生。當時,正值 Firefox 跟 Chrome 在競爭效能之際,我看 top 的資料,都是 Firefox 用的資源比 Chrome 還多,所以有好一段時間,以為罪魁禍首是 Firefox 。

後來,我開始接觸並上手 ganglia/graphite 這類監測工具,有天我想到,之前都是在監看遠端主機的數據,我何不運用這工具直接觀測我自己每天在用的NB呢?! 於是我開始撰寫幾個抓 cputime/memory 的script 跟設定,我直到我後來取得下面這張關鍵數據圖之後,我才明白,原來我的筆電上的問題源頭是 Chrome。

https://imgur.com/0Rt5Kie

從上圖中可以觀察到,Xorg 跟 Chrome 的 cputime 在執行到某一個時間點時,就同時出現斜率的變化,跟問題症狀出現的時間也吻合,我這才恍然大悟, 原來禍首是 Chrome。而這關鍵證據,也洗清了 Firefox 的嫌疑。最後是分析出,是 Chrome 有用到 graphic render 加速的功能,關閉後就沒事了。

後來又有一次,大約是 2015/02/20 前後,有更新系統,升級了好些函式庫, Firefox 也從 34版升級到 36版,之後就又產生類似的 Xorg reset 的問題。幸運的是,當時我已經建好有觀測數據的機制,再加上有高手在網路上的討論幫忙,最後將分析範圍縮小至 Xorg intel driver 跟 Firefox 的 render 加速相關的功能。

https://imgur.com/dLncsQK

其中,由上圖這張當時取得的數據圖中,可以看到 Xorg 的 vesa/intel 驅動程式的不同,及 accel 的 on/off 的一些變化。由此可證實是 Xorg accel 跟其相關的下游應用程式的部份所致。雖然,最終沒能找出是那一段程式導致 mem leak ,但透過更換 driver 並關閉 accel 的選項,暫時避免掉嚴重的 reset 危機。

(註: 後來 Firefox 在某一版本更新後,該問題就消失了)

這兩次經驗(還有先前的一些小案例),給我非常非常大的啟發。 以往程式 debug 經驗,多數都依賴 gdb, xdebug, … 或是跑 unit test 來測試程式的問題,這些通常重複操作就能重現一樣的問題。但對於這類正常執行運作一段時間,然後突然崩潰的問題,常常是束手無策。因為,問題發生的那瞬間,我們往往不在現場,也沒有任何除錯紀錄。沒想到,僅僅透過簡單的 cputime/memory 這2道的數據偵測歷史紀錄,對問題的原因分析的幫助會這麼大。

這也讓我意識到,將飛行安全提升到更高等級的關鍵,或許不是風洞測試,而是黑盒子(Flight Recorder)。想想看,一台穩定飛行20年的飛機,出事的時候就只有那瞬間,我們只有一次機會可以補捉相關線索。我想,在時光機發明出來之前,只有透過 Logger 機制,才能迎戰這類 runtime 執行期的問題。這也讓我開始有了 Logger Driven Design 這個想法思考。

最後,再提供一張當時截圖到的 Xorg 在系統升級前後的對照:

https://imgur.com/kqEYRj8

從上圖中,可以觀察到 Xorg 在系統更新前的舊版本,其使用的 mem 都不到 200MB , 而且運作 2~3 個星期以上,都沒有任何 mem 用量累積。 zero memory leak 水準的軟體確實存在,是真的可以達成的,而且就在我每天都在使用的軟體之中。或許,更高水準的軟體方法,不見得在最新的技術潮流,而是隱身在這些老牌軟體專案的某個角落,等待著我們再重新去發掘它。

註1:

FB 上的留言討論:

https://www.facebook.com/groups/hackingday/permalink/903494983005834/

註2:

我所使用的觀測工具是 shell script + collectd + graphite/influxdb, 我寫的 scripts 大致如下:

cputime:

#!/usr/bin/env bash

source "$(readlink -f $(dirname $0))/common.inc"

HOSTNAME="${COLLECTD_HOSTNAME:-localhost}"
INTERVAL="${COLLECTD_INTERVAL:-60}"

OUTPUT=`get_cpu_time_output`
CNT=`echo "$OUTPUT"| wc -l`
for (( i=1 ; i &lt;= $CNT ; i++ ))
do
      key=`echo "$OUTPUT" | tail -n +${i} | head -1 | awk '{print $1}'`
    VALUE=`echo "$OUTPUT" | tail -n +${i} | head -1 | awk '{print $2}'`
    echo "PUTVAL \"$HOSTNAME/me/gauge-cputime.$key\" interval=$INTERVAL N:$VALUE" 
done

xorg:

#!/usr/bin/env bash

source "$(readlink -f $(dirname $0))/common.inc"

HOSTNAME="${COLLECTD_HOSTNAME:-localhost}"
INTERVAL="${COLLECTD_INTERVAL:-60}"

NAME="xorg"
PROC_PATTERN="X -nolisten"


qpid=$( get_proc_pid "$PROC_PATTERN" )

echo "PUTVAL \"$HOSTNAME/me/gauge-${NAME}_rss\" interval=$INTERVAL N:$( get_ps_output $qpid | get_rss )" 
echo "PUTVAL \"$HOSTNAME/me/gauge-${NAME}_vsz\" interval=$INTERVAL N:$( get_ps_output $qpid | get_vsz )" 
echo "PUTVAL \"$HOSTNAME/me/gauge-${NAME}_cputime\" interval=$INTERVAL N:$( get_ps_output $qpid | get_cputime )" 

firefox:

#!/usr/bin/env bash

source "$(readlink -f $(dirname $0))/common.inc"

HOSTNAME="${COLLECTD_HOSTNAME:-localhost}"
INTERVAL="${COLLECTD_INTERVAL:-60}"

NAME="firefox"
PROC_PATTERN="opt.*firefox"


qpid=$( get_proc_pid "$PROC_PATTERN" )
if [ -n "$qpid" ]; then
  echo "PUTVAL \"$HOSTNAME/me/gauge-${NAME}_pss\" interval=$INTERVAL N:$( get_pss_by_proc_grp "${PROC_PATTERN}" )" 
  echo "PUTVAL \"$HOSTNAME/me/gauge-${NAME}_rss\" interval=$INTERVAL N:$( get_ps_output $qpid | get_rss )" 
  echo "PUTVAL \"$HOSTNAME/me/gauge-${NAME}_vsz\" interval=$INTERVAL N:$( get_ps_output $qpid | get_vsz )" 
  echo "PUTVAL \"$HOSTNAME/me/gauge-${NAME}_cputime\" interval=$INTERVAL N:$( get_ps_output $qpid | get_cputime )" 
fi

chrome:

#!/usr/bin/env bash

source "$(readlink -f $(dirname $0))/common.inc"

HOSTNAME="${COLLECTD_HOSTNAME:-localhost}"
INTERVAL="${COLLECTD_INTERVAL:-60}"

NAME="chrome"
PROC_PATTERN="opt.*chrome"


qpid=$( get_proc_pid "$PROC_PATTERN" )

if [ -n "$qpid" ]; then
  echo "PUTVAL \"$HOSTNAME/me/gauge-${NAME}_pss\" interval=$INTERVAL N:$( get_pss_by_proc_grp "${PROC_PATTERN}" )" 
  echo "PUTVAL \"$HOSTNAME/me/gauge-${NAME}_rss\" interval=$INTERVAL N:$( get_ps_output $qpid | get_rss )" 
  echo "PUTVAL \"$HOSTNAME/me/gauge-${NAME}_vsz\" interval=$INTERVAL N:$( get_ps_output $qpid | get_vsz )" 
  echo "PUTVAL \"$HOSTNAME/me/gauge-${NAME}_cputime\" interval=$INTERVAL N:$( get_ps_output $qpid | get_cputime )" 
fi

common.inc:

#!/usr/bin/env bash

function get_proc_pid(){
    local proc_pattern="$1"
    ps aux --forest | grep -e "${proc_pattern}" | grep -v 'grep' | head -1 | awk '{print $2}'
}

function get_ps_output(){
    local proc_id="$1"

    ps --no-header -o rss,vsz,cputime,etimes,nlwp $proc_id
}

function get_cpu_time_output(){
    cat /proc/stat | grep -e '^cpu' | awk '{ print $1, ($2+$4)/100.0 }'
}

function do_sum(){
        awk '{sum += $1} END {print sum}'
}

function get_pss_by_pid(){
    while read proc_id; do
        sudo cat /proc/$proc_id/smaps | grep Pss | awk '{print $2 * 1024 }' | do_sum
    done
}

function get_pss_by_proc_grp(){
	local proc_patt="$1"

	ps aux --forest | grep -A 9999 "${proc_patt}" | parse_child | get_pss_by_pid | do_sum
}

function get_rss() {
    awk "{print \$1 * 1024 }"
}

function get_vsz() {
    awk "{print \$2 * 1024 }"
}

function time2int() {

python -c "$(cat &lt;&lt;EOPY
import sys
res = 0
for line in sys.stdin:
        ary = line.strip().split(':')
        ary.reverse()
        if len(ary) &gt; 0:
          res = res + int(ary[0])
        if len(ary) &gt; 1:
          res = res + int(ary[1]) * 60
        if len(ary) &gt; 2:
          res = res + int(ary[2]) * 60 * 60
        if len(ary) &gt; 3:
          res = res + int(ary[3]) * 60 * 60 * 24
        print( res )
EOPY
)"

}


function get_cputime() {
    awk "{print \$3}" |  time2int
}

function parse_child(){
python -c "$(cat &lt;&lt;EOPY
import sys
beg = False
end = False
for line in sys.stdin:
        ary = line.split()
        cmd = ary[10].strip()
        pid = ary[1].strip()
        if cmd != "\_" and cmd != "|":
            if beg == False:
                beg = True
            else:
                if end == False:
                    end = True
                    break

        print ( "%s" % ( pid ) )
EOPY
)"
}