Ошибка MySQL Server Has Gone Away (error 2006) может возникнуть в двух случаях.

Схема работы
Схема работы

Причины возникновения ошибки

1. Таймаут соединения

Наиболее распространенная проблема: таймаут соединения, в результате чего сервер его закрывает. Решение весьма тривиальное — увеличение лимита времени wait_timeout в файле конфигурации my.cnf. Для этого нужно выполнить следующие команды:

# Открытие файла настроек MySQL
sudo nano /etc/mysql/my.cnf
# Установить тайм-аут ожидания в секундах, можно установить вплоть до 28800 с (8 часов)
wait_timeout = 600
# Необходима перезагрузка базы данных MySQL
sudo /etc/init.d/mysql restart
# Настройка timeout в MySQL

Иногда, при выполнении длительных запланированных задач, также может появиться ошибка MySQL Server Has Gone Away все из-за того же таймаута соединения. При этом лимит времени не получится существенно увеличить (максимум до нескольких часов), так как это может привести к заполнению буфера ожидающими соединениями. Поэтому лучше проверить соединение и, при необходимости, переподключиться.

2. Большой или некорректный пакет

Вторая распространенная проблема: сервер получает большой или некорректный пакет и отклоняет его. В этом случае сервер считает, что проблема на стороне клиента и закрывает соединение. Так что для решения нужно увеличить лимит на максимальный размер пакета(в МБ) max_allowed_packet = 64M все в том же файле конфигурации. Также не забудьте перезагрузить базу данных.

Оптимальные настройки

После того, как ошибка MySQL Server Has Gone Away устранена, поиграйтесь с параметрами wait_timeout и max_allowed_packet для получения оптимальных лимитов. Стандартные конфигурации MySQL под различные размеры оперативной памяти можно посмотреть на сайте ruhighload.

Подробнее об этой ошибке советую почитать в документации на русском и английском

Reconnect в Rails, каков он?

Каково было мое удивление, когда однажды сервис на Rails прислал 1,5к сообщение о том что соединения потеряно, и при этом даже не попробовал повторно подключиться!!!

Как бы странно это не было, но раньше возможности автоматического переподключения не было… Пакеты просто бились об закрытое соединения, заполняя логи и бесконечно уведомляя разработчиков. Приходилось либо в ручную делать MonkeyPatch, либо каждый раз, при возникновении ошибки, перезапускать приложение(особо хардкорный путь).

MySQL поддерживает флаг повторного подключения в своих соединения. И в случае, если установлено значение true, клиент будет пытаться повторно подключиться к серверу, прежде чем выкинуть ошибку в случаее потерянного соединения.

И вот, начиная с версии Rails 2.3, у нас появился параметр reconnect. Теперь, чтобы получуть такое поведение из приложений Rails, достаточно установить флаг reconnect: true для подключений mysql в файле database.yml.

По умалчанию этот флаг равен false. Команда Rails cделала это затем, чтобы не нарушать работу текущих приложений, т.к. у данной опции есть существенный минус - ЭТО НЕ БЕЗОПАСНО ДЛЯ ВЫПОЛНЕНИЯ ТРАНЗАКЦИЙ . И в документации mysql об этом явно сказано:

Any active transactions are rolled back and autocommit mode is reset

А также указаны другие минусы. Опираясь на это руководство ActiveRecord с установленным флагом попытается подключиться всего один раз!

The MySQL client library can perform an automatic reconnection to the server if
it finds that the connection is down when you attempt to send a statement to the server to be executed. 
If auto-reconnect is enabled, the library tries once to reconnect to the server and send the statement again.

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

Один из способов сделать это - добавить патч к AR, который будет выполнять автоматический reconnect нужно количество раз через нужные нам интервалы.

module Mysql2AdapterPatch
def execute(*args)
# During `reconnect!`, `Mysql2Adapter` first disconnect
# and set the @connection to nil, and then tries to connect.
# When connect fails, @connection will be left as nil
# value which will cause issues later.
connect if @connection.nil?

begin
super(*args)
rescue ActiveRecord::StatementInvalid => e
if e.message =~ /server has gone away/i
try_reconnect
transaction_open? ? raise : retry
else
raise
end
end
end

private
def try_reconnect
sleep_times = [0.1, 0.5, 1, 2, 4, 8]

begin
reconnect!
rescue Mysql2::Error => e
sleep_time = sleep_times.shift
if sleep_time && e.message =~ /can't connect/i
warn "Server timed out, retrying in #{sleep_time} sec."
sleep sleep_time
retry
else
raise
end
end
end

def transaction_open?
transaction_manager.current_transaction.open?
end
end

require 'active_record/connection_adapters/mysql2_adapter'
ActiveRecord::ConnectionAdapters::Mysql2Adapter.prepend Mysql2AdapterPatch

В случае, если соединение будет потеряно, он будет пытаться повторить запрос и завершиться успешно когда сервер БД поднимется.

>> Post.count
(0.6ms) SELECT COUNT(*) FROM `posts`
Server timed out, retrying in 0.1 sec.
Server timed out, retrying in 0.5 sec.
Server timed out, retrying in 1 sec.
Server timed out, retrying in 2 sec.
Server timed out, retrying in 4 sec.
(1.1ms) SELECT COUNT(*) FROM `posts`
=> 0

Стоит отметить, что если разрыв соединения произошел в блоке транзакции, и затем восстановилось, то будут продолжаться выполняться следующие запросы, а все предыдущие запросы от начала транзакции до момента, когда соединение было отключено, будут проигнорированы. Вот почему с этим патчем в блоке transaction безопаснее повторно вызвать ошибку подключения.

Рассмотрим пример:

Post.transaction do
Post.create
sleep 5
Post.count
end

В данном случае, если при sleep 5 произошло успешное переподключение, то все равно будет вызвана ошибка подключения, т.к. в соответствии с документацией MySQL, описанной выше, Post.create выполнено не будет.

(0.3ms)  BEGIN
SQL (0.2ms) INSERT INTO `posts` (`created_at`, `updated_at`)
VALUES ('2017-01-18 20:18:14', '2017-01-18 20:18:14')
(0.2ms) SELECT COUNT(*) FROM `posts`
Server timed out, retrying in 0.1 sec.
Server timed out, retrying in 0.5 sec.
Server timed out, retrying in 1 sec.
Server timed out, retrying in 2 sec.
(0.1ms) ROLLBACK
ActiveRecord::StatementInvalid:
Mysql2::Error: MySQL server has gone away: SELECT COUNT(*) FROM `posts`