Завантаження процесора вашої бази даних сягає 100% о третій годині ночі. Трафік виглядає нормальним. Кількість запитів не є незвично високою. Проте ваше основне сховище «плавиться», тому що кожен із цих запитів запитує одне й те саме саме в той самий момент.

Це проблема thundering herd.

Уявіть стадіон після концерту. Протягом години той самий вихід може спокійно пропустити всіх. Але якщо весь натовп вирішить вийти через ці єдині двері протягом одного десятисекундного вікна, двері не зламаються. Вони просто не зможуть впоратися з такою конкурентністю. Проблема в натовпі, а не в дверях.

Де формується стадо

Ця закономірність проявляється щоразу, коли багато процесів синхронізуються на одній події. Вам не потрібен шкідливий трафік. Звичайні системи роблять це самі собі.

Протермінування кешу. Популярний ключ кешу зникає. Це може бути каталог товарів, feature flag або список дозволів користувача. Тисячі серверів додатків одночасно помічають порожнє місце. Кожен сервер, діючи доброчесно, відкриває власне з'єднання з базою даних і запускає той самий важкий запит, щоб відновити значення. База даних ніколи не була налаштована на те, щоб відповідати на одне й те саме дороге запитання дві тисячі разів паралельно. Один протермінований ключ виводить із ладу весь кластер.

Пробудження пулу з'єднань. У деяких архітектурах багато робочих процесів блокуються на одній умові, чекаючи на появу роботи. Коли ця умова виконується, операційна система пробуджує кожного сплячого працівника. Лише один працівник фактично отримує з'єднання або завдання. Решта прокидаються, розуміють, що програли в гонці, і знову засинають. Цей цикл споживає ресурси процесора на перемикання контексту та може позбавити можливості виконувати реальну продуктивну роботу.

Шторми повторних спроб (retry storms). У сервісу, що знаходиться нижче по ланцюгу, виникає збій. Кожен клієнт отримує тайм-аут і чекає фіксований інтервал, скажімо, рівно одну секунду, перш ніж спробувати знову. Коли сервіс відновлюється і знову приймає трафік, кожен клієнт звертається до нього в той самий момент. Сервіс, який ще «холодний» і відновлюється, знову падає. Цикл повторюється.

Колізії cron. Якщо ви плануєте технічне обслуговування, генерацію звітів або прогрів кешу на рівно 00:00 UTC для цілого флоту екземплярів, ви створюєте сплеск трафіку з годинниковою точністю. Навантаження передбачуване, але його концентрація є фатальною.

Об'єднання запитів (Request Coalescing)

Найефективніший спосіб вирішення cache stampedes — це припинити відповідати на одне й те саме запитання більше одного разу. Коли стається cache miss, вам не потрібно, щоб 2500 потоків кожен запускав запит до бази даних. Ви хочете, щоб один потік виконував запит, поки інші 2499 чекають на цей результат.

Цей патерн часто називають request coalescing. У Go пакет singleflight у golang.org/x/sync надає канонічну реалізацію. Перша горутина, яка викликає Do для певного ключа, починає роботу. Наступні викликачі з тим самим ключем блокуються на тому самому виклику Do. Коли робота завершується, усі очікувачі одночасно отримують результат. Ви виконали один запит і обслужили 2500 запитів.

Ви можете побудувати схожу поведінку за допомогою in-memory конкурентної карти промісів (promises), ф'ючерсів (futures) або каналів. Хитрість полягає в тому, щоб атомарно перевірити та зареєструвати запит, що виконується (in-flight request). Якщо запит завершується помилкою, усі отримують цю помилку, що зазвичай є правильною поведінкою. Якщо він успішний, усі отримують кешоване значення, а наступні запити звертаються безпосередньо до кешу. Навантаження на базу даних перетворюється з вертикального сплеску на рівну лінію.

Ймовірне раннє закінчення терміну дії

Іноді ви хочете повністю уникнути «наступу», а не керувати ним. Тут на допомогу приходить ймовірне раннє закінчення терміну дії (probabilistic early expiration), реалізоване за допомогою таких алгоритмів, як XFetch.

Замість жорсткого часу закінчення терміну дії, кожне кешоване значення має «м'яке» вікно закінчення терміну дії. Коли надходить запит, додаток генерує випадкове число і застосовує просту перевірку ймовірності. Якщо результат «кидка кубиків» каже «так», цей єдиний запит оновлює кеш завчасно. Якщо ні, запит повертає злегка застаріле значення.

Оскільки кожен запит «кидає свої кубики», оновлення розподіляються протягом м'якого вікна. Один клієнт може оновити кеш за сорок секунд до жорсткого закінчення терміну дії, інший — за дванадцять секунд, а більшість інших — взагалі не оновлюватимуть. Робота перетворюється з одного синхронізованого сплеску на м'який