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

Istio 进阶 — 为什么会那样流

读懂注入结果中的数字,搭建两个入口

在 TT Lab 中继续学习

目标

离线生成 sidecar 注入产物,找到写有拦截号码的位置,并亲手搭建来确认这些号码会成为 Envoy 的哪个监听器和集群。

为什么重要

sidecar 行为异常时,最先看的就是注入产物。只要有一个号码对不上,代理就会把自己的请求又接收回来(UID),健康检查会走上路由(-d),外部调用会全部变成 503(BlackHoleCluster)。会读这张号码牌,仅凭症状就能区分这三种情况。

步骤

  1. 创建 /root/ist2-inject,并在其中用 istioctl kube-inject 给 /opt/lab/fixtures/istio/inject-target.yaml 注入 sidecar,保存为 /root/ist2-inject/inject.yaml(要把三个注入配置文件 /opt/istio/inject-config.yaml、mesh-config.yaml、values-config.yaml 全部传入)。然后在 /root/ist2-inject/01-containers.txt 中写入三行——containers=(注入后的容器名称,按顺序用逗号分隔)、init=(初始化容器名称)、proxy_image=(istio-proxy 容器的镜像)。
  2. 读取 /root/ist2-inject/inject.yaml 中 istio-init 容器的 args,在 /root/ist2-inject/02-redirect.txt 中写入五行——outbound_port=(-p 的值)、inbound_port=(-z)、proxy_uid=(-u)、mode=(-m)、excluded_inbound_ports=(-d,保持用逗号连接的原样)。
  3. 在 /root/ist2-inject/inject.yaml 中找到三个端口,写入 /root/ist2-inject/03-ports.txt——readiness_port=(istio-proxy 的 readinessProbe.httpGet.port)、merged_metrics_port=(Pod 模板注解 prometheus.io/port)、envoy_prom_port=(istio-proxy 的端口中名称为 http-envoy-prom 的那个)。最后一行 all_excluded=,如果三个端口都在 istio-init 的 -d 列表中则写 yes,否则写 no。
  4. 读取 /root/ist2-inject/inject.yaml 的 securityContext,在 /root/ist2-inject/04-uid.txt 中写入五行——proxy_uid=(istio-proxy 的 runAsUser)、proxy_run_as_non_root=(同一个容器的 runAsNonRoot)、proxy_caps_drop=(capabilities.drop,用逗号分隔)、init_run_as_user=(istio-init 的 runAsUser)、init_caps_add=(istio-init 的 capabilities.add,用逗号分隔,按清单中的顺序)。
  5. 在 /root/ist2-inject/mesh.yaml 中编写 Envoy 配置——管理端口 9981,名为 virtualInbound 的监听器在 127.0.0.1:15006 上以 traffic_direction: INBOUND 监听,把所有路径发往集群 inbound|8101||(扮演应用,127.0.0.1:8101)。在 8101 上以 ok 启动上游并启动 Envoy 之后,把 curl localhost:15006/orders 的结果以 status=(HTTP 状态码)和 body=(响应正文的一行)两行写入 /root/ist2-inject/05-inbound.txt。
  6. 在 /root/ist2-inject/mesh.yaml 中再添加第二个监听器(前面的 virtualInbound 保持不变)——名称 virtualOutbound,127.0.0.1:10081,traffic_direction: OUTBOUND,把所有路径发往集群 BlackHoleCluster。BlackHoleCluster 是 type: STATIC 且一个端点都没有的集群。重新启动之后,把 curl localhost:10081/ 的结果以 status=、body=、cluster=(该监听器的路由所指向的集群名称)三行写入 /root/ist2-inject/06-outbound.txt。
  7. 从正在运行的 Envoy 的 localhost:9981/config_dump?resource=static_listeners 中,为每个监听器提取一行 이름 방향 포트(占位符依次为名称、方向、端口,用空格分隔),保存到 /root/ist2-inject/07-listeners.txt(两行,顺序无关)。
  8. 在 /root/ist2-inject/08-report.md 中写入 outbound_capture=、inbound_capture=、proxy_uid=、unknown_destination= 四行(依次为转入出站连接的端口、入站端口、被排除在拦截之外的用户 ID、第 6 步中目的地未知的请求所去往的集群名称),并在下面至少写四行以 - 开头的说明。

参考

生成注入产物,统计增加了什么

创建 /root/ist2-inject,并在其中用 istioctl kube-inject 给 /opt/lab/fixtures/istio/inject-target.yaml 注入 sidecar,保存为 /root/ist2-inject/inject.yaml(要把三个注入配置文件 /opt/istio/inject-config.yaml、mesh-config.yaml、values-config.yaml 全部传入)。然后在 /root/ist2-inject/01-containers.txt 中写入三行——containers=(注入后的容器名称,按顺序用逗号分隔)、init=(初始化容器名称)、proxy_image=(istio-proxy 容器的镜像)。

这个 Pod 中没有 istiod,所以 kube-inject 无法像平常那样从集群获取注入配置。必须把三个配置文件全部传入,才不会向外查找——只给两个的话,它会去找剩下的那一个。产物末尾会附带空文档,所以用 yq 读取时请用 select(.kind=="Deployment") 过滤。值不要用眼睛抄,用 yq 提取再写下来才不会出错。

从 istio-init 的参数中读取拦截设计

读取 /root/ist2-inject/inject.yaml 中 istio-init 容器的 args,在 /root/ist2-inject/02-redirect.txt 中写入五行——outbound_port=(-p 的值)、inbound_port=(-z)、proxy_uid=(-u)、mode=(-m)、excluded_inbound_ports=(-d,保持用逗号连接的原样)。

