LD_PRELOAD为何不能劫持printf

环境gcc linux

简而言之是gcc在某些时刻会优化掉printf,优化成puts


下面是牢骚。群友们针对printf咋被优化的进行了探讨,可能是弱符号,或者涉及到变参的复杂场景,以及printf可以使用寄存器作为参数。一顿天马行空

这个链接具有一定的参考性

特别摘抄过来,作为后续分析glibc的一个参考思路

printf的代码在哪里?

显然,Helloworld的源代码需要经过编译器编译,操作系统的加载才能正确执行。而编译器包含预编译、编译、汇编和链接四个步骤。

#include<stdio.h>
int main()
{
​    **printf("Hello World !\n");**
​    return 0;
}

首先,预编译器处理源代码中的宏,比如#include。预编译结束后,我们发现printf函数的声明。

$/usr/lib/gcc/i686-linux-gnu/4.7/cc1 -E -quiet
​ main.c -o main.i # 1 “main.c” # 1 “<命令行>" \# 1 "main.c" ... extern int printf (const char *__restrict __format, ...); ...

int main() { printf(“Hello World!\n”); return 0; }

然后编译器将高级语言程序转化为汇编代码。

$/usr/lib/gcc/i686-linux-gnu/4.7/cc1 -fpreprocessed -quiet  \
​    main.i -o main.s
​    .file      "main.c"
​    .section   .rodata
.LC0:
​    .string    "Hello World!"
​    .text
​    .globl     main
​    .type      main, @function
main:
​    pushl      %ebp
​    movl       %esp,  %ebp
​    andl       $-16,  %esp
​    subl       $16,   %esp
​    movl       $.LC0, (%esp)
​    `call       puts`
​    movl       $0,    %eax
​    leave
​    ret
​    .size      main, .-main
...

我们发现printf函数调用被转化为call puts指令,而不是call printf指令,这好像有点出乎意料。不过不用担心,这是编译器对printf的一种优化。实践证明,对于printf的参数如果是以’\n’结束的纯字符串,printf会被优化为puts函数,而字符串的结尾’\n’符号被消除。除此之外,都会正常生成call printf指令。

如果我们仍希望通过printf调用”Hello World !\n”的话,只需要按照如下方式修改即可。不过这样做就不能在printf调用结束后立即看到打印字符串了,因为puts函数可以立即刷新输出缓冲区。我们仍然使用puts作为例子继续阐述。

    .section   .rodata
.LC0:
​    .string    "hello world!\n"
​    ...
​    call       printf
...

接下来,汇编器开始工作。将汇编文件转化为我们不能直接阅读的二进制格式——可重定位目标文件,这里我们需要gcc工具包的objdump命令查看它的二进制信息。可是我们发现call puts指令里保存了无效的符号地址。

$as -o main.o main.s
$objdump –d main.o
main.o:     文件格式 elf32-i386
Disassembly of section .text:
00000000 <main>:
   0:  55                     push   %ebp
   1:  89 e5                  mov    %esp,%ebp
   3:  83 e4 f0               and    $0xfffffff0,%esp
   6:  83 ec 10               sub    $0x10,%esp
   9:  c7 04 24 00 00 00 00   movl   $0x0,(%esp)
  10:  e8 fc ff ff ff         call   11 <main+0x11>
  15:  b8 00 00 00 00         mov    $0x0,%eax
  1a:  c9                     leave  
  1b:  c3                     ret

而链接器最终会将puts的符号地址修正。由于链接方式分为静态链接和动态链接两种,虽然链接方式不同,但是不影响最终代码对库函数的调用。我们这里关注printf函数背后的原理,因此使用更易说明问题的静态链接的方式阐述。

$/usr/lib/gcc/i686-linux-gnu/4.7/collect2                   \
​    -static -o main                                         \
​    /usr/lib/i386-linux-gnu/crt1.o                          \
​    /usr/lib/i386-linux-gnu/crti.o                          \
​    /usr/lib/gcc/i686-linux-gnu/4.7/crtbeginT.o             \
​    main.o                                                  \
​    --start-group                                           \
​    /usr/lib/gcc/i686-linux-gnu/4.7/libgcc.a                \
​    /usr/lib/gcc/i686-linux-gnu/4.7/libgcc_eh.a             
​    /usr/lib/i386-linux-gnu/libc.a                          \
​    --end-group                                             \
​    /usr/lib/gcc/i686-linux-gnu/4.7/crtend.o                \
​    /usr/lib/i386-linux-gnu/crtn.o

$objdump –sd main

Disassembly of section .text:

...

08048ea4 <main>:
 8048ea4:  55                     push   %ebp
 8048ea5:  89 e5                  mov    %esp,%ebp
 8048ea7:  83 e4 f0               and    $0xfffffff0,%esp
 8048eaa:  83 ec 10               sub    $0x10,%esp
 8048ead:  c7 04 24 e8 86 0c 08   movl   $0x80c86e8,(%esp)
 8048eb4:  e8 57 0a 00 00         call   8049910 <_IO_puts>
 8048eb9:  b8 00 00 00 00         mov    $0x0,%eax
 8048ebe:  c9                     leave  
 8048ebf:  c3                     ret
...

静态链接时,链接器将C语言的运行库(CRT)链接到可执行文件,其中crt1.o、crti.o、crtbeginT.o、crtend.o、crtn.o便是这五个核心的文件,它们按照上述命令显示的顺序分居在用户目标文件和库文件的两侧。由于我们使用了库函数puts,因此需要库文件libc.a,而libc.a与libgcc.a和libgcc_eh.a有相互依赖关系,因此需要使用-start-group和-end-group将它们包含起来。

链接后,call puts的地址被修正,但是反汇编显示的符号是_IO_puts而不是puts!难道我们找的文件不对吗?当然不是,我们使用readelf命令查看一下main的符号表。竟然发现puts和_IO_puts这两个符号的性质是等价的!objdump命令只是显示了全局的符号_IO_puts而已。

$readelf main –s
Symbol table '.symtab' contains 2307 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
...
  1345: 08049910   352 FUNC    WEAK   DEFAULT    6 puts
...
  1674: 08049910   352 FUNC    GLOBAL DEFAULT    6 _IO_puts
...

那么puts函数的定义真的是在libc.a里吗?我们需要对此确认。我们将libc.a解压缩,然后全局符号_IO_puts所在的二进制文件,输出结果为ioputs.o。然后查看该文件的符号表。发现ioputs.o定义了puts和_IO_puts符号,因此可以确定ioputs.o就是puts函数的代码文件,且在库文件libc.a内。

$ar -x /usr/lib/i386-linux-gnu/libc.a
$grep -rin "_IO_puts" *.o
​    $readelf -s ioputs.o
Symbol table '.symtab' contains 20 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
...
​    11: 00000000   352 FUNC    GLOBAL DEFAULT    1 _IO_puts
...
​    19: 00000000   352 FUNC    WEAK   DEFAULT    1 puts

二、printf的调用轨迹

我们知道对于”Hello World !\n”的printf调用被转化为puts函数,并且我们找到了puts的实现代码是在库文件libc.a内的,并且知道它是以二进制的形式存储在文件ioputs.o内的,那么我们如何寻找printf函数的调用轨迹呢?换句话说,printf函数是如何一步步执行,最终使用Linux的int 0x80软中断进行系统调用陷入内核的呢?

如果让我们向终端输出一段字符串信息,我们一般会使用系统调用write()。那么打印Helloworld的printf最终是这样做的吗?我们借助于gdb来追踪这个过程,不过我们需要在编译源文件的时候添加-g选项,支持调试时使用符号表。

$/usr/lib/gcc/i686-linux-gnu/4.7/cc1 -fpreprocessed -quiet -g\

​ main.i -o main.s

然后使用gdb调试可执行文件。

$gdb ./main

(gdb)break main

(gdb)run

(gdb)stepi

在main函数内下断点,然后调试执行,接着不断的使用stepi指令执行代码,直到看到Hello World !输出为止。这也是为什么我们使用puts作为示例而不是使用printf的原因。

(gdb)

0xb7fff419 in __kernel_vsyscall ()

(gdb)

Hello World!

我们发现Hello World!打印位置的上一行代码的执行位置为0xb7fff419。我们查看此处的反汇编代码。

(gdb)disassemble
Dump of assembler code for function __kernel_vsyscall:
   0xb7fff414 <+0>:  push   %ecx
   0xb7fff415 <+1>:  push   %edx
   0xb7fff416 <+2>:  push   %ebp
   0xb7fff417 <+3>:  mov    %esp,%ebp
   0xb7fff419 <+5>:  sysenter
   0xb7fff41b <+7>:  nop
   0xb7fff41c <+8>:  nop
   0xb7fff41d <+9>:  nop
   0xb7fff41e <+10>: nop
   0xb7fff41f <+11>: nop
   0xb7fff420 <+12>: nop
   0xb7fff421 <+13>: nop
   0xb7fff422 <+14>: int    $0x80
=> 0xb7fff424 <+16>: pop    %ebp
   0xb7fff425 <+17>: pop    %edx
   0xb7fff426 <+18>: pop    %ecx
   0xb7fff427 <+19>: ret    
End of assembler dump.

我们惊奇的发现,地址0xb7fff419正是指向sysenter指令的位置!这里便是系统调用的入口。如果想了解这里为什么不是int 0x80指令,请参考文章《Linux 2.6 对新型 CPU 快速系统调用的支持》。或者参考Linus在邮件列表里的文章《Intel P6 vs P7 system call performance》

系统调用的位置已经是printf函数调用的末端了,我们只需要按照函数调用关系便能得到printf的调用轨迹了。

(gdb)backtrace
#0  0xb7fff424 in __kernel_vsyscall ()
#1  0x080588b2 in __write_nocancel ()
#2  0x0806fa11 in _IO_new_file_write ()
#3  0x0806f8ed in new_do_write ()
#4  0x080708dd in _IO_new_do_write ()
#5  0x08070aa5 in _IO_new_file_overflow ()
#6  0x08049a37 in puts ()
#7  0x08048eb9 in main () at main.c:4

我们发现系统调用前执行的函数是__write_nocancel,它执行了系统调用__write!

三、printf源码阅读

虽然我们找到了Hello World的printf调用轨迹,但是仍然无法看到函数的源码。跟踪反汇编代码不是个好主意,最好的方式是直接阅读glibc的源代码!我们可以从官网下载最新的glibc源代码(glibc-2.18)进行阅读分析,或者直接访问在线源码分析网站LXR。然后按照调用轨迹的的逆序查找函数的调用点。

1.puts 调用 _IO_new_file_xsputn

具体的符号转化关系为:_IO_puts => _IO_sputn => _IO_XSPUTN => __xsputn => _IO_file_xsputn => _IO_new_file_xsputn

