1. 程式人生 > >微服務那麼熱,創業公司怎麼選用實踐?

微服務那麼熱,創業公司怎麼選用實踐?

近年來,後臺服務領域最火的方向非微服務莫屬。本文從微服務歷史、現狀回顧開始,用實際案例落地實踐中的問題。雖是創業公司的經驗,卻可廣而用之。

老司機簡介

陳輝,曾任職Google、阿里巴巴、Facebook,現任杭州映兔科技CTO,創業中。

前言網際網路公司的核心技術資產之一是雲端執行的後臺服務。

近些年在後臺服務領域有多個熱門趨勢,如容器化、微服務、DevOps等,如果要找出一個最火的方向,非微服務莫屬。事實上,其他的熱門趨勢很多是伴隨微服務技術興起的。

在這篇文章裡,我們首先闡述了微服務的歷史和現狀,並用實際案例回答兩個大家非常關注的問題,“為什麼要採用微服務架構”和“如何構建微服務應用”。同時,我們也分享了創業公司在容器化、服務治理、DevOps、團隊組織等方面的第一手案例,希望對同在創業的小夥伴有幫助。

微服務簡史

微服務(microservices)的概念已經存在了好多年,其前身是“面向服務的架構”(SOA,serviceorientedarchitecture)。但SOA並沒有大範圍流行並取代單體應用(monolithicapplication,可以簡單理解成只有一個binary的應用),主要有兩個原因:

1、採用SOA的好處主要來自專案模組化而非模組服務化。

SOA化的第一步是將單體應用拆分為多個模組,當你完成了這一步,並且有好的開發、測試和整合工具保證大團隊在一個巨型程式碼庫裡協同工作的時候,你已經得到了“SOA化”帶來的絕大多數好處。然後大家會對進一步模組服務化產生合理質疑,因為這需要大量重構工作,而帶來的生產效率的提升並不那麼明顯,投入產出比並不高。

實際上相當數量的公司止步於此,比如Google的Adsense系統樞紐(mixer)就是一個巨大的單體應用,數百個專案從一箇中央repository裡編譯為一個上GB的二進位制程式,然後通過金絲雀部署(canary)做線上版本迭代。

2、SOA本身並沒有解決一個核心問題:多服務運維。

開源社群比較流行的方案比如thrift、protobufRPC、json-rpc等,只實現了多語言服務介面框架,最多加上服務治理的功能(比如dubbo),並沒有提供完整的運維解決方案。缺乏運維工具的支援導致單體應用切換為SOA的代價遠高於分散式服務帶來的好處。之前需要運維一個單體應用,而現在需要運維100個小的服務,運維工作量隨著服務數量的增加而線性增加。

相比SOA,微服務提供了一套更為完整的解決方案,很重要的原因是近幾年開源界出現的一些工具極大降低了微服務的運維門檻,從而使得多服務的優勢更為凸顯。

對微服務最好的詮釋是MartinFowler在2014年寫的一篇文章,詳細描述了微服務區別於單體服務的九個特徵。具體內容大家可以看他的原文【備註1】,下面我嘗試用最直白的語言來概況:

微服務的九大特徵

1、服務即元件。一個服務實現一個元件,這有兩個好處:服務可以獨立部署,youonlydeploywhatyoudevelop;更清晰的模組邊界,每個服務提供方可以專注釋出API,隱藏實現細節和版本。

2、按照業務域來組織微服務。通過微服務邊界劃分業務邊界,一個跨職能team(包含完整的UI、中介軟體、DBA工程師)完全掌控業務內的微服務。

3、按產品而非專案劃分微服務。和第二條類似,一個團隊負責完整的端到端開發和維護,youbuild,yourunit。

4、關注業務邏輯,而非服務間通訊。換句話說,所有微服務呼叫使用統一協議,這個協議將輸入輸出從底層實現細節中抽象出來,微服務團隊只需要關注如何將輸入轉化為輸出的邏輯,而不需要考慮網路層實現細節。

5、分散式管理。只關注介面實現的功能,對如何實現不做強制規範。換句話說,每個微服務團隊有充分自由選擇自己團隊熟悉的程式語言、資料庫和其他中介軟體等技術棧。

6、分散式資料。每個微服務有自己的資料庫,並且這些資料庫不可被其他微服務直接訪問,所有資料的讀寫操作都要通過微服務介面完成。

7、基礎設施自動化,包括服務自動化構建、部署。強大的自動化測試和部署工具是微服務的必要條件,我們在下文中會進一步闡述。

8、容錯。微服務設計允許服務調用出錯,呼叫方在呼叫失敗時不應該產生災難性結果。其實容錯設計和微服務無關,任何好的模組化設計必須是允許呼叫失敗的。只不過微服務將容錯設計作為一個強條件,換句話說,如果你的服務決不允許呼叫失敗的,那最好使用單體架構。

9、進化。使用合適的工具,你可以更快地更頻繁更快地對系統做修改,因為你可以專注在一個服務上,而無需對整個單體應用做改動。

這九個特性都很抽象,我們在實踐中發現,如果用一條特徵來描述微服務帶來的最大好處的話,那就是對團隊協作方式的影響:

微服務極大降低了團隊成員工作的耦合度,解放了每個人的工作效率。畢竟任何工程問題最後都是工程師問題,這可能不是微服務設計的本意,但對人的工作方式的改變卻成了最大的意外驚喜。

何時不需要微服務

在開始對微服務的討論之前,我們需要潑一盆冷水,幫你看到這項技術的適用邊界在哪裡。

如果你的專案滿足下面的條件之一,那麼不需要微服務。

1、你的程式碼沒有模組化

不是所有的程式碼都模組化了,有可能你的專案還沒有複雜到需要抽離成幾個模組。但多數專案沒有模組化都是因為沒有很好地設計,建議你從最開始設計時就考慮到模組,或者把你的歷史程式碼重構為多個模組。這是好的設計的一部分,和用不用微服務無關。

即便你的程式碼模組化了,也未必需要微服務,比如

2、你的服務要求極高的效能

要求極低的延遲或者無法容忍服務間調用出錯,比如廣告、高頻交易中負責關鍵路徑的系統,最好把所有的模組都打包編譯成為單體應用。不過,多數網際網路的業務系統不屬於此類。

3、你沒有一個好的容器編排系統

容器化幾乎是微服務的先決條件,如果你沒有一個好的容器編排系統幫你解決服務發現、負載均衡、彈性擴容、自動化部署等問題,運維上的負擔就會大大超過微服務帶來的好處。

MartinFowler有一張著名的圖說明這個問題【來源2】

容器編排系統

圖中橫軸是程式碼複雜度,縱軸是生產效率,藍線是微服務,綠線是單體服務。當代碼複雜度不高時(最左邊),運維多服務的代價會超過微服務帶來的效率提升,這時單體服務效率佔優。而當代碼變複雜時情況出現反轉,微服務帶來的解耦導致效率高於單體應用。

這個圖做於2015年5月,這一年多來開源社群的容器編排解決方案已經有了長足進步,比如Kubernetes、Mesos、Swarm等越來越成熟,同時誕生了很多企業級的CaaS(containerasaservice)服務比如AmazonECS、GoogleContainerEngine、國內的DaoCloud、靈雀雲等,實際上圖中左邊兩條線的差距已經大大縮小。

微服務技術選型

微服務架構成功運用的關鍵是選擇合適的自動化工具降低運維難度,這對人力資源有限的創業公司尤為重要。我們的《談談創業公司的技術選型》【來源3】一文詳細說明了創業公司技術選型的幾個原則,對微服務技術棧選型同樣有效:

一是利用好新技術選型的後發優勢。微服務技術是一項比較新的技術,在大公司很少有可以參考的經驗,應該多關注開源技術,多方比較後大膽採用。

二是自力更生、造輪子。微服務是條創新的不歸路,雖然有相當多可以使用的開源元件,但你仍然需要開發配套工具讓這些元件協同工作。

下面是我們選擇的一部分和微服務相關的開源工具,以及這些工具的使用經驗。

容器編排系統:Kubernetes

微服務技術的核心是容器編排系統,現在最流行的三個容器編排系統是Kubernetes,Mesos,Swarm。

通過比較我們選擇了Kubernetes(簡稱k8s),因為Kubernetes的設計最吸引我們,有Google支援,社群活躍度和發展前景俱佳。我們整個後臺系統基於Kubernetes,並且已經完全微服務化,有近100個微服務數百個容器在執行。

我們在實戰中使用Kubernetes的幾點經驗如下。

1、二進位制版本和配置版本要做分離,且程式碼化

微服務的配置yaml檔案check-in到git程式碼庫,而且做binary/config分離,分別控制二進位制和配置環境的版本,所有的線上部署的改動都在程式碼中反映出來。舉個k8s中微服務配置的例子,如下圖。

程式碼優化

這是我們一個微服務的 deployment 檔案,我們用 git 的版本號做 docker 映象的 tag(Jenkins 自動打包後加上去的),docker 映象裡只包含 binary 檔案,配置檔案通過 configmap 的 volume mount 為容器內的一個目錄,而且配置檔案也做了版本號控制,資料庫密碼等不走程式碼,而是由叢集管理員手工輸入為 kubernetes 的 secret,不留任何記錄,從而避免了敏感資訊的洩露。

2、混合雲管理

考慮混合雲上多 k8s 叢集的管理需求,我們用 zone 來標識不同資料中心的 kubernetes 叢集,zone 由三個字母標識,如下圖

混合雲管理

通過三字母標識法,我們將混合雲部署統一化,極大方便了程式碼和文件中的服務標識。

3、Namespace 使用

Kubernetes 的 namespace 極有用,我們用 production namespace 指代生產環境,staging 指代預發,kube-system 指代集群系統級別的服務比如 DNS、prometheus 監控和報警等。

另外 k8s 也支援通過 namespace 的 node selector 來指定某個服務需要執行在哪類機器節點上,這樣就可以將預發和生產環境執行在不同的機器上,做到不同環境的資源隔離。

4、基於 DNS 的自動化服務註冊、發現和負載均衡

通過 skyDNS 就可以將一個服務的分佈在不同伺服器上的 instance 命名歸一化,比如通過呼叫 ama-server.production.svc.k8s:20001 就可以將呼叫請求自動路由到某個服務節點上,呼叫端不需要關心服務是怎麼部署的,服務註冊和服務發現自動完成。

5、Overlay network 我們使用 flannel。

程式語言:Go

使用哪種語言和微服務看上去是無關的,而且貌似微服務的設計原本就是鼓勵用不同語言實現微服務。

但是,程式語言的確會影響到程式碼的微服務化難度,有兩個原因:

1、你需要一個執行時環境較小的程式語言,最好是能靜態編譯不需要虛擬機器的語言。執行環境龐大不適合容器化,如果你給 Java 程式打過 docker 包就知道了,動輒上百兆的執行時,啟動就消耗數百兆記憶體。而 Go 可編譯為 standalone binary 無需執行時環境,docker 映象一般 10 幾兆就搞定了,memory footprint 也小很多。

2、你需要一個適合寫網路服務的語言。這種語言最好原生支援多執行緒程式設計,可以非常簡潔高效地寫 HTTP 或者 RPC 服務而不需要藉助第三方框架。Go 就是為寫後臺服務而生的語言。

另外,Go 自帶格式化工具能夠統一團隊的程式設計風格,而且學習上手快,Java 或者 C++ 程式設計師只要一個星期就可以達到熟練運用的水平。

實際上我們用 Go 從頭實現了整套後臺微服務,包括 RTMP 直播伺服器、使用者體系、交易、IM、搜尋、監控、小二後臺,我們甚至用 Go 寫機器學習程式碼和機械臂控制程式。實踐證明 Go 完全可以勝任所有的後臺開發工作,而且有極高的效率和工程實現質量。

線上監控:Prometheus + Grafana

微服務的監控系統必不可少。有些人用 ELK,我們用 Prometheus+Grafana。比如我們在 gRPC 服務的 /metrics 下添加了類似下面的指標來監控 RPC 效能:

監控

然後 Prometheus 會根據 Kubernetes 的 pod 註冊資訊自動找到這些 metrics,我們設定這樣的語句

metrics Prometheus

最後在 Grafana 裡通過這樣的介面展示出來:

如果你在 Google 工作過會心中竊喜,這不就是 Google 的 Borgmon 嘛!對的,Prometheus 就是由一位 xoogler 工程師寫的,參考了 Google 內部資料監控系統的設計。最後在 Grafana 裡通過這樣的介面展示出來:

這套體系和 ELK 相比較輕,且非常容易擴充套件,我們寫了幾個模組把服務日誌和前端訪問記錄融合在一起做分析,同時 Prometheus 指標描述能力非常強大幾乎可以做任何運算(事實上這種語言是圖靈完備的)。

離線資料分析:fluentd + ODPS

我們的資料分析有兩類,離線和線上。

線上資料分析就是上面寫的 Prometheus + Grafana,適合服務報警、除錯等日常任務。

離線資料主要是 fluentd 從 log 中提取出來後直接傳送到阿里雲的 ODPS,然後寫定時排程生成表格分析。另外,資料庫資料也通過 datax 發到 ODPS,可以和 log joining 處理。

我們這套離線資料體系適合每日報表或者即席查詢。

同步通訊:gRPC + HTTP RESTful API

叢集內部服務間通訊我們用 Google 開源的 gRPC,最近出了 1.0 穩定版。

外部呼叫使用 JSON 格式的 HTTP API,通過負載均衡先經過多個 Nginx 節點(也部署在 Kubernetes),然後通過 Nginx 的重定向傳送給後面各個業務的 “gateway” 微服務,這些微服務再把一部分邏輯通過 gRPC 傳送給更後端的微服務。

非同步通訊:RabbitMQ

除了同步的 RPC/HTTP 呼叫外,你還需要非同步呼叫,通常使用訊息佇列來完成。有兩個應用場景

1、廣播類的請求。比如使用者資料發生更新後,通過 RabbitMQ 通知搜尋引擎的所有例項完成增量索引。

2、所有跨叢集呼叫必須走訊息佇列。這是一個很好的設計習慣,跨叢集呼叫一定要從設計上就是高度容錯的,而且必須對延遲要求很低。如果不滿足這兩個條件,你需要將被呼叫的服務在多個叢集都部署一份。

我們對 RabbitMQ 呼叫的另一個約定是,佇列訊息最小化原則:如果需要傳遞較多資訊,請使用引用到資料庫。比如在通知搜尋引擎完成增量索引的時候,我們只往訊息佇列傳遞新文件的 docid 和資料來源地址,然後由訊息的接收方自行從對應資料來源(MySQL 或者 Redis)中抓取對應資料完成更新。

持續整合、部署:Jenkins

我們的程式碼按照微服務劃分,每個微服務是一個單獨的 repo 存放在 github,共用元件放在 common repo,然後微服務用 import 呼叫(這可以說是 Go 的另一個適合微服務的好處,程式碼引用機制比較簡單)。

所有的 github commit 都會通過 webhook 觸發 Jenkins 裡的構建行為,

Docker 私有倉庫:Harbor

這是 vmware 的一個開源專案,實現了使用者和專案的許可權管理。

非容器化元件

有些元件不適合放在 k8s 叢集中,比如負載均衡、資料庫(MySQL,Redis)、NFS等帶狀態的服務,我們直接使用阿里雲服務。

我們的微服務架構

微服務架構

先上一張圖直觀地展示給大家我們的微服務叢集長什麼樣子。

我們一共有 3 個 Kubernetes 叢集,這張圖展示的是其中一個叢集(zone hba)中所有的微服務和他們之間的呼叫關係(Call Graph)。其中,

  • 藍色節點是 k8s svc,如果你不熟悉 k8s,可以簡單理解為服務閘道器,每個服務有且只有一個閘道器,這個閘道器本身也是去中心化的,分佈在多臺伺服器上,可以自動將呼叫請求重定向到多個服務例項中的一個。服務閘道器地址通過域名自動解析。
  • 綠色節點是 k8s deployment,可以理解為服務本身,每個節點實際上有多個例項(pod)分佈在多臺伺服器上。
  • 節點之間的單向箭頭描述了服務間的呼叫關係,其中藍線是預發環境的服務呼叫,橙線是生產環境的服務呼叫,灰線是生產和預發呼叫的公共服務,虛線是非同步呼叫 rabbitmq。

