本系列文章将介绍用户从 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 的问题,在后续文章中再继续和大家分享。

附录

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

Istio 运维实战系列(2):让人头大的『无头服务』-上的更多相关文章

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

    本系列文章将介绍用户从 Spring Cloud,Dubbo 等传统微服务框架迁移到 Istio 服务网格时的一些经验,以及在使用 Istio 过程中可能遇到的一些常见问题的解决方法. 失败的 Eur ...

  2. Istio 运维实战系列(1):应用容器对 Envoy Sidecar 的启动依赖问题

    本系列文章将介绍用户从 Spring Cloud,Dubbo 等传统微服务框架迁移到 Istio 服务网格时的一些经验,以及在使用 Istio 过程中可能遇到的一些常见问题的解决方法. 故障现象 该问 ...

  3. DEVOPS 运维开发系列

    DEVOPS 运维开发系列四:ITIL事态管理流程.事态监控系统设计以及基于Devops的效率提升实践 - watermelonbig的专栏 - CSDN博客https://blog.csdn.net ...

  4. Hadoop运维记录系列

    http://slaytanic.blog.51cto.com/2057708/1038676 Hadoop运维记录系列(一) Hadoop运维记录系列(二) Hadoop运维记录系列(三) Hado ...

  5. 《Splunk智能运维实战》——1.7 为本书加载样本数据

    本节书摘来自华章计算机<Splunk智能运维实战>一书中的第1章,第1.7节,作者 [美]乔史·戴昆(Josh Diakun),保罗R.约翰逊(Paul R. Johnson),德莱克·默 ...

  6. CentOS7系统管理与运维实战

    CentOS7系统管理与运维实战 下载地址 https://pan.baidu.com/s/1KFHVI-XjGaLMrh39WuhyCw 扫码下面二维码关注公众号回复100007 获取分享码 本书目 ...

  7. 运维实战案例之“Too many open files”错误与解决方法

    运维实战案例之"Too many open files"错误与解决方法   技术小甜 2017-11-16 15:02:00 浏览869 服务器 shell tomcat 脚本 o ...

  8. Linux运维面试题:请简要说明Linux系统在目标板上的启动过程?

    Linux运维面试题:请简要说明Linux系统在目标板上的启动过程? 该问题是Linux运维面试最常见的问题之一,问题答案如下: 1.用户打开PC的电源,BIOS开机自检,按BIOS中设置的启动设备( ...

  9. 企业Shell面试题及企业运维实战案例(三)

    1.企业Shell面试题1:批量生成随机字符文件名案例 使用for循环在/oldboy目录下批量创建10个html文件,其中每个文件需要包含10个随机小写字母加固定字符串oldboy,名称示例如下: ...

随机推荐

  1. Java引用类型之最终引用

    FinalReference类只有一个子类Finalizer,并且Finalizer由关键字final修饰,所以无法继承扩展.类的定义如下: class FinalReference<T> ...

  2. 3.MongoDB恢复探究:为什么oplogReplay参数只设置了日志应用结束时间oplogLimit,而没有设置开始时间?

    (一)问题 在使用MySQL数据库binlog日志基于时间点恢复数据库时,我们必须要指定binlog的开始位置和结束位置,而在MongoDB里面,如果使用oplog进行恢复,只有oplogLimit参 ...

  3. PHP基础之常量与变量

    1.变量:用来存储信息的空间大小 $var 2.常量:定义之后不可以更改,标识符,并且给其赋值,常量是全局,在整个页面中均可使用,常量一般有英文字母.下划线.数字组成,开头不能是数字和$ 使用defi ...

  4. MAC安装Navicat Premiun12

    链接地址:https://blog.csdn.net/wenyicodedog/article/details/97970154

  5. Soft-to-Hard Vector Quantization for End-to-End Learning Compressible Representations

    郑重声明:原文参见标题,如有侵权,请联系作者,将会撤销发布! Abstract: 我们提出了一种新的方法,通过端到端的训练策略来学习深度架构中的可压缩表征.我们的方法是基于量化和熵的软(连续)松弛,我 ...

  6. docker run 创建容器

    docker run常用命令 docker run :创建一个新的容器并运行一个命令 - 语法:docker run [OPTIONS] IMAGE [COMMAND] [ARG...] 1.OPTI ...

  7. 怎么下载chrome的扩展程序

    很多时候我们是没办法访问谷歌扩展应用程序 chrome应用商店的,这时候我们最好能把对应扩展应用程序下载保存,以便提供给其他人员使用. 搜索得到知乎有很全的方法: 如何导出并打包第三方chrome扩展 ...

  8. 牛客网PAT练兵场-跟奥巴马一起编程

    题目地址: 题意:无 /** * *作者:Ycute *时间:2019-11-14-21.29.07 *题目题意简单描述:模拟题输出 */ #include<iostream> #incl ...

  9. Java多线程_Atomic

    1.什么是Atomic?Atomic,中文意思是“原子的”,在java多线程中,有这样的一个包: java.util.concurrent.atomic——线程安全的原子操作包 这是JDK1.5的版本 ...

  10. 仓库ERP管理系统(springboot)

    查看更多系统:系统大全,课程设计.毕业设计,请点击这里查看 01 系统概述 基于SpringBoot框架和SaaS模式,非常好用的ERP软件,目前专注进销存+财务功能.主要模块有零售管理.采购管理.销 ...