$cat ./libio/ioputs.c
int
_IO_puts (str)
​     const char *str;
{
  int result = EOF;
  _IO_size_t len = strlen (str);
  _IO_acquire_lock (_IO_stdout);

  if ((_IO_vtable_offset (_IO_stdout) != 0
​       || _IO_fwide (_IO_stdout, -1) == -1)
​      && **_IO_sputn (_IO_stdout, str, len)** == len
​      && _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
​    result = MIN (INT_MAX, len + 1);
  _IO_release_lock (_IO_stdout);
  return result;
}
#ifdef weak_alias
weak_alias (_IO_puts, puts)
#endif

这里注意weak_alias宏的含义,即将puts绑定到符号_IO_puts,并且puts符号为weak类型的。这也就解释了puts符号被解析为_IO_puts的真正原因。

2._IO_new_file_xsputn 调用 _IO_new_file_overflow

具体的符号转化关系为:_IO_new_file_xsputn => _IO_OVERFLOW => __overflow => _IO_new_file_overflow

$cat ./libio/fileops.c
_IO_size_t
_IO_new_file_xsputn (f, data, n)
​     _IO_FILE *f;
​     const void *data;
​     _IO_size_t n;
{
 ...
  if (to_do + must_flush > 0)
​    {
​      _IO_size_t block_size, do_write;
​      /* Next flush the (full) buffer. */
​      if (**_IO_OVERFLOW (f, EOF)** == EOF)
​    /* If nothing else has to be written or nothing has been written, we
​       must not signal the caller that the call was even partially
​       successful.  */
​    return (to_do == 0 || to_do == n) ? EOF : n - to_do;
...

3._IO_new_file_overflow 调用 _IO_new_do_write

具体的符号转化关系为:_IO_new_file_overflow =>_IO_do_write =>_IO_new_do_write

$cat ./libio/fileops.c
int
_IO_new_file_overflow (f, ch)
​      _IO_FILE *f;
​      int ch;
{
 ...
  if (INTUSE(**_IO_do_write**) (f, f->_IO_write_base,
  f->_IO_write_ptr - f->_IO_write_base) == EOF)
  return EOF;
  return (unsigned char) ch;
}

4. _IO_new_do_write 调用 new_do_write

具体的符号转化关系为:_IO_new_do_write => new_do_write

$cat ./libio/fileops.c
int
_IO_new_do_write (fp, data, to_do)
​     _IO_FILE *fp;
​     const char *data;
​     _IO_size_t to_do;
{
  return (to_do == 0
​      || (_IO_size_t) **new_do_write** (fp, data, to_do) == to_do) ? 0 : EOF;
}

5. new_do_write调用 _IO_new_file_write

具体的符号转化关系为:new_do_write =>_IO_SYSWRITE => __write() => write() => _IO_new_file_write

$cat ./libio/fileops.c
_IO_size_t
new_do_write (fp, data, to_do)
_IO_FILE *fp;
const char *data;
_IO_size_t to_do;
{
 ...
  count = **_IO_SYSWRITE** (fp, data, to_do);
  if (fp->_cur_column && count)
  fp->_cur_column = INTUSE(_IO_adjust_column) (fp->_cur_column - 1, data, count) + 1;
 ...
}

6. _IO_new_file_write调用 write_nocancel

具体的符号转化关系为:_IO_new_file_write=>write_not_cancel => write_nocancel

$cat ./libio/fileops.c
_IO_ssize_t
_IO_new_file_write (f, data, n)
_IO_FILE *f;
const void *data;
_IO_ssize_t n;
{
 _IO_ssize_t to_do = n;
  while (to_do > 0)
  {
​    _IO_ssize_t count = (__builtin_expect (f->_flags2& _IO_FLAGS2_NOTCANCEL, 0)? **write_not_cancel** (f->_fileno, data, to_do): write (f->_fileno, data, to_do));
...
}

7. write_nocancel 调用 linux-gate.so::__kernel_vsyscall

具体的符号转化关系为:write_nocancel => INLINE_SYSCALL => INTERNAL_SYSCALL =>__kernel_vsyscall

注意 linux-gate.so在磁盘上并不存在,它是内核镜像中的特定页,由内核编译生成。关于它的更多信息,可以参考文章《linux-gate.so技术细节》和《What is linux-gate.so.1?》

Read More

redis release note 与 redis命令cheatsheet

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 放开了对客户端的连接数限制  
2.6 hash函数种子随机化,有效防止碰撞  
2.6 SHUTDOWN命令添加SAVE和NOSAVE两个参数,分别用于指定SHUTDOWN时用不用执行写RDB的操作  
2.6 虚拟内存Virtual Memory相关代码全部去掉  
2.6 sort命令会拒绝无法转换成数字的数据模型元素进行排序  
2.6 不同客户端输出缓冲区分级,比如普通客户端、slave机器、pubsub客户端,可以分别控制对它们的输出缓冲区大小  
2.6 更多的增量过期(减少阻塞)的过期key收集算法 ,当非常多的key在同一时间失效的时候,意味着redis可以提高响应的速度  
2.6 底层数据结构优化,提高存储大数据时的性能  
2.8 引入PSYNC,主从可以增量同步,这样当主从链接短时间中断恢复后,无需做完整的RDB完全同步  
2.8 从显式ping主,主可以扫描到可能超时的从  
2.8 新增命令:SCAN、SSCAN、HSCAN和ZSCAN  
2.8 crash的时候自动内存检查  
2.8 新增键空间通知功能,客户端可以通过订阅/发布机制,接收改动了redis指定数据集的事件  
2.8 可绑定多个IP地址  
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 全新的”embedded string”对象编码方式,从而实现了更少的缓存丢失和性能的提升  
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.0 提高了BITCOUNT、INCR操作的性能  
3.0 调整Redis日志格式  
3.2 新增对GEO(地理位置)功能的支持  
3.2 新增Lua脚本远程debug功能  
3.2 SDS相关的优化,提高redis性能  
3.2 修改Jemalloc相关代码,提高redis内存使用效率  
3.2 提高了主从redis之间关于过期key的一致性  
3.2 支持利用upstart和systemd管理redis进程  
3.2 将list底层数据结构类型修改为quicklist,在内存占用和RDB文件大小方面有极大的提升  
3.2 SPOP命令新增count参数,可控制随机删除元素的个数  
3.2 支持为RDB文件增加辅助字段,比如创建日期,版本号等,新版本可以兼容老版本RDB文件,反之不行  
3.2 通过调整哈希表大小的操作码RDB_OPCODE_RESIZEDB,redis可以更快得读RDB文件  
3.2 新增HSTRLEN命令,返回hash数据类型的value长度  
3.2 提供了一个基于流水线的MIGRATE命令,极大提升了命令执行速度  
3.2 redis-trib.rb中实现将slot进行负载均衡的功能  
3.2 改进了从机迁移的功能  
3.2 改进redis sentine高可用方案,使之可以更方便地监控多个redis主从集群  
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的内存使用、查看整体内存使用细节、申请释放内存、深入查看内存分配器内部状态等功能  
4.0 兼容NAT和Docker  
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 redis-cli 中的集群管理器从 Ruby (redis-trib.rb) 移植到了 C 语言代码。 执行 redis-cli –cluster help 命令以了解更多信息
5.0 新的有序集合(sorted set)命令:ZPOPMIN/MAX 和阻塞变体(blocking variants)  
5.0 升级 Active defragmentation 至 v2 版本  
5.0 增强 HyperLogLog 的实现  
5.0 更好的内存统计报告  
5.0 许多包含子命令的命令现在都有一个 HELP 子命令  
5.0 客户端频繁连接和断开连接时,性能表现更好  
5.0 许多错误修复和其他方面的改进  
5.0 升级 Jemalloc 至 5.1 版本  
5.0 引入 CLIENT UNBLOCK 和 CLIENT ID  
5.0 新增 LOLWUT 命令 http://antirez.com/news/123  
5.0 在不存在需要保持向后兼容性的地方,弃用 “slave” 术语  
5.0 网络层中的差异优化  
5.0 Lua 相关的改进:将 Lua 脚本更好地传播到 replicas / AOF, Lua 脚本现在可以超时并在副本中进入 -BUSY 状态  
5.0 引入动态的 HZ(Dynamic HZ) 以平衡空闲 CPU 使用率和响应性  
5.0 对 Redis 核心代码进行了重构并在许多方面进行了改进  

redis cheatsheet

命令 版本 复杂度 可选 返回值
SET 1.0 O1 2.6.12后增加EX PX NX XX 2.6.12永远回复ok,之后如果有NX XX导致的失败,NULL Bulk Reply
SETNX 1.0 O(1)   1/0
SETEX 2.0 O(1) set +expire 院子 ok
PSETEX 2.6 O(1) s时间单位毫秒 ok
GET 1.0 O(1)   value/nil/特定的错误字符串
GETSET 1.0 O(1)   value/nil/特定的错误字符串
STRLEN 2.2 O(1)   不存在返回0
APPEND 2.0 平摊O(1)   返回长度
SETRANGE 2.2 如果本身字符串短。平摊O(1),否则为O(len(value)) 如果字符串长会阻塞(?不是inplace?) 返回长度
GETRANGE 2.4 O(N)N为返回字符串的长度 2.0以前是SUBSTR 子串
INCR 1.0 O(1)   返回+1之后的值字符串,如果不是数字,会报错(error) ERR value is not an integer or out of range
INCRBY 1.0 O(1) 负数也可以。 返回增量之后的值字符串
INCRBYFLOAT 2.6 O(1)   返回增量之后的值字符串
DECR 1.0 O(1)   返回-1之后的值字符串
DECRBY 1.0 O(1)   返回减法操作之后的值字符串
MSET 1.0.1 O(N) N为键个数 multiple set,原子性  
MSETNX 1.0.1 O(N) N为键个数 都不存在则进行操作,否则都不操作。  
MGET 1.0.0 O(N) N为键个数   如果有不存在的键,返回nil
SETBIT 2.2 O(1) offset 参数必须大于或等于 0 ,小于 2^32 (bit 映射被限制在 512 MB 之内) 该位原来的值
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) ?这个命令的需求?  
HSET 2.0 O(1)   新的值返回1 覆盖了返回0
HSETNX 2.0 O(1)   设置成功返回1,已经存在放弃执行返回0
HGET 2.0 O(1)   不存在返回nil
HEXISTS 2.0 O(1)   字段存在返回1不存在返回0
HDEL 2.0 O(N) N为删除的字段个数 2.4之前只能一个字段一个字段的删除,如果要求原子需要MULTI+EXEC,后续版本支持多字段删除 被成功移除的字段数量 (<=N)
HLEN 2.0 O(1)   返回字段数量,不存在返回0
HSTRLEN 3.2 O(1) 类似STRLEN,操作哈希表的字段 返回字段关联的值的字符串长度
HINCRBY 2.0 O(1) 类似INCRBY,操作哈希表的字段  
HINCRBYFLOAT 2.6 O(1) 类似INCRBYFLOAT  
HMSET 2.0 O(N) N为filed-value数量 类似MSET 能不能用在集群? OK
HMGET 2.0 O(N) 类似MGET 能不能用在集群? 不存在的字段返回nil
HKEYS 2.0 O(N)N为哈希表大小?   返回所有字段的一个表(list or set)不存在返回空表
HVALS 2.0 O(N)   返回所有字段对应的值的表
HGETALL 2.0 O(N)   返回字段和值的表,奇数字段偶数值
HSCAN 2.0 O(N)? 类似SCAN  
LPUSH 1.0 O(1) 2.4以前只接受单个值,之后接受多个值。 返回列表长度
LPUSHX 2.2 O(1) key不存在什么也不做 返回长度,不存在返回0
RPUSH 1.0 O(1) 2.4以前只接受单个值  
RPUSHX 2.2 O(1)    
LPOP 1.0 O(1)   不存在返回nil
RPOP 1.0 O(1)   不存在返回nil
RPOPLPUSH 1.2 O(1) 原子的,原地址右边弹出目的地址左边插入。如果源地址目的地址相同就是旋转列表,可以实现循环列表 返回弹出元素,不存在就是nil
LREM 1.0 O(N) LREM key count value 移除与value相等的count个值,0表示所有,负数从右向左正数从左向右 被移除的数量。
LLEN 1.0 O(1)   返回列表长度
LINDEX 1.0 O(N) N为遍历到index经过的数量 为啥不叫LGET,怕使用者漏了index参数吗?参数和GET系命令不一致做个区分? 返回下表为index的值,不存在返回nil
LINSERT 2.2 O(N) N 为寻找目标值经过的值   返回成功后列表的长度。如果没找到目标值pivot,返回-1,不存在或空列表返回0
LSET 1.0 O(N) N为遍历到index处的元素个数   OK
LRANGE 1.0 O(S+N) S为start偏移量,N为区间stop-start LRANGE的区间是全闭区间,包含最后一个元素,注意stop取值范围。不过超出下表范围不会引起命令错误。可以使用负数下表,和python用法一致 返回一个列表
LTRIM 1.0 O(N) N为被移除元素的数量 和LRANGE类似,要注意下标 OK
BLPOP 2.0 O(1) LPOP阻塞版本,timeout0表示不超时 阻塞命令和事务组合没有意义,因为会阻塞整个服务器,其他客户端无法PUSH,会退化成LPOP  
BRPOP 2.0 O(1)    
BRPOPLPUSH 2.2 O(1) 实现安全队列  
SADD 1.0 O(N) N是元素个数 2.4版本之前只能一个一个添加 返回总数
SISMEMBER 1.0 O(1)   1/0
SPOP 1.0 O(1) 随机返回一个值并移除, 值或nil
SRANDMEMBER 1.0 O(N) N为返回值的个数 2.6开始支持count 类似SPOP但是不移除 值 nil/数组
SREM 1.0 O(N) N为移除的元素个数 2.4以前只支持单个移除 移除成功的数量
SMOVE 1.0 O(1) 原子的,移除src添加dst 移除成功1,其他0或者错误
SCARD 1.0 O(1) cardinalty基数 不存在返回0
SMEMBERS 1.0 O(N) N为集合基数   不存在返回空集合
SSCAN     SCAN  
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)    
ZADD 1.2 O(M*logN) N 是基数M是添加新成员个数 2.4之前只能一个一个加 成员数量
ZSCORE 1.2 O(1) HGET和LINDEX api风格  
ZINCRBY 1.2 O(logN) HINCRBY  
ZCARD 1.2 O(1) SCARD  
ZCOUNT 2.0 O(logN) ZRANGEBYSCORE拼的 返回score在所给范围内的个数
ZRANGE 1.2 O(logN+M) M结果集基数 N有序集基数 从小到大排序  
ZREVRANGE 1.2 O(logN+M) M结果集基数 N有序集基数 和上面相反  
ZRANGEBYSCORE 1.05 O(logN+M) M结果集基数 N有序集基数 按照score过滤  
ZREVRANGEBYSCORE 1.05 O(logN+M) M结果集基数 N有序集基数    
ZRANK 2.0 O(logN)   返回排名,从0开始,从小到大,0最小
ZREVRANK 2.0 O(logN)   从大到小 0最大
ZREM 1.2 O(logN*M) N基数M被移除的个数 2.4之前只能删除一个 个数
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为元素个数 类似ZCOUNT,前提是分值相同,不然没意义 数量
ZREMRANGEBYLEX 2.8.9 O(logN+M) N基数 M被移除数量 分值相同 被移除数量
ZSCAN     SCAN  
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))    
PFADD 2.8.9 O(1)   变化返回1不变0
PFCOUNT 2.8.9 O(1),多个keyO(N)   个数
PFMERGE 2.8.9 O(N)   OK
GEOADD 3.2 O(logN)    
GEOPOS 3.2 O(logN) GET  
GEODIST 3.2 O(logN)   返回距离,节点不存在就返回nil
GEORADIUS 3.2 O(N+logM)N元素个数M被返回的个数 范围内所有元素  
GEORADIUSBYMEMBER 3.2 O(N+logM)    
GEOHASH 3.2 O(logN)   字段的hash
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)    
EXISTS 1.0 O(1)    
TYPE 1.0 O(1)   类型
RENAME 1.0 O(1) rocksdb不能in-place改key,seek出来,复制value,写旧key删除,写新key。pika为什么不支持? OK
RENAMENX 1.0 O(1) 新key不存在才改 1成功
MOVE 1.0 O(1) redis支持多库,redis多库数据导入pika? 1成功0失败
DEL 1.0 O(N) N为key个数    
RANDOMKEY 1.0 O(1) 怎么实现的O1?pika O(N)  
DBSIZE 1.0 O(1)   所有key数量
KEYS 1.0 O(N) N大会阻塞redis  
SCAN 2.8 O(1)迭代,O(N)完整迭代 keys替代品  
SORT 1.0 O(N+MlogM) 返回结果不是in-place,可以STORE保存  
FLUSHDB 1.0 O(1) drop db OK
FLUSHALL 1.0 O(1) drop all db OK
SELECT 1.0 O(1) 切换数据库 OK
SWAPDB 4.0 O(1) 交换数据库 OK
EXPIRE 1.0 O(1) 单位秒 随机的过期时间防止雪崩 1成功0失败
EXPIREAT 1.2 O(1) 时间戳  
TTL 1.0 O(1)   不存在返回-2过期返回-1其余返回时间
PERSIST 2.2 O(1) 去除失效时间 1成功
PEXPIRE 2.6 O(1) 毫秒为单位 1成功
PEXPIREAT 2.6 O(1) 同expireat  
PTTL 2.6 O(1) 同ttl,2.8以前 失败或不存在都返,回-1后来用-2区分不存在  
MULTI 1.2 O(1) 事务块开始 OK
EXEC 1.2 事务块内执行的命令复杂度和 执行事务块 被打断返回nil
DISCARD 2.2 O(1) 放弃事务块内命令 OK
WATCH 2.2 O(1) 乐观锁 OK
UNWATCH 2.2 O(1) 当EXEC DISCARD没执行的时候取消WATCH OK
EVAL 2.6 O(1) 找到脚本。其余复杂度取决于脚本本身 推荐纯函数,有全局变量会报错  
EVALSHA 2.6 根据脚本的复杂度而定 缓存过的脚本。可能不存在  
SCRIPT LOAD 2.6 O(N) N为脚本长度 添加到脚本缓存  
SCRIPT EXISTS 2.6 O(N) N为判断的sha个数   0 1
SCRIPT FLUSH 2.6 O(N) N为缓存中脚本个数   OK
SCRIPT KILL 2.6 O(1) 如果脚本中没有写,能杀掉,否则无效,只能shutdown nosave  
SAVE 1.0 O(N) N为key个数 保存当前快照到rdb,阻塞,保存数据的最后手段 OK
BGSAVE 1.0 O(N) fork执行复制。,lastsave查看bgsave执行成功 反馈信息
BGREWRITEAOF 1.0 O(N) N为追加到AOF文件中的数据数量 AOF redis自己会重写,该命令是手动重写 反馈信息
LASTSAVE 1.0 O(1)   时间戳
PUBLISH 2.0 O(M+N) channel订阅者数量+模式订阅客户端数量   收到消息的个数
SUBSCRIBE 2.0 O(N) N是channel个数    
PSUBSCRIBE 2.0 O(N) N是模式的个数 通配符模式。满足该通配符字符串的channel  
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) SLAVEOF NO ONE升主 OK
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) SAVE QUIT有可能丢失数据 该命令屏蔽了后续的写动作?  
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)   pong
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) 调试命令,查对象信息,模拟segfault  
MIGRATE 2.6 O(N) dump key + restore  
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    
Strings List Set Sorted Set hash stream
APPEND          
BITCOUNT          
BITFIELD          
BITOP          
BITPOS          
DECR          
DECRBY          
GET          
GETBIT          
GETRANGE          
GETSET          
INCR          
INCRBY          
INCRBYFLOAT          
MGET          
MSET          
MSETNX          
PSETEX          
SET          
SETBIT          
SETEX          
SETNX          
SETRANGE          
STRLEN          
Read More

2018年度总结

昨天玩了一天荒野大镖客2,忘记写了。。今天补上 我收藏的待整理的文件和资料都没有处理。。搞不好得拖延到2019年年底。。新年计划就是整理完收藏夹,读更多的书看更多好看的电影电视剧了。

今年最大的改变就是从东北来到深圳。上半年就想走,一直拖延到十月末。总算过来了。在哈尔滨的日子十分悠闲,感觉提前退休了一样。没啥机遇和挑战,也学不到什么真正的东西。都是自己在研究,不能落地的知识那不是知识,何况我在那边就是在闭门造车。

新工作强度也挺大,变化挺快,我感觉自己都有点跟不上了。刚开始第一个月闲得很,然后这个月忙的很,每天东忙西芒东西都不能落地,都是研究。这种工作看起来爽实际上很有危机感,没有输出很容易被干掉。目前没转正应该不会这么快辞退我。抓点紧赶紧输出

还是挺舍不得哈尔滨那边的工作环境的。十分舒适。同事们也都就很友善,即使我看上去很不合群大家也能和我唠起来。舒适。跳出舒适区太难了。我费了大半年才跳出来。

2018年基本上都在做无用功,找工作等等,时间碎片化。看的书电影数量一年不如一年。不过还能检出一些值得推荐的

今年都看了啥电影 剧集

今年我看完了老友记十季和老爸老妈浪漫史九季,我之前以为比较难看完,2018年终于搞定了。

《老友记》比较无害,黄段子也很少,这也是老少咸宜的原因吧。我是不太喜欢。第四季第五季比较好看,第七季往后就可以不看了,太烂。

《老爸老妈浪漫史》比较推荐。太好看了。我反反复复看了两遍,下了网易云听力听了两遍。现在基本上梗如数家珍。好看。这应该是未来这些年情景喜剧的巅峰了。桥段设计和故事叙述模式都十分先进。(而且被爱情公寓抄了个遍)只要不看第九季最后两集,或者最后五分钟,这个剧还是很感人的。

《我是大哥大》也挺好看。浮夸搞笑型的。里面的理子太美了。

《马男波杰克 第五季》,这季十分真实,故事剪辑模式十分先锋。看的爽的不行。bojack又搞砸了,我也是。

《硅谷 第五季》 创业团队总算成功了,Gilfoyle也有对象了。我还没有。

《进击的巨人 第三季》这季总算知道点之前买下的伏笔。不过动画片节奏太慢了。不太推荐。

《非自然死亡》 全方位展示为什么石原里美这么好看的一部日剧。确实好看。

《扯蛋英国史》 一本正经的恶搞。被采访的教授忍者不打记者

《四畳半神话大系》换着法讲一个故事。风格型

《荒川爆笑团》也算有趣。扯淡的

电影

《大佛普拉斯》 他人是深渊。这个电影是2018年最值得看的国产片。很会玩。

《三块广告牌》 杰作。突然的真实。谁都没错,但就是不对。冲突让人难过。

《斯大林之死》 十分讽刺,想看讽刺的这个不要错过

《文科恋曲》 老爸老妈浪漫史里的Ted自导自演的一部作品。讲不要逃避年龄,正确的时间做正确的事。装嫩逃避是不对的。个人比较喜欢

脱口秀以及其他

这些都能在bilibili找到

瑞奇·热维斯:人性 https://movie.douban.com/subject/30156995/
Ricky的段子十分冒犯。比路易CK冒犯多了。这个视频讲的主要就是喜剧中嘲讽元素是为了表达什么,不要因为圣母玻璃心感觉被冒犯了就蹦出来抬杠。首先事实摆对,然后就是角度问题,有人通过喜剧嘲讽,有人简单陈述,但你不能不接受把脑袋埋起来。

克里斯·洛克:塔姆柏林https://movie.douban.com/subject/30148257/
讲的自身以及他人的自我认知。不要太把自己当回事儿

还看了腾讯吐槽大会。可以看看笑果团队讲的(就是那些叫不出名的人),明星基本背稿表达可能不太行。

还看了PewDiePie视频。真是快乐源泉,如果感到烦恼,点开PewDiePie视频,随便傻乐一下

今年都看了啥书

当当优惠和京东优惠可坑苦了我。买的书基本都没有看完

看的都是网上找的电子版。惭愧惭愧。有的实在是买不到

甲骨文 流離時空裡的新生中國
这本书也是我自己审视自己来读的书,这本书真挚。亲切,看一个美国人怎么亲切的描述中国的。这本书能更好的了解这个国家。

花街往事
波澜壮阔的故事。平凡但让人感动

冬泳
这本书是微博上的坦克手贝吉塔写的。东北人。书写的沉稳,锋利。建议东北人人手一本。

鱼翅与花椒
这本书总结起来就是一句话,看饿了。这书有一种让我快速搬到成都的冲动。英国人写的。同时吐槽她们自家的土豆料理。真的亲切。我挺佩服这些老外的。怎么写的呢

剩下的书都是些计算机方面的书,《高性能MySQL》,读完不会MySQL也能胡诌几句忽悠技术面试官,《深入C++对象模型》深入C++细节的入门书,年轻人别学C++,太让人头大。

和同事/朋友吃饭。没有聊关于未来的话题。没有啥未来。走一步算一步健康活着就好啦。哪里想那么多啊。 2016这句话原封不动抄过来 现在明白是做的太少而想的太多。新的一年没有任何宏大蓝图,只求能够独立生活不耽误别人。 外加多和同学朋友唠唠嗑。多对社会做点贡献。 外加保持健康


Read More

MongoDB权威指南笔记

##< MongoDB权威指南>笔记

MongoDB基础知识

  • 文档(行)-》 集合(动态模式的表,集合可以有子集合(GridFS))-》数据库

  • 每个文档有个特殊的键_id (唯一生成方式,时间戳+机器ID+PID+计数器)

  • 命名
  • 集合system保留,注意有些保留字没有强制限定,比如version,就只能用getCollection来访问了,或者跳过直接访问,使用数组迭代语法
  • 数据库,admin local config 保留

  • JavaScriptShell操作 use db CRUD

