← 中文资源

简体中文版本

W3C Baggage 传播

baggage 与 trace context 并行传播任意键值上下文——租户、标志、实验分组——每一跳都可读,无需手工塞进消息载荷或请求头。

阅读英文原文

概念定义

trace context 为诊断回答“这是哪个请求”;baggage 为行为回答它:请求随身携带运营上下文——租户、分组、区域——深层服务据此一致行动,不必每个调用方手工穿参数。

工作机制

baggage 头按定义的转义规则列出键值对;框架上下文 API 在请求内提供 get/set/remove,出站调用序列化当前 baggage。传播必须逐跳可选——不是每个外部调用都该转发内部上下文。

使用场景

多租户路由、跨服务边界保持一致的实验分组、区域与合规标志、流量来源这类请求级提示以做差异化处理。键集合保持精简并显式版本化,因为每一跳都看得见 baggage。

常见误区

baggage 不等于 trace 属性:属性落遥测存储,baggage 是带内传播给运行中服务的运营上下文。把它当万能请求域会把每个请求的每一跳都撑大;入口必须强制尺寸与键白名单,否则多跳累加直到中间盒拒绝请求。公开边缘的入站 baggage 是攻击者可控输入——先校验限界再信任;把令牌放进 baggage 等于向路径上每个服务与代理广播凭据。

代码示例

package main

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"net/http"
)

type requestIDKey struct{}

func validRequestID(id string) bool {
	if len(id) == 0 || len(id) > 128 {
		return false
	}
	for _, char := range id {
		switch {
		case char >= 'a' && char <= 'z':
		case char >= 'A' && char <= 'Z':
		case char >= '0' && char <= '9':
		case char == '-' || char == '_':
		default:
			return false
		}
	}
	return true
}

func RequestIDMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		id := r.Header.Get("X-Request-ID")
		if !validRequestID(id) {
			buffer := make([]byte, 8)
			if _, err := rand.Read(buffer); err != nil {
				http.Error(w, "cannot create request id", http.StatusInternalServerError)
				return
			}
			id = hex.EncodeToString(buffer)
		}
		w.Header().Set("X-Request-ID", id)
		ctx := context.WithValue(r.Context(), requestIDKey{}, id)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

func RequestIDFrom(ctx context.Context) string {
	id, _ := ctx.Value(requestIDKey{}).(string)
	return id
}

func main() {
	handler := RequestIDMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "request", RequestIDFrom(r.Context()))
	}))
	http.Handle("/", handler)
	_ = http.ListenAndServe(":8080", nil)
}
package main

import (
	"context"
	"fmt"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	pb "example.com/proto/inventory/v1"
)

func reserveWithDeadline(
	ctx context.Context,
	client pb.InventoryClient,
	sku string, qty int32,
) (*pb.ReserveReply, error) {
	ctx, cancel := context.WithTimeout(ctx, 1200*time.Millisecond)
	defer cancel()

	reply, err := client.Reserve(ctx, &pb.ReserveRequest{
		Sku: sku, Quantity: qty,
	})
	if err != nil {
		st := status.Convert(err)
		switch st.Code() {
		case codes.DeadlineExceeded:
			return nil, fmt.Errorf("reserve timed out: %v", st.Message())
		case codes.Canceled:
			return nil, fmt.Errorf("reserve canceled by caller: %v", st.Message())
		case codes.Unavailable:
			return nil, fmt.Errorf("service unavailable, retry later: %v", st.Message())
		default:
			return nil, fmt.Errorf("reserve failed: %v", err)
		}
	}
	return reply, nil
}

func main() {
	conn, err := grpc.NewClient(
		"inventory.example.com:443",
		grpc.WithDefaultCallTimeout(5*time.Second),
	)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer conn.Close()

	client := pb.NewInventoryClient(conn)

	parent, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	reply, err := reserveWithDeadline(parent, client, "SKU-1", 3)
	fmt.Println(reply, err)
}