博客 / 工程

在安卓手机上运行 ClickHouse

author avatar
Alexander Kuzmenkov
2020 年 7 月 16 日 - 分钟阅读

这是我在安卓上构建 ClickHouse 实验的简要描述。如果您是第一次听说 ClickHouse,它是一个速度惊人的列式 SQL DBMS,用于实时报告。它通常用于广告技术等领域,部署在数百台机器的集群上,存储高达 PB 级的数据。但是 ClickHouse 在较小规模上也易于使用——您的笔记本电脑就可以,如果您能够在这台硬件上每秒处理几 GB 的数据,请不要感到惊讶。还有另一种小规模但功能强大的平台,现在无处不在——智能手机。结论不可避免地是:您必须能够在智能手机上运行 ClickHouse。而且我忍不住对使用十几部手机建立高性能移动 OLAP 集群的想法暗自发笑。或者看到怀旧的 Segmentation fault (core dumped) 出现在可爱的 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 进行交叉编译。Android NDK 也集成了 CMake,并且有一个关于如何设置它的 手册。下载 Android 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 函数。我们期望查询分析器在生产环境中使用,因此默认情况下启用它。禁用它后,我们有一个在安卓上运行的正常工作的 ClickHouse 服务器。

您的手机足够好吗?

存在一种老套的做法,即使用不同程度的合成数据集和查询来证明您正在使用的特定 DBMS 具有优于其他不太先进的 DBMS 的性能。我们已经超越了这一点,而是使用我们喜爱的 DBMS 作为硬件的基准。对于此基准测试,我们使用来自 Yandex.Metrica 的小型 1 亿行混淆数据集(压缩后约 12 GB)以及一些代表 Metrica 仪表板的查询。有一个 此页面 包含各种云和传统服务器甚至一些笔记本电脑的众包结果,但手机的性能如何?让我们找出答案。按照 手册 将必要的数据下载到手机并运行基准测试非常简单。一个问题是一些查询无法运行,因为它们使用了太多内存,并且服务器被安卓杀死,因此我不得不为此编写脚本。此外,我不确定如何在安卓上重置文件系统缓存,因此“冷运行”数据不正确。结果看起来相当不错

compare.jpg

我的手机是 Google Pixel 3a,平均速度仅比我的 Dell XPS 15 工作笔记本电脑慢 5 倍。数据不适合内存并且必须转到磁盘(我的意思是闪存)的查询明显较慢,最多慢 20 倍,但大多数查询无法完成,因为服务器被杀死——它只有大约 3 GB 的可用内存。总的来说,我认为手机的结果看起来相当不错。高端型号应该性能更高,达到与一些小型笔记本电脑相当的性能。

结论

这是一次相当愉快的练习。在手机上运行服务器是进行演示的好方法,因此我们可能应该发布一个用于 ClickHouse 的 Termux 软件包。为此,我们必须调试和修复 unw_backtrace 段错误(我祈祷在添加 -fno-omit-frame-pointer 后它会消失),并修复一些目前只是注释掉的怪癖。安卓构建所需的大部分更改已经合并到我们的主分支中。

事实证明,为安卓构建相对简单——所有这些实验和写作花了我大约四天时间,而且这是我第一次做任何与安卓相关的编程。NDK 易于使用,并且我们的代码具有足够的跨平台性,因此我只需要进行少量修改。如果我们没有例行地为 AArch64 构建并且对 SSE 4.2 或类似的东西有硬依赖,那将是另一回事了。

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

2020-07-16 Alexander Kuzmenkov

分享这篇文章

订阅我们的新闻通讯

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