技術開發者應該如何構建小團隊的微服務方案?

技術開發者應該如何構建小團隊的微服務方案?

作者 | 徐鵬

出品 | CSDN(ID:CSDNnews)

我們的產品是Linkflow,企業運營人員使用的客戶數據平臺(CDP)。產品的一個重要部分類似企業版的”捷徑”,讓運營人員可以像搭樂高積木一樣創建企業的自動化流程,無需編程即可讓數據流動起來。從這一點上,我們的業務特點就是聚少成多,把一個個服務連接起來就成了數據的海洋。理念上跟微服務一致,一個個獨立的小服務最終實現大功能。當然我們一開始也沒有使用微服務,當業務還未成型就開始考慮架構,那麼就是”過度設計”。另一方面需要考慮的因素就是”人”,有沒有經歷過微服務項目的人,團隊是否有devops文化等等,綜合考量是否需要微服務化。

技术开发者应该如何构建小团队的微服务方案?

要不要微服務

微服務的好處是什麼?

  • 相比於單體應用,每個服務的複雜度會下降,特別是數據層面(數據表關係)更清晰,不會一個應用上百張表,新員工上手快;

  • 對於穩定的核心業務可以單獨成為一個服務,降低該服務的發佈頻率,也減少測試人員壓力;

  • 可以將不同密集型的服務搭配著放到物理機上,或者單獨對某個服務進行擴容,實現硬件資源的充分利用;

  • 部署靈活,在私有化項目中,如果客戶有不需要的業務,那麼對應的微服務就不需要部署,節省硬件成本,就像上文提到的樂高積木理念。

微服務有什麼挑戰?

  • 一旦設計不合理,交叉調用,相互依賴頻繁,就會出現牽一髮動全身的局面。想象單個應用內service層依賴複雜的場面就明白了;

  • 項目多了,輪子需求也會變多,需要有人專注公共代碼的開發;

  • 開發過程的質量需要通過持續集成(CI)嚴格把控,提高自動化測試的比例,因為往往一個接口改動會涉及多個項目,光靠人工測試很難覆蓋所有情況;

  • 發佈過程會變得複雜,因為微服務要發揮全部能力需要容器化的加持,容器編排就是最大的挑戰;

  • 線上運維,當系統出現問題需要快速定位到某個機器節點或具體服務,監控和鏈路日誌分析都必不可少。

下面詳細說說我們是怎麼應對這些挑戰的:

技术开发者应该如何构建小团队的微服务方案?

開發過程的挑戰

持續集成:

  • 通過CI將開發過程規範化,串聯自動化測試和人工Review;

  • 我們使用Gerrit作為代碼&分支管理工具,在流程管理上遵循Gitlab的工作流模型;

  • 開發人員提交代碼至Gerrit的magic分支;

  • 代碼Review人員Review代碼並給出評分;

  • 對應Repo的Jenkins job監聽分支上的變動,觸發Build job。經過IT和Sonar的靜態代碼檢查給出評分;

  • Review和Verify皆通過之後,相應Repo的負責人將代碼merge到真實分支上若有一項不通過,代碼修改後重複過程;

  • Gerrit將代碼實時同步備份至的兩個遠程倉庫中。

技术开发者应该如何构建小团队的微服务方案?

集成測試

一般來說代碼自動執行的都是單元測試(Unit Test),即不依賴任何資源(數據庫,消息隊列)和其他服務,只測試本系統的代碼邏輯。但這種測試需要mock的部分非常多,一是寫起來複雜,二是代碼重構起來跟著改的測試用例也非常多,顯得不夠敏捷。而且一旦要求開發團隊要達到某個覆蓋率,就會出現很多造假的情況。所以我們選擇主要針對API進行測試,即針對controller層的測試。另外對於一些公共組件如分佈式鎖,json序列化模塊也會有對應的測試代碼覆蓋。測試代碼在運行時會採用一個隨機端口拉起項目,並通過

http client對本地API發起請求,測試只會對外部服務做mock,數據庫的讀寫,消息隊列的消費等都是真實操作,相當於把Jmeter的事情在Java層面完成一部分。Spring Boot項目可以很容易的啟動這樣一個測試環境,代碼如下:

<code>@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
/<code>

測試過程的http client推薦使用io.rest-assured:rest-assured支持JsonPath,十分好用。

測試時需要注意的一個點是測試數據的構造和清理。構造又分為schema的創建和測試數據的創建。

  • schema由flyway處理,在啟用測試環境前先刪除所有表,再進行表的創建;

  • 測試數據可以通過@Sql讀取一個sql文件進行創建,在一個用例結束後再清除這些數據。

順帶說一下,基於flyway的schema upgrade功能我們封成了獨立的項目,每個微服務都有自己的upgrade項目,好處一是支持command-line模式,可以細粒度的控制升級版本,二是也可以支持分庫分表以後的schema操作。upgrade項目也會被製作成docker image提交到docker hub。

測試在每次提交代碼後都會執行,Jenkins監聽gerrit的提交,通過docker run -rm {upgrade項目的image}先執行一次schema upgrade,然後gradle test執行測試。最終會生成測試報告和覆蓋率報告,覆蓋率報告採用jacoco的gradle插件生成。如圖:

技术开发者应该如何构建小团队的微服务方案?技术开发者应该如何构建小团队的微服务方案?

這裡多提一點,除了集成測試,服務之間的接口要保證兼容,實際上還需要一種consumer-driven testing tool,就是說接口消費端先寫接口測試用例,然後發佈到一個公共區域,接口提供方發佈接口時也會執行這個公共區域的用例,一旦測試失敗,表示接口出現了不兼容的情況。比較推薦大家使用Pact或是Spring Cloud Contact。我們目前的契約基於”人的信任“,畢竟服務端開發者還不多,所以沒有必要使用這樣一套工具。

集成測試的同時還會進行靜態代碼檢查,我們用的是sonar,當所有檢查通過後jenkins會+1分,再由reviewer進行代碼review。

自動化測試

單獨拿自動化測試出來說,就是因為它是質量保證的非常重要的一環,上文能在CI中執行的測試都是針對單個微服務的,那麼當所有服務(包括前端頁面)都在一起工作的時候是否會出現問題,就需要一個更接近線上的環境來進行測試了。

在自動化測試環節,我們結合Docker提高一定的工作效率並提高測試運行時環境的一致性以及可移植性。在準備好基礎的Pyhton鏡像以及Webdriver(selenium)之後,我們的自動化測試工作主要由以下主要步驟組成:

  • 測試人員在本地調試測試代碼並提交至Gerrit;

  • Jenkins進行測試運行時環境的鏡像製作,主要將引用的各種組件和庫打包進一個Python的基礎鏡像;

  • 通過Jenkins定時或手動觸發,調用環境部署的job將專用的自動化測試環境更新,然後拉取自動化測試代碼啟動一次性的自動化測試運行時環境的Docker容器,將代碼和測試報告的路徑鏡像至容器內;

  • 自動化測試過程將在容器內進行;

  • 測試完成之後,不必手動清理產生的各種多餘內容,直接在Jenkins上查看發佈出來的測試結果與趨勢。

技术开发者应该如何构建小团队的微服务方案?
技术开发者应该如何构建小团队的微服务方案?

關於部分性能測試的執行,我們同樣也將其集成到Jenkins中,在可以直觀的通過一些結果數值來觀察版本性能變化情況的迴歸測試和基礎場景,將會很大程度的提高效率、便捷的觀察趨勢。

  • 測試人員在本地調試測試代碼並提交至Gerrit;

  • 通過Jenkins定時或手動觸發,調用環境部署的job將專用的性能測試環境更新以及可能的Mock Server更新;

  • 拉取最新的性能測試代碼,通過Jenkins的性能測試插件來調用測試腳本;

  • 測試完成之後,直接在Jenkins上查看通過插件發佈出來的測試結果與趨勢。

技术开发者应该如何构建小团队的微服务方案?技术开发者应该如何构建小团队的微服务方案?
技术开发者应该如何构建小团队的微服务方案?

布過程的挑戰

上面提到微服務一定需要結合容器化才能發揮全部優勢,容器化就意味線上有一套容器編排平臺。我們目前採用是Redhat的Openshift。所以發佈過程較原來只是啟動jar包相比要複雜的多,需要結合容器編排平臺的特點找到合適的方法。

鏡像準備

公司開發基於gitlab的工作流程,git分支為master,pre-production和prodution三個分支,同時生產版本發佈都打上對應的tag。每個項目代碼裡面都包含dockerfile與jenkinsfile,通過jenkins的多分支pipeline來打包docker鏡像並推送到harbor私庫上。

技术开发者应该如何构建小团队的微服务方案?

docker鏡像的命令方式為 項目名/分支名:git_commit_id,如 funnel/production:4ee0b052fd8bd3c4f253b5c2777657424fccfbc9,tag版本的docker鏡像命名為 項目名/release:tag名,如 funnel/release:18.10.R1

技术开发者应该如何构建小团队的微服务方案?
技术开发者应该如何构建小团队的微服务方案?

在jenkins中執行build docker image job時會在每次pull代碼之後調用harbor的api來判斷此版本的docker image是否已經存在,如果存在就不執行後續編譯打包的stage。在jenkins的發佈任務中會調用打包job,避免了重複打包鏡像,這樣就大大的加快了發佈速度。

數據庫Schema升級

數據庫的升級用的是flyway,打包成docker鏡像後,在openshift中創建job去執行數據庫升級。job可以用最簡單的命令行的方式去創建:

<code>oc run upgrade-foo --image=upgrade/production --replicas=1 --restart=OnFailure --command -- java -jar -Dprofile=production /app/upgrade-foo.jar 

/<code>

腳本升級任務也集成在jenkins中。

容器發佈

openshift有個特別概念叫DeploymentConfig,原生k8s Deployment與之相似,但openshift的DeploymentConfig功能更多些。

Deploymentconfig關聯了一個叫做ImageStreamTag的東西,而這個ImagesStreamTag和實際的鏡像地址做關聯,當ImageStreamTag關聯的鏡像地址發生了變更,就會觸發相應的DeploymentConfig重新部署。我們發佈是使用了jenkins+openshift插件,只需要將項目對應的ImageStreamTag指向到新生成的鏡像上,就觸發了部署。

技术开发者应该如何构建小团队的微服务方案?

如果是服務升級,已經有容器在運行怎麼實現平滑替換而不影響業務呢?

配置Pod的健康檢查,Health Check只配置了ReadinessProbe,沒有用LivenessProbe。因為LivenessProbe在健康檢查失敗之後,會將故障的pod直接幹掉,故障現場沒有保留,不利於問題的排查定位。而ReadinessProbe只會將故障的pod從service中踢除,不接受流量。使用了ReadinessProbe後,可以實現滾動升級不中斷業務,只有當pod健康檢查成功之後,關聯的service才會轉發流量請求給新升級的pod,並銷燬舊的pod。

<code>readinessProbe:
failureThreshold: 4
httpGet:
path: /actuator/metrics
port: 8090
scheme: HTTP
initialDelaySeconds: 60
periodSeconds: 15
successThreshold: 2
timeoutSeconds: 2
/<code>
技术开发者应该如何构建小团队的微服务方案?

線上運維的挑戰

服務間調用

Spring Cloud使用eruka接受服務註冊請求,並在內存中維護服務列表。當一個服務作為客戶端發起跨服務調用時,會先獲取服務提供者列表,再通過某種負載均衡算法取得具體的服務提供者地址(ip + port),即所謂的客戶端服務發現。在本地開發環境中我們使用這種方式。

