[Перевод] Всё плохо

image


Что ж, всё плохо. Немного забавно так говорить: на конференции (Web à Québec) было много разговоров об удивительном будущем и вещах, возможных благодаря новым технологиям. О новых средствах и устройствах, которые должны сделать нашу жизнь проще. Мои знакомые знают, что у меня обычно очень циничный взгляд на технологии; лично я боюсь всех этих умных устройств, которые реагируют на мои слова, чем восхищались другие спикеры.


В основном потому, что чем больше времени я трачу на программирование и провожу в этой отрасли, тем больше узнаю, как всё работает изнутри, и тем меньше доверия всё это мне внушает. Я подобрал изображение для слайда. Это картина «Триумф смерти» Питера Брейгеля. В некоторой степени она раскрывает моё отношение к «умному дому».


image


Я хочу показать, что имея даже очень простое приложение, выглядящее очень и очень разумно, и показать множество проблем и потенциальных ошибок, которые могут скрываться в нём и неприятно удивлять нас. Я хочу продемонстрировать, что сложно чувствовать себя по-настоящему в безопасности, когда дело касается кода. Это будет страшная история!


image


Чтобы раскрыть свою точку зрения, я начну с базового приложения, знакомого любому разработчику в этом зале. Здесь у меня маленькое веб-приложение. Справа — пользователи, они с помощью своих устройств подключаются к моему фронтенду, исполняющему код, написанный на некотором языке и отвечающий за логику приложения. Там происходит подключение к базе данных, где хранится информация. Далее — cron, он нужен для ряда фоновых задач, возможно, не особо сложных. Затем идёт облако, которое, вероятно, используется множеством других составляющих системы. Может быть, вы храните изображения в S3 или что-то ещё.


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


image


Такая схема безопаснее. Теперь у меня есть дополнительный фронт, и я могу подключать или отключать любой из них и по-прежнему оставаться доступным для клиентов. Второй узел базы данных даёт нам возможность аварийного переключения в случае возникновения проблем, но это не резервное решение. Достаточно всего одного человека, DROP TABLE users и репликацией в реальном времени, чтобы удалить таблицу «Пользователи» и потерять этот горячий резерв. Так что давайте скажем, что мы поступили разумно и создали резервную копию в этом облаке.


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


image


Вот несколько абстракций.


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


Дальше у нас идут идентификаторы. Я считаю их абстракциями. Они могут быть вашими автоинкрементными идентификаторами в базе данных, UUID или GUID, а также переменными, указателями, URL или URI. Они в основном позволяют ссылаться на элемент, часть данных или объект без необходимости его полного описания. Обычно идентификаторы связаны контекстом, который придает им смысл. Я могу сказать: «Ты Джон», и люди, которые знают конкретного Джона, будут иметь представление о том, кто такой Джон, основываясь на своём знакомом, на таких показателях, как его рост, профессия, возраст и т. д. Идентификаторы позволяют нам определить то, что поддерживает весь истинный элемент.


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


Внизу слева у нас сеть. Это всё просто абстракции. Ваши соединения, упорядоченные потоки данных, IP-адреса и номера портов (которые также являются идентификаторами!), пакеты и т. д. К счастью, мы не просто передаём электрические сигналы по кабелю.


Далее идёт время. И здесь я не буду его описывать. Философы, метафизики и учёные тысячелетиями спорили, чтобы приблизиться к тому, что мы сегодня понимаем под этим словом.


И далее у нас строки, формат для всего остального, что мы не знаем, как изобразить.


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


image


Начнём с простых чисел с плавающей запятой. Если вы когда-нибудь видели сумму 0,1 + 0,2, равную 0,30000000000000004, вы знаете, что это такое. Существует бесконечность чисел от 0,1 до 0,2, но у нас есть ограниченное количество байтов для их представления. Некоторые цифры не делятся нацело, и без работы с дробной частью будет очень сложно не потерять точность, когда компьютер начнёт подменять цифры, чтобы они приобрели смысл.


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


Я слышал об одном проекте, в котором банк пытался транслировать свои старые базы кода с помощью node.js. К несчастью для команды, никто не сказал им о том, что в JavaScript есть только числа с плавающей запятой, и команде пришлось отказаться от проекта.


