Kitex 的 xDS 模块已经支持了以 Proxyless 模式对接 Istio,支持服务发现、服务路由和超时配置等功能,过往也有文章对此进行了介绍:Kitex 新增功能特性:支持 xDS 对接 Istio。Kitex 除了上述功能外,还支持熔断、限流、重试等治理策略,在 xDS 模块最新的 0.4.1 版本也对这些特性做了针对 Istio 的适配。
GitHub 地址: github.com/kitex-contr...
xDS 对接
首先来简单回顾一下接入 xDS 的步骤:
- xDS 模块初始化,调用
xds.Init()
便可开启对 xDS 模块的初始化,其中包括xdsResourceManager
负责 xDS 资源的管理。xdsClient
负责与控制面进行交互。 - Kite Client/Server 的 Option 配置
- 熔断和重试是针对 Kitex Client 的配置,需要在构造 Kitex Client 添加上配置所需要的 Middleware。
less
greetservice.NewClient(
"kitex-server:6789", // 服务名称
client.WithSuite(xdssuite.NewClientSuite()), // 包含了 xDS client 端所有的配置
)
- 限流针对 Kitex Server 生效,在构造 Kitex Server 时添加限流 Middleware。
less
greetservice.NewServer(
&GreetServiceImpl{},
xdssuite.NewLimiter(), // 限流配置
)
BootStrap
xdsClient 负责与控制面(例如 Istio)交互,以获得所需的 xDS 资源。在初始化时,需要读取环境变量用于构建 node 标识。所以,需要在 K8S 的容器配置文件 spec.containers.env
部分加入以下几个环境变量。
- POD_NAMESPACE:当前 pod 所在的 namespace。
- POD_NAME:pod 名。
- INSTANCE_IP: pod 的 ip。
- KITEX_XDS_METAS: 和 istiod 建立链接的元信息
在需要使用 xDS 功能的容器配置中加入以下定义即可:
yaml
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
- name: INSTANCE_IP
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.podIP
- name: KITEX_XDS_METAS
value: '{"CLUSTER_ID":"Kubernetes","DNS_AUTO_ALLOCATE":"true","DNS_CAPTURE":"true","INSTANCE_IPS":"$(INSTANCE_IP)","ISTIO_VERSION":"1.13.5","NAMESPACE":"$(POD_NAMESPACE)"}'
治理策略配置
熔断
熔断器的主要作用是在下游服务大量请求出错的情况下,熔断上游服务对下游服务的请求以防止故障蔓延,保护系统的整体稳定性。
现在 Kitex 仅支持错误率熔断,Istio 的 DestinationRule 只支持根据连续错误数量进行熔断,所以需要使用 Envoyfilter 的配置下发,xDS 的配置参考 www.envoyproxy.io/docs/envoy/... 配置参考 www.cloudwego.io/zh/docs/kit... 支持服务级别和实例级别的熔断,xDS 模块默认使用实例级别熔断,如果要切换到服务级别使用 xdssuite.WithServiceCircuitBreak(true)
方法进行切换。
下面的例子表示对 default 命名空间下所有访问 hello 的 client 生效的熔断配置,现在只支持针对服务、客户端的配置,不支持以方法的维度进行熔断,参数介绍:
spec.configPatches[0].match.cluster.service
:表示访问的服务,需要遵循 Kuberntes 的 FQDN 格式。failure_percentage_threshold
:触发熔断阈值,当错误率达到该值时进行熔断。failure_percentage_request_volume
:触发熔断的最小请求量,当总请求量小于该值时不会触发熔断。
yaml
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: circuitbreak-client
namespace: default
spec:
configPatches:
- applyTo: CLUSTER
match:
context: SIDECAR_OUTBOUND
cluster:
service: hello.default.svc.cluster.local
patch:
operation: MERGE
value:
outlier_detection:
failure_percentage_threshold: 10
failure_percentage_request_volume: 100
workloadSelector:
labels:
# the label of the client pod.
app.kubernetes.io/name: kitex-client
重试
Kitex 重试的规则比较复杂,参考:www.cloudwego.io/zh/docs/kit...
- max_retry_times: 重试次数
- max_duration_ms: 最大超时时间,如果请求耗时超过这个时间不会进行重试,以免整体耗时过大
- error_rate:错误率,如果错误率超过该值,不再进行重试,在错误率过大的情况下进行重试没有实际意义,而且还会扩大 QPS,对服务器造成更大的压力。取值范围为(0, 0.3],如果不在该有效范围内使用默认值 0.1。
- backoff_policy: 重试间隔策略,支持类型为
fixed
、random
、none
,cfg_items 根据实际类型配置fix_ms
、min_ms
、max_ms
等内容。
json
{
"enable": true,
"failure_policy": {
"retry_same_node": false,
"stop_policy": {
"max_retry_times": 2,
"max_duration_ms": 300,
"cb_policy": {
"error_rate": 0.2
}
},
"backoff_policy": {
"backoff_type": "fixed",
"cfg_items": {
"fix_ms": 100
}
}
}
}
Istio 的 VirtualService 支持配置重试规则,但是该规则配置相对比较简单,只支持重试次数以及重试超时时间,不建议生产使用,内容如下:
yaml
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: retry-client
namespace: default
spec:
hosts:
- hello.prod.svc.cluster.local:21001
http:
- route:
- destination:
host: hello.prod.svc.cluster.local:21001
retries:
attempts: 1
perTryTimeout: 2s
Envoy 自身 xDS 配置相对比较丰富,可以和 Kitex 的配置较好的搭配,参考:www.envoyproxy.io/docs/envoy/... Envoyfilter 配置重试规则,这里说明下两者之间的映射关系:
- numRetries: 对应 max_retry_times。
- perTryTimeout:单个请求超时,乘以重试次数为 max_duration_ms。
- retryBackOff: 对应 backoff_policy,会根据 baseInterval 和 maxInterval 两者的大小关系自动设置 backoff_policy 的类型。
- retriableHeaders:由于两者配置存在一定的差异,这里使用
kitexRetryErrorRate
映射 Kitex 的 error_rate;kitexRetryMethods
匹配具体需要执行重试策略的方法,多个方法之间使用逗号隔开,建议只针对幂等方法配置重试策略。 - workloadSelector:针对生效的 pod 客户端,如果不填会对该命名空间下的客户端生效。
- routeConfiguration:name 对应需要生效的服务名称,需要遵循 FQDN 规则,如果不填则对所有的服务生效。
yaml
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: retry-policy
namespace: default
spec:
configPatches:
- applyTo: HTTP_ROUTE
match:
context: SIDECAR_OUTBOUND
routeConfiguration:
name: hello.default.svc.cluster.local:21001
vhost:
name: hello.default.svc.cluster.local:21001
patch:
operation: MERGE
value:
route:
retryPolicy:
numRetries: 3
perTryTimeout: 100ms
retryBackOff:
baseInterval: 100ms
maxInterval: 100ms
retriableHeaders:
- name: "kitexRetryErrorRate"
stringMatch:
exact: "0.29"
- name: "kitexRetryMethods"
stringMatch:
exact: "Echo,Greet"
workloadSelector:
labels:
# the label of the service pod.
app.kubernetes.io/name: kitex-client
限流
限流需要使用 Envoyfilter 来配置,xDS 配置参考: www.envoyproxy.io/docs/envoy/... 限流器参考 www.cloudwego.io/zh/docs/kit... tokens_per_fill
字段表示每秒最大的请求数量,超出的请求将会被拒绝。Kitex 的 QPS 限流算法采用了令牌桶算法,每隔 100ms 往令牌桶添加 tokens_per_fill/10
的数量,所以建议该值的配置为 10 的整数。
yaml
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: ratelimit-client
namespace: default
spec:
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_INBOUND
listener:
filterChain:
filter:
name: "envoy.filters.network.http_connection_manager"
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.local_ratelimit
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
stat_prefix: http_local_rate_limiter
token_bucket:
# the qps limit
tokens_per_fill: 100
workloadSelector:
labels:
# the label of the server pod.
app.kubernetes.io/name: kitex-server
完整用例
Client 代码
go
package main
import (
"context"
"os"
"github.com/cloudwego/kitex/client"
"github.com/cloudwego/kitex/pkg/klog"
xdsmanager "github.com/kitex-contrib/xds"
"github.com/kitex-contrib/xds/xdssuite"
hello "github.com/whalecold/kitex-demo/helloworld/kitex_gen/hello"
"github.com/whalecold/kitex-demo/helloworld/kitex_gen/hello/echoservice"
)
func main() {
if err := xdsmanager.Init(); err != nil {
klog.Fatal(err)
}
dest := os.Getenv("DEST_SERVICE")
cli, err := echoservice.NewClient(dest,
client.WithSuite(xdssuite.NewClientSuite(xdssuite.WithMatchRetryMethod(true))),
)
if err != nil {
panic(err)
}
resp, err := cli.Greet(context.Background(), &hello.GreetRequest{
Req: "thrift client",
})
if err != nil {
klog.Error(err)
} else {
klog.Infof("resp: %s", resp)
}
}
Server 代码
go
package main
import (
"log"
"net"
"github.com/cloudwego/kitex/pkg/klog"
"github.com/cloudwego/kitex/server"
xdsmanager "github.com/kitex-contrib/xds"
"github.com/kitex-contrib/xds/xdssuite"
echo "github.com/whalecold/kitex-demo/helloworld/kitex_gen/hello/echoservice"
)
func main() {
if err := xdsmanager.Init(); err != nil {
klog.Fatal(err)
}
addr, _ := net.ResolveTCPAddr("tcp", ":6789")
svr := echo.NewServer(new(GreetServiceImplProto),
server.WithServiceAddr(addr),
xdssuite.NewLimiter(),
)
err := svr.Run()
if err != nil {
log.Println(err.Error())
}
}