spymemcached的整体源码结构介绍

73 阅读5分钟

spymemcached背景介绍

memcached是一款比较悠久、设计优秀、结构简单、性能优良的对象缓存数据库;由于其多线程能力,在相同CPU核数、相同内存的情况下,memcached的性能要优于redis的性能;一些大公司,诸如:FaceBook、Twitter、YouTube等公司也都在使用;

spymemcached sdk则是java用户连接memcached的便捷通道;但由于memcached本身的可靠性、可扩展性不够鲁棒,所以在SDK侧补齐这块的功能,比如:

  1. 客户端实现的ketama一致性hash环,来保障服务端无限横向扩展;
  2. 通过ketama hash虚拟节点可以保证数据不会产生倾斜,数据分布不均匀的情况;
  3. 通过Redistribute Mode来做到数据的冗余备份,增强服务的可用性、可靠性;

同时spymemcached sdk为了高性能,避免频繁的上下文切换带来的性能损耗;(曾经处理过一个问题:服务上线以后,性能劣化严重;原因是8核CPU开了1600个线程,导致上下文切换每秒达到160万次); spymemcached会在一个线程里循环执行等待IO、解析IO、回填结果等流程;

本文先整体介绍一下memcached的整体流程,后续章节在介绍一些memcached有特色的地方

原理介绍

整体类图

  1. MemcachedClient作为整个SDK的入口
  2. MemcachedConnectionFactory:作为一个工厂类,创建MemcachedConnection
  3. MemcachedConnection:整体是一个单线程IO循环,主要有获取待执行的MemcachedNode,发送命令、接收服务端响应结果等
  4. MemcachedNode:主要保存Operation、分发Operation(根据不同状态保存到不同队列,比如:刚创建的Operation放入InputQueue, readQ:等待server响应结果的Op, writeQ:即将要发送给server的Op);一个memcached分片对应一个MemecachedNode
  5. OperationImpl:从ByteBuf获取数据,进行解码后,赋值给CallBack的value; image.png

整体流程图

image.png

源码上分析:spymemcached 有特色的地方

1、客户端实现ketama 一致性hash

  1. 用户在执行命令的时候,locator调用getPrimary从hash环上寻找最优节点
  2. 如果没有找到;locator调用getSequence寻找备用节点
  3. 常见的中间件如: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实现源码
  1. 如果是加权ketama,则计算总权重,查看当前节点权重占总权重的百分比,来确定在一致性hash环上生成的虚拟节点数量;
  2. 如果不是加权ketama,首先利用MD5生成一个byte[], 然后从后往前,每次取4位,分别左移0,8,16,24位,再异或形成一个新的long型数字,保存到nodeMap中;
  3. 调用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

  1. 从OperationFactory中获取的Operation时,就会传入一个CallBack,
  2. 在这个CallBack类中,定义了三个主要方法;
  3. receiveStatus() 回填状态,
  4. gotData():设置从服务端返回的数据结果,
  5. complete():在执行完成后, 进行做signalComplete,看看是否有后置的数据流、逻辑流需要执行;
  6. 最后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;
}

总结

  1. spymemcached为了高性能,实现了IO异步化;在异步化,不可避免的需要通过回调来回填从memcached返回的结果;以及针对可能得结果做一些回调事件的监听;比如:CallBack、Listener
  2. 在客户端实现Ketama一致性hash环,可以解决server侧的扩展性问题,同时引入虚拟节点,避免数据分布不均,也为了适配memcached集群无中心的架构