雲端設計平臺Coohom在生產環境中使用istio的經驗與實踐

ServiceMesher發表於2018-10-29

介紹

自從istio-1.0.0在今年釋出了正式版以後,Coohom專案在生產環境中也開啟了使用istio來作為服務網格。

本文將會介紹與分享在Coohom專案在使用istio中的一些實踐與經驗。

雲端設計平臺Coohom在生產環境中使用istio的經驗與實踐

Coohom專案

杭州群核資訊科技有限公司成立於2011年,公司總部位於浙江杭州,佔地面積超過5000平方米。酷家樂是公司以分散式平行計算和多媒體資料探勘為技術核心,推出的家居雲設計平臺,致力於雲渲染、雲設計、BIM、VR、AR、AI等技術的研發,實現“所見即所得”體驗,5分鐘生成裝修方案,10秒生成效果圖,一鍵生成VR方案,於2013年正式上線。作為“設計入口”,酷家樂致力於打造一個連線設計師、家居品牌商、裝修公司以及業主的強生態平臺。

依託於酷家樂快速的雲端渲染能力與先進的3D設計工具經驗,Coohom致力於打造一個讓使用者擁有自由編輯體驗、極致視覺化設計的雲端設計平臺。Coohom專案作為一個新興的產品,在架構技術上沒有歷史包袱,同時Coohom自從專案開啟時就一直部署執行在Kubernetes平臺。作為Coohom專案的技術積累,我們決定使用服務網格來作為Coohom專案的服務治理。

為什麼使用istio

由於istio是由Google所主導的產品,使用istio必須在Kubernetes平臺上。所以對於Coohom專案而言,在生產環境使用istio之前,Coohom已經在Kubernetes平臺上穩定執行了。我們先列一下istio提供的功能(服務發現與負載均衡這些Kubernetes就已經提供了):

  1. 流量管理: 控制服務之間的流量和API呼叫的流向、熔斷、灰度釋出、A/BTest都可以在這個功能下完成;

  2. 可觀察性: istio可以通過流量梳理出服務間依賴關係,並且進行無侵入的監控(Prometheus)和追蹤(Zipkin);

  3. 策略執行: 這是Ops關心的點, 諸如Quota、限流乃至計費這些策略都可以通過網格來做,與應用程式碼完全解耦;

  4. 服務身份和安全:為網格中的服務提供身份驗證, 這點在小規模下毫無作用, 但在一個巨大的叢集上是不可或缺的。

但是, 這些功能並不是決定使用istio的根本原因, 基於Dubbo或Spring-Cloud這兩個國內最火的微服務框架不斷進行定製開發,同樣能夠實現上面的功能,真正驅動我們嘗試istio的原因是:

  • 第一:它使用了一種全新的模式(SideCar)進行微服務的管控治理,完全解耦了服務框架與應用程式碼。業務開發人員不需要對服務框架進行額外的學習,只需要專注於自己的業務。而istio這一層則由可以由專門的人或團隊深入並管理,這將極大地降低"做好"微服務的成本。

  • 第二: istio來自GCP(Google Cloud Platform),是Kubernetes上的“官方”Service Mesh解決方案,在Kubernetes上一切功能都是開箱即用,不需要改造適配的,深入istio並跟進它的社群發展能夠大大降低我們重複造輪子的成本。

Coohom在istio的使用進度

目前Coohom在多個地區的生產環境叢集內都已經使用了istio作為服務網格,對於istio目前所提供的功能,Coohom專案的網路流量管理已經完全交給istio,並且已經通過istio進行灰度釋出。對於從K8S叢集內流出的流量,目前也已經通過istio進行管理。

從單一Kubenertes切換為Kubernetes+istio

在使用istio之前,Coohom專案就已經一直在Kubernetes平臺穩定執行了。關於Coohom的架構,從技術棧的角度可以簡單的分為:

  • Node.js egg應用

  • Java Springboot應用

從網路流量管理的角度去分類,可以分為三類:

  • 只接受叢集外部流量;

  • 只接受叢集內部流量;

  • 既接受叢集外部流量,也接受叢集內部流量

