<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆 &#187; ServiceMesh</title>
	<atom:link href="https://blog.gmem.cc/tag/servicemesh/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Sun, 19 Apr 2026 07:54:29 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>Istio中的透明代理问题</title>
		<link>https://blog.gmem.cc/istio-tproxy</link>
		<comments>https://blog.gmem.cc/istio-tproxy#comments</comments>
		<pubDate>Wed, 22 Jul 2020 02:07:07 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[ServiceMesh]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=31995</guid>
		<description><![CDATA[<p>为何需要透明代理 Istio的Sidecar作为一个网络代理，它拦截入站、出站的网络流量。拦截入站流量后，会使用127.0.0.1作为源地址，将流量转发给本地服务进程。本地服务进程看不到真实源IP地址。 很多应用场景下，真实源IP地址是必须的，可能原因包括： IP地址作为标识的一部分。以ZooKeeper为例，它通过成员的IP地址来验证集群成员身份 IP地址用于网络策略，或者用于审计目的 本文将设置这样的场景：一个启用了Istio Sidecar的Nginx Pod，需要被当前命名空间的另外一个Pod访问。我们将尝试解决Nginx不能看到真实的客户端IP地址的问题。 Envoy的现状 &#160; 目前Envoy已经能够很好的支持IP Transparency了。 它提供了多种机制把真实源地址提供给上游服务。 http.original_src 真实源地址可以通过[crayon-69e5e2351adb9523262854-i/]这样的请求头获取，很多应用都能识别这种请求头。 Envoy还提供了[crayon-69e5e2351adc0506694683-i/]，此过滤器能够从请求头读取真实源地址，并修改底层TCP连接的源地址。此过滤器还能处理单一下游连接携带来自多个源的HTTP请求的情况。此过滤器的缺点包括： 下游连接必须正确设置了x-forwarded-for头 由于连接池方面的限制，会导致些许性能影响 配置较为复杂，可能需要路由的配合，即使在Sidecar场景（Envoy和上游在同一网络命名空间）下，也需要配置好iptables规则 listener.proxy_protocol HAProxy代理协议提供了交换连接元数据的机制，这些元数据就包括真实源IP。Envoy通过监听器过滤器[crayon-69e5e2351adc2758232673-i/]支持代理协议。此过滤器的缺点包括： 上游主机需要支持代理协议 <a class="read-more" href="https://blog.gmem.cc/istio-tproxy">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/istio-tproxy">Istio中的透明代理问题</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">为何需要透明代理</span></div>
<p>Istio的Sidecar作为一个网络代理，它拦截入站、出站的网络流量。拦截入站流量后，会使用127.0.0.1作为源地址，将流量转发给本地服务进程。本地服务进程看不到真实源IP地址。</p>
<p>很多应用场景下，真实源IP地址是必须的，可能原因包括：</p>
<ol>
<li>IP地址作为标识的一部分。以ZooKeeper为例，它通过成员的IP地址来验证集群成员身份</li>
<li>IP地址用于网络策略，或者用于审计目的</li>
</ol>
<p>本文将设置这样的场景：一个启用了Istio Sidecar的Nginx Pod，需要被当前命名空间的另外一个Pod访问。我们将尝试解决Nginx不能看到真实的客户端IP地址的问题。</p>
<div class="blog_h1"><span class="graybg">Envoy的现状</span></div>
<p>&nbsp;</p>
<p>目前Envoy已经能够<a href="https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/ip_transparency">很好的支持IP Transparency</a>了。 它提供了多种机制把真实源地址提供给上游服务。</p>
<div class="blog_h2"><span class="graybg">http.original_src</span></div>
<p>真实源地址可以通过<pre class="crayon-plain-tag">x-forwarded-for</pre>这样的请求头获取，很多应用都能识别这种请求头。</p>
<p>Envoy还提供了<pre class="crayon-plain-tag">envoy.filters.http.original_src</pre>，此过滤器能够从请求头读取真实源地址，并修改底层TCP连接的源地址。此过滤器还能处理单一下游连接携带来自多个源的HTTP请求的情况。此过滤器的缺点包括：</p>
<ol>
<li>下游连接必须正确设置了x-forwarded-for头</li>
<li>由于连接池方面的限制，会导致些许性能影响</li>
<li>配置较为复杂，可能需要路由的配合，即使在Sidecar场景（Envoy和上游在同一网络命名空间）下，也需要配置好iptables规则</li>
</ol>
<div class="blog_h2"><span class="graybg">listener.proxy_protocol</span></div>
<p>HAProxy代理协议提供了交换连接元数据的机制，这些元数据就包括真实源IP。Envoy通过监听器过滤器<pre class="crayon-plain-tag">envoy.filters.listener.proxy_protocol</pre>支持代理协议。此过滤器的缺点包括：</p>
<ol>
<li>上游主机需要支持代理协议</li>
<li>仅仅支持TCP</li>
</ol>
<p>该监听器过滤器可以和envoy.filters.listener.original_src联用。</p>
<div class="blog_h2"><span class="graybg">listener.original_src</span></div>
<p>在受控部署环境下，通过监听器过滤器<pre class="crayon-plain-tag">envoy.filters.listener.original_src</pre>可以把下游连接源地址复制为上游连接的源地址。</p>
<p>这需要使用透明代理，让Envoy直接以下游地址向上游服务发起连接。对于上游服务，没有任何要求。此过滤器的缺点包括：</p>
<ol>
<li>Envoy要能够获得真实的下游地址</li>
<li>由于路由方面的限制，可能无法实现</li>
<li>由于连接池方面的限制，会导致些许性能影响</li>
</ol>
<p>这个过滤器是让Istio能够解决透明代理问题的途径，回答一下对它的缺点的规避：</p>
<ol>
<li>Envoy获取真实下游IP地址，也就是入站连接的真实源地址：这可以通过TPROXY拦截模式让Envoy看到真实下游地址</li>
<li>路由方面的限制：不存在，因为Envoy和上游服务（入站连接需要访问的服务）在一个网络命名空间中，可以软件控制路由</li>
</ol>
<div class="blog_h1"><span class="graybg">Istio的现状</span></div>
<p>在两年前就有了关于此问题的Issue：<a href="https://github.com/istio/istio/issues/5679">https://github.com/istio/istio/issues/5679</a>。到目前为止，Istio官方没有提供支持透明代理的方案。</p>
<div class="blog_h2"><span class="graybg">关于拦截模式</span></div>
<p>Istio支持两种拦截模式：</p>
<ol>
<li>REDIRECT：使用iptables的REDIRECT目标来拦截入站请求，转给Envoy</li>
<li>TPROXY：使用iptables的TPROXY目标来拦截入站请求，转给Envoy</li>
</ol>
<p>你可以全局的设置默认拦截模式，也可以通过注解<pre class="crayon-plain-tag">sidecar.istio.io/interceptionMode: TPROXY</pre>给某个工作负载单独设置。</p>
<p>需要注意的是TPROXY模式<span style="background-color: #c0c0c0;">解决的仅仅是Envoy看到的入站连接源IP地址的问题</span>，被代理本地服务看到的地址仍然是127.0.0.1。</p>
<p>下面对比一下两种拦截模式下生成的iptables规则的差异：</p>
<div class="blog_h3"><span class="graybg">TPROXY</span></div>
<p>mangle表的内容如下：</p>
<pre class="crayon-plain-tag"># iptables -t mangle -L -n
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination         
ISTIO_INBOUND  tcp  --  0.0.0.0/0            0.0.0.0/0           

Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination         

Chain ISTIO_DIVERT (1 references)
target     prot opt source               destination         
MARK       all  --  0.0.0.0/0            0.0.0.0/0            MARK set 0x539
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           

Chain ISTIO_INBOUND (1 references)
target     prot opt source               destination        
# 不拦截特殊端口 
RETURN     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:22
RETURN     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:15090
RETURN     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:15020
# 如果SRC_IP:SRC_PORT:DST_IP:DST_PORT已经建立拦截，则打标记，接受封包
ISTIO_DIVERT  tcp  --  0.0.0.0/0            0.0.0.0/0            socket
# 否则，如果目的地不是127.0.0.1，则重定向给Envoy
ISTIO_TPROXY  tcp  --  0.0.0.0/0            0.0.0.0/0           

Chain ISTIO_TPROXY (1 references)
target     prot opt source               destination         
TPROXY     tcp  --  0.0.0.0/0           !127.0.0.1            TPROXY redirect 0.0.0.0:15001 mark 0x539/0xffffffff</pre>
<p>可以看到，拦截的逻辑比较简单，仅仅改了 PREROUTING （关注进入的封包）链，增加以下逻辑：</p>
<ol>
<li>对于一些特殊端口，不做拦截</li>
<li>对于已经建立了连接的封包，直接打标记1337并允许通过</li>
<li>对于目的地址不是127.0.0.1的封包，进行透明代理，发送给Envoy的15001监听器，给封包打标记1337</li>
</ol>
<p>istio-init在启动工作负载之前会设置策略路由：</p>
<pre class="crayon-plain-tag">ip -f inet rule add fwmark 1337 lookup 133
ip -f inet route add local default dev lo table 133</pre>
<p>这保证了目的地不是127.0.0.1的封包都会被15001处理，也就是<span style="background-color: #c0c0c0;">所有外部请求都需要经过Envoy处理</span>，而<span style="background-color: #c0c0c0;">Envoy向本地被代理服务转发时，会使用目的地址127.0.0.1</span>，不会被拦截。</p>
<p>nat表的内容如下：</p>
<pre class="crayon-plain-tag"># iptables -t nat -L -n -v
Chain PREROUTING (policy ACCEPT 1271 packets, 76260 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain INPUT (policy ACCEPT 1271 packets, 76260 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain OUTPUT (policy ACCEPT 38 packets, 3183 bytes)
 pkts bytes target     prot opt in     out     source               destination         
    7   420 ISTIO_OUTPUT  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0           

Chain POSTROUTING (policy ACCEPT 38 packets, 3183 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain ISTIO_IN_REDIRECT (2 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 REDIRECT   tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            redir ports 15006

Chain ISTIO_OUTPUT (1 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 RETURN     all  --  *      lo      127.0.0.6            0.0.0.0/0 
# 下面根据UID进行匹配的规则，应该有问题。因为TPROXY模式下，UID固定为0，因此下面3条规则应该去掉
    0     0 ISTIO_IN_REDIRECT  all  --  *      lo      0.0.0.0/0           !127.0.0.1            owner UID match 1337
    2   120 RETURN             all  --  *      lo      0.0.0.0/0            0.0.0.0/0            ! owner UID match 1337
    0     0 RETURN             all  --  *      *       0.0.0.0/0            0.0.0.0/0            owner UID match 1337
# 根据用户不同决定行为，如果GID为1337，意味着是Envoy进程发起的封包，否则是其它进程发起的
# 对于将从lo发出的封包，如果用户是Envoy，目的地址非127.0.0.1的，则重定向到入站虚拟监听器15006
    0     0 ISTIO_IN_REDIRECT  all  --  *      lo      0.0.0.0/0           !127.0.0.1            owner GID match 1337
# 对于将从lo发出的封包，如果用户不是Envoy，则允许通过。这保证了本机上的服务可以访问自己
    0     0 RETURN             all  --  *      lo      0.0.0.0/0            0.0.0.0/0            ! owner GID match 1337
# 对于将从非lo发出的封包，如果用户是Envoy，允许通过。这保证了Envoy可以访问外部
    5   300 RETURN             all  --  *      *       0.0.0.0/0            0.0.0.0/0            owner GID match 1337
# 到这里，所有目的地址是127.0.0.1的都被允许
    0     0 RETURN             all  --  *      *       0.0.0.0/0            127.0.0.1           
# 重定向给出站虚拟监听器15001，可能情况：
# 对于将从非lo发出的封包，如果用户不是Envoy，目的地址不是本机，则重定向到出站虚拟监听器15001
#     这保证了服务的对外访问，需要经过Envoy代理
    0     0 ISTIO_REDIRECT     all  --  *      *       0.0.0.0/0            0.0.0.0/0           

Chain ISTIO_REDIRECT (1 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 REDIRECT   tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            redir ports 15001</pre>
<p>基于UID匹配的3条规则，我觉得没有意义。原因是TPROXY模式下，运行Envoy的用户是0，而非1337，这个可以从istio-sidecar-injector这个Configmap中看出来：</p>
<pre class="crayon-plain-tag">allowPrivilegeEscalation: {{ .Values.global.proxy.privileged }}
capabilities:
  {{ if or (eq (annotation .ObjectMeta `sidecar.istio.io/interceptionMode` .ProxyConfig.InterceptionMode) `TPROXY`) (eq (annotation .ObjectMeta `sidecar.istio.io/capNetBindService` .Values.global.proxy.capNetBindService) `true`) -}}
  add:
  # 如果是TPROXY模式，增加NET_ADMIN权限
  {{ if eq (annotation .ObjectMeta `sidecar.istio.io/interceptionMode` .ProxyConfig.InterceptionMode) `TPROXY` -}}
  - NET_ADMIN
  {{- end }}
  {{ if eq (annotation .ObjectMeta `sidecar.istio.io/capNetBindService` .Values.global.proxy.capNetBindService) `true` -}}
  - NET_BIND_SERVICE
  {{- end }}
  {{- end }}
  drop:
  - ALL
privileged: {{ .Values.global.proxy.privileged }}
readOnlyRootFilesystem: {{ not .Values.global.proxy.enableCoreDump }}
# 总是使用GID 1337运行Envoy
runAsGroup: 1337
fsGroup: 1337
{{ if or (eq (annotation .ObjectMeta `sidecar.istio.io/interceptionMode` .ProxyConfig.InterceptionMode) `TPROXY`) (eq (annotation .ObjectMeta `sidecar.istio.io/capNetBindService` .Values.global.proxy.capNetBindService) `true`) -}}
# 如果是TPROXY模式，则使用UID 0运行
runAsNonRoot: false
runAsUser: 0
{{- else -}}
# 否则，使用UID 1337运行
runAsNonRoot: true
runAsUser: 1337
{{- end }}</pre>
<p>对nat表的更改发生在 OUTPUT 链（关注发出的封包）。核心逻辑：</p>
<ol>
<li>Envoy通过lo发出的，目的地址不是127.0.0.1的封包，重定向给入站监听器。根据观察，Envoy代理外部请求后，都是从lo发给127.0.0.1的，因此不会匹配此规则</li>
<li>允许本机的服务访问自身</li>
<li>服务对外发出的访问，必须经过Envoy</li>
</ol>
<p>我们仔细分析一下重定向到的15001、15006是什么东西。这些端口是istio-iptables设置的，我们看一下它的帮助：</p>
<pre class="crayon-plain-tag">Script responsible for setting up port forwarding for Istio sidecar.

Usage:
  istio-iptables [flags]

Flags:
  -p, --envoy-port string             Specify the envoy port to which redirect all TCP traffic 
                                          (default $ENVOY_PORT = 15001)
  -z, --inbound-capture-port string   Port to which all inbound TCP traffic to the pod/VM should be redirected to 
                                          (default $INBOUND_CAPTURE_PORT = 15006)</pre>
<p>看样子15006是需要将所有入站流量重定向到的端口，而在TPROXY中将入站流量都重定向到15001，这两端口如何分工？</p>
<p>这里Dump一下它们的配置。15001的：</p>
<pre class="crayon-plain-tag">// istioctl proxy-config listener nginx-84c66c7fb9-95wrd  --port 15001 -o json
[
    {
        "name": "virtualOutbound",
        "address": {
            "socketAddress": {
                "address": "0.0.0.0",
                "portValue": 15001
            }
        },
        "filterChains": [
            {
                "filters": [
                    {
                        "name": "envoy.tcp_proxy",
                        "typedConfig": {
                            "@type": "type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy",
                            "cluster": "PassthroughCluster",
                        }
                    }
                ]
            }
        ],
        // 使用原始的（被透明代理之前的）连接的目标地址来判断，由哪个监听器（Envoy进程内）来处理连接
        // 如果找不到这样的监听器，则当前监听器来处理，也就是Passthrough
        "useOriginalDst": true,
        // 可以作为TPROXY的目标，和useOriginalDst联用
        "transparent": true,
        // 期望的、相对于Envoy的流量方向
        "trafficDirection": "OUTBOUND"
    }
]

// istioctl proxy-config cluster nginx-84c66c7fb9-95wrd  --fqdn=PassthroughCluster -o json
{
    "name": "PassthroughCluster",
    "type": "ORIGINAL_DST",
    "connectTimeout": "1s",
    "lbPolicy": "CLUSTER_PROVIDED"
}</pre>
<p>可以看到，这个监听器非常简单，仅仅是做穿透处理。从它的名字virtualOutbound和字段trafficDirection上来看，它是用来处理从Pod向外发起的流量的。但是iptables却把入站流量发给它，似乎有些矛盾？</p>
<p>再看看15006的配置：</p>
<pre class="crayon-plain-tag">// istioctl proxy-config listener nginx-84c66c7fb9-95wrd  --port 15001 -o json
[
    {
        "name": "virtualInbound",
        "address": {
            "socketAddress": {
                "address": "0.0.0.0",
                "portValue": 15006
            }
        },
        "filterChains": [
            // 兜底的过滤器链
            {
                "filterChainMatch": {
                    "prefixRanges": [
                        {
                            "addressPrefix": "0.0.0.0",
                            "prefixLen": 0
                        }
                    ]
                },
                "filters": [
                    {
                        "name": "envoy.tcp_proxy",
                        "typedConfig": {
                            "statPrefix": "InboundPassthroughClusterIpv4",
                            "cluster": "InboundPassthroughClusterIpv4"
                        }
                    }
                ]
            },
            // 匹配请求本地Nginx进程的流量
            {
                "filterChainMatch": {
                    "destinationPort": 80,
                    "prefixRanges": [
                        {
                            "addressPrefix": "172.27.155.72",
                            "prefixLen": 32
                        }
                    ]
                },
                "filters": [
                    {
                        "name": "envoy.http_connection_manager",
                        "typedConfig": {
                            "statPrefix": "inbound_172.27.155.72_80",
                            "routeConfig": {
                                "name": "inbound|80|http|nginx.default.svc.k8s.gmem.cc",
                                "virtualHosts": [
                                    {
                                        "name": "inbound|http|80",
                                        "domains": [
                                            "*"
                                        ],
                                        "routes": [
                                            {
                                                "name": "default",
                                                "route": {
                                                    "cluster": "inbound|80|http|nginx.default.svc.k8s.gmem.cc"
                                                }
                                            }
                                        ]
                                    }
                                ]
                            }
                        }
                    }
                ],
            }
        ],
        "listenerFilters": [
            {
                "name": "envoy.listener.original_dst"
            },
            {
                "name": "envoy.listener.tls_inspector"
            }
        ],
        "transparent": true,
        "trafficDirection": "INBOUND"
    }
]


// istioctl proxy-config cluster nginx-84c66c7fb9-95wrd  --fqdn=InboundPassthroughClusterIpv4 -o json
{
    "name": "InboundPassthroughClusterIpv4",
    "type": "ORIGINAL_DST",
    "connectTimeout": "1s",
    "lbPolicy": "CLUSTER_PROVIDED",
    "upstreamBindConfig": {
        // 绑定新创建上游连接时使用的源地址
        "sourceAddress": {
            "address": "127.0.0.6",
            "portValue": 0
        }
    }
}


// istioctl proxy-config cluster nginx-84c66c7fb9-95wrd  --fqdn=nginx.default.svc.k8s.gmem.cc  --direction inbound -o json
[
    {
        "name": "inbound|80|http|nginx.default.svc.k8s.gmem.cc",
        "type": "STATIC",
        "loadAssignment": {
            "clusterName": "inbound|80|http|nginx.default.svc.k8s.gmem.cc",
            "endpoints": [
                {
                    "lbEndpoints": [
                        {
                            "endpoint": {
                                "address": {
                                    "socketAddress": {
                                        "address": "127.0.0.1",
                                        "portValue": 80
                                    }
                                }
                            }
                        }
                    ]
                }
            ]
        }
    }
]</pre>
<p>可以看到，这个监听器叫virtualInbound，从它的名字和配置trafficDirection上来看，它是用来处理从外面发给Pod的流量的，它明确的定义了处理连接的集群，127.0.0.1:80，即本地Nginx服务。 </p>
<div class="blog_h3"><span class="graybg">REDIRECT</span></div>
<p>此模式下，mangle表没有变动，Istio只修改了nat表。入站、出站流量的处理都在此完成：</p>
<pre class="crayon-plain-tag">Chain PREROUTING (policy ACCEPT 23 packets, 1380 bytes)
 pkts bytes target     prot opt in     out     source               destination         
   23  1380 ISTIO_INBOUND  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0           

Chain INPUT (policy ACCEPT 23 packets, 1380 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain OUTPUT (policy ACCEPT 21 packets, 1675 bytes)
 pkts bytes target     prot opt in     out     source               destination         
    5   300 ISTIO_OUTPUT  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0           

Chain POSTROUTING (policy ACCEPT 21 packets, 1675 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain ISTIO_INBOUND (1 references)
 pkts bytes target     prot opt in     out     source               destination         
# 特殊端口不处理
    0     0 RETURN     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22
    1    60 RETURN     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:15090
   22  1320 RETURN     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:15020
# 其它的一律转发给15006
    0     0 ISTIO_IN_REDIRECT  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0           

Chain ISTIO_IN_REDIRECT (3 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 REDIRECT   tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            redir ports 15006

Chain ISTIO_OUTPUT (1 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 RETURN     all  --  *      lo      127.0.0.6            0.0.0.0/0           
    0     0 ISTIO_IN_REDIRECT  all  --  *      lo      0.0.0.0/0           !127.0.0.1            owner UID match 1337
    0     0 RETURN     all  --  *      lo      0.0.0.0/0            0.0.0.0/0            ! owner UID match 1337
    5   300 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0            owner UID match 1337
    0     0 ISTIO_IN_REDIRECT  all  --  *      lo      0.0.0.0/0           !127.0.0.1            owner GID match 1337
    0     0 RETURN     all  --  *      lo      0.0.0.0/0            0.0.0.0/0            ! owner GID match 1337
    0     0 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0            owner GID match 1337
    0     0 RETURN     all  --  *      *       0.0.0.0/0            127.0.0.1           
    0     0 ISTIO_REDIRECT  all  --  *      *       0.0.0.0/0            0.0.0.0/0           

Chain ISTIO_REDIRECT (1 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 REDIRECT   tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            redir ports 15001</pre>
<p>可以看到，REDIRECT模式下，处理进入封包的逻辑是完全一样的。 </p>
<p>REDIRECT模式下，将入站流量重定向给15006，这很好理解，因为15006是 virtualInbound监听器嘛。</p>
<div class="blog_h3"><span class="graybg">有何区别</span></div>
<p>从Nginx的日志上看，不管是REDIRECT还是TPROXY模式，看到的IP都不是真实IP，没有区别。</p>
<p>Envoy访问日志也没有任何区别，至少可以说，在REDIRECT模式下，Envoy也是可以看到真实源IP的：</p>
<pre class="crayon-plain-tag"># 开始时间                请求方法 原始地址  协议 响应码 响应标记
[2020-04-22T12:52:23.278Z] "GET  /    HTTP/1.1" 200   - 
  # 元数据mixer状态  # 上游传输失败原因   接受字节数   发送字节数  耗时 上游访问耗时
  "-"                "-"                 0            612         0 0 
  # x-forwarded-for头     User Agent
  "-"                   "curl/7.67.0" 
  # 请求ID                                AUTHORITY  上游主机
  "d05b5196-c413-9003-be2a-6b2841efe4e1" "nginx" "127.0.0.1:80" 
  # 上游集群
  inbound|80|http|nginx.default.svc.k8s.gmem.cc 
  # 访问上游使用的本地地址    下游访问本机使用目的地址   下游远程地址
  127.0.0.1:33024          172.27.155.70:80        172.27.155.74:45326 
  # 请求的服务名称                               路由名称
  outbound_.80_._.nginx.default.svc.k8s.gmem.cc default</pre>
<p>TPROXY模式下，Envoy也没有使用真实源IP来请求上游集群。</p>
<p>感觉这TPROXY很鸡肋，从<a href="https://github.com/istio/istio/issues/5679">https://github.com/istio/istio/issues/5679</a>上看到的，它的价值是： </p>
<blockquote>
<p>Contrary to REDIRECT, TPROXY doesn't perform NAT, and therefore preserves both source and destination IP addresses and ports of inbound connections. One benefit is that the source.ip attributes reported by Mixer for inbound connections will always be correct, unlike when using REDIRECT.</p>
</blockquote>
<p>也就是说，TPROXY模式下允许Mixer获得真实源IP地址。</p>
<div class="blog_h2"><span class="graybg">EnvoyFilter</span></div>
<p>目前Istio支持一种自定义资源EnvoyFilter，使用它，你可以对生成的Envoy配置进行深度定制。比如添加监听器过滤器：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: nginx-original-src
  namespace: default
spec:
  workloadSelector:
    labels:
      app: nginx
  configPatches:
  - applyTo: LISTENER
    match:
      context: SIDECAR_INBOUND
      listener:
        portNumber: 80
    patch:
      operation: MERGE
      value:
        listenerFilters:
        - name: envoy.listener.original_src</pre>
<p>像上面这个过滤器，它为入站监听器添加了envoy.listener.original_src这个监听器过滤器。生成的配置如下：</p>
<pre class="crayon-plain-tag">// istioctl proxy-config listener nginx-84c66c7fb9-7mfwz   --port 80 --type http -o json

...
        "deprecatedV1": {
            "bindToPort": false
        },
        "listenerFilters": [
            {
                "name": "envoy.listener.tls_inspector"
            },
            {
                "name": "envoy.listener.original_src",
            }
        ],
        "listenerFiltersTimeout": "0.100s",
        "continueOnListenerFiltersTimeout": true,
        "trafficDirection": "INBOUND"
    }
]</pre>
<div class="blog_h1"><span class="graybg">如何实现透明代理</span></div>
<div class="blog_h2"><span class="graybg">原理</span></div>
<div class="blog_h3"><span class="graybg">典型场景</span></div>
<p><a href="https://cdn.gmem.cc/wp-content/uploads/2020/04/tproxy-classic.png"><img class="size-full wp-image-32083 aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2020/04/tproxy-classic.png" alt="tproxy-classic" width="830" height="345" /></a></p>
<p>关键点：</p>
<ol>
<li>路由器发现目的地址、源地址是REAL_SERVER:80的，且不是来自透明代理的封包，都会路由给透明代理。而不是路由给服务器、客户端</li>
<li>透明代理能够在非本机IP地址上监听，例如REAL_SERVER:80</li>
<li>透明代理能够以非本机IP地址发起TCP连接，例如以客户端的IP地址</li>
</ol>
<p>第一条，可能需要硬件支持。</p>
<p>后面两条，可以由透明代理在软件上支持，相关套接字选项：</p>
<ol>
<li>IP_FREEBIND：允许绑定非本地的，或者尚不存在的IP地址</li>
<li>IP_TRANSPARENT：在套接字上启用透明代理。该选项运行应用程序绑定非本地地址，并使用这个外部地址来扮演客户端、服务器角色。需要CAP_NET_ADMIN权限才能启用</li>
</ol>
<p>此外，根据实际需要，“透明度”可以变化：</p>
<ol>
<li>如果仅仅想让客户端觉得透明，那么代理可以直接使用自己的IP地址请求服务器。这样服务器看不到客户端真实IP</li>
<li>如果服务器仅仅需要知道客户端真实IP，不关心真实端口，那么代理可以用客户端地址+任意端口发起请求</li>
<li>如果需要绝对透明，则代理必须以客户端地址+客户端端口发起请求</li>
</ol>
<div class="blog_h3"><span class="graybg">Sidecar场景</span></div>
<p>在Envoy Sidecar部署场景下，情况变的简单，透明代理和服务器位于同一台主机内部，这意味着：</p>
<ol>
<li>不需要路由器/网关的配合</li>
<li>代理请求的目的地址可以从真实服务器地址换为127.0.0.1</li>
</ol>
<p>可以实现透明代理的通信模型如下：</p>
<p><a href="https://cdn.gmem.cc/wp-content/uploads/2020/04/tproxy-envoy-expected.png"><img class="size-full wp-image-32085 aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2020/04/tproxy-envoy-expected.png" alt="tproxy-envoy-expected" width="465" height="760" /></a></p>
<div class="blog_h2"><span class="graybg">Istio的问题</span></div>
<p>在Istio的TPROXY拦截模式下，实际的通信模型如下：</p>
<p><a href="https://cdn.gmem.cc/wp-content/uploads/2020/04/tproxy-envoy-actual.png"><img class="size-full wp-image-32089 aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2020/04/tproxy-envoy-actual.png" alt="tproxy-envoy-actual" width="463" height="756" /></a></p>
<p>&nbsp;</p>
<p>差别似乎仅仅是Envoy用127.0.0.1作为源地址，而非客户端真实IP，向服务器发送请求。</p>
<p>使用EnvoyFilter，为virtualOutbound所引用的，80监听器配置一个EnvoyFilter，配置envoy.listener.original_src，可以让Envoy访问服务器时使用真实客户端IP，解决我们的问题吗？</p>
<p>我们参考3.2节配置好EnvoyFilter，然后从外部访问Pod的Nginx服务，很遗憾，并不能正常工作，curl给出的错误是：</p>
<pre class="crayon-plain-tag">upstream connect error or disconnect/reset before headers. reset reason: connection failure</pre>
<p>从Envoy访问日志上看：</p>
<pre class="crayon-plain-tag">[2020-04-23T09:18:42.434Z] "GET / HTTP/1.1" 503 
# 日志格式取决于配置/版本。通过
#   kubectl exec nginx-tproxy-774fb7958c-t2lnk -c istio-proxy -- curl 0:15000/config_dump | grep .log_format
# 响应标记：
#   LR   本地重置
#   UH   没有健康的上游主机，和503一起发送
#   UF   连接到上游主机时失败，和503一起发送
#   UO   针对上游的访问溢出（断路器触发），和503一起发送
#   NR   没有匹配的路由，和404一起发送
#   URX  请求被拒绝，原因是超过上游的最大重试次数，或者TCP最大连接尝试次数

#  上游连接失败     收  发   耗时
   UF "-" "-"      0   91   999   - "-" "curl/7.67.0" "747cdfcb-5d1e-9ac0-8858-33aa1b1eaa4d" 
"nginx" "127.0.0.1:80" inbound|80|http|nginx.default.svc.k8s.gmem.cc 
# 访问上游使用的本地地址    下游访问本机使用目的地址   下游远程地址
-                       172.27.155.94:80         172.27.155.90:56356 outbound_.80_._.nginx.default.svc.k8s.gmem.cc default</pre>
<p>存在如下异常：</p>
<ol>
<li>访问上游时使用的源地址为空了</li>
<li>响应标记UF，耗时999，提示连接不到上游服务器 </li>
</ol>
<p>为什么连接不到上游服务器？我们尝试通过iptables日志诊断一下。在Nginx的例子里，数据报的特点是，源或目的端口为80，因此增加以下规则：</p>
<pre class="crayon-plain-tag"># 删除基于UID匹配的规则，因为TPROXY模式下Envoy的运行用户是0而非1337
iptables -t nat -D ISTIO_OUTPUT 2
iptables -t nat -D ISTIO_OUTPUT 2
iptables -t nat -D ISTIO_OUTPUT 2

# 增加入站流量TPROXY规则日志
iptables -t mangle -I ISTIO_INBOUND 5 -p tcp --dport 80 -j LOG --log-prefix "b-tproxy: " --log-tcp-sequence --log-uid
iptables -t mangle -A ISTIO_INBOUND -p tcp --dport 80 -j LOG --log-prefix "a-tproxy: " --log-tcp-sequence --log-uid

# 在nat表的OUTPUT链，需要增加源、目标端口是80的，分别对应服务向Envoy发出、Envoy向服务发出的封包
iptables -t nat -I ISTIO_OUTPUT 6 -p tcp --dport 80 -j LOG --log-prefix 't-redir-*-*-*-*: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 6 -p tcp --sport 80 -j LOG --log-prefix 'f-redir-*-*-*-*: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 5 -p tcp --dport 80 -j LOG --log-prefix 't-rturn-*-*-*-1: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 5 -p tcp --sport 80 -j LOG --log-prefix 'f-rturn-*-*-*-1: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 4 -p tcp --dport 80 -j LOG --log-prefix 't-rturn-*-*-*-*-1337: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 4 -p tcp --sport 80 -j LOG --log-prefix 'f-rturn-*-*-*-*-1337: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 3 -p tcp --dport 80 -j LOG --log-prefix 't-rturn-*-l-*-*-!1337: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 3 -p tcp --sport 80 -j LOG --log-prefix 'f-rturn-*-l-*-*-!1337: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 2 -p tcp --dport 80 -j LOG --log-prefix 't-inred-*-l-*-!1-1337: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 2 -p tcp --sport 80 -j LOG --log-prefix 'f-inred-*-l-*-!1-1337: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 1 -p tcp --dport 80 -j LOG --log-prefix 't-inred-*-l-6-*: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 1 -p tcp --sport 80 -j LOG --log-prefix 'f-inred-*-l-6-*: ' --log-tcp-sequence --log-uid</pre>
<p>拦截到的日志：</p>
<pre class="crayon-plain-tag"># [30714.928765] 客户端往POD的连接，首次SYN，TPROXY之前
b-tproxy: IN=eth0 OUT=  SRC=172.27.155.90 DST=172.27.155.108     ID=35338   SPT=57252 DPT=80 SEQ=1901693983    SYN  
# 没有出现a-tproxy，说明SYN被TPROXY拦截，发往15001，也就是Envoy

# Envoy往Nginx的连接，出站，首次SYN，注意看到SRC是172.27.155.90:44297，和客户端172.27.155.90:57252的IP一致，端口用了新的
# 没有启用EnvoyFilter时是这样：
# inred-*-l-6-*: IN= OUT=lo SRC=127.0.0.1 DST=127.0.0.1 ...
# 可以看到EnvoyFilter达到我们的目的：传递真实源IP

t-inred-*-l-6-*: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=33217   SPT=44297 DPT=80 SEQ=147818454    SYN  UID=0 GID=1337 
t-inred-*-l-*-!1-1337: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=33217   SPT=44297 DPT=80 SEQ=147818454    SYN  UID=0 GID=1337 
t-rturn-*-l-*-*-!1337: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=33217   SPT=44297 DPT=80 SEQ=147818454    SYN  UID=0 GID=1337 
# 由于GID是1337，因此下面的规则匹配，ACCEPT，封包发出去了
t-rturn-*-*-*-*-1337: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=33217   SPT=44297 DPT=80 SEQ=147818454    SYN  UID=0 GID=1337 

# Envoy往Nginx的连接，入站，由于目的地址是127.0.0.1，因此不TPROXY
b-tproxy: IN=lo OUT=  SRC=172.27.155.90 DST=127.0.0.1     ID=33217   SPT=44297 DPT=80 SEQ=147818454    SYN  
a-tproxy: IN=lo OUT=  SRC=172.27.155.90 DST=127.0.0.1     ID=33217   SPT=44297 DPT=80 SEQ=147818454    SYN  



# [30715.971504] 一秒过了，客户端往POD的连接，二次SYN
b-tproxy: IN=eth0 OUT=  SRC=172.27.155.90 DST=172.27.155.108     ID=60309   SPT=57258 DPT=80 SEQ=829877388    SYN  
# Envoy往Nginx的连接，出站，二次SYN
t-inred-*-l-6-*: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=51130   SPT=51761 DPT=80 SEQ=3083321507    SYN  UID=0 GID=1337 
t-inred-*-l-*-!1-1337: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=51130   SPT=51761 DPT=80 SEQ=3083321507    SYN  UID=0 GID=1337 
t-rturn-*-l-*-*-!1337: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=51130   SPT=51761 DPT=80 SEQ=3083321507    SYN  UID=0 GID=1337 
t-rturn-*-*-*-*-1337: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=51130   SPT=51761 DPT=80 SEQ=3083321507    SYN  UID=0 GID=1337 
b-tproxy: IN=lo OUT=  SRC=172.27.155.90 DST=127.0.0.1     ID=51130   SPT=51761 DPT=80 SEQ=3083321507    SYN  
a-tproxy: IN=lo OUT=  SRC=172.27.155.90 DST=127.0.0.1     ID=51130   SPT=51761 DPT=80 SEQ=3083321507    SYN  

# [30717.046657] 一秒过了，客户端往POD的连接，三次SYN
b-tproxy: IN=eth0 OUT=  SRC=172.27.155.90 DST=172.27.155.108     ID=8963   SPT=57268 DPT=80 SEQ=3705219877    SYN  
t-inred-*-l-6-*: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=12974   SPT=48739 DPT=80 SEQ=2548447881    SYN  UID=0 GID=1337 
t-inred-*-l-*-!1-1337: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=12974   SPT=48739 DPT=80 SEQ=2548447881    SYN  UID=0 GID=1337 
t-rturn-*-l-*-*-!1337: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=12974   SPT=48739 DPT=80 SEQ=2548447881    SYN  UID=0 GID=1337 
t-rturn-*-*-*-*-1337: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=12974   SPT=48739 DPT=80 SEQ=2548447881    SYN  UID=0 GID=1337 
b-tproxy: IN=lo OUT=  SRC=172.27.155.90 DST=127.0.0.1     ID=12974   SPT=48739 DPT=80 SEQ=2548447881    SYN  
a-tproxy: IN=lo OUT=  SRC=172.27.155.90 DST=127.0.0.1     ID=12974   SPT=48739 DPT=80 SEQ=2548447881    SYN</pre>
<p>可以看到：</p>
<ol>
<li>客户端向Pod发请求，被TPROXY给Envoy 15001</li>
<li>Envoy 15001是透明套接字，因此它虽然客户端请求的DPT=80，它也接收并处理了</li>
<li>Envoy执行代理，通过lo向127.0.0.1:80发送请求，注意这里<span style="background-color: #c0c0c0;">它使用的源地址是客户端地址，这意味着我们的EnvoyFilter起作用了</span></li>
<li>Envoy代理的请求，通过lo入站，由于目的地址是127.0.0.1，因此不被TPROXY，通过PREROUTING - mangle链</li>
</ol>
<p>此外，在OUTPUT链中nat表里，好像根据SPT=80无法匹配，所以看不到任何f-开头的日志。此链对于nat表来说，应该是用于做DNAT，Istio生成的规则遵循了这一点，REDIRECT可以看作是一种DNAT。Istio的规则有基于源IP进行匹配的，我基于源端口为何不行，目前不清楚。</p>
<p>换个位置来诊断吧，目前我们已经明确，Envoy接收到请求后，会冒充客户端源IP向localhost:80发请求，此请求已经通过PREROUTING-mangle。它有没有被Nginx接收到？</p>
<p>我们可以在INPUT-mangle上做日志，如果能监控到发往127.0.0.1:80的封包，就可以认定Nginx接收到了，因为整个Iptables中没有设置INPUT链的任何拦截规则。</p>
<pre class="crayon-plain-tag">iptables -t mangle -I INPUT 1 -p tcp -d 127.0.0.1/32 --dport 80 -j LOG --log-prefix='input-mangle-d80: '
iptables -t nat -I INPUT 1 -p tcp -d 127.0.0.1/32  --dport 80 -j LOG --log-prefix='input-nat-d80: '
iptables -t filter -I INPUT 1 -p tcp -d 127.0.0.1/32  --dport 80 -j LOG --log-prefix='input-filter-d80: '</pre>
<p>日志如下：</p>
<pre class="crayon-plain-tag">[3612374.269256] input-mangle-d80: IN=lo OUT= SRC=172.27.252.159 DST=127.0.0.1 SPT=40283 DPT=80 SYN
[3612374.269276] input-filter-d80: IN=lo OUT= SRC=172.27.252.159 DST=127.0.0.1 SPT=40283 DPT=80 SYN</pre>
<p>nat表仍然没有日志，<span style="background-color: #c0c0c0;">看样子是在DNAT时，不能使用源端口匹配，SNAT时，不能使用目的端口匹配</span>。</p>
<p>不过从日志上，从lo端口进入的、Envoy仿冒客户端身份发往127.0.0.1:80的封包，的确是通过iptables了。</p>
<p>那么，应该是Nginx没有给出应答。我们需要监控一下源是Nginx，目的是客户端真实IP地址的出站封包的流向： </p>
<pre class="crayon-plain-tag">iptables -t mangle -R POSTROUTING 1 -p tcp -d 172.27.252.159/32 -s 127.0.0.1/32 \
         --sport 80  -j LOG --log-prefix='pr-mangle-to-clientip: '</pre>
<p>日志如下：</p>
<pre class="crayon-plain-tag">pr-mangle-to-clientip: IN= OUT=eth0 SRC=127.0.0.1 DST=172.27.252.159 PROTO=TCP SPT=80 DPT=54969 ACK SYN URGP=0 
pr-mangle-to-clientip: IN= OUT=eth0 SRC=127.0.0.1 DST=172.27.252.159 PROTO=TCP SPT=80 DPT=50979 ACK SYN URGP=0 
pr-mangle-to-clientip: IN= OUT=eth0 SRC=127.0.0.1 DST=172.27.252.159 PROTO=TCP SPT=80 DPT=54969 ACK SYN URGP=0</pre>
<p>相似的日志会连续出现很多条。我们可以看到Nginx收到首次握手SYN后，尝试ACK+SYN，但是一致没有收到第三次握手信息…… 原因很明显，<span style="background-color: #c0c0c0;">出口网卡是eth0，封包发走了，没有返回给Envoy代理</span>。</p>
<p>到这里，问题就算定位完毕了。 </p>
<div class="blog_h2"><span class="graybg">解决方案</span></div>
<p>我们需要保证，对于Envoy以客户端IP发起的，给Nginx的请求，它的响应能够原路返回。响应的封包具有以下特点：</p>
<ol>
<li>源地址（请求封包的目的地址）是 127.0.0.1，因为Envoy总是向127.0.0.1发请求</li>
<li>目的地址（请求封包的源地址）不是本机地址，因为Envoy发请求时，FREEBIND源地址为客户端IP</li>
</ol>
<p>我们需要将这种封包，从lo网卡，而非eth0路由出去。 可以使用下面的iptables规则：</p>
<pre class="crayon-plain-tag">iptables -t mangle -I OUTPUT 1 -s 127.0.0.1/32 ! -d 127.0.0.1/32 \
    -j MARK --set-xmark 0x539/0xffffffff</pre>
<p>再次访问服务，Nginx可以看到真实客户端IP地址了：</p>
<pre class="crayon-plain-tag"># TPORXY mode without envoyfilter
127.0.0.1 - - [24/Apr/2020:02:58:53 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.67.0" "-"
# TPROXY mode + envoyfilter, iptable rule applied
172.27.252.159 - - [24/Apr/2020:05:52:04 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.67.0" "-"</pre>
<p>到此为止，问题解决。初步测试，没有发现负面效果，<a href="https://github.com/istio/istio/issues/5679#issuecomment-618824519">已经将此方案提交社区讨论</a>。</p>
<div class="blog_h3"><span class="graybg">提交社区</span></div>
<p>此方案已经通过PR <a href="https://github.com/istio/istio/pull/23275">https://github.com/istio/istio/pull/23275</a> 合并到上游Istio仓库的master分支（1.7dev），并将<a href="https://github.com/istio/istio/pull/23446">自动Cherry Pick到1.6版本</a>。</p>
<p>1.5版本的逻辑稍有不同，仅仅在我Fork的Istio中实现：<a href="https://github.com/gmemcc/istio/tree/release-1.5.1-patch">https://github.com/gmemcc/istio/tree/release-1.5.1-patch</a>，不准备提交到上游Istio仓库。</p>
<div class="blog_h3"><span class="graybg">1.6版本TPROXY问题</span></div>
<p>在此版本中验证时，发现TPROXY模式损坏，无限循环自我请求。我已经提起Issue：<a href="https://github.com/istio/istio/issues/23369">23369</a>。</p>
<p>解决无限循环的方法是把TPROXY目标从15001改为15006。我一直就怀疑为什么要把入站流量重定向给出站监听器15001，现在想想，最初只有一个“虚拟监听器”15001，最近版本的Istio才拆分为virtualInbound（15006）、virtualOutbound（15001）两个，在这个变更过程中，TPROXY相关代码没有跟着改动。</p>
<div class="blog_h1"><span class="graybg">问题23369</span></div>
<p>解决透明代理源IP的PR 23275并没有达到预期效果，问题原因参考ISSUE 23369。</p>
<p>即使按照上节的方法，将TPROXY目标从15001改为15006，也仅仅能解决无限自我请求的问题。新得到的错误信息是：upstream connect error or disconnect/reset before headers. reset reason: local reset</p>
<div class="blog_h2"><span class="graybg">抓包分析</span></div>
<p>我们从10.0.0.1发起针对启用了Sidecar的、IP地址为172.27.0.10的请求。可以在Nginx Pod的网络命名空间中看到如下连接信息：</p>
<pre class="crayon-plain-tag">netstat -nt
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      1 10.0.0.1:50829          172.27.0.10:80          SYN_SENT</pre>
<p>源地址为10.0.0.1:50829套接字，应该是Envoy发起上游请求时创建的，因为我们配置了监听器过滤器original_src。</p>
<p>但是，这个套接字的状态一直是SYN_SENT，这提示它没有收到答复。结合抓包结果：</p>
<pre class="crayon-plain-tag"># 从客户端发起的原始包，源端口39062
10.0.0.1.39062 &gt; 172.27.0.10.80: Flags [S]
# Envoy给的ACK
172.27.0.10.80 &gt; 10.0.0.1.39062: Flags [S.]
# 客户端发起HTTP请求
10.0.0.1.39062 &gt; 172.27.0.10.80: Flags [.]
10.0.0.1.39062 &gt; 172.27.0.10.80: Flags [P.] GET / HTTP/1.1
# Envoy给的ACK
172.27.0.10.80 &gt; 10.0.0.1.39062
# Envoy向上游发起请求，注意这里它不是发给127.0.0.1，而是Pod IP
# 尽管目的地址是172.27.0.10.80，这个包仍然是从lo发出去的
# 当从本机访问时，不论使用哪个目的IP时，默认都会从lo出去
10.0.0.1.50829 &gt; 172.27.0.10.80: Flags [S]
# Nginx给出ACK，但是这个ACK没有收到，所以SYN+ACK反复了几次
# 实际上这些封包都从eth0发出去了
172.27.0.10.80 &gt; 10.0.0.1.50829: Flags [S.]
10.0.0.1.50829 &gt; 172.27.0.10.80: Flags [S]
172.27.0.10.80 &gt; 10.0.0.1.50829: Flags [S.]
172.27.0.10.80 &gt; 10.0.0.1.50829: Flags [S.]
10.0.0.1.50829 &gt; 172.27.0.10.80: Flags [S]
172.27.0.10.80 &gt; 10.0.0.1.50829: Flags [S.]
172.27.0.10.80 &gt; 10.0.0.1.50829: Flags [S.]
10.0.0.1.50829 &gt; 172.27.0.10.80: Flags [S]
172.27.0.10.80 &gt; 10.0.0.1.50829: Flags [S.]
# Envoy没有收到上游应答，认为服务不可用
172.27.0.10.80 &gt; 10.0.0.1.39062  HTTP/1.1 503 Service Unavailable
# 终止连接
10.0.0.1.39062 &gt; 172.27.0.10.80: Flags [.]
10.0.0.1.39062 &gt; 172.27.0.10.80: Flags [F.]</pre>
<p>可以看到，新版本的Istio，向上游发请求时，使用的目的地址是原始Dest地址，而不是127.0.0.1，因此， PR 23275也就失效了。</p>
<p>在当前的场景下，Envoy以客户端真实IP、通过lo向Nginx进程发起TCP连接，这个是OK的。但是回程报文从容器eth0发走了。回程报文到达宿主机后，被丢弃。</p>
<div class="blog_h2"><span class="graybg">解决方案</span></div>
<p>我们需要识别，哪些请求是Envoy代表客户端转发的，并把这些请求的响应封包发回给Envoy，而不是通过eth0发送出去。</p>
<p>早前版本可以根据目的地址识别，现在直接来自客户端的、Envoy代表客户端转发的请求（以及响应），连接5元组完全一样，这意味着无法从IP地址上进行区分了。</p>
<p>幸运的是，iptables支持的CONNMARK目标可以在连接级别上打标记，这意味着往返报文可以共享信息。此外，original_src支持为封包设置标记，我们可以利用这一特性识别Envoy代表客户端发出的封包。结合这两点，我们可以得到23369的解决方案。</p>
<p>首先，我们需要为监听器过滤器original_src增加一个参数：</p>
<pre class="crayon-plain-tag">{
    "name": "envoy.listener.original_src",
    "typedConfig": {
        "@type": "type.googleapis.com/envoy.extensions.filters.listener.original_src.v3.OriginalSrc",
        "mark": 1337
    }
},</pre>
<p>这样，Envoy请求上游（Nginx）时，发出的封包具有标记 1337。</p>
<p>然后，我们增加如下iptables规则：</p>
<pre class="crayon-plain-tag"># Envoy发出的封包，被Nginx处理之前，获取封包标记，保存为连接标记
iptables  -t mangle -I PREROUTING -m mark     --mark 1337  -j CONNMARK --save-mark
# Nginx处理请求...
# Nginx返回的响应封包，被打上从连接标记上取得的1337标记
iptables  -t mangle -I OUTPUT     -m connmark --mark 1337 -j CONNMARK --restore-mark</pre>
<p>结合现有的策略路由，Nginx的回程封包就会从lo发出，并被Envoy接收到了。</p>
<p>到这一步，会出现先前的无限自我请求问题，这是由于规则：</p>
<pre class="crayon-plain-tag">Chain ISTIO_TPROXY (1 references)
 pkts bytes target     prot opt in     out     source               destination         
    8   480 TPROXY     tcp  --  *      *       0.0.0.0/0           !127.0.0.1            TPROXY redirect 0.0.0.0:15006 mark 0x539/0xffffffff</pre>
<p>该规则要求，只要目的地址不是127.0.0.1的请求，都会重定向到15006。在前面我们已经发现，TPROXY模式下访问上游Nginx不像先前版本那样使用127.0.0.1作为目的地址，因此这个规则必须要处理。</p>
<p>我的做法是，在它的前面做个判断，如果具有标记1337（意味着这是Envoy和上游Nginx之间的通信），就不走ISTIO_TPROXY：</p>
<pre class="crayon-plain-tag">iptables -t mangle -I ISTIO_INBOUND 5 -p tcp -m mark --mark 0x539   -j RETURN</pre>
<p>修改后mangle表的整体内容如下：</p>
<pre class="crayon-plain-tag"># iptables -t mangle -L -n -v
Chain PREROUTING (policy ACCEPT 6280 packets, 680K bytes)
 pkts bytes target     prot opt in     out     source               destination         
1163K   97M CONNMARK   all  --  *      *       0.0.0.0/0            0.0.0.0/0            mark match 0x539 CONNMARK save
1440K  115M ISTIO_INBOUND  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0           

Chain INPUT (policy ACCEPT 7459 packets, 817K 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 6126 packets, 781K bytes)
 pkts bytes target     prot opt in     out     source               destination         
1107K   93M CONNMARK   all  --  *      *       0.0.0.0/0            0.0.0.0/0            connmark match  0x539 CONNMARK restore
    0     0 MARK       tcp  --  *      *       127.0.0.1           !127.0.0.1            MARK set 0x539

Chain POSTROUTING (policy ACCEPT 6126 packets, 781K bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain ISTIO_DIVERT (1 references)
 pkts bytes target     prot opt in     out     source               destination         
1308K  107M MARK       all  --  *      *       0.0.0.0/0            0.0.0.0/0            MARK set 0x539
1308K  107M ACCEPT     all  --  *      *       0.0.0.0/0            0.0.0.0/0           

Chain ISTIO_INBOUND (1 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 RETURN     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22
    0     0 RETURN     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:15090
14058 1047K RETURN     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:15021
 4713  814K RETURN     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:15020
   39  7165 RETURN     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            mark match 0x539
1308K  107M ISTIO_DIVERT  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
 113K 6778K ISTIO_TPROXY  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0           

Chain ISTIO_TPROXY (1 references)
 pkts bytes target     prot opt in     out     source               destination         
    8   480 TPROXY     tcp  --  *      *       0.0.0.0/0           !127.0.0.1            TPROXY redirect 0.0.0.0:15006 mark 0x539/0xffffffff</pre>
<p>从Nginx Pod外部访问、从Nginx Pod内部访问localhost以及Pod IP，一切行为正常，解决方案有效。 </p>
<p>此方案将通过<a href="https://github.com/istio/istio/pull/28363">PR 28363</a>提交社区讨论。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/istio-tproxy">Istio中的透明代理问题</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/istio-tproxy/feed</wfw:commentRss>
		<slash:comments>10</slash:comments>
		</item>
		<item>
		<title>服务网格的现状和未来</title>
		<link>https://blog.gmem.cc/the-present-and-future-of-servicemesh</link>
		<comments>https://blog.gmem.cc/the-present-and-future-of-servicemesh#comments</comments>
		<pubDate>Mon, 13 Apr 2020 04:10:44 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[ServiceMesh]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=31771</guid>
		<description><![CDATA[<p>引言 服务网格（Service Mesh）是一种微服务治理基础设施，用于控制、监测微服务之间的东西向流量。它通常由控制平面、数据平面两部分组成。其中数据平面就是伴随着业务应用部署的网络代理，控制平面则是一组独立的组件，和数据平面交互，发送控制网络流量的规则，接收各类监测指标。业务应用的开发人员对服务网格的存在并无感知，这是服务网格最关键的优势。 服务网格的概念出现于2010年代早期，2017年前后开始风行。从ServiceMesher社区在2020年2月发起的终端用户调研结果来看，在生产环境下使用该技术的公司占比不到15%，大部分公司仍然在观望： 是什么原因阻碍了服务网格的大面积应用，它的未来会如何？本文将从主要从技术角度来细化分析。 现状和问题 社区发展情况 Kubernetes早已成为容器编排领域的事实标准，主流服务网格框架都选择构筑在Kubernetes之上。Kubernetes和服务网格是相辅相成的，尽管功能上有些许重叠。Kubernetes主要专注于应用的部署，服务网格更关注应用的运行时管理。 目前占据着领导地位的服务网格框架是Istio + Envoy的组合。早期的Linkerd运行在JVM之上，资源消耗较高，已经被Linkerd 2所取代。Linkerd 2的进展不尽如人意，支持的特性比Istio要少很多。国内的参与者，包括阿里的SOFAMesh、华为的ASM等，这些框架一部分没有开源，另一部分更新较为缓慢，SOFAMesh的代码库2020年尚未有新代码合并到主干。 从ServiceMesher社区在2020年2月发起的终端用户调研结果来看，Istio和Envoy是用户关注度最高的服务网格开源项目： 要打造自研服务网格产品，理性的方案是以一种成熟的社区技术为基础，这是因为服务网格牵涉到的技术面很广，从零开发的成本非常高。目前来看，Istio + Envoy是较好的选择，包括AWS App Mesh、F5 Aspen Mesh等商业方案都是在Istio / Envoy的基础上进行深入定制实现的。 <a class="read-more" href="https://blog.gmem.cc/the-present-and-future-of-servicemesh">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/the-present-and-future-of-servicemesh">服务网格的现状和未来</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">引言</span></div>
<p>服务网格（Service Mesh）是一种微服务治理基础设施，用于控制、监测微服务之间的东西向流量。它通常由控制平面、数据平面两部分组成。其中数据平面就是伴随着业务应用部署的网络代理，控制平面则是一组独立的组件，和数据平面交互，发送控制网络流量的规则，接收各类监测指标。业务应用的开发人员对服务网格的存在并无感知，这是服务网格最关键的优势。</p>
<p>服务网格的概念出现于2010年代早期，2017年前后开始风行。从ServiceMesher社区在2020年2月发起的终端用户调研结果来看，在生产环境下使用该技术的公司占比不到15%，大部分公司仍然在观望：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/04/service-mesh-usage.png"><img class="aligncenter  wp-image-31779" src="https://blog.gmem.cc/wp-content/uploads/2020/04/service-mesh-usage.png" alt="service-mesh-usage" width="1024" height="311" /></a></p>
<p>是什么原因阻碍了服务网格的大面积应用，它的未来会如何？本文将从主要从技术角度来细化分析。</p>
<div class="blog_h1"><span class="graybg">现状和问题</span></div>
<div class="blog_h2"><span class="graybg">社区发展情况</span></div>
<p>Kubernetes早已成为容器编排领域的事实标准，主流服务网格框架都选择构筑在Kubernetes之上。Kubernetes和服务网格是相辅相成的，尽管功能上有些许重叠。Kubernetes主要专注于应用的部署，服务网格更关注应用的运行时管理。</p>
<p>目前占据着领导地位的服务网格框架是Istio + Envoy的组合。早期的Linkerd运行在JVM之上，资源消耗较高，已经被Linkerd 2所取代。Linkerd 2的进展不尽如人意，支持的特性比Istio要少很多。国内的参与者，包括阿里的SOFAMesh、华为的ASM等，这些框架一部分没有开源，另一部分更新较为缓慢，SOFAMesh的代码库2020年尚未有新代码合并到主干。</p>
<p>从ServiceMesher社区在2020年2月发起的终端用户调研结果来看，Istio和Envoy是用户关注度最高的服务网格开源项目：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/04/focused-service-mesh-tech.png"><img class="aligncenter  wp-image-31785" src="https://blog.gmem.cc/wp-content/uploads/2020/04/focused-service-mesh-tech.png" alt="focused-service-mesh-tech" width="1012" height="323" /></a></p>
<p>要打造自研服务网格产品，理性的方案是以一种成熟的社区技术为基础，这是因为服务网格牵涉到的技术面很广，从零开发的成本非常高。目前来看，Istio + Envoy是较好的选择，包括AWS App Mesh、F5 Aspen Mesh等商业方案都是在Istio / Envoy的基础上进行深入定制实现的。</p>
<div class="blog_h2"><span class="graybg">标准化问题</span></div>
<p>2019年5月，微软联合一系列厂商发布了一个服务网格规范（Service Mesh Interface，SMI），它是一个基于Kubernetes的服务网格接口标准，致力于实现不同服务网格框架的互操作性。</p>
<p>巨头们早已看到了服务网格的价值，并且纷纷出手，支持或参与开源项目，推出自己的商业产品。百花齐放的服务网格产品，接口不一致，必然会导致供应商锁定问题，一旦选择了某个厂商的服务网格产品，你将很难切换到另一家。SMI需要解决的就是供应商锁定问题，微软、Vmware、HashiCorp、F5等都加入了SMI阵营。遗憾的是，社区的领头羊，Istio阵营的Google、IBM等却对SMI不感兴趣，个中利益纷争耐人寻味。</p>
<p>标准化的分歧，的确会导致技术选型相关的风险，因为我们难以准确判断中长期的技术方向。为了规避这类风险，自研服务网格产品应该面向上层应用暴露技术中立的API，避免在API上和某种服务网格实现耦合。</p>
<div class="blog_h2"><span class="graybg">遗留技术</span></div>
<p>我已经在多个团队中目睹推行服务网格甚至是Kubernetes时遇到强大的阻力，原因是这些团队已经在使用“等位”技术，而且工作的不错。</p>
<p>这种阻力在以Java为单一技术栈的公司/团队中尤为明显。Java生态圈一直非常繁荣，自成体系。特别是Spring Cloud项目提供了服务发现、负载均衡、内容感知路由等特性，这些正和Kubernetes、Istio存在功能重叠，导致技术迁移的动力不足。</p>
<p>在决定引入服务网格技术之前，首先要确信自己的团队的确需要它。对于规模很小的公司/团队，答案往往是否。答案反转的一个重要的契机是，你提供的服务需要被别的团队所消费，且这个团队和你使用不一样的技术栈，例如使用Go而非Java。</p>
<p>另外需要注意一点，Spring Cloud和Kubernetes并非水火不容，它不但可以容器化部署在Kubernetes上，改用Kubernetes作为服务发现机制也非常简单。 事实上，Spring背后的VMware是云原生领域的重要参与者，Spring Cloud对Kubernetes的集成能力也在不断提高。服务网格属于PaaS层而非应用级的解决方案，它和Spring Cloud这类框架可以长期共同存在，平滑迁移。</p>
<p>服务网格的边缘代理，则又和API网关存在功能上的重叠。前些年在Kubernetes尚未普遍应用的时候，很多互联网开发团队就在自研API网关产品。我的建议是，现有的API网关产品可以继续使用，如果团队已经全面转向云原生，则不要新造轮子，可以基于Envoy构建API网关。</p>
<div class="blog_h2"><span class="graybg">性能问题</span></div>
<p>Istio的架构，在1.0版本之后已经经过多次重大调整，以平衡架构的优雅性和性能。1.1版本进一步微服务化，分离了 Pilot 的配置下发功能到新的 Galley 组件中，将 Mixer 组件中原本进程内插件改为进程外插件，这一版本加剧了性能问题。随后的版本又开始180度转弯，放弃架构之美，追求性能和实用。到1.5版本为止，Mixer已经废弃，遥测功能下沉到Envoy代理中，控制平面变成一个单体的istiod：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/04/istiod-archeture.jpg"><img class="aligncenter  wp-image-31797" src="https://blog.gmem.cc/wp-content/uploads/2020/04/istiod-archeture.jpg" alt="istiod-archeture" width="521" height="321" /></a></p>
<p>即使是在禁用Mixer，仅仅启用最基础的流量管理功能的情况下，Envoy引入的性能开销仍然需要关注。对于规模很大的命名空间，Envoy占用的内存会比较可观，甚至大于被代理的微服务本身。使用Istio提供的Sidecar CRD，可以对代理进行细粒度配置，很大程度上降低内存消耗。</p>
<p>在服务网格内部，每个请求都要经由一个客户端代理、一个服务端代理。在Istio 1.5版本的默认配置下的基准测试中，客户端代理的P99延迟大约为2.8ms，服务端代理的P99延迟大约为2.7ms，也就是一次微服务调用会可能会引入5.5ms的延迟。如果调用链比较复杂，引入的总延迟可能达到数十毫秒。大部分场景下，这样的延迟不是问题，但对于一些本身响应时间很短的HTTP服务，影响就难以忽略了。</p>
<p>Istio数据平面引入的延迟，主要有三个来源：</p>
<ol>
<li>代理本身的逻辑：L7代理逻辑较为复杂，执行这些逻辑需要CPU时间。如果扩展了自定义的过滤器，这些过滤器的性能很关键</li>
<li>流量重定向：Istio使用iptables重定向拦截Pod的全部流量给Enovy处理，这种重定向需要多次遍历TCP/IP栈，单次拦截可能引入0.3ms左右的延迟。基于Socket感知的BPF程序，例如Cilium CNI，可以消除这一延迟，但是对内核版本有要求</li>
<li>网络传输：不管是否引入代理，网络传输导致的延迟都存在。但是代理给我们带来了降低延迟的契机。QUIC可以代替TCP作为HTTP的底层协议，它具有更好的拥塞控制、多路复用、前向纠错、链接迁移特性，在网格的边缘这些特性具有显著的优势。Envoy已经逐步加入对QUIC的支持，当实现了QUIC Termination后，就能透明的将业务应用流量的底层传输协议更换为QUIC，提升性能</li>
</ol>
<div class="blog_h2"><span class="graybg">可扩展性</span></div>
<p>Envoy基于C++开发，扩展它需要编写自定义的过滤器。2019年12月进入W3C推荐标准的WebAssembly（Wasm）改变了这一状况。Wasm是一套可移植的字节码格式，你可以将任何主流语言编写的代码编译为Wasm字节码。Envoy现在已经能够支持Wasm Filter，这意味着扩展Envoy，和团队现有的系统进行集成，不会再遇到编程语言上的障碍，也不需要重新部署新版本的Enovy二进制文件。</p>
<p>尽管<a href="https://github.com/envoyproxy/envoy-wasm/">尚未合并到主干</a>，Istio 1.5版本附带的Envoy，已经包含了Alpha版本的Wasm Filter支持。</p>
<p>Istio的开发语言是Go，经典的扩展方式是Mixer插件，如今这种方式已经被废弃。 Mixer的两大功能，Check和Report，将分别由<a href="https://github.com/proxy-wasm/spec">Proxy-Wasm</a> plugins和Telemetry V2代替。Proxy-Wasm是一套ABI规范，规定了Envoy这样的L4/L7代理软件如何和它们的Wasm扩展进行交互。Istio 1.6将提供Proxy-Wasm plugins的统一配置API。 </p>
<div class="blog_h2"><span class="graybg">可观察性</span></div>
<p>服务网格的可观察性有三个方面的内容：日志收集、分布式追踪、指标收集。</p>
<p>日志收集仍然是基于EFK/ELK这样的集中化日志解决方案。Envoy代理可以提供完善的访问日志，收集这些日志后，可以在ElasticSearch中检索和分析。</p>
<p>指标收集，粒度可以细致到微服务级别，这个从设计上来说是刻意的，因为服务网格中的服务，就是指微服务。遗憾的是，很多业务应用并没有按照微服务的理念进行设计，单个服务提供了太多的职责。这样的服务出现了问题，在网格拓扑图上只能进行模糊的定位。另一方面，Envoy支持的协议仍然很少，HTTP、WebSocket、gRPC被支持的很好，Redis、MySQL、ZooKeeper、Dubbo、Kafka目前获得了一定的支持，其它的协议目前Enovy都不能理解，只能看作是原始的L4流量。无法理解协议，也就不能收集有价值的指标，并展现在网格拓扑图上。</p>
<p>应用层协议过于繁多，每种协议还可能有多个版本，要支持这么多协议的确是个苦差。但是在自研服务网格的产品时，我们只需要关注团队经常用到的协议，针对它们进行解析就足够了。</p>
<p>对于分布式追踪，Istio能够自动添加必要的请求头，以便在微服务之间传递Trace ID、Span ID、Parent ID，并且，Istio会将这些信息上报给分布式追踪系统。 但是，Istio能做的也仅仅是这些了，对于单个微服务来说，还得依靠开发人员把入站请求中的Trace ID传递给出站请求。</p>
<p>通用的、零入侵的自动传递调用链上下文的解决方案，在技术上是不可能实现的。因为各种编程语言具有不同的线程模型、运行时架构，无法单纯的从网络流量中分析出足够的信息。某些语言，可以几乎没有入侵的自动传递调用链上下文，例如Java，可以通过Java Instrumentation来穷举式的拦截各种流行的客户端库，自动传递调用链信息。另外一些语言，则只能编写代码传递。 </p>
<div class="blog_h1"><span class="graybg">未来之路</span></div>
<p>经过上文的分析，我们了解到服务网格技术仍然存在一定的不足。在标准化方面，云厂商巨头们竞争激烈，这为我们带来了技术选型的风险。由于功能类似的遗留技术的存在，导致服务网格技术推广起来遇到阻力。此外，在性能、可扩展性、可观察性等非功能因素方面，现有主流服务网格开源项目存在不尽如人意的地方。</p>
<p>不过，我们应当看到社区的繁荣发展，开源项目的不断进步，以及服务网格具有的，不可替代的技术优势。</p>
<p>以Istio为例，它的性能自发布以来已经有了长足的进步，代理的P99延迟从数十ms降为10ms，进一步降低到5ms级别，已经能满足绝大部分场景的需求。如果需要进一步降低延迟，可以考虑BPF和QUIC等技术。</p>
<p>零入侵是服务网格不可替代的技术优势，这种优势在大型企业中更加明显。大型企业会有很多开发团队，使用不同的技术栈，Spring Cloud这种入侵式、绑定到JVM语言的解决方案显然是不可取的。微服务甚至是无服务是可预见的趋势，相对笨重的JVM并不十分适合这种应用场景，将企业的技术栈锁死在JVM甚至是Spring Cloud不是明智的选择。</p>
<p>服务网格领域仍然有大量的事情等待人们去做，比如各种中间件协议的解析、提升边缘节点性能的QUIC Termination、智能化的灰度发布平台，等等。自研服务网格产品，对内提升服务治理水平，对外输出社区影响力，现在就是很好的契机。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/the-present-and-future-of-servicemesh">服务网格的现状和未来</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/the-present-and-future-of-servicemesh/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>通过WebAssembly扩展Envoy</title>
		<link>https://blog.gmem.cc/extend-envoy-with-wasm</link>
		<comments>https://blog.gmem.cc/extend-envoy-with-wasm#comments</comments>
		<pubDate>Thu, 30 Jan 2020 09:04:10 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[ServiceMesh]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=32235</guid>
		<description><![CDATA[<p>WebAssembly简介 WebAssembly（简称Wasm）是一种供基于栈的虚拟机使用的二进制指令格式。它作为C/C++/Rust这样的高级语言的编译目标，部署在现代浏览器或者服务器端应用程序中运行。 Wasm的优势： 性能：基于通用硬件能力实现Native运行速度 安全：在内存安全的沙盒环境下执行 易用：容易调试、编写、测试 起步 工具链 你可以用多种语言编写逻辑，并利用相应的工具链，将代码编译为Wasm字节码。本节以C/C++为例。 Emscripten是一个基于LLVM的将C/C++编译为asm.js或WebAssembly的工具链。执行下面的命令下载预编译的工具链并安装： [crayon-69e5e2351bc17564207918/] 安装工具链后，需要执行下面的命令，进入Emscripten编译环境： [crayon-69e5e2351bc1b318455655/] Hello world 这里我们编写一个很简单的C应用： [crayon-69e5e2351bc1e780428191/] 执行命令：[crayon-69e5e2351bc20104265332-i/]， 会编译出Wsam，以及用于测试的HTML、JS文件。 执行命令：[crayon-69e5e2351bc22815613819-i/]，可以开启Web服务器。访问http://localhost:8080/hello.html可以看到Wasm运行结果。 JS API <a class="read-more" href="https://blog.gmem.cc/extend-envoy-with-wasm">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/extend-envoy-with-wasm">通过WebAssembly扩展Envoy</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">WebAssembly简介</span></div>
<p>WebAssembly（简称Wasm）是一种供<span style="background-color: #c0c0c0;">基于栈的虚拟机</span>使用的<span style="background-color: #c0c0c0;">二进制指令格式</span>。它作为C/C++/Rust这样的高级语言的编译目标，部署在现代浏览器或者服务器端应用程序中运行。</p>
<p>Wasm的优势：</p>
<ol>
<li>性能：基于<a href="https://webassembly.org/docs/portability/#assumptions-for-efficient-execution">通用硬件能力</a>实现Native运行速度</li>
<li>安全：在内存安全的沙盒环境下执行</li>
<li>易用：容易调试、编写、测试</li>
</ol>
<div class="blog_h2"><span class="graybg">起步</span></div>
<div class="blog_h3"><span class="graybg">工具链</span></div>
<p>你可以用多种语言编写逻辑，并利用相应的工具链，将代码编译为Wasm字节码。本节以C/C++为例。</p>
<p>Emscripten是一个基于LLVM的将C/C++编译为asm.js或WebAssembly的工具链。执行下面的命令下载预编译的工具链并安装：</p>
<pre class="crayon-plain-tag">git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest</pre>
<p>安装工具链后，需要执行下面的命令，进入Emscripten编译环境：</p>
<pre class="crayon-plain-tag">source ./emsdk_env.sh --build=Release</pre>
<div class="blog_h3"><span class="graybg">Hello world</span></div>
<p>这里我们编写一个很简单的C应用：</p>
<pre class="crayon-plain-tag">#include &lt;stdio.h&gt;
int main(int argc, char ** argv) {
  printf("Hello, world!\n");
}</pre>
<p>执行命令：<pre class="crayon-plain-tag">emcc hello.c -o hello.html</pre>， 会编译出Wsam，以及用于测试的HTML、JS文件。</p>
<p>执行命令：<pre class="crayon-plain-tag">emrun --no_browser --port 8080 .</pre>，可以开启Web服务器。访问http://localhost:8080/hello.html可以看到Wasm运行结果。</p>
<div class="blog_h3"><span class="graybg">JS API</span></div>
<p>在浏览器端，未来可能支持通过<pre class="crayon-plain-tag">&lt;script type='module'&gt;</pre>直接加载Wasm模块，就像加载ES6模块一样。目前则必须通过JS来加载和编译，步骤如下：</p>
<ol>
<li>获取.wasm字节码，存储到ArrayBuffer中</li>
<li>编译字节码为<pre class="crayon-plain-tag">WebAssembly.Module</pre></li>
<li>实例化WebAssembly.Module</li>
</ol>
<p>后面两步骤可以在下面的函数中完成</p>
<pre class="crayon-plain-tag">function instantiate(bytes, imports) {
         // 返回一个解析为WebAssembly.Module的Promise
                                         // 实例化，传入Module以及Module需要的imports
  return WebAssembly.compile(bytes).then(m =&gt; new WebAssembly.Instance(m, imports));
}</pre>
<p>类似于ES6模块，Wasm模块也可以导入、导出函数（或其它对象） ：</p>
<pre class="crayon-plain-tag">;; simple.wasm
(module
  (func $i (import "imports" "i") (param i32))
  (func (export "e")
    i32.const 42
    call $i))</pre>
<p>上面是一段文本格式（text format）的Wasm，它将<pre class="crayon-plain-tag">imports.i</pre>导入为内部名称<pre class="crayon-plain-tag">$i</pre>，我们在实例化此Wasm模块时，需要传入适当的导入对象：</p>
<pre class="crayon-plain-tag">var importObject = { imports: { i: arg =&gt; console.log(arg) } };

fetch('simple.wasm').then(response =&gt; response.arrayBuffer())
  .then(bytes =&gt; instantiate(bytes, importObject))
  .then(instance =&gt; instance.exports.e());
                    // 调用Wasm导出的函数</pre>
<div class="blog_h1"><span class="graybg">Envoy中的Wasm</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>利用Wasm，可以将Istio的扩展能力从控制平面下沉到Sidecar中。2019年Google团队付出努力，为Envoy添加了基于Wasm的动态扩展能力（proxy-wasm）。Google团队还和Solo.io合作构建了<a href="https://webassemblyhub.io/">WebAssembly Hub</a>，作为Wasm扩展的公共仓库。通过Wasm Hub可以很容易的下载Wasm扩展 、安装并以容器方式运行。</p>
<p>目前proxy-wasm处于Alpha状态，Istio 1.5使用此Alpha状态的Envoy。Wasm扩展在仓库https://github.com/envoyproxy/envoy-wasm/中开发，它：</p>
<ol>
<li>使用V8作为Wasm运行时</li>
<li>设计Wasm for Proxies（Proxy-Wasm）这一ABI，用于将Wasm嵌入到Envoy代理中。Telemetry V2是此ABI的第一个应用。需要注意，此ABI被设计为Proxy无感知的，也就是说不针对Envoy，其它代理也可以使用</li>
<li>用于简化基于Wasm的Envoy扩展开发的SDK，目前已经支持C++ / Rust / AssemblyScript</li>
</ol>
<p>和运行在浏览器中的Wasm类似，Envoy中的Wasm也在基于栈的虚拟机中运行，其内存和Envoy是隔离的。Envoy和Wasm过滤器的<span style="background-color: #c0c0c0;">交互，全部基于proxy-wasm SDK提供的函数、回调进行</span>。</p>
<div class="blog_h2"><span class="graybg">优劣</span></div>
<div class="blog_h3"><span class="graybg">优势</span></div>
<p>通过Wasm来扩展Envoy，具有以下优势：</p>
<ol>
<li>敏捷性：可以在运行时，通过Istio控制平面来分发、重新载入扩展，而不需要重新发版Envoy</li>
<li>稳定的Envoy版本：一旦envoy-wasm成熟并且合并到Envoy主线，Istio以及其它基于Envoy的框架都可以使用稳定Envoy版本，而不需要自行构建。Envoy社区本身也可以将一些in-tree的扩展改为基于Wasm实现</li>
<li>可靠/隔离性：Wasm扩展运行在沙盒中，具有资源约束。Wasm可以崩溃、泄漏内存，却不会导致整个Envoy进程挂掉。内存和CPU用量可以被限制</li>
<li>安全性：Wasm运行沙盒具有明确定义的、和Envoy通信的API，Wasm扩展因而只能修改连接/请求的受限数量的属性</li>
<li>灵活性：超过30种语言可以编译为Wasm</li>
</ol>
<div class="blog_h3"><span class="graybg">缺点</span></div>
<ol>
<li>性能不如C++扩展，可以达到70%性能</li>
<li>由于需要启动一或多个Wasm虚拟机，具有较高的内存消耗</li>
</ol>
<div class="blog_h2"><span class="graybg">应用</span></div>
<p>在Istio 1.5中，很多扩展被下沉到Envoy中，基于Wasm实现，这极大的提升了性能。</p>
<p>Istio控制平面及其Envoy配置API也在积极改进，以支持Wasm。</p>
<p>Istio社区也整合Mixer适配器的供应商进行协作，将Mixer适配器迁移到Wasm。</p>
<div class="blog_h2"><span class="graybg">Wasm Hub</span></div>
<p>WebAssembly Hub是一个仓库 + 一套工具集，便于构建、共享、发现Envoy的Wasm扩展。</p>
<p>WasmHub很大程度上自动化了Envoy Wasm扩展的开发、部署。使用它提供的工具集你能够很方便的把任何支持语言的代码<span style="background-color: #c0c0c0;">编译为Wasm扩展，上传到Hub，一键部署到Istio</span>。</p>
<p>在本节内容中，我们体验以下基于Wasm的Envoy扩展的开发流程。这个流程包括以下步骤：</p>
<ol>
<li>编写自定义过滤器</li>
<li>构建为Wasm模块，存储在OCI镜像中</li>
<li> 推送到Wasm Hub</li>
<li>将镜像部署到运行中的Envoy</li>
<li>测试过滤器的效果</li>
</ol>
<div class="blog_h3"><span class="graybg">安装wasme</span></div>
<p>可以通过命令行工具wasme来创建、部署WASM过滤器。</p>
<pre class="crayon-plain-tag">curl -sL https://run.solo.io/wasme/install | sh
export PATH=$HOME/.wasme/bin:$PATH

# 也可以到 https://github.com/solo-io/wasme/releases  下载期望的版本</pre>
<div class="blog_h3"><span class="graybg">创建新项目</span></div>
<p>创建一个名为new-filter的新项目：</p>
<pre class="crayon-plain-tag">wasme init ./new-filter</pre>
<p>命令会提示你：</p>
<ol>
<li>选择开发语言，目前仅仅支持CPP和AssemblyScript（Typescript的子集）</li>
<li>选择运行Envoy的平台，目前支持gloo:1.3.x, istio:1.5.x</li>
</ol>
<p>产生的新项目的结构如下：</p>
<pre class="crayon-plain-tag">.
├── assembly
│   ├── index.ts
│   └── tsconfig.json
├── package-lock.json
├── package.json
└── runtime-config.json</pre>
<div class="blog_h3"><span class="graybg">过滤器逻辑</span></div>
<p>过滤器编写在assembly/index.ts中，主要包括RootContext、Context两个类。</p>
<div class="blog_h3"><span class="graybg">构建镜像</span></div>
<pre class="crayon-plain-tag">wasme build assemblyscript -t webassemblyhub.io/gmemcc/add-header:v0.1 .</pre>
<div class="blog_h3"><span class="graybg">列出镜像</span></div>
<pre class="crayon-plain-tag">wasme list
wasme list --search $YOUR_USERNAME</pre>
<div class="blog_h3"><span class="graybg">推送到Hub</span></div>
<pre class="crayon-plain-tag">wasme login -u $YOUR_USERNAME -p $YOUR_PASSWORD</pre>
<div class="blog_h3"><span class="graybg">部署</span></div>
<pre class="crayon-plain-tag">wasme deploy gloo webassemblyhub.io/$YOUR_USERNAME/add-header:v0.1 --id=add-header</pre>
<div class="blog_h3"><span class="graybg">卸载</span></div>
<pre class="crayon-plain-tag">wasme undeploy istio --id add-header --namespace bookinfo</pre>
<div class="blog_h2"><span class="graybg">使用proxy-wasm SDK</span></div>
<p>你也可以不使用Wasm Hub提供的工具，直接基于proxy-wasm SDK进行过滤器开发。</p>
<p>SDK提供多种语言的绑定：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">语言</td>
<td style="text-align: center;">绑定</td>
</tr>
</thead>
<tbody>
<tr>
<td>C++</td>
<td>
<p><a href="https://github.com/proxy-wasm/proxy-wasm-cpp-sdk">https://github.com/proxy-wasm/proxy-wasm-cpp-sdk</a></p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td>Rust</td>
<td><a href="https://github.com/proxy-wasm/proxy-wasm-rust-sdk">https://github.com/proxy-wasm/proxy-wasm-rust-sdk</a></td>
</tr>
<tr>
<td>AssemblyScript</td>
<td><a href="https://github.com/solo-io/proxy-runtime">https://github.com/solo-io/proxy-runtime</a></td>
</tr>
<tr>
<td>Go</td>
<td><a href="https://github.com/mathetake/proxy-wasm-go">https://github.com/mathetake/proxy-wasm-go</a>  目前处于试验状态</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">C++</span></div>
<p>这里我们演示一下如何基于C++来开发Wasm过滤器。C++ SDK中关键的类包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">类</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>RootContext</td>
<td>
<p>当Wasm插件（包含了过滤器的Wasm二进制文件）并加载后，此根上下文被创建。根上下文具有和VM实例一样的生命周期</p>
<p>VM实例负责执行过滤器，并且：</p>
<ol>
<li>在最初Setup阶段，和Envoy交互</li>
<li>在每个请求处理阶段，和Envoy交互</li>
</ol>
<p><pre class="crayon-plain-tag">onConfigure(size_t)</pre>仅被Envoy调用，传递VM、插件配置。如果插件包含一或多个需要被传入配置的过滤器，你需要覆盖此函数，并通过<pre class="crayon-plain-tag">WasmBufferType::VmConfiguration and WasmBufferType::PluginConfiguration</pre>调用助手函数<pre class="crayon-plain-tag">getBufferBytes()</pre></p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td>Context</td>
<td>
<p>Envoy处理的网络流量，会穿过监听器的某个过滤器链。对于每个新的Stream，Envoy会为它创建一个Context，此Context的生命周期截至Stream终止</p>
<p>Context类以onXXX的形式提供若干钩子（回调）函数，这些虚函数中一部分用于HTTP流量，另一部分用于TCP流量。<span style="background-color: #c0c0c0;">到底哪些钩子被调用，取决于你的Wasm被插入的过滤器链的级别</span>（L4/L7）。例如，如果被插入到TCP过滤器链链，则<pre class="crayon-plain-tag">FilterHeadersStatus onRequestHeaders(uint32_t)</pre> 不会被调用</p>
<p>在Stream的完整生命周期中，你实现的Context都会被Envoy调用，在Context的回调函数中，你可以修改/静默掉流量，操控流量的方法包括：</p>
<ol>
<li>对于L4流量：getBufferBytes、setBufferBytes……</li>
<li>对于L7流量：getRequestHeader、addRequestHeader……</li>
</ol>
<p>回调函数返回的Status提示Envoy，是否需要将流量转给过滤器链中下一个过滤器处理</p>
</td>
</tr>
<tr>
<td>RegisterContextFactory</td>
<td>你需要声明此类型的静态变量，从而注册用于创建RootContext、Context的工厂 </td>
</tr>
</tbody>
</table>
<p>下面的过滤器使用了C++ SDK： </p>
<pre class="crayon-plain-tag">#include "proxy_wasm_intrinsics.h"

// 根上下文类实现
class ExampleRootContext: public RootContext {
public:
  explicit ExampleRootContext(uint32_t id, StringView root_id): RootContext(id, root_id) {}

  bool onStart(size_t) override;
};

// 上下文类实现
class ExampleContext: public Context {
public:
  explicit ExampleContext(uint32_t id, RootContext* root) : Context(id, root) {}

  FilterHeadersStatus onResponseHeaders(uint32_t) override;

  FilterStatus onDownstreamData(size_t, bool) override;
};

// 注册工厂类的静态变量
static RegisterContextFactory register_FilterContext(CONTEXT_FACTORY(ExampleContext),
                                                      ROOT_FACTORY(ExampleRootContext),
                                                      "my_root_id");

// 插件初始化完毕，可以处理流之后，调用此钩子
bool ExampleRootContext::onStart(size_t n) {
  LOG_DEBUG("ready to process streams");

  return true;
}

// 当HTTP响应头被解码后，调用此钩子
FilterHeadersStatus ExampleContext::onResponseHeaders(uint32_t) {
  // 增加一个响应头
  addResponseHeader("resp-header-demo", "added by our filter");
  // 让下一个过滤器继续处理流
  return FilterHeadersStatus::Continue;
}

// 当接收到下游TCP数据块后，调用此钩子
FilterStatus ExampleContext::onDownstreamData(size_t, bool) {
  auto res = setBuffer(WasmBufferType::NetworkDownstreamData, 0, 0, "prepend payload to downstream data");

   if (res != WasmResult::Ok) {
     LOG_ERROR("Modifying downstream data failed: " + toString(res));
      return FilterStatus::StopIteration;
   }

   return FilterStatus::Continue;
}</pre>
<p>要构建上述过滤器，最键单方式是使用容器（不需要在本机上安装依赖）： </p>
<ol>
<li>参考<a href="https://github.com/proxy-wasm/proxy-wasm-cpp-sdk#docker">这里的步骤</a>，创建包含了C++ SDK的Docker镜像</li>
<li>为Wasm过滤器创建Makefile：<br />
<pre class="crayon-plain-tag">.PHONY = all clean

PROXY_WASM_CPP_SDK=/sdk

all: example-filter.wasm

include ${PROXY_WASM_CPP_SDK}/Makefile.base_lite</pre>
</li>
<li>构建过滤器：<pre class="crayon-plain-tag">docker run -v $PWD:/work -w /work wasmsdk:v2 /build_wasm.sh</pre></li>
</ol>
<p>构建结果是一个<pre class="crayon-plain-tag">.wasm</pre>文件。</p>
<p>用在Istio中使用Wasm过滤器，你需要将.wasm文件挂载到Pod，然后使用EnvoyFilter：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: frontpage-v1-examplefilter
spec:
  configPatches:
  - applyTo: HTTP_FILTER
    match:
      context: SIDECAR_INBOUND
      proxy:
        proxyVersion: '^1\.5.*'
      listener:
        portNumber: 8080
        filterChain:
          filter:
            name: envoy.http_connection_manager
            subFilter:
              name: envoy.router
    patch:
      operation: INSERT_BEFORE
      value:
        config:
          config:
            name: example-filter
            rootId: my_root_id
            # V8虚拟机配置
            vmConfig:
              code:
                local:
                  filename: /var/local/lib/wasm-filters/example-filter.wasm
              runtime: envoy.wasm.runtime.v8
              vmId: example-filter
              allow_precompiled: true
        # Wasm HTTP过滤器，envoy.filters.network.wasm是TCP过滤器
        name: envoy.filters.http.wasm
  workloadSelector:
    labels:
      app: frontpage
      version: v1</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/extend-envoy-with-wasm">通过WebAssembly扩展Envoy</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/extend-envoy-with-wasm/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>扩展Envoy</title>
		<link>https://blog.gmem.cc/extend-envoy</link>
		<comments>https://blog.gmem.cc/extend-envoy#comments</comments>
		<pubDate>Thu, 25 Apr 2019 03:53:37 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C++]]></category>
		<category><![CDATA[PaaS]]></category>
		<category><![CDATA[ServiceMesh]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=26573</guid>
		<description><![CDATA[<p>前言 Enovy进程中运行着一系列的Inbound/Outbound监听器，分别代理外部发往工作负载、工作负载发往外部的网络流量。监听器的核心是过滤器链 ，链中每个过滤器都能够控制流量的处理流程。 扩展Envoy的主要方式就是开发新的过滤器。过滤器分为两个类别： 网络过滤器（L3/L4），是Envoy网络连接处理的核心 HTTP过滤器（L7），由特殊的网络过滤器HttpConnectionManager管理，专门处理HTTP1/HTTP2/gRPC请求 网络过滤器 根据行为的不同，网络过滤器分为： 读过滤器，当Envoy从下游连接接收到流量时调用 写过滤器，当Envoy准备向下游连接发送流量时调用 读/写过滤器，在上述两种情况下均调用  由于网络过滤器操控套接字的原始字节（外加少量事件，例如TLS握手完毕、连接断开），因此它的接口比较简单。 每个过滤器都可以中止迭代流程，并在未来继续后续过滤器的迭代。这种中止/继续迭代的机制，让实现复杂的需求成为可能，例如调用限速服务，异步的根据调用结果决定是否继续迭代。 网络过滤器之间可以在同一个下游连接的上下文内共享一些静态或动态数据。 接口 L4过滤器的接口非常简单，总共只有4个方法。 ReadFilter [crayon-69e5e2351c373767591342/] WriteFilter [crayon-69e5e2351c378479131180/] Filter [crayon-69e5e2351c37a128046623/] 示例 <a class="read-more" href="https://blog.gmem.cc/extend-envoy">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/extend-envoy">扩展Envoy</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">前言</span></div>
<p>Enovy进程中运行着一系列的Inbound/Outbound监听器，分别代理外部发往工作负载、工作负载发往外部的网络流量。<span style="background-color: #c0c0c0;">监听器的核心是过滤器链</span> ，链中每个过滤器都能够控制流量的处理流程。</p>
<p>扩展Envoy的主要方式就是开发新的过滤器。过滤器分为两个类别：</p>
<ol>
<li>网络过滤器（L3/L4），是Envoy网络连接处理的核心</li>
<li>HTTP过滤器（L7），由特殊的网络过滤器HttpConnectionManager管理，专门处理HTTP1/HTTP2/gRPC请求</li>
</ol>
<div class="blog_h1"><span class="graybg">网络过滤器</span></div>
<p>根据行为的不同，网络过滤器分为：</p>
<ol>
<li>读过滤器，当Envoy从下游连接接收到流量时调用</li>
<li>写过滤器，当Envoy准备向下游连接发送流量时调用</li>
<li>读/写过滤器，在上述两种情况下均调用 </li>
</ol>
<p>由于网络过滤器操控套接字的原始字节（外加少量事件，例如TLS握手完毕、连接断开），因此它的接口比较简单。</p>
<p>每个过滤器都<span style="background-color: #c0c0c0;">可以中止迭代流程</span>，并在未来<span style="background-color: #c0c0c0;">继续后续过滤器</span>的迭代。这种中止/继续迭代的机制，让实现复杂的需求成为可能，例如调用限速服务，异步的根据调用结果决定是否继续迭代。</p>
<p>网络过滤器之间可以<span style="background-color: #c0c0c0;">在同一个下游连接的上下文内<a href="https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/data_sharing_between_filters#arch-overview-data-sharing-between-filters">共享</a>一些静态或动态数据</span>。</p>
<div class="blog_h2"><span class="graybg">接口</span></div>
<p>L4过滤器的接口非常简单，总共只有4个方法。</p>
<div class="blog_h3"><span class="graybg">ReadFilter</span></div>
<pre class="crayon-plain-tag">class ReadFilter {
public:
 
  /**
   * 当连接上的数据被读取时调用
   * @param data 读取到的，可能已经被修改过的数据
   * @param end_stream 当连接启用半关闭语义时，用于提示是否到了最后一字节
   * @return status 过滤器管理器使用此状态决定如何进一步迭代其它过滤器
   */
  virtual FilterStatus onData(Buffer::Instance&amp; data, bool end_stream) PURE;
 
  /**
   * 当新连接刚创建时调用，过滤器链的迭代可以被中止
   * @return status 过滤器管理器使用此状态决定如何进一步迭代其它过滤器
   */
  virtual FilterStatus onNewConnection() PURE;
 
  /**
   * 初始化用于和过滤器管理器交互的读过滤器回调，过滤器被注册时，将被过滤器管理器调用一次
   * 任何需要用到底层连接的构造，需要在此函数的回调中执行
   *
   * IMPORTANT: 出站、复杂逻辑不要在此，放在onNewConnection()
   *
   */
  virtual void initializeReadFilterCallbacks(ReadFilterCallbacks&amp; callbacks) PURE;
}</pre>
<div class="blog_h3"><span class="graybg">WriteFilter</span></div>
<pre class="crayon-plain-tag">class WriteFilter {
public:
  /**
   * 当在此连接上发生数据写入时调用
   * @param data 需要写入的，可能已经被修改过的数据
   * @param end_stream 当连接启用半关闭语义时，用于提示是否到了最后一字节
   */
  virtual FilterStatus onWrite(Buffer::Instance&amp; data, bool end_stream) PURE;
};</pre>
<div class="blog_h3"><span class="graybg">Filter</span></div>
<pre class="crayon-plain-tag">class Filter : public WriteFilter, public ReadFilter {}; </pre>
<div class="blog_h2"><span class="graybg">示例</span></div>
<p>Envoy提供了一个<a href="https://github.com/envoyproxy/envoy-filter-example">过滤器的Demon项目</a>。我们基于这个项目来入门过滤器的开发。</p>
<p>构建此项目之前，注意将Bazel升级到0.23以上，否则你可能遇到错误：bazel parameter 'ctx' has no default value ...</p>
<div class="blog_h3"><span class="graybg">构建示例</span></div>
<p>参考如下命令完成示例项目的构建：</p>
<pre class="crayon-plain-tag"># 签出源码
git clone https://github.com/envoyproxy/envoy-filter-example.git
git submodule update --init
cd envoy-filter-example

# 根据你的构建环境选择适当的bazel选项
# bazel需要到Google下载部分软件包源码，可能需要代理
bazel build  -c dbg --copt "-DENVOY_IGNORE_GLIBCXX_USE_CXX11_ABI_ERROR=1"  //:envoy

# 运行Envoy单元测试
bazel test -c dbg --copt "-DENVOY_IGNORE_GLIBCXX_USE_CXX11_ABI_ERROR=1" @envoy//test/...

# 运行集成测试
bazel test -c dbg --copt "-DENVOY_IGNORE_GLIBCXX_USE_CXX11_ABI_ERROR=1" //:echo2_integration_test</pre>
<div class="blog_h3"><span class="graybg">代码解读</span></div>
<p>示例项目实现了名为Echo2的网络读过滤器，此过滤器的实现如下：</p>
<pre class="crayon-plain-tag">#pragma once

#include "envoy/network/filter.h"

#include "common/common/logger.h"

namespace Envoy {
namespace Filter {

//            实现接口
class Echo2 : public Network::ReadFilter, Logger::Loggable&lt;Logger::Id::filter&gt; {
public:
  Network::FilterStatus onData(Buffer::Instance&amp; data, bool end_stream) override;
  // 新连接到达后不做任何处理，继续调用下一个过滤器
  Network::FilterStatus onNewConnection() override { return Network::FilterStatus::Continue; }
  // 初始化回调集
  void initializeReadFilterCallbacks(Network::ReadFilterCallbacks&amp; callbacks) override {
    read_callbacks_ = &amp;callbacks;
  }

private:
  Network::ReadFilterCallbacks* read_callbacks_{};
};

} // namespace Filter
} // namespace Envoy


namespace Envoy {
namespace Filter {

// 接收到下游发来的数据后，简单的记录日志
Network::FilterStatus Echo2::onData(Buffer::Instance&amp; data, bool) {
  ENVOY_CONN_LOG(trace, "echo: got {} bytes", read_callbacks_-&gt;connection(), data.length());
  // 并把收到的数据直接Echo给下游
  read_callbacks_-&gt;connection().write(data, false);
  // 然后停止过滤器迭代，不调用它们
  return Network::FilterStatus::StopIteration;
}

} // namespace Filter
} // namespace Envoy</pre>
<p>每个过滤器都需要以一个独特的名称进行注册，否则Envoy无法知道它的存在，你也不能在配置文件中引用之。</p>
<p>Envoy过滤器的注册，一律通过模板化的静态变量Registry::RegisterFactory进行：</p>
<pre class="crayon-plain-tag">#include &lt;string&gt;

#include "echo2.h"

#include "envoy/registry/registry.h"
#include "envoy/server/filter_config.h"

namespace Envoy {
namespace Server {
namespace Configuration {

class Echo2ConfigFactory : public NamedNetworkFilterConfigFactory {
public:
  //                                                    没有配置
  Network::FilterFactoryCb createFilterFactoryFromProto(const Protobuf::Message&amp;, FactoryContext&amp;) override {
    // 过滤器工厂回调，初始化过滤器链时，Envoy会调用此方法
    // 通常你会在这里实例化过滤器，并添加到过滤器管理器中
    return [](Network::FilterManager&amp; filter_manager) -&gt; void {
      filter_manager.addReadFilter(Network::ReadFilterSharedPtr{new Filter::Echo2()});
    };
  }

  // 创建空白的过滤器配置Proto消息对象
  // 任何过滤器的配置，均以不透明的google.protobuf.Struct类型传递，并被转换为JSON、解析，然后填充到此Proto对象
  ProtobufTypes::MessagePtr createEmptyConfigProto() override {
    return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Empty()};
  }

  // 过滤器的独特名称，很重要
  std::string name() override { return "echo2"; }
};

/**
 * 静态的注册
 */
static Registry::RegisterFactory&lt;Echo2ConfigFactory, NamedNetworkFilterConfigFactory&gt; registered_;

} 
} 
}</pre>
<p>Registry::RegisterFactory是一个模板，在这个例子中，它会创建一个Echo2ConfigFactory，并在调用FactoryRegistry&lt;NamedNetworkFilterConfigFactory&gt;::registerFactory时传入它：</p>
<pre class="crayon-plain-tag">template &lt;class T, class Base&gt; class RegisterFactory {
public:
  RegisterFactory() { FactoryRegistry&lt;Base&gt;::registerFactory(instance_); }

private:
  T instance_{};
};</pre>
<div class="blog_h3"><span class="graybg">运行示例</span></div>
<p>可以使用如下的Envoy配置文件：</p>
<pre class="crayon-plain-tag">static_resources:
  listeners:
    name: listener_0
    address:
      socket_address:
        address: 127.0.0.1
        port_value: 15001
    filter_chains:
    - filters:
      - name: echo2</pre>
<p>启动Envoy的命令行参数：</p>
<pre class="crayon-plain-tag">bazel-bin/envoy -c echo2_server.yaml -l trace</pre>
<p>使用Telnet登陆，然后可以输入文字并回车，Envoy会回响你的输入：</p>
<pre class="crayon-plain-tag">telnet 127.0.0.1 15001</pre>
<div class="blog_h1"><span class="graybg">HTTP过滤器</span></div>
<p>HTTP过滤器类似于网络过滤器，也是形成一个栈。HTTP过滤器栈由HttpConnectionManager管理，HttpConnectionManager是一个L4过滤器。</p>
<p>根据行为的不同，HTTP过滤器分为：</p>
<ol>
<li>解码器（Decoder），当HTTP连接管理器解码请求流的一部分（头、体、尾）时调用</li>
<li>编码器（Encoder），当HTTP连接管理器准备编码响应流的一部分（头、体、尾）时调用</li>
<li>编解码器，在上述两种情况下均调用</li>
</ol>
<p>需要注意，HTTP过滤器操作的对象是流，而不是连接：</p>
<ol>
<li>对于HTTP1.1，在任意时间点每个连接上最多有一个流</li>
<li>对于HTTP2或者gRPC，实现了连接的多路复用，允许多个流同时依托于单个L4连接</li>
</ol>
<div class="blog_h2"><span class="graybg">接口</span></div>
<p>HTTP过滤器接口屏蔽了L4协议的细节。和L4过滤器一样，HTTP过滤器也能够中止、继续过滤器迭代，各HTTP过滤器同样可以在同一个请求流的上下文内共享一些静态或动态数据。</p>
<p>L7过滤器的类图如下：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2019/04/streamfilter-diagram.png"><img class="aligncenter size-full wp-image-26603" src="https://blog.gmem.cc/wp-content/uploads/2019/04/streamfilter-diagram.png" alt="streamfilter-diagram" width="394" height="230" /></a></p>
<div class="blog_h3"><span class="graybg">StreamFilterBase</span></div>
<p>HTTP流编解码器公共的父接口：</p>
<pre class="crayon-plain-tag">class StreamFilterBase {
public:

  /**
   * 当过滤器将要被销毁时调用，销毁可能在流正常结束后，或者因为RESET提前发生
   *
   * 任何过滤器都应在此方法中确保，所有异步事件 —— 例如定时器、网络调用 —— 被清理干净
   */
  virtual void onDestroy() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">StreamDecoderFilter</span></div>
<p>HTTP流解码器，负责处理下游发来的请求：</p>
<pre class="crayon-plain-tag">class StreamDecoderFilter {
public:
 
  /**
   * 处理已经被http_parser解析好的请求头
   * @param headers 请求头的映射
   * @param end_stream 提示当前流是否header-only的
   * @return FilterHeadersStatus 用于确定是否继续迭代后续过滤器
   */
  virtual FilterHeadersStatus decodeHeaders(HeaderMap&amp; headers, bool end_stream) PURE;

  /**
   * 处理已经被http_parser解析好的数据帧
   * @param data 存放数据帧的缓冲区
   * @param end_stream 提示当前是否最后一个数据帧
   * @return FilterDataStatus 用于确定是否继续迭代后续过滤器
   */
  virtual FilterDataStatus decodeData(Buffer::Instance&amp; data, bool end_stream) PURE;

  /**
   * 处理已经被http_parser解析好的请求尾，隐含end_stream = true
   * @param trailers supplies the decoded trailers.
   */
  virtual FilterTrailersStatus decodeTrailers(HeaderMap&amp; trailers) PURE;

  /**
   * 过滤器管理器调用此方法来初始化解码回调集
   */
  virtual void setDecoderFilterCallbacks(StreamDecoderFilterCallbacks&amp; callbacks) PURE;
};</pre>
<div class="blog_h3"><span class="graybg">StreamEncoderFilter</span></div>
<p>HTTP流编码器，可以处理准备发给下游的响应：</p>
<pre class="crayon-plain-tag">class StreamEncoderFilter : public StreamFilterBase {
public:
  /*
   * 当配置Envoy，让其代理（通常不会配置）Expect:100-continue请求，
   * 并且当前请求指定了Expect:100-continue时，会调用此方法
   */
  virtual FilterHeadersStatus encode100ContinueHeaders(HeaderMap&amp; headers) PURE;

  /**
   * 处理响应头
   */
  virtual FilterHeadersStatus encodeHeaders(HeaderMap&amp; headers, bool end_stream) PURE;

  /**
   * 处理响应体
   */
  virtual FilterDataStatus encodeData(Buffer::Instance&amp; data, bool end_stream) PURE;

  /**
   * 处理响应尾，隐含end_stream = true
   */
  virtual FilterTrailersStatus encodeTrailers(HeaderMap&amp; trailers) PURE;

  /**
   * 处理元数据，新的元数据应该直接存入metadata_map
   * 不要通过StreamDecoderFilterCallbacks::encodeMetadata()来添加元数据
   *
   */
  virtual FilterMetadataStatus encodeMetadata(MetadataMap&amp; metadata_map) PURE;

  /**
   * 滤器管理器调用此方法来初始化编码回调集
   */
  virtual void setEncoderFilterCallbacks(StreamEncoderFilterCallbacks&amp; callbacks) PURE;
};</pre>
<div class="blog_h3"><span class="graybg">StreamFilter </span></div>
<pre class="crayon-plain-tag">class StreamFilter : public virtual StreamDecoderFilter, public virtual StreamEncoderFilter {};</pre>
<div class="blog_h2"><span class="graybg">示例</span></div>
<p>Envoy提供的envoy-filter-example示例项目中也提供了一个HTTP过滤器， 其代码存放在http-filter-example目录下。这是一个解码过滤器，它会为下游请求添加一个请求头。</p>
<div class="blog_h3"><span class="graybg">构建示例</span></div>
<p>参考如下命令完成示例项目的构建：</p>
<pre class="crayon-plain-tag">bazel build  -c dbg --copt "-DENVOY_IGNORE_GLIBCXX_USE_CXX11_ABI_ERROR=1"  //http-filter-example:envoy </pre>
<div class="blog_h3"><span class="graybg">代码解读</span></div>
<p>过滤器配置、过滤器类的声明：</p>
<pre class="crayon-plain-tag">#pragma once

#include &lt;string&gt;

#include "envoy/server/filter_config.h"

#include "http-filter-example/http_filter.pb.h"

namespace Envoy {
namespace Http {

// 过滤器的配置对象
class HttpSampleDecoderFilterConfig {
public:
  // 构造函数，配置对象的入参是Proto消息，sample::Decoder依据你写的Proto文件自动生成
  HttpSampleDecoderFilterConfig(const sample::Decoder&amp; proto_config);

  const std::string&amp; key() const { return key_; }
  const std::string&amp; val() const { return val_; }

private:
  const std::string key_;
  const std::string val_;
};

typedef std::shared_ptr&lt;HttpSampleDecoderFilterConfig&gt; HttpSampleDecoderFilterConfigSharedPtr;

// 过滤器声明
class HttpSampleDecoderFilter : public StreamDecoderFilter {
public:
  HttpSampleDecoderFilter(HttpSampleDecoderFilterConfigSharedPtr);
  ~HttpSampleDecoderFilter();

  // 需要实现Http::StreamFilterBase的方法
  void onDestroy() override;

  // 需要实现Http::StreamDecoderFilter的方法
  FilterHeadersStatus decodeHeaders(HeaderMap&amp;, bool) override;
  FilterDataStatus decodeData(Buffer::Instance&amp;, bool) override;
  FilterTrailersStatus decodeTrailers(HeaderMap&amp;) override;
  void setDecoderFilterCallbacks(StreamDecoderFilterCallbacks&amp;) override;

private:
  const HttpSampleDecoderFilterConfigSharedPtr config_;
  StreamDecoderFilterCallbacks* decoder_callbacks_;
  const LowerCaseString headerKey() const;
  const std::string headerValue() const;
};

} // namespace Http
} // namespace Envoy</pre>
<p>Proto文件：</p>
<pre class="crayon-plain-tag">syntax = "proto3";

package sample;

import "validate/validate.proto";

message Decoder {
    string key = 1 [(validate.rules).string.min_bytes = 1];
    string val = 2 [(validate.rules).string.min_bytes = 1];
}</pre>
<p>BUILD文件中的规则http_filter_proto负责从Proto文件生成C++代码。 </p>
<p>过滤器的实现如下：</p>
<pre class="crayon-plain-tag">#include &lt;string&gt;

#include "http_filter.h"

#include "envoy/server/filter_config.h"

namespace Envoy {
namespace Http {

// 配置对象的构造函数，入参是Proto对象
HttpSampleDecoderFilterConfig::HttpSampleDecoderFilterConfig(const sample::Decoder&amp; proto_config)
    : key_(proto_config.key()), val_(proto_config.val()) {}

// 过滤器的构造函数，入参是配置对象
HttpSampleDecoderFilter::HttpSampleDecoderFilter(HttpSampleDecoderFilterConfigSharedPtr config): config_(config) {}

HttpSampleDecoderFilter::~HttpSampleDecoderFilter() {}

void HttpSampleDecoderFilter::onDestroy() {}

const LowerCaseString HttpSampleDecoderFilter::headerKey() const {
  return LowerCaseString(config_-&gt;key());
}

const std::string HttpSampleDecoderFilter::headerValue() const {
  return config_-&gt;val();
}

FilterHeadersStatus HttpSampleDecoderFilter::decodeHeaders(HeaderMap&amp; headers, bool) {
  // 添加一个请求头
  headers.addCopy(headerKey(), headerValue());
  // 设置响应体
  auto body_text = fmt::format("{}:{}", headerKey().get(), headerValue().c_str());
  // 添加一个响应头
  auto modify_headers = [this](HeaderMap&amp; headers) -&gt; void {
    headers.addCopy(headerKey(), headerValue());
  };
  decoder_callbacks_-&gt;sendLocalReply(Http::Code::OK, body_text, modify_headers, absl::nullopt);
  // 本地响应已经发送，必须停止迭代，否则执行到Router会出现断言失败
  return FilterHeadersStatus::StopIteration;
}

// 如果不增加任何逻辑，简单返回Continue即可
FilterDataStatus HttpSampleDecoderFilter::decodeData(Buffer::Instance&amp;, bool) {
  return FilterDataStatus::Continue;
}

FilterTrailersStatus HttpSampleDecoderFilter::decodeTrailers(HeaderMap&amp;) {
  return FilterTrailersStatus::Continue;
}

void HttpSampleDecoderFilter::setDecoderFilterCallbacks(StreamDecoderFilterCallbacks&amp; callbacks) {
  decoder_callbacks_ = &amp;callbacks;
}

} // namespace Http
} // namespace Envoy</pre>
<p>配置工厂，能够创建过滤器工厂，过滤器工厂FilterFactoryCb本质上就是一个函数，Envoy调用它来创建过滤器。</p>
<pre class="crayon-plain-tag">#include &lt;string&gt;

#include "http_filter.h"

#include "common/config/json_utility.h"
#include "envoy/registry/registry.h"

#include "http-filter-example/http_filter.pb.h"
#include "http-filter-example/http_filter.pb.validate.h"

namespace Envoy {
namespace Server {
namespace Configuration {

class HttpSampleDecoderFilterConfigFactory : public NamedHttpFilterConfigFactory {
public:
  // 从JSON配置创建过滤器工厂
  Http::FilterFactoryCb createFilterFactory(const Json::Object&amp; json_config, const std::string&amp;, FactoryContext&amp; context) override {
    sample::Decoder proto_config;
    // 将JSON配置转化为Proto配置
    translateHttpSampleDecoderFilter(json_config, proto_config);
    return createFilter(proto_config, context);
  }
  // V2 API的createFilterFactory变体，过滤器配置以Proto消息的形式传入，目前可以不实现此方法
  // 未来V1 API废弃后，必须实现
  Http::FilterFactoryCb createFilterFactoryFromProto(const Protobuf::Message&amp; proto_config,
                                                     const std::string&amp;,
                                                     FactoryContext&amp; context) override {

    return createFilter(Envoy::MessageUtil::downcastAndValidate&lt;const sample::Decoder&amp;&gt;(proto_config), context);
  }

  // 新的空白配置Proto
  ProtobufTypes::MessagePtr createEmptyConfigProto() override {
    return ProtobufTypes::MessagePtr{new sample::Decoder()};
  }

  // 独特的名称
  std::string name() override { return "sample"; }

private:
  Http::FilterFactoryCb createFilter(const sample::Decoder&amp; proto_config, FactoryContext&amp;) {
    // 将Proto对象转化为配置对象
    Http::HttpSampleDecoderFilterConfigSharedPtr config =
        std::make_shared&lt;Http::HttpSampleDecoderFilterConfig&gt;( Http::HttpSampleDecoderFilterConfig(proto_config));

    return [config](Http::FilterChainFactoryCallbacks&amp; callbacks) -&gt; void {
      auto filter = new Http::HttpSampleDecoderFilter(config);
      // 添加此过滤器，注意L7过滤器由过滤器链工厂管理，每个监听器可以有多个过滤器链
      callbacks.addStreamDecoderFilter(Http::StreamDecoderFilterSharedPtr{filter});
    };
  }

  void translateHttpSampleDecoderFilter(const Json::Object&amp; json_config, sample::Decoder&amp; proto_config) {
    JSON_UTIL_SET_STRING(json_config, proto_config, key);
    JSON_UTIL_SET_STRING(json_config, proto_config, val);
  }
};

// 静态注册，类似于L4过滤器
static Registry::RegisterFactory&lt;HttpSampleDecoderFilterConfigFactory, NamedHttpFilterConfigFactory&gt; register_;

} // namespace Configuration
} // namespace Server
} // namespace Envoy</pre>
<div class="blog_h3"><span class="graybg">运行示例</span></div>
<p>可以使用如下的Envoy配置文件： </p>
<pre class="crayon-plain-tag">static_resources:
  listeners:
    name: listener_0
    address:
      socket_address:
        address: 127.0.0.1
        port_value: 15001
    filter_chains:
      - filters:
          - name: envoy.http_connection_manager
            config:
              stat_prefix: sample
              route_config:
                name: gmem
                virtual_hosts:
                  - name: gmem
                    domains: ["*"]
                    routes:
                      - match:
                          prefix: "/"
                        route:
                          cluster: gmem
              http_filters:
                - name: sample
                  config:
                    key: via
                    val: sample-filter
                - name: envoy.router
  clusters:
    - name: gmem
      connect_timeout: 1s
      type: STRICT_DNS
      dns_lookup_family: V4_ONLY
      lb_policy: ROUND_ROBIN
      hosts:
        - socket_address:
            address: gmem.cc
            port_value: 80</pre>
<p>使用curl来测试：</p>
<pre class="crayon-plain-tag">curl -D - http://127.0.0.1:15001

HTTP/1.1 200 OK
content-length: 17
content-type: text/plain
# 添加的响应头
via: sample-filter
date: Tue, 30 Apr 2019 03:44:45 GMT
server: envoy

# 设置的响应体
via:sample-filter</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/extend-envoy">扩展Envoy</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/extend-envoy/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>扩展Istio</title>
		<link>https://blog.gmem.cc/extend-istio</link>
		<comments>https://blog.gmem.cc/extend-istio#comments</comments>
		<pubDate>Mon, 25 Mar 2019 07:42:09 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Go]]></category>
		<category><![CDATA[ServiceMesh]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=26071</guid>
		<description><![CDATA[<p>前言 如果Istio不能满足你的需求，你可以考虑扩展它。 Pilot的功能相对比较固定，主要负责和Envoy代理基于xDS协议的数据交换，通常不需要进行扩展和定制。 Mixer本身即是高度模块化、并且鼓励扩展的。我们可以定义自己的模板，从网格流量中抽取新的属性，也可以开发自己的适配器，来支持和各种后端基础设施的对接。本文的主要篇幅将用来探讨Mixer的扩展。 如何贡献 Istio开发所需的资源参考官方Wiki。 前提条件 在进行Istio开发之前，先准备好： Go 1.11版本 Docker，Istio包含一个镜像构建系统，能够创建、发布Docker镜像 如果在K8S环境下运行Istio，你需要K8S 1.7.3以上版本 签出源码 [crayon-69e5e2351cd02052110092/] 环境变量 [crayon-69e5e2351cd06235111355/] 构建Istio 在本机环境下构建： [crayon-69e5e2351cd09689230052/] 构建并打包到容器： [crayon-69e5e2351cd0b531449123/] 调试Istio <a class="read-more" href="https://blog.gmem.cc/extend-istio">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/extend-istio">扩展Istio</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">前言</span></div>
<p>如果Istio不能满足你的需求，你可以考虑扩展它。</p>
<p>Pilot的功能相对比较固定，主要负责和Envoy代理基于xDS协议的数据交换，通常不需要进行扩展和定制。</p>
<p>Mixer本身即是高度模块化、并且鼓励扩展的。我们可以定义自己的模板，从网格流量中抽取新的属性，也可以开发自己的适配器，来支持和各种后端基础设施的对接。本文的主要篇幅将用来探讨Mixer的扩展。</p>
<div class="blog_h1"><span class="graybg">如何贡献</span></div>
<p>Istio开发所需的资源参考<a href="https://github.com/istio/istio/wiki">官方Wiki</a>。</p>
<div class="blog_h2"><span class="graybg">前提条件</span></div>
<p>在进行Istio开发之前，先准备好：</p>
<ol>
<li>Go 1.11版本</li>
<li>Docker，Istio包含一个镜像构建系统，能够创建、发布Docker镜像</li>
<li>如果在K8S环境下运行Istio，你需要K8S 1.7.3以上版本</li>
</ol>
<div class="blog_h2"><span class="graybg">签出源码</span></div>
<pre class="crayon-plain-tag"># 必须签出到$GOPATH/src/istio.io下
pushd /home/alex/Go/workspaces/default/src/istio.io
git clone https://github.com/istio/istio.git</pre>
<div class="blog_h2"><span class="graybg">环境变量</span></div>
<pre class="crayon-plain-tag">export GOPATH=~/go
export PATH=$PATH:$GOPATH/bin
export ISTIO=$GOPATH/src/github.com/istio/istio

# Docker镜像仓库和Tag
export HUB="docker.gmem.cc/istio"
export TAG=1.0.5

export KUBECONFIG=${HOME}/.kube/config </pre>
<div class="blog_h2"><span class="graybg">构建Istio</span></div>
<p>在本机环境下构建：</p>
<pre class="crayon-plain-tag"># 基于本机的体系结构构建Istio的Pilot、Mixer、Citadel等组件
make

# 构建包含调试信息的组件，可以基于Delve等Debugger进行单步调试
make DEBUG=1

# 提升非第一次构建的速度，-i让Go缓存中间结果
GOBUILDFLAGS=-i make</pre>
<p>构建并打包到容器：</p>
<pre class="crayon-plain-tag">make docker
make DEBUG=1 docker

# 推送到镜像仓库
make push</pre>
<div class="blog_h2"><span class="graybg">调试Istio</span></div>
<div class="blog_h3"><span class="graybg">本地</span></div>
<p>你可以参考下面的命令在本地启动Pilot：</p>
<pre class="crayon-plain-tag">... discovery --log_output_level=default:debug --domain=k8s.gmem.cc --kubeconfig=/home/alex/.kube/config --meshConfig=pilot/mesh </pre>
<div class="blog_h3"><span class="graybg">K8S</span></div>
<p>在K8S环境下，调试Istio容器的步骤如下：</p>
<ol>
<li>定位到Istio容器在什么节点下运行</li>
<li>确保必要的工具都在节点上安装好，包括Go、Delve</li>
<li>将可执行文件基于的源码拷贝到节点上</li>
<li>在节点上找到Istio容器对应的进程</li>
<li>执行<pre class="crayon-plain-tag">sudo dlv attach pilot-pid</pre>开始调试 </li>
</ol>
<p>你也可以使用Squash配合Delve进行调试，可能需要修改Istio的基础镜像（例如alpine）。使用Squash的优势是不需要在所有节点上都安装Delve+Go</p>
<div class="blog_h3"><span class="graybg">连接到本地</span></div>
<p>给Deployment增加注解：<pre class="crayon-plain-tag">sidecar.istio.io/discoveryAddress: 10.0.0.1:15010</pre> 即可强制指定Pilot的地址。</p>
<div class="blog_h2"><span class="graybg">运行测试</span></div>
<pre class="crayon-plain-tag"># 运行所有测试
make test

# 运行Pilot的单元测试
make pilot-test

# 使用Go竞态条件检测工具运行测试
make racetest


# 获取测试覆盖率信息
make coverage</pre>
<div class="blog_h3"><span class="graybg">测试和PR</span></div>
<p>只有通过单元测试、集成测试之后，才能提交PR，否则不会被合并：</p>
<ol>
<li>单元测必须是密封的。仅仅访问test binary中的资源</li>
<li>所有包、重要文件必须进行单元测试</li>
<li>单元测试使用标准的Go测试包</li>
<li>测试多种场景/输入时，最好使用<a href="https://github.com/golang/go/wiki/TableDrivenTests">表驱动测试</a></li>
<li>必须通过并发测试</li>
</ol>
<div class="blog_h2"><span class="graybg">格式化代码</span></div>
<pre class="crayon-plain-tag">make format</pre>
<div class="blog_h2"><span class="graybg">代码检查</span></div>
<pre class="crayon-plain-tag">make lint

# 仅仅针对本地变更进行检查
bin/linters.sh -s HEAD^</pre>
<div class="blog_h2"><span class="graybg">使用CircleCI </span></div>
<p>Istio使用CircleCI作为持续集成系统，所有PR必须通过全部CircleCI测试才能被合并。当Fork了Istio之后，CircleCI测试环境也被复制到本地，可以完整重现Istio的测试基础设施。</p>
<p>你可以注册CircleCI账号，并在Fork中测试代码的变更，防止PR不被通过。</p>
<div class="blog_h2"><span class="graybg">Git工作流</span></div>
<ol>
<li>Fork主仓库</li>
<li>克隆Fork到本地</li>
<li>启用提交前钩子：<pre class="crayon-plain-tag">./bin/pre-commit</pre></li>
<li> 创建一个分支，修改一些代码</li>
<li>保持Fork和主仓库同步：<br />
<pre class="crayon-plain-tag">git fetch upstream
git rebase upstream/master</pre>
</li>
<li>
<p>提交变更到Fork</p>
</li>
<li>
<p>推送变更到Fork</p>
</li>
<li>
<p>创建一个PR</p>
</li>
<li>PR会分配给1-N个reviewer，他们会检查代码、文档、注释，包括代码样式</li>
</ol>
<div class="blog_h2"><span class="graybg">在特性分支上开发</span></div>
<p>开发新的试验特性，或者进行可能对master稳定性造成巨大影响的变更时，应当新开启特性分支，并遵守：</p>
<ol>
<li>以<pre class="crayon-plain-tag">collab-&lt;feature-name&gt;</pre>的方式命名分支</li>
<li>周期性的从master合并代码，长期不合并，导致最终将特性分支合并到master时非常困难</li>
</ol>
<div class="blog_h2"><span class="graybg">Istio测试框架</span></div>
<p>让用测试例本身快速、可靠的基于云环境运行是困难的，Istio测试框架尝试解决该问题。</p>
<p>Istio测试框架的目标：</p>
<ol>
<li>编写测试：
<ol>
<li>平台不可知：API将底层平台的细节屏蔽掉，让开发人员专注于测试Istio本身的裸机</li>
<li>可重用测试：可以基于任何支持Istio的底层平台运行测试</li>
</ol>
</li>
<li>运行测试：
<ol>
<li>基于Go语言标准测试机制</li>
<li>简单：不需要或需要很少的标记即可运行测试</li>
<li>快速</li>
<li>可靠：在本机运行测试天然比在集群中可靠，但是针对各平台的组件都具有可靠性机制，例如重试</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">起步</span></div>
<p>使用此测试框架，你需要编写一个TestMain函数：</p>
<pre class="crayon-plain-tag">func TestMain(m *testing.M) { 
    framework.
        NewSuite("my_test", m).
        Run()
}</pre>
<p>在此函数中你需要调用NewSuite，从而：</p>
<ol>
<li>启动一个平台特定的环境，默认使用本地环境，如果需要在K8S上运行测试，设置标记<span style="color: #24292e;"><pre class="crayon-plain-tag">--istio.test.env=kube</pre></span></li>
<li>运行当前包的所有测试用例</li>
<li>停止环境</li>
</ol>
<p>然后在当前包中编写一个个的测试用例：</p>
<pre class="crayon-plain-tag">func TestHTTP(t *testing.T) {
    // 获取测试环境上下文
    ctx := framework.GetContext(t)
    defer ctx.Done()
    
    // 获取需要测试的组件（例如Pilot、Mixer、Apps）
    apps := components.GetApps(t, ctx)
    a := apps.GetAppOrFail("a", t)
    b := apps.GetAppOrFail("b", t)

    // 和组件进行交互，每个组件都定义了自己的API
    be := b.EndpointsForProtocol(model.ProtocolHTTP)[0]
    result := a.CallOrFail(be, components.AppCallOptions{}, t)[0]
    if !result.IsOK() {
        t.Fatalf("HTTP Request unsuccessful: %s", result.Body)
    }
}</pre>
<p>如果你需要执行测试套件级别的检查，可以：</p>
<pre class="crayon-plain-tag">func TestMain(m *testing.M) {
    framework.NewTest("my_test", m).
    // 要求Kubernetes环境
    RequireEnvironment(environment.Kube).                             
    // 部署供整个测试套件使用的Istio 
    SetupOnEnv(environment.Kube, istio.Setup(&amp;ist, setupIstioConfig)).
    // 调用你的setp函数
    Setup(setup).
    Run()
}

func setupIstioConfig(cfg *istio.Config) {
    cfg.Values["your-feature-enabled"] = "true"
}

func setup(ctx resource.Context) error {
  // 准备测试环境
}</pre>
<div class="blog_h3"><span class="graybg">支持的环境</span></div>
<table class="full-width fixed-word-wrap">
<tbody>
<tr>
<td style="width: 120px;">Native</td>
<td>
<p>在本机（进程内或进程外）运行测试，默认值</p>
<p>好处是简单、快、可靠</p>
</td>
</tr>
<tr>
<td>Kubernetes</td>
<td>需要使用标记<pre class="crayon-plain-tag">--istio.test.env=kube</pre>，默认情况下会使用<pre class="crayon-plain-tag">~/.kube/config</pre>来部署Istio</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">标记</td>
<td style="width: 20%; text-align: center;">默认值</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>istio.test.env</td>
<td>native</td>
<td>
<p>运行测试的环境</p>
</td>
</tr>
<tr>
<td>istio.test.work_dir</td>
<td>''</td>
<td>创建 logs/temp文件的本地目录，如果不指定使用系统临时目录</td>
</tr>
<tr>
<td>istio.test.hub</td>
<td>''</td>
<td>使用的Docker仓库，默认从HUB环境变量读取</td>
</tr>
<tr>
<td>istio.test.tag</td>
<td>''</td>
<td>使用的镜像标签，默认从TAG环境变量读取</td>
</tr>
<tr>
<td>istio.test.pullpolicy</td>
<td>Always</td>
<td>镜像拉取策略，可用环境变量PULL_POLICY指定</td>
</tr>
<tr>
<td>istio.test.nocleanup</td>
<td>false</td>
<td>测试完毕后不要清理资源</td>
</tr>
<tr>
<td>istio.test.ci</td>
<td>false</td>
<td>启用CI模式，以打印更多日志和状态信息</td>
</tr>
<tr>
<td>istio.test.kube.config</td>
<td>~/.kube/config</td>
<td>使用的Kubeconfig</td>
</tr>
<tr>
<td>istio.test.kube.minikube</td>
<td>false</td>
<td>基于Minikube环境运行</td>
</tr>
<tr>
<td>istio.test.kube.systemNamespace</td>
<td>istio-system</td>
<td>废弃</td>
</tr>
<tr>
<td>istio.test.kube.istioNamespace</td>
<td>istio-system</td>
<td>Istio CA和证书分发组件所在命名空间</td>
</tr>
<tr>
<td>istio.test.kube.configNamespace</td>
<td>istio-system</td>
<td>配置文件、服务发现、自动注入组件部署到的命名空间</td>
</tr>
<tr>
<td>istio.test.kube.telemetryNamespace</td>
<td>istio-system</td>
<td>mixer, kiali, tracing providers, graphana, prometheus 部署到的命名空间</td>
</tr>
<tr>
<td>istio.test.kube.policyNamespace</td>
<td>istio-system</td>
<td>policy checker部署到的命名空间</td>
</tr>
<tr>
<td>istio.test.kube.ingressNamespace</td>
<td>istio-system</td>
<td>ingressgateway部署到的命名空间</td>
</tr>
<tr>
<td>istio.test.kube.egressNamespace</td>
<td>istio-system</td>
<td>egressgateway部署到的命名空间</td>
</tr>
<tr>
<td>istio.test.kube.deploy</td>
<td>true</td>
<td>如果为true则部署组件，否则假设组件已经部署了</td>
</tr>
<tr>
<td>istio.test.kube.helm.chartDir</td>
<td>$(ISTIO)/install/kubernetes/helm/istio</td>
<td>Istio的Helm Chart位置</td>
</tr>
<tr>
<td>istio.test.kube.helm.valuesFile</td>
<td>values-e2e.yaml</td>
<td>相对于relative to istio.test.kube.helm.chartDir的Chart 覆盖变量文件</td>
</tr>
<tr>
<td>istio.test.kube.helm.values</td>
<td>''</td>
<td>提供Chart覆盖变量</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">使用Prow</span></div>
<p><a href="https://prow.istio.io/">Prow</a>提供CI特性、一套工具集来提升开发人员额度生产力，它由K8S社区开发，部署在GCE中。你也可以在任何K8S集群中部署它。</p>
<p>Prow能运行：<span style="background-color: #c0c0c0;">pre-submit、post-submit、周期性的Jobs</span>，并提供生产力工具：</p>
<ol>
<li>Tide：自动合并PR</li>
<li>hold：保持没有被合并的PR</li>
<li>分支保护：基于配置更新Github分支保护策略</li>
<li>needs-rebase：提示PR需要rebase</li>
</ol>
<div class="blog_h3"><span class="graybg">配置</span></div>
<p>配置文件主要有两个：</p>
<ol>
<li>config.yaml：定义Job、一般性设置</li>
<li>plugins.yaml：插件配置</li>
</ol>
<div class="blog_h1"><span class="graybg">开发模板</span></div>
<p>&nbsp;</p>
<p>Mixer使用模板（Template）来结构化入站的属性。模板描述了需要发送给适配器的<span style="background-color: #c0c0c0;">数据的形式</span>，它还定义了适配器为了接受数据<span style="background-color: #c0c0c0;">必须实现的gRPC接口</span>。Mixer提供了一些开箱即用的默认模板，当<span style="background-color: #c0c0c0;">实现自己的适配器时，应当尽可能使用这些默认模板</span>。</p>
<p>模板以Proto文件的形式声明，此定义中包含Template消息，指定了Template变体（Check/Report/Quota）。从Template消息会生成：</p>
<ol>
<li>InstanceMsg消息，在请求期间，作为参数传递</li>
<li>Handle服务，InstanceMsg消息传递给该服务</li>
<li>Type消息，在配置期间传递，描述InstanceMsg的规格</li>
</ol>
<p>如果可能，不要自己定义模板。Istio内置的模板通常可以满足需要。</p>
<div class="blog_h2"><span class="graybg">Proto文件</span></div>
<p>前面提到过，Template是使用Proto文件定义的，它对应一个名为<span style="color: #24292e;">Template的消息。所有Go代码都基于此消息自动生成。</span></p>
<p>每个模板具有两个额外的属性：</p>
<ol>
<li>Name，模板的独特的名称。<span style="background-color: #c0c0c0;">适配器会使用此名称来注册到Mixer，声明自己需要消费这种类型模板的Instance</span></li>
<li>template_variety，表示模板的种类，种类<span style="background-color: #c0c0c0;">决定了</span>适配器必须实现的、消费模板Instance的<span style="background-color: #c0c0c0;">方法的签名</span>
<ol>
<li>Check，这种模板需要的实例仅仅在Mixer客户端进行Check API调用时生成</li>
<li>Report，这种模板需要的实例仅仅在Mixer客户端进行Report API调用时生成</li>
<li>Quota，这种模板需要的实例仅仅在Mixer客户端进行Check API调用，以要求进行Quota分配时生成</li>
<li>AttributeGenerator，这种模板需要的实例在Check/Report调用时都会生成并分发，这种模板的处理发生在补充属性生成阶段（supplementary attribute generation phase） —— <span style="background-color: #c0c0c0;">早于任何其它模板的处理</span>。处理AttributeGenerator的适配器称为属性生成适配器，它们负责生成模板声明的输出数据，你可以基于这些数据来配置新的属性</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">示例</span></div>
<p>下面是<span style="color: #24292e;">listentry模板的Proto文件：</span></p>
<pre class="crayon-plain-tag">syntax = "proto3";

// 模板的包名词，它决定了模板的名字，对应CRD的名字
package listEntry;

import "mixer/adapter/model/v1beta1/extensions.proto";

// 这是一个CHECK模板，可选的种类 CHECK, REPORT, QUOTA, or ATTRIBUTE_GENERATOR
// 种类决定了在Mixer处理流水线的什么地方调用消费此模板的适配器
option (istio.mixer.adapter.model.v1beta1.template_variety) = TEMPLATE_VARIETY_CHECK;


// 配置示例：
//
// apiVersion: "config.istio.io/v1alpha2"
// kind: listentry
// metadata:
//   name: appversion
//   namespace: istio-system
// spec:
//   实例的数据
//   value: source.labels["version"]

// 根据模板类型的不同，需要定义不同的消息。你总是需要定义一个名为Template的消息
message Template {
    // 实例的元数据，决定了在运行时，此模板的实例是什么形状，实例会发送给适配器进行处理
    string value = 1;
}</pre>
<p>&nbsp;</p>
<p>需要注意：Template消息上面的注释，将用作模板的文档，该文档同时面向适配器开发人员、运维操作人员。 </p>
<div class="blog_h3"><span class="graybg">OutputTemplate</span></div>
<p>对于ATTRIBUTE_GENERATOR类型的模板，还需要定义一个额外的OutputTemplate消息：</p>
<ol>
<li>Template消息，定义传递给使用该模板实例的适配器的输入</li>
<li>OutputTemplate消息，定义上述适配器需要返回的输出</li>
</ol>
<div class="blog_h3"><span class="graybg">字段类型</span></div>
<p>注意：目前不支持内嵌Message，enum，oneof，repeated。</p>
<p>可以在Proto中使用的模板字段类型包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模板字段类型</td>
<td style="width: 30%; text-align: center;">Go字段类型</td>
</tr>
</thead>
<tbody>
<tr>
<td>string</td>
<td>string</td>
</tr>
<tr>
<td>int64</td>
<td>int64</td>
</tr>
<tr>
<td>double</td>
<td>float64</td>
</tr>
<tr>
<td>bool</td>
<td>bool</td>
</tr>
<tr>
<td>istio.mixer.adapter.model.v1beta1.TimeStamp</td>
<td>time.Time</td>
</tr>
<tr>
<td>istio.mixer.adapter.model.v1beta1.Duration</td>
<td>time.Duration</td>
</tr>
<tr>
<td>istio.mixer.adapter.model.v1beta1.IPAddress</td>
<td>net.IP</td>
</tr>
<tr>
<td>istio.mixer.adapter.model.v1beta1.DNSName</td>
<td>adapter.DNSName</td>
</tr>
<tr>
<td>istio.mixer.adapter.model.v1beta1.Value</td>
<td>interface{}</td>
</tr>
<tr>
<td>map&lt;string, string&gt;</td>
<td>map[string]string</td>
</tr>
<tr>
<td>map&lt;string, int64&gt;</td>
<td>map[string]int64</td>
</tr>
<tr>
<td>map&lt;string, double&gt;</td>
<td>map[string]float64</td>
</tr>
<tr>
<td>map&lt;string, bool&gt;</td>
<td>map[string]bool</td>
</tr>
<tr>
<td>map&lt;string, istio.mixer.adapter.model.v1beta1.TimeStamp&gt;</td>
<td>map[string]time.Time</td>
</tr>
<tr>
<td>map&lt;string, istio.mixer.adapter.model.v1beta1.Duration&gt;</td>
<td>map[string]time.Duration</td>
</tr>
<tr>
<td>map&lt;string, istio.mixer.adapter.model.v1beta1.IPAddress&gt;</td>
<td>map[string]net.IP</td>
</tr>
<tr>
<td>map&lt;string, istio.mixer.adapter.model.v1beta1.DNSName&gt;</td>
<td>map[string]adapter.DNSName</td>
</tr>
<tr>
<td>map&lt;string, istio.mixer.adapter.model.v1beta1.Value&gt;</td>
<td>map[string]interface{}</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">生成的代码</span></div>
<p>基于上述Proto文件生成的Go代码包括：</p>
<ol>
<li>InstanceMsg结构：定义了在请求期间传递给适配器的数据的结构。Mixer会基于请求属性和你给出的配置，生成此结构的实例</li>
<li>OutputMsg结构：仅AttributeGenerator模板生成此结构。定义在属性生成阶段，适配器应当返回的数据的结构</li>
<li>Handler***Service服务：定义Mixer用来分发InstanceMsg消息给适配器时使用的gRPC接口</li>
<li>Type结构：如果InstanceMsg中的某些字段的数据类型是动态的（istio.policy.v1beta1.Value），则你提供的配置决定这些字段的<a href="https://github.com/istio/api/blob/master/policy/v1beta1/value_type.proto">真实类型</a></li>
</ol>
<p>注意：生成的服务接口，由<span style="background-color: #c0c0c0;">消费模板实例的那些适配器负责实现</span>。</p>
<div class="blog_h2"><span class="graybg">示例</span></div>
<div class="blog_h3"><span class="graybg">REPORT模板</span></div>
<p>这是内置的metric模板的例子：</p>
<pre class="crayon-plain-tag">syntax = "proto3";

package metric;

import "mixer/adapter/model/v1beta1/type.proto";
import "mixer/adapter/model/v1beta1/extensions.proto";

option (istio.mixer.v1.config.template.template_variety) = TEMPLATE_VARIETY_REPORT;

// 表示需要报告的单个数据
message Template {
   // 报告的值
   istio.mixer.adapter.model.v1beta1.Value value = 1;

   // 唯一性标识此指标的维度列表
   map&lt;string, istio.mixer.adapter.model.v1beta1.Value&gt; dimensions = 2;
}</pre>
<p>自动生成如下供适配器使用的Proto定义：</p>
<pre class="crayon-plain-tag">// 需要处理请求期间metric类型的实例的适配器，都需要实现该服务接口
service HandleMetricService {
    // 处理指标
    rpc HandleMetric(HandleMetricRequest) returns (istio.mixer.adapter.model.v1beta1.ReportResult);

}

// 请求消息结构
message HandleMetricRequest {

    // metric的实例
    repeated InstanceMsg instances = 1;

    // 适配器特定的Handler配置
    //
    // 注意：可以实现InfrastructureBackend服务，从而可以在会话创建（InfrastructureBackend.CreateSession）期间
    // 接收处理器配置。在这种情况下，adapter_config会包含type_url: google.protobuf.Any.type_url字段，并且包含
    // 由InfrastructureBackend.CreateSession调用返回的session_id: string
    google.protobuf.Any adapter_config = 2;

    // 用于去除针对Mixer的重复调用
    string dedup_id = 3;
}

// metric模板的载荷
message InstanceMsg {

    // 实例名
    string name = 72295727;

    // 报告的值
    istio.policy.v1beta1.Value value = 1;

    // 指标的维度
    map&lt;string, istio.policy.v1beta1.Value&gt; dimensions = 2;
}

// 包含推断出的、metric模板实例的类型信息
// 在配置期间，通过InfrastructureBackend.CreateSession调用传递
message Type {

    // The value being reported.
    istio.policy.v1beta1.ValueType value = 1;

    // The unique identity of the particular metric to report.
    map&lt;string, istio.policy.v1beta1.ValueType&gt; dimensions = 2;
}</pre>
<div class="blog_h3"><span class="graybg">CHECK模板</span></div>
<p>这是内置listentry模板的例子： </p>
<pre class="crayon-plain-tag">syntax = "proto3";

package listentry;

import "mixer/adapter/model/v1beta1/extensions.proto";

option (istio.mixer.v1.config.template.template_variety) = TEMPLATE_VARIETY_CHECK;

message Template {
    string value = 1;
}</pre>
<p>自动生成如下供适配器使用的Proto定义：</p>
<pre class="crayon-plain-tag">service HandleListEntryService {
    rpc HandleListEntry(HandleListEntryRequest) returns (istio.mixer.adapter.model.v1beta1.CheckResult);
}

message HandleListEntryRequest {
    InstanceMsg instance = 1;
    google.protobuf.Any adapter_config = 2;
    string dedup_id = 3;
}

message InstanceMsg {
    string name = 72295727;
    string value = 1;
}

message Type {
}</pre>
<div class="blog_h3"><span class="graybg">QUOTA模板</span></div>
<pre class="crayon-plain-tag">package quota;

import "policy/v1beta1/type.proto";
import "mixer/adapter/model/v1beta1/extensions.proto";

option (istio.mixer.adapter.model.v1beta1.template_variety) = TEMPLATE_VARIETY_QUOTA;

message Template {
    map&lt;string, istio.policy.v1beta1.Value&gt; dimensions = 1;
}</pre>
<p>自动生成如下供适配器使用的Proto定义：  </p>
<pre class="crayon-plain-tag">service HandleQuotaService {
    rpc HandleQuota(HandleQuotaRequest) returns (istio.mixer.adapter.model.v1beta1.QuotaResult);

}

message HandleQuotaRequest {

    InstanceMsg instance = 1;
    google.protobuf.Any adapter_config = 2;
    string dedup_id = 3;
    istio.mixer.adapter.model.v1beta1.QuotaRequest quota_request = 4;
}

message InstanceMsg {
    string name = 72295727;
    map&lt;string, istio.policy.v1beta1.Value&gt; dimensions = 1;

}

message Type {
    map&lt;string, istio.policy.v1beta1.ValueType&gt; dimensions = 1;
} </pre>
<div class="blog_h1"><span class="graybg">开发适配器</span></div>
<p>注意：早先Istio支持扩展进程内的适配器，这种适配器（和内置适配器一样）是在Mixer进程内部运行的。目前进程内适配器已经被弃用，应该考虑开发进程外（Out Of Process）的gRPC适配器。</p>
<p>适配器将Mixer和各种基础设施后端，例如负责指标采集的Prometheus、负责日志收集的Fluentd，集成起来。Mixer是一个属性处理引擎，它负责基于用户提供的配置来将请求属性映射为适配器输入参数，然后通过适配器来调用后端系统。</p>
<div class="blog_h2"><span class="graybg">两种实现方式</span></div>
<p>gRPC适配器可以由两种实现模型——基于会话或者无会话的。</p>
<div class="blog_h3"><span class="graybg">基于会话</span></div>
<p>注意：目前尚未支持。</p>
<p>Mixer仅仅在使用会话标识符创建会话时，将适配器的配置信息传递给适配器一次。未来Mixer和适配器的通信，均是基于会话标识符，你需要通过此标识符来引用最初传入的配置。</p>
<p>基于这种实现模型的适配器，需要在实现Handle***服务的同时实现<a href="https://github.com/istio/api/blob/master/mixer/adapter/model/v1beta1/infrastructure_backend.proto">InfrastructureBackend</a>服务，Mixer调用后者方法的时序如下：</p>
<ol>
<li>调用Validate方法</li>
<li>调用CreateSession方法，返回的session_id将用作后续的
<ol>
<li>针对Handle***的实时调用</li>
<li>最终的CloseSession调用</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">无会话</span></div>
<p>适配器仅仅需要实现Handle***服务，Mixer仅仅在请求（Check/Report/Quota）时期和适配器交互，每次交互都传递完整的适配器配置信息。</p>
<div class="blog_h2"><span class="graybg">添加到Mixer</span></div>
<p>每种适配器都需要提供一个资源配置，你需要在Istio的配置存储中添加该配置。<span style="background-color: #c0c0c0;">资源配置是adapter类型的CR</span>。</p>
<p>创建这种资源配置的方法有两种：</p>
<ol>
<li>调用工具<pre class="crayon-plain-tag">mixer/tool/mixgen</pre>，创建一个adapter资源：<br />
<pre class="crayon-plain-tag">//go:generate go run $GOPATH/src/istio.io/istio/mixer/tools/mixgen/main.go adapter \
  # 适配器类型    是否基于会话  消费的模板类型
  -n mygrpcadapter -s=false -t metric  \
  -c $GOPATH/src/istio.io/istio/mixer/adapter/mygrpcadapter/config/config.proto_descriptor \
  -o mygrpcadapter-nosession.yaml</pre>
</li>
<li>
<p>使用<pre class="crayon-plain-tag">mixer_codegen.sh -a</pre> 命令传入适配器的config.proto：</p>
<p><pre class="crayon-plain-tag">//go:generate $GOPATH/src/istio.io/istio/bin/mixer_codegen.sh \
  -a mixer/adapter/mygrpcadapter/config/config.proto -x "-n mygrpcadapter -s=false -t metric "</pre>
</li>
</ol>
<p>不管使用哪种方式，命令都可以作为<pre class="crayon-plain-tag">go generate</pre>阶段的一部分自动执行，都会生成adapter类型的CR。对于上面的例子，会生成一个无会话的、支持metric模板的，名为mygrpcadapter的资源配置，适配器的配置的Proto（声明该适配器支持哪些配置项）也会被编码到其中：</p>
<pre class="crayon-plain-tag">apiVersion: "config.istio.io/v1alpha2"
kind: adapter
metadata:
  name: mygrpcadapter
  namespace: istio-system
spec:
  description:
  # 是否基于会话
  session_based: false
  # 支持的模板
  templates:
  - metric
  # 适配器配置的Proto
  config: CsD3AgogZ29vZ2xlL3Byb3RvYnVmL2Rlc2NyaXB0b3.....</pre>
<div class="blog_h2"><span class="graybg">测试适配器 </span></div>
<p>Istio提供了一个简单的用于测试适配器的框架。该框架会：</p>
<ol>
<li>创建一个进程内的Mixer gRPC服务器，该服务器使用基于本地文件系统的配置存储。</li>
<li>创建一个Mixer gRPC客户端</li>
</ol>
<p>测试框架的实现位于pkg/adapter/test/integration.go。Istio提供了基于此测试框架来<a href="https://github.com/istio/istio/blob/master/mixer/test/prometheus/prometheus_integration_test.go">测试Prometheus REPORT适配器</a>的例子。</p>
<div class="blog_h2"><span class="graybg">生成CRD</span></div>
<p>Mixs支持为自定义的适配器生成专门的CRD，执行下面的命令：</p>
<pre class="crayon-plain-tag">$GOPATH/out/linux_amd64/release/mixs crd adapter</pre>
<p>此Mixer内嵌的适配器的CRD信息会打印到控制台。找到自定义适配器的CRD，用kubectl命令存储到K8S中即可。</p>
<p>有了专门的CRD后，你不需要使用通用的handler来创建处理器，直接创建CR即可。</p>
<div class="blog_h1"><span class="graybg">适配器示例</span></div>
<div class="blog_h2"><span class="graybg">REPORT适配器</span></div>
<p>本节给出实现、测试、插入一个简单的进程外gRPC适配器的完整例子。该适配器：</p>
<ol>
<li>支持metric模板</li>
<li>对于每个请求，打印它从Mixer接收的数据到文件</li>
</ol>
<div class="blog_h3"><span class="graybg">准备</span></div>
<p>在开始前，请参考“如何贡献”一节，签出Istio代码，准备好环境：</p>
<ol>
<li>你需要安装<a href="https://github.com/protocolbuffers/protobuf/releases">3.5.1</a>或者更高版本的<span style="color: #24292e;">protoc（Protocol编译器）</span></li>
<li>设置环境变量：<br />
<pre class="crayon-plain-tag">export MIXER_REPO=$GOPATH/src/istio.io/istio/mixer
export ISTIO=$GOPATH/src/istio.io </pre>
</li>
<li>确保Mixer能构建成功：<br />
<pre class="crayon-plain-tag">pushd $ISTIO/istio &amp;&amp; make mixs</pre>
</li>
</ol>
<div class="blog_h3"><span class="graybg">编写骨架代码</span></div>
<p>在Istio源码树中为新的适配器创建目录：</p>
<pre class="crayon-plain-tag">cd $MIXER_REPO/adapter &amp;&amp; mkdir mygrpcadapter &amp;&amp; cd mygrpcadapter</pre>
<p>然后在mygrpcadapter.go中编写如下骨架代码：</p>
<pre class="crayon-plain-tag">package mygrpcadapter

import (
	"context"
	"fmt"
	"net"

	"google.golang.org/grpc"

	"istio.io/api/mixer/adapter/model/v1beta1"
	"istio.io/istio/mixer/template/metric"
)

type (
	// gRPC服务器的接口
	Server interface {
		Addr() string
		Close() error
		Run(shutdown chan error)
	}

	// 适配器结构
	MyGrpcAdapter struct {
		listener net.Listener
		server   *grpc.Server
	}
)

// 该适配器消费metric实例，因此必须实现下面的接口
var _ metric.HandleMetricServiceServer = &amp;MyGrpcAdapter{}

// 编写所有接口方法的骨架

/* 实现HandleMetricServiceServer */
func (s *MyGrpcAdapter) HandleMetric(ctx context.Context, r *metric.HandleMetricRequest) (*v1beta1.ReportResult, error) {
	return nil, nil
}


/* 实现Server */
func (s *MyGrpcAdapter) Addr() string {
	return s.listener.Addr().String()
}
func (s *MyGrpcAdapter) Run(shutdown chan error) {
        // 传递监听器，启动gRPC服务器
	shutdown&lt;- s.server.Serve(s.listener)
}

// 优雅关闭服务器，测试用
func (s *MyGrpcAdapter) Close() error {
	if s.server != nil {
		s.server.GracefulStop()
	}

	if s.listener != nil {
		_ = s.listener.Close()
	}

	return nil
}

// 创建gRPC服务器并监听
func NewMyGrpcAdapter(addr string) (Server, error) {
	if addr == "" {
		addr = "0"
	}
	listener, err := net.Listen("tcp", fmt.Sprintf(":%s", addr))
	if err != nil {
		return nil, fmt.Errorf("unable to listen on socket: %v", err)
	}
	s := &amp;MyGrpcAdapter{
		listener: listener,
	}
	fmt.Printf("listening on \"%v\"\n", s.Addr())
	s.server = grpc.NewServer()
	metric.RegisterHandleMetricServiceServer(s.server, s)
	return s, nil
}</pre>
<p>执行下面的命令，确保能构建成功：</p>
<pre class="crayon-plain-tag">go build ./...</pre>
<div class="blog_h3"><span class="graybg">编写适配器配置</span></div>
<p>我们开发的适配器需要将接收到的信息打印到文件中，因此需要一个参数，提供文件的路径。</p>
<p>创建mygrpcadapter/config子目录，然后创建Proto文件config.proto：</p>
<pre class="crayon-plain-tag">syntax = "proto3";

// 包名
package adapter.mygrpcadapter.config;

import "gogoproto/gogo.proto";

// 生成的Go代码使用的包名
option go_package="config";

// 适配器的配置，使用Params消息表示
message Params {
    // 文件路径
    string file_path = 1;
}</pre>
<p>我们需要从上述Proto生成对应的Go源码，以及适配器的adaptor CR。 在适配器源码上添加以下注释：</p>
<pre class="crayon-plain-tag">// nolint:lll
//go:generate $GOPATH/src/istio.io/istio/bin/mixer_codegen.sh -a mixer/adapter/mygrpcadapter/config/config.proto -x "-s=false -n mygrpcadapter -t metric"

package mygrpcadapter</pre>
<p>并执行下面的命令：</p>
<pre class="crayon-plain-tag">go generate ./...
go build ./...</pre>
<p>如果一切正常，会生成以下文件：</p>
<ol>
<li>类型为adapter的自定义资源，<span style="background-color: #c0c0c0;">此资源提供自定义适配器的元数据，包括是否基于会话、描述、适配器参数信息</span>：<br />
<pre class="crayon-plain-tag">apiVersion: "config.istio.io/v1alpha2"
kind: adapter
metadata:
  name: mygrpcadapter
  namespace: istio-system
spec:
  description:
  session_based: false
  templates:
  - metric
  Config: ... </pre>
</li>
<li>Config.pb.go，适配器的配置的Go代码</li>
<li>mysampleadapter.config.pb.html，适配器的文档</li>
<li>Config.proto_descriptor，一个中介文件，适配器代码不会直接使用它</li>
</ol>
<div class="blog_h3"><span class="graybg">完善业务逻辑</span></div>
<p>适配器完整的代码如下： </p>
<pre class="crayon-plain-tag">// nolint:lll
// Generates the mygrpcadapter adapter's resource yaml. It contains the adapter's configuration, name, supported template
// names (metric in this case), and whether it is session or no-session based.
//go:generate $GOPATH/src/istio.io/istio/bin/mixer_codegen.sh -a mixer/adapter/mygrpcadapter/config/config.proto -x "-s=false -n mygrpcadapter -t metric"

package mygrpcadapter

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io/ioutil"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"

	"bytes"
	"os"

	"istio.io/api/mixer/adapter/model/v1beta1"
	policy "istio.io/api/policy/v1beta1"
	"istio.io/istio/mixer/adapter/mygrpcadapter/config"
	"istio.io/istio/mixer/template/metric"
	"istio.io/istio/pkg/log"
)

type (
	Server interface {
		Addr() string
		Close() error
		Run(shutdown chan error)
	}

	MyGrpcAdapter struct {
		listener net.Listener
		server   *grpc.Server
	}
)

var _ metric.HandleMetricServiceServer = &amp;MyGrpcAdapter{}

func (s *MyGrpcAdapter) HandleMetric(ctx context.Context, r *metric.HandleMetricRequest) (*v1beta1.ReportResult, error) {

	log.Infof("received request %v\n", *r)
	var b bytes.Buffer
        // 配置参数
	cfg := &amp;config.Params{}

	if r.AdapterConfig != nil {
                //  将请求中附带的适配器配置进行反序列化处理
		if err := cfg.Unmarshal(r.AdapterConfig.Value); err != nil {
			log.Errorf("error unmarshalling adapter config: %v", err)
			return nil, err
		}
	}

	b.WriteString(fmt.Sprintf("HandleMetric invoked with:\n  Adapter config: %s\n  Instances: %s\n", cfg.String(), instances(r.Instances)))
	if cfg.FilePath == "" {
		fmt.Println(b.String())
	} else {
               // 输出到文件
		_, err := os.OpenFile("out.txt", os.O_RDONLY|os.O_CREATE, 0666)
		f, err := os.OpenFile(cfg.FilePath, os.O_APPEND|os.O_WRONLY, 0600)
		defer f.Close()
		log.Infof("writing instances to file %s", f.Name())
	}
        // 返回空的报告结果
	return &amp;v1beta1.ReportResult{}, nil
}

// 解码metric的维度，注意维度值类型可以是动态的
func decodeDimensions(in map[string]*policy.Value) map[string]interface{} {
	out := make(map[string]interface{}, len(in))
	for k, v := range in {
		out[k] = decodeValue(v.GetValue())
	}
	return out
}

// 解码metric的值，注意值的类型可以是动态的
func decodeValue(in interface{}) interface{} {
	switch t := in.(type) {
	case *policy.Value_StringValue:
		return t.StringValue
	case *policy.Value_Int64Value:
		return t.Int64Value
	case *policy.Value_DoubleValue:
		return t.DoubleValue
	default:
		return fmt.Sprintf("%v", in)
	}
}

func instances(in []*metric.InstanceMsg) string {
	var b bytes.Buffer
        // 对于每个InstanceMsg，解码其值、维度，并打印
	for _, inst := range in {
		b.WriteString(fmt.Sprintf("'%s':\n"+
			"  {\n"+
			"		Value = %v\n"+
			"		Dimensions = %v\n"+
			"  }", inst.Name, decodeValue(inst.Value.GetValue()), decodeDimensions(inst.Dimensions)))
	}
	return b.String()
}

// ...</pre>
<p>编写一个main函数，以独立进程的形式启动该适配器：</p>
<pre class="crayon-plain-tag">package main

import (
	"fmt"
	"os"

	mygrpcadapter "istio.io/istio/mixer/adapter/mygrpcadapter"
)

func main() {
	addr := ""
	if len(os.Args) &gt; 1 {
		addr = os.Args[1]
	}
	s, err := mygrpcadapter.NewMyGrpcAdapter(addr)
	shutdown := make(chan error, 1)
	go func() {
		s.Run(shutdown)
	}()
	_ = &lt;-shutdown
}</pre>
<div class="blog_h3"><span class="graybg">编写Istio配置</span></div>
<p>要使用上述适配器，你需要配置三类Istio资源：</p>
<ol>
<li>处理器（Handler）：为适配器提供配置参数</li>
<li>实例（Instance）：指定如何从请求属性来生成实例，在这里就是metric</li>
<li>规则（Rule） ：将处理器和实例组合起来</li>
</ol>
<p>配置示例如下：</p>
<pre class="crayon-plain-tag"># 处理器配置
apiVersion: "config.istio.io/v1alpha2"
# 那些基于内置适配器的处理器，类型可以是prometheus, fluentd ... 
# 基于自定义适配器的，可以统一配置为handler
kind: handler
metadata:
 name: h1
 namespace: istio-system
spec:
 # 需要指定适配器类型
 adapter: mygrpcadapter
 connection:
   # address: "{ADDRESS}"
   address: "127.0.0.1：38355"
 # 适配器参数
 params:
   file_path: "out.txt"
---

# 模板metric的实例
apiVersion: "config.istio.io/v1alpha2"
kind: instance
metadata:
 name: i1metric
 namespace: istio-system
spec:
 template: metric
 params:
   value: request.size | 0
   dimensions:
     response_code: "200"
---

# 规则
apiVersion: "config.istio.io/v1alpha2"
kind: rule
metadata:
 name: r1
 namespace: istio-system
spec:
 actions:
 - handler: h1.istio-system
   instances:
   - i1metric
---</pre>
<div class="blog_h3"><span class="graybg">启动Mixer并验证适配器 </span></div>
<p>首先启动适配器，注意我们没有指定端口，随机分配的监听端口会打印到标准输出：</p>
<pre class="crayon-plain-tag">export ISTIO=$GOPATH/src/istio.io
export MIXER_REPO=$GOPATH/src/istio.io/istio/mixer
cd $MIXER_REPO/adapter/mygrpcadapter
go run cmd/main.go 127.0.0.1：38355</pre>
<p>我们使用文件系统作为Mixer的配置存储， 将所有配置文件拷贝到同一目录：</p>
<pre class="crayon-plain-tag">mkdir testdata
# 处理器、实例、规则
cp sample_operator_cfg.yaml $MIXER_REPO/adapter/mygrpcadapter/testdata
# 适配器CR
cp config/mygrpcadapter.yaml $MIXER_REPO/adapter/mygrpcadapter/testdata
# Istio供测试使用的属性清单
cp $MIXER_REPO/testdata/config/attributes.yaml $MIXER_REPO/adapter/mygrpcadapter/testdata
# Metric模板
cp $MIXER_REPO/template/metric/template.yaml $MIXER_REPO/adapter/mygrpcadapter/testdata</pre>
<p>构建Mixer，并从上述配置存储启动它：</p>
<pre class="crayon-plain-tag">pushd $ISTIO/istio &amp;&amp; make mixs
$GOPATH/out/linux_amd64/release/mixs server --configStoreURL=fs://$(pwd)/mixer/adapter/mygrpcadapter/testdata</pre>
<p>启动Mixer后，可以用命令行工具mixc来向Mixer报告：</p>
<pre class="crayon-plain-tag">pushd $ISTIO/istio &amp;&amp; make mixc
                                            # 报告字符串属性                            报告整数属性
$GOPATH/out/linux_amd64/release/mixc report -s destination.service="svc.cluster.local" -i request.size=1235</pre>
<p>打开输出文件，应该可以看到如下内容：</p>
<pre class="crayon-plain-tag">HandleMetric invoked with:
  Adapter config: &amp;Params{FilePath:out.txt,}
  Instances: 'i1metric.instance.istio-system':
  {
		Value = 1235
		Dimensions = map[response_code:200]
  }</pre>
<div class="blog_h3"><span class="graybg">编写测试 </span></div>
<p>你可以利用pkg/adapter/test包编写集成测试，启动进程内的Mixer服务器，并通过Mixer客户端调用它：</p>
<pre class="crayon-plain-tag">package mygrpcadapter

import (
  "fmt"
  "io/ioutil"
  "testing"

  adapter_integration "istio.io/istio/mixer/pkg/adapter/test"
  "os"
  "strings"
)

func TestReport(t *testing.T) {
  // 读取适配器的CR
  adptCrBytes, err := ioutil.ReadFile("config/mygrpcadapter.yaml")
  if err != nil {
     t.Fatalf("could not read file: %v", err)
  }
  // 读取处理器、实例、规则配置
  operatorCfgBytes, err := ioutil.ReadFile("sample_operator_cfg.yaml")
  if err != nil {
     t.Fatalf("could not read file: %v", err)
  }
  operatorCfg := string(operatorCfgBytes)
  shutdown := make(chan error, 1)

  // 输出文件
  var outFile *os.File
  outFile, err = os.OpenFile("out.txt", os.O_RDONLY|os.O_CREATE, 0666)
  if err != nil {
     t.Fatal(err)
  }
  defer func() {
     // 测试完毕后移除输出文件
     if removeErr := os.Remove(outFile.Name()); removeErr != nil {
        t.Logf("Could not remove temporary file %s: %v", outFile.Name(), removeErr)
     }
  }()

  // 适配器集成测试框架
  adapter_integration.RunTest(
     t,
     nil,
     // Scenario定义一个完整的集成测试场景
     adapter_integration.Scenario{

        // 测试前的准备
        Setup: func() (ctx interface{}, err error) {
           // 创建适配器
           pServer, err := NewMyGrpcAdapter("")
           if err != nil {
              return nil, err
           }
           go func() {
              // 启动服务器
              pServer.Run(shutdown)
              _ = &lt;-shutdown
           }()
           return pServer, nil
        },
        // 测试后清理
        Teardown: func(ctx interface{}) {
           s := ctx.(Server)
           s.Close()
        },
        // 需要对Mixer并行发起的调用列表
        ParallelCalls: []adapter_integration.Call{
           {
              CallKind: adapter_integration.REPORT,
              Attrs:    map[string]interface{}{"request.size": int64(555)},
           },
        },
        // 测试结果验证
        GetState: func(ctx interface{}) (interface{}, error) {
           bytes, err := ioutil.ReadFile("out.txt")
           if err != nil {
              return nil, err
           }
           s := string(bytes)
           wantStr := `HandleMetric invoked with:
               Adapter config: &amp;Params{FilePath:out.txt,}
               Instances: 'i1metric.instance.istio-system':
               {
                   Value = 555
                   Dimensions = map[response_code:200]
               }
           `
           // 断言失败
           if normalize(s) != normalize(wantStr) {
              return nil, fmt.Errorf("got adapters state as : '%s'; want '%s'", s, wantStr)
           }
           return nil, nil
        },
        // Mixer需要读取的CRDs数组
        GetConfig: func(ctx interface{}) ([]string, error) {
           s := ctx.(Server)
           return []string{
              string(adptCrBytes),
              strings.Replace(operatorCfg, "{ADDRESS}", s.Addr(), 1),
           }, nil
        },

        // 期望的测试结果的JSON字符串形式
        Want: `
            {
             "AdapterState": null,
             "Returns": [
              {
               "Check": {
                "Status": {},
                "ValidDuration": 0,
                "ValidUseCount": 0
               },
               "Quota": null,
               "Error": null
              }
             ]
            }
        `,
     },
  )
}</pre>
<p>执行测试：<pre class="crayon-plain-tag">cd $MIXER_REPO/adapter/mygrpcadapter &amp;&amp; go build ./... &amp;&amp; go test *.go</pre></p>
<div class="blog_h3"><span class="graybg">通信加密</span></div>
<p>Istio支持基于mTLS来保护任何工作负载之间的通信，mTLS同样可以用于进程外适配器和Mixer之间的流量。</p>
<p>任何处理器都可以指定基于mTLS进行双向认证：</p>
<pre class="crayon-plain-tag">apiVersion: "config.istio.io/v1alpha2"
kind: handler
metadata:
 name: h1
 namespace: istio-system
spec:
 adapter: mygrpcadapter
 connection:
  address: "{ADDRESS}" #replaces at runtime by the test
  authentication:
    # 这些数字证书文件必须位于Mixer服务器对应目录
    mutual:
      private_key: "/tmp/grpc-test-key-cert/mixer.key"
      client_certificate: "/tmp/grpc-test-key-cert/mixer.crt"
      ca_certificates: "/tmp/grpc-test-key-cert/ca.pem"</pre>
<p> 改造我们的适配器，使其支持TLS：</p>
<pre class="crayon-plain-tag">// 创建适配器的TLS选项
func getServerTLSOption(credential, privateKey, caCertificate string) (grpc.ServerOption, error) {
        // 从文件加载X509密钥对
	certificate, err := tls.LoadX509KeyPair(
		credential,
		privateKey,
	)
        // 证书池
	certPool := x509.NewCertPool()
	bs, err := ioutil.ReadFile(caCertificate)
        // 将CA证书加入证书池
	ok := certPool.AppendCertsFromPEM(bs)

        // TLS配置
	tlsConfig := &amp;tls.Config{
		Certificates: []tls.Certificate{certificate},
		ClientCAs:    certPool,
	}
        // 要求客户端（Mixer）提供证书，并基于CA验证证书的合法性
	tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
        // 返回ServerOption
	return grpc.Creds(credentials.NewTLS(tlsConfig)), nil
}

func NewMyGrpcAdapter(addr string) (Server, error) {
	if addr == "" {
		addr = "0"
	}
	listener, err := net.Listen("tcp", fmt.Sprintf(":%s", addr))
	if err != nil {
		return nil, fmt.Errorf("unable to listen on socket: %v", err)
	}
	s := &amp;MyGrpcAdapter{
		listener: listener,
	}
	fmt.Printf("listening on \"%v\"\n", s.Addr())

        // 适配器使用的证书
	credential := os.Getenv("GRPC_ADAPTER_CREDENTIAL")
	privateKey := os.Getenv("GRPC_ADAPTER_PRIVATE_KEY")
	certificate := os.Getenv("GRPC_ADAPTER_CERTIFICATE")
	if credential != "" {
                // 获取TLS选项
		so, err := getServerTLSOption(credential, privateKey, certificate)
                // 使用该选项创建gRPC服务器
		s.server = grpc.NewServer(so)
	} else {
		s.server = grpc.NewServer()
	}
	metric.RegisterHandleMetricServiceServer(s.server, s)
	return s, nil
}</pre>
<div class="blog_h2"><span class="graybg">属性生成适配器</span></div>
<p>从Istio 1.1开始支持进程外的属性生成适配器，这类适配器需要实现某种<span style="color: #24292e;">ATTRIBUTE_GENERATOR类型的模板（所生成的接口）。这类适配器的目的是在执行Check/Report调用之前，添加额外的属性。</span></p>
<p>本节，我们会实现一个名为<span style="color: #24292e;">mapper的简单属性生成适配器，它提供一个额外的属性值。</span></p>
<div class="blog_h3"><span class="graybg">定义模板</span></div>
<p>注意：如果Istio内置的模板能满足需要，不要定义自己的模板。在K8S环境下，属性生成器kubernetesenv开箱即用，可以抽取工作负载的各种元数据。</p>
<p>首先在Istio源码树中为我们的适配器创建一个目录：</p>
<pre class="crayon-plain-tag">mkdir -p $GOPATH/src/istio.io/ &amp;&amp; \
cd $GOPATH/src/istio.io/  &amp;&amp; \
git clone https://github.com/istio/istio
cd istio

mkdir -p mixer/adapter/mapper</pre>
<p>然后，定义如下的模板：</p>
<pre class="crayon-plain-tag">syntax = "proto3";
package mapper;
import "mixer/adapter/model/v1beta1/extensions.proto";
option (istio.mixer.adapter.model.v1beta1.template_variety) = TEMPLATE_VARIETY_ATTRIBUTE_GENERATOR;
message Template {
  string key = 1;
}
message OutputTemplate {
  string value = 1;
}</pre>
<p>执行下面的命令，从该模板生成相关文件：</p>
<pre class="crayon-plain-tag">bin/mixer_codegen.sh -t mixer/adapter/mapper/template.proto</pre>
<div class="blog_h3"><span class="graybg">实现适配器 </span></div>
<pre class="crayon-plain-tag">package mapper

import context "golang.org/x/net/context"

type MyAdapter struct{}

// 模板暴露的方法，处理输入模板规定的消息，返回输出模板规定的消息
func (MyAdapter) HandleMapper(_ context.Context, req *HandleMapperRequest) (*OutputMsg, error) {
        lookup := map[string]string{
                "hello": "world",
        }
        // Instance.Key，对应上面模板的Template消息的key字段，注意自动大写
        return &amp;OutputMsg{Value: lookup[req.Instance.Key]}, nil
        // 返回值对存储到OutputTmmplate.value
}</pre>
<div class="blog_h3"><span class="graybg">主函数</span></div>
<pre class="crayon-plain-tag">package main

import (
        "net"
        "google.golang.org/grpc"
        "istio.io/istio/mixer/adapter/mapper"
)

func main() {
        listener, err := net.Listen("tcp", ":38355")
        server := grpc.NewServer()
        // 注册服务实现到gRPC服务器
        mapper.RegisterHandleMapperServiceServer(server, mapper.MyAdapter{})
        server.Serve(listener)
}</pre>
<div class="blog_h3"><span class="graybg">配置适配器 </span></div>
<p>所有适配器都需要提供配置参数，这样Mixer才能调用适配器。对于这个例子，我们只需要一个空的配置参数（Schema）即可：</p>
<pre class="crayon-plain-tag">syntax = "proto3";
package config;
message Params{}</pre>
<p>生成对应的Go代码：</p>
<pre class="crayon-plain-tag">bin/mixer_codegen.sh -a mixer/adapter/mapper/config/config.proto -x "-s=false -n myadapter -t mapper"</pre>
<div class="blog_h3"><span class="graybg">编写Istio配置</span></div>
<pre class="crayon-plain-tag"># template资源
kubectl apply -f mixer/adapter/mapper/template.yaml

# 适配器的adaptor资源
kubectl apply -f mixer/adapter/mapper/config/myadapter.yaml

# 处理器
cat &lt;&lt;EOF | kubectl create -f -
apiVersion: config.istio.io/v1alpha2
kind: handler
metadata:
  name: h1
  namespace: istio-system
spec:
  adapter: myadapter
  connection:
    address: ":9070"
  params: {}
EOF

# 模板实例
cat &lt;&lt;EOF | kubectl create -f -
apiVersion: config.istio.io/v1alpha2
kind: instance
metadata:
  name: i1
  namespace: istio-system
spec:
  template: mapper
  params:
    key: destination.namespace
  attribute_bindings:
    source.namespace: output.value | "unknown"
EOF

# 规则
cat &lt;&lt;EOF | kubectl create -f -
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: r1
  namespace: istio-system
spec:
  actions:
  - handler: h1.istio-system
    instances: ["i1"]
EOF</pre>
<div class="blog_h3"><span class="graybg">使用新适配器</span></div>
<p>发起一个报告：</p>
<pre class="crayon-plain-tag">go run mixer/cmd/mixc/main.go report -s destination.namespace="hello"</pre>
<p>查看mixc的调试日志，可以看到在预处理期间生成的source.namespace属性：</p>
<pre class="crayon-plain-tag">debug   api     Dispatching Preprocess
debug   api     Dispatching to main adapters after running preprocessors
debug   api     Attribute Bag: 
destination.namespace         : hello
# 新生成的属性
source.namespace              : world</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/extend-istio">扩展Istio</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/extend-istio/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Istio Mixer与Envoy的交互机制解读</title>
		<link>https://blog.gmem.cc/interaction-between-istio-mixer-and-envoy</link>
		<comments>https://blog.gmem.cc/interaction-between-istio-mixer-and-envoy#comments</comments>
		<pubDate>Mon, 18 Mar 2019 07:50:34 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C++]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[PaaS]]></category>
		<category><![CDATA[ServiceMesh]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=25903</guid>
		<description><![CDATA[<p>前言 在前些日子的文章Istio Pilot与Envoy的交互机制解读中我们详细研究了Istio Pilot如何基于xDS协议和Envoy代理进行各种配置信息的交换。Istio的另一个核心组件是Mixer，它提供三类功能： 遥测报告（Telemetry Reporting），该功能是服务网格可观察性的基础。为服务启用日志记录、监控、追踪、计费流 前置条件检查（Precondition Checking），响应服务请求之前进行一系列检查，例如身份验证、白名单检查、ACL检查 配额管理（Quota Management），基于特定的维度进行配额，控制对受限资源的争用 本文结合源码分析Mixer的设计、实现细节，同时关注Envoy与它的集成机制。 代码结构 istio Mixer的代码位于mixer目录下： 子目录 说明 adapter 包含各种适配器的实现，适配器封装了Mixer和外部基础设施后端（例如Prometheus）交互的逻辑 cmd 包含以下可执行文件的入口点： mixc  用于和Mixer服务器实例进行交互的命令行客户端 <a class="read-more" href="https://blog.gmem.cc/interaction-between-istio-mixer-and-envoy">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/interaction-between-istio-mixer-and-envoy">Istio Mixer与Envoy的交互机制解读</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">前言</span></div>
<p>在前些日子的文章<a href="/interaction-between-istio-pilot-and-envoy">Istio Pilot与Envoy的交互机制解读</a>中我们详细研究了Istio Pilot如何基于xDS协议和Envoy代理进行各种配置信息的交换。Istio的另一个核心组件是Mixer，它提供三类功能：</p>
<ol>
<li>遥测报告（Telemetry Reporting），该功能是服务网格可观察性的基础。为服务启用日志记录、监控、追踪、计费流</li>
<li>前置条件检查（Precondition Checking），响应服务请求之前进行一系列检查，例如身份验证、白名单检查、ACL检查</li>
<li>配额管理（Quota Management），基于特定的维度进行配额，控制对受限资源的争用</li>
</ol>
<p>本文结合源码分析Mixer的设计、实现细节，同时关注Envoy与它的集成机制。</p>
<div class="blog_h1"><span class="graybg">代码结构</span></div>
<div class="blog_h2"><span class="graybg">istio</span></div>
<p>Mixer的代码位于<a href="https://github.com/istio/istio/tree/master/mixer">mixer目录</a>下：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">子目录</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>adapter</td>
<td>包含各种适配器的实现，适配器封装了Mixer和外部基础设施后端（例如Prometheus）交互的逻辑</td>
</tr>
<tr>
<td>cmd</td>
<td>
<p>包含以下可执行文件的入口点：</p>
<p style="padding-left: 30px;">mixc  用于和Mixer服务器实例进行交互的命令行客户端</p>
<p style="padding-left: 30px;">mixs  在本地启动一个Mixer服务器，或者列出可用的CRD、探测Mixer服务器的状态</p>
</td>
</tr>
<tr>
<td>docker</td>
<td>Docker镜像定义</td>
</tr>
<tr>
<td>template</td>
<td>
<p>模板，Mixer架构的基础构建块，通过自定义模板可以扩展Mixer</p>
<p>模板定义了将请求属性（Attribute）转换为适配器的输入的Schema（类型信息，使用Protubuf语法描述），每个适配器可以支持任意数量的template</p>
<p>模板决定了适配器会收到的数据、也决定了使用适配器必须创建的instance</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">Mixs启动过程</span></div>
<p>如果使用Istio官方默认的Chart来部署，则会创建istio-telemetry、istio-policy两套Deployment。它们的启动参数没有区别，分别负责Mixer的遥测、策略检查。这两个Deployment分别对应同名的Service，监听9091端口。</p>
<p>实际上网络监听是由Mixs的Sidecar，也就是Envoy负责的。Mixs Pod本身监听的是UDS  unix:///sock/mixer.socket，Envoy负责将9091端口的请求转发给此UDS。</p>
<div class="blog_h2"><span class="graybg">如何调试</span></div>
<p>在本地调试Mixer服务端时，参考如下启动参数：</p>
<pre class="crayon-plain-tag">mixs server --port 9091 --monitoringPort 9099  --log_output_level api:debug \
    --configStoreURL=k8s:///home/alex/.kube/config --configDefaultNamespace=istio-system</pre>
<div class="blog_h2"><span class="graybg">入口点</span></div>
<p>mixs server的入口点位于：</p>
<pre class="crayon-plain-tag">func main() {
// supportedTemplates()  map[string]template.Info
// supportedAdapters() []adptr.InfoFn
// 这两个方法都是自动生成的，包含编译的Mixer支持的模板、适配器的列表
// 模板/适配器信息中包含其属性清单
	rootCmd := cmd.GetRootCmd(os.Args[1:], supportedTemplates(), supportedAdapters(), shared.Printf, shared.Fatalf)

	if err := rootCmd.Execute(); err != nil {
		os.Exit(-1)
	}
}

func serverCmd(info map[string]template.Info, adapters []adapter.InfoFn, printf, fatalf shared.FormatFn) *cobra.Command {
// 默认Mixer参数
	sa := server.DefaultArgs()
// 使用自动生成的模板、适配器信息
	sa.Templates = info
	sa.Adapters = adapters

	serverCmd := &amp;cobra.Command{
		Use:   "server",
		Short: "Starts Mixer as a server",
		Run: func(cmd *cobra.Command, args []string) {
// 调用runServer启动服务
			runServer(sa, printf, fatalf)
		},
	}
}

func runServer(sa *server.Args, printf, fatalf shared.FormatFn) {
	// 创建服务器对象
	s, err := server.New(sa)
	// 启动gRPC服务
	s.Run()
	// 等待shutdown信号可读
	err = s.Wait()
        // 执行清理工作
	_ = s.Close()
}</pre>
<div class="blog_h3"><span class="graybg">server.New</span></div>
<p>该函数创建一个全功能的Mixer服务器，并且准备好接收请求：</p>
<pre class="crayon-plain-tag">func New(a *Args) (*Server, error) {
	return newServer(a, newPatchTable())
}</pre>
<div class="blog_h3"><span class="graybg">server.Run</span></div>
<p>该方法启动Mixs服务器：</p>
<pre class="crayon-plain-tag">func (s *Server) Run() {
// 准备好关闭通道
	s.shutdown = make(chan error, 1)
// 设置可用性状态，并通知探针控制器，探针被嵌入到Server
	s.SetAvailable(nil)
	go func() {
		// 启动gRPC服务，传入原始套接字的监听器对象
		err := s.server.Serve(s.listener)

		// 关闭通道
		s.shutdown &lt;- err
	}()
}</pre>
<div class="blog_h3"><span class="graybg">server.Wait</span></div>
<p>该方法很简单，就是在shutdown通道上等待。</p>
<div class="blog_h3"><span class="graybg">server.Close</span></div>
<p>该方法关闭Mixs服务器使用的各种资源。</p>
<div class="blog_h2"><span class="graybg">patchTable</span></div>
<p>newPatchTable创建一个新的patchTable结构：</p>
<pre class="crayon-plain-tag">func newPatchTable() *patchTable {
	return &amp;patchTable{
		newRuntime:    runtime.New,
		configTracing: tracing.Configure,
		startMonitor:  startMonitor,
		listen:        net.Listen,
		configLog:     log.Configure,
		runtimeListen: func(rt *runtime.Runtime) error { return rt.StartListening() },
	}
}</pre>
<p>此结构就是几个函数的集合：</p>
<pre class="crayon-plain-tag">type patchTable struct {
// 此函数创建一个Runtime，Runtime是Mixer运行时环境的主要入口点
// 它监听配置、实例化Handler、创建分发机制（dispatch machinery）、处理请求
	newRuntime func(s store.Store, templates map[string]*template.Info, adapters map[string]*adapter.Info,
		defaultConfigNamespace string, executorPool *pool.GoroutinePool,
		handlerPool *pool.GoroutinePool, enableTracing bool) *runtime.Runtime
// 配置追踪系统，通常在启动时调用一次，此调用返回后，追踪系统可以接受数据
	configTracing func(serviceName string, options *tracing.Options) (io.Closer, error)
// 暴露Mixer自我监控信息的HTTP服务
	startMonitor  func(port uint16, enableProfiling bool, lf listenFunc) (*monitor, error)
// 监听本地端口并返回一个监听器
	listen        listenFunc
// 配置Istio的日志子系统
	configLog     func(options *log.Options) error
// 让Runtime开始监听配置变更，每当配置变更，Runtime处理新配置并创建Dispatcher
	runtimeListen func(runtime *runtime.Runtime) error
}</pre>
<div class="blog_h2"><span class="graybg">newServer</span></div>
<p>此方法创建一个新的Mixs服务器，服务器由下面的结构表示：</p>
<pre class="crayon-plain-tag">type Server struct {
// 关闭通道
	shutdown  chan error
// 服务API请求的gRPC服务器
	server    *grpc.Server
// API线程池
	gp        *pool.GoroutinePool
// 适配器线程池
	adapterGP *pool.GoroutinePool
// API网络监听器
	listener  net.Listener
// 监控服务器，此结构包含两个字段，一个是http.Server，一个是关闭通道
	monitor   *monitor
// 用于关闭追踪子系统
	tracer    io.Closer
// 可伸缩的策略检查缓存
	checkCache *checkcache.Cache
// 将入站API调用分发给配置好的适配器
	dispatcher dispatcher.Dispatcher

	livenessProbe  probe.Controller
	readinessProbe probe.Controller
// 管理探针控制器所需要的可用性状态，内嵌
	*probe.Probe
}</pre>
<p>该方法的逻辑如下：</p>
<pre class="crayon-plain-tag">func newServer(a *Args, p *patchTable) (*Server, error) {
// 校验Mixs启动参数
	if err := a.validate(); err != nil {
		return nil, err
	}
// 配置日志子系统
	if err := p.configLog(a.LoggingOptions); err != nil {
		return nil, err
	}

	apiPoolSize := a.APIWorkerPoolSize
	adapterPoolSize := a.AdapterWorkerPoolSize

	s := &amp;Server{}

// 创建线程池
// API 线程池
	s.gp = pool.NewGoroutinePool(apiPoolSize, a.SingleThreaded)
	s.gp.AddWorkers(apiPoolSize)

// 适配器线程池
	s.adapterGP = pool.NewGoroutinePool(adapterPoolSize, a.SingleThreaded)
	s.adapterGP.AddWorkers(adapterPoolSize)

	tmplRepo := template.NewRepository(a.Templates)
// 从适配器名称到adapter.Info的映射
	adapterMap := config.AdapterInfoMap(a.Adapters, tmplRepo.SupportsTemplate)

// 状态探针
	s.Probe = probe.NewProbe()

// gRPC选项
	var grpcOptions []grpc.ServerOption
	grpcOptions = append(grpcOptions, grpc.MaxConcurrentStreams(uint32(a.MaxConcurrentStreams)))
	grpcOptions = append(grpcOptions, grpc.MaxMsgSize(int(a.MaxMessageSize)))
// 一元（请求/应答模式）gRPC请求的服务器端拦截器
	var interceptors []grpc.UnaryServerInterceptor
	var err error

// 如果启用了追踪（tracing.option提供了ZipkinURL、JaegerURL或LogTraceSpans=true）
	if a.TracingOptions.TracingEnabled() {
		s.tracer, err = p.configTracing("istio-mixer", a.TracingOptions)
		if err != nil {
			_ = s.Close()
			return nil, fmt.Errorf("unable to setup tracing")
		}
// 则添加基于OpenTracing的追踪拦截器
		interceptors = append(interceptors, otgrpc.OpenTracingServerInterceptor(ot.GlobalTracer()))
	}
// OpenTracing、Prometheus监控拦截器，都来自项目https://github.com/grpc-ecosystem
// 将Prometheus拦截器添加到末尾
	interceptors = append(interceptors, grpc_prometheus.UnaryServerInterceptor)
// 启用Prometheus时间直方图记录，RPC调用的耗时会被记录。Prometheus持有、查询Histogram指标的成本比较高
// 生成的指标都是面向gRPC协议的、通用的，不牵涉Istio的逻辑。指标名以grpc_开头
	grpc_prometheus.EnableHandlingTimeHistogram()
// 将所有拦截器串连为单个拦截器，并添加到gRPC选项
	grpcOptions = append(grpcOptions, grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(interceptors...)))

	network := "tcp"
	address := fmt.Sprintf(":%d", a.APIPort)
	if a.APIAddress != "" {
		idx := strings.Index(a.APIAddress, "://")
		if idx &lt; 0 {
			address = a.APIAddress
		} else {
			network = a.APIAddress[:idx]
			address = a.APIAddress[idx+3:]
		}
	}

	if network == "unix" {
// 如果监听UDS，则移除先前的文件
		if err = os.Remove(address); err != nil &amp;&amp; !os.IsNotExist(err) {
			// 除了文件未找到以外的错误，都不允许
			return nil, fmt.Errorf("unable to remove unix://%s: %v", address, err)
		}
	}
// 调用net.Listen监听
	if s.listener, err = p.listen(network, address); err != nil {
		_ = s.Close()
		return nil, fmt.Errorf("unable to listen: %v", err)
	}
// ConfigStore用于测试目的，通常都会使用ConfigStoreURL（例如k8s:///home/alex/.kube/config）
	st := a.ConfigStore
	if st != nil &amp;&amp; a.ConfigStoreURL != "" {
		_ = s.Close()
		return nil, fmt.Errorf("invalid arguments: both ConfigStore and ConfigStoreURL are specified")
	}

	if st == nil {
		configStoreURL := a.ConfigStoreURL
		if configStoreURL == "" {
			configStoreURL = "k8s://"
		}
// Registry存储URL scheme与后端实现之间的对应关系

		reg := store.NewRegistry(config.StoreInventory()...)
		groupVersion := &amp;schema.GroupVersion{Group: crd.ConfigAPIGroup, Version: crd.ConfigAPIVersion}
// 创建一个Store实例，它持有Backend，Backend代表一个无类型的Mixer存储后端 —— 例如K8S
// 默认情况下，configStoreURL的Scheme为k8s，Istio会调用config/crd.NewStore
// 传入configStoreURL、GroupVersion、criticalKinds 来创建Backend
		if st, err = reg.NewStore(configStoreURL, groupVersion, rc.CriticalKinds()); err != nil {
			_ = s.Close()
			return nil, fmt.Errorf("unable to connect to the configuration server: %v", err)
		}
	}

	var rt *runtime.Runtime
// 所有模板，目标决定了各分类的适配器（例如所有metric类适配器）在运行时需要处理的数据类型
	templateMap := make(map[string]*template.Info, len(a.Templates))
	for k, v := range a.Templates {
		t := v
		templateMap[k] = &amp;t
	}
// 创建运行时，传入存储、模板、适配器、线程池、是否启用追踪等信息
	rt = p.newRuntime(st, templateMap, adapterMap, a.ConfigDefaultNamespace,
		s.gp, s.adapterGP, a.TracingOptions.TracingEnabled())

// 监听配置存储的变更，初始化配置
	if err = p.runtimeListen(rt); err != nil {
		_ = s.Close()
		return nil, fmt.Errorf("unable to listen: %v", err)
	}

// 等待配置存储同步完成
	log.Info("Awaiting for config store sync...")
	if err := st.WaitForSynced(30 * time.Second); err != nil {
		return nil, err
	}

// 设置分发器，分发器负责将API请求分发给配置好的适配器处理
	s.dispatcher = rt.Dispatcher()

// 如果启用了策略检查缓存，则创建LRU缓存对象
	if a.NumCheckCacheEntries &gt; 0 {
		s.checkCache = checkcache.New(a.NumCheckCacheEntries)
	}

// 此全局变量决定是否利用包golang.org/x/net/trace进行gRPC调用追踪
	grpc.EnableTracing = a.EnableGRPCTracing

// 节流阀，限制调用频度
	throttler := loadshedding.NewThrottler(a.LoadSheddingOptions)
// Evaluator方法根据名称返回配置好的LoadEvaluator
// LoadEvaluator能够评估请求是否超过阈值
	if eval := throttler.Evaluator(loadshedding.GRPCLatencyEvaluatorName); eval != nil {
		grpcOptions = append(grpcOptions, grpc.StatsHandler(eval.(*loadshedding.GRPCLatencyEvaluator)))
	}

// 创建gRPC服务器
	s.server = grpc.NewServer(grpcOptions...)
// 注册服务到gRPC服务器
// 注册时需要提供grpc.ServiceDesc，其中包含服务名、方法集合（方法名到处理函数的映射
// api.NewGRPCServer返回 mixerpb.MixerServer 接口，它仅仅包含Check / Report两个方法
	mixerpb.RegisterMixerServer(s.server, api.NewGRPCServer(s.dispatcher, s.gp, s.checkCache, throttler))

// 探针
	if a.LivenessProbeOptions.IsValid() {
		s.livenessProbe = probe.NewFileController(a.LivenessProbeOptions)
		s.RegisterProbe(s.livenessProbe, "server")
		s.livenessProbe.Start()
	}

	if a.ReadinessProbeOptions.IsValid() {
		s.readinessProbe = probe.NewFileController(a.ReadinessProbeOptions)
		rt.RegisterProbe(s.readinessProbe, "dispatcher")
		st.RegisterProbe(s.readinessProbe, "store")
		s.readinessProbe.Start()
	}

// 启动监控服务
	if s.monitor, err = p.startMonitor(a.MonitoringPort, a.EnableProfiling, p.listen); err != nil {
		_ = s.Close()
		return nil, fmt.Errorf("unable to setup monitoring: %v", err)
	}
// 启动ControlZ监听器
	go ctrlz.Run(a.IntrospectionOptions, nil)

	return s, nil
}</pre>
<div class="blog_h2"><span class="graybg">p.newRuntime</span></div>
<p>patchTable的newRuntime函数会调用runtime.New，创建一个新的Mixer运行时 —— Mixer运行时环境的主要入口点，负责监听配置、实例化Handler、创建分发机制（dispatch machinery）、处理请求：</p>
<pre class="crayon-plain-tag">func New(
	s store.Store,
	templates map[string]*template.Info,
	adapters map[string]*adapter.Info,
	defaultConfigNamespace string,
	executorPool *pool.GoroutinePool,
	handlerPool *pool.GoroutinePool,
	enableTracing bool) *Runtime {

	// Ephemeral表示一个短暂的配置状态，它可以被入站配置变更事件所更新
	// Ephemeral本身包含的数据没有价值，你必须调用它的BuildSnapshot方法来创建稳定的、完全解析的配置的快照
	e := config.NewEphemeral(templates, adapters)
	rt := &amp;Runtime{
// 默认配置命名空间
		defaultConfigNamespace: defaultConfigNamespace,
// 短暂配置状态
		ephemeral:              e,
// 配置快照
		snapshot:               config.Empty(),
// 适配器处理器列表
		handlers:               handler.Empty(),
// API请求分发器，需要协程池
		dispatcher:             dispatcher.New(executorPool, enableTracing),
// 适配器处理器的协程池
		handlerPool:            handlerPool,
		Probe:                  probe.NewProbe(),
		store:                  s,
	}

// 从ephemeral构建出新c.snapshot、新c.handlers、新路由表（用于解析入站请求并将其路由给适当的处理器）
// 然后替换路由表，最后清理上一次配置对应的处理器
	rt.processNewConfig()
// 设置探针结果为：尚未监听存储
	rt.Probe.SetAvailable(errNotListening)

	return rt
}</pre>
<div class="blog_h2"><span class="graybg">p.runtimeListen</span></div>
<p>创建Runtime之后，p.runtimeListen被调用。此函数会调用Runtime.StartListening方法来监听配置的变更，同样会立即触发processNewConfig调用。之后，processNewConfig调用会通过store.WatchChanges的回调反复发生。</p>
<pre class="crayon-plain-tag">func (c *Runtime) StartListening() error {
// Runtime的状态锁
	c.stateLock.Lock()
	defer c.stateLock.Unlock()

	if c.shutdown != nil {
		return errors.New("already listening")
	}

// 生成adapter、template等对象类型到它的proto消息的映射（合并到一个映射中）
// adapter.Info.DefaultConfig、template.Info.CtrCfg，以及
// &amp;configpb.Rule{}、&amp;configpb.AttributeManifest{}、&amp;v1beta1.Info{} ...
// 都实现了proto.Message接口
	kinds := config.KindMap(c.snapshot.Adapters, c.snapshot.Templates)
// 开始监控存储，返回当前资源集（key到spec的映射）、监控用的通道
	data, watchChan, err := store.StartWatch(c.store, kinds)
	if err != nil {
		return err
	}

// 设置并覆盖相同的临时状态，其实就是把ephemeral.entries = data
	c.ephemeral.SetState(data)
// 处理新配置
	c.processNewConfig()
// 初始化运行时的关闭通道
	c.shutdown = make(chan struct{})
// 增加一个计数
	c.waitQuiesceListening.Add(1)
	go func() {
// 只有shutdown通道关闭，此监控配置存储变化的循环才会退出
// 当有新的配置变更被发现后，调用onConfigChange，此方法会导致processNewConfig
		store.WatchChanges(watchChan, c.shutdown, watchFlushDuration, c.onConfigChange)
// shutdown通道关闭后，
		c.waitQuiesceListening.Done()
	}()
// 重置可用性状态，此等待组不再阻塞，StopListening方法可以顺利返回
	c.Probe.SetAvailable(nil)

	return nil
}</pre>
<div class="blog_h2"><span class="graybg">onConfigChange</span></div>
<p>当配置存储有变化后，Runtime的该方法会被调用，它的逻辑很简单：</p>
<pre class="crayon-plain-tag">func (c *Runtime) onConfigChange(events []*store.Event) {
// 更新或者擅长ephemeral.entries中的条目
	c.ephemeral.ApplyEvent(events)
// 对最新的配置进行处理
	c.processNewConfig()default
}</pre>
<div class="blog_h2"><span class="graybg">processNewConfig </span></div>
<p>Runtime的processNewConfig方法负责处理从配置存储（K8S）中<span style="background-color: #c0c0c0;">拉取的最新CR，然后创建配置快照、创建处理器表、路由表，并改变Dispatcher的路由</span>：</p>
<pre class="crayon-plain-tag">func (c *Runtime) processNewConfig() {
// 构建一个稳定的、完全解析的配置的快照
	newSnapshot, _ := c.ephemeral.BuildSnapshot()
// 当前运行时使用的处理器
	oldHandlers := c.handlers
// 创建新的处理器表
	newHandlers := handler.NewTable(oldHandlers, newSnapshot, c.handlerPool)
// 返回ExpressionBuilder，用于创建一系列预编译表达式
	builder := compiled.NewBuilder(newSnapshot.Attributes)
// 构建并返回路由表，路由表决定了什么条件下调用什么适配器
	newRoutes := routing.BuildTable(
		newHandlers, newSnapshot, builder, c.defaultConfigNamespace, log.DebugEnabled())

// 改变分发器的路由，分发器负责基于路由表来调用适配器
	oldContext := c.dispatcher.ChangeRoute(newRoutes)

// 修改实例变量
	c.handlers = newHandlers
	c.snapshot = newSnapshot

	log.Debugf("New routes in effect:\n%s", newRoutes)

// 关闭旧的处理器，注意处理器实现了io.Closer接口，这个接口由Istio自己负责，和适配器开发无关
	cleanupHandlers(oldContext, oldHandlers, newHandlers, maxCleanupDuration)
}</pre>
<div class="blog_h2"><span class="graybg">e.BuildSnapshot</span></div>
<p>该方法生成一个完全解析的（没有任何外部依赖）的配置快照。<span style="background-color: #c0c0c0;">快照主要包含静态、动态模板/适配器信息、以及规则信息</span>：</p>
<pre class="crayon-plain-tag">func (e *Ephemeral) BuildSnapshot() (*Snapshot, error) {
	errs := &amp;multierror.Error{}
// 下一个快照的ID
	id := e.nextID
	e.nextID++

	log.Debugf("Building new config.Snapshot: id='%d'", id)

// 一组和istio本身状态监控有关的Prometheus计数器
	counters := newCounters(id)

	e.lock.RLock()

// 处理属性清单，获得属性列表。清单来源有三个地方：
// 1、配置存储中attributemanifest类型的CR。第一次调用该方法时，尚未加载这些CR
// 2、自动生成的template.Info.AttributeManifests
// 注意清单中每个属性，都具有全网格唯一的名称
	attributes := e.processAttributeManifests(counters, errs)

// 处理静态适配器的处理器配置 —— 各种适配器的CR/实例，获得处理器（HandlerStatic）列表
// 对于从配置存储加载的资源，如果在自动生成的adapter.Info中找到对应条目，则认为是合法的处理器
// 对于每个处理器，会创建HandlerStatic结构，此结构表示基于Compiled-in的适配器的处理器
	shandlers := e.processStaticAdapterHandlerConfigs(counters, errs)

// 返回属性描述符查找器（AttributeDescriptorFinder）
	af := ast.NewFinder(attributes)
// 处理静态模板的实例配置 —— 各种模板的CR，获得实例（InstanceStatic）列表
// 对于从配置存储加载的资源，如果在自动生成的template.Info中找到对应条目，则认为是合法的实例
// 对于每个实例，会创建InstanceStatic结构，此结构表示基于Compiled-in的模板的Instance
	instances := e.processInstanceConfigs(af, counters, errs)

// 开始处理动态资源，所谓动态资源，是指没有特定CRD的模板（也就没有对应CR的实例）
// 以及没有特定CRD的适配器（也就没有对应CR的处理器）
// 动态模板注册为template类型的CR
	dTemplates := e.processDynamicTemplateConfigs(counters, errs)
// 动态适配器注册为adapter类型的CR
	dAdapters := e.processDynamicAdapterConfigs(dTemplates, counters, errs)
// 动态处理器注册为handler类型的CR，它必须引用某个adapter的名称
	dhandlers := e.processDynamicHandlerConfigs(dAdapters, counters, errs)
// 动态处理器注册为instance类型的CR，它必须引用某个template的名称
	dInstances := e.processDynamicInstanceConfigs(dTemplates, af, counters, errs)

// 处理规则，规则可以引用上述的静态和动态资源
	rules := e.processRuleConfigs(shandlers, instances, dhandlers, dInstances, af, counters, errs)

// 构建配置快照
	s := &amp;Snapshot{
		ID:                id,
		Templates:         e.templates,
		Adapters:          e.adapters,
		TemplateMetadatas: dTemplates,
		AdapterMetadatas:  dAdapters,
		Attributes:        ast.NewFinder(attributes),
		HandlersStatic:    shandlers,
		InstancesStatic:   instances,
		Rules:             rules,

		HandlersDynamic:  dhandlers,
		InstancesDynamic: dInstances,

		Counters: counters,
	}
	e.lock.RUnlock()

	return s, errs.ErrorOrNil()
}</pre>
<div class="blog_h1"><span class="graybg">适配器初始化过程</span></div>
<p>适配器的初始化过程，是Mixer服务器初始化的一部分。在Mixer服务器启动过程中有如下逻辑：</p>
<pre class="crayon-plain-tag">adapterMap := config.AdapterInfoMap(a.Adapters, tmplRepo.SupportsTemplate)</pre>
<p> 该方法会生成得到所有适配器的adaptor.Info对象：</p>
<pre class="crayon-plain-tag">type Info struct {
	// 适配器的正式名称，必须是RFC 1035兼容的DNS标签
	// 此名称会用在Istio配置中，因此应当简短而具有描述性
	Name string
	// 实现此适配器的包，例如
	// istio.io/istio/mixer/adapter/denier
	Impl string
	// 人类可读的适配器的描述信息
	Description string
	// 该函数指针能够创建一个新的HandlerBuilder，HandlerBuilder能够创建出此适配器的Handler
	NewBuilder NewBuilderFn
	// 此适配器声明支持的模板
	SupportedTemplates []string
	// 传递给HandlerBuilder.Build的适配器的默认参数
	DefaultConfig proto.Message
}</pre>
<p>入参a.Adapters来自supportedAdapters()，此函数是自动生成的。a.Adapters的每个元素的类型是<span style="background-color: #c0c0c0;">adapter.InfoFn。调用此函数即得到对应的adaptor.Info对象</span>：</p>
<pre class="crayon-plain-tag">type InfoFn func() Info</pre>
<p>config.AdapterInfoMap的主要逻辑就是调用各种适配器的adapter.InfoFn方法，并且对adaptor.Info进行各种校验。例如检查它的NewBuilder、NewBuilder字段是否为非空，检查它是否和声明支持的模板兼容。</p>
<p>适配器如果需要初始化，那么<span style="background-color: #c0c0c0;">初始化逻辑就发生在InfoFn中</span>。</p>
<div class="blog_h2"><span class="graybg">Prometheus</span></div>
<p>本节以Prometheus适配器为例，了解适配器的初始化过程。</p>
<div class="blog_h3"><span class="graybg">初始化Info</span></div>
<pre class="crayon-plain-tag">const (
	metricsPath = "/metrics"
// Istio会暴露三个和Prometheus Exporter端口：
// istio-mixer.istio-system:42422，所有由Mixer的Prometheus适配器生成的网格指标
// istio-mixer.istio-system:9093，用于监控Mixer自身的指标
// istio-mixer.istio-system:9102，Envoy生成的原始统计信息，从Statsd转换为Prometheus格式
	defaultAddr = ":42422"
)

func GetInfo() adapter.Info {
	ii, _ := GetInfoWithAddr(defaultAddr)
	return ii
}</pre>
<p>GetInfoWithAddr方法的实现如下：</p>
<pre class="crayon-plain-tag">func GetInfoWithAddr(addr string) (adapter.Info, Server) {
// HandlerBuilder单例
	singletonBuilder := &amp;builder{
// HTTP服务器，这里不会启动监听
		srv: newServer(addr),
	}
// 创建注册表singletonBuilder.registry = prometheus.NewPedanticRegistry()
// 情况指标信息 singletonBuilder.metrics = make(map[string]*cinfo)
	singletonBuilder.clearState()
// 返回adaptor.Info对象
	return adapter.Info{
		Name:        "prometheus",
		Impl:        "istio.io/istio/mixer/adapter/prometheus",
		Description: "Publishes prometheus metrics",
		SupportedTemplates: []string{
			metric.TemplateName,
		},
		NewBuilder:    func() adapter.HandlerBuilder { return singletonBuilder },
		DefaultConfig: &amp;config.Params{},
	}, singletonBuilder.srv
}</pre>
<div class="blog_h3"><span class="graybg">初始化Handler </span></div>
<p>每当配置变更后，适配器的Handler会被初始化。Runtime.processNewConfig会调用：</p>
<pre class="crayon-plain-tag">newHandlers := handler.NewTable(oldHandlers, newSnapshot, c.handlerPool)</pre>
<p>创建handler.Table，此表包含了所有实例化的、配置好的适配器的处理器的信息：</p>
<pre class="crayon-plain-tag">type Table struct {
// 表格条目
	entries map[string]Entry

	counters tableCounters
}

// 单个处理器
type Entry struct {
	// 处理器的名称
	Name string

	// 处理器对象
	Handler adapter.Handler

	// 适配器名称
	AdapterName string

	// 创建此处理器使用的适配器配置（参数）的签名信息
	Signature signature

	// 传递给处理器的adapter.Env
	env env
}</pre>
<p>每个适配器可以消费多个实例，对于适配器和实例的每个组合，handler.NewTable方法会为其创建Handler：</p>
<pre class="crayon-plain-tag">// 适配器实例 - 模板实例的映射
// map[*HandlerStatic][]*InstanceStatic
instancesByHandler := config.GetInstancesGroupedByHandlers(snapshot)
// map[*HandlerDynamic][]*InstanceDynamic
instancesByHandlerDynamic := config.GetInstancesGroupedByHandlersDynamic(snapshot)

// 表
t := &amp;Table{
	entries:  make(map[string]Entry, len(instancesByHandler)+len(instancesByHandlerDynamic)),
	counters: newTableCounters(snapshot.ID),
}

// 对于每个静态处理器 - 实例组合
for handler, instances := range instancesByHandler {
        // 为其创建条目，并加入到表中
	createEntry(old, t, handler, instances, snapshot.ID,
// 这个回调用于用于创建处理器
		func(handler hndlr, instances interface{}) (h adapter.Handler, e env, err error) {
// 环境信息
			e = NewEnv(snapshot.ID, handler.GetName(), gp).(env)
// 创建出处理器
			h, err = config.BuildHandler(handler.(*config.HandlerStatic), instances.([]*config.InstanceStatic), e, snapshot.Templates)
			return h, e, err
		})
}

// 对于每个动态处理器 - 实例组合
for handler, instances := range instancesByHandlerDynamic {
	createEntry(old, t, handler, instances, snapshot.ID, ...
}</pre>
<p>config.BuildHandler经过几层转发，最终会调用Prometheus适配器的方法：</p>
<pre class="crayon-plain-tag">func (b *builder) Build(ctx context.Context, env adapter.Env) (adapter.Handler, error) {

	cfg := b.cfg
	var metricErr *multierror.Error

// 用于收集指标配置
	newMetrics := make([]*config.Params_MetricInfo, 0, len(cfg.Metrics))

	// 检查指标是否被重新定义，也就是对应的CR是否被修改
	// 如果是，则清空指标注册表、指标映射。重定义会导致Prometheus客户端Panic
	// 添加、移除则没有问题
	var cl *cinfo
// 遍历新配置的指标列表
	for _, m := range cfg.Metrics {
		// 当前指标表中没有匹配项，加入
		if cl = b.metrics[m.InstanceName]; cl == nil {
			newMetrics = append(newMetrics, m)
			continue
		}

		// 散列值没有变，和之前的指标配置一样
		if cl.sha == computeSha(m, env.Logger()) {
			continue
		}

		// 散列值不匹配，发生了重定义。适配器需要重现加载
		env.Logger().Warningf("Metric %s redefined. Reloading adapter.", m.Name)
		// 重建注册表、清空指标信息
		b.clearState()
		// 将所有指标作为新配置看待
		newMetrics = cfg.Metrics
		break
	}

	env.Logger().Debugf("%d new metrics defined", len(newMetrics))

// 遍历处理所有新指标
	var err error
	for _, m := range newMetrics {
		ns := defaultNS
		if len(m.Namespace) &gt; 0 {
			ns = safeName(m.Namespace)
		}
// 指标全名，即CR的名称
		mname := m.InstanceName
		if len(m.Name) != 0 {
// 转换为短名
			mname = m.Name
		}
// 构建出指标信息cinfo
		ci := &amp;cinfo{kind: m.Kind, sha: computeSha(m, env.Logger())}
		ci.sortedLabels = make([]string, len(m.LabelNames))
		copy(ci.sortedLabels, m.LabelNames)
		sort.Strings(ci.sortedLabels)

// 根据指标类型的不同，分别处理。逻辑都是注册指标到注册表
		switch m.Kind {
		case config.GAUGE:
			ci.c, err = registerOrGet(b.registry, newGaugeVec(ns, mname, m.Description, m.LabelNames))
			b.metrics[m.InstanceName] = ci
		case config.COUNTER:
			ci.c, err = registerOrGet(b.registry, newCounterVec(ns, mname, m.Description, m.LabelNames))
			b.metrics[m.InstanceName] = ci
		case config.DISTRIBUTION:
			ci.c, err = registerOrGet(b.registry, newHistogramVec(ns, mname, m.Description, m.LabelNames, m.Buckets))
			b.metrics[m.InstanceName] = ci
		default:
			metricErr = multierror.Append(metricErr, fmt.Errorf("unknown metric kind (%d); could not register metric %v", m.Kind, m))
		}
	}

// 启动Exporter的HTTP服务器，如果已经启动则不管
	if err := b.srv.Start(env, promhttp.HandlerFor(b.registry, promhttp.HandlerOpts{})); err != nil {
		return nil, err
	}

// 如果配置了指标过期功能，则定期删除老旧指标
	var expiryCache cache.ExpiringCache
	if cfg.MetricsExpirationPolicy != nil {
		checkDuration := cfg.MetricsExpirationPolicy.ExpiryCheckIntervalDuration
		if checkDuration == 0 {
			checkDuration = cfg.MetricsExpirationPolicy.MetricsExpiryDuration / 2
		}
		expiryCache = cache.NewTTLWithCallback(
			cfg.MetricsExpirationPolicy.MetricsExpiryDuration,
			checkDuration,
			deleteOldMetrics)
	}

	return &amp;handler{b.srv, b.metrics, expiryCache}, metricErr.ErrorOrNil()
}</pre>
<div class="blog_h3"><span class="graybg">暴露指标</span></div>
<p>b.srv.Start会启动作为Exporter的HTTP服务器： </p>
<pre class="crayon-plain-tag">func (s *serverInst) Start(env adapter.Env, metricsHandler http.Handler) (err error) {
// 加锁保护
	s.lock.Lock()
	defer s.lock.Unlock()

	// 如果服务器已经启动了，则委托
	// just switch the delegate handler.
	if s.srv != nil {
		s.refCnt++
		s.handler.setDelegate(metricsHandler)
		return nil
	}
// 否则，创建监听
	listener, err := net.Listen("tcp", s.addr)
	s.port = listener.Addr().(*net.TCPAddr).Port
// 配置ServerMux
	srvMux := http.NewServeMux()
	s.handler = &amp;metaHandler{delegate: metricsHandler}
	srvMux.Handle(metricsPath, s.handler)
	srv := &amp;http.Server{Addr: s.addr, Handler: srvMux}
// 在后台运行
	env.ScheduleDaemon(func() {
// 开始监听
		env.Logger().Infof("serving prometheus metrics on %d", s.port)
		if err := srv.Serve(listener.(*net.TCPListener)); err != nil {
			if err == http.ErrServerClosed {
				env.Logger().Infof("HTTP server stopped")
			} else {
				_ = env.Logger().Errorf("prometheus HTTP server error: %v", err) 
			}
		}
	})
	s.srv = srv
	s.refCnt++

	return nil
} </pre>
<p>使用Istio官方Chart安装时，其内置的Prometheus服务器会自动采集该HTTP服务器暴露的指标。</p>
<div class="blog_h1"><span class="graybg">Mixs处理请求过程 </span></div>
<p>在运行期间，Envoy代理会向Mixer服务发起CHECK/REPORT/QUOTA等调用。Mixer会将这些请求转发给匹配的适配器进行处理。</p>
<p>本节以Prometheus适配器为例，说明REPORT请求的处理过程。</p>
<div class="blog_h2"><span class="graybg">相关配置</span></div>
<p>以官方Chart部署Istio时，会创建如下Rule：</p>
<pre class="crayon-plain-tag">apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: promhttp
spec:
  actions:
  - handler: handler.prometheus
    instances:
    - requestcount.metric
    - requestduration.metric
    - requestsize.metric
    - responsesize.metric
  match: context.protocol == "http" || context.protocol == "grpc"</pre>
<p>这里我们测试requestcount这个指标，和它相关的Handler、Instance配置片断如下：</p>
<pre class="crayon-plain-tag"># Handler
apiVersion: config.istio.io/v1alpha2
kind: prometheus
metadata:
  name: handler
  namespace: istio-system
spec:
  metrics:
  - instance_name: requestcount.metric.istio-system
    kind: COUNTER
    label_names:
    - reporter
    - source_app
    - source_principal
    - source_workload
    - source_workload_namespace
    - source_version
    - destination_app
    - destination_principal
    - destination_workload
    - destination_workload_namespace
    - destination_version
    - destination_service
    - destination_service_name
    - destination_service_namespace
    - request_protocol
    - response_code
    - connection_security_policy
    name: requests_total

# Instance
kind: metric
metadata:
  name: requestcount
spec:
  dimensions:
    connection_security_policy: conditional((context.reporter.kind | "inbound") == "outbound", "unknown", conditional(connection.mtls | false, "mutual_tls", "none"))
    destination_app: destination.labels["app"] | "unknown"
    destination_principal: destination.principal | "unknown"
    destination_service: destination.service.host | "unknown"
    destination_service_name: destination.service.name | "unknown"
    destination_service_namespace: destination.service.namespace | "unknown"
    destination_version: destination.labels["version"] | "unknown"
    destination_workload: destination.workload.name | "unknown"
    destination_workload_namespace: destination.workload.namespace | "unknown"
    reporter: conditional((context.reporter.kind | "inbound") == "outbound", "source", "destination")
    request_protocol: api.protocol | context.protocol | "unknown"
    response_code: response.code | 200
    source_app: source.labels["app"] | "unknown"
    source_principal: source.principal | "unknown"
    source_version: source.labels["version"] | "unknown"
    source_workload: source.workload.name | "unknown"
    source_workload_namespace: source.workload.namespace | "unknown"
  monitored_resource_type: '"UNSPECIFIED"'
  value: "1"</pre>
<div class="blog_h2"><span class="graybg">发送请求</span></div>
<p>要触发Mixer服务器端的处理逻辑，不需要运行Envoy代理，调用命令行客户端mixc就可以了。  </p>
<p>为了匹配上面的promhttp规则，我们需要发送一个属性context.protocol的值为http的REPORT请求：</p>
<pre class="crayon-plain-tag">mixc report -m localhost:9091 \
    -t request.time=2019-03-27T11:00:00.000Z,response.time=2019-03-27T11:00:00.900Z  \
    -a context.protocol=http,context.reporter.kind=outbound,source.namespace=default  \
    -a destination.service=kubernetes

# 2019-03-27T03:52:05.237085Z     info    parsed scheme: ""
# 2019-03-27T03:52:05.237179Z     info    scheme "" not registered, fallback to default scheme
# 2019-03-27T03:52:05.237532Z     info    ccResolverWrapper: sending new addresses to cc: [{localhost:9091 0  &lt;nil&gt;}]
# 2019-03-27T03:52:05.237592Z     info    ClientConn switching balancer to "pick_first"
# 2019-03-27T03:52:05.237768Z     info    pickfirstBalancer: HandleSubConnStateChange: 0xc0001940b0, CONNECTING
# 2019-03-27T03:52:05.237788Z     info    blockingPicker: the picked transport is not ready, loop back to repick
# 2019-03-27T03:52:05.241228Z     info    pickfirstBalancer: HandleSubConnStateChange: 0xc0001940b0, READY
# Report RPC returned OK</pre>
<div class="blog_h2"><span class="graybg">拦截请求</span></div>
<div class="blog_h3"><span class="graybg">gRPC接口</span></div>
<p>Mixer处理请求的接口由以下Proto文件定义：</p>
<pre class="crayon-plain-tag">service Mixer {
  // 进行先决条件检查，或者进行配额
  rpc Check(CheckRequest) returns (CheckResponse) {}

  // 遥测报告
  rpc Report(ReportRequest) returns (ReportResponse) {}
}</pre>
<div class="blog_h3"><span class="graybg">Prometheus拦截器</span></div>
<p>通过前面章节的源码分析，我们了解到，在Mixer服务启动时，注册了OpenTracing、Prometheus的gRPC拦截器。因此首先会执行Prometheus拦截器：</p>
<pre class="crayon-plain-tag">// 自动生成的代码
func _Mixer_Report_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
	in := new(ReportRequest)
	if err := dec(in); err != nil {
		return nil, err
	}
// 没有拦截器，直接调用MixerServer实现
	if interceptor == nil {
		return srv.(MixerServer).Report(ctx, in)
	}
	info := &amp;grpc.UnaryServerInfo{
		Server:     srv,
		FullMethod: "/istio.mixer.v1.Mixer/Report",
	}
	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
		return srv.(MixerServer).Report(ctx, req.(*ReportRequest))
	}
// 实际上是有拦截器的，调用拦截器，通过拦截器再调用MixerServer实现
	return interceptor(ctx, in, info, handler)
}</pre>
<p>来自<a href="https://github.com/grpc-ecosystem/go-grpc-prometheus">go-grpc-prometheus</a>项目的Prometheus拦截器，逻辑如下：</p>
<pre class="crayon-plain-tag">func UnaryServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// grpc_server_started_total指标
	monitor := newServerReporter(Unary, info.FullMethod)
// grpc_server_msg_received_total指标
	monitor.ReceivedMessage()
// 调用MixerServer实现
	resp, err := handler(ctx, req)
// grpc_server_handled_total指标
// grpc_server_handling_seconds指标，直方图
	monitor.Handled(grpc.Code(err))
	if err == nil {
// grpc_server_msg_sent_total
		monitor.SentMessage()
	}
	return resp, err
}</pre>
<div class="blog_h2"><span class="graybg">处理请求</span></div>
<div class="blog_h3"><span class="graybg">Report</span></div>
<p>MixerServer接口的实现定义在api.grpcServer结构中。Report方法会逐个处理每条消息，并进行：</p>
<ol>
<li>预处理：调用匹配的属性生成处理器</li>
<li>处理：调用匹配的主处理器</li>
</ol>
<p>注意，单次Mixer请求可以携带多条消息，每条消息都对应Envoy代理处理的一个实际请求。</p>
<p>Report方法的实现如下：</p>
<pre class="crayon-plain-tag">func (s *grpcServer) Report(ctx context.Context, req *mixerpb.ReportRequest) (*mixerpb.ReportResponse, error) {
// 限流逻辑，默认情况下Mixer的限流是关闭的
// req.Attributes的类型是 []v1.CompressedAttributes，每个元素表示报告的一条信息，客户端可以一次报送多条信息
// 但是对于非REPORT请求，每次只能有一条消息
	if s.throttler.Throttle(loadshedding.RequestInfo{PredictedCost: float64(len(req.Attributes))}) {
		return nil, grpc.Errorf(codes.Unavailable, "Server is currently overloaded. Please try again.")
	}

	if len(req.Attributes) == 0 {
		// 没有报告任何东西
		return reportResp, nil
	}

// Words表示消息级别的字典 —— 属性名的数组、字符串属性值
	for i := 0; i &lt; len(req.Attributes); i++ {
		if len(req.Attributes[i].Words) == 0 {
// req.DefaultWords为所有消息的默认字典。可以让请求中多个消息共享字典，进而减少请求大小
			req.Attributes[i].Words = req.DefaultWords
		}
	}

	// bag around the input proto that keeps track of reference attributes
// 创建一个ProtoBag —— 基于属性Proto消息，实现Bag接口（用于访问属性集）
	protoBag := attribute.NewProtoBag(&amp;req.Attributes[0], s.globalDict, s.globalWordList)

// 从对象池中取得一个MutableBag，对象池避免了反复的内存分配，然后将其parent设置为protoBag
// accumBag（请求包requestBag），跟踪除了第一个以外，所有消息相对于第一个的delta
	accumBag := attribute.GetMutableBag(protoBag)

// reportBag（响应包responseBag），持有预处理之后的输出状态，预处理适配器可能会生成一些新属性，这些新属性以delta的形式存储在此
	reportBag := attribute.GetMutableBag(accumBag)

// 基于GlobalTracer，启动并返回操作名称（operationName）为Report的Span，使用从ctx中找到的Span作为ChildOfRef
// 如果找不到作为parent的Span，则创建一个根Span
	reportSpan, reportCtx := opentracing.StartSpanFromContext(ctx, "Report")
// 从对象池中获得reporter，为其提供路由上下文（report.rc）、报告上下文（r.ctx，其中包含了Trace树的信息）
	reporter := s.dispatcher.GetReporter(reportCtx)

	var errors *multierror.Error
/* 开始逐个处理消息 */
	for i := 0; i &lt; len(req.Attributes); i++ {
// 以Report为父Span，依次创建子Span： attribute bag N
		span, newctx := opentracing.StartSpanFromContext(reportCtx, fmt.Sprintf("attribute bag %d", i))

// 第一个属性块（消息）作为protoBag的基础，计算每个子包的delta
		if i &gt; 0 {
			if err := accumBag.UpdateBagFromProto(&amp;req.Attributes[i], s.globalWordList); err != nil {
				err = fmt.Errorf("request could not be processed due to invalid attributes: %v", err)
// 为子Span记录字段，然后结束Span
				span.LogFields(otlog.String("error", err.Error()))
				span.Finish()
				errors = multierror.Append(errors, err)
				break
			}
		}

		lg.Debug("Dispatching Preprocess")
// 预处理，将请求包分发给那些需要提前执行的适配器，例如属性生成适配器
		if err := s.dispatcher.Preprocess(newctx, accumBag, reportBag); err != nil {
			err = fmt.Errorf("preprocessing attributes failed: %v", err)
			span.LogFields(otlog.String("error", err.Error()))
			span.Finish()
			errors = multierror.Append(errors, err)
			continue
		}

// 主处理，分发给主适配器
		lg.Debug("Dispatching to main adapters after running preprocessors")
		lg.Debuga("Attribute Bag: \n", reportBag)
		lg.Debugf("Dispatching Report %d out of %d", i+1, len(req.Attributes))

		if err := reporter.Report(reportBag); err != nil {
			span.LogFields(otlog.String("error", err.Error()))
			span.Finish()
			errors = multierror.Append(errors, err)
			continue
		}

		span.Finish()

		// 清空包内容，准备处理下一个请求包使用
		reportBag.Reset()
	}
/* 结束逐个处理消息 */

// 重置，并放回对象池
	reportBag.Done()
	accumBag.Done()
	protoBag.Done()

// 刷出，调用reporter.impl.getSession.dispatchBufferedReports()，将之前缓冲的dispatchState全部分发出去
// 然后将会话放回对象池
	if err := reporter.Flush(); err != nil {
		errors = multierror.Append(errors, err)
	}
// 将Reporter对象也放回池中
	reporter.Done()

// 结束Span
	if errors != nil {
		reportSpan.LogFields(otlog.String("error", errors.Error()))
	}
	reportSpan.Finish()

	if errors != nil {
		lg.Errora("Report failed:", errors.Error())
		return nil, grpc.Errorf(codes.Unknown, errors.Error())
	}
// 返回响应
	return reportResp, nil
}</pre>
<div class="blog_h3"><span class="graybg">Preprocess </span></div>
<p>Dispatcher.Preprocess方法负责请求预处理，将请求包分发给那些需要提前执行的适配器，并收集它们产生的属性：</p>
<pre class="crayon-plain-tag">func (d *Impl) Preprocess(ctx context.Context, bag attribute.Bag, responseBag *attribute.MutableBag) error {
// 返回一个session，此结构表示对Dispatcher接口（的实现Impl）的一个调用会话
// 其中包含了处理调用所需的所有可变状态
// getSession从对象池获取一个session对象，然后设置它的
// s.impl，Dispatcher对象
// s.rc，路由上下文对象
// s.ctx 包含Span信息的上下文
// s.variety 需要调用的适配器的种类
// s.bag 请求包
	s := d.getSession(ctx, tpb.TEMPLATE_VARIETY_ATTRIBUTE_GENERATOR, bag)
// s.responseBag 响应包
	s.responseBag = responseBag
// 执行分发
	err := s.dispatch()
	if err == nil {
		err = s.err
	}
// 放回对象池
	d.putSession(s)
	return err
}</pre>
<div class="blog_h3"><span class="graybg">dispatch </span></div>
<p>session.dispatch方法真正负责请求包的分发工作：</p>
<pre class="crayon-plain-tag">func (s *session) dispatch() error {
// 根据报告者类型（从context.reporter.kind获取），默认inbound推断命名空间
// inbound 则命名空间为destination.namespace
// outbound 则命名空间为source.namespace
	namespace, err := getIdentityNamespace(s.bag)
	if err != nil {
// 无法获取命名空间，出错
// 更新直方图（Observe一个值）：
// mixer_dispatcher_destinations_per_request
// mixer_dispatcher_instances_per_request
		updateRequestCounters(0, 0)
		log.Warnf("unable to determine identity namespace: '%v', operation='%d'", err, s.variety)
		return err
	}
// 从路由表获得s.variety类型的、namespace命名空间的目的地列表
// 注意：如果当前命名空间没有匹配的目的地，则使用默认配置存储命名空间（istio-system）中定义的目的地
	destinations := s.rc.Routes.GetDestinations(s.variety, namespace)

// 要访问的目标服务 
	destinationService := ""
	v, ok := s.bag.Get("destination.service")
	if ok {
		destinationService = v.(string)
	}
// 创建一个新的Context，携带键值对，以前面的子Span上下文为父，0=adapter.RequestData为键值对
// RequestData定义了关于请求的信息，例如它的目的服务
	ctx := adapter.NewContextWithRequestData(s.ctx, &amp;adapter.RequestData{
		DestinationService: adapter.Service{
			FullName: destinationService,
		},
	})

// 确保能够将请求并行的分发给所有处理器，将s.completed设置为足够大的chan *dispatchState
// 每个chan *dispatchState收集单个目的地的处理结果
	s.ensureParallelism(destinations.Count())

	foundQuota := false
// 构建出的实例数量
	ninputs := 0
// 匹配的目的地数量
	ndestinations := 0
	for _, destination := range destinations.Entries() {
// dispatchState持有和单个目的地相关的输入/输出状态
		var state *dispatchState

// 对于REPORT处理器
		if s.variety == tpb.TEMPLATE_VARIETY_REPORT {
// 生成并缓存分发状态到s.reportStates
			state = s.reportStates[destination]
			if state == nil {
// 从对象池中获取一个dispatchState并对其赋值，对象池在Mixer中大量使用，减少了内存分配
				state = s.impl.getDispatchState(ctx, destination)
				s.reportStates[destination] = state
			}
		}

		for _, group := range destination.InstanceGroups {
// 判断请求包是否和每个实例组匹配
			groupMatched := group.Matches(s.bag)

			if groupMatched {
				ndestinations++
			}

// 遍历每个组中的每个实例，调用其构建器。构建器的逻辑取决于你配置的各种模板实例，例如metric的CR
			for j, input := range group.Builders {
				if s.variety == tpb.TEMPLATE_VARIETY_QUOTA {
// 对于配额适配器，必须要求实例构建器名称和实例名一致
// CRD名称即模板信息名TemplateInfo.Name，例如        logentries
// 实例名，即CR名，例如 kubectl -n istio-system get logentries.config.istio.io
// 得到的accesslog、tcpaccesslog                  
					if !strings.EqualFold(input.InstanceShortName, s.quotaArgs.Quota) {
						continue
					}
					if !groupMatched {
						// 这是一个条件性的配额，并且当前不匹配条件，直接返回请求的额度
						s.quotaResult.Amount = s.quotaArgs.Amount
						s.quotaResult.ValidDuration = defaultValidDuration
					}
					foundQuota = true
				}

				if !groupMatched {
					continue
				}

				var instance interface{}
// 从请求包构建出实例，Builder方法是自动生成的
				if instance, err = input.Builder(s.bag); err != nil {
					log.Errorf("error creating instance: destination='%v', error='%v'", destination.FriendlyName, err)
					s.err = multierror.Append(s.err, err)
					continue
				}
				ninputs++
// 对于REPORT模板，在执行分发前，尽可能的将实例累积到分发状态的instances列表中
				if s.variety == tpb.TEMPLATE_VARIETY_REPORT {
					state.instances = append(state.instances, instance)
					continue
				}

// 对于其它模板类型，直接分发给处理器
				state = s.impl.getDispatchState(ctx, destination)
				state.instances = append(state.instances, instance)
				if s.variety == tpb.TEMPLATE_VARIETY_ATTRIBUTE_GENERATOR {
// 属性生成处理器需要处理Mapper —— 将处理器输出映射入主属性集的函数
					state.mapper = group.Mappers[j]
					state.inputBag = s.bag
				}

// 配额模板相关参数
				state.quotaArgs.BestEffort = s.quotaArgs.BestEffort
				state.quotaArgs.DeduplicationID = s.quotaArgs.DeduplicationID
				state.quotaArgs.QuotaAmount = s.quotaArgs.Amount
// 直接分发
				s.dispatchToHandler(state)
			}
		}
	}

// Observe mixer_dispatcher_destinations_per_request
// Observe mixer_dispatcher_instances_per_request
	updateRequestCounters(ndestinations, ninputs)

// 等待所有处理器处理完毕
	s.waitForDispatched()

// 如果当前执行的是配额处理器，且没有找到配额，则警告但是允许访问
	if s.variety == tpb.TEMPLATE_VARIETY_QUOTA &amp;&amp; !foundQuota {
		s.quotaResult.Amount = s.quotaArgs.Amount
		s.quotaResult.ValidDuration = defaultValidDuration
		log.Warnf("Requested quota '%s' is not configured", s.quotaArgs.Quota)
	}

	return nil
}</pre>
<p>需要注意： </p>
<ol>
<li>对于REPORT模板，仅仅是将生成的Instance存放到dispatchState.instances数组中，不分发。延迟到所有请求消息处理完毕后，由Reporter.Flush统一分发</li>
<li>对于CHECK模板，直接调用session.dispatchToHandler进行分发</li>
</ol>
<p>分发不是直接在当前线程调用适配器，而是排队，由协程池的调度循环异步处理：</p>
<pre class="crayon-plain-tag">func (s *session) dispatchToHandler(ds *dispatchState) {
	s.activeDispatches++
	ds.session = s
// 调用协程池，调度一个工作
	s.impl.gp.ScheduleWork(ds.invokeHandler, nil)
}</pre>
<p>dispatchState.invokeHandler方法真正直接调用适配器：</p>
<pre class="crayon-plain-tag">func (ds *dispatchState) invokeHandler(interface{}) {
// 顺利处理完毕，没有Panic
	reachedEnd := false

	defer func() {
		if reachedEnd {
			return
		}
// 从适配器代码导致的Panic中恢复，防止Mixer直接崩了
		r := recover()
		ds.err = fmt.Errorf("panic during handler dispatch: %v", r)
		log.Errorf("%v\n%s", ds.err, debug.Stack())

		if log.DebugEnabled() {
			log.Debugf("stack dump for handler dispatch panic:\n%s", debug.Stack())
		}
// 提示此此目的地的分发处理完毕
		ds.session.completed &lt;- ds
	}()

// 跟踪
	span, ctx, start := ds.beginSpan(ds.ctx)

	log.Debugf("begin dispatch: destination='%s'", ds.destination.FriendlyName)

	switch ds.destination.Template.Variety {
// 属性生成器
	case tpb.TEMPLATE_VARIETY_ATTRIBUTE_GENERATOR:
		ds.outputBag, ds.err = ds.destination.Template.DispatchGenAttrs(
			ctx, ds.destination.Handler, ds.instances[0], ds.inputBag, ds.mapper)
// 前置条件检查
	case tpb.TEMPLATE_VARIETY_CHECK:
		ds.checkResult, ds.err = ds.destination.Template.DispatchCheck(
			ctx, ds.destination.Handler, ds.instances[0])
// 遥测/报告
	case tpb.TEMPLATE_VARIETY_REPORT:
		ds.err = ds.destination.Template.DispatchReport(
			ctx, ds.destination.Handler, ds.instances)
// 配额
	case tpb.TEMPLATE_VARIETY_QUOTA:
		ds.quotaResult, ds.err = ds.destination.Template.DispatchQuota(
			ctx, ds.destination.Handler, ds.instances[0], ds.quotaArgs)
// 无法处理的模板类型，Panic
	default:
		panic(fmt.Sprintf("unknown variety type: '%v'", ds.destination.Template.Variety))
	}

	log.Debugf("complete dispatch: destination='%s' {err:%v}", ds.destination.FriendlyName, ds.err)
// 追踪
	ds.completeSpan(span, time.Since(start), ds.err)
// 将当前目的地设置为分发处理完毕
	ds.session.completed &lt;- ds

	reachedEnd = true
}</pre>
<p>可以看到，上述方法都是把调用委托给目的地的TemplateInfo.Dispatch***函数指针处理的。这些函数指针就是适配器的相应方法。对于Metric模板，Prometheus适配器的方法实现如下：</p>
<pre class="crayon-plain-tag">func (h *handler) HandleMetric(_ context.Context, vals []*metric.Instance) error {
	var result *multierror.Error

// 遍历Instance
	for _, val := range vals {
// 获取该Instance对应的handler（例如requestcount.metric.istio-system）的信息（cinfo）
		ci := h.metrics[val.Name]
		if ci == nil {
			result = multierror.Append(result, fmt.Errorf("could not find metric info from adapter config for %s", val.Name))
			continue
		}
		collector := ci.c
		switch ci.kind {
// 按指标类型分别处理
		case config.GAUGE:
			vec := collector.(*prometheus.GaugeVec)
			amt, err := promValue(val.Value)
			if err != nil {
				result = multierror.Append(result, fmt.Errorf("could not get value for metric %s: %v", val.Name, err))
				continue
			}
			pl := promLabels(val.Dimensions)
			if h.labelsCache != nil {
				h.labelsCache.Set(key(val.Name, "gauge", pl, ci.sortedLabels), &amp;cacheEntry{vec, pl})
			}
			vec.With(pl).Set(amt)
		case config.COUNTER:
// 转换为指标向量，指标向量的每个元素是具有不同标签值的同一类（名字相同）指标
			vec := collector.(*prometheus.CounterVec)
			amt, err := promValue(val.Value)
			if err != nil {
				result = multierror.Append(result, fmt.Errorf("could not get value for metric %s: %v", val.Name, err))
				continue
			}
			pl := promLabels(val.Dimensions)
			if h.labelsCache != nil {
				h.labelsCache.Set(key(val.Name, "counter", pl, ci.sortedLabels), &amp;cacheEntry{vec, pl})
			}
// vec.With(pl)返回具有指定标签集的指标对象，这里是Counter，然后加上一个值（在当前时间点）
			vec.With(pl).Add(amt)
		case config.DISTRIBUTION:
// DISTRIBUTION映射为Prometheus类型 Histogram
			vec := collector.(*prometheus.HistogramVec)
			amt, err := promValue(val.Value)
			if err != nil {
				result = multierror.Append(result, fmt.Errorf("could not get value for metric %s: %v", val.Name, err))
				continue
			}
			pl := promLabels(val.Dimensions)
			if h.labelsCache != nil {
				h.labelsCache.Set(key(val.Name, "distribution", pl, ci.sortedLabels), &amp;cacheEntry{vec, pl})
			}
			vec.With(pl).Observe(amt)
		}
	}

	return result.ErrorOrNil()
}


cinfo struct {
// 负责收集指标的接口，gauge counter等都实现了此接口
	c            prometheus.Collector
	sha          [sha1.Size]byte
	kind         config.Params_MetricInfo_Kind
	sortedLabels []string
}</pre>
<div class="blog_h2"><span class="graybg">主要接口</span></div>
<div class="blog_h3"><span class="graybg">Dispatcher</span></div>
<p>将入站的API调用分发给配置的适配器：</p>
<pre class="crayon-plain-tag">type Dispatcher interface {
	// 进行预处理，将请求包分发给那些需要提前执行的适配器，
	// 目前这种适配器主要指属性生成适配器
	Preprocess(ctx context.Context, requestBag attribute.Bag, responseBag *attribute.MutableBag) error

	// 进行CHECK分发，基于CHECK类型模板的Instance，将被转发给感兴趣的适配器
	Check(ctx context.Context, requestBag attribute.Bag) (adapter.CheckResult, error)

	// 获取能够缓冲REPORT请求的报告器
	GetReporter(ctx context.Context) Reporter

	// 进行QUOTA分发
	Quota(ctx context.Context, requestBag attribute.Bag, qma QuotaMethodArgs) (adapter.QuotaResult, error)
}</pre>
<div class="blog_h3"><span class="graybg">Reporter</span></div>
<p>负责产生一系列的报告：</p>
<pre class="crayon-plain-tag">type Reporter interface {
	// 添加一个条目（请求包）到报告状态中
	Report(requestBag attribute.Bag) error

	// 刷出所有缓冲的状态到适当的适配器
	Flush() error

	// 完成Reporter的处理过程
	Done()
}</pre>
<div class="blog_h2"><span class="graybg">主要结构</span></div>
<div class="blog_h3"><span class="graybg">Destination</span></div>
<p>目的地，包含一个目标处理器，以及需要（在满足条件的情况下）发送给它的实例：</p>
<pre class="crayon-plain-tag">type Destination struct {
	// 用于调试的目的地ID
	id uint32

	// 需要调用的处理器
	Handler adapter.Handler

	// 用于监控/日志目的的处理器名称
	HandlerName string

	// 用于监控/日志目的的适配器名称（处理器类型）
	AdapterName string

	// 使用的模板，由于某些适配器支持多种模板，这些适配器可能对应多个Destination
	// 每种模板都有类型，并且定义了支持它的适配器必须实现的接口
	Template *TemplateInfo

	// 实例组，每组实例在满足条件的情况下，会发送给处理器
	InstanceGroups []*InstanceGroup

	// 最大允许的实例数
	maxInstances int

	// 用于监控/日志目的目的地名称
	FriendlyName string

	// 性能计数器
	Counters DestinationCounters
}</pre>
<div class="blog_h3"><span class="graybg">dispatchState</span></div>
<p>此结构用于收集<span style="background-color: #c0c0c0;">单个目的地（适配器+模板组合）</span>的处理状态和结果：</p>
<pre class="crayon-plain-tag">type dispatchState struct {
// 所属的分发调用会话
	session *session
// 上下文，其中包含了OpenTracing的Span信息
	ctx     context.Context
// 目的地
	destination *routing.Destination
// 对于属性生成模板，将模板输出映射入主属性列表的函数
	mapper      template.OutputMapperFn
// 输入包
	inputBag  attribute.Bag
// 配额请求的参数
	quotaArgs adapter.QuotaArgs
// 构建出的，供适配器消费的实例列表
	instances []interface{}

// 处理过程中的错误信息
	err         error
// 输出包
	outputBag   *attribute.MutableBag
// CHECK调用的结果
	checkResult adapter.CheckResult
// QUOTA调用的结果
	quotaResult adapter.QuotaResult
}</pre>
<div class="blog_h3"><span class="graybg">session</span></div>
<p>对一个客户端CHECK/REPORT/QUOTA请求的预处理和主处理的过程，是一个会话。此结构存储相关的信息：</p>
<pre class="crayon-plain-tag">type session struct {
// 拥有此会话的Dispatcher
	impl *Impl

// 本次会话使用的路由上下文
	rc *RoutingContext

// 上下文信息
	ctx          context.Context
// 输入包
	bag          attribute.Bag
// 配额调用的参数
	quotaArgs    QuotaMethodArgs
// 输出包
	responseBag  *attribute.MutableBag
// 报告请求的分发状态
	reportStates map[*routing.Destination]*dispatchState

// CHECK/QUOTA调用的结果
	checkResult adapter.CheckResult
	quotaResult adapter.QuotaResult
	err         error

// 正在执行的分发操作数量
	activeDispatches int

// 收集已完成的分发
	completed chan *dispatchState

// 本次操作的模板类别
	variety tpb.TemplateVariety
}</pre>
<div class="blog_h3"><span class="graybg">TemplateInfo</span></div>
<p>和模板有关的信息：</p>
<pre class="crayon-plain-tag">type TemplateInfo struct {
// 模板名称
	Name             string
// 模板种类
	Variety          tpb.TemplateVariety
// 各种Mixer调用的函数指针
	DispatchReport   template.DispatchReportFn
	DispatchCheck    template.DispatchCheckFn
	DispatchQuota    template.DispatchQuotaFn
	DispatchGenAttrs template.DispatchGenerateAttributesFn
}</pre>
<div class="blog_h3"><span class="graybg">InstanceGroup </span></div>
<p>按照匹配条件分组的、需要发送给适配器的实例的信息：</p>
<pre class="crayon-plain-tag">type InstanceGroup struct {
	// 用于调试的ID
	id uint32

	// 预编译的表达式，何时应用此实例组
	Condition compiled.Expression

	// 用于构建出实例的函数+名称
	Builders []NamedBuilder

	// 映射器函数，用于将属性生成适配器的输出属性，映射入主属性集
	Mappers []template.OutputMapperFn
}

type NamedBuilder struct {
	InstanceShortName string
	Builder           template.InstanceBuilderFn
}

OutputMapperFn func(attrs attribute.Bag) (*attribute.MutableBag, error)</pre>
<div class="blog_h3"><span class="graybg">QuotaMethodArgs</span></div>
<p>进行配额请求时，需要的参数 + 配额（资源）的类型：</p>
<pre class="crayon-plain-tag">type QuotaMethodArgs struct {
	// 在出现RPC调用并重试时，用于额度分配/释放（Quota allocation/allocation）调用的去重复
	DeduplicationID string

	// 分配那种配额
	Quota string

	// 分配的量
	Amount int64

	// 如果设置为true，则允许响应返回比请求少的额度。如果设置为false，那么额度不足时，直接返回0
	BestEffort bool
}</pre>
<div class="blog_h3"><span class="graybg">QuotaArgs</span></div>
<p>进行配额请求时，需要的参数：</p>
<pre class="crayon-plain-tag">QuotaArgs struct {
	DeduplicationID string
	QuotaAmount int64
	BestEffort bool
}</pre>
<div class="blog_h3"><span class="graybg">QuotaResult</span></div>
<p>由处理器提供的，额度分配的结果： </p>
<pre class="crayon-plain-tag">QuotaResult struct {
	// RPC调用的状态（状态码、消息、详情）
	Status rpc.Status
	// 分配的额度何时过期，0表示永不过期
	ValidDuration time.Duration
	// 分配的额度，可能比请求的额度小
	Amount int64
}</pre>
<div class="blog_h1"><span class="graybg">Envoy代理请求过程</span></div>
<p>在探索Envoy如何向Mixer发送请求之前，我们先来分析一下Envoy作为网络代理，是如何工作的。</p>
<div class="blog_h2"><span class="graybg">整体过程</span></div>
<div class="blog_h3"><span class="graybg">启动监听</span></div>
<ol>
<li>通过xDS或者静态配置，获得Envoy代理的监听器信息</li>
<li>如果监听器bind_to_port，则直接调用libevent的接口，绑定监听，回调函数设置为ListenerImpl::listenCallback</li>
</ol>
<div class="blog_h3"><span class="graybg">连接接受</span></div>
<ol>
<li>DispatcherImpl通过libevent，接收到请求，调用ListenerImpl::listenCallback</li>
<li>根据入站时的目的端口，选择适当的监听器处理请求，调用onAccept。存在Iptables重定向的情况下，监听器为15001
<ol>
<li>构建出监听器过滤器链</li>
<li>执行过滤器链，对于15001来说，此链只有OriginalDstFilter一个过滤器</li>
<li>OriginalDstFilter恢复原始目的地址</li>
<li>查找和原始目的地址匹配的监听器，并<span style="background-color: #c0c0c0;">转交请求</span></li>
</ol>
</li>
<li>如果发生请求转交，则接受者监听器也会执行类似于2的逻辑。但是<span style="background-color: #c0c0c0;">不会再次发生转交</span></li>
<li>实际负责连接的那个监听器，会调用ActiveListener.newConnection，并间接的创建ConnectionImpl</li>
<li>ConnectionImpl会利用<span style="background-color: #c0c0c0;">连接套接字（ConnectionSocketPtr）的文件描述符，调用Dispatcher.createFileEvent，注册读写事件的回调</span></li>
<li>到此，连接接受完毕，后续的读写事件由libevent异步触发</li>
</ol>
<div class="blog_h3"><span class="graybg">数据读写</span></div>
<ol>
<li>发生可读、可写、关闭事件时，ConnectionImpl::onFileEvent被调用</li>
<li>可写事件的回调onWriteReady先调用</li>
<li>可读事件的回调onReadReady后调用
<ol>
<li>尝试循环读取，根据读取结果设置Post操作</li>
<li>处理读取到的数据
<ol>
<li>遍历网络过滤器链
<ol>
<li>如果是L7连接，则执行HTTP网络管理器</li>
</ol>
</li>
</ol>
</li>
<li>执行Post IO操作</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">L4核心接口</span></div>
<div class="blog_h3"><span class="graybg">LinkedObject</span></div>
<p>这个混入类为任意的unique_ptr所持有的对象增加行为，允许方便的将这种对象link/unlink到列表中：</p>
<pre class="crayon-plain-tag">template &lt;class T&gt; class LinkedObject {
public:
  // 对象唯一性指针的列表
  typedef std::list&lt;std::unique_ptr&lt;T&gt;&gt; ListType;

  // 返回列表的迭代器
  typename ListType::iterator entry();

  // 对象当前是否被插入到列表，只要调用过moveInto***方法就返回true
  bool inserted();

  // 在两个列表之间移动对象
  void moveBetweenLists(ListType&amp; list1, ListType&amp; list2);

  // 移动对象到列表，放在最前面，注意所有权的转移
  void moveIntoList(std::unique_ptr&lt;T&gt;&amp;&amp; item, ListType&amp; list);

  // 移动对象到列表，放在最后面
  void moveIntoListBack(std::unique_ptr&lt;T&gt;&amp;&amp; item, ListType&amp; list);

  // 从列表中移除条目
  std::unique_ptr&lt;T&gt; removeFromList(ListType&amp; list);
};</pre>
<div class="blog_h3"><span class="graybg">DeferredDeletable</span></div>
<p>标记性接口。任何实现此接口的对象，都可以传递给Dispatcher。Dispatcher确保，未来在事件循环中删除对象。</p>
<pre class="crayon-plain-tag">class DeferredDeletable {
public:
  virtual ~DeferredDeletable() {}
};</pre>
<p>使用此接口，进行事件处理时，不需要担心栈unwind的问题</p>
<div class="blog_h3"><span class="graybg">ConnectionHandler</span></div>
<p>抽象的连接处理器，总体负责网络连接的处理。<span style="background-color: #c0c0c0;">ActiveListener、ActiveSocket的_parent都指向此对象</span>。</p>
<pre class="crayon-plain-tag">class ConnectionHandler {
public:

  // 此处理器持有的活动连接数
  virtual uint64_t numConnections() PURE;

  // 添加一个监听器到此处理器
  virtual void addListener(ListenerConfig&amp; config) PURE;

  // 根据地址查找监听器。返回监听器的指针，所有权不转移
  virtual Network::Listener* findListenerByAddress(const Network::Address::Instance&amp; address) PURE;

  // 移除使用指定tag作为键的监听器。监听器拥有的所有连接也会被移除
  virtual void removeListeners(uint64_t listener_tag) PURE;

  // 停止使用指定tag作为键的监听器。监听器拥有的所有连接不会被关闭，此方法用于draining
  virtual void stopListeners(uint64_t listener_tag) PURE;

  // 停止所有监听器
  virtual void stopListeners() PURE;

  // 禁用所有监听器。不会关闭监听器拥有的连接鹅，用于临时暂停接受连接
  virtual void disableListeners() PURE;

  // 启用所有监听器
  virtual void enableListeners() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">Listener</span></div>
<p>套接字监听器的抽象接口，是否此对象则停止对套接字的监听：</p>
<pre class="crayon-plain-tag">class Listener {
public:
  // 临时禁止接收新连接
  virtual void disable() PURE;

  // 继续接收新连接
  virtual void enable() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">ActiveListener</span></div>
<p>表示某个连接处理器ConnectionHandler所拥有的活动的监听器， ActiveListener引用一个Listener。</p>
<div class="blog_h3"><span class="graybg">ListenerFilter</span></div>
<pre class="crayon-plain-tag">class ListenerFilter {
public:
  /**
   * 在新的连接被接受，但是Connection对象尚未创建之前调用
   * @param cb 此回调提供一些重要方法
   * @return 过滤器管理器根据此返回状态，决定是否继续迭代过滤器链
   */
  virtual FilterStatus onAccept(ListenerFilterCallbacks&amp; cb) PURE;
};</pre>
<p>通过参数cb，可以continueFilterChain。</p>
<div class="blog_h3"><span class="graybg">ListenerFilterManager</span></div>
<p>此接口用于管理监听器过滤器链：</p>
<pre class="crayon-plain-tag">class ListenerFilterManager {
public:
  // 为指定的监听器添加过滤器
  virtual void addAcceptFilter(ListenerFilterPtr&amp;&amp; filter) PURE;
};</pre>
<p>ActiveSocket实现了ListenerFilterManager、ListenerFilterCallbacks。</p>
<div class="blog_h3"><span class="graybg">ListenerFilterCallbacks</span></div>
<p>此接口供监听器过滤器使用，后者通过它和监听器管理器通信：</p>
<pre class="crayon-plain-tag">class ListenerFilterCallbacks {
public:
  /**
   * @return ConnectionSocket 过滤器所操作的连接套接字
   */
  virtual ConnectionSocket&amp; socket() PURE;

  /**
   * @return 分发事件的Dispatcher
   */
  virtual Event::Dispatcher&amp; dispatcher() PURE;

  /**
   * 继续执行过滤器链
   */
  virtual void continueFilterChain(bool success) PURE;
};TransportSocket</pre>
<p>ActiveSocket实现了ListenerFilterManager、ListenerFilterCallbacks。 </p>
<div class="blog_h3"><span class="graybg">ListenerCallbacks</span></div>
<p>此接口供监听器使用：</p>
<pre class="crayon-plain-tag">class ListenerCallbacks {
public:
  /**
   * 当新连接被接受后，回调此方法
   * @param socket 移动到被调用者的套接字
   * @param redirected 提示套接字已经经过重定向
   */
  virtual void onAccept(ConnectionSocketPtr&amp;&amp; socket, bool hand_off_restored_destination_connections = true) PURE;

  /**
   * 当新连接被接受后，回调此方法
   * @param new_connection 移动到被调用者的套接字
   */
  virtual void onNewConnection(ConnectionPtr&amp;&amp; new_connection) PURE;
};</pre>
<p>ActiveListener实现了此接口。</p>
<div class="blog_h3"><span class="graybg">FilterChainManager</span></div>
<p>此接口用于管理过滤器链：</p>
<pre class="crayon-plain-tag">class FilterChainManager {
public:
  /**
   * 查找匹配新连接的元数据的过滤器链
   * @param socket 提供元数据
   * @return const FilterChain* 使用的过滤器链，如果没有匹配返回nullptr
   */
  virtual const FilterChain* findFilterChain(const ConnectionSocket&amp; socket) const PURE;
};</pre>
<p>ListenerImpl实现了此接口。</p>
<div class="blog_h3"><span class="graybg">FilterManager</span></div>
<p>负责添加网络过滤器给过滤器管理器，也就是Connection：</p>
<pre class="crayon-plain-tag">class FilterManager {
public:
  virtual ~FilterManager() {}

  // 添加一个写过滤器，过滤器以FIFO顺序调用
  virtual void addWriteFilter(WriteFilterSharedPtr filter) PURE;

  // 添加读写过滤器，相当于同时调用addWriteFilter/addReadFilter
  virtual void addFilter(FilterSharedPtr filter) PURE;

  // 添加一个读过滤器，过滤器以FIFO顺序调用
  virtual void addReadFilter(ReadFilterSharedPtr filter) PURE;

  // 实例化所有安装的读过滤器，相当于针对每个过滤器调用onNewConnection()
  virtual bool initializeReadFilters() PURE;
}</pre>
<div class="blog_h3"><span class="graybg">FilterChain</span></div>
<p>单个过滤器链的接口：</p>
<pre class="crayon-plain-tag">class FilterChain {
public:
  // 基于此过滤器链的新连接，使用的TransportSocketFactory，不同链使用的工厂可能不同（传输协议不同，RAW，TLS...）
  virtual const TransportSocketFactory&amp; transportSocketFactory() const PURE;
  // 基于此过滤器链的新连接，为了创建所有过滤器需要的工厂的集合
  virtual const std::vector&lt;FilterFactoryCb&gt;&amp; networkFilterFactories() const PURE;
}; </pre>
<div class="blog_h3"><span class="graybg">Filter</span></div>
<pre class="crayon-plain-tag">class Filter : public WriteFilter, public ReadFilter {};</pre>
<div class="blog_h3"><span class="graybg">ReadFilter</span></div>
<p>读处理路径（处理下游发来的数据）上的二进制（4层）过滤器：</p>
<pre class="crayon-plain-tag">class ReadFilter {
public:

  /**
   * 当连接上的数据被读取时调用
   * @param data 读取到的，可能已经被修改过的数据
   * @param end_stream 当连接启用半关闭语义时，用于提示是否到了最后一字节
   * @return status 过滤器管理器使用此状态决定如何进一步迭代其它过滤器
   */
  virtual FilterStatus onData(Buffer::Instance&amp; data, bool end_stream) PURE;

  /**
   * 当新连接刚创建时调用，过滤器链的迭代可以被中止
   * @return status 过滤器管理器使用此状态决定如何进一步迭代其它过滤器
   */
  virtual FilterStatus onNewConnection() PURE;

  /**
   * 初始化用于和过滤器管理器交互的读过滤器回调，过滤器被注册时，将被过滤器管理器调用一次
   * 任何需要用到底层连接的构造，需要在此函数的回调中执行
   *
   * IMPORTANT: 出站、复杂逻辑不要在此，放在onNewConnection()
   *
   */
  virtual void initializeReadFilterCallbacks(ReadFilterCallbacks&amp; callbacks) PURE;
}</pre>
<div class="blog_h3"><span class="graybg">WriteFilter</span></div>
<p>写处理路径（向下游发送数据）上的二进制（4层）过滤器： </p>
<pre class="crayon-plain-tag">class WriteFilter {
public:
  /**
   * 当在此连接上发生数据写入时调用
   * @param data 需要写入的，可能已经被修改过的数据
   * @param end_stream 当连接启用半关闭语义时，用于提示是否到了最后一字节
   */
  virtual FilterStatus onWrite(Buffer::Instance&amp; data, bool end_stream) PURE;
};</pre>
<div class="blog_h3"><span class="graybg">ConnectionSocket</span></div>
<p>连接套接字，表示传递给一个Connection的套接字：</p>
<ol>
<li>对于服务端，该对象表示已经Accept的套接字</li>
<li>对于客户端，该对象表示正在连接到远程地址的套接字</li>
</ol>
<pre class="crayon-plain-tag">class ConnectionSocket : public virtual Socket {
public:
  // 返回远程地址
  virtual const Address::InstanceConstSharedPtr&amp; remoteAddress() const PURE;
  // 用于服务器端，恢复原始目的地址
  virtual void restoreLocalAddress(const Address::InstanceConstSharedPtr&amp; local_address) PURE;
  // 设置远程地址
  virtual void setRemoteAddress(const Address::InstanceConstSharedPtr&amp; remote_address) PURE;
  // 原始目的地址是否被恢复
  virtual bool localAddressRestored() const PURE;
  // 设置传输协议，例如RAW_BUFFER, TLS
  virtual void setDetectedTransportProtocol(absl::string_view protocol) PURE;
  // 返回传输协议
  virtual absl::string_view detectedTransportProtocol() const PURE;
  // 设置请求的应用协议，例如ALPN in TLS
  virtual void setRequestedApplicationProtocols(const std::vector&lt;absl::string_view&gt;&amp; protocol) PURE;
  // 返回请求的应用协议
  virtual const std::vector&lt;std::string&gt;&amp; requestedApplicationProtocols() const PURE;
  // 设置请求的服务器名称
  virtual void setRequestedServerName(absl::string_view server_name) PURE;
  // 返回请求的服务器名称
  virtual absl::string_view requestedServerName() const PURE;
};</pre>
<div class="blog_h3"><span class="graybg">TransportSocket</span></div>
<p>传输套接字，负责实际的读写，也进行某些数据转换（例如TLS）：</p>
<pre class="crayon-plain-tag">class TransportSocket {
public:
  // 连接对象调用此方法依次，初始化传输套接字的回调
  virtual void setTransportSocketCallbacks(TransportSocketCallbacks&amp; callbacks) PURE;

  // 由网络级协商选择的协议
  virtual std::string protocol() const PURE;

  // 套接字是否已经被flush和close
  virtual bool canFlushClose() PURE;

  // 关闭传输套接字
  virtual void closeSocket(Network::ConnectionEvent event) PURE;

  // 读取到缓冲
  virtual IoResult doRead(Buffer::Instance&amp; buffer) PURE;

  /**
   * 将缓冲写入底层套接字
   * @param buffer 缓冲
   * @param end_stream 提示是否是流的终点，如果true则缓冲中所有数据都被写出去，连接变成半关闭
   */
  virtual IoResult doWrite(Buffer::Instance&amp; buffer, bool end_stream) PURE;

  // 底层传输建立后回调
  virtual void onConnected() PURE;

  // 如果当前是SSL连接，则返回Ssl::Connection，否则返回nullptr
  virtual const Ssl::Connection* ssl() const PURE;
};</pre>
<div class="blog_h3"><span class="graybg">TransportSocketCallbacks</span></div>
<p>传输套接字使用此回调集，和Connection通信：</p>
<pre class="crayon-plain-tag">class TransportSocketCallbacks {
public:

  // 返回关联到连接的IO句柄，从此局部可以得到连接套接字的FD
  virtual IoHandle&amp; ioHandle() PURE;
  virtual const IoHandle&amp; ioHandle() const PURE;

  // 返回关联的连接
  virtual Network::Connection&amp; connection() PURE;

  // 是否读缓冲应该被排干（drain，也就是调用过滤器链进行处理），用于强制配置的读缓冲大小限制
  virtual bool shouldDrainReadBuffer() PURE;

  // 将读缓冲标记为可（被事件循环）读
  virtual void setReadBufferReady() PURE;

  // 发起（Raise）一个连接事件到Connection对象，TLS使用此方法告知握手完成
  virtual void raiseEvent(ConnectionEvent event) PURE;
};</pre>
<div class="blog_h3"><span class="graybg">Connection</span></div>
<p>该接口表示原始的连接，它实现了FilterManager接口：</p>
<pre class="crayon-plain-tag">class Connection : public Event::DeferredDeletable, public FilterManager {
public:
  // 状态枚举
  enum class State { Open, Closing, Closed };

  // 连接发送字节后的回调
  typedef std::function&lt;void(uint64_t bytes_sent)&gt; BytesSentCb;

  // 注册当此连接上发生事件后执行的回调
  virtual void addConnectionCallbacks(ConnectionCallbacks&amp; cb) PURE;

  // 注册每当bytes被写入底层TransportSocket后执行的回调
  virtual void addBytesSentCallback(BytesSentCb cb) PURE;

  // 为此连接启用半关闭语义，从一个已经被对端半关闭的连接上进行读操作，不会关闭连接
  virtual void enableHalfClose(bool enabled) PURE;

  // 关闭连接
  virtual void close(ConnectionCloseType type) PURE;

  // 返回分发器
  virtual Event::Dispatcher&amp; dispatcher() PURE;

  // 返回唯一性的本地连接ID
  virtual uint64_t id() const PURE;

  // 返回网络级协商选择的下一个使用的协议
  virtual std::string nextProtocol() const PURE;

  // 为连接启用/禁用NO_DELAY
  virtual void noDelay(bool enable) PURE;

  // 启禁针对此连接的套接字读。当重新启用读时，如果输入缓冲有内容，会通过过滤器链分发
  virtual void readDisable(bool disable) PURE;

  // 当禁用套接字读后，Envoy是否应当检测TCP连接关闭。默认对新连接来说，检测
  virtual void detectEarlyCloseWhenReadDisabled(bool should_detect) PURE;

  // 读操作是否启用
  virtual bool readEnabled() const PURE;

  // 返回远程地址
  virtual const Network::Address::InstanceConstSharedPtr&amp; remoteAddress() const PURE;

  // 返回本地地址，对于客户端连接来说，即原始地址；对于服务器连接来说
  // 是本地的目的地址
  // 对于服务器连接来说，此地址可能和代理的监听地址不一样，因为下游连接可能被重定向，或者代理在透明模式下运行
  virtual const Network::Address::InstanceConstSharedPtr&amp; localAddress() const PURE;

  // 更新连接状态，出于性能的考虑，最终一致
  virtual void setConnectionStats(const ConnectionStats&amp; stats) PURE;

  // 如果该连接是SSL，则返回SSL连接数据；否则返回nullptr
  virtual const Ssl::Connection* ssl() const PURE;

  // 返回服务器名称，对于TLS来说即SNI
  virtual absl::string_view requestedServerName() const PURE;

  // 返回连接状态
  virtual State state() const PURE;

  /**
   * 写入数据到连接，数据将经过过滤器链
   * @param data 需要写入的数据
   * @param end_stream 如果为true，则提示此为最后一次写操作，导致连接半关闭。必须enableHalfClose(true)才能传入true
   */
  virtual void write(Buffer::Instance&amp; data, bool end_stream) PURE;

  // 设置该连接的缓冲区的软限制
  // 对于读缓冲，限制处理流水线在flush到下一个stage前能缓冲的最大字节数
  // 对于写缓冲，设置水位。如果缓冲了足够的数据，触发onAboveWriteBufferHighWatermark调用
  virtual void setBufferLimits(uint32_t limit) PURE;

  // 获得软限制
  virtual uint32_t bufferLimit() const PURE;

  // 本地地址是否被还原为原始目的地址
  virtual bool localAddressRestored() const PURE;

  // 连接当前是否高于高水位
  virtual bool aboveHighWatermark() const PURE;

  // 获取此连接的套接字选项
  virtual const ConnectionSocket::OptionsSharedPtr&amp; socketOptions() const PURE;

  // 获取关联到此连接的StreamInfo对象。StreamInfo典型用于日志目的
  // 每个过滤器都可以通过StreamInfo.FilterState来添加特定的信息
  // 在此上下文中每个连接对应一个StreamInfo。而对于HTTP连接管理器，每个请求对应一个StreamInfo
  virtual StreamInfo::StreamInfo&amp; streamInfo() PURE;
  virtual const StreamInfo::StreamInfo&amp; streamInfo() const PURE;

  // 设置延迟连接关闭的超时
  virtual void setDelayedCloseTimeout(std::chrono::milliseconds timeout) PURE;
  virtual std::chrono::milliseconds delayedCloseTimeout() const PURE;
}</pre>
<div class="blog_h3"><span class="graybg">ConnectionCallbacks</span></div>
<p>L4连接上发生的事件的回调：</p>
<pre class="crayon-plain-tag">class ConnectionCallbacks {
public:
  virtual ~ConnectionCallbacks() {}

  // ConnectionEvent的回调
  virtual void onEvent(ConnectionEvent event) PURE;

  // 当连接的写缓冲超过高水位后调用
  virtual void onAboveWriteBufferHighWatermark() PURE;

  // 当连接的写缓冲，从超过高水位变为低于低水位后调用
  virtual void onBelowWriteBufferLowWatermark() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">ActiveConnection</span></div>
<p>表示某个连接处理器所（通过ActiveListener）拥有的活动的连接。ActiveConnection引用一个Connection、一个ActiveListener。</p>
<div class="blog_h2"><span class="graybg">L7核心接口</span></div>
<div class="blog_h3"><span class="graybg">Connection</span></div>
<p>表示可以拥有多个流（Stream）的HTTP客户端/服务器连接：</p>
<pre class="crayon-plain-tag">class Connection {
public:

  // 分发入站的请求数据
  virtual void dispatch(Buffer::Instance&amp; data) PURE;

  // 给对端提示以go away，从此时开始，不能创建新的流
  virtual void goAway() PURE;

  // 返回连接的协议
  virtual Protocol protocol() PURE;

  // 给对端提示以shutdown notice，对端不应该在发送任何新的流，但是对于已经达到的流u，不会被重置
  virtual void shutdownNotice() PURE;

  // HTTP编解码器是否有数据需要写入，但是由于协议的原因（例如窗口更新），无法完成
  virtual bool wantsToWrite() PURE;

  // 当底层的Network::Connection超过高水位后，调用此方法
  virtual void onUnderlyingConnectionAboveWriteBufferHighWatermark() PURE;

  // 当底层的Network::Connection超过高水位后，然后由低于低水位后调用此方法
  virtual void onUnderlyingConnectionBelowWriteBufferLowWatermark() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">ConnectionCallbacks</span></div>
<p>HTTP连接级别的回调：</p>
<pre class="crayon-plain-tag">class ConnectionCallbacks {
public:
  // 对端提示go away时触发此回调，不允许创建新流
  virtual void onGoAway() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">ServerConnection</span></div>
<p>服务器端连接：</p>
<pre class="crayon-plain-tag">class ServerConnection : public virtual Connection {};</pre>
<p>HTTP连接管理器ConnectionManagerImpl.codec字段是ServerConnection类型，后者承担读取到的请求数据的分发（Dispatch，给HTTP解析器）职责。</p>
<div class="blog_h3"><span class="graybg">ServerConnectionCallbacks</span></div>
<p>继承ConnectionCallbacks回调，并添加方法：</p>
<pre class="crayon-plain-tag">class ServerConnectionCallbacks : public virtual ConnectionCallbacks {
public:
  /**
   * 当对端初始化一个新的请求流后触发此回调
   * @param response_encoder 提供用于创建响应的编码器，请求、响应由同一流对象管理
   * @param is_internally_created 提示此流是流客户端创建，还是由Envoy自己创建（例如内部重定向）
   */
  virtual StreamDecoder&amp; newStream(StreamEncoder&amp; response_encoder,
                                   bool is_internally_created = false) PURE;
};</pre>
<div class="blog_h3"><span class="graybg">StreamDecoder</span></div>
<p>HTTP流解码器，可以解码下游发来的请求：</p>
<pre class="crayon-plain-tag">class StreamDecoder {
public:
  virtual ~StreamDecoder() {}

  // 处理解码后的100-Continue头的map
  virtual void decode100ContinueHeaders(HeaderMapPtr&amp;&amp; headers) PURE;

  // 处理解码后的头
  virtual void decodeHeaders(HeaderMapPtr&amp;&amp; headers, bool end_stream) PURE;

  // 处理解码后的数据帧
  virtual void decodeData(Buffer::Instance&amp; data, bool end_stream) PURE;

  // 处理解码后的尾帧
  virtual void decodeTrailers(HeaderMapPtr&amp;&amp; trailers) PURE;

  // 处理解码后的元数据
  virtual void decodeMetadata(MetadataMapPtr&amp;&amp; metadata_map) PURE;
};</pre>
<p>这里这里的decode有歧义：</p>
<ol>
<li>decoded，表示经由http_parser解析，结构化为C++对象 —— HTTP语境</li>
<li>decode，调用Envoy的流解码器处理那些C++对象 —— Envoy语境</li>
</ol>
<div class="blog_h3"><span class="graybg">StreamEncoder</span></div>
<p>HTTP流编码器，可以编码需要发送给下游的应答，接口类似于StreamDecoder。</p>
<div class="blog_h3"><span class="graybg">StreamCallbacks</span></div>
<p>针对HTTP流的回调：</p>
<pre class="crayon-plain-tag">class StreamCallbacks {
public:
  // 对端重置了流后调用，参数是重置原因
  virtual void onResetStream(StreamResetReason reason) PURE;

  // 当一个流（HTTP2），或者流发向的连接（HTTP1）超过高水位后调用
  virtual void onAboveWriteBufferHighWatermark() PURE;

  // 当一个流（HTTP2），或者流发向的连接（HTTP1）从超过高水位降到低于低水位后调用
  virtual void onBelowWriteBufferLowWatermark() PURE;
} </pre>
<div class="blog_h3"><span class="graybg">ActiveStream</span></div>
<p>表示连接上的单个HTTP流，实现了StreamDecoder、StreamCallbacks、FilterChainFactoryCallbacks接口。</p>
<div class="blog_h3"><span class="graybg">FilterChainFactoryCallbacks</span></div>
<p>HTTP连接管理器提供给过滤器链工厂的回调集，依赖于此回调工厂能够以应用程序特定的方式构建过滤器链：</p>
<pre class="crayon-plain-tag">class FilterChainFactoryCallbacks {
public:

  // 添加读取流数据时使用的解码器
  virtual void addStreamDecoderFilter(Http::StreamDecoderFilterSharedPtr filter) PURE;

  // 添加写入流数据时使用的编码器
  virtual void addStreamEncoderFilter(Http::StreamEncoderFilterSharedPtr filter) PURE;

  // 添加读写编解码器
  virtual void addStreamFilter(Http::StreamFilterSharedPtr filter) PURE;

  // 添加访问日志处理器，在流被销毁时调用
  virtual void addAccessLogHandler(AccessLog::InstanceSharedPtr handler) PURE;
}</pre>
<div class="blog_h3"><span class="graybg">StreamFilterBase</span></div>
<p>流编码/解码过滤器的基类：</p>
<pre class="crayon-plain-tag">class StreamFilterBase {
public:
  /**
   * 在过滤器被销毁前调用此方法，这可能发生在
   * 1、正常的流（下游+上游）完成后
   * 2、发生重置后
   * 每个过滤器负责确保在此方法的上下文中，所有异步事件被清理完毕。这些异步事件包括定时器、网络调用等
   *
   * 不在析构函数中进行清理而使用onDestroy钩子的原因和Envoy的延迟删除模型有关。此模型规避了Stack unwind
   * 有关的复杂性。在onDestroy之后，过滤器不得调用编码/解码过滤器回调
   */
  virtual void onDestroy() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">StreamDecoderFilter</span></div>
<p>流解码过滤器，继承StreamFilterBase：</p>
<pre class="crayon-plain-tag">class StreamDecoderFilter : public StreamFilterBase {
public:
  // 解码请求头
  virtual FilterHeadersStatus decodeHeaders(HeaderMap&amp; headers, bool end_stream) PURE;

  // 解码数据帧
  virtual FilterDataStatus decodeData(Buffer::Instance&amp; data, bool end_stream) PURE;

  // 解码请求尾
  virtual FilterTrailersStatus decodeTrailers(HeaderMap&amp; trailers) PURE;

  // 设置此解码过滤器的过滤器回调
  virtual void setDecoderFilterCallbacks(StreamDecoderFilterCallbacks&amp; callbacks) PURE;
} </pre>
<div class="blog_h3"><span class="graybg">StreamFilterCallbacks</span></div>
<p>传递给所有（读/写）过滤器的回调函数集，用于写响应数据、和底层流交互：</p>
<pre class="crayon-plain-tag">class StreamFilterCallbacks {
public:

  // 获取L4网络连接
  virtual const Network::Connection* connection() PURE;

  // 返回线程本地的Dispatcher，从此分发器来分配定时器
  virtual Event::Dispatcher&amp; dispatcher() PURE;

  // 将底层的流进行重置
  virtual void resetStream() PURE;

  // 返回当前请求使用的路由
  // 实现应当能够进行路由缓存，避免反复查找。如果过滤器修改了请求头，则路由可能需要改变，此时应当调用clearRouteCache()

  // 未来可能会允许过滤器对路由条目进行覆盖
  virtual Router::RouteConstSharedPtr route() PURE;

  // 返回被缓存的路由条目的上游集群信息（clusterInfo）。该方法用于避免在过滤器链中进行反复的查找，同时
  // 确保当路由被picked/repicked后能提供clusterInfo的一致性视图
  virtual Upstream::ClusterInfoConstSharedPtr clusterInfo() PURE;

  // 为当前请求清除路由缓存，如果过滤器修改了请求头，并且此修改可能影响选路，则必须调用该方法
  virtual void clearRouteCache() PURE;

  // 返回用于日志目的的流唯一标识
  virtual uint64_t streamId() PURE;

  // 返回用于日志目的的StreamInfo
  virtual StreamInfo::StreamInfo&amp; streamInfo() PURE;

  // 返回追踪用的当前追踪上下文
  virtual Tracing::Span&amp; activeSpan() PURE;

  // 返回追踪配置
  virtual const Tracing::Config&amp; tracingConfig() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">StreamDecoderFilterCallbacks</span></div>
<p>继承StreamFilterCallbacks，添加专用于解码（读）过滤器的回调：</p>
<pre class="crayon-plain-tag">class StreamDecoderFilterCallbacks : public virtual StreamFilterCallbacks {
public:
  /**
   * 使用缓冲的头，以及请求体，继续迭代过滤器链。该方法仅仅在以下情况之一才会调用
   * 1、先前的过滤器在decodeHeaders()后返回StopIteration
   * 2、先前的过滤器在decodeData()后返回StopIterationAndBuffer, StopIterationAndWatermark 或 StopIterationNoBuffer
   *
   * HTTP连接管理器会分发请求头、缓冲的请求体给过滤器链中的下一个过滤器
   * 
   * 如果请求没有完成，当前过滤器仍然会继续接受decodeData()调用，并且必须返回适当的的状态码
   *
   */
  virtual void continueDecoding() PURE;

  // 返回当前过滤器、或者链中先前过滤器缓冲的数据。如果尚未缓冲任何内容，返回nullpt
  virtual const Buffer::Instance* decodingBuffer() PURE;

  /**
   * 添加解码处理后的、缓冲的请求体数据。在某些高级用例中，decodeData()返回StopIterationAndBuffer不能满足
   * 需要，需要调用此方法：
   *
   * 1) 对于header-only请求需要被转换为包含请求体的请求，可以在 decodeHeaders() 回调中调用此方法，添加请求体
   * 后续过滤器会依次接收调用decodeHeaders(..., false)、decodeData(..., true)。在直接迭代、停止后继续迭代
   * 场景下，都可以使用
   * 
   *
   * 2) 如果某个过滤器希望在end_stream=true的情况下，在一个数据回调中查看所有缓冲的数据，可以调用该方法，以立即缓冲数据
   * 避免同时处理已缓冲数据、以及当前回调产生的数据
   *
   * 3) 如果某个过滤器在调用后续过滤器时，需要添加额外的缓冲请求体数据
   *
   * 4) 如果在decodeTrailers()回调中需要添加额外的数据。可以在前述回调的上下文中调用此方法
   * 所有后续过滤器会依次接受decodeData(..., false)、decodeTrailers()调用
   *
   * 在其它场景下调用此方法是错误
   *
   * @param data Buffer::Instance 添加需要被解码的数据
   * @param streaming_filter boolean 提示该过滤器是流式处理还是缓冲了完整请求体
   */
  virtual void addDecodedData(Buffer::Instance&amp; data, bool streaming_filter) PURE;

  /**
   * 添加解码后的请求尾。只能在end_stream=true时在decodeData中调用 
   * 在decodeData中调用时，请求尾映射被初始化为空map并以引用的方式返回
   * 该方法最多调用一次
   *
   * @return 返回新的空请求尾map
   */
  virtual HeaderMap&amp; addDecodedTrailers() PURE;

  /* 基于其它的状态码、请求体，生成一个Envoy本地的响应并发送给下游
   * 如果是gRPC请求，则本地响应编码为gRPC响应，HTTP状态码置为200。从参数生成grpc-status、grpc-message
   *
   * @param response_code HTTP状态码
   * @param body_text HTTP请求体，以text/plain发送或者编码在grpc-message头中
   * @param modify_headers 可选的回调函数，用于修改响应头
   * @param grpc_status gRPC状态码，覆盖通过httpToGrpcStatus推导出的gRPC状态码
   */
  virtual void sendLocalReply(Code response_code, absl::string_view body_text,
                              std::function&lt;void(HeaderMap&amp; headers)&gt; modify_headers,
                              const absl::optional&lt;Grpc::Status::GrpcStatus&gt; grpc_status) PURE;

  /**
   * 编码100-Continue响应头。该头不在encodeHeaders中处理，因为大部分情况下Envoy用户和过滤器
   * 不希望代理100-Continue，而是直接吐出，可以忽略多次编码响应头encodeHeaders()的复杂性
   *
   * @param headers supplies 需要编码的头
   */
  virtual void encode100ContinueHeaders(HeaderMapPtr&amp;&amp; headers) PURE;

  /**
   * 编码响应头。HTTP连接管理器会自动探测一些不发给下游的伪头
   *
   * @param headers 需要编码的头
   * @param end_stream 这是不是一个header-only的request/response
   */
  virtual void encodeHeaders(HeaderMapPtr&amp;&amp; headers, bool end_stream) PURE;

  /**
   * 编码响应数据
   * @param data 需要编码的数据
   * @param end_stream 提示这是不是最后一个数据帧
   */
  virtual void encodeData(Buffer::Instance&amp; data, bool end_stream) PURE;

  /**
   * 编码响应尾数据，隐含意味着流的结束
   * @param trailers supplies 需要编码的尾
   */
  virtual void encodeTrailers(HeaderMapPtr&amp;&amp; trailers) PURE;

  /**
   * 编码元数据
   *
   * @param metadata_map supplies 需要编码的元数据的unique_ptr
   */
  virtual void encodeMetadata(MetadataMapPtr&amp;&amp; metadata_map) PURE;

  /**
   * 当解码过滤器的缓冲，或者过滤器需要发送数据到的那些缓冲，超越高水位后调用
   *
   * 对于路由过滤器这样的HTTP过滤器，会使用多个缓冲（codec、connection...），该方法可能被调用多次
   * 这些过滤器应当负责，在对应的缓冲被排干后，以等同次数调用低水位回调connection etc.)
   */
  virtual void onDecoderFilterAboveWriteBufferHighWatermark() PURE;

  /**
   * 当解码过滤器的缓冲，或者过滤器需要发送数据到的那些缓冲，超越高水位后，后降低到低于低水位后调用
   */
  virtual void onDecoderFilterBelowWriteBufferLowWatermark() PURE;

  /**
   * 需要订阅下游流、下游连接上的水位事件的过滤器，调用此方法
   * 订阅后，对于每个outstanding backed up buffer，过滤器的回调都被调用
   */
  virtual void addDownstreamWatermarkCallbacks(DownstreamWatermarkCallbacks&amp; callbacks) PURE;

  /**
   * 需要停止订阅下游流、下游连接上的水位事件的过滤器，调用此方法
   * 在DownstreamWatermarkCallbacks的回调函数的栈下面调用此方法不安全
   */
  virtual void removeDownstreamWatermarkCallbacks(DownstreamWatermarkCallbacks&amp; callbacks) PURE;

  /**
   * 用于改变解码过滤器的缓冲区大小
   *
   * @param limit 新的缓冲大小
   */
  virtual void setDecoderBufferLimit(uint32_t limit) PURE;

  // 返回解码过滤器缓冲大小，0表示无限制u
  virtual uint32_t decoderBufferLimit() PURE;

  // 将当前流看作是新的，就好像它的所有头都是刚到达一样
  // 如果操作成功，会导致创建新的过滤器链，并且上游请求可能和原始的下游流关联
  // 如果操作失败，并且下面列出的前置条件不满足，则调用者负责处理和终止原始流
  //
  // 前置条件
  //   - 流必须已经被完整的读取
  //   - 流必须没有请求体
  //
  // 注意HTTP消毒器不会针对这种重新创建的流进行操作，它假设消毒已经完成
  virtual bool recreateStream() PURE;
} </pre>
<div class="blog_h3"><span class="graybg">ActiveStreamDecoderFilter</span></div>
<p>表示活动的解码过滤器，继承ActiveStreamFilterBase，实现了StreamFilterCallbacks、StreamDecoderFilterCallbacks，也就是说，过滤器和过滤器回调是一体的。</p>
<p>该对象持有一个StreamDecoderFilter对象。ActiveStream通过字段<pre class="crayon-plain-tag">std::list&lt;ActiveStreamDecoderFilterPtr&gt; decoder_filters_</pre>来引用这种对象。</p>
<div class="blog_h2"><span class="graybg">HTTP1核心接口</span></div>
<div class="blog_h3"><span class="graybg">ActiveRequest</span></div>
<p>多个地方存在命名为ActiveRequest的结构，表示一个活动的HTTP1请求。</p>
<p>例如，作为Http::Http1::ServerConnectionImpl的私有成员：</p>
<pre class="crayon-plain-tag">struct ActiveRequest {
  // 构造请求对象时，必须传入响应编码器
  ActiveRequest(ConnectionImpl&amp; connection) : response_encoder_(connection) {}

  HeaderString request_url_;
  // 请求解码器
  StreamDecoder* request_decoder_{};
  // 响应编码器
  ResponseStreamEncoderImpl response_encoder_;
  // 请求的处理是否已经完毕
  bool remote_complete_{};
};</pre>
<p>作为Http::CodecClient的私有成员：</p>
<pre class="crayon-plain-tag">struct ActiveRequest : LinkedObject&lt;ActiveRequest&gt;,
                       public Event::DeferredDeletable,
                       public StreamCallbacks,
                       public StreamDecoderWrapper {
  ActiveRequest(CodecClient&amp; parent, StreamDecoder&amp; inner) : StreamDecoderWrapper(inner), parent_(parent) {}

  // 流回调函数
  void onResetStream(StreamResetReason reason) override { parent_.onReset(*this, reason); }
  void onAboveWriteBufferHighWatermark() override {}
  void onBelowWriteBufferLowWatermark() override {}
  void onPreDecodeComplete() override { parent_.responseDecodeComplete(*this); }
  void onDecodeComplete() override {}

  // 编码器
  StreamEncoder* encoder_{};
  CodecClient&amp; parent_;
} </pre>
<div class="blog_h2"><span class="graybg">启动监听</span></div>
<p>Envoy代理的需要创建哪些监听器，由Bootstrap配置 + xDS响应共同决定，本文不讨论细节，可以参考<a href="/interaction-between-istio-pilot-and-envoy">Istio Pilot与Envoy的交互机制解读</a>。</p>
<p>如果某个监听器配置了bind_to_port（默认情况下virtual 15001配置了），则会调用libevent的API，注册套接字监听事件：</p>
<pre class="crayon-plain-tag">ListenerImpl::ListenerImpl(Event::DispatcherImpl&amp; dispatcher, Socket&amp; socket, ListenerCallbacks&amp; cb,
                           bool bind_to_port, bool hand_off_restored_destination_connections)
    : BaseListenerImpl(dispatcher, socket), cb_(cb),
      hand_off_restored_destination_connections_(hand_off_restored_destination_connections),
      listener_(nullptr) {
  if (bind_to_port) {
    // 注册监听
    setupServerSocket(dispatcher, socket);
  }
}

void ListenerImpl::setupServerSocket(Event::DispatcherImpl&amp; dispatcher, Socket&amp; socket) {
  // 重置 CSmartPtr&lt;evconnlistener, evconnlistener_free&gt; ListenerPtr引用为新的evconnlistener
  listener_.reset(
      // libevent的base                      当前对象方法                   套接字的文件描述符
      evconnlistener_new(&amp;dispatcher.base(), listenCallback, this, 0, -1, socket.ioHandle().fd()));

  if (!listener_) {
    throw CreateListenerException(
        fmt::format("cannot listen on socket: {}", socket.localAddress()-&gt;asString()));
  }

  if (!Network::Socket::applyOptions(socket.options(), socket,
                                     envoy::api::v2::core::SocketOption::STATE_LISTENING)) {
    throw CreateListenerException(fmt::format("cannot set post-listen socket option on socket: {}",
                                              socket.localAddress()-&gt;asString()));
  }

  evconnlistener_set_error_cb(listener_.get(), errorCallback);
}</pre>
<div class="blog_h2"><span class="graybg">接受连接</span></div>
<p>Envoy默认会在15001端口上监听，当流量到达（通常是通过其它端口重定向到15001）时，Envoy的DispatcherImpl循环得到通知，并通过libevent回调Envoy::Network::ListenerImpl::listenCallback方法，该方法是一切<span style="background-color: #c0c0c0;">新连接处理的起点</span>：</p>
<pre class="crayon-plain-tag">void ListenerImpl::listenCallback(evconnlistener*, evutil_socket_t fd, sockaddr* remote_addr,
                                  int remote_addr_len, void* arg) {
  // 传递的参数是监听器对象
  ListenerImpl* listener = static_cast&lt;ListenerImpl*&gt;(arg);

  // IoSocketHandle为IO操作的抽象接口
  IoHandlePtr io_handle = std::make_unique&lt;IoSocketHandle&gt;(fd);

  // 如果监听器在ANY地址（0.0.0.0）上监听，从新套接字上获取本地地址
  const Address::InstanceConstSharedPtr&amp; local_address =
  // 获取远程地址
  const Address::InstanceConstSharedPtr&amp; remote_address =
      (remote_addr-&gt;sa_family == AF_UNIX)
          ? Address::peerAddressFromFd(io_handle-&gt;fd())
          : Address::addressFromSockAddr(*reinterpret_cast&lt;const sockaddr_storage*&gt;(remote_addr),
                                         remote_addr_len,
                                         local_address-&gt;ip()-&gt;version() == Address::IpVersion::v6);
  // 调用监听器的onAccept回调
  listener-&gt;cb_.onAccept(
      std::make_unique&lt;AcceptedSocketImpl&gt;(std::move(io_handle), local_address, remote_address),
      listener-&gt;hand_off_restored_destination_connections_);
}</pre>
<p>回调ConnectionHandlerImpl::ActiveListener::onAccept的逻辑如下：</p>
<pre class="crayon-plain-tag">// 此回调在新连接创建后调用
// socket 需要移动给被调用者的套接字对象
// redirected 如果套接字是第一次被其它监听器接受，并且随后被重定向给一个新的监听器时，为true
//            接收者监听器不得再次重定向
void ConnectionHandlerImpl::ActiveListener::onAccept(
    Network::ConnectionSocketPtr&amp;&amp; socket, bool hand_off_restored_destination_connections) {
  // 代表当前正在处理的套接字对象
  auto active_socket = std::make_unique&lt;ActiveSocket&gt;(*this, std::move(socket),
                                                      hand_off_restored_destination_connections);

  // 构建出过监听器过滤器链
  config_.filterChainFactory().createListenerFilterChain(*active_socket);
  // 开始迭代过滤器链
  active_socket-&gt;continueFilterChain(true);

  // 如果监听器过滤器链没有迭代完毕，则active_socket暂存到sockets_列表里
  // 防止active_socket因为超出作用域而被自动删除
  if (active_socket-&gt;iter_ != active_socket-&gt;accept_filters_.end()) {
    active_socket-&gt;startTimer();
    active_socket-&gt;moveIntoListBack(std::move(active_socket), sockets_);
  }
}</pre>
<div class="blog_h3"><span class="graybg">监听器过滤器链</span></div>
<p>ActiveSocket实现了Network::ListenerFilterManager：</p>
<pre class="crayon-plain-tag">class ListenerFilterManager {
public:
  virtual ~ListenerFilterManager() {}
  // 为监听器添加一个过滤器，过滤器以FIFO顺序被调用
  virtual void addAcceptFilter(ListenerFilterPtr&amp;&amp; filter) PURE;
};</pre>
<p>ListenerImpl的createListenerFilterChain方法支持为ListenerFilterManager提供过滤器链：</p>
<pre class="crayon-plain-tag">bool ListenerImpl::createListenerFilterChain(Network::ListenerFilterManager&amp; manager) {
  return Configuration::FilterChainUtility::buildFilterChain(manager, listener_filter_factories_);
}</pre>
<p>listener_filter_factories_在ListenerImpl初始化阶段创建，它是Network::ListenerFilterFactoryCb的迭代器，表示<span style="background-color: #c0c0c0;">当前监听器启用的所有监听器过滤器的工厂回调的集合</span>。调用ListenerFilterFactoryCb可以将过滤器安装到ListenerFilterManager，也就是ActiveSocket上：</p>
<pre class="crayon-plain-tag">bool FilterChainUtility::buildFilterChain( Network::ListenerFilterManager&amp; filter_manager,
    const std::vector&lt;Network::ListenerFilterFactoryCb&gt;&amp; factories) {
  for (const Network::ListenerFilterFactoryCb&amp; factory : factories) {
    factory(filter_manager);
  }
  return true;
}</pre>
<p>默认情况下，监听器virtual（15001端口）只配置一个监听器过滤器OriginalDstFilter，它的工厂如下：</p>
<pre class="crayon-plain-tag">class OriginalDstConfigFactory : public Server::Configuration::NamedListenerFilterConfigFactory {
public:
  // 此即工厂回调的工厂
  Network::ListenerFilterFactoryCb createFilterFactoryFromProto(const Protobuf::Message&amp;,
                               Server::Configuration::ListenerFactoryContext&amp;) override {
    return [](Network::ListenerFilterManager&amp; filter_manager) -&gt; void {
      // 上段代码的factory(filter_manager)调用的是此Lambda
      // 简单的创建OriginalDstFilter并添加到管理器
      filter_manager.addAcceptFilter(std::make_unique&lt;OriginalDstFilter&gt;());
    };
  }

  ProtobufTypes::MessagePtr createEmptyConfigProto() override {
    return std::make_unique&lt;Envoy::ProtobufWkt::Empty&gt;();
  }

  std::string name() override { return ListenerFilterNames::get().OriginalDst; }
};</pre>
<div class="blog_h3"><span class="graybg">迭代监听器过滤器链</span></div>
<p>当一个过滤器返回FilterStatus::StopIteration以终止过滤器迭代，那么：</p>
<ol>
<li>如果希望继续遍历后续过滤器链，以true参数调用下面的方法</li>
<li>如果过滤器执行失败，需要关闭连接，以false参数调用下面的方法</li>
</ol>
<pre class="crayon-plain-tag">void ConnectionHandlerImpl::ActiveSocket::continueFilterChain(bool success) {
  // 开始/继续迭代
  if (success) {
    if (iter_ == accept_filters_.end()) {
      iter_ = accept_filters_.begin();
    } else {
      iter_ = std::next(iter_);
    }
    // 从当前过滤器迭代到监听器过滤器集的尾部
    for (; iter_ != accept_filters_.end(); iter_++) {
     // 调用监听器过滤器的onAccept方法，this就是ActievSocket对象
      Network::FilterStatus status = (*iter_)-&gt;onAccept(*this);
      // 上一个过滤器提示，应当停止迭代
      if (status == Network::FilterStatus::StopIteration) {
        // 则本次过滤器迭代终止。上一个过滤器负责在未来重启迭代
        return;
      }
    }
    // 所有监听器过滤器都执行成功

    // 检查套接字是否需要重定向给其它监听器
    ActiveListener* new_listener = nullptr;

    // OriginalDstFilter会导致下面的分支执行
    if (hand_off_restored_destination_connections_ &amp;&amp; socket_-&gt;localAddressRestored()) {
      // 找到匹配原始目的地址的那个监听器
      new_listener = listener_.parent_.findActiveListenerByAddress(*socket_-&gt;localAddress());
    }
    if (new_listener != nullptr) {
      // 将由Iptables重定向到当前监听器的连接转交给匹配原始目的地址的监听器处理
      // 同时传递hand_off_restored_destination_connections=false，防止再次重定向
      new_listener-&gt;onAccept(std::move(socket_), false);
    } else {
      // 非重定向连接，或者重定向连接的接收者监听器
      if (socket_-&gt;detectedTransportProtocol().empty()) {
        // 设置默认传输协议
        socket_-&gt;setDetectedTransportProtocol(
            Extensions::TransportSockets::TransportSocketNames::get().RawBuffer);
      }
      // 在此监听器上创建新的连接对象
      listener_.newConnection(std::move(socket_));
    }
  }

  // 过滤器执行完毕，如果ActiveSocket已经linked，则unlink并删除
  if (inserted()) {
    unlink();
  }
}</pre>
<div class="blog_h3"><span class="graybg">OriginalDstFilter</span></div>
<p>该监听器过滤器的onAccept方法的实现如下：</p>
<pre class="crayon-plain-tag">Network::FilterStatus OriginalDstFilter::onAccept(Network::ListenerFilterCallbacks&amp; cb) {
  ENVOY_LOG(debug, "original_dst: New connection accepted");
  Network::ConnectionSocket&amp; socket = cb.socket();
  const Network::Address::Instance&amp; local_address = *socket.localAddress();

  // 通过系统调用os_syscalls.getsockopt(fd, SOL_IP, SO_ORIGINAL_DST, &amp;orig_addr, &amp;addr_len)获取原始目的IP
  if (local_address.type() == Network::Address::Type::Ip) {
    // 我们的例子中，原始目的IP地址为127.0.0.1:9898
    Network::Address::InstanceConstSharedPtr original_local_address = getOriginalDst(socket.ioHandle().fd());

    // 即使对于use_original_dst设置为true的监听器（也就是当前监听器），仍然能够接收不是由iptables重定向的连接
    // 如果连接不是被重定向的，那么getOriginalDst()的返回值和当前套接字的本地地址（Envoy代理端）相同
    // 这种情况下，当前监听器直接处理连接，而不会转交（hand off）给其它监听器
    if (original_local_address) {
      // 修改local_address_，并设置local_address_restored_为true
      socket.restoreLocalAddress(original_local_address);
    }
  }
  // 总是继续迭代监听器过滤器链
  return Network::FilterStatus::Continue;
}</pre>
<div class="blog_h2"><span class="graybg">处理TCP连接</span></div>
<p>监听器过滤器的处理是以ActiveSocket为中心的，套接字请求接受后，连接的处理则以ActiveListener为中心。 </p>
<p>执行完监听器过滤器后，ActiveSocket调用ActiveListener.newConnection，开始连接的处理：</p>
<pre class="crayon-plain-tag">void ConnectionHandlerImpl::ActiveListener::newConnection(Network::ConnectionSocketPtr&amp;&amp; socket) {
  // 首先，查找匹配此套接字的过滤器链
  const auto filter_chain = config_.filterChainManager().findFilterChain(*socket);
  if (filter_chain == nullptr) {
    // 找不到过滤器，记录统计信息，关闭套接字，结束处理
    ENVOY_LOG_TO_LOGGER(parent_.logger_, debug, "closing connection: no matching filter chain found");
    stats_.no_filter_chain_match_.inc();
    socket-&gt;close();
    return;
  }
  // 创建一个传输套接字，此套接字负责实际的读写操作
  // 具体工厂和协议有关，默认RawBufferSocketFactory，创建RawBufferSocket
  auto transport_socket = filter_chain-&gt;transportSocketFactory().createTransportSocket(nullptr);
  // 创建连接对象，设置为connected
  Network::ConnectionPtr new_connection = parent_.dispatcher_.createServerConnection(std::move(socket), std::move(transport_socket));
  // 设置缓冲区大小
  new_connection-&gt;setBufferLimits(config_.perConnectionBufferLimitBytes());
  // 写过滤器的顺序可能需要倒置
  new_connection-&gt;setWriteFilterOrder(config_.reverseWriteFilterOrder());

  // 为连接初始化过滤器链
  const bool empty_filter_chain = !config_.filterChainFactory().createNetworkFilterChain(
      *new_connection, filter_chain-&gt;networkFilterFactories());
  // 如果初始化过滤器链失败，则关闭连接
  if (empty_filter_chain) {
    ENVOY_CONN_LOG_TO_LOGGER(parent_.logger_, debug, "closing connection: no filters", *new_connection);
    new_connection-&gt;close(Network::ConnectionCloseType::NoFlush);
    return;新连接处理的起点
  }

  // 监听器的新连接回调
  onNewConnection(std::move(new_connection));
}</pre>
<div class="blog_h3"><span class="graybg">查找过滤器链配置</span></div>
<p>在ActiveListener::newConnection期间，调用config_.filterChainManager().findFilterChain来查找匹配连接的过滤器链配置：</p>
<pre class="crayon-plain-tag">const Network::FilterChain* ListenerImpl::findFilterChain(const Network::ConnectionSocket&amp; socket) const {
  // 本地地址（恢复到原始目的地址后的）
  const auto&amp; address = socket.localAddress();

  // 根据目的端口匹配
  if (address-&gt;type() == Network::Address::Type::Ip) {
    const auto port_match = destination_ports_map_.find(address-&gt;ip()-&gt;port());
    if (port_match != destination_ports_map_.end()) {
      return findFilterChainForDestinationIP(*port_match-&gt;second.second, socket);
    }
  }

  // 缺省匹配
  const auto port_match = destination_ports_map_.find(0);
  if (port_match != destination_ports_map_.end()) {
    return findFilterChainForDestinationIP(*port_match-&gt;second.second, socket);
  }

  return nullptr;
}</pre>
<div class="blog_h3"><span class="graybg">实例化过滤器链</span></div>
<p>在ActiveListener::newConnection期间，调用config_.filterChainFactory().createNetworkFilterChain()为连接实例化过滤器链：</p>
<pre class="crayon-plain-tag">bool ListenerImpl::createNetworkFilterChain(
    Network::Connection&amp; connection,
    const std::vector&lt;Network::FilterFactoryCb&gt;&amp; filter_factories) {
  return Configuration::FilterChainUtility::buildFilterChain(connection, filter_factories);
}

// 和构建监听器过滤器时的逻辑一样，遍历过滤器工厂，传入Connection调用之
bool FilterChainUtility::buildFilterChain(Network::FilterManager&amp; filter_manager,
                                          const std::vector&lt;Network::FilterFactoryCb&gt;&amp; factories) {
  for (const Network::FilterFactoryCb&amp; factory : factories) {
    factory(filter_manager);
  }
  // 初始化所有读过滤器，也就是调用每个过滤器的onNewConnection 
  return filter_manager.initializeReadFilters();
}</pre>
<div class="blog_h3"><span class="graybg">监听器新连接回调</span></div>
<p>当ActiveListener为新连接准备好过滤器链后，会调用自身的：</p>
<pre class="crayon-plain-tag">void ConnectionHandlerImpl::ActiveListener::onNewConnection( Network::ConnectionPtr&amp;&amp; new_connection) {
  ENVOY_CONN_LOG_TO_LOGGER(parent_.logger_, debug, "new connection", *new_connection);

  // 如果新连接的状态不是已经关闭
  if (new_connection-&gt;state() != Network::Connection::State::Closed) {
    ActiveConnectionPtr active_connection(
        new ActiveConnection(*this, std::move(new_connection), parent_.dispatcher_.timeSystem()));
    // 存放到当前ActiveListener的字段中，防止析构
    active_connection-&gt;moveIntoList(std::move(active_connection), connections_);
    // 将新连接加入到连接处理器中。注意C++ 11中++是原子操作
    parent_.num_connections_++;
  }
  // 否则，新连接将在此立即析构
}</pre>
<div class="blog_h2"><span class="graybg">处理HTTP连接</span></div>
<p>对于HTTP协议， 处理TCP连接的逻辑仍然使用。</p>
<div class="blog_h3"><span class="graybg">HTTP连接管理器</span></div>
<p>在实例化过滤器链时，HTTP连接会有一个过滤器 —— HTTP连接管理器（ConnectionManagerImpl），它的工厂如下：</p>
<pre class="crayon-plain-tag">Network::FilterFactoryCb HttpConnectionManagerFilterConfigFactory::createFilterFactoryFromProtoTyped(
    const envoy::config::filter::network::http_connection_manager::v2::HttpConnectionManager&amp; roto_config,
    Server::Configuration::FactoryContext&amp; context) {
  // 线程本地的一个缓存提供者，每500ms为当前线程更新日期信息
  std::shared_ptr&lt;Http::TlsCachingDateProviderImpl&gt; date_provider =
      context.singletonManager().getTyped&lt;Http::TlsCachingDateProviderImpl&gt;(
          SINGLETON_MANAGER_REGISTERED_NAME(date_provider), [&amp;context] {
            return std::make_shared&lt;Http::TlsCachingDateProviderImpl&gt;(context.dispatcher(),
                                                                      context.threadLocal());
          });
  // 此管理器维护RouteConfigProvider，后者提供路由信息
  std::shared_ptr&lt;Router::RouteConfigProviderManager&gt; route_config_provider_manager =
      context.singletonManager().getTyped&lt;Router::RouteConfigProviderManager&gt;(
          SINGLETON_MANAGER_REGISTERED_NAME(route_config_provider_manager), [&amp;context] {
            return std::make_shared&lt;Router::RouteConfigProviderManagerImpl&gt;(context.admin());
          });

  // 连接管理器的配置
  std::shared_ptr&lt;HttpConnectionManagerConfig&gt; filter_config(new HttpConnectionManagerConfig(
      proto_config, context, *date_provider, *route_config_provider_manager));

  // 此Lambda捕获了上面的共享指针，因此避免了引用计数清零
  // 此Lambda即HTTP连接管理器的L4过滤器工厂
  return [route_config_provider_manager, filter_config, &amp;context,
          date_provider](Network::FilterManager&amp; filter_manager) -&gt; void {
    // 为Connection添加一个读过滤器ConnectionManagerImpl
    filter_manager.addReadFilter(Network::ReadFilterSharedPtr{new Http::ConnectionManagerImpl(
        *filter_config, context.drainDecision(), context.random(), context.httpContext(),
        context.runtime(), context.localInfo(), context.clusterManager(),
        &amp;context.overloadManager(), context.dispatcher().timeSystem())});
  };
}</pre>
<p>ConnectionManagerImpl的构造函数如下：</p>
<pre class="crayon-plain-tag">ConnectionManagerImpl::ConnectionManagerImpl(ConnectionManagerConfig&amp; config,
                                             const Network::DrainDecision&amp; drain_close,
                                             Runtime::RandomGenerator&amp; random_generator,
                                             Http::Context&amp; http_context, Runtime::Loader&amp; runtime,
                                             const LocalInfo::LocalInfo&amp; local_info,
                                             Upstream::ClusterManager&amp; cluster_manager,
                                             Server::OverloadManager* overload_manager,
                                             Event::TimeSystem&amp; time_system)
    // ConnectionManagerConfig 连接管理器的配置
    //                 ConnectionManagerStats 统计指标
    : config_(config), stats_(config_.stats()),
    // 连接持续的时长
      conn_length_(new Stats::Timespan(stats_.named_.downstream_cx_length_ms_, time_system)),
    // Network::DrainDecision，给出连接是否应该被Drain并关闭
    //                           随机数生成器                           HTTP上下文，每个服务器一个，提供Tracer等信息
      drain_close_(drain_close), random_generator_(random_generator), http_context_(http_context),
   // 能从磁盘读取Envoy运行时快照   本地环境信息        集群管理器
      runtime_(runtime), local_info_(local_info), cluster_manager_(cluster_manager),
   // ConnectionManagerListenerStats 连接管理器监听器统计信息
      listener_stats_(config_.listenerStats()),
   // 过载管理，停止接受请求、禁止Keepalive
      overload_stop_accepting_requests_ref_(
          overload_manager ? overload_manager-&gt;getThreadLocalOverloadState().getState(
                                 Server::OverloadActionNames::get().StopAcceptingRequests)
                           : Server::OverloadManager::getInactiveState()),
      overload_disable_keepalive_ref_(
          overload_manager ? overload_manager-&gt;getThreadLocalOverloadState().getState(
                                 Server::OverloadActionNames::get().DisableHttpKeepAlive)
                           : Server::OverloadManager::getInactiveState()),
    // 授时和定时器管理
      time_system_(time_system) {}</pre>
<div class="blog_h2"><span class="graybg">注册读写回调</span></div>
<p>实际负责连接的ActiveListener，会调用自己的newConnection创建新Connection对象：</p>
<pre class="crayon-plain-tag">Network::ConnectionPtr new_connection =
    parent_.dispatcher_.createServerConnection(std::move(socket), std::move(transport_socket));</pre>
<p>可以看到，创建服务器端连接的职责委托给ConnectionHandle.Dispatcher对象，连接套接字、传输套接字的所有权都被转移：</p>
<pre class="crayon-plain-tag">Network::ConnectionPtr DispatcherImpl::createServerConnection(Network::ConnectionSocketPtr&amp;&amp; socket,
                                       Network::TransportSocketPtr&amp;&amp; transport_socket) {
  ASSERT(isThreadSafe());
  return std::make_unique&lt;Network::ConnectionImpl&gt;(*this, std::move(socket),
                                                   std::move(transport_socket), true);
}</pre>
<p>可以看到连接套接字、传输套接字的所有权继续转移，给ConnectionImpl的构造函数：</p>
<pre class="crayon-plain-tag">ConnectionImpl::ConnectionImpl(Event::Dispatcher&amp; dispatcher, ConnectionSocketPtr&amp;&amp; socket,
                               TransportSocketPtr&amp;&amp; transport_socket, bool connected)
      // 传输套接字                                     连接套接字
    : transport_socket_(std::move(transport_socket)), socket_(std::move(socket)),
      // 过滤器管理器                  流信息（日志用）
      filter_manager_(*this, *this), stream_info_(dispatcher.timeSystem()),
      // 创建写缓冲
      write_buffer_(
                                                                   // 高低水位回调
          dispatcher.getWatermarkFactory().create([this]() -&gt; void { this-&gt;onLowWatermark(); },
                                                  [this]() -&gt; void { this-&gt;onHighWatermark(); })),
      dispatcher_(dispatcher), id_(next_global_id_++) {
  // 如果连接套接字的fd不可用，认为发生OOM，让进程崩掉
  RELEASE_ASSERT(ioHandle().fd() != -1, "");
  // 设置为已连接
  if (!connected) {
    connecting_ = true;
  }

  // 边缘触发，注册读写事件监听器
  file_event_ = dispatcher_.createFileEvent(
      // 传输套接字的描述符
      ioHandle().fd(), [this](uint32_t events) -&gt; void { onFileEvent(events); },
      Event::FileTriggerType::Edge, Event::FileReadyType::Read | Event::FileReadyType::Write);
  // 注册传输套接字回调
  transport_socket_-&gt;setTransportSocketCallbacks(*this);
}</pre>
<p>可以看到，当读写事件到达时，libevent会回调ConnectionImpl::onFileEvent</p>
<div class="blog_h2"><span class="graybg">触发读写回调</span></div>
<p>当发生可读、可写事件时，ConnectionImpl::onFileEvent被调用：</p>
<pre class="crayon-plain-tag">void ConnectionImpl::onFileEvent(uint32_t events) {
  // 期望连接状态为Connected，否则认为是错误，需要关闭套接字
  if (immediate_error_event_ != ConnectionEvent::Connected) {
    if (bind_error_) {
      // 绑定失败
      if (connection_stats_ &amp;&amp; connection_stats_-&gt;bind_errors_) {
        connection_stats_-&gt;bind_errors_-&gt;inc();
      }
    } else {
      // 其它错误
      ENVOY_CONN_LOG(debug, "raising immediate error", *this);
    }
    // 关闭套接字并退出
    closeSocket(immediate_error_event_);
    return;
  }

  // 关闭事件
  if (events &amp; Event::FileReadyType::Closed) {
    // 过早关闭（early close）和读操作绝不需要同时发生
    ASSERT(!(events &amp; Event::FileReadyType::Read));
    // 关闭套接字
    ENVOY_CONN_LOG(debug, "remote early close", *this);
    closeSocket(ConnectionEvent::RemoteClose);
    return;
  }

  // 可写事件
  if (events &amp; Event::FileReadyType::Write) {
    onWriteReady();
  }

  // 可读事件，由于写事件回调可能会关闭套接字（导致fd为-1），因此做个判断
  if (ioHandle().fd() != -1 &amp;&amp; (events &amp; Event::FileReadyType::Read)) {
    onReadReady();
  }
}</pre>
<div class="blog_h2"><span class="graybg">处理TCP读</span></div>
<div class="blog_h3"><span class="graybg">整体流程</span></div>
<ol>
<li>尝试循环读取，根据读取结果设置Post操作</li>
<li>处理读取到的数据</li>
<li>执行后操作</li>
</ol>
<pre class="crayon-plain-tag">void ConnectionImpl::onReadReady() {
  ENVOY_CONN_LOG(trace, "read ready", *this);

  // 断言已经连接成功
  ASSERT(!connecting_);

  // 调用传输套接字执行循环的读操作，直到没有更多数据可读，或者出错
  IoResult result = transport_socket_-&gt;doRead(read_buffer_);
  // 实际读取的字节数
  uint64_t new_buffer_size = read_buffer_.length();
  // 更新指标
  updateReadBufferStats(result.bytes_processed_, new_buffer_size);

  // 如果到达流的终点（对端关闭），但是不支持半关闭语义
  // 则后操作设置为关闭
  if ((!enable_half_close_ &amp;&amp; result.end_stream_read_)) {
    result.end_stream_read_ = false;
    result.action_ = PostIoAction::Close;
  }

  // 如果到达流终点了，或者读取的字节数不为零
  read_end_stream_ |= result.end_stream_read_;
  if (result.bytes_processed_ != 0 || result.end_stream_read_) {
    // 处理读取的数据
    onRead(new_buffer_size);
  }

  // 如果后操作应当关闭连接，或者两边都进入半关闭状态（一方关闭发送通道后，仍可接受另一方发送过来的数据）
  if (result.action_ == PostIoAction::Close || bothSidesHalfClosed()) {
    // 则关闭套接字
    closeSocket(ConnectionEvent::RemoteClose);
  }
}</pre>
<div class="blog_h3"><span class="graybg">读取到缓冲区</span></div>
<p>传输套接字的真实类型取决于传输协议（transport_protocol），默认协议是raw_buffer，对应RawBufferSocket：</p>
<pre class="crayon-plain-tag">IoResult RawBufferSocket::doRead(Buffer::Instance&amp; buffer) {
  // IO操作之后应当执行的操作，枚举Close/KeepOpen
  PostIoAction action = PostIoAction::KeepOpen;
  uint64_t bytes_read = 0;
  bool end_stream = false;
  // 循环读取
  do {
    // 尝试读取最多16K，这个16K是随便取的值
    Api::SysCallIntResult result = buffer.read(callbacks_-&gt;ioHandle().fd(), 16384);
    ENVOY_CONN_LOG(trace, "read returns: {}", callbacks_-&gt;connection(), result.rc_);
    
    // 依据系统调用返回码决定进一步操作
    if (result.rc_ == 0) {
      // 对端关闭
      end_stream = true;
      break;
    } else if (result.rc_ == -1) {
      // 远程错误（可能是没有数据可读，读完了）
      ENVOY_CONN_LOG(trace, "read error: {}", callbacks_-&gt;connection(), result.errno_);
      if (result.errno_ != EAGAIN) {
        // 错误号不是11（Try again）后操作设置为关闭
        action = PostIoAction::Close;
      }
      break;
    } else {
      // 否则，返回码是实际读取的字节数
      bytes_read += result.rc_;
      // 如果缓冲区超过限制
      if (callbacks_-&gt;shouldDrainReadBuffer()) {
        callbacks_-&gt;setReadBufferReady();
        break;
      }
    }
  } while (true);

  return {action, bytes_read, end_stream};
}</pre>
<div class="blog_h3"><span class="graybg">处理缓冲区数据</span></div>
<p>循环读取了可用的数据到缓冲区后， ConnectionImpl会调用自己的onRead方法：</p>
<pre class="crayon-plain-tag">void ConnectionImpl::onRead(uint64_t read_buffer_size) {
  // 连接不可读则返回
  if (!read_enabled_) {
    return;
  }

  // 缓冲区为空，同时没有读取到流终点
  if (read_buffer_size == 0 &amp;&amp; !read_end_stream_) {
    return;
  }

  if (read_end_stream_) {
    // 针对原始套接字的read()调用，在EOF首次（可能是对端半关闭导致）发生后，总会返回0。这里要进行判断，以免重复处理
    if (read_end_stream_raised_) {
      ASSERT(read_buffer_size == 0);
      return;
    }
    // 防止重复处理
    read_end_stream_raised_ = true;
  }
  // 转交给过滤器管理器，过滤器管理器就是Connection本身
  filter_manager_.onRead();
}</pre>
<div class="blog_h3"><span class="graybg">遍历过滤器链</span></div>
<p>如果有数据需要处理，则调用过滤器管理器的onRead方法：</p>
<pre class="crayon-plain-tag">void FilterManagerImpl::onRead() {
  // 断言上行过滤器（读取下游发来的数据）不为空
  ASSERT(!upstream_filters_.empty());
  // 传入nullptr，则从过滤器链的头部开始遍历
  onContinueReading(nullptr);
}</pre>
<p>这里的过滤器链遍历逻辑，和监听器过滤器链遍历很类似：</p>
<pre class="crayon-plain-tag">void FilterManagerImpl::onContinueReading(ActiveReadFilter* filter) {
  // 如果不设置上一个迭代的过滤器，则从头开始，否则，从下一个开始
  std::list&lt;ActiveReadFilterPtr&gt;::iterator entry;
  if (!filter) {
    entry = upstream_filters_.begin();
  } else {
    entry = std::next(filter-&gt;entry());
  }
 
  // 遍历过滤器
  for (; entry != upstream_filters_.end(); entry++) {
    // 延迟初始化，如果过滤器尚未初始化，则调用其onNewConnection
    if (!(*entry)-&gt;initialized_) {
      (*entry)-&gt;initialized_ = true;
      FilterStatus status = (*entry)-&gt;filter_-&gt;onNewConnection();
      if (status == FilterStatus::StopIteration) {
        return;
      }
    }
    // 获取先前的读缓冲区
    BufferSource::StreamBuffer read_buffer = buffer_source_.getReadBuffer();
    // 不管是可读、还是EOF，都要调用过滤器
    if (read_buffer.buffer.length() &gt; 0 || read_buffer.end_stream) {
      FilterStatus status = (*entry)-&gt;filter_-&gt;onData(read_buffer.buffer, read_buffer.end_stream);
      if (status == FilterStatus::StopIteration) {
        // 任何一个过滤器都可以终止迭代
        return;
      }
    }
  }
}</pre>
<p>过滤器链上的过滤器会被依次的调用。</p>
<div class="blog_h2"><span class="graybg">处理HTTP下游请求读</span></div>
<div class="blog_h3"><span class="graybg">HTTP连接管理器 </span></div>
<p>对于L7连接，需要调用的网络过滤器通常就是ConnectionManagerImpl：</p>
<pre class="crayon-plain-tag">Network::FilterStatus ConnectionManagerImpl::onData(Buffer::Instance&amp; data, bool) {
  // 如果编解码器没有创建，则创建之
  if (!codec_) {
    // 编解码器的类型是ServerConnection
    codec_ = config_.createCodec(read_callbacks_-&gt;connection(), data, *this);
    // 更新监控指标
    if (codec_-&gt;protocol() == Protocol::Http2) {
      stats_.named_.downstream_cx_http2_total_.inc();
      stats_.named_.downstream_cx_http2_active_.inc();
    } else {
      stats_.named_.downstream_cx_http1_total_.inc();
      stats_.named_.downstream_cx_http1_active_.inc();
    }
  }

  bool redispatch;
  do {
    redispatch = false;

    try {
      // 尝试进行报文分发
      codec_-&gt;dispatch(data);
    } catch (const CodecProtocolException&amp; e) {
      // 分发出现错误
      // 执行到这里，HTTP/1.1编解码器已经发送400状态码，HTTP/2编解码器已经发送GOAWAY
      ENVOY_CONN_LOG(debug, "dispatch error: {}", read_callbacks_-&gt;connection(), e.what());
      stats_.named_.downstream_cx_protocol_error_.inc();

      // 当出现协议错误的情况下，连接上的所有流都需要重置
      resetAllStreams();
      // 刷出写缓冲区、延迟关闭
      read_callbacks_-&gt;connection().close(Network::ConnectionCloseType::FlushWriteAndDelay);
      return Network::FilterStatus::StopIteration;
    }

    // 处理入站数据可能会导致出站数据的释放，这里再次检查
    // 看此连接是否可以在未决编解码数据发送后优雅的关闭
    // 调用Network::ReadFilterCallbacks-&gt;connection().close(Network::ConnectionCloseType::FlushWriteAndDelay)
    checkForDeferredClose();

    // 对于HTTP/1编解码器来说，它会在单个消息完成后，暂停分发
    if (codec_-&gt;protocol() != Protocol::Http2) {
       // 如果没有额外流并且还有更多数据，执行重分发
      if (read_callbacks_-&gt;connection().state() == Network::Connection::State::Open &amp;&amp;
          data.length() &gt; 0 &amp;&amp; streams_.empty()) {
        redispatch = true;
      }
      // 如果仅有单个已经完成请求处理但未应答的非WebSockert流，则暂停套接字读，以apply back pressure
      if (!streams_.empty() &amp;&amp; streams_.front()-&gt;state_.remote_complete_) {
        read_callbacks_-&gt;connection().readDisable(true);
      }
    }
  } while (redispatch);

  return Network::FilterStatus::StopIteration;
}</pre>
<p>注意ConnectionManagerImpl总是会终止网络过滤器的迭代过程。</p>
<div class="blog_h3"><span class="graybg">创建编解码器</span></div>
<pre class="crayon-plain-tag">Http::ServerConnectionPtr
HttpConnectionManagerConfig::createCodec(Network::Connection&amp; connection,
                                         const Buffer::Instance&amp; data,
                                         Http::ServerConnectionCallbacks&amp; callbacks) {
  // 根据协议类型创建适当的HTTP编解码器
  switch (codec_type_) {
  case CodecType::HTTP1:
    return Http::ServerConnectionPtr{
        new Http::Http1::ServerConnectionImpl(connection, callbacks, http1_settings_)};
  case CodecType::HTTP2:
    return Http::ServerConnectionPtr{new Http::Http2::ServerConnectionImpl(
        connection, callbacks, context_.scope(), http2_settings_, maxRequestHeadersKb())};
  case CodecType::AUTO:
    return Http::ConnectionManagerUtility::autoCreateCodec(connection, data, callbacks,
                                                           context_.scope(), http1_settings_,
                                                           http2_settings_, maxRequestHeadersKb());
  }

  NOT_REACHED_GCOVR_EXCL_LINE;
}</pre>
<p>默认情况下，走的是CodecType::AUTO分支：</p>
<pre class="crayon-plain-tag">ServerConnectionPtr ConnectionManagerUtility::autoCreateCodec(
    Network::Connection&amp; connection, const Buffer::Instance&amp; data,
    ServerConnectionCallbacks&amp; callbacks, Stats::Scope&amp; scope, const Http1Settings&amp; http1_settings,
    const Http2Settings&amp; http2_settings, const uint32_t max_request_headers_kb) {
  // 基于协议探测+应用层协议协商（ALPN）来确定下一个L7协议
  // Http2::ALPN_STRING == "h2"，是HTTP/2在ALPN中的代号
  if (determineNextProtocol(connection, data) == Http2::ALPN_STRING) {
    // 使用HTTP/2协议
    return ServerConnectionPtr{new Http2::ServerConnectionImpl(
        connection, callbacks, scope, http2_settings, max_request_headers_kb)};
  } else {
    // 使用HTTP/1协议
    return ServerConnectionPtr{
        new Http1::ServerConnectionImpl(connection, callbacks, http1_settings)};
  }
}</pre>
<p>HTTP协议的版本不同，则ServerConnection的实际类型不同，对于HTTP/1来说，调用Http1::ServerConnectionImpl的构造函数：</p>
<pre class="crayon-plain-tag">ServerConnectionImpl::ServerConnectionImpl(Network::Connection&amp; connection,
                                           ServerConnectionCallbacks&amp; callbacks,
                                           Http1Settings settings)
    : ConnectionImpl(connection, HTTP_REQUEST), callbacks_(callbacks), codec_settings_(settings) {}</pre>
<p>这个函数没什么好说的，它的初始化列表中创建了ConnectionImpl，这是http::Connection的实现：</p>
<pre class="crayon-plain-tag">ConnectionImpl::ConnectionImpl(Network::Connection&amp; connection, http_parser_type type)
    // L2 Connection对象       支持水位的缓冲                    低水位回调
    : connection_(connection), output_buffer_([&amp;]() -&gt; void { this-&gt;onBelowLowWatermark(); },
    //                                                        高水位回调
                                              [&amp;]() -&gt; void { this-&gt;onAboveHighWatermark(); }) {
  // 设置水位，低水位为入参的1/2，高水位为入参
  output_buffer_.setWatermarks(connection.bufferLimit());
  // 初始化HTTP报文解析器
  http_parser_init(&amp;parser_, type);
  parser_.data = this;
}</pre>
<p>http_parser_init来自Node.js项目：</p>
<pre class="crayon-plain-tag">请求头void
http_parser_init (http_parser *parser, enum http_parser_type t)
{
  void *data = parser-&gt;data;
  memset(parser, 0, sizeof(*parser));
  parser-&gt;data = data;
  parser-&gt;type = t;
  parser-&gt;state = (t == HTTP_REQUEST ? s_start_req : (t == HTTP_RESPONSE ? s_start_res : s_start_req_or_res));
  parser-&gt;http_errno = HPE_OK;
}</pre>
<p>此解析器比较严格，如果你的应用程序的HTTP报文头不符合规范可能导致无法解析。 </p>
<div class="blog_h3"><span class="graybg">HTTP1数据分发</span></div>
<p>HTTP连接管理器会调用ServerConnection的dispatch方法进行数据分发，后者从http1::ConnectionImpl继承的dispatch逻辑如下：</p>
<pre class="crayon-plain-tag">void ConnectionImpl::dispatch(Buffer::Instance&amp; data) {
  ENVOY_CONN_LOG(trace, "parsing {} bytes", connection_, data.length());

  // 是否可以直接分发，仅对于Upgrade请求返回true
  if (maybeDirectDispatch(data)) {
    return;
  }

  // 总是尝试将解析器从暂停中恢复
  http_parser_pause(&amp;parser_, 0);

  ssize_t total_parsed = 0;
  if (data.length() &gt; 0) {
    // 获取原始缓冲切片，第一个参数是切片数组，第二个为数组大小，返回值是实际需要的切片数
    // 第一次调用就是为了获得实际需要的切片数
    uint64_t num_slices = data.getRawSlices(nullptr, 0);
    // #define STACK_ARRAY(var, type, num) StackArray&lt;type&gt; var(::alloca(sizeof(type) * num), num)
    // 在栈上创建数组变量slices
    STACK_ARRAY(slices, Buffer::RawSlice, num_slices);
    // 将带解析数据分到切片中
    data.getRawSlices(slices.begin(), num_slices);
    // 逐个处理切片
    for (const Buffer::RawSlice&amp; slice : slices) {
    //                              获取切片的裸数据，传递给HTTP解析器
      total_parsed += dispatchSlice(static_cast&lt;const char*&gt;(slice.mem_), slice.len_);
    }
  } else {
    dispatchSlice(nullptr, 0);
  }
  // 解析完毕，分发完毕，对应的Envoy解码也完毕
  ENVOY_CONN_LOG(trace, "parsed {} bytes", connection_, total_parsed);
  // 排干已经解析的数据
  data.drain(total_parsed);

  // 如果Upgrade请求已经被处理，并且存在：
  // 1、请求体数据
  // 2、或者early upgrade载荷
  // 需要发送，则发送之
  maybeDirectDispatch(data);
}</pre>
<p>从上面的代码我们看到，HTTP请求数据是划分为切片，逐个切片进行解析的：</p>
<pre class="crayon-plain-tag">// 切片内容示例
// GET /healthz HTTP/1.1\r\nUser-Agent: curl/7.35.0\r\nAccept: */*\r\nHost: podinfo-canary.default.svc.k8s.gmem.cc\r\n\r\n
size_t ConnectionImpl::dispatchSlice(const char* slice, size_t len) {
  ssize_t rc = http_parser_execute(&amp;parser_, &amp;settings_, slice, len);
  // 解析失败则抛出异常
  if (HTTP_PARSER_ERRNO(&amp;parser_) != HPE_OK &amp;&amp; HTTP_PARSER_ERRNO(&amp;parser_) != HPE_PAUSED) {
    sendProtocolError();
    throw CodecProtocolException("http/1.1 protocol error: " + std::string(http_errno_name(HTTP_PARSER_ERRNO(&amp;parser_))));
  }

  return rc;
}</pre>
<div class="blog_h3"><span class="graybg">HTTP1数据解析</span></div>
<p>注意：由于HTTP1不支持多路复用，因此请求解析结果信息都是以Http::Http1::ConnectionImpl的实例变量的形式存放的。</p>
<p>http_parser_execute的实现细节我们不去深究，这里主要关注一下settings_，其类型为：</p>
<pre class="crayon-plain-tag">struct http_parser_settings {
  // 在解析了HTTP报文的各个部分之后，执行对应的回调
  http_cb      on_message_begin;
  http_data_cb on_url;
  http_data_cb on_status;
  http_data_cb on_header_field;
  http_data_cb on_header_value;
  http_cb      on_headers_complete;
  http_data_cb on_body;
  http_cb      on_message_complete;
  // 调用on_chunk_header时当前chunk的长度存放在 parser-&gt;content_length
  http_cb      on_chunk_header;
  http_cb      on_chunk_complete;
};</pre>
<p>Envoy提供的settings_变量如下，注意ConnectionImpl对象调用了HTTP解析器，并且把自身传递给parser.data：</p>
<pre class="crayon-plain-tag">http_parser_settings ConnectionImpl::settings_{
    [](http_parser* parser) -&gt; int {
      static_cast&lt;ConnectionImpl*&gt;(parser-&gt;data)-&gt;onMessageBeginBase();
      return 0;
    },
    [](http_parser* parser, const char* at, size_t length) -&gt; int {
      static_cast&lt;ConnectionImpl*&gt;(parser-&gt;data)-&gt;onUrl(at, length);
      return 0;
    },
    nullptr, // on_status
    [](http_parser* parser, const char* at, size_t length) -&gt; int {
      static_cast&lt;ConnectionImpl*&gt;(parser-&gt;data)-&gt;onHeaderField(at, length);
      return 0;
    },
    [](http_parser* parser, const char* at, size_t length) -&gt; int {
      static_cast&lt;ConnectionImpl*&gt;(parser-&gt;data)-&gt;onHeaderValue(at, length);
      return 0;
    },
    [](http_parser* parser) -&gt; int {
      return static_cast&lt;ConnectionImpl*&gt;(parser-&gt;data)-&gt;onHeadersCompleteBase();
    },
    [](http_parser* parser, const char* at, size_t length) -&gt; int {
      static_cast&lt;ConnectionImpl*&gt;(parser-&gt;data)-&gt;onBody(at, length);
      return 0;
    },
    [](http_parser* parser) -&gt; int {
      static_cast&lt;ConnectionImpl*&gt;(parser-&gt;data)-&gt;onMessageCompleteBase();
      return 0;
    },
    nullptr, // on_chunk_header
    nullptr  // on_chunk_complete
};</pre>
<p>最初被回调的是onMessageBeginBase方法，表示开始解析HTTP报文了：</p>
<pre class="crayon-plain-tag">void ConnectionImpl::onMessageBeginBase() {
  ENVOY_CONN_LOG(trace, "message begin", connection_);
  ASSERT(!current_header_map_);
  // HeaderMapImpl是为性能高度优化的Http::HeaderMap实现，尽量避免拷贝和内存分配
  current_header_map_ = std::make_unique&lt;HeaderMapImpl&gt;();
  // 解析状态，Field / Value / Done
  header_parsing_state_ = HeaderParsingState::Field;
  onMessageBegin();
}

void ServerConnectionImpl::onMessageBegin() {
  if (!resetStreamCalled()) {
    // 如果没有进行流重置，则初始化当前ActiveRequest对象
    ASSERT(!active_request_);
    active_request_ = std::make_unique&lt;ActiveRequest&gt;(*this);
    //               StreamDecoder                                        ResponseStreamEncoderImpl
    active_request_-&gt;request_decoder_ = &amp;callbacks_.newStream(active_request_-&gt;response_encoder_);
  }
}</pre>
<p>解析出URL路径后，回调：</p>
<pre class="crayon-plain-tag">void ServerConnectionImpl::onUrl(const char* data, size_t length) {
  if (active_request_) {
    active_request_-&gt;request_url_.append(data, length);
  }
}</pre>
<p>为请求设置请求URL的路径部分，例如 /healthz。</p>
<p>解析完每个请求头后，依次回调onHeaderField、onHeaderValue方法：</p>
<pre class="crayon-plain-tag">void ConnectionImpl::onHeaderField(const char* data, size_t length) {
  if (header_parsing_state_ == HeaderParsingState::Done) {
    // 忽略 trailers
    return;
  }
  // 解析请求值后，下面的判断为true
  if (header_parsing_state_ == HeaderParsingState::Value) {
    // 完成上一个请求头的处理
    completeLastHeader();
  }

  // 暂存到一个缓冲区中
  current_header_field_.append(data, length);
}

void ConnectionImpl::onHeaderValue(const char* data, size_t length) {
  if (header_parsing_state_ == HeaderParsingState::Done) {
    // 忽略 trailers
    return;
  }
  // 设置头解析状态
  header_parsing_state_ = HeaderParsingState::Value;
  // 暂存到一个缓冲区中
  current_header_value_.append(data, length);
} </pre>
<p>在解析完最后一个请求头后会执行：</p>
<pre class="crayon-plain-tag">void ConnectionImpl::completeLastHeader() {
  if (!current_header_field_.empty()) {
    toLowerTable().toLowerCase(current_header_field_.buffer(), current_header_field_.size());
    // 存储到请求头映射中
    current_header_map_-&gt;addViaMove(std::move(current_header_field_),
                                    std::move(current_header_value_));
  }
  // 设置头解析状态
  header_parsing_state_ = HeaderParsingState::Field;
  // 由于std::move的移动语义 HeaderString 变成一个空壳子
  ASSERT(current_header_field_.empty());
  ASSERT(current_header_value_.empty());
}</pre>
<p>完成全部请求头的处理后，回调onHeadersCompleteBase：</p>
<pre class="crayon-plain-tag">int ConnectionImpl::onHeadersCompleteBase() {
  // 将最后一个请求头加入映射
  completeLastHeader();
  if (!(parser_.http_major == 1 &amp;&amp; parser_.http_minor == 1)) {
    // 如果不是HTTP/1.1，则设置协议
    protocol_ = Protocol::Http10;
  }
  if (Utility::isUpgrade(*current_header_map_)) {
    // 根据请求头判定是否客户端在请求升级协议
    handling_upgrade_ = true;
  }

  // 移动请求头映射
  int rc = onHeadersComplete(std::move(current_header_map_));
  current_header_map_.reset();
  // 设置请求头解析状态
  header_parsing_state_ = HeaderParsingState::Done;

  // 返回2，提示http_parser不去期望请求体和更多的信息
  return handling_upgrade_ ? 2 : rc;
}


int ServerConnectionImpl::onHeadersComplete(HeaderMapImplPtr&amp;&amp; headers) {
  // 需要处理响应比请求完成发生更早的情况，这种情况可能由上层代码导致
  if (active_request_) {
    const char* method_string = http_method_str(static_cast&lt;http_method&gt;(parser_.method));

    // 如果请求使用HEAD方法，则给与响应编码器以提示，便于它正确设置内容长度、传输编码等头字段
    active_request_-&gt;response_encoder_.isResponseToHeadRequest(parser_.method == HTTP_HEAD);

    // 当前CONNECT方法是不支持的，但是http_parser_parse_url需要知晓CONNECT
    handlePath(*headers, parser_.method);
    ASSERT(active_request_-&gt;request_url_.empty());
    // 添加Method头
    headers-&gt;insertMethod().value(method_string, strlen(method_string));

    // 判断请求体是否存在，这里使用新的RFC语义来判断 ——  content-length头、hunked transfer-encoding头存在
    // 意味着请求体存在 —— 而不是基于HTTP方法判断
    // 如果没有请求体，延迟对StreamDecoder.decodeHeaders()的调用，直到http解析器flush（回调onMessageComplete）
    if (parser_.flags &amp; F_CHUNKED || (parser_.content_length &gt; 0 &amp;&amp; parser_.content_length != ULLONG_MAX) || handling_upgrade_) {
      // 没有请求体，立即解码请求头
      active_request_-&gt;request_decoder_-&gt;decodeHeaders(std::move(headers), false);

      // If the connection has been closed (or is closing) after decoding headers, pause the parser
      // so we return control to the caller.
      if (connection_.state() != Network::Connection::State::Open) {
        http_parser_pause(&amp;parser_, 1);
      }

    } else {
      // 移动以便延迟解码请求头
      deferred_end_stream_headers_ = std::move(headers);
    }
  }

  return 0;
}</pre>
<p>完成整个请求处理后，回调onMessageCompleteBase： </p>
<pre class="crayon-plain-tag">void ConnectionImpl::onMessageCompleteBase() {
  if (handling_upgrade_) {
    // 如果当前是Upgrade请求则不调用onMessageComplete
    // Upgrade载荷将作为流的体看待
    ASSERT(!deferred_end_stream_headers_);
    // 暂停解析
    http_parser_pause(&amp;parser_, 1);
    return;
  }
  onMessageComplete();
}

void ServerConnectionImpl::onMessageComplete() {
  if (active_request_) {
    Buffer::OwnedImpl buffer;
    // 提示请求端消息处理完毕
    active_request_-&gt;remote_complete_ = true;

    // 如果延迟了请求头解码，这里进行解码
    if (deferred_end_stream_headers_) {
      active_request_-&gt;request_decoder_-&gt;decodeHeaders(std::move(deferred_end_stream_headers_),
                                                       true);
      deferred_end_stream_headers_.reset();
    } else {
    // 否则，解码数据
      active_request_-&gt;request_decoder_-&gt;decodeData(buffer, true);
    }
  }

  // 总是暂停HTTP解析器，这样调用者同时只能处理单个请求，从而施加反向压力（apply back pressure）
  // 调用者需要检测缓冲中有更多的数据，并进行再次分发
  http_parser_pause(&amp;parser_, 1);
}</pre>
<div class="blog_h3"><span class="graybg">HTTP请求头解码 </span></div>
<p>经过上一节的分析，我们了解到，当HTTP解析器处理完请求后，会调用ServerConnectionImpl::onMessageComplete回调，该回调则会调用ActiveStream（实现了StreamDecoder）进行请求解码。</p>
<p>这个请求解码是Envoy上下文的，它会执行Envoy的核心代理逻辑 —— 遍历HTTP过滤器链、进行路由选择：</p>
<pre class="crayon-plain-tag">// 该函数的逻辑顺序很复杂，但也很重要
//
// 我们希望在选路之前做尽量少的工作，并且创建一个过滤器链来最大化需要定制过滤器行为—— 例如注册访问日志器 ——的请求数量
// 要达成目标，需要在以下几个事项之间进行权衡：
// 1、对无效请求进行合法性检查，因为无效请求可能因为没有完整的头信息而无法选路
// 2、检查服务器错误响应（连接关闭、HEAD请求...）所需要的状态
// 3、过滤器对请求本身的、可能影响选路的修改
//
void ConnectionManagerImpl::ActiveStream::decodeHeaders(HeaderMapPtr&amp;&amp; headers, bool end_stream) {
  // 移动请求头为ActiveStream的字段
  request_headers_ = std::move(headers);
  if (Http::Headers::get().MethodValues.Head == request_headers_-&gt;Method()-&gt;value().c_str()) {
    // 判断是否HEAD请求
    is_head_request_ = true;
  }
  ENVOY_STREAM_LOG(debug, "request headers complete (end_stream={}):\n{}", *this, end_stream, *request_headers_);

  // 如果请求仅有请求头（header-only，没有体），则在此可以结束解码流程
  // 如果我们将请求转换为header-only，则一旦后续的decodeData/decodeTrailers被调用则当前流就被标记为完成
  // 下面的方法设置remote_complete_ = end_stream
  maybeEndDecode(end_stream);

  // 如果过载了，只要解码了请求头，就丢弃请求
  // 连接管理器是为当前L4连接服务的，它是一个网络过滤器。当出现过载后，其overload_stop_accepting_requests_ref_ == Active
  if (connection_manager_.overload_stop_accepting_requests_ref_ == Server::OverloadActionState::Active) {
    // 在此特殊分支下，不去创建过滤器链 —— 如果存在内存过载风险更重要的是避免内存分配，而非创建过滤器
    // 标记为过滤器已创建
    state_.created_filter_chain_ = true;
    connection_manager_.stats_.named_.downstream_rq_overload_close_.inc();
    // 由Envoy直接应答下游
    sendLocalReply(Grpc::Common::hasGrpcContentType(*request_headers_),
                   Http::Code::ServiceUnavailable, "envoy overloaded", nullptr, is_head_request_,
                   absl::nullopt);
    return;
  }
  //                               是否Envoy需要代理Expect: 100-Continue
  if (!connection_manager_.config_.proxy100Continue() &amp;&amp; request_headers_-&gt;Expect() &amp;&amp;
      request_headers_-&gt;Expect()-&gt;value() == Headers::get().ExpectValues._100Continue.c_str()) {
    // 执行到这里意味着Envoy在处理100-Continue，跳过过滤器链，直接发送100-Continue给编码器
    // 100-Continue用于客户端在发送POST数据给服务器前，征询服务器情况，看服务器是否处理POST的数据，
    // 如果不处理，客户端则不上传POST数据，如果处理，则POST上传数据。在现实应用中，通常在POST大数据时，
    // 才会使用100-continue协议
    // 服务器端的行为应该是：返回100-Continue表示自己能够处理POST数据，或者错误码

    // 执行一些统计指标收集
    chargeStats(continueHeader());

    // 执行响应编码
    response_encoder_-&gt;encode100ContinueHeaders(continueHeader());
    // 移除Expect头，防止在上游再次处理
    request_headers_-&gt;removeExpect();
  }

  // 从请求头中读取UserAgent —— 针对特定user agent的统计指标
  connection_manager_.user_agent_.initializeFromHeaders(
      *request_headers_, connection_manager_.stats_.prefix_, connection_manager_.stats_.scope_);

  // 确保codec版本（HTTP协议版本）是支持的
  Protocol protocol = connection_manager_.codec_-&gt;protocol();
  if (protocol == Protocol::Http10) {
    // 这种情况下，HTTP/1.x中除了1.1都可以
    stream_info_.protocol(protocol);
    if (!connection_manager_.config_.http1Settings().accept_http_10_) {
      // 如果配置中没有显式支持HTTP/1.0，发送Envoy本地响应Upgrade Required
      sendLocalReply(false, Code::UpgradeRequired, "", nullptr, is_head_request_, absl::nullopt);
      return;
    } else {
      // HTTP/1.0 默认不支持连接复用，除非请求头指定Keep-Alive，需要保证连接关闭
      state_.saw_connection_close_ = true;
      if (request_headers_-&gt;Connection() &amp;&amp;
          absl::EqualsIgnoreCase(request_headers_-&gt;Connection()-&gt;value().getStringView(),
                                 Http::Headers::get().ConnectionValues.KeepAlive)) {
        state_.saw_connection_close_ = false;
      }
    }
  }
  // 如果缺少Host头
  if (!request_headers_-&gt;Host()) {
    if ((protocol == Protocol::Http10) &amp;&amp; !connection_manager_.config_.http1Settings().default_host_for_http_10_.empty()) {
      // 当前是HTTP10且配置了缺省Host头，则设置此头
      request_headers_-&gt;insertHost().value(connection_manager_.config_.http1Settings().default_host_for_http_10_);
    } else {
      // 非HTTP10，必须有Host头，对于HTTP11来说Host头重命名为:authority
      // Envoy本地应答
      sendLocalReply(Grpc::Common::hasGrpcContentType(*request_headers_), Code::BadRequest, "", nullptr, is_head_request_, absl::nullopt);
      return;
    }
  }
  
  // 处理请求头部过长的情况
  ASSERT(connection_manager_.config_.maxRequestHeadersKb() &gt; 0);
  if (request_headers_-&gt;byteSize() &gt; (connection_manager_.config_.maxRequestHeadersKb() * 1024)) {
    sendLocalReply(Grpc::Common::hasGrpcContentType(*request_headers_),
                   Code::RequestHeaderFieldsTooLarge, "", nullptr, is_head_request_, absl::nullopt);
    return;
  }

  // 当前在应用层，我们仅仅支持相对路径。在这里预期codec已经把路径打散成片
  // 注意：目前HTTP11 codec仅在allow_absolute_url标记启用的情况下才进行打散操作
  //  我们也会检查:path头，因为CONNECT请求没有URL路径，而当前不支持CONNECT请求
  if (!request_headers_-&gt;Path() || request_headers_-&gt;Path()-&gt;value().c_str()[0] != '/') {
    connection_manager_.stats_.named_.downstream_rq_non_relative_path_.inc();
    sendLocalReply(Grpc::Common::hasGrpcContentType(*request_headers_), Code::NotFound, "", nullptr,
                   is_head_request_, absl::nullopt);
    return;
  }

  // 对于HTTP11，如果请求头Connection: Close，表示不启用keep-Alive
  if (protocol == Protocol::Http11 &amp;&amp; request_headers_-&gt;Connection() &amp;&amp;
      absl::EqualsIgnoreCase(request_headers_-&gt;Connection()-&gt;value().getStringView(),
                             Http::Headers::get().ConnectionValues.Close)) {
    // 那么意味着客户端已经关闭连接
    state_.saw_connection_close_ = true;
  }

  // 如果当前请求不是内部重定向
  if (!state_.is_internally_created_) { // Only sanitize headers on first pass.
    // 根据配置、请求头来修改下游连接的远程地址（客户端地址）
    // 日志目的
    stream_info_.setDownstreamRemoteAddress(ConnectionManagerUtility::mutateRequestHeaders(
        *request_headers_, connection_manager_.read_callbacks_-&gt;connection(),
        connection_manager_.config_, *snapped_route_config_, connection_manager_.random_generator_,
        connection_manager_.runtime_, connection_manager_.local_info_));
  }
  ASSERT(stream_info_.downstreamRemoteAddress() != nullptr);

  ASSERT(!cached_route_);
  // 刷新缓存的路由（条目），可能设置cached_cluster_info_ —— 目标上游集群信息，意味着选路可能完成
  refreshCachedRoute();
  const bool upgrade_rejected = createFilterChain() == false;

  // TODO 如果在准备过滤器迭代时，发现链中没有任何过滤器，连接管理器应该返回404，当前实现时不返回响应
  if (protocol == Protocol::Http11 &amp;&amp; cached_route_.value()) {
    if (upgrade_rejected) {
      // 当前路由不支持升级，因此发送Envoy本地响应
      connection_manager_.stats_.named_.downstream_rq_ws_on_non_ws_route_.inc();
      sendLocalReply(Grpc::Common::hasGrpcContentType(*request_headers_), Code::Forbidden, "",
                     nullptr, is_head_request_, absl::nullopt);
      return;
    }
    // 允许WebSocket请求穿过启用了WebSocket支持的路由
  }

  // 如果有路由，且路由配置了idle超时
  if (cached_route_.value()) {
    const Router::RouteEntry* route_entry = cached_route_.value()-&gt;routeEntry();
    if (route_entry != nullptr &amp;&amp; route_entry-&gt;idleTimeout()) {
      idle_timeout_ms_ = route_entry-&gt;idleTimeout().value();
      if (idle_timeout_ms_.count()) {
        // 如果流超时定时器没创建，则创建之
        if (stream_idle_timer_ == nullptr) {
          stream_idle_timer_ =
              connection_manager_.read_callbacks_-&gt;connection().dispatcher().createTimer(
                  [this]() -&gt; void { onIdleTimeout(); });
        }
      } else if (stream_idle_timer_ != nullptr) {
        // 如果存在流超时定时器，但是路由的idle超时为0，则禁用定时器
        stream_idle_timer_ = nullptr;
      }
    }
  }

  // 进行请求追踪
  if (connection_manager_.config_.tracingConfig()) {
    traceRequest();
  }

  // 解码请求头
  decodeHeaders(nullptr, *request_headers_, end_stream);

  // 重置超时定时器
  resetIdleTimer();
}</pre>
<p>请求头的解码逻辑位于decodeHeaders方法中，上面的方法传入的第一个参数是nullptr，表示从头开始迭代过滤器链：</p>
<pre class="crayon-plain-tag">void ConnectionManagerImpl::ActiveStream::decodeHeaders(ActiveStreamDecoderFilter* filter,
                                                        HeaderMap&amp; headers, bool end_stream) {
  // 从头，或者从指定过滤器开始迭代
  std::list&lt;ActiveStreamDecoderFilterPtr&gt;::iterator entry;
  std::list&lt;ActiveStreamDecoderFilterPtr&gt;::iterator continue_data_entry = decoder_filters_.end();
  if (!filter) {
    entry = decoder_filters_.begin();
  } else {
    entry = std::next(filter-&gt;entry());
  }

  // 遍历剩下的过滤器
  for (; entry != decoder_filters_.end(); entry++) {
    ASSERT(!(state_.filter_call_state_ &amp; FilterCallState::DecodeHeaders));
    // 设置状态位
    state_.filter_call_state_ |= FilterCallState::DecodeHeaders;
    (*entry)-&gt;end_stream_ =
    // 仅仅解码请求头，或者传入end_stream=true（表示这是header-only的请求）
        decoding_headers_only_ || (end_stream &amp;&amp; continue_data_entry == decoder_filters_.end());
    // 调用过滤器来解码请求头，返回的状态决定后续流程走向
    FilterHeadersStatus status = (*entry)-&gt;decodeHeaders(headers, (*entry)-&gt;end_stream_);
    // ContinueAndEndStream表示继续迭代后续过滤器，但是忽略后续的请求体、尾 —— 这意味着创建header-only请求/应答
    ASSERT(!(status == FilterHeadersStatus::ContinueAndEndStream &amp;&amp; (*entry)-&gt;end_stream_));
    // 清空状态位
    state_.filter_call_state_ &amp;= ~FilterCallState::DecodeHeaders;
    ENVOY_STREAM_LOG(trace, "decode headers called: filter={} status={}", *this,
                     static_cast&lt;const void*&gt;((*entry).get()), static_cast&lt;uint64_t&gt;(status));

    // 处理请求头的回调被调用后的通用处理逻辑：
    // 根据status设置ActiveStream的一些字段，例如stopped_、headers_only、headers_continued_
    // 只有返回true，才可能继续迭代
    if (!(*entry)-&gt;commonHandleAfterHeadersCallback(status, decoding_headers_only_) &amp;&amp;
        std::next(entry) != decoder_filters_.end()) {
      // 如果当前不是最后一个过滤器，停止迭代。否则，还需要继续处理先前过滤器添加了体的情况
      return;
    }

    // 这里处理特殊的情况：我们使用header-only请求，但是前面的过滤器填充了请求体
    // 这意味着不能在内联迭代（inline iteration）阶段再向后面的过滤器传递end_stream = true了
    if (end_stream &amp;&amp; buffered_request_data_ &amp;&amp; continue_data_entry == decoder_filters_.end()) {
      // 设置下一个执行的过滤器（为当前过滤器）
      continue_data_entry = entry;
    }
  }

  if (continue_data_entry != decoder_filters_.end()) {
    // 从当前过滤器继续迭代，调用continueDecoding()以防再调用decodeHeaders()
    ASSERT(buffered_request_data_);
    // 仿冒stopped_ = true，因为continueDecoding() 期望之
    (*continue_data_entry)-&gt;stopped_ = true;
    // 使用缓冲的请求头、体数据继续迭代
    (*continue_data_entry)-&gt;continueDecoding();
  }

  if (end_stream) {
    // 解除超时过滤器
    disarmRequestTimeout();
  }
}</pre>
<p>单个过滤器解码请求头的逻辑由ActiveStreamDecoderFilter.decodeHeaders提供：</p>
<pre class="crayon-plain-tag">FilterHeadersStatus decodeHeaders(HeaderMap&amp; headers, bool end_stream) {
  is_grpc_request_ = Grpc::Common::hasGrpcContentType(headers);
  return handle_-&gt;decodeHeaders(headers, end_stream);
}</pre>
<p>可以看到，它只是判断一下是否gRPC请求，然后就转交给 StreamDecoderFilter handle_，这个handle是一个个具体的HTTP过滤器。</p>
<p>HTTP过滤器可能对请求头进行任意的操作，例如修改某个头，最终它会返回下面的枚举值之一：</p>
<pre class="crayon-plain-tag">enum class FilterHeadersStatus {
  // 继续迭代下一个过滤器
  Continue,
  // 不再迭代后续过滤器
  StopIteration,
  // 继续迭代下一个过滤器，但是不忽略报文体、尾，也就是创建header-only的请求/响应
  ContinueAndEndStream
};</pre>
<p>返回值会影响如何进行后续的过滤器迭代。</p>
<div class="blog_h3"><span class="graybg">创建过滤器链</span></div>
<p>此方法考虑了协议升级的情况：</p>
<pre class="crayon-plain-tag">bool ConnectionManagerImpl::ActiveStream::createFilterChain() {
  // 过滤器链已经创建则返回，HTTP过滤器链只有一个（相对于单个HTTP连接管理器），而不像网络过滤器，可以有多个
  if (state_.created_filter_chain_) {
    return false;
  }
  bool upgrade_rejected = false;
  // 升级的目标协议
  auto upgrade = request_headers_ ? request_headers_-&gt;Upgrade() : nullptr;
  // 标记为过滤器已创建
  state_.created_filter_chain_ = true;
  if (upgrade != nullptr) {
    // 需要进行协议升级判断
    const Router::RouteEntry::UpgradeMap* upgrade_map = nullptr;
    // 设置UpgradeMap，包含路由条目支持的升级协议信息
    if (cached_route_.has_value() &amp;&amp; cached_route_.value() &amp;&amp; cached_route_.value()-&gt;routeEntry()) {
      upgrade_map = &amp;cached_route_.value()-&gt;routeEntry()-&gt;upgradeMap();
    }
    // 创建升级的过滤器链
    if (connection_manager_.config_.filterFactory().createUpgradeFilterChain( upgrade-&gt;value().c_str(), upgrade_map, *this)) {
      state_.successful_upgrade_ = true;
      connection_manager_.stats_.named_.downstream_cx_upgrades_total_.inc();
      connection_manager_.stats_.named_.downstream_cx_upgrades_active_.inc();
      return true;
    } else {
      upgrade_rejected = true;
      // 失败，退化为默认过滤器链，调用者将会发送Envoy本地响应提示升级失败
    }
  }
  // 创建默认过滤器链
  connection_manager_.config_.filterFactory().createFilterChain(*this);
  return !upgrade_rejected;
}</pre>
<p>默认过滤器链在下面的方法中创建：</p>
<pre class="crayon-plain-tag">void HttpConnectionManagerConfig::createFilterChain(Http::FilterChainFactoryCallbacks&amp; callbacks) {
  for (const Http::FilterFactoryCb&amp; factory : filter_factories_) {
    factory(callbacks);
  }
}</pre>
<p>可以看到，和网络过滤器一样的模式，调用各过滤器提供的工厂，传输FilterChainFactoryCallbacks。 </p>
<div class="blog_h2"><span class="graybg">和HTTP上游集群交互 </span></div>
<div class="blog_h3"><span class="graybg">HTTP路由处理</span></div>
<p>最后一个HTTP过滤器通常都是Envoy::Router::Filter，此过滤器决定如何转发下游请求给上游集群。毕竟Envoy只是个代理，它不负责实质性的请求处理。 </p>
<pre class="crayon-plain-tag">Http::FilterHeadersStatus Filter::decodeHeaders(Http::HeaderMap&amp; headers, bool end_stream) {
  // 确保向上游发送的出站请求需要的HTTP/2头都存在
  ASSERT(headers.Path());
  ASSERT(headers.Method());
  ASSERT(headers.Host());

  // 来自下游的头
  downstream_headers_ = &amp;headers;

  // 是否为gRPC请求
  grpc_request_ = Grpc::Common::hasGrpcContentType(headers);

  // 增加rq_total计数
  config_.stats_.rq_total_.inc();

  // 获取路由
  route_ = callbacks_-&gt;route();
  if (!route_) {
    // 增加no_route计数
    config_.stats_.no_route_.inc();
    ENVOY_STREAM_LOG(debug, "no cluster match for URL '{}'", *callbacks_, headers.Path()-&gt;value().c_str());
    // 记录没有路由这一情况到StreamInfo
    callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::NoRouteFound);
    // 设置本地响应
    callbacks_-&gt;sendLocalReply(Http::Code::NotFound, "", nullptr, absl::nullopt);
    return Http::FilterHeadersStatus::StopIteration;
  }

  // 如果有请求的直接响应，则返回直接响应，否则返回nullptr
  // 直接响应即Envoy自己生成的响应，而非代理上游集群的
  const auto* direct_response = route_-&gt;directResponseEntry();
  if (direct_response != nullptr) {
    config_.stats_.rq_direct_response_.inc();
    // 重写Path头
    direct_response-&gt;rewritePathHeader(headers, !config_.suppress_envoy_headers_);
    // 发送本地响应
    callbacks_-&gt;sendLocalReply(
        // 使用直接响应的头、体
        direct_response-&gt;responseCode(), direct_response-&gt;responseBody(),
        // 修改响应头的Lambda
        [this, direct_response,
         &amp;request_headers = headers](Http::HeaderMap&amp; response_headers) -&gt; void {
          // 基于请求头得到重定向路径
          const auto new_path = direct_response-&gt;newPath(request_headers);
          if (!new_path.empty()) {
            // 添加头
            response_headers.addReferenceKey(Http::Headers::get().Location, new_path);
          }
          // 在转发之前，进行可能是破坏性的响应头转换，例如添加/删除头
          // 只能在获取所有初始响应头后调用单次
          direct_response-&gt;finalizeResponseHeaders(response_headers, callbacks_-&gt;streamInfo());
        },
        absl::nullopt);
    // Router过滤器总是停止迭代
    return Http::FilterHeadersStatus::StopIteration;
  }

  // 匹配请求的路由条目
  route_entry_ = route_-&gt;routeEntry();
  // 从集群管理器cm_中后去路由条目提供的山有集群名称，例如 outbound|9898||podinfo-canary.default.svc.k8s.gmem.cc
  Upstream::ThreadLocalCluster* cluster = config_.cm_.get(route_entry_-&gt;clusterName());
  if (!cluster) {
    // 找不到集群
    config_.stats_.no_cluster_.inc();
    ENVOY_STREAM_LOG(debug, "unknown cluster '{}'", *callbacks_, route_entry_-&gt;clusterName());
    // 记录错误并进行本地应答
    callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::NoRouteFound);
    callbacks_-&gt;sendLocalReply(route_entry_-&gt;clusterNotFoundResponseCode(), "", nullptr, absl::nullopt);
    return Http::FilterHeadersStatus::StopIteration;
  }
  // 从线程本地的cluster获得ClusterInfo对象，此对象可以安全的超越ThreadLocalCluster的生命周期存在
  cluster_ = cluster-&gt;info();

  // 虚拟上游集群，根据请求路径确定
  request_vcluster_ = route_entry_-&gt;virtualCluster(headers);
  ENVOY_STREAM_LOG(debug, "cluster '{}' match for URL '{}'", *callbacks_, route_entry_-&gt;clusterName(), headers.Path()-&gt;value().c_str());

  // 上游集群的统计指标的备选前缀
  const Http::HeaderEntry* request_alt_name = headers.EnvoyUpstreamAltStatName();
  if (request_alt_name) {
    alt_stat_prefix_ = std::string(request_alt_name-&gt;value().c_str()) + ".";
    headers.removeEnvoyUpstreamAltStatName();
  }

  // 看看是不是应该立即杀死一定比例的、此集群的流量
  // maintenanceMode()返回集群是否出于维护模式，出于此模式则不应该作为路由的目标，过滤器
  // 可以根据自己的需要来处理此调用的返回值。此方法的实现可能引入某种随机性，不会每次返回一致的值
  if (cluster_-&gt;maintenanceMode()) {
    // 上游服务器资源溢出，流需要被重置
    callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::UpstreamOverflow);
    chargeUpstreamCode(Http::Code::ServiceUnavailable, nullptr, true);
    // 进行本地应答
    callbacks_-&gt;sendLocalReply(Http::Code::ServiceUnavailable, "maintenance mode",
                               [this](Http::HeaderMap&amp; headers) {
                                 if (!config_.suppress_envoy_headers_) {
                                   // 添加Envoy特殊响应头
                                   headers.insertEnvoyOverloaded().value( Http::Headers::get().EnvoyOverloadedValues.True);
                                 }
                               },
                               absl::nullopt);
    cluster_-&gt;stats().upstream_rq_maintenance_mode_.inc();
    return Http::FilterHeadersStatus::StopIteration;
  }

  // 获取上游集群的连接池
  Http::ConnectionPool::Instance* conn_pool = getConnPool();
  if (!conn_pool) {
    // 如果无法得到/创建线程池，所有该集群没有任何可用（健康的）端点
    // 发送本地响应
    sendNoHealthyUpstreamResponse();
    return Http::FilterHeadersStatus::StopIteration;
  }

  /* 开始向上游集群的主机发送请求 */

  // 根据路由配置和请求头来决定实际使用的请求超时时间。请求头中的超时优先级更高
  timeout_ = FilterUtility::finalTimeout(*route_entry_, headers, !config_.suppress_envoy_headers_, grpc_request_);

  // 如果请求头x-envoy-upstream-rq-timeout-alt-response存在，则在请求上游超时后
  if (headers.EnvoyUpstreamRequestTimeoutAltResponse()) {
    // 设置响应码
    timeout_response_code_ = Http::Code::NoContent;
    // 同时移除x-envoy-upstream-rq-timeout-alt-response头
    headers.removeEnvoyUpstreamRequestTimeoutAltResponse();
  }

  // 如果此RouteEntry所属的虚拟主机的配置要求在上游请求中添加x-envoy-attempt-count头，则添加之
  include_attempt_count_ = route_entry_-&gt;includeAttemptCount();
  if (include_attempt_count_) {
    headers.insertEnvoyAttemptCount().value(attempt_count_);
  }

  // 将当前Span的追踪上下文注入到请求头
  callbacks_-&gt;activeSpan().injectContext(headers);
  // 在转发请求前，进行可能是销毁性的请求头转换 —— 例如URL重写、添加额外的头、删除头
  // 此方法必须仅在转发前调用单次
  route_entry_-&gt;finalizeRequestHeaders(headers, callbacks_-&gt;streamInfo(), !config_.suppress_envoy_headers_);

  // 设置Scheme头，HTTP和HTTPS
  FilterUtility::setUpstreamScheme(headers, *cluster_);

  // 重试状态
  retry_state_ = createRetryState(route_entry_-&gt;retryPolicy(), headers, *cluster_, config_.runtime_,
                       config_.random_, callbacks_-&gt;dispatcher(), route_entry_-&gt;priority());
  // 请求是否应该被shadow（镜像）
  do_shadowing_ = FilterUtility::shouldShadow(route_entry_-&gt;shadowPolicy(), config_.runtime_,  callbacks_-&gt;streamId());

  ENVOY_STREAM_LOG(debug, "router decoding headers:\n{}", *callbacks_, headers);
  // 上游请求对象
  upstream_request_ = std::make_unique&lt;UpstreamRequest&gt;(*this, *conn_pool);
  // 上游请求不会在本地走过滤器链，下面的方法仅仅是
  // 1、调用conn_pool_.newStream()创建新的流
  // 2、将新的流赋值给UpstreamRequest.conn_pool_stream_handle_变量
  upstream_request_-&gt;encodeHeaders(end_stream);
  if (end_stream) {
    // 执行此回调，用于上游请求以异步发送的，这里不代表上游请求处理完毕
    // 在Dispatcher上注册超时定时器，在上游请求执行超时后回调onResponseTimeout
    onRequestComplete();
  }

  return Http::FilterHeadersStatus::StopIteration;
}</pre>
<p>如果选择的路由的上游集群没有健康的端点，则会调用：</p>
<pre class="crayon-plain-tag">void Filter::sendNoHealthyUpstreamResponse() {
  callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::NoHealthyUpstream);
  chargeUpstreamCode(Http::Code::ServiceUnavailable, nullptr, false);
  callbacks_-&gt;sendLocalReply(Http::Code::ServiceUnavailable, "no healthy upstream", nullptr, absl::nullopt);
}</pre>
<p>给与客户端503响应，响应体设置为 no healthy upstream 。</p>
<div class="blog_h3"><span class="graybg">上游集群连接池</span></div>
<p>Envoy和上游集群主机的交互，是通过连接池进行的。每个上游主机对应一个连接池对象，根据协议和配置的不同，连接池中维持的连接数量也不同。对于HTTP/2协议，由于多路复用的关系，不考虑套接字选项的情况下，池中总是只有单个连接。</p>
<p>路由过滤器会调用getConnPool()来获取连接池：</p>
<pre class="crayon-plain-tag">Http::ConnectionPool::Instance* Filter::getConnPool() {
  // 获取集群支持的特性，位域字段
  auto features = cluster_-&gt;features();
  // 根据上游集群的配置、下游连接的类型来决定使用什么协议
  // 根据运行时配置，集群可能将HTTP2降级为HTTP1
  Http::Protocol protocol;
  if (features &amp; Upstream::ClusterInfo::Features::USE_DOWNSTREAM_PROTOCOL) {
    // 如果使用下游的协议
    protocol = callbacks_-&gt;streamInfo().protocol().value();
  } else {
    // 否则，如果上游支持HTTP2则使用之，不支持则HTTP11
    protocol = (features &amp; Upstream::ClusterInfo::Features::HTTP2) ? Http::Protocol::Http2
                                                                   : Http::Protocol::Http11;
  }
  // cm_是集群管理器
  return config_.cm_.httpConnPoolForCluster(route_entry_-&gt;clusterName(), route_entry_-&gt;priority(),
                                            protocol, this);
}</pre>
<p>连接池的管理，实际上由集群管理器负责：</p>
<pre class="crayon-plain-tag">Http::ConnectionPool::Instance*
ClusterManagerImpl::httpConnPoolForCluster(const std::string&amp; cluster, ResourcePriority priority,
                                           Http::Protocol protocol, LoadBalancerContext* context) {
  // 获取线程本地的集群管理器对象
  ThreadLocalClusterManagerImpl&amp; cluster_manager = tls_-&gt;getTyped&lt;ThreadLocalClusterManagerImpl&gt;();
  // 根据名称查找上游集群
  auto entry = cluster_manager.thread_local_clusters_.find(cluster);
  if (entry == cluster_manager.thread_local_clusters_.end()) {
    return nullptr;
  }

  // 委托给上游集群
  return entry-&gt;second-&gt;connPool(priority, protocol, context);
} </pre>
<p>获取连接池的工作进一步委托给上游集群（ClusterEntry）：</p>
<pre class="crayon-plain-tag">Http::ConnectionPool::Instance*
ClusterManagerImpl::ThreadLocalClusterManagerImpl::ClusterEntry::connPool(
    ResourcePriority priority, Http::Protocol protocol, LoadBalancerContext* context) {
  // 根据负载均衡上下文（就是Router这个过滤器），也就是负载均衡策略，来决定使用哪个主机的连接池
  HostConstSharedPtr host = lb_-&gt;chooseHost(context);
  if (!host) {
    ENVOY_LOG(debug, "no healthy host for HTTP connection pool");
    cluster_info_-&gt;stats().upstream_cx_none_healthy_.inc();
    return nullptr;
  }

  // 从下游连接继承套接字选项
  std::vector&lt;uint8_t&gt; hash_key = {uint8_t(protocol), uint8_t(priority)};

  // 基于下游套接字选项来计算连接池的哈希键。以便基于套接字选项来控制连接池，让不同选项的连接不池化在一起
  bool have_options = false;
  if (context &amp;&amp; context-&gt;downstreamConnection()) {
    const Network::ConnectionSocket::OptionsSharedPtr&amp; options =
        context-&gt;downstreamConnection()-&gt;socketOptions();
    if (options) {超时
      for (const auto&amp; option : *options) {
        have_options = true;
        option-&gt;hashKey(hash_key);
      }
    }
  }

  // 获取单个主机的连接池容器
  ConnPoolsContainer&amp; container = *parent_.getHttpConnPoolsContainer(host, true);

  // 根据套接字选项的哈希，从连接池容器中获得连接池
  Http::ConnectionPool::Instance&amp; pool = container.pools_-&gt;getPool(hash_key, [&amp;]() {
    return parent_.parent_.factory_.allocateConnPool(
        parent_.thread_local_dispatcher_, host, priority, protocol,
        have_options ? context-&gt;downstreamConnection()-&gt;socketOptions() : nullptr);
  });

  return &amp;pool;
}</pre>
<div class="blog_h3"><span class="graybg">准备上游请求</span></div>
<p>获得可用的连接池对象后，Router过滤器会创建UpstreamRequest ，并调用它的encodeHeaders方法：</p>
<pre class="crayon-plain-tag">// encodeHeaders不需要变量过滤器链，因为过滤器链是为下游连接服务的
void Filter::UpstreamRequest::encodeHeaders(bool end_stream) {
  ASSERT(!encode_complete_);
  encode_complete_ = end_stream;

  // 创建一个新的流，并赋值给UpstreamRequest.conn_pool_stream_handle_
  // 注意UpstreamRequest实现了StreamDecoder，能够解码上游响应
  Http::ConnectionPool::Cancellable* handle = conn_pool_.newStream(*this, *this);
  if (handle) {
    // 可能在newStream()调用期间发生reset，这种情况下handle为nullptr
    conn_pool_stream_handle_ = handle;
  }
}</pre>
<p>连接池的newStream方法创建一个连接到某个上游主机的新的流：</p>
<pre class="crayon-plain-tag">/**
   * 在连接池上创建一个新的流
   * @param response_decoder 响应解码器 —— 对于上游请求，Router过滤器需要对其返回的应答进行解码
   * @param cb 当连接准备好和失败时执行的回调，如果有可用的连接/出现立即的失败，这些回调可能在当前方法的上下文中直接调用
   *           这种情况下，此函数返回nullptr
   * @return Cancellable* 如果池中没有可用的连接，上述cb不会被立即调用，该方法会返回一个Cancellable类型的handle
   *                      调用者可以使用该句柄来取消请求
   *                      注意：一旦任何回调函数被调用，则句柄不再有效。要取消请求，必须将流重置
   */
  virtual Cancellable* newStream(Http::StreamDecoder&amp; response_decoder, Callbacks&amp; callbacks) PURE;</pre>
<p>上述方法的实现如下：</p>
<pre class="crayon-plain-tag">ConnectionPool::Cancellable* ConnPoolImpl::newStream(StreamDecoder&amp; response_decoder,
                                                     ConnectionPool::Callbacks&amp; callbacks) {
  // 统计指标收集
  host_-&gt;cluster().stats().upstream_rq_total_.inc();
  host_-&gt;stats().rq_total_.inc();
  if (!ready_clients_.empty()) {
    // 如果有可用的客户端，则取出一个放到不可用列表中
    ready_clients_.front()-&gt;moveBetweenLists(ready_clients_, busy_clients_);
    ENVOY_CONN_LOG(debug, "using existing connection", *busy_clients_.front()-&gt;codec_client_);
    // 然后将请求关联到客户端
    attachRequestToClient(*busy_clients_.front(), response_decoder, callbacks);
    return nullptr;
  }

  //                   ResourceManager非完全一致的同步最大连接数、未决请求等信息
  //                                              是否可以创建新的请求
  if (host_-&gt;cluster().resourceManager(priority_).pendingRequests().canCreate()) {
    //                                            是否可以创建新的连接
    bool can_create_connection = host_-&gt;cluster().resourceManager(priority_).connections().canCreate();
    if (!can_create_connection) {
      // 连接总数超标
      host_-&gt;cluster().stats().upstream_cx_overflow_.inc();
    }

    // 如果池中根本没有连接，则立即创建一个防止饥饿
    if ((ready_clients_.size() == 0 &amp;&amp; busy_clients_.size() == 0) || can_create_connection) {
      // 创建新的客户端ActiveClient
      // 将其放入busy_clients_列表
      createNewConnection();
    }
    // 创建请求并排队
    return newPendingRequest(response_decoder, callbacks);
  } else {
    // 超过允许的未决请求的最大数量
    ENVOY_LOG(debug, "max pending requests overflow");
    callbacks.onPoolFailure(ConnectionPool::PoolFailureReason::Overflow, nullptr);
    host_-&gt;cluster().stats().upstream_rq_pending_overflow_.inc();
    return nullptr;
  }
} </pre>
<p>可以看到，如果连接池有空闲的HTTP客户端，则将UpstreamRequest关联到一个空闲连接：</p>
<pre class="crayon-plain-tag">void ConnPoolImpl::attachRequestToClient(ActiveClient&amp; client, StreamDecoder&amp; response_decoder,
                                         ConnectionPool::Callbacks&amp; callbacks) {
  ASSERT(!client.stream_wrapper_);
  // 将UpstreamRequest+ActiveClient封装为流编解码包装器
  client.stream_wrapper_ = std::make_unique&lt;StreamWrapper&gt;(response_decoder, client);
  // 回调onPoolReady：当连接池中有连接能够处理上游请求时执行
  callbacks.onPoolReady(*client.stream_wrapper_, client.real_host_description_);
}

// StreamWrapper的构造函数：
ConnPoolImpl::StreamWrapper::StreamWrapper(StreamDecoder&amp; response_decoder, ActiveClient&amp; parent)
    // CodecClient支持多种HTTP协议类型下的多路流、底层连接的管理
    : StreamEncoderWrapper(parent.codec_client_-&gt;newStream(*this)),
      StreamDecoderWrapper(response_decoder), parent_(parent) {
  // 添加回调
  StreamEncoderWrapper::inner_.getStream().addCallbacks(*this);
}

// 底层请求流
StreamEncoder&amp; CodecClient::newStream(StreamDecoder&amp; response_decoder) {
  // response_decoder即UpstreamRequest
  ActiveRequestPtr request(new ActiveRequest(*this, response_decoder));
  // 创建出站请求流
  request-&gt;encoder_ = &amp;codec_-&gt;newStream(*request);
  request-&gt;encoder_-&gt;getStream().addCallbacks(*request);
  request-&gt;moveIntoList(std::move(request), active_requests_);
  disableIdleTimer();
  return *active_requests_.front()-&gt;encoder_;
}

StreamEncoder&amp; ClientConnectionImpl::newStream(StreamDecoder&amp; response_decoder) {
  if (resetStreamCalled()) {
    throw CodecClientException("cannot create new streams after calling reset");
  }
  // 为连接启用读
  while (!connection_.readEnabled()) {
    connection_.readDisable(false);
  }
  request_encoder_ = std::make_unique&lt;RequestStreamEncoderImpl&gt;(*this);
  // 将UpstreamRequest纳入未决响应列表
  pending_responses_.emplace_back(&amp;response_decoder);
  return *request_encoder_;
}</pre>
<p>反之，如果连接池没有空闲HTTP客户端，则创建PendingRequest并排队：</p>
<pre class="crayon-plain-tag">ConnectionPool::Cancellable* ConnPoolImplBase::newPendingRequest(StreamDecoder&amp; decoder, ConnectionPool::Callbacks&amp; callbacks) {
  ENVOY_LOG(debug, "queueing request due to no available connections");
  // 创建PendingRequest
  PendingRequestPtr pending_request(new PendingRequest(*this, decoder, callbacks));
  // 加入pending_requests_列表，然后返回
  pending_request-&gt;moveIntoList(std::move(pending_request), pending_requests_);
  return pending_requests_.front().get();
}</pre>
<p>排队的请求会在以后，因为某种事件而关联到可用连接。例如新的针对上游主机的L4连接建立后：</p>
<pre class="crayon-plain-tag">// Envoy::Http::Http1::ConnPoolImpl::attachRequestToClient conn_pool.cc:66
// Envoy::Http::Http1::ConnPoolImpl::processIdleClient conn_pool.cc:238
  client.stream_wrapper_.reset();
  if (pending_requests_.empty() || delay) {
    // 没有未决请求，将客户端加入空闲列表
    client.moveBetweenLists(busy_clients_, ready_clients_);
  } else {
    // 绑定请求到客户端
    attachRequestToClient(client, pending_requests_.back()-&gt;decoder_, pending_requests_.back()-&gt;callbacks_);
    pending_requests_.pop_back();
  }
// Envoy::Http::Http1::ConnPoolImpl::onConnectionEvent conn_pool.cc:183
  if (event == Network::ConnectionEvent::Connected) {
    conn_connect_ms_-&gt;complete();
    // 有空闲客户端了，处理之
    processIdleClient(client, false);
  }
// Envoy::Http::Http1::ConnPoolImpl::ActiveClient::onEvent conn_pool.h:89
    void onEvent(Network::ConnectionEvent event) override {
      parent_.onConnectionEvent(*this, event);
    }
// Envoy::Network::ConnectionImpl::raiseEvent connection_impl.cc:329
void ConnectionImpl::raiseEvent(ConnectionEvent event) {
  for (ConnectionCallbacks* callback : callbacks_) {
    callback-&gt;onEvent(event);
  }
  if (state() == State::Open &amp;&amp; event == ConnectionEvent::Connected &amp;&amp; write_buffer_-&gt;length() &gt; 0) {
    onWriteReady();
  }
}
// Envoy::Network::RawBufferSocket::onConnected raw_buffer_socket.cc:83
void RawBufferSocket::onConnected() { callbacks_-&gt;raiseEvent(ConnectionEvent::Connected); }
// Envoy::Network::ConnectionImpl::onWriteReady connection_impl.cc:519
    if (error == 0) {
      ENVOY_CONN_LOG(debug, "connected", *this);
      connecting_ = false;
      transport_socket_-&gt;onConnected();
      ...
// Envoy::Network::ConnectionImpl::onFileEvent connection_impl.cc:467</pre>
<p>到这里为止，我们还没搞清楚，针对上游主机的请求到底是何时、由谁发出去的。实际上这是在Router过滤器的onPoolReady回调中进行的。</p>
<div class="blog_h3"><span class="graybg">发送上游请求</span></div>
<p>不管请求是异步还是同步的关联到HTTP客户端（attachRequestToClient），都会触发onPoolReady。此回调会真正发出请求：</p>
<pre class="crayon-plain-tag">void Filter::UpstreamRequest::onPoolReady(Http::StreamEncoder&amp; request_encoder,
                                          Upstream::HostDescriptionConstSharedPtr host) {
  ENVOY_STREAM_LOG(debug, "pool ready", *parent_.callbacks_);

  // 设置UpstreamRequest.upstream_host_ = host
  // 调用UpstreamRequest、Router的StreamInfo.onUpstreamHostSelected()
  onUpstreamHostSelected(host);
  request_encoder.getStream().addCallbacks(*this);

  // 创建per-try的定时器。per_try_timeout_字段被设置为已启用的定时器
  setupPerTryTimeout();
  conn_pool_stream_handle_ = nullptr;
  // 将StreamWrapper设置为请求编码器
  setRequestEncoder(request_encoder);
  calling_encode_headers_ = true;
  if (parent_.route_entry_-&gt;autoHostRewrite() &amp;&amp; !host-&gt;hostname().empty()) {
    // 如果当前路由条目设置了自动头重写，则使用目标上游主机的名称来覆盖请求头
    parent_.downstream_headers_-&gt;Host()-&gt;value(host-&gt;hostname());
  }
  
  // 注入传递当前追踪需要的头
  if (span_ != nullptr) {
    span_-&gt;injectContext(*parent_.downstream_headers_);
  }

  // 日志用途信息
  stream_info_.onFirstUpstreamTxByteSent();
  parent_.callbacks_-&gt;streamInfo().onFirstUpstreamTxByteSent();
  // 进行请求头编码，调用StreamEncoderWrapper，后者装饰一个StreamEncoder的实现RequestStreamEncoderImpl
  request_encoder.encodeHeaders(*parent_.downstream_headers_, !buffered_request_body_ &amp;&amp; encode_complete_ &amp;&amp; !encode_trailers_);
  calling_encode_headers_ = false;

  // 在encodeHeaders()调用过程中可能发生RESET，这里需要进行测试，尽管是非常边缘的情况
  // 例如对于HTTP/2 codec，当帧由于某种原因无法编码的情况下就会出现RESET —— 比如头过大，超过64K
  if (deferred_reset_reason_) {
    // 重置回调
    onResetStream(deferred_reset_reason_.value());
  } else {
    // 编码请求体
    if (buffered_request_body_) {
      stream_info_.addBytesSent(buffered_request_body_-&gt;length());
      request_encoder.encodeData(*buffered_request_body_, encode_complete_ &amp;&amp; !encode_trailers_);
    }
    // 编码请求尾
    if (encode_trailers_) {
      request_encoder.encodeTrailers(*parent_.downstream_trailers_);
    }
    // 记录日志用的流信息
    if (encode_complete_) {
      stream_info_.onLastUpstreamTxByteSent();
      parent_.callbacks_-&gt;streamInfo().onLastUpstreamTxByteSent();
    }
  }
} </pre>
<p>上游请求的编码逻辑如下：</p>
<pre class="crayon-plain-tag">void RequestStreamEncoderImpl::encodeHeaders(const HeaderMap&amp; headers, bool end_stream) {
  // Method、Path头必须存在
  const HeaderEntry* method = headers.Method();
  const HeaderEntry* path = headers.Path();
  if (!method || !path) {
    throw CodecClientException(":method and :path must be specified");
  }
  // 如果是HEAD请求
  if (method-&gt;value() == Headers::get().MethodValues.Head.c_str()) {
    head_request_ = true;
  }
  // 如果是HEAD请求，则设置pending_response.head_request_ = true
  connection_.onEncodeHeaders(headers);
  // 写入报文最前面的部分
  connection_.reserveBuffer(std::max(4096U, path-&gt;value().size() + 4096));
  connection_.copyToBuffer(method-&gt;value().c_str(), method-&gt;value().size());
  connection_.addCharToBuffer(' ');
  connection_.copyToBuffer(path-&gt;value().c_str(), path-&gt;value().size());
  connection_.copyToBuffer(REQUEST_POSTFIX, sizeof(REQUEST_POSTFIX) - 1);
  // 写入请求头部分，包括写入一些额外的头
  StreamEncoderImpl::encodeHeaders(headers, end_stream);
}</pre>
<p>可以看到，<span style="background-color: #c0c0c0;">上游请求的编码，是不走HTTP过滤器链</span>的。 </p>
<div class="blog_h3"><span class="graybg">接收上游响应</span></div>
<p>那么，上游的响应又是如何接收到的呢？在newStream方法调用createNewConnection创建新客户端时，对应的L4连接也会被创建 —— libevent事件回调会被注册：</p>
<pre class="crayon-plain-tag">void ConnPoolImpl::createNewConnection() {
  ActiveClientPtr client(new ActiveClient(*this));
  client-&gt;moveIntoList(std::move(client), busy_clients_);
}
// ActiveClient的构造函数会创建L4连接
ConnPoolImpl::ActiveClient::ActiveClient(ConnPoolImpl&amp; parent)
    : parent_(parent),
      // 连接到服务器端的超时回调
      connect_timer_(parent_.dispatcher_.createTimer([this]() -&gt; void { onConnectTimeout(); })),
      remaining_requests_(parent_.host_-&gt;cluster().maxRequestsPerConnection()) {
  // ...
  // 调用HostImpl.createConnection()
  Upstream::Host::CreateConnectionData data =
      parent_.host_-&gt;createConnection(parent_.dispatcher_, parent_.socket_options_, nullptr);
}

Host::CreateConnectionData HostImpl::createConnection(
    Event::Dispatcher&amp; dispatcher, const Network::ConnectionSocket::OptionsSharedPtr&amp; options,
    Network::TransportSocketOptionsSharedPtr transport_socket_options) const {
  // 创建L4客户端连接
  return {createConnection(dispatcher, *cluster_, address_, options, transport_socket_options),
          shared_from_this()};
}</pre>
<p>响应就是通过libevent回调传递，其<span style="background-color: #c0c0c0;">网络层的处理路径和处理下游请求时是完全一样的——不管是读下游请求还是上游响应，L4过滤器的onData都会被调用</span>，在onContinueReading方法中进行报文的读取。</p>
<p>对于HTTP1来说，当报文头读取完毕后，Http::Http1::ClientConnectionImpl::onHeadersComplete被回调，它会转调PendingResponse.decoder.decodeHeaders方法，后者进而调用UpstreamRequest::decodeHeaders：</p>
<pre class="crayon-plain-tag">void Filter::UpstreamRequest::decodeHeaders(Http::HeaderMapPtr&amp;&amp; headers, bool end_stream) {
  stream_info_.onFirstUpstreamRxByteReceived();
  parent_.callbacks_-&gt;streamInfo().onFirstUpstreamRxByteReceived();
  maybeEndDecode(end_stream);
  // 读取头
  upstream_headers_ = headers.get();
  // 获取响应码
  const uint64_t response_code = Http::Utility::getResponseStatus(*headers);
  stream_info_.response_code_ = static_cast&lt;uint32_t&gt;(response_code);
  // 调用Router
  parent_.onUpstreamHeaders(response_code, std::move(headers), end_stream);
}</pre>
<p>UpstreamRequest.parent_就是Router过滤器，其onUpstreamHeaders的实现如下：</p>
<pre class="crayon-plain-tag">void Filter::onUpstreamHeaders(const uint64_t response_code, Http::HeaderMapPtr&amp;&amp; headers,
                               bool end_stream) {
  ENVOY_STREAM_LOG(debug, "upstream headers complete: end_stream={}", *callbacks_, end_stream);

  // 异常检测信息收集，为上游主机添加一个状态码
  upstream_request_-&gt;upstream_host_-&gt;outlierDetector().putHttpResponseCode(response_code);

  // 健康检查快速失败标记 x-envoy-immediate-health-check-fail，可能通过管理端点设置
  if (headers-&gt;EnvoyImmediateHealthCheckFail() != nullptr) {
    // 设置上游主机健康状态
    upstream_request_-&gt;upstream_host_-&gt;healthChecker().setUnhealthy();
  }

  // 重试相关的处理
  if (retry_state_) {
    // onHostAttempted：当针对一个主机的请求尝试失败了，并且可以进行下一个尝试时，调用此回调
    retry_state_-&gt;onHostAttempted(upstream_request_-&gt;upstream_host_);
    // 判断是否应该进行重试，如果是，调用回调，也就是doRetry()
    RetryStatus retry_status = retry_state_-&gt;shouldRetry(
        headers.get(), absl::optional&lt;Http::StreamResetReason&gt;(), [this]() -&gt; void { doRetry(); });
    // 捕获上游主机，因为后面的setupRetry()调用会清除upstream_request_
    const auto upstream_host = upstream_request_-&gt;upstream_host_;
    if (retry_status == RetryStatus::Yes &amp;&amp; setupRetry(end_stream)) {
      // 重试
      Http::CodeStats&amp; code_stats = httpContext().codeStats();
      code_stats.chargeBasicResponseStat(cluster_-&gt;statsScope(), "retry.", static_cast&lt;Http::Code&gt;(response_code));
      upstream_host-&gt;stats().rq_error_.inc();
      return;
    } else if (retry_status == RetryStatus::NoOverflow) {
      // 上游过载
      callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::UpstreamOverflow);
    } else if (retry_status == RetryStatus::NoRetryLimitExceeded) {
      // 达到最大重试次数
      callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::UpstreamRetryLimitExceeded);
    }

    // 由于end_stream为false时不会调用cleanup()，保证重试定时器被销毁
    retry_state_.reset();
  }
  
  // 处理重定向
  if (static_cast&lt;Http::Code&gt;(response_code) == Http::Code::Found &amp;&amp;
      route_entry_-&gt;internalRedirectAction() == InternalRedirectAction::Handle &amp;&amp; setupRedirect(*headers)) {
    return;
    // If the redirect could not be handled, fail open and let it pass to the
    // next downstream.
  }

  // 处理响应头x-envoy-upstream-service-time
  if (DateUtil::timePointValid(downstream_request_complete_time_)) {
    Event::Dispatcher&amp; dispatcher = callbacks_-&gt;dispatcher();
    MonotonicTime response_received_time = dispatcher.timeSystem().monotonicTime();
    std::chrono::milliseconds ms = std::chrono::duration_cast&lt;std::chrono::milliseconds&gt;(
        response_received_time - downstream_request_complete_time_);
    if (!config_.suppress_envoy_headers_) {
      headers-&gt;insertEnvoyUpstreamServiceTime().value(ms.count());
    }
  }

  // 根据响应头来设置此上游主机是否金丝雀版本
  upstream_request_-&gt;upstream_canary_ =
      (headers-&gt;EnvoyUpstreamCanary() &amp;&amp; headers-&gt;EnvoyUpstreamCanary()-&gt;value() == "true") ||
      upstream_request_-&gt;upstream_host_-&gt;canary();
  chargeUpstreamCode(response_code, *headers, upstream_request_-&gt;upstream_host_, false);

  // 处理非500响应头，主要是进行一些指标的收集
  if (!Http::CodeUtility::is5xx(response_code)) {
    handleNon5xxResponseHeaders(*headers, end_stream);
  }

  // downstream_set_cookies_为需要添加到上游响应头中的Cookies
  for (const auto&amp; header_value : downstream_set_cookies_) {
    headers-&gt;addReferenceKey(Http::Headers::get().SetCookie, header_value);
  }

  // 对响应头进行一系列最后处理：
  // 添加一系列用户定义的响应头，按照顺序： route-action-level、route-level、virtual host level、route-action-level
  route_entry_-&gt;finalizeResponseHeaders(*headers, callbacks_-&gt;streamInfo());

  downstream_response_started_ = true;
  if (end_stream) {
    onUpstreamComplete();
  }

  // 开始向下游发送响应头，这个是要走过滤器链的
  callbacks_-&gt;encodeHeaders(std::move(headers), end_stream);
}</pre>
<p>可以看到，<span style="background-color: #c0c0c0;">上游响应的解码，也是不走HTTP过滤器链</span>的。</p>
<p>另外需要注意，不管是下游请求、上游响应，<span style="background-color: #c0c0c0;">都会经由http_parser回调L7连接的on***Complete方法，不同之处是，对于下游请求来说L7连接的实现是ServerConnectionImpl，而对于上游响应来说L7连接的实现是ClientConnectionImpl</span>。</p>
<p>上游响应头处理完毕后，响应体回调onMessageComplete很快执行：</p>
<pre class="crayon-plain-tag">void ClientConnectionImpl::onMessageComplete() {
  ENVOY_CONN_LOG(trace, "message complete", connection_);
  if (ignore_message_complete_for_100_continue_) {
    ignore_message_complete_for_100_continue_ = false;
    return;
  }
  if (!pending_responses_.empty()) {
    // 取出未决响应，注意这里是HTTP11，每个连接上同时只会有一个未决响应
    PendingResponse response = pending_responses_.front();
    pending_responses_.pop_front();

    if (deferred_end_stream_headers_) {
      // 解码响应头
      response.decoder_-&gt;decodeHeaders(std::move(deferred_end_stream_headers_), true);
      deferred_end_stream_headers_.reset();
    } else {
      // 解码响应体
      Buffer::OwnedImpl buffer;
      response.decoder_-&gt;decodeData(buffer, true);
    }
  }
}</pre>
<p>response.decoder就是UpstreamRequest，其decodeData方法会调用Router过滤器的onUpstreamData，这类似于读取响应头时调用onUpstreamHeaders，类似的、可能被调用的其它回调包括onUpstreamTrailers、onUpstreamMetadata。</p>
<div class="blog_h3"><span class="graybg">处理上游超时 </span></div>
<p>Router过滤器不负责真正的发送上游请求，这是由连接池异步进行的。它调用upstream_request_的encodeHeaders后，<span style="background-color: #c0c0c0;">立即回调onRequestComplete</span>，后者注册了定时器来处理请求超时：</p>
<pre class="crayon-plain-tag">void Filter::onRequestComplete() {
  downstream_end_stream_ = true;
  // 获取事件分发器
  Event::Dispatcher&amp; dispatcher = callbacks_-&gt;dispatcher();
  downstream_request_complete_time_ = dispatcher.timeSystem().monotonicTime();

  // 有可能我们得到一个立即的RESET，因此这里判断上游请求是否为空
  if (upstream_request_) {
    maybeDoShadowing();
    // 如果配置了超时，则注册定时器，回调为onResponseTimeout
    if (timeout_.global_timeout_.count() &gt; 0) {
      response_timeout_ = dispatcher.createTimer([this]() -&gt; void { onResponseTimeout(); });
      response_timeout_-&gt;enableTimer(timeout_.global_timeout_);
    }
  }
}</pre>
<p>如果上游请求超时，下面的函数被调用：</p>
<pre class="crayon-plain-tag">void Filter::onResponseTimeout() {
  ENVOY_STREAM_LOG(debug, "upstream timeout", *callbacks_);
  cluster_-&gt;stats().upstream_rq_timeout_.inc();

  // 可能在执行上游请求重试backoff期间发生超时，那时是没有上游请求的。这种情况下仿冒一个RESET
  if (upstream_request_) {
    if (upstream_request_-&gt;upstream_host_) {
      upstream_request_-&gt;upstream_host_-&gt;stats().rq_timeout_.inc();
    }
    // 请求已经处理，不能取消，必须重置流
    upstream_request_-&gt;resetStream();
  }
  // 触发上游重置，重置的原因有Reset, GlobalTimeout, PerTryTimeout几种，这里是GlobalTimeout
  onUpstreamReset(UpstreamResetType::GlobalTimeout, absl::optional&lt;Http::StreamResetReason&gt;());
}</pre>
<div class="blog_h3"><span class="graybg">处理上游重置</span></div>
<pre class="crayon-plain-tag">void Filter::onUpstreamReset(UpstreamResetType type, const absl::optional&lt;Http::StreamResetReason&gt;&amp; reset_reason) {
  // 全局性超时
  ASSERT(type == UpstreamResetType::GlobalTimeout || upstream_request_);
  // 上游重置
  if (type == UpstreamResetType::Reset) {
    ENVOY_STREAM_LOG(debug, "upstream reset", *callbacks_);
  }

  Upstream::HostDescriptionConstSharedPtr upstream_host;
  if (upstream_request_) {
    upstream_host = upstream_request_-&gt;upstream_host_;
    if (upstream_host) {
      // 为上游主机的断路检测器提供信息，如果是RESET，则记录503，否则记录504（网关超时）
      upstream_host-&gt;outlierDetector().putHttpResponseCode(
          enumToInt(type == UpstreamResetType::Reset ? Http::Code::ServiceUnavailable
                                                     : timeout_response_code_));
    }
  }

  // 全局超时时不会重试，已经开始响应处理后也不会重试
  if (type != UpstreamResetType::GlobalTimeout &amp;&amp; !downstream_response_started_ &amp;&amp; retry_state_) {
    // 回调retry modifiers
    if (upstream_host != nullptr) {
      retry_state_-&gt;onHostAttempted(upstream_host);
    }
    // 判断是否需要重试
    RetryStatus retry_status = retry_state_-&gt;shouldRetry(nullptr, reset_reason, [this]() -&gt; void { doRetry(); });
    if (retry_status == RetryStatus::Yes &amp;&amp; setupRetry(true)) {
      // 需要重试
      if (upstream_host) {
        upstream_host-&gt;stats().rq_error_.inc();
      }
      return;
    // 不应该重试
    } else if (retry_status == RetryStatus::NoOverflow) {
      callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::UpstreamOverflow);
    } else if (retry_status == RetryStatus::NoRetryLimitExceeded) {
      callbacks_-&gt;streamInfo().setResponseFlag(
          StreamInfo::ResponseFlag::UpstreamRetryLimitExceeded);
    }
  }

  // 如果尚未向下游发送任何信息，则发送具有适当响应码的响应；否则仅仅是重置响应
  if (downstream_response_started_) {
    if (upstream_request_ != nullptr &amp;&amp; upstream_request_-&gt;grpc_rq_success_deferred_) {
      upstream_request_-&gt;upstream_host_-&gt;stats().rq_error_.inc();
    }
    // 删除重试定时器
    cleanup();
    callbacks_-&gt;resetStream();
  } else {
    cleanup();
    Http::Code code;
    const char* body;
    if (type == UpstreamResetType::GlobalTimeout || type == UpstreamResetType::PerTryTimeout) {
      callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::UpstreamRequestTimeout);

      code = timeout_response_code_;
      body = code == Http::Code::GatewayTimeout ? "upstream request timeout" : "";
    } else {
      StreamInfo::ResponseFlag response_flags =
          streamResetReasonToResponseFlag(reset_reason.value());
      callbacks_-&gt;streamInfo().setResponseFlag(response_flags);
      code = Http::Code::ServiceUnavailable;
      body = "upstream connect error or disconnect/reset before headers";
    }

    const bool dropped = reset_reason &amp;&amp; reset_reason.value() == Http::StreamResetReason::Overflow;
    chargeUpstreamCode(code, upstream_host, dropped);
    // 如果有非5xx响应，却仍然被后端重置，或者在响应开始前超时，作为一个错误看待
    if (upstream_host != nullptr &amp;&amp; !Http::CodeUtility::is5xx(enumToInt(code))) {
      upstream_host-&gt;stats().rq_error_.inc();
    }
    // 发送本地响应
    callbacks_-&gt;sendLocalReply(code, body,
                               [dropped, this](Http::HeaderMap&amp; headers) {
                                 if (dropped &amp;&amp; !config_.suppress_envoy_headers_) {
                                   headers.insertEnvoyOverloaded().value(
                                       Http::Headers::get().EnvoyOverloadedValues.True);
                                 }
                               },
                               absl::nullopt);
  }
}</pre>
<div class="blog_h2"><span class="graybg">处理HTTP下游响应写</span></div>
<div class="blog_h3"><span class="graybg">发送本地响应</span></div>
<p>在Envoy的HTTP解码过滤器处理下游请求的过程中，可能由于多种原因（通常是异常情况），立即应答下游客户端，而不向上游转发请求。此时会调用ActiveStreamDecoderFilter，或者直接调用ActiveStream的：</p>
<pre class="crayon-plain-tag">void ConnectionManagerImpl::ActiveStream::sendLocalReply(
    bool is_grpc_request, Code code, absl::string_view body,
    const std::function&lt;void(HeaderMap&amp; headers)&gt;&amp; modify_headers, bool is_head_request,
    const absl::optional&lt;Grpc::Status::GrpcStatus&gt; grpc_status) {
  // 断言当前流的响应头尚未设置
  ASSERT(response_headers_ == nullptr);
  // 对于过早错误的处理，尽可能尝试创建出过滤器链，以便记录访问日志
  if (!state_.created_filter_chain_) {
    createFilterChain();
  }
  // 调用此工具函数
  Utility::sendLocalReply(is_grpc_request,
                          // 编码响应头的回调
                          [this, modify_headers](HeaderMapPtr&amp;&amp; headers, bool end_stream) -&gt; void {
                            if (modify_headers != nullptr) {
                              // 转发sendLocalReply的入参
                              modify_headers(*headers);
                            }
                            // 移动响应头
                            response_headers_ = std::move(headers);
                            // 编码响应头
                            encodeHeaders(nullptr, *response_headers_, end_stream);
                          },
                          // 编码响应体的回调
                          [this](Buffer::Instance&amp; data, bool end_stream) -&gt; void {
                            // 编码响应体
                            encodeData(nullptr, data, end_stream);
                          },
                          // 被销毁？重置      响应码 响应体 gRPC状态码    提示是否header-only
                          state_.destroyed_, code, body, grpc_status, is_head_request);
}</pre>
<p>上面代码调用的工具函数实现如下：</p>
<pre class="crayon-plain-tag">void Utility::sendLocalReply(
    bool is_grpc, std::function&lt;void(HeaderMapPtr&amp;&amp; headers, bool end_stream)&gt; encode_headers,
    std::function&lt;void(Buffer::Instance&amp; data, bool end_stream)&gt; encode_data, const bool&amp; is_reset,
    Code response_code, absl::string_view body_text,
    const absl::optional&lt;Grpc::Status::GrpcStatus&gt; grpc_status, bool is_head_request) {
  // encode_headers()调用可能重置流，但是在调用它之前，不能是已重置状态
  ASSERT(!is_reset);
  // 如果请求是gRPC，则返回trailers-only的响应
  if (is_grpc) {
    // 处理gRPC协议的响应头
    HeaderMapPtr response_headers{new HeaderMapImpl{
        {Headers::get().Status, std::to_string(enumToInt(Code::OK))},
        {Headers::get().ContentType, Headers::get().ContentTypeValues.Grpc},
        // gRPC状态码作为响应头
        {Headers::get().GrpcStatus,
         std::to_string(
             enumToInt(grpc_status ? grpc_status.value()
                                   : Grpc::Utility::httpToGrpcStatus(enumToInt(response_code))))}}};
    if (!body_text.empty() &amp;&amp; !is_head_request) {
      // 如果提供了响应体，则编码为gRPC消息
      response_headers-&gt;insertGrpcMessage().value(body_text);
    }
    encode_headers(std::move(response_headers), true); // 编码响应头
    return;
  }
  // 处理非gRPC协议的响应头
  HeaderMapPtr response_headers{ new HeaderMapImpl{{Headers::get().Status, std::to_string(enumToInt(response_code))}}};
  if (!body_text.empty()) {
    response_headers-&gt;insertContentLength().value(body_text.size());
    response_headers-&gt;insertContentType().value(Headers::get().ContentTypeValues.Text);
  }

  // 对于header-only响应，编码完头即返回
  if (is_head_request) {
    encode_headers(std::move(response_headers), true);
    return;
  }
  // 否则，如果响应体不为空，则编码头后，再编码体
  encode_headers(std::move(response_headers), body_text.empty());
  // encode_headers()) 调用可能修改了is_reset，因此再次测试：
  if (!body_text.empty() &amp;&amp; !is_reset) {
    // OwnedImpl封装一个分配的evbuffer，evbuffer用于libevent的缓冲网络I/O的缓冲区的处理
    Buffer::OwnedImpl buffer(body_text);
    encode_data(buffer, true);
  }
}</pre>
<div class="blog_h3"><span class="graybg">HTTP响应头编码</span></div>
<p>响应头编码由HTTP连接管理器的ActiveStream::encodeHeaders方法完成：</p>
<pre class="crayon-plain-tag">void ConnectionManagerImpl::ActiveStream::encodeHeaders(ActiveStreamEncoderFilter* filter,
                                                        HeaderMap&amp; headers, bool end_stream) {
  // 重置per-stream的空闲定时器，也就是重新计时
  resetIdleTimer();
  // 解除请求超时报警
  disarmRequestTimeout();
  // 设置 state_.local_complete_ = end_stream，并开始迭代过滤器链
  std::list&lt;ActiveStreamEncoderFilterPtr&gt;::iterator entry = commonEncodePrefix(filter, end_stream);
  // 在何处开始encodeData()调用
  std::list&lt;ActiveStreamEncoderFilterPtr&gt;::iterator continue_data_entry = encoder_filters_.end();

  for (; entry != encoder_filters_.end(); entry++) {
    // 设置过滤器调用状态为正在编码响应头
    ASSERT(!(state_.filter_call_state_ &amp; FilterCallState::EncodeHeaders));
    state_.filter_call_state_ |= FilterCallState::EncodeHeaders;
    // 设置过滤器的end_stream，如果header-only，或者传入end_stream==true
    // end_stream意味着后面没有响应体需要处理
    (*entry)-&gt;end_stream_ =
        encoding_headers_only_ || (end_stream &amp;&amp; continue_data_entry == encoder_filters_.end());
    // 调用过滤器进行编码
    FilterHeadersStatus status = (*entry)-&gt;handle_-&gt;encodeHeaders(headers, (*entry)-&gt;end_stream_);
    // 重置过滤器调用状态
    state_.filter_call_state_ &amp;= ~FilterCallState::EncodeHeaders;
    ENVOY_STREAM_LOG(trace, "encode headers called: filter={} status={}", *this,
                     static_cast&lt;const void*&gt;((*entry).get()), static_cast&lt;uint64_t&gt;(status));

    // 根据上一个过滤器的处理结果决定是否需要继续迭代
    const auto continue_iteration = (*entry)-&gt;commonHandleAfterHeadersCallback(status, encoding_heade刷出rs_only_);

    // 对于header-only应答，标记为local_complete_
    // 这样可以保证不会在doEndStream()中重置下游请求
    if (encoding_headers_only_) {
      state_.local_complete_ = true;
    }

    // 不继续迭代，也不会执行后面的基本响应头
    if (!continue_iteration) {
      return;
    }

    // 这里处理使用header-only响应，但是某个过滤器添加了响应体的情况
    // 不能传递end_stream = true给后续的过滤器
    if (end_stream &amp;&amp; buffered_response_data_ &amp;&amp; continue_data_entry == encoder_filters_.end()) {
      continue_data_entry = entry;
    }
  }

  // 基本响应头
  // 设置Date头
  connection_manager_.config_.dateProvider().setDateHeader(headers);
  // 设置Server头
  // 使用setReference()是安全的，因为serverName()在监听器的生命周期内不会改变
  headers.insertServer().value().setReference(connection_manager_.config_.serverName());
  // 如果是Upgrade请求，且没有响应体，则设置Content-Length头为0
  // 否则，移除Connection头
  // 移除Transfer=Encoding头
  // 如果请求头中设置了Envoy强制跟踪头（x-envoy-force-trace），且存在request-id，则在响应头中设置request-id
  // 移除KeepAlive头
  // 移除ProxyConnection头
  // 根据需要添加内容到Via头
  ConnectionManagerUtility::mutateResponseHeaders(headers, request_headers_.get(), connection_manager_.config_.via());

  // 如果当前应当drain/close连接，在编码响应头块之前发送go away帧
  if (connection_manager_.drain_state_ == DrainState::NotDraining &amp;&amp;
      // drainClose如果连接应当被drain和close返回true
      // 如果监听器正处于draing状态（原因可能是健康检查、热重启）。此方法的返回值由监听器本地、全局DrainManager共同决定
      // local_drain_manager_-&gt;drainClose() || parent_.server_.drainManager().drainClose()
      connection_manager_.drain_close_.drainClose()) {

    // 对于HTTP/1.1请求来说不做什么实质性的事情，仅仅让L4连接有额外的时间和后续请求竞争
    // 此方法在HTTP/1.1和HTTP/2之间保持逻辑一致
    connection_manager_.startDrainSequence();
    connection_manager_.stats_.named_.downstream_cx_drain_close_.inc();
    ENVOY_STREAM_LOG(debug, "drain closing connection", *this);
  }

  // 由于Connection: Close头，的原因，设置DrainState为Closing
  if (connection_manager_.drain_state_ == DrainState::NotDraining &amp;&amp; state_.saw_connection_close_) {
    ENVOY_STREAM_LOG(debug, "closing connection due to connection close header", *this);
    connection_manager_.drain_state_ = DrainState::Closing;
  }
  // 由于过载，且配置了在过载后禁用Keepalive，设置DrainState为Closing
  if (connection_manager_.drain_state_ == DrainState::NotDraining &amp;&amp;
      connection_manager_.overload_disable_keepalive_ref_ == Server::OverloadActionState::Active) {
    ENVOY_STREAM_LOG(debug, "disabling keepalive due to envoy overload", *this);
    connection_manager_.drain_state_ = DrainState::Closing;
    connection_manager_.stats_.named_.downstream_cx_overload_disable_keepalive_.inc();
  }

  // 如果准备在对端尚未完成的情况下销毁流，同时连接不支持多路分发（非HTTP2），设置DrainState为Closing
  if (!state_.remote_complete_) {
    if (connection_manager_.codec_-&gt;protocol() != Protocol::Http2) {
      connection_manager_.drain_state_ = DrainState::Closing;
    }

    connection_manager_.stats_.named_.downstream_rq_response_before_rq_complete_.inc();
  }

  // DrainState被置为Closing，且当前不是HTTP2
  if (connection_manager_.drain_state_ == DrainState::Closing &amp;&amp;
      connection_manager_.codec_-&gt;protocol() != Protocol::Http2) {
    // 如果不是Upgrade请求，则设置Connection:Close响应头
    // 关于Connection: close，如果出现在：
    // 1、请求头，表示它希望服务器在发送应答消息后关闭连接
    // 2、响应头，表示服务器会在发送应答消息后关闭连接，如果请求头是Connection: Keep-Alive则同时意味着服务器不支持连接重用
    if (!Utility::isUpgrade(headers)) {
      headers.insertConnection().value().setReference(Headers::get().ConnectionValues.Close);
    }
  }

  // 分布式追踪相关处理
  // 关于x-envoy-decorator-operation头：
  // 1、如果入站请求提供了此头，则应该覆盖在由追踪系统生成的server span中本地定义的operation(span)名
  // 2、如果出站响应存在此头，则应该覆盖任何本地定义的client span的operation(span)名
  if (connection_manager_.config_.tracingConfig()) {
    if (connection_manager_.config_.tracingConfig()-&gt;operation_name_ == Tracing::OperationName::Ingress) {
      // 对于ingress（inbound）响应
      // 如果请求头没有指定x-envoy-decorator-operation，则使用decorator的operation name作为x-envoy-decorator-operation响应头
      if (decorated_operation_) {
        headers.insertEnvoyDecoratorOperation().value(*decorated_operation_);
      }
    } else if (connection_manager_.config_.tracingConfig()-&gt;operation_name_ == Tracing::OperationName::Egress) {
      // 对于egress（outbound）响应
      const HeaderEntry* resp_operation_override = headers.EnvoyDecoratorOperation();
      // 如果已经提供x-envoy-decorator-operation，则覆盖当前Spance的operation值
      if (resp_operation_override) {
        if (!resp_operation_override-&gt;value().empty() &amp;&amp; active_span_) {
          active_span_-&gt;setOperation(resp_operation_override-&gt;value().c_str());
        }
        // 移除x-envoy-decorator-operation头，防止传播给服务
        headers.removeEnvoyDecoratorOperation();
      }
    }
  }

  // 进行统计指标收集
  chargeStats(headers);
  stream_info_.onFirstDownstreamTxByteSent();

  // 现在实际完成基于codec的响应头编码，生成、刷出响应。如果end_stream则endEncode()
  response_encoder_-&gt;encodeHeaders( headers, encoding_headers_only_ || (end_stream &amp;&amp; continue_data_entry == encoder_filters_.end()));
  if (continue_data_entry != encoder_filters_.end()) {
    // 调用之前中止迭代的过滤器的continueEncoding()方法，此方法不会重复调用encodeHeaders()
    // 仿冒的设置stopped_ since=true，原因是continueEncoding()要求如此
    ASSERT(buffered_response_data_);
    (*continue_data_entry)-&gt;stopped_ = true;
    (*continue_data_entry)-&gt;continueEncoding();
  } else {
    // 对于header-only响应 —— 不管是过滤器将其转换为header-only还是上游仅仅返回headers，结束响应编码的处理
    maybeEndEncode(encoding_headers_only_ || end_stream);
  }
}


void ConnectionManagerImpl::ActiveStream::maybeEndEncode(bool end_stream) {
  if (end_stream) {
    // 应当接受响应编码的处理
    // 增加日志信息
    stream_info_.onLastDownstreamTxByteSent();
    // 结束span
    request_response_timespan_-&gt;complete();
    // 处理由于上游响应或者reset导致应当结束的流
    connection_manager_.doEndStream(*this);
  }
}</pre>
<p>每个过滤器的<pre class="crayon-plain-tag">encodeHeaders(Http::HeaderMap&amp; headers, bool)</pre>方法会被调用，返回的Http::FilterHeadersStatus会影响响应头编码的后续处理流程。</p>
<div class="blog_h3"><span class="graybg">HTTP响应体编码</span></div>
<p>如果响应体缓冲区不为空，则需要在编码响应头后，继续处理响应体。响应体缓冲区的内容可能是由上游服务提供，也可能是由某个过滤器写入和修改。</p>
<p>响应体编码由HTTP连接管理器的ActiveStream::encodeData方法完成：</p>
<pre class="crayon-plain-tag">void ConnectionManagerImpl::ActiveStream::encodeData(ActiveStreamEncoderFilter* filter,  Buffer::Instance&amp; data, bool end_stream) {
  // 和编码响应头时一样，重置空闲定时器
  resetIdleTimer();

  // 如果先前已经设置此状态，则直接返回
  if (encoding_headers_only_) {
    return;
  }

  // 产生编码过滤器的列表
  std::list&lt;ActiveStreamEncoderFilterPtr&gt;::iterator entry = commonEncodePrefix(filter, end_stream);
  // 在何处开始encodeTrailers调用
  auto trailers_added_entry = encoder_filters_.end();

  // 是否在响应体编码之前，响应尾已经存在了
  const bool trailers_exists_at_start = response_trailers_ != nullptr;
  for (; entry != encoder_filters_.end(); entry++) {
    // 如果任何一个过滤器的end_stream_被标记，则意味着这个以及后续的过滤器不应该处理数据
    if ((*entry)-&gt;end_stream_) {
      return;
    }
    ASSERT(!(state_.filter_call_state_ &amp; FilterCallState::EncodeData));

    // 设置过滤器调用状态
    state_.filter_call_state_ |= FilterCallState::EncodeData;
    if (end_stream) {
      // 最后一个数据帧      
      state_.filter_call_state_ |= FilterCallState::LastDataFrame;
    }
    // 检查response_trailers_，应对前面的过滤器的encodeData()方法调用addEncodedTrailers()的情况
    // 如果前面的过滤器添加了响应尾，则通知当前、后续过滤器，流处理尚不能结束
    (*entry)-&gt;end_stream_ = end_stream &amp;&amp; !response_trailers_;
    // 调用过滤器进行响应体编码
    FilterDataStatus status = (*entry)-&gt;handle_-&gt;encodeData(data, (*entry)-&gt;end_stream_);
    // 重置过滤器调用状态
    state_.filter_call_state_ &amp;= ~FilterCallState::EncodeData;
    if (end_stream) {
      state_.filter_call_state_ &amp;= ~FilterCallState::LastDataFrame;
    }
    ENVOY_STREAM_LOG(trace, "encode data called: filter={} status={}", *this,
                     static_cast&lt;const void*&gt;((*entry).get()), static_cast&lt;uint64_t&gt;(status));

    // 迭代前没有没有响应尾，但是        现在有响应尾（某过滤器添加）
    if (!trailers_exists_at_start &amp;&amp; response_trailers_ &amp;&amp; trailers_added_entry == encoder_filters_.end()) {
      // 这设置为当前过滤器
      trailers_added_entry = entry;
    }
    // 消息体回调通用处理逻辑
    if (!(*entry)-&gt;commonHandleAfterDataCallback(status, data, state_.encoder_filters_streaming_)) {
      return;
    }
  }

  ENVOY_STREAM_LOG(trace, "encoding data via codec (size={} end_stream={})", *this, data.length(),  end_stream);
  // 日志信息
  stream_info_.addBytesSent(data.length());

  // 如果在encodeData期间添加了响应尾，则需要触发decodeTrailers，让过滤器有机会处理这些尾数据
  if (trailers_added_entry != encoder_filters_.end()) {
    response_encoder_-&gt;encodeData(data, false);
    encodeTrailers(trailers_added_entry-&gt;get(), *response_trailers_);
  } else {
    // 调用StreamEncoder进行实际的响应体写入，并刷出
    response_encoder_-&gt;encodeData(data, end_stream);
    maybeEndEncode(end_stream);
  }
}</pre>
<p>每个过滤器的<pre class="crayon-plain-tag">encodeData(Buffer::Instance&amp;, bool)</pre>方法会被调用，返回的Http::FilterHeadersStatus会影响响应体编码的后续处理流程。 </p>
<div class="blog_h1"><span class="graybg">Envoy发送请求过程</span></div>
<p>Istio使用的不是原版的Envoy，它在项目<a href="https://github.com/istio/proxy">istio/proxy</a>中对Envoy进行了扩展，并在构建时引用Envoy的某个特定Commit Id，构建出完整的、增强的Envoy二进制文件。</p>
<p>Istio对Envoy做的增强主要是引入若干自定义过滤器，Mixer的客户端功能就是依赖于过滤器实现的。 </p>
<div class="blog_h2"><span class="graybg">如何调试</span></div>
<div class="blog_h3"><span class="graybg">启动程序</span></div>
<p>如果要创建完全本地的调试环境，你需要签出两个项目并构建：</p>
<pre class="crayon-plain-tag"># istio
git clone https://github.com/istio/istio.git

# istio/proxy
git clone https://github.com/istio/proxy.git</pre>
<p> 通过上面的项目，启动Pilot Discovery、Pilot Agent、Mixer三个程序。Mixer的启动方式前文已经有说明，Pilot Discovery、Agent的启动方式可以参考<a href="https://blog.gmem.cc/interaction-between-istio-pilot-and-envoy">Istio Pilot与Envoy的交互机制解读</a>一文，需要注意的是，必须把binaryPath参数设置为istio/proxy项目构建出的envoy的路径。</p>
<pre class="crayon-plain-tag">pilot proxy sidecar  ... --binaryPath=/home/alex/CPP/projects/clion/istio/proxy/bazel-bin/src/envoy/envoy</pre>
<p>从istio/proxy构建出envoy时，注意保留调试符号。 </p>
<div class="blog_h3"><span class="graybg">调试Envoy</span></div>
<p>在Pilot Agent启动后，它会产生一个Envoy子进程。你可以用GDB连接到该进程，并在GDB控制台设置源码目录：</p>
<pre class="crayon-plain-tag">directory /home/alex/CPP/lib/libevent/2.1.8-stable
directory /home/alex/CPP/projects/clion/istio/proxy 
directory /home/alex/CPP/projects/clion/istio/proxy/bazel-proxy</pre>
<p>然后暂停程序执行，确保源码路径已经匹配上。 </p>
<div class="blog_h2"><span class="graybg">流量拦截</span></div>
<p>执行下面的命令，获取本地运行的Envoy代理的配置：</p>
<pre class="crayon-plain-tag">curl http://127.0.0.1:15000/config_dump</pre>
<p>可以看到，监听器virtual的端口是15001。假设我们想访问podinfo-canary.default.svc.k8s.gmem.cc在9898端口提供的服务，来了解Envoy代理的行为，可以先设置Iptables规则：</p>
<pre class="crayon-plain-tag"># 针对lo接口的请求不走PREROUTING链
iptables -t nat -A OUTPUT -p tcp -o lo --dport 9898 -j REDIRECT --to-port 15001</pre>
<p>然后，发起请求：</p>
<pre class="crayon-plain-tag">curl -H 'Host: podinfo-canary.default.svc.k8s.gmem.cc' http://127.0.0.1:9898/healthz</pre>
<p>此请求会触发Envoy的处理流程，包括对Mixer的L4、L7过滤器的调用。</p>
<div class="blog_h2"><span class="graybg">Check调用</span></div>
<p>Mixer过滤器处理HTTP请求头的逻辑如下：</p>
<pre class="crayon-plain-tag">FilterHeadersStatus Filter::decodeHeaders(HeaderMap&amp; headers, bool) {
  ENVOY_LOG(debug, "Called Mixer::Filter : {}", __func__);
  request_total_size_ += headers.byteSize();
  // 配置，包含目的服务信息
  ::istio::control::http::Controller::PerRouteConfig config;
  auto route = decoder_callbacks_-&gt;route();
  if (route) {
    ReadPerRouteConfig(route-&gt;routeEntry(), &amp;config);
  }
  // control是每个线程对应一个的控制对象
  // controller是Mixer控制器，以MixerFitlerConfig为参数，执行任务来保证对HTTP/TCP请求的控制
  // RequestHandler handler_，请求处理器，支持对Mixer服务器发起CHECK/REPORT调用
  handler_ = control_.controller()-&gt;CreateRequestHandler(config);

  state_ = Calling;
  initiating_call_ = true;
  // CheckData用于抽取额外的HTTP数据，供Mixer Check使用 —— 它持有HeaderMap、Envoy Metadata、网络连接等信息
  CheckData check_data(headers,
                       decoder_callbacks_-&gt;streamInfo().dynamicMetadata(),
                       decoder_callbacks_-&gt;connection());
  // HeaderUpdate用Istio属性来更新HTTP请求头
  Utils::HeaderUpdate header_update(&amp;headers);
  headers_ = &amp;headers;

  // Check调用相关逻辑：
  // 1、从客户端代理中抽取转发的属性
  // 2、从请求中抽取属性
  // 3、从配置中抽取属性
  // 4、如果有必要，将一部分属性转发给下游
  // 5、执行Check调用
  cancel_check_ = handler_-&gt;Check(
      &amp;check_data, &amp;header_update,
      // TransportCheckFunc 用于异步发起Check调用
      control_.GetCheckTransport(decoder_callbacks_-&gt;activeSpan()),
      // CheckDoneFunc 用于异步调用完成后处理CheckResponse
      [this](const CheckResponseInfo&amp; info) { completeCheck(info); });
  initiating_call_ = false;

  if (state_ == Complete) {
    return FilterHeadersStatus::Continue;
  }
  ENVOY_LOG(debug, "Called Mixer::Filter : {} Stop", __func__);
  return FilterHeadersStatus::StopIteration;
}</pre>
<p>从上面的代码我们可以看到，Mixer过滤器在处理下游请求头期间，会<span style="background-color: #c0c0c0;">异步的发起Check调用</span>：</p>
<pre class="crayon-plain-tag">CancelFunc RequestHandlerImpl::Check(CheckData* check_data,
                                     HeaderUpdate* header_update,
                                     TransportCheckFunc transport,
                                     CheckDoneFunc on_done) {
  // 添加转发的属性
  AddForwardAttributes(check_data);
  // 移除Istio属性 x-istio-attributes
  header_update-&gt;RemoveIstioAttributes();
  // 注入一个包含静态转发属性的头
  service_context_-&gt;InjectForwardedAttributes(header_update);

  if (!service_context_-&gt;enable_mixer_check()) {
    // 如果没有启动Check功能，直接以OK响应回调CheckDoneFunc
    CheckResponseInfo check_response_info;
    check_response_info.response_status = Status::OK;
    on_done(check_response_info);
    return nullptr;
  }

  // 添加Check相关属性
  AddCheckAttributes(check_data);

  // 根据Quota配置添加quota需求
  service_context_-&gt;AddQuotas(&amp;request_context_);
  // 异步发送Check调用
  return service_context_-&gt;client_context()-&gt;SendCheck(transport, on_done,
                                                       &amp;request_context_);
}</pre>
<p>此异步调用完成后，回调：</p>
<pre class="crayon-plain-tag">void Filter::completeCheck(const CheckResponseInfo&amp; info) {
  auto status = info.response_status;
  ENVOY_LOG(debug, "Called Mixer::Filter : check complete {}", status.ToString());
  // 流已经被重置，停止回调
  if (state_ == Responded) {
    return;
  }

  route_directive_ = info.route_directive;

  Utils::CheckResponseInfoToStreamInfo(info, decoder_callbacks_-&gt;streamInfo());

  // 处理来自路由指令的直接响应
  if (route_directive_.direct_response_code() != 0) {
    int status_code = route_directive_.direct_response_code();
    ENVOY_LOG(debug, "Mixer::Filter direct response {}", status_code);
    state_ = Responded;
    decoder_callbacks_-&gt;sendLocalReply(
        Code(status_code), route_directive_.direct_response_body(),
        [this](HeaderMap&amp; headers) {
          UpdateHeaders(headers, route_directive_.response_header_operations());
        },
        absl::nullopt);
    return;
  }

  // 如果状态不是OK，即使没有直接响应，也sendLocalReply
  if (!status.ok()) {
    state_ = Responded;

    int status_code = ::istio::utils::StatusHttpCode(status.error_code());
    decoder_callbacks_-&gt;sendLocalReply(Code(status_code), status.ToString(),
                                       nullptr, absl::nullopt);
    return;
  }

  // 将状态置为完成
  state_ = Complete;

  // 更新请求头
  if (nullptr != headers_) {
    UpdateHeaders(*headers_, route_directive_.request_header_operations());
    headers_ = nullptr;
    if (route_directive_.request_header_operations().size() &gt; 0) {
      decoder_callbacks_-&gt;clearRouteCache();
    }
  }

  if (!initiating_call_) {
    decoder_callbacks_-&gt;continueDecoding();
  }
}</pre>
<div class="blog_h2"><span class="graybg">Report调用</span></div>
<p>Report调用是延迟触发的，Mixer过滤器实现了Envoy::AccessLog::Instance（访问记录器），Report调用作为log方法逻辑的一部分。</p>
<p>Envoy在处理请求之后，可能会延迟的删除一些对象：</p>
<pre class="crayon-plain-tag">DispatcherImpl::DispatcherImpl(Buffer::WatermarkFactoryPtr&amp;&amp; factory, Api::Api&amp; api)
    : deferred_delete_timer_(createTimer([this]() -&gt; void { clearDeferredDeleteList(); })), 
      // 延迟删除定时器</pre>
<p>代表当前请求流的ActiveStream对象就是这样延迟删除的，删除时其析构函数被调用：</p>
<pre class="crayon-plain-tag">ConnectionManagerImpl::ActiveStream::~ActiveStream() {
  // ...
  // 遍历所有日志访问记录器
  for (const AccessLog::InstanceSharedPtr&amp; access_log : connection_manager_.config_.accessLogs()) {
    access_log-&gt;log(request_headers_.get(), response_headers_.get(), response_trailers_.get(),
                    stream_info_);
  }
  // ...
}</pre>
<p>可以看到，在ActiveStream析构时会调用所有访问日志记录器，包括Envoy::Http::Mixer::Filter::log：</p>
<pre class="crayon-plain-tag">void Filter::log(const HeaderMap* request_headers,
                 const HeaderMap* response_headers,
                 const HeaderMap* response_trailers,
                 const StreamInfo::StreamInfo&amp; stream_info) {
  ENVOY_LOG(debug, "Called Mixer::Filter : {}", __func__);
  if (!handler_) {
    if (request_headers == nullptr) {
      return;
    }
    // 可能因为请求被其它过滤器拒绝，Mixer过滤器没调用，因此handler尚未初始化
    ::istio::control::http::Controller::PerRouteConfig config;
    ReadPerRouteConfig(stream_info.routeEntry(), &amp;config);
    handler_ = control_.controller()-&gt;CreateRequestHandler(config);
  }

  // 如果没有调用check，则check属性没被抽取
  CheckData check_data(*request_headers, stream_info.dynamicMetadata(), decoder_callbacks_-&gt;connection());
  // ReportData提供接口，抽取HTTP属性，供Mixer Report调用使用
  ReportData report_data(response_headers, response_trailers, stream_info, request_total_size_);
  handler_-&gt;Report(&amp;check_data, &amp;report_data);
}</pre>
<p>Report方法的实现如下：</p>
<pre class="crayon-plain-tag">void RequestHandlerImpl::Report(CheckData* check_data,
                                ReportData* report_data) {
  if (!service_context_-&gt;enable_mixer_report()) {
    return;
  }

  // 添加转发的属性
  AddForwardAttributes(check_data);
  // 添加Check属性
  AddCheckAttributes(check_data);

  AttributesBuilder builder(&amp;request_context_);
  // 抽取Report属性
  builder.ExtractReportAttributes(report_data);

  // 发送Report请求
  service_context_-&gt;client_context()-&gt;SendReport(request_context_);
}</pre>
<p>发送Report请求的工作最终委托给::istio::mixerclient::MixerClient：</p>
<pre class="crayon-plain-tag">void ClientContextBase::SendReport(const RequestContext&amp; request) {
  mixer_client_-&gt;Report(*request.attributes);
}</pre>
<p>MixerClient包含批量处理的逻辑：</p>
<pre class="crayon-plain-tag">void MixerClientImpl::Report(const Attributes &amp;attributes) {
  report_batch_-&gt;Report(attributes);
}

void ReportBatch::Report(const Attributes&amp; request) {
  std::lock_guard&lt;std::mutex&gt; lock(mutex_);
  ++total_report_calls_;
  // 添加请求、压缩
  batch_compressor_-&gt;Add(request);
  // 如果超过批量限制，立即Report
  if (batch_compressor_-&gt;size() &gt;= options_.max_batch_entries) {
    FlushWithLock();
  } else {
    // 否则，延迟发送
    if (batch_compressor_-&gt;size() == 1 &amp;&amp; timer_create_) {
      if (!timer_) {
        timer_ = timer_create_([this]() { Flush(); });
      }
      timer_-&gt;Start(options_.max_batch_time_ms);
    }
  }
} </pre>
<div class="blog_h2"><span class="graybg">属性抽取 </span></div>
<p>MixerClient通过rRPC向Mixer服务器发送的是属性（Attributes），过滤器在调用MixerClient之前，会进行属性的抽取。</p>
<div class="blog_h3"><span class="graybg">Report属性 </span></div>
<pre class="crayon-plain-tag">void AttributesBuilder::ExtractReportAttributes(ReportData *report_data) {
  utils::AttributesBuilder builder(request_-&gt;attributes);

  std::string dest_ip;
  int dest_port;
  // 抽取 destination.ip
  if (report_data-&gt;GetDestinationIpPort(&amp;dest_ip, &amp;dest_port)) {
    if (!builder.HasAttribute(utils::AttributeName::kDestinationIp)) {
      builder.AddBytes(utils::AttributeName::kDestinationIp, dest_ip);
    }
    if (!builder.HasAttribute(utils::AttributeName::kDestinationPort)) {
      builder.AddInt64(utils::AttributeName::kDestinationPort, dest_port);
    }
  }

  std::string uid;
  // 抽取 destination.uid
  if (report_data-&gt;GetDestinationUID(&amp;uid)) {
    builder.AddString(utils::AttributeName::kDestinationUID, uid);
  }

  // 抽取 response.headers  所有响应头作为一个属性
  std::map&lt;std::string, std::string&gt; headers = report_data-&gt;GetResponseHeaders();
  builder.AddStringMap(utils::AttributeName::kResponseHeaders, headers);

  // 抽取 response.time
  builder.AddTimestamp(utils::AttributeName::kResponseTime, std::chrono::system_clock::now());

  ReportData::ReportInfo info;
  report_data-&gt;GetReportInfo(&amp;info);
  // 抽取 request.size
  builder.AddInt64(utils::AttributeName::kRequestBodySize, info.request_body_size);
  // 抽取 response.size
  builder.AddInt64(utils::AttributeName::kResponseBodySize, info.response_body_size);
  // 抽取 request.total_size
  builder.AddInt64(utils::AttributeName::kRequestTotalSize, info.request_total_size);
  // 抽取 response.total_size
  builder.AddInt64(utils::AttributeName::kResponseTotalSize, info.response_total_size);
  // 抽取 response.duration
  builder.AddDuration(utils::AttributeName::kResponseDuration, info.duration);

  // 抽取check属性
  if (!request_-&gt;check_status.ok()) {
    // 抽取 response.code
    builder.AddInt64(utils::AttributeName::kResponseCode, utils::StatusHttpCode(request_-&gt;check_status.error_code()));
    // 抽取 check.error_code
    builder.AddInt64(utils::AttributeName::kCheckErrorCode, request_-&gt;check_status.error_code());
    // 抽取 check.error_message
    builder.AddString(utils::AttributeName::kCheckErrorMessage, request_-&gt;check_status.ToString());
  } else {
    builder.AddInt64(utils::AttributeName::kResponseCode, info.response_code);
  }

  ReportData::GrpcStatus grpc_status;
  if (report_data-&gt;GetGrpcStatus(&amp;grpc_status)) {
    // 抽取 response.grpc_status
    builder.AddString(utils::AttributeName::kResponseGrpcStatus,  grpc_status.status);
    // 抽取 response.grpc_message
    builder.AddString(utils::AttributeName::kResponseGrpcMessage, grpc_status.message);
  }

  builder.AddString(utils::AttributeName::kContextProxyErrorCode, info.response_flags);

  ReportData::RbacReportInfo rbac_info;
  if (report_data-&gt;GetRbacReportInfo(&amp;rbac_info)) {
    if (!rbac_info.permissive_resp_code.empty()) {
      // 抽取 context.proxy_error_code
      builder.AddString(utils::AttributeName::kRbacPermissiveResponseCode, rbac_info.permissive_resp_code);
    }
    if (!rbac_info.permissive_policy_id.empty()) {
      // 抽取 rbac.permissive.effective_policy_id"
      builder.AddString(utils::AttributeName::kRbacPermissivePolicyId, rbac_info.permissive_policy_id);
    }
  }

  builder.FlattenMapOfStringToStruct(report_data-&gt;GetDynamicFilterState());
}</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/interaction-between-istio-mixer-and-envoy">Istio Mixer与Envoy的交互机制解读</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/interaction-between-istio-mixer-and-envoy/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Istio Pilot与Envoy的交互机制解读</title>
		<link>https://blog.gmem.cc/interaction-between-istio-pilot-and-envoy</link>
		<comments>https://blog.gmem.cc/interaction-between-istio-pilot-and-envoy#comments</comments>
		<pubDate>Fri, 25 Jan 2019 03:37:18 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C++]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[PaaS]]></category>
		<category><![CDATA[ServiceMesh]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=24743</guid>
		<description><![CDATA[<p>前言 在基于Istio+Envoy实现的服务网格中，Istio的角色是控制平面，它是实现了Envoy的发现协议集xDS的管理服务器端。Envoy本身则作为网格的数据平面，和Istio通信，获得各种资源的配置并更新自身的代理规则。 除了实现xDS协议，Istio还负责： Envoy统计数据的收集，从Statd格式转换为Prometheus格式。（注：目前看来Envoy也直接暴露了Prometheus的Exporter） 限速服务、策略服务 和第三方Tracer的对接 数字证书分发 等功能。这些功能都需要Istio和Envoy的协同才能生效。最基础和关键的协同是Istio组件Pilot和Envoy之间基于xDS协议进行的各种Envoy配置信息的推送。 Istio的文档并没有对Istio Pilot和Envoy如何交互进行描述，本文结合Istio、Envoy的源码来探讨这些细节。 model包分析 Pilot的model包为很多Pilot抽象创建了模型（结构），并定义了它们支持的操作。注意这里建模的是Pilot的抽象，因此名词Service是指Istio的抽象服务，而非K8S的Service或者Envoy的Cluster。 config.go 对Istio的配置信息、配置存储进行建模。 Config 代表一个Istio配置单元： [crayon-69e5e23522675390177193/] ConfigMeta 配置的元数据： [crayon-69e5e23522679526993276/] ConfigStore 定义一组平台无关的，但是底层平台（例如K8S）必须支持的API，通过这些API可以存取Istio配置信息 <a class="read-more" href="https://blog.gmem.cc/interaction-between-istio-pilot-and-envoy">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/interaction-between-istio-pilot-and-envoy">Istio Pilot与Envoy的交互机制解读</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">前言</span></div>
<p>在基于Istio+Envoy实现的服务网格中，Istio的角色是控制平面，它是实现了Envoy的发现协议集xDS的管理服务器端。Envoy本身则作为网格的数据平面，和Istio通信，获得各种资源的配置并更新自身的代理规则。</p>
<p>除了实现xDS协议，Istio还负责：</p>
<ol>
<li>Envoy统计数据的收集，从Statd格式转换为Prometheus格式。（注：目前看来Envoy也直接暴露了Prometheus的Exporter）</li>
<li>限速服务、策略服务</li>
<li>和第三方Tracer的对接</li>
<li>数字证书分发</li>
</ol>
<p>等功能。这些功能都需要Istio和Envoy的协同才能生效。最基础和关键的协同是Istio组件Pilot和Envoy之间基于xDS协议进行的各种Envoy配置信息的推送。</p>
<p>Istio的文档并没有对Istio Pilot和Envoy如何交互进行描述，本文结合Istio、Envoy的源码来探讨这些细节。</p>
<div class="blog_h1"><span class="graybg">model包分析</span></div>
<p>Pilot的model包为很多<span style="background-color: #c0c0c0;">Pilot抽象</span>创建了模型（结构），并定义了它们支持的操作。注意这里建模的是Pilot的抽象，因此名词Service是指Istio的抽象服务，而非K8S的Service或者Envoy的Cluster。</p>
<div class="blog_h2"><span class="graybg">config.go</span></div>
<p>对Istio的配置信息、配置存储进行建模。</p>
<div class="blog_h3"><span class="graybg">Config</span></div>
<p>代表一个Istio配置单元：</p>
<pre class="crayon-plain-tag">type Config struct {
	ConfigMeta
	// 配置内容以Proto消息的形式存储
	Spec proto.Message
}</pre>
<div class="blog_h3"><span class="graybg">ConfigMeta</span></div>
<p>配置的元数据：</p>
<pre class="crayon-plain-tag">type ConfigMeta struct {
	//匹配内容消息类型的短类型名称，例如route-rule
	Type string 
	// API组和版本
	Group string 
	Version string 
	// 命名空间范围内唯一性名称
	Name string 
	// 命名空间
	Namespace string 
	// FQDN后缀
	Domain string 
	// 标签集
	Labels map[string]string 
	// 注解集
	Annotations map[string]string 
	// 资源版本，跟踪对配置注册表的变更
	ResourceVersion string 
	CreationTimestamp meta_v1.Time 
}</pre>
<div class="blog_h3"><span class="graybg">ConfigStore</span></div>
<p>定义一组平台无关的，但是底层平台（例如K8S）必须支持的API，通过这些API可以存取Istio配置信息</p>
<p>每个配置信息的键，由type + name + namespace的组合构成，确保每个配置具有唯一的键</p>
<p>写操作是异步执行的，也就是说Update后立即Get可能无法获得最新结果。有资源版本判断资源是否更新</p>
<p>此接口返回的引用，仅支持只读操作，对其修改存在线程安全问题</p>
<pre class="crayon-plain-tag">type ConfigStore interface {
    // 返回配置描述符，其实就是[]ProtoSchema类型，ProtoSchema描述了资源的Group/Version/Type等信息
    ConfigDescriptor() ConfigDescriptor
    Get(typ, name, namespace string) (config *Config, exists bool)
    List(typ, namespace string) ([]Config, error)
    Create(config Config) (revision string, err error)
    Update(config Config) (newRevision string, err error)
    Delete(typ, name, namespace string) error
}</pre>
<div class="blog_h3"><span class="graybg">ConfigStoreCache</span></div>
<p>表示ConfigStore的本地完整复制的缓存，此缓存主动和远程存储保持同步，并且在获取更新时提供提供通知机制。</p>
<p>为了获得通知，事件处理器必须在Run之前注册，缓存需要在Run之后有一个初始的同步延迟。</p>
<pre class="crayon-plain-tag">type ConfigStoreCache interface {
        // CRUD接口
    ConfigStore
    // 添加某种配置类型的事件处理器
    RegisterEventHandler(typ string, handler func(Config, Event))
    Run(stop &lt;-chan struct{})
    // 初始缓存同步完毕后返回true
    HasSynced() bool
}</pre>
<div class="blog_h3"><span class="graybg">IstioConfigStore</span></div>
<p>此接口扩展ConfigStore，增加一些针对Istio资源的操控接口：</p>
<pre class="crayon-plain-tag">type IstioConfigStore interface {
	ConfigStore
	// 列出ServiceEntry
	ServiceEntries() []Config
	// 列出绑定到指定工作负载标签的Gateway
	Gateways(workloadLabels LabelsCollection) []Config
	// 列出绑定到指定工作负载标签EnvoyFilter
	EnvoyFilter(workloadLabels LabelsCollection) *Config
	// 列出关联到指定目标服务实例的Mixerclient HTTP API Specs
	HTTPAPISpecByDestination(instance *ServiceInstance) []Config
	// 列出关联到指定目标服务实例的Mixerclient quota specifications
	QuotaSpecByDestination(instance *ServiceInstance) []Config
	// 列出关联到指定服务+端口的身份验证策略
	// 如果存在多个不同范围（全局、命名空间、服务）的策略，最精确的那个被返回。如果同一范围有多个策略，返回第一个
	AuthenticationPolicyByDestination(service *Service, port *Port) *Config
	// 列出指定命名空间的ServiceRoles
	ServiceRoles(namespace string) []Config
	// 列出指定命名空间的ServiceRoleBindings
	ServiceRoleBindings(namespace string) []Config
	// 列出名字为DefaultRbacConfigName的RbacConfig
	RbacConfig() *Config
}</pre>
<div class="blog_h2"><span class="graybg">context.go</span></div>
<div class="blog_h3"><span class="graybg">Environment</span></div>
<p>此结构为Pilot提供聚合的环境性的API：</p>
<pre class="crayon-plain-tag">type Environment struct {
	// 内嵌接口：用于列出服务、实例
	ServiceDiscovery

	// 已经废弃，使用 PushContext.ServiceAccounts
	ServiceAccounts

	// 内嵌接口：用于列出路由规则
	IstioConfigStore

	// 网格配置信息
	Mesh *meshconfig.MeshConfig

	// 用于和Mixer通信
	MixerSAN []string

	// 全局的推送上下文，已经废弃
	// 除非出于测试、处理新连接的目的，不要使用此字段
	PushContext *PushContext
}</pre>
<div class="blog_h3"><span class="graybg">Proxy</span></div>
<p>此结构建模代理（Envoy代理）的属性，xDS使用此结构对代理进行识别：</p>
<pre class="crayon-plain-tag">type Proxy struct {
	// 此代理所在的集群
	ClusterID string
	// 节点类型（也就是说运行代理的那个Pod的代理角色
	Type NodeType
	// 用于识别代理以及它的同地协作的服务实例的IP地址
	IPAddress string
	// 平台先关的Sidecar代理ID
	ID string
	// 短主机名的DNS后缀
	Domain string
	// 节点的元数据
	Metadata map[string]string
}</pre>
<div class="blog_h3"><span class="graybg">NodeType</span></div>
<p>用于区分不同代理在网格中的职责。</p>
<pre class="crayon-plain-tag">type NodeType string
const (
	// 应用程序容器的边车代理，普通被网格管理的Pod使用这种代理角色
	Sidecar NodeType = "sidecar"
	// 独立运行的，集群入口代理，istio-ingress中运行的是这种代理
	Ingress NodeType = "ingress"
	// 独立运行的，作为L7/L4路由器的代理，istio-ingressgateway、istio-egressgateway中运行的是这种代理
	Router NodeType = "router"
)</pre>
<div class="blog_h2"><span class="graybg">push_context.go</span></div>
<div class="blog_h3"><span class="graybg">EndpointShard</span></div>
<p>端点分片，存储<span style="background-color: #c0c0c0;">单个服务</span>的<span style="background-color: #c0c0c0;">单个注册表</span>中的<span style="background-color: #c0c0c0;">单个分片</span>的名称及其端点列表：</p>
<pre class="crayon-plain-tag">type EndpointShard struct {
	Shard   string
	Entries []*IstioEndpoint
}</pre>
<div class="blog_h3"><span class="graybg">EndpointShardsByService</span></div>
<p>存储<span style="background-color: #c0c0c0;">单个服务的所有分片</span>信息。使用K8S作为注册表时，Shards通常只有一个元素，其键是"Kubernetes"，其值是Shard名为"Kubernetes"的EndpointShard</p>
<pre class="crayon-plain-tag">type EndpointShardsByService struct {
	// 这种结构下，每个注册表只能有一个分片
	// 映射的键是注册表名称
	Shards map[string]*EndpointShard
	ServiceAccounts map[string]bool
}</pre>
<div class="blog_h3"><span class="graybg">IstioEndpoint</span></div>
<p>此结构用于代替NetworkEndpoint和ServiceInstance，做了以下优化：</p>
<ol>
<li>ServicePortName字段代替ServicePort字段。原因是进行了端点回调（endpoint callbacks are made）时端口号、协议可能不可用</li>
<li>合并两个结构，原因是一对一关系</li>
<li>不再持有Service的指针。原因是接收到端点时，服务对象可能不可用</li>
<li>提供缓存的EnvoyEndpoint对象，避免为每次请求/每个客户端重新分配</li>
</ol>
<pre class="crayon-plain-tag">type IstioEndpoint struct {
	// 工作负载的标签
	Labels map[string]string
	Family AddressFamily
	Address string
	EndpointPort uint32
	// 跟踪端口的名称，避免最终一致性相关的问题。某些情况下Endpoint先于Service可见，这时进行端口查找会失败
	// 端口名到号的映射将在集群计算时进行
	ServicePortName string
	// 用于遥测
	UID string
	// 缓存的LbEndpoint（来自Envoy Go客户端包），通过数据转换得到，避免重复计算
	EnvoyEndpoint *endpoint.LbEndpoint
	ServiceAccount string
}</pre>
<div class="blog_h3"><span class="graybg">PushContext</span></div>
<p><a href="#PushContext">参考下文</a>。</p>
<div class="blog_h2"><span class="graybg">service.go</span></div>
<div class="blog_h3"><span class="graybg">Service</span></div>
<p>此结构对Istio服务进行建模，每个服务具有全限定的名称（FQDN），一个或多个监听的端口，一个可选的和服务关联的负载均衡器/虚拟IP地址（FQDN解析到此地址）。</p>
<p>例如，在K8S中，服务kubernetes关联到FQDN kubernetes.default.svc.cluster.local，具有虚拟IP地址10.96.0.1，监听443端口。</p>
<pre class="crayon-plain-tag">// 主机名，可能是通配符
type Hostname string

type Service struct {
	// 主机名
	Hostname Hostname 

	// 服务的负载均衡器IPv4地址
	Address string 

	// 多集群支持，服务在每个集群中的负载均衡器IPv4地址
	ClusterVIPs map[string]string 

	// 监听的端口列表
	Ports PortList 

	// 运行服务的账号
	ServiceAccounts []string 

	// 指示服务是否位于网格外部，这种服务通过ServiceEntry定义
	MeshExternal bool

	// 在路由之前，如何解析服务的实例
	Resolution Resolution

	// 服务创建时间
	CreationTime time.Time 

	// 额外的属性，Mixer/RBAC 策略会用到
	Attributes ServiceAttributes
}</pre>
<div class="blog_h3"><span class="graybg">Resolution</span></div>
<p>用于指示在路由请求之前，如何解析出服务的实例：</p>
<pre class="crayon-plain-tag">type Resolution int

const (
	// 代理根据自己本地的负载均衡池决定使用哪个端点
	ClientSideLB Resolution = iota
	// 代理进行DNS解析，并把请求发给解析结果
	DNSLB
	// 代理直接根据请求者指定的目的地址
	Passthrough
)</pre>
<div class="blog_h3"><span class="graybg">ServiceInstance</span></div>
<p>服务的特定版本的一个实例，绑定到一个NetworkEndpoint：</p>
<pre class="crayon-plain-tag">type ServiceInstance struct {
	// 关联的端点
	Endpoint         NetworkEndpoint 
	// 所属的服务
	Service          *Service        
	// 标签集
	Labels           Labels          
	AvailabilityZone string          
	ServiceAccount   string          
}</pre>
<div class="blog_h3"><span class="graybg">NetworkEndpoint</span></div>
<p>建模<span style="background-color: #c0c0c0;">关联到服务的实例</span>的网络地址：</p>
<pre class="crayon-plain-tag">type NetworkEndpoint struct {
	// 地址族
	Family AddressFamily
	Address string
	Port int
	ServicePort *Port
	UID string
}</pre>
<div class="blog_h3"><span class="graybg">Port</span></div>
<p>对服务监听的网络端口进行建模：</p>
<pre class="crayon-plain-tag">type Port struct {
	// 易读的端口名，如果服务包含多个端口，则此字段必须
	Name string 

	// 服务的端口号，非必须关联到服务背后的实例的端口
	Port int 

	// 使用的协议
	Protocol Protocol 
}

// 端口集
type PortList []*Port</pre>
<div class="blog_h3"><span class="graybg">Protocol</span></div>
<p>通信协议：</p>
<pre class="crayon-plain-tag">type Protocol string
// 目前支持的协议枚举
const (
	ProtocolGRPC Protocol = "GRPC"
	ProtocolHTTP Protocol = "HTTP"
	ProtocolHTTP2 Protocol = "HTTP2"
	ProtocolHTTPS Protocol = "HTTPS"
	ProtocolTCP Protocol = "TCP"
	ProtocolTLS Protocol = "TLS"
	ProtocolUDP Protocol = "UDP"
	ProtocolMongo Protocol = "Mongo"
	ProtocolRedis Protocol = "Redis"
	ProtocolUnsupported Protocol = "UnsupportedProtocol"
)</pre>
<div class="blog_h3"><span class="graybg">TrafficDirection</span></div>
<p>流量的方向：</p>
<pre class="crayon-plain-tag">type TrafficDirection string
const (
	TrafficDirectionInbound TrafficDirection = "inbound"
	TrafficDirectionOutbound TrafficDirection = "outbound"
)</pre>
<div class="blog_h3"><span class="graybg">ServiceDiscovery</span></div>
<p>此接口用于发现服务的实例：</p>
<pre class="crayon-plain-tag">type ServiceDiscovery interface {
	// 列出所有服务
	Services() ([]*Service, error)

	// 废弃，根据主机名获得服务
	GetService(hostname Hostname) (*Service, error)

	// 取回服务的、匹配指定标签集的实例
	Instances(hostname Hostname, ports []string, labels LabelsCollection) ([]*ServiceInstance, error)
	InstancesByPort(hostname Hostname, servicePort int, labels LabelsCollection) ([]*ServiceInstance, error)

	// 返回和指定代理同地协作（co-located）的实例，所谓co-located是指运行在相同的命名空间和安全上下文
	//
	// 对于以Sidecar方式运行的代理，返回非空的切片；对于独立运行的代理，返回空切片
	GetProxyServiceInstances(*Proxy) ([]*ServiceInstance, error)

	// 返回一个IPv4地址关联的管理端口
	ManagementPorts(addr string) PortList

	// 返回一个IPv4地址关联的健康检查探针
	WorkloadHealthCheckInfo(addr string) ProbeList
}</pre>
<div class="blog_h1"><span class="graybg">发现服务启动过程</span></div>
<p><span style="background-color: #c0c0c0;">Pilot（Pilot Discovery，其对应的客户端组件是Pilot Agent）</span>是Istio最关键的组件，它的职责是将用户提供的、简单的、CRD形式的配置文件，转换为Envoy能理解的格式，并推送给Envoy以更新代理配置。</p>
<p>Pilot的启动逻辑位于bootstrap包中。</p>
<div class="blog_h2"><span class="graybg">如何调试</span></div>
<p>我们需要进行单步跟踪，才能了解Pilot在初始化期间做了哪些事情。为了调试的方便，我们在K8S集群外部启动Pilot服务。参考下面的启动参数：</p>
<pre class="crayon-plain-tag">export POD_NAME=istio-pilot-54f79f8bd7-w8b2g
export POD_NAMESPACE=istio-system

# pilot-discovery的入口点代码位于 pilot/cmd/pilot-discovery/main.go

                # 输出详尽日志
pilot discovery --log_output_level=default:debug --log_caller=default --domain=k8s.gmem.cc \
                # 提供kubeconfig，注意不支持--masterUrl，不提供此参数Istio会假设在集群内部运行
                --kubeconfig=/home/alex/.kube/config \
                # 配置文件
                --meshConfig=pilot/meshConfig</pre>
<p>其中meshConfig可以直接从K8S集群中提取，此配置存放在Configmap中，名称为istio-system/istio。内容如下：</p>
<pre class="crayon-plain-tag">disablePolicyChecks: false
enableTracing: true
accessLogFile: "/dev/stdout"
policyCheckFailOpen: false
sdsUdsPath: ""
sdsRefreshDelay: 15s
defaultConfig:
  connectTimeout: 10s
  configPath: "/etc/istio/proxy"
  binaryPath: "/usr/local/bin/envoy"
  serviceCluster: istio-proxy
  drainDuration: 45s
  parentShutdownDuration: 1m0s
  proxyAdminPort: 15000
  concurrency: 0
  zipkinAddress: zipkin.istio-system:9411
  controlPlaneAuthPolicy: NONE
  discoveryAddress: istio-pilot.istio-system:15007</pre>
<div class="blog_h2"><span class="graybg">启动过程概述</span></div>
<p>Pilot的入口点位于pilot/cmd/pilot-discovery/main.go，它使用包<a href="https://github.com/spf13/cobra">spf13.cobra</a>来管理一组子命令，Pilot核心功能由discovery子命令实现。</p>
<p>入口点的启动过程如下：</p>
<ol>
<li>解析命令行参数</li>
<li>创建主服务，服务建模在结构bootstrap.Server中
<ol>
<li>调用initKubeClient方法，初始化K8S客户端</li>
<li>调用initClusterRegistries方法，初始化clusterStore字段</li>
<li>调用initMesh方法，初始化mesh字段</li>
<li>调用initMixerSan方法，初始化Mixer服务的SAN</li>
<li>调用initConfigController方法，初始化configController字段。通过addStartFunc延后调用配置控制器的Run方法</li>
<li>如果启用了Istio的IngressController功能，则调用configaggregate.MakeCache，包装configController，使其能处理Ingress类型的资源</li>
<li>调用initServiceControllers方法，此控制器能够从底层服务发现机制中获取Istio抽象服务。通过addStartFunc延后调用服务控制器的Run方法</li>
<li>调用initDiscoveryService方法，初始化发现服务，发现服务依赖于前面创建的配置控制器、服务控制器，对xDS的支持有发现服务提供</li>
<li>调用initMonitor方法，初始化Pilot监控服务</li>
<li>调用initMultiClusterController方法，初始化多集群控制器，目前可以用于跨越多个K8S集群的服务网格</li>
<li>启动ControlZ监听器</li>
</ol>
</li>
<li>启动主服务，其实就是执行先前注册的延迟启动函数：
<ol>
<li>启动配置控制器</li>
<li>启动Ingress同步器</li>
<li>启动Service控制器</li>
<li>启动HTTP服务、gRPC服务、安全gRPC服务</li>
</ol>
</li>
<li>等待停止信号</li>
</ol>
<div class="blog_h2"><span class="graybg">bootstrap.Server</span></div>
<p>表示Pilot主服务，它不是一个简单的（侦听单个端口）服务，而是很多服务的集合。包含的字段如下：</p>
<pre class="crayon-plain-tag">type Server struct {
	// xDS服务管理服务器的实现
	EnvoyXdsServer    *envoyv2.DiscoveryServer
	ServiceController *aggregate.Controller
        // 网格配置
	mesh             *meshconfig.MeshConfig
        // 配置控制器，负责监控K8S集群中，Istio的自定义资源的变更
	configController model.ConfigStoreCache
        // Mixer服务的SAN列表
	mixerSAN         []string
        // K8S客户端
	kubeClient       kubernetes.Interface
        // 启动需要执行的函数列表
	startFuncs       []startFunc
        // 集群信息存储
	clusterStore     *clusterregistry.ClusterStore
	httpServer       *http.Server
	grpcServer       *grpc.Server
	secureGRPCServer *grpc.Server
        // 发现服务，支持xDS协议
	discoveryService *envoy.DiscoveryService
        // 包装configController，提供针对Gateways、EnvoyFilter、ServiceEntries等资源的强类型接口
	istioConfigStore model.IstioConfigStore
        // HTTP请求多路分发器，根据URL匹配来决定由哪个handler处理请求
	mux              *http.ServeMux
        // 监控各种K8S原生对象，并将更新推送给EDS等组件
	kubeRegistry     *kube.Controller
}</pre>
<div class="blog_h2"><span class="graybg">initKubeClient</span></div>
<p>仅仅当args.Service.Registries包含Kubernetes，才会创建K8S客户端：</p>
<pre class="crayon-plain-tag">for _, r := range args.Service.Registries {
	if serviceregistry.ServiceRegistry(r) == serviceregistry.KubernetesRegistry {
		needToCreateClient = true
		break
	}
}</pre>
<div class="blog_h2"><span class="graybg">initClusterRegistries</span></div>
<p>仅当创建了K8S客户端，才会调用此方法。</p>
<p>此方法创建的对象很简单，就是创建一个空的clusterregistry.ClusterStore：</p>
<pre class="crayon-plain-tag">type ClusterStore struct {
	rc        map[string]*RemoteCluster
	storeLock sync.RWMutex
}</pre><br />
<pre class="crayon-plain-tag">type RemoteCluster struct {
	Cluster        *k8s_cr.Cluster
	FromSecret     string
	Client         *clientcmdapi.Config
	ClusterStatus  string
	Controller     *kube.Controller
	ControlChannel chan struct{}
}</pre>
<div class="blog_h2"><span class="graybg">initMixerSan</span></div>
<p>Istio组件Mixer和Pilot需要相互通信，如果mesh.DefaultConfig.ControlPlaneAuthPolicy为mTLS，也就是说Mixer - Pilot通信需要双向TLS认证时，才调用此方法：</p>
<pre class="crayon-plain-tag">func (s *Server) initMixerSan(args *PilotArgs) error {
	if s.mesh.DefaultConfig.ControlPlaneAuthPolicy == meshconfig.AuthenticationPolicy_MUTUAL_TLS {
		s.mixerSAN = envoy.GetMixerSAN(args.Config.ControllerOptions.DomainSuffix, args.Namespace)
	}
	return nil
}</pre>
<div class="blog_h2"><span class="graybg">initConfigController</span></div>
<p>此方法转调makeKubeConfigController方法创建配置控制器：</p>
<pre class="crayon-plain-tag">func (s *Server) makeKubeConfigController(args *PilotArgs) (model.ConfigStoreCache, error) {
	kubeCfgFile := s.getKubeCfgFile(args)
        // crd.Client，负责Istio自定义资源的CRUD操作
        // 为每组API的每个版本（apiVersion）创建独立的REST客户端
        // apiVersion包括networking.istio.io/v1alpha3、config.istio.io/v1alpha2、authentication.istio.io/v1alpha1、rbac.istio.io/v1alpha1
	configClient, err := crd.NewClient(kubeCfgFile, "", model.IstioConfigTypes, args.Config.ControllerOptions.DomainSuffix)
        // 注册CRD
	if !args.Config.DisableInstallCRDs {
		if err = configClient.RegisterResources(); err != nil {
			return nil, multierror.Prefix(err, "failed to register custom resources.")
		}
	}
        // 创建配置控制器
	return crd.NewController(configClient, args.Config.ControllerOptions), nil
}</pre>
<div class="blog_h3"><span class="graybg">crd.controller</span></div>
<p>initConfigController方法返回值的真实类型是crd.controller，除了实现ConfigStoreCache 接口的方法以外，它还负责管理Informer。它为每种CR创建一个Informer：</p>
<pre class="crayon-plain-tag">for _, schema := range client.ConfigDescriptor() {
	out.addInformer(schema, options.WatchedNamespace, options.ResyncPeriod)
}</pre>
<p>Pilot中的控制器的工作模式基本都是一样的，<a href="#resources-processing-framework">下文</a>我们详细分析。</p>
<div class="blog_h3"><span class="graybg">ingress.controller</span></div>
<p>如果必要（允许Istio处理Ingress资源的话），initConfigController还会创建IngressController：</p>
<pre class="crayon-plain-tag">// 下面的方法创建了一个监听Ingress资源的控制器
ingress.NewController(s.kubeClient, s.mesh, args.Config.ControllerOptions)</pre>
<div class="blog_h3"><span class="graybg">config/aggregate</span></div>
<p>包config/aggregate可以将处理不同资源类型的model.ConfigStoreCache组合起来，形成一个更大的model.ConfigStoreCache。接口保持不变，根据资源类型委托给适当的被包装的子对象。</p>
<p>默认的配置下，Pilot会把Config控制器、Ingress控制器组合起来：</p>
<pre class="crayon-plain-tag">configController, err := configaggregate.MakeCache([]model.ConfigStoreCache{
	s.configController,
	ingress.NewController(s.kubeClient, s.mesh, args.Config.ControllerOptions),
})</pre>
<div class="blog_h2"><span class="graybg">Istio作为Ingress控制器</span></div>
<p>Istio网格的入口通常是Gateway，但是它也能够处理K8S原生的Ingress资源，也就是说Istio可以扮演K8S通常意义上的Ingress Controller。实际<span style="background-color: #c0c0c0;">上入口流量由istio-ingress这个deployment的Pod处理</span>。</p>
<p>哪些Ingress资源会被管理，由主服务的mesh.IngressControllerMode配置决定：</p>
<ol>
<li>MeshConfig_OFF（0）：禁用Istio的IngressController功能</li>
<li>MeshConfig_DEFAULT（1）：作为整个K8S集群默认的Ingress控制器</li>
<li>MeshConfig_STRICT（2）：仅仅处理包含了注解kubernetes.io/ingress.class，且值等于mesh.IngressClass（默认istio）的Ingress资源</li>
</ol>
<p>创建IngressController的逻辑位于initConfigController方法中。</p>
<div class="blog_h2"><span class="graybg">initServiceControllers</span></div>
<p>此方法首先创建一个聚合控制器：</p>
<pre class="crayon-plain-tag">serviceControllers := aggregate.NewController()</pre>
<p>serviceregistry/aggregate中定义的“聚合控制器”，能够管理多个平台（例如K8S）的服务发现机制（ServiceRegistry）。</p>
<p>然后，它调用createK8sServiceControllers方法创建kube.Controller：</p>
<pre class="crayon-plain-tag">func (s *Server) createK8sServiceControllers(serviceControllers *aggregate.Controller, args *PilotArgs) (err error) { 
	kubectl := kube.NewController(s.kubeClient, args.Config.ControllerOptions) // 创建控制器
	s.kubeRegistry = kubectl // 赋值给主服务的kubeRegistry字段
	serviceControllers.AddRegistry( // 添加到聚合控制器的registries中
		aggregate.Registry{
			Name:             serviceregistry.KubernetesRegistry, // 服务注册表类型
			ClusterID:        string(serviceregistry.KubernetesRegistry), // 如果多个同一类型的注册表存在则有意义
			ServiceDiscovery: kubectl, // 服务发现，可获得Istio的抽象服务（ServiceInstance）
			ServiceAccounts:  kubectl, // 暴露Istio的抽象服务账号（字符串）
			Controller:       kubectl, // 控制器，可通知Service、ServiceInstance的变更
		}
	)
}</pre>
<p>然后，创建ServiceEntry发现服务，并添加到聚合控制器：</p>
<pre class="crayon-plain-tag">serviceEntryStore := external.NewServiceDiscovery(s.configController, s.istioConfigStore)
serviceEntryRegistry := aggregate.Registry{
	Name:             "ServiceEntries",
	Controller:       serviceEntryStore,
	ServiceDiscovery: serviceEntryStore,
	ServiceAccounts:  serviceEntryStore,
}
serviceControllers.AddRegistry(serviceEntryRegistry)</pre>
<p>最后，注册ServiceController的延迟启动。</p>
<div class="blog_h3"><span class="graybg">kube.Controller</span></div>
<p>kube.Controller能够监控K8S中service、endpoint、node、pod等对象。它实现了model.Controller、model.ServiceDiscovery、model.ServiceAccounts等接口。</p>
<p>该结构包含以下字段：</p>
<pre class="crayon-plain-tag">type Controller struct {
	domainSuffix string
	client    kubernetes.Interface
	queue     Queue   // 事件队列
	services  cacheHandler  // 各种K8S对象的处理器
	endpoints cacheHandler
	nodes     cacheHandler
	pods *PodCache 
	// 允许此控制器读取环境信息，并发布状态信息
	Env *model.Environment
	// 多集群环境下识别集群
	ClusterID string
	// XDSUpdater推送EDS变更到ADS模型
	EDSUpdater model.XDSUpdater
	// 用于请求全局配置的变更
	ConfigUpdater model.ConfigUpdater
	stop chan struct{}
}</pre>
<div class="blog_h2"><span class="graybg"><a id="initDiscoveryService"></a>initDiscoveryService</span></div>
<p>此方法首先创建一个新的发现服务：</p>
<pre class="crayon-plain-tag">discovery, err := envoy.NewDiscoveryService(
	s.ServiceController,
	s.configController,
	environment,  // 提供聚合性的上下文信息API
	args.DiscoveryOptions, // 监听地址等信息
)

s.discoveryService = discovery
s.mux = s.discoveryService.RestContainer.ServeMux</pre>
<p>结构envoy.DiscoveryService是真正的发现服务，是Pilot的核心。它负责推送配置给Envoy。</p>
<p>然后，创建一个gRPC服务器、一个HTTP服务器：</p>
<pre class="crayon-plain-tag">s.initGrpcServer()

s.httpServer = &amp;http.Server{
	Addr:    args.DiscoveryOptions.HTTPAddr,
	Handler: discovery.RestContainer}</pre>
<p>创建xDS服务：</p>
<pre class="crayon-plain-tag">// 基于Envoy协议v2版本
s.EnvoyXdsServer = envoyv2.NewDiscoveryServer(environment, istio_networking.NewConfigGenerator(args.Plugins))
// 用于将envoy/v2从envoy/解耦
s.EnvoyXdsServer.ConfigUpdater = s.discoveryService</pre>
<p>注册envoy包的全局函数：</p>
<pre class="crayon-plain-tag">// 当配置变更时，请求一个更新
envoy.Push = s.EnvoyXdsServer.Push
envoy.BeforePush = s.EnvoyXdsServer.BeforePush</pre>
<p>将xDS服务注册到上述gRPC服务器上：</p>
<pre class="crayon-plain-tag">s.EnvoyXdsServer.Register(s.grpcServer)</pre>
<p>在使用K8S作为后端时，还会配置kube.Controller：</p>
<pre class="crayon-plain-tag">s.kubeRegistry.Env = environment
s.kubeRegistry.ConfigUpdater = discovery
s.kubeRegistry.EDSUpdater = s.EnvoyXdsServer</pre>
<p>最后，注册httpServer、grpcServer的延迟启动。</p>
<div class="blog_h3"><span class="graybg">NewDiscoveryService</span></div>
<p>proxy/envoy包中定义的这个函数，负责创建一个新的发现服务。它会创建一个RESTful容器并将发现服务注册到此容器：</p>
<pre class="crayon-plain-tag">container := restful.NewContainer()
out.Register(container)
out.RestContainer = container</pre>
<p>它还会向serviceregistry/kube.Controller注册Service、Instance的处理器，对于K8S来说，当Serivce、Endpoint发生变更时，会获得通知。</p>
<pre class="crayon-plain-tag">serviceHandler := func(*model.Service, model.Event) { out.clearCache() }
ctl.AppendServiceHandler(serviceHandler)
instanceHandler := func(*model.ServiceInstance, model.Event) { out.clearCache() }
err := ctl.AppendInstanceHandler(instanceHandler)</pre>
<p>这些处理器的逻辑很简单，就是清空Envoy缓存，但是kube.Controller附加了一些<a href="#Service">前置逻辑</a>。</p>
<div class="blog_h1"><span class="graybg">发现服务如何处理K8S资源</span></div>
<div class="blog_h2"><span class="graybg"><a id="resources-processing-framework"></a>资源处理框架</span></div>
<p>不论是K8S内置资源，而是Istio的CR，都由client-go负责List&amp;Watch，并把事件会传递给config/kube/crd/controller.go、config/kube/ingress/controller.go、serviceregistry/kube/controller.go等控制器中的回调函数处理：</p>
<pre class="crayon-plain-tag">informer.AddEventHandler(
	cache.ResourceEventHandlerFuncs{
		AddFunc: func(obj interface{}) {
// 处理Prometheus指标
			k8sEvents.With(prometheus.Labels{"type": otype, "event": "add"}).Add(1)
// 放入队列
			c.queue.Push(kube.NewTask(handler.Apply, obj, model.EventAdd))
		},</pre>
<p>可以看到，此回调函数的逻辑仅仅是将事件及其处理函数封装为Task并存放到控制器的队列中：</p>
<pre class="crayon-plain-tag">func (q *queueImpl) Push(item Task) {
	q.cond.L.Lock()
	defer q.cond.L.Unlock()
	if !q.closing {
		q.queue = append(q.queue, item)
	}
        // 唤醒等待cond的一个goroutine
	q.cond.Signal()
}
type Task struct {
	handler Handler
	obj     interface{}
	event   model.Event
}</pre>
<p>队列的Run循环则取出事件并调用其处理函数：</p>
<pre class="crayon-plain-tag">if err := item.handler(item.obj, item.event); err != nil {
   ...
}</pre>
<p>处理函数就是ChainHandler结构的Apply方法，此结构允许针对一个事件串行的调用多个实际的处理函数：</p>
<pre class="crayon-plain-tag">type ChainHandler struct {
	funcs []Handler
}

func (ch *ChainHandler) Apply(obj interface{}, event model.Event) error {
	for _, f := range ch.funcs {
		if err := f(obj, event); err != nil {
// 链中任何一个实际处理函数出错，都会导致中止处理
			return err
		}
	}
	return nil
}</pre>
<p>ChainHandler是在对应控制器的初始化阶段创建的，实际处理函数也在那时注册。</p>
<div class="blog_h2"><span class="graybg">Envoy缓存清除</span></div>
<p>Pilot对很多K8S资源变更的响应，都是简单的清除掉Envoy的配置缓存。它们调用的是envoy包的v1版本的DiscoveryService.clearCache方法：</p>
<pre class="crayon-plain-tag">func (ds *DiscoveryService) clearCache() {
	ds.ConfigUpdate(true)
}</pre>
<p>可以看到，此方法仅仅是简单的请求一次Envoy配置的Full Push。 也就是说<span style="background-color: #c0c0c0;">Pilot在配置变更的情况下，通常会完整的推送Envoy配置</span>，幸好推送过程具有防抖动支持，而且配置变更不是频繁操作，否则可能出现性能问题。</p>
<div class="blog_h2"><span class="graybg">Ingress的处理</span></div>
<p>config/kube/ingress/controller.go注册了多个处理函数，第一个处理函数判断是否informer已经完全同步，如果不是，则中止处理。第二个处理函数则是简单的清除Envoy缓存配置。</p>
<div class="blog_h2"><span class="graybg">CRD的处理</span></div>
<p>第一个处理函数是控制器的notify方法，此函数判断是否informer已经完全同步，如果不是，则中止处理：</p>
<pre class="crayon-plain-tag">func (c *controller) notify(obj interface{}, event model.Event) error {
	if !c.HasSynced() {
		return errors.New("waiting till full synchronization")
	}
        // 检查对象是否是DeletedFinalStateUnknown，如果否，对其调用MetaNamespaceKeyFunc，看看能否获得缓存键
	_, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
	return nil
}</pre>
<p>第二个处理函数，首先将IstioObject转换为model.Config，然后<span style="background-color: #c0c0c0;">转调</span>在启动阶段通过s.configController.RegisterEventHandler()为<span style="background-color: #c0c0c0;">不同资源类型注册的实际处理函数</span>：</p>
<pre class="crayon-plain-tag">c.kinds[typ].handler.Append(func(object interface{}, ev model.Event) error {
	item, ok := object.(IstioObject)
	config, err := ConvertObject(schema, item, c.client.domainSuffix)
	f(*config, ev)
})</pre>
<p><span class="graybg">MeshPolicy、VirtualService、DestinationRule、Gateway的实际处理函数，都仅仅是清除掉所有Envoy缓存。</span></p>
<div class="blog_h3"><span class="graybg">ServiceEntry</span></div>
<p>此CRD的实际处理函数的逻辑复杂一些。首先将model.Config.Spec转换为具体的Istio结构：</p>
<pre class="crayon-plain-tag">serviceEntry := config.Spec.(*networking.ServiceEntry)</pre>
<p>然后，将ServiceEntry转换为Istio内部的“服务”（model.Service）并异步处理 。model.Service具有FQDN、若干端口、可选的负载均衡/虚拟IP：</p>
<pre class="crayon-plain-tag">// 一个ServiceEntry可能转换为多个服务
services := convertServices(serviceEntry, config.CreationTimestamp.Time)
for _, handler := range c.serviceHandlers {
	for _, service := range services {
		go handler(service, event)
	}
}</pre>
<p>最后，将ServiceEntry转换为Istio内部的“实例”（model.ServiceInstance）并异步处理。model.ServiceInstance关联到一个网络端点（ip:port），具有一个服务描述和一组描述服务版本的标签：</p>
<pre class="crayon-plain-tag">instances := convertInstances(serviceEntry, config.CreationTimestamp.Time)
for _, handler := range c.instanceHandlers {
	for _, instance := range instances {
		go handler(instance, event)
	}
}</pre>
<p>不管是服务还是实例，go handler ...的逻辑仍然仅仅是清除掉所有Envoy缓存。</p>
<p>在这里可以注意到，ServiceEntry所（通常）表示的外部服务、和一般性的K8S服务，在Istio内部具有相同的表示 —— Service + ServiceInstance。</p>
<div class="blog_h2"><span class="graybg">其它K8S原生资源</span></div>
<p>第一个处理函数是控制器的notify方法，此函数判断是否informer已经完全同步，如果不是，则中止处理。</p>
<div class="blog_h3"><span class="graybg">Node</span></div>
<p>对于节点资源，默认情况下没有其它处理逻辑。</p>
<div class="blog_h3"><span class="graybg">Pod</span></div>
<p>pilot的serviceregistry包维护了一个最终一致性的Pod缓存：</p>
<pre class="crayon-plain-tag">func newPodCache(ch cacheHandler, c *Controller) *PodCache {
	out := &amp;PodCache{
		cacheHandler: ch,
		c:            c,
		keys:         make(map[string]string),
	}
	ch.handler.Append(func(obj interface{}, ev model.Event) error {
		return out.event(obj, ev)
	})
	return out
}</pre>
<p>Pod事件会触发PodCache.event方法的调用，此方法会更新Pod的IP地址和namespace/name形式的Key之间的映射关系，并且<a href="#WorkloadUpdate">为EDS服务更新工作负载</a>：</p>
<pre class="crayon-plain-tag">func (pc *PodCache) event(obj interface{}, ev model.Event) error {
	// 注意，Pod被删除后，可能得到*v1.Pod，也可能得到DeletionFinalStateUnknown这个标记对象
	pod, ok := obj.(*v1.Pod)
	if !ok {
		tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
		if !ok {
			return fmt.Errorf("couldn't get object from tombstone %+v", obj)
		}
                // 可以从DeletionFinalStateUnknown.Obj获得原始对象
		pod, ok = tombstone.Obj.(*v1.Pod)
		if !ok {
			return fmt.Errorf("tombstone contained object that is not a pod %#v", obj)
		}
	}

	ip := pod.Status.PodIP
	// 新创建的Pod的IP地址为空，直到通过UpdateStatus分配了IP
	if len(ip) &gt; 0 {
		key := KeyFunc(pod.Name, pod.Namespace)
		switch ev {
		case model.EventAdd:
			switch pod.Status.Phase {
			case v1.PodPending, v1.PodRunning:
				// 锁定并更新Pod缓存
				pc.rwMu.Lock()
				pc.keys[ip] = key
				pc.rwMu.Unlock()
// model.XDSUpdater
// 更新工作负载，在EDS看来工作负载的ID是IP地址
				if pc.c.EDSUpdater != nil {
					pc.c.EDSUpdater.WorkloadUpdate(ip, pod.ObjectMeta.Labels, pod.ObjectMeta.Annotations)
				}
			}
		case model.EventUpdate:
			switch pod.Status.Phase {
			case v1.PodPending, v1.PodRunning:
				// 更新Pod缓存
			default:
				// 其它状态下，删除Pod缓存

			}
		case model.EventDelete:
			// 删除Pod缓存
		}
	}

	return nil
}</pre>
<div class="blog_h3"><span class="graybg"><a id="Service"></a>Service</span></div>
<pre class="crayon-plain-tag">// 当接收到K8S Service更新时触发
c.services.handler.Append(func(obj interface{}, event model.Event) error {
	svc := *obj.(*v1.Service)

	// 不处理kube-system中定义的service
	if svc.Namespace == meta_v1.NamespaceSystem {
		return nil
	}
	if c.EDSUpdater != nil {
		hostname := svc.Name + "." + svc.Namespace
		ports := map[string]uint32{}
		portsByNum := map[uint32]string{}

		for _, port := range svc.Spec.Ports {
			ports[port.Name] = uint32(port.Port)
			portsByNum[uint32(port.Port)] = port.Name
		}
		// 变更服务信息后调用
		c.EDSUpdater.SvcUpdate(c.ClusterID, hostname, ports, portsByNum)
		// Bypass convertService and the cache invalidation.

		// 请求完整的配置更新并返回
		c.ConfigUpdater.ConfigUpdate(true)
		return nil
	}
// f的逻辑是清空缓存，默认不会执行到这个旧逻辑，只有当禁用了Direct EDS（环境变量PILOT_DIRECT_EDS=0）才会执行
	if svcConv := convertService(svc, c.domainSuffix); svcConv != nil {
		f(svcConv, event)
	}
	return nil
})</pre>
<div class="blog_h3"><span class="graybg">Endpoint</span></div>
<pre class="crayon-plain-tag">// 当接收到K8S Endpoint更新时触发
c.endpoints.handler.Append(func(obj interface{}, event model.Event) error {
	ep := obj.(*v1.Endpoints)

	// 不处理kube-system中的端点
	if ep.Namespace == meta_v1.NamespaceSystem {
		return nil
	}

	if c.EDSUpdater != nil {
// 更新端点
		c.updateEDS(ep)
	} else {
// 旧逻辑，如果端点对应的服务存在，则清空缓存
		if item, exists := c.serviceByKey(ep.Name, ep.Namespace); exists {
			if svc := convertService(*item, c.domainSuffix); svc != nil {
				f(&amp;model.ServiceInstance{Service: svc}, event)
			}
		}
	}
	return nil
})</pre>
<div class="blog_h1"><span class="graybg">发现服务如何更新和推送配置</span></div>
<p>&nbsp;</p>
<p>Pilot Discovery和Envoy代理配置的更新（这些配置更新都是由于K8S资源变化而引发）、推送有关的逻辑，主要分布在三个包中：</p>
<ol>
<li>proxy/envoy，主要的逻辑在此，包括xDS的实现</li>
<li>model，推送上下文（PushContext）定义在此包中，此结构在每次推送时创建，持有和本次推送相关的所有上下文信息</li>
<li>networking，负责生成Envoy的配置文件格式</li>
</ol>
<p>推送<span style="background-color: #c0c0c0;">由DiscoveryService.ConfigUpdate()方法触发</span>，<span style="background-color: #c0c0c0;">转调DiscoveryServer.Push</span>()，后者<span style="background-color: #c0c0c0;">调用PushContext</span>初始化一系列相关的数据结构，并调用<span style="background-color: #c0c0c0;">networking包中ConfigGenerator的方法生成Envoy v2的结构</span>，然后向所有连接到Pilot的Envoy代理发起推送。</p>
<div class="blog_h2"><span class="graybg">DiscoveryService</span></div>
<p>此结构负责发布服务、集群、路由给所有的代理。</p>
<pre class="crayon-plain-tag">type DiscoveryService struct {
	*model.Environment
	webhookClient   *http.Client
	webhookEndpoint string
	// 缓存，目前的实现，在任何路由、服务、端点发生变化时，都会Flush缓存。应当实现某种缓存过期策略
	// 避免反复Flush或者过期缓存滞留其中
	sdsCache *discoveryCache
    // 发现服务会注册REST路由到此容器
	RestContainer *restful.Container
	// 是否在去回弹之后，需要进行一个完整推送。如果仅仅要求EDS则为false
	fullPush bool
}</pre>
<p>缓存的结构如下，是一种通用的存储：</p>
<pre class="crayon-plain-tag">type discoveryCacheEntry struct {
	data          []byte // 什么都可以存
	hit           uint64 // atomic
	miss          uint64 // atomic
	resourceCount uint32
}

type discoveryCache struct {
	name     string
	disabled bool
	mu       sync.RWMutex
	cache    map[string]*discoveryCacheEntry
}</pre>
<div class="blog_h3"><span class="graybg">ClearCache</span></div>
<p>此方法清除缓存，目前的实现很简陋，就是发起<pre class="crayon-plain-tag">ds.ConfigUpdate(true)</pre>调用，会请求完整的配置推送。</p>
<div class="blog_h3"><span class="graybg">ConfigUpdate</span></div>
<p><a id="ConfigUpdater-Impl"></a>此方法实现ConfigUpdater接口。逻辑如下：</p>
<pre class="crayon-plain-tag">// 100ms
DebounceAfter = envDuration("PILOT_DEBOUNCE_AFTER", 100*time.Millisecond)
// 10s
DebounceMax = envDuration("PILOT_DEBOUNCE_MAX", 10*time.Second)

func (ds *DiscoveryService) ConfigUpdate(full bool) {
    // 设置完整推送标记
	if full {
		ds.fullPush = true
	}
    // 去回弹逻辑
	if DebounceAfter &gt; 0 {
        // 如果定时器标记已经设置，不做任何操作，这意味着频繁调用ConfigUpdate不会引发不良后果
		if !clearCacheTimerSet {
            // 此标记在实际执行推送时清除
			clearCacheTimerSet = true
            // 记录去回弹操作开始时间戳
			startDebounce := lastClearCacheEvent
            // 100ms后开始第一次去回弹判断
			time.AfterFunc(DebounceAfter, func() {
				ds.debouncePush(startDebounce)
			})
		}
		return
	}

    // 旧逻辑
    // 如果上一次配置变更发生在1秒前，执行推送
	if time.Since(lastClearCacheEvent) &gt; 1*time.Second {
		ds.doPush()
		return
	}
    // 如果上一次变更在1秒内，但是上一次推送大于clearCacheTime，也推送
	if time.Since(lastClearCache) &gt; time.Duration(clearCacheTime)*time.Second {
		ds.doPush()
		return
	}
    // 否则
	if !clearCacheTimerSet {
		clearCacheTimerSet = true
		time.AfterFunc(1*time.Second, func() {
			ds.clearCache() // 一秒后判断重新判断是否需要推送
		})
	}

}</pre>
<p>debouncePush方法中包含一些额外的去回弹逻辑：</p>
<pre class="crayon-plain-tag">func (ds *DiscoveryService) debouncePush(startDebounce time.Time) {
	// 上一次请求清除缓存的时间
	since := time.Since(lastClearCacheEvent)
    // 距今如果大于200ms，或者从启动去回弹到到现在大于10s
    // 如果反复请求推送，则||左侧的表达式一直不会满足。为了防止无限的去回弹，必然的推送在最初请求的10s后发生
	if since &gt; 2*DebounceAfter ||
		time.Since(startDebounce) &gt; DebounceMax {
		ds.doPush()

	} else {
        // 下一轮去回弹
		time.AfterFunc(DebounceAfter, func() {
			ds.debouncePush(startDebounce)
		})
	}
}</pre>
<p>doPush方法真正触发配置推送：</p>
<pre class="crayon-plain-tag">func (ds *DiscoveryService) doPush() {
	// 本次推送正在处理时，可能有别的配置变更发生，因此这里需要撤销标记，防止遗漏事件
	clearCacheTimerSet = false
	lastClearCache = time.Now()
	full := ds.fullPush
    // 获取自上次推送依赖的服务端点更新
	edsUpdates := BeforePush()

	// Update the config values, next ConfigUpdate and eds updates will use this
	clearCacheMutex.Lock()
	ds.fullPush = false
	clearCacheMutex.Unlock()
    // 推送，全部或者增量的EDS更新
	Push(full, edsUpdates)
}</pre>
<p>注意，全局函数BefoerPush实际上是envoyv2.DiscoveryServer.BeforePush方法：</p>
<pre class="crayon-plain-tag">// 以envoy包的全局变量作为媒介，在DiscoveryService和v2.DiscoveryServer之间传递这两个函数，避免它们直接依赖
envoy.Push = s.EnvoyXdsServer.Push
envoy.BeforePush = s.EnvoyXdsServer.BeforePush</pre>
<p>类似的，全局函数Push 也是envoyv2.DiscoveryServer.Push方法。</p>
<p>也就是说，推送的职责在这里转交给DiscoveryServer了。</p>
<div class="blog_h2"><span class="graybg">DiscoveryServer</span></div>
<p>此结构提供了Envoy v2 xds API的gRPC实现，在启动阶段由<a href="#initDiscoveryService">initDiscoveryService</a>创建。</p>
<pre class="crayon-plain-tag">type DiscoveryServer struct {
	// 环境信息
	Env *model.Environment

	// 仅仅用于调试和压力测试
	MemRegistry *MemServiceDiscovery

	// 负责使用 Istio的网络API + Service注册表信息 生成数据平面的配置信息（也就是Envoy代理配置）
	ConfigGenerator core.ConfigGenerator

	// 目前主要用于判断（K8S的）初始缓存同步是否完成
	ConfigController model.ConfigStoreCache

	// 初始连接的限速器
	initThrottle chan time.Time
	// 限速器
	throttle chan time.Time

	// 为/debug/adsz提供配置快照。默认false，可以通过环境变量PILOT_DEBUG_ADSZ_CONFIG=1启用
	DebugConfigs bool

	// 保护被ADS读写的全局数据结构, 包括 EDSUpdates和shards
	mutex sync.RWMutex

	// 服务的端点分片列表，此字段从增量更新构建
	EndpointShardsByService map[string]*model.EndpointShardsByService

	// 工作负载列表，可用于检测变更。直接由registry推送的更新计算得到
	WorkloadsByID map[string]*Workload

	// 负责请求配置更新，实现了放抖动（debouncing，延迟配置推送，防止Regsitry连续的资源更新导致过频推送）且能进行变更检测
	// 用于将envoy/v2从envoy/解耦，在Istio 1.1将进行简化/清理
	ConfigUpdater model.ConfigUpdater

	// 跟踪自上一次完整推送之后的所有服务的（端点的）变更，从1.0.3+仅仅用于跟踪两次推送之间的增量
	// 示例：
	// {
	//   details.default.svc.k8s.gmem.cc: {
	//     Shards: {
	//       "Kubernetes": {
	//         Shard: "Kubernetes",
	//         Entries: [ endpoints... ]
	//       }
	//     }  
	//   }
	// }
	edsUpdates map[string]*model.EndpointShardsByService

	// 保护全局推送上下文，一旦配置变更此上下文就改变，多个地方需要读取此上下文
	pushContextMutex sync.RWMutex
}</pre>
<p>此结构支持的方法繁多，定义在proxy/envoy/v2包中的ads.go、cds.go、eds.go、lds.go、rds.go等文件中，对应xDS API的各子集以及ADS。其中一部分方法是通过接口暴露的，例如：</p>
<ol>
<li>AggregatedDiscoveryServiceServer，Envoy提供的gRPC接口。在ads.go中实现</li>
<li>XDSUpdater，在push_context.go中定义，在eds.go中实现</li>
</ol>
<div class="blog_h3"><span class="graybg">BeforePush</span></div>
<p>此方法的逻辑很简单，将上次变更依赖的服务端点变更增量提取出来返回，并重置此“变更增量”为空：</p>
<pre class="crayon-plain-tag">func (s *DiscoveryServer) BeforePush() map[string]*model.EndpointShardsByService {
	edsUpdates := s.edsUpdates
	// 重置，后续的更新由新的map跟踪
	s.edsUpdates = map[string]*model.EndpointShardsByService{}
	return edsUpdates
}</pre>
<p>在未来，Istio对增量更新提供完整支持后，这里需要重构。在将proxy/envoy/discovery合并到v2 discovery之后，此方法不再允许外部访问。</p>
<div class="blog_h3"><span class="graybg">Push</span></div>
<p>该方法在准备好配置后，使用ADS协议执行推送：</p>
<pre class="crayon-plain-tag">func (s *DiscoveryServer) Push(full bool, edsUpdates map[string]*model.EndpointShardsByService) {
	if !full {
		// 执行增量推送
		go s.AdsPushAll(version, s.globalPushContext(), false, edsUpdates)
		return
	}
	// 获取推送上下文
	pc := s.globalPushContext()
	if pc != nil {
        // 将model.LastPushStatus赋值为当前推送上下文，并更新监控指标
		pc.OnConfigChange()
	}
	// 创建新的全局推送上下文，基于此上下文进行推送
	t0 := time.Now()
	push := model.NewPushContext()
	push.ServiceAccounts = s.ServiceAccounts
    // 初始化此推送上下文
	if err := push.InitContext(s.Env); err != nil {
		adsLog.Errorf("XDS: failed to update services %v", err)
		return
	}
    // 生成所有代理共享的配置，例如出站监听器/集群的配置
	if err := s.ConfigGenerator.BuildSharedPushState(s.Env, push); err != nil {
		adsLog.Errorf("XDS: Failed to rebuild share state in configgen: %v", err)
		totalXDSInternalErrors.Add(1)
		return
	}
    // 列出端点并创建分片
	if err := s.updateServiceShards(push); err != nil {
		return
	}
    // 替换掉全局上下文
	s.Env.PushContext = push

    // 异步推送
	go s.AdsPushAll(versionLocal, push, true, nil)
}</pre>
<div class="blog_h3"><span class="graybg">AdsPushAll</span></div>
<pre class="crayon-plain-tag">func (s *DiscoveryServer) AdsPushAll(version string, push *model.PushContext, full bool, edsUpdates map[string]*model.EndpointShardsByService) {
	if !full {
        // 更新集群信息，进行增量EDS推送。只有那些变化了的集群才被更新、推送
		s.edsIncremental(version, push, edsUpdates)
		return
	}

	// 赋值一个临时映射，避免加锁
	cMap := make(map[string]*EdsCluster, len(edsClusters))
	for k, v := range edsClusters {
		cMap[k] = v
	}

	// 为每个集群更新端点
	for clusterName, edsCluster := range cMap {
		if err := s.updateCluster(push, clusterName, edsCluster); err != nil {
			adsLog.Errorf("updateCluster failed with clusterName %s", clusterName)
		}
	}
    // 向所有客户端连接发送一个信号
	s.startPush(version, push, true, nil)
}</pre>
<div class="blog_h3"><span class="graybg">startPush</span></div>
<pre class="crayon-plain-tag">func (s *DiscoveryServer) startPush(version string, push *model.PushContext, full bool,
	edsUpdates map[string]*model.EndpointShardsByService) {

	// 遍历所有已连接的Envoy，这覆盖ADS / EDS(0.7)，它们共享一样的连接表
	adsClientsMutex.RLock()
	// 复制以避免加锁
	pending := []*XdsConnection{}
	for _, v := range adsClients {
		pending = append(pending, v)
	}
	adsClientsMutex.RUnlock()

	// 触发每个代理的配置的重现计算，包括Envoy监听的所有配置，包括EDS
	pendingPush := int32(len(pending))
	tstart := time.Now()
	for {
		if len(pending) == 0 {
			// 无需推送，或者都推送完了，退出
			return
		}
		currentVersion := versionInfo()
		// 如果具有更新版本的推送正在进行，则当前推送取消
		if !allowConcurrentPush &amp;&amp; version != currentVersion &amp;&amp; full {
			return
		}
		c := pending[0]
		pending = pending[1:]
		edsOnly := edsUpdates
		if full {
			edsOnly = nil
		}
		// 非阻塞性推送，如果下一次推送紧跟着就到来，可能导致问题
		// 这里是逐个处理客户端，可以优化为并行的进行
		client := c
		// time.After返回一个只读通道，在给定的超时到达后，通道可读
		to := time.After(PushTimeout)
		select {
		// 尝试向客户端的推送通道发送事件
		case client.pushChannel &lt;- &amp;XdsEvent{
			push:               push,
			pending:            &amp;pendingPush,
			version:            version,
			edsUpdatedServices: edsOnly,
		}:
 			// 该客户端推送成功
			client.LastPush = time.Now()
			client.LastPushFailure = timeZero
 		// 如果推送通道不可用，检查客户端连接是否关闭
		case &lt;-client.doneChannel:
			adsLog.Infof("Client closed connection %v", client.ConID)
		case &lt;-to:
 		// 推送超时，可能是由于Envoy代理处于异常状态，无法接收数据
			pushTimeouts.Add(1)
			// 放回去等待重试
			pending = append(pending, c)
		}
	}
}</pre>
<p>可以看到，startPush<span style="background-color: #c0c0c0;">仅仅是把事件放入通道</span>，并不直接牵涉到gRPC相关的操作。这<span style="background-color: #c0c0c0;">实现了数据和传输的解耦</span>。</p>
<div class="blog_h2"><span class="graybg"><a id="PushContext"></a>PushContext</span></div>
<p>推送上下文，此结构跟踪推送的状态（指标、错误）。在一次推送完毕后，指标全部置零。</p>
<pre class="crayon-plain-tag">type PushContext struct {
	// 操控下面的map的锁      
	proxyStatusMutex sync.RWMutex
	//             ID         ErrCode 记录推送给代理时的事件
	ProxyStatus map[string]map[string]ProxyPushStatus

	// 最后一次配置变更（导致推送状态重置）的时间
	Start time.Time
	End   time.Time

	// 保护下面的字段
	Mutex sync.Mutex 

	// 推送开始时系统中的所有服务
	Services []*Service 

	// 以主机名索引的所有服务
	ServiceByHostname map[Hostname]*Service 

	// 服务关联的DestinationRule（和目的地相关的规则）
	destinationRuleHosts   []Hostname
	destinationRuleByHosts map[Hostname]*combinedDestinationRule

	// 环境信息
	Env *Environment 

	ServiceAccounts func(string) []string 

	// 用于跟踪服务名称和端口的映射关系ServicePort2Name is used to keep track of service name and port mapping.
	// ADS的名字使用端口号，而端点的名字则使用端口名为准
	//                   服务名  端口号+名称信息
	ServicePort2Name map[string]PortList 

	initDone bool
}</pre>
<p>此结构提供了Add、UpdateMetrics方法，和Prometheus统计指标有关：</p>
<pre class="crayon-plain-tag">// 可以步进和推送有关的Prometheus计数器
func (ps *PushContext) Add(metric *PushMetric, key string, proxy *Proxy, msg string)
// 基于推送的当前状态来更新Prometheus指标
func (ps *PushContext) UpdateMetrics()</pre>
<div class="blog_h3"><span class="graybg">InitContext</span></div>
<p>该方法负责初始化一个新的推送上下文，生成所需的一切数据结构，在执行Envoy配置推送之前你需要（从创建PushContext的协程）调用它。</p>
<div class="blog_h3"><span class="graybg">initServiceRegistry</span></div>
<p>该方法缓存注册表中的所有服务。</p>
<div class="blog_h3"><span class="graybg">initVirtualServices</span></div>
<p>该方法缓存所有VirtualService对象。</p>
<div class="blog_h3"><span class="graybg">initDestinationRules</span></div>
<p>该方法初始化所有DestinationRule对象。</p>
<div class="blog_h3"><span class="graybg">VirtualServices</span></div>
<p>列出绑定到指定网关的序列服务。</p>
<div class="blog_h3"><span class="graybg">DestinationRule</span></div>
<p>返回一个服务的DestinationRule。</p>
<div class="blog_h3"><span class="graybg">SubsetToLabels</span></div>
<p>计算出给定服务的给定子集对应的标签。Istio使用K8S Pod标签区分子集。</p>
<div class="blog_h2"><span class="graybg">ConfigUpdater</span></div>
<p>此接口负责请求配置更新，在配置推送之前，需要进行去回弹（防止连续触发的频繁推送）。</p>
<pre class="crayon-plain-tag">type ConfigUpdater interface {
	ConfigUpdate(full bool)
}</pre>
<p>目前，接口由<a href="#ConfigUpdater-Impl">DiscoveryService实现</a>，而DiscoveryServer也引用前者：</p>
<pre class="crayon-plain-tag">s.EnvoyXdsServer.ConfigUpdater = s.discoveryService</pre>
<div class="blog_h2"><span class="graybg">XDSUpdater</span></div>
<p>XDSUpdater用于<span style="background-color: #c0c0c0;">xDS模型的直接更新，以及增量推送</span>。DiscoveryServer实现了此接口。</p>
<p>Pilot使用多个注册表 —— 例如每个K8S集群就是一个注册表。每个<span style="background-color: #c0c0c0;">注册表负责跟踪关联到网格中服务的端点的变更</span>，并在变更后调用EDSUpdate方法。</p>
<p>注册表可以单个服务的端点分组为更小的子集（例如每个Deployment一个子集），以处理端点数量巨大的服务。由于限制可扩容性，Istio倾向于避免传递大量的对象，例如某个注册表的全部端点，或者某个服务在全部注册表中的端点。</p>
<p>Istio在未来会进行一些优化，例如以标签、网关、或者区域来分组端点。</p>
<pre class="crayon-plain-tag">type XDSUpdater interface {

	// 当端点列表发生变化，或者ServiceEntry的标签发生变化，该方法被调用
	// 对于一个集群/主机名，其所有端点必须一并发送给代理
	// shard是一个键，目前是注册表的名称（例如Kubernetes）
	EDSUpdate(shard, hostname string, entry []*IstioEndpoint) error

	// 当一个服务-端口映射定义发生变化后调用，对服务的标签、注解或者其它属性进行变更，可能触发EDS/CDS重新计算、增量推送
	// 但不会影响LDS/RDS
	SvcUpdate(shard, hostname string, ports map[string]uint32, rports map[uint32]string)

	// 当一个工作负载的标签、注解发生变化时由注册表负责调用
	// 对于K8S来说，id是Pod的IP地址（如果Pod在默认/主网络中）
	WorkloadUpdate(id string, labels map[string]string, annotations map[string]string)
}</pre>
<div class="blog_h3"><span class="graybg">SvcUpdate</span></div>
<p>当服务的信息发生变化后，调用此方法，主要逻辑是更新全局推送上下文中的ServicePort2Name字段。</p>
<pre class="crayon-plain-tag">func (s *DiscoveryServer) SvcUpdate(cluster, hostname string, ports map[string]uint32, rports map[uint32]string) {
	pc := s.globalPushContext()
	s.mutex.Lock()
	defer s.mutex.Unlock()
	// 在1.0中，服务仅仅来自primary K8S集群
	if cluster == string(serviceregistry.KubernetesRegistry) {
		pl := model.PortList{}
		for k, v := range ports {
			pl = append(pl, &amp;model.Port{
				Port: int(v),
				Name: k,
			})
		}
		pc.ServicePort2Name[hostname] = pl
	}
}</pre>
<div class="blog_h3"><span class="graybg"><a id="WorkloadUpdate"></a>WorkloadUpdate</span></div>
<p>当工作负载的标签/注解发生变化后，PodCache调用此方法：</p>
<pre class="crayon-plain-tag">func (s *DiscoveryServer) WorkloadUpdate(id string, labels map[string]string, annotations map[string]string) {
	if labels == nil {
		// 没有标签，不需要推送更新给Envoy，删除缓存
		s.mutex.Lock()
		delete(s.WorkloadsByID, id)
		s.mutex.Unlock()
		return
	}
        // 使用读锁，禁止写
	s.mutex.RLock()
	w, f := s.WorkloadsByID[id]
	s.mutex.RUnlock()
	if !f {
		// 不存在于缓存中，可能从未连接到此工作负载，不需要推送
		s.mutex.Lock()
		s.WorkloadsByID[id] = &amp;Workload{
			Labels:      labels,
			Annotations: annotations,
		}
		s.mutex.Unlock()
		return
	}
        // 深度比较
	if reflect.DeepEqual(w.Labels, labels) {
		// 标签没有变化，也不推送
		return
	}

	w.Labels = labels
	// Pod标签变化，意味着需要重新计算Envoy配置

	// 为了简单和安全，简单的进行全部推送。其实只需要识别出影响到哪些工作负载（那些可能使用到当前工作负载的），并且仅仅为这些负载进行推送
	adsLog.Infof("Label change, full push %s ", id)
	s.ConfigUpdater.ConfigUpdate(true)
}</pre>
<div class="blog_h3"><span class="graybg">updateEDS</span></div>
<p>kube.Controller在接收到K8S的端点更新后，会调用其自身的updateEDS方法：</p>
<pre class="crayon-plain-tag">func (c *Controller) updateEDS(ep *v1.Endpoints) {
// 计算出K8S服务名
	hostname := serviceHostname(ep.Name, ep.Namespace, c.domainSuffix)
// 收集端点
	endpoints := []*model.IstioEndpoint{}
	for _, ss := range ep.Subsets {
		for _, ea := range ss.Addresses {
			// 如果端点对应的Pod不存在
			pod, exists := c.pods.getPodByIP(ea.IP)
			if !exists {
				// 则请求一次配置更新。Pod信息可能后续很快推送过来
				if c.ConfigUpdater != nil {
					c.ConfigUpdater.ConfigUpdate(true)
				}
				continue
			}
// 如果Pod存在，则抓取其标签、UID等信息，构建出IstioEndpoint
			labels := map[string]string(convertLabels(pod.ObjectMeta))
			uid := fmt.Sprintf("kubernetes://%s.%s", pod.Name, pod.Namespace)
			for _, port := range ss.Ports {
				endpoints = append(endpoints, &amp;model.IstioEndpoint{
					Address:         ea.IP,
					EndpointPort:    uint32(port.Port),
					ServicePortName: port.Name,
					Labels:          labels,
					UID:             uid,
					ServiceAccount:  kubeToIstioServiceAccount(pod.Spec.ServiceAccountName, pod.GetNamespace(), c.domainSuffix),
				})
			}
		}
	}
	//                     更新   Kubernetes分片  这个服务          这些端点
	err := c.EDSUpdater.EDSUpdate(c.ClusterID, string(hostname), endpoints)
	if err != nil {
		//如果仅仅进行EDS推送不可以，则全局推送
		c.ConfigUpdater.ConfigUpdate(true)
	} else {
                // 仅仅进行EDS推送
		c.ConfigUpdater.ConfigUpdate(false)
	}
}</pre>
<div class="blog_h2"><span class="graybg">ConfigGenerator</span></div>
<p>此接口位于networking.core包中，<span style="background-color: #c0c0c0;">负责生成xDS响应</span>。其实现位于v1alpha3子包（Istio CRD的API版本号）中。接口规格如下：</p>
<pre class="crayon-plain-tag">type ConfigGenerator interface {
	// 此方法重新计算所有Envoy代理之间的共享状态，这些状态包括：
	// 1、Sidecar的出站集群（outbound clusters）、出站监听器（outbound listener）
	// 2、Sidecar/Gateway的路由表
	// 这些状态保存在ConfigGenerator对象中，在后续调用BuildClusters/BuildListeners/BuildHTTPRoutes时重用
	// 该方法不会调用插件，大部分插件需要的是特定代理的（例如mixer/authn/authz)的信息
	// BuildYYY函数会针对预计算对象调用插件
	BuildSharedPushState(env *model.Environment, push *model.PushContext) error

	// LDS。为指定的代理构建inbound/outbound listeners信息，多次调用此函数，不会反复构建同一监听器
	BuildListeners(env *model.Environment, node *model.Proxy, push *model.PushContext) ([]*v2.Listener, error)

	// CDS。为指定代理构建clusters信息
	BuildClusters(env *model.Environment, node *model.Proxy, push *model.PushContext) ([]*v2.Cluster, error)

	// RDS。 为指定代理构建HTTP routes信息
	BuildHTTPRoutes(env *model.Environment, node *model.Proxy, push *model.PushContext, routeName string) (*v2.RouteConfiguration, error)
}</pre>
<p>注意BuildYYY函数的返回值均为Envoy v2协议的ProtoBuf消息类型。</p>
<div class="blog_h3"><span class="graybg">BuildSharedPushState</span></div>
<p>该方法的实现如下：</p>
<pre class="crayon-plain-tag">func (configgen *ConfigGeneratorImpl) BuildSharedPushState(env *model.Environment, push *model.PushContext) error {
    // 为Sidecar代理、Router代理分别构建出站集群
	configgen.OutboundClustersForSidecars = configgen.buildOutboundClusters(env, model.Sidecar, push)
	configgen.OutboundClustersForGateways = configgen.buildOutboundClusters(env, model.Router, push)
	return nil
}</pre><br />
<pre class="crayon-plain-tag">func (configgen *ConfigGeneratorImpl) buildOutboundClusters(env *model.Environment, proxyType model.NodeType, push *model.PushContext) []*v2.Cluster {
	clusters := make([]*v2.Cluster, 0)
	// 遍历上下文中包含的所有服务
	for _, service := range push.Services {
		// 获取服务的目的地规则（可能为空）
		config := push.DestinationRule(service.Hostname)
		for _, port := range service.Ports {
			if port.Protocol == model.ProtocolUDP {
				continue
			}
			// 得到服务包含的主机列表，如果服务的Resolution设置为DNSLB则返回nil
			hosts := buildClusterHosts(env, service, port.Port)

			// BuildSubsetKey：生成引用特定（限定服务、端口、子集）实例集的键，Envoy使用此键查询Pilot，进而获得子集中包含的实例列表
			// 示例：outbound|443||kubernetes.default.svc.k8s.gmem.cc
			clusterName := model.BuildSubsetKey(model.TrafficDirectionOutbound, "", service.Hostname, port.Port)
            // 获得集群（上游服务）的Istio账号
			upstreamServiceAccounts := env.ServiceAccounts.GetIstioServiceAccounts(service.Hostname, []string{port.Name})
            // convertResolution将Istio解析枚举转换为Envoy的集群发现类型（默认v2.Cluster_EDS）
            // ClientSideLB -&gt; v2.Cluster_EDS
            // DNSLB -&gt; v2.Cluster_STRICT_DNS
            // Passthrough -&gt; v2.Cluster_ORIGINAL_DST
            // 生成默认（相对于子集来说是默认）集群v2.Cluster信息，包括流量策略TrafficPolicy（连接池、异常检测、TLS设置、负载均衡器）
			defaultCluster := buildDefaultCluster(env, clusterName, convertResolution(service.Resolution), hosts)
            // 更新v2.Cluster.EdsClusterConfig，将此集群的EDS的配置源设置为ADS
			updateEds(env, defaultCluster, service.Hostname)
            // 如果是HTTP/2协议，则将最大并发流数量设置为1073741824
			setUpstreamProtocol(defaultCluster, port)
            // 将当前集群添加到列表
			clusters = append(clusters, defaultCluster)
			if config != nil {
                // 如果此服务具有关联的目的地规则
				destinationRule := config.Spec.(*networking.DestinationRule)
                // 如果使用mTLS，将DestinationRule的TrafficPolicy的证书字段填充上
				convertIstioMutual(destinationRule, upstreamServiceAccounts)
                // 将TrafficPolicy转换为v2.Cluster的对应字段
				applyTrafficPolicy(defaultCluster, destinationRule.TrafficPolicy, port)
				for _, subset := range destinationRule.Subsets {
                    // 处理子集，处理方式和默认集群类似
                    // 子集集群的名字示例 outbound|9080|v1|details.default.svc.k8s.gmem.cc
					subsetClusterName := model.BuildSubsetKey(model.TrafficDirectionOutbound, subset.Name, service.Hostname, port.Port)
					subsetCluster := buildDefaultCluster(env, subsetClusterName, convertResolution(service.Resolution), hosts)
					updateEds(env, subsetCluster, service.Hostname)
					setUpstreamProtocol(subsetCluster, port)
					applyTrafficPolicy(subsetCluster, destinationRule.TrafficPolicy, port)
					applyTrafficPolicy(subsetCluster, subset.TrafficPolicy, port)
					// 调用插件，以修改生成的配置
					for _, p := range configgen.Plugins {
						p.OnOutboundCluster(env, push, service, port, subsetCluster)
					}
					clusters = append(clusters, subsetCluster)
				}
			} else {
				// 如果全局启用了mTLS，并且此服务不是外部服务，并且当前代理是边车
				if env.Mesh.AuthPolicy == meshconfig.MeshConfig_MUTUAL_TLS &amp;&amp; !service.MeshExternal &amp;&amp; proxyType == model.Sidecar {
					applyUpstreamTLSSettings(defaultCluster, buildIstioMutualTLS(upstreamServiceAccounts, ""))
				}
			}

			// 为默认集群调用插件
			for _, p := range configgen.Plugins {
				p.OnOutboundCluster(env, push, service, port, defaultCluster)
			}
		}
	}

	return clusters
}</pre>
<div class="blog_h2"><span class="graybg">Plugin</span></div>
<p>networking包提供了一种插件机制，允许在构造xdsapi.Listener期间，对xdsapi.Listener进行任意的修改。例如启用：</p>
<ol>
<li>AuthenticationPlugin插件，可以在入站监听器、出站集群上设置mTLS认证</li>
<li>mixer插件，可以在入站监听器上设置策略检查</li>
</ol>
<p>插件类型包括：</p>
<pre class="crayon-plain-tag">const (
	// 身份验证插件
	Authn = "authn"
	// RBAC插件
	Authz = "authz"
	// Envoyfilter插件
	Envoyfilter = "envoyfilter"
	// 健康检查插件
	Health = "health"
	// Mixer 插件
	Mixer = "mixer"
)</pre>
<p>插件接口规格如下：</p>
<pre class="crayon-plain-tag">type Plugin interface {
	// 每当为一个服务添加一个出站监听器（到LDS输出）后，调用此方法
	// 可用于在出站路径上添加额外的监听器
	OnOutboundListener(in *InputParams, mutable *MutableObjects) error

	// 每当为一个服务添加一个监听器（到LDS输出）后，调用此方法
	// 可用于添加额外的监听器
	OnInboundListener(in *InputParams, mutable *MutableObjects) error

	// 每当一个新集群添加到CDS输出后，调用此方法
	// 为每次推送调用一次，而非为每个边车/代理
	OnOutboundCluster(env *model.Environment, push *model.PushContext, service *model.Service, servicePort *model.Port,
		cluster *xdsapi.Cluster)

	// 每当一个新集群添加到CDS输出后，调用此方法
	// 为每个边车/代理调用
	OnInboundCluster(env *model.Environment, node *model.Proxy, push *model.PushContext, service *model.Service, servicePort *model.Port,
		cluster *xdsapi.Cluster)

	// 每当新的虚拟主机（和路由）添加到出站路径的RDS后调用
	OnOutboundRouteConfiguration(in *InputParams, routeConfiguration *xdsapi.RouteConfiguration)

	// 每当新的虚拟主机（和路由）添加到入站路径的RDS后调用
	OnInboundRouteConfiguration(in *InputParams, routeConfiguration *xdsapi.RouteConfiguration)
}</pre>
<p>具体的插件，只需要按需实现部分方法即可。</p>
<div class="blog_h3"><span class="graybg">InputParams</span></div>
<p>一系列传递给插件回调函数（Plugin接口函数）的值，这些值应该被插件<span style="background-color: #c0c0c0;">只读访问</span>：</p>
<pre class="crayon-plain-tag">type InputParams struct {
	// 必须字段，监听器的协议，例如TCP/HTTP
	ListenerProtocol ListenerProtocol
	// 必须字段，环境信息
	Env *model.Environment
	// xDS响应是给哪个节点的
	Node *model.Proxy
	// 网格中所有代理服务的实例
	ProxyInstances []*model.ServiceInstance
	// 和监听器同地协作的服务实例（应用到Sidecar）
	ServiceInstance *model.ServiceInstance
	// S和监听器同地协作的服务（应用到Sidecar）
	// 对于出站TCP监听器，此字段指向目的地服务
	Service *model.Service
	// 监听器的端口
	// 对于边车的outbound/inbound监听器，指向服务端口（而非端点端口）
	// 对于网关的inbound监听器，指向网关服务器端口
	Port *model.Port

	// 推送上下文
	Push *model.PushContext
}</pre>
<div class="blog_h3"><span class="graybg">MutableObjects</span></div>
<p>传递给On*Listener回调的一系列对象：</p>
<pre class="crayon-plain-tag">type MutableObjects struct {
	// 当前正在生成的监听器配置
	Listener *xdsapi.Listener
	// 关联到此监听器的过滤器链
	FilterChains []FilterChain
}</pre>
<div class="blog_h3"><span class="graybg">xdsapi.Cluster</span></div>
<p>传递给On*Cluster回调的Envoy Cluster对象，直接对应ProtoBuffer消息。</p>
<div class="blog_h3"><span class="graybg">xdsapi.RouteConfiguration</span></div>
<p>传递给On*RouteConfiguration回调的Envoy RouteConfiguration对象，直接对应ProtoBuffer消息。</p>
<div class="blog_h1"><span class="graybg">代理启动和重载</span></div>
<p>本章讨论Pilot Agent的启动过程，以及Envoy的Bootstrap配置是如何生成和重新载入的。Pilot Agnet和Pilot Discovery如何通信在下一章分析。</p>
<div class="blog_h2"><span class="graybg">如何调试</span></div>
<p>为了方便调试，我们在本地启动Pilot Agent，参考下面的命令行参数：</p>
<pre class="crayon-plain-tag">export POD_NAME=productpage-v1-8d69b45c-5qcv7
export POD_NAMESPACE=default
export INSTANCE_IP=10.0.0.1
export ISTIO_BOOTSTRAP=pilot/envoy_bootstrap_tmpl.tpl

# pilot-agent的入口点代码位于 pilot/cmd/pilot-agent/main.go

            # 此代理的角色，可选sidecar、ingress、router
pilot proxy sidecar --log_output_level=default:debug --log_caller=default --domain=default.svc.k8s.gmem.cc \
                # Envoy二进制文件的位置
                --binaryPath=/home/alex/CPP/projects/clion/envoy/bazel-bin/source/exe/envoy-static \
                # 生成的Envoy配置文件存放在何处
                --configPath=pilot/envoyConfig \
                # 当前代理的节点属于哪个服务
                --serviceCluster=productpage \
                # 发现服务的地址
                --discoveryAddress=localhost:15010 \
                # Envoy进程的日志级别
                --proxyLogLevel=debug</pre>
<div class="blog_h2"><span class="graybg">启动过程概述</span></div>
<p>Pilot Agent的入口点位于pilot/cmd/pilot-agent/main.go，它使用包spf13.cobra来管理一组子命令，Pilot核心功能由proxy子命令实现。</p>
<p>入口点的启动过程如下：</p>
<ol>
<li>解析命令行参数</li>
<li>初始化model.Proxy对象，在K8S环境下
<ol>
<li>ID=$POD_NAME.$POD_NAMESPACE</li>
<li>Domain 从命令行--domain参数传入</li>
<li>IPAddress，从环境变量INSTANCE_IP读入</li>
</ol>
</li>
<li>如果控制平面的身份验证策略是mTLS，则初始化Pilot的SAN</li>
<li>调用model.ValidateProxyConfig进行代理配置合法性验证</li>
<li>初始化数字证书位置信息，默认存储在/etc/certs/。如果当前代理的角色不是sidecar而是ingress，还要初始化Ingress数字证书位置信息</li>
<li>如果提供了Envoy配置模板，且CustomConfigFile为空（不使用自定义Envoy配置文件），则根据此模板生成一个Envoy配置文件，并将其路径赋值给CustomConfigFile</li>
<li>如果提供了statusPort，则调用status.NewServer启动一个状态服务。访问状态服务的/healthz/ready可以知晓代理是否准备好</li>
<li>调用envoy.NewProxy，创建一个envoy结构</li>
<li>调用proxy.NewAgent，创建一个agent结构</li>
<li>调用envoy.NewWatcher，创建一个watcher结构</li>
<li>调用<a href="#watcher.Run">watcher.Run</a>启动Watcher
<ol>
<li>启动Agent控制循环</li>
<li>调用Reload立即载入，导致Envoy进程启动</li>
<li>调用watchCerts监听数字证书的变化</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">envoy</span></div>
<p>此结构位于proxy.envoy包中，封装启动envoy所需的信息：</p>
<pre class="crayon-plain-tag">func NewProxy(config meshconfig.ProxyConfig, node string, logLevel string, pilotSAN []string) proxy.Proxy {
	// 设置命令行传入的envoy的日志级别
	var args []string
	if logLevel != "" {
		args = append(args, "-l", logLevel)
	}

	return envoy{
        // ProxyConfig结构
		config:    config,
        // 节点的标识符，例如sidecar~10.0.0.1~productpage-v1-8d69b45c-5qcv7.default~default.svc.k8s.gmem.cc
		node:      node,
        // 额外的Envoy参数
		extraArgs: args,
        // 启用mTLS时所需的Pilot的SAN
		pilotSAN:  pilotSAN,
	}
}</pre>
<p>该结构实现了Proxy接口：</p>
<pre class="crayon-plain-tag">type Proxy interface {
	// 启动代理
	Run(interface{}, int, &lt;-chan error) error
	// 在代理退出后，清除对应的纪元
	Cleanup(int)

	// 如果以期望配置启动代理的最大尝试次数到达仍然失败，Agent在终结之前调用此方法
	Panic(interface{})
}</pre>
<div class="blog_h3"><span class="graybg">Run</span></div>
<p>此方法生成Envoy的Bootstrap配置，并启动Envoy进程：</p>
<pre class="crayon-plain-tag">func (proxy envoy) Run(config interface{}, epoch int, abort &lt;-chan error) error {
	var fname string
	// 使用自定义配置文件，或者通过模板生成配置文件
	if len(proxy.config.CustomConfigFile) &gt; 0 {
		fname = proxy.config.CustomConfigFile
	} else {
		out, err := bootstrap.WriteBootstrap(&amp;proxy.config, proxy.node, epoch, proxy.pilotSAN, proxy.opts, os.Environ())
		fname = out
	}

	// 处理Envoy命令行参数
	args := proxy.args(fname, epoch)
	if len(proxy.config.CustomConfigFile) == 0 {
		args = append(args, "--v2-config-only")
	}
    // e.g. -c pilot/envoyConfig/envoy-rev0.json --restart-epoch 0 --drain-time-s 2 --parent-shutdown-time-s 3 --service-cluster productpage --service-node sidecar~10.0.0.1~productpage-v1-8d69b45c-5qcv7.default~default.svc.k8s.gmem.cc --max-obj-name-len 189 --allow-unknown-fields -l warn --v2-config-only]
	log.Infof("Envoy command: %v", args)

	/* 异步命令行调用 */
	cmd := exec.Command(proxy.config.BinaryPath, args...)
	cmd.Stdout = os.Stdout  // 直接把Envoy子进程的标准输出和错误收集过来
	cmd.Stderr = os.Stderr
	if err := cmd.Start(); err != nil {
		return err
	}

	// 等待Envoy进程退出
	if proxy.errChan != nil {
		// 把Envoy退出错误码存入err通道
		go func() {
			proxy.errChan &lt;- cmd.Wait()
		}()
		return nil
	}
    // 再建一个通道和协程，当Envoy退出时协程写入通道，让当前协程从select case中退出
	done := make(chan error, 1)
	go func() {
		done &lt;- cmd.Wait()
	}()

	select {
   // Agent可以发送Abort信号，作为响应此Envoy实例需要停止
	case err := &lt;-abort:
		log.Warnf("Aborting epoch %d", epoch)
		if errKill := cmd.Process.Kill(); errKill != nil {
			log.Warnf("killing epoch %d caused an error %v", epoch, errKill)
		}
		return err
   // Envoy实例自然死亡
	case err := &lt;-done:
		return err
	}
}</pre>
<div class="blog_h3"><span class="graybg">WriteBootstrap</span></div>
<p>此方法生成Envoy Bootstrap配置。使用的配置模板，按以下逻辑寻找：</p>
<ol>
<li>如果指定了--customConfigFile，使用此文件。这步逻辑仅仅用于测试，实际无用</li>
<li>如果config.ProxyBootstrapTemplatePath不为空，使用此文件。此字段没有通过命令行暴露</li>
<li>否则，使用常量DefaultCfgDir指定的值，也就是默认配置</li>
<li>如果设置了环境变量ISTIO_BOOTSTRAP，使用此变量指定的配置模板路径</li>
</ol>
<p>默认配置的路径是/var/lib/istio/envoy/envoy_bootstrap_tmpl.json，内容为Envoy的JSON格式的配置的Go Template。</p>
<p>生成的Envoy配置文件样例：</p>
<pre class="crayon-plain-tag">{
  // 节点（边车和其代理的应用所在Pod）信息
  "node": {
    "id": "sidecar~10.0.0.1~productpage-v1-8d69b45c-5qcv7.default~default.svc.k8s.gmem.cc",
    "cluster": "productpage",
    "metadata": {
      "istio": "sidecar"
    }
  },
  // 监控指标配置，抽取标签
  "stats_config": {
    "use_all_default_tags": false,
    "stats_tags": [
      {
        "tag_name": "cluster_name",
        "regex": "^cluster\\.((.+(\\..+\\.svc\\.cluster\\.local))\\.)"
      },
      {
        "tag_name": "tcp_prefix",
        "regex": "^tcp\\.((.*)\\.)\\w+$"
      },
      {
        "tag_name": "response_code",
        "regex": "_rq(_(\\d{3}))$"
      },
      {
        "tag_name": "response_code_class",
        "regex": "_rq(_(\\dxx))$"
      },
      {
        "tag_name": "http_conn_manager_listener_prefix",
        "regex": "^listener(=\\.).*\\.http\\.(((:[_.[:digit:]]*|[_\\[\\]aAbBcCdDeEfF[:digit:]]*))\\.)"
      },
      {
        "tag_name": "http_conn_manager_prefix",
        "regex": "^http\\.(((:[_.[:digit:]]*|[_\\[\\]aAbBcCdDeEfF[:digit:]]*))\\.)"
      },
      {
        "tag_name": "listener_address",
        "regex": "^listener\\.(((:[_.[:digit:]]*|[_\\[\\]aAbBcCdDeEfF[:digit:]]*))\\.)"
      }
    ]
  },
  // 管理端口配置，管理端点也暴露了Prometheus Exporter
  "admin": {
    "access_log_path": "/dev/null",
    "address": {
      "socket_address": {
        "address": "127.0.0.1",
        "port_value": 15000
      }
    }
  },
  // 动态发现配置
  "dynamic_resources": {
    "lds_config": {
      "ads": {}
    },
    "cds_config": {
      "ads": {}
    },
    // 使用集群xds-grpc提供的聚合发现服务
    "ads_config": {
      "api_type": "GRPC",
      "refresh_delay": "1s",
      "grpc_services": [
        {
          "envoy_grpc": {
            "cluster_name": "xds-grpc"
          }
        }
      ]
    }
  },
  // 静态配置
  "static_resources": {
    "clusters": [
      // Prometheus Exporter
      {
        "name": "prometheus_stats",
        "type": "STATIC",
        "connect_timeout": "0.250s",
        "lb_policy": "ROUND_ROBIN",
        "hosts": [
          {
            "socket_address": {
              "protocol": "TCP",
              "address": "127.0.0.1",
              "port_value": 15000
            }
          }
        ]
      },
      // xDS服务地址，测试时注意把前文提到的发现服务开启
      {
        "name": "xds-grpc",
        "type": "STRICT_DNS",
        "connect_timeout": "1s",
        "lb_policy": "ROUND_ROBIN",
        "hosts": [
          {
            "socket_address": {
              "address": "localhost",
              "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": {}
      }
    ],
    // 暴露Prometheus指标的监听器
    "listeners": [
      {
        "address": {
          "socket_address": {
            "protocol": "TCP",
            "address": "0.0.0.0",
            "port_value": 15090
          }
        },
        "filter_chains": [
          {
            "filters": [
              {
                "name": "envoy.http_connection_manager",
                "config": {
                  "codec_type": "AUTO",
                  "stat_prefix": "stats",
                  "route_config": {
                    "virtual_hosts": [
                      {
                        "name": "backend",
                        "domains": [
                          "*"
                        ],
                        "routes": [
                          // 访问curl http://localhost:15090/stats/prometheus可以直接看到指标
                          {
                            "match": {
                              "prefix": "/stats/prometheus"
                            },
                            "route": {
                              "cluster": "prometheus_stats"
                            }
                          }
                        ]
                      }
                    ]
                  },
                  "http_filters": {
                    "name": "envoy.router"
                  }
                }
              }
            ]
          }
        ]
      }
    ]
  }
}</pre>
<div class="blog_h2"><span class="graybg">agent</span></div>
<p>此结构位于proxy.envoy包中，它是envoy proxy的代理人（Agent） 。它负责管理envoy进程的生命周期和重启。</p>
<p>Agent跟踪所有运行中的代理epochs及其配置。Envoy<span style="background-color: #c0c0c0;">热重启通过启动具有递增的重启纪元的（strictly incremented restart epoch）代理进程</span>实现。<span style="background-color: #c0c0c0;">优雅的关闭旧的epochs并且将所有必须的状态传递给最新的epoch是envoy的职责</span>，Agent不会去主动关闭旧的epoch对应的envoy进程。</p>
<p>最初的epoch是0。</p>
<p>这里使用的<a href="https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/hot_restart.html">重启协议</a>（restart protocol ）是和Envoy的重启纪元（restart epochs）语义匹配的：为了成功启动用来替代正在运行的Envoy的新进程，新进程的重启纪元必须<span style="background-color: #c0c0c0;">正好比所有正在运行的其它Envoy进程中纪元最大的那个高1</span>。</p>
<p>Agent需要调用Proxy的两个函数：</p>
<ol>
<li>Run函数，用于启动代理，且必须一直阻塞直到代理退出</li>
<li>Cleanup函数，用于在代理退出后立即进行清理，此函数必须是非阻塞的，因为它在Agent的主控制循环中被调用</li>
</ol>
<p>这两个函数都以代理的纪元作为入参。</p>
<p>每当Run函数返回了错误，Agent都假设代理启动失败了，并且会重试若干次。后续的重启可能重用之前失败的启动的纪元号。</p>
<pre class="crayon-plain-tag">func NewAgent(proxy Proxy, retry Retry) Agent {
	return &amp;agent{
        // Proxy接口的实现，也就是上面的envoy
		proxy:    proxy,
        // 重试（为proxy应用新配置）规则
		retry:    retry,
        // 当前纪元以及对应的配置
		epochs:   make(map[int]interface{}),
        // 通过此通道传递期望的配置
		configCh: make(chan interface{}),
        // 通过此通道提示代理退出
		statusCh: make(chan exitStatus),
        // 通过此通道终止运行中的实例
		abortCh:  make(map[int]chan error),
	}
}</pre>
<p>该结构实现了Agent接口：</p>
<pre class="crayon-plain-tag">type Agent interface {
	// 为代理设置期望的配置 —— Agent对比当前活动的配置和期望的配置，如果有必要，触发重启操作
	// 如果重启失败，以指数后退（exponential back-off）重试
	ScheduleConfigUpdate(config interface{})
    // 启动Agent控制循环
	Run(ctx context.Context)
}</pre>
<div class="blog_h3"><span class="graybg">Run</span></div>
<pre class="crayon-plain-tag">func (a *agent) Run(ctx context.Context) {
	// 限速器，平滑1QPS，爆发10QPS。为了处理所有通道的消息需要高QPS
	rateLimiter := rate.NewLimiter(1, 10)
	for {
        // Wait 阻塞，直到有了1个事件许可，等价于WaitN(ctx,1)
		err := rateLimiter.Wait(ctx)
		var delay time.Duration = 1&lt;&lt;63 - 1
        // 如果先前已经安排了下一次调度，设置延迟时间戳
		if a.retry.restart != nil {
			delay = time.Until(*a.retry.restart)
		}

		select {
        // 尝试从配置通道读取新配置，注意，Watcher从此通道传来的仅仅是散列值，此值仅用于识别配置是否变化，而不包含实际的配置信息
		case config := &lt;-a.configCh:
            // 如果和当前配置不同
			if !reflect.DeepEqual(a.desiredConfig, config) {
                // 赋值给期望配置
				a.desiredConfig = config

				// 重置 重试预算（剩余的重试次数）
				a.retry.budget = a.retry.MaxRetries
				a.reconcile()
			}
         // 如果此通道可读，则说明代理应该退出
		case status := &lt;-a.statusCh:
			// 删除status中的epoch
			delete(a.epochs, status.epoch)
             // 删除终止通道，防止在非Abort错误时自我终止
			delete(a.abortCh, status.epoch)
             // 更新配置
			a.currentConfig = a.epochs[a.latestEpoch()]
             // Abort错误
			if status.err == errAbort {
				log.Infof("Epoch %d aborted", status.epoch)
			} else if status.err != nil {
				// 用于Envoy热重启竞态条件的关系，这里需要通过杀死所有Envoy进程，立即的、非优雅的重启
				a.abortAll()
			} else {
                 // 正常退出
				log.Infof("Epoch %d exited normally", status.epoch)
			}

			// 清理此纪元的配置文件
			a.proxy.Cleanup(status.epoch)

			// 如果存在错误，调度一次重试
			if status.err != nil {
				// 延迟已调度的延迟，如果不为空，说明已经调度了
				if a.retry.restart == nil {
                        // 如果还有重试预算
					if a.retry.budget &gt; 0 {
						delayDuration := a.retry.InitialInterval * (1 &lt;&lt; uint(a.retry.MaxRetries-a.retry.budget))
						restart := time.Now().Add(delayDuration)
						a.retry.restart = &amp;restart // 调度下一次重试
						a.retry.budget = a.retry.budget - 1
					} else {
                        // 没有重试预算了，Panic
						a.proxy.Panic(status.epoch)
						return
					}
				}
			}
           // 如果已经到了下次调度的延迟
		case &lt;-time.After(delay):
			a.reconcile()
           // 通道关闭
		case _, more := &lt;-ctx.Done():
			if !more {
				a.terminate()
				return
			}
		}
	}
}</pre>
<div class="blog_h3"><span class="graybg">reconcile</span></div>
<p>此方法重现载入新的代理配置：</p>
<pre class="crayon-plain-tag">func (a *agent) reconcile() {
	// 取消掉任何计划重启
	a.retry.restart = nil

	// 再次检查期望配置是否和当前配置相同
	if reflect.DeepEqual(a.desiredConfig, a.currentConfig) {
		return
	}

	// 新增纪元
	epoch := a.latestEpoch() + 1
	// 重置abort通道
	abortCh := make(chan error, MaxAborts)
	a.epochs[epoch] = a.desiredConfig
	a.abortCh[epoch] = abortCh
	a.currentConfig = a.desiredConfig
    // 在新协程中启动代理
	go a.waitForExit(a.desiredConfig, epoch, abortCh)
}</pre>
<div class="blog_h3"><span class="graybg">waitForExit</span></div>
<p>此方法调用代理的Run()方法，并且在其崩了后，将错误码写入通道：</p>
<pre class="crayon-plain-tag">func (a *agent) waitForExit(config interface{}, epoch int, abortCh &lt;-chan error) {
	err := a.proxy.Run(config, epoch, abortCh)
	a.statusCh &lt;- exitStatus{epoch: epoch, err: err}
}</pre>
<div class="blog_h2"><span class="graybg">watcher</span></div>
<p>此结构位于proxy.envoy包中，<span style="background-color: #c0c0c0;">负责当代理配置发生变化后，触发Agent的reload</span>。它持有agent，还监控数字证书的变更：</p>
<pre class="crayon-plain-tag">func NewWatcher(config meshconfig.ProxyConfig, agent proxy.Agent, role model.Proxy,
	certs []CertSource, pilotSAN []string) Watcher {
	return &amp;watcher{
        // Agent
		agent:    agent,
        // 代理角色
		role:     role,
        // ProxyConfig
		config:   config,
        // CertSource列表
		certs:    certs,
        // Pilot的SAN
		pilotSAN: pilotSAN,
	}
}</pre>
<p>此结构实现了Watcher接口：</p>
<pre class="crayon-plain-tag">type Watcher interface {
	// 阻塞性的运行监控循环
	Run(context.Context)
	// 使用最新的配置Reload Agent
	Reload()
}</pre>
<div class="blog_h3"><span class="graybg"><a id="watcher.Run"></a>Run</span></div>
<pre class="crayon-plain-tag">func (w *watcher) Run(ctx context.Context) {
	// 启动Agent的控制循环
	go w.agent.Run(ctx)

	// 立即以最新配置重载Agent
	w.Reload()

	// 监控数字证书文件的变更
	certDirs := make([]string, 0, len(w.certs))
	for _, cert := range w.certs {
		certDirs = append(certDirs, cert.Directory)
	}
    // 如果数字证书发生变化，则调用Reload方法重载Agent
	go watchCerts(ctx, certDirs, watchFileEvents, defaultMinDelay, w.Reload)

	&lt;-ctx.Done()
}</pre>
<div class="blog_h3"><span class="graybg">Reload</span></div>
<p>促使Agent立即启动Envoy：</p>
<pre class="crayon-plain-tag">func (w *watcher) Reload() {
    // 生成数字证书的哈希
	h := sha256.New()
	for _, cert := range w.certs {
		generateCertHash(h, cert.Directory, cert.Files)
	}
    // 以哈希值作为配置信息，调度下次更新
	w.agent.ScheduleConfigUpdate(h.Sum(nil))
}</pre>
<p>Agnet的ScheduleConfigUpdate很简单，就是写通道：</p>
<pre class="crayon-plain-tag">func (a *agent) ScheduleConfigUpdate(config interface{}) {
	a.configCh &lt;- config
}</pre>
<p>从这里可以看出，<span style="background-color: #c0c0c0;">导致代理需要重新载入的唯一原因，就是数字证书发生了变更</span>。 </p>
<div class="blog_h2"><span class="graybg">ProxyConfig</span></div>
<p>此结构表示当前代理的配置，和命令行选项是对应的：</p>
<pre class="crayon-plain-tag">type ProxyConfig struct {
    // 生成的配置文件存放的路径
    ConfigPath string 
    // Envoy二进制文件的位置
    BinaryPath string 
    // 所属的集群，也就是K8S服务
    ServiceCluster string 
    // 热重启时多久Drain掉连接，必须大于1s
    DrainDuration *duration.Duration 
    // 热重启时关闭父进程的延迟
    ParentShutdownDuration *duration.Duration 
    // 发现服务（xDS）的访问地址，代理连接此服务进行各种资源的发现
    DiscoveryAddress string 
    // 服务发现的轮询间隔，用于EDS, CDS, LDS, 但不用于RDS，必须大于1ms
    DiscoveryRefreshDelay *duration.Duration 
    // Zipkin兼容的Tracer地址
    ZipkinAddress string 
    // Envoy连接到上游集群端点的超时
    ConnectTimeout *duration.Duration 
    // Statsd UDP监听地址
    StatsdUdpAddress string 
    // 管理端口，在此端口监听管理命令
    ProxyAdminPort int32 
    // 此实例所在的可用性区域
    // 在K8S中，节点的可用性区域通过注解failure-domain.beta.kubernetes.io/zone设置
    AvailabilityZone string 
    // 控制平面的身份验证策略
    ControlPlaneAuthPolicy AuthenticationPolicy 
    // 自定义的代理配置文件路径，当前Mixer、Pilot的前置代理使用自定义配置文件
    CustomConfigFile string 
    // Envoy监控指标的name字段的最大长度
    StatNameLength int32 
    // 并发的工作线程数量
    Concurrency int32 
    // Envoy Bootstrap配置模板的路径
    ProxyBootstrapTemplatePath string 
    // 如何重定向入站流量到Envoy
    InterceptionMode     ProxyConfig_InboundInterceptionMode 
    XXX_NoUnkeyedLiteral struct{}                            
    XXX_unrecognized     []byte                              
    XXX_sizecache        int32                               
}</pre>
<div class="blog_h2"><span class="graybg">CertSource</span></div>
<p>此结构表示数字证书的存储位置：</p>
<pre class="crayon-plain-tag">type CertSource struct {
	// 所在目录
	Directory string
	// 证书文件列表
	Files []string
}</pre>
<div class="blog_h1"><span class="graybg">代理和发现服务的通信</span></div>
<p>代理端的通信行为（主要是xDS）完全由Envoy负责，和Istio无关，后者仅仅提供Bootstrap配置并在必要时Reload。</p>
<div class="blog_h2"><span class="graybg">启动过程</span></div>
<p>Envoy的入口点定义在source/exe/main.cc中，仅仅是创建一个Envoy::MainCommon对象并调用其run方法：</p>
<pre class="crayon-plain-tag">int main(int argc, char** argv) {
  std::unique_ptr&lt;Envoy::MainCommon&gt; main_common;
  main_common = std::make_unique&lt;Envoy::MainCommon&gt;(argc, argv);
  return main_common-&gt;run() ? EXIT_SUCCESS : EXIT_FAILURE;
}</pre>
<p>Envoy::MainCommon对象的创建过程如下：</p>
<ol>
<li>创建化负责信号处理、线程池、命令参数解析、定时器的实例变量</li>
<li>创建MainCommonBase base_：
<ol>
<li>创建热重启器</li>
<li>进行日志配置</li>
<li>创建化监控指标的存储</li>
<li>创建Envoy服务<span style="background-color: #c0c0c0;">InstanceImpl —— Envoy服务本体</span>
<ol>
<li>初始化热重启器</li>
<li>通过组件工厂ComponentFactory创建DrainManager</li>
<li>初始化InstanceImpl：
<ol>
<li>加载Bootstrap配置</li>
<li>配置StoreRoot，该对象是负责监控指标的存储，包括Sink的管理</li>
<li>从Bootstrap配置创建一个InitialImpl，表示Envoy服务器的最初配置</li>
<li>利用InitialImpl创建Admin端点，并启动其监听器，开始监听15000端口</li>
<li>创建过载管理器，用于保护Envoy实例所在节点不资源耗尽</li>
<li>创建监听器管理器ListenerManager</li>
<li>通过主线程的Dispatcher，为主线程注册线程本地存储（TLS）</li>
<li>调用StoreRoot的initializeThreading，启用多线程支持</li>
<li>创建Runtime::Loader，此对象能够从磁盘读取Envoy的运行时快照</li>
<li>创建SSL上下文管理器</li>
<li>创建集群管理器工厂ProdClusterManagerFactory</li>
<li>传入集群管理器工厂，初始化Envoy服务器的主配置Configuration::MainImpl
<ol>
<li>使用密钥管理器添加bootstrap_中配置的静态密钥</li>
<li>使用集群管理工厂，传入bootstrap配置，<a href="#init-cluster-mgr">创建集群管理器</a></li>
<li>创建限速客户端工厂RateLimitClientFactory</li>
<li>调用监听器管理器，添加所有静态配置的监听器</li>
<li>调用initializeTracers，为当前Envoy服务器初始化Tracer</li>
<li>调用initializeStatsSinks，初始化监控信息的Sink</li>
</ol>
</li>
<li>为HTTP上下文设置Tracer</li>
<li>如果配置了LDS动态资源，则调用监听器管理器，创建注册gRPC订阅</li>
<li>将Sinks添加到StoreRoot</li>
<li>为StoreRoot的Flush设置定时器</li>
<li>初始化用于死锁检测的GuardDog</li>
</ol>
</li>
</ol>
</li>
</ol>
</li>
</ol>
<p>run方法简单的转发给MainCommonBase.run，进而转发给Server::InstanceImpl的run方法。如果运行模式是Serve，则后者的逻辑如下：</p>
<ol>
<li>初始化RunHelper</li>
<li>调用GuardDog创建针对主线程的WatchDog</li>
<li>启动WatchDog，由主线程的Dispatcher创建定时器，定期touch此WatchDog</li>
<li>阻塞性的运行Dispatcher事件循环
<ol>
<li>调用runPostCallbacks，在运行事件循环之前执行所有后置回调。默认情况下没有需要执行的回调</li>
<li>调用event_base_loop，运行event_base直到1-N个未决/活动事件可用</li>
</ol>
</li>
<li>当Dispatcher.exit被调用（也就是主事件循环退出）后，停止WatchDog</li>
<li>调用Server::InstanceImpl的terminate方法，停止Envoy服务器</li>
<li>重置RunHelper</li>
</ol>
<p>从第5步开始，主线程会阻塞很长时间。后续的逻辑都主要通过libevent事件驱动 —— 当发生网络事件后执行某种回调。</p>
<div class="blog_h3"><span class="graybg">信号处理</span></div>
<p>如果定义了宏ENVOY_HANDLE_SIGNALS，则MainCommon的成员变量负责处理信号：</p>
<pre class="crayon-plain-tag">#ifdef ENVOY_HANDLE_SIGNALS
  // 在备选栈上执行信号处理
  Envoy::SignalAction handle_sigs;
  // 打印Backtrace并退出
  Envoy::TerminateHandler log_on_terminate;
#endif</pre>
<div class="blog_h3"><span class="graybg">线程池</span></div>
<p>MainCommon的成员变量<pre class="crayon-plain-tag">PlatformImpl platform_impl_;</pre>定义平台依赖的操作，目前仅仅包含一个线程工厂实现。</p>
<div class="blog_h3"><span class="graybg">命令参数解析</span></div>
<p>MainCommon的成员变量<pre class="crayon-plain-tag">Envoy::OptionsImpl options_;</pre>负责利用<a href="http://tclap.sourceforge.net/">TCLAP</a>解析命令行参数，它实现<pre class="crayon-plain-tag">Envoy::Server::Option</pre>接口，通过此接口可获得各种Envoy启动选项。</p>
<div class="blog_h3"><span class="graybg">定时器</span></div>
<p>MainCommon的成员变量<pre class="crayon-plain-tag">Event::RealTimeSystem real_time_system_;</pre>用于墙上时间度量，以及设置定时器、执行回调。</p>
<div class="blog_h3"><span class="graybg">MainCommonBase</span></div>
<p>MainCommon持有此类型的一个对象，并且把绝大部分职责委托给此类处理。构造函数逻辑如下：</p>
<pre class="crayon-plain-tag">MainCommonBase::MainCommonBase(OptionsImpl&amp; options, Event::TimeSystem&amp; time_system,
                               TestHooks&amp; test_hooks, Server::ComponentFactory&amp; component_factory,
                               std::unique_ptr&lt;Runtime::RandomGenerator&gt;&amp;&amp; random_generator,
                               Thread::ThreadFactory&amp; thread_factory)
    : options_(options), component_factory_(component_factory), thread_factory_(thread_factory) {
  // c-ares是一个C语言实现的异步请求DNS的实现，在实例初始化时，应该先调用该函数对该库相关内部模块
  ares_library_init(ARES_LIB_INIT_ALL);
  // 忽略SIGPIPE信号
  Event::Libevent::Global::initialize();

  switch (options_.mode()) {
  case Server::Mode::InitOnly:
  case Server::Mode::Serve: {
    if (!options.hotRestartDisabled()) {
      // 热重启器，接口由HotRestart提供，实现代码和配置的“热”重启
      restarter_ = std::make_unique&lt;Server::HotRestartImpl&gt;(options_);init-cluster-mgr
    }
    // ThreadLocal::InstanceImpl实现Instance，负责注册线程，读写线程本地数据
    tls_ = std::make_unique&lt;ThreadLocal::InstanceImpl&gt;();
    // 写、读日志锁，实现类是ProcessSharedMutex，可以跨进程使用的互斥量
    Thread::BasicLockable&amp; log_lock = restarter_-&gt;logLock();
    Thread::BasicLockable&amp; access_log_lock = restarter_-&gt;accessLogLock();
    auto local_address = Network::Utility::getLocalAddress(options_.localAddressIpVersion());
    // 日志上下文，相当于同时调用setLogLevel, setLogFormat, setLock
    logging_context_ =
        std::make_unique&lt;Logger::Context&gt;(options_.logLevel(), options_.logFormat(), log_lock);
    // 配置各Envoy组件的日志
    configureComponentLogLevels();
    // 监控指标存储，ThreadLocalStoreImpl是支持线程本地缓存的StoreRoot实现
    stats_store_ = std::make_unique&lt;Stats::ThreadLocalStoreImpl&gt;(options_.statsOptions(),
    // Stats::StatDataAllocator负责创建Counter、Gauge等Metric的实例
                                                                 restarter_-&gt;statsAllocator());

    // 创建Envoy服务器
    server_ = std::make_unique&lt;Server::InstanceImpl&gt;(
        options_, time_system, local_address, test_hooks, *restarter_, *stats_store_,
        access_log_lock, component_factory, std::move(random_generator), *tls_, thread_factory);

    break;// 1、使用密钥管理器添加bootstrap_中配置的静态密钥
// 2、使用集群管理工厂，传入bootstrap配置，创建集群管理器
// 3、创建限速客户端工厂RateLimitClientFactory
// 4、调用监听器管理器，添加所有静态配置的监听器
// 对于每个监听器，如果存在draining的监听器占用了它绑定的地址，则夺取其SocketSharedPtr
// 如果不存在，则调用创建新的SocketSharedPtr，可能导致创建底层套接字，并绑定到端口
// 5、调用initializeTracers，为当前Envoy服务器初始化Tracer
// 6、调用initializeStatsSinks，初始化监控信息的Sink
  }
  case Server::Mode::Validate:
    break;
  }
}</pre>
<div class="blog_h3"><span class="graybg">InstanceImpl</span></div>
<p>InstanceImpl类实现接口Instance，代表一个运行中的、由若干紧密协作的组件构成的、独立运行的Envoy服务。</p>
<p>该类的构造函数逻辑如下：</p>
<pre class="crayon-plain-tag">// 创建基于文件的日志Sink
if (!options.logPath().empty()) {
    file_logger_ = std::make_unique&lt;Logger::FileSinkDelegate&gt;(
            options.logPath(), access_log_manager_, Logger::Registry::getSink());
}
// 初始化热重启器
restarter_.initialize(*dispatcher_, *this);
// 创建DrainManager
drain_manager_ = component_factory.createDrainManager(*this);
// 初始化Envoy服务
initialize(options, local_address, component_factory);</pre>
<p>初始化逻辑如下：</p>
<pre class="crayon-plain-tag">// 首先打印epoch信息，以及所有以及注册的、静态链接的扩展
ENVOY_LOG(info, "initializing epoch {} (hot restart version={})", options.restartEpoch(),
          restarter_.version());

ENVOY_LOG(info, "statically linked extensions:");
ENVOY_LOG(info, "  access_loggers: {}",
          Registry::FactoryRegistry&lt;Configuration::AccessLogInstanceFactory&gt;::allFactoryNames());
// 编写扩展时，你需要负责注册扩展，只需要声明如下形式的静态变量即可
//                                  扩展类名               扩展所属分类
// static Registry::RegisterFactory&lt;FileAccessLogFactory, Server::Configuration::AccessLogInstanceFactory&gt; register_;


// 将Bootstrap配置（由Pilot Agent生成）读取到envoy::config::bootstrap::v2::Bootstrap中
// Bootstrap是Protobuf消息（::google::protobuf::Message）
// 自动依据扩展名来决定如何解析配置文件
// 如果指定了--config-yaml选项，则YAML中的配置覆盖到--config-path指定的配置中
// 如果以Bootstrap V2配置解析失败，则尝试以V1重新解析
InstanceUtil::loadBootstrapConfig(bootstrap_, options);
// 记录Bootstrap配置修改时间
bootstrap_config_update_time_ = time_system_.systemTime();

// 需要尽早配置StoreRoot，以支持监控指标收集
// TagProducer分析一个指标名，从中抽取一系列标签
stats_store_.setTagProducer(Config::Utility::createTagProducer(bootstrap_));
// StatsMatcher决定哪些指标启用禁用
stats_store_.setStatsMatcher(Config::Utility::createStatsMatcher(bootstrap_));

// 在上述存储中创建Envoy服务状态统计
// struct ServerStats {
//   Stats::Gauge &amp;uptime_;
//   Stats::Gauge &amp;concurrency_;
//   ...
// };
server_stats_ = std::make_unique&lt;ServerStats&gt;(
                // (stats_store_).gauge("server." + std::string("uptime")), ...
    ServerStats{ALL_SERVER_STATS(POOL_GAUGE_PREFIX(stats_store_, "server."))});

// 收集server.***指标
server_stats_-&gt;concurrency_.set(options_.concurrency());
server_stats_-&gt;hot_restart_epoch_.set(options_.restartEpoch());

// 本地信息，例如节点名称、所属集群、IP地址
local_info_ = std::make_unique&lt;LocalInfo::LocalInfoImpl&gt;(
    bootstrap_.node(), local_address, options.serviceZone(), options.serviceClusterName(),
    options.serviceNodeName());

// 创建一个Initial对象，此对象表示初始化配置 —— 在加载主配置之前需要知道的配置信息
Configuration::InitialImpl initial_config(bootstrap_);

// 如果可以，关闭父进程中的admin processing，这让admin processing可以启动一个新进程
HotRestart::ShutdownParentAdminInfo info;
info.original_start_time_ = original_start_time_;
restarter_.shutdownParentAdmin(info);
original_start_time_ = info.original_start_time_;

// 全局的admin HTTP端点
admin_ = std::make_unique&lt;AdminImpl&gt;(initial_config.admin().profilePath(), *this);
admin_-&gt;startHttpListener(initial_config.admin().accessLogPath(), options.adminAddressPath(),
                              initial_config.admin().address(),
                              stats_store_.createScope("listener.admin."));

// ConfigTracker供admin端点/config_dump使用，管理一系列能够提供配置信息的回调函数
config_tracker_entry_ =
// add返回EntryOwner，此类型实现了map条目的RAII语义 —— 仅当EntryOwner或ConfigTracker销毁后条目被移除
      admin_-&gt;getConfigTracker().add("bootstrap", [this] { return dumpBootstrapConfig(); });

// 将admin监听器添加到连接管理器。ConnectionHandler能够添加/删除/启用/禁用/停止网路监听器
// 调用handler-&gt;addListener(*listener_)后，创建ActiveListener，监听15000端口。也就是说ConnectionHandler负责启动端口监听
admin_-&gt;addListenerToHandler(handler_.get());

// 创建过载管理器
overload_manager_ = std::make_unique&lt;OverloadManagerImpl&gt;(dispatcher(), stats(), threadLocal(),
                                                            bootstrap_.overload_manager());

// 创建监听器管理器，负责管理：所有监听器、所有负责连接处理的工作线程（Worker）
// 创建指定数量的WorkerImpl，为每个WorkerImpl向过载管理器注册过载回调（行为是不再接受连接）
// 为每个WorkerImpl创建Dispatcher，并将此Dispatcher注册到ThreadLocal::Instance以支持后续读写线程本地变量
listener_manager_ = std::make_unique&lt;ListenerManagerImpl&gt;(*this, listener_component_factory_,
    worker_factory_, time_system_);

// 主线程也需要注册到TLS
thread_local_.registerThread(*dispatcher_, true);

// 当所有工作线程对象初始化后，调用下面的方法，调用Stats::StoreRoot的如下方法，以支持多线程操作
stats_store_.initializeThreading(*dispatcher_, thread_local_);

// Runtime::Loader能够从磁盘读取运行时快照
runtime_loader_ = component_factory.createRuntime(*this, initial_config);

// SSL上下文管理器，管理进程中所有SSL上下文
// 实现类ContextManagerImpl，线程模型如下：上下文可以从任意线程创建（但是实践上通常从主线程分配）
// 上下文的分配/销毁是少见操作，因此整体上使用一把锁来保护
ssl_context_manager_ = std::make_unique&lt;Ssl::ContextManagerImpl&gt;(time_system_);

// ProdClusterManagerFactory是ClusterManagerFactory的产品环境实现，Envoy很多命名包含Prod的类，用于和测试、Mock用途的类区分
// 集群管理器工厂，负责创建集群管理操作所需要的对象
cluster_manager_factory_ = std::make_unique&lt;Upstream::ProdClusterManagerFactory&gt;(
      runtime(), stats(), threadLocal(), random(), dnsResolver(), sslContextManager(), dispatcher(),
      localInfo(), secretManager(), api(), http_context_);

// Configuration::MainImpl是主服务器配置的实现，其初initialize方法必须在Envoy服务器完全准备好后调用，应用自举配置到服务器：
config_.initialize(bootstrap_, *this, *cluster_manager_factory_);
// 为HTTP上下文设置Tracer
http_context_.setTracer(config_.httpTracer());

// 如果包含LDS动态资源
if (bootstrap_.dynamic_resources().has_lds_config()) {
    // 则调用监听器管理器，创建LDS API Provider
    // 委托给ListenerComponentFactory.createLdsApi，最终会在cm.adsMux()上注册gRPC订阅
    listener_manager_-&gt;createLdsApi(bootstrap_.dynamic_resources().lds_config());
}

// 将Sink添加到StoreRoot
for (Stats::SinkPtr&amp; sink : config_.statsSinks()) {
    stats_store_.addSink(*sink);
}
// 注册Stat刷出定时器
// 某些Sink需要Dispatcher的支持，因此在主循环开始前，不能刷出
stat_flush_timer_ = dispatcher_-&gt;createTimer([this]() -&gt; void { flushStats(); });
stat_flush_timer_-&gt;enableTimer(config_.statsFlushInterval());

// 看门狗，用于死锁检测，在Worker启动之前、主循环run之前初始化
guard_dog_ = std::make_unique&lt;Server::GuardDogImpl&gt;(stats_store_, config_, time_system_, api());</pre>
<div class="blog_h3"><span class="graybg">MainCommonBase::run</span></div>
<p>此方法的代码如下：</p>
<pre class="crayon-plain-tag">bool MainCommonBase::run() {
  // 对应不同的运行模式
  switch (options_.mode()) {
  case Server::Mode::Serve:
    // 调用Server::InstanceImpl::run
    server_-&gt;run();
    return true;
  case Server::Mode::Validate: {
    ...
  }
  case Server::Mode::InitOnly: {
    ...
  }
}</pre>
<div class="blog_h3"><span class="graybg">InstanceImpl::run</span></div>
<p>此方法的代码如下：</p>
<pre class="crayon-plain-tag">void InstanceImpl::run() {
  // 创建运行助手
  //                                        服务器实例
  //                                               服务器选项  事件分发器     集群管理器
  run_helper_ = std::make_unique&lt;RunHelper&gt;(*this, options_, *dispatcher_, clusterManager(),
  //                                        日志管理器             初始化管理器     过载管理器
                                            access_log_manager_, init_manager_, overloadManager(),
  //                                        用于启动工作线程的回调函数
                                            [this]() -&gt; void { startWorkers(); });
  // 看门狗
  auto watchdog = guard_dog_-&gt;createWatchDog(api_-&gt;threadFactory().currentThreadId());
  watchdog-&gt;startWatchdog(*dispatcher_);
  // Event::Dispatcher是对libevent的包装，负责事件分发
  // 启动事件循环，阻塞
  dispatcher_-&gt;run(Event::Dispatcher::RunType::Block);
  // 重置看门狗
  guard_dog_-&gt;stopWatching(watchdog);
  watchdog.reset();
  // 停止
  terminate();
  // 重置运行助手
  run_helper_.reset();
}</pre>
<div class="blog_h3"><span class="graybg">RunHelper</span></div>
<p>此运行助手对象的职责包括：</p>
<ol>
<li>初始化信号处理，主要时关闭Instance</li>
<li>设置集群管理器的<span style="background-color: #c0c0c0;">初始化后</span>回调
<ol>
<li>暂停RDS订阅</li>
<li>执行初始化管理器的初始化，初始化其上注册的所有目标，<span style="background-color: #c0c0c0;">最后启动工作线程</span></li>
<li>恢复RDS订阅</li>
</ol>
</li>
<li>启动过载管理器</li>
</ol>
<p>构造函数代码如下：</p>
<pre class="crayon-plain-tag">RunHelper::RunHelper(Instance&amp; instance, Options&amp; options, Event::Dispatcher&amp; dispatcher,
                     Upstream::ClusterManager&amp; cm, AccessLog::AccessLogManager&amp; access_log_manager,
                     InitManagerImpl&amp; init_manager, OverloadManager&amp; overload_manager,
                     std::function&lt;void()&gt; workers_start_cb) {

  // 创建基于Dispatcher的信号处理事件回调
  if (options.signalHandlingEnabled()) {
    sigterm_ = dispatcher.listenForSignal(SIGTERM, [&amp;instance]() {
      instance.shutdown();
    });

    sigint_ = dispatcher.listenForSignal(SIGINT, [&amp;instance]() {
      instance.shutdown();
    });

    sig_usr_1_ = dispatcher.listenForSignal(SIGUSR1, [&amp;access_log_manager]() {
      access_log_manager.reopen();
    });

    sig_hup_ = dispatcher.listenForSignal(SIGHUP, []() {
      // 不响应挂断信号，要知道如何热重启，查看文档
    });
  }

  // 启动集群管理器初始化后回调，直到上游集群全部初始化完毕，不会启动工作线程并处理流量
  cm.setInitializedCb([&amp;instance, &amp;init_manager, &amp;cm, workers_start_cb]() {
    // 如果实例被关闭，不执行任何操作。随时都可能收到信号
    if (instance.isShutdown()) {
      return;
    }

    // 暂停RDS发现，确保在订阅了所有RDS资源之前，不会发送任何请求
    // 订阅在初始化回调中发生，因此在初始化管理器的初始化回调执行完毕之前暂停RDS发现
    cm.adsMux().pause(Config::TypeUrl::get().RouteConfiguration);

    // 所有集群均已经初始化，现在初始化 初始化管理器

    // 下面的回调不能捕获this，因为它执行的时候RunHelper可能已经销毁
    init_manager.initialize([&amp;instance, workers_start_cb]() {
      // 如果实例被关闭，不执行任何操作。随时都可能收到信号
      if (instance.isShutdown()) {
        return;
      }
      // 否则（在初始化所有注册的target之后）启动工作线程
      workers_start_cb();
    });

    // 初始化回调执行完毕
    cm.adsMux().resume(Config::TypeUrl::get().RouteConfiguration);
  });
  // 启动负载管理器
  overload_manager.start();
}</pre>
<div class="blog_h3"><span class="graybg">DispatcherImpl::run</span></div>
<p>此方法的代码如下：</p>
<pre class="crayon-plain-tag">void DispatcherImpl::run(RunType type) {
  run_tid_ = api_.threadFactory().currentThreadId();
  // 在执行事件循环之间的钩子回调
  runPostCallbacks();
  // 启动libevent事件循环
  event_base_loop(base_.get(), type == RunType::NonBlock ? EVLOOP_NONBLOCK : 0);
}</pre>
<p>到这里可以看到，启动过程的最后，主线程陷入无限循环。</p>
<p>所有后续的逻辑，由Envoy的事件机制来触发。此事件机制的核心是事件分发器接口（Dispatcher），在Envoy启动期间，很多事件回调被注册到Dispatcher，并在以后异步的、可能反复的执行。</p>
<div class="blog_h2"><span class="graybg">事件分发器</span></div>
<p>Envoy使用<a href="/libevent-study-note">libevent2</a>提供的事件机制，但是在其上做了一层封装 —— Dispatcher，事件回调就是通过此Dispatcher注册的。Dispatcher的接口如下：</p>
<pre class="crayon-plain-tag">namespace Envoy {
namespace Event {


struct FileReadyType {
  // 文件可读
  static const uint32_t Read = 0x1;
  // 文件可写
  static const uint32_t Write = 0x2;
  // 对端关闭了文件
  static const uint32_t Closed = 0x4;
};

/**
 * 事件分发器的post()调用之后执行的回调
 */
typedef std::function&lt;void()&gt; PostCb;

/**
 * 抽象的事件分发循环
 */
class Dispatcher {
public:
  virtual ~Dispatcher() {}

  /**
   * 此分发器使用的时间源
   */
  virtual TimeSystem&amp; timeSystem() PURE;

  /**
   * 清空延迟删除队列
   */
  virtual void clearDeferredDeleteList() PURE;

  /**
   * 创建一个服务器连接
   * @param socket 该指针指向ConnectionSocket，此结构持有一个套接字（FD）以及一些元数据（例如本地地址）
   *               对于服务器连接来说，此结构代表一个已经accept的套接字
   *               对于客户端连接来说，此结构代码一个正连接到远程地址的套接字
   * @param transport_socket 提供连接使用的传输套接字。TransportSocket负责实际的读写以及数据转换（例如TLS）
   * @return Network::ConnectionPtr 返回一个归属调用者的服务器连接对象
   */
  virtual Network::ConnectionPtr
  createServerConnection(Network::ConnectionSocketPtr&amp;&amp; socket, Network::TransportSocketPtr&amp;&amp; transport_socket) PURE;

  /**
   * 创建一个客户端连接
   * @param address 需要连接到的服务器
   * @param source_address 绑定到的本地地址，或者nulptr自动绑定
   * @param transport_socket 此连接使用的传输套接字
   * @param options 套接字选项
   * @return Network::ClientConnectionPtr 返回一个归属调用者的客户端连接对象
   */
  virtual Network::ClientConnectionPtr
  createClientConnection(Network::Address::InstanceConstSharedPtr address,
                         Network::Address::InstanceConstSharedPtr source_address,
                         Network::TransportSocketPtr&amp;&amp; transport_socket,
                         const Network::ConnectionSocket::OptionsSharedPtr&amp; options) PURE;

  /**
   * 创建一个供启动事件循环的线程独占使用的异步DNS解析器
   * @param resolvers DNS服务器地址，默认/etc/resolv.conf
   * @return 返回归属调用者的Network::DnsResolverSharedPtr
   */
  virtual Network::DnsResolverSharedPtr
  createDnsResolver(const std::vector&lt;Network::Address::InstanceConstSharedPtr&gt;&amp; resolvers) PURE;

  /**
   * 创建一个文件（对于Linux来说，任何文件式的接口，包括普通文件、套接字都支持），当其可读可写时触发回调
   * @param fd 监控的文件描述符
   * @param cb 文件可读写时执行的回调
   * @param trigger 边缘触发还是水平触发
   * @param events 此事件最初监听的事件类型，FileReadyType按位或
   */
  virtual FileEventPtr createFileEvent(int fd, FileReadyCb cb, FileTriggerType trigger, uint32_t events) PURE;

  /**
   * @return Filesystem::WatcherPtr 返回归属调用者的文件系统监控器
   */
  virtual Filesystem::WatcherPtr createFilesystemWatcher() PURE;

  /**
   * 在指定的端口上创建一个监听器
   * @param socket 需要监听的套接字
   * @param cb 监听器事件的处理回调
   * @param bind_to_port 是否需要绑定到传输端口
   * @param hand_off_restored_destination_connections 当恢复（restoring）了新连接的目的地址后，该监听器是否
   *                                                  应该搜索另外一个（更匹配连接的）监听器
   * @return Network::ListenerPtr 返回归属调用者的监听器
   */
  virtual Network::ListenerPtr createListener(Network::Socket&amp; socket,
                                              Network::ListenerCallbacks&amp; cb, bool bind_to_port,
                                              bool hand_off_restored_destination_connections) PURE;

  /**
   *  分配一个定时器
   */
  virtual Event::TimerPtr createTimer(TimerCb cb) PURE;

  /**
   * 提交一个条目，延迟删除
   */
  virtual void deferredDelete(DeferredDeletablePtr&amp;&amp; to_delete) PURE;

  /**
   * 退出事件循环
   */
  virtual void exit() PURE;

  /**
   * 监听信号事件，进程中只有单个分发器可以处理信号，否则行为未定义
   *
   * @param signal_num 需要监听的信号
   * @param cb 信号发生时执行的回调
   * @return SignalEventPtr返回归属调用者的信号事件
   */
  virtual SignalEventPtr listenForSignal(int signal_num, SignalCb cb) PURE;

  /**
   * 添加一个Post回调，此回调在执行事件循环的那个线程异步的执行。此调用线程安全
   */
  virtual void post(PostCb callback) PURE;

  /**
   * 执行事件循环，知道某个回调或其它线程调用exit()
   * 阻塞模式：除非exit()被调用，不会退出循环
   * 非阻塞模式：仅仅执行活动事件的回调，然后事件循环就退出
   */
  enum class RunType { Block, NonBlock };
  virtual void run(RunType type) PURE;

  /**
   * 获取此分发器的带水位支持的缓冲的工厂
   */
  virtual Buffer::WatermarkFactory&amp; getWatermarkFactory() PURE;
};

typedef std::unique_ptr&lt;Dispatcher&gt; DispatcherPtr;

} // namespace Event
} // namespace Envoy</pre>
<p>DispatcherImpl是基于libevent的Dispatcher实现。可以看到，注册回调主要依靠createFileEvent方法。</p>
<p>在Envoy启动阶段，会注册多个事件回调，包括热重启模块的信号回调、DNS解析模块的回调、ADS客户端的定时器回调。</p>
<div class="blog_h2"><span class="graybg">初始化管理器</span></div>
<div class="blog_h3"><span class="graybg">Init::Manager</span></div>
<p>该接口用于执行<span style="background-color: #c0c0c0;">多个目标的初始化</span>：</p>
<pre class="crayon-plain-tag">class Manager {
public:
  virtual ~Manager() {}

  /**
   * 注册一个在未来需要初始化的目标，初始化管理器会在适当的时候，调用所有目标的initialize方法
   */
  virtual void registerTarget(Target&amp; target) PURE;

  enum class State {
    /**
     * 目标们尚未被初始化
     */
    NotInitialized,
    /**
     * 目标们正在被初始化
     */
    Initializing,
    /**
     * 所有目标已经初始化完毕
     */
    Initialized
  };

  /**
   * 返回状态
   */
  virtual State state() const PURE;
}; </pre>
<p>单个初始化目标由下面的类型表示：</p>
<pre class="crayon-plain-tag">class Target {
public:
  virtual ~Target() {}

  /**
   * 当目标应该进行初始化时调用该方法
   * @param callback 目标的初始化完成后，调用的回调
   */
  virtual void initialize(std::function&lt;void()&gt; callback) PURE;
};</pre>
<div class="blog_h3"><span class="graybg">Server::InitManagerImpl</span></div>
<p>该实现用于Post集群管理器、Pre监听时的初始化管理。集群管理器并不是单个实例，例如每个Cluster都有自己的初始化管理器。</p>
<pre class="crayon-plain-tag">class InitManagerImpl : public Init::Manager {
public:
  // 初始化所有目标，并在完毕后执行总回调
  // 1、如果目标集为空，直接总回调，进入Initialized状态
  // 2、否则，将此总回调赋值给实例变量。进入Initializing状态，依次initializeTarget每个目标
  void initialize(std::function&lt;void()&gt; callback);
  void registerTarget(Init::Target&amp; target) override;
  State state() const override { return state_; }

private:
  // 调用单个目标的initialize，并在完成后回调中移除此目标
  // 如果移除后没有更多目标，将管理器设置为Initialized状态，并且执行总回调
  void initializeTarget(Init::Target&amp; target);
  std::list&lt;Init::Target*&gt; targets_;
  State state_{State::NotInitialized};
  // 总回调
  std::function&lt;void()&gt; callback_;
};</pre>
<div class="blog_h2"><span class="graybg">热重启</span></div>
<p>HotRestartImpl实现热重启功能，在初始化时它为UDS注册一个事件回调：</p>
<pre class="crayon-plain-tag">void HotRestartImpl::initialize(Event::Dispatcher&amp; dispatcher, Server::Instance&amp; server) {
  socket_event_ =
      // 注册事件回调
      dispatcher.createFileEvent(my_domain_socket_,
                                 [this](uint32_t events) -&gt; void {
                                   ASSERT(events == Event::FileReadyType::Read);
                                   onSocketEvent();
                                 },
                                 // 边缘触发，监听可读事件
                                 Event::FileTriggerType::Edge, Event::FileReadyType::Read);
  server_ = &amp;server;
}</pre>
<p>事件到达后调用onSocketEvent()方法，获取RCP消息，根据消息类型做出各种处理，例如：关闭Admin端点、Drain监听器、返回监控统计信息、停止当前进程。</p>
<div class="blog_h2"><span class="graybg">DNS解析器</span></div>
<p>DnsResolver负责异步的DNS解析。多个组件需要进行DNS解析，例如StrictDnsClusterImpl.startPreInit方法会解析集群的DNS域名。</p>
<div class="blog_h3"><span class="graybg">初始化</span></div>
<p>InstanceImpl在构造函数中创建DnsResolverImpl：</p>
<pre class="crayon-plain-tag">InstanceImpl::InstanceImpl( ... ): ...
   dns_resolver_(dispatcher_-&gt;createDnsResolver({})){
   ...
}
Network::DnsResolverSharedPtr DispatcherImpl::createDnsResolver( const std::vector&lt;Network::Address::InstanceConstSharedPtr&gt;&amp; resolvers) {
  return Network::DnsResolverSharedPtr{new Network::DnsResolverImpl(*this, resolvers)};
}</pre>
<p>DnsResolverImpl在初始化期间，会将onAresSocketStateChange方法作为回调传递给ares：</p>
<pre class="crayon-plain-tag">DnsResolverImpl::DnsResolverImpl(){
  initializeChannel(&amp;options, 0);
}

void DnsResolverImpl::initializeChannel(ares_options* options, int optmask) {
  // 将回调提供给ares
  options-&gt;sock_state_cb = [](void* arg, int fd, int read, int write) {
    static_cast&lt;DnsResolverImpl*&gt;(arg)-&gt;onAresSocketStateChange(fd, read, write);
  };
  options-&gt;sock_state_cb_data = this;
  // 初始化ares
  ares_init_options(&amp;channel_, options, optmask | ARES_OPT_SOCK_STATE_CB);
}</pre>
<p>当某个ares套接字可读可写时，会调用onAresSocketStateChange：</p>
<pre class="crayon-plain-tag">void DnsResolverImpl::onAresSocketStateChange(int fd, int read, int write) {
  auto it = events_.find(fd);
  // 如果不可读不可写，而且之间已经跟踪了此fd，则删除fd不再跟踪
  if (read == 0 &amp;&amp; write == 0) {
    if (it != events_.end()) {
      events_.erase(it);
    }
    return;
  }

  // 如果之前没有跟踪过此fd
  if (it == events_.end()) {
    // 注册事件回调
    events_[fd] = dispatcher_.createFileEvent(
        fd, [this, fd](uint32_t events) { onEventCallback(fd, events); },
        Event::FileTriggerType::Level, Event::FileReadyType::Read | Event::FileReadyType::Write);
  }
}

// 事件回调的逻辑是调用ares：
void DnsResolverImpl::onEventCallback(int fd, uint32_t events) {
  const ares_socket_t read_fd = events &amp; Event::FileReadyType::Read ? fd : ARES_SOCKET_BAD;
  const ares_socket_t write_fd = events &amp; Event::FileReadyType::Write ? fd : ARES_SOCKET_BAD;
  ares_process_fd(channel_, read_fd, write_fd);
  updateAresTimer();
}</pre>
<div class="blog_h3"><span class="graybg">解析过程</span></div>
<p>上面的“ares套接字可读可写”，可以由解析DNS的请求来触发，例如StrictDnsClusterImpl.startPreInit会触发DNS解析：</p>
<pre class="crayon-plain-tag">void StrictDnsClusterImpl::startPreInit() {
  for (const ResolveTargetPtr&amp; target : resolve_targets_) {
    target-&gt;startResolve();
  }
}

void StrictDnsClusterImpl::ResolveTarget::startResolve() {
  active_query_ = parent_.dns_resolver_-&gt;resolve(
  ..., []( address_list )-&gt;void {
         // 解析成功后的回调
       }
  )
}

ActiveDnsQuery* DnsResolverImpl::resolve(const std::string&amp; dns_name, DnsLookupFamily dns_lookup_family, ResolveCb callback) {
  std::unique_ptr&lt;PendingResolution&gt; pending_resolution( new PendingResolution(callback, dispatcher_, channel_, dns_name));  
  pending_resolution-&gt;getHostByName(AF_INET6);
}</pre>
<p>上面代码的最后一个方法，回调被传递给PendingResolution对象，随即调用其getHostByName方法：</p>
<pre class="crayon-plain-tag">void DnsResolverImpl::PendingResolution::getHostByName(int family) {
  ares_gethostbyname(channel_, dns_name_.c_str(), family,
                     [](void* arg, int status, int timeouts, hostent* hostent) {
                       static_cast&lt;PendingResolution*&gt;(arg)-&gt;onAresHostCallback(status, timeouts, hostent);
                     }, this);
}</pre>
<p>此方法调用c-ares库进行主机名解析，回调是PendingResolution的onAresHostCallback方法：</p>
<pre class="crayon-plain-tag">void DnsResolverImpl::PendingResolution::onAresHostCallback(int status, int timeouts,
                                                            hostent* hostent) {
  // 解析处地址列表，并调用上面的回调C0
  callback_(std::move(address_list));
}</pre>
<p>总之，DNS解析是由Dispatcher的回调机制和ares的回调机制协作实现的，达到的效果是异步、高效的DNS解析。</p>
<div class="blog_h2"><span class="graybg"><a id="init-cluster-mgr"></a>集群初始化</span></div>
<p>在Envoy主服务InstanceImpl的主配置的初始化过程中，会创建集群管理器ClusterManagerImpl。</p>
<div class="blog_h3"><span class="graybg">ClusterManagerImpl</span></div>
<p>集群管理器负责Bootstrap配置中定义的<span style="background-color: #c0c0c0;">静态集群的初始化，以及CDS/EDS的订阅的启动</span>。这个处理过程比较复杂，分为多个阶段完成</p>
<ol>
<li>初始化静态/DNS集群</li>
<li>初始化预定义的EDS集群</li>
<li>如果需要，初始化CDS订阅，并等待响应</li>
<li>初始化CDS提供的集群，分为两个阶段
<ol>
<li>主（Primary）集群初始化，所有非EDS集群是主集群</li>
<li>次（Secondary）集群初始化，EDS集群为次集群。每个EDS集群会独自创建一个xDS订阅，这导致EDS集群需要依赖于非EDS集群（xds-grpc，STRICT_DNS集群），因此EDS集群需要在第二阶段初始化</li>
</ol>
</li>
<li>如果集群启用了主动健康检查，此时会触发单轮检查</li>
</ol>
<pre class="crayon-plain-tag">ClusterManagerImpl::ClusterManagerImpl(...)
    : ...,
      // 注册针对每个集群的初始化回调，每个集群本身初始化完毕后调用此回调。集群管理器使用此回调进行后初始化处理
      init_helper_([this](Cluster&amp; cluster) { onClusterInit(cluster); }),
      config_tracker_entry_(
          admin.getConfigTracker().add("clusters", [this] { return dumpClusterConfigs(); })),
      // 使用主线程分发器，及其时间源
      time_source_(main_thread_dispatcher.timeSystem()), dispatcher_(main_thread_dispatcher),
      http_context_(http_context) {
  // gRPC异步客户端管理器（AsyncClientManager）可以为每个gRPC服务（envoy::api::v2::core::GrpcService配置）
  // 创建AsyncClient的工厂
  async_client_manager_ =
      std::make_unique&lt;Grpc::AsyncClientManagerImpl&gt;(*this, tls, time_source_, api);
  // 获得自举配置中的集群管理器配置
  const auto&amp; cm_config = bootstrap.cluster_manager();
  ...
  // 处理v1 API的遗留风格的服务发现配置
  if (bootstrap.dynamic_resources().deprecated_v1().has_sds_config()) {
    eds_config_ = bootstrap.dynamic_resources().deprecated_v1().sds_config();
  }

  // 读取所有静态资源
  for (const auto&amp; cluster : bootstrap.static_resources().clusters()) {
    // 加载所有主集群，默认配置下有prometheus-stat、xds-grpc两个静态配置的主集群
    if (cluster.type() != envoy::api::v2::Cluster::EDS) {
      loadCluster(cluster, "", false, active_clusters_);
    }
  }

  // 如果必要，创建ADS，可能依赖于主集群
  if (bootstrap.dynamic_resources().has_ads_config()) {
    ads_mux_ = std::make_unique&lt;Config::GrpcMuxImpl&gt;(
        local_info, // 代理本地环境信息
        // 创建gRPC异步客户端
        Config::Utility::factoryForGrpcApiConfigSource(
            *async_client_manager_, bootstrap.dynamic_resources().ads_config(), stats)
            -&gt;create(),
        // 使用主线程的事件分发器
        main_thread_dispatcher,
        // 寻找gRPC服务
        *Protobuf::DescriptorPool::generated_pool()-&gt;FindMethodByName(
            "envoy.service.discovery.v2.AggregatedDiscoveryService.StreamAggregatedResources"),
        // 随机数生成器
                 // Stats::Scope，被限定了scope的stats
        random_, stats_,
        // 限速配置
        Envoy::Config::Utility::parseRateLimitSettings(bootstrap.dynamic_resources().ads_config()));
  } else {
    // 没有ADS配置，提供一个占位符
    ads_mux_ = std::make_unique&lt;Config::NullGrpcMuxImpl&gt;();
  }

  // 在ADS初始化后，加载EDS类型的静态集群，这种集群可能依赖ADS来发现端点
  for (const auto&amp; cluster : bootstrap.static_resources().clusters()) {
    // Now load all the secondary clusters.
    if (cluster.type() == envoy::api::v2::Cluster::EDS) {
      loadCluster(cluster, "", false, active_clusters_);
    }
  }
  // 更新监控指标
  cm_stats_.cluster_added_.add(bootstrap.static_resources().clusters().size());
  updateGauges();

  // 到这里，所有静态集群已经加载完毕，现在检查基于v1 API SDS的集群、基于ADS的集群
  const ClusterInfoMap loaded_clusters = clusters();
  if (bootstrap.dynamic_resources().deprecated_v1().has_sds_config()) {
    ...
  }

  // 获取当前本地集群的名称
  absl::optional&lt;std::string&gt; local_cluster_name;
  if (!cm_config.local_cluster_name().empty()) {
    local_cluster_name_ = cm_config.local_cluster_name();
    local_cluster_name = cm_config.local_cluster_name();
    if (active_clusters_.find(local_cluster_name.value()) == active_clusters_.end()) {
      throw EnvoyException(
          fmt::format("local cluster '{}' must be defined", local_cluster_name.value()));
    }
  }

  // 一旦最初的静态Bootstrap集群被创建（包括本地集群），就可以创建线程本地的集群管理器
  tls_-&gt;set([this, local_cluster_name](
                Event::Dispatcher&amp; dispatcher) -&gt; ThreadLocal::ThreadLocalObjectSharedPtr {
    // ThreadLocalClusterManagerImpl在线程本地缓存集群数据，并从parent central dynamic cluster获取更新
    // 此对象维护负载均衡器状态、所有已经创建的线程池
    return std::make_shared&lt;ThreadLocalClusterManagerImpl&gt;(*this, dispatcher, local_cluster_name);
  });

  // 创建CDS客户端，并传递给集群管理器初始化助手ClusterManagerInitHelper对象
  if (bootstrap.dynamic_resources().has_cds_config()) {
    cds_api_ = factory_.createCds(bootstrap.dynamic_resources().cds_config(), eds_config_, *this);
    init_helper_.setCds(cds_api_.get());
  } else {
    init_helper_.setCds(nullptr);
  }

  // 将所有静态自举集群传递给初始化助手处理，这会导致：
  // 1、初始化所有主集群
  // 2、进行post-init处理，来初始化任何线程感知的负载均衡器，并创建per-worker的主机（端点）集更新
  for (auto&amp; cluster : active_clusters_) {
    init_helper_.addCluster(*cluster.second-&gt;cluster_);
  }

  // 将状态设置为WaitingForStaticInitialize
  // 如果所有主集群都初始化完毕了，可能进行静态配置的次集群（EDS）初始化
  // 此方法会调用maybeFinishInitialize
  init_helper_.onStaticLoadComplete();

  // 启动ADS客户端，创建新的gRPC流
  ads_mux_-&gt;start();
} </pre>
<div class="blog_h3"><span class="graybg">ClusterImplBase::initialize</span></div>
<p>由集群管理器调用init_helper_.addCluster完成，该方法实现如下：</p>
<pre class="crayon-plain-tag">void ClusterManagerInitHelper::addCluster(Cluster&amp; cluster) {
  // 集群第一次初始化后执行的回调。例如，对于动态DNS集群，此回调将在最初的DNS解析完成后调用
  const auto initialize_cb = [&amp;cluster, this] { onClusterInit(cluster); };
  if (cluster.initializePhase() == Cluster::InitializePhase::Primary) {
    // 主集群初始化
    primary_init_clusters_.push_back(&amp;cluster);
    cluster.initialize(initialize_cb);
  } else {
    // 从CDS获取到的动态集群，走这个分支
    secondary_init_clusters_.push_back(&amp;cluster);
    // 如果当前已经启动了第二阶段初始化，则立即开始初始化
    if (started_secondary_initialize_) {
      cluster.initialize(initialize_cb);
    }
  }
}</pre>
<p>可以看到，init_helper会调用集群的initialize方法，这是定义在ClusterImplBase中的模板方法：</p>
<pre class="crayon-plain-tag">void ClusterImplBase::initialize(std::function&lt;void()&gt; callback) {
  // 设置初始化回调
  initialization_complete_callback_ = callback;
  // 开始进行预初始化
  startPreInit();
}</pre>
<div class="blog_h3"><span class="graybg">StaticClusterImpl::startPreInit</span></div>
<p>集群初始化的<span style="background-color: #c0c0c0;">第一步是预初始化</span>，静态集群对该方法的实现如下：</p>
<pre class="crayon-plain-tag">void StaticClusterImpl::startPreInit() {
  // 检查是否配置了监控检查，如果是，将所有节点标记为不健康
  const auto&amp; health_checker_flag =
      health_checker_ != nullptr
          ? absl::optional&lt;Upstream::Host::HealthFlag&gt;(Host::HealthFlag::FAILED_ACTIVE_HC)
          : absl::nullopt;
  // PriorityState为每个优先级绑定一组 主机+对应的位置权重映射
  auto&amp; priority_state = priority_state_manager_-&gt;priorityState();
  for (size_t i = 0; i &lt; priority_state.size(); ++i) {
    priority_state_manager_-&gt;updateClusterPrioritySet(
        i, std::move(priority_state[i].first), absl::nullopt, absl::nullopt, health_checker_flag,
        overprovisioning_factor_);
  }
  priority_state_manager_.reset();
  // 预初始化完成后回调
  onPreInitComplete();
}</pre>
<div class="blog_h3"><span class="graybg">StrictDnsClusterImpl::startPreInit</span></div>
<p>DNS集群的startPreInit，从DNS解析开始：</p>
<pre class="crayon-plain-tag">void StrictDnsClusterImpl::startPreInit() {
  for (const ResolveTargetPtr&amp; target : resolve_targets_) {
    target-&gt;startResolve();
  }
}</pre>
<p>DNS解析完成后，异步的执行下面的回调：</p>
<pre class="crayon-plain-tag">void StrictDnsClusterImpl::ResolveTarget::startResolve() {
  ENVOY_LOG(debug, "starting async DNS resolution for {}", dns_address_);
  parent_.info_-&gt;stats().update_attempt_.inc();

  active_query_ = parent_.dns_resolver_-&gt;resolve(
      dns_address_, parent_.dns_lookup_family_,
      [this](const std::list&lt;Network::Address::InstanceConstSharedPtr&gt;&amp;&amp; address_list) -&gt; void {
        active_query_ = nullptr;
        // 异步解析完成，更新指标
        parent_.info_-&gt;stats().update_success_.inc();
         
        // 为每个解析结果创建主机对象HostImpl
        std::unordered_map&lt;std::string, HostSharedPtr&gt; updated_hosts;
        HostVector new_hosts;
        for (const Network::Address::InstanceConstSharedPtr&amp; address : address_list) {
          new_hosts.emplace_back(new HostImpl(
              parent_.info_, dns_address_, Network::Utility::getAddressWithPort(*address, port_),
              lb_endpoint_.metadata(), lb_endpoint_.load_balancing_weight().value(),
              locality_lb_endpoint_.locality(), lb_endpoint_.endpoint().health_check_config(),
              locality_lb_endpoint_.priority()));
        }

        HostVector hosts_added;
        HostVector hosts_removed;
        // 调用BaseDynamicClusterImpl::updateDynamicHostList更新主机列表
        if (parent_.updateDynamicHostList(new_hosts, hosts_, hosts_added, hosts_removed, updated_hosts, all_hosts_)) {
          parent_.updateAllHosts(hosts_added, hosts_removed, locality_lb_endpoint_.priority());
        } else {
          parent_.info_-&gt;stats().update_no_rebuild_.inc();
        }

        all_hosts_ = std::move(updated_hosts);

        // 结束处理，尽管集群可能有多个DNS名称，这里在解析成功一个后就结束初始化过程
        parent_.onPreInitComplete();
        resolve_timer_-&gt;enableTimer(parent_.dns_refresh_rate_ms_);
      });
}</pre>
<div class="blog_h3"><span class="graybg">EdsClusterImpl::startPreInit</span></div>
<p>EDS集群的startPreInit，从启动EDS订阅开始：</p>
<pre class="crayon-plain-tag">void EdsClusterImpl::startPreInit() { subscription_-&gt;start({cluster_name_}, *this); } </pre>
<div class="blog_h3"><span class="graybg">ClusterImplBase::onPreInitComplete</span></div>
<p>预初始化完成后，此时端点列表已经获得。onPreInitComplete执行下面的回调，进行必要的健康检查：</p>
<pre class="crayon-plain-tag">void ClusterImplBase::onPreInitComplete() {
  // 防止重复调用
  if (initialization_started_) {
    return;
  }
  initialization_started_ = true;

  // 执行onInitDone回调
  init_manager_.initialize([this]() { onInitDone(); });
}


void ClusterImplBase::onInitDone() {
  if (health_checker_ &amp;&amp; pending_initialize_health_checks_ == 0) {
    for (auto&amp; host_set : prioritySet().hostSetsPerPriority()) {
      pending_initialize_health_checks_ += host_set-&gt;hosts().size();
    }

    // 在健康检查完毕后调用finishInitialization
    health_checker_-&gt;addHostCheckCompleteCb([this](HostSharedPtr, HealthTransition) -&gt; void {
      if (pending_initialize_health_checks_ &gt; 0 &amp;&amp; --pending_initialize_health_checks_ == 0) {
        finishInitialization();
      }
    });
  }
  // 不需要健康检查，直接调用
  if (pending_initialize_health_checks_ == 0) {
    finishInitialization();
  }
}</pre>
<p>然后调用finishInitialization方法。</p>
<div class="blog_h3"><span class="graybg">ClusterImplBase::finishInitialization</span></div>
<pre class="crayon-plain-tag">void ClusterImplBase::finishInitialization() {
  ASSERT(initialization_complete_callback_ != nullptr);
  ASSERT(initialization_started_);

  auto snapped_callback = initialization_complete_callback_;
  initialization_complete_callback_ = nullptr;
  
  // 重新载入健康（通过健康检查）的主机
  if (health_checker_ != nullptr) {
    reloadHealthyHosts();
  }

  if (snapped_callback != nullptr) {
    // 执行回调，也就是ClusterManagerInitHelper::onClusterInit
    snapped_callback();
  }
}</pre>
<div class="blog_h3"><span class="graybg">ClusterManagerInitHelper.onClusterInit</span></div>
<p>此回调的实现：</p>
<pre class="crayon-plain-tag">void ClusterManagerInitHelper::onClusterInit(Cluster&amp; cluster) {
  // 针对每个集群的初始化回调
  per_cluster_init_callback_(cluster);
  // 将当前集群，移除主集群列表/此集群列表
  removeCluster(cluster);
}

void ClusterManagerInitHelper::removeCluster(Cluster&amp; cluster) {
  if (state_ == State::AllClustersInitialized) {
    return;
  }
  std::list&lt;Cluster*&gt;* cluster_list;
  if (cluster.initializePhase() == Cluster::InitializePhase::Primary) {
    cluster_list = &amp;primary_init_clusters_;
  } else {
    ASSERT(cluster.initializePhase() == Cluster::InitializePhase::Secondary);
    cluster_list = &amp;secondary_init_clusters_;
  }
  cluster_list-&gt;remove(&amp;cluster);
  maybeFinishInitialize();
}</pre>
<p>per_cluster_init_callback_在集群管理器构造时，通过初始化列表传递给ClusterManagerInitHelper，其实现就是集群管理器的onClusterInit方法：</p>
<div class="blog_h3"><span class="graybg">ClusterManagerImpl::onClusterInit</span></div>
<p>集群管理器通过此方法进行集群的后初始化处理：</p>
<pre class="crayon-plain-tag">void ClusterManagerImpl::onClusterInit(Cluster&amp; cluster) {
  // 到这里为止，集群尚未配置以支持跨线程更新
  // ClusterData，持有集群配置、版本信息、Cluster的引用等信息
  auto cluster_data = active_clusters_.find(cluster.info()-&gt;name());
  if (cluster_data-&gt;second-&gt;thread_aware_lb_ != nullptr) {
    // 如果线程感知的负载均衡器已经存在，则初始化之
    cluster_data-&gt;second-&gt;thread_aware_lb_-&gt;initialize();
  }管理器

  // 配置以支持跨线程更新
  // PrioritySet，包含单个集群的所有HostSet，以优先级分组
  // addMemberUpdateCb，如果任何HostSet发生变化，或者新HostSet创建，调用此回调
  cluster.prioritySet().addMemberUpdateCb([&amp;cluster, this](uint32_t priority,
                                                           const HostVector&amp; hosts_added,
                                                           const HostVector&amp; hosts_removed) {
    // 当集群的主机集更新时，此回调触发。需要将此更新发送给所有线程本地配置

    // 某些情况下，合并一个时间窗口内多次主机集更新可以提升性能。目前能实现的、安全的合并，不支持添加、删除
    // 主机的情况。也就是说，仅仅对那些改变主机健康状态/权重/元数据的更新，可以被合并
    bool scheduled = false;
    // 合并时间窗口1000ms
    const auto merge_timeout =
        PROTOBUF_GET_MS_OR_DEFAULT(cluster.info()-&gt;lbConfig(), update_merge_window, 1000);
    // Remember: we only merge updates with no adds/removes — just hc/weight/metadata changes.
    const bool is_mergeable = !hosts_added.size() &amp;&amp; !hosts_removed.size();

    if (merge_timeout &gt; 0) {
      // 尝试调度合并，如果不支持合并，返回false管理器
      scheduled = scheduleUpdate(cluster, priority, is_mergeable, merge_timeout);
    }

    // 无法调度
    if (!scheduled) {
      // 立即向线程本地集群递送更新
      postThreadLocalClusterUpdate(cluster, priority, hosts_added, hosts_removed);
    }
  });

  // 递送第一次集群主机集更新
  // 遍历集群的所有HostSet
  for (auto&amp; host_set : cluster.prioritySet().hostSetsPerPriority()) {
    if (host_set-&gt;hosts().empty()) {
      continue;
    }
    // 对于包含主机的HostSet，向线程本地集群递送更新
    postThreadLocalClusterUpdate(cluster, host_set-&gt;priority(), host_set-&gt;hosts(), HostVector{});
  }
}


void ClusterManagerImpl::postThreadLocalClusterUpdate(const Cluster&amp; cluster, uint32_t priority,
                                                      const HostVector&amp; hosts_added,
                                                      const HostVector&amp; hosts_removed) {
  const auto&amp; host_set = cluster.prioritySet().hostSetsPerPriority()[priority];

  // 拷贝各类主机副本
  HostVectorConstSharedPtr hosts_copy(new HostVector(host_set-&gt;hosts()));
  HostVectorConstSharedPtr healthy_hosts_copy(new HostVector(host_set-&gt;healthyHosts()));
  HostVectorConstSharedPtr degraded_hosts_copy(new HostVector(host_set-&gt;healthyHosts()));
  HostsPerLocalityConstSharedPtr hosts_per_locality_copy = host_set-&gt;hostsPerLocality().clone();
  HostsPerLocalityConstSharedPtr healthy_hosts_per_locality_copy =  host_set-&gt;healthyHostsPerLocality().clone();
  HostsPerLocalityConstSharedPtr degraded_hosts_per_locality_copy = host_set-&gt;degradedHostsPerLocality().clone();

  // 在所有已经注册的线程的Dispatcher上执行回调（异步），并最后在主线程上同步的执行
  // 每个工作线程都有自己的Dispatcher
  // 调用Dispatcher.post()。导致回调在目标线程的Dispatcher事件循环上下文中执行
  tls_-&gt;runOnAllThreads([this, name = cluster.info()-&gt;name(), priority, hosts_copy,
                         healthy_hosts_copy, degraded_hosts_copy, hosts_per_locality_copy,
                         healthy_hosts_per_locality_copy, degraded_hosts_per_locality_copy,
                         locality_weights = host_set-&gt;localityWeights(), hosts_added,
                         hosts_removed]() {
    // 这里应该只对线程本地数据进行操作
    // 调用线程本地集群管理器，更新集群的主机成员
    // 如果使用线程感知LB（TLS集群），则重新创建负载均衡器
    ThreadLocalClusterManagerImpl::updateClusterMembership(
        name, priority,
        HostSetImpl::updateHostsParams(hosts_copy, hosts_per_locality_copy, healthy_hosts_copy,
                                       healthy_hosts_per_locality_copy, degraded_hosts_copy,
                                       degraded_hosts_per_locality_copy),
        locality_weights, hosts_added, hosts_removed, *tls_);
  });
}</pre>
<div class="blog_h3"><span class="graybg">InitHelper::maybeFinishInitialize </span></div>
<p>在每个集群的初始化后都会调用， 判断初始化过程是否可以结束：</p>
<pre class="crayon-plain-tag">void ClusterManagerInitHelper::maybeFinishInitialize() {
  // 如果当前正在进行初始的静态集群的加载，或者正等待CDS初始化完毕，不做任何事情
  if (state_ == State::Loading || state_ == State::WaitingForCdsInitialize) {
    return;
  }

  // 如果主集群没有完全初始化，不做任何事情
  if (!primary_init_clusters_.empty()) {
    return;
  }

  // 如果正在等待次集群初始化
  if (!secondary_init_clusters_.empty()) {
    // 如果次集群初始化阶段尚未开始
    if (!started_secondary_initialize_) {
      // 此触发次集群初始化阶段
      started_secondary_initialize_ = true;
      // Cluster::initialize()方法可能修改列表secondary_init_clusters_，移除当前正在初始化的集群
      for (auto iter = secondary_init_clusters_.begin(); iter != secondary_init_clusters_.end();) {
        Cluster* cluster = *iter;
        ++iter;
        // 初始化次集群
        cluster-&gt;initialize([cluster, this] { onClusterInit(*cluster); });
      }
    }
    return;
  }

  started_secondary_initialize_ = false;
  // 等待静态初始化，且需要CDS
  if (state_ == State::WaitingForStaticInitialize &amp;&amp; cds_) {
    // 进行CDS的初始化
    state_ = State::WaitingForCdsInitialize;
    cds_-&gt;initialize();
  } else {
    // 所有集群已经初始化完毕
    state_ = State::AllClustersInitialized;
    if (initialized_callback_) {
      initialized_callback_();
    }
  }
}</pre>
<div class="blog_h3"><span class="graybg">ClusterManagerImpl::setInitializedCb</span></div>
<p>调用此方法，可以设置在所有集群都初始化之后，调用的回调。</p>
<p>RunHelper会调用此函数，注册的回调会导致InitManager初始化，后者会导致：</p>
<ol>
<li>LdsApiImpl，注册到InitManager的Target，被初始化</li>
<li>LdsApiImpl初始化完毕后，工作进程启动</li>
</ol>
<div class="blog_h2"><span class="graybg">监听器初始化</span></div>
<div class="blog_h3"><span class="graybg">ListenerManagerImpl</span></div>
<p>在InstanceImpl初始化阶段，它会创建全局的监听器管理器：</p>
<pre class="crayon-plain-tag">listener_manager_ = std::make_unique&lt;ListenerManagerImpl&gt;(*this, listener_component_factory_,
                                                            worker_factory_, time_system_);</pre>
<p>构造函数如下：</p>
<pre class="crayon-plain-tag">ListenerManagerImpl::ListenerManagerImpl(Instance&amp; server,
                                         ListenerComponentFactory&amp; listener_factory,
                                         WorkerFactory&amp; worker_factory, TimeSource&amp; time_source)
    : server_(server), time_source_(time_source), factory_(listener_factory),
      stats_(generateStats(server.stats())),
      config_tracker_entry_(server.admin().getConfigTracker().add(
          "listeners", [this] { return dumpListenerConfigs(); })) {
  // 创建工作线程
  for (uint32_t i = 0; i &lt; server.options().concurrency(); i++) {
    workers_.emplace_back(worker_factory.createWorker(server.overloadManager()));
  }
}</pre>
<div class="blog_h3"><span class="graybg">MainImpl::initialize</span></div>
<p>在主配置初始化阶段，静态监听器被创建：</p>
<pre class="crayon-plain-tag">const auto&amp; listeners = bootstrap.static_resources().listeners();
for (ssize_t i = 0; i &lt; listeners.size(); i++) {
  server.listenerManager().addOrUpdateListener(listeners[i], "", false);
}</pre>
<div class="blog_h3"><span class="graybg">ListenerManagerImpl::addOrUpdateListener</span></div>
<p>MainImpl::initialize将监听器创建工作委托给监听器管理器完成：</p>
<pre class="crayon-plain-tag">bool ListenerManagerImpl::addOrUpdateListener(const envoy::api::v2::Listener&amp; config,
                                              const std::string&amp; version_info, bool modifiable) {
  // 如果监听器没有配置名称，随机生成
  std::string name;
  if (!config.name().empty()) {
    name = config.name();
  } else {
    name = server_.random().uuid();
  }
  const uint64_t hash = MessageUtil::hash(config);
  ENVOY_LOG(debug, "begin add/update listener: name={} hash={}", name, hash);

  auto existing_active_listener = getListenerByName(active_listeners_, name);
  auto existing_warming_listener = getListenerByName(warming_listeners_, name);

  if ((existing_warming_listener != warming_listeners_.end() &amp;&amp;
       (*existing_warming_listener)-&gt;blockUpdate(hash)) ||
      (existing_active_listener != active_listeners_.end() &amp;&amp;
       (*existing_active_listener)-&gt;blockUpdate(hash))) {
    // 重复或者被锁定的监听器，不得更新
    return false;
  }

  // 实例化监听器对象
  ListenerImplPtr new_listener(
      new ListenerImpl(config, version_info, *this, name, modifiable, workers_started_, hash));
  ListenerImpl&amp; new_listener_ref = *new_listener;

  // 强制要求相同名字的监听器配置必须具有相同的IP地址，以避免更新期间出现冲突，兵却让我们可以使用相同的绑定地址
  // 需要注意，如果绑定到端口0（让内核自由选择端口），新监听器会使用对应的旧监听器所监听的端口
  if ((existing_warming_listener != warming_listeners_.end() &amp;&amp;
       *(*existing_warming_listener)-&gt;address() != *new_listener-&gt;address()) ||
      (existing_active_listener != active_listeners_.end() &amp;&amp;
       *(*existing_active_listener)-&gt;address() != *new_listener-&gt;address())) {
    throw EnvoyException("监听地址发生变化，不允许");
  }

  bool added = false;
  if (existing_warming_listener != warming_listeners_.end()) {
    // 现有监听器在预热阶段，获取现有监听器的套接字，然后替换原先的监听器
    new_listener-&gt;setSocket((*existing_warming_listener)-&gt;getSocket());
    *existing_warming_listener = std::move(new_listener);
  } else if (existing_active_listener != active_listeners_.end()) {
    // 现有监听器是激活的，工作线程是否已经启动，影响处理方式
    // 但是不管工作线程是否启动，都需要从现有监听器将套接字拿过来
    new_listener-&gt;setSocket((*existing_active_listener)-&gt;getSocket());
    if (workers_started_) {
      // 工作线程已经启动，加入到预热列表
      warming_listeners_.emplace_back(std::move(new_listener));
    } else {
      // 工作线程尚未启动，替换激活列表中现有监听器
      *existing_active_listener = std::move(new_listener);
    }
  } else {
    if (!new_listener-&gt;bindToPort() &amp;&amp;
        (hasListenerWithAddress(warming_listeners_, *new_listener-&gt;address()) ||
         hasListenerWithAddress(active_listeners_, *new_listener-&gt;address()))) {
      const std::string message =
      throw EnvoyException("无法添加，现有监听器的地址和当前监听器的重复");
    }

    // 新添加的监听器，工作线程是否已经启动影响处理方式

    // 查找是否存在draining的监听器绑定到相同地址
    Network::SocketSharedPtr draining_listener_socket;
    auto existing_draining_listener = std::find_if(
        draining_listeners_.cbegin(), draining_listeners_.cend(),
        [&amp;new_listener](const DrainingListener&amp; listener) {
          return *new_listener-&gt;address() == *listener.listener_-&gt;socket().localAddress();
        });
    if (existing_draining_listener != draining_listeners_.cend()) {
      // Draining监听器已经监听了我们的套接字，这是一个边缘情况（Edge case）
      // 发生的原因可能是监听器移除，然后由很快被添加回来（使用相同的地址，相同或不同的名称）
      draining_listener_socket = existing_draining_listener-&gt;listener_-&gt;getSocket();
    }

    // 为新监听器设置套接字，使用draining的
    new_listener-&gt;setSocket(draining_listener_socket
                                ? draining_listener_socket
                                // 或者创建新的
                                : factory_.createListenSocket(new_listener-&gt;address(),
                                                              new_listener-&gt;listenSocketOptions(),
                                                              new_listener-&gt;bindToPort()));
    if (workers_started_) {
      // 如果工作线程已经启动，作为预热监听器添加
      warming_listeners_.emplace_back(std::move(new_listener));
    } else {
      // 否则作为激活监听器添加
      active_listeners_.emplace_back(std::move(new_listener));
    }

    added = true;
  }
  
  // 执行监听器的初始化
  new_listener_ref.initialize();
  return true;
}</pre>
<div class="blog_h3"><span class="graybg">ListenerImpl</span></div>
<p>如果需要创建新的监听器，则监听器管理器会调用下面的构造函数：</p>
<pre class="crayon-plain-tag">ListenerImpl::ListenerImpl(const envoy::api::v2::Listener&amp; config, const std::string&amp; version_info,
                           ListenerManagerImpl&amp; parent, const std::string&amp; name, bool modifiable,
                           bool workers_started, uint64_t hash)
      // 监听器管理器    监听地址
    : parent_(parent), address_(Network::Address::resolveProtoAddress(config.address())),
      // 全局的监控指标scope
      global_scope_(parent_.server_.stats().createScope("")),
      // 监听器的监控指标scope
      listener_scope_(
          parent_.server_.stats().createScope(fmt::format("listener.{}.", address_-&gt;asString()))),
      // 是否需要绑定到端口
      bind_to_port_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(config.deprecated_v1(), bind_to_port, true)),
      // 是否直接转发给原始目的地对应的监听器
      hand_off_restored_destination_connections_(
          PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, use_original_dst, false)),
      // 每个连接的缓冲区限额
      per_connection_buffer_limit_bytes_(
          PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, per_connection_buffer_limit_bytes, 1024 * 1024)),
      // 监听器Tag，用于连接处理器跟踪
      listener_tag_(parent_.factory_.nextListenerTag()), name_(name),
      reverse_write_filter_order_(
          PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, bugfix_reverse_write_filter_order, true)),
      // 监听器是否可修改或删除    
                               // 监听器是在工作线程启动前，还是启动后添加的
                                                                  // 查重哈希
      modifiable_(modifiable), workers_started_(workers_started), hash_(hash),
      // Drain管理器，负责处理连接的draining
      local_drain_manager_(parent.factory_.createDrainManager(config.drain_type())),
      // 配置           配置版本信息
      config_(config), version_info_(version_info),
      // 过滤器处理超时
      listener_filters_timeout_(
          PROTOBUF_GET_MS_OR_DEFAULT(config, listener_filters_timeout, 15000)) {
  // 允许侦听任意地址
  if (config.has_transparent()) {
    addListenSocketOptions(Network::SocketOptionFactory::buildIpTransparentOptions());
  }
  // 允许绑定到非本地的，以及目前不存在的地址
  if (config.has_freebind()) {
    addListenSocketOptions(Network::SocketOptionFactory::buildIpFreebindOptions());
  }
  // TFO，简化握手，提高连接打开速度
  if (config.has_tcp_fast_open_queue_length()) {
    addListenSocketOptions(Network::SocketOptionFactory::buildTcpFastOpenOptions(
        config.tcp_fast_open_queue_length().value()));
  }

  // 其它套接字选项
  if (config.socket_options().size() &gt; 0) {
    addListenSocketOptions(
        Network::SocketOptionFactory::buildLiteralOptions(config.socket_options()));
  }

  // 如果监听器过滤器不为空，则创建监听器过滤器工厂（的列表）
  if (!config.listener_filters().empty()) {
    listener_filter_factories_ =
        parent_.factory_.createListenerFilterFactoryList(config.listener_filters(), *this);
  }
  // 如果设置了use_original_dst标记，强制添加original dst这个监听器过滤器
  if (PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, use_original_dst, false)) {
    auto&amp; factory =
        Config::Utility::getAndCheckFactory&lt;Configuration::NamedListenerFilterConfigFactory&gt;(
            Extensions::ListenerFilters::ListenerFilterNames::get().OriginalDst);
    listener_filter_factories_.push_back(
        factory.createFilterFactoryFromProto(Envoy::ProtobufWkt::Empty(), *this));
  }
  // 如果设置了use_proxy_proto标记，强制添加代理协议监听器
  // 此监听器位于监听器过滤器链的尾部
  if (PROTOBUF_GET_WRAPPED_OR_DEFAULT(config.filter_chains()[0], use_proxy_proto, false)) {
    auto&amp; factory =
        Config::Utility::getAndCheckFactory&lt;Configuration::NamedListenerFilterConfigFactory&gt;(
            Extensions::ListenerFilters::ListenerFilterNames::get().ProxyProtocol);
    listener_filter_factories_.push_back(
        factory.createFilterFactoryFromProto(Envoy::ProtobufWkt::Empty(), *this));
  }

  bool need_tls_inspector = false;
  // 存放所有过滤器链匹配
  std::unordered_set&lt;envoy::api::v2::listener::FilterChainMatch, MessageUtil, MessageUtil&gt; filter_chains;

  // 遍历过滤器链
  for (const auto&amp; filter_chain : config.filter_chains()) {
    const auto&amp; filter_chain_match = filter_chain.filter_chain_match();
    if (filter_chains.find(filter_chain_match) != filter_chains.end()) {
      throw EnvoyException("多个过滤器链使用了相同的匹配规则");
    }
    filter_chains.insert(filter_chain_match);

    // 如果没有配置传输套接字，那么，默认：
    // 1、对于普通TCP流量，使用raw_buffer
    // 2、对于TLS流量，使用基于BoringSSL的tls
    auto transport_socket = filter_chain.transport_socket();
    if (!filter_chain.has_transport_socket()) {
      if (filter_chain.has_tls_context()) {
        transport_socket.set_name(Extensions::TransportSockets::TransportSocketNames::get().Tls);
        MessageUtil::jsonConvert(filter_chain.tls_context(), *transport_socket.mutable_config());
      } else {
        transport_socket.set_name(
            Extensions::TransportSockets::TransportSocketNames::get().RawBuffer);
      }
    }

    // DownstreamTransportSocketConfigFactory，每个用于下游连接的传输套接字（例如RawBufferSocketFactory）实现此接口
    // 其createTransportSocketFactory方法返回传输套接字的工厂
    auto&amp; config_factory = Config::Utility::getAndCheckFactory&lt;
        Server::Configuration::DownstreamTransportSocketConfigFactory&gt;(transport_socket.name());
    // 转换为上述工厂的配置信息
    ProtobufTypes::MessagePtr message =
        Config::Utility::translateToFactoryConfig(transport_socket, config_factory);

    // 可以基于IP地址CIDR匹配
    std::vector&lt;std::string&gt; destination_ips;
    for (const auto&amp; destination_ip : filter_chain_match.prefix_ranges()) {
      const auto&amp; cidr_range = Network::Address::CidrRange::create(destination_ip);
      destination_ips.push_back(cidr_range.asString());
    }

    // 可以基于服务器名称（例如TLS协议的SNI）匹配
    std::vector&lt;std::string&gt; server_names(filter_chain_match.server_names().begin(),
                                          filter_chain_match.server_names().end());

    // 不支持局部通配符名称，仅仅支持*.gmem.cc，而不支持www.*.cc
    for (const auto&amp; server_name : server_names) {
      if (server_name.find('*') != std::string::npos &amp;&amp; !isWildcardServerName(server_name)) {
        throw EnvoyException();
      }
    }

    // 可以基于应用层协议匹配
    std::vector&lt;std::string&gt; application_protocols(
        filter_chain_match.application_protocols().begin(),
        filter_chain_match.application_protocols().end());

    // 传输套接字工厂上下文
    Server::Configuration::TransportSocketFactoryContextImpl factory_context(
        parent_.server_.sslContextManager(), *listener_scope_, parent_.server_.clusterManager(),
        parent_.server_.localInfo(), parent_.server_.dispatcher(), parent_.server_.random(),
        parent_.server_.stats());
    // 关联初始化管理器，此管理器作为动态密钥提供者
    factory_context.setInitManager(initManager());
    // 添加此过滤器链配置
    addFilterChain(
        PROTOBUF_GET_WRAPPED_OR_DEFAULT(filter_chain_match, destination_port, 0), destination_ips,
        server_names, filter_chain_match.transport_protocol(), application_protocols,
        filter_chain_match.source_type(),
        // 创建传输套接字工厂
        config_factory.createTransportSocketFactory(*message, factory_context, server_names),
        parent_.factory_.createNetworkFilterFactoryList(filter_chain.filters(), *this));

    need_tls_inspector |= filter_chain_match.transport_protocol() == "tls" ||
                          (filter_chain_match.transport_protocol().empty() &amp;&amp;
                           (!server_names.empty() || !application_protocols.empty()));
  }

  // 用于更快的查找
  convertDestinationIPsMapToTrie();

  // 如果需要，且没有明确配置，自动注入TLS Inspector这个监听器过滤器
  if (need_tls_inspector) {
    for (const auto&amp; filter : config.listener_filters()) {
      if (filter.name() == Extensions::ListenerFilters::ListenerFilterNames::get().TlsInspector) {
        need_tls_inspector = false;
        break;
      }
    }
    if (need_tls_inspector) {
      // 过滤器链规则依赖TLS Inspector但是没有明确配置，这里进行注入
      // 如果没有编译TLS Inspector到Envoy二进制文件中，注入会失败
      auto&amp; factory =
          Config::Utility::getAndCheckFactory&lt;Configuration::NamedListenerFilterConfigFactory&gt;(
              Extensions::ListenerFilters::ListenerFilterNames::get().TlsInspector);
      listener_filter_factories_.push_back(
          factory.createFilterFactoryFromProto(Envoy::ProtobufWkt::Empty(), *this));
    }
  }
}</pre>
<div class="blog_h3"><span class="graybg">ListenerComponentFactory::createListenSocket</span></div>
<p>监听器管理器创建新的监听器后，可能需要为其创建监听套接字，这是通过调用ListenerComponentFactory实现的：</p>
<pre class="crayon-plain-tag">Network::SocketSharedPtr ProdListenerComponentFactory::createListenSocket(Network::Address::InstanceConstSharedPtr address,
                                                 const Network::Socket::OptionsSharedPtr&amp; options, bool bind_to_port) {

  // 对于每个监听器配置，跨越所有工作线程共享单个网络监听套接字

  // 使用UNIX管道（UDS）
  if (address-&gt;type() == Network::Address::Type::Pipe) {
    const std::string addr = fmt::format("unix://{}", address-&gt;asString());
    const int fd = server_.hotRestart().duplicateParentListenSocket(addr);
    if (fd != -1) {
      return std::make_shared&lt;Network::UdsListenSocket&gt;(fd, address);
    }
    return std::make_shared&lt;Network::UdsListenSocket&gt;(address);
  }
  // 使用TCP
  const std::string addr = fmt::format("tcp://{}", address-&gt;asString());
  // 尝试通过IPC请求，从父进程获取套接字文件描述符
  const int fd = server_.hotRestart().duplicateParentListenSocket(addr);
  if (fd != -1) {
    // 获取到了
    return std::make_shared&lt;Network::TcpListenSocket&gt;(fd, address, options);
  }
  // 自行创建套接字
  return std::make_shared&lt;Network::TcpListenSocket&gt;(address, options, bind_to_port);
}

// 监听套接字构造函数
NetworkListenSocket(const Address::InstanceConstSharedPtr&amp; address,
                    const Network::Socket::OptionsSharedPtr&amp; options, bool bind_to_port)
                      // 进行系统调用，创建socket，返回其句柄
    : ListenSocketImpl(address-&gt;socket(T::type), address) {
  RELEASE_ASSERT(fd_ != -1, "");

  // 绑定前设置套接字选项
  setPrebindSocketOptions();
  // 创建底层套接字
  setupSocket(options, bind_to_port);
}
// 套接字选项：重用地址
void NetworkListenSocket&lt;NetworkSocketTrait&lt;Address::SocketType::Stream&gt;&gt;::setPrebindSocketOptions() {
  int on = 1;
  // 提供系统调用封装
  auto&amp; os_syscalls = Api::OsSysCallsSingleton::get();
  Api::SysCallIntResult status = os_syscalls.setsockopt(fd_, SOL_SOCKET, SO_REUSEADDR, &amp;on, sizeof(on));
}
// 创建底层套接字
void ListenSocketImpl::setupSocket(const Network::Socket::OptionsSharedPtr&amp; options, bool bind_to_port) {
  // 监听套接字选项
  setListenSocketOptions(options);
  if (bind_to_port) {
     // 绑定套接字到地址
     const Api::SysCallIntResult result = local_address_-&gt;bind(fd_);
  }
}</pre>
<div class="blog_h3"><span class="graybg">ListenerImpl::initialize</span></div>
<p>监听器管理创建/更新监听器后，会调用此方法：</p>
<pre class="crayon-plain-tag">void ListenerImpl::initialize() {
  last_updated_ = timeSource().systemTime();
  // 如果工作线程已经启动，则不使用全局的Init管理器，而使用局部的/每个监听器专有的初始化管理器
  if (workers_started_) {
    dynamic_init_manager_.initialize([this]() -&gt; void {
      if (!initialize_canceled_) {
        // 预热后回调
        parent_.onListenerWarmed(*this);
      }
    });
  }
} </pre>
<div class="blog_h2"><span class="graybg">工作线程</span></div>
<div class="blog_h3"><span class="graybg">创建</span></div>
<p>工作线程对象（WorkerImpl）是由监听器管理器在其构造函数中创建的。 ProdWorkerFactory::createWorker的实现如下：</p>
<pre class="crayon-plain-tag">WorkerPtr ProdWorkerFactory::createWorker(OverloadManager&amp; overload_manager) {
  // 为工作线程创建一个事件分发器
  Event::DispatcherPtr dispatcher(api_.allocateDispatcher(time_system_));
  return WorkerPtr{new WorkerImpl(
      tls_, hooks_, std::move(dispatcher),
      // ConnectionHandler用于增删改查启禁监听器
      Network::ConnectionHandlerPtr{new ConnectionHandlerImpl(ENVOY_LOGGER(), *dispatcher)},
      overload_manager, api_)};
}</pre>
<p>工作线程的构造函数如下：</p>
<pre class="crayon-plain-tag">WorkerImpl::WorkerImpl(ThreadLocal::Instance&amp; tls, TestHooks&amp; hooks,
                       Event::DispatcherPtr&amp;&amp; dispatcher, Network::ConnectionHandlerPtr handler,
                       OverloadManager&amp; overload_manager, Api::Api&amp; api)
    : tls_(tls), hooks_(hooks), dispatcher_(std::move(dispatcher)), handler_(std::move(handler)),
      api_(api) {
  // 注册线程到TLS
  tls_.registerThread(*dispatcher_, false);
  // 过载后的行为，不再接受新连接
  overload_manager.registerForAction(
      OverloadActionNames::get().StopAcceptingConnections, *dispatcher_,
      [this](OverloadActionState state) { stopAcceptingConnectionsCb(state); });
}</pre>
<p>需要注意，注册到TLS时，使用的是线程的分发器，而不是线程本身。</p>
<div class="blog_h3"><span class="graybg">启动</span></div>
<p>工作线程的启动，默认配置下，是由RunHelper注册给集群管理器的setInitializedCb回调触发的。此回调会初始化InitManager，默认配置下，注册到InitManager的唯一Target是LdsApiImpl。而InitManager的总回调就是InstanceImpl::startWorkers方法。</p>
<p>也就是说，当LDS客户端初始化完毕后，工作线程才会启动。</p>
<p>Envoy服务实例提供了启动线程的方法：</p>
<pre class="crayon-plain-tag">void InstanceImpl::startWorkers() {
  // 转调监听器管理器
  listener_manager_-&gt;startWorkers(*guard_dog_);
  // 到这里，所有监听端口已经正常运作，可以接管（热重启场景下）所有流量了
  // 通知父进程，drain掉所有它的监听器
  restarter_.drainParentListeners();
  // 下面的方法在新发动的主进程中调用，开始父进程的关闭逻辑，最终导致原来的主（父）进程终结
  drain_manager_-&gt;startParentShutdownSequence();
}</pre>
<p>监听器管理器的同名方法内容如下：</p>
<pre class="crayon-plain-tag">void ListenerManagerImpl::startWorkers(GuardDog&amp; guard_dog) {
  // 所有依赖初始化完毕，启动工作线程
  workers_started_ = true;
  for (const auto&amp; worker : workers_) {
    // 此时应该没有处于预热中的监听器
    for (const auto&amp; listener : active_listeners_) {
      // 将监听器添加到工作线程
      addListenerToWorker(*worker, *listener);
    }
    worker-&gt;start(guard_dog);
  }
}</pre>
<p>WorkerImpl::start方法就是启动一个物理线程：</p>
<pre class="crayon-plain-tag">void WorkerImpl::start(GuardDog&amp; guard_dog) {
  thread_ = api_.threadFactory().createThread([this, &amp;guard_dog]() -&gt; void { threadRoutine(guard_dog); });
}</pre>
<p>在新线程中，会执行下面的函数：</p>
<pre class="crayon-plain-tag">void WorkerImpl::threadRoutine(GuardDog&amp; guard_dog) {
  // 和主线程类似，先创建看门狗
  auto watchdog = guard_dog.createWatchDog(api_.threadFactory().currentThreadId());
  watchdog-&gt;startWatchdog(*dispatcher_);
  // 然后阻塞的运行此线程的事件循环
  dispatcher_-&gt;run(Event::Dispatcher::RunType::Block);
  // 事件循环退出，进程需要退出，关闭看门狗
  guard_dog.stopWatching(watchdog);

  // 在实际退出线程之前，必须关闭所有活动的网络连接
  // 这会阻止主线程运行任何可能引用线程本地变量的析构函数
  // 析构网络连接处理器
  handler_.reset();
  // 准备退出当前线程，所有线程本地变量将被释放
  tls_.shutdownThread();
  watchdog.reset();
}</pre>
<p>只要当线程真正启动后，它的事件循环才会运作，  ListenerManagerImpl::addListenerToWorker注册的关联监听器到工作线程的请求才会被执行。</p>
<div class="blog_h3"><span class="graybg">关联监听器</span></div>
<p>在工作线程启动之前，需要关联到监听器：</p>
<pre class="crayon-plain-tag">void ListenerManagerImpl::addListenerToWorker(Worker&amp; worker, ListenerImpl&amp; listener) {
  // 需要添加的监听器             添加成功或失败后执行的回调
  worker.addListener(listener, [this, &amp;listener](bool success) -&gt; void {
    // 这些逻辑会在工作线程上执行，不会阻塞主线程的的post调用
    server_.dispatcher().post([this, success, &amp;listener]() -&gt; void {
      // 理论上说，可能出现监听器添加到一个工作线程，却无法添加到其它工作线程的情况
      if (!success &amp;&amp; !listener.onListenerCreateFailure()) {
        // 除了记录日志以外，可能需要添加一个服务器选项，在发生这种以外时退出整个服务器
        ENVOY_LOG(critical, "listener '{}' failed to listen on address '{}' on worker", listener.name(), listener.socket().localAddress()-&gt;asString());
        stats_.listener_create_failure_.inc();
        // 移除监听器
        removeListener(listener.name());
      }
    });
  });
}

void WorkerImpl::addListener(Network::ListenerConfig&amp; listener, AddListenerCompletion completion) {
  // 监听器添加动作是在工作线程的Dispatcher.post中，异步的、在Dispatcher的事件循环上下文中执行的
  // 我们必须处理监听器无法成功添加的情况：存在一种竞态条件，两个进程可以成功绑定到一个地址，但是以EADDRINUSE listen()却失败
  dispatcher_-&gt;post([this, &amp;listener, completion]() -&gt; void {
    try {
      // 调用此线程的ConnectionHandler，添加监听器
      handler_-&gt;addListener(listener);
      // 监听器添加完成后的钩子
      hooks_.onWorkerListenerAdded();
      // 执行回调
      completion(true);
    } catch (const Network::CreateListenerException&amp; e) {
      // 执行回调
      completion(false);
    }
  });
}


void ConnectionHandlerImpl::addListener(Network::ListenerConfig&amp; config) {
  // 此连接处理器所管理的监听器对象
  ActiveListenerPtr l(new ActiveListener(*this, config));
  if (disable_listeners_) {
    l-&gt;listener_-&gt;disable();
  }
  listeners_.emplace_back(config.socket().localAddress(), std::move(l));
} </pre>
<div class="blog_h2"><span class="graybg">ADS客户端</span></div>
<p>在集群管理器初始化一节中，我们了解到，如果Bootstrap配置中包含了ADS配置，则集群管理器会创建<span style="background-color: #c0c0c0;">GrpcMuxImpl —— ADS API的客户端实现</span>，用于通过gRPC协议，在连接到管理服务器的单个流中<span style="background-color: #c0c0c0;">管理多个gRPC订阅</span>。GrpcMuxImpl支持ADS订阅的同时，也可用于单种xDS API，例如EDS。</p>
<div class="blog_h3"><span class="graybg">初始化</span></div>
<p>GrpcMuxImpl的start方法包括延迟创建到管理服务器的流的逻辑：</p>
<pre class="crayon-plain-tag">void GrpcMuxImpl::start() { establishNewStream(); }</pre>
<p>集群管理器在初始化完毕所有静态集群后，会调用上述方法。 </p>
<p>GrpcMuxImpl的构造函数中也包括延迟创建到管理服务器的流的逻辑：</p>
<pre class="crayon-plain-tag">//             创建定时器
retry_timer_ = dispatcher.createTimer([this]() -&gt; void { establishNewStream(); });</pre>
<p>定时器是libevent支持的一种事件触发方式。在定时器到期后，会执行GrpcMuxImpl::establishNewStream方法：</p>
<pre class="crayon-plain-tag">void GrpcMuxImpl::establishNewStream() {
  // 异步客户端，创建gRPC双向流。将GrpcMuxImpl自身作为回调（AsyncStreamCallbacks）传入
  stream_ = async_client_-&gt;start(service_method_, *this);
  if (stream_ == nullptr) {
    handleFailure(); // 通过定时器重试
    return;
  }  
  // 设置统计指标
  control_plane_stats_.connected_state_.set(1);
  // subscriptions_为订阅列表，最初值为：type.googleapis.com/envoy.api.v2.Cluster
  for (const auto type_url : subscriptions_) {
    queueDiscoveryRequest(type_url);
  }
}</pre>
<p>queueDiscoveryRequest会调用sendDiscoveryRequest发送xDS请求：</p>
<pre class="crayon-plain-tag">void GrpcMuxImpl::queueDiscoveryRequest(const std::string&amp; type_url) {
  // 排队，然后触发所有请求处理
  request_queue_.push(type_url);
  drainRequests();
}
void GrpcMuxImpl::drainRequests() {
  while (!request_queue_.empty()) {
    if (!rate_limiting_enabled_ || limit_request_-&gt;consume()) {
      // 不限速
      // 逐个发送请求
      sendDiscoveryRequest(request_queue_.front());
      request_queue_.pop();
    } else {
      // 限速
      drain_request_timer_-&gt;enableTimer(
          std::chrono::milliseconds(limit_request_-&gt;nextTokenAvailableMs()));
      break;
    }
  }
}
void GrpcMuxImpl::sendDiscoveryRequest(const std::string&amp; type_url) {
  ...
  stream_-&gt;sendMessage(request, false);
  ...
}</pre>
<p>除了establishNewStream以外，subscribe方法也会调用queueDiscoveryRequest。<span style="background-color: #c0c0c0;">各种xDS API的客户端实现，例如CdsApiImpl，都会调用subscribe方法</span>，来订阅某种资源：</p>
<pre class="crayon-plain-tag">GrpcMuxWatchPtr GrpcMuxImpl::subscribe(const std::string&amp; type_url,
                                       const std::vector&lt;std::string&gt;&amp; resources,
                                       GrpcMuxCallbacks&amp; callbacks) {
  // GrpcMuxWatch是多路复用gRPC订阅的句柄，此句柄销毁后，订阅被取消
  auto watch = std::unique_ptr&lt;GrpcMuxWatch&gt;(new GrpcMuxWatchImpl(resources, callbacks, type_url, *this));

  // 如果这种类型的API尚未订阅，则订阅之
  if (!api_state_[type_url].subscribed_) {
    api_state_[type_url].request_.set_type_url(type_url);
    api_state_[type_url].request_.mutable_node()-&gt;MergeFrom(local_info_.node());
    api_state_[type_url].subscribed_ = true;
    subscriptions_.emplace_back(type_url);
  }
  // 发送订阅请求
  queueDiscoveryRequest(type_url);
  return watch;
} </pre>
<div class="blog_h3"><span class="graybg">处理推送</span></div>
<p>ADS客户端启动后，Istio Pilot随时可能将动态的集群信息推送过来。</p>
<p>推送信息经过一系列的处理（包括Envoy的过滤器链）后，被onReceiveMessage处理：</p>
<pre class="crayon-plain-tag">void GrpcMuxImpl::onReceiveMessage(std::unique_ptr&lt;envoy::api::v2::DiscoveryResponse&gt;&amp;&amp; message) {
  // type_url，例如 type.googleapis.com/envoy.api.v2.Cluster
  const std::string&amp; type_url = message-&gt;type_url();
  if (api_state_.count(type_url) == 0) {
    // 当前没有订阅者
    return;
  }
  if (api_state_[type_url].watches_.empty()) {
    // 更新nonce值，此值由服务器提供，标识最后一次响应
    api_state_[type_url].request_.set_response_nonce(message-&gt;nonce());
    if (message-&gt;resources().empty()) {
      // 没有订阅句柄也没有资源，可能的原因是，Envoy解除了某个资源的订阅，而且此资源也从管理服务器上删除掉了
      // 例如，一个被删除的集群，触发对ClusterLoadAssignment的unwatch，与此同时XDS服务器发送了
      // ClusterLoadAssignment的空列表过来
      // 接受此资源，但是不再发送请求，原因是没有watch了
      api_state_[type_url].request_.set_version_info(message-&gt;version_info());
    } else {
      // 没有订阅句柄（GrpcMuxWatchImpl），但是获取了资源，不应该发生
      // 发送一个NACK（不更新version_info）
      queueDiscoveryRequest(type_url);
    }
    return;
  }
  try {
    // 使用一个新的映射存储资源
    std::unordered_map&lt;std::string, ProtobufWkt::Any&gt; resources;
    GrpcMuxCallbacks&amp; callbacks = api_state_[type_url].watches_.front()-&gt;callbacks_;
    for (const auto&amp; resource : message-&gt;resources()) {
      if (type_url != resource.type_url()) {
        throw EnvoyException("type_url不匹配");
      }
      const std::string resource_name = callbacks.resourceName(resource);
      resources.emplace(resource_name, resource);
    }
    // 遍历订阅句柄
    for (auto watch : api_state_[type_url].watches_) {
       // 如果当前没有资源
      if (watch-&gt;resources_.empty()) {
        // 执行SubscriptionCallbacks.onConfigUpdate回调
        // 各资源客户端，例如CdsApiImpl，就是Config::SubscriptionCallbacks&lt;envoy::api::v2::Cluster&gt;的实现
        // 这导致转调CdsApiImpl.onConfigUpdate
        watch-&gt;callbacks_.onConfigUpdate(message-&gt;resources(), message-&gt;version_info());
        continue;
      }
      // 如果当前有资源，则针对新资源调用onConfigUpdate
      Protobuf::RepeatedPtrField&lt;ProtobufWkt::Any&gt; found_resources;
      for (auto watched_resource_name : watch-&gt;resources_) {
        auto it = resources.find(watched_resource_name);
        if (it != resources.end()) {
          found_resources.Add()-&gt;MergeFrom(it-&gt;second);
        }
      }
      watch-&gt;callbacks_.onConfigUpdate(found_resources, message-&gt;version_info());
    }
    // 更新资源版本号
    api_state_[type_url].request_.set_version_info(message-&gt;version_info());
  } catch (const EnvoyException&amp; e) {
    for (auto watch : api_state_[type_url].watches_) {
      // 调用更新失败回调
      watch-&gt;callbacks_.onConfigUpdateFailed(&amp;e);
    }
    ::google::rpc::Status* error_detail = api_state_[type_url].request_.mutable_error_detail();
    error_detail-&gt;set_code(Grpc::Status::GrpcStatus::Internal);
    error_detail-&gt;set_message(e.what());
  }
  // 更新nonce
  api_state_[type_url].request_.set_response_nonce(message-&gt;nonce());
  // 发起下一次发现请求
  queueDiscoveryRequest(type_url);
} </pre>
<p>可以看到onReceiveMessage会转调各订阅者提供的回调函数，完毕后则会触发下一次发现请求。</p>
<div class="blog_h2"><span class="graybg">CDS客户端</span></div>
<div class="blog_h3"><span class="graybg">初始化</span></div>
<p>CDS客户端是在集群管理器初始化阶段创建的：</p>
<pre class="crayon-plain-tag">ClusterManagerImpl::ClusterManagerImpl(){
  if (bootstrap.dynamic_resources().has_cds_config()) {
    // ProdClusterManagerFactory::createCds
    //   CdsApiImpl::create
    cds_api_ = factory_.createCds(bootstrap.dynamic_resources().cds_config(), eds_config_, *this);
    init_helper_.setCds(cds_api_.get());
  } else {
    init_helper_.setCds(nullptr);
  }
}</pre>
<p>构造函数如下：</p>
<pre class="crayon-plain-tag">CdsApiImpl::CdsApiImpl(const envoy::api::v2::core::ConfigSource&amp; cds_config,
                       const absl::optional&lt;envoy::api::v2::core::ConfigSource&gt;&amp; eds_config,
                       ClusterManager&amp; cm, Event::Dispatcher&amp; dispatcher,
                       Runtime::RandomGenerator&amp; random, const LocalInfo::LocalInfo&amp; local_info,
                       Stats::Scope&amp; scope)
               // 创建一个stats的scope（前缀）
    : cm_(cm), scope_(scope.createScope("cluster_manager.cds.")) {
  // 检查本地节点信息
  Config::Utility::checkLocalInfo("cds", local_info);

  subscription_ =
      Config::SubscriptionFactory::subscriptionFromConfigSource&lt;envoy::api::v2::Cluster&gt;(
          cds_config, local_info, dispatcher, cm, random, *scope_,
          // 遗留的REST构造器
          [this, &amp;cds_config, &amp;eds_config, &amp;cm, &amp;dispatcher, &amp;random, &amp;local_info,
           &amp;scope]() -&gt; Config::Subscription&lt;envoy::api::v2::Cluster&gt;* {
            return new CdsSubscription(Config::Utility::generateStats(*scope_), cds_config,
                                       eds_config, cm, dispatcher, random, local_info,
                                       scope.statsOptions());
          },
          // REST方法
          "envoy.api.v2.ClusterDiscoveryService.FetchClusters",
          // GRPC方法
          "envoy.api.v2.ClusterDiscoveryService.StreamClusters");
}</pre>
<p>在每个集群初始化后都会触发的maybeFinishInitialize中，进行CDS客户端初始化：</p>
<pre class="crayon-plain-tag">void ClusterManagerInitHelper::maybeFinishInitialize() {
  ...
  if (state_ == State::WaitingForStaticInitialize &amp;&amp; cds_) {
    state_ = State::WaitingForCdsInitialize;
    cds_-&gt;initialize();
  } else {
  }
}</pre>
<p>初始化逻辑很简单，就是启动订阅：</p>
<pre class="crayon-plain-tag">void initialize() override { subscription_-&gt;start({}, *this); }</pre>
<p>CdsApiImpl.subscription_字段类型为<pre class="crayon-plain-tag">Config::Subscription&lt;envoy::api::v2::Cluster&gt;</pre>，就是上文提到的GrpcMuxImpl，CDS会订阅type.googleapis.com/envoy.api.v2.Cluster这种资源。</p>
<div class="blog_h3"><span class="graybg">处理推送 </span></div>
<p>GrpcMuxImpl::onReceiveMessage对接收到的推送进行处理后，转交给CdsApiImpl::onConfigUpdate处理：</p>
<pre class="crayon-plain-tag">void CdsApiImpl::onConfigUpdate(const ResourceVector&amp; resources, const std::string&amp; version_info) {
  // 暂定对端点资源的发现请求。在处理LDS/CDS更新时，为了避免RDS/EDS泛洪更新，调用此方法
  cm_.adsMux().pause(Config::TypeUrl::get().ClusterLoadAssignment);
  // 此方法结束后恢复端点资源的发现请求
  Cleanup eds_resume([this] { cm_.adsMux().resume(Config::TypeUrl::get().ClusterLoadAssignment); });

  // 遍历集群，获取名称，如果存在重复项，抛出异常
  std::unordered_set&lt;std::string&gt; cluster_names;
  for (const auto&amp; cluster : resources) {
    if (!cluster_names.insert(cluster.name()).second) {
      throw EnvoyException(fmt::format("duplicate cluster {} found", cluster.name()));
    }
  }
  // 进行消息验证
  for (const auto&amp; cluster : resources) {
    MessageUtil::validate(cluster);
  }
  // 跟踪哪些集群需要被移除。每次CDS更新，都把所有集群推送过来，没有在推送列表中的动态集群，需要移除
  ClusterManager::ClusterInfoMap clusters_to_remove = cm_.clusters();
  for (auto&amp; cluster : resources) {
    const std::string cluster_name = cluster.name();
    clusters_to_remove.erase(cluster_name);
    // 添加或更新集群
    if (cm_.addOrUpdateCluster(cluster, version_info)) {
      ENVOY_LOG(debug, "cds: add/update cluster '{}'", cluster_name);
    }
  }

  for (auto cluster : clusters_to_remove) {
    const std::string cluster_name = cluster.first;
    // 删除集群
    if (cm_.removeCluster(cluster_name)) {
      ENVOY_LOG(debug, "cds: remove cluster '{}'", cluster_name);
    }
  }

  version_info_ = version_info;
  runInitializeCallbackIfAny();
}</pre>
<p>addOrUpdateCluster负责添加或更新集群：</p>
<pre class="crayon-plain-tag">bool ClusterManagerImpl::addOrUpdateCluster(const envoy::api::v2::Cluster&amp; cluster,
                                            const std::string&amp; version_info) {
  // 首先判断这个集群是新的，还是对既有动态集群的更新
  // 同时检查预热中的集群列表、活动集群列表，确定是否需要执行更新，或者更新需要被阻止。阻止条件
  //   1、静态配置的集群不允许在主配置（相对Bootstrap配置）中更新
  //   2、或，配置哈希没变化
  const std::string cluster_name = cluster.name();
  const auto existing_active_cluster = active_clusters_.find(cluster_name);
  const auto existing_warming_cluster = warming_clusters_.find(cluster_name);
  const uint64_t new_hash = MessageUtil::hash(cluster);
  if ((existing_active_cluster != active_clusters_.end() &amp;&amp;
       existing_active_cluster-&gt;second-&gt;blockUpdate(new_hash)) ||
      (existing_warming_cluster != warming_clusters_.end() &amp;&amp;
       existing_warming_cluster-&gt;second-&gt;blockUpdate(new_hash))) {
    return false;
  }

  if (existing_active_cluster != active_clusters_.end() ||
      existing_warming_cluster != warming_clusters_.end()) {
    // 已经初始化的情况下，下面的调用没有作用。其本意是从对应待初始化列表中移除集群
    init_helper_.removeCluster(*existing_active_cluster-&gt;second-&gt;cluster_);
    cm_stats_.cluster_modified_.inc();
  } else {
    cm_stats_.cluster_added_.inc();
  }

  // 添加/修改集群的时机不同，则逻辑完全不同：
  // 1、在Envoy服务初始化时期，使用Init管理器来处理和主/次集群、静态/CDS集群、预热所有集群相关的复杂逻辑
  // 2、在初始化之后，为每个集群独立的处理预热
  // 
  // 注意：将所有预热逻辑集中到Init管理器中是可能的，但是为了不让Init管理器更加复杂，这里独立处理了。未来可能进行重构
  // 统一处理
 
  // 所有集群初始化完毕之后use_active_map=true
  const bool use_active_map = init_helper_.state() != ClusterManagerInitHelper::State::AllClustersInitialized;
  // 加入到活动or预热ClusterMap中
  loadCluster(cluster, version_info, true, use_active_map ? active_clusters_ : warming_clusters_);

  if (use_active_map) {
    auto&amp; cluster_entry = active_clusters_.at(cluster_name);
    // 更新各线程本地信息
    createOrUpdateThreadLocalCluster(*cluster_entry);
    // 调用集群初始化助手添加集群
    init_helper_.addCluster(*cluster_entry-&gt;cluster_);
  } else {
    auto&amp; cluster_entry = warming_clusters_.at(cluster_name);
    // 初始化集群，并在回调中预热集群
    cluster_entry-&gt;cluster_-&gt;initialize([this, cluster_name] {
      auto warming_it = warming_clusters_.find(cluster_name);
      auto&amp; cluster_entry = *warming_it-&gt;second;

      updates_map_.erase(cluster_name);
      active_clusters_[cluster_name] = std::move(warming_it-&gt;second);
      warming_clusters_.erase(warming_it);
      
      // 预热完成
      createOrUpdateThreadLocalCluster(cluster_entry);
      // 调用ClusterManagerImpl::onClusterInit
      // 此时集群还没有设置以支持跨线程更新，原因是避免在初始化阶段进行不必要的更新
      // 如果必要，该方法会首先初始化线程感知的负载均衡器
      onClusterInit(*cluster_entry.cluster_);
      updateGauges();
    });
  }

  updateGauges();
  return true;
}</pre>
<p>addOrUpdateCluster可能会调用ClusterManagerInitHelper::addCluster，就是集群管理器初始化，针对静态配置的集群调用的方法。但是这次走针对次集群的分支。</p>
<div class="blog_h2"><span class="graybg">EDS客户端</span></div>
<p>EdsClusterImpl是一种集群实现，它基于EDS协议发现自己的主机（端点，对应Envoy API是ClusterLoadAssignment.endpoints_）。</p>
<p>EdsClusterImpl同时充当了EDS客户端的角色。CDS客户端是单例的，而<span style="background-color: #c0c0c0;">每个EDS集群都是EDS客户端，它们各自独立向ADS服务器发起订阅</span>。</p>
<div class="blog_h3"><span class="graybg">初始化</span></div>
<p>Eds集群本身可能就是通过CDS订阅获得的，在EdsClusterImpl的构造函数中，它会创建订阅：</p>
<pre class="crayon-plain-tag">EdsClusterImpl::EdsClusterImpl(
    // 集群配置                              运行时加载器
    const envoy::api::v2::Cluster&amp; cluster, Runtime::Loader&amp; runtime,
    Server::Configuration::TransportSocketFactoryContext&amp; factory_context,
    // 监控指标scope                提示是否通过xDS添加了此集群
    Stats::ScopePtr&amp;&amp; stats_scope, bool added_via_api)
    // 父对象
    : BaseDynamicClusterImpl(cluster, runtime, factory_context, std::move(stats_scope), added_via_api),
      //  集群管理器                           本地信息
      cm_(factory_context.clusterManager()), local_info_(factory_context.localInfo()),
      // 获取集群名称
      cluster_name_(cluster.eds_cluster_config().service_name().empty()
                        ? cluster.name()
                        : cluster.eds_cluster_config().service_name()) {
  // 检查本地信息
  Config::Utility::checkLocalInfo("eds", local_info_);
  // 获取EDS配合自
  const auto&amp; eds_config = cluster.eds_cluster_config().eds_config();
  Event::Dispatcher&amp; dispatcher = factory_context.dispatcher();
  Runtime::RandomGenerator&amp; random = factory_context.random();
  Upstream::ClusterManager&amp; cm = factory_context.clusterManager();
  // 创建ClusterLoadAssignment订阅
  subscription_ = Config::SubscriptionFactory::subscriptionFromConfigSource&lt;envoy::api::v2::ClusterLoadAssignment&gt;(
      // 配置源envoy::api::v2::core::ConfigSource
      eds_config, local_info_, dispatcher, cm, random, info_-&gt;statsScope(),
      // 遗留的v1 REST订阅构造器
      [this, &amp;eds_config, &amp;cm, &amp;dispatcher,
       &amp;random]() -&gt; Config::Subscription&lt;envoy::api::v2::ClusterLoadAssignment&gt;* {
        return new SdsSubscription(info_-&gt;stats(), eds_config, cm, dispatcher, random);
      },
      // REST订阅方法
      "envoy.api.v2.EndpointDiscoveryService.FetchEndpoints",
      // GRPC订阅方法
      "envoy.api.v2.EndpointDiscoveryService.StreamEndpoints");
}</pre>
<p>在EDS集群的preInit阶段，会启动订阅，<span style="background-color: #c0c0c0;">只会订阅当前集群的端点</span>：</p>
<pre class="crayon-plain-tag">// subscription_的start方法支持传入一个vector，提供需要抓取的资源的名称
void EdsClusterImpl::startPreInit() { subscription_-&gt;start({cluster_name_}, *this); }</pre>
<div class="blog_h3"><span class="graybg">处理推送</span></div>
<p>GrpcMuxImpl::onReceiveMessage对接收到的推送进行处理后，转交给EdsClusterImpl::onConfigUpdate处理： </p>
<pre class="crayon-plain-tag">void EdsClusterImpl::onConfigUpdate(const ResourceVector&amp; resources, const std::string&amp;) {
  if (resources.empty()) {
    // 当前集群没有ClusterLoadAssignment
    info_-&gt;stats().update_empty_.inc();
    // 仍然需要进行预初始化
    onPreInitComplete();
    return;
  }
  // EDS资源长度必须为1
  if (resources.size() != 1) {
    throw EnvoyException(fmt::format("Unexpected EDS resource length: {}", resources.size()));
  }
  // ClusterLoadAssignme
  const auto&amp; cluster_load_assignment = resources[0];
  // 消息合法性验证
  MessageUtil::validate(cluster_load_assignment);
  // 被更新主机列表
  std::unordered_map&lt;std::string, HostSharedPtr&gt; updated_hosts;
  // 负责管理一个集群的PriorityState，PriorityState持有每个优先级对应的 主机集+对应的位置权重图
  PriorityStateManager priority_state_manager(*this, local_info_);
  // locality_lb_endpoint不是一个端点，而是一个位置（例如可用性区域）中所有端点的集合，
  // 它具有权重（相对于同优先级的其它locality）、优先级等属性
  for (const auto&amp; locality_lb_endpoint : cluster_load_assignment.endpoints()) {
    const uint32_t priority = locality_lb_endpoint.priority();
    if (priority &gt; 0 &amp;&amp; !cluster_name_.empty() &amp;&amp; cluster_name_ == cm_.localClusterName()) {
      // 对于本地集群，端点路由优先级必须是0
      throw EnvoyException(fmt::format("Unexpected non-zero priority for local cluster '{}'.", cluster_name_));
    }
    // 放入 PriorityState 也就是 std::vector&lt;std::pair&lt;HostListPtr, LocalityWeightsMap&gt;&gt; 中
    // vector是以优先级为索引的列表，HostListPtr是Host列表，LocalityWeightsMap是Locality到Weight的映射
    priority_state_manager.initializePriorityFor(locality_lb_endpoint);

    for (const auto&amp; lb_endpoint : locality_lb_endpoint.lb_endpoints()) {
      // 遍历端点列表
      // 先创建HostImpl，然后存放到PriorityState中
      priority_state_manager.registerHostForPriority(
          "", resolveProtoAddress(lb_endpoint.endpoint().address()), locality_lb_endpoint,
          // 设置主机状态为不健康
          lb_endpoint, Host::HealthFlag::FAILED_EDS_HEALTH);
    }
  }

  // 跟踪是否重建负载均衡结构
  bool cluster_rebuilt = false;

  const uint32_t overprovisioning_factor = PROTOBUF_GET_WRAPPED_OR_DEFAULT(
      cluster_load_assignment.policy(), overprovisioning_factor, kDefaultOverProvisioningFactor);

  // 遍历新配置中所有优先级
  auto&amp; priority_state = priority_state_manager.priorityState();
  for (size_t i = 0; i &lt; priority_state.size(); ++i) {
    if (priority_state[i].first != nullptr) {
      if (locality_weights_map_.size() &lt;= i) {
        locality_weights_map_.resize(i + 1);
      }
      // 为某个优先级的所有locality更新主机列表
      cluster_rebuilt |= updateHostsPerLocality(
          i, overprovisioning_factor, *priority_state[i].first, locality_weights_map_[i],
          priority_state[i].second, priority_state_manager, updated_hosts);
    }
  }

  // 遍历所有不在配置中的优先级
  for (size_t i = priority_state.size(); i &lt; priority_set_.hostSetsPerPriority().size(); ++i) {
    const HostVector empty_hosts;
    LocalityWeightsMap empty_locality_map;

    if (locality_weights_map_.size() &lt;= i) {
      locality_weights_map_.resize(i + 1);
    }
    cluster_rebuilt |=
        updateHostsPerLocality(i, overprovisioning_factor, empty_hosts, locality_weights_map_[i],
                               empty_locality_map, priority_state_manager, updated_hosts);
  }

  all_hosts_ = std::move(updated_hosts);

  if (!cluster_rebuilt) {
    info_-&gt;stats().update_no_rebuild_.inc();
  }

  // Preinit完成回调
  onPreInitComplete();
}</pre>
<div class="blog_h2"><span class="graybg">LDS客户端</span></div>
<p>LDS客户端是作为InitManager的target之一，进行初始化的。InitManager会先初始化它的所有目标，最后初始化它自己。</p>
<p>InitManager会在集群管理器初始化完毕——所有集群都添加之后被调用。</p>
<div class="blog_h3"><span class="graybg">初始化</span></div>
<p>如果配置了动态LDS资源，则在InstanceImpl的初始化过程中，会创建LDS客户端：</p>
<pre class="crayon-plain-tag">if (bootstrap_.dynamic_resources().has_lds_config()) {
  listener_manager_-&gt;createLdsApi(bootstrap_.dynamic_resources().lds_config());
}

void ListenerManagerImpl::createLdsApi(const envoy::api::v2::core::ConfigSource&amp; lds_config) override {
  ASSERT(lds_api_ == nullptr);
  lds_api_ = factory_.createLdsApi(lds_config);
}

LdsApiPtr ProdListenerComponentFactory::createLdsApi(const envoy::api::v2::core::ConfigSource&amp; lds_config) override {
  return std::make_unique&lt;LdsApiImpl&gt;(
      lds_config, server_.clusterManager(), server_.dispatcher(), server_.random(),
      server_.initManager(), server_.localInfo(), server_.stats(), server_.listenerManager());
}

LdsApiImpl::LdsApiImpl(const envoy::api::v2::core::ConfigSource&amp; lds_config,
                       Upstream::ClusterManager&amp; cm, Event::Dispatcher&amp; dispatcher,
                       Runtime::RandomGenerator&amp; random, Init::Manager&amp; init_manager,
                       const LocalInfo::LocalInfo&amp; local_info, Stats::Scope&amp; scope,
                       ListenerManager&amp; lm)
    : listener_manager_(lm), scope_(scope.createScope("listener_manager.lds.")), cm_(cm) {
  // std::unique_ptr&lt;Config::Subscription&lt;envoy::api::v2::Listener&gt;&gt; subscription_;
  // 订阅，获得一个GrpcMuxSubscriptionImpl对象
  subscription_ =
      Envoy::Config::SubscriptionFactory::subscriptionFromConfigSource&lt;envoy::api::v2::Listener&gt;(
          lds_config, local_info, dispatcher, cm, random, *scope_,
          [this, &amp;lds_config, &amp;cm, &amp;dispatcher, &amp;random, &amp;local_info,
           &amp;scope]() -&gt; Config::Subscription&lt;envoy::api::v2::Listener&gt;* {
            return new LdsSubscription(Config::Utility::generateStats(*scope_), lds_config, cm,
                                       dispatcher, random, local_info, scope.statsOptions());
          },
          "envoy.api.v2.ListenerDiscoveryService.FetchListeners",
          "envoy.api.v2.ListenerDiscoveryService.StreamListeners");
  Config::Utility::checkLocalInfo("lds", local_info);
  // 向初始化管理器注册自己
  init_manager.registerTarget(*this);
}</pre>
<p>初始化管理器会调用：</p>
<pre class="crayon-plain-tag">void LdsApiImpl::initialize(std::function&lt;void()&gt; callback) {
  initialize_callback_ = callback;
  subscription_-&gt;start({}, *this);
}</pre>
<p>导致LDS订阅启动。 </p>
<div class="blog_h3"><span class="graybg">处理推送</span></div>
<p>GrpcMuxImpl::onReceiveMessage对接收到的推送进行处理后，转交给LdsApiImpl::onConfigUpdate处理：</p>
<pre class="crayon-plain-tag">void LdsApiImpl::onConfigUpdate(const ResourceVector&amp; resources, const std::string&amp; version_info) {
  // 暂停RDS订阅并在此函数执行完毕后恢复
  cm_.adsMux().pause(Config::TypeUrl::get().RouteConfiguration);
  Cleanup rds_resume([this] { cm_.adsMux().resume(Config::TypeUrl::get().RouteConfiguration); });

  // 返回结果中包含重复的监听器，不允许
  std::unordered_set&lt;std::string&gt; listener_names;
  for (const auto&amp; listener : resources) {
    if (!listener_names.insert(listener.name()).second) {
      throw EnvoyException(fmt::format("duplicate listener {} found", listener.name()));
    }
  }
  // 验证每个消息的合法性
  for (const auto&amp; listener : resources) {
    MessageUtil::validate(listener);
  }
  // 跟踪需要移除的监听器
  std::unordered_map&lt;std::string, std::reference_wrapper&lt;Network::ListenerConfig&gt;&gt; listeners_to_remove;

  // 将所有现存监听器存放到一个映射中
  for (const auto&amp; listener : listener_manager_.listeners()) {
    listeners_to_remove.emplace(listener.get().name(), listener);
  }
  // 对于存在于本次订阅结果中的监听器，不被移除
  for (const auto&amp; listener : resources) {
    listeners_to_remove.erase(listener.name());
  }

  // 不存在的监听器，移除。必须全量推送？
  for (const auto&amp; listener : listeners_to_remove) {
    if (listener_manager_.removeListener(listener.first)) {
    }
  }

  // 添加或更新监听器，逻辑类似于静态监听器
  for (const auto&amp; listener : resources) {
    const std::string listener_name = listener.name();
    try {
      // 如果新旧配置完全一样，添加会失败
      if (listener_manager_.addOrUpdateListener(listener, version_info, true)) {
        ENVOY_LOG(info, "lds: add/update listener '{}'", listener_name);
      } else {
        ENVOY_LOG(debug, "lds: add/update listener '{}' skipped", listener_name);
      }
    } catch (const EnvoyException&amp; e) {
      throw EnvoyException(fmt::format("Error adding/updating listener {}: {}", listener_name, e.what()));
    }
  }

  version_info_ = version_info;
  // LDS的初始化回调尚未调用，调用之
  runInitializeCallbackIfAny();
}</pre>
<div class="blog_h1"><span class="graybg">总结</span></div>
<div class="blog_h2"><span class="graybg">Pilot Discovery</span></div>
<p>该组件一方面实现了完整的xDS协议管理服务器端，一方面对接底层基础设施（K8S），从中获取各种各样的资源。</p>
<p>Discovery关注的K8S资源类型包括：</p>
<ol>
<li>Ingress：需要此资源的原因是，Istio可以作为Ingress Controller</li>
<li>Istio CRD，Istio需要：
<ol>
<li>VirtualService、DestinationRule来构建Envoy的Cluster配置、Listener路由规则</li>
<li>Gateway来为作为网关的Envoy代理（ingressgateway、egressgateway，或者任Pod）提供监听器，并由绑定到网关的VirtualService提供路由表</li>
<li>ServiceEntry来创建一部分Istio服务，进而构建Envoy的Cluster配置、Listener配置</li>
</ol>
</li>
<li>Service：Istio需要这种资源创建Istio服务，进而构建Envoy的Cluster配置、Listener配置</li>
<li>Pod：Istio需要这种资源为EDS集群更新工作负载</li>
</ol>
<p>当Discovery发现上述某种资源变更后，会准备好一个推送上下文，并且依据此上下文生成所需的Envoy配置，推送给Envoy代理。</p>
<p>推送服务由DiscoveryService负责，它转调DiscoveryServer。后者提供了Envoy v2 xDS API的gRPC客户端实现，其逻辑在ads.go、cds.go、eds.go、lds.go、rds.go等多个源文件中实现，对应了xDS API的不同部分。</p>
<p>所有连接到Discovery的、发起了订阅请求的代理，都会接收到推送。</p>
<div class="blog_h2"><span class="graybg">Pilot Proxy</span></div>
<p>该组件包装了Envoy代理，主要实现以下功能：</p>
<ol>
<li>从模板生成Bootstrap配置</li>
<li>守护Envoy代理，如果宕机重新启动</li>
<li>当Bootstrap配置文件更新后，重现配置Envoy代理</li>
<li> 更新数字证书</li>
</ol>
<p>Proxy有多种运行模式：sidecar、ingress、proxy，分别用作普通Pod的边车、Ingress控制器、入口/出口网关。</p>
<div class="blog_h2"><span class="graybg">Envoy</span></div>
<p>该组件实现了完整的xDS协议客户端。和Pilot Discovery的交互主要由GrpcMuxImpl负责，它是基于gRPC的ADS实现。</p>
<p>除了通过Bootstrap配置加载的静态资源，其它资源都需要通过xDS协议，向Pilot Discovery订阅以获取。GrpcMuxImpl获得资源推送后，会调用CDS、EDS、LDS、RDS等API客户端：</p>
<ol>
<li>CDS，动态加载集群，可能创建EDS</li>
<li>EDS，为EDS集群更新端点</li>
<li>LDS，动态加载监听器，可能触发工作线程的启动</li>
</ol>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/interaction-between-istio-pilot-and-envoy">Istio Pilot与Envoy的交互机制解读</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/interaction-between-istio-pilot-and-envoy/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Envoy学习笔记</title>
		<link>https://blog.gmem.cc/envoy-study-note</link>
		<comments>https://blog.gmem.cc/envoy-study-note#comments</comments>
		<pubDate>Wed, 03 Oct 2018 03:11:19 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[K8S]]></category>
		<category><![CDATA[ServiceMesh]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=24261</guid>
		<description><![CDATA[<p>简介 Envoy是一个大规模面向服务架构设计的7层代理和通信总线，它的信条是 —— 网络应该对应用程序透明，当出现问题时，应该很容易定位到源头在网络还是应用。 Envoy的高层特性包括： 进程外架构：Envoy以独立的进程、伴随着每个应用服务运行。每个应用服务都和localhost通信而不关注网络拓扑，Envoy监听localhost并转发给其它Envoy，所有Envoy构成一个网格。进程外架构的优势是： 让Envoy可以和任何编程语言交互 不会像库那样引入升级、分发的负担 基于C++ 11编写：性能好 L3/L4过滤器架构：Envoy的核心包含L3/L4网络代理功能，可拔插的过滤器链能实现各种TCP层代理行为，以支持HTTP、TLS等协议 HTTP L7过滤器架构：Envoy为HTTP单独设计了一个过滤器链，实现缓冲、限速、路由/转发等功能 HTTP/2支持：Envoy同时支持HTTP 1.1和HTTP 2 HTTP L7路由：支持基于URL路径、内容类型、各种上下文属性来路由HTTP请求 gRPC支持：gRPC是Google开源的、以HTTP/2为传输机制的RPC框架。Envoy支持gRPC请求的路由、负载均衡 MongoDB、DynamoDB的L7支持 服务发现和动态配置：Envoy支持一组动态配置API，能够在运行时更改流量管理配置 健康检查：Envoy支持对上游服务集群进行主动健康检查、被动健康检查（断路器） <a class="read-more" href="https://blog.gmem.cc/envoy-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/envoy-study-note">Envoy学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>Envoy是一个大规模面向服务架构设计的7层代理和通信总线，它的信条是 —— 网络应该对应用程序透明，当出现问题时，应该很容易定位到源头在网络还是应用。</p>
<p>Envoy的高层特性包括：</p>
<ol>
<li>进程外架构：Envoy以独立的进程、伴随着每个应用服务运行。每个应用服务都和localhost通信而不关注网络拓扑，Envoy监听localhost并转发给其它Envoy，所有Envoy构成一个网格。进程外架构的优势是：
<ol>
<li>让Envoy可以和任何编程语言交互</li>
<li>不会像库那样引入升级、分发的负担</li>
</ol>
</li>
<li>基于C++ 11编写：性能好</li>
<li>L3/L4过滤器架构：Envoy的核心包含L3/L4网络代理功能，<span style="background-color: #c0c0c0;">可拔插的过滤器链</span>能实现各种TCP层代理行为，以支持HTTP、TLS等协议</li>
<li>HTTP L7过滤器架构：Envoy为HTTP单独设计了一个过滤器链，实现缓冲、限速、路由/转发等功能</li>
<li>HTTP/2支持：Envoy同时支持HTTP 1.1和HTTP 2</li>
<li>HTTP L7路由：支持基于URL路径、内容类型、各种上下文属性来路由HTTP请求</li>
<li>gRPC支持：gRPC是Google开源的、以HTTP/2为传输机制的RPC框架。Envoy支持gRPC请求的路由、负载均衡</li>
<li>MongoDB、DynamoDB的L7支持</li>
<li>服务发现和动态配置：Envoy支持一组<span style="background-color: #c0c0c0;">动态配置API</span>，能够在运行时更改流量管理配置</li>
<li>健康检查：Envoy支持对上游服务集群进行主动健康检查、被动健康检查（断路器）</li>
<li>高级流量管理特性：自动重试、断路器、全局限速、请求复制等等</li>
<li>前置/边缘代理支持：尽管Envoy主要用作服务之间的代理，但是它包含TLS Termination、HTTP 1/1和HTTP/2支持、HTTP L7路由等特性，可以作为大部分现代Web应用的边缘代理</li>
<li>可观察性：Envoy的所有子系统都支持指标统计，指标可以落地到Statd或其它兼容的服务。<span style="background-color: #c0c0c0;">统计信息也可以通过管理端口查询</span></li>
<li>分布式跟踪：支持基于第三方Tracer的分布式跟踪</li>
</ol>
<div class="blog_h2"><span class="graybg">架构图</span></div>
<p>此图来自<a href="https://jimmysong.io">https://jimmysong.io</a>。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/10/envoy-arch.jpg"><img class="aligncenter  wp-image-24645" src="https://blog.gmem.cc/wp-content/uploads/2018/10/envoy-arch.jpg" alt="envoy-arch" width="1016" height="746" /></a></p>
<div class="blog_h2"><span class="graybg">Envoy和服务网格</span></div>
<p>Envoy可以作为服务网格的数据平面。</p>
<p>Envoy提出的资源发现协议集 —— xDS（包括CDS、EDS、LDS、RDS，以及后续补充的密钥发现服务/SDS、聚合发现服务/ADS、健康发现服务/HDS、限速服务/RLS等），已经被多个服务网格项目支持，其中包括著名的Istio。这些项目主要实现服务网格的控制平面。</p>
<p>xDS有时候也用于代指实现xDS API的那些发现服务。Envoy通过订阅（subscription）方式获取资源的更新，具体有三种：</p>
<ol>
<li>文件订阅：监控指定路径下的文件，最简单的动态资源发现方式</li>
<li>gRPC订阅：向发现服务发起gRPC请求，后者主动将更新推送过来</li>
<li>REST-JSON轮询订阅：向发现服务发起REST长轮询</li>
</ol>
<div class="blog_h2"><span class="graybg">线程模型</span></div>
<p>Envoy采用单进程多线程的线程模型，其中一个master线程控制一些零散的协作任务，若干worker线程负责<span style="background-color: #c0c0c0;">监听、过滤、转发</span>。一旦监听器接受了连接，<span style="background-color: #c0c0c0;">连接的后续生命周期都绑定到单个工作线程</span>。</p>
<p>Envoy完全使用<span style="background-color: #c0c0c0;">非阻塞</span>的网络调用，大部分工作负载下，<span style="background-color: #c0c0c0;">worker的数量应该设置的和硬件线程数量一致</span>。</p>
<div class="blog_h2"><span class="graybg">监听器</span></div>
<p>Envoy支持在单个进程中创建任意数量的监听器，通常建议在一台机器上仅部署单个Envoy，不管需要多少监听器。<span style="background-color: #c0c0c0;">目前Envoy仅仅支持TCP监听器</span>。</p>
<p>每个<span style="background-color: #c0c0c0;">监听器都可以独立的配置若干网络（L3/L4）过滤器</span>。一旦监听器接收到新的连接，<span style="background-color: #c0c0c0;">过滤器链</span>就会被初始化，并负责处理连接的后续事件。</p>
<p>你还可以为监听器配置<span style="background-color: #c0c0c0;">“监听器过滤器”，这些过滤器在网络过滤器之前执行</span>，能够操控连接元数据，影响连接的后续处理行为。</p>
<p>监听器发现服务（Listener discovery service，LDS）用于动态的发现监听器。</p>
<div class="blog_h3"><span class="graybg">监听器预热</span></div>
<p>通过LDS加载的监听器必须在预热（warmed）之后才能接收流量。例如，如果监听器引用一个RDS配置，RDS必须被解析、抓取，然后监听器才能预热。</p>
<div class="blog_h2"><span class="graybg">监听器过滤器</span></div>
<p>这种过滤器用于<span style="background-color: #c0c0c0;">操控连接的元数据</span>信息。监听器过滤器的API比较简单，它仅仅操控新接受的套接字。</p>
<div class="blog_h2"><span class="graybg">网络过滤器</span></div>
<p>网络层（L3/L4）过滤器是Envoy连接处理的核心。Envoy的过滤器API允许若干过滤器被编排、匹配、关联到指定的监听器。</p>
<p>网络过滤器分为三类：</p>
<ol>
<li>读过滤器：当从下游连接接收到数据后调用</li>
<li>写过滤器：当写入数据给下游服务器时调用</li>
<li>读写过滤器：对下游连接读写数据时都调用</li>
</ol>
<p>网络过滤器的API也比较简单，因为过滤器仅仅<span style="background-color: #c0c0c0;">处理原始字节和少量连接事件</span>（例如TLS握手完成、连接断开）。</p>
<p>过滤器链中某个成员可以<span style="background-color: #c0c0c0;">停止处理并迭代到下一个过滤器</span>。</p>
<p>过滤器可以共享针对同一<span style="background-color: #c0c0c0;">下游连接的静态、动态的状态信息</span>。</p>
<div class="blog_h2"><span class="graybg">HTTP连接管理</span></div>
<p>HTTP对于现代SOA架构非常重要，因此Envoy实现了大量针对HTTP协议的功能。其中一个是网络过滤器  —— HTTP连接管理器。该管理器将原始字节转换为<span style="background-color: #c0c0c0;">HTTP报文和事件</span>（报文头已接收、报文体已接收…），并且负责<span style="background-color: #c0c0c0;">访问日志、请求ID生成和追踪、请求/响应头操控、路由表管理、统计指标收集</span>。</p>
<p>HTTP连接管理器支持HTTP 1.1、HTTP 2、WebSocket协议，但是不支持SPDY。</p>
<div class="blog_h2"><span class="graybg">HTTP过滤器</span></div>
<p>Envoy在HTTP连接管理器内部支持HTTP过滤器链。HTTP过滤器操控HTTP消息，不需要理解底层协议（HTTP/1.1，HTTP/2…）。</p>
<p>HTTP过滤器分为三种：</p>
<ol>
<li>解码过滤器：当HTTP连接管理器执行请求解码时调用</li>
<li>编码过滤器：当HTTP连接管理器进行响应编码时调用</li>
<li>编解码过滤器</li>
</ol>
<div class="blog_h3"><span class="graybg">路由过滤器</span></div>
<p>Envoy包含了一个HTTP Router Filter，用于执行高级的路由任务，不管是处理边缘流量还是网格内部流量，都可以用到此过滤器。</p>
<p>Envoy具有作为<span style="background-color: #c0c0c0;">正向代理的能力，网格的客户端需要将其HTTP代理设置为一个Envoy实例</span>，以使用代理。</p>
<p>此过滤器实现的特性包括：</p>
<ol>
<li><span style="background-color: #c0c0c0;">虚拟主机</span>：将Domain/Authority映射到一组路由规则（作为路由决策的一部分）</li>
<li>前缀或精确的<span style="background-color: #c0c0c0;">路径匹配</span></li>
<li>在虚拟主机级别的<span style="background-color: #c0c0c0;">TLS重定向</span></li>
<li>在路由级别的<span style="background-color: #c0c0c0;">Path/Host重定向</span></li>
<li>在路由级别<span style="background-color: #c0c0c0;">直接返回响应</span></li>
<li>显式的<span style="background-color: #c0c0c0;">Host改写</span></li>
<li>基于选中的上游Host的DNS名称进行<span style="background-color: #c0c0c0;">自动的Host改写</span></li>
<li><span style="background-color: #c0c0c0;">路径前缀改写</span></li>
<li>根据请求头或者路由配置，进行<span style="background-color: #c0c0c0;">HTTP重试</span></li>
<li>根据请求头或者路由配置，设定<span style="background-color: #c0c0c0;">HTTP超时</span></li>
<li>流量转移：根据运行时值将流量从一个上游集群转移到另外一个</li>
<li>流量分割：在<span style="background-color: #c0c0c0;">多个上游集群之间分配流量</span>比例</li>
<li><span style="background-color: #c0c0c0;">根据请求头路由</span></li>
<li>基于优先级的路由</li>
<li>基于<span style="background-color: #c0c0c0;">哈希策略的路由</span></li>
<li>对于非TLS正向代理，支持绝对URL</li>
</ol>
<p><span style="background-color: #c0c0c0;">HTTP连接管理器维护</span>所有HTTP过滤器所使用（尽管路由过滤器是主要使用者）的<span style="background-color: #c0c0c0;">路由表</span>，这样过滤器可以根据目的地作出决策。</p>
<div class="blog_h2"><span class="graybg">状态</span></div>
<p>Envoy提供一系列静态、动态状态，用于在<span style="background-color: #c0c0c0;">过滤器之间或者向其它核心子系统</span>（例如访问日志）<span style="background-color: #c0c0c0;">传递配置、元数据、请求/连接的状态</span>。</p>
<div class="blog_h3"><span class="graybg">静态状态</span></div>
<p>静态状态是在配置加载阶段（例如通过xDS）生成的不可变信息。静态状态分为三类：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">静态状态</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>元数据</td>
<td>
<p>某些Envoy配置（例如监听器、路由、集群）包含元数据，<span style="background-color: #c0c0c0;">元数据是任意数量的键值对</span>。通常以过滤器名称的DNS反转写法为Key，以过滤器特有的配置为Value。</p>
<p>元数据是不可变的，且被所有请求/连接共享。这些元数据要么在Bootstrap时提供，要么作为xDS的一部分提供</p>
</td>
</tr>
<tr>
<td>强类型元数据</td>
<td>
<p>过滤器可以注册一个转换器，在接收到（通过xDS）配置元数据时将指定的Key转换为强类型的C++对象，这样过滤器不需要在每次处理连接/请求时都执行ProtobufWkt::Struct到某种内部结构的转换</p>
</td>
</tr>
<tr>
<td>HTTP路由过滤器配置</td>
<td>HTTP过滤器可以提供针对单个虚拟主机的配置，用于覆盖全局的、面向所有虚拟主机的配置</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">动态状态</span></div>
<p><span style="background-color: #c0c0c0;">动态状态为每个网络连接或者HTTP流生成</span>。取决于生成动态状态的过滤器，这些状态可以变化。</p>
<p>Envoy::Network::Connection和Envoy::Http::Filter提供<span style="background-color: #c0c0c0;">StreamInfo对象，此对象包含当前TCP连接/HTTP流的信息</span>。StreamInfo除了包含一系列静态字段外，还包含map&lt;string, FilterState::Object&gt;映射，用于存储任意类型的状态。 </p>
<div class="blog_h2"><span class="graybg">集群管理器</span></div>
<p>负责管理上游集群，可以配置任意数量的上游集群。此管理器支持两种方式获得集群：</p>
<ol>
<li>静态配置</li>
<li>通过集群发现服务（CDS）API动态获取</li>
</ol>
<p><span style="background-color: #c0c0c0;">上游集群</span>从网络/HTTP过滤器栈中剥离出来，以便被不同的代理任务<span style="background-color: #c0c0c0;">重用</span>。集群管理器向过滤器暴露了API，允许它们：</p>
<ol>
<li>获取到上游集群的L3/L4连接</li>
<li>获取用于处理上游集群的抽象（隐藏协议类型）连接池的句柄</li>
</ol>
<p>过滤器根据需要来获取L3/L4连接或者新的HTTP流，<span style="background-color: #c0c0c0;">集群管理器负责</span>处理其它的复杂性，包括：</p>
<ol>
<li>确定哪些主机<span style="background-color: #c0c0c0;">可用、健康</span></li>
<li>进行<span style="background-color: #c0c0c0;">负载均衡</span></li>
<li>在ThreadLocal<span style="background-color: #c0c0c0;">存储上游连接数据</span></li>
</ol>
<div class="blog_h3"><span class="graybg">集群预热</span></div>
<p>不管是通过静态配置，还是通过CDS加载，只有当集群经过以下处理后，才是<span style="background-color: #c0c0c0;">warmed状态，才可以使用</span>：</p>
<ol>
<li>初始的<span style="background-color: #c0c0c0;">服务发现加载，例如DNS解析、EDS更新</span></li>
<li>初始的<span style="background-color: #c0c0c0;">主动健康检查</span>（如果配置的话）。Envoy会向每个发现的主机发送健康检查，确定其初始健康状态</li>
</ol>
<p>讨论集群预热（cluster warming）时，所谓集群可用，是指：</p>
<ol>
<li>对于新添加的集群，仅当集群<span style="background-color: #c0c0c0;">预热完毕，它才被Envoy的其它部分可见</span>。否则，一个引用集群的HTTP Route会可能会返回404/503</li>
<li>对于被更新的集群，在<span style="background-color: #c0c0c0;">新配置预热之前，旧的集群仍然会提供服务</span>。预热之后，新集群替换旧集群，<span style="background-color: #c0c0c0;">不会发生任何的流量中断</span></li>
</ol>
<div class="blog_h2"><span class="graybg">服务发现</span></div>
<p>服务发现是Envoy<span style="background-color: #c0c0c0;">确定集群成员</span>的过程，支持的服务发现类型包括：</p>
<ol>
<li>静态发现：在配置中明确指出每个上游主机的网络名称（IP地址、端口，或者UDS）</li>
<li>严格DNS发现：Envoy持续异步的解析目标服务的DNS名称，DNS结果中返回的每个IP地址都视为上游主机</li>
<li>逻辑DNS发现：类似于上一个，但是<span style="background-color: #c0c0c0;">在需要初始化新连接时，仅仅使用DNS结果中第一个IP地址</span>。大型Web服务常常使用循环DNS返回很多IP地址，如果使用严格DNS发现，连接池太多（每个上游主机需要一个连接池）会导致系统资源枯竭</li>
<li>原始目的地：如果传入的连接来自<span style="background-color: #c0c0c0;">Iptables重定向</span>（可以通过conntrack表或读取SO_ORIGINAL_DST选项得到原始目的地），或者使用了<span style="background-color: #c0c0c0;">代理协议</span>（从代理协议头得到原始目的地），则可以使用其原始目的地，这种情况下实际上不需要发现上游主机</li>
<li>端点发现服务（Endpoint Discovery Service，EDS）：一个基于gRPC/REST-JSON的xDS管理服务，Envoy使用它来获取集群成员。每个集群成员都叫端点。<span style="background-color: #c0c0c0;">EDS是首选的服务发现机制</span></li>
</ol>
<p>很多RPC系统的服务发现是一个完全一致性过程，它们使用一致性的领导选举算法，例如ZK、Etcd、Consul。<span style="background-color: #c0c0c0;">大规模使用完全一致性会存在性能问题</span>，因此<span style="background-color: #c0c0c0;">Envoy使用最终一致性</span>的方式。</p>
<div class="blog_h2"><span class="graybg">健康检查</span></div>
<div class="blog_h3"><span class="graybg">主动健康检查</span></div>
<p>可以为每个上游集群配置主动健康检查，对于<span style="background-color: #c0c0c0;">EDS服务发现来说，主动健康检查是和服务发现过程一同进行</span>的。其它类型的服务发现也可以联用主动健康检查。</p>
<p>Envoy目前支持三种健康检查：</p>
<ol>
<li>HTTP：将上游主机发送HTTP请求，如果返回200则认为正常，如果返回503则立即禁止转发流量给目标端点</li>
<li>L3/L4：发送一个可配置的字节缓冲给上游主机，如果上游主机Echo了此字节缓冲，则认为通过检查</li>
<li>Redis：发送PING命令给上游Redis服务，如果返回PONG应答则认为通过检查。Envoy也可以针对特定的键进行EXISTS操作，如果键不存在则认为通过检查</li>
</ol>
<p>Envoy目前支持<span style="background-color: #c0c0c0;">检查间隔、标记为不健康需要的连续失败次数、标记为健康需要的连续成功次数</span>等健康检查相关配置。此外，可以<span style="background-color: #c0c0c0;">针对每个端点配置健康检查</span>逻辑。</p>
<div class="blog_h3"><span class="graybg">主动健康检查快速失败</span></div>
<p>联用主动健康检查+被动健康检查时，通常<span style="background-color: #c0c0c0;">会延长主动检查的间隔</span>，避免过大的流量。你甚至可以通过管理端点<pre class="crayon-plain-tag">/healthcheck/fail</pre>来快速的Drain掉某个上游主机。</p>
<p>如果你通过管理端点标记主机为失败，则该主机的Envoy中的<span style="background-color: #c0c0c0;">HTTP健康检查过滤器</span>会自动设置响应头 <span style="background-color: #c0c0c0;">x-envoy-immediate-health-check-fail</span>。请求方的路由过滤器会识别这个头，并立即将目标主机标记为主动健康检查失败。</p>
<div class="blog_h3"><span class="graybg">主机身份识别</span></div>
<p>仅仅简单的检查一个URL，不足以判断某个服务的主机正常。原因是，在云环境下，服务实例可能被销毁，而其IP地址被其它类型服务的实例重用。</p>
<p>为了解决此问题，HTTP健康检查支持配置service_name选项，此选项设置后，健康检查器会额外检查x-envoy-upstream-healthchecked-cluster响应头，只有它的值和service_name选项一致，才可能通过健康检查。</p>
<div class="blog_h3"><span class="graybg">被动健康检查</span></div>
<p>Envoy支持<span style="background-color: #c0c0c0;">基于异常检测（outlier detection）的被动健康检查</span>（不刻意发起请求，而是检测业务流量是否正常）</p>
<div class="blog_h3"><span class="graybg">HTTP健康检查过滤器</span></div>
<p>Envoy支持为HTTP监听器配置<span style="background-color: #c0c0c0;">HTTP健康检查过滤器</span>，此过滤器允许以下操作模式：</p>
<ol>
<li>不穿透：此模式下健康检查请求不会转发给Envoy背后的上游服务，Envoy根据服务当前的draining state决定返回200还是503</li>
<li>不穿透，根据上游集群健康状态决定检查结果：如果一个或者多个上游集群中，至少特定比例的主机是健康的，则返回200，否则503</li>
<li>穿透：直接将健康检查请求转发给背后的服务</li>
<li>穿透+缓存：支持一定时间的缓存</li>
</ol>
<p>太多的穿透可能引发大量的健康检查流量。</p>
<div class="blog_h2"><span class="graybg">连接池</span></div>
<p>对于<span style="background-color: #c0c0c0;">HTTP流量来说，Envoy支持连接池</span>。HTTP/1.1和HTTP/2连接池的行为有所不同：</p>
<ol>
<li>HTTP/1.1：按需创建到上游主机的TCP连接，当连接可用时，将请求绑定到连接。如果TCP连接意外断开，则仅仅一个请求会被重置</li>
<li>HTTP/2：创建到上游主机的单个TCP连接，所有请求复用此连接。如果<span style="background-color: #c0c0c0;">接收到GOAWAY帧或者到达最大Stream限制，则连接池会创建一个新的连接</span></li>
</ol>
<div class="blog_h3"><span class="graybg">和健康检查的交互</span></div>
<p>如果<span style="background-color: #c0c0c0;">针对上游主机的主动/被动健康检查失败</span>，则<span style="background-color: #c0c0c0;">此主机的连接池会被关闭</span>；反之，如果上游主机恢复健康，则连接池会重新创建。</p>
<div class="blog_h2"><span class="graybg">负载均衡</span></div>
<p>Envoy根据知晓的上游集群成员信息，自己决定应该把负载分发给哪个端点。</p>
<p>一个<span style="background-color: #c0c0c0;">全局性的权威负责决定负载如何在主机之间分发</span>。对于Envoy来说，则可以通过控制平面达成。控制平面可以调整优先级、权重、端点健康状况来满足复杂的负载均衡需求。</p>
<div class="blog_h3"><span class="graybg">支持的算法</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">算法</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>加权轮询</td>
<td>Weighted round robin，轮询上游主机，但是主机可以设置权重</td>
</tr>
<tr>
<td>加权最少请求</td>
<td>Weighted least request，使用 O(1) 算法选择两个随机健康主机，并选择主动请求较少的主机</td>
</tr>
<tr>
<td>环哈希</td>
<td>
<p>Ring hash，针对上游主机实现一致性哈希，此算法将所有主机映射到一个环上，这样添加/删除一个主机，仅仅1/N的请求受到影响</p>
<p>也叫做ketama hashing</p>
<p>目前不支持权重</p>
</td>
</tr>
<tr>
<td>磁悬浮</td>
<td>Maglev，类似针对上游主机实现一致性哈希，但具有<span style="background-color: #c0c0c0;">快得多的查表编译时间以及主机选择时间</span>（当使用 256K 条目的大环时大约分别为 10 倍和 5 倍），缺点是<span style="background-color: #c0c0c0;">不如环哈希稳定，主机移除时更多请求需要移动（以达到再平衡）</span></td>
</tr>
<tr>
<td>随机</td>
<td>Random，随机选择一个健康主机</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">异常检测和驱除</span></div>
<p><span style="background-color: #c0c0c0;">异常检测属于被动健康检查</span>，是被动的（观测业务流量，而非主动发起流量）检查上游集群，并发现其中的不正常主机，然后将其剔除出负载均衡池的过程。</p>
<p>异常检测<span style="background-color: #c0c0c0;">通常和主动健康检查联合使用</span>。</p>
<div class="blog_h3"><span class="graybg">驱除算法</span></div>
<p>根据异常检测的类型，<span style="background-color: #c0c0c0;">驱除行为要么定期触发</span>，要么在<span style="background-color: #c0c0c0;">连续的异常时间发生后触发</span>。算法工作方式如下：</p>
<ol>
<li>主机被确定为异常</li>
<li>如果集群中<span style="background-color: #c0c0c0;">没有主机被驱除，则立即驱除当前主机</span>。否则检查被驱除主机<span style="background-color: #c0c0c0;">是否小于阈值</span>outlier_detection.max_ejection_percent，如果大于则不驱除</li>
<li>主机被<span style="background-color: #c0c0c0;">驱除，也就是标记为不健康，且不参与负载均衡</span>。<span style="background-color: #c0c0c0;">除非上游集群处于Panic状态</span>（也就是健康主机太少）</li>
<li>驱除持续的时间是<em> outlier_detection.base_ejection_time_ms * 主机已经被驱除的次数</em>，<span style="background-color: #c0c0c0;">超过此时间后，自动回到负载均衡池</span>。主机被驱除的次数越多，则返回负载均衡池需要等待的时间越长</li>
</ol>
<div class="blog_h3"><span class="graybg">检测类型</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">检测类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>连续5xx</td>
<td>如果上游主机连续返回若干（由outlier_detection.consecutive_5xx 控制）5xx响应，则会被驱除</td>
</tr>
<tr>
<td>连续网关故障</td>
<td>如果上游主机连续返回若干（由outlier_detection.consecutive_gateway_failure控制）网关错误（502 503 504），则会被驱除</td>
</tr>
<tr>
<td>成功率</td>
<td>
<p>如果在指定聚合周期内，请求处理成功率低于阈值，则驱除</p>
<p>如果聚合周期内，主机请求量小于outlier_detection.success_rate_request_volume，或者聚合周期内请求量小于outlier_detection.success_rate_request_volume的主机总数小于outlier_detection.success_rate_minimum_hosts ，都不进行驱除</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">断路器</span></div>
<p>Envoy支持多种类型的<span style="background-color: #c0c0c0;">完全分布式的断路器</span>，以保护上游集群不因过载而崩溃：</p>
<ol>
<li>集群最大连接数：为上游集群的<span style="background-color: #c0c0c0;">所有主机设置总和最大连接数</span>。注意这仅仅适用于HTTP/1.1，因为HTTP/2为每个上游主机维护单个连接</li>
<li>集群最大挂起请求数<span style="background-color: #c0c0c0;">：排队等待可用连接的最大请求数</span>。注意这仅仅适用于HTTP/1.1，因为HTTP/2允许多个请求复用TCP连接</li>
<li>集群最大请求数：<span style="background-color: #c0c0c0;">在给定的时间内，集群总计可以处理的请求数量</span></li>
<li>集群最大活动重试次数：在给定的时间内，<span style="background-color: #c0c0c0;">集群总计允许发送的重试次数</span></li>
</ol>
<p>你可以针对每个上游集群进行断路器配置。</p>
<p>对于HTTP请求，被断路后，路由过滤器会设置响应头x-envoy-overloaded。</p>
<div class="blog_h2"><span class="graybg">全局限速</span></div>
<p>分布式系统中，断路器对吞吐量的控制一般非常有效。但是某些场景下，可能需要引入<span style="background-color: #c0c0c0;">全局的限速机制</span>。典型的场景例如数据库，来自<span style="background-color: #c0c0c0;">大量主机的请求被转发给少量的服务器</span>，很难在所有下游主机上配置断路器来防止级联性失败（雪崩）。</p>
<p>Envoy集成了全局的gRPC限速服务，Lyft提供该服务的参考实现，此外任何实现了RPC/IDL协议的程序都可以作为服务实现。</p>
<p>限速服务的功能包括：</p>
<ol>
<li>网络层限速过滤器：安装此过滤器的监听器，在接受新连接时就会调用限速服务。支持根据<span style="background-color: #c0c0c0;">每秒连接次数进行限制</span></li>
<li>HTTP级限速过滤器</li>
</ol>
<div class="blog_h2"><span class="graybg">TLS支持</span></div>
<p>对于监听器，Envoy支持TLS Termination。向上游发起请求时，Envoy支持TLS Origination。Envoy的SSL支持由BoringSSL库提供。</p>
<div class="blog_h2"><span class="graybg">统计</span></div>
<p>Envoy的一个目标是使网格可理解（观察），为此它可以收集大量的统计信息，统计信息可以分为三类：</p>
<ol>
<li>下游：牵涉到传入的连接/请求。由监听器、HTTP连接管理器、TCP代理过滤器生成</li>
<li>上游：牵涉到传出的连接/请求。由连接池、路由器过滤器、TCP代理过滤器生成</li>
<li>服务器：牵涉到服务实例，统计指标包括服务器正常运行时间、内存分配量等</li>
</ol>
<p>在v1版本的API中，Envoy仅仅支持Statd作为输出格式。<span style="background-color: #c0c0c0;">在v2版本的API中，可以使用自定义，可拔插的统计信息接收器</span>。</p>
<p>统计信息都使用规范化的字符串来标识，字符串的<span style="background-color: #c0c0c0;">动态部分被抽取成为标签（Tag）</span>。</p>
<p>Envoy支持<span style="background-color: #c0c0c0;">Counter、Gauge、Histogram三种指标类型</span>。</p>
<div class="blog_h2"><span class="graybg">Tracing</span></div>
<p>在大型分布式架构中，分布式追踪让调用过程能够被可视化的展示，在了解串并行执行、延迟分析时非常重要。</p>
<p>Envoy包含三个和分布式追踪相关的特性：</p>
<ol>
<li>请求ID生成：如果需要，Envoy会自动生成 x-request-id 头表示请求UUID</li>
<li>外部追踪服务集成：Envoy支持可拔插的、外部的追踪可视化服务。目前支持LightStep，Datadog，Zipkin，以及任何Zipkin兼容的服务（例如Jaeger）</li>
<li>客户端Trace ID连接：头x-client-trace-id可以用于将不受信任的请求ID连接到内部受信任的x-request-id</li>
</ol>
<div class="blog_h3"><span class="graybg">Trace的触发</span></div>
<p>初始化追踪的方式有多种：</p>
<ol>
<li>外部客户端，使用 x-client-trace-id 头</li>
<li>内部服务，使用x-envoy-force-trace头</li>
<li>随机采样，使用运行时设置random_sampling</li>
</ol>
<p>路由过滤器<span style="background-color: #c0c0c0;">可以使用start_child_span为出口调用创建下一级Span</span>。</p>
<div class="blog_h3"><span class="graybg">追踪上下文传播</span></div>
<p>Envoy提供了根据网格中服务之间通信来报告追踪信息的能力，但是要想把一个调用流中各个代理产生的追踪片断关联起来，服务必须<span style="background-color: #c0c0c0;">在入口、出口请求之间传递一些上下文信息</span>。</p>
<p>不管使用哪种Tracer，所有服务都<span style="background-color: #c0c0c0;">必须传播x-request-id头</span>，服务实现者可能需要在<span style="background-color: #c0c0c0;">代码层次上保证这一点</span>（技术上无法实现通用的自动传播x-request-id的机制）。</p>
<p>Tracer还会需要额外的上下文信息，确保Span之间的父子关系能够被理解。</p>
<p>可以在服务中直接调用LightStep（OpenTracing API）或Zipkin来抽取入站请求的上下文信息，并注入到出站请求中。使用这种方式，服务也可以创建额外的Span。</p>
<p>另外一种方式是<span style="background-color: #c0c0c0;">由服务手动的传播追踪信息</span>：</p>
<ol>
<li>如果使用LightStep，Envoy依赖服务在发起请求时传播x-ot-span-context头</li>
<li>如果使用Zipkin，Envoy依赖服务B3请求头（x-b3-traceid, x-b3-spanid, x-b3-parentspanid, x-b3-sampled, x-b3-flags）。外部客户端也可以提供x-b3-sampled头，以启用/禁用对某个请求的追踪</li>
<li>如果使用Datadog，Envoy依赖服务在发起请求时传播 x-datadog-trace-id, x-datadog-parent-id, x-datadog-sampling-priority头</li>
</ol>
<div class="blog_h3"><span class="graybg">Trace中包含什么</span></div>
<p>一个端到端的Trace包含1-N个Span。一个<span style="background-color: #c0c0c0;">Span代表了一个逻辑的工作单元</span>。工作单元包括开始时间，持续时间，以及一系列的元数据：</p>
<ol>
<li>源自的服务集群，通过--service-cluster指定</li>
<li>开始时间、持续时长</li>
<li>源自的主机，通过--service-node指定</li>
<li>下游服务集群，通过x-envoy-downstream-service-cluster头指定</li>
<li>HTTP URL</li>
<li>HTTP方法</li>
<li>HTTP响应码</li>
<li>Tracer相关的信息</li>
</ol>
<p>Envoy<span style="background-color: #c0c0c0;">自动将Span发送给跟踪收集器</span>（Tracing collector），<span style="background-color: #c0c0c0;">Span根据一些公共信息（例如x-request-id）组合在一起，产生单个Trace</span>。</p>
<div class="blog_h2"><span class="graybg">TCP代理</span></div>
<p>TCP代理过滤器可以实现<span style="background-color: #c0c0c0;">下游 - 上游的基本的1:1代理功能</span>。</p>
<p>TCP代理过滤器受到上游集群全局资源管理器的连接限制的约束。</p>
<div class="blog_h2"><span class="graybg">访问日志</span></div>
<p>HTTP 连接管理器与 TCP 代理支持可扩展的访问记录，并拥有以下特性：</p>
<ol>
<li>可按每个 HTTP 连接管理器或 TCP 代理记录任何数量的访问记录</li>
<li>异步 IO 架构。<span style="background-color: #c0c0c0;">访问记录将永远不会阻塞主网络处理线程</span></li>
<li>可定制化的访问记录格式，可使用预定义的字段，也可使用任意的 HTTP 请求和应答</li>
<li>可定制化的访问记录过滤器，可允许不同类型的请求以及应答写入至不同的访问记录</li>
</ol>
<div class="blog_h2"><span class="graybg">MongoDB支持</span></div>
<p>Envoy实现了网络层的MongoDB嗅探过滤器，特性包括：</p>
<ol>
<li>MongoDB的数据格式BSON的解析</li>
<li>详细的MongoDB查询/操作统计，例如耗时、Scatter/multi-get次数</li>
<li>查询日志</li>
<li>基于$comment 参数做每个调用点的统计分析报告</li>
<li>故障注入</li>
</ol>
<div class="blog_h2"><span class="graybg">Redis支持</span></div>
<p>Envoy可以作为Redis代理，在Redis实例之间进行<span style="background-color: #c0c0c0;">命令分区（partitioning commands）</span>。Envoy的目标是维持可用性、分区容忍性（partition tolerance），它实现一个尽最大努力的缓存，而<span style="background-color: #c0c0c0;">不会尝试维护数据一致性或者Redis集群成员的一致性</span>。这和Redis Cluster不同。</p>
<p>Envoy Redis的特性包括：</p>
<ol>
<li>Redis协议编解码</li>
<li>基于哈希的分区</li>
<li>基于一致性哈希（Ketama）的分区</li>
<li>详细的命令统计</li>
<li>主动和被动健康检查</li>
</ol>
<p>未来计划添加的特性包括：</p>
<ol>
<li>额外的耗时统计</li>
<li>断路器</li>
<li>对片断化命令进行请求合并</li>
<li>数据复制</li>
<li>内置重试</li>
<li>追踪支持</li>
<li>Hash Tagging</li>
</ol>
<p><span style="background-color: #c0c0c0;">Redis上游集群应该以哈希环负载均衡的方式配置</span>（ring hash load balancing）。</p>
<p>此代理支持Redis流水线，但是不支持MULTI。此外，不是所有Redis命令被支持，仅仅那些<span style="background-color: #c0c0c0;">可以被可靠的哈希到一个服务器的操作被支持</span>。</p>
<p>一个特殊命令是PING，Envoy会直接答复PONG，为PING指定参数是不允许的。</p>
<div class="blog_h2"><span class="graybg">gRPC支持</span></div>
<p>gRPC是Google开源的RPC框架，它使用Protocol Buffer作为串行化格式。在传输层，它使用HTTP/2进行请求/应答的多路复用。</p>
<p>Envoy对gRPC在传输层、应用层都做了支持：</p>
<ol>
<li><span style="background-color: #c0c0c0;">gRPC使用HTTP/2 Trailers来递送请求状态</span>。Envoy是少数几个能正确支持HTTP/2 Trailers的代理——也就是能支持gRPC的代理</li>
<li>某些语言的gRPC实现尚不成熟，Envoy提供了一些过滤器，能够将gRPC桥接到这些语言</li>
<li>Envoy通过过滤器grpc_web支持gRPC-Web，允许<span style="background-color: #c0c0c0;">客户端向Envoy发送HTTP/1.1请求，由Envoy将其转换为gRPC协议</span>并转发给上游服务器。此过滤器将在未来代替grpc_http1_bridge</li>
<li>Envoy通过过滤器支持gRPC-JSON转码，允许客户端基于REST JSON API发送请求，由Envoy转换其为gRPC请求给上游服务器</li>
</ol>
<div class="blog_h3"><span class="graybg">gRPC桥接</span></div>
<p>Envoy支持两种gRPC桥接：</p>
<ol>
<li>grpc_http1_bridge filter：允许gRPC请求通过HTTP/1.1发送给Envoy，后者通过标准的gRPC协议转发给gRPC服务器</li>
<li>grpc_http1_reverse_bridge filter：能够将gRPC请求转换为HTTP/1.1并发送给上游服务器。此过滤器能够管理gRPC帧的头，允许上游服务器对gRPC协议一无所知</li>
</ol>
<div class="blog_h3"><span class="graybg">gRPC服务</span></div>
<p>Envoy在它的控制平面中大量使用gRPC协议，例如从管理服务器订阅资源配置信息时。一些过滤器，例如限速、授权，也使用gRPC协议。</p>
<p>Envoy gRPC client是最小化的gRPC客户端实现，利用Envoy的HTTP/2上游连接管理机制。</p>
<p>Google C++ gRPC提供完备的gRPC实现，包含Envoy所缺失的特性，具有自己的负载均衡、重试、超时、端点管理等功能。</p>
<p>进行Envoy相关的开发时，建议使用Envoy gRPC client。</p>
<div class="blog_h2"><span class="graybg">热重启</span></div>
<p>Envoy 可以在<span style="background-color: #c0c0c0;">不丢失任何连接的情况下完全地重载自己</span>，包括<span style="background-color: #c0c0c0;">代码与配置</span>。</p>
<div class="blog_h2"><span class="graybg">动态配置</span></div>
<div class="blog_h3"><span class="graybg">全静态配置</span></div>
<p>你可以提供一组监听器（和过滤器链）、集群、HTTP路由配置。动态主机仅通过DNS发现。</p>
<p>如果变更配置，则可以利用Envoy的热重启机制加载最新配置。</p>
<div class="blog_h3"><span class="graybg">SDS/EDS</span></div>
<p>通过服务发现服务（SDS）API，Envoy可以发现上游集群的成员，<span style="background-color: #c0c0c0;">在v2版本的API中，SDS已经更名为EDS</span>。</p>
<p>使用SDS可以规避DNS发现的缺点（记录数量受限），并遵从负载均衡、路由配置。</p>
<div class="blog_h3"><span class="graybg">CDS</span></div>
<p>通过集群发现服务（CDS）API，Envoy可以发现上游集群。</p>
<div class="blog_h3"><span class="graybg">RDS</span></div>
<p>通过路由发现服务（RDS）API，Envoy可以在运行时发现用于HTTP连接管理器过滤器的<span style="background-color: #c0c0c0;">整个路由配置</span>。路由配置可以优雅的切换，而不影响处理中的请求。</p>
<div class="blog_h3"><span class="graybg">LDS</span></div>
<p>通过监听器发现服务（LDS）API，Envoy可以发现整个监听器，包括它的完整过滤器栈。</p>
<p>配合使用SDS/EDS、CDS、RDS 和 LDS，几乎可以动态配置Envoy的所有方面。只有非常少见的配置更改（管理员、追踪驱动程序等）或二进制更新时才需要热重启。</p>
<div class="blog_h2"><span class="graybg">初始化过程</span></div>
<p>任何监听器启动监听并接收新连接前，会发生如下事件序列：</p>
<ol>
<li>启动期间，集群管理器会执行多步骤的初始化过程：
<ol>
<li>初始化静态/DNS集群</li>
<li>初始化预定义EDS集群</li>
<li>如果可用，初始化CDS并等待响应，然后初始化CDS提供的集群</li>
</ol>
</li>
<li>如果<span style="background-color: #c0c0c0;">集群启用了健康检查，则执行一轮检查</span></li>
<li>集群管理器初始化完毕后，RDS和LDS开始初始化</li>
<li>如果LDS本身返回的监听器要求RDS响应，则Envoy会等待RDS响应的到达</li>
<li>监听器开始接受连接</li>
</ol>
<div class="blog_h2"><span class="graybg">关闭过程</span></div>
<p>关闭（Draining）发生在以下时机：</p>
<ol>
<li>通过healthcheck/fail管理客户端，对服务器执行手工的健康检查失败</li>
<li>Envoy被热重启</li>
<li>单个监听器通过LDS添加或移除</li>
</ol>
<p>每个监听器都具有drain_type配置，决定何时发生Draining：</p>
<ol>
<li>default，上述三种时机都发生</li>
<li>modify_only，仅仅上述2-3时发生</li>
</ol>
<div class="blog_h2"><span class="graybg">脚本支持</span></div>
<p>HTTP过滤器支持使用Lua脚本。</p>
<div class="blog_h2"><span class="graybg">过载管理器</span></div>
<p>这是一个可扩展组件，用于保护Envoy本身（和断路器不同），防止过载导致各种系统资源（内存、CPU、文件描述符）不足。和断路器用于保护上游服务的目的不同，过载管理器是为了保护Envoy代理所在机器本身。</p>
<div class="blog_h1"><span class="graybg">新特性</span></div>
<div class="blog_h2"><span class="graybg">v1.14</span></div>
<ol>
<li>访问日志：
<ol>
<li>支持字段%DOWNSTREAM_LOCAL_PORT%</li>
<li>引入<a href="https://www.envoyproxy.io/docs/envoy/v1.14.1/api-v2/api/v2/listener.proto#envoy-api-field-listener-access-log">连接级别的日志记录器</a></li>
</ol>
</li>
<li>自适应并发：
<ol>
<li>修复允许的并发限制小于配置的最小值的缺陷</li>
</ol>
</li>
<li>管理：
<ol>
<li>支持再certs页面显示IP地址的SAN</li>
<li>POST /reopen_logs用于控制日志轮换</li>
</ol>
</li>
<li>API：冻结v2 xDS API，新特性将在v3 xDS中开发。v2 xDS API目前已经启用，支持在2020年底结束</li>
<li>AWS特性：
<ol>
<li>过滤器aws_lambda，能够将HTTP请求转换为Lambda调用</li>
<li>aws_request_signing，一些缺陷修复</li>
</ol>
</li>
<li>配置：
<ol>
<li>添加统计项update_time</li>
</ol>
</li>
<li>数据源：为远程异步数据源天津爱重试策略</li>
<li>DNS：
<ol>
<li>为DNS缓存添加dns_failure_refresh_rate配置项，可以设置失败期间的DNS刷新率</li>
<li>如果DNS查询返回0主机，则STRICT_DNS类型的集群仅仅解析0主机</li>
</ol>
</li>
<li>EDS：
<ol>
<li>为端点、端点的健康检查增加hostname字段。可以实现自动hostname重写，定制健康检查使用的host头</li>
</ol>
</li>
<li>HTTP：
<ol>
<li>HTTP1.1泛洪保护</li>
<li>配置 headers_with_underscores_action，指定遇到具有下划线的HTTP头时怎么处理，可以允许头、丢弃头、拒绝请求</li>
<li>配置max_stream_duration，允许<a href="https://www.envoyproxy.io/docs/envoy/v1.14.1/faq/configuration/timeouts#faq-configuration-timeouts">指定现有流持续时间</a></li>
</ol>
</li>
<li>LB：
<ol>
<li>支持使用hostname进行一致性哈希负载均衡（consistent_hash_lb_config）</li>
</ol>
</li>
<li>Lua：
<ol>
<li>为httpCall添加一个参数，是其可以异步化</li>
<li>添加moonjit支持</li>
</ol>
</li>
<li>网络过滤器：
<ol>
<li>添加envoy.filters.network.direct_response</li>
</ol>
</li>
<li>RBAC：
<ol>
<li>添加remote_ip、 direct_remote_ip 用于匹配下游远程地址。启用source_ip</li>
</ol>
</li>
<li>request_id_extension：可以在HTTP连接管理器中扩展request id处理逻辑</li>
</ol>
<div class="blog_h2"><span class="graybg">v1.13</span></div>
<ol>
<li>完全<span style="background-color: #c0c0c0;">移除对v1 API</span>的支持</li>
<li>添加对<span style="background-color: #c0c0c0;">xDS v3 API的支持</span></li>
<li>集群：支持<a href="https://www.envoyproxy.io/docs/envoy/v1.13.0/intro/arch_overview/upstream/aggregate_cluster#arch-overview-aggregate-cluster">聚合集群</a>，在<span style="background-color: #c0c0c0;">多个集群之间进行负载均衡</span></li>
<li>监听器：添加reuse_port选项</li>
<li>限速：支持<a href="https://www.envoyproxy.io/docs/envoy/v1.13.0/configuration/listeners/network_filters/local_rate_limit_filter#config-network-filters-local-rate-limit">本地限速</a></li>
<li>RBAC：支持基于所有SAN进行匹配principal_name</li>
<li>路由：支持基于百分比的<a href="https://www.envoyproxy.io/docs/envoy/v1.13.0/api-v2/api/v2/cluster/circuit_breaker.proto#envoy-api-field-cluster-circuitbreakers-thresholds-retry-budget">retry budgets</a></li>
<li>路由：允许<span style="background-color: #c0c0c0;">基于查询参数进行一致性哈希</span></li>
<li>路由：添加auto_sni，支持<span style="background-color: #c0c0c0;">基于下游HTTP host/authority头来设置SNI</span></li>
<li>TCP代理：添加hash_policy</li>
<li><span style="background-color: #c0c0c0;">UDP代理：初步支持</span></li>
<li>支持HTTP/1.1泛洪保护，此特性可以通过运行时特性envoy.reloadable_features.http1_flood_protection停用</li>
</ol>
<div class="blog_h2"><span class="graybg">v1.12</span></div>
<ol>
<li>配置：<span style="background-color: #c0c0c0;">支持<a href="https://www.envoyproxy.io/docs/envoy/v1.13.0/intro/arch_overview/operations/dynamic_configuration#arch-overview-dynamic-config-delta"><span style="background-color: #c0c0c0;">增量xDS</span></a>（包括ADS）传递</span></li>
<li>配置：确保终止过滤器（例如L4的HTTP连接管理器、L7的路由器）位于过滤器链的尾部</li>
<li>HTTP：支持自动合并Path中连续的 /</li>
<li>HTTP：支持在正向代理中进行<a href="https://www.envoyproxy.io/docs/envoy/v1.13.0/api-v2/config/filter/http/dynamic_forward_proxy/v2alpha/dynamic_forward_proxy.proto#envoy-api-msg-config-filter-http-dynamic-forward-proxy-v2alpha-perrouteconfig">host rewrite</a></li>
</ol>
<div class="blog_h2"><span class="graybg">v1.11</span></div>
<ol>
<li>支持Dubbo Proxy过滤器</li>
<li>健康检查：添加initial jitter，在第一次健康检查时添加jitter，防止Envoy启动时的惊群效应</li>
<li>热重启：新旧进程不再通过共享内存共享状态，而是gRPC</li>
<li>HTTP：<span style="background-color: #c0c0c0;">支持正向代理</span></li>
<li>监听器：添加基于source IP、source Port的过滤器链匹配</li>
<li>添加<span style="background-color: #c0c0c0;">original_src过滤器</span>，可以获取原始的源地址</li>
</ol>
<div class="blog_h2"><span class="graybg">v1.10</span></div>
<ol>
<li>添加CSRF过滤器</li>
<li>支持将gRPC请求转换为HTTP/1.1请求</li>
<li>添加MySQL过滤器，能够基于MySQL wire协议解析SQL查询</li>
<li>路由：虚拟主机域名支持前缀统配</li>
<li>添加Tap HTTP过滤器，用于录制流量</li>
</ol>
<div class="blog_h1"><span class="graybg">术语</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">名词</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h2">Host</td>
<td>
<p>一个支持网络通信的实体，例如手机、服务器、虚拟机上的应用程序 </p>
<p>这里的Host是逻辑的网络应用，一个物理设备上可以有很多Host，只要它们可以被独立寻址</p>
</td>
</tr>
<tr>
<td class="blog_h2">Downstream</td>
<td>向Envoy发送请求、接收响应的Host</td>
</tr>
<tr>
<td class="blog_h2">Upstream</td>
<td>Envoy向其发送请求、接收响应的Host </td>
</tr>
<tr>
<td class="blog_h2">Listener</td>
<td>
<p>命名的网络位置，例如端口、UNIX域套接字，可以被下游Host连接</p>
<p>Envoy暴露一个或多个Listener供下游主机连接 </p>
</td>
</tr>
<tr>
<td class="blog_h2">Cluster</td>
<td>
<p>一组逻辑相似的上游Host。Envoy通过服务发现机制识别Cluster的成员，并可选的通过主动健康检查确认Cluster成员的健康状态</p>
<p>Envoy根据负载均衡策略来决定将请求路由给哪个成员</p>
</td>
</tr>
<tr>
<td class="blog_h2">Mesh</td>
<td>一组Host通过协作产生的网络拓扑。Envoy Mesh是指由Envoy代理构成的Mesh</td>
</tr>
<tr>
<td class="blog_h2">Runtime Config</td>
<td>不需要重启就可以重新配置Envoy的机制</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">部署方式</span></div>
<div class="blog_h2"><span class="graybg">仅服务到服务</span></div>
<p>这种场景下，Envoy仅仅代理一个SOA架构的内部的通信，Envoy需要暴露若干监听器，供本地发起的流量、服务到服务的流量使用。</p>
<div class="blog_h3"><span class="graybg">S2S出口监听器</span></div>
<p>用于本地应用程序和基础设施中其它服务进行交互。HTTP/gRPC请求通过HTTP/1.1的host头或HTTP/2的authority来指示它的目标是哪里。Envoy负责处理服务发现、负载均衡、限速。</p>
<p>本地应用程序仅仅需要和Envoy通信，而不需要知道网络拓扑，不需要知道自己运行在开发环境还是线上环境。</p>
<div class="blog_h3"><span class="graybg">S2S入口监听器</span></div>
<p>这种监听器供需要和本地Envoy通信的外部Envoy使用。流量被路由到本地服务的端口。本地代理负责缓冲、断路器等功能。</p>
<div class="blog_h3"><span class="graybg">外部服务出口监听器</span></div>
<p>通常来说，对于每个本地服务需要与之通信的外部服务，都分配一个明确的、独占的出口端口。这是由于某些外部服务的SDK无法容易的基于重写host头来实现标准的HTTP反向代理行为。</p>
<div class="blog_h2"><span class="graybg">S2S+边缘代理</span></div>
<p>可以在服务到服务部署方式基础上，配合基于Envoy实现的边缘代理。并在边缘代理实现：</p>
<ol>
<li>TLS Termination</li>
<li>同时支持HTTP/1.1和HTTP/2</li>
<li>完整的HTTP L7路由支持</li>
<li>基于标准的Ingress端口和S2S的Envoy集群通信</li>
</ol>
<div class="blog_h1"><span class="graybg">起步</span></div>
<div class="blog_h2"><span class="graybg"><a id="first-example"></a>第一个例子</span></div>
<p>尝试Envoy功能最轻松的途径是使用官方的Docker镜像：</p>
<pre class="crayon-plain-tag">docker pull envoyproxy/envoy:latest</pre>
<p> 此镜像包含了一个默认的配置，将入站请求路由给某个外部网站：</p>
<pre class="crayon-plain-tag"># 管理端口配置
admin:
  access_log_path: /tmp/admin_access.log
  address:
    socket_address:
      protocol: TCP
      address: 127.0.0.1
      port_value: 9901
# 硬编码的静态资源
static_resources:
  # 唯一的监听器，监听10000端口
  listeners:
  - name: listener_0
    address:
      socket_address:
        protocol: TCP
        address: 0.0.0.0
        port_value: 10000
    # 可以为监听器配置过滤器链
    filter_chains:
    - filters:
      # HTTP连接管理器，内部可以配置多个HTTP过滤器
      - name: envoy.http_connection_manager
        config:
          stat_prefix: ingress_http
          # 路由配置，不论域名是什么，都发给上游集群service_google，同时改写
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match:
                  prefix: "/"
                route:
                  host_rewrite: blog.gmem.cc
                  cluster: service_gmem
          # 启用路由过滤器
          http_filters:
          - name: envoy.router
  # 上游集群配置
  clusters:
  - name: service_gmem
    connect_timeout: 1s
    type: LOGICAL_DNS
    # 仅仅基于IPv4网络进行测试
    dns_lookup_family: V4_ONLY
    lb_policy: ROUND_ROBIN
    hosts:
      - socket_address:
          address: blog.gmem.cc
          port_value: 443
    tls_context: { sni: blog.gmem.cc }</pre>
<p>在容器中执行下面的容器启动Envoy：</p>
<pre class="crayon-plain-tag">docker run -it --rm --name envoy --network local --ip 172.21.0.2 --dns 172.21.0.1 docker.gmem.cc/envoyproxy/envoy bash
envoy --config-path /etc/envoy/envoy.yaml</pre>
<p>启动完毕后，Envoy会监听环回网卡的9901端口，以及0.0.0.0:10000端口。可以在容器中</p>
<pre class="crayon-plain-tag">curl http://localhost:10000</pre>
<p>验证是否访问到了blog.gmem.cc。 </p>
<div class="blog_h2"><span class="graybg">沙盒</span></div>
<p>在Envoy源码的examples目录下，有若干基于Docker Compose创建的沙盒环境，可以用来试验Envoy各方面的功能。 </p>
<div class="blog_h1"><span class="graybg">构建和调试</span></div>
<div class="blog_h2"><span class="graybg">构建</span></div>
<p>Envoy使用Bazel作为构建系统。 </p>
<div class="blog_h3"><span class="graybg">工具链要求</span></div>
<p>尽管基于GCC 4.9编译的Envoy已经在生产环境中运行，最好使用GCC 5以上版本，原因是<span style="color: #24292e;">std::string线程安全有关的一个缺陷。</span></p>
<p>从GCC 5.1开始，libstdc++引入了一个新的库ABI，此ABI包含新的std::string、std::list实现，遵循2011 C++标准：</p>
<ol>
<li>禁止Copy-On-Write字符串</li>
<li>List应当跟踪自身的尺寸</li>
</ol>
<p>为了保持对已经链接到libstdc++的程序的向后兼容，libstdc++的soname没有修改，老版本和新版本并行存在于同一个库文件中。具体实现方式是，在内联的命名空间中定义新的实现，例如新版本的 std::list&lt;int&gt;实际上定义在std::__cxx11::list&lt;int&gt;。</p>
<p>宏_GLIBCXX_USE_CXX11_ABI决定了libstdc++库的头文件使用新的还是就的ABI，这意味着每个源文件都可以决定自己在编译时使用新的还是就的ABI。GCC 5默认配置此宏为1也就是新ABI，但是某些Linux发行版配置为0，你需要手工定义此宏为1才能使用新ABI。</p>
<p>Clang 4.0以上版本也被支持。但是注意默认情况下Clang丢弃了格式化打印所需的一些调试符号，你需要在.bazelrc文件中添加<span style="color: #24292e;"><pre class="crayon-plain-tag">--copt=-fno-limit-debug-info</pre>解决。</span></p>
<div class="blog_h3"><span class="graybg">编译模式</span></div>
<p>Envoy支持三种编译模式，可以通过Bazel的-c参数传递：</p>
<ol>
<li>fastbuild，即 -O0，默认值，用于开发环境</li>
<li>opt，即 -O2 -DNDEBUG -ggdb3，用于产品环境和性能基准测试</li>
<li>dbg，即 -O0 -ggdb3，不优化，保留调试符号</li>
</ol>
<p>修改编译模式的例子：</p>
<pre class="crayon-plain-tag">bazel build -c opt //source/exe:envoy-static</pre>
<div class="blog_h3"><span class="graybg">禁用可选特性</span></div>
<p>下面的可选特性可通过Bazel命令行参数禁用：</p>
<ol>
<li>热重启，通过--define hot_restart=disabled禁用</li>
<li>Google C++ gRPC客户端，通过--define google_grpc=disabled禁用</li>
<li>捕获到信号时打印堆栈，通过--define signal_trace=disabled禁用</li>
<li>tcmalloc，通过-define tcmalloc=disabled禁用</li>
</ol>
<div class="blog_h3"><span class="graybg">启用可选特性</span></div>
<p>下面的可选特性可通过Bazel命令行参数启用：</p>
<ol>
<li>在链接时导出符号：--define exported_symbols=enabled，如果你的Lua脚本加载共享对象库，可以启用</li>
<li>Perf注解：--define perf_annotation=enabled，用于配合性能剖析</li>
<li>在FIPS兼容模式下构建BoringSSL：--define boringssl=fips</li>
<li>内存调试：--define tcmalloc=debug</li>
</ol>
<div class="blog_h3"><span class="graybg">禁用扩展</span></div>
<p>Envoy使用模块化的构建风格，不需要的扩展可以被移除。所有可以被禁用的扩展列在文件中<a href="https://github.com/envoyproxy/envoy/blob/master/source/extensions/extensions_build_config.bzl">extensions_build_config.bzl</a>中。</p>
<p>使用下面的步骤来定制你需要的扩展集：</p>
<ol>
<li>构建Envoy时，默认假设@envoy_build_config//:extensions_build_config.bzl存在，此文件默认包含所有扩展</li>
<li>创建一个新的WORKSPACE，在其中包含：
<ol>
<li>空白的WORKSPACE文件</li>
<li>空白的BUILD文件</li>
<li>extensions_build_config.bzl的拷贝</li>
<li>注释掉你不需要的扩展</li>
</ol>
</li>
</ol>
<p>要想让你的本地构建使用修改后的extensions_build_config.bzl，可以使用下面两种方法之一：</p>
<ol>
<li>使用--override_repository选项，覆盖@envoy_build_config存储库</li>
<li>在你的WORKSPACE中添加如下片断：<br />
<pre class="crayon-plain-tag">workspace(name = "envoy")

local_repository(
    name = "envoy_build_config",
    # Relative paths are also supported.
    path = "/somewhere/on/filesystem/envoy_build_config",
) </pre>
</li>
</ol>
<div class="blog_h3"><span class="graybg">容器中构建</span></div>
<p>Envoy把构建环境固化在Docker镜像中，可以执行下面的命令在容器中构建Envoy：</p>
<pre class="crayon-plain-tag">./ci/run_envoy_docker.sh './ci/do_ci.sh bazel.dev'</pre>
<div class="blog_h3"><span class="graybg">生产环境构建</span></div>
<pre class="crayon-plain-tag">bazel --bazelrc=/dev/null build -c opt //source/exe:envoy-static.stripped </pre>
<div class="blog_h3"><span class="graybg">开发环境构建</span></div>
<p>为了便于开发者，Envoy提供了一个WORKSPACE、构建最近版本的依赖的规则集，这仅仅适用于开发和测试，因为依赖可能不是最新的而包含安全缺陷。</p>
<p>构建步骤如下：</p>
<ol>
<li>安装Bazel</li>
<li>安装必要的库和工具：<br />
<pre class="crayon-plain-tag">echo "deb http://apt.llvm.org/trusty/ llvm-toolchain-trusty-7 main" | sudo tee /etc/apt/sources.list.d/llvm.list
sudo apt-get install \
   libtool \
   cmake \
   realpath \
   clang-format-7 \
   automake \
   ninja-build \
   curl \
   unzip

# Ubuntu 14.04执行下面的步骤，因为需要GCC-5，因此添加下面的源并安装
sudo add-apt-repository ppa:ubuntu-toolchain-r/test
sudo apt-get update
sudo apt-get install gcc-5 g++-5
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-5 1 --slave /usr/bin/g++ g++ /usr/bin/g++-5</pre>
</li>
<li>安装Golang SDK，构建BoringSSL、Buildifer（用于格式化BUILD文件）需要用到</li>
<li>安装buildifier：<br />
<pre class="crayon-plain-tag">go get github.com/golang/protobuf/proto
go get github.com/bazelbuild/buildtools/buildifier</pre>
</li>
<li>进入Envoy源码根目录，执行：<br />
<pre class="crayon-plain-tag">bazel build  --copt "-DENVOY_IGNORE_GLIBCXX_USE_CXX11_ABI_ERROR=1"  //source/exe:envoy-static</pre>
</li>
</ol>
<div class="blog_h3"><span class="graybg">清空构建结果</span></div>
<pre class="crayon-plain-tag">bazel clean --expunge</pre>
<p>可以手工删除~/.cache/bazel，以清除全部缓存。 </p>
<div class="blog_h3"><span class="graybg">调试构建过程</span></div>
<p>如果想理解Bazel在做什么，可以启用-s、--explain选项，Bazel会提供更多的输出。</p>
<div class="blog_h3"><span class="graybg">IDE支持</span></div>
<p>Bazel提供了支持CLion的插件，可以将Bazel项目导入到CLion中。但是CLion是基于CMake的，必须生成CMakeLists.txt才能进行代码导航，否则到处都是红色的错误。</p>
<p><a href="https://github.com/lizan/bazel-cmakelists">bazel-cmakelists</a>是一个Python小工具，能够从Bazel的C++目标生成CMakeLists.txt。</p>
<p>执行下面的命令，可以生成CMakeLists.txt：</p>
<pre class="crayon-plain-tag">bazel-cmakelists --targets //source/exe:envoy-static</pre>
<div class="blog_h2"><span class="graybg">为Istio构建</span></div>
<p>Istio为Envoy添加了mixer、authn等多个过滤器，这些过滤器代码是独立在<a href="https://github.com/istio/proxy">Istio Proxy项目</a>中维护的。</p>
<p>Istio Proxy项目以Bazel外部依赖的形式引入Envoy的源码</p>
<pre class="crayon-plain-tag">ENVOY_SHA = "925810d00b0d3095a8e67fd4e04e0f597ed188bb"

ENVOY_SHA256 = "26d1f14e881455546cf0e222ec92a8e1e5f65cb2c5761d63c66598b39cd9c47d"

http_archive(
    name = "envoy",
    sha256 = ENVOY_SHA256,
    strip_prefix = "envoy-" + ENVOY_SHA,
    url = "https://github.com/envoyproxy/envoy/archive/" + ENVOY_SHA + ".tar.gz",
)</pre>
<p>你可以修改ENVOY_SHA来引用其它版本的Envoy源码。</p>
<p>执行下面的命令，构建出包含了Istio特有过滤器的Envoy二进制文件：</p>
<pre class="crayon-plain-tag">bazel build  -c dbg --copt "-DENVOY_IGNORE_GLIBCXX_USE_CXX11_ABI_ERROR=1" \
                           --experimental_cc_skylark_api_enabled_packages=tools/build_defs,@foreign_cc_impl// \
                           //src/... //test/...</pre>
<div class="blog_h2"><span class="graybg">测试</span></div>
<div class="blog_h3"><span class="graybg">运行测试</span></div>
<p>运行下面的命令，执行所有测试：</p>
<pre class="crayon-plain-tag">bazel test //test/... </pre>
<p>如果报错ERROR: circular symlinks detected，说明存在符号链接错误。存在错误的路径会打印在控制台，手工修改一下即可。 </p>
<p>运行下面的命令，仅仅执行的单元测试test/common/http:async_client_impl_test：</p>
<pre class="crayon-plain-tag">bazel test //test/common/http:async_client_impl_test</pre>
<p>要查看更多的测试日志输出，使用下面的选项：</p>
<pre class="crayon-plain-tag">bazel test --test_output=streamed</pre>
<div class="blog_h3"><span class="graybg">传递参数</span></div>
<p>要为测试用例传递额外的命令行参数，使用下面的选项：</p>
<pre class="crayon-plain-tag"># 传参
bazel test --test_output=streamed //test/common/http:async_client_impl_test --test_arg="-l trace"</pre>
<div class="blog_h3"><span class="graybg">禁用IPv6</span></div>
<p>默认情况下，用例同时针对IPv4和IPv6测试，在仅仅支持IPv4或IPv6的环境下，可以使用下面的环境变量：</p>
<pre class="crayon-plain-tag"># 禁用IPv6或IPv4
bazel test //test/... --test_env=ENVOY_IP_TEST_VERSIONS=v4only
bazel test //test/... --test_env=ENVOY_IP_TEST_VERSIONS=v6only</pre>
<div class="blog_h3"><span class="graybg">内存泄漏检查</span></div>
<p>默认情况下，Envoy运行测试时使用gperftools的normal模式来检查堆内存泄漏，你可以禁用gperftools或者修改模式：</p>
<pre class="crayon-plain-tag"># 禁用
bazel test //test/... --test_env=HEAPCHECK=
# 使用minimal模式
bazel test //test/... --test_env=HEAPCHECK=minimal</pre>
<p>内存泄漏的偏移量需要通过addr2line来解释，可以利用<pre class="crayon-plain-tag">--config=clang-asan</pre>选项自动化的进行地址到代码行的转换。 </p>
<div class="blog_h3"><span class="graybg">测试缓存 </span></div>
<p>默认情况下，已经通过的测试结果会被缓存，不再测试。如果要强制重新测试，使用选项：</p>
<pre class="crayon-plain-tag">bazel test //test/common/http:async_client_impl_test --cache_test_results=no</pre>
<div class="blog_h3"><span class="graybg">禁用沙盒</span></div>
<p>默认情况下，所有测试在沙盒中运行，不能访问本地文件系统。 使用下面的选项可以禁用沙盒：</p>
<pre class="crayon-plain-tag">--strategy=TestRunner=standalone</pre>
<div class="blog_h3"><span class="graybg">特权模式</span></div>
<p>某些测试要求特殊权限，例如CAP_NET_ADMIN。在物理机上运行测试时，你可以用sudo提权。</p>
<p>如果在容器环境下测试且需要特权，可以利用tools/bazel-test-docker.sh脚本，此脚本可以在本地或者远程Docker引擎上以--privileged运行测试。此脚本的调用格式如下：</p>
<pre class="crayon-plain-tag">tools/bazel-test-docker.sh &lt;bazel-test-target&gt; [optional-flags-to-bazel]
# 示例，在容器中执行集成测试
tools/bazel-test-docker.sh  //test/integration:integration_test --jobs=4 -c dbg</pre>
<p>此脚本读取两个环境变量：</p>
<ol>
<li>RUN_REMOTE=yes|no，是否在远程Docker引擎上执行</li>
<li>LOCAL_MOUNT=yes|no，是否拷贝或挂载本地库到容器中</li>
</ol>
<div class="blog_h3"><span class="graybg">sanitizer</span></div>
<p>你可以启用GCC的ASAN（<a href="https://github.com/google/sanitizers/wiki/AddressSanitizer">address sanitizer</a>，地址消毒剂，检测内存访问错误）、UBSAN（<a href="https://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/">undefined behavior sanitizer</a>，未定义行为消毒剂），来构建和测试Envoy：</p>
<pre class="crayon-plain-tag">bazel test -c dbg --config=asan //test/...</pre>
<p>包含行号的ASAN错误栈会打印出来，如果看不见符号，尝试将环境变量ASAN_SYMBOLIZER_PATH设置到 llvm-symbolizer所在位置，后者将 llvm-symbolizer放入$PATH。</p>
<p>如果使用clang-5.0或更高版本构建，可以启用额外的sanitizer：</p>
<pre class="crayon-plain-tag">bazel test -c dbg --config=clang-asan //test/...
bazel test -c dbg --config=clang-tsan //test/... </pre>
<div class="blog_h2"><span class="graybg">调试</span></div>
<div class="blog_h3"><span class="graybg">栈追踪符号解析</span></div>
<p>Envoy能够根据需要生成堆栈追踪（backtraces），也可以由断言或其它失败（例如段错误）触发栈追踪。</p>
<p>在不支持absl::Symbolization的系统上，打印到日志或者标准错误的栈追踪包含地址而非符号。脚本tools/stack_decode.py可以来将这些地址转换为符号+行号。用法示例：</p>
<pre class="crayon-plain-tag">bazel test -c dbg //test/server:backtrace_test
--run_under=`pwd`/tools/stack_decode.py --strategy=TestRunner=standalone
--cache_test_results=no --test_output=all</pre>
<p>你需要使用dbg或者opt构建类型，这样才能从二进制文件中获取符号信息。</p>
<div class="blog_h3"><span class="graybg">使用GDB调试</span></div>
<p>下面的命令示意了如何在GDB之下运行单个Bazel测试：</p>
<pre class="crayon-plain-tag"># Bazel的-c dbg选项让二进制文件包含符号信息
tools/bazel-test-gdb //test/common/http:async_client_impl_test -c dbg</pre>
<p>下面的命令示意了如何测试符号表正常：</p>
<pre class="crayon-plain-tag">cd ~/CPP/projects/clion/envoy
gdbtui bazel-bin/source/exe/envoy-static</pre>
<p>如果调试符号表可用，则可以看到envoy-static的main.cc的源码。</p>
<p>你可能需要设置souce path，否则GDB找不到源码而进行反汇编：</p>
<pre class="crayon-plain-tag"># 这些外部依赖的源码路径，可能总是需要添加
(gdb) directory /home/alex/CPP/lib/libevent/2.1.8-stable

# 根据你IDE配置、调试方式的不同，可能需要添加Envoy的源码路径
(gdb) directory /home/alex/CPP/projects/clion/envoy </pre>
<div class="blog_h3"><span class="graybg">在Clion中调试</span></div>
<p>如果想Attach到现有Envoy进程，可以点选Run - Attach to Process...并选中envoy-static进程。关于Clion Attach方式调试的注意点参考：<a href="/clion-faq#debug">CLion知识集锦</a></p>
<div class="blog_h1"><span class="graybg">配置</span></div>
<p><a href="https://github.com/envoyproxy/data-plane-api/tree/master/envoy/api">Envoy v2 API</a>以Protocol Buffers的proto3语言定义，该API支持：</p>
<ol>
<li>通过gRPC进行<a href="https://github.com/envoyproxy/data-plane-api/blob/master/XDS_PROTOCOL.md">xDS</a> API更新的流式递送，这可以降低更新延迟。Envoy可以从文件系统或管理服务器动态发现资源定义，这一功能由一系列发现服务完成，这些发现服务统称为xDS</li>
<li>一个新的REST-JSON API，支持以YAML/JSON格式传递配置，底层依靠proto3 canonical JSON mapping实现</li>
<li>通过文件系统，REST-JSON或gRPC端点来递送更新</li>
<li>如果需要，可以保证更强的一致性。最终一致性总是得到保证</li>
</ol>
<p>旧版本的Envoy v1 API已经废弃，但是其功能尚未完全迁移到v2中，有些情况下，需要使用<pre class="crayon-plain-tag">deprecated_v1</pre>字段使用v1 API。</p>
<div class="blog_h2"><span class="graybg">规格总览</span></div>
<p>Envoy的配置API文档比较零散，本节使用从一个Istio proxy中dump出的Envoy完整配置来总览的了解Envoy配置规格：</p>
<pre class="crayon-plain-tag">kubectl exec -it ubuntu -c istio-proxy -- curl http://127.0.0.1:15000/config_dump &gt; /tmp/config</pre>
<p>转换为yaml格式后，内容如下：</p>
<pre class="crayon-plain-tag"># 配置主要分为以下几个段落
# configs:
#   bootstrap: 自举配置
#   clusters:  上游集群配置
#   listeners: 监听器配置
#   routes:    路由配置
configs:

  ### 自举配置 ###
  bootstrap:
    # 这个类型提示，用于辅助反串行化
    '@type': type.googleapis.com/envoy.admin.v2alpha.BootstrapConfigDump
    # 这一段是自举配置，启动时需要提供的就是这种（bootstrap的值）格式
    bootstrap:
      # 管理服务器的配置
      admin:
        # 如何输出访问日志
        access_log_path: /dev/null
        # 监听地址，默认在环回网卡上监听15000
        address:
          socket_address:
            address: 127.0.0.1
            port_value: 15000
      # 动态资源获取方式配置，在Istio环境下使用基于GRPC的xDS协议，准确的说是使用ADS
      dynamic_resources:
        ads_config:
          api_type: GRPC
          grpc_services:
          - envoy_grpc:
              # 在这里指定xDS管理服务器集群（istio-pilot服务）的名称
              cluster_name: xds-grpc
          refresh_delay: 1s
        # CDS和LDS都通过ADS获得
        cds_config:
          ads: {}
        lds_config:
          ads: {}
      # 节点信息，也就是当前Pod信息
      node:
        # Envoy版本
        build_version: 0/1.8.0-dev//RELEASE
        # 所属集群，也就是所属的K8S Service
        cluster: ubuntu
        # 对于边车模式的代理，ID格式为：
        #   sidecar~POD地址~POD名称.POD命名空间~DNS名称后缀
        id: sidecar~172.27.121.134~ubuntu.default~default.svc.k8s.gmem.cc
        # 元数据列表
        metadata:
          # 各种环境变量
          INTERCEPTION_MODE: REDIRECT
          ISTIO_PROXY_SHA: istio-proxy:930841ca88b15365737acb7eddeea6733d4f98b9
          ISTIO_PROXY_VERSION: 1.0.2
          ISTIO_VERSION: 1.0.5
          POD_NAME: ubuntu
          # POD标签
          app: ubuntu
          istio: sidecar
          # 上一次应用的POD的Spec
          kubectl.kubernetes.io/last-applied-configuration: |
            {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"labels":{"app":"ubuntu"},"name":"ubuntu","namespace":"default"},"spec":{"containers":[{"args":["-c","sleep 365d"],"command":["/bin/sh"],"image":"docker.gmem.cc/ubuntu:16.04","imagePullPolicy":"Always","name":"ubuntu"}]}}
      # 这里配置静态资源
      static_resources:
        # 上游集群列表，注意，在Istio场景下，每个服务子集，都映射为不同的集群
        clusters:
        ### 这个集群暴露Prometheus Exporter
        - connect_timeout: 0.250s
          # 集群的访问地址，从此地址解析出端点
          hosts:
          - socket_address:
              address: 127.0.0.1
              port_value: 15000
          # 集群的名称
          name: prometheus_stats
          # 其它集群属性
          # type 集群服务发现类型
          # eds_cluster_config 对于EDS集群来说，提供EDS配置
          # connect_timeout 对集群主机的新连接的默认超时
          # per_connection_buffer_limit_bytes 每个连接的读写缓冲的软限制
          # lb_policy 负载均衡类型
          # hosts 对于 STATIC, STRICT_DNS, LOGICAL_DNS类型的集群，在此指定主机列表
          # load_assignment 代替hosts
          #   cluster_name
          #   named_endpoints 从string到endpoint.Endpoint的映射，为可以在endpoints引用的端点分配名字
          #   endpoints:
          #   - locality 提示上游主机的位置
          #     lb_endpoints 属于此位置的主机列表
          #     load_balancing_weight 每priority/region/zone/sub_zone的权重值，1-128之间
          #                           一个locality的优先级，在所有相同优先级的locality中的占比
          #                           决定了它接收多少流量。仅当启用Locality weighted load balancing才有用
          #     priority 此locality的优先级，0最高。
          #              在不常见的场景下，Envoy仅仅使用最高优先级的端点。如果最高优先级的端点全部不可用/不健康
          #              才使用次高优先级的端点
          # health_checks 可选的主动健康检查配置
          # max_requests_per_connection 每个连接最大服务的请求数，设置为1相当于禁用keepalive
          # circuit_breakers 断路器配置
          # tls_context TLS上下文配置
          # common_http_protocol_options 处理HTTP请求时额外的通用选项
          # http_protocol_options 处理HTTP1请求时额外的选项
          # http2_protocol_options 
          # dns_refresh_rate DNS刷新速率
          # dns_lookup_family DNS IP地址解析策略
          # dns_resolvers 指定DNS服务器
          # outlier_detection 异常检测配置
          # cleanup_interval 对于ORIGINAL_DST类型的集群，移除陈旧主机的间隔。超过此间隔某个主机一直没有作为上游使用认为陈旧
          # upstream_bind_config 绑定新建立的上游连接时的选项
          # lb_subset_config 负载均衡子集配置
          # common_lb_config 负载均衡通用配置，各负载均衡器配置：
          #   ring_hash_lb_config original_dst_lb_config least_request_lb_config 
          # transport_socket 为上游连接指定自定义的传输套接字实现
          # 
        ### 这个集群指定xDS服务器也就是Pilot的配置信息
          # 断路器配置
        - circuit_breakers:
            # 触发断路的并发度
            thresholds:
            - max_connections: 100000
              max_pending_requests: 100000
              max_requests: 100000
            - max_connections: 100000
              max_pending_requests: 100000
              max_requests: 100000
              priority: HIGH
          # 超时配置
          connect_timeout: 10s
          # 集群的访问地址
          hosts:
          - socket_address:
              # K8S集群中的pilot服务
              address: istio-pilot.istio-system
              port_value: 15010
          http2_protocol_options: {}
          name: xds-grpc
          # 如何发现端点，STRICT_DNS表示通过解析域名来发现
          type: STRICT_DNS
          # 对上游每个端点所创建的TCP连接池配置
          upstream_connection_options:
            tcp_keepalive:
              keepalive_time: 300
        ### 这个集群指定Tracer
        - connect_timeout: 1s
          hosts:
          - socket_address:
              address: zipkin.istio-system
              port_value: 9411
          name: zipkin
          type: STRICT_DNS
        # 监听器列表，注意监听器不一定真正绑定到网络端口
        listeners:
          # 监听地址
        - address:
            socket_address:
              address: 0.0.0.0
              port_value: 15090
          # 过滤器链
          filter_chains:
          - filters:
            # TCP过滤器 envoy.http_connection_manager，内部管理多个HTTP过滤器，任何HTTP协议都使用此过滤器
            - config:
                # 传输时如何压缩
                codec_type: AUTO
                # HTTP过滤器列表，通常至少包含envoy.router，以实现流量路由
                http_filters:
                  name: envoy.router
                # 路由配置，供HTTP过滤器envoy.router使用
                route_config:
                  # 虚拟主机，根据请求的HTTP头:authority或host匹配归哪个虚拟主机处理
                  virtual_hosts:
                    # 此虚拟主机匹配所有流量，名为backend
                  - domains:
                    - '*'
                    name: backend
                    # 路由规则是将针对路径/stats/prometheus的请求转发给上面定义的prometheus_stats集群
                    routes:
                    - match:
                        prefix: /stats/prometheus
                      route:
                        cluster: prometheus_stats
                stat_prefix: stats
              name: envoy.http_connection_manager
      # 这里是全局的监控统计配置
      stats_config:
        # config.metrics.v2.TagSpecifier，用来指定抽取指标标签名称、值的规则
        #   tag_name 抽取到的标签名叫什么
        #   fixed_value 使用固定标签值
        #   regex 从原始统计指标名中抽取标签值的正则式
        # 例如，对于名为cluster.foo_cluster.upstream_rq_timeout的原始统计指标名
        # 如果指定tag_name=envoy.cluster_name，regex="^cluster\.((.+?)\.)"
        # 则子串 foo_cluster. 被捕获分组((.+?)\.)匹配，从而从抽取后的统计指标名
        # 中移除，导致抽取后统计指标名变为 cluster.upstream_rq_timeout
        # 内部捕获分组 (.+?) 将作为抽取得到的标签envoy.cluster_name的值
        # 因此最终统计指标变为 cluster.upstream_rq_timeout { tag_name="foo_cluster"}
        stats_tags:
        # 抽取cluster_name、tcp_prefix、response_code、response_code_class
        # http_conn_manager_listener_prefix、http_conn_manager_prefix、listener_address
        - regex: ^cluster\.((.+?(\..+?\.svc\.cluster\.local)?)\.)
          tag_name: cluster_name
        - regex: ^tcp\.((.*?)\.)\w+?$
          tag_name: tcp_prefix
        - regex: _rq(_(\d{3}))$
          tag_name: response_code
        - regex: _rq(_(\dxx))$
          tag_name: response_code_class
        - regex: ^listener(?=\.).*?\.http\.(((?:[_.[:digit:]]*|[_\[\]aAbBcCdDeEfF[:digit:]]*))\.)
          tag_name: http_conn_manager_listener_prefix
        - regex: ^http\.(((?:[_.[:digit:]]*|[_\[\]aAbBcCdDeEfF[:digit:]]*))\.)
          tag_name: http_conn_manager_prefix
        - regex: ^listener\.(((?:[_.[:digit:]]*|[_\[\]aAbBcCdDeEfF[:digit:]]*))\.)
          tag_name: listener_address
        # 是否使用Envoy默认的基于正则式的tag抽取规则
        use_all_default_tags: false
      # 这里是全局调用链跟踪配置
      tracing:
        http:
          config:
            # 将追踪信息发送到哪里
            collector_cluster: zipkin
          name: envoy.zipkin
    last_updated: "2019-02-14T10:35:05.077Z"

  ### 集群配置 ###
  clusters:
    '@type': type.googleapis.com/envoy.admin.v2alpha.ClustersConfigDump
    # 动态获取的、活动的集群列表
    dynamic_active_clusters:
    # 黑洞集群，处理无匹配监听器的连接
    - cluster:
        connect_timeout: 1s
        name: BlackHoleCluster
    # 此集群可以用于获取代理状态
    - cluster:
        circuit_breakers:
          thresholds:
          - {}
        connect_timeout: 1s
        hosts:
        - socket_address:
            address: 127.0.0.1
            port_value: 15020
        # 注意命名方式
        #     供入站流量使用
        #             端口
        #                  子集
        #                    域名
        name: inbound|15020||mgmtCluster
    # 此集群由于Pod属于一个K8S服务而创建，用于处理入站8086请求
    - cluster:
        circuit_breakers:
          thresholds:
          - {}
        connect_timeout: 1s
        hosts:
        - socket_address:
            address: 127.0.0.1
            port_value: 8086
        name: inbound|8086||ubuntu.default.svc.k8s.gmem.cc
    # 下面这个集群是出站集群，用于服务本Pod对外的请求
    - cluster:
        circuit_breakers:
          thresholds:
          - {}
        connect_timeout: 1s
        # 此集群的端点如何获取（EDS）
        eds_cluster_config:
          eds_config:
            # 通过ADS获取端点
            ads: {}
          # 可以指定一个和集群名称不一样的名字给EDS服务器
          service_name: outbound|14267||jaeger-collector.istio-system.svc.k8s.gmem.cc
        # 集群名称
        name: outbound|14267||jaeger-collector.istio-system.svc.k8s.gmem.cc
        # 端点发现类型Cluster.DiscoveryType，取值：
        # STATIC 静态发现
        # STRICT_DNS 严格DNS发现
        # LOGICAL_DNS 逻辑DNS发现
        # EDS 使用EDS协议发现
        # ORIGINAL_DST 总是使用原始请求指定的目的地
        type: EDS


  ### 监听器配置 ###
  listeners:
    '@type': type.googleapis.com/envoy.admin.v2alpha.ListenersConfigDump
    # 动态获取的、活动的监听器列表
    dynamic_active_listeners:
    - last_updated: "2019-02-14T10:35:10.423Z"
      # 监听器 172.27.121.134_8086，在Pod IP上监听的，一般都是入站监听器
      listener:
        # 此监听器的监听地址
        address:
          socket_address:
            address: 172.27.121.134
            port_value: 8086
        # 指定Envoy v1 API属性
        deprecated_v1:
          # 不绑定到端口，表示此监听器不创建监听套接字，因此只能接收其它监听器转发来的连接请求
          bind_to_port: false
        # 过滤器链列表
        filter_chains:
          # 此过滤器链，普通HTTP请求匹配
        - filter_chain_match:
            # destination_port: 当监听器的use_original_dst=true，则当目的地端口是该字段指定的值时，匹配
            # prefix_ranges: 当监听器的use_original_dst=true，或者监听器绑定到0.0.0.0:***时
            #                什么样的目的地IP范围匹配此链，core.CidrRange形式
            # source_type: 根据来源匹配，ANY|LOCAL|EXTERNAL，LOCAL表示连接来自当前主机
            # server_names: 根据服务器名称（例如TLS协议的SNI）进行匹配，字符串列表
            # transport_protocol: 根据连接的传输协议匹配
            #                     raw_buffer 默认值，如果没有检测到传输协议时匹配
            #                     tls 如果检测到TLS协议， envoy.listener.tls_inspector 设置为该值
            # application_protocols: 根据连接的应用层协议匹配
            #
            transport_protocol: raw_buffer
          # 此链的过滤器列表
          filters:
          # 唯一的过滤器envoy.http_connection_manager
          - config:
              # 访问日志格式和位置
              access_log:
              - config:
                  format: |
                    [%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%%PROTOCOL%" %RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% "%REQ(X-FORWARDED-FOR)%" "%REQ(USER-AGENT)%" "%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%" %UPSTREAM_CLUSTER% %UPSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_REMOTE_ADDRESS%
                  path: /dev/stdout
                name: envoy.file_access_log
              # 是否为请求生成request_id
              generate_request_id: true
              # HTTP过滤器列表
              http_filters:
                # 部分过滤器是在https://github.com/istio/proxy上开发的
                # istio_authn过滤器
              - config:
                  policy:
                    peers:
                    - mtls:
                        mode: PERMISSIVE
                name: istio_authn
                # mixer过滤器
              - config:
                  default_destination_service: default
                  # 为Mixer提供Attributes
                  mixer_attributes:
                    attributes:
                      context.reporter.kind:
                        string_value: inbound
                      context.reporter.uid:
                        string_value: kubernetes://ubuntu.default
                      destination.ip:
                        bytes_value: AAAAAAAAAAAAAP//rBt5hg==
                      destination.namespace:
                        string_value: default
                      destination.port:
                        int64_value: "8086"
                      destination.uid:
                        string_value: kubernetes://ubuntu.default
                  service_configs:
                    default: {}
                  transport:
                    attributes_for_mixer_proxy:
                      attributes:
                        source.uid:
                          string_value: kubernetes://ubuntu.default
                    check_cluster: outbound|9091||istio-policy.istio-system.svc.k8s.gmem.cc
                    network_fail_policy:
                      policy: FAIL_CLOSE
                    report_cluster: outbound|9091||istio-telemetry.istio-system.svc.k8s.gmem.cc
                name: mixer
              # 其它HTTP过滤器
              - name: envoy.cors
              - name: envoy.fault
              # 这个过滤器总是有，用于支持路由
              - name: envoy.router
              # 路由配置
              route_config:
                name: inbound|8086||ubuntu.default.svc.k8s.gmem.cc
                # 集群管理器是否应该验证此路由表所引用的集群
                # 引用不存在集群时，如果此字段设置为：
                #   true：路由表不被加载
                #   false：路由表加载，但是如果选路到此表，总是返回404
                validate_clusters: false
                # 虚拟主机列表
                virtual_hosts:
                  # 匹配所有域名
                - domains:
                  - '*'
                  # 此虚拟主机的逻辑名称
                  name: inbound|http|8086
                  # 逐条匹配下面的路由，找到匹配请求的立即使用
                  routes:
                    # 路由装饰器
                  - decorator:
                      # 关联到匹配此路由的请求的operation name，在调用链追踪场景下
                      # 此字段用作span name
                      operation: ubuntu.default.svc.k8s.gmem.cc:8086/*
                    # 什么情况下匹配此路由 route.RouteMatch
                    # prefix: 根据路径（:path头）前缀匹配
                    # path: 精确匹配路径
                    # regex: 正则式匹配路径
                    # case_sensitive: false 指定匹配时是否大小写敏感
                    # headers: 根据请求头匹配
                    # query_parameters: 根据查询参数匹配
                    # grpc: 如果存在此字段，仅仅匹配gRPC请求  route.RouteMatch.GrpcRouteMatchOptions
                    match:
                      prefix: /
                    # 覆盖过滤器配置
                    per_filter_config:
                      mixer:
                        mixer_attributes:
                          attributes:
                            destination.service:
                              string_value: ubuntu.default.svc.k8s.gmem.cc
                            destination.service.host:
                              string_value: ubuntu.default.svc.k8s.gmem.cc
                            destination.service.name:
                              string_value: ubuntu
                            destination.service.namespace:
                              string_value: default
                            destination.service.uid:
                              string_value: istio://default/services/ubuntu
                    # route.RouteAction，如何转发请求
                    route:
                      # cluster: 发往的上游集群名称
                      # cluster_header: 该字段的值为一请求头名称，从此请求头读取上游集群名称
                      # weighted_clusters: 为单个路由指定多重、权重化的上游集群route.WeightedCluster
                      # cluster_not_found_response_code: 如果上游集群不存在，返回什么错误码，默认503
                      # metadata_match: 仅上游集群中元数据匹配的端点有资格参与负载均衡
                      # prefix_rewrite: 改写URL前缀
                      # host_rewrite: 改写host头为此值
                      # auto_host_rewrite: 自动改写host头，不能和host_rewrite同时指定
                      # timeout: 访问上游集群的超时，默认15s
                      # idle_timeout: 连接超时
                      # retry_policy: 重试策略
                      # request_mirror_policy: 镜像策略
                      # priority: 路由优先级
                      # rate_limits: 限速配置
                      # include_vh_rate_limits: 虚拟主机的限速配置是否忽略，默认忽略
                      # hash_policy: 环哈希负载均衡下使用的哈希策略
                      # cors: CORS策略
                      # max_grpc_timeout: 最大gRPC超ishi
                      cluster: inbound|8086||ubuntu.default.svc.k8s.gmem.cc
                      max_grpc_timeout: 0.000s
                      timeout: 0.000s
              # http_connection_manager的统计指标前缀
              stat_prefix: 172.27.121.134_8086
              # 此HTTP连接管理器管理的连接，其上的流的空闲超时，默认5m
              stream_idle_timeout: 0.000s
              # 调用链追踪配置
              tracing:
                client_sampling:
                  value: 100
                overall_sampling:
                  value: 100
                random_sampling:
                  value: 1
              # 为每种Upgrade类型配置
              upgrade_configs:
              - upgrade_type: websocket
            name: envoy.http_connection_manager
          # 此过滤器链，TLS请求匹配
        - filter_chain_match:
            transport_protocol: tls
          # 其它配置和普通HTTP的过滤器链一致，但是多了tls_context配置
          # TLS上下文配置，例如数字证书信息、是否要求客户端证书
          tls_context:
            common_tls_context:
              alpn_protocols:
              - h2
              - http/1.1
              tls_certificates:
              - certificate_chain:
                  filename: /etc/certs/cert-chain.pem
                private_key:
                  filename: /etc/certs/key.pem
              validation_context:
                trusted_ca:
                  filename: /etc/certs/root-cert.pem
            require_client_certificate: true
        # 此监听器的监听器过滤器
        listener_filters:
        - config: {}
          # 探测连接是基于TLS还是明文的，并导致选择适当的过滤器链
          name: envoy.listener.tls_inspector
        # 监听器名称
        name: 172.27.121.134_8086
    - last_updated: "2019-02-14T10:35:10.491Z"
      # 监听器 0.0.0.0_8086，针对任何地址的8086端口的请求都走这个监听器
      listener:
        address:
          socket_address:
            address: 0.0.0.0
            port_value: 8086
        deprecated_v1:
          bind_to_port: false
        filter_chains:
        - filters:
          - config:
              http_filters:
              - config:
                  default_destination_service: default
                  forward_attributes:
                  mixer_attributes:
                    attributes:
                      context.reporter.kind:
                        string_value: outbound
                name: mixer
              - name: envoy.cors
              - name: envoy.fault
              - name: envoy.router
              # 和入站监听器不同，没有使用route_config静态指定路由表
              # 而是通过RDS，查询名为8086的路由表
              rds:
                config_source:
                  ads: {}
                route_config_name: "8086"
              stat_prefix: 0.0.0.0_8086
              tracing:
                operation_name: EGRESS
            name: envoy.http_connection_manager
        name: 0.0.0.0_8086
    - last_updated: "2019-02-14T10:35:10.428Z"
      # 上面两个都是基于HTTP协议的，下面这个是TCP协议的出站监听器
      listener:
        address:
          socket_address:
            # 对于TCP协议，每个IP+端口组合都需要独立的监听器
            address: 10.96.0.10
            port_value: 53
        deprecated_v1:
          bind_to_port: false
        filter_chains:
          - filters:
              - config:
                name: mixer
              - config:
                  # 简单的TCP代理
                  # cluster 连接发往的上游集群名称
                  # weighted_clusters 带权重的上游集群配置
                  # access_log 访问日志配置
                  # metadata_match 只有元数据匹配的端点有资格参与负载均衡
                  cluster: outbound|53||kube-dns.kube-system.svc.k8s.gmem.cc
                name: envoy.tcp_proxy
        name: 10.96.0.10_53
    - last_updated: "2019-02-14T10:35:10.491Z"
      # 下面这是一个特殊的监听器，它的名字叫virtual，真正绑定并监听端口
      listener:
        address:
          socket_address:
            address: 0.0.0.0
            port_value: 15001
        filter_chains:
        - filters:
          - config:
              # 直接发给黑洞
              cluster: BlackHoleCluster
            name: envoy.tcp_proxy
        name: virtual
        # 转发连接给虚拟监听器处理时，依据原始目的地址判断目标监听器
        use_original_dst: true
    # 静态配置的监听器
    static_listeners:
    - last_updated: "2019-02-14T10:35:05.082Z"
      # 来自Bootstrap配置
      listener:
        address:
          socket_address:
            address: 0.0.0.0
            port_value: 15090
  routes:
    '@type': type.googleapis.com/envoy.admin.v2alpha.RoutesConfigDump
    # 动态获取的路由配置
    dynamic_route_configs:
    - last_updated: "2019-02-15T02:53:56.778Z"
      route_config:
        # 引用此路由配置所需要的名称
        name: "8086"
        validate_clusters: false
        virtual_hosts:
        - domains:
          # 虚拟主机匹配的域名
          - ubuntu.default.svc.k8s.gmem.cc
          - ubuntu.default.svc.k8s.gmem.cc:8086
          - ubuntu
          - ubuntu:8086
          - ubuntu.default.svc.k8s.gmem
          - ubuntu.default.svc.k8s.gmem:8086
          - ubuntu.default.svc.k8s
          - ubuntu.default.svc.k8s:8086
          - ubuntu.default.svc
          - ubuntu.default.svc:8086
          - ubuntu.default
          - ubuntu.default:8086
          - 10.100.207.211
          - 10.100.207.211:8086
          name: ubuntu.default.svc.k8s.gmem.cc:8086
          # 虚拟主机的路由表
          routes:
            # 匹配规则
          - match:
              prefix: /
            # 为每个过滤器覆盖配置
            per_filter_config:
              mixer:
                disable_check_calls: true
            # 路由目的地
            route:
              # 上游集群名称
              cluster: outbound|8086||ubuntu.default.svc.k8s.gmem.cc
              max_grpc_timeout: 0s
              timeout: 0s
    # 静态配置的路由
    static_route_configs:
    - last_updated: "2019-02-14T10:35:05.081Z"
      # 来自Bootstrap配置
      route_config:
        virtual_hosts:
        - domains:
          - '*'
          name: backend
          routes:
          - match:
              prefix: /stats/prometheus
            route:
              cluster: prometheus_stats</pre>
<div class="blog_h2"><span class="graybg">配置实例</span></div>
<div class="blog_h3"><span class="graybg">简单TCP代理</span></div>
<pre class="crayon-plain-tag">static_resources:
  listeners:
    name: listener_0
    address:
      socket_address:
        address: 127.0.0.1
        port_value: 15001
    filter_chains:
    - filters:
      - name: echo</pre>
<div class="blog_h3"><span class="graybg">简单HTTP代理</span></div>
<pre class="crayon-plain-tag">static_resources:
  listeners:
    name: listener_0
    address:
      socket_address:
        address: 127.0.0.1
        port_value: 15001
    filter_chains:
      - filters:
          - name: envoy.http_connection_manager
            config:
              stat_prefix: sample
              route_config:
                name: gmem
                virtual_hosts:
                  - name: gmem
                    domains: ["*"]
                    routes:
                      - match:
                          prefix: "/"
                        route:
                          cluster: gmem
              http_filters:
                - name: sample
                  config:
                    key: via
                    val: sample-filter
                - name: envoy.router
  clusters:
    - name: gmem
      connect_timeout: 1s
      type: STRICT_DNS
      dns_lookup_family: V4_ONLY
      lb_policy: ROUND_ROBIN
      hosts:
        - socket_address:
            address: gmem.cc
            port_value: 80</pre>
<div class="blog_h3"><span class="graybg">HTTPS透传</span></div>
<p>Envoy同时支持<span style="background-color: #c0c0c0;">TLS Termination（接受下游HTTPS请求）</span>和<span style="background-color: #c0c0c0;">TLS Origination（向上游发送HTTPS请求）</span>：</p>
<pre class="crayon-plain-tag">static_resources:
  clusters:
  - name: zircon
    type: static
    hosts:
    - socket_address:
        address: 10.0.11.10
        port_value: 443
    # 必须配置此字段，否则会发送HTTP流量给上游
    tls_context:
      # 这个字段不填写也可以
      sni: kiali.k8s.gmem.cc
    connect_timeout: 0.25s
  listeners:
    name: gmem-tls
    address:
      socket_address:
        address: 127.0.0.1
        port_value: 4433
    listener_filters:
    # 必须配置监听器过滤器tls_inspector
    - name: envoy.listener.tls_inspector        
    filter_chains:
    - filter_chain_match:
        # 才能匹配到这种传输协议
        transport_protocol: tls
      # 指定此Envoy的HTTPS服务器端配置
      tls_context:
        common_tls_context:
          alpn_protocols:
          - h2
          - http/1.1
          # 服务器端证书，一个类型的证书只能指定一个
          tls_certificates:
          - certificate_chain:
              # 需要提供完整的证书链
              filename: /etc/letsencrypt/live/kiali.k8s.gmem.cc/fullchain.pem
            private_key:
              filename: /etc/letsencrypt/live/kiali.k8s.gmem.cc/privkey.pem
        require_client_certificate: false      
      filters:
      - name: envoy.http_connection_manager
        config:
          stat_prefix: ingress_gmem
          http_filters:
          - name: envoy.router
          route_config:
            name: inbound|443|gmem.cc
            virtual_hosts:
            - name: zircon
              # 端口、域名部分的通配，只能支持一个
              # 因为Envoy的Router过滤器支持域名的前缀、后缀通配，但是不能同时支持
              domains: [ "kiali.k8s.gmem.cc:*" ]
              routes:
              - route:
                  cluster: zircon
                match:
                  prefix: "/"</pre>
<div class="blog_h2"><span class="graybg">简介</span></div>
<div class="blog_h3"><span class="graybg">自举配置</span></div>
<p>要使用v2 API，Envoy实例需要一个Bootstrap配置信息。Bootstrap配置中可以包含静态的服务器信息，告诉Envoy如何访问动态配置。自举配置通过-c传入：</p>
<pre class="crayon-plain-tag">./envoy -c &lt;path to config&gt;.{json,yaml,pb,pb_text}</pre>
<p>配置的根是一个Bootstrap消息，该消息的一个核心特点是将静态资源（static_resources）和动态资源分离（dynamic_resources）</p>
<div class="blog_h3"><span class="graybg">静态配置示例</span></div>
<p><a href="#first-example">第一个例子</a>就是最简单的全静态配置。 </p>
<p>下面的示例，在clusters配置中使用了EDS，以动态的发现上游集群包含哪些端点：</p>
<pre class="crayon-plain-tag">static_resources:
  clusters:
  - name: some_service
    connect_timeout: 0.25s
    lb_policy: ROUND_ROBIN
    # 集群类型为EDS
    type: EDS
    eds_cluster_config:
      eds_config:
        api_config_source:
          api_type: GRPC
          grpc_services:
            envoy_grpc:
              # 引用EDS服务集群
              cluster_name: xds_cluster
  # 静态配置的EDS集群
  - name: xds_cluster
    connect_timeout: 0.25s
    type: STATIC
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: xds_cluster
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 5678</pre>
<p>EDS管理服务器可能会提供如下的DiscoveryResponse：</p>
<pre class="crayon-plain-tag">version_info: "0"
resources:
- "@type": type.googleapis.com/envoy.api.v2.ClusterLoadAssignment
  cluster_name: some_service
  endpoints:
  - lb_endpoints:
    - endpoint:
        address:
          socket_address:
            address: 127.0.0.2
            port_value: 1234</pre>
<p>从而为some_service提供端点。</p>
<div class="blog_h3"><span class="graybg">动态配置示例</span></div>
<p>下面是一个完全动态的自举配置，除了和管理服务器相关的信息之外，都是通过xDS发现：</p>
<pre class="crayon-plain-tag"># 动态资源
dynamic_resources:
  # 动态发现监听器
  lds_config:
    api_config_source:
      api_type: GRPC
      grpc_services:
        envoy_grpc:
          cluster_name: xds_cluster
  # 动态发现集群
  cds_config:
    api_config_source:
      api_type: GRPC
      grpc_services:
        envoy_grpc:
          cluster_name: xds_cluster

# 静态的xDS服务器集群配置
static_resources:
  clusters:
  - name: xds_cluster
    connect_timeout: 0.25s
    type: STATIC
    lb_policy: ROUND_ROBIN
    http2_protocol_options: {}
    load_assignment:
      cluster_name: xds_cluster
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 5678</pre>
<p>xDS管理服务器可能这样应答一个LDS请求：</p>
<pre class="crayon-plain-tag">version_info: "0"
resources:
- "@type": type.googleapis.com/envoy.api.v2.Listener
  name: listener_0
  address: ...
  filter_chains: ...</pre>
<p>这样应答一个RDS请求：</p>
<pre class="crayon-plain-tag">version_info: "0"
resources:
- "@type": type.googleapis.com/envoy.api.v2.RouteConfiguration
  name: local_route
  virtual_hosts:
  - name: local_service
    domains: ["*"]
    routes:
    - match: { prefix: "/" }
      route: { cluster: some_service }</pre>
<p>这样应答一个CDS请求：</p>
<pre class="crayon-plain-tag">version_info: "0"
resources:
- "@type": type.googleapis.com/envoy.api.v2.Cluster
  name: some_service
  connect_timeout: 0.25s
  lb_policy: ROUND_ROBIN
  type: EDS
  eds_cluster_config:
    eds_config:
      api_config_source:
        api_type: GRPC
        grpc_services:
          envoy_grpc:
            # 集群的端点也依靠xDS发现
            cluster_name: xds_cluster</pre>
<p>这样应答一个EDS请求：</p>
<pre class="crayon-plain-tag">version_info: "0"
resources:
- "@type": type.googleapis.com/envoy.api.v2.ClusterLoadAssignment
  cluster_name: some_service
  endpoints:
  - lb_endpoints:
    - endpoint:
        address:
          socket_address:
            address: 127.0.0.2
            port_value: 1234</pre>
<div class="blog_h3"><span class="graybg">管理服务器</span></div>
<p>一个v2 xDS管理服务器需要实现若干gRPC/REST端点，请求总是以 DiscoveryRequest发送，应答则以遵守xDS协议的DiscoveryResponse发送：</p>
<p>gRPC端点如下表：</p>
<table class="full-width fixed-word-wrap">
<tbody>
<tr>
<td>
<p>POST /envoy.api.v2.ClusterDiscoveryService/StreamClusters，服务定义参考<a href="https://github.com/envoyproxy/data-plane-api/blob/master/envoy/api/v2/cds.proto">cds.proto</a></p>
<p>如果在Envoy的自举配置的dynamic_resources段包含以下形式的配置，Envoy调用此端点：</p>
<pre class="crayon-plain-tag">cds_config:
  api_config_source:
    api_type: GRPC
    grpc_services:
      envoy_grpc:
        cluster_name: some_xds_cluster</pre>
</td>
</tr>
<tr>
<td>
<p>POST /envoy.api.v2.EndpointDiscoveryService/StreamEndpoints ，服务定义参考<a href="https://github.com/envoyproxy/data-plane-api/blob/master/envoy/api/v2/eds.proto">eds.proto</a>
<p>如果在Envoy的Cluster配置的eds_cluster_config段包含以下形式的配置，Envoy调用此端点：</p>
<pre class="crayon-plain-tag">eds_config:
  api_config_source:
    api_type: GRPC
    grpc_services:
      envoy_grpc:
        cluster_name: some_xds_cluster</pre>
</td>
</tr>
<tr>
<td>
<p>POST /envoy.api.v2.ListenerDiscoveryService/StreamListeners，服务定义参考<a href="https://github.com/envoyproxy/data-plane-api/blob/master/envoy/api/v2/lds.proto">lds.proto</a>
<p>如果在Envoy的自举配置的dynamic_resources段包含以下形式的配置，Envoy调用此端点：</p>
<pre class="crayon-plain-tag">lds_config:
  api_config_source:
    api_type: GRPC
    grpc_services:
      envoy_grpc:
        cluster_name: some_xds_cluster</pre>
</td>
</tr>
<tr>
<td>
<p>POST /envoy.api.v2.RouteDiscoveryService/StreamRoutes，服务定义参考<a href="https://github.com/envoyproxy/data-plane-api/blob/master/envoy/api/v2/rds.proto">rds.proto</a>
<p>如果在Envoy的HttpConnectionManager配置的rds段包含以下形式的配置，Envoy调用此端点：</p>
<pre class="crayon-plain-tag">route_config_name: some_route_name
config_source:
  api_config_source:
    api_type: GRPC
    grpc_services:
      envoy_grpc:
        cluster_name: some_xds_cluster </pre>
</td>
</tr>
</tbody>
</table>
<p>REST端点包括：
<ol>
<li>POST /v2/discovery:clusters</li>
<li>POST /v2/discovery:endpoints</li>
<li>POST /v2/discovery:endpoints</li>
<li>POST /v2/discovery:routes</li>
</ol>
<div class="blog_h3"><span class="graybg">聚合发现服务ADS</span></div>
<p>Envoy使用了最终一致性模型，这意味着存在配置不一致的时间窗口。ADS能够将Envoy节点绑定到单个管理服务器，并且串行化API更新推送。</p>
<div class="blog_h2"><span class="graybg">监听器</span></div>
<p>监听器包含的属性如下：</p>
<pre class="crayon-plain-tag"># 监听器唯一的名称，如果不指定自动生成UUID。如果此监听器通过LDS发现，则必须手工通过唯一性名称
name: "..."
# 监听定制
address: "{...}"
# 过滤器链
filter_chains: 
# 如果连接是通过iptables重定向来的，则Envoy接收请求的端口，可能和原始的Dest地址的端口不同
# 默认情况下Envoy在virtual(绑定端口15001)
# 这个监听器接收所有iptables重定向来的请求，原始目的端口
# 可能是任何值，booinfo的例子中，原始目的地一般是***:9080
# 
# 如果此属性设置为true，则当前监听器将重定向的来请求转发（这个转发不需要走网络栈，在Envoy内部就完成）
# 给和原始Dest地址匹配的那个监听器处理，booinfo的例子中就是名为***:9080的那个监听器
# 
# 如果没有匹配的监听器，则接收请求（当前）监听器负责处理请求。如果当前监听器是virtual则由转发给BlackHoleCluster
# 
# 此属性已经废弃，用监听器过滤器use_original代替
use_original_dst: "{...}"
# 每个连接的读写缓冲的软限制
per_connection_buffer_limit_bytes: "{...}"
# 监听器元数据
metadata: "{...}"
# 在监听器级别的Draining操作如何进行
# DEFAULT
# MODIFY_ONLY
drain_type: "..."
# 监听器过滤器列表，监听器过滤器可以操控、增强连接的元数据，这些元数据可能被连接过滤器链使用
listener_filters: 
# 监听器过滤器的超时，默认15秒，0表示永不超时
# 如果超时后，监听器过滤器链还没完成处理，则套接字被关闭，连接不会创建
listener_filters_timeout: "{...}"
# 此监听器是否应该被设置为透明套接字
# 如果为true，连接可以通过iptables TPROXY 目标重定向到此监听器的端口，
# 原始的Src/Dest地址会被保留在创建的连接上
# 此标记应该和监听器过滤器original_dst联用，以标记连接的本地地址为“restored”
# 可用于将重定向到本监听器的连接转发给和原始Dest关联的其它监听器处理
# 直接连接到本监听器端口的连接，和通过TPROXY转发的连接是无法区分的，这意味着前者被当作后者处理
# 当此标记设置为false时，监听器的套接字被明确的重置为非透明的
# 设置此标记要求Envoy以CAP_NET_ADMIN权限运行
# 如果此标记为空（默认值）则套接字不被修改，其transparent选项不是set也不是reset
transparent: "{...}"
# 监听器是否设置套接字选项IP_FREEBIND。如果true，则监听器可以绑定到本机没有的IP地址上
# 如果不设置此字段，则套接字不会改变，而不是禁止FREEBIND
freebind: "{...}"
# 额外的套接字选项
socket_options:
# 是否接受TCP Fast Open（TFO）连接，如果设置为大于0的值，则启用TCP_FASTOPEN套接字选项
# 在Linux上内核参数net.ipv4.tcp_fastopen必须包含标记0x2才能启用TCP_FASTOPEN 
tcp_fast_open_queue_length: "{...}"
deprecated_v1: 
  # 此监听器是否需要绑定到（监听）端口，默认true
  # 如果false，则监听器仅仅能够接受从其它use_original_dst=true的监听器重定向来的连接
  bind_to_port: false</pre>
<div class="blog_h2"><span class="graybg">监听器过滤器</span></div>
<p>包含的属性如下：</p>
<pre class="crayon-plain-tag"># 监听器名称
name: "..."
# 监听器配置，config或者typed_config二选一
config: "{...}"
typed_config: "{...}"</pre>
<div class="blog_h3"><span class="graybg">original_dst </span></div>
<p>过滤器名称envoy.listener.original_dst。当一个连接通过iptables REDIRECT（或TPROXY+监听器的transparent配置）重定向到监听器时，此过滤器读取套接字选项SO_ORIGINAL_DST。Envoy的后续处理过程中看到的连接本地地址（connection’s local，也就是服务器端地址）是原始的目的地址，而不是监听器正在监听的地址。</p>
<p>此外，基于<a href="https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/service_discovery#arch-overview-service-discovery-types-original-destination">原始目的地类型的上游集群</a>，可以作为转发目标，也就是说Envoy直接把HTTP/TCP发送给原始的目的地址。</p>
<div class="blog_h3"><span class="graybg"><a id="original_src"></a>original_src</span></div>
<p>过滤器名称envoy.listener.original_src。</p>
<p>此过滤器能够复制下游连接的远程地址，并将上游连接的源地址设置为此原始的源地址，而不是默认的使用Envoy代理自身的地址。例如，如果下游连接到Envoy时使用的IP地址时10.1.2.3，则Envoy连接上游的时候，使用10.1.2.3，而非Envoy本机IP地址。</p>
<p>如果下游连接的源地址没有被代理或转换，可以简单的获得原始源地址。<span style="background-color: #c0c0c0;">如果使用的代理协议，则原始源地址可以通过proxy_protocol过滤器获取</span>。</p>
<p>此过滤器支持IPv4和IPv6。</p>
<p>下游的远程地址常常是全局可路由的，默认情况下，<span style="background-color: #c0c0c0;">从上游返回的包不会经过Envoy路由</span>，你必须进行网络配置，将任何Envoy复制了（作为自己的源地址发往上游）的那些下游IP的路由强制的定向给Envoy。</p>
<p>如果Envoy和上游在同一主机（Sidecar模式），则通过配置Iptables和路由策略即可保证正确行为。此过滤器包含配置项mark：</p>
<ol>
<li>如果将其设置为X，则Envoy会将所有<span style="background-color: #c0c0c0;">从当前监听器发起的对上游的流量打上标记X</span></li>
<li>如果将其设置为0，则不会打标记</li>
</ol>
<p>假设mark=1986，则配合下面的规则即可正确的路由回来：</p>
<pre class="crayon-plain-tag"># MARK标记用于将特定的数据包打上标签，供iptables配合TC做QOS流量限制或应用策略路由

# 匹配带有标记1986的数据包，并将标记存储到连接中
iptables  -t mangle -I PREROUTING -m mark     --mark 1986 -j CONNMARK --save-mark
# 匹配连接被标记为1986的连接，并且将标记设置到数据包中
iptables  -t mangle -I OUTPUT     -m connmark --mark 1986 -j CONNMARK --restore-mark

# 对于具有1986标记的数据报，到912表查路由
ip rule add fwmark 1986 lookup 912
# 对于这种数据报，直接路由给本地环回网卡
ip route add local 0.0.0.0/0 dev lo table 912

# 允许eth0转发目的或源地址为127.0.0.0/8的数据包，也就是来自或去往lo设备的数据包
# 这样，你就可以使用NAT强制将目的地址是127.0.0.1的封包
#    iptables -t nat -A OUTPUT -d 127.0.0.1 -p tcp -m tcp --dport 22 
#                的目的地址改为其它值
#        -j SNAT --to-destination 16.186.74.32
echo 1 &gt; /proc/sys/net/ipv4/conf/eth0/route_localnet</pre>
<p>Envoy配置示例：</p>
<pre class="crayon-plain-tag">listeners:
- address:
    socket_address:
      address: 0.0.0.0
      port_value: 8888
  listener_filters:
    - name: envoy.listener.proxy_protocol
    - name: envoy.listener.original_src
      config:
        mark: 1986 </pre>
<p>回顾一下工作原理：</p>
<ol>
<li>Envoy接收到下游连接</li>
<li>得到下游连接的IP地址，然后以此地址作为源地址，标记封包为1986</li>
<li>由于封包具有1987标记，因此到912表查路由，结果发给lo</li>
<li>lo网卡接收到上述封包，在PREROUTING时发现有标记1986，将当前连接标记为1986</li>
<li>路由给本地的上游服务进程</li>
<li>进程处理完毕请求后，在OUTPUT时，将封包标记为1986，注意这里的连接同一个，因此可以标记封包</li>
<li>由于封包具有1987标记，因此到912表查路由，还是发给lo</li>
<li>由于目的地址是最初下游连接的地址，因此封包发给Envoy</li>
</ol>
<div class="blog_h3"><span class="graybg">proxy_protocol</span></div>
<p>过滤器名称envoy.listener.proxy_protocol，用于支持<a href="http://www.haproxy.org/download/1.9/doc/proxy-protocol.txt">HAProxy代理协议</a>。</p>
<p>启用此过滤器后，连接被假设来自一个代理，此代理将原始的源地址（IP+PORT）存放在一个连接字符串中，Envoy会抽取此字符串，并将其设置为远程地址。</p>
<p>此协议自动识别HAProxy代理协议的v1/v2版本。</p>
<div class="blog_h3"><span class="graybg">tls_inspector</span></div>
<p>过滤器名称envoy.listener.tls_inspector，用于探测连接是基于TLS还是明文的。如果是基于TLS，则此过滤器会：</p>
<ol>
<li>检测SNI（Server Name Indication，服务器名称指示）</li>
<li>检测应用层协议协商（ Application-Layer Protocol Negotiation）</li>
</ol>
<p>利用这些信息，可以FilterChainMatch的server_names、application_protocols来选择一个FilterChain</p>
<div class="blog_h2"><span class="graybg">网络过滤器</span></div>
<p>这里讨论除了HTTP连接管理器之外的网络过滤器。</p>
<div class="blog_h3"><span class="graybg">client_ssl_auth</span></div>
<p>过滤器名称envoy.client_ssl_auth，用于执行TLS身份验证。</p>
<p>此过滤器每个刷新间隔都调用REST API：GET /v1/certs/list/approved，以获取被许可的证书/主体列表。</p>
<p>包含的属性如下：</p>
<pre class="crayon-plain-tag"># 运行身份验证服务的集群名称
auth_api_cluster: "..."
# 统计信息前缀
stat_prefix: "..."
# Principal刷新间隔，没人60000，也就是60秒
refresh_delay: "{...}"
# IP白名单列表
ip_white_list: 
- address_prefix:
  prefix_len:</pre>
<div class="blog_h3"><span class="graybg">echo</span></div>
<p>过滤器名称envoy.echo，仅仅用于示例网络过滤器API的用法。它会回响所有数据包给下游服务器。</p>
<div class="blog_h3"><span class="graybg">ext_authz</span></div>
<p>过滤器名称envoy.ext_authz，调用外部的授权服务，验证入站请求是否有权限访问。如果无权访问则连接会被关闭。发送给外部服务的请求格式由<a href="https://www.envoyproxy.io/docs/envoy/latest/api-v2/service/auth/v2alpha/external_auth.proto#envoy-api-msg-service-auth-v2alpha-checkrequest">CheckRequest</a>提供。</p>
<p>建议把此过滤器配置在过滤器链的头部，这样非法请求不会执行其它过滤器。</p>
<p>包含的属性如下：</p>
<pre class="crayon-plain-tag"># 统计指标前缀
stat_prefix: "..."
# 外部的gRPC授权服务配置，没人超时200ms
grpc_service: "{...}"
# 如果外部授权服务不能响应，是否允许请求授权通过，没人false
failure_mode_allow: "..."</pre>
<p>配置示例：</p>
<pre class="crayon-plain-tag">filters:
  - name: envoy.ext_authz
    stat_prefix: ext_authz
    grpc_service:
      envoy_grpc:
        cluster_name: ext-authz

clusters:
  - name: ext-authz
    type: static
    http2_protocol_options: {}
    hosts:
      - socket_address: { address: 127.0.0.1, port_value: 10003 }</pre>
<div class="blog_h3"><span class="graybg">mongo_proxy</span></div>
<p>过滤器名称envoy.mongo_proxy，支持嗅探MongoDB协议，支持故障注入。</p>
<p>包含的属性如下：</p>
<pre class="crayon-plain-tag"># 统计指标前缀
stat_prefix: "..."
# 访问日志路径
access_log: "..."
# 在代理一个Mongo操作之前，注入一个固定的延迟
delay: "{...}"
# 是否产生动态元数据，没人false
emit_dynamic_metadata: "..." </pre>
<div class="blog_h3"><span class="graybg">ratelimit</span></div>
<p>过滤器名称envoy.ratelimit，用于支持全局性限速。</p>
<p>包含的属性如下：</p>
<pre class="crayon-plain-tag"># 统计指标前缀
stat_prefix: "..."
# 在限速服务请求中使用的限速域
domain: "..."
# 限速描述符
descriptors: 
# 访问限速服务的超时，没人20ms
timeout: "{...}"
# 如果限速服务不能响应，是否拒绝请求，默认false
failure_mode_deny: "..."
# 外部限速服务提供者的配置信息
rate_limit_service: "{...}" </pre>
<p>此过滤器包含以下运行时设置：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">设置</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>ratelimit.tcp_filter_enabled</td>
<td>多少百分比的连接会调用限速服务，默认100</td>
</tr>
<tr>
<td>ratelimit.tcp_filter_enforcing</td>
<td>多少百分比的连接会调用限速服务并且强制限速策略，默认100</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">local_ratelimit</span></div>
<p>过滤器名称envoy.filters.network.local_ratelimit，实现本地限速。</p>
<p>使用令牌桶算法，每个入站连接会占用一个令牌，如果没有令牌可用，立即关闭下游连接。</p>
<div class="blog_h3"><span class="graybg">redis_proxy</span></div>
<p>过滤器名称envoy.redis_proxy。Envoy实现了一个Redis代理，可以基于哈希来将请求分发给上游Redis服务集群。</p>
<p>此过滤器包含的属性如下：</p>
<pre class="crayon-plain-tag">stat_prefix: "..."
# 上游集群名称
cluster: "..."
# 设置
settings: 
  op_timeout: "{...}"</pre>
<div class="blog_h3"><span class="graybg">tcp_proxy</span></div>
<p>过滤器名称envoy.tcp_proxy。</p>
<p>此代理使用的上游集群可以被其它网络过滤器动态的设置。具体做法时为每个连接设置在键envoy.tcp_proxy.cluster下设置一个状态对象。</p>
<p>此过滤器包含的属性如下：</p>
<pre class="crayon-plain-tag">stat_prefix: "..."
# 需要连接到的上游集群
cluster: "..."
# 一个路由可以对应多个集群，这些集群按权重处理连接
weighted_clusters:
  clusters:
  - name：
    weight：
# 元数据匹配，仅上游集群中的、匹配此规则的端点被考虑作为转发目标
metadata_match: "{...}"
# 最大空闲时间，也就是上下游都没有收发数据的时间
idle_timeout: "{...}"
# 访问日志存储位置
access_log: 
# 最大连接请求次数
max_connect_attempts: "{...}"</pre>
<div class="blog_h3"><span class="graybg">sni_cluster</span></div>
<p>过滤器名称envoy.filters.network.sni_cluster。此过滤器在TLS连接中，以SNI为上游集群的名称。</p>
<p>没有配置项，必须在tcp_proxy之前安装。</p>
<div class="blog_h3"><span class="graybg">mysql_proxy</span></div>
<p>过滤器名称：envoy.filters.network.mysql_proxy。此过滤器处于开发阶段，<a href="https://github.com/envoyproxy/envoy/pull/4975">2019年1月17日PR刚刚合并</a>。</p>
<p>此过滤器能够解析MySQL协议，能解析出协议载荷中的SQL查询。解码后的信息作为作为动态元数据释放。这些动态元数据包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">元数据</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>&lt;table.db&gt;</td>
<td>
<p>string，以table.db形式表示的资源名称，如果无法得知db，则为table</p>
<p>值是在资源上执行单操作的列表，操作包括insert/update/select/drop/delete/create/alter/show</p>
</td>
</tr>
</tbody>
</table>
<p>配置示例：</p>
<pre class="crayon-plain-tag">filter_chains:
- filters:
  - name: envoy.filters.network.mysql_proxy
    config:
      stat_prefix: mysql
  # 应当和TCP代理联用
  - name: envoy.tcp_proxy
    config:
      stat_prefix: tcp
      cluster: ...</pre>
<p>MySQL释放的动态元数据，可以和RBAC过滤器联用，实现对表的访问控制：</p>
<pre class="crayon-plain-tag">filter_chains:
- filters:
  - name: envoy.filters.network.mysql_proxy
    config:
      stat_prefix: mysql
  - name: envoy.filters.network.rbac
    config:
      stat_prefix: rbac
      rules:
        # 拒绝对productdb库的catalog表进行update操作
        action: DENY
        policies:
          "product-viewer":
            permissions:
            - metadata:
                filter: envoy.filters.network.mysql_proxy
                path:
                - key: catalog.productdb
                value:
                  list_match:
                    one_of:
                      string_match:
                        exact: update
            principals:
            - any: true
  - name: envoy.tcp_proxy
    config:
      stat_prefix: tcp
      cluster: mysql</pre>
<div class="blog_h3"><span class="graybg">rbac</span></div>
<p>过滤器名称envoy.filters.network.rbac。该过滤器完成对下游服务器的授权（依据其principal）操作，可以明确管理到应用程序的调用，阻止非法访问。</p>
<p>此过滤器支持配置以下两者之一：</p>
<ol>
<li>安全列表（ALLOW）</li>
<li>禁止列表（DENY） </li>
</ol>
<p>列表基于连接属性（IP、端口、SSL subject）来设置访问策略。</p>
<p>访问策略可以工作在enforcement或shadown模式，后者不会实际影响用户，用于在上线前测试策略会产生的影响。</p>
<p>此过滤器包含的属性如下：</p>
<pre class="crayon-plain-tag"># 全局应用的RBAC规则
rules: "{...}"
# 不会对流量产生实际影响，但是会产生统计信息、日志
shadow_rules: "{...}"
# 统计指标前缀
stat_prefix: "..."
# RBAC应用的策略，默认情况下，仅当下游服务第一个字节传输过来时应用ONE_TIME_ON_FIRST_BYTE
# 如果设置为CONTINUOUS则在每个消息边界（依赖于对应协议的过滤器）都应用
enforcement_type: "..."</pre>
<div class="blog_h3"><span class="graybg">dubbo_proxy</span></div>
<p>此过滤器名称envoy.filters.network.dubbo_proxy。</p>
<p>解码Dubbo客户端和服务器之间的RPC协议，信息中抽取的信息转换为元数据，包括：请求ID、请求类型、串行化类型、请求服务名、方法名、参数名、参数值。</p>
<p>此过滤器会暴露很多统计信息。</p>
<p>支持配置dubbo_filters，你可以影响Dubbo路由：</p>
<pre class="crayon-plain-tag">filter_chains:
- filters:
  - name: envoy.filters.network.dubbo_proxy
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.network.dubbo_proxy.v3.DubboProxy
      stat_prefix: dubbo_incomming_stats
      # 指定通信协议和串行化协议
      protocol_type: Dubbo
      serialization_type: Hessian2
      # 路由配置
      route_config:
        name: local_route
        interface: org.apache.dubbo.demo.DemoService
        routes:
        - match:
            method:
              name:
                exact: sayHello
          route:
            cluster: user_service_dubbo_server
      dubbo_filters:
      - name: envoy.filters.dubbo.testFilter
        typed_config:
          "@type": type.googleapis.com/google.protobuf.Struct
          value:
            name: test_service
      - name: envoy.filters.dubbo.router</pre>
<div class="blog_h3"><span class="graybg">kafka_broker</span></div>
<p>过滤器名称envoy.filters.network.kafka_broker。</p>
<p>该过滤器解码Kafka客户端协议，包括请求和应答。Kafka 2.4中的消息版本被支持。该过滤器尝试不去影响客户端和服务器之间的通信，因此无法解码的报文原样转发。</p>
<p>该过滤器必须和tcp_proxy联用，示例：</p>
<pre class="crayon-plain-tag">listeners:
- address:
    socket_address:
      address: 127.0.0.1 # 客户端连接的Kafka地址
      port_value: 19092  # 客户端连接的Kafka端口
  filter_chains:
  - filters:
    - name: envoy.filters.network.kafka_broker
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.filters.network.kafka_broker.v3.KafkaBroker
        stat_prefix: exampleprefix
    - name: envoy.filters.network.tcp_proxy
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
        stat_prefix: tcp
        # 穿透给localkafka集群
        cluster: localkafka
clusters:
- name: localkafka
  connect_timeout: 0.25s
  type: strict_dns
  lb_policy: round_robin
  load_assignment:
    cluster_name: some_service
    endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1 # Kafka broker的实际地址
                port_value: 9092 # Kafka broker的实际端口</pre>
<div class="blog_h3"><span class="graybg">postgres_proxy</span></div>
<p>解码PostgresSQl客户端（下游）和服务器（上游）之间的wire协议，获取的信息用于生成Postgres级别的统计信息：会话、语句、执行的事务…… 目前不去解析SQL查询。</p>
<p>应当和tcp_proxy联用：</p>
<pre class="crayon-plain-tag">filter_chains:
- filters:
  - name: envoy.filters.network.postgres_proxy
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.network.postgres_proxy.v3alpha.PostgresProxy
      stat_prefix: postgres
  - name: envoy.tcp_proxy
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
      stat_prefix: tcp
      cluster: postgres_cluster</pre>
<div class="blog_h3"><span class="graybg">sni_cluster</span></div>
<p>使用TLS连接中的SNI来作为上游集群的名称。对于非TLS连接没有影响。</p>
<div class="blog_h3"><span class="graybg">sni_dynamic_forward_proxy</span></div>
<p>需要联用tls_inspector。基于SNI进行动态代理，下面是一个完整配置： </p>
<pre class="crayon-plain-tag">admin:
  access_log_path: /tmp/admin_access.log
  address:
    socket_address:
      protocol: TCP
      address: 127.0.0.1
      port_value: 9901
static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address:
        protocol: TCP
        address: 0.0.0.0
        port_value: 10000
    listener_filters:
      - name: envoy.filters.listener.tls_inspector
    filter_chains:
      - filters:
          - name: envoy.filters.network.sni_dynamic_forward_proxy
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.sni_dynamic_forward_proxy.v3alpha.FilterConfig
              port_value: 443
              dns_cache_config:
                name: dynamic_forward_proxy_cache_config
                dns_lookup_family: V4_ONLY
          - name: envoy.tcp_proxy
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
              stat_prefix: tcp
              cluster: dynamic_forward_proxy_cluster
  clusters:
  - name: dynamic_forward_proxy_cluster
    connect_timeout: 1s
    lb_policy: CLUSTER_PROVIDED
    cluster_type:
      name: envoy.clusters.dynamic_forward_proxy
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.clusters.dynamic_forward_proxy.v3.ClusterConfig
        dns_cache_config:
          name: dynamic_forward_proxy_cache_config
          dns_lookup_family: V4_ONLY</pre>
<div class="blog_h2"><span class="graybg">HTTP连接管理器</span></div>
<div class="blog_h3"><span class="graybg">路由匹配</span></div>
<p>对一个下游HTTP请求进行路由匹配的流程如下：</p>
<ol>
<li>HTTP请求的host或authority头匹配到一个虚拟主机（virtual host）</li>
<li>虚拟主机中的每个路由条目按顺序检查，如果找到匹配立即使用，不进行后续检查</li>
<li>虚拟主机中的每个虚拟集群（virtual cluster），如果找到立即使用</li>
</ol>
<div class="blog_h3"><span class="graybg">流量转移</span></div>
<p>Envoy支持在虚拟主机的多个上游集群之间划分流量，具体有两种应用场景：</p>
<ol>
<li>版本升级，流量逐步的从一个集群转移（Shifting）到另外一个</li>
<li>A/B测试，多个版本同时被使用。下一节讨论</li>
</ol>
<p>路由配置中的runtime对象决定选择虚拟主机的某个特定集群的概率。通过runtime_fraction配置，某个虚拟主机的流量可以逐步的转移，下面是个例子：</p>
<pre class="crayon-plain-tag">virtual_hosts:
   - name: www2
     domains:
     - '*'
     # 此虚拟主机包含两个路由，分别转发给v1和v2集群
     routes:
       - match:
           prefix: /
           runtime_fraction:
             default_value:
               # 分子，通过改变不断修改此值，实现流量转移
               numerator: 50
               # 分母
               denominator: HUNDRED
             runtime_key: routing.traffic_shift.helloworld
         route:
           cluster: helloworld_v1
       - match:
           prefix: /
         route:
           cluster: helloworld_v2</pre>
<p>完成流量转移的步骤如下：</p>
<ol>
<li>最初，将routing.traffic_shift.helloworld设置为100，所有请求路由给v1</li>
<li>逐步减小routing.traffic_shift.helloworld的值，导致越来越多的请求路由给v2</li>
<li>routing.traffic_shift.helloworld设置为0，完成转移</li>
</ol>
<div class="blog_h3"><span class="graybg">流量分割</span></div>
<p>示例：</p>
<pre class="crayon-plain-tag">virtual_hosts:
   - name: www2
     domains:
     - '*'
     routes:
       - match: { prefix: / }
         route:
           weighted_clusters:
             clusters:
               - name: helloworld_v1
                 weight: 33
               - name: helloworld_v2
                 weight: 33
               - name: helloworld_v3
                 weight: 34</pre>
<p>对于该例子，可以通过运行时变量routing.traffic_split.helloworld.helloworld_v1/2/3动态调整权重。</p>
<div class="blog_h3"><span class="graybg">HTTP头操控</span></div>
<p>HTTP连接管理器在编码响应/解码请求时，都会操控一系列HTTP头：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">头</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>user-agent</td>
<td>
<p>如果配置了add_user_agent选项，解码请求时会添加此头。如果头已经存在则不会修改</p>
<p>此头的值取决于--service-cluster命令行参数</p>
</td>
</tr>
<tr>
<td>server</td>
<td>解码时设置为 server_name选项的值</td>
</tr>
<tr>
<td>x-client-trace-id</td>
<td>
<p>如果外部客户端设置了此头，则Envoy将此ID和内部生成的x-request-id连接起来</p>
<p>如果此头被设置，其效果类似于x-envoy-force-trace，参考运行时配置tracing.client_enabled </p>
</td>
</tr>
<tr>
<td>x-envoy-downstream-service-cluster</td>
<td>
<p>用于提示上游集群，是哪个服务集群调用了它。其值由命令行选项--service-cluster决定</p>
<p>需要将user_agent选项设置为true以使用该头</p>
</td>
</tr>
<tr>
<td>
<p>&nbsp;</p>
<p>x-envoy-downstream-service-node</p>
</td>
<td>用于提示上游集群，是哪个服务实例调用了它。其值由命令行选项--service-node决定</td>
</tr>
<tr>
<td>x-envoy-external-address</td>
<td>
<p>如果请求来自网格外部，则设置为客户端地址（trusted client address ）</p>
<p>如果请求来自内部，不会设置或修改此头。也就是说，此头可以在多个内部服务之间传递来传递去，不需要考虑XFF头那样的复杂性</p>
</td>
</tr>
<tr>
<td>x-envoy-force-trace</td>
<td>
<p>如果某个内部服务设置了此头，则Envoy会修改自动生成的x-request-id并强制调用链跟踪</p>
<p>这也强制在响应头中返回x-request-id </p>
<p>如果此x-request-id传播到了其它主机，这些主机也会进行调用链收集，这会最终生成完整的调用链</p>
<p>参考运行时配置 tracing.global_enabled、tracing.random_sampling </p>
</td>
</tr>
<tr>
<td>x-envoy-internal</td>
<td>用于区分请求来自内部还是外部，Envoy根据XFF判断，并自动设置此头为true或false</td>
</tr>
<tr>
<td>x-envoy-original-dst-host</td>
<td>
<p>使用原始目的地这个负载均衡策略时，此头用于覆盖目的地地址</p>
<p>默认情况下此头被忽略，除非特定通过<a href="https://www.envoyproxy.io/docs/envoy/latest/api-v2/api/v2/cds.proto#envoy-api-field-cluster-originaldstlbconfig-use-http-header">use_http_header</a>启用</p>
</td>
</tr>
<tr>
<td>x-forwarded-client-cert</td>
<td>XFCC是一个代理头，用于指出请求穿过的客户端、所有中间代理的的证书信息</td>
</tr>
<tr>
<td>x-forwarded-for</td>
<td>
<p>XFF是一个标准代理头，用于指示请求穿过的客户端、所有中间代理的IP地址。任何代理都应该用逗号分隔的方式，将直接客户端的地址附加到此头中</p>
<p>仅仅当HTTP连接管理器的use_remote_address选项为true且skip_xff_append=false，Envoy才会附加XFF。否则HTTP连接管理器运行在“透明”模式，导致上游服务不会感知它的存在</p>
</td>
</tr>
<tr>
<td>x-forwarded-proto</td>
<td>用于指示原始的协议（HTTP还是HTTPS）</td>
</tr>
<tr>
<td>x-request-id</td>
<td>
<p>Envoy根据此头来唯一的识别请求，并进行稳定的访问日志和请求追踪</p>
<p>Envoy会为所有外部原始请求生成x-request-id（消毒），对于内部请求，如果没有此头，Envoy也会生成一个</p>
<p>经过网格中各节点时，此x-request-id应该被正确的传播，以便调用链可以被追踪。<span style="background-color: #c0c0c0;">由于Envoy的进程外架构，它无法自动的转发此ID</span>，你需要自己转发，要么手工编码，要么使用某种Agent或库</p>
<p>如果x-request-id正确的传播到所有主机上，则以下特性可用：</p>
<ol>
<li>基于<a href="https://www.envoyproxy.io/docs/envoy/latest/api-v2/config/filter/accesslog/v2/accesslog.proto#envoy-api-field-config-filter-accesslog-v2-accesslogfilter-runtime-filter">v2 API 运行时过滤器</a>的稳定的访问日志</li>
<li>稳定的请求追踪，不管是采样（tracing.random_sampling）、还是强制（x-envoy-force-trace、x-client-trace-id 头）</li>
</ol>
</td>
</tr>
<tr>
<td>x-ot-span-context</td>
<td>使用LightStep这一Tracer时，Envoy使用此头来建立Tracing Span之间的父子关系</td>
</tr>
<tr>
<td>x-b3-traceid</td>
<td>使用Zipkin这一Tracer时，此头是一个8字节的字符串，表示整个Trace的标识</td>
</tr>
<tr>
<td>x-b3-spanid</td>
<td>使用Zipkin这一Tracer时，此头是一个8字节的字符串，表示当前操作在Span树中的位置</td>
</tr>
<tr>
<td>x-b3-parentspanid</td>
<td>使用Zipkin这一Tracer时，此头是一个8字节的字符串，表示当前操作的父操作在Span树中的位置</td>
</tr>
<tr>
<td>x-b3-sampled</td>
<td>使用Zipkin这一Tracer时，如果设置为1或者不设置，则Span会被报告给Tracer，整个Trace中此值应该恒定</td>
</tr>
<tr>
<td>x-b3-flags</td>
<td>使用Zipkin这一Tracer时，编码多个选项</td>
</tr>
<tr>
<td>b3</td>
<td>使用Zipkin这一Tracer时，更加复杂的头格式</td>
</tr>
<tr>
<td>x-datadog-trace-id</td>
<td rowspan="3">Datadog（一种Tracer）相关的头</td>
</tr>
<tr>
<td>x-datadog-parent-id</td>
</tr>
<tr>
<td>x-datadog-sampling-priority</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">自定义头</span></div>
<p>在权重集群、路由、虚拟主机、全局配置级别，都可以添加自定义请求/响应头。</p>
<p>以-开头的伪头可以通过自定义头来覆盖。而:path和:authority头则可以通过prefix_rewrite或host_rewrite机制修改。</p>
<p>头的优先级顺序（从高到低）：权重集群、路由、虚拟主机、全局配置级别。</p>
<p>在自定义头中可以使用以下变量：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">变量</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%</td>
<td>下游连接的地址，不带端口</td>
</tr>
<tr>
<td>%DOWNSTREAM_LOCAL_ADDRESS%</td>
<td>
<p>下游连接的本地的地址，包含IP和端口</p>
<p>如果原始请求基于Iptables REDIRECT转发，此变量存储原始目的地址</p>
<p>如果原始请求基于Iptables TPROXY转发，且监听器的transparent为true，此变量存储原始目的地址和端口</p>
</td>
</tr>
<tr>
<td>%DOWNSTREAM_LOCAL_ADDRESS_WITHOUT_PORT%</td>
<td>上面的，不包含端口</td>
</tr>
<tr>
<td>%PROTOCOL%</td>
<td>原始的协议，和x-forwarded-proto头相同</td>
</tr>
<tr>
<td>%UPSTREAM_METADATA([“namespace”, “key”, …])%</td>
<td>
<p>从被路由选择的上游主机的<a href="https://www.envoyproxy.io/docs/envoy/latest/api-v2/api/v2/endpoint/endpoint.proto#envoy-api-field-endpoint-lbendpoint-metadata">EDS端点元数据</a>中获取的元数据</p>
</td>
</tr>
<tr>
<td>%PER_REQUEST_STATE(reverse.dns.data.name)%</td>
<td> </td>
</tr>
<tr>
<td>%START_TIME%</td>
<td>
<p>请求处理开始时间，示例：</p>
<pre class="crayon-plain-tag">route:
  cluster: www
  request_headers_to_add:
    - header:
        key: "x-request-start"
        # 当前时间，以秒表示，精确到ms
        value: "%START_TIME(%s.%3f)%"
      append: true</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">头消毒</span></div>
<p>由于安全原因，Envoy会对收到的HTTP头进行消毒（<span style="color: #404040;">sanitize） —— 要么添加、修改、删除。</span>
<div class="blog_h3"><span class="graybg">运行时设置</span></div>
<p>HTTP连接管理器支持以下运行时设置：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">设置</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>tracing.client_enabled</td>
<td>如果设置了x-client-trace-id头，多少比例的请求被追踪，默认100</td>
</tr>
<tr>
<td>tracing.global_enabled</td>
<td>如果其它条件都满足，则多少比例的请求被追踪，默认100</td>
</tr>
<tr>
<td>tracing.random_sampling</td>
<td>多少请求被采样式的追踪，取值0-10000之间，100表示百分之一</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">RDS</span></div>
<p>路有发现服务RDS是一个可选的API，Envoy可以调用此API以动态的抓取路由配置。每个HTTP连接管理器可以独立的抓取。</p>
<div class="blog_h2"><span class="graybg">HTTP过滤器</span></div>
<div class="blog_h3"><span class="graybg">buffer</span></div>
<p>过滤器名称envoy.buffer。此过滤器用于停止过滤器迭代，然后等待完整的请求到达。用途是：</p>
<ol>
<li>保护某些应用程序，使其不必处理不完整请求（partial request）</li>
<li>降低网络延迟</li>
</ol>
<p>此过滤器包含的属性：</p>
<pre class="crayon-plain-tag"># 此过滤器最大能缓冲的请求大小，超过请求后HTTP连接管理器返回413错误
max_request_bytes: "{...}"</pre>
<p>在每个路由级别，buffer过滤器可以被覆盖配置或者禁用：</p>
<pre class="crayon-plain-tag"># 是否禁用
disabled: true
# 覆盖的配置
buffer:
  max_request_bytes: ...</pre>
<div class="blog_h3"><span class="graybg">cors </span></div>
<p>过滤器名称envoy.cors。此过滤器基于路由/虚拟主机的配置来处理跨站资源共享（Cross-Origin Resource Sharing）请求。</p>
<p>此过滤器包含的属性：</p>
<pre class="crayon-plain-tag"># 允许的源
allow_origin: []
# 允许的源的需要匹配的正则式
allow_origin_regex: []
# 指定access-control-allow-methods头的内容
allow_methods: "..."
# 指定access-control-allow-headers头的内容
allow_headers: "..."
# 指定access-control-expose-headers头的内容
expose_headers: "..."
# 指定access-control-max-age头的内容
max_age: "..."
# 是否allows credentials
allow_credentials: "{...}"
# 是否启用CORS
enabled: "{...}"</pre>
<p>此过滤器支持以下运行时RuntimeFractionalPercent设置：</p>
<ol>
<li>filter_enabled：对于多少比例的请求启用此过滤器，默认100/HUNDRED</li>
<li>shadow_enabled：多少比例的请求在shadow模式下运行此过滤器，默认0</li>
</ol>
<div class="blog_h3"><span class="graybg">ext_authz</span></div>
<p>过滤器名称envoy.ext_authz。此过滤器调用外部的gRPC/HTTP服务，以确认请求是否有权访问。如果验证失败会返回403/Forbidden应答。</p>
<p>从授权服务传递额外的元数据到上游、下游也是支持的，反方向也是。发送给授权服务的请求内容由CheckRequest规定。</p>
<p>此过滤器包含的属性：</p>
<pre class="crayon-plain-tag"># 外部gRPC授权服务配置
grpc_service: "{...}"
# 外部HTTP授权服务配置
http_service: "{...}"
# 如果授权服务不响应，是否允许请求通过
failure_mode_allow: "..."</pre>
<p>基于gRPC授权服务的配置示例：</p>
<pre class="crayon-plain-tag">http_filters:
  - name: envoy.ext_authz
    config:
      grpc_service:
        envoy_grpc:
          cluster_name: ext-authz
        # 默认超时200ms
        timeout: 0.5s

...

clusters:
  - name: ext-authz
    type: static
    http2_protocol_options: {}
    hosts:
      - socket_address: { address: 127.0.0.1, port_value: 10003 }

    # 最初握手的超时，不是请求超时
    connect_timeout: 0.25s</pre>
<p>基于HTTP授权服务的配置示例： </p>
<pre class="crayon-plain-tag">http_filters:
  - name: envoy.ext_authz
    config:
      http_service:
          server_uri:
            uri: 127.0.0.1:10003
            cluster: ext-authz
            timeout: 0.25s
            failure_mode_allow: false

...

clusters:
  - name: ext-authz
    connect_timeout: 0.25s
    type: logical_dns
    lb_policy: round_robin
    hosts:
      - socket_address: { address: 127.0.0.1, port_value: 10003 }</pre>
<p>在路由级别定制配置：</p>
<pre class="crayon-plain-tag">route_config:
  name: local_route
  virtual_hosts:
  - name: local_service
    domains: ["*"]
    per_filter_config:
      envoy.ext_authz:
        check_settings:
          context_extensions:
            virtual_host: local_service
    routes:
    - match: { prefix: "/static" }
      route: { cluster: some_service }
      # 为某个过滤器覆盖配置
      per_filter_config:
        # 禁用ext_authz
        envoy.ext_authz:
          disabled: true
    - match: { prefix: "/" }
      route: { cluster: some_service }</pre>
<div class="blog_h3"><span class="graybg">fault</span></div>
<p>过滤器名称envoy.fault。故障注入过滤器可以用于测试微服务的顽强性（resiliency） ，它能够注入延迟（目前仅支持固定延迟），或者注入用户指定的错误码（仅仅支持HTTP状态码），从而模拟各种错误场景。这些场景包括：服务过载、高网络延迟、网络分区。</p>
<p>故障注入可以仅仅应用于特定的请求集，这些请求集以请求头、上游服集群等信息识别。</p>
<p>此过滤器的属性包括：</p>
<pre class="crayon-plain-tag"># 注入延迟
delay: 
  # 目前仅仅支持FIXED
  type: FIXED
  fixed_delay: "{...}"
  percentage: "{...}"
# 注入中断
abort: 
  http_status: "..."
  percentage: "{...}"
# 此过滤器匹配的上游集群
upstream_cluster: "..."
# 此过滤器匹配的请求头
headers: 
- name: "..."
  exact_match: "..."
  regex_match: "..."
  range_match: "{...}"
  present_match: "..."
  prefix_match: "..."
  suffix_match: "..."
  invert_match: "..."
# 故障为以下下游主机注入， 下游主机的名字从x-envoy-downstream-service-node头读取
downstream_nodes: []</pre>
<p>此过滤器支持以下运行时配置：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">配置</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>fault.http.abort.abort_percent</td>
<td>
<p>请求头匹配的请求，有多少百分比被注入abort</p>
<p>下游集群可以覆盖：</p>
<p>fault.http.&lt;downstream-cluster&gt;.abort.abort_percent</p>
</td>
</tr>
<tr>
<td>fault.http.abort.http_status</td>
<td>
<p>注入什么状态码</p>
<p>下游集群可以覆盖：</p>
<p>fault.http.&lt;downstream-cluster&gt;.abort.http_status</p>
</td>
</tr>
<tr>
<td>fault.http.delay.fixed_delay_percent</td>
<td>
<p>请求头匹配的请求，有多少百分比被注入延迟</p>
<p>下游集群可以覆盖：</p>
<p>fault.http.&lt;downstream-cluster&gt;.delay.fixed_delay_percent</p>
</td>
</tr>
<tr>
<td>fault.http.delay.fixed_duration_ms</td>
<td>
<p>注入多久的延迟</p>
<p>下游集群可以覆盖：</p>
<p>fault.http.&lt;downstream-cluster&gt;.delay.fixed_duration_ms</p>
</td>
</tr>
</tbody>
</table>
<p>这些运行时设置可以为每个下游集群覆盖，&lt;downstream-cluster&gt;取请求头x-envoy-downstream-service-cluster。</p>
<div class="blog_h3"><span class="graybg">grpc_http1_bridge</span></div>
<p>过滤器名称envoy.grpc_http1_bridge，用于桥接一个不支持响应报尾（response trailers）的HTTP/1.1客户端到gRPC服务器。其工作方式如下：</p>
<ol>
<li>请求发送时，此过滤器检查连接是否基于HTTP/1.1，且MIME类型为application/grpc，如果是</li>
<li>当Envoy接收到应答时，过滤器会缓冲它，并等待trailers，检查grpc-status码</li>
<li>如果此码不为0，则过滤器将HTTP状态码设置为503，同时将grpc-status、grpc-message这两个tailers存放到响应头中</li>
<li>客户端必须发送包含以下伪头的HTTP/1.1请求：
<ol>
<li>:method，取值POST</li>
<li>:path，取值gRPC方法的名称</li>
<li>content-type，取值application/grpc</li>
<li>请求体必须是串行化的gRPC体：
<ol>
<li>第一字节为非压缩的0</li>
<li>网络字节序的4字节，Proto消息的长度</li>
<li>串行化后的Proto消息</li>
</ol>
</li>
</ol>
</li>
<li>由于需要缓冲响应并寻找grpc-status报尾，因此仅仅支持一元gRPC API</li>
</ol>
<div class="blog_h3"><span class="graybg">grpc_http1_reverse_bridge</span></div>
<p>过滤器名称envoy.grpc_http1_reverse_bridge。将gRPC请求转换为HTTP 1.1请求，并转发给不理解gRPC或HTTP/2的服务器处理。</p>
<div class="blog_h3"><span class="graybg">grpc_json_transcoder</span></div>
<p>过滤器名称envoy.grpc_json_transcoder。将RESTful JSON API请求转换为gRPC请求并转发给gRPC服务器。</p>
<div class="blog_h3"><span class="graybg">grpc_web</span></div>
<p>过滤器名称envoy.grpc_web。实现gRPC-Web客户端到gRPC服务器的<a href="https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md">桥接</a>。</p>
<div class="blog_h3"><span class="graybg">gzip</span></div>
<p>过滤器名称envoy.gzip。实现压缩功能。</p>
<div class="blog_h3"><span class="graybg">health_check</span></div>
<p>过滤器名称envoy.health_check。参考1.12 健康检查。</p>
<div class="blog_h3"><span class="graybg">header_to_metadata</span></div>
<p>过滤器名称envoy.filters.http.header_to_metadata。此过滤器包含一系列规则，规则对请求/响应进行匹配。每个规则包含一个头，当请求/响应包含或不包含此头时，规则匹配，并添加动态元数据。</p>
<p>动态元数据可以用来决定如何负载均衡，或者被日志模块使用。最典型的用法是动态的匹配请求和负载均衡子集：</p>
<pre class="crayon-plain-tag"># 过滤器配置
http_filters:
  - name: envoy.filters.http.header_to_metadata
    config:
      request_rules:
        - header: x-version
          # 有x-version头的请求，使用和此头的值匹配的version的端点
          on_header_present:
            metadata_namespace: envoy.lb
            key: version
            type: STRING
          # 没有x-version头的请求，使用默认端点
          on_header_missing:
            metadata_namespace: envoy.lb
            key: default
            value: 'true'
            type: STRING
          remove: false


# 上游集群配置

clusters:
  - name: versioned-cluster
    type: EDS
    lb_policy: ROUND_ROBIN
    lb_subset_config:
      fallback_policy: ANY_ENDPOINT
      # 负载均衡子集选择器
      subset_selectors:
        - keys:
            - default
        - keys:
            - version</pre>
<div class="blog_h3"><span class="graybg">lua</span></div>
<p>过滤器名称envoy.lua。此过滤器运行你提供Lua脚本，处理请求/应答流。此过滤器适用于简单的场景，如果<span style="background-color: #c0c0c0;">要实现复杂的、高性能的逻辑，请编写原生的C++过滤器</span>。</p>
<p>注意：</p>
<ol>
<li>所有Lua环境是每个工作线程独占的，也就是说没有真正全局性的数据。加载期间生成的全局变量，对于每个工作线程来说都是独立副本</li>
<li>所有脚本以协程的形式执行，也就是说它们以同步的方式运行，即使实际执行的是复杂的异步工作。网络/异步处理由Envoy执行，Envoy在适当的时机调用Lua脚本</li>
<li><span style="background-color: #c0c0c0;">绝不要在Lua中包含阻塞性操作</span></li>
<li>默认构建的Envoy没有导出符号，和以共享对象安装的Lua模块交互时可能需要这些符号</li>
</ol>
<p>目前支持的特性：</p>
<ol>
<li>查看报文头、报文体、报尾</li>
<li>修改报文头、报文尾</li>
<li>阻塞并等到完整的请求/应答，以进行完整的inspection</li>
<li>向上游主机发起异步的HTTP出站调用</li>
<li>直接提供响应，终止过滤器迭代</li>
</ol>
<p>LuaJIT作为Lua的运行时，支持的版本是5.1，以及一些5.2特性。</p>
<p>示例：</p>
<pre class="crayon-plain-tag">http_filters:
- name: envoy.lua
  config:
    inline_code: |
      # 处理请求
      function envoy_on_request(request_handle)
        request_handle:headers():add("foo", "bar")
      end
      # 处理应答
      function envoy_on_response(response_handle)
        body_size = response_handle:body():length()
        response_handle:headers():add("response-body-size", tostring(body_size))
      end</pre>
<p>流句柄（Stream handle）API，也就是envoy_on_request/envoy_on_response函数的入参handle的方法列表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">方法</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>headers()</td>
<td>返回请求/应答流的头，在发送到header chain中另一个过滤器链之前，这些头可以被修改。例如，你可以在httpCall()、body()调用之后进行头修改</td>
</tr>
<tr>
<td>body()</td>
<td>返回流的体，此调用导致Envoy yield掉脚本，直到整个体被缓冲完毕</td>
</tr>
<tr>
<td>bodyChunks()</td>
<td>
<p>返回一个迭代器，用于编译流体的所有片断。Envoy会在获取每个片断时yield脚本，但是不会进行缓冲操作。示例：</p>
<pre class="crayon-plain-tag">for chunk in request_handle:bodyChunks() do
  request_handle:log(0, chunk:length())
end</pre>
</td>
</tr>
<tr>
<td>trailers()</td>
<td>返回报文尾，如果没有可能返回nil。发送到下一个过滤器之间你可以修改trailers</td>
</tr>
<tr>
<td>log*()</td>
<td>
<p>记录Envoy应用程序日志：
<pre class="crayon-plain-tag">handle:logTrace(message)
handle:logDebug(message)
handle:logInfo(message)
handle:logWarn(message)
handle:logErr(message)
handle:logCritical(message)</pre>
</td>
</tr>
<tr>
<td>httpCall()</td>
<td>
<p>向上游主机发起HTTP调用：
<pre class="crayon-plain-tag">headers, body = handle:httpCall(cluster, headers, body, timeout)</pre>
<p>Envoy会yield脚本，知道请求完成或者出错。cluster是已经配置好的集群名称，超时单位ms </p>
</td>
</tr>
<tr>
<td>respond()</td>
<td>
<p>不再继续过滤器迭代，立即进行应答。仅仅在请求流中可以调用：</p>
<pre class="crayon-plain-tag">handle:respond(headers, body)</pre>
</td>
</tr>
<tr>
<td>metadata()</td>
<td>
<p>返回当前路由条目元数据（route entry metadata），这些元数据必须声明在命名空间envoy.lua下面，例如：
<pre class="crayon-plain-tag">metadata:
  filter_metadata:
    envoy.lua:
      foo: bar
      baz:
        - bad
        - baz</pre>
</td>
</tr>
<tr>
<td>streamInfo() </td>
<td>返回和当前流有关的信息</td>
</tr>
<tr>
<td>connection()</td>
<td>返回当前请求使用的底层连接对象</td>
</tr>
</tbody>
</table>
<p>头对象的API：
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">方法</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>add()</td>
<td>添加一个头</td>
</tr>
<tr>
<td>get()</td>
<td>获取一个头</td>
</tr>
<tr>
<td>__pairs()</td>
<td>
<p>用于迭代所有头：</p>
<pre class="crayon-plain-tag">for key, value in pairs(headers) do
end</pre>
</td>
</tr>
<tr>
<td>remove()</td>
<td>移除头</td>
</tr>
<tr>
<td>
<p>replace()
</td>
<td>替换头</td>
</tr>
</tbody>
</table>
<p>Buffer的API：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">方法</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>length()</td>
<td>返回缓冲的长度</td>
</tr>
<tr>
<td>getBytes()</td>
<td>从缓冲中读取字节：<pre class="crayon-plain-tag">buffer:getBytes(index, length)</pre></td>
</tr>
</tbody>
</table>
<p>Metadata对象的API：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">方法</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>get()</td>
<td>获取一个元数据，值的类型可能是nil, boolean, number, string, table</td>
</tr>
<tr>
<td>__pairs()</td>
<td>用于迭代所有元数据</td>
</tr>
</tbody>
</table>
<p>StreamInfo对象的API：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">方法</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>protocol()</td>
<td>返回使用的协议，可能的值包括HTTP/1.0, HTTP/1.1, HTTP/2</td>
</tr>
<tr>
<td>dynamicMetadata()</td>
<td>返回动态元数据</td>
</tr>
</tbody>
</table>
<p>动态元数据的API：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">方法</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>get()</td>
<td>
<p>获取动态元数据：</p>
<p><pre class="crayon-plain-tag">dynamicMetadata:get(filterName)
dynamicMetadata:get(filterName)[key]</pre>
</td>
</tr>
<tr>
<td>set()</td>
<td>设置动态元数据：<pre class="crayon-plain-tag">dynamicMetadata:set(filterName, key, value)</pre></td>
</tr>
<tr>
<td>__pairs()</td>
<td>迭代所有元数据</td>
</tr>
</tbody>
</table>
<p>API示例：</p>
<pre class="crayon-plain-tag">-- 添加头
request_handle:headers():add("request_body_size", request_handle:body():length())

-- 删除头
response_handle:headers():remove("foo")

-- 发起HTTP调用
local headers, body = request_handle:httpCall( "lua_cluster", {
    [":method"] = "POST",
    [":path"] = "/",
    [":authority"] = "lua_cluster"
  }, "hello world", 5000)

-- 直接应答
request_handle:respond({[":status"] = "403", ["upstream_foo"] = headers["foo"]}, "nope")</pre>
<div class="blog_h3"><span class="graybg">rate_limit</span></div>
<p>过滤器名称envoy.rate_limit。如果请求的路由/虚拟主机配置包含1-N个限速配置匹配过滤器的stage，则此过滤器会调用限速服务，将每个限速配置产生的描述符发送给限速服务。</p>
<p>如果限速服务返回的响应，提示任何一个描述符超限，则返回429响应给客户端。</p>
<p>如果调用限速服务失败，且failure_mode_deny配置为true，则返回500应答。</p>
<p>此过滤器包含的属性：</p>
<pre class="crayon-plain-tag"># 调用限速服务时使用的限速域
domain: "..."
# 应用的限速配置对应的Stage号，默认0
stage: "..."
# 此过滤器应用到的请求类型。可选internal、external、both
# 如果请求包含头x-envoy-internal=true则认为是内部请求
request_type: "..."
# 调用限速服务的超时默认20ms
timeout: "{...}"
# 如果调用限速服务失败，是否拒绝请求
failure_mode_deny: "..."
# 对于gRPC请求，是否返回RESOURCE_EXHAUSTED而非默认的UNAVAILABLE gRPC码
rate_limited_as_resource_exhausted: "..."
# 外部限速服务的配置
rate_limit_service: 
  grpc_service: "{...}"</pre>
<div class="blog_h3"><span class="graybg">http.rbac</span></div>
<p>过滤器名称envoy.filters.http.rbac。此过滤器用于授权下游客户端的访问权限，用于明确管理谁能访问服务。可以根据连接属性（IP、端口、SSL主体）或者请求头来决定ALLOW/DENY访问请求。</p>
<div class="blog_h3"><span class="graybg">router</span></div>
<p>过滤器名称envoy.router。此过滤器实现了HTTP forwarding，几乎会用在所有HTTP代理场景下。此过滤器的职责包括：</p>
<ol>
<li>根据路由表配置进行转发</li>
<li>处理重试</li>
<li>处理统计信息收集 </li>
</ol>
<p>此过滤器包含的属性：</p>
<pre class="crayon-plain-tag"># 是否生成动态集群统计信息，默认true，在高性能场景下可以禁用
dynamic_stats: "{...}"
# 是否为出口路由调用生成child span
start_child_span: "..."
# 上游调用日志的配置，这个日志类似于访问日志，但是每个条目表示一个对上游的请求，如果发生重试，一个访问日志可能对应多个上游日志
upstream_log: 
# 是否不添加额外的x-envoy-头
suppress_envoy_headers: "..."</pre>
<p>此过滤器会读取以下头：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">头</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>x-envoy-max-retries</td>
<td>
<p>如果路由/虚拟主机的重试策略存在，默认情况下Envoy会重试一次，除非明确指定重试次数</p>
<p>重试次数可以在几个地方提供：</p>
<ol>
<li>路由、虚拟主机的重试配置</li>
<li>x-envoy-max-retries头中</li>
</ol>
<p>如果重试策略没有配置，且x-envoy-retry-on、x-envoy-retry-grpc-on头不存在，则Envoy不会进行重试</p>
<p>如果有多个配置，取其中最大的重试次数</p>
<p>关于Envoy重试的一些细节：</p>
<ol>
<li>路由超时（在路由配置中指定，或者通过x-envoy-upstream-rq-timeout-ms头设置）是<span style="background-color: #c0c0c0;">包含所有重试的总超时</span></li>
<li>Envoy基于 fully jittered exponential backoff algorithm 来进行重试，第一次重试随机延迟0-24ms，第二次0-74ms，第三次0-174ms</li>
</ol>
</td>
</tr>
<tr>
<td>x-envoy-retry-on</td>
<td>
<p>为出站请求设置该头，会导致在特定时机自动重试，多个时机用逗号分隔：</p>
<ol>
<li>5xx，如果接收到上游主机的5xx响应码</li>
<li>gateway-error，如果接收到502、503、504响应码</li>
<li>connect-failure，如果出现连接错误（例如超时），包括5xx</li>
<li>retriable-4xx，如果接收到可重试的4xx，当前仅仅支持409</li>
<li>refused-stream，如果上游服务器以REFUSED_STREAM重置了流</li>
<li>retriable-status-codes，如果解说道任何可重试的状态码</li>
</ol>
</td>
</tr>
<tr>
<td>x-envoy-retry-grpc-on</td>
<td>
<p>为出站请求设置该头，会导致在特定时机自动重试gRPC请求：</p>
<ol>
<li>cancelled，如果gRPC响应头状态码是cancelled</li>
<li>deadline-exceeded</li>
<li>internal</li>
<li>resource-exhausted</li>
<li>unavailable</li>
</ol>
</td>
</tr>
<tr>
<td>x-envoy-retriable-status-codes</td>
<td>提示哪些状态码是“可重试”的</td>
</tr>
<tr>
<td>x-envoy-upstream-rq-timeout-ms</td>
<td>覆盖路由配置中的超时，单位ms</td>
</tr>
<tr>
<td>x-envoy-upstream-rq-per-try-timeout-ms</td>
<td>每次重试的超时，单位ms</td>
</tr>
<tr>
<td>x-envoy-immediate-health-check-fail</td>
<td>如果上游主机返回此响应头，则Envoy立即认为上游主机主动检查失败（如果配置了主动检查的话），用于快速失败而不是等待下次健康检查</td>
</tr>
<tr>
<td>x-envoy-overloaded</td>
<td>如果上游主机返回此响应头，则不进行重试</td>
</tr>
<tr>
<td>x-envoy-ratelimited</td>
<td>如果上游主机返回此响应头，则不进行重试</td>
</tr>
</tbody>
</table>
<p>此过滤器会设置以下头：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">头</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>x-envoy-attempt-count</td>
<td>发送给上游主机，提示已经重试的次数，最初请求设置为1，每次重试+1</td>
</tr>
<tr>
<td>x-envoy-expected-rq-timeout-ms</td>
<td>路由器期望请求在多长时间内完成。这个头发送给上游服务器，辅助其决策（例如超时后仍然在处理中，可以直接终止处理）</td>
</tr>
<tr>
<td>x-envoy-upstream-service-time</td>
<td>上游主机处理请求消耗的时间，客户端可以依次判断网络延迟的影响</td>
</tr>
<tr>
<td>x-envoy-original-path</td>
<td>原始的路径，由于前缀重写，路径可能变化了，用于日志和调试</td>
</tr>
<tr>
<td>x-envoy-overloaded</td>
<td>如果请求由于维护模式、上游断路而失败，在给下游的应答中设置此头</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">集群管理器</span></div>
<p>包含的属性：</p>
<pre class="crayon-plain-tag"># 本地集群（拥有运行此配置的Envoy的集群）名称。要使用Zone可知路由则必须设置，此属性和 --service-cluster 无关
local_cluster_name: "..."
# 全局性异常检测设置
outlier_detection: 
  event_log_path: "..."
# 绑定新创建的上游连接时使用的配置
upstream_bind_config: 
  source_address: "{...}"
  freebind: "{...}"
  socket_options: 
# 
load_stats_config:</pre>
<div class="blog_h2"><span class="graybg">访问日志</span></div>
<p>访问日志配置，是HTTP连接管理器或者TCP代理配置的一部分。具体参考<a href="https://www.envoyproxy.io/docs/envoy/latest/configuration/access_log">官方文档</a>。 </p>
<div class="blog_h2"><span class="graybg">过载管理器</span></div>
<p>过载管理器用于保护Envoy代理所在机器本身，过载管理器的配置，放在Bootstrap配置的overload_manager字段下。</p>
<div class="blog_h2"><span class="graybg">SDS</span></div>
<p>TLS证书、密码等敏感信息可以在static_resource中配置，也可以从远程的SDS服务抓取。</p>
<p>SDS的价值主要是简化证书管理。如果没有此特性，在K8S场景下，证书只能以Secret的方式定义并挂载到代理容器中。如果证书过期，则需要重现挂载。使用SDS，过期后新证书自动被推送到Envoy代理。</p>
<div class="blog_h1"><span class="graybg">指标</span></div>
<div class="blog_h2"><span class="graybg">监听器管理器</span></div>
<p>监听器管理器在命名空间listener_manager.下暴露以下监控指标：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">指标</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td><span style="color: #404040;">listener_added</span></td>
<td>Counter，总计添加的监听器数量（包括静态和LDS）</td>
</tr>
<tr>
<td><span style="color: #404040;">listener_modified</span></td>
<td>Counter，通过LDS修改监听器的次数</td>
</tr>
<tr>
<td>listener_removed</td>
<td>Counter，通过LDS删除监听器的次数</td>
</tr>
<tr>
<td>listener_create_success</td>
<td>Counter，总计成功添加到Worker中的监听器对象数量</td>
</tr>
<tr>
<td>listener_create_failure</td>
<td>Counter，总计失败添加到Worker中的监听器对象数量</td>
</tr>
<tr>
<td>total_listeners_warming</td>
<td>Gauge，当前处于预热状态的监听器数量</td>
</tr>
<tr>
<td>total_listeners_active</td>
<td>Gauge，当前处于活动状态的监听器数量</td>
</tr>
<tr>
<td>total_listeners_draining</td>
<td>Gauge，当前处于draining状态的监听器数量</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">监听器</span></div>
<p>每个监听器都在命名空间<pre class="crayon-plain-tag">listener.&lt;address&gt;</pre>下暴露以下监控指标：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">指标</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>downstream_cx_total</td>
<td>Counter，连接总数</td>
</tr>
<tr>
<td>downstream_cx_destroy</td>
<td>Counter，销毁的连接总数</td>
</tr>
<tr>
<td>downstream_cx_active</td>
<td>Gauge，当前活动的连接总数</td>
</tr>
<tr>
<td>downstream_cx_length_ms</td>
<td>Histogram，连接持续时间ms</td>
</tr>
<tr>
<td>downstream_pre_cx_timeout</td>
<td>Counter，在监听器过滤器处理期间，超时的套接字数量</td>
</tr>
<tr>
<td>downstream_pre_cx_active</td>
<td>Gauge，正在被监听器过滤器处理的连接请求数量</td>
</tr>
<tr>
<td>no_filter_chain_match</td>
<td>Counter，总计的不匹配任何过滤器链的连接数量</td>
</tr>
<tr>
<td>ssl.connection_error</td>
<td>Counter，总计的TLS连接错误数量，包括证书验证失败的情况</td>
</tr>
<tr>
<td>ssl.handshake</td>
<td>Counter，总计的TLS握手成功次数</td>
</tr>
<tr>
<td>ssl.session_reused</td>
<td>Counter，总计的TLS会话重用次数/home/alex/CPP/projects/clion/envoy</td>
</tr>
<tr>
<td>ssl.no_certificate</td>
<td>Counter，总计的没有客户端证书的成功TLS连接次数</td>
</tr>
<tr>
<td>ssl.fail_verify_no_cert</td>
<td>Counter，总计的由于没有客户端证书而导致TLS连接失败的次数</td>
</tr>
<tr>
<td>ssl.fail_verify_error</td>
<td>Counter，总计的由于证书验证失败而导致TLS连接失败的次数</td>
</tr>
<tr>
<td>ssl.fail_verify_san</td>
<td>Counter，总计的由SAN验证失败而导致TLS连接失败的次数</td>
</tr>
<tr>
<td>ssl.fail_verify_cert_hash</td>
<td>Counter，总计的由证书哈希验证失败而导致TLS连接失败的次数</td>
</tr>
<tr>
<td>ssl.ciphers.&lt;cipher&gt;</td>
<td>Counter，使用cipher的成功TLS连接次数</td>
</tr>
<tr>
<td>ssl.curves.&lt;curve&gt;</td>
<td>Counter，使用curve的成功TLS连接次数</td>
</tr>
<tr>
<td>ssl.sigalgs.&lt;sigalg&gt;</td>
<td>Counter，使用签名算法sigalg的成功TLS连接次数</td>
</tr>
<tr>
<td>ssl.versions.&lt;version&gt;</td>
<td>Counter，使用协议版本version的成功TLS连接次数</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">监听器过滤器</span></div>
<div class="blog_h3"><span class="graybg">proxy_protocol</span></div>
<p>此过滤器产生以下指标：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">指标</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>downstream_cx_proxy_proto_error</td>
<td>Counter，总计的代理协议错误数量</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">tls_inspector</span></div>
<p>此过滤器在命名空间下暴露以下指标：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">指标</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>connection_closed</td>
<td>Counter，总计关闭的连接数量</td>
</tr>
<tr>
<td>client_hello_too_large</td>
<td>Counter，总计收到的客户端不合理的大尺寸Hello报文数量</td>
</tr>
<tr>
<td>read_error</td>
<td>Counter，总计读错误数量</td>
</tr>
<tr>
<td>tls_found</td>
<td>Counter，TLS被找到的数量（也就是说入站连接没有使用TLS协议）</td>
</tr>
<tr>
<td>tls_not_found</td>
<td>Counter，TLS没被找到的数量</td>
</tr>
<tr>
<td>alpn_found</td>
<td>Counter，应用层协议协商找到的次数</td>
</tr>
<tr>
<td>alpn_not_found</td>
<td>Counter，应用层协议协商未找到的次数</td>
</tr>
<tr>
<td>sni_found</td>
<td>Counter，SNI找到的次数</td>
</tr>
<tr>
<td>sni_not_found</td>
<td>Counter，SNI未找到的次数</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">网络过滤器</span></div>
<div class="blog_h3"><span class="graybg">client_ssl_auth</span></div>
<p>此过滤器在命名空间 auth.clientssl.&lt;stat_prefix&gt;.下暴露以下指标：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">指标</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>update_success</td>
<td>Counter，总计Principal更新成功次数</td>
</tr>
<tr>
<td>update_failure</td>
<td>Counter，总计Principal更新失败次数</td>
</tr>
<tr>
<td>auth_no_ssl</td>
<td>Counter，由于没有TLS而忽略的连接数量</td>
</tr>
<tr>
<td>auth_ip_white_list</td>
<td>Counter，由于IP白名单而允许的连接数量</td>
</tr>
<tr>
<td>auth_digest_match</td>
<td>Counter，由于证书匹配而允许的连接数量</td>
</tr>
<tr>
<td>auth_digest_no_match</td>
<td>Counter，由于证书不匹配而拒绝的连接数量</td>
</tr>
<tr>
<td>total_principals</td>
<td>Gauge，总计加载的Principal数量</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">ext_authz</span></div>
<p>此过滤器在命名空间config.ext_authz.下暴露以下指标：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">指标</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>total</td>
<td>Counter，此过滤器的响应总数</td>
</tr>
<tr>
<td>error</td>
<td>Counter，联系授权服务失败的次数</td>
</tr>
<tr>
<td>denied</td>
<td>Counter，无权访问的次数</td>
</tr>
<tr>
<td>failure_mode_allowed</td>
<td>Counter，验证不通过，但是由于failure_mode_allow设置为true而允许通过的请求数量</td>
</tr>
<tr>
<td>ok</td>
<td>Counter，授权服务允许连接通过的次数</td>
</tr>
<tr>
<td>cx_closed</td>
<td>Counter，被关闭的连接数</td>
</tr>
<tr>
<td>active</td>
<td>Gauge，当前活动的，正在被授权服务处理的请求</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">ratelimit</span></div>
<p>此过滤器在ratelimit.&lt;stat_prefix&gt;下暴露以下指标：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">指标</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>total</td>
<td>Gauge，向限速服务发送的总请求数</td>
</tr>
<tr>
<td>error</td>
<td>Gauge，访问限速服务出错的次数</td>
</tr>
<tr>
<td>over_limit</td>
<td>Gauge，限速服务提示超过限制的次数</td>
</tr>
<tr>
<td>ok</td>
<td>Gauge，限速服务提示没有超过限制的次数/home/alex/CPP/projects/clion/envoy</td>
</tr>
<tr>
<td>cx_closed</td>
<td>Gauge，由于限速服务提示超限，而关闭的连接数量</td>
</tr>
<tr>
<td>active</td>
<td>Gauge，正在被限速服务处理的请求数量</td>
</tr>
<tr>
<td>failure_mode_allowed</td>
<td>Gauge，虽然超限，但是被允许的连接数</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">redis</span></div>
<p>此过滤器在redis.&lt;stat_prefix&gt;下暴露如下指标：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">指标</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>downstream_cx_active</td>
<td>Gauge，活动连接数</td>
</tr>
<tr>
<td>downstream_cx_protocol_error</td>
<td>Counter，总的协议错误数量</td>
</tr>
<tr>
<td>downstream_cx_rx_bytes_buffered</td>
<td>Gauge，当前缓冲的接收字节数</td>
</tr>
<tr>
<td>downstream_cx_rx_bytes_total</td>
<td>Counter，总计接收字节数</td>
</tr>
<tr>
<td>downstream_cx_total</td>
<td>Counter，总计连接数</td>
</tr>
<tr>
<td>downstream_cx_tx_bytes_buffered</td>
<td>Gauge，当前缓冲的发送字节数</td>
</tr>
<tr>
<td>downstream_cx_tx_bytes_total</td>
<td>Counter，总计发送字节数</td>
</tr>
<tr>
<td>downstream_cx_drain_close</td>
<td>Counter，由于Draing导致关闭的连接数</td>
</tr>
<tr>
<td>downstream_rq_active</td>
<td>Gauge，活动的请求数</td>
</tr>
<tr>
<td>downstream_rq_total</td>
<td>Counter，总请求数</td>
</tr>
</tbody>
</table>
<p> 此过滤器在 redis.&lt;stat_prefix&gt;.splitter 下暴露了命令分流器（command splitter）相关的指标：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">指标</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>invalid_request</td>
<td>Counter，参数数量不正确的请求数</td>
</tr>
<tr>
<td>unsupported_command</td>
<td>Counter，命令分流器不能识别的命令数量</td>
</tr>
</tbody>
</table>
<p>对于每个不同的Redis命令，此过滤器在 redis.&lt;stat_prefix&gt;.command.&lt;command&gt; 下暴露指标：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">指标</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>total</td>
<td>Counter，命令数量</td>
</tr>
<tr>
<td>success</td>
<td>Counter，成功数量</td>
</tr>
<tr>
<td>error</td>
<td>Counter，失败数量</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">tcp_proxy</span></div>
<p>此代理会在tcp.&lt;stat_prefix&gt;下产生下游、上游相关的统计指标：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">指标</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>downstream_cx_total</td>
<td>Counter，此过滤器处理的连接总数</td>
</tr>
<tr>
<td>downstream_cx_no_route</td>
<td>Counter，没有匹配路由、或者路由对应的集群不存在的连接数</td>
</tr>
<tr>
<td>downstream_cx_tx_bytes_total</td>
<td>Counter，写到下游的字节总数</td>
</tr>
<tr>
<td>downstream_cx_tx_bytes_buffered</td>
<td>Gauge，当前缓冲的准备发给下游集群的字节总数</td>
</tr>
<tr>
<td>downstream_cx_rx_bytes_total</td>
<td>Counter，从下游读取的字节总数</td>
</tr>
<tr>
<td>downstream_cx_rx_bytes_buffered</td>
<td>Gauge，从下游接收并正在缓冲中的字节总数</td>
</tr>
<tr>
<td>downstream_flow_control_paused_reading_total</td>
<td>Counter，流控制暂停从下游读取数据的次数</td>
</tr>
<tr>
<td>downstream_flow_control_resumed_reading_total</td>
<td>Counter，流控制恢复从下游读取数据的次数</td>
</tr>
<tr>
<td>idle_timeout</td>
<td>Counter，由于空闲而被关闭的连接数</td>
</tr>
<tr>
<td>upstream_flush_total</td>
<td>Counter，下游连接关闭后仍然向上游刷出数据的连接总数</td>
</tr>
<tr>
<td>upstream_flush_active</td>
<td>Gauge，下游连接关闭，正在向上游刷出数据的连接数</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">mysql_proxy</span></div>
<p>此过滤器在命名空间mysql.&lt;stat_prefix&gt;下暴露以下指标：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">指标</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>auth_switch_request</td>
<td>Counter，上游服务器要求客户端切换到其它身份验证模式的次数</td>
</tr>
<tr>
<td>decoder_errors</td>
<td>Counter，MySQL协议解码错误次数</td>
</tr>
<tr>
<td>login_attempts</td>
<td>Counter，登陆尝试次数</td>
</tr>
<tr>
<td>login_failures</td>
<td>Counter，登陆失败次数</td>
</tr>
<tr>
<td>protocol_errors</td>
<td>Counter，在会话中协议消息失序的次数</td>
</tr>
<tr>
<td>queries_parse_error</td>
<td>Counter，MySQL查询解析失败次数</td>
</tr>
<tr>
<td>queries_parsed</td>
<td>Counter，MySQL查询解析成功次数</td>
</tr>
<tr>
<td>sessions</td>
<td>Counter，从启动依赖创建的MySQL会话个数</td>
</tr>
<tr>
<td>upgraded_to_ssl</td>
<td>Counter，升级到SSL的会话/连接个数</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">HTTP过滤器</span></div>
<div class="blog_h3"><span class="graybg">cors</span></div>
<p>此过滤器在命名空间&lt;stat_prefix&gt;.cors.* 下暴露以下指标：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">指标</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>origin_valid</td>
<td>Counter，有效源计数</td>
</tr>
<tr>
<td>origin_invalid</td>
<td>Counter，无效源计数</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">ext_authz</span></div>
<p>此过滤器在命名空间cluster.&lt;route target cluster&gt;.ext_authz.下暴露以下指标：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">指标</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>ok</td>
<td>Counter，从此过滤器返回的应答总数</td>
</tr>
<tr>
<td>error</td>
<td>Counter，连接外部授权服务失败的次数</td>
</tr>
<tr>
<td>denied</td>
<td>Counter，外部授权服务认为请求无权的次数</td>
</tr>
<tr>
<td>failure_mode_allowed</td>
<td>Counter，由于failure_mode_allow=true，尽管出错仍然允许通过的请求数</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">fault</span></div>
<p>此过滤器在命名空间http.&lt;stat_prefix&gt;.fault. 下暴露以下指标：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">指标</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>delays_injected</td>
<td>Counter，总计注入延迟数量</td>
</tr>
<tr>
<td>aborts_injected</td>
<td>Counter，总计注入中断数量</td>
</tr>
<tr>
<td>&lt;downstream-cluster&gt;.delays_injected</td>
<td>Counter，某个特定下游集群总计注入延迟数量</td>
</tr>
<tr>
<td>&lt;downstream-cluster&gt;.aborts_injected</td>
<td>Counter，某个特定下游集群总计注入中断数量</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">rate_limit</span></div>
<p>此过滤器在命名空间 cluster.&lt;route target cluster&gt;.ratelimit 下暴露以下指标：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">指标</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>ok</td>
<td>Counter，未超限的请求数</td>
</tr>
<tr>
<td>error</td>
<td>Counter，调用限速服务错误的次数</td>
</tr>
<tr>
<td>over_limit</td>
<td>Counter，超限的请求数</td>
</tr>
<tr>
<td>failure_mode_allowed</td>
<td>Counter，请求限速服务失败，但是允许请求通过的次数</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">router</span></div>
<p>此过滤器在命名空间 http.&lt;stat_prefix&gt;下暴露以下指标，stat_prefix取决于所属的HTTP连接管理器：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">指标</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>no_route</td>
<td>Counter，由于没有路由而404的请求数</td>
</tr>
<tr>
<td>no_cluster</td>
<td>Counter，由于上游集群不存在而404的请求数</td>
</tr>
<tr>
<td>rq_redirect</td>
<td>Counter，导致重定向的请求数</td>
</tr>
<tr>
<td>rq_direct_response</td>
<td>Counter，导致直接应答的请求数</td>
</tr>
<tr>
<td>rq_total</td>
<td>Counter，总计被路由的请求数</td>
</tr>
</tbody>
</table>
<p>虚拟集群的统计信息暴露在&lt;virtual host name&gt;.vcluster.&lt;virtual cluster name&gt;，包含以下指标：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">指标</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>upstream_rq_&lt;*xx&gt;</td>
<td>Counter，聚合状态码计数，例如5xx</td>
</tr>
<tr>
<td>upstream_rq_&lt;*&gt;</td>
<td>Counter，具体状态码计数，例如500</td>
</tr>
<tr>
<td>upstream_rq_time</td>
<td>Histogram，请求耗时ms</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">运维和管理</span></div>
<div class="blog_h2"><span class="graybg">命令行选项</span></div>
<p>Envoy由一个JSON配置文件和一系列命令行选项一起驱动。命令行选项如下：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>-c</td>
<td>
<p>v2版本的JSON/YAML/Proto3格式的配置文件路径。如果此选项缺失，则必须提供--config-yaml</p>
<p>有效的扩展名包括.json, .yaml, .pb, .pb_text</p>
</td>
</tr>
<tr>
<td>--config-yaml</td>
<td>
<p>v2版本的YAML格式、JSON格式的Bootstrap配置</p>
<p>如果同时指定了--config-path，则当前选项的配置覆盖到--config-path指定的配置</p>
</td>
</tr>
<tr>
<td>--mode</td>
<td>
<p>运行模式：</p>
<ol>
<li>serve，默认，启动代理</li>
<li>validate，验证配置是否有效并退出</li>
</ol>
</td>
</tr>
<tr>
<td>--admin-address-path</td>
<td>管理地址/端口写入到什么文件中</td>
</tr>
<tr>
<td>--local-address-ip-version</td>
<td>
<p>服务本地IP地址使用v4还是v6</p>
</td>
</tr>
<tr>
<td>--base-id</td>
<td>
<p>共享内存区域的Base ID，Envoy在热重启时使用此区域</p>
<p>如果在一个节点上运行多个Envoy实例，则每个实例必须具有不同的Base ID</p>
</td>
</tr>
<tr>
<td>--concurrency</td>
<td>工作线程数量</td>
</tr>
<tr>
<td>-l</td>
<td>日志级别</td>
</tr>
<tr>
<td>--component-log-level</td>
<td>为每个组件定制日志级别</td>
</tr>
<tr>
<td>--log-path</td>
<td>日志记录未知，默认stderr</td>
</tr>
<tr>
<td>--log-format</td>
<td>日志格式，默认[%Y-%m-%d %T.%e][%t][%l][%n] %v</td>
</tr>
<tr>
<td>--service-cluster</td>
<td>定义Envoy所在节点的服务集群的名称</td>
</tr>
<tr>
<td>--service-node</td>
<td>定义Envoy所在节点的名称</td>
</tr>
<tr>
<td>--service-zone</td>
<td>定义Envoy所在的区域，区域的定义取决于上下文，例如AWS的可用性区域</td>
</tr>
<tr>
<td>--file-flush-interval-msec</td>
<td>文件（例如访问日志）刷出间隔ms，默认10秒</td>
</tr>
<tr>
<td>--drain-time-s</td>
<td>
<p>热重启时，等待多久Envoy会Drain掉连接。默认600s</p>
<p>通常应该小于--parent-shutdown-time-s</p>
</td>
</tr>
<tr>
<td>--parent-shutdown-time-s</td>
<td>热重启时，等待多久关闭主进程</td>
</tr>
<tr>
<td>--max-obj-name-len</td>
<td>cluster/route_config/listener的名字最大长度</td>
</tr>
<tr>
<td>--disable-hot-restart</td>
<td>禁用热重启</td>
</tr>
<tr>
<td>--allow-unknown-fields</td>
<td>用于禁用protobuf未知字段校验</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">管理接口</span></div>
<p>Envoy暴露了一个本地的管理接口，可以用于查询/修改Envoy的各个方面。端点列表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">端点</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>GET /</td>
<td>返回一个HTML页面，包含所有选项的链接</td>
</tr>
<tr>
<td>GET /help</td>
<td>所有选项的文本表格</td>
</tr>
<tr>
<td>GET /certs</td>
<td>列出加载的TLS证书</td>
</tr>
<tr>
<td>GET /clusters<br />GET /clusters?format=json</td>
<td>列出所有已经配置的集群管理器集群，包括每个集群的所有上游主机、每个主机的统计信息</td>
</tr>
<tr>
<td>GET /config_dump</td>
<td>以JSON形式导出当前配置</td>
</tr>
<tr>
<td>GET /contention</td>
<td>如果mutex争用跟踪被启用，导出Envoy mutex争用信息</td>
</tr>
<tr>
<td>POST /cpuprofiler</td>
<td>启用或禁用CPU剖析，需要启用gperftools编译</td>
</tr>
<tr>
<td>POST /healthcheck/fail</td>
<td>发起一个入站健康检查失败</td>
</tr>
<tr>
<td>POST /healthcheck/ok</td>
<td>发起一个入站健康检查成功</td>
</tr>
<tr>
<td>POST /logging</td>
<td>
<p>修改组件的日志级别</p>
<pre class="crayon-plain-tag"># 修改所有级别
curl -X POST localhost:15000/logging?level=trace
# 修改特定组件级别
curl -X POST localhost:15000/logging?main=trace</pre>
</td>
</tr>
<tr>
<td>POST /memor</td>
<td>显示内存、堆使用情况</td>
</tr>
<tr>
<td>POST /quitquitquit</td>
<td>立即停止服务</td>
</tr>
<tr>
<td>POST /reset_counters</td>
<td>重置所有计数器</td>
</tr>
<tr>
<td>GET /server_info</td>
<td>获取服务器信息</td>
</tr>
<tr>
<td>GET /stats<br />GET /stats?filter=regex<br />GET /stats?format=json</td>
<td>输出统计信息</td>
</tr>
<tr>
<td>GET /stats/prometheus</td>
<td>以Prometheus v0.04格式输出统计信息，目前仅仅输出Counter、Gauge</td>
</tr>
<tr>
<td>GET /runtime</td>
<td>输出所有运行时设置</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">xDS协议</span></div>
<p>Envoy可以从文件系统或者一个/多个管理服务器发现各种动态资源。这些管理服务器提供的发现服务，以及发现服务的API，被称为xDS。
<p>资源的通过<span style="background-color: #c0c0c0;">订阅（subscription）</span>的方式获得，订阅的初始化方式是 —— 指定需要监控的文件系统路径，初始化gRPC流，或者轮询REST-JSON URL。后两种方式需要发送DiscoveryRequest请求，资源则通过DiscoveryResponse返回。</p>
<p>本章主要讨论基于gRPC的流式订阅。</p>
<div class="blog_h2"><span class="graybg">版本</span></div>
<div class="blog_h3"><span class="graybg">v1</span></div>
<p>已经彻底废弃，基于REST-JSON的API。现在使用的是基于<span style="background-color: #c0c0c0;">REST/gRPC的xDS API</span></p>
<div class="blog_h3"><span class="graybg">v2</span></div>
<p>不赞成使用，2020 EOL</p>
<div class="blog_h3"><span class="graybg">v3</span></div>
<p>推荐使用此版本。仍然是基于gRPC的REST。</p>
<div class="blog_h2"><span class="graybg">单资源类型发现</span></div>
<p>对于每一个xDS API，你都可以指定一个ApiConfigSource：</p>
<pre class="crayon-plain-tag">{
  "api_type": "...",
  // 管理服务器集群的名称
  "cluster_names": [],
  "grpc_services": [],
  "refresh_delay": "{...}",
  "request_timeout": "{...}",
  "rate_limit_settings": "{...}"
}</pre>
<p>这样，可以为每种xDS资源类型发起独立的（可以和不同服务器建立）双向gRPC流。</p>
<div class="blog_h3"><span class="graybg">Type URLs</span></div>
<p>每种xDS API都操作某种类型的资源。xDS API和资源类型具有1:1对应关系：</p>
<ol>
<li>LDS: envoy.api.v2.Listener</li>
<li>RDS: envoy.api.v2.RouteConfiguration</li>
<li>CDS: envoy.api.v2.Cluster</li>
<li>EDS: envoy.api.v2.ClusterLoadAssignment</li>
<li>SDS: envoy.api.v2.Auth.Secret</li>
</ol>
<p>Envoy发送的多种请求、管理服务器返回的多种响应中，都需要声明Type URL。这些xDS API的Type URL的形式均为：type.googleapis.com/资源类型，例如type.googleapis.com/envoy.api.v2.Listener。</p>
<div class="blog_h3"><span class="graybg">ACK/NACK和版本化</span></div>
<p>每个gRPC流以来自Envoy的DiscoveryRequest请求开始，指定一系列需要订阅的资源、这些资源对应的Type URL、节点标识符、空白的version_info，示例：</p>
<pre class="crayon-plain-tag">version_info:
node: { id: envoy }
resource_names:
- foo
- bar
type_url: type.googleapis.com/envoy.api.v2.ClusterLoadAssignment
response_nonce:</pre>
<p>管理服务器可以立即应答，或者在请求的资源可用时进行应答，应答都封装在DiscoveryResponse中：</p>
<pre class="crayon-plain-tag">version_info: X
resources:
- foo ClusterLoadAssignment proto encoding
- bar ClusterLoadAssignment proto encoding
type_url: type.googleapis.com/envoy.api.v2.ClusterLoadAssignment
nonce: A</pre>
<p>处理完DiscoveryResponse之后，Envoy会在gRPC流上发送新请求，提供最后一次成功应用的版本号以及管理服务器提供的nonce。在上面的例子中，如果DiscoveryResponse成功应用，则后续请求的version_info为X， response_nonce为A。</p>
<p>如果Envoy无法应用DiscoveryResponse，则会回复error_detail和上一个版本号给管理服务器。</p>
<p>每个gRPC流具有自己的版本号，不存在跨资源共享的版本号机制。当不使用ADS时，每种资源的版本不一样很正常。</p>
<div class="blog_h3"><span class="graybg">何时发送更新</span></div>
<p>仅仅DiscoveryResponse中的资源发生变化后，管理服务器才应该发送更新给Envoy。Envoy会在应用/拒绝DiscoveryResponse后，发送包含ACK/NACK的DiscoveryRequest给管理服务器。</p>
<p>在单个流中，后面的DiscoveryRequest代替先前的、相同资源类型的DiscoveryRequest。如果管理服务器接收到多个DiscoveryRequest，它可能只需要回复最后一个。</p>
<div class="blog_h3"><span class="graybg">资源提示</span></div>
<p>DiscoveryRequest中的resource_names字段是一个“提示”。</p>
<p>Cluster、Listener等资源类型的resource_name字段为空，管理服务器应该每次都提供完整的LDS/CDS资源列表。<span style="background-color: #c0c0c0;">列表中不存在，但是Envoy中存在的资源会被删除</span>。Listener/Cluster对应的EDS/RDS资源会自动被级联删除。</p>
<p>对于EDS/RDS，管理服务器不需要提供每个请求的资源，另外它也可以提供没有请求的资源。resource_names仅仅是个提示。管理服务器能够从DiscoveryRequest的node（节点标识）字段推断出哪些资源是需要的。</p>
<div class="blog_h3"><span class="graybg">最终一致性</span></div>
<p>Envoy的xDS API是最终一致性的。在一次次DiscoveryResponse可能导致应用程序流量丢失。</p>
<p>考虑这样的一个场景，当前通过CDS/EDS仅仅获知了集群X的信息。一个引用X的RouteConfiguration，将X改为Y并更新，在Y集群的尚未通过CDS/EDS发现指前，流量都进入黑洞。</p>
<p>部分应用程序允许流量丢失，应用代码或者Sidecar中的重试可能隐藏这种流量丢失。另外一些应用则不能忍受流量丢失，这就要求管理服务器以一定的顺序推送更新。一般来说，避免流量丢失的推送时序如下：</p>
<ol>
<li>如果存在CDS更新，则必须首先推送</li>
<li>EDS更新必须在对应CDS更新之后推送</li>
<li>LDS更新必须在对应的CDS/EDS更新之后推送</li>
<li>和新添加监听器有关的RDS必须在最后推送</li>
</ol>
<p>如果没有新的集群/路由/监听器、或者可以容忍临时的流量丢失，则xDS更新可以独立的推送。</p>
<div class="blog_h2"><span class="graybg">ADS</span></div>
<p>前面提到了最终一致性问题和解决流量丢失的手段。在分布式管理服务器场景下，保证更新的时序是很困难的。</p>
<p>ADS允许<span style="background-color: #c0c0c0;">单个管理服务器</span>通过<span style="background-color: #c0c0c0;">单个gRPC流</span>来推送<span style="background-color: #c0c0c0;">所有的API更新</span>。使用ADS，管理服务器可以小心编排推送时序，避免流量丢失。多个DiscoveryRequest/DiscoveryResponse序列可以基于Type URL进行gRPC流的多路复用，并且保证上节描述的推送时序</p>
<div class="blog_h2"><span class="graybg">增量xDS</span></div>
<p>Incremental xDS是ADS/CDS/RDS的独立的xDS端点，允许：</p>
<ol>
<li>增量式的更新被xDS客户端跟踪的资源列表。这允许Envoy按需/延迟的请求额外的资源。例如当一个针对位置集群的请求到达时，触发对此集群的更新</li>
<li>增强xDS可扩容性，避免单个集群信息变更就被迫推送100K的完整集群列表</li>
</ol>
<p>Incremental xDS仅仅支持gRPC，增量xDS会话总是在单个gRPC双向流的上下文中发生，xDS服务器可以跟踪每个连接到它的xDS客户端的状态，以决定什么需要推送。</p>
<p>增量xDS使用nonce字段将 IncrementalDiscoveryResponse和IncrementalDiscoveryRequest的ACK/NACK进行配对。 </p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/envoy-study-note">Envoy学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/envoy-study-note/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Istio学习笔记</title>
		<link>https://blog.gmem.cc/istio-study-note</link>
		<comments>https://blog.gmem.cc/istio-study-note#comments</comments>
		<pubDate>Mon, 10 Sep 2018 06:36:45 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[K8S]]></category>
		<category><![CDATA[ServiceMesh]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=22611</guid>
		<description><![CDATA[<p>简介 服务网格是什么 术语服务网格（Service Mesh）用于描述微服务之间的网络，以及通过此网络进行的服务之间的交互。随着服务数量和复杂度的增加，服务网格将变的难以理解和管理。 对服务网格的需求包括：服务发现、负载均衡、故障恢复、指标和监控，以及A/B测试、金丝雀发布、限速、访问控制、端对端身份验证等。 Istio是什么 使用云平台给DevOps团队带来了额外的约束，为了Portability开发人员通常需要使用微服务架构，运维人员则需要管理非常多数量的服务。Istio能够连接、保护、控制、观察这些微服务。 Istio是运行于分布式应用程序之上的透明（无代码入侵）服务网格，它同时也是一个平台，提供集成到其它日志、监控、策略系统的接口。 Istio的实现原理是，为每个微服务部署一个Sidecar，代理微服务之间的所有网络通信。在此基础上你可以通过Istio的控制平面实现： 针对HTTP、gRPC、WebSocket、TCP流量的负载均衡 细粒度的流量控制行为，包括路由、重试、故障转移、故障注入（fault injection） 可拔插的策略层+配置API，实现访问控制、限速、配额 自动收集指标、日志，跟踪集群内所有流量，包括Ingress/Egress 基于强身份认证和授权来保护服务之间的通信 Istio核心特性 流量管理 使用Istio你可以很容易的通过配置，对流量和API调用进行控制。服务级别的可配置属性包括断路器（circuit breakers）、超时、重试。 Istio支持基于流量百分比切分的A/B测试、金丝雀滚动发布、分阶段滚动发布。 安全性 可以提供安全信道，管理身份验证和授权，加密通信流量。 <a class="read-more" href="https://blog.gmem.cc/istio-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/istio-study-note">Istio学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<div class="blog_h2"><span class="graybg">服务网格是什么</span></div>
<p>术语服务网格（Service Mesh）用于描述微服务之间的网络，以及通过此网络进行的服务之间的交互。随着服务数量和复杂度的增加，服务网格将变的难以理解和管理。</p>
<p>对服务网格的需求包括：<span style="background-color: #c0c0c0;">服务发现、负载均衡、故障恢复、指标和监控</span>，以及<span style="background-color: #c0c0c0;">A/B测试、金丝雀发布、限速、访问控制、端对端身份验证</span>等。</p>
<div class="blog_h2"><span class="graybg">Istio是什么</span></div>
<p>使用云平台给DevOps团队带来了额外的约束，为了Portability开发人员通常需要使用<span style="background-color: #c0c0c0;">微服务架构</span>，运维人员则需要管理非常多数量的服务。Istio能够连接、保护、控制、观察这些微服务。</p>
<p>Istio是运行于分布式应用程序之上的<span style="background-color: #c0c0c0;">透明（无代码入侵）服务网格</span>，它同时也是一个平台，提供集成到其它日志、监控、策略系统的接口。</p>
<p>Istio的实现原理是，为每个微服务部署一个Sidecar，代理微服务之间的所有网络通信。在此基础上你可以通过Istio的控制平面实现：</p>
<ol>
<li>针对<span style="background-color: #c0c0c0;">HTTP、gRPC、WebSocket、TCP</span>流量的负载均衡</li>
<li>细粒度的流量控制行为，包括<span style="background-color: #c0c0c0;">路由、重试</span>、故障转移、故障注入（fault injection）</li>
<li>可拔插的策略层+配置API，实现访问控制、<span style="background-color: #c0c0c0;">限速、配额</span></li>
<li>自动<span style="background-color: #c0c0c0;">收集指标、日志，跟踪集群内所有流量</span>，包括Ingress/Egress</li>
<li>基于强身份认证和授权来保护服务之间的通信</li>
</ol>
<div class="blog_h2"><span class="graybg">Istio核心特性</span></div>
<div class="blog_h3"><span class="graybg">流量管理</span></div>
<p>使用Istio你可以很容易的通过配置，对流量和API调用进行控制。服务级别的可配置属性包括断路器（circuit breakers）、超时、重试。</p>
<p>Istio支持基于<span style="background-color: #c0c0c0;">流量百分比切分</span>的A/B测试、金丝雀滚动发布、分阶段滚动发布。</p>
<div class="blog_h3"><span class="graybg">安全性</span></div>
<p>可以提供安全信道，管理身份验证和授权，加密通信流量。</p>
<p>联用K8S的网络策略可以获得更多益处，例如保护Pod-to-Pod之间的通信。</p>
<div class="blog_h3"><span class="graybg">可观察性</span></div>
<p>Istio强大的跟踪、监控、日志能力，让服务网格内部结构更容易观察 —— 一个服务的<span style="background-color: #c0c0c0;">性能对上下游的影响</span>可以直观的展现在仪表盘上。</p>
<p>Istio的<span style="background-color: #c0c0c0;">Mixer组件——通用的策略和监控（Telemetry）中心（Hub）——</span>负责策略控制、指标收集。Mixer提供<span style="background-color: #c0c0c0;">后端基础设施（例如K8S）的隔离层</span>，Istio的其它部分不需要关注后端细节。</p>
<div class="blog_h3"><span class="graybg">后端支持</span></div>
<p>Istio当前支持：</p>
<ol>
<li>Kubernetes上的Service</li>
<li>通过Consul注册的服务</li>
<li>在独立虚拟机上运行的服务</li>
</ol>
<div class="blog_h2"><span class="graybg">架构</span></div>
<p>从整体上看，Istio的服务网格由<span style="background-color: #c0c0c0;">数据平面、控制平面</span>两部分组成：</p>
<ol>
<li>数据平面由一系列作为Sidecar部署的<span style="background-color: #c0c0c0;">智能代理（Envoy）</span>构成。这些代理<span style="background-color: #c0c0c0;">联合Mixer</span>，<span style="background-color: #c0c0c0;">中继、控制</span>所有微服务之间的网络通信。需要注意，还有一些Envoy是独立部署（而非Sidecar）的，用来实现K8S Ingress控制器、Istio的Ingress/Egress网关</li>
<li>控制平面负责<span style="background-color: #c0c0c0;">管理、配置智能代理</span>，实现流量路由；配置Citadel实现TLS证书管理；配置<span style="background-color: #c0c0c0;">Mixers来应用策略、收集指标</span></li>
</ol>
<p>架构图：</p>
<p><img class="aligncenter size-full wp-image-22625" src="https://blog.gmem.cc/wp-content/uploads/2018/09/istio-arch.png" alt="istio-arch" width="877" height="654" /></p>
<div class="blog_h3"><span class="graybg">Envoy</span></div>
<p>Istio使用一个<span style="background-color: #c0c0c0;">扩展过的Envoy版本</span>。Envoy是基于C++开发的高性能代理，Istio使用它的以下特性：</p>
<ol>
<li>动态服务发现</li>
<li>负载均衡</li>
<li>TLS termination —— 可将后端的HTTP服务包装为HTTPS</li>
<li>HTTP/2和gRPC代理</li>
<li>断路器</li>
<li>健康检查</li>
<li>分阶段（基于流量百分比）发布</li>
<li>故障注入</li>
<li>丰富的监控指标</li>
</ol>
<p>一般情况下Envoy在和目标服务的相同Pod中，以Sidecar形式部署。少量的Istio组件的主进程就是Envoy，包括Ingress控制器、Ingress/Egress网关。</p>
<div class="blog_h3"><span class="graybg">Mixer</span></div>
<p>一个平台无关的组件：</p>
<ol>
<li>为服务网格应用访问控制策略</li>
<li>从Envoy和其它服务中收集指标</li>
<li>Envoy收集的请求级别的属性，被发送到Mixer进行分析</li>
</ol>
<p>Mixer提供了一个灵活的插件模型，让Istio能够灵活的和多种宿主机环境、基础设施后端进行对接。</p>
<div class="blog_h3"><span class="graybg">Pilot</span></div>
<p><span style="background-color: #c0c0c0;">该组件是Istio的控制器</span>，它会监控各种规则、策略（通常存储在K8S中），一旦配置文件发生变化，就会提取、处理，并同步给Envoy：</p>
<ol>
<li>为Envoy提供服务发现</li>
<li>为智能路由（AB测试、金丝雀部署……）提供流量管理能力</li>
<li>提供弹性（Resiliency）——超时、重试、断路器等</li>
<li>分发身份验证策略给Envoy</li>
</ol>
<p>Pilot将<span style="background-color: #c0c0c0;">高级别的路由规则转换为Envoy理解的配置信息</span>，并在运行时将这些配置传播到Sidecars。</p>
<p>Pilot将<span style="background-color: #c0c0c0;">平台相关的服务发现机制抽象为标准</span>的（Envoy data plane API，xDS）格式，这让Istio可以在K8S、Consul、Nomad等多种环境下运行。</p>
<div class="blog_h3"><span class="graybg">Citadel</span></div>
<p><span style="background-color: #c0c0c0;">负责证书、密钥的管理</span>。提供服务-服务之间、或者针对终端用户的身份验证功能，可以加密服务网格中的流量。</p>
<div class="blog_h2"><span class="graybg">Istio的性能</span></div>
<p>部署服务网格带来了额外的性能损耗，下面是Istio组件的推荐资源配置：</p>
<ol>
<li>对于<span style="background-color: #c0c0c0;">启用了访问日志（默认启用）</span>的Sidecar，<span style="background-color: #c0c0c0;">每1000请求/s可以配备1个vCPU</span>，如果没有启用访问日志则配备<span style="background-color: #c0c0c0;">0.5个vCPU</span></li>
<li>节点上的Fluentd是资源消耗大户，它会捕获并上报日志，如果不关心可以排除Sidecar日志不上报</li>
<li>在大部分情况下，Mixer Check有超过80%的缓存命中率，在此前提下，可以为Mixer的Pod每1000请求/s配备<span style="background-color: #c0c0c0;">0.5个vCPU</span></li>
<li>由于在通信两端都<span style="background-color: #c0c0c0;">引入了Sidecar代理、并且需要上报Mixer</span>，大概会引入<span style="background-color: #c0c0c0;">10ms的延迟</span></li>
</ol>
<div class="blog_h2"><span class="graybg">Istio的HA</span></div>
<p><span style="background-color: #c0c0c0;">任何控制平面组件都支持多实例部署</span>，在K8S中可以考虑配置HPA。</p>
<p>考虑将Mixer的check、report功能分开部署。当前官方Chart已经分开，分别对应istio-policy、istio-telemetry两个Deployment。</p>
<div class="blog_h1"><span class="graybg">术语列表</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">术语</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Service</td>
<td>
<p>一个应用程序“行为单元”，它对应服务注册表（例如K8S的集群服务DNS）中的唯一的名称</p>
<p>服务具有0-N个网络<span style="background-color: #c0c0c0;">端点（Endpoint）</span>，这些端点可以是Pod、容器、虚拟机</p>
</td>
</tr>
<tr>
<td>Service versions</td>
<td>
<p>也叫subsets，这是持续部署（CD）中的概念</p>
<p>在持续部署时，网格中可能运行着某个微服务的多种变体，这些<span style="background-color: #c0c0c0;">变体的二进制代码不同，但是API版本不一定不同</span>。这些变体代表了对某个服务的迭代性的改变，部署在不同的环境中</p>
</td>
</tr>
<tr>
<td>Source</td>
<td>访问当前服务的下游（downstream）客户端</td>
</tr>
<tr>
<td>Host</td>
<td>客户端连接到服务器时所使用的DNS名称</td>
</tr>
<tr>
<td>Destination</td>
<td>表示一个可寻址的服务，请求/连接在经过路由处理后，将发送到Destination</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">新特性</span></div>
<div class="blog_h2"><span class="graybg">1.6版本</span></div>
<ol>
<li>进一步完善istiod（单体化）</li>
<li>可观察性增强：更多配置项、对追踪采样率的控制、更新的Grafana仪表盘</li>
<li>更好的虚拟机支持（未来将更进一步强化）：WorkloadEntry资源让非K8S工作负载成为Istio的一等公民，物理机或者虚拟机和Pod现在处于同一级别。你可以定义<span style="background-color: #c0c0c0;">同时由Pod和物理机Back的服务</span>。这让逐步迁移到K8S提供便利</li>
</ol>
<div class="blog_h2"><span class="graybg">1.5版本</span></div>
<div class="blog_h3"><span class="graybg">单体化</span></div>
<p>1.5版本的最大变化是，控制平面的大部分组件（pilot、citadel、sidecar-injector）合并到名为istiod的单体应用中，现在istiod负责：</p>
<ol>
<li>配置处理和推送</li>
<li>证书分发</li>
<li>Sidecar注入</li>
</ol>
<p>以下组件被移除：</p>
<ol>
<li>SDS的node-agent（合并到Pilot Agent）</li>
</ol>
<div class="blog_h3"><span class="graybg">废弃Mixer</span></div>
<p>Mixer被废弃，但是仍然可以使用。</p>
<p>新版本HTTP遥测基于Envoy过滤器Stats filter实现，可以节省50%的CPU用量。</p>
<div class="blog_h3"><span class="graybg">流量管理</span></div>
<ol>
<li>提升了 ServiceEntry 的性能。</li>
<li>修复了 readiness 探针不一致问题</li>
<li>通过<span style="background-color: #c0c0c0;">定向局部更新的方式改善了配置更新的性能</span></li>
<li>添加了为 host 设置所在负载均衡器设置的选项</li>
<li>修复了 Pod 崩溃会触发过度配置推送的问题</li>
<li>添加了使用 Istio CNI 时对 iptables 的探测</li>
<li>添加了 consecutive_5xx 和 gateway_errors 作为离群值探测选项。</li>
<li>提升了 EnvoyFilter 匹配性能</li>
<li>添加了对 HTTP_PROXY 协议的支持</li>
<li>改进了 iptables 设置，默认使用 iptables-restore</li>
<li>默认开启<span style="background-color: #c0c0c0;">自动协议探测</span></li>
</ol>
<div class="blog_h3"><span class="graybg">安全</span></div>
<ol>
<li>添加 Beta 认证 API。新 API 分为 PeerAuthentication 和 RequestAuthenticaiton，面向工作负载</li>
<li>添加认证策略，支持 deny 操作和语义排除</li>
<li>Beta 版本默认开启自动 mTLS</li>
<li>稳定版添加 SDS</li>
<li>Node agent 和 Pilot agent 合并，移除了 Pod 安全策略的需要，提升了安全性</li>
<li>合并 Citadel 证书发放功能到 Pilot</li>
<li>支持 Kubernetes first-party-jwt 作为集群中 CSR 认证的备用 token</li>
<li>通过 Istio Agent 向 Prometheus 提供密钥和证书</li>
<li>支持 Citadel 提供证书给控制平面</li>
</ol>
<div class="blog_h3"><span class="graybg">遥测</span></div>
<ol>
<li>为 v2 版本的遥测添加 TCP 协议支持</li>
<li>在指标和日志中支持添加 gRPC 响应状态码</li>
<li>改进 v2 遥测流程的稳定性</li>
<li>为 v2 遥测的可配置性提供 alpha 级别的支持</li>
<li>支持在 Envoy 节点的元数据中添加 AWS 平台的元数据</li>
<li>更新了 Mixer 的 Stackdriver 适配器，以支持可配置的刷新间隔来跟踪数据</li>
<li>支持对 Jaeger 插件的 headless 收集服务</li>
<li>修复了 kubernetesenv 适配器以提供对名字中有.的 Pod 的支持</li>
<li>改进了 Fluentd 适配器，在导出的时间戳中提供毫秒级输出</li>
</ol>
<div class="blog_h2"><span class="graybg">1.4版本</span></div>
<p>仍然以用户体验改进为主，简化配置难度。</p>
<div class="blog_h3"><span class="graybg">无Mixer遥测</span></div>
<p>可以简化安装、Mesh的操控，并很大程度上改善性能。</p>
<p>istio-proxy内部生成HTTP指标的特性，从试验升级为Alpha。</p>
<p>istio-proxy内部生成TCP指标（试验）</p>
<div class="blog_h3"><span class="graybg">自动mTLS</span></div>
<p>不需要配置DestinationRule即可自动启用。</p>
<div class="blog_h2"><span class="graybg">1.3版本</span></div>
<p>该版本主要专注于用户体验的改善。</p>
<div class="blog_h3"><span class="graybg">智能协议检测（试验）</span></div>
<p>为了使用Istio的路由特性，服务端口名必须遵循特定的命名约定，来声明其使用的协议。</p>
<p>从1.3开始，出站流量的协议可以自动识别为TCP或HTTP，这意味着服务端口命名协定不再必须</p>
<div class="blog_h3"><span class="graybg">无Mixer遥测（试验）</span></div>
<p>现在大部分的安全策略（例如RBAC）已经直接在Envoy里面实现了。</p>
<p>istio-proxy现在可以直接向Prometheus发送数据，而不需要istio-telemetry来丰富、完善信息。</p>
<div class="blog_h3"><span class="graybg">对生成的Envoy配置的定制能力</span></div>
<p>对于Envoy中的高级特性，Istio可能尚未提供对应API。你现在可以基于EnvoyFilter API来定制：</p>
<ol>
<li>LDS返回的HTTP/TCP监听器，及其过滤器链</li>
<li>RDS返回的Envoy HTTP路由配置</li>
<li>CDS返回的上游集群</li>
</ol>
<div class="blog_h2"><span class="graybg">1.2版本</span></div>
<div class="blog_h3"><span class="graybg">一般特性</span></div>
<ol>
<li>添加注解<pre class="crayon-plain-tag">traffic.sidecar.istio.io/includeInboundPorts</pre>，不再强制要求服务在deployment的YAML中声明contrainerPort</li>
<li>添加IPv6的试验性支持</li>
</ol>
<div class="blog_h3"><span class="graybg">流量管理</span></div>
<ol>
<li>在多集群环境下，优化了基于位置的路由</li>
<li>优化了ALLOW_ANY模式（即允许任何未知的出站流量，配置位于configmap istio中）下的出站流量策略。针对<span style="background-color: #c0c0c0;">未知HTTP/HTTPS主机+已知端口</span>的流量，会<span style="background-color: #c0c0c0;">原样</span>的转发</li>
<li>支持设置针对上游服务的idle超时</li>
<li>改进了NONE模式（不用iptables，不进行流量捕获）的Sidecar支持</li>
<li>可以配置Sidecar的Envoy的DNS刷新率，防止DNS服务器过载</li>
</ol>
<div class="blog_h3"><span class="graybg">安全方面</span></div>
<ol>
<li>扩展自签名Citadel根证书有效期为10年</li>
<li>可以为Deployment添加PodSpec注解<pre class="crayon-plain-tag">sidecar.istio.io/rewriteAppHTTPProbers: "true"</pre>，来重写健康探针</li>
</ol>
<div class="blog_h3"><span class="graybg">遥感方面</span></div>
<ol>
<li>对Envoy统计信息生成的完全控制，基于注解控制</li>
<li>Prometheus生成的不包含在指标中</li>
</ol>
<div class="blog_h2"><span class="graybg">1.1版本</span></div>
<div class="blog_h3"><span class="graybg">性能方面</span></div>
<p>控制平面性能：Pilot的CPU、内存消耗受网格的配置变化情况、工作负载变化情况、连接到Pilot的代理数量的影响。<span style="background-color: #c0c0c0;">增加Pilot的实例数来可以减少配置分发处理的时间</span>，提高性能。网格中包含1000服务、2000工作负载、1000QPS的情况下，单个Pilot实例消耗1个vCPU和1.5GB内存。</p>
<p>数据平面性能：</p>
<ol>
<li>CPU，代理在1000QPS下大概消耗0.6个vCPU</li>
<li>内存，代理的内存消耗取决于它需要处理的配置数量，大量的Listener、Cluster、Route会增加内存使用。此外，代理在1000QPS下需要消耗50MB内存</li>
<li>延迟，请求需要同时经过客户端/服务器的sidecar，在<span style="background-color: #c0c0c0;">1000QPS下P99大概是10ms级别</span>。启用策略检查（Check）会额外增加延迟。L4过滤器的逻辑比较简单，因此TCP流量不会引入显著延迟</li>
</ol>
<div class="blog_h3"><span class="graybg">安装部署方面</span></div>
<ol>
<li>CRD从Istio的Chart中独立出来，放入istio-init这个Chart。这避免了升级Istio而丢失CR数据</li>
<li>添加几个安装配置Profile，这些配置提供了常用的安装模式，简化了Istio的安装过程</li>
<li>增强了多集群集成</li>
</ol>
<div class="blog_h3"><span class="graybg">流量管理方面</span></div>
<ol>
<li>新的资源类型Sidecar。用于对工作负载的<span style="background-color: #c0c0c0;">边车代理进行细粒度的控制</span>，特别是可以限制边车能够向哪些服务发送流量。此资源可以减少需要计算和传输给边车代理的配置信息，提升启动速度，减少资源消耗，提升控制平面可扩容性。在大型集群中，建议为每个命名空间提供一个Sidecar资源</li>
<li>支持限制服务（ServiceEntry、VirtualService）的<span style="background-color: #c0c0c0;">可见范围</span>。exportTo可用于限制哪些命名空间能够访问本服务。除了在Istio CR上指定exportTo以外，你还可以在K8S的Service上使用networking.istio.io/exportTo注解</li>
<li>Gateway引用VirtualService时，可能存在<span style="background-color: #c0c0c0;">歧义</span>，因为不同命名空间可能定义具有相同hostname的VirtualService。在1.1版本中，你可以用<span style="background-color: #c0c0c0;">namespace/hostname-match</span>的形式来来设置hosts字段，以避免歧义。类似的在Sidecar中你也可以为egress配置为这种形式</li>
<li>支持设置ServiceEntry的Locality，以及关联的SAN（用于mTLS）。现在使用HTTPS端口的ServiceEntry不再需要配套的VirtualService来工作</li>
<li><span style="background-color: #c0c0c0;">位置感知路由</span>（Locality-Aware Routing），优先路由到位于当前地理位置（Locality）的服务端点</li>
<li>简化了多集群模式的安装、支持额外的部署模式。现在可以用Ingress网关连接多个集群，而不需要Pod级别的VPN。为了HA可以在各集群中部署控制平面，这种部署方式下位置感知路由自动开启。可以将命名空间扩展到多个集群，以创建全局命名空间</li>
<li>Istio的Ingress组件被废弃</li>
<li>性能和可扩容性得到提升</li>
<li>默认关闭访问日志</li>
</ol>
<div class="blog_h3"><span class="graybg">安全方面</span></div>
<ol>
<li>健康检查探针，当启用mTLS的情况下，支持Kubernetes的Readiness/Liveness探针。当启用mTLS后，进入Envoy的HTTP探针请求会被拒绝，1.1能够自动进行HTTP探针的重写，将探针请求转发给pilot-agent，并由后者直接转发给工作负载，绕开Envoy的TLS认证</li>
<li>集群RBAC配置，将RbacConfig资源替换为ClusterRbacConfig资源</li>
<li>基于SDS的身份配置（Identity Provisioning ），不需要重启Envoy即可实现动态证书轮换，实现on-node的密钥生成增强了安全性</li>
<li>在HTTP/gRPC的基础上，支持TCP服务的授权</li>
<li>支持基于终端用户组的授权</li>
<li>Ingress网关控制器支持外部证书管理，新的控制器用于支持动态的加载、轮换外部证书</li>
<li>定制PKI集成，支持Vault PKI，基于Vault CA签发证书</li>
</ol>
<div class="blog_h3"><span class="graybg">策略和遥感方面</span></div>
<ol>
<li>策略检查默认关闭，主要是出于性能方面的考虑</li>
<li>性能的增强：
<ol>
<li>极大的<span style="background-color: #c0c0c0;">减少了Envoy默认生成的统计信息</span>的收集</li>
<li>为Mixer工作负载添加了负载限制的功能</li>
<li>改善了Mixer和Envoy之间的交互协议</li>
</ol>
</li>
<li>适配器现在能够影响入站请求的头和路由</li>
<li>进程外适配器，生产级可用，进程内适配器被废弃</li>
<li>添加了默认的用于跟踪TCP连接的指标</li>
</ol>
<div class="blog_h3"><span class="graybg">多集群支持</span></div>
<p>在1.0版本Istio只提供了一种基于<span style="background-color: #c0c0c0;">扁平网络的多集群方案</span>：Istio<span style="background-color: #c0c0c0;">控制平面部署在其中单个Kubernetes集群</span>中。这种方案要求<span style="background-color: #c0c0c0;">各集群的 Pod 地址范围不能重叠</span>，且<span style="background-color: #c0c0c0;">所有的 Kubernetes 控制平面API Server 互通</span>。看上去就是物理上把多个Kubernetes并到一个Istio控制面下，<span style="background-color: #c0c0c0;">在Istio看来是同一个网格</span>。这种方案的网络要求苛刻，实际应用并不多。</p>
<p>1.1版本对多集群上做了非常多的增强，除了<span style="background-color: #c0c0c0;">保留1.0扁平网络</span>作为一种单控制面的方案外，还提出了另外两种方案供用户根据实际环境和需求灵活选择，这两种方案都不要求是扁平网络：</p>
<ol>
<li>多控制平面方案：在<span style="background-color: #c0c0c0;">每个集群中安装完整的Istio控制平面</span>，可以看成是一种松散的联邦，<span style="background-color: #c0c0c0;">集群间服务在Gateway处联通即可</span>。通过一个<span style="background-color: #c0c0c0;">全局的DNS将跨集群的请求路由到另外一个集群里去</span>。这种<span style="background-color: #c0c0c0;">集群的访问是基于Istio的ServiceEntry和Gateway来实现</span>的，配置较多且<span style="background-color: #c0c0c0;">复杂</span>，需用户自己维护</li>
<li>一种<span style="background-color: #c0c0c0;">集群感知（Split Horizon EDS）的单控制平面方案</span>：Istio控制平面只在一个Kubernetes集群中安装，Isti<span style="background-color: #c0c0c0;">o控制平面仍然需要连接所有Kubernetes集群的K8S API Serve</span>r。<span style="background-color: #c0c0c0;">每个集群都有集群标识和网络标识</span>。在服务间访问时，如果目标是本集群的负载，则类似单集群的方式直接转发；如果是其他<span style="background-color: #c0c0c0;">集群的实例，则将流量转发到集群的入口网关上，再经过网关转发给实际负载</span></li>
</ol>
<div class="blog_h3"><span class="graybg">exportTo字段</span></div>
<p>在1.1版本中添加了一个重要字段exportTo。用于控制VirtualService、DestinationRule和 ServiceEntry 跨Namespace的可见性。这样就可以控制一个Namespace下定义的资源是否可以被其他Namespace下的Envoy访问。</p>
<p>如果<span style="background-color: #c0c0c0;">不设置exportTo则默认全局可见</span>。目前exportTo仅仅支持两个取值：</p>
<p style="padding-left: 30px;">"."，表示仅仅当前命名空间可以使用当前资源</p>
<p style="padding-left: 30px;">"*"，表示任何命名空间都可以使用当前资源</p>
<p>如果服务对Pod不<span style="background-color: #c0c0c0;">可见，则Istio不会为该Pod的Envoy生成Listener、Cluster等信息</span>，因而可以减少内存消耗。</p>
<div class="blog_h3"><span class="graybg"><a id="sidecar-cr"></a>sidecar资源</span></div>
<p>Sidecar是具有命名空间的资源。每个Sidecar应用到命名空间的一个和多个工作负载，工作负载的选择通过workloadSelector进行，如果不指定workloadSelector（每个命名空间只能有一个这样的Sidecar）则Sidecar应用到命名空间的所有（没有被其它带有workloadSelector的Sidecar匹配的）工作负载。</p>
<p>如果命名空间包含多个没有workloadSelector的Sidecar，或者多个Sidecar的workloadSelector匹配同一工作负载，则网格的行为是未定义的。</p>
<p>配置示例一，允许prod-us1命名空间的Pod发起针对prod-us1, prod-apis, istio-system命名空间的公共服务的egress流量：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: Sidecar
metadata:
  name: default
  namespace: prod-us1
spec:
  # 需要通信的服务白名单，可以很大程度上减少Envoy内存消耗
  # 经过测试，创建此资源后，istioctl proxy-config listener PODNAME 输出立刻变得非常少
  # 但是并没有禁止对外访问，可能和ALLOW_ANY有关？
  egress:
  - hosts:
    # 支持namespace/hostname-match形式的host规则
    - "prod-us1/*"
    - "prod-apis/*"
    - "istio-system/*"
    # 当前命名空间
    - "./ratings.default.svc.cluster.local"</pre>
<p>配置示例二：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: Sidecar
metadata:
  name: default
  namespace: prod-us1
spec:
  # 入站流量规则
  ingress:
  # 对于从9080端口入站的HTTP流量，将其转发给Sidecar关联的工作负载监听的UDS
  - port:
      number: 9080
      protocol: HTTP
      name: somename
    defaultEndpoint: unix:///var/run/someuds.sock
  # 出站流量规则
  egress:
  # 允许针对istio-system命名空间的出站流量
  - hosts:
    - "istio-system/*"
  # 允许针对prod-us1命名空间的9080端口的HTTP流量
  - port:
      number: 9080
      protocol: HTTP
      name: egresshttp
    hosts:
    - "prod-us1/*"</pre>
<p>配置示例三，如果<span style="background-color: #c0c0c0;">工作负载没有启用基于Iptables的流量劫持，则Sidecar资源是唯一的配置工作负载边车端口的途径</span>：</p>
<pre class="crayon-plain-tag"># 假设匹配的目标工作负载没有设置Iptables规则（没有istio-init容器）并且代理元数据ISTIO_META_INTERCEPTION_MODE设置为NONE
apiVersion: networking.istio.io/v1alpha3
kind: Sidecar
metadata:
  name: no-ip-tables
  namespace: prod-us1
spec:
  # 启用此规则的工作负载选择器
  workloadSelector:
    labels:
      app: productpage
  # 入站流量规则
  ingress:
  - port:
      # 让边车绑定 0.0.0.0:9080
      number: 9080
      protocol: HTTP
      name: somename
    # 并且将9080的流量转发给工作负载的8080端口
    defaultEndpoint: 127.0.0.1:8080
    # 如果为整个代理设置了元数据则不需要
    captureMode: NONE
  # 出站流量规则
  egress:
  # 让边车绑定 127.0.0.1:3306，并且将流量转发给外部的MySQL服务mysql.gmem.cc:3306
  - port:
      number: 3306
      protocol: MYSQL
      name: egressmysql
    captureMode: NONE
    bind: 127.0.0.1
    hosts:
    # 外部服务，命名空间部分写*
    - "*/mysql.gmem.cc"


# 外部服务用ServiceEntry来定义
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: external-svc-mysql
  namespace: ns1
spec:
  hosts:
  - mysql.gmem.cc
  ports:
  - number: 3306
    name: mysql
    protocol: MYSQL
  location: MESH_EXTERNAL
  resolution: DNS</pre>
<p>字段captureMode指定流向监听器的流量如何被捕获（劫持），仅仅当监听器绑定到IP地址的情况下此字段才有意义：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 100px; text-align: center;">取值</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>DEFAULT</td>
<td>由环境定义的默认捕获模式</td>
</tr>
<tr>
<td>IPTABLES</td>
<td>基于Iptables重定向进行捕获</td>
</tr>
<tr>
<td>NONE</td>
<td>
<p>不进行流量捕获，当用在：</p>
<ol>
<li>egress监听器中时，应用程序被期望明确的和监听的端口/UDS进行直接通信</li>
<li>ingress监听器中时，必须保证工作负载的其它进程不会占用目标监听端口</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">位置感知路由</span></div>
<p>Istio支持使用三元组：Region、Zone、Sub-zone来描述网格的地理位置，地理位置通常精确到某个数据中心。Istio能够使用此地理位置信息来对负载均衡池进行优先级控制。</p>
<p>在1.1版本中，基于地理位置的负载均衡仍然是试验特性，且默认关闭。设置环境变量PILOT_ENABLE_LOCALITY_LOAD_BALANCING到所有Pilot实例以开启此特性。</p>
<p>服务发现平台负责自动产生地理位置信息，在K8S中，Pod的地理位置基于它所在Node的知名标签来获取。在云环境下，Node的这些标签会自动设置，否则你需要手工设置Node标签。这些标签包括：</p>
<p style="padding-left: 30px;">failure-domain.beta.kubernetes.io/region</p>
<p style="padding-left: 30px;">failure-domain.beta.kubernetes.io/zone</p>
<p>注意，K8S没有sub-zone的概念。</p>
<p>基于地理位置的负载均衡，其默认模式是地理位置优先负载均衡（locality-prioritized load balancing ）。Istio会提示Envoy，尽可能将流量转发给和当前Enovy更近的目标工作负载实例。</p>
<p>另一种模式是地理位置权重负载均衡（Locality-weighted load balancing），直接指定不同地理位置负责提供服务的权重。</p>
<div class="blog_h1"><span class="graybg">CLI</span></div>
<div class="blog_h2"><span class="graybg">istioctl</span></div>
<p>Istio的命令行配置工具，用于创建、查询、修改、删除Istio配置资源。</p>
<div class="blog_h3"><span class="graybg">通用选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--context</td>
<td>使用的kubeconfig context，默认""</td>
</tr>
<tr>
<td>-i</td>
<td>Istio安装在的命名空间，默认istio-system</td>
</tr>
<tr>
<td>-c</td>
<td>使用的kubeconfig</td>
</tr>
<tr>
<td>--namespace</td>
<td>在哪个命名空间执行命令</td>
</tr>
<tr>
<td>--log_output_level  </td>
<td>
<p>日志级别，格式为scope:level,scope:level...默认default:info</p>
<p>scope取值：ads, default, model, rbac</p>
<p>level取值：debug, info, warn, error, none</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">authn</span></div>
<p>一组子命令，用于操控Istio的身份验证策略：</p>
<pre class="crayon-plain-tag"># 列出所有服务的身份验证策略
istioctl authn tls-check
# 仅仅显示单个服务的
istioctl authn tls-check grafana.istio-system.svc.k8s.gmem.cc
# 服务主机名和端口                 状态                    身份验证策略（服务器配置）                     客户端配置
# HOST:PORT                     STATUS  SERVER  CLIENT  AUTHN POLICY                              DESTINATION RULE
# grafana.istio-system...:3000  OK      HTTP    HTTP    grafana-ports-mtls-disabled/istio-system  -</pre>
<div class="blog_h3"><span class="graybg">create</span></div>
<p>创建策略、规则：</p>
<pre class="crayon-plain-tag">istioctl create -f example-routing.yaml</pre>
<div class="blog_h3"><span class="graybg">get</span></div>
<p>获取策略、规则：</p>
<pre class="crayon-plain-tag"># 获取所有虚拟服务
istioctl get virtualservices
# 获取所有目的地规则
istioctl get destinationrules

# 输出为YAML
istioctl get virtualservice bookinfo -o yaml </pre>
<div class="blog_h3"><span class="graybg">replace</span></div>
<p>替换掉策略、规则：</p>
<pre class="crayon-plain-tag">istioctl replace -f example-routing.yaml </pre>
<div class="blog_h3"><span class="graybg">delete </span></div>
<p>删除策略、规则，命令格式：</p>
<pre class="crayon-plain-tag">istioctl delete &lt;type&gt; &lt;name&gt; [&lt;name2&gt; ... &lt;nameN&gt;] [flags]</pre>
<p>示例：</p>
<pre class="crayon-plain-tag">istioctl delete virtualservice bookinfo

# 也可以通过yaml文件删除
istioctl delete -f example-routing.yaml</pre>
<div class="blog_h3"><span class="graybg">register</span></div>
<p>注册一个服务实例（例如一个VM）到服务网格中：</p>
<pre class="crayon-plain-tag">istioctl register &lt;svcname&gt; &lt;ip&gt; [name1:]port1 [name2:]port2 ... [flags]</pre>
<div class="blog_h3"><span class="graybg">deregister </span></div>
<p>注销虚拟服务实例，命令格式：</p>
<pre class="crayon-plain-tag">istioctl deregister &lt;svcname&gt; &lt;ip&gt; [flags]</pre>
<div class="blog_h3"><span class="graybg">experimental </span></div>
<p>一组试验性命令，未来可能修改或删除。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">子命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>convert-ingress</td>
<td>
<p>尽最大努力的把Ingress资源转换为VirtualService，示例：</p>
<pre class="crayon-plain-tag">kubectl -n devops get ingresses.extensions grafana --export -o yaml &gt; /tmp/grafana.yaml
istioctl experimental convert-ingress -f /tmp/grafana.yaml</pre>
<p>输出的VirtualService总是以svc.cluster.local为集群DNS后缀，可能需要修改 </p>
</td>
</tr>
<tr>
<td>metrics</td>
<td>
<p>打印一个或多个虚拟服务的统计指标，示例：</p>
<pre class="crayon-plain-tag"># 列出所有虚拟服务
istioctl get virtualservice
# 显示某个服务的指标
istioctl experimental metrics productpage
#  工作负载名称  请求/秒       错误/秒     延迟百分比分布
   WORKLOAD    TOTAL RPS    ERROR RPS  P50 LATENCY  P90 LATENCY  P99 LATENCY
productpage        0.000        0.000           0s           0s           0s</pre>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td>rbac</td>
<td>
<p>检查是否有权限访问服务，格式：</p>
<pre class="crayon-plain-tag">istioctl experimental rbac can METHOD SERVICE PATH [flags]</pre>
<p>示例：</p>
<pre class="crayon-plain-tag"># 用户alex是否有权访问rating服务的/v1/health路径
istioctl experimental rbac can -u alex GET rating /v1/health
# 服务product-page是否有权以POST方法访问rating服务的dev版本的/data路径
istioctl experimental rbac can -s service=product-page POST rating /data -a version=dev</pre>
</td>
</tr>
<tr>
<td>post-install</td>
<td>
<p>安装后相关操作：
<p style="padding-left: 30px;">post-install webhook disable 禁用Webhook<br />post-install webhook enable 启用Webhook<br />post-install webhook status 查看Webhook状态</p>
</td>
</tr>
<tr>
<td>remove-from-mesh</td>
<td>
<p>将工作负载从Mesh中移除：</p>
<p style="padding-left: 30px;">remove-from-mesh deployment 移除此Deployment对应的Pod的Sidecar并重启<br />remove-from-mesh service 移除此服务对应的Pod的Sidecar并重启<br />remove-from-mesh external-service 移除ServiceEntry以及对应的K8S服务</p>
</td>
</tr>
<tr>
<td>upgrade</td>
<td>检查新版本并升级</td>
</tr>
<tr>
<td>wait</td>
<td>
<p>等待和Istio有关的资源Ready</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">kube-inject</span></div>
<p>手工的将Envoy Sidecar注入到K8S的工作负载的定义文件中。定义文件中包含多种不支持注入的资源是安全的：</p>
<pre class="crayon-plain-tag"># 输出到标准输出
kubectl apply -f &lt;(istioctl kube-inject -f resource.yaml)
# 输出到文件
istioctl kube-inject -f deployment.yaml -o deployment-injected.yaml</pre>
<div class="blog_h3"><span class="graybg">proxy-config</span></div>
<p>一组命令，用于Dump出Envoy代理的各方面的配置：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">子命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>bootstrap</td>
<td>
<p>获取Envoy实例的Boostrap信息：</p>
<pre class="crayon-plain-tag">istioctl proxy-config bootstrap &lt;pod-name&gt; [flags]</pre>
</td>
</tr>
<tr>
<td>cluster</td>
<td>获取Envoy实例的集群配置信息</td>
</tr>
<tr>
<td>listener</td>
<td>获取Envoy实例的listener配置信息</td>
</tr>
<tr>
<td>route</td>
<td>获取Envoy实例的路由配置信息</td>
</tr>
</tbody>
</table>
<p>本节不做更细致的描述，具体参考<a href="#dump-envoy-config">运维-Dump出Envoy配置</a>一节。
<div class="blog_h3"><span class="graybg">proxy-status</span></div>
<p>查询从Pilot最后一次发送到各Envoy的xDS同步的状态：</p>
<pre class="crayon-plain-tag">istioctl proxy-status
# PROXY                               CDS     LDS    EDS            RDS    PILOT                         VERSION
# details-v1-67c8f895b4-dnddn.default SYNCED  SYNCED SYNCED (100%)  SYNCED istio-pilot-77646875cd-jqmdh  1.0.2</pre>
<div class="blog_h3"><span class="graybg">analyze</span></div>
<p>分析配置文件并打印校验信息：<pre class="crayon-plain-tag">istioctl analyze &lt;file&gt;... [flags]</pre></p>
<p>示例：</p>
<pre class="crayon-plain-tag"># 分析当前服务网格
istioctl analyze

# 分析应用指定配置文件后的当前服务网格
istioctl analyze a.yaml b.yaml my-app-config/

# 不连接到服务网格，进行分析
istioctl analyze --use-kube=false a.yaml b.yaml my-app-config/</pre>
<div class="blog_h3"><span class="graybg">dashboard</span></div>
<p>访问Istio的Web UI。</p>
<pre class="crayon-plain-tag"># 为Istio控制仪表盘中打开指定Pod的ControlZ WebUI
istioctl dashboard controlz &lt;pod-name[.namespace]&gt; [flags]

# 为指定的Sidecar打开Envoy Admin仪表盘
istioctl dashboard envoy &lt;pod-name[.namespace]&gt; [flags]

# 打开集成的软件
istioctl dashboard grafana [flags]
istioctl dashboard jaeger [flags]
istioctl dashboard kiali [flags]
istioctl dashboard prometheus [flags]
istioctl dashboard zipkin [flags]</pre>
<div class="blog_h3"><span class="graybg">manifest</span></div>
<p>生成、应用（到K8S）Istio资源的清单文件，或者显示Diff：</p>
<pre class="crayon-plain-tag"># 生成清单文件直接写入K8S
istioctl manifest apply
  --dry-run	                     # 只是打印会做什么
  --filename,-f                  # 指向IstioOperator资源，用于在Base配置上进行Overlay，可以指定多次
  --force	                     # 即使校验失败也继续
  --kubeconfig,-c                # Kubeconfig文件
  --set,-s                       # 覆盖IstioOperator中的值，下载Istio并到install/kubernetes/operator/profiles
                                 # 中看有哪些值，Spec字段下的值可以直接Set，例如Spec.hub可以 -s hub=docker.gmem.cc
  --wait,-w                      # 等待必要的资源就绪

# 应用默认配置
istioctl manifest apply
# 设置变量
istioctl manifest apply --set values.grafana.enabled=true
# 应用Profile，可用的Profile列表位于install/kubernetes/operator/profiles
istioctl manifest apply --set profile=demo --skip-confirmation
# 特殊字符转义
istioctl manifest apply --set "values.sidecarInjectorWebhook.injectedAnnotations.container\.apparmor\.security\.beta\.kubernetes\.io/istio-proxy=runtime/default"

# 显示两个目录或文件的区别
istioctl manifest diff

# 只是生成到标准输出，不Apply到K8S
istioctl manifest generate [flags]

# 从Helm Values或IstioControlPlane转换为IstioOperator格式的配置
istioctl manifest migrate</pre>
<div class="blog_h3"><span class="graybg">profile</span></div>
<p>列出、Dump、Diff可用的Profile，这些Profile可以在manifest命令中引用：</p>
<pre class="crayon-plain-tag"># 列出Profile
istioctl profile list [flags]


# 打印出来
istioctl profile dump [&lt;profile&gt;] [flags]


# 显示两个Profile的差异
istioctl profile diff &lt;file1.yaml&gt; &lt;file2.yaml&gt; [flags]</pre>
<div class="blog_h2"><span class="graybg">fortio</span></div>
<p>这是Istio的负载测试工具。</p>
<div class="blog_h3"><span class="graybg">格式</span></div>
<pre class="crayon-plain-tag"># 子命令 # 选项   # 目标URL
fortio command [flags] target</pre>
<div class="blog_h3"><span class="graybg">子命令</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">子命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>load</td>
<td>执行负载测试</td>
</tr>
<tr>
<td>server</td>
<td>启动GRPC ping/http服务</td>
</tr>
<tr>
<td>grpcping</td>
<td>启动GRPC客户端</td>
</tr>
<tr>
<td>report</td>
<td>启动用于查看报告的UI服务器</td>
</tr>
<tr>
<td>redirect</td>
<td>启动仅执行重定向的服务器</td>
</tr>
<tr>
<td>curl</td>
<td>简单的调试一个URL，显示响应的详细信息</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>-H value</td>
<td>添加额外的请求头</td>
</tr>
<tr>
<td>-abort-on int</td>
<td>如果遇到目标HTTP状态码，立即终止运行。例如503或-1，表示套接字错误</td>
</tr>
<tr>
<td>-allow-initial-errors</td>
<td>允许预热期间的错误，且不终止运行</td>
</tr>
<tr>
<td>-c int</td>
<td>连接/Goroutine/线程的并发数，默认4</td>
</tr>
<tr>
<td>-compression</td>
<td>启用HTTP压缩</td>
</tr>
<tr>
<td>-http1.0</td>
<td>使用HTTP 1.0而非1.1</td>
</tr>
<tr>
<td>-k</td>
<td>不校验服务器端证书</td>
</tr>
<tr>
<td>-keepalive</td>
<td>传输层连接重用，默认true</td>
</tr>
<tr>
<td>-qps float</td>
<td>目标QPS，如果为0则不做任何休眠，尽可能提高QPS，默认8</td>
</tr>
<tr>
<td>-n int</td>
<td>运行指定的次数</td>
</tr>
<tr>
<td>-t duration</td>
<td>运行指定的时间，默认5s，指定为0则一直运行</td>
</tr>
<tr>
<td>-p string</td>
<td>统计的百分比值（即耗时N%百分比的请求的最大耗时值），默认 50,75,90,99,99.9</td>
</tr>
<tr>
<td>-timeout duration</td>
<td>HTTP连接/读取超时，默认15s</td>
</tr>
<tr>
<td>-user string</td>
<td>HTTP基本认证的凭证信息，格式user:pswd</td>
</tr>
<tr>
<td>-loglevel lv</td>
<td>日志级别，可以取值Warning、Error等</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">mixc</span></div>
<p>Mixer的命令行客户端。</p>
<div class="blog_h3"><span class="graybg">check</span></div>
<p>用于执行前置条件检查、配额（quota allocations）。Mixer期望以一组Attributes为输入，它基于自己的配置来判断调用哪个适配器、如何传递参数，进而进行策略检查、配额。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">选项</td>
<td style="width: 75px; text-align: center;">短选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--attributes </td>
<td>-a</td>
<td>自动感知的属性列表，形式name1=value1,name2=value2...</td>
</tr>
<tr>
<td>--bool_attributes</td>
<td>-b</td>
<td>布尔值属性的列表</td>
</tr>
<tr>
<td>--bytes_attributes</td>
<td> </td>
<td>字节值属性的列表</td>
</tr>
<tr>
<td>--concurrency</td>
<td>-c</td>
<td>并行的调用Mixer的线程数</td>
</tr>
<tr>
<td>--double_attributes</td>
<td>-d</td>
<td>浮点数属性的列表</td>
</tr>
<tr>
<td>--duration_attributes</td>
<td> </td>
<td>时间长度属性的列表</td>
</tr>
<tr>
<td>--int64_attributes</td>
<td>-i</td>
<td>整数属性的列表</td>
</tr>
<tr>
<td>--string_attributes</td>
<td>-s</td>
<td>字符串属性的列表</td>
</tr>
<tr>
<td>--stringmap_attributes</td>
<td> </td>
<td>string:string映射类型的属性列表，形式name1=k1:v1;k2:v2,name2=k3:v3...</td>
</tr>
<tr>
<td>--timestamp_attributes</td>
<td>-t</td>
<td>
<p>时间戳属性的列表</p>
<p>时间戳必须以ISO8601格式指定，例如：</p>
<pre class="crayon-plain-tag"># UTC时间
2019-03-27T11:42:57Z

# 北京时间
2019-03-27T11:42:57+08:00

# 带毫秒
2019-03-27T11:42:57.000+08:00</pre>
</td>
</tr>
<tr>
<td>--mixer</td>
<td>-m</td>
<td>目标Mixer实例</td>
</tr>
<tr>
<td>--quotas</td>
<td>-q</td>
<td>需要分配的配额的列表，形式name1=amount1,name2=amount2...</td>
</tr>
<tr>
<td>--repeat</td>
<td>-r</td>
<td>连续发送指定次数的请求，默认1</td>
</tr>
<tr>
<td>--trace_jaeger_url</td>
<td> </td>
<td rowspan="3">追踪相关</td>
</tr>
<tr>
<td>--trace_log_spans</td>
<td> </td>
</tr>
<tr>
<td>--trace_zipkin_url</td>
<td> </td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">report</span></div>
<p>用于产生遥感（telemetry）数据。Mixer期望以一组Attributes为输入，它基于自己的配置来判断调用哪个适配器、如何传递参数，进而输出遥感数据。
<p>选项和check子命令一样。</p>
<div class="blog_h1"><span class="graybg">安装到K8S</span></div>
<div class="blog_h2"><span class="graybg">下载最新版</span></div>
<pre class="crayon-plain-tag">cd /home/alex/Applications
curl -L https://git.io/getLatestIstio | sh -
mv istio-1.0.3 istio</pre>
<p>然后把istio/bin目录加入PATH环境变量。</p>
<div class="blog_h2"><span class="graybg">支持Sidecar自动注入</span></div>
<p>Istio可以自动为Pod注入Sidecar，它基于Webhook实现此功能。</p>
<p>你可以执行下面的命令，确认MutatingAdmissionWebhook、ValidatingAdmissionWebhook这两种Admission controllers是否默认启用：</p>
<pre class="crayon-plain-tag">kubectl -n kube-system exec kube-apiserver-xenon -- kube-apiserver -h | grep enable-admission-plugins
# 如果在你的K8S版本中，in addition to default enabled ones...后面的括号中有MutatingAdmissionWebhook
# 和ValidatingAdmissionWebhook则说明默认已经启用</pre>
<p>如果没有启用，则需要修改API Server的命令行参数。</p>
<p>此外，你还需要检查admissionregistration API是否启用：</p>
<pre class="crayon-plain-tag">kubectl api-versions | grep admissionregistration
admissionregistration.k8s.io/v1beta1</pre>
<p>和手工注入不同，<span style="background-color: #c0c0c0;">自动注入是发生在Pod级别的</span>。在Deployment上看不到任何变化，你需要直接检查Pod才能看到注入的Envoy。 </p>
<div class="blog_h2"><span class="graybg">对Pod/Service的要求</span></div>
<p>为了加入到服务网格，K8S中的相关Pod、Service必须满足如下条件：</p>
<ol>
<li>命名端口：<span style="background-color: #c0c0c0;">服务（指K8S的服务，不是Pod）的端口必须被命名</span>，为了使用Istio的路由特性，端口名必须是<pre class="crayon-plain-tag">&lt;protocol&gt;[-&lt;suffix&gt;]</pre>的形式。protocol可以是http, http2, grpc, mongo或者redis，suffix是可选的，如果指定suffix则必须在前面加上短横线。如果protocol不支持，或者端口没有命名，则针对该端口的流量作为普通TCP流量看待（除非通过Protocol: UDP明确指定为UDP）。示例：<br />
<pre class="crayon-plain-tag">apiVersion: v1
kind: Service
metadata:
  name: details
spec:
  ports:
  - port: 9080
    # 命名服务端口
    name: http
  selector:
    app: details </pre>
</li>
<li>服务关联：如果一个Pod属于多个Kubernetes Service，则这些Service不得将同一端口用作不同协议</li>
<li>app和version标签：建议为Pod添加app和version两个标签，分别表示应用程序的名称和版本。在分布式追踪（distributed tracing）中app标签用于提供上下文信息。在Metrics收集时app、version标签也提供上下文信息</li>
</ol>
<div class="blog_h2"><span class="graybg">通过Helm安装</span></div>
<p>Istio的Chart定义在<a href="https://git.gmem.cc/alex/chart-istio.git">install/kubernetes/helm/instio</a>目录下 。默认情况下，Istio会以Sub chart的方式安装很多组件：</p>
<pre class="crayon-plain-tag">dependencies:
  # 这是一个Webhook，自动为所有Pod注入Sidecar。默认启用
  - name: sidecarInjectorWebhook
    version: 1.0.3
    condition: sidecarInjectorWebhook.enabled
  # 安全模块，即citadel。默认禁用
  - name: security
    version: 1.0.3
    condition: security.enabled
  # Ingress控制器，用于处理K8S的Ingress资源。默认禁用
  - name: ingress
    version: 1.0.3
    condition: ingress.enabled
  # 入口、出口网关。默认启用
  - name: gateways
    version: 1.0.3
    condition: gateways.enabled
  # 默认启用，核心组件
  - name: mixer
    version: 1.0.3
    condition: mixer.enabled
  # 默认启用，核心组件
  - name: pilot
    version: 1.0.3
    condition: pilot.enabled
  # 默认禁用
  - name: grafana
    version: 1.0.3
    condition: grafana.enabled
  # 默认启用
  - name: prometheus
    version: 1.0.3
    condition: prometheus.enabled
  # 用于绘制调用链图。默认禁用，被kiali代替
  - name: servicegraph
    version: 1.0.3
    condition: servicegraph.enabled
  # APM。默认禁用
  - name: tracing
    version: 1.0.3
    condition: tracing.enabled
  # 用于服务器端的配置合法性验证。默认启用
  - name: galley
    version: 1.0.3
    condition: galley.enabled
  # 服务网格可视化。默认禁用
  - name: kiali
    version: 1.0.3
    condition: kiali.enabled
  # 负责基于ACME执行数字证书签名。默认禁用
  - name: certmanager
    version: 1.0.3
    condition: certmanager.enabled </pre>
<p>如需禁用，在values.yaml中修改对应配置项即可。</p>
<div class="blog_h3"><span class="graybg">镜像列表</span></div>
<p>1.0.3版本：</p>
<pre class="crayon-plain-tag">docker pull docker.io/istio/proxyv2:1.0.3
docker pull docker.io/istio/proxy_init:1.0.3
docker pull docker.io/istio/sidecar_injector:1.0.3
docker pull docker.io/istio/galley:1.0.3
docker pull docker.io/istio/mixer:1.0.3
docker pull docker.io/istio/pilot:1.0.3
docker pull docker.io/istio/citadel:1.0.3
docker pull docker.io/istio/servicegraph:1.0.3
docker pull docker.io/jaegertracing/all-in-one:1.5
docker pull docker.io/kiali/kiali:v0.9
docker pull docker.io/prom/prometheus:v2.3.1
docker pull quay.io/jetstack/cert-manager-controller:v0.3.1
docker pull quay.io/coreos/hyperkube:v1.7.6_coreos.0
docker pull grafana/grafana:5.2.3</pre>
<p>1.1.4版本：</p>
<pre class="crayon-plain-tag">docker pull docker.io/istio/kubectl:1.1.4
docker pull docker.io/istio/node-agent-k8s:1.1.4
docker pull docker.io/istio/citadel:1.1.4
docker pull docker.io/istio/proxy_init:1.1.4
docker pull docker.io/istio/proxyv2:1.1.4
docker pull docker.io/istio/galley:1.1.4
docker pull docker.io/istio/pilot:1.1.4
docker pull docker.io/istio/mixer:1.1.4
docker pull docker.io/istio/sidecar_injector:1.1.4
docker pull docker.io/jaegertracing/all-in-one:1.9
docker pull docker.io/kiali/kiali:v0.16
docker pull docker.io/prom/prometheus:v2.3.1
docker pull busybox:1.30.1
docker pull grafana/grafana:6.0.2
docker pull quay.io/jetstack/cert-manager-controller:v0.6.2</pre>
<div class="blog_h3"><span class="graybg">执行安装</span></div>
<p>到Github下载<a href="https://github.com/istio/istio/releases">Istio的压缩包</a>，解压后在其根目录执行。</p>
<p>我的环境下，镜像拉取需要Secret：</p>
<pre class="crayon-plain-tag">export PSWD=...

# 创建命名空间
kubectl create ns istio-system

# 创建访问Docker镜像仓库的保密字典
kubectl -n istio-system create secret docker-registry gmemregsecret \
    --docker-server=docker.gmem.cc --docker-username=alex  \
    --docker-password=$PSWD --docker-email=k8s@gmem.cc</pre>
<p>对于Istio 1.1，需要首先安装独立的Chart istio-init，进行CRD的注册：</p>
<pre class="crayon-plain-tag">helm install install/kubernetes/helm/istio-init  --name istio-init --namespace istio-system -f install/kubernetes/helm/istio-init/overrides/gmem.yaml</pre>
<p>安装Istio组件：</p>
<pre class="crayon-plain-tag"># 创建入口网关控制器所需的数字证书，这里是*.k8s.gmem.cc的通配符证书
# 注意，ingressgateway没有自动reload数字证书的能力，必须重启Pod
pushd /home/alex/Documents/puTTY/mesh.gmem.cc
kubectl create -n istio-system secret generic istio-ingressgateway-certs --from-file=.
popd

# 创建kiali的Ingress所需的数字证书保密字典
pushd /etc/letsencrypt/live/kiali.k8s.gmem.cc
kubectl create -n istio-system secret generic gmemk8scert-kiali --from-file=tls.crt=fullchain.pem,tls.key=privkey.pem
popd
# 创建Jaeger的Ingress所需的数字证书保密字典
pushd /etc/letsencrypt/live/jaeger.k8s.gmem.cc
kubectl create -n istio-system secret generic gmemk8scert-jaeger --from-file=tls.crt=fullchain.pem,tls.key=privkey.pem
popd

# 为默认证书设置imagePullSecrets
kubectl -n istio-system patch serviceaccount default -p '{"imagePullSecrets": [{"name": "gmemregsecret"}]}'

# 不让Citadel使用自签名根证书
pushd ~/Documents/puTTY/
touch empty.pem
kubectl -n istio-system create secret generic cacerts --from-file=ca-cert.pem=ca.crt \
                                                      --from-file=ca-key.pem=ca.key \
                                                      --from-file=root-cert.pem=ca.crt \
                                                      --from-file=cert-chain.pem=empty.pem
popd

# 挂载Ceph卷所需的密钥
kubectl get secrets pvc-ceph-key -o yaml --export | kubectl -n istio-system apply -f -


# 安装Istio
# 1.0
helm install install/kubernetes/helm/istio --name istio --namespace istio-system --set kiali.dashboard.passphrase=$PSWD -f install/kubernetes/helm/istio/overrides/gmem.yaml
# 1.1
kubectl -n istio-system create secret generic kiali --from-literal=username=alex --from-literal=passphrase=$PSWD

# 为kiali的服务账号授予必要的权限
kubectl create clusterrolebinding istio-system-kiali-role-binding --clusterrole=cluster-admin --serviceaccount=istio-system:kiali-service-account</pre>
<div class="blog_h3"><span class="graybg">最小化安装</span></div>
<pre class="crayon-plain-tag">helm install install/kubernetes/helm/istio --name istio-minimal --namespace istio-system \
  --set security.enabled=false \
  --set ingress.enabled=false \
  --set gateways.istio-ingressgateway.enabled=false \
  --set gateways.istio-egressgateway.enabled=false \
  --set galley.enabled=false \
  --set sidecarInjectorWebhook.enabled=false \
  --set mixer.enabled=false \
  --set prometheus.enabled=false \
  --set global.proxy.envoyStatsd.enabled=false \
  --set pilot.sidecar=false</pre>
<p>最小化安装时，仅仅安装pilot组件。 </p>
<div class="blog_h3"><span class="graybg">执行删除</span></div>
<pre class="crayon-plain-tag">helm delete istio --purge

kubectl -n istio-system delete all --all

kubectl delete ns istio-system

kubectl delete crd adapters.config.istio.io             
kubectl delete crd apikeys.config.istio.io              
kubectl delete crd attributemanifests.config.istio.io   
kubectl delete crd authorizations.config.istio.io       
kubectl delete crd bypasses.config.istio.io             
kubectl delete crd certificates.certmanager.k8s.io      
kubectl delete crd checknothings.config.istio.io        
kubectl delete crd circonuses.config.istio.io           
kubectl delete crd cloudwatches.config.istio.io         
kubectl delete crd clusterissuers.certmanager.k8s.io    
kubectl delete crd deniers.config.istio.io              
kubectl delete crd destinationrules.networking.istio.io 
kubectl delete crd dogstatsds.config.istio.io           
kubectl delete crd edges.config.istio.io                
kubectl delete crd envoyfilters.networking.istio.io     
kubectl delete crd fluentds.config.istio.io             
kubectl delete crd gateways.networking.istio.io         
kubectl delete crd handlers.config.istio.io             
kubectl delete crd httpapispecbindings.config.istio.io  
kubectl delete crd httpapispecs.config.istio.io         
kubectl delete crd instances.config.istio.io            
kubectl delete crd issuers.certmanager.k8s.io           
kubectl delete crd kubernetesenvs.config.istio.io       
kubectl delete crd kuberneteses.config.istio.io         
kubectl delete crd listcheckers.config.istio.io         
kubectl delete crd listentries.config.istio.io          
kubectl delete crd logentries.config.istio.io           
kubectl delete crd memquotas.config.istio.io            
kubectl delete crd meshpolicies.authentication.istio.io 
kubectl delete crd metrics.config.istio.io              
kubectl delete crd noops.config.istio.io                
kubectl delete crd opas.config.istio.io                 
kubectl delete crd policies.authentication.istio.io     
kubectl delete crd prometheuses.config.istio.io         
kubectl delete crd quotas.config.istio.io               
kubectl delete crd quotaspecbindings.config.istio.io    
kubectl delete crd quotaspecs.config.istio.io           
kubectl delete crd rbacconfigs.rbac.istio.io            
kubectl delete crd rbacs.config.istio.io                
kubectl delete crd redisquotas.config.istio.io          
kubectl delete crd reportnothings.config.istio.io       
kubectl delete crd rules.config.istio.io                
kubectl delete crd servicecontrolreports.config.istio.io
kubectl delete crd servicecontrols.config.istio.io      
kubectl delete crd serviceentries.networking.istio.io   
kubectl delete crd servicerolebindings.rbac.istio.io    
kubectl delete crd serviceroles.rbac.istio.io           
kubectl delete crd signalfxs.config.istio.io            
kubectl delete crd solarwindses.config.istio.io         
kubectl delete crd stackdrivers.config.istio.io         
kubectl delete crd statsds.config.istio.io              
kubectl delete crd stdios.config.istio.io               
kubectl delete crd templates.config.istio.io            
kubectl delete crd tracespans.config.istio.io           
kubectl delete crd virtualservices.networking.istio.io</pre>
<div class="blog_h3"><span class="graybg">处理集群DNS后缀</span></div>
<p>如果你的Kubernetes集群不使用缺省的集群DNS后缀（即cluster.local），则很多组件的参数需要修改，可以参考<a href="https://git.gmem.cc/alex/chart-istio">chart-istio</a>项目，搜索{{ .Values.global.domain }}找到需要修改的位置。</p>
<p>注意：1.1版本的Chart这块已经处理好，修改values.yaml中的global.proxy.clusterDomain即可。</p>
<div class="blog_h2"><span class="graybg">配置</span></div>
<p>Istio暴露了几个Configmap，修改这些Configmap以影响Istio的行为。</p>
<div class="blog_h3"><span class="graybg">istio</span></div>
<pre class="crayon-plain-tag">apiVersion: v1
kind: ConfigMap
metadata:
  name: istio
  namespace: istio-system
  labels:
    app: istio
    chart: istio-1.0.5
    release: istio
    heritage: Tiller
data:
  mesh: |-
    # 设置为true则禁用策略检查
    # 不影响遥测指标的报送
    disablePolicyChecks: false

    # 是否启用请求跟踪
    enableTracing: true

    # 设置为空字符串可以禁用访问日志
    accessLogFile: "/dev/stdout"

    # Deprecated: mixer is using EDS
    mixerCheckServer: istio-policy.istio-system.svc.k8s.gmem.cc:9091
    mixerReportServer: istio-telemetry.istio-system.svc.k8s.gmem.cc:9091

    # 如果设置为true，则Mixer的策略服务不可用导致策略检查失败时，允许请求而非拒绝
    policyCheckFailOpen: false

    defaultConfig:
      #
      # Envoy和应用之间、Envoy之间的TCP链接超时
      connectTimeout: 10s
      #
      ### ADVANCED SETTINGS #############
      # istio-proxy容器中，Envoy的配置存放在何处
      configPath: "/etc/istio/proxy"
      # Envoy二进制文件的路径
      binaryPath: "/usr/local/bin/envoy"
      # The pseudo service name used for Envoy.
      serviceCluster: istio-proxy
      # 如果发生偶然性的Envoy reload，旧的Envoy进程保留多长时间
      drainDuration: 45s
      parentShutdownDuration: 1m0s
      #
      # 将入口流量重定向到Envoy的模式，对出口流量无影响，后者总是使用iptables REDIRECT
      # 取值：
      #   REDIRECT，使用iptables REDIRECT来NAT，并重定向到Envoy。源地址信息会丢失
      #   TPROXY，使用iptables TPROXY来重定向到Envoy，同时保留原始的source/dest IP地址和端口
      #           TPROXY可以在不对封包进行任何变动（例如NAT）的情况下重定向封包给一个本地套接字
      #           可以用于高级过滤和操控。此模式会配置sidecar，启用CAP_NET_ADMIN能力，这是TPROXY所需的
      #
      interceptionMode: REDIRECT
      #
      # Envoy在本地环回网卡上监听的管理端口，你可以进入istio-proxy容器并执行
      # curl http://localhost:15000/获取Envoy的诊断信息
      # 访问https://lyft.github.io/envoy/docs/operations/admin.html了解更多信息
      proxyAdminPort: 15000
      #
      # Envoy工作线程数量，如果设置为0，自动根据CPU核心数量确定
      concurrency: 0
      #
      # Zipkin/Jaeger的访问地址，Envoy向其报送APM数据
      zipkinAddress: zipkin.istio-system:9411
      #
      # Sidecar和Istio控制平面的mTLS认证策略
      controlPlaneAuthPolicy: NONE
      #
      # Pilot服务的地址
      discoveryAddress: istio-pilot.istio-system:15007</pre>
<div class="blog_h2"><span class="graybg">跨集群网格</span></div>
<p>可以跨越多个Kubernetes集群来部署Istio服务网格。</p>
<div class="blog_h3"><span class="graybg">前提条件</span></div>
<ol>
<li>两个或更多的运行1.9+版本的Kubernetes集群</li>
<li>有权在其中一个集群上部署Istio控制平面</li>
<li>使用RFC1918网络、VPN或更高级的网络技术将集群连接在一起，并且满足：
<ol>
<li>所有集群的Pod网络CIDR、Service网络CIDR不重叠</li>
<li>所有集群的Pod网络可以相互路由</li>
<li>所有集群的API Server可相互联通</li>
</ol>
</li>
<li>Helm 2.7.2或更高版本</li>
</ol>
<div class="blog_h2"><span class="graybg">Istio CNI插件</span></div>
<p>Istio 将用户 pod 流量转发到 proxy 的默认方式是使用 privileged 权限的 istio-init 容器，Istio CNI插件可以消除这一priviledged要求。</p>
<p>Istio CNI Plugin是Kubernetes CNI的一个实现，Kubernetes CNI插件是一个链，在<span style="background-color: #c0c0c0;">创建和销毁pod的时候会调用链上所有插件来安装和卸载容器的网络</span>。</p>
<p>此插件会运行一个Daemonset： istio-cni-node，在每个节点上安装CNI插件。此插件负责写入iptables规则。</p>
<p>启用此插件，istio-init容器不再需要。</p>
<div class="blog_h3"><span class="graybg">安装</span></div>
<p>首先安装CNI插件：</p>
<pre class="crayon-plain-tag">helm install install/kubernetes/helm/istio-cni --name istio-cni --namespace kube-system</pre>
<p>然后，安装Istio，注意设置参数：</p>
<pre class="crayon-plain-tag">helm install install/kubernetes/helm/istio --name istio --namespace istio-system --set istio_cni.enabled=true </pre>
<div class="blog_h2"><span class="graybg">安装1.5版本</span></div>
<p>此版本的Helm Chart不正常，安装出来的不是isitd单体模式。</p>
<p>推荐使用<pre class="crayon-plain-tag">istioctl manifest</pre>命令来安装，生成的清单文件包括namespace istio-system，因此删除操作：</p>
<pre class="crayon-plain-tag">istioctl manifest generate &lt;your original installation options&gt; | kubectl delete -f -</pre>
<p>会导致预先创建手工创建在istio-system中的资源丢失。建议先<pre class="crayon-plain-tag">istioctl manifest generate</pre>，然后修改它。 </p>
<div class="blog_h3"><span class="graybg">覆盖默认值</span></div>
<p>使用istioctl预置的Profile不能满足需要的话，可通过-f参数自己提供一个IstioOperator资源，在其中覆盖参数值：</p>
<pre class="crayon-plain-tag">istioctl manifest generate -f gmem.yaml</pre><br />
<pre class="crayon-plain-tag">apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
  namespace: istio-system
spec:
  hub: docker.gmem.cc/istio
  imagePullSecrets:
    - gmemregsecret
  addonComponents:
    kiali:
      enabled: true
    grafana:
      enabled: true
    tracing:
      enabled: true
  values:
    global:
      logging:
        level: "default:info"
      proxy:
        clusterDomain: "k8s.gmem.cc"
        logLevel: info
        privileged: true
        accessLogFile: /dev/stdout
      trustDomain: "k8s.gmem.cc"
      sds:
        enabled: true
    security:
      selfSigned: false
    prometheus:
      hub: docker.gmem.cc/prom
    grafana:
      image:
        repository: docker.gmem.cc/grafana
    tracing:
      jaeger:
        hub: docker.gmem.cc/jaegertracing
    kiali:
      hub: docker.gmem.cc/kiali</pre>
<p>1.5.0版本中-f的行为异常，不能生成资源清单。暂时可用-s代替：</p>
<pre class="crayon-plain-tag">istioctl manifest apply \
  -s profile=demo -s hub=docker.gmem.cc/istio -s values.global.logging.level="default:info" \
  -s values.global.proxy.clusterDomain=k8s.gmem.cc \
  -s values.global.proxy.logLevel=info -s values.global.proxy.privileged=true \
  -s values.global.proxy.accessLogFile=/dev/stdout \
  -s values.global.trustDomain=k8s.gmem.cc -s values.global.sds.enabled=true \
  -s values.security.selfSigned=false  -s values.prometheus.hub=docker.gmem.cc/prom \
  -s values.grafana.image.repository=docker.gmem.cc/grafana \
  -s values.tracing.jaeger.hub=docker.gmem.cc/jaegertracing \
  -s values.kiali.hub=docker.gmem.cc/kiali -s values.kiali.contextPath=/ \
  -s values.tracing.contextPath=/ \
  -s values.global.imagePullSecrets[0]=gmemregsecret -s values.global.imagePullPolicy=Always</pre>
<div class="blog_h3"><span class="graybg">卸载</span></div>
<pre class="crayon-plain-tag">istioctl manifest generate --set profile=demo | kubectl delete -f -</pre>
<p>警告：命名空间也会被删除。 </p>
<div class="blog_h1"><span class="graybg">构建和调试</span></div>
<div class="blog_h2"><span class="graybg">构建</span></div>
<div class="blog_h3"><span class="graybg">环境变量</span></div>
<pre class="crayon-plain-tag">export HUB=docker.gmem.cc
export TAG=1.5
# 使用本地构建的，位于$GOPATH/src/github.com/istio/proxy/bazel-bin/src/envoy/envoy的Istio Proxy
export USE_LOCAL_PROXY=1
export GOOS=linux</pre>
<div class="blog_h3"><span class="graybg">构建Istio Proxy</span></div>
<pre class="crayon-plain-tag">export BAZEL_BUILD_ARGS="-c dbg"

cd $GOPATH/src/istio.io/
git clone https://github.com/istio/proxy.git

# 安装Bazelisk
sudo wget -O /usr/local/bin/bazel https://github.com/bazelbuild/bazelisk/releases/download/v0.0.8/bazelisk-linux-amd64
sudo chmod +x /usr/local/bin/bazel

# 安装相关依赖
sudo apt-get install libtool  cmake automake autoconf make ninja-build curl unzip  build-essential software-properties-common
sudo pip install virtualenv

# 安装GCC 7+ 或 Clang/LLVM 7+，因为使用了C++14。我们使用LLVM 7.1.0
cd/home/alex/CPP/llvm
wget https://github.com/llvm/llvm-project/releases/download/llvmorg-7.1.0/clang+llvm-7.1.0-x86_64-linux-gnu-ubuntu-14.04.tar.xz
xz -d clang+llvm-7.1.0-x86_64-linux-gnu-ubuntu-14.04.tar.xz
tar xvf clang+llvm-7.1.0-x86_64-linux-gnu-ubuntu-14.04.tar
mv clang+llvm-7.1.0-x86_64-linux-gnu-ubuntu-14.04 7.1.0

# Bazel文件格式化工具
go get -u github.com/bazelbuild/buildtools/buildifier
# 用于移动应用打包
go get -u github.com/bazelbuild/buildtools/buildozer

# 执行构建
cd proxy
bazel-proxy/external/envoy/bazel/setup_clang.sh /home/alex/CPP/llvm/7.1.0
http_proxy=http://127.0.0.1 https_proxy=http://127.0.0.1:8087 make</pre>
<div class="blog_h3"><span class="graybg">构建Istio</span></div>
<pre class="crayon-plain-tag">cd $GOPATH/src/istio.io/
git clone https://github.com/istio/istio.git

cd istio/
# 构建出Docker镜像
make docker
# 推送镜像
make push

# 生成K8S资源清单
make generate_yaml</pre>
<div class="blog_h1"><span class="graybg">示例应用</span></div>
<div class="blog_h2"><span class="graybg">bookinfo</span></div>
<p>Istio提供了一个示例应用程序Bookinfo，我可以部署该应用 ，并为其配置服务网格，以学习Istio。该应用程序能够显示书籍的基本信息，包括ISBN、页数，还能显示书籍的评论信息。</p>
<p>Bookinfo由4个独立的微服务组成：</p>
<ol>
<li>productpage，调用details、reviews渲染页面</li>
<li>details，提供书籍基本信息</li>
<li>reviews，提供书评信息，调用ratings。该服务有v1、v2、v3三个版本</li>
<li>ratings，提供书籍的评级信息</li>
</ol>
<p>下图显示了没有部署Istio之前，Bookinfo的端对端架构：</p>
<p><img class="aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2018/11/noistio.svg" alt="" width="765" height="523" /></p>
<p>下图显示受到Istio服务网格管理的Bookinfo的架构：</p>
<p><img class="aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2018/11/withistio.svg" alt="" width="771" height="456" /></p>
<div class="blog_h3"><span class="graybg">为命名空间启用自动注入</span></div>
<p>如果启用了自动化的Sidecar注入，你需要在安装到的命名空间上打标签：</p>
<pre class="crayon-plain-tag">kubectl label namespace default istio-injection=enabled</pre>
<p>打上此标签后，default命名空间中创建的<span style="background-color: #c0c0c0;">新Pod</span>，自动会有一个名为stio-proxy的容器，它运行istio/proxyv2镜像。 </p>
<div class="blog_h3"><span class="graybg">手工注入</span></div>
<p>如果没有启用自动化的Sidecar注入，你需要执行：</p>
<pre class="crayon-plain-tag">kubectl apply -f &lt;(istioctl kube-inject -f samples/bookinfo/platform/kube/bookinfo.yaml)</pre>
<div class="blog_h3"><span class="graybg">安装bookinfo资源</span></div>
<p>然后，执行下面的命令安装所有组件：</p>
<pre class="crayon-plain-tag">kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml</pre>
<p>等所有容器都运行起来了，检查一下当前命名空间，应该多出6个Pod、4个Service。</p>
<div class="blog_h3"><span class="graybg">安装Istio网关</span></div>
<p>当所有Pod都进入Running状态后，你需要创建一个（入口）网关，这样浏览器才能访问到集群中的Bookinfo服务：</p>
<pre class="crayon-plain-tag">kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml</pre>
<p>上述命令会创建一个Gateway，一个VirtualService：</p>
<pre class="crayon-plain-tag"># kubectl get gateway bookinfo-gateway -o yaml --export

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: bookinfo-gateway
  namespace: default
spec:
  selector:
    istio: ingressgateway
  servers:
  - hosts:
    - '*'
    port:
      name: http
      number: 80
      protocol: HTTP


# kubectl get virtualservice bookinfo -o yaml --export

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: bookinfo
spec:
  gateways:
  - bookinfo-gateway  # 和前面的网关绑定
  hosts:
  - '*'
  http:
  - match:
    # 以下3个URL模式发送给productpage服务
    - uri:
        exact: /productpage
    - uri:
        exact: /login
    - uri:
        exact: /logout
    - uri:
        prefix: /api/v1/products
    route:
    - destination:
        host: productpage
        port:
          number: 9080</pre>
<div class="blog_h3"><span class="graybg">获取网关地址</span></div>
<p>我的本地环境，通过配置路由，允许集群外部访问ClusterIP：</p>
<pre class="crayon-plain-tag">kubectl get svc istio-ingressgateway -n istio-system
# NAME                   TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)                                                                                                                AGE
# istio-ingressgateway   NodePort   10.109.97.20   &lt;none&gt;        80:20080/TCP,443:20443/TCP,31400:31400/TCP,15011:20493/TCP,8060:1164/TCP,853:5745/TCP,15030:1299/TCP,15031:11899/TCP   14d</pre>
<p>因此直接使用上述地址10.109.97.20即可。如果你使用NodePort方式，则需要使用宿主机IP + 映射后的端口。</p>
<p>如果使用外部负载均衡器（LoadBalancer类的Service），则参考如下命令：</p>
<pre class="crayon-plain-tag">export INGRESS_HOST=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
export INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].port}')
export SECURE_INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="https")].port}')

# 某些环境下，LB通过主机名而非IP暴露
export INGRESS_HOST=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')</pre>
<div class="blog_h3"><span class="graybg">确认应用可访问</span></div>
<pre class="crayon-plain-tag">export INGRESS_HOST=`kubectl -n istio-system get service istio-ingressgateway --no-headers | awk '{print $3}'`
export INGRESS_PORT=80
export GATEWAY_URL=$INGRESS_HOST:$INGRESS_PORT

curl -o /dev/null -s -w "%{http_code}\n" http://${GATEWAY_URL}/productpage</pre>
<p>如果一切正常，应该打印200到控制台。 如果通过浏览器查看，界面右侧的Reviews面板会随机出现无星、黑星、红星，这种<span style="background-color: #c0c0c0;">随机是K8S的Service负载均衡机制导致的</span>。</p>
<div class="blog_h3"><span class="graybg">创建默认DestRule</span></div>
<p>下面的DestinationRule声明了productpage、reviews、ratings、details等微服务的不同版本。版本通过Pod的version标签来区分。</p>
<pre class="crayon-plain-tag"># kubectl apply -f samples/bookinfo/networking/destination-rule-all.yaml
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: productpage
spec:
  host: productpage
  subsets:
  - name: v1
    labels:
      version: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: reviews
spec:
  host: reviews
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
  - name: v3
    labels:
      version: v3
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: ratings
spec:
  host: ratings
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
  - name: v2-mysql
    labels:
      version: v2-mysql
  - name: v2-mysql-vm
    labels:
      version: v2-mysql-vm
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: details
spec:
  host: details
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
---</pre>
<div class="blog_h2"><span class="graybg">httpbin</span></div>
<p>一个简单的<a href="http://httpbin.default.svc.k8s.gmem.cc:8000/">HTTP测试服务</a>，用于试验Istio的各种特性：</p>
<pre class="crayon-plain-tag">kubectl apply -f samples/httpbin/httpbin.yaml</pre>
<div class="blog_h2"><span class="graybg">fortio</span></div>
<p>Fortio是Istio的负载测试工具，提供CLI和Web UI。</p>
<p>Fortio能够按照指定的QPS来执行HTTP请求，录制执行耗时、百分比分布（percentiles，例如p99表示99%的请求耗时小于的数值）直方图。</p>
<pre class="crayon-plain-tag">kubectl apply -f  samples/httpbin/sample-client/fortio-deploy.yaml</pre>
<div class="blog_h2"><span class="graybg">sleep</span></div>
<p>这是一个基于Ubuntu的容器，它启动后只是简单的休眠。你可以通过kubectl连接到此容器并执行curl命令：</p>
<pre class="crayon-plain-tag">kubectl apply -f samples/sleep/sleep.yaml </pre>
<div class="blog_h2"><span class="graybg">试验流量管理</span></div>
<div class="blog_h3"><span class="graybg">请求路由</span></div>
<p>下面的例子创建了几个“虚拟服务“，这些虚拟服务将请求流量全部路由给各微服务的v1版本：</p>
<pre class="crayon-plain-tag"># kubectl apply -f samples/bookinfo/networking/virtual-service-all-v1.yaml

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: productpage
spec:
  hosts:
  - productpage
  http:
  - route:
    - destination:
        host: productpage
        subset: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
  - reviews
  http:
  - route:
    - destination:
        host: reviews
        subset: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: ratings
spec:
  hosts:
  - ratings
  http:
  - route:
    - destination:
        host: ratings
        subset: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: details
spec:
  hosts:
  - details
  http:
  - route:
    - destination:
        host: details
        subset: v1
---</pre>
<p>虚拟服务的会被转换为Envoy配置信息，在若干秒内传递到<span style="background-color: #c0c0c0;">所有Pod的Sidecar</span>，达到最终一致性。</p>
<p>虚拟服务<span style="background-color: #c0c0c0;">可以遮蔽掉K8S的同名Service</span>，<span style="background-color: #c0c0c0;">启用了Envoy Sidecar的Pod（包括入口网关）访问</span>productpage、reviews、ratings、details这几个微服务（各自对应了<span style="background-color: #c0c0c0;">一个K8S Service、一个虚拟服务、以及一个DNS名称</span>）时，会自动经由上面定义的几个虚拟服务处理，其效果就是全部访问v1版本的微服务。虚拟服务的<span style="background-color: #c0c0c0;">路由逻辑在请求方的Sidecar中实现</span>。</p>
<p>现在再打开浏览器访问/productpage，你会发现，不管刷新多少次，Reviews面板总是显示为无星。</p>
<p>从本章开始处的架构图可以知道，productpage会访问reviews服务，我们看一下前者Sidecar日志，可以发现类似下面的内容：</p>
<pre class="crayon-plain-tag">"GET /reviews/0 HTTP/1.1" 200 - 0 295 85 84 "-" "python-requests/2.18.4" "4e51c6d9-b7d7-483f-912c-986b4ca1ccff"
# 目标服务      实际访问的IP:PORT组合    出站流量      访问reviews的v1版本                     # 服务端             客户端
"reviews:9080" "172.27.252.190:9080" outbound|9080|v1|reviews.default.svc.k8s.gmem.cc - 10.105.173.219:9080 172.27.121.172:56202</pre>
<p>在看看后者v1版本的Sidecar日志，可以发现相呼应的内容：</p>
<pre class="crayon-plain-tag">"GET /reviews/0 HTTP/1.1" 200 - 0 295 14 8 "-" "python-requests/2.18.4" "148f4c19-8106-4833-8a62-7b464b33e807" 
                                     # 入站流量
"reviews:9080" "127.0.0.1:9080"      inbound|9080||reviews.default.svc.k8s.gmem.cc    - 172.27.252.190:9080 172.27.121.172:49704</pre>
<p>你可以修改虚拟服务reviews，将v1替换为v2、v3，看看有何效果：</p>
<pre class="crayon-plain-tag">kubectl edit virtualservice reviews</pre>
<div class="blog_h3"><span class="graybg">基于请求头路由</span></div>
<p>虚拟服务支持根据请求信息路由，下面的例子依据请求头end-user的值决定使用什么版本的reviews服务：</p>
<pre class="crayon-plain-tag"># kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-test-v2.yaml

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
    - reviews
  http:
  # 如果登陆用户为alex，则使用v2版本
  - match:
    - headers:
        end-user:
          exact: alex
    route:
    - destination:
        host: reviews
        subset: v2
  # 否则使用v1版本
  - route:
    - destination:
        host: reviews
        subset: v1</pre>
<p>现在，刷新/productpage可以发现是无星，以alex登陆（密码随意）后则显示为黑星。 </p>
<div class="blog_h3"><span class="graybg">故障注入</span></div>
<p>故障注入（Fault Injection）属于破坏性测试，用于考验应用的韧性（resiliency）。</p>
<p>下面的例子，专为alex用户在reviews:v2和ratings之间引入7s的延迟：</p>
<pre class="crayon-plain-tag"># kubectl apply -f samples/bookinfo/networking/virtual-service-ratings-test-delay.yaml

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: ratings
spec:
  hosts:
  - ratings
  http:
  # 对于alex用户，全部请求引入7s延迟，使用ratings的v1版本
  - match:
    - headers:
        end-user:
          exact: alex
    fault:
      delay:
        percent: 100
        fixedDelay: 7s
    route:
    - destination:
        host: ratings
        subset: v1
  # 对于其它用户，也使用ratings的v1版本，但是不引入延迟
  - route:
    - destination:
        host: ratings
        subset: v1</pre>
<p>以alex访问/productpage，页面会在挂起6秒后提示reviews不可用。这是由于productpage到reviews的超时被硬编码为6秒，而reviews到ratings的超时被硬编码为10秒，我们的故障注入导致productpage访问reviews出现超时错误。</p>
<p>除了timeout之外，你还可以注入abort形式的故障，即立刻访问一个HTTP错误码。</p>
<div class="blog_h3"><span class="graybg">灰度发布</span></div>
<p>Istio支持逐步的将流量从一个版本迁移到另一个，通常是从老版本迁移到新版本。</p>
<p>定义虚拟服务时，你可以设置不同destination的权重，并逐步调整，直到完全切换到另一个版本。下面的例子会让你由50%的概率看到无星或红星：</p>
<pre class="crayon-plain-tag"># kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-50-v3.yaml

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
    - reviews
  http:
  - route:
    - destination:
        host: reviews
        subset: v1
      weight: 50
    - destination:
        host: reviews
        subset: v3
      weight: 50</pre>
<div class="blog_h3"><span class="graybg">设置请求超时</span></div>
<p>你可以为虚拟服务中的路由规则设置超时，<span style="background-color: #c0c0c0;">默认的HTTP调用超时为15秒</span>。</p>
<p>下面的例子把reviews服务的timeout设置为1ms，这意味着访问该服务的客户端都会基本都会收到超时错误：</p>
<pre class="crayon-plain-tag"># kubectl edit virtualservice reviews

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
  namespace: default
spec:
  hosts:
  - reviews
  http:
  - route:
    - destination:
        host: reviews
        subset: v1
    # 立即超时
    timeout: 1ms</pre>
<p>此时再访问/productpage，会提示product reviews are currently unavailable。</p>
<div class="blog_h3"><span class="graybg">使用HTTPS网关</span></div>
<p>istio-ingressgateway负责处理Gateway资源，就像K8S中Ingress Controller负责处理Ingress资源那样。</p>
<p>istio-ingressgateway默认已经同时支持HTTP/HTTPS，并在80/443端口上监听。</p>
<p>下面的例子创建一个HTTPS网关：</p>
<pre class="crayon-plain-tag">cat &lt;&lt;EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: httpbin-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 443
      name: https
      protocol: HTTPS
    tls:
      # 单向认证
      mode: SIMPLE
      # 下面指定istio-ingressgateway的Pod的一个文件系统路径，提示HTTPS所需的证书、私钥的所在位置
      # 默认情况下，网关控制器会将保密字典istio-ingressgateway-certs挂载到/etc/istio/ingressgateway-certs目录下
      # 但是此保密字典默认并不创建，挂载也是可选的（optional: true），本文在《安装到K8S》一章已经创建了保密字典

      # 如果你要多套数字证书需要使用，则必须在部署Charts时指定：
      #   --set gateways.istio-ingressgateway.secretVolumes[N].name=ingressgateway-*-certs \
      #   --set gateways.istio-ingressgateway.secretVolumes[N].secretName=istio-ingressgateway-*-certs \
      #   --set gateways.istio-ingressgateway.secretVolumes[N].mountPath=/etc/istio/ingressgateway-*-certs \
      #   ...
      # 来挂载所有数字证书，添加新证书后，deployment istio-ingressgateway必须被编辑以使用新证书，对应Pod会重启
      # 使用通配符证书以免除此麻烦
      serverCertificate: /etc/istio/ingressgateway-certs/tls.crt
      privateKey: /etc/istio/ingressgateway-certs/tls.key
      # 如果mode为MUTUAL则需要提供CA证书位置
      caCertificates: /etc/istio/ingressgateway-certs/ca.crt
    hosts:
    - "httpbin.k8s.gmem.cc"
EOF</pre>
<p>你总是需要一个VirtualService来配合Gateway：</p>
<pre class="crayon-plain-tag">cat &lt;&lt;EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin
spec:
  hosts:
  - "httpbin.k8s.gmem.cc"
  gateways:
  - httpbin-gateway
  http:
  - match:
    - uri:
        prefix: /status
    - uri:
        prefix: /delay
    route:
    - destination:
        port:
          number: 8000
        host: httpbin
EOF</pre>
<p>注意SSL Termination在网关，后端服务不需要启用HTTPS。</p>
<p>完成上述步骤后，访问https://httpbin.k8s.gmem.cc/status/418可以看到如下输出：</p>
<pre class="crayon-plain-tag"># 这里是入口网关的IP
curl -HHost:httpbin.k8s.gmem.cc --resolve httpbin.k8s.gmem.cc:443:10.102.172.91 https://httpbin.k8s.gmem.cc/status/418

#   -=[ teapot ]=-

#      _...._
#    .'  _ _ `.
#   | ."` ^ `". _,
#   \_;`"---"`|//
#     |       ;/
#     \_     _/
#       `"""`</pre>
<div class="blog_h3"><span class="graybg">使用双向认证网关</span></div>
<p>和上一节的单向认证类似，但双向（mutual TLS ）认证需要额外为入口网关控制器提供一个CA证书，用于校验客户端的数字证书。</p>
<p>使用官方Istio的Chart安装时，它默认从保密字典istio-ingressgateway-ca-certs加载CA证书，且挂载到/etc/istio/ingressgateway-ca-certs。</p>
<p>参考下面的代码修改Gateway的定义：</p>
<pre class="crayon-plain-tag">cat &lt;&lt;EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: httpbin-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 443
      name: https
      protocol: HTTPS
    tls:
      # 双向认证
      mode: MUTUAL
      # 客户端证书和私钥
      serverCertificate: /etc/istio/ingressgateway-certs/tls.crt
      privateKey: /etc/istio/ingressgateway-certs/tls.key
      # 服务器用来校验客户端证书的CA证书
      caCertificates: /etc/istio/ingressgateway-ca-certs/ca-chain.cert.pem
    hosts:
    - "httpbin.k8s.gmem.cc"
EOF</pre>
<p>然后，通过CA为客户端签发证书，并在访问服务器时指定自己的密钥和此证书：</p>
<pre class="crayon-plain-tag">curl ... --cacert /home/alex/Documents/puTTY/ca.crt --cert alex.cert --key alex.key ... </pre>
<div class="blog_h3"><span class="graybg">控制出口流量</span></div>
<p>要从服务网格内访问外部URL，需要配置绕过代理的IP范围，或者配置出口网关，否则无法访问仅仅能得到404：</p>
<pre class="crayon-plain-tag">SLEEP_POD=`kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name}`

kubectl exec -it $SLEEP_POD -c sleep -- curl -o /dev/null -s -w "%{http_code}\n" http://gmem.cc 
# 404</pre>
<p>下面是通过出口网关访问gmem.cc的例子：</p>
<pre class="crayon-plain-tag">cat &lt;&lt;EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: gmem-ext
spec:
  hosts:
  - gmem.cc
  ports:
  - number: 443
    name: https
    protocol: HTTPS
  # 如果解析名称：
  #   NONE，假设入站连接已经被解析。也就是说必须路由给请求发起者已经解析出的那个IP
  #   STATIC，使用Endpoint中定义的静态IP地址
  #   DNS，通过DNS服务器解析。可能转发给不同的IP（与请求者的解析结果不同）
  resolution: DNS
  # 服务位置：
  #   MESH_EXTERNAL，提示目标服务位于网格外部
  #   MESH_INTERNAL，提示目标服务位于网格内部。用于那些手工加入的服务，例如手工加入到K8S网格的VM
  #                  对于网格外部的服务，必须明确指定，否则Kiali会显示Unkown节点
  location: MESH_EXTERNAL
EOF</pre>
<p>现在再尝试上面的kubectl exec命令，会发现能正常获得响应。 </p>
<p>当访问外部HTTPS服务（TLS协议）时，<span style="background-color: #c0c0c0;">可能需要同时创建</span>ServiceEntry + VirtualService。此外如同访问网格内部服务一样，你可以<span style="background-color: #c0c0c0;">通过VirtualService对外部服务进行流量管理</span>。</p>
<p>下面的例子，允许网格内部访问mynewproddb.idc2提供的MySQL服务：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: mynewproddb
spec:
  hosts:
  - mynewproddb.idc2
  ports:
  - name: tcp
    number: 3306
    protocol: TCP
  resolution: NONE
  location: MESH_EXTERNAL</pre>
<div class="blog_h3"><span class="graybg"><a id=" circuit-breaker-example"></a>断路器</span></div>
<p>断路器（和异常检测）是创建韧性微服务应用所常用的一种模式，可以让应用程序免受<span style="background-color: #c0c0c0;">上游（Upstream，即调用链中更加远离根的节点）</span>服务<span style="background-color: #c0c0c0;">失败、延迟峰值（latency spikes）或其它网络异常</span>的侵害。</p>
<p>下面的示例配置了一个断路器，它在客户端访问httpbin服务时生效：</p>
<pre class="crayon-plain-tag">cat &lt;&lt;EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: httpbin
spec:
  host: httpbin
  trafficPolicy:
    # 在客户端Sidecar中维护连接池，包含断路器配置（负载限制）
    connectionPool:
      # 对应Envoy的断路器配置
      tcp:
        # 最大TCP连接数
        maxConnections: 1
      http:
        # 最大等待转发的、从应用容器发来的HTTP请求数
        http1MaxPendingRequests: 1
        # 每个TCP连接最多可以处理的HTTP请求数
        maxRequestsPerConnection: 1
    # 异常检测，用于剔除不正常的上游服务的实例
    outlierDetection:
      # 将实例从负载均衡池中剔除，需要连续的错误（HTTP 5XX或者TCP断开/超时）次数
      consecutiveErrors: 1
      # 分析是否需要剔除的频率，多久分析一次
      interval: 1s
      # 实例被剔除后，至少多久不得返回负载均衡池
      baseEjectionTime: 3m
      # 负载均衡池中最多有多大比例被剔除
      maxEjectionPercent: 100
EOF</pre>
<p>登陆到fortio来访问受断路器控制的httpbin服务： </p>
<pre class="crayon-plain-tag">FORTIO_POD=`kubectl get pod -l app=fortio -o  jsonpath={.items..metadata.name}`
                                           # 负载测试   # 并发2，不限QPS，执行20次     
kubectl exec -it $FORTIO_POD  -c fortio -- fortio load -c 2 -qps 0 -n 20 -loglevel Error http://httpbin:8000/get</pre>
<p>上面的fortio命令并发度为2，而断路器仅仅允许1个并发请求，因此会有几率出现错误：</p>
<pre class="crayon-plain-tag">Fortio 1.0.1 running at 0 queries per second, 4-&gt;4 procs, for 20 calls: http://httpbin:8000/get
Aggregated Function Time : count 20 avg 0.022291169 +/- 0.03153 min 0.000243608 max 0.086822606 sum 0.44582338
# target 50% 0.0055
# target 75% 0.0225
# target 90% 0.0834113
# target 99% 0.0864815
# target 99.9% 0.0867885
Sockets used: 5 (for perfect keepalive, would be 2)
# 错误率15%
Code 200 : 17 (85.0 %)
Code 503 : 3 (15.0 %)
All done 20 calls (plus 0 warmup) 22.291 ms avg, 86.0 qps</pre>
<p>如果将并发度设置为20，连续发送1000次，则错误率飙高到90%：</p>
<pre class="crayon-plain-tag">Code 200 : 94 (9.4 %)
Code 503 : 906 (90.6 %)</pre>
<p>查询客户端Proxy的统计信息，可以看到更多细节：</p>
<pre class="crayon-plain-tag">kubectl exec -it $FORTIO_POD  -c istio-proxy  -- sh -c 'curl localhost:15000/stats' | grep httpbin | grep pending

# cluster.outbound|8000||httpbin.default.svc.k8s.gmem.cc.upstream_rq_pending_active: 0
# cluster.outbound|8000||httpbin.default.svc.k8s.gmem.cc.upstream_rq_pending_failure_eject: 0
# pstream_rq_pending_overflow提示被断路掉的请求次数
# cluster.outbound|8000||httpbin.default.svc.k8s.gmem.cc.upstream_rq_pending_overflow: 10326
# cluster.outbound|8000||httpbin.default.svc.k8s.gmem.cc.upstream_rq_pending_total: 1016</pre>
<div class="blog_h3"><span class="graybg"><a id="mirror-example"></a>流量复制</span></div>
<p>下面的虚拟服务，将所有请求发送给v1，同时镜像一份给v2：</p>
<pre class="crayon-plain-tag">cat &lt;&lt;EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin
spec:
  hosts:
    - httpbin
  http:
  - route:
    - destination:
        host: httpbin
        subset: v1
      weight: 100
    # 流量镜像
    mirror:
      host: httpbin
      subset: v2
EOF</pre>
<p>发送给镜像服务的请求，其Host/Authority请求头被<span style="background-color: #c0c0c0;">自动加上-shadow后缀</span>。 </p>
<div class="blog_h2"><span class="graybg">试验遥测功能</span></div>
<p>利用Mixer和Sidercar，可以获得服务网格的各种统计指标（Metrics）、日志，或者跨越不同的服务进行分布式的调用链追踪。</p>
<div class="blog_h3"><span class="graybg">收集指标和日志</span></div>
<p>通过配置，Istio可以自动收集网格中某个服务的统计指标，或者为每次请求产生日志。</p>
<p>下面的例子，为每个请求生成一个Prometheus指标值：</p>
<pre class="crayon-plain-tag"># 定义一个指标（Metrics）类型的instances的规格，将Istio请求的属性映射为包含维度、度量的指标
# 针对每次请求，都会根据这里的配置，生成一个metrics类型的实例
apiVersion: "config.istio.io/v1alpha2"
kind: metric
metadata:
  # 名称
  name: doublerequestcount
  namespace: istio-system
spec:
  # 度量信息，为固定值2
  value: "2"
  # 维度信息
  dimensions:
    # 维度名   维度值
    # context,source,destination等都是预定义的Istio请求属性
    reporter: conditional((context.reporter.kind | "inbound") == "outbound", "client", "server")
    #                            竖线用于提供默认值
    source: source.workload.name | "unknown"
    destination: destination.workload.name | "unknown"
    #        除了请求属性表达式外，还可以使用字面值
    message: '"twice the fun!"'
  monitored_resource_type: '"UNSPECIFIED"'

---
# 配置一个prometheus适配器，也叫处理器（Handler）
# 适配器能够处理instances并和输出到外部系统
apiVersion: "config.istio.io/v1alpha2"
kind: prometheus
metadata:
  name: doublehandler
  namespace: istio-system
spec:
  metrics:
    # 生成的Prometheus指标的名称，注意，prometheus适配器总是自动添加istio_前缀
  - name: double_request_count
    # instance规格的全限定名称，提供当前适配器的输入
    instance_name: doublerequestcount.metric.istio-system
    # 指标的类型
    kind: COUNTER
    # 产生哪些标签
    label_names:
      # 这些维度作为标签
    - reporter
    - source
    - destination
    - message

---
# 将doublerequestcount.metric的所有instance派发给适配器prometheus
apiVersion: "config.istio.io/v1alpha2"
kind: rule
metadata:
  name: doubleprom
  # 定义在默认配置命名空间，且没有match配置，因此会派发所有instance
  namespace: istio-system
spec:
  actions:
  - handler: doublehandler.prometheus
    instances:
    - doublerequestcount.metric</pre>
<p>下面的例子，为每个请求产生一条日志：</p>
<pre class="crayon-plain-tag"># 定义另外一种instance —— 日志条目 —— 的规格
apiVersion: "config.istio.io/v1alpha2"
kind: logentry
metadata:
  name: newlog
  namespace: istio-system
spec:
  # 日志严重度级别
  severity: '"warning"'
  # 日志时间戳
  timestamp: request.time
  # 日志变量
  variables:
    source: source.labels["app"] | source.workload.name | "unknown"
    user: source.user | "unknown"
    destination: destination.labels["app"] | destination.workload.name | "unknown"
    responseCode: response.code | 0
    responseSize: response.size | 0
    latency: response.duration | "0ms"
  monitored_resource_type: '"UNSPECIFIED"'
---

# 这种处理器处理instance并输出到标准输出
apiVersion: "config.istio.io/v1alpha2"
kind: stdio
metadata:
  name: newhandler
  namespace: istio-system
spec:
 severity_levels:
   # 将instance.severity == warning映射为Params.Level.WARNING这个日志级别
   warning: 1 
 # 生成JSON格式的日志
 outputAsJson: true

---
# 将logentry的所有实例发送给stdio处理
apiVersion: "config.istio.io/v1alpha2"
kind: rule
metadata:
  name: newlogstdio
  namespace: istio-system
spec:
  # 匹配所有请求
  match: "true"
  actions:
   - handler: newhandler.stdio
     instances:
     - newlog.logentry
---</pre>
<p> 将上述两端配置文件保存为yaml文件，并通过kubectl apply命令安装到K8S，然后进行下一步试验。 </p>
<div class="blog_h3"><span class="graybg">查询指标</span></div>
<p>打开Istio<a href="http://prometheus.istio-system.svc.k8s.gmem.cc:9090/">内置Prometheus服务的Web界面</a>， 输入istio_double_request_count进行查询，可以看到每个微服务的请求次数都被client、server方分别报送，并预先聚合了。</p>
<p>实际上Istio官方提供的Chart中，以及提供了HTTP/TCP请求的次数、持续时间、请求长度、应答长度，以及访问日志的instance/handler/rule配置，可以直接安装使用。</p>
<div class="blog_h3"><span class="graybg">指标可视化</span></div>
<p>打开<a href="http://grafana.istio-system.svc.k8s.gmem.cc:3000/">Istio内置Grafana服务的Web界面</a>，可以看到Istio目录下预定义好了7个仪表盘页面。该服务使用grafana/grafana:5.0.4版本的镜像。</p>
<div class="blog_h2"><span class="graybg">试验策略功能</span></div>
<div class="blog_h3"><span class="graybg">启用请求限速</span></div>
<p>参考<a href="#rate-limits">配置请求限速</a>一节。</p>
<div class="blog_h3"><span class="graybg">禁止访问</span></div>
<p>你可以使用Mixer中可见的任何请求属性来控制对服务的访问。下面的例子禁用了对ratings:v3的访问：</p>
<pre class="crayon-plain-tag"># 这种适配器用于提示Envoy拒绝访问
apiVersion: "config.istio.io/v1alpha2"
kind: denier
metadata:
  name: denyreviewsv3handler
spec:
  status:
    # 状态码和消息
    code: 7
    message: Not allowed
---
# instance
apiVersion: "config.istio.io/v1alpha2"
kind: checknothing
metadata:
  name: denyreviewsv3request
spec:
---
# rule
apiVersion: "config.istio.io/v1alpha2"
kind: rule
metadata:
  name: denyreviewsv3
spec:
  # 只要是reviews访问ratings的v3版本，立即拒绝
  match: destination.labels["app"] == "ratings" &amp;&amp; source.labels["app"]=="reviews" &amp;&amp; source.labels["version"] == "v3"
  actions:
  - handler: denyreviewsv3handler.denier
    instances: [ denyreviewsv3request.checknothing ]</pre>
<div class="blog_h3"><span class="graybg">黑白名单</span></div>
<p>Istio也支持基于请求属性的黑白名单，下面的例子和上面的denier是等价的：</p>
<pre class="crayon-plain-tag"># 列表检查器，值匹配列表项则进入黑或白名单
apiVersion: config.istio.io/v1alpha2
kind: listchecker
metadata:
  name: whitelist
spec:
  overrides: ["v1", "v2"]  # 静态值列表
  # 这是白名单而非黑名单
  blacklist: false         

---
# 列表条目instance，从请求抽取单个值
apiVersion: config.istio.io/v1alpha2
kind: listentry
metadata:
  name: appversion
spec:
  # 取版本号
  value: destination.labels["version"]

---
# 关联两者，效果就是检查版本号，如果不是v1/v2则拒绝访问
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: checkversion
spec:
  match: destination.labels["app"] == "ratings"
  actions:
  - handler: whitelist.listchecker
    instances:
    - appversion.listentry&amp;nbsp;</pre>
<div class="blog_h2"><span class="graybg">试验分布式追踪</span></div>
<p>以--set tracing.enabled=true选项安装Istio Chart，即可使用。由于默认设置的采样率较低，你<span style="background-color: #c0c0c0;">需要反复访问多次/productpage才能捕获到Trace</span>。</p>
<div class="blog_h3"><span class="graybg">Search</span></div>
<p>打开<a href="https://jaeger.k8s.gmem.cc/search">Istio内置Jaeger服务的Web界面</a>，点击顶部按钮切换到Search页签，在Find Traces面板中Service选取productpage，点击Find Traces，看到如下的Trace列表：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/09/jaeger-search.png"><img class="aligncenter  wp-image-23759" src="https://blog.gmem.cc/wp-content/uploads/2018/09/jaeger-search.png" alt="jaeger-search" width="914" height="484" /></a></p>
<p>点击其中一个Trace，可以查看其调用链信息：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/09/jaeger-search-2.png"><img class="aligncenter  wp-image-23761" src="https://blog.gmem.cc/wp-content/uploads/2018/09/jaeger-search-2.png" alt="jaeger-search-2" width="919" height="228" /></a></p>
<p>可以看到，这个由<span style="background-color: #c0c0c0;">单次/productpage请求</span>产生的调用链由6个Span构成，每个Span对应一个彩色条带。每个Span消耗的时间都标注在界面上了。</p>
<div class="blog_h3"><span class="graybg">Dependencies</span></div>
<p>打开<a href="https://jaeger.k8s.gmem.cc/dependencies">Istio内置Jaeger服务的Web界面</a>，点击顶部按钮切换到Dependencies页签，可以看到Jaeger依据Traces计算出的服务依赖关系图（DAG，有向无环图）：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/09/jaeger-dependencies.png"><img class="aligncenter  wp-image-23757" src="https://blog.gmem.cc/wp-content/uploads/2018/09/jaeger-dependencies.png" alt="jaeger-dependencies" width="787" height="565" /></a></p>
<p>&nbsp;</p>
<div class="blog_h3"><span class="graybg">Trace如何收集</span></div>
<p>尽管Istio的Sidecar能够<span style="background-color: #c0c0c0;">自动发送Span给Jaeger</span>，要将这些Span合并为完整的Trace还需要额外的信息，应用程序必须提供必要的HTTP请求头。</p>
<p>每个微服务都需要收集调用它的HTTP请求的x-request-id、x-b3-traceid、x-b3-spanid、x-b3-parentspanid、x-b3-sampled、x-b3-flags、x-ot-span-context头，并在它<span style="background-color: #c0c0c0;">调用任何上游（Upstream，上图的ratings是reviews的Upstream）微服务时，带上这些头</span>。</p>
<p>Bookinfo中，productpage服务基于Python编写，收集请求头的代码如下：</p>
<pre class="crayon-plain-tag">def getForwardHeaders(request):
    headers = {}

    if 'user' in session:
        headers['end-user'] = session['user']

    incoming_headers = [ 'x-request-id',
                         'x-b3-traceid',
                         'x-b3-spanid',
                         'x-b3-parentspanid',
                         'x-b3-sampled',
                         'x-b3-flags',
                         'x-ot-span-context'
    ]

    for ihdr in incoming_headers:
        val = request.headers.get(ihdr)
        if val is not None:
            headers[ihdr] = val

    return headers</pre>
<p>reviews服务则基于Java编写， 收集请求头的代码如下：</p>
<pre class="crayon-plain-tag">@GET
@Path("/reviews/{productId}")
public Response bookReviewsById(@PathParam("productId") int productId,
                            @HeaderParam("end-user") String user,
                            @HeaderParam("x-request-id") String xreq,
                            @HeaderParam("x-b3-traceid") String xtraceid,
                            @HeaderParam("x-b3-spanid") String xspanid,
                            @HeaderParam("x-b3-parentspanid") String xparentspanid,
                            @HeaderParam("x-b3-sampled") String xsampled,
                            @HeaderParam("x-b3-flags") String xflags,
                            @HeaderParam("x-ot-span-context") String xotspan) {
  int starsReviewer1 = -1;
  int starsReviewer2 = -1;

  if (ratings_enabled) {
    JsonObject ratingsResponse = getRatings(Integer.toString(productId), u
      ser, xreq, xtraceid, xspanid, xparentspanid, xsampled, xflags, xotspan);
  }
}</pre>
<div class="blog_h3"><span class="graybg">关于采样率</span></div>
<p>Istio默认会追踪任何请求， 对于开发、测试环境这是可以的，线上环境通常需要降低采样率，否则压力太大。调整采样率有两种方式：</p>
<ol>
<li>使用官方Chart安装时，调整pilot.traceSampling，当前使用的Chart版本已经将其设置为1.0，即百分之一的采样率</li>
<li>修改Deployment istio-pilot的环境变量PILOT_TRACE_SAMPLING</li>
</ol>
<p>采样率的精度是0.01</p>
<div class="blog_h2"><span class="graybg">删除示例应用</span></div>
<pre class="crayon-plain-tag">samples/bookinfo/platform/kube/cleanup.sh
kubectl delete -f samples/httpbin/httpbin.yaml
kubectl delete -f  samples/httpbin/sample-client/fortio-deploy.yaml
kubectl delete -f samples/sleep/sleep.yaml</pre>
<div class="blog_h1"><span class="graybg">流量管理基础</span></div>
<p>Istio的流量管理模型，从根本上将流量（traffic flow）和基础设施扩容（infrastructure scaling）进行了解耦。通过Pilot，你可以通过更<span style="background-color: #c0c0c0;">细致的规则</span>来说明流量如何流动，而<span style="background-color: #c0c0c0;">不是简单的指定哪个VM/Pod来接收流量</span>。例如，你可以指定某个服务的5%的流量发往金丝雀版本（不管金丝雀的实例数量），或者指定根据请求内容来选择特定版本的服务：</p>
<p><img class="aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2018/09/TrafficManagementOverview.svg" alt="" width="922" height="692" /></p>
<p>将流量（traffic flow）和基础设施扩容解耦后，Istio可以在应用程序代码外部提供大量的流量管理特性，包括AB测试的请求路由、逐步发布、金丝雀发布，基于<span style="background-color: #c0c0c0;">超时、重试、断路器的故障恢复</span>，以及为了测试故障恢复策略而进行的故障注入（fault injection）。</p>
<div class="blog_h2"><span class="graybg">Pilot和Envoy</span></div>
<p><span style="background-color: #c0c0c0;">流量管理的核心组件是Pilot</span>，它<span style="background-color: #c0c0c0;">管理、配置</span>所有部署在服务网格中的Envoy代理实例，<span style="background-color: #c0c0c0;">对Envoy的完整生命周期负责</span>。Pilot除了可以配置路由规则、配置故障恢复特性之外，还维护网格中<span style="background-color: #c0c0c0;">所有服务的规范（canonical）模型</span>。Envoy就是基于此模型，通过自己的发现服务，找到网格中其它Envoy。</p>
<p>每个Envoy实例都基于从<span style="background-color: #c0c0c0;">Pilot得到的信息</span>、<span style="background-color: #c0c0c0;">以及</span>针对负载均衡池中的实例进行周期性<span style="background-color: #c0c0c0;">的健康检查</span>，来<span style="background-color: #c0c0c0;">维护负载均衡信息</span>。根据此负载均衡信息，Envoy能够在遵守路由规则的同时，智能的在目的实例之间分发请求。</p>
<p><img class="alignnone" src="https://cdn.gmem.cc/wp-content/uploads/2018/09/PilotAdapters.svg" alt="" width="916" height="641" /></p>
<p>规范模型是独立于底层平台的，和底层平台的对接由Pilot中的适配器负责，适配器调用底层平台的API（例如K8S适配器实现了必要的控制器来Watch API Server，获取Pod、Ingress等资源的变动），并产生适当的规范模型。</p>
<p>Pilot启用了<span style="background-color: #c0c0c0;">服务发现</span>，<span style="background-color: #c0c0c0;">路由表</span>，以及对<span style="background-color: #c0c0c0;">负载均衡池的动态更新</span>。</p>
<p>使用Pilot的规则配置（<span style="background-color: #c0c0c0;">Rule Configuration</span>），可以制定高级别的流量管理规则。这些规则会被转换为低级别的配置信息，并分发给Envoy实例。</p>
<div class="blog_h2"><span class="graybg">请求路由</span></div>
<div class="blog_h3"><span class="graybg">服务版本</span></div>
<p>如上文所述，网格中服务的规范模型由Pilot维护。Istio还引入了<span style="background-color: #c0c0c0;">服务版本的概念，用于对服务实例进行细粒度区分</span>。服务版本既可以指服务API的版本（v1,v2），也<span style="background-color: #c0c0c0;">可以指服务运行环境（stg,pdt）或任何其它属性</span>。</p>
<p><img class="aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2018/09/ServiceModel_Versions.svg" alt="" width="920" height="920" /></p>
<p>在上图中，服务的客户端对服务的不同版本毫无感知。客户端仍然使用服务的IP或主机名来访问，只是Envoy在客户端-服务器之间拦截并转发了请求和响应。</p>
<p>Envoy根据你通过Pilot指定的路由规则，决定实际使用的服务版本。这让应用程序代码和被依赖服务之间实现解耦，可分别独立演化。在路由规则中，你可以指定让Envoy依据<span style="background-color: #c0c0c0;">源/目标的消息头、Tag来判定服务版本</span>。</p>
<div class="blog_h3"><span class="graybg">入口/出口</span></div>
<p>Istio假设服务网格的所有出入流量都经由Envoy代理转发。通过这种前置代理可以实现面向用户的服务，包括AB测试、金丝雀部署。在访问<span style="background-color: #c0c0c0;">外部服务</span>时，Envoy可以实现必要的<span style="background-color: #c0c0c0;">故障恢复特性，包括超时、重试，并获取外部服务的性能指标</span>。</p>
<p><img class="alignnone" src="https://cdn.gmem.cc/wp-content/uploads/2018/09/ServiceModel_RequestFlow.svg" alt="" width="915" height="392" /></p>
<div class="blog_h2"><span class="graybg">发现和负载均衡</span></div>
<p>Istio能够在网格中服务实例之间进行负载均衡。 </p>
<p>Istio假设基础设置提供一个服务注册表（service registry，例如K8S的Service），此表跟踪某个服务的VM/Pod集。此<span style="background-color: #c0c0c0;">服务注册表应当负责新实例的加入、不健康实例的移除</span>。</p>
<p>Istio从服务注册表中读取服务的信息，并生成平台无关的服务发现接口。Envoy执行服务发现（基于xDS和Pilot交互），并动态的更新其本地负载均衡池。</p>
<p><img class="alignnone" src="https://cdn.gmem.cc/wp-content/uploads/2018/11/LoadBalancing.svg" alt="" width="915" height="732" /></p>
<p>网格中的服务，利用DNS名称来相互访问。所有针对某个服务的流量，都会在出站前经由Envoy进行重新路由。负载均衡池是决定路由到哪个节点的关键因素。Envoy<a href="https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/load_balancing">支持多种负载均衡算法</a>，但是Istio目前<span style="background-color: #c0c0c0;">仅仅允许：轮询、随机、带权重的最少请求</span>。</p>
<p>Envoy还会周期性的检查负载均衡池中实例的健康情况，这种检查从外部（准确的说是服务消费者）发出，而不是像K8S Pod健康检查那样，在Pod本地执行。</p>
<p>判断实例是否健康时，Envoy遵循一种断路器模式（circuit breaker pattern），此模式下调用实例API的故障率决定了实例是否健康。当健康检查连续失败指定的次数后，实例被移除负载均衡池；当被移除的实例连续成功指定次数后，它又回到负载均衡池。</p>
<p>如果实例以503代码响应健康检查请求，则它会立即被调用者移出负载均衡池。</p>
<div class="blog_h2"><span class="graybg">错误处理</span></div>
<p>Envoy提供了一系列开箱即用的错误恢复机制：</p>
<ol>
<li>超时</li>
<li>有限次数的重试，支持可变的重试延迟（jitter）</li>
<li>限制针对上游服务的并发连接数、并发请求数</li>
<li>主动健康检查——针对负载均衡池中所有成员进行健康检查，并移除不健康实例，移回健康实例</li>
<li>可精细控制的断路器（被动健康检查） ，同样针对负载均衡池中的每个实例</li>
</ol>
<p>所有这些特性，都可以在运行时，利用Istio的流量管理规则动态配置。</p>
<p>联用主动、被动健康检查，可以更大程度上降低访问到不健康实例的几率。联用底层基础设施的健康检查机制，不健康实例可以很快的被剔除。 </p>
<p>利用流量管理规则，你可以为<span style="background-color: #c0c0c0;">每个服务/版本</span>来配置默认的故障恢复特性。</p>
<div class="blog_h2"><span class="graybg">故障注入</span></div>
<p>用于辅助测试端对端的故障恢复能力。</p>
<p>Istio支持多种具体的协议，并向其注入错误信息。而不是粗暴的杀死Pod，或者在TCP层延迟/污染网络包。你可以在应用层注入更加有意义的错误，例如HTTP的状态码。</p>
<p>Istio支持对特定请求进行错误注入，或者指定多少百分比的请求被错误注入。</p>
<p>可以注入的错误有两类：</p>
<ol>
<li>延迟：模拟网络延迟和上游服务过载</li>
<li>中止：模拟上游服务错误，通常使用HTTP错误码、TCP连接错误的方式</li>
</ol>
<div class="blog_h2"><span class="graybg">流量镜像/复制</span></div>
<p>流量镜像（Traffic mirroring）也叫shadowing，允许开发团队在最小风险的前提下改变产品的功能。其做法是<span style="background-color: #c0c0c0;">将线上环境的流量复制到一个“镜像服务“中</span>，镜像服务和主请求处理链是隔离的。</p>
<p>VirtualService支持为route配置mirror，实现流量镜像。</p>
<div class="blog_h2"><span class="graybg">网关</span></div>
<p>网关是运行在服务网格边缘的负载均衡器，它处理入站或出站的TCP/HTTP连接。</p>
<p>Istio提供入口网关，出口网关，对应：</p>
<ol>
<li>的CRD为Gateway、Gateway+ServiceEntry</li>
<li>的Controller为istio-ingressgateway、istio-egressgateway</li>
</ol>
<p>当提起术语“网关”时，基于上下文可能指Gateway这种资源，也可能指出口/入口网关的Service/Pod。</p>
<div class="blog_h3"><span class="graybg">IngressGateway</span></div>
<p>在典型的Kubernetes环境下，Ingress控制器读取Ingress资源，提供外部访问K8S集群的入口。</p>
<p>而使用Istio时，<span style="background-color: #c0c0c0;">Ingress资源的地位可以由Gateway + VirtualServices代替</span>。在<span style="background-color: #c0c0c0;">集群内部组件相互交互时，不需要配置Gateway资源</span>。需要注意，Istio也可以作为Ingress控制器，处理K8S原生的Ingress资源。</p>
<p>使用Istio Gateway时的典型的客户端访问时序如下：</p>
<ol>
<li>客户端发起针对负载均衡器的请求</li>
<li>负载均衡器将请求转发给集群的istio-ingressgateway服务，该服务可以部署为NodePort，它的后端是一个Deployment</li>
<li><span style="background-color: #c0c0c0;">IngressGateway根据Gateway资源、VirtualService资源的配置信息</span>，将请求路由给K8S的Service
<ol>
<li>Gateway提供端口、协议、数字证书信息</li>
<li>VirtualService提供到K8S Service的路由信息</li>
</ol>
</li>
<li>K8S Service将请求路由给Pod</li>
</ol>
<p>该时序如下图所示，注意<span style="background-color: #c0c0c0;">IngressGateway的Sidecar负责转发客户端请求</span>：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/09/istio-networking.png"><img class="aligncenter size-full wp-image-23533" src="https://blog.gmem.cc/wp-content/uploads/2018/09/istio-networking.png" alt="istio-networking" width="692" height="488" /></a></p>
<p>&nbsp;</p>
<p>每当你创建/修改Gateway、VirtualService资源，<span style="background-color: #c0c0c0;">Pilot会监测</span>到并将其转换为Envoy配置，然后发送给<span style="background-color: #c0c0c0;">相关的Sidecar</span>，<span style="background-color: #c0c0c0;">包括</span>运行在<span style="background-color: #c0c0c0;">IngressGateway</span> Pod中的Envoy。</p>
<div class="blog_h3"><span class="graybg">EgressGateway</span></div>
<p>默认情况下，启用了Istio的微服务<span style="background-color: #c0c0c0;">无法访问集群外部的URL</span>， 这是因为Envoy修改了Pod所在网络命名空间的<span style="background-color: #c0c0c0;">Iptables，透明的将所有出口流量都重定向到Sidecar</span>，而不经任何配置Sidecar<span style="background-color: #c0c0c0;">只会处理网格内部的Destination</span>。</p>
<p>要将外部服务暴露给启用了Sidecar的网格内部Pod/VM（它们是外部服务的客户端），必须：</p>
<ol>
<li><span style="background-color: #c0c0c0;">定义ServiceEntry</span>，这种自定义资源允许针对指定的外部IP/DNS名称的流量通过Sidecar。<span style="background-color: #c0c0c0;">ServiceEntry支持通配符</span>匹配主机，可以降低配置工作量</li>
<li><span style="background-color: #c0c0c0;">或者，配置一个IP范围，让针对此IP范围的出站请求经过/绕过Envoy代理</span>。你可以设置Chart变量，<br />
<pre class="crayon-plain-tag"># 仅仅10.5.0.0/16走Envoy代理，其它直连。你可以将此Pod网络、K8S服务网络的CIDR配置到该字段中。这样，所有针对网格外部的访问都开放了
--global.proxy.includeIPRanges="10.5.0.0/16"</pre></p>
<p>则当从网格内部访问10.5网段时直接绕过代理。<br />注意：此特性已经弃用，可以使用配置字典istio-sidecar-injector中的的includeOutboundIpRanges，或者Pod注解traffic.sidecar.istio.io/includeOutboundIPRanges代替</p>
</li>
</ol>
<p>强调一下，<span style="background-color: #c0c0c0;">访问集群内部域名、IP地址不受限制</span>，即使不配置VirtualService也可以访问，而且Kiali直接可以识别出这种流量。</p>
<p>ServiceEntry还可以配合Gateway+VirtualService，将<span style="background-color: #c0c0c0;">出口流量导向特殊的服务 —— istio-egressgateway</span>，这种配置方式的适用场景是：</p>
<ol>
<li>离开网格的所有流量必须流经一组专用节点，这一组节点有特殊的监控和审查</li>
<li>集群中的一般性节点不能联通网格外部</li>
</ol>
<div class="blog_h1"><span class="graybg">流量管理配置</span></div>
<p>Istio使用一个简单的配置模型来描述API调用/TCP流量应该如何在服务网格中流动。使用此配置模型，你可以：</p>
<ol>
<li>配置<span style="background-color: #c0c0c0;">服务级别的属性</span>，例如断路器、超时、重试</li>
<li>配置通用的<span style="background-color: #c0c0c0;">持续交付（CD）任务</span>，例如金丝雀发布（Canary rollout，即灰度发布）、A/B测试、阶段性部署（Staged rollout）——基于百分比的流量分配 </li>
</ol>
<p>Istio的配置模型映射为K8S的资源，包括VirtualService、DestinationRule、ServiceEntry、Gateway四种。在K8S中你可以用kubectl来配置这些资源，它们都是CR。</p>
<div class="blog_h2"><span class="graybg">VirtualService</span></div>
<p>定义服务的请求<span style="background-color: #c0c0c0;">如何在服务网格中路由</span>。虚拟服务能够将请求路由给<span style="background-color: #c0c0c0;">服务的不同版本</span>，甚至是<span style="background-color: #c0c0c0;">完全不同的服务</span>。</p>
<p>VirtualService通过Istio的服务注册表定义了如何路由到一个或多个特定的Destinations（目标服务）。VirtualService可以根据请求的属性（如HTTP头、路径等）来匹配请求，并根据这些规则将流量路由到不同的目标。这意味着，对于给定的域名，基于请求的不同属性（比如URL路径），VirtualService可以将流量路由到多个不同的Destination。</p>
<p><span style="background-color: #c0c0c0;">虚拟服务自身不会在Istio服务注册表中添加新的服务条目。它的spec.hosts，用来指定进行路由时，如何匹配主机名</span>，这通常对应HTTP请求的Host头。</p>
<p>Istio服务注册表中服务条目的来源包括：</p>
<ol>
<li>K8s服务</li>
<li>ServiceEntry定义的服务</li>
<li>Consul、Eureka等服务发现机制暴露的服务</li>
<li>在某些情况下，服务信息可以通过Istio的配置文件静态定义。这种方式较少用于生产环境，但可能在测试或特定场景下有其用途</li>
</ol>
<div class="blog_h3"><span class="graybg">按版本路由</span></div>
<p>下面的示例，将针对review服务的请求，3/4发给v1版本，1/4发给v2版本：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  # 虚拟服务的名字
  name: reviews
spec:
  hosts:
  # 访问到此虚拟服务时，使用主机名，可以是DNS名称、带通配符的DNS名称、IP地址
  # DNS名称可以是FQDN，对于K8S这样的平台，可以使用短DNS名称（无点号），例如使用reviews代替reviews.default.svc.cluster.local
  # Istio会根据此虚拟服务（而非K8S的Service）所在的命名空间，生成FQDN
  # 注意：
  # 1、每个host只能被单个虚拟服务声明。单个虚拟服务可以用于描述多个HTTP/TCP端口的流量属性
  # 2、对于HTTP/TCP流量，hosts字段均生效
  # 3、对于网格内部的服务（存在于服务注册表），必须总是使用服务DNS名，不得使用IP地址
  - reviews
  http:
  # 路由规则的数组
  - route:
    - destination:
        # host必须是Istio服务注册表中，没有歧义的名称。此注册表中的服务来源包括：
        # 1、底层平台（K8S）服务注册表的全部条目（即K8S Service）
        # 2、通过ServiceEntry声明的服务条目
        #
        # 对于K8S用户需要注意，使用DNS短名时，Istio将根据DestinationRule的命名空间来生成FQDN
        # 而不是当前VirtualService的命名空间
        host: reviews
        # 发送给reviews服务实例的v1子集
        # 这里仅仅指出子集的名称，子集的规格通过DestinationRule配置
        subset: v1
      # 路由到此版本的权重，取值0-100
      weight: 75
    - destination:
        host: reviews
        # 如果目标服务仅仅暴露单个端口，则不需要声明下面这个字段
        port: 80
        subset: v2
      weight: 25</pre>
<div class="blog_h3"><span class="graybg">超时和重试</span></div>
<p>默认情况下HTTP请求的超时为15秒，你可以覆盖此默认值。重试也类似：</p>
<pre class="crayon-plain-tag">...
spec:
  ...
  http:
  - route:
    # 超时
    timeout: 10s
    # 重试
    retries:
      # 最多重试次数
      attempts: 3
      # 每次重试的超时
      perTryTimeout: 2s</pre>
<p>也可以在请求级别覆盖超时、重试配置，只需要提供Envoy规定的特殊请求头x-envoy-upstream-rq-timeout-ms、x-envoy-max-retries即可。 </p>
<div class="blog_h3"><span class="graybg">故障注入</span></div>
<p>你可以为http进行故障注入，要么是delay，要么是abort。</p>
<p>下面的例子，为ratings服务的v1版本10%的请求引入5秒的延迟：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: ratings
spec:
  hosts:
  - ratings
  http:
  - fault:
      delay:
        # 多少比例的请求被注入故障
        percent: 10
        # 引入固定5秒的延迟
        fixedDelay: 5s
    route:
    - destination:
        host: ratings
        subset: v1</pre>
<p>下面的例子，则为10%的请求引发400错误：</p>
<pre class="crayon-plain-tag">...
spec:
  ...
  http:
  - fault:
      abort:
        # 多少比例的请求被注入故障
        percent: 10
        # 返回的HTTP状态码
        httpStatus: 400</pre>
<p>delay和abort是可以联合使用的：</p>
<pre class="crayon-plain-tag">...
metadata:
  name: ratings
spec:
  http:
  # 从reviews服务v2版本发起的，针对ratings服务v1版本的请求
  - match:
    - sourceLabels:
        app: reviews
        version: v2
    fault:
      # 引入5秒延迟
      delay:
        fixedDelay: 5s
      # 为10%请求引发400错误
      abort:
        percent: 10
        httpStatus: 400</pre>
<div class="blog_h3"><span class="graybg">有条件路由</span></div>
<p>规则可以仅仅针对满足特定条件的请求。</p>
<p>可以<span style="background-color: #c0c0c0;">限定请求者的标签</span>，在K8S环境下即Pod的Label：</p>
<pre class="crayon-plain-tag">...
spec:
  http:
  # 仅针对reviews服务v2版本发起的请求
  - match:
    - sourceLabels:
        app: reviews
        version: v2
    route: ...</pre>
<p>也可以<span style="background-color: #c0c0c0;">限定HTTP请求头</span>： </p>
<pre class="crayon-plain-tag">...
spec:
  http:
  - match:
    - headers:
        # end-user头必须为alex
        end-user:
          exact: alex</pre>
<p>还可以<span style="background-color: #c0c0c0;">限定请求URL路径、HTTP方法、HTTP Scheme、HTTP Authority值</span>： </p>
<pre class="crayon-plain-tag">...
spec:
  http:
  - match:
    - uri:
        # 请求URL，除去scheme://host部分，必须以/api/v1开头
        # 除了prefix，还可以配置
        #   exact，表示精确匹配
        #   regex，表示正则式匹配
        prefix: /api/v1

    - scheme:
        exact: https
    - authority:
        regex: ".*.gmem.cc"
    - method:
        regex: "GET|POST"</pre>
<p>多个条件可以进行逻辑与/或。在单个match元素中声明多个条件，则为AND：</p>
<pre class="crayon-plain-tag">spec:
  http:
  - match:
    # 同时限定Pod标签、请求头
    - sourceLabels:
        app: reviews
        version: v2
      headers:
        end-user:
          exact: alex</pre>
<p>在多个match元素中分别声明，则为OR：</p>
<pre class="crayon-plain-tag">spec:
  http:
  - match:
    # Pod标签匹配
    - sourceLabels:
        app: reviews
        version: v2
    # 或者请求头匹配
    - headers:
        end-user:
          exact: alex</pre>
<div class="blog_h3"><span class="graybg">多规则优先级</span></div>
<p>如果对于相同的目的地（host + subset），配置了两个或更多的规则（http子元素），那么<span style="background-color: #c0c0c0;">第一个（配置文件最前面）匹配的规则优先级最高</span>。 </p>
<div class="blog_h3"><span class="graybg">CORS配置</span></div>
<p>Istio支持为虚拟服务配置跨源资源共享策略：</p>
<pre class="crayon-plain-tag">spec:
  http:
  - route:
    corsPolicy:
      # 允许发起CORS请求的源，设置为*则允许任何源
      allowOrigin:
      - gmem.cc
      # CORS请求可以使用的HTTP方法
      allowMethods:
      - POST
      - GET
      # Access-Control-Allow-Credentials头
      allowCredentials: false
      # 允许的请求头
      allowHeaders:
      - X-Foo-Bar
      # preflight request可以被缓存的时间
      maxAge: "1d" </pre>
<div class="blog_h3"><span class="graybg">请求重定向</span></div>
<p>虚拟服务可以直接返回301重定向给调用者：</p>
<pre class="crayon-plain-tag">spec:
  http:
  redirect:
    # 重定向时覆盖URL查询路径
    uri: /v1/bookRatings
    # 重定向时覆盖URL的Authority（Host）部分
    authority: newratings.default.svc.cluster.local</pre>
<div class="blog_h3"><span class="graybg">请求重写</span></div>
<p>虚拟服务可以在转发请求给Dest之前，对HTTP请求的特定部分进行修改：</p>
<pre class="crayon-plain-tag">spec:
  http:
  - match:
    - uri:
        prefix: /ratings
    # 将URL前缀从/ratings修改为/v1/bookRatings
    rewrite:
      uri: /v1/bookRatings
      authority: ...</pre>
<div class="blog_h3"><span class="graybg">流量复制</span></div>
<p>参考<a href="#mirror-example">上文的例子</a>。</p>
<div class="blog_h3"><span class="graybg">TCP服务</span></div>
<p>这里是一个TCP路由的例子：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: tcp-route
  namespace: default
spec:
  hosts:
  - mesh.gmem.cc
  tcp:
  - match:
    - port: 3306
      source_labels:
        app: mysql-client
    route:
    - destination:
        host: mysql
        subset: v2
        port:
          number: 3306
      weight: 100
  - match:
    - port: 2181
    route:
    - destination:
        host: zk
        subset: v1
        port:
          number: 2181
      weight: 100</pre>
<div class="blog_h2"><span class="graybg">DestinationRule</span></div>
<p>在VirtualService定义了路由规则后，DestinationRule进一步应用一系列的策略到请求上。DestinationRule定义了对于特定目标服务的流量策略，如负载均衡策略、连接池大小、TLS设置等。当VirtualService根据规则将流量路由到特定目标后，DestinationRule定义了如何处理到达这些目标的流量。</p>
<p>DestinationRule中的spec.host同样是指定这个规则应用到哪些请求，不会在Istio注册表中定义服务条目。</p>
<div class="blog_h3"><span class="graybg">服务子集</span></div>
<p>下面的示例，声明了服务子集：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: reviews
spec:
  # 访问什么服务时应用此规则
  host: reviews
  # 配置服务的子集
  # 在K8S中，服务的具有标签version=v1的Pod，并加入到v1子集
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2</pre>
<div class="blog_h3"><span class="graybg">简单负载均衡</span></div>
<p>可以针对所有子集配置：</p>
<pre class="crayon-plain-tag">...
spec:
  host: reviews
  trafficPolicy:
    # 负载均衡策略
    loadBalancer:
      # 算法：ROUND_ROBIN（默认）
      #      LEAST_CONN， O(1)复杂度算法，随机选取两个健康实例，并取其中活动请求少的
      #      RANDOM
      #      PASSTHROUGH，直接转发给调用者请求的IP地址，不进行任何负载均衡
      simple: RANDOM </pre>
<p>也可以针对特定子集：</p>
<pre class="crayon-plain-tag">spec:
  subsets:
  - name: v2
    labels:
      version: v2
    trafficPolicy:
      loadBalancer:
        simple: ROUND_ROBIN</pre>
<div class="blog_h3"><span class="graybg">一致性哈希负载均衡</span></div>
<p>基于一致性哈希的负载均衡可以实现基于<span style="background-color: #c0c0c0;">HTTP请求头、Cookie或其它属性</span>的<span style="background-color: #c0c0c0;">软的会话绑定（Session affinity）</span>：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
 kind: DestinationRule
 metadata:
   name: bookinfo-ratings
 spec:
   host: ratings.prod.svc.cluster.local
   trafficPolicy:
     loadBalancer:
       consistentHash:
         # 三选一
         # 基于请求头进行哈希计算
         httpHeaderName: end-user
         # 基于源IP地址进行哈希计算
         useSourceIp： true
         # 基于Cookie进行哈希计算，如果Cookie不存在，Envoy会自动产生
         httpCookie:
           # Cookie的名称、路径、生命周期
           name: user
           path: ..
           ttl: 0s</pre>
<div class="blog_h3"><span class="graybg">并发限制</span></div>
<p>可以限制某个服务/子集的并发度：</p>
<pre class="crayon-plain-tag">spec:
  subsets:
  - name: v1
    labels:
      version: v1
    trafficPolicy:
      # 限制并发
      # 每个Sidecar（也就是每个Pod）为每个上游服务的实例都配备一个连接池，断路器依赖此连接池才能工作
      # connectionPool可以有tcp、http两个字段
      connectionPool:
        tcp:
          # 最大连接数
          maxConnections: 100&amp;nbsp;
          # 连接超时
          connectTimeout:
        http:
          # 最大等待处理的请求数量
          http1MaxPendingRequests: 1024
          # HTTP2最大请求数
          http2MaxRequests: 1024
          # 每个TCP连接可以被多少请求共享使用（重用），设置为1则禁用keep-alive
          maxRequestsPerConnection: 32
          # 最大重试次数
          maxRetries: 3 </pre>
<div class="blog_h3"><span class="graybg">断路器</span></div>
<p>可以为某个服务/子集配置断路器：</p>
<pre class="crayon-plain-tag">spec:
  subsets:
  - name: v1
    labels:
      version: v1
    trafficPolicy:
      # 异常检测，断路
      outlierDetection:
        # 将实例从负载均衡池中剔除，需要连续的错误（HTTP 5XX或者TCP断开/超时）次数
        consecutiveErrors: 1
        # 分析是否需要剔除的频率，多久分析一次
        interval: 1s
        # 实例被剔除后，至少多久不得返回负载均衡池
        baseEjectionTime: 3m
        # 负载均衡池中最多有多大比例被剔除
        maxEjectionPercent: 100</pre>
<div class="blog_h2"><span class="graybg">ServiceEntry</span></div>
<p>用于访问不受网格管理的服务，将其作为服务条目添加到Istio内部管理的服务注册表（service registry）中。</p>
<p>ServiceEntry允许在Istio的内部服务注册表中手动添加条目，这些条目可以是网格内部的服务（MESH_INTERNAL）或网格外的服务（MESH_EXTERNAL）。这意味着你可以使Istio意识到那些不直接由Istio管理的服务。</p>
<p>对于MESH_INTERNAL类型的ServiceEntry，它用于添加网格内的服务到Istio的内部服务发现系统，这些服务可能由于某些原因（比如，它们运行在不同的Kubernetes集群或不同的网络命名空间中）而未被自动识别为网格服务。</p>
<div class="blog_h3"><span class="graybg">简单示例</span></div>
<p>下面的例子，允许Envoy代理针对gmem.cc的HTTP请求：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: gmem-ext-svc
spec:
  hosts:
  # 可以使用通配符（仅仅针对网格外部服务），表示一个白名单，允许从服务网格内部访问它们
  # 注意，1.0.5貌似不支持通配符？
  - *.gmem.cc
  location: MESH_EXTERNAL
  resolution: DNS
  ports:
  - number: 80
    name: http
    protocol: HTTP
  - number: 443
    name: https
    protocol: HTTPS</pre>
<p>ServiceEntry可以和VirtualService一起工作，作为VirtualService的host。这是因为从概念上来说，来自底层基础设施注册表的服务、ServiceEntry定义的服务，都是Istio的概念性的Service。</p>
<div class="blog_h3"><span class="graybg">指定静态端点</span></div>
<p>下面的例子以主机名来静态的指定外部服务端点：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: external-svc-dns
spec:
  hosts:
  - foo.bar.com
  location: MESH_EXTERNAL
  ports:
  - number: 80
    name: https
    protocol: HTTP
  resolution: DNS
  # 端点列表
  endpoints:
  - address: us.foo.bar.com
    ports:
      https: 8080
  - address: uk.foo.bar.com
    ports:
      https: 9080
  - address: in.foo.bar.com
    ports:
      https: 7080</pre>
<p>下面的例子以IP来指定外部服务端点：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: external-svc-mongocluster
spec:
  hosts:
  - mymongodb.somedomain
  # 下面的字段仅仅支持resolution=NONE|STATIC
  addresses:
  - mymongodb.ip/32
  ports:
  - number: 27018
    name: mongodb
    protocol: MONGO
  location: MESH_INTERNAL
  # 使用endpoints中定义的静态IP地址
  resolution: STATIC
  # 解析到：
  endpoints:
  - address: 2.2.2.2
  - address: 3.3.3.3</pre>
<p>关于hosts、addresses，以及服务的“唯一性标识”，<a href="https://github.com/istio/api/blob/master/networking/v1alpha3/service_entry.proto">Istio官方API文档</a>有如下说明：</p>
<ol>
<li>hosts字段：
<ol>
<li>对于非HTTP服务（包括不透明HTTPS）来说，此字段中指定的<span style="background-color: #c0c0c0;">DNS名称被忽略</span></li>
<li>对于非HTTP服务，如果此字段指定的是DNS名称，则<span style="background-color: #c0c0c0;">ports，或/和addresses中的IP地址用于唯一性的标识目的地</span></li>
</ol>
</li>
<li>addresses字段： 表示关联到此服务的虚拟IP地址，可以是CIDR前缀：
<ol>
<li>对于HTTP服务来说，此字段被忽略，目的地根据HTTP头Host/Authority确定</li>
<li>对于非HTTP服务，hosts头被忽略。如果指定了addresses，则入站请求的IP地址和addresses指定的CIDR进行匹配，如果匹配成功，则认为请求的是当前服务。如果不指定addresses，则入站流量仅仅根据ports来识别，这种情况下，服务的访问端口不能被网格内部任何其它服务共享</li>
</ol>
</li>
</ol>
<p>这段描述让人费解，也不符合实际试验结果。下面的两个ServiceEntry，都是非HTTP服务，都使用443端口，都没有指定addresses字段，但是没有任何冲突出现：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: cdn.gmem.cc
spec:
  hosts:
  - cdn.gmem.cc
  ports:
  - name: tcp
    number: 443
    protocol: TCP
  resolution: DNS
  location: MESH_EXTERNAL

---

apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: blog.gmem.cc
spec:
  hosts:
  - blog.gmem.cc
  ports:
  - name: tcp
    number: 443
    protocol: TCP
  resolution: DNS
  location: MESH_EXTERNAL</pre>
<p>生成的Envoy配置如下（可以看到集群、端点信息都是正常有效的）：</p>
<pre class="crayon-plain-tag">// istioctl proxy-config cluster ubuntu --fqdn=cdn.gmem.cc -o json
[
    {
        "name": "outbound|443||cdn.gmem.cc",
        "type": "STRICT_DNS",
        "hosts": [
            {
                "socketAddress": {
                    "address": "cdn.gmem.cc",
                    "portValue": 443
                }
            }
        ]
    }
]

// istioctl proxy-config cluster ubuntu --fqdn=blog.gmem.cc -o json
[                                                                                                            
    {                                                                                                        
        "name": "outbound|443||blog.gmem.cc",                                                                
        "type": "STRICT_DNS",                                                                                 
        "hosts": [                                                                                           
            {                                                                                                
                "socketAddress": {                                                                              
                    "address": "blog.gmem.cc",                                                               
                    "portValue": 443                                                                         
                }                                                                                            
            }                                                                                                
        ]                                                                                      
    }                                                                                                        
]  
// istioctl proxy-config endpoint ubuntu --cluster='outbound|443||cdn.gmem.cc' -o json       
[
    {
        "name": "outbound|443||cdn.gmem.cc",
        "addedViaApi": true,
        "hostStatuses": [
            {
                "address": {
                    "socketAddress": {
                        "address": "27.221.30.56",
                        "portValue": 443
                    }
                }
            }
        ]
    }
]

// istioctl proxy-config endpoint ubuntu --cluster='outbound|443||blog.gmem.cc' -o json        
[                                                                                                            
    {                                                                                                        
        "name": "outbound|443||blog.gmem.cc",                                                                
        "addedViaApi": true,                                                                                  
        "hostStatuses": [                                                                                      
            {                                                                                                
                "address": {                                                                                 
                    "socketAddress": {
                        "address": "39.107.94.255",
                        "portValue": 443
                    }
                }
        ]
    }
]</pre>
<div class="blog_h3"><span class="graybg">UDS的例子</span></div>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: unix-domain-socket-example
spec:
  hosts:
  - "example.unix.local"
  location: MESH_EXTERNAL
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: STATIC
  endpoints:
  - address: unix:///var/run/example/socket</pre>
<div class="blog_h2"><span class="graybg">Gateway</span></div>
<p>配置在网格的边缘，用于外部访问服务网格，为TCP/HTTP流量提供负载均衡。 Gateway仅仅用在独立运行Istio Proxy的那些Ingress/Egress网关上，定义在网格边缘上暴露的名字：</p>
<ol>
<li>如果Gateway定义在Ingress网关上，那么对网格外部暴露名字。当然，外部需要将这些名字的DNS记录指向Istio网关对外的IP地址</li>
<li>如果Gateway定义在Egress网关上：
<ol>
<li>如果通过配置限定所有出口流量必须走Egress网关，那么对集群内部暴露名字</li>
<li>使用VirtualService，可以将某个名字强行引导自网关，自网关出去</li>
</ol>
</li>
</ol>
<p>和Kubernetes的Ingress不同，Istio网关仅仅在L4-L6上配置，而不使用L7。 你可以把Gateway绑定到VirtualService，进而使用Istio标准的方式来管理HTTP请求和TCP流量。</p>
<p>Gateway可以定义多个“服务器”（servers），每个服务器可以匹配多个hosts，注意，这些hosts同样只是用来说明哪些名字匹配这个Gateway定义。每个服务器会在网关Istio Proxy中产生一个监听端口，这个端口有port字段确定。</p>
<p>当流量进入网关后，具体转发给谁，取决于此流量HTTP头Host，和Istio什么服务条目（来自K8s的服务或者ServiceEntry定义的服务）、路由规则（hosts相匹配，且应用到此网关的VirtualService）匹配。</p>
<div class="blog_h3"><span class="graybg">HTTP网关</span></div>
<p>下面的例子，允许网格外部发送针对gmem.cc的HTTPS请求：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: mesh.gmem.cc
spec:
  # 标签列表，指定应用此网关配置的Pod/VM具有的标签集
  # 具有匹配标签的Pod（通常是ingressgateway/egressgateway）的Envoy接收配置
  # 目标Pod必须是以proxy模式运行的Pilot agent，也就是说你不能随意指定标签选择器
  selector:
    istio: ingressgateway
  # 在这里定义所有的服务器，其端口都将成为网关Pod的物理监听端口
  # 对于这个例子，你可以使用*.mesh.gmem.cc域名，访问istio-ingressgateway的任意Pod的443端口
  servers:
  # 多个服务器可以分开配置，也可以配置在同一个Gateway资源中
  - port:
      # 此Gateway创建后，ingressgateway的Envoy会立即添加一个0.0.0.0_443监听器
      # 并且引用名为https.443.https的路由
      number: 443
      name: https
      protocol: HTTPS
    hosts:
    # 可以使用通配符，这样可以任何子域名访问网关
    - *.mesh.gmem.cc
    tls:
      mode: SIMPLE
      # 服务器数字证书，必须挂载到istio-ingressgateway
      serverCertificate: /etc/istio/ingressgateway-certs/tls.crt
      # 服务器私钥，必须挂载到istio-ingressgateway
      privateKey: /etc/istio/ingressgateway-certs/tls.key
      # 如果客户端发送HTTP请求，则将其重定向到HTTPS
      httpsRedirect: true

   # 下面是一个TCP网关，使用MongoDB协议
  - port:
      number: 2379 
      name: mongo
      protocol: MONGO
    hosts: [ "*" ]</pre>
<p>必须把上述网关绑定到VirtualService，它才能工作，精确的说，是<span style="background-color: #c0c0c0;">网关产生的物理监听器才拥有了路由表</span>：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: ubuntu.mesh.gmem.cc
spec:
  hosts:
    # 域名必须和绑定的网关的域名兼容，例如是它的子集
    # 这里的hosts+绑定网关的hosts，生成Envoy路由表的virtualHosts.domains字段
    - ubuntu.mesh.gmem.cc

  # 将虚拟服务绑定到网关
  # 指定需要应用此虚拟服务（定义的路由规则）的Gateway（所选择的Pod）或Sidecar
  # 保留字mesh表示，网格中所有Sidecar使用此虚拟服务定义的路由规则
  # 如果要同时为Gateway和Sidecar应用此虚拟服务，传入mesh+相关Gateway的名字
  gateways:
  - mesh.gmem.cc
  http:
  - match:
    - uri:
        prefix: /blog
    route:
    - destination:
        # 生成Envoy路由表的virtualHosts[0].routes[0].route.cluster
        # 例如 outbound|8086||ubuntu.default.svc.k8s.gmem.cc
        host: ubuntu
        port:
          number: 8000
          # 端口可以自动选择，也可以明确指定
          # 这里明确指定了8000，生成Envoy Cluster
          # outbound|8000||ubuntu.default.svc.k8s.gmem.cc
          # 注意，不能随意指定，必须在default命名空间有个叫ubuntu的服务，在暴露其8000端口才可以</pre>
<div class="blog_h3"><span class="graybg">TCP网关</span></div>
<p>为集群内的TCP服务建立入口Gateway时，<span style="background-color: #c0c0c0;">hosts字段被忽略</span>：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: zookeeper
spec:
  selector:
    istio: ingressgateway
  servers:
  - hosts:
    # 这边写什么都无所谓，只认客户端的目的IP地址
    - 'zk.mesh.gmem.cc'
    port:
      name: zookeeper
      number: 2181
      protocol: TCP</pre><br />
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: zk.mesh.gmem.cc
spec:
  gateways:
  - zookeeper
  hosts:
  - zk.mesh.gmem.cc
  tcp:
  - route:
    - destination:
        host: zk</pre>
<p>需要以下细节：</p>
<ol>
<li>上述虚拟服务和网关配对后，istio-ingressgateway的Pod立刻开始监听2181端口</li>
<li>但是，istio-ingressgateway服务默认只配置了80、443端口</li>
</ol>
<p>这意味着上述网关配置不可用，你还需要修改istio-ingressgateway服务的配置。</p>
<div class="blog_h3"><span class="graybg">限制所有出口流量必须走网关</span></div>
<p>首先，需要在Istio的安装或配置中<span style="background-color: #c0c0c0;">禁用允许服务直接访问外部服务的能力</span>。这通常通过设置Istio的<span style="background-color: #c0c0c0;">出站流量策略为 REGISTRY_ONLY</span> 来实现。REGISTRY_ONLY 模式意味着所有出站流量都必须通过Istio的Egress网关，除非流量的目的地已经在Istio的服务注册表中定义。这可以通过在安装Istio时配置meshConfig.outboundTrafficPolicy.mode为REGISTRY_ONLY来完成，或者在已经安装的Istio上通过修改ConfigMap来实现。</p>
<p>接下来，需要配置一个或多个Egress网关来处理出站流量。这通常涉及到创建一个Gateway资源，用于指定Egress网关应该接受哪些出站流量（基于端口、主机名等条件）。</p>
<p>为了允许出站流量访问外部服务，需要为每个外部服务定义一个ServiceEntry资源。ServiceEntry使外部服务在Istio的内部服务注册表中可见，即使这些服务不是网格的一部分。这样，即使启用了REGISTRY_ONLY模式，出站流量也可以被正确地路由到外部服务。</p>
<p>然后，为每个需要通过Egress网关访问的外部服务创建一个VirtualService。VirtualService定义了流量从Egress网关到外部服务的具体路由规则，可以基于URL路径、请求头等条件进行路由。</p>
<p>最后，可能需要为通过Egress网关访问的外部服务配置DestinationRule，以指定流量策略，如负载均衡、TLS设置等。</p>
<p>一个简单的配置示例：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: istio-egressgateway
spec:
  selector:
    istio: egressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*.example.com"
---
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: external-svc-example-com
spec:
  hosts:
  - "example.com"
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: DNS
  location: MESH_EXTERNAL
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: route-via-egressgateway
spec:
  hosts:
  - "example.com"
  gateways:
  - istio-egressgateway
  - mesh
  http:
  - match:
    - gateways:
      - mesh
    route:
    - destination:
        host: istio-egressgateway.istio-system.svc.cluster.local
        subset: v1
        port:
          number: 80
      weight: 100
  - route:
    - destination:
        host: example.com
        port:
          number: 80
      weight: 100</pre>
<div class="blog_h3"><span class="graybg">将出口流量导向网关</span></div>
<p>ServiceEntry可以配合Gateway+VirtualService，将出口流量导向一组特殊的Pod —— istio-egressgateway。本节给出一个样例。</p>
<p>首先定义一个ServiceEntry：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: gmem
spec:
  hosts:
  - k8s.gmem.cc
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: DNS</pre>
<p>有了这个配置以后，就可以从网格内部访问k8s.gmem.cc了，但是流量直接从Sidecar发往k8s.gmem.cc。</p>
<p>现在，我们要让流量通过istio-egressgateway的Pod转发。首先，定义一个Gateway：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: gmem
spec:
  # 指定哪些虚拟机/Pod应用这个网关配置，在K8S中，可匹配任意命名空间中的Pod
  selector:
    # 使用deployment istio-egressgateway定义的Pod，此deployment作为出口网关使用
    istio: egressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - k8s.gmem.cc</pre>
<p>然后，定义一个虚拟服务：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
spec:
  hosts:
  - k8s.gmem.cc
  gateways:
  # 此虚拟服务生成的Envoy配置（监听器路由），将应用到网关gmem所应用到的Pod，以及所有sidecar
  - gmem
  - mesh
  http:
  - match:
    # 如果HTTP请求从mesh发出，也就是从sidecar发出
    - gateways:
      - mesh
      port: 80
    route:
    # 那么将流量转发到istio-egressgateway，也就是出口网关服务
    - destination:
        host: istio-egressgateway.istio-system.svc.k8s.gmem.cc
        port:
          number: 80
      weight: 100
  - match:
    # 如果HTTP请求从gmem网关所应用到的Pod，也就是istio-egressgateway对应的Pod发出
    - gateways:
      - gmem
      port: 80
    route:
    # 那么将流量直接发往外部
    - destination:
        host: k8s.gmem.cc
        port:
          number: 80
      weight: 100 </pre>
<div class="blog_h2"><span class="graybg">WorkloadEntry</span></div>
<p>1.6新增，让运维人员能够描述单个非K8S工作负载，例如物理机或虚拟机，的属性。</p>
<p>必须伴随ServiceEntry一起使用，ServiceEntry使用选择器来匹配工作负载（包括Pod以及虚拟机、物理机），产生MESH_INTERNAL类型的服务。</p>
<p>当工作负载连接到istiod后，WorkloadEntry的Status会发生变化，反应工作负载的状态和一些其它细节，类似于K8S更新Pod的状态。</p>
<p>下面是一个例子：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1beta1
kind: WorkloadEntry
metadata:
  name: details-svc
spec:
  # 此工作负载具有Sidecar，且Sidecar使用该SA启动
  serviceAccount: details-legacy
  # 工作负载的IP地址
  address: 2.2.2.2
  # 此工作负载的标签
  labels:
    app: details-legacy
    instance-id: vm1</pre>
<p>对应的ServiceEntry：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
  name: details-svc
spec:
  hosts:
  - details.bookinfo.com
  # 内部服务
  location: MESH_INTERNAL
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: STATIC
  # 此服务匹配的工作负载，不管它是Pod还是WorkloadEntry
  workloadSelector:
    labels:
      app: details-legacy</pre>
<p>WorkloadEntry的address也可以指定为FQDN，对应的ServiceEntry的解析策略应该设置为<pre class="crayon-plain-tag">resolution: DNS</pre>。 </p>
<div class="blog_h2"><span class="graybg">配置案例参考</span></div>
<div class="blog_h3"><span class="graybg">访问ServiceEntry定义的服务时重定向到出口网关</span></div>
<p>定义 ServiceEntry 来允许访问外部 HTTP 服务：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: external-svc-httpbin
spec:
  hosts:
  - httpbin.org
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: DNS
  location: MESH_EXTERNAL</pre>
<p>注意：</p>
<ol>
<li>没有 ServiceEntry，网格中的服务无法通过 Istio 的 DNS 发现机制解析外部服务的名称。但是底层DNS机制还是能够解析域名的</li>
<li>如果你需要对某些外部服务应用特定的路由规则，或者需要 Istio 对这些流量进行更细粒度的管理（例如，只允许访问特定的外部服务），则 ServiceEntry 是必需的</li>
<li>如果你需要详细控制如何访问特定的外部服务，包括服务发现、安全、和流量管理，那么 ServiceEntry 是必要的</li>
</ol>
<p>如果你只是定义了一个 ServiceEntry 来允许访问外部服务，而没有配置流量经过 Egress Gateway，那么流量将直接从 Pod 的 Sidecar 代理出去，直接到达外部服务。ServiceEntry 使得服务网格内的服务能够访问和调用网格外的服务，但它本身并不强制流量经过 Egress Gateway。 </p>
<p>定义 Gateway 作为 Egress Gateway：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: istio-egressgateway
spec:
  selector:
    istio: egressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - httpbin.org</pre>
<p>注意：</p>
<ol>
<li>Gateway定义的是一个网关规则，而不是定义网关</li>
<li>网关其实就是运行Envoy的容器，它可以作为独立Pod运行，用作边界网关；也可以作为Sidecar运行，作为每个工作负载和外部通信的网关</li>
<li>selector选择器决定了这个规则运用到哪些网关或者说Envoy/Istio Proxy容器。这里的含义是仅仅应用到出口网关</li>
<li>这个网关中定义的域名httpbin.org用前面的ServiceEntry负责，即方访问外部</li>
</ol>
<p>定义 VirtualService 将流量从 Sidecar 代理路由到 Egress Gateway，然后到外部服务：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: route-via-egressgateway
spec:
  hosts:
  - httpbin.org
  gateways:
  - mesh
  - istio-egressgateway
  http:
  - match:
    - gateways:
      - mesh
      port: 80
    route:
    - destination:
        host: istio-egressgateway.istio-system.svc.cluster.local
        port:
          number: 80
      weight: 100
  - match:
    - gateways:
      - istio-egressgateway
      port: 80
    route:
    - destination:
        host: httpbin.org
        port:
          number: 80
      weight: 100</pre>
<p>注意：</p>
<ol>
<li>gateways定义了这个虚拟服务应用到哪些网关，这里是应用到：
<ol>
<li>mesh 特殊关键字，代表所有Sidecar</li>
<li>特定网关，具体来说，是VirtualService所在命名空间中，名为istio-egressgateway的网关。如果要应用到其它命名空间中运行的网关，使用gateway-namespace/gateway-name这种形式</li>
</ol>
</li>
<li>路由规则：
<ol>
<li>对于来自mesh内部也就是sidecar的流量，转发给istio-egressgateway这个网关</li>
<li>对于来自istio-egressgateway的流量，发送给httpbin.org前面已经定义了对应的Gateway，负责发送到网格外部</li>
</ol>
</li>
</ol>
<div class="blog_h1"><span class="graybg">策略和遥测基础</span></div>
<p>Istio提供了一个灵活的模型，用以<span style="background-color: #c0c0c0;">收集网格中服务的各种指标</span>（telemetry）。指标收集工作主要由Mixer负责。</p>
<p><img class="aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2018/11/topology-without-cache.svg" alt="" width="809" height="566" /></p>
<p>从逻辑上说，每次：</p>
<ol>
<li>请求发起前，消费者的Envoy都会调用Mixer，执行前提条件检查（Check）</li>
<li>每次请求处理完成后，都会调用Mixer，发送监控指标（Report）</li>
</ol>
<p>Sidecar在本地具有缓存，<span style="background-color: #c0c0c0;">大部分前提条件检查都在本地完成</span>。此外<span style="background-color: #c0c0c0;">监控指标也具有本地缓冲</span>，不会频繁的发送给Mixer。</p>
<div class="blog_h2"><span class="graybg">配置模型</span></div>
<p>Istio的策略和监控特性基于一个公共的模型来配置，你需要配置三类资源：</p>
<ol>
<li>Handlers：决定使用哪些适配器，这些适配器如何运作</li>
<li>Instances：<span style="background-color: #c0c0c0;">定义如何把请求属性映射为适配器的输入</span>。Instance代表一个或多个适配器需要处理的数据块</li>
<li>Rules：决定何时调用适配器，传递什么instance给它</li>
</ol>
<p>而这些配置是基于：</p>
<ol>
<li>Adapters：封装Mixer和特定的基础设施后端的交互逻辑</li>
<li>Templates：<span style="background-color: #c0c0c0;">定义将请求属性映射为适配器输入的Schema</span>。一个适配器可以支持多个模板</li>
</ol>
<div class="blog_h2"><span class="graybg">适配器</span></div>
<p>Mixer是高度模块化的、可扩展的组件。通过适配器，它支持多种<span style="background-color: #c0c0c0;">日志记录、配额、授权、监控后端</span>，并将这些策略、监控后端和Istio的其它部分解耦。</p>
<p>适配器封装了Mixer和外部基础设施后端（例如Prometheus）交互的逻辑，每个适配器都需要特定的配置参数才能工作，例如logging适配器需要知道后端服务的IP和端口。</p>
<p>具体使用哪些<a href="https://istio.io/docs/reference/config/policy-and-telemetry/adapters/">适配器</a>，也就是启用哪些后端，通过配置文件来指定。</p>
<p>Istio提供了很多内置适配器，每种适配器都对应一个CRD，这些CRD的实例就是适配器的实例，也即Handler。<span style="background-color: #c0c0c0;">Adaptor CRD的实例，表示一种自定义的适配器</span>。<span style="background-color: #c0c0c0;">自定义适配器实例化时使用Handler类型的CR</span>。</p>
<div class="blog_h3"><span class="graybg">适配器类型</span></div>
<p>适配器的类型使用CRD来描述，执行下面的命令查看系统中可用的适配器类型：</p>
<pre class="crayon-plain-tag">kubectl get crd -l istio=mixer-adapter
# NAME                              CREATED AT
# adapters.config.istio.io          2019-03-21T11:09:28Z
# bypasses.config.istio.io          2019-03-21T11:09:28Z
# circonuses.config.istio.io        2019-03-21T11:09:28Z
# deniers.config.istio.io           2019-03-21T11:09:28Z
# fluentds.config.istio.io          2019-03-21T11:09:29Z
# kubernetesenvs.config.istio.io    2019-03-21T11:09:29Z
# listcheckers.config.istio.io      2019-03-21T11:09:29Z
# memquotas.config.istio.io         2019-03-21T11:09:29Z
# noops.config.istio.io             2019-03-21T11:09:29Z
# opas.config.istio.io              2019-03-21T11:09:29Z
# prometheuses.config.istio.io      2019-03-21T11:09:29Z
# rbacs.config.istio.io             2019-03-21T11:09:29Z
# redisquotas.config.istio.io       2019-03-21T11:09:29Z
# servicecontrols.config.istio.io   2019-03-21T11:09:30Z
# signalfxs.config.istio.io         2019-03-21T11:09:30Z
# solarwindses.config.istio.io      2019-03-21T11:09:30Z
# stackdrivers.config.istio.io      2019-03-21T11:09:30Z
# statsds.config.istio.io           2019-03-21T11:09:30Z
# stdios.config.istio.io            2019-03-21T11:09:30Z</pre>
<p>执行下面的命令查看Prometheus适配器的实例：</p>
<pre class="crayon-plain-tag">kubectl -n istio-system get prometheuses.config.istio.io
# NAME      AGE
# handler   1h</pre>
<p><span class="graybg">本节后续内容讨论主要的内置适配器。</span></p>
<div class="blog_h3"><span class="graybg">Prometheus</span></div>
<p>支持 metric 模板</p>
<p>此适配器暴露了HTTP访问端点，Prometheus可以来采集此端点，获取服务网格的各项指标</p>
<p>使用Istio官方提供的Chart时，默认会在当前命名空间安装一个Prometheus服务器，并自动配置以抓取以下端点：</p>
<ol>
<li>istio-mixer.istio-system:42422，所有由Mixer的Prometheus适配器生成的网格指标。指标名以istio_开头</li>
<li>istio-mixer.istio-system:9093，用于监控Mixer自身的指标</li>
<li>istio-mixer.istio-system:9102，Envoy生成的原始统计信息。指标名以envoy_开头</li>
</ol>
<p>注意：目前Envoy的http://0.0.0.0:15090/stats/prometheus也暴露了Prometheus指标</p>
<p>该适配器包含的配置参数如下：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 100px; text-align: center;">参数</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>metrics</td>
<td>
<p>类型 Params.MetricInfo[]，需要插入到Prometheus的指标的列表。如果指标在Istio中定义了，但是没有在这里进行适当配置，则不会推送到Prometheus：</p>
<pre class="crayon-plain-tag">metrics:
  # 作为指标名前缀的命名空间。如果指标名为requests_total，该字段设置为istio，则最终完整的、
  # 提供给Prometheus的指标名是istio_requests_total。默认istio
- namespace: string
  # 注册到Prometheus的指标的名称，如果不指定从instanceName自动生成
  name: string
  # 必须，此MetricInfo处理的Istio Metric实例的全限定名称
  instanceName: string
  # 易读的描述
  description: string
  # 该指标的类别
  # Params.MetricInfo.Kind
  kind： UNSPECIFIED | GAUGE | COUNTER | DISTRIBUTION
  # 对于DISTRIBUTION类型的metric，该配置指定了聚合后数据所存放的桶
  # 对于非DISTRIBUTION类型的metric该字段无意义
  # Params.MetricInfo.BucketsDefinition
  buckets:
    # 三选一：
    # 线性桶，每个桶的大小（区间一致）
    linearBuckets：
      # 桶数量为： N = num_finite_buckets + 2
      numFiniteBuckets: int32
      # 桶的宽度
      width: double
      # 第一个桶的下限
      offset: double
    # 指数桶
    exponentialBuckets：
      # 桶数量为： N = num_finite_buckets + 2，两个额外的桶用作下溢、上溢桶
      # 对于桶i，它的左右边界为：
      # [scale * (growth_factor ^ (i - 1)), scale * (growth_factor ^ i) ]
      growthFactor: double
      scale: double
      numFiniteBuckets: int32
    # 明确定义的桶
    explicitBuckets：
      bounds: [ double,... ]
  # 指标的标签名的数组，这些标签必须和metric实例的dimension匹配
  labelNames：
  - dimension1
  - dimension2</pre>
<p>示例：</p>
<pre class="crayon-plain-tag">apiVersion: config.istio.io/v1alpha2
kind: prometheus
metadata:
  name: handler
spec:
  metrics:
  # 从那个metric类型的instance上读取生成该指标所需的信息
  - instance_name: requestcount.metric.istio-system
    # 生成Prometheus的Counter
    kind: COUNTER
    # 将instance上的以下维度作为标签
    label_names:
    - reporter
    - source_app
    - source_principal
    - source_workload
    - source_workload_namespace
    - source_version
    - destination_app
    - destination_principal
    - destination_workload
    - destination_workload_namespace
    - destination_version
    - destination_service
    - destination_service_name
    - destination_service_namespace
    - request_protocol
    - response_code
    - connection_security_policy
    name: requests_total
  - buckets:
      # 明确定义的桶，桶数量是下面数组的长度+2
      explicit_buckets:
        bounds:
        - 0.005
        - 0.01
        - 0.025
        - 0.05
        - 0.1
        - 0.25
        - 0.5
        - 1
        - 2.5
        - 5
        - 10
    instance_name: requestduration.metric.istio-system
    # 生成Prometheus的Histogram
    kind: DISTRIBUTION
    label_names:
    - reporter
    - source_app
    - source_principal
    - source_workload
    - source_workload_namespace
    - source_version
    - destination_app
    - destination_principal
    - destination_workload
    - destination_workload_namespace
    - destination_version
    - destination_service
    - destination_service_name
    - destination_service_namespace
    - request_protocol
    - response_code
    - connection_security_policy
    name: request_duration_seconds</pre>
</td>
</tr>
<tr>
<td>metricsExpirationPolicy</td>
<td>
<p>可选参数，指定此Prometheus适配器生成的指标的过期策略
<p>示例：</p>
<pre class="crayon-plain-tag">metricsExpirationPolicy:
  # 删除超过10分钟没有接收到更新的指标
  metricsExpiryDuration: "10m"
  # 每分钟进行一次是否过期的检查
  expiryCheckIntervalDuration: "1m"</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Statsd</span></div>
<p>支持 metric 模板
<p>将指标数据递送给StatsD后端</p>
<div class="blog_h3"><span class="graybg">Denier </span></div>
<p>支持 checknothing、listentry、quota模板</p>
<p>用于Check，总是让检查失败</p>
<div class="blog_h3"><span class="graybg">Fluentd</span></div>
<p>支持logentry模板</p>
<p>将Istio日志条目递送给Fluentd守护程序</p>
<div class="blog_h3"><span class="graybg">KubernetesEnv</span></div>
<p>支持kubernetes模板</p>
<p>从Kubernetes环境抽取信息，转换为Attributes，供下游适配器使用</p>
<div class="blog_h3"><span class="graybg">List</span></div>
<p>支持listentry模板</p>
<p>用于Check，实现简单的黑、白名单</p>
<div class="blog_h3"><span class="graybg">MemQuota</span></div>
<p>支持listentry模板</p>
<p>用于支持Istio的配额管理系统，用于开发环境下</p>
<div class="blog_h3"><span class="graybg">RedisQuota</span></div>
<p>支持quota模板</p>
<p>用于支持Istio的配额管理系统，用于生产环境下</p>
<div class="blog_h3"><span class="graybg">Stdio</span></div>
<p>支持 logentry、metric 模板</p>
<p>让Istio将日志、指标输出到标准输出、错误流，或者任何本地文件</p>
<div class="blog_h3"><span class="graybg">Zipkin</span></div>
<p>支持tracespan模板 </p>
<p>将追踪数据递送给Zipkin和兼容的Tracer</p>
<div class="blog_h2"><span class="graybg">模板</span></div>
<p>模板是Instance如何生成的依据。通常情况下都是直接定义Instance，并使用kind字段引用实例的模板类型。</p>
<p><span style="background-color: #c0c0c0;">内置模板不需要在配置存储中定义Template类型的CR</span>。每种内置模板都对应独立的CRD，这些CRD的实例就是模板的实例，也即Instance<span style="background-color: #c0c0c0;">。Template CRD的实例，表示一种自定义的模板。自定义模板实例化时使用Instance类型的CR</span>。</p>
<p>本节描述的是内置模板的实例配置方式。要了解如何使用自定义模板，参考<a href="/extend-istio">扩展Istio</a>一文。</p>
<div class="blog_h3"><span class="graybg">Metric</span></div>
<p>该模板用于描述需要分发给特定监控后端的运行时指标，配置示例：</p>
<pre class="crayon-plain-tag">apiVersion: "config.istio.io/v1alpha2"
kind: metric
metadata:
  name: requestsize
  namespace: istio-system
spec:
  value: request.size | 0
  dimensions:
    source_version: source.labels["version"] | "unknown"
    destination_service: destination.service.host | "unknown"
    destination_version: destination.labels["version"] | "unknown"
    response_code: response.code | 200
  monitored_resource_type: '"UNSPECIFIED"'</pre>
<p>模板规格如下：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">字段</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>value</td>
<td>
<p>stio.policy.v1beta1.Value，此指标报告的值</p>
</td>
</tr>
<tr>
<td>dimensions</td>
<td>
<p>map&lt;string, istio.policy.v1beta1.Value&gt;，用以唯一的识别此指标的维度名值映射</p>
</td>
</tr>
<tr>
<td>monitoredResourceType</td>
<td rowspan="2">如果后端基础设施支持监控资源（monitored resource），这两个字段有用  </td>
</tr>
<tr>
<td>monitoredResourceDimensions</td>
</tr>
</tbody>
</table>
<p>注意：当编写配置时，字段的值可以是字面值或Go表达式。如果字段的类型不是istio.policy.v1beta1.Value，则Go表达式的推断类型必须和字段的类型匹配。</p>
<div class="blog_h3"><span class="graybg">Listentry</span></div>
<p>该模板用于从请求中抽取单个值，并和listchecker适配器联用，实现值检查。配置示例：</p>
<pre class="crayon-plain-tag">apiVersion: "config.istio.io/v1alpha2"
kind: listentry
metadata:
  name: appversion
  namespace: istio-system
spec:
  value: source.labels["version"]</pre>
<div class="blog_h3"><span class="graybg">Logentry</span></div>
<p>从请求中抽取单条日志。配置示例：</p>
<pre class="crayon-plain-tag">apiVersion: "config.istio.io/v1alpha2"
kind: logentry
metadata:
  name: accesslog
  namespace: istio-system
spec:
  # 日志严重性级别
  severity: '"Default"'
  # 时间戳
  timestamp: request.time
  # 变量列表
  variables:
    sourceIp: source.ip | ip("0.0.0.0")
    destinationIp: destination.ip | ip("0.0.0.0")
    sourceUser: source.principal | ""
    method: request.method | ""
    url: request.path | ""
    protocol: request.scheme | "http"
    responseCode: response.code | 0
    responseSize: response.size | 0
    requestSize: request.size | 0
    latency: response.duration | "0ms"
  monitored_resource_type: '"UNSPECIFIED"'</pre>
<div class="blog_h3"><span class="graybg">Quota</span></div>
<p>声明用于进行限额的各个维度的取值。配置示例：</p>
<pre class="crayon-plain-tag">apiVersion: "config.istio.io/v1alpha2"
kind: quota
metadata:
  name: requestcount
  namespace: istio-system
spec:
  # 维度列表，配额适配器可以针对不同的维度，应用不同的限额策略
  dimensions:
    source: source.name | "unknown"
    sourceVersion: source.labels["version"] | "unknown"
    destination: destination.labels["app"] | destination.service.host | "unknown"
    destinationVersion: destination.labels["version"] | "unknown"</pre>
<div class="blog_h3"><span class="graybg">Tracespan</span></div>
<p>表示分布式跟踪中一个单独的Span。配置示例：</p>
<pre class="crayon-plain-tag">apiVersion: "config.istio.io/v1alpha2"
kind: tracespan
metadata:
  name: default
  namespace: istio-system
spec:
  # 此Span所属的Trace
  traceId: request.headers["x-b3-traceid"]
  # 此Span的标识符
  spanId: request.headers["x-b3-spanid"] | ""
  # 父Span的标识符，单个Trace中的所有Span，形成一棵树
  parentSpanId: request.headers["x-b3-parentspanid"] | ""
  # 此Span的名称
  spanName: request.path | "/"
  # 此Span的开始、结束时间
  startTime: request.time
  endTime: response.time
  # 是由客户端还是服务器Span，默认服务器Span
  clientSpan: (context.reporter.kind | "inbound") == "inbound"
  rewriteClientSpanId: false
  # Span的元数据
  spanTags:
    http.method: request.method | ""
    http.status_code: response.code | 200
    http.url: request.path | ""
    request.size: request.size | 0
    response.size: response.size | 0
    source.principal: source.principal | ""
    source.version: source.labels["version"] | ""</pre>
<div class="blog_h2"><span class="graybg">规则</span></div>
<div class="blog_h3"><span class="graybg">Rule</span></div>
<p>如果满足选择器，则执行一组intentions —— 将instance发送给handler处理。示例：</p>
<pre class="crayon-plain-tag"># 基于属性来匹配请求，如果匹配，则执行所有Actions
match: match(destination.service.host, "*")
# Action[]
actions:
  # 需要调用的处理器（适配器）的全限定名称
- handler: prometheus-handler
  # 发送给处理器的instance的全限定名称
  # 列出的instance的所有属性/字面值被解析，最后创建出一个对象，然后发送给处理器
  instances:
  - RequestCountByService
# 针对请求头的模板化的操作，使用由actions产生的值。需要Check action结果为OK
requestHeaderOperations: Rule.HeaderOperationTemplate[]
# 针对响应头的模板化的操作，使用由actions产生的值。需要Check action结果为OK
responseHeaderOperations: Rule.HeaderOperationTemplate[] </pre>
<div class="blog_h3"><span class="graybg">Action</span></div>
<p>每个Rule可以包含多个Action，表示选择器匹配时应该将哪些Instance发送给某个handler：</p>
<pre class="crayon-plain-tag">handler: prometheus-handler
instances:
- RequestCountByService
- RequestCountByEndpoint</pre>
<div class="blog_h3"><span class="graybg">Handler</span></div>
<p>Handler就是适配器的实例。配置项说明：</p>
<pre class="crayon-plain-tag"># 供Action引用的处理器的名称，必须全局唯一
name: string
# 需要实例化的、编译进Mixer二进制文件的适配器的名称
# 适配器名称通常以常量形式写在代码中
compiledAdapter: string
# 需要实例化的、外置的适配器实现的名称
# 适配器名称通常以常量形式写在代码中
adapter: string
# 传递给适配器的参数
params: google.protobuf.Struct
# 包含如何连接到外置（独立进程）适配器的信息
connection: Connection
  address: string
  timeout: google.protobuf.Duration
  # tls和mutual
  authentication: Authentication</pre>
<p>下面的例子，定义了一个基于外置适配器的Prometheus类型的处理器：</p>
<pre class="crayon-plain-tag">apiVersion: "config.istio.io/v1alpha2"
# 此处理器的适配器的类型
kind: prometheus
metadata:
  name: handler
  namespace: istio-system
spec:
  # 传递给适配器的参数
  param:
    metrics:
    - name: request_count
      instance_name: requestcount.metric.istio-system
      kind: COUNTER
      label_names:
      - source_service
      - source_version
      - destination_service
      - destination_version
  # 适配器的连接方式
  connection:
    address: localhost:8090</pre>
<div class="blog_h3"><span class="graybg">Instance</span></div>
<p>Instance就是模板的实例，或者说，<span style="background-color: #c0c0c0;">Instance是依据模板创建出来的</span>。 配置项说明：</p>
<pre class="crayon-plain-tag"># Instance的名称
name: string
# 此实例使用的内置模板的名称
compiledTemplate: string
# 用于引用非内置（compiled-in）模板
template: string
# 传递给模板的参数
params: google.protobuf.Struct
# 用于将适配器产生的属性，绑定回属性空间
attributeBindings: map&lt;string, string&gt;
  # 将适配器的source_namespace作为Attribute source.namespace的值
  source.namespace: output.source_namespace</pre>
<p>下面的例子创建了一个可以由Action直接引用的Instance：</p>
<pre class="crayon-plain-tag">- name: RequestCountByService
  # 该实例使用的模板
  template: istio.mixer.adapter.metric.Metric
  # 传递给模板的参数
  params:
    value: 1
    dimensions:
      source: source.name
      destination_ip: destination.ip </pre>
<div class="blog_h2"><span class="graybg">性能和可靠性</span></div>
<p>Mixer是非常高可用的组件，并且，它能在整体上提供服务网格的可用性、降低延迟：</p>
<ol>
<li>无状态，Mixer没有任何持久化状态</li>
<li>高度健壮，每个实例都具有99.999%的可用性</li>
<li>缓存和缓冲，Mixer可以在本地累积大量的临时数据</li>
</ol>
<p>在每个Pod上都要部署的Sidecar，必须尽可能占用少的内存。这意味着Sidecar的本地缓存、缓冲不能太大。<span style="background-color: #c0c0c0;">Mixer可以作为Sidercar的高可用二级缓存使用</span>。此外，Mixer的<span style="background-color: #c0c0c0;">缓冲可以在后端基础设施</span>（例如Prometheus）失败的情况下，<span style="background-color: #c0c0c0;">继续运行</span>（接受Envoy发来的指标），因而提高了可用性。</p>
<p>Mixer的缓存/缓冲也减少了对后端基础设施的访问频率，甚至减少访问数据量，因为它可以<span style="background-color: #c0c0c0;">在本地进行数据聚合</span>。</p>
<div class="blog_h2"><span class="graybg">AttributeManifest</span></div>
<p>描述一组属性，这些属性由Istio的某个组件产生。配置示例：</p>
<pre class="crayon-plain-tag"># 此配置的修订版号，由服务器分配
revision: string
# 产生属性的组件的名称，可以是istio-proxy
# 或者是类型为attributes的Mixer适配器
name: string
# 在运行时，组件负责产生的属性的映射，从属性名到属性Spec
attributes: map&lt;string, AttributeManifest.AttributeInfo&gt;</pre>
<div class="blog_h2"><span class="graybg">Attributes</span></div>
<p>属性（Attributes）是Istio策略/监控功能的关键概念。<span style="background-color: #c0c0c0;">属性是一小块数据，描述某个请求的自身的特征（property）或请求所处的环境</span>，例如一个属性可能表示请求的长度、响应的状态码、请求来自的IP地址。每个<span style="background-color: #c0c0c0;">属性都具有名称和取值</span>。这里的请求不一定是HTTP请求，它可以是任意的网络活动，例如TCP连接，TCP连接的原始客户端IP就是一个属性。</p>
<p>属性的字段，可能由请求的客户端Sidecar报告，<span style="background-color: #c0c0c0;">也可能由请求的服务端Sidecar报告</span>。</p>
<p>为了简化系统、改善开发者体验，Istio使用<span style="background-color: #c0c0c0;">共享的属性定义</span> —— 所由组件都使用这套定义。</p>
<p>Mixer本质上就是属性处理程序。E<span style="background-color: #c0c0c0;">nvoy为每个请求调用Mixer，并且发送请求的属性</span>。Mixer<span style="background-color: #c0c0c0;">依据请求属性，以及Mixer自己的配置，对若干基础设施后端发起调用</span>：</p>
<p><img class="alignnone" src="https://cdn.gmem.cc/wp-content/uploads/2018/11/machine.svg" alt="" width="920" height="392" /></p>
<div class="blog_h3"><span class="graybg">AttributeInfo</span></div>
<p>此对象用于描述一个属性的Schema —— <span style="background-color: #c0c0c0;">定义属性的元数据：属性值的类型、属性的意义的描述</span>。</p>
<p>任何Istio属性都必须对应<span style="background-color: #c0c0c0;">某个AttributeManifest中的一个AttributeInfo</span>。每个属性的名字必须是全局唯一的，在所由属性清单中，它只能出现一次。</p>
<p>属性的运行时形式没有做规定，不管使用JSON、XML还是Protocol Buffer来传递属性，其语义不会变化。</p>
<div class="blog_h3"><span class="graybg">可用属性列表</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>source.uid</td>
<td>
<p>平台相关的源<span style="background-color: #c0c0c0;">工作负载实例</span>的唯一标识，例如：</p>
<p style="padding-left: 30px;">kubernetes://redis-master-0.default</p>
</td>
</tr>
<tr>
<td>source.ip</td>
<td>ip_address，源工作负载实例的IP地址</td>
</tr>
<tr>
<td>source.labels</td>
<td>
<p>map[string, string]，源工作负载实例的标签集，例如：</p>
<p style="padding-left: 30px;">version =&gt; v1</p>
</td>
</tr>
<tr>
<td>source.name</td>
<td>源工作负载实例的名称，例如：redis-master-0</td>
</tr>
<tr>
<td>source.namespace</td>
<td>源工作负载实例所在的命名空间，例如default</td>
</tr>
<tr>
<td>source.principal</td>
<td>源工作负载实例的身份认证主体，例如service-account-redis</td>
</tr>
<tr>
<td>source.owner</td>
<td>源工作负载实例的所有者资源的唯一标识，Pod常常由Deployment所有，例如：kubernetes://apis/extensions/v1beta1/namespaces/istio-system/deployments/istio-policy</td>
</tr>
<tr>
<td>source.workload.uid</td>
<td rowspan="3">源工作负载（非实例，也就是服务）的信息</td>
</tr>
<tr>
<td>source.workload.name</td>
</tr>
<tr>
<td>source.workload.namespace</td>
</tr>
<tr>
<td>destination.uid</td>
<td rowspan="8">目标工作负载实例的信息</td>
</tr>
<tr>
<td>destination.ip</td>
</tr>
<tr>
<td>destination.port</td>
</tr>
<tr>
<td>destination.labels</td>
</tr>
<tr>
<td>destination.name</td>
</tr>
<tr>
<td>destination.namespace</td>
</tr>
<tr>
<td>destination.principal</td>
</tr>
<tr>
<td>destination.owner</td>
</tr>
<tr>
<td>destination.workload.uid</td>
<td rowspan="3">目标工作负载的信息</td>
</tr>
<tr>
<td>destination.workload.name</td>
</tr>
<tr>
<td>destination.workload.namespace</td>
</tr>
<tr>
<td>destination.container.name</td>
<td>目标工作负载实例的容器名，例如runtime</td>
</tr>
<tr>
<td>destination.container.image</td>
<td>目标工作负载实例的容器镜像 </td>
</tr>
<tr>
<td>destination.service.host</td>
<td>目标工作负载的主机名，例如grafana.default.svc.k8s.gmem.cc </td>
</tr>
<tr>
<td>destination.service.uid</td>
<td>目标工作负载的唯一标识 </td>
</tr>
<tr>
<td>destination.service.name</td>
<td>目标工作负载的名称，也就是K8S服务短名 </td>
</tr>
<tr>
<td>destination.service.namespace</td>
<td>目标工作负载所在命名空间</td>
</tr>
<tr>
<td>request.headers</td>
<td>map[string, string]，HTTP请求头，对于gRPC，其元数据存放在此 </td>
</tr>
<tr>
<td>request.id</td>
<td>低碰撞风险的请求唯一标识 </td>
</tr>
<tr>
<td>request.path</td>
<td>请求的URL查询路径部分 </td>
</tr>
<tr>
<td>request.host</td>
<td>请求的URL主机部分 </td>
</tr>
<tr>
<td>request.method</td>
<td>请求的HTTP方法 </td>
</tr>
<tr>
<td>request.reason</td>
<td>用于审计系统的请求原因 </td>
</tr>
<tr>
<td>request.referer</td>
<td>HTTP referer头 </td>
</tr>
<tr>
<td>request.scheme</td>
<td>请求URL的Schema部分 </td>
</tr>
<tr>
<td>request.size</td>
<td>请求字节数，对于HTTP来说等于Content-Length  </td>
</tr>
<tr>
<td>request.total_size</td>
<td>请求总大小，包括请求头、请求体、请求尾 </td>
</tr>
<tr>
<td>request.time</td>
<td>timestamp，目标接收到请求的时间</td>
</tr>
<tr>
<td>request.useragent</td>
<td>HTTP User-Agent头 </td>
</tr>
<tr>
<td>response.headers</td>
<td>HTTP响应头 </td>
</tr>
<tr>
<td>response.size</td>
<td rowspan="2">HTTP响应大小  </td>
</tr>
<tr>
<td>response.total_size</td>
</tr>
<tr>
<td>response.time</td>
<td>timestamp，目标返回响应的时间</td>
</tr>
<tr>
<td>response.duration</td>
<td>duration，目标处理请求所消耗的时间 </td>
</tr>
<tr>
<td>response.code</td>
<td>int64，HTTP状态码 </td>
</tr>
<tr>
<td>response.grpc_status</td>
<td>gRPC状态码</td>
</tr>
<tr>
<td>response.grpc_message</td>
<td>gRPC状态消息</td>
</tr>
<tr>
<td>connection.id</td>
<td>低碰撞风险的TCP连接的唯一标识</td>
</tr>
<tr>
<td>connection.event</td>
<td>TCP连接的状态，open/continue/close之一 </td>
</tr>
<tr>
<td>connection.received.bytes</td>
<td>针对连接的最后一次Report()调用之后，接收到的字节数</td>
</tr>
<tr>
<td>connection.received.bytes_total</td>
<td>接收到的总字节数 </td>
</tr>
<tr>
<td>connection.sent.bytes</td>
<td>针对连接的最后一次Report()调用之后，发送的字节数</td>
</tr>
<tr>
<td>connection.sent.bytes_total</td>
<td>发送的总字节数 </td>
</tr>
<tr>
<td>connection.duration</td>
<td>TCP连接已经打开的时间</td>
</tr>
<tr>
<td>connection.mtls</td>
<td>boolean，是否启用mTLS </td>
</tr>
<tr>
<td>connection.requested_server_name</td>
<td>连接所请求的服务器名称（SNI） </td>
</tr>
<tr>
<td>context.protocol</td>
<td>请求或被代理连接使用的协议，例如tcp </td>
</tr>
<tr>
<td>context.time</td>
<td>Mixer操作的时间戳</td>
</tr>
<tr>
<td>context.reporter.kind</td>
<td>
<p>如果当前请求属性是从客户端报告，则为outbound</p>
<p>如果当前请求属性是从服务端报告，则为inbound</p>
</td>
</tr>
<tr>
<td>context.reporter.uid</td>
<td>请求报告者的UID</td>
</tr>
<tr>
<td>api.service</td>
<td>gRPC公共服务名，不是网格内部的服务标识，而是暴露给客户端的服务的名称</td>
</tr>
<tr>
<td>api.version</td>
<td>gRPC接口版本 </td>
</tr>
<tr>
<td>api.operation</td>
<td>gRPC操作名称，例如getPetsById </td>
</tr>
<tr>
<td>api.protocol</td>
<td>暴露给客户端的协议，可以是http, https, grpc</td>
</tr>
<tr>
<td>request.auth.principal</td>
<td rowspan="5">源身份验证相关 </td>
</tr>
<tr>
<td>request.auth.audiences</td>
</tr>
<tr>
<td>request.auth.presenter</td>
</tr>
<tr>
<td>request.auth.claims</td>
</tr>
<tr>
<td>request.api_key</td>
</tr>
<tr>
<td>check.error_code</td>
<td>Mixer Check调用的错误码</td>
</tr>
<tr>
<td>check.error_message</td>
<td>Mixer Check调用的错误消息 </td>
</tr>
<tr>
<td>check.cache_hit</td>
<td>Mixer Check调用是否命中本地缓存 </td>
</tr>
<tr>
<td>quota.cache_hit</td>
<td>Mixer Quota调用是否命中本地缓存</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Attributes表达式</span></div>
<p>在你配置Instance的时候，往往需要对Attributes进行各种计算，才能得到Instance的字段。Attributes表达式是<a href="https://golang.org/ref/spec#Expressions">Go expressions</a>的子集。</p>
<div class="blog_h3"><span class="graybg">操作符</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 100px; text-align: center;">操作符</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>==</td>
<td>逻辑等于，示例：<pre class="crayon-plain-tag">request.size == 200</pre></td>
</tr>
<tr>
<td>!=</td>
<td>逻辑不等，示例：<pre class="crayon-plain-tag">request.auth.principal != "admin"</pre></td>
</tr>
<tr>
<td>||</td>
<td>逻辑或，示例：<pre class="crayon-plain-tag">(request.size == 200) || (request.auth.principal == "admin")</pre></td>
</tr>
<tr>
<td>&amp;&amp;</td>
<td>逻辑与</td>
</tr>
<tr>
<td>[ ]</td>
<td>用于访问映射，示例：<pre class="crayon-plain-tag">request.headers["x-id"]</pre></td>
</tr>
<tr>
<td>+</td>
<td>算术加或字符串拼接，示例：<pre class="crayon-plain-tag">request.host + request.path</pre></td>
</tr>
<tr>
<td>|</td>
<td>
<p>如果左操作数为空，则表达式值为右操作数，示例：</p>
<p><pre class="crayon-plain-tag">source.labels["app"] | source.labels["svc"] | "unknown"</pre></p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">函数</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">函数</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>match</td>
<td>Glob匹配，示例：<pre class="crayon-plain-tag">match(destination.service, "*.default.svc.k8s.gmem.cc")</pre></td>
</tr>
<tr>
<td>email</td>
<td>将文本转换为EMAIL_ADDRESS类型，示例：<pre class="crayon-plain-tag">email("alex@gmem.cc")</pre></td>
</tr>
<tr>
<td>dnsName</td>
<td>将文本转换为DNS_NAME类型，示例：<pre class="crayon-plain-tag">dnsName("www.gmem.cc")</pre></td>
</tr>
<tr>
<td>ip</td>
<td>将文本转换为IP_ADDRESS类型，示例：<pre class="crayon-plain-tag">source.ip == ip("10.11.12.13")</pre></td>
</tr>
<tr>
<td>timestamp</td>
<td>将RFC 3339文本转换为TIMESTAMP类型，示例：<pre class="crayon-plain-tag">timestamp("2015-01-02T15:04:35Z")</pre></td>
</tr>
<tr>
<td>uri</td>
<td>将文本转换为URI类型，示例：<pre class="crayon-plain-tag">uri("http://istio.io")</pre></td>
</tr>
<tr>
<td>.matches</td>
<td>正则式匹配，示例：<pre class="crayon-plain-tag">"svc.*".matches(destination.service)</pre></td>
</tr>
<tr>
<td>.startsWith</td>
<td rowspan="2">
<p>判断是否字符串以目标子串开头/结尾，示例：</p>
<pre class="crayon-plain-tag">destination.service.startsWith("acme")
destination.service.endsWith("acme")</pre>
</td>
</tr>
<tr>
<td>.endsWith</td>
</tr>
<tr>
<td>emptyStringMap</td>
<td>
<p>创建一个空的map[string][string]，示例：
<pre class="crayon-plain-tag">request.headers | emptyStringMap()</pre>
</td>
</tr>
<tr>
<td>conditional</td>
<td>
<p>模拟三元表达式：
<pre class="crayon-plain-tag">// 如果为true                                      则        否则
conditional((context.reporter.kind | "inbound") == "outbound", "client", "server")</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">示例</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">表达式</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>request.size | 200</td>
<td>返回int，返回请求的尺寸或者200</td>
</tr>
<tr>
<td>request.headers["x-forwarded-host"] == "myhost"</td>
<td>返回boolean，判断请求头的值</td>
</tr>
<tr>
<td>(request.headers["x-user-group"] == "admin") || (request.auth.principal == "admin")</td>
<td>返回boolean，逻辑或</td>
</tr>
<tr>
<td>(request.auth.principal | "nobody" ) == "user1"</td>
<td>返回boolean，如果request.auth.principa<span style="background-color: #c0c0c0;">l缺失不会导致错误</span></td>
</tr>
<tr>
<td>source.labels["app"]=="reviews" &amp;&amp; source.labels["version"]=="v3"</td>
<td>返回boolean，逻辑与</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">默认指标</span></div>
<p>使用Istio默认Chart安装时，它自动提供某些指标：
<pre class="crayon-plain-tag">kubectl -n istio-system get metrics.config.istio.io
NAME              AGE
# COUNTER Istio代理处理的请求计数
requestcount      1d
# DISTRIBUTION 请求处理耗时
requestduration   1d
# DISTRIBUTION 请求体尺寸
requestsize       1d
# DISTRIBUTION 响应体尺寸
responsesize      1d
# COUNTER TCP接收字节数
tcpbytereceived   1d
# COUNTER TCP发送字节数
tcpbytesent       1d</pre>
<div class="blog_h2"><span class="graybg">mixer客户端</span></div>
<p>Mixer客户端库被静态编译到Envoy代理中。你可以通过Istio提供的CRD和其它对象，对Mixer客户端进行配置。</p>
<div class="blog_h3"><span class="graybg">Attributes</span></div>
<p>istio.mixer.v1.Attributes表示一组强类型的名称/值对。很多Mixer的API可能产生或消费属性。</p>
<p>Istio使用属性来控制网格中服务的运行时行为。属性可以描述：</p>
<ol>
<li>Ingress流量的信息</li>
<li>Egress流量的信息</li>
<li>流量所在的环境的信息</li>
</ol>
<p>你部署的Istio具有一个固定的、它能够理解的属性表（vocabulary）。尽管<span style="background-color: #c0c0c0;">某些特殊的Mixer适配器、服务能产生属性，但是属性主要由Envoy产生</span>。</p>
<p>配置示例：</p>
<pre class="crayon-plain-tag">attributes: map&lt;string, istio.mixer.v1.Attributes.AttributeValue&gt;</pre>
<p>AttributeValue是string、int64、double、bool、bytes、 google.protobuf.Timestamp、google.protobuf.Duration、StringMap的一种 </p>
<div class="blog_h3"><span class="graybg">APIKey</span></div>
<p>执行下面的命令列出这类资源：</p>
<pre class="crayon-plain-tag">kubectl -n istio-system get apikeys.config.istio.io</pre>
<p>使用APIKey，你可以明确的定义<span style="background-color: #c0c0c0;">如何生成HTTP请求的Attribute：request.api_key</span>。</p>
<p>APIKey的字段包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">字段</td>
<td style="text-align: center;">描述</td>
</tr>
</thead>
<tbody>
<tr>
<td>query</td>
<td rowspan="3">
<p>string</p>
<p>三选一，是从给定的请求参数、请求头，还是Cookie中获取api_key</p>
</td>
</tr>
<tr>
<td>header</td>
</tr>
<tr>
<td>cookie</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">AttributeMatch</span></div>
<p>指定用来匹配Istio Attribute的子句：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">字段</td>
<td style="text-align: center;">描述</td>
</tr>
</thead>
<tbody>
<tr>
<td>clause</td>
<td>
<p>map&lt;string, StringMatch&gt;</p>
<p>将属性名string映射到StringMatch。示例：</p>
<pre class="crayon-plain-tag">clause:
  source.uid:
    exact: SOURCE_UID
  request.http_method:
    exact: POST</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">HTTPAPISpec</span></div>
<p>通过匹配HTTP请求的HTTP方法、URI路径，来定义用于<span style="background-color: #c0c0c0;">生成API相关的Attributes</span>的规范化配置。此CRD用于API属性生成的目的，而不是用于呈现身份验证、配、文档等其它API规范（例如OpenAPI）中常见的其它信息。
<p>现有的，基于HTTP动词+路径的定义操作（方法），可以用此CRD来规范化，以便在Istio中使用。</p>
<p>例如，<a href="https://github.com/googleapis/gnostic/blob/master/examples/v2.0/yaml/petstore-simple.yaml">基于OpenAPI v2的宠物商店的API</a>可以配置为如下的HTTPAPISpec：</p>
<pre class="crayon-plain-tag">apiVersion: config.istio.io/v1alpha2
kind: HTTPAPISpec
metadata:
  name: petstore
  namespace: default
spec:
  # istio.mixer.v1.Attributes
  # 任何HTTP图式匹配的情况下，都会生成的属性列表
  # 通常会包含api.service、api.version属性
  attributes:
    attributes:
      api.service:
        stringValue: petstore.swagger.io
      api.version:
        stringValue: 1.0.0
  # 用于匹配的HTTP图式 HTTPAPISpecPattern[]
  patterns:
    # 如果请求匹配该图式（的HTTP方法或URI模板），则会生成的属性列表
  - attributes:
      attributes:
        api.operation:
          stringValue: findPets
    # 什么情况下匹配此图式：HTTP方法为GET，且URI模板为/api/ptes
    httpMethod: GET
    # uriTemplate 和 regex 二选一，请求的路径必须匹配它们

    # uriTemplate，来自规范rfc6570，示例：
    # /pets
    # /pets/{id}
    # /dictionary/{term:1}/{term}
    # /search{?q*,lang} 
    uriTemplate: /api/pets
    # 正则式
    regex: "^/pets/(.*?)?"

  # 更多图式
  - attributes:
      attributes:
        api.operation:
          stringValue: addPet
    httpMethod: POST
    uriTemplate: /api/pets
  - attributes:
      attributes:
        api.operation:
          stringValue: findPetById
    httpMethod: GET
    uriTemplate: /api/pets/{id}
  - attributes:
      attributes:
        api.operation:
          stringValue: deletePet
    httpMethod: DELETE
    uriTemplate: /api/pets/{id}
  # 如何从HTTP请求中抽取API-KEY
  api_keys:
  # 首先尝试从请求参数中抽取，然后header，然后cookie
  - query: api-key
    header: api-key
    cookie: api-key</pre>
<div class="blog_h3"><span class="graybg">HTTPAPISpecBinding</span></div>
<p>定义HTTPAPISpecs和IstioService之间的绑定关系。下面的例子将HTTPAPISpec petstore和命名空间bar中的foo服务建立绑定：</p>
<pre class="crayon-plain-tag">apiVersion: config.istio.io/v1alpha2
kind: HTTPAPISpecBinding
metadata:
  name: my-binding
  namespace: default
spec:
  # Istio服务 IstioService[]
  services:
  - name: foo
    namespace: bar
  # API规范引用 HTTPAPISpecReference[]
  api_specs:
  - name: petstore
    namespace: default</pre>
<div class="blog_h3"><span class="graybg">IstioService</span></div>
<p>这不是一个CRD，但是多个CRD会引用它，IstioService用于识别一个服务（包括可选的版本），服务的FQDN由名称、命名空间、实现相关的Domain后缀组成。</p>
<p>IstioService包括的字段如下：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">字段</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>name</td>
<td>服务的短名称</td>
</tr>
<tr>
<td>namespace</td>
<td>服务的命名空间</td>
</tr>
<tr>
<td>domain</td>
<td>服务的DNS后缀</td>
</tr>
<tr>
<td>service</td>
<td>FQDN</td>
</tr>
<tr>
<td>labels</td>
<td>标签集</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">QuotaSpec</span></div>
<p>指定用于每个独立请求的配额规则集。示例：</p>
<pre class="crayon-plain-tag"># QuotaRule[] 此规则集包括的规则列表
rules:
  # AttributeMatch[] 如果为空，匹配所有请求。仅仅匹配的请求才应用配额
- match:
  # Quota[] 使用的配额列表
  quotas:
    # 配额的名称
  - name: string
    # 配额的量
    charge: int64</pre>
<div class="blog_h3"><span class="graybg">QuotaSpecBinding</span></div>
<p>用于指定配额规则集和IstioService之间的绑定关系。示例：</p>
<pre class="crayon-plain-tag"># IstioService[] 服务列表
services:
# QuotaSpecBinding.QuotaSpecReference[] 对配额规则集的引用
quotaSpecs
  # QuotaSpec对象的名字和命名空间
- name: string
  namespace: string</pre>
<div class="blog_h3"><span class="graybg">ServiceConfig</span></div>
<p>定义每个服务的（per-service）的客户端配置。示例：</p>
<pre class="crayon-plain-tag"># 是否禁用Check调用
disableCheckCalls: bool
# 是否禁用Report调用
disableReportCalls: bool
# 不管是Check/Report调用，都发送下面的属性到Mixer
mixerAttributes: istio.mixer.v1.Attributes	
# 用于生成API相关属性的HTTPAPISpec列表
httpApiSpec: HTTPAPISpec[]
# 用于生成配额需求的QuotaSpec列表
quotaSpec: QuotaSpec[]
# 如果客户端无法连接到Mixer时的行为
# 覆盖网格级别的策略istio.mixer.v1.config.client.TransportConfig.networkfailpolicy
networkFailPolicy:
  # FAIL_OPEN  如果无法连接到Mixer，允许请求并将其转发给目标服务
  # FAIL_CLOSE 如果无法连接到Mixer，不允许请求
  policy: NetworkFailPolicy.FailPolicy
  # 最大重试次数
  maxRetry: uint32
  # 基本的重试等待延迟。实际使用的延迟以此值为基础进行指数backoff和抖动
  baseRetryWait: google.protobuf.Duration
  # 最大重试等待延迟
  maxRetryWait: google.protobuf.Duration

# 默认的转发给上游的属性列表。典型包括source.ip、source.uid
# 转发的属性，其优先级高于静态Mixer属性。具体来说优先级从低到高为：
#   过滤器中配置的静态Mixer属性
#   路由配置中的静态Mixer属性
#   来自源过滤器配置的转发属性
#   来自源路由配置的转发属性
#   从请求元数据推导出的属性
forwardAttributes: istio.mixer.v1.Attributes</pre>
<div class="blog_h3"><span class="graybg">TcpClientConfig</span></div>
<p>定义客户端的TCP配置。示例：</p>
<pre class="crayon-plain-tag"># TCP传输配置
transport: TransportConfig
  # 是否禁用Check缓存
  disableCheckCache: bool
  # 是否禁用Quota缓存
  disableQuotaCache： bool
  # 是否禁用批量Report
  disableReportBatch: bool
  # 无法连接到Mixer时是否拒绝请求
  networkFailPolicy: NetworkFailPolicy
  # 将客户端统计信息刷入到Envoy共享内存的间隔。默认10s
  statsUpdateInterval: google.protobuf.Duration
  # 可以将Check、Report请求发送给不同的Mixer集群
  # 会将Check请求转发到一组Mixer服务器的Cluster的名称，默认mixer_server
  checkCluster: string
  # 会将Report请求转发到一组Mixer服务器的Cluster的名称，默认mixer_server
  reportCluster: string
  # 默认的转发给Mixer上游集群的属性，典型包括source.ip、source.uid
  # 这些属性被位于Mixer前面的Proxy（Sidecar）消费
  attributesForMixerProxy: istio.mixer.v1.Attributes
# 不管是Check/Report调用，都发送下面的属性到Mixer
mixerAttributes: istio.mixer.v1.Attributes
# 禁用Check或Report调用
disableCheckCalls: bool
disableReportCalls: bool
connectionQuotaSpec: QuotaSpec
# 对于长TCP连接，发送周期性报告的间隔。默认10s
# 不得小于1s
reportInterval: google.protobuf.Duration </pre>
<div class="blog_h2"><span class="graybg">mixs命令</span></div>
<div class="blog_h3"><span class="graybg">crd子命令</span></div>
<p>列出Mixer可用的CRD：</p>
<pre class="crayon-plain-tag"># 列出此二进制文件中所有CRD
mixs crd all

# 列出此二进制文件中所有适配器的CRD
mixs crd adapter

# 列出此二进制文件中所有模板的CRD
mixs crd instance</pre>
<div class="blog_h3"><span class="graybg">probe子命令</span></div>
<p>对本地运行的Mixer服务进行liveness/readiness探测。</p>
<div class="blog_h3"><span class="graybg">server子命令</span></div>
<p>启动一个Mixer服务器。选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--adapterWorkerPoolSize</td>
<td>适配器工作线程池最多包含的Goroutine数量，默认1024</td>
</tr>
<tr>
<td>--address</td>
<td>Mixer的gRPC API的地址，例如tcp://127.0.0.1:9092或unix:///path/to/file </td>
</tr>
<tr>
<td>--port</td>
<td>如果不指定--address，Mixer的gRPC API的端口，默认9091</td>
</tr>
<tr>
<td>--apiWorkerPoolSize</td>
<td>API工作线程池最多包含的Goroutine数量，默认1024</td>
</tr>
<tr>
<td>--configDefaultNamespac</td>
<td>网格范围的配置信息存放在什么命名空间，默认istio-system</td>
</tr>
<tr>
<td>--configStoreURL</td>
<td>配置存储的URL，例如k8s://path_to_kubeconfig或fs://，对于k8s://（path_to_kubeconfig为空）则使用in-cluster的kubeconfig</td>
</tr>
<tr>
<td>--ctrlz_address</td>
<td>ControlZ自省机制的监听地址，默认127.0.0.1</td>
</tr>
<tr>
<td>--ctrlz_port</td>
<td>ControlZ自省机制的监听端口，默认9876</td>
</tr>
<tr>
<td>--livenessProbeInterval</td>
<td>liveness探针文件的更新间隔，默认0s</td>
</tr>
<tr>
<td>--livenessProbePath</td>
<td>liveness探针文件的存储位置</td>
</tr>
<tr>
<td>--readinessProbeInterval</td>
<td>readiness探针文件的更新间隔，默认0s</td>
</tr>
<tr>
<td>--readinessProbePath</td>
<td>readiness探针文件的存储位置</td>
</tr>
<tr>
<td>--log_as_json</td>
<td>日志输出为console还是json格式</td>
</tr>
<tr>
<td>--log_caller</td>
<td>需要包含调用者信息的日志Scope列表，Scope可用：adapters, api, attributes, default, grpcAdapter</td>
</tr>
<tr>
<td>--log_output_level</td>
<td>日志输出级别，格式为scope:level,scope:level...，级别可用：debug, info, warn, error, none</td>
</tr>
<tr>
<td>--log_target</td>
<td>日志输出目标，默认stdout</td>
</tr>
<tr>
<td>--maxConcurrentStreams</td>
<td>每个TCP链接上开启的出站GRPC流最大数量，默认1024</td>
</tr>
<tr>
<td>--maxMessageSize</td>
<td>每个GRPC消息的最大长度，默认1048576</td>
</tr>
<tr>
<td>--monitoringPort</td>
<td>暴露Mixer自我监控信息的端口，默认9093</td>
</tr>
<tr>
<td>--numCheckCacheEntries</td>
<td>（策略）检查（Check）缓存条目的最大数量，默认1500000</td>
</tr>
<tr>
<td>--profile</td>
<td>启用基于Web接口http://host:port/debug/pprof的性能剖析</td>
</tr>
<tr>
<td>--singleThreaded</td>
<td>如果为true，则所有针对Mixer的请求都会在单个Goroutine中处理，用于调试</td>
</tr>
<tr>
<td>--trace_jaeger_url</td>
<td>Jaeger的地址，例如http://jaeger:14268/api/traces?format=jaeger.thrift</td>
</tr>
<tr>
<td>--trace_log_spans</td>
<td>是否打印追踪Span的日志</td>
</tr>
<tr>
<td>--trace_zipkin_url</td>
<td>Zipkin的地址，例如http://zipkin:9411/api/v1/spans</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">策略和遥测配置</span></div>
<div class="blog_h2"><span class="graybg">三类自定义资源</span></div>
<div class="blog_h3"><span class="graybg">Handlers</span></div>
<p>每种适配器都有自己的CRD，下面的例子是listchecker。该适配器检查输入参数是否在列表中，如果是，适配器返回true：</p>
<pre class="crayon-plain-tag">apiVersion: config.istio.io/v1alpha2
kind: listchecker
metadata:
  name: staticversion
  namespace: istio-system
spec:
  providerUrl: http://white_list_registry/
  blacklist: false</pre>
<p>下面的例子是prometheus，该适配器消费指标并且进行预聚合：</p>
<pre class="crayon-plain-tag">apiVersion: config.istio.io/v1alpha2
kind: prometheus
metadata:
  name: handler
  namespace: istio-system
spec:
  metrics:
  # 指标列表
  - name: request_count
    # 使用哪个instance
    instance_name: requestcount.metric.istio-system
    # 指标类型
    kind: COUNTER
    # 额外添加的标签，这里指定的都是instance的dimensions元素
    label_names:
    - destination_service
    - destination_version
    - response_code
  - name: request_duration
    instance_name: requestduration.metric.istio-system
    kind: DISTRIBUTION
    label_names:
    - destination_service
    - destination_version
    - response_code
    buckets:
      explicit_buckets:
        bounds: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]</pre>
<div class="blog_h3"><span class="graybg">Instances</span></div>
<p>每种Instance都有自己的CRD，用于将请求属性映射为适配器的输入。下面的例子为prometheus适配器提供输入：</p>
<pre class="crayon-plain-tag">apiVersion: config.istio.io/v1alpha2
kind: metric
metadata:
  name: requestduration
  namespace: istio-system
spec:
  # 注意可以用 | 提供默认值
  value: response.duration | "0ms"
  # 额外的标签
  dimensions:
    destination_service: destination.service | "unknown"
    destination_version: destination.labels["version"] | "unknown"
    response_code: response.code | 200
  monitored_resource_type: '"UNSPECIFIED"'</pre>
<div class="blog_h3"><span class="graybg">Rules</span></div>
<p>指定在何时调用handler，传递什么instance给它。下面的规则表示，如果被访问的服务是service1，且请求头x-user为user1，则将名为requestduration的metric传递给Prometheus：</p>
<pre class="crayon-plain-tag">apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: promhttp
  namespace: istio-system
spec:
  match: destination.service == "service1.ns.svc.cluster.local" &amp;&amp; request.headers["x-user"] == "user1"
  actions:
  - handler: handler.prometheus
    instances:
    - requestduration.metric.istio-system</pre>
<div class="blog_h2"><span class="graybg"><a id="rate-limits"></a>配置请求限速</span></div>
<p>适配器memquota可以用于实现流量配额，也就是限制请求速率。在生产环境下常常<span style="background-color: #c0c0c0;">使用redisquota代替之</span>。</p>
<p>要实现速率限制，需要在Client<span style="background-color: #c0c0c0;">、Mixer两个地方进行配置</span>：</p>
<ol>
<li>Client：
<ol>
<li>QuotaSpec，指定配额的名称，客户端需要请求的量</li>
<li>QuotaSpecBinding，条件性的将QuotaSpec关联到一个或多个服务</li>
</ol>
</li>
<li>Mixer：
<ol>
<li>quota instance：从请求总抽取配额使用信息</li>
<li>memquota handler：处理instance的适配器</li>
<li>quota rule：规定何时将instance发送给handler</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">限速逻辑</span></div>
<p>每个Quota实例都维护了一组计数器的集合，集合的大小即为dimensions的笛卡尔积。</p>
<p>当一个新请求到来时，如果<span style="background-color: #c0c0c0;">Mixer</span>发现超过在validDuration时间内超过maxAmount限制，则它<span style="background-color: #c0c0c0;">发送RESOURCE_EXHAUSTED消息给请求方的Envoy</span>，Envoy则<span style="background-color: #c0c0c0;">返回429响应码给调用者</span>。</p>
<p>memquota的时间区间使用滑动窗口的方式，分辨率为亚秒级。redisquota则既支持滚动窗口、也支持固定窗口算法。</p>
<div class="blog_h3"><span class="graybg">memquota</span></div>
<p>适配器配置如下：</p>
<pre class="crayon-plain-tag">apiVersion: "config.istio.io/v1alpha2"
kind: memquota
metadata:
  name: handler
  namespace: istio-system
spec:
  # 定义了四个不同的限速模式（Scheme），本质上是依据请求属性的不同，应用不同的限速规则
  quotas:
    # 针对的instance的全限定名称
  - name: requestcount.quota.istio-system
    # 第一个，默认模式：对于不匹配任何overrides的请求，应用 500 req/s 的限制
    maxAmount: 500
    validDuration: 1s
    overrides:
    # 第二个，不管源（source，在本示例中即请求头x-forwarded-for）是谁，只要目标（服务）是reviews，应用 1 req/5s 的限制
    - dimensions:
        destination: reviews
      maxAmount: 1
      validDuration: 5s
    # 第三个，如果源的IP是10.28.11.20，且目标是productpage服务，应用 500 req/s 的限制
    - dimensions:
        destination: productpage
        source: "10.28.11.20"
      maxAmount: 500
      validDuration: 1s
    # 第四个，如果目标是productpage服务，应用 2 req/5s 的限制
    - dimensions:
        destination: productpage
      maxAmount: 2
      validDuration: 5s</pre>
<p>对于每个请求，从<span style="background-color: #c0c0c0;">上往下寻找匹配的overrides，如果发现匹配则不再继续寻找，如果找不到匹配使用默认模式</span>。</p>
<div class="blog_h3"><span class="graybg">quota</span></div>
<p>该Instance从请求中抽取memquota所需的输入：</p>
<pre class="crayon-plain-tag">apiVersion: "config.istio.io/v1alpha2"
kind: quota
metadata:
  name: requestcount
  namespace: istio-system
spec:
  dimensions:
    # 源，以x-forwarded-for头为准，此头是请求经过的代理的链，逗号分隔，第一个是真实IP
    source: request.headers["x-forwarded-for"] | "unknown"
    # 目的服务，优先取app标签
    destination: destination.labels["app"] | destination.service | "unknown"
    # 目的服务的版本，取version标签
    destinationVersion: destination.labels["version"] | "unknown"</pre>
<div class="blog_h3"><span class="graybg">rule</span></div>
<pre class="crayon-plain-tag">apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: quota
  # 需要注意，官方下载的sample中忘记了下面这行
  namespace: istio-system
spec:
  # 条件性配额，只有满足条件的请求才被限速
  match: match(request.headers["cookie"], "user=*") == false
  actions:
  # 将handler和instance绑定
  - handler: handler.memquota
    instances:
    - requestcount.quota</pre>
<div class="blog_h3"><span class="graybg">quotaspec</span></div>
<p>配额规格，可以指定单次请求会消耗哪些配额，消耗的量是多大：</p>
<pre class="crayon-plain-tag">apiVersion: config.istio.io/v1alpha2
kind: QuotaSpec
metadata:
  name: request-count
  namespace: istio-system
spec:
  rules:
  - quotas:
    # 每个请求消耗多少份量的requestcount配额。如果配额500，该参数设置为5，则validDuration时间区间内你最多请求100次
    - charge: 1
      quota: requestcount</pre>
<div class="blog_h3"><span class="graybg">quotaspecbinding</span></div>
<p>将配额规格关联到需要启用限速功能的<span style="background-color: #c0c0c0;">服务</span>：</p>
<pre class="crayon-plain-tag">apiVersion: config.istio.io/v1alpha2
kind: QuotaSpecBinding
metadata:
  name: request-count
  namespace: istio-system
spec:
  quotaSpecs:
  - name: request-count
    namespace: istio-system
  services:

  # default命名空间的productpage服务，客户端访问它时，会被限速
  - name: productpage
    # 由于被限制的服务不在当前命名空间（istio-system），需要明确指定
    namespace: default</pre>
<p>如果要将配额绑定到所有服务，限制它们作为客户端时的访问速率，则：</p>
<pre class="crayon-plain-tag">- service: '*'</pre>
<div class="blog_h3"><span class="graybg">测试效果</span></div>
<p>上述几个CR可以通过下面的命令创建：</p>
<pre class="crayon-plain-tag">kubectl apply -f samples/bookinfo/policy/mixer-rule-productpage-ratelimit.yaml</pre>
<p>然后，反复刷新/productpage， 你会接收到错误提示：RESOURCE_EXHAUSTED:Quota is exhausted for: requestcount。这个信息是被调用服务的Envoy的响应体，它同时提供了429状态码。</p>
<p>查看productpage的日志，可以看到：</p>
<pre class="crayon-plain-tag">[2018-11-29T13:37:47.294Z] "GET /productpageHTTP/1.1" 429</pre>
<p>再查看调用者即入口网关的日志，可以看到： </p>
<pre class="crayon-plain-tag">"GET /productpageHTTP/1.1" 429 ... outbound|9080||productpage.default.svc.k8s.gmem.cc</pre>
<p>被限速的情况下，入口网关根本没有访问到productpage，直接被后者的Sidecar给拦截了。B</p>
<div class="blog_h3"><span class="graybg">关于命名空间</span></div>
<p>本例中大部分资源都在istio-system中创建，如果你希望仅仅<span style="background-color: #c0c0c0;">影响单个命名空间而非整个服务网格</span>，则可以把它们都创建到指定的其它命名空间。</p>
<div class="blog_h1"><span class="graybg">安全特性基础</span></div>
<p>将单体应用分解为微服务可以获得很多好处，<span style="background-color: #c0c0c0;">例如更灵活、更容易扩容、更容易重用</span>。但是，微服务也引入额外的安全需求：</p>
<ol>
<li>为了防止中间人攻击，需要进行流量加密。安全内网环境下可以忽略</li>
<li>为了提供灵活的访问控制，需要双向TLS认证，以及细粒度的访问控制策略</li>
<li>为了审计谁在什么时候访问了什么，需要审计工具 </li>
</ol>
<p>Istio的安全特性能够满足以上需求。Istio安全支持：</p>
<ol>
<li>强身份验证（strong identity）</li>
<li>强大的策略配置</li>
<li>透明的TLS传输</li>
<li>AAA —— 认证、授权、审计</li>
</ol>
<div class="blog_h2"><span class="graybg">架构</span></div>
<p>和Istio安全有关的组件包括：</p>
<ol>
<li>Citadel，负责密钥、证书管理</li>
<li>Sidecar和周边代理（perimeter proxies，运行在Ingress/Egress的代理 ），实现客户端 - 服务之间的安全通信</li>
<li>Pilot，分发身份验证策略、安全命名信息给代理</li>
<li>Mixer，管理授权和审计</li>
</ol>
<p><img class="alignnone" src="https://cdn.gmem.cc/wp-content/uploads/2018/11/architecture.svg" alt="" width="908" height="511" /></p>
<div class="blog_h2"><span class="graybg">身份标识</span></div>
<p>身份标识（Identity）是任何安全基础设施的基础。两个服务开始交互前，需要相互交换自己身份的凭证信息，以进行双向身份验证：</p>
<ol>
<li>客户端利用<span style="background-color: #c0c0c0;">安全命名（secure naming information）</span>信息检查服务器的身份，以确保它是服务的授权提供者（Runner）</li>
<li>在服务器端：
<ol>
<li>利用<span style="background-color: #c0c0c0;">授权策略（authorization policies）</span>来决定是否接受客户访问</li>
<li>记录审计日志 —— 谁在何时访问了什么</li>
<li>根据其使用的服务，对客户端计费</li>
<li>拒绝没付费的客户端的访问请求</li>
</ol>
</li>
</ol>
<p>在不同平台上，Istio使用不同方式来实现服务身份（Service identities）：</p>
<ol>
<li>在K8S上使用ServiceAccount</li>
<li>在GKE/GCE上使用GCP服务账号</li>
<li>在AWS上使用AWS IAM用户/角色账号</li>
<li>裸机器，可以使用用户账号、自定义服务账号、服务名称、Istio服务账号，等等</li>
</ol>
<div class="blog_h2"><span class="graybg">SPIFFE</span></div>
<p>SPIFFE标准提供了一套框架，用来向复杂环境下的任何服务授予身份标识。</p>
<p>Istio兼容SPIFFE，例如在K8S中，X.509证书具有URI字段，并且格式为：<pre class="crayon-plain-tag">spiffe://&lt;domain&gt;/ns/&lt;namespace&gt;/sa/&lt;serviceaccount&gt;</pre>，这允许K8S的服务和其它SPIFFE兼容系统进行交互。</p>
<div class="blog_h2"><span class="graybg">密钥和证书</span></div>
<p>Istio的PKI建立在Citadel之上，能够安全的为任何工作负载<span style="background-color: #c0c0c0;">提供代表身份的数字证书</span>（X.509，SPIFFE格式），并且支持密钥和<span style="background-color: #c0c0c0;">证书的自动轮换</span>（更新）。</p>
<p>在K8S场景下：</p>
<ol>
<li>Citadel会监控API Server，为所有Service Account创建SPIFFE格式的密钥对，并且存储为K8S的Sercret</li>
<li>当创建Pod后，Pod使用的Service Account的密钥对会挂载为卷</li>
<li>Citadel会监控证书的生命周期，并自动轮换，然后自动更新Secret，从而让Pod自动获得更新</li>
<li>Pilot会创建<span style="background-color: #c0c0c0;">安全命名信息</span>，此信息定义了<span style="background-color: #c0c0c0;">什么Service Account能够运行什么服务</span>。安全命名信息被传播给Envoy</li>
</ol>
<div class="blog_h2"><span class="graybg">身份验证</span></div>
<p>Istio提供两种类型的身份验证：</p>
<ol>
<li><span style="background-color: #c0c0c0;">传输身份验证</span>：即<span style="background-color: #c0c0c0;">服务-服务身份验证</span>。 认证直接的请求发起者的身份，Istio支持双向身份验证</li>
<li><span style="background-color: #c0c0c0;">源（Origin）身份验证</span>：即<span style="background-color: #c0c0c0;">终端用户身份验证</span>。验证最初发起请求的<span style="background-color: #c0c0c0;">用户或者设备</span>。Istio支持基于JSON Web Token（JWK）的请求级身份验证</li>
</ol>
<p>通过配置服务的<span style="background-color: #c0c0c0;">身份验证策略（authentication policies）</span>，可以为其启用身份验证功能。</p>
<div class="blog_h3"><span class="graybg">双向TLS认证 </span></div>
<p>通过服务消费者、提供者的Envoy代理，Istio实现了服务-服务通信的安全隧道。请求的处理步骤如下：</p>
<ol>
<li>将消费者的请求重新路由到本地的Sidecar —— Envoy</li>
<li>消费者Envoy向提供者的Envoy发起双向认证的TLS握手。在握手期间，消费者Envoy也会对提供者进行安全命名检查，看看它的Service Account是否有权提供目标服务</li>
<li>两个Envoy建立TLS连接，流量通过此连接转发</li>
<li>提供者确认消费者有权限访问后，将流量转发给本地的应用容器</li>
</ol>
<div class="blog_h3"><span class="graybg">安全命名</span></div>
<p>安全命名信息是一个多对多映射，从<span style="background-color: #c0c0c0;">编码在数字证书中</span>的服务的<span style="background-color: #c0c0c0;">身份标识</span>（K8S中为Service Account），到（被发现服务或DNS引用的）<span style="background-color: #c0c0c0;">服务名称</span>。从A到B的映射，其含义是<span style="background-color: #c0c0c0;">A有资格运行B服务</span>。</p>
<p>Pilot会监控K8S的API Server，取得Service Account + Pod等信息，生成安全命名信息，并安全的分发给Envoy。</p>
<div class="blog_h3"><span class="graybg">Node Agent</span></div>
<p>在K8S中，Node Agent负责证书、密钥的提供，流程如下：</p>
<ol>
<li>Citadel创建一个gRPC服务，负责处理CSR请求</li>
<li>Envoy通过SDS（Secrets discovery service） API发送证书、密钥请求给Node Agent</li>
<li>Node Agent会根据请求来创建私钥、发送CSR给Citadel来签名。私钥一直不会离开节点，保证了安全性</li>
<li>Citadel校验CSR并签名证书</li>
<li>Node Agent将签名后的证书通过SDS API返回给Envoy</li>
</ol>
<p>证书轮换时流程类似。</p>
<div class="blog_h2"><span class="graybg">权限控制</span></div>
<p>Istio使用基于角色的权限控制（RBAC），允许<span style="background-color: #c0c0c0;">命名空间级别、服务级别、HTTP方法级别</span>的访问控制。Istio支持：</p>
<ol>
<li>基于角色的授权，简单易用</li>
<li>服务-服务、终端用户-服务，两种访问控制</li>
<li>高性能，授权直接在Envoy上发生</li>
</ol>
<p>架构图如下：</p>
<p><img class="aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2018/11/authz.svg" alt="" width="749" height="599" /></p>
<div class="blog_h1"><span class="graybg">安全特性配置</span></div>
<div class="blog_h2"><span class="graybg">Citadel配置</span></div>
<div class="blog_h3"><span class="graybg">使用外部CA证书</span></div>
<p>默认情况下Citadel会自己生成密钥对并自签名，作为CA证书。你也可以提供现有的CA证书，只需要创建对应的保密字典即可：</p>
<pre class="crayon-plain-tag">kubectl create secret generic cacerts -n istio-system \
    # CA证书
    --from-file=certs/ca-cert.pem \
    # CA密钥
    --from-file=certs/ca-key.pem \
    # 根证书
    --from-file=certs/root-cert.pem \
    # 证书链
    --from-file=scerts/cert-chain.pem</pre>
<p>你需要使用如下参数重新部署：<pre class="crayon-plain-tag">--set values.global.mtls.enabled=true,values.security.selfSigned=false</pre>，Citadel会使用该CA证书为工作负载的数字证书进行签名。 </p>
<p>为了确保工作负载使用新的证书，你可能需要删除Citadel生成的、命名为istio.*的Secrets。</p>
<div class="blog_h3"><span class="graybg">通过SDS分发身份标识</span></div>
<p>默认情况下，Citadel为工作负载生成密钥和证书，并且通过Secret卷挂载给Sidecar。这种方式的缺点包括：</p>
<ol>
<li>证书轮换时的性能减退：混乱时Envoy需要热重启来得到新的证书、密钥</li>
<li>可能的安全隐患，因为私钥保存在K8S Secret中</li>
</ol>
<p>除了使用适当的Helm参数之外，你还需要修改API Server，添加命令行参数：</p>
<pre class="crayon-plain-tag">--service-account-issuer=kubernetes.default.svc
--service-account-signing-key-file=/etc/kubernetes/pki/sa.key </pre>
<div class="blog_h2"><span class="graybg">服务端传输身份验证</span></div>
<p>这里的<span style="background-color: #c0c0c0;">策略都是针对服务端（服务提供者）</span>的：</p>
<div class="blog_h3"><span class="graybg">自动mtls</span></div>
<p>与此特性相关的istio配置参数为：enableAutoMtls，对应的注入istio-proxy环境变量为ISTIO_AUTO_MTLS_ENABLED。</p>
<p>当启用此特性时，你仅仅需要配置授权策略（Authentication policy），而不需要配置DestinationRule的字段：</p>
<ol>
<li>对于具有Sidecar的服务端负载，Istio自动配置客户端，使用mTLS流量</li>
<li>对于没有Sidecar的服务器负载，Istio自动配置客户端，使用明文</li>
</ol>
<p>要启用，设置Chart参数<pre class="crayon-plain-tag">--set values.global.mtls.auto=true  --set values.global.mtls.enabled=false</pre></p>
<div class="blog_h3"><span class="graybg">全局强制mtls</span></div>
<p>你可以为整个服务网格设置默认的身份验证策略（不指定targets）：</p>
<pre class="crayon-plain-tag">apiVersion: "authentication.istio.io/v1alpha1"
# 全局授权策略
kind: "MeshPolicy"
metadata:
  # 名字必须为default
  name: "default"
spec:
  peers:
  - mtls: {}</pre>
<p>上述配置应用后，所有<span style="background-color: #c0c0c0;">被网格管理的服务仅仅接受基于TLS加密的请求</span>。</p>
<p>Chart参数global.mtls.enabled=true则自动配置上述MeshPolicy。这样，如果目标服务对应的DestinationRule没有对应的mTLS配置，则无法访问。</p>
<div class="blog_h3"><span class="graybg">命名空间启用mtls</span></div>
<p>或者为某个命名空间设置默认的身份验证策略：</p>
<pre class="crayon-plain-tag">apiVersion: "authentication.istio.io/v1alpha1"
kind: "Policy"
metadata:text-processing-with-gotext-processing-with-go
  name: "default"
  namespace: "ns1"
spec:
  peers:
  - mtls: {}</pre>
<div class="blog_h3"><span class="graybg">服务启用mtls</span></div>
<pre class="crayon-plain-tag">apiVersion: "authentication.istio.io/v1alpha1"
kind: "Policy"
metadata:
  name: "reviews"
spec:
  targets:
  # 对于reviews服务
  - name: reviews
  peers:
  # 必须启用双向的TLS
  - mtls: {} </pre>
<p>使用mutual TLS时，客户端必须在DestinationRule中配置TLSSettings。</p>
<div class="blog_h3"><span class="graybg">目标选择器</span></div>
<p>身份验证策略所针对的服务，在targets字段中配置：</p>
<pre class="crayon-plain-tag">spec:
  targets:
   # 任何单品页服务
   - name: product-page
   # 运行在9000端口的reviews服务
   - name: reviews
     ports:
     - number: 9000</pre>
<div class="blog_h3"><span class="graybg">可选验证</span></div>
<p>peers字段定义了服务-服务的身份验策略。0.7版本的Istio<span style="background-color: #c0c0c0;">仅支持mTLS</span>：</p>
<pre class="crayon-plain-tag">spec:  
  peers:
    - mtls: {}</pre>
<p>配置mode为PERMISSIVE，则mTLS是可选的，允许基于明文发起请求：</p>
<pre class="crayon-plain-tag">peers:
- mTLS:
    mode: PERMISSIVE</pre>
<div class="blog_h2"><span class="graybg">客户端传输身份验证</span></div>
<p>有了上面的、针对服务端的配置后，如果客户端不经任何配置，所有请求都会遭遇503错误。</p>
<p>要解决此问题，你必须为客户端配置DestinationRule，声明使用mtls：</p>
<pre class="crayon-plain-tag">apiVersion: "networking.istio.io/v1alpha3"
kind: "DestinationRule"
metadata:
  name: "default"
  namespace: "default"
spec:
  # 针对所有DNS名称的访问，都启用mtls
  host: "*.svc.k8s.gmem.cc"
  trafficPolicy:
    tls:
      # Istio会自动设置客户端私钥和证书的路径
      mode: ISTIO_MUTUAL</pre>
<p>不要忘记，DestinationRule也用于流量管理的配置。因此，如果某个<span style="background-color: #c0c0c0;">具体服务需要独立的DestinationRule配置</span>，一定要加上tls配置，因为上述DestinationRule<span style="background-color: #c0c0c0;">被覆盖，而不是合并</span>。</p>
<div class="blog_h3"><span class="graybg">客户端证书位置</span></div>
<p>Istio会自动把客户端证书注入到所有Sidecar的/etc/certs目录下：</p>
<pre class="crayon-plain-tag">kubectl exec $(kubectl get pod -l app=httpbin -o jsonpath={.items..metadata.name}) -c istio-proxy -- ls /etc/certs
# 证书链
# cert-chain.pem
# 客户端私钥
# key.pem
# CA跟证书，用于验证服务端
# root-cert.pem</pre>
<div class="blog_h2"><span class="graybg">与网格外部交互</span></div>
<p>本节介绍当启用mTLS后，服务网格内外进行通信的可能性。</p>
<div class="blog_h3"><span class="graybg">从网格外部发请求</span></div>
<p>对于来自网格外部（包括没有Sidecar的集群内部组件）的请求，由于无法初始化mTLS连接，一定会失败。curl会得到退出码56，表示接收网络数据失败。</p>
<p>这个<span style="background-color: #c0c0c0;">问题目前无解</span>，除非降低目标服务的身份验证需求。</p>
<div class="blog_h3"><span class="graybg">向网格外部发请求</span></div>
<p>同样会失败，因为客户端会发起mTLS连接，而没有Sidecar的外部服务无法处理这种连接。</p>
<p>解决办法是为目标服务配置DestinationRule，并禁用TLS：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
 name: "httpbin-noistio"
spec:
 host: "httpbin.noistio.svc.k8s.gmem.cc"
 trafficPolicy:
   tls:
     # 禁用TLS
     mode: DISABLE</pre>
<div class="blog_h3"><span class="graybg">向API Server发请求</span></div>
<p>Kubernetes的API Server也没有Sidecar，如果网格内部服务需要访问K8S API Server，则也需要DestinationRule配置：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
 name: "api-server"
spec:
 host: "kubernetes.default.svc.k8s.gmem.cc"
 trafficPolicy:
   tls:
     mode: DISABLE</pre>
<div class="blog_h2"><span class="graybg">源身份验证</span></div>
<div class="blog_h3"><span class="graybg">JWT认证</span></div>
<p>origins字段定义了终端用户的身份验证策略。目前仅仅支持JWT认证。</p>
<pre class="crayon-plain-tag">spec:
  origins:
  # 承认Google签发的JWT
  - jwt:
      issuer: "https://accounts.google.com"
      jwksUri: "https://www.googleapis.com/oauth2/v3/certs"
    # 哪些路径不需要身份验证
    trigger_rules:
    - excluded_paths:
      - exact: /health</pre>
<div class="blog_h2"><span class="graybg">身份验证策略更新</span></div>
<p>你可以随时修改身份验证策略，Istio会实时的推送给Envoy，但是不保证同时送达。为了保证行为一致性：</p>
<ol>
<li>需要启用/禁用mTLS时，先切换为临时的mode: PERMISSIVE，这样明文和加密都可以接受</li>
<li>对于JWT源身份认证，在切换策略之前，请求必须携带新的JWT</li>
</ol>
<div class="blog_h2"><span class="graybg">启用授权</span></div>
<p>要启用访问控制，你需要配置CRD —— RbacConfig。这是一个全局的单例对象，名字必须为default：</p>
<pre class="crayon-plain-tag">apiVersion: "rbac.istio.io/v1alpha1"
kind: RbacConfig
metadata:
  name: default
spec:
  # OFF 禁用访问控制
  # ON  为所有服务启用访问控制
  # ON_WITH_INCLUSION 仅仅为指定的服务、命名空间启用访问控制
  # ON_WITH_EXCLUSION 除了指定的服务、命名空间，都启用访问控制
  mode: 'ON_WITH_INCLUSION'
  # 指定启用访问控制的命名空间、服务
  inclusion:
    namespaces: ["default"]</pre>
<div class="blog_h2"><span class="graybg">授权策略</span></div>
<div class="blog_h3"><span class="graybg">ServiceRole</span></div>
<p>已经废弃，使用AuthorizationPolicy代替。</p>
<p>定义一个角色，并声明该角色对哪些服务具有哪些访问权限。</p>
<p>ServiceRole包含一系列rules，也就是许可（permissions）：</p>
<pre class="crayon-plain-tag">apiVersion: "rbac.istio.io/v1alpha1"
kind: ServiceRole
metadata:
  name: service-admin
  namespace: default
spec:
  # 许可列表
  rules:
    # 针对的服务，通配符*表示当前命名空间的任何服务。admin-*表示任何以admin-开头的服务
  - services: ["*", "admin-*", "httpbin.default.svc.k8s.gmem.cc"]
    # 允许的HTTP方法，对于gRPC请求来说，HTTP方法总是POST。通配符*表示允许任何方法
    methods: ["*"]
    # 允许的URL路径，或者gRPC方法。
    # gRPC方法必须是 /packageName.serviceName/methodName 形式，大小写敏感
    paths:
    - /admin
    # 允许 /books/reviews, /events/booksale/reviews, /reviews
    - */reviews</pre>
<p>除了限定命名空间 + 服务 + 动词 + 路径以外，你还可以提供额外的约束，例如限定请求头：</p>
<pre class="crayon-plain-tag">constraints:
    # 请求头中的version必须声明为v1或v2
    - key: request.headers[version]
      values: ["v1", "v2"]</pre>
<div class="blog_h3"><span class="graybg">ServiceRoleBinding</span></div>
<p>已经废弃，使用AuthorizationPolicy代替。</p>
<p>为用户、组、服务授予ServiceRole。示例：</p>
<pre class="crayon-plain-tag">apiVersion: "rbac.istio.io/v1alpha1"
kind: ServiceRoleBinding
metadata:
  name: test-binding-products
  namespace: default
spec:
  # 被授予角色的主体
  subjects:
    # 主体为service-account-a，它是服务a使用的service-account
  - user: "service-account-a"
    # 主体为Istio的Ingress服务账号
  - user: "istio-ingress-service-account"
    # 同时要求请求的JWT的email为me@gmem.cc
    properties:
      request.auth.claims[email]: "me@gmem.cc"
    # 允许任何人访问服务
  - user: "*"
    # 允许任何通过身份验证的人访问服务
  - properties:
      source.principal: "*"
  # 角色
  roleRef:
    kind: ServiceRole
    name: "products-viewer"</pre>
<div class="blog_h3"><span class="graybg">AuthorizationPolicy</span></div>
<p>要配置Istio授权策略，现在应该使用这个新API。</p>
<p>每个AuthorizationPolicy包含：</p>
<ol>
<li>一个选择器，对应selector字段，指定策略作用于的Target，<span style="background-color: #c0c0c0;">selector和metadata.namespace共同决定了Target</span></li>
<li>多个规则，每个规则指定Who允许在什么Conditions下做What动作
<ol>
<li>rule.from字段即Who</li>
<li>rule.to字段对应What</li>
<li>rule.when对应Conditions</li>
</ol>
</li>
</ol>
<p>示例，如果请求来自dev命名空间或SA账号并且携带了合法JWT则允许访问foo命名空间的httpbin:v1服务：</p>
<pre class="crayon-plain-tag">apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
# 对foo命名空间的httpbin服务
metadata:
 name: httpbin
 namespace: foo
spec:
 selector:
   matchLabels:
     app: httpbin
     version: v1
 rules:
 # 不指定from则针对所有用户
 - from:
   # 允许SA和命名空间
   - source:
       principals: ["cluster.local/ns/default/sa/sleep"]
   - source:
       namespaces: ["dev"]
   # 所有通过身份验证的用户
   - source:
       principals: ["*"]

   # 进行GET操作
   to:
   - operation:
       methods: ["GET"]
       paths: ["/test/*", "*/info"]

   when:
   # 如果具有合法的JWT
   - key: request.auth.claims[iss]
     values: ["https://accounts.google.com"]
   # 所有支持的键 https://istio.io/docs/reference/config/security/conditions/ 
   - key: request.headers[version]
     values: ["v1", "v2"]</pre>
<p> 对于任何Plain TCP协议，Istio授权策略也支持，配置方式和上面这个HTTP的例子类似，但是需要注意某些字段仅仅支持HTTP。</p>
<div class="blog_h1"><span class="graybg">Sidecar配置</span></div>
<p>要定制Sidecar的行为，可以修改istio-sidecar-injector这个Configmap，或者给Pod添加对应的注解。</p>
<p>istio-sidecar-injector声明了需要给目标Pod注入的两个容器 —— istio-init、istio-proxy —— 的全部配置信息。这两个容器会自动读取Pod上的注解，覆盖对应的默认配置。</p>
<p>默认配置在安装时，通过Helm Chart的Values传入。</p>
<div class="blog_h2"><span class="graybg">禁止注入</span></div>
<div class="blog_h3"><span class="graybg">特定资源</span></div>
<p>如果当前命名空间启用了自动化的Sidecar注入，而某个Deployment却不想加入网格，可以在其PodTemplate上添加注解：</p>
<pre class="crayon-plain-tag">apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: ignored
spec:
  template:
    metadata:
      annotations:
        # 禁止注入
        sidecar.istio.io/inject: "false"
    spec:</pre>
<div class="blog_h3"><span class="graybg">基于选择器</span></div>
<p>你还可以修改配置字典istio-sidecar-injector，指定什么样的Pod绝不被注入Sidecar：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: ConfigMap
metadata:
  name: istio-sidecar-injector
data:
  config: |-
    policy: enabled
    neverInjectSelector:
      - matchExpressions:
        - {key: openshift.io/build.name, operator: Exists}
      - matchExpressions:
        - {key: openshift.io/deployer-pod-for.name, operator: Exists} </pre>
<div class="blog_h2"><span class="graybg">istio-init</span></div>
<div class="blog_h3"><span class="graybg">命令行选项</span></div>
<p>此容器运行脚本/usr/local/bin/istio-iptables.sh，命令行选项如下：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30px; text-align: center;"> </td>
<td style="width: 90px; text-align: center;">默认值</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>-p</td>
<td>15001</td>
<td>通过Iptables将所有TCP流量重定向到什么端口，Envoy必须在此端口上监听</td>
</tr>
<tr>
<td>-u</td>
<td>1337</td>
<td>对于<span style="background-color: #c0c0c0;">什么用户的</span>流量，不进行重定向，通常是代理容器的UID，此UID默认值为$ENVOY_USER=1337</td>
</tr>
<tr>
<td>-g</td>
<td> </td>
<td>对于什么用户组的流量，不进行重定向</td>
</tr>
<tr>
<td>-m</td>
<td>REDIRECT</td>
<td><span style="background-color: #c0c0c0;">重定向入站流量</span>到Envoy的方式。REDIRECT或TPROXY</td>
</tr>
<tr>
<td>-i</td>
<td>*</td>
<td>
<p>逗号分隔的<span style="background-color: #c0c0c0;">CIRD形式的IP范围列表</span>，<span style="background-color: #c0c0c0;">匹配此CIRD的出站流量</span>将被重定向给Envoy。设置为*则<span style="background-color: #c0c0c0;">所有出站流量都被重定向给Envoy</span></p>
<p>如果没有配置适当的Istio资源，例如ServiceEntry，Envoy默认情况下可能返回404，这导致默认情况下你无法访问网格外部的服务</p>
</td>
</tr>
<tr>
<td>
<p>-x</p>
</td>
<td>""</td>
<td>
<p>当<span style="background-color: #c0c0c0;">上一个参数设置为*时</span>，用来指明哪些出站流量<span style="background-color: #c0c0c0;">不重定向给Envoy</span>，形式与上一参数相同</p>
<p>不重定向给Envoy，则<span style="background-color: #c0c0c0;">流量的行为和没有部署服务网格时一致</span></p>
</td>
</tr>
<tr>
<td>-b</td>
<td>""</td>
<td>
<p>逗号分隔的端口列表，来自这些端口的<span style="background-color: #c0c0c0;">入站流量将重定向给Envoy</span>。设置为*则所有入站流量都重定向给Envoy，设置为空则禁用入站流量重定向</p>
<p>默认情况下，已经<span style="background-color: #c0c0c0;">在匹配当前Pod的K8S Service声明的端口，重定向给Envoy</span>，也就是说，虽然容器监听了8086端口，但是没有配置相应的K8S Service，就不走Envoy，因而不能进行策略、安全检查</p>
</td>
</tr>
<tr>
<td>-d</td>
<td>15020</td>
<td>
<p>当上一个参数设置为*时，用来指定哪些入站流量不重定向给Envoy，形式与上一参数相同</p>
<p>默认情况下，Pilot代理状态端口15020不重定向给Envoy</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">注解列表</span></div>
<p>所有注解：<a href="https://istio.io/docs/reference/config/annotations/">https://istio.io/docs/reference/config/annotations/</a></p>
<p>常用的注解如下：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">注解</td>
<td style="width: 100px; text-align: center;">对应选项</td>
</tr>
</thead>
<tbody>
<tr>
<td>sidecar.istio.io/interceptionMode</td>
<td>
<p>-m</p>
</td>
</tr>
<tr>
<td>traffic.sidecar.istio.io/includeOutboundIPRanges</td>
<td>-i</td>
</tr>
<tr>
<td>traffic.sidecar.istio.io/excludeOutboundIPRanges</td>
<td>
<p>-x</p>
</td>
</tr>
<tr>
<td>traffic.sidecar.istio.io/includeInboundPorts</td>
<td>-b</td>
</tr>
<tr>
<td>traffic.sidecar.istio.io/excludeInboundPorts</td>
<td>-d</td>
</tr>
</tbody>
</table>
<p>traffic.sidecar.istio.io/*会导致Iptables规则的变更。例如：</p>
<pre class="crayon-plain-tag"># 注解：
# traffic.sidecar.istio.io/includeOutboundIPRanges: "221.0.0.0/8"
# 产生
iptables -t nat -A ISTIO_OUTPUT -d 221.0.0.0/8 -j ISTIO_REDIRECT

# 注解：
# traffic.sidecar.istio.io/includeOutboundIPRanges: "*"
# traffic.sidecar.istio.io/excludeOutboundIPRanges: "221.0.0.0/8"
# 产生：
iptables -t nat -A ISTIO_OUTPUT -j ISTIO_REDIRECT
iptables -t nat -A ISTIO_OUTPUT -d 221.0.0.0/8 -j RETURN</pre>
<p>istio-init会把上述规则打印到标准输出。</p>
<div class="blog_h2"><span class="graybg">istio-proxy</span></div>
<p>可以修改istio-sidecar-injector中istio-proxy容器的命令行参数，从而间接的影响Envoy的行为。</p>
<div class="blog_h3"><span class="graybg">命令行选项</span></div>
<p>此代理的入口点是pilot-agent，该进程会创建envoy进程，执行下面的命令可以了解可传递给Envoy的参数列表：</p>
<pre class="crayon-plain-tag">docker run -it --rm registry.k8s.arch.mid/istio/proxyv2:1.0.3 proxy --help
# 查看proxy子命令（启动envoy）的参数：
# --applicationPorts stringSlice      应用程序暴露的端口        
# --availabilityZone string           可用性区域
# --binaryPath string                 Envoy的位置，默认/usr/local/bin/envoy
# --concurrency int                   工作线程数量
# --configPath string                 自动产生的Envoy配置文件位置，默认/etc/istio/proxy
# --connectTimeout duration           Envoy连接到支持性服务的超时，默认1s   
# --controlPlaneAuthPolicy string     控制平面身份验证策略，默认NONE      
# --customConfigFile string           自定义配置文件的位置
# --disableInternalTelemetry          禁用内部遥测
# --discoveryAddress string           暴露xDS的发现服务的地址，默认istio-pilot:15007
# --discoveryRefreshDelay duration    服务发现轮询间隔，EDS, CDS, LDS使用，RDS不使用，默认1秒
# --domain string                     DNS后缀，默认${POD_NAMESPACE}.svc.cluster.local 
# --drainDuration duration            热重启时Envoy Drain连接的时间，默认2秒
# --id string                         代理的唯一标识，默认${POD_NAME}.${POD_NAMESPACE}
# --ip string                         代理的IP地址，默认${INSTANCE_IP}                         
# --proxyAdminPort uint16             Envoy监听管理命令的端口，默认15000
# --proxyLogLevel string              Envoy的日志级别，trace, debug, info, warn, err, critical, off。默认warn
# --serviceregistry string            服务注册表的底层平台，Kubernetes, Consul, CloudFoundry, Mock, Config。默认Kubernetes
# --statsdUdpAddress string           Statd的UDP监听地址
# --statusPort uint16                 在什么端口上暴露Pilot代理的状态
# --zipkinAddress string              Zipkin服务的地址

# 全局参数：
# --log_as_json                   是否将日志输出为JSON格式，默认输出人类友好的控制台格式
# --log_output_level string       debug, info, warn, error, none，默认default:info</pre>
<div class="blog_h2"><span class="graybg">注解配置示例</span></div>
<div class="blog_h3"><span class="graybg">出站流量默认经过Envoy</span></div>
<p>下面的配置，可以让位于网格内部的容器访问10.0.0.0/8网段，而不需要定义ServiceEntry即可访问该网络的任意IP（或者DNS名称）的任意端口：</p>
<pre class="crayon-plain-tag">apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: ubuntu
    tier: devops
  name: ubuntu
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ubuntu
      tier: devops
  template:
    metadata:
      annotations:
        traffic.sidecar.istio.io/includeOutboundIPRanges: "*"
        # 10.0.0.0/8网段直接访问，不走Envoy代理
        # 要仅仅允许单个IP绕过Envoy代理，可以用 10.5.12.157/32的形式
        traffic.sidecar.istio.io/excludeOutboundIPRanges: "10.0.0.0/8"
      labels:
        app: ubuntu
        tier: devops
    spec:
      containers:
      - args:
        - -c
        - sleep 365d
        command:
        - /bin/sh
        image: docker.gmem.cc/ubuntu:16.04
        imagePullPolicy: Always
        name: ubuntu
      dnsPolicy: ClusterFirst
      restartPolicy: Always</pre>
<div class="blog_h3"><span class="graybg">出站流量默认绕过Envoy</span></div>
<pre class="crayon-plain-tag"># 让容器网络、集群服务网络，以及单个IP地址10.5.12.157走Envoy代理
# 其它地址都不走Envoy代理
traffic.sidecar.istio.io/includeOutboundIPRanges: "172.27.0.0/16,10.96.0.0/12,10.5.12.157/32"</pre>
<p>注意一个现象：通过IP地址10.5.12.157访问，可以发现是通过Envoy的（符合预期）。但是<span style="background-color: #c0c0c0;">通过域名（解析到10.5.12.157）访问，则绕过了Envoy</span>。</p>
<div class="blog_h2"><span class="graybg">Sidecar</span></div>
<p>使用这个CRD，可以配置其关联的工作负载（如果没有workloadSelector则命名空间所有工作负责）的入站、出站通信规则。</p>
<p>具体参考<a href="#sidecar-cr">Sidecar资源</a>一节。</p>
<div class="blog_h2"><span class="graybg">EnvoyFilter</span></div>
<p>这个CRD用于描述Envoy代理的网络过滤器/HTTP过滤器的配置信息，可以用来定制化Istio生成的Envoy配置。使用EnvoyFilter你可以：</p>
<ol>
<li>修改某个字段的值</li>
<li>添加过滤器</li>
<li>添加一个完整的Listener、Cluster，等等</li>
</ol>
<p>使用此CR要小心，错误的配置可能破坏网格。</p>
<p>对于每个工作负载，多个EnvoyFilter可以被增量的应用上去。这些EnvoyFilter的应用顺序是：</p>
<ol>
<li>定义在rootNamespace中的所有EnvoyFilter</li>
<li>定义在工作负载所属命名空间的、匹配工作负载的过滤器</li>
</ol>
<div class="blog_h3"><span class="graybg">配置说明</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">配置项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>ApplyTo</td>
<td>INVALID<br />LISTENER 给监听器打补丁<br />FILTER_CHAIN  给过滤器链打补丁<br />NETWORK_FILTER 给网络过滤器打补丁，增删改HTTP过滤器<br />HTTP_FILTER 给HTTP过滤器打补丁，增删改HTTP过滤器<br />ROUTE_CONFIGURATION 修改路由配置（RDS输出），不会修改虚拟主机，仅仅支持MERGE操作<br />VIRTUAL_HOST 修改路由配置中的虚拟主机<br />HTTP_ROUTE 修改路由配置中匹配的虚拟主机的路由对象HTTP_ROUTE<br />CLUSTER  修改集群配置（CDS输出）</td>
</tr>
<tr>
<td>InsertPosition.Index</td>
<td>FIRST  插入在最前面<br />LAST 插入在最后<br />BEFORE  插入在特定过滤器之前<br />AFTER 插入在特定过滤器之后</td>
</tr>
<tr>
<td>Patch.Operation</td>
<td>MERGE  将提供的配置合并到现有的、自动生成的配置<br />ADD  添加到现有的监听器、集群、虚拟主机、网络过滤器、HTTP过滤器的列表中<br />REMOVE  从列表中移除<br />INSERT_BEFORE  在命名对象之前插入<br />INSERT_AFTER 在命名对象之后插入<br />INSERT_FIRST 插入在列表最前面</td>
</tr>
<tr>
<td>PatchContext</td>
<td>ANY  修改网关+边车<br />SIDECAR_INBOUND  修改边车入站配置<br />SIDECAR_OUTBOUND  修改边车出站配置<br />GATEWAY 修改网关配置</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">配置示例</span></div>
<p>例子一，定义在rootNamespace中的全局默认EnvoyFilter：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: custom-protocol
  # rootNamespace的实际值取决于meshConfig
  namespace: istio-config
spec:
  # Envoy补丁，由于此资源定义在rootNamespace，因此对所有Sidecar有效
  configPatches:
    # 应用到L4过滤器
  - applyTo: NETWORK_FILTER
    # 匹配规则
    match:
      # 匹配出站监听器
      context: SIDECAR_OUTBOUND
      # 匹配9307监听器
      # 的过滤器 envoy.tcp_proxy
      listener:
        portNumber: 9307
        filterChain:
          filter:
            name: "envoy.tcp_proxy"
    # 补丁规格
    patch:
      # 在envoy.tcp_proxy后面插入
      operation: INSERT_BEFORE
      value:
        #  插入的过滤器的name，以及config或typed_config配置
        name: "envoy.config.filter.network.custom_protocol"
        config:
         ...
  - applyTo: NETWORK_FILTER
    match:
      # 匹配HTTP连接管理器（L4）
      listener:
        filterChain:
          filter:
            name: "envoy.http_connection_manager"
    patch:
      # 合并配置项
      operation: MERGE
      value:
        name: "envoy.http_connection_manager"
        typed_config:
          "@type": "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"
          idle_timeout: 30s</pre>
<p>例子二，匹配特定工作负载的EnvoyFilter：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: reviews-lua
  namespace: bookinfo
spec:
  # 匹配当前命名空间的哪些工作负载
  workloadSelector:
    labels:
      app: reviews
  configPatches:
    # 添加L7过滤器
  - applyTo: HTTP_FILTER
    match:
      # 匹配入站8080监听器
      context: SIDECAR_INBOUND
      listener:
        portNumber: 8080
        filterChain:
          filter:
            name: "envoy.http_connection_manager"
            subFilter:
              name: "envoy.router"
    patch:
      # 在L7过滤器envoy.router之前插入
      operation: INSERT_BEFORE
      value:
       name: envoy.lua
       typed_config:
          "@type": "type.googleapis.com/envoy.config.filter.http.lua.v2.Lua"
         inlineCode: |
           function envoy_on_request(request_handle)
             local headers, body = request_handle:httpCall(
              "lua_cluster",
              {
               [":method"] = "POST",
               [":path"] = "/acl",
               [":authority"] = "internal.org.net"
              },
             "authorize call",
             5000)
           end
  # 上面的Lua代码调用lua_cluster，这里是Cluster的定义
  - applyTo: CLUSTER
    match:
      # 添加出站集群
      context: SIDECAR_OUTBOUND
    patch:
      operation: ADD
      value:
        name: "lua_cluster"
        type: STRICT_DNS
        connect_timeout: 0.5s
        lb_policy: ROUND_ROBIN
        hosts:
        - socket_address:
            protocol: TCP
            address: "internal.org.net"
            port_value: 8888</pre>
<p>例子三， 配置入站网关：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: hcm-tweaks
  namespace: istio-system
spec:
  workloadSelector:
    # 这里要和网关的Pod匹配
    labels:
      istio: ingress-gateway
  configPatches:
  - applyTo: NETWORK_FILTER
    match:
      # 配置网关，针对SNI进行匹配
      context: GATEWAY
      listener:
        filterChain:
          sni: app.example.com
          filter:
            name: "envoy.http_connection_manager"
    patch:
      operation: MERGE
      # 修改一些字段
      value:
        idle_timeout: 30s
        xff_num_trusted_hops: 5</pre>
<p><a id="original_src"></a>例子四，添加监听器过滤器：</p>
<pre class="crayon-plain-tag">apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: nginx-original-src
  namespace: default
spec:
  workloadSelector:
    labels:
      app: nginx
  configPatches:
  - applyTo: LISTENER
    match:
      context: SIDECAR_INBOUND
      listener:
        portNumber: 80
    patch:
      operation: MERGE
      value:
        # 增加一个监听器过滤器，这会添加在尾部
        listenerFilters:
        - name: envoy.listener.original_src
          config:
            mark: 1986</pre>
<div class="blog_h1"><span class="graybg">运维</span></div>
<div class="blog_h2"><span class="graybg">组件日志</span></div>
<p>Istio的组件使用了一个灵活的日志框架，你可以很方便的切换日志级别（修改命令行参数），查看某个组件的日志。</p>
<div class="blog_h3"><span class="graybg">日志范围</span></div>
<p>一个组件输出的日志被分类到scope中，scope代表一组相关的、可以一起控制的日志消息。不同组件具有不同的scopes，所有组件都包含一个名为default的scope，所有未分类的日志消息都在其中。</p>
<p>例如，Mixer包含5个scope，分别对应了Mixer的不同功能区域：adapter、sapi、attributes、default、grpcAdapter。</p>
<p>每个scope都包含以下日志级别：none、error、warning、info、debug。</p>
<p>要控制输出哪些日志，你可以用--log_output_level参数，例如：</p>
<pre class="crayon-plain-tag">mixs server --log_output_level attributes=debug,adapters=warning</pre>
<p>你也可以使用ControlZ在运行时动态的修改日志级别。</p>
<div class="blog_h3"><span class="graybg">日志输出</span></div>
<p>选项--log_target用于重定向日志到任意位置，可以指定逗号分割的系统路径，以及stdout、stderr。</p>
<p>选项--log_as_json可以让日志输出为JSON格式。</p>
<div class="blog_h3"><span class="graybg">日志轮换</span></div>
<p>选项--log_rotate控制日志轮换的basename。--log_rotate_max_size指定日志的最大MB。--log_rotate_max_backups指定保留的日志文件个数。</p>
<div class="blog_h3"><span class="graybg">调试信息</span></div>
<p>选项--log_caller和--log_stacktrace_level可以控制日志中是否包含编程级别的信息，用于跟踪组件的潜在缺陷。</p>
<div class="blog_h2"><span class="graybg">组件内省</span></div>
<p>Istio的组件使用了一个灵活的内省（Introspection）框架，让你能够轻松的查看、操控运行中的组件的内部状态，这个功能叫ControlZ。</p>
<p>组件可以暴露一个端口，此端口提供一个WebUI供你访问ControlZ。</p>
<p>通过ControlZ可以调整<span style="background-color: #c0c0c0;">日志级别</span>、查看组件<span style="background-color: #c0c0c0;">内存用量、环境变量、进程信息、命令行参数、版本信息、统计指标</span>。</p>
<p>选项--ctrlz_address、--ctrlz_port用于控制在什么网络接口、端口上暴露ControlZ。 </p>
<div class="blog_h2"><span class="graybg">调试Envoy/Pilot</span></div>
<p>使用proxy-status命令可以获取网格的整体状态，并识别由代理造成的问题。使用proxy-config命令可以查看Envoy的配置以帮助诊断问题。</p>
<div class="blog_h3"><span class="graybg">网格整体状态</span></div>
<pre class="crayon-plain-tag">istioctl proxy-status
# PROXY               CDS        LDS        EDS            RDS      PILOT             VERSION
# details-v1.default  SYNCED     SYNCED     SYNCED (100%)  SYNCED   istio-pilot-8499b 1.0.2</pre>
<p>如上面的例子所示，Envoy的CDS（集群发现服务）、LDS（监听器发现服务）、EDS（端点发现服务）、RDS（路由发现服务）的状态都被显示。状态的值可以是：</p>
<ol>
<li>SYNCED，Envoy已经确认了Pilot发送给它的最后配置</li>
<li>SYNCED (100%) ，Envoy已经完全同步了集群的端点</li>
<li>NOT SENT ，Pilot没有发送任何东西给Envoy</li>
<li>STALE，Pilot发送了更新给Enovy，但是后者没有接收到并给与确认。可能原因是网络故障或者Istio的Bug </li>
</ol>
<div class="blog_h3"><span class="graybg">检查未同步配置</span></div>
<p>执行下面的命令可以查看Pilot配置和某个Envoy实例配置存在什么差异，也就是哪些配置没有同步：</p>
<pre class="crayon-plain-tag">istioctl proxy-status details-v1-876bf485f-bxwh7.default</pre>
<div class="blog_h2"><span class="graybg"><a id="dump-envoy-config"></a>Dump出Envoy配置</span></div>
<p>istioctl proxy-config包含以下子命令：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 100px; text-align: center;">子命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">bootstrap</td>
<td>
<p>自举配置，包含一些启动时必要的信息，例如Pilot地址（对于Envoy来说，xDS管理服务器的地址）是什么</p>
<p>命令示例：</p>
<pre class="crayon-plain-tag">// istioctl proxy-config bootstrap productpage-v1-8d69b45c-jltzt 
{
    "bootstrap": {
        "node": {
            "id": "sidecar~172.27.226.67~productpage-v1-8d69b45c-jltzt.default~default.svc.k8s.gmem.cc",
            "cluster": "productpage",
            "metadata": {
                // 各种元数据，例如Pod的标签
            },
            // Envoy的版本
            "buildVersion": "0/1.8.0-dev//RELEASE"
        },
        "staticResources": {
            "listeners": [/* 静态配置的监听器 */],
            "clusters": [
                {
                    // Pilot集群，xDS服务器
                    "name": "xds-grpc",
                    "type": "STRICT_DNS",
                    "connectTimeout": "10.000s",
                    "hosts": [
                        {
                            "socketAddress": {
                                "address": "istio-pilot.istio-system",
                                "portValue": 15010
                            }
                        }
                    ],
                },
                {
                    // Tracer集群
                    "name": "zipkin",
                    "type": "STRICT_DNS",
                    "hosts": [
                        {
                            "socketAddress": {
                                "address": "zipkin.istio-system",
                                "portValue": 9411
                            }
                        }
                    ]
                }
            ]
        },
        // 动态发现配置
        "dynamicResources": {
            "ldsConfig": {
                "ads": {
                }
            },
            "cdsConfig": {
                "ads": {
                }
            },
            // ADS，聚合发现服务（Aggregated Discovery Service）
            "adsConfig": {
                "apiType": "GRPC",
                "grpcServices": [
                    {
                        "envoyGrpc": {
                            "clusterName": "xds-grpc"
                        }
                    }
                ],
                "refreshDelay": "1.000s"
            }
        },
        "statsConfig": {
            // 监控指标配置
        },
        // 追踪配置
        "tracing": {
            "http": {
                "name": "envoy.zipkin",
                "config": {
                    "collector_cluster": "zipkin"
                }
            }
        },
        // 管理端口
        "admin": {
            "accessLogPath": "/dev/null",
            "address": {
                "socketAddress": {
                    "address": "127.0.0.1",
                    "portValue": 15000
                }
            }
        }
    }
}</pre>
</td>
</tr>
<tr>
<td class="blog_h3">cluster</td>
<td>
<p>上游集群配置
<p>命令示例，根据全限定DNS名称查询某个Pod的某个集群：</p>
<pre class="crayon-plain-tag">// istioctl proxy-config clusters productpage-v1-8d69b45c-jltzt \
//          --fqdn details.default.svc.k8s.gmem.cc -o json

[
    {
        //     出站集群，即当前节点需要访问的集群
        //               此集群对应的Istio服务是details.default.svc.k8s.gmem.cc:9080
        //                     不是服务子集
        "name": "outbound|9080||details.default.svc.k8s.gmem.cc",
        // 集群的端点通过EDS协议发现
        "type": "EDS",
         // EDS通过ADS协议获得
        "edsClusterConfig": {
            "edsConfig": {
                "ads": {}
            },
            "serviceName": "outbound|9080||details.default.svc.k8s.gmem.cc"
        },
        // 超时和断路器配置，对应Istio的DestinationRule中的配置信息
        // 由于每个子集都产生Cluster，因此单个DestinationRule可以产生多个Cluster配置
        "connectTimeout": "1.000s",
        "circuitBreakers": {
            "thresholds": [
                {}
            ]
        }
    },
    {
        "name": "outbound|9080|v1|details.default.svc.k8s.gmem.cc",
        "type": "EDS",
    },
    {
        "name": "outbound|9080|v2|details.default.svc.k8s.gmem.cc",
        "type": "EDS",
    }
]</pre>
<p>可以看到此例子中有三个上游集群的FQDN是details.default.svc.k8s.gmem.cc，它们对应了不同的subset</p>
</td>
</tr>
<tr>
<td class="blog_h3">endpoint</td>
<td>
<p>查看某个节点所有集群的端点列表，或者某个集群的端点列表，或者特定端点的信息：</p>
<pre class="crayon-plain-tag"># 查看全限定DNS名称为kubernetes.default.svc.k8s.gmem.cc的集群列表
istioctl proxy-config cluster ubuntu --fqdn=kubernetes.default.svc.k8s.gmem.cc

# 查看上述集群的端点列表                   此参数可以从上个命令的-o json中获得
istioctl proxy-config endpoint  ubuntu --cluster='outbound|443||kubernetes.default.svc.k8s.gmem.cc'
# ENDPOINT          STATUS      CLUSTER
# 10.0.2.1:6443     HEALTHY     outbound|443||kubernetes.default.svc.k8s.gmem.cc
# 10.0.3.1:6443     HEALTHY     outbound|443||kubernetes.default.svc.k8s.gmem.cc
# 10.0.5.1:6443     HEALTHY     outbound|443||kubernetes.default.svc.k8s.gmem.cc

# 查看某个特定端点
istioctl proxy-config endpoint ubuntu --address=10.0.2.1 --port=6443 -o json</pre>
<p>你也可以使用-o json选项，将集群的端点配置输出为JSON：</p>
<pre class="crayon-plain-tag">[                                                                                    
    {                       
        // 集群名称                                                         
        "name": "outbound|443||kubernetes.default.svc.k8s.gmem.cc",                  
        "addedViaApi": true,                                                           
        // 端点列表
        "hostStatuses": [                                                               
            {     
                // 端点的地址                                                                   
                "address": {                                                         
                    "socketAddress": {                                                   
                        "address": "10.0.2.1",                                       
                        "portValue": 6443                                            
                    }                                                                
                },         
                // 端点的监控指标                                                          
                "stats": {                                                           
                    "cx_active": {                                                   
                        "type": "GAUGE"                                              
                    },                                                               
                    "cx_connect_fail": {},                                                 
                    "cx_total": {},                                                  
                    "rq_active": {                                                   
                        "type": "GAUGE"                                              
                    },                                                               
                    "rq_error": {},                                                  
                    "rq_success": {},                                                 
                    "rq_timeout": {},                                                 
                    "rq_total": {}                                                   
                },             
                // 端点的健康状态                                                      
                "healthStatus": {                                                       
                    "edsHealthStatus": "HEALTHY"                                           
                }                                                                    
            },
            {
                "address": {
                    "socketAddress": {
                        "address": "10.0.3.1",
                        "portValue": 6443
                    }
                },
                "stats": {
                }
             }
         ]
    }
]</pre>
<p>注意，<span style="background-color: #c0c0c0;">只能将集群的所有端点一起输出</span>，不能输出单个端点</p>
</td>
</tr>
<tr>
<td class="blog_h3"><a id="proxy-config-listener"></a>listener</td>
<td>
<p>获取监听器配置</p>
<p>下面的例子获取名为ubuntu的工作负载的监听器列表。此工作负载有一个监听8080端口的服务：</p>
<pre class="crayon-plain-tag"># istioctl proxy-config listener ubuntu 
ADDRESS            PORT      TYPE 

# 绝大部分监听器，都是虚拟监听器
# 之所以叫“虚拟”，是因为这些监听器的deprecatedV1.bindToPort = false
# 因而并没有创建对应的监听套接字，通过netstat你可以验证这一事实：
# tcp        0      0 0.0.0.0:15090           0.0.0.0:*               LISTEN     
# tcp        0      0 127.0.0.1:15000         0.0.0.0:*               LISTEN     
# tcp        0      0 0.0.0.0:15001           0.0.0.0:*               LISTEN     
# tcp6       0      0 :::15020                :::*                    LISTEN
# 可以看到，真正监听的只有Prometheus stat、Admin endpoint、
#                     IP table劫持目的端口、代理状态端口
# 警告：这里谈得是以sidecar模式运行的代理
# 以proxy模式运行（ingressgateway、egressgatey）的代理，可能由Gateway资源生成很多物理监听器



# 下面这批，是为每个K8S服务所创建的监听器
# 它们负责接收并处理 Pod ⇨ 0.0.0.0_15001 ⇨ 对应IP:PORT的出站非HTTP流量
10.110.228.191     80        TCP  
10.103.208.103     42422     TCP  
10.106.199.4       15011     TCP  
10.107.171.192     9300      TCP  
10.98.46.166       16686     TCP  
10.107.18.161      14268     TCP  
10.102.62.178      80        TCP  
10.100.71.207      9300      TCP  
10.96.0.10         53        TCP  
10.99.111.5        44134     TCP  
10.101.112.205     15011     TCP  
10.101.112.205     31400     TCP  
10.101.112.205     853       TCP  
10.110.142.202     80        TCP  
10.103.113.112     80        TCP  
10.104.57.35       443       TCP  
10.96.0.1          443       TCP  
10.106.219.132     443       TCP  
10.99.131.204      80        TCP  
10.101.112.205     443       TCP  
10.101.112.205     8060      TCP  
10.98.219.80       443       TCP  
10.109.240.127     9100      TCP  
10.110.126.109     27017     TCP  
10.107.18.161      14267     TCP  
10.96.63.168       80        TCP  
10.97.147.18       443       TCP  
10.99.99.66        443       TCP  

# 它们负责接收并处理 Pod ⇨ 0.0.0.0_15001 ⇨ 对应PORT的出站HTTP流量
# 也就是说，此Pod需要对外访问的HTTP服务，如果它们端口相同，则共享一个监听器
# 而不是像非HTTP服务那样，每个IP+PORT的组合都需要一个监听器

# Istio Pilot发现服务
0.0.0.0            15010     HTTP 
0.0.0.0            8080      HTTP 
0.0.0.0            15030     HTTP 
0.0.0.0            15031     HTTP 
0.0.0.0            15004     HTTP
0.0.0.0            3000      HTTP
0.0.0.0            9091      HTTP
0.0.0.0            9200      HTTP
0.0.0.0            8060      HTTP
0.0.0.0            9411      HTTP
0.0.0.0            80        HTTP
0.0.0.0            9093      HTTP
0.0.0.0            20001     HTTP
0.0.0.0            9090      HTTP
0.0.0.0            8086      HTTP
# 上面这个是因为本Pod在8086上暴露服务（因为有对应的Service配置）而创建的供出站流量
# 使用的监听器 —— 因为可以访问自己



# 这些在Pod IP上监听的虚拟监听器，接收并处理：
# 外部 ⇨ PodIP:8086 ⇨ 0.0.0.0_15001 ⇨ 127.0.0.1:8086 的入站流量
172.27.121.134     8086      HTTP

# Pilot代理状态端口
172.27.121.188     15020     TCP

# 这个是实际存在的监听套接字，负责处理所有（被iptables重定向给15001的）出入Pod的流量
0.0.0.0            15001     TCP</pre>
<p>你会看到输出的监听器很多，主要包括：</p>
<ol>
<li>0.0.0.0:15001，此监听器接收<span style="background-color: #c0c0c0;">出入Pod的所有流量</span>，然后将请求转发给虚拟监听器处理，它<span style="background-color: #c0c0c0;">本身不进行任何处理</span></li>
<li> 每个K8S <span style="background-color: #c0c0c0;">Service IP</span>、每<span style="background-color: #c0c0c0;">个非HTTP流量出口（Host+Port）</span>对应一个<span style="background-color: #c0c0c0;">虚拟监听器</span></li>
<li>在<span style="background-color: #c0c0c0;">Pod IP上监听</span>的，为<span style="background-color: #c0c0c0;">入口流量暴露的每一个端口</span>，对应一个虚拟监听器</li>
<li>为每个HTTP出口端口创建的在0.0.0.0上监听的虚拟监听器</li>
</ol>
<p>使用-p参数，可以输出单个监听器的详细配置：</p>
<pre class="crayon-plain-tag">// istioctl proxy-config listener productpage-v1-8d69b45c-jltzt  --port 15001 -o json
[
    {
        "name": "virtual",
        "address": {
            "socketAddress": {
                "address": "0.0.0.0",
                "portValue": 15001
            }
        },
        "filterChains": [
            {
                "filters": [
                    {
                        "name": "envoy.tcp_proxy",
                        "config": {
                            "cluster": "BlackHoleCluster",
                            "stat_prefix": "BlackHoleCluster"
                        }
                    }
                ]
            }
        ],
        "useOriginalDst": true
    }
]</pre>
<p>需要注意的是0.0.0.0:15001监听器，<span style="background-color: #c0c0c0;">Iptables会把所有入口、出口流量都路由到此监听器</span>。 此监听器的useOriginalDst设置为true，表示<span style="background-color: #c0c0c0;">该监听器会把请求转交给最匹配请求目的地的虚拟监听器处理</span>，如果找不到匹配，则给BlackHoleCluster处理，BlackHoleCluster返回404</p>
<p>例如，对于向9080端口发送的<span style="background-color: #c0c0c0;">出站请求</span>，会<span style="background-color: #c0c0c0;">由虚拟监听器0.0.0.0:9080负责处理</span>。此监听器会从RDS中查找路由，路由的名字可以使用下面的命令得到：</p>
<pre class="crayon-plain-tag">//  istioctl proxy-config listener productpage-v1-8d69b45c-jltzt --address 0.0.0.0 --port 9080

"rds": {
    "config_source": {
        "ads": {}
    },
    // 配置的来源是ADS，路由的名字是9080
    "route_config_name": "9080"
},</pre>
</td>
</tr>
<tr>
<td class="blog_h3">route</td>
<td>
<p>路由配置，注意监听器配置中可能仅仅提示了路由配置的名字，你需要使用route子命令再查询一次
<p>执行下的命令，可以看到路由单品页Pod的9080路由的详细信息：</p>
<pre class="crayon-plain-tag">// istioctl proxy-config routes productpage-v1-8d69b45c-jltzt --name 9080 -o json

[                                                                                                                                                                                        
    {                                                                                                                                                                                    
        "name": "9080",                                                                                                                                                                  
        "virtualHosts": [                                                                                                                                                                
            {                                                                                                                                                                            
                "name": "details.default.svc.k8s.gmem.cc:9080",                                                                                                                          
                "domains": [                                                                                                                                                             
                    "details.default.svc.k8s.gmem.cc",                                                                                                                                   
                    "details.default.svc.k8s.gmem.cc:9080",                                                                                                                              
                    "details",
                    "details:9080",
                    "details.default.svc.k8s.gmem",
                    "details.default.svc.k8s.gmem:9080",
                    "details.default.svc.k8s",
                    "details.default.svc.k8s:9080",
                    "details.default.svc",
                    "details.default.svc:9080",
                    "details.default",
                    "details.default:9080",
                    "10.98.119.24",
                    "10.98.119.24:9080"
                ],
                "routes": [
                    {
                        "match": {
                            "prefix": "/"
                        },
                        "route": {
                            "cluster": "outbound|9080|v1|details.default.svc.k8s.gmem.cc",
                            "timeout": "0.000s",
                            "maxGrpcTimeout": "0.000s"
                        },
...</pre>
<p>可以看到一个路由下面可以定义很多虚拟主机， 以details.default.svc.k8s.gmem.cc:9080为例，它仅仅包含一个路由，将请求发往集群outbound|9080|v1|details.default.svc.k8s.gmem.cc</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">Kiali</span></div>
<p>Kiali是当前Istio主推的前端工具，能够很容易的显示服务网格包含哪些微服务，这些微服务如何连接在一起。</p>
<p>Kiali能够显示Service Mesh拓扑结构，展示请求速率、断路器等Istio特性。它还能结合Jaeger，以提供调用链跟踪功能。</p>
<div class="blog_h1"><span class="graybg">客户端编程</span></div>
<p>你可以使用<a href="/client-go-code-snippet#dynamic-client">动态客户端</a>来CRUD各种Istio对象，Istio项目本身将K8S资源表示为IstioObject，后者需要转化为model.Config，并进一步获取Istio内部对象：</p>
<pre class="crayon-plain-tag">config, err := ConvertObject(schema, item, c.client.domainSuffix)
serviceEntry := config.Spec.(*networking.ServiceEntry)</pre>
<div class="blog_h2"><span class="graybg">knative</span></div>
<p>该项目提供了Istio的强类型客户端：</p>
<pre class="crayon-plain-tag">import istioclientset "github.com/knative/pkg/client/clientset/versioned"

// 客户端
type Clientset struct {
	*discovery.DiscoveryClient
	authenticationV1alpha1 *authenticationv1alpha1.AuthenticationV1alpha1Client
	networkingV1alpha3     *networkingv1alpha3.NetworkingV1alpha3Client
}
// networking.istio.io/v1alpha3组的接口
func (c *Clientset) NetworkingV1alpha3() networkingv1alpha3.NetworkingV1alpha3Interface {
	return c.networkingV1alpha3
}
type NetworkingV1alpha3Interface interface {
	RESTClient() rest.Interface
	DestinationRulesGetter
	GatewaysGetter
	VirtualServicesGetter
}
type VirtualServicesGetter interface {
	VirtualServices(namespace string) VirtualServiceInterface
}
// VirtualService CRD的接口
type VirtualServiceInterface interface {
	Create(*v1alpha3.VirtualService) (*v1alpha3.VirtualService, error)
	Update(*v1alpha3.VirtualService) (*v1alpha3.VirtualService, error)
	Delete(name string, options *v1.DeleteOptions) error
	DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error
	Get(name string, options v1.GetOptions) (*v1alpha3.VirtualService, error)
	List(opts v1.ListOptions) (*v1alpha3.VirtualServiceList, error)
	Watch(opts v1.ListOptions) (watch.Interface, error)
	Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha3.VirtualService, err error)
	VirtualServiceExpansion
}</pre>
<p>要使用此客户端，可以：</p>
<pre class="crayon-plain-tag">// 先获得*restclient.Config
cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig)
// 然后构造Istio客户端
istioClient, err := istioclientset.NewForConfig(cfg) </pre>
<div class="blog_h1"><span class="graybg">高级主题</span></div>
<div class="blog_h2"><span class="graybg">Istio如何劫持流量</span></div>
<ol>
<li>当创建Pod之后，Admission Controller自动为其注入两个容器：istio-init、istio-proxy</li>
<li>istio-init仅仅是一个脚本，它负责配置Pod网络命名空间的iptables。默认使用REDIRECT方式，将任何入站、出站流量重定向给istio-proxy中运行的Envoy所监听的15001端口，此端口由名为virtual的Envoy监听器监听</li>
<li>istio-proxy启动后，使用xDS协议向Pilot发起请求，以同步配置</li>
<li>不论是入站流量，还是出站流量，都会被劫持到15001这个Envoy监听器：
<ol>
<li>入站流量的流动轨迹：iptables  ⇨ Istio代理入站处理器（InboundHandler） ⇨ 应用容器</li>
<li>出站流量的流动轨迹：iptables  ⇨ Istio代理出站处理器（OutboundHandler） ⇨ 上游集群的特定端点</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">入站流量流动轨迹</span></div>
<p>以Bookinfo应用为例，当productpage访问reviews时，入站（到reivew的Pod）流量流动轨迹如下：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/09/inbound-istio-traffic-flow.png"><img class="aligncenter  wp-image-25191" src="https://blog.gmem.cc/wp-content/uploads/2018/09/inbound-istio-traffic-flow.png" alt="inbound-istio-traffic-flow" width="1011" height="1080" /></a></p>
<p>说明：</p>
<ol>
<li>Iptables的FILTER-PREROUTING链拦截到进入review pod的流量</li>
<li>流量被转发给ISTIO_INBOUND链</li>
<li>流量被转发给ISTIO_IN_REDIRECT链</li>
<li>流量被重定向给15001端口，即运行在Sidecar中Envoy的virtual监听器</li>
<li>virtual监听器use_original_dst=true，导致：
<ol>
<li>流量转发给虚拟监听器ReviewPod_9080</li>
<li>监听器ReviewPod_9080将流量路由给上游集群inbound|9080|reviews.default.svc.gmem.cc</li>
<li>此上游集群的唯一端点是127.0.0.1:9080，也就是应用程序容器</li>
</ol>
</li>
<li>发往127.0.0.1:9080的流量被iptables的OUTPUT链拦截，转发给ISTIO_OUTPUT链</li>
<li>ISTIO_OUTPUT将流量转发给ISTIO_REDIRECT链</li>
<li>ISTIO_REDIRECT链将流量达到应用程序容器的9080端口上</li>
</ol>
<div class="blog_h3"><span class="graybg">出站流量流动轨迹</span></div>
<p>以Bookinfo应用为例，当reviews访问ratings时，出站（到ratings的Pod）流量流动轨迹如下：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/09/outbound-istio-traffic-flow.png"><img class="aligncenter  wp-image-25189" src="https://blog.gmem.cc/wp-content/uploads/2018/09/outbound-istio-traffic-flow.png" alt="outbound-istio-traffic-flow" width="1011" height="1007" /></a></p>
<p>说明：</p>
<ol>
<li>Iptables的FILTER-OUTPUT链拦截到从review pod发往ratings集群的流量</li>
<li>流量被转发给ISTIO_OUTPUT链</li>
<li>流量被转发给ISTIO_REDIRECT链</li>
<li>流量被重定向给15001端口，即运行在Sidecar中Envoy的virtual监听器。virtual监听器的use_original_dst=true，导致：
<ol>
<li>流量转发给虚拟监听器0.0.0.0_9080</li>
<li>通过解析HTTP头，虚拟监听器将请求路由给上游集群outbound|9080|ratings.default.svc.gmem.cc</li>
<li>经过负载均衡策略判断，选中某个RatingsPod作为端点</li>
</ol>
</li>
<li>流量再次被转发给OUTPUT链，并依次经过ISTIO_OUTPUT ⇨ ISTIO_REDIRECT ⇨ POSTROUTING链出站，送达RatingsPod的监听端口</li>
</ol>
<div class="blog_h3"><span class="graybg"><a id="tproxy-iptables-rules"></a>Iptables解读-TPROXY</span></div>
<pre class="crayon-plain-tag">### MANGLE表  ###

# 如果使用透明代理模式

# 在MANGLE表创建新规则链ISTIO_DIVERT，注意此表优先级高于NAT
iptables -t mangle -N ISTIO_DIVERT
# 为封包打标记 1337
iptables -t mangle -A ISTIO_DIVERT -j MARK --set-mark 1337
# 允许封包通过
iptables -t mangle -A ISTIO_DIVERT -j ACCEPT

# 上面的封包路由给本地lo接口
ip -f inet rule add fwmark 1337 lookup 133
ip -f inet route add local default dev lo table 133

# 在MANGLE表创建新规则链ISTIO_TPROXY，位于ISTIO_DIVERT链后面
iptables -t mangle -N ISTIO_TPROXY

# 目的地址不是127.0.0.1的封包被重定向给15001的监听进程，但却不修改包头的任何信息，这和REDIRECT行为不同
# 同时设置标记1337，导致后续路由时从lo发出，给15001进程
iptables -t mangle -A ISTIO_TPROXY '!' -d 127.0.0.1/32 -p tcp -j TPROXY --tproxy-mark 1337/0xffffffff --on-port 15001

# 创建新规则链ISTIO_INBOUND，PREROUTING链直接跳转到此链
iptables -t mangle -N ISTIO_INBOUND
iptables -t mangle -A PREROUTING -p tcp -j ISTIO_INBOUND        ##### PREROUTING入口规则

# 处理负载服务端口
iptables -t mangle -A ISTIO_INBOUND -p tcp --dport 80 -m socket -j ISTIO_DIVERT
iptables -t mangle -A ISTIO_INBOUND -p tcp --dport 80 -j ISTIO_TPROXY


### NAT表  ###

iptables -t nat -N ISTIO_REDIRECT
# 任何TCP协议，都重定向到15001端口，也就是修改封包的目的地址为本机地址
iptables -t nat -A ISTIO_REDIRECT -p tcp -j REDIRECT --to-port 15001

# 类似的另一个规则链
iptables -t nat -N ISTIO_IN_REDIRECT
iptables -t nat -A ISTIO_IN_REDIRECT -p tcp -j REDIRECT --to-port 15001

# 在NAT表创建新规则链
iptables -t nat -N ISTIO_OUTPUT
iptables -t nat -A OUTPUT -p tcp -j ISTIO_OUTPUT

# 如果出口网卡是lo，但是目的地址非本地地址，重定向
iptables -t nat -A ISTIO_OUTPUT -o lo '!' -d 127.0.0.1/32 -j ISTIO_REDIRECT

# 如果封包由1337用户/组产生，则退出ISTIO_OUTPUT链，返回OUTPUT
iptables -t nat -A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN
iptables -t nat -A ISTIO_OUTPUT -m owner --gid-owner 1337 -j RETURN
# 否则，执行重定向
iptables -t nat -A ISTIO_OUTPUT -j ISTIO_REDIRECT</pre>
<p>大概分析一下包处理流程：</p>
<ol>
<li>外部客户端请求PodIP:80端口</li>
<li>第一次进入Iptabels</li>
<li>进入PREROUTING链
<ol>
<li>进入mangle表
<ol>
<li>跳转到ISTIO_INBOUND链</li>
<li>跳转到ISTIO_TPROXY链</li>
<li>由于目的地址不是127.0.0.1/32，因此封包被打上标记1337，并标记为由15001的监听者处理</li>
<li>封包通过mangle表，进而通过Iptables</li>
</ol>
</li>
</ol>
</li>
<li>PREROUTING之后需要进行选路处理，由于它包含1337标记，因此路由目标是lo网卡</li>
<li>由于之前已经标记由15001处理，因此Envoy进程获得封包的处理权</li>
<li>Envoy进行内部处理后转发给匹配其元素目的地址的虚拟监听器，也就是PodIP:80</li>
<li>此监听器向127.0.0.1:80发起请求，这是另一个TCP连接，进入Iptables，OUTPUT链
<ol>
<li>如果目的地址不是127.0.0.1则会强制重定向到15001 —— Envoy总是以127.0.0.1访问负载的服务，因此不会重定向</li>
<li>如果发起请求的用户不是1337/1337，强制重定向到15001 —— Envoy总是以1337身份运行，因此不会重定向</li>
</ol>
</li>
<li>请求被负载接收并处理</li>
</ol>
<p>ISTIO_DIVERT链相关的规则并非必须（可以快速确定一个套接字是否应该本地处理，不需要遍历一系列iptables规则），删除Iptables的相应条目不影响Enovoy的工作。</p>
<div class="blog_h2"><span class="graybg">Headless服务问题</span></div>
<p>我们可以通过<a href="#proxy-config-listener">istio proxy-config listener</a>来Dump出任何代理的监听器列表，并且已经发现以下规律：</p>
<ol>
<li>对于每个K8S服务的IP:PORT组合
<ol>
<li>如果服务是TCP的，则创建IP:PORT监听器</li>
<li>如果服务是HTTP的，则创建0.0.0.0:PORT监听器，如果多个HTTP服务使用PORT，则贡献之</li>
</ol>
</li>
<li>对于本负载暴露的任何服务，创建IP:PORT监听器</li>
</ol>
<p>上述行为会导致一些问题：</p>
<ol>
<li>网格中任意一个HTTP服务占用了PORT，则TCP服务无法再使用此PORT</li>
</ol>
<p>如果使用了Headless服务，额外的问题会出现。假设部署了三节点的ZooKeeper集群：</p>
<pre class="crayon-plain-tag">zk            ClusterIP   10.111.213.56   &lt;none&gt;        2181/TCP                     83m
zk-headless   ClusterIP   None            &lt;none&gt;        2181/TCP,3888/TCP,2888/TCP   83m</pre>
<p>则在每个节点的代理上，都会创建如下监听器：</p>
<pre class="crayon-plain-tag"># 这三个是入站监听器，转发给inbound|3888|election|zk-headless.default.svc.k8s.gmem.cc
# 由于Headless服务引发
172.27.252.180     2181      TCP
172.27.252.180     3888      TCP
172.27.252.180     2888      TCP

# 这三个是出站监听器，转发给outbound|3888||zk-headless.default.svc.k8s.gmem.cc
# 由于Headless服务引发
0.0.0.0            2888      TCP
0.0.0.0            3888      TCP
0.0.0.0            2181      TCP

# 这是出站监听器，由于zk服务导致
10.111.213.56 2181 TCP</pre>
<p>可以看到，由于Headless服务没有IP地址，Istio直接在0.0.0.0上监听，代理ZooKeeper节点的集群请求。这就导致了第二个问题：</p>
<ol>
<li>网格中任何两个Headless服务，不能使用相同的端口</li>
<li>Headless服务的端口不能和集群中某个TCP服务的端口冲突</li>
<li>Headless服务的端口也不能和集群中某个HTTP服务的端口冲突</li>
</ol>
<p>相关Issue：<a href="https://github.com/istio/istio/issues/9784">https://github.com/istio/istio/issues/9784</a></p>
<p>此问题目前的进展：</p>
<ol>
<li>现在具有命名空间隔离机制，不同命名空间可以使用同一端口</li>
<li>现在具有细粒度控制机制，只要一个服务不同时依赖于2-N个使用相同端口的Headless服务，也可以规避端口冲突问题</li>
<li>HTTP服务和Headless TCP服务端口冲突问题，从1.3开始，可以通过协议嗅探特性解决</li>
<li><a href="https://github.com/istio/istio/pull/16845">有个PR</a>解决Headless服务在0.0.0.0:PORT监听的问题，它为Headless服务的每个Endpoint创建一个PodIP:Port监听器，这可以解决99%的场景，除非你的集群非常大，Headless服务下Pod总数很多。使用此特性，需要为Pilot设置环境变量<pre class="crayon-plain-tag">PILOT_ENABLE_HEADLESS_SERVICE_POD_LISTENERS=true</pre>。我在1.5.1版本测试，发现这个环境变量没有效果，监测Headless服务的OUTBOUND监听器，发现上游集群类型设置为ORIGINAL_DST，也就是简单的透传，这样貌似也就不会有问题？</li>
</ol>
<div class="blog_h2"><span class="graybg">真实源IP问题</span></div>
<p>接上个问题，对于ZooKeeper这样的中间件，其节点集之间本身的交互就无法在Istio下进行。</p>
<p>我们看一下ZooKeeper节点的Envoy集群配置：</p>
<pre class="crayon-plain-tag"># istioctl proxy-config cluster zk-0 --fqdn zk-headless.default.svc.k8s.gmem.cc
SERVICE FQDN                            PORT     SUBSET       DIRECTION     TYPE
# 和其他ZK节点进行通信时，会使用原始目的地址
zk-headless.default.svc.k8s.gmem.cc     2181     -            outbound      ORIGINAL_DST
zk-headless.default.svc.k8s.gmem.cc     2888     -            outbound      ORIGINAL_DST
zk-headless.default.svc.k8s.gmem.cc     3888     -            outbound      ORIGINAL_DST

zk-headless.default.svc.k8s.gmem.cc     2181     client       inbound       STATIC
zk-headless.default.svc.k8s.gmem.cc     3888     election     inbound       STATIC
zk-headless.default.svc.k8s.gmem.cc     2888     server       inbound       STATIC</pre>
<p>可以看到zk-0节点向zk-1节点发送的通信请求，是可以正确到达的，原因是ORIGINAL_DST。</p>
<p>但是，在zk-1节点那边，zk-0的请求被拦截，其源IP地址变为127.0.0.1：</p>
<pre class="crayon-plain-tag"># netstat -nt | grep 2181
tcp        0      0 127.0.0.1:48020         127.0.0.1:2181          TIME_WAIT  
tcp        0      0 127.0.0.1:48124         127.0.0.1:2181          TIME_WAIT  
tcp        0      0 127.0.0.1:47984         127.0.0.1:2181          TIME_WAIT  
tcp        0      0 127.0.0.1:48144         127.0.0.1:2181          TIME_WAIT  
tcp        0      0 127.0.0.1:48038         127.0.0.1:2181          TIME_WAIT  
tcp        0      0 127.0.0.1:48108         127.0.0.1:2181          TIME_WAIT  
tcp        0      0 127.0.0.1:48000         127.0.0.1:2181          TIME_WAIT  
tcp        0      0 127.0.0.1:48088         127.0.0.1:2181          TIME_WAIT  
tcp        0      0 127.0.0.1:47946         127.0.0.1:2181          TIME_WAIT  
tcp        0      0 127.0.0.1:48162         127.0.0.1:2181          TIME_WAIT  
tcp        0      0 127.0.0.1:48070         127.0.0.1:2181          TIME_WAIT  </pre>
<p>这无法满足ZooKeeper的要求，它要求集群节点必须是直接进行通信的，也就是说，要能够得到正确的源IP地址。在Istio代理存在的情况下，显然无法满足。</p>
<p>很多场景下都需要知道源的真实IP地址，例如：</p>
<ol>
<li>源IP地址作为身份信息的一部分</li>
<li>源IP地址和网络策略有关</li>
<li>需要对源IP地址进行审计</li>
</ol>
<p>但凡这些场景，都会面临问题。</p>
<div class="blog_h3"><span class="graybg">禁用或穿透</span></div>
<p>解决ZooKeeper不能在网格中运行的问题，可以使用Istio的注解：</p>
<pre class="crayon-plain-tag"># 让入站的这些端口不走Envoy代理
traffic.sidecar.istio.io/excludeInboundPorts: 2888,3888
traffic.sidecar.istio.io/includeInboundPorts: '*'

# 或者，直接禁止ZooKeeper的自动注入
sidecar.istio.io/inject: "false"</pre>
<div class="blog_h3"><span class="graybg">透明代理</span></div>
<p>透明代理是解决ZooKeeper问题的一个可能方案，不幸的是，Istio的TPROXY模式行为并不符合期望：</p>
<ol>
<li>由于FeatureGate RunAsGroup没有开启，导致istio-proxy容器运行在root用户下，这与istio-init的Iptable规则冲突：<br />
<pre class="crayon-plain-tag">iptables -t nat -A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN
iptables -t nat -A ISTIO_OUTPUT -m owner --gid-owner 1337 -j RETURN</pre></p>
<p>其后果就是istio-proxy无限重定向，根本没法启动起来</p>
</li>
<li>
<p>即使开启此RunAsGroup或者修改istio-sidecar-injector的Configmap将istio-proxy用户改为root，仍然不行，负载获得的源IP地址仍然是127.0.0.1，仅仅是Envoy看到了真实源IP，也就是说<span style="background-color: #c0c0c0;">Istio的TPROXY模式只是解决了Envoy看不到真实源IP的问题</span></p>
</li>
</ol>
<p>第2个问题和Envoy相关，默认情况下，Envoy不会将下游真实地址暴露给工作负载，因此后者看到的是127.0.0.1。</p>
<p>Envoy提供了获取真实源IP地址的多种方法：</p>
<ol>
<li>对于L7：可以考虑时用HTTP头传递源IP地址，例如x-forwarded-for</li>
<li>代理协议：HAProxy的代理协议可以提供源IP地址的元数据信息，通过过滤器envoy.listener.proxy_protocol，Envoy可以消费元数据，但是如何把源IP发给上游还是个问题</li>
<li>使用监听器过滤器envoy.listener.original_src，此过滤器能够复制下游连接的源IP地址，并将其作为上游的连接的源IP地址</li>
</ol>
<p>original_src出现的比较晚，<a href="https://github.com/istio/istio/issues/5679">到2019年底Istio仍然没有考虑支持该监听器过滤器</a>。</p>
<p>目前（2020），透明代理问题已经可以解决，具体方案如下：</p>
<ol>
<li>Istio开启TPROXY拦截模式</li>
<li>使用EnvoyFilter，注入监听器过滤器original_src，参考<a href="#original_src">EnvoyFilter</a></li>
<li>添加必要的路由规则、Iptables规则，参考<a href="/envoy-study-note#original_src">original_src</a></li>
</ol>
<div class="blog_h2"><span class="graybg">性能问题</span></div>
<p>服务网格中，调用链路中每多一跳，就会引入两个Sidecar的逻辑，让Sidecar性能优化到极致非常必要。可以从如何拦截流量，如何在Sidecar之间进行通信这两个角度进行性能优化：</p>
<div class="blog_h3"><span class="graybg">流量拦截</span></div>
<p>为了将流量引入到Sidecar，Istio使用了iptables规则，所有流量需要经过内核态 - 用户态 -内核态的转移，<span style="background-color: #c0c0c0;">数据被反复拷贝、大量的中断、上下文切换</span>，都影响了性能。</p>
<p>这属于iptables的固有缺陷，解决思路有两个：</p>
<ol>
<li>bpfilter，基于BPF的网络过滤内核模块，用于替换netfilter作为iptables的底层实现。对内核版本有需求</li>
<li>使用自定义网络套接字，支持VPP、Cilium，以便完全在用户态或内核态处理封包</li>
</ol>
<p>DPDK（Intel Data Plane Development Kit）允许在用户态高效处理网络包，它提供了必要库函数和驱动支持。VPP（Vector Packet Processor）是基于DPDK的网络包处理框架。</p>
<p>eBPF允许用户提供指令来改变内核处理数据的行为。XDP（eXpress Data Path）为Linux内核提供了高性能、可编程的网络数据路径。由于网络包在还未进入网络协议栈之前就处理，它给Linux网络带来了巨大的性能提升（性能比DPDK还要高）。</p>
<p>Fackbook的katran就是基于eBPF+XDP，它是一个网络负载均衡器，据说性能比IPVS高10倍。</p>
<div class="blog_h3"><span class="graybg">通信协议</span></div>
<p>UDP容易丢包，TCP慢。QUIC结合了两者的优点——既保证了TCP的可靠与安全性，又兼具UDP的速度与效率，QUIC还内置TLS支持、一部分HTTP/2的支持。</p>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">proxy-init</span></div>
<div class="blog_h3"><span class="graybg">无法操控iptables</span></div>
<p>报错信息：iptables v1.6.0: can't initialize iptables table `nat': Permission denied (you must be root)</p>
<p>报错原因：如果Pod使用了securityContext，会出现该情况。</p>
<p>解决方案：</p>
<ol>
<li>不使用securityContext，或者将运行用户改为0</li>
<li>或者，修改Configmap istio-sidecar-injector，将istio-init容器的运行用户设置为0：<br />
<pre class="crayon-plain-tag">apiVersion: v1
data:
  config: |-
    policy: enabled
    template: |-
      initContainers:
      - name: istio-init
        securityContext:
          runAsUser: 0
          capabilities:
            add:
            - NET_ADMIN
          privileged: true
        restartPolicy: Always </pre>
</li>
</ol>
<div class="blog_h2"><span class="graybg">TPROXY相关</span></div>
<div class="blog_h3"><span class="graybg">TPROXY模式下无法连接pilot</span></div>
<p>Sidecar报错信息：Envoy proxy is NOT ready: config not received from Pilot (is Pilot running?)</p>
<p>原因：Kubernetes 1.13默认没有启用特性RunAsGroup，而Istio假设这一特性是开启的，这属于兼容性问题。<a href="https://github.com/istio/istio/issues/21812">已经提Issue到Istio</a>。</p>
<p>解决方法一：修改istio-sidecar-injector，将<pre class="crayon-plain-tag">-u</pre>改为<pre class="crayon-plain-tag">0 </pre>即可：</p>
<pre class="crayon-plain-tag">command:
  - istio-iptables
  - "-p"
  - "15001"
  - "-z"
  - "15006"
  - "-u"
  - 1337  # 改为0</pre>
<p>解决方法二：为API Server开启特性<pre class="crayon-plain-tag">--feature-gates=RunAsGroup=true</pre>。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/istio-study-note">Istio学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/istio-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
