Target Audience

本系列文寫給 ”使用過 Jenkins Pipeline 且希望了解如何更進一步的將其寫的更有規模” 的人

Intro

最近因為工作上的需要,寫了好一段時間的 Jenkins Pipeline,剛好可以分享一些心得、以及踩過的雷,最後是我們推薦的 Architecture。

Jenkins pipeline 是 Jenkins 在 2016 年推出的 Plugin。在此之前,大家使用 Jenkins 的方式幾乎都是 Freestyle project,也就是用 UI 操作一些執行的設定 (在哪個 Node 執行、執行前、中、後要幹麻,等等),而大家很快的就發現整個專案包含設定檔都有進行版本控制的必要性。因此 Jenkins 的開發團隊就開發了 Jenkins Pipeline 來完成這件事情。有了 Jenkins Pipeline 之後整個流程都可以用 Groovy 語言和 Jenkins 的 DSL(Domain Specific Language) 來完成,從此 Jenkins Owner 就可以用更 Organize 的方式去控制 Jenkins。

Declarative Pipeline and Scripted Pipeline

Jenkins pipeline 有分 Declarative Pipeline 和 Scripted Pipeline,前者透過類似 definition 的方式去 “define” 你的 pipeline,後者是用程式化的方式去 “execute” 你的 pipeline,兩種各有愛用者,我自己偏好用後者去寫,我們這次介紹的架構也是用 Scripted Pipeline 去撰寫。

Common Problems About Jenkins Pipelines

接下來介紹一下一系列在我改成目前架構之前常常遇到的 Jenkins Pipeline 問題,而這些問題我後來通通有找到方法解決。

ScriptApproval hell

Jenkins 的 Script 都 run 在 sandbox 裡面,而他們的權限機制下,若你的 script 中有用到一些「Jenkins 不預期的」 Object 或 method,通通都會在執行期間遇到 RejectedAccessException,這時你就只能到 global setting 裡面把剛剛被擋下的那個 rule 開起來。

Image for post
Image for post

Non-Testable

隨著 Pipeline 日益複雜,如果沒有完整的測試(Unit test + Integration test),「改 pipeline」這個動作本身就是一件非常恐怖的事情,我們的服務是提供很多 developer 一個共用的 CI / CD Pipeline service,這些 pipeline 壞掉的話很多 team…


/usr/local/x86_64-pc-linux-gnu/lib/gcc/x86_64-pc-linux-gnu/4.9.3/../../../../x86_64-pc-linux-gnu/bin/ld: contacts-mailplus-server: hidden symbol `__gcov_init’ in /usr/
local/x86_64-pc-linux-gnu/lib/gcc/x86_64-pc-linux-gnu/4.9.3/libgcov.a(_gcov.o) is referenced by DSO
/usr/local/x86_64-pc-linux-gnu/lib/gcc/x86_64-pc-linux-gnu/4.9.3/../../../../x86_64-pc-linux-gnu/bin/ld: final link failed: Bad value

編譯時遇到這個問題,花了好長一段時間分析之後發現是 library 沒有 link 到 -lgcov 的問題,但不是很直覺。

我的 library 如下:

A.so <- B.so <- binary

其中 A.so 編譯的時候有下 --coverage 但 linking 的時候沒有下,編譯的時候下會導致 gcc gen 出一些 code,需要在 link time 得時候 link 起來,但 linking 沒下就導致那個 .so 存在一些 undefined symbol (__gcov_init),然而 __gcov_init 這隻 symbol 被宣告成 hidden 因此不能借用其他 so 的 symbol,就導致了這個狀況。

解法就是 A.so 在 linking 的時候也要下 --coverage


redis 是個非常好用的工具,網路上有太多文章在介紹如何安裝、使用、以及有什麼功能,在此不贅述。我就 focus 在敝公司我的使用情境上遇到一些我們之前沒意識到的問題。

我們拿來幹麻

某個古老的產品一直以來 daemon 和 deamon 之間溝通的方式就是透過 domain socket 直接對對方下指令,因為功能日漸龐大的關係,有很多細節問題並不想在一開始處理 (ex. delay 送 job、retry 機制、對面的 daemon 因為某些原因掛掉,sender 就會卡住等等)

我們決定改成使用 Message Queue 類似的機制,先是 survey 了幾套現成的 Job Queue,kafka、RabbitMQ、甚至不太像 job queue 的 zeroMQ 等,但不是啟動太 heavy 就是記憶體用量過大,反正不符我們需求。

因此就把歪腦筋動到 Redis 身上,非常 lightweight、也已經非常穩定,完全可以依賴它建構一個非常可靠的服務。

因此我們透過 Redis 在上面用 C++ 打造了一套 JobQueue system,包含一些功能像是 retry、delay job、priority 等等,反正足夠我們使用,一開始使用上也相當不錯(乍看之下)。我們透過 redis 的 AOF 和 RDB 功能達到 persistent Message Queue 功能。

問題來了

AOF Startup slow problem

AOF 好用歸好用,但是有個有點大的硬傷是啟動的時候會花你不確定多少時間來 initialize redis,這造成我們的啟動時間不確定,而 redis 啟動的時間你都沒辦法塞 job 進去,有點困擾。

啟動的時間和 AOF 大小有關,這可以透過 config 中的
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

值來設定,照這個設定就是 AOF 大小到了 64mb 之後 100% 會重寫,但我們實測結果還是有超過 64mb 的狀況,並沒有像他說的那麼美好。

啟動時間不確定性這點造成我們小小困擾,那時還寫了 polling redis startup 來達到確保 redis 可用。

RDB Disk bandwidth 100% problem

那如果不用 AOF 只用 RDB 呢?

RDB 寫檔的機制是 redis 本身會先 fork 然後把 memory 全部一次寫進去 rdb 檔案再換掉原本的 rdb 檔案,但這會導致 rdb dump 那瞬間 disk IO 會直接被吃滿,也造成 application 效能會因此被影響。

一般 cloud 上解決方式通常是用 replication 先丟到另外一台,就想幹麻就可以幹麻了。(但我們不能)

Queue Full problem

這其實滿可以想像的,Redis 畢竟是存在記憶體,一旦你的 worker 因為某些事情而 delay 處理 job 或是它掛了再起不能,你的 redis 就會直接塞爆,通常可以透過一些機制處理掉(限制 queue 大小、drop job、服務偵測等)。

總結

新服務總是會帶來新問題,就算你覺得某個東西很穩定想起來一定用起來沒啥問題,但實務上使用的時候還是會需要手動解掉一些意想不到的事情。


最近遇到一個神秘的問題,症狀是我的 Query 看起來非常正常,但就是會少 Query 出一些東西。

Query 大概長這樣:

SELECT * FROM some_table         --- A
WHERE some_column IN ((
SELECT * FROM another_table --- B
));

因為 B Query 在我的 case 裡面相當複雜(有用到 WITH RECURSIVE 的巢狀 Query),因此我直覺一直認為 B Query 出了問題,但丟進去 Query 都沒任何異狀,結果相當正常,但 A Query 的結果一直都不太對勁。

找了一段時間之後才發現問題在 IN 後面的兩層括號 (( SubQuery ))

原來 IN 後面的語法本來就可接多個 expr 如 IN (expr, expr, …),如果你又多了一層括號,那麼就會當作你正在用這個語法,因此它只會 evaluate expr 中的第一個值。

Image for post
Image for post
expr 中的 IN 的部份

這可以從以下 sample code 中得到證實:

sqlite> CREATE TABLE temp (
...> id integer
...> );
sqlite> INSERT INTO temp VALUES(1),(2),(3);
sqlite> SELECT * FROM temp WHERE id IN (1,2,3);
id = 1
id = 2
id = 3
sqlite> SELECT * FROM temp WHERE id IN (SELECT id FROM temp);
id = 1
id = 2
id = 3
sqlite> SELECT * FROM temp WHERE id IN ((SELECT id FROM temp));
id = 1
sqlite> SELECT * FROM temp WHERE id IN ((SELECT id FROM temp), (SELECT 2 FROM temp));
id = 1
id =…


從昨天開始,我不管搜尋什麼,在點開搜尋結果的時候都會出現 “無法連線”,像是這樣:

Image for post
Image for post

但除了詳細資料無法正常顯示出來之外,其他的功能全部都正常,包含:

  • 路況顯示
  • 導航
  • 搜尋

因此很難想像真的是它上面的 tooltip:連線問題。

排除連線問題之後,我還嘗試了以下方式修復:

  • 重設 google map
  • 解安裝 google map 更新並重新安裝
  • 升級 Samsung Galaxy S9+ 至最新板
  • 根據這篇文章重新設定過日期時間(follow ntp)
  • 試過用安全模式進入 Map

全部無效。

這表示已排除以下原因造成的錯誤:

  • 網路錯誤(其他服務完全沒問題)
  • 應用程式錯誤(各種應用程式錯誤的方式已經測試過,且和朋友對過版本號)
  • S9 錯誤 / 系統造成的錯誤 (網路上有看到另外一個人有一樣問題也是最近才發生,他是 OPPO 機器)
  • 日期與時間造成的問題(maybe API 加密的方法有用到時間? not sure)
  • 該功能壞掉(其他人都正常)

而在這其中最奇怪的點就是「只有顯示商家詳細資訊這個部份出錯,其他都是正常的」這個部份了。

這表示這塊功能區之中只有我會壞,也許是 API 拉資料拉不回來,但只有我拉不回來。

於是我就試了換一個帳號登入,結果就成功了!!!

結果就成功了!!!

結果就成功了!!!

因為很激動所以說了三次。

而這大概表示,google map 用了我個人相關的資訊 build 出商家資訊的 api,但我的相關資訊有東西壞掉了 (database 壞了?我的 record 沒更新到?有 corrupt data 沒有被處理好?升級時漏掉我了?有各種可能)

看了一下 Google Map 可能怎樣用我的資料之後,我把這個選項勾掉:

Image for post
Image for post

結果就復活了!!

看起來是我的個人搜尋結果的資料造成了這個錯誤,在 Google 修好這個 bug 之前我可能都要關閉搜尋建議了。

明天又可以正常使用 Google Map 了 :D


最近遇到一個工作上的問題,就是客戶在我們的 daemon 不正常關閉 (類似 kill -9) 之後重開,sqlite 常常會出現問題。

由於前人做了不良的示範:Log 印不夠多,因此我們初步只知道以下狀況

  1. 通常中獎的機器先前都發生過不正常斷電
  2. 接著就無法啟動,顯示 sqlite database 錯誤(這裡 log 印不夠不知道是甚麼錯誤)
  3. 我們進去手動使用 sqlite 指令開起來下個 PRAGMA integrity_check; 然後就好了

傑克這實在太神奇了,到底為甚麼下個 check 就會好,這個 database 是發生了甚麼事情?

考慮過以下狀況:

  • database 如果檔案真的爛掉,那麼 integrity check 只會檢查出錯誤而已 -> 應該不是
  • 相關的 lock 被卡住 -> 還是無法解釋 integrity check 之後會好

因為實在太迷了所以一時之間我和 Jack Yu 都沒想到原因,trace code 也沒有發現可疑的部分。

因為很多 user 都遇到這一個問題,因此我嘗試 reproduce 看看有沒有機會做出來,在一串激烈的 kill -9 -> 重啟 -> kill -9 -> 重啟的暴力測試下很快地就做出來了,趕緊掛個 code 進去印出那該死的 log,得到:

sqlite3_exec error: attempt to write a readonly database

喔喔喔喔喔!(howhow語氣),這看起來超級有幫助的。

看了一下我們開 sqlite db 的部分,發現我們是用 sqlite3_open_v2 帶 SQLITE_OPEN_READONLY 開的,也就是我們將 database 開啟成 read-only mode。

問題來了,但我們的 query 就只是一個 select 而已,為甚麼會需要寫入呢?

配合剛剛的事實:這問題總是在斷電時產生,我到 sqlite db 旁一看發現有 sqlite.db-journal 檔產生,直覺告訴我,因為 sqlite 想要將 journal 檔案寫回原本的…


最近 Twitch 只要撥放 VOD 就會開始轉轉轉,實在是非常困擾。

要解決轉轉轉問題,首先就是要先分析到底在轉幾點的,照慣例打開 F12,經過簡單的分析發現 Twitch 的 VOD 是放在 akamai 的 CDN 上,並且速度非常不穩定。

Image for post
Image for post
速度時快時慢,有時候速度只有 257KB/s
Image for post
Image for post
從影片檔的 host name 可看到是使用 akamai 的 CDN 平台
Image for post
Image for post
Response header 表示 server 是放在 AmazonS3

而稍微檢測一下可以發現,同一個影片載第二次的速度和第一次差非常多,推測是有進行 cache。

Image for post
Image for post

traceroute 一下發現這個 hostname 對應到的 ip 位址在台灣

Image for post
Image for post

這些證據指出一件事情:akamai 在台灣有 CDN Server,但沒載過的資料就會速度極慢(100KB/s ~ 1MB/s 不等),聽起來就像是 akamai 跟 hinet 買的頻寬不夠導致(或其他原因),因為我們使用其他國外的服務並不會這麼慢。

而這個問題聽起來沒辦法藉由 routing 解決,因為問題不在家裡 -> akamai 而是 akamai TW -> akamai US 中間的頻寬不夠導致。

那麼,如果我們經由 VPN 將流量全部往 US 丟,應該就可以直接連到美國的 akamai CDN,享受台灣 -> 美國的頻寬,不再受限於 akamai 的內部頻寬囉?

為了驗證這個假設,我決定自己架設一台 amazon EC2 的 VPN Server。

google “PPTP server ec2” 就可以根據 這篇文章 快速架設好一台 PPTP server。設定好並連上線之後我立刻發現,居然沒辦法看 twitch 了?!想了一下推測是因為我之前都在台灣的網路環境下進行 dns lookup,我的 browser 已經 cache 了 akamaized 的台灣 IP,然後 akami 可能有擋台灣的 CDN 美國 IP 連不過來之類的。

其實我只是希望讓 *.akamaized.net 都改用美國的 IP 連而已,用 VPN 讓整台都 route 過去好像有點過頭了。了解到這點之後,我改為使用 HTTP proxy,而且用 HTTP Proxy 有個好處,就是有 chrome extension(Swichy Omega) 可以直接讓符合某個 pattern 的網址都過某個 Proxy。

於是簡單架設了一個 squid proxy 在 EC2 上,並透過 swichy omega 讓 *.akamaized.net 全都過該 proxy 就完成囉!!

Image for post
Image for post

實測起來速度有 2MB/s 左右,比沒 cache 過的 akamai TW 快太多了,讚讚讚 :)

Image for post
Image for post


上周六下午看著看著 Overwatch League 突然發現最近很熱門的話題 (XQC 被 ban) 造成被瘋狂的洗版,像是這樣:

Image for post
Image for post

… 真的是不太能用。

有沒有 Tool 可以 filter 掉這些 SPAM 呢?

google 了 “twitch filter” “twitch spam” “twitch chat filter” 之類的關鍵字都沒有找到任何的相關 tool 可以做這件事情,那只好自己開發一個了

(結果整個寫完了才發現有一個叫做 BetterTTV 的東西可以做到這件事情,有感 googl e 實力退步了)

Step 1.先想好 User Stories

  • 可以在聊天室開始洗的時候就可以直接 filter,不需要進入設定才做,那樣很慢
  • 可以快速 filter,不要有繁瑣的步驟
  • 可以 filter keyword,只要 substring 符合就直接 filter,才不用一個一個鎖
  • Twitch 已經很慢了,不希望影響太多速度

Step 2. 想好 spec

  • 於聊天室雙擊訊息,會彈出一個 dialog 問你 keyword,按下確定直接開始 filter
  • filter 顯示方式是 opacity: 0.1,有空洞在那邊 user 才會有感這個 extension 有在做事情(誤)

… spec 好像就這樣

Step 3. 設計架構

Image for post
Image for post
  • Observer: 聽取 DOM 的 event,有新訊息的時候會發送 event
  • Filter Button Appender: 自動於每筆聊天訊息插入 filter 的 button(後改為對整個 container 上 click event 之後再看 target)
  • Filter Engine: 根據 config 決定 Message 是否…


logdown 未來聽說不會維護 && 討厭 logdown 作者的關係搬到 Medium

舊家在 http://ensky.logdown.com/


因為 Facebook 疑似親中的問題我重新打開閒置已久的 Twitter,第一眼觀察到的是我的塗鴉牆怎麼都是一堆看起來跟我一點關係都沒有的東西。

像是這樣:

Image for post
Image for post

越想越怪,怎麼看都不應該推這些內容給我啊?直到我意識到上面的追隨中數量,一看不得了:

Image for post
Image for post

平常完全沒在使用 Twitter 居然會有 255 個追隨中,這一定是有甚麼誤會。看了一下清單:

Image for post
Image for post

直覺想法是被盜了,立馬先開啟兩步驟驗證,但瀏覽一遍 Twitter 帳號安全的 section ,幾百年沒有任何人登入過了,看起來又不像,只好暫時認定是 Twitter 的 bug。

但我總不成一個一個 unfollow 吧?

身為全端軟體工程師,用 js unfollow 全部 follower 也是很簡單的事情,首先找出 button 的 selector

Image for post
Image for post

看起來 .user-actions-follow-button.js-follow-btn.follow-button button:nth-child(2) 應該可以解決我的需求,於是自動 unfollow 的 script 就寫完了

document.querySelectorAll(‘.user-actions-follow-button.js-follow-btn.follow-button button:nth-child(2)’).forEach((button)=>button.click())

Image for post
Image for post

於是世界又恢復了和平,當個軟體工程師還是不錯的呢。

Ensky Lin

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store