优雅的 Dockerfile 是怎样炼成的?

Docker 简介

目前,Docker 主要有两个形态:Docker Desktop 和 Docker Engine。

Docker Desktop 是专门针对个人使用而设计的,支持 Mac(已支持arm架构的M系芯片) 和 Windows 快速安装,具有直观的图形界面,还集成了许多周边工具,甚至可以在docker engine 中直接创建出一个 kubernetes(kubernetes in docker方案,与 kind 类似),极大的方便个人用户使用。

不过,我并不推荐使用Docker Desktop,原因有两个:第一,Docker Desktop是商业化产品,有一些非通用的东西,不利于对容器相关技术的进一步学习。第二,Docker Desktop 只对个人学习免费,受条款限制不能商用,万一工作中使用可能会“踩到雷区”。

Docker Engine 恰恰和 Docker Desktop 相反,它完全免费,但目前还只能在 Linux 运行,只能使用命令行操作,缺乏辅助工具,需要用户自行 DIY 运行环境。实际上,Docker Engine 才是 Docker 真正的形态,也是核心功能,Docker Desktop 只也是在其基础之上封装而来的,以便为非IT用户提供了一个易于使用的界面和工具,以简化 Docker 的安装、配置和管理过程。但 Docker Engine 是现在各个公司在生产环境中实际使用的 Docker 产品,毕竟机房里 99% 的服务器运行的操作系统都是 Linux。

Docker 使用

Docker 安装

因为大多数用户主要使用 Linux 版本 Docker,这里也只列出 Linux 系统下 docker 的安装

目前 Linux 服务器市场主要有两类系统分庭抗礼,请按自己操作系统选择即可

  • RHEL系,代表系统有:RHEL(Redhat Enterprise Linux)、Centos、Fedora
  • Debian系,代表系统有:Debian、Ubuntu
# RHEL 系
yum install -y docker

# Debian 系
apt install -y docker

安装完毕以后执行以下命令确实是否完成

# 输出 Docker 客户端和服务器各自的版本信息
docker version

# 显示当前 Docker 系统相关的信息,如 CPU、内存、容器数量、镜像数量、容器运行时、存储文件系统等
docker info

使用 Docker 创建容器

docker run busybox echo hello world

启动 busybox 镜像,使用 echo 命令输出 hell word,这也正是 Solomon Hykes 在大会上所展示的最精彩的那部分。当使用 run 命令启动容器时,Docker Daemon 会先检查本地是存在镜像,若没有则从远端镜像仓库拉取,当然也可以使用 docker pull busybox命令将用到的镜像提前下载到本地。

image-20231104211527702

Docker 架构

下面的这张图来自 Docker 官网,精准地描述了 Docker Engine 的内部角色和工作流程:

img

刚才我所使用的 docker 命令实际是一个客户端 client,它的作用是与 Docker Engine 里的后台服务 Docker daemon 通信,并将我发起的各个指令(run、pull、build 等)转换为 Docker daemon 能够识别的命令并由 Docker daemon 真正执行相关任务。此外,镜像是存储在元旦仓库里,客户端无法直接访问镜像仓库,只能有 Docker daemon 访问。

因此,在 Docker Engine 里,真正干活的是默默运行在后台的 Docker daemon(是不是像极了在公司的你)。

容器化应用

在上面的例子中,我们运行的容器,显然不是从零开始的,而是要先拉取一个“镜像”(image),然后从这个“镜像”来启动容器。那么,“镜像”和“容器”到底是什么,两者又有什么关系呢?

在其他场合中我们也遇到过“镜像”一词,比如:光盘镜像、重装电脑时的硬盘镜像、使用 VMWare 时的虚拟机系统镜像。说到这里,有没有发现他们的一些相同点?是的,这些“镜像”都是只读的,不允许修改,它们是以标准格式存储了一系列的文件,只在我们需要的时候从中提取数据运行起来,而运行起来的这个东西可理解为“容器”。

