MySQL proxy та логування запитів
Вже більше тижня я ніяк не можу одужати. Переважно лежу вдома, відпочиваю та набираюсь сил. Тому досить довгий час я не міг зібратись з думками та дописати (хоча й почав доволі давно) продовження своєї розповіді про молодий та цікавий проект MySQL proxy, що дозволяє досить легко проксювати запити до одного чи декількох серверів MySQL. Нагадаю, що таким чином можна створювати системи, що можуть витримувати підвищені навантаження, внаслідок розподілення запитів між декількома серверами, систему автоматичного коректування запитів, чи систему ведення логів запитів та підключень для декількох різних серверів. Думаю, що невеличке занурення в написання lua-скриптів для конфігурування MySQL proxy слід розпочати саме з такої досить простої задачі (це я про ведення логів).
Для початку рекомендую продивитись попередній матеріал та мою ж розповідь про Lua, хоча це й необов'язково
Загальна структура скрипта, параметри виклику функцій
Скрипти конфігурації MySQL proxy мажуть включати декілька функцій:
- connect_server() - виконується під час підключення клієнта до серверу
- read_handshake(auth) - виконуэться після спроби підключення клієнта до серверу
- read_auth(auth) - виконується тоді, коли клієнт відправляє запит на авторизацію
- read_auth_result(packet) - відповідь на резльтат запиту на авторизацію
- read_query(packet) - виконуэться після відправки запиту від клієнту, перед його виконанням
- read_query_result(inj) - виконується перед відправкою результатів клієнту
- disconnect_client() - виконується під час підключення клієнта до серверу
Ці функції можуть виконуватись лише в тому порядку, в якому я вони були перераховані, і цей порядок змінити неможливо. З одного боку, це є обмеженням (неможливо, якщо спроба авторизації була невдалою, змінити логін та пароль користувача на якийсь інший та спробувати авторизуватись знову, чи спробувати інший бекенд для авторизації), однак на практиці є доволі гнучкою (звісно, якщо розібратись з основною ідеєю, доступними параметрами та мовою lua).
Тепер перейдемо до параметрів, що передаються в кожну з функцій. connect_server() не має параметрів. На цьому етапі робота може проводитись з глобальною таблицею proxy.connection, яка містить інформацію про підключення. Думаю, що поле proxy.connection.thread_id, що вказую id потоку-підключення, не є настільки важливим, як proxy.connection.backend_ndx. Останнє поле таблиці вказує на номер бекенду, який використовується при спробі виконати підключення. Тобто, якщо проксі-сервер використовується для підключення для однієї з декількох серверів БД, то цей індекс буде вказувати на номер серверу, до якого ми будемо виконувати підключення. Маніпучюючи цим параметром маємо змогу змінювати сервер. (неприклад, для балансування навантаженням).
read_handshake() - в якості параметру отримує данні про підюклчення. Ця таблиця містить поля.- mysqld_version - версія MySQL серверу
- thread_id - id потоку
- scramble - буфер паролю
- server_addr - IP адреса серверу
- client_addr - IP адреса клієнту
Функція read_auth() як параметр отримує таблицю, що містить дані про авторизацію користувача, а саме username - логін користувача для підключення, password - пароль користувача, та default_db - БД за замовчуванням.
У функцію read_auth_result() у якості параметру передається пакет, якої поверне сервер у результаті авторизації. Щоб оцінити результат, який повертає сервер, можна конвертувати рельутат приблизно так:
local state = auth.packet:byte()
Порівнюючи значення змінної state з константами proxy.MYSQLD_PACKET_OK та proxy.MYSQLD_PACKET_ERR можемо перевірити результати авторизації.
Функція read_query() в акості параметру отримує пакет, що був переданий клієнтом. Взагалі, для обробки є додаткові скрипти, що розбирають пакет для отримання самого запиту, його розбору і т.д. Взагалі шляхом конвертування пакету в byte можемо отримати код того, що міститься в пакеті (якщо це нормальний запит, то значення має бути рівним константі proxy.COM_QUERY), а запит можна отримати як підрядок пакету починаючи з другого символу:
string.sub(packet, 2)
Функція read_query_result() - в якості параметру отримує таблицю, що містить:
- id - ID результату, що відповідає ID який було присвоєно запиту
- query - оригінальний рядок запит
- query_time - час у мілісекундах, що пройшов до моменту отримання першого рядку результату запиту
- response_time - час у мілісекундах, що пройшов до моменту отримання останнього рядку результату запиту
- resultset — результуючі дані
Тепер вже можна перейти до написання першого повноцінного скрипта.
Додамо трошки Lua
Дозволю собі нагадати, що пишемо ми скрипт для логування запитів і підключень до серверу. Для цього нам треба додати функцію, що буде виконувати запис у файл:
function write(message)
print("["..session.."] "..message)
fh:write(string.format("[%s\t%6d] -- %s \n", os.date('%Y-%m-%d %H:%M:%S'), session, message))
fh:flush()
end
function debug_write(message)
if is_debug then
write(message)
end
end
Вище наведено дві функції, що будуть використовуватись для ведення логів. Перша використовується для прямого запису в лог, а друга для запису у випадку, якщо скрипт знаходиться в режимі відлагодження (змінна is_debug). Як можна помітити, наведені функції використовуюить деякі глобальні змінні. Для того, щоб корситовуватись глобальними змінніми можна в середені скрипта описати змінні поза функціями. Наприклад так:
local is_debug = false
Однак вони будуть дійсні лише в рамках однієї сессії роботи MySQL proxy. Для створення змінних, які будуть використовуватись для конфігурації скриптів та містити дані необхідні для роботи всіх сессій (наприклад кількість сессій) можна використовувати таблицю proxy.global:
if not proxy.global.config.log then
proxy.global.config.log = {
sessions = 0,
is_debug = true
}
end
proxy.global.config.log.sessions = proxy.global.config.log.sessions+1
local session = proxy.global.config.log.sessions -- session id
local is_debug = proxy.global.config.log.is_debug -- is need to write debug info
Тут ми ініціалізуємо глобальну таблицю для збереження конфігурації, ініціалізуємо початковими занченням та виводимо в наш скрипт в якості глобальних для цієї сесії даних.
Залишається відкрити файл для запису:
local log_file = os.getenv("PROXY_LOG_FILE")
if (log_file == nil) then
log_file = "/tmp/mysql-proxy.log"
end
local fh = io.open(log_file, "a+")
В наведеному вище коді спочатку намагаємось отримати значення змінної оточення PROXY_LOG_FILE. Якщо воно не встановлене, то використовуэмо значення за замовчуванням (/tmp/mysql-proxy.log). І отриманий файл відкривається для дозапису.
Настав час перейти до написання самого скрипту для логування подій:
local commands = require("proxy.commands")
function connect_server()
-- new connection created
debug_write("")
write("")
debug_write("[connect_server] "..proxy.connection.client.address)
end
--
function read_auth_result(auth)
debug_write(" [read_auth_result] "..proxy.connection.client.address)
local packet = auth.packet:byte()
if packet == proxy.MYSQL_PACKET_OK then
proxy.connection.backend_ndx = 0
elseif packet == proxy.MYSQL_PACKET_ERR then
write(" (read_auth_result) auth failed")
else
write(" (read_auth_result) ... don't know: "..string.format("%q", auth.packet))
end
end
-- on query execution
function read_query(packet)
-- prepare
debug_write(" [read_query] "..proxy.connection.client.address)
local cmd = commands.parse(packet)
local c = proxy.connection.client
-- look at forward connected to backend
debug_write(" current backend = "..proxy.connection.backend_ndx)
debug_write(" client username = "..c.username)
if cmd.type == proxy.COM_QUERY then
debug_write(" query = "..cmd.query)
end
end
-- get query result
function read_query_result(inj)
debug_write(" [read_query_result]")
end
-- on client disconnect
function disconnect_client()
debug_write("[disconnect_client]"..proxy.connection.client.address)
end
Наведений вище код підключає додаткову бібліотеку proxy.commands, що використовується для обробки запитів до БД. Також використовується логування підключень до серверів, вдалих та невдалих авторизацій, запитів (з вказанням імені користувача, що виконував цей запит та номером бекенд-серверу до якого було направлено запит) та відключення клієнтів. Скрипт доволі простий, і якщо Ви уважно читали початок попередньо написане мною, то без проблем розберетесь з ним.
Далі буде
Цього в принципі вже достатньо для того, щоб писати свою власні "бойові" скрипти. Однак я планую написати ще одну статтю з на цю тему, в якій опишу реальну задачу, та наведу програмний код, який мені довелось написати для її вирішення. Сподіваюсь це буде цікаво та корисно.
Дякую за увагу!



