ClickHouse:用可复现 ZIP 架构提升 AWS Lambda CI/CD 一致性
本文字数:4593;估计阅读时间:12 分钟
作者:Misha Shiryaev
Meetup活动
ClickHouse 北京第三届 Meetup 火热报名中,详见文末海报!
在 ClickHouse 的 CI/CD 系统中,我们广泛使用了 Lambda 函数(https://docs.aws.amazon.com/lambda/)。AWS 提供两种部署 Lambda 函数的方式:Docker 镜像和 ZIP 压缩包。
由于 ZIP 文件更轻便,我们从 2022 年 11 月(https://github.com/ClickHouse/ClickHouse/pull/43769) 起就一直采用这种方式。除了构建产物体积更小外,它也更容易通过一个简单的 脚本(https://github.com/ClickHouse/ClickHouse/blob/1443e490ea5287175f031eaee567fe5024cd7306/tests/ci/team_keys_lambda/build_and_deploy_archive.sh) 来实现自动化部署。
到了 2023 年底,我们开始使用 Terraform/OpenTofu 来管理 CI/CD 基础设施,这就带来了一个新的问题:“我们应该如何管理部署所用的构建产物?”
这个问题包含两个关键点:
构建产物在每次重新构建后,内容应该保持一致;否则,每次执行 tofu apply 命令时都会触发不必要的重新部署。
理想情况下,构建产物应在本地即时生成,无需借助额外的基础设施,比如上传到 S3 等。
挑战一:文件顺序会影响结果
很多因素会影响 ZIP 文件的内容。其中一个让我颇感意外的是:被压缩文件的顺序也会影响最终结果。下面是一个例子:
# create files 1 2 3 4 5$ touch {1..5}# archive them in sorted and reserved order$ ls [1-5] | sort | zip -q -0 --names-stdin sorted.zip$ ls [1-5] | sort -r | zip -q -0 --names-stdin reversed.zip# compare two files byte-by-byte$ cmp -l sort.zip rev.zip31 61 6562 62 64124 64 62155 65 61202 61 65249 62 64343 64 62390 65 61
我们要解决的第一个问题就是:确保每次打包时文件顺序完全一致。
此外,还需要避免将目录路径直接传递给 zip 命令,这也可能引入差异。此时可以使用参数 -D 来避免这种情况。
这就是我们解决的第一个问题。
export LC_ALL=c# zip uses random files order by default, so we sort the files alphabeticallyfind . ! -type d -print0 | sort -z | tr '\0' '\n' | zip -XD -0 ../"$PACKAGE".zip --names-stdin
我们会先查找目标文件,将它们用 null 字符分隔后进行排序,接着再将 null 替换为换行符,并将结果传递给 ZIP 命令。
挑战二:时间戳也会影响结果
以下是 zip 命令手册中关于前述某个参数的描述:
-X--no-extraDo not save extra file attributes (Extended Attributes on OS/2,uid/gid and file times on Unix). The zip format uses extrafields to include additional information for each entry. Someextra fields are specific to particular systems while othersare applicable to all systems. Normally when zip reads entriesfrom an existing archive, it reads the extra fields it knows,strips the rest, and adds the extra fields applicable to thatsystem. With -X, zip strips all old fields and only includesthe Unicode and Zip64 extra fields (currently these two extrafields cannot be disabled).
按理说,它应该忽略时间戳……但事实并非如此!
$ touch -t 201212121212 {1..5}$ zip -XD0q older.zip {1..5}$ touch -t 201212121213 {1..5}$ zip -XD0q newer.zip {1..5}$ cmp -l older.zip newer.zip11 200 24042 200 24073 200 240104 200 240135 200 240168 200 240215 200 240262 200 240309 200 240356 200 240
实际上,时间戳仍然会影响最终的归档结果。因此,在将文件打包前,我们需要统一修改它们的最后修改时间:
find "$PACKAGE" ! -type d -exec touch -t 201212121212 {} +
挑战三:Python 字节码
根据 AWS 官方建议(https://docs.aws.amazon.com/lambda/latest/dg/python-package.html#python-package-pycache),在 Lambda 包中包含 .pyc 字节码文件可以加快函数的冷启动速度。但这也可能引发兼容性问题,因为字节码版本与 Python 版本及系统架构紧密相关。
经过测试,我们发现跳过 .pyc 文件的最佳方式,是使用 zip 命令的 -x,--exclude 参数来排除这类文件。
最终挑战:操作系统差异
另一个关键问题是操作系统。ZIP 文件的格式会受到创建它的操作系统影响。即便压缩内容完全一致,MacOS 和 Linux 所生成的归档文件仍可能有所不同。我们尝试排查问题但始终无法定位根因。可以肯定的是,任何 zip 工具或实现上的差异,都可能导致归档结果在字节层级出现不一致。
最终,我们决定使用 Python 来创建 ZIP 文件。Python 自带的 zipfile 模块能够生成跨平台一致的归档内容。为了确保完全一致性,我们使用了与 AWS Lambda 相同环境的 Python —— 即 public.ecr.aws/lambda/python 镜像中的版本。
docker_cmd=(docker run -i --net=host --rm --user="${UID}" -e HOME=/tmp --entrypoint=/bin/bash--volume="${WORKDIR}/..:/ci" --workdir="/ci/${DIR_NAME}" "${DOCKER_IMAGE}")"${docker_cmd[@]}" -ex <<EOFcd '$PACKAGE'find ! -type d -exec touch -t 201212121212 {} +python <<'EOP'import zipfileimport osfiles_path = []for root, _, files in os.walk('.'):files_path.extend(os.path.join(root, file) for file in files)# persistent file orderfiles_path.sort()with zipfile.ZipFile('../$PACKAGE.zip', 'w') as zf:for file in files_path:zf.write(file)EOPEOF
为了解决上述问题,我们编写了一个简洁的构建脚本。关键在于使用与 AWS Lambda 一致的 Python 版本来创建虚拟环境。
docker_cmd=(docker run -i --net=host --rm --user="${UID}" -e HOME=/tmp --entrypoint=/bin/bash--volume="${WORKDIR}/..:/ci" --workdir="/ci/${DIR_NAME}" "${DOCKER_IMAGE}")rm -rf "$PACKAGE" "$PACKAGE".zipmkdir "$PACKAGE"cp app.py "$PACKAGE"if [ -f requirements.txt ]; thenVENV=lambda-venvrm -rf "$VENV""${docker_cmd[@]}" -ex <<EOF'$PY_EXEC' -m venv '$VENV' &&source '$VENV/bin/activate' &&pip install -r requirements.txt &&# To have consistent pyc filesfind '$VENV/lib' -name '*.pyc' -deletecp -rT '$VENV/lib/$PY_EXEC/site-packages/' '$PACKAGE'rm -r '$PACKAGE'/{pip,pip-*,setuptools,setuptools-*}chmod 0777 -R '$PACKAGE'EOFfi
脚本会首先检查 Lambda 函数目录下是否存在 requirements.txt 文件。如果存在,就会使用相应版本的 Python 创建虚拟环境、安装依赖,并将 site-packages 拷贝到 Lambda 包目录中。为避免包含冗余内容,还会删除 pip 和 setuptools 目录及所有 Python 字节码文件。
用于构建归档文件的完整脚本可在 这里(https://github.com/ClickHouse/ClickHouse/blob/1443e490ea5287175f031eaee567fe5024cd7306/tests/ci/team_keys_lambda/build_and_deploy_archive.sh) 查看。该脚本后来被迁移到了私有仓库,但仍作为 Terraform/OpenTofu 配置的一部分,持续用于部署 Lambda 函数。
好消息:ClickHouse Beijing User Group第 3 届 Meetup 火热报名中,将于2025年09月20日在北京海淀永泰福朋喜来登酒店(北京市海淀区远大路25号1座)举行,扫码免费报名
/END/
试用阿里云 ClickHouse企业版
轻松节省30%云资源成本?阿里云数据库ClickHouse 云原生架构全新升级,首次购买ClickHouse企业版计算和存储资源组合,首月消费不超过99.58元(包含最大16CCU+450G OSS用量)了解详情:https://t.aliyun.com/Kz5Z0q9G
征稿启示
面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:[email protected]