从docker v0.1.0开始

从docker v0.1.0开始

本文概述

本文讲述笔者在学习docker过程中的心路历程,从docker的整体框架开始初识docker,之后开始阅读v0.1.0版本的docker源码,然后跟着教程用go实现了一个mini的docker,最后再去回看docker的源码,总结而言,是一个理论指导实践,实践加深理论的理论与实践的循环学习过程。

初识docker

Docker is a set of platform as a service(PaaS) products that use OS-level virtualization to deliver software in packages called containers.

这是wikipedia上对docker的定义,这句话高瞻远瞩地从宏观上告诉了我们docker是什么,确实很难找到比这更为准确的定义,但是,OS-level的虚拟化技术又是指什么,为什么要有docker,容器又有什么用呢?

在接触docker之前一个相关的概念是虚拟机,它可以模拟完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。它与docker一样,都是一种虚拟化技术,不同点在于:

在这里插入图片描述

  • Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。它并不是完整的操作系统,只是相当于在正常进程外套了壳。容器内的进程接触到的各种资源都是虚拟的,但本质上都是在调用底层系统,不过通过命名空间等技术实现了与底层系统的有效隔离。
  • 虚拟机通过在物理主机上运行一个完整的操作系统来实现虚拟化。每个虚拟机都有自己的操作系统内核、系统进程和设备驱动程序。

简单而言,很多时候我们运行一些程序并不需要一个整个操作系统环境,只需要某些接口就能work,那从资源、从运行时间等角度考虑,就没必要模拟整个操作系统(虚拟机),只需要模拟一部分(容器)。

我们已经明确了这个东西是有用的, 那怎么用呢?

在这里插入图片描述

docker 有镜像(Image)、容器(Container)、仓库(Respository)三个基本概念。其实可以用代码库的概念来类比,仓库就如github、gitlab等公开或者私有的仓库,镜像就像是代码里的仓库,这是不变的,而容器就是我将仓库里的代码拉到的本地版本,可以本地修改,也可以通过某些操作将这种修改传递到仓库的镜像上。当然,镜像与容器更像是类与实例的关系。使用docker过程即,明确需求获取对应的仓库镜像,创建本地容器,根据业务的具体要求修改容器。

那这又是如何实现的呢,这样一个虚拟化的技术,这样一个从镜像到容器的过程docker又是怎么做的呢?

在这里插入图片描述

这个答案其实在docker的架构中都能找到答案,docker服从C/S架构。Clinet即用户与Docker Daemon通信的客户端,即命令行终端,其包装命令发送api请求。而Docker的服务端是送耦合的结构,各模块各司其职,有机组合。其中daemon是常驻后台运行的进程,接受客户端请求并管理docker容器,engine是真正处理客户端请求的后端请求。具体为:

  1. docker client和daemon建立通信,client发送请求给daemon
  2. daemon作为主体部分,提供server功能,能让其接受client的请求
  3. engine执行处理内部的一系列工作
  4. 每个工作是一个job形式存在,需要镜像时,从docker registry中下载,通过graphdrive镜像管理驱动下载镜像并存储(Graph形式)。
  5. networkdrive负责创建配置容器的网络
  6. 运行用户指令或者限制容器资源时,通过execdrive完成
  7. execdrive以及networkdrive通过libcontainer具体实现

从镜像到容器:docker run命令发生了什么?

(1) Docker Client接受docker run命令, 发送http请求给到server; (2)Docker Server接受以上HTTP请求,并交给mux.Router,mux.Router通过URL以及请求方法来确定执行该请求的具体handler; (3)mux.Router将请求路由分发至相应的handler,具体为PostContainersCreate;(4) PostContainersCreate中create job被创建 (5)create job调用graphdriver并将rootfs下镜像加载到docker容器对应位置 (6) 上述过程无误,则类似上述过程创建start job并交由networkdrive来实现,制止创建容器并最终执行用户要求启动的命令。详见参考连结

可以发现,本质上就是client与server的通信,是serve内部组件的通信,接收到client指令,server负责根据命令分发至对应handle, 对应handle创建相应job, 而engine则根据不同的job调用不同的后端组件来实现job, 实现job后再将结果返回到client。

