Керування мільйонами мікро-оновлень може вивести будь-яку систему на межу можливостей. Уявіть, що ви працюєте з системою індикаторів онлайн/офлайн для 1 мільйона активних користувачів. Кожен користувач відправляє серцебиття на сервер кожні 10 секунд, щоб підтвердити, що вони все ще онлайн. Це 6 мільйонів оновлень кожну хвилину — і кожне серцебиття є викликом API, що призводить до виклику бази даних.
Щоб зробити кожен виклик до бази даних, нам потрібно встановити TCP-з'єднання між сервером API та базою даних. Це не просто одноетапний процес. Кожне TCP-з'єднання передбачає 3-стороннє рукостискання (SYN, SYN-ACK, ACK) для налаштування та додаткове 2-стороннє рукостискання (FIN, ACK) для завершення. Це 5 мережевих викликів на одне з'єднання, що створює суттєве уповільнення для оновлень з високою частотою.
TCP Three-Way Handshake
Ось де пул з'єднань (Connection Pool) стає справжнім рятівником. Завдяки повторному використанню заздалегідь встановлених з'єднань з базою даних ви можете мінімізувати накладні витрати частих TCP-рукостискань, таким чином знижуючи затримки.
Що таке пул з'єднань?
Пул з'єднань (Connection Pool) — це набір заздалегідь встановлених, повторно використовуваних з'єднань з базою даних, які підтримуються та керуються сервером. Замість того, щоб відкривати нове з'єднання кожного разу, коли надходить запит, пул з'єднань дозволяє додаткам позичати та повертати з'єднання з пулу, що значно покращує ефективність.
Pool of Connections
Занадто багато відкритих з'єднань також може споживати цінні ресурси сервера, що призводить до потенційних сповільнень або навіть збоїв. Тому важливо знайти правильний баланс між відкриттям нових з'єднань і повторним використанням наявних.
Для досягнення цього балансу пул з'єднань повинен бути розумно керований. Він повинен знати:
- Коли створювати нові з'єднання (наприклад, якщо пул порожній, а надходять нові запити).
- Коли зберігати з'єднання в пулі (наприклад, коли вони активно використовуються).
- Коли закривати з'єднання (наприклад, якщо вони були неактивними занадто довго або система недовантажена).
Визначення кількості з'єднань
Для керування розміром пулу з'єднань ми встановлюємо мінімальні та максимальні ліміти з'єднань. Мінімальний ліміт гарантує, що завжди буде певна кількість відкритих з'єднань, готових до обробки запитів. Максимальний ліміт обмежує кількість з'єднань, які можуть бути створені, що дозволяє запобігти надмірному використанню ресурсів.
Видалення неактивних з'єднань
Щоб уникнути утримання з'єднань відкритими занадто довго, ми можемо ввести TTL (Time to Live). Це встановлює час життя для неактивних з'єднань. Якщо з'єднання не використовується протягом визначеного часу, воно закривається, щоб звільнити ресурси.
Простий пул TCP-з'єднань
Щоб краще зрозуміти концепцію пулу з'єднань, давайте розглянемо реалізацію в Java.
Розуміння BlockingQueue
Перед тим як глибше зануритись у реалізацію пулу з'єднань, давайте спочатку обговоримо BlockingQueue, ключовий компонент, що відіграє важливу роль у ефективному керуванні доступними з'єднаннями в пулі.
BlockingQueue — це спеціалізований тип черги, який підтримує потокобезпечні операції додавання (put) та видалення (take) елементів. Якщо черга порожня, коли потік намагається отримати елемент, потік заблокується (зачекає), поки елемент не стане доступним. Аналогічно, якщо черга досягла межі ємності і заповнена, потік, що намагається додати елемент, заблокується до того часу, поки не з'явиться місце.
Чому використовувати BlockingQueue у пулі з'єднань?
-
Потокобезпечність (Thread Safety): В багатопотокових додатках кілька потоків можуть одночасно намагатися позичити або повернути з'єднання. BlockingQueue гарантує, що ці операції будуть синхронізовані, запобігаючи гонитві за ресурсами (race condition).
2.
Блокуюча поведінка (Blocking Behavior): Коли в пулі немає доступних з'єднань, потік, що намагається позичити з'єднання, буде блокуватися, поки яке-небудь з'єднання не повернеться. Це важливо для ефективного керування ресурсами. -
Керування ємністю (Capacity Management): Хоча наша реалізація використовує не обмежену чергу (
LinkedBlockingQueue
), можна також використовувати обмеженуBlockingQueue
, щоб встановити суворіші ліміти на розмір пулу.
Ініціалізація пулу з'єднань (Initializing the Connection Pool)
Перший етап нашої реалізації — налаштування пулу з'єднань. Це включає визначення мінімальної та максимальної кількості з'єднань і забезпечення того, щоб пул починав з певної базової кількості з'єднань (minConnections).
public ConnectionPool(String host, int port, int minimumConnections, int maximumConnections) throws IOException{
this.host = host;
this.port = port;
this.minimumConnections = minimumConnections;
this.maximumConnections = maximumConnections;
this.idleConntions = new LinkedBlockingQueue<>(); // Необмежена блокуюча черга
this.currnetConnections = 0;
setUpLogger();
// Забезпечити мінімальну кількість відкритих з'єднань при запуску
for (int i=1; i<=this.minimumConnections; i++) {
this.currnetConnections++;
createNewConnection();
}
// Запустити потік для створення нових з'єднань кожні 5 секунд
startConnectionCreator();
}
Позичання з'єднання (Borrowing a Connection)
Коли потоку потрібно з'єднання, він позичає його з пулу. Якщо немає вільних з'єднань, і лімітmaxConnections
ще не досягнуто, створюється нове з'єднання.
public TcpConn borrowConnection() throws InterruptedException, IOException {
// Спроба взяти вільне з'єднання, якщо воно є
TcpConn connection = idleConntions.poll();
if(connection != null) {
logger.info("Позичене з'єднання: " + connection.getId());
return connection;
}
synchronized(lock) {
if(this.currnetConnections **Вивільнення з'єднання (Releasing a Connection)**
> Як тільки потік завершить роботу з з'єднанням, він **повертає** його в пул.
public void releaseConnection(TcpConn connection){
idleConntions.offer(connection);
// Повідомити потоки, що чекають, що з'єднання було повернуто
synchronized(lock){
lock.notify();
}
logger.info("Вивільнене з'єднання: " + connection.getId());
}
```
Щоб вивчити повну реалізацію та спробувати її самостійно, відвідайте GitHub репозиторій тут. Не соромтесь вносити свій внесок або залишати відгуки!
Перекладено з: Building a Custom Connection Pool for High-Throughput Systems