容器是由操作系统动态创建,因此必然可以将它的初始化环境固定下来,保存成一个静态文件,这样就可以方便存放、传输、版本化管理了。

img

可以将镜像比喻为“样板间”,它把运行进程所需要的文件系统、依赖库、环境变量、启动参数等信息打包整合到一起,之后镜像文件无论放在哪里,操作系统都能根据这个“样板间”快速重建容器。

从功能上来看,镜像和常见的rpm、deb 等安装包一样,都打包了应用程序,但最大的不同点在于它里面不仅有应用程序的可执行文件,还有应用程序运行时的整个系统环境,开发者可以在 CentOS 系统上开发,然后打包成镜像,再去 Ubuntu 系统上运行,完全不需要考虑环境依赖的问题。

此外,镜像的完整名字由两个部分组成,名字和标签,中间用 : 连接起来。名字代表镜像仓库地址,标签也就是 tag 代表版本号。上面我们执行 docker run busybox ... 命令时没有指定标签,则 默认使用 latest 作为标签。

(base) 🐳  ➜  ~ docker images
REPOSITORY   TAG       IMAGE ID       CREATED        SIZE
busybox      latest    a416a98b71e2   3 months ago   4.26MB

常用容器操作

上面我们只介绍了 docker rundocker pull 命令,下面列举几个工作中常用的命令

########### 镜像相关 ###########
# 查看镜像
docker images
# 删除镜像
docker rmi busybox:latest

########### 容器相关 ###########
# 运行容器
## -id:后台运行(执行命令后终端依旧是)
## --name:设置容器名
## bash:容器中运行的命令
docker run -id --name busybox busybox:latest bash
## -it:开启一个交互式操作的 Shell,直接进入容器内部
docker run -it --name busybox busybox:latest bash
# 查看容器(正在运行)
docker ps
# 查看容器(全部容器,包含正在运行和已退出)
docker ps -a
# 进容器并执行 bash 命令
docker exec -it busybox bash
# 停止容器
docker stop busybox
# 启动容器
docker start busybox
# 删除容器
docker rm busybox

容器镜像详解

镜像内部并不是一个平坦的结构,而是由许多的镜像层组成的,每层都是只读不可修改的一组文件,相同的层可以在镜像之间共享,然后多个层像搭积木一样堆叠起来,再使用一种叫“Union FS 联合文件系统”的技术把它们合并在一起,就形成了容器最终看到的文件系统。

img

我们可以使用 docker inspect busybox:latest 查看 busybox 镜像的分层信息:

image-20231104215930409

通过这张图可以看到,busybox:latest 镜像里一共有 1 个 Layer,这相比其他镜像已经小很多了,在工作中遇到的镜像有5~10个 Layer 是再正常不过的事情了。

如果有体验过前面 docker 命令的话,这里会明白在使用 docker pull、docker rmi 等命令操作镜像的时候,那些“奇怪”的输出信息就是镜像里的各个 Layer。每次镜像操作 Docker 会检查是否有重复的层,如果本地已经存在就不会重复下载,如果层被其他镜像共享就不会删除,这样可以节约磁盘和网络成本。

Dockerfile 是什么

上面我们将镜像比喻为“样板间”,要造出这个“样板间”,就必然需要一个“施工图纸”,又它来规定怎样建造地基、铺设水电、开窗撘门等动作,而这个“施工图纸”就是 Dockerfile。

与镜像、容器相比而言,Dockerfile 非常普通,它仅仅是一个纯文本,只不过里面记录了一系列的构建指令,比如选择基础镜像、拷贝文件、运行命令等,几乎每个指令都会生成一个 Layer,而 Docker 顺序执行这个文件里的所有步骤,最后就会创建出一个新的镜像出来。

我们可以将上面运行 busybox 镜像输出 hello word 信息的容器封装为一个新的镜像,其 Dockerfile 如下(我这里将文件命名为 Dockerfile)