容器的OS-level虚拟化支持主要是由libcontainer提供的, 它是Docker架构中一个使用Go语言设计实现的库,设计初衷是希望该库可以不依靠任何依赖,直接访问内核中与容器相关的API。正是由于libcontainer的存在,Docker可以直接调用libcontainer,而最终操纵容器的namespace、cgroups、apparmor、网络设备以及防火墙规则等。这一系列操作的完成都不需要依赖LXC或者其他包。

从docker v0.1.0开始

已经基本了解了docker的架构和它的一些模块,那接下来想要深入了解它最好的方法自然是阅读源码,当前github上moby仓库目前的版本代码量极大,并不容易阅读,于是从docker的v0.1.0版本代码开始,代码文件并不多,且已经具备了docker的核心功能。

入口函数位于/docker/docker.go

// ./docker/docker.go
func main() {
	if docker.SelfPath() == "/sbin/init" {
		// Running in init mode
		docker.SysInit()
		return
	}
	// FIXME: Switch d and D ? (to be more sshd like)
	fl_daemon := flag.Bool("d", false, "Daemon mode")
	fl_debug := flag.Bool("D", false, "Debug mode")
	flag.Parse()
	rcli.DEBUG_FLAG = *fl_debug
	if *fl_daemon {
		if flag.NArg() != 0 {
			flag.Usage()
			return
		}
		if err := daemon(); err != nil {
			log.Fatal(err)
		}
	} else {
		if err := runCommand(flag.Args()); err != nil {
			log.Fatal(err)
		}
	}
}

可以看到,代码运行首先判断docker可执行文件的绝对路径是否在/sbin/init目录下,如果在,则设置docker容器启动之前的环境 ,进行初始化。如果不存在则根据参入的命令行参数:去选择是启动docker deamon 还是执行 docker cli 的命令调用。

SysInit

系统的初始化设置位于/sysinit.go

//sysinit.go
func SysInit() {
	if len(os.Args) <= 1 {
		fmt.Println("You should not invoke docker-init manually")
		os.Exit(1)
	}
	var u = flag.String("u", "", "username or uid")
	var gw = flag.String("g", "", "gateway address")

	flag.Parse()

	setupNetworking(*gw)
	changeUser(*u)
	executeProgram(flag.Arg(0), flag.Args())
}

其中包含

  • 网络设置:根据参数中的网关ip添加网关路由
  • 用户设置:根据参数中的username或uid,通过系统调用设置uid和gid
  • 启动docker程序:启动docker程序,根据命令行参数决定启动docker deamon还是docker cli的命令

daemon

如果输出参数为-d 则启动daemon

// ./docker/docker.go
func daemon() error {
	// NewServer在commands.go中
	service, err := docker.NewServer()
	if err != nil {
		return err
	}
	return rcli.ListenAndServe("tcp", "127.0.0.1:4242", service)
}

启动daemon是在创建server对下岗,然后通过该对象启用tcp服务。而创建server其实就是在创建运行时对象

// command
func NewServer() (*Server, error) {
	rand.Seed(time.Now().UTC().UnixNano())
	if runtime.GOARCH != "amd64" {
		log.Fatalf("The docker runtime currently only supports amd64 (not %s). This will change in the future. Aborting.", runtime.GOARCH)
	}
	runtime, err := NewRuntime()
	if err != nil {
		return nil, err
	}
	srv := &Server{
		runtime: runtime,
	}
	return srv, nil
}

在创建runtime时,首先会在 /var/lib/docker目录下创建对应的文件:containers,graph文件夹,然后创建对应的镜像tag存储对象,通过名为lxcbr0的卡的网络创建网络管理,最后创建dockerhub的认证对象AuthConfig,详见代码注释。

func NewRuntime() (*Runtime, error) {
	return NewRuntimeFromDirectory("/var/lib/docker")
}

