Перейти к основному содержимому

Сетевые аномалии и системные процессы

Разработчику Архитектору Инженеру

Сетевые аномалии и системные процессы

Системное администрирование обеспечивает стабильность базовой операционной среды, на которой функционируют все прикладные сервисы. Операционная система, сетевой стек, планировщик задач и файловая система образуют фундамент, от надёжности которого зависит работоспособность вышестоящих компонентов. Скрытые дефекты на этом уровне проявляются непредсказуемо и затрагивают множество приложений одновременно.

Сетевая аномалия — отклонение в работе сетевых протоколов и служб, нарушающее штатное взаимодействие между компонентами распределённой системы.

Системный процесс — фоновая задача, выполняемая операционной системой или пользовательскими планировщиками для обеспечения работы сервисов и выполнения периодических операций.

Системный администратор рассматривает инфраструктурные проблемы как результат взаимодействия множества независимых компонентов. Каждый компонент имеет собственные часы, собственные таблицы маршрутизации, собственные ограничения ресурсов. Слаженная работа системы достигается через тщательную настройку и постоянный мониторинг каждого элемента.


Смещение времени и синхронизация часов

Смещение времени — расхождение показаний системных часов на разных узлах распределённой системы, приводящее к нарушению логики приложений, зависящих от временных меток.

Каждый физический сервер оснащён аппаратным генератором тактовых импульсов на основе кварцевого резонатора. Кристаллы имеют производственный разброс характеристик, температурную зависимость частоты и эффект старения. Разница хода в несколько миллисекунд за секунду накапливается до секунд за сутки и до минут за месяц.

Последствия временного дрейфа

Современные распределённые системы критически зависят от точности времени:

Аутентификация и токены. JWT-токены содержат временные метки iat (issued at) и exp (expiration). Сервис валидации сравнивает эти метки со своими часами. Расхождение приводит к отклонению валидных токенов или принятию просроченных.

TTL и кэширование. Записи в кэшах (Redis, Memcached) и DNS имеют время жизни. Рассинхронизация часов вызывает преждевременное удаление актуальных данных или длительное хранение устаревших.

Корреляция событий. Логи с разных серверов объединяются по временным меткам для восстановления последовательности событий. Дрейф часов меняет порядок событий и затрудняет диагностику инцидентов.

Выборы лидера. Алгоритмы консенсуса (Raft, Paxos) используют таймауты для определения живых узлов и инициирования перевыборов. Нестабильные часы вызывают бесконечные циклы перевыборов.

Идемпотентность. Ключи идемпотентности часто включают временное окно. Расхождение часов между сервисами делает ключи неэффективными.

Базы данных. Временные метки записей используются для репликации, разрешения конфликтов и аудита. Рассинхронизация нарушает порядок применения изменений.

Протокол NTP и его реализация

NTP (Network Time Protocol) — протокол синхронизации системных часов через сеть с эталонными источниками времени. Протокол компенсирует сетевые задержки и обеспечивает точность в пределах миллисекунд при стабильном сетевом соединении.

Современные дистрибутивы Linux используют две основные реализации NTP:

Chrony — современная реализация, оптимизированная для нестабильных сетей и виртуальных машин:

# /etc/chrony.conf
# Эталонные серверы времени
server 0.pool.ntp.org iburst
server 1.pool.ntp.org iburst
server 2.pool.ntp.org iburst
server 3.pool.ntp.org iburst

# Локальные часы как резервный источник
local stratum 10

# Ограничение максимальной коррекции за один шаг
maxchange 1000 0 0

# Принудительная синхронизация при большом начальном расхождении
makestep 1.0 3

# Директория для хранения измерений
logdir /var/log/chrony

# Логирование статистики
log measurements statistics tracking

# Разрешение запросов от локальной сети
allow 10.0.0.0/8

Разбор параметров конфигурации:

  • server задаёт адрес NTP-сервера для синхронизации.
  • iburst ускоряет начальную синхронизацию серией из восьми быстрых запросов.
  • local stratum определяет уровень локальных часов как резервного источника при недоступности внешних серверов.
  • maxchange ограничивает разовую коррекцию времени для предотвращения резких скачков.
  • makestep разрешает мгновенную коррекцию при первых трёх измерениях с расхождением более одной секунды.
  • allow разрешает другим узлам сети использовать данный сервер как источник времени.

systemd-timesyncd — легковесный SNTP-клиент, встроенный в systemd:

# /etc/systemd/timesyncd.conf
[Time]
NTP=0.pool.ntp.org 1.pool.ntp.org 2.pool.ntp.org 3.pool.ntp.org
FallbackNTP=ntp.ubuntu.com
RootDistanceMaxSec=5
PollIntervalMinSec=32
PollIntervalMaxSec=2048

Монотонные часы в приложениях

Монотонные часы — источник времени, гарантирующий постоянный ход вперёд независимо от корректировок системных часов через NTP.

Системные часы (CLOCK_REALTIME) могут идти назад при синхронизации с NTP-сервером, показывающим более раннее время. Монотонные часы (CLOCK_MONOTONIC) никогда не идут назад и идеально подходят для измерения интервалов времени.

import time

# Системные часы могут идти назад при коррекции NTP
wall_time_start = time.time()
time.sleep(1)
wall_time_end = time.time()
# wall_time_end может быть меньше wall_time_start!

# Монотонные часы гарантируют постоянный ход
monotonic_start = time.monotonic()
time.sleep(1)
monotonic_end = time.monotonic()
# monotonic_end всегда больше monotonic_start

duration = monotonic_end - monotonic_start

Разные языки программирования предоставляют доступ к монотонным часам:

ЯзыкФункция монотонных часовФункция системных часов
Pythontime.monotonic()time.time()
JavaSystem.nanoTime()System.currentTimeMillis()
C#Stopwatch.GetTimestamp()DateTime.UtcNow
Gotime.Since(start)time.Now()
JavaScriptperformance.now()Date.now()
C/C++clock_gettime(CLOCK_MONOTONIC)clock_gettime(CLOCK_REALTIME)

Логические часы Лэмпорта

Логические часы Лэмпорта — механизм установления причинно-следственного порядка событий в распределённой системе без использования физических часов.

Каждый узел поддерживает счётчик, увеличивающийся при каждом локальном событии. При отправке сообщения узел передаёт текущее значение счётчика. Получатель устанавливает свой счётчик в максимум из собственного значения и полученного, увеличенный на единицу.

class LamportClock:
"""Реализация логических часов Лэмпорта."""

def __init__(self):
self.time = 0
self.node_id = None

def tick(self) -> int:
"""Локальное событие."""
self.time += 1
return self.time

def send(self) -> tuple:
"""Отправка сообщения."""
self.time += 1
return (self.time, self.node_id)

def receive(self, message_time: int) -> int:
"""Получение сообщения."""
self.time = max(self.time, message_time) + 1
return self.time

def happened_before(self, event_a: tuple, event_b: tuple) -> bool:
"""Проверка причинно-следственной связи."""
time_a, node_a = event_a
time_b, node_b = event_b

if time_a < time_b:
return True
if time_a == time_b and node_a < node_b:
return True
return False

Векторные часы расширяют идею Лэмпорта, отслеживая знания каждого узла о состоянии других узлов. Это позволяет точно определять параллельные события, для которых причинно-следственная связь отсутствует.


Сбои DNS-резолвинга

Сбой DNS-резолвинга — невозможность преобразовать доменное имя в IP-адрес или получение устаревшего адреса из-за кэширования.

DNS-система обеспечивает преобразование человекочитаемых имён в сетевые адреса. Распределённая природа DNS, иерархическая структура и агрессивное кэширование создают уникальные проблемы для приложений.

Архитектура DNS и точки отказа

DNS-запрос проходит через несколько уровней:

  1. Локальный кэш приложения или библиотеки.
  2. Кэш операционной системы (systemd-resolved, dnsmasq).
  3. Локальный DNS-резолвер провайдера или корпоративной сети.
  4. Рекурсивные DNS-серверы.
  5. Авторитетные DNS-серверы домена.
  6. Корневые DNS-серверы.

Каждый уровень имеет собственные таймауты, политики кэширования и механизмы обработки ошибок. Проблема на любом уровне влияет на все приложения, использующие этот путь разрешения имён.

Источники проблем

Негативное кэширование. Ошибки разрешения имён кэшируются на срок, указанный в поле minimum SOA-записи зоны (обычно от 5 минут до нескольких часов). Временная недоступность DNS-сервера приводит к долгосрочным сбоям даже после восстановления сервиса.

Долгий TTL. Записи с большим временем жизни (TTL) кэшируются на всех уровнях, делая невозможным быстрое переключение трафика при аварийном переводе на резервный дата-центр.

Кэширование в приложении. HTTP-клиенты и пулы соединений часто резолвят имя один раз при создании и продолжают использовать полученный IP-адрес бесконечно, игнорируя изменения в DNS.

Отсутствие резервных серверов. Приложение или система используют единственный DNS-сервер, недоступность которого останавливает все сетевые операции.

IPv4/IPv6 dual-stack. Современные резолверы возвращают оба типа адресов. Приложение пытается подключиться к недоступному IPv6-адресу, тратя время на таймаут перед переключением на IPv4.

Round-robin записи. Домен имеет несколько A-записей для балансировки нагрузки. Клиент получает все адреса и должен самостоятельно реализовать failover.

Стратегии устойчивого резолвинга

Периодическое обновление адресов в пулах соединений:

// Apache HttpClient с периодическим обновлением DNS
PoolingHttpClientConnectionManager connectionManager =
new PoolingHttpClientConnectionManager(
30, TimeUnit.SECONDS // Время жизни соединения
);

// Настройка eviction для устаревших соединений
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.evictExpiredConnections()
.evictIdleConnections(60, TimeUnit.SECONDS)
.setRetryHandler((exception, executionCount, context) -> {
// Повторная попытка при UnknownHostException
if (exception instanceof UnknownHostException && executionCount < 3) {
// Принудительное обновление DNS-кэша
java.security.Security.setProperty(
"networkaddress.cache.ttl", "0"
);
return true;
}
return false;
})
.build();

Резервные DNS-резолверы на уровне приложения:

import dns.resolver
import dns.exception
from typing import List, Optional

class ResilientResolver:
"""Устойчивый DNS-резолвер с несколькими источниками."""

def __init__(self):
self.resolvers = []

# Основной резолвер - системный
primary = dns.resolver.Resolver()
primary.timeout = 2.0
primary.lifetime = 4.0
self.resolvers.append(primary)

# Резервные публичные DNS
for nameservers in [
['8.8.8.8', '8.8.4.4'], # Google Public DNS
['1.1.1.1', '1.0.0.1'], # Cloudflare DNS
['9.9.9.9', '149.112.112.112'] # Quad9 DNS
]:
resolver = dns.resolver.Resolver(configure=False)
resolver.nameservers = nameservers
resolver.timeout = 2.0
resolver.lifetime = 4.0
self.resolvers.append(resolver)

def resolve(self, hostname: str, rdtype: str = 'A') -> List[str]:
"""Разрешение имени с попытками через несколько серверов."""
last_exception = None

for resolver in self.resolvers:
try:
answers = resolver.resolve(hostname, rdtype)
return [rdata.to_text() for rdata in answers]
except dns.exception.DNSException as e:
last_exception = e
continue

raise ResolutionFailed(
f"Не удалось разрешить {hostname}: {last_exception}"
)

def resolve_with_cache(
self,
hostname: str,
cache_ttl: int = 300
) -> List[str]:
"""Разрешение с локальным кэшированием."""
cache_key = f"{hostname}:{self._cache_namespace}"

cached = self._cache.get(cache_key)
if cached and not cached.is_expired():
return cached.addresses

addresses = self.resolve(hostname)
self._cache.set(cache_key, addresses, ttl=cache_ttl)
return addresses

Предварительное разрешение для критических зависимостей:

package resolver

import (
"context"
"net"
"sync"
"time"
)

type CachedResolver struct {
cache map[string]*cacheEntry
mutex sync.RWMutex
resolver *net.Resolver
refreshInterval time.Duration
}

type cacheEntry struct {
addresses []string
expiresAt time.Time
refreshing bool
}

func NewCachedResolver(refreshInterval time.Duration) *CachedResolver {
return &CachedResolver{
cache: make(map[string]*cacheEntry),
resolver: &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{Timeout: 2 * time.Second}
return d.DialContext(ctx, network, address)
},
},
refreshInterval: refreshInterval,
}
}

func (r *CachedResolver) Resolve(ctx context.Context, host string) ([]string, error) {
r.mutex.RLock()
entry, exists := r.cache[host]
r.mutex.RUnlock()

if exists && time.Now().Before(entry.expiresAt) {
// Кэш актуален
return entry.addresses, nil
}

if exists && entry.refreshing {
// Другая горутина уже обновляет
return entry.addresses, nil
}

// Разрешение имени
addrs, err := r.resolver.LookupHost(ctx, host)
if err != nil {
if exists {
// Возврат устаревших данных при ошибке
return entry.addresses, nil
}
return nil, err
}

// Обновление кэша
r.mutex.Lock()
r.cache[host] = &cacheEntry{
addresses: addrs,
expiresAt: time.Now().Add(r.refreshInterval),
refreshing: false,
}
r.mutex.Unlock()

// Планирование фонового обновления
go r.backgroundRefresh(host)

return addrs, nil
}

func (r *CachedResolver) backgroundRefresh(host string) {
ticker := time.NewTicker(r.refreshInterval / 2)
defer ticker.Stop()

for range ticker.C {
ctx, cancel := context.WithTimeout(
context.Background(),
5*time.Second,
)

addrs, err := r.resolver.LookupHost(ctx, host)
cancel()

if err == nil {
r.mutex.Lock()
r.cache[host] = &cacheEntry{
addresses: addrs,
expiresAt: time.Now().Add(r.refreshInterval),
refreshing: false,
}
r.mutex.Unlock()
}
}
}

Мониторинг DNS-резолвинга

Системный администратор настраивает непрерывный мониторинг доступности DNS:

#!/bin/bash
# Мониторинг DNS-резолвинга

DOMAINS=(
"api.company.com"
"database.internal"
"cache.internal"
"external-payment-gateway.com"
)

DNS_SERVERS=(
"8.8.8.8"
"1.1.1.1"
"local-dns-01"
"local-dns-02"
)

for domain in "${DOMAINS[@]}"; do
for server in "${DNS_SERVERS[@]}"; do
start_time=$(date +%s%N)

if dig @"$server" "$domain" +short +time=2 +tries=1 > /dev/null 2>&1; then
end_time=$(date +%s%N)
duration_ms=$(( (end_time - start_time) / 1000000 ))

echo "dns_resolution_success{domain=\"$domain\",server=\"$server\"} 1"
echo "dns_resolution_duration_ms{domain=\"$domain\",server=\"$server\"} $duration_ms"
else
echo "dns_resolution_success{domain=\"$domain\",server=\"$server\"} 0"
echo "ALERT: DNS resolution failed for $domain via $server" | send_alert
fi
done
done

Зомби-процессы и управление процессами

Зомби-процесс — завершённый процесс, запись о котором сохраняется в таблице процессов до момента, пока родительский процесс не считает его код завершения.

Операционная система Linux поддерживает иерархию процессов, где каждый процесс имеет родителя. При завершении дочернего процесса ядро сохраняет минимальную информацию о нём: идентификатор процесса (PID), код завершения и статистику использования ресурсов. Эта информация предназначена для родительского процесса, который должен вызвать системный вызов wait() для её получения.

Механизм возникновения зомби

Жизненный цикл процесса включает несколько состояний:

R (Running) - процесс выполняется или ожидает выполнения
S (Sleeping) - процесс ожидает события (I/O, сигнал)
D (Disk sleep) - процесс ожидает I/O, не реагирует на сигналы
T (Stopped) - процесс остановлен сигналом
Z (Zombie) - процесс завершён, ожидает чтения статуса родителем
X (Dead) - процесс полностью удалён из таблицы

Переход в состояние зомби происходит следующим образом:

  1. Дочерний процесс вызывает exit() или получает сигнал завершения.
  2. Ядро освобождает все ресурсы процесса: память, файловые дескрипторы, сетевые сокеты.
  3. Ядро сохраняет структуру task_struct с кодом завершения и статистикой.
  4. Ядро отправляет родителю сигнал SIGCHLD.
  5. Процесс переходит в состояние Z (Zombie).
  6. Родитель вызывает wait() или waitpid() для получения статуса.
  7. Ядро удаляет структуру процесса из таблицы.

Если родитель игнорирует сигнал SIGCHLD или завершается раньше дочернего процесса, зомби остаётся в таблице до перезагрузки системы или_until_ усыновления процессом init.

Опасность массового появления зомби

Одиночный зомби-процесс занимает минимальные ресурсы — несколько сотен байт в таблице процессов. Проблемы возникают при массовом появлении зомби:

Исчерпание PID-пространства. Linux имеет ограниченное количество доступных идентификаторов процессов (обычно 32768 или 4194304). Массовые зомби занимают PID'ы, делая невозможным создание новых процессов.

Утечка файловых дескрипторов. Зомби удерживает файловые дескрипторы, открытые до момента завершения. При большом количестве зомби система исчерпывает лимит открытых файлов.

Затруднённая диагностика. Таблица процессов, заполненная зомби, усложняет поиск реально работающих процессов.

Правильная обработка дочерних процессов

Родительский процесс обязан корректно обрабатывать завершение дочерних процессов:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <errno.h>

volatile sig_atomic_t child_exited = 0;

void sigchld_handler(int signo) {
child_exited = 1;
}

void reap_children() {
int status;
pid_t pid;

// Сбор всех завершённых дочерних процессов
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
if (WIFEXITED(status)) {
printf("Процесс %d завершился с кодом %d\n",
pid, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Процесс %d убит сигналом %d\n",
pid, WTERMSIG(status));
}
}
}

int main() {
// Регистрация обработчика SIGCHLD
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);

// Создание дочерних процессов
for (int i = 0; i < 10; i++) {
pid_t pid = fork();

if (pid == 0) {
// Дочерний процесс
sleep(rand() % 5);
exit(i);
} else if (pid > 0) {
printf("Создан дочерний процесс %d\n", pid);
} else {
perror("fork failed");
exit(1);
}
}

// Основной цикл с обработкой сигналов
while (1) {
pause(); // Ожидание сигнала

if (child_exited) {
reap_children();
child_exited = 0;
}
}

return 0;
}

Разбор ключевых элементов:

  • sigaction регистрирует обработчик сигнала SIGCHLD с флагами SA_RESTART и SA_NOCLDSTOP.
  • SA_NOCLDSTOP предотвращает отправку сигнала при остановке (но не завершении) дочерних процессов.
  • waitpid с флагом WNOHANG возвращает управление немедленно, если нет завершённых процессов.
  • Цикл while необходим, так как несколько процессов могут завершиться до обработки сигнала.
  • Макросы WIFEXITED, WEXITSTATUS, WIFSIGNALED, WTERMSIG извлекают информацию о способе завершения.

Автоматическое предотвращение зомби

Двойной fork — техника, при которой дочерний процесс немедленно создаёт собственного потомка и завершается:

pid_t pid = fork();

if (pid == 0) {
// Первый дочерний процесс
pid_t grandchild = fork();

if (grandchild == 0) {
// Второй дочерний процесс (внук)
// Выполняет полезную работу
do_work();
exit(0);
} else if (grandchild > 0) {
// Первый дочерний процесс завершается
// Внук становится сиротой и усыновляется init
exit(0);
}
}

// Родитель ждёт завершения первого дочернего процесса
waitpid(pid, NULL, 0);
// Первый дочерний процесс собран, зомби нет
// Внук работает независимо, его собирает init

Игнорирование SIGCHLD указывает ядру автоматически собирать завершённые дочерние процессы:

signal(SIGCHLD, SIG_IGN);

// Все дочерние процессы автоматически собираются ядром
for (int i = 0; i < 100; i++) {
if (fork() == 0) {
do_work();
exit(0);
}
}
// Зомби не появляются

Системные супервизоры

Современные системы инициализации автоматически управляют жизненным циклом процессов:

# /etc/supervisor/conf.d/worker.conf
[program:worker]
command=/opt/app/bin/worker --config /etc/app/worker.yaml
directory=/opt/app
user=appuser
autostart=true
autorestart=true
startsecs=10
startretries=3
exitcodes=0,2
stopsignal=TERM
stopwaitsecs=30
stdout_logfile=/var/log/app/worker.stdout.log
stderr_logfile=/var/log/app/worker.stderr.log
environment=ENVIRONMENT="production"

systemd как супервизор сервисов:

# /etc/systemd/system/myapp.service
[Unit]
Description=My Application Service
After=network.target postgresql.service
Wants=postgresql.service

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/server --config /etc/myapp/config.yaml
ExecReload=/bin/kill -HUP $MAINPID

# Автоматический перезапуск при сбое
Restart=on-failure
RestartSec=10s
StartLimitInterval=60s
StartLimitBurst=3

# Ограничение ресурсов
LimitNOFILE=65536
LimitNPROC=4096
MemoryMax=4G
CPUQuota=200%

# Логирование
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp

[Install]
WantedBy=multi-user.target

Зависшие cron-задачи и оркестрация фоновых работ

Cron-задача — периодическое задание, выполняемое системным планировщиком cron в заданное время или с заданным интервалом.

Классический cron предоставляет простой механизм запуска задач по расписанию, но lacks многих возможностей, необходимых для надёжного выполнения фоновых операций в production-среде.

Ограничения классического cron

Отсутствие контроля параллелизма. Cron запускает задачу по расписанию независимо от того, завершился ли предыдущий запуск. При замедлении задачи несколько экземпляров выполняются одновременно, создавая конкуренцию за ресурсы.

Отсутствие таймаутов. Задача выполняется бесконечно до завершения или до принудительного останова. Зависшая задача потребляет ресурсы неопределённо долго.

Ограниченное логирование. Вывод задач отправляется по email пользователю или теряется. Отсутствует централизованный сбор логов и метрик выполнения.

Отсутствие retry-механизмов. Сбой задачи требует ручного вмешательства для повторного запуска.

Сложность мониторинга. Отсутствуют встроенные механизмы оповещения о сбоях или превышении времени выполнения.

Типичные проблемы cron-задач

Накопление зависших экземпляров. Задача, выполняющаяся дольше интервала запуска, порождает множество параллельных процессов:

# crontab - выполнение каждые 5 минут
*/5 * * * * /opt/app/scripts/sync_data.sh

# Если sync_data.sh выполняется 15 минут,
# через час будет 12 параллельных экземпляров

Конкуренция за ресурсы. Параллельные экземпляры задачи обращаются к одним файлам, базам данных или внешним API, вызывая взаимные блокировки и ошибки.

Утечка ресурсов. Незавершённые задачи удерживают файловые дескрипторы, соединения с базами данных, временные файлы.

Невозможность graceful shutdown. При остановке системы cron-задачи получают сигнал SIGTERM без возможности корректного завершения работы.

Современные системы оркестрации задач

Celery для Python-приложений:

from celery import Celery
from celery.schedules import crontab
from datetime import timedelta

app = Celery(
'tasks',
broker='redis://localhost:6379/0',
backend='redis://localhost:6379/1'
)

# Конфигурация
app.conf.update(
# Таймауты выполнения
task_time_limit=3600, # Максимум 1 час
task_soft_time_limit=3000, # Предупреждение через 50 минут

# Повторные попытки
task_acks_late=True,
worker_prefetch_multiplier=1,

# Расписание
beat_schedule={
'sync-catalog-every-hour': {
'task': 'tasks.sync_catalog',
'schedule': crontab(minute=0), # Каждый час в :00
'options': {'expires': 3000} # Задача истекает через 50 минут
},
'cleanup-temp-files-daily': {
'task': 'tasks.cleanup_temp_files',
'schedule': crontab(hour=3, minute=0), # Ежедневно в 03:00
},
'generate-reports-weekly': {
'task': 'tasks.generate_weekly_reports',
'schedule': crontab(day_of_week='monday', hour=6, minute=0),
},
},

# Результат выполнения
result_expires=timedelta(days=7),
)

@app.task(
bind=True,
max_retries=3,
default_retry_delay=60,
autoretry_for=(ConnectionError, TimeoutError),
retry_backoff=True,
retry_backoff_max=600,
retry_jitter=True
)
def sync_catalog(self):
"""Синхронизация каталога с внешним API."""
try:
# Проверка единственного экземпляра через Redis
lock_key = f"lock:sync_catalog"
lock = self.app.backend.client.lock(lock_key, timeout=3500)

if not lock.acquire(blocking=False):
self.retry(countdown=300)
return

try:
external_api = ExternalCatalogAPI()
items = external_api.fetch_all_items()

processed = 0
for batch in chunked(items, 100):
process_batch(batch)
processed += len(batch)

# Обновление прогресса
self.update_state(
state='PROGRESS',
meta={
'processed': processed,
'total': len(items),
'percent': processed * 100 // len(items)
}
)

return {'status': 'success', 'processed': processed}
finally:
lock.release()

except Exception as exc:
# Автоматический перезапуск с экспоненциальной задержкой
raise self.retry(exc=exc)

Kubernetes CronJob для контейнерных сред:

apiVersion: batch/v1
kind: CronJob
metadata:
name: data-sync
namespace: production
spec:
schedule: "0 */2 * * *" # Каждые 2 часа
concurrencyPolicy: Forbid # Запрет параллельных запусков
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
startingDeadlineSeconds: 600 # Дедлайн запуска 10 минут

