本文转载自微信公众号《黑客下午茶》,作者少。转载本文请联系黑客下午茶公众号。Snuba有一个查询处理管道,首先将Snuba查询语言(遗留和SnQL)解析为AST,然后在Clickhouse上执行SQL查询。在这两个阶段之间,对AST执行了几次传递以应用查询处理转换。处理管道有两个主要目标:优化查询和防止对我们的基础设施造成危险的查询。在数据模型上,查询处理流水线分为逻辑部分进行产品相关处理,物理部分侧重于查询优化。逻辑部分包含查询验证等步骤,以确保它与数据模型匹配或应用自定义函数。物理部分包括诸如提升标签和选择预聚合视图来为查询提供服务等步骤。查询处理阶段本节介绍上述每个阶段的代码和示例,并提供一些提示。旧版和SnQL解析器Snuba支持两种语言,一种是传统的基于JSON的语言,另一种是称为SnQL的新语言。除了传统语言不支持的连接和复合查询外,无论使用一种语言还是另一种语言,查询处理管道都不会改变。Snuba支持两种语言,一种是基于JSON的旧语言和一种称为SnQL的新语言。除了遗留语言不支持的连接和复合查询外,无论使用哪种语言,查询处理管道都不会改变。它们都生成由以下数据结构表示的逻辑查询AST。https://github.com/getsentry/snuba/tree/master/snuba/query基于JSON的语言老解析器源码:https://github.com/getsentry/snuba/blob/master/snuba/query/parser/__init__.pySnQLparser:https://github.com/getsentry/snuba/tree/master/snuba/query/snql查询验证(QueryValidation)这个阶段确保查询可以运行(大部分,我们还没有抓到所有可能无效的查询)。此阶段的职责是在查询无效时返回HTTP400,并向用户提供适当的帮助消息。这分为两个子阶段:一般验证和实体特定验证。一般验证由一组检查组成,这些检查在解析器生成后立即应用于每个查询。这发生在QueryEntity函数中。这包括防止验证,例如别名隐藏和函数签名验证。QueryEntity:https://github.com/getsentry/snuba/blob/master/snuba/query/parser/__init__.py#L91每个实体还可以以所需列的形式提供一些验证逻辑。这发生在类Entity(Describable,ABC):中。这允许查询处理拒绝在project_id上没有条件或没有时间范围的查询。https://github.com/getsentry/snuba/blob/master/snuba/datasets/entity.py#L46-L47逻辑查询处理器(LogicalQueryProcessors)查询处理器是接收查询对象(及其AST)和就地转换。这是为逻辑处理器实现的接口。在逻辑阶段,每个实体提供按顺序应用的查询处理器。常见用例是像apdex这样的自定义函数,或者像时间序列处理器这样的计时。apdex:https://github.com/getsentry/snuba/blob/10b747da57d7d833374984d5eb31151393577911/snuba/query/processors/performance_expressions.py#L12-L20时间序列处理器:https://github.com/getsentry/snuba/blob/master/snuba/query/processors/timeseries_processor.py查询处理器不应该依赖于之前或之后执行的其他处理器,并且应该相互独立。存储选择器(StorageSelector)正如Snuba数据模型中描述的那样,每个实体都可以定义多个存储。多个存储代表多个表,并且可以出于性能原因定义物化视图,因为某些视图可以更快地响应某些查询。在逻辑处理阶段(完全基于实体)结束时,存储选择器可以检查查询并为查询选择合适的存储。存储选择器在实体数据模型中定义并实现此接口。一个例子是Errors实体,它有两个存储,一个用于一致的查询(它们被路由到写入事件的相同节点),另一个只包含我们没有写入的副本来服务于大多数查询。这减少了我们写入的节点上的负载。https://github.com/getsentry/snuba/blob/master/snuba/datasets/storage.py#L155-L165查询翻译器(QueryTranslator)不同的存储有不同的schema(这些反映了clickhouse表或viewschema)。它们通常都不同于实体模型,最值得注意的是标签tags[abc]的可订阅表达式,它在clickhouse中不存在,其中访问标签看起来像tags.values[indexOf(tags.key,'abc')]。选择存储后,需要将查询转换为物理查询。Translator是一个基于规则的系统,其中规则由实体(每个存储)定义并按顺序应用。与查询处理器不同,翻译规则没有查询的完整上下文,只能翻译单个表达式。这使我们能够轻松编写翻译规则并跨实体重用它们。这些是交易实体的转换规则。https://github.com/getsentry/snuba/blob/master/snuba/datasets/entities/transactions.py#L33-L81物理查询处理器(PhysicalQueryProcessors)与逻辑查询处理器相比,物理查询处理器的工作方式非常类似的方式。它们的接口非常相似并且具有相同的语义。不同之处在于它们对物理查询进行操作,因此,它们主要是为优化而设计的。例如,该处理器在标签上找到相等条件,并用标签哈希图(使用Bloom过滤器索引)上的等效条件替换它们,从而使过滤操作更快。https://github.com/getsentry/snuba/blob/master/snuba/query/processors/mapping_optimizer.pyQuerySplitter(查询拆分器)将某些查询拆分为多个单独的Clickhouse查询并组合每个查询的结果,某些查询可以以优化的方式执行。两个例子是时间拆分和列拆分。两者都在下面的文件中。https://github.com/getsentry/snuba/blob/master/snuba/web/split.py时间拆分(Timesplitting)将一个查询(没有聚合并且被正确排序)在一个可变的时间范围内拆分成多个查询,当获得足够的结果时,时间范围会变大并停止顺序执行。列拆分(Columnsplitting)拆分过滤和列获取。它对最小列数执行查询的过滤部分,以便Clickhouse加载更少的列,然后通过第二个查询只为第一个查询过滤的行获取缺失的列。查询格式化程序(QueryFormatter)该组件只是简单地将查询格式化为Clickhouse查询字符串。复合查询处理上面的讨论仅适用于简单查询、复合查询(连接和包含子查询的查询遵循稍微不同的路径)。上面讨论的简单查询管道不适用于连接查询或包含子查询的查询。为了使这项工作有效,每个步骤都必须考虑连接查询和子查询,这增加了流程的复杂性。为了解决这个问题,我们将每个连接查询转换为多个简单子查询的连接。每个子查询都是一个简单的查询,可以通过上述管道进行处理。这也是运行Clickhouse连接的首选方式,因为它允许我们在连接之前应用过滤器。此类查询的查询处理管道包括与上述相关的几个附加步骤。子查询生成器(SubqueryGenerator)该组件采用简单的SnQL连接查询,并为连接中的每个表创建一个子查询。表达式下推上一步中生成的查询将是一个有效的连接,但效率极低。这一步基本上是一个连接优化器,它将所有可以成为子查询一部分的表达式下推到子查询中。这是独立于子查询处理的必要步骤,因为Clickhouse连接引擎不执行任何表达式下推,因此由Snuba来优化查询。简单查询处理管道(SimpleQueryProcessingPipeline)这与上面讨论的从逻辑查询验证到物理查询处理器的管道是一样的。连接优化在处理结束时,我们可以对整个复合查询应用一些优化,例如将连接转换为半连接。
