DoubleCloud 即将停止运营。立即迁移到 ClickHouse,享受限时免费迁移服务。立即联系我们。 ->->

博客 / 工程

在安卓手机上运行 ClickHouse

author avatar
Alexander Kuzmenkov
2020 年 7 月 16 日

本文简要描述了我在安卓上构建 ClickHouse 的实验。如果您是第一次听说 ClickHouse,它是一款令人惊讶地快速的列式 SQL DBMS,用于实时报表。它通常用于 AdTech 等领域,部署在数百台机器的集群上,存储高达 PB 级的数据。但 ClickHouse 在更小的规模上也易于使用 - 您的笔记本电脑就可以,而且不要惊讶,您可以在这种硬件上每秒处理数 GB 的数据。现在,另一种小型但功能强大的平台无处不在 - 智能手机。因此,结论不可避免:您也应该能够在智能手机上运行 ClickHouse。我忍不住要嘲笑用几十部手机建立高性能移动 OLAP 集群的想法。或者看到可爱的 OLED 屏幕上出现怀旧的“段错误 (核心已转储)”,但我偏离了主题。让我们开始吧。

第一次廉价尝试

我听说过安卓使用 Linux 内核,我可以用 Termux 运行熟悉的类 Unix shell 和工具。ClickHouse 已经支持 ARM 平台,甚至发布了为 64 位 ARM 构建的二进制文件。这个二进制文件也没有很多依赖项 - 只有相当旧版本的 glibc。也许我可以从 CI 下载 ClickHouse 二进制文件到手机上运行它?

事实证明并没有那么简单。

  • 尝试运行后,我们会看到的第一个问题是荒谬的错误消息:./clickhouse: file is not found。但它就在那里!strace 帮了忙:找不到的是 /lib64/ld-linux-x86-64.so.2,这是 ClickHouse 二进制文件中指定的链接器。链接器在这种情况下是一个系统程序,它会在将控制权传递给应用程序之前,首先加载应用程序二进制文件及其依赖项。安卓使用不同的链接器,位于另一个路径,这就是我们收到错误的原因。如果我们显式调用链接器,例如 /system/bin/linker64 $(readlink -f ./clickhouse),可以克服这个问题。

  • 我们立即遇到了另一个问题:链接器抱怨二进制文件的类型错误 ET_EXEC。这是什么意思?安卓二进制文件必须支持动态重定位,以便它们可以加载到任何地址,可能是为了 ASLR 目的。ClickHouse 二进制文件通常不使用位置无关代码,因为我们已经测量到它会带来约 1% 的轻微性能损失。在调整编译和链接标志以尽可能包含 -fPIC 之后,并解决了一些非常奇怪的链接器错误,我们最终得到了一个具有正确类型 ET_DYN 的可重定位二进制文件。

  • 但情况只会更糟。现在它抱怨 TLS 部分偏移错误。在阅读了一些邮件档案后,我几乎无法理解一个字,我得出结论,安卓使用了一些不同的内存布局来表示保存线程局部变量的可执行文件部分,而来自安卓工具链的 clang 已经进行了修补以解决这个问题。之后,我不得不接受我无法使用熟悉的工具,并无奈地转向安卓工具链。

使用安卓工具链

令人惊讶的是,设置起来相当简单。我们的构建系统使用 CMake,并且已经支持交叉编译 - 我们有 CI 配置,用于交叉编译 Mac、AArch64 Linux 和 FreeBSD。安卓 NDK 也与 CMake 集成,并提供了一个 手册,介绍如何设置它。下载安卓 NDK,向您的 cmake 调用中添加一些标志:DCMAKE_TOOLCHAIN_FILE=~/android-ndk-r21d/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=28,就可以了。它(几乎)构建了。这次我们遇到了什么障碍?

  • 我们的 glibc 兼容性层有很多编译错误。它借用了 musl 代码来提供旧版本 glibc 中不存在的函数,以便我们可以在各种发行版上运行相同的二进制文件。由于严重依赖于系统头文件,它遇到了 Linux 和安卓之间各种各样的差异,例如 pthread 支持范围有限,或者只是 API 变体略有不同。感谢我们构建的是特定版本的安卓,因此我们只需禁用它,并直接从系统 libc 中使用所有需要的函数。
  • 一些第三方库和我们的 CMake 文件以各种无法想象的方式损坏。我们只需禁用所有可以禁用的东西,并修复所有无法修复的东西。
  • 我们的一些代码使用 #if defined(__linux__) 来检查 Linux 平台。这并不总是有效,因为安卓也导出 __linux__,但存在一些 API 差异。
  • std::filesystem 在 NDK r21 中仍然没有完全支持。支持将在计划于 2020 年第三季度的 r22 中实现,但我现在就想要它... 好在,我们捆绑了自己的 libcxxlibcxxabi 分支,以减少依赖关系,而且它们足够新,可以完全支持 C++20。启用它们后,一切都正常了。
  • std::map<int> 中出现的奇怪的二十个屏幕错误之类的东西,也可以通过使用我们的 libcxx 来解决。

在设备上

最后,我们得到了一个可以实际运行的二进制文件。将其复制到手机上,chmod +x./clickhouse server --config-path db/config.xml,运行一些查询,它可以工作!

segfault.jpg

看到我喜欢的消息真是太好了。

