docker容器内拉流不稳定的原因

93 阅读3分钟

参考链接:www.seafog.cn/archives/19…

问题

在docker容器内部署了图像识别应用,容器启动时执行程序中的拉流步骤发现时好时坏,而且拉流失败次数大于成功次数,对此很迷惑。

分析

怀疑是opencv依赖库没有缓存上,进入容器中执行echo $LD_LIBRARY_PATH 发现没问题

怀疑是容器内其他依赖或者网络的问题,在容器内安装了ffmpeg并试了一下拉流发现还是没问题

最后在网上又搜了下资料,发现容器启动时也需要先执行一些初始化操作,缓存包括host、dns等配置。因此怀疑可能是拉流的时候网络不通导致的。

解决

修改docker的配置文件,添加dns配置,把该加的都加上

sudo vim /etc/docker/daemon.json

"dns": ["8.8.8.8"]

接着在代码中对拉流操作添加重试,失败后release,最后发现确实稳定了好多,目前还没发现什么大问题

如果不行就在docker run的时候加上--network host,使用宿主机的网络配置。

重开

过了几天发现又变回了原样,这次不管怎么去尝试,总是时好时坏的,代码中打开异常模式查看一下报错信息。

查看opencv470对应位置的源码,猜测可能是backends没有获取到导致的

接下来尝试指定捕获api

发现还是拉流失败,怀疑可能是编译opencv的时候,依赖库不全导致的。

因为项目使用的jdk17,接下来尝试使用jdk17重新编译opencv470。

先下载opencv470然后解压,进入目录,创建build文件夹,进入build文件夹。

使用cmake构建opencv,CMAKE_INSTALL_PREFIX是自定义的构建后保存路径

cmake -D CMAKE_BUILD_TYPE=RELEASE \
      -D CMAKE_INSTALL_PREFIX=/userdata/opencv470 \
      -D BUILD_opencv_java=ON \
      -D JAVA_HOME=/usr/lib/jvm/java-17-openjdk-arm64 \
      ..

然后发现缺少ffmpeg的库,相关打印日志如下

于是开始安装一些需要的基础依赖

apt update
apt install build-essential cmake net-tools git vim unzip iputils-ping -y
apt install -y libgtk2.0-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev libswresample-dev libavresample-dev libgphoto2-dev
apt install python3-dev python3-numpy libtbb2 libtbb-dev libjpeg-dev libpng-dev libtiff-dev  libdc1394-22-dev
apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav ffmpeg

接下来删除build目录并重建,然后重新cmake,发现相关库都有了

接下来想继续编译时发现ant存在问题,这样编译不了Java的依赖库了,安装ant

apt install -y ant

但是发现还是不行,搜了一下相关帖子,发现了如下解决方法

blog.csdn.net/quantum7/ar…

重新链接ant

ln -snf /usr/share/ant/bin/ant /bin/ant

然后继续cmake

构建成功,接下来继续按照常规步骤进行编译和安装

make -j$(nproc) 
sudo make install

重新制作镜像然后启动,发现还是不行 !!!

接下来得改变思路了,不一定非得Java去拉流,可以在容器内起一个ffmpeg去拉流,然后持续保存在一个固定路径下面,接下来Java应用只需要去监听该路径下的文件变化就ok了。

Java可以使用文件监听器WatchService来实现对文件变化的感知,下图中右终端路径下1-13图片未被删除,是因为脚本先后启动顺序,漏掉了,这个部分无伤大雅。或者ffmpeg反复覆盖同一个文件。

考虑到开发板闪存反复读写可能会导致损坏,最好不要直接存硬盘,参考了网上的帖子,可以考虑额外挂载一个内存分区。

www.cnblogs.com/DragonStart…

接下来就是重新编写dockerfile和启动脚本

Dockerfile文件

# 使用官方的 Ubuntu 20.04 作为基础镜像
FROM ubuntu:20.04

# 设置工作目录
WORKDIR /usr/app

