ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux运维绕不开的systemd:服务管理、日志排查与实战案例

Linux运维绕不开的systemd:服务管理、日志排查与实战案例 都说十年Linux老运维最怕两件事一是机器突然起不来二是起不来之后发现是某个服务拖的。而这两件事十有八九都和systemd有关。做了这么多年运维我从一开始抵触systemd到后来发现这玩意儿确实是现代Linux绕不开的核心基础设施——系统里跑着几百个systemd管理的服务开机启动、崩溃自愈、依赖编排、日志收集全都要靠它。这篇笔记我把这些年用systemd的经验和踩过的坑梳理一遍从unit文件怎么写、管理命令怎么用、日志怎么看到几个典型的实战案例和排查技巧尽量写得让新人能上手、让老手能查漏补缺。这篇笔记适合正在学Linux基础的人、刚接手服务器运维的同行以及那些跟我一样被“服务又起不来了”折磨过的朋友。看完之后你至少能自己写一个service文件能看懂systemctl status到底在说什么遇到“Exit code不是0”时知道去哪查。1. 为什么是systemd从服务启动方式的演进说起1.1 传统init到底别扭在哪在systemd之前各个Linux发行版用的是SysV init脚本。那套机制说白了就是一堆放在/etc/init.d/下的shell脚本用/etc/rc.d/下一堆不同级别runlevel的软链来控制启动顺序。跑什么服务就执行什么脚本不管依赖、不管失败重试、不管日志收集脚本写得像天书一样乱。有时候Oracle数据库起来慢后面的脚本还得靠sleep 10这种毫无技术含量的写法去硬等启动时候浪费一堆时间。另外一个问题是管理粒度太粗。你想看一个服务是不是活着service sshd status能不能查出真实状态完全取决于脚本作者当时的心情。进程起了但脚本没写好你就可能看到明明进程还活着、服务却显示“已死”的诡异现象。用ps去查进程又没法直接看出它是由哪个服务拉起的。这些痛点在我刚做运维那会儿天天都在亲身经历。1.2 systemd的核心设计unit和targetsystemd的出现把这些旧账一次性算清楚了。它把一切可以被系统管理的东西都抽象成unitunit不只是服务还有挂载点.mount、套接字.socket、定时器.timer、设备.device等十几种类型。每个unit用一份独立的描述文件定义格式统一、内容透明。以前区分“开机启动级别”用的是runlevelsystemd用target替代了。理解target不用想得太玄你可以把它当成一个“启动目的地的分组”。比如multi-user.target对应传统意义上的“多用户命令行模式”graphical.target对应带图形界面的模式。一个服务要开机自启就是在/etc/systemd/system/下做一条指向它的软链接放进某个target目录里系统启动到那个target时就会带上它。这个过程不用写脚本一条systemctl enable就能完成。systemd另一个让人上头的地方是依赖关系和并行启动。传统init是一个脚本一个脚本串行跑systemd会在unit文件里声明依赖同一层级的服务尽量并行拉起所以现代Linux发行版开机就是快。同时它对进程的跟踪是接近“天网”级别的即使你的服务启动后自己forks出了几个子进程它依然能把日志、状态管理到位。后面我们用systemctl命令时你会感觉到这种“打通的体验”是真的舒服。2. unit文件systemd的“心脏”2.1 文件放哪、优先级怎么算写systemd的unit文件第一步要搞清楚文件放哪儿。系统自带的unit文件在/usr/lib/systemd/system/下发行版升级时会被覆盖别去改它。自己写的一律放/etc/systemd/system/这里是优先级最高的目录同样的unit名字/etc下的会覆盖/lib下的。两个目录都会被systemd搜索到所以有个很实用的技巧你想改某个系统自带服务的行为不用去复制整个文件只需要在/etc/systemd/system/下放一个同名文件里面写上你要改的片段即可。systemd读取的时候会“合并”生效后加载的/etc配置覆盖前者的同键值设置。比如我觉得系统的日志服务参数不合适我就写一个/etc/systemd/system/rsyslog.service.d/override.conf。注意这个目录名带.d后缀的还有一个妙用场景如果你写了一个自定义服务在做版本升级时希望某些参数能动态调整用override.conf这种drop-in片段去覆盖比直改主文件安全得多。因为在某些发行版的系统升级流程里/etc下的文件一般不会被动这是行业里的约定。改完unit文件之后必须跑systemctl daemon-reload不执行这句你改了等于白改systemd会继续用旧配置。这个坑我见过无数新人踩包括我自己当年也栽过一回——改了环境变量服务重启还是拿旧值排查半天才发现是忘记reload。2.2 三段式结构[Unit]、[Service]、[Install]一个典型的service unit文件长这样我们以SSH服务为例[Unit] DescriptionOpenSSH server daemon Afternetwork.target Wantssshd-keygen.service [Service] Typesimple ExecStart/usr/sbin/sshd -D ExecReload/bin/kill -HUP $MAINPID Restarton-failure RestartSec3s Userroot Grouproot LimitNOFILE65536 [Install] WantedBymulti-user.target[Unit]段是整个unit的元信息区。Description是给人看的说明文字After和Requires、Wants是用来表达依赖顺序的。这里有个关键区别Requires是硬依赖被依赖的unit起不来自己就不允许启动。Wants是软依赖被依赖的unit会尽量拉起但如果它失败了Wants方并不会罢工。After只管顺序不管依赖它只告诉systemd“我等前面那个启动完了再启动”至于前面那个挂没挂不关我事。所以通常会把Wants和After配合一起写既保证顺序、又允许容错。另外如果写了Requiresfoo.service却不加After在启动顺序上可能出现一个反直觉的情况两者并行启动systemd并不保证foo一定先于你启动完这在依赖强场景下可能会踩坑。想表达“必须先起来”的意思就一定要同时写Requires/After。[Service]段是核心中的核心它定义服务怎么跑。这一段的参数非常多后面我用单独的章节重点拆解因为大部分配置错误都出在这里。[Install]段和“开机自启”直接相关。WantedBymulti-user.target的意思是当执行systemctl enable时systemd会在/etc/systemd/system/multi-user.target.wants/下创建一个指向本unit文件的软链接。同理如果你写一个WantedBydefault.target它就出现在默认target的wants目录里。这个设计的好处是禁用和启用只是一个软链接的增删不碰原文件本身既安全又清晰。RequiredBy则更硬它会放进.requires/目录。2.3 Type服务类型的选择Type是我见过踩坑最多的参数。它有几种取值每一次都对应不同的进程行为描述simple默认值。systemd认为ExecStart启动的进程就是主进程直接算做服务运行成功。如果这个进程退出服务就结束。适用于sshd -D这类以前台方式常驻的命令。forking适用于传统daemon化程序也就是那种启动后父进程退出、子进程继续跑的。比如nginx、httpd。systemd需要你用PIDFile指定子进程的pid文件否则它没法确认真正的服务进程是谁。写这一类时你还需要配一个适当的ExecStartPost比如在pid文件出现之后再确认状态。oneshot适用于一次性任务执行完就退出不常驻。比如初始化脚本、建目录、删临时文件等。配Typeoneshot时通常还会加RemainAfterExityes意思是退出之后systemd依然认为该unit处于“激活active”状态直到显式被stop。我写K8s节点上的某些初始化任务时经常用这个组合。notify进程启动完毕之后主动通过sd_notify接口通知systemd“我准备好了”在这之前systemd会一直等。数据库、Nginx这类启动比较慢、对“读就绪”要求敏感的服务用这个更稳。需要程序本身支持systemd通知机制不支持的话别硬用。dbus服务启动后在D-Bus上注册一个名字。systemd等这个名字出现才认为服务就绪。桌面的系统服务常用这个。还有exec、idle等更冷门的类型日常服务用不着按需查阅手册即可。让我举个实际选择示例假设你在写一个Oracle数据库的启动服务。sqlplus / as sysdba startup这种命令是持续执行的不能直接当ExecStart扔进去因为sqlplus不是一个常驻进程——它执行完startup就退出。如果你用Typeoneshot那么命令执行完systemd就认为任务完成配合RemainAfterExityes这个服务就会像“已启动且常驻”一样hold住进程状态上表现为数据库进程在跑systemd认为服务active。但如果你想对数据库做更精细的生命周期管理那就得靠/etc/init.d/oracle脚本提供的start动作配Typeforking再去指定进程的pid文件。两种都可行差别在于你想让systemd为你承担多大的监控责任。2.4 ExecStart的写法、环境变量和权限ExecStart的坑主要在两点路径、引号。路径上systemd执行的命令不会加载用户的shell环境PATH只有系统默认的那几个目录。如果你的程序装在/opt下、用了自定义的环境变量请在unit文件里明确写绝对路径或者用Environment提前声明。我见过有人用ExecStartmysqld_safe然后怎么也起不来一查就是/usr/local/mysql/bin不在PATH里。另外别把/bin/sh的习惯带进来比如ExecStart/bin/sh -c xxx yyy虽说不至于报错但完全没必要本来就有ExecStartPre和ExecStartPost可以拆步骤。引号方面ExecStart整行会被systemd的命令行解析器拆词不支持shell的转义规则。文件路径里的空格得小心大白话建议路径尽量别带空格带了的话要用引号括起来还得注意转义。权限上有两个最常见的错误不写User导致服务以root跑以及写了User却忘了目录和日志的属主权限。在安全合规要求严格的场景下我强烈建议写明确User和Group别默认root跑一切。比如我写一个Web应用的部署unit都是建好专用用户然后Userwebapp配WorkingDirectory/srv/webapp。这样即使程序被攻破影响也被限制在用户权限之内。WorkingDirectory也很关键有的程序会找当前目录下的配置文件不设置它启动目录取决于systemd的默认工作目录通常是/下。所以要么在进程启动前cd进去要么直接写WorkingDirectory。我通常两种都会做不依赖程序内部对当前目录的假设。3. systemctl管理命令日常操作核心3.1 最常用的命令清单systemctl的命令我日常用得最多的是这些# 启动、停止、重启 systemctl start xxx.service systemctl stop xxx.service systemctl restart xxx.service # 查看状态 systemctl status xxx.service # 开机自启 systemctl enable xxx.service systemctl disable xxx.service systemctl is-enabled xxx.service # 查看所有正在运行的unit systemctl list-units --typeservice --staterunning # 查看启动失败的unit这个要经常用 systemctl --failedsystemctl status输出里包含非常多关键信息unit文件路径、进程PID、内存状态、最近日志、Active状态是active (running)还是failed还是inactive (dead)还有Main PID对应的CGroup路径。新手可以先把Active状态和status末尾的日志读明白这两个看懂问题基本能定位一半。3.2 enable、mask、daemon-reload的区别这三个概念必须区分清白因为它们的语义完全不同enable是在.wants目录里创建软链让unit在启动时被自动拉起。它不影响当前状态。你执行了systemctl enable服务并不会立即启动除非你额外再执行start。mask比disable更霸道。disable只是移除软链服务还是能手动startmask会在/etc/systemd/system/下建立一个指向/dev/null的链接等于把这个unit“封印”了无论手动还是依赖它都起不来除非unmask。我遇到过有些服务被依赖拖累反复重启临时排除时就用mask。daemon-reload前面说过了是让systemd重新扫描unit文件。改了文件、加了文件、删了文件都要跑一次很朴素但极其重要。还有个命令我几乎每天用systemctl list-dependencies xxx.service它会树状打印这个unit的依赖关系排查“为什么这个服务拉起了一大堆别的服务”特别直观。3.3 延时任务与立即执行运维场景里经常要“过几分钟再重启服务”不必去写脚本配合at命令systemd-run可以直接做systemd-run --on-active300 systemctl restart mysql.service这个命令本质是临时创建一个timer unit和一个service unit到时间自动帮你执行。好处是它由systemd管理有日志可查比挂个后台nohup进程靠谱得多。但我每次用完会提醒自己这种临时timer在重启后不会保留只适合一次性的现场操作。4. journal日志排查问题的第一现场4.1 journalctl的核心用法systemd日志管理用的是journald日志统一存放在/var/log/journal/下。排查问题时的第一命令往往是journalctl# 看指定服务的完整日志 journalctl -u nginx.service # 看最近30分钟的日志 journalctl -u mysql.service --since 30 min ago # 跟随输出相当于tail -f journalctl -f -u php-fpm.service # 指定进程PID的日志 journalctl _PID1234最实用的组合是journalctl -u xxx.service -n 50查看最近50行比status里截断的日志更完整。加上-e直接跳到日志末尾操作习惯上更顺。为什么会选择把日志放到journal而不是纯文本文件因为journal提供了结构化元数据进程号、用户、单元名、时间戳、优先级都可用过滤器查。比如排查崩溃journalctl -p err -b能列出当前启动以来的所有error级日志扫一眼就知道哪个服务在搞鬼。4.2 日志持久化问题journald默认不会把日志落盘重启后日志消失。要持久化必须建目录mkdir -p /var/log/journal systemctl restart systemd-journald这个坑很隐蔽很多服务器跑了好几个月你想查上一次崩溃时的日志发现啥都没有。后来我把服务部署规范里加了一条凡是生产环境必须启用journal持久化。同理还要注意日志体积默认设置下journal可能无限长大建议在/etc/systemd/journald.conf里配SystemMaxUse500M按磁盘实际情况给个上限。4.3 跟业务日志打通StandardOutput与StandardError还有一个很多人没注意到的能力systemd不仅仅收集服务自己打到syslog的日志还支持捕获服务进程的标准输出和标准错误。unit文件里写StandardOutputjournal StandardErrorjournal这样一来那些没有自己的日志库、只会往终端print的应用程序日志也会自动塞进journald。我排查第三方闭源程序问题时经常靠这一招。如果你想同时保留终端的可读性用StandardOutputappend:/var/log/xxx.log把标准输出重定向到普通日志文件journal的优先级还是会建议保留。这里一个小技巧如果某个服务打印的日志量巨大你可以只把StandardErrorjournal标准输出定向到日志文件避免把journal塞爆。5. 实战案例从0到1编写一个可用的服务5.1 案例一用systemd管理数据库服务的启动搜索词里有一条”systemd execstart 如何添加sqlplus / as sysdba startup oracle”这正好带我回忆起一个真实的配置场景。假设你有一台Oracle数据库服务器想让数据库通过systemd在开机时自动拉起来。难点在于Oracle进程需要一堆环境变量ORACLE_HOME、ORACLE_SID等并且启动数据库的命令本身带有/as sysdba的特殊权限要求。写一个service文件/etc/systemd/system/oracle-db.service[Unit] DescriptionOracle Database 19c Instance Afternetwork.target Wantsnetwork-online.target [Service] Typeoneshot Useroracle Groupoinstall EnvironmentORACLE_HOME/u01/app/oracle/product/19c/dbhome_1 EnvironmentORACLE_SIDORCL EnvironmentPATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ExecStart/bin/su - oracle -c export ORACLE_HOME/u01/app/oracle/product/19c/dbhome_1; export ORACLE_SIDORCL; $ORACLE_HOME/bin/sqlplus / as sysdba /u01/app/oracle/scripts/startup.sql ExecStop/bin/su - oracle -c export ORACLE_HOME/u01/app/oracle/product/19c/dbhome_1; export ORACLE_SIDORCL; $ORACLE_HOME/bin/sqlplus / as sysdba /u01/app/oracle/scripts/shutdown.sql RemainAfterExityes Restartno [Install] WantedBymulti-user.target这段配置是典型的oneshot RemainAfterExit方案。为什么不用Typeforking因为Oracle的进程管理和传统daemon行为有差异它没有一个明确的PID文件作为唯一主进程手工处理很容易出现systemd监控和实际进程状态不一致的情况。用oneshot的好处是只要sqlplus执行的startup命令成功完成systemd就认为这个service已激活。数据库本身是否真的对外可用由它自己的后台进程维持systemd只需要保证开机执行了启动动作即可。配合su - oracle去隔离用户环境变量问题也一起解决了。踩坑提醒这个ExecStart里千万不要偷懒写成ExecStart/u01/app/oracle/product/19c/dbhome_1/bin/sqlplus / as sysdba startup。因为sqlplus进程需要在oracle用户的环境下运行直接以root去执行这条路权限和环境都不对。用/bin/su其实是“模拟用户在交互式环境下跑命令”的兜底方案。如果程序支持setuid或systemd原生User字段管理那更规范但Oracle这种老牌重件本身对“启动环境”这套要求太细用su -是我实践下来最稳的方式。5.2 案例二VNC服务迁移到systemd unit在一台Rocky Linux 9合集中观察到VNC服务的变化是“vncserver has been replaced by a systemd unit”。这个改动其实代表着VNC这个老伙计也正式被systemd收编了。Rocky Linux 9上你可以看到这样的unit文件[Unit] DescriptionVNC Server for %I [Service] Typeforking ExecStart/usr/bin/vncserver -start %i ExecStop/usr/bin/vncserver -kill %i Userroot PIDFile/root/.vnc/%H:%i.pid注意到它使用了模板unit的模式文件名里带符号比如vncserver:1.service中的:1就是传给服务的display编号参数。%i会在运行时被替换成后面的实例名这就是为什么同一个模板能管理多个显示器的原因。这种设计特别适合“同一种程序、多实例运行”的场景比如多个VNC display、多份Tomcat实例。用实例启动systemctl start vncserver:1.service systemctl enable vncserver:1.service如果你的环境变量、分辨率要求因用户不同而不同还可以给每个实例写一个具体的unit文件覆盖掉模板里的ExecStart。模板unit是个很有价值的知识点学会之后管理一堆同构实例会轻松很多。5.3 案例三timer定时器替代cron的部分场景systemd自带timer unit可以部分替代cron。它在精确性、日志可查性、依赖管理方面有一点优势。一个简单例子每5分钟执行一次备份先写service文件backup-db.service[Unit] DescriptionDatabase Backup Job [Service] Typeoneshot ExecStart/usr/local/bin/backup.sh再写同样主名的timer文件backup-db.timer[Unit] DescriptionSchedule database backup every 5 minutes [Timer] OnCalendar*:0/5 Persistenttrue [Install] WantedBytimers.target启用systemctl daemon-reload systemctl enable --now backup-db.timerOnCalendar*:0/5表示每小时的0分、5分、10分……执行。Persistenttrue很聪明如果你在定时器本应执行的时候机器处于关机状态下次开机后它会补跑一次错过的任务等于一个“赶工”逻辑。这个特性在隔夜关机的工作站上做每日备份任务特别有用。看看timer系统的状态systemctl list-timers --all对比老派的crontabtimer的优势是任务执行状态、输出日志全都被journal收纳出问题看日志一步到位不用再配一套额外的日志机制。如果你现在还维护着一堆cron脚本且它们之间有先后依赖用timer加After表达依赖比cron自带的顺序控制清晰得多。6. 常见问题排查与避坑实录6.1 服务启动失败从哪几方面入手状态是failed不要慌按这个顺序排查第一步用journalctl -u xxx.service -n 50 --no-pager看最近的日志。大多数时候答案就在最后几行。如果日志完全没输出检查StandardOutput配置、程序是不是在写自己的日志文件。第二步手动前台执行ExecStart里的命令看能不能跑。这条看起来简单但能快速区分是程序本身的问题还是systemd环境的问题。比如之前提到的PATH不包含你的二进制路径手动跑OK、systemd跑失败就是环境差异。第三步检查退出码。ExecStart命令返回非零systemd会显示Main process exited, codeexited, status1/FAILURE。退出码1是通用错误具体看日志。如果是status203/EXEC那是二进制不存在或没有执行权限——查可执行文件路径和权限。如果是status217/USER检查User配置的用户是否存在。这些退出码都是速查表级别的线索背下来排查能快不少。6.2 为什么我改了环境变量服务还是拿到旧值这个问题我吃过亏真实心态是“改了三遍还在闹鬼”。实际上原因很简单unit文件修改后没有systemctl daemon-reload或者重启服务的动作不是通过systemctl restart做的——比如你只是手动kill进程再让systemd因为它自己设置的Restarton-failure而去拉起。这个拉起动作依然会用旧的配置。必须明确改unit文件后第一步永远是daemon-reload然后才是restart。重读配置、重加载行为是两件不同的事。类似的坑还有Restartalways的服务被stop之后又自己拉起让人以为是“stop失效了”。其实Restart是一项“本服务启动后持续有效”的规则只有真正执行systemctl stopsystemd才把它理解为一个显式的停止指令不再自动重启。但如果你先disable后stop下次开机还是会自启——disable只管开机stop只管现在。6.3 重启后服务起不来的几种原因一种是Afternetwork.target写得不严谨。很多服务的网络依赖错了导致重启时数据库、Web服务抢着起动结果网络栈还没就绪。现在的主流做法是依赖network-online.target这个target要配合SystemdNetworkd或NetworkManager-wait-online.service使用并且在unit里同时加Afternetwork-online.target和Wantsnetwork-online.target。另一种是/etc/fstab里的挂载点比服务晚出现。你写的服务需要某个目录比如备份磁盘先挂载但是没写依赖。加一行RequiresMountsFor/data就搞定。这个指令特别适合依赖数据盘的应用比盲写Afterdata.mount通用得多。6.4 加固建议所有人都应知道的安全与性能调优关闭不需要的服务新装的系统上顺手执行systemctl list-units --typeservice扫一遍每个不明服务都查一下是干什么的通过mask处理不用的服务减小攻击面。限制服务资源给[Service]段加上LimitNOFILE、LimitNPROC、MemoryMax这类资源限制对数据库、Web服务的效果非常直接。LimitNOFILE65535基本是标配。看门狗service文件里有一个隐藏很深的强大配置WatchdogSec30。如果你的服务是长期运行的交互进程通过通知机制与WatchdogSec维持心跳systemd可以在进程卡死、停止响应时强制杀掉并重启。这比单纯依赖TCP健康检查要早一步发现进程假死。当然程序如果没实现systemd通知协议这个配置就不适用。考虑Security加强systemd提供的ProtectSystemstrict、ProtectHometrue、PrivateTmptrue等沙箱选项对加固有奇效。给每个服务按最小权限逻辑做评估能read-only挂载的目录就配只读能不开网络就关网络命名空间。别把这些全堆上因为有些程序确实需要写临时文件或访问家目录配之前先拿测试环境跑一遍。7. 写在最后十年经验的几点私房心得说几条我在实际项目中总结的心得供参考。第一尽量做到所有的服务都交给systemd管。哪怕是临时脚本只要它需要“长期存活、自愈、跟随开机启动”中的任意一条我就给它写个unit。好处是不用在服务器上记一堆nohup的pid文件、不用到处找哪个进程是谁拉起来的一条systemctl status全部讲清楚。这种统一管理带来的省心谁用谁知道。第二写好注释。unit文件里面#注释多写几行说明这个服务是干嘛的、依赖什么、为什么用这个Type。这不仅是为了别人方便三个月后的你自己大概率也会忘了这段配置当初的思路。我维护的服务器上每个自定义unit文件都有一块“变更记录”注释这门手艺在很多大公司项目里是硬性要求。第三出了问题先把“重启”念头按住。很多人在服务挂掉后第一反应就是restart结果日志还没来得及看一眼。我现在的习惯是先journalctl -u xxx -n 200再systemctl status确认现场之后再做处置。因为restart能解决一时的现象但会掩盖底层的配置问题尤其那种“几天崩一次”的服务容不得你每次都靠重启糊弄。最后如果你所在的环境还对System V念念不忘我理解这种习惯的惯性。但systemd已经是所有主流发行版的基础设施了这几年连各种嵌入式设备、容器镜像里的最小init都开始自带适配尽早把systemd用得溜起来对今后的排障、部署、架构设计都是稳赚的投资。这篇笔记里的内容不能说覆盖了systemd的全部但如果你能把service、target、journal、timer这几块吃透再把常见的退出码和依赖写法记牢日常工作里绝大多数场景都已经能handle住了。
返回列表