由於Openshift天然就提供服務端服務發現,即service模塊,客戶端無需關注服務發現具體細節,只需知道服務的域名就可以發起調用。由於我們有nodejs應用,在實現eureka的註冊和去註冊的過程中都遇到過一些問題,不能達到生產級別。所以決定直接使用service方式替換掉eureka,也為以後採用service mesh做好鋪墊。具體的做法是,配置環境變量EUREKA_CLIENT_ENABLED=false,RIBBON_EUREKA_ENABLED=false,並將服務列表如 FOO_RIBBON_LISTOFSERVERS: '[http://foo:8080](http://foo:8080/)' 寫進configmap中,以envFrom: configMapRef方式獲取環境變量列表。

如果一個服務需要暴露到外部怎麼辦,比如暴露前端的html文件或者服務端的gateway。

Openshift內置的haproxy router,相當於k8s的ingress,直接在Openshift的web界面裡面就可以很方便的配置。我們將前端的資源也作為一個Pod並有對應的Service,當請求進入haproxy符合規則就會轉發到ui所在的Service。router支持A/B test等功能,唯一的遺憾是還不支持url rewrite。

技术开发者应该如何构建小团队的微服务方案?技术开发者应该如何构建小团队的微服务方案?

對於需要url rewrite的場景怎麼辦?那麼就直接將nginx也作為一個服務,再做一層轉發。流程變成 router → nginx pod → 具體提供服務的pod。

鏈路跟蹤

開源的全鏈路跟蹤很多,比如spring cloud sleuth + zipkin,國內有美團的CAT等等。其目的就是當一個請求經過多個服務時,可以通過一個固定值獲取整條請求鏈路的行為日誌,基於此可以再進行耗時分析等,衍生出一些性能診斷的功能。不過對於我們而言,首要目的就是trouble shooting,出了問題需要快速定位異常出現在什麼服務,整個請求的鏈路是怎樣的。

為了讓解決方案輕量,我們在日誌中打印RequestId以及TraceId來標記鏈路。RequestId在gateway生成表示唯一一次請求,TraceId相當於二級路徑,一開始與RequestId一樣,但進入線程池或者消息隊列後,TraceId會增加標記來標識唯一條路徑。舉個例子,當一次請求會向MQ發送一個消息,那麼這個消息可能會被多個消費者消費,此時每個消費線程都會自己生成一個TraceId來標記消費鏈路。加入TraceId的目的就是為了避免只用RequestId過濾出太多日誌。

實現上,通過ThreadLocal存放APIRequestContext串聯單服務內的所有調用,當跨服務調用時,將APIRequestContext信息轉化為Http Header,被調用方獲取到

Http Header後再次構建APIRequestContext放入ThreadLocal,重複循環保證RequestId和TraceId不丟失即可。如果進入MQ,那麼APIRequestContext信息轉化為Message Header即可(基於Rabbitmq實現)。

當日志彙總到日誌系統後,如果出現問題,只需要捕獲發生異常的RequestId或是TraceId即可進行問題定位。

技术开发者应该如何构建小团队的微服务方案?

經過一年來的使用,基本可以滿足絕大多數trouble shooting的場景,一般半小時內即可定位到具體業務。

容器監控

容器化前監控用的是telegraf探針,容器化後用的是prometheus,直接安裝了openshift自帶的cluster-monitoring-operator。自帶的監控項目已經比較全面,包括node,pod資源的監控,在新增node後也會自動添加進來。

Java項目也添加了prometheus的監控端點,只是可惜cluster-monitoring-operator提供的配置是隻讀的,後期將研究怎麼將java的jvm監控這些整合進來。

技术开发者应该如何构建小团队的微服务方案?技术开发者应该如何构建小团队的微服务方案?
技术开发者应该如何构建小团队的微服务方案?

MORE

開源軟件是對中小團隊的一種福音,無論是Spring Cloud還是k8s都大大降低了團隊在基礎設施建設上的時間成本。當然其中有更多的話題,比如服務升降級,限流熔斷,分佈式任務調度,灰度發佈,功能開關等等都需要更多時間來探討。對於小團隊,要根據自身情況選擇微服務的技術方案,不可一味追新,適合自己的才是最好的。

作者簡介:徐鵬, Linkflow運維開發負責人

【END】


分享到:


相關文章: