1. 程式人生 > >微服務架構上雲最佳實踐(轉自阿里中介軟體)

微服務架構上雲最佳實踐(轉自阿里中介軟體)

摘要:7月27日,雲棲社群、阿里中介軟體舉辦了首屆阿里巴巴中介軟體技術峰會,揭祕阿里10年分散式技術乾貨。在首屆阿里巴巴中介軟體技術峰會上,具有10年研發經驗的阿里巴巴中介軟體技術專家李顏良結合EDAS團隊上雲兩年多以來積累的經驗為大家分享瞭如何進行微服務拆分、微服務架構上雲最佳實踐以及微服務架構常用的模式,精彩不容錯過。

以下內容根據演講嘉賓現場視訊以及PPT整理而成。

本次分享主要圍繞以下三個方面:

  • 微服務拆分
  • 服務化案例分享與最佳實踐
  • 一些微服務架構的常用模式

一、微服務拆分

首先從真實的案例開始講起,當一些客戶接觸到了一些微服務之後,他們就會將原本經典的分層架構模式拆分成為微服務。如下圖所示,客戶原本的架構是比較經典的分層架構模型,可以看到每一層的結構都非常清晰,當此時想要做微服務拆分的時候第一步就是將DAO層直接拆分出來。而大家都知道,這樣做肯定會有一些不妥之處,那麼不妥之處到底在哪裡呢?在這裡就埋下一個疑問,大家可以先思考一下對於這樣的經典的分層架構究竟應該如何去進行拆分。

微服務的拆分其實要先從DDD開始說起,那麼什麼是DDD呢?DDD其實是一種軟體設計的指導思想,它可以指導我們設計一些高質量的軟體架構,DDD的核心是想讓技術人員和業務人員使用一種共同的語言並且都把關注點聚焦在業務層面,在這樣情況下去討論、發現和實現業務的價值。DDD中有一些核心的概念,首先其中有“領域”的概念,所謂“領域”這個概念的意思就是團隊所關注的業務。在領域中我們會提煉出一些模型,而這些模型有的是需要口口相傳的;也有的是需要我們寫在開發文件上面,便於新進入團隊的同學學習開發的;還有的是需要直接寫在給客戶看的文件上面供客戶去理解的;等等這些我們稱之為為領域模型。在領域模型確定之後,就需要有一個稱之為“通用語言”的東西,這個通用語言就是用於大家進行交流的。大家可能會覺得這個是非常自然的事情,但是事實上並不是這樣的,這裡舉一個例子給大家解釋一下,比如兩個程式設計師之間進行溝通時,描述轉賬這個場景可能是這樣的:定義一個變數transformMoney接收這個錢這個值,從第一個賬戶上面把這個錢減掉然後加到另一個賬戶上面去,然後使用DAO進行Save操作就可以了,這是程式設計師之間很自然的溝通語言。但是如果這個時候有業務人員進來了之後,他可能並不理解這樣的描述,業務人員的描述可能是這樣的:轉賬的這筆錢可能會需要分為系統內部轉賬和系統外部轉賬,根據轉賬金額的大小可能會採用不同的審計策略,根據這些策略每一筆資金可能使不同的系統感知到不同的事件,而且這些路徑可能需要做沉澱和記錄,可能會有交易的記錄產生等等,這樣描述出來可能技術人員也能夠聽懂,業務人員也能可以聽懂。

這是什麼意思呢?其實大家可以仔細想想,上面的這段對話其實是可以直接翻譯成為程式碼的,這就是所謂的通用語言。但是通用語言的產生過程也是不斷地提升的過程,就像程式碼需要不斷地進行重構和完善。而上面這段對話其實需要在一定的邊界之內才會有意義,而邊界又是什麼意思呢?還是使用剛才轉賬的例子,轉賬時使用的賬戶和登入時使用的賬戶雖然都叫做賬戶,但是肯定不是同一個東西,這時候就產生了限界,也就是BoundedContext 這個概念。