從圖中我們可以非常直觀地總結出微服務架構的幾個特徵:

呼叫關係即架構

回憶一下你閱讀過的所有講架構的文章,裡面畫的架構圖比較側重業務關係,而且很非常抽象。在微服務體系下,架構圖就等於呼叫關係圖:微服務劃分就代表了業務的劃分,微服務間的呼叫關係就是業務依賴關係。

程式碼自動生成呼叫關係

如果你的 Call Graph 不能通過解析你的程式碼自動生成的話,你一定沒有用好微服務架構。

我們上面的呼叫圖是這樣生成的:首先,所有的呼叫關係都儲存在配置檔案中,見我上文中對 k8s yaml 檔案的描述;然後我們通過一個指令碼解析這些配置檔案,生成描述關聯關係的 DOT 檔案,這步並不難,因為配置檔案是結構化的 yaml 檔案;最後我們用 Graphviz 從 DOT 檔案自動畫出呼叫圖。同時,yaml 配置都是 check-in 到 git repository 裡的,帶版本號,所以我們能夠分析出架構演化路徑。

工程師獨立推動架構演化

架構圖等於呼叫圖,呼叫圖由程式碼生成,程式碼由開發工程師控制的,一個微服務有且只有一個工程師獨立負責。結果就是所有工程師都能直接修改架構,生產力得到了極大的解放!

事實上,每個採用微服務技術的團隊初期都會經歷一個“寒武紀大爆炸”階段,在這段時間會很快有各種微服務被開發出來,架構圖在幾個星期的時間就會演化到相當的複雜度。比如圖中 80 多個微服務主要由 3 個全職工程師開發,這在傳統的大公司不敢想象。

開發即運維

You own your microservices. 每一個微服務都由一位工程師獨立開發、測試、部署、運維。聽起來非常恐怖,但在微服務時代這是最高效的,原因有兩個

1、運維自動化:容器編排系統 kubernetes 基本上已經將部署和運維完全自動化了,作為開發不需要知道負載均衡、動態擴容、服務自癒合等所有這些運維細節,工具已經幫你搞定。

2、端到端開發:開發可以直接面對需求,端到端測試這些需求,如果出了問題,沒有誰能比寫程式碼的人更快地找到問題所在。

DevOps 的工作方式在微服務時代很好地實現了。

架構視覺化

呼叫關係圖非常好地可視化了架構的幾個特徵:

1、有向無環圖。你的微服務呼叫必須是無閉環的,circular dependency 會帶來意想不到的麻煩。

2、有層次。比如我們的架構圖中最左邊的幾個 Nginx 節點負責接收來自瀏覽器或者移動端的 HTTP API 呼叫,然後 nginx 再呼叫內部的服務(中間),這些服務再呼叫更深層次的服務(右邊)。服務的層次越深,提供的功能越基礎。

3、衡量架構複雜度的幾個指標:depth(深度,從 nginx 開始到最深路徑上的微服務個數),fan-in(入度,一個服務被幾個服務呼叫),fan-out(出度,一個微服務呼叫了幾個微服務)。你應該盡力降低架構深度和出度,提升服務的入度。

我們上面的架構圖不是那種只在做報告時用用然後就被遺忘的架構圖。實際上,這張圖幫我們定位到了架構中的一些不合理處並及時作出修正,比如有些服務忘記了部署在多個環境,或者已經廢棄的服務並沒有從架構中刪除等。

下面我們結合兩個具體例子講講我們是如何構建微服務的。

微服務案例一:搜尋

Go 語言開發

呼叫流程是這樣的:

1、客戶端通過 HTTP API 請求閘道器 Nginx 服務。

2、然後 Nginx 會根據檢索場景的不同(比如有些請求是查詢視訊,有些查詢使用者)分別分流到對應的檢索微服務上。

3、然後檢索微服務將使用者請求翻譯成悟空引擎的查詢語句,呼叫引擎微服務完成檢索。

4、檢索微服務再呼叫其他的服務將客戶端需要的附加資訊新增在返回結果中。