# 选择基础镜像
FROM busybox
# 启动容器时默认运行的命令
CMD echo "hello world"

接下来执行命令构建新的镜像,注意:docker 版本不同,构建镜像时的输出信息也不一定完全相同,

# -t:指定镜像名字
# -f:指定 Dockerfile 文件名,若为 Dockerfile 也省略该参数
docker build -t busybox:v1 -f Dockerfile .

image-20231104225741461

这里还需要特别注意命令的格式,用 -f 参数指定 Dockerfile 文件名,后面必须跟一个文件路径,叫做“构建上下文”(build’s context),这里只是一个简单的点号,表示当前路径的意思,构建上下文里的信息会被复制到容器中,所以建议这个构建上下文的目录只保留需要拷贝到容器中的内容。

查看新镜像信息

docker inspect busybox:v1

运行容器并查看输出信息

docker run -id --name busybox busybox:v1

image-20231104225949769

如何编写优雅的 Dockerfile

了解 Dockerfile 之后,下面来一起了解 Dockerfile 的一些常用指令和最佳实践,以便在今后的工作中少踩些坑。

Dockerfile 基础

假设当前目录有一个名为 main.go 的 golang 源码文件和 Dockerfile 文件

package main

import "fmt"

func main() {
	fmt.Println("hello world.")
}

Dockerfile 文件内容如下:

FROM golang:1.21.3
WORKDIR /root/
COPY main.go /root/
RUN go build -o hello-world main.go
CMD  ["/root/hello-world"]

构建镜像

docker build -t hello-world:go -f Dockerfile .

启动容器

docker run -id --name hello-world hello-world:go

image-20231105175452996

上面就是一个从开发到构建镜像再到运行服务的完整流程,接下来解释一下之前没有遇到过的 Dockerfile 中的 WORKDIR、COPY 和 RUNUN 命令。

WORKDIR 命令的意思是设置容器内部的默认工作目录(working directory),在指定 WORK /root/ 后,之后的 RUN、CMD、ENTRYPOINT 等命令将在该目录下执行。如果指定的目录不存在,Docker 将自动创建它。注意:大多数原生的基础镜像默认工作目录都是 /

在本机上开发时会产生源码、配置等文件,需要将它们打包进镜像里,这时就可以使用 COPY 或 ADD 命令,用法和 Linux 的 cp 差不多,不过拷贝的源文件必须是“构建上下文”路径里存在的文件。

RUN 通常会是 Dockerfile 里最复杂的指令,会包含很多的 Shell 命令,但 Dockerfile 里一条指令只能是一行。为了避免使用多个 RUN 指令导致镜像层过多且镜像较大,我们往往会尽量使用一个 RUN 指令,并且在每行的末尾使用续行符 \,命令之间也会用 && 来连接,这样保证在逻辑上是一行,就像下面这样:

RUN apt-get update \
    && apt-get install -y \
        build-essential \
        tar \
        vim \
        wget \
        binutils \
        curl \
        gcc \
        gcc-c++ \
    && apt-get clean all \
    && ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

多段构建

在上面的 Dockerfile 示例中,大家是否发现了一个问题:最终运行的程序是 hello-world 二进制文件,并不需要 main.go 文件和 golang 的环境,那这里保留这两类文件岂不是占用了一定的空间,且可能会带来一定的安全隐患。(生产环境容器镜像追求“最小化原则”),那么我们可以怎样解决这个问题呢?

方案一:在本机先通过 go build 命令将源码构建为二进制文件,再通过 Dockerfile 的 COPY 指令将二进制文件拷贝到镜像中:

# 本机编译
go build -o docker/hello-world main.go
FROM debian:12.2
WORKDIR /root/
COPY hello-world /root/
CMD  ["/root/hello-world"]

那么问题又来了,这里为了保证构件上下文的“最小化”,我们要单独创建一个目录,这个目录仅存在编译后的二进制文件和 Dockerfile。

(base) 🐳  ➜  example tree .
.
├── docker
│   ├── Dockerfile
│   └── hello-world
└── main.go

此外,在本机编译也需要依赖 golang 环境的,在工作中,构建任务往往运行在流水线中,开发人员仅需编写 Dockerfile 即可,编译、构建均在“构建机”执行。既然依赖构建机,保证构建机的一致性和稳定性又是一个比较重要的工作了,为了减少依赖项,我们可以通过“多段构建”,在一个文件中编写“两个” Dockerfile,第一个执行编译,第二个是构建出我们最终需要的镜像。

# 第一次构建(编译)
FROM golang:1.21.3 AS builder
## 设置工作目录
WORKDIR /root/
## 复制构建上下文的全部文件到指定目录(构建上下文包含源码 main.go)
COPY . /root/
# 执行编译
RUN go build -o hello-world main.go

# 第二次构建
FROM debian:12.2
## 设置工作目录
WORKDIR /root/
## 从第一次构建的命令中获取所需文件
COPY --from=builder /root/hello-world /root/
## 设置启动命令
CMD ["/root/hello-world"]

这样一来,既不依赖本机或“构建机”的编译环境,也不会将不需要的文件打包到最终镜像中。

最佳实践

虽然我们已经解决了编译环境和不必要依赖的问题,但依然还有一些没有考虑到的内容,下面我来全方位梳理 Dockerfile 的最佳实践。

基础镜像

构建镜像的第一条指令必须是 FROM,所以基础镜像的选择是非常关键的。基础镜像使用不当会对造成生产环境的诸多风险。如果应用程序可独立运行而不依赖系统库等,那么一般会选择scratch,就像著名的分布式键值存储 etcd 一样;如果关注的是镜像的安全和大小,那么一般会选择 Alpine;如果关注的是应用的稳定性,那么可能会选择 Debian、Ubuntu和CentOS;如果我们需要在镜像里将源码编译,那么可能会选择对应语言的基础镜像,如:golang:1.21.3、openjdk:22等

FROM scratch         # 选择 scratch 镜像
FROM debian:12.2     # 选择 Debian 镜像
FROM ubuntu:23.04    # 选择 Ubuntu 镜像
FROM alpine:3.18     # 选择Alpine镜像
FROM centos:8        # 选择 CentOS 镜像

对于以上基础镜像,如果我们程序很简单,不需要系统环境和依赖库等支撑,完全可以选择 scratch 镜像,就比如上面 go 语言版的 hello world 就可以使用该镜像,这样构建出的镜像会很小。结果如下图所示:

(base) 🐳  ➜  ~ docker images
REPOSITORY     TAG       IMAGE ID       CREATED         SIZE
hello-world    debian    bb7b64c8c159   2 minutes ago   118MB
hello-world    scratch   1a66709e9ad0   2 hours ago     1.8MB

对于需要系统环境和依赖库的应用程序(这也更符合大多数应用的场景),我个人推荐基础镜像的顺序是:debian -> ubuntu -> alpine --> centos,主要原因有以下几点:

  • centos 从 RHEL 9 起,其从 RHEL 复刻系统,转变为了 RHEL 前的验证系统(CentOS Stream),其稳定性还需进一步验证,因此个人将其放到了最后;
  • alpine 虽然容器符合“最小化”原则,但它是通过使用 musl libc 库而非常用 Linux 发行版用到的 glibc 库,可能存在一些兼容性问题。以 go 语言为例,在我们需要用到 cgo 时,如果使用 alpine 镜像可能会遇到一些奇怪的问题,若不了解 Linux 操作系统的 libc 库不建议轻易尝试;
  • 至于 debian 和 ubuntu,ubuntu 是在 debian 基础上进行定制和优化的另一个 Linux 发行版操作系统,而且很多主流应用(如:MySQL)的官方镜像都是使用 debian 作为基础镜像来构建的,因此这里更推荐使用 debian;同时 ubuntu 也因为其长期开源、社区活跃等特性排在前列。

总之,若不熟悉 Linux 底层知识,建议直接选择 debian 镜像。

此外,基础镜像尽量使用具体标签,而不是 latest ,因为每次新版本发布后,latest 标签都会指向新版本镜像。若本次构建和上次构建前,恰好基础镜像有过更新,那两次构建出的镜像并不相同,这对生产环境而言是非常危险的。

层最小化

比如,我有一个 Dockerfile,内容如下:

# 第一次构建
FROM golang:1.21.3 AS builder
WORKDIR /data/workspace/
## 复制代码至相关文件
COPY . /data/workspace/
## 下载依赖
RUN go mod tidy
## 编译(二进制文件统一命名为server, 业务也可自定义)
RUN mkdir cmd && go build -o /data/workspace/cmd/server

# 第二次构建
FROM debian:12.2
WORKDIR /data/workspace/
## 下载所需命令
RUN apt-get update
RUN apt-get install -y tar procps vim less which binutils lsof telnet iputils wget net-tools tcpdump nc lrzsz bind-utils
RUN ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN apt-get clean all
## 从第一次构建的命令中获取所需文件(二进制名统一定位server, 业务也可自定义)
COPY --from=builder /data/workspace/cmd/server /data/workspace/
## 设置启动命令
CMD ["/data/workspace/server"]

每一个 RUN 指令都是可缓存的执行单元。太多的 RUN 指令会增加镜像的层数,增大镜像体积。当使用包管理器(apt、yum、dnf)安装软件时,一般会先更新软件索引信息,然后再安装软件。个人更推荐将更新索引和安装软件放在同一个 RUN 指令中,这样可以形成一个可缓存的执行单元,修改后的 Dockerfile 内容如下:

# 第一次构建
FROM golang:1.21.3 AS builder
WORKDIR /data/workspace/
## 复制代码至相关文件
COPY . /data/workspace/
## 下载依赖
RUN go mod tidy
## 编译(二进制文件统一命名为server, 业务也可自定义)
RUN mkdir cmd && go build -o /data/workspace/cmd/server

# 第二次构建
FROM debian:12.2
WORKDIR /data/workspace/
## 下载所需命令
RUN apt-get update \
    && apt-get install -y tar procps vim less which binutils lsof telnet iputils wget net-tools tcpdump nc lrzsz bind-utils \
    && ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
    && apt-get clean all
## 从第一次构建的命令中获取所需文件(二进制名统一定位server, 业务也可自定义)
COPY --from=builder /data/workspace/cmd/server /data/workspace/
## 设置启动命令
CMD ["/data/workspace/server"]

还有,上面 RUN 命令中的最后一步 apt-get clean all 也很关键,该命令会将包管理工具维护的缓存清空,进一步减少镜像体积。而且,我更推荐在每一个 RUN 指令的末尾执行该命令,如果在下一条指令中执行,镜像的体积并不会减少。

此外,在多个 Dockerfile 中,指令相同的 layer 应尽量放在相同的步骤,这样多个 Dockerfile 构建时能够使用构建缓存,减少构建时长。

安全性

容器中的 root 和主机上的 root 相同,但会受到 docker 守护程序配置的限制。无论有什么限制,如果程序突破了容器,他也能够找到一种方法来获取访问主机的完整权限。

这里可以说一下我之前在维护 kubernetes 集群时的一个故事:因为工作交接没做好,同事离职后,集群中的一台服务器密码忘记了,这时我通过亲和性在该服务器启动了一个容器,并将服务器的 /etc 目录挂载到容器中,进入容器执行了修改服务器密码的命令,就成功修改了容器所在服务器的密码。由此可见,安全性是多么的重要。

因此,我们可以指定一个用户而不使用默认 root 用户,可以在一定程度上提升容器的安全性:

# 第一次构建
FROM golang:1.21.3 AS builder
WORKDIR /data/workspace/
## 复制代码至相关文件
COPY . /data/workspace/
## 下载依赖
RUN go mod tidy
## 编译(二进制文件统一命名为server, 业务也可自定义)
RUN mkdir cmd && go build -o /data/workspace/cmd/server

# 第二次构建
FROM debian:12.2
USER server
WORKDIR /home/server/
## 从第一次构建的命令中获取所需文件(二进制名统一定位server, 业务也可自定义)
COPY --from=builder /data/workspace/cmd/server /home/server/
## 设置启动命令(第一个server是用户目录,第二个server是二进制文件)
CMD ["/home/server/server"]

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/117236.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【教3妹学编程-算法题】117. 填充每个节点的下一个右侧节点指针 II

2哥 : 3妹,听说你昨天去面试了,怎么样啊? 3妹:嗨,别提了,让我回去等通知,估计是没有通知了, 还浪费我请了一天假。 2哥 : 你又请假了啊, 你是怎么跟你那个严厉的老板请假…

微服务架构之路1,服务如何拆分?使用微服务的注意事项?

目录 一、前言二、单体服务的弊端三、微服务化四、服务如何拆分?1、拆分原则2、拆分时机和拆分方法3、拆分实践 五、使用微服务的注意事项1、确保相关业务和利益相关者的支持2、确定微服务的拆分粒度3、遵循微服务架构的原则4、确保接口的稳定性5、关注数据一致性6、…

postMessage