istio-iptables 的每个参数都对应一块规则。-p 是要把应用向外发送的连接转入的端口,-z 是要把从外部进入的连接转入的端口,-u 是不被拦截的用户 ID。args 是字符串列表,所以紧跟在标志之后的元素就是值——用 awk 提取“看到标志之后的下一行”即可。像 -x 这样值为空字符串的标志也存在,所以不要按顺序去数。

找出被排除在拦截之外的端口是为谁准备的

在 /root/ist2-inject/inject.yaml 中找到三个端口,写入 /root/ist2-inject/03-ports.txt——readiness_port=(istio-proxy 的 readinessProbe.httpGet.port)、merged_metrics_port=(Pod 模板注解 prometheus.io/port)、envoy_prom_port=(istio-proxy 的端口中名称为 http-envoy-prom 的那个)。最后一行 all_excluded=,如果三个端口都在 istio-init 的 -d 列表中则写 yes,否则写 no。

前面说过,入站连接会全部转向 15006,但如果连 kubelet 的健康检查和 Prometheus 的抓取也被转向,就会经过 Envoy 的路由规则而跑到莫名其妙的地方。所以代理必须直接接收的端口,要用 -d 从拦截中排除。注解名称中有点和斜杠,所以请像 .metadata.annotations["prometheus.io/port"] 这样用方括号读取。

查看 UID 1337 与权限分散在哪里

读取 /root/ist2-inject/inject.yaml 的 securityContext,在 /root/ist2-inject/04-uid.txt 中写入五行——proxy_uid=(istio-proxy 的 runAsUser)、proxy_run_as_non_root=(同一个容器的 runAsNonRoot)、proxy_caps_drop=(capabilities.drop,用逗号分隔)、init_run_as_user=(istio-init 的 runAsUser)、init_caps_add=(istio-init 的 capabilities.add,用逗号分隔,按清单中的顺序)。

修改 iptables 规则需要 NET_ADMIN,而这个权限只交给只运行一次就结束的初始化容器。长期运行的代理既不是 root,而且丢弃了所有权限后才启动。而且代理的 UID 必须与第 2 步的 -u 值相同——不相同的话,代理发出的请求会再次被转回代理,陷入无限循环。列表请用 yq 的 join(",") 连接。

在 15006 上搭建 virtualInbound,把请求发往应用

在 /root/ist2-inject/mesh.yaml 中编写 Envoy 配置——管理端口 9981,名为 virtualInbound 的监听器在 127.0.0.1:15006 上以 traffic_direction: INBOUND 监听,把所有路径发往集群 inbound|8101||(扮演应用,127.0.0.1:8101)。在 8101 上以 ok 启动上游并启动 Envoy 之后,把 curl localhost:15006/orders 的结果以 status=(HTTP 状态码)和 body=(响应正文的一行)两行写入 /root/ist2-inject/05-inbound.txt。

sidecar 的入站入口就是这个监听器。iptables 把发往 Pod 的连接全部转向 15006,在这里根据原始目的地端口,转交给 inbound|<포트>||(占位符为端口)集群——名称中间的字段为空,是因为入站一侧没有子集。集群名称中有 |,所以在 YAML 中要用引号括起来。traffic_direction 不改变行为,但方向会留在统计和 config_dump 中。

不知道目的地的请求会去往 BlackHoleCluster

在 /root/ist2-inject/mesh.yaml 中再添加第二个监听器(前面的 virtualInbound 保持不变)——名称 virtualOutbound,127.0.0.1:10081,traffic_direction: OUTBOUND,把所有路径发往集群 BlackHoleCluster。BlackHoleCluster 是 type: STATIC 且一个端点都没有的集群。重新启动之后,把 curl localhost:10081/ 的结果以 status=、body=、cluster=(该监听器的路由所指向的集群名称)三行写入 /root/ist2-inject/06-outbound.txt。

在拦截发往网格所不知道的目的地的请求时(outboundTrafficPolicy: REGISTRY_ONLY),Istio 会把这些请求发往没有端点的集群。默认值(ALLOW_ANY)下,则会去往把请求原样放行到原目的地的 PassthroughCluster。端点为 0 个的集群,只要完全不写 load_assignment 即可。请原样抄下 Envoy 返回的正文——了解了这句话,在生产日志中就能立刻认出来。

从 config_dump 中读回两个入口的名称、方向、端口

从正在运行的 Envoy 的 localhost:9981/config_dump?resource=static_listeners 中,为每个监听器提取一行 이름 방향 포트(占位符依次为名称、方向、端口,用空格分隔),保存到 /root/ist2-inject/07-listeners.txt(两行,顺序无关)。

写在文件里的内容与 Envoy 实际读取的内容可能不同。在生产环境中,istioctl proxy-config listeners 所显示的,说到底也是这份转储。用 resource=static_listeners 缩小范围后,.configs[].listener 中每个监听器各占一项。请用 jq -r 的字符串插值把三个值连成一行。

用拦截号码牌来整理

在 /root/ist2-inject/08-report.md 中写入 outbound_capture=、inbound_capture=、proxy_uid=、unknown_destination= 四行(依次为转入出站连接的端口、入站端口、被排除在拦截之外的用户 ID、第 6 步中目的地未知的请求所去往的集群名称),并在下面至少写四行以 - 开头的说明。

号码从前面步骤的文件中转写即可——不要凭猜测填写,请重新查看 02-redirect.txt 和 06-outbound.txt。说明行中与其写“做了什么”,不如写“为什么这样设计”,这样下次遇到故障时,这张表才会有用。