Как Мы Сканировали Весь Интернет

Всем привет! Меня зовут Александр и я пишу код для 2ip.ru. За добрую половину сервисов меня можно кикнуть, я готов дать отпор.

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

Это, конечно, не «большие данные», но это всё равно достаточно большой объём информации, поэтому думаю будет интересно.

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

Как это всегда работало? Мы зашли в Bing из большого пула адресов и проанализировали результаты по специальному запросу.

Да, решение было так себе, но как бы то ни было.

Это произошло потому, что Бинг закрутил гайки и мы решили сделать всё по-человечески.



Собственная база

А что если взять и разобрать весь Интернет? В целом это не проблема, но мы не Google и не обладаем большими ресурсами для сканирования.

Или мы? Есть сервер с 12 ядрами и 64 гигами памяти, а в арсенале MySQL, PHP, golang и еще куча разных фреймворков.

Очевидно, что благодаря горутинам можно добиться хороших результатов.

Golang работает быстро и требует минимальных ресурсов.

Вопросы по базе данных: справится ли со всем этим обычный MySQL? Давай попробуем.



Создание прототипа

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

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

Итак, у меня на диске есть CSV-файл размером 5 ГБ, осталось написать масс-резольвер, который будет читать построчно и выводить в STDOUT, выдавая пару «домен — IP-адрес».

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

Несколько часов работы и мой го-демон готов.

Главное получилось примерно так:

  
  
   

func main() { file, err := os.Open("domains.txt") if err != nil { log.Fatal(err) } defer file.Close() maxGoroutines := 500 guard := make(chan struct{}, maxGoroutines) scanner := bufio.NewScanner(file) for scanner.Scan() { guard <- struct{}{} host := scanner.Text() go func(host string) { resolve(host) <-guard }(host) } if err := scanner.Err(); err != nil { log.Fatal(err) } }

Суть в том, что мы можем создать пул из 500 горутин и непрерывно обрабатывать большой объем данных в единицу времени, равномерно загружая все 12 ядер сервера.

Функция разрешения опущена, но кратко — это обычный IP-резольвер с выводом результата в STDOUT. Мы связываемся с DNS, получаем A-записи и отображаем результат.

DNS

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

В несколько кликов я получил рекурсивный DNS, попробовал его запустить и разочаровался.

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

Это происходит очень и очень медленно, около десятка доменов в секунду.

Вторая версия Google DNS, та, что с четырьмя восьмёрками, оказалась намного быстрее.

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

Тестируем на локальном хосте и в продакшене

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

Машина не могла обработать 500 горутин; процесс аварийно завершился через несколько секунд. А вот на боевом сервере все кардинально изменилось.

1000 горутин падали на 12 ядрах, но 500 практически не нагружали процессор и работали стабильно.

Мощность оказалась ~2000 доменов в секунду.

Это приемлемо; в такой ситуации всю базу можно разобрать за пару дней.

На практике все оказалось немного хуже, TLD .

bar очень глупый, о котором, наверное, нормальный человек никогда не слышал.

В итоге я оставил процесс в tmux и через три дня получил CSV объемом 10 ГБ.

Вперед, продолжать.

Ура! Перейдем к следующему шагу.



База данных

Я создал таблицу domain_ip, в которой всего два столбца — домен и IP. Оба не уникальны; один домен может иметь несколько IP-адресов.

IP — это обычный домен BIGINT — VARCHAR 255.

Индексы

Очевидно, что выборка 260 миллионов записей — это довольно большая работа.

Поэтому без индексов нам не обойтись; мы ищем по IP-адресу, а это значит, что индексируем его.

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

260 миллионов записей — это очень много.

Перейдем к плану Б.



Разделение

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

Весь пул IP-адресов я разделил на 20 таблиц с шагом 200 миллионов.

Получилось примерно так:

ALTER TABLE domain_ip PARTITION BY RANGE COLUMNS (ip) ( PARTITION p0 VALUES LESS THAN (200000000), PARTITION p1 VALUES LESS THAN (400000000), PARTITION p2 VALUES LESS THAN (600000000), PARTITION p3 VALUES LESS THAN (800000000), PARTITION p4 VALUES LESS THAN (1000000000), PARTITION p5 VALUES LESS THAN (1200000000), PARTITION p6 VALUES LESS THAN (1400000000), PARTITION p7 VALUES LESS THAN (1600000000), PARTITION p8 VALUES LESS THAN (1800000000), PARTITION p9 VALUES LESS THAN (2000000000), PARTITION p10 VALUES LESS THAN (2200000000), PARTITION p11 VALUES LESS THAN (2400000000), PARTITION p12 VALUES LESS THAN (2600000000), PARTITION p13 VALUES LESS THAN (2800000000), PARTITION p14 VALUES LESS THAN (3000000000), PARTITION p15 VALUES LESS THAN (3200000000), PARTITION p16 VALUES LESS THAN (3400000000), PARTITION p17 VALUES LESS THAN (3600000000), PARTITION p18 VALUES LESS THAN (3800000000), PARTITION p19 VALUES LESS THAN (4000000000), PARTITION p20 VALUES LESS THAN (MAXVALUE) );

И как вы поняли, что это работает, иначе зачем эта статья?

Импортировать

Любой, кто работал с MySQL, знает, что создание больших дампов данных — довольно длительная операция.

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

LOAD DATA INFILE '/tmp/domains.csv' IGNORE INTO TABLE domain_ip FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n'

Машина обрабатывает CSV-файл размером ~10 ГБ за 30 минут.

Финал

Как результат получился таким милым услуга .

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

Для всего этого вам понадобится около 8 ГБ оперативной памяти.

Сейчас вы можете узнать, например, что к IP 8.8.8.8 человечество присоединило 8194 домена, или додумайтесь сами.

Спасибо за внимание.

Теги: #linux #Разработка сайтов #Высокая производительность #Go #Big Data #bigdata #MySQL #разбиение на разделы #сканирование #2ip.ru

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.