在 Linux 6 / CentOS 6 中,使用 service 来进行服务的起停,但是在 Linux 7 / CentOS 7 中,替换为使用 systemctl 命令来控制。将一些常用应用注册成服务后,可以使用 systemctl 命令来方便的操作启动、停止、重启,但是作者最近发现如果配置不当,严重情况下可能影响正常业务运行,请大家务必关注。

$ service start loop.service
The service command supports only basic LSB actions (start, stop, restart, try-restart, reload, force-reload, status). For other actions, please try to use systemctl.

问题描述

如果我们有一些负责自动化的应用,例如 puppet / mco、ansible,这里简称为宿主服务。宿主服务注册为系统服务并且随系统开机自启动。宿主服务支持接收服务端指令并拉起一些常驻进程,拉起的进程我们简称为子进程。当宿主服务被 kill 或意外终止时,会引起子进程一起被 kill。

为了复现这个问题,我写了两个脚本。parent_pro.sh 作为宿主脚本,注册为系统 loop.service 并且随系统启动。该脚本的作用是每隔10秒钟检查 config.txt 的配置,如果配置文件中的数字变成 1 则拉起一个无限循环的子进程。具体的代码可以在我的 Github 上看到。

我的 service 定义信息:

[Unit]
Description=loop service
After=network.target [Service]
Type=forking
StandardOutput=/root/namespace/parent_pro.log
StandardError=/root/namespace/parent_pro.log
WorkingDirectory=/root/namespace
User=root
Group=root
ExecStart=/usr/bin/sh parent_pro.sh [Install]
WantedBy=multi-user.target

下面执行启动服务。

$ systemctl start loop.service
$ ps -ef | grep sh
root 4615 1 0 15:59 ? 00:00:00 /usr/bin/sh parent_pro.sh
root 5153 4615 0 16:00 ? 00:00:00 sh /root/namespace/child_loop.sh
root 5197 4615 0 16:00 ? 00:00:00 sh /root/namespace/child_loop.sh
root 5248 4615 0 16:00 ? 00:00:00 sh /root/namespace/child_loop.sh

从上面的这段输出可以看到,parent_pro.sh 已经启动了三个子进程,我们再看下 parent_pro.sh 的详细信息。

$ systemctl status
● qking-101
State: running
Jobs: 0 queued
Failed: 0 units
Since: Thu 2022-08-11 15:59:54 CST; 29s ago
CGroup: /
├─1 /usr/lib/systemd/systemd --switched-root --system --deserialize 22
├─user.slice
│ └─user-0.slice
│ └─session-1.scope
│ ├─5125 sshd: root@pts/0
│ ├─5129 -bash
│ ├─5224 systemctl status
│ └─5225 less
└─system.slice
├─rsyslog.service
│ └─4618 /usr/sbin/rsyslogd -n
├─postfix.service
│ ├─4944 /usr/libexec/postfix/master -w
│ ├─4948 pickup -l -t unix -u
│ └─4949 qmgr -l -t unix -u
├─loop.service
│ ├─4615 /usr/bin/sh parent_pro.sh
│ ├─5153 sh /root/namespace/child_loop.sh
│ ├─5197 sh /root/namespace/child_loop.sh
│ ├─5198 sleep 10
│ ├─5220 sleep 1
│ └─5222 sleep 1

可以看到我注册的 loop.service 在 system.slice 这个 cgroup 目录下。接下来复现问题场景。

$ kill 4615
$ ps -ef | grep sh
root 1857 2 0 15:59 ? 00:00:00 [kdmflush]
root 1873 2 0 15:59 ? 00:00:00 [kdmflush]
root 3220 2 0 15:59 ? 00:00:00 [kdmflush]
root 4613 1 0 15:59 ? 00:00:00 /usr/sbin/sshd -D
root 5125 4613 0 16:00 ? 00:00:00 sshd: root@pts/0
root 5129 5125 0 16:00 pts/0 00:00:00 -bash
root 5330 5129 0 16:00 pts/0 00:00:00 grep --color=auto sh

杀掉父进程之后,子进程跟着消失了。但是如果手工执行 parent_pro.sh 不会出现这个问题。

