歡迎光臨
每天分享高質量文章

小團隊微服務落地實踐

微服務是否適合小團隊是個見仁見智的問題。但小團隊並不代表出品的一定是小產品,當業務變得越來越複雜,如何使用微服務分而治之就成為一個不得不面對的問題。因為微服務是對整個團隊的考驗,從開發到交付,每一步都充滿了挑戰。經過1年多的探索和實踐,本著將DevOps落實到產品中的願景,一步步建設出適合我們的微服務平臺。
要不要微服務

我們的產品是Linkflow,企業運營人員使用的客戶資料平臺(CDP)。產品的一個重要部分類似企業版的“捷徑”,讓運營人員可以像搭樂高積木一樣創建企業的自動化流程,無需編程即可讓資料流動起來。從這一點上,我們的業務特點就是聚少成多,把一個個服務連接起來就成了資料的海洋。理念上跟微服務一致,一個個獨立的小服務最終實現大功能。當然我們一開始也沒有使用微服務,當業務還未成型就開始考慮架構,那麼就是“過度設計”。另一方面需要考慮的因素就是“人”,有沒有經歷過微服務專案的人,團隊是否有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專案可以很容易的啟動這樣一個測試環境,代碼如下:
  1. @RunWith(SpringRunner.class)

  2. @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

測試過程的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可以用最簡單的命令列的方式去創建:

  1. oc run upgrade-foo --image=upgrade/production --replicas=1 --restart=OnFailure --command -- java -jar -Dprofile=production /app/upgrade-foo.jar

腳本升級任務也集成在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。
  1. readinessProbe:

  2. failureThreshold: 4

  3. httpGet:

  4. path: /actuator/metrics

  5. port: 8090

  6. scheme: HTTP

  7. initialDelaySeconds: 60

  8. periodSeconds: 15

  9. successThreshold: 2

  10. timeoutSeconds: 2

線上運維的挑戰

服務間呼叫
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都大大降低了團隊在基礎設施建設上的時間成本。當然其中有更多的話題,比如服務升降級,限流熔斷,分佈式任務調度,灰度發佈,功能開關等等都需要更多時間來探討。對於小團隊,要根據自身情況選擇微服務的技術方案,不可一味追新,適合自己的才是最好的。
Q&A;

Q:服務治理問題,服務多了,呼叫方請求服務方,超時或者網絡抖動等需要可能需要重試,客戶端等不及了怎麼辦?比如A->B->C,等待超時時間都是6s,因為C服務不穩定,B做了重試,那麼增加了A訪問B的時長,導致連鎖反應?
A:服務發現有兩種,一種是客戶端發現,一種是服務端發現。如果是客戶端發現的話,客戶端需要設置超時時間,如果超時客戶端需要自己重試,此時如果是輪詢應該可以呼叫到正常的服務提供方。Spring Coud的Ribbon就是對這一流程做了封裝。至於連鎖反應的問題,需要有降級熔斷,配置Hystrix相關引數並實現fallback方法。看原始碼能夠發現hystrixTimeout要大於ribbonTimeout,即Hystrix熔斷了以後就不會重試了,防止雪崩。
Q:JVM如何export,是多container嗎,監控資料,搜刮到Prometheus?
A:JVM的用的是Prometheus埋點,Java裡面的路徑是/actuator/prometheus,在yaml裡面定義prometheus.io/path: /actuator/prometheu prometheus.io/port: ‘8090’ prometheus.io/scrape: ‘true’,再在Prometheus裡面進行相應的配置,就可以去搜刮到這些暴露的指標。
Q:Kubernetes和OpenShift哪個更適合微服務的使用?
A:OpenShift是Kubernetes的下游產品,是Kubernetes企業級的封裝,都是一樣的。OpenShift封裝有功能更強大的監控管理工具,並且擁有Kubernetes不太好做的權限管理系統。
Q:可以介紹一下你們在優化鏡像體積上面做了哪些工作嗎?
A:RUN命令寫在一行上,產生的臨時檔案再刪掉。只安裝必須要的包。JDK和Node.Js都有slim鏡像,一般都是以那個為基礎鏡像去做。
Q:資料庫是否真的適合最容器化?
A:我們生產資料庫用的是RDS,開發測試環境用的是Docker Compose起的。從理論上,資料庫最好做容器化,模塊的獨立性高,需要考慮好的是資料庫容器的資料永久化儲存。
Q:為什麼選擇了OpenShift?
A:因為OpenShift有個很方便的UI,大多數都可以在UI裡面操作,包括yaml檔案的修改,重新部署回退等操作。對於開發測試來講,學習的成本比較低,不需要花時間熟悉CLI操作。
Q:Python基礎鏡像怎麼製作最好,如果加入GCC,C++等編譯需要的工具,鏡像會特別大?
A:Python基礎鏡像直接從Python官方Docker鏡像上做就行了。GCC,C++那個做出來的鏡像大也沒辦法。如果沒這個需求的話,可以用Python slim鏡像去做。
Q:在Gateway中Ribbon如何根據客戶端的IP負載到對應的IP註冊的服務?
A:如果使用了Eureka的話,服務提供方啟動時會自註冊到Eureka。服務呼叫方發起請求前會從Eureka上讀取提供方的串列,再進行負載均衡定位到具體的IP和Port。如果跟我們一樣直接使用Kubernetes的Service,其實就是由Kubernetes控制了,服務呼叫方訪問Kubernetes暴露的Service,然後由Kubernetes負載均衡到這個Service背後的具體的Pod。
Q:如何實現遠程發佈、打包?
A:Jenkins打包鏡像發佈到Harbor上,Jenkins再呼叫OpenShift去從Harbor上拉取鏡像,重新tag一下就可以實現發佈。
Q:譬如客戶端IP是10,希望Gateway負載到10註冊的order服務,而不是其他IP註冊的order服務,希望開發使用集中的Eureka和Gateway?
A:是說不需要負載均衡?最簡單的可以看下Ribbon的實現,負載均衡演算法可以自己定義,如果只是要固定IP的話,那麼遍歷服務串列再判斷就可以了。兩次判斷,if serviceId=order,if ip = 10。
Q:Docker管理工具一般用什麼?
A:Kubernetes,簡稱k8s,是目前較熱門的Docker管理工具。離線安裝Kubernetes比較繁瑣,有兩款比較好的自動化部署工具,Ubuntu系統的Juju和Red Hat系統的OpenShift,OpenShift又稱為企業版的Kubernetes,有收費的企業版和免費版。
Q:Prometheus是每個集群部署一套嗎?儲存是怎麼處理?存本地還是?
A:每個集群部署一套,儲存暫時存在本地,沒有用持久化儲存。因為現在環境都是在雲上面,本身雲廠商就有各種的監控資料,所以Prometheus的監控資料也只是做個輔助作用。

Kubernetes入門與進階實戰培訓

Kubernetes入門與進階實戰培訓將於2019年4月19日在北京開課,3天時間帶你系統學習Kubernetes,學習效果不好可以繼續學習。本次培訓包括:Docker基礎、容器技術、Docker鏡像、資料共享與持久化、Docker三駕馬車、Docker實踐、Kubernetes基礎、Pod基礎與進階、常用物件操作、服務發現、Helm、Kubernetes核心組件原理分析、Kubernetes服務質量保證、調度詳解與應用場景、網絡、基於Kubernetes的CI/CD、基於Kubernetes的配置管理等,點擊下麵圖片查看具體詳情。