Linux 下多线程和多进程程序的优缺点,各自适合什么样的业务场景?
2024-04-15 00:00:33  阅读数 517

简单说,对于需要资源隔离的场景,多进程能解决,但多线程无法解决,在这里,讲一个我们的小故事,先说下背景:

我是 Terark 和 Topling 的创始人,ToplingDB(兼容 RocksDB)是我们的核心产品。

ToplingDB 的一个重要功能是 分布式 Compact,去年我们实现了 托管 Todis 的 分布式 Compact 支持。最近我们正在实现 MySQL 的 分布式 Compact,我们通过 Facebook 的 MyRocks 来实现 基于 ToplingDB 的 MySQL,内部名称 MyTopling。

在我们的分布式 Compact 架构 上,MySQL 实例与 Compact Worker(后面简称 Worker)是多对多的,用白话说就是同一个 Worker 可以服务来自多个不同的 MySQL 实例的 Compact 任务,同一个 MySQL 实例的多个 Compact 任务也会由多个不同的 Worker 执行。


这个架构在 MySQL 实例这边没啥问题,但是在 Worker 端就有问题了,因为 Worker 是多线程架构,其实这个架构本来没啥问题,在 Todis 中,它就工作得很好。真正问题在于:MyRocks 使用了大量的全局变量(全局数据字典、统计信息等等),而我们的分布式 Compact 直接复用了 MyRocks 的代码,这样就导致,当同一个 Worker 服务不同 MyRocks 实例的 Compact 任务时,全局变量就会冲突混淆

要解决这个问题,我们要么修改 MyRocks 代码,把全局变量都消除掉,要么在架构上支持 MyRocks 这样 不太规范 的应用。我们选择了后者,这就需要将多线程模型改成多进程模型。为了最小化工作量,我们使用了“微创手术”修改法,主要包括以下修改:

在 Worker 程序中,把执行 Compact 的代码,包在新 fork 出来的子进程中

实现中碰到一个小问题,子进程在执行 ::exit 时,陷入了死锁,最后直接通过 ::_exit 解决掉

关于 ::exit 和 ::_exit,备战面试的同学们需要仔细了解下

在 ToplingZipTable 中,分离出一个 ZipServer 进程,执行 Topling 压缩算法

在 Topling PA-Zip 算法中,数据的压缩在一个多线程 Pipeline 中执行,并且通过一些技巧来最大化 CPU L2/L3 的利用率,跟不使用 Pipeline 相比,性能提高了 40% 以上

在多线程架构中,Compact 线程将每条原始 Value 提交到 Pipeline 的队列

在多进程架构中,Compact 线程将保存了所有原始 Value 的文件(通过本地 Http 调用)提交给 ZipServer 进程,在 ZipServer 进程内部,执行原先多线程架构中执行的压缩计算,压缩结束后 Http 调用返回给客户端(Compact 线程)

在具体实现上,使用本地 127.0.0.1 http 服务,通过 json 传递元数据,通过文件传递大块数据,中等长度和较短的二进制数据,通过 process_vm_readv/writev 来传递。

为了最小化对外依赖,http server 我们继续使用(内嵌的)civetweb,http client 使用 libcurl,期间遭遇了一个 libcurl 的 Expect: 100-continue 问题,这充分说明,即便是象 libcurl 这样千锤百炼的基础组件,也得防范它们的“天坑”

最后,我们得到了一个惊喜:在 MySQL 的分布式 Compact 开发完成之前,我们使用 Todis 来测试多进程模型(使用 wikipedia 数据),发现 Worker 的吞吐率竟然提升了 30%,在双路至强 E5-2682(共32核64线程)上,单 Worker 结点的 Compact 吞吐率达到 900MB/sec,而多线程模型中吞吐率不到 700MB/sec。我们猜测,可能是因为多进程中锁的冲突降低,同时因为资源隔离,操作系统也可以进行更好的调度!