Как кодировка файла сломала EMR Spark
История о том, как изменение кодировки файла сломало Spark в EMR и весь пайплайн.
Начну с предыстории :)
У нас имеется один клиент у которого есть огромные пайплайны, которые включают в себя:
- API Gateway
- Тонны Lambdas
- StepFunctions
- EMR’ы со спарком для преобразования сотни гигабайт данных в
parquet
файлы - SSM Run Commands для создания Redshift таблиц для чтения
perquet
файлов.
Все это приправлено десятками SQS, Firehouse, ElasticSearch.
Как работает пайплайн
Каждый понедельник тригерится лямбда, которая запускает EC2 для скачивания очень ОЧЕНЬ много данных на S3 бакет.
Как только в бакете появляются файлы - тригерится EMR для преобразования файлов parquet
файлы.
Ну а дальше они обрабатываются другими EMR’ами и сервисами, которые выполняют не менее важную работу, что бы потом мы могли зайти на сайт и увидить уже преобразованные данные которые вытягиваются из Redshift через API Gateway с лямбдами.
Что сломалось
В один прекрасный день, один файл (размером в 450Mb), который обрабатывал Spark изменил свою кодировку из us-ascii
на utf16le
.
Возможно вы скажете, что вроде ничего не поменялось, но нет, при попытке открыть его у вас в некоторых местах появляются символы ����.
Мало того, с такой кодировкой у вас перестают нормально работать команды sed
и gsub
Сейчас покажу на примере.
Для начала давайте убедимся, что за кодировка файла, для этого выполняем команду file -i NAME
Данный файл имеет следующий вид:
A'~'1111111'~'1111111'~'2021-02-17'~''~'Secret name 1'~'#@#@#A'~'2222222'~'2222222'~'2021-02-18'~''~'Secret name 2'~'#@#@#A'~'33333333'~'33333333'~'2021-02-19'~''~'Secret name 3'~'#@#@#
Где '~'
- делимитер а #@#@#
- терминатор
И так, начнем.
EC2 скачивает данный файл и помещает его на S3, при этом делает замену делиметра и терминатора на |
и \n
соответственно. И для этого используется команда (Этот код писался еще до нас)
count_lt=$(head -c 10000000 "${FOLDER}"/"${file}" | awk "{print gsub(/'~'/, \"\")}")
count=$(echo $count_lt | cut -d " " -f1)
[[ $count != 0 ]] && line_terminator="#@#@#" || line_terminator="\\\n"
[[ $count != 0 ]] && field_delimiter="'~'" || field_delimiter="|"
Он получает кусок данных, ищет там '~'
и если количество найденных данных > 0 - тогда идет замена.
Давайте запустим эту команду на файле с кодировкой utf16le
и us-ascii
UTF16LE:
US-ASCII:
А все, что я сделал - это просто запустил iconv
и поменял кодировку.
Кстати, если использовать в коде выше tr -d '\000' | grep -o "'~'" | wc -l
, то получим следующее
На следующем шаге EMR пытаясь конвертнуть файл в parquet
вываливался со странными ошибками
Input row doesn't have expected number of values required by the schema
Где количество полей должно было = 6, но он находит то 10, то 3.
Так как файлу присваивались не правильный делимитер и терминатор, Spark начинал искать в файле пайп |
вместо '~'
, а так как в тексте было много пайпов - он принимал их за делимитер и ругался, что количество полей не совпадает.
В итоге фикс выглядел следующим:
- EC2 получает файлы и проверяет кодировку.
- Как только кодировка не
us-ascii
илиutf-8
- запускается командаiconv
которая преобразовывает файл.
Самое забавное, что теперь, EMR стал обрабатывать пул файлов не за ~4 часа а за ~2. Что сократило стоимость пайплайна на 30$ только за один прогон EMR’а.
В заключение хочу сказать, что порой обычная кодировка файла может поломать вам весь пайплайн. Так что, если что-то сломалось и вы не понимаете что - сделайте file -i NAME
и возможно это сэкономит вам кучу времени.