Автор Дэвид Рэгг

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

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

Пролог

Еще в феврале 2017 года Cloudflare обнаружила проблему безопасности, которая стала известна как Cloudbleed. Ошибка, стоящая за этим инцидентом, заключалась в некотором коде, который запускался на наших серверах для анализа HTML. В некоторых случаях, связанных с недопустимым HTML, синтаксический анализатор будет читать данные из области памяти за пределами анализируемого буфера. Смежная память может содержать данные других клиентов, которые затем будут возвращены в ответе HTTP, и результатом будет Cloudbleed.

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

Мы очень быстро предприняли меры по устранению Cloudbleed и таким образом прекратили сбои из-за этой ошибки, но это не остановило все сбои. Мы приступили к расследованию этих других сбоев.

Сбой - это не технический термин

Но что именно в данном контексте означает «крах»? Когда процессор обнаруживает попытку доступа к недействительной памяти (точнее, к адресу без действительной страницы в таблицах страниц), он сообщает ядру операционной системы об ошибке страницы. В случае Linux эти сбои страниц приводят к доставке сигнала SIGSEGV соответствующему процессу (название SIGSEGV происходит от исторического термина Unix «нарушение сегментации», также известного как ошибка сегментации или segfault). По умолчанию SIGSEGV завершает процесс. Это резкое прекращение работы было одним из симптомов ошибки Cloudbleed.

Эта возможность недействительного доступа к памяти и связанного с этим завершения в основном актуальна для процессов, написанных на C или C ++. Компилируемые языки более высокого уровня, такие как Go и языки на основе JVM, используют системы типов, которые предотвращают низкоуровневые ошибки программирования, которые могут привести к доступу к недействительной памяти. Более того, у таких языков есть сложные среды выполнения, которые используют преимущества ошибок страниц для уловок реализации, которые делают их более эффективными (процесс может установить обработчик сигналов для SIGSEGV, чтобы он не прерывался, а вместо этого мог восстановиться из ситуации). А для интерпретируемых языков, таких как Python, интерпретатор проверяет, что условия, приводящие к недопустимому доступу к памяти, не могут возникнуть. Поэтому необработанные сигналы SIGSEGV, как правило, ограничиваются программированием на C и C ++.

SIGSEGV - не единственный сигнал, который указывает на ошибку в процессе и вызывает его завершение. Мы также видели, что процессы завершаются из-за SIGABRT и SIGILL, что указывает на другие виды ошибок в нашем коде.

Если бы единственная информация, которая у нас была об этих завершенных процессах NGINX, была бы задействованным сигналом, исследовать причины было бы сложно. Но есть еще одна особенность Linux (и других операционных систем, производных от Unix), которая обеспечивает путь вперед: дампы ядра. Дамп ядра - это файл, записываемый операционной системой при внезапном завершении процесса. Он записывает полное состояние процесса на момент его завершения, что позволяет проводить посмертную отладку. Зарегистрированное состояние включает:

  • Значения регистров процессора для всех потоков в процессе (значения некоторых переменных программы будут храниться в регистрах)
  • Содержимое обычных областей памяти процесса (с указанием значений других переменных программы и данных кучи)
  • Описание областей памяти, которые являются отображениями файлов только для чтения, таких как исполняемые файлы и разделяемые библиотеки.
  • Информация, связанная с сигналом, вызвавшим завершение, например адрес попытки доступа к памяти, которая привела к SIGSEGV

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

После того, как дамп ядра был записан, его можно проверить с помощью инструмента отладки, такого как gdb. Это позволяет исследовать состояние из дампа ядра с точки зрения исходного исходного кода программы, так что вы можете узнать о программном стеке и содержимом переменных и кучи достаточно удобным способом.

Небольшое отступление: почему дампы ядра называются дампами ядра? Это исторический термин, появившийся в 1960-х годах, когда основной формой оперативной памяти была память на магнитных сердечниках. В то время слово ядро использовалось как сокращение для обозначения памяти, поэтому дамп ядра означал дамп содержимого памяти.

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

Но на наших серверах по-прежнему создавались дампы ядра - примерно по одному в день на всем нашем парке серверов. И найти первопричину этих оставшихся оказалось труднее.

Постепенно мы начали подозревать, что эти остаточные дампы ядра возникли не из-за ошибок в нашем коде. Эти подозрения возникли потому, что мы обнаружили случаи, когда состояние, записанное в дампе ядра, казалось невозможным на основе программного кода (и при изучении этих случаев мы не полагались на код C, а смотрели на созданный машинный код. компилятором, на случай, если мы столкнулись с ошибками компилятора). Сначала, когда мы обсуждали эти дампы ядра среди инженеров Cloudflare, был некоторый здоровый скептицизм по поводу идеи, что причина может лежать вне нашего кода, и была по крайней мере одна шутка о космических лучах. Но по мере того, как мы собирали все больше и больше примеров, становилось ясно, что происходит что-то необычное. Обнаружение еще одного загадочного дампа ядра, как мы привыкли их называть, стало обычным делом, хотя детали этих дампов ядра были разнообразны, и код, запускающий их, был разбросан по всей нашей кодовой базе. Общей чертой была их кажущаяся невозможность.

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

В поисках решения

Количество тайных дампов ядра было достаточно низким, чтобы существенно не повлиять на обслуживание наших клиентов. Но мы по-прежнему были привержены изучению каждого возникающего дампа ядра. Хотя мы стали лучше распознавать эти загадочные дампы ядра, их исследование и классификация потребовали больших затрат инженерных ресурсов. Мы хотели найти первопричину и исправить ее. Итак, мы начали рассматривать причины, которые казались несколько правдоподобными:

Посмотрели аппаратные проблемы. В частности, возможны ошибки памяти. Но наши серверы используют память ECC (код исправления ошибок), которая может обнаруживать и в большинстве случаев исправлять любые возникающие ошибки памяти. Кроме того, любые ошибки памяти должны регистрироваться в журналах IPMI серверов. Мы действительно наблюдаем некоторые ошибки памяти на нашем парке серверов, но они не были связаны с дампами ядра.

Если не ошибки памяти, то может быть проблема в аппаратном обеспечении процессора? В основном мы используем процессоры Intel Xeon разных моделей. Они имеют хорошую репутацию надежных, и хотя частота дампов ядра была низкой, казалось, что она может быть слишком высокой, чтобы ее можно было отнести к ошибкам процессора. Мы искали сообщения о похожих проблемах и спрашивали о них, но не слышали ничего, что могло бы соответствовать нашей проблеме.

Пока мы занимались расследованием, обнаружилась проблема с процессорами Intel Skylake. Но в то время у нас не было в производстве серверов на базе Skylake, и, кроме того, эта проблема была связана с конкретными шаблонами кода, которые не были обычным явлением для наших таинственных дампов ядра.

Может быть, дамп ядра неправильно записывался ядром Linux, так что обычный сбой из-за ошибки в нашем коде выглядел загадочным? Но мы не обнаружили никаких закономерностей в дампах ядра, которые указывали бы на что-то подобное. Кроме того, при необработанном SIGSEGV ядро ​​генерирует строку журнала с небольшим количеством информации о причине, например:

segfault at ffffffff810c644a ip 00005600af22884a sp 00007ffd771b9550 error 15 in nginx-fl[5600aeed2000+e09000]

Мы сравнили эти строки журнала с дампами ядра, и они всегда были согласованы.

Ядро играет роль в управлении Блоком управления памятью процессора для предоставления виртуальной памяти прикладным программам. Так что ошибки ядра в этой области могут привести к неожиданным результатам (и мы столкнулись с такой ошибкой в ​​Cloudflare в другом контексте). Но мы изучили код ядра и искали отчеты о соответствующих ошибках в Linux, но ничего не нашли.

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

Решение

Но в конце концов мы заметили кое-что важное, что упускали до этого момента: все загадочные дампы ядра исходили от серверов, содержащих Intel Xeon E5–2650 v4. Эта модель принадлежит к поколению процессоров Intel с кодовым названием Broadwell, и это единственная модель этого поколения, которую мы используем в наших пограничных серверах, поэтому мы просто называем эти серверы Broadwells. В то время Бродвеллы составляли около трети нашего флота, и они были во многих наших центрах обработки данных. Это объясняет, почему закономерность не сразу была очевидна.

Это понимание немедленно вернуло фокус нашего исследования на возможность проблем с аппаратным обеспечением процессора. Мы загрузили Обновление спецификаций Intel для этой модели. В этих документах с обновлением спецификаций Intel раскрывает все способы отклонения ее процессоров от опубликованных спецификаций, будь то из-за незначительных несоответствий или ошибок в оборудовании (Intel забавно называет эти ошибки).

В обновлении спецификаций описано 85 проблем, большинство из которых являются непонятными проблемами, интересующими в основном разработчиков BIOS и операционных систем. Но одно привлекло наше внимание: «BDF76 Процессор с поддержкой технологии Intel® Hyper-Threading может демонстрировать внутренние ошибки четности или непредсказуемое поведение системы». Симптомы, описанные для этой проблемы, очень широки («может произойти непредсказуемое поведение системы»), но то, что мы наблюдали, похоже, соответствовало описанию этой проблемы лучше, чем что-либо другое.

Кроме того, в обновлении спецификации указано, что BDF76 был исправлен в обновлении микрокода. Микрокод - это микропрограмма, которая контролирует работу процессора на самом низком уровне и может обновляться BIOS (от поставщика системы) или ОС. Обновления микрокода могут до некоторой степени изменить поведение процессора (насколько именно это является строго охраняемым секретом Intel, хотя недавние обновления микрокода для устранения уязвимости Spectre дают некоторое представление о впечатляющей степени, в которой Intel может перенастроить поведение процессора).

На тот момент наиболее удобным способом применить обновление микрокода к нашим серверам Broadwell было обновление BIOS от поставщика сервера. Но для развертывания обновления BIOS на таком большом количестве серверов в таком большом количестве центров обработки данных требуется некоторое планирование и время. Из-за низкой скорости тайных дампов ядра мы не узнаем, действительно ли BDF76 является основной причиной наших проблем, пока не будет обновлена ​​значительная часть наших серверов Broadwell. Последовала пара недель острого ожидания, пока мы ждали результата.

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

Как видите, после обновления микрокода наблюдается заметное снижение скорости дампов ядра. Но кое-какие дампы ядра все равно получаются. Это не загадки, это обычные проблемы в нашем программном обеспечении. Мы продолжаем изучать и исправлять их, чтобы убедиться, что они не представляют проблем с безопасностью в нашем сервисе.

Вывод

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

А для некоторых дампов ядра, которые мы видим сейчас, понять причину может быть очень непросто. Они соответствуют очень маловероятным условиям и часто связаны с первопричиной, далекой от непосредственной проблемы, вызвавшей дамп ядра. Например, мы видим сбои в LuaJIT (которые мы встраиваем в NGINX через OpenResty), которые возникают не из-за проблем в LuaJIT, а из-за того, что LuaJIT особенно подвержен повреждению структур данных из-за ошибок в несвязанном C-коде.

В восторге от детективной работы по дампу ядра? Или создание систем в масштабе, в котором проблемы, возникающие раз в десятилетие, могут возникать каждый день? Тогда присоединяйся к нашей команде.

Первоначально опубликовано на blog.cloudflare.com 18 января 2018 г.