Прошёл второй день РИТ++, и по горячим следам мы хотим рассказать о том, как всем миром пытались сломать нашу голосовалку. Под катом — код, метрики, имена победителей и самых активных участников, и прочие грязные подробности.
Незадолго перед РИТ++ мы задумались, чем можно развлечь народ? Решили сделать голосовалку за самый крутой язык программирования. И чтобы результаты в реальном времени выводились на дашборд. Процедуру голосования сделали простой: можно было с любого устройства зайти на сайт ODN.PW, указать своё имя и e-mail, и проголосовать за какой-нибудь язык. Со списком языков не мудрили — взяли девять самых популярных по данным GitHub, десятым пунктом сделали язык “Other”. Цвета для плашек тоже взяли с GitHub.
Но мы понимали, что накрутки будут неизбежны, причём с применением «тяжёлой артиллерии» — все свои, аудитория продвинутая, это же не голосование на мамочкином форуме. Поэтому мы решили приветствовать накрутки всеми возможными способами. Более того, предложили попробовать сообществу положить нашу голосовалку большой нагрузкой. А чтобы участникам было ещё проще, выложили ссылку на API для накруток с помощью ботов. И заодно решили наградить поощрительными призами первых трёх участников с наибольшим количеством RPS. Отдельная номинация была заготовлена для того силача, который сможет поломать голосовалку в одиночку.
День первый
Голосовалку запустили почти с самого начала работы РИТ++, и работала она до 18 часов. Наше развлечение понравилось посетителям и докладчикам РИТ++. Специалисты по высокопроизводительным сервисам активно включились в гонку за RPS. Гости стенда живо обсуждали способы положить голосовалку. Стихийно возникали команды адептов того или иного языка, которые начинали придумывать стратегии продвижения. Кто-то тут же садился и начинал писать микросервисы или ботов для участия в голосовании.
Некоторые компании, участвующие в РИТ++ и предоставляющие услуги защищённых хостингов, тоже включились в наше соревнование. К самому концу дня совместными усилиями участники всё же смогли ненадолго положить систему. Ну как «положили» — сервис-то работал, просто мы упёрлись в потолок по количеству одновременно регистрируемых голосов. Поэтому к 18 часам мы приостановили голосование, иначе результаты были бы недостоверными.
По результатам первого дня мы получили 160 млн голосов, а пиковая нагрузка достигала 20 000 RPS. Любопытно, что в этот день первое и второе места заняли активный участник РИТ++
Николай Мациевский (Айри) и спикер Елена Граховац из Openprovider.
Ночью мы подготовились к следующему дню, чтобы встретить его во всеоружии: оптимизировали общение с базой и поставили nginx перед Node.js-приложением на каждом воркере.
День второй
Многих заинтересовало наше предложение положить голосовалку, ведь гонка за RPS — задача увлекательная. Утром нас уже «ждали»: едва мы переключили DNS, как количество RPS взлетело до 100 000. И через полчаса нагрузка поднялась до 300 000 RPS.
Забавно, что когда мы только приступали к разработке голосовалки, то решили, что «неплохо было бы поддерживать 100 000 RPS». И на всякий случай заложили максимальную производительность в 1 млн RPS, но при этом даже всерьёз не рассматривали возможность приближения к такому показателю. А к середине второго дня уже практически делали ставки на то, пробьём ли потолок в миллион запросов в секунду. В результате мы достигли порядка 500 000 RPS.
Реализация
Проект мы запилили втроём за 1,5 дня, перед самым РИТ++. Голосовалку разместили в облачном сервисе Google Cloud Platform. Архитектура трёхуровневая:
• Верхний уровень: балансировщик, выступающий в роли фронтенда, на который приходит поток запросов. Он раскидывает нагрузку по серверам.
• Средний уровень: бэкенд на Node.js 8.0. Количество задействованных машин масштабируется в зависимости от текущей нагрузки. Делается это экономно, а не с запасом, чтобы не переплачивать впустую. К слову, проектик обошёлся в 8000 рублей.
• Нижний уровень: кластеризованная MongoDB для хранения голосов, состоящая из трёх серверов (один master и два slave’а).
Все компоненты голосовалки — open source, доступны на Github:
• Backend: https://github.com/spukst3r/counter-store
• Frontend: https://github.com/weglov/treechart
Во время разработки бэкенда в воздухе витала идея кешировать каждый запрос на накрутку голосов и периодически отправлять их в базу. Но из-за нехватки времени, неуверенности в количестве участников и банальной лени было решено отложить эту идею и оставить отправку данных в базу на каждый запрос. Заодно и производительность MongoDB в таком режиме проверить.
Что ж, как показал первый день, прикрутить кеш надо было сразу. Каждый воркер Node.js выдавал не больше 3000 RPS на каждый POST на /poll, а мастер MongoDB тяжело кашлял с LA >100. Не очень помогла даже оптимизация агрегации запросов для получения статистики путём изменения read preference на использование slave'ов для чтения. Ну ничего, самое время реализовать кеш для накрутки счётчиков и для проверки валидности email'а (который был завёрнут в простой _.memoize, ведь мы никогда не удаляем пользователей). Также мы задействовали новый проект в Google Compute Engine, с бОльшими квотами.
После включения кеширования голосов MongoDB чувствовала себя превосходно, показывая LA <1 даже в пике загрузки. А производительность каждого воркера выросла на 50% — до 4500 RPS. Для периодической отправки данных мы использовали bulkWrite с отключённым параметром ordered, чтобы оставить на стороне базы очередность исполнения запросов для оптимизации скорости.
В первый день на каждом воркере работал Node.js-сервер, создающий через модуль cluster четыре дочерних процесса, каждый из которых слушал порт 3000. Для второго дня мы отказались от такого сервера и отдали обработку HTTP «профессионалам». Опыты показали, что nginx, взаимодействующий с приложением через unix-сокет, даёт примерно +500 RPS. Настройка достаточно стандартная для большого количества соединений: увеличенный worker_rlimit_nofile, достаточный worker_connections, включенный tcp_nopush и tcp_nodelay. Кстати, отключение алгоритма Нейгла помогло поднять RPS и в Node.js. В каждой виртуалке потребовалось увеличить лимит на количество открытых файлов и максимальный размер backlog'а.
Итоги
За два дня ни одному участнику в одиночку не удалось положить наш сервис. Но в конце первого дня общими усилиями добились того, что система не успевала регистрировать все входящие запросы. На второй день мы поставили рекорд в нагрузке ~450 000 RPS. Различие в показаниях RPS на фронте (который высчитывал и усреднял RPS по фактическим записям в базе) и показания мониторинга Google пока остаётся для нас тайной.
И рады объявить победителей нашего маленького соревнования:
1 место — { "_id": "ivan@buymov.ru", "count": 2107126721 }
2 место — { "_id": "burik666@gmail.com", "count": 1453014107 }
3 место — { "_id": "256@flant.com", "count": 626160912 }
Для получения призов пишите kosheleva_ingram_micro!
UPD: Зал славы TOP50
Комментарии (28)
onyxmaster
06.06.2017 18:08+15-е место, отлично, особенно пропорционально усилиям =)
А есть разбивка по языкам? Вообще ввязался только потому что за C# стало обидно, он вчера почти ниже всех был =)
UA3MQJ
06.06.2017 20:00+1Уря!
33. ua3mqj@… 963611
Если кому-то интересно.
Пристреливался с ноута на i7. Потом оставил на кухне на компе, где супруга кино обычно смотрит.
AMD Athlon(tm) ii x2 250 processor 3.00 ghz
ОС: Windows 7
Язык программирования — Elixir
Проект никакой не делал, просто из консоли запустил. В 100 процессов. По показаниям WIN, аплоад составлял 10 мегабит (потолок моего интернета).
кодrequire HTTPotion :observer.start Enum.map(1..100, fn(xx) -> spawn(fn -> Enum.map(1..1000000, fn(x) -> url = "http://stats.df.wtf/api/v1/poll" header = ["Content-Type": "application/json"] body = "{\"email\":\"ua3mqj@...\",\"language\":9}" result = HTTPotion.post(url, [body: body, headers: header]) end) end) end)
BuriK666
06.06.2017 20:12+2Я подпилил wrk (убрал парсинг ответа и писал несколько запросов подряд не дожидаясь ответа)
~300Mbit ~90k ppsdmalchenko
07.06.2017 09:51Красавчик) с какими параметрами запускал? Какая нагрузка на процессор была?
dmalchenko
06.06.2017 21:55+1в десяточку влез)
интересно как бы кто взламывал?
я нашел только несколько методов api(уже отключены)
http://stats.df.wtf/api/v1/poll?full=true
http://stats.df.wtf/api/v1/userstats
http://stats.df.wtf/api/v1/top
DzmitryT
06.06.2017 22:51+134. dzmitry_t@…
Консольный скриптик на коленке. Сразу на одной из рабочих виртуалок, а потом с домашнего неттопа-роутера (AMD C-70 2x1GHz). Вообще интересно, а сколько всего человек так или иначе пыталось DDOS'ить голосовалку?
Код#!/bin/bash
i=0;
while [ $i -le 1000000 ]; do
nohup curl --silent -H «Content-Type: application/json» -X POST -d '{ «email»: «dzmitry_t@..», «language»: 9}' http://stats.df.wtf/api/v1/poll > /dev/null 2>&1 &
let i=i+1;
done;jatx
07.06.2017 01:29+1В первый день я тупо сделал в баше цикл:
while true do curl ... done
И запустил таких около 1000 процессов.
На второй день не поленился и написал на Java такой вот класс:
Main.javaimport org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; /** * Created by jatx on 06.06.17. */ public class Main { private static volatile int success = 0; private static volatile int total = 0; private static int THREADS; public static void main(String[] args) { THREADS = Integer.parseInt(args[0]); for (int i=0; i<THREADS; i++) { Voter voter = new Voter(); voter.start(); } while (true) { try { Thread.sleep(2500); System.err.println(success + " / " + total); } catch (Exception e) {} } } private static class Voter extends Thread { @Override public void run() { HttpClient httpClient = HttpClientBuilder.create().build(); while (true) { try { HttpPost request = new HttpPost("http://stats.df.wtf/api/v1/poll"); request.setHeader("Content-type", "application/json"); StringEntity entity = new StringEntity("{\"email\":\"e.tabatsky@gmail.com\",\"language\":1"); request.setEntity(entity); HttpResponse response = httpClient.execute(request); String result = EntityUtils.toString(response.getEntity()); if (result.equals("{\"status\":\"ok\"}")) success++; total++; } catch (Exception e) { //e.printStackTrace(); } } } } }
shurup
07.06.2017 06:20В корпоративном чатике сообщают, что коллеги («Флант») оказались на третьем месте :-) Спасибо за фан!
evgensoft
07.06.2017 07:37Написал на GO, запустил в пару сотен потоков, уперся в потолок исходящего канала провайдера
З.Ы. 22 место
FunApple
07.06.2017 09:33+5За эти смайлики тем, кто пихает их куда ни попадя, нужно делать бОлЬнО
allexx
07.06.2017 09:34-2Ой, да ладно уж Вам (тут не вставлен emoji).
Alabastr
07.06.2017 10:36+2Все верно говорит. Как читать?
1) Самый активный «медалька» получит «приз»? — берем все смайлы.
2) Самый активный получит? — не берем смайлы вообще. Но тут уже начинаются опасения…
Или надо угадывать, какой смайл оставить, какой убрать?ookami_kb
07.06.2017 11:50Это как с матами. Умело ввернутый в нужной ситуации – отлично передает эмоциональный накал и придает экспрессии. Вставляемый же после каждой фразы "неопределенный артикль %ля" ничего кроме омерзения не вызывает.
BuriK666
07.06.2017 18:27А призы то будут?
GHostly_FOX
07.06.2017 18:59-1Я запили на Node.JS отправку запросов а несколько потоков.
К ним же добавил консольных PHP процессов, но они быстро ресурсы скушали… :( прошлось остаться только на Node.JS
allexx
Делаем зал славы TOP50 ботовод :) 10-15 минут.
UA3MQJ
Огласите весь список, пожалуйста (с) :)
allexx
ух, пока только TOP50 http://odn.pw/#/topkek
GHostly_FOX
Я 29-й ))))