Poseidon系统是360的开源日志搜索平台。已在生产过程中使用,可搜索数百PB的数万亿日志数据,快速分析检索特定字符串。由于Golang独特地支持并发编程,Poseidon的核心搜索引擎、传输器、查询代理都是用Golang开发的,goroutine+channel广泛应用于核心引擎查询、多天查询、多天数据异步下载。大家,早安。我是郭军,很高兴今天能在这里与大家交流。我今天演讲的主题是Golang在万亿搜索引擎中的应用。Poseidon在希腊语中意为海神,这里是海量数据集的主人。之前我的工作都是面向大量用户的。去年年中开始接触大数据、海量数据等场景。在今天的演讲中,我将主要涵盖以下几个方面:设计目标围棋应用场景以及如何应对遇到的挑战?开源变更总结设计目标首先说说为什么要搭建这个系统。这是一家安全公司,APT(高威胁持续事件)。在追踪APT事件时,我们通常会寻找样本在一定时间内做了什么。如果是在大量的日志中查找这些信息,运气好,没有堵塞的话,大概两三个小时就可以跑完了。如果运气不好,任务运行过多,堵塞,就需要修复了。显然,获取数据可能需要一两天时间。效率不高。我们的设计目标,我们的总数据量保留了三年的历史数据,一共一百万亿条记录,大小为100PB。秒级交互搜索响应,从前端发起的请求到某一天的数据,我们会在几秒内返回给您。我们将第二层设置为在60秒内返回。事实上,测试结果都在3到5秒以内,90%的请求都在10秒以内。每天支持2000亿数据量的输入,原始数据只有一份,不侵犯现有MR任务。ES的原始数据不仅保存一份,还会保存一份。这么大的数据量,如果再保存一份,维护成本和价格都会非常高。ES无法支持EB级的数据量。现在行业已经到了1000亿,而我们只能做到300多G。然后自定义分词策略,我们每个业务的日志格式都不一样,分词策略需要特别灵活;支持故障转移节点负载均衡、自动恢复、批量下载原始日志。图1图1是我们的整体流程。这个数字比较复杂。我们的一些同事之前分享过这种架构。今天分享架构可能时间不够。图2是它的一个非常简单粗略的图。图2Go的应用场景及遇到的挑战首先,日志原文。转换时,我们将原始日志每128行提取为一个文档,多个文档拼接成一个文件。这里有人会问为什么我们选择128行。我们每天的日志量是700亿。根据每行一个文档,我们有700亿个文档。一行日志一个文件,700亿个文件太占空间;700亿数据将膨胀。选择128线的原因是:***,700亿除以128大约是5.46亿,在一定范围内可以承受;第二,因为我们的身份证都是数字形式的,是以发号人的形式发放的,所以我们在压缩号码的时候,必须采用各种压缩方式。我们这里使用的插值对于128个数字的压缩效果更好。压缩128行日志比压缩1行日志要昂贵得多。我们每天都有原始日志。我提到的业务每天有60个原始日志。压缩后,我们可以记录10条左右,这是每天的数据。我们在输出的时候,这是原来的日志。最后,我们需要在原始日志中找到它,最后我们需要构建数据。因为当我们要存进去的时候,很多人不理解我刚才说的,多个链接连接起来形成一个文件。有一个非常大的好处,我把里面的数据放到另一个文件里,一直叠加,***这个文件可以解压。换句话说,所有文件都输出到一个文件中。作为这个文件,我可以从这个文件中提取某个部分,我可以解压它。这是一个非常大的特点。因为我需要读取一个日志,所以我必须知道我从哪里读取的,并且我需要知道我读取的压缩文件,解压后的日志是128行。我们把整个原始数据放在这里建立索引和原始数据,大致就是这样一个过程。先看离线引擎,客户端请求日志,包括PC卫士、网络和浏览器等,相当于传统搜索引擎的爬虫。下面会具体讲离线生成DocGz和DocGzmeta,然后构建原始数据。在线引擎,web我们做简单的页面开发,发送到proxy集群,然后发送到searcher集群,再去readHDFS。readHDFS服务是用Java开发的。Java开发有很多坑,但是我们不得不用,因为Java仍然是最适合操作Hadoop的语言。再说说数据结构。我们使用ProtrBuffer来描述核心数据结构。每个ID分为两段,docID是我的文档编号;第二个是rowIndex,每一个都会对应多行日志,我对应的128行中的哪一行日志就是这个的定位。我们用map的形式来描述,它是由DocID组成的一个列表,每一个会对应多个DocIDList。在map和string中,我需要先找到map,然后再把数据取出来。如图3所示。图3讲的是搜索引擎的核心技术。首先是倒排索引,倒排索引有趋势,DocidList很长。我们将首先计算分词的hashid。当我们知道了hashid,想要查询的时候,我们需要创建一个平台,给哪个业务查询。查询的时间,查询哪一天的数据,我们的引擎不是实时的,因为数据量太大做不了实时,只能查今天和昨天。然后解析invertedindex,得到里面对应的文档信息。找到这个位置后,将我们需要的原始数据全部提取出来,然后解压。我们知道某个分词对应于哪个DocidList。根据DocidList,我们可以查到要查的地图信息在哪里,得到之后拼个路径,取出原始数据。取出原始数据后,一个文件中会有128行日志。128行日志Doc中的Rowindx可以找出文档在哪一行,过滤即可。用很简单的话总结一下,因为Docid比较长,我们保存一个位置,我们的DocidList中每个Docid对应更多的文档。当我们阅读原始文件时,我们也保存了一个位置。在计算机领域,每一个难以解决的问题都可以通过增加一个间接的中间层来解决。如图4所示。这句话在我们的系统中有一个很好的尝试,不只是这个。图4再说idgeneratror。按照日业务27.7亿计算,分词后就是100亿。每个词段对应277行日志。这是平均数。每天有27.7亿个Docid。按照每4个字节计算,仅Docid这个数字就有将近11TB。这里已经处理过了,使用分段区间获取更低的qps,每天的id从0重新分配。我们每天的Docid倒排索引量是2.4T。每天27700亿我们做起来有点不好意思,我们想了个办法,我们把time作为key加到我们的businessname里,每天从头重新分配id,保证我每天的量不会太高,会分配Docid不需要太大。如果太大,数据可能会膨胀。哪个业务,什么时间段,我哪天建了索引,我这次要请求哪个segment,如果我请求1到100个segment,1到100个segment会在idgeneratro这个void里提前预留。代理/搜索器详细设计。Searcher的核心引擎就是在四级索引中做了什么,包括过滤和模糊查询等,我没说这些不是主要业务。从中取出地图数据,然后取原始数据。取完数据后,我们有很多原始数据,非常大,大约有几十兆字节。对于数据比较大的业务,会展示在页面上,点击链接查看原始数据,点击后再回来请求。这是一个非常简单的结构。如图5所示。图5Searcher并发模型。因为在读取四级索引时,读取Docid的过程是完全一样的,所以我这里以读取Docid为例。比如我拿到DocidList的数据,我会为每个Docid分配一个Goroutine,拼接出doc路径,读取原始日志,然后过滤,最后返回给前端。如图6所示。图6显示了如何应用第一个瓶颈。我们团队的基础组件都是C++,我们团队的核心业务,还有在线引擎和核心引擎都是用C++来完成的。我们使用gdb进行调试。进程太多。我们一开始用c++组件偷懒,后来编成C,再放到Go。每次读Docid,每一个文件都会被读取,我们的应用经常挂,当时没有原因,***我们只看到在执行CGO的时候,收到一个信号,就是signalexit,然后我们debugwithGDB,说是进程太多,因为CGO执行的时候会新建一个M。解决方案:用Go重新实现,将组件作为http服务,用GoClient调用,集中处理。第二个瓶颈。在系统中,我们大量使用Goroutine,子程序panic无法在主程序中处理。解决方案:我们在channel类型中使用struct封装正常数据和error,在主协程中取数据统一处理。经验总结。即使精通多种语言,也最好不要混用,在引入其他语言的解决方案时要非常谨慎。不要相信完全恢复,它不能恢复一些运行时的恐慌。看看我们的Proxy多日并发查询设计。如图7所示,多日查询有两种选择。第一个解决方案添加了多天查询,这使得我们的核心查询引擎非常臃肿。我们还是说,加个中间层。将多天变成一天,然后在Proxy中获取所有的单天数据,形成多天查询。图7我们有另一个项目,向Poseidon请求数据。我们想到了两种解决方案。第一种方案是你在自己的第三方系统做缓存,或者我们做缓存。这是我们的选择。如果在第三方系统中做缓存,所有的查询和缓存只能在第三方系统中使用。如果缓存在这里,他们会向我们发送请求,所有其他第三方都可以使用它。我们是这样实现的,先请求Searcher获取当天的数据,比如查询一个月的数据,请求Searcher单日的数据,如果每个Goroutine都查询一天,则每个Goroutine获取到Searcher一天的数据,解决一下,看看是不是错误的数据。如果是错误的数据,直接将这条数据作为错误返回给客户端,而不是将整个错误返回给客户端,因为这一天只有一条数据有错误。并不是说我们查询30天的数据时,只要某天某条数据有错误,就直接返回给用户。我的系统不可用。如果不是错误数据,则根据请求参数,请求参数有很多。除了这些,还有查询的时候,根据这个做一个CaceKey,然后回调给前端。我们遇到了一个问题,每个用户都会运行整个索引过程,也就是说,用户会实时测试我们。同时,同样的数据不会在缓存时间内走完整个readhdfs进程。buildindex是programmatic的,我们会有监控,如果是programmatic的,我们会知道,程序挂了会报警,但是数据错误是未知的,我们还没有实现这种监控.但是这个数据错误是未知的,我们会花很多时间修复索引,重写日志,运行Docid,解决漏洞。我们的解决方案是先减少缓存时间。在可容忍错误的数据时间内,用户查询能及时发现问题,数据恢复一两天即可。没必要缓存30天,一两个月。***错误的数据会越来越多。第二种方案,参考NSQ,利用for+select的不确定性来分流,随机流量到chanel和hdfs进行热测试。缺点是相比第一种方案开发成本有点高。需要注意的是开发成本不是很高,因为select只能从Chanel获取数据。第二次经验总结。不要选择一些很高端的技术,或者我们所说的一些黑科技,简单、有效、足以解决问题就完全没问题。使用Goroutine设计并发程序非常方便,但是并发运行模型必须持有。我们之前在Gopher群里发过一篇博客,里面发了很多动态图,一些Go的Goroutines和channel是如何并发的,动态图很炫。我们在写自己业务的时候,看了看Goroutine,看Goroutine和channel是怎么联系起来的,有自己的概念。当我想表达我的观点时,我找不到一个非常合适的词来描述它。不知道这个词以前有没有,或者有其他含义。代理下载多天异步。如图8所示,前端发起请求的时候,你要选择下载多少天,下载多少数据。服务器收到请求后,会立即返回给客户端。我已经收到了,并将此消息写入频道。一开始我们说过readHDFS是JAVA写的,Goroutine太多,底层挂了。两个搜索者去HDFS,一个分词对应上百个docid,可能对应上百个文件,因为每个docid不一定在一个文件中。在Searcher的时候,好像是一个request进来了,但实际上以后会越来越大,最后可能呈指数增长,就像我们的滚雪球一样。图8.首先JAVA做了一个简单的连接池,然后是熔断机制。如果连接数超过一定数量,则直接返回错误。就像我们很久以前的保险丝,当家里的电费很高的时候,保险丝是用铅丝做的,铅丝会熔化。先说GC的变化。首先我要说,GC从来都不是我们整个系统的瓶颈。这里说的几点是我们在升级后做的简单测试,在这里分享给大家。如果有其他同学比我们做过更详细的测试,可以交流。去1.7。我们之前用的是1.5,升级到1.7后,我们的GC降到了三分之一。Nginx代理问题,之前分享的时候,有同学问我Go前端要不要加nginx代理。我之前做的系统是针对大量用户的。我们只是将GoServer打包成二进制可执行包,请求发送到lvs的80端口,再转发到GoServer8080,非常简单。在这个项目中我们使用了nginx,我们使用它是有原因的。访问控制和负载平衡。我们可以使用LVS来做负载均衡。在我们项目的场景中,很少有人使用它。***我们是内部项目,权限问题,我们的前端端口只能被一些开放的机器访问,除了我们自己的前端设备可以访问,其实还有其他团队会直接来写脚本请求我们的数据。这两个我们在nginx中直接使用,所以我不需要在Go中做,直接用nginx做简单的负载均衡就可以了。是否使用nginx完全取决于你的业务场景。因为在这种场景下,nginx的加入只是稍微增加了运维的负担,而ip限制和负载均衡不需要重新开发。之前没用,因为在里面没有起到任何作用,之前是对外服务,没有任何限制,任何人都可以来索取。开源变化我们考虑开源。去年11月,我们开源了系统,系统中66%的代码是用Golang写的。我们有两个问题需要解决。第一个问题是第三方依赖问题。我们的开源主解决方案不使用我们自己的内部依赖包。我们应该如何维护这些第三方组件呢?我当时和很多人谈过。我们交流过,发现有很多方法可以做到这一点,但是它们各有优缺点。几乎没有完美的解决方案可以解决添加依赖到依赖和多层依赖的问题。至少我还没有找到一个。既然没有,就选择最流行,最简单的方案,用这个方法解决。在我们整个服务中,我们自己开发了几个服务,一共五个。我们当时考虑到,如果让用户部署五个服务,即使我们写好了脚本,部署之后,每个用户的操作系统不一样,CPU位数不一样等等,就会出现各种各样的问题。排查问题时,不知道该检查哪个服务。对于我们开发者来说,在排查问题的时候,也会根据日志一一查找。我们认为我们将所有服务打包成一个多合一包。在实际沟通试用中,我们了解到很多人并没有选择AllinOne,而是选择了这五个服务独立部署。我们已经开源五个月了,很多人要我们开源模糊查询和过滤。我们做的模糊查询很简单。我们使用具有并发功能的数据库。我们先把模糊查询需要的分词分出来,放到数据库里,我在数据库里操作。我们平时用的模糊查询关键词大概是几十亿,几十亿的量是一个Operation,那简直太简单了。找到关键词后,你就知道关键词了。拿到关键词之后,接下来的解决方案就是用多个关键词查询多天。使用多个关键字与使用单个关键字相同。的。使用多个关键字查询与查询多天相同。将每个关键字分成一个Goroutine去查询,问题就可以解决了。总结回顾首先,Go的开发体验比较好,性能比较高,服务也很稳定。除了线上的一次意外,我们似乎再也没有发生过。我们使用自己的文字进行在线监控。如果挂了,它会自动拉起来。当然这是比较低级的方式,因为不一定挂了,但确实是死了。它可以满足大多数需求场景。GO语言程序开发需要在代码可读性和性能之间做出平衡,应用并发模型需要可控。我们群里有很多人问连接池和对象池。我们不说连接池,因为很多客户端都会实现连接池的功能。我们考虑对象池。对象池的优势确实很大,因为它可以复用对象来减轻压力,这是最核心的功能。重用对象解决了gc压力,但是还是存在代码可读性的问题。引入对象池与业务无关。你得看对象池是怎么做的,代码可读性会很差。更何况对象池的解决方案在Go1.2用起来很爽,但是从1.4到1.7到现在,对象池的解决方案已经远远没有被使用了,因为gc不再那么明显了。除非在非常极端的情况下,我们可能会用这种非常极端的方式来解决问题,但是我想大部分公司都不会遇到这种问题。我们知道Go正在开发Android。我们现在用的最多的是它和c++、c的结合,然后用CGO引入GO。将它与其他语言一起使用时要小心。即使你非常熟悉这门语言,你也不知道两者的结合。起床可能会导致一个问题,一个你可能永远无法解决的问题。需要合理引入第三方解决方案,平衡运维成本和系统维护成本。
