searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

基于wasm插件实现istio路由级熔断

2024-07-11 09:37:48
47
0

社区Istio提供的熔断功能,Envoy通过连接池实现对上游集群的限流熔断,通过周期性的动态异常检测来确定上游集群中的某些主机是否异常。如果发现异常,则将该主机从连接池中隔离出去。这本质上是针对上游主机整体的熔断。有以下两个缺点:

仅支持服务级别熔断,不支持某个API进行限流熔断。

在路由规则后起作用,无法做到流量分发之前进行限流熔断。

本文介绍一种通过wasm插件实现路由级熔断的技术方案

改链方式

首先比较明确的一点,这个熔断点应该在route前,因此,改链的方式大概率是要跟在route前。

关于envoy的filter链,可以参考Envoy 架构与配置结构 – Envoy 中文指南 (icloudnative.io)

目前改链的方法有三种

  1. 通过直接改envoy源码,即侵入envoy代码,相当于实现一个内置的熔断逻辑。

  2. 通过envoyfilter扩展wasm,代码编进istio镜像中。

  3. 通过wasmplugin扩展wasm,代码是单独的镜像地址,可以按需拉取。

以上方式灵活度从1-3递增

  1. 需要编译envoy源码,一旦有问题需要所有sidecar更新重启

  2. 需要修改istio,把wasm插件编到istio中,后续依然要所有sidecar更新重启。

  3. 只需要修改镜像地址和标签,更新代价小一点。

以上方式代码权限从1-3递减

  1. 有几乎所有的envoy上下文,自由决定在什么范围生效。

  2. 仅能获取有限的沙箱上下文,可以配置生效范围,粒度细到vhost和route级别。

  3. 仅能获取有限的沙箱上下文,生效范围只能配置到workload级别。

比对2,3方案:

2:相对性能稍好一点,可以少改一些链。但是要编译和维护多版本的istio源码。

如果用这套方案,envoyfilter的applyTo基本可以照抄,插件的逻辑可以少写一点匹配vhost之类的逻辑,仅关注熔断算法即可。

3:代码很好维护,但是下发的配置给到workload级别之后,还需要通过wasm的上下文attributes来做简单的过滤。

如果用这套方案,需要根据attributes来对route做一些过滤,以达到“生效范围”的效果

wasmplugin实现可行性

参考envoyfilter的一些字段:Istio / Envoy Filter

1. vhost(name,port)

2. route的名字(对应在vs中)

3. header(vs也能配,但是只有prefix,exact,regex选择较少),能力不够丰富,自己继续扩充了,多了invert_match,presenct_match,suffix_match。

 

沿着这个思路,其实vhost和route也只是方便用户关联而已,最后如果要做更精细的熔断策略,应该还是要结合url,path,query参数等自由配置,最后还是得继续扩展,这也是方案3也可以接受的原因。


 

仅就目前支持vhost和route_name的场景,我们看下wasm能否支持:

wasm能力

attributes的列表:

Attributes — envoy 1.30.0-dev-da6cea documentation (envoyproxy.io)

我们特别需要关注的attribute如下:

request相关

request.path

string

The path portion of the URL

request.url_path

string

The path portion of the URL without the query string

request.host

string

The host portion of the URL

request.scheme

string

The scheme portion of the URL e.g. “http”

request.method

string

Request method e.g. “GET”

request.headers

map<string, string>

All request headers indexed by the lower-cased header name

 

Additional attributes are available once the request completes:

Attribute

Type

Description

request.duration

duration

Total duration of the request

request.size

int

Size of the request body. Content length header is used if available.

request.total_size

int

Total size of the request including the approximate uncompressed size of the headers

 

  1. 这里大概率需要结合host和scheme,决定port,如果参考envoyfilter的定义,是可以指定在某些vhost下生效的,由于wasm没有暴露vhost上下文,所以我们大概需要通过这两个字段来组合出vhost,正常来说就是domain:port

  2. 需要关注duration,这个是熔断算法的核心指标

response相关

response.* attributes are only available in http filters.

Attribute

Type

Description

response.code

int

Response HTTP status code

response.code_details

string

Internal response code details (subject to change)

response.flags

int

Additional details about the response beyond the standard response code encoded as a bit-vector

response.grpc_status

int

Response gRPC status code

response.headers

map<string, string>

All response headers indexed by the lower-cased header name

response.trailers

map<string, string>

All response trailers indexed by the lower-cased trailer name

response.size

