OK 那我们就开始吧今天下午的第一场好像也是整个Database Session的唯一的一个Speaker对 我先做一下自我介绍对 我是云山网络的向阳然后我们主要做的工作是云元身应用的可观色性然后今天的一个分享的议题是我们怎么给Promise Use做一个高性能的Remote Storage这里面的一个特点是它支持Unlimited Time Series无限的一个时序 对大概是这样一个议题可能需要花一小点时间一夜PPT来去和大家分享一下Promise Use它的一个背景 背景它是Kubernetes生态里面的用于指标监控的这样一个重要的一个组件然后一条监控数据这个PPT里面展示的一条监控数据我们看到的是它有Metrix Name然后Nable以及Metric Value和Time Stamp组成对然后我们今天其实主要要探讨的一个话题就是说当前面的这一部分 红色的这个部分它的Cardinality它的技术很高的时候我们有没有一些什么样的好的解决办法通过一些开源 已有的一些开源的生态比如说一些劣势的OLAP的数据库能去解决我们所面临的高级数的场景下数据的持久 长期存出的一个问题对这是我们今天讨论的一个议题我们在我们的产品的客户处也经常会碰到一些问题实际上大家对于实际数据库的这个点也谈论了很多了就是我们所谓的这个高级数问题High Cardinality Problem对可能从数学上来讲Cardinality可能会在极端的情况下会约等于Metrics Name的技术乘以后面的每一个Label的它的可能的值得一个情况我们下面也接了一个图这是一个2000万Series这个不算大那可能有2亿 甚至20亿的这些场景2000万Series的一个我们的一个生产环境它里面的一些Label那其实用户在去使用的时候可能会去注入中间的C内应列的是说相应的Label它的Value的数量可以看到像ID有17万Container ID 16万Name 14万这都是一些非常高级数的一些Label对其实这个就会导致像Promise它本身的一个内存的一些用量的问题以及它的查询的速度的问题比如我们最简单的像CPU的一个查询对吧我们刚才上一页是一个CPU total那我们由于有一些像CPU核的那个编号以及State比如System等等这样一些存在其实我们只要查一条曲线可能都是Hate到很多很多时序这个时候也会导致查询会在Hate到大量时序的时候一次查询速度会慢然后我们为什么会去考虑这样一个问题呢就是Unlimited Time Series这其实在小规模的程序下一般不会碰到可能在去用插庄的办法暴露Promise的数据的时候指标数据的时候也可能不大是那么经常的碰到这个时候一般的可能管理员或者是运为团队会要求开发再去使用暴露指标的时候会去尽量避免暴露一些高技术的一些标签对那我们用到的我们思考这个问题的点的是我们有一个Deplowed这样一个Open Source的一个项目那么它其实在里面会去通过EBPF就左侧的这个EBPF来去采集很多任何服务任何Request等等这样一些指标数据那这里面的指标数据就是我们屏幕里面第一行看到的其实它会有一些很高维的一些字段像Client IPClient Pod原端口号是一个随机的比如Endpoint URLPod Name刚才我们看到Pod Name它有十几万那还是一个挺小的环境所以每次Pod的重建都会增加一个对Cardinality有些增加我们的结果就是说基于最右侧的Clickhouse能去解决这样一个问题那今天是想分享一下我们再去用到这些比如说成熟的一些用Clickhouse在此之上去怎么样构建一个计算层来去解决高级数的实驱数据库实驱数据的存储查询问题那下面的这一线的片子可能我首先会讲一下我们是怎么思考这个问题的以及它是一步一步怎么迭代到现有的我们产品里面的解决方案的最简单的办法因为其实对于指标来讲它的Label是不固定的最简单最直观的办法就是把所有的Label或者叫Tag把它存到两个数组里面两个ArrayString Array标蓝色的这两面然后我们再去查询的时候只需要找到Name比如说我们要查Host这个Label相应值我们要找到它的下标就能够去对应这个下标所在的TagValues这个位置的就是Deglabel的Value我们就可以去做一些搜索这个是非常非常原始的一个做法这是因为劣势数据库它是一个schema for这样一个数据库你需要先定义数据库的schema你就要不可避免地去把这种动态劣字段要有一个解决办法当然这个解决办法很快就会遇到问题比如说我们在很多环境下就会遇到这是一个metric它的打的Tag或者打Nable吧这些Nable的数量下面有一个数79个这其实还不算最大的我们有些尝一下打到比如说打到180个或者是更大的Nable的数量都会有所以可以想象一下在用这种方法来去做存储和搜索的时候会有一个非常慢的搜索体验比如说你要匹配的Tag的name在比较靠后的位置所以它只是一种可行的方案并不是说只是一种可以去存下去的方案并不是说能带来能可接受的一个查询体验所以我们的一个想法就是说劣势数据库一般都会有比较强大的大宽表的存储能力像ClickHouse在16年的就能达到说我存1000个列应该是没问题了我们在大量的环境下都碰到可能所谓的指标它所涉及到的所谓的Nable的name可能是数百辆级的低于1000的可能ClickHouse现在能支持的列出更多所以一个显然的解决刚才这个问题的方案我们可以把所有的Nable给评铺开App ENV什么什么一列一列的放下来你可以评铺很多列因为整个环境下我们看到一些比较大规模的环境下也就五六百七八百这些列就OK就能做进去了但是其实它带来一个问题带来这个问题是说对 带来这个问题是说我们会得到一个非常稀疏的这样一张表这张表才去写入特别是批量写入的时候都会很不方便比如说你要写入一万条数据这一万条数据它可能是最简单的办法就是把一万条数据的所有的列都写下去这个时候你会写很多无效的一些列写的那些列因为它不具有比如说某一个Matrix它不就App这个Nable那你可能白写了也有可能是要把Matrix做一些汇总再写下去因为同一个Matrix它的表现还都是一样的所以我们再去碰到这个问题的时候又会去想一想这个可以怎么办我们又往前叠态一下我们的解决方案那这个其实就是我们可以把这些columns比如说以ClickHouse这样的劣势数据库为例我们把这些columns的tag的columnsNable的columns做一个对齐因为我们会发现一个特定的Matrix它其实相关的Nable都是比较固定的虽然我们刚才看到的比如说在上面这个解决方案里面我们可能要存个1000列或者是几千列但是我们稍微想一想每一个Matrix它所相关的Nable我们在我们的自己的客户处能碰到的极限也就是200多一点点那这个时候其实是不是开闭200多个列然后就OK了但是这里面肯定要做一些额外的工作比如说要把每个Matrix它的columns的layout去记录下来但这也是很方便去做的而且你不用去关心columns的Nable会有一些比较高的动态性这个动态性一般比较低而且总的来说比如说增加列可以往后去增加使用删除列就不要再使用这一个相应的columns就行了所以我们假设我们认为每一个Matrix都不会有超过256个Nable的话我们完全可以用256个预定义的列来让它去存储所有的指标的所有的Nable再去查询的时候因为我们每次查的都是一个Matrix那每一个Matrix我们再去查它的时候我们就可以去从它的Nayoutcolumns的Nayout就能知道它的第一列代表的是App第二列代表的是env第三列可能是一个cluster都可以去解释出来这其实比较好的去比如说这里面有我们假设NableLame38它是host它代表的是host对于一个特定的一个指标它代表的是host我们就可以直接把这一列 as 一下用于它的一个过滤或者分组都是可以的其实这已经慢慢地靠近我们比较理想的一个解决方案了但是看起来这个256还是挺大的我们要维护一个相对来讲还是会比较庞大的一个宽的一个表我们可能在有些写入的情况下我们带来的写入量还是比较大的实际数据库它会用一些它会说把每一个metric对应的这些列构建一个serious ID所以它写每一行数据的时候会去根据这些Nable的这个值来得到一个serious ID的时候只需要写一个ID就行了所以相比较下来写入的这个数据量还是相比于实际数据库的机制会有一个比较大的差异相当于实际数只要写一列叫serious ID我们要写256列这个事还可以怎么往前进一步吗那我们就再思考一些问题比如说实际上我们分析一下Premises里面的Labels我们就会发现它会有两种类型的Labels比如说一般在大码里面通过插庄或者是直接去业务大码里面填的Label比如说我们的App owner那个Endpoint owner都可以在下面比如Endpoint,UserID,ArrowCode这些人可能是它是业务级别的一些Label也可能是我们可以理解成每一个砲的里面这个Label的曲直可能是很多样化的我们可以认为它是一个App的Label还有一些可以认为是Infra级别的Label就是说每一个砲的里面或者至少每一个进程每一个container它的相应的Label从它创建的那一刻起进程启动container启动从那一刻起就已经确定了我们把它叫做Infra的Label实际上我们也在观察一些现象比如说POD就是Kubernetes里面POD再去高频的去重建的时候往往会导致我们的实际数据库里面的技术失控这是因为比如说上面的AppLabel它可能它有一个G比如说它是一个100万的G下面的Infra的Label一般它的GiKartinality应该是正比于或者线性关系于POD的数量或者叫做一段时间比如说你的数据留存时间是14天应该是14天以内所有的POD的Lame的数量虽然这两个数都不大这两个两组标签的Kartinality的值都不大但是当我们把它放在一起去看的时候插沉起来拿到一个GiKartinality它可能会达到上千万它可能会或者是上亿的这样一个量级我们下面就有这样的一个例子其实也是从我们的一个生产环境下拿到的一些数据我们可以看到对刚才我们举的一个例子比如说就像CPUtoto这样的一个Magic它会有一些Target EnableTarget Enable我们可以从PromisesTargeten API里面我们会去get它它其实就能够去返回这些Target Enable实际上也是我们在Renabling Root等等这样一些环节我们可以去给它注入进来的这是由Promises Third Alliance去注入的一些Enable还有一些可以认为是在Connector或者叫Exporter这一侧自己生产的比如CPUERState System这些可能就是我们认为是App Label刚才举的例子我们有一个环境它有2000万的Cardinality整个的值实际上我们分析以后发现Target的数量才有一万多而Cardinality所有的App Label就蓝色这一部分可能才有不到100万这些是差距就是二十倍而且其实这个差距会随着你的整个指标数据流存时间的一个扩大会增长得非常快这因为POD的一个重建是非常高频发生的比如说我们有些场景下会要求指标数据要去流存几个月这种情况下这个差距可能会达到100倍这样的一个量级对我们就利用这样一个特点因为刚才其实我们已经做了一些工作把每一个Matric Label的Nayout去存储下来了我们就在想能不能存储更多呢实际上我们就做了这样一件事就把Target Label和App Label把它去分开了刚才我们可能要去每一个Matric它可能有256个Nabel要去存储下来但实际上可能由Explorer这一侧去暴露的或者去注入的Nabel可能是非常有限的这里面64个都是算比较多的64个已经足够我们来去使用了那其余的和POD相当于能推导出来的观念出来的Nabel我们可以都把它归接到Target ID里面对Target ID就是我们可以把所有的Target做一个编码这样的话其实我们就把原来的256个Stream的column去降低到了64个Stream Column加一个Target ID我们需要做的一些额外的工作包括我们可能要维护一下相应的每个Target ID它的对应的KVKV的信息一个Target ID通常是由一个Instance加一个job的这样一个KV pair来去确定的就像我们前面看到的比如说我们Instance加一个job这是Promises它的Target API返回的Instance加job其实就能够去定位到这是一个Target其他的这些Nabel其实都是和它来去有一个相当于一一的音设关系的所以我们可以通过维护这样一张额外的其实这个不大了就一万多个这换一下就一万多个Target的信息使得我们在去搜索的时候我们在去搜索TargetNabel的时候一定会可以去转换为一个Target ID或者一组Target ID然后另外的这些就是比较灵活等等它是应用程序内部来去生成的一些Nabel其实这个距离我们最终的一个现在的效果还是差了一些因为64还是偏大导致64产生的一个原因是因为我们其实在很多场景下没有特别的用好所谓的TargetNabel有好多Label我们都是可以通过PromiseServe端来去注入的但是我们去让它在Exporter的一侧来去生成了其实如果说我们非常灵活地去使用这样一个能力的话我们可以看到比如说DeepLoan就做了一件事我们从所有的Nabel里面去抽取出来一些MetalNabelMetalNabel可能就是我们所有InfraNabel相关的比如说POD 或者是一个Container决定了唯一已经确定了它所相关的所有的比如说Namespace然后Service, Cluster, ENV等等所有的信息都已经确定了而这些信息本来就已经在比如说在Annotation里面Nables在ENV里面这个都是我们在定义一个Service或者是Workload的时候就已经能够去掌握的这些信息假设我们认为这些信息都可以叫做TargetNabel而且这些信息都可以不用去通过Export来去主动注入到指标里面其实Export需要注入的标签应该是非常非常少的应该不会达到64个那么多我们在实际的一些客户处可能顶多就是十几个当然这个可以是一个动态的比如说你可以是有需要的时候你增加NabelName这种列但是总的来讲我们可以通过增加两个Matter的Nabel我们再去查选的时候就能把MatterNabel相关的所有的其他的属性都能查出来然后后面举个例子比如说这样一个例子这样一个例子我们是刚才这样的一个Layout比如说我们ChannelPod和Container当然你也可以去选择一些其他的MatterNabel比如说PID等等或者ProcessName等等我们再增加16个我们认为的是一个应用程序里面或者Exporter里面去注入的我们怎么去查像我们注入到KBusNabel里面或者Annotation里面ENV里面的这样一些属性呢实际上我们只需要在ClickHouse里面用一个Dictionary这样一个机制就能解决这个问题Dictionary它是维护了一个PodID我们可以把Pod做一个编码Pod的数量一般可能比如说是几十万几百万这样一个量级这个在Dictionary里面可能还是很容易的来去把它去承载下来的MainPod它所相关的Nabel我们都可以有Key和Value我们都可以把这张CustomNabel的这张表我们可以进入下来同样对于Annotation对于ENV我们也可以有类似的一个进度的办法这个DictionaryClickHouse里面的Dictionary它可以加载到内存里面去做一个非常快速的翻译了所以假设我们上面这里面有一个Pod的其实这里面也可以存成一个Uint64也可以存成一个ID假设我们在这里面我们已经有了一个PodID在Dictionary里面我们也已经有了一个PodID那我们现在如果说我们要去查询KBSNabel的App这个TagApp这个Label它的值是什么其实我们可以很简单的用ClickHouse的一个DictionaryGateDictionaryGate它有几个参数那第一个参数就是我用那张Dictionary表这是相应一个密码本的表来去做一个翻译这个表已经在内存里面已经在Memory里面了它是一张非常高效的翻译的机制然后我要去得到它的哪个字段得到它的返回DictionaryGate要返回哪个字段比如返回Value字段那我需要去查的是比如说我这个数据里面就是这个数据里面假设这里面是PodID有一些差异如果说数据里面是PodID我把Sympos里面的PodID和我的Nable的KeyApp如果是这样我们要查App就这就写App如果查EnvEnvironment这些Environment的Env我们把它As成一个新的列叫App那这样去查出来的话实际上就等于相当于你在原来的PromiseSympos这张表里面把所有的KeyBusNable都存下去了其实这里面就基本上是我们最终的一个方案我们将PromiseSymposTarget里面的一些Nable以及所有的KeyBus的再去定义一个Pod的时候使用到的AnnotationNable和Env等等都可以去关联到每一个Pod或者Container上面一旦关联以后我们就可以发现实际上我们需要的我们Explorer这一侧自己去生成的Nable的数量并不是太多我们直接把它平铺存储下来就行了然后上面我们再存两个Matter的Nable来去使得我们整个Diction翻译的机制使得有Work基本上我们会看到它的列的数量已经很少很少了如果说我们再去结合起来另外一个因素就是ClickHouse是一个非常非常高性能的劣势数据库劣势数仓对于这样少的一个列它也是能够完全能够媲美和TSDB存储一个SeriesID这样一个性能的那TSDB存储SeriesID之前可能还要做一系列的翻译把整个Nables的组合翻译到一个特定的SeriesID上如果我们的SeriesID的体量特别大这个翻译可能还是比较困难和有挑战的比如我们Series的Cardinality我们的Cardinality是2亿或者是两个B020亿这个时候我们在内存里面去做这样一个翻译可能有挑战我们还得在数据库里面去查一查等等做这样一些处理而如果我们直接到哪里去做结合Metanable和Dictionary以及把一部分没有规律的这一部分屏幕展开那我们其实我们的存储会非常简单我们整个存储的逻辑就会非常简单那这个在DeanFlow里面来我们把它叫做smart encoding对 其实整个流程来讲我们从最初这个问题再去产生出来的时候刚才我最初开始的时候也分享了一下我们通过EBPFmly去查一查的数据它的Cardinality非常高那我们会发现我们再去把data传过来的时候如果还同时把KBS里面的所有的tag这里面其实就是PromiseJustEnable也注入到这些data里面一方面会使得data变得非常非常宽可能会有带着100多年或者是200年甚至200多年的tag存下去那我们做的一个做法就是分离的去传输tag和data那我们把tag来去做一个编码做一个ID化那这个tag是很少的量的tag是meta tag可以认为就是少数几个大量的customer tag包括KBS的nabel annotationNV等等这样一些包括PromiseJustEnable里面targetNable我们都可以去分离的把它存出在dictionary里面所以我们的data和nabel是分离存储的但是依靠cleanhouse的dictionary的机制我们可以在查询的时候就像在查一个big table这样的一个大表一样我们的体验还是非常好的查询的时候可以是tag和无线的customer tag这样一个非常宽的一个表然后因为我们在这里面做的分离存储也做了tag的ID化所以我们再去写入cleanhouse的时候会取得得到比它的no cardinality的字段或者是stream的字段都要好的一个性能开销就是蓝色的一个部分比如我们相比于no card的字段我们的CPU消耗会小很多不需要cleanhouse对于每一份数据把所有的这些对里面的tag都去翻译到一个ID了我们只需要翻译meta tag就行了另外我们还可以说把对于存储的次盘量因为我们已经转换成了一个ID所以次盘量也能降下来但是这个里面右边的数据的评估其实没有去包含到这些这16个字段这16个字段我们实际上直接存一个stream的no card的一个stream就行了OK我们下面会有一些benchmark的一些结果比如说我们可以看到这是和victurial matrix的一个比较这个里面我们的deepflow和victurial matrix和memure对吧我们可以看到至少和VM的消耗是相当的因为中间这一行数据直接是从victurial matrix官方的博客benchmark的博客里面取的数据这是一个比较大的压力的一个情况下好处就在于我们确实是相当的一个资源消耗当然cartinality可以是unlimited的对吧我们可以增加任意多的比如说我们所有的kbass-nabel的字段都能去查询对然后再往下我们会有一些这是我们的一个小的生产环境比如说我们会有6000多个metric然后我们target是2,000多个然后nabel的value是79万个这nabel value其实是拼不开的它不存在一个decard的一个情况不像是cartinality的一个机制所以这个里面其实可以很大了对吧其实这里面就已经是很丰富的一些nabel的值了比如说我们写入4000k symbols每秒的这样一个写入量消耗的资源其实也是比较客观的刚才对比victurial matrix已经做了写入的一个资源对比因为从查询上如果说单接电有1500qps的情况下我们通过一些cash的一些机制其实能达到也是能达到比较好就是查询加写入400k的sambles每秒就是40000sambles每秒加1500k的查询我们整个的这个机器的cpu的usage是40%到60%之间那最后这一页其实就介绍一下我们做的这个工作背后的一个项目就是deepflow对deepflow呢它其实也可以作为primaces它的后它的一个back end对 它的一个remote storage我们现在也支持向primaces把数据通过remote right的这个API写给deepflow那同样呢deepflow更多的人是去采集ebpf的向指标数据深沉脱谱向分布式追踪数据我们有基于ebpf的完全零侵扰的分布式追踪那以及continue profile数据这个都是deepflow原生的功能那这些数据呢也可以以一个sourcedata source的方式比如说为golfana提供ccode的API为primaces提供pramql的API对 还可以去export到open telemetry的collector等等都可以去来去做这样一个能力这里面有我们的一个微信的群大家有兴趣可以扎一下然后当然也有discord的discord在我们的github的这是我们github的地址这是我们的官网大家可以去关注一下实际上这是我们把我们的项目里面的可观色性能力里面形容一小部分来去和大家做一个分享来去分享一下怎么去做一个其实是特定场景下因为可能我们的一些使用场景我们希望primaces的后端它可以不是那么需要有人去维护不是那么去关心你不需要去提开发的屁股说你这个nable为什么又有一个高技术的这个字段写进来了你不需要去特别小心地去维护它的内存等等这样一些资源消耗我看时间差不多了看看大家有什么问题吗谢谢回到前面那个电影Skeema的那个页面就是具体是在哪个页面对 这个也行比如说我看到你电影了一个LabelName1和LabelName到16那比如说它这个LabelName具体的值和比如说MetricName这个关系是怎么维护的我们可以有一张额外的一张表来去维护一下但是这个表可以在Dictionary里面或者在Message里面都行这个我就没有多做介绍了它可以把每一个MetricName其实把它去存下来Name就是说它有哪些哪些Nabel对往后追加就行了这个不会太多就在我们做那么多事以后都很少了已经好 谢谢你好 我想问一下就是怎么样把这个这个你们Depro做成一个Remote Story就是比如说我可能有很多个Commonets集群然后我通过这个我每个集群里面会有一个Promise HAA然后它负责采集这个集群然后我再通过Remote Right过来做一个Deep Flow的这个存到一个Deep Flow的集群里面去然后我这个查询我就统一从Deep Flow来查询就这样的一个方案里面是支持的吗对 支持的就是数据是怎么过来的像这些External SignalsExternal Signals包括Promise它会先到我们的Agent用Agent里面Agent它可以在每一个KBS Caster里面都会有其实Agent也可以作为Promise is a server的一个side card或者可以和它半身的都行Agent会把数据给到比如说一个server的一个集群这个server集群就是面对所有的Promise is the caster的再往上我们提供的Querry的API这些都有包括Promise is the query language所以你能让来讲把Promise is的这些所谓的Exporter的Metric导到Deep Flow里面来以后像Graphana上的Dashboard应该是不用改的就说那你们是必须要有把Deep Flow的Agent作为Promise is a side card来传输数据到你的server是吧不见得就是它可以是side card也可以是两个独立的pod只要是能够去把数据传给我们的Agent就行但是我们没有去采用一个直接转给server的办法主要是考虑到其实Agent它可能可以散不到好多这个caster里面我们不需要把server也散不过去这种情况下可能对于我们server端的管理的一个复杂队就会低一点因为Agent它是一个很轻量的一个存在而且除了去集成integre的Promise is the这些数据以外我们还可以integre的像Open Tentemetry像ABPF那都可以去采集到那其实我要确切问你就是说你们支持PromiseRemote Right吗对确切的支持所以下面第一侧是Remote Right上面第一侧是Remote Rate和Promise Toil两个OK 谢谢好您好还是想问一下因为您提到的分离了相当于infra就cubanitis本身的那些label和application的label对然后我就想问一下因为我们其实看到的是很多时候那些infra的那些label是cardinality很高比如说像seal deviser暴露出来的jmx我想问一下就这些的话你们有没有测试过是大概能优化多少因为刚刚提的那个case里面主要是针对applabel的优化所以说拆封了这两块对 是其实两个方面第一部分就是infra的label它的数量就是Promise to targetABI返回的一个数量可能一般是1或者是10万量级对吧其实取决于你的这个pod的数量对 因为我们看到就是说在class的数量和pod的数量非常大之后像seal deviser里面的很多的ID它是变得非常非常快的是的所以cardinality就很高对 但这个问题不大就是说它再怎么快呢其实它还是有一些关联性的就是虽然有这么多这么多label但其实它都关联到instance or job那可能就会比较方便一点这是故事的其中一个部分另外一个部分就是说我们其实认为不需要target的注入这么多label我们会发现所有这些label比如说castnamelamespace其实我们完全可以从pod的属性就可以拿到不需要注入到target里面pod属性拿到以后我们再怎么去办再再用一个所谓的dictionary就可以查出来了比如说dictionary里面存了pod的ID那如果说我pod的ID增长得很快的话会不会对于本身dictionary性能有一些平民或者优先moren的话clienthouse的dictionary在50万以内那是一个flatten的dictionary再往上它可以有一些像hash其实查询的速度也不是任何问题而且我们在这上也做一小点catch就是查label的这个部分其实可以是做catch的然后最后想问一下就是整个table创建因为是要预先根据你的label的structure来创建吗对这部分是手动的过程吗还是是自动的其实这个不是太复杂动态的可能在这一部分比如说16不够了再给它加一点好的谢谢很像了然后我看最后是是我们用一个abplabel加那个tour guidelabel然后一个组合对然后我们刚开始不是第一个方案就是在探索的时候然后是先用了一个数组对去把所有label进行下标然后我在想你是否可以在最后那个只需要那个16个label的时候也是可以的对也是可以的但是数组有一个就看你对于查询性能的一个诉秀有多大但查询查label的这个过程确实是可以用cash来去解决的就如果说是用数组的形式去存下去以后它可能会减少很多维护的复杂度但同时会这是一个balance就是说你如果说你愿意去扫那个16个长度的一个数组那完全是OK的好谢谢谢谢大家