參考資料:
http://www.mysqlops.com/2011/09/06/redis-kv-design.html
http://blog.nosqlfan.com/html/3379.html
通過對文章《節(jié)約內(nèi)存:Instagram的Redis實踐》的閱讀之后,感覺受益不少。
在文章中,Instagram 通過對數(shù)據(jù)結(jié)構(gòu)的設(shè)計優(yōu)化,使內(nèi)存從之前的21GB逐步降低到15GB,5GB最后到達了3GB,效果非常顯著。
因此自己打算在測試環(huán)境中模擬其思路,通過實踐加深理解并得出一些真實的數(shù)據(jù)。
首先,需要生成一些數(shù)據(jù),為了方便理解,我從本地CloudStack中的vm_instance表中取了一些數(shù)據(jù)。
下面我們來看一個關(guān)系型數(shù)據(jù)庫的設(shè)計:
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作為數(shù)據(jù)庫,首先需要將關(guān)系型數(shù)據(jù)轉(zhuǎn)化為Key/Value數(shù)據(jù)。
可采用如下的方式來實現(xiàn):
Key --> 表名:主鍵值:列名
Value --> 列值
使用冒號作為分隔符,目前算是一個不成文的規(guī)矩。例如工具php-admin for redis就是默認以冒號分割的。
下面我以前五行數(shù)據(jù)為例,數(shù)據(jù)轉(zhuǎn)化的命令如下:
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" |
后面在大數(shù)據(jù)量生成時我將通過腳本來實現(xiàn)。
這樣在已知主鍵的情況下,通過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 |
瀏覽一下當(dāng)前所有的KEY數(shù)據(jù):
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" |
下面,我將通過腳本來生成大量的數(shù)據(jù)(100萬條)。
首先,清除所有的數(shù)據(jù):
1 | redis 127.0.0.1:6379> FLUSHALL |
查看當(dāng)前的內(nèi)存耗用:
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 |
內(nèi)存的耗用非常少,僅為718.62K (735864)。
下面的Shell腳本將生成100萬條數(shù)據(jù)(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 |
創(chuàng)建一個screen終端,將腳本放到終端中后臺執(zhí)行是個不錯的注意。
dongguo@redis:~/shell$ screen -dmS redis
dongguo@redis:~/shell$ screen -r redis
dongguo@redis:~/shell$ ./redis-cli-generate.sh
同時按下Ctrl+AD三個按鈕退出終端。
等待大約2個小時以后,數(shù)據(jù)終于寫入完成(因為是虛擬機環(huán)境,所以才等這么久)。
查看一下當(dāng)前的內(nèi)存開銷:
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 |
目前的內(nèi)存耗用為124.50M (130548280)。
在數(shù)據(jù)生成之后,接下來才是本文的重點,即參考Instagram的例子做一些優(yōu)化的實踐。
首先,讓我們確認現(xiàn)在的內(nèi)存開銷:
124.50M (130548280)
第一個優(yōu)化點很明顯也很簡單,可以把所有key值前面相同的vm_instance:去掉,也就是之前定義的表名,將其放置在一個獨立的數(shù)據(jù)庫(這里選擇2號)中,避免其他的數(shù)據(jù)混進來就可以了。
這里就立刻節(jié)省了12個字節(jié)的開銷,然后剩下的繼續(xù)設(shè)法減少開銷,可以將instance_name優(yōu)化為name,private_ip_address優(yōu)化為ip,這樣就累積節(jié)省了12+9+16=37個字節(jié)的開銷。
初步優(yōu)化過后的數(shù)據(jù)如下:
2 | SET 1:uuid 8c252255-82b8-4934-830e-0573cc9e0a1c |
4 | SET 1:created "2012-05-27 04:06:54" |
通過腳本導(dǎo)入優(yōu)化過后的數(shù)據(jù),并做內(nèi)存開銷上的對比。
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" |
清除數(shù)據(jù),用腳本導(dǎo)入新的數(shù)據(jù)。
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個小時以后,數(shù)據(jù)再次寫入完成。
查看內(nèi)存開銷:
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 |
所占內(nèi)存大小為112.15M (117601616)。
結(jié)論:
通過對字節(jié)數(shù)的優(yōu)化,內(nèi)存從124.50M (130548280) 減少到了 112.15M (117601616)。
比例為 1 - (117601616/130548280) = 1 - 0.9008285363851596 = 0.0991714636148404,即節(jié)省了9%的內(nèi)存,感覺效果并不是很明顯。
這個結(jié)果倒也不出乎以外,因為Instagram將內(nèi)存得到了顯著提升,是在使用了Hash結(jié)構(gòu)對數(shù)據(jù)進行存儲之后。
具體的做法呢就是將數(shù)據(jù)分段,每一段使用一個Hash結(jié)構(gòu)來存儲,這一點在String結(jié)構(gòu)里是不存在的。
據(jù)稱經(jīng)過一些開發(fā)者們的實驗,將hash-zipmap-max-entries設(shè)置為1000時,性能比較好,超過1000后HSET命令就會導(dǎo)致CPU消耗變得非常大。
于是我們可以考慮將數(shù)據(jù)做成如下結(jié)構(gòu):
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位數(shù)以上的ID值轉(zhuǎn)換為Hash結(jié)構(gòu)的Key值,保證每個Hash內(nèi)部只包含3位的Key,也就是1000個。
對4位數(shù)以下的處理呢就很簡單了,全部把他們放到ID為0的key值中。
對應(yīng)的腳本如下,重新設(shè)計數(shù)據(jù),采用Hash結(jié)構(gòu)來存儲:
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 |
清除數(shù)據(jù):
1 | redis 127.0.0.1:6379> FLUSHALL |
停止Redis服務(wù)器,以便修改配置文件參數(shù):
dongguo@redis:~/shell$ sudo /etc/init.d/redis stop
Stopping ...
Redis stopped.
修改配置文件參數(shù):
dongguo@redis:~/shell$ sudo vim /opt/redis/etc/redis_6379.conf
1 | hash -max-zipmap-entries 1000 |
用腳本導(dǎo)入新的數(shù)據(jù)
dongguo@redis:~/shell$ screen -r redis
dongguo@redis:~/shell$ ./redis-cli-generate_2.sh
同時按下Ctrl+AD三個按鈕退出終端。
等待大約2個小時以后,數(shù)據(jù)再次寫入完成。
激動人心的時刻就要到來了。
查看內(nèi)存開銷:
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 |
所占內(nèi)存大小為25.77M (27022992)。
結(jié)論:
使用HASH結(jié)構(gòu)的25.77M (27022992)和使用String結(jié)構(gòu)的112.15M (117601616) 相比,節(jié)省內(nèi)存為 1 - (27022992/117601616) = 1 - 0.229784189360119 = 0.770215810639881 。
即節(jié)省了 77% 的內(nèi)存。
優(yōu)化結(jié)果果然十分顯著,由此看來,我們在Redis中,通過采用HASH結(jié)構(gòu)來存儲數(shù)據(jù),和直接使用String結(jié)構(gòu)相比,可以十分有效的優(yōu)化內(nèi)存的占用。
目前公司的線上數(shù)據(jù)大部分都采用了String結(jié)構(gòu),且String中的內(nèi)容是經(jīng)過加密過后的JSON數(shù)據(jù)。
我的想法是,可以嘗試通過對現(xiàn)有的key進行修改或再次設(shè)計,將數(shù)據(jù)存儲到HASH結(jié)構(gòu)中,來實現(xiàn)對內(nèi)存占用的優(yōu)化。