微服務是否適合小團隊是個見仁見智的問題。但小團隊並不表明出品的必定是小產品,當業務變得愈來愈複雜,如何使用微服務分而治之就成爲一個不得不面對的問題。前端
由於微服務是對整個團隊的考驗,從開發到交付,每一步都充滿了挑戰。通過 1 年多的探索和實踐,本着將 DevOps 落實到產品中的願景,一步步建設出適合咱們的微服務平臺。git
要不要微服務算法
咱們的產品是 Linkflow,企業運營人員使用的客戶數據平臺(CDP)。產品的一個重要部分相似企業版的「捷徑",讓運營人員能夠像搭樂高積木同樣建立企業的自動化流程,無需編程便可讓數據流動起來。docker
從這一點上,咱們的業務特色就是聚少成多,把一個個服務鏈接起來就成了數據的海洋。數據庫
理念上跟微服務一致,一個個獨立的小服務最終實現大功能。固然咱們一開始也沒有使用微服務,當業務還未成型就開始考慮架構,那麼就是「過分設計"。編程
另外一方面須要考慮的因素就是「人",有沒有經歷過微服務項目的人,團隊是否有 DevOps 文化等等,綜合考量是否須要微服務化。架構
微服務的好處是什麼?負載均衡
相比於單體應用,每一個服務的複雜度會降低,特別是數據層面(數據表關係)更清晰,不會一個應用上百張表,新員工上手快。運維
對於穩定的核心業務能夠單獨成爲一個服務,下降該服務的發佈頻率,也減小測試人員壓力。分佈式
能夠將不一樣密集型的服務搭配着放到物理機上,或者單獨對某個服務進行擴容,實現硬件資源的充分利用。
部署靈活,在私有化項目中,若是客戶有不須要的業務,那麼對應的微服務就不須要部署,節省硬件成本,就像上文提到的樂高積木理念。
微服務有什麼挑戰?
一旦設計不合理,交叉調用,相互依賴頻繁,就會出現牽一髮動全身的局面。想象單個應用內 Service 層依賴複雜的場面就明白了。
項目多了,輪子需求也會變多,須要有人專一公共代碼的開發。
開發過程的質量須要經過持續集成(CI)嚴格把控,提升自動化測試的比例,由於每每一個接口改動會涉及多個項目,光靠人工測試很難覆蓋全部狀況。
發佈過程會變得複雜,由於微服務要發揮所有能力須要容器化的加持,容器編排就是最大的挑戰。
線上運維,當系統出現問題須要快速定位到某個機器節點或具體服務,監控和鏈路日誌分析都必不可少。
下面詳細說說咱們是怎麼應對這些挑戰的。
開發過程的挑戰
持續集成
經過 CI 將開發過程規範化,串聯自動化測試和人工 Review。
咱們使用 Gerrit 做爲代碼&分支管理工具,在流程管理上遵循 GitLab 的工做流模型:
開發人員提交代碼至 Gerrit 的 Magic 分支。
代碼 Review 人員 Review 代碼並給出評分。
對應 Repo 的 Jenkins job 監聽分支上的變更,觸發 Build job。通過 IT 和 Sonar 的靜態代碼檢查給出評分。
Review 和 Verify 皆經過以後,相應 Repo 的負責人將代碼 Merge 到真實分支上。
如有一項不經過,代碼修改後重複過程。
Gerrit 將代碼實時同步備份至兩個遠程倉庫中。
集成測試
通常來講代碼自動執行的都是單元測試(Unit Test),即不依賴任何資源(數據庫,消息隊列)和其餘服務,只測試本系統的代碼邏輯。
但這種測試須要 Mock 的部分很是多,一是寫起來複雜,二是代碼重構起來跟着改的測試用例也很是多,顯得不夠敏捷。並且一旦要求開發團隊要達到某個覆蓋率,就會出現不少造假的狀況。
因此咱們選擇主要針對 API 進行測試,即針對 Controller 層的測試。另外對於一些公共組件如分佈式鎖,Json 序列化模塊也會有對應的測試代碼覆蓋。
測試代碼在運行時會採用一個隨機端口拉起項目,並經過 HTTP Client 對本地 API 發起請求,測試只會對外部服務作 Mock,數據庫的讀寫,消息隊列的消費等都是真實操做,至關於把 Jmeter 的事情在 Java 層面完成一部分。
Spring Boot 項目能夠很容易的啓動這樣一個測試環境,代碼以下:
測試過程的 HTTP Client 推薦使用 io.rest-assured:rest-assured 支持 JsonPath,十分好用。
測試時須要注意的一個點是測試數據的構造和清理。構造又分爲 Schema 的建立和測試數據的建立:
Schema 由 Flyway 處理,在啓用測試環境前先刪除全部表,再進行表的建立。
測試數據能夠經過 @Sql 讀取一個 SQL 文件進行建立,在一個用例結束後再清除這些數據。
順帶說一下,基於 Flyway 的 Schema Upgrade 功能咱們封成了獨立的項目,每一個微服務都有本身的 Upgrade 項目。
好處:一是支持 command-line 模式,能夠細粒度的控制升級版本;二是也能夠支持分庫分表之後的 Schema 操做。Upgrade項目也會被製做成 Docker image 提交到 Docker hub。
測試在每次提交代碼後都會執行,Jenkins 監聽 Gerrit 的提交,經過 docker run -rm {upgrade 項目的 image}先執行一次 Schema Upgrade,而後 Gradle test 執行測試。
最終會生成測試報告和覆蓋率報告,覆蓋率報告採用 JaCoCo 的 Gradle 插件生成,以下圖:
這裏多提一點,除了集成測試,服務之間的接口要保證兼容,實際上還須要一種 consumer-driven testing tool。
就是說接口消費端先寫接口測試用例,而後發佈到一個公共區域,接口提供方發佈接口時也會執行這個公共區域的用例,一旦測試失敗,表示接口出現了不兼容的狀況。
比較推薦你們使用 Pact 或是 Spring Cloud Contact。咱們目前的契約基於「人的信任」,畢竟服務端開發者還很少,因此沒有必要使用這樣一套工具。
集成測試的同時還會進行靜態代碼檢查,咱們用的是 Sonar,當全部檢查經過後 Jenkins 會 +1 分,再由 Reviewer 進行代碼 Review。
自動化測試
單獨拿自動化測試出來講,就是由於它是質量保證的很是重要的一環,上文能在 CI 中執行的測試都是針對單個微服務的。
那麼當全部服務(包括前端頁面)都在一塊兒工做的時候是否會出現問題,就須要一個更接近線上的環境來進行測試了。
在自動化測試環節,咱們結合 Docker 提升必定的工做效率並提升測試運行時環境的一致性以及可移植性。
在準備好基礎的 Pyhton 鏡像以及 Webdriver(Selenium)以後,咱們的自動化測試工做主要由如下主要步驟組成:
測試人員在本地調試測試代碼並提交至 Gerrit。
Jenkins 進行測試運行時環境的鏡像製做,主要將引用的各類組件和庫打包進一個 Python 的基礎鏡像。
經過 Jenkins 定時或手動觸發,調用環境部署的 Job 將專用的自動化測試環境更新,而後拉取自動化測試代碼啓動一次性的自動化測試運行時環境的 Docker 容器,將代碼和測試報告的路徑鏡像至容器內。
自動化測試過程將在容器內進行。
測試完成以後,沒必要手動清理產生的各類多餘內容,直接在 Jenkins 上查看發佈出來的測試結果與趨勢。
關於部分性能測試的執行,咱們一樣也將其集成到 Jenkins 中,在能夠直觀的經過一些結果數值來觀察版本性能變化狀況的迴歸測試和基礎場景,將會很大程度的提升效率,便捷的觀察趨勢:
測試人員在本地調試測試代碼並提交至 Gerrit。
經過 Jenkins 定時或手動觸發,調用環境部署的 Job 將專用的性能測試環境更新以及可能的 Mock Server 更新。
拉取最新的性能測試代碼,經過 Jenkins 的性能測試插件來調用測試腳本。
測試完成以後,直接在 Jenkins 上查看經過插件發佈出來的測試結果與趨勢。
發佈過程的挑戰
上面提到微服務必定須要結合容器化才能發揮所有優點,容器化就意味着線上有一套容器編排平臺。咱們目前採用是 Redhat 的 OpenShift。
因此發佈過程較原來只是啓動 Jar 包相比要複雜的多,須要結合容器編排平臺的特色找到合適的方法。
鏡像準備
公司開發基於 GitLab 的工做流程,Git 分支爲 Master,Pre-production和 Prodution 三個分支,同時生產版本發佈都打上對應的 Tag。
每一個項目代碼裏面都包含 Dockerfile 與 Jenkinsfile,經過 Jenkins 的多分支 Pipeline 來打包 Docker 鏡像並推送到 Harbor 私庫上。
Docker 鏡像的命令方式爲:項目名/分支名:git_commit_id,如 funnel/production:4ee0b052fd8bd3c4f253b5c2777657424fccfbc9。
Tag 版本的 Docker 鏡像命名爲:項目名 /release:tag 名,如 funnel/release:18.10.R1。
在 Jenkins 中執行 build docker image job 時會在每次 Pull 代碼以後調用 Harbor 的 API 來判斷此版本的 Docker image 是否已經存在,若是存在就不執行後續編譯打包的 Stage。
在 Jenkins 的發佈任務中會調用打包 Job,避免了重複打包鏡像,這樣就大大的加快了發佈速度。
數據庫 Schema 升級
數據庫的升級用的是 Flyway,打包成 Docker 鏡像後,在 OpenShift 中建立 Job 去執行數據庫升級。
Job 能夠用最簡單的命令行的方式去建立:
腳本升級任務也集成在 Jenkins 中。
容器發佈
OpenShift 有個特別概念叫 DeploymentConfig,原生 Kubernetes Deployment 與之類似,但 OpenShift 的 DeploymentConfig 功能更多。
DeploymentConfig 關聯了一個叫作 ImageStreamTag 的東西,而這個 ImagesStreamTag 和實際的鏡像地址作關聯,當 ImageStreamTag 關聯的鏡像地址發生了變動,就會觸發相應的 DeploymentConfig 從新部署。
咱們發佈是使用了 Jenkins+OpenShift 插件,只須要將項目對應的 ImageStreamTag 指向到新生成的鏡像上,就觸發了部署。
若是是服務升級,已經有容器在運行怎麼實現平滑替換而不影響業務呢?
配置 Pod 的健康檢查,Health Check 只配置了 ReadinessProbe,沒有用 LivenessProbe。
由於 LivenessProbe 在健康檢查失敗以後,會將故障的 Pod 直接幹掉,故障現場沒有保留,不利於問題的排查定位。而 ReadinessProbe 只會將故障的 Pod 從 Service 中踢除,不接受流量。
使用了 ReadinessProbe 後,能夠實現滾動升級不中斷業務,只有當 Pod 健康檢查成功以後,關聯的 Service 纔會轉發流量請求給新升級的 Pod,並銷燬舊的 Pod。
線上運維的挑戰
服務間調用
Spring Cloud 使用 Eruka 接受服務註冊請求,並在內存中維護服務列表。
當一個服務做爲客戶端發起跨服務調用時,會先獲取服務提供者列表,再經過某種負載均衡算法取得具體的服務提供者地址(IP + Port),即所謂的客戶端服務發現。在本地開發環境中咱們使用這種方式。
因爲 OpenShift 自然就提供服務端服務發現,即 Service 模塊,客戶端無需關注服務發現具體細節,只需知道服務的域名就能夠發起調用。
因爲咱們有 Node.js 應用,在實現 Eureka 的註冊和去註冊的過程當中都遇到過一些問題,不能達到生產級別。
因此決定直接使用 Service 方式替換掉 Eureka,也爲之後採用 Service Mesh 作好鋪墊。
具體的作法是,配置環境變量:
EUREKA_CLIENT_ENABLED=false,RIBBON_EUREKA_ENABLED=false
並將服務列表如:
FOO_RIBBON_LISTOFSERVERS: '[http://foo:8080](http://foo:8080/)'
寫進 ConfigMap 中,以 envFrom: configMapRef 方式獲取環境變量列表。
若是一個服務須要暴露到外部怎麼辦,好比暴露前端的 HTML 文件或者服務端的 Gateway。
OpenShift 內置的 HAProxy Router,至關於 Kubernetes 的 Ingress,直接在 OpenShift 的 Web 界面裏面就能夠很方便的配置。
咱們將前端的資源也做爲一個 Pod 並有對應的 Service,當請求進入 HAProxy 符合規則就會轉發到 UI 所在的 Service。
Router 支持 A/B test 等功能,惟一的遺憾是還不支持 URL Rewrite。
對於須要 URL Rewrite 的場景怎麼辦?那麼就直接將 Nginx 也做爲一個服務,再作一層轉發。流程變成 Router → Nginx Pod → 具體提供服務的 Pod。
鏈路跟蹤
開源的全鏈路跟蹤不少,好比 Spring Cloud Sleuth + Zipkin,國內有美團的 CAT 等等。
其目的就是當一個請求通過多個服務時,能夠經過一個固定值獲取整條請求鏈路的行爲日誌,基於此能夠再進行耗時分析等,衍生出一些性能診斷的功能。
不過對於咱們而言,首要目的就是 Trouble Shooting,出了問題須要快速定位異常出如今什麼服務,整個請求的鏈路是怎樣的。
爲了讓解決方案輕量,咱們在日誌中打印 RequestId 以及 TraceId 來標記鏈路。
RequestId 在 Gateway 生成表示惟一一次請求,TraceId 至關於二級路徑,一開始與 RequestId 同樣,但進入線程池或者消息隊列後,TraceId 會增長標記來標識惟一條路徑。
舉個例子,當一次請求向 MQ 發送一個消息,那麼這個消息可能會被多個消費者消費,此時每一個消費線程都會本身生成一個 TraceId 來標記消費鏈路。加入 TraceId 的目的就是爲了不只用 RequestId 過濾出太多日誌。
實現上,經過 ThreadLocal 存放 APIRequestContext 串聯單服務內的全部調用。
當跨服務調用時,將 APIRequestContext 信息轉化爲 HTTP Header,被調用方獲取到 HTTP Header 後再次構建 APIRequestContext 放入 ThreadLocal,重複循環保證 RequestId 和 TraceId 不丟失便可。
若是進入 MQ,那麼 APIRequestContext 信息轉化爲 Message Header 便可(基於 RabbitMQ 實現)。
當日志彙總到日誌系統後,若是出現問題,只須要捕獲發生異常的 RequestId 或是 TraceId 便可進行問題定位。
通過一年來的使用,基本能夠知足絕大多數 Trouble Shooting 的場景,通常半小時內便可定位到具體業務。
容器監控
容器化前監控用的是 Telegraf 探針,容器化後用的是 Prometheus,直接安裝了 OpenShift 自帶的 cluster-monitoring-operator。
自帶的監控項目已經比較全面,包括 Node,Pod 資源的監控,在新增 Node 後也會自動添加進來。
Java 項目也添加了 Prometheus 的監控端點,只是惋惜 cluster-monitoring-operator 提供的配置是隻讀的,後期將研究怎麼將 Java 的 JVM 監控這些整合進來。
總結
開源軟件是對中小團隊的一種福音,不管是 Spring Cloud 仍是 Kubernetes 都大大下降了團隊在基礎設施建設上的時間成本。
固然其中有更多的話題,好比服務升降級,限流熔斷,分佈式任務調度,灰度發佈,功能開關等等都須要更多時間來探討。
對於小團隊,要根據自身狀況選擇微服務的技術方案,不可一味追新,適合本身的纔是最好的。