参考资料:
http://www./2011/09/06/redis-kv-design.html
http://blog./html/3379.html
通过对文章《节约内存:Instagram的Redis实践》的阅读之后,感觉受益不少。
在文章中,Instagram 通过对数据结构的设计优化,使内存从之前的21GB逐步降低到15GB,5GB最后到达了3GB,效果非常显著。
因此自己打算在测试环境中模拟其思路,通过实践加深理解并得出一些真实的数据。
首先,需要生成一些数据,为了方便理解,我从本地CloudStack中的vm_instance表中取了一些数据。
下面我们来看一个关系型数据库的设计:
1 | mysql> select id ,instance_name,private_ip_address,uuid,created from vm_instance; |
2 | +----+---------------+--------------------+--------------------------------------+---------------------+ |
3 | | id | instance_name | private_ip_address | uuid | created | |
4 | +----+---------------+--------------------+--------------------------------------+---------------------+ |
5 | | 1 | s-1-VM | 10.6.59.6 | 8c252255-82b8-4934-830e-0573cc9e0a1c | 2012-05-27 04:06:54 | |
6 | | 2 | v -2-VM | 10.6.88.209 | 1aae6ab9-73cb-46e3-aafb-985f6a143a08 | 2012-05-27 04:06:54 | |
7 | | 4 | r-4-VM | 169.254.1.42 | 5520f0e9-4c5a-4599-be5c-0ea74b59d6dd | 2012-05-27 10:45:42 | |
8 | | 5 | i-2-5-VM | 10.6.8.55 | 2191b464-58be-423d-9863-ce9c0397fc67 | 2012-05-27 11:10:06 | |
9 | | 6 | i-2-6-VM | 10.6.8.56 | c5be506a-aaae-475a-beb7-e6af2a33c8d3 | 2012-05-28 02:07:55 | |
下面我们采用Redis作为数据库,首先需要将关系型数据转化为Key/Value数据。
可采用如下的方式来实现:
Key --> 表名:主键值:列名
Value --> 列值
使用冒号作为分隔符,目前算是一个不成文的规矩。例如工具php-admin for redis就是默认以冒号分割的。
下面我以前五行数据为例,数据转化的命令如下:
01 | SET vm_instance:1:instance_name s-1-VM |
02 | SET vm_instance:2:instance_name v -2-VM |
03 | SET vm_instance:4:instance_name r-4-VM |
04 | SET vm_instance:5:instance_name i-2-5-VM |
05 | SET vm_instance:6:instance_name i-2-6-VM |
07 | SET vm_instance:1:uuid 8c252255-82b8-4934-830e-0573cc9e0a1c |
08 | SET vm_instance:2:uuid 1aae6ab9-73cb-46e3-aafb-985f6a143a08 |
09 | SET vm_instance:4:uuid 5520f0e9-4c5a-4599-be5c-0ea74b59d6dd |
10 | SET vm_instance:5:uuid 2191b464-58be-423d-9863-ce9c0397fc67 |
11 | SET vm_instance:6:uuid c5be506a-aaae-475a-beb7-e6af2a33c8d3 |
13 | SET vm_instance:1:private_ip_address 10.6.59.6 |
14 | SET vm_instance:2:private_ip_address 10.6.88.209 |
15 | SET vm_instance:4:private_ip_address 169.254.1.42 |
16 | SET vm_instance:5:private_ip_address 10.6.8.55 |
17 | SET vm_instance:6:private_ip_address 10.6.8.56 |
19 | SET vm_instance:1:created "2012-05-27 04:06:54" |
20 | SET vm_instance:2:created "2012-05-27 04:06:54" |
21 | SET vm_instance:4:created "2012-05-27 10:45:42" |
22 | SET vm_instance:5:created "2012-05-27 11:10:06" |
23 | SET vm_instance:6:created "2012-05-28 02:07:55" |
后面在大数据量生成时我将通过脚本来实现。
这样在已知主键的情况下,通过GET,SET就可以获得或修改instance_name,private_ip_address等属性了。
一般的用户是无法知道自己的id的,只知道自己的instance_name,所以增加一个从instance_name到id的映射是个不错的注意。
1 | SET vm_instance:s-1-VM: id 1 |
2 | SET vm_instance: v -2-VM: id 2 |
3 | SET vm_instance:r-4-VM: id 4 |
4 | SET vm_instance:i-2-5-VM: id 5 |
5 | SET vm_instance:i-2-6-VM: id 6 |
这样,就可以通过instance_name来方便的查找所需的值了,如下所示:
1 | redis 127.0.0.1:6379> GET vm_instance:r-4-VM: id |
3 | redis 127.0.0.1:6379> GET vm_instance:4:private_ip_address |
6 | redis 127.0.0.1:6379> GET vm_instance:i-2-5-VM: id |
8 | redis 127.0.0.1:6379> GET vm_instance:5:created |
浏览一下当前所有的KEY数据:
01 | redis 127.0.0.1:6379> KEYS * |
02 | 1) "vm_instance:r-4-VM:id" |
03 | 2) "vm_instance:v-2-VM:id" |
04 | 3) "vm_instance:1:instance_name" |
05 | 4) "vm_instance:i-2-5-VM:id" |
06 | 5) "vm_instance:2:instance_name" |
07 | 6) "vm_instance:i-2-6-VM:id" |
08 | 7) "vm_instance:1:uuid" |
09 | 8) "vm_instance:1:created" |
10 | 9) "vm_instance:4:instance_name" |
11 | 10) "vm_instance:2:uuid" |
12 | 11) "vm_instance:1:private_ip_address" |
13 | 12) "vm_instance:2:created" |
14 | 13) "vm_instance:5:instance_name" |
15 | 14) "vm_instance:2:private_ip_address" |
16 | 15) "vm_instance:6:instance_name" |
17 | 16) "vm_instance:4:uuid" |
18 | 17) "vm_instance:4:created" |
19 | 18) "vm_instance:5:uuid" |
20 | 19) "vm_instance:4:private_ip_address" |
21 | 20) "vm_instance:5:created" |
22 | 21) "vm_instance:5:private_ip_address" |
23 | 22) "vm_instance:6:uuid" |
24 | 23) "vm_instance:s-1-VM:id" |
25 | 24) "vm_instance:6:created" |
26 | 25) "vm_instance:6:private_ip_address" |
下面,我将通过脚本来生成大量的数据(100万条)。
首先,清除所有的数据:
1 | redis 127.0.0.1:6379> FLUSHALL |
查看当前的内存耗用:
01 | redis 127.0.0.1:6379> INFO |
03 | redis_git_sha1:00000000 |
09 | run_id:362c470ccf38b87aa955d1e1e447f58522a271c6 |
10 | uptime_in_seconds:54193 |
15 | used_cpu_sys_children:46.74 |
16 | used_cpu_user_children:162.62 |
19 | client_longest_output_list:0 |
20 | client_biggest_input_buf:0 |
23 | used_memory_human:718.62K |
24 | used_memory_rss:6701056 |
25 | used_memory_peak:236219680 |
26 | used_memory_peak_human:225.28M |
27 | mem_fragmentation_ratio:9.11 |
28 | mem_allocator:jemalloc-3.0.0 |
31 | changes_since_last_save:30 |
33 | last_save_time:1348610141 |
34 | bgrewriteaof_in_progress:0 |
35 | total_connections_received:1812645 |
36 | total_commands_processed:5430976 |
46 | slave0:10.6.1.144,6379,online |
内存的耗用非常少,仅为718.62K (735864)。
下面的Shell脚本将生成100万条数据(20万*5):
dongguo@redis:~/shell$ vim redis-cli-generate.sh
03 | REDISCLI= "redis-cli -a slavepass -n 2 SET" |
08 | INSTANCE_NAME= "i-2-$ID-VM" |
09 | UUID=` cat /proc/sys/kernel/random/uuid` |
10 | PRIVATE_IP_ADDRESS=10.` echo "$RANDOM % 255 + 1" | bc `.` echo "$RANDOM % 255 + 1" | bc `.` echo "$RANDOM % 255 + 1" | bc `\ |
11 | CREATED=` date "+%Y-%m-%d %H:%M:%S" ` |
13 | $REDISCLI vm_instance:$ID:instance_name $INSTANCE_NAME |
14 | $REDISCLI vm_instance:$ID:uuid $UUID |
15 | $REDISCLI vm_instance:$ID:private_ip_address $PRIVATE_IP_ADDRESS |
16 | $REDISCLI vm_instance:$ID:created $CREATED |
18 | $REDISCLI vm_instance:$INSTANCE_NAME: id $ID |
创建一个screen终端,将脚本放到终端中后台执行是个不错的注意。
dongguo@redis:~/shell$ screen -dmS redis
dongguo@redis:~/shell$ screen -r redis
dongguo@redis:~/shell$ ./redis-cli-generate.sh
同时按下Ctrl+AD三个按钮退出终端。
等待大约2个小时以后,数据终于写入完成(因为是虚拟机环境,所以才等这么久)。
查看一下当前的内存开销:
01 | redis 127.0.0.1:6379> info |
03 | redis_git_sha1:00000000 |
09 | run_id:362c470ccf38b87aa955d1e1e447f58522a271c6 |
10 | uptime_in_seconds:60658 |
15 | used_cpu_sys_children:58.20 |
16 | used_cpu_user_children:190.09 |
19 | client_longest_output_list:0 |
20 | client_biggest_input_buf:0 |
23 | used_memory_human:124.50M |
24 | used_memory_rss:134524928 |
25 | used_memory_peak:236219680 |
26 | used_memory_peak_human:225.28M |
27 | mem_fragmentation_ratio:1.03 |
28 | mem_allocator:jemalloc-3.0.0 |
31 | changes_since_last_save:1 |
33 | last_save_time:1348616616 |
34 | bgrewriteaof_in_progress:0 |
35 | total_connections_received:2863881 |
36 | total_commands_processed:8584847 |
46 | slave0:10.6.1.144,6379,online |
47 | db2:keys=999995,expires=0 |
目前的内存耗用为124.50M (130548280)。
在数据生成之后,接下来才是本文的重点,即参考Instagram的例子做一些优化的实践。
首先,让我们确认现在的内存开销:
124.50M (130548280)
第一个优化点很明显也很简单,可以把所有key值前面相同的vm_instance:去掉,也就是之前定义的表名,将其放置在一个独立的数据库(这里选择2号)中,避免其他的数据混进来就可以了。
这里就立刻节省了12个字节的开销,然后剩下的继续设法减少开销,可以将instance_name优化为name,private_ip_address优化为ip,这样就累积节省了12+9+16=37个字节的开销。
初步优化过后的数据如下:
2 | SET 1:uuid 8c252255-82b8-4934-830e-0573cc9e0a1c |
4 | SET 1:created "2012-05-27 04:06:54" |
通过脚本导入优化过后的数据,并做内存开销上的对比。
dongguo@redis:~/shell$ cat redis-cli-generate_2.sh
03 | REDISCLI= "redis-cli -a slavepass -n 2 SET" |
08 | INSTANCE_NAME= "i-2-$ID-VM" |
09 | UUID=` cat /proc/sys/kernel/random/uuid` |
10 | PRIVATE_IP_ADDRESS=10.` echo "$RANDOM % 255 + 1" | bc `.` echo "$RANDOM % 255 + 1" | bc `.` echo "$RANDOM % 255 + 1" | bc `\ |
11 | CREATED=` date "+%Y-%m-%d %H:%M:%S" ` |
13 | $REDISCLI $ID:name "$INSTANCE_NAME" |
14 | $REDISCLI $ID:uuid "$UUID" |
15 | $REDISCLI $ID:ip "$PRIVATE_IP_ADDRESS" |
16 | $REDISCLI $ID:created "$CREATED" |
18 | $REDISCLI $INSTANCE_NAME: id "$ID" |
清除数据,用脚本导入新的数据。
1 | redis 127.0.0.1:6379> FLUSHALL |
dongguo@redis:~/shell$ screen -r redis
dongguo@redis:~/shell$ ./redis-cli-generate_2.sh
同时按下Ctrl+AD三个按钮退出终端。
等待大约2个小时以后,数据再次写入完成。
查看内存开销:
01 | redis 127.0.0.1:6379> info |
03 | redis_git_sha1:00000000 |
09 | run_id:362c470ccf38b87aa955d1e1e447f58522a271c6 |
10 | uptime_in_seconds:65449 |
15 | used_cpu_sys_children:66.33 |
16 | used_cpu_user_children:211.75 |
19 | client_longest_output_list:0 |
20 | client_biggest_input_buf:0 |
23 | used_memory_human:112.15M |
24 | used_memory_rss:121319424 |
25 | used_memory_peak:236219680 |
26 | used_memory_peak_human:225.28M |
27 | mem_fragmentation_ratio:1.03 |
28 | mem_allocator:jemalloc-3.0.0 |
31 | changes_since_last_save:2795 |
33 | last_save_time:1348621199 |
34 | bgrewriteaof_in_progress:0 |
35 | total_connections_received:3863886 |
36 | total_commands_processed:11584940 |
46 | slave0:10.6.1.144,6379,online |
47 | db2:keys=999995,expires=0 |
所占内存大小为112.15M (117601616)。
结论:
通过对字节数的优化,内存从124.50M (130548280) 减少到了 112.15M (117601616)。
比例为 1 - (117601616/130548280) = 1 - 0.9008285363851596 = 0.0991714636148404,即节省了9%的内存,感觉效果并不是很明显。
这个结果倒也不出乎以外,因为Instagram将内存得到了显著提升,是在使用了Hash结构对数据进行存储之后。
具体的做法呢就是将数据分段,每一段使用一个Hash结构来存储,这一点在String结构里是不存在的。
据称经过一些开发者们的实验,将hash-zipmap-max-entries设置为1000时,性能比较好,超过1000后HSET命令就会导致CPU消耗变得非常大。
于是我们可以考虑将数据做成如下结构:
01 | redis 127.0.0.1:6379> GET 63233:name |
04 | redis 127.0.0.1:6379> HSET 63:name 233 i-2-63233-VM |
05 | redis 127.0.0.1:6379> HGET 63:name 233 |
08 | redis 127.0.0.1:6379> get 63233:uuid |
09 | "556caf0f-3e6a-4b4f-a2d2-165144edaa5f" |
11 | redis 127.0.0.1:6379> HGET 63:uuid 233 |
12 | "556caf0f-3e6a-4b4f-a2d2-165144edaa5f" |
将4位数以上的ID值转换为Hash结构的Key值,保证每个Hash内部只包含3位的Key,也就是1000个。
对4位数以下的处理呢就很简单了,全部把他们放到ID为0的key值中。
对应的脚本如下,重新设计数据,采用Hash结构来存储:
dongguo@redis:~/shell$ cat redis-cli-generate_3.sh
03 | REDISCLI= "redis-cli -a slavepass -n 2 HSET" |
08 | INSTANCE_NAME= "i-2-$ID-VM" |
09 | UUID=` cat /proc/sys/kernel/random/uuid` |
10 | PRIVATE_IP_ADDRESS=10.` echo "$RANDOM % 255 + 1" | bc `.` echo "$RANDOM % 255 + 1" | bc `.` echo "$RANDOM % 255 + 1" | bc ` |
11 | CREATED=` date "+%Y-%m-%d %H:%M:%S" ` |
13 | $REDISCLI 0:name $ID "$INSTANCE_NAME" |
14 | $REDISCLI 0:uuid $ID "$UUID" |
15 | $REDISCLI 0:ip $ID "$PRIVATE_IP_ADDRESS" |
16 | $REDISCLI 0:created $ID "$CREATED" |
18 | $REDISCLI i-2-0: id $ID-VM $ID |
25 | INSTANCE_NAME= "i-2-$ID-VM" |
26 | UUID=` cat /proc/sys/kernel/random/uuid` |
27 | PRIVATE_IP_ADDRESS=10.` echo "$RANDOM % 255 + 1" | bc `.` echo "$RANDOM % 255 + 1" | bc `.` echo "$RANDOM % 255 + 1" | bc ` |
28 | CREATED=` date "+%Y-%m-%d %H:%M:%S" ` |
30 | LENGTH=` expr length $ID` |
31 | LENGTHCUT=` expr $LENGTH - 3` |
32 | LENGTHEND=` expr $LENGTHCUT + 1` |
34 | VALUE1=` echo $ID | awk '{print substr($1,1,"' $LENGTHCUT '")}' ` |
35 | VALUE2=` echo $ID | awk '{print substr($1,"' $LENGTHEND '",3)}' ` |
37 | $REDISCLI $VALUE1:name $VALUE2 "$INSTANCE_NAME" |
38 | $REDISCLI $VALUE1:uuid $VALUE2 "$UUID" |
39 | $REDISCLI $VALUE1:ip $VALUE2 "$PRIVATE_IP_ADDRESS" |
40 | $REDISCLI $VALUE1:created $VALUE2 "$CREATED" |
42 | $REDISCLI i-2-$VALUE1: id $VALUE2-VM $ID |
清除数据:
1 | redis 127.0.0.1:6379> FLUSHALL |
停止Redis服务器,以便修改配置文件参数:
dongguo@redis:~/shell$ sudo /etc/init.d/redis stop
Stopping ...
Redis stopped.
修改配置文件参数:
dongguo@redis:~/shell$ sudo vim /opt/redis/etc/redis_6379.conf
1 | hash -max-zipmap-entries 1000 |
用脚本导入新的数据
dongguo@redis:~/shell$ screen -r redis
dongguo@redis:~/shell$ ./redis-cli-generate_2.sh
同时按下Ctrl+AD三个按钮退出终端。
等待大约2个小时以后,数据再次写入完成。
激动人心的时刻就要到来了。
查看内存开销:
01 | redis 127.0.0.1:6379> info |
03 | redis_git_sha1:00000000 |
09 | run_id:35f282a72a80f2a82c13c89ba78b1b1d1281ae47 |
15 | used_cpu_sys_children:3.14 |
16 | used_cpu_user_children:4.49 |
19 | client_longest_output_list:0 |
20 | client_biggest_input_buf:0 |
23 | used_memory_human:25.77M |
24 | used_memory_rss:29540352 |
25 | used_memory_peak:27022968 |
26 | used_memory_peak_human:25.77M |
27 | mem_fragmentation_ratio:1.09 |
28 | mem_allocator:jemalloc-3.0.0 |
31 | changes_since_last_save:0 |
33 | last_save_time:1348628119 |
34 | bgrewriteaof_in_progress:0 |
35 | total_connections_received:1000026 |
36 | total_commands_processed:3000338 |
46 | slave0:10.6.1.144,6379,online |
47 | db2:keys=1000,expires=0 |
所占内存大小为25.77M (27022992)。
结论:
使用HASH结构的25.77M (27022992)和使用String结构的112.15M (117601616) 相比,节省内存为 1 - (27022992/117601616) = 1 - 0.229784189360119 = 0.770215810639881 。
即节省了 77% 的内存。
优化结果果然十分显著,由此看来,我们在Redis中,通过采用HASH结构来存储数据,和直接使用String结构相比,可以十分有效的优化内存的占用。
目前公司的线上数据大部分都采用了String结构,且String中的内容是经过加密过后的JSON数据。
我的想法是,可以尝试通过对现有的key进行修改或再次设计,将数据存储到HASH结构中,来实现对内存占用的优化。
|