Ruby or JavaScript?

最近接觸到一些 RoR 的東西,覺得很有趣,以開發速度而言 RoR 真的很快,且 生態完整,但效能就是問題。反觀 node.js 族群,效能好些,但似乎開發速度還 沒那麼快。很可能接下來會變成看是 Ruby 先出現 HHVM 那樣的 VM,還是 JavaScript 開發時間也越來越短,最終真的一統天下。

以語言來說並不喜歡 JavaScript,就算到 ES6 也還是沒有很愛,但若把目 標放在 full stack,那一定得懂前端,要懂前端,就一定得把 JavaScript 學好。 既然學好了,後端時若能繼續用同樣語言開發,就能少學一種語言。

然而,若以開發時間而論,這不一定是最快的方式。因為很多 best practice (convention) 仍然存在 RoR 之中,所以花時間去學 Ruby,然後直接用 RoR,現 階段來說省下的功夫很可能可以把學習 Ruby 的時間賺回來。

語言跟應用領域,往往有很強的相關性。例如 Linux 之於 C,userspace 的基礎 架構如 database, browser engine, server 之於 C/C++,資料科學之於 R。但 也有相當通用的語言,例如 Python 以及 Java 用途都相當廣,Python 開發快, Java 效能好。所以說,若打定主意專精於 web 領域,那為了 RoR 學習 Ruby 是 很合理的。

但這優勢其實可取代。因 JavaScript 在 web 應用太廣泛,而基於 isomorphic 的需求,JS 會很自然的往後端與 mobile app 那邊長。且 async non-blocking 的天性已經在語言中,這並不只是效能問題,還牽涉到 scale up 的難易程度。 若把 node.js 的 opinionated web framework 例如 sails.js 開發到像 RoR 一 樣的方便,那麼全部轉換到 JS 為基礎的全端架構,有明顯的優勢。

這件事適合熟悉 RoR,知道其中哪些特性相對於其他解決方案是比較有優勢的工 程師去做。相對於寫一個 VM 可能要公司的能力才做得到,把 sails.js 這類 framework 往 RoR 的方向改善則只需要幾個工程師就行了,後者或許比較容易發 生。

另一個有趣的可能性是,Ruby 到 WebAssembly 的 interpreter 被發展出來且內 建到瀏覽器。由於 client 端運算能力日漸強大,interpreter 慢些也還好,說 不定 RoR 也可以用目前的方式往前端長,直接用一樣的方式寫前端行為,打包成 一整個 full stack web app,這樣感覺也是很厲害。

Method as Argument in Go

說來慚愧,這麼好用的作法竟然今天才發現。

一直以來都覺得 net/http 有點難用,http.Handlehttp.Handler,定義是

1
2
3
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

所以非得有個 method ServeHTTP,但如果要在一個 Handler 底下有好幾個 method 用以處理各類 request,進入點都是 ServeHTTP 就有點麻煩。這時用 http.HandleFunc 就比較合理,只要傳入一個 function:

1
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))

然而,這類函數經常需要 context 才能執行,例如把資料庫連結存在 type 裡。 以往碰到這情況會利用 closure 的特性,在 method 中宣告符合介面的函數,然 後再傳給 HandleFunc,現在才知這完全是不必要的。舉例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package test

import (
"log"
"net/http"
)

type Model struct {
// Your database connection, etc.
}

func (t Model) Foo(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("Foo"))
}

func (t Model) Bar(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("Bar"))
}

func Example() {
model := Model{}
http.HandleFunc("/foo", model.Foo) // method as argument
http.HandleFunc("/bar", model.Bar)
log.Fatal(http.ListenAndServe(":8080", nil))
}

實在是蠻漂亮的作法,搭配 Gorilla web toolkit 很容易就可以寫出 簡單明瞭的 server side 程式。

寫 Mobile App 簡單嗎?

之前面試 mobile app 工程師時,一位人選告訴我,他不想在原先公司(頗知名, 薪水也高)繼續做下去的原因,是因為 Android SDK 他已經很熟,再寫覺得也沒 有進步空間,所以想出來學一些其他的,例如底層一些的技術。

這似乎是個常見的說法,從剛入職場就常聽到,但可能年歲漸長,有些不同體悟。 例如寫 driver 就是那樣,系統整合就是那樣,所以想學別的。這其中必定有什 麼不對的地方,但是是什麼呢?

問題出在需求

Android SDK 本來創作出來的用意就是希望盡可能簡單,因為 Google 希望能讓 開發 Android app 是簡單的,吸引更多 developer 投入,才能讓生態圈蓬勃發 展。但開發界面上的簡單、API 的簡單,不表示功能上的簡單。如果覺得 SDK 學 完了,工作就變得太簡單,那應該是因為需求太簡單了。

為何需求是簡單的

Google 新出的 Inbox 功能簡單嗎?Facebook app 簡單嗎?要看由什麼角度來看。 他們致力追求的都是使用界面上的單純,但往往越單純的界面,後面的機制越複 雜。一個極度好用的功能,往往實作上相對困難。所以這裡延伸出的問題是,為 何在知名大廠,所面對的,仍然是實作上的簡單。

Mobile App 之外也經常如此

例如在 ODM 廠,處理來自各廠商的 BSP,做久了架構熟了,釐清的方式也熟了, 有問題找原廠支援,工作就開始容易。這表示 Android 系統面簡單嗎?問題恐怕 出在,為何沒辦法做到複雜的東西。BSP 已經是相對應用端,使用一個本來就是 為了方便使用者的 package,本來就不該困難。

廣 vs 深

於是可以發現,很少有真正簡單的領域,往往問題出在身處那領域的什麼位置。 台灣求深的廠商不多,相對的職位也不多,往往造成求深不如求廣,我自身就是 個例子。但這是個惡性循環,在職涯選擇上,或許可針對某特定領域,透過自我 要求,追求卓越,盡力往研發的上游端前進。

大勢不佳,但人才全球流動,以海盜心態武裝自己,拋棄故鄉,該去哪就去哪, 應該是概率最佳的選擇。若在本土發展,另闢蹊徑,總是比較困難的。

Solarized color 8 (bright black) 與背景雷同問題

使用 Solarized 這套配色對眼睛蠻 舒服的,而且有 light 或 dark 型態可以切換,跟 Base 16Zenburn 比起來,我是最喜歡 Solarized。但他有個長久以來的問題,就是在 terminal 下,background 跟 color 8 (bright black) 是一樣的顏色,都是 base 03。Node.js 很多套件在 彩色顯示時都會用到 bright black,一旦用到就會跟背景同色,看不見。

之前我的解法是把 background 設為 color 0,也就是真的 black,但在 Solarized 設計上,這兩色是鄰近的而不是對比色,所以雖不是同色,但仍然很 難看到。後來參考了內建 Solarized theme 的 Terminator 解 法,是直接把 color 8 設定成 #6a848a,這不論在 light 或 dark 模式下, 反差都很清晰,是目前為止我覺得最好的解。

相關討論可以看 issue 220,這問 題存在很久了但作者就是不爽解決。

多核心下 nginx - node.js 的組合

目前我們網站都是寫在 node.js 上,然後用 nginx 當 reverse proxy 來接。

以四核然後有 VT 為例,nproc 看到的會是 8,此時一些相對應的 nginx.conf 設定:

1
2
worker_processes 8;
worker_cpu_affinity 00010001 00010001 00100010 00100010 01000100 01000100 10001000 10001000;

worker_cpu_affinity 的值是按照 /proc/cpuinfo 內容來決定的,用來做範例的機器排列是 processor 0~3 對應到 core 0~3,然後 processor 4~7 又對應到 core 0~3,所以 processor [0, 4], [1, 5], [2, 6], [3, 7] 各自屬於同一個 core。同一個 core 可以允許 processes 互換,所以上面 worker_cpu_affinity 的意思是指定 worker 0 可以跑在 0, 4 兩個 CPU 上(第 0 跟第 4 bit 為 1),然後 worker 1 也是跑在 0, 4 上面。依此類推。

