По следам highloadcup: php vs node.js vs go, swoole vs workerman, splfixedarray vs array и многое другое

    Рассказ о том как я участвовал в highloadcup (чемпионат для backend-разработчиков) от Mail.Ru, написал на php сервер обслуживающий 10000 RPS, но всё равно не получил победную футболку.



    Вступление


    Итак начнём с футболки. Своё решение нужно было написать до первого сентября, а я дописал только шестого.

    Первого сентября были подведены итоги, места распределены, всем кто уложился в тысячу секунд пообещали футболки. После этого организаторы позволили ещё неделю улучшать свои решения, но уже без призов. Этим временем я и воспользовался, чтобы переписать своё решение (на самом деле мне не хватило всего пару вечеров). Собственно футболка мне не положена, а жаль :(

    Во время своей предыдущей статьи я сравнивал библиотеки на php для создания вебсокет-сервера тогда мне порекомендовали библиотеку swoole — она написана на C++ и устанавливается из pecl. К слову сказать, все эти библиотеки могут использоваться не только для создания вебсокет сервера, но подходят и просто для http-сервера. Этим я и решил воспользоваться.

    Я взял библиотеку swoole, создал базу данных sqlite в памяти и сразу поднялся в первую двадцатку с результатом 159 секунд, потом меня сместили, я добавил кеш и уменьшил время до 79 секунд, попал назад в двадцатку, меня сместили, переписал с sqlite на swoole_table и уменьшил время до 47 секунд. Конечно до первых мест мне было далеко, но мне удалось обойти в таблице моих нескольких знакомых с решением на Go.

    Так выглядит старая рейтинговая таблица сейчас:



    Немного похвалы для Mail.Ru и можно идти дальше.

    Благодаря этому замечательному чемпионату я более близко познакомился с библиотеками swoole, workerman, научился лучше оптимизировать php под высокие нагрузки, научился использовать yandex tank и многое другое. Продолжайте устраивать такие чемпионаты, соревновательность подстёгивает к изучению новой информации и прокачке навыков.

    php vs node.js vs go


    Для начала я взял swool, потому что он написан C++ и однозначно должен работать быстрее workerman, который написан на php.

    Я написал hello world код:

    $server = new Swoole\Http\Server('0.0.0.0', 1080);
    $server->set(['worker_num' => 1,]);
    $server->on('Request', function($req, $res) {$res->end('hello world');});
    $server->start();

    Запустил линуксовую консольную утилиту Apache Benchmark, которая делает 10к запросов в 10 потоков:

    ab -c 10 -n 10000 http://127.0.0.1:1080/

    и получил время ответа 0.17 ms

    После этого написал пример на workerman:

    require_once DIR . '/vendor/autoload.php';
    use Workerman\Worker;
    $http_worker = new Worker("http://0.0.0.0:1080");
    $http_worker->count = 1;
    $http_worker->onMessage = function($conn, $data) {$conn->send("hello world");};
    Worker::runAll();

    и получил 0.43 ms, т.е. результат в 3 раза хуже.
    Но я не сдавался, установил библиотеку event:

    pecl install event
    добавил в код:

    Worker::$eventLoopClass = '\Workerman\Events\Ev';

    Итоговый код
    require_once DIR . '/vendor/autoload.php';
    use Workerman\Worker;
    Worker::$eventLoopClass = '\Workerman\Events\Ev';
    $http_worker = new Worker("http://0.0.0.0:1080");
    $http_worker->count = 1;
    $http_worker->onMessage = function($conn, $data) {$conn->send("hello world");};
    Worker::runAll();

    Измерения показали 0.11 ms, т.е. workerman написанный на php и использующий libevent стал работать быстрее чем swoole написанный на C++. Я с помощью гуглтранслейта прочитал тонны документации на китайском. Но ничего не нашёл. К слову сказать обе библиотеки написаны китайцами и комментарии на китайском в коде библиотек для них — …

    Нормальная практика
    swoole:



    workerman:



    первая версия моей библиотеки для вебсокетов:



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

    Я завёл тикет на гитхабе swoole с вопросом как такое может происходить.

    Там мне порекомендовали использовать:

    $serv = new Swoole\Http\Server('0.0.0.0', 1080, SWOOLE_BASE);

    вместо:

    $serv = new Swoole\Http\Server('0.0.0.0', 1080);

    Итоговый код
    $serv = new Swoole\Http\Server('0.0.0.0', 1080, SWOOLE_BASE);
    $serv->set(['worker_num' => 1,]);
    $serv->on('Request', function($req, $res) {$res->end('hello world');});
    $serv->start();


    Я воспользовался их советом и получил 0.10 ms, т.е. чуть-чуть быстрее чем workerman.

    На этот момент у меня уже было готовое приложение на php, которое я уже не знал как оптимизировать, оно отвечало за 0.12 ms и решил переписать приложение на что-нибудь другое.

    Попробовал node.js:

    const http = require('http');
    const server = http.createServer(function(req, res) {
        res.writeHead(200);
        res.end('hello world');
    });
    server.listen(1080);

    получил 0.15 ms, т.е. на 0.03 ms меньше чем моё готовое приложение на php

    Взял fasthttp на go и получил 0.08 ms:

    Hello world на fasthttp
    package main
    
    import (
    	"flag"
    	"fmt"
    	"log"
    
    	"github.com/valyala/fasthttp"
    )
    
    var (
    	addr     = flag.String("addr", ":1080", "TCP address to listen to")
    	compress = flag.Bool("compress", false, "Whether to enable transparent response compression")
    )
    
    func main() {
    	flag.Parse()
    
    	h := requestHandler
    	if *compress {
    		h = fasthttp.CompressHandler(h)
    	}
    
    	if err := fasthttp.ListenAndServe(*addr, h); err != nil {
    		log.Fatalf("Error in ListenAndServe: %s", err)
    	}
    }
    
    func requestHandler(ctx *fasthttp.RequestCtx) {
    	fmt.Fprintf(ctx, "Hello, world!")
    }

    Итоговая таблица (таблица и все тесты на опубликованы на гитхабе):



    splfixedarray vs array


    За неделю до окончания конкурса условия немного усложнили:

    • объём данных увеличили в 10 раз
    • количество запросов в секунду увеличили в 10 раз

    Структура данных, которые нужно хранить — это 3 таблицы: users (1kk), locations (1kk) и visits (11kk).

    Описание полей
    User (Профиль):
    id — уникальный внешний идентификатор пользователя. Устанавливается тестирующей системой и используется для проверки ответов сервера. 32-разрядное целое беззнаковое число.
    email — адрес электронной почты пользователя. Тип — unicode-строка длиной до 100 символов. Уникальное поле.
    first_name и last_name — имя и фамилия соответственно. Тип — unicode-строки длиной до 50 символов.
    gender — unicode-строка m означает мужской пол, а f — женский.
    birth_date — дата рождения, записанная как число секунд от начала UNIX-эпохи по UTC (другими словами — это timestamp).

    Location (Достопримечательность):
    id — уникальный внешний id достопримечательности. Устанавливается тестирующей системой. 32-разрядное целое беззнаковоее число.
    place — описание достопримечательности. Текстовое поле неограниченной длины.
    country — название страны расположения. unicode-строка длиной до 50 символов.
    city — название города расположения. unicode-строка длиной до 50 символов.
    distance — расстояние от города по прямой в километрах. 32-разрядное целое беззнаковое число.

    Visit (Посещение):
    id — уникальный внешний id посещения. 32-разрядное целое беззнакое число.
    location — id достопримечательности. 32-разрядное целое беззнаковое число.
    user — id путешественника. 32-разрядное целое беззнаковое число.
    visited_at — дата посещения, timestamp.
    mark — оценка посещения от 0 до 5 включительно. Целое число.

    Моё решение перестало укладываться в выделенные для него 4Гб. Пришлось искать варианты.
    Для начала мне нужно было залить в память из json-файлов 11 миллионов записей.

    Попробовал swoole_table, замерил потребление памяти — 2200 Мб

    Код загрузки данных
    $visits = new swoole_table(11000000);
    $visits->column('id', swoole_table::TYPE_INT);
    $visits->column('user', swoole_table::TYPE_INT);
    $visits->column('location', swoole_table::TYPE_INT);
    $visits->column('mark', swoole_table::TYPE_INT);
    $visits->column('visited_at', swoole_table::TYPE_INT);
    $visits->create();
    
    $i = 1;
    while ($visitsData = @file_get_contents("data/visits_$i.json")) {
        $visitsData = json_decode($visitsData, true);
        foreach ($visitsData['visits'] as $k => $row) {
            $visits->set($row['id'], $row);
        }
        $i++;
    }
    unset($visitsData);
    
    gc_collect_cycles();
    
    echo 'memory: ' . intval(memory_get_usage() / 1000000) . "\n";

    Попробовал ассоциативный массив, потребление памяти сильно больше — 6057 Мб

    Код загрузки данных
    $visits = [];
    
    $i = 1;
    while ($visitsData = @file_get_contents("data/visits_$i.json")) {
        $visitsData = json_decode($visitsData, true);
        foreach ($visitsData['visits'] as $k => $row) {
            $visits[$row['id']] = $row;
        }
        $i++;echo "$i\n";
    }
    unset($visitsData);
    
    gc_collect_cycles();
    
    echo 'memory: ' . intval(memory_get_usage() / 1000000) . "\n";

    Попробовал SplFixedArray, потребление памяти немого меньше, чем у обычного массива — 5696 Мб

    Код загрузки данных
    $visits = new SplFixedArray(11000000);
    
    $i = 1;
    while ($visitsData = @file_get_contents("data/visits_$i.json")) {
        $visitsData = json_decode($visitsData, true);
        foreach ($visitsData['visits'] as $k => $row) {
            $visits[$row['id']] = $row;
        }
        $i++;echo "$i\n";
    }
    unset($visitsData);
    
    gc_collect_cycles();
    
    echo 'memory: ' . intval(memory_get_usage() / 1000000) . "\n";

    Решил отдельные свойства визита сохранять в отельные массивы, потому что текстовые ключи могут занимать в памяти много места, т.е. изменил такой код:

    $visits[1] = ['user' => 153, 'location' => 17, 'mark' => 5, 'visited_at' => 1503695452];

    на такой:

    $visits_user[1] = 153; 
    $visits_location[1] = 17; 
    $visits_mark[1] = 5; 
    $visits_visited_at[1] => 1503695452;

    потребление памяти при разбивке трёхмерного массива на двумерные составило — 2147 Мб, т.е. в 3 раза меньше. Т.о. имена ключей в трёхмерном массиве съедали 2/3 от всей занимаемой им памяти.

    Код загрузки данных
    $user = $location = $mark = $visited_at = [];
    
    $i = 1;
    while ($visitsData = @file_get_contents("data/visits_$i.json")) {
        $visitsData = json_decode($visitsData, true);
        foreach ($visitsData['visits'] as $k => $row) {
            $user[$row['id']] = $row['user'];
            $location[$row['id']] = $row['location'];
            $mark[$row['id']] = $row['mark'];
            $visited_at[$row['id']] = $row['visited_at'];
        }
        $i++;echo "$i\n";
    }
    unset($visitsData);
    
    gc_collect_cycles();
    
    echo 'memory: ' . intval(memory_get_usage() / 1000000) . "\n";

    Решил использовать разбиение трёхмерного массива совместно с SplFixedArray и потребление памяти упало ещё в 3 раза составив 704 МБ

    Код загрузки данных
    $user = new SplFixedArray(11000000);
    $location = new SplFixedArray(11000000);
    $mark = new SplFixedArray(11000000);
    $visited_at = new SplFixedArray(11000000);
    
    $user_visits = [];
    $location_visits = [];
    
    $i = 1;
    while ($visitsData = @file_get_contents("data/visits_$i.json")) {
        $visitsData = json_decode($visitsData, true);
        foreach ($visitsData['visits'] as $k => $row) {
            $user[$row['id']] = $row['user'];
            $location[$row['id']] = $row['location'];
            $mark[$row['id']] = $row['mark'];
            $visited_at[$row['id']] = $row['visited_at'];
    
            if (isset($user_visits[$row['user']])) {
                $user_visits[$row['user']][] = $row['id'];
            } else {
                $user_visits[$row['user']] = [$row['id']];
            }
    
            if (isset($location_visits[$row['location']])) {
                $location_visits[$row['location']][] = $row['id'];
            } else {
                $location_visits[$row['location']] = [$row['id']];
            }
        }
        $i++;echo "$i\n";
    }
    unset($visitsData);
    
    gc_collect_cycles();
    
    echo 'memory: ' . intval(memory_get_usage() / 1000000) . "\n";

    Ради интереса попробовал тоже самое на node.js и получил 780 Мб

    Код загрузки данных
    const fs = require('fs');
    
    global.visits = []; global.users_visits = []; global.locations_visits = [];
    
    let i = 1; let visitsData;
    
    while (fs.existsSync(`data/visits_${i}.json`) && (visitsData = JSON.parse(fs.readFileSync(`data/visits_${i}.json`, 'utf8')))) {
        for (y = 0; y < visitsData.visits.length; y++) {
            //visits[visitsData.visits[y]['id']] = visitsData.visits[y];
            visits[visitsData.visits[y]['id']] = {
                user:visitsData.visits[y].user,
                location:visitsData.visits[y].location,
                mark:visitsData.visits[y].mark,
                visited_at:visitsData.visits[y].visited_at,
                //id:visitsData.visits[y].id,
            };
        }
        i++;
    }
    
    global.gc();
    
    console.log("memory usage: " + parseInt(process.memoryUsage().heapTotal/1000000));

    Итоговая таблица (таблица и все тесты опубликованы на гитхабе):



    Хотел попробовать apc_cache и redis, но у них ещё дополнительно память тратится на хранения имён ключей. В реальной жизни можно использовать, но для этого чемпионата вообще не вариант.

    Послесловие


    После всех оптимизаций общее время составило 404 секунд, что почти в 4 раза медленнее, чем первое место.

    Ещё раз спасибо организаторам, которые не спали по ночам, перезагружали зависшие контейнеры, фиксили баги, допиливали сайт, отвечали в телеграмме на вопросы.

    Актуальные итоговые таблицы и сходный код всех тестов опубликованы на гитхабе:
    сравнение скорости работы разных вебсерверов
    сравнение расхода памяти разными структурами

    Другая моя сегодняшняя статья на Хабре: бесплатный сервер в облаке

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

    Участвовал ли ты в highloadcup?
    Хотел бы ты поучаствовать в следующем подобном чемпионате для backend разработчиков?
    Поделиться публикацией
    Комментарии 39
      +6
      >научился использовать yandex tank. У меня наоборот — одно из основных полученных знаний: «никогда не используй танк для хайлоад тестов». Там баги лезли изо всех щелей с самого начала: неправильная обработка connection close (что сыграло на руку наивным гошникам поначалу), «пики» (это отдельная песня, слово «пик» в официальном чате уступало по встречаемости разве что epoll(0). В чате участниками открывались новые учения (гадания по пикам, пики: путь к просвещению, роль пик в становлении христианства и пр.). До конца победить пики так и не смогли и они до сих пор проскакивают даже в песочнице. Для меня большая загадка почему орги мейлру решили использовать именно этот клиент.
        +2
        да, яндекс танк вёл себя как сумасшедший. при чём локально он был ещё более менее стабильный, а вот на серверах организаторов был абсолютно не предсказуем
        +1
        Забавно) читал ваш коммент на гите swoole про сравнение с workerman) это я предлагал вам перейти на go)
          +3
          мир неожиданно теснее чем я думал :)
          +4

          Сразу по обьявлению конкурса понял, что этот этап соревнования не вполне четко описан и не совсем highload.


          Понятно, что нужна in-memory db и относительно быстрый event loop.


          Написал от балды сервер на Crystal (просто прямолинейно, с хэшами вместо массивов и без индексов). И результат мне лично понравился, хоть и далеко… Настроения адаптировать код под конкретную нагрузку не было, зато код читаем :)


          https://github.com/akzhan/highload-cup-1/blob/master/src/cup1.cr

            +3

            попутно добавили в Crystal поддержку проверки на отсутствие ключа в объекте JSON/YAML, что позволит в будущем упростить код и избавиться от on_presence/on_absence.


            https://github.com/crystal-lang/crystal/issues/4840

            0

            Непонятно, как именно запускались проекты, так что уточню — нода у вас была в один поток, как все обычно делают на подобных бенчмарках?

              0
              да у меня был один поток, но он не держит нагрузку от рейтингового обстрела и нужно переписывать на node claster.
              +1

              Ну таки сразу нужно было это делать, без многопоточности сравнивать как-то странно. Насколько я помню, тот же Go по умолчанию использует количество потоков по числу CPU.
              (промазал, комментарий к ответу выше).

                0
                Моё решение делалось по предыдущие условия конкурса, тогда один поток был эффективнее, тогда в топе были решения на одном потоке. у меня процесс ел не больше 1% процессора, разбивать на два — только дополнительные расходы.
                А вот уже после того как поменяли условия и 1 процесс был загружен на 100% и не справлялся, вот тогда уже стала необходимость распараллеливать нагрузку. Но я сначала решил сделать это на php, до node.js ещё просто руки не дошли.
                  +1

                  Будет здорово, если попробуете — интересно, пока не видел внятных бенчмарков с кластером.

                    0
                    сейчас в рейтинге уже есть решение на cluster, оно набрало 8843 секунд. Не уверен, что напишу лучше. Я не большой специалист в node.js — я писал всего три приложения (включая это).
                +1

                del

                  0
                  Решил использовать разбиение трёхмерного массива совместно с SplFixedArray и потребление памяти упало ещё в 3 раза составив 704 МБ


                  Не понял почему вы не стали хранить данные в формате типа:
                  $visits[$row['id']] = [
                              $row['user'],
                              $row['location'],
                              $row['visited_at'],
                              $row['mark'],
                          ];
                  

                  заняло бы еще меньше на PHP 7 точно
                    0
                    Надеюсь это было не на PHP 5 ???
                      0
                      ахаха, хорошая шутка, на конкурсе по хайлоаду использовать докер-контенер, работать с шаред мемари, и при этом php 5, почему не 4 или апач?
                      +1
                      $visits = new SplFixedArray(11000000);
                      
                      $i = 1;
                      while ($visitsData = @file_get_contents("data/visits_$i.json")) {
                          $visitsData = json_decode($visitsData, true);
                          foreach ($visitsData['visits'] as $k => $row) {
                              $visits[$row['id']] = [$row['user'],
                                  $row['location'],
                                  $row['visited_at'],
                                  $row['mark'],
                              ];
                          }
                          $i++;echo "$i\n";
                      }
                      unset($visitsData);
                      
                      gc_collect_cycles();
                      
                      echo 'memory: ' . intval(memory_get_usage() / 1000000) . "\n";

                      ваш вариант — 3936 Мб, php7 если что
                        0
                        Внутри тоже можно использовать fixed.
                        Теперь понял, вы убрали оверхэд на создание мелкого массива свойств вовсе.
                          0
                          ваш вариант 2790 Мб:
                          $visits = new SplFixedArray(11000000);
                          
                          $i = 1;
                          while ($visitsData = @file_get_contents("data/visits_$i.json")) {
                              $visitsData = json_decode($visitsData, true);
                              foreach ($visitsData['visits'] as $k => $row) {
                                  $visits[$row['id']] = new SplFixedArray(4);
                                  $visits[$row['id']][0] = $row['user'];
                                  $visits[$row['id']][1] = $row['location'];
                                  $visits[$row['id']][2] = $row['visited_at'];
                                  $visits[$row['id']][3] = $row['mark'];
                              }
                              $i++;echo "$i\n";
                          }
                          unset($visitsData);
                          
                          gc_collect_cycles();
                          
                          echo 'memory: ' . intval(memory_get_usage() / 1000000) . "\n";

                          вариант с объектом 5510 Мб:
                          class MyArray {
                              public $user;
                              public $location;
                              public $visited_at;
                              public $mark;
                          
                              public function __construct($user, $location, $visited_at, $mark) {
                                  $this->user = $user;
                                  $this->location = $location;
                                  $this->visited_at = $visited_at;
                                  $this->$mark = $mark;
                              }
                          }
                          
                          $visits = new SplFixedArray(11000000);
                          
                          $i = 1;
                          while ($visitsData = @file_get_contents("data/visits_$i.json")) {
                              $visitsData = json_decode($visitsData, true);
                              foreach ($visitsData['visits'] as $k => $row) {
                                  $visits[$row['id']] = new MyArray(
                                      $row['user'], 
                                      $row['location'], 
                                      $row['visited_at'], 
                                      $row['mark']
                                  );
                              }
                              $i++;echo "$i\n";
                          }
                          unset($visitsData);
                          
                          gc_collect_cycles();
                          
                          echo 'memory: ' . intval(memory_get_usage() / 1000000) . "\n";
                            0
                            Спасибо за статью, интересная тема оказалось.
                            Для себя понял, что видимо индексированные массивы всетаки не до конца оптимизировали в 7ке по сравнению с fixed.
                              +1

                              У вас в конструкторе $this->$mark ошибка, довольно странно что объекты заняли настолько много.
                              Вот твит Никиты Попова по этому поводу

                                0
                                спасибо, исправил опечатку, результат 1430 Мб
                                Я собственно почему и решил попробовать вариант с классом, потому что раньше на конференции слышал, что они эффективнее работают чем массивы. Так и есть, в текущей таблице меньше класса ест память только SplFixedArray, правда разница аж в два раза.
                                Сейчас попробовал разбить объект на отдельные массивы в node.js там тоже стало памяти меньше занимать.
                            0
                            Что-нибудь типа:
                            $visits[$row['id']] = implode(';', array_values($row));

                            не тестировали?
                              0
                              735 Мб
                          0
                          А почему не пробовали вариант с golang?
                            0
                            в разделе «php vs node.js vs go» я пробовал fasthttp, который написан на golang
                            +1

                            Еще можно хранить данные в бинарном виде. Это сэкономит кучу памяти, которая используются php на хранение переменных. В итоге 1 visit — это 13 байт, 11M visits — 143MB.


                            define('BLOCK_LENGTH', 13);
                            
                            // fill 11 heaps with zeros
                            // 1 heap stores 1M visits (1 visit is 13 bytes, 1M visits - 13MB)
                            $heaps = [
                                str_repeat(pack('LLLc', 0, 0, 0, 0), 1000000),
                                str_repeat(pack('LLLc', 0, 0, 0, 0), 1000000),
                                str_repeat(pack('LLLc', 0, 0, 0, 0), 1000000),
                                str_repeat(pack('LLLc', 0, 0, 0, 0), 1000000),
                                str_repeat(pack('LLLc', 0, 0, 0, 0), 1000000),
                                str_repeat(pack('LLLc', 0, 0, 0, 0), 1000000),
                                str_repeat(pack('LLLc', 0, 0, 0, 0), 1000000),
                                str_repeat(pack('LLLc', 0, 0, 0, 0), 1000000),
                                str_repeat(pack('LLLc', 0, 0, 0, 0), 1000000),
                                str_repeat(pack('LLLc', 0, 0, 0, 0), 1000000),
                                str_repeat(pack('LLLc', 0, 0, 0, 0), 1000000)
                            ];
                            
                            echo 'memory: ' . intval(memory_get_usage() / 1000000) . "\n";
                            
                            $i = 1;
                            while ($visitsData = @file_get_contents("data/visits_$i.json")) {
                                $visitsData = json_decode($visitsData, true);
                                foreach ($visitsData as $k => $row) {
                                    $id = $row['id'];
                                    $heapIndex = intval(floor($id / 1000000));
                                    $startPosition = $id - $heapIndex * 1000000;
                            
                                    $data = pack('LLLc', 
                                        $row['user'],
                                        $row['location'],
                                        $row['visited_at'],
                                        $row['mark']
                                    );
                            
                                    for ($t = 0; $t < strlen($data); $t++) {
                                        $heaps[$heapIndex][$startPosition + $t] = $data[$t];
                                    }
                                }
                                echo "$i\n";
                                $i++;
                            }
                            unset($visitsData);
                            gc_collect_cycles();
                            
                            echo 'memory: ' . intval(memory_get_usage() / 1000000) . "\n";
                            
                            /**
                             * Get visitor by id
                             *
                             * @param string[] $heaps
                             * @param int $id
                             * @return array
                             */
                            function read_from_heap($heaps, $id)
                            {
                                $heapIndex = intval(floor($id / 1000000));
                                $startPosition = $id - $heapIndex * 1000000;
                                $data = substr($heaps[$heapIndex], $startPosition * BLOCK_LENGTH, BLOCK_LENGTH);
                                return unpack('Luser/Llocation/Lvisited_at/cmark', $data);
                            }
                              0
                              Очень ценный комментарий, большое вам спасибо.
                              Я думал делать как в вашем коде, но хотелось бы всё таки использовать готовые библиотеки. Приблизительно такую реализацию я ожидал от swool_table.
                              Я вчера задал на гитхабе им вопрос github.com/swoole/swoole-src/issues/1345 и жду ответа, иначе смыла в нём никакого нету, я смогу написать на php с использованием shmop свой swool_table и он будет занимать меньше памяти.
                                0
                                жаль только то что код с ошибками.
                                попробовал:
                                var_export(read_from_heap($heaps, 1));

                                получил:
                                array (
                                  'user' => 220939112,
                                  'location' => 442239836,
                                  'visited_at' => 556494424,
                                  'mark' => 27,
                                );

                                цифры ненастоящие, такой оценки, пользователя и локации не может быть. Оценка вообще может быть только от 1 до 5.
                                Что-то пошло не так.
                              0
                              За статью спасибо!
                              Теперь я понимаю смысл этих конкурсов!
                              Было желание участвовать, но времени не было.
                                +1
                                для версии на Node не пробовали использовать асинхронные версии методов чтения файлов?
                                и/или потоки?
                                  0
                                  а смысл? или если читать из файлов асинхронно, то тогда данные в памяти меньше будут занимать? чтение файлов и так быстро работает, к тому же на него переде запуском даётся достаточно времени на загрузку раз в 5-10 больше чем у меня всё успевает загрузиться. чтение необходимо только в самом начале, далее всё отдача в апи происходит из памяти.
                                    +1
                                    Данные в памяти в итоге могут занимать места меньше, могут так же. Потому что Вы уже их часть обработали, и отдали в АПИ или еще куда. И часть из памяти удалили… дожидаться загрузки всех данных, когда можно их уже начинать обрабатывать?

                                    Ниже пример просто на чтение текстового файла большого объема. На основе Вашего кода:
                                    visits_i.txt — текстовый файл около 140 Мб каждый. Простые строки каждая по 64 символа латиница

                                    take.ms/RLGBS
                                    duration_ts = 2.266 sec
                                    memory usage: 153
                                    fs.existsSync and fs.readFileSync
                                    const fs = require('fs');
                                    
                                    let start_ts = (new Date()).getTime();
                                    let i = 1;
                                    let visitsData;
                                    
                                    function fileSync()
                                    {
                                    	while (fs.existsSync(`./visits_${i}.txt`) && (visitsData = fs.readFileSync(`./visits_${i}.txt`, 'utf8')))
                                    	{
                                    		//...
                                    		console.log({
                                    			'file': `./visits_${i}.txt`,
                                    			'visitsData': visitsData.length
                                    		});
                                    		i++;
                                    	}
                                    }
                                    fileSync();
                                    
                                    let end_ts = (new Date()).getTime();
                                    let diffMts = end_ts - start_ts;
                                    let duration_ts = diffMts/1000;
                                    console.log(`duration_ts = ${duration_ts} sec`);
                                    
                                    global.gc();
                                    console.log("memory usage: " + parseInt(process.memoryUsage().heapTotal/1000000));
                                    



                                    take.ms/km7CT

                                    duration_ts = 2.318 sec — а бывало и меньше ( = 1.99 sec)
                                    memory usage: 11
                                    fs.stat & fs.readFile
                                    const fs = require('fs');
                                    let start_ts = (new Date()).getTime();
                                    let i = 1;
                                    let visitsData;
                                    //fs.exists - is Deprecated
                                    function fileAsync(i)
                                    {
                                    	let path = `./visits_${i}.txt`;
                                    	fs.stat(path, (err, stats)=>
                                    	{
                                    		if (err) //когда i++ файла не будет найдено 
                                    		{
                                    			let end_ts = (new Date()).getTime();
                                    			let diffMts = end_ts - start_ts;
                                    			let duration_ts = diffMts/1000;
                                    			console.log(`duration_ts = ${duration_ts} sec`);
                                    			global.gc();
                                    			console.log("memory usage: " + parseInt(process.memoryUsage().heapTotal/1000000));
                                    			return;
                                    		}
                                    		fs.readFile(path, {encoding: 'utf8'}, (err, visitsData) =>
                                    		{
                                    			if (err) throw err;
                                    			
                                    			//TODO parse visitsData
                                    			console.log({
                                    				'file': path,
                                    				'visitsData': visitsData.length
                                    			});
                                    			i++;
                                    			return fileAsync(i);
                                    		});
                                    	});
                                    }
                                    fileAsync(i);
                                    



                                    take.ms/UcgL9
                                    duration_ts = 1.167 sec
                                    memory usage: 12
                                    fs.createReadStream
                                    const fs = require('fs');
                                    
                                    let start_ts = (new Date()).getTime();
                                    let i = 1;
                                    let data;
                                    function streamFile(i)
                                    {
                                    	let path = `./visits_${i}.txt`;
                                    	const RS = fs.createReadStream(path);
                                    	console.log('path = ', path);
                                    	RS.on('data', (data)=>
                                    	{
                                    		//TODO parse data
                                    	});
                                    	RS.on('end', () =>
                                    	{
                                    		if (i >= 10)
                                    		{
                                    			let end_ts = (new Date()).getTime();
                                    			let diffMts = end_ts - start_ts;
                                    			let duration_ts = diffMts/1000;
                                    			
                                    			console.log(`duration_ts = ${duration_ts} sec`);
                                    			global.gc();
                                    			console.log("memory usage: " + parseInt(process.memoryUsage().heapTotal/1000000));
                                    		}
                                    		else
                                    		{
                                    			i++;
                                    			return streamFile(i);
                                    		}
                                    	});
                                    }
                                    streamFile(i);
                                    



                                    Вроде условия вывода memoryUsage поставил в правильном месте? По окончании чтения всех файлов.
                                      +1
                                      вы условия конкурса читали? 10 минут даётся времени на загрузка данных (в базу/память/куда угодно). у меня всё загружается за 1 минуту. оптимизировать время загрузки не вижу никакого смысла. далее на апи делают нагрузку до 10к запросов в секунду и читать их с диска под такой нагрузкой невозможно. это хайлоад. благо памяти хватает вместить все данные.
                                      те программисты, которые используют чтение с диска во время нагрузки получают решения, которые медленнее в сотни и тысячи раз, но скорее всего вообще падают. все решения в топ 100 не используют чтения с диска под такой нагрузкой. именно поэтому я загружаю все данные в память. на php моё решение держит нагрузки и всего в 4 раза медленнее, чем первое место и при этом моё решение отвечает на http-запросы за время от 300 микросекунд до 6 миллисекунд, т.е. за 0.0003-0.006 секунды.
                                      лучшее решение на ноде, которое есть в рейтинге, держит все данные в памяти и в 20 раз медленнее моего. вы можете попробовать написать своё решение использующее чтение с диска под такой нагрузкой и посмотреть что из этого выйдет.
                                      Песочница будет работать ещё неделю, вы можете поэкспериментировать и проверить разные гипотезы. Я проверил на своём стеке около 10 гипотез, часть из них я описал в статье.
                                        +1
                                        пОнято :) я же не говорил, что что-то сделали не так или не правильно. Решил поинтересоваться, а что если… Вы ответили на вопрос — спасибо!
                                  0
                                  В таблице вижу только потребление памяти, но непонятно, как эти разные способы влияют на скорость, особенно насколько медленнее Pack.
                                    0
                                    Время не вносил, потому что на это ушло бы очень много времени, т.к. у меня на ноутбуке был большой разброс, но могу точно сказать, что pack оказался сильно медленнее, чем swoole_pack. Насколько медленнее не скажу.
                                    0
                                    Интересно было бы также услышать решение Виталия Дятлова (PHP, MySQL+Memcache, Lightttpd), которое каким-то образом ещё в полтора раза быстрее.

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

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