В первой части своей истории я рассказал, что происходило в Контуре в момент, когда многие российские ИТ-компании попали в санкционные листы, как мы писали свой велосипед экспорт из Slack, и о том, как мы начали переезд в Mattermost. Во второй части, как и обещал, я расскажу вам самые болезненные и интересные грабли.

Глава 5. Грабли

Напомню, что мы хотели начать историю с импортом данных с эмодзи. По документации они выглядели очень просто — всего два поля! А еще они были нужны для импорта постов, а стандартный механизм вообще не умел их переливать. Следом по плану были каналы — они тоже довольно простые и нужные для импорта пользователей. Дальше шли пользователи и посты без вложений. И в самом конце мы учились импортировать посты уже вместе с вложениями.

Эмодзи

Написали первые двадцать строчек кода в наш конвертер, запустили импорт и сразу словили ошибку: Emoji.IsValid: Name must be 1 to 64 lowercase alphanumeric characters. Оказалось, что ограничения на названия эмодзи в Slack и в Mattermost очень даже разные. И, помимо ограничений на название эмодзи, при импорте были недопустимы кириллица и пробелы в пути к файлу с картинкой. И, конечно, в ошибке нет ни слова про то, какая именно эмодзи не прошла валидацию. А у нас их было больше трех тысяч! 

Поскольку Mattermost — это опенсорс, мы подсмотрели в код, написали валидацию на стороне нашего конвертера и стали отбрасывать те эмодзи, которые мы точно не могли импортировать. Всего их было около 100 штук и после ручного маппинга мы не смогли импортировать всего 8 штук — те, что использовались за 10 лет нашей истории со Slack меньше 20 раз. 

Каналы

Первое, с чем мы столкнулись – каналы в Slack и Mattermost тоже отличаются. В Slack создание канала выглядело так:

Создание канала в Slack. Шаг 1
Создание канала в Slack. Шаг 1
Создание канала в Slack. Шаг 2
Создание канала в Slack. Шаг 2

На первом шаге задаем имя канала, например, #csharp. На втором шаге задаем тип канала. И всё, канал создан. Остается только добавить участников и начать святые войны за код стайл.

А вот так выглядело окошко создания канала в Mattermost:

У канала тоже есть имя, правда по факту оно называется DISPLAY_NAME. При этом в ссылке еще будет NAME. И когда вы автоматикой ходите в API, вам нужен NAME. А когда вы в интерфейсе что-то делаете, вы обычно видите DISPLAY_NAME

Фанфакт: DISPLAY_NAME — неуникальный, а вот NAME — очень даже!

Получается, что в Slack у нас было одно поле, а теперь нам нужно два. Ограничения на поле NAME, которые выставлял Mattermost, опять же отличались от Slack. Например, нельзя было использовать кириллицу. А у нас было довольно много таких каналов!

Решили транслитерировать. Перегоняли название канала известным способом – через словарик по ГОСТ-7.79-2000. Но опять похожая ошибка: Channel.IsValid: Name must be 1 to 64 lowercase alphanumeric characters

Но у нас уже была регулярка, которую мы написали для валидации имени эмодзи: ^[a-zA-Z0-9+_-]{1,64}$. А раз ошибка та же, решили им воспользоваться. Запускаем и… Ничего не работает 😭 Пришлось снова смотреть вкод — нашли другую регулярку: ^[a-z0-9]+([a-z-_0-9]+|(__)?)[a-z0-9]*$

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

Но, как можно заметить, мы в спешке забыли про ограничение по длине — в нашей первой регулярке оно было, а в коде сервера, как оказалось, отдельный if был написан. Окей, добавили, импорт все равно падает! Мы добавили проверку длины названия канала до транслитерирования, при этом некоторые русские буквы при транслитерировании превращаются в несколько латинских, например, «я»  превращается в «ya». Правим, заливаем, запускаем… ура? Не ура – импорт упал с ошибкой, что такой канал уже есть. 

А какой канал есть-то? 😡 Опять забыли сказать. 

Пытались вручную бинпоиском по архиву и логам импорта найти, какие каналы залились, а какие — нет. Тщетно. Загрузка каналов шла в несколько потоков, в логах все вразброс, и вручную найти канал, на котором все упало, было просто нереально. Пошли за помощью к команде наших DBA, чтобы они включили DEBUG-логи в Postgres, и позже нашли, что дело в канале #openapi-kontur

Дело было вот в чем. Еще в 2022 году, когда мы только запускали нашу экспортилку из Slack, этот канал был приватным. А спустя какое-то время его сделали публичным, и оказалось, что при этом у него поменялся channel_id. Изменяемый идентификатор объекта… хммхмх…  При написании экспортилки мы на такое не рассчитывали. Хоть тогда и ничего не взорвалось, де-факто мы выгрузили историю канала дважды. А вот при импорте в Mattermost канал идентифицируется по NAME, а не по channel_id. И действительно получилось два одинаковых канала с идентичной историей. Дедлайны уже горели, переписывать экспортилку ой как не хотелось, поэтому прикрутили дедупликацию прямо в конвертер. 

Еще наткнулись на то, что заголовок (он же топик) пары каналов был больше 1024 символов — Slack такое допускал, а Mattermost — нет.

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

Пользователи

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

Membership

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

Дубли учеток

Со времен печенегов в наше пространство в Slack можно было регистрироваться и входить с разных почтовых доменов — у нас их два. Кто-то регался через @kontur, забывал об этом и в следующий раз использовал @skbkontur. Какое-то время мы так жили, а потом нормализовали все почты под один домен, а дубли заблочили. Но нужно было мигрировать всю историю за 10 лет, так что с этим тоже намучались.

Username не совпадает с User из Email

При регистрации в Slack проставляется username из почты, но его можно было поменять, и так сделали где-то 20 наших сотрудников. В Slack с этим не было проблемы, так как наши сервисы доверяли только email, по которому происходила верификация. А в Mattermost для импорта нужен именно username, который в нашем случае совпадал с доменным логином. 

Решили обрезать домен у почты и считать это за username. Пишем код, запускаем и… отпал локальный админ (эээээм), из под которого мы запускали импорт.

Смотрим в Slack и находим такое:

Скорее всего, это наши коллеги из отдела безопасности что-то тестировали и, судя по всему, автоматика отработала правильно – пользователь оказался заблокирован. Но нам же надо было перелить вообще всех пользователей и, когда мы запустили импорт, этот пользователь влился в нашего локального админа. Пришлось прикрутить валидацию и отбрасывать учетки с подобными почтами в отдельный список. Но импортировать их все равно нужно было, поэтому потом мы принудительно поставили таким пользователям почту вида username@example.com

Такую почту в нашем Mattermost честным способом получить было никак нельзя, так как почта с нашим правильным доменом проставляется при аутентификации. Поменять почту уже у существующего пользователя тоже нельзя – мы ограничили это настройками на сервере. Но из-за таких почт мы опять словили ошибку: UpdateUser: The email you provided does not belong to an accepted domain. Please contact your administrator or sign up with a different email.. В итоге на время импорта эту настройку пришлось отключить.

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

Посты

Первое, с чем мы столкнулись – различия в языке разметки: блоки кода, листы и так далее. Те самые 53 часа, про которые я упоминал в первой статье, при конвертации с помощью slack-advanced-exporter уходили именно на конвертацию всех постов. Причем, как оказалось, тот конвертер был однопоточный. 

Вот пара примеров отличий в разметке:

Slack

Mattermost

<#C53UMKYKD|infra_support> 

~infra_support

<@U3W72BL12|dstarasov>

@dstarasov

<!subteam^SKW266Z5K|@sentinel_duty>

@sentinel_duty

<https://tech.kontur.ru| Технологии в Контуре>

[Технологии в Контуре]        (https://tech.kontur.ru)

<!channel>, <!everyone>, <!here|@here>

@all, @channel, @here

А еще Bold, Italic, Strikethrough, Inline Code, Snippet, Quote, List… Почти всё было отлично друг от друга. 

Решили не изобретать велосипед и подсмотрели в реализацию утилиты mmetl. Реализация была такая: составляем массив пар, где первым значением записываем регулярку с шаблоном «что меняем», а вторым значением новое значение. По сути, у нас было чуть больше, чем 10 тысяч таких пар с регулярками.

А еще на тот момент в базе было экспортировано 12 миллионов сообщений и КАЖДОЕ сообщение нужно было прогнать по всему набору регулярок. А раз mmetl был однопоточный, то это и занимало те самые 53 часа. Учитывая возникающие в процессе конвертации и импорта грабли, мы не могли себе это позволить, ведь на вообще весь проект было чуть меньше месяца, а тут двое суток на одну попытку… 

Решили запараллелить. Попросили у команды виртуализации самый жирный физический сервер, который могли себе позволить. Нашлась новая свободная железка с 24 Core/48Gb RAM/2 TiB Storage. На этой железке получилось уложить конвертацию в 40 минут – это уже прям хорошо.

Инженер может бесконечно смотреть на три вещи: как горит прод, течет память и работают машины
Инженер может бесконечно смотреть на три вещи: как горит прод, течет память и работают машины

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

Когда затюнили базу и начали лить посты, сразу налетели на ошибки, связанные (внезапно) с эмодзи. Во-первых, нам попались эмодзи с модификатором. В Slack можно было выбирать цвет кожи у стандартных реакций с руками, например, с пальцем вверх: :thumbsup::skin-tone-4:. А в Mattermost такого не было (и нет до сих пор), поэтому пришлось добавить обработку и на лету отбрасывать все подобные модификаторы.

Во-вторых, список built-in реакций сильно отличался. Каких-то эмодзи вообще не было, у каких-то были другие названия. Повезло, что среди 6000+ эмодзи не совпали только ~100, так что я их смэтчил руками:

Посты + Вложения

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

Я за 10 дней до блокировки нашего пространства в Slack
Я за 10 дней до блокировки нашего пространства в Slack

Когда я открыл «картинку» в блокноте, оказалось, что это html-страница. Наш код, который качал файлы, считал, что если пришел статус «код 200» и в теле ответа что-то есть, значит это тот файл, что мы запрашивали. Как оказалось — нет… И на тот момент у нас было 500Gb и где-то 800 тысяч таких вот вложений:

screenshot_20220215_145203.png
screenshot_20220215_145203.png

Было очень-очень нервно, но мы успели перекачать все файлы буквально за трое суток и ничего не потерялось.

Глава 6. Финал

За неделю до отключения Slack мы мигрировали каналы, пользователей, эмодзи — все то, без чего нельзя было вечером закрыть Slack, с утра открыть Mattermost и продолжить работу. С постами мы немного не уложились в дедлайн: полностью историю переписки мы залили только спустя неделю после отключения нашего пространства в Slack. Готовый архив для импорта со всеми нашими данными весил около 600GB, и у нас не получалось его залить за один раз, поэтому мы заливали историю пачками по первой букве имени канала. Это заняло чуть больше недели.

Всего мы перенесли из Slack в Mattermost около 3 тысяч Custom Emoji, 5 тысяч каналов, 7 тысяч пользователей, тысячу User Groups, 82 тысяч «членств» в каналах, 6,5 млн корневы�� сообщений, 7,5 млн реплаев и 813 тысяч вложений.

Также для упрощения процесса полного переезда мы сделали редиректилку ссылок вида https://<your-namespace>.slack.com/archives/<channel> и https://<your-namespace>.slack.com/archives/<channel>/<thread_ts> в наш Mattermost. Выглядит она примерно так:

Миграция данных из Slack в Mattermost оказалась сложным, но выполнимым процессом, который потребовал значительных усилий как в техническом, так и в организационном планах. Мы столкнулись с огромным множеством подводных камней, лишь о небольшой части которых я повествовал в статье. Однако благодаря слаженной работе небольшой команды нам удалось успешно завершить проект в сжатые сроки, а полная история переписки, сохраненные вложения и рабочие ссылки сделали переход всей нашей разработки менее болезненным. 

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