Теория очередей и модель M/M/1

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

M/M/1 — это система массового обслуживания с одним сервером, в которой клиенты приходят пуассоновским потоком (интервалы между приходами распределены экспоненциально со средним $1/\lambda$), а время обслуживания тоже экспоненциально (со средним $1/\mu$). Три символа кодируют: M — приходы без памяти, M — обслуживание без памяти, 1 — один сервер.

Зачем это нужно? Теория очередей отвечает на практические вопросы: сколько касс открыть, чтобы клиенты не ждали дольше пяти минут? Хватит ли одного сервера, или при росте нагрузки очередь «взорвётся»? M/M/1 — самая простая модель, на которой эти эффекты видно как на ладони.

Загрузка системы $\rho$

Ключевая величина — загрузка (utilization) $\rho = \frac{\lambda}{\mu}$. Это отношение скорости прихода к скорости обслуживания, то есть доля времени, которую сервер занят.

  • Если $\rho \lt 1$ — сервер успевает обслуживать поток, система стабильна, очередь колеблется вокруг конечного среднего.
  • Если $\rho \ge 1$ — приходят быстрее, чем уходят, очередь растёт неограниченно. Система нестабильна.
  • Если $\rho$ стремится к 1 снизу (например 0.95, 0.99) — система формально стабильна, но очередь становится огромной.

Теоретические формулы

Для стабильной M/M/1 ($\rho \lt 1$) теория даёт замечательно простые ответы:

  • Среднее число клиентов в системе (в очереди плюс на обслуживании): $L = \frac{\rho}{1 - \rho}$.
  • Среднее время, проведённое клиентом в системе (ожидание плюс обслуживание): $W = \frac{1}{\mu - \lambda}$.

Эти две формулы связаны законом Литтла $L = \lambda W$ — фундаментальным результатом теории очередей. Посмотрите, как $L$ ведёт себя у границы: при $\rho = 0.5$ получаем $L = 1$; при $\rho = 0.9$ уже $L = 9$; при $\rho = 0.99$ — целых $L = 99$. Рост лавинообразный.

Симуляция M/M/1

Проверим формулы дискретно-событийной симуляцией. У нас два типа будущих событий: следующий приход (next_arr) и следующий уход (next_dep). На каждой итерации мы берём ближайшее из них, переводим часы и обрабатываем. Попутно копим площадь под графиком числа клиентов (area), чтобы усреднением получить L по времени.

import random, math
from collections import deque
random.seed(11)
lam, mu = 0.7, 1.0
clock = 0.0
next_arr = random.expovariate(lam)
next_dep = math.inf
in_system = 0
served = 0
total_wait = 0.0
arrivals = deque()
END = 20000.0
area = 0.0
last = 0.0
while clock < END:
    t = min(next_arr, next_dep)
    area += in_system * (t - last)
    last = t
    clock = t
    if next_arr <= next_dep:
        in_system += 1
        arrivals.append(clock)
        if in_system == 1:
            next_dep = clock + random.expovariate(mu)
        next_arr = clock + random.expovariate(lam)
    else:
        in_system -= 1
        a = arrivals.popleft()
        total_wait += clock - a
        served += 1
        next_dep = clock + random.expovariate(mu) if in_system > 0 else math.inf
rho = lam / mu
print(f"rho (загрузка) = {rho}")
print(f"Обслужено: {served}")
print(f"L: симуляция={area/END:.3f}, теория={rho/(1-rho):.3f}")
print(f"W: симуляция={total_wait/served:.3f}, теория={1/(mu-lam):.3f}")

Вывод:

rho (загрузка) = 0.7
Обслужено: 14085
L: симуляция=2.377, теория=2.333
W: симуляция=3.375, теория=3.333

Симуляция и теория совпали с точностью до сотых: L получилось 2.377 против 2.333, W — 3.375 против 3.333. Маленькое расхождение — это статистический шум конечного прогона; увеличив END, мы сделали бы его меньше. Главное, что симуляция, не зная формул, воспроизвела их результат — значит, и формулы, и симуляция описывают одно и то же.

Как работает под капотом

Разберём ключевые приёмы.

expovariate — экспоненциальные интервалы. Вызов random.expovariate(lam) возвращает случайный интервал до следующего прихода со средним 1/lam. Экспоненциальное распределение «без памяти»: сколько бы мы ни ждали, ожидаемое время до следующего события не меняется. Именно это свойство делает поток пуассоновским и даёт ту самую букву M.

Два указателя в будущее. Вместо полноценной кучи здесь хватает двух переменных: next_arr и next_dep. Сервер один, поэтому уход в любой момент времени запланирован максимум один. Когда сервер пуст, next_dep = math.inf — «ухода не предвидится», и min всегда выберет приход.

Площадь под графиком. Число клиентов in_system — это ступенчатая функция времени. Чтобы усреднить её по времени, мы на каждом шаге прибавляем in_system * (t - last) — площадь прямоугольника шириной от прошлого события до текущего. В конце делим накопленную площадь на длину прогона и получаем среднее по времени число клиентов — это и есть L.

число клиентов
 3 |          ____
 2 |     ____|    |__
 1 |  __|           |____
 0 |_|                   |___
   +--------------------------- время
   площадь под этой ступенькой / END = L

Очередь приходов. deque хранит времена приходов клиентов, ещё не покинувших систему. Когда клиент уходит, мы достаём из её левого конца время самого старого прихода (popleft) и считаем, сколько он провёл в системе — это вклад в total_wait. В конце total_wait / served даёт среднее W.

Что будет при перегрузке

Поставьте мысленно lam = 0.95, оставив mu = 1.0. Тогда $\rho = 0.95$, и теория обещает $L = \frac{0.95}{0.05} = 19$ клиентов в системе и $W = \frac{1}{0.05} = 20$ единиц времени. Сравните с нашими 2.33 и 3.33 при $\rho = 0.7$! Стоит подобраться к $\rho = 0.99$ — и $L$ взлетает до 99. Вот почему перегруженные системы (касса в час пик, сервер под нагрузкой) «встают»: при загрузке у самой единицы любая случайная пачка приходов раскачивает очередь до огромных значений, и рассосаться она не успевает. Запас по производительности — не роскошь, а условие устойчивости.

Частые ошибки

  • Думать, что при $\rho \lt 1$ очереди не будет. Будет: при $\rho = 0.9$ среднее число в системе уже 9. Стабильность означает лишь, что среднее конечно, а не мало.
  • Считать L и W линейными по нагрузке. Они растут гиперболически: знаменатель $(1 - \rho)$ или $(\mu - \lambda)$ стремится к нулю, и значения взрываются у границы.
  • Усреднять число клиентов по событиям, а не по времени. L — это среднее по времени. Нужно взвешивать каждое значение длительностью интервала (площадь под графиком), иначе результат смещён.
  • Забыть про math.inf для пустого сервера. Если не «отключать» уход, когда система пуста, min может выбрать несуществующее завершение обслуживания и сломать логику.
  • Доверять одному короткому прогону. Симуляция шумит. Чем дольше END, тем ближе оценки к теории; по одному короткому прогону судить о системе нельзя.

Итоги

  • M/M/1 — очередь с одним сервером, пуассоновскими приходами (rate $\lambda$) и экспоненциальным обслуживанием (rate $\mu$).
  • Загрузка $\rho = \frac{\lambda}{\mu}$ — доля занятости сервера; при $\rho \lt 1$ система стабильна, при $\rho \ge 1$ очередь растёт без предела.
  • Теория даёт $L = \frac{\rho}{1-\rho}$ и $W = \frac{1}{\mu-\lambda}$, связанные законом Литтла $L = \lambda W$.
  • DES-симуляция с двумя событиями (приход/уход) и площадью под графиком воспроизводит эти формулы с точностью до статистического шума.
  • У границы $\rho$ стремится к 1 величины $L$ и $W$ взрываются — поэтому перегруженные системы лавинообразно «встают», и нужен запас по производительности.
Проверьте себя
1. Что означает загрузка rho = lambda/mu в модели M/M/1?
AСреднее число клиентов в очереди
BДолю времени, которую сервер занят; при rho меньше 1 система стабильна
CВремя обслуживания одного клиента
DВероятность того, что клиент уйдёт, не дождавшись
2. Что происходит со средним числом клиентов L, когда rho приближается к 1?
AL стремится к 1
BL растёт линейно и остаётся небольшим
CL взрывается (растёт неограниченно), так как L = rho/(1-rho)
DL стремится к нулю
3. Почему среднее число клиентов L нужно считать как площадь под графиком, делённую на время?
AЧтобы код работал быстрее
BПотому что L — это среднее по времени, и каждое значение надо взвешивать длительностью интервала
CПотому что площадь всегда больше суммы
DЧтобы избавиться от случайности в симуляции
4. Зачем next_dep устанавливают в math.inf, когда система пуста?
AЧтобы симуляция остановилась
BЧтобы оператор min никогда не выбрал несуществующий уход и всегда взял приход
CЧтобы сэкономить память
DЧтобы увеличить загрузку rho
py
Курс по теме
Пройдите курс «Python с нуля» — по шагам, с проверкой
8 уроков · ~14 ч · теория, упражнения и экзамен с бейджем
Открыть курс →