ServerSAN前端接口设计

本文探讨了ServerSAN存储系统前端接口的选择,着重分析了iSCSI接口在分布式存储中的局限性,如多路径支持能力弱、不支持Shard分片等问题,并提出了ServerSAN接口设计应具备的支持动态系统特性、多路径和高性能编程模型等原则。PureFlash客户端接口设计案例展示了如何实现自动获取元数据和动态变化感知,以适应ServerSAN的特性需求。
摘要由CSDN通过智能技术生成

前端接口是指Client端使用什么方法访问存储系统。比如一个硬盘,可以是SAS接口也可以是IDE接口,操作系统里就要用不同的驱动来访问这个硬盘。对于一个网络存储系统也是这个原理,操作系统要有相应的访问方法。

对于网络存储系统,Client端接口大体可以分为标准接口和非标准接口。所谓的标准接口就是某个访问方式或者驱动已经内置到了OS里面,或者更进一步,这个驱动已经非常成熟,广为接受,你不需要要求用户装额外的软件就可以使用。非标准就是需要用户安装这个存储系统专有的Client软件、驱动才能使用。

能够被称为存储系统的标准接口其实是非常少的。块存储而言也就是iSCSI/SCSI/FC,未来NVME over Fabric有望成为新的标准接口。文件存储也就是NFS, CIFS两种。当前NoF还不普及,对于IP SAN存储而言也就只有iSCSI这一个接口能称之为标准接口了。

iSCSI接口:

iSCSI协议是SCSI over IP的简称,SCSI协议是主机内部总线,iSCSI为了适应TCP/IP网络,做了很多改进和完善[1]:

  1. SCSI的命令字是变长的,这个特性对于网络应用是非常不友好的,应用需要读2次才能把command PDU读回来。第一次读Op或者length,第二次读剩下的部分。iSCSI协议在设计时也考虑到了这点,在iSCSI协议里command PDU被改为了固定的48Byte长,应用只需要按照固定长度读一次。

  2. 数据传输从pull模式改成push模式,允许initiator在target没有请求的情况下就发送write payload。
    这些都是为了让SCSI从本机总线适应到远端网络总线模型,提高传输性能。虽然如此,iSCSI仍然被认为是性能差,CPU消耗高。除此之外iSCSI还有一些问题使得其在分布式存储场景难以适用:

  3. 多路径支持能力弱
    iSCSI 多路径通常支持2条路径,这是因为传统SAN设备多为双控的原因。使用多路径需要client端的配置非常复杂,且是人工配置,对用户很不友好。
    在分布式存储通常会采用多副本存储,3副本是很常见的,这样就需要超过2条路径。更重要的是,一个volume存储的位置是不确定的,只有SAN的元数据服务器知道这个Volume的3个副本存储在哪些节点上。考虑到副本会因为recover/rebalance等操作而转移位置,这种情况下在initiator端手工配置多路径几乎是无法进行的。

  4. 无法支持Shard分片
    分布式存储为了解决大容量volume的问题,通常会采用Shard的方式。也就是类似RAID0那样,用多个小盘拼成一个 大盘。但是不同于RAID0, 分布式系统的不同Shard是位于不同的存储节点上的,也就是说client访问一个Volume的不同部分时需要访问不同的存储节点。举个例子:
    如果一个128GB的Volume分成2个64G的Shard,Shard[0]放置在nodeA, Shard[1]放置在nodeB,那么client访问时就需要根据IO的LBA地址,如果是落在前64G,就要通过与nodeA的网络连接发送请求,否则就通过nodeB的网络连接。 多Shard支持是iSCSI规范里没有定义的,也无法支持。
    当然我们这里仍然可以为iSCSI做一次辩护,可以将每个Shard作为一个LUN 挂载到client端,然后在Client端通过RAID0或者device mapper技术组成一个大的Volume,同样能达到相同大Volume效果。然而这仍然有不同:
    1)系统的发展总是希望由繁向简,用软件工程的话讲是高内聚低耦合,不要把复杂性扩散。只有这样系统才能向更高层级进化。所以最好由存储系统自己解决超大容量问题,而不是由client来处理。
    2)仍然是老生常谈的Server SAN动态变化,Shard的位置是不固定的,对于的缺乏动态迁移能力的iSCSI而言无论如何也是胜任不了的。
    上面的论述并没有否认iSCSI可以作为分布式存储的前端接口,只是说iSCSI不是分布式存储的最佳接口。

ServerSAN接口设计原则

那么ServerSAN存储的最佳接口应该是什么样子的?

  1. 能够良好支持ServerSAN的分布式系统特性
    传统的存储对外暴露的接口点相对是固定的,可能是双控或者多控的几个控制器。双控SAN存储,所有的Volume都是由同样的一对冗余机头导出,Client在挂载Volume时通过multipath软件同时指定这两个机头的IP即可。而ServerSAN则不然,ServerSAN系统由众多控制器组成,每个控制器都直接对外服务,不同的Volume可能由完全不同或者部分不同的存储节点导出。这就使得具体访问点的确定变得困难,必须由软件自动协商确定而不是人工确定。因此Client端需要参与元数据流程,获取元数据并根据元数据来确定Volume是由哪些机头导出。

  2. 能够良好支持ServerSAN的动态特性
    进一步,一次性获取元数据还不够。在系统运行过程种,数据的分布存储情况会发生变化,比如由于节点故障,导致对部分数据的访问点发生了变化,或者rebalance动作导致数据位置进行了迁移,元数据相应会发生变化。Client端需要能同步感知这样的变化。

  3. 支持多种类型的Client端
    在云计算时代,访问存储的客户端类型变得多样化,粗分的话可以分成hypervisor和物理机两种。再细分可以包括不同类型的Hypervisor(qemu, Xen, vSphere …) , 裸金属虚拟化,物理机, 普通容器,安全容器,等等…。作为ServerSAN的提供方会发现每一种类型的应用都有细微的差异,但是基本上能支持物理机和hypervisor访问就够了,剩下的变化不大。
    有一类新的需要关注的接口是智能网卡接口。智能网卡当前云计算的一个重要技术变革,对于存储而言就是将本来hypervisor需要做的工作卸载到智能网卡上执行,网卡可以用硬件逻辑也可以用CPU完成。

在云计算时代,除了数据路径支持,还需考虑控制路径。传统的Client端只有数据路径,这是因为控制路径由人工完成了,比如iSCSI在挂载时由人工指定target的IP。而在云计算时代,控制路径通常由IaaS, PaaS软件自动完成,而不同的平台软件需要不同的控制接口。OpenStack的块存储接口是Cinder, K8S的块存储接口是CSI。因此一个现代分布式存储系统还要为各种云平台软件提供接口。

控制路径的驱动和数据路径的驱动是不同的,控制路径一般只是在Volume创建、加载时工作,强调的是功能完备;对于数据路径一般对性能更敏感。

  1. 多路径与强壮的故障恢复能力
    多路径能力是高可靠的企业存储必备的能力。通常由multipath软件来实现这个功能,当一条路径出现IO超时后就切换到另外的路径继续IO操作。ServerSAN系统由于集群变得更加复杂,路径也是动态变化的,通用的多路径软件无法适应这样的复杂环境。需要ServerSAN自己的客户端深度参与集群的运行才能实现多路径能力。
  2. 高性能编程模型
    严格说这并不是ServerSAN对Client端软件提出的需求, 这是所有软件永远的追求。只是在SSD普及后,应用对性能的追求上升到了新高度。这方面可以考虑的技术点包括使用epoll模型处理网络、使用RDMA技术、使用多队列接口等。
  3. 其他因素
    有一些ServerSAN的实现需要Client参与实现更多功能,比如:
    多Client并发访问,以支持OracleRAC场景;
    卷组协调,在Volume间同步做快照;
    QoS 能力;

PureFlash客户端接口设计

我们按照PureFlash client的工作过程,来说明其设计原理,以及如何满足上面的设计原则。

  1. open volume
    open volume的过程是client获取元数据的过程。包括下面几个步骤:
    a) 从config 文件获取zookeeper的地址,然后访问zookeeper获取master conductor.
    配置文件以的格式如下:
[cluster]
name=cluster1
[zookeeper]
ip=192.168.1.1:2181,192.168.1.2:2181,192.168.1.3:2181

从配置文件获得zk的IP,然后获取active conductor, 过程如下:

string get_master_conductor_ip(const char *zk_host, const char* cluster_name)
{
    struct String_vector condutors = {0};
    char **str = NULL;
    zhandle_t *zkhandle = zookeeper_init(zk_host, NULL, ZK_TIMEOUT, NULL, NULL, 0);
	DeferCall _r_z([zkhandle]() { zookeeper_close(zkhandle); });

	int rc = 0;
	string zk_root = format_string("/pureflash/%s/conductors", cluster_name);
	rc = zoo_get_children(zkhandle, zk_root.c_str(), 0, &condutors);
	DeferCall _r_c([&condutors]() {deallocate_String_vector(&condutors); });
    str = (char **)condutors.data;
    qsort(str, condutors.count, sizeof(char *), cmp);  //这一行是关键,对zk上注册的conductor进行排序

	char leader_path[256];
    int len = snprintf(leader_path, sizeof(leader_path), "%s/%s", zk_root.c_str(), **str[0]**);  // str[0], 即排序第一的conductor就是active conductor 在zk上的node

	char ip_str[256];
	len = sizeof(ip_str);
	rc = zoo_get(zkhandle, leader_path, 0, ip_str, &len, 0);  //从active conductor node获取active conductor IP
    ip_str[len] = 0;
    S5LOG_INFO("Get S5 conductor IP:%s", ip_str);
    return std::string(ip_str);
}

这里有个前提,是conductor之间通过zookeeper实现了一个分布式锁。只有获得锁的conductor才能成为active conductor.
分布式锁的实现是一个协作过程,具体过程为:

  1. conductor上线后向zookeeper注册一个EPHEMERAL_SEQUENTIAL类型的节点
  2. 检查所有zk上注册的conductor节点,按照序号进行排序,看排在第一位的是不是自己。如果是,就表示自己获得了锁。
	public static void registerAsConductor(String managmentIp, String zkIp) throws Exception
	{

			myZkNodePath = zk.create(ClusterManager.zkBaseDir + "/conductors/conductor", managmentIp.getBytes(),
					Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
	}
	public static void waitToBeMaster(String managmentIp)
	{
			synchronized (locker)
			{
				List<String> list = zk.getChildren(zkBaseDir + "/conductors", true);
				String[] nodes = list.toArray(new String[list.size()]);
				Arrays.sort(nodes);
				while (true) //一直等待,直到获得锁
				{
					String leader = new String(zk.getData(zkBaseDir + "/conductors/" + nodes[0], true, null));
					if(leader.equals(managmentIp))
						break; //自己处于第一位,表示自己可以获得锁成为Master
					logger.info("the master is {}, not me, waiting...",	leader);
					locker.wait(); //在register注册时同时监听了父节点,当zk上有变化时会唤醒这里的等待
					list = zk.getChildren(zkBaseDir + "/conductors", true);
					nodes = list.toArray(new String[list.size()]);
					Arrays.sort(nodes);
				}
			}
	}

继续open volume的操作,
b) 从master conductor获得Volume的元数据
Client向master conductor发送一个类似这样的请求: curl “http://conductor:49180/s5c/?op=open_volume&volume_name=test_v1”
master向Client返回类似下面的json应答

{
  "status": "OK",                    //Volume的状态,可以是OK, DEGRADED, ERROR
  "volume_name": "test_v1",
  "volume_size": 2147483648,         //以byte为单位的volume大小,在PureFLash里所有大小/容量的单位都是byte
  "volume_id": 1124073472,
  "shard_count": 2,
  "rep_count": 2,
  "meta_ver": 2,
  "snap_seq": -1,
  "shards": [
    {
      "index": 0,
      "store_ips": "172.21.0.13, 172.21.0.15",
      "status": "OK"
    },
    {
      "index": 1,
      "store_ips": "172.21.0.15, 172.21.0.17",
      "status": "OK"
    }
  ],
  "op": "open_volume_reply",
  "ret_code": 0
}

从open volume的应答信息可以看到,Volume的每个shard都返回了各自的IP地址,也就是说不需要client人工确定Volume target的IP地址了。这个设计就可以满足前面第一条原则,无论系统由多少个节点构成,client可以自动获得需要的IP地址。

另外在返回的应答里面还有meta_ver,这一点也很关键。当和某个Volume相关的状态信息,数据布局信息发生变化时,meta_ver就会变化。从而系统就会检测到不一致,这时就会导致client reopen volume。这个设计满足了上面原则的第二条,系统动态变化时client可以感知变化并进行更新。具体感知的过程在我们谈Server端设计时会再讲。

从应答消息里也看到,每个shard都有多个IP地址。每个IP地址都可以完成对这个shard的服务。当client访问某个IP失败时,就会fail over到下一个IP进行访问。

[1] https://storageconference.us/2003/papers/19-Meth-Design.pdf 《Design of the iSCSI Protocol》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值