当你的Celery任务太慢,而你想让它们运行得更快时,你通常想找到并修复性能瓶颈。诚然,你可以架构一个解决方案,让慢的任务不影响快的任务,而且你有时可能需要这样做。但是,如果你能设法让所有的任务都变得快速,那是最理想的。
为了加速你的代码,你需要识别瓶颈:哪个任务慢,以及为什么慢。在这篇文章中,我们将通过寻找Celery任务中的性能瓶颈的过程,解释沿途的一些权衡和选择。
- 识别缓慢的任务。
- 用代码仪表找到可预测的性能瓶颈。
- 剖析来寻找更难预测的瓶颈,也是如此。
- 在一台开发机器上。
- 或者,在生产中。
1.识别慢速任务
如果你正在运行多个不同的任务,你可能需要弄清楚哪个特定的任务是慢的,这取决于你如何发现你有一个问题。如果你正在运行一个混合的任务A、B和C,而你发现的最初症状是工作母机上的CPU过高,这并没有告诉你哪个任务有问题,只是告诉你有一个或多个任务很慢。
要弄清哪些任务速度慢,你可以首先查看 Celery 工作者的日志,特别是任务的开始和结束时间。确保你用--loglevel INFO 或更高的版本启动该工作者,以便它记录这些信息。
[2022-07-21 14:53:06,457: INFO/MainProcess] Task celery_tasks.mytask[d5707ad7-5640-4034-b39a-b038e0754281] received
[2022-07-21 14:53:11,100: INFO/MainProcess] Task celery_tasks.mytask[d5707ad7-5640-4034-b39a-b038e0754281] succeeded in 4.642702493991237s: None
某种跟踪或 "应用程序性能监控"(APM)服务也可以在这里派上用场,作为一种更复杂的记录形式。
其他选择包括。
- 使用Celery的Flower监控工具。
- 看一下调度任务的任何代码的上游日志。
一旦你确定了哪个任务慢,下一步就是找出它慢的原因。
2.使用日志和追踪来发现可预测的性能瓶颈
在某些情况下,日志、追踪或APMs可以帮助你找到瓶颈。然而,前两者只能给你提供有关被专门检测的代码的信息,在大多数情况下,APM也是如此。
如果性能瓶颈恰好被你的代码被记录或以其他方式检测的地方捕获,你就找到了问题所在!你现在可以转向修复它。你现在可以转而去解决它了。例如,APM通常会自动记录常用的远程交互库,如HTTP调用的requests ,SQL的sqlalchemy 。因此,如果你的性能问题是一个缓慢的SQL查询,这类仪器会很快找到它。
**然而,由于性能或可读性的原因,很难对软件中的每一个操作进行检测,更不用说每一行代码了。**这意味着你只有在有人事先决定可能成为问题来源的地方才会有日志或跟踪仪器。
有时,不幸的是,你的代码由于难以预测的原因而变得缓慢。这意味着你可能没有足够的日志或跟踪来识别瓶颈。这就是剖析变得有用的地方。
3.剖析Celery任务
剖析包括检查正在运行的代码的任何部分,而不需要高级仪器。一些剖析器如 cProfile和VizTracer会检查每一个函数和每一行代码,但这会增加大量的性能开销。因此,通常你会使用抽样分析器,它以固定的时间间隔检查运行中的代码。慢的任务往往会在结果的样本中显示得更多,所以这在实践中效果很好。
你可以在你的开发机器或笔记本电脑上,或者在生产中对代码进行剖析。
在开发机器上进行剖析
在非生产机器上进行剖析有一些好处。
- 你不必担心破坏生产,降低生产速度,或以其他方式干扰实际使用。
- 你可以完全控制代码的运行。
但也有坏处。
- 运行代码可能需要各种资源,而这些资源可能更难在本地设置。
- 性能问题可能只发生在真实的输入,或真实的数据上,例如,生产数据库可能有不同的性能特征,因为它的数据比测试设置要多得多。
- 其他环境差异也可能隐藏性能瓶颈,例如,网络延迟可能不同,足以歪曲结果。从S3下载大量的记录,在云数据中心所花的时间与从你的家或办公室所花的时间不同,而且都会与你在笔记本电脑上运行的S3模拟服务所花的时间不同。
让我们看看你如何在本地进行剖析。
通常情况下,在调用Celery任务时,你可能会使用类似delay() 。
from celery_tasks import mytask
def yourcode():
async_result = mytask.delay(10_000)
# ...
result = async_result.get()
这就把任务安排在一个单独的进程中运行,即worker。
然而,你可以直接在当前进程中运行一个任务,这使得剖析更容易。例如,我们可以创建一个文件toprofile.py 。
from celery_tasks import mytask
mytask(10_000)
当我们运行这个文件时,mytask() 将在同一个进程中运行,而不是在一个单独的worker中。然后我们可以很容易地使用一个剖析器,比如 py-spy的剖析器来剖析它。
$ pip install py-spy
$ py-spy record python toprofile.py
py-spy> Sampling process 100 times a second. Press Control-C to exit.
py-spy> Stopped sampling because process exited
py-spy> Wrote flamegraph data to 'python-2022-07-21T13:43:50-04:00.svg'. Samples: 522 Errors: 0
结果看起来像这样。
这是一个火焰图;左/右位置并不重要,但宽度很重要。框架越宽,运行时间的百分比就越高。在这里,我们可以看到JSON编码是大部分时间,还有一些时间花在一个叫做deepcopy_list() 的函数上(点击最右边的堆栈放大并查看细节)。
生产中的剖析
在生产中进行剖析比在你的开发机器上进行剖析更为棘手。
- 你不希望破坏生产!
- 放慢生产速度也不是很好。
- 要控制特定任务的运行时间更难甚至不可能。
- 使用
gevent或eventlet池的工作者可以在一个线程中交错运行多个任务。 - 使用
threading池的工作者可以在同一进程中运行多个任务,因此进程级的分析可能会混合不同的任务。
在生产中,你可以采取多种方案进行剖析。
选项#1:附加到一个正在运行的进程
假设你使用的是solo 或prefork/processes 池,每个工作进程在同一时间只会运行一个任务。在一些前提条件下,你可以从一个已经运行的工作进程中获取剖析信息。
- 你知道你想要剖析的任务已经开始,或者即将开始(也许你可以触发它?)
- 你知道或可以检查哪个工作器将运行它(也许你已经确保有足够的工作排队,所以只有一个工作器是空闲的?)
- 您可以使用 SSH、
docker exec、kubectl exec或其他机制,对运行该 Worker 的机器或容器进行终端访问。 - 你有root权限,或者,在容器化的工作负载中,你已经将容器配置为具有
CAP_SYS_PTRACE。
然后,你可以使用py-spy ,在任务开始时附加到相关的、已经运行的进程中。你让它运行,直到任务结束,这时你点击Ctrl-C,就可以得到一份剖析报告。
$ pip install py-spy
$ ps xa | grep celery
576600 pts/7 S+ 0:00 celery worker --pool prefork --concurrency 4
576602 pts/7 S+ 0:00 celery worker --pool prefork --concurrency 4
576603 pts/7 S+ 0:00 celery worker --pool prefork --concurrency 4
576604 pts/7 S+ 0:00 celery worker --pool prefork --concurrency 4
576605 pts/7 S+ 0:00 celery worker --pool prefork --concurrency 4
578258 pts/8 S+ 0:00 grep --color=auto celery
$ py-spy record --pid 576602
Permission Denied: Try running again with elevated permissions by going 'sudo env "PATH=$PATH" !!'
$ sudo env "PATH=$PATH" py-spy record --pid 576602
py-spy> Sampling process 100 times a second. Press Control-C to exit.
py-spy> Stopped sampling because Control-C pressed
py-spy> Wrote flamegraph data to '576602-2022-07-21T14:04:05-04:00.svg'. Samples: 89 Errors: 0
你也可以类似地使用Austin剖析器来附加到正在运行的进程。
方案二:在进程中进行分析
该 pyinstrument剖析器可以让你只剖析你的代码的特定部分。那么,你可以事先修改你的任务,使其在运行时带有剖析功能,然后每当它在生产中运行时,就会将剖析信息转储到磁盘上,而不是附加到一个正在运行的进程上。
你将修改你的任务Python模块来剖析你关心的特定任务;注意,与日志不同,你不必告诉它在start() 和stop() 之间要剖析什么;在这之间运行的任何慢速函数最终都将被剖析。
# ... pre-existing imports, creation of app, etc. ...
from pyinstrument import Profiler
@app.task
def mytask(length):
profiler = Profiler()
profiler.start()
# ... your actual code ...
profiler.stop()
profiler.print()
现在,如果你重新部署,然后在生产中正常地运行你的celery worker ,日志将包括剖析信息,例如。
4.293 mytask ./sandbox/celery-tasks/celery_tasks.py:11
├─ 2.518 dumps json/__init__.py:183
│ [7 frames hidden] json, ..
│ 2.418 iterencode json/encoder.py:204
├─ 0.858 [self]
├─ 0.480 deepcopy_list ./sandbox/celery-tasks/celery_tasks.py:7
│ └─ 0.480 <listcomp> ./sandbox/celery-tasks/celery_tasks.py:8
├─ 0.211 TextIOWrapper.write ./sandbox/celery-tasks/<built-in>:0
│ [2 frames hidden] ..
├─ 0.151 list.append ./sandbox/celery-tasks/<built-in>:0
│ [2 frames hidden] ..
└─ 0.075 fsync ./sandbox/celery-tasks/<built-in>:0
[2 frames hidden] ..
同样,我们可以看到JSON编码使用了很多时间。你也可以将HTML报告输出到磁盘上,详情请见pyinstrument 文档。
这种方法的问题是,pyinstrument ,像大多数剖析器一样,不是为生产使用而设计的;在生产中使用它可能会对性能产生重大影响,这取决于你的代码的瓶颈在哪里。
额外的选择。增加更多的日志记录
如果以上两种方法都不奏效,你还可以选择添加越来越多的日志或跟踪。最终,根据生产日志,你将设法隔离代码中的瓶颈部分,如果问题是CPU,而你还无法进一步隔离,那么你可以切换到本地机器剖析。
测量是困难的,但往往是必要的
有些性能问题很明显,你不会花任何时间去发现它们。
其他的,不那么明显,但仍然可以提前预测的问题,可以通过良好的日志或跟踪来发现。缓慢的SQL查询就是一个典型的例子。
然后,还有一些问题只有通过用分析器测量性能才能发现。如果你很幸运,你可以在你的开发机器上进行剖析。但如果没有,你就需要在生产中测量性能,因为代码在那里运行。作为中间选择,一个尽可能接近生产的暂存或测试环境也是足够的。
如果上述在生产中测量性能的方案都没有吸引力,你也可以试试Sciagraph,一个针对Python批处理作业的性能观察服务。它是为在生产中运行而设计的(不需要root权限),它的性能开销很低,所以不会影响运行时间,而且它有内置的Celery支持。