Uuid аппаратного обеспечения что это

UUID версии 7, или как не потеряться во времени при создании идентификатора

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

Хотя, подобные решения, не всегда хороши. В отличие от обыкновенных цифровых значений, которые легко кешировать и сортировать, UUID не так гибки в использовании. UUID версии 7 предназначен как раз для того, чтобы разобраться с подобными проблемами.

Добро пожаловать в мир отсортированных случайностей.

Сами по себе UUID это не просто набор случайных битов. Существует несколько вариантов их генерации. @AloneCoder в своей статье как генерируются UUID в подробностях описывает уже существующие форматы идентификаторов, версии с первой по пятую.

UUID в базах данных

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

Всё бы хорошо, но вот именно это и не очень-то просто. Выбирать даты из строковых значений UUID это та ещё свистопляска. Почему? Ну, давайте посмотрим на последовательность генерации UUIDv1.

Берутся младшие 32 бита текущей временной метки UTC. Это будут первые 4 байта (8 шестнадцатеричных символов) UUID [ TimeLow ].

Берутся средние 16 битов текущей временной метки UTC. Это будут следующие 2 байта (4 шестнадцатеричных символа) [ TimeMid ].

Следующие 2 байта (4 шестнадцатеричных символа) конкатенируют 4 бита версии UUID с оставшимися 12 старшими битами текущей временной метки UTC (в которой всего 60 битов) [ TimeHighAndVersion ].

Как всё замечательно запутано. На самом деле, распарсить дату из такого идентификатора достаточно просто, но парсинг это парсинг. Это не весело и нагружает процессор.

Герой дня

Основная разработка ведётся силами двух разработчиков: bradleypeabody и kyzer-davis. Хабрачеловеки и хабраалиены могут поучаствовать в обсуждении и написании формата на гитхабе https://github.com/uuid6/uuid6-ietf-draft/.

Пять дней назад эта спецификация вызвала оживлённую дискуссию на hackernews.

При разработке спецификаций, были рассмотрены следующие форматы генерации UUID:

И так, что же такого особого в UUIDv7 и чем он отличается от предыдущих версий?

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

Ну а нам-то какая разница? Можно же сделать select id, creation_date order by creation_date и жить себе спокойно.

Вы не поняли вопроса.

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

Представьте, у вас есть высоконагруженная система. 100 серверов генерируют новые записи с UUID несколько раз в секунду, и всё это летит в Redis, которые грузит эти данные в Postgresql.

Ага. Вот тут вот жизнь с UUIDv7 становится проще. Значения индексов не настолько разбросаны и следить за ними намного проще.

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

Но, как же это работает?

Ок, в отношении самой даты — тут всё просто. Запишите число, как unix timestamp и у вас есть что-то бинарно-сортируемое. Только я вас прошу, не стоит записывать эту дату кусками, в разнобой. Просто и понятно, первые 36 битов содержат в себе одно число. Но, если вы пытаетесь записать миллисекунды, то всё становится сложнее.

Давайте поговорим о математике. О приближении и лимитах. Любимая тема, а? Давайте посмотрим на следующую запись секунды: 05,625. Пять целых, шестьсот двадцать пять секунд. Отбрасываем 5, поскольку это будет записано в unix timestamp.

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

Uuid аппаратного обеспечения что это. Смотреть фото Uuid аппаратного обеспечения что это. Смотреть картинку Uuid аппаратного обеспечения что это. Картинка про Uuid аппаратного обеспечения что это. Фото Uuid аппаратного обеспечения что это

Достаточно, просто, правда? Если сложить все числа в этом ряду, то вы получите единицу. А что если складывать не каждый? Ну, с этим можно что-то сделать. Давайте присвоим каждому числу из этого ряда один бит. Каждый бит будет показывать, если этот член присутствует в ряду или нет.

Берём нашу суб-секундную точность, 0,625 и начинаем записывать эту точность с помощью битов.

Первое число 1/2, то есть 0,5. Если наше значение точности больше этого числа, то выставляем битовое значение в 1 и вычитаем это число из нашего текущего значения точности. В итоге получаем, битовую последовательность 1 и 0,125 в остатке.

Uuid аппаратного обеспечения что это. Смотреть фото Uuid аппаратного обеспечения что это. Смотреть картинку Uuid аппаратного обеспечения что это. Картинка про Uuid аппаратного обеспечения что это. Фото Uuid аппаратного обеспечения что это

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

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

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

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

Что мы получаем в итоге? Возможность сохранения времени с суб-секундной точностью в бинарно-сортируемом формате.

А в случае коллизий?

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

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

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

Плюс, всё пространство, которое не используется для времени, счётчика и номера компьютера (порядка 54х бит) необходимо заполнять случайными значениями для предотвращения каких-либо совпадений на разных узлах.

Источник

Uuid аппаратного обеспечения что это. Смотреть фото Uuid аппаратного обеспечения что это. Смотреть картинку Uuid аппаратного обеспечения что это. Картинка про Uuid аппаратного обеспечения что это. Фото Uuid аппаратного обеспечения что это

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

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

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

СОДЕРЖАНИЕ

История

Стандарты

UUID стандартизированы Open Software Foundation (OSF) как часть распределенной вычислительной среды (DCE).

Engineering Task Force Интернет (IETF) опубликовала стандарты-Track RFC 4122, технически эквивалент ITU-T Rec. X.667 | ИСО / МЭК 9834-8.

Формат

В каноническом текстовом представлении 16 октетов UUID представлены в виде 32 шестнадцатеричных (base-16) цифр, отображаемых в пяти группах, разделенных дефисами, в форме 8-4-4-4-12, всего 36 символов. (32 шестнадцатеричных символа и 4 дефиса). Например:

Четырехбитовые поля M и 1-3-битные поля N кодируют формат самого UUID.

Строка канонического формата 8-4-4-4-12 основана на структуре записи для 16 байтов UUID:

Макет записи UUID

ИмяДлина (байты)Длина (шестнадцатеричные цифры)Длина (бит)СОДЕРЖАНИЕ
time_low4832целое число, дающее младшие 32 бита времени
time_mid2416целое число, дающее средние 16 бит времени
time_hi_and_version24164-битная «версия» в старших разрядах, за которыми следуют старшие 12 бит времени
clock_seq_hi_and_res clock_seq_low24161–3-разрядный «вариант» в наиболее значимых битах, за которым следует 13–15-разрядная тактовая последовательность.
узел6124848-битный идентификатор узла

Эти поля соответствуют полям UUID версий 1 и 2 (то есть UUID на основе времени), но одно и то же представление 8-4-4-4-12 используется для всех UUID, даже для UUID, построенных по-разному.

RFC 4122 Раздел 3 требует, чтобы символы создавались в нижнем регистре, при этом регистр не учитывался при вводе.

Идентификаторы GUID Microsoft иногда обозначаются фигурными скобками:

Этот формат не следует путать с « реестром Windows форматом», который относится к формату в фигурных скобках.

RFC 4122 определяет пространство имен Uniform Resource Name (URN) для UUID. UUID, представленный как URN, выглядит следующим образом:

Кодирование

Варианты

Поле «вариант» UUID или позиция N указывают их формат и кодировку. RFC 4122 определяет четыре варианта длины от 1 до 3 бит:

Варианты 1 и 2 используются текущей спецификацией UUID. В текстовом представлении варианты 1 и 2 одинаковы, за исключением битов варианта. В двоичном представлении существует разница в порядке байтов. Когда для преобразования между прямым порядком байтов варианта 1 и прямым порядком байтов варианта 2 требуется перестановка байтов, поля выше определяют перестановку. Первые три поля представляют собой 32- и 16-разрядные целые числа без знака и подлежат замене местами, в то время как последние два поля состоят из неинтерпретируемых байтов и не подлежат замене местами. Эта перестановка байтов применяется даже для версий 3, 4 и 5, где канонические поля не соответствуют содержимому UUID.

Версии

Для обоих вариантов 1 и 2 в стандартах определены пять «версий», и каждая версия может быть более подходящей, чем другие, в конкретных случаях использования. Версия указывается M в строковом представлении.

UUID версии 1 генерируются из времени и идентификатора узла (обычно MAC-адреса ); UUID версии 2 генерируются из идентификатора (обычно идентификатора группы или пользователя), времени и идентификатора узла; версии 3 и 5 производят детерминированные UUID, генерируемые хешированием идентификатора и имени пространства имен ; и UUID версии 4 генерируются с использованием случайного или псевдослучайного числа.

Nil UUID

Версия 1 (дата, время и MAC-адрес)

RFC 4122 позволяет заменять MAC-адрес в UUID версии 1 (или 2) случайным 48-битным идентификатором узла либо потому, что у узла нет MAC-адреса, либо потому, что его нежелательно раскрывать. В этом случае RFC требует, чтобы младший бит первого октета идентификатора узла был установлен в 1. Это соответствует биту многоадресной рассылки в MAC-адресах, и его установка служит для различения UUID, где идентификатор узла генерируется случайным образом. из UUID на основе MAC-адресов сетевых карт, которые обычно имеют одноадресные MAC-адреса.

Версия 2 (дата, время и MAC-адрес, версия безопасности DCE)

RFC 4122 резервирует версию 2 для UUID «безопасности DCE»; но не содержит подробностей. По этой причине во многих реализациях UUID не используется версия 2. Однако спецификация UUID версии 2 обеспечивается спецификацией служб аутентификации и безопасности DCE 1.1.

UUID версии 2 похожи на версию 1, за исключением того, что 8 младших битов тактовой последовательности заменяются номером «локального домена», а младшие 32 бита метки времени заменяются целочисленным идентификатором, имеющим значение в пределах указанного локальный домен. В системах POSIX номера локальных доменов 0 и 1 предназначены для идентификаторов пользователей ( UID ) и групп ( GID ) соответственно, а другие номера локальных доменов определяются сайтом. В системах, отличных от POSIX, все номера локальных доменов определяются сайтом.

Возможность включения 40-битного домена / идентификатора в UUID требует компромисса. С одной стороны, 40 бит позволяют использовать около 1 триллиона значений домена / идентификатора для каждого идентификатора узла. С другой стороны, когда значение часов усечено до 28 старших битов, по сравнению с 60 битами в версии 1, часы в UUID версии 2 будут «тикать» только один раз каждые 429,49 секунды, то есть чуть более 7 минут, поскольку в отличие от каждых 100 наносекунд для версии 1. И с тактовой последовательностью всего 6 бит, по сравнению с 14 битами в версии 1, только 64 уникальных UUID для каждого узла / домена / идентификатора могут быть сгенерированы за 7-минутный такт часов, по сравнению с 16 384 значения тактовой последовательности для версии 1. Таким образом, версия 2 может не подходить для случаев, когда требуются UUID для каждого узла / домена / идентификатора со скоростью, превышающей примерно один каждые семь минут.

Версии 3 и 5 (на основе имен пространств имен)

Чтобы определить UUID версии 3, соответствующий заданному пространству имен и имени, UUID пространства имен преобразуется в строку байтов, объединяется с именем ввода, затем хешируется с помощью MD5, что дает 128 бит. Затем 6 или 7 бит заменяются фиксированными значениями, 4-битной версией (например, 0011 2 для версии 3) и 2- или 3-битным «вариантом» UUID (например, 10 2 указывает на RFC 4122 UUID или 110 2 с указанием устаревшего идентификатора GUID Microsoft). Поскольку таким образом заранее определены 6 или 7 бит, только 121 или 122 бита вносят вклад в уникальность UUID.

UUID версии 5 аналогичны, но вместо MD5 используется SHA-1. Поскольку SHA-1 генерирует 160-битные дайджесты, дайджест обрезается до 128 бит перед заменой битов версии и варианта.

UUID версии 3 и версии 5 обладают тем свойством, что одно и то же пространство имен и имя будут сопоставляться с одним и тем же UUID. Однако ни пространство имен, ни имя не могут быть определены из UUID, даже если один из них указан, кроме как методом перебора. RFC 4122 рекомендует версию 5 (SHA-1) вместо версии 3 (MD5) и предостерегает от использования UUID любой версии в качестве учетных данных безопасности.

Версия 4 (случайная)

UUID версии 4 генерируется случайным образом. Как и в других UUID, 4 бита используются для обозначения версии 4 и 2 или 3 бита для обозначения варианта (10 2 или 110 2 для вариантов 1 и 2 соответственно). Таким образом, для варианта 1 (то есть для большинства UUID) случайный UUID версии 4 будет иметь 6 заранее определенных битов варианта и версии, оставляя 122 бита для случайно сгенерированной части, всего 2122 или 5,3 × 10 36 (5,3 × 10). undecillion ) возможные UUID версии 4 варианта 1. Существует вдвое меньше возможных UUID версии 4 варианта 2 (устаревших идентификаторов GUID), потому что доступно на один случайный бит меньше, а для варианта используется 3 бита.

Столкновения

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

Таким образом, вероятность найти дубликат в 103 триллионах UUID версии 4 составляет один на миллиард.

Использует

Обычно используется в протоколах Bluetooth для определения служб и общего профиля Bluetooth.

В COM

Существует несколько разновидностей идентификаторов GUID, используемых в объектной модели компонентов (COM) Microsoft :

Как ключи базы данных

Некоторые веб-фреймворки, такие как Laravel, поддерживают UUID «сначала временная метка», которые могут эффективно храниться в индексируемом столбце базы данных. Это делает COMB UUID с использованием формата версии 4, но где первые 48 бит составляют метку времени, выложенную, как в UUIDv1. Более определенные форматы, основанные на идее COMB UUID, включают:

Источник

Про uuid-ы, первичные ключи и базы данных

Uuid аппаратного обеспечения что это. Смотреть фото Uuid аппаратного обеспечения что это. Смотреть картинку Uuid аппаратного обеспечения что это. Картинка про Uuid аппаратного обеспечения что это. Фото Uuid аппаратного обеспечения что это

Статья посвящена альтернативным версиям Qt-драйверов для работы с базами данных. По большому счету отличий от нативных Qt-драйверов не так много, всего пара: 1) Поддержка типа UUID; 2) Работа с сущностью «Транзакция» как с самостоятельным объектом. Но эти отличия привели к существенному пересмотру кодовой реализации исходных Qt-решений и изменили подход к написанию рабочего кода.

Первичный ключ: UUID или Integer?

Впервые с идеей использовать UUID в качестве первичного ключа я познакомился в 2003 году, работая в команде дельфистов. Мы разрабатывали программу для автоматизации технологических процессов на производстве. СУБД в проекте отводилась существенная роль. На тот момент это была FireBird версии 1.5. По мере усложнения проекта появились трудности с использованием целочисленных идентификаторов в качестве первичных ключей. Опишу пару сложностей:

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

Проблема программная: чтобы получить доступ к вставленной записи нужно было выполнить дополнительный SELECT-запрос, который возвращал максимальное значение первичного ключа (значение для только что вставленной записи). Причем этот процесс должен был проходить в пределах одной транзакции. Далее можно было обновлять или корректировать запись. Это сейчас я знаю, что некоторые драйверы БД возвращают значение первичного ключа для вставленной записи, но в 2003 году мы такими знаниями не обладали, да и не припомню что бы Делфи-компоненты возвращали что-то подобное.

Использование UUID-ов в качестве первичных ключей сводило к минимуму архитектурную проблему, и полностью решало программную. UUID-ключ генерировался перед началом вставки записи на стороне программы, а не в недрах сервера БД, таким образом дополнительный SELECT-запрос стал не нужен, и требование единой транзакции утратило актуальность. FireBird версии 1.5 не имел нативной поддержки UUID-ов, поэтому использовались строковые поля длинной в 32 символа (дефисы из UUID-ов удалялись). Факт использования строковых полей в качестве первичных ключей нисколько не смущал, нам не терпелось опробовать новый подход при работе с данными.

У UUID-ов есть свои минусы: 1) Существенный объем; 2) Более низкая скорость работы по сравнению с целочисленными идентификаторами. В рамках проекта достоинства оказались более значимы, чем указанные недостатки. В целом, опыт оказался положительным, поэтому в последующих решениях при создании реляционных связей предпочтение отдавалось именно UUID-ам.

Примечание: Более подробный анализ UUID vs Integer для СУБД MS SQL можно посмотреть в статье «Первичный ключ – GUID или автоинкремент?»

Первый драйвер для FireBird

В 2012 году мне снова довелось поработать с FireBird. Нужно было создать небольшую программу по анализу данных. Разработка велась с использованием QtFramework. Примерно в это же время у FireBird вышла версия 2.5 с нативной поддержкой UUID-ов. Я подумал: «Почему бы не добавить в Qt-драйвер для FireBird поддержку типа QUuid?» Так появилась первая версия Qt-драйвера с поддержкой UUID-ов. Этот вариант не сильно отличался от оригинальной версии драйвера и, в основном, был ориентирован на использование в однопоточных приложениях.

Появление сущности «Транзакция»

Следующая модификация Qt-драйвера для FireBird произошла в конце 2018 года. Наша фирма взялась за разработку проекта по анализу данных большого объема. Для фирмы выросшей из стартап-а эта работа была очень важна, как с финансовой, так и с репутацио́нной точек зрения. Сроки исполнения были весьма жесткие. В качестве СУБД была выбрана FireBird, несмотря на определенные сомнения в ее пригодности. Хорошим вариантом могла бы стать PostgreSQL, но у нашей команды на тот момент отсутствовал опыт эксплуатации данной СУБД.

Новая концепция не нуждалась в сущности «транзакция», как в самостоятельной единице, тем не менее, я не стал ее упразднять. Дальнейшая эксплуатация показала, что наличие объекта «транзакция» делает работу с базой данных более гибкой, дает больше инвариантов при написании кода. Например, разработчик может передать объект «Транзакция» в функцию в качестве параметра, явно говоря таким образом, что внутри нужно работать в контексте указанной транзакции. В функции можно проверить активна транзакция или нет, можно выполнить COMMIT или ROLLBACK. Для вспомогательных операций можно создавать альтернативную транзакцию, не затрагивающую основную. Таких возможностей нет у нативных Qt-драйверов.

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

Приведенный пример не будет работать с нативным Qt-драйвером, причина описана выше: ограничение на одно подключение и одну транзакцию

В примере экземпляры транзакций (1-3) созданы для наглядности. В рабочем коде их можно опустить. В этом случае транзакции будут создаваться неявно внутри объекта QSqlQuery. Неявные транзакции всегда завершаются ROLLBACK-ом для SELECT-запросов и попыткой COMMIT-а для всех остальных.

Ниже показано как можно использовать одну транзакцию для трех sql-запросов. Подтвердить или откатить транзакцию можно в любой из трех функций.

Драйвер для PostgreSQL

Драйвер для MS SQL

Чего нет в классе Driver

Описываемые здесь драйверы не повторяют один в один функционал Qt-решений. В классе оставлены следующие методы:

С введением сущности «Транзакция» они утратили актуальность и нужны исключительно для отладки и диагностирования их вызовов из Qt-компонентов.

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

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

Новые функции

Лицензионные ограничения

Зависимости

В реализации драйверов используется система логирования ALog, которая является составной частью библиотеки общего назначения SharedTools.

Демо-примеры

Специально для этой статьи был создан демонстрационный проект. Он содержит примеры работы с тремя СУБД: FireBird, PostgreSQL, MS SQL. Репозиторий с драйверами расположен здесь, он подключен в проект как субмодуль. Библиотека SharedTools так же подключена как субмодуль.

Проект создан с использованием QtCreator, сборочная система QBS. Есть четыре сборочных сценария:

Драйвера в первую очередь разрабатывались для работы в Linux, поэтому эксплуатационное тестирование выполнялось именно для этой ОС. В Windows будет работать FireBird-драйвер (проверено), для остальных драйверов тестирование не проводилось.

Демо-примеры записывают следующие логи:

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

Заключение

Черновой вариант статьи не предполагал наличие этого раздела, за что старый товарищ и, по совместительству, корректор подверг меня критике: «Мол, непонятна мотивация, целеполагание неясно. Зачем ты вообще писал эту статью?!» Что ж, исправляюсь!

Источник

Добавить комментарий

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