第一章:Windows注册表在Go客户端中的关键角色与金融级可靠性挑战
Windows注册表是操作系统核心配置与状态存储的权威来源,在高频交易、风控引擎和合规审计类Go客户端中,它承载着证书路径、加密密钥句柄、服务启动策略、审计日志开关等不可替代的系统级元数据。与文件或数据库不同,注册表提供原子性写入、内核级访问控制(如KEY_WRITE权限隔离)及跨会话持久化能力,这使其成为金融场景下保障配置强一致性的首选载体。
注册表访问需绕过Go标准库限制
Go原生不支持Windows注册表操作,必须通过syscall或封装良好的第三方包(如github.com/google/winregistry)调用Win32 API。以下为安全读取TLS证书存储路径的示例:
package main
import (
"fmt"
"golang.org/x/sys/windows/registry"
)
func readCertStorePath() (string, error) {
// 打开 HKEY_LOCAL_MACHINE\SOFTWARE\MyFinApp\Settings
key, err := registry.OpenKey(registry.LOCAL_MACHINE,
`SOFTWARE\MyFinApp\Settings`,
registry.READ)
if err != nil {
return "", fmt.Errorf("failed to open registry key: %w", err)
}
defer key.Close()
// 读取字符串值 "CertStorePath",避免空值导致panic
path, _, err := key.GetStringValue("CertStorePath")
if err != nil {
return "", fmt.Errorf("missing or invalid CertStorePath: %w", err)
}
return path, nil
}
金融级可靠性三大硬约束
- 事务性保障:单次写入必须包含
RegCreateKeyEx+RegSetValueEx+RegFlushKey三步,确保断电后数据不丢失; - 权限最小化:客户端进程应以
SERVICE_LOGON_USER身份运行,仅申请KEY_READ而非KEY_ALL_ACCESS; - 变更审计闭环:所有写操作须同步记录至ETW日志,并触发SHA-256校验值比对,例如:
| 操作类型 | 注册表路径 | 必须校验字段 |
|---|---|---|
| 写入 | HKLM\SYSTEM\CurrentControlSet\Services\MyTradeSvc\Parameters |
StartupMode, TimeoutSec |
| 删除 | HKCU\Software\MyFinApp\TempConfig |
删除前快照哈希值 |
任何未签名的注册表修改均触发熔断机制——客户端自动进入只读降级模式,并向风控中心上报REG_UNAUTHORIZED_MOD事件。
第二章:Go语言读取Windows注册表的底层机制与竞态风险建模
2.1 Windows注册表API封装原理与syscall包调用链剖析
Windows Go程序通过syscall包间接调用NT内核导出的注册表相关系统服务(如NtCreateKey、NtSetValueKey),而非直接使用Win32 RegOpenKeyEx等API。
封装层级关系
- 底层:
ntdll.dll中NtCreateKey等Native API(syscall.Syscall6触发) - 中间层:
golang.org/x/sys/windows中RegOpenKeyEx等封装(自动处理KEY_WOW64_*标志与错误码转换) - 上层:标准库
os/user或第三方库(如github.com/google/winregistry)进一步抽象
关键调用链示例
// 调用 RegOpenKeyExW → 内部转为 NtOpenKey + ObjectAttributes 构造
ret, err := syscall.RegOpenKeyEx(syscall.HKEY_LOCAL_MACHINE,
`SOFTWARE\Microsoft\Windows NT\CurrentVersion`, 0,
syscall.KEY_READ, &key)
此调用最终经
syscall.NewLazySystemDLL("advapi32.dll").NewProc("RegOpenKeyExW")加载,再由syscall.Syscall6转发至ntdll!NtOpenKey。参数表示无重定向,KEY_READ被映射为0x20019访问掩码。
| 层级 | 组件 | 特点 |
|---|---|---|
| Win32 API | advapi32.dll |
提供兼容性封装,含UAC虚拟化逻辑 |
| Native API | ntdll.dll |
直接进入内核模式,无权限检查绕过风险 |
graph TD
A[Go代码调用 RegOpenKeyEx] --> B[advapi32!RegOpenKeyExW]
B --> C[ntdll!NtOpenKey]
C --> D[Kernel: ObOpenObjectByName]
2.2 Go runtime调度器视角下的Registry读取并发行为实证分析
数据同步机制
Registry读取常通过 sync.Map 封装,规避全局锁竞争:
var reg = sync.Map{} // key: string, value: *ServiceInstance
// 并发安全读取
if val, ok := reg.Load("api-gateway"); ok {
inst := val.(*ServiceInstance)
// 使用inst...
}
sync.Map.Load() 内部采用分段锁+只读映射快照,避免 Goroutine 在高读场景下陷入 runtime.lock 等待队列;其 read 字段为原子指针,读操作几乎不触发调度器抢占。
调度器可观测行为
在 GOMAXPROCS=4 下压测 10K QPS 读请求,pprof trace 显示:
runtime.futex调用下降 92%(相比map + mutex)- 平均 Goroutine 阻塞时间
G状态切换频次稳定在 12–15 次/秒(无抖动)
| 指标 | sync.Map | map+RWMutex |
|---|---|---|
| P99 延迟(μs) | 82 | 417 |
| GC STW 影响占比 | 0.3% | 2.1% |
| 协程就绪队列峰值长度 | 17 | 214 |
执行路径可视化
graph TD
A[Goroutine Load] --> B{read.amended?}
B -->|Yes| C[原子读取 readonly.m]
B -->|No| D[尝试 slowLoad → upgrade]
C --> E[返回值 或 nil]
D --> E
2.3 竞态条件复现:多goroutine高频ReadString/ReadInteger触发的内存撕裂实验
数据同步机制
当多个 goroutine 并发调用 ReadString 和 ReadInteger(底层共享同一 bufio.Reader 的 buf 和 rd)时,若未加锁,buf.off、buf.n 等字段可能被同时读写,导致偏移错乱与字节截断。
复现实验代码
var r = bufio.NewReader(strings.NewReader("42hello123world"))
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
s, _ := r.ReadString('h') // 竞态点:修改 buf.off/buf.n
fmt.Print(s)
}()
}
wg.Wait()
逻辑分析:
ReadString内部反复调用fill()和原子更新buf.off;100 个 goroutine 无序推进off,造成读取位置跳跃或重叠,输出如"42","","ello123world"等撕裂片段。buf.n(有效字节数)亦被并发覆写,引发越界或漏读。
关键竞态字段对比
| 字段 | 读操作频率 | 写操作来源 | 撕裂表现 |
|---|---|---|---|
buf.off |
极高 | ReadString/ReadInt |
读取起始偏移错乱 |
buf.n |
高 | fill() |
缓冲区长度误判 |
graph TD
A[goroutine 1] -->|read off=5| B(buf.off)
C[goroutine 2] -->|write off=12| B
D[goroutine 3] -->|read off=12| B
B --> E[内存撕裂:跳过字节/重复解析]
2.4 基于go tool trace的goroutine阻塞与系统调用争用可视化验证
go tool trace 是 Go 运行时提供的深度可观测性工具,可捕获 Goroutine 调度、网络/系统调用、GC 等全生命周期事件。
启动带 trace 的程序
go run -gcflags="-l" -trace=trace.out main.go
# -gcflags="-l" 禁用内联,提升 goroutine 栈帧可读性
# trace.out 包含 nanosecond 级精度的运行时事件流
分析关键视图
- Goroutine analysis:识别长时间处于
runnable或syscall状态的 Goroutine - Network blocking:定位
netpoll阻塞点(如未就绪的 socket read) - Syscall duration heatmap:高亮 >1ms 的
read/write/accept调用
trace 可视化核心指标对比
| 视图类型 | 关注信号 | 典型阻塞诱因 |
|---|---|---|
| Goroutine view | 黄色“blocked”段持续 >10ms | channel send/receive 无接收者 |
| Syscall view | 红色长条(syscall exit → go sched) | epoll_wait 超时或 fd 就绪延迟 |
graph TD
A[main goroutine] -->|发起 http.Get| B[net/http transport]
B --> C[syscall: connect]
C --> D{fd 是否就绪?}
D -->|否| E[进入 netpoll wait 队列]
D -->|是| F[继续 write/read]
E --> G[被 runtime.sysmon 唤醒]
2.5 pprof火焰图中Registry读取热点定位:从user-space到kernel-mode的栈穿透解读
当 pprof 火焰图显示 registry.Get() 占用显著 CPU 时,需穿透至内核态验证是否受 copy_to_user 或 futex_wait 阻塞影响。
栈帧采样关键路径
- 用户态:
http.HandlerFunc → registry.Get → sync.RWMutex.RLock() - 内核态(perf record -e ‘syscalls:sys_enter_futex’):
do_futex → futex_wait → __wait_event_common
典型阻塞点识别
// 在 registry.Get 中加采样标记(非侵入式)
func (r *Registry) Get(key string) interface{} {
runtime.SetFinalizer(&key, func(_ *string) { /* noop */ }) // 触发 GC 栈快照锚点
r.mu.RLock()
defer r.mu.RUnlock()
return r.items[key]
}
该代码强制在 RLock 前插入 GC 可见的栈锚点,使 pprof 能对齐 perf 的 kernel stack trace。
| 工具 | 采样粒度 | 是否包含 kernel 栈 |
|---|---|---|
go tool pprof -http |
Goroutine-level | ❌(仅 user-space) |
perf script -F +kstack |
CPU-cycle | ✅(需 CONFIG_KALLSYMS) |
graph TD
A[pprof CPU profile] --> B[用户态 goroutine 栈]
C[perf record -e cycles,u] --> D[kernel + user callchain]
B --> E[栈对齐匹配]
D --> E
E --> F[定位 registry.Get → futex_wait → do_futex]
第三章:sync.RWMutex在注册表访问路径中的精准注入策略
3.1 读多写少场景下RWMutex vs Mutex的吞吐量对比基准测试(含QPS/latency/P99)
数据同步机制
在高并发读操作(如配置缓存、路由表查询)中,sync.RWMutex 允许多个 goroutine 并发读取,而 sync.Mutex 强制串行化所有操作。
基准测试代码片段
func BenchmarkRWMutexRead(b *testing.B) {
var rw sync.RWMutex
b.ResetTimer()
for i := 0; i < b.N; i++ {
rw.RLock() // 非阻塞读锁(仅当有活跃写锁时等待)
blackhole() // 模拟轻量读操作
rw.RUnlock()
}
}
RLock() 不阻塞其他读操作,但会阻塞写锁请求;b.N 由 go test 自动调整以保障统计稳定性。
性能对比(16核/32GB,1000并发)
| 锁类型 | QPS | Avg Latency (μs) | P99 Latency (μs) |
|---|---|---|---|
Mutex |
42,100 | 237 | 892 |
RWMutex |
189,600 | 53 | 176 |
关键结论
RWMutex在读占比 >95% 场景下 QPS 提升超 4.5×;- P99 延迟显著收敛,体现读操作的并行可扩展性。
3.2 注册表读取临界区边界界定:从RegOpenKeyEx到RegQueryValueEx的原子性范围推演
注册表读取操作并非天然原子——RegOpenKeyEx 与 RegQueryValueEx 分属两个独立系统调用,其间存在内核对象引用计数、句柄验证、ACL检查等非原子中间状态。
数据同步机制
临界区实际始于 RegOpenKeyEx 成功返回有效句柄(hKey),终于 RegQueryValueEx 完成数据拷贝并返回 ERROR_SUCCESS。二者间若发生并发写入或键删除,可能导致 ERROR_NO_MORE_ITEMS 或 ERROR_ACCESS_DENIED。
关键参数语义
samDesired: 控制访问权限粒度(如KEY_QUERY_VALUE);lpdwDisposition: 仅对RegCreateKeyEx有效,此处无关;dwFlags:REG_OPTION_OPEN_LINK影响符号链接解析时机,影响临界区起始点。
HKEY hKey;
LONG res = RegOpenKeyEx(HKEY_LOCAL_MACHINE, L"SOFTWARE\\MyApp", 0,
KEY_READ, &hKey); // ← 临界区起点:句柄有效且权限已验
if (res == ERROR_SUCCESS) {
DWORD dwType, dwSize = 0;
RegQueryValueEx(hKey, L"Version", NULL, &dwType, NULL, &dwSize); // ← 首次探查
BYTE* buf = malloc(dwSize);
RegQueryValueEx(hKey, L"Version", NULL, &dwType, buf, &dwSize); // ← 数据读取终点
RegCloseKey(hKey); // ← 临界区终结(但非原子性保障点)
}
逻辑分析:
RegOpenKeyEx返回成功仅表示句柄可被当前线程安全使用,不保证键路径持续存在;RegQueryValueEx的两次调用构成“查询-分配-读取”三阶段,第二次调用完成时才真正完成值读取的原子性边界。dwSize必须在首次调用后重置,否则缓冲区溢出风险。
| 阶段 | 系统行为 | 是否属于临界区 |
|---|---|---|
RegOpenKeyEx 返回前 |
权限检查、对象管理器查找 | 否(未获句柄) |
RegOpenKeyEx 返回后 |
句柄已注册,引用计数+1 | 是(起点) |
RegQueryValueEx 返回后 |
数据已复制至用户缓冲区 | 是(终点) |
graph TD
A[RegOpenKeyEx 开始] --> B[ACL验证 & 对象查找]
B --> C{成功?}
C -->|是| D[返回hKey → 临界区起点]
D --> E[RegQueryValueEx 第一次调用]
E --> F[获取值大小/类型]
F --> G[RegQueryValueEx 第二次调用]
G --> H[数据拷贝完成 → 临界区终点]
3.3 零侵入式锁封装:基于registry.Key接口的装饰器模式实现与泛型适配
核心设计思想
将锁生命周期与业务逻辑解耦,通过 registry.Key 抽象资源标识,使锁获取/释放对原方法零修改。
装饰器泛型实现
func WithLock[T any, K registry.Key](keyFn func(T) K) func(fn func(T) error) func(T) error {
return func(fn func(T) error) func(T) error {
return func(t T) error {
k := keyFn(t)
lock := registry.GetLock(k) // 基于Key动态获取租户/资源粒度锁
lock.Lock()
defer lock.Unlock()
return fn(t)
}
}
}
逻辑分析:
keyFn将业务参数T映射为唯一registry.Key(如UserKey{ID: t.UserID}),确保锁作用域精准;registry.GetLock内部按 Key 类型复用或新建锁实例,避免全局竞争。泛型约束K registry.Key保障类型安全与接口契约。
锁策略对比
| 策略 | 侵入性 | 粒度控制 | 泛型支持 |
|---|---|---|---|
| 手动加锁 | 高 | 弱 | ❌ |
| AOP代理 | 中 | 中 | ⚠️(需反射) |
| Key装饰器 | 零 | 强 | ✅ |
graph TD
A[业务函数] --> B[WithLock装饰器]
B --> C[Key提取]
C --> D[Registry锁池]
D --> E[Lock/Unlock]
E --> F[执行原函数]
第四章:修复方案的全链路验证与生产级加固实践
4.1 修复前后pprof火焰图对比分析:CPU时间分布、goroutine阻塞时长、系统调用频次三维度验证
CPU热点收敛效果
修复后 runtime.scanobject 占比从 38% 降至 5%,主因是避免了频繁的全堆扫描——改为增量式标记 + 脏对象队列驱动:
// 修复前(同步全堆扫描)
gcMarkRoots() // 阻塞式,扫描全部 roots + heap
// 修复后(异步增量标记)
gcMarkWorker() {
for !workQueue.empty() {
obj := workQueue.pop() // 仅处理已知脏对象
markObject(obj)
}
}
workQueue 由写屏障(write barrier)动态填充,显著降低单次 STW 时间。
goroutine阻塞与系统调用变化
| 维度 | 修复前 | 修复后 |
|---|---|---|
| 平均阻塞时长 | 127ms | 8.3ms |
read 系统调用频次 |
42k/s | 1.1k/s |
关键路径优化示意
graph TD
A[HTTP Handler] --> B{DB Query}
B --> C[旧:sync.Mutex + 全局连接池]
B --> D[新:per-conn context + io_uring 提交]
D --> E[内核异步完成通知]
4.2 压力测试场景设计:模拟500+并发goroutine持续读取HKEY_LOCAL_MACHINE\SOFTWARE\MyBank的稳定性压测
测试目标与约束
聚焦Windows注册表键 HKEY_LOCAL_MACHINE\SOFTWARE\MyBank 的高并发只读稳定性,排除写操作干扰,验证内核句柄复用、RegQueryValueExW调用吞吐及GC压力。
并发读取核心实现
func readMyBankKey(wg *sync.WaitGroup, ch chan<- error) {
defer wg.Done()
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\MyBank`, registry.READ)
if err != nil {
ch <- fmt.Errorf("open key failed: %w", err)
return
}
defer key.Close()
buf := make([]byte, 1024)
for i := 0; i < 100; i++ { // 每goroutine执行100次查询
_, _, err = key.GetStringValue("ConfigVersion")
if err != nil {
ch <- fmt.Errorf("get value failed: %w", err)
return
}
runtime.Gosched() // 避免调度饥饿
}
}
逻辑说明:使用
registry.OpenKey复用句柄(非每次重开),GetStringValue封装RegQueryValueExW;runtime.Gosched()确保公平调度;100次循环模拟持续负载。
压测参数配置
| 参数 | 值 | 说明 |
|---|---|---|
| 并发goroutine数 | 520 | 覆盖典型中型服务峰值 |
| 单goroutine请求量 | 100 | 防止单点超时掩盖资源泄漏 |
| 超时阈值 | 3s | 注册表I/O异常响应判定基准 |
执行流程
graph TD
A[启动520 goroutine] --> B[各自OpenKey一次]
B --> C[循环100次GetStringValue]
C --> D[错误通过channel聚合]
D --> E[统计成功率/99分位延迟]
4.3 eBPF辅助观测:通过tracepoint捕获Registry API调用序列,验证锁持有/释放时序正确性
为精准追踪服务注册中心(如Nacos/Eureka)中关键锁(registryLock)的生命周期,我们利用内核原生 tracepoint(如 syscalls/sys_enter_futex 和 sched:sched_lock_acquire)挂载eBPF程序,实现无侵入式观测。
核心eBPF探测逻辑
// trace_lock_events.c —— 捕获futex_wait/futex_wake及锁事件
SEC("tracepoint/sched/sched_lock_acquire")
int trace_lock_acquire(struct trace_event_raw_sched_lock_acquire *ctx) {
if (bpf_strncmp(ctx->lock_name, 12, "registryLock") == 0) {
u64 ts = bpf_ktime_get_ns();
bpf_map_update_elem(&lock_start, &pid, &ts, BPF_ANY);
}
return 0;
}
逻辑说明:
sched_lock_acquiretracepoint在内核锁获取路径触发;lock_name字段需提前确认是否暴露(部分内核需启用CONFIG_LOCKDEP=y);bpf_map_update_elem以PID为键记录锁获取时间戳,供后续时序比对。
关键观测维度对比
| 维度 | 正常时序 | 异常信号 |
|---|---|---|
| 锁持有时长 | > 200ms(可能阻塞) | |
| acquire→release间隔 | 单次调用内严格成对 | 缺失release事件(死锁风险) |
| 跨线程重入 | 同PID重复acquire被忽略 | 不同PID高频acquire(竞争激增) |
时序验证流程
graph TD
A[tracepoint捕获acquire] --> B{PID存在start记录?}
B -->|是| C[记录release时间戳]
B -->|否| D[告警:无匹配acquire]
C --> E[计算Δt并写入ringbuf]
E --> F[用户态校验Δt ∈ [0, 200ms]]
4.4 金融级SLA保障:引入registry.ReadTimeout上下文控制与panic recovery兜底机制
在高一致性要求的金融场景中,服务注册中心的瞬时不可用可能导致全链路雪崩。为此,我们强化了客户端容错能力。
上下文超时控制
ctx, cancel := context.WithTimeout(context.Background(), registry.ReadTimeout)
defer cancel()
resp, err := client.Get(ctx, key) // 阻塞调用受ReadTimeout约束
registry.ReadTimeout(默认800ms)由全局配置注入,避免goroutine永久阻塞;context.WithTimeout确保即使底层连接未关闭,调用也能准时退出。
panic 自愈机制
采用 recover() + 熔断标记双保险:
- 每次 registry 操作包裹 defer-recover;
- 捕获 panic 后立即标记
isRegistryUnstable = true,后续请求快速失败( - 启动后台 goroutine 每3s探测恢复状态。
| 机制 | 触发条件 | 响应延迟 | SLA影响 |
|---|---|---|---|
| ReadTimeout | 网络抖动/etcd慢读 | ≤800ms | 可控 |
| Panic Recovery | 序列化崩溃/空指针 | 零感知 |
graph TD
A[Registry调用] --> B{panic?}
B -->|是| C[recover → 标记不稳定]
B -->|否| D[正常返回]
C --> E[快速失败熔断]
D --> F[更新健康状态]
第五章:从单点修复到Go生态注册表安全规范的演进建议
Go模块生态长期依赖proxy.golang.org和sum.golang.org双服务模型,但2023年某金融基础设施团队遭遇的供应链攻击暴露了根本性缺陷:攻击者通过劫持已归档的第三方模块(github.com/legacy-utils/jsoncodec@v1.2.0)的Git仓库并篡改其go.mod中replace指令,诱导下游项目在未启用GOPROXY=direct时拉取恶意构建产物——而校验和服务器仅验证zip哈希,对go.mod内动态替换逻辑无感知。
重构校验和验证机制
当前sum.golang.org仅存储<module>@<version>对应.zip与.mod文件的SHA256哈希。建议强制扩展为三元组校验:
zip_hash(原始归档完整性)mod_hash(模块定义文件一致性)replace_hash(若go.mod含replace指令,则对其目标路径、版本、校验和生成派生哈希)
该机制已在CNCF沙箱项目goreleaser-provenance中验证,可拦截92%的replace注入类攻击。
引入模块签名注册表协议
参考Sigstore的Fulcio+Rekor架构,设计轻量级Go模块签名层:
# 签名流程示例
cosign sign-blob \
--oidc-issuer https://accounts.google.com \
--output-signature jsoncodec-v1.2.0.sig \
go.sum # 对go.sum生成签名而非源码
签名需绑定至模块版本元数据,并由注册表在/v2/{module}/versions/{version}/attestation端点提供查询。
建立分层信任锚点体系
| 信任层级 | 运营主体 | 验证要求 | 生效范围 |
|---|---|---|---|
| 核心模块 | Go Team | 人工审计+CI流水线签名 | std, net/http等标准库 |
| 认证供应商 | CNCF/OWASP | 自动化SBOM扫描+模糊测试报告 | cloud.google.com/go, github.com/aws/aws-sdk-go |
| 社区模块 | 模块所有者 | 至少1个Cosign签名+Rekor存证 | 所有非认证模块 |
2024年Q2,Kubernetes SIG-Auth在k8s.io/client-go v0.29.0发布中试点该模型,将模块签名嵌入go.mod注释区(// signed-by: k8s-sig-auth@k8s.io),使go get客户端可自动触发远程验证。
强制透明日志审计能力
所有模块版本发布必须写入公开可查的Merkle Tree日志(如Rekor实例),日志条目包含:
- 模块路径与版本
.zip与.mod哈希- 签名证书指纹
- 提交时间戳(UTC纳秒精度)
Go工具链应默认启用GOSUMDB=off时的本地日志验证模式,拒绝未出现在指定日志中的版本。
构建开发者友好的安全反馈环
当go list -m -u -f '{{.Path}} {{.Version}}' all检测到高危模块时,go mod tidy应输出结构化告警:
{
"module": "github.com/legacy-utils/jsoncodec",
"version": "v1.2.0",
"vulnerability_id": "GHSA-xxxx-xxxx-xxxx",
"remediation": "replace github.com/legacy-utils/jsoncodec => github.com/safe-utils/jsoncodec v2.1.0",
"log_entry": "https://rekor.sigstore.dev/api/v1/log/entries/7a8b9c"
}
该格式已被VS Code Go插件v0.38.0原生支持,点击即可跳转验证页面。
推动企业级私有注册表合规适配
大型机构部署的私有代理(如JFrog Artifactory Go Registry)需实现/v2/{module}/versions/{version}/provenance端点,返回符合SLSA Level 3的构建证明。某国有银行在2024年3月完成改造,其内部Go模块仓库已强制要求所有v2+版本提交SBOM(SPDX JSON格式)及构建环境快照哈希。
制定渐进式迁移路线图
Go社区应设立三年过渡期:第一年要求所有新发布模块提供签名;第二年将签名验证设为go get默认行为(可设GOINSECURE例外);第三年废止无签名模块的GOPROXY缓存能力。此路径已在GopherCon EU 2024开发者投票中获87%支持率。
