發表文章

從 IT 轉型看微服務

從電腦、資訊科技開始普及後,每過一段時間,企業界就會開始出現新的 IT 轉型計畫,以整頓整個資訊系統、商業運行模式,然後試圖找出下一個世代的新方向。每一個具規模的企業,都生怕與新世代的技術和商業模式脫鉤,於是砸重金、拚人力,說什麼也要追上世界的腳步。當然,中間的技術供應鏈也不斷在改變,上一世代的王者或是贏家,在下一個世代可能不一定跟得上,因此身為終端用戶的企業,每當面臨新的 IT 轉型計畫,往往也跟無頭蒼蠅般, 不知所措甚至不小心會做錯了決定。回到十幾二十年前,你也許可以有機會一次性把一個大系統整個替換掉。但在今天資訊系統高度介入管理的時代,版本更迭無數次的平台,沒有一個系統能在一夕之間被整個換掉,哪怕你的品管和測試做得非常足夠,甚至是超標達成,也哪怕你事先的需求訪談談到祖宗十八代都瞭然於心。這常常是系統設計者和規劃者的誤區,看見許多表面問題,於是設計了一套全新、看起來完美的解決方案。而剩下沒照顧到的部分,都覺得只要能配合測試就可以邊走邊補足。事實上,現代資訊系統的維度和複雜度,已經超出人類的想像,太多的例外、不可控、溝通執行問題,遠遠超過理論上的執行方法。所以從系統規劃設計的角度來看,我們總是要慢慢來,一段段改善、替換,最終達成全面換血。而且越是龐大複雜且重要的系統,越要切割得越細,透過降維度的方法,來慢慢替換。所以我們在討論IT轉型,從一個肥大的系統換到另一個肥大的系統,已經不會是選項,系統架構的去耦合、高擴展性甚至是服務不中斷才是考量的重點。這也是為什麼「微服務架構」在近年來特別熱門,微服務真正在討論的不是容器化,而是在討論的是怎麼讓一個龐大的系統,更容易受到管控、維護和擴充,除了可以更容易實現災難隔離、例外狀況、整體高可用性,也可以更有彈性做各種變化。更重要的是,微服務架構下,其系統風險管理上,從在開發階段開始,每個部分就已經受到隔離和管控。也由於每個被拆解後的系統都非常小,遵循 SRP 原則,在開發維護上相對容易,也可以保證每個單元有更低的問題發生率。至少,你不會因為一顆螺絲或換一顆螺絲,導致整個塔倒塌。不過微服務架構卻也不是說要做就能做,對於規劃設計者來說,他必須很了解各種新舊技術議題、有能力做實務執行面的評估,也需要耐心做各種服務拆解梳理和引入正確的 Pattern 及技術,甚至很多時候要有走一步是一步且可以快速反應決策的能力。這些都不是隨便讓工程師…

GitLab DevOps:Kubernetes 整合的 Applications 裝不起來怎麼辦?

圖片
我們的平民化旅遊黑卡秘書服務 LiMaGo,系統已經開發了兩年,有一定的複雜度,一直以來在維護上其實都有一定困難。此外,不知道是好消息還是壞消息,由於我們開始拓展日本等其他國家的業務,也加強了行銷通路,所以業務數字持續增加,導致各種功能和後勤機制也持續在擴張。還有,許多隱藏尚未公開的新計畫,整個系統複雜度不斷的再增加,慢慢的一切都開始混亂。所以,為了重整系統,以及重建開發團隊的各種資源和管理流程,開始試圖引入了 Kubernetes 和 GitLab,並試著將 DevOps 流程給搭建起來。畢竟,自己的另外一家公司,是微服務和容器化的解決方案供應商,自己手上的東西要是沒有導入,也太講不過去。還好轉換的問題不太大,LiMaGo 的系統從第一天設計,就是採用微服務架構(Micro-service Architecture),甚至當時還設計了一套框架「Engined」,可以在 Node.js 上更容易開發微服務架構,只要專心在容器化和 DevOps 的工作上即可。於是動手把 Kubernetes 架好,也安裝好 GitLab CE。下一步,就是把 Kubernetes 的設定加入 GitLab,讓 GitLab 可以把 GitLab Runner 和 DevOps 相關機制跟手上的 Kubernetes 整合起來。糟糕!Applictions 裝不起來!原本一切很順利,但在 GitLab 設定頁裡面的最後一個步驟「在 Kubernetes 上安裝 Applications」遭遇到莫名失敗,一開始是 Ingress 裝不起來,後來發現 Prometheus 也裝不起來,如下圖: 透過 kubectl 直接去 Kubernetes 裡面挖 logs ,得到了錯誤訊息:$ kubectl logs install-prometheus -n gitlab-managed-apps -f + helm init --upgrade Creating /root/.helm Creating /root/.helm/repository Creating /root/.helm/repository/cache Creating /root/.helm/repository/local Creating /root/.helm/plugins Creating /root/.hel…

有趣的洗牌演算法

圖片
最近因為一些專案,所以需要實做一些撲克牌的洗牌機制。雖然這個動作看起來簡單,但其實對於開發者來說相當有趣,因為真的除了做這種牌類遊戲之外,平常很少用到這樣演算法,也由於有太多種做法,不免著迷於其中。

洗牌目的就是讓結果隨機、不能預期,只不過雖然很多遊戲同樣都是圍繞在亂數產生上面,但撲克牌遊戲(或麻將遊戲)最大的不同,就是同一排組每次發出來的牌,一但發過了就不會再出現一次。這一點,和每次都可以出一到六點數的骰子遊戲,就完全不一樣,不是隨機出一個亂數就可以搞定。
準備工作:先準備個牌組 開始前,先準備四種花色、A 到 K 的牌組,使我們可以以 0 至 51 的號碼去取得任意一張牌。
const suits = [ 'S', 'H', 'D', 'C' ]; const points = [ 'A', 2, 3, 4, 5, 6, 7, 8, 9, 10, 'J', 'Q', 'K' ]; const cards = []; for (let i = 0; i < 4; i++) { for (let j = 0; j < 13; j++) { cards.push(points[j] + suits[i]); } } console.log(cards[10]); // 10S(黑桃10) 方法一:硬幹 最直覺的方法,不外乎就是不斷產生 52 張牌的亂數(0 ~ 51),然後檢查這張牌發過沒,如果牌發過了就重新產生一個新的亂數,持續這個步驟。
let shuffledCards = []; while(shuffledCards.length != 52) { // 取得 0 ~ 51 的亂數 let idx = Math.floor(Math.random() * 51); let card = cards[idx]; // 檢查這張牌是否已經出現過 if (shuffledCards.indexOf(card) !== -1) continue; // 沒出現過則放入陣列 shuffledCards.pus…

打造自己的 Node.js Transform Stream

圖片
熟悉並學習實作 Node.js Stream,在 Node.js 開發者生涯裡是一件很重要的事,尤其在資料處理的工作上更是需要運用 Stream。在這些應該用的情境下,若不懂得使用 Stream,我們所開發出來的程式其執行效能及穩定性會相當令人擔心。而如果你從未自己實作過 Stream,從 Transform Stream 開始入手是一個好選擇,也是一個非常實用的開發技巧。更多關於 Stream 的說明,可以參閱 Node.js 官網上的文件:https://nodejs.org/api/stream.html什麼是 Transform Stream?你可能知道 Node.js 裡有多種 Stream 的機制,但其實主要是 ReadableStream 和 WritableStream 兩種基本 Stream 的組成和變化。而對一般開發者來說,最常自己實作的是 Transform Stream,你可以想像這是一個產品生產線上的加工器,進入 Transform Stream 的資料會被加工後輸出。而以一個 Stream 而言,Transform Stream 同時具有 ReadableStream(讀入)和 WritableStream(輸出)的特性,俗話說「左耳進右耳出」就是其最佳的寫照。舉一個 Node.js 官方的例子,利用 Gzip 的 Transform Stream 將通過的資料流進行壓縮:const zlib = require('zlib'); const gzip = zlib.createGzip(); const fs = require('fs'); const inp = fs.createReadStream('input.txt'); const out = fs.createWriteStream('input.txt.gz'); inp.pipe(gzip).pipe(out); 實作第一個 Transform Stream先不必暸解太多 Stream 的專有名詞和機制,若想要實作一個標準的「耳邊風」Stream,程式碼如下:const Transform = require('stream').Transform; const util = requi…

救火奇兵之 Android USB Host API 反應遲緩

圖片
話說,Android 在某個版本後,開始提供了 USB Host API,這代表開發者可以不必再用 NDK 和硬梆梆的 C 語言去開發 USB 裝置的驅動程式,而可以完全用 Java 來開發。但是,現實往往沒有這麼美好。日前,就協助了一個案子,解決了一個 USB 裝置驅動程式的問題,起因就是客戶用了 Android USB Host API 去控制 USB 裝置,但發現 USB 裝置的回應一直不如預期,有時像是掉資料,有時像是沒反應。而同樣的控制邏輯,用純 C 開發的驅動程式配上 libusb 就完全正常,所以我們相信肯定不是控制邏輯上的問題。剛開始,大家都懷疑是 Java 本身的問題,懷疑是不是 JVM 執行驅動程式太慢,而造成接收 USB 裝置的資料時來不及。但我一直保持著懷疑,因為 USB 裝置回傳的資料並不多,如果 JVM 本身的效能連處理這幾 KB 的資料量都如此差,就實在是太可笑了,我無論如何不相信。還好最後還是解決了,雖然過程曲折。USB Request Block 的 16KB 限制事實上,每次最多傳送 16KB 資料,是一個 bulk transfer 的 URB 限制,使用 Android USB Host API 就會直接遭遇到這個問題,所以不管用什麼方法,怎麼收資料,只要資料太大,你最多一次就只能收到 16KB。多次收資料所發現的延遲問題當然,既然一次最多只能收 16KB,我們可以分多次向 USB 裝置要求收資料,但就會發現會莫名掉資料。從 USB 的分析器上來看,該有的命令都有,但就是有掉,後續的資料不管怎麼取都是 0。後續資料為 0,在這個案子的 USB 裝置設計上是可以理解的狀況,因為該 USB 裝置只會保留資料一小段時間,然後就會清空,所以若之後跟它要任何資訊,他都會回傳空的東西回來。這很明顯,就是我們要資料的過程時間,已經超過了該 USB 裝置正常的情況。而從收到的資料來看,有收到的資料,經驗證過後發現是斷斷續續的,中間有漏資料。經過測試,發現是每個命令之間的間距時間太長,因為該 USB 裝置會不斷復寫一段緩衝區,如果我們太慢去要資料,那段緩衝區就會被新的資料蓋掉,理所當然的,我們就會漏掉一些資料。經過各種測試紀錄,很明顯的,Android USB Host API 並沒有這麼聽我們的話,每當我們下命令或進行控制時,他並沒有馬上送…

JavaScript async/await 的奇淫技巧

圖片
JavaScript async/await 的奇淫技巧
話說,最新的 ECMAScript 已經引入了 async/await 語法,讓開發者可以更容易控制非同步的程式邏輯,換言之,我們可以減少許多 callback 的使用,讓 JavaScript 這種單線程、事件驅動的程式語言更易讀、好寫。

關於 async/await 的基礎使用,有興趣的人可以參考舊文「JavaScript 好用的 async 異步函數!」,而本文將探討更多實際使用上的小技巧。

另外,瀏覽器不一定有支援 async/await,你可以在新版的 Node.js 上面測試本文的內容。

呼叫 async 函數與一般的函數沒有差別
想像一下,async 函數就是一個在執行後會回傳 Promise 物件的「普通函數」,和一般常見的函數的使用差異,僅僅只是 async 函數在執行後「不是回傳函數執行結果」。這代表我們可以把 async 函數當作一般函數來呼叫使用,用法一模一樣。

async/await 與 Promise 是可以共通的
非常有趣,async 函數與 Promise 其實能夠共通,這代表我們可以玩一些特別的組合技。所以,若要把 async/await 玩得通透,建議你盡量熟悉 Promise 的各種用法。

實現 delay 函數
過去因為單線程和事件驅動的關係,JavaScript 不可能實現一個沒有嚴重副作用的 delay 函數,所以取而代之的是使用 setTimeout() 加上 callback 來實現「一定時間後執行什麼工作」的需要。

不過來到 async/await 的世界後,我們可以一行行描述程式邏輯,無論是不是同步(Synchronous)的程式碼,所以我們可以用 Promise 來包裝 setTimeout(),以實現一個在 async 函數裡可以跑的 delay 函數:

// 實現一個等待函數 const delay = (interval) => { return new Promise((resolve) => { setTimeout(resolve, interval); }); }; const main = async () => { console.log('Starting...…

自幹 JavaScript 的 Tail Call Optimization

圖片
ECMAScript 6 開始,規範中出現了一項被稱為「尾呼叫優化(Tail Call Optimization, TCO)」的優化技術,這讓開發者可以在函數的執行過程中,減少 Stack Frame 的數量,進而提升效能。TCO 尤其是在遞迴這種不停呼叫自己或新函數的工作上,能得到最大的優化效益,能提升遞迴的執行效能如同迴圈一樣。

只不過很可惜的是,截至本文發稿前,大多數瀏覽器及 JavaScript 引擎尚未支援這項技術。但我們還是可以自幹並模擬一個 TCO 的行為,雖然比起語言本身、編譯器(Compiler)及虛擬機(VM)層面的實現,效果差了些,但仍然可以減少 Stack Frame 的數量避免達到 Stack Frame 的數量上限。

什麼時候會啟用尾呼叫優化機制?
如果 JavaScript 引擎有支援,通常一個函數執行到最後一行 return 時,是回傳另一個函數的執行結果,就會啟用 TCO 機制,如:

const f = () => { return 999; }; const g = () => { // 執行並直接回傳 f 函數的執行結果:會啟用尾呼叫優化機制 return f(); }; g();
但要注意的是,回傳的「必定為函數的直接回傳值」,所以下面這些寫法不會啟用 TCO 機制:

// 不會啟用 TCO 機制的設計 const g = () => { return f() + 1; }; // 不會啟用 TCO 機制的設計 const g = () => { let ret = f(); return ret; };
創造一個跑不完的函數
首先我們先創造一個肯定跑不完的遞迴,然後改善它:

const func = (x) => { // 讓他跑 10000000 次 if (x === 10000000) return x; return func(x + 1); }; let ret = func(0);
理論上,如果你直接執行上述程式碼,會得到 stack size 超過上限的錯誤訊息:

RangeError: Maximum call stack size exceeded
簡單模擬實現自己的 TCO 效果
簡單…