在我們的場景裡,基本上所有的Node應用屬於第一類,一部分Java應用屬於第二類,一部分Java應用屬於第三類。 為了更清楚的表達,我們這裡可以想象一個簡單的場景:

雲端設計平臺Coohom在生產環境中使用istio的經驗與實踐

從上面的場景我們可以看到,我們有一個頁面服務負責渲染併發頁面內容到使用者的瀏覽器,使用者會從瀏覽器訪問到頁面服務和賬戶服務。 賬戶服務負責記錄使用者名稱,使用者密碼等相關資訊。賬戶服務同時還會在許可權服務內檢視使用者是否具有相應的許可權,並且頁面服務同樣也會請求賬戶服務的某些介面。 所以按照我們上面的流量管理的分類法,頁面服務屬於第一類服務,許可權服務屬於第二類服務,賬戶服務則屬於第三類服務。 同時,賬戶服務和許可權服務也接了外部的RDS作為儲存,需要注意的是RDS並非在Kubernetes叢集內部。

那麼在過去只用Kubenretes時,為了讓使用者能正確訪問到對應的服務,我們需要編寫Kubernetes Ingress: 值得注意的是,由於只有賬戶服務和頁面服務需要暴露給外部訪問,所以Ingress中只編寫了這兩個服務的規則。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: example-ingress
spec:
  rules:
  - host: www.example.com
    http:
      paths:
      - backend:
          serviceName: page-service
          servicePort: 80
        path: /
  - host: www.example.com
    http:
      paths:
      - backend:
          serviceName: account-service
          servicePort: 80
        path: /api/account
複製程式碼

在接入istio體系後,雖然這三個服務在所有POD都帶有istio-proxy作為sidecar的情況下依舊可以沿用上面Kubernetes Ingress將流量匯入到對應的服務。 不過既然用了istio,我們希望充分利用istio的流量管理能力,所以我們先將流量匯入到服務這一職責交給istio VirtualService去完成。所以在我一開始接入istio時,我們將上述Kubernetes方案改造成了通過下述方案:

入口Ingress

首先,我們在istio-system這個namespace下建立Ingress,將所有www.example.com這個host下的流量匯入到istio-ingressgateway中。 這樣我們就從叢集的流量入口開始將流量管理交付給istio來進行管理。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: istio-ingress
  namespace: istio-system
spec:
  rules:
  - host: www.example.com
    http:
      paths:
      - backend:
          serviceName: istio-ingressgateway
          servicePort: 80
        path: /複製程式碼

在交付給istio進行管理以後,我們需要將具體的路由-服務匹配規則告訴給istio,這一點可以通過Gateway+VirtualService實現。 需要注意的是,下面的服務名都是用的簡寫,所以必須將這兩個檔案和對應的服務部署在同一個Kubernetes namespace下才行。

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: example-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: example-http
      protocol: HTTP
    hosts:
    - "www.example.com"

---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: example-virtualservice
spec:
  hosts:
  - "www.example.com"
  gateways:
  - example-gateway
  http:
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        port:
          number: 80
        host: page
  - match:
    - uri:
        prefix: /api/account
    route:
    - destination:
        port:
          number: 80
        host: account-service複製程式碼

外部服務註冊

在經過上述的操作以後,重新啟動服務例項並且自動注入istio-proxy後,我們會發現兩個後端的Java應用並不能正常啟動。經過查詢啟動日誌後發現,無法啟動的原因則是因為不能連線到外部RDS。這是因為我們的所有網路流量都經過istio的管控後,所有需要叢集外部服務都需要先向istio進行註冊以後才能被順利的轉發過去。一個非常常見的場景則是通過TCP連線的外部RDS。當然,外部的HTTP服務也是同理。

以下是一個向istio註冊外部RDS的例子。

apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: mysql-external
  namespace: istio-system
spec:
  hosts:
  - xxxxxxxxxxxx1.mysql.rds.xxxxxx.com
  - xxxxxxxxxxxx2.mysql.rds.xxxxxx.com

  addresses:
  - xxx.xxx.xxx.xxx/24
  ports:
  - name: tcp
    number: 3306
    protocol: tcp
  location: MESH_EXTERNAL複製程式碼

支援灰度釋出

上面istio-ingress+Gateway+VirtualService的方案可以替代我們之前只使用Kubernetes Ingress的方案,但如果只是停留在這一步的話那麼對於istio給我們帶來的好處可能就不能完全體現。值得一提的是,在上文的istio-ingress中我們將www.example.com的所有流量匯入到了istio-ingressGateway,通過這一步我們可以在istio-ingressGateway的log當中檢視到所有被轉發過來的流量的網路情況,這一點在我們日常的debug中非常有用。然而在上述所說的方案中istio的能力還並未被完全利用,接下來我將介紹我是如何基於上述方案進行改造以後來進行Coohom日常的灰度釋出。

還是以上文為例,假設需要同時釋出三個服務,並且三個服務都需要進行灰度釋出,並且我們對灰度釋出有著以下幾個需求:

  • 最初的灰度釋出希望只有內部開發者才能檢視,外部使用者無法進入灰度。

  • 當內部開發者驗證完灰度以後,逐漸開發切換新老服務流量的比例。

  • 當某個外部使用者進入新/老服務,我希望他背後的整個服務鏈路都是新/老服務

為了支援以上灰度釋出的需求,我們有如下工作需要完成:

  1. 定義規則告訴istio,對於一個Kubernetes service而言,後續的Deployment例項哪些是新服務,哪些是老服務。

  2. 重新設計VirtualService結構策略,使得整個路由管理滿足上述第二第三點需求。

  3. 需要設計一個合理的流程,使得當灰度釋出完成以後,最終狀態能恢復成與初始一致。

定義規則

為了使得istio可以知道對於某個服務而言新老例項的規則,我們需要用到DestinationRule,以賬戶服務為例:

從下文的例子我們可以看到,對於賬戶服務而言,所有Pod中帶有type為normal的標籤被分為了normal組,所有type為grey的標籤則被分為了grey組, 這是用來在後面幫助我們讓istio知道新老服務的規則,即帶有type:normal標籤的POD為老例項,帶有type:grey標籤的POD為新例項。這裡所有三個服務分類都可以套用該規則,就不再贅述。

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: account-service-dest
spec:
  host: account-service
  subsets:
  - name: normal
    labels:
      type: normal
  - name: grey
    labels:
      type: grey複製程式碼

重構VirtualService

前文我們提到,我們在Kubernetes平臺內將在網路流量所有服務分為三類。之所以這麼分,就是因為在這裡每一類服務的VirtualService的設計不同。 我們先從第一類,只有外部連線的服務說起,即頁面服務,下面是頁面服務VirtualService的例子:

從下文這個例子,我們可以看到對於頁面服務而言,他定義了兩種規則,對於headers帶有end-user:test的請求,istio則會將該請求匯入到上文我們所提到的 grey分組,即特定請求進入灰度,而所有其他請求則像之前匯入到normal分組,即老例項。

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: page-service-external-vsc
spec:
  hosts:
    - "www.example.com"
  gateways:
  - example-gateway
  http:
  - match:
    - headers:
        end-user:
          exact: test
      uri:
        prefix: /
    route:
    - destination:
        port:
          number: 80
        host: page-service
        subset: grey
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        port:
          number: 80
        host: page-service
        subset: normal複製程式碼

然後我們再看第二類服務,即許可權服務,下面是許可權服務的virtualService例子:

從下面這個例子我們可以看到,首先在取名方面,上面的page-service的virtualService name為xxx-external-vsc,而這裡許可權服務則名為xxx-internal-service。這裡的name對實際效果其實並沒有影響,只是我個人對取名的習慣,用來提醒自己這條規則是適用於外部流量還是叢集內部流量。 在這裡我們定義了一個內部服務的規則,即只有是帶有type:grey的POD例項流過來的流量,才能進入grey分組。即滿足了我們上述的第三個需求,整個服務鏈路要麼是全部新例項,要麼是全部老例項。

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: auth-service-internal-vsc
spec:
  hosts:
  - auth-service
  http:
  - match:
    - sourceLabels:
        type: grey
    route:
    - destination:
        host: auth-service
        subset: grey
  - route:
    - destination:
        host: auth-service
        subset: normal複製程式碼

對於我們的第三類服務,即既接收外部流量,同樣也接受內部流量的賬戶服務來說,我們只需要將上文提到的兩個virtualService結合起來即可:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: account-service-external-vsc
spec:
  hosts:
    - "www.example.com"
  gateways:
  - example-gateway
  http:
  - match:
    - headers:
        end-user:
          exact: test
      uri:
        prefix: /api/account
    route:
    - destination:
        port:
          number: 80
        host: account-service
        subset: grey
  - match:
    - uri:
        prefix: /api/account
    route:
    - destination:
        port:
          number: 80
        host: account-service
        subset: normal

---

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: account-service-internal-vsc
spec:
  hosts:
  - "account-service"
  http:
  - match:
    - sourceLabels:
        type: grey
    route:
    - destination:
        host: account-service
        subset: grey
  - route:
    - destination:
        host: account-service
        subset: normal
複製程式碼

至此,我們就已經完成了灰度釋出準備的第一步,也是一大步。當新服務例項釋出上去以後,我們在最初通過新增特定的header進入新服務,同時保證所有的外部服務只會進入老服務。當內部人員驗證完新服務例項在生產環境的表現後,我們需要逐漸開放流量比例將外部的使用者流量匯入到新服務例項,這一塊可以通過更改第一類和第三類服務的external-vsc來達到,下面給出一個例子:

下面這個例子則是表現為對於外部流量而言,將會一半進入grey分組,一般進入normal分組。最終我們可以將grey分組的weigth變更為100,而normal分組的weight變更為0,即將所有流量匯入到grey分組,灰度釋出完成。

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: page-service-external-vsc
spec:
  hosts:
  - "www.example.com"
  http:
  - route:
    - destination:
        host: page-service
        subset: grey
      weight: 50
    - destination:
        host: page-service
        subset: normal
      weight: 50複製程式碼

收尾工作

從上述的方案當中,我們將所有服務根據網路流量來源分為三類,並且通過istio實現了整個業務的灰度釋出。然而整個灰度釋出還並沒有完全結束,我們還需要一點收尾工作。

考慮整個業務剛開始的狀態我們有3個Kubernetes service,3個Kubernetes Deployment,每個Deployment的POD都帶有了type:normal的標籤。 然而現在經過上述方案以後,我們同樣有3個Kubernetes service,3個Kubernetes Deployment,但是這裡每個Deployment的POD卻都帶有了type:grey的標籤。

所以在經過上述灰度釋出以後,我們還要狀態恢復為初始值,這有利於我們下一次進行灰度釋出。由於對於Coohom專案,在CICD上使用的是Gitlab-ci,所以我們的自動化灰度釋出收尾工作深度繫結了Gitlab-ci的指令碼,所以這裡就不做介紹,各位讀者可以根據自身情況量身定製。

結語

以上就是目前Coohom在istio使用上關於灰度釋出的一些實踐和經驗。對於Coohom專案而言,在生產環境中使用istio是從istio正式釋出1.0.0版本以後才開始的。但是在這之前,我們在內網環境使用istio已經將近有半年的時間了,Coohom在內網中從istio0.7.1版本開始使用。內網環境在中長期時間內與生產環境環境架構不一致是反直覺的,一聽就不靠譜。然而,恰恰istio 是對業務完全透明的, 它可以看作是基礎設施的一部分,所以我們在生產環境使用istio之前,在內網環境下先上了istio,積累了不少經驗。

ServiceMesher社群資訊

微信群:聯絡我入群

社群官網:www.servicemesher.com

Slack:servicemesher.slack.com 需要邀請才能加入

Twitter: twitter.com/servicemesh…

GitHub:github.com/servicemesh…

更多Service Mesh諮詢請掃碼關注微信公眾號ServiceMesher。

雲端設計平臺Coohom在生產環境中使用istio的經驗與實踐


相關文章