基本数据结构 ALL IN JSON

  • null 二进制以及代码

  • bool 数值,默认64位浮点型,可以用NumberInt类NumberLong类

  • 字符串,日期(new Date() 直接调用Date构造函数得到的是个字符串,所以这里用new,标识Date 对象,而不是对象生成的字符串)
  • 正则 /foobar/i js正则语法(需要学一下js)

  • 数组,数组可以包含不同类型的元素,数组内容可以建索引,查询以及更新
  • 内嵌文档(嵌套Json也可以建索引,查询以及更新
  • 对象ID
  • _id默认是ObjectID对象,每个文档都有唯一的 _id ObjectId全局唯一的原理
  • 自动生成id 通常能交给客户端驱动程序搞定就不放在服务器,扩展应用层比扩展数据库层要方便

JS Shell

  • .mongorc.js文件,放在bin下自动运行,可以覆盖危险Shell辅助函数,取消定义,也可以启动Shell时 –norc 取消加载
  • Windows用户,该文件会默认生成在用户 用户名或administrator目录下

创建,更新和删除文档

插入文档
  • insert, batchinsert 批量插入,多个文档插入到同一个集合中,多个文档插入多个集合不行。

  • 导入原始数据使用mongoimport (需要学一下go)
  • 插入最大接受48M插入,多于48M会分片插入,如果插入途中有一个文档插入失败,前面的灰尘共,后面的都失败,不是强事务的。
  • 插入校验,添加_id字段,检查大小(目前是小于16M)(可能是不良的设计)
删除
  • db.foo.remove() 删除集合的所有文档删除数据是永久的,不能撤销,也不能恢复
  • drop删除整个表(和数据库一样,快,没有限定条件)
更新文档
  • 文档替换,直接更改文档的字段,扩展,重命名,建立子文档等 可以用=,直接用delete删除字段(只能用替换,不能直接删数据库,用findOne,find返回值是个cursor

  • 文档更新相同字段导致的更新错误(不唯一,有可能给id在前的更新了),索引问题,用id来查找

  • 修改器,一个Json封起来。key是动作,value是个修改的Json,其中,内部key对应修改的字段,value对应修改的值

  • $set == replace or insert, $unset 直接就可以删掉这个键 可以修改内嵌文档

  • $inc == add or insert

  • 数组修改器

  • $push 添加元素 添加一个数组元素,$push 和$each结合使用,添加多个数组
  • 数组作为数据集,保证数据不重复 $ne一个限定集,或使用$addToSet($addToSet和$each可以结合使用)

  • 删除元素 {“$pop”:{“key”:-1}}基于位置,如果基于条件,使用$pull
  • 基于位置的数组修改器 直接使用定位符$来匹配,匹配第一个 db.blog.update({“cmt.author”:”John”},{“$set”:{“cmt.$.author”:”Jim”}})

  • 修改器速度, 主要取决于文档大小是否变化,如果有变化,就会影响速度,因为插入是相邻的,如果一个文档变大,位置就放不下,就会有移动
  • paddingFactor,填充因子,MongoDB位每个新文档预留的增长空间,(db.coll.stats(),Windows上我运行这个没找到paddingFactor项,比较尴尬)

  • upsert update + insert Update第三个参数true 原子性
  • $setOnInsert 考虑寻找并修改同一个字段,多次运行可能会生成多个文档,setOnInsert会找到之前生成的文档,不会多次生成。
  • save shell (db.foo.save(x))在写到这行之前我都是先findOne查回来,改,在Update回去。。。

  • 更新多个文档 db.user.update({“birthday”:”10/13/1978”},{““$set”:{“gift”:”Happy Birthday”}},faset,true)
  • db.runCommand({getLaseError:1}) 查看更新文档数量

  • 返回被更新的文档 findAndModify ?有点没理解
写入安全

查询

  • find 默认匹配全部文档{},也可以写多个条件(and关系)
  • 指定返回,第二个参数指定{“key”:1/0},类似select指定,其中1是选中,0是排除
  • 限制 ?有点没理解

  • 查询条件 “$lt” “$lte” “$gt” “$gte” “$ne”
  • OR “$in” “$or “ “$not” “$nin”
  • 条件语义
特定类型的查询
  • null
  • 正则表达式
  • 查询数组 与查询普通文档是一样的,直接find value能匹配到数组中的元素,就会被选出来
  • $all 特定元素的并列条件,顺序无关紧要,普通find子数组的形式不行,会有顺序限制,如果想找特定位置的元素,需要使用key.index (上面的 key.$就是个迭代形式。)
  • $size 指定长度的数组
  • $slice 返回一个子集 截取一段
  • 数组和范围查询的相互作用,因为查找数组会匹配所有元素,有满足的就会被选出来,所以范围查询可能会和设想的结果不一样
  • 针对数组,使用 $elemMatch 效率稍低
  • 如果该列有索引,直接用min max

  • 查询内嵌文档 用.访问 (URL作为键的弊端)
  • $where 结合函数 会很慢,用不了索引,还有文档转换为js(配合where函数)的开销
  • 注意可能引入的注入攻击

  • 游标 find

  • while(cursor.hasNext()){obj=cursor.next(),do….}
  • cursor.forEach(function(x){…})
  • limit skip sort 类似SQL中的limit sort
  • skip不要过滤大量结果,会慢
  • 不要用skip对结果分页
  • 注意比较顺序
  • 随机选取文档,不要算出所有然后在找随机数,太坑了。可以每个文档加一个随机数key,在key上建索引,然后用随机随机数过滤就ok
  • 也有可能找不到结果,那就反向找一下,还没有那就是空的

  • 高级查询选项
  • 获取一致结果:迭代器修改文档可能引入的问题,修改的文档大小发生变化,结果只能放在文档最后,导致同一个文档返回多次(太坑了)
  • 解决方案,snapshot,会使查询变慢,只在必要的时候使用快照,mongodump

  • 游标的生命周期 默认十分钟自动销毁,有immortal选项

  • 数据库命令 runCommand 实际上内置命令是db.$cmd.findOne({“”})的语法糖

设计应用

索引

  • 和关系型数据库类似,也有explain
  • 复合索引 db.coll.ensureIndex({“key1:1”,”key2:!”})
  • 覆盖索引与隐式索引
  • $如何使用索引
  • 低效率的操作符,基本都不能用索引
  • 范围 要考虑与索引结合导致的效率低下问题
  • $or使用索引

  • 索引对象和数组
  • 可以嵌套对象的任意层级来做索引 a.b.c.d.e.f ,得用最后级别的对象来搜索才能用上这个索引,只用其中一个字段是不行的。
  • 索引数组 实际上是对每一个数组元素都建立了索引,代价很大,如果有更新操作,那就完了,不过这里可以对字段来查找,毕竟每个字段都有索引。但是索引不包含位置信息。没法针对特定位置来找。
  • 限定只有一个数组可以索引。为了避免多键索引中索引条目爆炸

  • 多键索引 多键索引可能会有多个索引条目指向同一个文档 ?为什么 会很慢,mongo要先去重
索引基数
  • 基数 衡量复杂度,某个字段拥有不同值的数量,基数越高索引越有效,索引能将搜索范围缩小到一个小的结果集

  • 查询优化器

何时不应该使用索引
  • 索引扫描和全盘扫描在集合与返回结果上进行比较

索引类型

  • 唯一索引 和主键概念一致(虽然已经有_id 这个主键了,新增的唯一索引是可以删除的)db.coll.ensureindex({“ukey”:1},{“unique”:true})

  • 复合唯一索引。一例 GridFS files_id:ObjectId, n:1 所有索引键的组合必须唯一

  • 去处重复,建唯一索引失败 ,加个条件ensureindex({“ukey”:1},{“unique”:true,”dropDups”:true}) ( 慎用)

  • 稀疏索引 和关系型数据库中的索引不同?
  • 有些文档可能没有索引这个字段,建立稀疏索引就把这种过滤掉了。

  • 索引管理 system.indexes db.coll.getIndexes()
  • 标识索引 keyname1_dir1_keyname2_dir2….keynameN_dirN 名字,方向
  • 修改索引 db.coll.dropIndex(“indexname”)

特殊的索引和集合

固定集合capped collection
  • 事先创建好,大小固定,类似循环队列 在机械硬盘写入速度很快 顺序写入,没有随机访问。如果拥有专属磁盘,没有其他集合的写开销,更快

  • db.createCollection(“my_coll”,{“capped”:true,”size”:100000,”max”:100}); 也可以用普通的集合转成固定集合 db.runCommand(“convertToCapped”:”test”,”size”:100000),无法将固定集合改成非固定集合,只能删掉
  • 自然排序
  • 循环游标 tailable cursor 判断cursor是否tailable 一直循环处理,直到cursor死了(灵感来自tail命令)

  • 没有_id索引的集合,在插入上带来速度提上,但是不符合mongod复制策略
TTL索引
  • 创建索引有个expireAfterSecs参数 db.foo.ensureIndex({}”lastUpdated”:1},{“expireAfterSecs”:60*60*24})
全文本索引
  • 自然语言处理?

  • 优化的全文本索引

  • 在其他语言中搜索,设定语言

地理空间索引2dsphere

  • 复合地理空间索引
  • 2D索引
使用GridFS存储文件
  • 使用场景,不经常改变的需要连续访问的大文件,缺点性能低,文件是多个文档,无法同一时间对所有文档加锁
  • mongofiles put foo.txt get foo.txt

聚合

聚合框架
  • 管道pipeline 投射project 过滤filter 分组group 排序sort限制limit跳过skip,都在内存中进行,顺序不影响结果(但可能影响性能)
db.articles.aggregate({"$project"{"author"1}}

{"$group":{"_id":"author","count":{"$sum":1}}},

{"$sort":{"$count":-1}},

{"$limit":5})

$match 最开始用可以用上索引减小结果集

$project 修改字段前先用上索引

  • 管道表达式 数学表达式 $add $ subtract $multiply $divide $mod 都是接一个数组对象的

  • 日期表达式 $year $month $week $dayOfMonth $dayOfWeek $dayOfYear $hour $minute $second

  • 字符串表达式 $substr $concat $toLower:expr $toUpper:expr

  • 逻辑表达式

  • $cmp $strcasecmp $eq ne ge get lt lte and or not

  • $cond :[boolexpr,trueexpr, falseexpr] $ifNull:[expr,replacementexpr]

$group 分组 SQL - groupby

  • 算术操作符 $sum $average:value
  • 极值操作符 $max:expr $min:expr $first:expr $last:expr
  • 数组操作符 $addToSet $push
  • 分组行为不能用于流式工作,有个归结的过程,是个整体
  • $unwind 拆分成独立的文档
  • $sort limit skip
  • 使用管道开始阶段前尽可能把多于的文档和字段过滤掉,可以排序来方便使用索引 $match
mapreduce?
聚合命令 和上面的不一样,但是感觉差不多。
  • db.col.count()
  • runCommand({“distinct”:”key1”,”…”}
  • runCommand({“group”:} SQL group by

应用程序设计

注意一致性,以及mongodb不支持事务

复制

创建副本集 replica set

副本集概念 大多数,半数以上,低于半数,全部降备

选举机制

选举仲裁者 最多一个 arbiter 只参与选举,放在外部观察的故障域中,与其他成员分开,尽量不要用,虽然轻量,极端场景 1主1备1仲裁,主挂掉,备升主,新主机还要承担起复制备机的责任

尽量使用奇数个数据成员

优先级与被动成员

隐藏成员?为啥有这个设定

延迟备份节点,主节点意外被毁被删库还能活过来 不支持回滚只好这么搞

备份节点可以手动设定不创建索引(得是被动成员,优先级为0

副本集的组合

同步 操作日志oplog,主节点local数据库的一个固定集合,备份节点查这个集合来进行复制,每个节点维护自己的oplog,每个成员都可以作为同步源提供给其他成员使用

  • oplog同一条记录执行一次和执行多次效果一致

初始化同步 会先将现有的数据删除 克隆 通过oplog复制数据,然后复制oplog 然后建索引

  • 如果当前节点远远落后于同步源,oplog通不过城最后一步就是将创建索引期间的所有操作全同步过来,防止该成员成为备份节点
  • 克隆可能损坏同步元的工作集?
  • 克隆或创建索引耗时过长,导致新成员与同步源oplog脱节 初始化同步无法正常进行

处理陈旧数据 stale 会从成员中的oplog(足够详尽)中同步恢复,如果都没有参考价值,这个成员的复制操作就会停止,这个成员需要重新进行完全同步

  • 心跳 让主节点知道自己是否满足集合的大多数条件
  • 成员状态 主节点备份节点
  • STARTUP成员刚启动 ->STARTUP2初始化同步 ->RECOVERING 成员运转正常,但暂时还不能处理读取请求 比如成为备份节点前的准备活动,在处理非常耗时的操作(压缩,replSetMaintenance),成员脱节
  • ARBITER 仲裁者
  • DOWN变的不可达 UNKNOWN 无法到达其他成员 分别描述对端和自己
  • REMOVED移除工作集
  • ROLLBACK 数据回滚
  • FATAL发生了不可挽回的错误 grep replSet FATAL 通常只能重启服务器

  • 选举 选举通常很快,太多错误发生比较倒霉的话可能花费较长
  • 回滚
  • 一个场景,假如主节点op写写死了,然后选举新的主节点没有这条记录,则原主节点需要回滚
  • 如果回滚失败,备分节点差的太多,升主回滚内容太多,mongodb受不了。

从应用程序连接副本集

主节点崩溃的错误?最后一次操作成功失败?留给应用程序去查询
  • db.runCommand({“getLastError”:1,”w”:majority})检查是否写入操作被同步到了副本集的大多数 阻塞,可以设置超时时间,阻塞不一定失败,超时不一定失败

  • 使用majority选项可以避免一场写入失败导致的回滚丢失。一直阻塞直到大家都有这条数据,或失败。
  • “w”也可以设定其他值,值包含主节点。应该设置成n+1 默认1
自定义复制保证规则
  • 保证复制到每个数据中心的一台服务器上
  • 重写rs.config,rs.reconfig(config) 添加字段
  • 隐藏节点到底是干啥的?
将读请求发送到备份节点
  • 对一致性要求不是特别的高
  • 分布式负载(另一个选择,分片分布式负载)
  • 何时可以从备份节点读取数据?
  • 失去主节点时,应用程序进入只读状态-》主节点优先

管理

维护独立的成员
  • 单机模式启动成员 改端口,重启时不使用replSet选项,然后访问维护,其他成员会连接失败,认为它挂了,维护结束后重新以原有的参数启动,自动连接原来的副本集,进行同步
副本集设置

local.system.replSet

var config = {"_id":setname,

"members":["_id":0,"host":host1,

"_id":1,"host":host2,

"_id":2,"host":host3

]}

re.initiate(config)

修改副本集成员rs.add remove reconfig

修改成员状态 re.stepDown降备rs.freeze (time)在time时间内阻止升主

使用维护模式 RECOVERING replSetManitenanceMode

监控复制

获取状态 replSetGetStatus

复制图谱,指定复制syncFrom

  • 复制循环
  • 禁用复制链,强制从主节点复制

计算延迟 lag

调整oplog大小 维护工作的时间窗,超过改时间就不得不重新进行完全同步

  • 在被填满之前,没有办法确认大小,即使他是个capped collection,也不可以运行时调整大小,因为他是个capped collection

  • 修改步骤,如果是主节点,先退位,关闭服务器,单机模式启动,临时将oplog中最后一条insert操作保存到其他集合中 确认保存成功 删除当前oplog,创建一个新的oplog,将最后一条操作记录写回oplog确保写入成功

var cursor = db.oplog.rs.find({"op":i})
var lastInser = cusor.sort({"$narutal":-1}).limit(1).next()
db.tempLastOp.save(lastInsert)
db.tempLastOp.findOne()
db.oplog.rs.drop()
db.createCollection("oplog.rs",{"capped":1,"size":10000})
var temp = db.tempLastOp.findOne()
db.oplog.rs.insert(temp)
db.oplog.rs.findOne()
从延迟备份节点中恢复
  • 简单粗暴方法,关闭所有其他成员,删掉其他成员的数据,重启所有成员,数据量大可能会过载
  • 稍作修改,关闭所有成员,删掉其他成员的数据,把延迟备份节点数据文件复制到其他数据目录,重启所有成员,所有服务器都与延迟被分界点拥有同样大小的oplog
创建索引

创建索引消耗巨大,成员不可用,避免最糟糕的情况,所有成员节点同时创建索引

  • 可以每次只在一个成员创建索引,分别备份节点单机启动创建索引然后重新启动,然后主节点创建索引
  • 可以直接创建,选择负载较少的空闲期间
  • 修改读取首选项,在创建节点期间把读分散在备份节点上
  • 主节点创建索引后,备份节点仍然会复制这个操作,但是由于备份节点中已经有同样的索引,实际上不会再次创建索引

  • 让主节点退化为备份节点,执行上面的步骤,这时就会发生故障转移
  • 可以使用这个技术为某个备份节点创建与其他成员不同的索引,在离线数据处理时非常有用?
  • 但是如果某个备份节点的索引与其他成员不同,它永远不会成为主节点,应该将它的优先级设为0
  • 如果要创建唯一的索引,需要确保主节点中没有被插入重复的数据,或者首先为主节点穿件唯一索引,否则主节点重复数据,备份节点复制出错,备份节点下线,你不得不单机启动,删除索引,重新加入副本集
在预算有限的情况下进行复制

没有多台高性能服务器,考虑将备份节点只用于灾难恢复

  • “priority”:0 优先级0, 永远不会成为主节点
  • “hidden”:1 隐藏,客户端无法将读请求发送给他
  • “buildIndexes”:0 optional 备份节点创建索引的话开极大地降低备份节点的性能。如果不在备份节点上创建索引,从备份节点上恢复数据需要重新创建索引
  • “votes”:0 只有两台服务器的情况下,备份节点挂掉后,主节点仍然一直会是主节点,不会因为达不到大多数要求而推诿,如果还有第三台服务器,设为仲裁者
主节点如何跟踪延迟

local.me local.slaves

##### 主从模式 传统模式,会被废弃

从主从模式切换到副本集模式 

  • 停止所有系统的写操作哦,关闭所有mongod 使用–replSet选项重启主节点不再使用–master 初始化这个只有一个成员的副本集,这个成员会成为副本集中的主节点,使用–replSet和–fastsync启动从节点,使用rs.add将之前的从节点添加到副本集,然后去掉fastsync

让副本集模仿主从模式行为

  • 除了主节点,都是优先级0投票0 备份节点挂了无所谓,主节点不会退位,主节点下线手动选父节点,指定优先级和投票,运行rs.reconfig(config,{“force”,1})

分片

配置分片 数据分片
拆分块
  • 有服务器不可达,尝试拆分,拆分失败 拆分风暴
  • mongos频繁重启,,重新记录点,永远达不到阈值点
均衡器 config.locks

均衡阈值

选择片键

如何在多个可用的片键中作出选择?不同使用场景中的片键选择?哪些键不能作为片键?自定义数据分发的可选策略?如何手动对数据分片

分片的目的
  • 减少读写延迟?将请求分布在近的机器或高性能服务器
  • 增大读写吞吐量?集群1000次/20 ms 可能需要5000次/20ms,需要提高并行,保证请求均匀分布在各个集群成员上
  • 增加系统资源?每GB数据提供Mongodb更多的可用RAM,使工作集尽量小
数据分发
  • 升序片键 date ObjectId

  • 随机分发片键
  • 基于位置片键 tag
片键策略
  • 散列片键 数据加载速度
  • 缺点 无法范围查询
  • 无法使用unique和数组字段
  • 浮点型的值会被先取整,然后算hash

  • GridFStab的散列片键
  • 流水策略,一个SSD接着写,指定tag,更新tag范围,把片转到其他磁盘上
  • 多热点?
片键规则和指导方针
  • 不能是数组
  • 片键的势要高一些,或者组合起来

##### 控制数据分发

  • addShardTag第二个参数可以指定高低,然后tagrange将不同集合放到不同的分片上。

  • 手动分片 moveChunk

分片管理

检查集群状态 sh.status() use config config.shards config.chunks config.collections cofig.changelog

应用管理

了解应用的动态

mongotop

mongostat

  • insert/query/update/delete/getmore/command

  • fulshed mongod将数据刷新到磁盘的次数
  • mapped所映射的内存大小,约等于数据目录大小
  • vsize 虚拟内存大小,通常为数据目录二倍
  • res正在使用的内存大小
  • locked db锁定时间最长的数据库
  • qrw 阻塞的读写队列大小
  • arw正在读写的客户端数量
  • netin 网络传输进来的字节数
  • netOut网络传输输出的字节数
  • conn打开的连接数

数据管理

身份认证

建立和删除索引

OOM Killer 日志在/var/log/messages

数据预热

for file in /data/db/* do

dd if=$file of=/dev/null

done
  • fine-grained 细粒度的预热

  • 将集合移动至内存

db.runCommand({"touch":"logs","data":1,"index":1})
  • 加载特定的索引 覆盖查询
  • 加载最新的文档
  • 加载最近创建的文档 结合ObjectId
  • 重放应用使用记录 诊断日志?
压缩数据
  • 会进入RECOVERING模式

  • 运行repair来回收磁盘空间,需要有当前数据大小同样的空闲空间,或指定外部的磁盘目录,修复路径。

移动集合
  • renameCollection 改集合名。不用担心性能消耗
  • 数据库间移动,只能dump&restore也可以用cloneCollection

预分配数据文件

if test $# -lt 2 || test $# -gt 3 then
echo "$0 <db> <number-of-files> "
fi

db =$1
num=$2
for i in {0..$num}
do
echo "preallocation %db.$i"
head -c 2146435072 /dev/zero >$db.$i
done

持久性

日记系统

批量提交写入操作 100ms 或写入数据MB以上

设定提交时间间隔

关闭日记系统 数据可能损坏,两种替代方案

  • 替换数据文件,用副本集的,或者初始化同步,重启同步
  • 修复数据文件,mongod内置(repair)或mongodump 耗时较长

关于mongod.lock文件,别手动删。。恢复数据再搞。也有可能是隐蔽的异常退出

MongoDB无法保证的事项

  • 检验数据损坏
  • 副本集的持久性

服务器管理

停止和启动MongoDB

监控MongoDB

  • 内存使用情况
  • 跟踪监测缺页中断
  • 索引树的脱靶次数
  • IO延迟
  • 后台刷新平均时间,将脏页写入磁盘所花费的时间
  • 跟踪监测性能状况
  • 空余空间
  • 监控副本集
  • oplog长度

备份

  • 文件系统快照,开启日记系统,文件系统本身支持快照
  • 复制数据文件,先db.fsyncLock() 然后cp就完了
  • 不要同时使用fsyncLock和mongodump 会死锁

部署

Read More

MongoDB中的装饰器模式

实现在util/Decorable.h中 本质是CRTP的一个使用

子类继承Decorable 就可以了。能保证每个实例在不同的装饰器实例上有不同的表现,完全正交。

装饰的原理:

先定义装饰器实例DecorableInstance,“被装饰的类”

通过DecorableInstance::declareDecoration调用获得若干“装饰”实例

用DecorableInstance的生成若干实例,每个Decoration在DecorableInstance的表现都是分开的。

每个组件都是个加强版的单例,可以直接通过declareDecoration()实例来访问,对应每个被装饰的类都不一样

简单示例在util/decorable_test.cpp中

具体在MongoDB中

比如ServiceContext 本身应用了这个装饰

class ServiceContext : public Decorable<ServiceContext>// service_context.h

通过ServiceContext::declareDecoration来为ServiceContext添加“装饰”组件,简单grep

...
mongo/src/mongo/util/net/listen.cpp:const auto getListener = ServiceContext::declareDecoration<Listener*>();

直接通过getListener(ser)来访问当前service_context的Listener组件的相关信息。

类似还有许多类是这么实现的

mongo/src/mongo/db/client.h:class Client: public Decorable<Client> {
mongo/src/mongo/db/operation_context.h:class OperationContext : public Decorable<OperationContext> {
mongo/src/mongo/db/service_context.h:class ServiceContext : public Decorable<ServiceContext> {
mongo/src/mongo/transport/session.h:class Session : public std::enable_shared_from_this<Session>, public Decorable<Session> {

在MongoDB中的 类图

img

实现原理

Decorable有DecorationRegistry和DecorationContainer成员

其中

  • DecorationRegistry内部持有DecorationInfoVector和totalsize,将数组偏移信息用DecorationDescriptorWithType 抛出来
  • DecorationContainer内部持有DecorationRegistry(Decorable的)和一个字符数组,间接通过DecorationContainer来访问DecorationRegistry
  • DecorationContainer构造函数会构造DecorationRegistry中的DecorationInfo对象
  • 字符数组就当一块内存使用,连续排放各种装饰对象实例T,placement new
  • 每个DecorationInfo会记录自己的index,index是std::alignment_of::value算出来的。

  • DecorationDescriptorWithType是DecorationDescriptor的封装,DecorationDescriptor内部就记录一个index,通过调用一层一层的把index抛出来

调用declareDecoration会间接调用DecorationRegistry->declareDecoration,底层调用栈是DecorationContainer构造 ->返回DecorationDescriptor -> 返回DecorationDescriptorWithType ->返回到Decoration

当使用declareDecoration生成的实例的时候,实际上调用的是T& Decoration::operator(),

该函数会调用把Decorable内部的DecorationContainer传进去,结合该Decoration自身记录的index来定位到具体的DecorationInfo的T

挺复杂的。没能理解为啥实现的这么复杂

PS1: CRTP常见用法

singleton

template <class T>

class singleton{

public:

singleton()=delete;

static void release(){  delete p;   p=nullptr;}

static T* get(){

    if (!p)p=   new T();    

    return p;

}

static T * p;

};

然后单例类就继承singleton就可以了

还有比较常见的是std::enable_shared_from_this

其他用法见WIKI

PS2: plantUML

@startuml
class Decorable {
 -_decorations:DecorationContainer 

 ~static DecorationRegistry* getRegistry()
 +static Decoration<T> declareDecoration()
}


class Decoration{
 -_raw:DecorationContainer::DecorationDescriptorWithType<T>
 +explicit Decoration(DecorationContainer::DecorationDescriptorWithType<T> raw)
 +T&:operator()
}

class DecorationRegistry{
 -_decorationInfo:std::vector<DecorationInfo>
 -_totalSizeBytes:size_t
 +DecorationContainer::DecorationDescriptorWithType<T> declareDecoration()
 +size_t getDecorationBufferSizeBytes()
 +void construct(DecorationContainer* decorable) const
 +void destruct(DecorationContainer* decorable) const
 ~DecorationContainer::DecorationDescriptor declareDecoration(size_t sizeBytes, 
size_t alignBytes, 
function<void(void*)>constructor, 
function<void(void*)>destructor)
}

class DecorationInfo {
 -descriptor:DecorationContainer::DecorationDescriptor 
 -constructor :function<void(void*)>
 -destructor :function<void(void*)>
 +DecorationInfo(DecorationContainer::DecorationDescriptor descriptor,
                       function<void(void*)>constructor,
                       function<void(void*)>destructor)
}

class DecorationContainer {
 -const DecorationRegistry* const _registry
 -const std::unique_ptr<unsigned char[]> _decorationData
 +explicit DecorationContainer(const DecorationRegistry* registry)
 +~DecorationContainer()
 +T& getDecoration(DecorationDescriptorWithType<T> descriptor)
 +void* getDecoration(DecorationDescriptor descriptor)

}

class DecorationDescriptorWithType<T> {
 - _raw: DecorationDescriptor
 -friend class DecorationContainer
 -friend class DecorationRegistry
 +explicit DecorationDescriptorWithType(DecorationDescriptor raw)
}

class DecorationDescriptor {
 -_index:size_t
 -friend class DecorationContainer;
 -friend class DecorationRegistry;
 +explicit DecorationDescriptor(size_t index)
 
}

Decorable *- DecorationContainer
DecorationContainer *- DecorationRegistry
DecorationContainer +- DecorationDescriptor 
DecorationContainer +- DecorationDescriptorWithType
DecorationRegistry *- DecorationInfo 
DecorationInfo *- DecorationDescriptor 
Decoration *- DecorationDescriptorWithType
DecorationDescriptorWithType *- DecorationDescriptor

Decorable --> Decoration 
Decorable --> DecorationRegistry
DecorationRegistry -->DecorationDescriptorWithType
@enduml
Read More

(译)写好Pull Requests(PR)

原文链接

野生翻译,欢迎意见

最近和同事聊了聊关于合入请求(PR)的事儿,进过几个回合的讨论我觉得还是写下我的想法(对我俩都好)

简单介绍一下PR,GitHub术语,是提交请求改变代码库代码的一个方法,在过去(以及现在),内核社区一直用邮件系统。在GitLab这个叫合入请求(Merge Request)。以下原则在任何场景(代码管理方法)都适合使用

动机

动机是决定一个PR如何结束的主要因素,当我建了一个PR,我的主要动机是

  • 我希望评审员能理解我改动的意图
  • 评审员应该能够指出改动中遗漏疏忽的点

当我考虑这些动机,我的意图反应在我创建的PR改动中,当我的首要动机是尽快的解决想解决工单,这个PR在评审员严重可能就是噩梦。我不是贬低修改工单这个需求,尽管这和工资息息相关,单这个动机是次要的,在这片文章中我会列出我认为创建一个PR中至关重要的三条原则

1.PR 改动特别大

当一个PR特别长,这对评审员是个心智负担,改动越长,评审员想要在脑海中记住变更的全貌就越难。我倾向于短的PR,对于我个人可能多点努力,但是能保证我的PR能轻松的审计并及时合入,长的PR带来的长时间的评审

2.如果你非得传大量改动,使用git commits

有时候会遇到这样的场景:很难写出小的改动(PR),要不就功能实现不完整,要不就测试不通过,或者你的啥理由,这种场景我最起码把这些改动拆成小的git commits,这样评审能通过我的git log来看改动过程

3.一个PR里不要提交太多改动

当你改动一个bug的时候,往往也想顺便改个变量名字顺便改个文件结构,请不要这么做。这会使评审走读原本的改动变得困难。本来就是几个小改动,上面这样改可能看起来就是一大堆函数添加删除改动很大。将这种改动和修bug拆开

代码是写出来给别人读的。我认为这在社区中已经说过很多次,PR更应该遵循。它表达出我作为一个作者在项目中改动的意图。这不简单,这需要时间和思考。但利大于弊。如果我希望评审做好工作,那我的工作应该让他更轻松。记住这个动机,它会改变创建PR这个流程


Read More

(译)代码审核:关于信号

//翻译的十分糟糕,就当做自己的阅读笔记了原文点击

POSIX信号是个挺让人畏惧的话题,在这个帖子中,我会用几个真实环境中的例子来消除这些疑惑,看这些例子是如何通过信号来解决问题的

第一部分 POSIX 信号(Linux)

POSIX 信号有很复杂的规则,伴随而来的是一些bug(”段错误“带来的恐惧),基本上在核心代码中很少出现

本帖希望列举一些有用的依赖信号的设计模式并且解释内部信号的传递过程。我会主要在Linux环境上阐述,我以前的工作也基于Linux,大家对Linux或Unix-like系统也熟悉

这个帖子不会包含所有信号的整体介绍,这已经有很多不错的帖子列举了

我只会指出大家在使用信号时的错误观念

thread-local signal masks 和全局handler回调函数

我工作用的系统上有很多bug,和开源代码中一样,这些bug的根源在于对下列基本概念的误解

​ 信号处理(是否注册回调函数,回调函数做什么)是全局函数

​ 信号掩码(是否一个线程可以收到信号)是thread-local类型

这个现象一部分原因在于 线程工作之前信号就已经处理好了,所以就没必要为线程写特定的信号回调handler了,

另一部分原因在于POSIX sigprocmask(3) 文档包含了下面这行吓人的话

​ sigprocmask函数在多线程进程中是未明确的(unspecified )

这技术上是正确的,POSIX值明确表示pthread_sigmosk在多线程环境中是安全的

pthread_sigmask和sigprocmask在linux上的区别是pthread_sigmask(libc)是sigprocmask(syscall)实现的

信号到任意一个线程

POSIX标准明确区分了两种信号的产生

Thread-targeted 信号,标准明确了在线程中任何动作触发的信号会在这个线程收到

即信号从哪个线程生成就会给哪个线程

Process-targeted 信号,任何不是线程产生的信号就是Process-targeted信号

那些和进程ID或进程组ID或异步事件相关的生成的信号,会给进程本身,比如终端活动

如果你关心的信号时thread-targeted,其他线程是收不到的,如果你阻塞他(sigmask是thread-local对象),你就是在用默认的处理方式

如果这个信号是进程相关的,任何进程都能收到,然而,这不是什么魔法,内核中的代码能自圆其说,主线程(tid==pid)会尝试收到该信号,其他的线程会循环获取来平衡信号传递

人们想到许多可以知道 方法去处理这些乱七八糟的东西-从信号管道 到一个信号处理线程,总有一些你可以放到你应用中的点子,或许,你可以通过kill和tgkill发送信号

你不可以处理“致命”错误

POSIX标准对信号处理回调说过

对于 SIGBUS, SIGFPE, SIGILL, SIGSEGV这些不是由kill(), sigqueue(), raise(),生成的信号,假如用回调函数捕获处理了这些信号,之后程序的行为将是未定义的

在Linux上,内核会重新发陷阱指令并重新发信号,这个行为是ABI的一部分,不会改变

这也说明,程序返回并不是跳出信号处理回调函数的唯一办法,也可以调用setjmp/longjmp 或者make/set/getcontext (不反回),可以在程序处理期间做一些有意思的事。实际上POSIX在一开始就考虑到了,这也是为什么信号掩码,sigsetjmp siglongjmp还保留至今。

    thread_local jmp_context;
    thread_local in_dangerous_work;
    handler() {
      if (in_dangerous_work) {
        in_dangerous_work = false;
        siglongjmp(&jmp_context, 0);
      }
      // call previous handler here
    }
    work() {
      register_handler_for_dangerous_work(&handler);
      // calls sigaction for the signals we care about
      // the second argument means that the return
      // value from the sigsetjmp will be 1 if we
      // jumped to it
      if (sigsetjmp(&jmp_context, 1) == 0) {
        in_dangerous_work = true;
        do_dangerous_work();
        in_dangerous_work = false;
      } else {
        // we crashed while doing the dangerous work,
        // do something else.
      }
    }

通过使用thread-local变量和longjump 我们可以安全的识别有风险的部分并且挽救回来,且不影响整体的正确性

例子中的可能需要放到沙盒中搞危险工作包括

  • 与不能保证安全的库(ABI)进行交互
  • 在没有简单的方法查询每条指令的详细存在情况下,检测平台对CPU扩展指令的支持情况(咳咳,arm)
  • 全地与程序的其他部分进行竞争(当所做的工作不是和竞争相关的,例如,收集特定于线程的诊断数据)

信号处理回调函数不能做什么有用的事儿

信号处理回调函数不过是个小函数去打断内核信号动作,函数可以操作一个人一多 上下文,可以访问当前被打断状态下的任何东西的任何状态(SA_SIGINFO flag中给了ucontext访问券,包含了在信号传递过程中所有线程寄存器的状态),并且有足够清晰的语义来分析这些状态是怎么被传递和处理的

然而这需要谨慎的设计出正确的同步原语,不是不可能,我也希望下面这个项目能演示出来

第二部分 一些信号的有趣用法

下面是一些我在工作中和google中而然发现的关于信号的有趣用法

虚拟机内部实现(JavaScriptCore)

当实现一个虚拟机,一个需要你实现的机制是在一个设定的时间点挂住一个线程的能力,有时,你需要实现这个机制去遍历这个线程的所有堆栈来进行垃圾回收(GC),或者你需要这个功能区实现调试器断点功能。

javascriptCore,webkit‘s javascript 内核使用信号来实现Linux上的悬停/恢复原语,它也是用信号来实现所谓的“VM陷阱” -就是线程运行时附到调试器断点,结束线程等等操作

在虚拟机中的异常处理 (JavaScriptCore,ART,HotSpot)

继续虚拟机中使用信号的话题,另一个有趣的用法是允许内存踩踏发生,检测出来,抛出异常

举个例子,在java中,当程序引用一个无效的引用,虚拟机会抛出NullPointerException异常

当虚拟机编译程序(比如JIT编译,或ART的例子,AOT ),NullPointerException不是个主要路径,虚拟机可以省略所有的null检查代码。然而,为了保证运行时正确,当编译过的代码抛出了异常,虚拟机会使用一个信号回调函数来处理

实际上,如果你读一下链接里的Hotspot虚拟机代码,虚拟机很多功能都通过信号来实现,除零错误和堆栈溢出就在那里

用户态页错误(libsigsegv)

在GNU libsigsegv项目中有一个SIGSEGV信号处理的有趣的应用,主页地址

  • 持久化数据库内存映射访问
  • 通用的垃圾收集器
  • 堆栈溢出处理函数
  • 分布式共享内存

当处理内存映射文件,一大堆控制动作藏在用户态程序和内核中,当预取页文件,不管我们正在连续读还是随机读,读硬盘上的数据,什么时候把数据从脏页刷到硬盘,这些决定都是内核代表用户态程序去做的

通过处理SIGSEGV信号,(无论是否是libsigsegv还是标准POSIX调用),我们就获得了执行的控制权,可以自由获取地址空间,自己做决定。

这个需求普遍通用,linux内核实现了userfaultfd,更简单的实现用户态页错误

性能分析(gperftools)

一些POSIX性能分析API是基于信号的,比如POSIX测量CPU时间的方法,使用setitimer 和ITIMER_PROF,这个定时器发送一个线程检测信号SIGPROF到线程移动指定的CPU频率(?)

比如,gperftools项目用setitimer和栈回溯来实现CPU-time堆栈跟踪

崩溃分析(breakpad)

最近我们接触到信号处理的最平常用法 - 崩溃报告,实际上并没有实际处理信号,只是记录发生

breakpad实现了崩溃手机系统,通过处理SIGSEGV,SIGSBRT和其他终端信号和通过信号处理函数尽可能的收紧更多的信息,它记录了寄存器的状态,不活了所有线程的堆栈信息,并且尽可能的展开崩溃线程的对竹山,这些动作是的信号处理函数中预分配的内存在不安全的上下文下来获得的

结论

希望这篇文章能让我们了解这个常常令人恐惧的POSIX信号世界。我希望它能启发你或至少能驱散你的恐惧。我知道我很喜欢读这样的东西。


Read More

rocksdb 一些堆栈记录

一个堆栈信息,merge_test

(gdb) bt
#0  CountMergeOperator::Merge (this=0xebe410, key=..., existing_value=0x7fffffffd350, value=..., new_value=0x7fffffffd150, logger=0xebd220)
    at db/merge_test.cc:39
#1  0x00000000006123a4 in rocksdb::AssociativeMergeOperator::FullMergeV2 (this=0xebe410, merge_in=..., merge_out=0x7fffffffd1d0)
    at db/merge_operator.cc:62
#2  0x000000000060d3e4 in rocksdb::MergeHelper::TimedFullMerge (merge_operator=merge_operator@entry=0xebe410, key=...,
    value=value@entry=0x7fffffffd350, operands=..., result=0x7fffffffd9e0, logger=0xebd220, statistics=0x0,
    env=0xbc9340 <rocksdb::Env::Default()::default_env>, result_operand=0x0, update_num_ops_stats=true) at db/merge_helper.cc:81
#3  0x0000000000604b7f in rocksdb::SaveValue (arg=0x7fffffffd550, entry=<optimized out>) at db/memtable.cc:735
#4  0x00000000006a81ea in rocksdb::(anonymous namespace)::SkipListRep::Get (this=<optimized out>, k=..., callback_args=0x7fffffffd550,
    callback_func=0x604680 <rocksdb::SaveValue(void*, char const*)>) at memtable/skiplistrep.cc:77
#5  0x0000000000603c4f in rocksdb::MemTable::Get (this=0xed7150, key=..., value=0x7fffffffd9e0, s=s@entry=0x7fffffffd9d0,
    merge_context=merge_context@entry=0x7fffffffd680, range_del_agg=range_del_agg@entry=0x7fffffffd700, seq=0x7fffffffd670, read_opts=...,
    callback=0x0, is_blob_index=0x0) at db/memtable.cc:856
#6  0x0000000000580822 in Get (is_blob_index=0x0, callback=0x0, read_opts=..., range_del_agg=0x7fffffffd700, merge_context=0x7fffffffd680,
    s=0x7fffffffd9d0, value=<optimized out>, key=..., this=<optimized out>) at ./db/memtable.h:203
#7  rocksdb::DBImpl::GetImpl (this=0xec3730, read_options=..., column_family=<optimized out>, key=..., pinnable_val=0x7fffffffd920,
    value_found=0x0, callback=0x0, is_blob_index=0x0) at db/db_impl.cc:1163
#8  0x0000000000580fb7 in rocksdb::DBImpl::Get (this=<optimized out>, read_options=..., column_family=<optimized out>, key=...,
    value=<optimized out>) at db/db_impl.cc:1098
#9  0x0000000000517da5 in rocksdb::DB::Get (this=this@entry=0xec3730, options=..., column_family=0xed0800, key=...,
    value=value@entry=0x7fffffffd9e0) at ./include/rocksdb/db.h:335
#10 0x00000000005196bc in Get (value=0x7fffffffd9e0, key=..., options=..., this=0xec3730) at ./include/rocksdb/db.h:345
#11 Counters::get (this=this@entry=0x7fffffffde10, key=..., value=value@entry=0x7fffffffda68) at db/merge_test.cc:172
#12 0x0000000000519a5c in Counters::assert_get (this=this@entry=0x7fffffffde10, key=...) at db/merge_test.cc:209
#13 0x000000000051296f in (anonymous namespace)::testCounters (counters=..., db=0xec3730, test_compaction=test_compaction@entry=false)
    at db/merge_test.cc:280
#14 0x0000000000513c62 in (anonymous namespace)::runTest (argc=argc@entry=1, dbname=..., use_ttl=use_ttl@entry=false) at db/merge_test.cc:433
#15 0x000000000040ebe0 in main (argc=1) at db/merge_test.cc:510

#0  rocksdb::AssociativeMergeOperator::FullMergeV2 (this=0xebe410, merge_in=..., merge_out=0x7fffffffd1d0) at db/merge_operator.cc:68
#1  0x000000000060d3e4 in rocksdb::MergeHelper::TimedFullMerge (merge_operator=merge_operator@entry=0xebe410, key=..., value=value@entry=0x0,
    operands=..., result=0x7fffffffd9e0, logger=0xebd220, statistics=0x0, env=0xbc9340 <rocksdb::Env::Default()::default_env>,
    result_operand=0x0, update_num_ops_stats=true) at db/merge_helper.cc:81
#2  0x0000000000604e2f in rocksdb::SaveValue (arg=0x7fffffffd550, entry=<optimized out>) at db/memtable.cc:757
#3  0x00000000006a81ea in rocksdb::(anonymous namespace)::SkipListRep::Get (this=<optimized out>, k=..., callback_args=0x7fffffffd550,
    callback_func=0x604680 <rocksdb::SaveValue(void*, char const*)>) at memtable/skiplistrep.cc:77
#4  0x0000000000603c4f in rocksdb::MemTable::Get (this=0xed7150, key=..., value=0x7fffffffd9e0, s=s@entry=0x7fffffffd9d0,
    merge_context=merge_context@entry=0x7fffffffd680, range_del_agg=range_del_agg@entry=0x7fffffffd700, seq=0x7fffffffd670, read_opts=...,
    callback=0x0, is_blob_index=0x0) at db/memtable.cc:856
#5  0x0000000000580822 in Get (is_blob_index=0x0, callback=0x0, read_opts=..., range_del_agg=0x7fffffffd700, merge_context=0x7fffffffd680,
    s=0x7fffffffd9d0, value=<optimized out>, key=..., this=<optimized out>) at ./db/memtable.h:203
#6  rocksdb::DBImpl::GetImpl (this=0xec3730, read_options=..., column_family=<optimized out>, key=..., pinnable_val=0x7fffffffd920,
    value_found=0x0, callback=0x0, is_blob_index=0x0) at db/db_impl.cc:1163
#7  0x0000000000580fb7 in rocksdb::DBImpl::Get (this=<optimized out>, read_options=..., column_family=<optimized out>, key=...,
    value=<optimized out>) at db/db_impl.cc:1098
#8  0x0000000000517da5 in rocksdb::DB::Get (this=this@entry=0xec3730, options=..., column_family=0xed0800, key=...,
    value=value@entry=0x7fffffffd9e0) at ./include/rocksdb/db.h:335
#9  0x00000000005196bc in Get (value=0x7fffffffd9e0, key=..., options=..., this=0xec3730) at ./include/rocksdb/db.h:345
#10 Counters::get (this=this@entry=0x7fffffffde10, key=..., value=value@entry=0x7fffffffda68) at db/merge_test.cc:172
#11 0x0000000000519a5c in Counters::assert_get (this=this@entry=0x7fffffffde10, key=...) at db/merge_test.cc:209
#12 0x0000000000512a1b in (anonymous namespace)::testCounters (counters=..., db=0xec3730, test_compaction=test_compaction@entry=false)
    at db/merge_test.cc:292
#13 0x0000000000513c62 in (anonymous namespace)::runTest (argc=argc@entry=1, dbname=..., use_ttl=use_ttl@entry=false) at db/merge_test.cc:433
#14 0x000000000040ebe0 in main (argc=1) at db/merge_test.cc:510

get内部会调用merge _operator


#0  (anonymous namespace)::UInt64AddOperator::Merge (this=0xec1500, existing_value=0x7fffffffd140, value=..., new_value=0x7fffffffd150,
    logger=0xebd220) at utilities/merge_operators/uint64add.cc:27
#1  0x00000000006123a4 in rocksdb::AssociativeMergeOperator::FullMergeV2 (this=0xebe410, merge_in=..., merge_out=0x7fffffffd1d0)
    at db/merge_operator.cc:62
#2  0x000000000060d3e4 in rocksdb::MergeHelper::TimedFullMerge (merge_operator=merge_operator@entry=0xebe410, key=..., value=value@entry=0x0,
    operands=..., result=0x7fffffffd9e0, logger=0xebd220, statistics=0x0, env=0xbc9340 <rocksdb::Env::Default()::default_env>,
    result_operand=0x0, update_num_ops_stats=true) at db/merge_helper.cc:81
#3  0x0000000000604e2f in rocksdb::SaveValue (arg=0x7fffffffd550, entry=<optimized out>) at db/memtable.cc:757
#4  0x00000000006a81ea in rocksdb::(anonymous namespace)::SkipListRep::Get (this=<optimized out>, k=..., callback_args=0x7fffffffd550,
    callback_func=0x604680 <rocksdb::SaveValue(void*, char const*)>) at memtable/skiplistrep.cc:77
#5  0x0000000000603c4f in rocksdb::MemTable::Get (this=0xed7150, key=..., value=0x7fffffffd9e0, s=s@entry=0x7fffffffd9d0,
    merge_context=merge_context@entry=0x7fffffffd680, range_del_agg=range_del_agg@entry=0x7fffffffd700, seq=0x7fffffffd670, read_opts=...,
    callback=0x0, is_blob_index=0x0) at db/memtable.cc:856
#6  0x0000000000580822 in Get (is_blob_index=0x0, callback=0x0, read_opts=..., range_del_agg=0x7fffffffd700, merge_context=0x7fffffffd680,
    s=0x7fffffffd9d0, value=<optimized out>, key=..., this=<optimized out>) at ./db/memtable.h:203
#7  rocksdb::DBImpl::GetImpl (this=0xec3730, read_options=..., column_family=<optimized out>, key=..., pinnable_val=0x7fffffffd920,
    value_found=0x0, callback=0x0, is_blob_index=0x0) at db/db_impl.cc:1163
#8  0x0000000000580fb7 in rocksdb::DBImpl::Get (this=<optimized out>, read_options=..., column_family=<optimized out>, key=...,
    value=<optimized out>) at db/db_impl.cc:1098
#9  0x0000000000517da5 in rocksdb::DB::Get (this=this@entry=0xec3730, options=..., column_family=0xed0800, key=...,
    value=value@entry=0x7fffffffd9e0) at ./include/rocksdb/db.h:335
#10 0x00000000005196bc in Get (value=0x7fffffffd9e0, key=..., options=..., this=0xec3730) at ./include/rocksdb/db.h:345
#11 Counters::get (this=this@entry=0x7fffffffde10, key=..., value=value@entry=0x7fffffffda68) at db/merge_test.cc:172
#12 0x0000000000519a5c in Counters::assert_get (this=this@entry=0x7fffffffde10, key=...) at db/merge_test.cc:209
#13 0x0000000000512a1b in (anonymous namespace)::testCounters (counters=..., db=0xec3730, test_compaction=test_compaction@entry=false)
    at db/merge_test.cc:292
#14 0x0000000000513c62 in (anonymous namespace)::runTest (argc=argc@entry=1, dbname=..., use_ttl=use_ttl@entry=false) at db/merge_test.cc:433
#15 0x000000000040ebe0 in main (argc=1) at db/merge_test.cc:510

iter

#0  CountMergeOperator::Merge (this=0xd79bc0, key=..., existing_value=0x7fffffffd7d0, value=..., new_value=0x7fffffffd4c0, logger=0xeab050)
    at db/merge_test.cc:44
#1  0x000000000060ded4 in rocksdb::AssociativeMergeOperator::FullMergeV2 (this=0xd79bc0, merge_in=..., merge_out=0x7fffffffd540)
    at db/merge_operator.cc:62
#2  0x0000000000608f74 in rocksdb::MergeHelper::TimedFullMerge (merge_operator=0xd79bc0, key=..., value=value@entry=0x7fffffffd7d0,
    operands=..., result=0x7fffe0000a40, logger=0xeab050, statistics=0x0, env=0xbb8260 <rocksdb::Env::Default()::default_env>,
    result_operand=0x7fffe0000a60, update_num_ops_stats=true) at db/merge_helper.cc:81
#3  0x00000000005c7c45 in rocksdb::DBIter::MergeValuesNewToOld (this=this@entry=0x7fffe0000960) at db/db_iter.cc:657
#4  0x00000000005c9070 in rocksdb::DBIter::FindNextUserEntryInternal (this=this@entry=0x7fffe0000960, skipping=skipping@entry=false,
    prefix_check=prefix_check@entry=false) at db/db_iter.cc:551
#5  0x00000000005ce37e in rocksdb::DBIter::FindNextUserEntry (this=0x7fffe0000960, skipping=<optimized out>, prefix_check=<optimized out>)
    at db/db_iter.cc:410
#6  0x00000000005c9f84 in rocksdb::DBIter::SeekToFirst (this=0x7fffe0000960) at db/db_iter.cc:1366
#7  0x000000000050e9e6 in (anonymous namespace)::dumpDb (db=db@entry=0xea9520) at db/merge_test.cc:250
#8  0x000000000050ed5a in (anonymous namespace)::testCounters (counters=..., db=0xea9520, test_compaction=test_compaction@entry=false)
    at db/merge_test.cc:280
#9  0x000000000050fff4 in (anonymous namespace)::runTest (argc=argc@entry=1, dbname=..., use_ttl=use_ttl@entry=false) at db/merge_test.cc:431
#10 0x000000000040ebc0 in main (argc=1) at db/merge_test.cc:508

dumpdb

0  CountMergeOperator::Merge (this=0xd79bc0, key=..., existing_value=0x7fffffffd540, value=..., new_value=0x7fffffffd550, logger=0xeab050)
    at db/merge_test.cc:44
#1  0x000000000060ded4 in rocksdb::AssociativeMergeOperator::FullMergeV2 (this=0xd79bc0, merge_in=..., merge_out=0x7fffffffd5d0)
    at db/merge_operator.cc:62
#2  0x0000000000608f74 in rocksdb::MergeHelper::TimedFullMerge (merge_operator=0xd79bc0, key=..., value=value@entry=0x0, operands=...,
    result=0x7fffe0000a40, logger=0xeab050, statistics=0x0, env=0xbb8260 <rocksdb::Env::Default()::default_env>, result_operand=0x7fffe0000a60,
    update_num_ops_stats=true) at db/merge_helper.cc:81
#3  0x00000000005c7ae1 in rocksdb::DBIter::MergeValuesNewToOld (this=this@entry=0x7fffe0000960) at db/db_iter.cc:704
#4  0x00000000005c9070 in rocksdb::DBIter::FindNextUserEntryInternal (this=this@entry=0x7fffe0000960, skipping=skipping@entry=true,
    prefix_check=prefix_check@entry=false) at db/db_iter.cc:551
#5  0x00000000005c93b5 in FindNextUserEntry (prefix_check=false, skipping=true, this=0x7fffe0000960) at db/db_iter.cc:410
#6  rocksdb::DBIter::Next (this=0x7fffe0000960) at db/db_iter.cc:384
#7  0x000000000050ea1d in (anonymous namespace)::dumpDb (db=db@entry=0xea9520) at db/merge_test.cc:250
#8  0x000000000050ee14 in (anonymous namespace)::testCounters (counters=..., db=0xea9520, test_compaction=test_compaction@entry=false)
    at db/merge_test.cc:293
#9  0x000000000050fff4 in (anonymous namespace)::runTest (argc=argc@entry=1, dbname=..., use_ttl=use_ttl@entry=false) at db/merge_test.cc:431
#10 0x000000000040ebc0 in main (argc=1) at db/merge_test.cc:508

merge

#0  CountMergeOperator::Merge (this=0x7fffe0001320, key=..., existing_value=0x7fffffffc520, value=..., new_value=0x7fffffffc530,
    logger=0x7fffe00034b0) at db/merge_test.cc:44
#1  0x000000000060ded4 in rocksdb::AssociativeMergeOperator::FullMergeV2 (this=0x7fffe0001320, merge_in=..., merge_out=0x7fffffffc5b0)
    at db/merge_operator.cc:62
#2  0x0000000000608f74 in rocksdb::MergeHelper::TimedFullMerge (merge_operator=0x7fffe0001320, key=..., value=value@entry=0x7fffffffc730,
    operands=..., result=0x7fffffffcdb0, logger=0x7fffe00034b0, statistics=0x0, env=0xbb8260 <rocksdb::Env::Default()::default_env>,
    result_operand=0x0, update_num_ops_stats=true) at db/merge_helper.cc:81
#3  0x0000000000600877 in rocksdb::SaveValue (arg=0x7fffffffc930, entry=<optimized out>) at db/memtable.cc:709
#4  0x00000000006a369a in rocksdb::(anonymous namespace)::SkipListRep::Get (this=<optimized out>, k=..., callback_args=0x7fffffffc930,
    callback_func=0x6000c0 <rocksdb::SaveValue(void*, char const*)>) at memtable/skiplistrep.cc:77
#5  0x00000000005ff6df in rocksdb::MemTable::Get (this=0x7fffe0013a80, key=..., value=0x7fffffffcdb0, s=s@entry=0x7fffffffcd50,
    merge_context=merge_context@entry=0x7fffffffca60, range_del_agg=range_del_agg@entry=0x7fffffffcae0, seq=0x7fffffffca50, read_opts=...,
    callback=0x0, is_blob_index=0x0) at db/memtable.cc:830
#6  0x000000000057c8fb in Get (is_blob_index=0x0, callback=0x0, read_opts=..., range_del_agg=0x7fffffffcae0, merge_context=0x7fffffffca60,
    s=0x7fffffffcd50, value=<optimized out>, key=..., this=<optimized out>) at ./db/memtable.h:203
#7  rocksdb::DBImpl::GetImpl (this=0x7fffe0002510, read_options=..., column_family=<optimized out>, key=..., pinnable_val=0x7fffffffce90,
    value_found=0x0, callback=0x0, is_blob_index=0x0) at db/db_impl.cc:1145
#8  0x000000000057d067 in rocksdb::DBImpl::Get (this=<optimized out>, read_options=..., column_family=<optimized out>, key=...,
    value=<optimized out>) at db/db_impl.cc:1084
#9  0x00000000006652cd in Get (value=0x7fffffffcdb0, key=..., column_family=0x7fffe000e8b8, options=..., this=0x7fffe0002510)
    at ./include/rocksdb/db.h:335
#10 rocksdb::MemTableInserter::MergeCF (this=0x7fffffffd170, column_family_id=0, key=..., value=...) at db/write_batch.cc:1497
#11 0x000000000065e491 in rocksdb::WriteBatch::Iterate (this=0x7fffffffd910, handler=handler@entry=0x7fffffffd170) at db/write_batch.cc:479
#12 0x00000000006623cf in rocksdb::WriteBatchInternal::InsertInto (write_group=..., sequence=sequence@entry=21, memtables=<optimized out>,
    flush_scheduler=flush_scheduler@entry=0x7fffe0002e80, ignore_missing_column_families=<optimized out>, recovery_log_number=0,
    db=0x7fffe0002510, concurrent_memtable_writes=false, seq_per_batch=false) at db/write_batch.cc:1731
#13 0x00000000005c203c in rocksdb::DBImpl::WriteImpl (this=0x7fffe0002510, write_options=..., my_batch=<optimized out>,
    callback=callback@entry=0x0, log_used=log_used@entry=0x0, log_ref=0, disable_memtable=false, seq_used=0x0, batch_cnt=0,
    pre_release_callback=0x0) at db/db_impl_write.cc:309
#14 0x00000000005c24c1 in rocksdb::DBImpl::Write (this=<optimized out>, write_options=..., my_batch=<optimized out>) at db/db_impl_write.cc:54
#15 0x00000000005c2972 in rocksdb::DB::Merge (this=this@entry=0x7fffe0002510, opt=..., column_family=column_family@entry=0x7fffe000ebf0,
    key=..., value=...) at db/db_impl_write.cc:1501
#16 0x00000000005c2a2f in rocksdb::DBImpl::Merge (this=0x7fffe0002510, o=..., column_family=0x7fffe000ebf0, key=..., val=...)
    at db/db_impl_write.cc:33
#17 0x0000000000512ecd in rocksdb::DB::Merge (this=0x7fffe0002510, options=..., key=..., value=...) at ./include/rocksdb/db.h:312
#18 0x00000000005121f8 in MergeBasedCounters::add (this=<optimized out>, key=..., value=<optimized out>) at db/merge_test.cc:236
---Type <return> to continue, or q <return> to quit---
#19 0x000000000050eda0 in assert_add (value=18, key=..., this=0x7fffffffdda0) at db/merge_test.cc:214
#20 (anonymous namespace)::testCounters (counters=..., db=0x7fffe0002510, test_compaction=test_compaction@entry=false) at db/merge_test.cc:287
#21 0x0000000000510119 in (anonymous namespace)::runTest (argc=argc@entry=1, dbname=..., use_ttl=use_ttl@entry=false) at db/merge_test.cc:442
#22 0x000000000040ebc0 in main (argc=1) at db/merge_test.cc:508

reference

  1. 官方文档 https://github.com/facebook/rocksdb/wiki/Merge-Operator
  2. 上面的一个翻译https://www.jianshu.com/p/e13338a3f161

或者到博客上提issue 我能收到邮件提醒。

Read More

rocksdb

不整理就忘了

悲报 调用图网站挂了。只能手动doc gen了。有空再搞

另外,版本是5.x,现在已经8了。应该考虑更新一编

[toc]


rocksdb的整体框架和数据模型,leveldb的整体框架,网上文章遍地都是了。细分到组件,又一大堆分析。看的头都大了。事实上我现在还是分不清的。术语看了一遍又一遍,还是卡壳,看代码,还是会忘记。感觉不用硬看不行啊。还是上图吧。

dbimpl 属性图

img

column family 属性图

img

supervision属性图

img

version 属性图

img

version set 属性图

img

概念

Column Family

这个不是Cassandra那个CF,就是键空间。这个用来实现多种需求,比如上层元数据拆分。这涉及到编码的定制。我也不很懂

CRUD

插入就是插入,更新是追加插入,删除也是更新,value空。支持writebatch,原子性

put https://www.jianshu.com/p/daa18eebf6e1

https://glitterisme.github.io/2018/07/03/%E6%89%8B%E6%92%95RocksDB-1/

myrocks put http://mysql.taobao.org/monthly/2017/07/05/

wirte详解? https://blog.csdn.net/wang_xijue/article/details/46521605

http://mysql.taobao.org/monthly/2018/07/04/

并发写?

http://www.pandademo.com/2016/10/parallel-write-rocksdb-source-dissect-3/

https://www.jianshu.com/p/fd7e98fd5ee6

https://youjiali1995.github.io/rocksdb/write-batch/

write stall? 什么时候会write stall?

Gets Iterators Snapshots

iterator 和snapshot是一致的。底层引用计数保证不被删除。但是这个数据是不被持久化的,对比git,这就是拉个分支,然后在分支上修改,但是这个分支信息不记录,不保存就丢,可以在这基础上创建checkpoint然后搞备份

?疑问 checkpoint实现? backupdb实现?是不是基于checkpoint

minifest

这东西就是个一个index 内部实现, 一致性保证

文件具体为

  • MANIFEST-xx 内部是相关的一堆version edit
  • CURRENT 记录最新的MANIFEST-xx(类似git里的head)

这里要说下version edit version set 和version

version 和versionset version这就是个snapshot snapshot这东西就是版本号啦,或者可以理解成git里的commit,每个commit都能抽出来一个分支,这就是snapshot了,version由version edit组成,version edit可以理解成 modify。git中的相关思想在大多kv中都有。

version是某个状态,version set就是所有状态的集合(一个branch?)

参考3 ,介绍了实现,一个cv队列,versionSet 拥有manifest写,新的mannifest入队列,判断current。另外ldd文件可以导出内容,一个例子

 ../ldb manifest_dump --path=./MANIFEST-000006
--------------- Column family "default"  (ID 0) --------------
log number: 15
comparator: leveldb.BytewiseComparator
--- level 0 --- version# 1 ---
 16:31478713['00010' seq:5732151, type:1 .. '0004999995' seq:6766579, type:1]
 14:31206213['00011000020' seq:4753765, type:1 .. '00049999990' seq:4266837, type:1]
 12:31367971['00011000023' seq:3559804, type:1 .. '00049999988' seq:3516765, type:1]
 10:31331378['00011000025' seq:560662, type:1 .. '00049999994' seq:1786954, type:1]
--- level 1 --- version# 1 ---
...省略60行打印
--- level 63 --- version# 1 ---

next_file_number 18 last_sequence 7515368  prev_log_number 0 max_column_family 0 min_log_number_to_keep 15

WAL

这东西就是oplog一种表达,有编码。可以看参考链接3中的实现

什么时候调用NewWritableFile?

  • 打开db,肯定要生成对应的log
  • 切换memtable,写满了,生成新的memtable,旧的imm等持久化,wal日志的生命周期结束

什么时候清理? FIndObsoleteFiles 和sst manifest文件一起清理。

谁负责写?当然是dbimpl,对于memtable切换时全程感知的

考虑场景,WAL文件已经写满,但是memtable还没写满怎么办?

至于内容编码和配置选项,以及相关优化选项,又是另一个话题

内容一例

../ldb dump_wal --walfile=./000015.log |tail -f

7515329,4,76,495457,NOOP PUT(0) : 0x3030303334383431343337 PUT(0) : 0x3030303431353233363738 PUT(0) : 0x30303032383839343733 PUT(0) : 0x3030303135373830323031

` 事务`

这又是个复杂的话题,见参考链接

memtable

一般为了支持并发写,会使用skiplist

compaction

leveldb的由来,compaction gc

什么时候会compaction?

compaction原则?

compaction中的snapshot?

https://yq.aliyun.com/articles/655902

flush

什么时候会flush?

Get

DBImpl::GetImpl

  • db, cfd, sv之间的关系: column family data,cfd就相当于db键空间,cfd 有super version sv,这个就是version的上层抽象,version的集合
  • snapshot,lsn 。 get指定快照多是在事务中。快照实现事务。当然建立过快照,指定快照来取也可以。和git拉分支类似。增加引用计数。snapshot实际上就是lsn的一层薄封装,内部是链表。lsn,版本号。可以理解成两者等同
    • 如果不指定,就会算出一个,通常是versionSet中有的最后一个发布的lsn。versionSet会维护这个lsn
    • rocksdb内部key是有lsn编码的,会有查找构造类lookupkey,需要snapshot参数。

如果read_option没有指定跳过memtable,那就会从memtable开始遍历

sv->mem->Get

  • bloomFilter
  • Saver
  • MemTableRep,table_->Get memtable抽象。后面调用具体的实现。
  • 会有对Merge的判断。Merge途中就跳过。

sv->imm->Get

  • imm实际上是MemTableListVersion 是一系列version的list。不可插入,add就是插入链表。数据本身不变了。
  • 调用GetFromList最后内部遍历还是调用mem->Get,lsn做边界条件,同上,会有对Merge的判断

sv->current->Get

  • 通过Version中记录的信息遍历Level中的所有SST文件,利用SST文件记录的边界最大,最小key- smallest_keylargest_key来判断查找的key是否有可能存在
  • 如果在该Level中可能存在,调用TableReader的接口来查找SST文件
    • 首先通过SST文件中的Filter来初判key是否存在
    • 如果存在key存在,进入Data Block中查找
    • 在Data Block利用Block的迭代器BlockIter利用二分查找BinarySeek或者前缀查找PrefixSeek
  • 如果查找成功则返回,如果该Level不存在查找的Key,继续利用Version中的信息进行下一个Level的查找,直到最大的一个Level

table_cache_->Get(k)

#4   ./db_test() [0x6ae065] rocksdb::MemTable::KeyComparator::operator()(char const*, rocksdb::Slice const&) const      ??:?
#5   ./db_test() [0x7525f2] rocksdb::InlineSkipList<rocksdb::MemTableRep::KeyComparator const&>::FindGreaterOrEqual(char const*) const  ??:?
#6   ./db_test() [0x7516d5] rocksdb::(anonymous namespace)::SkipListRep::Get(rocksdb::LookupKey const&, void*, bool (*)(void*, char const*))skiplistrep.cc:?
#7   ./db_test() [0x6af31f] rocksdb::MemTable::Get(rocksdb::LookupKey const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*, rocksdb::Status*, rocksdb::MergeContext*, rocksdb::RangeDelAggregator*, unsigned long*, rocksdb::ReadOptions const&, rocksb::ReadCallback*, bool*)        ??:?
#8   ./db_test() [0x62f49b] rocksdb::DBImpl::GetImpl(rocksdb::ReadOptions const&, rocksdb::ColumnFamilyHandle*, rocksdb::Slice const&, rocksdb::PinnableSlice*, bool*, rocksdb::ReadCallback*, bool*)        ??:?
#9   ./db_test() [0x62fc07] rocksdb::DBImpl::Get(rocksdb::ReadOptions const&, rocksdb::ColumnFamilyHandle*, rocksdb::Slice const&, rocksdb::PinnableSlice*)  ??:?
#10  ./db_test() [0x59c9cd] rocksdb::DB::Get(rocksdb::ReadOptions const&, rocksdb::ColumnFamilyHandle*, rocksdb::Slice const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*)     ??:?
#11  ./db_test() [0x58fc9d] rocksdb::DB::Get(rocksdb::ReadOptions const&, rocksdb::Slice const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*)   ??:?
#12  ./db_test() [0x51c3a3] rocksdb::DBTest_MockEnvWithTimestamp_Test::TestBody()       ??:?
#13  ./db_test() [0x9401c8] void testing::internal::HandleExceptionsInMethodIfSupported<testing::Test, void>(testing::Test*, void (testing::Test::*)(), char const*) ??:?
#14  ./db_test() [0x93799c] testing::Test::Run() [clone .part.476]      gtest-all.cc:?
#15  ./db_test() [0x937b6d] testing::TestInfo::Run() [clone .part.477]  gtest-all.cc:?
#16  ./db_test() [0x937ce5] testing::TestCase::Run() [clone .part.478]  gtest-all.cc:?
#17  ./db_test() [0x93812d] testing::internal::UnitTestImpl::RunAllTests()      ??:?
#18  ./db_test() [0x940678] bool testing::internal::HandleExceptionsInMethodIfSupported<testing::internal::UnitTestImpl, bool>(testing::internal::UnitTestImpl*, bool (testing::internal::UnitTestImpl::*)(), char const*)   ??:?
#19  ./db_test() [0x93843f] testing::UnitTest::Run()    ??:?
#20  ./db_test() [0x40d9bd] main        ??:?
#21  /lib64/libc.so.6(__libc_start_main+0xf5) [0x7f3f6c423bb5] ??       ??:0
#22  ./db_test() [0x513761] _start      ??:?

OptimizeForPointLookup

ColumnFamilyOptions* ColumnFamilyOptions::OptimizeForPointLookup(
    uint64_t block_cache_size_mb) {
  prefix_extractor.reset(NewNoopTransform());
  BlockBasedTableOptions block_based_options;
  block_based_options.index_type = BlockBasedTableOptions::kHashSearch;
  block_based_options.filter_policy.reset(NewBloomFilterPolicy(10));
  block_based_options.block_cache =
      NewLRUCache(static_cast<size_t>(block_cache_size_mb * 1024 * 1024));
  table_factory.reset(new BlockBasedTableFactory(block_based_options));
  memtable_prefix_bloom_size_ratio = 0.02;
  return this;
}

优化点查询,实际上是改变了BlockBasedTable的index_type, 变成hash自然就不可迭代,主要是bloomfilter prefix tranform哪里会有hash查找

  bool const may_contain =
      nullptr == prefix_bloom_
          ? false
          : prefix_bloom_->MayContain(prefix_extractor_->Transform(user_key));

write

主要流程1中写的也很详细。我重新复述一下,加深印象

WriteBatch, WriteBatchInternal, WriteBatch::handler, WriteAheadLog

db调用的put等操作,最终调用的都是WriteBatch内部的put,然后调用WriteBatchInternal内部的动作,WriteBatch是一组操作,一起写要比一条条写要高效,并且每一组WriteBatch拥有同一个lsn。

由于WriteBatch是一组动作,putputdelget等等,每一条WriteBatch内部就是个string字节流,第一个字节记录动作类型。内部进行遍历iterate时,调用Handler实现的相关接口,判断是什么动作,最后由memtable执行,putCF等。

另外一个疑问,如何具体区分每一个动作和相关的kv呢,Slice。也就是string_view,每个kv都会有size,这个size提前编码到type的后面,相当于 type+(cfid)+ size+ sv+size+ sv 作为一个单元。del没有第二个size+sv

所以WriteBatch就是 lsn-count-header+{type size sv}…, 多条WriteBatch字节流组合,就是Write Ahead Log

WriteBatch::Handler是个接口类,memtable最终插入数据,memtableinserter要继承handler。

WriteBatchInternal内部工具类。当然是WriteBatch的友元类啦。可以操作WriteBatch的字节流string

编解码是一套很麻烦的动作,rocksdb util/coding.cc/h有很多小函数。可以看看抄一抄。由于string_view是不拥有值的,view嘛,所以复杂的编码动作都需要搞回到string在搞回来。直接搞到string上就append了。如果sv和sv连接,免不了string做交换,有机会找找解决办法。

Writer, WriteGroup, WriteBatch, 并发写

每个Writer是持有WriteBatch的。调用db put接口后就写到WriteBatch中,WriteBatch相当于缓存,这种异步和事件触发的异步不太一样,是同步造出一个二阶段缓存搞的异步。WriteBatch越大吞吐量越大,但是写到memtable越多则会影响后面的flush造成write stall,是个负反馈过程。

WriteBatch+WriteGroup 主要是为了写WAL,WAL是没办法并发写的,Leader Writer负责写,写完唤醒其他Writer,大家一起写memtable,无锁队列+condvar。无锁队列是用原子量实现的。主要逻辑在LinkOne

Write Stall, Write Controller

dbformat

internalkey setinternalkey parsedinternalkey userkey

简单来说 userkey就是internalkey 因为一个key会把lsn和类型编码进去(PackSequenceAndType),去掉这八个字节就是userkey,在rocksdb代码中,有些地方就直接是硬编码,-8了,有些地方转换回来还要+8, Jesus

Slice MemTableRep::UserKey(const char* key) const {
  Slice slice = GetLengthPrefixedSlice(key);
  return Slice(slice.data(), slice.size() - 8);
}

其实应该有个更小的封装,因为-8可能变(对于想要用rocksdb来修改的人来说)

memtable inserter, memtable, iterate

最终并发写入会调用memtableInserter, 也会生成memtable对象(就那么一个new入口),memtable inserter实现了memtable::handler,内部接口,操作memtable,调用add。

inserter会遍历当前的WriteBatch,解析出每个kv头的tag,然后分别调用putcf,deletecf 等等,内部都是memtable->add

merge


  options.merge_operator = MergeOperators::CreatePutOperator();

merge有时候语义上就是put

iterator

iteration

  1. ArenaWrappedDBIter是暴露给用户的Iterator,它包含DBIter,DBIter则包含InternalIterator,InternalIterator顾名思义,是内部定义,MergeIterator、TwoLevelIterator、BlockIter、MemTableIter、LevelFileNumIterator等都是继承自InternalIterator
  2. 图中绿色实体框对应的是各个Iterator,按包含顺序颜色由深至浅
  3. 图中虚线框对应的是创建各个Iterator的方法
  4. 图中蓝色框对应的则是创建过程中需要的类的对象及它的方法


注意到第二步,构造DBIter, 其中的Seek有IsVisiable的判断,也就是实现快照的关键。

每层iterator都会实现iterator头文件中定义好的接口,Seek Next等等,迭代过程中也是逐层调用

另外要注意Seek的语义(SeekForPrev正好相反),不同于Get,查不到不一定返回无效, 会返回所查找的元素的下一个存在于db中的数据

// Position at the first key in the source that at or past target.
// The iterator is Valid() after this call iff the source contains
// an entry that comes at or past target.
// All Seek*() methods clear any error status() that the iterator had prior to
// the call; after the seek, status() indicates only the error (if any) that
// happened during the seek, not any past errors.
virtual void Seek(const Slice& target) = 0;

iterator 牵连的引用以及生命周期, reseek

iterator迭代是依赖快照的,所以创建的迭代器是无法获得新加入的数据的。

iterator不释放 ->versionSet析构->cf set析构-> cfd析构,内部会持有引用。debug模式会assert挂掉

根据上面的结论, 如果版本skip设定比较小,是很容易发生reseek的,设定在Options中

// An iteration->Next() sequentially skips over keys with the same
// user-key unless this option is set. This number specifies the number
// of keys (with the same userkey) that will be sequentially
// skipped before a reseek is issued.
//
// Default: 8
//
// Dynamically changeable through SetOptions() API
uint64_t max_sequential_skip_in_iterations = 8;

比如这个没有flush的场景, option中设定skip=3

Put("a","v1");
Put("a","v2");
Put("b","v1");
Put("a","v3");
iter = NewIterator(RO, CF);
iter->SeekToFirst();//iter指向v4 ,这是最新的数据
int num_reseek = options.statistics->getTickerCount(NUMBER_OF_RESEEKS_IN_ITERATION);
iter->Next();// Seek本身是逆序搜最新的,按理说应该访问下一个,b在a后面,但是没击中,步进大于3,所以就需要重新Seek
assert(options.statistics->getTickerCount(NUMBER_OF_RESEEKS_IN_ITERATION)==num_reseek+1);

由于没有合并key a的所有版本都在memtable中,所以就需要找最旧的a,重新seek,下一个就是b

反过来,如果插入了新的b,先找b后找a,也会出现同样的场景

Put("b","v2");
// 现在是 a3 a2 a1 b2 b1 假设这个iter不指定snapshot
iter = NewIterator(RO, CF);
iter->SeekToLast();// iter指向b v2
iter->Prev();//正常来说是a,但是版本不确定,迭代大于3,就得reseek

iterate_lower_bound, iterate_upper_bound

ReadOption中带有的这两个选项,限定iterator的有效与否

  // `iterate_lower_bound` defines the smallest key at which the backward
  // iterator can return an entry. Once the bound is passed, Valid() will be
  // false. `iterate_lower_bound` is inclusive ie the bound value is a valid
  // entry.
  //
  // If prefix_extractor is not null, the Seek target and `iterate_lower_bound`
  // need to have the same prefix. This is because ordering is not guaranteed
  // outside of prefix domain.
  //
  // Default: nullptr
  const Slice* iterate_lower_bound;

  // "iterate_upper_bound" defines the extent upto which the forward iterator
  // can returns entries. Once the bound is reached, Valid() will be false.
  // "iterate_upper_bound" is exclusive ie the bound value is
  // not a valid entry.  If iterator_extractor is not null, the Seek target
  // and iterator_upper_bound need to have the same prefix.
  // This is because ordering is not guaranteed outside of prefix domain.
  //
  // Default: nullptr
  const Slice* iterate_upper_bound;

如果有key a b y z,设定upper_bound x,seek c,这个场景下iter是invalid的,过了x也没找到c,那就是无效的,同理lower_bound

tombstones, Next

看SeekAfterHittingManyInternalKeys这个测试用例

这里实际上是涉及到一个prefix bloom filter的,参考链接2给出了很详细的介绍和使用说明,我就直接抄过来了

[toc]

Prefix Seek

Prefix seek是RocksDB的一种模式,主要影响Iterator的行为。 在这种模式下,RocksDB的Iterator并不保证所有key是有序的,而只保证具有相同前缀的key是有序的。这样可以保证具有相同特征的key(例如具有相同前缀的key)尽量地被聚合在一起。

使用方法

prefix seek模式的使用如下,

int main() {
  DB* db;
  Options options;
  options.IncreaseParallelism();
  options.OptimizeLevelStyleCompaction();
  options.create_if_missing = true;
  options.prefix_extractor.reset(rocksdb::NewFixedPrefixTransform(3));

  Status s = DB::Open(options, kDBPath, &db);
  assert(s.ok());

  s = db->Put(WriteOptions(), "key1", "value1");
  assert(s.ok());
  s = db->Put(WriteOptions(), "key2", "value2");
  assert(s.ok());
  s = db->Put(WriteOptions(), "key3", "value3");
  assert(s.ok());
  s = db->Put(WriteOptions(), "key4", "value4");
  assert(s.ok());
  s = db->Put(WriteOptions(), "otherPrefix1", "otherValue1");
  assert(s.ok());
  s = db->Put(WriteOptions(), "abc", "abcvalue1");
  assert(s.ok());

  auto iter = db->NewIterator(ReadOptions());
  for (iter->Seek("key2"); iter->Valid(); iter->Next())
  {
    std::cout << iter->key().ToString() << ": " << iter->value().ToString() << std::endl;
  }

  delete db;
  return 0;
}

输出如下

key2: value2
key3: value3
key4: value4
otherPrefix1: otherValue1

从上面的输出,我们可以看到prefix seek模式下iterator的几个特性

  1. 首先seek(targetKey)方法会将iterator定位到具有相同前缀的区间,并且大于或等于targetKey的位置.
  2. Next()方法是会跨prefix的,就像例子中,当以”key”为前缀的key都遍历完之后,跨到了下一个prefix “oth”上继续遍历,直到遍历完所有的key,Valid()方法返回false.
  3. 在遍历的时候,如果只想遍历相同前缀的key,需要在每一次Next之后,判断一次key是前缀是否符合预期.
  auto iter = db->NewIterator(ReadOptions());
  Slice prefix = options.prefix_extractor->Transform("key0");
  for (iter->Seek("key0"); iter->Valid() && iter->key().starts_with(prefix); iter->Next())
  {
    std::cout << iter->key().ToString() << ": " << iter->value().ToString() << std::endl;
  }

输出如下:

key1: value1
key2: value2
key3: value3
key4: value4

Prefix Seek相关的一些优化

为了更快地定位指定prefix的key,或者排除不存在的key,RocksDB做了一些优化,包括:

  1. prefix bloom filter for block based table
  2. prefix bloom for memtable
  3. memtable底层使用hash skiplist
  4. 使用plain table

一个典型的优化设置见下例

Options options;

// Enable prefix bloom for mem tables
options.prefix_extractor.reset(NewFixedPrefixTransform(3));
options.memtable_prefix_bloom_bits = 100000000;
options.memtable_prefix_bloom_probes = 6;

// Enable prefix bloom for SST files
BlockBasedTableOptions table_options;
table_options.filter_policy.reset(NewBloomFilterPolicy(10, true));
options.table_factory.reset(NewBlockBasedTableFactory(table_options));

DB* db;
Status s = DB::Open(options, "/tmp/rocksdb",  &db);

......

auto iter = db->NewIterator(ReadOptions());
iter->Seek("foobar"); // Seek inside prefix "foo"

Prefix Seek相关类图

img

prefix seek相关的iterator类图

说是Prefix Seek相关的类图,其实就是rocksdb的Iterator类图。这里列出了几个主要的类。

  • InternalIterator 接口类,定义了iterator的接口,包括Seek、Next、Prev等接口。
  • MergingIterator 返回给用户的实际Iterator类型,之所以叫”Merging”,是因为它持有memtable和sst文件的iterater,对外提供了统一的iterator接口。
  • MemTableIterator 用于遍历memtable的iterator。主要的分析对象。
  • TwoLevelIterator 用于遍历sst文件的iterator。

Prefix Seek工作流程

Iterator简单分析

通过Iterator的相关接口,来逐层分析,持有PrefixTransform对象的option.prefix_extractor选项是如何工作的。 DB的Iterator由三部分组成

  1. 当前memtable的iterator
  2. 所有不可修改的memtable的iterator
  3. 所有level上的sst文件的iterator(TwoLevelIterator)

这些iterator统一由MergeIterator来管理。MergeIterator持有一个vector成员,上面三类iterator都会添加到该成员中。

autovector<IteratorWrapper, kNumIterReserve> children_;

autovector是对std::vector的完全模板特化版本

class autovector : public std::vector<T> {
  using std::vector<T>::vector;
};

autovector主要针对在栈上分配的数组,保存数量较少的item这种使用场景做了优化。 IteratorWrapper是对InternalIterator*类型的对象的一个简单封装,主要cache了iter)->Valid()和iter_->key()的结果,提供更好的局部性访问性能。其他接口和InternalIterator一样,只是转发请求给iter_。

DB的Iterator添加当前使用的memtable的iterator的代码如下

InternalIterator* DBImpl::NewInternalIterator(
    const ReadOptions& read_options, ColumnFamilyData* cfd,
    SuperVersion* super_version, Arena* arena,
    RangeDelAggregator* range_del_agg) {
  ...
    merge_iter_builder.AddIterator(
      super_version->mem->NewIterator(read_options, arena));
  ...

arena是为所有iterator已经分配的一块内存。 向MergeIterator添加Iterator的实现:

  1. 先将要添加的iter加入到vector成员children_中
  2. 如果iter是有效的iterator,将该iter添加到一个最小堆成员中。
  virtual void AddIterator(InternalIterator* iter) {
    assert(direction_ == kForward);
    children_.emplace_back(iter);
    if (pinned_iters_mgr_) {
      iter->SetPinnedItersMgr(pinned_iters_mgr_);
    }
    auto new_wrapper = children_.back();
    if (new_wrapper.Valid()) {
      minHeap_.push(&new_wrapper);
      current_ = CurrentForward();
    }
  }

这里的最小堆minHeap_用于维护上面所有合法iterator的访问顺序,指向key较小的iter,越靠近堆顶,这样,当连续多次调用Next接口时,都在同一个child iterator上,可以直接通过堆顶的iterator来获取Next,而不用遍历所有children。 如果堆顶iterator的下一个元素的不是合法的,则将该iterator从堆中pop出去,调整堆之后,拿到新的堆顶元素。 DB的Iterator MergingIterator的Next接口主要实现如下

  virtual void Next() override {
    assert(Valid());
    ...
    // For the heap modifications below to be correct, current_ must be the
    // current top of the heap.
    assert(current_ == CurrentForward());

    // as the current points to the current record. move the iterator forward.
    current_->Next();
    if (current_->Valid()) {
      // current is still valid after the Next() call above.  Call
      // replace_top() to restore the heap property.  When the same child
      // iterator yields a sequence of keys, this is cheap.
      minHeap_.replace_top(current_);
    } else {
      // current stopped being valid, remove it from the heap.
      minHeap_.pop();
    }
    current_ = CurrentForward();

Seek接口的实现

Seek一个target的实现,通过遍历每个child iterator,然后将合法的iterator插入到最小堆minHeap中,将current_成员指向minHeap的堆顶。

  virtual void Seek(const Slice& target) override {
    ClearHeaps();
    for (auto& child : children_) {
      {
        PERF_TIMER_GUARD(seek_child_seek_time);
        child.Seek(target);
      }
      PERF_COUNTER_ADD(seek_child_seek_count, 1);

      if (child.Valid()) {
        PERF_TIMER_GUARD(seek_min_heap_time);
        minHeap_.push(&child);
      }
    }
    direction_ = kForward;
    {
      PERF_TIMER_GUARD(seek_min_heap_time);
      current_ = CurrentForward();
    }
  }

以当前可写的memtable的iterator为例,当用户调用Seek接口时,如何定位到memtable中的目标key。

MemTableIterator的Seek

在memtable中持有一个MemTableRep::Iterator*类型的iterator指针iter_,iter_指向的对象的实际类型由MemTableRep的子类中分别定义。以SkipListRep为例, 在MemTableIterator的构造函数中,当设置了prefix_seek模式时,调用MemTableRep的GetDynamicPrefixIterator接口来获取具体的iterator对象。

  MemTableIterator(const MemTable& mem, const ReadOptions& read_options,
                   Arena* arena, bool use_range_del_table = false)
      : bloom_(nullptr),
        prefix_extractor_(mem.prefix_extractor_),
        comparator_(mem.comparator_),
        valid_(false),
        arena_mode_(arena != nullptr),
        value_pinned_(!mem.GetMemTableOptions()->inplace_update_support) {
    if (use_range_del_table) {
      iter_ = mem.range_del_table_->GetIterator(arena);
    } else if (prefix_extractor_ != nullptr && !read_options.total_order_seek) {
      bloom_ = mem.prefix_bloom_.get();
      iter_ = mem.table_->GetDynamicPrefixIterator(arena);
    } else {
      iter_ = mem.table_->GetIterator(arena);
    }
  }

对于SkipList来说,GetDynamicPrefixIterator也是调用GetIterator,只有对HashLinkListRep和HashSkipListRe,这个方法才有不同的实现。

  virtual MemTableRep::Iterator* GetIterator(Arena* arena = nullptr) override {
    if (lookahead_ > 0) {
      void *mem =
        arena ? arena->AllocateAligned(sizeof(SkipListRep::LookaheadIterator))
              : operator new(sizeof(SkipListRep::LookaheadIterator));
      return new (mem) SkipListRep::LookaheadIterator(*this);
    } else {
      void *mem =
        arena ? arena->AllocateAligned(sizeof(SkipListRep::Iterator))
              : operator new(sizeof(SkipListRep::Iterator));
      return new (mem) SkipListRep::Iterator(&skip_list_);
    }
  }

这样就创建了MemTableIterator。 当调用到MemTableIterator的Seek接口时:

  • 首先在DynamicBloom成员bloom_中查找目标key的prefix是否可能在当前的memtable中存在。
  • 如果不存在,则一定不存在,可以直接返回。
  • 如果可能存在,则调用MemTableRep的iterator进行Seek。
  virtual void Seek(const Slice& k) override {
    PERF_TIMER_GUARD(seek_on_memtable_time);
    PERF_COUNTER_ADD(seek_on_memtable_count, 1);
    if (bloom_ != nullptr) {
      if (!bloom_->MayContain(
              prefix_extractor_->Transform(ExtractUserKey(k)))) {
        PERF_COUNTER_ADD(bloom_memtable_miss_count, 1);
        valid_ = false;
        return;
      } else {
        PERF_COUNTER_ADD(bloom_memtable_hit_count, 1);
      }
    }
    iter_->Seek(k, nullptr);
    valid_ = iter_->Valid();
  }

当调用MemTableRep的iterator进行Seek时,则是通过调用实际使用的数据结构的Seek接口,找到第一个大于或等于key的目标。

    // Advance to the first entry with a key >= target    virtual void Seek(const Slice& user_key, const char* memtable_key)        override {      if (memtable_key != nullptr) {        iter_.Seek(memtable_key);      } else {        iter_.Seek(EncodeKey(&tmp_, user_key));      }    }

第一个分支主要用于确定在memtable中的key,做update类的操作.

MemTable Prefix Bloom的构建

prefix bloom作为memtable的一个成员,当向memtable插入一个key value时,会截取key的prefix,插入到prefix bloom中。

void MemTable::Add(SequenceNumber s, ValueType type,                   const Slice& key, /* user key */                   const Slice& value, bool allow_concurrent,                   MemTablePostProcessInfo* post_process_info) {  ...  if (!allow_concurrent) {    ...    if (prefix_bloom_) {      assert(prefix_extractor_);      prefix_bloom_->Add(prefix_extractor_->Transform(key));    }    ...  else {    ...    if (prefix_bloom_) {      assert(prefix_extractor_);      prefix_bloom_->AddConcurrently(prefix_extractor_->Transform(key));    }    ...  }  ...}

关于bloom filter的原理和实现可以参考前文。传送门 因为再BlockBasedTable中,对bloom filter的用法不同,所以使用了不同实现的bloom filter。基本原理是相同的。

WAL

对RocksDB的每一次update都会写入两个位置:1) 内存表(内存数据结构,后续会flush到SST file) 2)磁盘中的write ahead log(WAL)。在故障发生时,WAL可以用来恢复内存表中的数据。默认情况下,RocksDB通过在每次用户写时调用fflush WAL文件来保证一致性。

Life Cycle of a WAL

举个例子,RocksDB实例创建了两个column families,分别是 new_cf和default。一旦db被open,就会在磁盘上创建一个WAL来持久化所有的写操作。

DB* db;
std::vector<ColumnFamilyDescriptor> column_families;
column_families.push_back(ColumnFamilyDescriptor(
    kDefaultColumnFamilyName, ColumnFamilyOptions()));
column_families.push_back(ColumnFamilyDescriptor(
    "new_cf", ColumnFamilyOptions()));
std::vector<ColumnFamilyHandle*> handles;
s = DB::Open(DBOptions(), kDBPath, column_families, &handles, &db);

添加一些kv对数据

db->Put(WriteOptions(), handles[1], Slice("key1"), Slice("value1"));
db->Put(WriteOptions(), handles[0], Slice("key2"), Slice("value2"));
db->Put(WriteOptions(), handles[1], Slice("key3"), Slice("value3"));
db->Put(WriteOptions(), handles[0], Slice("key4"), Slice("value4"));

此时,WAL已经记录了所有的写操作。WAL文件会保持打开状态,一直记录后续所有的写,直到WAL 大小达到DBOptions::max_total_wal_size为止。 如果用户决定要flush new_cf中的数据时,会有以下操作:1) new_cf的数据 key1和key3会flush到一个新的SST file。 2)新建一个WAL,后续对所有列族的写都通过新的WAL记录。3)老的WAL不再记录新的写