Кстати говоря, языки с флоатами типа JavaScript имеют верхний предел точности целых чисел, в данном случае 253.


image


С целыми числами немного проще. Люди по большей части лучше понимают ограничения, что помогает. Но этого недостаточно: нужна правильная структура и тип использования, поскольку не все целые числа означают одно и то же. Вспомните о Mars Climate Orbiter, который потерпел неудачу, потому что одни части кода работали в имперских единицах, а другие — в метрической системе. Упс.


На изображении показан особый взгляд на эти запутанные единицы. Спросите Siri или Wolfram|Alpha, сколько китайских королей на одном квадратном метре, и вы получите 1,628 × 10–4. Не совсем уверен, что это значит, но каждый раз это достоверно определяется.


image


Ещё интереснее, когда добавляется языковая обработка. Я не понимаю почему, но на вопрос «Кто король Соединенных Штатов?» получаем ответ »1,57 миллиарда китайских королей».


Как правило, подобную ошибку довольно просто предотвратить. Будьте осторожны и, если имеете дело с системой причудливого типа, используйте её в полной мере. Не гонитесь за «целым» для всех своих типов; укажите, что представляют собой целые числа, какова их единица.


image


Допустим, что моё приложение отлично справляется со своей избыточностью и так же отлично справляется с манипуляцией цифрами. Возможно, у меня даже появились пользователи. Теперь мне необходимо обратить внимание на такую полезную вещь, как шардинг, или, может быть, я просто буду работать с внешними сервисами. В любом случае я, скорее всего, стану использовать UUID или GUID, поскольку они непредсказуемы и могут «гарантировать» уникальность.


UUID — это всего лишь группа битов, которые вы сравниваете. Если они все сошлись, то идентификатор прошёл проверку. Если нет, тогда он другой. Там множество разных вариантов, какие-то случайные, какие-то основаны на времени или адресе.


Как мы можем видеть, UUID — это то, что расположено на вершине слайда, с шестнадцатеричными числами, разделёнными дефисами. Это немного рискованно.


image


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


В этом заключается риск. По умолчанию у большинства строк в большинстве языков есть операторы сравнения, чувствительные к регистру. Значит, хотя эти три UUID идентичны в своём аутентичном двоичном представлении, они не будут сравниваться как равные строки. Чувствительность к регистру существует в любом случае и обязательно должна учитываться вашей системой, так как строковое представление не является идеальным абстрактом истинных свойств UUID.


image


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


Для определённых языков требуются особые библиотеки, одна часть которых всё сделает правильно, а от другой нельзя ждать того же. Базы данных могут быть сложными:


  • Используя PostgreSQL, вы получаете нечувствительное к регистру сравнение, но представление в нижнем регистре.
  • MySQL поддерживает создание UUID верхнего регистра, но не имеет формата хранения для них. Рекомендуется выбирать чувствительный к регистру VARCHAR.
  • MSSQL может хранить GUID, но подходит для представления в верхнем регистре.
  • Oracle может хранить необработанные данные, так что здесь не должно возникнуть проблем.
  • Redis не поддерживает UUID, и, скорее всего, вам придётся использовать строку с учётом регистра.
  • MongoDB всё делает правильно, с двоичным представлением.

Тем не менее даже если вы со своей стороны делаете всё безошибочно, нет никакой гарантии, что система будет работать. Достаточно всего одной подсистемы, чтобы всё разрушить. Например, внешние службы, такие как некоторые сервисы Amazon, имеют множество различных предложений, которые не всегда согласуются друг с другом (например, DynamoDB и SimpleDB чувствительны к регистру, поэтому третий сервис AWS, основывающийся на них, скорее всего, унаследует эти свойства). Если подобное произойдёт с вами, если вы, скажем, используете PostgreSQL локально и храните UUID в качестве UUID, то ничего хорошего из этого не выйдет. Теоретически объекты могут начать конфликтовать друг с другом или просто исчезнуть, поскольку ваше локальное представление нижнего регистра не существует для сервиса верхнего регистра.


Любая часть системы, действующая не так, как остальные, может вызвать проблемы. Вам придётся хранить канонический UUID (для сравнения) с его строчной копией (для его исходной версии) или изучать, как каждый сервис хранит материал. Не очень весело.


image


Однако сравнение строк может привести к большим неприятностям. Ещё одно интересное свойство сравниваемых строк (не только строк, но также большинства массивов и структур данных) заключается в вашем желании, чтобы операция проходила как можно быстрее.


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


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


Собственно говоря, это атака по времени. Хотя существует гораздо больше их разновидностей.


image


Эту проблему можно решить, заменив оператор сравнения строгой дизъюнкцией. Установите значение false, а затем проведите побитное или побайтное сравнение с XOR. Каждый раз, когда оба значения одинаковы, выдаётся 0 (false), и каждый раз, когда значения отличаются, выдаётся 1 (true). ИЛИ приводит к исходному значению, и если в итоге это не 0, то вы знаете, что есть разница.


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


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


image


Конечно, некоторые устройства принимают это слишком близко к сердцу, и их медлительность на самом деле необязательна!


image


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


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


image


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


image


И что же? Все мои действия возвращены. Чёрт, это даже быстрее, чем раньше!


Тем не менее есть некоторая проблема.


image


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


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


image


На самом деле, если работа не выполняется тщательно, события могут повторяться. Вот пользователь Reddit, который приобрёл игру за 12,74 доллара. Он купил её однократно, но получил запрос об оплате множество раз, пока на его банковском счёте не образовалось превышение кредита в –93 тыс. долларов.


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


image


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


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


image


В итоге очередь переполняется и вылетает. Конечно, выходит из строя не только она.


image


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


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


image


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


image


В следующий раз, когда произойдёт ошибка, мы сможем сделать то же самое.


image


Все эти очереди — хорошее решение. Но мы крутая крупная компания, и поэтому нам нужна технология следующего поколения. Все мы знаем, что эта архитектурная схема ужасна. Итак, мы внедряем нечто особенное…


image


И это микросервисы!


image


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


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


Конечно, это ещё не всё! Все наши компоненты должны уметь взаимодействовать друг с другом.


image


Мы могли бы выбрать что-то вроде JSON. Это диаграмма из seriot.ch, отслеживающая около 50 реализаций библиотек JSON на более чем десяти языках.


Почти каждая из них обрабатывает информацию по-своему.


image


Это не очень хорошо.


image


Стандарт JSON очень мал, но это, вероятно, означает, что он предоставляет пространство для интерпретации и, следовательно, для особого нестандартного поведения.


Самое интересное: если запустить несколько микросервисов и отправить всем им одинаково тщательно обработанный набор данных, это может привести к противоречивому поведению. Возьмём, к примеру, неверный JSON со множеством одинаковых ключей, имеющих разные значения. Скажем, у меня есть объект «человек», где атрибут «имя» появляется дважды, первый раз это «Марк», второй раз — «Джон».


Скорее всего, анализатор поведёт себя одним из трех способов:


  1. Откажется от разбора записи, как и должно быть.
  2. Сохранит первое имя (Марк).
  3. Сохранит второе имя (Джон).

Выбор последних двух способов зависит от порядка анализа (с использованием стека или по очереди).


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


image


Ещё одна проблема связана со временем. Различные часы работают на разных скоростях. В прошлом году я проводил эксперимент для презентации по календарям (нет, не смейтесь, календари на самом деле очень интересны), во время которого я отключил синхронизацию часов на своём компьютере. Затем я проделал то же самое с духовкой и микроволновой печью, чтобы посмотреть, где будет наибольшее отклонение. Где-то через три-четыре недели время на микроволновке сдвинулось примерно на 3 минуты, на ноутбуке — примерно на 2 минуты и 16 секунд, а на духовке — не изменилось.


Часы отклоняются в зависимости от аппаратного обеспечения, температуры, напряжения, влажности и т. д. Они не могут оставаться точными долгое время.


image


Значит, без синхронизации временные метки для одного и того же события могут разделиться и привести к разрозненным результатам. Чтобы этого избежать, необходим NTP. Вы можете проследить за временем и убедиться, что оно незначительно отклоняется. Тем не менее для событий, которые происходят очень быстро, даже у NTP может появиться достаточно вариаций, чтобы вызвать проблемы.


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


В судьбоносный день, когда транзакция выполнялась особенно быстро, NTP-синхронизация компьютерных часов привела к отклонению на какие-то доли секунды: вполне достаточно, чтобы поставить вторую временную метку перед первой. Для полного счастья это случилось примерно в полночь в последний день месяца или в какой-то другой оборот цикла.


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


Хитрость здесь состоит в том, чтобы создать значительное различие между монотонным и системным временем.


image


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


В данном случае клиент может отправить запрос, который переведёт его на первый сервис, затем на второй и на третий. Допустим, сервис подтверждает покупку товара и пересылает уведомление об этом в интерфейс. После третий сервис сразу же отправляет заказ на четвёртый сервис, который, скажем, подтверждает доставку или что-нибудь в этом роде. Он также отправляет уведомление в интерфейс.


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


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


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


image


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


image


Но это ещё не всё. Присутствующие здесь наверняка знакомы с часовыми поясами. Но кто-нибудь здесь слышал про пояса в полчаса?


(Большинство людей поднимают руки.)


Да, такой пояс расположен в Ньюфаундленде. Это внутри страны, хорошо, что вы знаете.


Но опять же, кто знает о существовании часового пояса в четверть часа? Подобный находился в Новой Зеландии. Или что в Либерии был зафиксирован 44-минутный? Может быть, некоторые здесь слышали, что в 1927 году в часовых поясах в Китае произошли отклонения в пять минут и несколько секунд?


Но и это ещё не всё. Не всегда изменения в часовых поясах незначительны. Например, острова Самоа существуют между UTC –11 и UTC +13, полностью переключая дни. Хитрость в том, что они находятся на линии изменения даты. В 1892 году острова решили согласовать свои календари с США, исходя из рыночных соображений. В том году у них дважды было 4 июля. Первое июля, второе июля, третье июля, четвёртое июля, четвёртое июля, пятое июля…


И опять же в 2011 году (совсем недавно!) они согласовали календари с Китаем, и на этот раз на островах Самоа не было 31 декабря.


К тому же существует летнее время. Если вы знакомы с бразильскими сисадминами, вы, возможно, слышали о таком забавном факте (я не знаю, делают ли они так до сих пор), что каждый регион/провинция/штат в Бразилии ежегодно голосует за то, будут ли они придерживаться DST. Невозможно узнать заранее, надо ли обновлять файлы часовых поясов.


image


Далее, високосные секунды. Проблемы часовых поясов решит просто UTC. Но это имеет весьма интересные последствия, поскольку каждый тип всё ещё использует временные метки UNIX и свободно конвертируется между UTC и эпохой UNIX, которая начинается 1 января 1970 года.


Это связано с тем, что UTC учитывает високосные секунды, которые часто корректируются: добавляются или удаляются, основываясь на вращении по земной орбите, чтобы синхронизировать часы с реальным миром. С 1970 года мы находимся в сети 27-секундных смещений. Эпоха UNIX в наших системах не учитывает эти секунды.


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


Моё личное предположение, почему это произошло (и я надеюсь, что я прав), связано с несоответствием временных представлений. Если вы используете целое число без знака — опять эти надоедливые целые числа — для представления времени, начинающегося 1 января 1970 года, в качестве эпохи, но с помощью преобразования UTC для установки даты и времени, вы получите 27, что невозможно считать целым числом без знака. Вместо желаемого результата произойдёт потеря значимости. При 32 битах устройство выдаст нам 2106 год, которого может просто не существовать в данном программном обеспечении, и это опять приведёт к поломке.


image


Но это ещё не всё. Далеко не всё. Здесь, в Квебеке и Северной Америке, мы используем григорианское время. Кто-нибудь из присутствующих допускал ошибки в коде, связанные с високосными годами? (Многие поднимают руки.)


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


  • Бенгальский календарь. Солнечный, разделён на шесть сезонов по два месяца в каждом.
  • Китайский календарь. Довольно запутанный лунно-солнечный календарь. Официально используется в Китае (где живёт более миллиарда человек) наряду с григорианским.
  • Эфиопский календарь. Косвенно происходит от старого египетского, первого солнечного календаря, который у нас есть. Технически его високосные дни не являются частью каждого месяца. Полночь в стране начинается тогда, когда в соседних странах шесть часов утра. Местные жители гордится тем, что Эфиопию никогда не колонизировали, поэтому они, как правило, не заинтересованы в принятии других систем.
  • Еврейский календарь. Один из самых старых. Одновременно и лунный, и солнечный. В нём самые сложные правила, связанные с выбором дат, которые я когда-либо видел. Простые правила выбора первого дня года могут занимать несколько страниц. Он официально используется в Израиле и имеет тот же статус, что и григорианский календарь.
  • Индуистский календарь, также очень сложный для меня. Он использует как солнечные, так и лунные циклы. Интересно, что в зависимости от региона в качестве начала цикла принимается новолуние или полнолуние, поэтому в одной и той же стране один и тот же календарь даёт два разных набора дат. В совокупности с жителями Индии мы близки к трети мирового населения, которое довольно часто пользуется негригорианскими календарями.
  • Также существует персидский календарь, особенности которого я сейчас не очень хорошо помню.
  • И наконец, исламский календарь, который используется в Саудовской Аравии официально, а в других странах — в культурных или религиозных целях. Интересно, что он основан на наблюдениях. То есть каждый новый лунный цикл какой-то уполномоченный человек должен выйти, посмотреть на небо и сказать: «Это новолуние, у нас официально новый месяц». Раньше, когда этот календарь использовался во многих странах, месяцы могли начинаться с разных дат, поскольку в зависимости от местоположения наблюдения отличаются.

Так что да. Здесь нужно многое иметь в виду. Давайте оставим всё это сложное время и пойдём дальше.


image


О нет. Юникод. Осмелюсь предположить: многие в этом зале сталкивались с тем, что различные системы коверкали ваши имена. Поднимите руки. (Более половины зала поднимают руки.)


Да. Стало быть, вы знаете, как это происходит. Проблема заключается в конфликте кодировки между Latin-1 (или ISO-8859–1) и UTF-8.


image


Выходит так, что в нижних символах UTF-8 и ISO-8859–1 абсолютно одинаковы, а самые нижние символы разделены с ASCII.


Поэтому для получения одной и той же кодировки нам необходимо настроить каждый шаг. Сначала пользователь, затем передача данных с помощью HTTP (включая надоедливые метатеги), сами экземпляры, затем языки программирования (некоторые функции по своей сути не являются UTF-8), далее соединение с базой данных (поскольку SQL основан на тексте, он чувствителен к кодировке), а затем и сама база данных. Если вам повезёт, то ваша БД также позволит настроить конфигурацию для таблицы и для столбца.


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


image


В Юникоде длина — это также переменная. Это можно объяснить четырьмя способами. Используем небольшую строку с, а и лошадью в ней:


  • Имеется длина в байтах, которая даёт нам 14 байтов в UTF-8, 12 в UTF-16 и 20 в UTF-32.
  • В кодовой единице содержится длина, которая говорит о том, сколько комбинаций битов используется для каждой кодировки строки. В данном случае 14 для UTF-8, 6 для UTF-16 и 5 для UTF-32.
  • Длина кода основана на количестве логических символов Юникода. У нас длина равна 5, так как второй символ здесь сделан с комбинированной меткой, придающей ему маленькую кривую сверху.
  • Кроме того, есть кластеры графем, которые мы, люди, считаем символами. Мне нравится описывать это как «сколько раз нужно нажимать клавишу Delete, пока строка не станет пустой». Длина равна 4.

Длина может изменяться от языка к языку, когда речь заходит о кластерах графем, поэтому важна локализация.


Это весело. На прошлой работе мы один раз удалили целый кластер на 40+ минут. Бинарный протокол запросил длину байта по переданным ему данным, но одна из двух точек связи использовала длину кода для работы. Это продолжалось годами, пока мы не начали отправлять произвольные данные, передаваемые клиентами. Потребовалось около 30 секунд, чтобы ввести с Юникод данные, которые десинхронизовали оба протокола и всё исправили.


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


image


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


image


А, как забавно. Я получил это в результате одной интересной ошибки Spotify, которая произошла несколько лет назад. Я не буду повторять, что я тогда сказал, а просто дам ссылку на сообщение в блоге.


image


На этом слайде объясняется нормализация Юникода. Он использовался в качестве поддержки ошибки Юникод в Spotify, так что вот ссылка на сообщение в блоге.


image


И теперь мы добрались до по-настоящему интересного материала! Безопасность! Это из статьи 2012 года «Самый опасный код в мире: проверка SSL-сертификатов вне браузера», которую я советую почитать.


© Habrahabr.ru