Розбираємо UUID у всьому його різноманітті
Всім привіт. Я Сергій Моренець, розробник, тренер, викладач, спікер і технічний письменник. Хочу розповісти вам про такий формат, як UUID, та сферу його застосування. У цій статті я опишу різні версії UUID, його підтримку в базах даних, операційних системах, мовах програмування і також поділюся результатами тестування швидкодії генерації UUID. Сподіваюся, що ця стаття буде корисна для всіх, хто хоче більше дізнатися про сучасні тенденції роботи з даними та створення унікальних ідентифікаторів.
Що таке UUID
Коли я тільки-но почав працювати з Java, то для одного із завдань мені знадобився унікальний ідентифікатор, який можна було отримати з Java. Я дізнався, що є стандартний клас UUID і досить простий код для генерації такого унікального ідентифікатора:
UUID id = UUID.randomUUID();
На довгі роки цей код був моїм основним досвідом роботи з UUID, поки
Отже, UUID (Universally Unique Identifier) був придуманий у
- Відсутність чи мінімальна ймовірність колізій, тобто глобальна унікальність.
- Відсутність необхідності в централізованому сервері для генерації таких значень, тобто можливість генерації їх розподілено.
Набагато пізніше з’явилися ще дві вимоги:
- Безпека (неможливість за UUID відстежити людину або систему, де був згенерований UUID).
- Монотонність (кожне наступне значення має бути за попереднім при лексикографічному сортуванні).
Але спочатку були лише перші дві вимоги, при чому головне це глобальна унікальність. Невипадково компанія Microsoft, яка також почала його використовувати, перейменувала його в GUID (Globally Unique Identifier). Потім він з’явився як специфікація в Open Software Foundation (OSF), після чого в
На сьогодні існує 8 версій формату UUID, при чому версія вказується в самому значенні й під неї відводиться 4 біти.
Версія 1
UUID у версії 1 містить дві складові:
- Кількість
100-наносекундних інтервалів після 15 жовтня 1582 (60 біт) в часовому поясі UTC. Ви також можете генерувати це значення у своєму локальному часі, але це не рекомендується, щоб уникнути колізій при генерації даних у різних часових поясах. Цікаво, що якщо ваш комп’ютер не може вимірювати час у наносекундах, допускається використання випадкового значення замість часу. - MAC-адреса комп’ютера, де генерується UUID (48 біт). Якщо у комп’ютера кілька MAC-адрес, то вибирається будь-яка, якщо жодної, замість неї використовується випадковим чином згенерована послідовність символів. Така ситуація може бути, якщо ви хочете згенерувати UUID у браузері, який не має доступу до MAC-адреси.
Такий підхід є найшвидшим у використанні. Достатньо було один раз отримати MAC-адресу (яка не змінювалася) і потім просто додавати поточний час. Але водночас він легко дозволяв визначити MAC-адресу комп’ютера та час створення UUID, що могли використовувати зловмисники для своїх атак. Є майже детективна історія, як Девід Сміт, автор вірусу «Melissa», був спійманий у 1999 році, оскільки залишив UUID зі своєю MAC-адресою. В результаті поява більш жорстких вимог до безпеки зробила цю версію менш популярною. Крім того, через широке застосування контейнерів і віртуальних мереж MAC-адреса могла бути неунікальною, що призводило до можливих колізій.
Ще одна потенційна небезпека цього підходу (і всіх, що беруть поточний час) — це можливість переведення поточного часу назад, що у майбутньому може призвести до колізій.
Загалом підхід, закладений у версію 1, був дуже привабливим. Зрозуміло, що для забезпечення глобальної унікальності потрібно додавати до UUID довільне число. Але навіщо це робити, якщо у нас вже є MAC-адреса, яка і так є унікальною? Але вимоги до безпеки та розвиток інфраструктури мереж призвели до того, що зараз ця версія використовується набагато менше, ніж раніше.
Версія 2
Версія 2 називається «DCE Security UUID», тому що її реалізація ніяк не вказана у специфікації RFC 4122, але вказується в старішій специфікації DCE Authentication and Security Service та враховує особливості UNIX-систем (стандарту POSIX). UUID у ній містить:
- MAC-адреса комп’ютера.
- Час у
7-хвилинних інтервалах. - Ідентифікатор користувача або його групи на локальному комп’ютері.
Важко сказати, чим керувалися автори цієї специфікації, зменшуючи кількість біт на часовий інтервал, але тепер можна було згенерувати лише (!) 64 унікальних UUID протягом
Версія 3
Одним із недоліків версії 1 було те, що вона була прив’язана до поточного часу. Таким чином, якщо ви використовували згенерований UUID як shard key у вашому кластері без його хешування, деякі вузли в кластері могли містити набагато більше даних, ніж інші. Ці та інші недоліки створили передумови для створення нового алгоритму генерації UUD, де використовувалося б хешування даних. Але не просто хешування, а хеширування, яке б не дозволяло б отримати вихідне значення. У версії 3 (named-based) для цієї мети був обраний алгоритм шифрування MD5, незважаючи на те, що ще у
У цій версії замість прив’язки до локальної MAC-адреси або доменної групи (як у версії 2) вибрали концепцію використання пари namespace і name. Що таке namespace? Специфікація говорить, що це має бути деяке фіксоване значення, прив’язане до вашої системи (теж UUID), і пропонує 4 стандартні значення:
- DNS: 6ba7b810-9dad-11d1-80b4-00c04fd430c8
- URL: 6ba7b811-9dad-11d1-80b4-00c04fd430c8
- OID: 6ba7b812-9dad-11d1-80b4-00c04fd430c8
- X.500 DN: 6ba7b814-9dad-11d1-80b4-00c04fd430c8
Name — це певний рядок, який ідентифікує той об’єкт, для якого ви генеруєте UUID. Таким чином під час генерації UUID Namespace і Name конкатенуються, а потім над отриманим рядком виконується хешування MD5.
Уявімо, що у вас є сайт www.sample.com і на ньому ресурс із відносним URL/products/1. Для створення UUID ви використовуєте функцію uuid_v5. Тоді процес створення ідентифікатора для вашого ресурсу може бути двоетапним. Спочатку ви обчислюєте перший UUID на базі стандартного DNS UUID та домену сайту:
firstUuid = uuid_v5 (DNS_UUD, «www.sample.com»)
А на другому етапі ви замість Namespace підставляєте перший UUD, а замість Name — адресу ресурсу:
secondUuid = uuid_v5 (firstUuid, «/product/1»)
Якщо вихідний Namespace був унікальним (у цьому разі — firstUuid), це гарантує відсутність колізій. Головний мінус такого підходу — це вимога до незмінності вихідного Namespace (URL, DNS, OID). Якщо ж він таки зміниться, то повторна генерація UUID поверне значення, відмінне від існуючого (у тій же базі даних) і може призвести до втрати цілісності ваших даних.
Відносний мінус такого підходу — у того ресурсу, для якого ви генеруєте UUID, має бути унікальний і immutable Name (або URI) в межах вашої системи.
Версія 4
Цей алгоритм є найпопулярнішим зараз (за деякими оцінками, до
Таким чином, цей підхід варто використовувати, якщо вам потрібен глобальний унікальний ідентифікатор, який не прив’язаний до конфігурації вашого комп’ютера або до поточного часу. Щоправда, ймовірність появи колізій залежить від того, наскільки випадковими будуть значення, отримані від використовуваного генератора випадкових чисел.
Версія 5
Згодом в алгоритмі MD5 знайшли таку кількість проблем з безпекою його роботи, що було ухвалено рішення на основі версії 3 додати версію 5, де замість MD5 для хешування використовувати більш стійкий алгоритм SHA-1. Щодо швидкодії, то SHA-1 дозволяє процесору розпаралелити частину своєї роботи, тому дослідження показують, що на сучасних комп’ютерах генерація SHA-1 хешу обчислюється швидше, ніж MD5.
Наскільки популярні версії 3 та 5? У 2021 році було проведено дослідження серед JavaScript-репозиторіїв на GitHub, і виявилося, що вони використовуються лише в 1% від усіх проєктів, де була генерація UUID (а версія 4 — 77%).
Версія 6
Версія 6 практично повністю збігається з версією 1, за винятком того, що там поміняли місцями верхні та нижні біти в часі, що дозволило сортувати UUID значення за датою створення.
Версія 7
У 2016 році відбулася визначна подія. Розробник Алізайн Фіраста оголосив про створення нового формату ULID (Universally-Unique, Lexicographically-Sortable Identifier), специфікацію якого він і опублікував. Головними його особливостями були:
- Повна сумісність із UUID (ті ж 128 біт).
- Можливість перетворення на
26-символьний рядок (у UUID 36 символів). Це досягається шляхом того, що використовується кодування base32 з 5 бітами на кожен символ. - Відсутність спеціальних символів, що дає можливість використання його в URL.
- Монотонність значень та можливість використання його при сортуванні.
Остання особливість найбільш важлива, оскільки вона була відсутня в UUID. На цей час реалізація ULID є практично для всіх популярних мов програмування. Який формат даних в ULID? Спочатку йде 48 біт (час у мілісекундах з початку епохи Unix), потім 80 біт — випадкові символи.
Популярність нового формату призвела до того, що IT-спільнота задумалася про створення нової версії UUID, яка також підтримувала б монотонність. І ось у 2023 нарешті вийшла версія 7, яка пропонує наступний формат:
- Кількість мілісекунд, що пройшли після 1 січня 1970 року (48 біт).
- Випадкове значення.
Використання на початку рядка часу в мілісекундах дозволяє отримати UUID-значення, відсортовані за зростанням, що якраз і потрібно для використання такого ключа в базах даних. Водночас з метою дотримання безпеки ви можете зрушити вперед або назад цей час, щоб зловмисники не могли дізнатися точний час створення UUID.
Лічильник випадкових значень ініціалізується при кожному створенні UUID, що дозволяє уникнути колізій, якщо час буде одним і тим же.
Версія 8
Ця версія ще називається vendor-specific, тому що вона лише вимагає наявності в UUID обов’язкових полів версія/variant, тоді як сама генерація значень залишається за вендором.
Підтримка у базах даних
Якщо ви використовуєте один сервер БД або один сервер з репліками, достатньо використовувати автоінкрементне поле або лічильник для генерації первинного ключа. Якщо ж у вас кластер, який може розташовуватися в різних регіонах, то необхідно забезпечити глобальну унікальність первинного ключа. І тут може стати в нагоді UUID.
MongoDB містить функцію UUID(), яка повертає UUID-об’єкт версії 4. Чи варто використовувати її замість дефолтного
MySQL також містить функцію UUID(), яка повертає значення версії 1. Цікаво, що ця реалізація функції залежить від платформи. На FreeBSD, Linux і Windows спочатку йде MAC-адреса (як і має бути), а ось на інших платформах (Solaris, MacOS) генерується
У Postgres 13 і пізніших версій цих цілей йде функція gen_random_uuid(), яка генерує значення у версії 4. Більше того, тут є і вбудований тип даних UUID.
Oracle містить функцію sys_guid(). Версія UUID не вказується в документації, визначається лише унікальність поверненого значення і його формат — MAC-адреса, ідентифікатор потоку/процесу і значення автоінкрементного лічильника. Тут також є тип даних UUID.
Підтримка в ОС
У Windows використовується термін GUID замість UUID, хоча це фактично те саме. В описі GUID немає жодного слова про те, як він генерується, лише його формат. А у Win32 API для розробників є функції UuidCreate та UuidCreateSequential. Різниця між ними в тому, що перша не використовує MAC-адресу для генерації значень, а друга використовує. Windows широко використовує GUID для ідентифікації різних інтерфейсів, ACL і ActiveX об’єктів. Більше того, ви можете згенерувати UUID з командного рядка:
powershell -Command «[guid]::NewGuid().ToString()»
У Linux є утиліта uuidgen, яка використовує бібліотеку libuuid та підтримує генерацію всіх версій UUID (крім другої). Якщо у вас немає цієї утиліти, ви можете просто читати з read-only файлу /proc/sys/kernel/random/uuid, який щоразу повертатиме новий UUID.
Підтримка в Java
У Java 5 з’явився клас UUID, що дозволяє генерувати UUID версії 4:
UUID id = UUID.randomUUID(); System.out.println(id.variant()); // 2 System.out.println(id.version()); //4
Він використовує стандартний клас SecureRandom для генерації випадкових чисел, а той своєю чергою може застосувати три алгоритми для цього завдання:
- SHA1PRNG
- WINDOWS-PRNG
- DRBG
Спочатку в JDK використовувався генератор SHA1PRNG (SHA-1 pseudo-random number generator), але зараз його слід застосовувати тільки для зворотної сумісності. А за замовчуванням задіяно DRBG (Deterministic Random Bit Generator), який вже використовує SHA-256 хешування.
Мало хто знає, що в UUID є підтримка і версії 3:
UUID id = UUID.nameUUIDFromBytes(bytes);
Тут ви повинні самостійно об’єднати Namespace і Name і потім конвертувати їх у байтовий масив.
Втім, якщо вам не підходять UUID версій 3/4, ви можете самі згенерувати його значення будь-яким іншим способом і передати йому через конструктор. Потім можна перевести його у рядковий формат:
System.out.println(id); // 164d5b77-51ad-4290-a42f-0a0461dae078
Але при цьому, якщо ви зберігаєте UUID як послідовність символів, ви на це витрачаєте 36 байт. Можна перетворити отримане значення, використовуючи Base64-кодування (де зберігається 6 біт на символ), і тоді у вас вже буде всього 22 символи:
var encoder = Base64.getUrlEncoder().withoutPadding(); ByteBuffer buffer = ByteBuffer.wrap(new byte[16]); buffer.putLong(id.getMostSignificantBits()); buffer.putLong(id.getLeastSignificantBits()); String base64Id = encoder.encodeToString(buffer.array()); // Fk1bd1GtQpCkLwoEYdrgeA
Якщо UUID найчастіше використовуються для баз даних, то невипадково, що й у ORM-технологіях роблять підтримку UUID-ідентифікаторів дедалі зручнішою. Починаючи з Hibernate 6 ви можете однією анотацією @UuidGenerator вказати Hibernate, щоб він використовував версію 4 для генерації UUID:
@Id @GeneratedValue @UuidGenerator private UUID id;
Якщо ви хочете використовувати версію 1, то для цього потрібно явно в анотації @UuidGenerator вказати атрибут style:
@Id @GeneratedValue @UuidGenerator(style = Style.TIME) private UUID id;
При цьому замість MAC-адреси буде братися IP-адреса сервера. Тут, правда, можуть виникнути колізії, якщо у вас сервери знаходяться в різних мережах, але мають один і той же IP.
Бібліотека Jackson з перших версій підтримує серіалізацію/десеріалізацію UUID-значень:
String json = "\"a7161c6c-be14-4ae3-a3c4-f27c2b2c6ef4\«"; ObjectMapper mapper = new ObjectMapper(); UUID uuid = mapper.readValue(json, UUID.class);
Підтримка JavaScript
Довгий час JavaScript не мав нативної підтримки UUID. У
const id = crypto.randomUUID(); // ffcf8cf8-2ec8-4493-a8c4-0d9e0a633eb7
Після цього підтримка UUID з’явилася в Node.js, починаючи з версії 14.17.:
import { randomUUID } from «node:crypto»; const uuid = crypto.randomUUID();
У браузерах така підтримка вперше з’явилася у Chrome 92 та Firefox. При цьому з урахуванням вимог безпеки цю функцію можна використовувати тільки на HTTPS з’єднаннях та на HTTP (але на localhost).
Benchmarks
Будь-яке дослідження нової технології має включати аналіз її ефективності, тим більше, що ми маємо такий чудовий інструмент, як JMH. Для тестування було обрано наступну конфігурацію:
- JMH 1.37
- JDK 23.0.1
- Intel Core i9, 8 cores
- 32 GB
- 10 ітерацій обчислення, 10 ітерацій прогріву (warm-up)
Для створення UUID я вибрав популярну бібліотеку java-uuid-generator, яка підтримує всі версії UUID, крім застарілих (2 і 3). Ось як виглядають тести:
@State(Scope.Thread) @BenchmarkMode(Mode.AverageTime) @Warmup(iterations = 10) @Measurement(iterations = 10) public class UuidBenchmark { private final Map<Integer, UuidGenerator> generators = new HashMap<>(); @Param({ «1», «4», «5», «6», «7» }) private int version; @Setup public void setup() { generators.put(1, () -> Generators.timeBasedGenerator().generate()); generators.put(4, () -> Generators.randomBasedGenerator().generate()); generators.put(5, () -> Generators.nameBasedGenerator().generate("www.uuid.com")); generators.put(6, () -> Generators.timeBasedReorderedGenerator().generate()); generators.put(7, () -> Generators.timeBasedEpochGenerator().generate()); } @Benchmark public UUID generateUuid() { return generators.get(version).generate(); } interface UuidGenerator { UUID generate(); }
Результати вимірювань (середній час виконання операції у наносекундах):
Benchmark (version) Mode Cnt Score Error Units UuidBenchmark.generateUuid 1 avgt 5 714.357 1.600 ns/op UuidBenchmark.generateUuid 4 avgt 5 691.462 1.364 ns/op UuidBenchmark.generateUuid 5 avgt 5 215.452 2.885 ns/op UuidBenchmark.generateUuid 6 avgt 5 727.468 1.083 ns/op UuidBenchmark.generateUuid 7 avgt 5 687.517 0.951 ns/op
Висновки
UUID — це набір версій/алгоритмів генерації унікальних значень, об’єднаних загальним форматом та структурою даних. Кожна з них має свої плюси та мінуси, виходячи з вимог до безпеки та сфери застосування. Це не стосується версій 2 і 3, які на цю мить застаріли і практично не застосовуються.
У чистій Java Core ви можете використовувати тільки версії 3 (застарілу) і 4 без застосування додаткових бібліотек. Бази даних підтримують тільки якусь одну версію, але самі версії відрізняються від СУБД до СУБД, що може ускладнювати міграцію з однієї бази даних на іншу.
Щодо швидкодії, то версії 1, 4, 6, 7 показали приблизно однаковий час. Ну а однозначно найкращий результат — у версії 5, що може бути пояснено відсутністю генерації випадкових чисел та обчислення поточного часу. І навіть застосування SHA-1 хешування не вплинуло на результат.
Загалом за унікальність потрібно платити свою ціну. Якщо при виборі цілого типу для первинного ключа ви використовуєте 4 (або 8) байт для кожного значення, то з UUID це вже буде 16 байт (або 32 символи). І це впливає не тільки на обсяг пам’яті, що витрачається, але і на пошук у базі даних по UUID, генерацію індексів і так далі.
45 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів