
編者按:本文源自阿裡雲雲效團隊出品的《阿裡巴巴DevOps實踐指南》,掃描上方二維碼或前往:
https://developer.aliyun.com/topic/devops,下載下傳完整版電子書,了解阿裡十年DevOps實踐經驗。
開發一個需求,需要先進行代碼的編寫和個人驗證,驗證功能符合預期之後,再送出代碼,并進入到內建環境,進行進一步的驗證及驗收。而這個編碼和驗證的過程占據了整個需求傳遞的大部分時間,是以提高這部分工作的效率就顯得至關重要。
問題
有什麼因素降低了開發調試的效率呢?
給定下面一個系統,其中為了開發某個需求,修改了 A 和 D 這兩個應用(這裡的應用指的是一個可提供服務的一組獨立程序加上可選的負載均衡,比如一個 kubernetes 下的 service 及其後端的 deployment)。
接下來看看為了本地調測這兩個應用,會遇到什麼問題。
本地難以啟動整個系統
我們通常都在開發一個複雜系統中的一個應用,這個應用可能在系統的最前端,也可能在系統的中間位置,有時候為了端到端驗證整個流程,需要把相關的應用都啟動起來。
比如上圖中的應用 A 為最前端應用,應用 D 處在中間位置,而黑框中部分是為了完整的測試這個需求而涉及到的應用,如果是 Java 應用,開發機上啟動這樣 5 個程序,就已經不堪重負了,而很多時候需要完整啟動的應用數量會遠大于這個數字。
依賴系統不穩定
既然不能把整個系統都在本地啟動起來,那麼本地就會一部分依賴于公共測試環境。雖然前面提到應該本地測試符合預期之後再把代碼部署到測試環境,但不可避免的還是會出現一些 bug,導緻測試環境不可用(這也是測試環境的價值所在,盡早的發現問題)。一旦依賴系統不可用,就無法正常的進行測試。
雲原生開發模式下的測試環境的連通性
在基于 Kubernetes 的基礎設施下,整個系統中大部分的應用通常不需要通過 Ingress 暴露到公網。如果你的測試環境是獨立的 K8S 叢集,那就意味着無法從本地無法通路到叢集内的應用,那麼依賴公共測試環境這件事情都無法進行,比如上圖中 A->C,D->E,D->F 的依賴。
還有另外一種依賴,即上遊應用對本地應用的依賴,比如 C->D 的依賴。但因為 C 是公共測試環境,不可以将所有的 C 對 D 的請求都打到本地來,這就需要某種機制來保證隻有特定規則的請求會路由到開發本地的 D 應用。
外部依賴系統到開發環境的連通性
有一些測試鍊路需要接受一些外部依賴系統的回調,比如微信或者支付寶的回調等。而本地應用通常沒有公網位址,這也給調試帶來了一些困難。
中間件的隔離
分布式系統中經常會用到 RocketMQ 等消息中間件,如果使用了公共測試環境,就意味着 MQ 也是共用的,那麼 MQ 的消息到底是應該被測試環境消費,還是某個個人的開發環境消費呢,這也是需要解決的問題。
高效本地開發
為了進行全流程的高效開發,應該盡量使用回報比較快的驗證方式,并及早發現問題,逐漸進行更加內建,更加真實的測試。
一般來講,一個開發過程可以經過下面的三個階段:
- 編碼+單元測試。在小的邏輯單元的層面保證正确性。
- 針對單個應用的內建測試,可能需要對依賴的應用進行 HTTP 級别的 mock。
- 結合公共測試環境進行完整的內建測試。
基于上面的三個階段,可以使用以下的方式來解決前面提到的幾個問題。
- 使用各個語言相應的測試工具(比如 JUnit)來進行單元測試。
- 使用 moco 等 HTTP Mock 工具來解決本地隔離驗證的問題,完成單個應用的內建測試。
- 使用 kt-connect 和 virtual-environment 等工具來解決雲原生基礎設施下,本地和測試環境的互相連通性問題,及 http 請求鍊路的染色和路由。
- 使用 ngrok 等工具解決外部依賴調用本地應用的問題。
- 使用“主幹穩定環境”作為公共測試環境,提高其穩定性。
- 使用中間件的染色隔離能力保證 http 請求之外的其它鍊路(比如消息)的染色和路由。
其中第 1、4 是成熟的技術,這裡不再贅述。第 5、6 點會在後面的測試環境相關的章節中我們詳細講解。本文主要就第 2、3 點展開講解。
單應用的內建測試方案
比如對應用 D 而言,測試範圍如下圖的橙色框所示:
應用本身的持久化等依賴使用真實的(一般使用本地 DB),但外部應用(應用 E、F)使用基于 HTTP協定的測試替身。這樣就可以保證所有的依賴都是穩定的。并且也可以很友善的修改測試替身的行為,以進行特定場景的測試。
應用 D 依賴了兩個應用:
- org-service(應用 F):提供查詢組織資訊等能力
- user-service(應用 E):提供查詢使用者資訊等能力
這兩個應用的通路位址配置在應用 D 的配置項中:
...
org-service-host: org-service
user-service-host: user-service
...
我們使用 docker compose + moco 的方案來講解如何使用本地測試替身。
首先建立如下的目錄結構:
├── Dockerfile
├── docker-compose.yml
├── moco-runner.jar
└── services
├── org-service
│ └── config.json
└── user-service
└── config.json
Dockerfile:
FROM openjdk:8-jre-slim
ARG SERVICE
ADD moco-runner.jar moco-runner.jar
COPY services/${SERVICE}/config.json config.json
ENTRYPOINT ["java", "-jar", "moco-runner.jar", "http", "-c", "config.json", "-p", "8080"]
docker-compose.yml:
version: '3.1'
services:
service-f:
ports:
- 8091:8080
build:
context: .
dockerfile: Dockerfile
args:
SERVICE: org-service
service-e:
ports:
- 8092:8080
build:
context: .
dockerfile: Dockerfile
args:
SERVICE: user-service
services/org-service/config.json:
[
{
"request": {
"uri": "/"
},
"response": {
"text": "org service stub"
}
},
{
"request": {
"uri": {
"match": "/orgs/[a-z0-9]{24}"
}
},
"response": {
"json": {
"name": "some org name",
"logo": "http://xx.assets.com/xxx.jpg"
}
}
}
]
services/user-service/config.json:
[
{
"request": {
"uri": "/"
},
"response": {
"text": "user service stub"
}
},
{
"request": {
"uri": {
"match": "/users/[a-z0-9]{24}"
}
},
"response": {
"json": {
"name": "somebody",
"email": "[email protected]"
}
}
}
]
然後使用如下指令來啟動兩個依賴的應用:
docker-compose up --build
驗證下本地測試替身的行為:
$ curl http://localhost:8092/users/111111111111111111111111
{"name":"somebody","email":"[email protected]"}
$ curl http://localhost:8091/orgs/111111111111111111111111
{"name":"some org name","logo":"http://xx.assets.com/xxx.jpg"}
然後再把應用 D 的依賴配置改成本地測試替身即可進行測試:
...
org-service-host: localhost:8091
user-service-host: localhost:8092
...
至此,我們得到了一個穩定的單應用的內建測試環境。當需要修改依賴的行為時,隻需要修改相應應用的config.json 即可。
使用 docker-componse 和 moco 是一種實作單應用內建測試的方式,你可以根據項目的具體情況選擇合适的工具和方案。
本地和公共測試環境的互訪及鍊路隔離
完成單應用的內建測試之後,可以獲得單個應用級别的品質信心,但更大範圍的驗證還是需要和真實的依賴內建在一起進行。
如上圖所示,為了能夠在本地按需啟動應用(A 和 D),并複用測試環境的其他應用(C),就需要解決兩個問題:
- 本地如何調用到公共測試環境的應用,即 A 如何調用到 C
- 公共測試環境如何調用到本地,即 C 如何調用到本地的 D
關于第一點,如果本地環境和測試環境的網絡是直接可達的,則直接修改本地應用 A 的配置項即可。如果你使用了雲原生的基礎設施,那麼就需要類似雲效 kt-connect 之類的工具來進行打通,這裡不再展開,有需求要的可以參看 kt-connect 的 connect 部分。
關于第二點,需要解決三個問題:
- 從測試環境的 A 發起的調用鍊,應該最終通路到測試環境的 D,而從本地環境的 A 發起的調用鍊,應該最終通路到本地環境的 D,互不影響。為了能夠對這兩種調用進行區分,需要對調用鍊進行“染色”,這裡采用的染色的方式是在請求中加入一個額外的 header。
- 根據這個染色的标志,即“染色标”,進行路由。
- 一個調用鍊會貫穿多個應用,要保證在調用到不同的應用時,染色标要能夠自動的傳遞下去。
關于第一點和第二點,在阿裡巴巴内部有一套完整的方案進行染色和路由,這套方案不僅僅适用于 HTTP 鍊路,也适用于 RPC,異步消息等。而在開源領域,也有基于雲原生基礎設施的 kt-connect 可以用,使用kt-connect 的 mesh 功能就可以針對特定染色規則的調用鍊進行路由。
kt-connect 基于 istio 的 VirtualService 和 DestinationRule 來進行路由。其基本原理是在叢集内建立一個影子副本的 service 和 deployment,然後送出一個應用 D 的 DestinationRule 資源,使得包含“local-env: true”header 的請求被路由到應用 D 的影子副本,然後應用 D 的影子副本再把請求轉發到本地。在這個過程裡,除了送出和更新 is t i o 相關資源的操作需要手動進行之外,其他的事情都可以使用ktctl mesh 指令來完成,詳情請參看 mesh 最佳實踐。
接下來解決第三點,染色标傳遞。即需要保證當本地的應用 A 把含有“local-env: true”header 的請求打到測試環境的應用 C 後,應用 C 繼續通路應用 D 時候,請求中也應該包含這個 header。
一般的思路是在 Web 層的入口加一個 Interceptor,将染色标記錄下來到一個 ThreadLocal 中,然後再出口的 HttpClient 層再從 ThreadLocal 中把這個染色标取出來,并填充到 Request 對象中。這裡有一個需要注意的問題,因為染色是放在 ThreadLocal 中的,是以在一個 web 請求的進行中一旦遇到多線程的情況,就需要小心的把這個 ThreadLocal 的值傳遞到相應的子線程中。所有的應用都正确的将染色标傳遞下去,就可以保證染色标在全鍊路進行傳遞。
使用 kt-connect 的 mesh 方案加上全鍊路染色标的方案,就可以輕松的在本地按需啟動應用,并進行開發調測。
總結
- 使用單元測試、單應用內建測試、端到端內建測試結合的方式進行本地調測,提高獲得回報的效率。
- 本地按需啟動應用進行端到端內建測試的關鍵技術是:全鍊路染色和路由。在不同的基礎設施下可以有不同的實作方式。
免費下載下傳《阿裡巴巴DevOps實踐指南》
阿裡巴巴合夥人和業界多位大佬力薦、何勉、陳鑫等17位阿裡資深技術專家聯袂出品、阿裡十年DevOps經驗沉澱總結、阿裡巴巴DevOps落地實踐一本通。
前往:
,下載下傳完整版電子書。