FunCorp是一家国际性娱乐App开发商,知名App iFunny就是由FunCorp出品的。iFunny是一款非常有趣的图片和GIF App,用户可以用它来打发时间,比如看模因、漫画、有趣的图片、宠物GIF等,也可以上传和分享自己的内容。iFunny一直使用Redshift作为后端服务和移动App的事件存储数据库。他们之所以选择Redshift,是因为当时从成本和便利性方面来看确实没有更好的选择。然而,ClickHouse的发布改变了这种状况。他们对它进行了一番彻底的研究,对成本和可能的架构作了一番比较。去年夏天,他们终于决定要尝试一下,看看是否可以使用它。这篇文章将介绍Redshift曾经帮助他们解决的挑战,以及他们如何将这个解决方案迁移到ClickHouse。
我们面临的挑战
iFunny需要类似Yandex.Metrika那样的服务,但要求专门为内部使用而设计。让我解释一下原因。
外部客户端——包括移动App、网站或内部后端服务——会创建很多事件。如果后端服务出现问题,我们很难向这些客户端说明事件处理服务当前不可用,让它们“在15分钟或者一小时后再试一次”。我们有很多客户端,它们持续不断地发送事件消息,一丁点都不想等待。
当然,我们的一些内部服务和用户在这方面却相当宽容:即使分析服务不可用,它们也能够正常运行。大多数产品指标和A/B测试每天只需要检查一次,甚至可能更少。这就是为什么读取吞吐量会很低。如果发生故障或进行协调更新,数据库有可能在几个小时内(最坏的情况下可能是几天)不可用或无法保持一致。
从数字方面来看:我们每天需要接收大约50亿个事件(300 GB的压缩数据),同时将数据保存在热存储中三个月,以便为SQL查询提供服务。我们还需要将数据保存在冷存储中几年或更长时间,同时能够在短短几天内将数据恢复到热存储中。
通常,我们的数据是按时间排序的事件集。大约有三百种类型的事件,每种事件都有自己的一组属性。我们还有一些来自外部源(例如MongoDB或外部AppsFlyer服务)的数据需要与分析数据库同步。
最终我们需要为数据库提供大约40 TB的磁盘空间,以及大约250 TB的磁盘空间用于冷存储。
基于Redshift的解决方案
我们有发送事件的移动客户端和后端服务。HTTP服务接收数据,执行基本的验证,按照分钟将事件分组到文件中,保存在本地磁盘上,然后压缩并发送到S3存储。这个服务的可用性取决于应用程序服务器和AWS S3的可用性。应用程序是无状态的,所以容易进行负载均衡、缩放和替换。S3是一个相当简单的文件存储服务,具有良好的声誉和高可用性,我们认为完全可以信任它。
然后我们需要以某种方式将数据发送到Redshift。这部分相对简单:我们使用内置的S3导入器将数据上传到Redshift。每10分钟执行一次脚本,脚本连接到Redshift,并要求接收以s3://events-bucket/main/year=2018/month=10/day=14/10_3*为前缀的请求数据。
我们使用Apache Airflow来跟踪任务上传状态,可以在发生错误时重启任务,而且它提供了易于读取的任务执行日志,如果任务数量很大,那么这些日志就非常有用。如果我们出现任何问题,可以重复上传特定时间间隔的数据,或者上传S3存储中的冷数据,最早可以追溯到一年前。
Airflow还提供了一些调度脚本,可连接到数据库,并定期从外部存储上传数据或使用类似INSERT INTO…SELECT…的操作进行事件聚合。
Redshift的可用性保证很差。AWS每周一次停止集群进行更新或计划维护,每次最长可达半小时。当一个节点离线时,集群也将变为不可用,直到离线的主机重新上线。这通常需要约15分钟,这种情况大约每年发生两次。这对当前系统来说这不是问题,因为我们从一开始就知道数据库会定期不可用。
我们为Redshift使用了4个ds2.8xlarge实例(36核CPU、16个TB硬盘),总磁盘空间为64 TB。
最后一个问题是备份。备份计划可以在集群中设置,运行得非常顺利。
为什么要迁移到ClickHouse
当然,如果没有遇到任何问题,没有人会想到要迁移到ClickHouse。我们也不例外。
在将ClickHouse的存储模式与Redshift进行比较时,我们发现它们的结构非常相似。这两个数据库都是基于列的,它们都擅长处理大量的列和压缩磁盘上的数据(在Redshift中,你甚至可以为每个列配置压缩类型)。甚至数据的存储方式都可以是一样的:它们按照主键排序,因此在读取时只读取特定的块,并且不需要在内存中保留特定的索引,这在处理大量数据时起到至关重要的作用。
但是,魔鬼存在于细节之中。
一天一张表
当运行VACUUM命令时,数据会在磁盘上排序,并从Redshift中删除。
vacuum过程适用于表中的所有数据。但是,如果三个月的数据都在一个表中,完成这个过程需要很长时间,而我们需要每天至少运行一次,因为在删除旧数据的同时也会添加新数据。我们必须每天创建一张特殊的表,并使用视图合并它们,这不仅使视图轮换和支持变得更加复杂,还减慢了查询速度。“explain”显示,在执行查询时会扫描所有表,即使扫描一张表只需要不到一秒钟的时间,但当我们有90张这样的表时,每个查询至少需要一分钟才能执行完。
数据重复
第二个问题是数据重复问题。在线传输数据总会出现两个问题:数据丢失或数据重复。我们不允许丢失消息,所以只能接受出现一小部分重复数据。为了给日数据表去重,我们先创建新表,将旧表中具有唯一ID的数据(使用窗口函数删除具有相同ID的行)插入到新表中,然后删除旧表并重命名新表。因为视图是从日数据表派生出来的,在重命名表时需要将它们删除。我们还要注意锁,如果收到一个锁定视图或某个表的查询,将会导致这个过程变长。
监控和维护
针对Redshift的查询几乎没有几个是少于几秒的。即使你只是想添加用户或列出活跃查询的清单,也需要等待上几十秒。当然,我们习惯了等待,对于这类数据库来说,这样的延迟是可接受的,但不管怎样,我们还是浪费了很多时间。
成本
根据我们的计算,在具有相同资源的AWS实例上部署ClickHouse只需要一半的成本。这是有道理的,因为Redshift是一个开箱即用的数据库。你只需要在AWS控制台中单击几下,然后使用PostgreSQL客户端连接数据库,剩下的事情就交给AWS。但这些真的值那么些钱吗?我们已经拥有了基础设施,并且知道如何进行备份、监控和配置内部服务。既然这样,为什么不使用СlickHouse呢?
迁移
最初,我们部署了一个小型的ClickHouse:只有一台机器。我们使用内置工具定期从S3导入数据。因此,我们能够测试我们对ClickHouse性能和功能的假设。在花了几个星期测试一个小数据副本之后,我们找到了在用Clickhouse完全替换Redshift之前需要解决的一些问题:
应该使用哪些类型的实例和磁盘进行部署?
需要进行复制吗?
如何安装、配置和启动它?
如何监控?
应该使用什么样的schema?
如何从S3将数据发送给它?
如何将所有的标准SQL重写为非标准SQL?
实例和磁盘类型。我们决定使用当前安装的Redshift作为参考来确定CPU的核心数量、磁盘数量和内存。我们有几种选择,包括带有本地NVMe磁盘的i3实例,但最终我们选择了r5.4xlarge和8T ST1 EBS存储。根据我们的估计,这样的性能可能与Redshift相当,而价格只有原来的一半。有了EBS磁盘,我们就可以通过磁盘快照进行简单的备份和恢复,这与Redshift几乎相同。
复制。因为是将现有的Redshift配置作为参考,所以我们决定不使用复制。这样做的另一个好处是我们不需要去学习ZooKeeper,我们的基础设施中本来就没有这项服务(但我们现在可以在需要时进行复制)。
安装。这是最简单的部分,我们只需要用一个Ansible角色来安装RPM软件包,并在每台主机上使用相同的配置。
监控。我们使用Prometheus、Telegraf和Grafana来监控所有的服务。我们将Telegraf代理安装到ClickHouse主机上,并准备了一个Grafana仪表盘,用来显示服务器CPU、内存和磁盘的工作负载。我们使用了一个Grafana插件来显示对当前集群的查询情况、从S3导入数据的状态以及其他有用的东西。效果比AWS控制台提供的仪表盘更好,信息更丰富,速度更快!
schema。我们在Redshift中犯的主要错误之一是将主要事件字段保存在单独的列中,然后将其他很少用到的列合并成一个叫作properties的列。事实上,在早期我们还不了解所收集的事件以及它们具有哪些属性(它们可能每天变5次)时,我们能够灵活地编辑字段。但另一方面,对properties列查询所花费的时间越来越长。在ClickHouse中,我们决定从一开始就把事情做对,我们采用了所有可能的列,并为它们指定最合适的类型。最终,我们得到了一张包含约两千个列的表。
下一步是为存储和分区选择合适的引擎。
至于分区,我们决定不重新发明轮子:我们重用了在Redshift中使用的方法,即每天创建一个分区,只是现在所有分区都存储在一个表中,这大大加快了查询速度并简化了维护工作。我们选择了ReplacingMergeTree引擎,因为我们可以使用OPTIMIZE…FINAL命令从特定分区中删除重复数据。此外,因为使用了日分区模型,我们只处理一天的数据,如果发生错误或故障,比处理一个月的数据要快得多。
将数据从S3传输到ClickHouse。这是最耗时的过程之一,因为我们无法使用ClickHouse的内置工具上传数据。S3以JSON格式存储数据,因此必须通过jsonpath提取每个字段,有时候甚至不得不进行转换:例如,将UUID从标准格式DD96C92F-3F4D-44C6-BCD3-E25EB26389E9转换为字节,并放入FixedString(16位)。
我们想要一个类似于Redshift COPY命令那样的特殊服务,但无法找到开箱即用的解决方案,所以我们必须自己开发。要描述这个解决方案可能需要另写一篇文章,所以长话短说,它就是一个HTTP服务,部署在与ClickHouse相同的主机上。在查询参数中,我们指定了S3前缀(表示应该从哪里获取文件)、jsonpath列表(用于将JSON转换为一组列),以及每列的转换集。接收查询的服务器从S3扫描文件,并将解析任务分配给其他主机。我们需要将无法导入的行保存到单独的СlickHouse表中,并记录错误消息,这个非常重要。这对于诊断事件处理服务和生成这些事件的客户端来说非常有用。当我们直接将导入器部署在数据库主机上时,我们利用了空闲的资源,因为它们不会全天候接收到复杂的查询。如果查询数量增加,我们还可以将导入器服务移动到独立主机。
从外部导入数据对我们来说并不是个大问题。我们在已有的脚本中将目的地从Redshift更改为ClickHouse。
还有一种办法,就是将MongoDB作为字典,而不是每日进行复制。可惜的是,这种方式并不适合我们,因为字典必须始终保存在内存中,而大多数MongoDB集合的大小不允许这么做。不过,我们仍然使用了字典,因为从MaxMind连接GeoIP数据库非常方便,而且在查询时也非常有用。为此,我们使用服务提供的ip_trie布局和CSV文件。例如,geoip_asn_blocks_ipv4字典的配置像下面这样:
\u0026lt;dictionaries\u0026gt; \u0026lt;dictionary\u0026gt; \u0026lt;name\u0026gt;geoip_asn_blocks_ipv4\u0026lt;/name\u0026gt; \u0026lt;source\u0026gt; \u0026lt;file\u0026gt; \u0026lt;path\u0026gt;GeoLite2-ASN-Blocks-IPv4.csv\u0026lt;/path\u0026gt; \u0026lt;format\u0026gt;CSVWithNames\u0026lt;/format\u0026gt; \u0026lt;/file\u0026gt; \u0026lt;/source\u0026gt; \u0026lt;lifetime\u0026gt;300\u0026lt;/lifetime\u0026gt; \u0026lt;layout\u0026gt; \u0026lt;ip_trie /\u0026gt; \u0026lt;/layout\u0026gt; \u0026lt;structure\u0026gt; \u0026lt;key\u0026gt; \u0026lt;attribute\u0026gt; \u0026lt;name\u0026gt;prefix\u0026lt;/name\u0026gt; \u0026lt;type\u0026gt;String\u0026lt;/type\u0026gt; \u0026lt;/attribute\u0026gt; \u0026lt;/key\u0026gt; \u0026lt;attribute\u0026gt; \u0026lt;name\u0026gt;autonomous_system_number\u0026lt;/name\u0026gt; \u0026lt;type\u0026gt;UInt32\u0026lt;/type\u0026gt; \u0026lt;null_value\u0026gt;0\u0026lt;/null_value\u0026gt; \u0026lt;/attribute\u0026gt; \u0026lt;attribute\u0026gt; \u0026lt;name\u0026gt;autonomous_system_organization\u0026lt;/name\u0026gt; \u0026lt;type\u0026gt;String\u0026lt;/type\u0026gt; \u0026lt;null_value\u0026gt;?\u0026lt;/null_value\u0026gt; \u0026lt;/attribute\u0026gt; \u0026lt;/structure\u0026gt; \u0026lt;/dictionary\u0026gt;\u0026lt;/dictionaries\u0026gt;
将这个配置放在/etc/clickhouse-server/geoip_asn_blocks_ipv4_dictionary.xml中,你就可以通过IP地址向字典发出请求以获取提供者名称:
SELECT dictGetString('geoip_asn_blocks_ipv4','autonomous_system_organization',tuple(IPv4StringToNum('192.168.1.1')));
更改数据schema。如前所述,我们决定放弃复制,因为我们可以承受因发生故障或进行计划维护导致的不可用,而且S3中已经保存了一个数据副本,可以在合理的时间内恢复到ClickHouse。如果我们不使用复制,就不需要部署ZooKeeper,但少了ZooKeeper就无法在DDL查询中使用ON CLUSTER表达式。我们用一个Python脚本解决了这个问题,这个脚本会连接到任意一台ClickHouse主机(现在只有8台)并执行指定的SQL查询。
ClickHouse对SQL的不完整支持。在我们开发导入器的同时,我们将查询语法从Redshift转换为ClickHouse,这个任务主要是由分析师团队完成的。我们的问题主要不在于JOIN,而在于窗口函数,这看起来可能有点奇怪。我们花了几天时间了解如何使用数组和lambda函数来实现它们。庆幸的是,这个问题在ClickHouse的众多文章中已经得到了解决,例如,https://events.yandex.ru/lib/talks/5420/。当时,我们的数据同时被记录到两个地方,Redshift和新的ClickHouse数据库,因此我们可以在传输查询时对结果进行比较。尽管如此,要比较性能仍然相当困难,因为我们删除了一个属性列,并且大多数查询只使用了实际需要的列。当然,在这种情况下,性能方面的提升还是很容易看得出来的。对于未解决属性列的查询,具有相当或略高的性能。
结果,数据库schema看起来像下面这样:
结果
最后,我们获得了以下这些东西:
一张表,而不是90张;
毫秒级的服务查询;
成本减半;
可以简单删除重复事件。
当然也有一些缺点,但我们为它们做好了应对准备:
如果发生故障,我们必须自己恢复集群;
必须分别在每台主机上执行schema变更;
我们必须自己更新系统。
我们无法对查询速度进行直接比较,因为数据schema发生了很大变化。很多查询的速度得到明显的提升,因为从需要从磁盘读取的数据更少了。
我们花了三个月的时间准备和实现迁移。7月初,两名专家开始参与该项目,并于9月份完成。9月27日,我们关闭了Redshift。从那时起,我们一直在使用ClickHouse。已经过去三个月了,时间不算很长,不过我们确实没有遇到会导致整个集群离线的数据丢失或严重错误。我们期待升级到新版本!
英文原文:https://www.altinity.com/blog/migrating-from-redshift-to-clickhouse