db->Flush(FlushOptions(), handles[1]);
// key5 and key6 will appear in a new WAL
db->Put(WriteOptions(), handles[1], Slice("key5"), Slice("value5"));
db->Put(WriteOptions(), handles[0], Slice("key6"), Slice("value6"));

此时,会有两个WAL文件,老的WAL会包含key1、key2、key3和key4,新的WAL文件包含key5和key6。因为老的WAL仍然含有default 列族的数据,所以还没有被删除。只有当用户决定flush default列族的数据之后,老的WAL 才会被归档然后从磁盘中删除。

db->Flush(FlushOptions(), handles[0]);
// The older WAL will be archived and purged separetely

总之,在以下情况下会创建一个WAL:

  • 新打开一个DB
  • flush了一个column family。一个WAL文件只有当所有的列族数据都已经flush到SST file之后才会被删除,或者说,所有的WAL中数据都持久化到SST file之后,才会被删除。归档的WAL文件会move到一个单独的目录,后续从磁盘中删除。

WAL Configurations

  • DBOptions::wal_dir : RocksDB保存WAL file的目录,可以将目录与数据目录配置在不同的路径下。
  • DBOptions::WAL_ttl_seconds, DBOptions::WAL_size_limit_MB:这两个选项影响归档WAL被删除的快慢。
  • DBOptions::max_total_wal_size : 为了限制WALs的大小,RocksDB使用该配置来触发 column family flush到SST file。一旦,WALs超过了这个大小,RocksDB会强制将所有列族的数据flush到SST file,之后就可以删除最老的WALs。
  • DBOptions::avoid_flush_during_recovery
  • DBOptions::manual_wal_flush: 这个参数决定了WAL flush是否在每次写之后自动执行,或者是纯手动执行(用户调用FlushWAL来触发)。
  • DBOptions::wal_filter:在恢复数据时可以过滤掉WAL中某些记录
  • WriteOptions::disableWAL: 打开或者关闭WAL支持

WAL LOG File Format

OverView

WAL将内存表的操作记录序列化后持久化存储到日志文件。当DB故障时可以使用WAL 文件恢复到DB故障前的一致性状态。当内存表的数据安全地flush到持久化存储文件中后,对应的WAL log(s)被归档,然后在某个时刻被删除。

WAL Manager

在WAL目录中,WAL文件按照序列号递增命名。为了重建数据库故障之前的一致性状态,必须按照序号号递增的顺序读取WAL文件。WAL manager封装了读WAL文件的操作。WAL manager内部使用Reader or Writer abstraction 来读取WAL file。

Reader/Writer

Writer提供了将log记录append到log 文件的操作接口(内部使用WriteableFile接口)。Reader提供了从log文件中顺序读日志记录的操作接口(内部使用SequentialFile接口)。

Log File Format

日志文件保安了一系列不停长度的记录。Record按照kBlockSize分配。如果一个特定的记录不能完全适配剩余的空间,那么就会将剩余的空间补零。writer按照kBlockSize去写一个数据库,reader按照kBlockSIze去读一个数据库。

       +-----+-------------+--+----+----------+------+-- ... ----+
 File  | r0  |        r1   |P | r2 |    r3    |  r4  |           |
       +-----+-------------+--+----+----------+------+-- ... ----+
       <--- kBlockSize ------>|<-- kBlockSize ------>|

  rn = variable size records
  P = Padding

Record Format

+---------+-----------+-----------+--- ... ---+
|CRC (4B) | Size (2B) | Type (1B) | Payload   |
+---------+-----------+-----------+--- ... ---+

CRC = 32bit hash computed over the payload using CRC
Size = Length of the payload data
Type = Type of record
       (kZeroType, kFullType, kFirstType, kLastType, kMiddleType )
       The type is used to group a bunch of records together to represent
       blocks that are larger than kBlockSize
Payload = Byte stream as long as specified by the payload size

日志文件由连续的32KB大小的block组成。唯一的例外是文件尾部数据包含一个不是完整的block。 每一个block包含连续的Records:

block := record* trailer?
record :=
  checksum: uint32  // crc32c of type and data[]
  length: uint16
  type: uint8       // One of FULL, FIRST, MIDDLE, LAST 
  data: uint8[length]

record type

FULL == 1
FIRST == 2
MIDDLE == 3
LAST == 4

FULL 表示包含了全部用户record记录 FIRST、MIDDLE、LAST用户表示一个record太大然后拆分为多个数据片。FIRST表示是用户record的第一块数据,LAST表示最后一个,MID是用户Record中间部分的数据。 Example

A: length 1000
B: length 97270
C: length 8000

A会在第一个block中存储一个完整的Record。 B会被拆分为3个分片,第一个分片占用了第一个block的剩余全部空间,第二个分片占用第二个block的全部空间,第三个分片占用第三个block的前面部分空间。会在第三个block中预留6个字节,置为空来表示结束。 C将会完整存储在第四个block。FULL Record

