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

3次阅读

共计 10879 个字符,预计需要花费 28 分钟才能阅读完成。

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

失败的 Eureka 心跳告诉

在上一篇文章中,咱们介绍了 Headless Service 和一般 Service 的区别。因为 Headless Service 的特殊性,在 Istio 下发给 Envoy Sidecar 的配置中,此类服务的配置参数和其余服务的参数有所不同。除了咱们上次遇到的 mTLS 故障之外,这些差别可能还会导致利用呈现一些其余意想不到的状况。

这次遇到的问题景象是:在 Spring Cloud 利用迁徙到 Istio 中后,服务提供者向 Eureka Server 发送心跳失败。

备注:Eureka Server 采纳心跳机制来断定服务的衰弱状态。服务提供者在启动后,周期性(默认 30 秒)向 Eureka Server 发送心跳,以证实以后服务是可用状态。Eureka Server 在肯定的工夫(默认 90 秒)未收到客户端的心跳,则认为服务宕机,登记该实例。

查看应用程序日志,能够看到 Eureka 客户端发送心跳失败的相干日志信息。

2020-09-24 13:32:46.533 ERROR 1 --- [tbeatExecutor-0] com.netflix.discovery.DiscoveryClient    : DiscoveryClient_EUREKA-TEST-CLIENT/eureka-client-544b94f967-gcx2f:eureka-test-client - was unable to send heartbeat!

com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server
    at com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient.execute(RetryableEurekaHttpClient.java:112) ~[eureka-client-1.9.13.jar!/:1.9.13]
    at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator.sendHeartBeat(EurekaHttpClientDecorator.java:89) ~[eureka-client-1.9.13.jar!/:1.9.13]
    at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator$3.execute(EurekaHttpClientDecorator.java:92) ~[eureka-client-1.9.13.jar!/:1.9.13]
    at com.netflix.discovery.shared.transport.decorator.SessionedEurekaHttpClient.execute(SessionedEurekaHttpClient.java:77) ~[eureka-client-1.9.13.jar!/:1.9.13]
    at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator.sendHeartBeat(EurekaHttpClientDecorator.java:89) ~[eureka-client-1.9.13.jar!/:1.9.13]
    at com.netflix.discovery.DiscoveryClient.renew(DiscoveryClient.java:864) ~[eureka-client-1.9.13.jar!/:1.9.13]
    at com.netflix.discovery.DiscoveryClient$HeartbeatThread.run(DiscoveryClient.java:1423) ~[eureka-client-1.9.13.jar!/:1.9.13]
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) ~[na:na]
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630) ~[na:na]
    at java.base/java.lang.Thread.run(Thread.java:832) ~[na:na]

过期的 IP 地址

对于申请失败类的故障,咱们首先能够通过 Envoy 的拜访日志查看失败起因。通过上面的命令查看客户端 Envoy Sidecar 的日志:

k logs -f eureka-client-66f748f84f-vvvmz -c eureka-client -n eureka

从 Envoy 日志中能够查看到客户端通过 HTTP PUT 向服务器收回的心跳申请。该申请的 Response 状态码为 “UF,URX”,示意其 Upstream Failure,即连贯上游服务失败。在日志中还能够看到,在连贯失败后,Envoy 向客户端利用返回了一个 “503” HTTP 错误码。

[2020-09-24T13:31:37.980Z] "PUT /eureka/apps/EUREKA-TEST-CLIENT/eureka-client-544b94f967-gcx2f:eureka-test-client?status=UP&lastDirtyTimestamp=1600954114925 HTTP/1.1" 503 UF,URX "-" "-" 0 91 3037 - "-" "Java-EurekaClient/v1.9.13" "1cd54507-3f93-4ff3-a93e-35ead11da70f" "eureka-server:8761" "172.16.0.198:8761" outbound|8761||eureka-server.eureka.svc.cluster.local - 172.16.0.198:8761 172.16.0.169:53890 - default

从日志中能够看到拜访的 Upstream Cluster 是 outbound|8761||eureka-server.eureka.svc.cluster.local,Envoy 将该申请转发到了 IP 地址 为 172.16.0.198 的 Upstream Host。

查看集群中部署的服务,能够看到 eureka-server 是一个 Headless Service。

HUABINGZHAO-MB0:eureka-istio-test huabingzhao$ k get svc -n eureka -o wide
NAME            TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE   SELECTOR
eureka-server   ClusterIP   None         <none>        8761/TCP   17m   app=eureka-server

在本系列的上一篇文章『Istio 运维实战系列(2):让人头大的『无头服务』- 上』中,咱们理解到 Headless Service 并没有 Cluster IP,DNS 会间接将 Service 名称解析到 Service 后端的多个 Pod IP 上。Envoy 日志中显示连贯 Eureka Server 地址 172.16.0.198 失败,咱们来看看这个 IP 来自哪一个 Eureka Server 的 Pod。

HUABINGZHAO-MB0:eureka-istio-test huabingzhao$ k get pod -n eureka -o wide | grep eureka-server
NAME                             READY   STATUS    RESTARTS   AGE     IP             NODE        NOMINATED NODE   READINESS GATES
eureka-server-0                  1/1     Running   0          6h55m   172.16.0.59    10.0.0.15   <none>           <none>
eureka-server-1                  1/1     Running   0          6m1s    172.16.0.200   10.0.0.7    <none>           <none>
eureka-server-2                  1/1     Running   0          6h56m   172.16.1.3     10.0.0.14   <none>           <none>

从下面的命令输入中能够看到 Eureka 集群中有三个服务器,但没有哪一个服务器的 Pod IP 是 Envoy 日志中显示的 172.16.0.198。进一步剖析发现 eureka-server-1 Pod 的启动工夫比客户端的启动工夫晚很多,初步狐疑 Envoy 采纳了一个曾经被销毁的 Eureka Server 的 IP 进行拜访,导致拜访失败。

通过查看 Envoy dump 文件中 outbound|8761||eureka-server.eureka.svc.cluster.local 的相干配置,进一步加深了我对此的狐疑。从上面的 yaml 片段中能够看到该 Cluster 的类型为“ORIGINAL_DST”。

