Подавляющее большинство серверов в production работают на Linux. Умение диагностировать проблемы, настраивать систему и понимать поведение ОС -- обязательный навык для backend-разработчика.
Области применения знаний Linux
Область
Примеры задач
Отладка production
Найти причину высокого CPU, утечки памяти
Настройка серверов
Тюнинг sysctl, лимиты ОС, файловые системы
Контейнеризация
Dockerfile, cgroups, namespaces
Мониторинг
Метрики ОС, логи, системные вызовы
Сеть
Firewall, routing, DNS, проблемы соединений
Управление процессами
Основные команды
Команда
Описание
Полезные флаги
ps
Список процессов
ps aux, ps -ef
top / htop
Интерактивный монитор
htop -p PID
kill
Отправить сигнал процессу
kill -9 PID (SIGKILL)
pgrep / pkill
Поиск/kill по имени
pgrep -f php-fpm
strace
Трассировка системных вызовов
strace -p PID -e trace=network
lsof
Открытые файлы/сокеты
lsof -p PID, lsof -i :8080
Сигналы
Сигналы -- это механизм межпроцессного взаимодействия в Unix.
Сигнал
Номер
Действие
Использование
SIGHUP
1
Перечитать конфиг
Graceful reload (nginx, php-fpm)
SIGINT
2
Прервать
Ctrl+C
SIGTERM
15
Завершить (graceful)
Стандартное завершение
SIGKILL
9
Убить (нельзя перехватить)
Последнее средство
SIGUSR1
10
Пользовательский
Reopen logs (nginx)
SIGUSR2
12
Пользовательский
Graceful restart (php-fpm)
<?php
declare(strict_types=1);
/**
* Handling Unix signals in PHP CLI scripts.
* Essential for long-running workers and daemons.
*/
final class GracefulWorker
{
private bool $shouldStop = false;
public function __construct(
private readonly string $queueName,
) {
// Register signal handlers
pcntl_signal(SIGTERM, [$this, 'handleSignal']);
pcntl_signal(SIGINT, [$this, 'handleSignal']);
pcntl_signal(SIGHUP, [$this, 'handleSignal']);
}
public function handleSignal(int $signal): void
{
match ($signal) {
SIGTERM, SIGINT => $this->shouldStop = true,
SIGHUP => $this->reloadConfig(),
default => null,
};
}
/**
* Main loop with graceful shutdown support.
* Process current job, then check for signals.
*/
public function run(): void
{
while (!$this->shouldStop) {
// Check for pending signals
pcntl_signal_dispatch();
$job = $this->fetchJob();
if ($job === null) {
// No jobs available, sleep briefly
usleep(100_000); // 100ms
continue;
}
// Process the job completely before checking signals
$this->processJob($job);
// Check for signals after each job
pcntl_signal_dispatch();
}
// Cleanup before exit
$this->cleanup();
}
private function reloadConfig(): void
{
// Re-read configuration without restart
}
private function fetchJob(): ?array
{
// Fetch from queue
return null;
}
private function processJob(array $job): void
{
// Process job
}
private function cleanup(): void
{
// Release resources, close connections
}
}
package worker
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"time"
)
// GracefulWorker handles Unix signals for graceful shutdown.
// Go uses os/signal and context for signal handling — no manual dispatch needed.
type GracefulWorker struct {
queueName string
}
func NewGracefulWorker(queueName string) *GracefulWorker {
return &GracefulWorker{queueName: queueName}
}
// Run starts the worker loop with graceful shutdown via context cancellation.
func (w *GracefulWorker) Run(ctx context.Context) {
// Create context that cancels on SIGTERM/SIGINT
ctx, stop := signal.NotifyContext(ctx, syscall.SIGTERM, syscall.SIGINT)
defer stop()
// SIGHUP for config reload (handled separately)
sighup := make(chan os.Signal, 1)
signal.Notify(sighup, syscall.SIGHUP)
for {
select {
case <-ctx.Done():
// SIGTERM or SIGINT received — graceful shutdown
log.Println("Shutting down gracefully...")
w.cleanup()
return
case <-sighup:
w.reloadConfig()
default:
job, ok := w.fetchJob(ctx)
if !ok {
// No jobs available, sleep briefly
time.Sleep(100 * time.Millisecond)
continue
}
// Process the job completely before checking signals
w.processJob(ctx, job)
}
}
}
func (w *GracefulWorker) reloadConfig() { /* re-read config */ }
func (w *GracefulWorker) fetchJob(ctx context.Context) (any, bool) { return nil, false }
func (w *GracefulWorker) processJob(ctx context.Context, job any) { /* process job */ }
func (w *GracefulWorker) cleanup() { /* release resources */ }
using System.Runtime.InteropServices;
namespace Foundations.Linux;
/// <summary>
/// Handling Unix signals in .NET worker processes.
/// Essential for long-running workers and daemons.
/// </summary>
public sealed class GracefulWorker : IDisposable
{
private readonly string _queueName;
private readonly CancellationTokenSource _stopSource = new();
private readonly List<PosixSignalRegistration> _registrations = [];
public GracefulWorker(string queueName)
{
_queueName = queueName;
// PosixSignalRegistration delivers signals to a callback,
// so no manual dispatch loop is needed
_registrations.Add(PosixSignalRegistration.Create(PosixSignal.SIGTERM, HandleStop));
_registrations.Add(PosixSignalRegistration.Create(PosixSignal.SIGINT, HandleStop));
_registrations.Add(PosixSignalRegistration.Create(PosixSignal.SIGHUP, HandleReload));
}
private void HandleStop(PosixSignalContext context)
{
// Suppress the default terminate action so cleanup can run
context.Cancel = true;
_stopSource.Cancel();
}
private void HandleReload(PosixSignalContext context)
{
context.Cancel = true;
ReloadConfig();
}
/// <summary>
/// Main loop with graceful shutdown support.
/// Process current job, then check for cancellation.
/// </summary>
public async Task RunAsync()
{
var token = _stopSource.Token;
while (!token.IsCancellationRequested)
{
var job = await FetchJobAsync(token);
if (job is null)
{
// No jobs available, sleep briefly
await Task.Delay(TimeSpan.FromMilliseconds(100), token)
.ContinueWith(_ => { }, TaskScheduler.Default);
continue;
}
// Process the job completely - do not pass the token here,
// an in-flight job must finish before shutdown
await ProcessJobAsync(job);
}
// Cleanup before exit
await CleanupAsync();
}
private void ReloadConfig()
{
// Re-read configuration without restart
}
private Task<object?> FetchJobAsync(CancellationToken ct) => Task.FromResult<object?>(null);
private Task ProcessJobAsync(object job) => Task.CompletedTask;
private Task CleanupAsync() => Task.CompletedTask;
public void Dispose()
{
foreach (var registration in _registrations)
{
registration.Dispose();
}
_stopSource.Dispose();
}
}
from __future__ import annotations
import signal
import time
from types import FrameType
from typing import Any
class GracefulWorker:
"""Handle Unix signals in Python workers and daemons.
CPython delivers signals only to the main thread, and only between
bytecode instructions, so the handler must stay tiny - flip a flag
and let the main loop react.
"""
def __init__(self, queue_name: str) -> None:
self._queue_name = queue_name
self._should_stop = False
# Register signal handlers
signal.signal(signal.SIGTERM, self._handle_signal)
signal.signal(signal.SIGINT, self._handle_signal)
signal.signal(signal.SIGHUP, self._handle_signal)
def _handle_signal(self, signum: int, frame: FrameType | None) -> None:
if signum in (signal.SIGTERM, signal.SIGINT):
self._should_stop = True
elif signum == signal.SIGHUP:
self._reload_config()
def run(self) -> None:
"""Main loop with graceful shutdown support.
Process the current job completely, then react to the stop flag.
"""
while not self._should_stop:
job = self._fetch_job()
if job is None:
# No jobs available, sleep briefly
time.sleep(0.1)
continue
# Process the job completely before reacting to signals
self._process_job(job)
# Cleanup before exit
self._cleanup()
def _reload_config(self) -> None:
"""Re-read configuration without restart."""
def _fetch_job(self) -> dict[str, Any] | None:
"""Fetch from queue."""
return None
def _process_job(self, job: dict[str, Any]) -> None:
"""Process job."""
def _cleanup(self) -> None:
"""Release resources, close connections."""
Приложение (PHP, Nginx)
|
Socket API (userspace)
|
TCP/UDP (transport layer)
|
IP (network layer)
|
Ethernet / Wi-Fi (link layer)
|
NIC (физический уровень)
Ключевые сетевые команды
Команда
Описание
Примеры
ss -tlnp
Активные TCP-соединения
Кто слушает порт
ip addr
IP-адреса интерфейсов
Сетевая конфигурация
ip route
Таблица маршрутизации
Проблемы с роутингом
tcpdump
Перехват пакетов
tcpdump -i eth0 port 80
curl -v
HTTP-запросы с деталями
Отладка API
dig / nslookup
DNS-запросы
Проблемы с DNS
mtr
Traceroute + ping
Диагностика сети
Тюнинг сетевого стека
Для высоконагруженных серверов нужно тюнить параметры ядра:
Параметр
Описание
Типичное значение
net.core.somaxconn
Макс. очередь соединений
65535
net.ipv4.tcp_max_syn_backlog
SYN backlog
65535
net.core.netdev_max_backlog
Очередь пакетов на NIC
50000
net.ipv4.tcp_tw_reuse
Повторное использование TIME_WAIT
1
net.ipv4.ip_local_port_range
Диапазон портов
1024 65535
PHP: взаимодействие с ОС
exec, shell_exec, proc_open
PHP предоставляет несколько способов взаимодействия с ОС. Важно понимать различия и выбирать правильный.
<?php
declare(strict_types=1);
/**
* Safe OS command execution from PHP.
* NEVER use user input directly in commands - always escape.
*/
final class SystemCommand
{
/**
* Execute a command and capture output safely.
* Uses proc_open for full control over stdin/stdout/stderr.
*
* @return array{exit_code: int, stdout: string, stderr: string}
*/
public static function execute(
string $command,
?float $timeoutSeconds = 30.0,
?string $workingDir = null,
): array {
$descriptors = [
0 => ['pipe', 'r'], // stdin
1 => ['pipe', 'w'], // stdout
2 => ['pipe', 'w'], // stderr
];
$process = proc_open(
$command,
$descriptors,
$pipes,
$workingDir,
);
if (!is_resource($process)) {
throw new \RuntimeException("Failed to execute: $command");
}
// Close stdin - we don't need to write
fclose($pipes[0]);
// Set non-blocking mode for timeout support
stream_set_blocking($pipes[1], false);
stream_set_blocking($pipes[2], false);
$stdout = '';
$stderr = '';
$startTime = microtime(true);
while (true) {
$status = proc_get_status($process);
if (!$status['running']) {
// Process finished - read remaining output
$stdout .= stream_get_contents($pipes[1]);
$stderr .= stream_get_contents($pipes[2]);
break;
}
// Check timeout
$elapsed = microtime(true) - $startTime;
if ($timeoutSeconds !== null && $elapsed > $timeoutSeconds) {
proc_terminate($process, SIGKILL);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
throw new \RuntimeException(
"Command timed out after {$timeoutSeconds}s: $command"
);
}
// Read available output
$stdout .= fread($pipes[1], 8192) ?: '';
$stderr .= fread($pipes[2], 8192) ?: '';
usleep(10_000); // 10ms polling interval
}
fclose($pipes[1]);
fclose($pipes[2]);
$exitCode = proc_close($process);
return [
'exit_code' => $exitCode,
'stdout' => trim($stdout),
'stderr' => trim($stderr),
];
}
/**
* Get system information using OS commands.
*
* @return array<string, mixed>
*/
public static function getSystemInfo(): array
{
$cpuInfo = self::execute('nproc');
$memInfo = self::execute("free -m | awk '/Mem:/ {print $2, $3, $4}'");
$diskInfo = self::execute("df -h / | tail -1 | awk '{print $2, $3, $5}'");
$loadAvg = self::execute('cat /proc/loadavg');
return [
'cpu_cores' => (int) $cpuInfo['stdout'],
'memory_raw' => $memInfo['stdout'],
'disk_raw' => $diskInfo['stdout'],
'load_average' => $loadAvg['stdout'],
'php_pid' => getmypid(),
'php_uid' => posix_getuid(),
'hostname' => gethostname(),
];
}
}
package system
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"strings"
"time"
)
// CommandResult holds the result of an executed command.
type CommandResult struct {
ExitCode int
Stdout string
Stderr string
}
// Execute runs a command with timeout and captures output safely.
func Execute(ctx context.Context, command string, timeout time.Duration) (CommandResult, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
cmd := exec.CommandContext(ctx, "sh", "-c", command)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
result := CommandResult{
Stdout: strings.TrimSpace(stdout.String()),
Stderr: strings.TrimSpace(stderr.String()),
}
if ctx.Err() == context.DeadlineExceeded {
return result, fmt.Errorf("command timed out after %v: %s", timeout, command)
}
if exitErr, ok := err.(*exec.ExitError); ok {
result.ExitCode = exitErr.ExitCode()
return result, nil
}
if err != nil {
return result, fmt.Errorf("execute %q: %w", command, err)
}
return result, nil
}
// SystemInfo holds basic system information.
type SystemInfo struct {
Hostname string
PID int
UID int
}
// GetSystemInfo returns current process and system information.
func GetSystemInfo() SystemInfo {
hostname, _ := os.Hostname()
return SystemInfo{
Hostname: hostname,
PID: os.Getpid(),
UID: os.Getuid(),
}
}
using System.Diagnostics;
namespace Foundations.Linux;
public readonly record struct CommandResult(int ExitCode, string Stdout, string Stderr);
public readonly record struct SystemInfo(string Hostname, int Pid, int CpuCores);
/// <summary>
/// Safe OS command execution.
/// NEVER concatenate user input into a shell string - pass arguments
/// through ArgumentList so the runtime escapes them.
/// </summary>
public static class SystemCommand
{
/// <summary>
/// Execute a command with a timeout and capture stdout/stderr.
/// </summary>
public static async Task<CommandResult> ExecuteAsync(
string fileName,
IReadOnlyList<string> arguments,
TimeSpan? timeout = null,
string? workingDirectory = null,
CancellationToken ct = default)
{
var startInfo = new ProcessStartInfo(fileName)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = false,
UseShellExecute = false,
WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory,
};
foreach (var argument in arguments)
{
startInfo.ArgumentList.Add(argument);
}
using var process = Process.Start(startInfo)
?? throw new InvalidOperationException($"Failed to execute: {fileName}");
using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeoutSource.CancelAfter(timeout ?? TimeSpan.FromSeconds(30));
var stdoutTask = process.StandardOutput.ReadToEndAsync(timeoutSource.Token);
var stderrTask = process.StandardError.ReadToEndAsync(timeoutSource.Token);
try
{
await process.WaitForExitAsync(timeoutSource.Token);
}
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
{
process.Kill(entireProcessTree: true);
throw new TimeoutException($"Command timed out: {fileName}");
}
return new CommandResult(
process.ExitCode,
(await stdoutTask).Trim(),
(await stderrTask).Trim());
}
/// <summary>
/// Get system information. Prefer runtime APIs over shelling out -
/// they avoid a process spawn and work across platforms.
/// </summary>
public static SystemInfo GetSystemInfo() => new(
Hostname: Environment.MachineName,
Pid: Environment.ProcessId,
CpuCores: Environment.ProcessorCount);
}
from __future__ import annotations
import os
import socket
import subprocess
from typing import NamedTuple
class CommandResult(NamedTuple):
exit_code: int
stdout: str
stderr: str
class SystemInfo(NamedTuple):
hostname: str
pid: int
uid: int
cpu_cores: int
def execute(
command: list[str],
timeout_seconds: float = 30.0,
working_dir: str | None = None,
) -> CommandResult:
"""Execute a command and capture output safely.
Pass the command as a list and keep shell=False - that way the args
go straight to execve and never through shell word splitting.
"""
try:
completed = subprocess.run(
command,
capture_output=True,
text=True,
timeout=timeout_seconds,
cwd=working_dir,
check=False,
)
except subprocess.TimeoutExpired as exc:
raise RuntimeError(
f"Command timed out after {timeout_seconds}s: {' '.join(command)}"
) from exc
return CommandResult(
exit_code=completed.returncode,
stdout=completed.stdout.strip(),
stderr=completed.stderr.strip(),
)
def get_system_info() -> SystemInfo:
"""Return current process and system information.
Prefer the os module over shelling out - no process spawn required.
"""
return SystemInfo(
hostname=socket.gethostname(),
pid=os.getpid(),
uid=os.getuid(),
cpu_cores=os.cpu_count() or 1,
)
### Мониторинг PHP-процессов из ОС
Метрика
Как получить
Что значит
PHP-FPM workers
ps aux | grep php-fpm
Количество активных воркеров
Memory per worker
/proc/{pid}/status -> VmRSS
Реальное потребление памяти
Open file descriptors
ls /proc/{pid}/fd | wc -l
Утечки соединений
CPU time
/proc/{pid}/stat
Время CPU на процесс
<?php
declare(strict_types=1);
/**
* Reading process information from /proc filesystem.
* Works only on Linux.
*/
final class ProcessInfo
{
/**
* Get memory usage of current process from /proc.
* More accurate than memory_get_usage() for total process memory.
*
* @return array{vm_size_kb: int, vm_rss_kb: int, vm_peak_kb: int}
*/
public static function getMemoryFromProc(): array
{
$pid = getmypid();
$statusFile = "/proc/{$pid}/status";
if (!file_exists($statusFile)) {
throw new \RuntimeException('Not running on Linux or /proc not available');
}
$content = file_get_contents($statusFile);
$result = [];
// Parse key metrics from /proc/PID/status
if (preg_match('/VmSize:\s+(\d+)\s+kB/', $content, $m)) {
$result['vm_size_kb'] = (int) $m[1];
}
if (preg_match('/VmRSS:\s+(\d+)\s+kB/', $content, $m)) {
$result['vm_rss_kb'] = (int) $m[1]; // Resident Set Size
}
if (preg_match('/VmPeak:\s+(\d+)\s+kB/', $content, $m)) {
$result['vm_peak_kb'] = (int) $m[1];
}
return $result;
}
/**
* Count open file descriptors for current process.
* High count may indicate connection leaks.
*/
public static function getOpenFileDescriptors(): int
{
$pid = getmypid();
$fdDir = "/proc/{$pid}/fd";
if (!is_dir($fdDir)) {
return -1;
}
return count(scandir($fdDir)) - 2; // subtract . and ..
}
}
package procinfo
import (
"fmt"
"os"
"runtime"
"strings"
)
// MemoryInfo holds process memory metrics from /proc.
type MemoryInfo struct {
VmSizeKB int64
VmRSSKB int64
VmPeakKB int64
}
// GetMemoryFromProc reads memory info from /proc/PID/status on Linux.
func GetMemoryFromProc() (MemoryInfo, error) {
pid := os.Getpid()
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid))
if err != nil {
return MemoryInfo{}, fmt.Errorf("read proc status: %w", err)
}
var info MemoryInfo
for _, line := range strings.Split(string(data), "\n") {
var val int64
switch {
case strings.HasPrefix(line, "VmSize:"):
fmt.Sscanf(line, "VmSize: %d kB", &val)
info.VmSizeKB = val
case strings.HasPrefix(line, "VmRSS:"):
fmt.Sscanf(line, "VmRSS: %d kB", &val)
info.VmRSSKB = val
case strings.HasPrefix(line, "VmPeak:"):
fmt.Sscanf(line, "VmPeak: %d kB", &val)
info.VmPeakKB = val
}
}
return info, nil
}
// GetMemoryStats returns Go runtime memory statistics.
// More portable than /proc — works on all platforms.
func GetMemoryStats() runtime.MemStats {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return m
}
// GetOpenFileDescriptors counts open file descriptors on Linux.
func GetOpenFileDescriptors() (int, error) {
pid := os.Getpid()
entries, err := os.ReadDir(fmt.Sprintf("/proc/%d/fd", pid))
if err != nil {
return -1, fmt.Errorf("read fd dir: %w", err)
}
return len(entries), nil
}
## Практические сценарии
Диагностика высокого CPU
1. top/htop -- найти процесс с высоким CPU
2. strace -p PID -- что делает процесс (syscalls)
3. perf top -p PID -- где тратится время (функции)
4. PHP: Xdebug/Xhprof profiling
Диагностика нехватки памяти
1. free -h -- общее состояние RAM
2. ps aux --sort=-rss -- процессы по потреблению RAM
3. cat /proc/meminfo -- детальная информация
4. dmesg | grep -i oom -- были ли OOM kills
Диагностика проблем с диском
1. df -h -- свободное место
2. iostat -x 1 -- I/O нагрузка и await
3. iotop -- процессы, нагружающие диск
4. lsof +D /var/log -- кто пишет в каталог
Совет: Научитесь читать вывод top и iostat. На production-серверах это ваши главные инструменты диагностики. Значение %wa (I/O wait) в top показывает, сколько времени CPU ждёт диск -- если больше 20%, у вас I/O bottleneck.
Выводы
Linux -- основная ОС для серверов, знание его инструментов обязательно
Сигналы позволяют graceful shutdown и reload без потери данных
/proc и /sys -- источники детальной информации о системе
Тюнинг sysctl критичен для высоконагруженных серверов
PHP может взаимодействовать с ОС через proc_open, pcntl-функции и чтение /proc
Умение диагностировать проблемы CPU, памяти, диска и сети -- ключевой навык