分类 Linux运维 下的文章

近来一直在做部门疑难问题的排查工作,最近也准备在周分享上做一个小培训(交流)——“排查问题的方法和思路”。回想历史问题处理,觉得这是个很不错的案例,便回溯了一下排查过程记录下来。

一、问题背景

有一个应用层程序,依赖信号接收配置更新事件。

即程序依赖某个配置文件,当配置更新后,需要通过信号告知程序也做相应的更新。

番外故事篇

正常情况下,上面描述的这个机制是没有问题的。

直到有一天,测试同学跑来跟我说:“你的配置更新有bug”,这才结束了这美好的宁静。

虽然我本能的第一反应也是“傻逼你会用吗”,但是我还是假装很淡定的对他说:“bug发来看下”(实际上心里已经想好接下来要怎么怼他了)。然而,事出意料之外,竟然真的有bug,我亲手写出来的bug竟然真的不生效了????

二、排查过程

第一步,理清排查思路。排查的方向是信号为什么不生效,不生效可能是信号被屏蔽了,因此首先要确认的是信号状态。查看/proc/${pid}/status确认信号状态:

图中只列出了关键的几行,各行含义:

  • SigPng: 表示未决信号
  • SigBlk: 表示阻塞信号
  • SigIgn: 表示忽略信号
  • SigCgt: 表示已经捕获的信号

数值都是十六进制表示的数字,通过对比分析发现,SigIgn行刚好就有我通知更新的信号,说明进程屏蔽了信号。但是这怎么可能,代码中明明是需要捕获这个信号的,为什么在这里就被屏蔽了。

这里一度百思不得其解,没有什么思路继续排查。场景就像是,某天东西突然被偷了,但是没有监控,要找出偷东西的人。于是在经过了多轮精神折磨后,我选择了重启进程。

果然,重启后,问题解决了。然而,bug并没有解决,更恐怖的是,问题无法重现了!BUG根本没法查下去。

这也是一个教训,在测试环境下,有条件去查的时候,尽量不要重启服务,不然问题无法重现就麻烦了。

直到了N天后,测试决定放弃这个bug的时候,问题又重现了,但是测试也不知道是什么操作导致它重现的,它也是测试其他案例时候偶然发现功能又不生效的。这就大大增加了排查难度,前无因后无果,依旧无法下手。

没办法,捋清楚思路,继续往下查。先找两个环境对比信号状态,发现没问题的环境信号是没有被屏蔽,但是有问题的环境信号确实被屏蔽了。这说明,正常情况下程序肯定是没有问题的,一定是有某个外部操作导致了环境异常。

这时候,我的思路放在了是什么操作导致异常上,这也是大部分人都会考虑到的点。于是我通过最近一天的访问日志和历史操作日志来反推可能的操作,但是找了很长时间后,都没有定位到具体触发操作,范围实在是太大了。

。。。

直到一天过去后,我和测试同学才终于找到了触发问题的操作,触发的操作是在前端页面上同步了一下系统时间。因为环境刚启动的时候设备时间不对,测试同学手动同步了一下时间,这才产生了这个问题。

找到了触发条件后,问题就好查了,追踪代码流程,发现同步系统时间后,会把所有的服务重启。具体的流程为:

关键点就在于这重启服务上,我们的web服务是部署了一个apache,apache重启后台服务的时候导致它变成了所有服务的父进程。而创建子进程的时候,所有子进程都会继承父进程的信号屏蔽字,刚好apache就屏蔽了这个信号,导致所有被拉起的子进程都无法接受到这个信号。至此,问题的原因也就明了了。

解决方案

创建子进程后,通过信号集sigset_t相关的函数接口,清空子进程所有的信号屏蔽字。让子进程自己重新注册信号处理函数。

三、思路分析和总结

其实通过重启操作就能明显发现异常的地方是重启,因为重启之后没有现象了,说明问题肯定和重启相关。这个时候只要ps看一下进程状态,是不是被重启过了,再看一下父进程的pid就能大概发现问题了,而不用花大量的时间去定位重现操作。