这里在 Termux 中是完整的开发环境,让我们安装 gdb 并附加到它,看看段错误发生在哪里。运行 gdb clickhouse --ex run '--config-path ....',等待它启动一分钟,结果却看到安卓杀死了 Termux,因为它内存不足。难道说 4 GB 的 RAM 还不够吗?查看 clickhouse 二进制文件,它的尺寸竟然高达 1.1 GB。膨胀的主要原因是,我们的一些计算代码针对特定数据类型进行了高度专门化(主要是通过 C++ 模板),而且我们还静态构建并链接了许多第三方库。二进制文件中不必要的部分是调试符号,它有助于在错误消息中生成良好的堆栈跟踪。我们可以在手机上直接使用 strip -s ./clickhouse 来删除它们,之后大小变得更加合理,大约 400 MB。最后,我们可以运行 gdb 并且发现段错误出现在 unw_backtrace 中。

Thread 60 "ConfigReloader" received signal SIGSEGV, Segmentation fault.                         
[Switching to LWP 21873]                        
0x000000556a73f740 in ?? ()          

(gdb) whe 20                                    
#0  0x000000556a73f740 in ?? ()                 
#1  0x000000556a744028 in ?? ()                 
#2  0x000000556a73e5a0 in ?? ()                 
#3  0x000000556a73d250 in unw_init_local ()     
#4  0x000000556a73deb8 in unw_backtrace ()      
#5  0x0000005562aabb54 in StackTrace::tryCapture() ()                                           
#6  0x0000005562aabb10 in StackTrace::StackTrace() ()                                           
#7  0x0000005562a8d73c in MemoryTracker::alloc(long) ()                                         
#8  0x0000005562a8db38 in MemoryTracker::alloc(long) ()                                         
#9  0x0000005562a8e8bc in CurrentMemoryTracker::alloc(long) ()                                  
#10 0x0000005562a8b88c in operator new[](unsigned long) ()                                      
#11 0x0000005569c35f08 in Poco::XML::NamePool::NamePool(unsigned long) ()                       
...

这个函数是做什么的,为什么我们需要它?在这个特定的堆栈跟踪中,我们遇到了内存不足的问题,即将为此抛出异常。unw_backtrace 函数被调用来为异常消息生成一个堆栈跟踪。但还有一个有趣的调用它的场景。信不信由你,ClickHouse 内置了一个类似 perf 的采样分析器,它可以保存 CPU 时间、实时时间以及内存分配的堆栈跟踪。这些数据保存在 system.trace_log 表中,因此您可以像将 SQL 查询的输出管道到 flamegraph.pl 一样轻松地构建查询的火焰图。这是一个有趣的功能,但现在相关的是它使用与我们已知会导致段错误的相同 unw_backtrace 函数向服务器的所有线程发送信号,以在某个随机时间中断它们并保存其当前堆栈跟踪。我们希望查询分析器在生产环境中使用,因此默认情况下它已启用。禁用它后,我们有一个在 Android 上运行的正常 ClickHouse 服务器。

你的手机够好吗?

使用不同程度合成的数据集和查询来证明你所使用的特定 DBMS 的性能优于其他不太先进的 DBMS,这是一种陈旧的套路。我们已经超越了这一点,而是使用我们喜欢的 DBMS 作为硬件的基准。对于这个基准测试,我们使用了一个来自 Yandex.Metrica 的 1 亿行混淆数据集,大约 12 GB 压缩,以及一些代表 Metrica 仪表板的查询。这里有一个页面,提供了各种云服务器、传统服务器,甚至一些笔记本电脑的众包结果,但手机的对比结果如何?让我们来一探究竟。按照手册将必要的数据下载到手机并运行基准测试非常简单。一个问题是,有些查询无法运行,因为它们使用了太多内存,服务器被 Android 杀掉了,所以我不得不编写一些脚本来绕过这个问题。此外,我不确定如何在 Android 上重置文件系统缓存,因此“冷启动”数据并不准确。结果看起来相当不错。

compare.jpg

我的手机是 Google Pixel 3a,它的平均速度只有我 Dell XPS 15 工作笔记本电脑的五分之一。数据无法放入内存而必须写入磁盘(指的是闪存)的查询明显变慢,最多慢 20 倍,但大多数情况下它们无法完成,因为服务器被杀掉了——它只有大约 3 GB 的可用内存。总的来说,我认为手机的测试结果相当不错。高端机型的性能应该会更高,能够达到与一些小型笔记本电脑相当的性能。

结论

这是一个相当令人愉快的练习。在手机上运行服务器是进行演示的一种好方法,因此我们应该发布一个 ClickHouse 的 Termux 包。为此,我们必须调试并修复 unw_backtrace 的段错误(我希望在添加 -fno-omit-frame-pointer 后它会消失),并修复一些现在只是注释掉的怪癖。Android 构建所需的大多数更改已经合并到我们的主分支中。

构建 Android 版本原来相对简单——所有这些实验和写作大约花了四天时间,这也是我第一次进行任何与 Android 相关的编程。NDK 很容易使用,我们的代码足够跨平台,因此我只需要进行一些小的修改。如果我们没有定期构建 AArch64,并且对 SSE 4.2 或类似的东西存在硬依赖,情况就不同了。

但最重要的收获是,现在您不必再纠结于选择新手机——只需用 ClickHouse 对其进行基准测试。

2020-07-16 Alexander Kuzmenkov

分享此文章

订阅我们的时事通讯

及时了解功能发布、产品路线图、支持和云服务!
加载表单中...
关注我们
Twitter imageSlack imageGitHub image
Telegram imageMeetup imageRss image