【IT168 评论】之前简单的看了一下 Tokyo Tyrant(包括 Tokyo Cabint) 在 hash 存储上的一些实现,最近 Redis 又比较火热,因此,自己也尝试性的去了解了一下 Redis,并且结合 Tokyo Tyrant(以下简称 tt server),说说自己对这两种产品的看法。
目录
- 服务端处理模型
- 数据存储方式、持久化比较
- 复制方式比较
- 性能方面比较
- 总结
服务端处理模型
在 tt server 中,是以多线程的方式向客户端提供服务的:一个主线程负责 accept 客户端的socket,一定数目的线程(可以指定)进行读写服务,同时,也有一定数目的timer线程,专门用来负责定时的任务,比如一些定时的 Lua 脚本,同时,如果是slaver,则会有专门一个timer线程,定时负责 do slave 的工作。
而在 Redis 中,采用的则是单线程的模型来处理所有的客户端请求。
应该说这两种模型,都有各自的优点和缺点。多线程可以利用多核CPU的计算能力,但因此也会增加CAS自旋或者是锁的一些消耗,同时,如果线程过多,那么线程之间上下文的切换,也是一种消耗。
而如果是单线程,则可以完全避免锁的消耗,同时,上下文切换消耗也不需要过多的考虑(但仍需要考虑系统上还有其他的进程),这会让单个CPU的利用率比较高。
但是,单线程服务,就意味着不能利用多核。同时,服务端对客户端过来的请求是串行执行和响应的,这也在一定程度上,会影响服务端的并发能力,特别是在有些请求执行比较耗时的情况下。想象一下,就这么一个线程,可能正在拼命的执行客户端A的一个请求,而此时客户端B,C,D的请求,还仍在等着线程执行完成之后再去搭理他们。
因此,像 redis 这种单线程的服务模型,如果对一些请求的处理相对较耗时,那其 TPS 也就相应的不能提高上去,也就是说其吞吐量会提不上去;但反过来想,redis 如果能控制每次请求在执行过程是简短并且快速的,那么也许使用单线程,反而会比多线程有更好的性能,毕竟单线程少了上下文切换,以及锁或者 cas 的开销。
而 tt server 则中规中矩:一个线程负责 accept ,一定数目的线程则进行请求的处理。因此,我们在设置 tt server 的时候,也应尽量考虑好工作线程的数目,尽量让CPU数目与工作线程数目一致或者略少。原则是最好的发挥多核CPU的作用,同时又不让工作线程之间去竞争 CPU。当然,这是需要不停的去实验的。
所以,在使用 redis 的时候,应尽量不要去使用一些相对耗时的请求;同时,我想 redis 的作者,也应该会尽量优化每种请求的执行速度(至少是一些常用的请求)。
而在使用 tt server 的时候,需要仔细调整使用的工作线程数目,让每个CPU都物尽其用。
数据存储方式、持久化比较
tt server 的 hash 数据库,是使用文件的方式,然后利用 mmap 系统调用映射到内存中。
这样,就可以利用操作系统的机制,不定期地将数据 flush 到磁盘中。同时,tt server 也提供了 sync 命令,可以让客户端手动将数据 flush 到磁盘中(使用 msync 系统调用)。最后,在关闭 tt server 进程的时候,应该使用 kill -15(TERM信号),或者使用 ttserver 自带的命令:ttserver -kl pid 进行关闭。这样 ttserver 会先把数据 flush 到磁盘上,再退出进程。
同时, tt server 也提供了 ulog 的方式,对数据库的变更操作进行记录,同样,可以利用 ulog 对 ttserver 进行恢复,但 ulog 的主要目的,按照我的理解,应是用来实现 replication 的。
而 redis 则是将数据直接写在了内存中,然后利用 redis 的持久化机制,将数据写到磁盘中。
redis 提供了两种持久化机制,分别是 RDB (redis DB) 和 AOF (appending only file)。
RDB的过程是:redis 进程 fork 一个子进程,然后子进程对内存中的数据写到一个临时文件,这个时候,两个进程就利用了操作系统的 copy on write 机制,共享一份内存数据,只有当父进程(也就是 redis 进程)对原有的数据进行修改或者删除之后,操作系统才为 redis 进程重新开辟新的内存空间(以页为单位)。Redis 本身也提供了 bgsave(background save) 命令支持手动将数据持久化( save 命令是同步的,而 redis 只有一个线程在服务,结果就是影响 redis 的性能,特别是在大数据量的情况下)。
AOF的过程是:在执行每次命令之后,或者每隔1秒钟之后,Redis会有一个线程将命令以 redis 协议的格式 append 到文件中,这也就是AOF名字的由来,这些命令当然是非只读的,只读不更改数据库,没有必要记录下来。
这里会有两个问题:
1、每次命令之后写文件,还是隔1秒之后写文件,影响会有哪些?
2、这些文件总会不断的膨胀,如何对文件进行压缩呢?
对于第一个问题,也是一个权衡的问题,如果每次命令之后都进行一次写磁盘操作,那么IO的程度可想而知,肯定会影响服务器性能(使用 write 系统调用,会因为文件系统而进入 page buffer,并非立刻写磁盘,而调用 fsync ,则会将 page buffer 中的数据写入磁盘,进行 IO 操作)。而如果每隔1秒进行一次 fsync,那么在这一秒和上一秒之间,如果服务器突然断电,那很有可能这些数据就会丢失。对于这个问题,redis 默认给出的方案是每隔1秒进行一次write。对于1秒的给定,我想,也是基于性能和数据安全的权衡,在性能和数据安全方面都可以让人接受。
对于第二个问题,redis 提供了 rewrite 的机制:当 aof 过大的时候,redis可以自动的进行 rewrite (从 redis 2.4 开始)。rewrite 的过程也是 fork 一个子进程;然后打开一个临时文件,将内存中的数据写入到文件中;在此期间,主进程继续将数据写入老的 aof 文件,同时也会将数据写入到一个内存缓存中;等子进程完成之后,主进程会将缓存中的数据写入到临时文件,再将临时文件进行rename,替换掉原来的文件。这样,就实现了写 aof 过程中的rewrite。
从数据的存储方式来说,尽管 tt server 和 redis 都是在内存上面进行数据的读写,我但认为两个产品对数据存储方式的观点是不一样的。
tt server 是将磁盘上的文件当作主要的存储方式,然后使用 mmap 将文件映射到内存中。本质上,这是数据应该存储在磁盘中的观点。
而 redis ,一开始就是将数据直接存储在内存中,在之后的持久化过程中,可以理解成只是将数据的日志写入到磁盘中。本质上,这是把数据应该存储在内存中的观点。
可见,由于作者的观点不一样,也就造成了两种实现方式不一样的产品,这还是比较有意思的。
从这个层面上来讲,我更加喜欢 redis 作者的思路,很可能作者就是受到 内存是新的磁盘,磁盘是新的磁带 的启发。
redis自带实现的VM将在以后不再使用(2.4将是最后一个自带vm功能的版本),作者认为数据就应该是放在物理内存中的,没有必要要将数据交换到磁盘中,磁盘只是作为日志的一种存储方式。这也是“内存是新的硬盘”思路的体现。