总结:处理问题的思路太局限了,换个思路可能一下就查出来了,要善于抓住问题的关键点。

ss5项目主页:http://ss5.sourceforge.net/,下载地址:https://sourceforge.net/projects/ss5/files/

先下载下来源码包,解压:

tar -zxvf ss5-3.8.9-8.tar.gz
cd ss5-3.8.9

安装依赖项:

yum install epel-release
yum update -y

yum groupinstall 'Development Tools' -y
yum install gcc automake autoconf libtool make yum-utils wget -y

yum install pam-devel openldap-devel openssl-devel -y

编译程序并安装:

./configure
make && make install

make的时候如果报下面的错误,说明是gcc版本太高了导致,需要使用低版本的(gcc4)来编译:

ss5-3.8.9/src/SS5Core.c:839: undefined reference to `S5DebugUdpRequestInfo'
ss5-3.8.9/src/SS5Core.c:842: undefined reference to `S5ChildClose'
ss5-3.8.9/src/SS5Core.c:843: undefined reference to `S5ChildClose'
ss5-3.8.9/src/SS5Core.c:866: undefined reference to `S5ChildClose'
ss5-3.8.9/src/SS5Core.c:759: undefined reference to `S5DebugUpstreamInfo'
ss5-3.8.9/src/SS5Core.c:397: undefined reference to `S5ChildClose'
collect2: error: ld returned 1 exit status
make[1]: *** [ss5] Error 1
make[1]: Leaving directory `ss5-3.8.9/src'
make: *** [src] Error 2
安装完成后,默认的配置文件在/etc/opt/ss5/ss5.conf,日志文件在/var/log/ss5/ss5.log

修改配置文件,去掉下面两行的注释:

auth    0.0.0.0/0               -               -
permit -    0.0.0.0/0    -    0.0.0.0/0    -    -    -    -    -

这两行是认证相关的参数,去掉注释表示使用默认的认证参数(不需要认证并允许所有IP代理)。ss5程序支持多种认证方式,包括密码认证以及ldap认证等,这里测试使用不需要认证。

服务默认监听1080端口,启动服务的办法:

ss5 -t -u root

参数说明:

  • -t: 使用多线程模式
  • -u: 指定启动用户
  • -p: 指定pid文件路径

测试代理是否成功:

加入systemd服务

配置文件:

[Unit]
Description=SS5 Socks Proxy
After=syslog.target network.target nss-lookup.target

[Service]
Type=forking
PIDFile=/var/run/ss5.pid
ExecStart=/sbin/ss5 -p /var/run/ss5.pid -u root

[Install]
WantedBy=multi-user.target

参考

Install ss5 on CentOS7 to implement SOCKS5 proxy service

Ss5 official document

Socks5 Secure Username and Password Authorization Agreement

一、问题现象

使用item2登录到远程linux后,系统的语言编程了中文:

本来对于中国人来说,使用中文没有什么不好。但是报错信息是中文就导致出问题了不好查,百度和google查不到相关信息。比较尴尬!

二、解决办法

通过locale命令查看当前终端的语言和字符编码信息:

[root@centos7:~]$ locale
LANG=zh_CN.UTF-8
LC_CTYPE="zh_CN.UTF-8"
LC_NUMERIC="zh_CN.UTF-8"
LC_TIME="zh_CN.UTF-8"
LC_COLLATE="zh_CN.UTF-8"
LC_MONETARY="zh_CN.UTF-8"
LC_MESSAGES="zh_CN.UTF-8"
LC_PAPER="zh_CN.UTF-8"
LC_NAME="zh_CN.UTF-8"
LC_ADDRESS="zh_CN.UTF-8"
LC_TELEPHONE="zh_CN.UTF-8"
LC_MEASUREMENT="zh_CN.UTF-8"
LC_IDENTIFICATION="zh_CN.UTF-8"
LC_ALL=

通过第一行输出可以看到语言是zh_CN.UTF-8,说明当前bash使用的是中文语言。

修改方法

~/.bashrc中修改语言控制相关的环境变量:

export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8

然后source ~/.bashrc生效:

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/word-frequency

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

写一个 bash 脚本以统计一个文本文件 words.txt 中每个单词出现的频率。

为了简单起见,你可以假设:

  • words.txt只包括小写字母和 ' ' 。
  • 每个单词只由小写字母组成。
  • 单词间由一个或多个空格字符分隔。

示例:

假设 words.txt 内容如下:

the day is sunny the the
the sunny is is

你的脚本应当输出(以词频降序排列):

the 4
is 3
sunny 2
day 1

说明:

  • 不要担心词频相同的单词的排序问题,每个单词出现的频率都是唯一的。
  • 你可以使用一行 Unix pipes 实现吗?

二、题解

2.1 使用awk

通过NF变量遍历所有字段,存到一个哈希表(数组)中,然后打印出所有的key-value组合,最后通过sort排序。

awk '{for (i = 1; i <= NF; i++) {m[$i]++;}} END {for (i in m) {print i, m[i]}}' words.txt | sort -nr -k 2

2.2 使用xargs

通过xargs的-n参数打印出所有的字段,然后使用uniqsort对字段排序:

cat file.txt | xargs -n 1 | sort | uniq -c | sort -nr -k 2 | awk '{print $2" "$1}'
uniq的-c参数是统计词频

一、磁盘的基本元素

磁盘由多个盘片组成,每个盘片的基本结构为:

磁盘结构

各标识含义:

  • A是磁道,多个磁盘的同一个磁道重叠起来叫做柱面,它包含了很多个扇区。
  • B是几何上的扇区,只做标示,此处无特殊含义。
  • C是扇区,扇区是磁盘的最小组成单元,通常是512字节(有的磁盘时4096字节)。
  • D是磁盘块(簇),块/簇是操作系统虚拟出来的概念,它由多个扇区组成。

读取磁盘数据时,磁盘上的磁头不断旋转变道,然后读取数据。因此寻道时间越短,I/O操作越快,目前磁盘的平均寻道时间一般在3-15ms,一般都在10ms左右。

关于随机IO和顺序IO

一般在测试磁盘性能的时候,都会额外测试一个功能点就是随机读写的性能。与随机读写相对的是顺序读写,他们的区别在于本次IO和上一次IO地址的差别。如果本次IO给出的初始扇区地址,和上一次IO的结束扇区地址,是不是完全连续的,或者相隔不多,则本次IO算是一个顺序IO。如果相差太大,则算一次随机IO。顺序IO,因为本次初始扇区和上次结束扇区相隔很近,则磁头几乎不用换道或换道时间极短,所以读写速度快;而随机IO中磁头需要很长的换道时间,导致磁头不停换道,读写速度非常慢。

为什么存在磁盘块?(簇)

  1. 读取方便:由于扇区的数量比较小,数目众多在寻址时比较困难,所以操作系统就将相邻的扇区组合在一起,形成一个块,再对块进行整体的操作。
  2. 分离对底层的依赖:操作系统忽略对底层物理存储结构的设计。通过虚拟出来磁盘块的概念,在系统中认为块是最小的单位。

我们平常所说的4K对齐也就是指的块大小,它表示操作系统读取磁盘时一次读取的数据大小。如果操作系统一次读取4K,但是块大小只有2K,就相当于一次IO要做2次磁盘寻址。而如果磁盘块大小刚好也是4K,那么一次IO就只需一次寻址。相对而言,磁盘寻址效率是很低的,多一次磁盘寻址肯定会更加导致IO效率低,因此对磁盘进行4K对齐也是提高了系统的IO性能。

二、linux系统下的查看扇区和磁盘块

使用fdisk -l可以看到磁盘的扇区大小:

查看扇区大小

使用tune2fs -l 可以看到读取磁盘的块大小,下面这个磁盘的块大小是4096

块大小

一、问题回顾

问题现象:线上业务,某个进程被卡住了,所有任务都不响应,导致业务中断。

问题原因:程序中调用了system命令,执行了一次pidof命令,然而作者万万没想到这个pidof命令会卡住了,导致整个进程都阻塞了。

排查过程

第一步,确定进程状态,看看进程在干什么:先通过ps命令得到进程的pid,然后执行strace -p ${pid}挂进去。

执行完strace后发现进程一直卡在wait调用上,但是因为程序卡住了,没有更多的信息可以输出了,也不知道程序执行到哪了,所以并不能知道为什么会执行wait卡住。

于是分析执行wait的场景:wait只会出现在创建了子进程、父进程在等待子进程退出的情况下。会不会是某个子进程卡住了导致父进程阻塞呢?这个是很有可能的,但是反复检查代码,没有发现有创建子进程的地方,并且程序就是单进程设计的,没有主动调用wait的场景。所以这一点上被排除了。

那还会是什么原因导致程序卡在wait了?会不会是strace程序分析有问题导致的?当然这种可能性很小,基本可以排除。

然后又想,程序没有主动创建子进程,会不会是通过其他系统函数间接创建了子进程?很多系统函数都会创建子进程出来,典型的是system和popen函数,代码中是不是调用这两个函数?这很有可能!

于是,就在代码中搜索system和popen,还真找到了一处调用system的地方,但是system调用的都是系统命令:

sh -c kill -usr2 `pidof xxx`

看到命令后想到的第一个问题就是:这个会卡住吗?没听说过kill会阻塞,也没听说过pidof会阻塞啊。虽然我不太相信真的是它卡住了,但是我还是不自觉的打开了gdb。。。使用gdb -p ${pid}挂进去,直接输入bt打印出堆栈调用,映入眼帘的刚好就是这个代码所在的system调用。说明,真的就是这个system卡住了。

为了确定到底是kill卡住了还是pidof卡住了,使用ps过滤出来所有执行这个命令的进程:

可以看到,所有执行pid命令的pid是大于kill的,说明是pidof卡住了才导致kill卡住。

再仔细看,系统已经存在多个被卡住的pidof命令了,最早的可以追溯到一个月前。难以相信一个pidof命令会执行一个月。

本想着再查查为什么pidof命令会卡住,但线上业务着急恢复,执行strace和gdb没看出什么信息后就先恢复了业务。后来本地再也无法重现了,也没有找到pidof卡住的真正原因。

解决方案

  1. 去掉了system机制,kill命令直接通过signal函数来完成。
  2. 进程启动的时候,保存一份pid变量,不再执行pidof命令来获取pid。

二、总结和思考

  1. 程序中少使用system或popen调用外部命令。这点是我一直以来都提倡的,排斥使用system是因为调用system要创建子进程,要屏蔽信号,会有各种不确定的因素在里面,不如系统调用简单、安全。我认为一个好的系统,要尽量减少调用外部命令(但我们公司的传统就是喜欢各种脚本、命令调来调去,实在令人头疼)。
  2. 尽可能减少重复的复杂的逻辑。这里的体现就是pidof命令,是否有必要每次都通过外部pidof命令来获取pid?现在大部分的程序都是把pid保存到一个pidfile中,需要用的时候从文件中读取,这种方式来设计是不是会好一些?

本文内容来源于知乎问答:Cache 和 Buffer 都是缓存,主要区别是什么?,根据各回答内容整理得到。

首先整理下两者的概念:

​ cache是缓存,buffer是缓冲。两者从名字来看十分相近,功能并不一样,不仔细琢磨很容易把两者混为一谈。

区别:

  1. 缓存的主要目的是为了提速,系统把部分磁盘的内存放到缓存中来提高运行速度。关键的一点是,如果缓存丢失,并不影响磁盘数据读取,只是读写速度慢一些。
  2. 缓冲的作用恰如其名——起一个缓冲的作用。例如写文件的时候,每次写一个字节,如果每次都把这1个字节写到磁盘,严重影响运行效率。而缓冲的作用就是把这些1字节的数据存起来,到一定的数量之后统一写到磁盘。同时,和缓存不同的是,缓冲区中的数据如果丢失了,数据就会永久丢失(例如:linux系统中dmesg的实现就是一个环形缓冲区,当日志过多时,后来的日志就会刷掉原来的,导致日志信息显示不全)。

一、下载安装

mariadb是属于mysql的一个分支,是其创始人在mysql被卖给oracle之后重新分出来的,maria取自于他女儿的名字。mariadb完全兼容于mysql,在很多新版本的linux系统中,mysql都已经被替换成了mariadb。

mariadb的官网:mariadb官网,下载地址:下载地址。最新稳定版本的下载直链为:

wget https://downloads.mariadb.com/MariaDB/mariadb-10.5.0/bintar-linux-systemd-x86_64/mariadb-10.5.0-linux-systemd-x86_64.tar.gz

首先把安装包下载到本地,然后解压到/usr/local目录:

tar -zxvf mariadb-10.5.0-linux-systemd-x86_64.tar.gz  -C /usr/local/
ln -s /usr/local/mariadb-10.5.0-linux-systemd-x86_64/ /usr/local/mysql

初始化数据库,设定数据存储目录为/appdata/mysql,启动用户为mysql

# 创建mysql用户
useradd -s /sbin/nologin -M mysql
# 创建数据库文件夹
mkdir /appdata/mysql -p
# 初始化数据库
/usr/local/mysql/scripts/mysql_install_db \
    --basedir=/usr/local/mysql \
    --datadir=/appdata/mysql \
    --user=mysql

初始化数据库的过程中如果报错:

> Installing MariaDB/MySQL system tables in '/xxx/mariadb' ...
/usr/local/mariadb/bin/mysqld: error while loading shared libraries: libaio.so.1: cannot open shared object file: No such file or directory
 
Installation of system tables failed!  Examine the logs in
/udata/mariadb for more information.
 
...

说明系统缺少组件库libaio,需要安装手动安装:

# centos
yum install libaio libaio-devel
# ubuntu
apt install libaio1

执行成功后输出:

Installing MariaDB/MySQL system tables in '/appdata/mysql' ...
OK

To start mysqld at boot time you have to copy
support-files/mysql.server to the right place for your system

...

到这里数据库就已经安装完成了,接下来要做的就是配置。

二、配置

修改/etc/mysql/my.cnf,设置pid/socket/log等文件的路径,把它们统一存到/appdata/mysql/run/下:

[mysqld]
datadir=/appdata/mysql
socket=/appdata/mysql/run/mysql.sock
# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0
# Settings user and group are ignored when systemd is used.
# If you need to run mysqld under a different user or group,
# customize your systemd unit file for mariadb according to the
# instructions in http://fedoraproject.org/wiki/Systemd

[mysqld_safe]
log-error=/appdata/mysql/run/mysql.log
pid-file=/appdata/mysql/run/mysql.pid

[mysql]
socket=/appdata/mysql/run/mysql.sock

[mysqladmin]
socket=/appdata/mysql/run/mysql.sock

#
# include all files from the config directory
#
!includedir /etc/my.cnf.d

注意:

  1. /appdata/mysql/run目录要提前创建
  2. 如果修改了socket的路径,还要修改[mysql]和[mysqladmin]段的socket路径,要和[mysqld]中的socket路径一致

设置路径权限:

chown mysql.mysql -R /usr/local/mysql /appdata/mysql

添加mysql命令到系统路径,修改/etc/profile文件:

MYSQL_HOME=/usr/local/mysql
export PATH=$PATH:$MYSQL_HOME/bin

修改后source /etc/profile生效。

三、添加系统服务

3.1 service系统服务

对于使用service命令启动的服务,复制mysql主目录下的support/mysql.server文件到/etc/init.d/

cp /usr/local/mysql/support-files/mysql.server /etc/init.d/mysqld
chmod +x /etc/init.d/mysqld

然后修改文件中的配置:

basedir=/usr/local/mysql # 安装目录
datadir=/appdata/mysql # 数据目录
mysqld_pid_file_path=/appdata/mysql/run/mysql.pid # pid文件目录
注意:配置要和上面my.cnf中的配置一一对应

启动:service mysqld start

添加到开机启动:

chkconfig --add mysqld
chkconfig mysqld on

3.2 systemd系统服务

systemd服务的文件在安装路径/support-files/systemd/mariadb.service

cp support-files/systemd/mariadb.service /etc/systemd/system/mysqld.service

复制完后执行systemctl start mysqld启动服务,然后设置开机启动:

systemctl enable mysqld

四、设置root用户密码

系统服务起来后,可以使用mysqladmin初始化root用户的密码:

mysqladmin -u root password '123456'

如果出现:

image.png

说明没有没有权限登录,需要通过安全模式启动mysql来修改root密码,在my.cnf中添加以下内容:

[mysqld]
skip-grant-tables
skip-networking

然后重启服务,使用root身份登录(不用密码),执行以下命令修改密码:

use mysql;
# 刷新权限
flush privileges;
# 设置密码
set password for 'root'@'localhost' = password('123456');
# 刷新权限
flush privileges;

如果执行命令的时候出现报错:

ERROR 1290 (HY000): The MariaDB server is running with the --skip-grant-tables option so it cannot execute this statement

说明安全模式下的权限还没有更新,要先刷新一下权限才行:

flush privileges;

修改完成后去掉my.cnf中添加的参数,重启服务,使用上面设置的密码登陆就可以了:

Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 11
Server version: 10.4.8-MariaDB MariaDB Server

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> 

一、证书相关

查看证书详细信息:

openssl x509 -in cert.pem -noout -text

查看证书所有者:

> openssl x509 -in cert.pem -noout -subject
subject= /CN=www.dyxmq.cn

打印指纹信息:

> openssl x509 -in cert.pem -noout -fingerprint
SHA1 Fingerprint=7B:29:CE:09:28:4B:38:82:DF:53:14:B0:15:00:03:66:2B:C0:90:9E

转换证书格式:

> openssl x509 -in cert.pem -inform PEM -out cert.der -outform DER

二、测试指令

测试openssl库功能,已ssl3协议连接baidu.com的443端口:

openssl s_client -connect baidu.com:443 -ssl3

一、问题现象

磁盘下有个大文件占满了空间,删除后发现文件不见了,但是磁盘空间并没有释放。

问题原因

文件还被其他进程占用,调用rm后实际上还并没有真正删除,只有当文件解除占用后(引用计数变为0)才会释放掉这部分空间。

解决方案

使用lsof命令查看文件还在被哪个进程占用的,关闭进程接触解除占用:

lsof | grep delete

过滤delete表示显示出文件已经被删除了,但是实际还被进程占有着没释放的文件。

二、场景模拟

通过以下代码模拟出进程占用文件场景,代码的作用是打开文件,然后睡眠100秒,最后结束退出

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char **argv) {
    int fd;

    if (argc < 2) {
        printf("Usage: %s file\n", argv[0]);
        return 0;
    }

    fd = open(argv[1], O_RDONLY);
    if (fd < 0) {
        perror("open error");
        return -1;
    }

    sleep(100);
    close(fd);

    return 0;
}

编译,并在当前目录生成一个1G左右的文件用作测试:

> gcc test.c # 编译程序
> df -h /data # 当前data分区使用了12G,剩余7.5G
Filesystem      Size  Used Avail Use% Mounted on
/dev/vdb1        20G   12G  7.5G  60% /data
> dd if=/dev/zero of=testfile bs=1M count=1024 # 生成一个1G左右的文件
1024+0 records in
1024+0 records out
1073741824 bytes (1.1 GB) copied, 4.7793 s, 225 MB/s
> df -h /data # 目前磁盘使用了13G,剩余6.5G
Filesystem      Size  Used Avail Use% Mounted on
/dev/vdb1        20G   13G  6.5G  66% /data

然后执行程序,程序会卡住并睡眠100S。删除测试文件,查看磁盘空间发现没有被归还:

使用lsof命令查看文件还在被占用,kill掉之后文件解除占用,空间被归还: