2024年3月

MYSQL 一个事务在提交的时候能够保证binlog和redo log是同时提交的,并且能在宕机恢复后保持binlog 和redo log的一致性。

先来看看什么是redo log 和binlog,以及为什么要保持它们的一致性。

什么是redo log,binlog

redo log
是innodb引擎层产生的日志, MYSQL从磁盘读取数据的单位是一页,当修改页中某条数据时,该行所在的数据页就变成了
脏页
,由于脏页并不会立马刷新到磁盘,所以
redo log会记录下数据页进行了哪些变动,用于服务崩溃时的数据恢复
。redo log是固定大小的,由多个文件组成一个环形的结构,

image.png

redo log由两个指针,write pos 和checkpoint,都是顺时针移动,write pos 记录redo log当前写入的位置, checkpoint往前移动,就代表就移动过的redo log记录的脏页刷新到磁盘上。所以,
write pos

checkpoint
之间的位置就代表redo log 还可以写的空间大小,当write pos等于checkpoint时,MYSQL则必须等待脏页刷新完毕后才能继续进行修改操作。

binlog
是mysql server服务层产生的日志。两者的用途也不一样。
binlog
则主要用于数据库的备份,主从同步。binlog记录的是行变化,记录格式也有3种,statement,row,mixed,这里就不细讲了。

在了解了redo log 和binlog的含义和各自的作用后,我们先来看看它们在一次sql更新中是如何运作的。

sql 更新过程详解

来看下在一次事务过程中,它们的工作机制。假设我们在进行修改操作,那么可以用下面的流程图来表示,

image.png

1
,首先判断要修改的数据是否在内存里,没有的话就从磁盘读取到内存。

2
,写入redo log,注意这里写入的redo log仅仅是prepare状态,只有等到正式提交的时候才会变成commit状态。并且写入redo log也不是直接落盘,其实是写入到了
redo log buffer
中,落盘时机受到
innodb_flush_log_at_trx_commit
参数控制。

MYSQL会有一个后台线程,定时刷新redo log buffer 中的数据到磁盘上。除此以外,当
innodb_flush_log_at_trx_commit 值为1
时 redo log则会在在prepare阶段将redo log buffer 中的数据落入磁盘。



注意

volume-by-bind-mount.png

本文为从零开始写 Docker 系列第六篇,实现类似 docker -v 的功能,通过挂载数据卷将容器中部分数据持久化到宿主机。


完整代码见:
https://github.com/lixd/mydocker
欢迎 Star

推荐阅读以下文章对 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 limit
			mydocker 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.判断参数是否包含command
		2.获取用户指定的command
		3.调用Run function去准备启动容器:
	*/
	Action: func(context *cli.Context) error {
		if len(context.Args()) < 1 {
			return fmt.Errorf("missing container command")
		}

		var cmdArray []string
		for _, 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 volume
	if 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,因此这里的容器目录也可以按层级拼接最终找到在宿主机上的位置。
  • 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 /containerPath
	cmd := exec.Command("mount", "-o", "bind", hostPath, containerPathInHost)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if 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/tmp
	containerPathInHost := path.Join(mntPath, containerPath)
	cmd := exec.Command("umount", containerPathInHost)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if 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

Pandas
无疑是我们数据分析时一个不可或缺的工具,它以其强大的数据处理能力、灵活的数据结构以及易于上手的API赢得了广大数据分析师和机器学习工程师的喜爱。

然而,随着数据量的不断增长,如何高效、合理地管理内存,确保
Pandas
DataFrame
在运行时不会因内存不足而崩溃,成为我们每一个人必须面对的问题。

在这个信息爆炸的时代,数据规模呈指数级增长,如何优化内存使用,不仅关乎到程序的稳定运行,更直接关系到数据处理的效率和准确性。通过本文,你将了解到一些实用的内存优化技巧,帮助你在处理大规模数据集时更加得心应手。

1. 准备数据

首先,准备一些包含各种数据类型的测试数据集。
封装一个函数(
fake_data
),用来生成数据集,数据集中包含后面用到的几种字段。

import pandas as pd
import numpy as np

def fake_data(size):
    """
    根据测试数据集:
    age:整数类型数值
    grade:有限个数的字符串
    qualified:是否合格
    ability:能力评估,浮点类型数值
    """
    df = pd.DataFrame()
    df["age"] = np.random.randint(1, 30, size)
    df["grade"] = np.random.choice(
        [
            "一年级",
            "二年级",
            "三年级",
            "四年级",
            "五年级",
            "六年级",
        ],
        size,
    )
    df["qualified"] = np.random.choice(["合格", "不合格"], size)
    df["ability"] = np.random.uniform(0, 1, size)

    return df

2. 检测内存占用

使用上面封装的函数(
fake_data
)先构造一个包含
一百万条
数据的
DataFrame

df = fake_data(1_000_000)
df.head()

image.png

看看优化前的内存占用情况:

df.info()

image.png
内存占用大约
26.7MB
左右。

3. 优化内存

接下来,我们开始一步步优化
DataFrame
的内存占用,
并测试每一步优化之后的内存使用情况和运行性能变化。

3.1. 优化整型数据

首先,优化整型数据的内存占用,也就是测试数据中的
年龄

age
)字段。
从上面
df.info()
的结果中,我们可以看出,
age
的类型是
int32
(也就是用32位,8个字节来存储整数)。
对于
年龄
来说,用不到这么大的整数,用
int8
(数值范围:-128~127)来存储绰绰有余。

df["age"] = df["age"].astype("int8")
df.info()

image.png
优化之后,内存占用从
26.7+ MB
减到
23.8+ MB

3.2. 优化浮点型数据

接下来优化
浮点类型
数据,也就是测试数据中的
能力评估值

ability
)。
测试数据中
ability
的值是6位小数,类型是
float64

转换成
float16
可能会改变值,所以这里转换成
float32

df["ability"] = df["ability"].astype("float32")
df.info()

image.png
优化之后,内存占用进一步从
23.8+ MB
减到
20.0+ MB

3.3. 优化布尔型数据

接下来,优化测试数据中的
是否合格

qualified
),
这个值虽然是
字符串类型
,但是它的值只有两种(
合格

不合格
),所以可以转换成
布尔类型

df["qualified"] = df["qualified"].map({"合格": True, "不合格": False})
df.info()

image.png
优化之后,内存占用进一步从
20.0+ MB
减到
13.4+ MB

3.4. 使用category类型

最后,我们再优化剩下的字段--
年级

grade
)。

这个字段也是字符串,不过它的值只有
6个
,虽然无法转换成布尔类型(布尔类型只有两种值
True

False
),但是它可以转换为
pandas
中的
category
类型。

df["grade"] = df["grade"].astype("category")
df.info()

image.png
优化之后,内存占用进一步从
13.4+ MB
减到
6.7+ MB

4. 总结

各类字段优化之后,内存占用从刚开始的
26.7+ MB
减到
6.7+ MB
,优化的效果非常明显。

仅仅是数据类型的简单调整,就带来了如此之大的内存效率提升,
这也给我们带来启示,在数据分析的过程中,构造
DataFrame
时,也可以根据数值的范围,特点等,
来赋予它合适的类型,不要一味简单的使用字符串,或者默认的整数(
int32
),默认的浮点(
float64
)等类型。

4)Playbook

4.1)Playbook 介绍

PlayBook

ad-hoc
相比,是一种完全不同的运用 Ansible 的方式,类似与 Saltstack 的 state 状态文件。ad-hoc 无法持久使用,PlayBook 可以持久使用。
PlayBook 剧本是 由一个或多个 "Play" 组成 的列表
Play 的主要功能在于将预定义的一组主机,装扮成事先通过 Ansible 中的 Task 定义好的角色。
从根本上来讲,所谓的 Task 无非是调用 Ansible 的一个 module。将多个 Play 组织在一个 PlayBook 中,即可以让它们联合起来按事先编排的机制完成某一任务。

PlayBook 文件是采用 YAML 语言 编写的。

4.1.1)PlayBook 核心元素

  • Host:
    执行的远程主机列表
  • Tasks:
    任务集
  • Varniables:
    内置变量或自定义变量在 PlayBook 中调用
  • Templates:
    模板文件,即使用模板语法的文件,比如配置文件等
  • Handlers:
    和 notity 结合使用,由特定条件触发的操作,满足条件方才执行,否则不执行
  • Tags:
    标签,指定某条任务执行,用于选择运行 PlayBook中的部分代码。

