воскресенье, 1 сентября 2013 г.

Ждем Mio в GHC 7.8

Когда в GHC 7.0 появился "Scalable IO Manager", то стало возможным писать на Haskell TCP/IP сервера и использовать их в реальных условиях.

По скорости работы с текстовыми данными (ByteString) и списками GHC почти не уступает OCaml, Mlton, manticore(http://manticore.cs.uchicago.edu/), PolyML проигрывает 20%. Но GHC имеет лучший GC, чем у OCaml и Mlton, а manticore находиться пока в разработке.

Все бы было хорошо, но вот реализация самого "Scalable IO Manager" не оптимальная и приводит почти к двукратному снижению производительности. В связи с этим стал смотреть в сторону multiMLton и manticore, а также PolyML. Оценил насколько быстро можно к их менеджерам легковесных потоков добавить IO менеджеры, и понял, что трата времени на это будет для меня не рациональна.

Одновременно с этим узнал, что производительность существующего в GHC IO Manager не устраивает не только меня и нашлись люди, которые сделали
Mio: A High-Performance Multicore IO Manager for GHC.

Что-ж, ждем выхода GHC 7.8, который будет включать в себя Mio...

вторник, 26 февраля 2013 г.

Haskell - энергичный лентяй

Какая то ленивость у Haskell не правильная, слишком энергичная. :-)
Да, ленивость - это дело не простое, тут уметь надо.

Возьмем для примера функцию foldl. Ну какой лентяй будет как сумасшедший создавать столько чанков, что стек переполняется. На это способна только бешеная лисица, которой сотня километров - не крюк, на и Haskell.

Вы можете возразить, что вот ленивые списки - это окно в бесконечность. Да любая комбинация из функций map свалила бы Haskell в эту бесконечность, если бы не тщательно прописанные правила преобразования различных комбинаций функций. Да и вместо длинных списков предпочтительней использовать Stream Fusion.

Ну что это за ленивый язык, которому надо говорить, как правильно лениться?

Возьмем ленивые строки. Ну какие они ленивые!

Разве ленивая строка при добавлении данных в конец будет как бешеная лисица пробегать по всем чанкам? Нет, она сохранит данные в некотором буфере и добавит лишь когда эти данные понадобятся.

Ну а когда в результате некоторой операции данные в первом чанке заканчиваются, так называемая ленивая строка энергично переходит к следующей порции данных, например, прочитай их из сокета. Ну не торопись, поленись немного, дай определить, что будет чтение новой порции данный!

Что тут можно сказать? Лень - это не просто. Правильно лениться - уметь надо!

среда, 6 февраля 2013 г.

RedisShardnig и GHC threaded

В последней версии RedisShardnig (http://github.com/kni/redis-sharding-hs-strict) буферизация данных и отправка их в сокеты происходит в отдельных user-lavel threads (forkIO), которым данные передаются посредством MVar переменных.

Разумеется, использование MVar имеет накладные расходы, но какие? Может, не смотря на некоторое усложнение кода, выгодней буфера таскать с собой?
Набросал простенький тестик, и кроме MVar добавил также RefIO. Тест показал, что и MVar и RefIO очень быстры и их можно использовать без опаски замедлить скорость выполнения программ.

Но тут я заметил, что компилировал без включения threaded ражима. А этот режим нужен, чтобы задействовать kqueue и epoll.
Скомпилировал я тесты в threaded режиме, запустил и увидел, что следует минимизировать их использование.

Но каждый тест является синтетическими, поэтому я решил вязать реальное приложение (RedisShardnig) и сделать версию с минимальным использованием MVar. В этой версии для буферизации не будут использоваться отдельные user-lavel threaded, с которыми общение происходит посредством MVar, а все буферизация будет происходить в рабочих threads и буыера будет таскаться с собой. Конечно множество MVar все равно останется, ведь при каждой операции записи чтения данных из сокетов используются MVar.

Текущая версия RedisShardnig (в таблице обозначена как mvar) и новая версия (в таблице обозначена как self) были собраны без и с поддержкой threaded режима и протестированы на производительность при помощи redis-benchmark (в 10 потоков). За RedisShardnig находилось 4 Redis node. Процессор имел 8 ядер. В таблице приведены тысячи SET запросов в секунду.

                 -P 1  -P 100
mvar             19      83
mvar threaded    10      93
self             23     116
self threaded    13     107

mvar -N2         21     166
self -N2         26     208

mvar -N4         23     232
self -N4         25     263


-P 100 - обозначает режим Pipeline с 100 команд в одном пакете (смотри redis-benchmark). То есть в этом режиме в 100 раз снижено количество отправки и получения данных из сокета.

Из таблицы видно, что для приложений с интенсивной сетевой нагрузкой включение threaded режима снижает производительность в 2 раза.
Впрочем и для приложений с другим характером нагрузки также заметно снижение производительность при использование threaded режима.
А не использовать threaded режим нельзя, так как для сетевых приложений необходим kqueue и epoll.
Кстати, разработчики GHC игнорируют это упущение.

четверг, 31 января 2013 г.

Haskell Lazy vs Strict ByteString for Network

У привязки ленивой строки с сокету при помощи getContents есть один недостаток.
Невозможно определить есть ли еще данные во входном буфере.

А это иногда очень важно знать. Например, чтобы сделать оптимальную буферизацию.

Невозможно определить из-за того, что операции над ленивыми строками построены так, что когда данные заканчиваются, то сразу происходит чтение новой порции из сокета, а не при следующем обращение к строке. И это при том, что для текущей операции существующих данных было достаточно.

пятница, 25 января 2013 г.

RedisSharding Benchmark

Let's go on testing RedisSharding, but this time we'll take redis-benchmark as the client. The conditions are the same.
Port:
6380 Redis (1 redis-server)
8000 RedisSharding (for 4 nodes)
9000 twemproxy (nutcracker) (for 4 nodes)

./redis_sharding --port=8000 --nodes=127.0.0.1:6381,127.0.0.1:6382,127.0.0.1:6383,127.0.0.1:6384 +RTS -s -N4 -A10M -qa

> redis-benchmark -p 8000 -n 10000 -c 10 -q -t set,get
SET: 24038.46 requests per second
GET: 24038.46 requests per second

> redis-benchmark -p 9000 -n 10000 -c 10 -q -t set,get
SET: 69444.45 requests per second
GET: 58823.53 requests per second
And now we'll enable the Pipeline mode and will be increasing the number of commands in one Pipeline gradually.
> redis-benchmark -p 8000 -n 10000 -c 10 -q -t set,get -P 100
SET: 217391.30 requests per second
GET: 250000.00 requests per second

> redis-benchmark -p 9000 -n 10000 -c 10 -q -t set,get -P 100
SET: 277777.78 requests per second
GET: 312500.00 requests per second
However, RedisSharding uses all the 4 core sets while the twemproxy is one-thread.
Let's launch the client with one connection:
> redis-benchmark -p 8000 -n 10000 -c 1 -q -t set,get -P 100
SET: 70422.54 requests per second
GET: 76335.88 requests per second

> redis-benchmark -p 9000 -n 10000 -c 1 -q -t set,get -P 100
SET: 227272.73 requests per second
GET: 263157.91 requests per second
But why should we abandon usage of all the processor's core sets?
> redis-benchmark -p 6380 -n 10000 -c 20 -q -t set,get -P 1000
SET: 256410.25 requests per second
GET: 232558.14 requests per second

> redis-benchmark -p 8000 -n 10000 -c 20 -q -t set,get -P 1000
SET: 147058.83 requests per second
GET: 151515.16 requests per second

> redis-benchmark -p 9000 -n 10000 -c 20 -q -t set,get -P 1000
SET: 129870.13 requests per second
GET: 100000.00 requests per second
We are increasing it more and we see that with such size of Pipeline it is Redis itself giving up.
> redis-benchmark -p 6380 -n 10000 -c 20 -q -t set,get -P 10000
SET: 17421.60 requests per second
GET: 21834.06 requests per second
While RedisSharding, judging from the above information, is perfectly fine:
> redis-benchmark -p 8000 -n 10000 -c 20 -q -t set,get -P 10000
SET: 13280.21 requests per second
GET: 19011.41 requests per second
What can't be said of nutcracker.
> redis-benchmark -p 9000 -n 10000 -c 20 -q -t set,get -P 10000
Writing to socket: Connection reset by peer
Writing to socket: Connection reset by peer
Writing to socket: Connection reset by peer
Writing to socket: Connection reset by peer
Writing to socket: Connection reset by peer
Writing to socket: Connection reset by peer
Writing to socket: Connection reset by peer
Writing to socket: Connection reset by peer
Writing to socket: Connection reset by peer
Writing to socket: Connection reset by peer
Writing to socket: Connection reset by peer
SET: 17064.85 requests per second
zmalloc: Out of memory trying to allocate 335799 bytes
Abort



> nutcracker-0.2.2/src/nutcracker -c twemproxy/nutcracker0.yml
[Fri Jan 25 09:36:19 2013] nc.c:177 nutcracker-0.2.2 started on pid 19962
[Fri Jan 25 09:36:19 2013] nc.c:181 run, rabbit run / dig that hole, forget the sun / and when at last the work is done / don't sit down / it's time to dig another one
[Fri Jan 25 09:38:28 2013] nc_util.c:223 malloc(16384) failed @ nc_mbuf.c:46
[Fri Jan 25 09:38:28 2013] nc_util.c:223 malloc(16384) failed @ nc_mbuf.c:46
[Fri Jan 25 09:38:28 2013] nc_util.c:223 malloc(16384) failed @ nc_mbuf.c:46
[Fri Jan 25 09:38:28 2013] nc_util.c:223 malloc(16384) failed @ nc_mbuf.c:46
[Fri Jan 25 09:38:28 2013] nc_util.c:223 malloc(16384) failed @ nc_mbuf.c:46
[Fri Jan 25 09:38:28 2013] nc_util.c:223 malloc(16384) failed @ nc_mbuf.c:46
[Fri Jan 25 09:38:28 2013] nc_util.c:223 malloc(16384) failed @ nc_mbuf.c:46
[Fri Jan 25 09:38:28 2013] nc_util.c:223 malloc(16384) failed @ nc_mbuf.c:46
[Fri Jan 25 09:38:28 2013] nc_util.c:223 malloc(16384) failed @ nc_mbuf.c:46
[Fri Jan 25 09:38:28 2013] nc_util.c:223 malloc(16384) failed @ nc_mbuf.c:46
[Fri Jan 25 09:38:28 2013] nc_util.c:223 malloc(16384) failed @ nc_mbuf.c:46
[Fri Jan 25 09:38:28 2013] nc_util.c:223 malloc(16384) failed @ nc_mbuf.c:46
[Fri Jan 25 09:38:28 2013] nc_connection.c:384 sendv on sd 8 failed: Connection reset by peer
[Fri Jan 25 09:38:28 2013] nc_connection.c:384 sendv on sd 9 failed: Broken pipe
[Fri Jan 25 09:38:28 2013] nc_connection.c:384 sendv on sd 10 failed: Broken pipe
[Fri Jan 25 09:38:28 2013] nc_connection.c:384 sendv on sd 11 failed: Broken pipe
[Fri Jan 25 09:38:28 2013] nc_connection.c:384 sendv on sd 12 failed: Broken pipe
[Fri Jan 25 09:38:28 2013] nc_connection.c:384 sendv on sd 13 failed: Broken pipe
[Fri Jan 25 09:38:28 2013] nc_connection.c:384 sendv on sd 14 failed: Broken pipe
[Fri Jan 25 09:38:28 2013] nc_connection.c:384 sendv on sd 15 failed: Broken pipe

Redis: SET vs MSET; Sharding: twemproxy vs RedisSharding

It's logical that one MSET command with multiple keys is faster than multiple SET commands. The question is: how much faster when using real clients? What is more rational for the data sharding? twemproxy or RedisSharding? The first one is very fast, the second one supports MSET, MGET commands properly.

Let's take these clients: http://search.cpan.org/~melo/Redis-1.955 (Simple client) and http://search.cpan.org/~dgl/AnyEvent-Redis-0.24 (Event client).

"No Mutly Keys" test was run SET command for 10 keys, then GET for this keys, and then DEL.
"Yes Mutly Keys" test was run MSET with 10 keys, then MGET with this keys, and then DEL.
This series of commands was run 1000 time for each test.

Version:
Redis         - 2.4.15 (http://redis.io)
nutcracker    - 0.2.2 (http://github.com/twitter/twemproxy)
RedisSharding - 1.0 (http://github.com/kni/redis-sharding-hs-strict)
twemproxy and RedisSharding used 4 Redis nodes.
Clients, Redis nodes, twemproxy, RedisSharding were run on one computer.
The comparison includes the data for the isolated Redis server.

Average time of executed test scripts:
Client | Mutly Keys | Redis | twemproxy   | RedisSharding
---------------------------------------------------------
Simple |   No       | 02.72 | 03.93       | 05.69
Event  |   No       | 03.65 | 03.54       | 03.54
Simple |   Yes      | 00.86 | no support* | 01.34
Event  |   Yes      | 00.61 | no support* | 00.75
* - twemproxy supports MSET basing on the hash tag only, while we are interested in proper full support! https://github.com/twitter/twemproxy/issues/51#issuecomment-12257220

And now, let's take the faster client (http://hackage.haskell.org/package/hedis):

Keys | Mutly Keys | Redis | twemproxy   | RedisSharding
-------------------------------------------------------
 10  |   No       | 00.08 | 00.13       | 00.26
 10  |   Yes      | 00.02 | no support* | 00.14
100  |   No       | 00.78 | 01.34       | 02.56
100  |   Yes      | 00.30 | no support* | 00.90 !!!
Conclusions:
If you don't have to use Mutly Keys commands, you can choose twemproxy for sharding.
However, if you have an opportunity to use MSET and MGET, it's worth trying RedisSharding.

вторник, 8 января 2013 г.

Redis Sharding: Lazy vs Strict

Так как у Redis команда KEYS достаточна требовательна к CPU, а сам Redis - однопоточен, где-то два года назад для одного проекта меня попросили распараллелить запросы к Redis на несколько экземпляров (node), по одному на каждое ядро CPU.

Так появился Redis Sharding (https://github.com/kni/redis-sharding). Он был написан на Perl. Но так как Perl не отдает память назад OS, при обработке команд MSET с большим количеством длинных ключей и значений процесс Redis Sharding разрастался в размерах и оставался таким.

В связи с этим появилась Haskell версия Redis Sharding (https://github.com/kni/redis-sharding-hs). Она не только красиво работала с памятью, но была быстрей в 2-4 раза.

Haskell версия была основана на Data.ByteString.Lazy. Ленивы строки очень удобны в работе. Ведь у нас есть как-бы все данные сразу, а фактически они подчитываются из сокета по мере необходимости. Однако что-бы отправкой данных из ленивой строки в сокет, создается массив из обычных ByteString, для которых и вызывается системный вызов writev. Именно эти дополнительные выделения памяти и копирование данных из Lazy ByteString в Strict ByteString существенно снижали производительность Redis Sharding.

И так, спустя два года, я решил попытаться ускорить Redis Sharding, используя Strict версию ByteString и Data.Attoparsec. Так под новогодние праздники появилась Strict версия Redis Sharding (https://github.com/kni/redis-sharding-hs-strict).

Однако существенно увеличения производительности добиться не удалось.
Strict версия на маленьких данных быстрей до 20% и в несколько раз меньше занимает памяти, но с ростом размера данных, это преимущество все уменьшается, а потребление памяти возрастает и перегоняет Lazy версию.

По видимому, это происходит из-за того, что при парсинге иногда происходит объединение нераспарсенного хвоста со следующим входным буферов.

Если сделать размер входного буфере не 4k, а 32k (это размер defaultChunkSize для Data.ByteString.Lazy), то скорости одинаковые, но Strict версия все еще использует меньше памяти (на 30%), но с ростом размера данных lazy версия начинает потреблять меньше памяти.


Вывод.
Когда размеры порций данных меньше, чем размер оптимального буфера, то предпочтительней Strict версия. Иначе - Lazy.