«Сломай голосовалку на РИТ++». Даёшь 1 000 000 RPS


    Прошёл второй день РИТ++, и по горячим следам мы хотим рассказать о том, как всем миром пытались сломать нашу голосовалку. Под катом — код, метрики, имена победителей и самых активных участников, и прочие грязные подробности.


    Незадолго перед РИТ++ мы задумались, чем можно развлечь народ? Решили сделать голосовалку за самый крутой язык программирования. И чтобы результаты в реальном времени выводились на дашборд. Процедуру голосования сделали простой: можно было с любого устройства зайти на сайт 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

    Ingram Micro Cloud
    0,00
    Дистрибьюторская платформа для облачных сервисов
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 33

      +1
      Делаем зал славы TOP50 ботовод :) 10-15 минут.
      +1
      5-е место, отлично, особенно пропорционально усилиям =)
      А есть разбивка по языкам? Вообще ввязался только потому что за C# стало обидно, он вчера почти ниже всех был =)
        +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)
        

        зы. Ждем подробностей от остальных участников!
          +2
          Я подпилил wrk (убрал парсинг ответа и писал несколько запросов подряд не дожидаясь ответа)
          ~300Mbit ~90k pps
            0
            У меня первой мыслью было JMeter натравить. Но посчитал его слишком тяжелым и отказался от затеи.
              0
              Красавчик) с какими параметрами запускал? Какая нагрузка на процессор была?
                0
                taskset -a -c 0-24 ./wrk -c 24000 -t 24 -d 36000 --timeout 5
                CPU usage

                Network

                Netstat

            –3
            Mongo

            Tarantool сделал бы монгу, как стоячую.

              +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
                +1
                18-й :)
                  0
                  Писал на Java, запускал с рабочего компа. Уперлось все в исходящий канал 0.5мбит/с — 150rps. Запустил на VPS, но проц очень быстро за нагрузку порезали с 0.5 ядра до 0.35, итог ~3100rps
                  +1
                  34. 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;
                    +1

                    В первый день я тупо сделал в баше цикл:


                    while true 
                    do
                         curl ...
                    done

                    И запустил таких около 1000 процессов.


                    На второй день не поленился и написал на Java такой вот класс:


                    Main.java
                    import 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();
                                    }
                                }
                             }
                        }
                    }

                    Запустил в 12500 потоков, больше ресурсы ноута не позволяли.
                    Итого 21 место :)

                      0
                      В корпоративном чатике сообщают, что коллеги («Флант») оказались на третьем месте :-) Спасибо за фан!
                        0
                        Написал на GO, запустил в пару сотен потоков, уперся в потолок исходящего канала провайдера
                        З.Ы. 22 место
                          +5
                          За эти смайлики тем, кто пихает их куда ни попадя, нужно делать бОлЬнО
                            –2
                            Ой, да ладно уж Вам (тут не вставлен emoji).
                              +2
                              Все верно говорит. Как читать?
                              1) Самый активный «медалька» получит «приз»? — берем все смайлы.
                              2) Самый активный получит? — не берем смайлы вообще. Но тут уже начинаются опасения…
                              Или надо угадывать, какой смайл оставить, какой убрать?
                                0

                                Это как с матами. Умело ввернутый в нужной ситуации – отлично передает эмоциональный накал и придает экспрессии. Вставляемый же после каждой фразы "неопределенный артикль %ля" ничего кроме омерзения не вызывает.

                                  0
                                  Ребусы современные они такие… Эмоциональные…
                                    0
                                    Все так. Сделал вывод о необходимости подучить грамматику и лексику с emoji.
                              0
                              А призы то будут?
                                0
                                Для получения призов пишите kosheleva_ingram_micro
                                  0
                                  Дважды написал. нет ответа.
                                    0
                                    Ну как там приз?
                                      0
                                      Так никто и не ответил.
                                        0
                                        В следующий раз надо сломать им голосовалку
                                          0
                                          «голосовалку» и не сломать, а набить…
                                      0
                                      Так блэт, что-то пошло не так. Написал вам в тви в ПМ чтобы разрешить.
                                  –1
                                  Я запили на Node.JS отправку запросов а несколько потоков.
                                  К ним же добавил консольных PHP процессов, но они быстро ресурсы скушали… :( прошлось остаться только на Node.JS

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое