ServerSAN前端接口设计
前端接口是指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]:
-
SCSI的命令字是变长的,这个特性对于网络应用是非常不友好的,应用需要读2次才能把command PDU读回来。第一次读Op或者length,第二次读剩下的部分。iSCSI协议在设计时也考虑到了这点,在iSCSI协议里command PDU被改为了固定的48Byte长,应用只需要按照固定长度读一次。
-
数据传输从pull模式改成push模式,允许initiator在target没有请求的情况下就发送write payload。
这些都是为了让SCSI从本机总线适应到远端网络总线模型,提高传输性能。虽然如此,iSCSI仍然被认为是性能差,CPU消耗高。除此之外iSCSI还有一些问题使得其在分布式存储场景难以适用: -
多路径支持能力弱
iSCSI 多路径通常支持2条路径,这是因为传统SAN设备多为双控的原因。使用多路径需要client端的配置非常复杂,且是人工配置,对用户很不友好。
在分布式存储通常会采用多副本存储,3副本是很常见的,这样就需要超过2条路径。更重要的是,一个volume存储的位置是不确定的,只有SAN的元数据服务器知道这个Volume的3个副本存储在哪些节点上。考虑到副本会因为recover/rebalance等操作而转移位置,这种情况下在initiator端手工配置多路径几乎是无法进行的。 -
无法支持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存储的最佳接口应该是什么样子的?
-
能够良好支持ServerSAN的分布式系统特性
传统的存储对外暴露的接口点相对是固定的,可能是双控或者多控的几个控制器。双控SAN存储,所有的Volume都是由同样的一对冗余机头导出,Client在挂载Volume时通过multipath软件同时指定这两个机头的IP即可。而ServerSAN则不然,ServerSAN系统由众多控制器组成,每个控制器都直接对外服务,不同的Volume可能由完全不同或者部分不同的存储节点导出。这就使得具体访问点的确定变得困难,必须由软件自动协商确定而不是人工确定。因此Client端需要参与元数据流程,获取元数据并根据元数据来确定Volume是由哪些机头导出。 -
能够良好支持ServerSAN的动态特性
进一步,一次性获取元数据还不够。在系统运行过程种,数据的分布存储情况会发生变化,比如由于节点故障,导致对部分数据的访问点发生了变化,或者rebalance动作导致数据位置进行了迁移,元数据相应会发生变化。Client端需要能同步感知这样的变化。 -
支持多种类型的Client端
在云计算时代,访问存储的客户端类型变得多样化,粗分的话可以分成hypervisor和物理机两种。再细分可以包括不同类型的Hypervisor(qemu, Xen, vSphere …) , 裸金属虚拟化,物理机, 普通容器,安全容器,等等…。作为ServerSAN的提供方会发现每一种类型的应用都有细微的差异,但是基本上能支持物理机和hypervisor访问就够了,剩下的变化不大。
有一类新的需要关注的接口是智能网卡接口。智能网卡当前云计算的一个重要技术变革,对于存储而言就是将本来hypervisor需要做的工作卸载到智能网卡上执行,网卡可以用硬件逻辑也可以用CPU完成。
在云计算时代,除了数据路径支持,还需考虑控制路径。传统的Client端只有数据路径,这是因为控制路径由人工完成了,比如iSCSI在挂载时由人工指定target的IP。而在云计算时代,控制路径通常由IaaS, PaaS软件自动完成,而不同的平台软件需要不同的控制接口。OpenStack的块存储接口是Cinder, K8S的块存储接口是CSI。因此一个现代分布式存储系统还要为各种云平台软件提供接口。
控制路径的驱动和数据路径的驱动是不同的,控制路径一般只是在Volume创建、加载时工作,强调的是功能完备;对于数据路径一般对性能更敏感。
- 多路径与强壮的故障恢复能力
多路径能力是高可靠的企业存储必备的能力。通常由multipath软件来实现这个功能,当一条路径出现IO超时后就切换到另外的路径继续IO操作。ServerSAN系统由于集群变得更加复杂,路径也是动态变化的,通用的多路径软件无法适应这样的复杂环境。需要ServerSAN自己的客户端深度参与集群的运行才能实现多路径能力。 - 高性能编程模型
严格说这并不是ServerSAN对Client端软件提出的需求, 这是所有软件永远的追求。只是在SSD普及后,应用对性能的追求上升到了新高度。这方面可以考虑的技术点包括使用epoll模型处理网络、使用RDMA技术、使用多队列接口等。 - 其他因素
有一些ServerSAN的实现需要Client参与实现更多功能,比如:
多Client并发访问,以支持OracleRAC场景;
卷组协调,在Volume间同步做快照;
QoS 能力;
PureFlash客户端接口设计
我们按照PureFlash client的工作过程,来说明其设计原理,以及如何满足上面的设计原则。
- 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.
分布式锁的实现是一个协作过程,具体过程为:
- conductor上线后向zookeeper注册一个EPHEMERAL_SEQUENTIAL类型的节点
- 检查所有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》
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构