
Зависшие запросы и бесконечно работающие задачи — это:
Правильно выставленные таймауты, дедлайны и отмена решают это. Мы ограничиваем время жизни работы, заранее делим "бюджет времени" между шагами, а если что-то идёт не так — быстро освобождаем ресурсы и пробуем альтернативы.
Отмена должна "протекать" вниз по стеку: от входящего запроса к бизнес‑логике, затем к базе, кэшу, внешним сервисам, и в фоновые задачи.
Таймаут везде. Любой сетевой вызов, запрос к базе, операция в очереди — с таймаутом. Никаких бесконечных ожиданий.
Дедлайн пользователя — главный. Если у пользовательского запроса 800 мс, внутренние вызовы должны жить внутри этого окна. Нельзя начинать новую дорогостоящую операцию, когда до дедлайна осталось 50 мс.
Бюджет времени. Делим SLA запроса на доли по шагам: чтение из кэша — 50 мс, запрос к БД — 200 мс, внешний сервис — 300 мс и т. д. Если шаг не успел — лучше быстро отказать или вернуть деградировавший ответ (кэш/заглушка), чем зависать.
Повторы с умом. Ретраи только для безопасных операций и только внутри оставшегося бюджета. С экспоненциальной паузой и случайным разбросом, чтобы не усиливать пиковую нагрузку.
Уважайте отмену. Если клиент закрыл соединение — остановите работу и отпустите ресурсы. То же в фоновых задачах: задача должна уметь завершаться мягко.
package main
import (
"context"
"database/sql"
"fmt"
"log"
"net/http"
"time"
_ "github.com/jackc/pgx/v5/stdlib" // pgx stdlib driver
)
func main() {
// HTTP‑клиент с жёстким верхним пределом
httpClient := &http.Client{
Timeout: 800 * time.Millisecond, // общий лимит на весь запрос
}
// Подключение к Postgres через pgx stdlib
db, err := sql.Open("pgx", "host=127.0.0.1 user=app dbname=appdb sslmode=disable")
if err != nil { log.Fatal(err) }
db.SetMaxOpenConns(20)
db.SetConnMaxIdleTime(30 * time.Second)
srv := &http.Server{
Addr: ":8080",
ReadHeaderTimeout: 2 * time.Second,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Бюджет запроса — 900 мс
ctx, cancel := context.WithTimeout(r.Context(), 900*time.Millisecond)
defer cancel()
// Шаг 1: внешний сервис не дольше 300 мс
if err := callExternal(ctx, httpClient, 300*time.Millisecond); err != nil {
http.Error(w, "external timeout", http.StatusGatewayTimeout)
return
}
// Шаг 2: запрос к БД с собственным лимитом 200 мс
if err := queryDB(ctx, db, 200*time.Millisecond); err != nil {
http.Error(w, "db timeout", http.StatusGatewayTimeout)
return
}
fmt.Fprintln(w, "ok")
}),
}
// Мягкое завершение
go func() {
c := make(chan struct{})
<-c // замените на сигнал из ОС в реальном коде
}()
log.Println("listen :8080")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}
func callExternal(parent context.Context, httpClient *http.Client, limit time.Duration) error {
ctx, cancel := context.WithTimeout(parent, limit)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.com/ping", nil)
resp, err := httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 500 {
return fmt.Errorf("upstream error: %d", resp.StatusCode)
}
return nil
}
func queryDB(parent context.Context, db *sql.DB, limit time.Duration) error {
ctx, cancel := context.WithTimeout(parent, limit)
defer cancel()
// Дополнительно ограничим statement_timeout на уровне транзакции
tx, err := db.BeginTx(ctx, nil)
if err != nil { return err }
defer tx.Rollback()
if _, err := tx.ExecContext(ctx, "SET LOCAL statement_timeout = '200ms'"); err != nil {
return err
}
var n int
if err := tx.QueryRowContext(ctx, "SELECT 1").Scan(&n); err != nil {
return err
}
return tx.Commit()
}
Ключевые моменты:
statement_timeout, который прекратит долгий запрос даже если драйвер потеряет отмену.import express from 'express'
import fetch from 'node-fetch'
const app = express()
function withTimeout(signal, ms) {
const ctrl = new AbortController()
const timer = setTimeout(() => ctrl.abort(new Error('timeout')), ms)
if (signal) signal.addEventListener('abort', () => ctrl.abort(signal.reason))
return { signal: ctrl.signal, cancel: () => clearTimeout(timer) }
}
app.get('/ping', async (req, res) => {
// Общий бюджет — 800 мс
const { signal: deadlineSignal, cancel } = withTimeout(undefined, 800)
req.on('aborted', () => {
// клиент закрыл соединение — прекращаем работу
cancel()
})
try {
// Внешний запрос не дольше 300 мс
const { signal: stepSignal, cancel: stepCancel } = withTimeout(deadlineSignal, 300)
const resp = await fetch('https://example.com/ping', { signal: stepSignal })
stepCancel()
if (!resp.ok) throw new Error(`upstream: ${resp.status}`)
// Запрос к базе лучше ограничивать на стороне самой БД (statement_timeout)
// Например, перед выполнением запроса выполните:
// await client.query("SET LOCAL statement_timeout = '200ms'")
res.json({ ok: true })
} catch (e) {
if (e.name === 'AbortError') {
res.status(504).json({ error: 'timeout' })
} else {
res.status(502).json({ error: e.message })
}
} finally {
cancel()
}
})
app.listen(8080, () => console.log('listen 8080'))
Главная идея — один общий контроллер отмены и отдельные таймауты на шаги. Для Postgres дополнительно задавайте серверный таймаут (см. ниже).
import asyncio
import aiohttp
async def handler():
# Общий бюджет 0.8 c
try:
async with asyncio.timeout(0.8):
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=0.3)) as session:
async with session.get('https://example.com/ping') as resp:
if resp.status >= 500:
raise RuntimeError('upstream error')
# Дальше — запрос к БД; ограничьте его statement_timeout на стороне сервера
return {"ok": True}
except TimeoutError:
return {"error": "timeout"}
Помимо таймаутов в приложении, используйте ограничения на стороне базы. Они спасут, если приложение не смогло передать отмену.
-- Максимальная длительность одного запроса
SET LOCAL statement_timeout = '300ms';
-- Сколько ждать блокировку перед ошибкой (против "висим в lock")
SET LOCAL lock_timeout = '150ms';
-- Прерывать соединения, висящие в транзакции без активности
SET LOCAL idle_in_transaction_session_timeout = '5s';
Настраивайте это на уровне сессии/транзакции. В pool‑конфигурации — безопасные значения по умолчанию.
Пример простого asyncio‑воркера с таймаутом задачи:
import asyncio
async def process_job(job_id):
# полезная работа
await asyncio.sleep(0.2)
async def worker(queue):
while True:
job_id = await queue.get()
try:
async with asyncio.timeout(1.0):
await process_job(job_id)
except TimeoutError:
# фиксируем таймаут задачи
print(f"job {job_id} timed out")
finally:
queue.task_done()
async def main():
q = asyncio.Queue()
for i in range(10):
await q.put(i)
w = asyncio.create_task(worker(q))
await q.join()
w.cancel()
asyncio.run(main())
Частая ловушка — прокси, у которых таймаут дольше, чем у приложения. В результате приложение уже отменило работу, а прокси ждёт и держит соединение.
read_timeout, send_timeout, connect_timeout на прокси не превышают бюджет запроса.terminationGracePeriodSeconds, чтобы сервис успевал завершить запросы на остановку пода.Пример для Nginx:
upstream app_upstream {
server 127.0.0.1:8080;
keepalive 64;
}
grpclog off; # если не используете gRPC
server {
listen 80;
proxy_connect_timeout 300ms;
proxy_read_timeout 1s;
proxy_send_timeout 1s;
location / {
proxy_pass http://app_upstream;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
Пример счётчиков (псевдо‑Prometheus):
app_request_duration_seconds_bucket{path="/ping",le="0.25"} 123
app_request_timeout_total{kind="db"} 4
app_request_timeout_total{kind="external"} 7
app_canceled_by_client_total 3
statement_timeout, lock_timeout, idle_in_transaction_session_timeout (на сессию/транзакцию).Таймауты, дедлайны и корректная отмена — это не про "ещё одну настройку", а про управляемую систему: понятный SLA, прогнозируемые расходы и отсутствие зомби‑процессов. Введите общий бюджет времени для запроса, раздайте его шагам, уважайте отмену и страхуйте приложение таймаутами на стороне базы и прокси. В результате сервис перестаёт зависать, быстрее освобождает ресурсы и ровно держит показатели даже под нагрузкой.