服务抽象模型

介绍

istio中,定义了一套服务注册的抽象模型,具体在文件`istio/pilot/pkg/model/service.go`中定义。

说明如下:

文件描述Istio中服务的抽象模型(以及它们的实例)。这个模型独立于底层平台 (Kubernetes, Mesos, 等.). 平台特定适配器用在平台中找到的元数据填充模型对象的各个字段。

平台无关的proxy代码使用这个模型的表示来为7层代理sidecar生成配置文件。proxy代码是各个代理实现特有的。

结合Pilot的架构图,就很容易理解了:

这里所说的服务抽象模型,就是图中的”Abstract Model”。

Service定义

Service描述Istio服务(如,catalog.mystore.com:8080)。每个服务有全限定域名(fully qualified domain name/FQDN)和一个或者多个端口。端口是服务用来监听连接的。可选的,服务可以关连有单个负载均衡器(load balancer)/虚拟IP(virtual IP),这样FQDN的DNS查询解析到虚拟IP地址(负载均衡器IP)。例如,在kubernetes中,服务foo和主机名foo.default.svc.cluster.local关连,有一个虚拟IP 10.0.1.1,并且在端口80,8080上监听。

type Service struct {
    // 服务的Hostname, 例如"catalog.mystore.com"
	Hostname string `json:"hostname"`

	// Address 指定服务的负载均衡器的IPv4地址
	Address string `json:"address,omitempty"`
    
    // ClusterVIPs 指定服务所在的每个集群的负载均衡器的服务地址
	ClusterVIPs map[string]string `json:"cluster-vips,omitempty"`

	// Ports 是服务监听连接所在的网络端口集
	Ports PortList `json:"ports,omitempty"`

	// ServiceAccounts 指定运行服务的服务账号(service account).
	ServiceAccounts []string `json:"serviceaccounts,omitempty"`

	// MeshExternal (如果为true) 表明这个服务是在mesh之外。
	// 这些服务通过 Istio 的 ExternalService 规范来定义.
	MeshExternal bool

	// Resolution 指出在路由流量前服务实例需要如何被解析。
	// 服务注册中的大多数服务将使用静态负载均衡,proxy将决定接收流量的服务实例。
	// 外部服务可以使用DNS负载均衡 (例如 proxy 将查询DNS服务器来获取服务的IP地址),
	// 也可以使用透传模式(passthrough model) (例如 proxy 将转发流量到调用者要求的网络终端)
	Resolution Resolution

	// CreationTime 记录服务创建的时间,如果可用
	CreationTime time.Time `json:"creationTime,omitempty"`

	// Attributes 包含和服务相关的额外属性,主要被mixer和RBAC用于策略加强。
	Attributes ServiceAttributes
}

Service字段定义

PortList和Port

// PortList 是一组port
type PortList []*Port

// Port 代表网络端口,服务在这里监听连接。
// 端口和使用这个端口的协议类型相关联。
type Port struct {
	// Name 是端口对象的可读名称。
	// 当服务有多个端口时,name字段是必须的
	Name string `json:"name,omitempty"`

	// Port 是可以访问到服务可以的端口号.
	// 不是非要映射到服务后面的实例的对应端口
	// 见下面的 NetworkEndpoint 定义.
	Port int `json:"port"`

	// Protocol 是用于这个端口的协议.
	Protocol Protocol `json:"protocol,omitempty"`
}

Protocol

// Protocol 定义用于端口的网络协议
type Protocol string

const (
	ProtocolGRPC Protocol = "GRPC"
    ProtocolGRPCWeb Protocol = "GRPC-Web"
    // HTTP/1.1 traffic,注意HTTP/1.0 或者更早版本不被支持
	ProtocolHTTP Protocol = "HTTP"
	ProtocolHTTPS Protocol = "HTTPS"
	ProtocolHTTP2 Protocol = "HTTP2"
	// TCP,这是默认协议
	ProtocolTCP Protocol = "TCP"
    ProtocolTLS Protocol = "TLS"
	// UDP.注意当前还不支持
	ProtocolUDP Protocol = "UDP"
	ProtocolMongo Protocol = "Mongo"
	ProtocolRedis Protocol = "Redis"
    ProtocolMySQL Protocol = "MySQL"
	ProtocolUnsupported Protocol = "UnsupportedProtocol"
)

