Istio 1.7.0

最近 Istio 1.7.0 发布了,这是一个令人非常激动的版本,因为这个版本解决了大量我们遇到的 Bug。

更激动的是,其中几个是我提交的代码修复的。

所以想在这把这几个 Bug 过一遍,顺便也介绍一下如何 debug Istio 代码。

 

Endpoints 端口错误

第一个 Bug 比较简单,重现步骤都在这个 Issue 里了:https://github.com/istio/istio/issues/25309

大致的现象如果ServiceEntryendpoints的端口是不一样的,那么删除一个endpoints之后,最终生成配置中endpoint端口就是错的。

Istio 本质上是在做模型转换工作,把自己的 CRDs 转换成 Envoy 的模型。这里的endpoints对应的就是 Envoy 的endpointsServiceEntry对应的就是 Envoy 里的cluster

从现象结合 Istio 原理可以猜测到这里肯定是转换过程中什么地方写错了。

虽然没通读 Istio 源码,但是找到所有对应字段赋值的代码,就可以找到线索了。

果不其然发现了疑点:

for _, instance := range allInstances {
  port := instance.ServicePort
  key := makeInstanceKey(instance)
  endpoints[key] = append(endpoints[key],
    &model.IstioEndpoint{
      Address:         instance.Endpoint.Address,
      EndpointPort:    uint32(port.Port), // 有问题的代码
      ServicePortName: port.Name,
      Labels:          instance.Endpoint.Labels,
      UID:             instance.Endpoint.UID,
      ServiceAccount:  instance.Endpoint.ServiceAccount,
      Network:         instance.Endpoint.Network,
      Locality:        instance.Endpoint.Locality,
      LbWeight:        instance.Endpoint.LbWeight,
      TLSMode:         instance.Endpoint.TLSMode,
    })
}

这里明明是简单的同对象复制,其中复制端口的地方却直接忽略了endpoint的数据而使用了ServiceEntry的数据,从逻辑上看就是不对的。

但毕竟是他特意写成这样的,我也不敢说,我也不敢问,就怕是什么精妙的逻辑。

所以在 Issue 贴上了代码提出了疑惑。

万万没想到,一位印度小哥看到我的 Issue 后立刻说:“是的,这里有问题”,然后自己提交了一个 Pull Request 把这个问题修复了。

这一切发生的太快了,我都没来得及反应。这年头都这么拼的吗?修复一个 Bug 也要抢?

 

ServiceEntry 创建多了以后出现内存占用过多的问题

这个问题是我们在对 Istio 进行压测的时候出现的,我们只是简单地在 Istio 中创建了 5000 个ServiceEntry,并且每个ServiceEntry有 10 个 endpoints

Issue 在这里:https://github.com/istio/istio/issues/25531

虽然知道 Istio 会把所有的资源放内存中,但这个内存暂用情况也不符合预期。

istiod memory

这里内存至少用了 12G,换算一下 12*1024*1024/5000/10=251.65824KB。平均每个endpoint占用 250KB 内存,从直觉上就觉得这个是不对的,因为一个endpoint的信息量很少,仅仅是一个 IP 和端口而已。不应该占用那么多内存。

遇到这种问题肯定是先用pprof看看内存使用情况,最终定位到一段代码后 debug 看看具体问题在哪。

istiod debug

debug 之后果然有了惊人的发现,从这张图中可以看到这里的数据结构是 O(n^2) 的,而不是想象中的 O(n)。

首先从直觉上看这个就是不合理的,但同样是因为 Istio 源码非复杂,没有通读就没办法理解上下文,一开始一只能在 Issue 里提出自己的疑问。

一开始虽然没找到 root cause,但在这部分的逻辑里,有一个点是可以优化的,这里的 map key 是一个比较大的 Struct,而这部分逻辑是判断这个 key 对应的资源是否要推送到 Envoy。

所以这里有误判也是可以接受的,无非就是多发送一点数据,那么存到 map 里的 key 就没必要用完整的 Struct 了,只要用它的 HashCode 就行了。虽说可能冲突,但冲突的概率其实是非常小的。

这部分提交了一个 Pull Request 来优化了一下:https://github.com/istio/istio/pull/25532

优化好后,内存占用差不多下降了一个数量级,从 10G 变成了 1G,有了很大的改善。

但是这个 O(n^2) 的空间复杂度还是有点奇怪,于是继续看源码。最后终于发现了 root cause,这里本来应该是 O(n) 的,但因为一个很傻的逻辑错误导致了这个问题。

准备修复这个问题的时候,发现已经有一个 Pull Request 解决这个问题了:https://github.com/istio/istio/pull/25118

最终,这个内存占用过大的问题解决了。

 

WorkloadEntry 匹配错误

这个问题也非常的低级,WorkloadEntry是 1.6.0 出来的一种新的资源,本质上就是ServiceEntryendpoints,但是新版本中允许把它们分开定义,管理起来更方便一点。

