ClickHouse:用可复现 ZIP 架构提升 AWS Lambda CI/CD 一致性

图片

本文字数: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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值