Resolution

// Resolution 指出在路由流量前服务实例需要如何被解析。
type Resolution int

const (
	// ClientSideLB  意味着proxy将从它的本地负载均衡池中选择终端。
	ClientSideLB Resolution = iota
	// DNSLB 意味着proxy将解析DNS地址并转发到解析后的地址
	DNSLB
	// Passthrough 意味着proxy将转发流量到调用者要求的目标IP
	Passthrough
)

ServiceAttributes

ServiceAttributes表示服务自定义属性组:

type ServiceAttributes struct {
	// Name 是 "destination.service.name" 属性
	Name string
	// Namespace 是 "destination.service.namespace" 属性
	Namespace string
	// UID 是 "destination.service.uid" 属性
	UID string
	// ConfigScope 定义当namespace被导入时,namespace中的服务的可见性
	ConfigScope networking.ConfigScope
}

ConfigScope 定义当 namespace 被导入时,在 namespace 中 Istio 配置实体的可见性。默认所有配置实体都是 public 。当包含 private 配置的 namespace 被导入到 Sidecar时,private 的配置将不会导入。

type ConfigScope int32

const (
	// 有这个范围的Config对于mesh中所有的工作负载都可见。
	ConfigScope_PUBLIC ConfigScope = 0
	// 有这个范围的Config 只对和配置资源在同一个namespace下的工作负载可见
	ConfigScope_PRIVATE ConfigScope = 1
)

Service模型的Json表述

Service模型的Json表述如下:

{
    hostname: "example-service1.default",
    address: "10.0.1.105",
    clusterVIPs: {
        "vip1": "172.100.1.10", 
        "vip2": "172.100.2.11"
    },
    ports: {
        [{
            name: "http",
            port: 80,
            protocol: "HTTP"
        },{
            name: "grpc",
            port: 8080,
            protocol: "GRPC"
        }]
    },
    serviceaccounts: ["accout1", "account2"],
    meshExternal: false,
    resolution: 0,
    creationTime: ,
    attributes: {
    	name: "example-service1",
    	namespace: "default",
    	uid: "1234",
    	configScope: 0
	}
}

ServiceInstance定义

ServiceInstance 表示服务特定版本的单个实例。

它绑定网络端点(ip:port), 服务描述(适用于不同版本)和标签集,标签集描述和这个实例关联的服务版本。

因为 ServiceInstance 有单个的NetworkEndpoint, 而NetworkEndpoint有单个端口, 所有要表示在多个端口上监听的工作负载需要多个ServiceInstances。

和服务实例关联的label在每个网络端口中是唯一的。每个服务实例网络端点有一套定义良好的标签集合。

例如, 和catalog.mystore.com关联的服务实例集合建模如下:

  • NetworkEndpoint(172.16.0.1:8888), Service(catalog.myservice.com), Labels(foo=bar)
  • NtworkEndpoint(172.16.0.2:8888), Service(catalog.myservice.com), Labels(foo=bar)
  • NetworkEndpoint(172.16.0.3:8888), Service(catalog.myservice.com), Labels(kitty=cat)
  • NetworkEndpoint(172.16.0.4:8888), Service(catalog.myservice.com), Labels(kitty=cat)
type ServiceInstance struct {
	Endpoint         NetworkEndpoint `json:"endpoint,omitempty"`
	Service          *Service        `json:"service,omitempty"`
	Labels           Labels          `json:"labels,omitempty"`
	ServiceAccount   string          `json:"serviceaccount,omitempty"`
}

ServiceInstance字段定义

NetworkEndpoint

NetworkEndpoint 定义和服务的实例关联的网络地址(IP:port)。服务有一个或者多个实例,每个实例运行于容器/虚拟机/pod。如果一个服务有多个端口,那么同一个实例IP会期待于在多个端口上监听(每个服务端口一个)

注意和实例关联的端口不是一定要和服务关联的端口一样。取决于网络搭建(NAT, overlays), 可以不同。例如, 如果 catalog.mystore.com 可以通过端口80和8080访问, 并且它影射到IP为 172.16.0.1 的实例, 然后端口80的连接将转发到端口55446,而端口8080的连接将转发到端口33333。

那么在内部,我们就有 catalog.mystore.com 服务的两个端点结构:

  • 172.16.0.1:54546 (带有指向80的ServicePort)
  • 172.16.0.1:33333 (带有指向8080的ServicePort)
type NetworkEndpoint struct {
    // Family 指示端点的类型, 如TCP 或者 Unix Domain Socket
	Family AddressFamily
    
	// 网络端点的地址,通常是IPv4地址,如果Family是`AddressFamilyTCP`;如果Family是`AddressFamilyUnix`,则是到domain socket的地址
	Address string

	// 这个实例监听连接的端口号。
	// 可以和服务访问的端口不一样。
	// 例如, catalog.mystore.com:8080 -> 172.16.0.1:55446
    // `AddressFamilyUnix`忽略这个字段。
	Port int

	// 在服务声明中声明的端口。
	// 这是和当前实例关联的服务(例如, catalog.mystore.com)的端口
	ServicePort *Port
    
	// 定义平台特有工作负载实例标识符(可选)
	UID string

	// 端点所在的网络
	Network string

    // 端点所在的局域(locality)
	Locality string

	// 和这个端点关联的负载均衡权重
	LbWeight uint32
}

AddressFamily

// AddressFamily 表示用于访问 NetworkEndpoint 的传输类型
type AddressFamily int

const (
	// AddressFamilyTCP 表示连接到TCP端点的地址。它由IP地址或者主机名和端口组成。
	AddressFamilyTCP AddressFamily = iota
	// AddressFamilyUnix 表示连接到Unix Domain Socket的地址。由socket 文件地址组成。
	AddressFamilyUnix
)

Labels

Labels 是任意字符串的非空集合(set)。

服务的每个版本可以通过和这个版本关联的标签的独特集合来区分。 这些标签被分配到特定服务版本的所有实例。例如, 假定 catalog.mystore.com 有两个版本 v1 和 v2:

  • v1 实例有标签 gitCommit=aeiou234, region=us-east
  • 而 v2 实例有标签 name=kittyCat,region=us-east
type Labels map[string]string

服务实例的Json表示:

{
	endpoint: {
		family: 0,
        address: "172.16.0.1",
        port: 54546,
        servicePort: {
            name: "http",
            port: 80,
            protocol: "HTTP",
        },
        uid:"",
        network:"",
        locality:""
        lwWeight: 10
	},
    service: {
        hostname: "example-service1.default",
        address: "10.0.1.105",
        clusterVIPs: {
            "vip1": "172.100.1.10", 
            "vip2": "172.100.2.11"
        },
        ports: {
            [{
                name: "http",
                port: 80,
                protocol: "HTTP"
            },{
                name: "grpc",
                port: 8080,
                protocol: "GRPC"
            }]
        },
        serviceaccounts: ["accout1", "account2"],
        meshExternal: false,
        resolution: 0,
        creationTime: ,
        attributes: {
            name: "example-service1",
            namespace: "default",
            uid: "1234",
            configScope: 0
        }
    },
    labels: {
		gitCommit: "aeiou234", 
		region: "us-east"
    },
    serviceaccount: "account1"
}

服务模型总结

Istio定义了服务的抽象模型,包括 Service 和 ServiceInstance,和通常的服务注册发现系统定义的服务模型相比,不同的地方主要有:

服务标识

在Istio的模型中,服务的标识符是用 hostname,比如”catalog.mystore.com”, 还有FQDN的支持要求。一般的服务注册机制是用一个简单的服务名如”UserService”来标识单个服务,可能会有service namespace或者service group用来避免重名。

虽然本质上没有大的差别,都是通过一个唯一标识来识别服务。

Istio的模型中对服务的标识是非常明确的:用 hostname 来标识,而且是FQDN。这样一方面符合 kubernetes 的习惯,另一方面也为DNS的使用留下了非常好的基础。

在Istio中定义 VirtualService 和 DestinationRule 时,我们会发现,对于这些规则要生效的服务,Istio 同样是通过 hostname 来标识的。

ClusterVIP

在Istio的模型中,有 clusterVIPs 字段,指定服务在不同的集群中负载均衡器的地址。

TBD: 具体使用方式待细细研究。

服务端口

Istio的服务模型中,端口是遵循 12 factor 中的 Port Binding 原则,非常明确的将端口和协议绑定,每个服务对外提供访问的端口都是和具体的通讯协议如HTTP绑定。

然后,每个 serviceInstance 可以有自己的特别的 port,容许和 ServicePort 不同,当然会有关联关系。

但是,对于客户端使用者,访问应该都是通过 ServicePort 来进行。

ServiceAccount

Istio的服务模型中,提供了service account信息,这位基于service account进行安全控制提供了基础。

MeshExternal

MeshExternal 属性表示服务在mesh之外,也就是在当前Istio的服务注册机制之外,这为访问外部服务提供了一个可用的方式,也为打通不同的体系留下了伏笔。

ConfigScope

可以通过ConfigScope属性来定义服务在namespace外的可见性,当设置为 private 时就只有当前 namespace 可见,这样可以实现让部分设计为 namespace 私有的服务不被外面访问。

也有一些不太容易适应的设计,比如:

ServiceInstance和ServicePort的一对一绑定

因为 ServiceInstance 只有单个的NetworkEndpoint, 而NetworkEndpoint 只有单个端口, 所有要表示在多个端口上监听的工作负载需要多个ServiceInstances。

也即是说,如果一个 Service ,定义有三个 ServicePort,比如 HTTP 、 HTTPS 、GRPC,则这个服务的一个实际运行的实例(也就是一个进程)就需要三个 ServiceInstance 对象,分别对应这三个 ServicePort。

通常情况下,会比较自然的采用的设计是:一个实际运行的实例对应一个 ServiceInstance 对象,然后在这个 ServiceInstance 对象中保存三个 Service Port 对象。

Istio的这个设计,使得 ServicePort 在进行服务发现时显得特别的重要,在后面一节讲述 Istio 的服务发现接口时再仔细介绍。

服务模型补充

IstioEndpoint

IstioEndpoint 具有关于特定服务和分片的单个地址+端口的信息。

// TODO: Replace NetworkEndpoint and ServiceInstance with Istio endpoints
type IstioEndpoint struct {
	Labels map[string]string  		// ServiceInstance
	Family AddressFamily			// NetworkEndpoint
	Address string					// NetworkEndpoint
	EndpointPort uint32				// NetworkEndpoint
	ServicePortName string			// NetworkEndpoint, 类型不同,从 int 变成了 string
	UID string						// NetworkEndpoint
	EnvoyEndpoint *endpoint.LbEndpoint // ServiceInstance
	ServiceAccount string 			// ServiceInstance
	Network string					// NetworkEndpoint
	Locality string					// NetworkEndpoint
	LbWeight uint32					// NetworkEndpoint
}

源代码中有TODO说要用 IstioEndpoint 替代 NetworkEndpoint 和 ServiceInstance。

IstioEndpoint 的特别说明:

  • ServicePortName 替代了 ServicePort,因为在进行端点回调时,端口号和协议可能不可用。
  • 不再分成一个 ServiceInstance 和一个 NetworkEndpoint - 两者都在一个结构中
  • 没有指向Service的指针 - 在收到端点时可能无法使用完整的Service对象。 服务名称作为key用于协调。
  • 它有一个缓存的 EnvoyEndpoint 对象 - 以避免为每个请求和客户端重新分配它。

Proxy定义

Proxy的定义在文件 pilot/pkg/model/context.go 中。虽然 Proxy 的定义不是服务发现的直接组成部分,但是后面发现服务发现的接口中明确出现了Proxy的信息,因此还是将Proxy的定义加进来。

Proxy包含有关代理的特定实例(envoy sidecar,Gateway等)的信息。 当Sidecar连接到Pilot时,代理被初始化,并从协议中的“node”信息以及从注册表中提取的数据填充。

在当前的Istio实现节点中,使用4部分’〜’分隔的ID。格式为 “Type~IPAddress~ID~Domain”。

type Proxy struct {
	// ClusterID 指定代理所在的集群
	// TODO: clarify if this is needed in the new 'network' model, likely needs to be renamed to 'network'
	ClusterID string

	// Type 指定node类型。是 ID 的第一部分
	Type NodeType

	// IPAddresses 是 proxy 用来标识自身和同处一地的服务实例的 IP 地址. 如: "10.60.1.6". 在某些情况下,proxy和服务实例所在的 host 可能有多个IP地址In
	IPAddresses []string

	// ID 是平台特有的sidecar proxy id。对于 k8s,是 pod id 和 namespace。
	ID string

	// Locality 是 envoy proxy 运行的位置
	Locality Locality

	// DNSDomain 为短主机名定义 DNS 域名后缀(如 "default.svc.cluster.local")
	DNSDomain string

	// TrustDomain 定义证书的信任域名
	TrustDomain string

	// ConfigNamespace 定义proxy所在的 namespace,用于网络范围目的
	// NOTE: DO NOT USE THIS FIELD TO CONSTRUCT DNS NAMES
	ConfigNamespace string

	// Metadata 是用于扩展 node 标识符的键值对
	Metadata map[string]string

	// 和proxy关联的 sidecarScope
	SidecarScope *SidecarScope
}

Node type的定义有:

// NodeType 决定 proxy 在mesh网络中的责任
type NodeType string

const (
	// SidecarProxy 类型用于在应用容器中的sidecar proxy
	SidecarProxy NodeType = "sidecar"

	// Ingress 类型用于集群的 ingress proxies
	Ingress NodeType = "ingress"

	// Router 类型用于以 L7/L4 路由器方式工作的独立代理
	Router NodeType = "router"
)

特意看了一下 Router 类型,支持以下 RouterMode:

// RouterMode 决定 Istio Gateway 的行为 (普通 或者 sni-dnat)
type RouterMode string

const (
	// StandardRouter 是普通的网关模式
	StandardRouter RouterMode = "standard"

	// SniDnatRouter 用于桥接两个网络
	SniDnatRouter RouterMode = "sni-dnat"
)

用于桥接两个网络的 SniDnatRouter ,非常有意思,稍后细看。

Controller定义

Controller定义了一个事件控制器循环。Proxy Agent 向控制器循环注册自身,并接收有关服务拓扑更改或配置工件更改的通知。

控制器保证以下一致性要求:控制器中的注册表视图与当前通知到达时一样新鲜,但可能更新鲜(例如“DELETE”取消“ADD”事件)。例如,创建服务的事件将看到注册表中没有这个服务,如果紧跟这个事件之后有服务删除事件。

Handler 按照它们附加的顺序在单个工作队列上执行。

Handler 接收通知事件和关联的对象。请注意,必须在启动控制器之前附加所有处理程序。

type Controller interface {
	// AppendServiceHandler 通知和服务目录相关的变更
	AppendServiceHandler(f func(*Service, Event)) error

	// AppendInstanceHandler 通知和服务的服务实例相关的变更
	AppendInstanceHandler(f func(*ServiceInstance, Event)) error

	// 运行直到接收到信号
	Run(stop <-chan struct{})
}