PlayBook
翻译过来就是
剧本
,可以简单理解为使用不同的模块完成一件事情,
具体 PlayBook 组成如下

  • Play
    :定义的是主机的角色
  • Task
    :定义的是具体执行的任务
  • PlayBook
    :由一个或多个 Play 组成,一个 Play 可以包含多个 Task 任务

image.png

4.1.2)PlayBook 优势

  • 功能比 ad-hoc 更全
  • 能很好的控制先后执行顺序,以及依赖关系
  • 语法展现更加的直观
  • ad-hoc 无法持久使用,PlayBook 可以持久使用

4.1.3)PlayBook 语法

PlayBook 的配置语法是由 yaml 语法描述的,扩展名是 yml 或 yaml,遵循 yaml 格式

  • 缩进:
    YAML 使用固定的缩进风格表示层级结构,每个缩进由两个空格组成,不能使用 Tab
  • 冒号:
    以冒号结尾的除外,其他所有冒号后面所有必须有空格
  • 短横线:
    表示列表项,使用一个短横杠加一个空格,多个项使用同样的缩进级别作为同一列表

4.2)YAML 语言

4.2.1)YAML 语言介绍

YAML:
YAML Ain't Markup Language,即 YAML 不是标记语言。
不过,在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"( 仍是一种标记语言 )

YAML 是一个可读性高的用来表达资料序列的格式。

YAML 参考了其他多种语言,包括:XML、C 语言、Python、Perl 以及电子邮件格式 RFC2822 等。Clark Evans 在 2001 年在首次发表了这种语言,另外 Ingy döt Net 与 Oren Ben-Kiki 也是这语言的共同设计者,目前很多最新的软件比较流行采用此格式的文件存放配置信息,如:Ubuntu,Anisble,Docker,Kubernetes 等

YAML 官方网站:
http://www.yaml.org
Ansible 官网:
https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html

4.2.2)YAML 语言特性

  • YAML 的可读性好
  • YAML 和脚本语言的交互性好
  • YAML 使用实现语言的数据类型
  • YAML 有一个一致的信息模型
  • YAML 易于实现
  • YAML 可以基于流来处理
  • YAML 表达能力强,扩展性好

4.2.3)YAML 语法简介

  1. 在单一文件第一行,用连续** 三个连字号 "-" 开始**
  2. 还有 选择性的连续三个点号 ( ... ) 用来
    表示文件的结尾
  3. 次行开始正常写 Playbook 的内容,一般建议写明该 Playbook 的功能描述
  4. 可以使用 # 号注释代码
  5. 缩进必须是统一的,不能空格和 Tab 混用
    ( 正常使用两个空格缩进 )
  6. 缩进的级别也必须是一致的,同样的缩进代表同样的级别,程序判别配置的级别是通过缩进结合换行来实现的
  7. YAML 文件内容是区别大小写的,key/value 的值
    均需大小写敏感
  8. 多个 key/value 可同行写也可换行写
    ,同行使用,分隔
  9. key 后面冒号要加一个空格,比如:key: value
  10. value 可是个字符串,也可是另一个列表
  11. YAML 文件扩展名通常为 yml 或 yaml

4.2.4)支持的数据类型

YAML 支持以下
常用几种数据类型

  • 标量:
    单个的、不可再分的值
    • 示例:
      age: 18
  • 对象:
    键值对的集合,又称为映射(mapping)/ 哈希(hashes) /
    字典
    (dictionary)
    • 示例:
      account:
  • 数组:
    一组按次序排列的值,又称为序列(sequence) /
    列表
    (list)
    • 示例:
      course: [ linux , golang , python ]

4.2.4.1)标量:scalar

方式一:
键值对

name: wang
age: 18

方式二:
使用缩进方式

name:
  wang
age:
  18

标量是最基本的,不可再分的值,包括:

  • 字符串
  • 布尔值
  • 整数
  • 浮点数
  • Null
  • 时间
  • 日期

4.2.4.2)字典:Dictionary

字典由多个 key 与 value 构成,key 和 value 之间用

分隔

并且,后面有一个空格,所有 k/v 可以放在一行,或者每个 k/v 分别放在不同行

格式

account: { name: wang, age: 30 }

使用缩进方式

account: 
  name: wang
  age: 18

范例:

# 不同行
# An employee record
name: Example Developer
job: Developer
skill: Elite(社会精英)

# 同一行, 也可以将 key:value放置于{}中进行表示,用,分隔多个key:value
# An employee record
{name: "Example Developer", job: "Developer", skill: "Elite"}

4.2.4.3)列表:List

列表由多个元素组成,每个元素放在不同行,且元素前均使用 "-" 打头,并且 - 后有一个空格

或者将所有元素用 [ ] 括起来放在同一行

// 格式
course: [ linux , golang , python ]

// 也可以写成以 - 开头的多行
course:
 - linux
 - golang
 - python
 
// 数据里面也可以包含字典
course:
 - linux: manjaro
 - golang: gin
 - python: django

范例:

# 不同行, 行以-开头, 后面有一个空格
# A list of tasty fruits
- Apple
- Orange
- Strawberry
- Mango

# 同一行
[Apple,Orange,Strawberry,Mango]

范例:
YAML 表示一个家庭

name: John Smith
age: 41
gender: Male
spouse: { name: Jane Smith, age: 37, gender: Female } # 1) 写在一行里
  name: Jane Smith   # 2) 也可以写成多行
  age: 37
  gender: Female
  
children: [ {name: Jimmy Smith,age: 17, gender: Male}, {name: Jenny Smith, age: 13, gender: Female}, {name: hao Smith, age: 20, gender: Male } ]  # 写在一行
  - name: Jimmy Smith    # 3) 写在多行, 更为推荐的写法
    age: 17
    gender: Male
  - {name: Jenny Smith, age: 13, gender: Female}
  - {name: hao Smith, age: 20, gender: Male }    

4.2.5)三种常见的数据格式

参考:
https://juejin.cn/post/7041815877825593374

XML:Extensible Markup Language,可扩展标记语言,可用于数据交换和配置。
JSON:JavaScript Object Notation,JavaScript 对象表记法,主要用来数据交换或配置,不支持注释。
YAML:YAML Ain't Markup Language YAML 不是一种标记语言, 主要用来配置,大小写敏感,不支持 Tab。


也可以用工具互相转换,参考网站:
https://www.json2yaml.com/
http://www.bejson.com/json/json2yaml/

4.3)Playbook 核心组件

官方文档:
https://docs.ansible.com/ansible/latest/reference_appendices/playbooks_keywords.html#playbook-keywords

一个 PlayBook 中由多个组件组成,其中所用到的 **常见组件类型 **如下:

  • Hosts
    :执行的 远程主机列表
  • Tasks:
    任务集,由多个 task 的元素组成的列表实现,每个 task 是一个字典,一个完整的代码块功能需最少元素需包括 name 和 task,一个 name 只能包括一个 task
  • Variables:
    内置变量或自定义变量 在 PlayBook 中调用
  • Templates:
    模板,可替换模板文件中的变量并实现一些简单逻辑的文件
  • Handlers

    notify
    结合使用,由特定条件触发的操作,满足条件方才执行,否则不执行
  • Tags:
    标签 指定某条任务执行,用于选择运行 playbook 中的部分代码。ansible 具有幂等性,因此会自动跳过没有变化的部分,即便如此,有些代码为测试其确实没有发生变化的时间依然会非常地长。此时,如果确信其没有变化,就可以通过tags 跳过此些代码片段。

4.3.1)hosts 组件

Hosts:
PlayBook 中的每一个 Play 的目的都是为了让特定主机以某个指定的用户身份执行任务。hosts 用于指定要执行指定任务的主机,须事先定义在主机清单中。

one.example.com
one.example.com:two.example.com
192.168.1.50
192.168.1.*
Websrvs:dbsrvs     # 或者, 两个组的并集
Websrvs:&dbsrvs    # 与, 两个组的交集
webservers:!dbsrvs # 在 websrvs 组, 但不在dbsrvs组

案例:

- hosts: websrvs:appsrvs

4.3.2)remote_user 组件

