第六章 BMC常用的调试工具
1 curl
1.1 为什么测 BMC 离不开它
上一章讲过 Redfish 走的是网页那一套,既然是网页那套,能发请求的东西都能当探针。curl 就是这么个东西,命令行里敲一条就出结果,不用装客户端,不用写脚本,改个参数再敲一条就能对比。
| 对比项 | curl | 图形界面 |
|---|---|---|
| 响应头 | 看得到 | 看不到 |
| 状态码 | 看得到 | 多数不显示 |
| 耗时分段 | 看得到 | 看不到 |
| 造异常请求 | 改参数就行 | 只能点界面给的 |
| 批量执行 | 能进脚本 | 不行 |
| 上手难度 | 要记参数 | 点就行 |
它比图形化工具有三个好处。一是能看全,响应头、状态码、耗时全在,界面上只给你渲染后的结果。二是能进脚本,一条命令复制进批量巡检里就能跑几十台。三是能造异常,改个方法、改个请求头,造出界面上点不出来的情形。
text
┌──────────────────────────────────────────────────────────┐
│ 一条 curl 能看到什么 │
├──────────────────────────────────────────────────────────┤
│ 一条命令能把六件事一次问清楚 │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 通不通 连不上先看网路、端口、防火墙 │ │
│ │ 状态码 应用层给的答案,比通不通更细 │ │
│ │ 响应头 内容类型、长度、连接怎么收 │ │
│ │ 响应体 JSON 结构与字段,对不对得上 │ │
│ │ 耗时分段 慢在哪一段,拆开才看得出来 │ │
│ │ 证书 加密版本与有效期,安全项必查 │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 六件里最容易被跳过的是耗时分段和证书 │
└──────────────────────────────────────────────────────────┘
六件里最容易被跳过的是耗时分段。网页慢的时候所有人都在猜是网慢还是 BMC 慢,把时间按段拆开,哪一段慢一眼就看出来了。我碰到过一次,客户说网页要十几秒才开,各段一拆,加密握手占了大半,换掉证书套件之后降到一秒出头,跟 BMC 的性能一点关系都没有。
1.2 常用参数
text
┌──────────────────────────────────────────────────────────┐
│ curl 高频参数 │
├──────────────────────────────────────────────────────────┤
│ 参数不用全记,常用的就这几条 │
│ │
│ +------------+--------------------+------------------+ │
│ |参数 |干什么 |看什么 | │
│ +------------+--------------------+------------------+ │
│ |-k |跳过证书校验 |临时排障用 | │
│ |-i |连响应头一起输出 |头部的字段 | │
│ |-s |不要进度条 |配合 -w 取值 | │
│ |-o /dev/null|正文扔掉 |只要状态码 | │
│ |-w |自定义输出格式 |各段耗时 | │
│ |-X |指定方法 |POST 与 DELETE | │
│ |-H |加请求头 |内容类型与令牌 | │
│ |-d |带请求体 |登录与配置下发 | │
│ |-u |带上用户名密码 |认证通不通 | │
│ |-m |整条命令的超时 |卡死时自己退出 | │
│ +------------+--------------------+------------------+ │
└──────────────────────────────────────────────────────────┘
参数不用背全,常用的就这几条。测 BMC 的时候通常是这么组合,先看通不通,再看返回什么,接着看耗时。
一条典型的取值命令长这样,把正文扔掉,只要状态码。
text
curl -k -s -o /dev/null -w "%{http_code}" https://<bmc>/redfish/v1/
想看耗时就把格式串换成几段拼起来,域名解析、建连、加密握手、首字节、总时间,五段分开打印。
text
curl -k -s -o /dev/null -w "dns %{time_namelookup} tcp %{time_connect} tls %{time_appconnect} ttfb %{time_starttransfer} total %{time_total}\n" https://<bmc>/redfish/v1/
这两条命令是所有 Redfish 排查的起点,单独记下来能省很多时间。慢在哪一段,看数字就知道。域名解析慢查地址配置,建连慢查网路和防火墙,加密握手慢查证书套件,首字节慢才是 BMC 自己慢。
1.3 状态码怎么读
text
┌──────────────────────────────────────────────────────────┐
│ 常见状态码怎么读 │
├──────────────────────────────────────────────────────────┤
│ 状态码是应用层给的第一手答案 │
│ │
│ +----------+--------------+--------------------------+ │
│ |状态码 |含义 |遇到时先怀疑什么 | │
│ +----------+--------------+--------------------------+ │
│ |200 |成功 |正常往下走 | │
│ |201 |创建成功 |资源真的建出来没有 | │
│ |202 |已收下待处理 |有没有任务链接可以跟 | │
│ |204 |成功但没有内容|删除类操作常见 | │
│ |400 |请求本身有问题|字段或格式写错 | │
│ |401 |没带身份 |令牌没传或已过期 | │
│ |403 |带了但不够格 |权限表配错了 | │
│ |404 |资源不存在 |路径拼错或机型没这个 | │
│ |405 |方法不允许 |该资源只支持读 | │
│ |429 |请求太密 |限流阈值是多少 | │
│ |500 |服务端出错 |要抓日志配合看 | │
│ |000 |压根没连上 |网路、端口、超时 | │
│ +----------+--------------+--------------------------+ │
└──────────────────────────────────────────────────────────┘
| 对比项 | 401 | 403 |
|---|---|---|
| 含义 | 没带身份 | 带了不够格 |
| 常见原因 | 没传令牌或已过期 | 用户权限不够 |
| 重试有没有用 | 补上身份就行 | 重试多少次都一样 |
| 验的是哪一块 | 认证机制 | 权限模型 |
401 和 403 最容易混。没带身份是 401,带了但权限不够是 403。测权限模型的时候这两个码必须分开验,只验一个等于没验。
202 也经常被误读。刷新固件、跑长任务这类操作返回 202,意思是收下了还没做完,响应里会给一个任务链接。脚本只判 202 就往下走,后面失败是抓不到的,得跟着任务链接轮询到终态。
000 不是状态码,是 curl 自己给的,意思是连接阶段就失败了。看到 000 别去查应用层,往网路、端口、超时那几个方向查。
1.4 上传固件与下发配置
刷机接口用 POST 带二进制体,请求头要指定内容类型。命令形态上跟登录那条差不多,区别在请求体是文件内容而不是 JSON。
text
curl -k -u <user>:<pass> -H "Content-Type: application/octet-stream" -X POST --data-binary @<file> https://<bmc>/redfish/v1/UpdateService
这里有个容易踩的点。上传大文件的时候如果中间断了,curl 报的是传输错误,跟 BMC 拒收混在一起分不清。判定的办法是分开看,先确认返回码,再确认任务状态,两边对不上才是真失败。
1.5 订阅事件
事件订阅接口也是 POST,请求体里带回调地址和关心的事件类型。测的时候除了确认返回 201,还要确认订阅真的建出来了,方法是再拉一次订阅集合看条目在不在。
订阅条目有上限。超过上限之后的请求该返回明确的错误,如果静默丢掉老的,客户端那边以为订阅成功了,实际上收不到事件,这种缺陷很难被发现。
1.6 测试清单
| 项目 | 预期结果 | 判定标准 |
|---|---|---|
| 未认证访问 | 401 | 没带身份一律拒绝 |
| 低权限访问高权限资源 | 403 | 不能因为重试而变化 |
| 正常读取 | 200 且字段齐全 | 必选字段一个都不能少 |
| 刷新固件 | 202 且带任务链接 | 能跟到终态 |
| 上传坏包 | 明确的错误码 | 不能返回成功 |
| 证书过期 | 连接失败 | 不能静默放行 |
| 并发一百次读取 | 全部有响应 | 不能出现连接失败 |
| 超时设置生效 | 到点自己退出 | 不能无限等待 |
| 加密套件 | 弱套件被拒 | 安全项必查 |
第九项在安全评审里是硬指标,弱套件能不能禁掉,要用 curl 指定套件去试,试出来能连上就是缺陷。
2 lsof
2.1 它回答的四类问题
text
┌──────────────────────────────────────────────────────────┐
│ lsof 能回答的四个问题 │
├──────────────────────────────────────────────────────────┤
│ 进程手里抓着什么,全在这张表里 │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 谁占着端口 端口起不来时第一问 │ │
│ │ 谁开着文件 配置改了不生效的原因 │ │
│ │ 句柄有多少 泄漏看的是趋势不是绝对值 │ │
│ │ 删了还占着 空间没释放的经典情形 │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 四类问题对应的参数都不一样 │
└──────────────────────────────────────────────────────────┘
| 问题 | 命令要点 | 结果怎么读 |
|---|---|---|
| 谁占着端口 | 加 -i 与端口号 | 进程号与进程名 |
| 谁开着文件 | 加目录参数 | 打开该文件的进程 |
| 句柄有多少 | 加 -p 与进程号 | 计数,看趋势 |
| 删了还占着 | 找已删除条目 | 有条目就是没释放 |
这四个问题看着不相干,底下的机制是同一个。系统里每个进程都有一张表,记着它开了哪些东西,lsof 干的事就是把这张表读出来给你看。
2.2 端口被谁占了
服务起不来报端口占用,是最常见的用法。对着端口号查一次,进程号、进程名、用户全出来。
text
lsof -nP -i :443
加 -n 和 -P 是不让它去解析名字,解析会很慢,而且解析出来的名字有时候会误导人。直接看数字最省事。
查出来之后要确认一件事,占着端口的进程是不是你以为的那个。遇到过一次,占着 443 的是上一版留下的僵尸进程,新版本起不来,报的错却是配置问题,绕了很久。
2.3 句柄泄漏
句柄泄漏是稳定性测试里的常客。它的表现很有特点,服务一开始好好的,跑上几天就出问题,重启就好,过几天又来。
text
┌──────────────────────────────────────────────────────────┐
│ 句柄数怎么涨才叫泄漏 │
├──────────────────────────────────────────────────────────┤
│ 句柄泄漏看的是曲线,不是某一个数 │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 第 1 天 180 |##### │ │
│ │ 第 2 天 340 |########### │ │
│ │ 第 3 天 520 |################# │ │
│ │ 第 4 天 700 |####################### │ │
│ │ 第 5 天 880 |############################# │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 每天涨的量差不多,就是有人开了不关 │
│ 第 5 天那一行已经贴到上限 │
└──────────────────────────────────────────────────────────┘
| 观察点 | 正常 | 泄漏 |
|---|---|---|
| 随时间变化 | 涨到平台期就持平 | 一直往上 |
| 起伏跟什么相关 | 跟业务量相关 | 跟时间相关 |
| 重启之后 | 回到基线 | 回到基线再涨 |
| 类型分布 | 各类都有 | 单一类型居多 |
判定的关键在于趋势。单看一个数是看不出来的,句柄多用一点本身不是问题。要在几天里定时采同一个进程的句柄数,画成曲线,每天涨的量差不多,那就是有人在开了不关。
采数的时候就一条命令。
text
lsof -p <pid> | wc -l
采回来的数要连着类型一起看,全是套接字说明是连接没关,全是文件说明是文件没关,两种的修法完全不一样。
2.4 删了文件空间没释放
这个情形在 BMC 上特别常见。日志把分区写满了,上去把日志文件删掉,空间没回来,分区还是满的。
原因是进程手里还抓着那个文件,删掉的是目录里的名字,数据块要等所有抓着它的进程都关掉才释放。查的办法是让 lsof 找已删除的条目。
text
lsof | grep deleted
处理方式也有讲究。直接重启服务能解决,但现场常常不允许重启。有的实现支持信号触发重新打开日志,发了信号句柄就换了,空间也就回来了。验这个的时候要确认空间真的降下来,光看命令没报错不算数。
2.5 参数
text
┌──────────────────────────────────────────────────────────┐
│ lsof 高频参数 │
├──────────────────────────────────────────────────────────┤
│ 参数 │
│ │
│ +--------------+--------------------+----------------+ │
│ |参数 |干什么 |典型场合 | │
│ +--------------+--------------------+----------------+ │
│ |-i |只看网络连接 |端口被谁占了 | │
│ |-i :443 |看某个端口 |服务起不来 | │
│ |-p |看某个进程 |句柄是不是在涨 | │
│ |-c |看某个名字的进程 |不确定进程号时 | │
│ |-u |看某个用户 |权限相关的排查 | │
│ |+D |看某个目录下 |配置改了不生效 | │
│ |-n |不要解析名字 |输出快很多 | │
│ |-P |不要解析端口名 |数字更直观 | │
│ +--------------+--------------------+----------------+ │
└──────────────────────────────────────────────────────────┘
2.6 测试清单
| 项目 | 预期结果 | 判定标准 |
|---|---|---|
| 服务停止后端口 | 没有进程占着 | 不能留下监听 |
| 服务启动失败 | 能查出占用方 | 报错要指明进程 |
| 连续运行七天 | 句柄数持平 | 增长不超阈值 |
| 大量建连断开 | 句柄回收 | 跑完回到基线 |
| 日志轮转 | 老句柄关掉 | 不出现已删除占空间 |
| 删大文件 | 空间立刻回来 | 不用重启也能释放 |
| 分区写满 | 服务不崩 | 降级或拒绝并记录 |
| 权限不足的用户 | 查不到别的进程 | 不能越权看信息 |
第三项的阈值各家不一样。先跑一轮基线,取稳定运行时的句柄数,往上留百分之三十当上限。没有基线就只能拍脑袋定一个数,事后很难说清。
3 objdump
3.1 什么时候才会用到它
日常测试用不到这个工具,它出现在两类场合。一类是崩溃定位,日志里只有一个地址,没有函数名,要去二进制里翻。一类是版本核对,怀疑刷进去的文件跟预期不一致,要看看里面到底有什么。
| 场合 | 用哪件 | 拿到什么 |
|---|---|---|
| 只有一个地址 | objdump | 函数所在的区间 |
| 有地址且有符号 | addr2line | 文件名与行号 |
| 查函数在不在 | nm | 符号清单 |
| 看段与权限 | readelf | 段表 |
| 找版本号与路径 | strings | 可打印字符串 |
| 确认架构对不对 | file | 目标平台信息 |
多数 BMC 的固件是交叉编译出来的,跑在 ARM 上,编译在 x86 上。所以本机装的 objdump 未必能读,得用对应架构的那一个,工具链里通常会带好几个版本,名字带前缀。
3.2 五种用法
text
┌──────────────────────────────────────────────────────────┐
│ objdump 的五种用法 │
├──────────────────────────────────────────────────────────┤
│ 同一个工具,换个参数就是另一件活 │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ -d 反汇编,看指令 │ │
│ │ -t 符号表,看函数名与地址 │ │
│ │ -h 段头,看各段位置与大小 │ │
│ │ -x 全部头信息,比 -h 全 │ │
│ │ -p 配合别的参数指定平台 │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 日常用得最多的是 -d 和 -t │
└──────────────────────────────────────────────────────────┘
日常用得最多的是反汇编和符号表这两样。符号表在不在,直接决定了后面定位的难易程度。带符号的二进制,函数名、行号都在里面,一步就能找到地方。被剥掉符号的,只剩一堆地址,得自己翻。
判断有没有符号很简单,列符号表试一次,有内容就是有,报没有符号就是被剥了。
3.3 从地址到函数名
text
┌──────────────────────────────────────────────────────────┐
│ 从地址到函数名 │
├──────────────────────────────────────────────────────────┤
│ 只知道一个地址,也能找到是哪段代码 │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 第一步 日志里拿到出错地址 │ │
│ │ 第二步 看段表,确定落在哪个段 │ │
│ │ 第三步 减去段起始,得到段内偏移 │ │
│ │ 第四步 反汇编,按偏移找指令 │ │
│ │ 第五步 往上翻,找到函数入口 │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 有符号表的话,前四步都能省掉 │
└──────────────────────────────────────────────────────────┘
| 步骤 | 输入 | 输出 |
|---|---|---|
| 拿地址 | 崩溃日志 | 一个十六进制地址 |
| 定段 | 地址与段表 | 落在哪个段 |
| 算偏移 | 地址减段起始 | 段内偏移 |
| 反汇编 | 段内偏移 | 附近几条指令 |
| 找入口 | 指令往上翻 | 函数名 |
这套流程看着长,其实核心就一句话,把绝对地址换成段内偏移,再拿着偏移去反汇编里找。段表告诉你每段从哪开始,减一下就得到偏移。
有符号表的时候这五步能省掉四步,直接拿地址问行号就行。这也是我一直建议开发版本保留符号的原因,剥掉符号省下的那点空间,远不如定位效率值钱。
3.4 几个工具怎么选
text
┌──────────────────────────────────────────────────────────┐
│ 二进制工具怎么选 │
├──────────────────────────────────────────────────────────┤
│ 几个工具长得像,干的活不一样 │
│ │
│ +------------+--------------------+------------------+ │
│ |工具 |擅长什么 |什么时候用它 | │
│ +------------+--------------------+------------------+ │
│ |objdump |反汇编与段信息 |要逐条看指令 | │
│ |readelf |读文件头与段表 |只看结构不关心指令| │
│ |nm |列符号 |查函数在不在 | │
│ |addr2line |地址换行号 |有符号时一步到位 | │
│ |strings |抽可打印串 |找版本号与路径 | │
│ |file |看文件类型 |架构对不对先问它 | │
│ +------------+--------------------+------------------+ │
└──────────────────────────────────────────────────────────┘
这几个工具经常被人混着用,其实各有分工。看结构用 readelf,看指令用 objdump,查符号用 nm,地址换行号用 addr2line,找版本号和路径用 strings 最快。
上手的时候先跑 file 看一眼架构。架构对不上,后面所有工具读出来的都是错的,而且不报错,能坑你半天。
3.5 测试清单
| 项目 | 预期结果 | 判定标准 |
|---|---|---|
| 发布包架构 | 与目标机一致 | file 命令确认 |
| 符号表 | 按版本策略保留 | 调试版必须保留 |
| 动态库依赖 | 依赖都在包里 | 不能缺库 |
| 版本号字符串 | 与发布单一致 | strings 能查到 |
| 段权限 | 没有可写可执行段 | 安全项必查 |
| 崩溃地址 | 能定位到函数 | 定位流程跑得通 |
| 刷机包完整性 | 校验值对得上 | 坏包必须拦住 |
| 关键函数 | 存在于二进制中 | 裁剪不能裁错 |
第五项跟安全加固有关。既可写又可执行的段是典型的危险配置,扫一遍就能发现,成本很低收益很高。
4 netstat
4.1 四件事
text
┌──────────────────────────────────────────────────────────┐
│ netstat 能看的四件事 │
├──────────────────────────────────────────────────────────┤
│ 连接这件事,netstat 能拆成四块看 │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 监听 服务有没有把端口开出来 │ │
│ │ 连接 谁连着,处在哪个状态 │ │
│ │ 路由 包往哪条路出去 │ │
│ │ 统计 重传、错包、丢包各有多少 │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 绝大多数时候只看前两块 │
└──────────────────────────────────────────────────────────┘
绝大多数时候只看前两块。端口有没有开出来,连接处在什么状态,这两条能覆盖日常八成的排查。路由和统计留给网路相关的专项。
4.2 状态怎么读
text
┌──────────────────────────────────────────────────────────┐
│ 几个关键状态的含义 │
├──────────────────────────────────────────────────────────┤
│ 状态停在哪个格子,决定了问题在谁身上 │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ LISTEN 等着被连,服务正常该在这 │ │
│ │ ESTABLISHED 正在通信,双方都在 │ │
│ │ TIME_WAIT 本端主动关,等一会儿 │ │
│ │ CLOSE_WAIT 对端关了,本端没关 │ │
│ │ SYN_RECV 握手收了一半 │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ CLOSE_WAIT 越堆越多,是本端自己没关 │
└──────────────────────────────────────────────────────────┘
| 状态 | 责任方 | 处理方向 |
|---|---|---|
| TIME_WAIT 多 | 正常现象 | 不用管 |
| SYN_RECV 堆着 | 对端或链路 | 查网路与防火墙 |
| CLOSE_WAIT 堆着 | 本端应用 | 查关闭逻辑 |
| ESTABLISHED 涨 | 连接没回收 | 查超时与上限 |
判断责任方主要靠状态。TIME_WAIT 多不用紧张,那是本端主动关连接之后的正常等待,等一会儿自己就收了。SYN_RECV 堆着要查对端,通常是握手包发不过来或者被丢了,问题在链路或者防火墙。
CLOSE_WAIT 是唯一一个能直接判定本端有问题的状态。它的含义是对端已经关了,本端还没关,堆着说明应用收到关闭之后没去处理。开篇那个句柄泄漏的案子,现场如果先数一下 CLOSE_WAIT,能早半天定位到方向。
4.3 堆积怎么看
数状态就一条命令,排序取前几名。
text
netstat -n | awk '/^tcp/ {print $6}' | sort | uniq -c | sort -rn
采数跟句柄一样,要连着采。一次的数说明不了问题,隔十分钟采一次,看哪个状态在涨。涨的那个就是方向。
阈值这件事给不出统一答案。连接数跟机型、跟业务量都有关系,可行的办法是取稳定运行时的数值当基线,异常时跟基线比,涨了三倍以上再去查。
4.4 参数
text
┌──────────────────────────────────────────────────────────┐
│ netstat 高频参数 │
├──────────────────────────────────────────────────────────┤
│ 参数 │
│ │
│ +------------+----------------------+----------------+ │
│ |参数 |干什么 |看什么 | │
│ +------------+----------------------+----------------+ │
│ |-t |只看 TCP |大部分场合够用 | │
│ |-u |只看 UDP |告警外发那条路 | │
│ |-l |只看监听 |服务起没起 | │
│ |-n |用数字不解析 |输出快、不误读 | │
│ |-p |带上进程信息 |哪个进程开的口 | │
│ |-a |全部连接 |含已建立的 | │
│ |-s |协议统计 |重传与错包 | │
│ |-r |路由表 |包从哪出去 | │
│ +------------+----------------------+----------------+ │
└──────────────────────────────────────────────────────────┘
4.5 与 ss 的关系
新一点的系统上 ss 更快,原理不一样,读的是内核里另一套接口,连接多的时候差距很明显。命令参数长得像,输出内容也差不多。
| 对比项 | netstat | ss |
|---|---|---|
| 数据来源 | 传统接口 | 内核新接口 |
| 连接多时速度 | 慢 | 快 |
| 参数形态 | 传统短参数 | 差不多 |
| 裁剪版系统 | 可能只留有 ss | 也可能只留 netstat |
两个都学会最稳。有的裁剪版系统里只留了一个,到现场发现没有 netstat 的时候不至于干瞪眼。写检查脚本的时候优先用有的那个,别写死。
4.6 测试清单
| 项目 | 预期结果 | 判定标准 |
|---|---|---|
| 服务启动后 | 443 与 80 在监听 | 缺一个就是缺陷 |
| 服务停止后 | 监听消失 | 不能残留 |
| 建连再断开 | 状态正常回收 | 不堆积 CLOSE_WAIT |
| 异常断开 | 本端能感知 | 超时后要释放 |
| 大量并发 | 状态不失控 | 达到上限有保护 |
| 防火墙只放行一个口 | 另一个连不上 | 要跟策略一致 |
| 告警外发 | 有到监控机的连接 | 方向要对 |
| 长时间运行 | 各状态数持平 | 增长要有上限 |
| 统计里的重传 | 数值很低 | 涨了要查链路 |
第三项的验证要造异常断开。拔网线、杀客户端进程、让对端直接复位,三种情形都要试。只测正常关连接,等于没测断线重连。
5 GDB 与栈回溯
5.1 core 是怎么来的
core 是进程崩溃那一刻的内存快照,把它存下来,事后就能回到那个时刻看现场。要拿到 core 得满足几个条件,缺一个都拿不到。
一是开关。系统有个限制值,限制为零就不生成。测之前先确认这个值是放开的,不然等崩溃了才发现没存下来,只能再等下一次。
| 拿不到 core 的原因 | 表现 | 怎么办 |
|---|---|---|
| 限制值为零 | 崩了没有文件 | 先放开限制 |
| 存放路径不可写 | 没有文件 | 改到大分区 |
| 空间不够 | 文件只有一半 | 预留够空间 |
| 版本不对应 | 栈里全是乱的 | 找同版二进制 |
二是存放位置。生成的 core 往哪写、叫什么名字,由一个内核参数控制。有的系统默认写到当前目录,有的走统一的收集服务。测试环境里都把它指到一个固定的大分区,免得写满根分区。
三是空间。core 的体积跟进程用了多少内存有关,大的能到几百兆。分区不够就会写一半,写一半的 core 打不开,等于白等。
四是版本对应。core 是某一版程序跑出来的,拿另一版的二进制去读,栈里全是乱的。这一条最容易被忽略,也最浪费时间,因为栈看着能打出来,只是每一层都对不上号,不比对版本的人会一直沿着错的方向查下去。
5.2 五步
text
┌──────────────────────────────────────────────────────────┐
│ 从 core 到结论 │
├──────────────────────────────────────────────────────────┤
│ 拿到 core 之后按这个顺序往下走 │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 第一步 确认 core 跟程序是同一版 │ │
│ │ 第二步 载入,先打一次 bt │ │
│ │ 第三步 看所有线程,找出事那条 │ │
│ │ 第四步 切到崩的那一帧,看变量 │ │
│ │ 第五步 往上看调用方,还原场景 │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 版本没对上,后面四步全是白看 │
└──────────────────────────────────────────────────────────┘
第一步说的版本对应,验的办法是比对二进制的构建编号或者校验值。对不上就别往下走了,先去找对应版本的二进制。
5.3 栈回溯怎么读
text
┌──────────────────────────────────────────────────────────┐
│ 栈回溯怎么读 │
├──────────────────────────────────────────────────────────┤
│ 栈回溯的每一行都长一个样,含义固定 │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ #0 崩的那一行,最该看 │ │
│ │ #1 谁调了它 │ │
│ │ #2 再往上一层 │ │
│ │ #N 一直排到线程入口 │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 一行里有四样东西:帧号、函数名、偏移、参数 │
└──────────────────────────────────────────────────────────┘
| 部分 | 含义 | 没有符号时 |
|---|---|---|
| 帧号 | 第几层,零号是崩点 | 照常显示 |
| 函数名 | 停在哪一个函数 | 显示成地址 |
| 偏移 | 函数内的位置 | 照常显示 |
| 参数 | 传进去的值 | 可能显示为地址 |
一行栈里有四样东西。帧号在最前,从零开始,零号就是崩的那一层。后面是函数名,再后面是偏移,最后括号里是参数。没有符号的时候函数名会显示成一串问号或者地址,那就只能回到 objdump 那套流程。
读栈的顺序也有讲究。先看零号确认崩在哪一句,再往上翻找调用方,一直翻到能认出来的业务函数为止。栈往往很长,底下那些是标准库和线程入口,一般不用管。
5.4 命令
text
┌──────────────────────────────────────────────────────────┐
│ GDB 常用命令 │
├──────────────────────────────────────────────────────────┤
│ 命令不多,够用就行 │
│ │
│ +-------------------+------------------+-------------+ │
│ |命令 |干什么 |什么时候敲 | │
│ +-------------------+------------------+-------------+ │
│ |bt |打印调用栈 |进来第一件事 | │
│ |bt full |连带局部变量 |要看变量值 | │
│ |frame N |切到第 N 帧 |定位某一层 | │
│ |info locals |看当前帧的变量 |怀疑值不对 | │
│ |print 表达式 |打印某个值 |看具体字段 | │
│ |info threads |列出线程 |多线程崩溃 | │
│ |thread N |切到某线程 |找到出事那条 | │
│ |thread apply all bt|所有线程打栈 |死锁卡死 | │
│ +-------------------+------------------+-------------+ │
└──────────────────────────────────────────────────────────┘
命令不多,够用就行。真正高频的就是前几条,打栈、切帧、看变量,三件事循环着做。
5.5 多线程与死锁
多线程崩溃要看所有线程的栈,出事的线程未必是当前那个。先列线程,再逐个看,或者一条命令把所有线程的栈都打出来。
死锁的情形跟崩溃长得不一样。进程还在,不退出,就是不干活。这种时候所有线程的栈都得看,重点找停在加锁那一步的线程,看它在等哪把锁,再看那把锁在谁手里。两下对起来,死锁的环就出来了。
卡死还有一种常见原因,线程卡在读写上。栈里会停在读或者写那一步,这时候去看它操作的那个句柄,配合 lsof 一起看,往往能发现对端早就不在了。
5.6 常见崩溃类型
| 类型 | 栈里的表现 | 常见原因 |
|---|---|---|
| 访问空指针 | 零号帧地址很小 | 指针没判空就用 |
| 越界写 | 崩在分配区相关函数 | 缓冲区溢出 |
| 重复释放 | 崩在释放函数 | 同一块内存放了两次 |
| 栈溢出 | 栈特别深或地址异常 | 递归或大局部数组 |
| 断言失败 | 停在断言处理函数 | 内部一致性检查 |
| 死锁 | 进程在但不动 | 加锁顺序不一致 |
| 除零 | 崩在算术指令 | 分母没保护 |
| 指令异常 | 地址看着不像代码 | 函数指针错或栈被踩 |
第六项没法从单次栈里看出来,要比较多个线程的状态,看它们各自卡在哪把锁上。
5.7 测试清单
| 项目 | 预期结果 | 判定标准 |
|---|---|---|
| 崩溃后生成 core | 文件完整可读 | 不能写一半 |
| core 与版本 | 能对上 | 对不上要能查到 |
| 栈回溯 | 能定位到函数 | 有符号时到行 |
| 多线程崩溃 | 能看出事线程 | 所有线程的栈都在 |
| 死锁 | 能看出锁的环 | 两条栈对得上 |
| 看门狗复位 | 留得下现场 | 复位前要能存 |
| 分区不足 | 有明确处理 | 不能静默丢弃 |
| 连续崩溃 | 有抑制机制 | 不能反复重启 |
| 敏感信息 | core 里不含明文口令 | 安全项必查 |
最后一项容易被漏。core 是内存快照,内存里有什么它就有什么,口令、令牌、密钥都在里面。core 往外传的时候要确认脱敏,或者干脆只在受控环境里分析。
6 ipmitool
6.1 三条路
text
┌──────────────────────────────────────────────────────────┐
│ ipmitool 的三条路 │
├──────────────────────────────────────────────────────────┤
│ 同一个工具,换参数就换了条路 │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 本机 走系统接口,要在主机上跑 │ │
│ │ 局域网 走网口,测试主力通道 │ │
│ │ 串口 走串口,网断后的备用 │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 通道不同,能用的命令范围也不一样 │
└──────────────────────────────────────────────────────────┘
| 通道 | 在哪跑 | 什么时候用 |
|---|---|---|
| 局域网 | 任意可达的机器 | 日常主力 |
| 本机 | 主机操作系统里 | 验带内通不通 |
| 串口 | 串口线连着 | 网断后的手段 |
局域网那条路是测试主力,绝大多数用例都走它。命令里要带地址、用户名、密码,有的机型还要指定接口类型。
text
ipmitool -I lanplus -H <addr> -U <user> -P <pass> <command>
本机那条路要在主机操作系统里跑,靠驱动跟 BMC 通信,不经过网口。这条路的价值在于能验证带内通不通,网口全断的时候它还能用。
串口那条路平时没人用,网断了、系统也进不去的时候,它是唯一剩下的手段。验收时至少确认一次它能通,别等到真出事才发现线序不对。
6.2 子命令五堆
text
┌──────────────────────────────────────────────────────────┐
│ 子命令怎么分组 │
├──────────────────────────────────────────────────────────┤
│ 子命令按用途分成五堆 │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 设备 设备信息、看门狗、复位 │ │
│ │ 传感器 读数、阈值、描述表 │ │
│ │ 日志 列出、清空、看容量 │ │
│ │ 资产 读写 FRU 那几块区域 │ │
│ │ 控制 电源、网络、用户、串口 │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 验收清单基本就是照这五堆往下排 │
└──────────────────────────────────────────────────────────┘
验收清单基本就是照这五堆往下排。设备这堆先做,确认 BMC 本身活着。传感器和日志两堆是重点,用例最多。资产这堆放末尾,因为它带写操作,写坏了会影响别的。
6.3 完成码怎么读
第四章讲过完成码,这里补一句实操上的经验。ipmitool 对某些完成码会直接给出人能读的文字,对另一些只给十六进制。遇到十六进制的,对照完成码表查一下,别跳过。
| 判断依据 | 命令不支持 | 参数越界 |
|---|---|---|
| 换参数再试 | 还是同样的错 | 可能就对了 |
| 换机型再试 | 有的机型能过 | 都错 |
| 归属 | 固件裁剪 | 脚本写错 |
命令不支持和参数越界,这两种码特别容易混淆。前者说明这台机器压根没实现这条命令,后者说明命令有但参数写错了。区分开很重要,前者可能是机型裁剪,后者是脚本的问题。
6.4 与 curl 的分工
| 对比项 | ipmitool | curl 打 Redfish |
|---|---|---|
| 走的协议 | IPMI | Redfish |
| 数据形态 | 二进制命令 | 文本结构 |
| 认证 | 用户名密码加会话 | 会话令牌 |
| 读传感器 | 一条命令全出 | 要遍历资源 |
| 改配置 | 部分支持 | 覆盖更全 |
| 批量巡检 | 快,开销小 | 慢一点,结构清楚 |
| 老机型 | 普遍支持 | 不一定有 |
| 排障定位 | 看完成码 | 看状态码 |
分工上我这么安排,批量巡检和快速确认走 ipmitool,功能验证和自动化走 Redfish。两条路都通的时候对比一下结果,对不上就要查,多数是对不上才发现问题。
6.5 测试清单
| 项目 | 预期结果 | 判定标准 |
|---|---|---|
| 三种通道 | 都能通 | 串口那条别漏测 |
| 读设备信息 | 返回完整 | 固件版本要对得上 |
| 列传感器 | 条目数与规格一致 | 不能少项 |
| 读一个数 | 与界面一致 | 允许取整差 |
| 列日志 | 能列出全部 | 时间顺序正确 |
| 清日志 | 清完为空 | 低权限要被拒 |
| 读资产 | 字段完整 | 厂商型号不能空 |
| 写资产 | 写后能读回 | 校验和要正确 |
| 错误参数 | 返回对应完成码 | 不能返回成功 |
| 低权限 | 越权命令被拒 | 完成码要明确 |
| 连续执行两百次 | 全部有响应 | 不能出现超时 |
| 网口断开 | 报错明确 | 不能卡住不返回 |
最后一项往往被跳过。网断了之后命令是立刻报错还是卡到超时,直接决定上层脚本的健壮性,卡住不返回的那种要提缺陷。
7 收口
7.1 六件工具谁管什么
text
┌──────────────────────────────────────────────────────────┐
│ 六件工具谁管什么 │
├──────────────────────────────────────────────────────────┤
│ 六件工具的分工 │
│ │
│ +------------+------------------------+--------------+ │
│ |工具 |回答什么问题 |在哪一层 | │
│ +------------+------------------------+--------------+ │
│ |curl |接口通不通,返回什么 |对外接口 | │
│ |ipmitool |带外还能不能用 |对外接口 | │
│ |netstat |连接处在什么状态 |本机状态 | │
│ |lsof |进程抓着哪些东西 |本机状态 | │
│ |objdump |这个文件里有什么 |二进制 | │
│ |GDB |崩的那一刻在干什么 |二进制 | │
│ +------------+------------------------+--------------+ │
└──────────────────────────────────────────────────────────┘
六件工具不是六选一,多数时候是连着用。外面的现象往里追,里层的证据往外对,来回对上才算定位完成。
7.2 一轮排障怎么走
text
┌──────────────────────────────────────────────────────────┐
│ 一轮排障怎么走 │
├──────────────────────────────────────────────────────────┤
│ 现象在外面,原因可能在任何一层 │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 第 1 步 ipmitool 确认带外还活着 │ │
│ │ 第 2 步 curl 打接口,看状态码 │ │
│ │ 第 3 步 netstat 看端口有没有开 │ │
│ │ 第 4 步 lsof 看进程抓着什么 │ │
│ │ 第 5 步 日志里找出错地址 │ │
│ │ 第 6 步 objdump 定位到函数 │ │
│ │ 第 7 步 core 加 GDB 看现场 │ │
│ │ 第 8 步 复现确认,再提缺陷 │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 前四步现场就能做,后四步多半要回实验室 │
└──────────────────────────────────────────────────────────┘
前四步在现场就能做,不需要额外准备。后四步多半要回实验室,因为要拿 core、要拿带符号的二进制、要复现。
复现这一步没法省。定位到一个可疑函数,不改任何东西再跑一遍,看是不是还在同一个地方崩,确认了再提缺陷,不然很容易提错方向。
7.3 几个容易踩的地方
| 坑 | 表现 | 怎么绕 |
|---|---|---|
| core 没生成 | 崩溃后找不到文件 | 测前先确认开关和位置 |
| core 与版本不符 | 栈里全是乱的 | 先比对构建编号 |
| 符号被剥掉 | 函数名显示成问号 | 保留调试版二进制 |
| 架构不对 | 读出来是错的不报错 | 先用 file 确认架构 |
| 只看一次数 | 判断不了是不是泄漏 | 定时采数画曲线 |
| 000 当应用层错 | 查错方向 | 先查网路和端口 |
| 把 202 当成功 | 后面失败抓不到 | 跟着任务链接看终态 |
| 命令卡住不返回 | 脚本整轮挂死 | 必测断网情形 |
| core 里带敏感信息 | 外传等于泄露 | 分析放在受控环境 |
第三条是吃过亏换来的。有一轮拿到 core 高高兴兴去打栈,栈里全是地址,回头问开发要带符号的版本,那版早就被清理了,只能从头复现一遍。那之后,测试环境的每一版二进制我都留一份带符号的。
7.4 回归顺序
做一轮完整的工具链回归,按这个顺序排最省事。
| 顺序 | 内容 | 为什么放这 |
|---|---|---|
| 1 | ipmitool 确认带外可用 | 后面全靠它兜底 |
| 2 | curl 打根路径与登录 | 先确认接口活着 |
| 3 | 跑一轮 Redfish 读取类 | 只读不改,先跑 |
| 4 | 跑一轮 IPMI 读取类 | 同上,不破坏状态 |
| 5 | netstat 与 lsof 采基线 | 干净状态才有基线 |
| 6 | 跑写入类用例 | 会改状态,放后面 |
| 7 | 跑稳定性,定时采数 | 时间最长,放后面 |
| 8 | 造崩溃,验 core 流程 | 破坏性最大,放最后 |
第五步的基线一定要在写入之前采。写入用例跑完之后机器状态已经变了,那时候采的基线没有参考价值。
| 采什么 | 多久一次 | 用什么 |
|---|---|---|
| 句柄数 | 每小时 | lsof 配进程号 |
| 连接状态 | 每小时 | netstat 配排序 |
| 接口响应 | 每十分钟 | curl 取状态码 |
| 内存占用 | 每小时 | 系统自带命令 |
第七步耗时长,安排的时候要留出足够的窗口。稳定性问题多数要跑三天以上才显现,跑一天就下结论,很容易放过泄漏类的缺陷。