參與Apisix開源的一次完整提交過程分享
前言
參與開源不是為了證明什么,而是為了更好的配合工作。開源和工作在絕大部分時間,都是可以和諧共處,互相促進,Win-WIn雙贏。
本文內容記錄了為 apisix 項目提交的一次pull request提交 (訪問地址:https://github.com/apache/apisix/pull/3615 )完整過程,提交內容為一個獨立的服務發現模塊,本文目的是為團隊的其他同學參與社區項目分享的行為提供一個簡單可遵循、可操作模型。
概括來講,簡要操作流程如下:
- 首先,確定需要開源的部分
- 其次,在項目社區中分享我們的看法和后續行為等
- 然后,準備提交內容
- 接著,提交pull request,接受社區審核,反復調整修改
- 后續,關注社區的走向,持續改進
下面為每一步具體操作的流水賬。
提前預警,圖多費流量,慎入 :))
首先,我們有一個Consul KV服務發現組件
作為Nginx用戶,我們實際場景使用 Nginx Upsync 模塊,結合Consul KV作為服務注冊和發現形式。
我們基于Apisix構建HTTP API服務網關,沒有發現現成的Consul KV形式服務發現模塊,既然實際業務需要,我們需要把它按照接口規范開發出來,以適應我們自己的實際場景。
當服務發現模塊功能開發出來后,也是僅僅能滿足基本需求,還不夠完善,但這時改進的思路并不是非常清楚,
既然開源社區也有類似的需求,那我們可以考慮分享開源出去,接收整個社區的考驗,大家一起改進。
限于日常思維角度的局限,若是僅僅滿足工作需要,那么開源出去會讓你的代碼接受到社區方方面面的審核,尤其是針對代碼風格、功能、執行等有嚴格要求的apisix項目。擺正心態,接受代碼評審并調整,最終結果無疑是讓代碼更加健壯,好事一樁嘛。
當然開源出去之后,該模塊的變更以及優化等行為就完全歸屬整個社區了,群策群力,是一種比較期待的演進方式。
第一步,咨詢社區意見
一個優秀的開源項目,為了穩定健康發展,一般會提供郵件組方便社區參與者咨詢、溝通協調等。
一般來說,Github會提供issues
列表方便項目使用者提交BUG,若我們想在社區中表達意圖、觀點等,就不如發在社區郵件組中,這樣能夠得到更多的關注。比如,我們想給社區共享一個完整的服務發現模塊,就可以直接在郵件組中描述大致功能,以及大致處理流程等,讓社區知道我們的真實意圖。
Apisix開發郵件組地址為:dev@apisix.apache.org
,但一般的郵件組都需要注意如下事項:
- 溝通需要使用英文
- 這也是Apisix項目國際化需求
- 雖然你也知道閱讀郵件的有幾個中國的糙老爺們,但也會有來自其他國家的用戶
- 當然在Github上所有的項目溝通都需要使用英文,這是一個良好的開源社區溝通習慣
- 推薦一個微軟英語在線協作輔助工具:https://aimwriting.mtutor.engkoo.com/ ,可以幫助校驗語法錯誤等
- 無法傳遞富文本
- 使用純文本即可
- 類似我有格式化強迫癥患者,直接粘貼 markdown 格式文本
無法傳遞圖片
- 直接傳遞圖片URL地址
- 若需要傳遞圖片,提供一個小技巧:新建一個issues表單,直接拖拽圖片到表單處,然后獲得圖片地址即可,無須提交
issues
表單
上傳圖片
下面是我發送的郵件截圖:
因為apache郵件組不支持富文本和圖片,實際看到的效果就沒有那么好看了,下面的連接包含了該討論完整的回復內容:
不方便打開的話,下面提供完整郵件討論截圖,很長的截圖,呵呵:
總之,斷斷續續經過三周時間的討論,這個過程需要有些耐心。發完郵件等有了積極反饋,下面就可以著手準備提交代碼了。
第二步,準備提交
Fork到自己倉庫
去 https://github.com/apache/apisix Fork到自己倉庫中,然后克隆到自己工作機來。
注意,需要時刻保持和主干保持一致:
git remote add upstream https://github.com/apache/apisix.git
下面就是動手開干了。
按需調整代碼
Consul KV服務發現模塊文件是 consul_kv.lua
,相對位置為:apisix/discovery/consul_kv.lua
。我們想提交到項目主干,那么代碼就必須遵循已有規范。
針對apisix
的服務發現代碼,需要有配置項,就必須給出一套完整的服務配置 schema
定義,如下。
local schema = {
type = "object",
properties = {
servers = {
type = "array",
minItems = 1,
items = {
type = "string",
}
},
fetch_interval = {type = "integer", minimum = 1, default = 3},
keepalive = {
type = "boolean",
default = true
},
prefix = {type = "string", default = "upstreams"},
weight = {type = "integer", minimum = 1, default = 1},
timeout = {
type = "object",
properties = {
connect = {type = "integer", minimum = 1, default = 2000},
read = {type = "integer", minimum = 1, default = 2000},
wait = {type = "integer", minimum = 1, default = 60}
},
default = {
connect = 2000,
read = 2000,
wait = 60,
}
},
skip_keys = {
type = "array",
minItems = 1,
items = {
type = "string",
}
},
default_service = {
type = "object",
properties = {
host = {type = "string"},
port = {type = "integer"},
metadata = {
type = "object",
properties = {
fail_timeout = {type = "integer", default = 1},
weigth = {type = "integer", default = 1},
max_fails = {type = "integer", default = 1}
},
default = {
fail_timeout = 1,
weigth = 1,
max_fails = 1
}
}
}
}
},
required = {"servers"}
}
當然,你需要區分每一個配置項是不是必填項,非必傳項需要具有默認值,以及上限或下限約束等。
下面需要在該模塊啟動時進行檢測用戶配置是否錯誤,無法兼容、恢復錯誤的話,需要直接使用Lua內置錯誤日志接口輸出:
error("Errr MSG")
另外,若要引入 resty.worker.events
組件,不要提前require
,比如在文件頭部提前聲明時:
loca events = require("resty.worker.events")
啟動后,就有可能在日志文件中出現如下異常:
2021/02/23 02:32:20 [error] 7#7: init_worker_by_lua error: /usr/local/share/lua/5.1/resty/worker/events.lua:175: attempt to index local 'handler_list' (a nil value)
stack traceback:
/usr/local/share/lua/5.1/resty/worker/events.lua:175: in function 'do_handlerlist'
/usr/local/share/lua/5.1/resty/worker/events.lua:215: in function 'do_event_json'
/usr/local/share/lua/5.1/resty/worker/events.lua:361: in function 'post'
/usr/local/share/lua/5.1/resty/worker/events.lua:614: in function 'configure'
/usr/local/apisix/apisix/init.lua:94: in function 'http_init_worker'
init_worker_by_lua:5: in main chunk
推薦做法是延遲加載,在該模塊被加載時進行引用。
local events
local events_list
......
function _M.init_worker()
......
events = require("resty.worker.events")
events_list = events.event_list(
"discovery_consul_update_application",
"updating"
)
if 0 ~= ngx.worker.id() then
events.register(discovery_consul_callback, events_list._source, events_list.updating)
return
end
......
end
單元測試依賴
單元測試代碼的執行,會在你提交PR代碼后自動執行持續集成行為內執行。
首先,需要本機執行單元測試前,需要提前準備好所需Docker測試實例:
docker run --rm --name consul_1 -d -p 8500:8500 consul:1.7 consul agent -server -bootstrap-expect=1 -client 0.0.0.0 -log-level info -data-dir=/consul/data
docker run --rm --name consul_2 -d -p 8600:8500 consul:1.7 consul agent -server -bootstrap-expect=1 -client 0.0.0.0 -log-level info -data-dir=/consul/data
docker run --rm -d \
-e ETCD_ENABLE_V2=true \
-e ALLOW_NONE_AUTHENTICATION=yes \
-e ETCD_ADVERTISE_CLIENT_URLS=http://0.0.0.0:2379 \
-e ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379 \
-p 2379:2379 \
registry.api.weibo.com/wesync/wbgw/etcd:3.4.9
然后,安裝項目依賴:
make deps
其次,別忘記在apisix項目持續集成腳本相應位置添加相應依賴。
比如,因為單元測試依賴于端口分別為7500和7600的兩個Consul Server實例,需要在執行單元測試之前提前運行,因此你需要在對應的持續集成文件上添加所需運行實例。比如其中一個位置:
無測試不編碼
僅僅提供服務發現consul_kv.lua
這一個文件,是無法被倉庫管理員采納的,因為除了你自己以外,別人無法確定你提交的代碼所提供功能是否足夠讓人信服,除非你能提供較為完整的 Test::Nginx
單元測試支持,自我證明。
Test::Nginx
單元測試可能針對很多人來講,是一個攔路虎,但其實有些耐心,你會發現它的美妙之處。
簡單入門可參考 https://time.geekbang.org/column/article/109506 (若只需要學習單元測試,其實不需要購買整個專輯的)。在使用過程中需要參考在線文檔:https://metacpan.org/pod/Test::Nginx::Socket ,需要一些耐心花費一點時間慢慢消化。
如何運行Nginx單元測試案例,具體參看:
https://github.com/apache/apisix/blob/master/doc/zh-cn/how-to-build.md
至于Apisix定制部分單元測試部分,可以直接參考已有的單元測試文件即可。
Consul KV服務發現的單元測試模塊相對路徑 t/discovery/consul_kv.lua
,在線地址為: https://github.com/apache/apisix/blob/master/t/discovery/consul_kv.t 。該文件大約500多行,比真正的模塊consul_kv.lua
代碼行數還多。但比較完整覆蓋了所能想到的所有場景,雖然寫起來雖然有些麻煩,但針對應用到線上大量業務的核心代碼,無論多認真和謹慎都是不為過的。
以往針對關鍵核心模塊的每一次迭代,心里面大概有些忐忑七上八下吧,也不太敢直接應用到線上。現在有了單元測試各種場景的覆蓋輔助驗證迭代變更效果,自信心是有了,也可以給別人拍著胸脯保證修改沒問題。當然若后續發現隱藏的問題,直接添加上對應的單元測試覆蓋上即可。
我們這次只提供一個服務發現模塊,因此只需要單獨測試consul_kv.t
文件即可:
# prove -Itest-nginx/lib -I./ t/discovery/consul_kv.t
......
t/discovery/consul_kv.t .. ok
All tests successful.
Files=1, Tests=102, 36 wallclock secs ( 0.05 usr 0.01 sys + 0.78 cusr 0.41 csys = 1.25 CPU)
Result: PASS
出現測試案例失敗問題,可以去 apisix/t/servroot/logs
路徑下查看 error.log
文件暴露出的異常等問題。
有些一些測試用例需要組合一組較為復雜的使用場景,比如我們準備一組后端節點:
- 127.0.0.1:30511,輸出
server 1
- 127.0.0.1:30512,輸出
server 2
- 127.0.0.1:30513,輸出
server 3
- 127.0.0.1:30514,輸出
server 4
這些節點將被頻繁執行注冊Consul節點然后再解除注冊若干循環過程:清理注冊 -> 注冊 -> 解除注冊 -> 注冊 -> 解除注冊 -> 注冊 -> 解除注冊 -> 注冊
,目的檢驗已解除注冊的失效節點是否還會存在內存中等。
有些操作,比如注冊或解除注冊節點這些操作,網關的consul_kv.lua
服務模塊在物理層面需要wait一點時間等待網關消化這些變化,因此我們需要額外提供一個 /sleep
接口,請求時需要故意休眠幾秒鐘時間等待下一次請求生效。
=== TEST 7: test register & unregister nodes
--- yaml_config eval: $::yaml_config
--- apisix_yaml
routes:
-
uri: /*
upstream:
service_name: http://127.0.0.1:8500/v1/kv/upstreams/webpages/
discovery_type: consul_kv
type: roundrobin
#END
--- config
location /v1/kv {
proxy_pass http://127.0.0.1:8500;
}
location /sleep {
content_by_lua_block {
local args = ngx.req.get_uri_args()
local sec = args.sec or "2"
ngx.sleep(tonumber(sec))
ngx.say("ok")
}
}
--- timeout: 6
--- request eval
[
"DELETE /v1/kv/upstreams/webpages/?recurse=true",
"PUT /v1/kv/upstreams/webpages/127.0.0.1:30511\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
"GET /sleep?sec=5",
"GET /hello",
"PUT /v1/kv/upstreams/webpages/127.0.0.1:30512\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
"GET /sleep",
"GET /hello",
"GET /hello",
"DELETE /v1/kv/upstreams/webpages/127.0.0.1:30511",
"DELETE /v1/kv/upstreams/webpages/127.0.0.1:30512",
"PUT /v1/kv/upstreams/webpages/127.0.0.1:30513\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
"PUT /v1/kv/upstreams/webpages/127.0.0.1:30514\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
"GET /sleep",
"GET /hello?random1",
"GET /hello?random2",
"GET /hello?random3",
"GET /hello?random4",
"DELETE /v1/kv/upstreams/webpages/127.0.0.1:30513",
"DELETE /v1/kv/upstreams/webpages/127.0.0.1:30514",
"PUT /v1/kv/upstreams/webpages/127.0.0.1:30511\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
"PUT /v1/kv/upstreams/webpages/127.0.0.1:30512\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
"GET /sleep?sec=5",
"GET /hello?random1",
"GET /hello?random2",
"GET /hello?random3",
"GET /hello?random4",
]
--- response_body_like eval
[
qr/true/,
qr/true/,
qr/ok\n/,
qr/server 1\n/,
qr/true/,
qr/ok\n/,
qr/server [1-2]\n/,
qr/server [1-2]\n/,
qr/true/,
qr/true/,
qr/true/,
qr/true/,
qr/ok\n/,
qr/server [3-4]\n/,
qr/server [3-4]\n/,
qr/server [3-4]\n/,
qr/server [3-4]\n/,
qr/true/,
qr/true/,
qr/true/,
qr/true/,
qr/ok\n/,
qr/server [1-2]\n/,
qr/server [1-2]\n/,
qr/server [1-2]\n/,
qr/server [1-2]\n/
]
準備文檔
除了代碼能夠正常運轉,我們還需要準備相應的Markdown文檔輔助說明如何使用我們的模塊,幫助社區用戶更好使用它。
社區一般以英文文檔為先, 只有在精力滿足的情況下,可以補充中文文檔。
下面就是要準備Markdown文檔了,其文檔路徑為:doc/discovery/consul_kv.md
,單獨的文檔需要在其它已有文檔掛接上對應鏈接,方便索引。
文檔路徑為:doc/discovery/consul_kv.md
,在線地址:https://github.com/apache/apisix/blob/master/docs/en/latest/discovery/consul_kv.md
一般建議需要在文檔中能夠清楚說明模塊的使用方式,以及注意事項,尤其是配置參數使用方式等。比如下面的配置項說明:
```yaml
discovery:
consul_kv:
servers:
- "http://127.0.0.1:8500"
- "http://127.0.0.1:8600"
prefix: "upstreams"
skip_keys: # if you need to skip special keys
- "upstreams/unused_api/"
timeout:
connect: 1000 # default 2000 ms
read: 1000 # default 2000 ms
wait: 60 # default 60 sec
weight: 1 # default 1
fetch_interval: 5 # default 3 sec, only take effect for keepalive: false way
keepalive: true # default true, use the long pull way to query consul servers
default_server: # you can define default server when missing hit
host: "127.0.0.1"
port: 20999
metadata:
fail_timeout: 1 # default 1 ms
weight: 1 # default 1
max_fails: 1 # default 1
```
......
The `keepalive` has two optional values:
- `true`, default and recommend value, use the long pull way to query consul servers
- `false`, not recommend, it would use the short pull way to query consul servers, then you can set the `fetch_interval` for fetch interval
每一個文檔都不應該成為信息孤島,它需要在其它文檔上掛載上一個連接地址,因此我們需要在合適的地方,比如需要在 doc/discovery.md
最下面添加鏈接地址描述:
## Discovery modules
- eureka
- [Consul KV](discovery/consul_kv.md)
模塊代碼,測試文件,以及文檔等準備好了之后,下面就是準備提交代碼到自己倉庫。
驗證提交語法規范
所有內容準備好之后,建議執行 make lint
和 make license-check
兩個命令檢測代碼、markdown文檔等是否滿足項目規范要求。
# make lint
./utils/check-lua-code-style.sh
+ luacheck -q apisix t/lib
Total: 0 warnings / 0 errors in 133 files
+ find apisix -name '*.lua' '!' -wholename apisix/cli/ngx_tpl.lua -exec ./utils/lj-releng '{}' +
+ grep -E 'ERROR.*.lua:' /tmp/check.log
+ true
+ '[' -s /tmp/error.log ']'
./utils/check-test-code-style.sh
+ find t -name '*.t' -exec grep -E '\-\-\-\s+(SKIP|ONLY|LAST)$' '{}' +
+ true
+ '[' -s /tmp/error.log ']'
+ find t -name '*.t' -exec ./utils/reindex '{}' +
+ grep done. /tmp/check.log
+ true
+ '[' -s /tmp/error.log ']'
# make license-check
.travis/openwhisk-utilities/scancode/scanCode.py --config .travis/ASF-Release.cfg ./
Reading configuration file [.travis/ASF-Release.cfg]...
Scanning files starting at [./]...
All checks passed.
若檢查出語法方面問題,認真調整,直到找不到問題所在。
這次PR提交之前,忘記這回事了,會導致多了若干次次submit提交。
第三步,提交Pull Request
去官網:https://github.com/apache/apisix/pulls 新建一個New pull request
,后面將使用PR指代pull request
。
PR標題格式
PR提交標題是規范要求的,模板如下:
{type}: {desc}
其中{type}
指代本次PR類型,具體值如下,盡量不要搞錯:
feat
:新功能(feature)fix
:修補bugdocs
:文檔(documentation)style
: 格式(不影響代碼運行的變動)refactor
:重構(即不是新增功能,也不是修改bug的代碼變動)test
:增加測試chore
:構建過程或輔助工具的變動- ……
其中{desc}
需要概括本次提交內容。
比如這次標題為:feat: add consul kv discovery module
。
填充PR內容
PR內容模板化,為標準的Github Markdown格式,主要目的說明本次提交內容,示范如下:
### What this PR does / why we need it:
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->
### Pre-submission checklist:
* [ ] Did you explain what problem does this PR solve? Or what new features have been added?
* [ ] Have you added corresponding test cases?
* [ ] Have you modified the corresponding document?
* [ ] Is this PR backward compatible? **If it is not backward compatible, please discuss on the [mailing list](https://github.com/apache/apisix/tree/master#community) first**
按照模板格式填寫,省心省力,如下:
### What this PR does / why we need it:
As I mentioned previously in the mail-list, my team submit our `consul_kv` discovery module now.
More introductions here:
https://github.com/yongboy/apisix/blob/consul_kv/doc/discovery/consul_kv.md
### Pre-submission checklist:
* [x] Did you explain what problem does this PR solve? Or what new features have been added?
* [x] Have you added corresponding test cases?
* [x] Have you modified the corresponding document?
* [x] Is this PR backward compatible? **If it is not backward compatible, please discuss on the [mailing list](https://github.com/apache/apisix/tree/master#community) first**
認真接受評審和建議
提交PR之后,才是一個開始,起點。
Apisix項目會自動針對我們所提交內容執行持續集成,apisix
項目的檢查項很多,比如針對Markdown格式就很嚴格:
持續集成不通過,按照要求微調吧,也是標準化的要求。
我們在PUSH代碼之前,使用 make lint
和 make license-check
兩個命令提前檢測還是十分有必要的,提前檢測語法等。
首先,一定要確保持續集成不能出錯。持續集成通不過,說明我們的準備還不充分,繼續調整修改,繼續提交,一直到持續集成完全執行成功為止。
保證持續集成執行成功,這是最基本的要求,否則社區無法確認我們的代碼是否基本合格。
放松心態,準備開始改進BUG,以及接受社區的各種代碼評審和改進意見吧。
其次,就是要虛心接受社區代碼評審和改進意見了,這是最關鍵的一步。
下面是一些建議:
- 真正代碼BUG,認真修改
- 邏輯處理不合理的地方,思考并給出一些處理思路,確定好之后開始調整即可
- 有些提議可能會超出本次提交范圍,說明原因,給出拒絕理由,可以婉拒嘛,比如可以放在下一次的提交中。
- 若有遇到自己處理不了的問題,積極向社區尋求幫助吧。
- 針對一批次修改再次提交后,會再次執行持續集成,一樣確保持續集成不能夠失敗,然后繼續等待下一輪的審核
認真對待每一個建議,有則改之無則加勉,不知不覺之間就進步了很多,代碼質量也得到了提升。
經過多次的微調,我們的服務發現核心模塊基本上已趨于完善了一版,這已經和還沒準備分享出來之前的原始文件相比已經天差地別了 :))
下面是本次PR包含的多次提交、代碼評審以及答復等完整流程截圖:
被合并到主分支之后,有沒有感覺到整個社區都在幫助我們一起改進,快不快哉 ?
關于依賴項的處理
本次提交的服務發現模塊依賴一個組件:lua-resty-consul
,其倉庫地址:https://github.com/hamishforbes/lua-resty-consul,最新版本為:`0.3.2`。因為我們在實際部署定制時,直接下載了該文件,簡單直接粗暴。
但apisix
項目針對項目依賴,采用的 LuaRocks 管理,在 2021-2-20 之前該組件托管在 https://luarocks.org/modules/hamish/lua-resty-consul 上面最新版本為 0.2-0
,這就很難辦了。
我的處理步驟如下:
- 首先我在github上面向作者提交一個求助:https://github.com/hamishforbes/lua-resty-consul/issues/20, 然而并沒有在一兩周時間內沒有等到作者回復
- 無奈,只好自己在 LuaRocks 單獨提交一個暫時性的解決方案:https://luarocks.org/modules/yongboy/lua-resty-consul-0.3.2,在本次PR中直接包含了該組件臨時地址
- 三周左右,終于等到該組件作者提交最新版到 LuaRocks 站點,既然官方更新了,那就把服務發現模塊里面的依賴修改為官方最新地址吧,再次提價一個PR:https://github.com/apache/apisix/pull/3654
有些一波三折 :))
第四步,關于后續
一旦合并到主分支后,后續的演進整個社區都可以參與進來,可能有人提 issue
,可能有人提 PR 修改等,后續我們想為該模塊繼續提交,那將是另外一個PR的事情。
我們可以繼續做以下事情:
- 根據實際需要重構
- 若有人提Issue是,自然是Fixbug;實踐中遇到的Bug,修復它
- 需要添加新的單元測試覆蓋到新的特性
- 若有需要,就需要添加新的文檔進行描述
毫無疑問,這是一個良性循環。
小結
參與社區開發的其它類型提交,可能會比上面所述簡單很多,但大都可以看做是以上行為的一個子集。
參與開源,也會為我們打開一扇窗戶,去除自身的狹隘。積極向社區靠攏,這需要磨去一些思維或認知的棱角,虛心認識到自我的不足,并不斷調整不斷進步。
加油!
posted on 2021-03-10 11:20 nieyong 閱讀(1671) 評論(0) 編輯 收藏 所屬分類: HTTP 、移動后端