remote_user:可用于 Host 和 task 中。也可以通过指定其通过 sudo 的方式在远程主机上执行任务,其可用于 play 全局或某任务;此外,甚至可以在 sudo 时使用 sudo_user 指定 sudo 时切换的用户

 remote_user: root            # 方式一
    
 tasks:
   - name: test connection
     ping:
       remote_user: magedu    # 方式二
       sudo: yes         		  # 默认 sudo 为 root
       sudo_user:wang    			# sudo 为 wang

4.3.3)task 列表和 action 组件

Play 的主体部分是 task list,task list 中有一个或多个 task,各个 task 按次序逐个在 hosts 中指定的所有主机上执行,即在所有主机上完成第一个 task 后,再开始第二个 task。

task 的目的是使用指定的参数执行模块,而在模块参数中可以使用变量。模块执行是幂等的,这意味着多次执行是安全的,因为其结果均一致。

每个 task 都应该有其 name,用于 PlayBook 的执行结果输出,建议其内容能清晰地描述任务执行步骤。如果未提供 name,则 action 的结果将用于输出。

task 两种格式:

action: module arguments    # 示例: action: shell wall hello 
module: arguments           # 建议使用 # 示例: shell: wall hello 

注意:
Shell 和 Command 模块后面跟命令,而非 key=value

范例:

[root@ansible ansible] cat hello.yaml
---
# first yaml file 
- hosts: websrvs
  remote_user: root
  gather_facts: no   # 不收集系统信息, 提高执行效率
  
  tasks:
    - name: test network connection
      ping:
    - name: wall
      shell: wall "hello world!"
      
# 检查语法
[root@ansible ansible] ansible-playbook --syntax-check hello.yaml

# 验证脚本 ( 不真实执行 )
# 模拟执行 hello.yaml 文件中定义的 playbook, 不会在被控节点上应用任何更改
[root@ansible ansible] ansible-playbook -C hello.yaml

# 真实执行
[root@ansible ansible] ansible-playbook hello.yaml

范例:
初识 Ansible

[root@ansible ansible] vim test.yml
---
# 初识 Ansible
- hosts: websrvs
  remote_user: root
  gather_facts: yes     # 需开启,否则无法收集到主机信息

  tasks:
    - name: '存活性检测'
      ping:
    - name: '查看主机名信息'
      setup: filter=ansible_nodename
    - name: '查看操作系统版本'
      setup: filter=ansible_distribution_major_version
    - name: '查看内核版本'
      setup: filter=ansible_kernel
    - name: '查看时间'
      shell: date

  tasks:
    - name: '安装 HTTPD'
      yum: name=httpd
    - name: '启动 HTTPD'                                                                   
      service: name=httpd state=started enabled=yes
      
# 检查语法
[root@ansible ansible] ansible-playbook --syntax-check test.yml

# 执行脚本
[root@ansible ansible] ansible-playbook test.yml

范例:

Ansible PlayBook 由有序列表中的一个或多个 Play 组成。在这里,您可以认为剧本是执行指令以实现剧本总体目标的代码的一部分。

每个 Play 运行一个
Task
,每个 Task 调用 Ansible Modules 在一个或多个 Nodes 托管目标节点上执行指令。

---
- hosts: websrvs
  remote_user: root
  gather_facts: no           # 是否收集系统 facts 信息
  
  tasks:
    - name: install httpd    # 描述信息
      yum: name=httpd        # 调用 yum 模块
    - name: start httpd
      service: name=httpd state=started enabled=yes    # 调用 service 模块

image.png

---
- hosts: websrvs
  remote_user: root
  gather_facts: no    # 是否收集系统 facts 信息
  
  tasks:
    - name: ping
      ping:
    - name: wall
      shell: wall hello                   

- hosts: websrvs
  remote_user: root                  
  tasks:
    - name: install httpd
      yum: name=httpd
    - name: start httpd
      service: name=httpd state=started enabled=yes

# 验证脚本 ( 不真实执行 )
[root@ansible ansible] ansible-playbook -C hello.yaml

# 真实执行
[root@ansible ansible] ansible-playbook hello.yaml      

4.3.4)其它组件

某任务的状态在运行后为
changed
时,可通过
"notify"
通知给相应的 handlers 任务。
还可以通过
"tags"
给 task 打标签,可在 ansible-playbook 命令上使用
-t
指定进行调用

4.3.5)Shell Scripts VS Playbook 案例

# SHELL 脚本实现
#!/bin/bash
# 安装 Apache
yum install --quiet -y httpd

# 复制配置文件
cp /tmp/httpd.conf /etc/httpd/conf/httpd.conf
cp /tmp/vhosts.conf /etc/httpd/conf.d/

# 启动 Apache, 并设置开机启动
systemctl enable --now httpd
# Playbook 实现
---
- hosts: dbsrvs
  remote_user: root
  gather_facts: no        # 是否收集系统 facts 信息 ( 取消收集能提高 ansible 执行速度 )

  tasks:
    - name: "安装Apache"
      yum: name=httpd
    - name: "复制配置文件"
      copy: src=/tmp/httpd.conf dest=/etc/httpd/conf/
    - name: "复制配置文件"
      copy: src=/tmp/vhosts.conf dest=/etc/httpd/conf.d/
    - name: "启动Apache, 并设置开机启动"
      service: name=httpd state=started enabled=yes

4.4)PlayBook 命令

// 格式
ansible-playbook <filename.yml> ... [options]

PlayBook 常用选项

// "常见选项"
--syntax-check    # 语法检查, 可缩写成 --syntax, 相当于 bash -n 
-C --check        # 模拟执行, 只检测可能会发生的改变, 但不真正执行操作, dry run
--list-hosts      # 列出运行任务的主机    # 举例: ansible-playbook hello.yaml --list-hosts
--list-tags       # 列出 tag             
--list-tasks      # 列出 task            # 举例: ansible-playbook hello.yaml --list-tasks
--limit 主机列表  # 针对主机列表中的特定主机执行    # 举例: ansible-playbook hello.yaml --limit 192.168.80.28
-i INVENTORY      # 指定主机清单文件, 通常一个项对应一个主机清单文件    # 举例: ansible-playbook hello.yaml -i /root/hosts
--start-at-task START_AT_TASK    # 从指定 task 开始执行, 而非从头开始, START_AT_TASK 为任务的 name    # 举例: ansible-playbook hello.yml --start-at="start httpd"
-v -vv  -vvv      # 显示过程
// 举例: ansible-playbook hello.yaml --list-hosts
// 举例: ansible-playbook hello.yaml --list-tasks
// 举例: ansible-playbook hello.yaml --limit 192.168.80.28
// 举例: ansible-playbook hello.yaml -i /root/hosts
// 举例: ansible-playbook hello.yml --start-at="start httpd"

4.5)Playbook 初步

4.5.1)利用 PlayBook 创建 MySQL 用户

根据写 Shell 脚本的思路来编写 PlayBook 即可。( 顺序执行 )

范例:
mysql_user.yml

---
- hosts: dbsrvs
  name: 创建 MySQL 用户
  remote_user: root
  gather_facts: no

  tasks:
    - name: '创建 MySQL 用户组'
      group: 
        name: mysql 
        gid: 306 
        system: yes
    
    - name: '创建 MySQL 用户'
      user: 
        name: mysql 
        uid: 306 
        group: mysql 
        system: yes
        shell: /sbin/nologin
        home: /data/mysql
        create_home: yes
    
    - name: '查看 MySQL 用户信息'
      shell: id mysql
  
# 检查远程主机用户信息              
[root@ansible ansible] ansible dbsrvs -m shell -a "id mysql"

# 验证 PlayBook 脚本                
[root@ansible ansible] ansible-playbook -C mysql_user.yml

# 执行脚本
[root@ansible ansible] ansible-playbook mysql_user.yml

image.png

范例:
delete-mysql-user.yaml

[root@ansible ansible] vim delete-mysql-user.yaml
---
- hosts: dbsrvs
  name: 移除 MySQL 用户
  remote_user: root
  gather_facts: no

  tasks:
    - name: '删除 MySQL 用户'
      user: name=mysql state=absent remove=yes
    
[root@ansible ansible] ansible-playbook delete-mysql-user.yaml

image.png

4.5.2)利用 PlayBook 安装 nginx

范例:
install_nginx.yml

# 先将远程主机的 HTTPD 服务卸载
[root@ansible ansible] ansible websrvs -m yum -a 'name=httpd state=absent'

# Ansible 控制节点 安装 nginx 软件包 ( 实验: 主要为了拿到 nginx conf 文件 )
[root@ansible ansible] yum install nginx -y
[root@ansible ansible] mkdir files

# 拷贝 nginx 配置文件
[root@ansible ansible] cp /etc/nginx/nginx.conf files/
[root@ansible ansible] vim files/nginx.conf
    server {
        listen       8080;           # 修改该行                                                           
        listen       [::]:80;
        server_name  _;
        root         /usr/share/nginx/html;


# 编写 HTML 文件 ( 增加 UTF-8 防止乱码 )
[root@ansible ansible] vim files/index.html
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <h1> 微信公众号: 开源极客行 </h1>                                                       
</head>
<body>
</body>
</html>
# 编写 PlayBook 文件
[root@ansible ansible] vim install_nginx.yml
---
# install nginx
- hosts: websrvs
  remote_user: root
  gather_facts: no

  tasks:
    - name: "创建 nginx 用户组"
      group: name=nginx state=present
    - name: "创建 nginx 用户"
      user: name=nginx state=present group=nginx
    - name: "安装 nginx"
      yum: name=nginx state=present
    - name: "拷贝 nginx 配置文件"
      copy: src=files/nginx.conf dest=/etc/nginx/nginx.conf                                 
    - name: "拷贝 nginx 网页文件"
      copy: src=files/index.html dest=/usr/share/nginx/html/index.html
    - name: "启动 nginx 服务"
      service: name=nginx state=started enabled=yes
      
# 验证 PlayBook 脚本 ( 重要 )
[root@ansible ansible] ansible-playbook -C install_nginx.yml

# 执行 PalyBook 脚本
[root@ansible ansible] ansible-playbook install_nginx.yml

# 验证控制节点端口启用情况
[root@ansible ansible] ansible websrvs -m shell -a 'netstat -nltp | grep 8080'


$ vim remove-nginx.yaml
---
- hosts: websrvs
  name: 移除 nginx 软件
  remote_user: root
  gather_facts: no

  tasks:
    - name: 停止nginx服务
      service: name=nginx state=stopped
    - name: 移除nginx软件
      yum: name=nginx state=absent
    - name: 删除nginx用户
      user: name=nginx state=absent remove=yes
    - name: 删除nginx用户组
      group: name=nginx state=absent

4.5.3)利用 PlayBook 安装和卸载 httpd

范例:
install_httpd.yml

[root@centos8 ansible] vim files/index.html
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <h1> 微信公众号: 开源极客行 </h1>                                                       
</head>
<body>
</body>
</html>

[root@centos8 ansible] vim install_httpd.yml 
---
# install httpd
- hosts: websrvs
  remote_user: root
  gather_facts: no
  
  tasks:
    - name: "Install httpd"
      yum: name=httpd state=present
    - name: "Modify config listen port"
      lineinfile:
        path: /etc/httpd/conf/httpd.conf
        regexp: '^Listen'
        line: 'Listen 8080'
    - name: "Modify config data directory one"
      lineinfile:
        path: /etc/httpd/conf/httpd.conf
        regexp: '^DocumentRoot "/var/www/html"'
        line: 'DocumentRoot "/data/html"'
    - name: "Modify config data directory two"
      lineinfile:
        path: /etc/httpd/conf/httpd.conf
        regexp: '^<Directory "/var/www/html">'
        line: '<Directory "/data/html">'
    - name: "Mkdir website directory"
      file: path=/data/html state=directory
    - name: "copy Web html file"
      copy: src=files/index.html dest=/data/html/
    - name: "Start httpd service"
      service: name=httpd state=started enabled=yes

# 仅针对 192.168.80.28 执行操作
[root@centos8 ansible] ansible-playbook install_httpd.yml --limit 192.168.80.28

image.png
image.png

范例:
remove_httpd.yml

[root@centos8 ansible] vim remove_httpd.yml
---
- hosts: websrvs
  remote_user: root
  gather_facts: no

  tasks:
  - name: "remove httpd package"
    yum: name=httpd state=absent
  - name: "remove apache user"
    user: name=apache state=absent
  - name: "remove config file"
    file: name=/etc/httpd state=absent
  - name: "remove web html"
    file: name=/data/html/ state=absent

# 验证 PlayBook 脚本 ( 重要 )                           
[root@centos8 ansible] ansible-playbook -C remove_httpd.yml

# 执行 PlayBook 脚本
[root@centos8 ansible] ansible-playbook remove_httpd.yml 

image.png

4.5.4)利用 PlayBook 安装 MySQL 5.6

范例:
安装 mysql-5.6.46-linux-glibc2.12
注意:
建议 MySQL 客户机的内存需超过 2 G,否则可能会报错

# 下载 MySQL 软件包
[root@ansible ~] mkdir /data/ansible/files -p && cd /data/ansible/files
[root@ansible ~] wget https://ftp.iij.ad.jp/pub/db/mysql/Downloads/MySQL-5.6/mysql-5.6.46-linux-glibc2.12-x86_64.tar.gz

# MySQL 配置文件
[root@ansible ~] vim /data/ansible/files/my.cnf
[mysqld]
socket=/tmp/mysql.sock
user=mysql
symbolic-links=0
datadir=/data/mysql
innodb_file_per_table=1
log-bin
pid-file=/data/mysql/mysqld.pid

[client]
port=3306
socket=/tmp/mysql.sock

[mysqld_safe]
log-error=/var/log/mysqld.log

# 编写 MySQL 初始脚本
[root@ansible ~] vim /data/ansible/files/secure_mysql.sh
#!/bin/bash
/usr/local/mysql/bin/mysql_secure_installation <<EOF
y
123456
123456
y
y
y
y

[root@ansible files] chmod +x secure_mysql.sh

[root@ansible files]# tree /data/ansible/files/
/data/ansible/files/
├── my.cnf
├── mysql-5.6.46-linux-glibc2.12-x86_64.tar.gz
└── secure_mysql.sh

0 directories, 3 files
# 编写 PlayBook
[root@ansible ~] vim /data/ansible/install_mysql.yml
---
# install mysql-5.6.46-linux-glibc2.12-x86_64.tar.gz
- hosts: dbsrvs
  remote_user: root
  gather_facts: no
  
  tasks:
    - name: "install packages"
      yum: name=libaio,perl-Data-Dumper,perl-Getopt-Long
    - name: "create mysql group"
      group: name=mysql gid=306
    - name: "create mysql user"
      user: name=mysql uid=306 group=mysql shell=/sbin/nologin system=yes create_home=no home=/data/mysql
    - name: "copy tar to remote host and file mode"
      unarchive: src=/data/ansible/files/mysql-5.6.46-linux-glibc2.12-x86_64.tar.gz dest=/usr/local/ owner=root group=root
    - name: "create linkfile /usr/local/mysql"
      file: src=/usr/local/mysql-5.6.46-linux-glibc2.12-x86_64 dest=/usr/local/mysql state=link
    - name: "create dir /data/mysql"
      file: path=/data/mysql state=directory
    - name: "data dir"	# 该步骤貌似有点问题
      shell: chdir=/usr/local/mysql ./scripts/mysql_install_db --datadir=/data/mysql --user=mysql
      tags: data
      ignore_errors: yes	# 忽略错误,继续执行
    - name: "config my.cnf"
      copy: src=/data/ansible/files/my.cnf dest=/etc/my.cnf 
    - name: "service script"
      shell: /bin/cp /usr/local/mysql/support-files/mysql.server /etc/init.d/mysqld
    - name: "enable service"
      shell: /etc/init.d/mysqld start;chkconfig --add mysqld;chkconfig mysqld on
      
      tags: service
    - name: "PATH variable"
      copy: content='PATH=/usr/local/mysql/bin:$PATH' dest=/etc/profile.d/mysql.sh
    - name: "secure script"
      script: src=/data/ansible/files/secure_mysql.sh
      tags: script

# 执行 PlayBook 脚本
[root@ansible ~] ansible-playbook install_mysql.yml

image.png

范例:
install_mariadb.yml

