## 深入理解Istio Service Mesh中的Envoy Sidecar注入與流量劫持
**注意:本書中的 Service Mesh 章節已不再維護,請轉到 [istio-handbook](https://jimmysong.io/istio-handbook) 中瀏覽。**
在講解 Istio 如何將 Envoy 代理注入到應用程序 Pod 中之前,我們需要先了解以下幾個概念:
- Sidecar 模式:容器應用模式之一,Service Mesh 架構的一種實現方式。
- Init 容器:Pod 中的一種專用的容器,在應用程序容器啟動之前運行,用來包含一些應用鏡像中不存在的實用工具或安裝腳本。
- iptables:流量劫持是通過 iptables 轉發實現的。
查看目前 `productpage-v1-745ffc55b7-2l2lw` Pod 中運行的容器:
```bash
$ kubectl -n default get pod productpage-v1-745ffc55b7-2l2lw -o=jsonpath='{..spec.containers[*].name}'
productpage istio-proxy
```
`productpage` 即應用容器,`istio-proxy` 即 Envoy 代理的 sidecar 容器。另外該 Pod 中實際上還運行過一個 Init 容器,因為它執行結束就自動終止了,所以我們看不到該容器的存在。關注 `jsonpath` 的用法請參考 [JSONPath Support](https://kubernetes.io/docs/reference/kubectl/jsonpath/)。
## Sidecar 模式
在了解 Istio 使用 Sidecar 注入之前,需要先說明下什么是 Sidecar 模式。Sidecar 是容器應用模式的一種,也是在 Service Mesh 中發揚光大的一種模式,詳見 [Service Mesh 架構解析](http://www.servicemesher.com/blog/service-mesh-architectures/),其中詳細描述了**節點代理**和 **Sidecar** 模式的 Service Mesh 架構。
使用 Sidecar 模式部署服務網格時,無需在節點上運行代理(因此您不需要基礎結構的協作),但是集群中將運行多個相同的 Sidecar 副本。從另一個角度看:我可以為一組微服務部署到一個服務網格中,你也可以部署一個有特定實現的服務網格。在 Sidecar 部署方式中,你會為每個應用的容器部署一個伴生容器。Sidecar 接管進出應用容器的所有流量。在 Kubernetes 的 Pod 中,在原有的應用容器旁邊運行一個 Sidecar 容器,可以理解為兩個容器共享存儲、網絡等資源,可以廣義的將這個注入了 Sidecar 容器的 Pod 理解為一臺主機,兩個容器共享主機資源。
例如下圖 [SOFAMesh & SOFA MOSN—基于Istio構建的用于應對大規模流量的Service Mesh解決方案](https://jimmysong.io/posts/sofamesh-and-mosn-proxy-sidecar-service-mesh-by-ant-financial/)的架構圖中描述的,MOSN 作為 Sidecar 的方式和應用運行在同一個 Pod 中,攔截所有進出應用容器的流量,[SOFAMesh](https://github.com/alipay/sofa-mesh) 兼容 Istio,其中使用 Go 語言開發的 [SOFAMosn](https://github.com/alipay/sofa-mosn) 替換了 Envoy。

**注意**:下文中所指的 Sidecar 都是指的 Envoy 代理容器。
## Init 容器
Init 容器是一種專用容器,它在應用程序容器啟動之前運行,用來包含一些應用鏡像中不存在的實用工具或安裝腳本。
一個 Pod 中可以指定多個 Init 容器,如果指定了多個,那么 Init 容器將會按順序依次運行。只有當前面的 Init 容器必須運行成功后,才可以運行下一個 Init 容器。當所有的 Init 容器運行完成后,Kubernetes 才初始化 Pod 和運行應用容器。
Init 容器使用 Linux Namespace,所以相對應用程序容器來說具有不同的文件系統視圖。因此,它們能夠具有訪問 Secret 的權限,而應用程序容器則不能。
在 Pod 啟動過程中,Init 容器會按順序在網絡和數據卷初始化之后啟動。每個容器必須在下一個容器啟動之前成功退出。如果由于運行時或失敗退出,將導致容器啟動失敗,它會根據 Pod 的 `restartPolicy` 指定的策略進行重試。然而,如果 Pod 的 `restartPolicy` 設置為 Always,Init 容器失敗時會使用 `RestartPolicy` 策略。
在所有的 Init 容器沒有成功之前,Pod 將不會變成 `Ready` 狀態。Init 容器的端口將不會在 Service 中進行聚集。 正在初始化中的 Pod 處于 `Pending` 狀態,但應該會將 `Initializing` 狀態設置為 true。Init 容器運行完成以后就會自動終止。
關于 Init 容器的詳細信息請參考 [Init 容器 - Kubernetes 中文指南/云原生應用架構實踐手冊](https://jimmysong.io/kubernetes-handbook/concepts/init-containers.html)。
## Sidecar 注入示例分析
我們看下 Istio 官方示例 `bookinfo` 中 `productpage` 的 YAML 配置,關于 `bookinfo` 應用的詳細 YAML 配置請參考 [bookinfo.yaml](https://github.com/rootsongjc/kubernetes-vagrant-centos-cluster/blob/master/yaml/istio-bookinfo/bookinfo.yaml)。
```yaml
apiVersion: v1
kind: Service
metadata:
name: productpage
labels:
app: productpage
spec:
ports:
- port: 9080
name: http
selector:
app: productpage
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: productpage-v1
spec:
replicas: 1
template:
metadata:
labels:
app: productpage
version: v1
spec:
containers:
- name: productpage
image: istio/examples-bookinfo-productpage-v1:1.8.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 9080
```
再查看下 `productpage` 容器的 [Dockerfile](https://github.com/istio/istio/blob/master/samples/bookinfo/src/productpage/Dockerfile)。
```docker
FROM python:2.7-slim
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY productpage.py /opt/microservices/
COPY templates /opt/microservices/templates
COPY requirements.txt /opt/microservices/
EXPOSE 9080
WORKDIR /opt/microservices
CMD python productpage.py 9080
```
我們看到 `Dockerfile` 中沒有配置 `ENTRYPOINT`,所以 `CMD` 的配置 `python productpage.py 9080` 將作為默認的 `ENTRYPOINT`,記住這一點,再看下注入 sidecar 之后的配置。
```bash
$ istioctl kube-inject -f yaml/istio-bookinfo/bookinfo.yaml
```
我們只截取其中與 `productpage` 相關的 `Service` 和 `Deployment` 配置部分。
```yaml
apiVersion: v1
kind: Service
metadata:
name: productpage
labels:
app: productpage
spec:
ports:
- port: 9080
name: http
selector:
app: productpage
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
creationTimestamp: null
name: productpage-v1
spec:
replicas: 1
strategy: {}
template:
metadata:
annotations:
sidecar.istio.io/status: '{"version":"fde14299e2ae804b95be08e0f2d171d466f47983391c00519bbf01392d9ad6bb","initContainers":["istio-init"],"containers":["istio-proxy"],"volumes":["istio-envoy","istio-certs"],"imagePullSecrets":null}'
creationTimestamp: null
labels:
app: productpage
version: v1
spec:
containers:
- image: istio/examples-bookinfo-productpage-v1:1.8.0
imagePullPolicy: IfNotPresent
name: productpage
ports:
- containerPort: 9080
resources: {}
- args:
- proxy
- sidecar
- --configPath
- /etc/istio/proxy
- --binaryPath
- /usr/local/bin/envoy
- --serviceCluster
- productpage
- --drainDuration
- 45s
- --parentShutdownDuration
- 1m0s
- --discoveryAddress
- istio-pilot.istio-system:15007
- --discoveryRefreshDelay
- 1s
- --zipkinAddress
- zipkin.istio-system:9411
- --connectTimeout
- 10s
- --statsdUdpAddress
- istio-statsd-prom-bridge.istio-system:9125
- --proxyAdminPort
- "15000"
- --controlPlaneAuthPolicy
- NONE
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: INSTANCE_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: ISTIO_META_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: ISTIO_META_INTERCEPTION_MODE
value: REDIRECT
image: jimmysong/istio-release-proxyv2:1.0.0
imagePullPolicy: IfNotPresent
name: istio-proxy
resources:
requests:
cpu: 10m
securityContext:
privileged: false
readOnlyRootFilesystem: true
runAsUser: 1337
volumeMounts:
- mountPath: /etc/istio/proxy
name: istio-envoy
- mountPath: /etc/certs/
name: istio-certs
readOnly: true
initContainers:
- args:
- -p
- "15001"
- -u
- "1337"
- -m
- REDIRECT
- -i
- '*'
- -x
- ""
- -b
- 9080,
- -d
- ""
image: jimmysong/istio-release-proxy_init:1.0.0
imagePullPolicy: IfNotPresent
name: istio-init
resources: {}
securityContext:
capabilities:
add:
- NET_ADMIN
privileged: true
volumes:
- emptyDir:
medium: Memory
name: istio-envoy
- name: istio-certs
secret:
optional: true
secretName: istio.default
status: {}
```
我們看到 Service 的配置沒有變化,所有的變化都在 `Deployment` 里,Istio 給應用 Pod 注入的配置主要包括:
- Init 容器 `istio-init`:用于給 Sidecar 容器即 Envoy 代理做初始化,設置 iptables 端口轉發
- Envoy sidecar 容器 `istio-proxy`:運行 Envoy 代理
接下來將分別解析下這兩個容器。
### Init 容器解析
Istio 在 Pod 中注入的 Init 容器名為 `istio-init`,我們在上面 Istio 注入完成后的 YAML 文件中看到了該容器的啟動參數:
```bash
-p 15001 -u 1337 -m REDIRECT -i '*' -x "" -b 9080 -d ""
```
我們再檢查下該容器的 [Dockerfile](https://github.com/istio/istio/blob/master/pilot/docker/Dockerfile.proxy_init) 看看 `ENTRYPOINT` 是什么以確定啟動時執行的命令。
```docker
FROM ubuntu:xenial
RUN apt-get update && apt-get install -y \
iproute2 \
iptables \
&& rm -rf /var/lib/apt/lists/*
ADD istio-iptables.sh /usr/local/bin/
ENTRYPOINT ["/usr/local/bin/istio-iptables.sh"]
```
我們看到 `istio-init` 容器的入口是 `/usr/local/bin/istio-iptables.sh` 腳本,再按圖索驥看看這個腳本里到底寫的什么,該腳本的位置在 Istio 源碼倉庫的 [tools/deb/istio-iptables.sh](https://github.com/istio/istio/blob/master/tools/deb/istio-iptables.sh),一共 300 多行,就不貼在這里了。下面我們就來解析下這個啟動腳本。
### Init 容器啟動入口
Init 容器的啟動入口是 `/usr/local/bin/istio-iptables.sh` 腳本,該腳本的用法如下:
```bash
$ istio-iptables.sh -p PORT -u UID -g GID [-m mode] [-b ports] [-d ports] [-i CIDR] [-x CIDR] [-h]
-p: 指定重定向所有 TCP 流量的 Envoy 端口(默認為 $ENVOY_PORT = 15001)
-u: 指定未應用重定向的用戶的 UID。通常,這是代理容器的 UID(默認為 $ENVOY_USER 的 uid,istio_proxy 的 uid 或 1337)
-g: 指定未應用重定向的用戶的 GID。(與 -u param 相同的默認值)
-m: 指定入站連接重定向到 Envoy 的模式,“REDIRECT” 或 “TPROXY”(默認為 $ISTIO_INBOUND_INTERCEPTION_MODE)
-b: 逗號分隔的入站端口列表,其流量將重定向到 Envoy(可選)。使用通配符 “*” 表示重定向所有端口。為空時表示禁用所有入站重定向(默認為 $ISTIO_INBOUND_PORTS)
-d: 指定要從重定向到 Envoy 中排除(可選)的入站端口列表,以逗號格式分隔。使用通配符“*” 表示重定向所有入站流量(默認為 $ISTIO_LOCAL_EXCLUDE_PORTS)
-i: 指定重定向到 Envoy(可選)的 IP 地址范圍,以逗號分隔的 CIDR 格式列表。使用通配符 “*” 表示重定向所有出站流量。空列表將禁用所有出站重定向(默認為 $ISTIO_SERVICE_CIDR)
-x: 指定將從重定向中排除的 IP 地址范圍,以逗號分隔的 CIDR 格式列表。使用通配符 “*” 表示重定向所有出站流量(默認為 $ISTIO_SERVICE_EXCLUDE_CIDR)。
環境變量位于 $ISTIO_SIDECAR_CONFIG(默認在:/var/lib/istio/envoy/sidecar.env)
```
通過查看該腳本你將看到,以上傳入的參數都會重新組裝成 [`iptables` 命令](https://wangchujiang.com/linux-command/c/iptables.html)的參數。
再參考 `istio-init` 容器的啟動參數,完整的啟動命令如下:
```bash
$ /usr/local/bin/istio-iptables.sh -p 15001 -u 1337 -m REDIRECT -i '*' -x "" -b 9080 -d ""
```
該容器存在的意義就是讓 Envoy 代理可以攔截所有的進出 Pod 的流量,即將入站流量重定向到 Sidecar,再攔截應用容器的出站流量經過 Sidecar 處理后再出站。
**命令解析**
這條啟動命令的作用是:
- 將應用容器的所有流量都轉發到 Envoy 的 15001 端口。
- 使用 `istio-proxy` 用戶身份運行, UID 為 1337,即 Envoy 所處的用戶空間,這也是 `istio-proxy` 容器默認使用的用戶,見 YAML 配置中的 `runAsUser` 字段。
- 使用默認的 `REDIRECT` 模式來重定向流量。
- 將所有出站流量都重定向到 Envoy 代理。
- 將所有訪問 9080 端口(即應用容器 `productpage` 的端口)的流量重定向到 Envoy 代理。
因為 Init 容器初始化完畢后就會自動終止,因為我們無法登陸到容器中查看 iptables 信息,但是 Init 容器初始化結果會保留到應用容器和 Sidecar 容器中。
### istio-proxy 容器解析
為了查看 iptables 配置,我們需要登陸到 Sidecar 容器中使用 root 用戶來查看,因為 `kubectl` 無法使用特權模式來遠程操作 docker 容器,所以我們需要登陸到 `productpage` Pod 所在的主機上使用 `docker` 命令登陸容器中查看。
查看 `productpage` Pod 所在的主機。
```bash
$ kubectl -n default get pod -l app=productpage -o wide
NAME READY STATUS RESTARTS AGE IP NODE
productpage-v1-745ffc55b7-2l2lw 2/2 Running 0 1d 172.33.78.10 node3
```
從輸出結果中可以看到該 Pod 運行在 `node3` 上,使用 `vagrant` 命令登陸到 `node3` 主機中并切換為 root 用戶。
```bash
$ vagrant ssh node3
$ sudo -i
```
查看 iptables 配置,列出 NAT(網絡地址轉換)表的所有規則,因為在 Init 容器啟動的時候選擇給 `istio-iptables.sh` 傳遞的參數中指定將入站流量重定向到 Envoy 的模式為 “REDIRECT”,因此在 iptables 中將只有 NAT 表的規格配置,如果選擇 `TPROXY` 還會有 `mangle` 表配置。`iptables` 命令的詳細用法請參考 [iptables](https://wangchujiang.com/linux-command/c/iptables.html),規則配置請參考 [iptables 規則配置](http://www.zsythink.net/archives/1517)。
## 理解 iptables
`iptables` 是 Linux 內核中的防火墻軟件 netfilter 的管理工具,位于用戶空間,同時也是 netfilter 的一部分。Netfilter 位于內核空間,不僅有網絡地址轉換的功能,也具備數據包內容修改、以及數據包過濾等防火墻功能。
在了解 Init 容器初始化的 iptables 之前,我們先來了解下 iptables 和規則配置。
下圖展示了 iptables 調用鏈。

### iptables 中的表
Init 容器中使用的的 iptables 版本是 `v1.6.0`,共包含 5 張表:
1. `raw` 用于配置數據包,`raw` 中的數據包不會被系統跟蹤。
2. `filter` 是用于存放所有與防火墻相關操作的默認表。
3. `nat` 用于 [網絡地址轉換](https://en.wikipedia.org/wiki/Network_address_translation)(例如:端口轉發)。
4. `mangle` 用于對特定數據包的修改(參考[損壞數據包](https://en.wikipedia.org/wiki/Mangled_packet))。
5. `security` 用于[強制訪問控制](https://wiki.archlinux.org/index.php/Security#Mandatory_access_control) 網絡規則。
**注**:在本示例中只用到了 `nat` 表。
不同的表中的具有的鏈類型如下表所示:
| 規則名稱 | raw | filter | nat | mangle | security |
| ----------- | ---- | ------ | ---- | ------ | -------- |
| PREROUTING | ? | | ? | ? | |
| INPUT | | ? | ? | ? | ? |
| OUTPUT | | ? | ? | ? | ? |
| POSTROUTING | | | ? | ? | |
| FORWARD | ? | ? | | ? | ? |
下圖是 iptables 的調用鏈順序。

關于 iptables 的詳細介紹請參考[常見 iptables 使用規則場景整理](https://www.aliang.org/Linux/iptables.html)。
### iptables 命令
`iptables` 命令的主要用途是修改這些表中的規則。`iptables` 命令格式如下:
```bash
$ iptables [-t 表名] 命令選項[鏈名][條件匹配][-j 目標動作或跳轉]
```
Init 容器中的 `/istio-iptables.sh` 啟動入口腳本就是執行 iptables 初始化的。
### 理解 iptables 規則
查看 `istio-proxy` 容器中的默認的 iptables 規則,默認查看的是 filter 表中的規則。
```bash
$ iptables -L -v
Chain INPUT (policy ACCEPT 350K packets, 63M bytes)
pkts bytes target prot opt in out source destination
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 18M packets, 1916M bytes)
pkts bytes target prot opt in out source destination
```
我們看到三個默認的鏈,分別是 INPUT、FORWARD 和 OUTPUT,每個鏈中的第一行輸出表示鏈名稱(在本例中為INPUT/FORWARD/OUTPUT),后跟默認策略(ACCEPT)。
下圖是 iptables 的建議結構圖,流量在經過 INPUT 鏈之后就進入了上層協議棧,比如

圖片來自[常見 iptables 使用規則場景整理](https://www.aliang.org/Linux/iptables.html)
每條鏈中都可以添加多條規則,規則是按照順序從前到后執行的。我們來看下規則的表頭定義。
- **pkts**:處理過的匹配的報文數量
- **bytes**:累計處理的報文大小(字節數)
- **target**:如果報文與規則匹配,指定目標就會被執行。
- **prot**:協議,例如 `tdp`、`udp`、`icmp` 和 `all`。
- **opt**:很少使用,這一列用于顯示 IP 選項。
- **in**:入站網卡。
- **out**:出站網卡。
- **source**:流量的源 IP 地址或子網,后者是 `anywhere`。
- **destination**:流量的目的地 IP 地址或子網,或者是 `anywhere`。
還有一列沒有表頭,顯示在最后,表示規則的選項,作為規則的擴展匹配條件,用來補充前面的幾列中的配置。`prot`、`opt`、`in`、`out`、`source` 和 `destination` 和顯示在 `destination` 后面的沒有表頭的一列擴展條件共同組成匹配規則。當流量匹配這些規則后就會執行 `target`。
關于 iptables 規則請參考[常見iptables使用規則場景整理](https://www.aliang.org/Linux/iptables.html)。
**target 支持的類型**
`target` 類型包括 ACCEPT`、REJECT`、`DROP`、`LOG` 、`SNAT`、`MASQUERADE`、`DNAT`、`REDIRECT`、`RETURN` 或者跳轉到其他規則等。只要執行到某一條鏈中只有按照順序有一條規則匹配后就可以確定報文的去向了,除了 `RETURN` 類型,類似編程語言中的 `return` 語句,返回到它的調用點,繼續執行下一條規則。`target` 支持的配置詳解請參考 [iptables 詳解(1):iptables 概念](http://www.zsythink.net/archives/1199)。
從輸出結果中可以看到 Init 容器沒有在 iptables 的默認鏈路中創建任何規則,而是創建了新的鏈路。
## 查看 iptables nat 表中注入的規則
Init 容器通過向 iptables nat 表中注入轉發規則來劫持流量的,下圖顯示的是 productpage 服務中的 iptables 流量劫持的詳細過程。

Init 容器啟動時命令行參數中指定了 `REDIRECT` 模式,因此只創建了 NAT 表規則,接下來我們查看下 NAT 表中創建的規則,這是全文中的**重點部分**,前面講了那么多都是為它做鋪墊的。下面是查看 nat 表中的規則,其中鏈的名字中包含 `ISTIO` 前綴的是由 Init 容器注入的,規則匹配是根據下面顯示的順序來執行的,其中會有多次跳轉。
```bash
# 查看 NAT 表中規則配置的詳細信息
$ iptables -t nat -L -v
# PREROUTING 鏈:用于目標地址轉換(DNAT),將所有入站 TCP 流量跳轉到 ISTIO_INBOUND 鏈上
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
2 120 ISTIO_INBOUND tcp -- any any anywhere anywhere
# INPUT 鏈:處理輸入數據包,非 TCP 流量將繼續 OUTPUT 鏈
Chain INPUT (policy ACCEPT 2 packets, 120 bytes)
pkts bytes target prot opt in out source destination
# OUTPUT 鏈:將所有出站數據包跳轉到 ISTIO_OUTPUT 鏈上
Chain OUTPUT (policy ACCEPT 41146 packets, 3845K bytes)
pkts bytes target prot opt in out source destination
93 5580 ISTIO_OUTPUT tcp -- any any anywhere anywhere
# POSTROUTING 鏈:所有數據包流出網卡時都要先進入POSTROUTING 鏈,內核根據數據包目的地判斷是否需要轉發出去,我們看到此處未做任何處理
Chain POSTROUTING (policy ACCEPT 41199 packets, 3848K bytes)
pkts bytes target prot opt in out source destination
# ISTIO_INBOUND 鏈:將所有目的地為 9080 端口的入站流量重定向到 ISTIO_IN_REDIRECT 鏈上
Chain ISTIO_INBOUND (1 references)
pkts bytes target prot opt in out source destination
2 120 ISTIO_IN_REDIRECT tcp -- any any anywhere anywhere tcp dpt:9080
# ISTIO_IN_REDIRECT 鏈:將所有的入站流量跳轉到本地的 15001 端口,至此成功的攔截了流量到 Envoy
Chain ISTIO_IN_REDIRECT (1 references)
pkts bytes target prot opt in out source destination
2 120 REDIRECT tcp -- any any anywhere anywhere redir ports 15001
# ISTIO_OUTPUT 鏈:選擇需要重定向到 Envoy(即本地) 的出站流量,所有非 localhost 的流量全部轉發到 ISTIO_REDIRECT。為了避免流量在該 Pod 中無限循環,所有到 istio-proxy 用戶空間的流量都返回到它的調用點中的下一條規則,本例中即 OUTPUT 鏈,因為跳出 ISTIO_OUTPUT 規則之后就進入下一條鏈 POSTROUTING。如果目的地非 localhost 就跳轉到 ISTIO_REDIRECT;如果流量是來自 istio-proxy 用戶空間的,那么就跳出該鏈,返回它的調用鏈繼續執行下一條規則(OUPT 的下一條規則,無需對流量進行處理);所有的非 istio-proxy 用戶空間的目的地是 localhost 的流量就跳轉到 ISTIO_REDIRECT
Chain ISTIO_OUTPUT (1 references)
pkts bytes target prot opt in out source destination
0 0 ISTIO_REDIRECT all -- any lo anywhere !localhost
40 2400 RETURN all -- any any anywhere anywhere owner UID match istio-proxy
0 0 RETURN all -- any any anywhere anywhere owner GID match istio-proxy
0 0 RETURN all -- any any anywhere localhost
53 3180 ISTIO_REDIRECT all -- any any anywhere anywhere
# ISTIO_REDIRECT 鏈:將所有流量重定向到 Envoy(即本地) 的 15001 端口
Chain ISTIO_REDIRECT (2 references)
pkts bytes target prot opt in out source destination
53 3180 REDIRECT tcp -- any any anywhere anywhere redir ports 15001
```
`iptables` 顯示的鏈的順序,即流量規則匹配的順序。其中要特別注意 `ISTIO_OUTPUT` 鏈中的規則配置。為了避免流量一直在 Pod 中無限循環,所有到 istio-proxy 用戶空間的流量都返回到它的調用點中的下一條規則,本例中即 OUTPUT 鏈,因為跳出 `ISTIO_OUTPUT` 規則之后就進入下一條鏈 `POSTROUTING`。
`ISTIO_OUTPUT` 鏈規則匹配的詳細過程如下:
- 如果目的地非 localhost 就跳轉到 ISTIO_REDIRECT 鏈
- 所有來自 istio-proxy 用戶空間的流量跳轉到它的調用點 `OUTPUT` 繼續執行 `OUTPUT` 鏈的下一條規則,因為 `OUTPUT` 鏈中沒有下一條規則了,所以會繼續執行 `POSTROUTING` 鏈然后跳出 iptables,直接訪問目的地
- 如果目的地是 localhost 但是流量又不是來自 istio-proxy 用戶空間的就跳轉到 `ISTIO_REDIRECT` 鏈
以上 iptables 規則都是 Init 容器啟動的時使用 [istio-iptables.sh](https://github.com/istio/istio/blob/master/tools/deb/istio-iptables.sh) 腳本生成的,詳細過程可以查看該腳本。
## 查看 Envoy 運行狀態
首先查看 `proxyv2` 鏡像的 [Dockerfile](https://github.com/istio/istio/blob/master/pilot/docker/Dockerfile.proxyv2)。
```docker
FROM istionightly/base_debug
ARG proxy_version
ARG istio_version
# 安裝 Envoy
ADD envoy /usr/local/bin/envoy
# 使用環境變量的方式明文指定 proxy 的版本/功能
ENV ISTIO_META_ISTIO_PROXY_VERSION "1.1.0"
# 使用環境變量的方式明文指定 proxy 明確的 sha,用于指定版本的配置和調試
ENV ISTIO_META_ISTIO_PROXY_SHA $proxy_version
# 環境變量,指定明確的構建號,用于調試
ENV ISTIO_META_ISTIO_VERSION $istio_version
ADD pilot-agent /usr/local/bin/pilot-agent
ADD envoy_pilot.yaml.tmpl /etc/istio/proxy/envoy_pilot.yaml.tmpl
ADD envoy_policy.yaml.tmpl /etc/istio/proxy/envoy_policy.yaml.tmpl
ADD envoy_telemetry.yaml.tmpl /etc/istio/proxy/envoy_telemetry.yaml.tmpl
ADD istio-iptables.sh /usr/local/bin/istio-iptables.sh
COPY envoy_bootstrap_v2.json /var/lib/istio/envoy/envoy_bootstrap_tmpl.json
RUN chmod 755 /usr/local/bin/envoy /usr/local/bin/pilot-agent
# 將 istio-proxy 用戶加入 sudo 權限以允許執行 tcpdump 和其他調試命令
RUN useradd -m --uid 1337 istio-proxy && \
echo "istio-proxy ALL=NOPASSWD: ALL" >> /etc/sudoers && \
chown -R istio-proxy /var/lib/istio
# 使用 pilot-agent 來啟動 Envoy
ENTRYPOINT ["/usr/local/bin/pilot-agent"]
```
該容器的啟動入口是 `pilot-agent` 命令,根據 YAML 配置中傳遞的參數,詳細的啟動命令入下:
```bash
/usr/local/bin/pilot-agent proxy sidecar --configPath /etc/istio/proxy --binaryPath /usr/local/bin/envoy --serviceCluster productpage --drainDuration 45s --parentShutdownDuration 1m0s --discoveryAddress istio-pilot.istio-system:15007 --discoveryRefreshDelay 1s --zipkinAddress zipkin.istio-system:9411 --connectTimeout 10s --statsdUdpAddress istio-statsd-prom-bridge.istio-system:9125 --proxyAdminPort 15000 --controlPlaneAuthPolicy NONE
```
主要配置了 Envoy 二進制文件的位置、服務發現地址、服務集群名、監控指標上報地址、Envoy 的管理端口、熱重啟時間等,詳細用法請參考 [Istio官方文檔 pilot-agent 的用法](https://istio.io/docs/reference/commands/pilot-agent/)。
`pilot-agent` 是容器中 PID 為 1 的啟動進程,它啟動時又創建了一個 Envoy 進程,如下:
```bash
/usr/local/bin/envoy -c /etc/istio/proxy/envoy-rev0.json --restart-epoch 0 --drain-time-s 45 --parent-shutdown-time-s 60 --service-cluster productpage --service-node sidecar~172.33.78.10~productpage-v1-745ffc55b7-2l2lw.default~default.svc.cluster.local --max-obj-name-len 189 -l warn --v2-config-only
```
我們分別解釋下以上配置的意義。
- `-c /etc/istio/proxy/envoy-rev0.json`:配置文件,支持 `.json`、`.yaml`、`.pb` 和 `.pb_text` 格式,`pilot-agent` 啟動的時候讀取了容器的環境變量后創建的。
- `--restart-epoch 0`:Envoy 熱重啟周期,第一次啟動默認為 0,每熱重啟一次該值加 1。
- `--drain-time-s 45`:熱重啟期間 Envoy 將耗盡連接的時間。
- `--parent-shutdown-time-s 60`: Envoy 在熱重啟時關閉父進程之前等待的時間。
- `--service-cluster productpage`:Envoy 運行的本地服務集群的名字。
- `--service-node sidecar~172.33.78.10~productpage-v1-745ffc55b7-2l2lw.default~default.svc.cluster.local`:定義 Envoy 運行的本地服務節點名稱,其中包含了該 Pod 的名稱、IP、DNS 域等信息,根據容器的環境變量拼出來的。
- `-max-obj-name-len 189`:cluster/route_config/listener 中名稱字段的最大長度(以字節為單位)
- `-l warn`:日志級別
- `--v2-config-only`:只解析 v2 引導配置文件
詳細配置請參考 [Envoy 的命令行選項](http://www.servicemesher.com/envoy/operations/cli.html)。
查看 Envoy 的配置文件 `/etc/istio/proxy/envoy-rev0.json`。
```json
{
"node": {
"id": "sidecar~172.33.78.10~productpage-v1-745ffc55b7-2l2lw.default~default.svc.cluster.local",
"cluster": "productpage",
"metadata": {
"INTERCEPTION_MODE": "REDIRECT",
"ISTIO_PROXY_SHA": "istio-proxy:6166ae7ebac7f630206b2fe4e6767516bf198313",
"ISTIO_PROXY_VERSION": "1.0.0",
"ISTIO_VERSION": "1.0.0",
"POD_NAME": "productpage-v1-745ffc55b7-2l2lw",
"istio": "sidecar"
}
},
"stats_config": {
"use_all_default_tags": false
},
"admin": {
"access_log_path": "/dev/stdout",
"address": {
"socket_address": {
"address": "127.0.0.1",
"port_value": 15000
}
}
},
"dynamic_resources": {
"lds_config": {
"ads": {}
},
"cds_config": {
"ads": {}
},
"ads_config": {
"api_type": "GRPC",
"refresh_delay": {"seconds": 1, "nanos": 0},
"grpc_services": [
{
"envoy_grpc": {
"cluster_name": "xds-grpc"
}
}
]
}
},
"static_resources": {
"clusters": [
{
"name": "xds-grpc",
"type": "STRICT_DNS",
"connect_timeout": {"seconds": 10, "nanos": 0},
"lb_policy": "ROUND_ROBIN",
"hosts": [
{
"socket_address": {"address": "istio-pilot.istio-system", "port_value": 15010}
}
],
"circuit_breakers": {
"thresholds": [
{
"priority": "default",
"max_connections": "100000",
"max_pending_requests": "100000",
"max_requests": "100000"
},
{
"priority": "high",
"max_connections": "100000",
"max_pending_requests": "100000",
"max_requests": "100000"
}]
},
"upstream_connection_options": {
"tcp_keepalive": {
"keepalive_time": 300
}
},
"http2_protocol_options": { }
}
,
{
"name": "zipkin",
"type": "STRICT_DNS",
"connect_timeout": {
"seconds": 1
},
"lb_policy": "ROUND_ROBIN",
"hosts": [
{
"socket_address": {"address": "zipkin.istio-system", "port_value": 9411}
}
]
}
]
},
"tracing": {
"http": {
"name": "envoy.zipkin",
"config": {
"collector_cluster": "zipkin"
}
}
},
"stats_sinks": [
{
"name": "envoy.statsd",
"config": {
"address": {
"socket_address": {"address": "10.254.109.175", "port_value": 9125}
}
}
}
]
}
```
下圖是使用 Istio 管理的 bookinfo 示例的訪問請求路徑圖。

圖片來自 [Istio 官方網站](https://istio.io/zh/docs/examples/bookinfo/)
對照 bookinfo 示例的 productpage 的查看建立的連接。在 `productpage-v1-745ffc55b7-2l2lw` Pod 的 `istio-proxy` 容器中使用 root 用戶查看打開的端口。
```bash
$ lsof -i
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
envoy 11 istio-proxy 9u IPv4 73951 0t0 TCP localhost:15000 (LISTEN) # Envoy admin 端口
envoy 11 istio-proxy 17u IPv4 74320 0t0 TCP productpage-v1-745ffc55b7-2l2lw:46862->istio-pilot.istio-system.svc.cluster.local:15010 (ESTABLISHED) # 15010:istio-pilot 的 grcp-xds 端口
envoy 11 istio-proxy 18u IPv4 73986 0t0 UDP productpage-v1-745ffc55b7-2l2lw:44332->istio-statsd-prom-bridge.istio-system.svc.cluster.local:9125 # 給 Promethues 發送 metric 的端口
envoy 11 istio-proxy 52u IPv4 74599 0t0 TCP *:15001 (LISTEN) # Envoy 的監聽端口
envoy 11 istio-proxy 53u IPv4 74600 0t0 UDP productpage-v1-745ffc55b7-2l2lw:48011->istio-statsd-prom-bridge.istio-system.svc.cluster.local:9125 # 給 Promethues 發送 metric 端口
envoy 11 istio-proxy 54u IPv4 338551 0t0 TCP productpage-v1-745ffc55b7-2l2lw:15001->172.17.8.102:52670 (ESTABLISHED) # 52670:Ingress gateway 端口
envoy 11 istio-proxy 55u IPv4 338364 0t0 TCP productpage-v1-745ffc55b7-2l2lw:44046->172.33.78.9:9091 (ESTABLISHED) # 9091:istio-telemetry 服務的 grpc-mixer 端口
envoy 11 istio-proxy 56u IPv4 338473 0t0 TCP productpage-v1-745ffc55b7-2l2lw:47210->zipkin.istio-system.svc.cluster.local:9411 (ESTABLISHED) # 9411: zipkin 端口
envoy 11 istio-proxy 58u IPv4 338383 0t0 TCP productpage-v1-745ffc55b7-2l2lw:41564->172.33.84.8:9080 (ESTABLISHED) # 9080:details-v1 的 http 端口
envoy 11 istio-proxy 59u IPv4 338390 0t0 TCP productpage-v1-745ffc55b7-2l2lw:54410->172.33.78.5:9080 (ESTABLISHED) # 9080:reivews-v2 的 http 端口
envoy 11 istio-proxy 60u IPv4 338411 0t0 TCP productpage-v1-745ffc55b7-2l2lw:35200->172.33.84.5:9091 (ESTABLISHED) # 9091:istio-telemetry 服務的 grpc-mixer 端口
envoy 11 istio-proxy 62u IPv4 338497 0t0 TCP productpage-v1-745ffc55b7-2l2lw:34402->172.33.84.9:9080 (ESTABLISHED) # reviews-v1 的 http 端口
envoy 11 istio-proxy 63u IPv4 338525 0t0 TCP productpage-v1-745ffc55b7-2l2lw:50592->172.33.71.5:9080 (ESTABLISHED) # reviews-v3 的 http 端口
```
從輸出級過上可以驗證 Sidecar 是如何接管流量和與 istio-pilot 通信,及向 Mixer 做遙測數據匯聚的。感興趣的讀者可以再去看看其他幾個服務的 istio-proxy 容器中的 iptables 和端口信息。
## 參考
- [SOFAMesh & SOFA MOSN—基于Istio構建的用于應對大規模流量的Service Mesh解決方案 - jimmysong.io](https://jimmysong.io/posts/sofamesh-and-mosn-proxy-sidecar-service-mesh-by-ant-financial/ - jimmysong.io)
- [Init 容器 - Kubernetes 中文指南/云原生應用架構實踐手冊 - jimmysong.io](https://jimmysong.io/kubernetes-handbook/concepts/init-containers.html)
- [JSONPath Support - kubernetes.io](https://kubernetes.io/docs/reference/kubectl/jsonpath/)
- [iptables 命令使用說明 - wangchujiang.com](https://wangchujiang.com/linux-command/c/iptables.html)
- [How To List and Delete Iptables Firewall Rules - digitalocean.com](https://www.digitalocean.com/community/tutorials/how-to-list-and-delete-iptables-firewall-rules)
- [一句一句解說 iptables的詳細中文手冊 - cnblog.com](https://www.cnblogs.com/fhefh/archive/2011/04/04/2005249.html)
- [常見iptables使用規則場景整理 - aliang.org](https://www.aliang.org/Linux/iptables.html)
- 序言
- 云原生
- 云原生(Cloud Native)的定義
- CNCF - 云原生計算基金會簡介
- CNCF章程
- 云原生的設計哲學
- Play with Kubernetes
- 快速部署一個云原生本地實驗環境
- Kubernetes與云原生應用概覽
- 云原生應用之路——從Kubernetes到Cloud Native
- 云原生編程語言
- 云原生編程語言Ballerina
- 云原生編程語言Pulumi
- 云原生的未來
- Kubernetes架構
- 設計理念
- Etcd解析
- 開放接口
- CRI - Container Runtime Interface(容器運行時接口)
- CNI - Container Network Interface(容器網絡接口)
- CSI - Container Storage Interface(容器存儲接口)
- Kubernetes中的網絡
- Kubernetes中的網絡解析——以flannel為例
- Kubernetes中的網絡解析——以calico為例
- 具備API感知的網絡和安全性管理開源軟件Cilium
- Cilium架構設計與概念解析
- 資源對象與基本概念解析
- Pod狀態與生命周期管理
- Pod概覽
- Pod解析
- Init容器
- Pause容器
- Pod安全策略
- Pod的生命周期
- Pod Hook
- Pod Preset
- Pod中斷與PDB(Pod中斷預算)
- 集群資源管理
- Node
- Namespace
- Label
- Annotation
- Taint和Toleration(污點和容忍)
- 垃圾收集
- 控制器
- Deployment
- StatefulSet
- DaemonSet
- ReplicationController和ReplicaSet
- Job
- CronJob
- Horizontal Pod Autoscaling
- 自定義指標HPA
- 準入控制器(Admission Controller)
- 服務發現
- Service
- Ingress
- Traefik Ingress Controller
- 身份與權限控制
- ServiceAccount
- RBAC——基于角色的訪問控制
- NetworkPolicy
- 存儲
- Secret
- ConfigMap
- ConfigMap的熱更新
- Volume
- Persistent Volume(持久化卷)
- Storage Class
- 本地持久化存儲
- 集群擴展
- 使用自定義資源擴展API
- 使用CRD擴展Kubernetes API
- Aggregated API Server
- APIService
- Service Catalog
- 資源調度
- QoS(服務質量等級)
- 用戶指南
- 資源對象配置
- 配置Pod的liveness和readiness探針
- 配置Pod的Service Account
- Secret配置
- 管理namespace中的資源配額
- 命令使用
- Docker用戶過度到kubectl命令行指南
- kubectl命令概覽
- kubectl命令技巧大全
- 使用etcdctl訪問kubernetes數據
- 集群安全性管理
- 管理集群中的TLS
- kubelet的認證授權
- TLS bootstrap
- 創建用戶認證授權的kubeconfig文件
- IP偽裝代理
- 使用kubeconfig或token進行用戶身份認證
- Kubernetes中的用戶與身份認證授權
- Kubernetes集群安全性配置最佳實踐
- 訪問Kubernetes集群
- 訪問集群
- 使用kubeconfig文件配置跨集群認證
- 通過端口轉發訪問集群中的應用程序
- 使用service訪問群集中的應用程序
- 從外部訪問Kubernetes中的Pod
- Cabin - Kubernetes手機客戶端
- Kubernetic - Kubernetes桌面客戶端
- Kubernator - 更底層的Kubernetes UI
- 在Kubernetes中開發部署應用
- 適用于kubernetes的應用開發部署流程
- 遷移傳統應用到Kubernetes中——以Hadoop YARN為例
- 最佳實踐概覽
- 在CentOS上部署Kubernetes集群
- 創建TLS證書和秘鑰
- 創建kubeconfig文件
- 創建高可用etcd集群
- 安裝kubectl命令行工具
- 部署master節點
- 安裝flannel網絡插件
- 部署node節點
- 安裝kubedns插件
- 安裝dashboard插件
- 安裝heapster插件
- 安裝EFK插件
- 生產級的Kubernetes簡化管理工具kubeadm
- 使用kubeadm在Ubuntu Server 16.04上快速構建測試集群
- 服務發現與負載均衡
- 安裝Traefik ingress
- 分布式負載測試
- 網絡和集群性能測試
- 邊緣節點配置
- 安裝Nginx ingress
- 安裝配置DNS
- 安裝配置Kube-dns
- 安裝配置CoreDNS
- 運維管理
- Master節點高可用
- 服務滾動升級
- 應用日志收集
- 配置最佳實踐
- 集群及應用監控
- 數據持久化問題
- 管理容器的計算資源
- 集群聯邦
- 存儲管理
- GlusterFS
- 使用GlusterFS做持久化存儲
- 使用Heketi作為Kubernetes的持久存儲GlusterFS的external provisioner
- 在OpenShift中使用GlusterFS做持久化存儲
- GlusterD-2.0
- Ceph
- 用Helm托管安裝Ceph集群并提供后端存儲
- 使用Ceph做持久化存儲
- 使用rbd-provisioner提供rbd持久化存儲
- OpenEBS
- 使用OpenEBS做持久化存儲
- Rook
- NFS
- 利用NFS動態提供Kubernetes后端存儲卷
- 集群與應用監控
- Heapster
- 使用Heapster獲取集群和對象的metric數據
- Prometheus
- 使用Prometheus監控kubernetes集群
- Prometheus查詢語言PromQL使用說明
- 使用Vistio監控Istio服務網格中的流量
- 分布式跟蹤
- OpenTracing
- 服務編排管理
- 使用Helm管理Kubernetes應用
- 構建私有Chart倉庫
- 持續集成與發布
- 使用Jenkins進行持續集成與發布
- 使用Drone進行持續集成與發布
- 更新與升級
- 手動升級Kubernetes集群
- 升級dashboard
- 領域應用概覽
- 微服務架構
- 微服務中的服務發現
- 使用Java構建微服務并發布到Kubernetes平臺
- Spring Boot快速開始指南
- Service Mesh 服務網格
- 企業級服務網格架構
- Service Mesh基礎
- Service Mesh技術對比
- 采納和演進
- 定制和集成
- 總結
- Istio
- 安裝并試用Istio service mesh
- 配置請求的路由規則
- 安裝和拓展Istio service mesh
- 集成虛擬機
- Istio中sidecar的注入規范及示例
- 如何參與Istio社區及注意事項
- Istio教程
- Istio免費學習資源匯總
- 深入理解Istio Service Mesh中的Envoy Sidecar注入與流量劫持
- 深入理解Istio Service Mesh中的Envoy Sidecar代理的路由轉發
- Linkerd
- Linkerd 使用指南
- Conduit
- Condiut概覽
- 安裝Conduit
- Envoy
- Envoy的架構與基本術語
- Envoy作為前端代理
- Envoy mesh教程
- SOFAMesh
- SOFAMesh中的Dubbo on x-protocol
- SOFAMosn
- 使用 SOFAMosn 構建 SOFAMesh
- 大數據
- Spark standalone on Kubernetes
- 運行支持Kubernetes原生調度的Spark程序
- Serverless架構
- 理解Serverless
- FaaS-函數即服務
- OpenFaaS快速入門指南
- 邊緣計算
- 人工智能