在Yandex中的持续集成。 第二部分

在上一篇文章中,我们讨论了使用基于主干的开发方法以及用于组装,测试,部署和监视的统一系统将开发转移到单个存储库,有关连续集成系统必须解决哪些任务才能在这种条件下有效地工作。


今天,我们将向Habr读者介绍持续集成系统的设备。


图片


持续集成系统必须可靠且快速地工作。 系统应快速响应传入的事件,并且不应在向用户传递测试运行结果的过程中引入额外的延迟。 组装和测试的结果必须实时交付给用户。


连续集成系统是具有最小延迟的流数据处理系统。


在某个阶段(配置,构建,样式,小型测试,中型测试等)发送所有结果后,构建系统将其发送给持续集成系统(“关闭”阶段),然后用户看到该检查结果并在这个阶段,所有结果都是已知的。 每个阶段均独立关闭。 用户更快地收到有用的信号。 关闭所有阶段后,检查被视为完成。


为了实现该系统,我们选择了Kappa体系结构。 该系统包含2个子系统:


  • 事件和数据处理在实时电路中进行。 任何输入数据都被视为数据流(流)。 首先,将事件记录在流中,然后才对其进行处理。
  • 数据处理的结果不断写入数据库,然后通过API进行调用。 在Kappa体系结构中,这称为服务层。

所有数据修改请求都必须经过实时电路,因为您始终需要了解系统的当前状态。 读取请求仅进入数据库。




只要有可能,我们都会遵循仅附加规则。 除删除旧的不必要数据外,不对对象进行任何修改或删除。


每天有超过2 Tb的原始数据通过该服务。


优点:


  • 流包含所有事件和消息。 我们始终可以了解发生的情况和时间。 流可以看作是一个大日志。
  • 高效率和最小的开销。 事实证明,这是一个完全面向事件的系统,没有任何轮询损失。 没有事件-我们没有做任何额外的事情。
  • 应用程序代码实际上不处理线程同步的原语和线程之间共享的内存。 这使系统更加可靠。
  • 处理器之间相互隔离良好,因为 不要直接互动,只能通过流互动。 可以提供良好的测试覆盖率。

但是流数据处理不是那么简单:


  • 需要对计算模型有很好的了解。 您将不得不重新考虑现有的数据处理算法。 并非所有算法都会立即落入流模型中,因此您必须稍微砸头。
  • 有必要保证事件的接收和处理顺序得以保留。
  • 您需要能够处理相互关联的事件,即 在处理新消息时可以快速访问所有必要的数据。
  • 您还需要能够处理重复的事件。

流处理


在进行该项目时,编写了流处理器库,这有助于我们在生产中快速实现和启动流数据处理算法。


流处理器是用于构建流数据处理系统的库。 流是可能无限循环的数据(消息)序列,只能在其中添加新消息;已经记录的消息不会更改,也不会从流中删除。 一个流到另一个流(流处理器)的转换器在功能上包括三个部分:传入消息提供程序,通常从一个或多个流中读取消息并将其放入处理队列中;消息处理器将传入消息转换成传出消息并将它们放入队列中到记录,再到记录器,其中在时间窗口内分组的传出消息将落入输出流。 由一个流处理器生成的数据消息可以在以后由其他流处理器使用。 因此,流和处理器形成有向图,在其中可能存在循环,特别是,流处理器甚至可以从接收数据的同一流中生成消息。


确保与输入流相关的每个处理器至少处理一次输入流的每个消息(语义至少一次)。 还保证所有消息将按照它们到达此流的顺序进行处理。 为此,流处理器分布在所有工作的服务节点上,因此,每个注册的处理器一次最多只能运行一个实例。


处理相关事件是构建用于流数据处理的系统中的主要问题之一。 通常,在流式传输消息时,流处理器将逐步创建一个在处理当前消息时有效的特定状态。 这种状态对象通常不与整个流整体相关,而是与消息的某个子集相关联,该消息子集由该流中的键值确定。 有效的财富储存是成功的关键。 在处理下一条消息时,对于处理器来说,重要的是要能够迅速获得此状态,并基于该状态和当前消息来生成传出消息。 这些状态对象可供位于内存中的L1 LRU缓存中的处理器访问(请不要与CPU缓存混淆)。 如果L1高速缓存中没有状态,则从位于同一存储区中的L2高速缓存恢复该状态,该存储区中存储有流,并且在处理器运行期间定期存储了该流。 如果L2高速缓存中没有状态,则将其从原始流消息中还原,就好像处理器已处理了与当前消息密钥关联的所有原始消息一样。 缓存技术还可以解决存储延迟高的问题,因为顺序处理通常不取决于服务器的性能,而是取决于与数据仓库通信时请求和响应的延迟。




为了有效地将数据存储在L1高速缓存中并将消息数据存储在内存中,除了使用内存高效的结构外,我们还使用对象池,这些对象池允许您在内存中仅保留一个对象(甚至对象的一部分)的一个副本。 此技术已在JDK中用于字符串嵌入字符串,并且类似地扩展到了其他类型的对象,它们应该是不变的。


为了将数据紧凑地存储在流存储中,一些数据在写入流之前被规范化,即 变成数字。 然后可以将有效的压缩算法应用于数字(对象标识符)。 对数字进行排序,对增量进行计数,然后使用ZigZag编码进行编码 ,然后由存档程序进行压缩。 对于流数据处理系统,规范化不是一种非常标准的技术。 但是,这种压缩技术非常有效,并且负载最大的流中的数据量减少了约1000倍。




对于每个流和处理器,我们跟踪消息处理生命周期:输入流中新消息的外观,未处理消息队列的大小,写入结果流的队列大小,消息处理时间以及消息处理阶段的时间分布:




数据仓库


流数据处理的结果应尽快提供给用户。 流中已处理的数据应连续记录在数据库中,然后您可以在其中查找数据(例如,显示包含测试结果的报告,显示测试的历史记录)。


存储的数据和查询的特征。
大多数数据是测试运行。 在一个多月的时间里,启动了超过15亿次构建和测试。 每次启动都会存储大量信息:错误的结果和类型,错误的简短描述(摘要),指向日志的多个链接,测试持续时间,一组数值,度量值,格式名称=值等。 其中一些数据(例如度量和持续时间)很难压缩,因为实际上它是随机值。 另一部分-例如结果,错误类型,日志-可以更有效地保存,因为在同一次测试中,每次运行几乎都不会改变它们。


以前,我们使用MySQL来存储处理后的数据。 我们逐渐开始反对数据库的功能:


  • 每六个月处理的数据量增加一倍。
  • 我们只能存储最近两个月的数据,但是我们希望存储至少一年的数据。
  • 一些繁重(接近分析)查询的执行速度问题。
  • 复杂的数据库架构。 许多表(规范化),这使写入数据库变得复杂。 基本方案与实时电路中使用的对象方案非常不同。
  • 没有遇到服务器关闭。 单独服务器的故障或数据中心的关闭可能导致系统故障。
  • 相当复杂的操作。

作为新数据仓库的候选者,我们考虑了几种选择:PostgreSQL,MongoDB和包括ClickHouse在内的几种内部解决方案。


某些解决方案不允许我们比旧的基于MySQL的解决方案更有效地存储数据。 其他人则不允许执行快速而复杂的(几乎是分析性的)查询。 例如,我们有一个非常繁重的请求,其中显示了影响特定项目的提交(某些测试集)。 在所有无法执行快速SQL查询的情况下,我们都不得不迫使用户等待很长时间或预先进行一些计算而失去灵活性。 如果您提前计算,那么您需要编写更多代码,同时失去灵活性-无法快速更改行为并重新计算任何内容。 编写一个SQL查询将返回用户所需的数据,并且如果您想更改系统的行为能够快速对其进行修改,则更加方便快捷。


Clickhouse


我们选择了ClickHouse 。 ClickHouse是用于联机分析查询处理(OLAP)的列式数据库管理系统(DBMS)。


切换到ClickHouse,我们特意放弃了其他DBMS提供的一些机会,以非常快速的分析查询和紧凑的数据仓库的形式获得了不菲的补偿。


在关系型DBMS中,与一行相关的值在物理上并排存储。 在ClickHouse中,来自不同列的值分开存储,并且来自一列的数据一起存储。 此数据存储顺序使您可以通过正确选择主键来提供高度的数据压缩。 它还影响DBMS在哪种情况下可以正常工作。 ClickHouse与查询效果更好,其中读取的列较少,查询使用一个大表,而其余表很小。 但是,即使在非分析查询中,ClickHouse也可以显示出良好的结果。


表中的数据按主键排序。 排序在后台执行。 这使您可以创建少量的稀疏索引,从而使您可以快速查找数据。 ClickHouse没有二级索引。 严格来说,有一个二级索引-分区键(ClickHouse会在请求中指定了分区键的地方切断分区数据)。 更多细节


具有规范化的数据方案不起作用,相反,最好根据对数据的请求对数据进行非规范化。 最好创建具有​​大量列的“宽”表。 该项目也与上一个项目有关,因为有时没有辅助索引会使用不同的主键来复制表。


ClickHouse没有经典意义上的UPDATE和DELETE,但是可以模拟它们。


数据需要大块插入,而不是太频繁地插入(每隔几秒钟一次)。 在实际数据量上,逐行数据加载实际上是无效的。


ClickHouse不支持交易;系统最终变得一致


但是,与其他DBMS相似,ClickHouse的某些功能使将现有系统转移到其中变得更加容易。


  • ClickHouse使用SQL,但有细微差别,对于OLAP系统的典型查询很有用。 有强大的聚合函数系统,ALL / ANY JOIN,函数中的lambda表达式以及其他SQL扩展,可让您编写几乎任何分析查询。
  • ClickHouse支持复制, 仲裁记录 ,仲裁读取。 仲裁写入对于可靠的数据存储是必需的:只有ClickHouse能够将数据写入给定数量的副本而不会出错,INSERT才会成功。

您可以在文档中阅读有关ClickHouse功能的更多信息。


使用ClickHouse的功能


选择主键和分区键。


如何选择主键和分区键? 也许这是创建新表时出现的第一个问题。 主键和分区键的选择通常由将对数据执行的查询决定。 同时,使用这两种条件的查询被证明是最有效的:通过主键和分区键。


在我们的例子中,主表是运行测试的矩阵。 逻辑上假设使用此数据结构时,必须选择键,以便其中一个键的旁路顺序以增加行号的顺序进行,而另一个键的旁路顺序以增加列号的顺序进行。


同样重要的是要记住,主键的选择会极大地影响数据存储的紧凑性,因为其他列中主键的旁路中的相同值几乎不会占用表中的空间。 因此,例如,在我们的案例中,测试的状态在提交之间几乎没有变化。 这个事实从本质上预先确定了主键的选择-一对测试标识符和提交编号。 而且,按此顺序。




分区键有两个用途。 一方面,由于分区中的数据已经过时,因此它可以使分区“归档”,从而可以将其从存储中永久删除。 另一方面,分区键是辅助索引,这意味着如果它们中存在表达式,它可以使您加快查询速度。


对于我们的矩阵,选择提交号作为分区键似乎很自然。 但是,如果您在分区键的表达式中设置修订值,则该表中将存在许多不合理的分区,这将降低对该表的查询性能。 因此,在分区键的表达式中,可以将修订值划分为一些较大的数字以减少分区数,例如,PARTITION BY intDiv(修订版,2000)。 此数字应足够大,以使分区数不会超过建议值,而该数字应足够小,以使没有太多数据落入一个分区,并且数据库不必读取太多数据。


如何实现UPDATE和DELETE?


通常,ClickHouse不支持UPDATE和DELETE。 但是,您可以在表中添加带有版本的列,而不是UPDATE和DELETE,并使用特殊的ReplacingMergeTree引擎(删除具有相同主键值的重复记录)。 在某些情况下,版本自然会从一开始就存在于表中:例如,如果我们要为测试的当前状态创建表,则该表中的版本将是提交编号。


CREATE TABLE current_tests ( test_id UInt64, value Nullable(String), version UInt64 ) ENGINE = ReplacingMergeTree(version) ORDER BY test_id 

在记录更改的情况下,我们为版本添加一个新值;在删除情况下,我们添加一个具有NULL值(或在数据中找不到的其他特殊值)的版本。


您通过新存储实现了什么?


转换为ClickHouse的主要目标之一是能够长时间存储测试历史记录(几年,或者在最坏的情况下至少一年)。 在原型阶段已经很明显,我们可以解决服务器中现有的SSD,以存储至少三年的历史。 分析查询已大大加快,现在我们可以从数据中提取更多有用的信息。 RPS保证金增加了。 此外,通过向ClickHouse群集添加新服务器,几乎可以线性缩放此值。 对于最终用户来说,向ClickHouse数据库创建新的数据仓库仅是一个引人注目的步骤,因为它具有存储和处理大量数据的能力,从而增加了新功能,加速并简化了开发工作,这对最终用户而言几乎是一步之遥。


来找我们


我们的部门正在不断扩大。 如果您想从事复杂而有趣的任务和算法,请访问我们 。 如果您有任何疑问,可以直接在PM中问我。


有用的链接


流处理



Kappa体系结构



ClickHouse:


Source: https://habr.com/ru/post/zh-CN429956/


All Articles