在介紹完這些基本概念之後,我們繼續分享DDD的戰略和戰術。從一個比較標準的解釋來說,戰略就是用於指導軍隊去打贏某場戰爭的思想,也就是一種方法和謀略。而DDD的戰略當然不是用於打仗的,而是用於拆分領域等其他方面的。當然DDD的戰略和戰爭的戰略有一定相同之處,比方在打仗的時候需要選好一塊什麼樣的地盤,以及在上面用什麼樣子的方式如何發動這場戰爭,而這塊地盤在微服務拆分上就是可以理解成選擇一個什麼樣子的領域,這個領域可以分為核心域和子域。對於核心域而言,從我們的業務就能顯而易見地看出來它是區別於其他業務的一塊領域,也就是核心業務。如果這塊領域不復存在的話,那麼整個團隊或者公司就會出現問題。在核心域周圍則會存在支撐子域,比如對於電商業務而言,交易就是核心,核心域的旁邊則會有支撐核心域來區分業務的子域,比如圖中的物流和支付,這兩部分的支撐子域也是整個業務獨有的。當然也會有其他的部分,比如通用子域,這部分就是其他公司可能會有或者其他業務也會存在的領域,這部分的業務邏輯可能可以直接從某些開源軟體或者商業軟體拿過來使用,比如像每個公司都會使用的會員系統以及積分系統等。除此之外還有一些基礎的技術服務,比如像簡訊服務這樣的技術服務。

當上面這樣整個圖構建完成之後,我們會發現每一塊領域之間,每一塊上下文之間都是存在一定的關係的。這些關係可能是比較簡單的上下游的關係,除此之外,還可能存在客戶關係或者供應商關係,或者是共享同一塊核心、模型這樣的關係,比方說前臺系統和後臺系統往往會共享同一種資料模型,還可能存在兩個領域之間需要做很多的介面卡等等,這些在DDD中也會有一些推薦架構模式去遵循。

除了上述在架構模式上面的東西以外,在一個領域內部還會有一些戰術上的模式讓我們定義一些東西。還是使用剛剛那麼轉賬的例子來分析,每一個賬戶以及每一筆交易的記錄都是需要進行唯一性區分的,這些需要進行唯一性區分儲存的東西可以我們叫做實體,還有說轉賬的時候的那筆錢其實也會是一個物件,因為它可能存在匯率不同、幣種等資訊,它也是以一個物件存在的,但是可能這個物件是不變的,因為我的三塊錢和你的三塊錢是一樣的,我們將這樣的物件稱為值物件。在做轉賬的時候,這個轉賬的過程可能會與某些交易機構或者其他的實體之間發生關係,所以需要內聚更加合理的地方,這個時候就出現了領域服務。然後回到剛才提到的需求之一就是每一筆錢都需要能夠被感知到,要能夠感知到就需要能夠產生或處理一些事件,這樣就會有一些領域事件出來。還有就是人與賬戶是什麼關係呢?想要拿到賬戶去做轉賬之前首先需要先找到人,此時這個人就會與賬戶出現一種聚合的關係,既然有了這樣聚合的關係;然後發現人對賬戶開通的過程,並不是簡單的new可以實現的,因為可能會需要繫結很多東西,開通很多東西,所以這時候很可能需要工廠來幫你的忙。同樣的道理,當你去做資源池的互動的時候,可能DAO幫不了你,因為DAO僅僅是對於DB的各種動作的轉義,它其實是沒有領域含義的,所以這時候就會需要資源庫。

聊完了DDD這部分之後,我們再來看一下它和我們今天提到的微服務的拆分到底存在什麼樣的關係。對於服務拆分而言,首先可以看到剛才分享的根據公司內部的核心域或者業務內部的核心域或者業務能力,拆分其實很簡單,首先就是“一縱”,縱向拆分就像阿里巴巴這樣將業務拆分成為淘寶、天貓、聚划算以及鹹魚等。然後再是“一橫”,橫指的就是拆分成剛才介紹的那些領域,包括核心域、支撐子域以及通用子域等。然後再找一些東西,也就是找其他的一些基礎的通用子域、一些基礎的能力域等。