int

Size of the response body

response.total_size

int

Total size of the response including the approximate uncompressed size of the headers and the trailers

这里code和grpc_status应该是主要关注的重点,请求是否失败应该需要用这个判断。

 

route上下文

Attribute

Type

Description

xds.node

Node

Local node description

xds.cluster_name

string

Upstream cluster name

xds.cluster_metadata

Metadata

Upstream cluster metadata

xds.listener_direction

int

Enumeration value of the listener traffic direction

xds.listener_metadata

Metadata

Listener metadata

xds.route_name

string

Route name

xds.route_metadata

Metadata

Route metadata

xds.upstream_host_metadata

Metadata

Upstream host metadata

xds.filter_chain_name

string

Listener filter chain name

 

listener_direction可以区分是否outbound 

route_name本地验证过就是vs里面的route名

route_metadata是结构体定义url,可以扩展加字段,看以后有没有必要,目前仅一个字段。

 

改链位置

从以下代码可以看出,right before the Router,在route之前是没问题的,问题是要有多前?

 

const (
    // Control plane decides where to insert the plugin. This will generally
    // be at the end of the filter chain, right before the Router.
    // Do not specify `PluginPhase` if the plugin is independent of others.
    PluginPhase_UNSPECIFIED_PHASE PluginPhase = 0
    // Insert plugin before Istio authentication filters.
    PluginPhase_AUTHN PluginPhase = 1
    // Insert plugin before Istio authorization filters and after Istio authentication filters.
    PluginPhase_AUTHZ PluginPhase = 2
    // Insert plugin before Istio stats filters and after Istio authorization filters.
    PluginPhase_STATS PluginPhase = 3
)

 

作为一个熔断器,走一次auth感觉也是有在消耗后端资源,个人倾向于在AUTHN前,尽早做熔断。这样的话链的顺序基本是

break -> authn -> authz -> stats

结论

使用wasm插件可以基本实现相应的效果,性能有一点损耗,因为wasmplugin是指定到route链前,但是apply的是整个httpFilter,不再做精细的vhost指定,需要在插件中劫持所有的流量,在根据上下文在内存中过滤。

 

熔断算法

目前主要参考hystrix。从参数来看,是比较明显的滑动窗口算法,hystrix的官方流程图如下:

其他细节可以参考How it Works · Netflix/Hystrix Wiki · GitHub

 

如果wasm来实现,hystrix底层的内存管理,数据共享,事件触发,线程池管理之类的肯定要重写的,因为以前调研过wasm的内存机制,锁竞争是很剧烈的,性能比较低下,所以在高压场景下,熔断器可能会有统计失败/统计不灵敏的情况,熔断器失常时应该放行流量。  

 

逻辑设计

滑动窗口的抽象,统计bucket的抽象目测可以复用。线程管理、信号量、锁等等逻辑在wasm环境下皆不可用,需要重写。

 

进一步分析代码

部分类是可以复用的,主要是rolling和metric*, 指标可以先用默认指标里面的一部分,然后http和线程池逻辑抽掉,http请求可以跳过了,直接用wasm的上下文中的耗时,请求次数,请求大小之类的填充。

 

// DefaultMetricCollector holds information about the circuit state.
// This implementation of MetricCollector is the canonical source of information about the circuit.
// It is used for for all internal hystrix operations
// including circuit health checks and metrics sent to the hystrix dashboard.
//
// Metric Collectors do not need Mutexes as they are updated by circuits within a locked context.
type DefaultMetricCollector struct {
    mutex *sync.RWMutex

    numRequests *rolling.Number
    errors      *rolling.Number

    successes               *rolling.Number
    failures                *rolling.Number
    rejects                 *rolling.Number
    shortCircuits           *rolling.Number
    timeouts                *rolling.Number
    contextCanceled         *rolling.Number
    contextDeadlineExceeded *rolling.Number

    fallbackSuccesses *rolling.Number
    fallbackFailures  *rolling.Number
    totalDuration     *rolling.Timing
    runDuration       *rolling.Timing
}

 

 

这里锁应该是没用的,wasm的shareData都是一个隔离的副本,而且读写之前envoy已经加了粗粒度的锁。可以考虑干掉。

 

默认指标大部分可以用,大部分也都可以在wasm上下文中取到。核心的通过率计算,熔断时间长度等,可以参考,但是估计也得重写,因为里面大量用了event和chan来互通,这些在wasm语境下肯定都是不可用的。

// IsOpen is called before any Command execution to check whether or
// not it should be attempted. An "open" circuit means it is disabled.
func (circuit *CircuitBreaker) IsOpen() bool {
    circuit.mutex.RLock()
    o := circuit.forceOpen || circuit.open
    circuit.mutex.RUnlock()

    if o {
       return true
    }

    if uint64(circuit.metrics.Requests().Sum(time.Now())) < getSettings(circuit.Name).RequestVolumeThreshold {
       return false
    }

    if !circuit.metrics.IsHealthy(time.Now()) {
       // too many failures, open the circuit
       circuit.setOpen()
       return true
    }

    return false
}


内存布局

wasm没有给出shareData的delete方法,所以要自行设计内存的布局。这里可能无限多的key,需要分哈希桶。

最终实现方式是:

circuit-breaker的name/namespace做桶。因为一个breaker就对应一个route_name,就是一个路由(header_match直接把不匹配的请求丢弃就行了)。

  • 每个route还能再指定host,port和header过滤
  • 一个breaker可以设置多个route,也可以不设置(不设置就是整个workload的所有流量都统计)
  • 综上,不是简单的取名字就行了,应该还要增加matcher的md5,如果matcher有修改,数据应该另开一个桶统计。可以先用breakername+##+matcher后缀做key。
  • 对应到sharedata的哈希桶key,应该还是再算一个哈希值取模1000。
  • 超时时间应大于等于break_duration,理论上也不能无限大,不如要求最多熔断180s。
  • 超时时间超过1s的请求,落桶按结束秒数来落
  • 每个桶再按时间窗口分slot,0-11(最多12个,即12s)个slot。每个slot里面仅有duration,requestNum之类的数值,应该占用很小。

 

按指标数估算,每个指标假定都是float64,即8字节,10个指标(或许可以预留多一点后续可以扩展)即80字节,12个slot即960字节,约算他1k。

大约1k个breaker占用就1M。前期感觉可以约定每个workload最多设定一万个熔断器,内存占用大约10M左右,属于可控水平。

0条评论
0 / 1000
g****m
3文章数
0粉丝数
g****m
3 文章 | 0 粉丝
g****m
3文章数
0粉丝数
g****m
3 文章 | 0 粉丝
原创

基于wasm插件实现istio路由级熔断

2024-07-11 09:37:48
47
0

社区Istio提供的熔断功能,Envoy通过连接池实现对上游集群的限流熔断,通过周期性的动态异常检测来确定上游集群中的某些主机是否异常。如果发现异常,则将该主机从连接池中隔离出去。这本质上是针对上游主机整体的熔断。有以下两个缺点:

仅支持服务级别熔断,不支持某个API进行限流熔断。

在路由规则后起作用,无法做到流量分发之前进行限流熔断。

本文介绍一种通过wasm插件实现路由级熔断的技术方案

改链方式

首先比较明确的一点,这个熔断点应该在route前,因此,改链的方式大概率是要跟在route前。

关于envoy的filter链,可以参考Envoy 架构与配置结构 – Envoy 中文指南 (icloudnative.io)

目前改链的方法有三种

  1. 通过直接改envoy源码,即侵入envoy代码,相当于实现一个内置的熔断逻辑。

  2. 通过envoyfilter扩展wasm,代码编进istio镜像中。

  3. 通过wasmplugin扩展wasm,代码是单独的镜像地址,可以按需拉取。

以上方式灵活度从1-3递增

  1. 需要编译envoy源码,一旦有问题需要所有sidecar更新重启

  2. 需要修改istio,把wasm插件编到istio中,后续依然要所有sidecar更新重启。

  3. 只需要修改镜像地址和标签,更新代价小一点。

以上方式代码权限从1-3递减

  1. 有几乎所有的envoy上下文,自由决定在什么范围生效。

  2. 仅能获取有限的沙箱上下文,可以配置生效范围,粒度细到vhost和route级别。

  3. 仅能获取有限的沙箱上下文,生效范围只能配置到workload级别。

比对2,3方案:

2:相对性能稍好一点,可以少改一些链。但是要编译和维护多版本的istio源码。

如果用这套方案,envoyfilter的applyTo基本可以照抄,插件的逻辑可以少写一点匹配vhost之类的逻辑,仅关注熔断算法即可。

3:代码很好维护,但是下发的配置给到workload级别之后,还需要通过wasm的上下文attributes来做简单的过滤。

如果用这套方案,需要根据attributes来对route做一些过滤,以达到“生效范围”的效果

wasmplugin实现可行性

参考envoyfilter的一些字段:Istio / Envoy Filter

1. vhost(name,port)

2. route的名字(对应在vs中)

3. header(vs也能配,但是只有prefix,exact,regex选择较少),能力不够丰富,自己继续扩充了,多了invert_match,presenct_match,suffix_match。

 

沿着这个思路,其实vhost和route也只是方便用户关联而已,最后如果要做更精细的熔断策略,应该还是要结合url,path,query参数等自由配置,最后还是得继续扩展,这也是方案3也可以接受的原因。


 

仅就目前支持vhost和route_name的场景,我们看下wasm能否支持:

wasm能力

attributes的列表:

Attributes — envoy 1.30.0-dev-da6cea documentation (envoyproxy.io)

我们特别需要关注的attribute如下:

request相关

request.path

string

The path portion of the URL

request.url_path

string

The path portion of the URL without the query string

request.host

string

The host portion of the URL

request.scheme

string

The scheme portion of the URL e.g. “http”

request.method

string

Request method e.g. “GET”

request.headers

map<string, string>

All request headers indexed by the lower-cased header name

 

Additional attributes are available once the request completes:

Attribute

Type

Description

request.duration

duration

Total duration of the request

request.size

int

Size of the request body. Content length header is used if available.

request.total_size

int

Total size of the request including the approximate uncompressed size of the headers

 

  1. 这里大概率需要结合host和scheme,决定port,如果参考envoyfilter的定义,是可以指定在某些vhost下生效的,由于wasm没有暴露vhost上下文,所以我们大概需要通过这两个字段来组合出vhost,正常来说就是domain:port

  2. 需要关注duration,这个是熔断算法的核心指标

response相关

response.* attributes are only available in http filters.

Attribute

Type

Description

response.code

int

Response HTTP status code

response.code_details

string

Internal response code details (subject to change)

response.flags

int

Additional details about the response beyond the standard response code encoded as a bit-vector

response.grpc_status

int

Response gRPC status code

response.headers

map<string, string>

All response headers indexed by the lower-cased header name

response.trailers

map<string, string>

All response trailers indexed by the lower-cased trailer name

response.size

int

Size of the response body

response.total_size

int

Total size of the response including the approximate uncompressed size of the headers and the trailers

这里code和grpc_status应该是主要关注的重点,请求是否失败应该需要用这个判断。

 

route上下文

Attribute

Type

Description

xds.node

Node

Local node description

xds.cluster_name

string

Upstream cluster name

xds.cluster_metadata

Metadata

Upstream cluster metadata

xds.listener_direction

int

Enumeration value of the listener traffic direction

xds.listener_metadata

Metadata

Listener metadata

xds.route_name

string

Route name

xds.route_metadata

Metadata

Route metadata

xds.upstream_host_metadata

Metadata

Upstream host metadata

xds.filter_chain_name

string

Listener filter chain name

 

listener_direction可以区分是否outbound 

route_name本地验证过就是vs里面的route名

route_metadata是结构体定义url,可以扩展加字段,看以后有没有必要,目前仅一个字段。

 

改链位置

从以下代码可以看出,right before the Router,在route之前是没问题的,问题是要有多前?

 

const (
    // Control plane decides where to insert the plugin. This will generally
    // be at the end of the filter chain, right before the Router.
    // Do not specify `PluginPhase` if the plugin is independent of others.
    PluginPhase_UNSPECIFIED_PHASE PluginPhase = 0
    // Insert plugin before Istio authentication filters.
    PluginPhase_AUTHN PluginPhase = 1
    // Insert plugin before Istio authorization filters and after Istio authentication filters.
    PluginPhase_AUTHZ PluginPhase = 2
    // Insert plugin before Istio stats filters and after Istio authorization filters.
    PluginPhase_STATS PluginPhase = 3
)

 

作为一个熔断器,走一次auth感觉也是有在消耗后端资源,个人倾向于在AUTHN前,尽早做熔断。这样的话链的顺序基本是

break -> authn -> authz -> stats

结论

使用wasm插件可以基本实现相应的效果,性能有一点损耗,因为wasmplugin是指定到route链前,但是apply的是整个httpFilter,不再做精细的vhost指定,需要在插件中劫持所有的流量,在根据上下文在内存中过滤。

 

熔断算法

