AWS: RDS Proxy – обзор, запуск, тестирование

Автор: | 12/11/2021
 

AWS RDS Proxy – сервис от AWS, позволяющий разгрузить сервера баз данных AWS RDS, в первую очередь за счёт переиспользования существующих подключений вместо открытия новых для выполнения запросов от клиентов.

Кроме того, RDS Proxy улучшает failover при переключении упавшего инстанса на резервный, например – когда AWS RDS Aurora выполняет переключение read-replica на роль master, если master ушёл в ребут.

Как работает RDS Proxy?

См. RDS Proxy concepts and terminology

В целом, из всего, что нагуглил за время знакомства с сервисом – это аналог Proxy SQL для MySQL и pgproxy для PostgreSQL.

Итак, у нас есть сервер баз данных, в этом примере говорим только о MySQL. На каждое новое подключение для выполнения запроса сначала устанавливается TCP-соединение между хостами клиента и сервера баз данных, затем выполняется handshake, и только потом начинается передача и непосредственно выполнение SQL-запроса. См. Connection Phase.

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

Для демонстрации того, насколько реально это влияет на ресурсы сервера – давайте запустим mysqlslap, который будет открывать 30 одновременных подключений, выполнять самый простой запрос типа select version(), отключаться, и повторять всё заново, и так 1000 раз:

mysqlslap -h proxy-test-rds-aurora.dev.bttrm.local -u rds-proxy-user -pxie0AhN5bee9 --detach=1 --concurrency=30 --iterations=1000 --query 'select version();'

И графики CPU и памяти:

RDS Proxy и “Too many connections

Самое интересное случается, когда количество подключений к RDS Proxy превышает его “capacity” (позже увидим, где оно настраивается): в отличии от подключения к RDS напрямую, который в таком случае начнёт отбрасывать новые подключения с ошибкой “Too many connections” – RDS Proxy начнёт ставить подключения и запросы в очередь. Это приведёт к дОльшему выполнению запросов, но избавит именно от ошибок подключения, что очень приятно для приложения, и для клиентов, которые этим приложением пользуются.

Инстансы RDS Proxy располагаются в нескольких Avalability Zones, что обеспечивает их отказоутойчивость, и используют свои CPU и RAM, не затрагивая таким образом ресурсы самого сервера баз данных.

Connection pooling и multiplexing

Вместо того, что бы подключать приложение напрямую к серверу баз данных – мы настраиваем RDS Proxy и его Target Group (бекенд), к которому RDS Proxy открывает пул подключений (connection pool) через свои собственные ендпоинты.

Клиенты подключаются к RDS Proxy, и их запросы отправляются через пул коннектов самого Proxy к серверу баз данных. При этом часть запросов могут выполняться через уже установленное соединение к бекенду вместо открытия нового – multiplexing.

Failover

Failover – одна из многих приятных вещей в AWS RDS Aurora, когда при выходе из строя или перезагрузке Aurora-кластер сам переключает ендпоинты, направляя таким образом новые подключения на новый инстанс, например – когда Мастер становится Слейвом.

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

RDS Proxy сам мониторит состояние серверов в своей таргет-группе, и при необходимости быстро переключает пул коннектов на новый инстанс сервера баз данных.

Надо будет протестировать – очень любопытная вещь.

См. Improving application availability with Amazon RDS Proxy, Failover и A First Look at Amazon RDS Proxy.

Где пригодится RDS Proxy?

См. Planning where to use RDS Proxy.

Да много где. К примеру, есть сервера баз данных Dev-окружения, где сравнительно “тонкие” клиенты, но где QA-команда любит запускать различные нагрузочные тесты. В таком случае перед нагрузочным нам приходится увеличивать типы серверов, иначе тестировщики сталкиваются с ошибками “Too many connections“.

Также, RDS Proxy пригодится для серверов, которые обслуживают множество короткоживущих запросов типа mysql_ping или для AWS Lambda functions, которые обычно выполняются сравнительно часто и быстро.

