1.什么是memcached
许多Web应用都将数据保存到RDBMS中,应用服务器从中读取数据并在浏览器中显示。但随着数据量的增大、访问的集中,就会出现RDBMS的负担加重、数据库响应恶化、网站显示延迟等重大影响。这时就该memcached大显身手了。memcached是高性能的分布式内存缓存服务器。一般的使用目的是,通过缓存数据库查询结果,减少数据库访问次数,以提高动态Web应用的速度、提高可扩展性。
2.memcached特性
•协议简单
•基于libevent的事件处理
•内置内存存储方式
•memcached不互相通信的分布式
3.理解memcached的内存存储
最近的memcached默认情况下采用了名为Slab Allocator的机制分配、管理内存。在该机制出现以前,内存的分配是通过对所有记录简单地进行malloc和free来进行的。但是,这种方式会导致内存碎片,加重操作系统内存管理器的负担,最坏的情况下,会导致操作系统比memcached进程本身还慢。Slab Allocator就是为解决该问题而诞生的。
也就是说,Slab Allocator的基本原理是按照预先规定的大小,将分配的内存分割成特定长度的块,以完全解决内存碎片问题。
下图即为Slab Allocation的构造图:
缓存记录的原理
下面说明memcached如何针对客户端发送的数据选择slab并缓存到chunk中。
memcached根据收到的数据的大小,选择最适合数据大小的slab。memcached中保存着slab内空闲chunk的列表,根据该列表选择chunk,然后将数据缓存于其中。
实际上,Slab Allocator也是有利也有弊。下面介绍一下它的缺点。
Slab Allocator解决了当初的内存碎片问题,但新的机制也给memcached带来了新的问题。
这个问题就是,由于分配的是特定长度的内存,因此无法有效利用分配的内存。例如,将100字节的数据缓存到128字节的chunk中,剩余的28字节就浪费了
4.使用Growth Factor进行调优
memcached在启动时指定Growth Factor因子(通过-f选项),就可以在某种程度上控制slab之间的差异。默认值为1.25。但是,在该选项出现之前,这个因子曾经固定为2,称为“powers of 2”策略。
让我们用以前的设置,以verbose模式启动memcached试试看:
$ memcached -f 2 -vv
5.查看memcached的内部状态
memcached有个名为stats的命令,使用它可以获得各种各样的信息。执行命令的方法很多,用telnet最为简单:
$ telnet 主机名 端口号
连接到memcached之后,输入stats再按回车,即可获得包括资源利用率在内的各种信息。此外,输入"stats slabs"或"stats items"还可以获得关于缓存记录的信息。结束程序请输入quit。
6.memcached的分布式算法
memcached虽然称为“分布式”缓存服务器,但服务器端并没有“分布式”功能。服务器端仅包括第2次、第3次前坂介绍的内存存储功能,其实现非常简单。至于memcached的分布式,则是完全由客户端程序库实现的。这种分布式是memcached的最大特点。
这里多次使用了“分布式”这个词,但并未做详细解释。现在开始简单地介绍一下其原理,各个客户端的实现基本相同。
6.1分布式客户端算法
6.1.1根据余数计算分散
有点:简单易实现
缺点:余数计算的方法简单,数据的分散性也相当优秀,但也有其缺点。那就是当添加或移除服务器时,缓存重组的代价相当巨大。添加服务器后,余数就会产生巨变,这样就无法获取与保存时相同的服务器,从而影响缓存的命中率。
6.1.2 一致性hash算法Consistent Hashing
Consistent Hashing的简单说明
Consistent Hashing如下所示:首先求出memcached服务器(节点)的哈希值,并将其配置到0~232的圆(continuum)上。然后用同样的方法求出存储数据的键的哈希值,并映射到圆上。然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器上。如果超过232仍然找不到服务器,就会保存到第一台memcached服务器上。
Consistent Hashing基本原理如下图:
从上图的状态中添加一台memcached服务器。余数分布式算法由于保存键的服务器会发生巨大变化而影响缓存的命中率,但Consistent Hashing中,只有在continuum上增加服务器的地点逆时针方向的第一台服务器上的键会受到影响。增加服务器节点如下图所示:
因此,Consistent Hashing最大限度地抑制了键的重新分布。而且,有的Consistent Hashing的实现方法还采用了虚拟节点的思想。使用一般的hash函数的话,服务器的映射地点的分布非常不均匀。因此,使用虚拟节点的思想,为每个物理节点(服务器)在continuum上分配100~200个点。这样就能抑制分布不均匀,最大限度地减小服务器增减时的缓存重新分布。
通过下文中介绍的使用Consistent Hashing算法的memcached客户端函数库进行测试的结果是,由服务器台数(n)和增加的服务器台数(m)计算增加服务器后的命中率计算公式如下:
(1 - n/(n+m)) * 100
1.6.3Consistent Hashing java客户端实现:
下面是参照的简单实现:
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
public class ConsistentHash {
private final HashFunction hashFunction;
private final int numberOfReplicas;
static SortedMap<Long, String> circle = new TreeMap<Long, String>();
public static void main(String args[]){
List<String> list = new ArrayList<String>();
String node1 = "node1";
String node2 = "node2";
String node3 = "node3";
list.add(node1);
list.add(node2);
list.add(node3);
HashFunction hf = new HashFunction();
ConsistentHash ch = new ConsistentHash(hf,1,list);
System.out.println("位置1:"+ch.get("abcdefg"));
System.out.println("位置2:"+ch.get("loadfsf"));
System.out.println("位置3:"+ch.get("ilfcdef"));
System.out.println("位置4:"+ch.get("mbcdefg"));
System.out.println("位置5:"+ch.get("tbcdefg"));
ch.add("node4");
ch.add("node5");
ch.add("node6");
System.out.println("===========================");
System.out.println("位置1:"+ch.get("abcdefg"));
System.out.println("位置2:"+ch.get("loadfsf"));
System.out.println("位置3:"+ch.get("ilfcdef"));
System.out.println("位置4:"+ch.get("mbcdefg"));
System.out.println("位置5:"+ch.get("tbcdefg"));
}
public ConsistentHash(HashFunction hashFunction,
int numberOfReplicas, Collection<String> nodes) {
this.hashFunction = hashFunction;
this.numberOfReplicas = numberOfReplicas;
for (String node : nodes) {
add(node);
}
}
public void add(String node) {
for (int i = 0; i < numberOfReplicas; i++) {
circle.put(hashFunction.hash(node.toString() +":" + i),
node);
}
}
public void remove(String node) {
for (int i = 0; i < numberOfReplicas; i++) {
circle.remove(hashFunction.hash(node.toString() + ":" + i));
}
}
public String get(String key) {
if (circle.isEmpty()) {
return null;
}
long hash = hashFunction.hash(key);
SortedMap<Long, String> tailMap =
circle.tailMap(hash);
hash = tailMap.isEmpty() ?
circle.firstKey() : tailMap.firstKey();
return circle.get(hash);
}
}
class HashFunction{
MessageDigest md5=null;
public Long hash(String key) {
if(md5==null) {
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException( "++++ no md5 algorythm found");
}
}
md5.reset();
md5.update(key.getBytes());
byte[] bKey = md5.digest();
long res = ((long)(bKey[3]&0xFF) << 24) | ((long)(bKey[2]&0xFF) << 16) | ((long)(bKey[1]&0xFF) << 8) | (long)(bKey[0]&0xFF);
return res;
}
}