
本文字数:4593;估计阅读时间:12 分钟
作者:Misha Shiryaev
本文在公众号【ClickHouseInc】首发

在 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 等。
在Linux上构建无元数据依赖的 ZIP 归档文件

挑战一:文件顺序会影响结果
很多因素会影响 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.zip
31 61 65
62 62 64
124 64 62
155 65 61
202 61 65
249 62 64
343 64 62
390 65 61
我们要解决的第一个问题就是:确保每次打包时文件顺序完全一致。
此外,还需要避免将目录路径直接传递给 zip 命令,这也可能引入差异。此时可以使用参数 -D 来避免这种情况。
这就是我们解决的第一个问题。
export LC_ALL=c
# zip uses random files order by default, so we sort the files alphabetically
find . ! -type d -print0 | sort -z | tr '\0' '\n' | zip -XD -0 ../"$PACKAGE".zip --names-stdin
我们会先查找目标文件,将它们用 null 字符分隔后进行排序,接着再将 null 替换为换行符,并将结果传递给 ZIP 命令。
挑战二:时间戳也会影响结果
以下是 zip 命令手册中关于前述某个参数的描述:
-X
--no-extra
Do not save extra file attributes (Extended Attributes on OS/2,
uid/gid and file times on Unix). The zip format uses extra
fields to include additional information for each entry. Some
extra fields are specific to particular systems while others
are applicable to all systems. Normally when zip reads entries
from an existing archive, it reads the extra fields it knows,
strips the rest, and adds the extra fields applicable to that
system. With -X, zip strips all old fields and only includes
the Unicode and Zip64 extra fields (currently these two extra
fields 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.zip
11 200 240
42 200 240
73 200 240
104 200 240
135 200 240
168 200 240
215 200 240
262 200 240
309 200 240
356 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 <<EOF
cd '$PACKAGE'
find ! -type d -exec touch -t 201212121212 {} +
python <<'EOP'
import zipfile
import os
files_path = []
for root, _, files in os.walk('.'):
files_path.extend(os.path.join(root, file) for file in files)
# persistent file order
files_path.sort()
with zipfile.ZipFile('../$PACKAGE.zip', 'w') as zf:
for file in files_path:
zf.write(file)
EOP
EOF
构建 Lambda 所需的虚拟环境
为了解决上述问题,我们编写了一个简洁的构建脚本。关键在于使用与 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".zip
mkdir "$PACKAGE"
cp app.py "$PACKAGE"
if [ -f requirements.txt ]; then
VENV=lambda-venv
rm -rf "$VENV"
"${docker_cmd[@]}" -ex <<EOF
'$PY_EXEC' -m venv '$VENV' &&
source '$VENV/bin/activate' &&
pip install -r requirements.txt &&
# To have consistent pyc files
find '$VENV/lib' -name '*.pyc' -delete
cp -rT '$VENV/lib/$PY_EXEC/site-packages/' '$PACKAGE'
rm -r '$PACKAGE'/{pip,pip-*,setuptools,setuptools-*}
chmod 0777 -R '$PACKAGE'
EOF
fi
脚本会首先检查 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 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:Tracy.Wang@clickhouse.com

720

被折叠的 条评论
为什么被折叠?



