Total Pageviews

Sunday, 7 October 2018

用 Systemd管理进程

服务进程的管理

本节将从service单元文件的编写(基础选项、起停及重载选项、依赖性处理选项,资源限制选项等),对服务进程基本操作,进程和进程组的查看和如何更好的杀死服务进程 四个方面介绍systemd下服务进程的管理。

service单元文件

systemd 的单元文件是受 XDG Desktop Entry .desktop 文件启发而产生,而最初起源是 Windows 下的 .ini 文件。 .service 文件是systemd的本地配置文件,类似于 sysvinit 中的 /etc/init.d 里脚本的作用。
一个.service 文件一定包含三个Sections,分别是[Unit]、[Service]、[Install]。每个Section有不同的Key,例如Unit常见有Description、Documentation、Requires等,Service常见有Type等,Install常见有Alias、WantedBy=、RequiredBy等,具体查看 man systemd.unit,以及 man systemd.service
systemd单元文件的加载路径(优先级从上到下依次降低):
系统模式
/etc/systemd/system/*
/run/systemd/system/*
/usr/lib/systemd/system/*
...
注:debian 下,关注:/lib/systemd/systemd/*目录。一般来说,自己定义的.service 放在/etc/systemd/system下。
用户模式
$XDG_CONFIG_HOME/systemd/user/*
$HOME/.config/systemd/user/*
/etc/systemd/user/*
/run/systemd/user/*
/usr/lib/systemd/user/*
...
先看个例子:
nginx.service
[Unit]
Description=A high performance web server and a reverse proxy server
After=network.target

[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t -q -g 'daemon on; master_process on;'
ExecStart=/usr/sbin/nginx -g 'daemon on; master_process on;'
ExecReload=/usr/sbin/nginx -g 'daemon on; master_process on;' -s reload
ExecStop=/usr/sbin/nginx -s quit

[Install]
WantedBy=multi-user.target
通过[Unit] section可以知道这是HTTP服务器与反向代理服务器--nginx的单元文件(Description,关于服务的说明),此单元需在某些target(network.target)之后才可启动(After),关于target的介绍请查看Target一节。
通过[Server] section,可以知道服务的启动方式为forking,PIDFile文件的路径为/run/nginx.pid(需与nginx.conf中指定的PID文件路径相同,让systemd追踪服务的主进程,若未指定或不一致将无法启动服务),ExecStartPre用于指定在服务启动之前需要执行的命令,ExecStart即服务启动调用的命令。ExecReload和ExecStop分别指定服务进程重载和停止的命令。更多配置选项介绍见下文。
[Install] section 中的WantedBy表示此服务被multi-user.target wants,在对应.wants/子目录中为服务建立相应的链接,当systemd启用此target,本服务也会启动。
基本介绍
service单元文件中的[Unit]和[Install]同其他单元文件类似,其特有的seciton是[Service]。单元文件中空行和以#和;开始的行会被忽略,所以可以用来写注释。以反斜杠(\)连接在一起的行,处理时会将反斜杠替换成空格,所以可以用这个来把太长的行分成多行。如果文件是空的,或链接到/dev/null,它将不被加载,这样可以用来禁用服务,让它即使手工也无法启动。
[Unit]
  • Description=   :一些描述,显示给用户界面看的,可以是任何字符串,一般是关于服务的说明。
  • Documentation=  :指定参考文档的列表,以空格分开的 URI 形式,如http://, https://, file:, info:, man:,这是有顺序的,最好是先解释这个服务的目的是什么,然后是它是如何配置的,再然后是其它文件,这个选项可以多次指定,会将多行的合并,如果指定了一个空的,那么会重置此项,前面的配置不再起作用。
  • Requires=   :指定此服务依赖的其它服务,如果本服务被激活,那么 Requires 后面的服务也会被激活,反之,如果 Requires 后面的服务被停止或无法启动,则本服务也会停止。这个选项可以指定多次,那么就要求所有指定的服务都被激活。需要注意的是这个选项不影响启动或停止的顺序,启动顺序使用单句的 After= 和 Before= 来配置。例如,如果 foo.service 依赖 bar.serivce,但是只配置了 Requires= 而没有 After= 或 Before=,那么 foo.service 启动时会同时激活 foo.service 和 bar.service。通常使用 Wants= 代替 Requires= 是更好的选择,因为系统会更好的处理服务失败的情况。注意,这种依赖关系,也可以在文件之外来处理,即使用 .requires/ 目录,可以参看上面的说明。
  • RequiresOverridable= :类似上面的 Requires= ,不过这种情况下,只要用户明确要求它启动,才会影响到被依赖的服务,不然服务出错什么的,不会影响被依赖服务的启动。
  • Requisite=, RequisiteOverridable=  :分别类似上面的两个,不过如果是这个指定服务没有启动,被依赖的服务会不启动,立即失败。
  • Wants= :相对弱化的 Requires= ,这里列出的服务会被启动,但如果无法启动或无法添加到事务处理,并不影响本服务做为一个整体的启动。这是推荐的两个服务关联的方式。这种依赖也可以配置文件外,通过 .wants/ 目录添加,具体可以看上面的说明。
  • BindsTo= :和 Requires= 很像,但是这种情况,如果他后面列出的服务停止运行或崩溃之类的,本服务也会同时停止。
  • PartOf=  :又一个类似 Requires= 的选项,但是限制在停止或重启动服务,如果这里列出的服务被停止或重启动,那么本服务也会停止或重启动,注意这个依赖是意向,即本服务停止或重启动,不会影响到这里列出服务的运行状态。
  • Conflicts= :配置一个依赖冲突,如果配置了些项,那么,当一个服务启动时,或停止此处列出的服务,反过来,如果这里列出的服务启动,那么本服务就会停止,即后启动的才起作用。注意,此设置和 After= 和 Before= 是互相独立的。如果服务 A 和 B 冲突,且在 B 启动的时候同时启动,那么有可能会启动失败(两都都是必需的)或修改以修复它(两者之一或两都都不是必需的),后一种情况,会将不需要的依赖删除,或停止冲突。
  • Before=, After=  :配置服务间的启动顺序,比如一个 foo.service 包含了一行 Before=bar.service,那么当他们同时启动时,bar.service 会等待 foo.service 启动完成后才启动。注意这个设置和 Requires= 的相互独立的,同时包含 After= 和 Requires= 也是常见的。此选项可以指定一次以上,这时是按顺序全部启动。
  • OnFailure=   :列出一个或更多的服务,当本服务启动状态是 failed 的时候,激活这些服务。
  • PropagatesReloadTo=, ReloadPropagatedFrom=  :这两个是列出一些服务,当其它服务 reload 时同时 reload 这个服务,或者反之。
  • RequiresMountsFor=  :用空格分开的绝对路径列表,是 Requires= 和 After= 添加的依赖中的 mount 文件需要访问的指定的路径。
  • OnFailureIsolate=  :是一个布尔值,如果是真,那么 OnFailure= 后面的服务会进入隔离模式,即所有不是它依赖的服务都会停止。如果只设置一个服务,可以放在 OnFailure= 后,默认值是假。
  • IgnoreOnIsolate=  :一个布尔值.如果是真则当隔离其它服务时本服务不会停止(不明白隔离是什么意思,大概在后面)。默认是假。
  • IgnoreOnSnapshot=  :一个布尔值.如果是真则本服务不包含快照(snapshots)。对 device 和 snapshot 服务默认为真,其它服务默认为假。
  • StopWhenUnneeded=  :一个布尔值。如果是真则当本服务不使用时会停止。 注意,为了尽量减少 systemd 的工作,默认情况下是不会停止不使用的服务的,除非和其它服务冲突,或用户明确要求停止。如果设置了这个选项,那么如果没有其它活动的服务需要此服务,它会自动停止。默认值是假。
  • RefuseManualStart=, RefuseManualStop=  :布尔值。如果设为真值,则此服务只能间接的激活或停止。这种情况下,用户直接启动或停止此服务会被拒绝,只有做为其它的服务依赖关系,由其它服务进行启动或停止才可以。这主要是为了停止用户误操作。默认值是假。
  • AllowIsolate=      :布尔值。如果是真值,则此服务可以使用 systemctl isolate 命令进行操作。否则会拒绝此操作。最好的办法是不要动这处选项,除非目标服务的行为类似于 SysV 启动系统中的 runlevels。只是一种预防措施,避免系统无法使用的状态。默认值是假。
  • DefaultDependencies=  :布尔值。如果是真(默认值),一些本服务默认的依赖会隐式的建立,具体是哪些依赖,则于服务的类型决定。比如,对于普通的服务(.service类型),它会确保在系统基本服务启动后才启动本服务,会在系统关机前确保本服务已关闭。一般来说,只有早期开机服务和后期的关机服务,才需要把这个设成假。强烈对大多数普通服务,让这个选项启用即可。如果设成假,也不会禁用所有的隐式依赖,只是禁用那些非必要的。
  • JobTimeoutSec=  :当一个客户端等待本服务的某个 Job 完成时,所指定的超时时间。如果达到了限制的时间,此 Job 会取消运行,但服务不会更改状态,包括进入“failed”状态。除了设备服务(即.device类型),其它的默认值是0(即没有超时设置)。注意,这个是独立于特定服务所设置的超时设置的(比如对 .service 类型所设置的 Timeout=),它对服务本身没有影响,但特定服务的设置是有影响的(能用来更改服务状态)。《-这段不明白到底是什么意思,所以翻译的也是乱七八糟,真对不起)。
  • ConditionPathExists=, ConditionPathExistsGlob=, ConditionPathIsDirectory=, ConditionPathIsSymbolicLink=, ConditionPathIsMountPoint=, ConditionPathIsReadWrite=, ConditionDirectoryNotEmpty=,ConditionFileNotEmpty=, ConditionFileIsExecutable=, ConditionKernelCommandLine=, ConditionVirtualization=, ConditionSecurity=, ConditionCapability=, ConditionHost=, ConditionACPower=,ConditionNull=  :这是一组类似的东西。检测特定的条件是不是真值,如果不是真值,服务会略过启动,但是它依赖的服务还是会正常运行的。这个条件测试失败不会让服务进入失败状态。条件是在服务开始运行时检查的。
  • ConditionPathExists= 是指定在服务启动时检查指定文件的存在状态。如果指定的绝对路径名不存在,这个条件的结果就是失败。如果绝对路径的带有!前缀,则条件反转,即只有路径不存在时服务才启动。
  • ConditionPathExistsGlob= 类似上面的选项,但支持通配符。
  • ConditionPathIsDirectory= 判断指定路径是不是目录。
  • ConditionPathIsSymbolicLink= 判断指定路径是不是链接。
  • ConditionPathIsMountPoint= 判断指定路径是不是一个挂载点。
  • ConditionPathIsReadWrite= 多年指定路径是否可读写(即不是做为只读系统挂载的)
  • ConditionDirectoryNotEmpty= 判断指定目录是否存在且不为空。
  • ConditionFileNotEmpty= 判断指定文件是否是常规文件且不为空(即大小不是0)。
  • ConditionFileIsExecutable= 判断指定文件是否是常规文件且可执行。
  • 类似的,ConditionKernelCommandLine=是判断有没有指定的内核命令行启动参数(或带有!反之),这个参数必须是一个单词或用=分开的两个单词,前一种情况下,会寻找内核参数是否有此单词或是赋值的左边。后一种情况则必须是赋值的左右同时符合。
  • ConditionVirtualization= 是判断是不是在虚拟化环境下执行的服务。这可以是个布尔值以判断是不是任意的虚拟化环境,或者下列的字符串之一: qemu, kvm, vmware, microsoft, oracle, xen, bochs, chroot, openvz, lxc, lxc-libvirt, systemd-nspawn,以判断是不是特定的虚拟化环境,多重嵌套的虚拟化环境,只判断最后一层。可以使用!进行反转判断。
  • ConditionSecurity= 是判断系统是否启用了安全环境,当前仅能识别selinux, apparmor, 和 smack。可以使用!进行反转判断。
  • ConditionCapability= 是判断服务管理器绑定的 capability 是否存在。(可以查看其它部分的详细信息。)设置为 capability 的名字,比如 CAP_MKNOD。可以通过在前面加!反转判断。
  • ConditionHost= 是判断主机名 (hostname)或机器ID(machine ID)是否匹配。可以加!反转。
  • ConditionACPower= 是判断机器是否在使用交流电源。如果设成 true,而只有至少连接一个交流电源时结果才为真,反过来,设成 false,则不连接所有交流电源时才为真。
  • ConditionNull= 是一个常量性质的判断条件,它应该是布尔值,如果设成 false ,则条件永远失败,反过来则永远成立。
  • 如果指定多个条件,则所有条件都需要成立(即条件之间是 AND 的关系)。条件前面可以加上 | 符号,这时条件变成一个触发条件,服务定义了触发条件,那么在满足其它非触发条件和这个触发条件的情况下,服务会至少执行一次。同时指定|和!前缀时,先处理|,后处理!。除了ConditionPathIsSymbolicLink=,其它条件均跟随链接。如果这些条件指定为空,则相当于重置,前面的任何设置都不再起作用。
  • SourcePath=  :这个服务生成的配置文件所在的路径,这主要是用在生成工具从外部配置文件的格式转换到本地服务的配置格式中。因此,对一般的服务不要使用此选项。
[Service]
介绍部分常见选项,其余选项请查看本节参考链接2。
  • Type= :
    • simple(默认值):systemd认为该服务将立即启动。服务进程不会fork。如果该服务要启动其他服务,不要使用此类型启动,除非该服务是socket激活型。
    • forking:systemd认为当该服务进程fork,且父进程退出后服务启动成功。对于常规的守护进程(daemon),除非你确定此启动方式无法满足需求,使用此类型启动即可。使用此启动类型应同时指定 - PIDFile=,以便systemd能够跟踪服务的主进程。
    • oneshot:这一选项适用于只执行一项任务、随后立即退出的服务。可能需要同时设置 RemainAfterExit=yes 使得 systemd 在服务进程退出之后仍然认为服务处于激活状态。
    • notify:与 Type=simple 相同,但约定服务会在就绪后向 systemd 发送一个信号。这一通知的实现由 libsystemd-daemon.so 提供。
    • dbus:若以此方式启动,当指定的 BusName 出现在DBus系统总线上时,systemd认为服务就绪。
  • PIDFile= :指定PID 文件绝对路径,当Type=forking时,必须设置此项。
  • ExecStart= :服务启动命令,可带参数。!!关于此选项有大量注意事项与细节(例如:命令中不可以使用重定向符号,管道,后台运行&以及其他特殊符号等),请查看用户手册。
  • ExecStartPre=, ExecStartPost= :之前ExecStart之前或者之后执行的命令。注意语法格式要求同ExecStart选项。若包含多条此选项,将按顺序串行执行。
  • ExecReload= :重载服务时候触发的命令。
  • ExecStop= :服务停止命令。
  • ExecStopPost= :停止服务之后执行的命令。语法格式要求同ExecStart选项。
  • Restart= :指定服务进程自动重启的条件。
    • no:默认选项,服务不会被systemd自动重启。
    • on-success:当服务进程成功退出后重启(exit code=0,signals SIGHUP, SIGINT, SIGTERM or SIGPIPE, and additionally, 或SuccessExitStatus=选项指定的退出信号)。
    • on-failure:服务进程不正常退出时进行重启(exit code 为非0,或被信号中断)。
    • on-abnormal:服务进程被信号中断时进行重启。
    • no-watchdog:watchdog观测到服务进程过期后重启服务。
    • no-abort:进程被未捕获的信号中断时将进行重启。
    • always:服务在无论何种情况退出后或者超时时总是重启。
关于启动、停止的超时设置请参考用户手册。
服务进程限制
  • PrivateNetwork=[BOOL] :若服务不需要网络连接可开启本选项,更加安全。
  • PrivateTmp=[BOOl] :由于传统/tmp目录是所有本地用户和服务共用,会带来很多安全性问题,开启本选项后,服务将有一个私有的tmp,可防止攻击。
  • InaccessibleDirectories= :限制服务进程访问某些目录。
  • ReadOnlyDirectories= :设置服务进程对某些目录只读,保证目录下数据不被服务意外撰改。
  • OOMScoreAdjust= :调整服务OOM值,从-1000(对该服务进程关闭OOM)到1000(严格)。
  • IOSchedulingClass= :IO调度类型,可设置为0,1,2,3中的某个数值,分配对应none,realtime,betst-effort和idle。
  • IOSchedulingPriority= :IO调度优先级,0~7(高到低)。
  • CPUSchedulingPriority= :CPU调度优先级,99~1(高到低)
  • Nice= :进程调度等级。
-(更多介绍请查看本节参考链接7,8)
关于服务进程的权限限制、资源限制、安全性管理将在进阶部分中进一步介绍。
[Install]
  • Alias= :在安装使用应该使用的额外名字(即别名)。名字必须和服务本身有同样的后缀(即同样的类型)。这个选项可以指定多次,所有的名字都起作用,当执行 systemctl enable 命令时,会建立相当的链接
  • WantedBy=, RequiredBy= :在 .wants/ 或 .requires/ 子目录中为服务建立相应的链接。这样做的效果是当列表中的服务启动,本服务也会启动。 在 bar.service 中的 WantedBy=foo.service  和 Alias=foo.service.wants/bar.service 基本是一个意思。
  • Also=  :当此服务安装时同时需要安装的附加服务。 如果用户请求安装的服务中配置了此项,则 systemctl enable 命令执行时会自动安装本项所指定的服务。
  • 在 [Install] 段使用这些字符串有特定含义: %n, %N, %p, %i, %U, %u, %m, %H, %b。请查看查看本节参考链接1。
注:修改service单元文件之后,需使用命令systemctl daemon-reload重新载入systemd,扫描新的或有变动的单元。
服务进程执行环境配置:
更多关于服务进程执行环境的配置请参考:systemd.exec

服务进程基本操作

systemd的主要命令行工具是systemctl,可用于管理单元文件,不仅仅是service。
注:若只给出名称,而为给出扩展名,将默认为service单元,例:systemctl start foosystemctl start foo.service等价。
在sysv下,常用的管理命令是service,在systemd中此命令可用(参考本文档“SysV兼容性”一节),更建议使用systemctl命令。
使用systemctl可以查看服务进程的状态。
查看服务进程运行状态(同时查看到其下属进程们的PID)
# systemctl status nginx.service
查看系统所有service及其状态,将会打印4列信息,分别为UNIT、LOAD、ACTIVE、SUB、DESCRIPTION
# systemctl list-units --type service
查看特定服务的依赖关系
# systemctl list-dependencies nginx
更多关于systemctl在服务进程查看方面的用法请查看systemctl用户手册,另外也可通过 pstree命令查看系统进程树。

杀死服务进程(组)

Systemd 采用 Linux 的 Cgroup 特性跟踪和管理进程的生命周期。CGroup 提供了类似文件系统的接口,使用方便。当进程创建子进程时,子进程会继承父进程的 CGroup。因此无论服务如何启动新的子进程,所有的这些相关进程都会属于同一个 CGroup,systemd 只需要简单地遍历指定的 CGroup 即可正确地找到所有的相关进程,将它们一一停止即可。 例如:一个CGI程序派生两次,脱离和Apache的父子关系,当apache进程被停止后,该CGI程序还在继续运行的情况,在systemd的管理下将不会存在。
停止服务进程建议使用systemctl工具。
杀死一个服务的所有进程(传递信号到指定服务的所有进程):
$ sudo systemctl kill crond.service
#指定信号类型
$ sudo systemctl kill -s SIGKILL crond.service
或
$ sudo systemctl kill -s 9 crond.service
#无论服务进程经过多少层fork,使用以上命令即可杀死所有进程。

# 发送指定信号到服务的主进程
$ sudo systemctl kill -s HUP --kill-who=main crond.service
另外在单元文件中,可以指定KillMode,KillSignal,SengSIGHUP,SendSIGKILL,请查看本节参考链接4。
编译生成二进制程序fork,编写fork.service,调用fork程序。运行fork.service,使用pstree查看进程树:
kill 掉某个fork出的子进程,使该子进程的子进程们脱离主进程。
查看fork.service 的状态,发现脱离父进程的进程们依然在其crgoup中,未脱离。
kill掉fork.service,发现该服务的所有进程,包括脱离主进程的进程都被停止,无一例外。

本节参考链接

  1. systemd.unit
  2. systemd.service
  3. Killing Services
  4. systemd.kill
  5. systemctl
  6. ArchWiki-systemd
  7. Securing Your Services
  8. systemd.exec

socket-based activation

绝大多数的服务依赖是套接字依赖。比如服务 A 通过一个套接字端口 S1 提供自己的服务,其他的服务如果需要服务 A,则需要连接 S1。因此如果服务 A 尚未启动,S1 就不存在,其他的服务就会得到启动错误。所以传统地,人们需要先启动服务 A,等待它进入就绪状态,再启动其他需要它的服务。Systemd 认为,只要我们预先把 S1 建立好,那么其他所有的服务就可以同时启动而无需等待服务 A 来创建 S1 了。如果服务 A 尚未启动,那么其他进程向 S1 发送的服务请求实际上会被 Linux 操作系统缓存,其他进程会在这个请求的地方等待。一旦服务 A 启动就绪,就可以立即处理缓存的请求,一切都开始正常运行。
Linux 操作系统有一个特性,当进程调用 fork 或者 exec 创建子进程之后,所有在父进程中被打开的文件句柄 (file descriptor) 都被子进程所继承。套接字也是一种文件句柄,进程 A 可以创建一个套接字,此后当进程 A 调用 exec 启动一个新的子进程时,只要确保该套接字的 close_on_exec 标志位被清空,那么新的子进程就可以继承这个套接字。子进程看到的套接字和父进程创建的套接字是同一个系统套接字,就仿佛这个套接字是子进程自己创建的一样,没有任何区别。
这个特性以前被一个叫做 inetd 的系统服务所利用。Inetd 进程会负责监控一些常用套接字端口,比如 Telnet,当该端口有连接请求时,inetd 才启动 telnetd 进程,并把有连接的套接字传递给新的 telnetd 进程进行处理。这样,当系统没有 telnet 客户端连接时,就不需要启动 telnetd 进程。Inetd 可以代理很多的网络服务,这样就可以节约很多的系统负载和内存资源,只有当有真正的连接请求时才启动相应服务,并把套接字传递给相应的服务进程。
和 inetd 类似,systemd 是所有其他进程的父进程,它可以先建立所有需要的套接字,然后在调用 exec 的时候将该套接字传递给新的服务进程,而新进程直接使用该套接字进行服务即可。
systemd中这个并行化Socket服务极大的加快了系统的启动速度(当然,不全是它的功劳,此处不赘述)。
systemd的并行启动能力(与sysv、upstart对比)
Socket Activation 带来的益处(摘自Systemd主作者Blog):
  • parallelization.
  • We no longer need to configure dependencies explicitly. Since the sockets are initialized before all services they are simply available, and no userspace ordering of service start-up needs to take place anymore. Socket activation hence drastically simplifies configuration and development of services.
  • If a service dies its listening socket stays around, not losing a single message. After a restart of the crashed service it can continue right where it left off.
  • If a service is upgraded we can restart the service while keeping around its sockets, thus ensuring the service is continously responsive. Not a single connection is lost during the upgrade.
  • We can even replace a service during runtime in a way that is invisible to the client. For example, all systems running systemd start up with a tiny syslog daemon at boot which passes all log messages written to /dev/log on to the kernel message buffer. That way we provide reliable userspace logging starting from the first instant of boot-up. Then, when the actual rsyslog daemon is ready to start we terminate the mini daemon and replace it with the real daemon. And all that while keeping around the original logging socket and sharing it between the two daemons and not losing a single message. Since rsyslog flushes the kernel log buffer to disk after start-up all log messages from the kernel, from early-boot and from runtime end up on disk.
上文提到inetd可以代理很多网络服务以此节约系统资源,Systemd 也可以提供按需启动的能力,只有在某个服务被真正请求的时候才启动它。空闲时使该服务结束,等待下次需要时再次启动它。例:
服务器上的SSHD服务,只有当系统管理员或者用户登录时候,此服务才被使用到,然后它却一直驻留在后台,极大的浪费系统资源。而在systemd下,我们可以做得按需启动,即有人通过SSH访问服务时,SSHD服务才会被启动。(具体实现请查看/lib/systemd/system下的ssh.servicessh.socket文件)
注:systemd的socket单元负责监控端口时,当外部连接带来,而systmed无法将socket转移给对应服务单元时候(若服务进程崩溃、停止退出、无法重启等),对应socket单元将进入failed状态,外部连接将不通(telnet ip pot 测试对应服务端口将不通)。
按需启动实例:
使用systemd部署Node.js应用(myservice.service),见图myservice.service:
$ sudo systemctl enable myservice.service # 设置服务随机启动
$ sudo systemctl start myservice.service #启动服务
在这种配置下,没有使用socket激活,systemd运行且监控着服务的守护进程,Node.js监听TCP端口和服务器请求,进程是时刻运行着,即使没有外部访问,极大的浪费了系统的资源。
需要将其改为socket激活。空闲时,systemd监控着TCP端口,当有请求来到时,systemd激活Node.js服务并将socket移教给Node.js处理,然后Nojde.js接管并处理全部的客户端请求,systemd重新变成监控Node.js的角色。 当 Node.js 处理完任务后,它自动停止。而再由 systemd 来监控 TCP 端口,直至下次客户端请求的到来并再启动 Node.js 的应用,循环往复。
要达到以上目的,Node.js需要安装systemd相关模块,同时安装node-autoquit模块在空闲时关闭应用,以及做好应用数据的保存工作,具体此处不赘述,请参考本节参考链接。
systemd的相关配置:需要修改之前的service单元文件(去除自动重启以及自动启动),并增加一个同名socket单元文件让systemd监听TCP端口,见图:


$ sudo systemctl enable myservice.socket #使socket单元自启动
$ sudo systemctl start myservice.socket #启动socket

本节参考链接


instantiated services

部分服务例如syslog、apache等,在系统上一般是只有单进程在运行。而另外一些服务需要多实例化,例如Dovecot-IMAP服务会有多个实例进程运行在不同的IP端口或者本地IP地址上。 在systemd中,我们不需要为每个实例化进程编写一份配置文件,只需要编写一个模板文件,然后在启用时候调用模板文件即可,模板文件可以通过匹配模板时传入的字符串实例化进程,例如我们系统上有3块网卡,分别为eth0、eth1、eth2,我们希望通过每块网卡都通过dhcpd配置动态ip。利用systemd的模板\实例机制,我们只需要写一份dhcpd的模板文件,即可为3块网卡所用。
dhcpd@.service
[Unit]
Description=dhcpcd on %I
Wants=network.target
Before=network.target
BindsTo=sys-subsystem-net-devices-%i.device
After=sys-subsystem-net-devices-%i.device

[Service]
Type=forking
PIDFile=/run/dhcpcd-%I.pid
ExecStart=/usr/bin/dhcpcd -4qb %I
ExecStop=/usr/bin/dhcpcd -x %I

[Install]
WantedBy=multi-user.target
通过以下命令启用:
# systemctl start dhcpcd@eth0.service
# systemctl start dhcpcd@eth1.service
# systemctl start dhcpcd@eth2.service
命令中的字符串ethN将在service文件中被匹配展开:
dhcp@eth0.service
[Unit]
Description=dhcpcd on eth0
Wants=network.target
Before=network.target
BindsTo=sys-subsystem-net-devices-eth0.device
After=sys-subsystem-net-devices-eth0.device

[Service]
Type=forking
PIDFile=/run/dhcpcd-eth0.pid
ExecStart=/usr/bin/dhcpcd -4qb eth0
ExecStop=/usr/bin/dhcpcd -x eth0

[Install]
WantedBy=multi-user.target
模板文件中的%I%i被展开为命令中@之后.service之前的字符串,即ethN%I%i的区别在此例中看不出来,实际上%I展开后是不转义的字符串,而%i展开后是转移的字符串。例如如果我们调用串口tty的模板文件实例化某个接口的tty服务,systemctl start 'serial-getty@serial-by\x2dpath-pci\x2d0000:00:1d.0\x2dusb\x2d0:1.4:1.1\x2dport0.service',实际上接口地址为 “serial/by-path/pci-0000:00:1d.0-usb-0:1.4:1.1-port0” ,以上命令是经过转义的, /被替换为-,特殊符号被替换为16进制符号等。service文件中的%I将展开为转义前的“serial/by-path/pci-0000:00:1d.0-usb-0:1.4:1.1-port0”, 而%i将展开为serial-getty@serial-by\x2dpath-pci\x2d0000:00:1d.0\x2dusb\x2d0:1.4:1.1\x2dport0.service
  • "%I" Unescaped instance name Same as "%i", but with escaping undone
  • "%i" Instance name For instantiated units: this is the string between the "@" character and the suffix of the unit name
单元文件的特殊符号更多解释请查看 systemd.unit#Specifiers
另外一个例子: systemd-fsck@.service
[Unit]
Description=File System Check on %f
Documentation=man:systemd-fsck@.service(8)
DefaultDependencies=no
BindsTo=%i.device
After=systemd-readahead-collect.service systemd-readahead-replay.service %i.device
Before=shutdown.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/lib/systemd/systemd-fsck %f
StandardOutput=journal+console
TimeoutSec=0
systemd-fsck是systemd的磁盘检查工具,通过此模板文件可以对多块磁盘设备进行实例化后检查(%f表示未转义的文件名)。

本节参考链接


进程资源管理

基础介绍

systemd内部使用cgroups对其下的单元进行资源管理,包括CPU、BlcokIO以及MEM方面。systemd的资源管理主要基于三个单元service、scope以及slice。
  • service单元主要用于配置单项服务进程,具体请参考上文的基础--进程管理--服务进程管理一节。
  • scope 单元由 systemd 在已有进程外自动创建。通过将某个进程和其子进程分组,scope 单元可用来组织进程,应用资源单元,或者杀死进程组。用户会话就是一个进程都包含在单个 scope 单元中的实例。
  • slice 单元用于将管理进程的单元分组成层级,层级可允许控制分配给 slice 的资源。例:默认的 slice 有用于虚拟机及容器 (container) 的 machine.slice;用于系统服务的 system.slice;用于用户会话的 user.slice。在service单元文件中使用 Slice=slicename,可将服务可添加到特定slice。
每一个用户登录时候会自动创建一个此用户的slice单元,同时用户的每个对话会自动创建一个scope用于管理对话下的进程。 每一个模板service单元文件实例化时会自动创建一个对应的slcie,例如getty@.service进行实例化为一个getty@tty1.service时会自动创建一个systemd-getty.slice用于管理此模板下的所有实例化的进程,关于service模板文件的介绍请查看上文的基础--进程管理-instandtiatied serivices一节。
可使用systemd-cgls( Recursively show control group contents)命令查看系统中以上三种单元的层级管理,见图systemd-cgls。
systemd另外提供了一个工具systemd-cgtop( Show top control groups by their resource usage)用于监控各个单元实时、动态的资源占用情况,见图systemd-cgtop。

配置与实例

本小节仅简单介绍关于CPU、MEM、BlockIO常见的资源管理选项,更多配置选项详情请查看本节参考链接4、6等。
CPU
  • CPUAccounting=
    • [true,false],是否打开服务单元的CPU用量统计
    • Note that turning on CPU accounting for one unit might also implicitly turn it on for all units contained in the same slice and for all its parent slices and the units contained therein.
  • CPUShare=,StartupCPUShare=
    • 设置服务单元的CPU时间比重,默认值为1024。设置此两项之后,即表示默认开启"CPUAccounting=true"。
    • StartupCPUShare作用于系统启动阶段,CPUShares作用与系统启动之后运行阶段。
  • CPUQuota=
    • 服务进程执行时的CPU时间配额设置。设置的值是百分数的形式,表示占一个CPU总共时间的百分比。若值大于100%,表示分配的的CPU时间多于一个CPU。
    • Example: CPUShares=20% ensures that the executed processes will never get more than 20% CPU time on one CPU.
    • 设置此项之后,默认设置"CPUAccounting=true"。
  • CPUSchedulingPolicy=
    • 设置服务进程执行时CPU的调度策略,可设置为other, batch, idle, fifo 或 rr,具体请查看sched_setscheduler(2)
  • CPUSchedulingPriorityp=
    • 设置服务进程执行时CPU的调度优先级,设置的数值范围依上文CPUSchedulingPolicy指定的类型不同而不通,例如real-time类型的调度方式,优先级数值范围为1(低优先级)到99(高优先级)。更多信息请参考sched_setscheduler(2)
/etc/systemd/system/httpd.service
.include /usr/lib/systemd/system/httpd.service

[Service]
CPUShares=1500
然后重载systemd deamon,重启http.service即可。
#systemctl daemon-reload
#systemctl restart httpd.service
MEM
  • MemoryAccounting=
  • [true,false],是否打开服务单元的内存用量统计。
  • MemoryLimit=bytes
  • 指定执行进程的内存使用量,默认单位为bytes。数值后可加单元:K,M,G或T,表示KB,MB,GB,TB。设置此项即意味着设置了 "MemoryAccounting=true"。
/etc/systemd/system/httpd.service
.include /usr/lib/systemd/system/httpd.service

[Service]
MemoryLimit=1G 
在debian testing 上无法完成内存限制的实验,原因不明。正常配置,service单元与slice单元可正常启动,但是slice单元中限制的内存无效,日志正常。更换到fedora 20上实验成功。 注意若在service单元中指定了slice单元,而限制选项主要在slice单元中,最好slice单元比service单元先启动,否则可能报错。 若服务程序内存超过MemoryLimit的限制,服务单元将退出进入failed状态。
BlockIO
  • BlockIOAccounting=
    • [true,false],是否开启服务单元的块设备的IO统计。
  • BlockIOWeight=weight, StartupBlockIOWeight=weight
  • 设置服务单元执行时候的块设备IO比重,默认数值为1000,可设置范围为10到1000。BlcokcIOWeight与StartupBlockIOWeight,前者为系统运行时做的限制,后者为系统启动阶段做的限制。
  • 例1:BlockIOWeight=500
  • 例2,指定设备:BlockIOWeight=/dev/disk/by-id/ata-SAMSUNG_MMCRE28G8MXP-0VBL1_DC06K01009SE009B5252 750
  • 例3,指定实际的块设备点:BlockIOWeight=/home/lennart 750
  • BlockIOReadBandwidth=device bytes, BlockIOWriteBandwidth=device bytes
  • 设置服务单元执行时对执行块设备的读写速率设置。
  • 例:BlockIOReadBandwith=/var/log 5M,表示服务单元对/var/log的读限制在5Mb/s内。
Others
上文提到的这些设置选项其实都是与cgroup对应的控制组相关,例如MemoryLimit选项对应memory.limit_in_bytes控制组。systemd的单元文件中可以通过ControlGroupAttribute配置对应cgroup的控制组,例如:controlGroupAttribute=memory.swappiness 70。更多介绍请查看本节参考链接9,10。
ControlGroupAttribute= Set a specific control group attribute for executed processes, and(if needed) add the executed processes to a cgroup in the hierarchy of the controller the attribute belongs to.
实例
我们将上文关于CPU,IO,BlockIO等相关的设置整合在一块,可得到如下的service文件:
/etc/systemd/system/httpd.service
.include /usr/lib/systemd/system/httpd.service

[Service]
CPUShares=1500
MemoryLimit=1G
BlockIOWeight=500
BlockIOReadBandwith=/var/log 5M
ControlGroupAttribute=memory.swappiness 70
我们知道service单元文件中service 段可以通过Slice=选项指定所属的slice单元,所以除了将资源管理配置写在service单元文件中,还可以将服务进程的资源限制写在特定slice单元中。
例:
/etc/systemd/system/limits.slice:
[Unit]
Description=Limited resources Slice
DefaultDependencies=no
Before=slices.target

[Slice]
CPUShares=512
MemoryLimit=1G
/lib/systemd/system/nginx.service:
[Unit]
Description=A high performance web server and a reverse proxy server
After=network.target

[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t -q -g 'daemon on; master_process on;'
ExecStart=/usr/sbin/nginx -g 'daemon on; master_process on;'
ExecReload=/usr/sbin/nginx -g 'daemon on; master_process on;' -s reload
ExecStop=/usr/sbin/nginx -s quit
Slice=limits.slice

[Install]
WantedBy=multi-user.target
然后执行以下命令:
# systemctl daemon-reload
# systemctl restart nginx.service
使用systemd-cgls查看,结果如图 nginx-slice。
另外也可通过systemctl永久性或者临时性的对正在运行的service、slice、scope单元进行即时的资源控制。
例如:
#systemctl set-property nginx.service CPUShares=500 MemoryLimit=500M
会发现/etc/systemd/system出现了nginx.service.d目录,其中包含着与nginx.service相关的资源控制配置文件:90-CPUShare.conf,90-MemoryLimit.conf。 这种更改是永久性的,即下次重启依然有效。如果需要临时性的更改使用选项--runtime替代set-property即可。

本节参考链接

  1. systemd.service
  2. systemd.scope
  3. systemd.slice
  4. systemd.resource-control
  5. systemd.cgroup
  6. systemd.exec
  7. Resource Management with systemd
  8. The New Control Group Interfaces
  9. systemd for Administrators, Part XVIII
  10. Man systemd.exec

进程权限与安全管理

关于本节内容建议阅读本节参考链接2:《Securing Your Services》。

chroot()与目录访问控制

service单元中可以使用 RootDirectory= 选项来达到chroot的效果。
RootDirectory=
Takes an absolute directory path. Sets the root directory for executed processes, with the chroot(2) system call. If this is used, it must be ensured that the process and all its auxiliary files are available in the chroot() jail.
例:
[Unit]
Description=A chroot()ed Service

[Service]
RootDirectory=/srv/chroot/foobar
ExecStartPre=/usr/local/bin/setup-foobar-chroot.sh
ExecStart=/usr/bin/foobard
RootDirectoryStartOnly=yes
如上文服务单元,对于foobard来说,根目录为/srv/chroot/foobar,注意若指定了RootDirectory,则ExecStart(ExecStop等等)选项的路径需以新指定的RootDirectory为准, 例如本例中ExecStart实际指向为/srv/chroot/foobar/usr/bin/foobard,但在service单元中需写成相对于RootDirectory的路径:/usr/bin/foorbad。
注:systemd中提供了一个方便的轻量级的容器管理工具systemd-naspawn,具体请查看container(systemd-nspawn)一节。
另外如果想控制服务单元对特定目录的访问,systemd的service单元提供了以下选项(详细解释请查看本节参考链接1对应条目):
  • User=,Group=
    • 指定服务单元执行时候的用户与用户组,可以配合目录权限设置与此选项的设置而控制服务单元的访问各目录的权限。
  • InaccessibleDirectories=
    • 指定服务单元不可访问的目录,指定的目录对于服务单元来说将为空,且访问模式为000,若值以'-'开头,则目录不存在时将自动忽略。
  • ReadWriteDirectories=
    • 指定服务单元可读写的目录。
  • ReadOnlyDirectories=
    • 指定服务单元只读的目录,若值以'-'开头,则目录不存在时将自动忽略。
    • 注意一个已知bug:ReadOnlyDirectories不支持递归,例如即使指定了/var目录,而/var目录下若包含其他子文件夹,这些子文件夹对于服务单元来说依旧可读写,后续版本将修复。
例(/home目录对于此服务单元不可访问,而/var目录对于此服务单元只读):
...
[Service]
ExecStart=...
InaccessibleDirectories=/home
ReadOnlyDirectories=/var
...

Isolating Services from the Network

若服务单元无需网络服务,可将其与网络隔离,以保证安全性。
PrivateNetwork= :
Takes a boolean argument. If true, sets up a new network namespace for the executed processes and configures only the loopback network device "lo" inside it. No other network devices will be available to the executed process. This is useful to securely turn off network access by the executed process. Defaults to false. It is possible to run two or more units within the same private network namespace by using the JoinsNamespaceOf= directive, see systemd.unit(5) for details. Note that this option will disconnect all socket families from the host, this includes AF_NETLINK and AF_UNIX. The latter has the effect that AF_UNIX sockets in the abstract socket namespace will become unavailable to the processes (however, those located in the file system will continue to be accessible).
例:
...
[Service]
ExecStart=...
PrivateNetwork=yes
...

Service-Private /tmp

由于/tmp目录的权限问题,容易带来一些安全问题,systemd中可以在服务单元中设置私有/tmp目录,可使服务进程更加安全。
PrivateTmp=
Takes a boolean argument. If true, sets up a new file system namespace for the executed processes and mounts private /tmp and /var/tmp directories inside it that is not shared by processes outside of the namespace. This is useful to secure access to temporary files of the process, but makes sharing between processes via /tmp or /var/tmp impossible. If this is enabled, all temporary files created by a service in these directories will be removed after the service is stopped. Defaults to false. It is possible to run two or more units within the same private /tmp and /var/tmp namespace by using the JoinsNamespaceOf= directive, see systemd.unit(5) for details. Note that using this setting will disconnect propagation of mounts from the service to the host (propagation in the opposite direction continues to work). This means that this setting may not be used for services which shall be able to install mount points in the main mount namespace.
注意:使用privatetmp之后,基于/tmp或/var/tmp目录进行进程间通讯(IPC)将不可用。
/privatetmp目录位于/tmp目录下,如图privatetmp (在nginx.service服务单元中启用了PrivateTmp):
例:
...
[Service]
ExecStart=...
PrivateTmp=yes
...

Capabilities

systemd还可对服务进程的Capabilities(能力)进行控制,相关的选项有Capabilities=,CapabilityBoundingSet=,SecureBits=。 具体请查看本节参考链接1与 capabilities 用户手册。
注:此部分选项未实验。

服务进程运行中的资源限制

LimitCPU=, LimitFSIZE=, LimitDATA=, LimitSTACK=, LimitCORE=, LimitRSS=, LimitNOFILE=, LimitAS=, LimitNPROC=, LimitMEMLOCK=, LimitLOCKS=, LimitSIGPENDING=, LimitMSGQUEUE=, LimitNICE=, LimitRTPRIO=, LimitRTTIME=
以上选项可用于服务进程运行时的资源限制,选项具体意义请参考setrlimit
例:
...
[Service]
ExecStart=...
LimitNPROC=1
LimitFSIZE=0
...
查看setrlimit手册,如下:
  • RLIMIT_NPROC
    • The maximum number of processes (or, more precisely on Linux,threads) that can be created for the real user ID of thecalling process.
  • RLIMIT_FSIZE
    • The maximum size of files that the process may create.Attempts to extend a file beyond this limit result in delivery of a SIGXFSZ signal
可知以上单元文件限制了此服务进程最多只能有一个进程,即禁止了其fork,且此服务进程能创建的最大文件大小为0。

设备节点访问控制

DeviceAllow选项可指定服务进程可访问的设备节点以及相关权限,
例如:
...
[Service]
ExecStart=...
DeviceAllow=/dev/null rw
...
该服务单元只对/dev/null有访问权限,且权限为读写。
另外还有PrivateDevices=,ProtectSystem=,ProtectHome=等等选项,具体用法请参考本节参考链接1。

本节参考链接

  1. systemd.exec
  2. Securing Your Services
  3. http://0pointer.de/blog/projects/changing-roots.html

监控与报警

思路:
  • service单元中的ExecStartPre与ExecStopPost等选项可用于监控与报警。关于service单元介绍请查看服务进程管理一节。
    • 服务启动前可通过ExecStartPre执行脚本进行通知或做环境的初始化
    • 服务退出后,可通过ExecStopPost执行脚本,例如报警或清理工作。
    • 注:除了正常停止服务进程会触发此选项指定的命令,即使是kill -9使服务进程异常中止,ExecStopPost指定的命令或脚本依旧成功运行,此特性可用于服务进程异常退出后的报警
    • 注:若ExecStart失败,ExecStopPost将不会执行!若能在ExecStopPost中进行了某些清除工作,需注意是否会因为清除工作不完全而带来后续的进程启动运行的异常。
    • 但是ExecStartPost在ExecStart执行失败之后依旧可执行,可以在此处检测服务单元日志状态判断ExecStart是否执行失败而执行清理工作。由于journal的日志转发到syslog是通过socket, 存在buffer,若读取/var/log下日志内容可能存在一定延时,所以请通过journal的接口查看服务单元日志。
  • service单元中的FailureAction选项可指定服务进程进入failed状态后的自动执行动作(操作系统的重启、强制重启或立即重启),默认为none。
  • 通过后台自定义守护进程或轮询任务检测 systemd journal 日志信息,对服务状态进行监控和报警。关于日志信息的过滤与查询等内容请查看日志的记录、分类、分发和监控一节。
    • 注:使用systemctl查看特定进程systemctl status name.service,除了输出服务进程基础状态,还可输出关于此服务进程最近的10条日志记录。
通过服务单元中指定ExecStopPost等命令,在服务单元异常退出时候,通过ExecStopPost指定的程序检测对应日志可即时报警,需要注意目前若ExecStart执行失败,EcecStopPost将不会执行(这可能是一个bug,不知后续是否会修复)!
ExecStopPost以及ExecStartPost等命令中可以传入Environment=等选项配置的环境变量以及Unit单元中内置的特殊变量。前者请man systemd.exec查看Exviroment相关条目,后者请man systemd.unit查看Specifiers。 所以在ExecStopPost中,除了通过检测日志得到服务进程信息作出报警,还可以通过服务进程中设置的环境变量作为参数传入ExecStopPost,从而得到主服务进程的相关信息而执行对应操作。
例:/etc/default/ssh 文件中指定了需要的环境变量($SSHD_OPTS),而$MAINPID是sshd daemon中设置的。
[Unit]
Description=OpenBSD Secure Shell server
After=network.target auditd.service
ConditionPathExists=!/etc/ssh/sshd_not_to_be_run

[Service]
EnvironmentFile=-/etc/default/ssh
ExecStart=/usr/sbin/sshd -D $SSHD_OPTS
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
Restart=on-failure

[Install]
WantedBy=multi-user.target
Alias=sshd.service
另外通过后台程序轮询查看过滤分析各个服务单元的日志也可做监控与报警,但由于是轮询可能存在一定的延时性。

本节参考链接

----------------------------

注册为Systemd服务

Systemd 是 Linux 系统工具,用来启动守护进程,已成为大多数发行版的标准配置

所有的服务的配置文件都存放在/usr/lib/systemd/system/,设置开机启动后会在/etc/systemd/system/multi-user.target.wants符号链接

命令格式systemctl [OPTIONS...] COMMAND [UNIT...]

# 列出所有unit文件
systemctl list-unit-files
# 当前已加载到内存中的unit
systemctl list-units
# 新增配置文件后用于刷新配置
systemctl daemon-reload
systemctl start app-web1.service
systemctl stop app-web1.service
# 查看所有日志
tail -f /var/log/messages
# 查看指定服务日志
journalctl -u app-web1.service

常用配置写法,如下app-web1.service

[Unit]
Description=演示网站1
After=network-online.target mysqld.service
Wants=mysqld.service

[Service]
Type=simple
WorkingDirectory=/root/
# ExecStart必须是绝对路径,否则直接报错
ExecStart=/root/go-live-server -p 8080 -d www
KillMode=process
Restart=on-failure
RestartSec=1min

[Install]
WantedBy=multi-user.target

为了简化操作,可以通过一个脚本来管理服务的安装与启动

#!/bin/bash
sName="app-web1.service"
installPath="/usr/lib/systemd/system/"

echo "管理服务: ${sName}"
echo "命令: [status|start|stop|restart|enable|disable, log, install, uninstall]"

echo -n "请输入操作:"
read input

case $input in
  status|start|stop|restart|enable|disable)
    if [ ! -f "${installPath}${sName}" ];then
      echo "文件不存在: ${installPath}${sName}"
    else
      echo "systemctl $input $sName"
      systemctl $input $sName
    fi
    ;;
  log)
    echo "journalctl -f -u $sName"
    journalctl -f -u $sName
    ;;
  install)
    echo "cp ${sName} ${installPath}"
    cp ${sName} ${installPath}
    echo "systemctl daemon-reload"
    systemctl daemon-reload
    ;;
  uninstall)
    if [ ! -f "${installPath}${sName}" ];then
      echo "文件不存在,无需卸载"
    else
      echo "systemctl stop $sName"
      systemctl stop $sName
      echo "systemctl disable $sName"
      systemctl disable $sName
      echo "rm ${installPath}${sName}"
      rm ${installPath}${sName}
      systemctl daemon-reload
    fi
    ;;
  *)
    echo "不支持的命令: $input"
    ;;
esac

No comments:

Post a Comment