Tuesday, October 31, 2017

讀網路上的技術文章有感

身為中文的使用者,偷懶的時候,還是會傾向先讀一下中文的資料。此外,國外的一些進階者寫的東西有時候相對比較難、多用了專有詞彙,我又會偷懶看一般人寫的心得。

然後就出現了一些問題:以訛傳訛

比方說像 microservices 。我看了很多文章都把 microservices  的重點放在 docker 、放在 container ,而不是 database 的獨立性:『每個微服務應該有自己獨立的持久性資料儲存,而且這些資料只能透過微服務本身的 API 來存取。』這些搞錯重點的文章居然還一大堆,無形中也助長了人的無知、誤了好多人。

此外, concurrency model 也是一個例子。Golang 很紅。然後就會有一些介紹 Golang 的文章錯誤地把 Erlang 的 Actor model 講成說這跟 Golang 的 CSP model 是一樣的、差不多的。這也是差很多啊…。

Friday, October 27, 2017

要不要用 cronjob ?

要不要使用 cronjob ,我覺得這是一個設計的問題:

我最近在重構的一隻 Script ,它做了下列幾件事:
(1) 每分鐘執行一次,依賴 cronjob 來啟動
(2) 每次執行,它都會呼叫數個外部的 http API

如果考慮布署的方便性,其實我可以考慮把 cron 的部分,直接在 script 裡做掉。這樣子 Devops team 就不用設定 cronjob ,Devops 的成本會比較低。此外,cronjob 因為是系統共用的,有時候運氣不好,就被改掉了。

另一方面,如果是讓 script 自行做 scheduling 的話,我就必須考慮 exception handling 。因為呼叫 API ,有時候網路如果斷掉,就會產生 exception 。產生 exception 的話,有時候整個程式就 crush 掉了,甚至連下一週期也受到波及。然後,我想有想到 Erlang 的設計就是每個 process 都是完全獨立的。任何一個 process 就算 crush 掉也沒有關系。照這個想法的話,用 cronjob 來跑的 script ,每一次的執行也都是獨立的 執行環境,確實也比較簡單。

上網查了一下:似乎也有其它人跟我有類似的疑問? 解法也很妙,還分成兩類:
(1) ofelia 的解法是,重新實作一個類似的 cron 的程式。但是適合跟 docker 合作。
(2) supercronic 的解法則是,實作一個適合在 docker 內部執行的 cron

Thursday, October 26, 2017

Multiple bindings were found on the class path

最近在使用 clojure 來開發和 Apache spark 相關的程式,有使用到 org.apache.spark/spark-core_2.10 這個 library 。但是,等到我要測試的時候,一啟動 lein repl 之後,問題就出現了,

vagrant@owl-docker:~/workspace/src/github.com/clo/october/intowow$ lein repl
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/home/vagrant/.m2/repository/org/slf4j/slf4j-log4j12/1.7.5/slf4j-lo
g4j12-1.7.5.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/home/vagrant/.m2/repository/ch/qos/logback/logback-classic/1.1.3/l
ogback-classic-1.1.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]
nREPL server started on port 37442 on host 127.0.0.1 - nrepl://127.0.0.1:37442

這個問題是這樣子,我的 clojure 專案因為是套用 luminus ,所以預設使用的 log library 是 logback 。但是不知道引入了什麼 library 之後,classpath 裡又多了一個 org.slf4j/slf4j-log4j12 。而這個又很不巧地先被引入了。所以最後專案使用的 log library 就變成使用 org.slf4j/slf4j-log4j12 而不是 logback 了。

這個問題,我研究了一陣子之後,想了一個解法:
(1) 先準備 lein-deps-tree 這個 leiningen plugin
(2) 執行之後,就可以看到專案的 dependency graph 。

果然,跟我預想的相差不遠,我最後引入的 library 不小心包含了 org.slf4j/slf4j-log4j12
最後,我的 project.clj 的其中一行修改成這樣子:
[org.apache.spark/spark-core_2.10  "1.1.0" :exclusions  [org.slf4j/slf4j-log4j12]]

引入 spark 的同時,不要引入它自帶的 log library

Sunday, October 15, 2017

抽象層滲漏的解決方案 --- 容許客製化的抽象層

「抽象層滲漏法則」(The law of leaky abstraction) 這個詞彙,在我寫這篇 blog 的時候,中文的維基條目還沒有創建出來,所以我的參考資料主要都是來自英文的資料。根據我查的資料,這個法則是因為 Joel Spolsky 寫了文章討論它,所以變得很流行。然而,我覺得真正有趣的部分是英文維基條目裡的一小段話。

An earlier paper by Kiczales describes some of the issues with imperfect abstractions and presents a potential solution to the problem by allowing for the customization of the abstraction itself.

「容許對抽象層的再客製化」是抽象層滲漏的可能解法?這是什麼概念?

(1) Lisp 直譯器可以視為是控制電腦的抽象層,而且這個抽象層是容許客製化的。Lisp 最著名的 macro 功能,就可以讓 Lisp 的使用者自訂自己需要的 syntax 。 換言之,Lisp 直譯器是可客製化的抽象層。Paul Graham 對於沒有 Lisp macro 的程式語言,下了如下的註解:  abstraction that not powerful enough

