全部博文(356)
分类: 服务器与存储
2018-08-08 10:50:50
以下分别分析大文件和小文件在读操作和写操作正常完成时CacheVC的函数调用流程图,这里主要讲述配置ts为single模式下的cache写方式,对于cluster模式,则有所不同。
小文件写
ts源代码中各个模块的设计都是Processor+Continuation+EventSystem机制。cache层提供给其他模块的外部接口为CacheProcessor,而内部接口,则为类CacheVC。Cache::open_write通过Cache::key_to_vol方法确定使用哪一个Vol存储要写入的object,同时生成一个负责写操作流程的CacheVC对象,并通过回调函数callcont提示上层Continuation写操作已经准备好了,这时上层Continuation调用CacheVC的openWriteMain函数,由此写操作开始。
这里要解释一下Vol这个数据结构。在第一章说过,用户通过storage.config文件配置存储空间,ts对每一个存储空间根据disk layout进行格式化操作,并最终以一个Vol表示这个空间。举例来说,用户在storage.config中配置了三个裸设备,同时没有配置volume.config文件的情况下,则一个裸设备对应一个Vol。总之,一个Vol对应一个实际划分好的存储空间。Vol维护一个写队列,它是一个CacheVC的list,当Vol调用aggWrite时,则从该list中取出CacheVC并通过函数agg_copy将要写的内容拷入一个buffer中,最后将该buffer一次性写入cache。这个buffer称为agg buffer。
openWriteMain读取上层Continuation要写入cache的object的body内容。对于小文件,openWriteMain会通过回调函数calluser提示上层Continuation直到整个内容读取完毕为止。这时,上层Continuation对CacheVC调用do_io_close操作,进入die函数,并进一步进入openWriteClose函数体。openWriteClose函数通过CacheVC提供的很多位状态信息,判断这是一个小文件,从而进一步调用openWriteCloseHead函数。openWriteCloseHead函数调用updateVector函数确定是否需要更新head内容,并最终调用do_write_call进入handleWrite,执行最后的写cache操作。handleWrite将CacheVC自己加入到Vol的写cache队列中,并判断Vol当前是否正在进行写cache,如果没有,则立即命令Vol进行cache写,并进入openWriteCloseHeadDone,通过宏dir_insert,使用first_key找到索引区中对应的未使用的索引,并将相关元信息如object在磁盘上的offset等保存到该索引中。最后,CacheVC进入openWriteCloseDir函数,对于正常流程来说,这时候执行free_CacheVC释放CacheVC的内存空间,整个写操作完成。而在CacheVC命令Vol进行cache写后,Vol进入aggWrite流程,它通过将写cache任务分派给一个aio线程(aio模块),执行异步写。
大文件写
对于大文件的读写,程序逻辑相对要复杂一些。结合小文件写以及第一章最后对大文件的存储策略的描述,大文件写有以下几点不同:
(1) 大文件是先写body的fragment,写完所有fragment后,再写head。head使用object生成的first_key作为它的key值找到对应的索引保存相关元信息,而对于fragment,第一个fragment使用随机数算法生成一个数作为它的key值,从第二个开始,每个fragment都是以前一个fragment的key值作为种子,通过随机数算法生成一个key值。
(2) 如果一个body被分成了几个fragment,则写第一个至倒数第二个fragment结束后程序逻辑走到openWriteWriteDone,而写最后一个fragment结束后程序逻辑走到openWriteCloseDataDone,代表写body完成。
小文件读
为了方便说明,大文件和小文件的读逻辑图中我都刻意回避了read while writer属性对应的操作,这个在最后解释。
小文件读的逻辑相对简单,类Cache提供的方法open_read首先通过Cache::key_to_vol方法定位到从哪个Vol中查找object。要查找一个object在不在cache中,只需要查询是否有索引保存object的元信息即可。open_read函数调用全局dir_probe函数查找object对应的索引,如果查询到,说明object保存在cache中,并创建一个CacheVC对象,同时调用CacheVC::do_read_call,进入CacheVC的读操作。
do_read_call函数调用handleRead,该函数主要判断读取object内容的位置:
(1) 如果object在ram cache中,则走ram cache查找。
(2) 如果object在mem cache中,则走mem cache查找。从源码中可以很清楚看到,ts的mem cache命中是很naive的,如果此次读请求与上次读请求的object是相同的,则mem cache命中。
(3) 如果object在Vol的agg buffer中,则从agg buffer中直接命中。
(4) 如果(1)(2)(3)条件皆不成立,这时候就走磁盘查找了,这时Vol分配cache读任务给一个aio线程,进入异步读操作。
在handleRead完成后,这时候进入handleReadDone,这个函数的主要任务是如果object不是内存命中时,这时候需要将object加载到内存中去。该函数结束后就进入到openReadStartHead函数,该函数将读取出来的object的head部分读取到数据结构CacheHttpInfoVector中。对于小文件,openReadStartHead通过回调函数callcont告诉上层Continuation读操作已经准备就绪,上层Continuation指示CacheVC调用openReadMain,将读取出来的数据写到指定buffer中以便它来读取,openReadMain在做完这一切后,通过回调函数calluser告诉上层Continuation读操作完成,此时Continuation调用CacheVC的do_io_close函数告诉CacheVC可以进行close read操作了,CacheVC这时候执行die函数,进一步调用openReadClose,执行free_CacheVC释放对象。至此,一个读操作完成。
大文件读
大文件的读逻辑与大文件的写逻辑刚好相反,首先读object的head,然后读每一个fragment,直到所有fragment读完为止。由于在写入object时候,head对应的Doc中的frags包含的是每一个fragment在object中的offset,而frags数组的元素个数就是fragment的个数,通过先读head再读fragment,就可以将一个大文件内容全部读取出来。openReadStartHead读取head,而openReadStartEarliest读取第一个fragment,余下的fragment由于逻辑相同,每次读取完成后,进入openReadReadDone,获取下一个fragment的key值以后,进入下一次读取,直到所有fragment读取完成。
到此,大文件与小文件的读操作介绍完了。我们回过头来大体介绍下read while write机制。安装好ts后,默认ts是不启用read while write的,需要修改records.config文件中变量proxy.config.cache.enable_read_while_writer的值以enable该选项。read while write这个机制的意思是,当有写一个object至cache时,会创建一个CacheVC负责写操作的完成,而该CacheVC保存有object的内容。这时,如果开启read while write机制,有读取相同object请求到来时,此时object还没有写到cache中去,cache查找会失败,但我们可以直接从这个负责写操作的CacheVC中读取object。这样不仅读取速度快,同时也避免了回源,减少了到源服务器的请求链接数。当然,这里在代码实现时候需要通过锁机制保持读取与写入操作同步。