# 编写 PlayBook
[root@ansible ~] vim /data/ansible/install_mariadb.yml
---
# Installing MariaDB Binary Tarballs
- hosts: dbsrvs
  remote_user: root
  gather_facts: no
  tasks:
    - name: create group
      group: name=mysql gid=27 system=yes
    - name: create user
      user: name=mysql uid=27 system=yes group=mysql shell=/sbin/nologin home=/data/mysql create_home=no
    - name: mkdir datadir
      file: path=/data/mysql owner=mysql group=mysql state=directory
    - name: unarchive package
      unarchive: src=/data/ansible/files/mariadb-10.2.27-linux-x86_64.tar.gz dest=/usr/local/ owner=root group=root
    - name: link
     file: src=/usr/local/mariadb-10.2.27-linux-x86_64 path=/usr/local/mysql state=link
     - name: install database
       shell: chdir=/usr/local/mysql  ./scripts/mysql_install_db --datadir=/data/mysql --user=mysql
    - name: config file
      copy: src=/data/ansible/files/my.cnf  dest=/etc/ backup=yes
    - name: service script
      shell: /bin/cp /usr/local/mysql/support-files/mysql.server /etc/init.d/mysqld
    - name: start service
      service: name=mysqld state=started enabled=yes
    - name: PATH variable
      copy: content='PATH=/usr/local/mysql/bin:$PATH' dest=/etc/profile.d/mysql.sh
    
# 执行 PlayBook 脚本
[root@ansible ~] ansible-playbook install_mariadb.yml    

4.6)ignore_errors 忽略错误

如果一个 Task 出错,
默认将不会继续执行后续的其它 Task

我们可以利用 **ignore_errors:yes ** 忽略此 Task 的错误,继续向下执行 PlayBook 其它 Task

[root@ansible ansible] vim test_ignore.yml
---
- hosts: websrvs
  
  tasks:
    - name: error test
      command: /bin/false    # 返回失败结果的命令
      ignore_errors: yes     # 忽略错误, 继续执行
    - name: continue
      command: wall continue
      
[root@ansible ansible] ansible-playbook test_ignore.yml

4.7)Playbook 中使用 handlers 和 notify

Handlers 本质是 task list,类似于 MySQL 中的触发器触发的行为,其中的 task 与前述的 task 并没有本质上的不同,主要用于当关注的资源发生变化时,才会采取一定的操作。
而 Notify 对应的 action 可用于在每个 play 的最后被触发,这样可避免多次有改变发生时每次都执行指定的操作,仅在所有的变化发生完成后一次性地执行指定操作。在 notify 中列出的操作称为 handler,也即 notify 中调用 handler 中定义的操作。

注意:

  • 如果多个 Task 通知了相同的 handlers, 此 handlers 仅会在所有 Tasks 结束后运行一次。
  • 只有 notify 对应的 task 发生改变了才会通知 handlers,没有改变则不会触发 handlers。
  • handlers 是在所有前面的 tasks 都成功执行才会执行,如果前面任何一个 task 失败,会导致 handler 跳过执行,可以使用
    force_handlers:yes
    强制执行 handler。

案例:

---
- hosts: websrvs
  remote_user: root
  gather_facts: no
  
  tasks:
    - name : Install httpd
      yum: name=httpd state=present
    - name : Install configure file
      copy: src=files/httpd.conf dest=/etc/httpd/conf/
    # 修改 http 服务的端口号
    - name: config httpd conf
      lineinfile: "path=/etc/httpd/conf/httpd.conf regexp='^Listen' line='Listen 8080'"
      notify:
        - restart httpd 
        - wall
    - name: ensure apache is running
      service: name=httpd state=started enabled=yes

  handlers:
    - name: restart httpd
      service: name=httpd state=restarted
    - name: wall
      command: wall "The config file is changed"

案例:
在 Ansible 中,handlers 部分 用于定义当某些条件满足时应该执行的任务。
这些任务通常是 由 notify 指令触发的,这些 notify 指令可以放在其他任务中。
当任务完成并且其状态发生变化时,与任务相关联的 notify 指令会触发相应的 handler。
案例:
当 Copy Nginx Config File 任务完成并发生改变时,它会触发名为 Restart Nginx Service 的 handler。Nginx 服务将被重启,以应用新的配置。
模块的大概执行流程:
https://blog.csdn.net/wangjiachenga/article/details/122980073

[root@ansible ansible] vim files/nginx.conf
    server {
        listen       80;                # 修改该行
        listen       [::]:80;
        server_name  _;
        root         /usr/share/nginx/html;

[root@ansible ansible] vim install_nginx.yml
---
# install nginx
- hosts: websrvs
  remote_user: root
  gather_facts: no

  tasks:
    - name: "Add Nginx Group"
      group: name=nginx state=present
    - name: "Add Nginx User"
      user: name=nginx state=present group=nginx
    - name: "Install Nginx"
      yum: name=nginx state=present
    - name: "Copy Nginx Config File"
      copy: src=files/nginx.conf dest=/etc/nginx/nginx.conf
      notify: Restart Nginx Service    # 定义 notify 触发器
    - name: "Copy Web Page File"
      copy: src=files/index.html dest=/usr/share/nginx/html/index.html
    - name: "Start Nginx Service"
      service: name=nginx state=started enabled=yes
        
   
  handlers:    # 触发如下操作
    - name: "Restart Nginx Service"
      service: name=nginx state=restarted enabled=yes
      
[root@ansible ansible] ansible-playbook install_nginx.yml

范例:
强制执行 handlers

- hosts: websrvs
  force_handlers: yes # 无论 task 中的任何一个 task 失败, 仍强制执行 handlers
  tasks:
    - name: config file
      copy: src=nginx.conf dest=/etc/nginx/nginx.conf
      notify: restart nginx
    - name: install package
      yum: name=no_exist_package
    
  handlers:
    - name: "restart nginx"
      service: name=nginx state=restarted

4.8)Playbook 中使用 tags 组件

官方文档:
https://docs.ansible.com/ansible/latest/user_guide/playbooks_tags.html
参考文档:
https://blog.csdn.net/weixin_42171272/article/details/135268747
如果写了一个很长的 PlayBook,其中有很多的任务,这并没有什么问题,不过在实际使用这个剧本时,可能只是想要执行其中的一部分任务。
或者,你只想要执行其中一类任务而已,而并非想要执行整个剧本中的全部任务,
这个时候我们就可以借助 tags 标签实现这个需求。

tags 可以帮助我们对任务进行
打标签
的操作,与模块名同级,当任务存在标签以后,我们就可以在执行 PlayBook 时,借助标签,指定执行哪些任务,或者指定不执行哪些任务了。

在 PlayBook 文件中,利用 tags 组件,为特定 task 指定标签。
当在执行 PlayBook 时,可以只执行特定 tags 的 task,而非整个 PlayBook 文件。可以一个 task 对应多个 tag,也可以多个 task 对应一个 tag。
还有另外 3 个特殊关键字用于标签,tagged,untagged 和 all,它们分别是仅运行已标记,只有未标记和所有任务。

[root@ansible ~] vim httpd.yml
---
# tags example
- hosts: websrvs
  remote_user: root
  gather_facts: no
  
tasks:
  - name: "Install httpd"
    yum: name=httpd state=present
  - name: "Install configure file"
    copy: src=files/httpd.conf dest=/etc/httpd/conf/
    tags: [ conf,file ]    # 写在一行
      - conf               # 写成多行
      - file
  - name: "start httpd service"
    tags: service          # 写在一行
    service: name=httpd state=started enabled=yes

# 查看标签
[root@ansible ~] ansible-playbook --list-tags httpd.yml

# 仅执行标签动作
[root@ansible ~] ansible-playbook -t conf,service httpd.yml

# 跳过标签动作 
[root@ansible ~] ansible-playbook --skip-tags conf httpd.yml
[root@ansible ~] ansible-playbook httpd.yml --skip-tags untagged

4.9)Playbook 中 使用变量

Playbook 中同样也支持变量

变量名:
仅能由字母、数字和下划线组成,且只能以字母开头

// 变量定义
# variable=value
variable: value    # 建议

范例:

# http_port=80
http_port: 80    # 建议

变量调用方式:
通过
{{ variable_name }}
调用变量,且变量名前后建议加空格,
有时用
"{{ variable_name }}"
才生效

变量来源:

  1. ansible 的 setup facts 远程主机的所有变量 都可直接调用
  2. 通过命令行指定变量,优先级最高
ansible-playbook -e varname=value test.yml
  1. 在 PlayBook 文件中定义
vars:
  var1: value1
  var2: value2
  1. 在独立的变量 YAML 文件中定义
- hosts: all
  vars_files:
    - vars.yml
  1. 在主机清单文件中定义
  • 主机(普通)变量:主机组中主机单独定义,优先级高于公共变量
  • 组(公共)变量:针对主机组中所有主机定义统一变量
  1. 在项目中针对主机和主机组定义

在项目目录中创建 host_vars 和 group_vars 目录

  1. 在 role 中定义

变量的优先级从高到低如下

-e 选项定义变量 > playbook 中 vars_files > playbook 中 vars 变量定义 > host_vars/主机名 文件 > 主机清单中主机变量 > group_vars/主机组名文件 > group_vars/all文件 > 主机清单组变量

4.9.1)使用 setup 模块中变量

本模块自动在 PlayBook 调用,不要用 ansible 命令调用,生成的系统状态信息,并存放在 facts 变量中。
facts 包括的信息很多,**如: **主机名,IP,CPU,内存,网卡等
facts 变量的实际使用场景案例

  • 通过 facts 变量获取被控端 CPU 的个数信息,从而生成不同的 Nginx 配置文件
  • 通过 facts 变量获取被控端内存大小信息,从而生成不同的 memcached 的配置文件
  • 通过 facts 变量获取被控端主机名称信息,从而生成不同的 Zabbix 配置文件
  • ......

案例:
使用 setup 变量

[root@centos8 ~] ansible 192.168.80.18 -m setup -a "filter=ansible_nodename"
[root@centos8 ~] ansible 192.168.80.18 -m setup -a 'filter="ansible_default_ipv4"'

范例:

[root@ansible ~] vim var.yml
---
# var1.yml
- hosts: websrvs
  remote_user: root
  gather_facts: yes    # 注意: 这个需要 yes 启用
  
  tasks:
    - name: "create log file"
      file: name=/root/{{ ansible_nodename }}.log state=touch owner=wangj mode=600
      
[root@ansible ~] ansible-playbook var.yml

范例:
显示 ens33 网卡的 IP 地址

[root@ansible ansible] vim show_ip.yml 
- hosts: websrvs
  
  tasks:
    - name: show eth0 ip address {{ ansible_facts["ens33"]["ipv4"]["address"] }}    # name 中也可以调用变量
      debug:
        msg: IP address {{ ansible_ens33.ipv4.address }}    # 注意: 网卡名称
        # msg: IP address {{ ansible_facts["eth0"]["ipv4"]["address"] }}
        # msg: IP address {{ ansible_facts.eth0.ipv4.address }}
        # msg: IP address {{ ansible_default_ipv4.address }}
        # msg: IP address {{ ansible_eth0.ipv4.address }}
        # msg: IP address {{ ansible_eth0.ipv4.address.split('.')[-1] }} # 取 IP 中的最后一个数字

[root@ansible ansible] ansible-playbook -v show_ip.yml

范例:

[root@ansible ~] vim test.yml
---
- hosts: websrvs
  tasks:
    - name: test var
      file: path=/root/{{ ansible_facts["ens33"]["ipv4"]["address"] }}.log state=touch    # 注意: 网卡名称信息
      # file: path=/root/{{ ansible_ens33.ipv4.address }}.log state=touch # 和上面效果一样

[root@ansible ~] ansible-playbook test.yml

4.9.2)在 PlayBook 命令行中定义变量

范例:

[root@ansible ~] vim var2.yml
---
- hosts: websrvs
  remote_user: root
  tasks:
    - name: "install package"
      yum: name={{ pkname }} state=present    # 调用变量

# 在 PlayBook 命令行中定义变量
[root@ansible ~] ansible-playbook -e pkname=vsftpd var2.yml

范例:
也可以将多个变量放在一个文件中

# 也可以将多个变量放在一个文件中
[root@ansible ~] cat vars
pkname1: memcached
pkname2: redis

[root@ansible ~] vim var2.yml
---
- hosts: websrvs
  remote_user: root
  tasks:
    - name: install package {{ pkname1 }}    # 名称也调用变量 ( 利于我们清楚正在安装什么软件包 )
      yum: name={{ pkname1 }} state=present
    - name: install package {{ pkname2 }}
      yum: name={{ pkname2 }} state=present

# 方式一
[root@ansible ~] ansible-playbook -e pkname1=memcached -e pkname2=redis var2.yml

# 方式二 ( 指定存放着变量的文件 )
[root@ansible ~] ansible-playbook -e '@vars' var2.yml

4.9.3)在 PlayBook 文件中定义变量

范例:
也可以在 PlayBook 文件中定义变量

[root@ansible ~] vim var3.yml
---
- hosts: websrvs
  remote_user: root
  vars:
    username: user1        # 定义变量
    groupname: group1      # 定义变量

  tasks:
    - name: "create group {{ groupname }}"
      group: name={{ groupname }} state=present
    - name: "create user {{ username }}"
      user: name={{ username }} group={{ groupname }} state=present

# 执行 PlayBook 文件
[root@ansible ~] ansible-playbook var3.yml

# 验证
[root@ansible ~] ansible websrvs -m shell -a 'id user1'

范例:
变量之间的
相互调用

[root@ansible ~] vim var4.yaml
---
- hosts: websrvs
  remote_user: root
  vars:
    collect_info: "/data/test/{{ansible_default_ipv4['address']}}/"	# 基于默认变量定义了一个新的变量

  tasks:
    - name: "Create IP directory"
      file: name="{{collect_info}}" state=directory		# 引用变量

# 执行结果
tree /data/test/
/data/test/
└── 192.168.80.18

1 directory, 0 files

范例:
变量之间的
相互调用

[root@ansible ansible] cat var2.yml
---
- hosts: websrvs
  vars:
    suffix: "txt"
    file: "{{ ansible_nodename }}.{{suffix}}"		# 基于默认变量定义了一个新的变量
    
  tasks:
    - name: test var
      file: path="/data/{{file}}" state=touch		# 引用变量

范例:安装多个包

# 实例一
[root@ansible ~] cat install.yml 
- hosts: websrvs
  vars:
    web: httpd
    db: mariadb-server
    
  tasks:
    - name: install {{ web }} {{ db }}
      yum:
        name:
          - "{{ web }}"
          - "{{ db }}"
        state: latest
# 实例二      
[root@ansible ~] cat install2.yml 
- hosts: websrvs
  tasks:
    - name: install packages
      yum: name={{ pack }}
      vars:
        pack:
          - httpd
          - memcached

范例:
安装指定版本的 MySQL
新增 PlayBook 定义变量功能

[root@ansible ansible] cat install_mysql.yml 
---
# install mysql-5.6.46-linux-glibc2.12-x86_64.tar.gz
- hosts: dbsrvs
  remote_user: root
  gather_facts: no
  vars:
    version: "mysql-5.6.46-linux-glibc2.12-x86_64"
    suffix: "tar.gz"
    file: "{{version}}.{{suffix}}"
    
  tasks:
    - name: "install packages"
      yum: name=libaio,perl-Data-Dumper,perl-Getopt-Long
    - name: "create mysql group"
      group: name=mysql gid=306
    - name: "create mysql user"
      user: name=mysql uid=306 group=mysql shell=/sbin/nologin system=yes create_home=no home=/data/mysql
    - name: "copy tar to remote host and file mode"
      unarchive: src=/data/ansible/files/{{file}} dest=/usr/local/ owner=root group=root
    - name: "create linkfile /usr/local/mysql"
      file: src=/usr/local/{{version}} dest=/usr/local/mysql state=link
    - name: "data dir"
      shell: chdir=/usr/local/mysql/ ./scripts/mysql_install_db --datadir=/data/mysql --user=mysql
      tags: data
    - name: "config my.cnf"
      copy: src=/data/ansible/files/my.cnf  dest=/etc/my.cnf
    - name: "service script"
      shell: /bin/cp /usr/local/mysql/support-files/mysql.server /etc/init.d/mysqld
    - name: "enable service"
      shell: /etc/init.d/mysqld start;chkconfig --add mysqld;chkconfig mysqld on
      tags: service
    - name: "PATH variable"
      copy: content='PATH=/usr/local/mysql/bin:$PATH' dest=/etc/profile.d/mysql.sh
    - name: "secure script"
      script: /data/ansible/files/secure_mysql.sh
      tags: script

4.9.4)使用变量文件

可以在一个
独立的 PlayBook 文件
中定义变量,在另一个 PlayBook 文件中引用变量文件中的变量,比 PlayBook 中定义的变量优化级高

# 编写变量文件
vim vars.yml
---
# variables file
  package_name: mariadb-server
  service_name: mariadb
# 在 PlayBook 调用变量文件
vim var5.yml
---
# install package and start service
- hosts: dbsrvs
  remote_user: root
  vars_files:            # 在 PlayBook 调用变量文件
    - vars.yml
    
  tasks:
    - name: "install package"
      yum: name={{ package_name }}
      tags: install
    - name: "start service"
      service: name={{ service_name }} state=started enabled=yes

范例:

cat vars2.yml
---
var1: httpd
var2: nginx
cat var6.yml
---
- hosts: web
  remote_user: root
  vars_files:
    - vars2.yml
    
  tasks:
    - name: create httpd log
      file: name=/app/{{ var1 }}.log state=touch
    - name: create nginx log
      file: name=/app/{{ var2 }}.log state=touch

4.9.5)针对主机和主机组 定义变量

4.9.5.1)在主机清单中 针对所有项目的主机和主机分组对应变量

所有项目的 主机变量
在 inventory 主机清单文件中 为指定的主机定义变量 以便于在 PlayBook 中使用

// 范例: 定义主机变量
[websrvs]
www1.magedu.com http_port=80 maxRequestsPerChild=808
www2.magedu.com http_port=8080 maxRequestsPerChild=909

所有项目的组(公共)变量
在 inventory 主机清单文件中 赋予给指定组内所有主机上
在 PlayBook 中可用的变量,如果和主机变量是同名,优先级低于主机变量

// 范例: 公共变量
[websrvs:vars]
http_port=80
ntp_server=ntp.magedu.com
nfs_server=nfs.magedu.com
-- K8S 案例 --
[all:vars]
# --------- Main Variables ---------------
# Cluster container-runtime supported: docker, containerd
CONTAINER_RUNTIME="docker"

# Network plugins supported: calico, flannel, kube-router, cilium, kube-ovn
CLUSTER_NETWORK="calico"

# Service proxy mode of kube-proxy: 'iptables' or 'ipvs'
PROXY_MODE="ipvs"

# K8S Service CIDR, not overlap with node(host) networking
SERVICE_CIDR="192.168.0.0/16"

# Cluster CIDR (Pod CIDR), not overlap with node(host) networking
CLUSTER_CIDR="172.16.0.0/16"

# NodePort Range
NODE_PORT_RANGE="20000-60000"

# Cluster DNS Domain
CLUSTER_DNS_DOMAIN="magedu.local."

范例:

[root@ansible ~] vim /etc/ansible/hosts
[websrvs]
192.168.80.18 hname=www1 domain=magedu.io    # 定义主机变量 ( 主机变量 优先级高 )
192.168.80.28 hname=www2

[websrvs:vars]        # 定义分组变量
mark="-"

[all:vars]            # 定义公共变量 ( 公共变量优先级低 )
domain=magedu.org

# 调用变量 ( 修改主机名 )
[root@ansible ~] ansible websrvs -m hostname -a 'name={{ hname }}{{ mark }}{{ domain }}'

# 命令行指定变量:
# -e 定义变量的优先级更高
[root@ansible ~] ansible websrvs -e domain=magedu.cn -m hostname -a 'name={{ hname }}{{ mark }}{{ domain }}'

范例:
K8S 的 ansible 变量文件

[etcd]
10.0.0.104 NODE_NAME=etcd1
10.0.0.105 NODE_NAME=etcd2
10.0.0.106 NODE_NAME=etcd3

[kube-master]
10.0.0.103 NEW_MASTER=yes
10.0.0.101
10.0.0.102

[kube-node]
10.0.0.109 NEW_NODE=yes
10.0.0.107
10.0.0.108

[harbor]

[ex-lb]
10.0.0.666666 LB_ROLE=master EX_APISERVER_VIP=10.0.0.100 EX_APISERVER_PORT=8443
10.0.0.112 LB_ROLE=backup EX_APISERVER_VIP=10.0.0.100 EX_APISERVER_PORT=8443

[chrony]

[all:vars]
CONTAINER_RUNTIME="docker"
CLUSTER_NETWORK="calico"
PROXY_MODE="ipvs"
SERVICE_CIDR="192.168.0.0/16"
CLUSTER_CIDR="172.16.0.0/16"
NODE_PORT_RANGE="20000-60000"
CLUSTER_DNS_DOMAIN="magedu.local."
bin_dir="/usr/bin"
ca_dir="/etc/kubernetes/ssl"
base_dir="/etc/ansible"

4.9.5.2)针对当前项目的主机和主机组的变量

上面的方式是针对所有项目都有效,而官方更建议的方式是使用 ansible 特定项目的主机变量和组变量。生产建议在项目目录中创建额外的两个变量目录,分别是 host_vars 和 group_vars。
host_vars:
下面的文件名和主机清单主机名一致,针对单个主机进行变量定义,格式:host_vars/hostname( 主机变量 )
group_vars:
下面的文件名和主机清单中组名一致,针对单个组进行变量定义,格式:gorup_vars/groupname( 分组变量 )
group_vars/all:
文件内定义的变量对所有组都有效( 公共变量 )

范例:
特定项目的主机变量和分组变量
建议:
主机清单不定义变量( 仅存放主机分组信息 )
变量统一定义在项目目录下的变量目录中( 条理非常清晰 )

# 创建项目目录
[root@ansible ansible] mkdir /data/ansible/test_project -p
[root@ansible ansible] cd /data/ansible/test_project

# 编写项目主机清单文件 ( 仅存放主机分组信息 )
[root@ansible test_project] vim hosts
[websrvs]
192.168.80.18
192.168.80.28

# 创建项目主机变量目录
[root@ansible test_project] mkdir host_vars

# 创建项目分组变量目录
[root@ansible test_project] mkdir group_vars

# 定义项目主机变量信息
[root@ansible test_project] vim host_vars/192.168.80.18
id: 1
[root@ansible test_project] vim host_vars/192.168.80.28
id: 2

# 定义项目分组变量信息
[root@ansible test_project] vim group_vars/websrvs 
name: web
[root@ansible test_project] vim group_vars/all
domain: magedu.org

# 验证项目变量文件
[root@ansible test_project] tree host_vars/ group_vars/
host_vars/
├── 192.168.80.18
└── 192.168.80.28
group_vars/
├── all
└── websrvs

0 directories, 4 files

# 定义 PlayBook 文件
[root@ansible test_project] vim test.yml
- hosts: websrvs

  tasks:
    - name: get variable
      command: echo "{{name}}{{id}}.{{domain}}"
      register: result
    - name: print variable
      debug:
        msg: "{{result.stdout}}"
        
# 执行
[root@ansible test_project] ansible-playbook test.yml

4.9.6)register 注册变量( 重要 )

参考:
https://blog.csdn.net/byygyy/article/details/105624602
在 PlayBook 中可以使用 register
将捕获命令的输出
保存在临时变量中
然后使用 debug 模块进行显示输出
范例:
利用 debug 模块输出变量
作用:
将 Shell 模块中命令的输出信息赋值给 register 注册变量中
注意:
ansible 执行结果一般都会返回一个字典类型的数据,你会看到很多你不关心的字段,可以通过指定字典的 key,例如 stdout 或 stdout_lines,只看到你关心的数据。

[root@ansible ~] vim register1.yml
- hosts: 192.168.80.18
  tasks:
    - name: "get variable"
      shell: hostname
      register: name
      
    - name: "print variable"
      debug:
        msg: "{{ name }}"                   # 输出 register 注册的 name 变量的全部信息, 注意: 变量要加 "" 引起来
        # msg: "{{ name.cmd }}"             # 显示命令
        # msg: "{{ name.rc }}"              # 显示命令成功与否
        # msg: "{{ name.stdout }}"          # 显示命令的输出结果为字符串形式
        # msg: "{{ name.stdout_lines }}"    # 显示命令的输出结果为列表形式
        # msg: "{{ name.stdout_lines[0] }}" # 显示命令的输出结果的列表中的第一个元素
        # msg: "{{ name['stdout_lines'] }}" # 显示命令的执行结果为列表形式
        
// 说明
在第一个 task 中, 使用了 register 注册变量名为 name;
当 Shell 模块执行完毕后, 会将数据放到该
变量中.
在第二个 task 中, 使用了 debug 模块, 并从变量 name 中获取数据.

// 注意:
# 输出的 name 实际上相当于是一个字典
# 里面包含很多个键值对信息 ( 我们需要哪个键值对信息,需要指定性选择该键值 )
# 比如: name.stdout ( 在 name 变量后调用键信息 )
[root@centos8 ~] ansible-playbook register1.yml

ansible 执行结果一般都会返回一个字典类型的数据,以此你会看到很多你不关心的字段,我们可以通过指定字典的 key,例如 stdout 或 stdout_lines,只看到你关心的数据。

[root@ansible ~] vim register1.yml
- hosts: 192.168.80.18
  tasks:
    - name: "get variable"
      shell: hostname
      register: name
      
    - name: "print variable"
      debug:
        msg: "{{ name.stdout }}"            # 取 name 变量的 stdout 键值
        
[root@centos8 ~] ansible-playbook register1.yml

范例:
使用 register 注册变量
创建文件

[root@ansible ~] vim register2.yml 
- hosts: websrvs

  tasks:
    - name: "get variable"
      shell: hostname
      register: name
    - name: "create file"
      file: dest=/root/{{ name.stdout }}.log state=touch

[root@ansible ~] ansible-playbook register2.yml
[root@centos8 ~] ll /root | grep log

范例:
register 和 debug 模块
参考:
https://www.cnblogs.com/dgp-zjz/p/15683546.html
自定义 debug 模块的输出结果( 默认输出的 msg 键内容 )

[root@ansible ~] vim debug_test.yml
---
- hosts: 192.168.80.8
  tasks:

    - shell: echo "hello world"
      register: say_hi

    - shell: "awk -F: 'NR==1{print $1}' /etc/passwd"
      register: user

    - debug:
        var: say_hi.stdout   # 自定义输出变量代替 msg
    - debug:
        var: user.stdout     # 自定义输出变量代替 msg

[root@ansible ~] ansible-playbook debug_test.yml

范例:
安装启动服务并检查

[root@ansible ansible] vim service.yml
---
- hosts: websrvs
  vars:
    package_name: nginx
    service_name: nginx

  tasks:
    - name: "install {{ package_name }}"
      yum: name={{ package_name }}
    - name: "start {{ service_name }}"
      service: name={{ service_name }} state=started enabled=yes
    - name: "check service status"
      shell: ps aux | grep {{ service_name }}
      register: check_service
    - name: debug
      debug:
        msg: "{{ check_service.stdout_lines }}"

[root@ansible ansible] ansible-playbook service.yml

范例:
批量修改主机名

[root@ansible ansible] vim hostname.yml
- hosts: websrvs
  vars:
    host: web
    domain: wuhanjiayou.cn

  tasks:
    - name: "get variable"
      shell: echo $RANDOM | md5sum | cut -c 1-8
      register: get_random
    - name: "print variable"
      debug:
        msg: "{{ get_random.stdout }}"
    - name: "set hostname"
      hostname: name={{ host }}-{{ get_random.stdout }}.{{ domain }}

[root@ansible ansible] ansible-playbook hostname.yml     

反射是 Java 面试中必问的面试题,但只有很少人能真正的理解“反射”并讲明白反射,更别说能说清楚它的底层实现原理了。所以本文就通过大白话的方式来系统的讲解一下反射,希望大家看完之后能真正的理解并掌握“反射”这项技术。

1.什么是反射?

反射在程序运行期间动态获取类和操纵类的一种技术。通过反射机制,可以在运行时动态地创建对象、调用方法、访问和修改属性,以及获取类的信息。

2.反射的应用有哪些?

反射在日常开发中使用的地方有很多,例如以下几个:

  1. 动态代理
    :反射是动态代理的底层实现,即在运行时动态地创建代理对象,并拦截和增强方法调用。这常用于实现 AOP 功能,如日志记录、事务管理等。
  2. Bean 创建
    :Spring/Spring Boot 项目中,在项目启动时,创建的 Bean 对象就是通过反射来实现的。
  3. JDBC 连接
    :JDBC 中的 DriverManager 类通过反射加载并注册数据库驱动,这是 Java 数据库连接的标准做法。

3.反射实现

反射的关键实现方法有以下几个:

  1. 得到类
    :Class.forName("类名")
  2. 得到所有字段
    :getDeclaredFields()
  3. 得到所有方法
    :getDeclaredMethods()
  4. 得到构造方法
    :getDeclaredConstructor()
  5. 得到实例
    :newInstance()
  6. 调用方法
    :invoke()

具体使用示例如下:

// 1.反射得到对象
Class<?> clazz = Class.forName("User");
// 2.得到方法
Method method = clazz.getDeclaredMethod("publicMethod");
// 3.得到静态方法
Method staticMethod = clazz.getDeclaredMethod("staticMethod");
// 4.执行静态方法
staticMethod.invoke(clazz);

反射执行私有方法代码实现如下:

// 1.反射得到对象
Class<?> clazz = Class.forName("User");
// 2.得到私有方法
Method privateMethod = clazz.getDeclaredMethod("privateMethod");
// 3.设置私有方法可访问
privateMethod.setAccessible(true);
// 4.得到实例
Object user = clazz.getDeclaredConstructor().newInstance();
// 5.执行私有方法
privateMethod.invoke(user);

4.底层实现原理

从上述内容可以看出,对于反射来说,操纵类最主要的方法是 invoke,所以搞懂了 invoke 方法的实现,也就搞定了反射的底层实现原理了。

invoke 方法的执行流程如下:

  1. 查找方法
    :当通过 java.lang.reflect.Method 对象调用 invoke 方法时,Java 虚拟机(JVM)首先确认该方法是否存在并可以访问。这包括检查方法的访问权限、方法签名是否匹配等。
  2. 安全检查
    :如果方法是私有的或受保护的,还需要进行访问权限的安全检查。如果当前调用者没有足够的权限访问这个方法,将抛出 IllegalAccessException。
  3. 参数转换和适配
    :invoke 方法接受一个对象实例和一组参数,需要将这些参数转换成对应方法签名所需要的类型,并且进行必要的类型检查和装箱拆箱操作。
  4. 方法调用
    :对于非私有方法,Java 反射实际上是通过 JNI(Java Native Interface,Java 本地接口)调用到 JVM 内部的 native 方法,例如 java.lang.reflect.Method.invoke0()。这个 native 方法负责完成真正的动态方法调用。对于 Java 方法,JVM 会通过方法表、虚方法表(vtable)进行查找和调用;对于非虚方法或者静态方法,JVM 会直接调用相应的方法实现。
  5. 异常处理
    :在执行方法的过程中,如果出现任何异常,JVM 会捕获并将异常包装成 InvocationTargetException 抛出,应用程序可以通过这个异常获取到原始异常信息。
  6. 返回结果
    :如果方法正常执行完毕,invoke 方法会返回方法的执行结果,或者如果方法返回类型是 void,则不返回任何值。

通过这种方式,Java 反射的 invoke 方法能够打破编译时的绑定,实现运行时动态调用对象的方法,提供了极大的灵活性,但也带来了运行时性能损耗和安全隐患(如破坏封装性、违反访问控制等)。

5.优缺点分析

反射的优点如下:

  1. 灵活性
    :使用反射可以在运行时动态加载类,而不需要在编译时就将类加载到程序中。这对于需要动态扩展程序功能的情况非常有用。
  2. 可扩展性
    :使用反射可以使程序更加灵活和可扩展,同时也可以提高程序的可维护性和可测试性。
  3. 实现更多功
    能:许多框架都使用反射来实现自动化配置和依赖注入等功能。例如,Spring 框架就使用反射来实现依赖注入。

反射的缺点如下:

  1. 性能问题
    :使用反射会带来一定的性能问题,因为反射需要在运行时动态获取类的信息,这比在编译时就获取信息要慢。
  2. 安全问题
    :使用反射可以访问和修改类的字段和方法,这可能会导致安全问题。因此,在使用反射时需要格外小心,确保不会对程序的安全性造成影响。

课后思考

为什么反射的执行效率比较低?动态代理的实现除了反射之外,还有没有其他的实现方法?

本文已收录到我的面试小站
www.javacn.site
,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。