WAL Recovery Modes

每个应用都是唯一的,都需要RocksDB保证特定状态的一致性。RocksDB中每一个提交的记录都是持久化的。没有提交的记录保存在WAL file中。当DB正常退出时,在退出之前会提交所有没有提交的数据,所以总是能够保证一致性。当RocksDB进程被kill或者服务器重启时,RocksDB需要恢复到一个一致性状态。最重要的恢复操作之一就是replay所有WAL中没有提交的记录。不同的WAL recovery 模式定义了不同的replay WAL的行为。

kTolerateCorruptedTailRecords

在这种模式下,WAL replay 会忽视日志文件末尾的任何error。这些error主要来源于不安全的进程退出后产生的不完全的写错误日志。这是一种启发式的或者探索式的模式,系统并不能区分是日志文件tail的data corruption还是不完全写操作。任何其他的IO error,都会被视作data corruption。

这种模式被大部分应用使用,这是因为该模式提供了一种比较合理的tradeoff between 不安全退出后重启和数据一致性。

kAbsoluteConsistency

在这种模式下,在replay WAL过程中任何IO 错误都被视为data corruption。这种模式适用于以下场景:应用不能接受丢失任何记录,而且有方法恢复未提交的数据。

kPointInTimeConsistency

在这种模式下,当发生IO errror时,WAL replay会stop,系统恢复到一个满足一致性的时间点上。这种模式适用于有副本的集群,来自另外一个副本的数据可以用来恢复当前的实例。