$ nohup sh parent_pro.sh > parent_pro.log  2>&1 &
$ systemctl status
● qking-101
State: running
Jobs: 0 queued
Failed: 0 units
Since: Thu 2022-08-11 15:59:54 CST; 2h 21min ago
CGroup: /
├─1 /usr/lib/systemd/systemd --switched-root --system --deserialize 22
├─user.slice
│ └─user-0.slice
│ ├─session-4.scope
│ │ ├─5797 sshd: root@pts/1
│ │ ├─5801 -bash
│ │ ├─6012 sh parent_pro.sh
│ │ ├─6013 sleep 10
│ │ ├─6014 systemctl status
│ │ └─6015 less
│ └─session-1.scope
│ ├─5125 sshd: root@pts/0
│ └─5129 -bash

修改配置文件让父进程拉起一些子进程。

$ echo 0 > config.txt
$ ps -ef | grep sh
root 6012 5801 0 18:21 pts/1 00:00:00 sh parent_pro.sh
root 6018 6012 0 18:21 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6041 6012 0 18:21 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6084 6012 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6147 6012 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6232 6012 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh
$ systemctl status
● qking-101
State: running
Jobs: 0 queued
Failed: 0 units
Since: Thu 2022-08-11 15:59:54 CST; 2h 23min ago
CGroup: /
├─1 /usr/lib/systemd/systemd --switched-root --system --deserialize 22
├─user.slice
│ └─user-0.slice
│ ├─session-4.scope
│ │ ├─5797 sshd: root@pts/1
│ │ ├─5801 -bash
│ │ ├─6012 sh parent_pro.sh
│ │ ├─6018 sh /root/namespace/child_loop.sh
│ │ ├─6041 sh /root/namespace/child_loop.sh
│ │ ├─6084 sh /root/namespace/child_loop.sh
│ │ ├─6147 sh /root/namespace/child_loop.sh
│ │ ├─6232 sh /root/namespace/child_loop.sh
│ │ ├─6337 sh /root/namespace/child_loop.sh
│ │ ├─6460 sh /root/namespace/child_loop.sh
│ │ ├─6603 sh /root/namespace/child_loop.sh
│ │ ├─6766 sh /root/namespace/child_loop.sh
│ │ ├─6949 sh /root/namespace/child_loop.sh
│ │ ├─6950 sleep 10
│ │ ├─7069 sleep 1
│ │ ├─7071 sleep 1
│ │ ├─7073 sleep 1
│ │ ├─7075 sleep 1
│ │ ├─7077 sleep 1
│ │ ├─7079 sleep 1
│ │ ├─7081 sleep 1
│ │ ├─7084 sleep 1
│ │ ├─7085 sleep 1
│ │ ├─7087 sleep 1
│ │ ├─7088 systemctl status
│ │ └─7089 less
│ └─session-1.scope
│ ├─5125 sshd: root@pts/0
│ └─5129 -bash
└─system.slice
├─rsyslog.service

这时杀掉父进程不会对子进程产生影响。

$ kill 6012
$ ps -ef | grep sh
root 6018 1 0 18:21 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6041 1 0 18:21 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6084 1 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6147 1 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6232 1 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6337 1 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6460 1 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6603 1 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6766 1 0 18:23 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6949 1 0 18:23 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 7154 1 0 18:23 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 7377 1 0 18:23 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 7620 1 0 18:23 pts/1 00:00:00 sh /root/namespace/child_loop.sh
[1]+ Terminated nohup sh parent_pro.sh > parent_pro.log 2>&1
$ systemctl status
● qking-101
State: running
Jobs: 0 queued
Failed: 0 units
Since: Thu 2022-08-11 15:59:54 CST; 2h 24min ago
CGroup: /
├─1 /usr/lib/systemd/systemd --switched-root --system --deserialize 22
├─user.slice
│ └─user-0.slice
│ ├─session-4.scope
│ │ ├─5797 sshd: root@pts/1
│ │ ├─5801 -bash
│ │ ├─6018 sh /root/namespace/child_loop.sh
│ │ ├─6041 sh /root/namespace/child_loop.sh
│ │ ├─6084 sh /root/namespace/child_loop.sh
│ │ ├─6147 sh /root/namespace/child_loop.sh
│ │ ├─6232 sh /root/namespace/child_loop.sh
│ │ ├─6337 sh /root/namespace/child_loop.sh
│ │ ├─6460 sh /root/namespace/child_loop.sh
│ │ ├─6603 sh /root/namespace/child_loop.sh
│ │ ├─6766 sh /root/namespace/child_loop.sh
│ │ ├─6949 sh /root/namespace/child_loop.sh
│ │ ├─7154 sh /root/namespace/child_loop.sh
│ │ ├─7377 sh /root/namespace/child_loop.sh
│ │ ├─7620 sh /root/namespace/child_loop.sh
│ │ ├─9114 sleep 1
│ │ ├─9116 sleep 1
│ │ ├─9119 sleep 1
│ │ ├─9120 sleep 1
│ │ ├─9122 sleep 1
│ │ ├─9126 sleep 1
│ │ ├─9127 sleep 1
│ │ ├─9128 sleep 1
│ │ ├─9130 sleep 1
│ │ ├─9132 sleep 1
│ │ ├─9134 sleep 1
│ │ ├─9136 sleep 1
│ │ ├─9138 sleep 1
│ │ ├─9139 systemctl status
│ │ └─9140 less
│ └─session-1.scope
│ ├─5125 sshd: root@pts/0
│ └─5129 -bash
└─system.slice

