查看原文
其他

Istio1.1新特性之限制服务可见性

敖小剑 几米宋 2022-09-10

背景

对于服务的可见性,在 Istio 设计之初,是没有特别考虑的,或者说,Istio 一开始的假定,就是建立在如下这个前提下的:

Istio中的每个服务都可以访问Mesh中的任意服务

即在服务发现/请求转发这个层面,对服务访问的可见性不做任何限制,而通过安全机制来解决服务间调用权限的问题,如RBAC的使用。在这个思想的指导下,Pilot组件是需要将全量的服务信息(服务注册信息和服务治理信息)下发到 Sidecar,这样Sidecar才能在做到不管服务要请求的目标是哪个服务,都可以做到正确的路由。

这个机制在 demo 和小规模使用时不是问题,但是,在实际项目落地时,如果服务数量比较多,数以百计/数以千计,则立即显露出来:

  • 下发的信息量太大:因此Pilot和Sidecar的CPU使用会很高,因为每次都要将全量的数据下发到每一个sidecar,需要编解码。Sidecar的内存使用也会增加。

  • 下发的频度非常密集:系统中任何一个服务的变动,都需要通知到每一个Sidecar,即使这个Sidecar所在的服务完全不访问它。

试想:假定 A 服务只需要访问 B/C 两个服务,但是在一个有1000个服务的系统中,A服务的 Sidecar 会不得不接收到其他 998 个服务的数据和每一次的变化通知。其所需有效数据和实际得到数据的比例高达 2:1000!

因此,在没有服务可见性控制的情况下,Pilot到Sidecar的数据下发的有效性低得可谓令人发指!

理想模式:同样假定 A 服务只需要访问 B/C 两个服务,如果能通过某个方式将这个信息(成为服务依赖或者服务可见性)提供出来,让Istio得到这个信息,那么在Pilot往A服务的Sidecar下发数据时,就可以做一个简单的过滤:只发送B/C服务的信息,和只在B/C服务发生变更时通知A。而这个简单过滤所带来的Pilot和Sidecar之间数据下发的性能提升,是和系统内服务数量成线性关系,很容易就实现两个或者三个数量级的提升。

在Istio问世快2年之际,Istio终于开始正视这个问题——好吧,我坦白,在这一点上,我是有怨言的:Istio的工程实现中,对实际生产问题的考虑,非常不到位。

Istio1.1新动态

Istio 1.1 即将发布(编者注:本文发表于 Istio 1.1发布前夕,Istio 1.1正式发布,中文文档同时释出),这几天陆续看到新文章介绍istio1.1的新功能:

其中和服务可见性直接相关的内容主要是 Sidecar CRD 和 ExportTo 属性。

引用上面文章的内容,"鸿沟前的服务网格—Istio 1.1 新特性预览" 的介绍如下:

新增 Sidecar 资源

目前版本中,Sidecar 会包含整个网格内的服务信息,在 1.1 中,新建了 Sidecar 资源,通过对这一 CRD 的配置,不但能够限制 Sidecar 的相关服务的数量,从而降低资源占用,提高传播效率;还能方便的对 Sidecar 的代理行为做出更多的精细控制——例如对 Ingress 场景中的被代理端点的配置能力。

ExportTo

多个路由管理对象加入了这一字段,用于指定该资源的生效范围。

"Istio1.1 功能预告" 一文的介绍是:

  • 新的sidecar资源:在指定命名空间中使用sidecar资源时,支持定义可访问的服务范围,这样可以降低发给proxy的配置数量。在大规模的集群中,我们推荐给每个namespace增加sidecar对象。 这个功能主要是为了提升性能,减轻proxy计算的负担。

  • 限制网络资源的生效范围:为所有的网络资源增加了exportTo的字段,用来表示此网络资源在哪些namespace中生效。这个字段目前只有两个值:

    • . 表示此网络资源只在自己定义的namespace生效;

    • * 表示此网络资源在所有的namespace生效。

    我们来围绕服务可见性,对Istio1.1新特性做一个深入了解。

    ExportTo 属性

    Istio 1.1 在 DestinationRule / ServiceEntry / VirtualService 三个 CRD 上新增加了 export_to 字段(忽略其他字段):

    1. message DestinationRule {

    2. repeated string export_to = 4;

    3. }

    4. message ServiceEntry {

    5. repeated string export_to = 7;

    6. }

    7. message VirtualService {

    8. repeated string export_to = 6;

    9. }

    下面是 exportTo 的字段说明,以 destination rule 为例:

    字段类型描述
    exportTostring[]当前destination rule要导出的 namespace 列表。 应用于 service 的 destination rule 的解析发生在 namespace 层次结构的上下文中。 destination rule 的导出允许将其包含在其他 namespace 中的服务的解析层次结构中。 此功能为服务所有者和网格管理员提供了一种机制,用于控制跨 namespace 边界的 destination rule 的可见性。

    如果未指定任何 namespace,则默认情况下将 destination rule 导出到所有 namespace。

    值 . 被保留,用于定义导出到 destination rule 被声明所在的相同 namespace ,类似的值 *保留,用于定义导出到所有 namespaces. 

    NOTE:在当前版本中,exportTo值被限制为 .或 *(即, 当前namespace或所有namespace)。

    从此,对以上三个CRD的使用,都必须满足 exportTo 对namespace的要求,才能被正确引用。如 gateway CRD 的说明:

    1. // NOTE: Only virtual services exported to the gateway's namespace

    2. // (e.g., `exportTo` value of `*`) can be referenced.

    3. // Private configurations (e.g., `exportTo` set to `.`) will not be

    4. // available. Refer to the `exportTo` setting in `VirtualService`,

    5. // `DestinationRule`, and `ServiceEntry` configurations for details.

    6. repeated string hosts = 2;

    Kubernetes原生service

    前面 exportTo 只能用于 DestinationRule / ServiceEntry / VirtualService ,对于我们最关注的 k8s 原生的 service对象没有涉及。而日常大多数服务都还是普通k8s服务,既然 ServiceEntry 这样Istio管理之外的服务都有可见性支持,没有理由不控制Istio内的服务。

    在 ServiceEntry 的定义中,发现注释部分有如下说明:

    1. message ServiceEntry {

    2. // For a Kubernetes Service, the equivalent effect can be achieved by setting

    3. // the annotation "networking.istio.io/exportTo" to a comma-separated list

    4. // of namespace names.

    5. repeated string export_to = 7;

    6. }

    对于k8s原生service,上面的注释说用 annotation "networking.istio.io/exportTo" 可以达到同样的效果。

    翻了一下Isito最新的代码, install/kubernetes/helm/istio/charts/mixer/templates/service.yaml 的例子:

    1. apiVersion: v1

    2. kind: Service

    3. metadata:

    4. name: istio-{{ $key }}

    5. namespace: {{ $.Release.Namespace }}

    6. annotations:

    7. networking.istio.io/exportTo: "*"

    pilot/pkg/serviceregistry/kube/conversion.go 文件中有这个annotation的常量定义:

    1. // ServiceExportAnnotation specifies the namespaces to which this service should be exported to.

    2. // "*" which is the default, indicates it is reachable within the mesh

    3. // "." indicates it is reachable within its namespace

    4. ServiceExportAnnotation = "networking.istio.io/exportTo"

    只在 pilot/pkg/serviceregistry/kube/conversion.go 的convertService() 方法中使用,这个方法将k8s 的 api core 中的 v1.Service 转为 istio 抽象模型中的 service:

    1. func convertService(svc v1.Service, domainSuffix string) *model.Service {

    2. ......

    3. if svc.Annotations[ServiceExportAnnotation] != "" {

    4. exportTo = make(map[model.Visibility]bool)

    5. for _, e := range strings.Split(svc.Annotations[ServiceExportAnnotation], ",") {

    6. exportTo[model.Visibility(e)] = true

    7. }

    8. }

    9. }

    逻辑很简单:用","将 annotation "networking.istio.io/exportTo" 的值拆开,然后转成对应 Visibility 对象作为key,以value为true保存起来。

    Visibility 类型定义如下:

    1. // Visibility defines whether a given config or service is exported to local namespace, all namespaces or none

    2. type Visibility string

    3. const (

    4. // VisibilityPrivate implies namespace local config

    5. VisibilityPrivate Visibility = "."

    6. // VisibilityPublic implies config is visible to all

    7. VisibilityPublic Visibility = "*"

    8. // VisibilityNone implies config is visible to none

    9. VisibilityNone Visibility = "~"

    10. )

    这里有三个特殊值,除了前面描述到的 .* 之外,还有一个 ~ 表示 none,不过目前没有使用。

    理论上说,这里可以通过 exportTo 字段(或者等效的 annotation "networking.istio.io/exportTo" )指定特定的 namespace,比如".,namespace1,namespace2"。但是目前文档中明确指出,只能使用 .*

    1. // NOTE: in the current release, the `exportTo` value is restricted to

    2. // "." or "*" (i.e., the current namespace or all namespaces).

    Sidecar CRD

    在 Istio 1.1 中增加了新的CRD Sidecar,具体的定义可见 https://github.com/istio/api/blob/8463cba039d858e8a849847b872ecea50b0994df/networking/v1alpha3/sidecar.proto

    从中摘录部分内容:

    Sidecar描述了sidecar代理的配置,sidecar代理调解与其连接的工作负载的 inbound 和 outbound 通信。 默认情况下,Istio将为网格中的所有Sidecar代理服务,使其具有到达网格中每个工作负载所需的必要配置,并在与工作负载关联的所有端口上接收流量。 Sidecar资源提供了一种的方法,在向工作负载转发流量或从工作负载转发流量时,微调端口集合和代理将接收的协议。 此外,可以限制代理在从工作负载转发 outbound 流量时可以达到的服务集合。

    Sidecar CRD的其他功能我们暂时不展开,只看和服务可见性相关的内容,看看Sidecar CRD是如何限制可以到达的服务结合的。文档中给出了下面这个简单例子:

    1. apiVersion: networking.istio.io/v1alpha3

    2. kind: Sidecar

    3. metadata:

    4. name: default

    5. namespace: prod-us1

    6. spec:

    7. egress:

    8. - hosts:

    9. - "prod-us1/*"

    10. - "prod-apis/*"

    11. - "istio-system/*"

    这个示例在 prod-us1 命名空间中声明了Sidecar资源,该资源配置命名空间中的sidecars,允许出口流量到prod-us1,prod-apis和istio-system命名空间中的公共服务。

    但是注意这个CRD需要配合前面的 exportTo 字段使用:即如果A服务(namespace为ns1)要访问B服务(namespace为ns2),则需要:

    1. 首先需要B服务申明 exportTo 到 ns1 中

    2. 然后再通过Sidecar CRD 设置 ns1 的 egress 的 hosts 为 "ns2/*"

    这样通过 exportTo 字段 + Sidecar.egress.hosts 字段的配合,实现了对服务可见性的限制。

    基本原理不复杂,具体实现时还有一些细节需要注意。

    WorkloadSelector

    上面的例子没有使用WorkloadSelector,因此设置的是整个 namespace 下所有的Sidecar的行为,或者说默认行为。可以通过带有 WorkloadSelector 的 Sidecar 资源来覆盖默认设置,hosts中也可以不用通配符,实现精确控制。

    例如下面的例子,就明确限制了 ns1 下的 服务 service-a 可以访问 ns2 下的服务:

    1. apiVersion: networking.istio.io/v1alpha3

    2. kind: Sidecar

    3. metadata:

    4. name: service-a-sidecar

    5. namespace: ns1

    6. spec:

    7. workloadSelector:

    8. labels:

    9. app: service-a

    10. egress:

    11. - hosts:

    12. - "ns2/*"

    hosts字段

    hosts 字段也是可以灵活设置的。文档中描述 hosts 字段为:

    必需:以 namespace/dnsName 格式被监听器暴露的的一个或多个服务主机。在指定namespace内与dnsName匹配的服务将被暴露(也就是可以访问)。相应的服务可以是服务注册表中的服务(例如,Kubernetes或cloud foundry服务)或使用ServiceEntry或 VirtualServicec 配置指定的服务。还可以使用同一名称空间中的任何关联的DestinationRule。

    应使用FQDN格式指定dnsName,在最左侧的组件中可以包含通配符(例如,prod / * .example.com)。将dnsName设置为 * 可以从指定的命名空间中选择所有服务(例如,prod/.example.com)。命名空间也可以设置为 * 以从任何可用的命名空间中选择特定服务(例如,"/ foo.example.com")。

    前面的例子我们使用了通配符,也可以不使用通配符而明确的指定特定可以访问的服务:

    1. apiVersion: networking.istio.io/v1alpha3

    2. kind: Sidecar

    3. metadata:

    4. name: service-a-sidecar

    5. namespace: ns1

    6. spec:

    7. workloadSelector:

    8. labels:

    9. app: service-a

    10. egress:

    11. - hosts:

    12. - "ns2/service-b.example.com"

    13. - "ns2/service-c.example.com"

    当然,记得有个前提条件:service-b/service-c 的 k8s service 和相关的 CRD(DestinationRule / ServiceEntry / VirtualService)都必须正确的设置 exportTo。

    备注:这里设计的有点复杂,按照这个思路,如果要实现上述的精确限制,多个环节都必须明确设置。一旦有一个地方出错,就会无法访问,然后debug的过程估计不会轻松。

    小结:Istio1.1 通过 exportTo 字段 + Sidecar.egress.hosts 字段的配合,实现了对服务可见性的约束

    代码实现

    pilot/pkg/model/push_context.go 中,PushContext 在保存 Service 和 VirtualService 信息时,都分为 private 和 public 两个结构:

    1. type PushContext struct {

    2. // privateServices are reachable within the same namespace.

    3. privateServicesByNamespace map[string][]*Service

    4. // publicServices are services reachable within the mesh.

    5. publicServices []*Service


    6. privateVirtualServicesByNamespace map[string][]Config

    7. publicVirtualServices []Config

    8. }

    以服务为例,会按照 ExportTo 字段的可见性设置来进行区分,将服务分别存放:

    1. // Caches list of services in the registry, and creates a map

    2. // of hostname to service

    3. func (ps *PushContext) initServiceRegistry(env *Environment) error {

    4. ......

    5. for _, s := range allServices {

    6. ns := s.Attributes.Namespace

    7. if len(s.Attributes.ExportTo) == 0 {

    8. if ps.defaultServiceExportTo[VisibilityPrivate] {

    9. ps.privateServicesByNamespace[ns]

    10. = append(ps.privateServicesByNamespace[ns], s)

    11. } else if ps.defaultServiceExportTo[VisibilityPublic] {

    12. ps.publicServices = append(ps.publicServices, s)

    13. }

    14. } else {

    15. if s.Attributes.ExportTo[VisibilityPrivate] {

    16. ps.privateServicesByNamespace[ns] =

    17. append(ps.privateServicesByNamespace[ns], s)

    18. } else {

    19. ps.publicServices = append(ps.publicServices, s)

    20. }

    21. }

    22. }

    23. ps.ServiceByHostname[s.Hostname] = s

    24. ps.ServicePort2Name[string(s.Hostname)] = s.Ports

    25. ......

    26. }

    当给具体的proxy下发数据时:

    1. // Services returns the list of services that are visible to a Proxy in a given config namespace

    2. func (ps *PushContext) Services(proxy *Proxy) []*Service {

    3. // 如果 proxy 有 sidecar scope,则从 sidecar scope 获取 service 列表

    4. if proxy != nil && proxy.SidecarScope != nil && proxy.SidecarScope.Config != nil && proxy.Type == SidecarProxy {

    5. return proxy.SidecarScope.Services()

    6. }


    7. out := []*Service{}


    8. // 没有 sidecar scope,就只考虑 exportTo 的影响

    9. if proxy == nil {

    10. for _, privateServices := range ps.privateServicesByNamespace {

    11. out = append(out, privateServices...)

    12. }

    13. } else {

    14. // 只给当前 proxy 所在 namespace 的 private 服务

    15. out = append(out, ps.privateServicesByNamespace[proxy.ConfigNamespace]...)

    16. }


    17. // 和 public 的服务

    18. out = append(out, ps.publicServices...)

    19. return out

    20. }

    SidecarScope 的说明,来自代码注释:

    SidecarScope是 Sidecar resource 的包装器,带有一些预处理数据,用于确定给定 Sidecar 可访问的Service,VirtualService和 DestinationRule。 预先计算 Sidecar 的 Service,VirtualService和 DestinationRule 可以提高性能,因为我们不再需要为每个 Sidecar 计算此列表。 我们只需将 Sidecar 与 SidecarScope 相匹配。

    1. type SidecarScope struct {

    2. // Union of services imported across all egress listeners for use by CDS code.

    3. services []*Service

    4. }

    预处理的细节就不继续展开了。

    分析

    从目前Istio1.1给出的信息看,Istio开始着手限制服务间可见性,以“降低资源占用,提高传播效率”——虽然我个人认为这个本应该是设计伊始就应该考虑的问题,但是无论如何,有比没有好。

    对于目前Istio1.1在限制服务可见性的做法,聊一下个人看法(保留后续更新修改的权利):

    1. 总算提供了一个避免全量数据下发的方式,理论上在服务数量比较多时,通过严格约束服务间的可见性,是可以让 Pilot 到 Sidecar 的数据下发数量起码降低一到两个数量级(1/10到1/100),Pilot的CPU使用/Sidecar的CPU使用/Sidecar的内存占用 应该都可以有明显改善。当然这是理论推断,具体是否做到了还要看 Istio 1.1 的实际测试结果。拭目以待吧,希望是个惊喜。

    2. 可见性的边界,是 namespace,这一点我有些担心:k8s 的 namespace 在实践中一般不会做非常细致的细分,搞不好一个体系里面可能就几个甚至一个 namespace,以 namespace 为边界来决定服务的可见性我个人觉得粒度太大——这一点稍后咨询一下各方情况再做更新。

    3. 设置上有些麻烦,从上面的分析上看,要实现服务A对服务B的精确限制,需要设置服务B的exportTo,包括k8s Service/Istio VitualService/Istio Destination Rule,还要设置服务A的 Sidecar CRD,至少要设置4个地方。繁琐且容易出错,而且语义也不直白:我相信大部分同学如果没有看过类似本文这样的讲解,恐怕很难一下就把这里面的条条道道梳理清楚。

    4. 只是限制服务的可见性,而不是明确的强制要求管理服务间的静态依赖关系,后者其实是我,或者说我们团队想要的。服务可见性和服务静态依赖关系之间有语义上的明确差别:服务可见性不具备强制性的,是笼统的,是可以含糊一点的,从Istio的意图看主要是为了效率的提升(毕竟之前的做法太浪费资源);而服务静态依赖关系是强制性的,依赖明确,设置精准,目标是为体系中的服务调用关系进行强力管控。

    先写到这,稍后深入后再补充。

    END

     推荐阅读 

    Istio 1.1正式发布,中文文档同时释出

    Istio 庖丁解牛1:组件概览

    自定义Istio Mixer Adapter示例教程(附源码)

    Istio中使用Prometheus进行应用程序指标度量

    通过自定义Istio Mixer Adapter在JWT场景下实现用户封禁


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存