下一代 Service Mesh -- istio 架构分析

3,865 阅读5分钟

前面的分享中,我们讲到,出于性能和稳定的考虑,我们没有采用以 istio 为代表的第二代 service mesh技术,而是直接使用了 Envoy 搭配自己的 xDS 服务。

然而我们还是有必要去了解下 Istio,毕竟这代表了 Service Mesh 的未来。不出意外,在不远的将来,扇贝也会迁移到第二代 Service Mesh 的框架上去。

本文就针对 Istio 的架构做个简单的分析,会涉及部分源码的分析。

1. Istio 的架构

我们在介绍Envoy的时候提到,Envoy的动态配置给我们提供了一种可能:我们可以按照Envoy的规范,通过实现提供特定API的服务,来控制Envoy的路由,流量规则, ratelimit,日志等等。

Istio 的理念里,Envoy 这种真正执行流量转发和控制的 Proxy,叫做 数据面板。而那些通过提供 API 控制 Envoy 行为的服务叫做 控制面板

现在借用官网的一幅图来解释下 Istio 的架构:

数据面板以 sidecar 的形式和微服务部署在一起,每个微服务实例通过各自的 sidecar 来实现发送和接受请求;微服务和微服务之间不直接通信,而是通过 sidecar 的代理(转发)来实现通信。sidecar 直接形成调用网络,就像一个“网格”一样。

控制面板由 Pilot, Mixer, Istio-Auth 组成。我们以 kubernetes 上部署以 Envoy 为数据面板的 Istio 为例来介绍。

Pilot 是控制面板的核心,是必不可少的。将 kubernetes 的资源信息翻译成 Envoy 需要的相关 xDS API(CDS, SDS/EDS, RDS),实现服务发现,包括用户定义的 Istio 的相关配置,翻译成 Envoy 所能理解的路由规则(RDS)。

Mixer 实现数据的收集,以及一些额外的流量控制。首先,数据面板会向 Mixer 汇报请求数据,这些请求数据都是按照 Istio 的规范结构化的。Mixer可以对这些汇报上来的数据做各种处理,例如打印日志,加工成 Prometheus 需要的 metrics 方便抓取从而进行性能监控,做 rate limit 等等。Mixer 是插件式的,这些数据处理都是通过配置一个个插件来实现的。

2. Pilot

以 Kubernetes 环境为例:Pilot 的每个 Pod 实际上包含两个 "容器":discoveryistio-proxy

2.1 discovery

discovery 对应的 image 是 docker.io/istio/pilot:0.4.0,是 Pilot 真正的功能提供者,其监听的地址是:tcp://127.0.0.1:8080,主要工作是将 Kubernetes的资源通过 xDS 服务的形式翻译成 Envoy 所能理解的配置。

其中:

xDS 的核心代码: pilot/proxy/envoy/discovery.go

// Struct,核心数据结构
type DiscoveryService struct {
	proxy.Environment
	server *http.Server

	sdsCache *discoveryCache
	cdsCache *discoveryCache
	rdsCache *discoveryCache
	ldsCache *discoveryCache
}

// Register adds routes a web service container
func (ds *DiscoveryService) Register(container *restful.Container) {
	ws := &restful.WebService{}
	ws.Produces(restful.MIME_JSON)

	// 例如: List all known services (informational, not invoked by Envoy)
	ws.Route(ws.
		GET("/v1/registration").
		To(ds.ListAllEndpoints).
        Doc("Services in SDS"))

    // 其他 xDS ...
}

翻译 Kubernetes 资源的核心代码: pilot/platform/kube/controller.go

// 例如: list services
func (c *Controller) Services() ([]*model.Service, error) {
	list := c.services.informer.GetStore().List()
	out := make([]*model.Service, 0, len(list))

	for _, item := range list {
		if svc := convertService(*item.(*v1.Service), c.domainSuffix); svc != nil {
			out = append(out, svc)
		}
	}
	return out, nil
}

2.2 istio-proxy

istio-proxy 对应的 image 是 docker.io/istio/proxy:0.4.0,是 Pilot 服务的 sidecar,负责反向代理发往 discovery 的请求,监听 tcp://0.0.0.0:15003。其 Envoy 的核心配置如下:

{
  "listeners": [
   {
    "address": "tcp://0.0.0.0:15003",
    "name": "tcp_0.0.0.0_15003",
    "filters": [
     {
      "type": "read",
      "name": "tcp_proxy",
      "config": {
       "stat_prefix": "tcp",
       "route_config": {
        "routes": [
         {
          "cluster": "in.8080"
         }
        ]
       }
      }
     }
    ],
    "bind_to_port": true
   }
  ],
  "admin": {
    "access_log_path": "/dev/stdout",
    "address": "tcp://127.0.0.1:15000"
  },
  "cluster_manager": {
    "clusters": [
      {
        "name": "in.8080",
        "connect_timeout_ms": 1000,
        "type": "static",
        "lb_type": "round_robin",
        "hosts": [
          {
            "url": "tcp://127.0.0.1:8080"
          }
        ]
      }
    ]
  }
}

3. Sidecar

以 Kubernetes 环境为例:作为数据面板的 Sidecar,其实会在微服务的每个 Pod中,插入两个容器: proxy-initistio-proxy

3.1 istio-proxy

istio-proxy 对应的 image 是 docker.io/istio/proxy:0.4.0,是 Sidecar 的实际功能承担者,监听 tcp://0.0.0.0:15003,接受所有发往该 Pod 的tcp流量,分发所有从 Pod 中发出的 tcp 流量。实际上,这个 proxy 由两部分组成:一个管理进程 agent 和 真正的代理进程 Envoy

agent 负责生成 Envoy 的配置,并适当监控 Envoy 的运行状况,必要的时候会进行 Envoy 进程的管理(例如配置变更后 reload Envoy)。另外也负责与 Mixer 组件交互(包括汇报数据等)。

agent 关于生成 Envoy 配置的核心代码位于:pilot/proxy/envoy/config.go

func buildConfig(config meshconfig.ProxyConfig, pilotSAN []string) *Config {
	listeners := Listeners{}

	clusterRDS := buildCluster(config.DiscoveryAddress, RDSName, config.ConnectTimeout)
	clusterLDS := buildCluster(config.DiscoveryAddress, LDSName, config.ConnectTimeout)
	clusters := Clusters{clusterRDS, clusterLDS}

	out := &Config{
		Listeners: listeners,
		LDS: &LDSCluster{
			Cluster:        LDSName,
			RefreshDelayMs: protoDurationToMS(config.DiscoveryRefreshDelay),
		},
		Admin: Admin{
			AccessLogPath: DefaultAccessLog,
			Address:       fmt.Sprintf("tcp://%s:%d", LocalhostAddress, config.ProxyAdminPort),
		},
		ClusterManager: ClusterManager{
			Clusters: clusters,
			SDS: &DiscoveryCluster{
				Cluster:        buildCluster(config.DiscoveryAddress, SDSName, config.ConnectTimeout),
				RefreshDelayMs: protoDurationToMS(config.DiscoveryRefreshDelay),
			},
			CDS: &DiscoveryCluster{
				Cluster:        buildCluster(config.DiscoveryAddress, CDSName, config.ConnectTimeout),
				RefreshDelayMs: protoDurationToMS(config.DiscoveryRefreshDelay),
			},
		},
		StatsdUDPIPAddress: config.StatsdUdpAddress,
    }
    // 其他相关逻辑  ...
}

值得注意的是,istio-proxy 可以以多种“角色”运行,会根据不同的角色,生成不同的配置。例如 2.2 节中,作为 Pilot 的 proxy,其配置就和 Sidecar 是不一样的。

3.2 proxy-init

proxy-init 对应的 image 是 docker.io/istio/proxy_init:0.4.0。在 3.1 节 中,我们讲到:istio-proxy会接受所有发往该 Pod 的tcp流量,分发所有从 Pod 中发出的 tcp 流量,而我们实际写代码的时候却完全不用考虑这些,那Istio是怎么做到的呢?答案就是通过proxy-init! 具体的做法是:通过注入 iptables 的改写流入流出 Pod 的流量的规则,使得流入流出 Pod 的流量重定向到 istio-proxy的不同监听端口。

例如关于流入流量的重定向规则:

iptables -t nat -N ISTIO_REDIRECT                                             -m comment --comment "istio/redirect-common-chain"
iptables -t nat -A ISTIO_REDIRECT -p tcp -j REDIRECT --to-port ${ENVOY_PORT}  -m comment --comment "istio/redirect-to-envoy-port"
iptables -t nat -A PREROUTING -j ISTIO_REDIRECT                               -m comment --comment "istio/install-istio-prerouting"

4. 小结

Istio 作为下一代的 Service Mesh 框架,虽然现在还不能用于生产,但是其思想和架构是很值得我们去学习的。希望本文对于对 Istio 感兴趣的同学有所帮助。