第一章:Golang数据抽取服务资源占用超标现象剖析
在高并发数据抽取场景中,Golang服务常出现CPU持续高于85%、RSS内存占用突破2GB阈值的异常现象,尤其在处理TB级MySQL分页拉取或Kafka批量消费任务时尤为显著。该问题并非源于单点逻辑缺陷,而是多个运行时行为叠加导致的资源泄漏与低效调度。
常见诱因识别
- goroutine 泄漏:未正确关闭的
time.Ticker或HTTP长连接监听器持续 spawn 新 goroutine; - 内存逃逸加剧:频繁使用
fmt.Sprintf拼接大字符串、切片未预分配容量导致底层数组反复扩容; - 同步原语误用:全局
sync.Mutex保护高频写入字段,引发goroutine排队阻塞,间接抬升调度开销; - GC压力失衡:
runtime.ReadMemStats显示NextGC增长缓慢但HeapAlloc持续攀升,表明对象生命周期过长。
实时诊断方法
通过以下命令组合快速定位瓶颈:
# 查看TOP 5 CPU占用goroutine(需启用pprof)
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
(pprof) top5 -cum
# 检查内存分配热点(采样10秒)
go tool pprof http://localhost:6060/debug/pprof/heap
(pprof) list main.ExtractData
关键修复实践
对分页抽取逻辑进行重构,避免隐式内存累积:
// ❌ 低效:每次循环创建新切片,触发多次堆分配
for page := 1; page <= totalPages; page++ {
rows := db.QueryRows("SELECT * FROM logs LIMIT ? OFFSET ?", pageSize, (page-1)*pageSize)
result = append(result, rows...) // 底层可能多次copy
}
// ✅ 优化:预分配+复用缓冲区
result := make([]LogRecord, 0, totalExpectedCount) // 一次分配
buffer := make([]LogRecord, pageSize) // 复用缓冲
for page := 1; page <= totalPages; page++ {
n := db.QueryRowsInto(buffer, "SELECT * FROM logs LIMIT ? OFFSET ?", pageSize, (page-1)*pageSize)
result = append(result, buffer[:n]...)
}
| 指标 | 修复前平均值 | 修复后平均值 | 改善幅度 |
|---|---|---|---|
| Goroutine数 | 12,480 | 890 | ↓92.8% |
| RSS内存 | 2.3 GB | 410 MB | ↓82.2% |
| P99响应延迟 | 3.7 s | 420 ms | ↓88.6% |
第二章:cgroup v2在Go数据抽取服务中的精细化资源隔离实践
2.1 cgroup v2核心机制与v1关键差异的理论解析与实测对比
cgroup v2 采用统一层级(unified hierarchy),所有控制器必须挂载到同一挂载点,彻底摒弃 v1 中多挂载、资源竞争与嵌套冲突问题。
统一控制模型
v1 允许 cpu 和 memory 分别挂载,导致配置不一致;v2 强制单树管理:
# v2 正确挂载(所有控制器启用)
mount -t cgroup2 none /sys/fs/cgroup
# v1 禁用:mount -t cgroup cpu,cpuacct /sys/fs/cgroup/cpu
逻辑分析:cgroup2 类型挂载自动激活全部启用控制器(需内核配置 CONFIG_CGROUPS=y 且 CONFIG_CGROUP_V2=y);参数 none 为占位设备名,符合 VFS 挂载规范。
关键差异对比
| 维度 | cgroup v1 | cgroup v2 |
|---|---|---|
| 层级结构 | 多挂载点、独立树 | 单挂载点、统一树 |
| 控制器启用 | 按需挂载(如 cpu, memory) |
启用/禁用通过 cgroup.subtree_control |
| 进程迁移 | 可跨控制器移动(易失配) | 原子迁移,父子组强约束 |
资源委派机制
v2 引入 cgroup.procs 与 cgroup.threads 分离语义,支持细粒度线程组管理。
2.2 基于systemd+unit文件动态配置CPU带宽与内存上限的工程落地
核心机制:cgroup v2 + systemd.slice 协同管控
systemd 通过 .slice 单元原生绑定 cgroup v2 资源控制器,无需手动挂载或修改 /sys/fs/cgroup。
配置示例:动态限制服务资源
# /etc/systemd/system/myapp.slice
[Unit]
Description=MyApp Resource Slice
[Slice]
CPUQuota=60% # 硬性限制:最多使用6核中的3.6核(按100%为单核等效)
MemoryMax=2G # 内存硬上限,超出触发OOM Killer
IOWeight=50 # 相对I/O优先级(默认100)
逻辑分析:
CPUQuota=60%实际映射为 cgroup v2 的cpu.max = 60000 100000(配额/周期),即每100ms周期内最多运行60ms;MemoryMax直接写入memory.max,启用内存压力检测与自动回收。
关键参数对照表
| 参数 | cgroup v2 文件 | 行为特性 |
|---|---|---|
CPUQuota |
cpu.max |
周期性带宽限制,支持毫秒级精度 |
MemoryMax |
memory.max |
触发直接OOM而非swap回退 |
生产就绪流程
- 修改 unit 文件后执行
sudo systemctl daemon-reload - 启用切片:
sudo systemctl start myapp.slice - 将目标服务
WantedBy=myapp.slice并重启
graph TD
A[service启动] --> B[systemd解析WantedBy]
B --> C[自动加入对应.slice cgroup]
C --> D[内核cgroup v2控制器实时限流]
2.3 使用runc+OCI runtime实现容器化抽取任务的cgroup v2原生绑定
在 cgroup v2 统一层级模型下,runc 通过 OCI runtime spec 直接声明资源约束,无需 systemd 或中间代理。
cgroup v2 原生挂载验证
# 确认系统启用 cgroup v2(unified hierarchy)
mount | grep cgroup
# 输出应含:cgroup2 on /sys/fs/cgroup type cgroup2 (rw,seclabel,nsdelegate)
该命令验证内核已启用 cgroup2 单一层级,是 runc 原生绑定的前提;nsdelegate 标志允许容器运行时自主创建子 cgroup。
OCI config.json 关键字段
| 字段 | 示例值 | 说明 |
|---|---|---|
linux.cgroupsPath |
/myapp/etl-job-001 |
在 /sys/fs/cgroup 下的相对路径,runc 自动创建并绑定 |
linux.resources.cpu.weight |
150 |
cgroup v2 的 CPU.weight(替代 v1 的 shares),范围 1–10000 |
资源隔离流程
graph TD
A[runc create --bundle ./etl-bundle] --> B[解析 config.json]
B --> C[在 /sys/fs/cgroup/myapp/etl-job-001 创建子树]
C --> D[写入 cpu.weight、memory.max 等 v2 接口文件]
D --> E[启动 init 进程并加入该 cgroup]
runc 通过 libcontainer 直接调用 openat() + write() 操作 cgroup v2 的 cpu.weight 等伪文件,实现零代理绑定。
2.4 cgroup v2中cpu.max与memory.max的实时调优策略与压力测试验证
实时调优的核心逻辑
cpu.max(格式:max us)与memory.max(字节单位)是cgroup v2中CPU带宽与内存上限的原子控制接口,支持毫秒级热更新,无需重启进程。
压力测试验证流程
- 启动受控进程(如
stress-ng --cpu 4 --timeout 60s) - 动态写入新限值:
# 将CPU配额从100ms/100ms(100%)降至50ms/100ms(50%) echo "50000 100000" > /sys/fs/cgroup/test/cpu.max
将内存上限设为512MB
echo “536870912” > /sys/fs/cgroup/test/memory.max
> 逻辑分析:`cpu.max`中`50000 100000`表示每100ms周期内最多使用50ms CPU时间;`memory.max`直接写入字节数,内核立即触发OOM-Killer或内存回收。
#### 关键指标观测表
| 指标 | 工具 | 预期响应 |
|------|------|----------|
| CPU实际使用率 | `cat /sys/fs/cgroup/test/cpu.stat` | `usage_usec`增长速率下降 |
| 内存分配延迟 | `perf stat -e 'mem-loads,mem-stores'` | 分配失败(`ENOMEM`)次数上升 |
#### 调优决策流
```mermaid
graph TD
A[监控metrics] --> B{CPU利用率 > 80%?}
B -->|是| C[提升cpu.max配额]
B -->|否| D{内存RSS接近memory.max?}
D -->|是| E[收紧memory.max或优化应用]
D -->|否| F[维持当前配置]
2.5 面向数据抽取场景的cgroup v2监控指标采集(cpu.stat、memory.current)与告警闭环
数据同步机制
在数据抽取任务中,需每10秒轮询 cpu.stat 与 memory.current,避免高频采样导致内核开销激增。
# 采集示例(需在对应cgroup v2路径下执行)
cat /sys/fs/cgroup/data-extractor/cpu.stat | grep -E "usage_usec|nr_periods"
cat /sys/fs/cgroup/data-extractor/memory.current
usage_usec表示该cgroup累计CPU使用微秒数;nr_periods指已调度周期数,用于计算CPU使用率;memory.current为实时内存占用字节数,是触发OOM前的关键阈值信号。
告警触发逻辑
- 当
memory.current > 80% * memory.max且持续3个周期 → 触发P1告警 - 若
cpu.stat中nr_throttled > 0且throttled_usec > 500_000→ 触发P2限流告警
指标映射表
| 指标名 | 来源文件 | 单位 | 业务含义 |
|---|---|---|---|
cpu_usage_pct |
cpu.stat + cpu.max |
% | CPU配额使用率 |
mem_used_mb |
memory.current |
MB | 实时内存占用 |
graph TD
A[采集器] -->|每10s读取| B(cpu.stat & memory.current)
B --> C{阈值判断}
C -->|超限| D[推送至Alertmanager]
C -->|正常| E[写入Prometheus]
D --> F[自动降级抽取并发度]
第三章:Go运行时线程绑定与NUMA感知调度优化
3.1 runtime.LockOSThread原理深度剖析及对长周期ETL任务的适用性验证
runtime.LockOSThread() 将当前 goroutine 与底层 OS 线程永久绑定,禁止运行时调度器将其迁移到其他线程。
核心机制
- 防止 goroutine 被抢占迁移
- 保持 TLS(线程局部存储)、信号处理上下文、C 语言库状态一致性
- 一旦锁定,必须显式调用
runtime.UnlockOSThread()解绑(否则泄漏)
ETL 场景适配性验证
func runETLWithLockedThread() {
runtime.LockOSThread()
defer runtime.UnlockOSThread() // 必须成对出现
// 初始化 C 库句柄(如 ODBC 连接池)
cHandle := C.init_odbc_connection()
defer C.free_odbc_handle(cHandle)
// 长周期数据抽取:需稳定线程上下文
for _, batch := range batches {
C.process_batch(cHandle, &batch)
}
}
逻辑分析:
LockOSThread确保C.process_batch始终运行在同一线程,避免因 goroutine 迁移导致 C 层 TLS 句柄失效或信号中断。defer UnlockOSThread在函数退出时释放绑定,防止 Goroutine 泄漏。
| 场景 | 是否适用 | 原因 |
|---|---|---|
| 纯 Go 数据清洗 | ❌ | 无 C 交互,反增调度开销 |
| JNI/ODBC/Cgo 密集型 | ✅ | 依赖线程局部资源稳定性 |
| 多阶段并行 ETL | ⚠️ | 需按子任务粒度锁/解锁控制 |
graph TD
A[goroutine 启动] --> B{是否调用 LockOSThread?}
B -->|是| C[绑定至固定 M/P]
B -->|否| D[正常调度迁移]
C --> E[执行 Cgo 调用]
E --> F[保持 TLS/信号/栈一致性]
3.2 NUMA拓扑识别与go runtime多线程亲和性控制的协同设计
Go 运行时默认不感知 NUMA 拓扑,但现代高性能服务需将 OS 线程(M)绑定到特定 NUMA 节点以减少跨节点内存访问延迟。
NUMA 拓扑探测
Linux 提供 /sys/devices/system/node/ 接口,可通过 filepath.Glob("/sys/devices/system/node/node*") 枚举节点:
nodes, _ := filepath.Glob("/sys/devices/system/node/node*")
for _, node := range nodes {
id := strings.TrimPrefix(filepath.Base(node), "node")
fmt.Printf("NUMA node %s detected\n", id)
}
该代码遍历系统 NUMA 节点目录,提取节点 ID;filepath.Base() 获取路径末尾名,TrimPrefix 剥离 "node" 前缀,得到纯数字 ID(如 "0"),为后续亲和性设置提供依据。
运行时线程绑定策略
需在 GOMAXPROCS 调度前,通过 syscall.SchedSetaffinity 将 M 绑定至对应 CPU mask:
| 节点 | CPU 核心范围 | 内存本地性 |
|---|---|---|
| node0 | 0-15 | 高 |
| node1 | 16-31 | 高 |
协同机制流程
graph TD
A[启动时读取/sys/devices/system/node/] --> B[构建节点→CPU集映射]
B --> C[初始化runtime.LockOSThread]
C --> D[按P分配M并绑定至对应NUMA节点CPU]
关键在于:先拓扑发现,再调度约束,最后内存分配对齐——三者缺一不可。
3.3 结合hwloc库实现Go进程级NUMA节点绑定与内存本地化分配实践
hwloc(Hardware Locality)提供跨平台的硬件拓扑发现能力,Go可通过cgo调用其C API实现精细化NUMA控制。
核心绑定流程
- 初始化hwloc拓扑并检测NUMA节点数
- 获取目标NUMA节点对象(如
node0) - 调用
hwloc_set_proc_cpubind()绑定CPU亲和性 - 使用
hwloc_set_proc_membind()强制内存分配在指定节点
内存本地化分配示例
// 绑定当前进程到NUMA节点0,并启用本地内存分配策略
topo := hwloc.NewTopology()
topo.Load()
node0 := topo.GetObjByTypeAndIndex(hwloc.OBJ_NUMANODE, 0)
hwloc.SetProcMemBind(topo, os.Getpid(), node0, hwloc.MEMBIND_BIND|hwloc.MEMBIND_STRICT)
MEMBIND_BIND确保仅从node0分配内存;MEMBIND_STRICT使越界分配失败而非回退——强制本地化语义。
| 策略标志 | 行为说明 |
|---|---|
MEMBIND_BIND |
限定内存来源节点 |
MEMBIND_PREFERRED |
允许fallback到其他节点 |
MEMBIND_STRICT |
分配失败时返回ENOMEM错误 |
graph TD A[Go进程启动] –> B[加载hwloc拓扑] B –> C[定位目标NUMA节点] C –> D[设置CPU亲和性] C –> E[设置内存绑定策略] D & E –> F[后续malloc自动本地化]
第四章:CPU/内存精准隔离的端到端架构整合与效能验证
4.1 构建“cgroup v2 + LockOSThread + NUMA绑定”三级隔离模型的架构设计
该模型通过三重硬件与运行时协同实现确定性调度:
- 第一级(资源边界):cgroup v2 统一层级树强制 CPU/memory/IO 配额,启用
memory.high与cpu.max实现硬限流; - 第二级(线程锚定):Go 运行时调用
runtime.LockOSThread()将 goroutine 绑定至特定 OS 线程,规避调度器迁移; - 第三级(物理亲和):通过
numactl --cpunodebind=0 --membind=0启动进程,确保 CPU 核与本地内存同 NUMA 节点。
func startIsolatedWorker() {
runtime.LockOSThread()
// 绑定后调用 syscall.SchedSetaffinity 指定 CPU mask
cpuset := uint64(1 << 4) // 绑定至 CPU 4
syscall.SchedSetaffinity(0, &cpuset)
}
此代码在 goroutine 启动时锁定 OS 线程,并显式设置 CPU 亲和掩码。
cpuset使用位图指定单核,避免 Go 调度器跨核抢占,为 NUMA 内存访问奠定基础。
| 隔离层级 | 控制粒度 | 生效范围 | 关键约束 |
|---|---|---|---|
| cgroup v2 | 进程组 | 容器/服务级 | 需 mount cgroup2 且禁用 legacy 接口 |
| LockOSThread | Goroutine | 单协程生命周期 | 须配对 UnlockOSThread() 防泄漏 |
| NUMA 绑定 | 进程 | 全生命周期 | 依赖 libnuma 与 BIOS 中 NUMA 启用 |
graph TD
A[应用进程] --> B[cgroup v2 资源限制]
B --> C[LockOSThread 锁定 OS 线程]
C --> D[NUMA-aware 内存分配]
D --> E[低延迟、零跨节点访存]
4.2 在Kafka消费者型抽取服务中实施混合隔离策略的代码重构与压测分析
数据同步机制
将单线程消费逻辑重构为“线程池 + 消费组分片 + 事务边界隔离”三层混合模型:
// 按业务域动态分配消费线程,避免跨域污染
KafkaConsumer<String, byte[]> consumer = new KafkaConsumer<>(props);
consumer.subscribe(List.of("topic-a", "topic-b"),
new ConsumerRebalanceListener() {
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
// 每个分区绑定专属ExecutorService(按tenant_id哈希分桶)
partitions.forEach(p -> executors.computeIfAbsent(
hashTenant(p.topic()), () -> Executors.newFixedThreadPool(3)
).submit(() -> pollAndProcess(consumer, p)));
}
});
该设计确保租户级数据处理完全隔离,hashTenant()基于topic前缀或消息头提取租户标识;线程池大小设为3,兼顾吞吐与资源争用。
压测对比结果
| 策略类型 | 平均延迟(ms) | P99延迟(ms) | 租户间错误率 |
|---|---|---|---|
| 单线程全局消费 | 182 | 496 | 12.7% |
| 混合隔离策略 | 41 | 103 | 0.0% |
隔离执行流程
graph TD
A[Consumer拉取批次] --> B{按tenant_id路由}
B --> C[专属线程池处理]
C --> D[本地事务提交offset]
D --> E[异步写入目标库]
4.3 Prometheus+eBPF联合监控方案:追踪隔离前后P99延迟、RSS波动与TLB miss率变化
数据采集协同架构
eBPF程序实时捕获内核级指标(如/proc/pid/stat中的RSS、perf_event_open采集的dtlb_load_misses.walk_completed),通过libbpf ringbuf推送至用户态 exporter;Prometheus定期抓取该exporter暴露的/metrics端点。
关键指标映射表
| 指标名 | eBPF来源 | Prometheus指标名 | 语义说明 |
|---|---|---|---|
| P99延迟 | tcp_sendmsg + tcp_recvmsg时序差 |
app_latency_p99_seconds |
应用层请求P99 RTT |
| RSS波动 | task_struct->mm->rss_stat |
container_memory_rss_bytes |
物理页驻留量变化率 |
| TLB miss率 | PERF_COUNT_HW_CACHE_DTLB:::MISS |
node_tlb_miss_ratio |
数据TLB未命中占比 |
eBPF采样逻辑(片段)
// bpf_program.c:TLB miss计数器绑定
SEC("perf_event")
int handle_tlb_miss(struct bpf_perf_event_data *ctx) {
u64 val = bpf_get_smp_processor_id(); // 仅作示例,实际读perf counter值
bpf_map_push_elem(&tlb_miss_map, &val, BPF_EXIST); // 原子累加
return 0;
}
该程序挂载至PERF_TYPE_HARDWARE事件,tlb_miss_map为BPF_MAP_TYPE_PERCPU_ARRAY,确保每CPU独立计数避免锁竞争;BPF_EXIST保证更新安全。
指标关联分析流程
graph TD
A[eBPF采集原始事件] --> B[Ringbuf批量导出]
B --> C[Go Exporter聚合为Prometheus指标]
C --> D[Prometheus scrape interval=15s]
D --> E[Grafana多维下钻:by pod, by QoS class, by isolation status]
4.4 生产环境灰度发布流程与隔离失效回滚机制的设计与验证
灰度发布需在流量、数据、配置三层面实现严格隔离,同时确保秒级失效感知与自动回滚。
流量路由与灰度标识注入
通过 Service Mesh 的 Envoy Proxy 在入口网关注入 x-gray-version: v2.1 请求头,结合 Istio VirtualService 实现 5% 流量切流:
# istio-virtualservice-gray.yaml
spec:
http:
- route:
- destination:
host: api-service
subset: v2-1
weight: 5
- destination:
host: api-service
subset: v2-0
weight: 95
逻辑分析:subset 依赖 DestinationRule 中预定义的标签选择器(如 version: v2.1),weight 为整数权重,总和必须为100;该配置经 kubectl apply 后由 Pilot 实时下发至所有 Sidecar。
失效检测与自动回滚触发条件
| 指标类型 | 阈值 | 触发动作 |
|---|---|---|
| 5xx 错误率 | >3% 持续60秒 | 启动回滚 |
| P99 延迟 | >800ms 持续30秒 | 发出告警并冻结 |
| 核心事务成功率 | 强制全量回退 |
回滚执行流程
graph TD
A[监控告警触发] --> B{是否满足回滚策略?}
B -->|是| C[暂停灰度实例扩缩]
C --> D[更新VirtualService权重为0/100]
D --> E[清理灰度ConfigMap与Secret]
E --> F[发送Slack通知]
B -->|否| G[持续观测]
回滚操作全程 ≤ 12 秒,依赖 Argo Rollouts 的 Rollback CRD 与 Helm hook 脚本协同完成状态清理。
第五章:Golang数据抽取服务资源治理的演进路径
从单体协程到资源隔离的调度器重构
早期版本中,所有数据抽取任务(如MySQL binlog监听、HTTP API轮询、S3文件扫描)共用一个全局 goroutine 池,通过 sync.Pool 复用 worker 实例。当某类任务突发高负载(例如电商大促期间订单库 binlog 峰值达 12k events/s),导致整个池阻塞,其他低频任务(如每日ETL元数据同步)超时率达 47%。团队引入基于 context.Context 的层级化资源配额系统:为每类数据源注册独立 ResourceQuota 结构体,绑定 CPU 时间片权重与内存上限,并通过 runtime.SetMutexProfileFraction(0) 配合 pprof 实时监控锁竞争热点。
动态限流策略的灰度落地实践
在 v2.3 版本中上线基于 QPS 与队列水位双因子的自适应限流器:
| 数据源类型 | 初始QPS阈值 | 内存占用阈值 | 触发降级动作 |
|---|---|---|---|
| MySQL CDC | 800 | 1.2GB | 暂停新任务入队 |
| Kafka Topic | 1500 | 900MB | 启用批处理压缩模式 |
| REST API | 200 | 400MB | 切换至指数退避重试 |
该策略通过 Prometheus 指标 extractor_task_queue_length{source="mysql"} 与 go_memstats_heap_inuse_bytes 联动触发,已在生产环境稳定运行 142 天,任务失败率由 12.6% 降至 0.8%。
内存泄漏根因定位与零拷贝优化
2023年Q4一次线上事故暴露 bytes.Buffer 在长连接 HTTP 抽取场景中的累积泄漏:每个连接平均持有 3.2MB 未释放缓冲区。使用 go tool trace 定位到 io.CopyN 调用链中重复 grow() 导致的底层数组复制。解决方案采用 unsafe.Slice 构建固定大小环形缓冲区,并将 JSON 解析逻辑迁移至 gjson 库的零拷贝路径:
// 优化前(触发内存逃逸)
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(data)
// 优化后(栈分配+复用)
ringBuf := ringbuffer.New(64 * 1024)
gjson.ParseBytes(ringBuf.Bytes()).Get("items.#.id")
多租户资源配额的 Kubernetes 原生集成
针对 SaaS 客户数据隔离需求,在 K8s Operator 中嵌入 ResourceQuotaController,自动为每个租户命名空间注入如下配置:
apiVersion: v1
kind: ResourceQuota
metadata:
name: extractor-quota
spec:
hard:
requests.cpu: "1200m"
requests.memory: "2Gi"
limits.cpu: "2500m"
limits.memory: "4Gi"
同时扩展 ExtractJob CRD,支持 spec.resourceClass: "high-throughput" 字段,驱动调度器选择对应 nodeSelector 的 GPU 加速节点处理视频元数据抽取任务。
混沌工程验证下的弹性恢复机制
在混沌测试平台部署 network-loss 故障注入,模拟跨可用区网络分区。观测到 Redis 缓存层不可用时,服务自动启用本地 LRU 缓存(lru.Cache{MaxEntries: 5000})维持核心指标查询,恢复时间从 8.3s 缩短至 412ms。该机制通过 go.uber.org/zap 记录 recovery_mode_active=true 日志字段,并触发 Slack webhook 告警。
运维可观测性能力升级
新增 extractor_resource_utilization 自定义指标,聚合维度包括 source_type, tenant_id, task_priority,通过 Grafana 看板实现资源消耗热力图分析。对连续 5 分钟内存使用率 >90% 的实例,自动触发 kubectl debug 创建临时调试容器并执行 go tool pprof -inuse_space http://localhost:6060/debug/pprof/heap。
