1. memcache理论部分
1)简介
MemCache 是一个自由、源码开放、高性能、分布式的分布式内存对象缓存系统,用于动态
Web 应用以减轻数据库的负载。它通过在内存中缓存数据和对象来减少读取数据库的次数,
从而提高了网站访问的速度。 MemCaChe 是一个存储键值对的 HashMap,在内存中对任意
的数据(比如字符串、对象等)所使用的 key-value 存储,数据可以来自数据库调用、API
调用,或者页面渲染的结果。
2)memcache分布式缓存模型
同时基于这张图,理一下 MemCache 一次写缓存的流程:
1、应用程序输入需要写缓存的数据
2、API 将 Key 输入路由算法模块,路由算法根据 Key 和 MemCache 集群服务器列表得到一
台服务器编号
3、由服务器编号得到 MemCache 及其的 ip 地址和端口号
4、API 调用通信模块和指定编号的服务器通信,将数据写入该服务器,完成一次分布式缓
存的写操作
这种 MemCache 集群的方式也是从分区容错性的方面考虑的,假如 Node2 宕机了,那么Node2 上面存储的数据都不可用了,此时由于集群中 Node0 和 Node1 还存在,下一次请求Node2 中存储的 Key 值的时候,肯定是没有命中的,这时先从数据库中拿到要缓存的数据,然后路由算法模块根据 Key 值在 Node0 和 Node1 中选取一个节点,把对应的数据放进去,这样下一次就又可以走缓存了,这种集群的做法很好,但是缺点是成本比
3)余数hash算法和一致性hash算法
1、余数Hash
比方说,字符串str对应的HashCode是50、服务器的数目是3,取余数得到1,str对应节点Node1,所以路由算法把str路由到Node1服务器上。由于HashCode随机性比较强,所以使用余数Hash路由算法就可以保证缓存数据在整个MemCache服务器集群中有比较均衡的分布。
如果不考虑服务器集群的伸缩性(什么是伸缩性,请参见大型网站架构学习笔记),那么余数Hash算法几乎可以满足绝大多数的缓存路由需求,但是当分布式缓存集群需要扩容的时候,就难办了。
就假设MemCache服务器集群由3台变为4台吧,更改服务器列表,仍然使用余数Hash,50对4的余数是2,对应Node2,但是str原来是存在Node1上的,这就导致了缓存没有命中。如果这么说不够明白,那么不妨举个例子,原来有HashCode为0~19的20个数据,那么:
如果我扩容到20+的台数,只有前三个HashCode对应的Key是命中的,也就是15%。当然这只是个简单例子,现实情况肯定比这个复杂得多,不过足以说明,使用余数Hash的路由算法,在扩容的时候会造成大量的数据无法正确命中(其实不仅仅是无法命中,那些大量的无法命中的数据还在原缓存中在被移除前占据着内存)。这个结果显然是无法接受的,在网站业务中,大部分的业务数据度操作请求上事实上是通过缓存获取的,只有少量读操作会访问数据库,因此数据库的负载能力是以有缓存为前提而设计的。当大部分被缓存了的数据因为服务器扩容而不能正确读取时,这些数据访问的压力就落在了数据库的身上,这将大大超过数据库的负载能力,严重的可能会导致数据库宕机。
这个问题有解决方案,解决步骤为:
(1)在网站访问量低谷,通常是深夜,技术团队加班,扩容、重启服务器
(2)通过模拟请求的方式逐渐预热缓存,使缓存服务器中的数据重新分布
2、一致性Hash算法
一致性Hash算法通过一个叫做一致性Hash环的数据结构实现Key到缓存服务器的Hash映射,看一下我自己画的一张图:
具体算法过程为:先构造一个长度为232的整数环(这个环被称为一致性Hash环),根据节点名称的Hash值(其分布为[0, 232-1])将缓存服务器节点放置在这个Hash环上,然后根据需要缓存的数据的Key值计算得到其Hash值(其分布也为[0, 232-1]),然后在Hash环上顺时针查找距离这个Key值的Hash值最近的服务器节点,完成Key到服务器的映射查找。
就如同图上所示,三个Node点分别位于Hash环上的三个位置,然后Key值根据其HashCode,在Hash环上有一个固定位置,位置固定下之后,Key就会顺时针去寻找离它最近的一个Node,把数据存储在这个Node的MemCache服务器中。使用Hash环如果加了一个节点会怎么样,看一下:
看到我加了一个Node4节点,只影响到了一个Key值的数据,本来这个Key值应该是在Node1服务器上的,现在要去Node4了。采用一致性Hash算法,的确也会影响到整个集群,但是影响的只是加粗的那一段而已,相比余数Hash算法影响了远超一半的影响率,这种影响要小得多。更重要的是,集群中缓存服务器节点越多,增加节点带来的影响越小,很好理解。换句话说,随着集群规模的增大,继续命中原有缓存数据的概率会越来越大,虽然仍然有小部分数据缓存在服务器中不能被读到,但是这个比例足够小,即使访问数据库,也不会对数据库造成致命的负载压力。
4)memcache实现原理
首先要说明一点,MemCache的数据存放在内存中,存放在内存中个人认为意味着几点:
1、访问数据的速度比传统的关系型数据库要快,因为Oracle、MySQL这些传统的关系型数据库为了保持数据的持久性,数据存放在硬盘中,IO操作速度慢
2、MemCache的数据存放在内存中同时意味着只要MemCache重启了,数据就会消失
3、既然MemCache的数据存放在内存中,那么势必受到机器位数的限制,memcache主机如果是32位机器,最多只能使用2GB的内存空间,64位机器可以认为没有上限
然后我们来看一下MemCache的原理,MemCache最重要的是内存分配的内容,MemCache采用的内存分配方式是固定空间分配,还是自己画一张图说明:
这张图片里面涉及了slab_class、slab、page、chunk四个概念,它们之间的关系是:
-
MemCache将内存空间分为一组slab
-
每个slab下又有若干个page,每个page默认是1M,如果一个slab占用100M内存的话,那么这个slab下应该有100个page
-
每个page里面包含一组chunk,chunk是真正存放数据的地方,同一个slab里面的chunk的大小是固定的
-
有相同大小chunk的slab被组织在一起,称为slab_class
MemCache内存分配的方式称为allocator,slab的数量是有限的,几个、十几个或者几十个,这个和启动参数的配置相关。
MemCache中的value过来存放的地方是由value的大小决定的,value总是会被存放到与chunk大小最接近的一个slab中,比如slab[1]的chunk大小为80字节、slab[2]的chunk大小为100字节、slab[3]的chunk大小为128字节(相邻slab内的chunk基本以1.25为比例进行增长,MemCache启动时可以用-f指定这个比例),那么过来一个88字节的value,这个value将被放到2号slab中。放slab的时候,首先slab要申请内存,申请内存是以page为单位的,所以在放入第一个数据的时候,无论大小为多少,都会有1M大小的page被分配给该slab。申请到page后,slab会将这个page的内存按chunk的大小进行切分,这样就变成了一个chunk数组,最后从这个chunk数组中选择一个用于存储数据。
如果这个slab中没有chunk可以分配了怎么办,如果MemCache启动没有追加-M(禁止LRU,这种情况下内存不够会报Out Of Memory错误),那么MemCache会把这个slab中最近最少使用的chunk中的数据清理掉,然后放上最新的数据。
针对MemCache的内存分配及回收算法,总结三点:
- MemCache的内存分配chunk里面会有内存浪费,88字节的value分配在128字节(紧接着大的用)的chunk中,就损失了30字节,但是这也避免了管理内存碎片的问题
- MemCache的LRU算法不是针对全局的,是针对slab的
- 应该可以理解为什么MemCache存放的value大小是限制的,因为一个新数据过来,slab会先以page为单位申请一块内存,申请的内存最多就只有1M,所以value大小自然不能大于1M了
memcache的lru算法
假如我们以122字节大小的chunk举例,122的chunk都满了,又有新的值(长度也为122)要加入,那么要挤掉谁?
MC这时使用的是LRU的删除机制
注:操作系统的常用内存管理,经常使用的算法是FIFO,LRU算法
lru:least recently used 最近最少使用
fifo:first in,first out (先进先出)
LRU原理:当某个单元被请求的时候,维护一个计数器,通过计数器来判断最近谁最少使用,那就把谁踢出去。
注:即使某个key设置的永久有效,也会被踢出来,这个就是永久数据被踢的现象。
2. memcache安装、启动、语法
1)安装memcache
5 tar -zxf libevent-2.0.22-stable.tar.gz -C /usr/src
6 cd /usr/src/libevent-2.0.22-stable/
7 ./configure --prefix=/usr/
8 make && make install
7 tar -zxf memcached-1.4.36.tar.gz -C /usr/src
8 cd /usr/src/memcached-1.4.36/
9 ./configure --prefix=/usr/local/memcached --with-libevent=/usr/
10 make
11 make install
16 ln -s /usr/local/memcached/bin/* /usr/local/bin/
2)启动memcache
启动命令参数
memcached命令
-d 以守护进程的方式,后台运行
-m 最大内存,默认64M
-l 监听的服务器的ip
-u 指定memcache启动的用户
-p 使用tcp端口为11211
-P 设置指定的pid文件
-vv 显示调度信息
-c 运行的最大并发链接数,默认1024个
启动memcache
memcached -d -m 512 -u root -l 192.168.1.117 -p 11211
3) memcache语法
(0)quit 退出memcache
(1)set
set key flags exptime bytes 【noreply】
value
flags 可以包括键值对的整型参数,客户机使用它存储关于键值对的额外信息
exptime 缓存时间,单位是秒,0表示永远
bytes 缓存中存储的字节数
noreply 告知服务不需要返回数据
返回结果:stored、error
(2)add(用于添加不存在的键,如果存在,在不改变键值)
用法和set相同
(3)replace(替换已经存在的键值,如果不存在,替换失败)
语法和set相同
(4)append 向后追加(用于对已存在的键追加值)
语法和set相同
(5)prepend 向前追加(用于已存在的键)
(6)cas (check and set) 用于执行一个检查并设置的操作,用于已存在 键
cas key flags exptime bytes unique_cas_token【noreply】
value
set c 0 900 9
memcached
STORED
gets c
VALUE c 0 9 6 //6位token值
memcached
END
cas c 0 900 5 6
abcde
STORED
get c
VALUE c 0 5
abcde
END
(7)get 用于查看,可跟多个键
(8)gets 可以得到cas需要的token值
(9)delete key
(10)incr key 5
(11)decr key 5
(12)stats 查看并返回
stats items
stats slabs
stats sizes
(13)flush_all 清空所有键值对
3. memcache集群
1. 实验准备
1)服务器规划
主机 | IP | 角色 |
---|---|---|
host1 | 192.168.1.115 | 客户机 |
host2 | 192.168.1.116 | magnet 1 |
host3 | 192.168.1.118 | magnet 2 |
host4 | 192.168.1.117 | memcache主 |
host5 | 192.168.1.131 | memcache从 |
2)需要软件包
magnet:libevent-2.022 keepalived
memcache:libevent-2.022 memcache-1.436
2. 操作过程
1)host2~5安装libevent
5 tar -zxf libevent-2.0.22-stable.tar.gz -C /usr/src
6 cd /usr/src/libevent-2.0.22-stable/
7 ./configure --prefix=/usr/
8 make && make install
2)host4 host5安装memcache
7 tar -zxf memcached-1.4.36.tar.gz -C /usr/src
8 cd /usr/src/memcached-1.4.36/
9 ./configure --prefix=/usr/local/memcached --with-libevent=/usr/
10 make
11 make install
16 ln -s /usr/local/memcached/bin/* /usr/local/bin/
3)启动两台memcache
host4 启动memcache
[root@localhost ~]# memcached -d -m 512 -u root -l 192.168.1.117 -p 11211
host 5 启动memcache
[root@localhost ~]# memcached -d -m 512 -u root -l 192.168.1.131 -p 11211
4)host2 host3 安装magnet
10 tar -zxf magent-0.6.tar.gz -C /usr/local/magnet/
11 cd /usr/local/magnet/
12 vim ketama.h
添加(头部添加):
#ifndef SSIZE_MAX
#define SSIZE_MAX 32767
#endif
13 ln -s /usr/lib64/libm.so /usr/lib64/libm.a
23 ln -s /usr/lib64/libevent-2.0.so.5 /usr/lib64/libevent.a
15 ldconfig
16 sed -i "s#LIBS = -levent#LIBS = -levent -lm#g" Makefile
18 make
26 cp magent /usr/bin/
5) host2 host3 上搭建keepalived 虚拟IP设定为192.168.1.100
32 yum -y install openssl-devel popt-devel
31 tar -zxf keepalived-1.2.13.tar.gz -C /usr/src/
32 cd /usr/src/keepalived-1.2.13/
33 ./configure --prefix=/
34 make && make install
[root@localhost keepalived-1.2.13]# vim /etc/keepalived/keepalived.conf
vrrp_instance VI_1 {
state MASTER #第二台改为BACKUP
interface ens33 #本机内网网卡名
virtual_router_id 51
priority 100 # 第二台改为90
virtual_ipaddress {
192.168.1.100
}
[root@localhost keepalived-1.2.13]# /etc/init.d/keepalived start
6)启动两台magent(启动命令相同)
magent 启动参数
-u:用户
-n:最大连接数
-l:magent 对外监听IP 地址
-p:magent 对外监听端口
-s:magent 主缓存IP 地址和端口
-b:magent 备缓存IP 地址和端口
# magent -u root -n 51200 -l 192.168.1.100 -p 12000 -s 192.168.1.117:11211 -b 192.168.1.131:11211
7)各主机关闭防火墙
systemctl stop firewalld
7)客户机登陆验证
# yum -y install telnet
[root@localhost ~]# telnet 192.168.1.100 12000
Trying 192.168.1.100...
Connected to 192.168.1.100.
Escape character is '^]'.
set today 0 0 6
sunday
STORED
去主memcache上查看键值对情况
[root@localhost ~]# telnet 192.168.1.117 11211
Trying 192.168.1.117...
Connected to 192.168.1.117.
Escape character is '^]'.
get today
VALUE today 0 6
sunday
END
去从memcache查看键值对情况
[root@localhost ~]# telnet 192.168.1.131 11211
Trying 192.168.1.131...
Connected to 192.168.1.131.
Escape character is '^]'.
get today
VALUE today 0 6
sunday
END
memcache主从转进行同步,集群搭建完成
4. memcache做数据库缓存
前提:源码安装MySQL和memcached
cd /mnt/hgfs/soft/
74 tar -zxf libmemcached-1.0.17.tar.gz -C /usr/src
75 cd /usr/src/libmemcached-1.0.17/
76 ./configure
77 make
78 make install
79 echo "/usr/loacl/lib" >> /etc/ld.so.conf
80 ldconfig
cd /mnt/hgfs/soft/
tar -zxf libmemcached-0.34.tar.gz
4 cd /usr/src/libmemcached-0.34/
5 ./configure --prefix=/usr/local/libmemcache34
6 make && make install
8 cd /mnt/hgfs/soft/
9 tar -zxf memcached_functions_mysql-1.1.tar.gz -C /usr/src/
10 cd /usr/src/memcached_functions_mysql-1.1/
11 ./configure --with-mysql=/usr/local/mysql/bin/mysql_config --with-libmemcached=/usr/local/libmemcache34/
12 make
13 make install
14 cp /usr/local/lib/libmemcached_functions* /usr/local/mysql/lib/plugin/
[root@qukuoyuan07 memcached_functions_mysql-1.1]# mysql -u root -p < sql/install_functions.sql
Enter password:
[root@qukuoyuan07 memcached_functions_mysql-1.1]# mysql -u root -p
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| test |
+--------------------+
4 rows in set (0.10 sec)
mysql> use mysql
Database changed
mysql> select name,dl from func;
+------------------------------+---------------------------------+
| name | dl |
+------------------------------+---------------------------------+
| memc_add | libmemcached_functions_mysql.so |
| memc_add_by_key | libmemcached_functions_mysql.so |
| memc_servers_set | libmemcached_functions_mysql.so |
| memc_server_count | libmemcached_functions_mysql.so |
| memc_set | libmemcached_functions_mysql.so |
| memc_set_by_key | libmemcached_functions_mysql.so |
| memc_cas | libmemcached_functions_mysql.so |
| memc_cas_by_key | libmemcached_functions_mysql.so |
| memc_get | libmemcached_functions_mysql.so |
| memc_get_by_key | libmemcached_functions_mysql.so |
| memc_get_cas | libmemcached_functions_mysql.so |
| memc_get_cas_by_key | libmemcached_functions_mysql.so |
| memc_delete | libmemcached_functions_mysql.so |
| memc_delete_by_key | libmemcached_functions_mysql.so |
| memc_append | libmemcached_functions_mysql.so |
| memc_append_by_key | libmemcached_functions_mysql.so |
| memc_prepend | libmemcached_functions_mysql.so |
| memc_prepend_by_key | libmemcached_functions_mysql.so |
| memc_increment | libmemcached_functions_mysql.so |
| memc_decrement | libmemcached_functions_mysql.so |
| memc_replace | libmemcached_functions_mysql.so |
| memc_replace_by_key | libmemcached_functions_mysql.so |
| memc_servers_behavior_set | libmemcached_functions_mysql.so |
| memc_servers_behavior_get | libmemcached_functions_mysql.so |
| memc_behavior_set | libmemcached_functions_mysql.so |
| memc_behavior_get | libmemcached_functions_mysql.so |
| memc_list_behaviors | libmemcached_functions_mysql.so |
| memc_list_hash_types | libmemcached_functions_mysql.so |
| memc_list_distribution_types | libmemcached_functions_mysql.so |
| memc_udf_version | libmemcached_functions_mysql.so |
| memc_libmemcached_version | libmemcached_functions_mysql.so |
| memc_stats | libmemcached_functions_mysql.so |
| memc_stat_get_keys | libmemcached_functions_mysql.so |
| memc_stat_get_value | libmemcached_functions_mysql.so |
+------------------------------+---------------------------------+
34 rows in set (0.00 sec)
mysql> use test
Database changed
mysql> create table urls ( id int(10) not null, url varchar(255) not null default '' );
Query OK, 0 rows affected (0.18 sec)
mysql> create table results ( id int(10) not null, results varchar(255) not null default 'error' , time timestamp null default current_timestamp );
Query OK, 0 rows affected (0.33 sec)
mysql> delimiter // //切换结束符
mysql> create trigger url_mem_insert before insert on urls for each row begin set @num = memc_set(NEW.id,NEW.url); if @num <> 0 then insert into results(id) values(NEW.id); end if ; end//
Query OK, 0 rows affected (0.05 sec)
mysql> create trigger url_mem_delete before delete on urls for each row begin set @num = memc_delete(OLD.id); if @num <> 0 then insert into results(id) values(OLD.id); end if ; end//
Query OK, 0 rows affected (0.31 sec)?
mysql> create trigger url_mem_update before update on urls for each row begin set @num = memc_replace(OLD.id,NEW.url); if @num <> 0 then insert into results(id) values(OLD.id); end if ; end //
Query OK, 0 rows affected (0.05 sec)
mysql> delimiter ;
mysql> select memc_servers_set('192.168.1.10:11211');
+----------------------------------------+
| memc_servers_set('192.168.1.10:11211') |
+----------------------------------------+
| 0 |
+----------------------------------------+
1 row in set (0.38 sec)
mysql> select memc_server_count();
+---------------------+
| memc_server_count() |
+---------------------+
| 1 |
+---------------------+
1 row in set (0.00 sec)
mysql> select memc_list_behaviors()\G
mysql> select memc_servers_behavior_set('MEMCACHED_BEHAVIOR_NO_BLOCK','1');
+--------------------------------------------------------------+
| memc_servers_behavior_set('MEMCACHED_BEHAVIOR_NO_BLOCK','1') |
+--------------------------------------------------------------+
| 0 |
+--------------------------------------------------------------+
1 row in set (0.00 sec)
mysql> select memc_servers_behavior_set('MEMCACHED_BEHAVIOR_TCP_NODELAY','1');
+-----------------------------------------------------------------+
| memc_servers_behavior_set('MEMCACHED_BEHAVIOR_TCP_NODELAY','1') |
+-----------------------------------------------------------------+
| 0 |
+-----------------------------------------------------------------+
1 row in set (0.00 sec)
mysql> insert into urls(id,url) values(1,'http://www.test.com');
Query OK, 1 row affected (0.02 sec)
mysql> insert into urls(id,url) values(2,'http://www.qu.com');
Query OK, 1 row affected (0.31 sec)
mysql> delete from urls where id=2;
Query OK, 1 row affected (0.33 sec)
在另一个终端验证
[root@qukuoyuan07 support-files]# telnet 192.168.1.10 11211
Trying 192.168.1.10...
Connected to 192.168.1.10.
Escape character is '^]'.
get 1
VALUE 1 0 19
http://www.test.com
END
get 2
VALUE 2 0 17
http://www.qu.com
END
get 2
END