从爬虫被 502 的小场面初窥 Istio 出站流量管理

1,186 阅读5分钟

最近笔者正努力将一个基于 Puppeteer 的小型数据爬虫从 docker-compose 部署的单机环境迁移到自建的微型混合云(k3s 集群)。出于学(lie)习(qi)的心态,集群上部署了 Istio 服务网格,结果 Ingress 配置的 API 接口一切正常,唯独数据爬取任务全部失败,脑壳开始微微发痛起来。

第一阶段 蒙圈

一顿查 log 发现是目标站点返回 502,而且是 100% 返回,不是偶发现象

由于这个界面是目标站点的 WAF 高防返回的,通常是被认为是垃圾流量或者配置有误导致被 ban IP 后才能见到,于是我的第一反应是被 ban 了,于是先后在 Pod 所在的 Node 上使用 curl 和 headless Chrome 请求了相同URL。

神奇的事情发生了,结果一切正常。

那么基本确定了就是 k3s Pod 内的环境有问题,考虑到虽然以前也是在 Docker 容器内运行的,但毕竟是传统的 Docker Network,现在的环境是 k3s 的 Containerd + Flannel,于是我决定以裸 Pod 形式,添加 label istio-injection=disabled 关闭网格的 Sidecar 注入。

更神奇的事情发生了,结果表现正常。

问题基本锁定在 Istio 的 Sidecar 注入后,容器内的就不能正常访问到该爬虫的目标站点,为了验证这一点,我们再创建两个裸 Pod 跑起 busybox

kubectl run curl-test-with-istio --image=radial/busyboxplus:curl -i --tty --rm --overrides='{"spec": { "nodeSelector": {"k3s.io/hostname": "homelab"}}}' # namespace default 默认注入sidecar,运行在 hostname 为 homelab 的节点上
kubectl run curl-test-without-istio --image=radial/busyboxplus:curl -i --tty --rm --overrides='{"metadata": {"label": {"istio-injection": "disabled"}}, "spec": { "nodeSelector": {"k3s.io/hostname": "homelab"}}}' # 用 label 确保sidecar注入已关闭,运行在 hostname 为 homelab 的节点上

在这个两个裸 Pod 中运行 curl 目标站点,发现确实是 istio 造成了这一问题。

第二阶段 寻根

从 Istio 的官方文档( istio.io/latest/docs… )中,我们知道,注入 Sidecar 后的 Pod 对外请求,都是由 Envoy 转发的,可以通过下面的命令查询转发的配置。

kubectl get istiooperator installed-state -n istio-system -o jsonpath='{.spec.meshConfig.outboundTrafficPolicy.mode}'

获取到的 mode 可能是 ALLOW_ANY 或者 REGISTRY_ONLY,ALLOW_ANY (默认)代表不做管控直接转发,REGISTRY_ONLY 代表需要强控不允许直接访问外网。这一模式可以在通过 istioctl 配置:

istioctl install -f config.yaml --set meshConfig.outboundTrafficPolicy.mode=REGISTRY_ONLY

其中 -f config.yaml 是安装是使用的参数。当 mode=REGISTRY_ONLY 时,需要配置 ServiceEntry 来容许对外链接,如下:

kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: httpbin-ext
spec:
  hosts:
  - httpbin.org
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: DNS
  location: MESH_EXTERNAL
EOF

上面的 ServiceEntry 配置了允许对 httpbin.org 的访问,可以通过对 port、protocol、resolution、location 等字段的配置分别实现对各种地址的容许规则。

说回问题,我安装网格的配置档中的 meshConfig.outboundTrafficPolicy.mode 为 ALLOW_ANY,按道理不会阻拦对外连接,而且目前的效果其实是连接出得去,只是转发似乎加了私货导致请求异常,所以上面的容许规则对于控制集群容器的流量出口很有必要,但并不是问题的元凶。

还是使用刚才的两个 curl 裸 Pod,请求 httpbin.org 的服务:

curl http://httpbin.org/headers
curl http://httpbin.org/user-agent
curl http://httpbin.org/ip

可以发现, Istio 的 Envoy 确实在请求头里面加了私货,包括 Trace 的头和 Envoy 的相关信息,但是我们在裸机上直接用相同的头访问目标站点,并未发现 502 的情况,可见这也不是问题的主因。

万念俱灰之后,还是掏出抓包大法:

sudo tcpdump tcp port 80 and host www.****.com -w dump.pcap

分别用istio内和裸机请求目标站点,抓到了两次请求包

直接对比没发现更多信息,直到我尝试将两次请求复制成 curl request进行复现,终于发现了一些端倪

再加上 data-bianry 和 content-length 头之后,果然在裸机上复现了 502 问题,至此基本确认,应当是目标站点较为老旧/WAF规则比较严格,拦截了/不能处理内容显式声明为空的请求,而 Envoy 转发后的请求正是这样的。

第三阶段 把坑填上

既然知道了问题,解决也就有方法了:

  1. 给 Istio 社区提个 Issue/PR,增加出站透明转发能力(入站透明转发因为被用于获取 SourceIP,在2018年底已被增加 istio.io/latest/docs…
  2. 按照前面文档中的说法,配置 includeIPRanges,仅集群内的出流量被转发,对外的部分绕过代理
  3. 尝试 Istio Egress Gateway 集中转发请求

考虑到这一情况较为特殊,社区没发现类似问题和诉求(也可能我英语太烂没发现),Istio 的代码我也还在熟悉,1 的选择是个长期工程,慢慢来。

我的集群节点资源都比较进展,也有多出口的诉求,Gateway 方案成本较高,而且考虑到底层也是 Envoy,对解决问题帮助不大。

最后就是 2.Bypass,还是可以通过 istioctl 完成全局配置:

istioctl install -f config.yaml --set values.global.proxy.includeIPRanges="10.42.0.0/24"

不过目前只有爬虫遇到这一问题,仅对单个 Deployment 下的 Pod 生效这一 Bypass 策略就好,可以通过配置注解完成:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
      annotations:
        traffic.sidecar.istio.io/includeOutboundIPRanges: "10.42.0.1/24" # bypass Istio Egress
    spec:
      ...

至此,爬虫的访问问题已解决。Istio 提供的出站流量管理可以很好的完成链路追踪、路由、安全管控等等特性,不过也增加了链路的长度和不兼容风险,有利有弊,还是要按需使用为宜~

以上就是一个弱小前端云原生实践的一点笔记,可能存在不妥之处,欢迎指出!大佬轻拍(doge