Բոլոր դասերը
Linux և սերվերներՄիջին12 րոպե ընթերցումԻմ ստեկում

Պրոցեսներ, signal-ներ և systemd

Ինչն է աշխատում, ինչպես այն ճիշտ կանգնեցնել և ինչպես systemd-ով կենդանի պահել հավելվածը։

  • ps / top
  • Signals
  • systemd units
  • journalctl
Քայլ 1/5
03:13։ myapp-ը աշխատում է որպես systemd service, unit ֆայլում՝ Restart=on-failure, RestartSec=5։ systemd-ը՝ PID 1-ը, թեյ է խմում։

30 վայրկյանում

Յուրաքանչյուր աշխատող ծրագիր պրոցես է՝ ID-ով, տիրոջով և ծնողով։ Պրոցեսները զննում ես ps-ով և top-ով, իսկ կառավարում՝ signal-ներով։ Ժամանակակից Linux-ում systemd-ը boot-ի ժամանակ գործարկում է ծառայությունները, restart է անում դրանք crash-ի դեպքում և հավաքում դրանց log-երը։ Փոքր unit ֆայլ գրելը Node կամ Python հավելվածը սերվերի վրա աշխատեցնելու ամենամաքուր ձևն է։

Ինչու եմ օգտագործում

  • Սերվերների վրա երկարատև աշխատանքը կենդանի եմ պահում process manager-ներով՝ supervisor Laravel-ի queue worker-ների համար և pm2 Node հավելվածների համար, որոնք երկուսն էլ աշխատում են systemd-ի վրա։
  • Երբ սերվերը դանդաղ է, առաջինը ստուգում եմ, թե որ պրոցեսն է օգտագործում CPU կամ հիշողություն, և դա սկսվում է top-ից և ps-ից։
  • systemd-ն ու journalctl-ը իմանալը նշանակում է, որ կարող եմ տեսնել, թե ինչու ծառայությունը չգործարկվեց, այլ ոչ թե գուշակել։

Ինչպես է աշխատում

01Պրոցեսներ և PID-ներ

Պրոցեսը ծրագրի աշխատող օրինակ է։ Kernel-ը յուրաքանչյուրին տալիս է process ID (PID), և յուրաքանչյուր պրոցես ունի ծնող (իր PPID-ն)։ systemd-ով մեքենայում PID 1-ը systemd-ն է՝ մնացած ամեն ինչի նախահայրը։ Պրոցեսն աշխատում է որոշակի օգտատիրոջ անունից, ունի իր հիշողությունը, բաց ֆայլերը և environment փոփոխականները, և ավարտվում է exit code-ով․ 0-ն նշանակում է հաջողություն, իսկ ցանկացած այլ արժեք՝ ինչ-որ ձախողում։ ps-ը ցույց է տալիս պրոցեսների snapshot, top-ը և htop-ը՝ կենդանի, դասավորված տեսք։ top-ում load average-ը ցույց է տալիս, թե միջինում քանի պրոցես է ուզում CPU 1, 5 և 15 րոպեների ընթացքում․ համեմատիր այն CPU core-երի քանակի հետ (nproc)։

bash
ps aux | head                    # all processes, with user, CPU, memory
ps aux --sort=-%mem | head -n 10 # biggest memory users
ps -ef --forest | less           # parent/child tree
pgrep -a node                    # find processes by name
top                              # live view (P = sort CPU, M = memory, q = quit)
htop                             # friendlier top (apt install htop)
uptime; nproc                    # load average vs. number of cores

02Signal-ներ և kill

Signal-ները կարճ հաղորդագրություններ են, որոնք kernel-ը հասցնում է պրոցեսին։ Չնայած անվանը՝ kill-ը պարզապես signal է ուղարկում։ SIGTERM-ը (15, լռելյայնը) քաղաքավարի խնդրում է պրոցեսին կանգնել․ լավ հավելվածն ավարտում է ընթացիկ request-ները, փակում տվյալների բազայի միացումները և դուրս գալիս։ SIGKILL-ը (9) հնարավոր չէ որսալ․ kernel-ը պրոցեսը հեռացնում է անմիջապես, առանց մաքրման, ինչը կարող է թողնել կիսագրված ֆայլեր կամ lock-եր, ուստի օգտագործիր այն միայն այն դեպքում, երբ TERM-ը չի աշխատում։ SIGINT-ը (2) այն է, ինչ ուղարկում է Ctrl+C-ն։ SIGHUP-ը (1) ավանդաբար նշանակում է «վերբեռնիր config-դ», և շատ daemon-ներ, ինչպես nginx-ը, հարգում են դա։ Լավ գրված հավելվածը մշակում է SIGTERM-ը, որովհետև հենց դա են ուղարկում systemd-ն, Docker-ը և Kubernetes-ը յուրաքանչյուր deploy-ի կամ shutdown-ի ժամանակ։

bash
kill 4242               # SIGTERM: please stop
kill -HUP 4242          # reload config (if the app supports it)
kill -9 4242            # SIGKILL: last resort, no cleanup
pkill -f "node server.js"   # by command line
# In Node: shut down gracefully on SIGTERM
# process.on("SIGTERM", () => server.close(() => process.exit(0)));

03Foreground, background և կենդանի մնալը

Տերմինալում գործարկած հրամանն աշխատում է foreground-ում և կապված է այդ session-ին։ Ctrl+Z-ը դադարեցնում է այն, bg-ն շարունակում է background-ում, fg-ն վերադարձնում է, իսկ վերջում &-ը սկզբից գործարկում է այն background-ում։ Երբ փակում ես SSH session-ը, shell-ը իր job-երին ուղարկում է SIGHUP, և դրանք սովորաբար մահանում են։ nohup-ը կամ tmux-ի նման terminal multiplexer-ը պրոցեսը աշխատեցնում են logout-ից հետո էլ, ինչը նորմալ է միանգամյա migration-ի կամ երկար պատճենման համար։ Բայց սրանցից ոչ մեկը չի restart անում crash եղած պրոցեսը և չի գործարկում այն reboot-ից հետո։ Այն ամենի համար, ինչ պետք է միշտ աշխատի, օգտագործիր service manager։

bash
long-task &                 # start in background
jobs                        # list background jobs
fg %1                       # bring job 1 back
nohup ./import.sh > import.log 2>&1 &   # survive logout
tmux new -s work            # detach with Ctrl+B then D
tmux attach -t work         # reattach later

04systemd unit-ներ և systemctl

systemd-ն init համակարգն ու service manager-ն է գրեթե բոլոր ներկայիս դիստրիբուտիվներում, այդ թվում՝ Ubuntu-ում, Debian-ում և RHEL-ում։ Այն կառավարում է unit-ներ․ .service-ը գործարկում է պրոցես, .timer-ը ժամանակացույցով trigger է անում ծառայություն, .socket-ը ծառայությունը գործարկում է ըստ պահանջի, իսկ .target-ը խմբավորում է unit-ները։ Փաթեթները իրենց unit-ները տեղադրում են /usr/lib/systemd/system-ում (կամ Debian-ում՝ /lib/systemd/system-ում), իսկ քո սեփական unit-ները և override-ները գնում են /etc/systemd/system, որն ունի առաջնահերթություն։ systemctl-ը դրանք կառավարելու հրամանն է։ Նախընտրիր reload-ը restart-ից, երբ ծառայությունն աջակցում է դրան, որովհետև reload-ը կիրառում է նոր config-ը՝ առանց միացումները կտրելու։ Unit ֆայլ ստեղծելուց կամ խմբագրելուց հետո գործարկիր systemctl daemon-reload, որպեսզի systemd-ն այն նորից կարդա։

bash
systemctl status nginx               # state, PID, last log lines
sudo systemctl restart nginx         # stop + start
sudo systemctl reload nginx          # re-read config, keep connections
sudo systemctl enable --now myapp    # start now and at every boot
sudo systemctl disable myapp         # do not start at boot
systemctl list-units --type=service --state=failed
systemctl cat nginx                  # show the unit file in use
sudo systemctl edit nginx            # safe override in /etc/systemd/system
sudo systemctl daemon-reload         # after editing unit files

05Unit-ի գրում Node հավելվածի համար

Unit ֆայլը կարճ INI ոճի config է։ [Unit]-ը նկարագրում է ծառայությունը և դրա հերթականությունը, [Service]-ը ասում է, թե ինչպես այն գործարկել, իսկ [Install]-ը՝ երբ գործարկել boot-ի ժամանակ։ Ստորև օրինակը Node հավելվածը գործարկում է իր չարտոնյալ օգտատիրոջով, secret-ները բեռնում է պաշտպանված env ֆայլից, restart է անում crash-ի դեպքում և ավելացնում է էժան sandboxing։ Type=simple-ը հարմար է foreground-ում մնացող հավելվածների համար, և հենց դա է քեզ պետք․ թույլ մի տուր հավելվածին ինքն իրեն daemon դարձնել։ Հավելվածը թող լսի localhost-ում, իսկ առջևում TLS-ի համար դիր nginx։ Պահպանիր սա որպես /etc/systemd/system/myapp.service, հետո գործարկիր daemon-reload և enable --now։

ini
[Unit]
Description=My Node app
After=network-online.target
Wants=network-online.target
 
[Service]
Type=simple
User=myapp
WorkingDirectory=/srv/myapp
EnvironmentFile=/etc/myapp/env
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
ProtectSystem=full
PrivateTmp=true
 
[Install]
WantedBy=multi-user.target

06journalctl՝ ծառայությունների log-երի ընթերցում

systemd-ն այն ամենը, ինչ ծառայությունը տպում է stdout-ում և stderr-ում, պահում է journal-ում՝ timestamp-ներով և metadata-ով, ուստի հավելվածդ կարող է պարզապես log անել console-ում։ journalctl-ը կարդում է այն։ Զտիր ըստ unit-ի -u-ով, հետևիր կենդանի -f-ով, սահմանափակիր ըստ ժամանակի --since-ով և ցույց տուր միայն սխալները -p err-ով։ Երբ ծառայությունը չի գործարկվում, systemctl status-ը ցույց է տալիս վերջին մի քանի տողը, իսկ journalctl -u name -b-ն՝ ամեն ինչ վերջին boot-ից ի վեր։ Դիստրիբուտիվների մեծ մասում journal-ը պահվում է սկավառակի վրա /var/log/journal-ի տակ, և դրա չափը կարելի է սահմանափակել, ինչը քննարկվում է log-երի դասում։

bash
journalctl -u myapp -f                 # follow live
journalctl -u myapp --since "1 hour ago"
journalctl -u myapp -b -p err          # errors since boot
journalctl -u nginx -n 50 --no-pager   # last 50 lines
journalctl -k                          # kernel messages (e.g. OOM kills)

07Timer-ներ ընդդեմ cron-ի

cron-ը դասական scheduler-ն է․ crontab-ում ամեն job-ի համար մեկ տող, պարզ և հասանելի ամենուր։ systemd timer-ները նույն աշխատանքն անում են ավելի շատ հնարավորություններով։ Timer unit-ը trigger է անում համապատասխան service unit, ուստի job-ը ստանում է log-եր journal-ում, ռեսուրսների սահմանափակումներ, dependency-ներ, իսկ Persistent=true-ն գործարկում է բաց թողնված job-ը, եթե մեքենան անջատված էր։ Բոլոր timer-ները և դրանց հաջորդ գործարկումը կարող ես տեսնել systemctl list-timers-ով։ Մեկ արագ job-ի համար cron-ը նորմալ է և լավ հասկացված։ Կարևոր job-երի համար, որտեղ ուզում ես log-եր, retry-ներ և իրար վրա չընկնող գործարկումներ, timer-ները ավելի լավ գործիք են։ Դիստրիբուտիվի սպասարկման շատ առաջադրանքներ, ինչպես apt-ի թարմացումները և log rotation-ը, արդեն աշխատում են որպես timer-ներ։

ini
# /etc/systemd/system/backup.timer  (runs backup.service)
[Unit]
Description=Nightly backup
 
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=10m
 
[Install]
WantedBy=timers.target
 
# Enable with: sudo systemctl enable --now backup.timer
# Check with:  systemctl list-timers

Օգտագործեք, երբ

  • Ցանկացած երկարատև հավելվածի (Node, Python, Go, worker-ներ) գործարկում VPS-ի վրա՝ առանց կոնտեյներների։
  • Բարձր CPU-ի, memory leak-երի կամ կախված պրոցեսի ախտորոշում։
  • Կրկնվող job-երի պլանավորում, որոնց պետք են log-եր, սահմանափակումներ կամ downtime-ից հետո բաց թողածի լրացում (timer-ներ)։
  • Graceful restart-ներ deploy-ի ժամանակ․ ուղարկիր SIGTERM, թող հավելվածն ավարտի ընթացիկ աշխատանքը, գործարկիր նոր տարբերակը։

Մի՛ օգտագործեք, երբ

  • Docker կոնտեյներների ներսում սովորաբար systemd չկա․ պրոցեսը restart է անում container runtime-ը կամ Compose-ը։
  • Production հավելվածները մի պահիր կենդանի nohup-ով, screen-ով կամ tmux-ով․ դրանք restart չեն անում crash-ի կամ boot-ի դեպքում։
  • Մի օգտագործիր kill -9 որպես կանգնեցնելու սովորական հրաման․ այն բաց է թողնում ամբողջ մաքրումը։

Այլընտրանքներ

  • pm2

    Հայտնի Node process manager՝ clustering-ով և log-երի մշակմամբ․ ինքը գործարկվում է systemd unit-ով։

  • supervisor

    Պարզ, հասուն manager, որը հաճախ օգտագործվում է Laravel-ի queue worker-ների և մի քանի worker պրոցեսների համար։

  • Docker Compose

    Restart policy-ները կոնտեյներները աշխատեցնում են․ Docker daemon-ը ինքը systemd ծառայություն է։

  • cron

    Ամենապարզ scheduler-ը փոքր job-երի համար, որտեղ journal-ի log-երը և dependency-ները նշանակություն չունեն։

Լավագույն պրակտիկաներ

  1. Յուրաքանչյուր ծառայություն գործարկիր իր չարտոնյալ օգտատիրոջով, երբեք՝ root-ով։
  2. Հավելվածում մշակիր SIGTERM-ը, որպեսզի restart-ները և deploy-ները չկտրեն request-ները և չփչացնեն տվյալները։
  3. Օգտագործիր Restart=on-failure RestartSec-ի հետ, որպեսզի crash-loop-ի մեջ ընկած հավելվածը չծանրաբեռնի համակարգը։
  4. Secret-ները դիր 600 ռեժիմով EnvironmentFile-ում, ոչ թե ուղիղ unit-ի մեջ։
  5. Override-ների համար օգտագործիր systemctl edit՝ փաթեթի unit ֆայլերը փոխելու փոխարեն։
  6. Log արա stdout/stderr-ում և թող journald-ը հավաքի դրանք․ սահմանափակիր journal-ի չափը։
  7. Reboot-ներից և թարմացումներից հետո ստուգիր systemctl --failed։

Տարածված սխալներ

  • Unit ֆայլը խմբագրել և մոռանալ systemctl daemon-reload-ը, ուստի հին տարբերակը շարունակում է աշխատել։
  • Հավելվածը SSH-ով գործարկել node server.js &-ով և զարմանալ, թե ինչու է այն անհետանում logout-ից կամ reboot-ից հետո։
  • Լռելյայն օգտագործել kill -9 և թողնել հին lock ֆայլեր կամ կիսագրված տվյալներ։
  • Թույլ տալ հավելվածին ինքն իրեն daemon դարձնել, մինչդեռ unit-ում գրված է Type=simple, ուստի systemd-ն կարծում է, որ այն դուրս է եկել։
  • Load average-ը շփոթել CPU-ի տոկոսի հետ՝ առանց այն core-երի քանակի հետ համեմատելու։

