【Linux】如何定位客户端程序的问题
文章目录
- 1 客户端程序和服务端程序的差别
- 2 问题类型
- 2.1 崩溃(crash)
- 2.2 CPU高
- 2.3 内存高
- 2.4 线程卡死
- 3 总结
1 客户端程序和服务端程序的差别
客户端程序是运行在终端上,通常都会与业务系统共存,而服务端程序通常会运行在单独的节点上,或者作为业务系统与客户端程序共存。
客户端程序主要关注的是资源占用
和稳定性
:
- 资源占用:客户端程序和业务系统共存,不能占用太多宿主机的资源,因此,客户端程序需要有能力限制自身的资源利用率,例如使用cgroup等机制
- 稳定性:客户端程序需要使用操作系统的许多机制,有时候还会使用内核模块,这可能会导致系统崩溃,在使用时需要详细评估
服务端程序主要关注的是业务功能
和性能
:
- 业务功能:前端作为展示入口,大量的功能需要服务端提供
- 性能:用户在操作系统时,最直观的感受是页面渲染的速度,这一方面包括前端页面展示的速度,另一方面还包括后端处理的速度,在使用一些消息队列时,还需要关注消息队列的拥塞导致的性能下降的问题
2 问题类型
2.1 崩溃(crash)
崩溃是程序遇到无法处理的错误时异常退出,对于程序来说,通常有两类错误:
- 预期会产生的错误,这种错误可以打印日志,然后让程序继续执行,或者进行重试
- 非预期的错误,这种错误通常无法完全避免,可能是代码缺少异常处理逻辑,也可能是并发处理造成的异常
对于预期会产生的错误,通常会有语言级别的处理机制:
- golang中有panic/recover
- c++/java中有try/catch
- python中有try/except
而对于非预期的错误,通常会造成程序崩溃,通常只能在问题出现时进行定位。
当程序崩溃时,最常用的手段是通过调用栈
定位出现问题的代码行,然后根据出现问题代码行确认问题出现的原因和场景。
对于C++语言来说,如果是在开发环境崩溃,可以通过配置coredump的参数使得程序崩溃时生成core文件,然后使用gdb program core
命令打开core文件,再执行bt
查看崩溃时的堆栈。如果之前程序崩溃时没有拿到core文件,现在希望复现得到堆栈,可以直接使用gdb命令启动程序:gdb program
或者gdb --args program args
(分别对应程序启动时不带参数和带参数的情况),这种方式可以用于处理不能复现的崩溃。
如果是在生产环境崩溃,那肯定就不能直接使用gdb定位:
- 生产环境执行的程序一般没有调试信息,也就是编译时不会带上
-g
选项 - 生产环境的权限一般管控很严格,不能随意登录和执行命令
如果是自有的服务器,可以开启服务器的coredump的参数,使得程序崩溃时生成coredump,当监控程序发现新的coredump文件,可以将coredump文件上报再将本地文件删除,开发人员可以从服务端下载coredump,然后使用gdb打开coredump找到对应的代码行。
以下面的代码为例:
// main.cpp
#include <iostream>void func() {int *ptr = nullptr;*ptr = 0;
}int main() {func();
}
上述代码会在func()函数中的*ptr = 0
处崩溃。
使用g++ -o main main.cpp
编译上述程序,此时的二进制中是包含符号信息的(file main
的输出包含字符串not stripped
,使用nm main
也可以看到输出了很多符号名),使用strip main
将符号信息移除(使用nm main
可以确认符号信息已被移除,输出no symbols
),而二进制还是可以执行。
然后执行main二进制会发现生成core文件(当然,前提是已经配置过coredump的参数)。
执行gdb
,然后在gdb中加载生成的core文件:core-file core-main.3272.1714987794
(里面的core-main.3272.1714987794就是生成的core文件),此时用bt
命令查看崩溃堆栈时会发现只有地址(崩溃的地址为0x000056382360917d
)而不知道实际调用栈,而且这个地址还是加载到内存中的地址。然后执行info proc mappings
可以查看内存加载的映像:
Mapped address spaces:Start Addr End Addr Size Offset objfile0x563823608000 0x563823609000 0x1000 0x0 /root/crash/main0x563823609000 0x56382360a000 0x1000 0x1000 /root/crash/main0x56382360a000 0x56382360b000 0x1000 0x2000 /root/crash/main0x56382360b000 0x56382360c000 0x1000 0x2000 /root/crash/main0x56382360c000 0x56382360d000 0x1000 0x3000 /root/crash/main
根据崩溃的地址可以找到代码段的偏移量为0x1000
,因此,崩溃的地址在二进制文件中的地址为0x117d
,然后使用IDA软件打开二进制文件,会发现崩溃的指令为:
对照源代码,也能够发现代码是崩溃在*ptr = 0
。
如果不想使用IDA,也可以使用addr2line
工具通过该地址得到符号信息:addr2line -f -e main.bak -a 0x117d
(此处的main.bak是执行strip命令前的带符号信息的二进制),会发现输出信息只包含所在的函数的名称,而且还是编译之后的函数名,也可以大致猜出对应的函数名,不过还是没有定位到具体的行,具体的行还是需要使用IDA软件查看。
总之,定位crash的方法主要就是通过生成的core文件中的堆栈地址得到源代码指令,然后再结合日志确定crash的原因和场景。
不过,这里有个问题:如果程序运行在自己的机器或者是定位可以复现的机器,是可以先配置core文件的大小和路径的,然后运行程序得到core,如果程序运行在其他人的机器上呢?难道每次都要求对方先配置下core吗?而且还需要管理生成的core文件,防止撑爆磁盘。
因此,需要有一种能力,可以在程序崩溃时自动生成core,并且将core上传到某个地方进行统一分析,Google的Breadpad就可以完成这项工作,它生成的core文件比较小,适合上报到服务端进行分析。
综上:
- 想办法获取到程序崩溃的堆栈文件(开发环境配置crash路径,生产环境使用Breadpad)
- 根据堆栈文件查看崩溃时的函数调用栈以及崩溃地址(如果带符号,可以直接获取到崩溃的代码行)
- 在带符号的二进制查找函数调用栈和崩溃的地址,就可以得到崩溃时的函数调用关系和代码行(根据崩溃的地址找到对应的代码行需要借助IDA)
- 根据崩溃的代码行猜测可能崩溃的原因和出现的场景,原因大部分都是由于访问空指针或者访问已经回收的指针
- 审查代码逻辑,确认出现问题的场景,然后确认修复方案
2.2 CPU高
CPU高分为内核态CPU占比高和用户态CPU占比高,分别表示执行系统调用和执行用户程序的耗时占比。
对于CPU高的问题,首先是能够发现CPU高的进程,这通常是通过top
和pidstat
命令来发现,首先通过top
命令查看当前运行的进程的CPU占比(此处的%CPU包含内核态和用户态),观察一会发现某个进程的CPU占比高,然后使用pidstat -p PID 1 10
查看该进程在10秒内的CPU占比情况,确定该进程是内核态CPU占比高还是用户态CPU占比高,或者是都高。
现在的程序通常都是多线程,如果发现某个进程的CPU高,可以先使用top -H -p PID
确认是哪个线程的CPU高,然后再根据线程名称或者日志确认该线程属于哪个功能模块,当然,如果有每个线程的监控数据的话就可以直接看到哪个线程的CPU高。
再使用perf
工具对进程进行采样,生成该进程的火焰图,确认耗时比较高的操作,可能是某个系统调用比较频繁,而该系统调用又比较耗时,也可能是某个用户态操作执行太过频繁。
在CPU高的场景中有类特殊的场景:死循环。死循环既可以造成CPU高,也可能造成内存高:当某个线程的用户态CPU占比高,而该线程的逻辑中有循环,则可能是死循环导致。
2.3 内存高
与CPU高的问题类似,首先是需要发现内存高的进程,这通常是通过top
和监控程序来发现。
常用的内存分析工具:
- Valgrind:Valgrind是Linux的一套开源的仿真调试工具的集合,由内核和调试工具组成。内核模拟CPU环境,调试工具利用模拟的CPU环境实现各种调试任务。
- Gperftools:Gperftools通过在用户代码中嵌入检测代码实现内存泄漏检测和CPU性能分析,由于不需要模拟器,通常比Valgrind更高效,适合生产环境或者对性能要求比较高的场景。
- Heaptrack:Heaptrack主要用于堆内存分析,可以追踪所有的内存分配并用调用栈进行注释。
- AddressSanitizer(ASAN):内存错误检查器,可以检查堆栈缓冲区溢出、内存泄漏等问题。
2.4 线程卡死
线程卡死是另一类不常出现,但是出现后也不太好定位的问题,从现象上看,就是线程不工作或者不处理任务了。
通常有两种方式发现线程卡死的问题:
- 程序定时打印日志(例如,打印性能指标数据),旁路进程监控日志的打印,如果一段时间没有打印日志,说明线程可能卡死,可以尝试干掉线程
- 多次查看程序的用户态堆栈(
/proc/pid/stack
只能查看内核态堆栈,使用pstack
或者gdb
工具可以查看用户态堆栈),如果每次都一样,该线程可能卡死
使用gdb
查看线程堆栈时,先执行gdb -p PID
连接到某个进程,然后执行info threads
查看所有的线程,每个线程前面有个序号,执行thread XX
(XX就是线程前面的序号)可以切换到该线程,最后执行bt
就可以查看该线程的用户态堆栈,而且,这个时候如果每个线程的都有唯一的名字就可以很容易确认线程的功能模块。
当程序运行在客户环境时,如果需要安装额外的软件是不太合适的,因此,第二种方式通常用于开发或者内部测试环境使用,取而代之的是程序可以使用命令行的方式获取自身的堆栈信息。
C++中可以使用glibc的backtrace
或者第三方的libunwind
:
- backtrace:backtrace适合快速获取当前线程的堆栈信息,开销比较小,在多线程环境中需要增加额外的逻辑
- libunwind:libunwind适合需要精确控制堆栈信息获取过程的场景,支持跨平台,可以为每个线程独立获取堆栈信息
综上:
- 如果某个功能没有打印任何日志,并且也不工作(不处理请求),可以怀疑线程退出和卡死
- 对于
开发和可以安装软件
的环境来说,可以使用pstack
或者gdb
多次查看线程堆栈,确认线程是退出还是卡死以及卡死的代码 - 对于
正式和无法安装软件
的环境来说,程序可以集成libunwind库来获取多线程的堆栈,并提供命令行的方式输出多线程的堆栈,多次获取多线程的堆栈,确认线程是退出还是卡死
3 总结
不管什么语言开发的程序,都会遇到两类比较棘手的问题:崩溃和性能问题,其中性能问题又可以分为CPU占用高和内存占用高。
对于崩溃,不同的语言有自己处理方式:
- C/C++需要获取core文件,根据堆栈分析出现问题的代码行,开发环境可以配置core路径,正式环境可以使用Breadpad
- golang中可以将GOTRACEBACK设置为crash让程序崩溃时生成core文件(但是,发现用gdb打开时没有程序的符号名,并且core文件很大),也可以在程序退出时利用
runtime/debug
中的Stack()
获取堆栈 - lua是脚本语言,不会由于指针问题崩溃,但是在出现无法处理问题的时候也会崩溃(例如,给cjson.decode()传递不是json的字符串)而退出lua线程,因此,lua提供了
xpcall()
函数,将函数放在xpcall()
中执行时如果出现问题得到调用堆栈
对于CPU高和内存高的问题,统一算作性能问题,性能问题需要通过perf
和valgrind
工具进行分析,找到导致问题的代码,然后重新审查代码,再给出优化方案。
对于线程卡死的问题,需要结合堆栈和代码,确认线程是已经退出,还是执行慢,或者是卡死在某个操作中。
相关文章:

【Linux】如何定位客户端程序的问题
文章目录 1 客户端程序和服务端程序的差别2 问题类型2.1 崩溃(crash)2.2 CPU高2.3 内存高2.4 线程卡死 3 总结 1 客户端程序和服务端程序的差别 客户端程序是运行在终端上,通常都会与业务系统共存,而服务端程序通常会运行在单独的节点上,或者…...

AI学习指南数学工具篇-PCA基础知识
AI学习指南数学工具篇-PCA基础知识 1. PCA是什么? PCA,即主成分分析(Principal Component Analysis),是一种常用的数据降维技术。它通过线性变换将原始数据投影到一个新的坐标系中,旨在找到数据中的“主成…...

《系统架构设计师教程(第2版)》第4章-信息安全技术基础知识-02-信息加密技术
文章目录 1. 信息加密技术1.1 数据加密1.2 对称密钥加密算法1)数据加密标准(DES)2)三重DES(Triple-DES)3)国际数据加密算法(IDEA)4)高级加密标准(AES…...

Leetcode 404:左叶子之和
给定二叉树的根节点 root ,返回所有左叶子之和。 思路:遍历树,寻找左叶子节点; 如果判断是左叶子节点,就更新sum。 public static int sumOfLeftLeaves(TreeNode root){int sum0;sumcompute(root,sum);return sum;}/…...

Keil问题解决:结构体数组初始化,初始化后的值不是目标值
省流:使用的编译器为compiler version 6,切换为compiler version 5 如果缺少编译器,请参考:Keil手动安装编译器V5版本 结构体定义: typedef struct _TASK_COMPONENTS {uint8_t Run; // 程序运行标…...

C++set关联式容器
Cset 1. 关联式容器 vector、list、deque、forward_list(C11)等STL容器,其底层为线性序列的数据结构,里面存储的是元素本身,这样的容器被统称为序列式容器。而map、set是一种关联式容器,关联式容器也是用来存储数据的࿰…...

Celery Redis 集群版连接和PyCharm启动配置
目录 使用Redis cluster版作为broker原因 PyCharm配置 使用Redis cluster版作为broker 在celery5及其之前版本,需要配置如下才可行 celery_app.conf.update( broker_transport_options{“global_keyprefix”: “{celery}:”}, ) 原因 https://github.com/celery/…...

「AIGC算法」readLink实现url识别pdf、网页标题和内容
本文主要介绍AIGC算法,readLink实现url识别pdf、html标题和内容 一、设计思路 识别url是pdf或者网页网页处理逻辑,使用cheerio解析网页PDF处理逻辑,使用pdf-parse解析PDF文件自定义的函数来提取标题和内容二、可执行核心代码 const express = require("express")…...

Vue3+ts(day06:路由)
学习源码可以看我的个人前端学习笔记 (github.com):qdxzw/frontlearningNotes 觉得有帮助的同学,可以点心心支持一下哈(笔记是根据b站上学习的尚硅谷的前端视频【张天禹老师】,记录一下学习笔记,用于自己复盘,有需要学…...

springboot集成dubbo实现微服务系统
目录 1.说明 2.示例 3.总结 1.说明 dubbo官网:https://cn.dubbo.apache.org/zh-cn/ Apache Dubbo 是一款 RPC 服务开发框架,用于解决微服务架构下的服务治理与通信问题,支持多种语言,官方提供了 Java、Golang 等多语言 SDK 实…...

idea使用gitee基本操作流程
1.首先,每次要写代码前,先切换到自己负责的分支 点击签出。 然后拉取一次远程master分支,保证得到的是最新的代码。 写完代码后,在左侧栏有提交按钮。 点击后,选择更新的文件,输入描述内容(必填…...

Docker容器里面有什么东西?
2024年5月15日,周三下午 Docker 容器内部包含了一个运行的应用程序及其依赖环境。当你创建一个 Docker 容器时,你可以指定容器应该运行哪个镜像。这个镜像是由一系列层组成的,每一层包含了一些文件和目录。当你运行这个镜像时,Doc…...

vue基础+高级用法
一、vue基础用法 mvvm的了解/认知 语义化模板mvc - model view controllermvvm - model view view-model vue是如何利用mvvm思想进行开发 双向数据绑定 花括号,构建了数据与视图的双向绑定通过视图绑定事件,来处理数据 生命周期-vue示例 建立&…...

鸿蒙应用布局ArkUI【基础运用案例】
布局基础运用案例 平级导航的复合网格视图 平级导航的复合网格视图常出现在同时展示多种不同内容的界面。 例如,市场类应用作为典型的平级导航,其首页不同板块采用了不同布局能力。 标题栏与搜索栏:因元素单一、位置固定在顶部,…...

GD32F103RCT6/GD32F303RCT6-UCOSIII底层移植(1)工程建立
本文章基于兆易创新GD32 MCU所提供的2.2.4版本库函数开发 后续项目主要在下面该专栏中发布: 手把手教你嵌入式国产化_不及你的温柔的博客-CSDN博客 感兴趣的点个关注收藏一下吧! 电机驱动开发可以跳转: 手把手教你嵌入式国产化-实战项目-无刷电机驱动&am…...

在本地设备上配置 Git 忽略特定文件
在本地设备上配置 Git 忽略特定文件 在日常的 Git 使用中,有时我们希望某些文件只在本地设备上被忽略,而不影响其他团队成员或设备。这篇博客将介绍如何在特定设备上配置 Git 忽略规则,使得一个文件不会被提交。 背景 通常,我们…...

cin.ignore()函数和stoll函数
cin.ignore()函数 cin.ignore() 是一个非常实用的函数,主要用于控制输入流 cin 的行为 cin.ignore(int n 1, char delimiter EOF); n:一个整数参数,表示要忽略的字符数量。默认值是1,意味着只忽略下一个字符。delimiter&#x…...

win11快速安装mysql数据库系统
win11快速安装mysql数据库系统 1、下载 1.1 打开官网 1.2 向下滚动页面 1.3 进入下载选项 1.4 下载8.0.4 LTS 1.5 开始下载 1.6 下载中 2、解压 大家注意,此时解压后目录是没有data目录的。 3、数据库初始化 3.1 管理员身份打开CMD 开始菜单上,输入…...

C# WinForm —— 21 RichTextBox 使用
1. 加载文件到控件中 加载文件时,要设置文件的路径和类型RichTextBoxStreamType,文件类型包含: RichText 0:富文本格式(RTF)流PlainText 1:纯文本流对象链接和嵌入(OLEÿ…...

【数据结构】堆(超详细)
文章目录 前言堆的概念及结构堆的实现堆的向下调整算法(建小堆为例)堆的向上调整算法(建小堆为例)堆的初始化销毁堆堆的插入堆的删除(规定删堆顶的数据)取堆顶元素判断堆是否为空获取堆的个数 完整代码(包括测试代码&a…...

常用正则 JS 持续更新
应用版本号正则验证 正则判断版本号(如:1.2.3 或 1.2.3.4),不允许出现 0.x.x;01.x.x; x.0x.x; x.00.x; x.x.00; x.x.0x/ ^ ([ 1-9 ] \d | [ 1-9 ])( . ([ 1-9 ] \d | \d )) {2,3} $ /0-10 保留一位小数的数…...

YOLO v6 iou_loss dfl_loss一直为0
Question img record infomation path is:…/mydata/images.train_cache.json Train: Final numbers of valid images: 1248/ labels: 1248. 0.1s for dataset initialization. img record infomation path is:…/mydata/images.val_cache.json Convert to COCO format 100%|█…...

FreeRTOS【4】线程挂起和恢复
1.开发背景 基于上一篇指引,成功创建并启动线程后,线程已经开始运行了,但是有时我们需要线程暂停运行,例如某个线程是控制 LED 闪灯的,如果现在需要让 LED 停止工作,单纯的关闭 LED 是没用的,因…...

CPU占用率过高排查
CPU占用率高是设备本身的一种现象,直观表现为display cpu-usage命令查询结果中整机CPU占用率“CPU usage”偏高,如超过70%。在网络运行中CPU高常常会导致其他业务异常,如BGP震荡、VRRP频繁切换、甚至设备无法登录。 通常,整机CPU占…...

关于 vs2019 c++20 规范里的 STL 库里模板 decay_t<T>
(1) 这个模板,在库代码里非常常见。 decay 英文是“衰弱,消减” 的意思,大概能感觉到就是要简化模板参数 T 的类型,去掉其上的修饰符。因为常用且复杂,故单独列出其源码和注释。先举例其应用场景…...

android C++打印堆栈
Android在Java层打印堆栈比较方便,代码如下: try {throw new Exception("Debug xxx call stack"); }catch(Exception e) {e.printStackTrace(); }但是在C模块中能打印调用堆栈吗?怎么打印调用栈呢? 答案是肯定的&…...

MySQL Undo Log、Redo Log、bin Log
Undo Log 回滚日志,用于将数据回滚到之前的状态。 MySQL在进行数据的增、删、改时,会将数据写入到Undo Log日志中。 对于Undo Log存在着insert和update两种类型的数据。插入语句对应的是insert类型,修改、删除语句对应的是update类型。 U…...

vld.ini配置文件说明
vld.ini配置文件说明 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; Visual Leak Detector - 初始化/配置文件 ;; 版权所有 (c) 2005-2017 VLD团队 ;; ;; 本库是自由软件;你可以在自由软件基金会发布的GNU宽通用公共…...

NSS【web】刷题
[SWPUCTF 2021 新生赛]jicao 类型:PHP、代码审计、RCE 主要知识点:json_decode()函数 json_decode():对JSON字符串解码,转换为php变量 用法: <?php $json {"ctf":"web","question"…...

将TailwindCSS默认单位rem转换为px
前言: 我这里需要将 默认的rem 转换为 px 原因是要使用 postcss-px-to-viewport 插件做移动端适配。 在tailwind.config.js文件中进行配置: 注意:这里 padding(内边距)、spacing(外边距)、width…...