构建 AWS Lambda Python Docker 镜像

AWS 的 Lambda 在 2020-12-01 开始支持用 Docker 镜像存放代码,见 New for AWS Lambda - Container Image Support。AWS Lambda 最初的对发布包的限制是 50M, 解压后(因为执行前需要解压缩)不能超过 250M,对于压缩比小于 1/5 的包来说,要突破 50M 部署包的限制就要用 2018-11-29 推出的层(layer), 即把 Lambda 的依赖可以组织为层,每个 Lambda 可引用最多 5 个层,但最终 Lambda 加上层所解压后的大小仍然有 250 M 的限制。

对于使用了大量依赖的 Lambda,比如 Python 中用了 Pandas 之类的数学分析包,250M 的大小是不够的,所以才有了 Docker 镜像化的 Lambda, 镜像的大小限制一下蹦到 10G,要构建出一个 10G Lambda 用的 Linux 镜像, 那绝对是个巨兽,至少目前是超越我的想像力,除非往里面塞入大量的业务数据。关于 Lambda 有哪些限制,请参阅 Lambda quotas

介绍完 Lambda 引入 Docker 镜像的背景后,本文接下来只关注如何构建一个 Python Lambda 镜像,对于如何部署 Docker 化的 Lambda, 不在本文的范围之内。主要的参考文档为 AWS Lambda 官方的 Deploy Python Lambda functions with container images. 阅读全文 >>

创建和发布自己的 Python 包到 PyPI 上

像 Java 可发布包到 Maven 仓库,NodeJS 发布包到 NPM 一样,我们也可以创建自己的 Python 包并发布到 PyPI 仓库中去。或者内部使用的包,只须发布到私有的 Nexus 服务器上。

本文中的例子将创建一个 Python 包 bounded-executor, 并发布到 PyPI 上。为什么创建这个包呢?原因是直接用 Python 的 ThreadPoolExecutor 或  ProcessPoolExecutor 来提交任务的话,任务的等待队列是没有边界的,这就会造成因提交任务过快而使得内存爆满。本包最为合适的名称应该是 bounded-pool-executor, 可是名字已被他人使用,但此外的实现有所不同,ThreadPoolExecutor 用 Queue(maxsize) 来控制,而 ProcessPoolExceutor 用 BoundedSemaphore 来控制。

我们以经典的 Python 工程目录结构为例,构建的核心是执行 setup.py 中的 setup 函数,由此来理解 setup 的最主要配置与关键命令做了些什么。这样有助于我们理解其他的 Python 包管理工具的底层行为,从中我们可以对比 poetry 的 build, install, 和 publish 命令。 阅读全文 >>

Python 中创建 PostgreSQL 数据库连接池

征战 Java 多年,习惯于使用数据库之前都必须创建一个连接池,即使是单线程的应用,只要有多个方法中需用到数据库连接,建立一两个连接的也会考虑先池化他们。连接池的好处多多,1) 如果反复创建连接相当耗时,2) 对于单个连接一路用到底的应用,有连接池时避免了数据库连接对象传来传去,3) 忘记关连接了,连接池幸许还能帮忙在一定时长后关掉,当然密集取连接的应用势将耗尽连接,3) 一个应用打开连接的数量是可控的

接触到 Python 后,在使用 PostgreSQL 也自然而然的考虑创建连接池,使用时从池中取,用完后还回去,而不是每次需要连接时创建一个物理的。Python 连接 PostgreSQL 是主要有两个包,py-postgresqlpsycopg2, 而本文的实例将使用后者。

Psycopg 在 psycopg2.pool 模块中提供了两个连接池的实现在,它们都继承自 psycopg2.pool.AbstractConnectionPool, 该抽象类的基本方法是

  1. getconn(key=None): 获取连接
  2. putconn(conn, key=None, close=False): 归还连接
  3. closeall(): 关闭连接池中的所有连接

阅读全文 >>

Python zipfile 只借助内存进行压缩与解压缩

Python zipfile 模块压缩与解压缩通常是对物理磁盘文件进行操作,比如参照官方的例子,生成压缩文件的代码是

with zipfile.ZipFile('spam.zip', 'w') as myzip:
    myzip.write('eggs.txt')
    myzip.write('beef.txt')

这样就生成了一个包含两个文件的压缩包 spam.zip, 相当于命令 zip spam.zip egges.txt beef.txt 的效果。用  unzip -l spam.zip 命令就能看到其中的两个文件。相应的解压缩的代码如下

with zipfile.ZipFile('spam.zip', 'r') as myzip:
    print(myzip.filelist())  # 可获得压缩包中的文件列表信息
    myzip.extractall()

同样是把压缩包 spam.zip 解压缩文件到当前目录中,相当于命令 unzip spam.zip 的效果。

前面顺便也是熟悉一下 zipfile 模块的常见用法,但有时候我们可能从数据库中,从网络上收到的是字节数据,希望直接处理字节的压缩解压缩,而不借助于中间的磁盘文件,因为通过磁盘文件来处理必须进行善后处理以及可能的资源的竞争,在内存宽裕的情况下效率也是个问题。 阅读全文 >>

Python Poetry 项目中相对路径模块引用的问题

最近一直在折腾 Python 项目,通过对几个 Python 项目依赖管理与构建工具的对比,最后选择了 Poetry。它管理依赖,构建与发布包还是简单的多,不需要处理 setup.py, setup.cfg 和 Makefile 文件, 甚至都不需要了解 wheel 是什么就能往 PyPI 发布包了。

可是,别看 Poetry 的官网一直守护着一副小清新的形像,其实照样处处是坑,其中一个就是与相对引用有关的问题。我们来看下什么样的现像,最后的结论就是:在 Python 中避免使用相对路径引用,因为相对路径的上下文经常在变,然后必要时先执行 poetry install, 甚至把入口代码拉到包外头去。

什么是相对引用与绝对路径引用,比如在一个包 my_package 中有两个模块(Python 文件) app.py 和 utils, app.py 中对 utils 资源的引用可以写成

from utils import md5               # 不确定 utils 是一个包还是一个模块,有点像是隐式相对路径模块引用
from .utils import md5              # 同一目录中的 utils 模块
import .utils
from ..utils import md5             # 上一级目录中的 utils 模块 (如果 utils.py 在与 app.py 上一级目录的话)
from my_package.utils import md5  # 绝对引用,总是从包名开始

注意 from 后面的 ...,相对路径引用不能直接 import, 如不能 import .utils.md5

阅读全文 >>

Python 依赖管理与构建工具(CookieCutter, PyScaffold, PyBuilder, Poetry)

Python 历时这么久以来至今还未有一个事实上标准的项目管理及构建工具,以至于造成 Python 项目的结构与构建方式五花八门。这或许是体现了 Python 的自由意志。不像 Java 在经历了最初的手工构建,到半自动化的 Ant, 再到 Maven 基本就是事实上的标准了。其间 Maven 还接受了其他的 Gradle(Android 项目主推), SBT(主要是 Scala 项目), Ant+Ivy, Buildr 等的挑战,但都很难撼动 Maven 的江湖地位,而且其他的差不多遵循了 Maven 的目录布局。

回到 Python,产生过 pip, pipenv, conda 那样的包管理工具,但对项目的目录布局没有任何约定。关于构建很多还是延续了传统的 Makefile 的方式,再就是加上 setup.py 和 build.py 用程序代码来进行安装与构建。关于项目目录布局,有做成项目模板的,然后做成工具来应用项目模板。下面大概浏览一下四个工具的使用

  1. CookieCutter
  2. PyScaffold
  3. PyBuilder
  4. Poetry

阅读全文 >>

用 Python 定义 Schema 并生成 Parquet 文件

原来用 Java 和 Python 实现过 Avro 转换成 Parquet 格式,所以 Schema 都是在 Avro 中定义的。这里要尝试的是如何定义 Parquet 的 Schema, 然后据此填充数据并生成 Parquet 文件。

本文将演示两个例子,一个是没有层级的两个字段,另一个是含于嵌套级别的字段,将要使用到的 Python 模块有 pandas 和 pyarrow

简单字段定义

定义 Schema 并生成 Parquet 文件

阅读全文 >>

Python 子进程与子进程池的应用

去年记录过一篇如何使用 Python 的线程,线程池的日志 Python 多线程编程, 需用到 threading.Thread, concurrent.futures.ThreadPoolExecutor。本文可以当作是上一文 Python 多线程编程的姊妹篇。

Python 的多线程受到 GIL(Global Interpreter Lock) 的限制,GIL 是一把加到了 Python 的解释器的锁,使得在任意时刻只允许一个 Python  进程使用 Python 解释器,也就是任意时刻,Python 只有一个线程在运行。

GIL 严重影响了计算密集型(CPU-bound) 的多线程程序,此时的多线程与单线程性能没什么差异,也发挥不了多核的威力。但对 I/O 密集型(I/O-bound) 影响不大,因为 CPU 多数时候是在等待。

为了突破 GIL 的 CPU 密集型程序的限制,可以使用非 CPython 解释器,如 Jython, IronPython 或 PyPy, 更为现实的做法就是使用子进程来替代线程去承担较为繁重的计算任务,因为 GIL 是加在进程上的,所以新的进程有独立的 GIL. 阅读全文 >>

Python 调用动态库时 Segmentation fault (core dumped) 问题

这几天一直纠缠在如何调用动态库的问题上,先是 Go 语言,而后迁移到 Python 语言。在测试 Python 调用动态库时,出现过 "Segmentation fault (core dumped)" 的问题,本文记录下怎么去寻找线索,找到并解决问题的。

出现 "Segmentation fault (core dumped)" 的原因是多方面的,比如在 C/C++ 语言中

  1. 内存访问越界(数组越界,strcpy, strcat, sprintf, strcmp 等字符串函数读写越界)
  2. 多线程使用了线程不安全的函数
  3. 多线程读写的数据未加锁保护
  4. 非法指针(NULL 指针,随意的指针类型转换
  5. 堆栈溢出(如大的分配在栈上的局部变量)

用 Python 来调用动态库很大的可能性会是内存访问越界 阅读全文 >>

Python 调用 C 动态库(Linux)

Go 调用 C 写的动态库完整例子(Linux版) 弄完了 Go 语言如何调用动态库,又开始琢磨起 Python 怎么调用动态库,首先仍然是以前一篇中的 C 实现为例,C 函数为原型为 char * Add(char* src, int n), 由于用符号直接定位函数,所以无需 C 的头文件。本文仍然是以 Linux 平台为例,GCC 编译为动态库 so 文件。并实验了两个例子,一个为基本的类型,char* 和  int, 再一个就是在 C 中使用到了结构体指针和无类型指针(void*) 时,如何在 Python 进行调用。

测试环境为:

  1. Linux Ubuntu 20.04
  2. gcc: gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
  3. Python: 3.8.10

阅读全文 >>