четверг, 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.