使用 ASM Hash Tagging 插件进行按比例灰度发布
概述
ASM Hash Tagging 插件是一个 WebAssembly (Wasm) 插件,专为基于请求头哈希的流量路由而设计。它支持复杂的流量管理场景,如金丝雀发布、A/B 测试和基于用户的路由,确保同一用户始终收到相同的服务版本。
核心概念
工作原理
插件在入口网关级别运行,执行以下操作:
- 从传入请求中提取指定的头部值(例如
x-user-id) - 使用 FNV-1a 算法执行哈希操作
- 计算哈希值对配置值(通常为 100,用于百分比路由)的模
- 根据配置的范围确定要分配的标签值
- 向请求中添加带有分配标签值的新头部
- 此标签头部随后被 Istio VirtualService 用于路由到适当的服务子集
关键组件
- Header(头部): 用于哈希的请求头部(例如
x-user-id) - Modulo(模数): 哈希结果的最大值(通常为 100,用于基于百分比的路由)
- Tag Header(标签头部): 要添加到请求的头部名称(例如
app-version) - Policies(策略): 定义范围和相应的标签值
- Partitioned Policies(分区策略): 使用权重而非范围定义策略的替代方式
使用场景
1. 独立金丝雀发布
多个应用程序可以同时执行独立的灰度发布。每个应用程序团队都可以控制自己的发布百分比,而不会影响其他团队。例如:
- 应用 A:10% 的用户到 v2,90% 到 v1
- 应用 B:30% 的用户到 v2,70% 到 v1
- 应用 C:50% 的用户到 v3,50% 到 v1
2. 基于用户的流量路由
确保来自同一用户的所有请求始终路由到相同的服务版本,提供稳定的用户体验。这对于有用户会话或有状态交互的应用程序尤其重要。
3. A/B 测试
将不同的用户段路由到不同的应用程序版本以进行功能测试,可以精确控制分配给每个变体的用户百分比。
4. 渐进式发布
将新功能或服务版本逐渐引入到受控百分比的用户中,允许在全面部署之前进行监控和验证。
配置结构
type HashTaggingConfig struct {
Debug *HashTaggingDebugConfig `json:"debug,omitempty"`
Rules []TaggingRule `json:"rules"`
}
type TaggingRule struct {
Name *string `json:"name,omitempty"` // 可选的规则名称,用于调试
Match *TaggingRuleMatch `json:"match,omitempty"` // 可选的主机匹配
Header string `json:"header"` // 要进行哈希的头部
Modulo uint32 `json:"modulo"` // 模数值
TagHeader string `json:"tagHeader"` // 要添加到请求的头部
Policies []TaggingPolicy `json:"policies,omitempty"` // 基于范围的策略
PartitionedPolicies []PartitionedTaggingPolicy `json:"partitionedPolicies,omitempty"` // 基于权重的策略
}
type TaggingPolicy struct {
Range uint32 `json:"range"` // 此策略的上限
TagValue string `json:"tagValue"` // 当哈希值落在范围内时分配的值
}
type PartitionedTaggingPolicy struct {
PartitionSize uint32 `json:"partitionSize"` // 权重/分区大小
TagValue string `json:"tagValue"` // 此分区分配的值
}
type TaggingRuleMatch struct {
Host *string `json:"host,omitempty"` // 要匹配的主机模式
}
高级功能
主机匹配
使用通配符匹配将规则应用于特定主机:
match:
host: "*.example.com"
调试配置
启用详细日志记录并为调试指定请求 ID 头部:
debug:
requestIdHeader: x-request-id
detailLogEnabled: true
范围策略与分区策略
- 范围策略: 定义上限(例如,范围 33、66、100 对应 33%、33%、34%)
- 分区策略: 定义分区大小(例如,大小 30、70 对应 30%、70%)
注意:每个规则应仅配置一种类型的策略。
流量分配算法
插件使用以下算法:
- 使用 FNV-1a 算法计算头部值的哈希值
- 计算
slot = hash % modulo - 查找第一个
slot < policy.Range的策略 - 向请求中添加相应的标签头部
这确保了相同的头部值的一致路由,同时实现精确的基于百分比的分配。
代码示例
基本配置示例
此示例根据用户的用户 ID 路由流量,将 33% 路由到 v1,33% 到 v2,34% 到 v3:
apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
name: hash-tagging
namespace: istio-system
spec:
imagePullPolicy: Always
selector:
matchLabels:
istio: ingressgateway
url: registry-cn-hangzhou.ack.aliyuncs.com/dev/asm-wasm-hash-tagging:v1.22.6.2-g8d22c57-aliyun
phase: AUTHN
pluginConfig:
rules:
- header: x-user-id
modulo: 100
tagHeader: app-version
policies:
- range: 33
tagValue: v1
- range: 66
tagValue: v2
- range: 100
tagValue: v3
分区策略示例
此示例使用分区策略而非基于范围的策略:
apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
name: hash-tagging
namespace: istio-system
spec:
imagePullPolicy: Always
selector:
matchLabels:
istio: ingressgateway
url: registry-cn-hangzhou.ack.aliyuncs.com/dev/asm-wasm-hash-tagging:v1.22.6.2-g8d22c57-aliyun
phase: AUTHN
pluginConfig:
rules:
- header: x-user-id
modulo: 100
tagHeader: app-version
partitionedPolicies:
- partitionSize: 30
tagValue: v1
- partitionSize: 50
tagValue: v2
- partitionSize: 20
tagValue: v3
多规则示例
此示例演示针对不同应用程序的多个规则:
apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
name: hash-tagging
namespace: istio-system
spec:
imagePullPolicy: Always
selector:
matchLabels:
istio: ingressgateway
url: registry-cn-hangzhou.ack.aliyuncs.com/dev/asm-wasm-hash-tagging:v1.22.6.2-g8d22c57-aliyun
phase: AUTHN
pluginConfig:
rules:
# 应用 A 规则:10% 到 v2,90% 到 v1
- name: "app-a-routing"
header: x-user-id
modulo: 100
tagHeader: app-a-version
policies:
- range: 10
tagValue: v2
- range: 100
tagValue: v1
# 应用 B 规则:30% 到 v2,70% 到 v1
- name: "app-b-routing"
header: x-user-id
modulo: 100
tagHeader: app-b-version
policies:
- range: 30
tagValue: v2
- range: 100
tagValue: v1
# 应用 C 规则:50% 到 v3,50% 到 v1
- name: "app-c-routing"
header: x-user-id
modulo: 100
tagHeader: app-c-version
policies:
- range: 50
tagValue: v3
- range: 100
tagValue: v1
特定主机规则示例
此示例仅将规则应用于特定主机:
apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
name: hash-tagging
namespace: istio-system
spec:
imagePullPolicy: Always
selector:
matchLabels:
istio: ingressgateway
url: registry-cn-hangzhou.ack.aliyuncs.com/dev/asm-wasm-hash-tagging:v1.22.6.2-g8d22c57-aliyun
phase: AUTHN
pluginConfig:
rules:
- name: "api-app-routing"
match:
host: "api.example.com"
header: x-user-id
modulo: 100
tagHeader: api-version
policies:
- range: 25
tagValue: v2-beta
- range: 100
tagValue: v1-stable
完整的服务网格设置
要完成流量路由设置,您还需要基于插件添加的标签配置 VirtualService:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: app-a-virtualservice
namespace: app-namespace
spec:
hosts:
- app-a
http:
- match:
- headers:
app-a-version:
exact: v2
route:
- destination:
host: app-a
subset: v2
- route:
- destination:
host: app-a
subset: v1
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: app-a-destinationrule
namespace: app-namespace
spec:
host: app-a
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
源码示例
主程序入口 (main.go)
package main
import (
"encoding/json"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
_ "github.com/wasilibs/nottinygc"
"istio.alibabacloud.com/hashtagging/pkg/config"
"istio.alibabacloud.com/hashtagging/pkg/proxy"
)
func main() {
proxywasm.SetVMContext(&vmContext{
})
}
type vmContext struct {
// Embed the default VM context here,
// so that we don't need to reimplement all the methods.
types.DefaultVMContext
}
// Override types.DefaultVMContext.
func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext {
return &pluginContext{
}
}
type pluginContext struct {
// Embed the default plugin context here,
// so that we don't need to reimplement all the methods.
types.DefaultPluginContext
config *config.HashTaggingConfig
runtimeConfig *proxy.HashTaggingConfigRuntime
}
// Override types.DefaultPluginContext.
func (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus {
proxywasm.LogDebug("loading hash tagging plugin config")
data, err := proxywasm.GetPluginConfiguration()
if err != nil {
proxywasm.LogErrorf("error in GetPluginConfiguration: %v", err)
return types.OnPluginStartStatusFailed
}
if data == nil {
proxywasm.LogError("empty config, pluginStartFailed")
return types.OnPluginStartStatusFailed
}
config, err := config.NewHashTaggingConfig(data)
if err != nil {
proxywasm.LogErrorf("error in NewHashTaggingConfig: %v", err)
return types.OnPluginStartStatusFailed
}
ctx.config = config
ctx.runtimeConfig = &proxy.HashTaggingConfigRuntime{
}
ctx.runtimeConfig.FromConfig(config)
marshaledConfig, _ := json.Marshal(config)
marshaledRuntimeConfig, _ := json.Marshal(ctx.runtimeConfig)
proxywasm.LogDebugf("raw config: %s", string(data))
proxywasm.LogDebugf("marshaled config: %s", string(marshaledConfig))
proxywasm.LogDebugf("marshaled runtime config: %s", string(marshaledRuntimeConfig))
return types.OnPluginStartStatusOK
}
// Override types.DefaultPluginContext.
func (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext {
return proxy.NewHashTaggingContext(ctx.runtimeConfig)
}
配置处理 (pkg/config/config.go)
package config
import (
"encoding/json"
"fmt"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
"hash/fnv"
)
// TaggingPolicy represents a policy for tagging based on hash ranges.
type TaggingPolicy struct {
Range uint32 `json:"range"`
TagValue string `json:"tagValue"`
}
// PartitionedTaggingPolicy represents a policy for tagging based on partitions.
type PartitionedTaggingPolicy struct {
PartitionSize uint32 `json:"partitionSize"`
TagValue string `json:"tagValue"`
}
// TaggingRuleMatch specifies matching criteria for rules.
type TaggingRuleMatch struct {
Host *string `json:"host,omitempty"`
}
// DeepCopy creates a deep copy of TaggingRuleMatch.
func (t *TaggingRuleMatch) DeepCopy() *TaggingRuleMatch {
result := &TaggingRuleMatch{
}
if t.Host != nil {
tempHost := *t.Host
result.Host = &tempHost
}
return result
}
// TaggingRule represents a rule for hash tagging.
type TaggingRule struct {
Name *string `json:"name,omitempty"`
Match *TaggingRuleMatch `json:"match,omitempty"`
Header string `json:"header"`
Modulo uint32 `json:"modulo"`
TagHeader string `json:"tagHeader"`
Policies []TaggingPolicy `json:"policies,omitempty"`
// configure each policy's weight is friendlier than range
PartitionedPolicies []PartitionedTaggingPolicy `json:"partitionedPolicies,omitempty"`
}
// DeepCopy creates a deep copy of TaggingRule.
func (t *TaggingRule) DeepCopy() *TaggingRule {
result := &TaggingRule{
Header: t.Header,
Modulo: t.Modulo,
TagHeader: t.TagHeader,
Policies: make([]TaggingPolicy, len(t.Policies)),
}
copy(result.Policies, t.Policies)
if t.Name != nil {
tempName := *t.Name
result.Name = &tempName
}
if t.Match != nil {
result.Match = t.Match.DeepCopy()
}
for _, policy := range t.PartitionedPolicies {
result.PartitionedPolicies = append(result.PartitionedPolicies, policy)
}
return result
}
// HeaderGetter is a function type for getting header values.
type HeaderGetter func(headerName string) (string, error)
// MatchPolicy matches a policy based on header value hashing.
func (t *TaggingRule) MatchPolicy(headerGetter HeaderGetter) (*TaggingPolicy, error) {
hashHeaderValue, err := headerGetter(t.Header)
if err != nil {
return nil, fmt.Errorf("failed to match policy since failed to get header value, err: %s", err.Error())
}
hash := fnv.New32a()
_, err = hash.Write([]byte(hashHeaderValue))
if err != nil {
return nil, fmt.Errorf("failed to match policy since failed to write hash, err: %s", err.Error())
}
hashNumber := hash.Sum32()
slot := hashNumber % t.Modulo
for _, policy := range t.Policies {
if slot >= policy.Range {
continue
}
return &policy, nil
}
return nil, nil
}
// GetName returns the name of the rule.
func (t *TaggingRule) GetName() string {
if t.Name == nil {
return "unknown-rule"
}
return *t.Name
}
// HashTaggingDebugConfig holds debug configuration options.
type HashTaggingDebugConfig struct {
// user can specify a header to be treated as request id in log for debug use.
RequestIdHeader *string `json:"requestIdHeader,omitempty"`
DetailLogEnabled *bool `json:"detailLogEnabled,omitempty"`
}
// HashTaggingConfig is the main configuration structure.
type HashTaggingConfig struct {
Debug *HashTaggingDebugConfig `json:"debug,omitempty"`
Rules []TaggingRule `json:"rules"`
}
// validateConfig validates the configuration.
func validateConfig(config *HashTaggingConfig) error {
ruleRepeatDetectMap := map[string]struct{
}{
}
for _, rule := range config.Rules {
if _, found := ruleRepeatDetectMap[rule.TagHeader]; found {
return fmt.Errorf("HashTagging plugin found repeated rule, key %s", rule.TagHeader)
}
ruleRepeatDetectMap[rule.TagHeader] = struct{
}{
}
rangeOfPreviousPolicy := uint32(0)
tagValueRepeatedDetectMap := map[string]struct{
}{
}
if len(rule.Policies) > 0 && len(rule.PartitionedPolicies) > 0 {
return fmt.Errorf("one one of policies and weightedPolicies should be configured")
}
for _, policy := range rule.Policies {
if policy.Range == 0 {
return fmt.Errorf("HashTagging plugin, invalid policy of rule[%s], range must greater than 0", rule.TagHeader)
}
if policy.Range <= rangeOfPreviousPolicy {
return fmt.Errorf("HashTagging plugin, invalid policy of rule [%s], policy will never hit since range[%d] is not greater than previous one[%d]", rule.TagHeader, policy.Range, rangeOfPreviousPolicy)
}
rangeOfPreviousPolicy = policy.Range
if policy.Range > rule.Modulo {
return fmt.Errorf("HashTagging plugin, invalid policy of rule [%s], range[%d] greater than modulo[%d]", rule.TagHeader, policy.Range, rule.Modulo)
}
if _, found := tagValueRepeatedDetectMap[policy.TagValue]; found {
return fmt.Errorf("HashTagging plugin, found repeated tag value [%s] in rule [%s]", policy.TagValue, rule.TagHeader)
}
}
// validate weighted policies with rules, sum of weight must equals to modulo
if len(rule.PartitionedPolicies) > 0 {
sumOfWeight := uint32(0)
for _, weightedPolicy := range rule.PartitionedPolicies {
sumOfWeight += weightedPolicy.PartitionSize
}
if sumOfWeight > rule.Modulo {
return fmt.Errorf("HashTagging plugin, invalid weighted policies of rule [%s], sum of weight[%d] is greater than modulo[%d]", rule.TagHeader, sumOfWeight, rule.Modulo)
}
}
}
return nil
}
// NewHashTaggingConfig creates a new configuration from JSON.
func NewHashTaggingConfig(jsonStr []byte) (*HashTaggingConfig, error) {
config := &HashTaggingConfig{
}
err := json.Unmarshal(jsonStr, config)
if err != nil {
proxywasm.LogErrorf("error in unmarshal HashTaggingConfig: %v", err)
return config, err
}
return config, validateConfig(config)
}
哈希处理逻辑 (pkg/proxy/hash-tagging.go)
package proxy
import (
"fmt"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
"istio.alibabacloud.com/hashtagging/pkg/config"
wildmatch "github.com/becheran/wildmatch-go"
)
var PROPERTY_REQ_HOST = []string{
"request", "host"}
const (
WILDCARD = "*"
)
// TaggingRuleList is a list of tagging rules.
type TaggingRuleList []*config.TaggingRule
// Add adds a rule to the list.
func (t *TaggingRuleList) Add(rule *config.TaggingRule) {
*t = append(*t, rule)
}
// HashTaggingConfigRuntime is the runtime configuration structure.
type HashTaggingConfigRuntime struct {
Debug *config.HashTaggingDebugConfig
RulesForAllHost TaggingRuleList
RulesForCertainHost map[string]TaggingRuleList
}
// FromConfig converts the raw config to runtime config.
func (h *HashTaggingConfigRuntime) FromConfig(config *config.HashTaggingConfig) {
h.Debug = config.Debug
for _, rule := range config.Rules {
h.add(&rule)
}
}
// convertPartitionedPoliciesToRangePolicies converts partitioned policies to range policies.
func convertPartitionedPoliciesToRangePolicies(rule *config.TaggingRule) {
totalParitionSize := uint32(0)
for _, weightedPolicy := range rule.PartitionedPolicies {
totalParitionSize += weightedPolicy.PartitionSize
rangePolicy := config.TaggingPolicy{
Range: totalParitionSize,
TagValue: weightedPolicy.TagValue,
}
rule.Policies = append(rule.Policies, rangePolicy)
}
if totalParitionSize > rule.Modulo {
proxywasm.LogErrorf("failed to convert partitioned policy to range policy, total partitionSize MUST <= modulo")
}
}
// add adds a rule to the runtime configuration.
func (h *HashTaggingConfigRuntime) add(rule *config.TaggingRule) {
ruleCopy := rule.DeepCopy()
convertPartitionedPoliciesToRangePolicies(ruleCopy)
isForAllHost := ruleCopy.Match == nil || ruleCopy.Match.Host == nil ||
*ruleCopy.Match.Host == WILDCARD || *ruleCopy.Match.Host == ""
if isForAllHost {
h.RulesForAllHost.Add(ruleCopy)
} else {
if h.RulesForCertainHost == nil {
h.RulesForCertainHost = map[string]TaggingRuleList{
}
}
rules, found := h.RulesForCertainHost[*ruleCopy.Match.Host]
if found {
rules.Add(ruleCopy)
} else {
temp := TaggingRuleList{
}
temp.Add(ruleCopy)
h.RulesForCertainHost[*ruleCopy.Match.Host] = temp
}
}
}
// Match returns matching rules for the given host.
func (h *HashTaggingConfigRuntime) Match(reqHost string) TaggingRuleList {
result := TaggingRuleList{
}
// add all host rules into result
result = append(result, h.RulesForAllHost...)
for host, rules := range h.RulesForCertainHost {
wm := wildmatch.NewWildMatch(host)
if !wm.IsMatch(reqHost) {
continue
}
result = append(result, rules...)
}
return result
}
// HashTaggingContext is the context for hash tagging operations.
type HashTaggingContext struct {
// Embed the default http context here,
// so that we don't need to reimplement all the methods.
types.DefaultHttpContext
config *HashTaggingConfigRuntime
enabled bool
requestId string
}
// NewHashTaggingContext creates a new hash tagging context.
func NewHashTaggingContext(cfg *HashTaggingConfigRuntime) *HashTaggingContext {
return &HashTaggingContext{
config: cfg,
}
}
// matchRules matches rules for the current request.
func (h *HashTaggingContext) matchRules() (TaggingRuleList, error) {
reqHost, err := proxywasm.GetProperty(PROPERTY_REQ_HOST)
if err != nil {
return nil, fmt.Errorf("failed to get host, err: %s", err.Error())
}
reqHostStr := string(reqHost)
result := h.config.Match(reqHostStr)
if h.config.Debug != nil && h.config.Debug.DetailLogEnabled != nil && *h.config.Debug.DetailLogEnabled {
for idx, rule := range result {
h.LogDebugf("matched rule %d: %s", idx, rule.Name)
}
}
return result, nil
}
// OnHttpRequestHeaders handles HTTP request headers.
func (h *HashTaggingContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
h.LogDebugf("HashTagging entered")
rules, err := h.matchRules()
if err != nil {
h.LogDebugf("failed to match rules")
return types.ActionContinue
}
h.LogDebugf("HashTagging rules num: %d", len(rules))
for _, rule := range rules {
matchedPolicy, err := rule.MatchPolicy(proxywasm.GetHttpRequestHeader)
if err != nil {
h.LogErrorf("error when match policy of rule %s, err: %s", rule.GetName(), err.Error())
continue
}
if matchedPolicy == nil {
h.LogDebugf("hash tagging plugin rule %s not matched", rule.GetName())
continue
}
h.LogDebugf("hash tagging plugin rule %s matched, add header %s=%s", rule.GetName(), rule.TagHeader, matchedPolicy.TagValue)
err = proxywasm.AddHttpRequestHeader(rule.TagHeader, matchedPolicy.TagValue)
if err != nil {
h.LogErrorf("error when add header %s=%s for request", rule.TagHeader, matchedPolicy.TagValue)
continue
}
}
return types.ActionContinue
}
// OnHttpRequestBody handles HTTP request body.
func (h *HashTaggingContext) OnHttpRequestBody(bodySize int, endOfStream bool) types.Action {
return types.ActionContinue
}
// OnHttpResponseHeaders handles HTTP response headers.
func (h *HashTaggingContext) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action {
return types.ActionContinue
}
// OnHttpResponseBody handles HTTP response body.
func (h *HashTaggingContext) OnHttpResponseBody(bodySize int, endOfStream bool) types.Action {
return types.ActionContinue
}
// initRequestId initializes the request ID.
func (h *HashTaggingContext) initRequestId() {
if h.requestId == "" {
if h.config.Debug != nil && h.config.Debug.RequestIdHeader != nil {
requestId, err := proxywasm.GetHttpRequestHeader(*h.config.Debug.RequestIdHeader)
if err != nil {
h.requestId = "unknown"
} else {
h.requestId = requestId
}
} else {
h.requestId = "unknown"
}
}
}
// LogDebugf logs a debug message.
func (h *HashTaggingContext) LogDebugf(format string, args ...interface{
}) {
h.initRequestId()
newArgs := append([]interface{
}{
h.requestId}, args...)
proxywasm.LogDebugf("[HashTaggingPlugin][%s] "+format, newArgs...)
}
// LogTracef logs a trace message.
func (h *HashTaggingContext) LogTracef(format string, args ...interface{
}) {
h.initRequestId()
newArgs := append([]interface{
}{
h.requestId}, args...)
proxywasm.LogTracef("[HashTaggingPlugin][%s] "+format, newArgs...)
}
// LogErrorf logs an error message.
func (h *HashTaggingContext) LogErrorf(format string, args ...interface{
}) {
h.initRequestId()
newArgs := append([]interface{
}{
h.requestId}, args...)
proxywasm.LogErrorf("[HashTaggingPlugin][%s] "+format, newArgs...)
}
如何编译构建?
tinygo build -o build/plugin.wasm -gc=custom -tags='custommalloc nottinygc_envoy' -target=wasi -scheduler=none main.go
结论
Hash Tagging 插件为在服务网格中实施基于用户的流量路由提供了一个强大的机制。通过利用基于哈希的算法,它实现了精确的基于百分比的分布,同时在服务版本之间保持用户会话的一致性。
这种方法对于以下场景特别有价值:
- 跨多个应用程序的独立金丝雀发布
- 具有一致用户体验的 A/B 测试
- 带有细粒度控制的渐进式发布
- 具有不同功能集的多租户应用程序
插件的灵活配置允许在常见用例中保持简单的同时处理复杂的路由场景。结合 Istio 的 VirtualService 和 DestinationRule 资源,它支持支持现代部署实践的复杂流量管理策略。
更多关于本插件的使用说明,参见 基于哈希打标插件的多标签路由实现按用户比例进行灰度发布 。