既然分开定义了,那么就需要用labelSelector把它们关联起来,而问题就出在这个关联逻辑了。

Issue 在这里:https://github.com/istio/istio/issues/25678

现象就是如果一组ServiceEntryWorkloadEntry关联起来了,这时候我把WorkloadEntryLabel改掉,那从逻辑上它们就不应该有关联了,但实际上却依然被关联着。最可怕的是,在这种情况下,如果你把WorkloadEntry删掉后,它依然不会消失。

很明显这也是一个逻辑错误,还是要看源码来找问题。

最后仔细把它做关联那部分源码读了几遍后终于发现了逻辑上的错误。

Istio 源码中如果WorkloadEntry有新增或变更,它会把所有ServiceEntry拿出来和它匹配一边,如果匹配上了,就在内存里和ServiceEntry关联起来。

如果WorkloadEntry删了,它也会把所有ServiceEntry拿出来和它匹配一边,如果匹配上了,就在内存里和ServiceEntry取消关联。

这里的逻辑漏洞就是,如果一个ServiceEntryWorkloadEntry本来是有关联的,这时候把WorkloadEntryLabel改了,根据上面第一条逻辑,并不会有任何变更产生。

此时如果再把WorkloadEntry删了,根据第二条逻辑,Istio 也不会取消关联。

修复这个逻辑 bug 也不难,在有变更的时候,不仅仅要判断当前版本是否能和ServiceEntry匹配上,还要用老版本的WorkloadEntry匹配一下。如果老版本匹配上了,新版本没匹配上,那么就应该把它从内存里删除。

这次我学乖了,没有先把自己的想法写在 Issue 里,赶紧先自己提了一个 Pull Request 把这个 bug 修复了:https://github.com/istio/istio/pull/26008

 

Debug Istio

最终,这三个问题都在 1.7.0 里得以修复,在这个过程中,debug Istio 对理解代码的帮助还是非常大的。

Istio 是 golang,所以用 delve 就可以 debug 了。但在编译过程中需要修改一些编译参数。

翻了一下 Istio Makefile 后找到了相关的配置参数:

ifeq ($(origin DEBUG), undefined)
  BUILDTYPE_DIR:=release
else ifeq ($(DEBUG),0)
  BUILDTYPE_DIR:=release
else
  BUILDTYPE_DIR:=debug
  export GCFLAGS:=all=-N -l
  $(info $(H) Build with debugger information)
endif

只要把 Istio 源码下载下来,启动 Docker 后运行:DEBUG=1 make docker

最终就可以在docker images中看到 build 好的镜像了:

❯ docker images
REPOSITORY                         TAG                                        IMAGE ID            CREATED              SIZE
istio/operator                     5bf4ade4e4ba40977a1bcacc07740631fb56a099   adfc97ecf9cd        27 seconds ago       247MB
istio/istioctl                     5bf4ade4e4ba40977a1bcacc07740631fb56a099   0e72b708e7b7        32 seconds ago       271MB
istio/mixer_codegen                5bf4ade4e4ba40977a1bcacc07740631fb56a099   f38a5fd51b73        36 seconds ago       176MB
istio/mixer                        5bf4ade4e4ba40977a1bcacc07740631fb56a099   f2d6bef16640        37 seconds ago       270MB
istio/test_policybackend           5bf4ade4e4ba40977a1bcacc07740631fb56a099   f3f44529950d        41 seconds ago       174MB
istio/app_sidecar                  5bf4ade4e4ba40977a1bcacc07740631fb56a099   2ba09d67f362        45 seconds ago       473MB
istio/app                          5bf4ade4e4ba40977a1bcacc07740631fb56a099   145d905dbf55        About a minute ago   187MB
istio/proxyv2                      5bf4ade4e4ba40977a1bcacc07740631fb56a099   21597c7cc8e9        About a minute ago   330MB

这一步完成后需要导出这个镜像,可以传到 dockerhub,然后安装 Istio 的时候修改一下 pilot 模块的 image 地址即可。

Istio 运行起来后,第一步先进去容器跑dlv,本地先编译一个 Linux 版本的dlv,然后想办法在容器内运行:

kubectl cp dlv istiod-xxxxxx-xxxx:/tmp/dlv # 复制 dlv 工具到 istiod 容器内
kubectl exec -it istiod-xxxxxx-xxxx /bin/bash # 进入 istiod 容器内
dlv --listen=:2345 --headless=true --api-version=2 attach 1 # 运行 dlv

这一步成功后,再起一个 Tab,利用kubectl port-forward pod/istiod-xxxxxx-xxxx 2345:2345把端口暴露出来。

最后一步就简单了,不管你用什么 IDE,只要支持 delve 就可以,连上本地的 2345 端口就可以 debug 了。

本作品由 Dozer 创作,采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。