问题分析

仔细查阅了 systemd 相关文档,发现这个问题主要与 type 参数有关。

Type:定义 service 的类型,主要有以下类型:

  • simple:默认类型,启动的程序就是主体程序,这个程序要是退出那么一切皆休。因为 simple 类型不存在主进程退出的情况也就不存在有返回状态的情况,所以它一旦启动就认为是成功的,除非没起来。
  • forking:标准 Unix Daemon 使用的启动方式。启动程序后会调用 fork() 函数,把必要的通信频道都设置好之后父进程退出,留下守护精灵的子进程。如果启动的进程能够自己创建pidfile,最好也指定下PIDFile=XXX。
  • oneshot:一次性服务,这种服务类型就是启动,完成,没进程了。因为这类服务运行完就没进程了,我们经常会需要 RemainAfterExit=yes。后面配置的意思是说,即使没进程了,我们也要 Systemd 认为该服务是存在并成功了的。其他类型千万不要去设置RemainAfterExit=yes,否则systemd会认为服务启动成功了,重启或再去启动都会失败。
  • dbus:这个程序启动时需要获取一块 DBus 空间,所以需要和 BusName= 一起用。只有它成功获得了 DBus 空间,依赖它的程序才会被启动。
  • notify:这个程序在启动完成后会通过 sd_notify 发送一个通知消息。所以还需要配合 NotifyAccess 来让 Systemd 接收消息,后者有三个级别:none,所有消息都忽略掉; main,只接受我们程序的主进程发过去的消息; all,我们程序的所有进程发过去的消息都算。NotifyAccess 要是不写的话默认是 main。
  • idle:这个程序要等它里面调度的全部其它东西都跑完才会跑它自己。比如你 ExecStart 的是个 shell 脚本,里面可能跑了一些别的东西,如果不这样的话,那很可能别的东西的控制台输出里会多一个“启动成功”这样的 Systemd 消息。

最后,如果大家的服务会处理我这种场景,请务必记住这个参数要设置好了,另外还有一个 KillMode 参数也与 cgroup 有关,当我们执行 systemctl stop loop.service 的时候,整个 cgroup 会被删除,通过设置 KillMode 参数,可以确保子进程不会被清除。

参考资料

  1. unary operator expected解决方法
  2. Linux 系统注册系统服务流程
  3. Systemd使用小结
  4. 使用systemctl管理服务

注意了!这样用 systemd 可能会有风险的更多相关文章

  1. COSO企业风险管理框架2017版发布!看看有哪些变化?

    近期,COSO发布了新版(2017版)的企业风险管理框架:<企业风险管理—与战略和业绩的整合>.相较于2004年发布的上一版框架<企业风险管理—整合框架>,新框架强调了制定战略 ...

  2. Linux集群环境下NTP服务器时间同步

    NTP介绍 NTP(Network Time Protocol,网络时间协议)是用来使网络中的各个计算机时间同步的一种协议.它的用途是把计算机的时钟同步到世界协调时UTC(Universal Time ...

  3. 谈一谈Http Request 与 Http Response

    写在前面的话:今天来总结一下http相关的request和response,就从以下几个问题入手吧. ======正文开始======== 1.什么是HTTP Request 与HTTP Respon ...

  4. 在MongoDB的MapReduce上踩过的坑

    太久没动这里,目前人生处于一个新的开始.这次博客的内容很久前就想更新上来,但是一直没找到合适的时间点(哈哈,其实就是懒),主要内容集中在使用Mongodb时的一些隐蔽的MapReduce问题: 1.R ...

  5. CentOS7 NTP 安装配置

    NTP 网络时间协议用来同步网络上不同主机的系统时间.你管理的所有主机都可以和一个指定的被称为 NTP 服务器的时间服务器同步它们的时间.而另一方面,一个 NTP 服务器会将它的时间和任意公共 NTP ...

  6. 解读2015年互联网UGC内容发展态势,安全事件频发

    <2015内容安全年报> 阿里移动安全 第一章 2015年内容安全形势 随着互联网业务的迅速发展,互联网上的信息内容带来了爆炸式的增长.由于缺乏对网络活动进行有效监督和管理的措施,致使互联 ...

  7. 内存中 OLTP - 常见的工作负荷模式和迁移注意事项(二)

    ----------------------------我是分割线------------------------------- 本文翻译自微软白皮书<In-Memory OLTP – Comm ...

  8. Nginx反向代理+负载均衡简单实现(http方式)

    1)nginx的反向代理:proxy_pass2)nginx的负载均衡:upstream 下面是nginx的反向代理和负载均衡的实例: 负载机:A机器:103.110.186.8/192.168.1. ...

  9. iOS身份证的正则验证

    在ios项目的开发中可能很多地方都需要用到身份证校验,一般在开发的时候很多人都是直接百度去网上荡相关的正则表达式和校验代码,但是网上疯狂粘贴复制的校验代码本身也可能并不准确,可能会有风险,比如2013 ...

随机推荐

  1. ERROR: manifest for elasticsearch:latest not found: manifest unknown: manife

    当我们用docker下载 elasticsearch 的时候出现如下错误: 这里错误的原因是没有发现最新版,需要我们指定版本. docker pull elasticsearch:7.12.0 那我们 ...

  2. 多校联训 DS 专题

    CF1039D You Are Given a Tree 容易发现,当 \(k\) 不断增大时,答案不断减小,且 \(k\) 的答案不超过 \(\lfloor\frac {n}{k}\rfloor\) ...

  3. 如果一个promise永不resolve,会内存泄漏吗

    答:跟内存泄漏没有直接关系gc的策略不会改变,如果该promise没有被人引用,就会被gc掉.如果仍被引用,就不会被gc掉.即使一个promise,resolve或者reject了,但是它还被人引用, ...

  4. SimpleMongoDbFactory类已经失效,被SimpleMongoClientDbFactory替代

    老版本的mongodbtemplate连接池的用法 spring: data: mongodb: address: 127.0.0.1:37017 replica-set: database: xxx ...

  5. Solution -「简单 DP」zxy 讲课记实

    魔法题位面级乱杀. 「JOISC 2020 Day4」治疗计划 因为是不太聪明的 Joker,我就从头开始理思路了.中途也会说一些和 DP 算法本身有关的杂谈,给自己的冗长题解找借口. 首先,治疗方案 ...

  6. Kubernetes组件介绍

    一.api-server   基本概念 该端口默认值为6443,可通过启动参数"--secure-port"的值来修改默认值. 默认IP地址为非本地(Non-Localhost)网 ...

  7. 【我的面试-01】Web前端开发实习岗-面试题总结

    简单开头 首先技术面试官会根据简历里所写的项目和个人掌握技术栈提问(我不知道已经改过多少次简历了,因为前期投简历是真的是沉在茫茫大海,捞漂流瓶都捞不到的那种) 我的技术栈:(Vue还在苦苦的自学当中, ...

  8. this关键字和构造方法

    构造方法: 构造方法的定义:构造方法是类的一个特殊成员,它会在类实例化对象的时候被自动调用 作用:可以在实例化对象的同时对这个对象的属性进行赋值 案例:Student student = new St ...

  9. 4.直方图介绍和使用|MySQL索引学习

    GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源. 目录 一.导读 二.步骤 2.1 SQL语句 2.2 直方图案例 2.3 查看直方图统计信息 2.3 直方图分类 2.4 ...

  10. MYSQL的Java操作器——JDBC

    MYSQL的Java操作器--JDBC 在学习了Mysql之后,我们就要把Mysql和我们之前所学习的Java所结合起来 而JDBC就是这样一种工具:帮助我们使用Java语言来操作Mysql数据库 J ...