Полезно при использовании языков, которые не умеют в пулы коннектов, такие как PHP и Ruby.

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

Ограничения RDS Proxy

Всё звучит очень вкусно, даже слишком, поэтому кратко рассмотрим ограничения, с которыми можем столкнуться при использовании AWS RDS Proxy. Также, см. ссылки в конце поста – там пара любопытных историй о проблемах, которые могут возникнуть при использовании SQL Proxy.

См. Quotas and limitations for RDS Proxy.

  • RDS Proxy на данный момент доступен не во всех регионах
  • RDS Proxy доступен только для MySQL и PostgreSQL
  • RDS Proxy недоступен для кластеров Aurora Serverless
  • RDS Proxy должен быть в той же VPC, где и сервер(а) баз данных, и не может быть доступен из мира (хотя сервера могут быть publicity accessible)

Стоимость RDS Proxy

Тут всё очень прозрачно: оплачиваем за количество доступных vCPU ($0.015 в us-east-1) на сервере/ах баз данных, которые входят в Target Group нашего Proxy.

К примеру, у нас инстанс Авроры db.t3.medium с двумя vCPU. Следовательно за его Прокси мы будем платить:

echo 0.015*2*24*30 | bc
21.600

См. Amazon RDS Proxy pricing.

Чем тестировать?

Для проверки того, когда сервер начнёт отбрасывать подключения с “Too many connections” и для того, что бы отслеживать время выполнения запросов – набросал скрипт на Golang.

Go трогал последний раз почти два года тому, когда разбирался с указателями в Go, гофер из меня так себе – но скрипт рабочий:

package main

import (
   "os"
   "time"
   "database/sql"
   "fmt"
   "strconv"
   _ "github.com/go-sql-driver/mysql"
)

func main() {

   dbHost := os.Getenv("RDS_HOST")
   dbUser := os.Getenv("RDS_USER")
   dbPass := os.Getenv("RDS_PASS")
   dbName := os.Getenv("RDS_DB")

   // create a connection object db'
   db, err := sql.Open("mysql", dbUser+":"+dbPass+"@tcp("+dbHost+":3306)/"+dbName)
   if err != nil {
      panic(err)
   }
   // close after main() finished
   defer db.Close()

   // check if we can establish a real connection
   if err := db.Ping(); err != nil {
      panic(err)
   }

   dbInter, err := strconv.Atoi(os.Getenv("RDS_ITERATIONS"))

   for i := 1; i < dbInter; i++ {

     start := time.Now()

     var vname string
     var conns int

     fmt.Printf("\nIneration: %d", i)

     // get currently connected threads/clients, save its number to the conns' var
     err = db.QueryRow("show status where `variable_name` = 'Threads_connected'").Scan(&vname, &conns)

     // intercept the 'Too many connections'  here
     if err != nil {
       fmt.Printf("\n%s\n", err)
     } else {
       // run a query and leave it for 20 seconds
       go func() {
         _, err := db.Query("SELECT sleep(100)")
         if err != nil {
           panic(err)
         }
       }()
       // this includes default RDS threads - 2 if no Proxy, 4 with Proxy
       fmt.Printf("\nConnected: %d", conns)
     }

     // make sleep between iterations so we can see graph in the CloudWatch metrics
     time.Sleep(500 * time.Millisecond)

     fmt.Printf("\nExecution time: %s\n", time.Since(start))

   }

   fmt.Println("Fin")
}

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

Дальше уже для нагрузочного тестирования – используем mysqlslap, а ещё случайно увидел утилиту mysqltest, но не пользовался.

Подготовка – сеть, EC2, RDS Aurora

Настройка сети

Для тестирования – создадим отдельную VPC, подсети, небольшой кластер AWS RDS Aurora и тестовый ЕС2, с которого будем подключаться.

Создаём VPC:

Создаём три подсети – одна публичная, в eu-west-2a, и две приватных – в eu-west-2b и eu-west-2c:

Создаём Internet Gateway для публичной подсети:

Подключаем к VPC:

Находим Route Table публичной посети, добавляем маршрут в 0.0.0.0/0 через созданный выше IGW:

Тестовый EC2

Запускаем тестовый EC2 в публичной подсети, подключаем ему публичный IP:

Проверяем подключение:

ssh -i rds-proxy-test-ec2-eu-west-2-rsa.pem ubuntu@13.40.69.72
...
root@ip-10-0-0-97:/home/ubuntu#

Устанавливаем mysql-tools и Golang:

root@ip-10-0-0-97:/home/ubuntu# apt update && apt -y install mysql-client golang

Тестовый RDS Aurora

Переходим в RDS > Subnet Groups, создаём новую группу, в которую включаем наши приватные подсети из Avvailability Zones eu-west-2b и eu-west-2c:

Создаём кластер Aurora:

Для генерации паролей я обычно использую консольную утилитку pwgen:

pwgen 12 -1
Lohn1aiceiPh

Указываем имя кластера, рутового пользователя и его пароль:

Используем наименьший доступный тип инстанса, добавим Multi-AZ:

Настраиваем сеть – выбираем VPC, нашу Subnet Group, публичный доступ не включаем, создаём новую Security Group rds-proxy-test-aurora-cluster-sg:

Запускаем создание кластера, пока он создаётся – находим созданную Security Group:

И добавляем в неё Security Group нашего инстанса EC2, что бы получить доступ для тестов:

Вкладку с Security Group для Aurora можно оставить открытой – она нам ещё пригодится во время настройки RDS Proxy.

Проверяем подключение с EC2:

root@ip-10-0-0-97:/home/ubuntu# mysqladmin -u admin -p -h rds-proxy-test-aurora-cluster.cluster-ci9hdkrgdpwy.eu-west-2.rds.amazonaws.com status
Enter password:
Uptime: 175  Threads: 5  Questions: 1176  Slow queries: 0  Opens: 140  Flush tables: 1  Open tables: 121  Queries per second avg: 6.720

Создаём тестовую базу, пользователя:

mysql> create database `rds-proxy-db`;
Query OK, 1 row affected (0.01 sec)
mysql> create user 'rds-proxy-user'@'%' identified by 'xie0AhN5bee9';
Query OK, 0 rows affected (0.01 sec)
mysql> grant all privileges on `rds-proxy-db`.* to 'rds-proxy-user'@'%';
Query OK, 0 rows affected (0.00 sec)

Проверяем доступ ещё раз:

root@ip-10-0-0-97:/home/ubuntu# mysql -u rds-proxy-user -p -h rds-proxy-test-aurora-cluster.cluster-ci9hdkrgdpwy.eu-west-2.rds.amazonaws.com
Enter password:
...
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| rds-proxy-db       |
+--------------------+

Тестирование количества подключений #1: на RDS Aurora

Сначала проверим на каком количестве активных подключений мы начнём получать “Too many connections” от самого кластера Aurora, без RDS PRoxy.

Проверяем макс клиентов:

mysql> show variables like "max_connections";
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 45    |
+-----------------+-------+

Готовим скрипт (не умею я в новую систему модулей, делаю по-старинке):

ubuntu@ip-10-0-0-97:~$ export GO111MODULE=off
ubuntu@ip-10-0-0-97:~$ go get github.com/go-sql-driver/mysql

Задаём переменные:

export RDS_HOST=proxy-test-rds-aurora.dev.bttrm.local
export RDS_USER=rds-proxy-user
export RDS_PASS=xie0AhN5bee9
export RDS_DB=rds-proxy-db
export RDS_ITERATIONS=100

Домен proxy-test-rds-aurora.dev.bttrm.local направлен на мастер-инстанс Aurora. Для Proxy потом добавим домен proxy-test-rds-proxy.dev.bttrm.local. См. AWS: Route53 Private Hosted Zones – прячем домены от мира.

Запускаем скрипт:

ubuntu@ip-10-0-0-97:~$ go run mysql-connect-test.go
Ineration: 1
Connected: 7
Execution time: 504.09813ms
...
Ineration: 39
Connected: 45
Execution time: 513.671499ms
Ineration: 40
Error 1040: Too many connections
Execution time: 528.578825ms
Ineration: 41
Error 1040: Too many connections
...
Ineration: 99
Error 1040: Too many connections
Execution time: 504.961624ms
Fin

Отлично – на 45+ коннектах мы начали отваливаться, как и ожидалось.

Создание RDS Proxy

Ну и наконец-то приступаем к RDS Proxy.

AWS Secret Manager

Сначала создаём секрет – переходим в Secrets Manager, кликаем Store a new secret, выбираем тип Credentials for Amazon RDS database.

Указываем логин и пароль, внизу выбираем кластер Авроры, к которому секрет применяется:

Задаём имя секрета, сохраняем:

RDS Proxy Security Group

С Security Group есть два варианта: использовать Security Group, которую делали для нашей Авроры – тогда в ней надо добавить правило MySQL и в source указать ID этой же группы, т.е. разрешить ей “ходить саму на себя”.

Другой вариант – сделать новую Security Group для инстанса RDS Proxy, а в Security Group кластера Авроры разрешить доступ с Security Group нашего Proxy.

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

Копируем группу:

Сохраним как rds-proxy-test-proxy-sg:

Копируем ID новой группы:

Редактируем Security Group Авроры, добавляем в ней доcтуп из новой Security Group, которая для Proxy:

И только теперь переходим в RDS > Proxies, и начинаем создавать сам RDS Proxy.

Создание RDS Proxy

Кликаем Create proxy, задаём имя, тип MySQL, TLS пока не трогаем:

Настраиваем Target Group – выбираем кластер Aurora, в пуле коннектов укажем 100% – это будет 45 коннектов пула самого RDS Proxy, 100% от капасити инстанса RDS Aurora:

Отмечаем Include reader – RDS Proxy определит, что у нашей Aurora есть slave-инстанс со своим read-only ендпоинтом, и создаст такой же ендпоинт у себя. Тогда приложения, которые используют мастер для всех write/update операций будут по-прежнему ходить через один ендпоинт, а все запросы типа SELECT будут как и раньше ходить на слейв-инстансы.

В Additional target group configuration можно настроить пиннинг – пока пропустим, см. Avoiding pinning.

Настраиваем Connectivity – выбираем секрет из Secrets Manager, оставляем Create IAM Role, IAM-аутентификацию тут не используем, выбираем теже приватные подсети, которые использовали в Subnet Group при создании Aurora-кластера.

В Additional connectivity configuration выбираем Security Group, которую создавали для RDS Proxy:

Ждём статус Available:

Статус таргет-группы можно проверить с AWS CLI:

aws --region eu-west-2 rds describe-db-proxy-targets --db-proxy-name rds-proxy-test-proxy
{
"Targets": [
{
"Endpoint": "rds-proxy-test-aurora-cluster-instance-1-eu-west-2b.ci9hdkrgdpwy.eu-west-2.rds.amazonaws.com",
"TrackedClusterId": "rds-proxy-test-aurora-cluster",
"RdsResourceId": "rds-proxy-test-aurora-cluster-instance-1-eu-west-2b",
"Port": 3306,
"Type": "RDS_INSTANCE",
"Role": "UNKNOWN",
"TargetHealth": {
"State": "UNAVAILABLE",
"Reason": "PENDING_PROXY_CAPACITY",
"Description": "DBProxy Target is waiting for proxy to scale to desired capacity"
}
},
{
"RdsResourceId": "rds-proxy-test-aurora-cluster",
"Port": 3306,
"Type": "TRACKED_CLUSTER"
},
{
"Endpoint": "rds-proxy-test-aurora-cluster-instance-1.ci9hdkrgdpwy.eu-west-2.rds.amazonaws.com",
"TrackedClusterId": "rds-proxy-test-aurora-cluster",
"RdsResourceId": "rds-proxy-test-aurora-cluster-instance-1",
"Port": 3306,
"Type": "RDS_INSTANCE",
"Role": "UNKNOWN",
"TargetHealth": {
"State": "UNAVAILABLE",
"Reason": "PENDING_PROXY_CAPACITY",
"Description": "DBProxy Target is waiting for proxy to scale to desired capacity"
}
}
]
}

Минут через 5 RDS Proxy пишет, что готов:

Но его ендпоинты и подключение к таргет-группам ещё настраиваются.

В целом процесс занял около 10-15 минут:

aws --region eu-west-2 rds describe-db-proxy-targets --db-proxy-name rds-proxy-test-proxy
{
"Targets": [
{
"RdsResourceId": "rds-proxy-test-aurora-cluster",
"Port": 3306,
"Type": "TRACKED_CLUSTER"
},
{
"Endpoint": "rds-proxy-test-aurora-cluster-instance-1.ci9hdkrgdpwy.eu-west-2.rds.amazonaws.com",
"TrackedClusterId": "rds-proxy-test-aurora-cluster",
"RdsResourceId": "rds-proxy-test-aurora-cluster-instance-1",
"Port": 3306,
"Type": "RDS_INSTANCE",
"Role": "READ_WRITE",
"TargetHealth": {
"State": "AVAILABLE"
}
},
{
"Endpoint": "rds-proxy-test-aurora-cluster-instance-1-eu-west-2b.ci9hdkrgdpwy.eu-west-2.rds.amazonaws.com",
"TrackedClusterId": "rds-proxy-test-aurora-cluster",
"RdsResourceId": "rds-proxy-test-aurora-cluster-instance-1-eu-west-2b",
"Port": 3306,
"Type": "RDS_INSTANCE",
"Role": "READ_ONLY",
"TargetHealth": {
"State": "AVAILABLE"
}
}
]
}

В AWS Route53 создаём запись proxy-test-rds-proxy.dev.bttrm.local с типом CNAME на адрес мастер-ендпоинта Proxy – rds-proxy-test-proxy.proxy-ci9hdkrgdpwy.eu-west-2.rds.amazonaws.com.

С тестового ЕС2 проверяем мастер-ендпоинт RDS Proxy, используя логин-пароль от Aurora:

ubuntu@ip-10-0-0-97:~$ mysqladmin -h proxy-test-rds-proxy.dev.bttrm.local -u rds-proxy-user -p status
Enter password:
Uptime: 13645  Threads: 8  Questions: 1003139  Slow queries: 0  Opens: 118  Flush tables: 1  Open tables: 111  Queries per second avg: 73.516

В случае проблем с подключением – можно заглянуть на страничку Troubleshooting for RDS Proxy.

Тестирование количества подключений #2: через RDS Proxy

Обновляем переменную для скрипта – указываем адрес RDS PRoxy:

ubuntu@ip-10-0-0-97:~$ export RDS_HOST=proxy-test-rds-proxy.dev.bttrm.local

Запускаем повторный тест – в прошлый раз мы получили “Too many connections” на 40-ой итерации, а сейчас:

...
Ineration: 38
Connected: 44
Execution time: 518.142051ms
Ineration: 39
Connected: 44
Execution time: 1m19.997379109s
Ineration: 40
Connected: 44
Execution time: 515.37535ms
...
Ineration: 76
Connected: 44
Execution time: 508.229272ms
Ineration: 77
Connected: 44
Execution time: 1m19.239202385s
Ineration: 78
Connected: 44
Execution time: 516.224887ms
...
Ineration: 99
Connected: 44
Execution time: 507.983456ms
Fin

И что мы видим?

  1. не пришло ни одного сообщения “Too many connections” – RDS Proxy ставил запросы в очередь на выполнение
  2. некоторые запросы выполнялись не ~500ms, как задано в time.Sleep(500 * time.Millisecond) между итерациями, а почти две минуты – RDS Proxy ждал, пока закончится один из предыдущих запросов SELECT sleep(100), 100 секунд, после чего запускал выполнение следующего

Т.е. новые запросы ожидали свободных коннекшенов в пуле Proxy, что бы начать выполнение.

Latency (время ответа) местами  выросло – но это намного лучше, чем ловить на API-бекенде мобильного приложения 500-ые ошибки, правда?

Нагрузочное тестирование

Ну и самые интересные результаты получаются уже с использованием mysqlslap.

Тут стоит учитывать, что тесты весьма синтетические-искусственные, и реальной картины для реального приложения не покажут – там надо проводить свои тесты, отслеживать ошибки, время ответа и так далее.

Первый прогон.

Используем:

  • хост: напрямую на Аврору, proxy-test-rds-aurora.dev.bttrm.local
  • --detach=1: выполняем новое подключение после каждого запроса
  • --concurrency=45: 45 одновременных подключений, наш лимит сервера  из max_connected_threads
  • --iterations=10: каждый клиент выполняет 10 запросов
  • --query 'select version();': и простой запрос на получение версии MySQL

Запускаем – и сразу ловим “Too many connections“:

ubuntu@ip-10-0-0-97:~$ mysqlslap -h proxy-test-rds-aurora.dev.bttrm.local -u rds-proxy-user -pxie0AhN5bee9 --detach=1 --concurrency=45 --iterations=10 --query 'select version();'
mysqlslap: [Warning] Using a password on the command line interface can be insecure.
mysqlslap: Error when connecting to server: 1040 Too many connections
...

Уменьшаем клиентов до 30:

ubuntu@ip-10-0-0-97:~$ mysqlslap -h proxy-test-rds-aurora.dev.bttrm.local -u rds-proxy-user -pxie0AhN5bee9 --detach=1 --concurrency=30 --iterations=10 --query 'select version();'
mysqlslap: [Warning] Using a password on the command line interface can be insecure.
Benchmark
Average number of seconds to run all queries: 0.762 seconds
...

Время – 0.762 seconds.

А теперь – тоже самое, но через RDS Proxy – меняем --host на proxy-test-rds-proxy.dev.bttrm.local, те же 30 клиентов:

ubuntu@ip-10-0-0-97:~$ mysqlslap -h proxy-test-rds-proxy.dev.bttrm.local -u rds-proxy-user -pxie0AhN5bee9 --detach=1 --concurrency=30 --iterations=10 --query 'select version();'
mysqlslap: [Warning] Using a password on the command line interface can be insecure.
Benchmark
Average number of seconds to run all queries: 0.264 seconds

Время – 0.264 seconds.

Попробуем увеличить клиентов до 45:

ubuntu@ip-10-0-0-97:~$ mysqlslap -h proxy-test-rds-proxy.dev.bttrm.local -u rds-proxy-user -pxie0AhN5bee9 --detach=1 --concurrency=100 --iterations=10 --query 'select version();'
mysqlslap: [Warning] Using a password on the command line interface can be insecure.
Benchmark
Average number of seconds to run all queries: 0.723 seconds

Время – 0.723 секунд.

И жахнем 200 клиентов:

ubuntu@ip-10-0-0-97:~$ mysqlslap -h proxy-test-rds-proxy.dev.bttrm.local -u rds-proxy-user -pxie0AhN5bee9 --detach=1 --concurrency=200 --iterations=10 --query 'select version();'
mysqlslap: [Warning] Using a password on the command line interface can be insecure.
Benchmark
Average number of seconds to run all queries: 1.796 seconds

Время – 1.796, но по-прежнему – “Ни единого разрыва“!

В целом – RDS Proxy выглядит крайне многообещающе, но перед самым Новым Годом, когда у нас ожидаются максимальные нагрузки, решили не вводить в работу – продолжим тестирование уже в 2022 году.

И обязательно посмотрите ссылки по теме – не всё может оказаться так гладко, как хотелось бы.

Ссылки по теме