當我們已經將服務拆分好了,那麼是不是這時候就可以真正地開始實現功能了呢?其實在真正開始做之前還需要好好思考一些問題,因為微服務沒有銀彈。首先第一個需要考慮的問題就是我們的服務的內聚和耦合是不是真的合理,因為內聚和耦合難以量化,所以這裡所強調的是合理。還有就是團隊的構成,因為微服務產生的目的就是為了減少團隊成員之間的溝通,那麼團隊構成是什麼意思呢?首先第一個要點就是團隊本身是需要全棧的,即自主性一定要很高。那麼團隊規模究竟應該多大呢?有一個參考的方法,就是“Two PizzaTeam”,也就是正好吃完兩個披薩的團隊。第二點的一個關鍵問題就是問自己準備好組織架構的變化了嗎?這樣的說法來自於康威定律,這個定律說的核心思想可以理解為:一個公司的技術架構可能最終會發展成為公司的組織架構的樣子,當然組織架構也有可能發展成為技術架構的樣子。第三點就是微服務在未來肯定會面臨更多的挑戰,到底是哪些挑戰呢?在後續會為大家揭曉。

二、服務化案例分享與最佳實踐

接下來為大家分享一些實際的案例,這些案例是提取自EDAS上雲兩年多以來在客戶的環境中存在的實際問題。在正式介紹案例之前需要首先介紹幾個簡單的名詞:

  • HSF:阿里巴巴集團所使用的 RPC 框架,全稱為 High Speed Framework,江湖人稱:“好舒服”。
  • EDAS:企業級分散式應用服務,阿里巴巴中介軟體提供的雲上商業微服務解決方案。
  • VPC:Virtual Private Cloud,虛擬私有云服務。阿里雲上的一個基礎網路產品,為隔離使用者的網路環境而生。

服務化:開發篇

下圖是HSF服務的簡單介紹。首先第一個要分享的案例就是開發的時候遇到的問題,在分享這個案例之前首先簡單地介紹一下HSF。HSF有一個註冊中心,這個註冊中心用來管理髮布和訂閱服務。當一個生產者啟動起來之後可能會對於自己的服務進行釋出,註冊中心監聽了這個服務就會發送給正在監聽的消費者,然後消費者拿到地址之後就可以直接進行RPC的呼叫了。

那麼如何使用HSF服務呢,在開始一個RPC的使用之前肯定會需要定義一個服務,而定義這個服務是通過一個介面的方式進行的。消費端可以直接使用這個介面在Spring容器的XML裡面宣告這是一個消費端,而生產者除了需要實現這個介面之外,也是需要向容器做宣告,表示自己是一個生產者。

當將服務部署完成之後,就可以實現RPC真正的呼叫了;下圖是一個客戶真實的案例,案例中的請求會牽涉到一個非常重的Task,客戶的想法是希望服務端做完這個 Task 之後非同步地通知給程序。如果在單程序層面下這樣寫程式碼一點問題都沒有,甚至可以說是很優雅,但是在分散式環境下這樣寫卻是存在問題的,因為callback是回不來的,所以說我們寫程式碼的思維方式需要發生一定的轉變。

那麼除了這樣思維方式的變化還需要有什麼東西呢?在下圖中就為大家列舉了開發過程中的最佳實踐,這些最佳實踐有些是參考的集團規約、有一部分是自己從客戶的案例中整理的,沒有高深的技術,我理解起來完全是平時的一些小的點,大的點我不講、道行不深也講不出什麼感覺、而且大的點肯定是有一幫人和幫你一起考慮的,反倒是一些小的點是我們容易忽略的,因為只有你一個人在思考,很容易因為偷懶或者著急上個廁所就忘了。小反而是一門更大的學問。這些小點就小到一行日誌、一個引數的校驗、一個返回值、一個命名等等。具體不多說,可以自己參考。

服務化:部署篇

分享完了開發,接下來為大家分享關於部署的問題。曾經有客戶提出了一個工單,就是服務都能夠看到,但是還是會出現時好時壞的情況。我們仔細地審視了客戶部署的架構,發現客戶的架構是這樣的:他們使用了兩個VPC,這兩個VPC之間有生產者和消費者,還有一個在另外一個VPC中,這時候就會發現我們所看到的部署架構肯定是呼叫不通的,因為他們的網路本身就是不通的。