Խորը վերլուծություն

08Պրոցեսների վիճակները, zombie-ները և load-ի իրական իմաստը

ps-ի STAT սյունակը ասում է, թե ինչ է անում պրոցեսը։ R-ը նշանակում է աշխատում է կամ պատրաստ է աշխատելու, S-ը՝ քնած է, մինչև ինչ-որ բան տեղի ունենա (պրոցեսների մեծ մասը՝ ժամանակի մեծ մասը), D-ն՝ անընդհատելի քուն, սովորաբար սկավառակի կամ ցանցային ֆայլային համակարգի սպասում, T-ն՝ կանգնեցված, իսկ Z-ը՝ zombie։ Zombie-ն արդեն ավարտվել է․ այն պահում է միայն իր գրառումը, որպեսզի ծնողը կարողանա wait-ով կարդալ exit code-ը։ Zombie-ները CPU կամ հիշողություն չեն օգտագործում, բայց դրանց մեծ քանակը նշանակում է bug-ով ծնող, որը երբեք չի հավաքում երեխաներին․ zombie-ին kill անելը ոչինչ չի տալիս, ծնողին շտկելը կամ restart անելը՝ տալիս է։ Եթե ծնողն առաջինն է մահանում, նրա երեխաներին որդեգրում է PID 1-ը կամ subreaper-ը։ Linux-ում load average-ը հաշվում է և՛ R, և՛ D task-երը, ուստի բարձր load-ը պարապ CPU-ով սովորաբար նշանակում է դանդաղ storage-ի վրա D վիճակում խրված պրոցեսներ, ոչ թե չափազանց շատ հաշվարկ։

bash
ps -eo pid,ppid,stat,wchan:20,cmd | awk '$3 ~ /D|Z/'   # stuck or zombie
ps -o ppid= -p 12345                 # parent of a zombie
cat /proc/loadavg                    # 1/5/15 min load, running/total tasks
cat /proc/pressure/cpu /proc/pressure/io   # PSI: time spent stalled
cat /proc/12345/status | grep -E 'State|VmRSS|Threads'
ls -l /proc/12345/fd | wc -l         # open file descriptors

09cgroups v2․ ծառայությունների սահմանափակում

systemd-ն յուրաքանչյուր ծառայություն դնում է իր սեփական control group-ում (cgroup)՝ kernel-ի կոնտեյներ հաշվառման և սահմանափակումների համար։ Ubuntu-ի, Debian-ի և RHEL-ի ընթացիկ թողարկումներն օգտագործում են միասնական cgroup v2 հիերարխիան, որը systemd-ն ներկայացնում է պարզ unit կարգավորումների տեսքով։ MemoryHigh-ը սահմանափակում և հետ է վերցնում հիշողությունը, երբ ծառայությունն անցնում է soft limit-ը, MemoryMax-ը կոշտ սահման է, որը OOM killer-ը գործարկում է միայն այդ ծառայության ներսում, ուստի հիշողություն արտահոսող worker-ն է մահանում, ոչ թե կողքի database-ը։ CPUQuota-ն սահմանափակում է CPU ժամանակը, CPUWeight-ը սահմանում է հարաբերական առաջնահերթություն մրցակցության ժամանակ, IOWeight-ը նույնն է անում սկավառակի համար, իսկ TasksMax-ը սահմանափակում է պրոցեսների և թելերի քանակը, ինչը կանգնեցնում է fork bomb-ը։ systemd-cgtop-ը ցույց է տալիս կենդանի օգտագործումը ըստ ծառայության։ Աշխատող ծառայության սահմանները կարող ես փոխել systemctl set-property-ով, որը նաև գրում է մշտական drop-in։

ini
# /etc/systemd/system/worker.service.d/limits.conf
[Service]
MemoryHigh=400M
MemoryMax=512M
CPUQuota=150%
TasksMax=256
IOWeight=50
 
# Apply without editing files:
#   sudo systemctl set-property worker.service MemoryMax=512M
# Watch usage live:
#   systemd-cgtop

10Կախվածություններ, start-up տիպեր և restart policy

Երկու տարբեր գաղափարներ հաճախ շփոթում են։ Wants=-ը և Requires=-ը ասում են, թե որ unit-ները պետք է միասին գործարկվեն, իսկ After=-ը և Before=-ը միայն հերթականություն են սահմանում։ Requires-ն առանց After-ի երկուսն էլ գործարկում է միաժամանակ, և Requires-ը նշանակում է, որ քո unit-ը կանգնեցվում է, եթե կախվածությունն էլ է կանգնեցվում, ուստի Wants-ը After-ի հետ մեղմ լռելյայն տարբերակն է։ Type=-ը systemd-ին ասում է, թե երբ է start-up-ն ավարտված․ simple-ը ծառայությունը համարում է գործարկված fork-ից անմիջապես հետո, exec-ը սպասում է, մինչև binary-ն իրականում գործարկվի, ուստի սխալ ճանապարհը ձախողում է start-ը, իսկ notify-ը սպասում է, մինչև հավելվածը sd_notify-ով հայտնի պատրաստ լինելու մասին։ Restart=on-failure-ը սահմանափակված է [Unit]-ի StartLimitIntervalSec-ով և StartLimitBurst-ով․ այդ պատուհանում չափազանց շատ restart-ներից հետո systemd-ն հանձնվում է «start request repeated too quickly» հաղորդագրությամբ։ Stop-ի ժամանակ systemd-ն ուղարկում է KillSignal (SIGTERM), սպասում է TimeoutStopSec, հետո SIGKILL է ուղարկում ամբողջ cgroup-ին, ուստի ոչ մի երեխա պրոցես չի փախչում։