# 安装 ca-certificates(确保证书可用)
RUN apt-get update && \
    apt-get install -y ca-certificates && \
    rm -rf /var/lib/apt/lists/*

# 复制宿主机的 APT 源列表和配置文件到容器内
COPY ./sources.list /etc/apt/sources.list

RUN apt-get update

# 安装基础工具
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
        build-essential \
        cmake \
        net-tools \
        git \
        vim \
        unzip \
        iputils-ping

# 安装多媒体库
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
        libavcodec-dev \
        libavformat-dev \
        libswscale-dev \
        libswresample-dev \
        libavresample-dev \
        libgphoto2-dev \
        python3-dev \
        python3-numpy \
        libtbb2 \
        libtbb-dev \
        libjpeg-dev \
        libpng-dev \
        libtiff-dev \
        libdc1394-22-dev

# 安装 GStreamer 相关包
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
        libgstreamer1.0-dev \
        libgstreamer-plugins-base1.0-dev \
        libgstreamer-plugins-bad1.0-dev \
        gstreamer1.0-plugins-base \
        gstreamer1.0-plugins-good \
        gstreamer1.0-plugins-bad \
        gstreamer1.0-libav \
   	ffmpeg

# 安装 Java 开发环境
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
        openjdk-17-jdk

# 清理 APT 缓存
RUN rm -rf /var/lib/apt/lists/*

# 将外部的 JAR 包复制到容器内
COPY start-1.0.0-SNAPSHOT.jar .

# 将配置文件夹复制到容器内
COPY ./config ./config
COPY ./opencv-build ./opencv-build

# 更新环境变量 LD_LIBRARY_PATH
ENV PATH=/usr/app/opencv-build/bin:$PATH
ENV LD_LIBRARY_PATH=/usr/app/opencv-build/lib:$LD_LIBRARY_PATH

# 运行 ldconfig 来更新动态库缓存
RUN ldconfig

# 将脚本复制到容器中
COPY entrypoint.sh .
# 确保脚本可执行
RUN chmod +x /usr/app/entrypoint.sh
# 设置容器启动时运行的命令
ENTRYPOINT ["/usr/app/entrypoint.sh"]

entrypoint.sh脚本

#!/bin/bash
  
# 设置一些默认值
FFMPEG_INPUT="rtsp://username:password@ip:554/cam/realmonitor?channel=1&subtype=0"
TEMP_DIR="/mnt/memdrive"
OUTPUT_DIR="$TEMP_DIR/output" # 使用 tmpfs 中的目录作为输出路径
INTERVAL=100 # 每n帧保存一次

# 创建输出目录(如果不存在)
mkdir -p $TEMP_DIR
mkdir -p $OUTPUT_DIR

# 使用 ffmpeg 拉流,并每隔 n 帧保存一帧为图片到 tmpfs,放到后台执行
ffmpeg -i $FFMPEG_INPUT -vf "select='not(mod(n,$INTERVAL))',setpts=N/FRAME_RATE/TB" -q:v 2 "$OUTPUT_DIR/image-%04d.jpg" &

# 获取 ffmpeg 的进程 ID 并保存到变量
FFMPEG_PID=$!

# 启动 Java 应用程序
echo "Starting Java application..."
exec java -jar -Djava.library.path=/usr/app/opencv-build/lib /usr/app/start-1.0.0-SNAPSHOT.jar --spring.config.location=file:///usr/app/config/application.yaml

# 让脚本挂起,防止容器退出
tail -f /dev/null

Java代码也得做修改

public void processImagesFromDirectory(String imagePath) {
        Path dir = Paths.get(imagePath);
        if (!Files.isDirectory(dir)) {
            log.error("Provided path is not a directory: " + imagePath);
            return;
        }

        log.info("Starting to watch and process images from directory: " + imagePath);

        Thread thread = new Thread(() -> {
            try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
                dir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);

                while (true) {
                    WatchKey key;
                    try {
                        key = watchService.take(); // 阻塞等待事件
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        log.info("Image processing thread interrupted.");
                        return;
                    }

                    for (WatchEvent<?> event : key.pollEvents()) {
                        WatchEvent.Kind<?> kind = event.kind();
                        if (kind == StandardWatchEventKinds.OVERFLOW) {
                            continue;
                        }

                        @SuppressWarnings("unchecked")
                        WatchEvent<Path> ev = (WatchEvent<Path>) event;
                        Path filename = ev.context();

                        if (filename.toString().toLowerCase().endsWith(".jpg") || filename.toString().toLowerCase().endsWith(".png")) {
                            File imgFile = dir.resolve(filename).toFile();

                            try {
                                // 读取图片
                                Mat img = Imgcodecs.imread(imgFile.getAbsolutePath());
                                if (img.empty()) {
                                    log.error("Failed to read image file: " + imgFile.getAbsolutePath());
                                    continue;
                                }

                                // 将图像的副本放入队列
                                if (!waitForDetectImgQueue.offer(img.clone())) {
                                    log.warn("Image queue is full, dropping image.");
                                }

                                // 释放原始图像
                                img.release();

                                // 处理完成后删除图片文件
                                if (!imgFile.delete()) {
                                    log.warn("Failed to delete image file: " + imgFile.getAbsolutePath());
                                } else {
                                    log.info("Deleted image file: " + imgFile.getAbsolutePath());
                                }

                            } catch (Exception e) {
                                log.error("Error processing image file: " + imgFile.getAbsolutePath(), e);
                            }
                        }
                    }

                    boolean valid = key.reset();
                    if (!valid) {
                        break;
                    }
                }
            } catch (Exception e) {
                log.error("Error setting up watch service.", e);
            }
        });

        thread.start();
    }

容器启动指令

docker run -d --name uj2 --mount type=tmpfs,destination=/mnt/memdrive,tmpfs-size=100m ubuntu:v2

好!重新制作和启动后发现问题完美解决!!!

思路有了,大部分脚本和代码都是gpt帮忙写的,哈哈!