kSkipAnyCorruptedRecord

在这种模式下,WAL replay会忽视日志文件中的任何错误。系统尝试恢复尽可能多的数据。适用于灾后重建的场景。

MISC

Write Buffer Manager

其实rocksdb整体很多这种插件接口语义,比如Write Buffer Manager, 比如Merge Operator,比如Rate Limiter,sst_file_maniger,eventlistener,用户创建,传进指针(有默认的,也可以继承重写接口)

Write Buffer Manager正如名字,就是控制写入buffer的,这个正是memtable的总大小上限,没有设置的话会有个默认的设定比如两个memtable大小

什么时候会切换memtable?

  • Memtable的大小在一次写入后超过write_buffer_size。
  • 所有列族中的memtable大小超过db_write_buffer_size了,或者write_buffer_manager要求落盘。在这种场景,最大的memtable会被落盘
  • WAL文件的总大小超过max_total_wal_size。在这个场景,有着最老数据的memtable会被落盘,这样才允许携带有跟这个memtable相关数据的WAL文件被删除。
ScheduleFlushes HandleWALFull HandleWriteBufferFull FlushMemTable

条件

  1. 当前线程持有db的大锁

  2. 当前线程是写wal文件的leader

    • 如果开启了enable_pipelined_write选项(写wal和写memtable分开), 那么同时要等到成为写memtable的leader

    • 判断当前wal文件是否为空,如果不为空就创建新的wal文件(recycle_log_file_num选项开启就复用),然后构建新memtable。刷盘当前wal文件
    • 把之前的memtable变成immutable,然后切到新memtable

    • 调用InstallSuperVersionAndScheduleWork函数,这个函数会更新super_version_

memtable

影响memtable的几个选项

影响memtable的最重要的几个选项是:

  • memtable_factory: memtable对象的工厂。通过声明一个工厂对象,用户可以改变底层memtable的实现,并提供事先声明的选项。
  • write_buffer_size:一个memtable的大小
  • db_write_buffer_size:多个列族的memtable的大小总和。这可以用来管理memtable使用的总内存数。
  • write_buffer_manager:除了声明memtable的总大小,用户还可以提供他们自己的写缓冲区管理器,用来控制总体的memtable使用量。这个选项会覆盖db_write_buffer_size
  • max_write_buffer_number:内存中可以拥有刷盘到SST文件前的最大memtable数。
Memtable类型 SkipList HashSkipList HashLinkList Vector
最佳使用场景 通用 带特殊key前缀的范围查询 带特殊key前缀,并且每个前缀都只有很小数量的行 大量随机写压力
索引类型 二分搜索 哈希+二分搜索 哈希+线性搜索 线性搜索
是否支持全量db有序扫描? 天然支持 非常耗费资源(拷贝以及排序一生成一个临时视图) 同HashSkipList 同HashSkipList
额外内存 平均(每个节点有多个指针) 高(哈希桶+非空桶的skiplist元数据+每个节点多个指针) 稍低(哈希桶+每个节点的指针) 低(vector尾部预分配的内存)
Memtable落盘 快速,以及固定数量的额外内存 慢,并且大量临时内存使用 同HashSkipList 同HashSkipList
并发插入 支持 不支持 不支持 不支持
带Hint插入 支持(在没有并发插入的时候) 不支持 不支持 不支持

Direct IO

关于buffered IO和DirectIO可以看参考链接3, 这个图不错,偷过来

rocksdb属于自缓存应用,有memtable+blockcache来保证了一定的局部性。本身已经实现了缓存算法(LRU,clock)可以不使用系统的页缓存,使用DirectIO,在options中有配置

  // Use O_DIRECT for user reads
  // Default: false
  // Not supported in ROCKSDB_LITE mode!
  bool use_direct_reads = false;

  // Use O_DIRECT for both reads and writes in background flush and compactions
  // When true, we also force new_table_reader_for_compaction_inputs to true.
  // Default: false
  // Not supported in ROCKSDB_LITE mode!
  bool use_direct_io_for_flush_and_compaction = false;

对于use_direct_io_for_flush_and_compaction, compaction有个compaction block cache 不建议用,具体可以参考链接4

Rate Limiter限流器

Rocksdb内置控制写入速率降低延迟的,当然包括compaction速率。如果compaction导致了较多毛刺,考虑一下是不是没使用limiter

通过调用NewGenericRateLimiter创建一个RateLimiter对象,可以对每个RocksDB实例分别创建,或者在RocksDB实例之间共享,以此控制落盘和压缩的写速率总量。

RateLimiter* rate_limiter = NewGenericRateLimiter(
    rate_bytes_per_sec /* int64_t */, 
    refill_period_us /* int64_t */,
    fairness /* int32_t */);

参数:

  • rate_bytes_per_sec:通常这是唯一一个你需要关心的参数。他控制压缩和落盘的总速率,单位为bytes/秒。现在,RocksDB并不强制限制除了落盘和压缩以外的操作(如写WAL)
  • refill_period_us:这个控制令牌被填充的频率。例如,当rate_bytes_per_sec被设置为10MB/s然后refill_period_us被设置为100ms,那么就每100ms会从新填充1MB的限量。更大的数值会导致突发写,而更小的数值会导致CPU过载。默认数值100,000应该在大多数场景都能很好工作了
  • fairness:RateLimiter接受高优先级请求和低优先级请求。一个低优先级任务会被高优先级任务挡住。现在,RocksDB把来自压缩的请求认为是低优先级的,把来自落盘的任务认为是高优先级的。如果落盘请求不断地过来,低优先级请求会被拦截。这个fairness参数保证低优先级请求,在即使有高优先级任务的时候,也会有1/fairness的机会被执行,以避免低优先级任务的饿死。默认是10通常是可以的。

尽管令牌会以refill_period_us设定的时间按照间隔来填充,我们仍然需要保证一次写爆发中的最大字节数,因为我们不希望看到令牌堆积了很久,然后在一次写爆发中一次性消耗光,这显然不符合我们的需求。GetSingleBurstBytes会返回这个令牌数量的上限。

这样,每次写请求前,都需要申请令牌。如果这个请求无法被满足,请求会被阻塞,直到令牌被填充到足够完成请求。比如:

// block if tokens are not enough
rate_limiter->Request(1024 /* bytes */, rocksdb::Env::IO_HIGH); 
Status s = db->Flush();

如果有需要,用户还可以通过SetBytesPerSecond动态修改限流器每秒流量。参考include/rocksdb/rate_limiter.h 了解更多细节

对那些RocksDB提供的原生限流器无法满足需求的用户,他们可以通过继承include/rocksdb/rate_limiter.h 来实现自己的限流器。

memory

主要几大块, blockcache,memtable,index & filter block, pinned by iterator

memtable也可以被write buffer manager来控制

table_options.block_cache->GetPinnedUsage()获得第四个

文件的快速导入rocksdb::SstFileWriter/ IngestExternalFile

具体可以看这篇文档https://github.com/facebook/rocksdb/wiki/Creating-and-Ingesting-SST-files

经常用于文件导入,快速导入,调用write接口还是太慢,直接生成L0sst更快。但是存在问题,L0文件过多,触发compaction,所以要先停掉compaction,这点在rockset使用该功能的时候分享过。这里标记备忘

Arena

分配器需要做什么

  • 状态,这里的分配器不需要实时的回收,也就仅仅是分配器,不管回收,生命周期内析构就可以了
  • 分配,尽可能的分配成功

看看arena的api什么样的

class Arena {
 public:
  Arena();
  ~Arena();
  // Return a pointer to a newly allocated memory block of "bytes" bytes.
  char* Allocate(size_t bytes);
  // Allocate memory with the normal alignment guarantees provided by malloc
  char* AllocateAligned(size_t bytes);
  // Returns an estimate of the total memory usage of data allocated
  // by the arena.
  size_t MemoryUsage() const {
    return reinterpret_cast<uintptr_t>(memory_usage_.NoBarrier_Load());
  }
 private:
  char* AllocateFallback(size_t bytes);
  char* AllocateNewBlock(size_t block_bytes);
  // Allocation state
  char* alloc_ptr_;
  size_t alloc_bytes_remaining_;
  // Array of new[] allocated memory blocks
  std::vector<char*> blocks_;
  // Total memory usage of the arena.
  port::AtomicPointer memory_usage_;
  Arena(const Arena&);
  void operator=(const Arena&);
};

能看出来就是个二维动态数组,vector<char*>,每次分一个数组,也就是block,如果不够用了在分配一个新的block

Allocate会调用AllocateFallBack和AllocateNewBlock 完全取决于剩余内存和新分配内存的关系

  1. 如果需求的内存小于剩余的内存,那么直接在剩余的内存分配就可以了;
  2. 如果需求的内存大于剩余的内存,而且大于4096/4,则给这内存单独分配一块bytes(函数参数)大小的内存。
  3. 如果需求的内存大于剩余的内存,而且小于4096/4,则重新分配一个内存块,默认大小4096,用于存储数据。

rocksdb的优化做了哪些工作

抽象出allocator api,以及支持hugepage

char* Arena::AllocateFromHugePage(size_t bytes)

###

reference

Read More

c++中文件操作的坑

  • 使用ofstream读写文件,记得一定要关闭,否则同进程看不到这个文件的修改内容
std::ofstream f(config_file);
f.write(content.data(), content.size());
// f.close(); // missing!
SomeConfig.Load(config_file); // error, it's empty!

当然,更推荐scope_guard 或者gsl::finally来管理

  • stream的继承关系
void foo(const std::istream&) {
    puts("istream");
}
void foo(const std::ifstream&) {
    puts("ifstream");
}
int main() {
    std::fstream t;
    foo(t);
}

猜猜调用那个?第一个

继承关系,深坑

  • stream是有状态的

  • 占用很大

using namespace std;
cout<<"std::fstream = "<<sizeof(fstream)<<endl
  <<"std::ifstream = "<<sizeof(ifstream)<<endl
  <<"std::ofstream = "<<sizeof(ofstream)<<endl;

  //  std::fstream = 528
  //  std::ifstream = 520
  //  std::ofstream = 512
  • 检验文件是否存在 ifstream并不怎么快。不过确实挺好用的
#include <sys/stat.h>
#include <unistd.h>
#include <string>
#include <fstream>

inline bool exists_test0 (const std::string& name) {
    ifstream f(name.c_str());
    return f.good();
}

inline bool exists_test1 (const std::string& name) {
    if (FILE *file = fopen(name.c_str(), "r")) {
        fclose(file);
        return true;
    } else {
        return false;
    }   
}

inline bool exists_test2 (const std::string& name) {
    return ( access( name.c_str(), F_OK ) != -1 );
}

inline bool exists_test3 (const std::string& name) {
  struct stat buffer;   
  return (stat (name.c_str(), &buffer) == 0); 
}
ifstream 0.485s
FILE fopen 0.302s
posix access() 0.202s
posix stat() 0.134s

ref

  • https://quuxplusone.github.io/blog/2018/11/26/remember-the-ifstream/
  • https://zhuanlan.zhihu.com/p/90194868
  • 检查文件的benchmark代码在这里https://stackoverflow.com/questions/12774207/fastest-way-to-check-if-a-file-exist-using-standard-c-c11-c
  • finally https://www.bfilipek.com/2017/04/finalact-follow-up.html

Read More

^