当前位置: 首页 > 后端技术 > Python

为什么eventlet卡在AppleM1上?

时间:2023-03-26 14:24:22 Python

本文同步发布于字节谈云公众号。背景前段时间,旧款MacBook已经到了报废的年龄。听了朋友的安利,换了一台基于苹果M1的MacBook。跑项目的时候,发现本来可以正常执行的任务,在新笔记本上还在进行中。bug调试之旅。初步探索有如下信息:Python版本为2.7.10+,eventlet版本为v0.21.0。导致这个问题的直接原因是代码中使用了eventlet.sleep。一旦当前协程执行到sleep语句,切出协程后eventlet不会切换回来。使用py-spy观察流程,发现形状如下调用信息:%Own%TotalOwnTimeTotalTimeFunction(filename:line)0.00%0.00%0.010s0.010srun(eventlet/hubs/hub.py:334)可以初步判断是sleep的问题,因为项目逻辑太复杂,不方便debug,不如自己写个最小的测试代码来验证一下猜想。重现问题既然我们怀疑问题出在sleep,又认为eventlet在sleep之后不会切换协程,那么我们不妨在一个协程池中开启两个协程,每个协程输出和sleep不断,那么就有如下代码:importeventletdefecho(name):whileTrue:print(name)eventlet.sleep(2)defmain():p=eventlet.GreenPool()p.spawn(echo,'foo')p.spawn(echo,'bar')p.waitall()if__name__=='__main__':main()运行后,依次输出foo和bar后,停止输出。调试分析既然输出了foo和bar,说明第一个echo协程输出foo进入sleep后,eventlet的调度没有阻塞,所以eventlet可以调度第二个echo协程输出bar。但是为什么两个协程在sleep之后不会再被调度呢?由于我们通过py-spy监控了进程调用栈,发现eventlet/hubs/hub.py被调用了,所以不妨在这个文件中查看和调试运行逻辑。defrun(self,*a,**kw):"""Runrunloopuntilabortiscalled."""#acceptanddiscardvariableargumentsbecausetheywillbesuppliedifothergreenletshaverunandexitedbeforethehub'sgreenlet有机会运行ifself.running:raiseRuntimeError("Alreadyrunning!")try:self.running=Trueself.stopping=Falsewhilenotself.stopping:whileself.closed:#Weditchallthesefirst.self.close_one()self.prepare_timers()如果self.debug_blocking:self.block_detect_pre()self.fire_timers(self.clock())如果self.debug_blocking:self.block_detect_post()self.prepare_timers()wakeup_when=self.sleep_until()如果wakeup_when为None:sleep_time=self.default_sleep()else:sleep_time=wakeup_when-self.clock()ifsleep_time>0:self.wait(sleep_time)else:self.wait(0)else:self.timers_canceled=0delself.timers[:]delself.next_timers[:]finally:self.running=Falseself.stopping=False这段代码中可以看到sleep的相关处理(第23~31行),获取协程被唤醒的时间减去当前时间,如果超过0则表示还是需要等待一段时间,不然唤醒实测发现是self.clock()返回的时钟值有问题。这里的时钟是eventlet/support/monotonic.py中的monotonic()函数,用来返回单调递增的时钟数(每递增1表示1秒)。此功能在macOS上的实现如下:ifsys.platform=='darwin':#OSX,iOS#参见Mac开发者库的技术问答QA1398:#libc=ctypes.CDLL('/usr/lib/libc.dylib',use_errno=True)classmach_timebase_info_data_t(ctypes.Structure):"""系统时基信息。定义在."""_fields_=(('numer',ctypes.c_uint32),('denom',ctypes.c_uint32))mach_absolute_time=libc.mach_absolute_timemach_absolute_time.restype=ctypes.c_uint64timebase=mach_timebase_info_data_t()libc.mach_timebase_infoctypes。byref(timebase))ticks_per_second=timebase.numer/timebase.denom*1.0e9defmonotonic():"""Monotonicclock,cannotgobackward."""t=mach_absolute_time()returnmach_absolute_time()/ticks_per_secondmonotonic()函数调用mach/mach_time.h的mach_absolute_time()系统函数,得到系数处理后的最终时钟数。处理方式相当于mach_absolute_time()/timebase.numer*timebase.denom/1.0e9,但是这个算法是错误的,正确的表达应该是mach_absolute_time()*timebase.numer/timebase.denom/1.0e9,请参考单调的.py实现。修改这个实现后,再运行测试代码,eventlet就可以正常休眠和调度了。为什么错误的逻辑在英特尔芯片上运行良好?mach_timebase_info_data_t中的numer和denom分别是时钟数比例因子的分子和分母,在Intel芯片上固定为1。这意味着number/denom的结果与denom/number相同。换句话说,即使比例因子表达式反转,计算结果也不会改变。但是在M1芯片上,分子和分母不相等,number大于denom,使用错误的比例因子denom/number会导致计算结果小于正确值,也就是说当real时间过去1秒,计算出来如果只过去了0.000x秒,那么eventlet会认为还没有到唤醒的时间,继续等待,从而造成卡死的错觉。针对该问题的修复,eventlet在v0.24.0版本引入了第三方库monotonic.py来解决该问题。综上所述,用AppleM1开发程序的,真的是勇士。由于CPU架构与Intel不同,极有可能出现意想不到的问题。一旦出现问题,基本思路是先观察现象,使用py-spy等工具进一步查看进程在做什么,然后尝试重现调试,从上层逐步分析代码到底层下层定位问题。