分布式内存存储式元数据服务的构建

Posted Android路上的人

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分布式内存存储式元数据服务的构建相关的知识,希望对你有一定的参考价值。

文章目录

前言


在前面的文章里(基于状态机方法构建高容错性服务, Apache Ratis的Ratis Server主从同步机制),笔者谈论过如何利用状态机理论来构建replicated server的文章,以此做到服务的高可用性,可做到随时的动态切换。前面文章已经详细阐述里面replicated的原理细节内容,本文笔者打算谈论其中一类特殊的数据服务:纯内存式的replicated server,即In memory的replicated server。纯内存式的数据服务在读写性能上毫无疑问有着极快的响应时间,在实际用途中也有很多的适用场景。下面我们来聊聊这块的内容以及对此的一个简单代码实现。

内存式元数据服务概述


内存元数据服务,顾名思义,就是将系统的元数据信息全部加载到内存中进行访问。鉴于内存访问速度远远快于磁盘读写速度,因此内存元数据服务有着其固有的一大优势:数据读写快。但是它同样有着相比于磁盘做存储而言的一些劣势,

  • 内存空间有限,不如磁盘空间大,论单节点而言,一个系统服务所在节点的可用内存大的会达到百GB级别,但是磁盘一个节点可达到TB级别。
  • 内存数据转瞬即逝,需要有好的策略定期持久化内存数据出去。

不过回头来看,上百GB级别的内存空间用来做元数据的存储还是足够的,毕竟每个元数据单位空间占据不至于那么大。以HDFS NameNode为例,上100GB的NN内存使用可以容纳2亿级别的block元数据信息了。

通用的简单K-V内存存储的replicated server


如小标题所示,本文笔者想要阐述的是一种较为简单的,纯内存存储的K-V键值对的replicated server。这类replicated server将会提供以下基本的服务要求:

  • 内存存储元数据数据,操作响应时间短
  • K-V存储的使用方式,对用户友好,易于使用
  • 对上层应用提供通用的元数据存储功能
  • 具有replicated server属性,能做到高可用性,错误出现时能做到容灾切换

以下是内存存储replicated server的架构模式图:

在上图中,MapStore是实质存储KV键值的map存储数据结构,MetaMap是map store的元数据存储集。Client到Replicated Server1连线部分的文字即为Client实际操作的使用命令方式:

put操作 <目标操作map名称> <key名称> <value待存储的值>
get操作 <目标操作map名称> <待取出的key名称>

了解完内存式的replicated server后,我们来了解对此server的一个简单实现,来自于Apache Ratis的JIRA: RATIS-40: Replicated Map

内存式Replicated server的实现


在内存式Replicated server的整个模块实现中,可以进一步划分为client side和server side的实现。

Client side的实现


Client side端的实现相比较Server端要简化许多。在Client端,暴露给用户使用的CLI命令如下所示:

Usage: ratis rmap <command> [<args>]");
Commands:
create <rmap_name> <key_class> <value_class>
put <rmap_name> <key> <value>
get <rmap_name> <key>

这里的rmap即replicated map的意思,map为KV内存键值对存储map,以下是对应crete操作方法:

  private int create(String[] args) throws ClassNotFoundException, IOException 
    if (args.length < 3) 
      return printUsage();
    

    // 1)构造replicated map的元信息
    RMapInfo info = RMapInfo.newBuilder()
        .withName(RMapName.valueOf(args[0]))
        .withKeyClass(Class.forName(args[1]))
        .withValueClass(Class.forName(args[2]))
        .build();

    // 2)获取admin接口创建Rmap
    try (Client client = ClientFactory.getClient(new FileQuorumSupplier());
         Admin admin = client.getAdmin()) 
      info = admin.createRMap(info);
      System.out.println("Created rmap:" + info);
    

    return 0;
  

另外的put方法实现如下:

  private int put(String[] args) throws IOException 
    if (args.length < 3) 
      return printUsage();
    

    // 1)解析得到目标RMap id
    long rmapId = Long.parseLong(args[0]);
    try (Client client = ClientFactory.getClient(new FileQuorumSupplier());
         RMap<String, String> rmap = client.getRMap(rmapId)) 
      // 2)将解析得到的k,v值存入解析得到的RMap内
      rmap.put(args[1], args[2]);
      System.out.println("Put success rmap:" + rmapId + ", " + args[1] + "=" + args[2]);
    

    return 0;
  

上述方法调用的rmap变量本质上为Client调用远端replicated server的API定义类,类接口定义如下:

/**
 * Replicated in memory Map API定义 
 */
public interface RMap<K, V> extends Closeable 

  // put方法
  void put(K key, V value) throws IOException;

  // 根据键值,取value操作
  V get(K key) throws IOException;

  // scan扫描方法(scan操作在这里并没有暴露在CLI上)
  Iterable<Entry<K, V>> scan(Scan<K> scan) throws IOException;

RMap实例实现如下:

public class RMapImpl<K, V> implements RMap<K, V> 
  // 实质Client调用类
  private final RaftClient raftClient;
  // 目标操作的RMmap元信息
  private final RMapInfo info;
  // key值序列化类
  private final Serde<K> keySerde;
  // value值序列化类
  private final Serde<V> valueSerde;

  ...
  
  public void put(K key, V value) throws IOException 
	// 1)根据key, value的序列化类进行K,V值的序列化操作
    ByteString k = keySerde.serialize(key);
    ByteString v = valueSerde.serialize(value);

    // 2)构造包含K,V的put request请求
    Request req = ProtobufConverter.buildMultiPutRequest(info.getId(), k, v);
    // 3)调用Client发起请求
    RaftClientReply reply = raftClient.send(req);

    if (!reply.isSuccess()) 
      throw new IOException("Request failed :" + reply);
    

    Response resp = ProtobufConverter.parseResponseFrom(reply.getMessage().getContent());
    MultiActionResponse multiActionResponse = resp.getMultiActionResponse();
    assert multiActionResponse != null;

    // nothing to check in the response
  