node.js 部份並沒有用 forever 來跑,而是用 upstart 來看住。相關的一些設定長這樣:

1
2
3
4
5
6
7
8
9
10
11
12
# portal.conf

start on runlevel [2345]

task

script
NPROC=$(nproc)
for N in $(seq $NPROC); do
start portal-worker N=$N
done
end script

用 portal 去跑好幾個 portal-worker 起來。而 portal-worker 則是會看執行時給的 N 值來決定怎麼跑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# portal worker

manual

respawn

instance $N

env NODE_ENV=production
setuid www-data
setgid www-data
chdir /home/johndoe/portal
script
test $N = 1 && exec taskset 0x11 node server.js -c config-1
test $N = 2 && exec taskset 0x11 node server.js -c config-2
test $N = 3 && exec taskset 0x22 node server.js -c config-3
test $N = 4 && exec taskset 0x22 node server.js -c config-4
test $N = 5 && exec taskset 0x44 node server.js -c config-5
test $N = 6 && exec taskset 0x44 node server.js -c config-6
test $N = 7 && exec taskset 0x88 node server.js -c config-7
test $N = 8 && exec taskset 0x88 node server.js -c config-8
end script

portal-worker 的 log 都會在 /var/log/upstart 底下,例如 N=1 時,就會是 /var/log/upstart/portal-worker-1.log。另外可以看到 taskset 的邏輯基本上跟 worker_cpu_affinity 是一樣的。後面 -c 是自訂參數,用來指定不同設定檔,例如規定 node process 聽在不同的 port 上。這樣搭配 nginx 裡頭 upstream 功能就可以平均分配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
upstream portal {
server 127.0.0.1:5001;
server 127.0.0.1:5002;
server 127.0.0.1:5003;
server 127.0.0.1:5004;
server 127.0.0.1:5005;
server 127.0.0.1:5006;
server 127.0.0.1:5007;
server 127.0.0.1:5008;
}

server {
# ...snipped...
location / {
proxy_pass http://portal;
proxy_redirect default;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $hostname;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

值得注意的是,upstart 在 12.04 有個 bug,Supplementary groups not set for user jobs,這會導致上面的 setuid www-data 不如預期,例如 user www-data 的 supplementary group 有一個 groupabc,然後某檔案設定為 groupabc 可以寫,結果 upstart 跑起來以後是寫不進去的。 解法之一是改用 sudo -u www-data -E -- 來跑。14.04 就沒這問題了。

MongoDB 搬空的 collections

show collections 會看到 system.indexes,只 dump 這個 collection 的 話,restore 時就會做出所有 collections 但是是空的。假定每個 collection 至少都有一個 index 的話。這在做測試用的 database 時很方便。

1
2
mongodump -d mydb -c system.indexes
mongorestore -d mydb dump/mydb

Golang import path `all`

go help packages 可以看到三個 reserved names 如下:

1
2
3
4
5
6
7
8
- "main" denotes the top-level package in a stand-alone executable.

- "all" expands to all package directories found in all the GOPATH
trees. For example, 'go list all' lists all the packages on the local
system.

- "std" is like all but expands to just the packages in the standard
Go library.

實際使用起來有如下的一些可能,不過最好用 root 裝 go,免得影響到 std libraries.

  1. 更新全部 go get -u all
  2. 清掉全部 build 出的 libraries go clean -i all
  3. install 全部 go install all

善用 exec redirection

寫 shell script 時常碰到 list 不好處理的問題,裡面如果包含空白之類的 IFS 區隔,一不小心就會發生意外。例如目錄中包含 My Music 這個目錄, 用以下的 shell script 處理 ls -1 時,就會分別印 MyMusic

1
2
3
for f in `ls -1`; do
echo $f
done

解法可以用 read 讀入變數:

1
2
3
ls -1 | while read f; do
echo $f
done

如果需要複雜一些的篩選,甚至處理有 \n 在裡頭的檔案,那可以用 find -print0

1
2
3
4
find -mindepth 1 -maxdepth 1 -print0 | \
while read -d $'\0' f; do
echo $f
done

但這又有個討厭的地方,就是在迴圈中如果需要用到 stdin 就出錯了,因為 stdin 是接到 findstdout。解法會動用到 process substitution 跟 exec redirection 用來改變環境的 file descriptor 設定。

1
2
3
4
exec 3< <(find -mindepth 1 -maxdepth 1 -type d -print0)
while read -d $'\0' f <&3-; do
echo $f
done

主要差異是第一行,<(COMMAND) 的用意是把 find 的輸出接到一個 named pipe 或者是 /dev/fd 之下當成一個檔案(詳情請參閱 info bash),而 exec 3< 的意思則是用 fd 3 打開這個檔案。下一行 read 後面接的 <&3- 是把 fd 3 move 到 read 的 stdin,這樣就不會影響到 loop 中的 stdin 了。

中文字型大不易

個人習慣的閱讀用字型為 sans-serif,為求搭配,中文字也想採用無襯線的黑體 或圓體。然而這件事情卻是超乎想像的困難,原因在於以下需求:

  1. 繁體部份不得為 GB 18030 寫法。(參考 GB 18030 就在你身邊 )若非為此,文泉驿系列字型,就是很好的選擇。

  2. 簡繁字體需同源。這是由於 UTF-8 中簡繁字有許多共用,若 fontconfig 先 指定繁體字集,再 failover 到簡體,而兩者風格不一致,那麼在顯示簡體文章 時,若在繁體字集中先找到字,就會直接使用,找不到的話就會用簡體字集。這 會導致整個版面交錯排列來自兩個字集的字型,此時若不一致,就會非常難看。 譬如這種慘況:

在 Ubuntu 13.10 中直接可找到的繁體黑體或圓體,比如 fonts-cwtex-heib 或 fonts-cwtex-yen 都有跟簡體字型不一致的問題。反倒是如果不要求要無襯線字 體,fonts-arphic-uming 就已經解決了,因為簡、繁、香港字型皆有提供。

Apple 的 黑體-繁黑体-简 , 以及微軟的正黑體,其實都符合這些需求,但 Ubuntu 套件中的中文字型則不然。

Update 2018/08/21:

Google 的 Noto CJK 符合以上所有需求, 且有襯線與無襯線皆提供。

Big Integer in JavaScript

應該很多人都知道 JavaScript 的數值型別沒有整數,一併都是 double precision 64-bit 浮點數。但實際運作起來還是很討厭啊:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
> b = parseInt('9007199254740992', 10); // 2^53
9007199254740992
> typeof(b);
'number'
> b + 1;
9007199254740992
> b = parseInt('9007199254740994', 10);
9007199254740994
> typeof(b);
'number'
> b + 1;
9007199254740996
> 'wtf?'
'wtf?'
> b = parseInt('18446744073709551616', 10); // 2^64
18446744073709552000
> isNaN(b)
false
> isFinite(b);
true

Reference: http://ecma262-5.com/ELS5_HTML.htm#Section_8.5

其實已經不準確了,但沒有錯誤訊息。這裡我本想做的事情是 JSON parse/stringify facebook user id,據 Facebook blog 說的長度是 64 bits. 當然會有人建議乾脆存成字串啦,但除非移民太空,不然其實不可能超過 64 了。在找支援的套件時,都不是很熱門,感覺怕怕的。後來看到 json-bignum,內部是直接用字串存,簡單,看了一下覺得還可以,但當然在比較之類的就要小心了,例如相等的話要先 toString() 之後才能比。