繼續(xù)來深入探討!在之前的文章(第一部分)中,我們?yōu)楸酒恼陆⒘艘粋€(gè)上下文環(huán)境(以便于討論)。一個(gè)基本原則是,當(dāng)微服務(wù)被引入到現(xiàn)有架構(gòu)中時(shí),不能也不應(yīng)該破壞當(dāng)前的請求流程(request flows)?!皢误w應(yīng)用(monolish)”程序依然能帶來很多商業(yè)價(jià)值(因此仍將在新的時(shí)代被使用,編者注),我們只能在迭代和擴(kuò)展時(shí),盡可能地減少其負(fù)面影響,這過程中就有一個(gè)經(jīng)常被忽略的事實(shí):當(dāng)我們開始探索如何從單體應(yīng)用過渡到微服務(wù)時(shí),會遇到一些我們不愿意碰到的難題,但顯然我們不能視而不見。如果你還沒讀過這段內(nèi)容,我建議你再回去看看第一部分。同時(shí)也可以參考什么時(shí)候不要做微服務(wù) [0]。
以下這些技術(shù)在我們的實(shí)踐過程中將具備一定的指導(dǎo)作用:
我使用的是 http://developers. 上的 TicketMonster 教程,顯示從單體應(yīng)用到微服務(wù)的演變,如果感興趣的話可以關(guān)注,你還可以在 github 上找到相關(guān)的代碼和文檔(文檔還在編寫中):https://github.com/ticket-monster-msa/monolith 讓我們一步步地讀完第一部分 [21],具體來看看每一步應(yīng)該怎么實(shí)施。中間還會引入上一部分中出現(xiàn)的一些注意事項(xiàng),并在當(dāng)前背景下再討論一遍。 回顧下注意事項(xiàng):
可以的話,盡可能為單體應(yīng)用安排大量的測試,哪怕不是一直有效。隨著演變的開始,無論是添加新功能還是替換現(xiàn)有功能,我們都需要清楚了解任何更改可能產(chǎn)生的影響。Michael Feathers 在他《重構(gòu)遺留代碼》[22] 的書中,將“遺留代碼(legacy code)”定義為沒有被測試所覆蓋的代碼。像 JUnit 和 Arquillian 這樣的工具就很能幫到大忙。使用 Arquillian,可以任意選擇遠(yuǎn)程方法調(diào)用的接口的顆粒大?。╢ine grain or coarse grain),然后打包應(yīng)用程序,不過仍需要用適當(dāng)?shù)哪M等方式,來運(yùn)行打算被測試的一部分程序。例如,在單體應(yīng)用(TicketMonster)中,我們可以定義一個(gè)微部署(micro-deployment),用來將原有的數(shù)據(jù)庫替換為內(nèi)存數(shù)據(jù)庫,并預(yù)加載一些樣例數(shù)據(jù)。Arquillian 適用于 Spring Boot 應(yīng)用、Java EE 等。在本例中,我們將測試一個(gè) Java EE 的單體架構(gòu): public static WebArchive deployment() { return ShrinkWrap .create(WebArchive.class, 'test.war') .addPackage(Resources.class.getPackage()) .addAsResource('META-INF/test-persistence.xml', 'META-INF/persistence.xml') .addAsResource('import.sql') .addAsWebInfResource(EmptyAsset.INSTANCE, 'beans.xml') // Deploy our test datasource .addAsWebInfResource('test-ds.xml');} 更有意思的是,嵌入在運(yùn)行環(huán)境中的測試可以用來驗(yàn)證內(nèi)部工作的所有組件。例如,在上面的一個(gè)測試中,我們可以將 BookingService 注入到測試中,并直接運(yùn)行: @RunWith(Arquillian.class)public class BookingServiceTest { @Deployment public static WebArchive deployment() { return RESTDeployment.deployment(); } @Inject private BookingService bookingService; @Inject private ShowService showService; @Test @InSequence(1) public void testCreateBookings() { BookingRequest br = createBookingRequest(1l, 0, new int[]{4, 1}, new int[]{1,1}, new int[]{3,1}); bookingService.createBooking(br); BookingRequest br2 = createBookingRequest(2l, 1, new int[]{6,1}, new int[]{8,2}, new int[]{10,2}); bookingService.createBooking(br2); BookingRequest br3 = createBookingRequest(3l, 0, new int[]{4,1}, new int[]{2,1}); bookingService.createBooking(br3); } 完整的示例請參閱 TicketMonster 單體應(yīng)用模塊 [23] 中的 BookingServiceTest。 測試的問題解決了,那么部署呢? Kubernetes 已成為容器化服務(wù)或應(yīng)用程序的實(shí)際部署平臺。Kubernetes 處理諸如健康度檢查、擴(kuò)展、重啟、負(fù)載平衡等事項(xiàng)。對于 Java 開發(fā)人員來說,像 fabric8-maven-plugin[24] 這樣的工具甚至都可以用來自動構(gòu)建容器或 docker 鏡像,并生成任意部署資源文件。OpenShift[25] 是 Red Hat 的 Kubernetes 的產(chǎn)品化版本,其中增加了開發(fā)人員的功能,包括 CI/CD pipelines 等。 無論是微服務(wù)、單體應(yīng)用還是其他平臺(比如能夠處理持續(xù)的工作負(fù)載,即數(shù)據(jù)庫等),Kubernetes/OpenShift 都是一個(gè)適用于應(yīng)用程序 / 服務(wù)的部署平臺。通過 Arquillian,容器和 OpenShift pipelines,可以持續(xù)地將變更引入生產(chǎn)環(huán)境。順便來看一下 openshift.io[26],它將開發(fā)經(jīng)驗(yàn)與自動 CI/CD pipelines、SCM 集成、Eclipse Che[27] 開發(fā)人員工作區(qū)、庫掃描等結(jié)合在一起。 目前,生產(chǎn)負(fù)載指向單體應(yīng)用。如果我們翻到它的主頁,我們會看到這樣的內(nèi)容: 接下來,讓我們開始做一些改變… 回顧下注意事項(xiàng):
如果我們看下 TicketMonster UI v1 [29] 代碼,就會發(fā)現(xiàn)它非常簡單。靜態(tài) HTML/JS/CSS 組件已經(jīng)被移到它自己的 Web 服務(wù)器,還被打包到一個(gè)容器中。通過這種方式,我們可以在單體應(yīng)用之外對它進(jìn)行單獨(dú)部署,并獨(dú)立更改或更新版本。這個(gè) UI 項(xiàng)目仍然需要與單體應(yīng)用對話來執(zhí)行它的功能,所以應(yīng)該是公開一個(gè) REST 接口,讓 UI 可以與之交互。對于一些單體應(yīng)用來說,這說起來容易做起來難。如果你想從遺留代碼中打包出來一個(gè)不錯(cuò)的 REST API,又遇到了挑戰(zhàn),我強(qiáng)烈推薦你看看 Apache Camel,尤其是它的 REST DSL。 比較有意思的是,實(shí)際上單體應(yīng)用并沒有被改變。它的代碼沒有變動,同時(shí)新 UI 也部署完成。如果查看 Kubernetes,我們會看到兩個(gè)單獨(dú)的部署對象和兩個(gè)單獨(dú)的 pod:一個(gè)用于單體架構(gòu),另一個(gè)用于 UI。 即使 tm-ui-v1 用戶界面部署完了,也沒有任何流量進(jìn)入這個(gè)新的 TicketMonster UI 組件。為了簡單起見,即使這個(gè)部署并沒有承載生產(chǎn)流量,而是 ticket-monster 這個(gè)單體應(yīng)用在承擔(dān)所有流量,我們?nèi)匀豢梢园阉?dāng)作一個(gè)簡單的灰度上線。相關(guān)的 UI 端口仍舊可以訪問: 接下來,用 kubectl cli 工具從本地端口轉(zhuǎn)發(fā)到特定的 pod(端口 80 上的 tm-ui-v1-3105082891-gh31x),并將其映射到本地端口 8080?,F(xiàn)在,如果導(dǎo)航到 http://localhost:8080,應(yīng)該得到一個(gè)新版本 UI(注意突出顯示的文本部分,表明這是一個(gè)不同的 UI,但它直接指向單體應(yīng)用) 如果我們這個(gè)新版本還算滿意,就可以開始將流量引入進(jìn)來。為此,我們將使用 Istio service mesh [30]。Istio 是用于管理由入口點(diǎn)和服務(wù)代理組成的網(wǎng)格控制層(control plane)。我已經(jīng)寫了一些關(guān)于像 Envoy 這樣的數(shù)據(jù)層 [31] 以及 service mesh[32] 的文章。我個(gè)人強(qiáng)烈建議看看 Istio 的全部功能。接下來的幾段內(nèi)容,我們會圍繞整個(gè)項(xiàng)目的全過程來依次展開討論 Istio 的各項(xiàng)功能。如果控制層和數(shù)據(jù)層之間的區(qū)分讓你困惑,請查看 Matt Klein[33] 撰寫的博客。 我們將從使用 Istio Ingress Controller[34] 開始。該組件允許使用 Kubernetes Ingress 規(guī)范來控制流量進(jìn)入 Kubernetes 集群。一旦安裝了 Istio,我們可以這樣創(chuàng)建一個(gè)入口資源,將流量指向 Ticket Monster UI 的 Kubernetes 服務(wù),tm-ui: apiVersion: extensions/v1beta1kind: Ingressmetadata: name: tm-gateway annotations: kubernetes.io/ingress.class: 'istio'spec: backend: serviceName: tm-ui servicePort: 80 一旦有了入口,就可以開始應(yīng)用 Istio 路由規(guī)則 [35]。例如,有一個(gè)規(guī)則,“任何時(shí)候有人試圖與在 Kubernetes 中運(yùn)行的 tm-ui 服務(wù)對話,將它們指向服務(wù)的第一版本 v1”: apiVersion: config.istio.io/v1alpha2kind: RouteRulemetadata: name: tm-ui-defaultspec: destination: name: tm-ui precedence: 1 route: - labels: version: v1 如此,我們能夠更好地控制進(jìn)入集群甚至深入集群內(nèi)部的流量。在這個(gè)步驟的最后,我們會將所有的流量都轉(zhuǎn)到 tm-ui-v1 部署。 回顧下注意事項(xiàng)
這一步相當(dāng)直接,通過刪除靜態(tài) UI 組件來更新單體應(yīng)用(刪除的部分已經(jīng)轉(zhuǎn)移到了 tm-ui-v1 部署)。既然應(yīng)用程序已經(jīng)被釋放成為一個(gè)單體應(yīng)用的服務(wù),以供 UI,API 或者其他一些程序調(diào)用,那么也可以對這個(gè)部署進(jìn)行一些 API 層級的更改。而如果想對 API 進(jìn)行一些更改,就需要部署一個(gè)新版本的 UI。此處我們部署了 backend-v1 服務(wù)以及一個(gè)新的 UI tm-ui-v2,可以利用后端服務(wù)中的這個(gè)新 API。 來看看在 Kubernetes 集群中的部署情況: 此時(shí),ticket-monster 和 tm-ui-v1 正接收實(shí)時(shí)流量。backend-v1 和指向它的 UI--tm-ui-v2 則沒有流量負(fù)載。需要注意的一點(diǎn)是,backend-v1 部署與 ticket-monster 部署共享數(shù)據(jù)庫,但各自有略微不同的外向 API(outward facing API)。 現(xiàn)在,新的 backend-v1 和 tm-ui-v2 組件已經(jīng)部署到生產(chǎn)環(huán)境中?,F(xiàn)在是時(shí)候把注意力放在一個(gè)簡單而又重要的事實(shí)上:生產(chǎn)環(huán)境部署發(fā)生了改變,但是它們還沒有發(fā)布。在 turblabs.io [36] 一些優(yōu)秀的博客更詳細(xì)地闡述了這一點(diǎn) [37]。現(xiàn)在,我們有機(jī)會部署一個(gè)非正式的灰度發(fā)布。也許我們希望這個(gè)部署慢慢來,首先面向內(nèi)部用戶,或者先對某個(gè)特定區(qū)域內(nèi),特定設(shè)備的部分用戶進(jìn)行部署等等。 既然已經(jīng)有了 Istio,接下來看看它能做些什么。我們只想為內(nèi)部用戶做一個(gè)灰度發(fā)布。我們可以用各種方式來識別內(nèi)部用戶,諸如 headers、IP 等等,在本例中,如果 HTTP header 帶有 x-dark-launch: v2 這樣的文本內(nèi)容,則該請求將會被路由到新的 backend-v1 和 tm -ui-v2 服務(wù)中。以下是 istio 路由規(guī)則的樣子: apiVersion: config.istio.io/v1alpha2kind: RouteRulemetadata: name: tm-ui-v2-dark-launchspec: destination: name: tm-ui precedence: 10 match: request: headers: x-dark-launch: exact: 'v2' route: - labels: version: v2 任意用戶身份登錄主頁時(shí),應(yīng)該可以看到當(dāng)前的部署(即指向 ticket-monster 單體應(yīng)用的 tm-ui-v1): 現(xiàn)在,如果改變?yōu)g覽器中的消息頭(例如使用 Firefox 的修改消息頭工具或其他類似工具),我們應(yīng)該被路由到已灰度上線的服務(wù)(指向 backend-v1 的 tm-ui-v2): 然后點(diǎn)擊“開始”開始修改消息頭并刷新頁面: 現(xiàn)在,我們已經(jīng)被重定向到服務(wù)的灰度發(fā)布版本。由此,可以通過做一個(gè)金絲雀發(fā)布(這里也許引 1%的實(shí)時(shí)流量到新部署),來向客戶群發(fā)布,同時(shí),如果沒有負(fù)面效果的話,那么就緩慢增加流量負(fù)載(5%、10%、50%等)。以下是 Istio 路由規(guī)則的一個(gè)例子,其將 v2 流量以 1%進(jìn)行金絲雀發(fā)布: apiVersion: config.istio.io/v1alpha2kind: RouteRulemetadata: name: tm-ui-v2-1pct-canaryspec: destination: name: tm-ui precedence: 20 route: - labels: version: v1 weight: 99 - labels: version: v2 weight: 1 能“看到”或“觀察”這個(gè)版本的影響是至關(guān)重要的,稍后我們會進(jìn)一步討論。另外請注意,這種金絲雀發(fā)布方式目前正在架構(gòu)外圍完成,但是也可以通過 istio 控制內(nèi)部服務(wù)間通訊 / 交互時(shí)采用金絲雀的方式。在接下來的幾個(gè)步驟中,我們將開始看到。 回顧下注意事項(xiàng)
在這一步中,我們開始設(shè)計(jì)我們所設(shè)想的新訂單服務(wù)的 API,在做一些領(lǐng)域驅(qū)動設(shè)計(jì)練習(xí)時(shí),我們常常需要確定一些邊界(boundaries),新的 API 應(yīng)該更多的與這種邊界相一致。這里可以使用 API 建模工具來設(shè)計(jì) API,部署一個(gè)虛擬化的實(shí)施,并且隨服務(wù)消費(fèi)者的需求變化 一起迭代,而不是一開始花費(fèi)大量的精力去構(gòu)建,最后又發(fā)現(xiàn)需要不斷修改。 在 TicketMonster 重構(gòu)時(shí),需要在單體應(yīng)用中保留一個(gè)上文所說的 API,以便在最初的服務(wù)拆分時(shí)盡可能輕松并且降低風(fēng)險(xiǎn)。無論是哪種情況,有兩個(gè)給力的工具可以幫到我們:一個(gè)是網(wǎng)頁式的 API 設(shè)計(jì)器,apicur.io[38],一個(gè)是測試 / API 虛擬化工具,Hoverfly[39]。Hoverlfy 是模擬 API 或捕獲現(xiàn)有 API 流量的好工具,可以用來模擬 mock 端點(diǎn)。 如果我們正在構(gòu)建一個(gè)新的 API,或在使用領(lǐng)域驅(qū)動設(shè)計(jì)方法后,想看看 API 什么樣,可以使用 apicur.io 工具建立一個(gè) Swagger/Open API 的規(guī)范。 在 TicketMonster 這個(gè)例子中,我們通過在代理模式下啟動 hoverfly,并使用 hoverfly 捕獲從應(yīng)用程序到后端服務(wù)的流量。我們可以在瀏覽器設(shè)置中設(shè)置 HTTP 代理,從而通過 hoverfly 發(fā)送所有流量。這將把每個(gè)請求 / 響應(yīng)對(request/response pair)的仿真存儲在 JSON 文件中。這樣我們就可以在 Mock 里使用這些請求 / 響應(yīng)對,或者更進(jìn)一步,用它們開始編寫測試,以規(guī)范具體的實(shí)現(xiàn)代碼中的一些行為。 對于所關(guān)注的請求或響應(yīng)對(response pairs),我們可以生成一個(gè) JSON 架構(gòu)并用于測試中,參見 https:///#/editor。 例如,結(jié)合使用 Rest Assured 和 Hoverfly,可以調(diào)用 hoverfly 模擬,并確定該響應(yīng)符合我們預(yù)期的 JSON 架構(gòu): @Testpublic void testRestEventsSimulation(){ get('/rest/events').then().assertThat().body(matchesJsonSchemaInClasspath('json-schema/rest-events.json'));} 在新的訂單服務(wù)中,可以查看 HoverflyTest.java [40] 測試。有關(guān)測試 Java 微服務(wù)的更多信息,請查閱 Manning 這本給力的書,《測試 Java 微服務(wù)》[41],我的一些同事 Alex Soto Bueno[42]、Jason Porter[43] 和 Andy Gumbrecht[44] 也參與了這本書的撰寫。 由于這篇博文已經(jīng)很長了,我決定將最后的部分單獨(dú)寫成本主題的第三部分,其中將涉及在單體應(yīng)用和微服務(wù)之間管理數(shù)據(jù)、服務(wù)消費(fèi)的契約測試(consumer contract testing), 功能發(fā)布控制( feature flagging),甚至更復(fù)雜的 istio 路由等內(nèi)容。本系列的第四部分將展示一個(gè)包含上述內(nèi)容的實(shí)操 Demo, 使用負(fù)載仿真測試(load simulation tests)和故障注入(fault injections)。歡迎訪問我的網(wǎng)站 [45] 和關(guān)注我的 Twitter [46]。 期望得到更多優(yōu)質(zhì)技術(shù)干貨,歡迎掃描群助手小波波二維碼,與近萬名技術(shù)人一起在 eaworld 社群參與定期微課、視頻分享、探討關(guān)于微服務(wù)、DevOps 實(shí)踐等技術(shù)內(nèi)容。入群暗號:1225 |
|