Представьте автодополнение, которое ищет среди 240 миллионов доменных имён и в 99% случаев готовит ответ ещё до того, как вы отпустили клавишу. Именно такой механизм создал автор Wirewiki — сервиса для проверки DNS-записей, делегирования доменов и настроек электронной почты.
Заявление о задержке p99 0 мс звучит почти неправдоподобно. Сервер ведь не может получить запрос, обработать его и вернуть данные за ноль миллисекунд. И действительно не может. Секрет в другом: система начинает работать раньше, чем пользователь ожидает увидеть результат.
Это отличный пример того, как ощущение скорости создаётся не только быстрым сервером. Иногда важнее правильно выбрать момент запуска операции.
Что на самом деле означает p99 0 мс
p99 — это 99-й процентиль задержки. Если он равен 0 мс, то в 99 случаях из 100 результаты уже готовы к моменту, с которого начинается измерение.
Ключевые слова здесь — «к моменту, с которого начинается измерение».
Обычно автодополнение работает довольно прямолинейно. Пользователь вводит символ, браузер фиксирует изменение текста, отправляет запрос на сервер, ждёт ответ и только потом показывает список вариантов. В такой схеме даже очень быстрая API неизбежно создаёт заметную паузу: к времени вычислений добавляется дорога до сервера и обратно.
В Wirewiki отсчёт устроен иначе. Запрос отправляется в момент нажатия клавиши, а результат нужен только к моменту её отпускания. Пока палец пользователя физически движется вниз и вверх, сеть и сервер уже работают.
Допустим, человек набрал «wi» и начинает нажимать следующую букву. Браузер заранее получает не только варианты для текущего префикса, но и подготовленные результаты для каждого возможного следующего символа. Если затем появляется буква «k», подходящий набор для «wik» можно взять из локального кэша без нового ожидания.
С точки зрения сервера запрос занял несколько десятков миллисекунд. С точки зрения пользователя — нисколько: к моменту, когда интерфейс должен измениться, данные уже лежат в браузере.
Звёздочка в заголовке нужна именно поэтому. Это не физическая задержка сети в 0 мс, а нулевая воспринимаемая задержка после отпускания клавиши.
Человеческая моторика становится частью архитектуры
Автор измерил собственный темп ввода ста доменных имён и получил бюджет около 121 мс на уровне p99. В него входят продолжительность нажатий и промежуток между ними.
Для серверной системы 121 мс — довольно много. За это время запрос способен пройти через несколько сетевых узлов, попасть в приложение, выполнить поиск и вернуться в браузер. Для человека же это почти незаметный интервал.
Есть и небольшой запас, связанный с обновлением экрана. Монитор с частотой 60 Гц рисует новый кадр примерно каждые 16,7 мс. Если результат прибыл вскоре после текущего кадра, у приложения остаётся несколько миллисекунд до следующего. Правда, рассчитывать на этот запас постоянно нельзя: в неудачном случае до перерисовки останется почти ноль времени.
Мне особенно нравится, что здесь пользователь не рассматривается как помеха, которую надо обслужить как можно быстрее. Его естественное поведение становится частью вычислительной модели. Пока человек нажимает клавишу, система получает бесплатное окно для работы.
Тот же принцип давно используется в других интерфейсах. Страница может заранее загружаться, когда указатель приблизился к ссылке. Следующий фрагмент видео скачивается до окончания текущего. Поисковая подсказка вычисляется до того, как пользователь закончил формулировать запрос. Хорошая производительность всё чаще оказывается не столько ускорением реакции, сколько точным предсказанием ближайшего действия.
Почему нельзя просто заранее скачать 240 миллионов доменов
Предварительная загрузка хороша, пока её объём разумен. Отправить в браузер всю базу доменов невозможно: это гигабайты данных, лишний трафик и огромная нагрузка на устройство.
Поэтому API возвращает компактный, но хитро устроенный ответ. Помимо восьми лучших совпадений для текущего префикса, в нём содержатся варианты для каждого допустимого следующего символа.
Для префикса «wi» сервер может сразу подготовить отдельные наборы для «wia», «wib», «wic» и так далее, а также для цифр, точки и дефиса. Когда пользователь вводит следующий символ, браузеру остаётся выбрать нужную ветку уже полученного ответа.
Это увеличивает размер одного ответа, зато резко сокращает число запросов, критичных для отрисовки. По сути, система меняет немного трафика на предсказуемость интерфейса.
Важно и то, что механизм не пытается угадать одну конкретную букву. Он заранее закрывает сразу все реалистичные продолжения. Поэтому ошибочный прогноз почти ничего не ломает: пользователь выбирает следующий символ сам, а нужный набор уже находится среди подготовленных вариантов.
Два мира: популярная «голова» и огромный «хвост»
Хранить 240 миллионов строк в одной универсальной структуре можно, но это не обязательно будет эффективно. У популярных и редких доменов разные роли.
Большинству пользователей при вводе короткого префикса нужны известные сайты. При этом система должна уметь находить и малоизвестный домен, если введено достаточно символов. Архитектура Wirewiki отражает именно это различие.
🔥 Популярная «голова» — миллион доменов из рейтинга Tranco. Они находятся в оперативной памяти и обслуживаются максимально быстро.
🧊 Длинный «хвост» — около 240 миллионов доменов из базы CZDS, доступной для многих общих доменных зон. Эти записи размещены на SSD в сжатом виде.
🧭 Ранжирование — сначала система предлагает наиболее популярные совпадения, а затем при необходимости дополняет их вариантами из большой базы.
Такое разделение полезно далеко за пределами поиска доменов. В реальных наборах данных популярность почти всегда распределена неравномерно. Небольшая доля объектов получает основную часть обращений, а гигантский хвост используется редко. Пытаться одинаково дорого оптимизировать каждую запись — обычно плохая трата памяти и денег.
Как работает быстрый слой в памяти
Для миллиона популярных доменов используется префиксное дерево. Это структура, в которой общие начала строк хранятся совместно.
Слова «wikipedia», «wikimedia» и «wiktionary», например, идут по одной ветке до символов «wiki», а затем расходятся. Чтобы найти префикс, не нужно сравнивать запрос с миллионом строк. Достаточно пройти по дереву символ за символом.
Более того, для каждого префикса заранее сохранены восемь лучших подсказок. Поэтому после прохода по нужной ветке серверу не приходится отдельно собирать, сортировать и ранжировать результаты. Он сразу берёт готовый список.
Время такой операции в основном зависит от длины введённой строки, а не от общего числа доменов. Поскольку доменные имена имеют ограниченную длину, практическая верхняя граница поиска получается очень небольшой.
Цена этого подхода — оперативная память и предварительная подготовка данных. Но для самой востребованной части базы это разумный обмен: память используется там, где она сильнее всего влияет на ощущения пользователя.
Как 240 миллионов доменов поместились в 2,5 ГБ
Редкие домены хранятся иначе. Их сортируют и разбивают на блоки по 256 имён. Внутри блоков применяется разностное сжатие.
Идея проста: у соседних строк в отсортированном списке часто совпадает длинное начало. Вместо полного повторения каждого домена можно сохранить длину общей части и только отличающийся остаток. Чем ближе строки друг к другу по алфавиту, тем заметнее экономия.
Так 240 миллионов доменных имён занимают примерно 2,5 ГБ на SSD. Для ориентира: если бы каждое имя вместе со служебными данными требовало хотя бы несколько десятков байт, несжатая база легко разрослась бы до многих гигабайт.
Перед дисковыми данными находится каталог размером около 27 МБ, который помещается в оперативную память. Сначала сервер выполняет двоичный поиск по этому каталогу и находит нужный блок, а затем последовательно просматривает его 256 записей.
На первый взгляд линейный просмотр звучит медленно. Но 256 коротких строк — это очень мало, особенно если нужная страница уже находится в кэше операционной системы. Кроме того, последовательное чтение небольшого блока обычно обходится дешевле, чем сложная цепочка случайных обращений к диску.
Здесь особенно хорошо виден здравый инженерный подход: не строить идеальную теоретическую структуру для всех случаев, а подобрать формат под реальные свойства оборудования. SSD, отображение файла в память и кэш операционной системы уже решают значительную часть задачи.
Отображение файла в память вместо самодельного кэша
Большая база открывается через механизм отображения файла в память. Приложение работает с содержимым так, будто это область памяти, а операционная система сама подгружает необходимые страницы с накопителя.
У этого решения несколько преимуществ.
💾 Не нужно загружать весь файл при запуске процесса.
🧠 Часто используемые страницы автоматически остаются в оперативной памяти.
♻️ При нехватке памяти система может вытеснить холодные страницы и позднее прочитать их снова.
⚡ Приложению не приходится самостоятельно поддерживать сложный кэш блоков и решать, какие данные выбрасывать.
Конечно, отображение файла в память не отменяет задержки накопителя. Первый доступ к холодной странице будет медленнее повторного. Но префиксный поиск обладает хорошей локальностью: популярные области данных постоянно прогреваются реальными запросами. Поэтому операционная система довольно точно кэширует то, что действительно нужно.
Сервер оказался быстрее сети
Для проверки производительности была создана нагрузка из 720 тысяч запросов, соответствующих вводу 60 тысяч доменных имён. Запросы отправлялись с фиксированной частотой, независимо от того, успевали ли завершаться предыдущие. Такой открытый режим создаёт более жёсткие условия, чем тест, который послушно ждёт каждый ответ.
Большинство обращений сама API обрабатывала менее чем за 2 мс. Даже при нагрузке около 1600 запросов в секунду связка Nginx и приложения укладывалась в 15 мс на уровне p99.
Это важный момент: после определённого рубежа дальнейшая оптимизация кода почти перестаёт улучшать пользовательский опыт. Можно потратить неделю и сократить серверное время с 2 до 1 мс, но пользователь на другом континенте этого не заметит. Его запрос всё равно проведёт десятки или сотни миллисекунд в сети.
В рабочей системе общая задержка примерно равна времени пути через Cloudflare туда и обратно плюс ещё около 10 мс. Cloudflare может отвечать из кэша на частые запросы и снимать часть нагрузки, однако не отменяет географию. Если запрос всё же должен дойти до исходного сервера, расстояние снова становится главным ограничением.
Почему один европейский сервер портит красивую метрику
Сервис работает на одном сервере в Европе. Для близких пользователей этого достаточно, но путь из США способен добавить ещё 100–200 мс. Такой запрос уже не всегда успеет вернуться за время следующего нажатия клавиши.
Именно здесь обещание p99 0 мс перестаёт быть глобальным. Оно выполняется при подходящей сетевой близости, нормальном соединении и достаточном времени между нажатиями. Человек с очень быстрым вводом или пользователь на другом континенте увидит ненулевую задержку чаще.
Техническое решение известно: запустить несколько копий сервиса в разных регионах и направлять пользователя к ближайшей. Тогда и данные, и вычисления окажутся ближе к браузеру.
Но распределённая система принесёт новые заботы: обновление индекса во всех регионах, наблюдение за состоянием узлов, маршрутизацию, резервирование, рост расходов и более сложное устранение сбоев. Для коммерческого продукта это могло бы быть оправданно. Для небольшого специализированного сервиса — уже не факт.
На мой взгляд, отказ от глобальной инфраструктуры здесь не недостаток, а признак инженерной зрелости. Возможность сделать систему ещё быстрее не означает, что это действительно нужно. Если текущий вариант почти всегда ощущается мгновенным для основной аудитории, дополнительные регионы могут оказаться дорогим способом улучшить красивую цифру в отчёте.
У предварительной загрузки есть своя цена
Подход Wirewiki выглядит элегантно, но его нельзя бездумно переносить в любой интерфейс.
Если для каждой клавиши сервер возвращает результаты для десятков возможных продолжений, объём ответа становится больше. На медленной мобильной сети лишние данные могут свести пользу на нет. Кроме того, пользователь способен быстро удалить символ, вставить текст из буфера обмена или изменить курсор в середине строки. Клиент должен правильно отменять устаревшие запросы и не показывать ответ для прежнего состояния поля.
Есть и нагрузочный эффект. Один пользователь, быстро набирающий строку, создаёт целую последовательность обращений. Тысяча одновременно печатающих людей — уже заметный поток. Здесь помогают кэширование популярных префиксов, небольшая стоимость серверного поиска и отсутствие тяжёлых операций для каждого запроса.
Наконец, предварительная загрузка полезна лишь тогда, когда будущее действие достаточно предсказуемо. Для доменного имени алфавит ограничен, а продолжение префикса естественно. В редакторе сложного запроса или в интерфейсе с сотнями возможных действий стоимость подготовки всех ветвей могла бы стать чрезмерной.
Главный урок — оптимизировать нужно не сервер, а момент ожидания
Эта история интересна не цифрой 240 миллионов и даже не быстрым префиксным деревом. Её главный урок — воспринимаемая скорость зависит от границ измерения.
Плохой интерфейс начинает работу после того, как пользователь уже потребовал результат. Хороший — замечает намерение немного раньше и использует короткие паузы, которые обычно пропадают впустую.
Wirewiki сочетает сразу несколько удачных решений:
🧠 запрос запускается при нажатии клавиши, а не после завершения ввода;
🔮 сервер готовит варианты для всех вероятных следующих символов;
🚀 популярные домены обслуживаются из префиксного дерева в памяти;
📦 огромный хвост хранится компактными блоками на SSD;
🔥 горячие страницы автоматически удерживаются системным кэшем;
🌍 CDN принимает повторяющиеся запросы ближе к пользователю;
🎯 производительность оценивается по p99, а не только по красивому среднему значению.
По отдельности ни один из этих приёмов не выглядит революционным. Сильной систему делает их сочетание. Быстрая структура данных бесполезна, если сеть съедает весь бюджет. Предварительная загрузка не спасёт, если сервер отвечает слишком долго. Кэш не поможет для редкого запроса, если холодный путь спроектирован плохо.
Вероятно, будущее быстрых интерфейсов именно за такими решениями: они будут всё чаще начинать вычисления не после команды, а при первых признаках намерения. Но граница здесь тонкая. Предсказание должно экономить больше времени, чем тратить трафика, энергии и серверных ресурсов.
Нулевая задержка Wirewiki — не нарушение законов физики. Это аккуратный трюк со временем: система прячет реальную работу внутри человеческого движения. И, пожалуй, именно так выглядят лучшие оптимизации — пользователь их не замечает, потому что ему просто не приходится ждать.
Источники
- Оригинальная статья Рюртяна Пула: https://ruurtjan.com/articles/p99-0ms-autocomplete-for-240-million-domain-names
- Wirewiki: https://www.wirewiki.com/
- Рейтинг доменов Tranco: https://tranco-list.eu/
- Служба централизованного доступа к данным зон CZDS: https://czds.icann.org/home
- Обзор Programming Digest: https://programmingdigest.net/newsletters/2309-incredibly-fast-autocomplete-with-in-memory-trie
- Страница публикации на daily.dev: https://daily.dev/posts/p99-0-ms-autocomplete-for-240-million-domain-names-h4tmbkwgo
- Испанский обзор ojeo.com: https://ojeo.com/noticia/autocompletado-de-240-millones-de-dominios-con-latencia-p99-de-0-ms/