新聞中心
微服務(wù)是否適合小團(tuán)隊(duì)是個(gè)見仁見智的問題。回歸現(xiàn)象看本質(zhì),隨著業(yè)務(wù)復(fù)雜度的提高,單體應(yīng)用越來越龐大,就好像一個(gè)類的代碼行越來越多,分而治之,切成多個(gè)類應(yīng)該是更好的解決方法。

10年積累的成都做網(wǎng)站、成都網(wǎng)站建設(shè)經(jīng)驗(yàn),可以快速應(yīng)對客戶對網(wǎng)站的新想法和需求。提供各種問題對應(yīng)的解決方案。讓選擇我們的客戶得到更好、更有力的網(wǎng)絡(luò)服務(wù)。我雖然不認(rèn)識你,你也不認(rèn)識我。但先網(wǎng)站設(shè)計(jì)后付款的網(wǎng)站建設(shè)流程,更有華坪免費(fèi)網(wǎng)站建設(shè)讓你可以放心的選擇與我們合作。
所以一個(gè)龐大的單體應(yīng)用分出多個(gè)小應(yīng)用也更符合這種分治的思想。當(dāng)然微服務(wù)架構(gòu)不應(yīng)該是一個(gè)小團(tuán)隊(duì)一開始就該考慮的問題,而是慢慢演化的結(jié)果,謹(jǐn)慎過度設(shè)計(jì)尤為重要。
公司的背景是提供 SaaS 服務(wù),對于大客戶也會有定制開發(fā)以及私有化部署。經(jīng)過 2 年不到的時(shí)間,技術(shù)架構(gòu)經(jīng)歷了從單體到微服務(wù)再到容器化的過程。
單體應(yīng)用時(shí)代
早期開發(fā)只有兩個(gè)人,考慮微服務(wù)之類的都是多余。不過由于受前公司影響,最初就決定了前后端分離的路線,因?yàn)椴恍枰紤] SEO 的問題,索性就做成了 SPA 單頁應(yīng)用。
多說一句,前后端分離也不一定就不能服務(wù)端渲染,例如電商系統(tǒng)或者一些匿名即可訪問的系統(tǒng),加一層薄薄的 View 層,無論是 PHP 還是用 Thymeleaf 都是不錯(cuò)的選擇。
部署架構(gòu)上,我們使用 Nginx 代理前端 HTML 資源,在接收請求時(shí)根據(jù)路徑反向代理到 Server 的 8080 端口實(shí)現(xiàn)業(yè)務(wù)。
接口定義
接口按照標(biāo)準(zhǔn)的 Restful 來定義:
- 版本,統(tǒng)一跟在 /api/ 后面,例如 /api/v2。
- 以資源為中心,使用復(fù)數(shù)表述,例如 /api/contacts,也可以嵌套,如 /api/groups/1/contacts/100。
- url 中盡量不使用動詞,實(shí)踐中發(fā)現(xiàn)做到這一點(diǎn)真的比較難,每個(gè)研發(fā)人員的思路不一致,起的名字也千奇百怪,都需要在代碼 Review 中覆蓋。
- 動作支持,POST / PUT / DELELE / GET ,這里有一個(gè)坑,PUT 和 PATCH 都是更新,但是 PUT 是全量更新而 PATCH 是部分更新,前者如果傳入的字段是空(未傳也視為空)那么也會被更新到數(shù)據(jù)庫中。
- 目前我們雖然是使用 PUT 但是忽略空字段和未傳字段,本質(zhì)上是一種部分更新,這也帶來了一些問題,比如確有置空的業(yè)務(wù)需要特殊處理。
- 接口通過 swagger 生成文檔供前端同事使用。
持續(xù)集成(CI)
團(tuán)隊(duì)初始成員之前都有在大團(tuán)隊(duì)共事的經(jīng)歷,所以對于質(zhì)量管控和流程管理都有一些共同的要求。
因此在開發(fā)之初就引入了集成測試的體系,可以直接開發(fā)針對接口的測試用例,統(tǒng)一執(zhí)行并計(jì)算覆蓋率。
一般來說代碼自動執(zhí)行的都是單元測試(Unit Test),我們之所以叫集成測試是因?yàn)闇y試用例是針對 API 的,并且包含了數(shù)據(jù)庫的讀寫,MQ 的操作等等。
除了外部服務(wù)的依賴基本都是符合真實(shí)生產(chǎn)場景,相當(dāng)于把 Jmeter 的事情直接在 Java 層面做掉了。
這在開發(fā)初期為我們提供了非常大的便利性。但值得注意的是,由于數(shù)據(jù)庫以及其他資源的引入,數(shù)據(jù)準(zhǔn)備以及數(shù)據(jù)清理時(shí)要考慮的問題就會更多,例如如何控制并行任務(wù)之間的測試數(shù)據(jù)互不影響等等。
為了讓這一套流程可以自動化的運(yùn)作起來, 引入 Jenkins 也是理所當(dāng)然的事情了。
開發(fā)人員提交代碼進(jìn)入 Gerrit 中,Jenkins 被觸發(fā)開始編譯代碼并執(zhí)行集成測試,完成后生成測試報(bào)告,測試通過再由 reviewer 進(jìn)行代碼 Review。
在單體應(yīng)用時(shí)代這樣的 CI 架構(gòu)已經(jīng)足夠好用,由于有集成測試的覆蓋,在保持 API 兼容性的前提下進(jìn)行代碼重構(gòu)都會變得更有信心。
微服務(wù)時(shí)代
服務(wù)拆分原則
從數(shù)據(jù)層面看,最簡單的方式就是看數(shù)據(jù)庫的表之間是否有比較少的關(guān)聯(lián)。例如最容易分離的一般來說都是用戶管理模塊。
如果從領(lǐng)域驅(qū)動設(shè)計(jì)(DDD)看,其實(shí)一個(gè)服務(wù)就是一個(gè)或幾個(gè)相關(guān)聯(lián)的領(lǐng)域模型,通過少量數(shù)據(jù)冗余劃清服務(wù)邊界。
單個(gè)服務(wù)內(nèi)通過領(lǐng)域服務(wù)完成多個(gè)領(lǐng)域?qū)ο髤f(xié)作。當(dāng)然 DDD 比較復(fù)雜,要求領(lǐng)域?qū)ο笤O(shè)計(jì)上是充血模型而非貧血模型。
從實(shí)踐角度講,充血模型對于大部分開發(fā)人員來說難度非常高,什么代碼應(yīng)該屬于行為,什么屬于領(lǐng)域服務(wù),很多時(shí)候非常考驗(yàn)人員水平。
服務(wù)拆分是一個(gè)大工程,往往需要幾個(gè)對業(yè)務(wù)以及數(shù)據(jù)最熟悉的人一起討論,甚至要考慮到團(tuán)隊(duì)結(jié)構(gòu),最終的效果是服務(wù)邊界清晰, 沒有環(huán)形依賴和避免雙向依賴。
框架選擇
由于之前的單體服務(wù)使用的是 Spring Boot,所以框架自然而然的選擇了 Spring Cloud。
其實(shí)個(gè)人認(rèn)為微服務(wù)框架不應(yīng)該限制技術(shù)與語言,但生產(chǎn)實(shí)踐中發(fā)現(xiàn)無論 Dubbo 還是 Spring Cloud 都具有侵入性。
我們在將 Node.js 應(yīng)用融入 Spring Cloud 體系時(shí)就發(fā)現(xiàn)了許多問題,也許未來的 Service Mesh 才是更合理的發(fā)展道路。
該圖取自純潔的微笑公眾號
這是典型的 Spring Cloud 的使用方法:
- Zuul 作為 Gateway,分發(fā)不同客戶端的請求到具體 Service。
- Eureka 作為注冊中心,完成了服務(wù)發(fā)現(xiàn)和服務(wù)注冊。
- 每個(gè) Service 包括 Gateway 都自帶了 Hystrix 提供的限流和熔斷功能。
- Service 之間通過 Feign 和 Ribbon 互相調(diào)用,F(xiàn)eign 實(shí)際上是屏蔽了 Service 對 Eureka 的操作。
上文說的一旦要融入異構(gòu)語言的 Service,那么服務(wù)注冊,服務(wù)發(fā)現(xiàn),服務(wù)調(diào)用,熔斷和限流都需要自己處理。
再有關(guān)于 Zuul 要多說幾句,Spring Cloud 提供的 Zuul 對 Netflix 版本的做了裁剪,去掉了動態(tài)路由功能(Groovy 實(shí)現(xiàn))。
另外一點(diǎn)就是 Zuul 的性能一般,由于采用同步編程模型,對于 IO 密集型等后臺處理時(shí)間長的鏈路非常容易將 Servlet 的線程池占滿。
所以如果將 Zuul 與主要 Service 放置在同一臺物理機(jī)上,在流量大的情況下,Zuul 的資源消耗非常大。
實(shí)際測試也發(fā)現(xiàn)經(jīng)過 Zuul 與直接調(diào)用 Service 的性能損失在 30% 左右,并發(fā)壓力大時(shí)更為明顯。
現(xiàn)在 Spring Cloud Gateway 是 Pivotal 主推的,支持異步編程模型,后續(xù)架構(gòu)優(yōu)化也許會采用,或是直接使用 Kong 這種基于 Nginx 的網(wǎng)關(guān)來提供優(yōu)質(zhì)性能。
當(dāng)然同步模型也有優(yōu)點(diǎn),編碼更簡單,后文將會提到使用 ThreadLocal 如何建立鏈路跟蹤。
架構(gòu)改造
經(jīng)過大半年的改造以及新需求的加入,單體服務(wù)被不斷拆分,最終形成了 10 余個(gè)微服務(wù),并且搭建了 Spark 用于 BI。
初步形成兩大體系,微服務(wù)架構(gòu)的在線業(yè)務(wù)系統(tǒng)(OLTP) + Spark 大數(shù)據(jù)分析系統(tǒng)(OLAP)。
數(shù)據(jù)源從只有 MySQL 增加到了 ES 和 Hive。多數(shù)據(jù)源之間的數(shù)據(jù)同步也是值得一說的話題,但內(nèi)容太多不在此文贅述。
服務(wù)拆分我們采用直接割接的方式,數(shù)據(jù)表也是整體遷移。因?yàn)閹状未蟾脑斓纳壣暾埩送7?,所以步驟相對簡單。
如果需要不停服升級,那么應(yīng)該采用先雙寫再逐步切換的方式保證業(yè)務(wù)不受影響。
自動化部署
與 CI 比起來,持續(xù)交付(CD)實(shí)現(xiàn)更為復(fù)雜,在資源不足的情況我們尚未實(shí)現(xiàn) CD,只是實(shí)現(xiàn)執(zhí)行了自動化部署。
由于生產(chǎn)環(huán)境需要通過跳板機(jī)操作,所以我們通過 Jenkins 生成 jar 包傳輸?shù)教鍣C(jī),之后再通過 Ansible 部署到集群。
簡單粗暴的部署方式在小規(guī)模團(tuán)隊(duì)開發(fā)時(shí)還是夠用的,只是需要在部署前保證測試(人工測試 + 自動化測試)到位。
鏈路跟蹤
開源的全鏈路跟蹤很多,比如 Spring Cloud Sleuth + Zipkin,國內(nèi)有美團(tuán)的 CAT 等等。
其目的就是當(dāng)一個(gè)請求經(jīng)過多個(gè)服務(wù)時(shí),可以通過一個(gè)固定值獲取整條請求鏈路的行為日志,基于此可以再進(jìn)行耗時(shí)分析等,衍生出一些性能診斷的功能。
不過對于我們而言,首要目的就是 Trouble Shooting,出了問題需要快速定位異常出現(xiàn)在什么服務(wù),整個(gè)請求的鏈路是怎樣的。
為了讓解決方案輕量,我們在日志中打印 RequestId 以及 TraceId 來標(biāo)記鏈路。
RequestId 在 Gateway 生成表示的一次請求,TraceId 相當(dāng)于二級路徑,一開始與 RequestId 一樣,但進(jìn)入線程池或者消息隊(duì)列后,TraceId 會增加標(biāo)記來標(biāo)識一條路徑。
舉個(gè)例子,當(dāng)一次請求會向 MQ 發(fā)送一個(gè)消息,那么這個(gè)消息可能會被多個(gè)消費(fèi)者消費(fèi),此時(shí)每個(gè)消費(fèi)線程都會自己生成一個(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 的場景,一般半小時(shí)內(nèi)即可定位到具體業(yè)務(wù)。
運(yùn)維監(jiān)控
在容器化之前,采用 Telegraf + InfluxDB + Grafana 的方案。Telegraf 作為探針收集 JVM,System,MySQL 等資源的信息,寫入 InfluxDB,最終通過 Grafana 做數(shù)據(jù)可視化。
Spring Boot Actuator 可以配合 Jolokia 暴露 JVM 的 Endpoint。整個(gè)方案零編碼,只需要花時(shí)間配置。
容器化時(shí)代
架構(gòu)改造
因?yàn)樵谧鑫⒎?wù)之初就計(jì)劃了容器化,所以架構(gòu)并未大動,只是每個(gè)服務(wù)都會建立一個(gè) Dockerfile 用于創(chuàng)建 docker image。
涉及變化的部分包括:
- CI 中多了構(gòu)建 docker image 的步驟。
- 自動化測試過程中將數(shù)據(jù)庫升級從應(yīng)用中剝離單獨(dú)做成 docker image。
- 生產(chǎn)中用 Kubernetes 自帶的 Service 替代了 Eruka。
理由下文一一道來。
Spring Cloud&Kubernetes 融合
我們使用的是 Redhat 的 OpenShift,可以認(rèn)為是 Kubernetes 企業(yè)版,其本身就有 Service 的概念。一個(gè) Service 下有多個(gè) Pod,Pod 內(nèi)即是一個(gè)可服務(wù)單元。
Service 之間互相調(diào)用時(shí) Kubernetes 會提供默認(rèn)的負(fù)載均衡控制,發(fā)起調(diào)用方只需要寫被調(diào)用方的 ServiceId 即可。
這一點(diǎn)和 Spring Cloud Fegin 使用 Ribbon 提供的功能如出一轍。也就是說服務(wù)治理可以通過 Kubernetes 來解決,那么為什么要替換呢?
其實(shí)上文提到了,Spring Cloud 技術(shù)棧對于異構(gòu)語言的支持問題,我們有許多 BFF(Backend for Frontend)是使用 Node.js 實(shí)現(xiàn)的。
這些服務(wù)要想融合到 Spring Cloud 中,服務(wù)注冊,負(fù)載均衡,心跳檢查等等都要自己實(shí)現(xiàn)。
如果以后還有其他語言架構(gòu)的服務(wù)加入進(jìn)來,這些輪子又要重造?;诖祟愒蚓C合考量后,決定采用 OpenShift 所提供的網(wǎng)絡(luò)能力替換 Eruka。
由于本地開發(fā)和聯(lián)調(diào)過程中依然依賴 Eruka,所以只在生產(chǎn)上通過配置參數(shù)來控制:
- eureka.client.enabled 設(shè)置為 false,停止各服務(wù)的 Eureka 注冊。
- ribbon.eureka.enabled 設(shè)置為 false,讓 Ribbon 不從 Eureka 獲取服務(wù)列表。
- 以服務(wù) foo 為例,foo.ribbon.listofservers 設(shè)置為 http://foo:8080,那么當(dāng)一個(gè)服務(wù)需要使用服務(wù) foo 的時(shí)候,就會直接調(diào)用到 http://foo:8080。
CI 的改造
CI 的改造主要是多了一部編譯 docker image 并打包到 Harbor 的過程,部署時(shí)會直接從 Harbor 拉取鏡像。另一個(gè)就是數(shù)據(jù)庫的升級工具。
之前我們使用 Flyway 作為數(shù)據(jù)庫升級工具,當(dāng)應(yīng)用啟動時(shí)自動執(zhí)行 SQL 腳本。
隨著服務(wù)實(shí)例越來越多,一個(gè)服務(wù)的多個(gè)實(shí)例同時(shí)升級的情況也時(shí)有發(fā)生,雖然 Flyway 是通過數(shù)據(jù)庫鎖實(shí)現(xiàn)了升級過程不會有并發(fā),但會導(dǎo)致被鎖服務(wù)啟動時(shí)間變長的問題。
從實(shí)際升級過程來看,將可能發(fā)生的并發(fā)升級變?yōu)閱我贿M(jìn)程可能更靠譜。此外后期分庫分表的架構(gòu)也會使隨應(yīng)用啟動自動升級數(shù)據(jù)庫變的困難。
綜合考量,我們將升級任務(wù)做了拆分,每個(gè)服務(wù)都有自己的升級項(xiàng)目并會做容器化。
在使用時(shí),作為 run once 的工具來使用,即 docker run -rm 的方式。并且后續(xù)也支持了設(shè)定目標(biāo)版本的功能,在私有化項(xiàng)目的跨版本升級中起到了非常好的效果。
至于自動部署,由于服務(wù)之間存在上下游關(guān)系,例如 Config,Eruka 等屬于基本服務(wù)被其他服務(wù)依賴,部署也產(chǎn)生了先后順序?;?Jenkins 做 Pipeline 可以很好的解決這個(gè)問題。
小結(jié)
以上的每一點(diǎn)都可以深入的寫成一篇文章,微服務(wù)的架構(gòu)演進(jìn)涉及到開發(fā),測試和運(yùn)維,要求團(tuán)隊(duì)內(nèi)多工種緊密合作。
分治是軟件行業(yè)解決大系統(tǒng)的不二法門,作為小團(tuán)隊(duì)我們并沒有盲目追新,而是在發(fā)展的過程通過服務(wù)化的方式解決問題。
從另一方面我們也體會到了微服務(wù)對于人的要求,以及對于團(tuán)隊(duì)的挑戰(zhàn)都比過去要高要大。未來仍需探索,演進(jìn)仍在路上。
網(wǎng)站名稱:大廠都在玩的微服務(wù),小團(tuán)隊(duì)如何應(yīng)用?
網(wǎng)址分享:http://www.5511xx.com/article/dheichh.html


咨詢
建站咨詢