ini
[Unit]
Description=Queue worker
Wants=redis-server.service
After=network-online.target redis-server.service
StartLimitIntervalSec=300
StartLimitBurst=5
 
[Service]
Type=exec
User=worker
ExecStart=/usr/bin/node /srv/worker/index.js
Restart=on-failure
RestartSec=5
TimeoutStopSec=60
KillMode=mixed

11Ծառայության hardening և արդյունքի չափում

systemd-ն կարող է ծառայությունը sandbox-ի մեջ դնել մի քանի տողով, հաճախ ավելի արդյունավետ, քան container-ը։ ProtectSystem=strict-ը ծառայության համար ամբողջ ֆայլային համակարգը mount է անում read-only, իսկ ReadWritePaths=-ը բացում է միայն այն, ինչ նա պետք է գրի։ ProtectHome=true-ն թաքցնում է /home-ը, PrivateTmp-ը տալիս է սեփական /tmp, PrivateDevices-ը թաքցնում է hardware-ը, NoNewPrivileges-ը արգելափակում է setuid-ով արտոնությունների բարձրացումը, իսկ RestrictAddressFamilies-ը սահմանափակում է, թե socket-ների որ տիպերը կարող է բացել։ Դատարկ CapabilityBoundingSet=-ը հանում է բոլոր capability-ները։ systemd-analyze security name-ը տպում է exposure score՝ չսահմանված օպցիաների ցուցակով, ինչը hardening-ը դարձնում է աստիճանական և չափելի։ Սկսիր էժան, անվտանգ օպցիաներից, restart արա, ստուգիր journal-ը սխալների համար և ավելացրու ևս։ Boot-ի և start-up-ի ժամանակը նույնպես չափելի է․ systemd-analyze blame-ը և critical-chain-ը ցույց են տալիս, թե որ unit-ներն են դանդաղեցնում boot-ը։

ini
# /etc/systemd/system/myapp.service.d/hardening.conf
[Service]
ProtectSystem=strict
ReadWritePaths=/var/lib/myapp /var/log/myapp
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
NoNewPrivileges=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
CapabilityBoundingSet=
 
# Check the score:  systemd-analyze security myapp.service

Իրական իրավիճակներ

01

Ծառայությունը խրվել է crash loop-ի մեջ

Խնդիր

Deploy-ից հետո systemctl status-ը ցույց է տալիս հավելվածը failed վիճակում՝ «start request repeated too quickly» հաղորդագրությամբ։ Ձեռքով restart-ն ամեն անգամ ձախողվում է մի քանի վայրկյանում։

Ինչպես լուծել

Start limit-ը հասել է, ուստի systemd-ն դադարեցրել է փորձերը։ Նախ կարդա պատճառը․ journalctl -u myapp -b -n 100-ը ցույց է տալիս իրական սխալը՝ հաճախ բացակայող environment փոփոխական, release-ի փոխարկումից հետո սխալ ճանապարհ, հին պրոցեսի կողմից դեռ զբաղված port (ss -ltnp) կամ թույլտվությունների խնդիր։ ExecStart տողը ձեռքով գործարկիր որպես ծառայության օգտատեր sudo -u myapp-ով՝ խնդիրը վերարտադրելու համար։ Շտկիր պատճառը, եթե unit-ը փոխվել է՝ գործարկիր daemon-reload, հետո systemctl reset-failed myapp և start արա այն։ Դրանից հետո deploy-ը դարձրու արագ ձախողվող՝ health check restart-ից հետո և symlink-ի ավտոմատ rollback, եթե ծառայությունը timeout-ի ընթացքում active չի դառնում։

02

Հիշողություն արտահոսող worker-ը վայր է գցում database-ը

Խնդիր

Մի քանի օրը մեկ սերվերը դադարում է արձագանքել, և OOM killer-ը սպանում է MySQL-ը։ Հիշողության գրաֆիկները ցույց են տալիս, որ queue worker-ը դանդաղ աճում է, մինչև RAM-ը վերջանում է։

Ինչպես լուծել

Նախ զսպիր, հետո շտկիր։ Worker-ին տուր cgroup սահման MemoryHigh-ով և MemoryMax-ով drop-in-ում, որպեսզի OOM killer-ը գործի worker-ի cgroup-ի ներսում, և Restart=on-failure-ը վերադարձնի այն, մինչ database-ը մնում է անձեռնմխելի։ Database-ը լրացուցիչ պաշտպանիր նրա unit-ում OOMScoreAdjust=-500-ով։ Queue framework-ների մեծ մասը կարող է նաև ավարտել աշխատանքը որոշակի քանակի job-երից կամ հիշողության շեմից հետո՝ թույլ տալով process manager-ին գործարկել թարմ worker։ Հետո գտիր արտահոսքը․ համեմատիր հիշողությունը ըստ job-ի տիպի, Node-ում heap snapshot-ներ արա, փնտրիր աճող cache-եր կամ չփակված connection-ներ։ Ստուգիր journalctl -k-ը «Memory cgroup out of memory» տողերի համար՝ հաստատելու, որ սահմանն աշխատում է։