至此,client端的调用部分基本完成,下面我们来看看server端的处理过程。

Server side的实现


Server side的实现还是基于Raft server带上一个指定的StateMachine进行实现的。

  private RMapServer(String id, QuorumSupplier quorumSupplier) throws IOException 
    properties = new RaftProperties();
    properties.setBoolean(RaftServerConfigKeys.RAFT_SERVER_USE_MEMORY_LOG_KEY, false);

    ...

    raftServer = RaftServer.newBuilder()
        .setServerId(myId)
        .setPeers(peers)
        .setProperties(properties)
        // 新建RMapStateMachine作为状态机
        .setStateMachine(new RMapStateMachine())
        .build();
  

在RMapStateMachine中,维护了内存元数据存储结果RMapStore,

public class RMapStateMachine extends BaseStateMachine 
  static final Logger LOG = LoggerFactory.getLogger(RMapStateMachine.class);

  // RMap的元数据map
  private final MetaMap metaMap;
  private final SimpleStateMachineStorage storage;
  // replicated map集
  private final ConcurrentHashMap<Long, RMapStore> rmaps = new ConcurrentHashMap<>();
...

client的rmap请求到StateMachine这边被转化为了transaction,然后再被进一步处理,

  public TransactionContext applyTransactionSerial(TransactionContext trx) throws IOException 

    ByteString data = trx.getSMLogEntry().get().getData();
    WALEntry walEntry = WALEntry.parseFrom(data);
    if (LOG.isDebugEnabled()) 
      LOG.debug("Applying WALEntry:", TextFormat.shortDebugString(walEntry)); // TODO: trace
    

    // 1)从transaction中获取rmapId,然后借此得到RMap store.
    long rmapId = walEntry.getRmapId();
    RMapStore store = rmaps.get(rmapId);

    if (store == null) 
      throw new IOException("Cannot find RMap with id:" + rmapId + ", for transaction: " + trx);
    

    // TODO: locks and all

    // 根据WAL类型,进行RMap store的操作
    switch (walEntry.getWALEntryTypeCase()) 
      // 如果是CREATE RMAP请求,则进行create map操作
      case CREATE_RMAP_ENTRY:
        trx.setStateMachineContext(applyCreateRMap(trx, walEntry));
        break;
      case WALENTRYTYPE_NOT_SET: // Multi action
      // 另外一种,则进行K,V信息的apply到RMap store内
        trx.setStateMachineContext(applyToStore(walEntry, store));
        break;
    

    LOG.info(store.debugDump()); // TODO: remove

    return super.applyTransactionSerial(trx);
  

...

  /**
   * 将WAL entry中的值apply到RMap store中
   */
  private Response applyToStore(WALEntry walEntry, RMapStore store) 
	// 遍历wal entry中的键值对,put到RMap store
    for (Entry entry : walEntry.getEntryList()) 
      ByteString key = entry.getKey();
      ByteString value = entry.getValue();
      store.put(key, value);
    
    return ProtobufConverter.buildMultiPutResponse(Stream.of(walEntry));
  

最后我们来进入上述的实质存储结构RMapStore的实现,对于外界来说,它提供的接口其实很简单,就是标准的KV键值对存储。

首先我们来看其内部变量定义,

/**
 * This is the in-memory implementation for a sorted map of K to V. The concurrency model
 * is pretty simple for now, and does not pipeline the memory insertions with the WAL commits.
 * @param <K> the class for keys
 * @param <V> the class for values
 */
public class RMapStore<
    K,
    V,
    KS extends Serde<K>,
    VS extends Serde<V>,
    KC extends Comparator<? super K>> 
  // RMap的元数据信息
  private final RMapInfo<K, V, KS, VS, KC> info;
  // key的序列化类信息
  private final Serde<K> keySerde;
  // value值的反序列类信息
  private final Serde<V> valueSerde;
  // 用线程安全,排序map(可便于scan)做K,V值存储介质 
  private final ConcurrentSkipListMap<K, V> map;
  ...

因为要支持线程安全的排序map的属性,于是这里使用了ConcurrentSkipListMap而非TreeMap。二者结构在排序实现原理上并不相同,后者使用的是红黑树原理而前者则使用的是"跳跃表"的概念。

我们挑选其中的put方法,看看它的实现是如何的,

  /**
   * Put执行方法
   */
  public void put(ByteString key, ByteString value) 
	// 1)反序列化键值
    K k = keySerde.deserialize(key);
    V v = valueSerde.deserialize(value);
    // 2)将反序列的键值放入排序map内
    put(k,v);
  

  public void put(K key, V value) 
    map.put(key, value);
  

综上实现,将client和server端的实现完全串联起来,关系图如下所示:

以上就是本文所要阐述的纯内存式存储的元数据replicated server实现,在一定场合还是能够提供一种较为简单的数据服务能力的。

引用


[1].https://issues.apache.org/jira/browse/RATIS-40 . Replicated Map
[2].https://blog.csdn.net/androidlushangderen/article/details/97974228 . 基于状态机方法构建高容错性服务
[3]https://blog.csdn.net/Androidlushangderen/article/details/99663173 . Apache Ratis的Ratis Server主从同步机制

以上是关于分布式内存存储式元数据服务的构建的主要内容,如果未能解决你的问题,请参考以下文章

MongoDB4.0构建分布式分片群集

补习系列(15)-springboot 分布式会话原理

dataframe最大存储多少数据?

k8s中的Mysql数据库持久化存储

如何用分布式缓存服务实现Redis内存优化

redis和memcache的区别