专栏首页腾讯云原生团队Istio 运维实战系列(2):让人头大的『无头服务』-上

Istio 运维实战系列(2):让人头大的『无头服务』-上

作者赵化冰,腾讯云高级工程师,Istio contributor,ServiceMesher管理委员,热衷于开源、网络和云计算。目前主要从事服务网格的开源和研发工作。

本系列文章将介绍用户从 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 的相关源代码(https://github.com/istio/istio/blob/514fb926e32fb95d8ee9b63d1741bf399c386a5e/pkg/kube/inject/webhook.go#L570),可以得知,当 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

(https://github.com/istio/istio/issues/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 的问题,在后续文章中再继续和大家分享。

附录

文章分享自微信公众号:
腾讯云原生

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!

如有侵权,请联系 yunjia_community@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • Istio 运维实战系列(3):让人头大的『无头服务』-下

    作者赵化冰,腾讯云高级工程师,Istio contributor,ServiceMesher管理委员,热衷于开源、网络和云计算。目前主要从事服务网格的开源和研...

    腾讯云原生
  • 开工必备!50+篇超实用云原生技术干货合集

    kai 开 gong 工 da 大 ji 吉 新年新气象,更要1G棒 2020年没写完的代码,现在还有思路吗? 2021年开始使用云原生技术了吗? 一开工就遇...

    腾讯云原生
  • 技术集锦 | 服务网格 & Istio 技术原理及最佳实践系列

    前言 作为 CNCF 定义的云原生关键技术之一,服务网格发展至今已经有五个年头了,其发展目前正处于大规模落地及生态发展阶段。 服务网格是一个专门处理服务通讯的基...

    腾讯云原生
  • 转发有礼 | 50篇+云原生系列干货文章汇总,请查收!

    云原生技术干货文章合集,来咯~ 2020 年,要说咱们技术圈子里什么最火? 云原生肯定是那 NO.1 截止目前,我们不难看出,K8s 容器、服务网格、...

    腾讯云原生
  • 中秋福利 | 15个系列100+篇超实用云原生原创干货合集(内含腾讯彩蛋)

    还有2天,就要迎来中秋小长假啦 这个中秋节你打算怎么过? 小云选择把这篇干货全部拿下! 云原生技术干货文章合集,来咯~ 2021 年,要说咱们技术圈子里什...

    腾讯云原生
  • 9月容器产品技术月报 | 腾讯云原生专题上线啦

    2020年9月 VOL:05 容器产品新特性 9月上新 腾讯云边缘服务TKE Edge 从中心云管理边缘云资源的容器系统 边缘容器服务(Tenc...

    腾讯云原生
  • Istio尚存众多挑战,能否最终吃鸡?

    Istio项目在公开发布14个月后,今天迎来了1.0发布版本。然而,该服务网格管理平台仍然缺少云计算巨头AWS和Azure的正式支持,而且其他云服务供应商仍然在...

    CNCF
  • 为微服务引入Istio服务网格(上)

    魏新宇
  • 解读服务网格的2021:告别架构“大跃进”,技术生态百家争鸣

    服务网格的 2021,“稳” 字当先。不管是原生社区发展,还是行业实践落地,都以 “稳定” 为第一要义。少了前几年大跃进式的架构演进、功能更迭,多了更务实、更...

    深度学习与Python
  • 谷歌与微软,勇士与恶龙的身份互换?

    近日,微软宣布开源轻量级服务网格(Service Mesh)项目 Open Service Mesh (OSM),并强调将在第一时间把该项目捐赠给云原生计算基金...

    程序IT圈
  • 【译文连载】 理解Istio服务网格(第六章 可观测性)

    微服务架构管理中最大的挑战之一是如何通过简单方法就能了解系统各个组件之间的关系。终端用户的一次会话可能会流经多个甚至几十个独立部署的微服务,因此,发现哪里有性能...

    SammyLiu
  • Istio简单介绍

    Istio是一个开放平台,提供统一的方式来集成微服务,管理跨微服务的流量,执行策略和汇总遥测数据。Istio的控制面板在底层集群管理平台(如Kubernetes...

    dogfei
  • Istio以及Service Mesh的未来

    不夸张的说,正是 Istio 的出现使 “Service Mesh” 这一概念开始流行起来。在深入介绍 Istio 的细节之前,让我们首先简单地了解一下 Ser...

    程序你好
  • Service Mesh实战(Istio)

    yeedomliu
  • 以后别人再问你什么是 Istio,就把这篇文章甩给他

    从上面的定义中可以了解到,Istio 为微服务应用提供了一个完整的解决方案,可以以统一的方式去检测和管理微服务。同时,它还提供了管理流量、实施访问策略、收集数据...

    米开朗基杨
  • 应用服务网格(Service Mesh)应对微服务中面临的三种挑战

    微服务适用于开发运维(DevOps),可是这些架构依赖的服务到服务通信在生产环境下运行和管理起来很复杂。这时候Service Mesh闪亮登场了:这是企业扩展、...

    程序你好
  • (译)SMI:推动服务网格社区举步前行

    在运行一个服务式架构的应用时,往往会面临服务间通信的挑战,服务网格技术正是为此而生。Kubernetes 和容器技术对工作负载的在大量服务器上的部署和进行提供了...

    崔秀龙
  • 云原生 - 体验Istio的完美入门之旅(一)

    随着微服务架构的流行,服务网格技术获得了业界的广泛关注,作为实现云原生的重要积木,各大厂商也纷纷开始布局,Amazon在2019年4月份推出了App Mesh;...

    justmine

扫码关注云+社区

领取腾讯云代金券