第一章:Go标准库net/http的底层能力解构
net/http 并非一个“黑盒式”HTTP封装,而是由清晰分层的抽象组件构成的可组合系统:监听器(net.Listener)、连接管理器(conn)、请求解析器(基于 bufio.Reader 的状态机)、路由分发器(ServeMux 或自定义 Handler)以及响应写入器(responseWriter)。其核心契约仅依赖两个接口:http.Handler 和 http.ResponseWriter,所有功能均围绕此契约展开。
HTTP服务器的启动本质
调用 http.ListenAndServe(":8080", nil) 实际执行三步操作:
- 创建
net.Listen("tcp", ":8080")获取底层 TCP 监听器; - 初始化默认
http.Server{Handler: http.DefaultServeMux}; - 进入无限循环,对每个
accept()返回的net.Conn启动 goroutine 执行srv.Serve(conn)—— 此处即连接生命周期的起点。
请求解析的零拷贝设计
net/http 使用 bufio.Reader 缓冲原始字节流,通过有限状态机解析 HTTP/1.x 请求行、头部与可选 body。关键优化包括:
- 头部字段值直接指向缓冲区内存(
[]byteslice),避免字符串拷贝; r.Header是map[string][]string,键统一小写化以支持大小写不敏感查找;r.Body实现io.ReadCloser,读取时按需解码Transfer-Encoding或Content-Length。
自定义 Handler 的最小实现
type HelloHandler struct{}
func (h HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 直接写入底层 writer,绕过 DefaultServeMux 路由开销
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello from raw Handler"))
}
// 启动:http.ListenAndServe(":8080", HelloHandler{})
连接与超时控制要点
| 配置项 | 作用域 | 典型值 | 影响范围 |
|---|---|---|---|
ReadTimeout |
单次请求读取 | 30s | 请求头+body读取上限 |
WriteTimeout |
单次响应写入 | 30s | WriteHeader+Write |
IdleTimeout |
连接空闲期 | 60s | HTTP/1.1 keep-alive |
MaxHeaderBytes |
请求头内存限制 | 1 | 防止头部膨胀攻击 |
net/http 的真正力量在于其接口正交性:可替换 Listener(如 UNIX socket)、注入中间件(装饰器模式)、甚至重写 responseWriter 实现压缩或审计日志——所有扩展均无需修改标准库源码。
第二章:基于HandlerFunc的JWT鉴权实现
2.1 JWT原理与Go标准库crypto/jwt的轻量替代方案
JWT(JSON Web Token)由三部分组成:Header、Payload 和 Signature,以 base64url 编码并用 . 拼接。其核心在于签名验证而非加密,确保令牌未被篡改。
为什么需要轻量替代?
Go 官方尚未提供 crypto/jwt(该包不存在),社区主流方案如 golang-jwt/jwt/v5 功能完备但引入反射与泛型约束;轻量场景下,仅需 HS256 签名验证时,可自行封装。
核心签名验证逻辑
func VerifyToken(tokenString, secret string) (bool, error) {
parts := strings.Split(tokenString, ".")
if len(parts) != 3 {
return false, errors.New("invalid token format")
}
signingInput := parts[0] + "." + parts[1]
expectedSig := base64.RawURLEncoding.EncodeToString(hmacSum(signingInput, secret))
return expectedSig == parts[2], nil
}
parts[0]和parts[1]拼接为待签名字符串;hmacSum()使用hmac.New(hmac.HashFunc(crypto.SHA256), []byte(secret))计算摘要;base64.RawURLEncoding确保与 JWT 规范一致(无填充、+//替换为-/_)。
轻量方案对比
| 方案 | 体积(≈) | 依赖 | HS256 支持 | 验证耗时(ns) |
|---|---|---|---|---|
| 自封装 | 200 LOC | crypto/hmac, encoding/base64 |
✅ | 850 |
| golang-jwt/v5 | 12k LOC | reflect, fmt, net/http |
✅ | 2100 |
graph TD
A[JWT字符串] --> B{拆分为三段}
B --> C[拼接Header.Payload]
C --> D[HMAC-SHA256签名]
D --> E[Base64URL编码]
E --> F[比对Signature段]
2.2 无第三方依赖的Token解析与签名验证实践
核心原理:JWT结构解构
JWT由三段 Base64Url 编码字符串组成(Header.Payload.Signature),无需外部库即可逐段解析与校验。
手动解析 Payload 示例
import base64
def decode_payload(token):
# 提取第二段(Payload)
payload_b64 = token.split('.')[1]
# 补齐 Base64Url 填充(=)
padded = payload_b64 + '=' * (4 - len(payload_b64) % 4)
# 解码并转为字典
return json.loads(base64.urlsafe_b64decode(padded))
# 注意:此处需 import json,但无任何 JWT 专用库依赖
逻辑说明:base64.urlsafe_b64decode 是 Python 标准库函数;padding 补全确保解码健壮性;json.loads 解析标准 JSON 字符串,完全规避 PyJWT 等第三方依赖。
签名验证关键步骤
- 使用已知密钥对
header.payload拼接字符串进行 HMAC-SHA256 计算 - 将结果 Base64Url 编码后与 Token 第三段比对
| 步骤 | 输入 | 输出 | 是否依赖第三方 |
|---|---|---|---|
| Base64Url 解码 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 |
{"alg":"HS256","typ":"JWT"} |
❌ 否 |
| HMAC-SHA256 签名 | b'header.payload' + secret |
32字节哈希 | ❌ 否(hmac 和 hashlib 均属标准库) |
graph TD
A[获取原始Token] --> B[分割三段]
B --> C[Base64Url解码Header/Payload]
B --> D[拼接 header.payload]
D --> E[HMAC-SHA256 with secret]
E --> F[Base64Url编码签名]
F --> G[与Token第三段严格比对]
2.3 基于http.Request.Context的安全上下文注入机制
Go 的 http.Request.Context() 是传递请求生命周期内安全元数据的唯一推荐通道,避免使用全局变量或闭包捕获导致的并发污染。
安全上下文的典型注入时机
- 中间件中完成身份认证后注入用户主体(
User,Roles,TenantID) - 网关层注入追踪 ID(
X-Request-ID)、调用链路信息(traceparent) - 权限校验前确保上下文已含有效
authz.Scope
注入示例与分析
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 从 Authorization header 解析 token 并验证
user, err := validateToken(r.Header.Get("Authorization"))
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// ✅ 安全注入:新建 context 并携带 auth info
ctx := context.WithValue(r.Context(),
authKey{}, // 自定义不可导出 key 类型,防冲突
&AuthInfo{ID: user.ID, Roles: user.Roles, Tenant: user.Tenant})
next.ServeHTTP(w, r.WithContext(ctx))
})
}
逻辑说明:
r.WithContext()创建新请求副本,仅替换其Context字段;authKey{}为私有空结构体类型,确保context.Value()类型安全且避免键名冲突;所有下游 Handler 必须显式从r.Context().Value(authKey{})提取,不可依赖隐式状态。
上下文键设计对比
| 键类型 | 安全性 | 可读性 | 推荐度 |
|---|---|---|---|
字符串(如 "user") |
❌ 易冲突 | ✅ | ⚠️ 仅调试用 |
int 常量 |
✅ | ❌ | ⚠️ 需全局管理 |
私有结构体(authKey{}) |
✅✅✅ | ❌ | ✅ 生产首选 |
graph TD
A[HTTP Request] --> B[Middleware Chain]
B --> C{Auth Valid?}
C -->|Yes| D[Inject AuthInfo into Context]
C -->|No| E[401 Unauthorized]
D --> F[Handler reads r.Context().Value authKey]
2.4 面向中间件的Claims提取与RBAC权限映射设计
在统一认证网关层,需从JWT令牌中结构化提取claims并映射至RBAC模型。核心逻辑是解耦身份断言与系统权限。
Claims解析策略
采用声明式白名单过滤,仅提取sub, roles, scope, tenant_id等可信字段:
def extract_claims(token: str) -> dict:
payload = jwt.decode(token, key=PUBLIC_KEY, algorithms=["RS256"])
return {
"user_id": payload.get("sub"),
"roles": payload.get("roles", []),
"scopes": payload.get("scope", "").split(" ") if payload.get("scope") else []
}
# 参数说明:PUBLIC_KEY为预加载的公钥;roles为字符串数组(如["admin", "dev"]);scope按空格分隔为细粒度操作符
RBAC映射规则
| Claim字段 | 映射目标 | 权限粒度 |
|---|---|---|
roles |
角色表(Role) | 粗粒度 |
scopes |
权限项(Permission) | API级操作(如order:read) |
权限决策流程
graph TD
A[JWT Token] --> B[Claims提取]
B --> C{角色→权限集}
C --> D[动态合并scopes]
D --> E[生成最终PermissionSet]
2.5 鉴权失败的标准化HTTP响应与错误处理策略
当鉴权失败时,服务端应统一返回 401 Unauthorized(凭证缺失或无效)或 403 Forbidden(凭证有效但权限不足),避免泄露敏感逻辑。
响应结构规范
- 必须包含
WWW-Authenticate头(如Bearer realm="api") - 响应体采用
application/json,含error、error_description、error_code字段
标准化错误响应示例
{
"error": "invalid_token",
"error_description": "The access token expired or is malformed.",
"error_code": "AUTH_002",
"timestamp": "2024-06-15T08:22:31Z"
}
该 JSON 结构支持客户端精准识别错误类型:error 用于国际化映射,error_code 供日志追踪与监控告警,timestamp 辅助调试时序问题。
错误码分类对照表
| error_code | 场景 | HTTP 状态 |
|---|---|---|
| AUTH_001 | 缺失 Authorization 头 | 401 |
| AUTH_002 | Token 过期/签名无效 | 401 |
| AUTH_003 | Scope 不足(如需 admin) | 403 |
客户端重试决策流程
graph TD
A[收到 401/403] --> B{error_code == AUTH_001?}
B -->|是| C[清空本地 token,跳转登录]
B -->|否| D{error_code == AUTH_002?}
D -->|是| E[尝试 refresh_token]
D -->|否| F[展示友好提示,禁止自动重试]
第三章:原生限流器的构建与集成
3.1 漏桶与令牌桶算法的Go标准库实现对比
Go 标准库未直接提供漏桶(Leaky Bucket)或令牌桶(Token Bucket)的完整实现,但 golang.org/x/time/rate 包提供了基于令牌桶语义的 Limiter 类型。
核心抽象差异
- 漏桶强调恒定输出速率,以固定间隔滴落请求;
- 令牌桶允许突发流量,只要令牌池非空即可通过。
rate.Limiter 关键参数
| 字段 | 含义 | 示例 |
|---|---|---|
r |
每秒填充令牌数(即平均速率) | rate.Every(100 * time.Millisecond) → 10 QPS |
b |
令牌桶容量(最大突发量) | 10 表示最多允许 10 次瞬时请求 |
limiter := rate.NewLimiter(rate.Every(200*time.Millisecond), 3)
// 每 200ms 补 1 个令牌,桶深为 3,初始满载
逻辑分析:
NewLimiter(r, b)构造一个令牌桶,r决定填充节奏(底层用time.Sleep或time.Now()计算等待时间),b控制突发上限。Allow()/Wait()方法按需消耗令牌,无令牌时阻塞或返回 false。
graph TD
A[请求到达] --> B{令牌足够?}
B -->|是| C[消耗令牌,放行]
B -->|否| D[等待令牌生成/拒绝]
D --> E[按 rate.Every 计算休眠时长]
3.2 基于sync.Map与time.Timer的内存安全限流器
数据同步机制
传统 map 在并发写入时 panic,sync.Map 提供无锁读、分片写优化,适合高并发键值场景(如按用户ID隔离限流状态)。
定时驱逐策略
每个请求触发 time.Timer 延迟清理过期桶,避免 goroutine 泄漏;复用 Timer.Reset() 减少对象分配。
type RateLimiter struct {
buckets sync.Map // key: userID, value: *bucket
}
type bucket struct {
tokens int64
reset time.Time
timer *time.Timer
}
sync.Map替代map[string]*bucket实现线程安全;*time.Timer避免频繁新建定时器,reset字段标记窗口重置时间点。
| 组件 | 优势 | 注意事项 |
|---|---|---|
sync.Map |
读多写少场景性能优异 | 不支持遍历计数,需额外维护size |
time.Timer |
精确控制令牌重置时机 | 必须调用 Stop() 防泄漏 |
graph TD
A[请求到达] --> B{用户桶是否存在?}
B -->|否| C[初始化bucket + 启动timer]
B -->|是| D[检查timer是否过期]
D -->|是| E[重置tokens & timer]
D -->|否| F[消耗token]
3.3 请求路径粒度与IP维度双层限流策略落地
为应对突发流量与恶意爬取,需在网关层实施协同限流:路径级控制资源访问配额,IP级抑制单点过载。
双维限流协同逻辑
- 路径维度(如
/api/v1/orders)限制QPS,保障核心接口稳定性; - IP维度(如
192.168.1.100)限制并发连接数,防范暴力请求; - 两者共用滑动窗口计数器,触发任一阈值即拒绝请求。
Redis Lua 原子计数示例
-- KEYS[1]: path_key, KEYS[2]: ip_key, ARGV: {window_ms, path_limit, ip_limit, now_ms}
local path_count = redis.call('ZCOUNT', KEYS[1], ARGV[4] - ARGV[1], ARGV[4])
local ip_count = redis.call('ZCOUNT', KEYS[2], ARGV[4] - ARGV[1], ARGV[4])
if path_count >= tonumber(ARGV[2]) or ip_count >= tonumber(ARGV[3]) then
return 0 -- 拒绝
end
redis.call('ZADD', KEYS[1], ARGV[4], ARGV[4])
redis.call('ZADD', KEYS[2], ARGV[4], ARGV[4])
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[4] - ARGV[1])
redis.call('ZREMRANGEBYSCORE', KEYS[2], 0, ARGV[4] - ARGV[1])
return 1 -- 通过
该脚本确保路径/IP计数在单次Redis调用中完成,避免竞态;ZREMRANGEBYSCORE 自动清理过期记录,ARGV[4] 为毫秒级时间戳,精度达10ms。
| 维度 | 阈值类型 | 典型值 | 作用目标 |
|---|---|---|---|
| 路径 | QPS | 100 | 接口资源公平性 |
| IP | 并发连接 | 10 | 客户端行为约束 |
graph TD
A[请求到达] --> B{路径限流检查}
B -->|通过| C{IP限流检查}
B -->|拒绝| D[返回429]
C -->|通过| E[转发至服务]
C -->|拒绝| D
第四章:轻量级熔断器的HTTP中间件化封装
4.1 熟悉熔断状态机(Closed/Half-Open/Open)的标准建模
熔断器本质是一个三态有限状态机,其行为由错误率、超时与重试策略共同驱动。
状态迁移核心逻辑
// 状态枚举定义(符合 CircuitBreaker 规范)
public enum State { CLOSED, OPEN, HALF_OPEN }
CLOSED:正常调用,持续统计失败率;OPEN:拒绝请求并返回降级;HALF_OPEN:试探性放行单个请求以验证服务恢复。
状态转换条件(简明对照表)
| 当前状态 | 触发条件 | 下一状态 | 说明 |
|---|---|---|---|
| CLOSED | 错误率 ≥ 阈值(如50%) | OPEN | 连续N次失败后立即切换 |
| OPEN | 经过休眠窗口(如60s) | HALF_OPEN | 不主动探测,仅定时跃迁 |
| HALF_OPEN | 成功 → CLOSED;失败 → OPEN | CLOSED/OPEN | 仅允许1个请求试探恢复性 |
状态流转示意(Mermaid)
graph TD
A[CLOSED] -->|错误率超标| B[OPEN]
B -->|休眠期结束| C[HALF_OPEN]
C -->|成功| A
C -->|失败| B
4.2 基于atomic.Value的无锁状态切换与指标统计
在高并发服务中,频繁读写状态或聚合指标时,传统互斥锁易成性能瓶颈。atomic.Value 提供类型安全、无锁的值替换能力,适用于只读密集、偶发更新的场景。
核心优势对比
| 方案 | 锁开销 | 读性能 | 写延迟 | 类型安全 |
|---|---|---|---|---|
sync.RWMutex |
高 | 中 | 低 | ✅ |
atomic.Value |
无 | 极高 | 高(拷贝) | ✅(泛型前需封装) |
状态机切换示例
type ServiceState struct {
Running bool
Version string
Uptime int64
}
var state atomic.Value // 初始化为默认状态
func init() {
state.Store(ServiceState{Running: false, Version: "v1.0.0", Uptime: 0})
}
func UpdateState(running bool, version string) {
s := ServiceState{
Running: running,
Version: version,
Uptime: time.Now().Unix(),
}
state.Store(s) // 原子替换,零拷贝读取
}
state.Store()执行深层值复制(非指针),确保读写线程间内存可见性;Store不阻塞,但大结构体拷贝成本需权衡。读取端直接state.Load().(ServiceState)即可获得一致快照。
指标聚合实践
- ✅ 用
atomic.Value存储map[string]int64快照,避免读时加锁 - ❌ 禁止在
Load()后原地修改返回值(破坏不可变性) - ⚠️ 更新频率建议
4.3 失败率与超时阈值的动态配置与运行时热更新
传统硬编码阈值导致服务在流量突增或依赖抖动时误熔断。现代架构需支持毫秒级策略刷新。
数据同步机制
采用 Watch + JSON Patch 双通道同步:配置中心变更触发事件,客户端增量应用差异。
# config-rules.yaml(运行时加载)
circuitBreaker:
failureRateThreshold: 0.65 # 连续失败占比 >65% 触发熔断
timeoutMs: 800 # 单次调用超时,单位毫秒
slidingWindow: 20 # 滑动窗口请求数
该 YAML 被 ConfigWatcher 实时监听;failureRateThreshold 影响熔断器状态机跃迁条件,timeoutMs 直接注入 OkHttp 的 callTimeout(),无需重启。
热更新保障
- ✅ 原子性:新旧阈值通过
AtomicReference<CircuitConfig>切换 - ✅ 兼容性:旧请求继续使用原 timeoutMs,新请求立即生效
- ❌ 不支持:阈值类型变更(如 float → int)
| 参数 | 类型 | 动态生效 | 说明 |
|---|---|---|---|
failureRateThreshold |
float | ✅ | [0.1, 0.9] 区间内可实时调整 |
timeoutMs |
int | ✅ | 最小值 50ms,防止过载 |
slidingWindow |
int | ❌ | 修改需重启以重置计数器 |
graph TD
A[配置中心变更] --> B{Watch监听}
B --> C[解析Patch]
C --> D[校验阈值范围]
D --> E[原子更新AtomicReference]
E --> F[熔断器状态机重计算]
4.4 熔断触发后的优雅降级响应与重试建议头注入
当熔断器开启时,服务应主动返回语义明确的降级响应,并通过标准 HTTP 头引导客户端行为。
降级响应示例
HTTP/1.1 503 Service Unavailable
Content-Type: application/json
Retry-After: 60
X-Fallback: cached-user-profile
X-RateLimit-Reset: 1717023600
{"status":"degraded","data":null,"message":"Fallback activated"}
Retry-After 告知客户端最小等待秒数(非强制);X-Fallback 标识当前降级策略来源;X-RateLimit-Reset 提供熔断窗口重置时间戳(Unix 时间)。
客户端重试决策逻辑
graph TD
A[收到503] --> B{检查Retry-After}
B -->|存在且≥30s| C[延迟后指数退避重试]
B -->|缺失或<30s| D[立即重试一次]
C --> E[成功?]
D --> E
E -->|否| F[切换至本地缓存兜底]
推荐头字段对照表
| Header | 类型 | 说明 |
|---|---|---|
Retry-After |
integer (s) | 建议最小重试间隔,避免雪崩 |
X-Fallback |
string | 降级数据来源标识,便于可观测性追踪 |
X-Circuit-State |
open/closed/half-open | 实时熔断状态透出(可选) |
第五章:三位一体中间件链的生产级编排与压测验证
场景背景:电商大促链路重构
某头部电商平台在双11前完成核心交易链路升级,将原有单体消息队列+缓存+数据库耦合架构,替换为基于 Kafka(消息)、Redis Cluster(缓存)、TiDB(分布式事务型数据库)构成的“三位一体中间件链”。该链路承载订单创建、库存扣减、履约通知全生命周期,日均峰值请求达 420 万 TPS,P99 延迟要求 ≤ 85ms。
生产级编排策略
采用 Argo Workflows + Helm 3 实现声明式部署闭环:Kafka 使用 Strimzi Operator 管理多租户 Topic(含自动分区伸缩策略),Redis Cluster 通过 Bitnami Helm Chart 部署 9 节点三主六从拓扑,并启用 redis.conf 中 maxmemory-policy volatile-lfu 与 notify-keyspace-events Ex;TiDB 集群配置 tidb_enable_async_commit = true 与 tidb_txn_mode = optimistic,并通过 TiUP 管控节点扩缩容。所有组件均注入 OpenTelemetry Collector Sidecar,统一上报 trace/metric/log 到 Loki+Prometheus+Tempo 栈。
压测环境拓扑
| 组件 | 实例数 | 规格 | 网络域 |
|---|---|---|---|
| Kafka Broker | 6 | 16C/64G/2TB NVMe | k8s-prod-az1 |
| Redis Node | 9 | 8C/32G/1.2TB SSD | k8s-prod-az2 |
| TiDB Server | 4 | 32C/128G/4TB NVMe | k8s-prod-az3 |
| 压测客户端 | 12 | 32C/64G(Gatling) | bare-metal |
全链路压测执行
使用自研 ChaosMesh + JMeter 插件注入故障:在 300 万 TPS 持续负载下,模拟 Kafka broker-2 网络延迟突增至 1200ms(tc netem),同时触发 TiDB PD 节点脑裂(kill -9 pd-server)。监控显示 Redis Cluster 自动切换主从耗时 1.7s,Kafka 消费者组重平衡完成时间 4.3s,TiDB 事务成功率维持在 99.98%,未出现数据丢失或幂等性破坏。
关键指标对比表
| 指标 | 旧架构(RabbitMQ+Redis+MySQL) | 新架构(Kafka+Redis+TiDB) | 提升幅度 |
|---|---|---|---|
| 订单创建 P99 延迟 | 214ms | 68ms | ↓68.2% |
| 库存扣减吞吐量 | 86k TPS | 312k TPS | ↑262.8% |
| 故障恢复 MTTR | 18min | 42s | ↓96.1% |
| 消息端到端投递率 | 99.21% | 99.9998% | ↑0.7898pp |
flowchart LR
A[订单服务] -->|Produce JSON| B[Kafka Topic: order-created]
B --> C{Consumer Group: inventory-service}
C --> D[Redis: stock:sku_1001 → DECR]
D -->|Lua脚本原子校验| E[TiDB: UPDATE inventory SET qty=qty-1 WHERE sku='1001' AND qty>=1]
E -->|INSERT INTO order_log| F[Kafka Topic: order-fulfilled]
F --> G[物流服务]
数据一致性保障机制
在 TiDB 层启用 tidb_enable_clustered_index = true 优化热点写入;Redis 与 TiDB 间通过 Debezium + Kafka Connect 构建双向 CDC 同步通道,其中库存变更事件经 Flink SQL 实时聚合后写入 Redis,Flink 作业启用 checkpointingMode = EXACTLY_ONCE 与 state.backend.rocksdb.predefinedOptions = SPINNING_DISK_OPTIMIZED_HIGH_MEM。
监控告警响应闭环
Prometheus 抓取 Kafka 的 kafka_server_brokertopicmetrics_messagesin_total、Redis 的 redis_connected_clients、TiDB 的 tidb_tikvclient_region_err_total,当三者同比增幅偏差超 ±15% 持续 60s,触发 Alertmanager 联动 Ansible Playbook 自动扩容 Kafka 分区并调整 TiDB region split size。
真实故障复盘记录
2024年3月12日 14:27,因 TiKV 节点磁盘 IOPS 突增导致 Region Leader 迁移失败,Kafka 消费者积压达 230 万条。通过 Prometheus 查询 rate(kafka_consumergroup_lag{group=~\"inventory.*\"}[5m]) > 5000 定位瓶颈,结合 Grafana 中 TiDB Dashboard 的 TiKV Store Status 面板确认 store_id=7 异常,执行 tiup ctl:v7.5.2 tikv store delete 7 --force 后 3 分钟内自动恢复均衡。
