Permalink
Browse files

revise server start

  • Loading branch information...
1 parent e3b2fb5 commit f3e204087d43994f27ca610d796bd9b631221f01 @hoterran hoterran committed Aug 4, 2012
Showing with 381 additions and 16 deletions.
  1. +41 −0 1
  2. +12 −0 README.md
  3. +2 −0 redis-ae.md
  4. +3 −1 redis-directory-intro.md
  5. +176 −0 redis-memory.md
  6. +3 −3 redis-protocol.md
  7. +3 −0 redis-pubsub.md
  8. +97 −3 redis-server-data-structure.md
  9. +44 −9 redis-server-start.md
View
41 1
@@ -0,0 +1,41 @@
+#简介
+
+
+Redis(REmote DIctionary Server)是一个开源的键-值内存数据库,与它类似的有 memcached,Tokyo Cabinet 等。Redis 以支持丰富的数据结构著称,同时兼具主从复制、持久化等高可用特性,与程序无缝的结合,
+
+
+##无第三方库依赖
+
+memcached 使用 libevent 这个已经不那么轻量级的网络事件库,而 Redis 本身不依赖任何第三方的函数库,无论是网络事件、哈希表,数据结构都是自己实现的,全部代码只有 2w 行,算是一个小型的项目,代码清晰,阅读起来非常的流畅,甚至都无须 debug 调试来辅助理解。
+
+
+##从哪里开始
+
+本书将对其的源代码进行分析,版本为2.4.16。下载源代码解压 redis.tar.gz 包后,进入``redis``目录,我们从``src/Redis.c``的主函数开始我们代码旅行。
+
+推荐读者使用 cscope 这样可以很方便的从函数之间跳转,如何使用 cscope 可见附录1,如何设置vim的快捷键可见附录2。
+
+
+##为何redis是单线程?
+
+由于支持复杂的数据结构,所以如果采用多线程,将非常的麻烦,试想一个双链表,要支持多线程将多么的复杂。
+
+Redis 并不是一个``fit all``的键-值数据库,单线程意味着任何一个客户端连接的处理速度影响的全局的性能,所以一些较消耗性能的操作(set的操作或者zset的排序操作)都尽量移动到备库来处理。
+
+受内存限制的特点使得目前Redis不能成为处理海量数据的total solutions,而仅仅是一个复杂大型系统里的一部分。
+
+
+##缺陷
+
+Redis 有很多缺陷,比如无法做到双主库 ,无法进行同步复制(当然这些都是可以改进的)。复制做增量复制,复制容易受网络的影响, 无完美的集群方案,但这不影响 Redis 成为一个优秀的可信赖的组件。
+
+
+#同类项目
+
+##memcached
+
+##tc
+
+#趋势
+
+
View
12 README.md
@@ -1 +1,13 @@
# Redis源代码分析系列文章
+
+
+
+
+#TODO
+ 1. 原来图片都是用dia,xmind,gliffy画的,准备全部改成graphviz,工程浩大...
+
+
+
+#附录
+ 1. 利用cscope和vim看代码
+ 2. graphviz的使用
View
2 redis-ae.md
@@ -0,0 +1,2 @@
+
+Redis 处理的比较巧妙。先执行 aeSearchNearestTimer 确定距离下次时间事件执行还有多少时间,假设第一次执行直到下次时间事件还有100ms,先执行文件事件,epool_wait 的超时时间就设置为 100ms,如果 10ms后,有网络交互后经过一系列的处理后消耗 20ms,该次循环结束。aeSearchNearestTimer 会再次计算距离下次时间事件的间隔为 100 - 10 - 20 = 70ms,于是 epoll_wait 的超时时间为 70ms,70ms之内如果没有处理文件事件,则执行时间事件。这样即保证了即时处理文件事件,在文件事件处理完毕后又能按时处理时间事件。
View
4 redis-directory-intro.md
@@ -20,6 +20,8 @@
##Makefile
+连``automake``都未使用,够简洁的。
+
``redis-server``,``redis-client``是编译之后的关键二进制,前者是服务端,后者是官方出品的客户端软件。
##redis.conf
@@ -37,7 +39,7 @@ redis.conf 是``redis-server``的启动配置文件, 后面有一章节会单独
###hiredis
-Redis 的c api,编译 Redis 官方客户端``redis-client``,工具``redis-checkaof``都需要使用它。
+Redis 的 c api,编译 Redis 官方客户端``redis-client``,工具``redis-checkaof``都需要使用它。
api 包含了处理网络的``net.c``,包含多种多路复用的``adapters``目录,动态字符串``sds.c``,处理哈希结构的``dict.c``
View
176 redis-memory.md
@@ -48,3 +48,179 @@ update_zmalloc_stat_alloc 会记录全局的内存申请状况 (used_memory),
如果发现这个数字的大小,正好在这个范围内(0 - 1000),那么就可以重用这个数字,而不需要动态的 malloc 一个对象了。这种使用引用技术的不变类的方法,在很多虚拟机语言里也常被使用,利用Python,java。
+
+
+
+##如何评估内存的使用大小?
+
+redis是个内存全集的kv数据库,不存在部分数据在磁盘部分数据在内存里的情况,所以提前预估和节约内存非常重要.本文将以最常用的string和zipmap两类数据结构在jemalloc内存分配器下的内存容量预估和节约内存的方法.
+
+
+先说说jemalloc,传说中解决firefox内存问题freebsd的默认malloc分配器,area,thread-cache功能和tmalloc非常的相识.在2.4版本被redis引入,在antirez的博文中提到内节约30%的内存使用.相比glibc的malloc需要在每个内存外附加一个额外的4字节内存块,jemalloc可以通过je_malloc_usable_size函数获得指针实际指向的内存大小,这样redis里的每个key或者value都可以节约4个字节,不少阿.
+
+下面是jemalloc size class categories,左边是用户申请内存范围,右边是实际申请的内存大小.这张表后面会用到.
+
+1 - 4 size class:4
+5 - 8 size class:8
+9 - 16 size class:16
+17 - 32 size class:32
+33 - 48 size class:48
+49 - 64 size class:64
+65 - 80 size class:80
+81 - 96 size class:96
+97 - 112 size class:112
+113 - 128 size class:128
+129 - 192 size class:192
+193 - 256 size class:256
+257 - 320 size class:320
+321 - 384 size class:384
+385 - 448 size class:448
+449 - 512 size class:512
+513 - 768 size class:768
+769 - 1024 size class:1024
+1025 - 1280 size class:1280
+1281 - 1536 size class:1536
+1537 - 1792 size class:1792
+1793 - 2048 size class:2048
+2049 - 2304 size class:2304
+2305 - 2560 size class:2560
+STRING
+string类型看似简单,但还是有几个可优化的点.先来看一个简单的set命令所添加的数据结构.
+
+
+
+一个set hello world命令最终(中间会malloc,free的我们不考虑)会产生4个对象,一个dictEntry(12字节),一个sds用于存储key,还有一个redisObject(12字节),还有一个存储string的sds.sds对象除了包含字符串本生之外,还有一个sds header和额外的一个字节作为字符串结尾共9个字节.
+
+sds.c
+========
+ 51 sds sdsnewlen(const void *init, size_t initlen) {
+ 52 struct sdshdr *sh;
+ 53
+ 54 sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
+
+sds.h
+=======
+ 39 struct sdshdr {
+ 40 int len;
+ 41 int free;
+ 42 char buf[];
+ 43
+};
+根据jemalloc size class那张表,这个命令最终申请的内存为16(dictEtnry) + 16 (redisObject) + 16(“hello”) + 16(“world”),一共64字节.注意如果key或者value的字符串长度+9字节超过16字节,则实际申请的内存大小32字节.
+
+提一下string常见的优化方法
+
+尽量使VALUE为纯数字
+
+这样字符串会转化成int类型减少内存的使用.
+
+redis.c
+=========
+37 void setCommand(redisClient *c) {
+38 c->argv[2] = tryObjectEncoding(c->argv[2]);
+39 setGenericCommand(c,0,c->argv[1],c->argv[2],NULL);
+40 }
+object.c =======
+275 o->encoding = REDIS_ENCODING_INT;
+276 sdsfree(o->ptr);
+277 o->ptr = (void*) value;
+可以看到sds被释放了,数字被存储在指针位上,所以对于set hello 1111111就只需要48字节的内存.
+
+调整REDIS_SHARED_INTEGERS
+
+如果value数字小于宏REDIS_SHARED_INTEGERS(默认10000),则这个redisObject也都节省了,使用redis Server启动时的share Object.
+
+object.c
+=======
+269 if (server.maxmemory == 0 && value >= 0 && value < REDIS_SHARED_INTEGERS &&
+270 pthread_equal(pthread_self(),server.mainthread)) {
+271 decrRefCount(o);
+272 incrRefCount(shared.integers[value]);
+273 return shared.integers[value];
+274 }
+这样一个set hello 111就只需要32字节,连redisObject也省了.所以对于value都是小数字的应用,适当调大REDIS_SHARED_INTEGERS这个宏可以很好的节约内存.
+
+出去kv之外,dict的bucket逐渐变大也需要消耗内存,bucket的元素是个指针(dictEntry**), 而bucket的大小是超过key个数向上求整的2的n次方,对于1w个key如果rehash过后就需要16384个bucket.
+
+开始string类型的容量预估测试, 脚本如下
+
+#! /bin/bash
+
+redis-cli info|grep used_memory:
+
+for (( start = 10000; start < 30000; start++ ))
+do
+ redis-cli set a$start baaaaaaaa$start > /dev/null
+done
+
+redis-cli info|grep used_memory:
+根据上面的总结我们得出string公式
+
+string类型的内存大小 = 键值个数 * (dictEntry大小 + redisObject大小 + 包含key的sds大小 + 包含value的sds大小) + bucket个数 * 4
+
+下面是我们的预估值
+
+>>> 20000 * (16 + 16 + 16 + 32) + 32768 * 4
+1731072
+运行一下测试脚本
+
+hoterran@~/Projects/redis-2.4.1$ bash redis-mem-test.sh
+used_memory:564352
+used_memory:2295424
+计算一下差值
+
+>>> 2295424 - 564352
+1731072
+都是1731072,说明预估非常的准确, ^_^
+
+ZIPMAP
+这篇文章已经解释zipmap的效果,可以大量的节约内存的使用.对于一个普通的subkey和value,只需要额外的3个字节(keylen,valuelen,freelen)来存储,另外的hash key也只需要额外的2个字节(zm头尾)来存储subkey的个数和结束符.
+
+
+
+zipmap类型的内存大小 = hashkey个数 * (dictEntry大小 + redisObject大小 + 包含key的sds大小 + subkey的总大小) + bucket个数 * 4
+
+开始容量预估测试,100个hashkey,其中每个hashkey里包含300个subkey, 这里key+value的长度为5字节
+
+#! /bin/bash
+
+redis-cli info|grep used_memory:
+
+for (( start = 100; start < 200; start++ ))
+do
+ for (( start2 = 100; start2 < 400; start2++ ))
+ do
+ redis-cli hset test$start a$start2 "1" > /dev/null
+ done
+done
+
+redis-cli info|grep used_memory:
+这里subkey是同时申请的的,大小是300 * (5 + 3) + 2 =2402字节,根据上面jemalloc size class可以看出实际申请的内存为2560.另外100hashkey的bucket是128.所以总的预估大小为
+
+>>> 100 * (16 + 16 + 16 + 2560) + 128 * 4
+261312
+运行一下上面的脚本
+
+hoterran@~/Projects/redis-2.4.1$ bash redis-mem-test-zipmap.sh
+used_memory:555916
+used_memory:817228
+计算一下差值
+
+>>> 817228 - 555916
+261312
+是的完全一样,预估很准确.
+
+另外扯扯zipmap的一个缺陷,zipmap用于记录subkey个数的zmlen只有一个字节,超过254个subkey后则无法记录,需要遍历整个zipmap才能获得subkey的个数.而我们现在常把hash_max_zipmap_entries设置为1000,这样超过254个subkey之后每次hset效率都很差.
+
+354 if (zm[0] < ZIPMAP_BIGLEN) {
+355 len = zm[0]; //小于254,直接返回结果
+356 } else {
+357 unsigned char *p = zipmapRewind(zm); //遍历zipmap
+358 while((p = zipmapNext(p,NULL,NULL,NULL,NULL)) != NULL) len++;
+359
+360 /* Re-store length if small enough */
+361 if (len < ZIPMAP_BIGLEN) zm[0] = len;
+362 }
+简单把zmlen设置为2个字节(可以存储65534个subkey)可以解决这个问题,今天和antirez聊了一下,这会破坏rdb的兼容性,这个功能改进推迟到3.0版本,另外这个缺陷可能是weibo的redis机器cpu消耗过高的原因之一.
+
+
View
6 redis-protocol.md
@@ -2,14 +2,14 @@
下面的协议是以一个 ”get a“ 这样的一个命令作为例子讲解,方便理解。
-Redis的协议是纯ascii协议,没有任何二进制东西,牺牲了效率,牺牲了解析代码量,但方便了诊断,方便理解。
+Redis 的协议是纯文本协议,没有任何二进制东西,牺牲了效率,牺牲了解析代码量,但方便了诊断,方便理解。
你可以通过 telnet 或者 Redis_cli、利用 lib 库发送请求给 Redis server。
前者的是一种裸协议的请求发送到服务端,而后两者会对键入的请求进行协议组装帮助更好的解析(常见的是长度放到前头,还有添加阿协议类型)。
-下面这个图涵盖了接收request,处理请求,调用函数,发送reply的过程。
+下面这个图涵盖了接收请求,处理请求,调用函数,发送reply的过程。
![protocol](https://raw.github.com/redisbook/book/master/image/redis_protocol_command.png)
@@ -55,7 +55,7 @@ bulk replies是以$打头消息体,格式$值长度\r\n值\r\n,一般的get
Redis>get aaa
$3\r\nbbb\r\n
-对应的的处理函数addReplyBulk
+对应的的处理函数 addReplyBulk
addReplyBulkLen(c,obj);
addReply(c,obj);
View
3 redis-pubsub.md
@@ -0,0 +1,3 @@
+#pubsub
+
+![pubsub]()
View
100 redis-server-data-structure.md
@@ -1,16 +1,110 @@
##数据结构
-![redis data structure](https://raw.github.com/redisbook/book/master/image/redis_db_data_structure.png)
+![Redis data structure](https://raw.github.com/Redisbook/book/master/image/Redis_db_data_structure.png)
+
+每个 key-value 的数据都会存储在 RedisDb 这个结构里,而 RedisDb 就是一个 hash table。
+
+
+
+
+
+###尽力节省空间
+
+REDIS_STRING 和REDIS_ENCODING_RAW,假设这些数据都要存储在 Redis 内部了,这个时候全字符串肯定不是最优的存储方法。于是需要尝试的转换格式,比如“1”就应该转化成long或者longlong类型
+
+ /* Try to encode a string object in order to save space */
+ robj *tryObjectEncoding(robj *o) {
+ /* Check if we can represent this string as a long integer */
+ if (isStringRepresentableAsLong(s,&value) == REDIS_ERR) return o;
+ /* Ok, this object can be encoded...
+ if (server.maxmemory == 0 && value >= 0 && value < REDIS_SHARED_INTEGERS &&
+ pthread_equal(pthread_self(),server.mainthread)) {
+ decrRefCount(o);
+ incrRefCount(shared.integers[value]);
+ return shared.integers[value];
+ } else {
+ o->encoding = REDIS_ENCODING_INT;
+ sdsfree(o->ptr);
+ o->ptr = (void*) value;
+ return o;
+ }
+ }
+
+如果处于共享区域,则自增加1,否则转化成INT类型。释放老的string类型,指向新的long或者longlong类型。
+
+
+###如何存储
+
+
+例如 "set a 1" 会创建3 个 argv, 如果数据保留了,则 1 都会incrRefCount,而不set,a 都会被删除掉
+在前面可以看到全局的key是以sds形式存储的,dictAdd的时候会拷贝一份,所以a对应的object也可以删除掉,而1对应的object必须保存,这就是数据阿。
+
+
+###伪代码
+
+ processInputBuffer
+ ProcessMultibulkBuffer
+ while
+ c->argv[c->argc++] = createStringObject(c->querybuf+pos,c->bulklen);
+
+ call
+ c->argv[2] = tryObjectEncoding(c->argv[2]);
+ incrRefCount(val);
+
+ resetClient
+ freeClientArgv
+ for
+ decrRefCount(c->argv[j]);
+ c->argc = 0;
+
+###表格?
+
+type
+encoding1
+encoding2
+condtion
+REDIS_STRING
+REDIS_ENCODING_RAW
+REDIS_ENCODING_INT
+
+REDIS_LIST
+REDIS_ENCODING_ZIPLIST
+REDIS_ENCODING_LINKEDLIST
+
+REDIS_SET
+REDIS_ENCODING_INTSET
+REDIS_ENCODING_HT
+
+REDIS_ZSET
+
+
+
+REDIS_HASH
+REDIS_ENCODING_HT
+REDIS_ENCODING_ZIPMAP
+
+
+ziplist是用来代替双链表,非常的节省内存
+<zlbytes><zltail><zllen><entry><entry>....<zlend>
+zlbytes是到zlend的距离
+zllen entry的个数
+zltail是最后一个entry的offset
+zlend是个单字节的值,等于255,暗示链表的结尾。
+
+String
+List
+Set
+zset
+Hash
-每个 key-value 的数据都会存储在 redisDb 这个结构里,而 redisDb 就是一个 hash table。
###字符串
从图上我们可以看出 key 为”hello”,value 为 ”world” 的存储格式。
###列表
-key 为 ”list“,value为一个字符串链表([“aaa”,”bbb”,”ccc”])的存储型式,
+key 为 ”list“,value为一个字符串链表(“aaa”,”bbb”,”ccc”)的存储型式,
###zset
View
53 redis-server-start.md
@@ -1,22 +1,57 @@
-#主框架
+#redis-server启动的主流程
-##主要流程
-
+我们从 redis-server(redis.c) 启动说起,从 main 函数开始遍历一下各个关键函数,先了解 Redis 主框架。
![Redis-server](https://raw.github.com/redisbook/book/master/image/redis_server.png)
-我们从 redis-server 启动说起,从 main 函数开始遍历一下各个关键函数,先了解 Redis 主框架。
-首先 initServerConfig 函数会设置一些默认的参数,比如监听端口为 6379 ,默认的 db 个数为 16 等。PopulateCommandTable 会把命令和函数数组转化成哈希表结构,这个后面会详细描述。如果启动参数里有 redis.conf,LoadServerConfig 还会读入redis.conf里的参数,覆盖默认值。
+##initServerConfig
+
+首先``initServerConfig``函数会设置一些默认的参数,比如监听端口为 6379 ,默认的内置 db 个数为 16 等。默认值对于一个用户友好的软件非常重要,谁愿意第一次使用软件还要设置一大堆云里雾里的参数呢?所有的参数后面会详细讲述。
+
+``PopulateCommandTable``会把命令与函数数组``readonlyCommandTable``转化成哈希表结构,例如用户键入了``set a 1``这个命令到服务端,服务端解析协议后知道了``set`` 这个命令,就可以找到``setCommand``这个相应的处理函数,这个后面会详细描述。
+
+
+##loadServerConfig
+
+如果``redis-server``启动参数里有``redis.conf````LoadServerConfig``就会读入``redis.conf``里的参数,覆盖默认值。
+
+
+##initServer
+
+ * 利用``signal``屏蔽一些信号,设置一些信号处理函数``setupSignalHandlers``
+
+ * ``struct RedisServer`` 这个数据结构做初始化,这个结构是``redis-server``最重要的结构。
+
+ * createSharedObjects
+
+ * aeCreateEventLoop 创建多路服务的文件、事件事件管理器。
+
+ * anetTcpServer 启动 6379 端口的监听。
+
+ * anetUnixServer 启动unix socket 的监听,本文不讲解。
+
+ * 添加文件和时间事件。
+ 1. 添加一个时间事件,函数是``serverCron``。这个函数会每 100ms 执行一次,后面会详细描述这个函数的作用。
+ 2. 添加一个监听的文件事件,把 accept 行为注册到只读的监听文件描述符上。
+
+ * slowlogInit 启动慢日志功能,发现比较慢的命令。
+
+ * bioInit 启动后台线程来处理耗时的操作。
+
+
+##aeMain(el)
+
+这就是 Redis 的主循环。
+
+每次循环之前还会执行``beforeSleep``
+
+然后开始循环,这个循环目前每隔 100ms 会执行一次``serverCron``函数,并仅仅盯着监听的 fd,等待外部的连接。
-initServer会给 RedisServer 这个数据结构做初始化,申请各自成员的空间,有些是 list 结构,有些是 dict 结构。然后添加一个时间事件,函数是 serverCron。这个函数会每 100ms 执行一次,后面会详细描述这个函数的作用。然后是启动监听,注册一个监听的文件事件,把 accept 行为注册到只读的监听文件描述符上。然后如果有激活 aof 功能,还会打开 aof 文件。接着会判断数据目录是否存在镜像文件或者 aof 文件,如果存在,Redis会讲数据载入到内存中。然后进入主循环。
-##文件事件, 时间事件
-主循环主要处理刚才注册的时间事件和文件事件。如何保证时间事件每 100ms 执行一次,又能即时的处理网络交互的文件事件呢?
-Redis 处理的比较巧妙。先执行 aeSearchNearestTimer 确定距离下次时间事件执行还有多少时间,假设第一次执行直到下次时间事件还有100ms,先执行文件事件,epool_wait 的超时时间就设置为 100ms,如果 10ms后,有网络交互后经过一系列的处理后消耗 20ms,该次循环结束。aeSearchNearestTimer 会再次计算距离下次时间事件的间隔为 100 - 10 - 20 = 70ms,于是 epoll_wait 的超时时间为 70ms,70ms之内如果没有处理文件事件,则执行时间事件。这样即保证了即时处理文件事件,在文件事件处理完毕后又能按时处理时间事件。
时间事件 serverCron 会处理很多函数,例如定时打出日志展现 Redis 目前的状况,查看是否需要 rehash 来迁移 keys 到新的 bucket,这个后面会详细讲。关闭长时间不工作的 client。处理 bgsave 或者 bgrewriteaof 的子进程退出后的收尾工作。判断有 keys 的变化而需要执行 bgsave。清理过期(expire)的key。检测 slave 节点的状况,处理自己作为 slave 的连接主库的工作。

0 comments on commit f3e2040

Please sign in to comment.