141 Followers
·
Follow

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、服務偵測等)。

總結

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

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