从Dart Shelf容器访问MySQL Docker容器时遇SocketException问题
背景
我正在使用Shelf库构建一个Dart服务器,希望它具备一定的可扩展性,以便项目就绪后能投入生产环境。为了简化生产部署,我决定使用Docker。
Dart Shelf服务器
以下是我的Dockerfile:
# Use latest stable channel SDK. FROM dart:stable AS build # Resolve app dependencies. WORKDIR /app COPY pubspec.* ./ RUN dart pub get # Copy app source code (except anything in .dockerignore) and AOT compile app. COPY . . RUN dart compile exe bin/server.dart -o bin/server # Build minimal serving image from AOT-compiled `/server` # and the pre-built AOT-runtime in the `/runtime/` directory of the base image. FROM scratch COPY --from=build /runtime/ / COPY --from=build /app/bin/server /app/bin/ # Start server. EXPOSE 8080 CMD ["/app/bin/server"]
(这是Shelf官方文档提供的基础Dockerfile)
带数据库的Docker-compose文件
我需要一个数据库,将其放在独立容器中。以下是我的docker-compose.yaml文件:
version: "3.7" services: db: restart: always image: mysql environment: MYSQL_USER: admin MYSQL_PASS: secret MYSQL_ROOT_PASSWORD: secret MYSQL_PASSWORD: secret MYSQL_DATABASE: mydatabase # volumes: # - toast-datavolume:/var/lib/mysql ports: - "3306:3306" networks: - mynetwork server: build: . ports: - "8000:8080" depends_on: - db environment: - MYSQL_HOST=db links: - db:mysql networks: - mynetwork networks: mynetwork: # volumes: # toast-datavolume:
(我注释了持久化部分,先确保功能正常)
Dart代码
以下是负责数据库连接的Dart代码:
try { print('try'); var settings = ConnectionSettings( host: 'db', port: 3306, user: 'admin', password: 'secret', db: 'mydatabase', timeout: Duration(seconds: 30), ); var conn = await MySqlConnection.connect(settings); var result = await conn.query('insert into users (name, email, age) values (?, ?, ?)', ['Bob', 'bob@bob.com', 25]); return 1; } catch (exception) { print(exception); return -1; }
错误信息
启动容器后一切正常,但触发这段代码时(通过curl请求),服务器容器日志中出现如下异常:
2023-06-07 11:45:14 2023-06-07T09:45:14.465441 0:00:00.011712 POST [400] /register 2023-06-07 11:46:42 try 2023-06-07 11:46:42 SocketException: Broken pipe (OS Error: Broken pipe, errno = 32), address = db, port = 33206
POST行是我的curl请求,第二行是print('try')的输出。
我已尝试的解决方法:
- 移除docker-compose.yaml中的network配置,使用默认网络
- 在Dart代码中将host设为mysql://db:3306
- 从本地机器登录MySQL容器(正常工作,说明问题出在Shelf服务器与MySQL容器的连接)
- 还尝试了其他很多方法,但已记不清,为此花费了2天时间调试
恳请各位帮忙解决!
解决方案
1. 修复MySQL用户权限问题
默认MySQL创建的admin用户仅允许localhost连接,而Dart服务器在独立容器中属于远程连接,需要授予跨容器访问权限:
- 进入MySQL容器:
docker exec -it <你的db容器名称> mysql -uroot -psecret
- 执行授权SQL:
GRANT ALL PRIVILEGES ON mydatabase.* TO 'admin'@'%' IDENTIFIED BY 'secret'; FLUSH PRIVILEGES;
(若要更安全,可将%替换为服务器容器所在的子网IP段,比如172.20.0.%)
2. 处理服务启动时序问题
depends_on仅保证容器启动顺序,不确保MySQL服务已完成初始化。当Dart服务器尝试连接时,MySQL可能还在加载,导致连接中断。可以通过两种方式解决:
方法A:在代码中添加重试逻辑
修改数据库连接代码,增加重试机制:
Future<int> connectToDb() async { int retries = 5; while (retries > 0) { try { print('尝试连接数据库...'); var settings = ConnectionSettings( host: 'db', port: 3306, user: 'admin', password: 'secret', db: 'mydatabase', timeout: Duration(seconds: 10), ); var conn = await MySqlConnection.connect(settings); var result = await conn.query('insert into users (name, email, age) values (?, ?, ?)', ['Bob', 'bob@bob.com', 25]); await conn.close(); return 1; } catch (exception) { print('连接失败,重试剩余次数:${retries-1}'); print(exception); retries--; await Future.delayed(Duration(seconds: 2)); } } return -1; }
方法B:使用等待脚本确保MySQL就绪
暂时放弃scratch镜像,改用dart:stable作为运行镜像,并安装wait-for-it脚本等待MySQL服务启动完成:
修改Dockerfile:
# Use latest stable channel SDK. FROM dart:stable AS build # Resolve app dependencies. WORKDIR /app COPY pubspec.* ./ RUN dart pub get # Copy app source code (except anything in .dockerignore) and AOT compile app. COPY . . RUN dart compile exe bin/server.dart -o bin/server # 使用dart:stable作为运行镜像 FROM dart:stable WORKDIR /app COPY --from=build /app/bin/server /app/bin/ # 安装wait-for-it脚本 RUN apt-get update && apt-get install -y wait-for-it # 等待MySQL服务就绪后启动服务器 EXPOSE 8080 CMD ["wait-for-it", "db:3306", "--timeout=30", "--", "/app/bin/server"]
3. 清理无效环境变量
docker-compose.yaml中的MYSQL_PASS是无效的,官方MySQL镜像仅识别MYSQL_PASSWORD,删除该冗余变量避免混淆。
4. 排查scratch镜像的潜在问题
scratch是极简空镜像,虽然Dart AOT编译为静态链接,但部分网络底层依赖可能缺失。如果上述步骤解决问题后,再尝试换回scratch镜像,若问题复发,说明需要补充必要系统文件,或改用轻量镜像如alpine配合Dart运行时。
内容的提问来源于stack exchange,提问作者Mathieu

