1.Scroll源码解析
2.ArrayList详解及扩容源码分析
3.CreateCollection API执行流程_milvus源码解析
4.lodash源码解析:find家族
5.API请求执行流程_milvus源码解析
6.Alluxio 客户端源码分析
Scroll源码解析
1. Scroll查询在指定_doc排序时相较于不指定排序或指定某个字段排序能明显更快,源码这是分析由于Scroll查询的机制及底层实现所致。
首先查看Elasticsearch的源码Collector,其主要功能是分析收集文档并按照特定规则排序。其中,源码TopDocsCollector类在收集文档后会返回一个有序的分析录音 源码TopDocs对象,该对象是源码搜索结果的返回值。TopDocsCollector有三个子类:SimpleFieldCollector、分析PagingFieldCollector、源码SimpleTopScoreDocCollector 和PagingTopScoreDocCollector。分析这些子类根据排序规则(如字段排序、源码简单排序等)进行文档排序。分析
2. 对于TopScoreDocCollector,源码其排序规则是分析先执行打分,分数相同的源码文档按文档号排序。TopFieldCollector则是先按照指定字段排序,值相同的文档再按文档号排序。
3. TopScoreDocsCollector的两个子类(SimpleTopScoreDocCollector和PagingTopScoreDocCollector)在功能上区别在于PagingTopScoreDocCollector针对翻页请求,代码上增加了对after的判断。对于使用TopScoreDocsCollector无论是否为翻页请求,每次请求都会扫描全部命中文档并计算分值。使用SimpleTopScoreDocCollector还是PagingTopScoreDocCollector取决于after是否为null。
4. 对于scroll请求,after参数等于scrollContext.lastEmittedDoc,即上次翻页最大的ScoreDoc。TopFieldCollector同样有两个子类(SimpleFieldCollector和PagingFieldCollector),其判断逻辑与TopScoreDocsCollector类似,也是根据searchContext.sort()是否为null来决定使用哪类Collector。
5. 在lucene6.4.1版本中,无论是SimpleFieldCollector和PagingFieldCollector都无法提前终止收集过程。然而,从更高版本的lucene开始,具备了提前结束收集的功能,判断依据是search sort=index sort一致时,通过抛出CollectionTerminatedException异常提前结束收集。Elasticsearch从6.x版本开始也支持了自定义写入顺序,可以不是_doc而是某个字段值。
6. 通过Elasticsearch的代码分析,我们确认scroll请求在指定_doc排序并从第二页开始时,只会收集指定数量的doc,性能表现更优。对于scroll请求,包装了一层MinDocQuery,hdr源码用于过滤掉已经翻页过的数据,大大减少文档命中数,避免收集无用的doc,这对于深度翻页性能提升明显。
7. 对于scroll请求,由于不支持向前翻页,每次查询对于已查过的数据无需收集。Elasticsearch通过MinDocQuery实现跳跃功能,将doc跳到segmentMinDoc(lastEmittedDoc+1),在合并倒排表之后,实际上就不会再命中上一页的内容。触发提前终止后,后续倒排表合并也不再必要,性能提升显著。
8. Scroll与search_after查询实际上走的是相同的逻辑,都是通过一个after变量进行翻页。scroll的after参数为scrollContext.lastEmittedDoc(ScoreDoc),search_after的after参数为包含sort字段信息的FieldDoc,都是ScoreDoc。最终都会收集全部命中文档才能得到排序结果,但scroll对于_doc排序做了优化,性能表现更佳。
9. 对于search_after查询,即使指定_doc排序,仍然需要收集全部命中文档,因为search_after是动态的,MinDocQuery跳跃功能不适用。然而,search_after在lucene后续版本中支持了提前终止功能,当查询时指定sort为index sort,可以触发提前终止,不再收集全部命中文档。
. Scroll请求保存的上下文信息主要是maxScore和lastEmittedDoc用于翻页,但实际保存的不仅仅是ScrollContext,而是SearchContext,其中包含了更多关键信息,如searcher和IndexReader,后者对于后续索引更新是感知不到的,除非重新打开reader或使用DirectoryReader.openIfChanged(oldreader)。这是Scroll查询无法感知索引更新的原因。
. 经过测试,即使在scroll过程中触发了merge,ivx源码被merge的segment文件也不会立即被删除,新的segment文件也不会被发现。这表明Scroll查询无法感知数据更新,其本质是快照了LeafReaderContext,并非检索命中的结果。
总结而言,Scroll查询在指定_doc排序时,通过优化收集过程和使用MinDocQuery实现跳跃功能,能显著提升性能,尤其是在翻页操作中。同时,Scroll请求的机制及底层实现使得其在查询处理上与search_after查询存在显著差异,但在Elasticsearch6.x版本中引入了索引预排序和提前终止功能,进一步优化了查询性能。
ArrayList详解及扩容源码分析
在集合框架中,ArrayList作为普通类实现List接口,如下图所示。 它实现了RandomAccess接口,表明支持随机访问;Cloneable接口,表明可以实现克隆;Serializable接口,表明支持序列化。 与其他类不同,如Vector,ArrayList在单线程环境下的线程安全性较差,但适用于多线程环境下的Vector或CopyOnWriteArrayList。 ArrayList底层基于连续的空间实现,为动态可扩展的顺序表。一、构造方法解析
使用ArrayList(Collection c)构造方法时,传入类型必须为E或其子类。二、扩容分析
不带参数的构造方法初始容量为,此时底层数组为空,即`DEFAULT_CAPACITY_EMPTY_ELEMENTDATA`长度为0。 元素添加时,默认插入数组末尾,调用`ensureCapacityInternal(size + 1)`增加容量。 若当前容量无法满足增加需求,计算新的容量以达到所需规模,确保添加元素成功并避免频繁扩容。三、常用方法
通过List.subList(int fromIndex,源码薪资 int toIndex)方法获取子列表,修改原列表元素亦会改变此子列表。四、遍历方式
ArrayList提供for循环、foreach循环、迭代器三种遍历方法。五、缺陷与替代方案
ArrayList基于数组实现,插入或删除元素导致频繁元素移动,时间复杂度高。在需要任意位置频繁操作的场景下,性能不佳。 因此,在Java集合中引入了更适合频繁插入和删除操作的LinkedList类。 版权声明:本文内容基于阿里云实名注册用户的贡献,遵循相关协议规定,包括用户服务协议和知识产权保护指引。发现抄袭内容,可通过侵权投诉表单举报,确保社区内容健康、合规。CreateCollection API执行流程_milvus源码解析
在分析milvus源码中的CreateCollection API执行流程时,我们需要详细拆解从客户端请求到数据最终存储在etcd的过程。在milvus版本v2.3.2中,CreateCollection API的执行流程大致分为以下几个关键步骤:
首先,客户端SDK接收用户创建集合(collection)的请求,并将此请求封装为createCollectionTask,随后将其放入ddQueue队列。
随后,此任务在proxy内依次执行PreExecute、Execute和PostExecute三个方法。PreExecute阶段进行参数校验等预处理工作,Execute阶段则是真正执行逻辑,而PostExecute阶段完成执行后的清理工作,通常不做任何操作并返回nil。
在Execute方法中,proxy调用rootCoord的CreateCollection接口,此接口进一步封装请求为rootcoord里的createCollectionTask。
接下来,rootCoord的CreateCollection接口执行CreateCollectionTask的Prepare、Execute和NotifyDone方法。核心操作在Execute阶段,其中涉及到多个步骤,ntl源码包括expireCacheStep、addCollectionMetaStep、watchChannelsStep、changeCollectionStateStep等。在这些步骤中,重点是addCollectionMetaStep,负责etcd元数据的操作。
在addCollectionMetaStep的Execute方法中,s.core.meta.AddCollection方法被调用。此方法在etcd中创建了多个与集合相关的key-value对,这些key值按照特定规则构建,反映了集合、分区和字段之间的关系。
具体而言,集合信息通过key `root-coord/database/collection-info/1/` 存储在etcd中,value为protobuf序列化的etcdpb.CollectionInfo,这表示集合由ID、DbId、schema等组成,schema中不记录字段、分区ID或名称等信息。etcd以二进制形式存储这些数据。
分区信息通过类似 `root-coord/partitions//` 的路径存储,value为etcdpb.PartitionInfo,同样采用protobuf序列化后存储在etcd中。此信息包括partitionID、partitionName、collectionId等。
字段信息通过 `root-coord/fields//` 的路径存储,value为schemapb.FieldSchema,包含字段ID、名称、描述、数据类型等信息。
在执行完毕后,将所有key-value对批量写入etcd,最终完成集合的创建。
总结而言,CreateCollection API的执行流程涉及多个组件协作,从客户端请求开始,经过proxy和rootCoord的处理,最终在etcd中存储集合、分区和字段的元数据,实现了集合的创建。整个流程中,etcd作为关键的数据存储层,提供了持久化和高可用性保障。
lodash源码解析:find家族
本文将探讨lodash库中的find家族方法,包括findLastIndex、findLast、findKey和findLastKey,以及它们背后涉及到的一些辅助函数,如keys、isTypedArray、isBuffer等。官方文档中,find家族通常适用于数组(array)的findIndex和findLastIndex,集合(collection)的find和findLast,以及对象(object)的findKey和findLastKey操作。
在lodash的4..版本中,尽管在固定打包分支中能找到完整的find方法,但在master分支上却未发现findIndex.js和find.js文件,这让人疑惑。我曾在github上提问,得到的答复是master分支用于存放V5版本的开发中代码,实际打包则是通过lodash-cli工具生成的完整版本。
理解collection、array和object的区别需要参考ECMA规范。ECMAScript以对象为中心,非原始类型的值被视为collection,对象是拥有任意属性的集合,而数组是对象的一种特殊形式,具有连续的数字索引。underscore库处理collection的方式是通过duck-typing判断,避免使用具有数字长度属性的非数组对象,同时,它推荐使用find代替each来中断循环。
在lodash的代码中,频繁使用Object(collection)来强制转换为对象,这是因为Object构造函数可以包裹任何值生成新对象。在浏览器和node.js环境中,全局对象有细微差异,但最新的规范中,globalThis、self和this在大部分情况下指向window或global。
类对象、类数组和类数组对象有特定的定义。类对象包括函数、数组和对象,类数组需要满足有数字类型的length属性,而类数组对象则在此基础上要求value也是类对象。find家族方法引用了诸如isObject、isArrayLike等lodash内部方法,如baseFindKey用于迭代,root、isIndexfreeGlobal、nodeTypes和getTag用于获取对象标签,arrayLikeKeys和baseForOwnRight则支持对不同类型的对象进行操作。
API请求执行流程_milvus源码解析
milvus客户端发起API RPC请求,内容为request。
proxy接收请求,将request转化为task。
task被推入队列等待执行。
调度器执行队列中的task。
创建collection的API(CreateCollection)流程:
客户端发起创建collection请求。
proxy接收request,包装为createCollectionTask并入队。
createCollectionTask等待调度器执行。
执行路径:internal\proxy\impl.go
调度器执行:
依次执行PreExecute()、Execute()、PostExecute()。
PreExecute()进行预处理。
Execute()真正执行task任务。
PostExecute()处理完成后的动作,通常返回nil。
执行路径:internal\proxy\task.go
PreExecute()、Execute()、PostExecute()的顺序源自task调度器源码。
执行路径:internal\proxy\task_scheduler.go
核心代码执行于task入队后的processTask()方法。
思考processTask()调用源:
task_scheduler.go的Start()方法启动goroutine进行调度。
createCollectionTask通过go sched.definitionLoop()路径执行。
processTask()方法在此循环中调用。
理解了这些,PreExecute()、Execute()、PostExecute()的执行流程清晰。
Alluxio 客户端源码分析
Alluxio是一个用于云分析和人工智能的开源数据编排技术,作为分布式文件系统,采用与HDFS相似的主从架构。系统中包含一个或多个Master节点存储集群元数据信息,以及Worker节点管理缓存的数据块。本文将深入分析Alluxio客户端的实现。
创建客户端逻辑在类alluxio.client.file.FileSystem中,简单示例代码如下。
客户端初始化包括调用FileSystem.Context.create创建客户端对象的上下文,在此过程中需要初始化客户端以创建与Master和Worker连接的连接池。若启用了配置alluxio.user.metrics.collection.enabled,将启动后台守护线程定时与Master节点进行心跳传输监控指标信息。同时,客户端初始化时还会创建负责重新初始化的后台线程,定期从Master拉取配置文件的哈希值,若Master节点配置发生变化,则重新初始化客户端,期间阻塞所有请求直到重新初始化完成。
创建具有缓存功能的客户端在客户端初始化后,调用FileSystem.Factory.create进行客户端创建。客户端实现分为BaseFileSystem、MetadataCachingBaseFileSystem和LocalCacheFileSystem三种,其中MetadataCachingBaseFileSystem和LocalCacheFileSystem对BaseFileSystem进行封装,提供元数据和数据缓存功能。BaseFileSystem的调用主要分为三大类:纯元数据操作、读取文件操作和写入文件操作。针对元数据操作,直接调用对应GRPC接口(例如listStatus)。接下来,将介绍客户端如何与Master节点进行通信以及读取和写入的流程。
客户端需要先通过MasterInquireClient接口获取主节点地址,当前有三种实现:PollingMasterInquireClient、SingleMasterInquireClient和ZkMasterInquireClient。其中,PollingMasterInquireClient是针对嵌入式日志模式下选择主节点的实现类,SingleMasterInquireClient用于选择单节点Master节点,ZkMasterInquireClient用于Zookeeper模式下的主节点选择。因为Alluxio中只有主节点启动GRPC服务,其他节点连接客户端会断开,PollingMasterInquireClient会依次轮询所有主节点,直到找到可以连接的节点。之后,客户端记录该主节点,如果无法连接主节点,则重新调用PollingMasterInquireClient过程以连接新的主节点。
数据读取流程始于BaseFileSystem.openFile函数,首先通过getStatus向Master节点获取文件元数据,然后检查文件是否为目录或未写入完成等条件,若出现异常则抛出异常。寻找合适的Worker节点根据getStatus获取的文件信息中包含所有块的信息,通过偏移量计算当前所需读取的块编号,并寻找最接近客户端并持有该块的Worker节点,从该节点读取数据。判断最接近客户端的Worker逻辑位于BlockLocationUtils.nearest,考虑使用domain socket进行短路读取时的Worker节点地址一致性。根据配置项alluxio.worker.data.server.domain.socket.address,判断每个Worker使用的domain socket路径是否一致。如果没有使用域名socket信息寻找到最近的Worker节点,则根据配置项alluxio.user.ufs.block.read.location.policy选择一个Worker节点进行读取。若客户端和数据块在同一节点上,则通过短路读取直接从本地文件系统读取数据,否则通过与Worker节点建立GRPC通信读取文件。
如果无法通过短路读取数据,客户端会回退到使用GRPC连接与选中的Worker节点通信。首先判断是否可以通过domain socket连接Worker节点,优先选择使用domain socket方式。创建基于GRPC的块输入流代码位于BlockInStream.createGrpcBlockInStream。通过GRPC进行连接时,每次读取一个chunk大小并缓存chunk,减少RPC调用次数提高性能,chunk大小由配置alluxio.user.network.reader.chunk.size.bytes决定。
读取数据块完成后或出现异常终止,Worker节点会自动释放针对该块的写入锁。读取异常处理策略是记录失败的Worker节点,尝试从其他Worker节点读取,直到达到重试次数上限或没有可用的Worker节点。
若无法通过本地Worker节点读取数据,则客户端尝试发起异步缓存请求。若启用了配置alluxio.user.file.passive.cache.enabled且存在本地Worker节点,则向本地Worker节点发起异步缓存请求,否则向负责读取该块数据的Worker节点发起请求。
数据写入流程首先向Master节点发送CreateFile请求,Master验证请求合法性并返回新文件的基本信息。根据不同的写入类型,进行不同操作。如果是THROUGH或CACHE_THROUGH等需要直接写入底层文件系统的写入类型,则选择一个Worker节点处理写入到UFS的数据。对于MUST_CACHE、CACHE_THROUGH、ASYNC_THROUGH等需要缓存数据到Worker节点上的写入类型,则打开另一个流负责将每个写入的块缓存到不同的Worker上。写入worker缓存块流程类似于读取流程,若写入的Worker与客户端在同一个主机上,则使用短路写直接将块数据写入Worker本地,无需通过网络发送到Worker上。数据完成写入后,客户端向Master节点发送completeFile请求,表示文件已写入完成。
写入失败时,取消当前流以及所有使用过的输出流,删除所有缓存的块和底层存储中的数据,与读取流程不同,写入失败后不进行重试。
零拷贝实现用于优化写入和读取流程中WriteRequest和ReadResponse消息体积大的问题,通过配置alluxio.user.streaming.zerocopy.enabled开启零拷贝特性。Alluxio通过实现了GRPC的MethodDescriptor.Marshaller和Drainable接口来实现GRPC零拷贝特性。MethodDescriptor.Marshaller负责对消息序列化和反序列化的抽象,用于自定义消息序列化和反序列化行为。Drainable扩展java.io.InputStream,提供将所有内容转移到OutputStream的方法,避免数据拷贝,优化内容直接写入OutputStream的过程。
总结,阅读客户端代码有助于了解Alluxio体系结构,明白读取和写入数据时的数据流向。深入理解Alluxio客户端实现对于后续阅读其他Alluxio代码非常有帮助。