以前用自定义运行时打包 Dart Server 到云函数,本质上也是跑 Docker,只是有着诸多限制和不足。 现在终于要手写 Dockfile,体验了一把真正云原生的感觉,还是很过瘾的。

简介&缘起

前端的朋友可能比较熟悉,Prisma 是一个大而全的 ORM 工具,它使用自己的 DSL 来定义数据结构,向前生成语义化、类型安全的查询方法供代码调用,向后在自己的引擎中生成 SQL 语句,发送到数据库进行交互。当你修改数据结构后,它还能帮你同步到数据库。前后一把抓,只需要维护一个数据类,岂不美哉?

虽然是个 Node 项目,但它的查询引擎是 Rust 写的,可以供不同语言调用,其中就包含了社区开源大佬支持的 Dart Client

两年前没啥生态只能手撸 SQL 生成器,如今一查居然有了这么好的东西,那必须积极拥抱一下。

遇到的问题

按照官网教程在服务端项目中添加,初始化后会下载一个查询引擎二进制文件,本地运行很顺利。 在 Dart 打包镜像文件的基础上,把这个文件复制了过去。

但是跑起来访问查询接口时,等待约半分钟后 Log 打印了如下错误:

Prisma binary query engine not ready

查询引擎没有就绪,显然是调用出了问题。

解决思路和过程

解决问题的第一步是先定位问题。

为了排除干扰,新建了一个测试项目,不引入服务端框架,跑起来后只调用一个 Prisma 的连接和查询方法。 问题依旧,这就确认了与其他部分无关。开始逐步修改 Dockerfile 进行排查。

整体的大概思路是:

先确认当前是否能在 Docker 中跑起来(可能版本更新出问题了,本身在 Linux 下就跑不起来,没跑起来前不排除这个假设hhh),如果可以再逐步替换变量,找到问题所在,最后再层层筛选减小镜像大小。

看源码

开始排查前还是先看看源码,也许能直接找到问题。

代码中调用 prisma 是通过自动生成的 prisma/generated_dart_client/client.dart 进行的。 这个客户端文件调用了 package:orm/engines/binary.dart 与查询引擎进行交互,报错的地方在 103 行:

1
2
3
4
5
6
7
8
9
  Future<Uri> _serverEndpoint() async {
    // Await _endpoint is not null
    // With `retry` package, retry 5 times with 1 second delay
    return retry<Uri>(
      () => _endpoint != null
          ? _endpoint!
          : throw Exception('Prisma binary query engine not ready'), // 这一行
    );
  }

很显然是连接的时候出错了,重试了5次每次都有延迟所以会卡顿一会,立刻想到文件的位置放的不对, 改了一下 COPY 命令换到另一个目录,结果这次提示的很明显:No query engine binary found in '/' '/prisma' '/.dart_tool',说明并不是没有找到文件。

排除了引擎文件位置不对的可能。

查文档

意识到不同系统下打包的二进制可能会不通用,特地搜索了引擎相关章节,果然发现一个参数 binaryTargets,可以指定平台。

填写打包时对应的基础镜像,失败。把 Prisma 的 init 和 generate 方法放在打包过程中,去生成默认的引擎,失败。

这里犯了个错误,如果再多看一会部署文档,就能发现其对环境的依赖列表,没有看到导致浪费了一些时间做实验。

此外也搜到一个 Dockerfile 示例,事后证明因为基础镜像的版本升级,某条语句没有生效,与正确答案擦肩而过。

从头排查,层层递进

编译成品打包失败了怎么办?笨办法验证,没有编译,直接把源代码和环境初始化,dart run xxx 命令搬过去,弄了个 1.9G 的超大镜像,这下终于可以了,哈哈哈。

至少说明了在 Linux 下是可用的。

接下来就是把 Dart 的部分给编译了,在新镜像里逐步删去 Prisma 相关的依赖。尝试过程中发现对引擎在不同系统对 openssl 的依赖也有差异,于是指定了对应的镜像版本号,最终移除了 node_moudels 目录,打包镜像大小降至 300 M 。

进一步减小体积

到这里基本确定了需要 node 环境和对应版本的 openssl,之前一直在默认镜像上打包,查资料时发现有个 Alpine 发行版体积非常小,官方镜像只有 5 M,立刻去替换了对应的 node 镜像和打包基础镜像。

一番折腾后,最终的镜像降低到了 100 M。

意外之喜

本来到这里就要结束,结果提前上传的 demo 被该库的作者 Seven Du 发现了,大佬为人和善,作为创建者主力维护这个开源库两年多,一眼发现我的打包尺寸过大,检查后给出了一份新的 Dockerfile 参考。

对比后发现,其实 Prisma 在此处的依赖并非是 node 环境,而是四个 so 文件。前面折腾的各种依赖只是恰好最终包含了这些运行时。


最终版的 Dockerfile (点击展开)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
FROM dart:stable AS build
WORKDIR /app

COPY pubspec.* ./
RUN dart pub get

COPY . .

RUN dart pub get --offline

# Install Node.js LTS to build prisma
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - &&\
    apt-get install -y nodejs

RUN npm install prisma
RUN npx prisma generate

RUN dart compile exe bin/dart_orm_docker_test.dart -o bin/server

# Copy Prisma Engine deps so to `/runtime/`
RUN FILES="libz.so libgcc_s.so libssl.so libcrypto.so"; \
    for file in $FILES; do \
    so="$(find / -name "${file}*" -print -quit)"; \
    dir="$(dirname "$so")"; \
    mkdir -p "/runtime${dir}"; \
    cp "$so" "/runtime$so"; \
    echo "Copied $so to /runtime${so}"; \
    done


FROM scratch

COPY --from=build /runtime /
COPY --from=build /app/bin/server /app/bin/
COPY --from=build /app/.env /app/.env
COPY --from=build /app/db/test.db /app/db/test.db
COPY --from=build /app/prisma-query-engine /app/prisma-query-engine

ENV TZ=Asia/Shanghai

WORKDIR /app
CMD ["./bin/server"]

这下打包镜像只剩 34 M 了。分层如下:

Layer Size
dart + prisma runtime 9.2 M
dart 编译成品 6.7 M
prisma-query-engine 18.2 M

太牛了,只能说一句牛逼。这服务端尺寸不把 node 按在地上爆杀 😆

总结

借这个机会熟悉了一下 Docker,打包镜像从最初的 1.9 G 到最后的 34 M,可见选择合适的基础镜像,和只包含必须的依赖是多么重要。

其实本质上还是一个问题:如何确认一个可执行文件的依赖?请教了 Seven Du 大佬,回答也很简单:

  1. prisma-engines 的 cargo.toml 里面找 feat,几乎这些地方就是声明 so 库依赖。
  2. 在容器里面运行 prisma-query-engine 看报啥 so 库依赖的错误,逐个添加。

前者如果说不懂 rust 不知道,后者其实是可以想到的,只要简单的逆向思维一下就好。看来自己写代码还需要更多冷静的思考,而非忙碌于尝试。

参考

本文的 demo 仓库: Dart ORM (Prisma Client Dart) Docker Example

prisma-dart 官网: Prisma Client Dart (orm package) Doc


BTW,偶然听说了 Prisma 官方对 prisma-dart 项目的区别对待,有点难受。对外低调内敛并不意味着甘愿低人一头,官方应该对他们涉嫌歧视的行为做出解释,对所有的 Client 项目一视同仁,而不是只会向个别 maintainer 提要求。如果连社区的开拓贡献者都不被尊重,这样的社区又如何让人尊重呢?