使用 ASM Hash Tagging 插件进行按比例灰度发布

简介: ASM Hash Tagging插件基于请求头哈希实现精准灰度发布,支持金丝雀、A/B测试等场景。通过FNV-1a算法将用户流量按比例稳定路由至不同服务版本,确保会话一致性,助力多应用独立灰度、渐进式发布。

使用 ASM Hash Tagging 插件进行按比例灰度发布

概述

ASM Hash Tagging 插件是一个 WebAssembly (Wasm) 插件,专为基于请求头哈希的流量路由而设计。它支持复杂的流量管理场景,如金丝雀发布、A/B 测试和基于用户的路由,确保同一用户始终收到相同的服务版本。

核心概念

工作原理

插件在入口网关级别运行,执行以下操作:

  1. 从传入请求中提取指定的头部值(例如 x-user-id
  2. 使用 FNV-1a 算法执行哈希操作
  3. 计算哈希值对配置值(通常为 100,用于百分比路由)的模
  4. 根据配置的范围确定要分配的标签值
  5. 向请求中添加带有分配标签值的新头部
  6. 此标签头部随后被 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%)

注意:每个规则应仅配置一种类型的策略。

流量分配算法

插件使用以下算法:

  1. 使用 FNV-1a 算法计算头部值的哈希值
  2. 计算 slot = hash % modulo
  3. 查找第一个 slot < policy.Range 的策略
  4. 向请求中添加相应的标签头部

这确保了相同的头部值的一致路由,同时实现精确的基于百分比的分配。

代码示例

基本配置示例

此示例根据用户的用户 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 资源,它支持支持现代部署实践的复杂流量管理策略。

更多关于本插件的使用说明,参见 基于哈希打标插件的多标签路由实现按用户比例进行灰度发布

相关文章
|
1天前
|
云安全 人工智能 算法
以“AI对抗AI”,阿里云验证码进入2.0时代
三层立体防护,用大模型打赢人机攻防战
1287 1
|
9天前
|
编解码 人工智能 自然语言处理
⚽阿里云百炼通义万相 2.6 视频生成玩法手册
通义万相Wan 2.6是全球首个支持角色扮演的AI视频生成模型,可基于参考视频形象与音色生成多角色合拍、多镜头叙事的15秒长视频,实现声画同步、智能分镜,适用于影视创作、营销展示等场景。
696 4
|
2天前
|
机器学习/深度学习 安全 API
MAI-UI 开源:通用 GUI 智能体基座登顶 SOTA!
MAI-UI是通义实验室推出的全尺寸GUI智能体基座模型,原生集成用户交互、MCP工具调用与端云协同能力。支持跨App操作、模糊语义理解与主动提问澄清,通过大规模在线强化学习实现复杂任务自动化,在出行、办公等高频场景中表现卓越,已登顶ScreenSpot-Pro、MobileWorld等多项SOTA评测。
519 2
|
2天前
|
人工智能 Rust 运维
这个神器让你白嫖ClaudeOpus 4.5,Gemini 3!还能接Claude Code等任意平台
加我进AI讨论学习群,公众号右下角“联系方式”文末有老金的 开源知识库地址·全免费
|
2天前
|
存储 弹性计算 安全
阿里云服务器4核8G收费标准和活动价格参考:u2a实例898.20元起,计算型c9a3459.05元起
现在租用阿里云服务器4核8G价格是多少?具体价格及配置详情如下:云服务器ECS通用算力型u2a实例,配备4核8G配置、1M带宽及40G ESSD云盘(作为系统盘),其活动价格为898.20元/1年起;此外,ECS计算型c9a实例4核8G配置搭配20G ESSD云盘,活动价格为3459.05元/1年起。在阿里云的当前活动中,4核8G云服务器提供了多种实例规格供用户选择,不同实例规格及带宽的组合将带来不同的优惠价格。本文为大家解析阿里云服务器4核8G配置的实例规格收费标准与最新活动价格情况,以供参考。
229 150
|
9天前
|
机器学习/深度学习 人工智能 前端开发
构建AI智能体:七十、小树成林,聚沙成塔:随机森林与大模型的协同进化
随机森林是一种基于决策树的集成学习算法,通过构建多棵决策树并结合它们的预测结果来提高准确性和稳定性。其核心思想包括两个随机性:Bootstrap采样(每棵树使用不同的训练子集)和特征随机选择(每棵树分裂时只考虑部分特征)。这种方法能有效处理大规模高维数据,避免过拟合,并评估特征重要性。随机森林的超参数如树的数量、最大深度等可通过网格搜索优化。该算法兼具强大预测能力和工程化优势,是机器学习中的常用基础模型。
353 164