這裡為大家簡單地介紹一下VPC。VPC其實是為了隔離使用者網路環境而生的網路產品,可以簡單地理解成為服務放入到VPC中去會更加安全。大家可能會想如果只是劃分網路的話,通過路由器或者交換機這些也能夠做到,那麼為什麼要使用VPC呢?其實VPC除了劃分網路的基礎功能之外,其實還可以實現跨可用區,可以實現同城容災,另外VPC中還有一些比較基礎的網路產品,比方說可以使用SNAT/DNAT做源地址和目的地址的轉換,甚至可以做VPC內的統一的安全組管理等等。在什麼樣的場景下面會使用到VPC呢?關於這一點在阿里雲官網上面有詳細的介紹,在這裡就不再贅述,大概就是做混合雲架構或者有NAT需求的時候就可以使用。

接下來介紹HSF路由,剛才提到當消費者啟動的時候就會去做監聽,生產者啟動的時候就會去做釋出,當釋出的時候註冊中心知道這個地址之後就會推送給消費者,這就是一次簡單的服務發現過程。那麼第一次選擇連線的時候,假設地址列表中有兩個,一個好的一個壞的,這時候應該怎麼樣呢?首先第一次Round Robin選擇連一個,與此同時啟動心跳程序,進行心跳檢測,如果此時發現地址有問題就會把這個地址放在一個不可用連線地址的列表中,但是心跳還會繼續,當第二次再進行連線的時候過程與上述大致相同,但是不會再去啟動一個心跳程序了。

在EDAS中的運維層面可能有一些點需要列出來給使用者看的,因為經常會有使用者遇到這些問題。在這裡想要強調的點是 iptables ,很多使用者非常喜歡設定iptables,但是卻不建議大家去設定 iptables,首先它的學習成本比較高,其次它真的非常不好進行維護和管理以及知識的傳遞,而且幾乎不可能去實現批量的運維和管理。除此之外,在 iptables 中用到的那些功能基本上可以在雲上面使用VPC + NAT的方式進行處理,除非需要有一些非常高階的特性,比如需要根據資料包中的字串進行過濾等這樣的需求。

服務化:連調篇

分享完部署我們再聊一聊連調,在連調過程中我們遇到了一些非常怪的事情,就是客戶的反饋就是在雲上面部署的一個服務,有的人可以連線,但是有的人就是偶爾不可以連線,非常不穩定!我們看到這樣的現象之後就去客戶的伺服器上提取了一些關鍵的資訊,其中的就包括這些:客戶開啟了recycle以及timestamps,然後主要說有NAT閘道器,而且辦公網下的開發機器上存在時間戳不一致的情況,說到了這裡很多有經驗的同學就已經猜出來問題到底是怎麼一回事了,原因就如下圖中左邊所列舉的這樣。

說到這裡可能有同學已經猜出來了是怎麼回事了,這裡我簡單簡述一下造成這個問題的原因,首先要從伺服器端主動關閉連線的 TCP TIMEWAIT 狀態開始說起,顧名思義,這個狀態是在等,等什麼呢?確保 Server 的最後一個 ACK 可以到達客戶端,因為客戶端在等著這個 ACK 來關閉她的 TCP 連線,等多久呢? 2MSL,這是一個很長的時間了,根據不同的系統不一樣,有的是60s,有的是一百多秒。那麼開啟 recyle 會有什麼好處?加速回收,怎麼加速的?他的原理是基於 RTO 的,所謂 RTO 就是資料包的超時重傳的時間,這個時間怎麼來的呢?基於一些值算出來的,但是會很短,最短可以到200 ms,簡單的說起來就是從幾十秒一下降到零點幾秒級別的時延,這是一個很大的改進,我覺得也很科學。但是這樣沒有規避根本問題,畢竟人家一開始等那麼長時間是有道理的,萬一真的有資料包隔了那麼長時間才到咋辦呢?所以這裡還有一種機制,叫做 PAWS,這兩哥們就會把同一個 host 過來的資料包基於時間戳單調遞增的這麼一個假設,會拒絕掉一些 server 認為不合理的包,且這個時間錯是一個 Per Host 的紀錄,因為上一個連結都不在了,肯定要 Per Host 的都行了對吧?

