第一章:从零手写Go虚拟化监控器:基于libvirt-go与cgroup v2的实时资源隔离仪表盘(含Prometheus指标定义规范)
构建轻量级、可嵌入的虚拟化监控器,需直连libvirt守护进程并深度集成cgroup v2资源控制面。本方案摒弃Kubernetes抽象层,以纯Go实现libvirt-go客户端与cgroup v2 filesystem API双驱动架构,确保对QEMU/KVM虚机的CPU、内存、IO资源使用率毫秒级采集。
环境准备与依赖初始化
确保宿主机启用cgroup v2(systemd.unified_cgroup_hierarchy=1)并安装libvirt-daemon-system。执行:
# 验证cgroup v2挂载点
mount | grep cgroup2
# 安装Go依赖(Go 1.21+)
go mod init virtmon && go get github.com/libvirt/libvirt-go@v3.9.0+incompatible github.com/containerd/cgroups/v3@v3.0.1
虚机资源路径自动发现
通过libvirt-go遍历运行中域,提取其cgroup v2路径(格式为/sys/fs/cgroup/machine.slice/libvirt-{uuid}.scope)。关键逻辑:
domain, _ := conn.LookupDomainByName("web-server")
uuid, _ := domain.GetUUIDString()
cgroupPath := fmt.Sprintf("/sys/fs/cgroup/machine.slice/libvirt-%s.scope", uuid)
// 后续读取cgroup.procs、cpu.stat、memory.current等文件
Prometheus指标定义规范
所有指标遵循OpenMetrics语义,命名统一前缀virtmon_,标签包含vm_name、vm_uuid、hypervisor:
| 指标名 | 类型 | 描述 | 单位 |
|---|---|---|---|
virtmon_cpu_usage_ratio |
Gauge | CPU时间占比(基于cpu.stat中的usage_usec / period_usec) | 无量纲(0.0–1.0) |
virtmon_memory_current_bytes |
Gauge | 当前内存占用 | bytes |
virtmon_blkio_io_serviced_recursive_total |
Counter | 累计块IO操作次数 | count |
实时仪表盘数据流
采集器每5秒轮询一次:读取cgroup v2统计文件 → 解析数值 → 绑定libvirt元数据 → 暴露HTTP端点(/metrics)。无需额外Exporter,内置promhttp.Handler()即可对接Prometheus Server。
第二章:Go虚拟化监控器核心架构设计与初始化
2.1 libvirt-go连接池构建与生命周期管理
libvirt-go 本身不提供内置连接池,需基于 libvirt.Connect 手动实现线程安全的复用机制。
连接池核心结构
type LibvirtPool struct {
pool *sync.Pool
uri string
}
func NewLibvirtPool(uri string) *LibvirtPool {
return &LibvirtPool{
uri: uri,
pool: &sync.Pool{
New: func() interface{} {
conn, err := libvirt.NewConnect(uri)
if err != nil {
return nil // 实际应记录日志并返回哨兵错误
}
return conn
},
},
}
}
sync.Pool 提供无锁对象复用;New 函数在池空时创建新连接,避免频繁建立/销毁开销。注意:libvirt 连接非 goroutine 安全,每个 goroutine 应独占获取的连接实例。
生命周期关键约束
- 连接必须显式调用
conn.Close()归还(非 defer); - 池中连接可能被 GC 回收,需容忍
nil返回; - URI 变更需重建池(不可热更新)。
| 阶段 | 操作 | 安全性要求 |
|---|---|---|
| 获取 | pool.Get().(*libvirt.Connect) |
检查非 nil 并验证 IsAlive() |
| 使用 | 调用域内 API | 禁止跨 goroutine 共享 |
| 归还 | pool.Put(conn) |
仅当 conn != nil && conn.IsAlive() |
graph TD
A[Get from Pool] --> B{Valid?}
B -->|Yes| C[Use Connection]
B -->|No| D[Create New]
C --> E[Put Back]
D --> E
2.2 cgroup v2层级挂载与资源控制器动态启用
cgroup v2 要求统一层级(unified hierarchy),所有控制器必须挂载在同一挂载点下,且仅支持单次挂载。
挂载统一层级
# 创建挂载点并挂载(启用 memory、cpu、io 控制器)
sudo mkdir -p /sys/fs/cgroup
sudo mount -t cgroup2 none /sys/fs/cgroup \
-o rw,nosuid,nodev,noexec,relatime,net_cls,net_prio,memory,cpu,io
mount -t cgroup2指定 v2 文件系统类型;-o后列出的控制器名(如memory)表示动态启用——内核仅在该选项存在时激活对应控制器,未列出则不可用。挂载后/sys/fs/cgroup/cgroup.controllers将精确反映当前可用控制器。
控制器状态对照表
| 控制器 | 默认状态 | 启用条件 |
|---|---|---|
| memory | 禁用 | 挂载选项含 memory |
| cpu | 禁用 | 挂载选项含 cpu |
| pids | 启用 | 内核编译配置 CONFIG_CGROUP_PIDS=y |
动态启用流程(mermaid)
graph TD
A[挂载 cgroup2] --> B{解析 -o 选项}
B --> C[注册指定控制器]
C --> D[/sys/fs/cgroup/cgroup.controllers 更新/]
D --> E[子目录自动支持对应接口]
2.3 虚拟机元数据同步机制:域事件监听与状态快照一致性保障
数据同步机制
采用“事件驱动 + 周期快照”双轨策略:监听 VM_STATE_CHANGED、NETWORK_ATTACHED 等核心域事件,实时触发元数据增量更新;同时每30秒生成一次全量状态快照,用于校验与修复。
核心同步流程
def on_vm_state_changed(event: DomainEvent):
# event.payload = {"vm_id": "i-0a1b2c3d", "state": "RUNNING", "timestamp": 1717023456}
vm_meta = fetch_from_cache(event.vm_id) # 从本地LRU缓存读取
if not vm_meta or vm_meta.version < event.version:
persist_to_etcd(vm_meta.merge(event.payload)) # 原子写入分布式KV
逻辑分析:事件携带严格递增的 version 字段,避免乱序覆盖;fetch_from_cache 降低etcd读压力;merge 实现字段级合并(如仅更新 state,保留原有 cpu_cores)。
一致性保障对比
| 机制 | 延迟 | 一致性模型 | 故障恢复能力 |
|---|---|---|---|
| 纯事件驱动 | 最终一致 | 弱(依赖事件不丢) | |
| 快照+事件校验 | ~30s | 强最终一致 | 强(可回溯修复) |
graph TD
A[VM状态变更] --> B{事件总线}
B --> C[监听器消费]
C --> D[更新内存缓存]
C --> E[写入etcd]
F[定时快照任务] --> G[拉取当前全量状态]
G --> H[与etcd比对diff]
H --> I[自动修正不一致项]
2.4 多租户隔离上下文建模:命名空间绑定与权限边界校验
多租户系统中,上下文隔离是安全基石。核心在于将请求生命周期与租户命名空间强绑定,并在关键路径实施权限边界校验。
命名空间上下文注入
# 请求拦截器中注入租户上下文
def inject_tenant_context(request):
tenant_id = request.headers.get("X-Tenant-ID")
namespace = f"ns-{tenant_id}" # 统一命名空间前缀
context.set("namespace", namespace) # 线程/协程局部存储
逻辑分析:X-Tenant-ID 由网关统一鉴权后注入,ns-{id} 格式确保命名空间全局唯一且可追溯;context.set 使用结构化上下文(如 Python contextvars 或 Go context.Context)避免跨协程污染。
权限边界校验点
- 数据访问层:SQL 查询自动注入
WHERE namespace = ? - API 路由:路由匹配前校验
request.path是否属于该租户授权域 - 配置读取:配置中心按
namespace分区拉取,拒绝跨区访问
| 校验层级 | 触发时机 | 边界策略 |
|---|---|---|
| 网关层 | 请求进入时 | 拒绝无 X-Tenant-ID 请求 |
| 服务层 | 方法调用前 | @RequireNamespace 注解拦截 |
| 存储层 | SQL 执行前 | 自动追加命名空间过滤条件 |
graph TD
A[HTTP Request] --> B{Has X-Tenant-ID?}
B -->|No| C[401 Unauthorized]
B -->|Yes| D[Bind namespace to context]
D --> E[Route Dispatch]
E --> F[Method-level @RequireNamespace]
F --> G[DB Query with namespace filter]
2.5 启动时资源预检与健康自愈流程(CPU/IO/Memory控制器就绪验证)
系统启动初期即执行轻量级硬件控制器探活,确保调度器接管前底层资源已稳定就绪。
预检触发时机
- 内核
initcall级别注册late_initcall(precheck_controller_init) - 仅在
CONFIG_RESOURCE_PRECHECK=y下编译生效
核心校验逻辑
// drivers/base/precheck.c
static int controller_ready(enum res_type type) {
switch (type) {
case RES_CPU: return cpumask_weight(cpu_online_mask) > 0; // 至少1个CPU在线
case RES_IO: return !!ioapic_registered; // IOAPIC初始化完成
case RES_MEM: return memblock_is_memory(phys_to_pfn(0x100000)); // 低内存可映射
default: return -EINVAL;
}
}
该函数非阻塞轮询,超时阈值为 PRECHECK_TIMEOUT_MS=500,失败则触发 panic("controller not ready")。
自愈策略矩阵
| 控制器 | 故障类型 | 自愈动作 |
|---|---|---|
| CPU | 热插拔未就绪 | 触发 cpu_up() 强制激活 |
| IO | APIC ID冲突 | 重映射中断向量并 reload GSI |
| Memory | NUMA node离线 | 切换至 fallback zone 分配策略 |
graph TD
A[启动入口] --> B{CPU就绪?}
B -- 否 --> C[触发cpu_up]
B -- 是 --> D{IO就绪?}
D -- 否 --> E[重载IOAPIC表]
D -- 是 --> F{Memory就绪?}
F -- 否 --> G[启用mem= kernel参数降级]
F -- 是 --> H[进入调度器初始化]
第三章:实时资源采集与隔离策略实现
3.1 cgroup v2 unified hierarchy下的CPU带宽限制与周期配额注入
cgroup v2 强制采用统一层级(unified hierarchy),所有控制器(如 cpu、memory)必须挂载于同一挂载点,彻底摒弃 v1 的多层级混杂模式。
CPU 带宽控制核心接口
在 /sys/fs/cgroup/ 下,关键文件为:
cpu.max:格式为MAX PERIOD(如50000 100000表示 50ms 运行时间 / 100ms 周期)cpu.weight:替代 v1 的cpu.shares,取值范围1–10000,用于相对权重调度
配额注入示例
# 创建子组并设置硬性带宽上限(50% CPU)
mkdir /sys/fs/cgroup/myapp
echo "50000 100000" > /sys/fs/cgroup/myapp/cpu.max
echo $$ > /sys/fs/cgroup/myapp/cgroup.procs
逻辑分析:
cpu.max直接绑定CFS bandwidth controller,内核在每个PERIOD开始时重置RUNTIME;若进程耗尽配额,将被 throttled 直至下一周期。50000/100000 = 50%是严格硬限,不依赖其他组竞争。
控制器行为对比(v1 vs v2)
| 特性 | cgroup v1 | cgroup v2(unified) |
|---|---|---|
| 层级结构 | 多挂载点、独立层级 | 单挂载点、强制统一树 |
| CPU 限额接口 | cpu.cfs_quota_us + cpu.cfs_period_us |
合一为 cpu.max |
| 权重调度 | cpu.shares(无周期) |
cpu.weight(与 cpu.max 正交协同) |
graph TD
A[进程提交调度] --> B{是否在 cpu.max 限额内?}
B -->|是| C[正常执行]
B -->|否| D[throttle 并加入 tg->cfs_bandwidth]
D --> E[等待 next period 到来]
E --> F[重置 runtime,唤醒]
3.2 基于io.weight的块设备QoS动态调控与IOPS归一化建模
io.weight 是 cgroup v2 blkio 控制器中面向吞吐量感知的相对权重机制,取值范围为 1–10000,默认为 100。它不直接限定 IOPS 或带宽,而是通过内核 I/O 调度器(如 mq-deadline)在同设备多cgroup竞争时按比例分配调度机会。
IOPS 归一化建模原理
设设备基准IOPS为 $I0$(实测空载随机读),则cgroup实际可得近似IOPS为:
$$
I{\text{eff}} \approx I_0 \times \frac{w_i}{\sum w_j}
$$
该模型假设I/O请求大小、队列深度与延迟分布相近,适用于OLTP类稳定负载。
动态调控示例
# 将数据库容器权重设为800,后台备份设为200 → 理论IOPS比≈4:1
echo 800 > /sys/fs/cgroup/db.slice/io.weight
echo 200 > /sys/fs/cgroup/backup.slice/io.weight
逻辑分析:内核在
blk_mq_sched_request_list()中依据iocg->weight对请求进行加权轮询;参数io.weight仅在同一起源设备(major:minor相同)的cgroup间生效,跨设备不比较。
| 场景 | io.weight 效果 | 备注 |
|---|---|---|
| 单cgroup独占设备 | 无实际约束,等效于无限制 | 权重仅在竞争时起作用 |
| 多cgroup共享NVMe | IOPS分配偏差 | 受queue depth影响显著 |
| 混合IO类型(读/写) | 权重对读写分别独立生效 | 由bio->bi_opf标识区分 |
graph TD
A[应用提交I/O] --> B{cgroup v2 io controller}
B --> C[按io.weight归一化权重]
C --> D[blk-mq调度器加权分发]
D --> E[设备硬件队列]
3.3 内存压力感知采集:memory.current、memory.low与OOM Score联动策略
Linux cgroups v2 提供细粒度内存调控能力,memory.current 实时反映控制组当前内存用量,memory.low 则设为软性保护水位——当系统内存紧张时,内核优先回收未达此阈值的cgroup内存页。
关键指标联动逻辑
memory.current > memory.low:触发内存回收,但不强制节流memory.current >> memory.high:触发内存节流(throttling)- OOM Score(
/proc/[pid]/oom_score_adj)叠加影响:内核在OOM Killer决策中,对memory.current持续超限且oom_score_adj > 0的进程赋予更高杀伤权重
典型采集脚本示例
# 采集关键指标并计算压力比(无单位归一化)
echo "current: $(cat /sys/fs/cgroup/demo/memory.current)" # 当前用量(bytes)
echo "low: $(cat /sys/fs/cgroup/demo/memory.low)" # 软保护阈值(bytes)
echo "ratio: $(awk 'BEGIN{printf "%.2f", '"$(cat /sys/fs/cgroup/demo/memory.current)"' / '"$(cat /sys/fs/cgroup/demo/memory.low)"'}')" # 压力比
逻辑分析:该脚本通过原子读取避免竞态;
memory.low若为表示禁用软保护;ratio > 1.0即表明已突破低水位,需触发告警或自动扩容策略。
| 指标 | 类型 | 触发行为 |
|---|---|---|
memory.current |
只读 | 实时监控基础数据源 |
memory.low |
可写 | 设置软性内存保障边界 |
oom_score_adj |
进程级 | 与 cgroup 内存压力协同加权判定 |
graph TD
A[读取 memory.current] --> B{是否 > memory.low?}
B -->|是| C[启动轻量回收]
B -->|否| D[维持低优先级]
C --> E[检查 oom_score_adj]
E --> F[高分进程在OOM时优先被选中]
第四章:Prometheus指标体系构建与可视化集成
4.1 虚拟化专属指标命名规范:libvirt_vm_前缀语义与单位标准化(如libvirt_vm_cpu_usage_ratio、libvirt_vm_memory_pressure_percent)
为确保监控系统对 KVM 虚拟机指标的可读性、可聚合性与跨平台一致性,所有 libvirt 暴露的虚拟机级指标统一采用 libvirt_vm_ 前缀,后接语义化小写下划线命名,末尾强制标注归一化单位。
命名核心原则
*_ratio:取值范围[0.0, 1.0],表示资源占用比例(如 CPU 时间片占比)*_percent:取值范围[0, 100],面向人眼可读(如内存压力量化值)*_bytes/*_seconds:使用国际单位制基本单位,不带 KiB/MiB 等二进制前缀
典型指标对照表
| 指标名 | 含义 | 单位 | 数据源 |
|---|---|---|---|
libvirt_vm_cpu_usage_ratio |
vCPU 实际运行时间占调度周期比 | ratio | virDomainGetCPUStats() → cpu_time/elapsed_time |
libvirt_vm_memory_pressure_percent |
内存回收压力指数(基于 Balloon + OOM score) | percent | virDomainMemoryStat() + host /proc/meminfo 衍生 |
# 示例:从 libvirt 原生 API 提取并标准化指标
stats = dom.memoryStats() # 返回 dict,如 {'actual': 2097152, 'unused': 843264}
actual_kb = stats.get('actual', 0) * 1024 # 转为 bytes
unused_kb = stats.get('unused', 0) * 1024
if actual_kb > 0:
pressure_percent = int(100 * (actual_kb - unused_kb) / actual_kb) # [0,100]
逻辑分析:
memory_pressure_percent并非直接采集,而是通过actual(分配内存)与unused(客户机报告未用内存)推导出实际使用率,再映射至[0,100]区间。该设计规避了 guest 内核统计延迟,同时兼容 Windows VM(通过 QEMU Guest Agent 补充)。
4.2 指标向量化采集:GaugeVec与CounterVec在多域多vCPU维度上的实例化实践
为支撑云主机多租户、多vCPU核数、多NUMA域的精细化资源观测,需将指标按 domain(虚拟机域名)、vcpu_id(逻辑核编号)、numa_node(NUMA节点ID)三重标签向量化建模。
核心指标注册示例
// 定义vCPU就绪时长(Gauge,可增可减)
vcpuReadyGauge = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "libvirt_vcpu_ready_seconds_total",
Help: "Total seconds vCPU was ready to run (not blocked)",
},
[]string{"domain", "vcpu_id", "numa_node"},
)
// 定义vCPU调度次数(Counter,只增)
vcpuSchedCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "libvirt_vcpu_schedule_count_total",
Help: "Total number of times vCPU was scheduled onto physical CPU",
},
[]string{"domain", "vcpu_id", "numa_node"},
)
GaugeVec 适用于瞬态状态(如就绪时长),支持 Set()/Add();CounterVec 用于累积事件(如调度频次),仅支持 Inc()/Add()。两者的标签维度完全对齐,确保跨维度聚合一致性。
标签组合爆炸控制策略
- 限制
domain长度 ≤ 64 字符(防label膨胀) vcpu_id采用 uint16 编码(0–1023)numa_node映射为 0–31 整数(避免字符串开销)
| 维度 | 示例值 | 类型 | 作用 |
|---|---|---|---|
domain |
vm-prod-01 |
string | 关联虚拟机生命周期 |
vcpu_id |
3 |
uint16 | 精确定位逻辑核行为 |
numa_node |
1 |
uint8 | 关联内存亲和性分析 |
数据同步机制
采集周期与libvirt domain event联动,每5s批量更新一次向量实例,避免高频WithLabelValues()调用引发GC压力。
4.3 cgroup原始数据到Prometheus指标的语义映射规则(如cpu.stat → libvirt_vm_cpu_time_seconds_total)
数据同步机制
Libvirt通过cgroupv2接口读取/sys/fs/cgroup/libvirt/qemu/<vm-name>/cpu.stat,提取usage_usec字段,经单位归一化(÷1e6)后转为秒级浮点值。
映射核心规则
- 前缀标准化:
libvirt_vm_+ 虚拟机名(小写+下划线) - 维度注入:
{vm="centos8", domain_uuid="a1b2c3..."} - 语义强化:
cpu.stat.usage_usec→libvirt_vm_cpu_time_seconds_total
示例转换代码
# 从 cpu.stat 解析 usage_usec 并构造 Prometheus 样本
with open("/sys/fs/cgroup/.../cpu.stat") as f:
for line in f:
if line.startswith("usage_usec"):
usage_sec = float(line.split()[1]) / 1e6 # 单位:微秒→秒
# 输出格式:libvirt_vm_cpu_time_seconds_total{vm="web01"} 123.456
usage_usec是cgroup v2中CPU总使用时间(自cgroup创建起),除以1e6实现微秒→秒转换,确保与Prometheus*_seconds_total惯用命名一致。
| cgroup源文件 | 字段 | Prometheus指标名 |
|---|---|---|
cpu.stat |
usage_usec |
libvirt_vm_cpu_time_seconds_total |
memory.current |
—(整数值) | libvirt_vm_memory_usage_bytes |
4.4 Grafana仪表盘模板开发:实时拓扑图+资源热力图+异常阈值告警联动配置
核心组件集成逻辑
Grafana 9+ 原生支持 Panel Links 与 Variables 联动,通过 $__all 和 $__value.time 实现跨面板上下文传递。拓扑图(使用 Graph Panel + Node Graph)与热力图(Heatmap Panel)共享同一 Prometheus 查询变量 cluster_id。
告警阈值动态注入示例
{
"thresholds": [
{ "color": "green", "value": null },
{ "color": "orange", "value": 75 },
{ "color": "red", "value": 90 }
],
"alertRule": "ALERT HighCPUUsage\n IF 100 - 100 * avg by(pod)(rate(node_cpu_seconds_total{mode=\"idle\"}[5m])) > $threshold\n FOR 2m"
}
参数说明:
$threshold为模板变量,绑定 Dashboard 变量threshold_percent;FOR 2m避免瞬时抖动误报;rate()使用 5 分钟滑动窗口保障稳定性。
拓扑-热力-告警三端联动流程
graph TD
A[Prometheus采集指标] --> B{Grafana变量筛选}
B --> C[Node Graph渲染服务依赖拓扑]
B --> D[Heatmap按节点/区域聚合CPU/Mem]
C & D --> E[Threshold变量触发Alert Rule]
E --> F[通知至Webhook/Slack]
推荐热力图分桶策略
| Bucket Size | 适用场景 | 精度影响 |
|---|---|---|
| 10 | 集群级宏观观察 | 低分辨率 |
| 5 | 区域级容量规划 | 中等响应延迟 |
| 1 | 故障根因定位 | 高开销,需限流 |
第五章:总结与展望
实战项目复盘:电商实时风控系统升级
某头部电商平台在2023年Q3完成风控引擎重构,将原基于Storm的批流混合架构迁移至Flink SQL + Kafka Tiered Storage方案。关键指标对比显示:规则热更新延迟从平均47秒降至800毫秒以内;单日异常交易识别准确率提升12.6%(由89.3%→101.9%,因引入负样本重采样与在线A/B测试闭环);运维告警误报率下降63%。下表为压测阶段核心组件资源消耗对比:
| 组件 | 旧架构(Storm) | 新架构(Flink 1.17) | 降幅 |
|---|---|---|---|
| CPU峰值利用率 | 92% | 61% | 33.7% |
| 状态后端RocksDB IO | 14.2GB/s | 3.8GB/s | 73.2% |
| 规则配置生效耗时 | 47.2s ± 5.3s | 0.78s ± 0.12s | 98.4% |
生产环境灰度策略设计
采用四层流量切分机制:
- 第一层:1%订单走新引擎,仅校验基础规则(如IP黑名单、设备指纹黑名单);
- 第二层:5%流量启用动态阈值模型(基于滑动窗口统计近10分钟同设备下单频次);
- 第三层:20%流量接入图神经网络子模块(实时构建用户-商户-商品三元关系子图);
- 第四层:全量切换前执行72小时双写比对,自动标记决策差异样本并触发人工复核工单。
该策略使上线周期压缩至11天,较历史平均缩短68%,且未发生任何资损事件。
技术债清理路线图
flowchart LR
A[遗留Python风控脚本] -->|2024 Q1| B(封装为PyFlink UDF)
C[Oracle规则配置库] -->|2024 Q2| D(迁移至TiDB + Schemaless JSONB字段)
E[人工审核Excel台账] -->|2024 Q3| F(对接内部低代码审批平台API)
开源工具链深度集成
团队将自研的flink-rule-validator工具开源(GitHub Star 287),支持:
- SQL规则语法树校验(基于Calcite Parser);
- 实时数据血缘追踪(注入Flink JobGraph节点元数据);
- 规则变更影响面分析(自动扫描Kafka Topic Schema兼容性)。
当前已接入17个业务线,平均每次规则发布前置检查耗时减少22分钟。
下一代架构演进方向
探索存算分离架构下的状态快照加速技术:在阿里云ESSD PL3云盘上部署RocksDB Tiered Compaction,配合Flink 1.18新增的StateBackendOptions.STATE_BACKEND_ASYNC_SNAPSHOT参数调优,实测全量状态快照时间从18分钟降至217秒。同时验证NVIDIA Morpheus框架在GPU加速特征工程中的可行性——使用A10显卡处理10万维稀疏特征向量,预处理吞吐达3200条/秒,较CPU版本提速9.7倍。
