并发计算对网页开发重要吗?
假设我有一个在 S 台服务器上运行的网页应用,每台服务器平均有 C 个核心。我的应用在任何时刻处理的请求数量大约是 R。假设 R 大约是 S * C 的 10 倍,那么把一个请求的工作分散到多个核心上,收益会不会很小,因为每个核心已经在处理大约 10 个请求了呢?
如果我说的没错,那为什么这个人说并发对 Python 作为网页开发语言的未来如此重要呢?
我能想到很多理由说明我的观点可能不对。比如,应用可能会收到一些特别难处理的请求,而这些请求的数量少于可用的核心数量。或者请求的难度差异很大,可能有一个核心运气不好,连续接到 10 个难处理的请求,结果是其中一些请求的处理时间比正常情况要长得多。考虑到写这篇文章的人比我经验丰富得多,我觉得我可能是错的,但我想知道原因是什么。
5 个回答
注意:我只大致浏览了“并发”这一部分,看起来这就是你提到的内容。问题似乎是(这并不是什么新鲜事):
- 由于GIL(全局解释器锁),Python的线程不能并行运行。
- 如果你的系统有很多核心,实际上你可能需要至少2倍于核心数的线程。
- 现在的系统越来越多核心;普通的个人电脑通常有四个核心,而价格合理的服务器系统可能会有128个或更多核心,这也不远了。
- 如果你运行256个独立的Python进程,那么数据就不会共享;每个进程中都会复制整个应用程序和任何加载的数据,这会导致巨大的内存浪费。
最后一点就是这个逻辑出错的地方。确实,如果你以简单的方式启动256个Python后端,数据是不会共享的。然而,这种做法没有考虑设计,这是启动大量后端进程的错误方式。
正确的做法是先在一个主进程中加载整个应用程序(包括你依赖的所有Python模块等)。然后,这个主进程会分叉出后端进程来处理请求。这些进程虽然是独立的,但由于采用了标准的写时复制(COW)内存管理,所有已经加载的固定数据都会与主进程共享。主进程提前加载的所有代码现在在所有工作进程之间共享,尽管它们都是独立的进程。
(当然,COW的意思是如果你对数据进行写操作,它会创建数据的新副本——但像编译后的Python字节码这样的东西在加载后不应该被改变。)
我不知道是否有与Python相关的问题阻止这种做法,但如果有,那就是需要解决的实现细节。这种方法比试图消除GIL要容易得多。它还消除了传统的锁和线程问题的任何可能性。虽然在某些语言和其他用例中,这些问题并没有那么严重——线程之间几乎没有交互或锁定——但它们并不会完全消失,Python中的竞争条件跟其他语言一样难以追踪。
我觉得这事儿不会很快发生。大多数网页请求的处理时间都不到一秒钟。考虑到这一点,把网页请求的任务拆分开来就没什么意义了,反而应该把这些请求的任务分配到不同的处理器核心上去。这是网页服务器可以做到的,而且大多数服务器已经在这么做了。
在你设想的情况下,每个核心大约处理10个请求,只要请求和核心的分配方式合理(其实最简单的轮询负载均衡就可以),那么每个请求在其生命周期内都在同一个核心上运行是完全没问题的。
关键是,这种情况只是其中一种可能性——那些比较“重”的请求,实际上可以通过使用多个核心来降低延迟,这也是一种可能性。我觉得在现在的网络环境中,你提到的情况可能更常见,但能够同时处理这两种情况,还有“批处理”类型的后台处理就更好了……尤其是现在核心的数量在增加,而不是每个核心的速度。
我并不是想反对Jacob Kaplan-Moss的观点,但在我工作的地方,我习惯于以更好、更明确和透明的方式来实现较好的并发处理——比如使用mapreduce来处理批量任务,基于分布式哈希的分片来将工作分配给多个后端来处理一个查询,等等。
也许我对Erlang、Scala或Haskell等相对较新的软件事务内存没有足够的实际经验,无法看到它们在低请求率、高工作负载的情况下如何出色地扩展到数十或数百个核心……但在我看来,目前还没有任何语言发明出能完美解决这种情况的“灵丹妙药”(除了那些可以使用mapreduce、pregel、分片等的相对有限的情况)。在我工作的经验中,使用明确且精心设计的架构,Python在处理这些情况时绝对不比Java、C#或C++差。