乐趣区

关于腾讯云:Istio-运维实战系列2让人头大的『无头服务』上

本系列文章将介绍用户从 Spring Cloud,Dubbo 等传统微服务框架迁徙到 Istio 服务网格时的一些教训,以及在应用 Istio 过程中可能遇到的一些常见问题的解决办法。

什么是『无头服务』?

『无头服务』即 Kubernetes 中的 Headless Service。Service 是 Kubernetes 对后端一组提供雷同服务的 Pod 的逻辑形象和拜访入口。Kubernetes 会依据调度算法为 Pod 调配一个运行节点,并随机调配一个 IP 地址;在很多状况下,咱们还会对 Pod 进行程度伸缩,启动多个 Pod 来提供雷同的服务。在有多个 Pod 并且 Pod IP 地址不固定的状况下,客户端很难通过 Pod 的 IP 地址来间接进行拜访。为了解决这个问题,Kubernetes 采纳 Service 资源来示意提供雷同服务的一组 Pod。

在缺省状况下,Kubernetes 会为 Service 调配一个 Cluster IP,不论后端的 Pod IP 如何变动,Service 的 Cluster IP 始终是固定的。因而客户端能够通过这个 Cluster IP 来拜访这一组 Pod 提供的服务,而无需再关注后端的各个实在的 Pod IP。咱们能够将 Service 看做放在一组 Pod 前的一个负载均衡器,而 Cluster IP 就是该负载均衡器的地址,这个负载均衡器会关注后端这组 Pod 的变动,并把发向 Cluster IP 的申请转发到后端的 Pod 上。(备注:这只是对 Service 的一个简化形容,如果对 Service 的外部实现感兴趣,能够参考这篇文章 如何为服务网格抉择入口网关?)

对于无状态的利用来说,客户端并不在意其连贯的是哪一个 Pod,采纳 Service 是没有问题的。但在某些非凡状况下,并不能这样做。例如,如果后端的这一组 Pod 是有状态的,须要由客户端依据某种利用相干的算法来抉择哪一个 Pod 提供服务;或者客户端须要连贯所有的后端 Pod,这时咱们就不能在这一组 Pod 前放一个负载均衡器了。这种状况下,咱们须要采纳 Headless Service,即无头服务(该命名把多个 Pod 后面的负载均衡器比作服务的头,很形象是不是?)。在定义 Headless Service,咱们须要把 Service 的 Cluster IP 显示设置为 None,这样 Kubernetes DNS 在解析该 Service 时会间接返回其后端的多个 Pod IP,而不是 Service 的 Cluster IP。

假如从客户端拜访一个 Redis 集群,别离采纳带 Cluster IP 的一般 Service 和 Headless Service 进行拜访的过程如下图所示:

Istio 中『无头服务』的 mTLS 故障

因为 Headless Service 的特殊性,Istio 中对 Headless Service 的解决和一般 Service 有所不同,在利用迁徙到 Isito 的过程中也经常遇到因为 Headless Service 导致的一些问题。上面咱们就以一个因为 Headless Service 的 mTLS 故障导致的典型案例进行阐明。

故障景象:运维同学反馈从带 Envoy Sidecar 的 Pod 中拜访 Redis 服务器,但在没有装置 Sidecar 的 Pod 中能够失常拜访该 Redis 服务器。

遇到无奈进行出向拜访的问题,咱们能够首先通过 Envoy 的治理接口来查看 Envoy 的拜访日志。在客户端 Pod 中运行上面的命令查看 Envoy 日志:

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 配置,以进一步剖析其失败起因。

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 客户端认为是这样的:

但实际上是这样的:

在服务器端没有装置 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 两种状况的。

通过和失常 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 的问题,在后续文章中再持续和大家分享。

附录

  • 如何为服务网格抉择入口网关?
  • Understanding Envoy Proxy HTTP Access Logs
  • 一文带你彻底厘清 Isito 中的证书工作机制
  • Istio 运维实战系列(1):利用容器对 Envoy Sidecar 的启动依赖问题

【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,扫码关注同名公众号,及时获取更多干货!!

退出移动版