TT Lab
开始
学习 学习路径 课程

Istio 进阶 — 为什么会那样流

应用毫不知情,所有连接却都经过代理的原因

在 TT Lab 中继续学习

一句话总结

sidecar 注入会给 Pod 增加两个容器。只运行一次就结束的 istio-init 在 iptables 中埋下号码牌(15001、15006、1337),长期运行的 istio-proxy 则在这些号码上等候。应用什么都没有改动,所有连接却都经过代理,原因就在这些号码牌。

为什么需要它

服务网格的承诺是“不修改代码,就给所有调用加上 mTLS、重试和可观测性”。要做到这一点,应用以 http://reviews:9080 发出的连接,必须在应用不知情的情况下被代理接住。植入库的方式要为每种语言分别开发,而且应用必须主动使用这个库。所以 Istio 选择在内核一侧拦截——在 Pod 的网络命名空间中加入 iptables 规则,把出站连接转向 15001,把入站连接转向 15006。

这里马上就产生两个问题。第一,代理接收的请求如果再次发往外部,这个连接也会命中规则,被重新转回 15001,成为无限循环。第二,修改规则需要 NET_ADMIN 权限,如果把这个权限交给常驻的代理,代理一旦被攻破,就能整体改变 Pod 的网络。注入产物的形态,就是对这两个问题的回答。

工作原理

打开 istioctl kube-inject 生成的清单,会发现号码分散在各处。汇总起来是这样的。

号码 写在哪里 含义
15001 istio-init -p 应用向外发送的连接被转入的地方(virtualOutbound)
15006 istio-init -z 从外部进入的连接被转入的地方(virtualInbound)
1337 istio-init -u、istio-proxy runAsUser 这个用户发出的数据包不会被拦截
15021 readinessProbe 代理自身的就绪状态
15020 prometheus.io/port 注解 合并应用与代理指标后导出的地方
15090 容器端口 http-envoy-prom Envoy 自身的指标

循环问题通过 -u 1337 解决。让代理容器以 UID 1337 启动,并把这个 UID 发出的数据包排除在规则之外。如果两处的号码对不上,代理就会把自己的请求又接收回来,白白消耗 CPU。权限问题通过拆分容器来解决。istio-init 以 root 身份持有 NET_ADMIN、NET_RAW,只负责埋下规则就结束;istio-proxy 不是 root,丢弃了所有权限,文件系统也是只读的。-d 15090,15021,15020 是要从入站拦截中排除的端口。因为 kubelet 的健康检查和 Prometheus 的抓取不能经过 Envoy 的路由。

从 Envoy 一侧来看,15006 是 virtualInbound 监听器,它会根据原始目的地端口,转交给 inbound|<포트>||(占位符为端口)集群。15001 是 virtualOutbound,如果是网格所知道的目的地,就发给该服务的监听器;不知道的,就发给 PassthroughCluster(直接放行)或 BlackHoleCluster(没有端点,返回 503)。具体是哪一种,由网格配置中的 outboundTrafficPolicy 决定。

在现场相遇的样子

注入之后,应用还没启动好就发送请求而失败。如果应用比代理先启动,并向外发起连接,规则虽然已经存在,但 15001 上没有接收方。这就是存在 holdApplicationUntilProxyStarts 的原因。查看注入产物中容器的顺序,就能知道这个配置是如何生效的。

“只有外部 API 返回 503。”如果日志中打印了 BlackHoleCluster,就说明网格是 REGISTRY_ONLY,并且没有告知该主机的 ServiceEntry。响应正文 no healthy upstream 是 sidecar 生成的,而不是应用。

应用以 1337 启动。如果应用容器碰巧以 UID 1337 启动,应用的连接也会被排除在拦截之外,不经过 mTLS,以明文发出。不会出现任何错误,直到安全检查时才会暴露。

官方文档:Application requirements · Debugging Envoy and Istiod

下一项实验要做什么

离线生成真实的注入产物,用 yq 逐一提取写有号码的位置。然后亲手搭建 virtualInbound 和 virtualOutbound,亲眼看到发往应用的请求,以及落入 BlackHoleCluster 的请求。