回到我們當前的這個案例,當兩臺機器通過 NAT 閘道器進行訪問的時候,伺服器端看到的是一個 Host,當有時間不一致的時候,根據我們剛剛說的這些論斷,有些資料包就自然被 drop 掉了,這個問題我們也引申出來了一些建議的做法。首先無論什麼樣的環境,伺服器上的時間戳一定要 check 是不是一致,尤其是有 https 和 openapi 呼叫的場景。還有生產環境我們不怎麼推薦使用 recycle 的,建議設定合理的 timewait buckets 代替。同時儘量避免在生產環境使用 NAT,最好走 SLB 的轉發。

服務化:壓測

接下來分享一下關於壓測的內容,這裡講的壓測可能會與大家通常理解的場景不太一樣,因為是客戶在做壓測的時候拿著我們的RPC框架和另外一個目前比較流行的Restful的框架進行對比,給回來的反饋是效能不行。在接到這樣的反饋之後,我們大概看了一下客戶的程式碼是什麼樣子,客戶的程式碼大概是將一個物件使用JSON序列化之後做RPC,RPC回來之後也是一個String,之後再將這個String反序列化成為一個物件返回給客戶端,也就是客戶在程式碼中做了兩次JSON的序列化。在HSF中大家都知道它是位元組支援序列化的,這裡的兩次序列化是浪費的;這種寫法很普遍,希望大家注意。

還有一種典型的問題就是將一個Request直接轉發出去,這種方式我們知道也是不行的。在HSF中支援的方式是Java的Native以及Hessian等,目前在序列化方面其實是不需要關注太多的效能的,整個框架對於效能而言已經優化的足夠好了,除此之外在做序列化的時候這裡也列出了一些大家比較容易遺漏的點,這些點就包括了為什麼 HTTP 物件不行,因為它其實一個不可序列化的物件。總結而言,就是在每一次序列化之前要好好思考一下這個物件是不是可以序列化的;其次在序列化之前還需要好好想想序列化之後的位元組大小為多少,是不是一個很大的物件。還有就是需要對於一些特殊的場景多留一些心眼,比如說列舉、單例和一些範型等。

服務化:上線執行

下圖的這個案例就是HSF的執行緒池滿了,這個問題我相信很多同學都已經遇到過,針對於這個問題這裡首先列出了一個大概的時序圖,也就是可能在自己的業務中需要依賴於外部服務,可能就會影響到內部的情況。

當客戶與我們溝通的時候我們首先解釋了為什麼連線池會滿,首先簡單介紹一下HSF三種呼叫模型,第一種就是很自然的同步呼叫模式,就是一個一個地來,呼叫完成之後再回去;第二種情況就是非同步呼叫,就是呼叫外部服務的請求不在乎呼叫的請求什麼時候返回,呼叫之後就繼續做自己的事情,對下圖中的例項而言,我在傳送驗證碼的之後就可以繼續執行後續的步驟,這樣的好處就是使用者的體驗將會得到提升,但是這種方式會帶來一些業務的損耗,如果簡訊真的傳送失敗了則可能無法感知,系統認為使用者拿到驗證碼了,但是事實上並沒有;所以比較推薦的是第三種方式——Future的方式,這種方式還是基於非同步的,Future是什麼意思呢?其實是可以程序可以先去幹自己的活,如果想要結果可以來拿,當自己的任務完成再回頭取得非同步呼叫結果的時候可能結果也就回來了,如果此時結果還沒有返回回來,那麼在這裡等待也不會影響太大的事情。

在介紹完這三種HSF的呼叫模型之後,還需要介紹一些執行緒池的模型,畢竟這個案例是與執行緒池相關的。其實HSF中有三種執行緒池,第一個就是IO執行緒,但是這個執行緒池中執行緒比較少,因為其所做的工作也很少,基本上就做兩件事情:序列化和處理協議;第二個執行緒池就是和我們這個案例息息相關的,在Server端會有一個很大的執行緒池,這個執行緒池預設的最大值可以達到600,當談到HSF的執行緒池滿了也就是說這個地方滿了。還有一個地方是大家比較容易忽略的,就是在Tomcat的入口其實也有一個執行緒池,這個執行緒池其實決定了服務的併發數,但是這個併發數並不是單單由這個執行緒池決定的,理論上還需要加上另外一個欄位Accept Count。

