LDAP-реплика Zimbra разъехалась: TLS, схемы и MDB_CORRUPTED

Строительная компания на Тверской, 45 рабочих мест, апрель 2022-го. Жалоба звучала так: «почта пускает через раз». У одних всё нормально, у других вход то проходит, то нет, а трое новых сотрудников не могли зайти вообще — при том, что ящики им завели неделю назад и в админке они были. В логах — почти пусто, и это оказалось самой информативной деталью во всей истории.

Симптом, который не похож на проблему каталога

Так это выглядит со стороны пользователя. Утром зашёл, днём выкинуло, пароль тот же, никто ничего не менял. Коллега за соседним столом работает нормально. Новый сотрудник не может войти вторую неделю, хотя ящик ему точно создали — вот он в списке.

Так это выглядит со стороны админа. В админской консоли всё в порядке. Учётка есть, пароль сбросили, попробовали ещё раз — иногда помогает, иногда нет. Логи молчат. Начинается охота на браузеры, кэши, мобильные клиенты и «а вы точно ту раскладку набираете».

Если у вас в почте больше одного узла с каталогом — а это любая установка, где кто-то когда-то поднял вторую ноду для отказоустойчивости, — держите в голове версию про рассинхрон реплики. Она объясняет главную странность: у одного пользователя работает, у другого нет, и это зависит не от пользователя, а от того, на какой узел его в этот раз приземлило.

У строителей было две ноды на Zimbra 8.8.15: мастер и реплика, поднятые сторонним подрядчиком в 2019 году. С тех пор в каталог /opt/zimbra/data/ldap никто не заглядывал ни разу.

Ложная версия: сработал встроенный фильтр по IP

Первое, о чём я подумал, услышав «пускает через раз», — про защиту от подбора паролей. В Zimbra есть встроенный механизм, который блокирует адрес после серии неудачных попыток входа, и жалобы от него выглядят один в один: человек ошибся паролем трижды, дальше его не пускают даже с правильным, а через какое-то время отпускает. В офисе все сидят за одним внешним адресом, так что один невнимательный сотрудник блокирует весь этаж.

Версия проверяется в один грep:

grep -c 'Access to IP suspended, for repeated failed login' /opt/zimbra/log/mailbox.log*
# 0
grep -i 'authentication failed' /opt/zimbra/log/mailbox.log | tail -5
# 2022-04-12 11:04:18,441 INFO  [qtp...] [name=petrov@sk-tverskaya.ru;] security -
#   cmd=Auth; account=petrov@sk-tverskaya.ru; error=authentication failed for [petrov@...],
#   account not found;

Ноль срабатываний фильтра. Зато нашлась формулировка, которая перевернула разбор: account not found. Не «неверный пароль», а «такой учётки нет». Сервер не отвергал пароль — он не находил пользователя.

Полчаса на ложную версию я всё же потратил и считаю их потраченными не зря: отсечь фильтр по IP надо было, иначе он висел бы фоном как «а вдруг ещё и это».

Считаем расхождение: сколько записей на каждом узле

Дальше — арифметика. Берём число учёток на мастере и на реплике и сравниваем. Пароль служебной учётки каталога лежит в локальной конфигурации.

su - zimbra
zmlocalconfig -s ldap_url ldap_master_url ldap_is_master
# ldap_url = ldap://mail2.sk-tverskaya.ru:389 ldap://mail1.sk-tverskaya.ru:389
# ldap_master_url = ldap://mail1.sk-tverskaya.ru:389
# ldap_is_master = false

PW=$(zmlocalconfig -s zimbra_ldap_password | awk '{print $3}')
for H in mail1 mail2; do
  N=$(ldapsearch -x -LLL -H ldap://$H.sk-tverskaya.ru:389 \
      -D uid=zimbra,cn=admins,cn=zimbra -w "$PW" \
      -b '' '(objectClass=zimbraAccount)' dn | grep -c '^dn:')
  echo "$H: $N учёток"
done
# mail1: 196 учёток
# mail2: 158 учёток

Тридцать восемь записей разницы. Цифра 196 при сорока пяти рабочих местах меня сначала смутила, но объяснение оказалось будничным: уволенных здесь не удаляли ни разу за три года — только отключали вход, а текучка на объектах большая; плюс полтора десятка ящиков объектов и бригад, общие адреса вроде smeta@ и snab@ и служебные учётки самой Zimbra. Живых людей с почтой — те самые 45.

Дальше смотрим метку синхронизации — она показывает, на каком моменте истории застряла реплика:

for H in mail1 mail2; do
  echo -n "$H "; ldapsearch -x -LLL -H ldap://$H.sk-tverskaya.ru:389 \
    -D uid=zimbra,cn=admins,cn=zimbra -w "$PW" -b '' -s base contextCSN
done
# mail1 contextCSN: 20220412083311.204518Z#000000#000#000000
# mail2 contextCSN: 20211118221407.913022Z#000000#000#000000

Реплика стояла с 18 ноября 2021 года. Почти пять месяцев. Всё это время половина запросов на аутентификацию уходила на узел, который не знал ни об одной учётке, созданной за зиму.

И ни одной жалобы до апреля — потому что зимой новых людей не нанимали. Как только в марте набрали бригаду на новый объект, посыпалось.

Причина первая и вторая: рукопожатие и молчащие схемы

В журнале каталога на реплике нашлась строка, которую никто не читал полгода:

grep -i 'slap_client_connect\|syncrepl' /var/log/zimbra.log | tail -3
# slap_client_connect: URI=ldap://mail1.sk-tverskaya.ru:389 Error,
#   ldap_start_tls_failed (-11)

Причина первая — рукопожатие TLS от реплики к мастеру не проходит. Настройка синхронизации на реплике требует обязательного шифрования канала, а договориться узлы не могут. Правится в конфигурации каталога: из описания синхронизации убирается требование обязательного TLS.

# где лежит конфигурация базы
ls /opt/zimbra/data/ldap/config/cn\=config/
# olcDatabase={2}mdb.ldif   (в старых сборках — {2}hdb.ldif)

grep -n 'starttls' /opt/zimbra/data/ldap/config/cn\=config/olcDatabase\=\{2\}mdb.ldif
# olcSyncrepl: {0}rid=100 provider=ldap://mail1... starttls=critical bindmethod=simple ...

Ключевое слово здесь critical: с ним неудачное рукопожатие означает «не подключаться вовсе». Убираете его — синхронизация пойдёт. Только помните, что вы при этом открываете канал между узлами, и если ноды стоят в разных сетях, лучше сначала разобраться с самим TLS, а не обходить его.

Причина вторая — отсутствующие схемы. Вот она и объясняет тишину в логах. Если на реплике нет схем, которые есть на мастере (обычно это nis и samba, доставленные когда-то под интеграцию с файловым сервером), реплика при старте молча игнорирует записи, которые от этих схем зависят. Не ошибка, не предупреждение — тишина. Запись просто не появляется.

for H in mail1 mail2; do
  echo "== $H"; ldapsearch -x -LLL -H ldap://$H.sk-tverskaya.ru:389 \
    -D uid=zimbra,cn=admins,cn=zimbra -w "$PW" \
    -b cn=schema,cn=config dn | grep -oE 'cn=\{[0-9]+\}[a-z]+'
done | sort | uniq -c

У строителей на мастере схем было на две больше — nis и samba, доставленные под старый файловый сервер ещё в 2019-м. Доставить их на реплику несложно — но учтите: после этого записи, пропущенные при прошлых стартах, сами не подтянутся. Нужен полный ресинк, о нём ниже.

Причина третья и четвёртая: пароль и окно журнала изменений

Третья — банальная. Пароль, под которым реплика привязывается к мастеру, разошёлся с тем, что мастер ожидает. Так бывает после смены пароля служебной учётки на одном узле без синхронной правки на втором. Проверяется прямой попыткой привязки от имени реплики:

RPW=$(zmlocalconfig -s ldap_replication_password | awk '{print $3}')
ldapwhoami -x -H ldap://mail1.sk-tverskaya.ru:389 \
  -D uid=zmreplica,cn=admins,cn=zimbra -w "$RPW"
# ldap_bind: Invalid credentials (49)

Если получили отказ — дальше искать нечего, пока пароли не сойдутся. Меняется штатной утилитой смены паролей каталога, и обязательно на обоих узлах.

Четвёртая причина самая неочевидная и именно она добила ситуацию у строителей. Репликация в Zimbra догоняет мастер по журналу изменений — это отдельная база, и записи в ней живут недолго. По умолчанию накладка чистит всё старше суток, а сам прогон очистки идёт раз в четыре часа.

Смысл в том, что реплика, отставшая на пять месяцев, догнать мастер по дельте не может физически: изменений за ноябрь в журнале давно нет. Даже если вы почините TLS, схемы и пароль, реплика подключится, попросит изменения с той точки, где остановилась, — и не получит их. Она останется в том же состоянии, но уже с рабочим соединением, что ещё обманчивее: вроде всё зелёное, а данных нет.

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

# на реплике, после того как связь и схемы приведены в порядок
su - zimbra -c 'zmcontrol stop'
mv /opt/zimbra/data/ldap/mdb/db /opt/zimbra/data/ldap/mdb/db.old-20220413
mkdir -p /opt/zimbra/data/ldap/mdb/db && chown zimbra:zimbra /opt/zimbra/data/ldap/mdb/db
/opt/zimbra/libexec/zmldapenablereplica
su - zimbra -c 'zmcontrol start'

Проверьте у себя за 2 минуты

Если у вас две ноды каталога, эта проверка отвечает на вопрос «а они вообще одинаковые». Из-под пользователя zimbra на любом узле.

PW=$(zmlocalconfig -s zimbra_ldap_password | awk '{print $3}')
for H in МАСТЕР РЕПЛИКА; do
  echo -n "$H  учёток: "
  ldapsearch -x -LLL -H ldap://$H:389 -D uid=zimbra,cn=admins,cn=zimbra -w "$PW" \
    -b '' '(objectClass=zimbraAccount)' dn | grep -c '^dn:'
  echo -n "$H  CSN: "
  ldapsearch -x -LLL -H ldap://$H:389 -D uid=zimbra,cn=admins,cn=zimbra -w "$PW" \
    -b '' -s base contextCSN | grep contextCSN
done
Что увиделиТрактовка
Число учёток совпадает, метки синхронизации отличаются на минутыНорма. Реплика догоняет мастер с небольшой задержкой
Метки отличаются на часыСинхронизация идёт, но с задержкой. Смотрите сеть между узлами и нагрузку на мастер
Метка реплики старше сутокДогнать по журналу изменений уже нельзя — записи за тот период вычищены. Нужен полный ресинк
Число учёток расходится, метки почти совпадаютПохоже на недостающие схемы: реплика молча пропускает часть записей и считает себя синхронной
Запрос к реплике вообще не отвечаетПроверьте, слушает ли она порт 389 и жив ли на ней процесс каталога
В логе есть ldap_start_tls_failed (-11)Рукопожатие между узлами не проходит. Синхронизация стоит с момента появления первой такой строки

Обе команды читают каталог только на чтение и идут на порт 389 — на работу почты они не влияют, выполнять можно днём. В однохостовой установке проверять нечего: реплики у вас нет. Но команду счёта учёток на мастере всё равно полезно знать: она пригодится при любой миграции, когда надо будет сверить, всё ли доехало.

MDB_CORRUPTED: почему на месте не чинится

Когда я остановил каталог на реплике, чтобы пересоздать её начисто, я по привычке сначала посмотрел состояние базы на мастере. И увидел то, из-за чего вся работа пошла в другую сторону:

mdb_stat -e /opt/zimbra/data/ldap/mdb/db
# Environment Info
#   Map size: 85899345920
#   Page size: 4096
#   Max pages: 20971520
#   Number of pages used: 48936
# ...
# mdb_stat: MDB_CORRUPTED: Located page was wrong type (-30796)

База каталога на мастере была повреждена. Работала, отвечала на запросы, отдавала учётки — и при этом уже была битой. Порча тянулась с февраля — с того момента, когда подрядчик впервые получил на этой базе ошибку и полез её «чинить»; чем именно он это делал, расскажу через два абзаца. Заодно обратите внимание на первые строки вывода, потому что на них регулярно пугаются. Map size: 85899345920 — это не занятый объём, а зарезервированная карта памяти, 80 ГиБ по умолчанию. Файл data.mdb из-за неё с первого дня показывает в ls -l все восемьдесят гигабайт, хотя занимает на диске несравнимо меньше. Сколько занято на самом деле, видно строкой ниже: 48 936 страниц по 4 КБ — около 190 МБ. Каталог на две сотни учёток столько и весит; его размер зависит от числа записей, а не от объёма почты.

Главный факт, ради которого стоит дочитать эту статью до конца: эта база не ремонтируется на месте. Нет утилиты, которая пройдёт по ней и починит страницы. Есть только выгрузка содержимого и загрузка в новое, чистое хранилище — либо восстановление из резервной копии.

И тут начинается самое опасное, потому что в интернете лежит совет, который делает хуже. Совет — прогнать db_recover. Инструмент реальный, он есть в поставке, он лежит в каталоге Zimbra, и на форумах его рекомендуют с 2010 года. Только он относится к другому движку базы данных, который использовался в старых версиях. На нынешнем движке он в лучшем случае не делает ничего, а в худшем — трогает служебные файлы, после чего восстановление усложняется.

Ровно это и произошло у строителей до меня. Их подрядчик в феврале получил ошибку, нашёл рецепт, прогнал восстановление чужого движка, удалил файлы блокировок «чтобы освободить базу» и получил ещё более неприятное состояние. Разбирать после него было тяжелее, чем было бы сразу.

Порядок восстановления, который работает

Последовательность одна, и отклоняться от неё не стоит. Выполняется на узле с повреждённой базой, службы должны быть остановлены.

# 1. останавливаем всё
su - zimbra -c 'zmcontrol stop'
ps -u zimbra -o pid,comm | grep slapd   # убедиться, что процесса нет

# 2. холодная копия ДО любых действий — на случай, если дамп выйдет неполным
tar czf /backup/ldap-broken-20220413.tgz /opt/zimbra/data/ldap

# 3. выгружаем то, что ещё читается, в текстовый формат
mkdir -p /tmp/ldapdump && chown zimbra:zimbra /tmp/ldapdump
su - zimbra -c '/opt/zimbra/libexec/zmslapcat /tmp/ldapdump'
wc -l /tmp/ldapdump/ldap.bak
# 38456 /tmp/ldapdump/ldap.bak

# 4. убираем битую базу и создаём пустую
mv /opt/zimbra/data/ldap/mdb/db /opt/zimbra/data/ldap/mdb/db.corrupted
mkdir -p /opt/zimbra/data/ldap/mdb/db
chown zimbra:zimbra /opt/zimbra/data/ldap/mdb/db

# 5. загружаем дамп в чистое хранилище
su - zimbra -c '/opt/zimbra/libexec/zmslapadd < /tmp/ldapdump/ldap.bak'

# 6. поднимаем и проверяем
su - zimbra -c 'zmcontrol start'
su - zimbra -c 'zmprov -l gaa | wc -l'
# 196

Три места, где эту процедуру ломают.

Первое — пропускают шаг 2. Выгрузка с повреждённой базы может выйти неполной: часть страниц не читается, и в дампе не окажется куска дерева. Если оригинал уже переименован и затёрт, повторить попытку другим способом будет не с чем. Холодная копия каталога данных — это двадцать секунд и полная страховка.

Второе — загружают дамп в старую директорию. Если не убрать битую базу, а просто залить поверх, вы получите смесь: часть страниц старая, часть новая, состояние — непредсказуемое. Я сам чуть не сделал этот шаг на автомате в час ночи, остановился на полпути.

Третье — сверяют результат на глаз. После загрузки обязательно посчитайте учётки и сравните с тем, что было на мастере до аварии, и с вашим списком сотрудников. У строителей после восстановления получилось 196 — ровно столько, сколько показывал счёт до начала работ. Если бы вышло 193, я бы искал, каких трёх не хватает, а не радовался, что «база поднялась».

И только после того, как мастер приведён в порядок и проверен, имеет смысл пересоздавать реплику с него. В обратном порядке вы аккуратно растиражируете повреждение на второй узел — что, собственно, я и собирался сделать в самом начале, когда думал, что проблема только в реплике.

Итог, деньги и что забрать с собой

По времени вышло так. Разбор и диагностика — 4 часа, включая 35 минут на ложную версию про фильтр по IP. Восстановление базы на мастере — 2 часа 10 минут, из них сама загрузка дампа заняла 38 минут. Пересоздание реплики с восстановленного мастера — 1 час 5 минут, полная синхронизация 196 учёток прошла за 12 минут. Плюс утро следующего дня на проверку: вход всех проблемных пользователей, рассылка, мобильные клиенты.

Работали в ночь со среды на четверг, почта была недоступна 2 часа 40 минут. Мои работы — 9 часов, счёт 44 000 ₽.

Что эта история стоила клиенту до моего появления. Пять месяцев трое-четверо новых сотрудников в среднем не имели рабочей почты по неделе-две каждый: переписка шла через личные адреса, часть документов по объектам ушла мимо корпоративного архива. Сметчик, которому не завели доступ вовремя, две недели получал коммерческие предложения на личную почту — потом это выясняли отдельно, когда сверяли цены с поставщиками. Прямых убытков никто не считал, но три дня работы бухгалтерии и снабжения на восстановление переписки — это порядка 30 000 ₽, и это самая мягкая оценка.

Что забрать с собой, если у вас две ноды каталога.

Тишина в логах — не признак здоровья. Самый неприятный вид рассинхрона именно молчаливый: реплика считает себя исправной и просто не показывает часть записей.

Сравнение двух чисел важнее любого дашборда. Количество учёток на мастере и на реплике плюс метки синхронизации — две команды, которые ловят проблему за минуту. Их стоит поставить в ежемесячную проверку.

Битую базу каталога не чинят на месте. Ни утилитой от другого движка, ни удалением файлов блокировок. Только выгрузка в текст, чистое хранилище, загрузка обратно — и обязательно холодная копия до первого действия. Попытка ремонта на месте — тот случай, когда каждый следующий шаг уменьшает шансы вытащить данные, а понимаешь это уже задним числом.

Если хотите проверить свою пару узлов — пришлите мне вывод двух команд из блока самопроверки: счёт учёток и метки синхронизации с обоих узлов. За день отвечу, синхронны ли они, чем лечится расхождение в вашем случае и нужен ли полный ресинк или хватит правки конфигурации.

Частые вопросы

Почему в логах ничего нет, если реплика отстала на месяцы?

Потому что молчание — свойство двух из четырёх причин. При отсутствующих схемах реплика при старте просто пропускает записи, которые от них зависят: это не ошибка с точки зрения каталога, а штатное поведение. При исчерпанном окне журнала изменений реплика подключается успешно, запрашивает изменения с точки своей остановки и не получает их — тоже без ошибки. Заметно только два случая: неудачное рукопожатие TLS, оно даёт строку про ldap_start_tls_failed, и неверный пароль привязки. У строителей на Тверской в логе была ровно одна строка за пять месяцев, и на неё никто не смотрел.

Можно ли починить повреждённую базу каталога, не останавливая почту?

Нет. Выгрузка требует остановленного каталога, иначе вы получите дамп в несогласованном состоянии, а именно согласованность вам и нужна. Планируйте окно: у меня на 196 учётках выгрузка, пересоздание хранилища и загрузка заняли 2 часа 10 минут, из которых сама загрузка дампа шла 38 минут. На каталоге в тысячу с лишним учёток закладывайте больше. Хорошая новость в том, что размер каталога зависит от числа учёток и настроек, а не от объёма почты: терабайт писем на длительность этой операции не влияет.

Нам посоветовали прогнать db_recover. Это поможет?

Скорее навредит. Этот инструмент относится к движку базы данных, который использовался в старых версиях Zimbra; современные версии хранят каталог на другом движке. В лучшем случае команда не сделает ничего, в худшем — тронет служебные файлы и усложнит последующую выгрузку. Совет живёт на форумах с начала десятых годов и до сих пор всплывает первым в поиске. У моего клиента подрядчик именно так и поступил за два месяца до меня: прогнал восстановление чужого движка, потом удалил файлы блокировок вручную — и разбирать после этого было заметно тяжелее.

Что делать раньше: чинить мастер или пересоздавать реплику?

Сначала мастер, всегда. Пересоздание реплики забирает данные с мастера — если мастер повреждён, вы аккуратно скопируете повреждение на второй узел и вместо одной проблемы получите две. Я сам собирался начать с реплики и остановился только потому, что по привычке посмотрел состояние базы на мастере перед работами. Там и лежал MDB_CORRUPTED. Порядок такой: проверить состояние базы на обоих узлах, восстановить мастер через выгрузку и загрузку, проверить число учёток, и только затем пересоздавать реплику.

Как часто проверять синхронность реплики, чтобы не повторилось?

Раз в месяц достаточно, если проверка автоматическая, и раз в неделю — если руками, потому что руками её быстро забрасывают. Проверяются два показателя: число учёток на обоих узлах и метки синхронизации. Расхождение по числу при совпадающих метках означает пропущенные записи, разница в метках больше суток означает, что догнать по журналу изменений уже нельзя. Ставьте это в ежемесячный регламент рядом с проверкой места на дисках и сроков сертификатов — пять минут на всё, а ловит аварию за месяцы до того, как она станет заметна пользователям.

Нужна помощь с проектом?

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#Zimbra#LDAP#репликация#восстановление#авария
Комментарии 0

Оставить комментарий

загрузка...

Подпишитесь на рассылку ITfresh

Раз в неделю — практические гайды для руководителя IT и сисадмина: безопасность, 1С, миграции, резервные копии, лайфхаки из реальных проектов.

Реквизиты оператора персональных данных

ООО «АЙТИ-ФРЕШ», ИНН 7719418495, КПП 771901001. Юридический адрес: 105523, г. Москва, Щёлковское шоссе, д. 92, корп. 7. Контакт: info@itfresh.ru, +7 903 729-62-41. Оператор обрабатывает e-mail подписчика в целях рассылки информационных и рекламных материалов до момента отзыва согласия.