Go在百万亿级搜索引擎中的应用

本文涉及的产品
网络型负载均衡 NLB,每月750个小时 15LCU
应用型负载均衡 ALB,每月750个小时 15LCU
日志服务 SLS,月写入数据量 50GB 1个月
简介:

Poseidon 系统是由 360 开源的日志搜索平台,目前已经用到了生产环节中,可以在数百万亿条、数百 PB 大小的日志数据中快速分析和检索特定字符串。因为 Golang 得天独厚的支持并发编程,Poseidon 的核心搜索引擎、发报器、查询代理是用 Golang 开发的,在核心引擎查询、多天查询、多天数据异步下载中大量使用了 goroutine+channel 。

大家上午好,我是郭军,很高兴今天在这里和大家交流。我今天演讲题目,Golang 在百万亿搜索引擎中的应用。Poseidon在希腊意思是海神,在这里是海量数据集的主宰者。

之前我的工作一直面向海量用户,去年年中我接触大数据以及海量数据这样的场景,在今天的演讲中,主要会涉及以下几方面内容:

  • 设计目标
  • Go 应用场景与遭遇的挑战
  • 怎样应对?
  • 开源的改变
  • 总结

设计目标

首先说一下为什么要做这个系统。这是一个安全公司,APT ( 高危威胁持续性事件)。在追查APT事件的时候,我们通常会找一个样本在某一样时间之内到底做了什么事情。在海量日志中找这些信息的话,运气好不堵塞的时候,大约两、三小时可以跑出来,如果运气不好,跑的任务太多堵塞的话就要修复,可能一天两天才能出来数据,显然这样的效率是不高的。

我们的设计目标,我们总的数据量保留三年的历史数据,一共有一百万亿条,大小有 100 PB。秒级交互式搜索响应,从前端发起请求到某一天数据,我们会在几秒钟之内给你返回。我们之前设定秒级60秒返回就可以,实际上做完之后测试的结果都在3秒到5秒之内,90%请求在10秒之内。每天要支持两千亿数据量灌入,原始数据仅存一份,对现有 MR 任务无侵略。ES 原始数据不止存一份,会再存一份,我们这么大数据量来说,再存副本的话,维护成本以及代价是非常大的。ES 支持不了百万亿级数据量,现在业界做到一千亿,我们只做到300多G。然后自定义的分词策略,我们每一个业务的日志格式都不一样,分词策略需要特别灵活;然后故障转移节点负载均衡,自动恢复,支持原始日志的批量下载。

Go在百万亿级搜索引擎中的应用
图1

图1是我们总体流程,这个图比较复杂,我们之前有同事分享过这个架构。如果今天再分享架构可能时间会不够,图2是它的一个非常简单的粗略图。

Go在百万亿级搜索引擎中的应用
图2

Go 应用场景与遭遇的挑战

首先原始日志。 在转化的时候我们把每 128 行原始日志抽取出来作为一个文档,多个文档联结在一起形成一个文件。这里会有人问为什么选择 128 行,我们每天日志量是700亿,按照每一行一个文档我们有700 亿文档。一行日志一个文档,700 亿文档占用空间太大;700 亿数据会膨胀。选择 128 行是因为:第一,700 亿除 128 ,大约是 5.46 亿左右,在一定范围内可以承受;第二,因为我们的ID都是数字形式,以发号器形式发出来的,我们压缩数字的时候,肯定要采取各种各样的压缩办法,我们在这个地方用的插分,对于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所示。

Go在百万亿级搜索引擎中的应用
图3

说一下搜索引擎的核心技术。 首先倒排索引,倒排索引有一个趋势,DocidList 非常长。我们一个分词会先计算出来 hashid ,知道 hashid 之后要查询的时候我们要做一个平台,给出要查询哪一个业务,比如我要查网络等等这些,我们以业务的简写拼接上hashid,然后要查询的时间,查询哪一天的数据,我们引擎不是实时,因为数据量太大做不了实时,只能做到今天查昨天。然后解析 invertedindex 拿到对应的文档信息在里面,找到这个位置之后,把我们所有的需要的原数据抽出来,然后解压。我们就知道某一个分词对应着 DocidList 是哪一个,根据 DocidList 去查要查的 map 信息在哪个地方,获取之后再拼一个路径,把原始数据拿出来。拿出原始数据之后,一个文件里面会有 128 行日志,这 128 行日志Doc里面rowindx 找到文档在哪一行,做过滤就可以了。用非常简单的话来总结一下,因为 Docid 比较长,我们存一个位置,我们的 DocidList 每一个 Docid 对应的文档也比较多,我们读原始文档的时候,也会存一个位置,在计算机领域中,各种难以解决的问题都可以添加一个间接的中间层来解决这个问题。如图4所示。这句话在我们系统中有了很好的尝试,不仅是这一块。

Go在百万亿级搜索引擎中的应用
图4

再来说一下 idgeneratror 。 按照每天业务 27700 亿来算,分词以后是 100 亿,每一个分词对应 277 行日志,这是平均数,每天 Docid 有 27700 亿个。按照每个 4 字节来计算,光是 Docid 数字将近 11TB。在这里进行了处理,采用分段区间获取降低 qps,每天的 id 重新从 0 开始分配。我们每天 Docid 倒排索引量在2.4T。每天 27700 亿我们做起来也稍微有点发怵,我们想了一个办法,我们业务名加时间作为 key,每天id 从零开始重新分配,这样就可以保证我每天的量不至于太高,而且分出来的 Docid 不用太大,如果太大的话,可能数据就会比较膨胀。我现在建了索引是哪个业务,什么时间段,哪一天的,我这次要请求哪一个区段,如果说我请求了 1 到 100 个这个区段,在 idgeneratro 会提前预留出 1 到 100 这个空隙。

Proxy/Searcher详细设计。 Searcher核心引擎就是走四级索引里面做的事情,其中包括过滤和模糊查询等等,这些不是主干业务我没有说。从里面拿出map数据,然后再取原始数据,取完数据以后,我们有很多原始数据非常大,大约有几十兆左右,如果放在处理器前端,前面会直接卡死,我们会把原始数据比较大的业务,在页面上面给大家展示,点击查看原始数据这么一个链接,点了以后再过来请求一遍,这是一个非常简单的架构。如图5所示。

Go在百万亿级搜索引擎中的应用
图5

Searcher并发模型。 因为读 四级索引的时候,读 Docid 的过程一模一样,所以我在这里用读 Docid 举例子,比如我拿到 DocidList 的数据,我会给每一个 Docid 分配一个 Goroutine ,拼接出来 doc path ,读取原始日志,然后做过滤,最后返回给前端。如图6所示。

Go在百万亿级搜索引擎中的应用
图6

怎样应用

第一个瓶颈。 我们团队的基础组件全是 c++,我们团队核心业务,以及在线引擎、核心引擎都是c++ 来做的。我们用到 gdb 进行调试,进程过多,用 c++ 组件一开始想偷懒,然后编辑进C,再放到 Go 里面去。每一个读取 Docid 中,每一个文件都会去读,我们的运用程序经常就挂,当时也没有原因,最后我们才看到执行 CGO 的时候,我们收到一个信号,就是 signal exit,然后我们进行GDB调试,说是进程太多,因为CGO在执行的时候会新建一个M。

解决方案:用Go重新实现一遍,将组件作为http服务,Go Client调用,做集中式处理。

第二个瓶颈。 在系统中,我们大量使用 Goroutine,子写程 panic 在主写程不能被处理掉。

解决方案:我们在通道类型里面为struct,封装正常数据和error,在主协程取取出数据,统一做处理。

经验小结。

  • 即使精通很多语言,最好不要混用,要非常谨慎引入其他语言的解决方案。
  • 不要完全相信recover,它不能恢复runtime的一些panic。

看一下我们的Proxy多天并发查询设计。 如图7所示。要做 多天查询有两种方案。第一种方案把多天查询加上,这样使我们核心查询引擎变得非常臃肿,我们还是那句话,加一个中间层。把多天变成单天,然后在Proxy 拿到所有的单天数据,就形成了多天查询。

Go在百万亿级搜索引擎中的应用
图7

我们还有另外一个项目,请求Poseidon的数据,我们想到两种解决方案,第一种解决方案,你在自己第三方系统里面做缓存,要不我们做缓存,我们是这样取舍。如果第三方系统里面做缓存,所有的查询,缓存只能在第三方系统里面用。如果在我们这里缓存,他们发了请求到我们这来,其他所有第三方里面都有可能能用上。我们是这样做的,首先请求 Searcher 拿到当天的数据,比如查一个月的数据,请求 Searcher 单天的数据,如果每一个Goroutine 去查一天,每一个 Goroutine 拿到 Searcher 单天数据之后,把它解出来,看一下是不是错误数据。如果是错误数据的话,直接给客户端把这条数据返回错误,并不是给客户端整个错误,因为只是这一天某一条数据有错误。而不至于我们在查询 30 天数据的时候,里面只要某一天某一条数据有错误,就直接返回给用户,我这个系统不可用。如果不是错误数据,会根据请求参数,请求参数有很多。除了这些之外,还有查询的时间,根据这个来做一个Cace Key,然后打回给前端。

我们遇到一个问题,每一个用户会把整个索引流程都跑一遍,也就是说用户会给我们实时测试。在同一个时间之内,同一份数据在缓存时间之内不会走完整个 readhdfs 流程。build index 程序化,我们会有监控,如果程序化我们会知道,程序挂了会报警感知,但是数据错误却是未知,我们现在还没有做到这种监控。但是这个数据错误是未知的,我们修复索引就会花费大量时间,去重新写日志,跑 Docid,还要解决漏洞。

我们的解决方案,第一个减少缓存时间,在可容忍错误数据时间之内,用户查询能及时发现问题,恢复一天两天数据还可以,不至于缓存 30 天或者一、两个月,到最后错误数据会越来越多。第二个解决方案,参考 NSQ,利用 for+select 的不确定性来分馏,随机流量到 chanel 和 hdfs 做热测试。缺点,就是开发成本相对第一种方案来说有点高。这块要注意,开发成本并不是非常高,因为 select 而只能从 chanel 拿数据。

第二个经验小结。 不要选择非常高大上的一些技术,或者说一些我们所说的黑科技,简单、有效、够用能解决问题完全可以。利用 Goroutine 设计并发程序很方便,但是并发运行模型一定要 hold 住。我们之前Gopher 群里面发过一个博客,里面发了很多动态图,一些 Go 的 Goroutine 和 channel 如何并发,动态图画的非常炫。我们在写自己业务的时候,我们看了 Goroutine 以及 Goroutine 和 channel 怎么联动,我们自己有概念。我要表达观点的时候,我一时也找不到非常恰当的名词来描述,我不知道这个名词之前有没有,或者有没有其他的意义。

Proxy多天异步下载。 如图8所示。前端发起请求,要选择下载多少天,下载多少数据,服务端接受到请求之后,马上给客户端返回,我已经收到了,把这个消息写到channel。刚开始我们已经说过在readHDFS是是用JAVA写的,Goroutine太多,底层挂掉。两个Searcher到HDFS的时候,一个分词对应上百个Docid,可能对应着上百个文件,因为每一个Docid不一定在一个文件里面。在Searcher里面的时候,看起来进来一个请求,实际上往后会越来越大,到最后可能就是指数级的增长,像我们滚雪球一样。

Go在百万亿级搜索引擎中的应用
图8

首先JAVA做了简单的连接池,然后有熔断机制,如果超出一定的连接数,直接返回error。像我们很早之前的时候,保险丝,家里面的电率大的时候,保险丝是用铅丝做的,铅丝会熔化掉。

再说一下GC的变化。 首先我说一下GC在我们整个系统中,从来都不是瓶颈。在这里说的几点,是我们升级之后简单做的测试,在这里和大家交流一下。如果有其他做测试比我们更细的同学,可以交流一下。

Go 1.7。 我们之前用的 1.5,升级到 1.7 之后,我们的 GC 下降到了三分之一。

nginx 代理问题,之前我做分享的时候,有同学问我在 Go 前端要不要加nginx代理。我之前做的系统面向海量用户,我们只把 GoServer 打包成二进制的可执行包,请求打到 lvs 的80 端口然后再转发到 GoServer 8080,非常简单。在这个项目我们用了 nginx,我们有用它的理由。

访问控制和负载均衡。 负载均衡我们可以用 LVS 做,我们这个项目的场景,使用的人非常少。第一我们是一个内部项目,权限问题,我们所在前端端口只能让开放的一些机器来访问,除了我们自己的前端器会访问以外,其实还有其他的一些团队,会过来直接写脚本请求我们的数据。我们nginx里面直接用了这两个,这样我不需要在Go里面做,前面就可以直接用nginx做了简单的负载均衡。要不要nginx,完全取决于自己业务的场景。因为在这个场景中,加了nginx也只是给运维稍微增加了负担,但是ip限制和负载均衡不需要重新开发了,之前没有用因为它没有在里面起到任何作用,而且之前是对外的服务,不需要有任何的限制,任何人都可以过来请求。

开源的改变

我们考虑开源。 在去年11月份的时候,我们开源了系统,系统有66%代码是用Golang写的。我们有两个问题需要解决,第一个问题第三方依赖的问题,我们开源主体方案没有用到我们自己的内部依赖包,这些第三方的组件,我们应该如何维护它,我当时和很多人交流过,这种方式也比较多,但是他们各有各的优点和缺点,几乎没有一个非常完美的方案,能解决到依赖里面再套依赖,以及多层依赖关系,至少我没有找到,既然没有的话,就选择最大众化,最简单的方案,用这个方式来解决。

在我们整个服务里面,我们自己开发了几个服务,一共有五个。我们当时考虑过,如果让用户部署五个服务,即使我们写好了脚本,部署起来在每个用户端操作系统不同,CPU位数不同等等,都会出各种各样的问题。排查起问题来,不知道排查哪一个服务,对于我们这些开发者来说,我们排查问题的时候,也会根据日志一个服务一个服务去找。我们考虑到,我们把所有的服务打成一个ALL in One一个包。在实际交流试用中,我们了解到有很多人没有选择All in One而选择这五个服务独立部署。

我们开源有五个月,有很多人想让我们把模糊查询以及过滤开源出来。模糊查询我们做的非常简单,我们用了一个数据库,有并发能力。我们先把我们需要模糊查询的分词给分出来,放到数据库里面,在数据库里面我就可以操作,我们平常用到的模糊查询关键词,也就是几十亿左右,几十亿的量做一个操作,那简直太简单了,查到之后就知道关键词,拿到关键词之后,接下来的方案就是一个用多个关键词查询多天的场景,用多个关键词和单个关键词是一样的。多个关键词去查询和用多天查询是一样的,每个关键词分一个Goroutine去查询,就可以解决问题了。

总结回顾

首先Go的开发体验比较好,性能比较高,服务很稳定,我们除了线上有一次事故之后,好像就再也没有过。我们线上是用自己写的做监控,如果它挂掉就会自动拉起来,当然这是一种比较low的方式,因为它可能没有挂,但是它的确死掉了。可以满足大部分的需求场景,GO语言程序开发需要在代码可读性和性能之间做平衡取舍,应用程序并发模型需要在控制之内。我们有很多人在群里面问连接池以及对象池,连接池我们不说,因为很多客户端都会实现连接池这个功能,我们考虑对象池。对象池优点的确很大,因为它可以复用对象减轻压力,这是最核心的功能。复用对象解决了gc压力,但还有一个代码可读性的问题,引进对象池,对象池和业务没有关系,你要看对象池怎么做,代码可读性会非常差。还要说的是,对象池这种解决方案,在Go1.2的时候,用起来很爽,但是目前为止1.4到1.7的时候,对象池这种方案已经远远用不到了,因为gc已经不是那么明显。除非在非常极端的情况下,我们可能会用到这种非常极端的方式解决问题,但是我想大部分的公司都不太会遇到这种问题。我们知道Go在开发安卓,我们现在用的最多就是它和c++以及c的配合然后在用CGO引入到GO,谨慎与其他语言合用,即使对语言都非常熟,你也并不知道他们两个结合起来说不定引发一个问题,可能是你永远解决不了的问题。要合理引进第三方解决方案,在运维成本和系统维护成本要做平衡。


作者:郭军

来源:51CTO

相关实践学习
SLB负载均衡实践
本场景通过使用阿里云负载均衡 SLB 以及对负载均衡 SLB 后端服务器 ECS 的权重进行修改,快速解决服务器响应速度慢的问题
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
相关文章
|
7天前
|
存储 Go 索引
go语言中数组和切片
go语言中数组和切片
20 7
|
7天前
|
Go 开发工具
百炼-千问模型通过openai接口构建assistant 等 go语言
由于阿里百炼平台通义千问大模型没有完善的go语言兼容openapi示例,并且官方答复assistant是不兼容openapi sdk的。 实际使用中发现是能够支持的,所以自己写了一个demo test示例,给大家做一个参考。
|
7天前
|
程序员 Go
go语言中结构体(Struct)
go语言中结构体(Struct)
85 71
|
6天前
|
存储 Go 索引
go语言中的数组(Array)
go语言中的数组(Array)
90 67
|
9天前
|
Go 索引
go语言for遍历数组或切片
go语言for遍历数组或切片
82 62
|
11天前
|
并行计算 安全 Go
Go语言中的并发编程:掌握goroutines和channels####
本文深入探讨了Go语言中并发编程的核心概念——goroutine和channel。不同于传统的线程模型,Go通过轻量级的goroutine和通信机制channel,实现了高效的并发处理。我们将从基础概念开始,逐步深入到实际应用案例,揭示如何在Go语言中优雅地实现并发控制和数据同步。 ####
|
7天前
|
存储 Go
go语言中映射
go语言中映射
24 11
|
9天前
|
Go
go语言for遍历映射(map)
go语言for遍历映射(map)
23 12
|
8天前
|
Go 索引
go语言使用索引遍历
go语言使用索引遍历
17 9
|
12天前
|
安全 Serverless Go
Go语言中的并发编程:深入理解与实践####
本文旨在为读者提供一个关于Go语言并发编程的全面指南。我们将从并发的基本概念讲起,逐步深入到Go语言特有的goroutine和channel机制,探讨它们如何简化多线程编程的复杂性。通过实例演示和代码分析,本文将揭示Go语言在处理并发任务时的优势,以及如何在实际项目中高效利用这些特性来提升性能和响应速度。无论你是Go语言的初学者还是有一定经验的开发者,本文都将为你提供有价值的见解和实用的技巧。 ####