func NewRuntimeFromDirectory(root string) (*Runtime, error) {
	runtime_repo := path.Join(root, "containers")

	// 创建/var/lib/docker/containers目录
	if err := os.MkdirAll(runtime_repo, 0700); err != nil && !os.IsExist(err) {
		return nil, err
	}
	// 创建/var/lib/docker/graph目录,同时创建Graph对象
	g, err := NewGraph(path.Join(root, "graph"))
	if err != nil {
		return nil, err
	}
	// 创建var/lib/docker/repositories目录,同时创建TagStore对象
	repositories, err := NewTagStore(path.Join(root, "repositories"), g)
	if err != nil {
		return nil, fmt.Errorf("Couldn't create Tag store: %s", err)
	}
	//  通过名为lxcbr0的卡的网络创建网络管理
	netManager, err := newNetworkManager(networkBridgeIface)
	if err != nil {
		return nil, err
	}
	// 读取认证文件
	authConfig, err := auth.LoadConfig(root)
	if err != nil && authConfig == nil {
		// If the auth file does not exist, keep going
		return nil, err
	}
	// 创建runtime对象
	runtime := &Runtime{
		root:           root, // /var/lib/docker
		repository:     runtime_repo, // /var/lib/docker/containers
		containers:     list.New(), // container/list(list.New())双向链表
		networkManager: netManager, // NetworkManager
		graph:          g, // Graph
		repositories:   repositories, // TagStore
		authConfig:     authConfig, // AuthConfig
	}
	// 读取/var/lib/docker/containers目录,实际就是所有之前运行过的容器的目录
    // 检查配置中的id和所加载的容器id是否一样,以此判断容器信息是否被更改过
	if err := runtime.restore(); err != nil {
		return nil, err
	}
	return runtime, nil
}

创建好serve后 配置tcp服务端

// ./rcli/tcp.go
func ListenAndServe(proto, addr string, service Service) error {
    // 创建监听器
	listener, err := net.Listen(proto, addr)
	if err != nil {
		return err
	}
	log.Printf("Listening for RCLI/%s on %s\n", proto, addr)
	defer listener.Close()
	for {
        // 接受tcp请求
		if conn, err := listener.Accept(); err != nil {
			return err
		} else {
			go func() {
				if DEBUG_FLAG {
					CLIENT_SOCKET = conn
				}
                // 处理请求
				if err := Serve(conn, service); err != nil {
					log.Printf("Error: " + err.Error() + "\n")
					fmt.Fprintf(conn, "Error: "+err.Error()+"\n")
				}
				conn.Close()
			}()
		}
	}
	return nil
}

请求处理过程于/rcli/type.go LocalCall()所示, 具体为获取请求中的参数然后调用call,call根据参数是否有值来执行不同方法,如果没有参数,则执行runtime的help方法;如果有参数,进行参数的处理,处理逻辑:获取第二个参数,作为docker后的命令,然后获取命令之后的所有参数,整条命令进行日志打印输出,之后再通过cmd命令和反射技术找到对应的cmd所对应的方法,最后将参数传入方法,执行cmd对应的方法,结果返回connect中。connect在此作为io.Writer类型参数,命令结果将写入到其中。

runCommand

runCommand即客户端模式,详见代码注释

func runCommand(args []string) error {
	var oldState *term.State
	var err error
    // 检查标准输入模式,并确保环境变量NOARW非空
	if term.IsTerminal(0) && os.Getenv("NORAW") == "" {
		oldState, err = term.MakeRaw(0)
		if err != nil {
			return err
		}
		defer term.Restore(0, oldState)
	}
	// FIXME: we want to use unix sockets here, but net.UnixConn doesn't expose
	// CloseWrite(), which we need to cleanly signal that stdin is closed without
	// closing the connection.
	// See http://code.google.com/p/go/issues/detail?id=3345
    // TCP连接服务端并传递参数
	if conn, err := rcli.Call("tcp", "127.0.0.1:4242", args...); err == nil {
        // 启动一个goroutine从连接中接收stdout并写入os.Stdout
		receive_stdout := docker.Go(func() error {
			_, err := io.Copy(os.Stdout, conn)
			return err
		})
         // 启动一个goroutine将os.Stdin的数据发送到连接中
		send_stdin := docker.Go(func() error {
			_, err := io.Copy(conn, os.Stdin)
			if err := conn.CloseWrite(); err != nil {
				log.Printf("Couldn't send EOF: " + err.Error())
			}
			return err
		})
		if err := <-receive_stdout; err != nil {
			return err
		}
		if !term.IsTerminal(0) {
			if err := <-send_stdin; err != nil {
				return err
			}
		}
	} else {
        // 如果连接失败,创建一个本地的docker服务器
		service, err := docker.NewServer()
		if err != nil {
			return err
		}
        // 使用本地docker服务器调用rcli.LocalCall来执行命令,将标准输入和标准输出传递给docker服务器
		if err := rcli.LocalCall(service, os.Stdin, os.Stdout, args...); err != nil {
			return err
		}
	}
	if oldState != nil {
		term.Restore(0, oldState)
	}
	return nil
}

本章总结

从源码可以发现,docker v0.1.0完全基于lxc来实现。其初始化阶段先实现切换用户全线,添加网桥默认路由等功能,而后创建server端与client,server端通过docker daemon进行管理,将相关容器数据加载到runtime对象中,创建服务监听client端请求,并根据对应请求调用相应模块进行处理。

动手实现minidocker

纸上得来终觉浅,绝知此事要躬行。了解docker最好的当然是自己动手写一个。主要参考教程实现docker的虚拟化功能,具体包括namspace、cgroups、文件系统以及网络配置。

实验环境

Linux version 6.1.55-1-MANJARO + go1.21.2 linux/amd64

命名空间

    cmd := exec.Command("/bin/zsh")
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,
	}
	cmd.Env = os.Environ()
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err := cmd.Run()
	if err != nil {
		log.Fatal(err)
	}

启动一个新的shell并通过syscall.SysProAttr来配置进程属性,创建新的命名空间:

  • CLONE_NEWUTS:隔离主机名和域名等系统标识。具体表现为:容器内更改hostname 主机名不受影响 hostname newname
  • CLONE_NEWPID:隔离进程 ID。具体表现为:容器内echo $$查看当前pid号,从1开始。但是pd -ef仍能看见主机内进程:
    • 解决方案: mount -t proc proc /proc top确实看不到了 但是父节点挂了被卸载掉了
    • 先mount —make-rprivate / 再mount -t proc proc
  • CLONE_NEWNS:新的 Mount 命名空间,用于隔离文件系统挂载点。
  • CLONE_NEWNET:新的网络命名空间,用于隔离网络设备。具体表现为 route -n 没有ip没有路由表都需要重新配置
  • CLONE_NEWIPC:新的 IPC 命名空间,用于隔离进程间通信资源。具体表现为 主机ipcmk -Q创建消息队列 容器内ipcs无法看到

通过li nu x 的命令进行实验可以发现确实达到了各种隔离,且提高了对命名空间的理解。

但是当前仍然存在问题,即使用了主机内的文件系统,pwd仍能看到主机。

文件系统

下载文件系统https://cdimage.ubuntu.com/ubuntu-base/releases/16.04/release/并进行挂载。

	switch os.Args[1] {
	case "run":
		fmt.Println("run mode: run pid", os.Getpid(), "ppid", os.Getppid())
		initCmd, err := os.Readlink("/proc/self/exe") //读取当前进程可执行文件的路径
		if err != nil {
			fmt.Println("get init process error", err)
			return
		}
        
		os.Args[1] = "init"
		cmd := exec.Command(initCmd, os.Args[1:]...)

		cmd.SysProcAttr = &syscall.SysProcAttr{
			Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,
		}
		cmd.Env = os.Environ()
		cmd.Stdin = os.Stdin
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		err = cmd.Run()
		if err != nil {
			log.Fatal(err)
		}
		return
	case "init":
		fmt.Println("init mode: run pid", os.Getpid(), "ppid", os.Getppid())
		pwd, err := os.Getwd()
		fmt.Println("pwd:", pwd)
		if err != nil {
			fmt.Println("pwd", err)
			return
		}
		path := pwd + "/ubuntu"
		syscall.Mount("", "/", "", syscall.MS_BIND|syscall.MS_REC, "")
		if err := syscall.Mount(path, path, "bind", syscall.MS_BIND|syscall.MS_REC, ""); err != nil {
			fmt.Println("Mount", err)
			return
		}
		if err := os.MkdirAll(path+"/.old", 0700); err != nil {
			fmt.Println("mkdir", err)
			return
		}
		//syscall.PivotRoot会报错invalid argument  可以先执行 unshare -m命令,然后将 ubuntu/.old 文件夹删除
		//原因是systemd会将 fs 修改为 shared,pivot root 不允许 parent mount point和 new mount point 是 shared。
		//参考:https://www.retainblog.top/2022/10/26/%E4%BD%BF%E7%94%A8Golang%E5%AE%9E%E7%8E%B0%E8%87%AA%E5%B7%B1%E7%9A%84Docker%EF%BC%88%E4%BA%8C%EF%BC%89/

		err = syscall.PivotRoot(path, path+"/.old")
		if err != nil {
			fmt.Println("pivot root", err)
			return

		}
		//syscall.Chroot("./ubuntu-base-16.04.6-base-amd64") //Chroot切换进程文件系统 进程的根文件系统变化 但是进程的命名空间无变化
		syscall.Chdir("/")

		defaultMountFlags := syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV
		syscall.Mount("proc", "/proc", "proc", uintptr(defaultMountFlags), "")

		cmd := os.Args[2]
		err = syscall.Exec(cmd, os.Args[2:], os.Environ())
		if err != nil {
			fmt.Println("exec proc fail", err)
			return
		}
		fmt.Println("forever exec it")
		return
	}

	fmt.Println("hello world")
}

