java 拉起服务_技术开发者应该如何构建小团队的微服务方案?
作者 | 徐鵬
責(zé)編 | 劉靜
出品 | CSDN(ID:CSDNnews)
我們的產(chǎn)品是Linkflow,企業(yè)運(yùn)營人員使用的客戶數(shù)據(jù)平臺(CDP)。產(chǎn)品的一個重要部分類似企業(yè)版的”捷徑”,讓運(yùn)營人員可以像搭樂高積木一樣創(chuàng)建企業(yè)的自動化流程,無需編程即可讓數(shù)據(jù)流動起來。從這一點(diǎn)上,我們的業(yè)務(wù)特點(diǎn)就是聚少成多,把一個個服務(wù)連接起來就成了數(shù)據(jù)的海洋。理念上跟微服務(wù)一致,一個個獨(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ù)的好處是什么?
相比于單體應(yīng)用,每個服務(wù)的復(fù)雜度會下降,特別是數(shù)據(jù)層面(數(shù)據(jù)表關(guān)系)更清晰,不會一個應(yīng)用上百張表,新員工上手快;
對于穩(wěn)定的核心業(yè)務(wù)可以單獨(dú)成為一個服務(wù),降低該服務(wù)的發(fā)布頻率,也減少測試人員壓力;
可以將不同密集型的服務(wù)搭配著放到物理機(jī)上,或者單獨(dú)對某個服務(wù)進(jìn)行擴(kuò)容,實(shí)現(xiàn)硬件資源的充分利用;
部署靈活,在私有化項(xiàng)目中,如果客戶有不需要的業(yè)務(wù),那么對應(yīng)的微服務(wù)就不需要部署,節(jié)省硬件成本,就像上文提到的樂高積木理念。
微服務(wù)有什么挑戰(zhàn)?
一旦設(shè)計(jì)不合理,交叉調(diào)用,相互依賴頻繁,就會出現(xiàn)牽一發(fā)動全身的局面。想象單個應(yīng)用內(nèi)service層依賴復(fù)雜的場面就明白了;
項(xiàng)目多了,輪子需求也會變多,需要有人專注公共代碼的開發(fā);
開發(fā)過程的質(zhì)量需要通過持續(xù)集成(CI)嚴(yán)格把控,提高自動化測試的比例,因?yàn)橥粋€接口改動會涉及多個項(xiàng)目,光靠人工測試很難覆蓋所有情況;
發(fā)布過程會變得復(fù)雜,因?yàn)槲⒎?wù)要發(fā)揮全部能力需要容器化的加持,容器編排就是最大的挑戰(zhàn);
線上運(yùn)維,當(dāng)系統(tǒng)出現(xiàn)問題需要快速定位到某個機(jī)器節(jié)點(diǎn)或具體服務(wù),監(jiān)控和鏈路日志分析都必不可少。
下面詳細(xì)說說我們是怎么應(yīng)對這些挑戰(zhàn)的:
開發(fā)過程的挑戰(zhàn)
持續(xù)集成:
通過CI將開發(fā)過程規(guī)范化,串聯(lián)自動化測試和人工Review;
我們使用Gerrit作為代碼&分支管理工具,在流程管理上遵循Gitlab的工作流模型;
開發(fā)人員提交代碼至Gerrit的magic分支;
代碼Review人員Review代碼并給出評分;
對應(yīng)Repo的Jenkins job監(jiān)聽分支上的變動,觸發(fā)Build job。經(jīng)過IT和Sonar的靜態(tài)代碼檢查給出評分;
Review和Verify皆通過之后,相應(yīng)Repo的負(fù)責(zé)人將代碼merge到真實(shí)分支上若有一項(xiàng)不通過,代碼修改后重復(fù)過程;
Gerrit將代碼實(shí)時同步備份至的兩個遠(yuǎn)程倉庫中。
集成測試
一般來說代碼自動執(zhí)行的都是單元測試(Unit Test),即不依賴任何資源(數(shù)據(jù)庫,消息隊(duì)列)和其他服務(wù),只測試本系統(tǒng)的代碼邏輯。但這種測試需要mock的部分非常多,一是寫起來復(fù)雜,二是代碼重構(gòu)起來跟著改的測試用例也非常多,顯得不夠敏捷。而且一旦要求開發(fā)團(tuán)隊(duì)要達(dá)到某個覆蓋率,就會出現(xiàn)很多造假的情況。所以我們選擇主要針對API進(jìn)行測試,即針對controller層的測試。另外對于一些公共組件如分布式鎖,json序列化模塊也會有對應(yīng)的測試代碼覆蓋。測試代碼在運(yùn)行時會采用一個隨機(jī)端口拉起項(xiàng)目,并通過http client對本地API發(fā)起請求,測試只會對外部服務(wù)做mock,數(shù)據(jù)庫的讀寫,消息隊(duì)列的消費(fèi)等都是真實(shí)操作,相當(dāng)于把Jmeter的事情在Java層面完成一部分。Spring Boot項(xiàng)目可以很容易的啟動這樣一個測試環(huán)境,代碼如下:
@RunWith(SpringRunner.class)@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
測試過程的http client推薦使用io.rest-assured:rest-assured支持JsonPath,十分好用。
測試時需要注意的一個點(diǎn)是測試數(shù)據(jù)的構(gòu)造和清理。構(gòu)造又分為schema的創(chuàng)建和測試數(shù)據(jù)的創(chuàng)建。
schema由flyway處理,在啟用測試環(huán)境前先刪除所有表,再進(jìn)行表的創(chuàng)建;
測試數(shù)據(jù)可以通過@Sql讀取一個sql文件進(jìn)行創(chuàng)建,在一個用例結(jié)束后再清除這些數(shù)據(jù)。
順帶說一下,基于flyway的schema upgrade功能我們封成了獨(dú)立的項(xiàng)目,每個微服務(wù)都有自己的upgrade項(xiàng)目,好處一是支持command-line模式,可以細(xì)粒度的控制升級版本,二是也可以支持分庫分表以后的schema操作。upgrade項(xiàng)目也會被制作成docker image提交到docker hub。
測試在每次提交代碼后都會執(zhí)行,Jenkins監(jiān)聽gerrit的提交,通過docker run -rm {upgrade項(xiàng)目的image}先執(zhí)行一次schema upgrade,然后gradle test執(zhí)行測試。最終會生成測試報告和覆蓋率報告,覆蓋率報告采用jacoco的gradle插件生成。如圖:
這里多提一點(diǎn),除了集成測試,服務(wù)之間的接口要保證兼容,實(shí)際上還需要一種consumer-driven testing tool,就是說接口消費(fèi)端先寫接口測試用例,然后發(fā)布到一個公共區(qū)域,接口提供方發(fā)布接口時也會執(zhí)行這個公共區(qū)域的用例,一旦測試失敗,表示接口出現(xiàn)了不兼容的情況。比較推薦大家使用Pact或是Spring Cloud Contact。我們目前的契約基于”人的信任“,畢竟服務(wù)端開發(fā)者還不多,所以沒有必要使用這樣一套工具。
集成測試的同時還會進(jìn)行靜態(tài)代碼檢查,我們用的是sonar,當(dāng)所有檢查通過后jenkins會+1分,再由reviewer進(jìn)行代碼review。
自動化測試
單獨(dú)拿自動化測試出來說,就是因?yàn)樗琴|(zhì)量保證的非常重要的一環(huán),上文能在CI中執(zhí)行的測試都是針對單個微服務(wù)的,那么當(dāng)所有服務(wù)(包括前端頁面)都在一起工作的時候是否會出現(xiàn)問題,就需要一個更接近線上的環(huán)境來進(jìn)行測試了。
在自動化測試環(huán)節(jié),我們結(jié)合Docker提高一定的工作效率并提高測試運(yùn)行時環(huán)境的一致性以及可移植性。在準(zhǔn)備好基礎(chǔ)的Pyhton鏡像以及Webdriver(selenium)之后,我們的自動化測試工作主要由以下主要步驟組成:
測試人員在本地調(diào)試測試代碼并提交至Gerrit;
Jenkins進(jìn)行測試運(yùn)行時環(huán)境的鏡像制作,主要將引用的各種組件和庫打包進(jìn)一個Python的基礎(chǔ)鏡像;
通過Jenkins定時或手動觸發(fā),調(diào)用環(huán)境部署的job將專用的自動化測試環(huán)境更新,然后拉取自動化測試代碼啟動一次性的自動化測試運(yùn)行時環(huán)境的Docker容器,將代碼和測試報告的路徑鏡像至容器內(nèi);
自動化測試過程將在容器內(nèi)進(jìn)行;
測試完成之后,不必手動清理產(chǎn)生的各種多余內(nèi)容,直接在Jenkins上查看發(fā)布出來的測試結(jié)果與趨勢。
關(guān)于部分性能測試的執(zhí)行,我們同樣也將其集成到Jenkins中,在可以直觀的通過一些結(jié)果數(shù)值來觀察版本性能變化情況的回歸測試和基礎(chǔ)場景,將會很大程度的提高效率、便捷的觀察趨勢。
測試人員在本地調(diào)試測試代碼并提交至Gerrit;
通過Jenkins定時或手動觸發(fā),調(diào)用環(huán)境部署的job將專用的性能測試環(huán)境更新以及可能的Mock Server更新;
拉取最新的性能測試代碼,通過Jenkins的性能測試插件來調(diào)用測試腳本;
測試完成之后,直接在Jenkins上查看通過插件發(fā)布出來的測試結(jié)果與趨勢。
發(fā)布過程的挑戰(zhàn)
上面提到微服務(wù)一定需要結(jié)合容器化才能發(fā)揮全部優(yōu)勢,容器化就意味線上有一套容器編排平臺。我們目前采用是Redhat的Openshift。所以發(fā)布過程較原來只是啟動jar包相比要復(fù)雜的多,需要結(jié)合容器編排平臺的特點(diǎn)找到合適的方法。
鏡像準(zhǔn)備
公司開發(fā)基于gitlab的工作流程,git分支為master,pre-production和prodution三個分支,同時生產(chǎn)版本發(fā)布都打上對應(yīng)的tag。每個項(xiàng)目代碼里面都包含dockerfile與jenkinsfile,通過jenkins的多分支pipeline來打包docker鏡像并推送到harbor私庫上。
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時會在每次pull代碼之后調(diào)用harbor的api來判斷此版本的docker image是否已經(jīng)存在,如果存在就不執(zhí)行后續(xù)編譯打包的stage。在jenkins的發(fā)布任務(wù)中會調(diào)用打包job,避免了重復(fù)打包鏡像,這樣就大大的加快了發(fā)布速度。
數(shù)據(jù)庫Schema升級
數(shù)據(jù)庫的升級用的是flyway,打包成docker鏡像后,在openshift中創(chuàng)建job去執(zhí)行數(shù)據(jù)庫升級。job可以用最簡單的命令行的方式去創(chuàng)建:
oc run upgrade-foo --image=upgrade/production --replicas=1 --restart=OnFailure --command -- java -jar -Dprofile=production /app/upgrade-foo.jar腳本升級任務(wù)也集成在jenkins中。
容器發(fā)布
openshift有個特別概念叫DeploymentConfig,原生k8s Deployment與之相似,但openshift的DeploymentConfig功能更多些。
Deploymentconfig關(guān)聯(lián)了一個叫做ImageStreamTag的東西,而這個ImagesStreamTag和實(shí)際的鏡像地址做關(guān)聯(lián),當(dāng)ImageStreamTag關(guān)聯(lián)的鏡像地址發(fā)生了變更,就會觸發(fā)相應(yīng)的DeploymentConfig重新部署。我們發(fā)布是使用了jenkins+openshift插件,只需要將項(xiàng)目對應(yīng)的ImageStreamTag指向到新生成的鏡像上,就觸發(fā)了部署。
如果是服務(wù)升級,已經(jīng)有容器在運(yùn)行怎么實(shí)現(xiàn)平滑替換而不影響業(yè)務(wù)呢?
配置Pod的健康檢查,Health Check只配置了ReadinessProbe,沒有用LivenessProbe。因?yàn)長ivenessProbe在健康檢查失敗之后,會將故障的pod直接干掉,故障現(xiàn)場沒有保留,不利于問題的排查定位。而ReadinessProbe只會將故障的pod從service中踢除,不接受流量。使用了ReadinessProbe后,可以實(shí)現(xiàn)滾動升級不中斷業(yè)務(wù),只有當(dāng)pod健康檢查成功之后,關(guān)聯(lián)的service才會轉(zhuǎn)發(fā)流量請求給新升級的pod,并銷毀舊的pod。
readinessProbe:failureThreshold: 4
httpGet:
path: /actuator/metrics
port: 8090
scheme: HTTP
initialDelaySeconds: 60
periodSeconds: 15
successThreshold: 2
timeoutSeconds: 2
線上運(yùn)維的挑戰(zhàn)
服務(wù)間調(diào)用
Spring Cloud使用eruka接受服務(wù)注冊請求,并在內(nèi)存中維護(hù)服務(wù)列表。當(dāng)一個服務(wù)作為客戶端發(fā)起跨服務(wù)調(diào)用時,會先獲取服務(wù)提供者列表,再通過某種負(fù)載均衡算法取得具體的服務(wù)提供者地址(ip + port),即所謂的客戶端服務(wù)發(fā)現(xiàn)。在本地開發(fā)環(huán)境中我們使用這種方式。
由于Openshift天然就提供服務(wù)端服務(wù)發(fā)現(xiàn),即service模塊,客戶端無需關(guān)注服務(wù)發(fā)現(xiàn)具體細(xì)節(jié),只需知道服務(wù)的域名就可以發(fā)起調(diào)用。由于我們有nodejs應(yīng)用,在實(shí)現(xiàn)eureka的注冊和去注冊的過程中都遇到過一些問題,不能達(dá)到生產(chǎn)級別。所以決定直接使用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)境變量列表。
如果一個服務(wù)需要暴露到外部怎么辦,比如暴露前端的html文件或者服務(wù)端的gateway。
Openshift內(nèi)置的haproxy router,相當(dāng)于k8s的ingress,直接在Openshift的web界面里面就可以很方便的配置。我們將前端的資源也作為一個Pod并有對應(yīng)的Service,當(dāng)請求進(jìn)入haproxy符合規(guī)則就會轉(zhuǎn)發(fā)到ui所在的Service。router支持A/B test等功能,唯一的遺憾是還不支持url rewrite。
對于需要url rewrite的場景怎么辦?那么就直接將nginx也作為一個服務(wù),再做一層轉(zhuǎn)發(fā)。流程變成 router → nginx pod → 具體提供服務(wù)的pod。
鏈路跟蹤
開源的全鏈路跟蹤很多,比如spring cloud sleuth + zipkin,國內(nèi)有美團(tuán)的CAT等等。其目的就是當(dāng)一個請求經(jīng)過多個服務(wù)時,可以通過一個固定值獲取整條請求鏈路的行為日志,基于此可以再進(jìn)行耗時分析等,衍生出一些性能診斷的功能。不過對于我們而言,首要目的就是trouble shooting,出了問題需要快速定位異常出現(xiàn)在什么服務(wù),整個請求的鏈路是怎樣的。
為了讓解決方案輕量,我們在日志中打印RequestId以及TraceId來標(biāo)記鏈路。RequestId在gateway生成表示唯一一次請求,TraceId相當(dāng)于二級路徑,一開始與RequestId一樣,但進(jìn)入線程池或者消息隊(duì)列后,TraceId會增加標(biāo)記來標(biāo)識唯一條路徑。舉個例子,當(dāng)一次請求會向MQ發(fā)送一個消息,那么這個消息可能會被多個消費(fèi)者消費(fèi),此時每個消費(fèi)線程都會自己生成一個TraceId來標(biāo)記消費(fèi)鏈路。加入TraceId的目的就是為了避免只用RequestId過濾出太多日志。
實(shí)現(xiàn)上,通過ThreadLocal存放APIRequestContext串聯(lián)單服務(wù)內(nèi)的所有調(diào)用,當(dāng)跨服務(wù)調(diào)用時,將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的場景,一般半小時內(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后也會自動添加進(jìn)來。
Java項(xiàng)目也添加了prometheus的監(jiān)控端點(diǎn),只是可惜cluster-monitoring-operator提供的配置是只讀的,后期將研究怎么將java的jvm監(jiān)控這些整合進(jìn)來。
MORE
開源軟件是對中小團(tuán)隊(duì)的一種福音,無論是Spring Cloud還是k8s都大大降低了團(tuán)隊(duì)在基礎(chǔ)設(shè)施建設(shè)上的時間成本。當(dāng)然其中有更多的話題,比如服務(wù)升降級,限流熔斷,分布式任務(wù)調(diào)度,灰度發(fā)布,功能開關(guān)等等都需要更多時間來探討。對于小團(tuán)隊(duì),要根據(jù)自身情況選擇微服務(wù)的技術(shù)方案,不可一味追新,適合自己的才是最好的。
作者簡介:徐鵬, Linkflow運(yùn)維開發(fā)負(fù)責(zé)人
聲明:本文為作者投稿,版權(quán)歸作者個人所有。
【END】
總結(jié)
以上是生活随笔為你收集整理的java 拉起服务_技术开发者应该如何构建小团队的微服务方案?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python中改变参数值的方法_Pyth
- 下一篇: html排序按钮_插件分享 | 可进行排