Как стать автором
Обновить

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

>научился использовать yandex tank. У меня наоборот — одно из основных полученных знаний: «никогда не используй танк для хайлоад тестов». Там баги лезли изо всех щелей с самого начала: неправильная обработка connection close (что сыграло на руку наивным гошникам поначалу), «пики» (это отдельная песня, слово «пик» в официальном чате уступало по встречаемости разве что epoll(0). В чате участниками открывались новые учения (гадания по пикам, пики: путь к просвещению, роль пик в становлении христианства и пр.). До конца победить пики так и не смогли и они до сих пор проскакивают даже в песочнице. Для меня большая загадка почему орги мейлру решили использовать именно этот клиент.
да, яндекс танк вёл себя как сумасшедший. при чём локально он был ещё более менее стабильный, а вот на серверах организаторов был абсолютно не предсказуем
НЛО прилетело и опубликовало эту надпись здесь
мир неожиданно теснее чем я думал :)

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


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


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


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

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


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

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

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

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

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

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

сейчас в рейтинге уже есть решение на cluster, оно набрало 8843 секунд. Не уверен, что напишу лучше. Я не большой специалист в node.js — я писал всего три приложения (включая это).
Решил использовать разбиение трёхмерного массива совместно с SplFixedArray и потребление памяти упало ещё в 3 раза составив 704 МБ


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

заняло бы еще меньше на PHP 7 точно
Надеюсь это было не на PHP 5 ???
ахаха, хорошая шутка, на конкурсе по хайлоаду использовать докер-контенер, работать с шаред мемари, и при этом php 5, почему не 4 или апач?
$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 если что
Внутри тоже можно использовать fixed.
Теперь понял, вы убрали оверхэд на создание мелкого массива свойств вовсе.
ваш вариант 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";
Спасибо за статью, интересная тема оказалось.
Для себя понял, что видимо индексированные массивы всетаки не до конца оптимизировали в 7ке по сравнению с fixed.

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

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

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

Еще можно хранить данные в бинарном виде. Это сэкономит кучу памяти, которая используются 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 затирал значения предыдущего.

Попробовал свой вариант - работает.

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

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

Поправка:

вы устанавливаете (по тексту) библиотеку 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 запросов в секунду (у меня и того меньше было на тесте)

так что полагаю он был бы самым медленным решением

Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории