Istio 運維實戰系列(2):讓人頭大的『無頭服務』-上

本系列文章將介紹用戶從 Spring Cloud,Dubbo 等傳統微服務框架遷移到 Istio 服務網格時的一些經驗,以及在使用 Istio 過程當中可能遇到的一些常見問題的解決方法。git

什麼是『無頭服務』?

『無頭服務』即 Kubernetes 中的 Headless Service。Service 是 Kubernetes 對後端一組提供相同服務的 Pod 的邏輯抽象和訪問入口。Kubernetes 會根據調度算法爲 Pod 分配一個運行節點,並隨機分配一個 IP 地址;在不少狀況下,咱們還會對 Pod 進行水平伸縮,啓動多個 Pod 來提供相同的服務。在有多個 Pod 而且 Pod IP 地址不固定的狀況下,客戶端很難經過 Pod 的 IP 地址來直接進行訪問。爲了解決這個問題,Kubernetes 採用 Service 資源來表示提供相同服務的一組 Pod。github

在缺省狀況下,Kubernetes 會爲 Service 分配一個 Cluster IP,無論後端的 Pod IP 如何變化,Service 的 Cluster IP 始終是固定的。所以客戶端能夠經過這個 Cluster IP 來訪問這一組 Pod 提供的服務,而無需再關注後端的各個真實的 Pod IP。咱們能夠將 Service 看作放在一組 Pod 前的一個負載均衡器,而 Cluster IP 就是該負載均衡器的地址,這個負載均衡器會關注後端這組 Pod 的變化,並把發向 Cluster IP 的請求轉發到後端的 Pod 上。(備註:這只是對 Service 的一個簡化描述,若是對 Service 的內部實現感興趣,能夠參考這篇文章 如何爲服務網格選擇入口網關?web

對於無狀態的應用來講,客戶端並不在乎其鏈接的是哪個 Pod,採用 Service 是沒有問題的。但在某些特殊狀況下,並不能這樣作。例如,若是後端的這一組 Pod 是有狀態的,須要由客戶端根據某種應用相關的算法來選擇哪個 Pod 提供服務;或者客戶端須要鏈接全部的後端 Pod,這時咱們就不能在這一組 Pod 前放一個負載均衡器了。這種狀況下,咱們須要採用 Headless Service,即無頭服務(該命名把多個 Pod 前面的負載均衡器比做服務的頭,很形象是否是?)。在定義 Headless Service,咱們須要把 Service 的 Cluster IP 顯示設置爲 None,這樣 Kubernetes DNS 在解析該 Service 時會直接返回其後端的多個 Pod IP,而不是 Service 的 Cluster IP。redis

假設從客戶端訪問一個 Redis 集羣,分別採用帶 Cluster IP 的普通 Service 和 Headless Service 進行訪問的過程以下圖所示:算法

Istio 運維實戰系列(2):讓人頭大的『無頭服務』-上

Istio 中『無頭服務』的 mTLS 故障

因爲 Headless Service 的特殊性,Istio 中對 Headless Service 的處理和普通 Service 有所不一樣,在應用遷移到 Isito 的過程當中也經常遇到因爲 Headless Service 致使的一些問題。下面咱們就以一個因爲 Headless Service 的 mTLS 故障致使的典型案例進行說明。後端

故障現象:運維同窗反饋從帶 Envoy Sidecar 的 Pod 中訪問 Redis 服務器,但在沒有安裝 Sidecar 的 Pod 中能夠正常訪問該 Redis 服務器。api

遇到沒法進行出向訪問的問題,咱們能夠首先經過 Envoy 的管理接口來查看 Envoy 的訪問日誌。在客戶端 Pod 中運行下面的命令查看 Envoy 日誌:bash

kubectl logs -f redis-client-6d4c6c975f-bm5w6 -c istio-proxy

日誌中對 Redis 的訪問記錄以下,其中 UR,URX 是 Response Flag,表示 upstream connection failure,即鏈接上游失敗。服務器

[2020-09-12T13:38:23.077Z] "- - -" 0 UF,URX "-" "-" 0 0 1001 - "-" "-" "-" "-" "10.1.1.24:6379" outbound|6379||redis.default.svc.cluster.local - 10.1.1.24:6379 10.1.1.25:45940 - -

咱們能夠經過 Envoy 管理接口導出其 xDS 配置,以進一步分析其失敗緣由。app

kubectl exec redis-client-6d4c6c975f-bm5w6 -c istio-proxy curl http://127.0.0.1:15000/config_dump

因爲是出向訪問錯誤,所以咱們主要關注客戶端中該出向訪問的 Cluster 的配置。在導出的 xDS 配置中,能夠看到 Redis Cluster 的配置,以下面的 yaml 片斷所示(爲了方便讀者查看,去掉了該 yaml 中一些無關的內容):

{
     "version_info": "2020-09-13T00:33:43Z/5",
     "cluster": {
      "@type": "type.googleapis.com/envoy.api.v2.Cluster",
      "name": "outbound|6379||redis.default.svc.cluster.local",
      "type": "ORIGINAL_DST",
      "connect_timeout": "1s",
      "lb_policy": "CLUSTER_PROVIDED",
      "circuit_breakers": {
        ...
      },

      # mTLS 相關設置
      "transport_socket": {
       "name": "envoy.transport_sockets.tls",
       "typed_config": {
        "@type": "type.googleapis.com/envoy.api.v2.auth.UpstreamTlsContext",
        "common_tls_context": {
         "alpn_protocols": [
          "istio-peer-exchange",
          "istio"
         ],

         # 訪問 Redis 使用的客戶端證書
         "tls_certificate_sds_secret_configs": [
          {
           "name": "default",
           "sds_config": {
            "api_config_source": {
             "api_type": "GRPC",
             "grpc_services": [
              {
                "envoy_grpc": {
                "cluster_name": "sds-grpc"
               }
              }
             ]
            }
           }
          }
         ],

         "combined_validation_context": {
          "default_validation_context": {
           # 用於驗證 Redis 服務器身份的 spiffe indentity
           "verify_subject_alt_name": [
            "spiffe://cluster.local/ns/default/sa/default"
           ]
          },
          # 用於驗證 Redis 服務器的根證書
          "validation_context_sds_secret_config": {
           "name": "ROOTCA",
           "sds_config": {
            "api_config_source": {
             "api_type": "GRPC",
             "grpc_services": [
              {
               "envoy_grpc": {
                "cluster_name": "sds-grpc"
               }
              }
             ]
            }
           }
          }
         }
        },
        "sni": "outbound_.6379_._.redis.default.svc.cluster.local"
       }
      },
      "filters": [
       {
         ...
       }
      ]
     },
     "last_updated": "2020-09-13T00:33:43.862Z"
    }

在 transport_socket 部分的配置中,咱們能夠看到 Envoy 中配置了訪問 Redis Cluster 的 tls 證書信息,包括 Envoy Sidecar 用於訪問 Redis 使用的客戶端證書,用於驗證 Redis 服務器證書的根證書,以及採用 spiffe 格式表示的,需驗證的服務器端身份信息。 這裏的證書相關內容是使用 xDS 協議中的 SDS(Secret discovery service) 獲取的,因爲篇幅緣由在本文中不對此展開進行介紹。若是須要了解 Istio 的證書和 SDS 相關機制,能夠參考這篇文章一文帶你完全釐清 Isito 中的證書工做機制。從上述配置能夠得知,當收到 Redis 客戶端發起的請求後,客戶端 Pod 中的 Envoy Sidecar 會使用 mTLS 向 Redis 服務器發起請求。

Redis 客戶端中 Envoy Sidecar 的 mTLS 配置自己看來並無什麼問題。但咱們以前已經得知該 Redis 服務並未安裝 Envoy Sidecar,所以實際上 Redis 服務器端只能接收 plain TCP 請求。這就致使了客戶端 Envoy Sidecar 在向 Redis 服務器建立連接時失敗了。

Redis 客戶端覺得是這樣的:

Istio 運維實戰系列(2):讓人頭大的『無頭服務』-上

但其實是這樣的:

Istio 運維實戰系列(2):讓人頭大的『無頭服務』-上

在服務器端沒有安裝 Envoy Sidecar,不支持 mTLS 的狀況下,按理客戶端的 Envoy 不該該採用 mTLS 向服務器端發起鏈接。這是怎麼回事呢?咱們對比一下客戶端 Envoy 中的其餘 Cluster 中的相關配置。

一個訪問正常的 Cluster 的 mTLS 相關配置以下:

{
     "version_info": "2020-09-13T00:32:39Z/4",
     "cluster": {
      "@type": "type.googleapis.com/envoy.api.v2.Cluster",
      "name": "outbound|8080||awesome-app.default.svc.cluster.local",
      "type": "EDS",
      "eds_cluster_config": {
       "eds_config": {
        "ads": {}
       },
       "service_name": "outbound|8080||awesome-app.default.svc.cluster.local"
      },
      "connect_timeout": "1s",
      "circuit_breakers": {
       ...
      },
      ...

      # mTLS 相關的配置
      "transport_socket_matches": [
       {
        "name": "tlsMode-istio",
        "match": {
         "tlsMode": "istio"  #對帶有 "tlsMode": "istio" lable 的 endpoint,啓用 mTLS
        },
        "transport_socket": {
         "name": "envoy.transport_sockets.tls",
         "typed_config": {
          "@type": "type.googleapis.com/envoy.api.v2.auth.UpstreamTlsContext",
          "common_tls_context": {
           "alpn_protocols": [
            "istio-peer-exchange",
            "istio",
            "h2"
           ],
           "tls_certificate_sds_secret_configs": [
            {
             "name": "default",
             "sds_config": {
              "api_config_source": {
               "api_type": "GRPC",
               "grpc_services": [
                {
                 "envoy_grpc": {
                  "cluster_name": "sds-grpc"
                 }
                }
               ]
              }
             }
            }
           ],
           "combined_validation_context": {
            "default_validation_context": {},
            "validation_context_sds_secret_config": {
             "name": "ROOTCA",
             "sds_config": {
              "api_config_source": {
               "api_type": "GRPC",
               "grpc_services": [
                {
                 "envoy_grpc": {
                  "cluster_name": "sds-grpc"
                 }
                }
               ]
              }
             }
            }
           }
          },
          "sni": "outbound_.6379_._.redis1.dubbo.svc.cluster.local"
         }
        }
       },
       {
        "name": "tlsMode-disabled",
        "match": {},   # 對全部其餘的 enpoint,不啓用 mTLS,使用 plain TCP 進行鏈接
        "transport_socket": {
         "name": "envoy.transport_sockets.raw_buffer"
        }
       }
      ]
     },
     "last_updated": "2020-09-13T00:32:39.535Z"
    }

從配置中能夠看到,一個正常的 Cluster 中有兩部分 mTLS 相關的配置:tlsMode-istio 和 tlsMode-disabled。tlsMode-istio 部分和 Redis Cluster 的配置相似,但包含一個匹配條件(match部分),該條件表示只對帶有 "tlsMode" : "istio" lable 的 endpoint 啓用 mTLS;對於不帶有該標籤的 endpoint 則會採用 tlsMode-disabled 部分的配置,使用 raw_buffer,即 plain TCP 進行鏈接。

查看 Istio 的相關源代碼,能夠得知,當 Istio webhook 向 Pod 中注入 Envoy Sidecar 時,會同時爲 Pod 添加一系列 label,其中就包括 "tlsMode" : "istio" 這個 label,以下面的代碼片斷所示:

patchLabels := map[string]string{
        label.TLSMode:                                model.IstioMutualTLSModeLabel,
        model.IstioCanonicalServiceLabelName:         canonicalSvc,
        label.IstioRev:                               revision,
        model.IstioCanonicalServiceRevisionLabelName: canonicalRev,
    }

因爲 Pod 在被注入 Envoy Sidecar 的同時被加上了該標籤,客戶端 Enovy Sidecar 在向該 Pod 發起鏈接時,根據 endpoint 中的標籤匹配到 tlsMode-istio 中的配置,就會採用 mTLS;而若是一個 Pod 沒有被注入 Envoy Sidecar,天然不會有該 Label,所以不能知足前面配置所示的匹配條件,客戶端的 Envoy Sidecar 會根據 tlsMode-disabled 中的配置,採用 plain TCP 鏈接該 endpoint。這樣同時兼容了服務器端支持和不支持 mTLS 兩種狀況。

下圖展現了 Istio 中是如何經過 endpoint 的標籤來兼容 mTLS 和 plain TCP 兩種狀況的。

Istio 運維實戰系列(2):讓人頭大的『無頭服務』-上

經過和正常 Cluster 的對比,咱們能夠看到 Redis Cluster 的配置是有問題的,按理 Redis Cluster 的配置也應該經過 endpoint 的 tlsMode 標籤進行判斷,以決定客戶端的 Envoy Sidecar 是經過 mTLS 仍是 plain TCP 發起和 Redis 服務器的鏈接。但實際狀況是 Redis Cluster 中只有 mTLS 的配置,致使了前面咱們看到的鏈接失敗故障。

Redis 是一個 Headless Service,經過在社區查找相關資料,發現 Istio 1.6 版本前對 Headless Service 的處理有問題,致使了該故障。參見這個 Issue Istio 1.5 prevents all connection attempts to Redis (headless) service #21964

解決方案

找到了故障緣由後,要解決這個問題就很簡單了。咱們能夠經過一個 Destination Rule 禁用 Redis Service 的 mTLS。以下面的 yaml 片斷所示:

kind: DestinationRule
metadata:
  name: redis-disable-mtls
spec:
  host: redis.default.svc.cluster.local
  trafficPolicy:
    tls:
      mode: DISABLE

再查看客戶端 Envoy 中的 Redis Cluster 配置,能夠看到 mTLS 已經被禁用,Cluster 中再也不有 mTLS 相關的證書配置。

{
     "version_info": "2020-09-13T09:02:28Z/7",
     "cluster": {
      "@type": "type.googleapis.com/envoy.api.v2.Cluster",
      "name": "outbound|6379||redis.dubbo.svc.cluster.local",
      "type": "ORIGINAL_DST",
      "connect_timeout": "1s",
      "lb_policy": "CLUSTER_PROVIDED",
      "circuit_breakers": {
        ...
      },
      "metadata": {
       "filter_metadata": {
        "istio": {
         "config": "/apis/networking.istio.io/v1alpha3/namespaces/dubbo/destination-rule/redis-disable-mtls"
        }
       }
      },
      "filters": [
       {
        "name": "envoy.filters.network.upstream.metadata_exchange",
        "typed_config": {
         "@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
         "type_url": "type.googleapis.com/envoy.tcp.metadataexchange.config.MetadataExchange",
         "value": {
          "protocol": "istio-peer-exchange"
         }
        }
       }
      ]
     },
     "last_updated": "2020-09-13T09:02:28.514Z"
    }

此時再嘗試從客戶端訪問 Redis 服務器,一切正常!

小結

Headless Service 是 Kubernetes 中一種沒有 Cluster IP 的特殊 Service,Istio 中對 Headless Service 的處理流程和普通 Service 有所不一樣。因爲 Headless Service 的特殊性,咱們在將應用遷移到 Istio 的過程當中經常會遇到與此相關的問題。

此次咱們遇到的問題是因爲 Istio 1.6 以前的版本,對 Headless Service 處理的一個 Bug 致使沒法鏈接到 Headless Service。該問題是一個高頻故障,咱們已經遇到過屢次。能夠經過建立 Destination Rule 禁用 Headless Service 的 mTLS 來規避該問題。該故障在1.6版本中已經修復,建議儘快升級到 1.6 版本,以完全解決本問題。也能夠直接採用騰訊雲上的雲原生 Service Mesh 服務 TCM(Tencent Cloud Mesh),爲微服務應用快速引入 Service Mesh 的流量管理和服務治理能力,而無需再關注 Service Mesh 基礎設施自身的安裝、維護、升級等事項。

Headless Service 的坑較多,除了這一個故障之外,咱們還在遷移過程當中遇到了其餘一些關於 Headless Service 的問題,在後續文章中再繼續和你們分享。

附錄

相關文章
相關標籤/搜索