月度归档:2022年11月

使用Python做嵌入式开发

出于性能的考虑,传统的嵌入式开发都以C、C++为主。如今嵌入式设备的性能早已今非昔比,开发工具的选择方面也有了更大的自由度。对于非性能敏感的业务,Go、Python等开发语言引入的开发速度提升还是非常诱人。Python有着丰富的开发资源,在系统资源足够的情况下,Python在嵌入式环境下有着不错的开发体验。

性能

同Python高效的开发速度相对应的是Python的运行速度非常的慢,即使在脚本语言里Python也是最慢的一档。如果你的程序需要高性能,Python显然是不合适的。即使不需要高性能,也需要特别注意以保证用户体验。

使用的库尽量精简。在PC下Python的启动速度不会有明显的感觉,但在嵌入式设备下,用到的库多了后,第一个明显的感觉就是启动时间变长。如果用到的库多,启动时间甚至会超过10秒。嵌入式环境下引入一个新库需要更为谨慎,平衡好开发体验及性能影响。

程序打包(应用分发)

Python在跨平台方面做的非常优秀,大多情况下可以不需要嵌入式设备,直接本地开发调试。但程序发布的时候还需要针对应用平台就行打包。

pex会把所有的依赖和你自己的代码打包成一个.pex为后缀的可执行文件。在运行环境下直接执行该文件即可。由于开发环境的构架和运行环境的架构不一致,可以通过Docker容器就行程序的pex打包。

代码保护

对于商业项目,必要的代码保护还是有一定的必要。代码的保护可以选择下面几种方式。

  1. 编译成pyc
    • 使用命令 `python3 -m compileall -b ./;find ./ -name “*.py” -delete` 将代码编译成pyc,并删除py文件。
    • 该做法可以提供最低限度的代码保护。pyc还是可以较容易的反编译成py文件。
  2. 使用代码加密(混淆)工具对源代码进行加密。
    • 开源的代码加密工具都缺乏维护,很久未更新。如果有代码加密需求,建议使用商业工具。pyarmor
  3. 使用 CythonNuitka 等工具将代码编译成二进制文件。
    • 相比 Cython,Nuitka的使用要简单很多,建议优先使用Nuitka。需要注意的是使用 Nuitka 后内存占用率会比直接用Python解释器高大概 1/3 。
    • Nuitka的编译也可在Docker容器中进行。

Python内存泄漏原因及问题排查

Python 会自动回收内存,一般情况下不用关心内存的申请和释放问题。事实上我也一直没怎么关心过Python的内存管理问题,直到我用了 Python Prompt Toolkit 。这是一个 Python 的CLI组件库,使用简单,效果很好。只是性能用点差,另外就是它居然有内存泄漏。

内存问题产生原因

Python里内存管理主要基于引用计数实现,另外会辅以全图遍历以解决循环引用问题。一般内存问题都是对象被全局变量直接或间接持有导致。出现内存泄漏后关键是找到问题对象到底被谁给持有了。

确认内存泄漏的对象

如果一个程序内存一直异常增长,那多半是存在内存泄漏。接下来就是定位问题了。Python内存分析的工具和手段主要有下面几个:

  1. objgraph 可用现实对象的增长情况。还可以根据对象的引用关系生成图像。
    • 可以根据对象生成引用关系树以及被引用关系树。第一感觉功能很强,实际用下来效果一般。对于复杂一些的项目,生成的关系树是在太过复杂。你以为对象都是通过属性持有,实际上各类的闭包,函数指针等都会持有对象。
  2. pympler 感觉和objgraph差不多,不支持生成图像。
  3. gc.get_referents()/gc.get_referents()/gc.* 获取对象的引用计数及指向该对象的对象,以及其它分析函数。
    • 其它内存分析的库应当都是基于Python的gc模块实现。
  4. print('PromptSession count: ', len([o for o in obj if isinstance(o, PromptSession)])) 打印对象数量,确认是否被释放。

解决内存泄漏问题

要解决内存问题,关键还是找到存在内存泄漏的问题被谁给持有里,然后在需要销毁对象时释放该持有。如果想该对象持有不影响对象的生命周期(比如缓存),可以使用 weakref 库来创建弱引用。

Python Prompt Toolkit 的内存问题

出于性能等考虑 Python Prompt Toolkit 添加来大量的缓存。其中一些看似简单的缓存对象持有了其它对象的函数(函数指针),从而间接持有了其它对象,最终导致大量的对象未被释放。一般情况下一个程序只有一个 PromptSession 对象,该对象贯穿程序的整个生命周期,因此问题不容易察觉。但我的应用时一个服务端程序,需要反复创建和销毁 PromptSession 对象,问题将出现了。

我尝试用 weakref.WeakValueDictionary 改写它的缓存实现,实际过程中发现key和value都会持有对象。

目前的做法是用户断开服务器连接时进行一次缓存的清理。