codewars刷题常用代码片

数学

快速算一个int有几位数

int sizeofint(int x) {
  const static int table[] = { 9, 99, 999, 9999, 99999, 999999, 9999999,    
            99999999, 999999999, std::numeric_limits<int>::max()};
  for (int i = 0; i<10; i++)    
    if (x <= table[i])    
      return i + 1;  
}

字符串操作

拆分

std::vector<std::string> split(const std::string& s, char delimiter)
{
   std::vector<std::string> tokens;
   std::string token;
   std::istringstream tokenStream(s);
   while (std::getline(tokenStream, token, delimiter))
   {
      tokens.push_back(token);
   }
   return tokens;
}

判断标点符号 std::ispunct,一可以自己写个数组暴力查表

Range for循环判定结尾, 这种写法注意可能vector有重复导致判断错误

for (auto v : vec) {
		if (v != vec.back()) {//...}
}

参考

  • geekforgeeks 问题列表 https://g4g.apachecn.org/#/docs/a-boolean-matrix-question

Read More

链表以及TAILQ

#链表以及TAILQ

最近重新看c语言的东西,对数据结构还是不熟悉

从链表说起,一个基本的链表,就是由一个个node组成

typedef struct node{
	node *next;
	node *prev;
	void* data;
}node;

而操作链表表头,引用整个链表,就有很多方法

比如,表头就是node,

typedef struct node linklist;

相关的操作函数比如构造函数就都以node做入参

linklist* createList();
void listAdd(linklist* l, node* v);

虽然写法不同但实际上是同一个类型,这种链表的缺点在于,比如合并两个链表,或者对整个链表排序都是leetcode上的题目

对于头结点,同时也是提领引用整个链表的入口,操作会很麻烦,这时候,就需要一个dummyhead,作为head的前节点。那么如何避免这个问题呢?

对linklist结构进行改造,通常就是封一层,比如

typedef struct linklist {
    node *head;
    ...其他函数指针或者记录信息的字段
}linklist;

这基本上就是c++list封装的办法,也是c中常见的封装方法。就是内存分配需要两次比较麻烦,c++为了避免手动malloc,加上了构造析构的语义。(我好像在Bjarne Stroustrup 那本自传书籍里看到过)

然后说到tailq

typedef struct linklist {
    node *head;
    node *tail;
} linklist;

就是这样了。增加一个记录结尾的字段。这个结构在libevent redis中都有(redis基本上把libevent组件抄了一遍,抽出来组装的),最早追述应该是内核中的TAILQ吧。整体就比原来的双端列表多了一个指针的开销,说是队列,实际上双端队列本身也可以实现队列,就是访问最后一个节点需要间接的操作一下,而这可能缓存不友好?不然为啥会有这么个数据结构。

数据结构还是不直观,上个图来说明一下,图用的libevent

img

Read More


pika 简单分析

[toc]

why

梳理思路,省着忘了

0. trivial

  • 全局PikaServer对象,所有变量都用读写锁保护了。真的有必要吗。
  • 模块拆分 (pika主程序,pink网络模块,blackwidow编码模块,slash公共组件(这个公共组件模块很坑爹),glog),底层模块又互相依赖。挺头疼的(需要画个图)
  • 利用多线程优势,类似memcache用libevent哪种用法。主事件循环处理io事件,写pipe通知子线程搞定

  • redis协议分析我以为得放在pika主程序中,结果没想到在pink里。糊一起了。之前还好奇,翻pika代码没发现redis代码,难道解析redis居然没用到redis源码自己搞的,结果在pink里。

1. 整体架构

一图胜千言 很多redis over rocksdb的实现都是在编码上有各种异同,比如ssdb ardb之类的,pika怎么做的?上图

img

复杂数据结构 set zset hash 是分成元数据和实体数据来做的。(大家都抄linux)

pika编译踩坑

  1. 运行测试,二进制文件运行几次测试用例就挂了,应该是不支持wait直接崩了

下面是log记录。我个人猜测是运行环境的问题,准备从头编译

bash pikatests.sh wait

ERROR:path : ./tests/tmp/redis.conf.29436.2
-----------Pika server 3.0.5 ----------
-----------Pika config list----------
1 thread-num 1
2 sync-thread-num 6
3 sync-buffer-size 10
4 log-path ./log/
5 loglevel info
6 db-path ./db/
7 write-buffer-size 268435456
8 timeout 60
9 requirepass
10 masterauth
11 userpass
12 userblacklist
13 dump-prefix
14 dump-path ./dump/
15 dump-expire 0
16 pidfile ./pika.pid
17 maxclients 20000
18 target-file-size-base 20971520
19 expire-logs-days 7
20 expire-logs-nums 10
21 root-connection-num 2
22 slowlog-write-errorlog no
23 slowlog-log-slower-than 10000
24 slowlog-max-len 128
25 slave-read-only yes
26 db-sync-path ./dbsync/
27 db-sync-speed -1
28 slave-priority 100
29 server-id 1
30 double-master-ip
31 double-master-port
32 double-master-server-id
33 write-binlog yes
34 binlog-file-size 104857600
35 identify-binlog-type new
36 max-cache-statistic-keys 0
37 small-compaction-threshold 5000
38 compression snappy
39 max-background-flushes 1
40 max-background-compactions 2
41 max-cache-files 5000
42 max-bytes-for-level-multiplier 10
-----------Pika config end----------
WARNING: Logging before InitGoogleLogging() is written to STDERR
W1217 17:22:48.644249 29438 pika.cc:167] your 'limit -n ' of 1024 is not enough for Redis to start. pika have successfully reconfig it to 25000
I1217 17:22:48.644497 29438 pika.cc:184] Server at: ./tests/tmp/redis.conf.29436.2
I1217 17:22:48.644691 29438 pika_server.cc:192] Using Networker Interface: eth0
I1217 17:22:48.644783 29438 pika_server.cc:235] host: 192.168.1.104 port: 0
I1217 17:22:48.644820 29438 pika_server.cc:68] Prepare Blackwidow DB...
I1217 17:22:48.776921 29438 pika_server.cc:73] DB Success
I1217 17:22:48.776942 29438 pika_server.cc:89] Worker queue limit is 20100
I1217 17:22:48.777190 29438 pika_binlog.cc:103] Binlog: Manifest file not exist, we create a new one.
I1217 17:22:48.777328 29438 pika_server.cc:109] double recv info: filenum 0 offset 0
F1217 17:22:48.779913 29438 pika_server.cc:284] Start BinlogReceiver Error: 1: bind port 1000 conflict, Listen on this port to handle the data sent by the Binlog Sender
*** Check failure stack trace: ***
@ 0x83c5aa google::LogMessage::Fail()
@ 0x83e2ff google::LogMessage::SendToLog()
@ 0x83c1f8 google::LogMessage::Flush()
@ 0x83ec2e google::LogMessageFatal::~LogMessageFatal()
@ 0x5f5725 PikaServer::Start()
@ 0x423db4 main
@ 0x7fe733418bb5 __libc_start_main
@ 0x58ac71 (unknown)
[1/1 done]: unit/wait (5 seconds)

The End

Execution time of different units:
5 seconds - unit/wait

\2. 源码编译由于我的服务器限制不能访问外网,自己手动git clone然后导回服务器(这里又有一个坑)(win7没有wsl,只能用服务器搞)

然后需要依赖子模块

git submodule init
git submodule update

重新在windows上更新后传回

分别编译,

子模块编译失败,其中有文件格式不对的问题 手动转dos2unix

然后才发现tortoisegit有个自动添加换行符的选项,默认打勾。所以基本上涉及到的shell文件都得手动dos2unix

img

我的服务器只有4.8.3,所以编译rocksdb需要c++11 需要修改Makefile(其实是detect脚本没加权限的问题)

img

编译glog失败

./configure make失败 提示没有什么repo

搜到了类似的问题,https://stackoverflow.com/questions/18839857/deps-po-no-such-file-or-directory-compiler-error

按照提示 (需要安装libtool)

autoreconf -if

提示

.ibtoolize: AC_CONFIG_MACRO_DIR([m4]) conflicts with ACLOCAL_AMFLAGS=-I m4 

结果还是gitwindows加换行符导致的。 That darn “libtoolize: AC_CONFIG_MACRO_DIR([m4]) conflicts with ACLOCAL_AMFLAGS=-I m4” error Is caused by using CRLFs in Makefile.am. “m4" != "m4" and thus the libtoolize script will produce an error. If you're using git, I strongly advise adding a .gitattributes file with the following: *.sh -crlf *.ac -crlf *.am -crlf http://pete.akeo.ie/2010/12/that-darn-libtoolize-acconfigmacrodirm4.html

最终编译过了,运行提示找不到glog 还要手动ldconfig一下

echo "/usr/local/lib" >> /etc/ld.so.conf
ldconfig

还有一种解决方案,export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib

1552045590285

总算能运行测试了。

redis 中runtest脚本不能直接用,但是测试用例可以直接拿过来用,support/server.tcl文件改下start_server函数就行(pika已经改过了,直接用那个文件就好了)

测试,类似下面,改改应该就行了

#!/bin/bash
for f in ${PWD}/tests/unit/*;do
    filename=${f##*/}
    tclsh tests/test_helper.tcl --clients 1 --single unit/${filename%.*}
done

for f in ${PWD}/tests/unit/type/*;do
    filename=${f##*/}
    tclsh tests/test_helper.tcl --clients 5 --single  unit/type/${filename%.*}
    #tclsh tests/test_helper.tcl --clients 1 --single unit/type/${filename%.*}
done

附调用图一张。组件太多,确实找不到在哪。

遇到的问题

  • valgrind不能attach到daemon进程上

只能停掉daemon从命令行执行

  • vallgrind提示非法指令
vex amd64->IR: unhandled instruction bytes: 0x62 0xF1 0x7D 0x48 0xEF 0xC0 0x48 0x8D
vex amd64->IR:   REX=0 REX.W=0 REX.R=0 REX.X=0 REX.B=0
vex amd64->IR:   VEX=0 VEX.L=0 VEX.nVVVV=0x0 ESC=NONE
vex amd64->IR:   PFX.66=0 PFX.F2=0 PFX.F3=0
==17407== valgrind: Unrecognised instruction at address 0x815dc7.
==17407==    at 0x815DC7: rocksdb::BlockBasedTableFactory::BlockBasedTableFactory(rocksdb::BlockBasedTableOptions const&) (block_based_table_factory.cc:44)
==17407==    by 0x7FC64D: rocksdb::ColumnFamilyOptions::ColumnFamilyOptions() (options.cc:99)
==17407==    by 0x4762EF: __static_initialization_and_destruction_0(int, int) [clone .constprop.1298] (options_helper.h:387)
==17407==    by 0x8E011C: __libc_csu_init (in /home/vdb/pika/output/pika)
==17407==    by 0x65F2B44: (below main) (in /usr/lib64/libc-2.17.so)
==17407== Your program just tried to execute an instruction that Valgrind
==17407== did not recognise.  There are two possible reasons for this.
==17407== 1. Your program has a bug and erroneously jumped to a non-code
==17407==    location.  If you are running Memcheck and you just saw a
==17407==    warning about a bad jump, it's probably your program's fault.
==17407== 2. The instruction is legitimate but Valgrind doesn't handle it,
==17407==    i.e. it's Valgrind's fault.  If you think this is the case or
==17407==    you are not sure, please let us know and we'll try to fix it.
==17407== Either way, Valgrind will now raise a SIGILL signal which will
==17407== probably kill your program.
==17407==
==17407== Process terminating with default action of signal 4 (SIGILL)
==17407==  Illegal opcode at address 0x815DC7
==17407==    at 0x815DC7: rocksdb::BlockBasedTableFactory::BlockBasedTableFactory(rocksdb::BlockBasedTableOptions const&) (block_based_table_factory.cc:44)
==17407==    by 0x7FC64D: rocksdb::ColumnFamilyOptions::ColumnFamilyOptions() (options.cc:99)
==17407==    by 0x4762EF: __static_initialization_and_destruction_0(int, int) [clone .constprop.1298] (options_helper.h:387)
==17407==    by 0x8E011C: __libc_csu_init (in /home/vdb/pika/output/pika)
==17407==    by 0x65F2B44: (below main) (in /usr/lib64/libc-2.17.so)
==17407==
==17407== Events    : Ir
==17407== Collected : 11565385

rocksdb 检测脚本 加上 PORTABLE=1 主要还是-march=native这条导致的

  • valgrind执行权限不足?子进程pika 无法修改sockte个数

14:17:17.436130 16158 pika.cc:173] your ‘limit -n ‘ of 1024 is not enough for Redis to start. pika can not reconfig it(Operation not permitted), do it by yourself

ulimit -n 102400

  • 无法启动,原因是目录下有个叫log的文件
I0527 14:47:45.087769 18132 pika_binlog.cc:87] Binlog: Manifest file not x
exist, we create a new one.                                              x
Could not create logging file: Not a directory                           x
COULD NOT CREATE A LOGGINGFILE!F0527 14:47:45.087787 18132 pika_binlog.ccx
:92] Binlog: new ./log/log_db0/write2file IO error: ./log/log_db0/write2fx
ile0: Not a directory                                                    x
*** Check failure stack trace: ***                                       x
    @     0x7fc9dd5d1b5d  google::LogMessage::Fail()                     x
    @     0x7fc9dd5d37dd  google::LogMessage::SendToLog()                x
    @     0x7fc9dd5d1773  google::LogMessage::Flush()                    x
    @     0x7fc9dd5d41fe  google::LogMessageFatal::~LogMessageFatal()    x
    @           0x6032e6  Binlog::Binlog()                               x
    @           0x615519  Partition::Partition()                         x
    @           0x5f5f83  Table::Table()                                 x
    @           0x621525  PikaServer::InitTableStruct()                  x
    @           0x625d0c  PikaServer::Start()                            x
    @           0x429a54  main                                           x
    @     0x7fc9dbaa9bb5  __libc_start_main                              x
    @           0x54ea41  (unknown)                                      x
Aborted 

测试select命令路径的脚本 需要安装redis-tools

#!/bin/bash
for i in {0..10000}
do
    echo select $(expr $i % 8 ) |redis-cli -p 9221
done

spop命令不支持count参数,需要拓展

zadd命令不支持nx xx ch incr 参数,比较复杂

pika跑单测

  1 #!/bin/bash
  2 ./runtest --list-tests |while read line
  3 do
  4 ./runtest --clients 1 --single $line
  5 done

  • 无法生成core文件
ulimit -c unlimited

core 目录

cat /proc/sys/kernel/core_pattern

也可以改core格式, 见参考链接

echo "core-%e-%p-%t" > /proc/sys/kernel/core_pattern

这样默认生成在程序所在目录

  • 原生redis test适配,tcl脚本修改

    • 需要把pika改成redis-server pika需要指定配置文件 -c,tests/support/server.tcl

      set pid [exec src/redis-server -c $config_file > $stdout 2> $stderr &]
      
    • 配置文件和redis不一样,a : b格式的,redis是a b格式,脚本用dict保存,前面第一个字段和剩下的组成dict,为了生成独一无二的端口,需要重新设置,这可能就丢掉了:,需要手动加上

      • port前加上list
       set config {}
       foreach line $data {
            if {[string length $line] > 0 && [string index $line 0] ne "#"} {
                set elements [split $line " "]
                set directive [lrange $elements 0 0]
                set arguments [lrange $elements 1 end]
                dict set config $directive $arguments
             }
        }
        # use a different directory every time a server is started
        dict set config dir [tmpdir server]
        dict set config db-path [list ":" [tmpdir server]]
          
        # start every server on a different port
        set ::port [find_available_port [expr {$::port+1}]]
        dict set config port [list ":" $::port]
        #    dict set config port $::port
      
    • 文件目录,pika是db-path, redis是dir,需要加上个db-path定义,同上,需要加上:

    • 大量测试不支持,需要注释掉,tcl注释见参考链接

  • python driver修改
    • conftest redis_version 改成pika_version #添加redis_version
    • conftest 注意db数,改成7 0~7 原生是0~9 #添加数据库
  • redigo 测试修改 redisx/db_test.go : DialTest函数 不然会报 out of range

    _, err = c.Do("SELECT", "7")
    //_, err = c.Do("SELECT", "9")
    
  • c driver 测试

    • 大量assert失败,简单hook

      /* The assert() calls below have side effects, so we need assert()
       * even if we are compiling without asserts (-DNDEBUG). */
      #ifdef NDEBUG
      #undef assert
      #define assert(e) (void)(e)
      #else
      #define assert(expr)     \
          if(expr) \
            do{ \
                fprintf(stderr, "Assertion failed:%s, file %s, line %d\n", #expr, __FILE__,__LINE__); \
               }while(0)
      #endif
      
    • 注意有redis_version 获取,要改成pika_version

    • unix sock,(/tmp/redis.sock)文件没有处理,相关测试屏蔽掉了,基本和上面没差别

redis release note

只列举功能相关部分,增强优化点不列出

redis版本 功能点
2.6 Lua脚本支持
2.6 新增PEXIRE、PTTL、PSETEX过期设置命令,key过期时间可以设置为毫秒级
2.6 新增位操作命令:BITCOUNT、BITOP
2.6 新增命令:dump、restore,即序列化与反序列化操作
2.6 新增命令:INCRBYFLOAT、HINCRBYFLOAT,用于对值进行浮点数的加减操作
2.6 新增命令:MIGRATE,用于将key原子性地从当前实例传送到目标实例的指定数据库上
2.6 SHUTDOWN命令添加SAVE和NOSAVE两个参数,分别用于指定SHUTDOWN时用不用执行写RDB的操作
2.6 sort命令会拒绝无法转换成数字的数据模型元素进行排序
2.6 不同客户端输出缓冲区分级,比如普通客户端、slave机器、pubsub客户端,可以分别控制对它们的输出缓冲区大小
2.8 引入PSYNC,主从可以增量同步,这样当主从链接短时间中断恢复后,无需做完整的RDB完全同步
2.8 新增命令:SCAN、SSCAN、HSCAN和ZSCAN
2.8 crash的时候自动内存检查
2.8 新增键空间通知功能,客户端可以通过订阅/发布机制,接收改动了redis指定数据集的事件
2.8 可通过CONFIGSET设置客户端最大连接数
2.8 新增CONFIGREWRITE命令,可以直接把CONFIGSET的配置修改到redis.conf里
2.8 新增pubsub命令,可查看pub/sub相关状态
2.8 支持引用字符串,如set ‘foo bar’ “hello world\n”
2.8 新增redis master-slave集群高可用解决方案(Redis-Sentinel)
2.8 当使用SLAVEOF命令时日志会记录下新的主机
3.0 实现了分布式的Redis即Redis Cluster,从而做到了对集群的支持
3.0 大幅优化LRU近似算法的性能
3.0 新增CLIENT PAUSE命令,可以在指定时间内停止处理客户端请求
3.0 新增WAIT命令,可以阻塞当前客户端,直到所有以前的写命令都成功传输并和指定的slaves确认
3.0 AOF重写过程中的”last write”操作降低了AOF child -> parent数据传输的延迟
3.0 实现了对MIGRATE连接缓存的支持,从而大幅提升key迁移的性能
3.0 为MIGRATE命令新增参数:copy和replace,copy不移除源实例上的key,replace替换目标实例上已存在的key
3.2 新增对GEO(地理位置)功能的支持
3.2 SPOP命令新增count参数,可控制随机删除元素的个数
3.2 新增HSTRLEN命令,返回hash数据类型的value长度
3.2 提供了一个基于流水线的MIGRATE命令,极大提升了命令执行速度
4.0 加入模块系统,用户可以自己编写代码来扩展和实现redis本身不具备的功能,它与redis内核完全分离,互不干扰
4.0 优化了PSYNC主从复制策略,使之效率更高
4.0 为DEL、FLUSHDB、FLUSHALL命令提供非阻塞选项,可以将这些删除操作放在单独线程中执行,从而尽可能地避免服务器阻塞
4.0 新增SWAPDB命令,可以将同一redis实例指定的两个数据库互换
4.0 新增RDB-AOF持久化格式,开启后,AOF重写产生的文件将同时包含RDB格式的内容和AOF格式的内容,其中 RDB格式的内容用于记录已有的数据,而AOF格式的内存则用于记录最近发生了变化的数据
4.0 新增MEMORY内存命令,可以用于查看某个key的内存使用、查看整体内存使用细节、申请释放内存、深入查看内存分配器内部状态等功能
5.0 新的流数据类型(Stream data type) https://redis.io/topics/streams-intro
5.0 新的 Redis 模块 API:定时器、集群和字典 API(Timers, Cluster and Dictionary APIs)
5.0 RDB 现在可存储 LFU 和 LRU 信息
5.0 新的有序集合(sorted set)命令:ZPOPMIN/MAX 和阻塞变体(blocking variants)
5.0 更好的内存统计报告
5.0 许多包含子命令的命令现在都有一个 HELP 子命令
5.0 引入 CLIENT UNBLOCK 和 CLIENT ID

兼容性对比

命令 版本 复杂度 pika
string      
SET 1.0 O1 支持
SETNX 1.0 O(1) 支持
SETEX 2.0 O(1) 支持
PSETEX 2.6 O(1) 支持
GET 1.0 O(1) 支持
GETSET 1.0 O(1) 支持
STRLEN 2.2 O(1) 支持
APPEND 2.0 平摊O(1) 支持
SETRANGE 2.2 如果本身字符串短。平摊O(1),否则为O(len(value)) 支持
GETRANGE 2.4 O(N)N为返回字符串的长度 支持
INCR 1.0 O(1) 支持
INCRBY 1.0 O(1) 支持
INCRBYFLOAT 2.6 O(1) 支持
DECR 1.0 O(1) 支持
DECRBY 1.0 O(1) 支持
MSET 1.0.1 O(N) N为键个数 支持
MSETNX 1.0.1 O(N) N为键个数 支持
MGET 1.0.0 O(N) N为键个数 支持
SETBIT 2.2 O(1) 部分支持
GETBIT 2.2 O(1) 部分支持
BITCOUNT 2.6 O(N) 部分支持
BITPOS 2.8.7 O(N) 支持
BITOP 2.6 O(N) 部分支持
BITFIELD 3.2 O(1) 不支持
hash      
HSET 2.0 O(1) 支持
HSETNX 2.0 O(1) 支持
HGET 2.0 O(1) 支持
HEXISTS 2.0 O(1) 支持
HDEL 2.0 O(N) N为删除的字段个数 支持
HLEN 2.0 O(1) 支持
HSTRLEN 3.2 O(1) 支持
HINCRBY 2.0 O(1) 支持
HINCRBYFLOAT 2.6 O(1)支持 支持
HMSET 2.0 O(N) N为filed-value数量 支持
HMGET 2.0 O(N) 支持
HKEYS 2.0 O(N)N为哈希表大小? 支持
HVALS 2.0 O(N) 支持
HGETALL 2.0 O(N) 支持
HSCAN 2.0 O(N)? 支持
list      
LPUSH 1.0 O(1) 支持
LPUSHX 2.2 O(1) 支持
RPUSH 1.0 O(1) 支持
RPUSHX 2.2 O(1) 支持
LPOP 1.0 O(1) 支持
RPOP 1.0 O(1) 支持
RPOPLPUSH 1.2 O(1) 支持
LREM 1.0 O(N) 支持
LLEN 1.0 O(1) 支持
LINDEX 1.0 O(N) N为遍历到index经过的数量 支持
LINSERT 2.2 O(N) N 为寻找目标值经过的值 支持
LSET 1.0 O(N) N为遍历到index处的元素个数 支持
LRANGE 1.0 O(S+N) S为start偏移量,N为区间stop-start 支持
LTRIM 1.0 O(N) N为被移除元素的数量 支持
BLPOP 2.0 O(1) 不支持
BRPOP 2.0 O(1) 不支持
BRPOPLPUSH 2.2 O(1) 不支持
set      
SADD 1.0 O(N) N是元素个数 支持
SISMEMBER 1.0 O(1) 支持
SPOP 1.0 O(1)支持 支持
SRANDMEMBER 1.0 O(N) N为返回值的个数 支持 行为可能不一致 复杂度ON
SREM 1.0 O(N) N为移除的元素个数支持 支持
SMOVE 1.0 O(1) 支持
SCARD 1.0 O(1) 支持
SMEMBERS 1.0 O(N) N为集合基数 支持
SSCAN     支持
SINTER 1.0 O(NxM) N是集合最小基数,M是集合个数 支持
SINTERSTORE 1.0 O(N*M) 支持
SUION 1.0 O(N) N是所有给定元素之和 支持
SUIONSTORE 1.0 O(N) 支持
SDIFF 1.0 O(N) 支持
SDIFFSTORE 1.0 O(N) 支持
Sorted Set      
ZADD 1.2 O(M*logN) N 是基数M是添加新成员个数 支持
ZSCORE 1.2 O(1) 支持
ZINCRBY 1.2 O(logN) 支持
ZCARD 1.2 O(1) 支持
ZCOUNT 2.0 O(logN) 支持
ZRANGE 1.2 O(logN+M) M结果集基数 N有序集基数 支持
ZREVRANGE 1.2 O(logN+M) M结果集基数 N有序集基数 支持
ZRANGEBYSCORE 1.05 O(logN+M) M结果集基数 N有序集基数 支持
ZREVRANGEBYSCORE 1.05 O(logN+M) M结果集基数 N有序集基数 支持
ZRANK 2.0 O(logN) 支持
ZREVRANK 2.0 O(logN) 支持
ZREM 1.2 O(logN*M) N基数M被移除的个数 支持
ZREMRANGEBYRANK 2.0 O(logN+M) N基数 M被移除数量 支持
ZREMRANGEBYSCORE 1.2 O(logN+M) N基数 M被移除数量 支持
ZRANGEBYLEX 2.8.9 O(logN+M) N基数 M返回元素数量 支持
ZLEXCOUNT 2.8.9 O(logN) N为元素个数 支持
ZREMRANGEBYLEX 2.8.9 O(logN+M) N基数 M被移除数量 支持
ZSCAN     支持
ZUNIONSTORE 2.0 时间复杂度: O(N)+O(M log(M)), N 为给定有序集基数的总和, M 为结果集的基数。 支持
ZINTERSTORE 2.0 O(NK)+O(Mlog(M)), N 为给定 key 中基数最小的有序集, K 为给定有序集的数量, M 为结果集的基数。 支持
ZPOPMAX 5.0 O(log(N)*M) M最大值个数 不支持
ZPOPMIN 5.0 O(log(N)*M) 不支持
BZPOPMAX 5.0 O(log(N)) 不支持
BZPOPMIN 5.0 O(log(N)) 不支持
HyperLogLog      
PFADD 2.8.9 O(1) 支持
PFCOUNT 2.8.9 O(1),多个keyO(N) 支持
PFMERGE 2.8.9 O(N) 支持
GEO      
GEOADD 3.2 O(logN) 支持
GEOPOS 3.2 O(logN) 支持
GEODIST 3.2 O(logN) 支持
GEORADIUS 3.2 O(N+logM)N元素个数M被返回的个数 支持
GEORADIUSBYMEMBER 3.2 O(N+logM) 支持
GEOHASH 3.2 O(logN) 支持
Stream      
XADD 5.0 O(1) 不支持
XACK 5.0 O(1) 不支持
XCLAIM 5.0 O(log N) 不支持
XDEL 5.0 O(1) 不支持
XGROUP 5.0 O(1) 不支持
XINFO 5.0 O(N) 不支持
XLEN 5.0 O(1) 不支持
XPENDING 5.0 O(N) 可以退化为O(1) 不支持
XRANGE 5.0 O(N) 不支持
XREAD 5.0 O(N) 可以退化为O(1) 不支持
XREADGROUP 5.0 O(M) 可以退化为O(1) 不支持
XREVRANGE 5.0 O(N) 可以退化为O(1) 不支持
XTRIM 5.0 O(N) 不支持
Keys      
EXISTS 1.0 O(1) 支持
TYPE 1.0 O(1) 支持 行为不一致但是pika允许重名,有类型输出顺序
RENAME 1.0 O(1) 不支持
RENAMENX 1.0 O(1) 不支持
MOVE 1.0 O(1) 不支持
DEL 1.0 O(N) N为key个数 支持
RANDOMKEY 1.0 O(1) 不支持
DBSIZE 1.0 O(1) 支持 行为不一致
EXPIRE 1.0 O(1) 支持
EXPIREAT 1.2 O(1) 支持
TTL 1.0 O(1) 支持
PERSIST 2.2 O(1) 支持
PEXPIRE 2.6 O(1) 支持 行为不一致,单位秒
PEXPIREAT 2.6 O(1) 支持 行为不一致,单位秒
PTTL 2.6 O(1) 支持
MULTI 1.2 O(1) 不支持
EXEC 1.2 事务块内执行的命令复杂度和 不支持
DISCARD 2.2 O(1) 不支持
WATCH 2.2 O(1) 不支持
UNWATCH 2.2 O(1) 不支持
EVAL 2.6 O(1) 找到脚本。其余复杂度取决于脚本本身 不支持
EVALSHA 2.6 根据脚本的复杂度而定 不支持
SCRIPT LOAD 2.6 O(N) N为脚本长度 不支持
SCRIPT EXISTS 2.6 O(N) N为判断的sha个数 不支持
SCRIPT FLUSH 2.6 O(N) N为缓存中脚本个数 不支持
SCRIPT KILL 2.6 O(1) 不支持
SAVE 1.0 O(N) N为key个数 不支持
BGSAVE 1.0 O(N) 支持 行为不一致
BGREWRITEAOF 1.0 O(N) N为追加到AOF文件中的数据数量 不支持
LASTSAVE 1.0 O(1) 不支持
Pub/Sub      
PUBLISH 2.0 O(M+N) channel订阅者数量+模式订阅客户端数量 支持
SUBSCRIBE 2.0 O(N) N是channel个数 支持
PSUBSCRIBE 2.0 O(N) N是模式的个数 支持
UNSUBSCRIBE 2.0 O(N) N是channel个数 支持
PUNSUBSCRIBE 2.0 O(N) N是channel个数 支持
PUBSUB CHANNELS
PUBSUB NUMSUB
PUBSUB NUMPAT
2.8 O(N) N是频道个数 支持
管理命令      
SLAVEOF 1.0 O(1) 支持
ROLE 2.8.12 O(1) 不支持
AUTH 1.0 O(1) 支持
QUIT 1.0 O(1) 支持
INFO 1.0 O(1) 支持 行为不一致
SHUTDOWN 1.0 O(1) 支持
TIME 2.6 O(1) 支持
CLIENT GETNAME CLIENT KILL CLIENT LIST SETNAME PAUSE REPLY ID 2.6.9
2.4
O(1)
O(N
O(N))
O(1)
部分支持
CONFIG SET CONFIG GET CONFIG RESETSTAT CONFIG REWRITE 2.0…2.8 O(1)O(N)O(1)O(N) 部分支持
PING 1.0 O(1) 支持
ECHO 1.0 O(1) 支持
OBJECT 2.2.3 O(1) 不支持
SLOWLOG 2.2.12 O(1) 支持
MONITOR 1.0 O(N) 支持
DEBUG OBJECT
DEBUG SEGFAULT
1.0 O(1) 不支持
MIGRATE 2.6 O(N) 不支持
DUMP 2.6 O(1)查找O(N*size)序列化 不支持
RESTORE 2.6 O(1)查找O(N*size)反序列化,有序集合还要再乘logN,插入排序的代价 不支持
SYNC 1.0 O(N) 支持,行为不一致
PSYNC 2.8 NA 支持 行为不一致
SORT     不支持
SELECT 1.0   支持 实现了一部分
  • bit操作部分支持主要是精度问题

  • stream 不支持,未实现,5.0之后的命令都没有实现

  • lua脚本相关命令都没有实现 这个依赖lua虚拟机。改动较大

  • module相关命令都没有实现 这个涉及api移植,改动较大

  • bit操作部分支持主要是精度问题

  • 集群相关命令都没有实现

  • 阻塞命令都没有实现

  • rename没有实现 这个涉及到pika的元数据。由于跨db,改名也稍稍麻烦

  • select数据库,pika只支持0-7 原生支持0-9 已经解决

  • client命令只支持list和kill

  • config命令 原生配置字段和pika配置字段不一致

  • info命令显式的字段和redis原生不一致

  • pexpire命令精度有限

  • spop不支持count指令

  • zadd不支持xxnxincr ch指令

  • ping不支持参数

  • 不支持quit

  • 不支持flushdb和randomkey

  • zset range 还有zincrby命令,double判断inf有问题,回复也不标准

  • lpushx rpushx不支持多参数

  • setrange 回复不标准

  • ping在pubsub中的回复不标准

  • expire ttl精度有问题,改时间戳解决

  • zadd 加入元素最后的会更新,pika没有这个逻辑,主要是实现insert_or_assign这种东西

  • smove行为也不太一样

  • incr incrbyfloat行为也不一致

  • zpopmax zpopmin命令没实现

ref

Read More



mongodb move chunk缓慢

有几个参考链接可以学习下

第三个链接,提供了一个check list

If it is slow, there are a number of possible reasons why that is the case, but the most common for a system that is not busy are:

  1. Source Disk I/O - if the data is not in active memory when it is read, it must be paged in from disk
  2. Network - if there is latency, rate limiting, packet loss etc. then the read may take quite a while
  3. Target Disk I/O - the data and indexes have to be written to disk, lots of indexes can make this worse, but usually this is not a problem on a lightly loaded system
  4. Problems with migrations causing aborts and failed migrations (issues with config servers, issues with deletes on primaries)
  5. Replication lag - for migrations to replica sets, write concern w:2 or w:majority is used by default and requires up to date secondaries to satisfy it.

前三项一般没啥问题,第四条,也不是放弃迁移,可能会导致反复迁移

考虑movechunk的流程,先打快照,把当前备份复制过去,然后再复制增量 如果这个增量没完没了的话,迁移时间必然会延长,有点像第五条的场景,第五条应该就是oplog赶不上了

再考虑oplog一直增长的情形 -> 插入。如果是顺序插入应该很快。如果是删除之类的操作可能会耗时过长,但是也不至于很慢, 再考虑什么会导致插入过慢 -> 索引过多,引用我老大的一句话,“索引过多导致插入变慢是数据库常识”,只要有索引,以前索引优化以及影响的那套理论还是成立的。

Read More

一次 Illegal instructions 问题(围观)定位

简单说,一个上层应用,下层是Rocksdb,编译机编好后放到测试机器上,崩溃, Got signal: 4 (Illegal instruction).,堆栈提示崩在shared_ptr上。

我简单搜了一下,发现可能有两个原因

  1. ud2a 指令导致的runtime abort。需要看堆栈反汇编。简单看代码不太像这个原因, 有个例子 warning: cannot pass objects of non-POD type ‘class A’ through ‘…’; call will abort at runtim

  2. 两个机器指令集有问题,同事和我说另一个测试机器内核版本号一毛一样,就没问题。不崩溃,就更让我确定是指令集的问题,简单搜了一圈,可以确定是-march=native 这个指令,现在就要确定这个指令在makefile中有没有了

期间我还查cpuinfo看指令集,看编译机和测试机有没有区别(没区别)

查指令集 grep -m1 ^flag /proc/cpuinfo | tr ' ' '\n' | sort

然后我查看了rocksdb的makefile cmakefile list 没有找到。(这是我的失误,我应该grep)

然后我又找上层应用 有没有这个编译指令,grep 没找到

最后同事发现在rocksdb的build_tools/build_detect_platform里。我才明白我实际上不懂rocksdb到底是怎么编译的。

makefile 会调用build_detect_platform 生成make_config.mk,包含了各种编译选项。

# detect what platform we're building on
dummy := $(shell (export ROCKSDB_ROOT="$(CURDIR)"; export PORTABLE="$(PORTABLE)"; "$(CURDIR)/build_tools/build_detect_platform" "$(CURDIR)/make_config.mk"))
# this file is generated by the previous line to set build flags and sources
include make_config.mk
CLEAN_FILES += make_config.mk

唉。

grep就会发现,且INSTALL中也有提示,要是想提高移植性,makefile 加上 PORTABLE=1

./INSTALL.md:(-march=native or the equivalent). If you want to build a portable binary, add 'PORTABLE=1' before 唉。

下面这个链接解释了为啥有人会用-march=native

https://stackoverflow.com/questions/52653025/why-is-march-native-used-so-rarelyWhy is -march=native used so rarely?Why is -march=native used so rarely?

下面这个链接解释了opencv类似的问题

https://answers.ros.org/question/11873/illegal-instruction-when-using-image_view-with-theora/

Read More

进程线程比较

进程 vs 线程

FIXME:需要重新验证

我们按照多个不同的维度,来看看多线程和多进程的对比(注:因为是感性的比较,因此都是相对的,不是说一个好得不得了,另外一个差的无法忍受)。

对比维度 多进程 多线程 总结
数据共享、同步 数据共享复杂,需要用IPC;数据是分开的,同步简单 因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 各有优势
内存、CPU 占用内存多,切换复杂,CPU利用率低 占用内存少,切换简单,CPU利用率高 线程占优
创建销毁、切换 创建销毁、切换复杂,速度慢 创建销毁、切换简单,速度很快 线程占优
编程、调试 编程简单,调试简单 编程复杂,调试复杂 进程占优
可靠性 进程间不会互相影响 一个线程挂掉将导致整个进程挂掉 进程占优
分布式 适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单 适应于多核分布式 进程占优

看起来比较简单,优势对比上是“线程 3.5 v 2.5 进程”,我们只管选线程就是了?

呵呵,有这么简单我就不用在这里浪费口舌了,还是那句话,没有绝对的好与坏,只有哪个更加合适的问题。我们来看实际应用中究竟如何判断更加合适。

1)需要频繁创建销毁的优先用线程

原因请看上面的对比。

这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的

2)需要进行大量计算的优先使用线程

所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。

这种原则最常见的是图像处理、算法处理。

3)强相关的处理用线程,弱相关的处理用进程

什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。

一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。

当然这种划分方式不是一成不变的,也可以根据实际情况进行调整。

4)可能要扩展到多机分布的用进程,多核分布的用线程

原因请看上面对比。

5)都满足需求的情况下,用你最熟悉、最拿手的方式

至于“数据共享、同步”、“编程、调试”、“可靠性”这几个维度的所谓的“复杂、简单”应该怎么取舍,我只能说:没有明确的选择方法。但我可以告诉你一个选择原则:如果多进程和多线程都能够满足要求,那么选择你最熟悉、最拿手的那个。

需要提醒的是:虽然我给了这么多的选择原则,但实际应用中基本上都是“进程+线程”的结合方式,千万不要真的陷入一种非此即彼的误区。

1、进程与线程

进程是程序执行时的一个实例,即它是程序已经执行到课中程度的数据结构的汇集。从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。

线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成(拥有很多相对独立的执行流的用户程序共享应用程序的大部分数据结构),线程与同属一个进程的其他的线程共享进程所拥有的全部资源。

“进程——资源分配的最小单位,线程——程序执行的最小单位”

进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

总的来说就是:进程有独立的地址空间,线程没有单独的地址空间(同一进程内的线程共享进程的地址空间)。(下面的内容摘自Linux下的多线程编程

使用多线程的理由之一是和进程相比,它是一种非常”节俭”的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种”昂贵”的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。

使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。

除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:

  • 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
  • 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
  • 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

在Unix上编程采用多线程还是多进程的争执由来已久,这种争执最常见到在B/S通讯中服务端并发技术 的选型上,比如WEB服务器技术中,Apache是采用多进程的(perfork模式,每客户连接对应一个进程,每进程中只存在唯一一个执行线 程),Java的Web容器Tomcat、Websphere等都是多线程的(每客户连接对应一个线程,所有线程都在一个进程中)。

从Unix发展历史看,伴随着Unix的诞生多进程就出现了,而多线程很晚才被系统支持,例如Linux直到内核2.6,才支持符合Posix规范的NPTL线程库。进程和线程的特点,也就是各自的优缺点如下:

进程优点:编程、调试简单,可靠性较高。 进程缺点:创建、销毁、切换速度慢,内存、资源占用大。 线程优点:创建、销毁、切换速度快,内存、资源占用小。 线程缺点:编程、调试复杂,可靠性较差。

上面的对比可以归结为一句话:“线程快而进程可靠性高”。线程有个别名叫“轻量级进程”,在有的书籍资料上介绍线程可以十倍、百倍的效率快于进程; 而进程之间不共享数据,没有锁问题,结构简单,一个进程崩溃不像线程那样影响全局,因此比较可靠。我相信这个观点可以被大部分人所接受,因为和我们所接受的知识概念是相符的。

在写这篇文章前,我也属于这“大部分人”,这两年在用C语言编写的几个C/S通讯程序中,因时间紧总是采用多进程并发技术,而且是比较简单的现场为 每客户fork()一个进程,当时总是担心并发量增大时负荷能否承受,盘算着等时间充裕了将它改为多线程形式,或者改为预先创建进程的形式,直到最近在网 上看到了一篇论文《Linux系统下多线程与多进程性能分析》作者“周丽 焦程波 兰巨龙”,才认真思考这个问题,我自己也做了实验,结论和论文作者的相似,但对大部分人可以说是颠覆性的。

下面是得出结论的实验步骤和过程,结论究竟是怎样的? 感兴趣就一起看看吧。

实验代码使用周丽论文中的代码样例,我做了少量修改,值得注意的是这样的区别:

论文实验和我的实验时间不同,论文所处的年代linux内核是2.4,我的实验linux内核是2.6,2.6使用的线程库是NPTL,2.4使用的是老的Linux线程库(用进程模拟线程的那个LinuxThread)。

论文实验和我用的机器不同,论文描述了使用的环境:单cpu 机器基本配置为:celeron 2.0 GZ, 256M, Linux 9.2,内核 2.4.8。我的环境是:双核 Intel(R) Xeon(R) CPU 5130 @ 2.00GHz(做实验时,禁掉了一核),512MG内存,Red Hat Enterprise Linux ES release 4 (Nahant Update 4),内核2.6.9-42。

进程实验代码(fork.c):

1. \#include <stdlib.h>
2. \#include <stdio.h>
3. \#include <signal.h>
4. 
5. \#define P_NUMBER 255 //并发进程数量
6. \#define COUNT 5 //每次进程打印字符串数
7. \#define TEST_LOGFILE "logFile.log"
8. FILE *logFile=NULL;
9. 
10. char *s="hello linux\0";
11. 
12. int main()
13. {
14.     int i=0,j=0;
15.     logFile=fopen(TEST_LOGFILE,"a+");//打开日志文件
16.     for(i=0;i<P_NUMBER;i++)
17.     {
18.         if(fork()==0)//创建子进程,if(fork()==0){}这段代码是子进程运行区间
19.         {
20.             for(j=0;j<COUNT;j++)
21.             {
22.                 printf("[%d]%s\n",j,s);//向控制台输出
23.                 /*当你频繁读写文件的时候,Linux内核为了提高读写性能与速度,会将文件在内存中进行缓存,这部分内存就是Cache Memory(缓存内存)。可能导致测试结果不准,所以在此注释*/
24.                 //fprintf(logFile,"[%d]%s\n",j,s);//向日志文件输出,
25.             }
26.             exit(0);//子进程结束
27.         }
28.     }
29.     
30.     for(i=0;i<P_NUMBER;i++)//回收子进程
31.     {
32.         wait(0);
33.     }
34.     
35.     printf("Okay\n");
36.     return 0;
37. }

进程实验代码(thread.c):

1. \#include <pthread.h>
2. \#include <unistd.h>
3. \#include <stdlib.h>
4. \#include <stdio.h>
5. 
6. \#define P_NUMBER 255//并发线程数量
7. \#define COUNT 5 //每线程打印字符串数
8. \#define TEST_LOG "logFile.log"
9. FILE *logFile=NULL;
10. 
11. char *s="hello linux\0";
12. 
13. print_hello_linux()//线程执行的函数
14. {
15.     int i=0;
16.     for(i=0;i<COUNT;i++)
17.     {
18.         printf("[%d]%s\n",i,s);//想控制台输出
19.         /*当你频繁读写文件的时候,Linux内核为了提高读写性能与速度,会将文件在内存中进行缓存,这部分内存就是Cache Memory(缓存内存)。可能导致测试结果不准,所以在此注释*/
20.         //fprintf(logFile,"[%d]%s\n",i,s);//向日志文件输出
21.     }
22.     pthread_exit(0);//线程结束
23. }
24. 
25. int main()
26. {
27.     int i=0;
28.     pthread_t pid[P_NUMBER];//线程数组
29.     logFile=fopen(TEST_LOG,"a+");//打开日志文件
30.     
31.     for(i=0;i<P_NUMBER;i++)
32.         pthread_create(&pid[i],NULL,(void *)print_hello_linux,NULL);//创建线程
33.         
34.     for(i=0;i<P_NUMBER;i++)
35.         pthread_join(pid[i],NULL);//回收线程
36.         
37.     printf("Okay\n");
38.     return 0;
39. }

两段程序做的事情是一样的,都是创建“若干”个进程/线程,每个创建出的进程/线程打印“若干”条“hello linux”字符串到控制台和日志文件,两个“若干”由两个宏 P_NUMBER和COUNT分别定义,程序编译指令如下:

> gcc -o fork fork.c
> gcc -lpthread -o thread thread.c

实验通过time指令执行两个程序,抄录time输出的挂钟时间(real时间):

> time ./fork
>  time ./thread

每批次的实验通过改动宏 P_NUMBER和COUNT来调整进程/线程数量和打印次数,每批次测试五轮,得到的结果如下:

一、重复周丽论文实验步骤

(注:本文平均值算法采用的是去掉一个最大值去掉一个最小值,然后平均)

单核(双核机器禁掉一核),进程/线程数:255,打印次数5            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m0.070s 0m0.071s 0m0.071s 0m0.070s 0m0.070s 0m0.070s
多线程 0m0.049s 0m0.049s 0m0.049s 0m0.049s 0m0.049s 0m0.049s
单核(双核机器禁掉一核),进程/线程数:255,打印次数10            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m0.112s 0m0.101s 0m0.100s 0m0.085s 0m0.121s 0m0.104s
多线程 0m0.097s 0m0.089s 0m0.090s 0m0.104s 0m0.080s 0m0.092s
单核(双核机器禁掉一核),进程/线程数:255,打印次数50            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m0.459s 0m0.531s 0m0.499s 0m0.499s 0m0.524s 0m0.507s
多线程 0m0.387s 0m0.456s 0m0.435s 0m0.423s 0m0.408s 0m0.422s
单核(双核机器禁掉一核),进程/线程数:255,打印次数100            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m1.141s 0m0.992s 0m1.134s 0m1.027s 0m0.965s 0m1.051s
多线程 0m0.925s 0m0.899s 0m0.961s 0m0.934s 0m0.853s 0m0.919s
单核(双核机器禁掉一核),进程/线程数:255,打印次数500            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m5.221s 0m5.258s 0m5.706s 0m5.288s 0m5.455s 0m5.334s
多线程 0m4.689s 0m4.578s 0m4.670s 0m4.566s 0m4.722s 0m4.646s
单核(双核机器禁掉一核),进程/线程数:255,打印次数1000            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m12.680s 0m16.555s 0m11.158s 0m10.922s 0m11.206s 0m11.681s
多线程 0m12.993s 0m13.087s 0m13.082s 0m13.485s 0m13.053s 0m13.074s
单核(双核机器禁掉一核),进程/线程数:255,打印次数5000            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 1m27.348s 1m5.569s 0m57.275s 1m5.029s 1m15.174s 1m8.591s
多线程 1m25.813s 1m29.299s 1m23.842s 1m18.914s 1m34.872s 1m26.318s
单核(双核机器禁掉一核),进程/线程数:255,打印次数10000            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 2m8.336s 2m22.999s 2m11.046s 2m30.040s 2m5.752s 2m14.137s
多线程 2m46.666s 2m44.757s 2m34.528s 2m15.018s 2m41.436s 2m40.240s

本轮实验是为了和周丽论文作对比,因此将进程/线程数量限制在255个,论文也是测试了255个进程/线程分别进行5次,10 次,50 次,100 次,500 次……10000 次打印的用时,论文得出的结果是:任务量较大时,多进程比多线程效率高;而完成的任务量较小时,多线程比多进程要快,重复打印 600 次时,多进程与多线程所耗费的时间相同。

虽然我的实验直到1000打印次数时,多进程才开始领先,但考虑到使用的是NPTL线程库的缘故,从而可以证实了论文的观点。从我的实验数据看,多线程和多进程两组数据非常接近,考虑到数据的提取具有瞬间性,因此可以认为他们的速度是相同的。

是不是可以得出这样的结论:多线程创建、销毁速度快,而多线程切换速度快,这个结论我们会在第二个试验中继续试图验证

当前的网络环境中,我们更看中高并发、高负荷下的性能,纵观前面的实验步骤,最长的实验周期不过2分钟多一点,因此下面的实验将向两个方向延伸,第一,增加并发数量,第二,增加每进程/线程的工作强度。

二、增加并发数量的实验

下面的实验打印次数不变,而进程/线程数量逐渐增加。在实验过程中多线程程序在后四组(线程数350,500,800,1000)的测试中都出现了“段错误”,出现错误的原因和多线程预分配线程栈有关。

实验中的计算机CPU是32位,寻址最大范围是4GB(2的32次方),Linux是按照3GB/1GB的方式来分配内存,其中1GB属于所有进程共享的内核空间,3GB属于用户空间(进程虚拟内存空间)。Linux2.6的默认线程栈大小是8M(通过ulimit -a查看),对于多线程,在创建线程的时候系统会为每一个线程预分配线程栈地址空间,也就是8M的虚拟内存空间。线程数量太多时,线程栈累计的大小将超过进程虚拟内存空间大小(计算时需要排除程序文本、数据、共享库等占用的空间),这就是实验中出现的“段错误”的原因。

Linux2.6的默认线程栈大小可以通过 ulimit -s 命令查看或修改,我们可以计算出线程数的最大上线: (1024102410243) / (10241024*8) = 384,实际数字应该略小与384,因为还要计算程序文本、数据、共享库等占用的空间。在当今的稍显繁忙的WEB服务器上,突破384的并发访问并不是稀 罕的事情,要继续下面的实验需要将默认线程栈的大小减小,但这样做有一定的风险,比如线程中的函数分配了大量的自动变量或者函数涉及很深的栈帧(典型的是 递归调用),线程栈就可能不够用了。可以配合使用POSIX.1规定的两个线程属性guardsize和stackaddr来解决线程栈溢出问 题,guardsize控制着线程栈末尾之后的一篇内存区域,一旦线程栈在使用中溢出并到达了这片内存,程序可以捕获系统内核发出的告警信号,然后使用 malloc获取另外的内存,并通过stackaddr改变线程栈的位置,以获得额外的栈空间,这个动态扩展栈空间办法需要手工编程,而且非常麻烦。

有两种方法可以改变线程栈的大小,使用 ulimit -s 命令改变系统默认线程栈的大小,或者在代码中创建线程时通过pthread_attr_setstacksize函数改变栈尺寸,在实验中使用的是第一种,在程序运行前先执行ulimit指令将默认线程栈大小改为1M:

ulimit -s 1024 time ./thread

单核(双核机器禁掉一核),进程/线程数:100 ,打印次数1000            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m3.834s 0m3.759s 0m4.376s 0m3.936s 0m3.851s 0m3.874
多线程 0m3.646s 0m4.498s 0m4.219s 0m3.893s 0m3.943s 0m4.018
单核(双核机器禁掉一核),进程/线程数:255 ,打印次数1000            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m9.731s 0m9.833s 0m10.046s 0m9.830s 0m9.866s 0m9.843s
多线程 0m9.961s 0m9.699s 0m9.958s 0m10.111s 0m9.686s 0m9.873s
单核(双核机器禁掉一核),进程/线程数:350 ,打印次数1000            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m13.773s 0m13.500s 0m13.519s 0m13.474s 0m13.351s 0m13.498
多线程 0m12.754s 0m13.251s 0m12.813s 0m16.861s 0m12.764s 0m12.943
单核(双核机器禁掉一核),进程/线程数: 500 ,打印次数1000            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m23.762s 0m22.151s 0m23.926s 0m21.327s 0m21.429s 0m22.413
多线程 0m20.603s 0m20.291s 0m21.654s 0m20.684s 0m20.671s 0m20.653
单核(双核机器禁掉一核),进程/线程数:800 ,打印次数1000            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m33.616s 0m31.757s 0m31.759s 0m32.232s 0m32.498s 0m32.163
多线程 0m32.050s 0m32.787s 0m33.055s 0m32.902s 0m32.235s 0m32.641
单核(双核机器禁掉一核),进程/线程数: 1000 ,打印次数1000            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m40.301s 0m41.083s 0m41.634s 0m40.247s 0m40.717s 0m40.700
多线程 0m41.633s 0m41.118s 0m42.700s 0m42.134s 0m41.170s 0m41.646

【实验结论】

当线程/进程逐渐增多时,执行相同任务时,线程所花费时间相对于进程有下降的趋势(本人怀疑后两组数据受系统其他瓶颈的影响),这是不是进一步验证了

多线程创建、销毁速度快,而多进程切换速度快。

出现了线程栈的问题,让我特别关心Java线程是怎样处理的,因此用Java语言写了同样的实验程序,Java程序加载虚拟机环境比较耗时,所以没 有用time提取测试时间,而直接将测时写入代码。对Linux上的C编程不熟悉的Java程序员也可以用这个程序去对比理解上面的C语言试验程序。

  1. import java.io.File;
  2. ​ import java.io.FileNotFoundException;
  3. ​ import java.io.FileOutputStream;
  4. ​ import java.io.IOException;
  5. ​ public class MyThread extends Thread
  6. ​ {
  7. ​ static int P_NUMBER = 1000; /* 并发线程数量 */
  8. ​ static int COUNT = 1000; /* 每线程打印字符串次数 */
  9. ​ static String s = “hello linux\n”;
  10. ​ static FileOutputStream out = null; /* 文件输出流 */
  11. ​ @Override
  12. ​ public void run()
  13. ​ {
  14. ​ for (int i = 0; i < COUNT; i++)
  15. ​ {
  16. ​ System.out.printf(“[%d]%s”, i, s); /* 向控制台输出 */
  17. ​ StringBuilder sb = new StringBuilder(16);
  18. ​ sb.append(“[”).append(i).append(“]”).append(s);
  19. ​ try
  20. ​ {
  21. ​ out.write(sb.toString().getBytes());/* 向日志文件输出 */
  22. ​ }
  23. ​ catch (IOException e)
  24. ​ {
  25. ​ e.printStackTrace();
  26. ​ }
  27. ​ }
  28. ​ }
  29. ​ public static void main(String[] args) throws FileNotFoundException, InterruptedException
  30. ​ {
  31. ​ MyThread[] threads = new MyThread[P_NUMBER]; /* 线程数组 */
  32. ​ File file = new File(“Javalogfile.log”);
  33. ​ out = new FileOutputStream(file, true); /* 日志文件输出流 */
  34. ​ System.out.println(“开始运行”);
  35. ​ long start = System.currentTimeMillis();
  36. ​ for (int i = 0; i < P_NUMBER; i++) //创建线程
  37. ​ {
  38. ​ threads[i] = new MyThread();
  39. ​ threads[i].start();
  40. ​ }
  41. ​ for (int i = 0; i < P_NUMBER; i++) //回收线程
  42. ​ {
  43. ​ threads[i].join();
  44. ​ }
  45. ​ System.out.println(“用时:” + (System.currentTimeMillis() – start) + “ 毫秒”);
  46. ​ return;
  47. ​ }
  48. ​ }
进程/线程数:1000 ,打印次数1000(用得原作者的数据)            
  第1次 第2次 第3次 第4次 第5次 平均
多线程 65664 ms 66269 ms 65546ms 65931ms 66540ms 65990 ms

Java程序比C程序慢一些在情理之中,但Java程序并没有出现线程栈问题,5次测试都平稳完成,可以用下面的ps指令获得java进程中线程的数量:

diaoyf@ali:~$ ps -eLf | grep MyThread | wc -l 1010

用ps测试线程数在1010上维持了很长时间,多出的10个线程应该是jvm内部的管理线程,比如用于GC。我不知道Java创建线程时默认栈的大 小是多少,很多资料说法不统一,于是下载了Java的源码jdk-6u21-fcs-src-b07-jrl-17_jul_2010.jar(实验环境 安装的是 SUN jdk 1.6.0_20-b02),但没能从中找到需要的信息。对于jvm的运行,java提供了控制参数,因此再次测试时,通过下面的参数将Java线程栈大 小定义在8192k,和Linux的默认大小一致:

diaoyf@ali:~/tmp1$ java -Xss8192k MyThread

出乎意料的是并没有出现想象中的异常,但用ps侦测线程数最高到达337,我判断程序在创建线程时在栈到达可用内存的上线时就停止继续创建了,程序运行的时间远小于估计值也证明了这个判断。程序虽然没有抛出异常,但运行的并不正常,另一个问题是最后并没有打印出“用时 xxx毫秒”信息。

这次测试更加深了我的一个长期的猜测:Java的Web容器不稳定。因为我是多年编写B/S的Java程序员,WEB服务不稳定常常挂掉也是司空见惯的,除了自己或项目组成员水平不高,代码编写太烂的原因之外,我一直猜测还有更深层的原因,如果就是线程原因的话,这颠覆性可比本篇文章的多进程性能颠覆性要大得多,想想世界上有多少Tomcat、Jboss、Websphere、weblogic在跑着,嘿嘿。

这次测试还打破了以前的一个说法:单CPU上并发超过6、7百,线程或进程间的切换就会占用大量CPU时间,造成服务器效率会急剧下降。但从上面的实验来看,进程/线程数到1000时(这差不多是非常繁忙的WEB服务器了),仍具有很好的线性。

三、增加每进程/线程的工作强度的实验

这次将程序打印数据增大,原来打印字符串为:

  1. char *s = “hello linux\0”;

现在修改为每次打印256个字节数据:

  1. char *s = “1234567890abcdef\
  2. ​ 1234567890abcdef\
  3. ​ 1234567890abcdef\
  4. ​ 1234567890abcdef\
  5. ​ 1234567890abcdef\
  6. ​ 1234567890abcdef\
  7. ​ 1234567890abcdef\
  8. ​ 1234567890abcdef\
  9. ​ 1234567890abcdef\
  10. ​ 1234567890abcdef\
  11. ​ 1234567890abcdef\
  12. ​ 1234567890abcdef\
  13. ​ 1234567890abcdef\
  14. ​ 1234567890abcdef\
  15. ​ 1234567890abcdef\
  16. ​ 1234567890abcdef\0”;
单核(双核机器禁掉一核),进程/线程数:255 ,打印次数100            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m6.977s 0m7.358s 0m7.520s 0m7.282s 0m7.218s 0m7.286
多线程 0m7.035s 0m7.552s 0m7.087s 0m7.427s 0m7.257s 0m7.257
单核(双核机器禁掉一核),进程/线程数: 255,打印次数500            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m35.666s 0m36.009s 0m36.532s 0m35.578s 0m41.537s 0m36.069
多线程 0m37.290s 0m35.688s 0m36.377s 0m36.693s 0m36.784s 0m36.618
单核(双核机器禁掉一核),进程/线程数: 255,打印次数1000            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 1m8.864s 1m11.056s 1m10.273s 1m12.317s 1m20.193s 1m11.215
多线程 1m11.949s 1m13.088s 1m12.291s 1m9.677s 1m12.210s 1m12.150

【实验结论】

从上面的实验比对结果看,即使Linux2.6使用了新的NPTL线程库(据说比原线程库性能提高了很多,唉,又是据说!),多线程比较多进程在效率上没有任何的优势,在线程数增大时多线程程序还出现了运行错误,实验可以得出下面的结论:

在Linux2.6上,多线程并不比多进程速度快,考虑到线程栈的问题,多进程在并发上有优势。

四、多进程和多线程在创建和销毁上的效率比较

预先创建进程或线程可以节省进程或线程的创建、销毁时间,在实际的应用中很多程序使用了这样的策略,比如Apapche预先创建进程、Tomcat 预先创建线程,通常叫做进程池或线程池。在大部分人的概念中,进程或线程的创建、销毁是比较耗时的,在stevesn的著作《Unix网络编程》中有这样 的对比图(第一卷 第三版 30章 客户/服务器程序设计范式):

行号 服务器描述 进程控制CPU时间(秒,与基准之差)    
Solaris2.5.1 Digital Unix4.0b BSD/OS3.0    
0 迭代服务器(基准测试,无进程控制) 0.0 0.0 0.0
1 简单并发服务,为每个客户请求fork一个进程 504.2 168.9 29.6
2 预先派生子进程,每个子进程调用accept   6.2 1.8
3 预先派生子进程,用文件锁保护accept 25.2 10.0 2.7
4 预先派生子进程,用线程互斥锁保护accept 21.5    
5 预先派生子进程,由父进程向子进程传递套接字 36.7 10.9 6.1
6 并发服务,为每个客户请求创建一个线程 18.7 4.7  
7 预先创建线程,用互斥锁保护accept 8.6 3.5  
8 预先创建线程,由主线程调用accept 14.5 5.0  

stevens已驾鹤西去多年,但《Unix网络编程》一书仍具有巨大的影响力,上表中stevens比较了三种服务器上多进程和多线程的执行效 率,因为三种服务器所用计算机不同,表中数据只能纵向比较,而横向无可比性,stevens在书中提供了这些测试程序的源码(也可以在网上下载)。书中介 绍了测试环境,两台与服务器处于同一子网的客户机,每个客户并发5个进程(服务器同一时间最多10个连接),每个客户请求从服务器获取4000字节数据, 预先派生子进程或线程的数量是15个。

第0行是迭代模式的基准测试程序,服务器程序只有一个进程在运行(同一时间只能处理一个客户请求),因为没有进程或线程的调度切换,因此它的速度是 最快的,表中其他服务模式的运行数值是比迭代模式多出的差值。迭代模式很少用到,在现有的互联网服务中,DNS、NTP服务有它的影子。第1~5行是多进 程服务模式,期中第1行使用现场fork子进程,2~5行都是预先创建15个子进程模式,在多进程程序中套接字传递不太容易(相对于多线 程),stevens在这里提供了4个不同的处理accept的方法。6~8行是多线程服务模式,第6行是现场为客户请求创建子线程,7~8行是预先创建 15个线程。表中有的格子是空白的,是因为这个系统不支持此种模式,比如当年的BSD不支持线程,因此BSD上多线程的数据都是空白的。

从数据的比对看,现场为每客户fork一个进程的方式是最慢的,差不多有20倍的速度差异,Solaris上的现场fork和预先创建子进程的最大差别是504.2 :21.5,但我们不能理解为预先创建模式比现场fork快20倍,原因有两个:

\1. stevens的测试已是十几年前的了,现在的OS和CPU已起了翻天覆地的变化,表中的数值需要重新测试。

\2. stevens没有提供服务器程序整体的运行计时,我们无法理解504.2 :21.5的实际运行效率,有可能是1504.2 : 1021.5,也可能是100503.2 : 100021.5,20倍的差异可能很大,也可能可以忽略。

因此我写了下面的实验程序,来计算在Linux2.6上创建、销毁10万个进程/线程的绝对用时。

创建10万个进程(forkcreat.c):

  1. #include
  2. #include
  3. #include
  4. #include
  5. #include <sys/stat.h>
  6. #include
  7. #include <sys/types.h>
  8. #include <sys/wait.h>
  9. int count;//子进程创建成功数量
  10. int fcount;//子进程创建失败数量
  11. int scount;//子进程回收数量
  12. /信号处理函数–子进程关闭收集/
  13. void sig_chld(int signo)
  14. {
  15. ​ pid_t chldpid;//子进程id
  16. ​ int stat;//子进程的终止状态
  17. ​ //子进程回收,避免出现僵尸进程
  18. ​ while((chldpid=wait(&stat)>0))
  19. ​ {
  20. ​ scount++;
  21. ​ }
  22. }
  23. int main()
  24. {
  25. ​ //注册子进程回收信号处理函数
  26. ​ signal(SIGCHLD,sig_chld);
  27. ​ int i;
  28. ​ for(i=0;i<100000;i++)//fork()10万个子进程
  29. ​ {
  30. ​ pid_t pid=fork();
  31. ​ if(pid==-1)//子进程创建失败
  32. ​ {
  33. ​ fcount++;
  34. ​ }
  35. ​ else if(pid>0)//子进程创建成功
  36. ​ {
  37. ​ count++;
  38. ​ }
  39. ​ else if(pid==0)//子进程执行过程
  40. ​ {
  41. ​ exit(0);
  42. ​ }
  43. ​ }
  44. ​ printf(“count:%d fount:%d scount:%d\n”,count,fcount,scount);
  45. }

创建10万个线程(pthreadcreat.c):

  1. #include
  2. #include
  3. int count=0;//成功创建线程数量
  4. void thread(void)
  5. {
  6. ​ //啥也不做
  7. }
  8. int main(void)
  9. {
  10. ​ pthread_t id;//线程id
  11. ​ int i,ret;
  12. ​ for(i=0;i<100000;i++)//创建10万个线程
  13. ​ {
  14. ​ ret=pthread_create(&id,NULL,(void *)thread,NULL);
  15. ​ if(ret!=0)
  16. ​ {
  17. ​ printf(“Create pthread error!\n”);
  18. ​ return(1);
  19. ​ }
  20. ​ count++;
  21. ​ pthread_join(id,NULL);
  22. ​ }
  23. ​ printf(“count:%d\n”,count);
  24. }

创建10万个线程的Java程序:

  1. public class ThreadTest
  2. ​ {
  3. ​ public static void main(String[] ags) throws InterruptedException
  4. ​ {
  5. ​ System.out.println(“开始运行”);
  6. ​ long start = System.currentTimeMillis();
  7. ​ for(int i = 0; i < 100000; i++) //创建10万个线程
  8. ​ {
  9. ​ Thread athread = new Thread(); //创建线程对象
  10. ​ athread.start(); //启动线程
  11. ​ athread.join(); //等待该线程停止
  12. ​ }
  13. ​ System.out.println(“用时:” + (System.currentTimeMillis() – start) + “ 毫秒”);
  14. ​ }
  15. ​ }
单核(双核机器禁掉一核),创建销毁10万个进程/线程            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m8.774s 0m8.780s 0m8.475s 0m8.592s 0m8.687s 0m8.684
多线程 0m0.663s 0m0.660s 0m0.662s 0m0.660s 0m0.661s 0m0.661
创建销毁10万个线程(Java)
12286毫秒

从数据可以看出,多线程比多进程在效率上有10多倍的优势,但不能让我们在使用哪种并发模式上定性,这让我想起多年前政治课上的一个场景:在讲到优越性时,面对着几个对此发表质疑评论的调皮男生,我们的政治老师发表了高见,“不能只横向地和当今的发达国家比,你应该纵向地和过去中国几十年的发展历史 比”。政治老师的话套用在当前简直就是真理,我们看看,即使是在赛扬CPU上,创建、销毁进程/线程的速度都是空前的,可以说是有质的飞跃的,平均创建销毁一个进程的速度是0.18毫秒,对于当前服务器几百、几千的并发量,还有预先派生子进程/线程的必要吗?

预先派生子进程/线程比现场创建子进程/线程要复杂很多,不仅要对池中进程/线程数量进行动态管理,还要解决多进程/多线程对accept的“抢” 问题,在stevens的测试程序中,使用了“惊群”和“锁”技术。即使stevens的数据表格中,预先派生线程也不见得比现场创建线程快,在 《Unix网络编程》第三版中,新作者参照stevens的测试也提供了一组数据,在这组数据中,现场创建线程模式比预先派生线程模式已有了效率上的优势。因此我对这一节实验下的结论是:

预先派生进程/线程的模式(进程池、线程池)技术,不仅复杂,在效率上也无优势,在新的应用中可以放心大胆地为客户连接请求去现场创建进程和线程。

我想,这是fork迷们最愿意看到的结论了。

五、双核系统重复周丽论文实验步骤

双核,进程/线程数:255 ,打印次数10            
  第1次 第2次 第3次 第4次 第5次 平均(单核倍数)
多进程 0m0.061s 0m0.053s 0m0.068s 0m0.061s 0m0.059s 0m0.060(1.73)
多线程 0m0.054s 0m0.040s 0m0.053s 0m0.056s 0m0.042s 0m0.050(1.84)
双核,进程/线程数: 255,打印次数100            
  第1次 第2次 第3次 第4次 第5次 平均(单核倍数)
多进程 0m0.918s 0m1.198s 0m1.241s 0m1.017s 0m1.172s 0m1.129(0.93)
多线程 0m0.897s 0m1.166s 0m1.091s 0m1.360s 0m0.997s 0m1.085(0.85)
双核,进程/线程数: 255,打印次数1000            
  第1次 第2次 第3次 第4次 第5次 平均(单核倍数)
多进程 0m11.276s 0m11.269s 0m11.218s 0m10.919s 0m11.201s 0m11.229(1.04)
多线程 0m11.525s 0m11.984s 0m11.715s 0m11.433s 0m10.966s 0m11.558(1.13)
双核,进程/线程数:255 ,打印次数10000            
  第1次 第2次 第3次 第4次 第5次 平均(单核倍数)
多进程 1m54.328s 1m54.748s 1m54.807s 1m55.950s 1m57.655s 1m55.168(1.16)
多线程 2m3.021s 1m57.611s 1m59.139s 1m58.297s 1m57.258s 1m58.349(1.35)

【实验结论】

双核处理器在完成任务量较少时,没有系统其他瓶颈因素影响时基本上是单核的两倍,在任务量较多时,受系统其他瓶颈因素的影响,速度明显趋近于单核的速度。

六、并发服务的不可测性

看到这里,你会感觉到我有挺进程、贬线程的论调,实际上对于现实中的并发服务具有不可测性,前面的实验和结论只可做参考,而不可定性。对于不可测性,我举个生活中的例子。

这几年在大都市生活的朋友都感觉城市交通状况越来越差,到处堵车,从好的方面想这不正反应了我国GDP的高速发展。如果你7、8年前来到西安市,穿 过南二环上的一些十字路口时,会发现一个奇怪的U型弯的交通管制,为了更好的说明,我画了两张图来说明,第一张图是采用U型弯之前的,第二张是采用U型弯 之后的。

南二环交通图一

南二环交通图二

为了讲述的方便,我们不考虑十字路口左拐的情况,在图一中东西向和南北向的车辆交汇在十字路口,用红绿灯控制同一时间只能东西向或南北向通行,一般 的十字路口都是这样管控的。随着车辆的增多,十字路口的堵塞越来越严重,尤其是上下班时间经常出现堵死现象。于是交通部门在不动用过多经费的情况下而采用 了图二的交通管制,东西向车辆行进方式不变,而南北向车辆不能直行,需要右拐到下一个路口拐一个超大的U型弯,这样的措施避免了因车辆交错而引发堵死的次 数,从而提高了车辆的通过效率。我曾经问一个每天上下班乘公交经过此路口的同事,他说这样的改动不一定每次上下班时间都能缩短,但上班时间有保障了,从而 迟到次数减少了。如果今天你去西安市的南二环已经见不到U型弯了,东西向建设了高架桥,车辆分流后下层的十字路口已恢复为图一方式。

从效率的角度分析,在图一中等一个红灯45秒,远远小于图二拐那个U型弯用去的时间,但实际情况正好相反。我们可以设想一下,如果路上的所有运行车 辆都是同一型号(比如说全是QQ3微型车),所有的司机都遵守交规,具有同样的心情和性格,那么图一的通行效率肯定比图二高。现实中就不一样了,首先车辆 不统一,有大车、小车、快车、慢车,其次司机的品行不一,有特别遵守交规的,有想耍点小聪明的,有性子慢的,也有的性子急,时不时还有三轮摩托逆行一下, 十字路口的“死锁”也就难免了。

那么在什么情况下图二优于图一,是否能拿出一个科学分析数据来呢?以现在的科学技术水平是拿不出来的,就像长期的天气预报不可预测一样,西安市的交管部门肯定不是分析各种车辆的运行规律、速度,再进行复杂的社会学、心理学分析做出U型弯的决定的,这就是要说的不可测性。

现实中的程序亦然如此,比如WEB服务器,有的客户在快车道(宽带),有的在慢车道(窄带),有的性子慢(等待半分钟也无所谓),有的性子急(拼命 的进行浏览器刷新),时不时还有一两个黑客混入其中,这种情况每个服务器都不一样,既是是同一服务器每时每刻的变化也不一样,因此说不具有可测性。开发者 和维护者能做的,不论是前面的这种实验测试,还是对具体网站进行的压力测试,最多也就能模拟相当于QQ3通过十字路口的场景。

结束语

本篇文章比较了Linux系统上多线程和多进程的运行效率,在实际应用时还有其他因素的影响,比如网络通讯时采用长连接还是短连接,是否采用 select、poll,java中称为nio的机制,还有使用的编程语言,例如Java不能使用多进程,PHP不能使用多线程,这些都可能影响到并发模 式的选型。

最后还有两点提醒:

\1. 文章中的所有实验数据有环境约束。 \2. 由于并行服务的不可测性,文章中的观点应该只做参考,而不要去定性。

【参考资料】

\1. 《Linux系统下多线程与多进程性能分析》作者“周丽 焦程波 兰巨龙”,这是我写这篇文章的诱因之一,只是不知道引用原作的程序代码是否属于侵权行为。

\2. stevens著作的《Unix网络编程(第一卷)》和《Unix高级环境编程》,这两本书应该收集入IT的四书五经。

\3. Robert Love著作的《Linux内核设计与实现》

\4. John Fusco 著作的《Linux开发工具箱》,这本书不太出名,但却是我读过的对内存和进程调度讲解最清晰明了的,第5章“开发者必备内核知识”和第6章“进程”是这本书的精华。

Read More

(译)针对 Redis on Flash 优化RocksDB

原文 Optimization of RocksDB for Redis on Flash 作者Keren Ouaknine, Oran Agra, Zvika GuzCCS Concepts Information systems → Key-value stores; Database performance evaluation; Keywords: Databases, Benchmark, Redis, Rocksdb, Key-Value Store, SSD,NVMe

0. 概述

RocksDB是一个热门的KV存储引擎,针对高速存储设备做了优化,随着SSD变得流行prevalent,RocksDB获得了广泛的使用(widespread adoption)现在在生产环境中很常见is now common in production settings,尤其是许多软件栈嵌入RocksDB作为存储引擎来优化访问块存储。不幸的是,调优RocksDB是一个复杂的任务,涉及到许多不同依赖程度的参数involving many parameters with different degrees of dependencies. 在我们这篇论文中,一个调优好的配置可以使性能比基线配置参数的表现提高一个数量级by an order of magnitude

在这篇论文中,我们将描述我们在优化RocksDB在Redis on Flash(RoF)上的经验。RoF是一个Redis用SSD作为内存拓展的商业实现,用以动态提高每个节点的容量效率。RoF用来存储载内存中的热数据,利用RocksDB存储管理SSD上的冷数据。我们将描述我们在调优RocksDB参数使用上的方法,展示我们的的经验和发现(包括调优结果的利弊),使用两个云,EC2和GCE,综上,我们会展示如何调参RocksDB提高RoF数据库的复制实践11倍以上。我们希望我们的经验能帮助到其他使用,配置和调优RocksDB来认识到RocksDB的全部潜能

1. 介绍

RocksDB是一个持久化KV存储特定面向告诉存储,主要是SSD而设计[1]。从LevelDB[2]分支出来,RocksDB提供了更好的性能[3],被设计成高度灵活来方便嵌入高层应用的一个存储引擎,事实上,许多大规模产品使用RocksDB来管理存储,Leveraging借助它的高性能来mitigate缓和日益增长的存储系统的压力[4]

不幸的是RocksDB的灵活性和高性能也有代价:调优RocksDB是一个复杂的任务牵扯到上百个参数且有varying不同程度的内部依赖,此外,“然而最近的改动使RocksDB变得越好,比起levelDB它配置就越困难”;表现差的场景太多是错误的配置造成的。[5]

当使用RocksDB,经常被问到的问题主要有

  1. 哪些配置参数使用在哪个硬件或那种工作场景下的?
  2. 这些参数的最佳optimal参数是什么
  3. 这些参数是内部独立的吗(比如说,调优参数a只在参数b,c,d有特定值的事后才生效)
  4. 两个不同的调优是累积cumulate正优化还是负优化?
  5. 如果有的话,这些参数调优后的副作用是什么?(last but not least最后但不是不重要

这篇论文试图回答这些问题,通过分享我们在RoF上优化RocksDB的经验[6,7]–这是一个Redis内存KV存储的一个商业拓展[8]。RoF用SSDs作为内存RAM的拓展来提供可媲美内存Redis变小的性能,在动态增长扩容数据集的时候数据也能存在一个单点服务器上。在ROF,热数据存储在内存中,类数据存储在SSDs中,由RocksDB来管理。RocksDB能处理所有ROF访问存储,它的性能在整个系统性能中占了主要角色。尤其是低访问的局部场景。因为ROF致力于提供可以媲美纯内存Redis表现的性能,调优RocksDB便成了首要挑战

在调优RocksDB适配ROF的过程中,我们分析了大量的参数并且测试了它们在不同工作模式下对性能的影响。数据库复制,只写模式,1:1读写模式。为了验证我们配置在不同硬件环境下的健壮性,我们测试了Amazon Elastic Compute Cloud (EC2)和Google Compute Engine(GCE),综上,我们的调优减少了复制一个节点的时间(11倍)这篇论文来描述方法,调优过程和产生效果的参数设置

在第三段,我们将描述我们的方法并解释我们的实验过程。第四段,我们会列出载性能上有最大正相关的参数。我们也列出了一些我们期望提高性能但实际上降低了或者you其他副作用的参数。我们相信这些信息会帮助到他人。然而我们的经验只限于RoF场景中,我们期望相似的系统用相似的配置,希望我们的方法和结果会帮助减少调优的时间

综上,这篇论文会有下面的产出

  • 我们发表我们在EC2和GCE上 在一系列工作集下 RocksDB benchmark的结果和分析

  • 我们描述了我们的条有过程,对性能有有最大影响的的参数,和最佳参数;并且

  • 我们描述了调优的负面结果,或不成功,降低了性能,或有非直观的副作用

2. 背景

这段简要回顾下Redis,Redis on Flash,和RocksDB,描述这些系统的上层架构,提供简要北京以帮助理解这篇论文的细节

2.1 Redis

Redis[8]是一个热门的开源内存KV村春提供了高级键值抽象,Redis是单线程的,只能在同一时间处理同一个玲玲。不像传统的KV系统(键只是个简单的数据类型,通常是字符串),Redis中的键can function as可以是复杂的数据类型,像hash,list,set,或者sorted set,furthermore此外,Redis可以使用复杂的原子操作,像是对列表的入队出队操作,在sorted set插入一个新值等等

事实证明,Redis的抽象和高速在许多延迟敏感的场景中特别有用,因此,Redis得到了广泛的使用,越来越多公司在生产环境中使用redis[9]

Redis支持高可用和持久化,高可用通过主从节点同步复制来实现,故障转移(failover),当主程序挂了,子程序能相应的接管过来。持久化可以通过以下两种方法配置

  1. 实时快照文件(RDB)
  2. 使用log文件(AOF)

要注意这三种机制(AOF重写,RDB快照,复制)都依赖于fork进程来处理一个时间点上的快照,(与此同时主程序继续处理客户端命令)

2.2 Redis on Flash (RoF)

像Redis这种内存数据库通常把数据保存在内存中,数据库快了但是也很贵,因为1内每个节点的内存容量有一定限制2每GB内存价格很贵。Redis on Flash(RoF)是一个Redis的商业拓展,用SSDs来作为内存的拓展,来高效的动态增加单个服务器上数据集大小。RoF完全兼容Redis并实现了所有的Redis命令和特性,Rof用和Redis相同的机制来保证高可用和持久化且依赖于SSD(非易失性闪存)

RoF把热数据保存在内存中,把冷数据持久化到固态硬盘上。它使用RocksDB作为存储引擎,所有的固态硬盘通过RocksDB来管理,通过RocksDB接口来访问硬盘上的值。当一个客户端请求一个冷的数据,请求会被临时阻塞直到designated指定的RoF I/O线程 将I/O请求提交到RocksDB,在此期间,Redis的主线程继续处理其他客户端的请求。

2.3 RocksDB

RocksDB[10] 是一个C++实现的开源的KV存储,它提供get/put/delete/scan 键值的操作。RocksDB可以保存大量的数据,它使用sst(sorted static table)来将数据保存到NVMes,SATA SSDs,或者机械磁盘上,尽可能的降低延迟。RocksDB使用布隆过滤器来判定键在哪个sst文件中。为了避免随机写,它将数据积累到内存中的memtable中,然后一次性刷写到硬盘中。RocksDB的文件是不可变的,一旦生成就不会继续写该文件。记录不会被更新或者删除,会生成一个新文件。这会在硬盘生成一些多余的数据,会需要数据库Compaction(压缩),Compaction文件会移除冗余的键值对并腾出空间,如图所示

图1

2.3.1 RocksDB架构

RocksDB用不同的排列组织数据,也就是层level,每层都有个目标大小,每层的目标大小增长倍数是相同的,默认是10倍,因此,如果第一层目标大小1g,那么2,3,4层大小就是10g,100g,1000g,一个键可能出现在不同的层,随着compaction,但是越新的值层越高,越旧的值层越低

RocksDBinitially首先将新的写入存储在内存memtable中,当memtable写满了,他会被转成不可写的memtable,并插入到落盘流程(flush pipeline)中,同时,分配一个新的memtable给后面的写,第0层就是memtable,当第0层满了,数据就会被compact,挪到下面的层中,compaction流程应用到所有的层,将文件从层n合并到层n+1

2.3.2 放大因子

我们通过检测各种工作负载实验下的吞吐量和持续时间来测评优化带来的影响。此外,我们基于以下RocksDB定义的放大因子定义来监测副作用

  • 读放大是从硬盘中(包括数据compaction中的读)读的数据 比 从KV存储中的读的数据
  • 写放大是总的硬盘中的写入数据 比写入KV存储中的数据
  • 空间放大是硬盘上总的数据文件比 KV存储的大小

2.3.3 memtier benchmark

Memtier benchmark[11]是一个benchmark工具,我们用它测量Redis流量。这是Redis实验室[12]开发并开源的, 它可以发送各种比例的读写,实现了不通模式的流量模式,比如连续,随机,高斯分布等。这个工具维护了一个一个Redis命令的pipeline,保证当回复响应时发送一个新的请求。这个命令行工具也提供堆请求流的各种程度的控制,比如操作数,读写比率,工作线程数,客户端数量,值大小等等。在每次运行的结尾,他会报告所有读写吞吐量的平均值和他们的延迟。

3. 方法

当我们优化RocksDB,我们首先的动机,以及我们首要的优化目标是减少ROF数据库复制的时间。复制流程在保证主节点高可用是必要的,且包含以下两个步骤,1从主节点服务区读取整个数据集并发送到网络中的其他从节点,2从节点服务器写入数据集。一旦首次复制流程完成,所有的后续的反动都会从主节点发送到从节点来保持和主节点的同步。当(假如)主节点挂掉了,从节点会成为新的主节点,新的从节点进行复制,这样整个数据库保持容错。因此,复制事件是很重要的,因为1复制流程期间由于主节点开始忙碌的读和发送数据,整个集群在一个低性能的状况中,且2这会有数据丢失的风险,因为当前主节点挂了,无法进行数据复制。

使用默认的RocksDB设置,复制50g数据,内存:固态比10:90的情况下会使用whoping特别长的212分钟。对我们的场景来说这是不可容忍的时长,应该降低到30分钟以内,在第四段中,我们将描述我们在配置中所处的更改,降低复制时间到18分钟。因为真正的生产环境通常单个Redis服务器有50g左右的数据,所以我们用50g数据来实验。

当我们首要的目的是减小数据库复制的时间,保证我们的系统稳定性能是十分必要的imperative,因此,每次每次优化,我们都会测量四种工作负载场景

  1. 只写,写50m key,1k value,总共50g,这个压力代表所当前常用的数据库规模

  2. 只读,读10%的数据集

  3. benchmark,混合读写,50-50

  4. 数据库复制,从主节点读50g写入从节点

使用这些工作模式,当我们的优化效果能显著提高第四个并且没有降低其他三个,我们接受这个结果。

优化流程第一步是确定瓶颈。因为数据库复制主要是主读从写,我们只分析这两种操作。我们跑两个实验,1主节点书全部存在内存,硬盘写只发生在从节点(这个实验叫纯内存主节点),2所有从节点数据都存在内存,硬盘压力都在主节点的读上(这个实验教纯内存从节点),比较两种场景的持续时间。帮助我们更好的优化,来达到减少复制时间的目的

我们也分析服务器的活动:我们统计每次调优过程中的运行时间,吞吐量和延迟。我们也检测各项系统指标,Redis和RocksDB的线程负载,IO状态,RocksDB的每层的状态,放大因子,放慢速度和写停止the slowdowns, and the write stalls.这些指标帮助我们去测量评估每次优化后的副作用,选择不使用opt-out那些降低性能的调优参数(见第四段)

硬件:我们所有的实验都泡在EC2和GCE云上,EC2是广泛使用的云服务器,我们使用i2.8x Large 32vCPU 244GB内存,8x800GB SSD,10g加强型网络带宽,此外我们使用的GCE用的是EC2上用不到的NVMe硬盘,同样采用32vCPU,Intel Xeon 2.20GHz, 208g RAM,8x375gb NVMe。

4. 测试和结果

这小节描述我们的测试过程和我们在优化期间的发现。首先 4.1小节我们通过实验详细的介绍了显著提高性能的两个参数和一个本以为能提升性能却带来负面影响的参数,这些实验是在EC2伤实验的,在4.3我们重复该实验在用NVMe的GCE上

在Throughout这节中,粗体字是RocksDB调优实验, 引用是RocksDB配置项的名字(knob name),它们的影响列在下表中

参数parameter 名字 初始值 改过的新值 性能影响
Compaction threads max_background_compactions 8 64 24%
Slowdowns level0_slowdown_writes_trigger 12 24 10%
Stops level0_stop_write_trigger 20 40 10%
Compaction readahead compaction_readhead_size 0 2MB 300%
Redis IO threads Redis IO threads 8 64 500%
RAID chunks chunk 512k 16k 68%
filter for hits optimize_filters_for_hits 0 1 7%
bulk mode prepare_for_bulk_mode 0 1 500%
Block size block_size 4k 16k 60%
Synchronization bytes_per_sec 0MB 2MB 0%

4.1 在EC2 上使用SATA接口SSD

我们的RocksDB基线配置用的RocksDB4.9和几个和默认参数不同,列在下表中

Parmeter value
memtable budget 128MB
Level 0 slowdown 12
max open files -1
WAL disabled
OS buffer disabled
Cache disabled

这些在开始阶段就设定了,是有一定的原因的,因为Redis on Flash用内存来缓存,所以我们禁用了RocksDB的cache,没有必要同一个值缓存两次。我们也禁用了OS缓冲(buffer)来保证写在内存缓冲或者直接写入硬盘。另外一个有效的改动是禁用WAL通过(rocksdb_writeoptions_disable_WAL),在我们这个场景下WAL有消耗但是用不到,Redis on Flash本身就保证了一致性,无需使用WAL。

4.1.1 最大化并行来消除瓶颈

开始优化,我们先降低每个线程上的负载来降低高CPU利用率high CPU utilization

我们让RocksDB后台县城和Redis I/O线程一样多,增加他们的并行性来防止他们成为瓶颈

RocksDB后台线程主要是两个函数,compaction 工作和刷到硬盘(flush job),在图片2可以看到, 使用max_background_compaction从把compaction 线程8改到64,性能提高24%,我们观察到compaction越并行化,compaction周期就越短,写就有更高的吞吐量。我们也针对flush线程(拷贝数据从memtable写到硬盘)做了测试,他们的并行化并没有显著的提高性能。

图2

4.1.2 稳定吞吐量和延迟

下一步,我们尝试稳定系统的性能,即(namely)在吞吐量和延迟方面降低服务器性能差异variance。在图三,我们能观察到服务器吞吐量上断断续续stop-start的现象,每十秒有着50k ops/sec低于10k ops/sec。这个表现也显示出长尾延迟,即少量请求造成长时间的响应

图3

正如预期,是compaction周期造成这种流量。这有由于level 0 文件到达上限产生的。比如说level0_slowdown_writes_trigger(默认12),当这个上限很低,compaction就会很频繁,就会扰乱流量,就像在图三中看到的那样。在另一方面,如果这个上限过高,我们积累的需要compaction的量(debt)就比较多,最终会造成RocksDB触发level0_stop_write_trigger 阻塞所有流量数秒。对于ROF,她最好有较慢但稳定的吞吐量载整个期间避免stop-start和写阻塞,一个例子,在图4中显示了一个稳定的吞吐量几乎没有出现1-2k ops/sec的情况

图4

我们实验了不同的参数来调优slowdownstop,我们观察到了很高的吞吐量,当我们继续提高这个参数的时候,又造成了负优化导致积累了太多的compaction debt,结果在图5中显示,最优的参数是slowdown=24stop=40,给我们带来了10%的性能提升。Compaction参数后面的值(slowdown=40,stop=60)表现出更快的复制但是在只读工作集种对于吞吐量带来了负面影响。在延迟的compaction的场景下,访问时间会变长,因为bloom filter页更大,数据也没压缩。

图5

4.1.3 产生更大的吞吐量

compaction预读选项compaction_readahead_size)起用能在compaction过程中 读取大的compaction inputs。默认RocksDB不使用预读(0MB),我们使用2MB readahead进行实验获得了缩短三倍复制时间的效果。前文提到,compaction效率受到RocksDB吞吐量的影响。此外,我们通过调整RedisIO线程数获得了更好的输出,因为RocksDB的API是同步的,我们使用多路IO线程模型来提高IO并行化,我们发现提高RedisIO页提高了读请求的延迟,但是这个提高也带来了memtable的争用,导致显著降低了写请求的性能。

因此我们决定用单线程IO来处理写,用其余的IO线程来处理并发读(一写多读 , one writer),这个提升提高了五倍。

因为RoF会部署在多个存储器组合的设备来增加存储带宽,我们也在各种RAID设置的环境中做了实验。RoF使用(employ)RAID-0来压缩卷,我们在不同的压缩块大小的环境中测试比较了性能。见图6,RAID 越小的块chunk越能提高硬盘的并行程度以及性能,因此,我们将默认的512kb改成16kb,从而提高了68%。注意RocksDB调优指南建议使用大的块大小(chunk size)[14],我们的实验结果正好相反,越小的块性能越好

图6

最后,因为RoF 把所有key和他们的位置放在了内存中,向RocksDB的请求不会错过(miss),请求不可用的key会被ROF处理,只有请求已知在硬盘上的数据才会转发到RocksDB子系统,因此,我们开启了RocksDB特定designatedbloom filter配置项来优化这个场景, optimize_filters_for_hits。RocksDB实现的布隆过滤器对于每个SST文件会快速检查搜索的key是否在该文件中存有一份,这个优化去掉了对最底层的过滤,因为一旦请求来到这层,那值肯定就在这层。这个优化会对复制性能带来7%的提升。

4.1.4 读速度

当复制一个数据库,主节点发送数据集的一个副本给从节点,主节点需要把硬盘和内存中的值都读出来。这有两种方法来从硬盘中读数据,第一种就是使用RocksDB的迭代器来按照顺序读RocksDB数据库文件,第二种方法就是读那些值不在内存中的,这降低了总的读但是增加了随机读。我们主要根据下面两个条件来对这两种方法进行取舍

  1. RocksDB在两种场景下的性能
  2. 顺序读和随机读下的硬盘速度

图7 画出了在复制过程中剩余的keys在内存中还有多少来比较这两种方法。x轴越长表示数据在闪存硬盘中(不在内存中)的比例越高。在顺序读RocksDB数据集的场景下,所有实验场景下,复制时间很稳定(大致保持不变stays roughly constant)(蓝线),相反,随机读只向硬盘访问必要的数据的方法下,复制时间随着闪存硬盘中数据的比例而线性上升。图中可以看到,随机读仅在内存中有85%数据以上的场景下表现较好,顺序读载配置了预读后有效果提升,读取大的块(chunk)能加速顺序读,上述结果在不同的数据集合对象大小表现是一致的。我们采用顺序读作为复制方案。

图7

4.2 负优化结果

许多参数我们试验后决定不采用,不仅是因为他们没有减少肤质是件,反而还有副作用,在这个小结,我们列举三个调优过程中反直觉counter-intuitive或造成意外副作用的几个场景

我们开始在(批量写?)bulk load模式上开始调优(prepareforbulkload),开启这个模式提高了复制时间五倍,然而,bulk load 模式有副作用,会使读变得特别慢(prohibitively slow),因为这个模式禁止了压缩,如果数据集不压缩,就会有非常多的SST文件,需要非常多的读取。我们在复制结束试着关掉bulk load启动手动compaction来恢复数据库可用,但是手动compaction是一个单线程处理,会花费很长时间来完成,比复制时间还要长,我们采用自动compaction,多线程,但是后续subsequent的读仍然很慢。

意外的是,这两种compaction最终结果不一样,第一种花费较长时间,第二种产生较低的读取,此外也没有权利compaction。还记得4.1.2我们遇到的类似的compaction debt问题吗。Bulk mode(13)是有许多不同的调优参数组成的is composed of,我们分别测试各种参数来试试能不能带来好处。然而,禁用compaction被证明是性能提升背后的主要原因,我们就放弃了这个配置选项。

全同步在RoF不是必须,因为我们使用闪存硬盘作为内存扩展,因此,我们针对全同步消耗来进行调优,我们配置了2MB同步一次,bytes_per_sync,意外的是,我们没观察到任何提升,我们用dd写数据到硬盘,我们比较了写入50g数据不开启同步的时间,发现性能提升了2倍,在复制实验中没有类似表现,我们有点困惑。我们猜测写被缓存了(cached)因此我们看不到同步的优化(saving),但是查看meminfo我们观察到数据没被缓存 ,我们也看了其他设备RAID和他们的iops,也没有降低。最后我们用strace确定确实没有同步动作。这个反直觉的现象仍然是个谜。

最后,我们调了block_size参数,每个SST文件包含索引和他自己的block,增加块(block)大小可以减少每个文件中索引的数量。但是块大了读取会变多(见2.3.2读放大),增加了block大小也减少了内存使用,因为内存中的索引更大了(?),默认的块大小是4kb,当我们block调到16kb,我们观察到了60%的降低,不像其他参数可以在运行时动态调节,block大小修改不能撤销(编译进去的)所以我们观测到了在读性能上的负面影响后我们就结束了这个调优。

4.3 在GCE NVMe SSD上的实验

在EC2上的调优经验也帮助我们在GCE上进行试验,首先我们调整RAID优化,我们把之前的在chunk 大小上的经验直接搬过来(a sweep of),证实了16kb确实是最优参数,能将复制时间提高41%。

默认的level0_slowdown(12)stop(20)延迟写是不必要的。当我们达到12个文件后,延迟写在吞吐量上面的表现非常明显,从60k降到600 ops/sec 直到compaction工作结束才恢复。为了避免系统的这种stop-start现象,我们增加了这两个参数到20 24,观察到性能提升了15%(类似EC2)

此外,我们加上了其他调优,比如优化filter等等,我们也加了RocksDB compaction线程和Redis IO线程,配置了一个写者 (one writer),增加了数据预读。这些改动累积效果,复制时间缩短到12分50秒,相同的数据规模在EC2上同样参数配置用了18分钟。不过硬件参数也不同,SATA-SSD和NVMe SSD,我们认为GCE本应该有更快的复制时间

5. 相关工作

虽然也有其他可用的存储引擎[15,16,17],RocksDB有着更好的性能而得到了广泛的使用,RocksDB调优指南[18]提供了RocksDB[10]基本的配置指引,也是我们测试中的基本配置。在我们的这篇论文中,更进一步的调优带来了可观considerably的性能提升(我们的例子中是11倍)

krishnamoorthy和Choi[19]做了在NVMe SSD上调优RocksDB的工作,据我们所知,这是最接近我们论文展示的工作,然而,他们的分析是在RocksDB压力测试场景下的,我们的优化是在工业实践(用RocksDB做后端引擎)场景下的。所以,两者的调优过程和结论也有所不同。

类似Redis on Flash,也有一些Redis的拓展使用了闪存硬盘为Redis提供拓展能力,在Intel的项目[20],也是Redis分支,依赖SSD来提供持久化,动机是消除eliminate硬盘备份写硬盘的同步代价而采用了AOF策略,如果有个节点故障,机器会通过固态硬盘上的数据来修复。这个方法不能用在云设备上,因为机器不能复用/修复。Redis原生硬盘存储(Naive-Disk-Store)使用LMDB[17]来管理硬盘上的数据,数据是周期性的刷到硬盘上的,如果一旦故障仅会丢失最后一次没刷盘的数据。它不会把key存到内存中也不支持持久化复制,定期删除,scan。

6. 结论

随着使用RocksDB作为存储引擎的应用越来越多,Rocksdb也是选型存储引擎的首选,然而,灵活和高性能是有代价的,调优RocksDB是个复杂的工作,在我们这篇论文中,调优好的配置要比基本配置表现好出一个数量级

在这篇论文中我们使用Redis on Flash 一个热门Redis KV存储的一个商业拓展,作为调优RocksDB的研究用例,我们描述条有方法,调优过程,和RocksDB设置上的改动点,使得RoF性能提升11倍以上,实验还展示了可能会导致负面效果或预料之外副作用的的配置。然而不同的使用场景有不同的最优配置,我们希望我们的经验会帮助其他人来优化RocksDB,提高新更能,减少研发时间。高度调优的Rocksdb的性能提升绝对值得付出这些工作。

7. 援引

[1] Siying Dong. RocksDB: Key-Value Store Optimized for Flash-Based SSD.https://www.youtube.com/watch?v=xbR0epinnqo. [2] RocksDB and LevelDB. http://rocksdb.org/blog/2016/01/29/compaction_pri.html. [3] Paul Dix. Benchmarking LevelDB vs RocksDB. https: //www.influxdata.com/benchmarking-leveldb-vs-rocksdbvs-hyperleveldb-vs-lmdb-performance-for-influxdb/. [4] RocksDB users. https: //github.com/facebook/rocksdb/blob/master/USERS.md. [5] Mark Callaghan blog. http://smalldatum.blogspot.co.il/2014/07/benchmarking-leveldb-family.html, July 7, 2014. [6] Redis on Flash documentation. https://redislabs.com/redis-enterprise-documentation/rlecflash. [7] Redis on Flash. https://redislabs.com/rlec-flash. [8] Redis. http://redis.io/. [9] Redis Users. http://techstacks.io/tech/redis. [10] RocksDB website. http://rocksdb.org/. [11] Memtier benchmark. https://redislabs.com/blog/memtier_benchmark-a-highthroughput-benchmarking-tool-for-redis-memcached. [12] Redis Labs. https://redislabs.com/. [13] Gartner. Magic Quadrant. https://www.gartner.com/doc/reprints?id=1-2G2O5FC&ct=150519. [14] RocksDB FAQ.https://github.com/facebook/rocksdb/wiki/RocksDB-FAQ. [15] Kyoto Cabinet. http://fallabs.com/kyotocabinet/. [16] LevelDB. http://leveldb.org/. [17] Lightning Memory Mapped Database.https://lmdb.readthedocs.io. [18] Facebook. RocksDB Tuning Guide. https://github.com/facebook/rocksdb/wiki/RocksDB-Tuning-Guide. [19] Praveen Krishnamoorthy and Choi Changho. Fine-tuning RocksDB for NVMe SSD. https://www.percona.com/live/data-performance-conference2016/sites/default/files/slides/Percona_RocksDB_v1.3.pdf. [20] Intel. Redis with persistent memory. https://github.com/pmem/redis#redis-with-persistent-memory. [21] Redis Naive Disk Storage.https://github.com/mpalmer/redis/blob/nds-2.6.

Read More

^