03

Deploy-ները կորցնում են ընթացիկ request-ները

Խնդիր

Deploy-ների ժամանակ Node API-ի յուրաքանչյուր restart առաջացնում է 502 սխալների ալիք nginx-ում և մի քանի ընդհատված background job։

Ինչպես լուծել

Հավելվածն ավարտվում է անմիջապես SIGTERM-ի ժամանակ կամ չի լսում այն և սպանվում է։ SIGTERM-ը մշակիր կոդում․ server.close-ով դադարիր նոր connection-ներ ընդունել, թույլ տուր բաց request-ներին ավարտվել, ավարտիր կամ ազատիր ընթացիկ job-ը, փակիր database pool-երը, հետո ավարտիր 0-ով։ Unit-ում TimeoutStopSec-ը դիր մի փոքր ավելի երկար, քան ամենաերկար նորմալ request-ը, որպեսզի systemd-ն չանցնի SIGKILL-ի։ Zero-downtime-ի համար գործարկիր երկու instance տարբեր port-երի կամ socket-ների վրա nginx upstream-ի հետևում և restart արա դրանք մեկ առ մեկ, կամ օգտագործիր graceful reload ունեցող process manager։ nginx-ում ավելացրու proxy_next_upstream, որպեսզի կանգնող instance-ին ուղղված request-ը կրկնվի մյուսի վրա։

Արագ ուղեցույց

Գտնել zombie և D վիճակում պրոցեսները
ps -eo pid,ppid,stat,cmd | awk '$3 ~ /Z|D/'
Կենդանի CPU և հիշողություն ըստ ծառայության
systemd-cgtop
Հիշողության սահման դնել աշխատող ծառայության վրա
sudo systemctl set-property myapp.service MemoryMax=512M
Unit-ի բոլոր հատկությունները
systemctl show myapp -p MainPID,MemoryCurrent,NRestarts
Շտկելուց հետո մաքրել failed վիճակը
sudo systemctl reset-failed myapp
Ստուգել unit ֆայլը սխալների համար
systemd-analyze verify /etc/systemd/system/myapp.service
Ծառայության hardening score
systemd-analyze security myapp.service
Ինչն է դանդաղեցնում boot-ը
systemd-analyze blame; systemd-analyze critical-chain
Միանգամյա հրաման գործարկել որպես transient unit
sudo systemd-run --unit=import -p MemoryMax=1G /srv/app/import.sh
Pressure stall info (CPU, հիշողություն, I/O)
cat /proc/pressure/{cpu,memory,io}

Իմ իրական նախագծերում

  • Սերվերների վրա Laravel-ի queue worker-ները գործարկում եմ supervisor-ով, իսկ Node հավելվածները՝ pm2-ով, և cron-ն է trigger անում Laravel-ի scheduler-ը։
  • POS SaaS սերվերի վրա երկարատև ծառայությունները և BullMQ worker-ները աշխատում են Docker Compose-ում՝ health check-երով, որպեսզի տեսնենք, որ դրանք կենդանի են։
  • Production խնդիրները ախտորոշելու համար օգտագործում եմ ծառայությունների log-երը՝ մինչև որևէ բան փոխելը, սկզբում միայն read-only։

Հարցեր և պատասխաններ

Q1Ո՞րն է տարբերությունը SIGTERM-ի և SIGKILL-ի միջև։

SIGTERM-ը դադարեցման խնդրանք է։ Պրոցեսը կարող է որսալ այն և graceful կերպով անջատվել․ դադարեցնել նոր աշխատանքի ընդունումը, ավարտել ընթացիկ request-ները, flush անել buffer-ները, փակել տվյալների բազայի միացումները և դուրս գալ։ Այն kill-ի լռելյայն signal-ն է և այն, ինչ systemd-ն, Docker-ը և Kubernetes-ը ուղարկում են առաջինը։ SIGKILL-ը հնարավոր չէ որսալ, արգելափակել կամ անտեսել․ kernel-ը պրոցեսն ավարտում է անմիջապես, ուստի մաքրում տեղի չի ունենում, և կարող ես ստանալ վնասված ֆայլեր, հին lock-եր կամ կորած job-եր։ Ճիշտ ընթացակարգն է՝ TERM, սպասել ողջամիտ timeout, հետո KILL միայն եթե պրոցեսը դեռ կենդանի է։ Հենց դա է անում systemd-ն TimeoutStopSec-ով։

Q2Ինչպե՞ս կպահես Node.js հավելվածն աշխատող վիճակում Linux սերվերի վրա։

Ես այն ձեռքով չէի գործարկի SSH-ով։ Ամենապարզ հուսալի տարբերակը systemd unit-ն է․ գործարկել այն առանձին օգտատիրոջով, սահմանել WorkingDirectory և EnvironmentFile secret-ների համար, ExecStart node binary-ով, Restart=on-failure, և enable անել, որպեսզի գործարկվի boot-ի ժամանակ։ Log-երը գնում են journal։ Այլընտրանքներն են pm2-ը, որն ավելացնում է clustering և log-երի իր մշակումը և boot-ի ժամանակ գործարկվում է pm2 startup-ով գեներացված systemd unit-ով, կամ կոնտեյներում գործարկելը restart policy-ով։ Բոլոր դեպքերում հավելվածը bind եմ անում localhost-ին, առջևում TLS-ի համար դնում եմ nginx և կոդում մշակում եմ SIGTERM-ը, որպեսզի restart-ներն առանց անակնկալների լինեն։

