DAOS带来的思考
根据daos docs的描述,DAOS是Intel基于NVMe全新设计开发并开源的异步对象存储,充分利用下一代NVMe技术的优势,对外提供KV存储接口,提供非阻塞事物I/O,端到端完整性,细粒度的数据控制,数据保护以及弹性存储等特征。
intel daos给我们提供了基于NVMe实现高性能存储引擎的参考实现,核心是并行性,如何发挥多核CPU及NVMe设备提供的并行能力。
以大家熟悉的Ceph,BeeGFS为例,对并行不足及超额订购问题进行说明:
- Ceph的每个OSD管理一块SSD,为提升并发能力每个OSD内部会划分多个Shard,Shard内I/O串行执行,Shard间I/O可并发,PG通过模Shard数映射到Shard,接收到的I/O请求根据pg_id取模放入指定的Shard,由Shard内的工作线程调度执行。I/O提交到引擎后,通过单线程libaio(批)提交到设备(独立的KV线程提交元数据I/O,元数据提交后再归还工作线程)。
Ceph OSD通过Shard来控制并发,以粗粒度的PG为并发单位,限制了PG及PG内对象I/O的并发能力,导致系统的并行性不足;理论上可以尽可能的增加PG及OSD Shard的数量(及工作线程数量),以此来提升系统的并发能力,但这会带来严重的线程上下文切换开销,大量的CPU时间被浪费在内核中,导致CPU超额订购问题。
- BeeGFS的每个OSD管理一块SSD,采用Multi-reactor的网络模式,从线程池给每个客户端连接分配一个工作线程用于处理I/O。I/O提交到引擎后,通过POSIX接口(pread/pwrite, read/write)提交到设备,I/O完成后再归还工作线程。
BeeGFS通过从线程池分配一个工作线程来处理每个客户端连接的I/O,工作线程采用同步操作来处理I/O,因此线程会被阻塞直到I/O完成,这严重影响了系统的并行性;SSD的性能很高,需要很多的并发操作才能充分发挥其性能,因此也可以往线程池中添加足够多的工作线程,以此来提升系统的并发能力,但这会带来严重的线程上下文切换开销,大量的CPU时间被浪费在内核中,导致CPU超额订购问题。
daos是如何解决上述问题的呢?,概括起来就两点:轻量线程和非阻塞I/O。下图是我结合dao docs以及daos 代码,绘制的一张daos 并行设计图:
轻量线程:在双路x86系统上,一个DAOS节点(DAOS Server)通常包含两个存储引擎(DAOS Engine),每个引擎绑定一个Socket,管理多个NVMe SSD。为了避免超额订购,daos引擎使用了用户态轻量线程框架-argobots,不仅减少了上下文切换的开销,也实现了在没有内核干预下的完全调度控制。如上图,daos_engine引擎启动时,启动若干执行单元(XS)并绑定到独立的cpu core。每个XS内运行一个自定义的调度器(Scheduler),负责根据策略执行三个线程池(pools)中的轻量线程(协程),进而执行用户任务。从功能角色上,XS分成3类:系统XS,负责处理系统管理任务,如:swim心跳,dRPC请求,dmg请求等;主XS,负责处理客户端I/O;辅助XS,协助主XS进行计算密集型任务,如:加密,压缩,重建等。daos引擎在运行时根据类型将用户任务添加到各XS的各线程池,每类XS有若干,其中每个主XS负责一个Target的I/O处理,Target是逻辑设备,一个Target和NVMe SSD设备的一个IO Channel(QP)绑定,通过多个主XS,多个Targets和多个IO Channel(QP)的绑定,实现NMVe SSD设备的多队列并发能力。
非阻塞I/O:Linux系统提供了多种存储I/O接口,包括:posix接口,libaio,io_uring以及spdk。posix接口是linux上使用最普遍的I/O接口,通过posix接口发起文件系统I/O后,调用线程会被阻塞知道I/O完成。libaio是异步I/O接口,允许一次提交多个I/O请求,减少用户态和内核态切换的开销。io_uring是Linux中新引入的异步I/O接口,用于替代libaio,它通过mmap共享内存在用户态和内核态直接交换数据,允许一次提交多个I/O请求,支持Polling模式,减少中断带来的延迟及CPU开销。spdk是intel实现的存储性能开发套件,它实现了用户态的NVMe驱动,与其他的I/O接口不同,spdk直接在用户态分配QP,提交请求到内存的ring buffer,接着更新标志通知SSD有新的请求到来。关于几种I/O的对比,在2022 SYSTOR上发的一篇论文UnderStanding Modern Storage APIs有详细的分析,daos中使用的是spdk。
DAOS服务端启动流程
下面这张图摘自daos internals,很好的说明了daos server内部的服务分层及其(和模块)的交互关系
如上图,daos服务端包括控制平面和数据平面两个平面,daos的控制面是非中心化的全分布式设计和数据面部署在相同的节点,两者通过dRPC通信(Unix Socket),这点与其他开源的分布式存储系统不同,如:glusterfs,ceph,beegfs等。daos控制服务(daos_server)负责daos数据服务(daos_engine)的管理和资源的供给,如:网络、存储的供给,数据服务的安装、启动等。各daos_server服务承担的角色并不完全对等,(根据配置)其中有部分服务会启动raft一致性协议,负责集群拓扑、节点状态的持久化。下图是我绘制的daos_server和daos_engine启动过程中的交互时序:4节点集群,配置了3个Raft节点,各节点上的daos_server负责拉起本地的daos_engine(在常见的两路x86服务器上,每个socket上会启动一个daos_engine,图中只画了一个)
- 执行
systemctl start daos_server
启动daos_server过程中,启动daos_engine - daos_engine启动后,向daos_server发送ready的dRPC通知,然后等待daos_server的SetUp消息更新init_state
- 各daos_server向MS Leader发起JoinSys的gRPC消息,MS Leader将daos_engine加入集群(db),置位needUpdateGroup标志
- 各daos_server向daos_engine发起SetRank的dRPC消息,daos_engine更新Cart group中的membs和swim membs以及缓存uri
- 各daos_server向daos_engine发起SetUp的dRPC消息,daos_engine设置init_state状态为DSS_INIT_STATE_SET_UP【daos_engine得以继续执行,接着会完整各模块的SetUp,最后激活utl barrier信号量,启动所有的xs,这样就可以提供服务了】
- MS Leader在检查到needUpdateGroup标志后,向daos_engine发起GroupUpdate的dRPC消息(任意一个daos_engine发送成功即可),daos_engine更新Cart group中的membs和swim membs以及缓存uri。【MS Leader所在节点上的daos_engine学习到其他daos_engine的存在】
- MS Leader节点上的daos_engine启动ULT线程(如果未启动),向其他daos_engine发送MAP_UPDATE的gRPC消息,广播membs及状态信息给其他daos_engine。【各daos_engine学习到其他daos_engine的存在】
- 各daos_engine通过SWIM Gossip获取,更新相互间的状态(swim xs调用注册的回调,直接或间接ping swim membs中其他daos_engine),监测到dead engine就通过注册的网络事件回调向daos_server发送RAS_TYPE_STATE_CHANGE的RAS dRPC消息
- 收到RAS dRPC消息的daos_server向MS Leader转发RAS消息,发起ClusterEvent类型的gRPC消息,MS Leader更新engine的状态到db,置位needUpdateGroup标志【会激活过程6,正是通过上述的过程6-9,daos系统实现了engine状态的管理】
注:上述的过程1-9,隐藏了三个daos_server的raft选主。daos系统的集群信息由选出的raft leader负责更新,并通过raft 日志复制在三个节点间实现集群配置的一致性和可靠性。
执行完上面的启动过程,一套新的daos系统就准备就绪了,下文是服务启动过程中核心的代码调用过程说明,代码基于v2.4版本,比较繁琐无趣,对代码感兴趣的读者可以了解下,希望对你走读代码有帮助。
Intel DAOS异步对象存储的实现涉及硬件、操作系统及KV等方面的知识,其内部集成了很多的第三方库,在研究其代码前,建议各位读者:
- 认真阅读daos docs,掌握各种概念,设计原理,代码结构,安装配置方法等
- 了解各第三方库的工作原理,结合examples掌握其使用方法,如:raft,argobots,cart(mercury),spdk,pmdk等
- 认真阅读代码中各模块的README文件,如:
rsvc
,rdb
,vos
,pool
,container
,object
,mgmt
,dtx
等 - 准备一套测试环境,结合运行Log,加深对代码细节的理解
DAOS Server的核心启动过程
DAOS Server是控制面服务,通过systemctl start daos_server
启动服务后,会执行/usr/bin/daos_server start
命令,最终调用src/control/server/server.go
中的Start方法,核心过程如下:
server.Start //环境检查及初始化,初始化server实例,初始化raft,control服务,ms服务,初始化网络,实例化Engine,实例化grpc,启动raft,注册raft回调
newServer //初始化server实例
createServices //初始化raft,control服务,ms服务
initNetwork //初始化网络,创建grpc监听
addEngines //发起BdevPrepareRequest请求准备nvme设备,实例化Engine,注册onAwaitFormat,onStorageReady,onReady等回调
//实例化grpc,注册control,ms服务,启动raft
//MS Leader会在raft回调中启动Loop,负责groupupdate的消息处理-将当前的Engines(Ranks)通过drpc发送给其中的一个Engine
//Egnine收到消息后,更新CART group中的membs列表,启动rsvc mgmt服务,启动广播ULT线程-发送MAP_UPDATE的grpc消息,广播membs给其他Engine,收到消息的Engine会更新membs,执行通知回调crt_plugin_gdata.cpg_event_cbs,这样就和SWIM关联起来了
setupGrpc
server::registerEvents //注册RAS事件,注册raft回调
server::start //启动server:启动gRPC,启动dRPC,启动MS的异步Loop-处理PoolCreate、Join、PoolEvict消息,启动Engine
EngineHarness::Start //逐个启动EngineInstance,安装newOnDrpcFailureFn回调【如果是MS Leader,该回调在dRPC处理失败会触发并发起MS重新选主】
EngineInstance::Run
EngineInstance::startRunner
EngineInstance::format
EngineInstance::awaitStorageReady //格式化(检查)SCM,回调onAwaitFormat【记录一个NoticeInfo的Log】
EngineInstance::createSuperblock //挂载SCM,初始化Superblock并持久化到Superblock文件
EngineInstance::onStorageReady //执行回调onStorageReady【发起BdevPrepareRequest请求清理大页,更新cfg mem_size,检查tmpfs的内存】
EngineInstance::start
Runner::Start //根据配置文件和环境变量,初始化启动命令行
Runner::Runner //启动Engine,在协成中安装退出channel,信号处理器等
EngineInstance::waitReady //等待来自Engine的dRPC ready通知
EngineInstance::finishStartup //更新Superblock,发起RPC加入系统,发起SetRank和SetUp dRPC请求,执行OnReady回调
EngineInstance::handleReady
EngineInstance::updateFaultDomainInSuperblock //更新Superblock中的FaultDomain故障域信息并持久化到Superblock文件
EngineInstance::determineRank //向MS Replica发起RPC Join请求-Instance加入系统,持久化到membership db,分配Rank,置位MS Leader的groupupdate标志,返回后更新Superblock并持久化到Superblock文件
//对于非MS Leader节点(Leader会在上面的join请求中直接发起SetupRank的dRPC请求),向Engine发起SetRank和SetUp dRPC请求,接着设置ready标志(MS Leader根据该标志进行一次groupupdate消息处理)
//收到SetRank dRPC消息,Engine会更新Cart group中的mems列表和 swim membs列表,缓存uri
//收到SetUp dRPC消息,Engine会更新init state,至此,Engine服务就完全run起来了
EngineInstance::SetupRank
EngineInstance::onReady //执行回调onReady【置位allStarted标志,执行Server的onEnginesStarted回调-启动Prometheus exporter】
DAOS Engine的核心启动过程
daos server负责daos engine的管理,包括:启停,成员状态管理等,它们直接通过dRPC(Unix socket)通信。在daos server启动过程中,会根据配置文件及环境变量构建参数启动daos engine,执行的命令是:/usr/bin/daos_engine --args...
,调用的是:src/engine/init.c
中的main方法
main //初始化选项,启动engine,等待退出
parse //根据命令行参数初始化选项
server_init //启动
hlc_recovery_begin //开始HLC时钟恢复
dss_topo_init //初始化CPU topology信息,这里会设置socket的core map以及engine上的目标target数,最佳的core数:2+helpers+targets
register_dbtree_classes //注册各种btree class到全局数组btr_class_registered
abt_init //初始化argobots任务框架,daos使用argobots来进行任务调度
crt_init_opt //初始化cart网络框架:初始化全局的cart结构,根据配置初始化primary和secondary provider,初始化primary和secondary的group信息,NA配置,及其他一些结构
daos_hhash_init //初始化全局handle hash表
pl_init //初始化全局placement hash表
ds_iv_init //初始化incast variable树
modules_load //加载各module,各模块保存在全局列表中,
hlc_recovery_end //停止HLC时钟恢复
dss_set_start_epoch() //初始化初始化epoch:dss_start_epoch
bio_nvme_init //根据配置初始化nvme全局结构,初始化spdk环境
//初始化各模块:调用模块的init接口初始化模块,(如果有key)添加模块到全局数组dss_module_keys,添加gRPC handler到cart的opc_map, 添加dRPC handler到全局register_table
//vos:注册container,dtx,object的btree结构到全局数组btr_class_registered;添加模块到全局数组dss_module_keys
//rdb:初始化rdb_hash表,用来暂存已open的rdb实例
//rsvc:初始化rsvc_hash表,用来暂存已open的rsvc服务,如:rsvc_pool
//pool:初始化pool lru cache,初始化pool handle hash表,注册pool的各incast variable类到ds_iv_class_list列表,初始化pool的默认ACL属性,注册rsvc_pool类到rsvc_classes数组,初始化nvme的reaction ops;添加模块到全局数组dss_module_keys
//container:初始化container lru cache,初始化container handle hash表;添加模块到全局数组dss_module_keys
//object:注册btree class到全局数组btr_class_registered,注册各副本类和EC类到全局数组oc_ident_array,注册EC类到全局数组ecc_array;添加模块到全局数组dss_module_keys
//mgmt:设置id,注册rsvc_mgmt类到rsvc_classes数组
//dtx:根据环境变量初始化选项,注册btree class到全局数组btr_class_registered;添加模块到全局数组dss_module_keys
//securty:初始化socket路径
//rebuild:初始化全局结构rebuild_gst,注册rebuild的各incast variable类到ds_iv_class_list列表;添加模块到全局数组dss_module_keys
dss_module_init_all
dss_srv_init //初始化全局xs结构:xstream_data,初始化全局tls,初始化系统db,启动各xs,启动dRPC监听
vos_standalone_tls_init //初始化全局tls:self_mode.self_tls
dss_sys_db_init //初始化全局系统db,打开系统pool及container
bio_register_bulk_ops //注册公共的bulk分配及释放方法
//逐个启动xs:system xs + main xs + helper xs,xs是argobots中的概念,类比硬件执行单元,可以将xs看成是其软件实现,通常和硬件执行单元一对一映射,如果将xs绑定到特定的core上
//system xs,包括:sys_xs负责系统任务,独占一个core,swim_xs负责保活心跳, drpc_xs负责drpc请求,swim_xs和drpc_xs共享一个core
//main xs:每个target一个 main xs,target是daos里面的逻辑设备抽象,与spdk中的io_channel(nvme qp)对应,用于提高并行性;每个main xs独占一个core
//helper xs:用于协助target处理加密,压缩等高CPU任务
dss_xstreams_init
dss_start_xs_id //分配 cpu core
dss_start_one_xstream
dss_xstream_alloc //创建xs实例
dss_sched_init //创建自定义argobots sheduler,设置任务池pool来区分任务优先级(网络,nvme,通用三个池),添加到argobots框架的执行单元(ULT和tasklet)根据该scheduler定义的策略调度
ABT_xstream_create_with_rank //创建xs,并设置上述的scheduler
daos_abt_thread_create //创建xs的主ULT:dss_srv_handler,添加到网络池(调度执行)
ABT_cond_wait //等待信号量,直到上面的主ULT被调度执行
bio_nvme_ctl //设置全局nvme bdev启动状态为:BIO_CTL_NOTIFY_STARTED,使得后文xs中spdk subsystem初始化及创建的nvme的ULT可以继续执行
drpc_listener_init //在 dRPC xs中创建ULT线程启动dRPC监听
drpc_notify_ready //向daos_server发起ready就绪通知,daos_server会向MS Leader发送JoinSys的gRPC消息,向daos_engine回发SetRank和SetUp的dRPC消息
server_init_state_wait //等待来自daos_server的init_state更新:收到daos_server的SetUp消息后会设置init_state状态为DSS_INIT_STATE_SET_UP
//pool:在system xs创建ULT逐个启动各pool,启动的pool服务添加到rsvc_hash表
//mgmt:创建ULT清理僵尸pool(如果有),清理未完成pool(如果有),初始化pooltgts结构用于跟踪创建过程中的pool
//dtx:给各main xs创建事务批提交ULT:dtx_batched_commit,聚合ULT:dtx_aggregation_main
dss_module_setup_all //安装各模块:调用各模块的setup接口安装模块
crt_register_event_cb //注册网络事件回调dss_crt_event_cb,如:groupupdate或者swim状态更新
crt_register_hlc_error_cb //注册HLC错误回调dss_crt_hlc_error_cb
dss_xstreams_open_barrier //激活utl barrier信号量,启动所有的xs
// xs的主线程ULT
dss_srv_handler
dss_xstream_set_affinity //绑核
dss_tls_init //初始化tls,与该xs相关的信息会存储在tls中
crt_context_create //(如果需要)初始化Cart网络上下文,sys_xs,swim_xs,main_xs以及每个main_xs的第一个helper_xs会起用网络
crt_context_init
crt_hg_ctx_init //根据配置的网络provider初始化HG(mercury),注册HG rpc handle
d_tm_add_metric //添加metric指标
crt_swim_init //如果是swim_xs,初始化swim上下文,注册swim回调crt_swim_progress_cb,注册rpc handler到opc_map
crt_context_register_rpc_task //注册rpc句柄dss_rpc_hdlr和dss_iv_resp_hdlr:io请求首先由上述注册的HG rpc handle响应,然后转发给这里注册的rpc handle做进一步的调度处理,最终调用op code的处理函数
tse_sched_init //初始化任务调度器
bio_xsctxt_alloc //(如果需要)通常只有main xs才会使用nvme,但是如果开启了metadata-on-ssd,sys_xs也会;第一个xs(main_xs或者sys_xs)会根据配置文件初始化所有的spdk subsystem,创建blobstore,将初始化的bio_bdev设备挂接到nvme_glb全局列表中。给target选择合适的nvme设备(targets循环绑定到已有target数最少的nvme上,dev和targets信息持久化到db中),并创建io_channel
daos_abt_thread_create //创建ULT添加到nvme池,等待argobots调度执行:负责nvme设备的状态监测,设备的热插拔监测等
ABT_cond_signal //激活信号量,以便调用线程能继续运行
ABT_cond_wait //如果是swim_xs则立即开始后面的循环,如果是非swim xs,等收到daos_server的dRPC消息setUp后再执行
for (;;) //主ULT循环,如果该xs开启了Cart网络,会调用HG(mercury)接口推进rpc的调用及回调处理(swim xs会执行上述注册的crt_swim_progress_cb进行engine状态探测)
wait_all_exited //等待所有ULT退出,engine退出