(2) edn 格式容許使用自訂自己需要的資料型態,並且嵌入自訂的 parser 。所以這個抽象層也是可以客製化。在 edn 的介紹文件中,它用了不同的方式解釋為什麼需要 edn ,原文大概是說,如果一個格式沒有包含足夠的資料型態,使用這個格式的程式就勢必得自行實現這些型態的特殊處理 (抽象層滲漏)  ,所以就會造成 context-dependency

(3) PostgreSQL 的 stored routine 和 user defined types 也是容許使用者訂定自己的特殊規格。這個資料庫也是一個可以客製化的抽象層。

由這個概念去思考之後,我覺得將軟體的實作設計成「容許客製化的抽象層」,算是滿重要的概念。有了這種概念,才容易設計出成熟、好用的產品。

類似的例子還有許多,比方說:
  • Emacs, Atom 編輯器容許 plugin
  • Chrome 可以安裝 plugin
  • C 語言可以嵌入組語
  • Linux kernel module
  • Redis 也有 Redis module
  • Excel 可以嵌入 VBA

session-based authentication v.s. JWT

最近做的一個 side project ,我用 clojure 的 buddy 模組,來實現使用者認証 (Authentication) 的功能。 clojure 的 buddy 模組的一個長處,是支援 JWT (JSON Web Token) ,也因為如此,我特別仔細研究了一下,到底傳統的 session-based authentication 與新的 JWT 有什麼分別?

JWT 和 session-based authentication 相同的地方是:browser 送 request 到 server 時,都在 http request 裡增加了一些資料,讓 request 變成 stateful。不同的地方是: session-based authentication 夾帶資訊的方式是用 cookie 來攜帶 session-id ,使用者的資訊是存放在 server side 的 session 裡頭,安全性比較高。 JWT 夾帶資訊的方式,是在 http header 裡放了 token ,而 token 內容就直接存放了加密過的「使用者資訊」。也因些, JWT 的作法,並不需要 server-side session ,集群 (cluster) 也因而得到了更好的「水平擴展性」(horizontal scalability)

參考資料

但是,實務上,大部分的情況,也可以不去使用 JWT 。因為就算是 session-based authentication 也有很簡單的方式就可以支援超大量的使用者。主要的作法有兩個:一種是把 session 放在 redis 裡,這樣子多台的 application server 就可以透過 redis 來取得使用者的 session 資料。 另一種作法,是在前端的負載均衡器,設定所謂的 sticky session 。讓負載均衡的分配 http 請求,可以讓同一個 session 的請求,永遠被分配到同一台的設備上。

在 12 factor app 的觀點, JWT 是比較好的作法, sticky session 是最糟的作法。但是,從 security 的觀點, session-based authentication 有比較安全一些。JWT 的話最好要搭配 https

Thursday, October 12, 2017

在 Golang 裡使用類似 Clojure 的 with-redefs 技巧

在寫 Golang 的 unit test 的時候,我覺得很不方便的一點就是沒有辦法像 clojure 一樣,利用 with-redefs 將 symbol 重新定義,讓本來有外部依賴的函數可以很快地被測試。但是,公司的資深工程師 Mike 教了我一招。

基本的概念就是這樣子:
1. 在 Golang 裡,可以被重新定義的東西就是 global variable 。
2. 如果要測的某個函數,它本身沒有寫好依賴注入之類的東西,又恰好呼叫了一個有 complex dependency 的函數。就把這個有 complex dependency 的函數用 global variable 來加上一層間接性。

Tuesday, October 3, 2017

curl, netcat, API testing

最近常常在測試一些同事寫的 API ,整理了一下自己覺得好用的小技巧:
(1) mock server
有時候想要看一下,自己生成的 curl 到底是產生怎麼樣的 http request ,產生怎樣的 body ,就需要先架一個簡單的 mock server 。

#如果需要看 http POST 會需要用 netcat
sudo nc -l 8000     #開啟一個 8000 port 的 tcp listener
# Python v2.7
python -m SimpleHTTPServer
# Python 3
python -m http.server 8000

(2) 快速生成 curl 指令
如果是對於既有的 web application 的話,可以利用 chrome 來取得。
Ctrl + Shift + i -> Network -> Copy as cURL 

(3) curl 常用的參數
-s  =>  silent, quiet mode 
-d =>  用來指定要放進 http request 的 body 的內容。
           用了 -d 選項之後,預設的 Content-Type 是 application/x-www-form-urlencoded
           如果要使用客制化的選項的話,要再加上 -H 參數。
-F =>  用了 -F 選項之後,預設的 Content-Type 是 multipart/form-data
           這個選項通常是用於「大量的資料」或是 binary data
-H => 用來指定 http  header 。常見的用法有:
           指定客戶端送出的資料格式                          -H 'Content-Type: application/json'
           告訴伺服器「客戶端想接受的資料格式」  -H 'Accept: application/json'

(4) 從 server side 的 source code 來理解
常見的 server side framework 可以處理的 http content type 通常是下列四種:
  • application/json
  • application/xml
  • application/x-www-form-urlencoded
  • multipart/form-data
(5) 由 server side 的 source code 來猜測, client side 的 curl 該如何產生?
若 server side 是用類似 parseForm, formValue 之類的函數來處理。curl 會是如下的形式:

curl -d 'title=標題&content=測試'  http://localhost:8000/