A:端口3000 import React, { useEffect } from react;function App() {useEffect(() > {const childWindow document.getElementById(child).contentWindow;const sendMessageToChild () > {childWindow.postMessage("主页面消息", "http://localhost:…

【QT5之QFtp模块】编译及使用

下载 传送门:https://github.com/qt/qtftp 或者 git clone https://github.com/qt/qtftp.git 下载ZIP,解压待用。 编辑 使用QtCreator打开qtftp.pro; 修改如下: qtftp.pro中,将第21行注释; src/qftp.pro中,将第4行…

Java程序设计2023-第四次上机练习

8-1 三子棋 编写程序,实现简单的三子棋游戏。在三子棋中,双方在33的棋盘中轮流下棋,一方用*示,另一方用O表示。如果一方的3个棋子占据了同一行,同一列或者对角线,则该方获胜。如果棋盘已被棋子占满&#x…

设置DevC++支持c++11标准

1.点击编译选项 2. 设置语言标准 3.点击确认 4.测试代码 使用auto成功 测试!

【漏洞复现】Apache_HTTPD_多后缀解析漏洞

感谢互联网提供分享知识与智慧,在法治的社会里,请遵守有关法律法规 文章目录 1.1、漏洞描述1.2、漏洞复现1、基础环境2、漏洞验证 1.3、深度利用GetShell 1.4、修复建议 1.1、漏洞描述 Apache HTTPD 支持一个文件拥有多个后缀,并为不同后缀执…

统计学习方法 条件随机场

文章目录 统计学习方法 条件随机场随机场马尔可夫随机场定义因子分解 条件随机场定义参数化形式简化形式矩阵形式 概率预测问题前向-后向算法概率的计算期望值的计算 学习问题改进的迭代尺度法拟牛顿法 解码问题 统计学习方法 条件随机场 学习李航的《统计学习方法》时&#x…

STM32 IAP应用开发--bootloader升级程序

STM32 IAP应用开发--bootloader升级程序 Chapter1 STM32 IAP应用开发——通过串口/RS485实现固件升级(方式2)前言什么是IAP?什么是BootLoader? 方案介绍:1)bootloader部分:2)APP部分…

基于单片机的胎压监测系统的设计

收藏和点赞,您的关注是我创作的动力 文章目录 概要 一、系统整体设计方案二、 系统设计4.1 主流程图 三 系统仿真5.1 系统仿真调试实物 四、 结论 概要 本文以STC89C52单片机为控制核心,通过气压传感器模块对汽车各轮胎的胎压进行实时数据的采集与处理&…

【数据结构】树与二叉树(二):树的表示C语言:树形表示法、嵌套集合表示法、嵌套括号表示法 、凹入表示法

文章目录 5.1 树的基本概念5.1.1 树的定义5.1.2 森林的定义5.1.3 树的术语5.1.4 树的表示1.树形表示法2.嵌套集合表示法结构体创建树主函数 3.嵌套括号表示法结构体创建树嵌套括号表示法主函数 4.凹入表示法结构体创建树凹入表示法…

IDEA 设置代码注释模板

功能简介: 每次看别人代码时,面对毫无注释的类,除了头大还是头大, 以下提供了一种代码类注释模板 新建java类的时候,自动增加类注释,养成代码开发好习惯 效果展示: 代码模板: #if (…

多媒体应用设计师 2023年(含答案回忆版)

以下是小红书上的回忆版 软考考完疯狂回忆,多媒体应用设计师选择题 1.pattern 2.effective 3.merge 4.applications 5.graphic 6.udp 7.rtp 8.rtsp 9.10cm 10.永久 11…97 12.工作技术管理标准 13.管理型元数据 14.premiere 15.wave 16.500km/h 17.3M 18.44000 19.…

Capto2024专为Mac电脑设计的屏幕录制和视频编辑软件

不得不说视频编辑功能:Capto提供了多种视频编辑功能,例如剪辑、旋转、裁剪、调整音频和视频的音量、加入水印、添加注释等,你能够使用Capto编辑你的视频,使之更加专业和生动。有目共睹的是录制完成后,你能够使用Capto提…

深入理解WPF中的依赖注入和控制反转

在WPF开发中,依赖注入(Dependency Injection)和控制反转(Inversion of Control)是程序解耦的关键,在当今软件工程中占有举足轻重的地位,两者之间有着密不可分的联系。今天就以一个简单的小例子&…

Paddle炼丹炉炸了Unexpected BUS error encountered in DataLoader worker

Paddle训练报错,内存不足 python train.py -c config/ResNet_W18.yaml修改配置文件config/ResNet_W18.yaml # 原配置 loader:num_workers: 4use_shared_memory: True# 修改后 loader:num_workers: 2use_shared_memory: False

数据分析实战 | 关联规则分析——购物车分析

目录 一、数据及分析对象 二、目的及分析任务 三、方法及工具 四、数据读入 五、数据理解 六、数据预处理 七、生成频繁项集 八、计算关联度 九、可视化 一、数据及分析对象 数据集链接:Online Retail.xlsx 该数据集记录了2010年12月01日至2011年12月09日…

ChinaSoft 论坛巡礼 | 安全攸关软件的智能化开发方法论坛

2023年CCF中国软件大会(CCF ChinaSoft 2023)由CCF主办,CCF系统软件专委会、形式化方法专委会、软件工程专委会以及复旦大学联合承办,将于2023年12月1-3日在上海国际会议中心举行。 本次大会主题是“智能化软件创新推动数字经济与社…

贝锐向日葵亮相阿里云“云栖大会”:独创专利算法赋能全新云桌面

2023年10月31日-11月2日,一年一度的云栖大会如期举办,国产远程连接服务创领者贝锐受邀参与。活动现场,贝锐CTO张小峰进行了分享,宣布贝锐旗下国民级远程控制品牌“贝锐向日葵”与无影展开合作,同时全新的“云桌面”将于…

Docker 学习路线 4:Docker 基础知识

Docker是一个平台,简化了在轻量、可移植的容器中构建、打包和部署应用程序的过程。在本节中,我们将介绍Docker的基础知识、其组件以及您需要开始使用的关键命令。 容器是什么? 容器是一个轻量级、独立的可执行软件包,包含运行应…