从零开始写 Docker(六)---实现 mydocker run -v 支持数据卷挂载

本文为从零开始写 Docker 系列第六篇,实现类似 docker -v 的功能,通过挂载数据卷将容器中部分数据持久化到宿主机。
完整代码见:https://github.com/lixd/mydocker
欢迎 Star
推荐阅读以下文章对 docker 基本实现有一个大致认识:
- 核心原理:深入理解 Docker 核心原理:Namespace、Cgroups 和 Rootfs
- 基于 namespace 的视图隔离:探索 Linux Namespace:Docker 隔离的神奇背后
- 基于 cgroups 的资源限制
- 初探 Linux Cgroups:资源控制的奇妙世界
- 深入剖析 Linux Cgroups 子系统:资源精细管理
- Docker 与 Linux Cgroups:资源隔离的魔法之旅
- 基于 overlayfs 的文件系统:Docker 魔法解密:探索 UnionFS 与 OverlayFS
- 基于 veth pair、bridge、iptables 等等技术的 Docker 网络:揭秘 Docker 网络:手动实现 Docker 桥接网络
开发环境如下:
root@mydocker:~# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.2 LTS
Release: 20.04
Codename: focal
root@mydocker:~# uname -r
5.4.0-74-generic
注意:需要使用 root 用户
1. 概述
上一篇中基于 overlayfs 实现了容器和宿主机文件系统间的写操作隔离。但是一旦容器退出,容器可读写层的所有内容都会被删除。
那么,如果用户需要持久化容器里的部分数据该怎么办呢?
docker volume 就是用来解决这个问题的。
启动容器时通过-v参数创建 volume 即可实现数据持久化。
本节将会介绍如何实现将宿主机的目录作为数据卷挂载到容器中,并且在容器退出后,数据卷中的内容仍然能够保存在宿主机上。
具体实现主要依赖于 linux 的 bind mount 功能。
bind mount 是一种将一个目录或者文件系统挂载到另一个目录的技术。它允许你在文件系统层级中的不同位置共享相同的内容,而无需复制文件或数。
例如:
mount -o bind /source/directory /target/directory/
这样,/source/directory 中的内容将被挂载到 /target/directory,两者将共享相同的数据。对其中一个目录的更改也会反映到另一个目录。
基于该技术我们只需要将 volume 目录挂载到容器中即可,就像这样:
mount -o bind /host/directory /container/directory/
这样容器中往该目录里写的数据最终会共享到宿主机上,从而实现持久化。
如果你对云原生技术充满好奇,想要深入了解更多相关的文章和资讯,欢迎关注微信公众号。
搜索公众号【探索云原生】即可订阅
2. 实现
volume 功能大致实现步骤如下:
- 1)run 命令增加 -v 参数,格式个 docker 一致
- 例如 -v /etc/conf:/etc/conf 这样
- 2)容器启动前,挂载 volume
- 先准备目录,其次 mount overlayfs,最后 bind mount volume
- 3)容器停止后,卸载 volume
- 先 umount volume,其次 umount overlayfs,最后删除目录
注意:第三步需要先 umount volume ,然后再删除目录,否则由于 bind mount 存在,删除临时目录会导致 volume 目录中的数据丢失。
runCommand
首先在 runCommand 命令中添 -v flag,以接收 volume 参数。
var runCommand = cli.Command{Name: "run",Usage: `Create a container with namespace and cgroups limitmydocker run -it [command]`,Flags: []cli.Flag{cli.BoolFlag{Name: "it", // 简单起见,这里把 -i 和 -t 参数合并成一个Usage: "enable tty",},cli.StringFlag{Name: "mem", // 限制进程内存使用量,为了避免和 stress 命令的 -m 参数冲突 这里使用 -mem,到时候可以看下解决冲突的方法Usage: "memory limit,e.g.: -mem 100m",},cli.StringFlag{Name: "cpu",Usage: "cpu quota,e.g.: -cpu 100", // 限制进程 cpu 使用率},cli.StringFlag{Name: "cpuset",Usage: "cpuset limit,e.g.: -cpuset 2,4", // 限制进程 cpu 使用率},cli.StringFlag{ // 数据卷Name: "v",Usage: "volume,e.g.: -v /ect/conf:/etc/conf",},},/*这里是run命令执行的真正函数。1.判断参数是否包含command2.获取用户指定的command3.调用Run function去准备启动容器:*/Action: func(context *cli.Context) error {if len(context.Args()) < 1 {return fmt.Errorf("missing container command")}var cmdArray []stringfor _, arg := range context.Args() {cmdArray = append(cmdArray, arg)}tty := context.Bool("it")resConf := &subsystems.ResourceConfig{MemoryLimit: context.String("mem"),CpuSet: context.String("cpuset"),CpuCfsQuota: context.Int("cpu"),}log.Info("resConf:", resConf)volume := context.String("v")Run(tty, cmdArray, resConf, volume)return nil},
}
在 Run 函数中,把 volume 传给创建容器的 NewParentProcess 函数和删除容器文件系统的 DeleteWorkSpace 函数。
func Run(tty bool, comArray []string, res *subsystems.ResourceConfig, volume string) {parent, writePipe := container.NewParentProcess(tty, volume)if parent == nil {log.Errorf("New parent process error")return}if err := parent.Start(); err != nil {log.Errorf("Run parent.Start err:%v", err)return}// 创建cgroup manager, 并通过调用set和apply设置资源限制并使限制在容器上生效cgroupManager := cgroups.NewCgroupManager("mydocker-cgroup")defer cgroupManager.Destroy()_ = cgroupManager.Set(res)_ = cgroupManager.Apply(parent.Process.Pid, res)// 在子进程创建后才能通过pipe来发送参数sendInitCommand(comArray, writePipe)_ = parent.Wait()container.DeleteWorkSpace("/root/", volume)
}
NewWorkSpace
在原有创建过程最后增加 volume bind 逻辑:
- 1)首先判断 volume 是否为空,如果为空,就表示用户并没有使用挂载参数,不做任何处理
- 2)如果不为空,则使用 volumeUrlExtract 函数解析 volume 字符串,得到要挂载的宿主机目录和容器目录,并执行 bind mount
func NewWorkSpace(rootPath, volume string) {createLower(rootPath)createDirs(rootPath)mountOverlayFS(rootPath)// 如果指定了volume则还需要mount volumeif volume != "" {mntPath := path.Join(rootPath, "merged")hostPath, containerPath, err := volumeExtract(volume)if err != nil {log.Errorf("extract volume failed,maybe volume parameter input is not correct,detail:%v", err)return}mountVolume(mntPath, hostPath, containerPath)}
}
volumeExtract
语法和 docker run -v 一致,两个路径通过冒号分隔。
// volumeExtract 通过冒号分割解析volume目录,比如 -v /tmp:/tmp
func volumeExtract(volume string) (sourcePath, destinationPath string, err error) {parts := strings.Split(volume, ":")if len(parts) != 2 {return "", "", fmt.Errorf("invalid volume [%s], must split by `:`", volume)}sourcePath, destinationPath = parts[0], parts[1]if sourcePath == "" || destinationPath == "" {return "", "", fmt.Errorf("invalid volume [%s], path can't be empty", volume)}return sourcePath, destinationPath, nil
}
mountVolume
挂载数据卷的过程如下。
- 1)首先,创建宿主机文件目录
- 2)然后,拼接处容器目录在宿主机上的真正目录,格式为:
$mntPath/$containerPath- 因为之前使用了 pivotRoot 将
$mntPath作为容器 rootfs,因此这里的容器目录也可以按层级拼接最终找到在宿主机上的位置。
- 因为之前使用了 pivotRoot 将
- 3)最后,执行 bind mount 操作,至此对数据卷的处理也就完成了。
// mountVolume 使用 bind mount 挂载 volume
func mountVolume(mntPath, hostPath, containerPath string) {// 创建宿主机目录if err := os.Mkdir(hostPath, constant.Perm0777); err != nil {log.Infof("mkdir parent dir %s error. %v", hostPath, err)}// 拼接出对应的容器目录在宿主机上的的位置,并创建对应目录containerPathInHost := path.Join(mntPath, containerPath)if err := os.Mkdir(containerPathInHost, constant.Perm0777); err != nil {log.Infof("mkdir container dir %s error. %v", containerPathInHost, err)}// 通过bind mount 将宿主机目录挂载到容器目录// mount -o bind /hostPath /containerPathcmd := exec.Command("mount", "-o", "bind", hostPath, containerPathInHost)cmd.Stdout = os.Stdoutcmd.Stderr = os.Stderrif err := cmd.Run(); err != nil {log.Errorf("mount volume failed. %v", err)}
}
DeleteWorkSpace
删除容器文件系统时,先判断是否挂载了 volume,如果挂载了则删除时则需要先 umount volume。
注意:一定要要先 umount volume ,然后再删除目录,否则由于 bind mount 存在,删除临时目录会导致 volume 目录中的数据丢失。
func DeleteWorkSpace(rootPath, volume string) {mntPath := path.Join(rootPath, "merged")// 如果指定了volume则需要umount volume// NOTE: 一定要要先 umount volume ,然后再删除目录,否则由于 bind mount 存在,删除临时目录会导致 volume 目录中的数据丢失。if volume != "" {_, containerPath, err := volumeExtract(volume)if err != nil {log.Errorf("extract volume failed,maybe volume parameter input is not correct,detail:%v", err)return}umountVolume(mntPath, containerPath)}umountOverlayFS(mntPath)deleteDirs(rootPath)
}
umountVolume
和普通 umount 一致
func umountVolume(mntPath, containerPath string) {// mntPath 为容器在宿主机上的挂载点,例如 /root/merged// containerPath 为 volume 在容器中对应的目录,例如 /root/tmp// containerPathInHost 则是容器中目录在宿主机上的具体位置,例如 /root/merged/root/tmpcontainerPathInHost := path.Join(mntPath, containerPath)cmd := exec.Command("umount", containerPathInHost)cmd.Stdout = os.Stdoutcmd.Stderr = os.Stderrif err := cmd.Run(); err != nil {log.Errorf("Umount volume failed. %v", err)}
}
3.测试
下面来验证一下程序的正确性。
挂载不存在的目录
第一个实验是把一个宿主机上不存在的文件目录挂载到容器中。
首先还是要在 root 目录准备好 busybox.tar,作为我们的镜像只读层。
$ ls
busybox.tar
启动容器,把宿主机的 /root/volume 挂载到容器的 /tmp 目录下。
root@mydocker:~/feat-volume/mydocker# ./mydocker run -it -v /root/volume:/tmp /bin/sh
{"level":"info","msg":"resConf:\u0026{ 0 }","time":"2024-01-18T16:47:29+08:00"}
{"level":"info","msg":"busybox:/root/busybox busybox.tar:/root/busybox.tar","time":"2024-01-18T16:47:29+08:00"}
{"level":"info","msg":"mount overlayfs: [/usr/bin/mount -t overlay overlay -o lowerdir=/root/busybox,upperdir=/root/upper,workdir=/root/work /root/merged]","time":"2024-01-18T16:47:29+08:00"}
{"level":"info","msg":"mkdir parent dir /root/volume error. mkdir /root/volume: file exists","time":"2024-01-18T16:47:29+08:00"}
{"level":"info","msg":"mkdir container dir /root/merged//tmp error. mkdir /root/merged//tmp: file exists","time":"2024-01-18T16:47:29+08:00"}
{"level":"info","msg":"command all is /bin/sh","time":"2024-01-18T16:47:29+08:00"}
{"level":"info","msg":"init come on","time":"2024-01-18T16:47:29+08:00"}
{"level":"info","msg":"Current location is /root/merged","time":"2024-01-18T16:47:29+08:00"}
{"level":"info","msg":"Find path /bin/sh","time":"2024-01-18T16:47:29+08:00"}
新开一个窗口,查看宿主机 /root 目录:
root@DESKTOP-9K4GB6E:~# ls
busybox busybox.tar merged upper volume work
多了几个目录,其中 volume 就是我们启动容器是指定的 volume 在宿主机上的位置。
同样的,容器中也多了 containerVolume 目录:
/ # ls
bin dev home root tmp var
containerVolume etc proc sys usr
现在往 /tmp 目录写入一个文件
/ # echo KubeExplorer > tmp/hello.txt
/ # ls /tmp
hello.txt
/ # cat /tmp/hello.txt
KubeExplorer
然后查看宿主机的 volume 目录:
root@mydocker:~# ls /root/volume/
hello.txt
root@mydocker:~# cat /root/volume/hello.txt
KubeExplorer
可以看到,文件也在。
然后测试退出容器后是否能持久化。
退出容器:
/ # exit
宿主机中再次查看 volume 目录:
root@mydocker:~# ls /root/volume/
hello.txt
文件还在,说明我们的 volume 功能是正常的。
挂载已经存在目录
第二次实验是测试挂载一个已经存在的目录,这里就把刚才创建的 volume 目录再挂载一次:
root@mydocker:~/feat-volume/mydocker# ./mydocker run -it -v /root/volume:/tmp /bin/sh
{"level":"info","msg":"resConf:\u0026{ 0 }","time":"2024-01-18T17:02:48+08:00"}
{"level":"info","msg":"busybox:/root/busybox busybox.tar:/root/busybox.tar","time":"2024-01-18T17:02:48+08:00"}
{"level":"info","msg":"mount overlayfs: [/usr/bin/mount -t overlay overlay -o lowerdir=/root/busybox,upperdir=/root/upper,workdir=/root/work /root/merged]","time":"2024-01-18T17:02:48+08:00"}
{"level":"info","msg":"mkdir parent dir /root/volume error. mkdir /root/volume: file exists","time":"2024-01-18T17:02:48+08:00"}
{"level":"info","msg":"mkdir container dir /root/merged//tmp error. mkdir /root/merged//tmp: file exists","time":"2024-01-18T17:02:48+08:00"}
{"level":"info","msg":"command all is /bin/sh","time":"2024-01-18T17:02:48+08:00"}
{"level":"info","msg":"init come on","time":"2024-01-18T17:02:48+08:00"}
{"level":"info","msg":"Current location is /root/merged","time":"2024-01-18T17:02:48+08:00"}
{"level":"info","msg":"Find path /bin/sh","time":"2024-01-18T17:02:48+08:00"}
查看刚才的文件是否存在
/ # ls /tmp/hello.txt
/tmp/hello.txt
/ # cat /tmp/hello.txt
KubeExplorer
还在,说明目录确实挂载进去了。
接下来更新文件内容并退出:
/ # echo KubeExplorer222 > /tmp/hello.txt
/ # cat /tmp/hello.txt
KubeExplorer222
/ # exit
在宿主机上查看:
root@mydocker:~# cat /root/volume/hello.txt
KubeExplorer222
至此,说明我们的 volume 功能是正常的。
4. 小结
本篇记录了如何实现 mydocker run -v 参数,增加 volume 以实现容器中部分数据持久化。
一些比较重要的点:
首先要理解 linux 中的 bind mount 功能。
bind mount 是一种将一个目录或者文件系统挂载到另一个目录的技术。它允许你在文件系统层级中的不同位置共享相同的内容,而无需复制文件或数。
其次,则是要理解宿主机目录和容器目录之间的关联关系。
以 -v /root/volume:/tmp 参数为例:
-
1)按照语法,
-v /root/volume:/tmp就是将宿主机/root/volume挂载到容器中的/tmp目录。 -
2)由于前面使用了 pivotRoot 将
/root/merged目录作为容器的 rootfs,因此,容器中的根目录实际上就是宿主机上的/root/merged目录- 第四篇:
-
3)那么容器中的
/tmp目录就是宿主机上的/root/merged/tmp目录。 -
4)因此,我们只需要将宿主机
/root/volume目录挂载到宿主机的/root/merged/tmp目录即可实现 volume 挂载。
在清楚这两部分内容后,整体实现就比较容易理解了。
如果你对云原生技术充满好奇,想要深入了解更多相关的文章和资讯,欢迎关注微信公众号。
搜索公众号【探索云原生】即可订阅

