工作中的难点问题正是我们知识技术栈全谱查漏补缺的最佳机遇,有问题不可怕,all in、死磕就完事了,哈哈哈~
一 问题回顾
启动进程时,shm_attach()方法报错:
代码语言:javascript复制failed for key 0x6104e88b: No space left on device
当时定位的原因是:
主进程异常退出,导致信号量和共享内存没有回收,资源耗尽,当再次申请资源时,无可用资源导致。
解决方案:清理信号量及共享内存。
二 问题再次剖析
2.1 shm_attach()方法
根据php官方文档中的描述 shm_attach:创建或打开一个共享内存段(Creates or open a shared memory segment),说明:
代码语言:javascript复制shm_attach ( int $key , int|null $size = null , int $permissions = 0666 ) : SysvSharedMemory|false
方法返回一个id,这个id可以用来通过指定的key来访问System V共享内存,第一次调用时创建共享内存段,需要设置参数size 和 可选参数permissions,默认$permissions值为0666。
第二次调用如果使用相同的key,将返回一个不同的SysvSharedMemory实例,但两个实例都访问相同的底层共享内存。size 和 permissions 参数都会被忽略。
2.2 System V共享内存
System V IPC 机制下的共享内存本质是一段特殊的内存区域,进程间需要共享的数据被放在该共享内存区域中,所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中去。共享内存允许一个或多个进程通过同时出现在他们的虚拟地址空间的内存进行通信,而这块虚拟内存的页面被每个共享进程的页表条目所引用,同时并不需要再所有进程的虚拟内存都有相同的地址。
- System V共享内存是一种最为搞笑的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝。
- 为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不要进行数据的拷贝,从而大大提高效率。
- 由于多个进程共享一段内存,因此也需要依靠某种同步机制。
System V 的IPC(Inter-Process Communication,进程间通信)对象有共享内存、消息队列、信号量(灯)。
注意:在IPC的通信模式下,不管是共享内存、消息队列还是信号量,每个IPC的对象都有唯一的名字,称为"键(key)"。通过"键",进程能够识别所用的对象。"键"与IPC对象的关系就如同文件名称于文件,通过文件名,进程能够读写文件内的数据,甚至多个进程能够公用一个文件。而在IPC的通信模式下,通过"键"的使用也能使得一个IPC对象能为多个进程所共用。
2.3 再看问题原因
报错信息是在shm_attach()方法,而错误原因是
代码语言:javascript复制failed for key 0x6104e88b: No space left on device。
比较容易确定非硬盘空间问题,加上已经对shm_attach()方法有了上面的了解,那么就是出在共享内存分配/获取。
进一步定位,由2.2可知,System V的IPC对象有共享内存、消息队列和信号量,其中可查的是共享内存空间和信号量,查询命令使用ipcs,常用命令如下:
代码语言:javascript复制ipcs可用来显示当前Linux系统中的共享内存段、信号量集、消息队列等的使用情况。
命令示例:
ipcs -a或ipc 显示当前系统中共享内存段、信号量集、消息队列的使用情况;
ipcs -m 显示共享内存段的使用情况;
ipcs -s 显示信号量集的使用情况;
ipcs -q 显示消息队列的使用情况;
ipcrm可用来删除对应的共享内存段、信号量、消息队列;
命令示例:
ipcrm -s semid 删除对应的信号量集
ipcrm -m shmid 删除对应的共享内存段
ipcrm -q msqid 删除对应的消息队列
批量删除可以使用命令:
ipcs -s|grep xxx|cut -d" " -f2|xargs -n1 ipcrm -s
ipcs -s|awk '/xxx/{print $2}'|xargs -n1 ipcrm -s
ipcs -s|awk '/xxx/{system("ipcrm -s "$2)}'
for i in echo `ipcs|grep xxx|cut -d" " -f2`; do ipcrm -s $i; done
通过ipcs -m 和 ipcs -s,确认是共享内存和信号量满导致,所以直接的解决方法就是先清理共享内存和信号量:
2.4 根源
为什么会造成共享内存和信号量满?
一个可以想到的原因就是二者在使用时并没有被正常释放。那么就需要其他信息来辅助我们更精确地定位问题。
通过与OP配合,以及当时常出现的问题(现象)结合考虑:
1)发布时间过长,脚本机kill pid失败后等待90s后触发kill -9 pid,而强杀进程可能会导致共享变量和信号量无法正常释放,这是其一;
2)为什么kill pid会无法生效?
通常来说,除非在代码中做了hook处理或触发其他异常情况(权限问题等)导致失败,通常不会触发这个问题;再考虑kill pid命令,等同于kill -15 pid命令,那么是否是我们的进程没有正确感知到这个信号量?
基于这个思路,并在测试环境不断尝试kill pid动作及进程关闭效果(代码日志),最终定位到是所使用的laravel框架版本及依赖的php版本的问题,导致异步信号量支持的判断失效。后面又通过重写进程、进程管理及信号量管理,彻底解决了这一问题。
三 守护进程
3.1 init.d
历史上,Linux 的启动一直采用init进程来启动服务。例如:
代码语言:javascript复制service apache2 start
sudo /etc/init.d/apache2 start
/etc/init.d 是 /etc/rc.d/init.d 的软链接(soft link)。可以通过 ll 命令查看。
代码语言:javascript复制ls -ld /etc/init.d
lrwxrwxrwx. 1 root root 11 Aug 30 2015 /etc/init.d -> rc.d/init.d
/etc/init.d是用来放服务脚本的,当Linux启动时,会寻找这些目录中的服务脚本,并根据脚本的run level确定不同的启动级别。
例如,某台测试机上的/etc/init.d目录下的内容如下:
3.2 systemd
init缺陷
init方法启动服务有两个缺陷:
1、启动时间长。init进程是串行执行,在一个进程启动完毕后,才能启动下一个进程
2、启动脚本复杂。init进程只负责执行启动脚本,脚本自身需要处理各种异常情况,这使得脚本内容过长且不易控制。
因此,就有了systemd的诞生。
systemd概述
关于systemd可以查看freedesktop的这篇文献:systemd System and Service Manager。
简单描述如下:systemd是Linux系统的一套基本构建块。它提供了一个系统和服务管理器,它作为PID 1运行并启动系统的其余部分。
system的命令位置和版本查看:
代码语言:javascript复制[xxx@wx-crm1 ~]$ systemctl --version
systemd 219
PAM AUDIT SELINUX IMA -APPARMOR SMACK SYSVINIT UTMP LIBCRYPTSETUP GCRYPT GNUTLS ACL XZ LZ4 -SECCOMP BLKID ELFUTILS KMOD IDN
[xxx@wx-crm1 ~]$ which systemctl /usr/bin/systemctl
Systemd 的优点是功能强大,使用方便,缺点是体系庞大,非常复杂。事实上,现在还有很多人反对使用 Systemd,理由就是它过于复杂,与操作系统的其他部分强耦合,违反"keep simple, keep stupid"的Unix 哲学。
相关争议内容链接:
Solidot | systemd作者抨击开源和Linux社区
Solidot | 抗议者创建Systemd分支Uselessd
systemd架构
systemd相关命令
systemctl是systemd的主命令,提供的主要功能和使用方式如下:
代码语言:javascript复制# 重启系统
$ sudo systemctl reboot
# 关闭系统,切断电源
$ sudo systemctl poweroff
# CPU停止工作
$ sudo systemctl halt
# 暂停系统
$ sudo systemctl suspend
# 让系统进入冬眠状态
$ sudo systemctl hibernate
除此之外,还有systemd-analyze,用语查看启动耗时;
代码语言:javascript复制# 查看启动耗时
$ systemd-analyze
# 查看每个服务的启动耗时
$ systemd-analyze blame
# 显示瀑布状的启动过程流
$ systemd-analyze critical-chain
# 显示指定服务的启动流
$ systemd-analyze critical-chain atd.service
- hostnamectl命令用于查看当前主机的信息
- localectl命令用于控制系统区域设置和键盘布局设置
- timedatectl命令用于查看当前时区设置。
- loginctl命令用于查看当前登录的用户,等等。
相关概念
1、单元
系统初始化需要做的事情较多,例如启动sshd,需要做很多配置工作。这个过程中的每一步将会被抽象为一个配置单元 unit。通常可以认为一个服务是一个配置单元,一个挂载点是一个配置单元,一个交换分区的配置是一个配置单元等。
通过这样的抽象,可以简化文件开发,例如一个mysql服务对应一个mysql.server文件,这种配置会非常简单,我们也不再需要编写和维护复杂的系统5脚本了。
2、依赖关系
systemd已经将大量的启动工作解除了依赖,使得他们可以并发启动,但还有有些任务之间存在依赖关系。systemd用配置单元定义文件中的关键字来描述配置单元之间的依赖关系。比如unit A依赖unit B,可以再unit B的定义中用"require A"来表示,这样systemd会保证优先启动A,然后再启动B。
3、事务
这里的事务,与数据库中事务的概念有所不同,是为了保证多个依赖的配置单元之间没有环形引用。比如有A,B,C三个单元,存在环形依赖关系:
如果存在这一的依赖,那么将无法启动任意一个服务。systemd会尝试通过依赖关系的强(required)和弱(want)的分别,通过去掉wants关键字指定的依赖来尝试打破循环,无法修复的话将会报错。
4、target和运行级别
systemd用target取代了运行级别的概念。systemd下目标和常见runlevelt的对应关系如下:
systemd的并发启动原理
主要有三种方式:解决 socket 依赖;解决D-Bus依赖,即desktop-bus依赖,是一种进程间通信机制;解决文件系统依赖。详细解释请查阅参考文献:浅析 Linux 初始化 init 系统,第 3 部分 systemd
代码语言:javascript复制后记:感谢程序员架构进阶的提供素材内容,点击阅读原文可达
- END -