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


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


  1. allexx
    06.06.2017 17:39
    +1

    Делаем зал славы TOP50 ботовод :) 10-15 минут.


    1. UA3MQJ
      06.06.2017 17:40

      Огласите весь список, пожалуйста (с) :)


      1. allexx
        06.06.2017 17:56
        +1

        ух, пока только TOP50 http://odn.pw/#/topkek


        1. GHostly_FOX
          07.06.2017 05:11
          +1

          Я 29-й ))))


  1. onyxmaster
    06.06.2017 18:08
    +1

    5-е место, отлично, особенно пропорционально усилиям =)
    А есть разбивка по языкам? Вообще ввязался только потому что за C# стало обидно, он вчера почти ниже всех был =)


  1. 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)
    


    1. BuriK666
      06.06.2017 20:12
      +2

      Я подпилил wrk (убрал парсинг ответа и писал несколько запросов подряд не дожидаясь ответа)
      ~300Mbit ~90k pps


      1. DzmitryT
        06.06.2017 23:05

        У меня первой мыслью было JMeter натравить. Но посчитал его слишком тяжелым и отказался от затеи.


      1. dmalchenko
        07.06.2017 09:51

        Красавчик) с какими параметрами запускал? Какая нагрузка на процессор была?


        1. BuriK666
          07.06.2017 10:10

          taskset -a -c 0-24 ./wrk -c 24000 -t 24 -d 36000 --timeout 5

          CPU usage


  1. antonksa
    06.06.2017 21:10
    -3

    Mongo

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


  1. 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


  1. alexover
    06.06.2017 21:55
    +1

    18-й :)


    1. alexover
      07.06.2017 10:16

      Писал на Java, запускал с рабочего компа. Уперлось все в исходящий канал 0.5мбит/с — 150rps. Запустил на VPS, но проц очень быстро за нагрузку порезали с 0.5 ядра до 0.35, итог ~3100rps


  1. DzmitryT
    06.06.2017 22:51
    +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. jatx
    07.06.2017 01:29
    +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();
                    }
                }
             }
        }
    }


  1. shurup
    07.06.2017 06:20

    В корпоративном чатике сообщают, что коллеги («Флант») оказались на третьем месте :-) Спасибо за фан!


  1. evgensoft
    07.06.2017 07:37

    Написал на GO, запустил в пару сотен потоков, уперся в потолок исходящего канала провайдера
    З.Ы. 22 место


  1. FunApple
    07.06.2017 09:33
    +5

    За эти смайлики тем, кто пихает их куда ни попадя, нужно делать бОлЬнО


    1. allexx
      07.06.2017 09:34
      -2

      Ой, да ладно уж Вам (тут не вставлен emoji).


      1. Alabastr
        07.06.2017 10:36
        +2

        Все верно говорит. Как читать?
        1) Самый активный «медалька» получит «приз»? — берем все смайлы.
        2) Самый активный получит? — не берем смайлы вообще. Но тут уже начинаются опасения…
        Или надо угадывать, какой смайл оставить, какой убрать?


        1. ookami_kb
          07.06.2017 11:50

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


        1. Louter
          07.06.2017 13:44

          Ребусы современные они такие… Эмоциональные…


          1. allexx
            07.06.2017 14:29

            Все так. Сделал вывод о необходимости подучить грамматику и лексику с emoji.


  1. BuriK666
    07.06.2017 18:27

    А призы то будут?


    1. allexx
      07.06.2017 23:34

      Для получения призов пишите kosheleva_ingram_micro


      1. BuriK666
        07.06.2017 23:56

        Дважды написал. нет ответа.


  1. GHostly_FOX
    07.06.2017 18:59
    -1

    Я запили на Node.JS отправку запросов а несколько потоков.
    К ним же добавил консольных PHP процессов, но они быстро ресурсы скушали… :( прошлось остаться только на Node.JS