Q3Սերվերը դանդաղ է։ Ինչպե՞ս կպարզես պատճառը։

Սկսում եմ uptime-ից և load average-ը համեմատում եմ nproc-ի հետ։ Հետո top կամ htop՝ դասավորված ըստ CPU-ի, ապա ըստ հիշողության, ծանր պրոցեսը գտնելու համար։ free -h-ը ցույց է տալիս՝ արդյոք հիշողությունը սպառված է և swap-ն օգտագործվում է, իսկ journalctl -k-ն կամ dmesg-ը՝ արդյոք OOM killer-ը պրոցեսներ է սպանում։ Եթե CPU-ն ցածր է, բայց load-ը բարձր, պրոցեսները հավանաբար սպասում են սկավառակին, ուստի ստուգում եմ iostat-ը կամ top-ում wa արժեքը և df -h-ը՝ լցված սկավառակի համար։ Վերջում նայում եմ հավելվածի կողմը՝ դանդաղ query-ներ, կախված queue worker կամ traffic-ի պոռթկում, որը երևում է nginx-ի access log-ում։

Q4Ո՞րն է տարբերությունը systemctl restart-ի և reload-ի միջև։

restart-ը կանգնեցնում է ծառայությունը և նորից գործարկում, ուստի կարճ պահ կա, երբ պրոցես չկա, և գոյություն ունեցող միացումները կտրվում են։ reload-ը խնդրում է աշխատող պրոցեսին նորից կարդալ իր կոնֆիգուրացիան, սովորաբար SIGHUP ուղարկելով կամ կարգավորված ExecReload հրամանը գործարկելով, և շարունակում է սպասարկել։ Օրինակ՝ nginx-ը գործարկում է նոր worker պրոցեսներ նոր config-ով և թույլ է տալիս հներին ավարտել իրենց request-ները, ուստի reload-ը օգտատերերի համար անտեսանելի է։ Ոչ բոլոր ծառայություններն են աջակցում reload-ին․ systemctl reload-or-restart-ը մշակում է երկու դեպքն էլ։ nginx-ը reload անելուց առաջ միշտ գործարկում եմ nginx -t, որովհետև կոտրված config-ը ձախողում է reload-ը, և ուզում ես դա նախապես բռնել։

Q5systemd timer-ներ, թե cron․ ո՞րը կընտրես։

Փոքր, պարզ job-ի համար cron-ը միանգամայն նորմալ է, և բոլորը գիտեն դրա syntax-ը։ Կարևոր job-երի համար նախընտրում եմ systemd timer-ները։ Job-ն աշխատում է որպես սովորական ծառայություն, ուստի դրա output-ը գնում է journal՝ timestamp-ներով, այն կարող է ունենալ ռեսուրսների սահմանափակումներ և առանձին օգտատեր, չի գործարկի երկրորդ օրինակը, քանի դեռ առաջինը աշխատում է, իսկ Persistent=true-ն լրացնում է այն գործարկումները, որոնք բաց են թողնվել սերվերի անջատված ժամանակ։ systemctl list-timers-ը նաև ցույց է տալիս, թե երբ է յուրաքանչյուր job վերջին անգամ աշխատել և երբ կաշխատի հաջորդը, ինչը cron-ը չի կարող։ Գինը մեկ տողի փոխարեն երկու փոքր ֆայլն է։

Q6Որտե՞ղ ես նայում, երբ systemd ծառայությունը չի գործարկվում։

systemctl status name-ը ցույց է տալիս վիճակը, exit code-ը և վերջին log տողերը, ինչը հաճախ բավական է։ Ավելի մանրամասն տեղեկության համար journalctl -u name -b-ն ցույց է տալիս դրա ամբողջ output-ը boot-ից ի վեր, իսկ -p err ավելացնելը զտում է սխալները։ Տարածված պատճառներն են՝ սխալ ճանապարհ ExecStart-ում, բացակայող environment փոփոխական, արդեն զբաղված port (ստուգիր ss -ltnp-ով), ֆայլերի թույլտվություններ ծառայության օգտատիրոջ համար կամ syntax-ի սխալ հենց unit-ում, որը systemd-analyze verify-ը կարող է բռնել։ Եթե խմբագրել եմ unit-ը, համոզվում եմ, որ գործարկել եմ daemon-reload։ Շտկելուց հետո systemctl reset-failed-ը մաքրում է failed վիճակը։

Սենիոր մակարդակի հարցեր

S1Ինչո՞ւ PID 1-ը պետք է լինի container-ը կամ service manager-ը, ոչ թե քո հավելվածը, և ի՞նչ է սխալ գնում հակառակ դեպքում։

PID 1-ն ունի հատուկ պարտականություններ։ Kernel-ը նրա վրա չի կիրառում signal-ների լռելյայն handler-ները, ուստի SIGTERM-ը, որը հավելվածը բացահայտ չի մշակում, պարզապես անտեսվում է, և container-ը սպասում է timeout-ին, մինչև kill արվի։ PID 1-ը պետք է նաև հավաքի որբացած երեխաներին․ հավելվածը, որը subprocess-ներ է գործարկում, օրինակ՝ shell wrapper կամ headless browser, կուտակում է zombie-ներ, եթե երբեք wait չի կանչում։ Host-ում systemd-ն է PID 1-ը և կատարում է երկուսն էլ։ Container-ներում կամ օգտագործիր փոքրիկ init, օրինակ՝ tini (docker run --init), կամ համոզվիր, որ հավելվածը մշակում է signal-ները և չի գործարկում չկառավարվող երեխաներ։ Նաև հավելվածը գործարկիր Dockerfile-ում exec ձևով կամ entry script-ում exec-ով, որպեսզի signal-ները ստանա հենց նա, ոչ թե միջանկյալ shell-ը։

S2systemd unit, pm2, supervisor, թե՞ container-ներ․ ինչպե՞ս ես ընտրում process manager։

Բոլորն էլ restart են անում պրոցեսները․ տարբերությունները շրջանակի և կախվածությունների մեջ են։ Պարզ systemd unit-ին ոչինչ լրացուցիչ պետք չէ, այն տալիս է cgroup սահմաններ, sandboxing և journal log-եր և ամենահուսալի ընտրությունն է host-ի վրա մեկ-երկու ծառայության համար։ pm2-ը ավելացնում է Node-ին հատուկ հարմարություններ, ինչպես cluster mode-ը բոլոր core-երի վրա, zero-downtime reload-ները և deploy workflow-ը, մեկ այլ daemon-ի գնով՝ իր սեփական log ֆայլերով։ supervisor-ը պարզ է և լեզվից անկախ, տարածված է մի քանի queue worker պրոցես գործարկելու համար, բայց չունի սեփական cgroup սահմաններ։ Container-ները բերում են վերարտադրելի image-ներ և մեկուսացում, ինչն արդարանում է շատ ծառայությունների կամ թիմերի դեպքում։ Ինչ էլ ընտրես, առանց պատճառի մի դիր manager-ները իրար վրա և հստակ իմացիր, թե որ շերտն է պատասխանատու restart-ների, log-երի և սահմանների համար։

S3Ինչպե՞ս կսահմանափակես հիշողության արտահոսքի ազդեցության շառավիղը ընդհանուր սերվերի վրա։

Ենթադրիր, որ ինչ-որ բան արտահոսելու է, և նախապես որոշիր, թե ով պետք է մահանա։ Յուրաքանչյուր ծառայություն դիր իր cgroup-ում՝ MemoryHigh-ով որպես soft limit, որպեսզի kernel-ը նախ հետ վերցնի հիշողությունը և դանդաղեցնի ծառայությունը, և MemoryMax-ով որպես hard limit, որպեսզի OOM killer-ը գործի միայն այդ cgroup-ի ներսում։ Կրիտիկական ծառայությունների, օրինակ՝ database-ի OOM score-ը իջեցրու OOMScoreAdjust-ով։ Թույլ տուր restart policy-ին վերականգնել արտահոսող ծառայությունը և alert դիր NRestarts-ի կամ OOM իրադարձությունների վրա, որպեսզի արտահոսքը տեսանելի լինի, ոչ թե լուռ «բուժվի»։ Պահիր որոշ swap ցատկերը կլանելու համար և դիտարկիր systemd-oomd-ն, որը կարող է ավելի վաղ գործել հիշողության ճնշման դեպքում։ Գինն այն է, որ չափազանց ցածր սահմանները restart-ներ են առաջացնում օրինական գագաթների ժամանակ, ուստի դրանք հիմնավորիր դիտարկված օգտագործմամբ՝ գումարած պաշար։

Բառարան

Zombie պրոցես
Պրոցես, որն ավարտվել է, բայց որի ծնողը դեռ wait-ով չի հավաքել նրա exit status-ը։
Uninterruptible sleep (D)
Վիճակ, երբ պրոցեսը սպասում է I/O-ի և չի կարող ընդհատվել, նույնիսկ SIGKILL-ով, մինչև I/O-ն ավարտվի։
Subreaper
Պրոցես, որը PID 1-ի փոխարեն որդեգրում է որբացած հետնորդներին, ինչպես անում են systemd user manager-ները և container-ների init պրոցեսները։
cgroup
Control group․ պրոցեսների kernel-ային խմբավորում՝ CPU-ի, հիշողության, I/O-ի և պրոցեսների քանակի հաշվառման և սահմանափակման համար։
Drop-in
Փոքր .conf ֆայլ unit-ի .d դիրեկտորիայում, որը վերագրում է ընտրված կարգավորումները՝ առանց ամբողջ unit-ը փոխարինելու։
sd_notify
Արձանագրություն, որով հավելվածը systemd-ին հայտնում է, որ պատրաստ է կամ դեռ կենդանի է․ օգտագործվում է Type=notify-ի և WatchdogSec-ի հետ։
Start limit
systemd-ի սահմանը restart-ների քանակի վրա ժամանակային պատուհանում։ Հասնելուց հետո unit-ը մնում է failed, մինչև reset։
PSI
Pressure Stall Information․ kernel-ի մետրիկաներ, որոնք ցույց են տալիս, թե task-երը որքան ժամանակ են սպասում CPU-ի, հիշողության կամ I/O-ի։
Transient unit
systemd-run-ով անմիջապես ստեղծված unit՝ սահմաններով և log-երով, որն անհետանում է ավարտվելիս։

Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։

Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։