jobTemplate:
spec:
activeDeadlineSeconds: 3600 # Максимум 1 час выполнения
backoffLimit: 3 # До 3 попыток

template:
spec:
restartPolicy: OnFailure

containers:
- name: sync
image: company/data-sync:latest
command: ["/app/sync", "--full"]

resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2000m"

env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: database-credentials
key: url

volumeMounts:
- name: config
mountPath: /etc/sync
readOnly: true

volumes:
- name: config
configMap:
name: sync-config

systemd timers как замена cron:

# /etc/systemd/system/data-sync.timer
[Unit]
Description=Run data sync every 2 hours
Requires=data-sync.service

[Timer]
OnCalendar=*-*-* 00/2:00:00
Persistent=true
RandomizedDelaySec=300
AccuracySec=1min

[Install]
WantedBy=timers.target
# /etc/systemd/system/data-sync.service
[Unit]
Description=Data synchronization service
After=network.target

[Service]
Type=oneshot
User=syncuser
WorkingDirectory=/opt/sync
ExecStart=/opt/sync/bin/sync --full

# Таймауты
TimeoutStartSec=3600
TimeoutStopSec=300

# Ограничения ресурсов
MemoryMax=2G
CPUQuota=200%
TasksMax=100

# Логирование
StandardOutput=journal
StandardError=journal
SyslogIdentifier=data-sync

# Безопасность
ProtectSystem=strict
ProtectHome=true
NoNewPrivileges=true
PrivateTmp=true

Заполнение диска и управление пространством

Заполнение диска — исчерпание свободного пространства на файловой системе, ведущее к невозможности записи логов, временных файлов, кэша и чекпоинтов.

Приложение, исчерпавшее дисковое пространство, теряет способность выполнять множество штатных операций. Запись логов прекращается, кэш перестаёт обновляться, транзакции баз данных не могут записать WAL-файлы, приложения не могут создать временные файлы.

Источники заполнения

Бесконтрольные логи. Лог-файлы, растущие без ротации, способны занять всё доступное пространство за несколько дней активной работы. Одно приложение может генерировать гигабайты логов ежедневно.

Временные файлы. Приложения создают временные файлы для обработки больших объёмов данных. Отсутствие очистки после завершения операции ведёт к накоплению мусора.

Кэши на диске. Дисковые кэши требуют явной политики вытеснения устаревших записей. Без ограничений кэш растёт бесконечно.

Дампы памяти. Автоматически создаваемые heap-dump-файлы при ошибках OutOfMemoryError занимают объём, равный размеру кучи приложения.

Core dumps. Файлы дампов памяти при падении процессов могут достигать гигабайтов.

Журналы баз данных. WAL-файлы PostgreSQL, binlog MySQL, redo log Oracle растут непрерывно и требуют периодической очистки.

Файлы репликации. Реплики баз данных накапливают сегменты журналов до момента их применения.

Система ротации логов

logrotate — стандартный инструмент Linux для управления лог-файлами:

# /etc/logrotate.d/myapp
/var/log/myapp/*.log {
# Периодичность ротации
daily

# Количество сохраняемых архивов
rotate 30

# Сжатие старых логов
compress
delaycompress

# Обработка отсутствующих файлов
missingok

# Пропуск пустых файлов
notifempty

# Создание нового файла с заданными правами
create 0640 myapp myapp

# Ротация при достижении размера
size 100M
maxsize 500M

# Общий скрипт для всех файлов
sharedscripts

# Команда после ротации
postrotate
systemctl reload myapp > /dev/null 2>&1 || true
endscript
}

# Отдельная конфигурация для access-логов
/var/log/myapp/access.log {
daily
rotate 90
compress
delaycompress
missingok
notifempty
create 0644 myapp myapp

# Специальная обработка для nginx
postrotate
[ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid`
endscript
}

# Логи с высокой интенсивностью записи
/var/log/myapp/api.log {
hourly
rotate 48
compress
delaycompress
missingok
notifempty
size 500M
create 0640 myapp myapp
sharedscripts
postrotate
/usr/bin/killall -HUP rsyslogd 2>/dev/null || true
endscript
}

Разбор параметров конфигурации:

  • daily задаёт ежедневную ротацию файлов.
  • hourly задаёт ежечасную ротацию для интенсивных логов.
  • rotate N сохраняет N архивных копий перед удалением.
  • compress включает сжатие старых логов через gzip.
  • delaycompress откладывает сжатие на один цикл, позволяя дочитать файл.
  • missingok предотвращает ошибки при отсутствии лог-файла.
  • notifempty пропускает ротацию пустых файлов.
  • size и maxsize задают пороги ротации по размеру файла.
  • create задаёт права и владельца нового файла.
  • sharedscripts выполняет postrotate один раз для всех файлов.
  • postrotate выполняет команду после ротации для переоткрытия файлов.

Мониторинг дискового пространства

Предупреждение о скором заполнении диска должно срабатывать задолго до достижения критического уровня:

#!/bin/bash
# /usr/local/bin/check_disk_space.sh

THRESHOLD_WARNING=80
THRESHOLD_CRITICAL=90
THRESHOLD_EMERGENCY=95

EXCLUDE_FS="tmpfs|devtmpfs|squashfs|overlay"

check_filesystem() {
local mount_point=$1
local usage=$2
local available=$3
local total=$4

local metric_name="disk_usage_percent{mount=\"$mount_point\"}"
echo "$metric_name $usage"

if [ $usage -ge $THRESHOLD_EMERGENCY ]; then
echo "CRITICAL: $mount_point заполнен на ${usage}% (свободно: $available)"
send_page_alert "Disk space emergency on $mount_point"
trigger_cleanup "$mount_point"
elif [ $usage -ge $THRESHOLD_CRITICAL ]; then
echo "CRITICAL: $mount_point заполнен на ${usage}%"
send_alert "Disk space critical on $mount_point"
elif [ $usage -ge $THRESHOLD_WARNING ]; then
echo "WARNING: $mount_point заполнен на ${usage}%"
send_warning "Disk space warning on $mount_point"
fi
}

# Проверка всех файловых систем
df -P | grep -vE "^Filesystem|$EXCLUDE_FS" | while read line; do
filesystem=$(echo "$line" | awk '{print $1}')
total=$(echo "$line" | awk '{print $2}')
used=$(echo "$line" | awk '{print $3}')
available=$(echo "$line" | awk '{print $4}')
usage=$(echo "$line" | awk '{print $5}' | tr -d '%')
mount_point=$(echo "$line" | awk '{print $6}')

check_filesystem "$mount_point" "$usage" "$available" "$total"
done

# Проверка inode (количество файлов)
df -iP | grep -vE "^Filesystem|$EXCLUDE_FS" | while read line; do
mount_point=$(echo "$line" | awk '{print $6}')
inode_usage=$(echo "$line" | awk '{print $5}' | tr -d '%')

if [ "$inode_usage" -ge $THRESHOLD_CRITICAL ]; then
echo "CRITICAL: Inodes exhausted on $mount_point (${inode_usage}%)"
send_alert "Inode exhaustion on $mount_point"
fi
done

Автоматическая очистка

Скрипт аварийной очистки при достижении критического порога:

#!/bin/bash
# /usr/local/bin/emergency_cleanup.sh

MOUNT_POINT=$1
LOG_FILE="/var/log/emergency_cleanup.log"

log_action() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
}

log_action "Начало аварийной очистки $MOUNT_POINT"

# 1. Очистка старых логов
log_action "Удаление логов старше 7 дней"
find "$MOUNT_POINT/var/log" -name "*.log.gz" -mtime +7 -delete
find "$MOUNT_POINT/var/log" -name "*.log.?" -mtime +3 -delete

# 2. Очистка временных файлов
log_action "Удаление временных файлов старше 1 дня"
find "$MOUNT_POINT/tmp" -type f -mtime +1 -delete
find "$MOUNT_POINT/var/tmp" -type f -mtime +1 -delete

# 3. Очистка кэша пакетного менеджера
if command -v apt-get &> /dev/null; then
log_action "Очистка apt cache"
apt-get clean
fi

if command -v yum &> /dev/null; then
log_action "Очистка yum cache"
yum clean all
fi

# 4. Удаление старых ядер (Ubuntu/Debian)
if [ -f /etc/debian_version ]; then
log_action "Удаление старых ядер"
dpkg --list | grep linux-image | awk '{ print $2 }' | \
sort -V | sed -n '/'"$(uname -r | sed "s/\([0-9.-]*\)-\([^0-9]\+\)/\1/")"'/q;p' | \
xargs sudo apt-get -y purge
fi

# 5. Очистка Docker (если установлен)
if command -v docker &> /dev/null; then
log_action "Очистка неиспользуемых Docker-ресурсов"
docker system prune -af --volumes
fi

# 6. Очистка journal-логов systemd
if command -v journalctl &> /dev/null; then
log_action "Ограничение размера journal"
journalctl --vacuum-size=500M
journalctl --vacuum-time=7d
fi

log_action "Аварийная очистка завершена"

Политики управления пространством для баз данных

PostgreSQL — управление WAL-файлами и логами:

-- Настройка архивации WAL
ALTER SYSTEM SET wal_level = 'replica';
ALTER SYSTEM SET archive_mode = 'on';
ALTER SYSTEM SET archive_command = 'test ! -f /archive/%f && cp %p /archive/%f';
ALTER SYSTEM SET max_wal_size = '4GB';
ALTER SYSTEM SET min_wal_size = '1GB';
ALTER SYSTEM SET wal_keep_size = '2GB';

-- Автоматическая очистка старых WAL после применения на репликах
ALTER SYSTEM SET wal_recycle = 'on';

-- Настройка log rotation
ALTER SYSTEM SET log_rotation_age = '1d';
ALTER SYSTEM SET log_rotation_size = '100MB';
ALTER SYSTEM SET log_truncate_on_rotation = 'on';

MySQL — управление binlog:

-- Автоматическое удаление старых binlog
SET GLOBAL expire_logs_days = 7;
SET GLOBAL binlog_expire_logs_seconds = 604800; -- 7 дней

-- Ограничение размера binlog
SET GLOBAL max_binlog_size = 104857600; -- 100MB

-- Ручная очистка
PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 7 DAY);

Диагностика системных аномалий

Выявление проблем в системных процессах требует специализированных инструментов и методологий.

Анализ процессов и ресурсов

htop — интерактивный монитор процессов:

# Запуск htop с сортировкой по использованию памяти
htop --sort-key=PERCENT_MEM

# Фильтрация процессов по имени
htop --filter="postgres"

# Отображение дерева процессов
htop --tree

pidstat — статистика процессов от sysstat:

# Мониторинг CPU всех процессов каждые 5 секунд
pidstat -u 5

# Мониторинг памяти
pidstat -r 5

# Мониторинг I/O
pidstat -d 5

# Статистика по конкретному процессу
pidstat -p 12345 1

strace — трассировка системных вызовов:

# Трассировка всех системных вызовов процесса
strace -p <pid>

# Трассировка с временными метками
strace -T -p <pid>

# Трассировка только сетевых вызовов
strace -e trace=network -p <pid>

# Трассировка файловых операций
strace -e trace=file -p <pid>

# Подсчёт системных вызовов
strace -c -p <pid>

Анализ сетевых проблем

tcpdump — захват сетевого трафика:

# Захват трафика на определённом порту
tcpdump -i eth0 port 5432 -w postgres_traffic.pcap

# Захват DNS-трафика
tcpdump -i eth0 port 53 -vv

# Захват с фильтром по хосту
tcpdump -i eth0 host api.external-service.com

# Отображение в человекочитаемом формате
tcpdump -i eth0 -A port 80

ss — анализ сокетов:

# Все TCP-соединения
ss -t -a

# Соединения с указанием процесса
ss -t -a -p

# UDP-сокеты
ss -u -a

# Статистика сокетов
ss -s

# Соединения в состоянии TIME-WAIT
ss -t -a state time-wait

# Соединения с определённым портом
ss -t -a '( dport = :443 or sport = :443 )'

mtr — комбинированный traceroute и ping:

# Непрерывный мониторинг маршрута
mtr api.external-service.com

# Отчёт с 100 пакетами
mtr --report --report-cycles 100 api.external-service.com

# Без разрешения DNS-имён
mtr -n api.external-service.com

Анализ дискового пространства

ncdu — интерактивный анализатор использования диска:

# Сканирование корневой файловой системы
ncdu /

# Сканирование с исключением директорий
ncdu --exclude /proc --exclude /sys /

# Экспорт результатов в JSON
ncdu -o disk_usage.json /

du с агрегацией:

# Топ-20 крупнейших директорий
du -h / | sort -rh | head -20

# Крупнейшие файлы
find / -type f -exec du -h {} + | sort -rh | head -20

# Использование по директориям первого уровня
du -sh /* 2>/dev/null | sort -rh

lsof для поиска открытых файлов:

# Файлы, открытые конкретным процессом
lsof -p <pid>

# Процессы, использующие конкретный файл
lsof /var/log/myapp/application.log

# Удалённые файлы, всё ещё открытые процессами
lsof +L1

# Сетевые соединения процесса
lsof -i -p <pid>

Системные журналы

journalctl для анализа логов systemd:

# Логи конкретного сервиса
journalctl -u myapp.service

# Логи за последний час
journalctl --since "1 hour ago"

# Логи с определённым приоритетом
journalctl -p err

# Логи с временными метками
journalctl -u myapp.service --output=short-precise

# Непрерывный мониторинг
journalctl -u myapp.service -f

# Логи конкретного PID
journalctl _PID=12345

# Логи за период
journalctl --since "2026-05-20" --until "2026-05-22"

Таблица системных аномалий и решений

АномалияПризнакИнструмент диагностикиРешение
Смещение времениОшибки аутентификации, некорректный порядок событийchronyc tracking, timedatectlNTP-синхронизация, монотонные часы
Сбои DNSUnknownHostException, таймауты подключенийdig, mtr, tcpdump port 53Резервные резолверы, кэширование с TTL
Зомби-процессыСостояние Z в ps, исчерпание PIDps aux, top, /proc/[pid]/statusОбработка SIGCHLD, systemd-супервизоры
Зависшие cronМножественные экземпляры, конкуренция за ресурсыps, pgrep, логи cronCelery, Kubernetes CronJob, systemd timers
Заполнение дискаENOSPC ошибки, невозможность записиdf, du, ncdu, lsof +L1logrotate, политики очистки, мониторинг

Принципы устойчивого системного администрирования

Эффективное управление системными процессами опирается на набор фундаментальных принципов.

Принцип синхронизации времени

Все узлы распределённой системы синхронизированы с надёжными источниками времени. Приложения используют монотонные часы для измерения интервалов.

Принцип наблюдаемости

Каждый системный процесс экспонирует метрики выполнения, логирует ключевые события и предоставляет механизмы проверки состояния.

Принцип ограничения ресурсов

Все фоновые задачи имеют явные ограничения по времени выполнения, потреблению памяти и процессорного времени.

Принцип идемпотентности

Периодические задачи допускают безопасное повторное выполнение без побочных эффектов.

Принцип graceful degradation

При дефиците ресурсов система снижает интенсивность фоновых операций, сохраняя работоспособность критичных сервисов.

Принцип автоматического восстановления

Сбои системных процессов автоматически обнаруживаются и устраняются через перезапуск или переключение на резервные механизмы.


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).

Освоение главы0%