Thank you for reading this post, don't forget to subscribe!
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 раз:
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. Следовательно за его Прокси мы будем платить:
Чем тестировать?
Для проверки того, когда сервер начнёт отбрасывать подключения с “Too many connections” и для того, что бы отслеживать время выполнения запросов – набросал скрипт на Golang.
гофер из меня так себе – но скрипт рабочий:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
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:
Проверяем подключение:
mysql-tools
и Golang:Тестовый RDS Aurora
Переходим в RDS > Subnet Groups, создаём новую группу, в которую включаем наши приватные подсети из Avvailability Zones eu-west-2b и eu-west-2c:
Создаём кластер Aurora:
Для генерации паролей я обычно использую консольную утилитку pwgen
:
Вкладку с Security Group для Aurora можно оставить открытой – она нам ещё пригодится во время настройки RDS Proxy.
Проверяем подключение с EC2:
1 2 3 4 5 6 7 8 9 10 |
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.
Проверяем макс клиентов:
1 2 3 4 5 6 |
mysql> show variables like "max_connections"; +-----------------+-------+ | Variable_name | Value | +-----------------+-------+ | max_connections | 45 | +-----------------+-------+ |
Готовим скрипт (не умею я в новую систему модулей, делаю по-старинке):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
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 минут:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
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:
В случае проблем с подключением – можно заглянуть на страничку Troubleshooting for RDS Proxy.
Тестирование количества подключений #2: через RDS Proxy
Обновляем переменную для скрипта – указываем адрес RDS PRoxy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
... 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 |
И что мы видим?
- не пришло ни одного сообщения “Too many connections” – RDS Proxy ставил запросы в очередь на выполнение
- некоторые запросы выполнялись не ~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“:
1 2 3 4 |
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:
1 2 3 4 5 |
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 клиентов:
1 2 3 4 |
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:
1 2 3 4 |
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 клиентов:
1 2 3 4 |
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, но по-прежнему – “Ни единого разрыва“!