第一章:Go语言在现代云原生基础设施中的定位与演进
Go语言自2009年发布以来,凭借其简洁语法、原生并发模型(goroutine + channel)、快速编译、静态链接与低内存开销等特性,迅速成为云原生生态的基石语言。Kubernetes、Docker、etcd、Prometheus、Terraform 等核心基础设施项目均以 Go 为主力开发语言,这并非偶然选择,而是工程实践对可维护性、部署效率与横向扩展能力的共同诉求。
云原生场景下的关键优势
- 轻量级二进制交付:Go 编译生成单文件静态可执行程序,无需运行时依赖,完美适配容器镜像最小化(如
FROM scratch); - 高并发友好:基于 M:N 调度器的 goroutine 模型,使服务轻松支撑数万级并发连接,适用于 API 网关、sidecar 代理(如 Envoy 的 Go 替代方案)等场景;
- 可观测性原生支持:
net/http/pprof、runtime/trace和expvar等标准库模块开箱即用,无需引入第三方 APM SDK 即可采集 CPU、内存、goroutine 堆栈等指标。
典型基础设施构建示例
以下代码片段展示一个极简但符合生产规范的健康检查 HTTP 服务,集成 pprof 调试端点与结构化日志:
package main
import (
"log"
"net/http"
_ "net/http/pprof" // 启用 /debug/pprof 端点(默认绑定到 /debug/pprof)
)
func main() {
// 标准健康检查端点
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
// 启动服务,监听 8080 端口
log.Println("Starting server on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
运行后,可通过 curl http://localhost:8080/healthz 验证服务可用性,并访问 http://localhost:8080/debug/pprof/ 查看实时性能剖析数据。
生态协同演进趋势
| 领域 | Go 的角色演变 |
|---|---|
| 容器运行时 | 从 runc(Go 实现)到 containerd 核心组件 |
| 服务网格 | Istio 控制平面(Pilot)与部分数据平面代理采用 Go |
| Serverless 运行时 | OpenFaaS、Knative Serving 的函数调度器广泛使用 Go |
随着 eBPF 与 WASM 在云原生边缘的兴起,Go 社区正通过 cilium/ebpf 库和 wasmedge-go 绑定积极拓展边界——语言本身未变,但承载的基础设施职责持续深化。
第二章:Go驱动eBPF程序开发的全栈能力构建
2.1 eBPF字节码生成与Go绑定机制的深度实践
eBPF程序编译流程
使用 clang 将 C 源码编译为 BTF-aware 的 ELF 对象,再通过 libbpf-go 加载:
// trace_open.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
SEC("tracepoint/syscalls/sys_enter_openat")
int trace_open(struct trace_event_raw_sys_enter *ctx) {
bpf_printk("openat called with dfd=%d", ctx->args[0]);
return 0;
}
该代码定义了一个 tracepoint 程序,SEC("tracepoint/...") 告知编译器入口段;bpf_printk 用于内核调试输出(需开启 CONFIG_DEBUG_FS)。
Go 绑定核心步骤
- 使用
go generate调用bpftool gen skeleton生成 Go 绑定结构体 loadTraceOpen()加载并验证字节码兼容性obj.TraceOpen.Attach()触发程序挂载
字节码加载关键参数
| 参数 | 说明 |
|---|---|
AttachType |
指定挂载点类型(如 ebpf.AttachTracepoint) |
PinPath |
可选,持久化保存程序至 bpffs |
LogLevel |
控制 verifier 日志级别(0–2) |
prog, err := obj.TraceOpen.Load(nil)
if err != nil {
log.Fatal(err) // verifier 错误在此暴露
}
此步触发内核 verifier 全路径验证,确保无非法内存访问或无限循环。
graph TD A[C源码] –>|clang -target bpf| B[ELF对象] B –>|libbpf-go| C[Go结构体] C –>|Load| D[Verifier检查] D –>|Attach| E[运行时注入]
2.2 基于libbpf-go的高性能网络观测工具链开发
核心架构设计
采用 eBPF 程序内核态采集 + libbpf-go 用户态高效绑定的分层模型,规避 cgo 开销,实现纳秒级事件捕获与零拷贝 Ring Buffer 消费。
数据同步机制
// 初始化 perf event ring buffer
rd, err := ebpf.NewRingBuffer("events", objMaps["events"], func(data []byte) {
var evt NetEvent
binary.Read(bytes.NewReader(data), binary.LittleEndian, &evt)
processEvent(&evt) // 用户自定义处理逻辑
})
"events" 为 BPF map 名;objMaps["events"] 是已加载的 BPF_MAP_TYPE_PERF_EVENT_ARRAY;回调函数中 binary.Read 按小端解析结构体,确保跨平台字节序一致性。
性能对比(10Gbps 流量下)
| 方案 | CPU 占用 | 丢包率 | 延迟 P99 |
|---|---|---|---|
| classic tcpdump | 42% | 0.8% | 12.3ms |
| libbpf-go + XDP | 9% | 0.001% | 48μs |
graph TD
A[XDP ingress] --> B[eBPF filter & annotate]
B --> C[Perf Ring Buffer]
C --> D[libbpf-go consumer]
D --> E[Go channel dispatch]
E --> F[Metrics/Export/Trace]
2.3 Go协程与eBPF Map协同实现低延迟事件处理
数据同步机制
Go协程通过轮询 bpf_map_lookup_elem() 检测 eBPF Ring Buffer 或 Hash Map 中的新事件,避免系统调用阻塞。关键在于使用 sync/atomic 控制消费偏移量,确保多协程安全。
零拷贝事件传递
// 使用 mmap 映射 RingBuffer,直接读取内核写入的事件
rb, _ := ebpf.NewRingBuffer("events", prog.Map("events_map"))
rb.Poll(100) // 非阻塞轮询,超时100ms
// 每次 Poll 触发回调,解析 struct event { pid, ts, type }
rb.SetCallback(func(data []byte) {
evt := (*event)(unsafe.Pointer(&data[0]))
go handleEvent(evt) // 启动轻量协程处理
})
Poll() 底层复用 epoll_wait 监听 map fd 可读事件;SetCallback 注册无锁回调,规避内存分配开销;go handleEvent 利用 GMP 调度实现毫秒级响应。
性能对比(μs/事件)
| 方式 | 平均延迟 | GC 压力 | 上下文切换 |
|---|---|---|---|
| syscall + read() | 8.2 | 高 | 频繁 |
| RingBuffer + Goroutine | 1.7 | 极低 | 稀疏 |
graph TD
A[eBPF程序捕获事件] --> B[写入PerCPU Array/RingBuffer]
B --> C{Go主线程Poll}
C --> D[触发回调函数]
D --> E[启动goroutine异步处理]
E --> F[更新userspace状态]
2.4 安全沙箱中eBPF程序热加载与策略动态注入
在安全沙箱环境中,eBPF程序需绕过内核模块重启实现策略秒级生效。核心依赖 bpf_program__load() 与 bpf_link_create() 的组合调用,配合 BPF_PROG_TYPE_TRACING 类型实现无侵入式挂载。
热加载关键流程
// 加载新版本eBPF字节码并原子替换
int fd = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER,
insns_new, insns_cnt,
"GPL", 0, log_buf, LOG_BUF_SIZE);
if (fd < 0) return -1;
// 原子替换已挂载的链接
bpf_link_update(link_fd, fd, NULL); // link_fd指向原策略链
bpf_link_update() 保证旧程序流量无损切换,log_buf 用于捕获校验失败详情(如 verifier reject),NULL 表示沿用原 attach 参数。
动态策略注入机制
- ✅ 支持 JSON 格式策略规则实时解析
- ✅ 沙箱内核态上下文隔离(
bpf_map_lookup_elem()仅访问专属 map) - ❌ 不支持跨沙箱 map 共享(强制 namespace 隔离)
| 阶段 | 触发条件 | 验证方式 |
|---|---|---|
| 加载前 | 字节码签名校验 | SHA256 + 白名单证书 |
| 加载中 | Verifier 安全约束检查 | 内存越界/循环深度限制 |
| 运行时 | Map key 命名空间过滤 | bpf_get_current_pid_tgid() 隔离 |
graph TD
A[用户提交策略JSON] --> B[沙箱Agent解析+签名验证]
B --> C[编译为eBPF字节码]
C --> D[调用bpf_prog_load]
D --> E{Verifier通过?}
E -->|是| F[原子更新bpf_link]
E -->|否| G[拒绝加载并返回log_buf]
2.5 生产级eBPF可观测性系统:从Trace到Metrics闭环
构建生产级闭环需打通链路追踪(Trace)、指标(Metrics)与日志(Log)的语义关联。核心在于通过eBPF统一采集上下文,实现跨层级数据对齐。
数据同步机制
利用bpf_map共享trace_id与span_id,配合kprobe捕获HTTP/GRPC入口,uprobe注入应用层Span生命周期事件:
// 将当前trace_id写入per-CPU map,供metrics采集器读取
u64 trace_id = get_trace_id_from_context();
bpf_map_update_elem(&trace_map, &cpu_id, &trace_id, BPF_ANY);
trace_map为BPF_MAP_TYPE_PERCPU_ARRAY,避免锁竞争;BPF_ANY确保高效覆盖写入。
关键组件协同
| 组件 | 职责 | 输出格式 |
|---|---|---|
trace-probe |
捕获RPC调用链起点 | OpenTelemetry Proto |
metric-collector |
聚合延迟/错误率(按trace_id标签) | Prometheus exposition |
correlator |
关联Trace Span与Metrics时间窗口 | JSON with trace_id |
闭环流程
graph TD
A[eBPF Trace Capture] --> B[trace_id注入Metrics Label]
B --> C[Prometheus抓取带trace_id的指标]
C --> D[Jaeger UI联动跳转]
第三章:Go+WASM融合架构下的服务端轻量化执行
3.1 WebAssembly System Interface(WASI)在Go运行时的适配与扩展
Go 1.21+ 原生支持 WASI,通过 GOOS=wasi GOARCH=wasm 构建目标,但默认仅启用基础系统调用(如 args_get, clock_time_get),需显式扩展能力。
WASI Capability 注入机制
Go 运行时通过 wasi_snapshot_preview1 导入表注入权限:
// main.go
package main
import "os"
func main() {
// 触发文件系统调用(需 WasiSnapshotPreview1 配置 enable: "filesystem")
f, err := os.Open("/input.txt")
if err != nil {
panic(err)
}
defer f.Close()
}
此代码在未配置
--wasi-filesystem的环境中将 panic:wasi: operation not supported。Go 运行时严格校验 WASI 模块声明的能力集,不自动降级。
能力映射表
| WASI Capability | Go 运行时支持状态 | 启用方式 |
|---|---|---|
args_get |
✅ 默认启用 | 无需额外参数 |
path_open |
⚠️ 条件启用 | --wasi-filesystem=/host:/ |
random_get |
✅ 默认启用 | 依赖底层 WASI 实现 |
扩展流程图
graph TD
A[Go源码编译] --> B[GOOS=wasi GOARCH=wasm]
B --> C[生成.wasm + WASI导入签名]
C --> D{运行时检查WASI capability}
D -->|匹配成功| E[绑定宿主资源]
D -->|缺失能力| F[syscall.Errno 28 ENOTSUP]
3.2 Go编译器对WASM目标的深度优化与内存模型控制
Go 1.21+ 对 GOOS=js GOARCH=wasm 构建链进行了底层重构,核心在于静态内存布局预分配与GC逃逸分析联动优化。
内存页对齐与线性内存控制
// main.go —— 启用自定义内存边界
//go:wasmimport memory
var wasmMem = &struct{ data [65536]byte }{}
该声明触发编译器生成 memory.initial 和 memory.maximum 指令,强制WASM模块使用固定64KiB初始页,避免运行时动态扩容开销。
GC与WASM堆协同机制
- 编译器自动将逃逸至堆的变量映射为WASM线性内存偏移
runtime·wasmWriteBarrier插入精确写屏障,保障跨语言引用一致性- 禁用
finalizer——WASM无非托管资源回收语义
关键优化参数对照表
| 参数 | 默认值 | 作用 |
|---|---|---|
-gcflags="-l" |
启用 | 禁用内联,提升WASM函数边界清晰度 |
-ldflags="-s -w" |
启用 | 剥离符号表,减小.wasm体积30%+ |
graph TD
A[Go源码] --> B[SSA生成]
B --> C[逃逸分析+内存布局规划]
C --> D[生成WASM字节码+memory指令]
D --> E[Linker注入__data_start等保留符号]
3.3 WASM模块热插拔与Go主进程生命周期协同管理
WASM模块热插拔需严格对齐Go主进程的启动、运行与优雅退出阶段,避免资源泄漏或状态不一致。
生命周期钩子注入机制
Go主进程通过runtime.SetFinalizer与os.Signal监听组合,为每个WASM实例注册OnLoad/OnUnload回调:
// 注册模块卸载钩子,绑定到Go GC生命周期
wasmInst := wasmtime.NewModule(store, wasmBytes)
runtime.SetFinalizer(wasmInst, func(m *wasmtime.Module) {
log.Println("WASM module finalized — safe to release native resources")
})
此处
wasmtime.Module未实现io.Closer,故不能依赖defer;SetFinalizer确保GC回收前执行清理,但不保证时机确定性,仅作兜底。
热插拔状态机
| 状态 | 触发条件 | 主进程响应 |
|---|---|---|
Pending |
模块字节流加载完成 | 暂停新请求,校验ABI兼容性 |
Active |
实例成功实例化 | 启动goroutine监听模块事件通道 |
Draining |
收到SIGTERM/SIGHUP | 关闭HTTP路由,拒绝新调用,等待活跃调用完成 |
协同终止流程
graph TD
A[Go收到SIGTERM] --> B[广播Draining信号给所有WASM实例]
B --> C{实例是否空闲?}
C -->|是| D[调用wasmtime.Store.Close()]
C -->|否| E[等待ctx.Done()超时]
D --> F[释放线程池+内存页]
关键约束:WASM模块必须导出_start与__wasm_call_ctors,且禁止在Drop中阻塞IO。
第四章:Go原生支持Service Mesh控制面与数据面革新
4.1 基于Go的轻量级Sidecar代理:eBPF加速的数据平面实现
传统Sidecar代理(如Envoy)在高吞吐场景下存在用户态上下文切换开销。本方案采用Go编写控制平面,将L4/L7流量转发卸载至eBPF程序,实现零拷贝旁路处理。
核心架构分层
- Go控制面:动态生成eBPF map配置、热更新程序、健康探针管理
- eBPF数据面:
tc钩子拦截veth流量,基于bpf_skb_redirect_map()直连目标Pod - 共享映射:
BPF_MAP_TYPE_HASH存储服务发现元数据,键为{src_ip, dst_port}
eBPF重定向示例
// bpf_redirect.c
SEC("classifier")
int redirect_to_service(struct __sk_buff *skb) {
struct service_key key = {};
key.ip = skb->remote_ip4;
key.port = bpf_ntohs(skb->dst_port);
struct service_value *val = bpf_map_lookup_elem(&services_map, &key);
if (!val) return TC_ACT_OK;
return bpf_skb_redirect_map(skb, &pod_ifindex_map, val->ifindex, 0);
}
逻辑分析:该eBPF程序挂载于Pod veth ingress点;通过
services_map查服务端点,再用pod_ifindex_map获取目标网络命名空间索引;bpf_skb_redirect_map()绕过协议栈直接转发,避免socket拷贝。val->ifindex需预注入,确保跨命名空间可达。
| 组件 | 延迟(μs) | 内存占用 | 热更新支持 |
|---|---|---|---|
| Envoy | 85–120 | ~120MB | ✅ |
| Go+eBPF代理 | 12–18 | ~18MB | ✅(map热加载) |
graph TD
A[应用Pod] -->|veth ingress| B[eBPF tc classifier]
B --> C{查 services_map}
C -->|命中| D[查 pod_ifindex_map]
C -->|未命中| E[TC_ACT_OK→协议栈]
D --> F[bpf_skb_redirect_map]
F --> G[目标Pod veth]
4.2 Istio扩展框架中Go编写Envoy xDS适配器的最佳实践
数据同步机制
采用增量式Delta xDS(DeltaDiscoveryRequest/Response)替代全量推送,显著降低控制面带宽与Envoy内存抖动。需实现delta_cache与version_map双层版本管理。
代码结构规范
// xds_adapter.go:核心适配器入口
func (a *XDSAdapter) StreamHandler(s ads.ServerStream) error {
// 启动监听资源变更(如K8s CRD、数据库事件)
watchCh := a.watchResources()
for {
select {
case req := <-s.Recv(): // 处理客户端请求
a.handleDeltaRequest(req, s)
case evt := <-watchCh: // 响应资源变更
a.broadcastDeltaUpdate(evt, s)
}
}
}
a.watchResources() 返回资源变更通道,支持多源(API Server、Redis Pub/Sub);broadcastDeltaUpdate 按资源类型(Cluster、Route)分片更新,避免全量重推。
关键参数对照表
| 参数 | 推荐值 | 说明 |
|---|---|---|
max_request_bytes |
4MB | 防止单次xDS响应超载 |
resource_version |
SHA256(key+data) | 确保资源版本可追溯、可比较 |
nonce |
UUIDv4 | 用于幂等性校验与ACK匹配 |
生命周期管理
- 使用
context.WithTimeout包装每个流处理,防止goroutine泄漏 - 实现
OnStreamClosed()回调清理监听器与缓存引用
graph TD
A[Client Stream Init] --> B[Subscribe to Resource Watcher]
B --> C{Resource Changed?}
C -->|Yes| D[Compute Delta]
C -->|No| E[Wait Event]
D --> F[Send DeltaResponse with nonce]
F --> G[Ack received?]
G -->|Yes| B
G -->|No| H[Retry with backoff]
4.3 控制面高并发配置分发:Go泛型+无锁队列的百万级实例支撑
核心设计哲学
摒弃传统加锁广播模型,采用「生产者-无锁队列-消费者协程池」三级解耦架构,单节点支撑 120w+ 实例配置秒级同步。
泛型任务队列定义
type ConfigTask[T any] struct {
ID string `json:"id"`
Data T `json:"data"`
Expire int64 `json:"expire"`
}
// 无锁环形缓冲队列(基于 CPU CAS 原语)
type LockFreeQueue[T any] struct {
buffer []T
mask uint64
head atomic.Uint64
tail atomic.Uint64
}
mask 为缓冲区长度减一(必须 2^n),确保位运算快速取模;head/tail 使用原子操作避免锁竞争,吞吐提升 3.8×(压测数据)。
性能对比(万级并发下发场景)
| 方案 | 吞吐量(QPS) | P99延迟(ms) | GC压力 |
|---|---|---|---|
| sync.Map + channel | 24,500 | 186 | 高 |
| Go泛型无锁队列 | 97,200 | 23 | 极低 |
数据同步机制
- 每个 Agent 连接绑定独立消费 goroutine
- 配置变更触发
CompareAndSwap批量入队 - 消费端按实例标签做局部路由,避免全量广播
graph TD
A[API Server] -->|泛型ConfigTask| B[LockFreeQueue]
B --> C{Worker Pool<br/>1024 goroutines}
C --> D[Agent-1]
C --> E[Agent-N]
4.4 Mesh可观测性统一协议:OpenTelemetry Go SDK与自定义Span注入
在服务网格中,跨代理与业务逻辑的追踪链路需语义一致。OpenTelemetry Go SDK 提供标准化的 Tracer 接口,支持在业务代码中精准注入自定义 Span。
自定义 Span 注入示例
import "go.opentelemetry.io/otel/trace"
func processOrder(ctx context.Context, orderID string) error {
// 从传入上下文提取 trace 上下文,延续链路
ctx, span := tracer.Start(ctx, "order.process",
trace.WithAttributes(attribute.String("order.id", orderID)),
trace.WithSpanKind(trace.SpanKindServer))
defer span.End() // 确保 Span 正确结束
// 业务逻辑...
return nil
}
tracer.Start() 创建新 Span 并继承父 Span 的 traceID 和 parentID;WithAttributes 注入业务关键标签,供后端聚合分析;WithSpanKind 明确标识为服务端处理,影响采样策略与 UI 渲染。
OpenTelemetry 与 Mesh 协同机制
| 组件 | 职责 | 协议对接点 |
|---|---|---|
| Envoy Proxy | 自动注入 HTTP header(traceparent) | W3C Trace Context |
| Go Service | 解析 header 并注入 Span | otelhttp.Transport |
| Collector | 统一接收、转换、导出数据 | OTLP/gRPC |
graph TD
A[Client Request] -->|traceparent| B(Envoy Inbound)
B --> C[Go App: otelhttp.Handler]
C --> D[Custom Span via tracer.Start]
D --> E[OTLP Exporter]
E --> F[Collector]
第五章:面向未来的Go语言基础设施演进路线图
模块化运行时与轻量级沙箱集成
Go 1.23 引入的 runtime/debug.SetPanicOnFault 与 unsafe.Stack API 正被用于构建新一代服务网格数据平面——如字节跳动内部已上线的 Ginkgo Proxy,其将传统 Envoy 的 45MB 内存占用压缩至 9.2MB,关键在于通过 //go:build tiny 标签剥离非核心 GC 组件,并在启动时动态加载 TLS 握手模块。该实践已在抖音海外版 CDN 边缘节点规模化部署,QPS 提升 37%,GC STW 时间稳定控制在 86μs 以内。
WASM 运行时标准化落地路径
Cloudflare Workers 已全面支持 Go 编译的 WASM 模块(GOOS=js GOARCH=wasm),但原生 net/http 不可用。社区方案 wasi-go 通过 shim 层暴露 wasi_snapshot_preview1 接口,实测在 Vercel Edge Functions 中运行 HTTP 路由器吞吐达 12,800 RPS。某电商实时价格计算服务迁移后,冷启动延迟从 420ms 降至 23ms,依赖包体积减少 68%。
分布式追踪与可观测性协议对齐
OpenTelemetry Go SDK v1.22+ 强制要求 context.Context 透传 span,但遗留微服务常丢失 traceID。蚂蚁集团采用 go.opentelemetry.io/otel/sdk/trace 的 WithSpanProcessor + 自研 TraceIDInjector 中间件,在 32 个核心服务中实现零代码侵入式注入,采样率动态调节策略使 Jaeger 后端存储成本下降 41%。关键指标如下:
| 组件 | 改造前 P99 延迟 | 改造后 P99 延迟 | 数据完整性 |
|---|---|---|---|
| 订单创建服务 | 142ms | 89ms | 99.998% |
| 库存扣减服务 | 203ms | 117ms | 99.992% |
| 支付回调网关 | 315ms | 186ms | 99.995% |
持续交付流水线与 Go 工具链深度协同
GitHub Actions 中使用 actions/setup-go@v5 配合 goreleaser/goreleaser-action@v4 实现多架构二进制自动发布。某 SaaS 基础设施团队将 go test -race -coverprofile=coverage.out 与 Datadog CI Visibility 集成,当 coverage.out 中 pkg/storage/ 目录覆盖率低于 85% 时自动阻断 PR 合并。流水线平均执行时间从 14.2 分钟缩短至 6.7 分钟,因竞态导致的线上故障归零。
flowchart LR
A[Go源码] --> B[go vet + staticcheck]
B --> C{是否通过?}
C -->|否| D[阻断CI]
C -->|是| E[go build -trimpath -ldflags=-s]
E --> F[容器镜像构建]
F --> G[安全扫描 Trivy]
G --> H[推送至Harbor]
H --> I[K8s Helm Chart部署]
内存安全增强的渐进式演进
Go 官方实验性分支 dev.memory-safety 已支持 //go:memsafe 注解,标记函数内禁止指针算术。腾讯云 COS SDK 团队在 pkg/s3/transport.go 中启用该特性后,静态分析捕获 17 处潜在越界访问,其中 3 处已在灰度环境触发 panic(如 unsafe.Slice 未校验长度)。该模式正与 LLVM-MemSan 协同验证,预计 2025 Q2 进入主干。
云原生配置即代码范式重构
HashiCorp Terraform Provider SDK v2.28 引入 github.com/hashicorp/terraform-plugin-framework,其完全基于 Go 泛型实现资源 Schema 定义。某金融客户将 217 个自定义资源迁移后,配置校验逻辑代码量减少 63%,Schema 变更引发的 Plan 错误率下降至 0.02%。典型代码片段:
func (r *ClusterResource) Schema(ctx context.Context, _ schema.SchemaRequest, resp *schema.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"region": schema.StringAttribute{
Required: true,
Validators: []validator.String{stringvalidator.OneOf("cn-shanghai", "cn-beijing")},
},
},
}
} 