掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
微服務(wù)是否適合小團(tuán)隊(duì)是個(gè)見仁見智的問題。但小團(tuán)隊(duì)并不代表出品的一定是小產(chǎn)品,當(dāng)業(yè)務(wù)變得越來越復(fù)雜,如何使用微服務(wù)分而治之就成為一個(gè)不得不面對(duì)的問題。

讓客戶滿意是我們工作的目標(biāo),不斷超越客戶的期望值來自于我們對(duì)這個(gè)行業(yè)的熱愛。我們立志把好的技術(shù)通過有效、簡(jiǎn)單的方式提供給客戶,將通過不懈努力成為客戶在信息化領(lǐng)域值得信任、有價(jià)值的長(zhǎng)期合作伙伴,公司提供的服務(wù)項(xiàng)目有:域名注冊(cè)、虛擬主機(jī)、營(yíng)銷軟件、網(wǎng)站建設(shè)、平房網(wǎng)站維護(hù)、網(wǎng)站推廣。
因?yàn)槲⒎?wù)是對(duì)整個(gè)團(tuán)隊(duì)的考驗(yàn),從開發(fā)到交付,每一步都充滿了挑戰(zhàn)。經(jīng)過 1 年多的探索和實(shí)踐,本著將 DevOps 落實(shí)到產(chǎn)品中的愿景,一步步建設(shè)出適合我們的微服務(wù)平臺(tái)。
要不要微服務(wù)
我們的產(chǎn)品是 Linkflow,企業(yè)運(yùn)營(yíng)人員使用的客戶數(shù)據(jù)平臺(tái)(CDP)。產(chǎn)品的一個(gè)重要部分類似企業(yè)版的“捷徑",讓運(yùn)營(yíng)人員可以像搭樂高積木一樣創(chuàng)建企業(yè)的自動(dòng)化流程,無(wú)需編程即可讓數(shù)據(jù)流動(dòng)起來。
從這一點(diǎn)上,我們的業(yè)務(wù)特點(diǎn)就是聚少成多,把一個(gè)個(gè)服務(wù)連接起來就成了數(shù)據(jù)的海洋。
理念上跟微服務(wù)一致,一個(gè)個(gè)獨(dú)立的小服務(wù)最終實(shí)現(xiàn)大功能。當(dāng)然我們一開始也沒有使用微服務(wù),當(dāng)業(yè)務(wù)還未成型就開始考慮架構(gòu),那么就是“過度設(shè)計(jì)"。
另一方面需要考慮的因素就是“人",有沒有經(jīng)歷過微服務(wù)項(xiàng)目的人,團(tuán)隊(duì)是否有 DevOps 文化等等,綜合考量是否需要微服務(wù)化。
微服務(wù)的好處是什么?
微服務(wù)有什么挑戰(zhàn)?
下面詳細(xì)說說我們是怎么應(yīng)對(duì)這些挑戰(zhàn)的。
開發(fā)過程的挑戰(zhàn)
持續(xù)集成
通過 CI 將開發(fā)過程規(guī)范化,串聯(lián)自動(dòng)化測(cè)試和人工 Review。
我們使用 Gerrit 作為代碼&分支管理工具,在流程管理上遵循 GitLab 的工作流模型:
集成測(cè)試
一般來說代碼自動(dòng)執(zhí)行的都是單元測(cè)試(Unit Test),即不依賴任何資源(數(shù)據(jù)庫(kù),消息隊(duì)列)和其他服務(wù),只測(cè)試本系統(tǒng)的代碼邏輯。
但這種測(cè)試需要 Mock 的部分非常多,一是寫起來復(fù)雜,二是代碼重構(gòu)起來跟著改的測(cè)試用例也非常多,顯得不夠敏捷。而且一旦要求開發(fā)團(tuán)隊(duì)要達(dá)到某個(gè)覆蓋率,就會(huì)出現(xiàn)很多造假的情況。
所以我們選擇主要針對(duì) API 進(jìn)行測(cè)試,即針對(duì) Controller 層的測(cè)試。另外對(duì)于一些公共組件如分布式鎖,Json 序列化模塊也會(huì)有對(duì)應(yīng)的測(cè)試代碼覆蓋。
測(cè)試代碼在運(yùn)行時(shí)會(huì)采用一個(gè)隨機(jī)端口拉起項(xiàng)目,并通過 HTTP Client 對(duì)本地 API 發(fā)起請(qǐng)求,測(cè)試只會(huì)對(duì)外部服務(wù)做 Mock,數(shù)據(jù)庫(kù)的讀寫,消息隊(duì)列的消費(fèi)等都是真實(shí)操作,相當(dāng)于把 Jmeter 的事情在 Java 層面完成一部分。
Spring Boot 項(xiàng)目可以很容易的啟動(dòng)這樣一個(gè)測(cè)試環(huán)境,代碼如下:
測(cè)試過程的 HTTP Client 推薦使用 io.rest-assured:rest-assured 支持 JsonPath,十分好用。
測(cè)試時(shí)需要注意的一個(gè)點(diǎn)是測(cè)試數(shù)據(jù)的構(gòu)造和清理。構(gòu)造又分為 Schema 的創(chuàng)建和測(cè)試數(shù)據(jù)的創(chuàng)建:
順帶說一下,基于 Flyway 的 Schema Upgrade 功能我們封成了獨(dú)立的項(xiàng)目,每個(gè)微服務(wù)都有自己的 Upgrade 項(xiàng)目。
好處:一是支持 command-line 模式,可以細(xì)粒度的控制升級(jí)版本;二是也可以支持分庫(kù)分表以后的 Schema 操作。Upgrade項(xiàng)目也會(huì)被制作成 Docker image 提交到 Docker hub。
測(cè)試在每次提交代碼后都會(huì)執(zhí)行,Jenkins 監(jiān)聽 Gerrit 的提交,通過 docker run -rm {upgrade 項(xiàng)目的 image}先執(zhí)行一次 Schema Upgrade,然后 Gradle test 執(zhí)行測(cè)試。
最終會(huì)生成測(cè)試報(bào)告和覆蓋率報(bào)告,覆蓋率報(bào)告采用 JaCoCo 的 Gradle 插件生成,如下圖:
這里多提一點(diǎn),除了集成測(cè)試,服務(wù)之間的接口要保證兼容,實(shí)際上還需要一種 consumer-driven testing tool。
就是說接口消費(fèi)端先寫接口測(cè)試用例,然后發(fā)布到一個(gè)公共區(qū)域,接口提供方發(fā)布接口時(shí)也會(huì)執(zhí)行這個(gè)公共區(qū)域的用例,一旦測(cè)試失敗,表示接口出現(xiàn)了不兼容的情況。
比較推薦大家使用 Pact 或是 Spring Cloud Contact。我們目前的契約基于“人的信任”,畢竟服務(wù)端開發(fā)者還不多,所以沒有必要使用這樣一套工具。
集成測(cè)試的同時(shí)還會(huì)進(jìn)行靜態(tài)代碼檢查,我們用的是 Sonar,當(dāng)所有檢查通過后 Jenkins 會(huì) +1 分,再由 Reviewer 進(jìn)行代碼 Review。
自動(dòng)化測(cè)試
單獨(dú)拿自動(dòng)化測(cè)試出來說,就是因?yàn)樗琴|(zhì)量保證的非常重要的一環(huán),上文能在 CI 中執(zhí)行的測(cè)試都是針對(duì)單個(gè)微服務(wù)的。
那么當(dāng)所有服務(wù)(包括前端頁(yè)面)都在一起工作的時(shí)候是否會(huì)出現(xiàn)問題,就需要一個(gè)更接近線上的環(huán)境來進(jìn)行測(cè)試了。
在自動(dòng)化測(cè)試環(huán)節(jié),我們結(jié)合 Docker 提高一定的工作效率并提高測(cè)試運(yùn)行時(shí)環(huán)境的一致性以及可移植性。
在準(zhǔn)備好基礎(chǔ)的 Pyhton 鏡像以及 Webdriver(Selenium)之后,我們的自動(dòng)化測(cè)試工作主要由以下主要步驟組成:
關(guān)于部分性能測(cè)試的執(zhí)行,我們同樣也將其集成到 Jenkins 中,在可以直觀的通過一些結(jié)果數(shù)值來觀察版本性能變化情況的回歸測(cè)試和基礎(chǔ)場(chǎng)景,將會(huì)很大程度的提高效率,便捷的觀察趨勢(shì):
發(fā)布過程的挑戰(zhàn)
上面提到微服務(wù)一定需要結(jié)合容器化才能發(fā)揮全部?jī)?yōu)勢(shì),容器化就意味著線上有一套容器編排平臺(tái)。我們目前采用是 Redhat 的 OpenShift。
所以發(fā)布過程較原來只是啟動(dòng) Jar 包相比要復(fù)雜的多,需要結(jié)合容器編排平臺(tái)的特點(diǎn)找到合適的方法。
鏡像準(zhǔn)備
公司開發(fā)基于 GitLab 的工作流程,Git 分支為 Master,Pre-production和 Prodution 三個(gè)分支,同時(shí)生產(chǎn)版本發(fā)布都打上對(duì)應(yīng)的 Tag。
每個(gè)項(xiàng)目代碼里面都包含 Dockerfile 與 Jenkinsfile,通過 Jenkins 的多分支 Pipeline 來打包 Docker 鏡像并推送到 Harbor 私庫(kù)上。
Docker 鏡像的命令方式為:項(xiàng)目名/分支名:git_commit_id,如 funnel/production:4ee0b052fd8bd3c4f253b5c2777657424fccfbc9。
Tag 版本的 Docker 鏡像命名為:項(xiàng)目名 /release:tag 名,如 funnel/release:18.10.R1。
在 Jenkins 中執(zhí)行 build docker image job 時(shí)會(huì)在每次 Pull 代碼之后調(diào)用 Harbor 的 API 來判斷此版本的 Docker image 是否已經(jīng)存在,如果存在就不執(zhí)行后續(xù)編譯打包的 Stage。
在 Jenkins 的發(fā)布任務(wù)中會(huì)調(diào)用打包 Job,避免了重復(fù)打包鏡像,這樣就大大的加快了發(fā)布速度。
數(shù)據(jù)庫(kù) Schema 升級(jí)
數(shù)據(jù)庫(kù)的升級(jí)用的是 Flyway,打包成 Docker 鏡像后,在 OpenShift 中創(chuàng)建 Job 去執(zhí)行數(shù)據(jù)庫(kù)升級(jí)。
Job 可以用最簡(jiǎn)單的命令行的方式去創(chuàng)建:
腳本升級(jí)任務(wù)也集成在 Jenkins 中。
容器發(fā)布
OpenShift 有個(gè)特別概念叫 DeploymentConfig,原生 Kubernetes Deployment 與之相似,但 OpenShift 的 DeploymentConfig 功能更多。
DeploymentConfig 關(guān)聯(lián)了一個(gè)叫做 ImageStreamTag 的東西,而這個(gè) ImagesStreamTag 和實(shí)際的鏡像地址做關(guān)聯(lián),當(dāng) ImageStreamTag 關(guān)聯(lián)的鏡像地址發(fā)生了變更,就會(huì)觸發(fā)相應(yīng)的 DeploymentConfig 重新部署。
我們發(fā)布是使用了 Jenkins+OpenShift 插件,只需要將項(xiàng)目對(duì)應(yīng)的 ImageStreamTag 指向到新生成的鏡像上,就觸發(fā)了部署。
如果是服務(wù)升級(jí),已經(jīng)有容器在運(yùn)行怎么實(shí)現(xiàn)平滑替換而不影響業(yè)務(wù)呢?
配置 Pod 的健康檢查,Health Check 只配置了 ReadinessProbe,沒有用 LivenessProbe。
因?yàn)?LivenessProbe 在健康檢查失敗之后,會(huì)將故障的 Pod 直接干掉,故障現(xiàn)場(chǎng)沒有保留,不利于問題的排查定位。而 ReadinessProbe 只會(huì)將故障的 Pod 從 Service 中踢除,不接受流量。
使用了 ReadinessProbe 后,可以實(shí)現(xiàn)滾動(dòng)升級(jí)不中斷業(yè)務(wù),只有當(dāng) Pod 健康檢查成功之后,關(guān)聯(lián)的 Service 才會(huì)轉(zhuǎn)發(fā)流量請(qǐng)求給新升級(jí)的 Pod,并銷毀舊的 Pod。
線上運(yùn)維的挑戰(zhàn)
服務(wù)間調(diào)用
Spring Cloud 使用 Eruka 接受服務(wù)注冊(cè)請(qǐng)求,并在內(nèi)存中維護(hù)服務(wù)列表。
當(dāng)一個(gè)服務(wù)作為客戶端發(fā)起跨服務(wù)調(diào)用時(shí),會(huì)先獲取服務(wù)提供者列表,再通過某種負(fù)載均衡算法取得具體的服務(wù)提供者地址(IP + Port),即所謂的客戶端服務(wù)發(fā)現(xiàn)。在本地開發(fā)環(huán)境中我們使用這種方式。
由于 OpenShift 天然就提供服務(wù)端服務(wù)發(fā)現(xiàn),即 Service 模塊,客戶端無(wú)需關(guān)注服務(wù)發(fā)現(xiàn)具體細(xì)節(jié),只需知道服務(wù)的域名就可以發(fā)起調(diào)用。
由于我們有 Node.js 應(yīng)用,在實(shí)現(xiàn) Eureka 的注冊(cè)和去注冊(cè)的過程中都遇到過一些問題,不能達(dá)到生產(chǎn)級(jí)別。
所以決定直接使用 Service 方式替換掉 Eureka,也為以后采用 Service Mesh 做好鋪墊。
具體的做法是,配置環(huán)境變量:
- EUREKA_CLIENT_ENABLED=false,RIBBON_EUREKA_ENABLED=false
并將服務(wù)列表如:
- FOO_RIBBON_LISTOFSERVERS: '[http://foo:8080](http://foo:8080/)'
寫進(jìn) ConfigMap 中,以 envFrom: configMapRef 方式獲取環(huán)境變量列表。
如果一個(gè)服務(wù)需要暴露到外部怎么辦,比如暴露前端的 HTML 文件或者服務(wù)端的 Gateway。
OpenShift 內(nèi)置的 HAProxy Router,相當(dāng)于 Kubernetes 的 Ingress,直接在 OpenShift 的 Web 界面里面就可以很方便的配置。
我們將前端的資源也作為一個(gè) Pod 并有對(duì)應(yīng)的 Service,當(dāng)請(qǐng)求進(jìn)入 HAProxy 符合規(guī)則就會(huì)轉(zhuǎn)發(fā)到 UI 所在的 Service。
Router 支持 A/B test 等功能,唯一的遺憾是還不支持 URL Rewrite。
對(duì)于需要 URL Rewrite 的場(chǎng)景怎么辦?那么就直接將 Nginx 也作為一個(gè)服務(wù),再做一層轉(zhuǎn)發(fā)。流程變成 Router → Nginx Pod → 具體提供服務(wù)的 Pod。
鏈路跟蹤
開源的全鏈路跟蹤很多,比如 Spring Cloud Sleuth + Zipkin,國(guó)內(nèi)有美團(tuán)的 CAT 等等。
其目的就是當(dāng)一個(gè)請(qǐng)求經(jīng)過多個(gè)服務(wù)時(shí),可以通過一個(gè)固定值獲取整條請(qǐng)求鏈路的行為日志,基于此可以再進(jìn)行耗時(shí)分析等,衍生出一些性能診斷的功能。
不過對(duì)于我們而言,首要目的就是 Trouble Shooting,出了問題需要快速定位異常出現(xiàn)在什么服務(wù),整個(gè)請(qǐng)求的鏈路是怎樣的。
為了讓解決方案輕量,我們?cè)谌罩局写蛴?RequestId 以及 TraceId 來標(biāo)記鏈路。
RequestId 在 Gateway 生成表示唯一一次請(qǐng)求,TraceId 相當(dāng)于二級(jí)路徑,一開始與 RequestId 一樣,但進(jìn)入線程池或者消息隊(duì)列后,TraceId 會(huì)增加標(biāo)記來標(biāo)識(shí)唯一條路徑。
舉個(gè)例子,當(dāng)一次請(qǐng)求向 MQ 發(fā)送一個(gè)消息,那么這個(gè)消息可能會(huì)被多個(gè)消費(fèi)者消費(fèi),此時(shí)每個(gè)消費(fèi)線程都會(huì)自己生成一個(gè) TraceId 來標(biāo)記消費(fèi)鏈路。加入 TraceId 的目的就是為了避免只用 RequestId 過濾出太多日志。
實(shí)現(xiàn)上,通過 ThreadLocal 存放 APIRequestContext 串聯(lián)單服務(wù)內(nèi)的所有調(diào)用。
當(dāng)跨服務(wù)調(diào)用時(shí),將 APIRequestContext 信息轉(zhuǎn)化為 HTTP Header,被調(diào)用方獲取到 HTTP Header 后再次構(gòu)建 APIRequestContext 放入 ThreadLocal,重復(fù)循環(huán)保證 RequestId 和 TraceId 不丟失即可。
如果進(jìn)入 MQ,那么 APIRequestContext 信息轉(zhuǎn)化為 Message Header 即可(基于 RabbitMQ 實(shí)現(xiàn))。
當(dāng)日志匯總到日志系統(tǒng)后,如果出現(xiàn)問題,只需要捕獲發(fā)生異常的 RequestId 或是 TraceId 即可進(jìn)行問題定位。
經(jīng)過一年來的使用,基本可以滿足絕大多數(shù) Trouble Shooting 的場(chǎng)景,一般半小時(shí)內(nèi)即可定位到具體業(yè)務(wù)。
容器監(jiān)控
容器化前監(jiān)控用的是 Telegraf 探針,容器化后用的是 Prometheus,直接安裝了 OpenShift 自帶的 cluster-monitoring-operator。
自帶的監(jiān)控項(xiàng)目已經(jīng)比較全面,包括 Node,Pod 資源的監(jiān)控,在新增 Node 后也會(huì)自動(dòng)添加進(jìn)來。
Java 項(xiàng)目也添加了 Prometheus 的監(jiān)控端點(diǎn),只是可惜 cluster-monitoring-operator 提供的配置是只讀的,后期將研究怎么將 Java 的 JVM 監(jiān)控這些整合進(jìn)來。
總結(jié)
開源軟件是對(duì)中小團(tuán)隊(duì)的一種福音,無(wú)論是 Spring Cloud 還是 Kubernetes 都大大降低了團(tuán)隊(duì)在基礎(chǔ)設(shè)施建設(shè)上的時(shí)間成本。
當(dāng)然其中有更多的話題,比如服務(wù)升降級(jí),限流熔斷,分布式任務(wù)調(diào)度,灰度發(fā)布,功能開關(guān)等等都需要更多時(shí)間來探討。
對(duì)于小團(tuán)隊(duì),要根據(jù)自身情況選擇微服務(wù)的技術(shù)方案,不可一味追新,適合自己的才是***的。

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流