Комментарии 43
Сразу по обьявлению конкурса понял, что этот этап соревнования не вполне четко описан и не совсем highload.
Понятно, что нужна in-memory db и относительно быстрый event loop.
Написал от балды сервер на Crystal (просто прямолинейно, с хэшами вместо массивов и без индексов). И результат мне лично понравился, хоть и далеко… Настроения адаптировать код под конкретную нагрузку не было, зато код читаем :)
https://github.com/akzhan/highload-cup-1/blob/master/src/cup1.cr
Непонятно, как именно запускались проекты, так что уточню — нода у вас была в один поток, как все обычно делают на подобных бенчмарках?
Ну таки сразу нужно было это делать, без многопоточности сравнивать как-то странно. Насколько я помню, тот же Go по умолчанию использует количество потоков по числу CPU.
(промазал, комментарий к ответу выше).
А вот уже после того как поменяли условия и 1 процесс был загружен на 100% и не справлялся, вот тогда уже стала необходимость распараллеливать нагрузку. Но я сначала решил сделать это на php, до node.js ещё просто руки не дошли.
del
Решил использовать разбиение трёхмерного массива совместно с SplFixedArray и потребление памяти упало ещё в 3 раза составив 704 МБ
Не понял почему вы не стали хранить данные в формате типа:
$visits[$row['id']] = [
$row['user'],
$row['location'],
$row['visited_at'],
$row['mark'],
];
заняло бы еще меньше на PHP 7 точно
$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 если что
Теперь понял, вы убрали оверхэд на создание мелкого массива свойств вовсе.
$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";
Для себя понял, что видимо индексированные массивы всетаки не до конца оптимизировали в 7ке по сравнению с fixed.
У вас в конструкторе $this->$mark ошибка, довольно странно что объекты заняли настолько много.
Вот твит Никиты Попова по этому поводу
Я собственно почему и решил попробовать вариант с классом, потому что раньше на конференции слышал, что они эффективнее работают чем массивы. Так и есть, в текущей таблице меньше класса ест память только SplFixedArray, правда разница аж в два раза.
Сейчас попробовал разбить объект на отдельные массивы в node.js там тоже стало памяти меньше занимать.
$visits[$row['id']] = implode(';', array_values($row));
не тестировали?
Еще можно хранить данные в бинарном виде. Это сэкономит кучу памяти, которая используются 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);
}
Я думал делать как в вашем коде, но хотелось бы всё таки использовать готовые библиотеки. Приблизительно такую реализацию я ожидал от swool_table.
Я вчера задал на гитхабе им вопрос github.com/swoole/swoole-src/issues/1345 и жду ответа, иначе смыла в нём никакого нету, я смогу написать на php с использованием shmop свой swool_table и он будет занимать меньше памяти.
попробовал:
var_export(read_from_heap($heaps, 1));
получил:
array (
'user' => 220939112,
'location' => 442239836,
'visited_at' => 556494424,
'mark' => 27,
);
цифры ненастоящие, такой оценки, пользователя и локации не может быть. Оценка вообще может быть только от 1 до 5.
Что-то пошло не так.
Похоже, что неправильно считается $startPosition, там больше бы подошло:
$startPosition = $id * BLOCK_LENGTH; // было $id - $heapIndex * 1000000
При беглом разборе не понял, зачем в сдвиге $heapIndex. И получается, что каждый следующий id затирал значения предыдущего.
Попробовал свой вариант - работает.
Теперь я понимаю смысл этих конкурсов!
Было желание участвовать, но времени не было.
и/или потоки?
Ниже пример просто на чтение текстового файла большого объема. На основе Вашего кода:
visits_i.txt — текстовый файл около 140 Мб каждый. Простые строки каждая по 64 символа латиница
take.ms/RLGBS
duration_ts = 2.266 sec
memory usage: 153
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
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
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 поставил в правильном месте? По окончании чтения всех файлов.
те программисты, которые используют чтение с диска во время нагрузки получают решения, которые медленнее в сотни и тысячи раз, но скорее всего вообще падают. все решения в топ 100 не используют чтения с диска под такой нагрузкой. именно поэтому я загружаю все данные в память. на php моё решение держит нагрузки и всего в 4 раза медленнее, чем первое место и при этом моё решение отвечает на http-запросы за время от 300 микросекунд до 6 миллисекунд, т.е. за 0.0003-0.006 секунды.
лучшее решение на ноде, которое есть в рейтинге, держит все данные в памяти и в 20 раз медленнее моего. вы можете попробовать написать своё решение использующее чтение с диска под такой нагрузкой и посмотреть что из этого выйдет.
Песочница будет работать ещё неделю, вы можете поэкспериментировать и проверить разные гипотезы. Я проверил на своём стеке около 10 гипотез, часть из них я описал в статье.
Там C+nxweb (а в первом туре еще whitedb для базы было).
Это я зашифровался так :)
Поправка:
вы устанавливаете (по тексту) библиотеку Event (https://www.php.net/manual/ru/book.event.php) в workerman подключаете Ev (https://www.php.net/manual/ru/book.ev.php) и пишите про libevent (ne вы наверно переработанный Event имели в виду но тем не менее это отдельная старая библиотека http://php.adamharvey.name/manual/ru/book.libevent.php)
Вообще мне кажется ради сжатия данных используя на +100500 запросах pack (вот как за первые места) будет существенная потеря в скорости на время сжатия (плюс там куда вы их передаете так же на распаковку)
По поводу библиотеки apc_cache (или новой apcu) у нее есть существенный минус - данные храниться либо в FPM (и делаться между всеми другими php-FPM процессами не уничтожаясь при завершении работы скрипта) , а в CLI (а workerman и сервер в целом это CLI процесс - те демон некий в) что что создано остается в cli (и уничтожается когда этот cli завершает свою работу и между другими cli не передается)
что же касается Redis при всей его быстроте он рассчитан на нагрузку (данные взяты с оф сайта https://redis.io/docs/reference/optimization/benchmarks/) на 60.000 запросов в секунду на хороших машинах , при этом заявлено что и пользователей может держать столько же без особой потери в скорости
На среднем сервере (и опять же данные с того же сайта) там ~20000 запросов в секунду (у меня и того меньше было на тесте)
так что полагаю он был бы самым медленным решением
По следам highloadcup: php vs node.js vs go, swoole vs workerman, splfixedarray vs array и многое другое