{
     "version_info": "2020-09-23T03:57:03Z/27",
     "cluster": {
      "@type": "type.googleapis.com/envoy.api.v2.Cluster",
      "name": "outbound|8761||eureka-server.eureka.svc.cluster.local",
      "type": "ORIGINAL_DST",  # 该选项表明 Enovy 在转发申请时会间接采纳 downstream 原始申请中的地址。"connect_timeout": "1s",
      "lb_policy": "CLUSTER_PROVIDED",
   ...

}  

依据 Envoy 的文档阐明,“ORIGINAL_DST”的解释为:

In these cases requests routed to an original destination cluster are forwarded to upstream hosts as addressed by the redirection metadata, without any explicit host configuration or upstream host discovery.

即对于“ORIGINAL_DST”类型的 Cluster,Envoy 在转发申请时会间接采纳 downstream 申请中的原始目的地 IP 地址,而不会采纳服务发现机制。Istio 中 Envoy Sidecar 的该解决形式和 K8s 对 Headless Service 的解决是相似的,即由客户端依据 DNS 间接抉择一个后端的 Pod IP,不会采纳负载平衡算法对客户端的申请进行重定向散发。但让人纳闷的是:为什么客户端通过 DNS 查问失去的 Pod 地址 172.16.0.198 拜访失败了呢?这是因为客户端查问 DNS 时失去的地址在访问期间曾经不存在了。下图解释了导致该问题的起因:

  1. Client 查问 DNS 失去 eureka-server 的三个 IP 地址。
  2. Client 抉择 Server-1 的 IP 172.16.0.198 发动连贯申请,申请被 iptables rules 拦挡并重定向到了客户端 Pod 中 Envoy 的 VirtualInbound 端口 15001。
  3. 在收到 Client 的连贯申请后,依据 Cluster 的配置,Envoy 采纳申请中的原始目标地址 172.16.0.198 连贯 Server-1,此时该 IP 对应的 Pod 是存在的,因而 Envoy 到 Server-1 的链接创立胜利,Client 和 Envoy 之间的链接也会建设胜利。Client 在创立链接时采纳了 HTTP Keep Alive 选项,因而 Client 会始终放弃该链接,并通过该链接以 30 秒距离继续发送 HTTP PUT 服务心跳告诉。
  4. 因为某些起因,该 Server-1 Pod 被 K8s 重建为 Server-1ꞌ,IP 产生了变动。
  5. 当 Server-1 的 IP 变动后,Envoy 并不会立刻被动断开和 Client 端的链接。此时从 Client 的角度来看,到 172.16.0.198 的 TCP 链接仍然是失常的,因而 Client 会持续应用该链接发送 HTTP 申请。同时因为 Cluster 类型为“ORIGINAL_DST”,Envoy 会持续尝试连贯 Client 申请中的原始目标地址 172.16.0.198,如图中蓝色箭头所示。然而因为该 IP 上的 Pod 曾经被销毁,Envoy 会连贯失败,并在失败后向 Client 端返回一个这样的错误信息:“upstream connect error or disconnect/reset before headers. reset reason: connection failure HTTP/1.1 503”。如果 Client 在收到该谬误后不立刻断开并重建链接,那么直到该链接超时之前,Client 都不会从新查问 DNS 获取到 Pod 重建后的正确地址。

为 Headless Service 启用 EDS

从后面的剖析中咱们曾经晓得出错的起因是因为客户端 HTTP 长链接中的 IP 地址过期导致的。那么一个最间接的想法就是让 Envoy 采纳正确的 IP 地址去连贯 Upstream Host。在不批改客户端代码,不重建客户端链接的状况下,如何能力实现呢?

如果比照一个其余服务的 Cluster 配置,能够看到失常状况下,Istio 下发的配置中,Cluster 类型为 EDS(Endopoint Discovery Service),如上面的 yaml 片段所示:

 {
  "version_info": "2020-09-23T03:02:01Z/2",
  "cluster": {
   "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
   "name": "outbound|8080||http-server.default.svc.cluster.local",
   "type": "EDS",       # 一般服务采纳 EDS 服务发现,依据 LB 算法从 EDS 下发的 endpoint 中抉择一个进行连贯
   "eds_cluster_config": {
    "eds_config": {"ads": {},
     "resource_api_version": "V3"
    },
    "service_name": "outbound|8080||http-server.default.svc.cluster.local"
   },
  ...

 }

在采纳 EDS 的状况下,Envoy 会通过 EDS 获取到该 Cluster 中所有可用的 Endpoint,并依据负载平衡算法(缺省为 Round Robin)将 Downstream 发来的申请发送到不同的 Endpoint。因而只有把 Cluster 类型改为 EDS,Envoy 在转发申请时就不会再采纳申请中谬误的原始 IP 地址,而会采纳 EDS 主动发现到的 Endpoint 地址。采纳 EDS 的状况下,本例的中的拜访流程如下图所示:

通过查阅 Istio 源码,能够发现 Istio 对于 Headless Service 缺省采纳了 “ORIGINAL_DST” 类型的 Cluster,但咱们也能够通过设置一个 Istiod 的环境变量 PILOT_ENABLE_EDS_FOR_HEADLESS_SERVICES 为 Headless Service 强制启用 EDS。

func convertResolution(proxy *model.Proxy, service *model.Service) cluster.Cluster_DiscoveryType {
    switch service.Resolution {
    case model.ClientSideLB:
        return cluster.Cluster_EDS
    case model.DNSLB:
        return cluster.Cluster_STRICT_DNS
    case model.Passthrough: // Headless Service 的取值为 model.Passthrough
        if proxy.Type == model.SidecarProxy {
            // 对于 Sidecar Proxy,如果 PILOT_ENABLE_EDS_FOR_HEADLESS_SERVICES 的值设为 True,则启用 EDS,否则采纳 ORIGINAL_DST
            if service.Attributes.ServiceRegistry == string(serviceregistry.Kubernetes) && features.EnableEDSForHeadless {return cluster.Cluster_EDS}

            return cluster.Cluster_ORIGINAL_DST
        }
        return cluster.Cluster_EDS
    default:
        return cluster.Cluster_EDS
    }
}

在将 Istiod 环境变量 PILOT_ENABLE_EDS_FOR_HEADLESS_SERVICES 设置为 true 后,再查看 Envoy 的日志,能够看到尽管申请原始 IP 地址还是 172.16.0.198,但 Envoy 曾经把申请散发到了理论可用的三个 Server 的 IP 上。

[2020-09-24T13:35:28.790Z] "PUT /eureka/apps/EUREKA-TEST-CLIENT/eureka-client-544b94f967-gcx2f:eureka-test-client?status=UP&lastDirtyTimestamp=1600954114925 HTTP/1.1" 200 - "-" "-" 0 0 4 4 "-" "Java-EurekaClient/v1.9.13" "d98fd3ab-778d-42d4-a361-d27c2491eff0" "eureka-server:8761" "172.16.1.3:8761" outbound|8761||eureka-server.eureka.svc.cluster.local 172.16.0.169:39934 172.16.0.198:8761 172.16.0.169:53890 - default
[2020-09-24T13:35:58.797Z] "PUT /eureka/apps/EUREKA-TEST-CLIENT/eureka-client-544b94f967-gcx2f:eureka-test-client?status=UP&lastDirtyTimestamp=1600954114925 HTTP/1.1" 200 - "-" "-" 0 0 1 1 "-" "Java-EurekaClient/v1.9.13" "7799a9a0-06a6-44bc-99f1-a928d8576b7c" "eureka-server:8761" "172.16.0.59:8761" outbound|8761||eureka-server.eureka.svc.cluster.local 172.16.0.169:45582 172.16.0.198:8761 172.16.0.169:53890 - default
[2020-09-24T13:36:28.801Z] "PUT /eureka/apps/EUREKA-TEST-CLIENT/eureka-client-544b94f967-gcx2f:eureka-test-client?status=UP&lastDirtyTimestamp=1600954114925 HTTP/1.1" 200 - "-" "-" 0 0 2 1 "-" "Java-EurekaClient/v1.9.13" "aefb383f-a86d-4c96-845c-99d6927c722e" "eureka-server:8761" "172.16.0.200:8761" outbound|8761||eureka-server.eureka.svc.cluster.local 172.16.0.169:60794 172.16.0.198:8761 172.16.0.169:53890 - default

神秘隐没的服务

在将 Eureka Server Cluster 的类型从 ORIGINAL_DST 改为 EDS 之后,之前心跳失败的服务失常了。但过了一段时间后,发现原来 Eureka 中注册的局部服务下线,导致服务之间无奈失常拜访。查问 Eureka Server 的日志,发现日志中有如下的谬误:

2020-09-24 14:07:35.511  WARN 6 --- [eureka-server-3] c.netflix.eureka.cluster.PeerEurekaNode  : EUREKA-SERVER-2/eureka-server-2.eureka-server.eureka.svc.cluster.local:eureka-server-2:8761:Heartbeat@eureka-server-0.eureka-server: missing entry.
2020-09-24 14:07:35.511  WARN 6 --- [eureka-server-3] c.netflix.eureka.cluster.PeerEurekaNode  : EUREKA-SERVER-2/eureka-server-2.eureka-server.eureka.svc.cluster.local:eureka-server-2:8761:Heartbeat@eureka-server-0.eureka-server: cannot find instance

从日志中咱们能够看到多个 Eureka Server 之间的数据同步产生了谬误。当部署为集群模式时,Eureka 集群中的多个实例之间会进行数据同步,本例中的 Eureka 集群中有三个实例,这些实例之间的数据同步如下图所示:

当改用 EDS 之后,当集群中的每一个 Eureka Server 向集群中的其余 Eureka Server 发动数据同步时,这些申请被申请方 Pod 中的 Envoy Sidecar 采纳 Round Robin 进行了随机散发,导致同步音讯产生了错乱,集群中每个服务器中的服务注册音讯不统一,导致某些服务被误判下线。该故障景象比拟随机,通过屡次测试,咱们发现在 Eureka 中注册的服务较多时更容易呈现改故障,当只有大量服务时不容易复现。

找到起因后,要解决该问题就很简略了,咱们能够通过将 Eureka Server 的 Sidecar Injection 设置为 false 来躲避该问题,如上面的 yaml 片段所示:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: eureka-server
spec:
  selector:
    matchLabels:
      app: eureka-server
  serviceName: "eureka-server"
  replicas: 3
  template:
    metadata:
      labels:
        app: eureka-server
      annotations:
        sidecar.istio.io/inject: "false"  # 不为 eureka-server pod 注入 Envoy Siedecar
    spec:
      containers:
      - name: eureka-server
        image: zhaohuabing/eureka-test-service:latest
        ports:
        - containerPort: 8761
          name: http

反思

对于 Headless Service,Istio 缺省采纳“ORIGINAL_DST”类型的 Cluster,要求 Envoy Sidecar 在转发时采纳申请原始目标 IP 地址的行为其实是正当的。如同咱们在本系列的上一篇文章『Istio 运维实战系列(2):让人头大的『无头服务』- 上』所介绍的,Headless Service 个别用于定义有状态的服务。对于有状态的服务,须要由客户端依据利用特定的算法来自行决定拜访哪一个后端 Pod,因而不应该在这些 Pod 前加一个负载均衡器。

在本例中,因为 Eureka 集群中各个节点之间会对收到的客户端服务心跳告诉进行同步,因而对于客户端来说,将心跳告诉发送到哪一个 Eureka 节点并不重要,咱们能够认为 Eureka 集群对于内部客户端而言是无状态的。因而设置 PILOT_ENABLE_EDS_FOR_HEADLESS_SERVICES 环境变量,在客户端的 Envoy Sidecar 中对客户端发往 Eureka Server 的申请进行负载平衡是没有问题的。然而因为 Eureka 集群外部的各个节点之间的是有状态的,批改后影响了集群中各个 Eureka 节点之间的数据同步,导致了前面局部服务谬误下线的问题。对于引发的该问题,咱们通过去掉 Eureka Server 的 Sidecar 注入来进行了躲避。

对于该问题,更正当的解决办法是 Envoy Sidecar 在尝试连贯 Upstream Host 失败肯定次数后被动断开和客户端侧的链接,由客户端从新查问 DNS,获取正确的 Pod IP 来创立新的链接。通过测试验证,Istio 1.6 及之后的版本中,Envoy 在 Upstream 链接断开后会被动断开和 Downstream 的长链接,倡议尽快降级到 1.6 版本,以彻底解决本问题。也能够间接采纳腾讯云上的云原生 Service Mesh 服务 TCM(Tencent Cloud Mesh),为微服务利用疾速引入 Service Mesh 的流量治理和服务治理能力,而无需再关注 Service Mesh 基础设施本身的装置、保护、降级等事项。

参考文档

  • All about ISTIO-PROXY 5xx Issues
  • Service Discovery: Eureka Server
  • Istio 运维实战系列(2):让人头大的『无头服务』- 上
  • Eureka 心跳告诉问题测试源码

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

正文完
 0