這個設計最重要的部分是檢索內容的更新機制,包括全量更新和實時增量更新:

1、無須持久化:不使用持久化儲存,所有索引和排序欄位存在記憶體。每次啟動時全量更新索引表到記憶體。

2、增量更新:其他業務系統通過 RabbitMQ 的廣播通知搜尋系統增量更新,包括新增、刪除、修改文件索引,RabbitMQ 不傳輸資料,只通知。然後搜尋引擎從 Redis 和 MySQL 讀入實際的增量資料,比如從 MySQL 讀入文件基本資訊,從 Redis 讀入需要高頻修改的資訊(比如計數,其他實時計算的分數等)。這些增量資訊都是各個業務系統生成並預先新增到資料庫的。

3、避免全量增量衝突:全量更新的同時,監聽 RabbitMQ 佇列,保證全量新增的同時不會遺漏增量更新:實現增刪改介面時,需要加鎖避免全量和增量同時寫衝突;如果增量更新的視訊還沒有先被全量載入,快取這個增量更新,等全量更新完畢後再增量更新。

另外,排序規則需要頻繁變動,我們通過悟空引擎的排序規則外掛實現了多種排序規則共存,並藉助 k8s 藍綠部署的機制實現了服務不下線更新程式碼。

通過基於 k8s 的微服務架構,加上合理的增量全量更新策略,我們實現了一個非常靈活且可靠的搜尋功能。

微服務案例二:深度學習

在這個案例裡,我們實現了深度學習的分散式 serving。

機器學習業務的開發和多數後臺服務不同,主要包括三個環節

1、實驗性研究:機器學習工程師使用部分資料在單機上實驗各種演算法,找到最好的演算法

2、大規模訓練:使用第一階段找到的演算法,加上全量資料,訓練得到一個最好的模型

3、分散式部署:將第二個階段得到的模型部署到線上環境,實現分類、識別、預測等服務

其中分散式部署機器學習模型有兩個挑戰

1、和資料查詢式業務相比,機器學習特別是深度學習模型需要較長的計算時間,因此延遲較高,thoughtput 較低,業務上線後需要更好的動態擴容能力。

2、機器學習程式依賴的執行環境比較複雜,部署比較麻煩。

這裡以我們的“圖說”服務作為例子說明。

我們把這個服務實現在微信公眾號上,向這個公眾號傳送一張圖片,公眾號會返回英語和漢語來描述圖片的內容。比如下面的例子:

圖片

上面這張圖上,準確識別出了一輛火車、這倆火車的顏色並描述了火車在鐵軌上行駛這一行為。

請求流程是這樣的:

1、使用者向公眾號發圖片

2、微信伺服器向我們的 SNT 微服務(多個例項負載均衡)傳送 XML 請求,帶有使用者圖片的地址

3、SNT 伺服器將圖片地址傳送到 IM2TXT 微服務(k8s 部署多個例項),這個微服務自行從地址下載圖片,呼叫深度學習模型完成分析,並將得到的英文描述返回給 SNT 服務

4、SNT 將英文描述傳送給百度翻譯 API 得到中文描述

5、SNT 將中英文描述一起返回給微信伺服器

6、使用者收到公眾號回覆

其中 IM2TXT 微服務使用 Tensorflow 的 Python API 開發,打包為 2GB 的 docker 映象,然後推送到我們的私有 docker 倉庫供 Kubernetes 使用。模型本身非常複雜,數百萬個引數,而且要對 Top N 的可能性做 beam search,跑一張圖片需要兩秒左右。

通過這套系統我們實現了較低的延遲,把模型預測時間控制在了 2 秒以內,整體延遲控制在 5 秒以下(微信要求後臺服務響應時間不超過 5 秒)。

最後的話

Kubernetes 容器編排系統和相關工具已經將微服務的使用門檻降到最低,運維自動化真正實現了 Dev 和 Ops 的統一,極大解放了工程師的生產效率。

我們在創業環境下實現了一整套基於微服務的後臺系統,並運用了一系列新技術。這篇文章通過分享我們的實踐經驗,證明基於微服務的後臺技術體系不僅是可行的,而且是未來大勢所趨。

原文出處:InfoQ