目前主要参考hystrix。从参数来看,是比较明显的滑动窗口算法,hystrix的官方流程图如下:

其他细节可以参考How it Works · Netflix/Hystrix Wiki · GitHub

 

如果wasm来实现,hystrix底层的内存管理,数据共享,事件触发,线程池管理之类的肯定要重写的,因为以前调研过wasm的内存机制,锁竞争是很剧烈的,性能比较低下,所以在高压场景下,熔断器可能会有统计失败/统计不灵敏的情况,熔断器失常时应该放行流量。  

 

逻辑设计

滑动窗口的抽象,统计bucket的抽象目测可以复用。线程管理、信号量、锁等等逻辑在wasm环境下皆不可用,需要重写。

 

进一步分析代码

部分类是可以复用的,主要是rolling和metric*, 指标可以先用默认指标里面的一部分,然后http和线程池逻辑抽掉,http请求可以跳过了,直接用wasm的上下文中的耗时,请求次数,请求大小之类的填充。

 

// DefaultMetricCollector holds information about the circuit state.
// This implementation of MetricCollector is the canonical source of information about the circuit.
// It is used for for all internal hystrix operations
// including circuit health checks and metrics sent to the hystrix dashboard.
//
// Metric Collectors do not need Mutexes as they are updated by circuits within a locked context.
type DefaultMetricCollector struct {
    mutex *sync.RWMutex

    numRequests *rolling.Number
    errors      *rolling.Number

    successes               *rolling.Number
    failures                *rolling.Number
    rejects                 *rolling.Number
    shortCircuits           *rolling.Number
    timeouts                *rolling.Number
    contextCanceled         *rolling.Number
    contextDeadlineExceeded *rolling.Number

    fallbackSuccesses *rolling.Number
    fallbackFailures  *rolling.Number
    totalDuration     *rolling.Timing
    runDuration       *rolling.Timing
}

 

 

这里锁应该是没用的,wasm的shareData都是一个隔离的副本,而且读写之前envoy已经加了粗粒度的锁。可以考虑干掉。

 

默认指标大部分可以用,大部分也都可以在wasm上下文中取到。核心的通过率计算,熔断时间长度等,可以参考,但是估计也得重写,因为里面大量用了event和chan来互通,这些在wasm语境下肯定都是不可用的。

// IsOpen is called before any Command execution to check whether or
// not it should be attempted. An "open" circuit means it is disabled.
func (circuit *CircuitBreaker) IsOpen() bool {
    circuit.mutex.RLock()
    o := circuit.forceOpen || circuit.open
    circuit.mutex.RUnlock()

    if o {
       return true
    }

    if uint64(circuit.metrics.Requests().Sum(time.Now())) < getSettings(circuit.Name).RequestVolumeThreshold {
       return false
    }

    if !circuit.metrics.IsHealthy(time.Now()) {
       // too many failures, open the circuit
       circuit.setOpen()
       return true
    }

    return false
}


内存布局

wasm没有给出shareData的delete方法,所以要自行设计内存的布局。这里可能无限多的key,需要分哈希桶。

最终实现方式是:

circuit-breaker的name/namespace做桶。因为一个breaker就对应一个route_name,就是一个路由(header_match直接把不匹配的请求丢弃就行了)。

  • 每个route还能再指定host,port和header过滤
  • 一个breaker可以设置多个route,也可以不设置(不设置就是整个workload的所有流量都统计)
  • 综上,不是简单的取名字就行了,应该还要增加matcher的md5,如果matcher有修改,数据应该另开一个桶统计。可以先用breakername+##+matcher后缀做key。
  • 对应到sharedata的哈希桶key,应该还是再算一个哈希值取模1000。
  • 超时时间应大于等于break_duration,理论上也不能无限大,不如要求最多熔断180s。
  • 超时时间超过1s的请求,落桶按结束秒数来落
  • 每个桶再按时间窗口分slot,0-11(最多12个,即12s)个slot。每个slot里面仅有duration,requestNum之类的数值,应该占用很小。

 

按指标数估算,每个指标假定都是float64,即8字节,10个指标(或许可以预留多一点后续可以扩展)即80字节,12个slot即960字节,约算他1k。

大约1k个breaker占用就1M。前期感觉可以约定每个workload最多设定一万个熔断器,内存占用大约10M左右,属于可控水平。

文章来自个人专栏
envoy源码分析:线程模型与沙箱机制
2 文章 | 1 订阅
0条评论
0 / 1000
请输入你的评论
1
1