spymemcached背景介绍
memcached是一款比较悠久、设计优秀、结构简单、性能优良的对象缓存数据库;由于其多线程能力,在相同CPU核数、相同内存的情况下,memcached的性能要优于redis的性能;一些大公司,诸如:FaceBook、Twitter、YouTube等公司也都在使用;
spymemcached sdk则是java用户连接memcached的便捷通道;但由于memcached本身的可靠性、可扩展性不够鲁棒,所以在SDK侧补齐这块的功能,比如:
- 客户端实现的ketama一致性hash环,来保障服务端无限横向扩展;
- 通过ketama hash虚拟节点可以保证数据不会产生倾斜,数据分布不均匀的情况;
- 通过Redistribute Mode来做到数据的冗余备份,增强服务的可用性、可靠性;
同时spymemcached sdk为了高性能,避免频繁的上下文切换带来的性能损耗;(曾经处理过一个问题:服务上线以后,性能劣化严重;原因是8核CPU开了1600个线程,导致上下文切换每秒达到160万次); spymemcached会在一个线程里循环执行等待IO、解析IO、回填结果等流程;
本文先整体介绍一下memcached的整体流程,后续章节在介绍一些memcached有特色的地方
原理介绍
整体类图
- MemcachedClient作为整个SDK的入口
- MemcachedConnectionFactory:作为一个工厂类,创建MemcachedConnection
- MemcachedConnection:整体是一个单线程IO循环,主要有获取待执行的MemcachedNode,发送命令、接收服务端响应结果等
- MemcachedNode:主要保存Operation、分发Operation(根据不同状态保存到不同队列,比如:刚创建的Operation放入InputQueue, readQ:等待server响应结果的Op, writeQ:即将要发送给server的Op);一个memcached分片对应一个MemecachedNode
- OperationImpl:从ByteBuf获取数据,进行解码后,赋值给CallBack的value;
整体流程图
源码上分析:spymemcached 有特色的地方
1、客户端实现ketama 一致性hash
- 用户在执行命令的时候,locator调用getPrimary从hash环上寻找最优节点
- 如果没有找到;locator调用getSequence寻找备用节点
- 常见的中间件如:kafka、grpc、rocketMQ等,都在客户端实现的hash路由,这样利用无限的横向能力增加集群的扩展能力,减少转发带来的性能损耗;
1.1 MemcachedNode.addOperation 调用点
protected void addOperation(final String key, final Operation o) {
MemcachedNode placeIn = null;
try {
locator.begin();
// 获取primary node
MemcachedNode primary = locator.getPrimary(key);
// 判断当前节点是否存活
if (isMemcachedNodeActive(primary) || failureMode.get() == FailureMode.Retry) {
placeIn = primary;
} else if (failureMode.get() == FailureMode.Cancel) {
o.cancel();
} else {
// primary node down -> 就会去找sequence node
Iterator<MemcachedNode> i = locator.getSequence(key);
while (placeIn == null && i.hasNext()) {
MemcachedNode n = i.next();
if (isMemcachedNodeActive(n)) {
placeIn = n;
}
}
if (placeIn == null) {
placeIn = primary;
logger.warn("Could not redistribute to another node, retrying primary node for {}.", key);
}
}
if (placeIn != primary) {
if (rehashListener != null) {
rehashListener.onRehash(key, o.getClass().getSimpleName(), primary, placeIn);
}
rateLogger.warn("Memcached Op rehash occurred, key: [{}], op: [{}], primaryNode: [{}], actualNode: [{}]",
key, o.getClass().getSimpleName(), primary.getSocketAddress(),
placeIn == null ? null : placeIn.getSocketAddress());
}
} finally {
locator.end();
}
assert o.isCancelled() || placeIn != null : "No node found for key " + key;
if (placeIn != null) {
MemcachedNode node = placeIn;
addOperationInterceptor.interceptCall(placeIn, o, () -> addOperation(node, o));
}
}
1.2 ketama hash实现源码
- 如果是加权ketama,则计算总权重,查看当前节点权重占总权重的百分比,来确定在一致性hash环上生成的虚拟节点数量;
- 如果不是加权ketama,首先利用MD5生成一个byte[], 然后从后往前,每次取4位,分别左移0,8,16,24位,再异或形成一个新的long型数字,保存到nodeMap中;
- 调用getPrimary的时候,先计算key的md5,然后从后往前,每次取4位,分别左移0,8,16,24位,再异或形成一个新的long型数字,在nodemap.ceilingEntry()取向下取整的MemcachedNode
protected void setKetamaNodes(List<MemcachedNode> nodes) {
TreeMap<Long, MemcachedNode> newNodeMap =
new TreeMap<>();
int numReps = config.getNodeRepetitions();
int nodeCount = nodes.size();
int totalWeight = 0;
if (isWeightedKetama) {
for (MemcachedNode node : nodes) {
totalWeight += weights.get(node.getSocketAddress());
}
}
for (MemcachedNode node : nodes) {
// 加权逻辑,如果权重比较重,那么生成的节点就会比较多;
if (isWeightedKetama) {
int thisWeight = weights.get(node.getSocketAddress());
float percent = (float)thisWeight / (float)totalWeight;
int pointerPerServer = (int)((Math.floor((float)(percent * (float)config.getNodeRepetitions() / 4 * (float)nodeCount + 0.0000000001))) * 4);
for (int i = 0; i < pointerPerServer / 4; i++) {
for(long position : ketamaNodePositionsAtIteration(node, i)) {
newNodeMap.put(position, node);
logger.debug("Adding node {} with weight {} in position {}", node, thisWeight, position);
}
}
} else {
// Ketama does some special work with md5 where it reuses chunks.
// Check to be backwards compatible, the hash algorithm does not
// matter for Ketama, just the placement should always be done using
// MD5
if (hashAlg == DefaultHashAlgorithm.KETAMA_HASH) {
// 生成4个点保存在nodeMap中;
for (int i = 0; i < numReps / 4; i++) {
for(long position : ketamaNodePositionsAtIteration(node, i)) {
newNodeMap.put(position, node);
logger.debug("Adding node {} in position {}", node, position);
}
}
} else {
for (int i = 0; i < numReps; i++) {
newNodeMap.put(hashAlg.hash(config.getKeyForNode(node, i)), node);
}
}
}
}
assert newNodeMap.size() == numReps * nodes.size();
ketamaNodes = newNodeMap;
}
private List<Long> ketamaNodePositionsAtIteration(MemcachedNode node, int iteration) {
List<Long> positions = new ArrayList<>();
// 根据socketAddress计算MD5
byte[] digest = DefaultHashAlgorithm.computeMd5(config.getKeyForNode(node, iteration));
// 随机打散(从后往前,每次取4位,每一位分别左移0,8,16,24位,然后相或形成一个随机数),保存到positions数组
for (int h = 0; h < 4; h++) {
Long k = ((long) (digest[3 + h * 4] & 0xFF) << 24)
| ((long) (digest[2 + h * 4] & 0xFF) << 16)
| ((long) (digest[1 + h * 4] & 0xFF) << 8)
| (digest[h * 4] & 0xFF);
positions.add(k);
}
return positions;
}
2、回调通知
源码地址:net.spy.memcached.MemcachedClient#asyncGet
- 从OperationFactory中获取的Operation时,就会传入一个CallBack,
- 在这个CallBack类中,定义了三个主要方法;
- receiveStatus() 回填状态,
- gotData():设置从服务端返回的数据结果,
- complete():在执行完成后, 进行做signalComplete,看看是否有后置的数据流、逻辑流需要执行;
- 最后OperationFuture增加一个Listener的回调通知,这是为了在Op执行结束后,做一些数据清理、完成通知之类的动作;
public <T> GetFuture<T> asyncGet(final String key, final Transcoder<T> tc) {
final Map<AsyncOpListener<Object>, Object> before = before(get);
final CountDownLatch latch = new CountDownLatch(1);
final GetFuture<T> rv = new GetFuture<>(latch, operationTimeout, key, executor);
Operation op = opFact.get(key, new GetOperation.Callback() {
private Future<T> val;
@Override
public void receivedStatus(OperationStatus status) {
rv.set(val, status);
}
@Override
public void gotData(String k, int flags, byte[] data) {
assert key.equals(k) : "Wrong key returned";
val =
tcService.decode(tc, new CachedData(flags, data, tc.getMaxSize()), k, decodeListeners);
}
@Override
public void complete() {
latch.countDown();
rv.signalComplete();
}
});
rv.setTimeoutListeners(get, timeoutListeners);
rv.setOperation(op);
mconn.enqueueOperation(key, op);
rv.addListener(future -> {
for (Entry<AsyncOpListener<Object>, Object> entry : before.entrySet()) {
entry.getKey().onGetCompletion(entry.getValue(), future);
}
});
return rv;
}
总结
- spymemcached为了高性能,实现了IO异步化;在异步化,不可避免的需要通过回调来回填从memcached返回的结果;以及针对可能得结果做一些回调事件的监听;比如:CallBack、Listener
- 在客户端实现Ketama一致性hash环,可以解决server侧的扩展性问题,同时引入虚拟节点,避免数据分布不均,也为了适配memcached集群无中心的架构