當我們向客戶介紹完了以上兩種的內部實現的時候,就開始進行優化了,優化的過程也是根據剛才的思路進行的,第一種方式主要是將呼叫的方法進行了修改,第二種方式則是將一些執行緒池的引數調高了,把Tomcat執行緒池入口的併發數以及HSF的執行緒池的值調高了,然而這樣的調整隻是緩解了一些問題,但是並沒有解決最根本的問題。然後使用者下意識地採取了在雲上進行擴容的方式,然後“奇蹟”出現了,整個服務全掛掉了。

為什麼服務會掛掉呢?後來經過了解發現客戶的架構是這樣的:使用Redis特別容易忽略掉一些東西,這些東西從運維的層面上講會有一些規格,這些規格主要分為兩種,包括連線數和頻寬。很多運維人員誤以為在雲上面的環境走內網沒有頻寬限制,其實完全錯了,所以對於雲上規格模型一定要特別清楚。還有就是在開發層面的問題,在開發時,大家都喜歡使用common-pool,並且預設喜歡設定成為一個很大的值,不管使不使用這些個連線都可能會預設起來這麼多連線,比如所有的機器都用50,機器規模擴容到百級別的時候就可能會撐死。還有就是在開發時很喜歡使用一些List,把這些List序列化成JSON然後儲存為String,再從List返回僅有的幾個元素,這些也是會引起問題的原因,以上這些就是我們用 Redis 的時候會遇到的一些問題。

三、一些微服務架構的常用模式

服務化:模式

在這裡大家可能就會希望有一種東西可以動態地調整Redis這部分的連線數,其實這就是微服務中的一種模式叫做Externalized configuration——外部配置動態推送,也就是通過外部的某些配置批量化地更新某些東西。

剛才提到的是因為某些東西不行了導致網站會怎麼樣,這時候可以選擇考慮當發生故障的時候可以選擇將這個服務進行限流降級或者甚至將整個鏈路都進行降級,這就是微服務推薦的第二種模式,Circuit Breaker——限流降級。

之前介紹的兩個應用比較簡單,只有兩個服務之間的模型。當面對如下圖所示的這樣的情況下就需要使用第三個模式Distributed tracing——分散式追蹤。

當然其實微服務的模式不止上面提到的這三種,下圖就展現了整個微服務這部分能夠想到的一些模式。首先我們需要這樣的一個基礎元件,這個基礎元件包含了一些東西,包括了一些架構的思想,以及服務之間的架構如何進行編排互動,還有采用什麼樣的部署模式,包括應該採用單個容器級別的部署還是單個機器級別的部署,甚至是單個VM級別的部署。還需要制定某一種服務發現策略等;當在真正決定好了架構之後,可能需要真正的微服務的底座來支撐起整個微服務的架構,底座規劃好之後就需要選擇服務之間的通訊模型。那麼通訊模型選擇好之後,就需要開始考慮服務高可用方面的事情了。當這方面規劃之後就需要考慮到一些日誌、審計以及分散式追蹤等等這些資料化運營之類的東西。還有一點比較重要的就是當要對於服務進行拆分就需要考慮資料庫到底是該分還是該合。如果需要分的話,那麼問題來了,資料的一致性今後應該如何保證,當然這裡也有一些模式可尋的;還有分開之前資料可能是從一個地方取,現在需要從各個地方取了、測試也是一樣,以前可能是測一塊,現在可能會需要測試一打,這些都是在微服務拆分完之後需要面對的挑戰,這也是本次最想傳遞給大家的東西。

當然挑戰之下必有陪伴,EDAS就是陪伴大家的解決方案。為了鼓勵更多的中小企業能夠使用EDAS的服務,現在阿里雲上推出了EDAS的“1元計劃”。基礎的應用釋出、運維、搭建分散式系統,低至1元/月。