增加init模式配置容器,此外运行多个容器进程会使用同一份根文件系统目录 容器的修改会改变其他 使用联和文件系统, 运行完在/root/mnt会出现两个容器空间, 运行完自动销毁。

网络配置

容器内部网络无法访问互联网,通过网桥实现。具体的linux内部配置为,minidocker即通过调用系统接口的该配置的代码实现。

# 允许防火墙 路由转发
iptables -A FORWARD -j ACCEPT
# 内核允许路由转发, 修改值为1
sudo bash -c 'echo 1 > /proc/sys/net/ipv4/ip_forward'


# 创建网桥
brctl addbr br0
# 设置网桥开启状态
sudo ip link set br0 up #开启后通过ifconfig可以看到网桥 但是没有ip地址
# 为网桥分配ip地址
ip addr add 192.168.15.6/24 dev br0

# 创建veth设备 veth设备一般成对出现
sudo ip link add veth-red type veth peer name veth-red-br
sudo ip link add veth-bule type veth peer name veth-blue-br

# 开启主机上的veth网卡
sudo ip link set veth-red-br up
sudo ip link set veth-blue-br up # ifconfig可见

# 将veth-red放到red namespace里 进程的网络命名空间 先启动进程查看进程pid
sudo ip link set veth-red netns 进程id
sudo ip link set veth-blue netns 进程id

# veth设备一端链接到网桥上
sudo ip link set veth-red-br master br0
sudo ip link set veth-blue-br master br0

# 为veth设备添加ip 容器内
sudo ip link set veth-red up
sudo ip addr add 192.168.15.5/24 dev veth-red # 设置后ifconfig可以看到veth-red route -n 路由表也配置上了192.168.15.0网段

sudo ip link set veth-blue up
sudo ip addr add 192.168.14.7/24 dev veth-blue

# 现在两个容器可以访问 但是无法访问外网,需要通过网桥转发到eth0网卡
# 添加网关路由 容器内
ip route add defaule via 192.168.15.6 dev veth-red
ip route add default via 192.168.15.6 dev veth-blue

# 但是网络包出去后,回来后还需要
# 防火墙nat设置
iptables -t nat -A POSTROUTING -s 192.168.15.0/24 -j MASQUEREAD

cgroups

之前通过命名空间达到资源隔离,但是这种隔离无法对硬件资源的使用隔离,cgroups实现对cpu使用率进行限制。内核基本已经完成,只需要调用相关接口就行了。

cd /sys/fs/cgroup/ # 文件夹下包含cpu memory等 为cgroup的子系统

# cpu.cfs_period_us代表了cpu运行一个周期的时长
# 修改memory_limit_in_bytes文件可以设置程序的限制内存大小

cgroups的实现即通过系统接口实现对容器内部资源管理文件的修改。

再探docker源码

可以发现docker v0.1.0版本基本实现来功能,但其实还是不少问题,或者说功能点的欠缺。如采用的是aufs文件系统,而其在2.6.32 Linux内核中并不完全支持。后续的版本主要围绕其隔离程度、安全性、稳定性、功能等进行补充优化。

比如:

  • 从LXC转向自有的libcontainer:后续版本中,Docker从依赖LXC转向了自己的容器运行时接口libcontainer(现在已经演化为containerd),这改善了安全性和可移植性。
  • 安全性增强:引入了更多的安全特性,比如AppArmor、SELinux策略支持,以及后来的用户命名空间,这些都增加了容器的隔离性和安全性。
  • 网络和存储驱动:引入可插拔的网络和存储驱动,支持多种网络配置和持久化存储选项。
  • Orchestration and Scaling:引入了Docker Compose, Docker Swarm等工具,为容器编排和扩展提供支持。
  • 界面和API:改进了命令行界面(CLI),增加了REST API,为自动化和集成提供了更强的支持。
  • 开放和标准化:Docker开始参与并推动开放容器标准,例如Open Container Initiative (OCI)。

问题

可以发现,docker本质上还是基于linux的namespace和cgroup等接口实现的,但是也有docker for windows, 它是如何实现的呢。

在这里插入图片描述

在这里插入图片描述

可以发现两者结构很类似。与 Linux 类似,Windows 也新抽象出来了 CGroup 和 Namespace 的概念,并提供出一个新的抽象层次 Compute Service,即宿主机运算服务(Host Compute Service,hcs)。相较于底层可能经常重构的实现细节,hcs 旨在为外部(比如 Docker 引擎)提供较稳定的操作接口。

所以,本质上就是通过加抽象来解决问题。

总结

Docker is a set of platform as a service(PaaS) products that use OS-level virtualization to deliver software in packages called containers.

再去回看wikipad对docker的定义,说的确实不错,容器即隔离,docker是云原生时代必不可少的一项产品,上云需要虚拟化,而虚拟化需要docker,这种轻量的、可扩展的容器技术能够有效避免重复造轮子,能够极大了改善传统的环境配置问题,让软件服务能够即插即用。

参考链接

https://github.com/moby/moby

http://en.wikipedia.org/wiki/Docker_(software)

https://learn-docker-the-hard-way.readthedocs.io/zh-cn/latest/Part1/1_docker.go/

https://zhuanlan.zhihu.com/p/25773225

https://zhuanlan.zhihu.com/p/551753838

https://www.kancloud.cn/infoq/docker-source-code-analysis/80525

https://www.imooc.com/article/335433vo

https://www.yzktw.com.cn/post/1281202.html

https://insights.thoughtworks.cn/can-i-use-docker-on-windows/**# 从docker v0.1.0开始

  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
为了从零开始部署Prometheus,需要进行以下步骤: 1. 首先,需要安装DockerDocker Compose。可以在官方网站上找到安装说明。 2. 创建一个新的目录,用于存储Prometheus配置文件和Docker Compose文件。 3. 在该目录中创建一个名为`prometheus.yml`的文件,用于存储Prometheus的配置。可以使用以下示例配置: ```yaml global: scrape_interval: 15s scrape_configs: - job_name: 'prometheus' static_configs: - targets: ['localhost:9090'] - job_name: 'node' static_configs: - targets: ['node-exporter:9100'] ``` 这个配置文件告诉Prometheus每隔15秒抓取一次数据,并监控本地主机和Node Exporter。 4. 在该目录中创建一个名为`docker-compose.yml`的文件,用于定义Prometheus容器和Node Exporter容器。可以使用以下示例配置: ```yaml version: '3' services: prometheus: image: prom/prometheus:v2.22.0 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml ports: - 9090:9090 depends_on: - node-exporter node-exporter: image: prom/node-exporter:v1.0.1 ports: - 9100:9100 ``` 这个配置文件告诉Docker Compose创建两个服务:Prometheus和Node Exporter。Prometheus服务使用Prometheus v2.22.0镜像,将本地的`prometheus.yml`文件挂载到容器中,并将容器的9090端口映射到主机的9090端口。Node Exporter服务使用Prometheus Node Exporter v1.0.1镜像,并将容器的9100端口映射到主机的9100端口。 5. 在终端中导航到该目录,并运行以下命令启动Docker Compose: ```shell docker-compose up -d ``` 这个命令将启动Prometheus和Node Exporter容器,并将它们作为后台服务运行。 6. 现在可以通过浏览器访问`http://localhost:9090`来访问Prometheus的Web界面,并开始监控主机和Node Exporter的指标。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值