Hypertable执行select命令时,操作主要集中在Rangeserver上,Client也会参与部分操作,但是Master不会涉及任何的读写数据的操作。
Hypertable的Client相当的“肥胖”,它缓存了一份range和RangeServer的映射关系列表,这个映射关系由LocationCache类维护。类中使用一个链表保存所有缓存的range,并使用一个map对缓存的range进行排序,map的key由table_name和end_row组成。
当执行select语句时,如果where子句包含多个row_interval,则每个row_interval会被拆分为一个查询,多个查询异步执行。查询的拆分由TableScannerAsync类完成,每个异步查询由IntervalScannerAsync类完成。该类根据row_interval可以确定此次查询的start_row,然后再结合table_name即可确定range。当然,确实range时需要查找缓存中的映射关系表,如果找不到对应的range,则从服务器端更新最新的映射关系到缓存。最后,该类将针对一台RangeServer发起查询特定range的请求。
RangeServer接收请求后,将针对Range的查询分解为针对range所包含的每个access group的查询请求,access group再将其分解为针对每个cellstore文件和query cache的查询请求。查询结果也将逐层返回,最终RangeServer交由FillScanBlock函数将结果汇总为一个数据块后返回给Client。
当select语句中带有“offset n limit m”选项时,将会对每个异步查询生效。每个异步查询在RangeServer端由最终将由MergeScannerRange类执行offset和limit操作。该类汇总一个range中满足查询条件的cell,当然这些cell可能来自多个access group。检测每个cell的row,如果row不同则计数,如果计数小于n,则抛弃该cell。当计数大于等于n时,表示offset操作已经结束了。limit操作也是同理,当计数小于m时,输出该cell,否则,表明limit操作结束,停止cell输出。其实,Client在ScanCell类的load函数中也对limit进行了限制。
Client对多个异步查询的返回结果并不进行排序,而是根据返回的次序形成一个结果队列。队列中的每个元素是一个ScanCell指针,其维护了RangeServer端一次返回的数据,当然这是经过处理后的K/V对。一个异步查询的结果由于Scanner.BufferSize的限制,可能会多次返回,每次是队列中不同的元素。Client对异步查询返回结果的处理见IntervalScannerAsync类的handle_result函数。
目前的offset和limit都是以row为单位进行统计,出于项目的需要,我们修改源码实现了row+timestamp为单位进行统计,以应付多版本的情况。
从select执行的基本思路可以发现目前存在的几处“硬伤”:
1. offset的cell已经从磁盘取到了内存,这将白白消耗硬盘的I/O。较好的做法是在range上跳过,即读取一个range时,就能知道是否要跳过该range。当然,这需要从range的元数据中就能发现row的个数(最好还有timestamp的个数),目前社区没有提供这样的元数据,看来要实现这样的方法是颇费周折呀。
2. 对where子句中多个row_interval的返回结果没有进行排序和合并。每个row_interval的异步查询“各自为政”的执行查询并返回结果,结果中有可能存在相同的cell,由于社区目前没有进行合并处理,则会显示多次。由于社区目前也没有进行排序处理,故offset和limit只能在每个查询结果上分别处理。虽然这个问题严重影响用户体验,但目前为止,好像看不到社区的改进计划。客观的说,这个问题的改进是任重道远的,毕竟对异步查询结果的排序将极大的影响查询响应速度,这应该是用户体验中首要的因素了吧......