完整代码见:https://github.com/lixd/mydocker
欢迎 Star
相关代码见 feat-volume 分支,测试脚本如下:
需要提前在 /root 目录准备好 busybox.tar 文件,具体见第四篇第二节。
# 克隆代码
git clone -b feat-volume https://github.com/lixd/mydocker.git
cd mydocker
# 拉取依赖并编译
go mod tidy
go build .
# 测试 查看文件系统是否变化
./mydocker run -it /bin/ls
./mydocker run -it -v /root/volume:/tmp /bin/sh
相关文章:
从零开始写 Docker(六)---实现 mydocker run -v 支持数据卷挂载
本文为从零开始写 Docker 系列第六篇,实现类似 docker -v 的功能,通过挂载数据卷将容器中部分数据持久化到宿主机。 完整代码见:https://github.com/lixd/mydocker 欢迎 Star 推荐阅读以下文章对 docker 基本实现有一个大致认识: …...
网站引用图片但它域名被墙了或者它有防盗链,我们想引用但又不能显示,本文附详细的解决方案非常简单!
最好的办法就是直接读取图片文件,用到php中一个常用的函数file_get_contents(图片地址),意思是读取远程的一张图片,在输出就完事。非常简单~话不多说,直接上代码 <?php header("Content-type: image/jpeg&quo…...
Java八股文(RabbitMQ)
Java八股文のRabbitMQ RabbitMQ RabbitMQ RabbitMQ 是什么?它解决了哪些问题? RabbitMQ 是一个开源的消息代理中间件,用于在应用程序之间进行可靠的异步消息传递。 它解决了应用程序间解耦、消息传递、负载均衡、故障恢复等问题。 RabbitMQ …...
科研学习|论文解读——一种用于短文本消息中的释义检测的深度网络模型(IPM, 2018)
论文原标题 A deep network model for paraphrase detection in short text messages 摘要 本文研究释义检测,即识别语义相同的句子。检测用自然语言编写的相似句子的能力对一些应用程序至关重要,如文本挖掘、文本摘要、剽窃检测、作者身份认证和问题回答。认识到这一…...
鸿蒙Harmony应用开发—ArkTS声明式开发(基础手势:Web)下篇
onRequestSelected onRequestSelected(callback: () > void) 当Web组件获得焦点时触发该回调。 示例: // xxx.ets import web_webview from ohos.web.webviewEntry Component struct WebComponent {controller: web_webview.WebviewController new web_webv…...
3月19日做题
[NPUCTF2020]验证🐎 if (first && second && first.length second.length && first!second && md5(firstkeys[0]) md5(secondkeys[0]))用数组绕过first1&second[1] 这里正则规律过滤位(Math.) (?:Math(?:\.\w)?) : 匹配 …...
Java8中Stream流API最佳实践Lambda表达式使用示例
文章目录 一、创建流二、中间操作和收集操作筛选 filter去重distinct截取跳过映射合并多个流是否匹配任一元素:anyMatch是否匹配所有元素:allMatch是否未匹配所有元素:noneMatch获取任一元素findAny获取第一个元素findFirst归约数值流的使用中…...
构建Helm chart和chart使用管道与函数简介
目录 一.创建helm chart(以nginx为例) 1.通过create去创建模板 2.查看模板下的文件 3.用chart模版安装nginx 二.版本更新和回滚问题 1.使用upgrade -f values.yaml或者命令行--set来设置 2.查看历史版本并回滚 三.helm模板内管道和函数 1.defau…...
深入理解OnCalculate函数的运行机制
文章目录 一、学习 OnCalculate 函数的运行原理的意义二、OnCalculate 函数原型三、OnCalculate 函数在MT4与MT5区别四、OnCalculate 函数的运行原理 一、学习 OnCalculate 函数的运行原理的意义 OnCalculate函数是MQL语言中的一个重要函数,它用于计算技术指标的值。…...
快速从0-1完成聊天室开发——环信ChatroomUIKit功能详解
聊天室是当下泛娱乐社交应用中最经典的玩法,通过调用环信的 IM SDK 接口,可以快速创建聊天室。如果想根据自己业务需求对聊天室应用的 UI界面、弹幕消息、礼物打赏系统等进行自定义设计,最高效的方式则是使用环信的 ChatroomUIKit 。 文档地址…...
nginx实现多个域名和集群
要通过Nginx实现多个域名和集群,你需要配置Nginx作为反向代理服务器,将来自不同域名的请求转发到集群中的相应后端服务器。下面是一个基本的配置示例,你可以根据自己的需求进行修改和扩展。 首先,确保你已经安装了Nginxÿ…...
C. Left and Right Houses
Problem - C - Codeforces 题目分析 <1>0:想被分割至左边; 1:想被分割至右边 <2>使得左右两侧均有一半及其以上的人满意(我*******) <3>答案若有多个,取最接近中间位置的答案 <4…...
缓存与内存:加速你的Python应用
在现代计算中,缓存和内存是提高程序性能的关键组件。在这篇文章中,我们将深入探讨这两个概念,了解它们是如何工作的,以及如何在Python中有效地使用它们来优化你的程序。 缓存与内存:加速你的Python应用 缓存和内存&…...
Go语言之函数、方法、接口
一、函数 函数的基本语法: func 函数名(形参列表)(返回值列表) {执行语句...return 返回值列表 } 1.形参列表:表示函数的输入 2.函数中的语句:表示为了实现某一功能的代码块 3.函数可以有返回…...
【Week Y2】使用自己的数据集训练YOLO-v5s
Y2-使用自己的数据集训练YOLO-v5s 零、遇到的问题汇总(1)遇到git的import error(2)Error:Dataset not found(3)Error:删除中文后,训练图片路径不存在 一、.xml文件里保存…...
蓝桥杯--基础(哈夫曼)
import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Scanner;public class BASIC28 {//哈夫曼书public static void main(String[] args) {Scanner Scannernew Scanner(System.in);int nScanner.nextInt();List<Integer&…...
【Redis内存数据库】NoSQL的特点和应用场景
前言 Redis作为当今最流行的内存数据库,已经成为服务端加速的必备工具之一。 NoSQL数据库采用了非关系型的数据存储模型,能够更好地处理海量数据和高并发访问。 内存数据库具有更快的读写速度和响应时间,因为内存访问速度比磁盘访问速度快…...
JavaScript基础知识2
求数组的最大值案例 let arr[2,6,1,7,400,55,88,100]let maxarr[0]let minarr[0]for(let i1;i<arr.length;i){max<arr[i]?maxarr[i]:maxmin>arr[i]?minarr[i]:min}console.log(最大值是:${max})console.log(最小值是:${min}) 操作数组 修改…...
Linux之线程同步
目录 一、问题引入 二、实现线程同步的方案——条件变量 1、常用接口: 2、使用示例 一、问题引入 我们再次看看上次讲到的多线程抢票的代码:这次我们让一个线程抢完票之后不去做任何事。 #include <iostream> #include <unistd.h> #inc…...
03 龙芯平台openstack部署搭建-keystone部署
#!/bin/bash #创建keystone数据库并授权,可通过mysql -ukeystone -ploongson验证授权登录 mysql -uroot -e “set password for rootlocalhost password(‘loongson’);” mysql -uroot -ploongson -e ‘CREATE DATABASE keystone;’ #本地登录 mysql -uroot -ploo…...
springboot 百货中心供应链管理系统小程序
一、前言 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,百货中心供应链管理系统被用户普遍使用,为方…...
uni-app学习笔记二十二---使用vite.config.js全局导入常用依赖
在前面的练习中,每个页面需要使用ref,onShow等生命周期钩子函数时都需要像下面这样导入 import {onMounted, ref} from "vue" 如果不想每个页面都导入,需要使用node.js命令npm安装unplugin-auto-import npm install unplugin-au…...
测试markdown--肇兴
day1: 1、去程:7:04 --11:32高铁 高铁右转上售票大厅2楼,穿过候车厅下一楼,上大巴车 ¥10/人 **2、到达:**12点多到达寨子,买门票,美团/抖音:¥78人 3、中饭&a…...
屋顶变身“发电站” ,中天合创屋面分布式光伏发电项目顺利并网!
5月28日,中天合创屋面分布式光伏发电项目顺利并网发电,该项目位于内蒙古自治区鄂尔多斯市乌审旗,项目利用中天合创聚乙烯、聚丙烯仓库屋面作为场地建设光伏电站,总装机容量为9.96MWp。 项目投运后,每年可节约标煤3670…...
Spring Boot面试题精选汇总
🤟致敬读者 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点睡觉 📘博主相关 🟧博主信息🟨博客首页🟫专栏推荐🟥活动信息 文章目录 Spring Boot面试题精选汇总⚙️ **一、核心概…...
leetcodeSQL解题:3564. 季节性销售分析
leetcodeSQL解题:3564. 季节性销售分析 题目: 表:sales ---------------------- | Column Name | Type | ---------------------- | sale_id | int | | product_id | int | | sale_date | date | | quantity | int | | price | decimal | -…...
是否存在路径(FIFOBB算法)
题目描述 一个具有 n 个顶点e条边的无向图,该图顶点的编号依次为0到n-1且不存在顶点与自身相连的边。请使用FIFOBB算法编写程序,确定是否存在从顶点 source到顶点 destination的路径。 输入 第一行两个整数,分别表示n 和 e 的值(1…...
在QWebEngineView上实现鼠标、触摸等事件捕获的解决方案
这个问题我看其他博主也写了,要么要会员、要么写的乱七八糟。这里我整理一下,把问题说清楚并且给出代码,拿去用就行,照着葫芦画瓢。 问题 在继承QWebEngineView后,重写mousePressEvent或event函数无法捕获鼠标按下事…...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
Caliper 负载(Workload)详细解析
Caliper 负载(Workload)详细解析 负载(Workload)是 Caliper 性能测试的核心部分,它定义了测试期间要执行的具体合约调用行为和交易模式。下面我将全面深入地讲解负载的各个方面。 一、负载模块基本结构 一个典型的负载模块(如 workload.js)包含以下基本结构: use strict;/…...
