嘉兴ing

嘉兴ing 查看完整档案

厦门编辑桂林理工大学  |  软件工程 编辑  |  填写所在公司/组织填写个人主网站
编辑

PHPer@厦门

个人动态

嘉兴ing 发布了文章 · 1月20日

⭐Redis 备忘录 - 基础

[TOC]

文档/链接

图片来自: https://segmentfault.com/a/11...

高性能

官方数据: 10W+ QPS

  • 数据存储在内存
  • 单线程: 避免上下文切换和锁
  • 多路I/O复用

数据类型及应用场景

String 字符串

  • 单个 String 最大可存储 512 MB
  • 二进制安全.
  • 内部实现采用 SDS(Simple Dynamic String)

    类似 Go 的切片, 通过额外分配空间避免频繁内存分配.

使用场景

  • 简单的 K-V 缓存功能
  • 计数器
  • 共享用户 Session
  • 分布式锁

    结合 lua
  • 限速器

    结合 lua
  • 并发控制器

    结合 lua

    比如控制热点行更新之类的, 可参考 Laravel 6.x 的 Illuminate\Redis\Limiters\ConcurrencyLimiter

Set 集合

  • 无序集合, 自动去重
  • 支持交集, 并集, 差集操作

使用场景

  • 排重
  • 共同XX(共同好友....)

ZSet 有序集合

使用场景

  • Top N 排行榜

    复合排序

List 列表

使用场景

  • New Top N
  • 消息队列

    lpush, rbpop

    Laravel的异步队列

    • Listqueues:default 等待执行的队列任务, rpush 和 lpop
    • Sorted Setqueues:default:delayed 延迟任务队列(包括失败重试)
    • Sorted Setqueue:default:reserved 保留任务队列(执行中的队列, 避免脚本崩溃)

    取任务:

    1. queues:default 取出任务执行时会原子性地将其加入queue:default:reserved队列(score为过期时间), 同时增加其 attempts 次数.
    2. 任务执行完毕, 移除 queue:default:reserved 中该任务. 若失败且失败次数未达到限制则插入到 queues:default:delayed 等待重试

Hash 哈希表

  • 适合存放相关的一组数据, 节省内存占用.

HyperLogLog 基数统计

使用少量固定大小的内存(12kb+)来存储集合中的唯一元素.

计数较小时采用稀疏矩阵, 较大时会自动转为稠密矩阵(此时占用12KB), 标准误差: 0.81%

大数据量下的近似基数统计

若要完全精确的, 则考虑 set 或 bitmap

使用场景

  • UV 统计

    UV: 独立访客.

    可通过 PFMERGE 合并统计所有页面的 UV 统计

Bitmap 位图

其实也是 String, 支持按位操作.

使用场景

  • 节省空间的统计

    精确的数量统计
  • 布隆过滤器

    重复判定.

    比如爬虫在对URL去重时, 数亿的 URL, 使用布隆过滤器可以大幅降低去重存储消耗, 代价仅仅是一小部分页面错过而已.

    垃圾邮件过滤功能.

Geo 地理位置

使用场景

  • 附近的人

Pub/Sub 发布订阅

使用场景

  • 消息广播

    之前用 Go 写了一个短链接应用, 多个节点之间利用 redis 的发布订阅功能来异步移除已创建的短链在各个节点的本地缓存.

Stream

Redis 5.0 新增该类型: 持久化的发布/订阅系统

个人暂时实际去使用.

Stream 特点

  • 消息 ack 后仅标记删除, 需自行控制最大消息队列长度

    对于消息量大且需持久保存, 还是建议 Kafka
  • 支持消费者组(Consumer Group)

    保证每个消息只会被消费者组内的其中一个消费者消费一次.

相关链接:

CAS, Check And Set

理解为乐观锁

Check And Set, CAS 机制

CAS 是一种乐观锁机制

  1. WATCH 键

    此时可以读取键值, 若不符合逻辑要求, 则可 UNWATCH 键, 并取消后续操作
  2. MULTI 开启事务
  3. ... 修改键值
  4. EXEC 执行事务

    此时若 watch 的键有任意一个在 WATCH 之后, EXEC 之前 值发生变化, 则事务取消.

伪代码

WATCH mykey

val = GET mykey
if (val <= 0) {
    return;
}

MULTI
INCRBY mykey -1
EXEC
若在 WATCH 之后, EXEC 之前, 其他客户端修改了键 mykey, 则当前客户端事务会被取消. 此时需要做的就是不断重复上面步骤, 直到不满足条件 或 事务成功.

题外话: 其实用Lua脚本来实现更给力, 如下:

eval 'if redis.call("get", KEYS[1]) >= ARGV[1] then redis.call("incrby", KEYS[1], -ARGV[1]) return 1 else return 0 end' 1 mykey 1

内部数据结构

sdshdr

struct sdshdr{
    int len;
    int free;
    char buf[];
}

ziplist 和 quicklist

内存淘汰策略

Redis 在内存不足时的淘汰策略:

  1. volatile-lru:从设置了过期时间的数据集中,选择最近最久未使用的数据释放
  2. allkeys-lru:从数据集中(包括设置过期时间以及未设置过期时间的数据集中),选择最近最久未使用的数据释放
  3. volatile-random:从设置了过期时间的数据集中,随机选择一个数据进行释放
  4. allkeys-random:从数据集中(包括了设置过期时间以及未设置过期时间)随机选择一个数据进行入释放
  5. volatile-ttl:从设置了过期时间的数据集中,选择马上就要过期的数据进行释放操作
  6. noeviction(默认):返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)

在Redis中LRU算法是一个近似 LRU 算法. 默认情况下,Redis随机挑选5个键,并且从中选取一个最近最久未使用的key进行淘汰,在配置文件中可以通过maxmemory-samples的值来设置redis需要检查key的个数,但是栓查的越多,耗费的时间也就越久,但是结构越精确(也就是Redis从内存中淘汰的对象未使用的时间也就越久~).

Redis 3.0 在使用 10 个采样键时效果接近原始 LRU 算法.

数据过期删除策略

一般的策略:

  • 定时删除: 在设置键过期的时间同时,创建一个定时器,让定时器在键过期时间来临,立即执行对键的删除操作。

    对CPU不友好, 性能影响
  • 惰性删除: 放任键过期不管,但是每次从键空间获取键时,都会检查该键是否过期,如果过期的话,就删除该键。

    对内存不友好
  • 定期删除: 每隔一段时间,程序都要对数据库进行一次检查,删除里面的过期键,至于要删除多少过期键,由算法而定。

Redis 的实现是 惰性删除 和 定期删除 同时使用.

持久化

参考

Redis 提供了两种持久化方式:

  1. RDB 持久化: 生成某个时间点的快照文件
  2. AOF 持久化(append only file): 日志追加模式(Redis协议格式保存)

Redis可以同时使用以上两种持久化, 但在启动时会优先使用 AOF 文件恢复.

Redis 4.0 之后存在一种新的持久化模式: 混合持久化

将rdb的文件和局部增量的aof文件相结合,rdb可以使用相隔较长的时间保存策略,aof不需要是全量日志,只需要保存前一次rdb存储开始到这段时间增量aof日志即可,一般来说,这个日志量是非常小的.

RDB

RDB 持久化(快照)

  1. Redis 会 fork 一个子进程, 主进程继续处理请求.

    关于 fork: fork的子进程占用内存大小等同于父进程, 理论需要2倍内存来完成持久化, 但 Linux 的 copy on write 机制可以让子进程共享父进程内存快照.仅当父进程做内存修改操作时, 才会创建所修改内存页的副本.
  2. 子进程将内存中数据写入到一个紧凑的文件中

    它保存的是某个时间点的完整数据

备份策略

  • 可以对 rdb 文件备份, 比如保存最近24小时的每小时备份文件,每个月每天的备份文件,便于遇到问题时恢复。
  • Redis 启动时会从 rdb 文件中恢复数据到内存, 因此恢复数据时只需将redis关闭后,将备份的rdb文件替换当前的rdb文件,再启动Redis即可。

    注意移除 aof 文件, 否则会优先从 aof 恢复.

优点

  • rdb文件体积比较小, 适合备份及传输
  • 性能会比 aof 好(aof 需要写入日志到文件中)
  • rdb 恢复比 aof 要更快

缺点

  • 服务器故障时会丢失最后一次备份之后的数据
  • Redis 保存rdb时, fork子进程的这个操作期间, Redis服务会停止响应(一般是毫秒级),但如果数据量大且cpu时间紧张,则停止响应的时间可能长达1秒

相关配置

################################ SNAPSHOTTING  ################################
# 快照配置
# 注释掉“save”这一行配置项就可以让保存数据库功能失效
# 设置redis进行数据库镜像的频率。
# 900秒(15分钟)内至少1个key值改变(则进行数据库保存--持久化) 
# 300秒(5分钟)内至少10个key值改变(则进行数据库保存--持久化) 
# 60秒(1分钟)内至少10000个key值改变(则进行数据库保存--持久化)
save 900 1
save 300 10
save 60 10000

#当RDB持久化出现错误后,是否停止,yes:停止工作,no:可以继续进行工作,可以通过info中的rdb_last_bgsave_status了解RDB持久化是否有错误
stop-writes-on-bgsave-error yes

#使用压缩rdb文件,rdb文件压缩使用LZF压缩算法,yes:压缩,但是需要一些cpu的消耗。no:不压缩,需要更多的磁盘空间
rdbcompression yes

#是否校验rdb文件。从rdb格式的第五个版本开始,在rdb文件的末尾会带上CRC64的校验和。这跟有利于文件的容错性,但是在保存rdb文件的时候,会有大概10%的性能损耗,所以如果你追求高性能,可以关闭该配置。
rdbchecksum yes

#rdb文件的名称
dbfilename dump.rdb

#数据目录,数据库的写入会在这个目录。rdb、aof文件也会写在这个目录
dir /var/lib/redis

AOF

AOF 其实就是将客户端每一次操作记录追加到指定的aof(日志)文件中,在aof文件体积多大时可以自动在后台重写aof文件(期间不影响正常服务,中途磁盘写满或停机等导致失败也不会丢失数据)

aof 持久化的 fsync 策略:

  • no : 不执行 fsync, 由操作系统保证数据同步到磁盘(linux 默认30秒), 速度最快
  • always : 每次写入都执行 fsync, 保证数据同步到磁盘(最影响性能)
  • everysec : 每秒执行一次 fsync, 最多丢失最近1s的数据(推荐)
fsync:同步内存中所有已修改的文件数据到储存设备

优点:

  • 充分保证数据的持久化,正确的配置一般最多丢失1秒的数据
  • aof 文件内容是以Redis协议格式保存, 易读
  • 日志是追加顺序写入.

缺点:

  • aof 文件比 rdb 快照大
  • 使用 aof 文件恢复比 rdb 慢
  • aof 重写会造成短暂阻塞, 并消耗大量磁盘 IO

AOF 重写(bgrewriteaof)

  1. fork 子进程
  2. 子进程通过读取服务器当前的数据库状态, 并将其转化为操作命令写入 aof 临时文件

    会复制主进程的空间内存页表, 对于 10GB 的 Redis 进程,需要复制大约 20MB 的内存页表
  3. 在此期间主进程会将新的命令同时写到 AOF缓冲区 和 AOF重写缓冲区.
  4. 子进程写入完毕后, 主进程将 AOF 重写缓冲区的内容写入 aof 临时文件, 并原子覆盖原 aof 文件.

何时会触发 AOF 重写

  • 当前没有 BGSAVE(RDB持久化)
  • 当前没有 BGREWRITEAOF进行
  • 用户调用 BGREWRITEAOF 手动触发
  • 根据条件自动触发

    • aof 文件大小大于 auto-aof-rewrite-min-size, 且大于增长比率(相对上一次重写后的aof文件大小)大于 auto-aof-rewrite-percentage

相关配置参数

############################## APPEND ONLY MODE ###############################
#默认redis使用的是rdb方式持久化,这种方式在许多应用中已经足够用了。但是redis如果中途宕机,会导致可能有几分钟的数据丢失,根据save来策略进行持久化,Append Only File是另一种持久化方式,可以提供更好的持久化特性。Redis会把每次写入的数据在接收后都写入 appendonly.aof 文件,每次启动时Redis都会先把这个文件的数据读入内存里,先忽略RDB文件。
appendonly yes

#aof文件名, 保存目录由 dir 参数决定
appendfilename "appendonly.aof"

#aof持久化策略的配置
#no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快。
#always表示每次写入都执行fsync,以保证数据同步到磁盘。
#everysec表示每秒执行一次fsync,可能会导致丢失这1s数据。
appendfsync everysec

# 在aof重写或者写入rdb文件的时候,会执行大量IO,此时对于everysec和always的aof模式来说,执行fsync会造成阻塞过长时间,no-appendfsync-on-rewrite字段设置为默认设置为no。如果对延迟要求很高的应用,这个字段可以设置为yes,否则还是设置为no,这样对持久化特性来说这是更安全的选择。设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no,建议yes。Linux的默认fsync策略是30秒。可能丢失30秒数据。
no-appendfsync-on-rewrite no

#aof自动重写配置。当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写,即当aof文件增长到一定大小的时候Redis能够调用bgrewriteaof对日志文件进行重写。当前AOF文件大小是上次日志重写得到AOF文件大小的二倍(设置为100)时,自动启动新的日志重写过程。
auto-aof-rewrite-percentage 100
#设置允许重写的最小aof文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写
auto-aof-rewrite-min-size 64mb

#aof文件可能在尾部是不完整的,当redis启动的时候,aof文件的数据被载入内存。重启可能发生在redis所在的主机操作系统宕机后,尤其在ext4文件系统没有加上data=ordered选项(redis宕机或者异常终止不会造成尾部不完整现象。)出现这种现象,可以选择让redis退出,或者导入尽可能多的数据。如果选择的是yes,当截断的aof文件被导入的时候,会自动发布一个log给客户端然后load。如果是no,用户必须手动redis-check-aof修复AOF文件才可以。
aof-load-truncated yes

RESP 通讯协议

RESP 是redis客户端和服务端之前使用的一种文本通讯协议

RESP 的特点:实现简单、快速解析、可读性好

间隔符号: \r\n

简单字符串 Simple Strings

+ <字符串>\r\n

字符串不能包含 CR或者 LF(不允许换行)

若要发送二进制安全的字符串, 推荐使用 Bulk Strings

eg. +OK\r\n

错误 Errors

-<错误前缀> <错误信息> \r\n

eg. -Error unknow command 'foobar'\r\n

错误信息不能包含 CR或者 LF(不允许换行),Errors与Simple Strings很相似,不同的是Erros会被当作异常来看待

整数型 Integer

:数字+r+n

eg. :1000\r\n

大字符串类型 Bulk Strings

$<字符串的长度>\r\n<字符串>\r\n

字符串不能包含 CR或者 LF(不允许换行);

eg. $6\r\nfoobar\r\n, 空字符串 $0\r\n\r\n, null $-1\r\n

数组类型 Arrays

*<数组元素个数>\r\n<其他所有类型> (结尾不需要rn)

注意:只有元素个数后面的rn是属于该数组的,结尾的rn一般是元素的

eg.

空数组
"*0\r\n"      

数组包含2个元素,分别是字符串foo和bar
"*2\r\n$2\r\nfoo\r\n$3\r\nbar\r\n"      

数组包含3个整数:1、2、3
"*3\r\n:1\r\n:2\r\n:3\r\n"       

包含混合类型的数组
"*5\r\n:1\r\n:2\r\n:3\r\n:4\r\n$6\r\nfoobar\r\n"  

Null数组
"*-1\r\n"         

数组嵌套,外层数组包含2个数组,
"*2\r\n*3\r\n:1\r\n:2\r\n:3\r\n*2\r\n+Foo\r\n-Bar\r\n"   
整理后如下:
*2\r\n
*3\r\n:1\r\n:2\r\n:3\r\n
*2\r\n+Foo\r\n-Bar\r\n

Lua

参考链接

执行 Lua

redis-cli --eval xxx.lua key1 key2 , argv1 argv2

注意:

  • key 和 argv 之间要用逗号分隔

示例

每分钟统计信息

-- KEYS 每分钟总负载, ZSET
-- KEYS 等待队列, LIST

-- ARGV[1] worker序号
-- ARGV[2] 本分钟的时间戳
-- ARGV[3] 本分钟的时间戳对应的字符串 y-m-d H:i:s
-- ARGV[4] 本分钟内的总战斗耗时
-- ARGV[5] 本分钟内的战斗次数
-- ARGV[6] 本分钟内的战斗丢弃次数(已包含在上面), 指的是战斗延迟过久, worker主动抛弃
-- ARGV[7] 本分钟内的战斗出错次数(已包含在上面), 指的是战斗在lua层面上出错
-- ARGV[8] worker进程内存占用
-- ARGV[9] worker启动次数
-- ARGV[10] 当前时间, 秒(携带小数部分)

local key_load_all = KEYS[1]
local key_wait_list = KEYS[2]

local worker = ARGV[1]
local cur_time = tonumber(ARGV[2])
local cur_time_str = ARGV[3]
local battle_time = tonumber(ARGV[4])
local battle_cnt = tonumber(ARGV[5])
local battle_discard_cnt = tonumber(ARGV[6])
local battle_error_cnt = tonumber(ARGV[7])
local worker_res = tonumber(ARGV[8])
local worker_start_cnt = tonumber(ARGV[9])
local time_now = ARGV[10]


local to_del

-- 每分钟的总体负载
local load_all = redis.call('ZRANGEBYSCORE', key_load_all, cur_time, cur_time);

if next(load_all) ~= nil then
    to_del = load_all[1]
    load_all = cjson.decode(load_all[1])
else
    load_all = {t = cur_time, ts = cur_time_str, battle_cost_time = 0, battle_cnt = 0, battle_error_cnt = 0, battle_discard_cnt = 0, wait_list = 0, worker_list = {}}
end
-- 总的累计, 方便查看
load_all["battle_cost_time"] = load_all["battle_cost_time"] + battle_time
load_all["battle_cnt"] = load_all["battle_cnt"] + battle_cnt
load_all["battle_error_cnt"] = load_all["battle_error_cnt"] + battle_error_cnt
load_all["battle_discard_cnt"] = load_all["battle_discard_cnt"] + battle_discard_cnt

-- 累计各个worker
if load_all["worker_list"][worker] == nil then
    load_all["worker_list"][worker] = {battle_cost_time = 0, battle_cnt = 0, battle_error_cnt = 0, battle_discard_cnt = 0, process_res = 0, start_cnt = 0}
end
load_all["worker_list"][worker]["battle_cost_time"] = load_all["worker_list"][worker]["battle_cost_time"] + battle_time
load_all["worker_list"][worker]["battle_cnt"] = load_all["worker_list"][worker]["battle_cnt"] + battle_cnt
load_all["worker_list"][worker]["battle_error_cnt"] = load_all["worker_list"][worker]["battle_error_cnt"] + battle_error_cnt
load_all["worker_list"][worker]["battle_discard_cnt"] = load_all["worker_list"][worker]["battle_discard_cnt"] + battle_discard_cnt
-- load_all["worker_list"][worker]["process_res"] = math.max(worker_res, load_all["worker_list"][worker]["process_res"])
load_all["worker_list"][worker]["process_res"] = worker_res
load_all["worker_list"][worker]["start_cnt"] = load_all["worker_list"][worker]["start_cnt"] + worker_start_cnt

-- 任务队列长度
local wait_list = tonumber(redis.call("LLEN", key_wait_list))
load_all["wait_list"] = math.max(load_all["wait_list"], wait_list)
if wait_list > 0 then
    local wait_list_item = redis.call("LINDEX", key_wait_list, -1)
    load_all["wait_longest"] = time_now - cjson.decode(wait_list_item)["BattleStartMicoTime"]
end


redis.call("ZADD", key_load_all, cur_time, cjson.encode(load_all))
if to_del then
    redis.call("ZREM", key_load_all, to_del)
    to_del = nil
end

return 1

redis-cli 命令行客户端

# 检查当前有哪些 big key
redis-cli --bigkeys -i 0.1

big key 会导致集群中访问 big key 时压力倾斜向对应的实例.

除了使用 redis-cli 外, 还可以使用其他工具(rdbtools)对 rdb 文件分析查找 big key.

优化原则

  • hash: 对 field 取模, 进行哈希, 分成多个小的 hash
# 查看与Redis 的延迟(基于PING指令), 单位: 毫秒
redis-cli --latency
# 查看简单状态
redis-cli --stat
# 输出如下
# ------- data ------ --------------------- load -------------------- - child -
#keys       mem      clients blocked requests            connections
#133        3.59M    15      0       115095975 (+0)      15035835

Redis 3.* 默认配置文件中文解释

#redis.conf
# Redis configuration file example.
# ./redis-server /path/to/redis.conf

################################## INCLUDES ###################################
#这在你有标准配置模板但是每个redis服务器又需要个性设置的时候很有用。
# include /path/to/local.conf
# include /path/to/other.conf

################################ GENERAL #####################################

#是否在后台执行,yes:后台运行;no:不是后台运行(老版本默认)
daemonize yes

  #3.2里的参数,是否开启保护模式,默认开启。要是配置里没有指定bind和密码。开启该参数后,redis只会本地进行访问,拒绝外部访问。要是开启了密码   和bind,可以开启。否   则最好关闭,设置为no。
  protected-mode yes
#redis的进程文件
pidfile /var/run/redis/redis-server.pid

#redis监听的端口号。
port 6379

#此参数确定了TCP连接中已完成队列(完成三次握手之后)的长度, 当然此值必须不大于Linux系统定义的/proc/sys/net/core/somaxconn值,默认是511,而Linux的默认参数值是128。当系统并发量大并且客户端速度缓慢的时候,可以将这二个参数一起参考设定。该内核参数默认值一般是128,对于负载很大的服务程序来说大大的不够。一般会将它修改为2048或者更大。在/etc/sysctl.conf中添加:net.core.somaxconn = 2048,然后在终端中执行sysctl -p。
tcp-backlog 511

#指定 redis 只接收来自于该 IP 地址的请求,如果不进行设置,那么将处理所有请求
bind 127.0.0.1

#配置unix socket来让redis支持监听本地连接。
# unixsocket /var/run/redis/redis.sock
#配置unix socket使用文件的权限
# unixsocketperm 700

# 此参数为设置客户端空闲超过timeout,服务端会断开连接,为0则服务端不会主动断开连接,不能小于0。
timeout 0

#tcp keepalive参数。如果设置不为0,就使用配置tcp的SO_KEEPALIVE值,使用keepalive有两个好处:检测挂掉的对端。降低中间设备出问题而导致网络看似连接却已经与对端端口的问题。在Linux内核中,设置了keepalive,redis会定时给对端发送ack。检测到对端关闭需要两倍的设置值。
tcp-keepalive 0

#指定了服务端日志的级别。级别包括:debug(很多信息,方便开发、测试),verbose(许多有用的信息,但是没有debug级别信息多),notice(适当的日志级别,适合生产环境),warn(只有非常重要的信息)
loglevel notice

#指定了记录日志的文件。空字符串的话,日志会打印到标准输出设备。后台运行的redis标准输出是/dev/null。
logfile /var/log/redis/redis-server.log

#是否打开记录syslog功能
# syslog-enabled no

#syslog的标识符。
# syslog-ident redis

#日志的来源、设备
# syslog-facility local0

#数据库的数量,默认使用的数据库是DB 0。可以通过”SELECT “命令选择一个db
databases 16

################################ SNAPSHOTTING ################################
# 快照配置
# 注释掉“save”这一行配置项就可以让保存数据库功能失效
# 设置sedis进行数据库镜像的频率。
# 900秒(15分钟)内至少1个key值改变(则进行数据库保存--持久化) 
# 300秒(5分钟)内至少10个key值改变(则进行数据库保存--持久化) 
# 60秒(1分钟)内至少10000个key值改变(则进行数据库保存--持久化)
save 900 1
save 300 10
save 60 10000

#当RDB持久化出现错误后,是否依然进行继续进行工作,yes:不能进行工作,no:可以继续进行工作,可以通过info中的rdb_last_bgsave_status了解RDB持久化是否有错误
stop-writes-on-bgsave-error yes

#使用压缩rdb文件,rdb文件压缩使用LZF压缩算法,yes:压缩,但是需要一些cpu的消耗。no:不压缩,需要更多的磁盘空间
rdbcompression yes

#是否校验rdb文件。从rdb格式的第五个版本开始,在rdb文件的末尾会带上CRC64的校验和。这跟有利于文件的容错性,但是在保存rdb文件的时候,会有大概10%的性能损耗,所以如果你追求高性能,可以关闭该配置。
rdbchecksum yes

#rdb文件的名称
dbfilename dump.rdb

#数据目录,数据库的写入会在这个目录。rdb、aof文件也会写在这个目录
dir /var/lib/redis

################################# REPLICATION #################################
#复制选项,slave复制对应的master。
# slaveof <masterip> <masterport>

#如果master设置了requirepass,那么slave要连上master,需要有master的密码才行。masterauth就是用来配置master的密码,这样可以在连上master后进行认证。
# masterauth <master-password>

#当从库同主机失去连接或者复制正在进行,从机库有两种运行方式:1) 如果slave-serve-stale-data设置为yes(默认设置),从库会继续响应客户端的请求。2) 如果slave-serve-stale-data设置为no,除去INFO和SLAVOF命令之外的任何请求都会返回一个错误”SYNC with master in progress”。
slave-serve-stale-data yes

#作为从服务器,默认情况下是只读的(yes),可以修改成NO,用于写(不建议)。
slave-read-only yes

#是否使用socket方式复制数据。目前redis复制提供两种方式,disk和socket。如果新的slave连上来或者重连的slave无法部分同步,就会执行全量同步,master会生成rdb文件。有2种方式:disk方式是master创建一个新的进程把rdb文件保存到磁盘,再把磁盘上的rdb文件传递给slave。socket是master创建一个新的进程,直接把rdb文件以socket的方式发给slave。disk方式的时候,当一个rdb保存的过程中,多个slave都能共享这个rdb文件。socket的方式就的一个个slave顺序复制。在磁盘速度缓慢,网速快的情况下推荐用socket方式。
repl-diskless-sync no

#diskless复制的延迟时间,防止设置为0。一旦复制开始,节点不会再接收新slave的复制请求直到下一个rdb传输。所以最好等待一段时间,等更多的slave连上来。
repl-diskless-sync-delay 5

#slave根据指定的时间间隔向服务器发送ping请求。时间间隔可以通过 repl_ping_slave_period 来设置,默认10秒。
# repl-ping-slave-period 10

#复制连接超时时间。master和slave都有超时时间的设置。master检测到slave上次发送的时间超过repl-timeout,即认为slave离线,清除该slave信息。slave检测到上次和master交互的时间超过repl-timeout,则认为master离线。需要注意的是repl-timeout需要设置一个比repl-ping-slave-period更大的值,不然会经常检测到超时。
# repl-timeout 60

#是否禁止复制tcp链接的tcp nodelay参数,可传递yes或者no。默认是no,即使用tcp nodelay。如果master设置了yes来禁止tcp nodelay设置,在把数据复制给slave的时候,会减少包的数量和更小的网络带宽。但是这也可能带来数据的延迟。默认我们推荐更小的延迟,但是在数据量传输很大的场景下,建议选择yes。
repl-disable-tcp-nodelay no

#复制缓冲区大小,这是一个环形复制缓冲区,用来保存最新复制的命令。这样在slave离线的时候,不需要完全复制master的数据,如果可以执行部分同步,只需要把缓冲区的部分数据复制给slave,就能恢复正常复制状态。缓冲区的大小越大,slave离线的时间可以更长,复制缓冲区只有在有slave连接的时候才分配内存。没有slave的一段时间,内存会被释放出来,默认1m。
# repl-backlog-size 5mb

#master没有slave一段时间会释放复制缓冲区的内存,repl-backlog-ttl用来设置该时间长度。单位为秒。
# repl-backlog-ttl 3600

#当master不可用,Sentinel会根据slave的优先级选举一个master。最低的优先级的slave,当选master。而配置成0,永远不会被选举。
slave-priority 100

#redis提供了可以让master停止写入的方式,如果配置了min-slaves-to-write,健康的slave的个数小于N,mater就禁止写入。master最少得有多少个健康的slave存活才能执行写命令。这个配置虽然不能保证N个slave都一定能接收到master的写操作,但是能避免没有足够健康的slave的时候,master不能写入来避免数据丢失。设置为0是关闭该功能。
# min-slaves-to-write 0

#延迟小于min-slaves-max-lag秒的slave才认为是健康的slave。
# min-slaves-max-lag 10

# 设置1或另一个设置为0禁用这个特性。
# Setting one or the other to 0 disables the feature.
# By default min-slaves-to-write is set to 0 (feature disabled) and
# min-slaves-max-lag is set to 10.

################################## SECURITY ###################################
#requirepass配置可以让用户使用AUTH命令来认证密码,才能使用其他命令。这让redis可以使用在不受信任的网络中。为了保持向后的兼容性,可以注释该命令,因为大部分用户也不需要认证。使用requirepass的时候需要注意,因为redis太快了,每秒可以认证15w次密码,简单的密码很容易被攻破,所以最好使用一个更复杂的密码。
# requirepass foobared

#把危险的命令给修改成其他名称。比如CONFIG命令可以重命名为一个很难被猜到的命令,这样用户不能使用,而内部工具还能接着使用。
# rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52

#设置成一个空的值,可以禁止一个命令
# rename-command CONFIG ""
################################### LIMITS ####################################

# 设置能连上redis的最大客户端连接数量。默认是10000个客户端连接。由于redis不区分连接是客户端连接还是内部打开文件或者和slave连接等,所以maxclients最小建议设置到32。如果超过了maxclients,redis会给新的连接发送’max number of clients reached’,并关闭连接。
# maxclients 10000

#redis配置的最大内存容量。当内存满了,需要配合maxmemory-policy策略进行处理。注意slave的输出缓冲区是不计算在maxmemory内的。所以为了防止主机内存使用完,建议设置的maxmemory需要更小一些。
# maxmemory <bytes>

#内存容量超过maxmemory后的处理策略。
#volatile-lru:利用LRU算法移除设置过过期时间的key。
#volatile-random:随机移除设置过过期时间的key。
#volatile-ttl:移除即将过期的key,根据最近过期时间来删除(辅以TTL)
#allkeys-lru:利用LRU算法移除任何key。
#allkeys-random:随机移除任何key。
#noeviction:不移除任何key,只是返回一个写错误。
#上面的这些驱逐策略,如果redis没有合适的key驱逐,对于写命令,还是会返回错误。redis将不再接收写请求,只接收get请求。写命令包括:set setnx setex append incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby getset mset msetnx exec sort。
# maxmemory-policy noeviction

#lru检测的样本数。使用lru或者ttl淘汰算法,从需要淘汰的列表中随机选择sample个key,选出闲置时间最长的key移除。
# maxmemory-samples 5

############################## APPEND ONLY MODE ###############################
#默认redis使用的是rdb方式持久化,这种方式在许多应用中已经足够用了。但是redis如果中途宕机,会导致可能有几分钟的数据丢失,根据save来策略进行持久化,Append Only File是另一种持久化方式,可以提供更好的持久化特性。Redis会把每次写入的数据在接收后都写入 appendonly.aof 文件,每次启动时Redis都会先把这个文件的数据读入内存里,先忽略RDB文件。
appendonly no

#aof文件名
appendfilename "appendonly.aof"

#aof持久化策略的配置
#no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快。
#always表示每次写入都执行fsync,以保证数据同步到磁盘。
#everysec表示每秒执行一次fsync,可能会导致丢失这1s数据。
appendfsync everysec

# 在aof重写或者写入rdb文件的时候,会执行大量IO,此时对于everysec和always的aof模式来说,执行fsync会造成阻塞过长时间,no-appendfsync-on-rewrite字段设置为默认设置为no。如果对延迟要求很高的应用,这个字段可以设置为yes,否则还是设置为no,这样对持久化特性来说这是更安全的选择。设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no,建议yes。Linux的默认fsync策略是30秒。可能丢失30秒数据。
no-appendfsync-on-rewrite no

#aof自动重写配置。当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写,即当aof文件增长到一定大小的时候Redis能够调用bgrewriteaof对日志文件进行重写。当前AOF文件大小是上次日志重写得到AOF文件大小的二倍(设置为100)时,自动启动新的日志重写过程。
auto-aof-rewrite-percentage 100
#设置允许重写的最小aof文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写
auto-aof-rewrite-min-size 64mb

#aof文件可能在尾部是不完整的,当redis启动的时候,aof文件的数据被载入内存。重启可能发生在redis所在的主机操作系统宕机后,尤其在ext4文件系统没有加上data=ordered选项(redis宕机或者异常终止不会造成尾部不完整现象。)出现这种现象,可以选择让redis退出,或者导入尽可能多的数据。如果选择的是yes,当截断的aof文件被导入的时候,会自动发布一个log给客户端然后load。如果是no,用户必须手动redis-check-aof修复AOF文件才可以。
aof-load-truncated yes

################################ LUA SCRIPTING ###############################
# 如果达到最大时间限制(毫秒),redis会记个log,然后返回error。当一个脚本超过了最大时限。只有SCRIPT KILL和SHUTDOWN NOSAVE可以用。第一个可以杀没有调write命令的东西。要是已经调用了write,只能用第二个命令杀。
lua-time-limit 5000

################################ REDIS CLUSTER ###############################
#集群开关,默认是不开启集群模式。
# cluster-enabled yes

#集群配置文件的名称,每个节点都有一个集群相关的配置文件,持久化保存集群的信息。这个文件并不需要手动配置,这个配置文件有Redis生成并更新,每个Redis集群节点需要一个单独的配置文件,请确保与实例运行的系统中配置文件名称不冲突
# cluster-config-file nodes-6379.conf

#节点互连超时的阀值。集群节点超时毫秒数
# cluster-node-timeout 15000

#在进行故障转移的时候,全部slave都会请求申请为master,但是有些slave可能与master断开连接一段时间了,导致数据过于陈旧,这样的slave不应该被提升为master。该参数就是用来判断slave节点与master断线的时间是否过长。判断方法是:
#比较slave断开连接的时间和(node-timeout * slave-validity-factor) + repl-ping-slave-period
#如果节点超时时间为三十秒, 并且slave-validity-factor为10,假设默认的repl-ping-slave-period是10秒,即如果超过310秒slave将不会尝试进行故障转移 
# cluster-slave-validity-factor 10

#master的slave数量大于该值,slave才能迁移到其他孤立master上,如这个参数若被设为2,那么只有当一个主节点拥有2 个可工作的从节点时,它的一个从节点会尝试迁移。
# cluster-migration-barrier 1

#默认情况下,集群全部的slot有节点负责,集群状态才为ok,才能提供服务。设置为no,可以在slot没有全部分配的时候提供服务。不建议打开该配置,这样会造成分区的时候,小分区的master一直在接受写请求,而造成很长时间数据不一致。
# cluster-require-full-coverage yes

################################## SLOW LOG ###################################
###slog log是用来记录redis运行中执行比较慢的命令耗时。当命令的执行超过了指定时间,就记录在slow log中,slog log保存在内存中,所以没有IO操作。
#执行时间比slowlog-log-slower-than大的请求记录到slowlog里面,单位是微秒,所以1000000就是1秒。注意,负数时间会禁用慢查询日志,而0则会强制记录所有命令。
slowlog-log-slower-than 10000

#慢查询日志长度。当一个新的命令被写进日志的时候,最老的那个记录会被删掉。这个长度没有限制。只要有足够的内存就行。你可以通过 SLOWLOG RESET 来释放内存。
slowlog-max-len 128

################################ LATENCY MONITOR ##############################
#延迟监控功能是用来监控redis中执行比较缓慢的一些操作,用LATENCY打印redis实例在跑命令时的耗时图表。只记录大于等于下边设置的值的操作。0的话,就是关闭监视。默认延迟监控功能是关闭的,如果你需要打开,也可以通过CONFIG SET命令动态设置。
latency-monitor-threshold 0

############################# EVENT NOTIFICATION ##############################
#键空间通知使得客户端可以通过订阅频道或模式,来接收那些以某种方式改动了 Redis 数据集的事件。因为开启键空间通知功能需要消耗一些 CPU ,所以在默认配置下,该功能处于关闭状态。
#notify-keyspace-events 的参数可以是以下字符的任意组合,它指定了服务器该发送哪些类型的通知:
##K 键空间通知,所有通知以 __keyspace@__ 为前缀
##E 键事件通知,所有通知以 __keyevent@__ 为前缀
##g DEL 、 EXPIRE 、 RENAME 等类型无关的通用命令的通知
##$ 字符串命令的通知
##l 列表命令的通知
##s 集合命令的通知
##h 哈希命令的通知
##z 有序集合命令的通知
##x 过期事件:每当有过期键被删除时发送
##e 驱逐(evict)事件:每当有键因为 maxmemory 政策而被删除时发送
##A 参数 g$lshzxe 的别名
#输入的参数中至少要有一个 K 或者 E,否则的话,不管其余的参数是什么,都不会有任何 通知被分发。详细使用可以参考http://redis.io/topics/notifications

notify-keyspace-events ""

############################### ADVANCED CONFIG ###############################
#数据量小于等于hash-max-ziplist-entries的用ziplist,大于hash-max-ziplist-entries用hash
hash-max-ziplist-entries 512
#value大小小于等于hash-max-ziplist-value的用ziplist,大于hash-max-ziplist-value用hash。
hash-max-ziplist-value 64

#数据量小于等于list-max-ziplist-entries用ziplist,大于list-max-ziplist-entries用list。
list-max-ziplist-entries 512
#value大小小于等于list-max-ziplist-value的用ziplist,大于list-max-ziplist-value用list。
list-max-ziplist-value 64

#数据量小于等于set-max-intset-entries用iniset,大于set-max-intset-entries用set。
set-max-intset-entries 512

#数据量小于等于zset-max-ziplist-entries用ziplist,大于zset-max-ziplist-entries用zset。
zset-max-ziplist-entries 128
#value大小小于等于zset-max-ziplist-value用ziplist,大于zset-max-ziplist-value用zset。
zset-max-ziplist-value 64

#value大小小于等于hll-sparse-max-bytes使用稀疏数据结构(sparse),大于hll-sparse-max-bytes使用稠密的数据结构(dense)。一个比16000大的value是几乎没用的,建议的value大概为3000。如果对CPU要求不高,对空间要求较高的,建议设置到10000左右。
hll-sparse-max-bytes 3000

#Redis将在每100毫秒时使用1毫秒的CPU时间来对redis的hash表进行重新hash,可以降低内存的使用。当你的使用场景中,有非常严格的实时性需要,不能够接受Redis时不时的对请求有2毫秒的延迟的话,把这项配置为no。如果没有这么严格的实时性要求,可以设置为yes,以便能够尽可能快的释放内存。
activerehashing yes

##对客户端输出缓冲进行限制可以强迫那些不从服务器读取数据的客户端断开连接,用来强制关闭传输缓慢的客户端。
#对于normal client,第一个0表示取消hard limit,第二个0和第三个0表示取消soft limit,normal client默认取消限制,因为如果没有寻问,他们是不会接收数据的。
client-output-buffer-limit normal 0 0 0
#对于slave client和MONITER client,如果client-output-buffer一旦超过256mb,又或者超过64mb持续60秒,那么服务器就会立即断开客户端连接。
client-output-buffer-limit slave 256mb 64mb 60
#对于pubsub client,如果client-output-buffer一旦超过32mb,又或者超过8mb持续60秒,那么服务器就会立即断开客户端连接。
client-output-buffer-limit pubsub 32mb 8mb 60

#redis执行任务的频率为1s除以hz。
hz 10

#在aof重写的时候,如果打开了aof-rewrite-incremental-fsync开关,系统会每32MB执行一次fsync。这对于把文件写入磁盘是有帮助的,可以避免过大的延迟峰值。
aof-rewrite-incremental-fsync yes
查看原文

赞 0 收藏 0 评论 0

嘉兴ing 发布了文章 · 1月19日

⭐《ElasticSearch核心技术与实战》笔记 - 5. 应用实战

[TOC]

电影搜索服务

需求分析及架构设计

img

img

img

img

img

https://www.elastic.co/blog/e...

img

img

将电影数据导入 Elasticsearch

img

img

img

img

img

搭建你的电影搜索服务

img

img

img

img

img

img

img

Stackoverflow 用户调查问卷分析

需求分析与架构设计

img

img

img

img

img

https://insights.stackoverflo...

数据 Extract & Enrichment

  1. 分析数据 StackOverflow 2020 年度开发者调查
  2. 使用 Logstash 导入数据到 ES
  3. ES 中进行相关配置

    1. Ingest 分割字段及转换字段类型
    2. 使用 dynamic template 设置导入数据的字段类型
  4. 在下面相关操作执行完毕后, 创建 Index Pattern

    Kibana 中的操作: 在 Management 的 Index Patterns 中创建名为 "final-stackoverflow-survey" 的 Index Pattern, 只包含 final-stackoverflow-survey 这一个索引.

logstash 相关配置: stackoverflow-surver.conf

input {
    file {
        # 这里的路径必须是绝对路径, 不能是相对路径
        path => ["D:/Programming/logstash-7.9.3/survey_results_public.csv"]
        start_position => "beginning"
        #        sincedb_path => "/dev/null"
        # windows 上没有 /dev/null, 使用 nul 作为替代.
        sincedb_path => "nul"
    }
}

filter {
    csv {
        autogenerate_column_names => false
        skip_empty_columns => true
        separator => ","
        columns => [
            "Respondent",
            "MainBranch",
            "Hobbyist",
            "Age",
            "Age1stCode",
            "CompFreq",
            "CompTotal",
            "ConvertedComp",
            "Country",
            "CurrencyDesc",
            "CurrencySymbol",
            "DatabaseDesireNextYear",
            "DatabaseWorkedWith",
            "DevType",
            "EdLevel",
            "Employment",
            "Ethnicity",
            "Gender",
            "JobFactors",
            "JobSat",
            "JobSeek",
            "LanguageDesireNextYear",
            "LanguageWorkedWith",
            "MiscTechDesireNextYear",
            "MiscTechWorkedWith",
            "NEWCollabToolsDesireNextYear",
            "NEWCollabToolsWorkedWith",
            "NEWDevOps",
            "NEWDevOpsImpt",
            "NEWEdImpt",
            "NEWJobHunt",
            "NEWJobHuntResearch",
            "NEWLearn",
            "NEWOffTopic",
            "NEWOnboardGood",
            "NEWOtherComms",
            "NEWOvertime",
            "NEWPurchaseResearch",
            "NEWPurpleLink",
            "NEWSOSites",
            "NEWStuck",
            "OpSys",
            "OrgSize",
            "PlatformDesireNextYear",
            "PlatformWorkedWith",
            "PurchaseWhat",
            "Sexuality",
            "SOAccount",
            "SOComm",
            "SOPartFreq",
            "SOVisitFreq",
            "SurveyEase",
            "SurveyLength",
            "Trans",
            "UndergradMajor",
            "WebframeDesireNextYear",
            "WebframeWorkedWith",
            "WelcomeChange",
            "WorkWeekHrs",
            "YearsCode",
            "YearsCodePro"
        ]
    }

    # ??? 没看懂
    if ([collector] == "collector") {
        drop {

        }
    }

    # 移除部分字段
    mutate {
        remove_field => ["message", "@version", "@timestamp", "host"]
    }
}

output {
    # 方便显示处理进度
    stdout {
        codec => "dots"
    }
    
    # 写入 es 的 stackoverflow-survey-raw 索引中
    elasticsearch {
        hosts => ["http://localhost:9200"]
        index => "stackoverflow-survey-raw"
        document_type => "_doc"
    }
}
  • Input Plugin

    • File Input
  • Filter Plugin

    • CSV Filter
    • Mutate Filter
  • Output Plugin

    • ES Output

windows 下运行 logstash 导入数据的示例

.\bin\logstash.bat -f .\stackoverflow-surver.conf

在 Kibana 中执行如下

DELETE stackoverflow-survey-raw

// 查看写入数据的字段类型, 发现都是 strng
// 由于我们不需要对这些数据进行全文搜索, 同时有聚合的需求, 因此需要将其指定为 keyword 类型
GET stackoverflow-survey-raw


// 设置 dynamic mapping
PUT final-stackoverflow-survey
{
  "mappings": {
    // 将所有的 string 类型转换为 keyword 类型
    "dynamic_templates": [
      {
        "string_as_keyword": {
          "match_mapping_type": "string",
          "mapping": {
            "type": "keyword"
          }
        }
      }
    ]
  },   
  "settings": {
    // 副本分片数设为 0
    "number_of_replicas": 0
  }
}

GET stackoverflow-survey-raw/_search
  • 使用 Dynamic Template 处理文本类型 Mapping

在 Kibana 中执行如下

// 创建一个 Ingest Pipeline, 对部分字段进行分割, 及格式转换操作
PUT _ingest/pipeline/stackoverflow_pipeline
{
  "description": "Pipeline for stackoverflow survey",
  "processors": [
    {
      "split": {
        "field": "NEWPurchaseResearch",
        "separator": ";"
      }
    },
    {
      "split": {
        "field": "NEWSOSites",
        "separator": ";"
      }
    },
    {
      "split": {
        "field": "NEWStuck",
        "separator": ";"
      }
    },
    {
      "split": {
        "field": "DevType",
        "separator": ";"
      }
    },
    {
      "split": {
        "field": "NEWJobHunt",
        "separator": ";"
      }
    },
    {
      "split": {
        "field": "NEWJobHuntResearch",
        "separator": ";"
      }
    },
    {
      "split": {
        "field": "DatabaseDesireNextYear",
        "separator": ";"
      }
    },
    {
      "split": {
        "field": "DatabaseWorkedWith",
        "separator": ";"
      }
    },
    {
      "split": {
        "field": "LanguageWorkedWith",
        "separator": ";"
      }
    },
    {
      "split": {
        "field": "LanguageDesireNextYear",
        "separator": ";"
      }
    },
    {
      "split": {
        "field": "MiscTechDesireNextYear",
        "separator": ";"
      }
    },
    {
      "split": {
        "field": "MiscTechWorkedWith",
        "separator": ";"
      }
    },
    {
      "split": {
        "field": "PlatformDesireNextYear",
        "separator": ";"
      }
    },
    {
      "split": {
        "field": "PlatformWorkedWith",
        "separator": ";"
      }
    },
    {
      "split": {
        "field": "WebframeWorkedWith",
        "separator": ";"
      }
    },
    {
      "split": {
        "field": "WebframeDesireNextYear",
        "separator": ";"
      }
    },
    {
      "split": {
        "field": "NEWCollabToolsDesireNextYear",
        "separator": ";"
      }
    },
    {
      "split": {
        "field": "NEWCollabToolsWorkedWith",
        "separator": ";"
      }
    },
    {
      "split": {
        "field": "JobFactors",
        "separator": ";"
      }
    },
    {
      "split": {
        "field": "Ethnicity",
        "separator": ";"
      }
    },
    {
      "split": {
        "field": "Sexuality",
        "separator": ";"
      }
    },
    {
      "convert": {
        "field": "YearsCode",
        "type": "integer",
        "on_failure": [
          {
            "set": {
              "field": "YearsCode",
              "value": 0
            }
          }
        ]
      }
    },
    {
      "convert": {
        "field": "WorkWeekHrs",
        "type": "integer",
        "on_failure": [
          {
            "set": {
              "field": "WorkWeekHrs",
              "value": 0
            }
          }
        ]
      }
    },
    {
      "convert": {
        "field": "Age",
        "type": "integer",
        "on_failure": [
          {
            "set": {
              "field": "Age",
              "value": 0
            }
          }
        ]
      }
    },
    {
      "convert": {
        "field": "Age1stCode",
        "type": "integer",
        "on_failure": [
          {
            "set": {
              "field": "Age1stCode",
              "value": 0
            }
          }
        ]
      }
    },
    {
      "convert": {
        "field": "YearsCodePro",
        "type": "integer",
        "on_failure": [
          {
            "set": {
              "field": "YearsCodePro",
              "value": 0
            }
          }
        ]
      }
    }
  ]
}

// 通过 reindex 将 logstash 导入的数据重新导入(应用上述创建的 ingest pipeline)到 final-stackoverflow-survey 索引中
POST _reindex
{
  "source": {
    "index": "stackoverflow-survey-raw"
  },
  "dest": {
    "index": "final-stackoverflow-survey",
    "pipeline": "stackoverflow_pipeline"
  }
}

GET final-stackoverflow-survey

GET final-stackoverflow-survey/_search
  • 创建 Ingest Pipeline

    • Split 一些字符串
    • 转换整形数

构建 Insights Dashboard

image-20201116003055417

image-20201116003120607

image-20201116003149869

image-20201116003224563

image-20201116003246850

Elastic认证

认证

...略

考纲整理

  • 安装配置

    • 根据需求, 配置部署集群
    • 配置集群的节点
    • 为集群设置安全保护
    • 基于 X-Pack, 为集群配置 RBAC
  • 索引数据

    • 根据需求, 定义一个索引
    • 执行索引的 Index, CRUD
    • 定义与使用 Index Alias
    • 定义与使用 Index Template
    • 定义与使用 Dynamic Template
    • 使用 Reindex API & Update By Query 重新索引文档
    • 定义 Ingest Pipeline (包括使用 Painless 脚本)
  • 查询

    • 使用 terms 或 phrase 查询一个或多个字段
    • 使用 Bool query
    • 高亮查询结果
    • 对查询结果排序
    • 对查询结果分页
    • 使用 Scroll API
    • 使用模糊查询
    • 使用 Search Template

      日常工作可能不会使用, 但使用它可以更好的分离 Search 的定义与使用
    • 跨集群搜索
  • 聚合

    • metric & Bucket Aggregation
    • sub-aggregation
    • pipeline aggregation
  • 映射与分词

    • 按需定义索引 mapping
    • 按需自定义 analyzer
    • 为字段定义多字段类型 (不同的字段使用不同的 type 和 analyzer)
    • 定义和查询 nested 文档
    • 定义及查询 parent/child 文档
  • 集群管理

    • 按需将索引的分片分配到特定的节点
    • 为索引配置 Shard allocation awareness & Force awareness
    • 诊断分片的问题, 恢复集群的 health 状态
    • Backup & Restore 集群或者特定的索引
    • 配置一个 hot & warm 架构的集群
    • 配置跨集群搜索

模拟测试

img

img

img

img

img

img

img

img

集群的备份与恢复

image-20201116005419399

image-20201116005428580

  • 这里的 my_fs_backup 是创建的 Repository

image-20201116005731101

image-20201116005759131

  • 可以只为指定索引创建快照

image-20201116005913554

  • restore 之前需要先将索引删除, 否则会报错.

image-20201116005438060

查看原文

赞 0 收藏 0 评论 0

嘉兴ing 发布了文章 · 1月18日

⭐《ElasticSearch核心技术与实战》笔记 - 4. 大数据分析

[TOC]

https://github.com/geektime-g...

用Logstash和Beats构建数据管道

Logstash 入门及架构介绍

架构

Logstash 是 ETL 工具/数据搜集处理引擎, 支持200多个插件.

image-20201106093138536

概念

Pipeline

  • 包含了 input -> filter -> output 三个阶段的处理流程
  • 插件生命周期管理
  • 队列管理

多 Pipelines 实例

- pipeline.id: my-pipeline_1
  path.config: "/etc/path/to/p1.config"
  pipeline.workers: 3
- pipeline.id: my-other-pipeline
  path.config: "/etc/different/path/p2.cfg"
  queue.type: persisted
  • pipeline.workers : Pipeline 线程数, 默认是 cpu 核心数
  • pipeline.batch.size: Batcher 一次批量获取等待处理的文档树, 默认 125. 需结合 jvm.options 调节
  • pipeline.batch.delay: Batcher 等待时间

Logstash Event

  • 数据在内部流转时的具体表现形式。数据在 input 阶段被转换为 Event,在 output 被转化成目标格式数据
  • Event 其实是一个 Java Object, 在配置文件中, 对 Event 的属性进行增删改查.

Queue

image-20201106095615272

Logstash 的 Queue 有两种

  1. In Memory Queue

    机器 Crash, 机器宕机, 都会引起数据的丢失

  2. Persistent Queue

    queue.type.persisted 默认是 memory

    queue.max_bytes: 4gb

    机器宕机数据也不会丢失, 数据保证会被消费, 可以替代 Kafka 等消息队列缓冲区的作用.

插件

Input Plugins

一个 Pipeline 可以有多个 input 插件. 完整 Plugins 列表

  • Stdin / File
  • Beats / Log4J / Elasticsearch / JDBC / Kafka / Rabbitmq / Redis
  • JMX / HTTP / Websocket / UDP / TCP
  • Google Cloud Storage / S3
  • Github / Twitter
stdin

示例

stdin {}
file

支持从文件中读取数据, 如日志文件

  • 读取到文件新内容, 发现新文件
  • 只读取一次, 重启后从上次读取的位置继续(通过 sincedb 实现)
  • 文件发生归档操作(文档位置发生变化, 日志 rotation), 不影响当前内容读取.

参数

  • path
  • start_position
  • sincedb_path

示例

file {
    path => "/Users/yiruan/dev/elk7/logstash-7.0.1/bin/movies.csv"
    start_position => "beginning"
    sincedb_path => "/dev/null"
}
jdbc

见下面

Codec Plugins

Codec 将原始数据 decode 成 Event, 同时负责将 Event encode 成目标数据, 完整 Plugins 列表

  • Line / Multiline
  • JSON / Avro / Cef (ArcSight Common Event Format)
  • Dots / Rubydebug
line 单行
bin/logstash -e "input{stdin{codec=>line}} output{stdout{codec=>rubydebug}}"
bin/logstash -e "input{stdin{codec=>line}} output{stdout{codec=>dots}}"

将输入解析成单行来处理.

multiline 多行

参数

  • pattern: 设置行匹配的正则表达式
  • what: 如果匹配成功, 那么匹配行属于上一个事件还是下一个事件

    • previous
    • next
  • negate: 是否对pattern结果取反

    • true
    • false

multiline-exception.conf

input {
    stdin {
        codec => multiline {
            pattern => "^\s"
            what => "previous"
        }
    }
}
bin/logstash -f multi-exception.conf

解析异常日志

image-20201106125512628

json

json codec: 对 JSON 格式的内容进行解码(input)和编码(output), 为 JSON 数组中的每个元素分别创建一个 event.

示例

input {
    file {
        path => "/path/to/myfile.json"
        codec =>"json"
    }
}
rubydebug

以 ruby debug 格式输出

示例

output {
    stdout {
        codec => rubydebug
    }
}

Filter Plugins

Filter 处理 Event. 完整 Plugins 列表

  • Metrics – Aggregate metrics

Filter Plugin 可以对 Logstash Event 进行各种处理,例如解析,删除字段,类型转换

  • Date:日期解析
  • Dissect:分割符解析
  • Grok:正则匹配解析
  • Mutate:处理字段。重命名,删除,替换
  • Ruby:利用 Ruby 代码来动态修改 Event
mutate

mutate filter: 对字段应用变化(mutation), 包括重命名, 移除, 替换, 修改 event 的字段.

参数

  • split 字符串分割
  • remove_field 移除字段
  • add_field 添加字段
  • convert 格式转换
  • strip 去除空白

示例

# 将 "HOSTORIP" 字段重命名为 "client_ip"
filter {
  mutate {
    rename => { "HOSTORIP" => "client_ip" }
  }
}


# 移除指定字段的前后空白
filter {
  mutate {
    strip => ["field1", "field2"]
  }
}

filter
mutate {
    split => { "genre" => "|" }
    remove_field => ["path", "host","@timestamp","message"]
}

mutate {
    split => ["content", "("]
    add_field => { "title" => "%{[content][0]}"}
    add_field => { "year" => "%{[content][1]}"}
}

mutate {
    convert => {
        "year" => "integer"
    }
    strip => ["title"]
    remove_field => ["path", "host","@timestamp","message","content"]
}
csv

将逗号分隔的值数据解析为各个字段

参数

  • seperator
  • columns

示例

filter {
    csv {
        separator => ","
        columns => ["id","content","genre"]
    }
}
date

date: 从字段中解析出日期, 并将其作为 events 中的 Logstash timestamp

示例

# 解析 logdate 字段, 并将其作为 logstash 
filter {
  date {
    match => [ "logdate", "MMM dd yyyy HH:mm:ss" ]
  }
}
drop

drop: (可按条件筛选)丢弃 events

参数

  • percentage 默认是 100, 按百分比来抛弃

示例

# 丢弃日志级别为 debug 的日志
filter {
    if [loglevel] == "debug" {
        drop { }
    }
}
    

filter {
    if [loglevel] == "debug" {
        drop {
            percentage => 40
        }
    }
}
fingerprint

fingerprint: 使用一致性哈希来生成 fingerprint 字段

示例

# 使用 ip, @timestamp, message 这几个字段来生成一致性哈希, 并存储在 metadata 字段 "generated_id"
filter {
  fingerprint {
    source => ["IP", "@timestamp", "message"]
    method => "SHA1"
    key => "0123"
    target => "[@metadata][generated_id]"
  }
}
ruby

ruby filter: 执行 Ruby 代码

# 取消 90% 的 event
filter {
  ruby {
    code => "event.cancel if rand <= 0.90"
  }
}
dissect

dissect filter: 使用分隔符将非结构化event数据解析为字段.

  • dissect 不使用正则表达式, 因此速度很快.
  • 但如果每行的数据格式不一样, 那么使用 grok filter 更合适

示例

# 用于解析下面这种格式的日志:
# Apr 26 12:20:02 localhost systemd[1]: Starting system activity accounting tool...
filter {
  dissect {
    mapping => { "message" => "%{ts} %{+ts} %{+ts} %{src} %{prog}[%{pid}]: %{msg}" }
  }
}


# 👆 上述的日志会被解析为
{
  "msg"        => "Starting system activity accounting tool...",
  "@timestamp" => 2017-04-26T19:33:39.257Z,
  "src"        => "localhost",
  "@version"   => "1",
  "host"       => "localhost.localdomain",
  "pid"        => "1",
  "message"    => "Apr 26 12:20:02 localhost systemd[1]: Starting system activity accounting tool...",
  "type"       => "stdin",
  "prog"       => "systemd",
  "ts"         => "Apr 26 12:20:02"
}
kv

kv filter: 解析键值对

示例

# 用于解析如下这种格式的日志:
# ip=1.2.3.4 error=REFUSED
filter {
  kv { }
}



#👆 上述日志会被解析为
{
    "ip" = "1.2.3.4"
    "error" => "REFUSED"
}
grok

grok filter: 解析任意文本, 并将其结构化.

  • 该工具非常适合 syslog、apache和其他 web 服务器日志、mysql日志, 以及通常用于供人类而非计算机使用的任何日志格式。
  • Grok 工作原理是通过组合文本模式来匹配日志
  • 官方提供了 Grok Debugger 来帮助调试 grok patterns, 该特性是由 X-Pack 特性提供的(Basic License), 免费使用.
  • dissect 的主要应用区别

    • dissect 并不使用正则, 性能更好. 它仅适用于当数据格式基本稳定重复时效果好.
    • grok 则适用于每行文本结构不一样的情况
    • 可以将 dissectgrok 同时混用于特定场景: 每行中的特定部分格式稳定重复, 但其他部分并不稳定时.

      • 使用 dissect 解析稳定重复的部分
      • 使用 grok 处理剩余的部分

在线 Grok 调试

Grok Pattern 语法

  • 基本语法: %{SYNTAX:SEMANTIC}

    SYNTAX: 匹配类型

    SEMANTIC: 字段名

  • Grok 是基于正则表达式的(语法: Oniguruma), 因此除了 Grok 模式外, 也可以直接使用/混用正则语法.

简单示例

# 下面示例用于解析如下这种格式的日志:
# 55.3.244.1 GET /index.html 15824 0.043
input {
    file {
        path => "/var/log/http.log"
    }
}
filter {
    grok {
        match => { "message" => "%{IP:client} %{WORD:method} %{URIPATHPARAM:request} %{NUMBER:bytes} %{NUMBER:duration}" }
    }
}


# 👆 上述日志解析结果为
{
    "client": "55.3.244.1"
    "method": "GET"
    "request": "/index.html"
    "bytes": 15824
    "duration": 0.043
}

Oniguruma 语法示例

#   Jan  1 06:25:43 mailserver14 postfix/cleanup[21403]: BEF25A72965: message-id=<20130101142543.5828399CCAF@mailserver14.example.com>
 filter {
      grok {
        patterns_dir => ["./patterns"]
        match => { "message" => "%{SYSLOGBASE} %{POSTFIX_QUEUEID:queue_id}: %{GREEDYDATA:syslog_message}" }
      }
    }


# contents of ./patterns/postfix:
# POSTFIX_QUEUEID [0-9A-F]{10,11}


# 匹配结果
timestamp: Jan  1 06:25:43
logsource: mailserver14
program: postfix/cleanup
pid: 21403
queue_id: BEF25A72965
syslog_message: message-id=<20130101142543.5828399CCAF@mailserver14.example.com>
dns

dns filter plugin: 执行标准或反向DNS

示例

# 执行反向域名解析(将ip解析到对应的域名)
filter {
  dns {
    reverse => [ "source_host" ]
    action => "replace"
  }
}
elasticsearch

elasticsearch filter: 从 es 中获取数据来填充字段

常用于从es中查找当前 event 对应的开始 event, 从而计算总的时间消耗等.

示例

# 从 logstash 收到 "end" event 时, 它使用 elasticsearch filter 来查找匹配的 "start" event(基于相同的操作id).
# 然后它从 "start" event 中获取 @timstamp 并赋值给 started
# 再使用 date 和 ruby filter 来计算出两个 event 的时间间隔
if [type] == "end" {
    elasticsearch {
        hosts => ["es-server"]
        query => "type:start AND operation:%{[opid]}"
        fields => { "@timestamp" => "started" }
    }
    date {
        match => ["[started]", "ISO8601"]
        target => "[started]"
    }
    ruby {
        code => 'event.set("duration_hrs", (event.get("@timestamp") - event.get("started")) / 3600) rescue nil'
    }
}
geoip

geoip filter: 添加 ip 对应的地理信息

示例

filter {
  geoip {
    source => "clientip"
  }
}
http

http filter: 集成外部 web 服务/REST APIs, 并允许通过任何 HTTP 服务或终端来充实 event.

jdbc_static

jdbc_static filter: 通过使用远程数据库预加载的数据来充实 event

示例

# 从远程数据库获取数据, 并在本地数据库缓存, 并使用这些缓存来充实当前 event
filter {
  jdbc_static {
    loaders => [ 
      {
        id => "remote-servers"
        query => "select ip, descr from ref.local_ips order by ip"
        local_table => "servers"
      },
      {
        id => "remote-users"
        query => "select firstname, lastname, userid from ref.local_users order by userid"
        local_table => "users"
      }
    ]
    local_db_objects => [ 
      {
        name => "servers"
        index_columns => ["ip"]
        columns => [
          ["ip", "varchar(15)"],
          ["descr", "varchar(255)"]
        ]
      },
      {
        name => "users"
        index_columns => ["userid"]
        columns => [
          ["firstname", "varchar(255)"],
          ["lastname", "varchar(255)"],
          ["userid", "int"]
        ]
      }
    ]
    local_lookups => [ 
      {
        id => "local-servers"
        query => "select descr as description from servers WHERE ip = :ip"
        parameters => {ip => "[from_ip]"}
        target => "server"
      },
      {
        id => "local-users"
        query => "select firstname, lastname from users WHERE userid = :id"
        parameters => {id => "[loggedin_userid]"}
        target => "user" 
      }
    ]
    # using add_field here to add & rename values to the event root
    add_field => { server_name => "%{[server][0][description]}" }
    add_field => { user_firstname => "%{[user][0][firstname]}" } 
    add_field => { user_lastname => "%{[user][0][lastname]}" }
    remove_field => ["server", "user"]
    jdbc_user => "logstash"
    jdbc_password => "example"
    jdbc_driver_class => "org.postgresql.Driver"
    jdbc_driver_library => "/tmp/logstash/vendor/postgresql-42.1.4.jar"
    jdbc_connection_string => "jdbc:postgresql://remotedb:5432/ls_test_2"
  }
}
jdbc_streaming

jdbc_streaming filter: 使用数据库数据来充实 event

示例

# The following example executes a SQL query and stores the result set in a field called `country_details`:
filter {
  jdbc_streaming {
    jdbc_driver_library => "/path/to/mysql-connector-java-5.1.34-bin.jar"
    jdbc_driver_class => "com.mysql.jdbc.Driver"
    jdbc_connection_string => "jdbc:mysql://localhost:3306/mydatabase"
    jdbc_user => "me"
    jdbc_password => "secret"
    statement => "select * from WORLD.COUNTRY WHERE Code = :code"
    parameters => { "code" => "country_code"}
    target => "country_details"
  }
}
memcached

memcached filter: 使用 memcach, 支持 GET 和 SET 操作.

translate

translate filter: 替换字段内容, 支持 hash 结构(字典) 或 文件的形式.

当前支持的文件格式:

  • YAML
  • JSON
  • CSV

示例

# The following example takes the value of the response_code field, translates it to a description based on the values specified in the dictionary, and then removes the response_code field from the event
filter {
  translate {
    field => "response_code"
    destination => "http_response"
    dictionary => {
      "200" => "OK"
      "403" => "Forbidden"
      "404" => "Not Found"
      "408" => "Request Timeout"
    }
    remove_field => "response_code"
  }
}
useragent

useragent filter: 解析 user agent 字符串

示例

# The following example takes the user agent string in the agent field, parses it into user agent fields, and adds the user agent fields to a new field called user_agent. It also removes the original agent field:
filter {
  useragent {
    source => "agent"
    target => "user_agent"
    remove_field => "agent"
  }
}


# 解析结果
"user_agent": {
    "os": "Mac OS X 10.12",
    "major": "50",
    "minor": "0",
    "os_minor": "12",
    "os_major": "10",
    "name": "Firefox",
    "os_name": "Mac OS X",
    "device": "Other"
    }

Output Plugins

Output 是 pipeline 的最后一个阶段, 是将处理完的 Event 发送到特定的目的地. 完整 Plugins 列表

  • Elasticsearch
  • Email / Pageduty
  • Influxdb / Kafka / Mongodb / Opentsdb / Zabbix
  • Http / TCP / Websocket
  • rubydebug
  • dots 输出一个 ., 常用于展示处理进度
elasticsearch

参数

  • hosts
  • index
  • document_id

示例

elasticsearch {
    hosts => "http://localhost:9200"
    index => "movies"
    document_id => "%{id}"
}
stdout

示例

stdout {}

stdout { codec => rubydebug }

示例

简单示例

Logstash配置文件示例

input { stdin {} }

filter {
    grok {
        match => { "message" => "%{COMBINEDAPACHELOG}" }
    }
    date {
        match => { "timestamp", "dd/MMM/yyyy:HH:mm:ss Z" ]
    }
}

output {
    elasticsearch { hosts => ["localhost:9200"] }
    stdout { codec => rubydebug }
}
  • 使用: bin/logstash -f demo.conf
  • 上面这个 pipeline 包含了: input / filter / output 这3个过程

导入 movies 示例

input {
  file {
    path => "/Users/yiruan/dev/elk7/logstash-7.0.1/bin/movies.csv"
    start_position => "beginning"
    sincedb_path => "/dev/null"
  }
}
filter {
  csv {
    separator => ","
    columns => ["id","content","genre"]
  }

  mutate {
    split => { "genre" => "|" }
    remove_field => ["path", "host","@timestamp","message"]
  }

  mutate {

    split => ["content", "("]
    add_field => { "title" => "%{[content][0]}"}
    add_field => { "year" => "%{[content][1]}"}
  }

  mutate {
    convert => {
      "year" => "integer"
    }
    strip => ["title"]
    remove_field => ["path", "host","@timestamp","message","content"]
  }

}
output {
   elasticsearch {
     hosts => "http://localhost:9200"
     index => "movies"
     document_id => "%{id}"
   }
  stdout {}
}

movies.csv 数据文件格式如下

image-20201106142650091

利用JDBC 插件导入数据到Elasticsearch

image-20201106145424454

image-20201106145436376

Demo

  • https://spring.io/guides/gs/a...
  • 支持 新增 / 更新 / 删除 三种 API
  • User 字段包含了一个 last update 的字段
  • User 表包含了一个 is deleted 字段
  1. mysql-connector-java-8.0.17.jar 拷贝到 logstash 的 logstash-core/lib/jars/ 地下
  2. 在 input 阶段加入 jdbc 相应配置

image-20201106145525291

image-20201106145532612

Beats介绍

image-20201106152324025

Beats 是轻量级数据采集器(light weight data shippers)

  • 以搜集数据为主
  • 支持与 Logstash 或 ES 集成
  • 特点: 全品类, 轻量级, 开箱即用(golang开发), 可插拔, 可扩展, 可视化

Metricbeat

Metricbeat 的组成

  • Module

    • 搜集的指标对象, 例如不同的操作系统, 不同的数据库, 不同的应用系统
  • Metricset

    • 一个 Module 可以有多个 metricset

      以 System Module 为例, 其 metricset 有:

      • core
      • cpu
      • disk io
      • filesystem
      • load
      • memory
    • 具体的指标集合. 以减少调用次数为原则进行划分

      • 不同的 metricset 可以设置不同的抓取时长

Metricbeat 提供了大量的开箱即用的 Module

  • https://www.elastic.co/guide/...
  • 通过执行 metricbeat modules list 查看
  • 通过执行 metricbeat moudles enable <module_name> 启用指定 module

    需自行修改相关 module 的配置, 比如 mysql module 的配置文件在 modules.d/mysql.yml

Metricbeat Event

image-20201106155612824

安装

  1. 安装 Metricbeat
  2. 配置连接到 Elastic Stack: metricbeat.yml

    metricbeat.config.modules:
      # Glob pattern for configuration loading
      path: ${path.config}/modules.d/*.yml
    
      # Set to true to enable config reloading
      # 将该值设为 true, 那么后续修改 modules.d 目录下的 yml 配置会自动重新 reload, 无需重启 metricbeat
      reload.enabled: true
    
      # Period on which files under path should be checked for changes
      reload.period: 10s
      
    
    
    setup.template.settings:
      index.number_of_shards: 1
      index.codec: best_compression
      #_source.enabled: false
      # 由于这里部署的是单节点集群, 因此将副本分片数设为 0
      index.number_of_replicas: 0
      
    
    # 设置 es 的url和账号密码
    output.elasticsearch:
        hosts: ["myEShost:9200"]
        #username: "metricbeat_internal"
        #password: "YOUR_PASSWORD"
    
    
    # 配置 Kibana 连接信息(仅在需要初始化 Kibana Dashboard 时使用), 若 Metricbeat 和 Kibana 在同一台服务器上则无需处理
    setup.kibana:
        host: "mykibanahost:5601" 
        username: "my_kibana_user"  
        password: "{pwd}"
        
        
    # ================================== General ===================================
    
    # The name of the shipper that publishes the network data. It can be used to group
    # all the transactions sent by a single shipper in the web interface.
    # 这里建议为每台主机设置一个 name, 后续可在 event 的 "agent.name" 和 "host.name" 获取到该值
    # 这里若未配置, 则会默认使用 host.hostname 来填充该值
    # 不然就只能后续通过 "agent.hostname", "host.hostname" 来读取主机名(hostname)了
    # 更推荐在这里设置 name
    #name: "...."
    
    # The tags of the shipper are included in their own field with each
    # transaction published.
    # 这里可以添加 tag, 方便后续在 kibana 中根据 tag 筛选要展示的一组服务器的数据
    #tags: ["webserver", "p3"]
    
    # Optional fields that you can specify to add additional information to the
    # output.
    #fields:
    #  env: staging
  3. 查看可用的 modules

    metricbeat modules list
  4. 启用需要的 module

    metricbeat modules enable <module-name>
  5. 修改各 module 的配置文件: modules/*.yml
  6. 验证 metricbeat 配置文件是否正确

    metricbeat test config -e
  7. 初始化 Kibana Dashboard

    metricbeat setup -e
  8. 启动 metricbeat

    systemctl start metricbeat
  9. 在 Kibana 的 Discover/Dashboard 中能看到 Metricbeat 数据
其他配置: https://www.elastic.co/guide/...

配置 Kibana Dashboard

metricbeat modules enable kibana
metricbeat modules enable elasticsearch
# 配置 kibana 可视化 dashboards 相关数据, 并写入 es
# 需确保 es 和 kibana 正在运行
metricbeat setup -e
#metricbeat setup --dashboards

image-20201106154602301

Packetbeat

实时网络数据分析

Packetbeat - 实时网络数据分析,监控应用服务器之间的网络流量

  • 常见抓包工具 - Tcpdump /wireshark
  • 常见抓包配置 - Pcap 基于 libpcap,跨平台 / Af_packet 仅支持 Linux,基于内存映射嗅探,高性能

Packetbeat 支持的协议

  • ICMP / DHCP / DNS / HTTP / Cassandra / Mysql / PostgresSQL / Redis / MongoDB / Memcache / TLS
  • Network flows:抓取记录网络流量数据,不涉及协议解析

配置 Kibana Dashboard

https://www.elastic.co/guide/...

先修改 packetbeat.yml, 配置需要监听的端口等

# 配置 kibana dashboard
packetbeat setup --dashboards

Filebeat

简介

Filebeat 是用于采集本地日志文件的工具.

  • 读取日志文件, Filebeat 不做数据的解析, 加工处理

    • 日志是非结构化数据
    • 需要进行处理后, 以结构化的方式保存到 ES
  • 保证数据至少被读取一次
  • 处理多行数据, 解析 JSON 格式, 简单的过滤

关于 FileBeat 的正则表达式支持可以查看: https://www.elastic.co/guide/...

组成

image-20201113142122448

类似 Logstash, 它也有很多开箱即用的日志 Modules

  • 简化使用流程
  • 减少开发的投入
  • 最佳参考实践

Filebeat 的执行流程

  1. 定义数据采集: Prospector 配置. 通过 filebeat.yml
  2. 建立数据模型: Index Template
  3. 建立数据处理流程: Ingest Pipeline
  4. 存储并提供可视化分析

与 Logstash 的对比

对比

  • Logstash 是基于 Java 编写, 插件则是 JRuby 编写, 功能强大, 不仅仅是一个日志采集工具,它也是可以作为一个日志搜集工具,有丰富的input|filter|output插件可以使用。.但是对机器的资源消耗很高.
  • Filebeat 则是采用 go 开发, 是 beats 的一个文件采集工具, 性能好, 部署简单.

推荐的架构

一些命令

# 查看所有可用的日志模块
./filebeat modules list

# 启用系统日志模块
./filebeat modules enable system

# 启用 nginx 日志模块
./filebeat modules enable nginx

./filebeat export template

./filebeat setup -e

# 启动 filebeat
./filebeat -e

导出配置 export

导出到标准输出

filebeat export <command>

- 参数: command
    config                # 导出当前的配置
     dashboard            # 导出已定义的 kibana dashboard
    ilm-policy            # 导出 ILM policy
    index-pattern        # 导出 kibana index patterm
    template            # 导出 index template

安装

预先创建 geo 的 ingest pipeline

PUT _ingest/pipeline/geoip-info
{
  "description": "Add geoip info",
  "processors": [
    {
      "geoip": {
        "field": "client.ip",
        "target_field": "client.geo",
        "ignore_missing": true
      }
    },
    {
      "geoip": {
        "field": "source.ip",
        "target_field": "source.geo",
        "ignore_missing": true
      }
    },
    {
      "geoip": {
        "field": "destination.ip",
        "target_field": "destination.geo",
        "ignore_missing": true
      }
    },
    {
      "geoip": {
        "field": "server.ip",
        "target_field": "server.geo",
        "ignore_missing": true
      }
    },
    {
      "geoip": {
        "field": "host.ip",
        "target_field": "host.geo",
        "ignore_missing": true
      }
    }
  ]
}
这里处理的字段目前是针对 nginx 日志
  1. 安装 Filebeat
  2. 配置连接到 Elastic Stack: filebeat.yml

    # ============================== Filebeat modules ==============================
    
    filebeat.config.modules:
      # Glob pattern for configuration loading
      path: ${path.config}/modules.d/*.yml
    
      # Set to true to enable config reloading
      reload.enabled: true
    
      # Period on which files under path should be checked for changes
      reload.period: 10s
    
    # ======================= Elasticsearch template setting =======================
    
    setup.template.settings:
      index.number_of_shards: 1
      index.number_of_replicas: 0
      #index.codec: best_compression
      #_source.enabled: false
      
      
    # ========================== Filebeat global options ===========================
    # How long filebeat waits on shutdown for the publisher to finish.
    # Default is 0, not waiting.
    # 在关闭 filebeat 时等待多久(默认是0, 这可能关闭时部分未 ack 的 event 在下一次启动 filebeat 时重复发送)
    # 在未达到该值时, 但所有 event 已 ack, 那么 filebeat 也会关闭.
    # 该值的设定很大程度取决于 filebeat 所在主机的运行环境以及当前output的状态
    #filebeat.shutdown_timeout: 0
    filebeat.shutdown_timeout: 1s
      
      
    # ================================== General ===================================
    
    # The name of the shipper that publishes the network data. It can be used to group
    # all the transactions sent by a single shipper in the web interface.
    # 这里建议为每台主机设置一个 name, 后续可在 event 的 "agent.name" 和 "host.name" 获取到该值
    # 这里若未配置, 则会默认使用 host.hostname 来填充该值
    # 不然就只能后续通过 "agent.hostname", "host.hostname" 来读取主机名(hostname)了
    # 更推荐在这里设置 name
    name: "...."
    
    # The tags of the shipper are included in their own field with each
    # transaction published.
    # 这里可以添加 tag, 方便后续在 kibana 中根据 tag 筛选要展示的一组服务器的数据
    #tags: ["webserver", "p3"]
    
    # Optional fields that you can specify to add additional information to the
    # output.
    #fields:
    #  env: staging
    
      
    
    # 设置 es 的url和账号密码
    output.elasticsearch:
        hosts: ["myEShost:9200"]
        #username: "metricbeat_internal"
        #password: "YOUR_PASSWORD"
        # 这里使用上面创建的 geoip-info 这个 ingest pipeline, 以添加 geo 信息
        pipeline: geoip-info
        
        
        
  3. 查看可用的 modules

    filebeat modules list
  4. 启用需要的 module

    filebeat modules enable <module-name>
  5. 修改各 module 的配置文件: modules/*.yml
  6. 验证 filebeat配置文件是否正确

    filebeat test config -e
  7. 初始化 Kibana Dashboard 等

    # 可查看官方文档, 可以只载入需要的 module 相关的配置, 而不是默认的一股脑全塞进去
    filebeat setup -e
  8. 启动 filebeat

    systemctl start filebeat
  9. 在 Kibana 的 Discover/Dashboard 中能看到 filebeat数据
其他配置: https://www.elastic.co/guide/...

使用 RPM 方式安装的文件目录结构

TypeDescriptionLocation
homeHome of the Filebeat installation./usr/share/filebeat
binThe location for the binary files./usr/share/filebeat/bin
configThe location for configuration files./etc/filebeat
dataThe location for persistent data files./var/lib/filebeat
logsThe location for the logs created by Filebeat./var/log/filebeat

配置选项

https://www.elastic.co/guide/...

在 filebeat.yml 中可以配置全局选项(global options)和通用选项(general options)

  • 全局选项: 控制 publisher 的一些行为, 以及文件的存放路径.
  • 通用选项: 所有 Elastic Beats 都支持的选项.
Filebeat 全局配置选项
  • filebeat.registry.path
  • filebeat.registry.file_permissions
  • filebeat.registry.flush
  • filebeat.registry.migrate_file
  • filebeat.shutdown_timeout

    默认禁用状态, 意味着当关闭 filebeat 并在下次启动时有可能导致部分 event 被重复发送.

    Filebeat 在关闭之前最长的等待时间.

    该项配置需要根据实际的运行环境及 output 的当前状态来决定

    示例

    filebeat.shutdown_timeout: 5s
Beats 通用配置选项

https://www.elastic.co/guide/...

  • name

    Beat 的 name, 位于 event 的 agent.name 字段中.

    若该项配置为空则会使用 hostname 的值.

    可以使用该项配置来分组不同的 Beat

    name: "my-shipper"
  • tags

    位于 event 的 tags 字段, 同样可以使用它来分组不同的服务器.

    tags: ["my-service", "hardware", "test"]
  • fileds

    添加自定义属性.

    默认位于 event 的 fields 字段中

    fields: {
        xxxxx1: "myproject", 
        xxxxx2: "574734885120952459"
    }
  • fields_under_root

    fields_under_root: true
    fields:
      instance_id: i-10a64379
      region: us-east-1
  • processors
  • max_procs

    默认是系统的逻辑cpu数量.

    设置可同时使用的最大CPU数

ILM 策略

从 Filebeat 7.0 开始, Filebeat 对于其所创建的索引(若启用了 output.elasticsearch)会自动应用默认的生命周期管理(ILM)策略.

关于 ILM, 具体可以参考这里: https://www.elastic.co/guide/...

更多关于Filebeat 的 ILM 设置请参见: https://www.elastic.co/guide/...

可用参数配置模板

# ====================== Index Lifecycle Management (ILM) ======================

# Configure index lifecycle management (ILM). These settings create a write
# alias and add additional settings to the index template. When ILM is enabled,
# output.elasticsearch.index is ignored, and the write alias is used to set the
# index name.

# Enable ILM support. Valid values are true, false, and auto. When set to auto
# (the default), the Beat uses index lifecycle management when it connects to a
# cluster that supports ILM; otherwise, it creates daily indices.
#setup.ilm.enabled: auto

# Set the prefix used in the index lifecycle write alias name. The default alias
# name is 'filebeat-%{[agent.version]}'.
#setup.ilm.rollover_alias: 'filebeat'

# Set the rollover index pattern. The default is "%{now/d}-000001".
#setup.ilm.pattern: "{now/d}-000001"

# Set the lifecycle policy name. The default policy name is
# 'beatname'.
#setup.ilm.policy_name: "mypolicy"

# The path to a JSON file that contains a lifecycle policy configuration. Used
# to load your own lifecycle policy.
#setup.ilm.policy_file:

# Disable the check for an existing lifecycle policy. The default is true. If
# you disable this check, set setup.ilm.overwrite: true so the lifecycle policy
# can be installed.
#setup.ilm.check_exists: true

# Overwrite the lifecycle policy at startup. The default is false.
#setup.ilm.overwrite: false

Process

Process 主要是用于增删字段, 解码, 过滤 event 等, 当定义多个 processes 时, event 会依次处理并输出.

若是仅仅需要在某个 input 中根据条件匹配过滤 event 的话, 可以在具体的 input 中使用 include_lines, exclude_lines, exclude_files 来配置.

但这种方式需要每个 input 中都配置.

而 Process 中则可以全局生效.

https://www.elastic.co/guide/...

可以在哪些地方使用 Process

  1. 在顶级的配置中(指 filebeat.yml), 此处配置的 process 会应用到全局.
  2. 在特定的 input 中, 此时仅对该 input 搜集到的 event 生效.
  3. 对于 modules, 可以在模块配置中的 input 部分定义.

    Similarly, for Filebeat modules, you can define processors under the input section of the module definition.
whenif-then-else

使用 When 的条件判断

processors:
  - <processor_name>:
      when:
        <condition>
      <parameters>

  - <processor_name>:
      when:
        <condition>
      <parameters>

...
  • condition: 若配置了该项, 则需满足条件才会处理, 否则每次都会处理.
  • parameter: 是传给给 process 的参数

使用 If-Then-Else 的条件判断

processors:
  - if:
      <condition>
    then: 
      - <processor_name>:
          <parameters>
      - <processor_name>:
          <parameters>
      ...
    else: 
      - <processor_name>:
          <parameters>
      - <processor_name>:
          <parameters>
      ...
  • then 必须配置一个或多个 process 来处理
  • else 是可选的, 同样可以一个或多个 process
支持的 Processes

列表

add_id

生成一个唯一值, 保存在 @metadata._id 字段, ES 在收到该 event 时会将该值作为文档 id 来使用.

这可以避免在 ES 中保存重复的 event (因为 Filebeat 只能保证 event 至少被交付一次, 但不能确保最多一次)

适用于无法从 event 中生成唯一值作为文档的 id 情况下使用.

若可以从多个字段中生成唯一值, 那么可使用 fingerprint 这个 process

fingerprint

根据给定的多个字段, 生成一个唯一值, 保存在 @metadata._id 字段.

processors:
  - fingerprint:
      fields: ["field1", "field2"]
      target_field: "@metadata._id"
drop_event

示例: 丢弃所有 DEBUG 级别的日志

processors:
  - drop_event:
      when:
        regexp:
          message: "^DBG:"

示例: 丢弃从特定日志文件搜集的 event

processors:
  - drop_event:
      when:
        contains:
          source: "test"
decode_json_fields

示例: 解析 json 字符串

{ "outer": "value", "inner": "{\"data\": \"value\"}" }
上述 json 数据中, inner 字段下是个 json 字符串.

使用 decode_json_fields process 将上述 inner 字段的值解析为对象

filebeat.inputs:
- type: log
  paths:
    - input.json
  json.keys_under_root: true

processors:
  - decode_json_fields:
      fields: ["inner"]

output.console.pretty: true
解析结果为:
{
  "@timestamp": "2016-12-06T17:38:11.541Z",
  "beat": {
    "hostname": "host.example.com",
    "name": "host.example.com",
    "version": "7.9.3"
  },
  "inner": {
    "data": "value"
  },
  "input": {
    "type": "log",
  },
  "offset": 55,
  "outer": "value",
  "source": "input.json",
  "type": "log"
}
add_fields
支持的 Conditions
equals

仅支持比较数字或字符串.

示例: http 状态码 = 200

equals:
  http.response.code: 200
contains

字符串匹配

contains:
  status: "Specific error"
regexp

正则匹配判断

regexp:
  system.process.name: "^foo.*"
range

范围匹配, 仅接受整数或浮点数.

range:
  http.response.code:
    gte: 400

👆 上面这种方式可以简写为:

range:
  http.response.code.gte: 400

示例: cpu 使用率在 [0.5~0.8) 范围

range:
  system.cpu.user.pct.gte: 0.5
  system.cpu.user.pct.lt: 0.8
network

匹配网络, 支持多种网络地址格式.

示例: private adress

network:
  source.ip: private 

示例: 192.168.1.0 ~ 192.168.1.255

network:
  destination.ip: '192.168.1.0/24'

示例: 在多个网段范围

network:
  destination.ip: ['192.168.1.0/24', '10.0.0.0/8', loopback]
has_fields

判断是否包含指定的所有字段

has_fields: ['http.response.code']
or

可以组合多个 condition

or:
  - <condition1>
  - <condition2>
  - <condition3>
  ...

示例: 组合2个 equals

or:
  - equals:
      http.response.code: 304
  - equals:
      http.response.code: 404
and

类似 or

not

对指定的 condition 判断结果取反.

not:
  <condition>

示例: 匹配 status 不是 OK 的 event

not:
  equals:
    status: OK

Inputs

通用

若不使用 module, 而是手动配置的话, 则应在 filebeat.inputs 一节配置(指 filebeat.yml 文件)

可以每一个将 input 配置独立成一个小配置文件, 但是必须在 filebeat.yml 中的 filebeat.config.inputs 中指定路径

filebeat.config.inputs:
    enabled: true
    path: inputs.d/*.yml
    #reload.enabled: true
    #reload.period: 10s
这里的 reload 配置仅对外部文件有效, 对于 filebeat.yml 本身是无效的.

具体的外部配置文件可以如下所示(以 log input 为例)

- type: log
  paths:
    - /var/log/mysql.log
  scan_frequency: 10s

- type: log
  paths:
    - /var/log/apache.log
  scan_frequency: 5s

以下这些选项是所有 Input 都适用的

  • enabled 是否启用该 input

    默认为 true.
  • tags

    这里设置的 tags 会追加到 filebeat 的通用设置中的 tags 中

  • fields 额外增加的字段

    可以是标量值(scalar values), 数组, 字典或任何嵌套组合.

    默认情况下, 这里增加的字段会被放置在 fields 这个字段下(作为其子字段), 若要将其作为顶级字段, 则应设置 fields_under_root 选项为 true.

    若是在 filebeat 中的设置了同名的 fields, 那么会被这里的配置覆盖.

  • fields_under_rootfields 中的字段作为顶级字段存储在 event 中
  • processors 设置要应用在输入数据的 process

    Processors

  • pipeline 指定写入 ES 时要应用的 ingest pipeline

    若是在 input 和 output 都定义了 pipeline id, 那么最后会使用的是 input 中设置的.
  • keep_null 若是 fields 的值是 null, 那么仍会被写入 event 中

    默认是 false
  • index 设置该 event 写入的索引(若 output 是 elasticsearch)或设置 raw_index 字段的值(若 output 非 elasticsearch)

    这里的索引名只能使用 agent name, version, event timestamp 这3个动态数据.

    若想使用动态字段, 那么应在 output.elasticsearch.index 中设置或是在 process 中设置.

    示例

    "%{[agent.name]}-myindex-%{+yyyy.MM.dd}" 实际对应的是 "filebeat-myindex-2019.11.01" 索引

  • publisher_pipeline.disable_host 禁用自动设置 host.name 的行为

    默认是 false
Multiline Message

通过设置 filebeat.yml 中的 multiline 选项可以设置哪些行要作为同一个 event 的组成部分(即视为一条日志消息)

比如程序输出调用栈(多行), 那么此时就需要将这个调用栈信息合并成同一条消息, 而不是被 filebeat 默认行为视为多条消息.

!!!! 如果 filebeat 是将搜集到的 event 发送给 logstash, 那么应该在 filebeat 中先处理好多行数据, 而不是在 logstash 中处理.

若是交由 logstash 处理多行数据的话, 可能导致流的混乱和数据损坏.

关于 filebeat 的 正则表达式支持 , 注意与 logstash 对正则的支持是不完全一样的.

官方提供了一个在线测试正则匹配 multiline: https://play.golang.org/p/uAd...

选项

以下选项可以在 filebeat.inputs 部分配置(指在 filebeat.yml 配置文件中), 从而控制 filebeat 如何处理跨多行的消息.

  • multiline.type

    默认是 "pattern"

    设置要使用哪个聚合方法.

    • pattern

      这是默认的方式
    • count

      适用于聚合常量数量的行(意思应该是指定连续多少行作为同一个消息)

  • multiline.pattern

    注意 filebeat 对正则的支持与 logstash 不完全一样.

    指定要使用哪个正则表达式来匹配, 根据其他选项来决定是将匹配行作为上一行的延续, 还是作为新的 event 的第一行.

  • multiline.negate

    默认是 false

    定义是否否定上述匹配的正则表达式(应该是取反的意思)

  • multiline.match

    指定 filebeat 如何将匹配的行合并到一个 event 中.

    • after
    • before

这两个选项的行为受到 multiline.negate 的影响.

| Setting for negate | Setting for match | Result | Example pattern: ^b |
| -------------------- | ------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| false | after | Consecutive lines that match the pattern are appended to the previous line that doesn’t match.
所有匹配 pattern 的行, 都归属到前一行 | image-20201121203723537 |
| false | before | Consecutive lines that match the pattern are prepended to the next line that doesn’t match.
所有匹配 pattern 的行, 都归属到下一行 | image-20201121203801427 |
| true | after | Consecutive lines that don’t match the pattern are appended to the previous line that does match.
所有不匹配 pattern 的行, 都归属到前一行 | image-20201121203807140 |
| true | before | Consecutive lines that don’t match the pattern are prepended to the next line that does match.
所有不匹配 pattern 的行, 都归属到下一行. | image-20201121203815589 |

这里按照 logstash 的方式来, 就非常容易理解.

    • after 等同于 logstash 中的 previous
    • before 等同于 logstash 的 next
    • multiline.flush_pattern

      指定一个正则表达式, 若匹配到则会将当前的 multiline 消息从内存中 flush, 并结束当前的多行消息.

      假定 multiline.pattern 匹配 event 的开始, 则 multiline.flush_pattern 就适合匹配 event 结束所在的那一行.

    • multiline.max_lines

      默认是 500

      指定最多多少行可以被合并成一个 event, 若超过该行数限制, 则超出的部分会被抛弃.

    • multiline.timout

      默认是 5s

      注意, 该值若设置得太小可能会导致数据丢失.

      指定在超时多久后(这里指的应该是针对每个多行消息匹配到第一行的那个时间点开始), 强制将当前的多行消息(缓存中)直接作为一条 event 发送出去.

    • multiline.count_lines

      指定每多少行的内容被合并成一个 event

    • multiline.skip_newline

      若设置该值, 那么多行消息之间的换行符会被移除.

    示例

    示例: 匹配每条消息都是以 [ 开头

    multiline.type: pattern
    multiline.pattern: '^\['
    multiline.negate: true
    multiline.match: after
    
    
    
    # 示例匹配的多行消息
    [beat-logstash-some-name-832-2015.11.28] IndexNotFoundException[no such index]
        at org.elasticsearch.cluster.metadata.IndexNameExpressionResolver$WildcardExpressionResolver.resolve(IndexNameExpressionResolver.java:566)
        at org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.concreteIndices(IndexNameExpressionResolver.java:133)
        at org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.concreteIndices(IndexNameExpressionResolver.java:77)
        at org.elasticsearch.action.admin.indices.delete.TransportDeleteIndexAction.checkBlock(TransportDeleteIndexAction.java:75)
    将不以 [ 开头的行都归属到上一行.

    示例: 每条消息以日期为开头的

    multiline.type: pattern
    multiline.pattern: '^\[[0-9]{4}-[0-9]{2}-[0-9]{2}'
    multiline.negate: true
    multiline.match: after
    
    
    
    # 匹配示例
    [2015-08-24 11:49:14,389][INFO ][env                      ] [Letha] using [1] data paths, mounts [[/
    (/dev/disk1)]], net usable_space [34.5gb], net total_space [118.9gb], types [hfs]

    示例: 应用程序事件

    multiline.type: pattern
    multiline.pattern: 'Start new event'
    multiline.negate: true
    multiline.match: after
    multiline.flush_pattern: 'End event'
    
    
    
    # 匹配示例
    [2015-08-24 11:49:14,389] Start new event
    [2015-08-24 11:49:14,395] Content of processing something
    [2015-08-24 11:49:14,399] End event
    Log

    完整的配置项: https://www.elastic.co/guide/...

    Log Input 导出的 event 字段: https://www.elastic.co/guide/...

    选项

    • paths

      支持 Go Glob 通配符

      示例

      目录结构:
      a
      |- a1.log
      |- a2.log
      |- b
          |- b1.log
          |- b2.log
      |- c
          |- c1.log
          |- c2.log
          |- d
              |- d1.log
              |- d2.log
              
      
      /a/*.log            匹配目录 a 中的 *.log
      /a/*/*.log            匹配目录 b 和目录 c 中的 *.log (不会匹配目录 a 中的 *.log)
      /a/*/*/*.log        (这个应该是可以, 没试过)匹配目录 d 中的 *.log (不会匹配目录 a,b,c 中的 *.log)
      
      /a/**                匹配 a/*, a/b/*, a/c/*, a/c/d/*, 最深可以递归8个层级

      glob 特殊字符

      • * 匹配任意个非路径分隔符
      • ? 匹配1个非路径分隔符
      • [] 匹配1个字符组

        [abc]

        [0-9]

      • [^] 不匹配1个字符组
    • recursive_glob.enabled 是否将 paths 中的 ** 自动扩展为最深8级的递归匹配

      默认是 true
    • encoding 指定待读取的文件编码

      • plain: plain ASCII encoding

        plain 这个 encoding 是特殊的, 应为它不校验也不转换数据
      • utf-8 or utf8: UTF-8 encoding
      • gbk: simplified Chinese charaters
      • ...
    • include_lines 指定正则表达式, 此时仅匹配的行才会被导出.

      默认会导出所有非空的行.

      若同时指定了 exclude_lines, 那么会先执行 include_lines, 则执行 exclude_lines.

    • exclude_lines 指定正则表达式, 若匹配到读取的行, 那么该行会被丢弃掉.

      默认是空.

      支持的正则表达式列表

    • max_bytes 指定单行日志消息的最大长度, 若超过该长度的部分会被丢弃. 特别适用于 multiline 消息.

      默认是 10MB
    • json 将消息视为一个 json 并解析

      每行仅限一条 json, 否则会解析出错.

      json 的解析是在 filter 和 multiline 之前.

      若设置了 json, 那么必须同时设置以下任意一个选项

      • keys_under_root

        默认解析的 json 字段会被放在一个叫做 json 的字段中.

      • overwrite_keys

        若同时指定了 keys_under_root, 那么会覆盖 filebeat 默认添加的字段(比如 type, source, offset 等)

      • add_error_key

        若出错 json 解析/设置出错, 那么会添加 error.message 字段及 error.type:json 字段

      • message_key

        指定 json 数据中要用哪个字段来作为 filtering 和 multiline 的作用目标, 这个字段必须在 json 中作为顶级字段, 且只能是字符串.

      • document_id 指定 json 中的某个字段作为 document id, 若配置该项, 则会从原始 json 中移除该字段, 并存储在 @metadata._id
      • ignore_decoding_error 是否忽略 json 解析失败的 event

        默认是 false
      json.keys_under_root: true
      json.add_error_key: true
      json.message_key: log
    • multiline.* 控制 filebeat 如何处理跨多行的日志消息.
    • exclude_files 设置要忽略的文件(正则表达式)

      filebeat.inputs:
      - type: log
        ...
        exclude_files: ['\.gz$']
    • ignore_older 忽略在某个时间段(modified time)前的文件

      这对于已经生成一部分日志, 但此时启动 filebeat 时只想处理某个时间段后的日志时特别有用.

      默认是 0, 表示忽略该配置.

      可以设置为 2h(2小时), 5m(5分钟) 之类的.

      该选项是依赖于文件的 modification time, 因此在 windows 上可能存在异常情况(比如修改了文件内容, 但文件的 modification time 没变时)

      ignore_older 的值必须大于 close_inactive

      该选项会影响到的文件分类两类

      1. 从没有被 harvested 的文件

        此时该文件的 offset state 会被设置为文件尾.

      2. 文件已经被 harvested, 但超过 ignore_older 时长没有更新过

        此时该文件的 offset state 不会被改变, 后续若该文件又被修改, 那么也会被继续正常处理.

      若要从 registry 文件中移除之前被 harvested 的文件状态, 则应使用 clean_inactive 选项

      文件必须被关闭后(指 filebeat 不能持有该文件的句柄)才能被 filebeat 忽略掉(ignore). 为了确保被忽略的文件不再处于 harvested 状态, 必须设置 ignore_older 的值比 close_inactive 大.

      若当前正在被 harvested 的文件受到 ignore_older 影响(指超过该设置的时间未更新), 则 harvester 首先读取该文件并在达到 close_inactive 时间到达时关闭该文件, 并在这之后忽略该文件.

    • close_*

      这一类选项是用于配置让 harvester 在经过特定的时间或策略后自动关闭(即关闭文件句柄).

      若文件对应的 harvester 关闭后被更新, 那么会在 scan_frequency 时间后再起启动.

      但是如果在文件对应的 harvester 关闭时, 文件被移动或修改, 则这部分数据会丢失.

      The close_* settings are applied synchronously when Filebeat attempts to read from a file, meaning that if Filebeat is in a blocked state due to blocked output, full queue or other issue, a file that would otherwise be closed remains open until Filebeat once again attempts to read from the file.

      每个文件有一个对应的 harvester 处理.

      这部分英文的不大理解, 我猜测是: 文件对应的 harvester 处于阻塞状态时, close_*设置会被同步应用, 也就意味着本应该关闭的文件会一致保持打开的状态(指句柄), 直到 filebeat 再次尝试读取该文件.

      👆 但还是觉得理解得怪怪的.

      • close_inactive

        默认是 5m.

        可以将该值设为 2h(2小时)或 5m(5分钟) 这种格式.

        当文件处于未被 harvest 一段时间后(并非文件的 modification time), filebeat 会关闭该文件句柄.

        若后续文件又被更新, 则 filebeat 会在最多 scan_frequency 时间内再次启动新的 harvester 来处理.

        通常建议将该值设置为比文件最小更新频率大的一个值, 比如日志文件每几秒更新一次, 那么可以安全地将该值设为 1m.

        若不同类型文件的更新频率不一致, 那么可以通过多个配置中设置不同的值来处理.

        close_inactive 设置为一个更小的值意味着文件句柄会很快被关闭(指距最后一次 harvest 空闲一小段时间). 这会导致如果 harvester 关闭时, 新的日志不能近实时地被收集到.

      • close_renamed

        设置该值可能会导致潜在的数据丢失, 请谨慎.

        默认关闭.

        默认情况下, 文件被重命名后, 其 inode(对应所在的 device)不会发生改变, 因此 harvester 能继续跟踪处理该文件, 但若开启该选项后, filebeat 会在文件被重命名后关闭该文件句柄, 这会导致潜在的数据丢失.

        这个选项一般没啥用, 可能只在 windows 中会用到.

      • close_removed

        默认开启.

        若禁用该选项, 则必须同时禁用 clean_removed

      • close_eof

        设置该值可能会导致潜在的数据丢失, 请谨慎.

        默认关闭

        开启该选项后, harvester 会在 harvest 到文件末尾时关闭文件.

        这适用于当文件仅仅是一次性写入完毕, 且不怎么更新的情况.

      • close_timeout

        设置该值可能会导致潜在的数据丢失, 请谨慎.

        其另一个副作用是多行(multiline)的事件有可能在 timeout 前未写入完整, 从而丢失数据.

        默认值为 0, 表示关闭.

        若设置该值, 则每次 harvester 启动后会在经过 close_timeout 时间后自动关闭(这意味着 multiline 事件有可能丢失一部分).

        若在上一次处理完之后有新的数据产生(即文件更新), 则会在经过 scan_frequency 时间后再次启动新的 harvster, close_timeout 仍然对新的 harvster 有效.

        在 output 阻塞的情况时, 设置该选项是很有用的, 从而避免 filebeat 一直持有已经被删除的文件的句柄. 将该选项设置为 5m 可以确保定期删除文件, 以便操作系统释放它们.

        如果将 close_timeout 设置的值同 ignore_older 一样时, 若 harvester 处于关闭状态时修改了该文件, 那么也不会再次启动对其的 harveste. 这种组合通常会导致数据丢失, 并且不会发送完整的文件.

    • clean_*

      这些选项是用于清理 registry 文件中的状态项, 不仅可以帮助减少 registry 文件的大小, 还可以防止 inode reuse 引起的问题.

      • clean_inactive

        设置该值可能会导致潜在的数据丢失, 请谨慎.

        若设置该项, 则 filebeat 会在指定的不活动周期(???)结束后删除文件的 registry 状态. 只有在文件已经被 filebeat 忽略时(指文件比 ignore_older 还老), 其 state 才会被移除.

        该值必须大于 ignore_older + scan_frequency 以确保不会有文件还在 harvest 时, 其 state 被移除的情况.

        文件的 state 被移除后, 若该文件再次更新/出现时, 文件会从头开始读.

        若每天会生成大量的新文件时, 使用 clean_inactive 选项可以有效地帮助减少 registry 文件的大小.

      • clean_removed

        默认开启.

        如果禁用了 close_removed, 那么必须同时禁用该选项.

        如果文件被移动了(指在硬盘上的路径发生变化), 那么这个文件会被从 registry 中移除.

    • scan_frequency

      默认是 10s

      该选项指定 Filebeat 多久确认一次符合条件的日志文件状态.

      若要尽可能快的让 filebeat 扫描, 那么建议设置为 1s. 非常不建议设置低于 1s 的值, 这会导致性能消耗过大.

      如果需要新增的日志行以近实时的速度捕获并发送, 那么不建议将 scan_frequency 设置得非常小, 而是建议调整 close_inactive, 从而使文件句柄保持打开并不断轮询日志文件.

    • tail_files

      默认设置为 false

      适用于在运行 filebeat 前有一堆不想捕获的旧日志文件, 此时可以将该选项设置为 true, 并执行. 这里需注意, 在执行完毕后, 要记得将该选项设置回 false, 以避免后续对日志可能进行 rotation 操作导致丢失数据.

      若开启该选项, filebeat 会从日志文件的最末尾开始读(即在 registry 文件中记录日志文件的最后读取位置是在当前的末尾), 若当前系统对这些日志进行 rotation 操作, 那么前面的一些日志项可能会丢失.

      若开启该选项前, 文件已经被 filebeat 处理过, 那么该选项无效, 若未被处理过, 则 filebeat 会从文件末尾开始读.

    • symlinks

      默认是 false

      .....

    • backoff

      默认是 1s, 这适用于大多数情况.

      该选项用于控制, 当 harvester 读取到文件末尾(EOF)后, 间隔多久再去检测是否有新的行写进来.

    • max_backoff

      默认是 10s

      设置最大的 backoff 等待时间

    • backoff_factor

      默认是 1s

      在读到文件末尾后, 每次去检测若无新行写入, 则下一次的等待时间是在 min(max_backoff, 上一次等待时间 * backoff_factor), 最早的一次等待时间是 backoff 设定的值.

    • harvester_limit

      默认是 0, 表示不限制.

      该选项用于限制每个 input 中可同时启动的 harvester 数量(这主要跟操作系统对 filebeat 文件句柄数限制有关)

      若设置了该选项, 那么建议同时设置 close_* 选项以确保 harvester 尽可能频繁的停止, 以确保其他(新)文件能够得到处理.

    • file_identity

      对于不同的日志消息手机环境, 可以设置不同的文件标识(file identity)方式来适应环境.

      可选的值:

      • native

        默认.

        使用设备id 和 inode来区分

        file_identity.native: ~
      • path

        使用文件路径作为文件标识, 因此若文件改名或移动路径(但仍处于该 input 的处理范围), 则会导致重复的日志被搜集.

        file_identity.path: ~
      • inode_marker

        不支持 windows

        若日志所在的设备id经常改变, 那么必须使用这种方式来区分文件.

        设置标记文件(marker file)的路径示例:

        file_identity.inode_marker.path: /logs/.filebeat-marker

    简单示例

    filebeat.inputs:
      - type: log
        enabled: true
        paths:
          - /var/log/system.log
          - /var/log/wifi.log
          # 这里会处理 /path/to/log 目录下的所有 *.log 文件(仅处理该层)
          - /path/to/log/*.log
          # 这里会处理 /path/to/log 目录的直接子目录下所有 *.log 文件(仅处理下一级子目录的所有 *.log 文件, 下下层的则不会处理)
          - /path/to/log/*/*.log
      - type: log
        enabled: true
        paths:
          - "/var/log/apache2/*"
        fields:
          apache: true
        fields_under_root: true
        include_lines: ['^ERR', '^WARN']
        exclude_lines: ['^DBG']
    

    Modules

    这里只整理一些官方文档未详细说明的部分.

    Module 提供了最快的开箱即用的方式.

    Nginx Log

    Filebeat 中 Nginx Module 的默认配置支持 Nginx 默认的 Combined 日志格式, 但若是需要新增一些字段, 则需要额外在 Ingest Pipeline 中新增对新字段的 Grok 捕获.

    参考链接:

    以 nginx 以下日志格式

    http {
        map $http_x_forwarded_for $clientRealIp {
            ""  $remote_addr;
    #        ~^(?P<firstAddr>[0-9\.]+),?.*$ $firstAddr;
            ~^.*?,?(?P<lastAddr>[0-9\.]+)$ $lastAddr;
        }
        log_format main '$clientRealIp - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" '
                        '"$upstream_addr" $upstream_response_time $request_time';
        
        # ...
    }

    相较 nginx 默认的 combined 日志格式, 这里的变化:

    • 将第一个参数由 $remote_addr 改为 $clientRealIp.

      由于 nginx 的上游是一个高防 ip, 因此这里取 $http_x_forwarded_for 的最后一个 ip 作为用户真实 ip.
    • 日志最后面追加了 3 个字段

    对应的 log:

    117.136.57.132 - - [19/Nov/2020:19:21:38 +0800] "POST /ccbp/server/p6_gamma/src/public/s1/index.php?_m=GetUserActivity&_uid=3031001 HTTP/1.1" 200 1094 "-" "libcurl" "127.0.0.1:9000" 0.020 0.020
    116.140.27.57 - - [19/Nov/2020:19:21:38 +0800] "POST /ccbp/server/p6_gamma/src/public/s1/index.php?_m=EndPlayFb&_uid=2108001 HTTP/1.1" 200 555 "-" "libcurl" "127.0.0.1:9000" 0.058 0.058
    218.67.252.171 - - [19/Nov/2020:19:21:38 +0800] "POST /ccbp/server/p6_gamma/src/public/s1/index.php?_m=Guild_GetGuildData&_uid=2099001 HTTP/1.1" 200 1528 "-" "libcurl" "127.0.0.1:9000" 0.018 0.018
    117.136.57.132 - - [19/Nov/2020:19:21:38 +0800] "POST /ccbp/server/p6_gamma/src/public/s1/index.php?_m=Guild_GetGuildData&_uid=3031001 HTTP/1.1" 200 253 "-" "libcurl" "127.0.0.1:9000" 0.007 0.007
    61.186.27.216 - - [19/Nov/2020:19:21:38 +0800] "POST /ccbp/server/p6_gamma/src/public/s1/index.php?_m=ReportUserState&_uid=3069001 HTTP/1.1" 200 303 "-" "libcurl" "127.0.0.1:9000" 0.008 0.008
    116.140.27.57 - - [19/Nov/2020:19:21:38 +0800] "POST /ccbp/server/p6_gamma/src/public/s1/index.php?_m=Guild_GetGuildData&_uid=2108001 HTTP/1.1" 200 1525 "-" "libcurl" "127.0.0.1:9000" 0.012 0.012
    117.136.57.132 - - [19/Nov/2020:19:21:38 +0800] "POST /ccbp/server/p6_gamma/src/public/s1/index.php?_m=WorldBoss_GetData&_uid=3031001 HTTP/1.1" 200 477 "-" "libcurl" "127.0.0.1:9000" 0.012 0.012
    110.176.211.27 - - [19/Nov/2020:19:21:38 +0800] "POST /ccbp/server/p6_gamma/src/public/s1/index.php?_m=HeroUpLv&_uid=2483001 HTTP/1.1" 200 438 "-" "libcurl" "127.0.0.1:9000" 0.119 0.119
    1. 若 Filebeat 还未写入过 Nginx 日志, 则此时处理 Nginx 日志的 Ingest Pipeline 就还未创建, 那么可以先修改 Filebeat 所在主机的 /usr/share/filebeat/module/nginx/access/ingest/pipeline.yml

      Filebeat 7.9.3 创建的对应 Ingest Pipeline 是: filebeat-7.9.3-nginx-access-pipeline 以及 filebeat-7.9.3-nginx-error-pipeline
      description: Pipeline for parsing Nginx access logs. Requires the geoip and user_agent
        plugins.
      processors:
      - grok:
          field: message
          patterns:
          # 这里追加了 3 个字段
          - (%{NGINX_HOST} )?"?(?:%{NGINX_ADDRESS_LIST:nginx.access.remote_ip_list}|%{NOTSPACE:source.address})
         %{NUMBER:http.response.status_code:long} %{NUMBER:http.response.body.bytes:long}
         "(-|%{DATA:http.request.referrer})" "(-|%{DATA:user_agent.original})"
       pattern_definitions:
         NGINX_HOST: (?:%{IP:destination.ip}|%{NGINX_NOTSEPARATOR:destination.domain})(:%{NUMBER:destination.port})?
         NGINX_NOTSEPARATOR: "[^\t ,:]+"
         NGINX_ADDRESS_LIST: (?:%{IP}|%{WORD})("?,?\s*(?:%{IP}|%{WORD}))*
       ignore_missing: true
    • grok:

      field: nginx.access.info
      patterns:
          - '%{WORD:http.request.method} %{DATA:url.original} HTTP/%{NUMBER:http.version}'
          - ""
       ignore_missing: true
    • grok:

      field: url.original
      patterns:
          - '%{URIPATH:url.path}(%{URIPARAM:url.param})?'
       ignore_missing: true
       description: "从 uri 中解析出 path 和 param"
    • grok:

      field: url.param
      patterns:
          - '[?&]_uid=%{NUMBER:game.uid:long}'
       ignore_missing: true
       description: "从 param 中解析出 uid"
    • grok:

      field: url.param
      patterns:
          - '[?&]_m=%{WORD:game.method}'
       ignore_missing: true
       description: "从 param 中解析出 method"
    • grok:

      field: url.path
      patterns:
          - 'server/%{WORD:game.group}/src/public/s%{NUMBER:game.server_id:long}/'
       ignore_missing: true
       description: "从 url 中抽取游戏组和游戏服 id"
               
    • remove:

      field: nginx.access.info
      

    ...
    ...
    ...

    
    > 注意, 上面增加了 3 个新字段:
    >
    > - nginx.upstream_addr
    > - nginx.upstream_time
    > - nginx.request_time
    
    2. 若 Filebeat 之前已写入过 nginx 日志, 那么此时日志的 Ingest Pipeline 已经创建, 则可以直接在 kibana 上修改 *filebeat-7.9.3-nginx-access-pipeline* 这个 ingest pipeline
    

    [

     {
       "grok": {
         "field": "message",
         "patterns": [
           // 这里追加了 3 个新字段
           "(%{NGINX_HOST} )?\"?(?:%{NGINX_ADDRESS_LIST:nginx.access.remote_ip_list}|%{NOTSPACE:source.address}) - (-|%{DATA:user.name}) \\[%{HTTPDATE:nginx.access.time}\\] \"%{DATA:nginx.access.info}\" %{NUMBER:http.response.status_code:long} %{NUMBER:http.response.body.bytes:long} \"(-|%{DATA:http.request.referrer})\" \"(-|%{DATA:user_agent.original})\" \"(-|%{DATA:nginx.upstream_addr})\" %{NUMBER:nginx.upstream_time:float} %{NUMBER:nginx.request_time:float}"
         ],
         "pattern_definitions": {
           "NGINX_HOST": "(?:%{IP:destination.ip}|%{NGINX_NOTSEPARATOR:destination.domain})(:%{NUMBER:destination.port})?",
           "NGINX_NOTSEPARATOR": "[^\t ,:]+",
           "NGINX_ADDRESS_LIST": "(?:%{IP}|%{WORD})(\"?,?\\s*(?:%{IP}|%{WORD}))*"
         },
         "ignore_missing": true
       }
     },
     {
       "grok": {
         "field": "nginx.access.info",
         "patterns": [
           "%{WORD:http.request.method} %{DATA:url.original} HTTP/%{NUMBER:http.version}",
           ""
         ],
         "ignore_missing": true
       }
     },
     // 从 uri 中解析出 path 和 param
     {
       "grok": {
         "field": "url.original",
         "patterns": [
           "%{URIPATH:url.path}(%{URIPARAM:url.param})?"
         ],
         "ignore_missing": true,
         "description": "从 uri 中解析出 path 和 param"
       }
     },
     // 从 param 中解析出 uid
     {
       "grok": {
         "field": "url.param",
         "patterns": [
           "[?&]_uid=%{NUMBER:game.uid:long}"
         ],
         "ignore_missing": true,
         "description": "从 param 中解析出 uid"
       }
     },
     // 从 param 中解析出 method
     {
       "grok": {
         "field": "url.param",
         "patterns": [
           "[?&]_m=%{WORD:game.method}"
         ],
         "ignore_missing": true,
         "description": "从 param 中解析出 method"
       }
     },
     // 从 url 中抽取游戏组和游戏服 id
     {
       "grok": {
         "field": "url.path",
         "patterns": [
           "server/%{WORD:game.group}/src/public/s%{NUMBER:game.server_id:long}/"
         ],
         "ignore_missing": true,
         "description": "从 url 中抽取游戏组和游戏服 id"
       }
     },
       
     ...
     ...
     ...

    ]

    
    > 相较原始的配置, 这里修改了 `patterns`, 在最后面追加了3个字段配置
    
    建议将本次的修改同步到 `/usr/share/filebeat/module/nginx/access/ingest/pipeline.yml` 中
    
    ![image-20201118185247119](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201118185247.png)
    
    
    
    
    
    网上有说要修改 `/etc/filebeat/fields.yml`, 往其中添加新增的字段, 但不理解其意思 但我这里没添加也能用.
    
    > 应该是 `filebeat setup` 初始化时创建 
    
    
    
    在上面步骤处理完之后, 在 kibana 的 discover 和 日志 界面中就可以看到新字段了, 但是这些新字段暂时无法被用于查询, 聚合操作, 这是因为 `filebeat-*` 索引模式中关于字段的配置还是旧的, 需要更新, 操作步骤:
    
    1. Kibana -> Stack Management -> 索引模式
    2. 进入 `filebeat-*` 索引模式页面
    3. 点击右上角的刷新按钮进行字段列表刷新
    
    ![image-20201118185135647](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201118185135.png)
    
    
    
    
    
    #### Outputs
    
    ##### ElasticSearch
    
    [写入 ES](https://www.elastic.co/guide/en/beats/filebeat/7.9/elasticsearch-output.html)
    
    
    
    *示例*
    

    output.elasticsearch:
    enabled: true
    hosts: ["https://myEShost:9200"]

    pipeline: my_pipeline_id

    compression_level: 0

    index: "%{[fields.log_type]}-%{[agent.version]}-%{+yyyy.MM.dd}"

    protocol: https

    username: "filebeat_writer"

    password: "YOUR_PASSWORD"

    
    
    
    部分选项说明
    
    - `compression_level` 数据传输时的压缩率(gzip)
    
      > 默认是 0
    
      可选值: 1~9 (9的压缩级别最高)
    
    - `index` 当使用每日(daily)索引时的索引名
    
      > 若使用该选项, 则会禁用 ILM 管理索引
      >
      > 示例:
      >
      > ```yaml
      > output.elasticsearch:
      >   index: "%{[fields.log_type]}-%{[agent.version]}-%{+yyyy.MM.dd}"
      > ```
    
      每天固定创建一个索引时指定的索引名字,  具体请查看[此处](https://www.elastic.co/guide/en/beats/filebeat/7.9/elasticsearch-output.html#index-option-es).
    
    - `indices`
    
      > 若使用该选项, 同样会禁用 ILM 管理索引.
    
      根据规则, 匹配要使用的索引名. 若未匹配到, 则使用  `index` 指定的索引名
    

    output.elasticsearch:

    hosts: ["http://localhost:9200"]
    indices:
      # 这是采用 condition 的方式
      - index: "warning-%{[agent.version]}-%{+yyyy.MM.dd}"
        when.contains:
          message: "WARN"
      - index: "error-%{[agent.version]}-%{+yyyy.MM.dd}"
        when.contains:
          message: "ERR"
      # 这是采用 mapping 的方式
      - index: "%{[fields.log_type]}"
        mappings:
          critical: "sev1"
          normal: "sev2"
        default: "sev3"
    
    具体的 when 的条件可以参见: https://www.elastic.co/guide/en/beats/filebeat/7.9/defining-processors.html#conditions
    
    - `ilm` 
    
    See [*Index lifecycle management (ILM)*](https://www.elastic.co/guide/en/beats/filebeat/7.9/ilm.html) for more information.
    
    - `pipeline`
    
    指定要使用哪个 ingest node pipeline 来写数据.
    

    output.elasticsearch:

    hosts: ["http://localhost:9200"]
    pipeline: my_pipeline_id
    # 也可以动态使用不同的 pipeline

    # pipeline: "%{[fields.log_type]}_pipeline"

    
    - `pipelines`
    
    类似 `indices`, 此处同样是指定一系列的 pipelines, 同样是使用第一个匹配的 pipeline, 若未匹配到则使用 `pipeline` 中设置的.
    

    output.elasticsearch:

    hosts: ["http://localhost:9200"]
    pipelines:
      # 采用 condition 的方式
      - pipeline: "warning_pipeline"
        when.contains:
          message: "WARN"
      - pipeline: "error_pipeline"
        when.contains:
          message: "ERR"
      # 采用 mapping 的方式
      - pipeline: "%{[fields.log_type]}"
        mappings:
          # 自定义字段 log_type 值为 "critical" 时, 使用 "sev1_pipeline" ingest pipeline
          critical: "sev1_pipeline"
          normal: "sev2_pipeline"
        default: "sev3_pipeline"
    
    具体的 when 的条件可以参见: https://www.elastic.co/guide/en/beats/filebeat/7.9/defining-processors.html#conditions
    
    - `bulk_max_size`
    
    filebeat 使用 bulk api 向 ES 写入数据时, 一次最多可以发送的最大 event
    
    - `timeout`
    
    > 默认是 90s
    
    http 超时时间
    
    
    
    
    
    
    
    
    
    
    
    
    
    ##### File
    
    一般用作调试
    
    
    
    ##### Console
    
    [Console](https://www.elastic.co/guide/en/beats/filebeat/7.9/console-output.html) 仅供调试
    
    
    

    output.console:
    pretty: true

    output.console:
    codec.json:

    pretty: true
    escape_html: false

    output.console:
    codec.format:

    string: '%{[@timestamp]} %{[message]}'
    
    
    
    
    
    # 用Kibana进行数据可视化分析
    
    ## 使用Index Pattern配置数据
    
    *导入数据*
    

    curl -H 'Content-Type: application/x-ndjson' -XPOST 'localhost:9200/_bulk?pretty' --data-binary @logs.jsonl

    curl -H 'Content-Type: application/x-ndjson' -XPOST 'localhost:9200/bank/account/_bulk?pretty' --data-binary @accounts.json

    
    > 数据来源: https://github.com/geektime-geekbang/geektime-ELK/tree/master/part-4/13.1-%E4%BD%BF%E7%94%A8IndexPattern%E9%85%8D%E7%BD%AE%E6%95%B0%E6%8D%AE
    
     
    
    *在 Kibana 中创建 Index Pattern*
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201109113755.jpg) 
    
    
    
    
    
    ## 使用Kibana Discover探索数据
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201109115113.jpg) 
    
     
    
     
    
      
    
     
    
     
    
     
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201109115114.jpg) 
    
    
    
    
    
    ## 基本可视化组件介绍
    
    ![image-20201109161006815](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201109161006.png)
    
    
    
    
    
    
    
    ## 构建Dashboard
    
    ![image-20201110091740334](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201110091740.png)
    
    
    
    
    
    Dashboard 可将一组相关的可视化组件组合在一起, 展示在同一个面板里.
    
    
    
    
    
    # 探索X-Pack套件
    
    ## 用Monitoring和Alerting监控Elasticsearch集群
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201111182543.jpg) 
    
     
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201111182544.jpg) 
    
    https://www.elastic.co/guide/en/x-pack/current/monitoring-settings.html
    
     
    
     
    
     
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201111182545.jpg) 
    
     
    
     
    
    
    
    
    
    ## 用APM进行程序性能监控
    
    !!!! [PHP 版本的 APM Agent](https://github.com/elastic/apm-agent-php) 目前(2020年11月12日 23:53:18)仍处于开发版本, 而非正式版.
    
    
    
    
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201112234529.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201112234530.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201112234531.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201112234532.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201112234533.jpg) 
    
    
    
    
    
    ## 用机器学习实现时序数据的异常检测(上)
    
    > 机器学习是 xpack 的收费功能
    
    
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113001751.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113001752.jpg)
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113001753.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113001754.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113001755.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113001756.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113001757.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113001758.jpg)
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113001759.jpg)
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113001800.jpg)
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113001801.jpg) 
    
    
    
    ## 用机器学习实现时序数据的异常检测(下)
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113002752.jpg)
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113002753.jpg)
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113002754.jpg)
    
    
    
    
    
     
    
    
    
    ## 用ELK进行日志管理
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113103654.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113103655.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113103656.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113103657.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113103658.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113103659.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113103647.jpg) 
    
    
    
    
    
    ## 用Canvas做数据演示
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113104916.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113104917.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113104918.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113104919.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113104920.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113104921.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113104922.jpg) 
    
    ![img](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20201113104923.jpg) 
    
    查看原文

    赞 0 收藏 0 评论 0

    嘉兴ing 发布了文章 · 1月18日

    ⭐《ElasticSearch核心技术与实战》笔记 - 3. 管理集群

    [TOC]

    TODO: 暂时略过, 后续再补课.

    (这部分暂时以截图为主)

    https://github.com/geektime-g...

    这一整节的内容 pdf, 可以搜索复制文本.

    保护你的数据

    集群身份认证与用户鉴权

    image-20201103095444936

    image-20201103095453930

    image-20201103095504944

    image-20201103095513198

    image-20201103095523390

    image-20201103095535669

    image-20201103095546113

    image-20201103095553620

    image-20201103095600831

    开启并配置 X-Pack 的认证与鉴权

    1. 修改配置文件, 打开认证与授权 elasticsearch.yml

      xpack.security.enabled: true
      # xpack.license.self_generated.type: basic
      # xpack.security.transport.ssl.enabled: true
    2. 启动 ES
    3. 创建默认的用户和分组

      bin/elasticsearch-setup-passwords interactive

      会创建用户:

      • elastic
      • apm_system
      • kibana
      • kibana_system
      • logstash_system
      • beats_system
      • remote_monitoring_user
    4. 配置 Kibana 身份认证 kibana.yml

      elasticsearch.username: "kibana_system"
      elasticsearch.password: "......"
    5. 配置 Logstash 身份认证 logstash.yml

      • 启用 X-Pack Monitoring(Basic Free)

        xpack.monitoring.enabled: true
        
        xpack.monitoring.elasticsearch.username: "logstash_system"
        
        xpack.monitoring.elasticsearch.password: "...."
        
        xpack.monitoring.elasticsearch.hosts: ["http://ip:9200"]
      • 在 logstash 定义 conf 时, 记得加上账号密码

        input {
          elasticsearch {
            ...
            user => "logstash_system"
            password => .....
          }
        }
        
        filter {
          elasticsearch {
            ...
            user => "logstash_system"
            password => .....
          }
        }
        
        output {
          elasticsearch {
            ...
            user => "logstash_system"
            password => "....."
            #ssl => true
            #cacert => '/path/to/cert.pem'
          }
        }
    注意, 若是在生产环境, 启动 ES 时会强制进行 Bootstrap Check, 在开启了 xpack.security.enabled: true 情况下会要求配置 xpack.security.transport.ssl, 具体见下面的 "集群内部安全通信" 一节.

    image-20201103095624616

    image-20201103095632690

    在 kibana 中 manager 的 security 可以方便图形化地添加 用户/角色

    集群内部安全通信

    img

    ES 内部使用 9300 端口传输

    生成节点证书

    # 直接一路回车, 然后会在 elasticsearch 的安装目录下生成一个文件: *elastic-stack-ca.p12*
    bin/elasticsearch-certutil ca
    
    
    # 一路回车, 会在 elasticsearch 的安装目录下生成一个文件: *elastic-certificates.p12*
    bin/elasticsearch-certutil cert --ca elastic-stack-ca.p12
    
    
    # 统一将上述生成的证书文件都放到 elasticsearch 安装目录下 certs 目录中
    # 若使用 rpm 方式安装, 那么配置文件和 elasticsearch 安装目录不在一起, 此时可以将这些证书移到 /etc/elasticsearch/certs 中
    # 注意证书文件的权限, 需确保至少 elasticsearch 可读
    mkdir certs
    mv *.p12 ./certs
    https://www.elastic.co/guide/...

    img

    默认生成的 CA 文件名: elastic-stack-ca.p12

    默认生成的节点证书文件名: elastic-certificates.p12

    img

    配置节点间通讯

    xpack.security.enabled: true
    xpack.security.transport.ssl.enabled: true
    xpack.security.transport.ssl.verification_mode: "certificate"
    xpack.security.transport.ssl.keystore.path: "certs/elastic-certificates.p12"
    xpack.security.transport.ssl.truststore.path: "certs/elastic-certificates.p12"

    img

    集群与外部间的安全通信

    img

    img

    img

    ↑ kibana 使用的证书格式是 pem, 而之前 es 生成的节点证书(默认文件名: elastic-certificates.p12)是p12 格式, 因此需要使用 openssl 将其转换为 pem 格式.img

    img

    img

    img

    ↑ 将上述生成的 elastic-stack-ca.zip 解压得到 ca.crt 文件和 ca.key 文件.

    配置 config/kibana.yml:

    img

    由于这里的证书是自签的, kibana启动时会报错, 只是做测试, 可以忽略.

    水平扩展 ES 集群

    常见的集群部署方式

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    Hot & Warm 架构与Shard Filtering

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    当使用 force awareness, 都指定了不存在的 zone, 那么会导致分片(副本)无法分配.

    img

    分片设计及管理

    img

    ↑ 7.0 之前是默认创建5个主分片...

    img

    img

    img

    img

    img

    img

    img

    如何对集群进行容量规划

    img

    img

    img

    后文有提到说, 搜索类的按照 1:16, 日志类的按照 1:48 ~ 1:96 之间.

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    在私有云上管理 ES 集群的一些方法

    img

    img

    img

    img

    img

    img

    img

    img

    在公有云上管理与部署 ES 集群

    img

    img

    img

    img

    生产环境中的集群运维

    生产环境常用配置与上线清单

    img

    img

    img

    需配置 discovery.seed_hosts, discovery.seed_providers, cluster.initial_master_nodes 这几个中至少一个, 比如:

    discovery.seed_hosts: ["127.0.0.1"]
    cluster.initial_master_nodes: ["node-1"]

    剩下的 50% 的物理内存要分配给 Lucene 使用.

    分配给JVM 的内存建议不要超过 32GB, 超过之后性能反而会下降...

    JVM 有 Server 和 Cli 这两种模式

    img

    > 官方文档中 Import Elasticsearch configuration 这一节中介绍了一些重要的 ES 参数配置项

    img

    > 需要对 Linux 主机进行相关设定, 否则在生产环境模式是无法通过检查, 启动报错.

    > 具体参见 Bootstrap Checks 一节

    img

    img

    > 这里的内存大小指的是给 JVM 分配的大小, 系统应该额外预留系统的另外一半内存给 Lucene.

    > 也就是上面单个节点分配给JVM 31G内存, 那么这台主机实际内存占用在 64GB 左右

    img

    img

    img

    img

    img

    img

    监控 ES 集群

    img

    img

    img

    img

    诊断集群的潜在问题

    img

    img

    img

    img

    img

    img

    img

    img

    解决集群 Yellow 与 Red 的问题

    img

    img

    img

    https://github.com/geektime-g...

    img

    img

    img

    img

    img

    提升集群写性能

    img

    img

    img

    ↓ 文档建模的一些最佳实践

    img

    img

    img

    img

    img

    img

    img

    一个索引设定的例子

    PUT my_test
    {
      "mappings": {
        // 避免不必要的字段索引.
        // 必要时可以通过 update by query 索引必要的字段
        "dynamic": "false",
        "properties": {}
      },
      "settings": {
        "index": {
          "routing": {
            "allocation": {
              // 控制该索引的在每个节点的分片数, 避免数据热点
              "total_shards_per_node": "3"
            }
          },
          // 30 秒一次 Refresh
          "refresh_interval": "30s",
          "number_of_shards": "2",
          // 降低 translog 落盘
          "translog": {
            "sync_interval": "30s",
            "durability": "async"
          },
          "number_of_replicas": "0"
        }
      }
    }

    提升集群读性能

    img

    img

    img

    img

    img

    img

    img

    img

    集群压力测试

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    段合并优化及注意事项

    img

    img

    img

    img

    缓存及使用 Breaker 限制内存使用

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    一些运维的相关建议

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    索引生命周期管理

    使用 Shrink 与 Rollover API 有效管理序列索引

    img

    img

    img

    img

    *Demo*

    img

    imgimg

    imgimg

    img

    Demo

    imgimg

    img

    img

    示例1

    imgimg

    示例2

    img

    Rollover API

    Rollover API

    • 类似 Log4J 记录日志的方式,索引尺寸或者时间超过一定值后,创建新的
    • 支持的条件判断(满足任一条件)

      • 存活时间 max_age
      • 最大文档数 max_docs
      • 最大的索引大小 max_size
    • Rollover API 是一次性的, ES 仅在调用时进行判断并处理, 后续并不会进行监控.

      若需要持续监控, 并在满足条件时 rollover 时, 则应使用 ILM(Index Lifecycle Management Policies) 结合使用

      这特别适合索引数据量持续增大, 且容易过大的.
    • 当调用 Rollover API 时, 若满足条件将会创建新索引, "写别名"(write alias)会被更新为指向新的索引, 后续更新都会被写入新的索引.
    • Rollover 对于别名指向的索引中显示设置了 is_write_index: true 的, 并不会简单地将别名指向新索引, 而是 rollover 设置了 is_write_index: true 的那个旧索引(指向的多个索引中只能有1个设置了该值), 在创建了新的索引后, 将别名新增指向该索引, 并设置 write 到新索引, 同时从所有已存在的索引中 read.

      示例

      PUT my_logs_index-000001
      {
        "aliases": {
           // configures my_logs_index as the write index for the logs alias
          "logs": { "is_write_index": true } 
        }
      }
      
      PUT logs/_doc/1
      {
        "message": "a dummy log"
      }
      
      POST logs/_refresh
      
      POST /logs/_rollover
      {
        "conditions": {
          "max_docs":   "1"
        }
      }
      
      // newly indexed documents against the logs alias will write to the new index: my_logs_index-000002
      PUT logs/_doc/2 
      {
        "message": "a newer log"
      }
      
      
      // 此时 alias 的配置如下:
      /*
      {
        "my_logs_index-000002": {
          "aliases": {
            "logs": { "is_write_index": true }
          }
        },
        "my_logs_index-000001": {
          "aliases": {
            "logs": { "is_write_index" : false }
          }
        }
      }
      */

    新创建的索引名

    • 如果是对 alias 做 rollover, 并且 alias 指向的已存在的索引的命名格式是 - 加上一个数字( 比如 logs-1), 那么新创建的索引会按照这个模式, 并递增数字序号来创建.

      新的数字序号是用 0 作填充的 6 位数(不管旧的索引的数字位数是多少位), 比如 logs-000002

    • Rollover API 支持 date math, 因此若旧的索引命名是按照 date math, 那么 rollover 新创建的索引也会是按照 date math 来创建

      示例

      // 注意, 这里必须用 urlencode 的来作为 uri path, 否则会报错.
      // 示例, 这里实际创建的索引名是 logs_2016.10.31-1
      // PUT /<logs-{now/d}-1> with URI encoding:
      PUT /%3Clogs-%7Bnow%2Fd%7D-1%3E 
      {
        "aliases": {
          "logs_write": {}
        }
      }
      
      
      PUT logs_write/_doc/1
      {
        "message": "a dummy log"
      }
      
      
      POST logs_write/_refresh
      
      // Wait for a day to pass
      
      // 如果是在当天执行, 那么这里创建的新索引名会是 logs_2016.10.31-000002
      // 如果是隔天, 那么就是 logs_2016.11.01-000002
      POST /logs_write/_rollover 
      {
        "conditions": {
          "max_docs":   "1"
        }
      }

    为什么要使用 Rollover API

    • 不断膨胀的历史数据(特别是时间序列数据)需要加以限制
    • rollover 功能可以以紧凑的聚合格式保存旧数据, 仅保存您感兴趣的数据

    Rollover API Demo

    // 当加上参数 ?dry_run ,那么仅仅是测试, 可以方便地查看是否满足 rollover 条件(及具体哪个条件), 并没有实际执行 rollover 操作.
    // 比如 POST /<rollover-target>/_rollover?dry_run
    
    POST /<rollover-target>/_rollover
    {
        "conditions": {
            // 如果时间超过7天,那么自动rollover,也就是使用新的index
            "max_age": "7d",
            // 如果文档的数目超过14000个,那么自动rollover
            "max_docs": 100000,
            // 如果index的大小超过5G(仅针对主分片统计, 副本分片不会被算进来),那么自动rollover
            "max_size": "5gb"
        }
    }
    
    
    // 也可以指定具体新创建的索引的名子
    POST /<rollover-target>/_rollover/<target-index>

    Path 路径参数

    • <rollover-target> 可以是 alias(索引别名) 或 data stream

      必选参数

      根据不同的 target, rollover 的行为也不一样

      • 若是指向一个单独索引的 alias

        1. 创建新索引
        2. alias 指向新的索引
        3. 从原来的索引上移除 alias
      • 若指向(多个)索引的 alias, 且这些索引中有(且只能是)一个设置了 is_write_index: true

        1. 创建新索引
        2. 对新创建的索引设置 is_write_index: true
        3. 对旧的索引设置 is_write_index: false
    > 此时 alias 会同时指向这些索引, 但只会 write 到新建的索引(即此时 `is_write_index: true` 的那个索引), read 依旧是从所有索引中读.
    
    • 若是"data stream"

      1. 创建新索引
      2. 在 data stream 上将新建的索引添加进去作为 backing index 和 write index
      3. 增加 data stream 属性的 generation 值.
    • <target-index> 可以指定新创建的索引的名字

      可选参数.

      <rollover-target> 是 data stream, 则不支持设置该参数.

      <rollover-target> 是 alias, 且对应的索引名不符合规则(以 -数字 为结尾, 比如 logs-001 是符合规则的), 那么就必须手动指定该参数.

      命名需满足规则

      • 仅小写
      • 不能包含 \/*"<>|(空格字符)、,#
      • ES 7.0 之前可以包含冒号(:),但从 7.0 开始就不支持了
      • 不能以 -_+ 作为索引名开头
      • 不可能是 ...
      • 不能超过 255个字节(多字节的要注意)
      • . 开头的索引不建议使用(deprecated), 除了隐藏索引( hidden indices)以及插件管理的内部索引外.

    建议索引命名格式: xxxxxxx-6位数字, 比如从 xxxxx-000001 开始

    Query 查询参数

    • dry_run 仅仅是测试是否满足 rollover 条件(包括查看索引名), 并不实际执行 rollover.

      可选

    Request boyd 请求体

    • aliases 索引别名
    • conditions rollover 条件

      • max_age
      • max_docs
      • max_size
      这里的 docs 和 size 都只算主分片的, 并不会受副本分片影响.
    • mappings 设置新索引的 mapping
    • settings 设置新索引的 settings

    filebeat 自动创建的 ILM 示例中配置的 rollover

    PUT _ilm/policy/filebeat
    {
      "policy": {
        "phases": {
          "hot": {
            "min_age": "0ms",
            "actions": {
              "rollover": {
                "max_age": "30d",
                "max_size": "50gb"
              },
              "set_priority": {
                "priority": null
              }
            }
          }
        }
      }
    }

    索引全生命周期管理及工具介绍

    索引生命周期管理(ILM)特别适合处理时间序列的索引.

    时间序列的索引是指:

    • 索引中的数据随着时间,持续不断增长.
    • 数据量大, 且早期的数据价值越来越低(很少写甚至是只读, 到完全无用)
    • 按照时间序列划分索引

      • 管理更方便(比如完整删除一个索引的操作会比 delete_by_query 性能更好)

    ILM 将索引生命周期按时间顺序划分为以下几个阶段(单向变化)

    1. Hot: 索引还存在着大量的读写操作
    2. Warm:索引不存在写操作,还有被查询的需要
    3. Cold:数据不存在写操作,读操作也不多
    4. Delete:索引不再需要,可以被安全删除
    Hot -> Warm -> Cold -> Delete

    并不要求所有阶段都要设置, 比如 filebeat 创建的 ILM 就只有一个 Hot

    ILM 能做的事:

    • 根据条件修改索引的生命周期

      比如 Cold 阶段的数据存放在性能比较差的 ES 节点上.
    • 定期关闭或删除索引

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    filebeat 的示例

    ILM Policy

    PUT _ilm/policy/filebeat
    {
      "policy": {
        "phases": {
          "hot": {
            "min_age": "0ms",
            "actions": {
              "rollover": {
                "max_age": "30d",
                "max_size": "50gb"
              },
              "set_priority": {
                "priority": null
              }
            }
          },
          "delete": {
            "min_age": "180d",
            "actions": {
              "delete": {
                "delete_searchable_snapshot": true
              }
            }
          }
        }
      }
    }

    Index Template

    PUT /_template/filebeat-7.9.3
    {
        "index_patterms": [
            "filebeat-7.9.3-*"
        ],
        "settings": {
      "index": {
        "lifecycle": {
          "name": "filebeat",
          "rollover_alias": "filebeat-7.9.3"
        },
        "mapping": {
          "total_fields": {
            "limit": "10000"
          }
        },
        // 将 refresh interval 设置为 5s 以提高性能
        "refresh_interval": "5s",
        "number_of_shards": "1",
        // 这个是我自己加的, 因为我在部署时是单节点的 ES 集群
        "number_of_replicas": "0",
        "max_docvalue_fields_search": "200",
        "query": {
          "default_field": [
            "message",
            "tags",
            "agent.ephemeral_id",
            "agent.id",
            "agent.name",
            "agent.type",
            "agent.version",
            ...
            ...
            ...
            "fields.*"
          ]
        }
      }
    }
    }
    PUT _template/filebeat-7.9.3?include_type_name
    {
      "order": 1,
      "index_patterns": [
        "filebeat-7.9.3-*"
      ],
      "settings": {
        "index": {
          "lifecycle": {
            "name": "filebeat",
            "rollover_alias": "filebeat-7.9.3"
          },
          "mapping": {
            "total_fields": {
              "limit": "10000"
            }
          },
          // 将 refresh interval 设置为 5s 以提高性能
          "refresh_interval": "5s",
          "number_of_shards": "1",
          // 这个是我自己加的, 因为我在部署时是单节点的 ES 集群因此将副本分片设为 0
          "number_of_replicas": "0",
          "max_docvalue_fields_search": "200",
          "query": {
            "default_field": [
              "message",
              "tags",
              "agent.ephemeral_id",
              "agent.id",
              "agent.name",
              "agent.type",
              "agent.version",
              "as.organization.name",
              // 这里省略大量字段
              ...
              ...
              ...
              "fields.*"
            ]
          }
        }
      },
      "mappings": {
        "_doc": {
          "dynamic": true,
          "numeric_detection": false,
          "date_detection": false,
          "_source": {
            "enabled": true,
            "includes": [],
            "excludes": []
          },
          "_meta": {
            "beat": "filebeat",
            "version": "7.9.3"
          },
          "_routing": {
            "required": false
          },
          "dynamic_templates": [
            {
              "labels": {
                "path_match": "labels.*",
                "mapping": {
                  "type": "keyword"
                },
                "match_mapping_type": "string"
              }
            },
            {
              "container.labels": {
                "path_match": "container.labels.*",
                "mapping": {
                  "type": "keyword"
                },
                "match_mapping_type": "string"
              }
            },
            {
              "dns.answers": {
                "path_match": "dns.answers.*",
                "mapping": {
                  "type": "keyword"
                },
                "match_mapping_type": "string"
              }
            },
            {
              "log.syslog": {
                "path_match": "log.syslog.*",
                "mapping": {
                  "type": "keyword"
                },
                "match_mapping_type": "string"
              }
            },
            {
              "network.inner": {
                "path_match": "network.inner.*",
                "mapping": {
                  "type": "keyword"
                },
                "match_mapping_type": "string"
              }
            },
            {
              "observer.egress": {
                "path_match": "observer.egress.*",
                "mapping": {
                  "type": "keyword"
                },
                "match_mapping_type": "string"
              }
            },
            {
              "observer.ingress": {
                "path_match": "observer.ingress.*",
                "mapping": {
                  "type": "keyword"
                },
                "match_mapping_type": "string"
              }
            },
            {
              "fields": {
                "path_match": "fields.*",
                "mapping": {
                  "type": "keyword"
                },
                "match_mapping_type": "string"
              }
            },
            {
              "docker.container.labels": {
                "path_match": "docker.container.labels.*",
                "mapping": {
                  "type": "keyword"
                },
                "match_mapping_type": "string"
              }
            },
            {
              "kubernetes.labels.*": {
                "path_match": "kubernetes.labels.*",
                "mapping": {
                  "type": "keyword"
                },
                "match_mapping_type": "*"
              }
            },
            {
              "kubernetes.annotations.*": {
                "path_match": "kubernetes.annotations.*",
                "mapping": {
                  "type": "keyword"
                },
                "match_mapping_type": "*"
              }
            },
            {
              "docker.attrs": {
                "path_match": "docker.attrs.*",
                "mapping": {
                  "type": "keyword"
                },
                "match_mapping_type": "string"
              }
            },
            {
              "azure.activitylogs.identity.claims.*": {
                "path_match": "azure.activitylogs.identity.claims.*",
                "mapping": {
                  "type": "keyword"
                },
                "match_mapping_type": "*"
              }
            },
            {
              "kibana.log.meta": {
                "path_match": "kibana.log.meta.*",
                "mapping": {
                  "type": "keyword"
                },
                "match_mapping_type": "string"
              }
            },
            {
              "strings_as_keyword": {
                "mapping": {
                  "ignore_above": 1024,
                  "type": "keyword"
                },
                "match_mapping_type": "string"
              }
            }
          ],
          "properties": {
            "@timestamp": {
              "type": "date"
            },
            ...
            ...
            ...
            }
          }
        }
      }
    }
    这是我自己敲的, 不能完全确保没敲错
    查看原文

    赞 0 收藏 0 评论 0

    嘉兴ing 发布了文章 · 1月18日

    ⭐《ElasticSearch核心技术与实战》笔记 - 2. 深入

    [TOC]

    Query DSL (官方文档整理)

    https://www.elastic.co/guide/...

    复合查询

    https://www.elastic.co/guide/...

    bool

    The default query for combining multiple leaf or compound query clauses, as must, should, must_not, or filter clauses. The must and should clauses have their scores combined — the more matching clauses, the better — while the must_not and filter clauses are executed in filter context.

    boosting

    Return documents which match a positive query, but reduce the score of documents which also match a negative query.

    constant_score

    A query which wraps another query, but executes it in filter context. All matching documents are given the same “constant” _score.

    dis_max

    A query which accepts multiple queries, and returns any documents which match any of the query clauses. While the bool query combines the scores from all matching queries, the dis_max query uses the score of the single best- matching query clause.

    function_score

    Modify the scores returned by the main query with functions to take into account factors like popularity, recency, distance, or custom algorithms implemented with scripting.

    Geo 查询

    Shape 查询

    Joining 查询

    基于全文的查询

    https://www.elastic.co/guide/...

    全匹配查询 match_all

    Span 查询

    Specialized 查询

    Term-level 查询

    intervals

    A full text query that allows fine-grained control of the ordering and proximity of matching terms.

    match

    The standard query for performing full text queries, including fuzzy matching and phrase or proximity queries.

    match_bool_prefix

    Creates a bool query that matches each term as a term query, except for the last term, which is matched as a prefix query

    match_phrase

    Like the match query but used for matching exact phrases or word proximity matches.

    match_phrase_prefix

    Like the match_phrase query, but does a wildcard search on the final word.

    multi_match

    The multi-field version of the match query.

    common

    A more specialized query which gives more preference to uncommon words.

    query_string

    Supports the compact Lucene query string syntax, allowing you to specify AND|OR|NOT conditions and multi-field search within a single query string. For expert users only.

    simple_query_string

    A simpler, more robust version of the query_string syntax suitable for exposing directly to users.

    深入搜索

    基于词项和基于全文的搜索

    基于 Term(词项) 的查询

    词项

    Term 的重要性

    • Term 是表达语义的最小单位.

      搜索和利用统计语言模型进行自然语言处理都需要处理 Term

    特点

    • Term Level Query

      • Term Query
      • Range Query
      • Exists Query
      • Prefix Query 前缀查询
      • Wildcard Query 通配符查询
    • 在 ES 中, Term 查询, 对输入不做分词. 会将输入作为一个整体, 在倒排索引中查找准确的词项, 并且使用相关度算分公式为每个包含该词项的文档进行 相关度算分 - 例如 "Apple Store"

      比如对于一个 Text 字段, 在 term 查询时使用大写的文本进行查询, 此时是拿不到结果的, 因此默认情况下该 Text 字段对应的倒排索引中存储的是经过 standard 分词器处理的文本(standard 包含 lowercase character filter).

      而对于 Keyword 字段, 倒排索引存储的是原始的数据(比如 "iPhone"), 那么在 term 查询时就必须使用 "iPhone", 而不是 "iphone"

    • 可以通过 Constant Score 将查询转换成一个 Filtering, 避免算分, 并利用缓存, 提高性能.

      对于 term 查询, 通常是不需要 score, 这种情况下可以通过 constant_score 将跳过算分的步骤.

    term 查询

    Term 查询的示例

    // productId 和 desc 都是 dynamic mapping 设置的 Text 类型, 且同时存在另一个类型为 keyword 的多字段.
    POST products/_bulk
    {"index":{"_id":1}}
    {"productID":"XHDK-A-1293-#fJ3", "desc":"iPhone"}
    {"index":{"_id":2}}
    {"productID":"KDKE-B-9947-#kL5", "desc":"iPad"}
    {"index":{"_id":3}}
    {"productID":"JODL-X-1937-#pV7", "desc":"MBP"}
    
    
    GET products/_search
    {
      "query": {
        "term": {
          "desc": {
            "value": "iphone"
            
            // 由于 term 查询不会做分词处理, 而底层存储的是小写处理的(视具体analyzer), 因此使用 iPhone 是无法匹配到文档的.
            //"value":"iPhone"  
          }
        }
      },
      "profile": "true"
    }
    
    
    GET products/_search
    {
      "query": {
        "term": {
          "productID": {
            "value": "xhdk"
            //"value": "XHDK-A-1293-#fJ3"
            //"value": "xhdk-a-1293-#fj3"
          }
        }
      }
    }

    多字段 mapping 和 term 查询

    多字段特性

    • ES 默认会为每个 Text 字段增加一个名为 "keyword" 的 keyword 类型子字段
    • 可以利用该子字段来实现精确匹配

    复合查询

    Constant Score

    • 将 Query 转成 Filter, 忽略 TF-IDF 计算, 避免相关性算分的开销

      • 即便是对 Keyword 进行 Term 查询, 也会进行算分 _score

        而大多数情况下这种场景是不需要得分的.
    • Filter 可以有效利用缓存

    示例

    POST products/_search
    {
      // 通过 explain,可以看到 _explanation.description 中没有了 TF, IDF 算分过程
      "explain": true,
      "query": {
        "constant_score": {
          "filter": {
            "term": {
              "productID.keyword": "XHDK-A-1293-#fJ3"
            }
          }
        }
      }
    }

    基于全文的查询

    https://www.elastic.co/guide/...

    特点

    • 全文本查找

      • Match Query

        • minimum_should_match: 默认是 1
        • operator: 默认是 OR
      • Match Phrase Query

        • slop: 默认是 0
      • Query String Query
    • 全文本查询的特点

      • 索引和搜索时都会进行分词. 查询字符串先传递到一个合适的分词器, 然后生成一个供查询的词项列表
      • 查询时候会对输入的查询进行分词, 然后每个分词逐个进行底层的查询, 最终将结果进行合并, 并为每个文档生成一个算分.

        例如查询 "Matrix reloaded", 会查找包括 Matrix 或 reloaded 的所有结果.

    Match Query 的查询过程

    1. 对待查询文本进行分词
    2. 对于上述的每个词项(term)到对应的倒排索引查询并打分
    3. 汇总上述各个查询的得分
    4. 按照得分排序, 返回结果

    结构化搜索

    ES 中的结构化搜索(Structured search)

    是指对结构化数据的搜索.
    • 日期, 布尔类型, 数字: 有精确的格式, 可以进行逻辑操作. 包括比较数字或时间的范围, 或判断两个值的大小
    • 结构化的文本可以做精确匹配或部分匹配

      颜色, 标签, 识别码
      • Term 查询 / Prefix 前缀查询
    • 结构化结果结果只有 "是" 或 "否" 两个值

      根据场景需要, 可以决定结构化搜索是否需要打分

    对不同结构化数据的搜索

    • 布尔

      // 对布尔值查询, 有算分
      POST products/_search
      {
        "profile": "true",
        "query": {
          "term": {
            "available": {        // available 是布尔类型
              "value": true
            }
          }
        }
      }
    • 对于不需要算分的, 可以通过 constant score 将查询转为 filter

      POST products/_search
      {
        "profile": "true",
        "query": {
          "constant_score": {
            "filter": {
              "term": {
                "available": {
                  "value": true
                }
              }
            }
          }
        }
      }
    • 数字

      // 数字 range 查询
      GET products/_search
      {
          "profile": "true",
          "query": {
              "range": {
                  "price": {
                      "gte": 20,
                      "lte": 30
                  }
              }
          }
      }
    • 日期

      // 日期 range 查询
      GET products/_search
      {
        "profile": "true",
        "query": {
          "constant_score": {
            "filter": {
              "range": {
                "date": {
                  // 相对时间
                  "gte": "now-2y"
                }
              }
            }
          }
        }
      }
    • 非空

      // Exists
      GET products/_search
      {
        "query": {
          "constant_score": {
            "filter": {
              "exists": {
                "field": "date"
              }
            }
          }
        }
      }
    • 多值字段

      • 多值字段的 term 查询是 "包含" 而不是 "完全相等"

        若要"完全相等"的解决方案: 增加一个额外字段进行计数.

      // 处理多值字段
      POST /movies/_bulk
      {"index":{"_id":1}}
      {"title":"Father of the Bridge part II", "year":1995, "genre":"Comedy"}
      {"index":{"_id":2}}
      {"title":"Dave", "year":1993, "genre":["Comedy", "Romance"]}
      
      GET movies/_mapping
      
      POST movies/_search
      {
        "query": {
          "constant_score": {
            "filter": {
              "term": {
                "genre.keyword": "Comedy"        // 可以匹配上面两条
              }
            }
          }
        }
      }

    搜索的相关性算分

    相关性 Relevance

    • 搜索的相关性算分, 描述了一个文档和查询语句匹配的程度(也可以说是 "相似程度"). ES 会对每个匹配查询条件的结果进行算分 _score
    • 打分的本质是排序, 需要把最符合用户需求的文档排在前面.

      • ES 5 之前默认的相关性算分采用 TF-IDF
      • 后续的默认采用 BM 25

        ?

    TF-IDF

    TF-IDF 被公认是信息检索领域最重要的发明.

    • TF-IDF 的本质就是将 TF 求和变成 TF 加权求和.

    • 现代搜索引擎对 TF-IDF 进行了大量细微的优化.
    • 可以通过 Explain API 查看 TF-IDF

    词频 TF

    • Term Frequency: 检索词在一篇文档中出现的频率 = 检索词出现的次数 / 文档总词数
    • 度量一条查询和结果文档相关性的简单方法: 简单将搜索中的每一个词的 TF 进行相加.
    • Stop Word

      停止词(比如 "的") 在文档中通常会出现多次, 但是对于贡献相关度几乎没有用户, 不用考虑其 TF.

    逆文档频率 IDF

    • Inverse Document Frequency = log(全部文档数 / 检索词出现的文档总数)

      • DF: 检索词在所有文档中出现的频率

    BM 25

    image-20201102092944876

    Boosting Relevance

    Query & Filter 与多字段查询

    Query Context 与 Filter Context

    ES 中的搜索提供 Query 和 Filter 这两种不同的 Context:

    • Query Context: 相关性算分
    • Filter Context: 不需要算分(Yes or No), 可以利用 Cache, 获得更好的性能.

    bool 查询

    复合查询: bool Query

    • 可以是一个或多个查询子句的组合
    • 每个查询子句计算的评分会被合并到总的相关性评分中.
    • 共4种子句

      • Query Context: 贡献得分

        • must: 必须匹配.
        • should: 选择性匹配.
      • Filter Context: 不贡献得分

        • filter: 必须匹配
        • must_not: 必须不匹配.

    bool 查询语法

    • 子查询可以任意顺序出现
    • 可以嵌套多个查询
    • 示例语法

    • 同一层级下的竞争字段, 具有相同的权重. 通过嵌套 bool 查询, 可以改变对算分的影响.

    • should 和 must_not 的搭配可以实现 should_not 的逻辑

    Boosting 是控制相关度的一种手段

    • boost 参数的含义

      • boost > 1: 打分的相关度相对性提高
      • 0 < boost < 1: 打分的权重相对性降低
      • boost < 0: 贡献负分

    • boosting

      POST news/_search
      {
        "query": {
          "boosting": {
            "positive": {
              "match": {
                "content": "apple"
              }
            },
            // 此时 boost 为 "negative_boost" 的值, 即 0.1
            // 也就是搜索结果中包含 juice 的打分的权重相对性会大幅降低.
            "negative": {
              "match": {
                "content": "juice"
              }
            },
            "negative_boost": 0.1
          }
        }
      }
    比如使用 bool 复合查询时, 同一个查询在文章的 title 和 content 匹配到时的权重可以设置不同, 从而优化搜索结果的相关性.

    当搜索某些查询的同时要减少其他特定类型查询时, 也可以使用 boosting 来实现.

    单字符串多字段查询: dis_max

    对多个字段同时查询相同内容时, 有以下几种策略

    • 使用 bool 查询的 should

      POST blogs/_search
      {
        "query": {
          "bool": {
            "should": [
              {
                "match": {
                  "title": "Brown fox"
                }
              },
              {
                "match": {
                  "body": "Brown fox"
                }
              }
            ]
          }
        }
      }

      算分过程

      1. 查询 should 语句中的两个查询
      2. sum 两个查询的评分
      3. 乘以匹配语句的总数
      4. 除以所有语句的总数

    这种方式在有些情况下(查询内容在每个字段上都有, 都每个的相关度都不高)会使相关度较低的评分反而相对更高.

    • 使用 disjunction max query

      POST blogs/_search
      {
        "query": {
          "dis_max": {
            "tie_breaker": 0.2,
            "queries": [
              {
                "match": {
                  "title": "brown fox"
                }
              },
              {
                "match": {
                  "body": "brown fox"
                }
              }
            ]
          }
        }
      }

      算分过程

      1. title 和 body 字段互相竞争, 这里会取单个最佳匹配的字段的评分
      2. 将其他匹配字段的评分与 tie_breaker 相乘
      3. 对以上评分求和并规范化
    • 使用下面的 multi_match

    关于 Tier Break

    • 这是一个介于 0~1 之间的浮点数.
    • 0 代表使用最佳匹配
    • 1代表所有语句(匹配字段)都同等重要

    单符串多字段查询: multi_match

    Multi Match 是一种支持在多字段上进行查询的语法.

    https://www.elastic.co/guide/...

    • 共有三种使用场景

      • 最佳字段 (Best Fields)

        这是默认类型.

        当字段之间相互竞争, 又相互关联. 例如 title 和 body 这样的字段. 评分来自最匹配字段.

        POST blogs/_search
        {
            "query": {
                "multi_match": {
                    "type": "best_fields",        // 这是默认的 type, 可以省略不写
                    "query": "Quick pets",
                    "fields": ["title", "body"],
                    "tie_breaker": 0.2,
                    "minimum_should_match": "20%"    // minimum_should_match 等参数可以传递到生成的 query 中
                }
            }
        }
        与 DisMaxQuery 的主要区别在于, MultiMatch 是专门用于单字符串多字段查询, 但 DisMaxQuery 并不局域于此.
      • 多数字段 (Most Fields)

        • 处理英文内容时, 一种常见的手段是, 在主字段(Analyzer: english) 抽取词干, 加入同义词以匹配更多的文档. 同时加入子字段(analyer: standar)以提供更加精确的匹配. 其他字段作为匹配文档提高相关度的信号. 匹配字段越多则越好.
        • 不支持 operator ??
        PUT titles
        {
          "mappings": {
            "properties": {
              "title": {
                "type": "text",
                "analyzer": "english",    // english 分词器会将不同时态的词统一抽取词干, 以匹配更多的文档, 但同时也会导致精确度降低, 时态信息丢失.
                "fields": {
                  // 这里创建了一个子字段, 名为 "std", 使用 "standard" 分词器.
                  "std":{"type": "text", "analyzer": "standard"}
                }
              }
            }
          }
        }
        
        
        GET titles/_search
        {
          "query": {
            "multi_match": {
              "query": "barking dogs",
              "type": "most_fields",
              "fields": ["title^10", "title.std"]        // 同时匹配主字段和子字段, 同时这里设置 title 的 boost 为 10 来提高 title 字段的重要性.
             }
          }    
          }
    • 用广度匹配字段 title, 包括尽可能多的文档, 以提高召回率. 同时有使用字段 title.std 作为信号将相关度更高的文档置于结果顶部.

      • 每个字段对于最终评分的贡献可以通过自定义值 boost 来控制.
      • 跨字段搜索 (Cross Fields)

        • 对于某些实体, 例如人名, 地址, 图书信息. 需要在多个字段中确认信息, 单个字段只能作为整体的一部分. 希望在任何这些列出的字段中找到尽可能多的词.
        • 支持 operator
        • 无需使用 copy_to 即可实现跨字段搜索, 同时支持搜索的内容在所有字段中出现(通过设置 operator: "AND")

          copy_to 需要消耗额外的存储空间
        • 相比 copy_to, 它的另一个优势是可以在搜索时指定不同字段的权重.
        POST address/_search
        {
            "query":{
                "multi_match":{
                    "query": "Poland Street W1V",        // 其中 Poland 是在 "street" 字段, W1V 是在 "postcode" 字段
                    "type": "cross_fields",
                    "operator": "AND",
                    "fields": ["street", "city", "country", "postcode"]
                }
            }
        }

    多语言及中文分词与检索

    自然语言与查询 Recall

    提升自然语言查询的召回率(Recall) 常用优化策略

    • 归一化词元: 清除变音符号, 如 rǒle 时也会匹配 role
    • 抽取词根: 清除单复数和时态的差异

      比如增加一个使用 analyzer: english 的子字段.
    • 包含同义词
    • 拼写错误: 拼写错误, 或者同音异形词

    混合多语言的挑战

    多语言场景

    • 不同的索引使用不同的语言
    • 同一个索引中, 不同的字段使用不同的语言
    • 同一个字段内混合不同的语言

    混合语言常见的问题

    • 词干提取: 以色列文档包含了希伯来语、阿拉伯语、俄语、英语.
    • 不正确的文档频率: 比如在英文为主的文章中, 德文算分高(因为稀少)
    • 用户语言识别(Compact Language Detector): 需要判断用户搜索时的语言
    • 分词的挑战

      • 英文分词: You're 分成一个还是多个? Half-baked 是要分成两个词?
      • 中文分词

        • 分词标准: 标准不一样, 比如姓名是否要分开, 具体情况需制定不同的标准.
        • 歧义(组合型歧义, 交集型歧义, 真歧义)

    中文分词

    中文分词法的演变

    1. 字典法
    2. 最小词数的分词理论

      无法解决二义性问题
    3. 统计语言模型

      解决了二义性问题
    4. 基于统计的及其学习算法

      常用的算法是 HMM, CRF, SVM, 深度学习等算法.

      以及基于神经网络的分词器等.

    中文分词器以统计语言模型为基础, 到今天基本可以看作是已经解决的问题.

    • 不同分词器的好坏, 主要差别在于数据的使用和工程使用的精度.
    • 常见的分词器都是使用机器学习算法和词典相结合

      机器学习: 提高分词准确率

      词典: 改善领域适应性

    HanLP

    一个面向生产环境的自然语言处理工具包

    同样提供 ES 版本

    http://hanlp.com

    https://github.com/KennFalcon...

    IK 分词器

    https://github.com/medcl/elas...

    Pinyin Analysis

    https://github.com/medcl/elas...

    Space Jam, 一次全文搜索的实例

    TMDB 数据库

    • 创建于 2008 年, 电影的 Meta Data 库.
    • 提供 API, 总共有超过20万开发人员和公司在使用

    测试的一般步骤(使用脚本处理)

    1. 选择合适的 mapping

      • 分词器
      • 多字段属性
    2. reindex 数据(delete, index)
    3. 选择合适的查询(可通过高亮来显示结果, 方便比较查询效果)

      • 同义词
      • 为字段设置不同的权重
    4. 查询并高亮显示结果
    5. 分析搜索结果, 重复步骤 1

    测试相关性: 理解原理 + 多分析 + 多调整测试.

    注意不要过度调试相关度, 而是要监控搜索结果, 监控用户点击最顶端结果的频次.

    将搜索结果提高到极高水平的唯一途径:

    • 需要具有度量用户行为的强大能力
    • 可以在后台实现统计数据, 比如用户的查询和结果, 有多少被点击了
    • 哪些搜索没有返回结果

    使用 Search Template 和 Index Alias 查询

    Search Template

    Search Template 的主要目的是为了解耦程序和搜索 DSL

    各司其职, 解耦开发人员, 搜索工程师, 性能工程师.

    https://www.elastic.co/guide/...

    在开发初期, 虽然可以明确查询参数, 但往往还不能确定查询的DSL的最终版本, 此时可以通过创建 Search Template.

    开发人员直接使用 Search Template, 传入参数. 若后续 DSL 语句需要优化/变化(不涉及传入参数), 则搜索工程师/性能工程师只需要直接修改 Search Template 即可, 无需修改程序代码.

    示例

    // 创建一个 search template
    POST _scripts/<templateid>
    {
      "script": {
        "lang": "mustache",
        "source": {
          "query": {
            "match": {
              "title": "{{query_string}}"
            }
          }
        }
      }
    }
    
    
    // 获取一个 search template
    GET _scripts/<templateid>
    
    
    // 删除一个 search tempalte
    DELETE _scripts/<templateid>
    
    
    // 使用一个 search template
    GET 索引名/_search/template
    {
        "id": "<templateid>",
        "params": {
            "query_string": "search for these words"
        }
    }
    
    
    
    // 或者是不创建 search template, 而是直接当场使用
    GET _search/template
    {
      "source" : {
        "query": { "match" : { "{{my_field}}" : "{{my_value}}" } },
        "size" : "{{my_size}}"
      },
      "params" : {
        "my_field" : "message",
        "my_value" : "foo",
        "my_size" : 5
      }
    }

    Index Alias

    Index Alias 主要目的是: 实现零停机的运维.

    程序使用 ES 搜索时, 可以指定固定的索引别名, 而无需关心其真实的索引名.

    甚至可以为索引别名指定额外条件:

    image-20200915195552095

    使用索引别名的大致步骤

    1. 为索引定 一个别名

    2. 通过索引别名读写数据

    写入实践序列的数据时利用 Index Alias

    也可以使用如下方式:

    PUT /索引名
    {
        "aliases": {
            "索引别名": {
    //            "is_write_index": true
                .....
            }
        }
    }
    
    // 或这种方式简单创建 index alias
    PUT /索引名/_alias/索引别名

    索引别名参数说明

    • is_write_index: 当别名指向多个索引时, 需配置写到其中哪一个具体索引上(只能指向 1 个索引).

      默认无需手动配置.

      常用的使用场景是在 rollover 时, 别名指向了这一系列所有索引, 但只对最新创建的索引设置 is_write_index: true. 当进行 rollover 操作(此时会创建新索引), 再将 is_write_index 设置到新索引上, 并移除老的索引上的该配置.

    综合排序: Function Score Query 优化算分

    ES 默认会按照文档相关度算分, 但是仅使用默认的 score 得分排序无法满足某些特定条件.

    Function Score Query

    Function Score Query: 可以在查询结束后, 对每一个匹配的文档进行一系列重新算分, 并根据新生成的分数进行排序.

    • 计算分值的函数

      • Weight: 为每一个文档设置一个简单而不被规范化的权重
      • Field Value Factor: 使用该数值来修改 _score, 例如将"热度"和"点赞数"作为算分的参考因素

        • modifier: 平滑曲线

        • factor

    $$
    新的得分 = 原始得分 * 平滑函数(字段值 * factor)
    $$
    
    • Random Score: 为每一个用户使用一个不同的随机算分结果
    • 衰减函数: 以某个字段的值为标准, 距离某个值越近, 得分越高
    • Scirpt Score: 自定义脚本完全控制所需逻辑

    Field Value Factor

    按受欢迎度提升权重

    Boost Mode 和 Max Boost

    Boost Mode: 选择合适的最终得分策略

    • Multiply: 原始分数与函数值的乘积
    • Sum: 原始分数与函数值的和
    • Min / Max: 原始分数与函数值取 最小 / 最大值
    • Replace: 直接使用函数值取代原始分数

    Max Boost: 可以将函数值控制在一个最大值, 避免影响过大.

    POST blogs/_search
    {
      "query": {
        "function_score": {
          "query": {
            "multi_match": {
              "query": "popularity",
              "fields": ["title", "content"]
            }
          },
         "field_value_factor": {
           "field": "votes",
           "modifier": "log2p",
           "factor": 0.1
         },
         "boost_mode": "sum",            // 指定 Boost Mode: Sum
         "max_boost": 3                    // 指定 Max Boost: 3
        }
      }
    }

    一致性随机函数

    使用场景: 网站的广告需要提高展现率.

    具体需求

    • 让每个用户能看到不同的随机排名, 但是也希望同一个用户访问时, 结果的相对顺序保持一致(Consistently Random)

    Suggest 搜索

    下列不同 Suggest 搜索的比较.

    • 精准度 (Precision)

      1. Completion
      2. Phrase
      3. Term
    • 召回率 (Recall)

      1. Term
      2. Phrase
      3. Completion
    • 性能 (Performance)

      1. Completion
      2. Phrase
      3. Term

    Term & Phrase Suggester

    搜索建议

    • 现代的搜索引擎一般都会提供 Suggest as you type 的功能

    • 可以帮助用户在搜索时自动补全或纠错
    • 在 google 搜索时, 一开始会自动补全. 当输入到一定长度, 如因为单词拼写错误无法补全, 就会开始提示相似的词或者句子.

    ES Suggester API

    • Suggester 是一种特殊类型的搜索, 并且它需要使用特定的数据类型来支撑 Suggest 搜索.

      PUT 索引名
      {
        "mappings": {
          "properties": {
            "字段名":  {
              "type": "completion"
            }
          }
        }
      }
    • 原理: 将输入的文本分解为 Token, 然后在索引的字典里查找相似的 Term 并返回.
    • ES 设计了 4 种类别的 Suggesters 供不同场景使用

      • Term & Phrase Suggester
      • Complete & Context Suggester

    Term Suggester

    每个建议都包含一个算分, 相似性是通过 Levenshtein Edit Distance 算法实现的.

    其核心思想就是一个词改动多少字符就可以和另外一个词一致.

    可通过不同的可选参数来控制相似性的模糊程度, 例如 "max_edits"

    Suggestion Mode

    • Missing: 如索引中已经存在 term, 就不提供建议
    • Popular: 推荐出现频率更加高的词
    • Always: 无论是否存在, 都提供建议

    • body 字段是 completion 数据类型.

    Phrase Suggester

    Phrase Suggester 在 Term Suggester 上增加了一些额外的逻辑.

    一些参数

    • Suggest Mode: missing, popular, always
    • Max Errors: 最多可以拼错的 Terms 数
    • Confidence: 限制返回结果数, 默认为 1

    image-20200916102438276

    自动补全与基于上下文的提示

    Completion Suggester

    Completion Suggester 提供了"自动完成"(Auto Complete)的功能. 用户每输入一个字符, 就需要即时发送一个查询请求到后端查找匹配项.

    • 并非通过倒排索引来完成, 而是采用了专门的数据结构: 将 Analyze 的数据编码成 FST 和索引一起存放.
    • FST会被ES整个加载到内存, 速度很快.
    • FST 只能用于前缀查找

    定义 Mapping

    PUT 索引名
    {
      "mappings": {
        "properties": {
          "属性名":  {
            "type": "completion"
          }
        }
      }
    }
    • type 是 completion
    • 通过 suggest 查询可以得到搜索建议

    suggest 查询

    POST 索引名/_search
    {
      "suggest": {
        "此次返回的suggest名": {
          "prefix": "前缀匹配内容",
          "completion": {
            "field": "前面定义的属性名"
          }
        }
      }
    }

    Context Suggester

    Context Suggester 是对 Completion Suggester 的扩展.

    • 可以理解为是一种过滤, 在写入数据时定义数据的上下文.
    • 在进行 Context Suggest 搜索时可以加入不同的上下文信息.

    ES 可以定义两种类型的 Context

    • Category: 任意的字符串
    • Geo: 地理位置信息

    实现 Context Suggester 的具体步骤

    1. 定制一个 Mappings
    2. 索引数据, 并且为每个文档加入 Context 信息
    3. 结合 Context 进行 Suggestion 查询
    // 1. 定制一个 Mappings
    PUT comments
    {
      "mappings": {
        "properties": {
          "comment_autocomplete": {
            "type": "completion",
            "contexts": [
              {"type": "category", "name": "comment_category"}
            ]
          }
        }
      }
    }
    
    // 2. 索引数据, 并且为每个文档加入 Context 信息
    POST comments/_doc
    {
      "comment": "I love the star war movies",
      "comment_autocomplete": {
        "input": ["star wars"],
        "contexts": {
          "comment_category": "movies"
        }
      }
    }
    
    POST comments/_doc
    {
      "comment": "Where can i find a starbucks",
      "comment_autocomplete": {
        "input": ["starbucks"],
        "contexts": {
          "comment_category": "coffee"
        }
      }
    }
    
    
    // 3. 结合 Context 进行 Suggestion 查询
    POST comments/_search
    {
      "suggest": {
        "YOUR_SUGGESTION": {
          "prefix": "sta",
          "completion": {
            "field": "comment_autocomplete",
            "contexts": {
              "comment_category": "coffee"
            }
          }
        }
      }
    }

    配置跨集群搜索

    为什么需要跨集群搜索

    单集群存在的问题

    • 当水平扩展时, 节点数不能无限增加

      当集群的 meta 信息(节点, 索引, 集群状态)过多, 会导致更新压力变大, 单个 Active Master 会成为性能瓶颈, 导致整个集群无法正常工作.

    多集群方案

    • 早期 Tribe Node 方案存在一定问题, 现已被 Deprecated
    • ElasticSearch 5.3 引入了跨集群搜索的功能(Cross Cluster Search), 推荐使用

      • 允许任何节点扮演 federated 节点, 以轻量的方式, 将搜索请求进行代理.
      • 不需要以 Client Node 的形式加入其它集群

    测试Demo

    步骤一: 本机启动 3 个集群

    elasticsearch -E node.name=cluster0node -E cluster.name=cluster0 -E path.data=cluster0_data -E discovery.type=single-node -E http.port=9200 -E transport.port=9300
    elasticsearch -E node.name=cluster1node -E cluster.name=cluster1 -E path.data=cluster1_data -E discovery.type=single-node -E http.port=9201 -E transport.port=9301
    elasticsearch -E node.name=cluster2node -E cluster.name=cluster2 -E path.data=cluster2_data -E discovery.type=single-node -E http.port=9202 -E transport.port=9302
    注意这是3个集群, 每个集群各自只配了一个节点

    步骤二: 在每个集群上设置动态的设置

    curl --location --request PUT 'http://127.0.0.1:9200/_cluster/settings' \
    --header 'Content-Type: application/json' \
    --data-raw '{
        "persistent": {
            "cluster": {
                "remote": {
                    "cluster0": {
                        "seeds": [
                            "127.0.0.1:9300"
                        ],
                        "transport.ping_schedule": "30s"
                    },
                    "cluster1": {
                        "seeds": [
                            "127.0.0.1:9301"
                        ],
                        "transport.ping_schedule": "30s"
                    },
                    "cluster2": {
                        "seeds": [
                            "127.0.0.1:9302"
                        ],
                        "transport.ping_schedule": "30s"
                    }
                }
            }
        }
    }'
    
    curl --location --request PUT 'http://127.0.0.1:9201/_cluster/settings' \
    --header 'Content-Type: application/json' \
    --data-raw '{
        "persistent": {
            "cluster": {
                "remote": {
                    "cluster0": {
                        "seeds": [
                            "127.0.0.1:9300"
                        ],
                        "transport.ping_schedule": "30s"
                    },
                    "cluster1": {
                        "seeds": [
                            "127.0.0.1:9301"
                        ],
                        "transport.ping_schedule": "30s"
                    },
                    "cluster2": {
                        "seeds": [
                            "127.0.0.1:9302"
                        ],
                        "transport.ping_schedule": "30s"
                    }
                }
            }
        }
    }'
    
    curl --location --request PUT 'http://127.0.0.1:9202/_cluster/settings' \
    --header 'Content-Type: application/json' \
    --data-raw '{
        "persistent": {
            "cluster": {
                "remote": {
                    "cluster0": {
                        "seeds": [
                            "127.0.0.1:9300"
                        ],
                        "transport.ping_schedule": "30s"
                    },
                    "cluster1": {
                        "seeds": [
                            "127.0.0.1:9301"
                        ],
                        "transport.ping_schedule": "30s"
                    },
                    "cluster2": {
                        "seeds": [
                            "127.0.0.1:9302"
                        ],
                        "transport.ping_schedule": "30s"
                    }
                }
            }
        }
    }'

    步骤三: 分别向每个集群写入数据

    curl --location --request POST 'http://127.0.0.1:9200/users/_doc' \
    --header 'Content-Type: application/json' \
    --data-raw '{
        "name": "user1",
        "age": 10
    }'
    
    curl --location --request POST 'http://127.0.0.1:9201/users/_doc' \
    --header 'Content-Type: application/json' \
    --data-raw '{
        "name": "user2",
        "age": 20
    }'
    
    curl --location --request POST 'http://127.0.0.1:9202/users/_doc' \
    --header 'Content-Type: application/json' \
    --data-raw '{
        "name": "user3",
        "age": 30
    }'

    步骤四: 跨集群搜索

    curl --location --request POST 'http://127.0.0.1:9200/users,cluster1:users,cluster2:users/_search' \
    --header 'Content-Type: application/json' \
    --data-raw '{
      "query": {
        "range": {
          "age": {
            "gte": 10,
            "lte": 30
          }
        }
      }
    }'

    分布式特性及分布式搜索的机制

    集群分布式模型及选主与闹裂问题

    分布式架构

    ES 的分布式

    • 不同的集群使用不同的名字来区分, 默认名字是 "elasticsearch"
    • 通过配置文件 elasticsearch.yml 修改 cluster.name , 或者在命令行中 -E cluster.name=xxxx 来指定

    ES 的分布式架构的好处

    • 存储的水平扩容, 支持 PB 级数据
    • 提高系统的可用性, 部分节点停止服务, 整个集群的服务不受影响

    Node

    节点

    • 节点是一个 ES 的实例

      • 本质是一个 JAVA 进程
      • 一个机器可以运行多个 ES 进程, 但生产环境一般建议一台机器上只运行一个 ES 实例.
    • 每个节点都有名字, 通过配置文件配置, 或者在启动时通过 -E node.name=xxxx 指定
    • 每个节点在启动后, 会自动分配一个 UID, 保存在 data 目录下.
    # 示例
    bin/elasticsearch -E node.name=node1 -E cluster.name=geektime -E path.data=node1_data -E http.port=9200

    配置节点类型

    Coordinating Node

    Coordinating Node: 处理客户端请求的节点.

    • 职责

      • 接收客户端请求, 并将其路由到正确的节点

        如创建索引的请求只有 Master 节点才能处理, 因此会自动将请求路由到 Master 节点.
    • 所有节点默认都是 Coordinating Node
    • 通过将其他类型设置成 False, 可以使自身成为 Delicated Coordinating Node

    Data Node

    Data Node: 可以保存数据的节点

    • 节点启动后, 默认就是数据节点. 可以通过设置 node.data: false 来禁止
    • 职责

      • 保存分片数据. 在数据扩展上起到了至关重要的作用 (由 Master Node 决定如何把分片分发到数据节点)
    • 通过增加数据节点可以解决 数据水平扩展数据单点 的问题

    Master Node

    Master Node: 每个集群只有一个主节点

    • 职责

      • 维护索引, 维护分片
      • 维护并更新 Cluster State
    • 最佳实践

      • Master 节点非常重要, 在部署上需要考虑单点问题
      • 可以为一个集群设置多个 Master 节点/ 每个节点只承担 Master 的单一角色

    Master Eligible Nodes

    Master Eligible Nodes: 候选节点, 指有资格通过选举成为 Master 的节点.

    • 一个集群支持配置多个 Master Eligible 节点.
    • Master Eligible 节点在必要时(如 Master 节点出现故障、 网络故障时)参与选主流程, 成为 Master 节点.
    • 每个节点启动后, 默认就是一个 Master Eligible 节点. 可以通过设置 node.master: false 来禁止
    • 当集群内第一个 Master Eligible 节点启动后, 它会选举自己成为 Master 节点.

    集群状态信息

    集群状态信息 (Cluster State) 维护了一个集群中必要的信息

    • 所有的节点信息
    • 所有的索引和其相关的 Mapping 与 Setting 信息
    • 分片的路由信息

    每个节点上都保存了集群的状态信息, 但只有 Master 节点才能修改集群的状态信息, 并负责同步给其他节点.

    为什么只有 Master 节点可以修改: 因为任意节点都能修改的话会导致 Cluster State 信息的不一致.

    Master Node 选举过程与脑裂问题

    选举

    • 若集群不存在主节点或被选中的主节点丢失, 就是开始选举 Master 节点的流程.
    • 集群中的 Master Eligible Node 会互相 Ping 对方, Node Id 低的会成为被选举的 Master 节点.
    • 其他节点会加入集群, 但是不承担 Master 节点的角色.

    Split-Brain 脑裂问题: 分布式网络的经典问题, 当出现网络问题, 一个节点和其他节点无法连接时, 各个被分割的网络区域内的节点会自行选举出一个 Master 节点, 导致在同一时刻集群存在多个 Master 节点, 各自维护不同的 cluster state. 当网络恢复时无法正确恢复集群状态.

    避免闹裂问题

    • 限定一个选举条件, 设置quorum(仲裁), 只有在 Master Eligible 节点数大于 quorum 时才能进行选举.

      • Quorum = 超过半数

        当 3 个 master Eligible 时, 设置 discovery.zen.minimum_master_nodes 为 2 即可避免脑裂
    • 从 7.0 开始无需上述配置

      • 移除 minimum_master_nodes 参数, ES 自己选择可以形成仲裁的节点.
      • 典型的主节点选举现在只需要很短的时间就可以完成. 集群的伸缩变得更安全, 更容易, 并且可能造成丢失数据的系统配置选项更少了.
      • 节点更清楚地记录它们的状态, 有助于诊断为什么它们不能加入集群或为什么无法选举出主节点.

    分片与集群的故障转移

    分片

    分片是 ES 分布式存储的基石.

    • 分片的类型

      • Primary Shard 主分片
      • Replica Shard 副本分片

        1个索引 -> N 个主分片

        1个主分片 -> M 个副本分片

    • Primary Shard

      • 通过主分片, 可以将一份索引的数据分散到多个 Data Node 上, 实现存储的水平扩展.
      • 主分片数在索引创建时指定, 后续默认不能修改, 除非重建索引.
    • Replica Shard

      • 数据可用性.

        • 通过引入副本分片, 可以提高数据的可用性.
        • 当主分片丢失时, 副本分片可以 Promote 成为主分片.
        • 副本分片数可以动态调整, 每个节点上都有完备的数据(指该副本分片对应的主分片的数据).
        • 若不设置副本分片, 一旦出现节点硬件故障, 就有可能造成数据丢失.
      • 提升系统的读取性能

        • 副本分片由主分片同步, 通过支持增加 Replica 个数, 一定程度可以提高读取的吞吐量.
    • 分片数的设定

      • 主分片数过小时: 若索引增长很快, 集群将无法通过增加节点来实现对这个索引的数据扩展.
      • 主分片数过大时: 导致每个分片的容量很小, 同时每个节点上存在过多分片是一定程度上影响性能的.
      • 副本节点数设置过多: 会降低集群整体的写入性能.

    设置主分片数及副本分片数

    PUT 索引名
    {
        "settings": {
            "number_of_shards": 3,        // 3 个主分片
            "number_of_replicas": 1        // 每个主分片对应一个副本分片
        }
    }
    上述配置后, 该索引共有 3 个主分片, 3 个副本分片.

    集群故障转移

    集群健康状态

    • 绿色 Green: 健康状态, 所有主分片和副本分片都可用.
    • 黄色 Yellow: 亚健康, 所有主分片可用, 部分副本分片不可用.
    • 红色 Red: 不健康状态, 部分主分片不可用.
    GET _cluster/health

    集群是具有故障转移能力的

    • Master 节点会决定分片分配到哪个节点上
    • 通过增加节点, 提高集群的计算能力.
    • 故障转移期间, 集群健康状态会持续一小段时间的黄色. 待分片分配完毕后, 会恢复为绿色.

    文档分布式存储

    文档与分片

    • 文档会存储在具体的某个 Primary 分片和对应的 Replica 分片上.

      例如文档 1 可能存储在 P0 和 R0 分片上
    • 文档到分片的映射算法

      • 文档应尽量均匀分布在所有分片上, 充分利用硬件资源, 避免热点分片
      • 潜在的算法

        • 随机 / Round Robin: 分片数多时需多次查询才能获取到, 不靠谱.
        • 查表法
        • 实时计算(比如 hash)

    ES 采取的文档到分片的路由算法

    • shard = hash(_routing) % number_of_primary_shards

      • Hash 算法确保文档均匀分散到所有分片
      • 默认的 _routing 值是文档 id
      • 可以自行制定 routing 数值

        // 比如相同国家的商品都分配到指定的 shard
        PUT goods/_doc/100?routing=country
        {
            "country": "...",
            ...
        }
      • 缺点: primary 分片数一旦确定后, 不能随意修改

    更新/删除操作

    分片及其生命周期

    分片的内部原理

    ES 的分片

    • 分片是 ES 中最小的工作单元
    • ES 中一个分片的本质就是一个 Lucene 的 Index

    了解分片的原理就能够理解 ES 的一些行为:

    • ES 的搜索是近实时的(写入1秒后才能被搜到)
    • ES 能够保证断电时数据不会丢失
    • 删除文档并不会立刻释放空间

    倒排索引不可变性

    倒排索引采用 Immutable Design, 一旦生成, 不可更改.

    不可变性带来的好处

    • 无需考虑并发写文件的问题, 避免锁机制带来的性能问题
    • 一旦读入内核的文件系统缓存, 便留在那里. 只要文件系统存有足够的空间, 大部分请求就会直接命中内存, 不会命中磁盘, 提升了很大的性能.
    • 缓存容易生成和维护, 系统可以充分利用缓存. 倒排索引允许数据被压缩.

    不可变性带来的问题

    • 如果需要让一个新的文档可以被搜索, 需要重建整个索引

    Lucene Index

    Segment

    在 Lucene, 单个倒排索引文件称为 Segment, Segment 是自包含, 不可变更的.

    多个 Segments 汇总在一起, 称为 Lucene 的 Index, 其对应 ES 中的 Shard.

    • 当有文档写入时, 会产生新 Segment.
    • 查询时会同时查询所有 Segments, 并且对结果汇总.

      Lucene 中有一个文件, 用来记录所有 Segments 信息, 叫做 Commit Point.

    • 删除的文档信息保存在 ".del" 文件中

    Refresh 过程

    Refresh 指将 Index Buffer 写入 Segment 的过程.

    数据在 Index Buffer 中是无法被搜索的, 只有 Segment 才会被搜索到.

    • Refresh 不执行 fsync 操作
    • Refresh 频率: 默认 1 秒发生一次, 可通过 index.refresh_interval 配置.
    • 当 Index Buffer(默认是 JVM 的 10%) 被写满时, 也会触发Refresh
    • 如果系统有大量的数据写入, 那就回产生很多 Segment

    Segment 写入磁盘的过程(指 Refresh 过程) 相对耗时, 借助文件系统缓存, Refresh 时先将 Segment 写入缓存以开放查询.

    Transaction Log

    Index Buffer 中的数据是在内存, 因此为了保证断电/程序崩溃时数据不丢失, 在 Index Document 写入 Index Buffer 的同时会写 Transaction Log. 在高版本中, Transaction Log 默认落盘.

    • 每个分片有一个 Transaction Log
    • 在 ES Refresh 时, Index Buffer 被清空, 但 Transaction Log 不会清空.

    Flush 过程

    ES Flush & Lucence Commit

    1. 调用 Refresh, 清空 Index Buffer
    2. 调用 fsync 将文件系统缓存中的 Segments 写入磁盘
    3. 清空(删除) Transaction Log

    Flush 的时机

    • 默认每 30 分钟调用一次
    • Transaction Log 满(默认 512 MB)

    image-20200917151304068

    Merge 过程

    Merge 过程: 由于每次 Refresh 都会生成一个 Segment, 会导致 Segment 很多, 因此需要定期合并.

    • 减少 Segments
    • 删除已经被标记删除的文档

    ES 和 Lucence 会自动进行 Merge 操作, 也可以通过 POST 索引名/_forcemerge 强制进行 Merge 操作.

    剖析分布式查询及相关性算分

    带着疑问: 为什么 ES 默认的主分片数设置为 1.

    ES 的分布式搜索是分两个阶段进行的

    1. Query 阶段

      用户发出请求到 ES 节点, 节点收到请求后会以 Coordinating 节点的身份在 6 个分片中随机选取 3个分片, 发送查询请求.

      被选中的分片执行查询时, 会进行排序, 然后每个分片都返回 from + size 个排序后的文档的 id 和排序值给 Coordinating 节点.

    2. Fetch 阶段

      Coordinating 在 Query 阶段共收到 3 * (from + size) 个文档 id 及其排序值后, 会重新进行排序, 并选取 from ~ (from + size) 个文档 的 id.

      之后再以 multi get 请求的方式到各自的分片获取详细的文档数量.

    这里以索引设置为 3个主分片, 2个副本分片为例.

    上述的两阶段称为 Query-then-Fetch, 会存在两个问题:

    问题一: 性能问题

    • 每个分片需要查的文档个数= from + size
    • coordinating(协调) 节点需要处理: number_of_shards * (from + size) 文档项
    • 在深度分页时这个数据量是非常大的.

    问题二: 评分问题

    • 每个分片在 Query 阶段评分时是根据各自分片内的文档数据进行相关度计算的, 这会导致打分偏离的情况.
    • 相关性算分在各个分片间是独立进行的, 当文档总数很少时, 且主分片数多时, 相关性算分会很不准确.

    解决评分不准的方法

    • 当数据量不大时: 将主分片数设置为 1

      当数据量足够多时, 只要保证文档均匀分布在各个分片上, 那么

    • 使用 DFS Query Then Fetch

      在搜索的 URL 中指定参数 _search?search_type=dfs_query_then_fetch

      到每个分片把各分片的词频和文档频率进行搜集, 然后完整的进行一次相关性算分, 耗费更加多的 CPU 和内存, 执行性能低下, 一般不建议使用.

    数据量少且主分片多的情况下打分偏差, 及使用 DFS Query Then Fetch 获取正确打分的例子.

    DELETE message
    
    PUT message
    {
      "settings": {
        "number_of_shards": 20
      }
    }
    
    POST message/_bulk
    {"create":{}}
    {"content": "good"}
    {"create":{}}
    {"content": "good morning"}
    {"create":{}}
    {"content": "good morning everyone"}
    
    // 此时查询结果评分是有问题的, 所有文档的评分都是 0.2876821
    POST message/_search
    {
      "query": {
        "term": {
          "content": {
            "value": "good"
          }
        }
      }
      //,"explain": true
    }
    
    
    // 使用 DFS Query Then Fetch 正确获取打分
    // 但这种方式性能很不好, 通常是不被推荐的
    POST message/_search?search_type=dfs_query_then_fetch
    {
      "query": {
        "term": {
          "content": {
            "value": "good"
          }
        }
      }
    }

    排序及Doc Values & Fielddata

    排序

    ES 默认采用相关性算分对结果进行降序排序

    • 可以通过设定 sort 参数自行设定排序

      • 若不指定 _score, 则算分为 null

        image-20201102092904596

    排序的过程

    • 排序是针对字段原始内容进行的.
    • 倒排索引无法发挥作用, 需要用到正排索引, 通过文档 Id 和字段快速得到字段原始内容.
    • ES 有两种实现方式

      • Fielddata

        针对 text 字段

        默认是关闭的, 可通过修改 Mapping 中字段的 fielddata: true 来开启, 但必须了解到, 这边存储的是分词后的结果.

        若这不是想要的效果, 那可以为该 text 字段加一个 keyword 子字段, 然后直接使用该子字段.

    PUT 索引名/_mapping

    {
      "properties": {
            "属性名": {
                "type": "text",            // 这里以 text 类型为例
                "fielddata": true        // keyword 类型的默认开启了该属性, 因此无需特地设置.
            }
        }
    }
    ```
    
    
    
    • Doc Values

      列式存储, 对 text 类型无效

      默认是开启的, 可通过修改 Mapping 中字段的 doc_values: false 来关闭

      • 关闭 Doc Values 的好处:

        • 增加索引的速度
        • 减少磁盘空间
      • 什么情况需要关闭:

        • 当明确不需要该字段参与排序及聚合分析时.

    关于 Doc ValuesField Data

    image-20200930184405588

    排序部分示例

    // 单字段
    POST kibana_sample_data_ecommerce/_search
    {
      "size": 5,
      "query": {
        "match_all": {}
      },
      "sort": [
        {
          "order_date": {
            "order": "desc"
          }
        }
      ]
    }
    
    
    
    // 多字段
    POST kibana_sample_data_ecommerce/_search
    {
      "size": 5,
      "query": {
        "match_all": {}
      },
      "sort": [
        {
          "order_date": {
            "order": "desc"
          }
        },
        {
          "_doc": {
            "order": "asc"
          }
        },
        {
          "_score": {
            "order": "desc"
          }
        }
      ]
    }

    打开 Field Data (默认是未开启的, 此时无法对text类型排序)

    // 打开  text 的 fielddata
    PUT kibana_sample_data_ecommerce/_mapping
    {
      "properties": {
        "customer_full_name": {
          "type": "text",
          "fielddata": true,
          "fields": {
            "keyword":{
              "type":"keyword",
              "ignore_above":256
            }
          }
        }
      }
    }
    
    POST kibana_sample_data_ecommerce/_search
    {
      "size": 5,
      "query": {
        "match_all": {}
      },
      "sort": [
        {
          "customer_full_name": {
            "order": "desc"
          }
        }
      ]
    }

    分页与遍历: From, Size, Search After & Scroll API

    常用的分页方式

    post users/_search
    {
      "from":100,
      "size":100,
      "query":{
        "match_all": {}
      }
    }

    默认情况使用相关度算分排序, 返回前10条记录.

    • from: 开始的位置
    • size: 期望获取的文档的的数量

    但是这种方式存在如下问题: 深度分页问题.

    分布式系统中深度分页的问题

    ES是分布式的, 其数据分别保存在多个分片, 多台机器上.

    当一个查询: From=990, Size=10

    1. 首先在每个分片上都获取 1000 个文档(默认是相关度评分前1000).
    2. 然后通过 Coordinating Node 聚合所有结果
    3. 最后再通过排序选取前 1000 个文档.

    上述操作存在的明显问题就是, 当页数越深, 占用的内存会越多.

    为了避免深度分页带来的内存开销, ES 有一个默认限定: 10000 个文档.

    post users/_search
    {
      "from":10000,
      "size":100,
      "query":{
        "match_all": {}
      }
    }
    
    // 此时会报错: Result window is too large, from + size must be less than or equal to: [10000] but was [10100].

    ES 提供了 Search After 来解决深度分页问题.

    Search After

    Search After 避免了深度分页带来的性能问题, 但是其本身有着如下的限制

    • 不支持指定页数 (From)
    • 只能往下翻

    使用:

    1. 第一次搜索需要指定 sort, 并且保证值是唯一的

      可以通过加入 _id 来保证唯一性
      post users/_search
      {
        "size": 100,
        "query": {
          "match_all": {}
        },
        "sort": [
          {"age":"desc"},
          {"_id":"asc"}
        ]
      }
    2. 每次搜索都会返回 sort, 后续的搜索需要使用上一次返回的 sort 以往下查询.

      post users/_search
      {
        "size": 2,
        "query": {
          "match_all": {}
        },
        "sort": [
          {"age":"desc"},
          {"_id":"asc"}
        ],
        "search_after": [
            2,                            // age
            "pq32RXUBu83_vZFOE_49"        // _id
          ]
      }

    Search After 解决深度分页的原理

    1. 每个分片通过唯一排序值定位, 仅返回需要处理的 size 个文档数
    2. Coordinating Node 聚合所有结果后排序并返回前 size 个

    Size=10, 当查询 990~1000 时如上图所示, 各个分片仅返回符合条件的前 10 个文档(根据唯一排序值过滤)

    Scroll Api

    Scroll Api 也是用于解决深度分页的问题, 不过其原理不一样.

    适用于大量导出数据.

    原理

    1. 第一次查询时创建一个快照
    2. 后续的每次查询都输入上一次的 Scroll Id

    存在的限制

    • 在快照生成后, 新写入的数据无法被查到
    // 第1次查询
    POST users/_search?scroll=5m
    {
      "size":1,
      "query": {
        "match_all": {}
      }
    }
    
    // 第2次查询
    POST _search/scroll
    {
      "scroll": "1m",
      "scroll_id": "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFEpxSVdSblVCbzV0OHBaRVFXU3pnAAAAAAAAAsQWblF6bktTeDZSN0tnbzRkRnE0SWJSQQ=="
    }
    关于 scroll 参数, 暂时还不大理解什么意思.

    不同的搜索类型及其使用场景

    • Regular

      需要实时获取顶部的部分文档(例如查询最新的订单)

      此时直接使用 "size" 即可

    • Scroll

      当需要全部文档(例如导出全部数据)时可以使用 Scroll Api

    • Pagination

      当需要分页时, 可以使用 From 和 Size.

      但如果需要深度分页, 则选用 Search After

    处理并发读写操作

    并发控制一般来说有两种

    • 悲观并发控制

      通过对资源加锁, 防止冲突

    • 乐观并发控制

      假定冲突不会发生, 不会阻塞正在尝试的操作.

      ES 采用的是乐观并发控制.

    ES 中的文档是不可变更的, 如果你更新了一个文档:

    1. 将旧文档标记删除
    2. 增加一个新文档, 并将其 version 字段加 1

    ES 的版本控制分为

    1. 内部版本控制

      if_seq_no + if_primary_term

    2. 外部版本控制(即由其他数据库作为主要数据存储, 并控制数据的版本)

      version + version_type = external

    示例 - 内部版本控制

    1. 初始化索引

    DELETE users
    
    
    PUT users/_doc/1
    {
      "name": "jason",
      "age": 18
    }

    2. 获取文档初始版本

    // 获取 _id=1 的文档
    GET users/_doc/1
    
    
    // 返回结果:
    {
      "_index" : "users",
      "_type" : "_doc",
      "_id" : "1",
      "_version" : 1,
      "_seq_no" : 0,
      "_primary_term" : 1,
      "found" : true,
      "_source" : {
        "name" : "jason",
        "age" : 18
      }
    }

    3. 第一次更新

    // 第1次更新
    PUT users/_doc/1?if_seq_no=0&if_primary_term=1
    {
      "title": "jason",
      "age": 19
    }
    
    
    // 返回值
    {
      "_index" : "users",
      "_type" : "_doc",
      "_id" : "1",
      "_version" : 2,
      "result" : "updated",
      "_shards" : {
        "total" : 2,
        "successful" : 2,
        "failed" : 0
      },
      "_seq_no" : 1,
      "_primary_term" : 1
    }

    4. 第二次更新

    // version_conflict_engine_exception
    PUT users/_doc/1?if_seq_no=1&if_primary_term=1
    {
      "title": "jason",
      "age": 20
    }

    此时会报版本冲突错误

    示例 - 外部版本控制

    PUT users/_doc/1?version=100&version_type=external
    {
      "title": "jason",
      "age": 20
    }

    每次更新, version 只能往大了加, 否则会提示版本冲突错误.

    深入聚合分析 Aggregation

    聚合类型

    聚合主要有以下几种

    • Bucket Aggregation 桶聚合

      分桶聚合

    • Metric Aggregation 度量聚合

      数学计算

    • Pipeline Aggregation 管道聚合

      对其他聚合结果进行二次聚合

    • Matrix Aggregation 矩阵聚合

      对多个字段的操作, 并提供一个结果矩阵

    SQL 与 ES 的聚合类比

    image-20201026143359542

    Aggregation 属于 Search 的一部分, 一般建议将其 Size 指定为 0.

    Aggregation 语法

    POST 索引名/_search
    {
        // 与 Query 同级的关键词
        "aggs": {
            "聚合名1": {
                // 聚合的定义: 不同的 type + body
                "<聚合 type>": {
                    // aggregation body, 不同 aggregation type 对应的 body 格式也不同
                    ...
                },
                // 可选: meta
                "meta": {
                    ...
                },
                // 可选: 子查询, 嵌套聚合
                "aggs": {                
                    ...
                }
            },
                
            // 可以包含多个同级的聚合查询
            "聚合名2": { 
                ... 
            }
        },
    }

    本节课程使用数据初始化

    DELETE employees
    
    PUT employees
    {
      "mappings": {
        "properties": {
          "age": {
            "type": "integer"
          },
          "gender": {
            "type": "keyword"
          },
          "job": {
            "type": "text",
            // text 类型默认不开启 fielddata, 因此该字段无法用于 terms 聚合.
            // "fielddata": true,
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 50
              }
            }
          },
          "name": {
            "type":"text"
          },
          "salary":{
            "type":"integer"
          }
        }
      }
    }
    
    GET employees
    
    PUT /employees/_bulk
    { "index" : {  "_id" : "1" } }
    { "name" : "Emma","age":32,"job":"Product Manager","gender":"female","salary":35000 }
    { "index" : {  "_id" : "2" } }
    { "name" : "Underwood","age":41,"job":"Dev Manager","gender":"male","salary": 50000}
    { "index" : {  "_id" : "3" } }
    { "name" : "Tran","age":25,"job":"Web Designer","gender":"male","salary":18000 }
    { "index" : {  "_id" : "4" } }
    { "name" : "Rivera","age":26,"job":"Web Designer","gender":"female","salary": 22000}
    { "index" : {  "_id" : "5" } }
    { "name" : "Rose","age":25,"job":"QA","gender":"female","salary":18000 }
    { "index" : {  "_id" : "6" } }
    { "name" : "Lucy","age":31,"job":"QA","gender":"female","salary": 25000}
    { "index" : {  "_id" : "7" } }
    { "name" : "Byrd","age":27,"job":"QA","gender":"male","salary":20000 }
    { "index" : {  "_id" : "8" } }
    { "name" : "Foster","age":27,"job":"Java Programmer","gender":"male","salary": 20000}
    { "index" : {  "_id" : "9" } }
    { "name" : "Gregory","age":32,"job":"Java Programmer","gender":"male","salary":22000 }
    { "index" : {  "_id" : "10" } }
    { "name" : "Bryant","age":20,"job":"Java Programmer","gender":"male","salary": 9000}
    { "index" : {  "_id" : "11" } }
    { "name" : "Jenny","age":36,"job":"Java Programmer","gender":"female","salary":38000 }
    { "index" : {  "_id" : "12" } }
    { "name" : "Mcdonald","age":31,"job":"Java Programmer","gender":"male","salary": 32000}
    { "index" : {  "_id" : "13" } }
    { "name" : "Jonthna","age":30,"job":"Java Programmer","gender":"female","salary":30000 }
    { "index" : {  "_id" : "14" } }
    { "name" : "Marshall","age":32,"job":"Javascript Programmer","gender":"male","salary": 25000}
    { "index" : {  "_id" : "15" } }
    { "name" : "King","age":33,"job":"Java Programmer","gender":"male","salary":28000 }
    { "index" : {  "_id" : "16" } }
    { "name" : "Mccarthy","age":21,"job":"Javascript Programmer","gender":"male","salary": 16000}
    { "index" : {  "_id" : "17" } }
    { "name" : "Goodwin","age":25,"job":"Javascript Programmer","gender":"male","salary": 16000}
    { "index" : {  "_id" : "18" } }
    { "name" : "Catherine","age":29,"job":"Javascript Programmer","gender":"female","salary": 20000}
    { "index" : {  "_id" : "19" } }
    { "name" : "Boone","age":30,"job":"DBA","gender":"male","salary": 30000}
    { "index" : {  "_id" : "20" } }
    { "name" : "Kathy","age":29,"job":"DBA","gender":"female","salary": 20000}

    Metric Aggregation

    Metric Aggregation 支持

    • 单值分析: 只输出一个结果

      • min, max, avg, sum: 最小, 最大, 平均, 总和

        // 找到最低的工资
        POST employees/_search
        {
          "size": 0,
          "aggs": {
            "min_salary": {
              "min": {
                "field": "salary"
              }
            }
          }
        }
      • cardinality: 基数统计, 理解为 count
    • 多值分析: 输出多个分析结果

      • stats: 输出 count, min, max, avg, sum 统计值

        // 输出多个值
        POST employees/_search
        {
          "size": 0,
          "aggs": {
            "stats_salary": {
              "stats": {
                "field": "salary"
              }
            }
          }
        }
      • extended_stats: 除了 stats 的输出结果外, 还额外输出 方差(variance), 标准差(std_deviation) 等
      • percentile: 百分位数
      • percentile_ranks
      • top_hits: 根据指定排序, 仅输出前N个文档

    Bucket Aggregation

    Bucket Aggregation:

    • 按照一定的规则, 将文档分配到不同的桶中, 从而达到分类的目的.
    • 支持嵌套: Bucket 聚合分析支持添加子聚合来进一步分析, 子聚合可以是 Bucket 及 Metric.

    Bucket Aggregation 支持以下几种分桶:

    • 非数字类型

      • terms

        POST employees/_search
        {
          "size": 0,
          "aggs": {
            "terms_job": {
              "terms": {
                "field": "job.keyword",
                // 指定返回的桶数(默认是按数量返回前N个)
                "size": 10
              }
            }
          }
        }

        Terms Aggregation 只能对打开了 fielddata 的字段进行聚合(具体是在索引的 mappings 中设置该字段时指定 fielddata 为 true)

    • 数字类型

      • range: 按数字范围分桶

        POST employees/_search
        {
          "size": 0,
          "aggs": {
            "range_age": {
              "range": {
                "field": "age",
                "ranges": [
                  {
                    "to": 30,
                    // 自定义 key
                    "key": "<30"
                  },
                  {
                    "from": 30,
                    "to": 50
                  },
                  {
                    "from": 50
                  }
                ]
              }
            }
          }
        }
      • date_range: 对日期的范围分桶
      • histogram: 对数字计算直方图数据

        POST employees/_search
        {
          "size": 0,
          "aggs": {
            "my_stats": {
              "histogram": {
                "field": "age",
                "interval": 1,
                "min_doc_count": 1
              }
            }
          }
        }
      • date_histogram: 对日期的直方图数据

    示例: 嵌套聚合, 查找各个工种中, 年纪最大的前 3 名员工

    POST employees/_search
    {
      "size": 0,
      "aggs": {
        "jobs": {
          "terms": {
            "field": "job.keyword"
          },
          "aggs": {
            "old_employers": {
              "top_hits": {
                "size": 3,
                "sort": [
                  {
                    "age": {
                      "order": "desc"
                    }
                  }
                ]
              }
            }
          }
        }
      }
    }

    优化 Terms 聚合性能

    当需要频繁聚合, 对聚合搜索有性能要求, 且持续地增加新文档时, 建议使用下面的方式来优化.

    这样每次新增文档时, 都会预先处理聚合, 从而提升 Terms 聚合搜索时的性能.

    • 设置 eager_global_ordinals 为 true

    image-20201026151040102

    Pipeline 聚合分析

    Pipeline 管道聚合分析: 对聚合分析的结果, 再次进行聚合分析.

    Pipeline 管道聚合, 根据分析结果输出到原结果中位置的不同, 分为两类

    • Sibling: 输出结果和现有分析结果同级

      • max_bucket, min_bucket, avg_bucket, sum_bucket
      • stats_bucket, extended_stats_bucket
      • percentiles_bucket
    • Parent: 输出结果内嵌到现有的聚合分析结果之中

      • derivative: 求导
      • cumulative_sum: 累计求和
      • moving_avg: 滑动窗口(移动平均值)

    示例: Sibling 类型的 Pipeline Aggregation

    POST employees/_search
    {
      "size": 0,
      "aggs": {
        "jobs": {
          "terms": {
            "field": "job.keyword"
          },
          "aggs": {
            "salary": {
              "stats": {
                "field": "salary"
              }
            }
          }
        },
        // 获取平均工资最低的工作类型
        "min_avg_salary_by_job": {
          "min_bucket": {
            "buckets_path": "jobs>salary.min"
          }
        },
        // 平均工资的统计分析
        "stats_avg_salary_by_job": {
          "stats_bucket": {
            "buckets_path": "jobs>salary.avg"
          }
        },
        // 平均工资的百分位数
        "percentiles_avg_salary_by_job": {
          "percentiles_bucket": {
            "buckets_path": "jobs>salary.avg"
          }
        }
        
      }
    }
    
    
    
    
    
    // 返回值:
    {
      // ...,
      "aggregations" : {
        "jobs" : {
          "doc_count_error_upper_bound" : 0,
          "sum_other_doc_count" : 0,
          "buckets" : [
            {
              "key" : "Java Programmer",
              "doc_count" : 7,
              "avg_salary" : {
                "count" : 7,
                "min" : 9000.0,
                "max" : 38000.0,
                "avg" : 25571.428571428572,
                "sum" : 179000.0
              }
            },
            {
              "key" : "Javascript Programmer",
              "doc_count" : 4,
              "avg_salary" : {
                "count" : 4,
                "min" : 16000.0,
                "max" : 25000.0,
                "avg" : 19250.0,
                "sum" : 77000.0
              }
            },
            // ...
          ]
        },
        "min_avg_salary_by_job" : {
          "value" : 9000.0,
          "keys" : [
            // 平均工资最低的工作是 Java
            "Java Programmer"
          ]
        },
        "stats_avg_salary_by_job" : {
          "count" : 7,
          "min" : 19250.0,
          "max" : 50000.0,
          "avg" : 27974.48979591837,
          "sum" : 195821.42857142858
        },
        "percentiles_avg_salary_by_job" : {
          "values" : {
            "1.0" : 19250.0,
            "5.0" : 19250.0,
            "25.0" : 21000.0,
            "50.0" : 25000.0,
            "75.0" : 35000.0,
            "95.0" : 50000.0,
            "99.0" : 50000.0
          }
        }
      }
    }

    示例: Parent 类型的 Pipeline Aggregation

    {
      "size": 0,
      "aggs": {
        // 对 age terms 分桶
        "age": {
          "histogram": {
            "field": "age",
            "interval": 1
          },
          "aggs": {
            // 桶内计算平均工资
            "avg_salary": {
              "avg": {
                "field": "salary"
              }
            },
            // 桶内计算平均工资的累计求和
            "cumulative_salary": {
              "cumulative_sum": {
                "buckets_path": "avg_salary"
              }
            }
          }
        }
      }
    }
    
    
    // 返回值
    {
      // ...
      "aggregations" : {
        "age" : {
          "buckets" : [
            {
              "key" : 20.0,
              "doc_count" : 1,
              "avg_salary" : {
                "value" : 9000.0
              },
              "cumulative_salary" : {
                "value" : 9000.0
              }
            },
            {
              "key" : 21.0,
              "doc_count" : 1,
              "avg_salary" : {
                "value" : 16000.0
              },
              "cumulative_salary" : {
                "value" : 25000.0
              }
            },
            // ...
          ]
        }
      }
    }          

    聚合范围

    ES 聚合分析的默认作用范围是 query 的查询结果集.

    ES 支持以下方式改变聚合的作用范围

    • query: 同时影响查询结果集和聚合范围
    • post_filter: 只影响查询结果集

      POST employees/_search
      {
        "aggs": {
          "jobs": {
            "terms": {
              "field": "job.keyword"
            }
          }
        },
        "post_filter": {
          "match": {
            "job.keyword": "Dev Manager"
          }
        }
      }
    • aggs.*.filter: 只影响聚合范围, 但同时也收到全局 query 的影响

      POST employees/_search
      {
        "size": 0,
        "aggs": {
          // 返回的 all_jobs 聚合结果, 是对所有人的 job 分桶
          "all_jobs": {
            "terms": {
              "field": "job.keyword"
            }
          },
          
          // 返回的 older_person 聚合结果, 是仅对年龄 >= 35 的人的 job 分桶
          "older_person": {
            "filter": {
              "range": {
                "age": {
                  "gte": 35
                }
              }
            },
            "aggs": {
              "jobs": {
                "terms": {
                  "field": "job.keyword"
                }
              }
            }
          }
        }
      }
    • aggs.*.global: 只影响聚合范围, 此时忽略全局 query 的影响

      POST employees/_search
      {
        "size": 0,
        "query": {
          "range": {
            "age": {
              "gte": 1000
            }
          }
        },
        "aggs": {
          "jobs": {
            "terms": {
              "field": "job.keyword"
            }
          },
          "all": {
            "global": {},
            "aggs": {
              "jobs": {
                "terms": {
                  "field": "job.keyword"
                }
              }
            }
          }
        }
      }

    聚合排序

    ES 中可以指定 order, 对聚合结果按照 count 和 key 进行排序

    • 默认情况下, 按照 count 降序
    • 指定 size 可以仅返回前 N 个结果

    示例: 按照桶内数量升序, 若值相等则按照 key 升序

    POST employees/_search
    {
      "size": 0,
      "aggs": {
        "jobs": {
          "terms": {
            "field": "job.keyword",
            // 按照桶内数量升序, 若值相等则按照 key 升序
            "order": [
              {"_count": "asc"},
              {"_key": "asc"}
            ]
          }
        }
      }
    }

    示例: 根据平均工资排序

    POST employees/_search
    {
      "size": 0,
      "aggs": {
        "jobs": {
          "terms": {
            "field": "job.keyword",
            // 根据平均工资排序
            "order": [
              {
                "avg_salary": "desc"
              }
            ]
          },
          "aggs": {
            "avg_salary": {
              "avg": {
                "field": "salary"
              }
            }
          }
        }
      }
    }

    聚合分析的原理及精准度问题

    分布式系统的近似统计算法

    image-20201027174612171

    对于分布式系统的近似统计算法来说, 数据量, 精确度, 实时性 是难以兼得的.

    • 当数据量很大, 对结果的精确度要求很高, 但对实时性要求不大时, 可以采用 Hadoop 离线计算
    • 当数据量有限, 且实时性和精确度要求很高时, 此时 ES 可以通过将数据都放在一个主分片上, 从而保证精确度和实时性
    • 当数据量很大, 且对实时性要求很高时, 数据在 ES 中会被存放在多个主分片上, 此时无法保证精确度.

    ES 中有些统计能够保证精确度, 以 min 聚合分析的执行流程为例

    1. Coordinating Node 分别向各个主分片请求 min value
    2. 各个主分片计算出当前分片内的 min value
    3. Coordinating Node 汇总结果, 并得出精确的 min value

    但其他的一些统计, ES 无法保证精确度, 以 term 分桶聚合为例

    1. 客户端向 Coordinating Node 发起 search 查询, size: 3(仅返回前3个最大的分桶)
    2. Coordinating Node 分别向各个主分片请求 size: 3 的 query

      实际上 ES 不一定是请求各个分片内的 top 3, 而是请求 top shard_size, 这里只是举个例子.
    3. 各个主分片返回当前分片内的 top 3
    4. Coordinating Node 汇总结果, 但此时结果并不一定是准确的.

      image-20201027175407534

      上面图中有点问题, 根据官方文档, 上图的 doc_count_error_upper_bound 应该是 4 + 2 = 6, 而不是 7.

    Terms Aggregation 的返回值示例

    • doc_count_error_upper_bound: 表示被遗漏的 term 分桶, 其中包含的文档可能的最大值

      个人理解, 比如说有3个主分片, 当 shard_size 值为 10 时, 3个主分片返回的桶中最小的那个桶的大小分别是 n1, n2, n3, 那么该值应该是 n1 + n2 + n3.

      以上面那个 《Terms 不正确的案例》为例, doc_count_error_upper_bound = 6, 表示可能存在遗漏的最大大小为 6 的分桶.

    • sum_other_doc_count: 除了返回结果中 bucket 的 terms 以外, 其他 terms 的文档总数, 即总文档数 - 返回的文档数.

    image-20201027180138075

    在查询时打开 show_term_doc_count_error 时会返回上述字段:

    解决 terms 不准的问题: 提升 shard_size

    Terms 聚合分布不准确的原因是, 数据分散在多个主分片上, Coordinating Node 无法获取数据全貌.

    这里存在2个解决方案:

    1. 数据量不大时, 设置 Primary Shard 为 1, 从而保证准确性
    2. 在分布式数据上, 查询时设置更大的 shard_size, 令各个分片返回额外的数据, 从而提升准确率.

    shard_size 设定

    • 该值表示, Coordinating Node 向各个主分片请求返回的 size
    • 调整 shard_size 大小, 可以降低 doc_count_error_upper_bound 来提升准确度

      副作用是: 增加了整体的计算量, 会降低响应时间
    • shard_size 默认大小设定

      shard_size = size * 1.5 + 10

    示例: terms 不准确的情况

    DELETE my_flights
    
    GET kibana_sample_data_flights
    
    
    // 将 my_flights 的主分片数故意调整成 20
    PUT my_flights
    {
      "settings": {
        "number_of_shards": 20,
        "number_of_replicas": 0
      },
      "mappings": {
        "properties": {
          "AvgTicketPrice": {
            "type": "float"
          },
          "Cancelled": {
            "type": "boolean"
          },
          "Carrier": {
            "type": "keyword"
          },
          "Dest": {
            "type": "keyword"
          },
          "DestAirportID": {
            "type": "keyword"
          },
          "DestCityName": {
            "type": "keyword"
          },
          "DestCountry": {
            "type": "keyword"
          },
          "DestLocation": {
            "type": "geo_point"
          },
          "DestRegion": {
            "type": "keyword"
          },
          "DestWeather": {
            "type": "keyword"
          },
          "DistanceKilometers": {
            "type": "float"
          },
          "DistanceMiles": {
            "type": "float"
          },
          "FlightDelay": {
            "type": "boolean"
          },
          "FlightDelayMin": {
            "type": "integer"
          },
          "FlightDelayType": {
            "type": "keyword"
          },
          "FlightNum": {
            "type": "keyword"
          },
          "FlightTimeHour": {
            "type": "keyword"
          },
          "FlightTimeMin": {
            "type": "float"
          },
          "Origin": {
            "type": "keyword"
          },
          "OriginAirportID": {
            "type": "keyword"
          },
          "OriginCityName": {
            "type": "keyword"
          },
          "OriginCountry": {
            "type": "keyword"
          },
          "OriginLocation": {
            "type": "geo_point"
          },
          "OriginRegion": {
            "type": "keyword"
          },
          "OriginWeather": {
            "type": "keyword"
          },
          "dayOfWeek": {
            "type": "integer"
          },
          "timestamp": {
            "type": "date"
          }
        }
      }
    }
    
    
    // reindex
    POST _reindex
    {
      "source": {
        "index": "kibana_sample_data_flights"
      },
      "dest": {
        "index": "my_flights"
      }
    }
    
    
    
    GET my_flights/_count
    GET kibana_sample_data_flights/_count
    
    POST kibana_sample_data_flights/_search
    {
      "size": 0,
      "aggs": {
        "weather": {
          "terms": {
            "field": "OriginWeather",
            "size": 1,
            "shard_size": 2,
            "show_term_doc_count_error": true
          }
        }
      }
    }
    
    POST my_flights/_search
    {
      "size": 0,
      "aggs": {
        "weather": {
          "terms": {
            "field": "OriginWeather",
            "size": 1,
            "shard_size": 5,
            "show_term_doc_count_error": true
          }
        }
      }
    }

    上述最后一个查询的返回结果

    {
      "took" : 11,
      "timed_out" : false,
      "_shards" : {
        "total" : 20,
        "successful" : 20,
        "skipped" : 0,
        "failed" : 0
      },
      "hits" : {
        "total" : {
          "value" : 10000,
          "relation" : "gte"
        },
        "max_score" : null,
        "hits" : [ ]
      },
      "aggregations" : {
        "weather" : {
          // 可能遗漏的桶最大可能为 1193
          "doc_count_error_upper_bound" : 1193,
          "sum_other_doc_count" : 10735,
          "buckets" : [
            {
              "key" : "Clear",
              "doc_count" : 2324,
              "doc_count_error_upper_bound" : 0
            }
          ]
        }
      }
    }

    数据建模

    ES 中处理关联关系

    • ES 中往往考虑 Denormalize 数据: 读的速度变快、无需表连接、无需行锁

      而关系型数据库则一般会考虑 Normalize 数据.
    • ES 主要采用以下 4 种方法处理关联

      1. 对象类型
      2. 嵌套对象(Nested Object)
      3. 父子关联关系(Parent / Child)
      4. 应用端关联

    对象

    使用示例

    初始化相关数据

    DELETE blog
    
    PUT blog
    {
      "mappings": {
        "properties": {
          "content":{
            "type": "text"
          },
          "time": {
            "type": "date"
          },
          "user":{
            "properties": {
              "userid": {
                "type": "long"
              },
              "username": {
                "type": "keyword"
              },
              "city": {
                "type": "text"
              }
            }
          }
        }
      }
    }
    
    // 插入一条 Blog 信息
    PUT blog/_doc/1
    {
      "content":"I like Elasticsearch",
      "time":"2019-01-01T00:00:00",
      "user":{
        "userid":1,
        "username":"Jack",
        "city":"Shanghai"
      }
    }

    查询对象字段

    // 查询对象字段
    POST blog/_search
    {
      "query": {
        "bool": {
          "must": [
            {
              "match": {"user.username": "Jack"}
            },
            {
              "match": {"content": "Elasticsearch"}
            }
          ]
        }
      }
    }
    

    存储数组时存在的问题

    ES 在存储时, 内部对象的边界并没有考虑在内, JSON 格式被处理成扁平式键值对的结构.

    当对多个字段进行查询时, 会导致意外的搜索结果: 可以使用 Nested (内嵌对象) 来解决这个问题.

    比如如下数据在 Lucene 中实际存储:

    POST my_movies/_doc/1
    {
      "title":"Speed",
      "actors":[
        {
          "first_name":"Keanu",
          "last_name":"Reeves"
        },
    
        {
          "first_name":"Dennis",
          "last_name":"Hopper"
        }
    
      ]
    }

    👆 上述数据在 Lucene 中实际存储如下 👇

    "title": "Speed"
    "actors.first_name": ["Keanu", "Dennis"]
    "actors.last_name": ["Reeves", "Hopper"]

    👆 因此, 我们在搜索 actor: {first_name: "Keanu", last_name: "Hopper"} 时会错误地匹配到上述文档.

    示例情况

    DELETE my_movies
    
    //  电影的Mapping信息
    PUT my_movies
    {
      "mappings": {
        "properties": {
          "actors": {
            "properties": {
              "first_name": {
                "type": "keyword"
              },
              "last_name": {
                "type": "keyword"
              }
            }
          },
          "title": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          }
        }
      }
    }
    
    // 写入一条电影信息(对于内部对象的存储, JSON格式被处理成扁平式键值对的结构存储)
    POST my_movies/_doc/1
    {
      "title":"Speed",
      "actors":[
        {
          "first_name":"Keanu",
          "last_name":"Reeves"
        },
    
        {
          "first_name":"Dennis",
          "last_name":"Hopper"
        }
    
      ]
    }
    
    
    # 查询电影信息(错误地匹配到上面的文档)
    POST my_movies/_search
    {
      "query": {
        "bool": {
          "must": [
            {
              "match": {
                "actors.first_name": "Keanu"
              }
            },
            {
              "match": {
                "actors.last_name": "Hopper"
              }
            }
          ]
        }
      }
    }

    Nested 嵌套对象

    什么是 Nested Data Type

    Nested 数据类型:

    • 允许对象数组中的对象被独立索引
    • 在内部, Nested 文档会被保存在两个 Lucene 文档中, 在查询时做 Join 处理.

      POST my_movies/_doc/1
      {
        "title":"Speed",
        "actors":[
          {
            "first_name":"Keanu",
            "last_name":"Reeves"
          },
      
          {
            "first_name":"Dennis",
            "last_name":"Hopper"
          }
      
        ]
      }

      当 actors 是 Nested 类型字段时, 上述数据的 actors 中的多个子文档是分别存储的, 并不是扁平化的键值对结构.

    使用示例

    DELETE my_movies
    
    PUT my_movies
    {
      "mappings": {
        "properties": {
          "title": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "actors": {
            "type": "nested",
            "properties": {
              "first_name": {
                "type": "keyword"
              },
              "last_name": {
                "type": "keyword"
              }
            }
          }
        }
      }
    }
    
    POST my_movies/_doc/1
    {
      "title":"Speed",
      "actors":[
        {
          "first_name":"Keanu",
          "last_name":"Reeves"
        },
    
        {
          "first_name":"Dennis",
          "last_name":"Hopper"
        }
    
      ]
    }
    
    // 对于 nested 字段, 这样是匹配不到任何结果的
    POST my_movies/_search
    {
      "query": {
        "bool": {
          "must": [
            {
              "match": {
                "actors.first_name": "Keanu"
              }
            },
            {
              "match": {
                "actors.last_name": "Reeves"
              }
            }
          ]
        }
      }
    }
    
    // 对于 nested, 这种常规写法也是搜索不到东西的
    POST my_movies/_search
    {
      "query": {
        "match": {
          "actors.first_name": "Keanu"
        }
      }
    }
    
    
    // 对于 nested 字段, 搜索时必须使用 nested 专用的搜索语法
    POST my_movies/_search
    {
      "query": {
        "nested": {
          "path": "actors",
          "query": {
            "bool": {
              "must": [
                {
                  "match": {
                    "actors.first_name": "Keanu"
                  }
                },
                {
                  "match": {
                    "actors.last_name": "Reeves"
                  }
                }
              ]
            }
          }
        }
      }
    }
    
    // 对于 nested 字段, 搜索时必须使用 nested 专用的搜索语法
    POST my_movies/_search
    {
      "query": {
        "nested": {
          "path": "actors",
          "query": {
            "match": {
              "actors.first_name": "Keanu"
            }
          }
        }
      }
    }
    
    // Nested 查询
    POST my_movies/_search
    {
      "query": {
        "bool": {
          "must": [
            {
              "match": {
                "title": "Speed"
              }
            },
            {
              "nested": {
                "path": "actors",
                "query": {
                  "bool": {
                    "must": [
                      {
                        "match": {
                          "actors.first_name": "Keanu"
                        }
                      },
                      {
                        "match": {
                          "actors.last_name": "Hopper"
                        }
                      }
                    ]
                  }
                }
              }
            }
          ]
        }
      }
    }
    
    // 对于 nested 的分桶聚合(普通 aggregation 对于 nested 无效)
    POST my_movies/_search
    {
      "size": 0,
      "aggs": {
        "actors": {
          "nested": {
            "path": "actors"
          },
          "aggs": {
            "first_name": {
              "terms": {
                "field": "actors.first_name",
                "size": 10
              }
            }
          }
        }
      }
    }

    文档的父子关系 Parent / Child

    Parent / Child

    Q: 要使用 Parent / Child ?

    A: 对象和 Nested 对象存在局限性: 每次更新时, 需要重新索引整个对象(包括根对象和嵌套对象)

    ES 提供了类似关系型数据库的 Join 的实现, 使用 Join 数据类型实现, 通过维护 Parent / Child 的关系, 从而分离两个对象

    • 父文档和子文档是两个独立的文档.
    • 更新父文档无需重新索引子文档.
    • 子文档被添加/更新/删除时, 也不会影响到父文档和其他子文档

    Q: 如何定义父子关系

    1. 设置索引的 Mapping
    2. 索引父文档
    3. 索引子文档
    4. 按需查询文档

    注意!!! Parent/Child 父子关系的文档在查询时, 查询速度会慢几百倍.

    Nested 类型的数据查询时是会慢几倍.

    与嵌套对象的对比

    image-20201031002651020

    使用示例

    创建索引

    DELETE my_blogs
    
    // 创建索引
    PUT my_blogs
    {
      "settings": {
        "number_of_shards": 2
      }, 
      "mappings": {
        "properties": {
          "blog_comments_relation": {
            // 指定 join 类型
            "type": "join",
            // 声明 Parent / Child 关系
            "relations": {
              // blog 是 Parent
              // comment 是 Child
              "blog": "comment"
            }
          },
          "content": {
            "type": "text"
          },
          "title": {
            "type": "keyword"
          }
        }
      }
    }

    写入数据

    // 索引父文档, "blog1" 是父文档的 id
    PUT my_blogs/_doc/blog1
    {
      "title": "Learning Elasticsearch",
      "content": "learning ELK @ geektime",
      "blog_comments_relation": {
        // 申明文档的类型是 blog, 即作为 Parent
        "name": "blog"
      }
    }
    
    
    PUT my_blogs/_doc/blog2
    {
      "title": "Learning Hadoop",
      "content": "learning Hadoop",
      "blog_comments_relation": {
        "name": "blog"
      }
    }
    
    
    // 索引子文档, "comment1" 是子文档的 id, 指定 routing, 确保子文档和父文档索引到同一个分片
    PUT my_blogs/_doc/comment1?routing=blog1
    {
      "comment": "I am learning ELK",
      "username": "Jack",
      "blog_comments_relation": {
        "name": "comment",
        // "blog1" 是父文档 id
        "parent": "blog1"
      }
    }
    
    PUT my_blogs/_doc/comment2?routing=blog2
    {
      "comment": "I like Hadoop!!!",
      "username": "Jack",
      "blog_comments_relation": {
        "name": "comment",
        "parent": "blog2"
      }
    }
    
    PUT my_blogs/_doc/comment3?routing=blog2
    {
      "content": "Hello Hadoop",
      "username": "Bob",
      "blog_comments_relation": {
        "name": "comment",
        "parent": "blog2"
      }
    }

    查询

    // 根据 Parent Id 查询所属的子文档
    POST my_blogs/_search
    {
      "query": {
        "parent_id": {
          "type": "comment",
          "id": "blog2"
        }
      }
    }
    
    // Has Child 查询, 根据子文档属性, 查询对应的父文档
    POST my_blogs/_search
    {
      "query": {
        "has_child": {
          "type": "comment",
          "query": {
            "match": {
              // 查找子文档(评论)中包含 "hadoop" 的父文档
              "comment": "hadoop"
            }
          }
        }
      }
    }
    
    // Has Parent 查询, 根据父文档属性, 查询相关的子文档
    POST my_blogs/_search
    {
      "query": {
        "has_parent": {
          "parent_type": "blog",
          "query": {
            "match": {
              // 查找父文档(文章)中内容包含 "Hadoop" 的文章的所有子文档
              "content": "Hadoop"
            }
          }
        }
      }
    }
    
    // 查询父文档时, 按照正常传入父文档 id 即可.
    GET my_blogs/_doc/blog2
    
    // 查询子文档时, 必须同时指定子文档id 和对应 routing 才能查询到
    GET my_blogs/_doc/comment3?routing=blog2

    重建索引

    需要重建索引的场景:

    • 索引内的 Mappings 发生变更: 字段类型变更, 分词器及字典更新
    • 索引的 Settings 发生变更: 索引主分片数发生改变
    • 集群内, 集群间需要做数据迁移

    ES 用于支持重建索引的内置 API

    • Update By Query: 在现有索引上重建
    • Reindex: 在其他索引上重建

    举例:为索引增加子字段, 修改 Mappings 对已存在的文档无效

    上述事例, 之前的 mappings 中 content 字段没有子字段 english

    并且已经索引了一条 content 为 “Hadoop is cool” 的文档。

    为了提高查询 recall(召回率), 修改 mappings, 为 content 添加了使用 english 分词器的子字段 english.

    但对于之前已经索引的文档, 这个新的子字段是无效的(即不包含已索引的文档).

    举例: 修改索引中已存在字段的类型时会报错

    比如字段 title 之前是 text 类型, 现在将它修改为 keyword 类型时会报错提示无法修改字段类型, 只能创建新的索引, 并且设定正确的字段类型, 再重新导入数据.

    Update By Query

    在修改了 mappings , 添加新字段后, 可以使用 _update_by_query, 在原来索引上重建索引.

    Reindex API

    Reindex API : 支持把文档从一个索引(src)拷贝到另一个索引(dest), dest 索引必须提前创建, 允许时其他集群的索引.

    使用的场景:

    • 修改索引的主分片数
    • 改变字段的 Mapping 中的字段类型
    • 集群内数据迁移 / 跨集群的数据迁移

    注意

    • Reindex 的 src 索引必须启用 _source 才可以(正常索引默认是启用的)
    • Reindex 并不会尝试去创建 dest 索引(并不会复制 src 的 settings 和 mappings), 因此需要自己提前创建好 dest 索引, 设定 settings 和 mappings

    使用 Reindex API

    image-20201031075430762

    使用 op_type 指定 reindex 的行为: 仅创建不存在的文档

    正常情况下, 文档如果已经存在, 会导致版本冲突

    image-20201031080551366

    跨集群 Reindex

    异步操作 Task API

    image-20201031080632860

    Ingest Pipeline & Painless Script

    ES 5.0 引入了 Ingest Node 节点类型. 默认情况下每个节点都是 Ingest Node:

    • 具有预处理数据的能力, 能拦截 Index 和 Bulk API 的请求
    • 对数据进行转换, 并重新返回给 Index 或 Bulk API

    Ingest Node 提供了独立的数据预处理功能(无需 Logstash), 例如:

    • 为某个字段设置默认值; 重命名字段名; 字段值分割
    • 支持设置 Painless 脚本, 对数据进行更复杂的加工

    Ingest Node v.s Logstash

    Pipeline & Process

    Pipeline: 管道会对通过的数据(文档)按照顺序进行加工

    Process: ES 对一些加工行为的抽象包装

    Pipeline 和 Process 的关系是 1:N

    Simulate API 模拟 Pipeline

    ES 提供了 Simulate API 用于模拟 Pipeline, 如下所示:

    创建 Pipeline

    在 ES 中创建一个 Pipeline

    image-20201101231906545

    索引文档时使用 Pipeline

    image-20201101231928517

    对已存在的文档引用 Pipeline

    如果有部分文档需要重新应用 Pipeline, 那么可以使用 _update_by_query 来批量处理

    image-20201101232125639

    这里使用了 query 根据条件筛选文档来重新应用管道, 若请求体为空, 则表示对所有文档都应用该管道.

    👆 这里指定 update_by_query 的条件, 仅仅对特定文档进行处理(未被该管道处理过的), 确保不会发生错误.

    内置的 Processes

    部分内置的 Processors

    https://www.elastic.co/guide/...

    split

    示例

    POST _ingest/pipeline/_simulate
    {
        "pipeline": {
            "description": "...",
            "processors": [
                {
                    "split": {
                        "field": "test_field",
                        // 分割符
                        "separator": ";"
                    }
                }
            ]
        },
        "docs": [
            {
                "_source": {
                    "test_field": "a;b;c"
                }
            }
        ]
    }
    convert

    https://www.elastic.co/guide/...

    支持的转换类型:

    • integer
    • long
    • float
    • double
    • string
    • boolean
    • auto

    示例

    POST _ingest/pipeline/_simulate
    {
        "pipeline": {
            "description": "...",
            "processors": [
                {
                    "convert": {
                        "field": "test_field",
                        "type": "integer",
                        "on_failure": [
                          {
                            "set": {
                              "field": "test_field",
                              "value": 0
                            }
                          }
                        ]
                    }
                }
            ]
        },
        "docs": [
            {
                "_source": {
                    "test_field": "123"
                }
            },
            {
                "_source": {
                    "test_field": "abc"
                }
            }
        ]
    }
    date

    关于 date process 支持的时间格式具体可以看: https://docs.oracle.com/javas...

    其实采用的是 java 的日期时间 formatter 的模式.
    grok

    关于自带的 grok 模式可以查看: https://github.com/logstash-p...

    👉 这是中文版 https://www.cnblogs.com/zhang...

    在定义 ingest pipeline 时, 建议使用 """ 来包裹 grok 表达式, 否则会各种蛋疼......

    比如:

    PUT _ingest/pipeline/php_fpm_log_pipeline
    {
      "description": "php-fpm 日志处理",
      "processors": [
        {
          "grok": {
            "field": "message",
            "patterns": [
              """\[%{PHPFPM_TIMESTAMP:tmp.time}\] %{WORD:log.level}:\s*(\[pool %{WORD:fpm.pool}\])?\s*%{GREEDYMULTILINE:message}"""
            ],
            "pattern_definitions": {
              "PHPFPM_TIMESTAMP": """%{MONTHDAY}-%{MONTH}-%{YEAR} %{TIME}""",
              "GREEDYMULTILINE": "(.|\n|\t)*"
            }, 
            "description": "提取时间和日志级别"
          }
        }
      ]
    }

    一些方便的自定义 Grok 模式

    // 匹配剩余的内容(多行)
    GREEDYMULTILINE (.|\n|\t)*
    

    Painless Scrit

    ES 从 5.x 开始引入 Painless, 这是专门为 ES 设计并扩展了 Java 的语法.

    6.0 开始, ES 只支持 Painless, 而不再支持之前的 Groovy, JS, Python 等脚本.
    • Painless 支持所有 Java 的数据类型及 Java API 子集
    • 特性: 高性能 / 安全, 其支持显示类型或者动态定义类型
    理解为一种特殊的 Process ???

    Painless 的用途

    通过 Painless 脚本访问字段

    一个示例

    image-20201101233031119

    保存脚本在 Cluster State

    示例: 返回数据前进行处理

    image-20201101233158758

    ES 数据建模实例

    什么是数据建模

    数据建模(Data modeling) 是创建数据模型的过程:

    • 是对真实世界进行抽象描述的一种工具和方法, 实现对现实世界的映射
    • 三个过程: 概念模型 -> 逻辑模型 -> 数据模型(第三范式)

    其中 数据模型 指结合具体的数据库, 在满足业务读写性能等需求的前提下, 确定的最终定义.

    如何对字段进行建模

    需要依次考虑的点

    1. 字段类型
    2. 是否需要搜索及分词
    3. 是否要聚合及排序
    4. 是否要额外的存储

    同时善用相关 API

    • Index Template & Dynamic Template

      • 根据索引的名字匹配不同的 Mappings 和 Settings
      • 可以在一个 Mapping 上动态的设定字段类型
    • Index Alias

      • 无需停机, 无需修改程序, 即可进行修改
    • Update By Query & Reindex

    考虑字段类型

    Text v.s Keyword

    • Text

      • 用于全文本字段, 文本会被 Analyzer 分词
      • 默认不支持聚合分析及排序, 若需要的话则将该字段的 fielddata 设置为 true
    • Keyword

      • 用于 id、枚举及不需要分词的文本 (如电话号码, email, 手机号码, 邮政编码, 性别等)
      • 适用于 Filter (精确匹配), Sorting 和 Aggregation
    • 设置多字段类型

      • 默认会为文本类型设置成 text, 并且设置一个 keyword 的子字段
      • 在处理人类语言时, 通过增加 "英文", "拼音" 和 "标准" 分词器, 提高搜索结构

    对于结构化数据

    • 数值类型

      • 尽量选择贴近的类型 (例如能用 byte , 就不用 long)
    • 枚举类型

      • 设置为 keyword (即使是数字, 但若是仅用于枚举, 那么也应该设置成 keyword 以获得更好的性能)
    • 其他

      • 日期 / 布尔 / 地理信息

    考虑搜索

    • 如果字段不需要搜索、排序、聚合分析, 那么可以将 enabled 设置为 false
    • 如果字段仅仅是不需要搜索, 那么可以将 index 设置为 false
    • 如果字段需要搜索

      • 如果不需要算分, 那么可以将 norms 设置为 false
      • 根据不同的需求, 设置对应的 index_options

    考虑聚合及排序

    • 如果字段不需要搜索、排序、聚合分析, 那么可以将 enabled 设置为 false
    • 如果字段不需要排序、聚合分析, 那么可以将 doc_values / fielddata 设置为 false
    • 如果是更新频繁且聚合查询频繁的 keyword 类型, 那么推荐将 eager_global_ordinals 设置为 true

    考虑额外的存储

    默认所有字段都会存储在 _source 中, 因此一般无需考虑将字段作额外的存储.

    一般禁用 _source 的原因是

    • 减少硬盘空间的占用(适用于指标性数据)
    • 节省 I/O

    但是如果仅仅是考虑到硬盘空间的占用, 那么应优先考虑 "增加压缩比 compression level", 而不是禁用 _source.

    禁用 _source 存在的不便之处:

    • 无法查看到 _source 字段
    • 无法做 Reindex
    • 无法做 Update 文档操作
    若仅仅是为了在最终返回数据给客户端不返回 _source 字段, 那么使用 source filter 来控制即可, 无需禁用 _source.

    一般来说, 是不建议禁用 _source 的!!!!

    但是当我们确定要禁用 _source 时, 若需要额外存储某些字段, 则对这些字段可以设置 store 属性为 true.

    几个简单示例

    图书的索引

    image-20201102152618391

    图书索引 - 需求变更 - 关闭 _source

    image-20201102152639169

    image-20201102152813425

    ES 数据建模最佳实践

    建议: 处理关联关系

    对于关联关系

    1. 应优先考虑反范式(Denormalization), 即使用 Object 存储
    2. 若数据包含多数值对象, 同时有查询需求, 那么应使用 Nested 嵌套对象

      比如电影需要存储多个演员的信息
    3. 当关联文档更新非常频繁时, 则应考虑使用 Child / Parent

      比如文章和评论, 由于评论更新非常频繁, 因此评论不适合以 Nested 直接存储在文章中.

    注意!!!!

    • Kibana 目前对于 nested 类型和 parent / child 类型支持不怎么好, 尽量在未来有可能更好的支持.

      课堂上的 PPT 原文是写着说暂不支持.
    • 如果需要使用 Kibana 进行数据分析, 在数据建模时仍需对嵌套和父子关联类型做出取舍

    建议: 避免过多字段

    一个文档中, 最好避免大量的字段:

    • 过多的字段数不容易维护
    • Mapping 信息保存在 Cluster State 中, 数据量过大, 对集群性能会有影响

      Cluster State 信息需要和所有节点同步
    • 删除或者修改数据需要 reindex
    • 默认的最大字段数是 1000

      可以设置 index.mapping.total_fields_limit 修改限定最大字段数

    产生大量(成百上千)字段的原因: Dynamic Mapping

    Dynamic:

    • true: 未知字段会被自动加入索引
    • false: 未知字段不会被自动加入索引, 但会保存在 _source
    • strict: 未知字段会导致文档写入失败

      控制到字段级别

    建议生产环境不要使用 true, 谨慎使用 false, 推荐使用 strict.

    👆 还是得具体应用场景

    示例: 使用 Cookie Service 不善导致的字段数量爆炸

    image-20201102160218867

    👆上述问题的解决方案: Nested Object & Key Value

    image-20201102160249181

    通过 Nested 对象保存 Key/Value 的优缺点

    • 优点

      • 有效地减少了字段数量
    • 缺点

      • 导致查询语句复杂度增加
      • Kibana 的可视化分析对 Nested 对象支持不好

    建议: 避免正则查询

    问题

    • 正则、通配符查询、前缀查询都属于 Term 查询, 但是性能不够好
    • 若将通配符放在开头, 会导致性能的灾难

    示例: 版本号查询

    对于特定格式的版本号 主版本号.次版本号.bugfix版本号, 若我们直接用正则查询搜索版本字段(keyword 类型), 此时性能会很差.

    但可以通过将字符串转换为对象来解决这个问题.

    image-20201102161012242

    建议: 避免空值引起的聚合不准

    字段的 null_value 设置项

    示例: 空值导致聚合结果不准确

    image-20201102161123620

    解决方案:

    • 对于 rating 字段的属性, 设置其 null_value 为 1 (根据业务来)

    建议: 为索引的 Mapping 加入Meta信息

    image-20201102161311902

    第二部分总结

    Term 查询和基于全文 Match 搜索的区别

    • 使用 Term 查询, 无论是查询 keyword 或 text 字段, 都不会对输入进行分词
    • 使用 Match 查询则会对输入先进行分词, 再搜索

      但若是对 keyword 字段进行查询, 此时 ES 会将本次的 Match 查询转换为 Term 查询, 且不对输入分词.

    相关性的优化是一个迭代的过程, 因此建议使用 Search Template 来避免频繁修改代码.

    image-20201102172240317

    image-20201102172258056

    image-20201102172327172

    查看原文

    赞 0 收藏 0 评论 0

    嘉兴ing 发布了文章 · 1月18日

    ⭐《ElasticSearch核心技术与实战》笔记 - 1. 入门

    相关链接

    极客时间课程: https://time.geekbang.org/cou...

    课程配套 Github: https://github.com/onebirdroc...

    每个部分都有一份课堂上 ppt 的 pdf 版本.

    概述

    ElasticSearch 简介及其发展历史

    ElasticSearch 是一个开源的分布式搜索与分析引擎, 提供了近实时搜索和聚合两大功能.

    ES 版本与升级

    • 0.4: 2010年2月
    • 1.0: 2014年1月
    • 2.0: 2015年10月
    • 5.0: 2016年10月

      • Lucene 6.x
      • Type 标记为 deprecated, 支持 Keyword 类型
    • 6.0: 2017年10月

      • Lucene 7.x
      • SQL 的支持
      • 索引生命周期管理
    • 7.0: 2019年4月

      • Lucene 8.x
      • 正式废除单个索引下多个 Type 的支持

        这意味着 一个索引 对应 一个Type
      • Security 功能免费试用

        从 6.8 和 7.1 开始
      • ECK - ElasticSearch Operator on Kubernetes

    Elastic Stack 家族成员

    Logstash: 数据处理管道

    • 开源的服务器端数据处理管道, 支持从不同的来源采集数据, 转换数据, 并将数据发送到不同的存储库中.

      最早是用于日志的采集和处理
    • 特性

      • 实时解析和转换数据

        • 从 IP 地址破译出地理坐标
        • 将 PII 数据匿名化, 完全排除敏感字段
      • 可扩展

        • 200 个多个插件(日志/数据库/Arcsigh/Netflow)
      • 可靠性安全性

        • Logstash 会通过持久化队列来保证至少将运行中的事件送达一次
        • 数据传输加密
      • 监控

    Kibana: 可视化分析利器

    • Kibana 名字含义 = Kiwifruit + Banana
    • 数据可视化工具, 帮助用户解开对数据的任何疑问
    • 基于 Logstash 的工具, 2013 年加入 Elastic 公司

    BEATS : 轻量的数据采集器

    Filebeat: 日志文件

    日志文件可以通过filebeat抓取,直接丢进es或者logstash处理后入库

    Packetbeat: 网络抓包

    ...

    X-Pack : 商业化套件

    • 6.3 之前的版本, X-Pack 以插件方式安装
    • X-Pack 开源后(2018年), ELasticSearch & Kibana 支持 OSS 版和 Basic 两种版本

      • 部分 X-Pack 功能支持免费试用, 6.8 和 7.1 开始, Security 功能免费
    • OSS, Basic(免费), 黄金级, 白金级

    应用场景

    应用场景

    • 搜索
    • 日志管理
    • 安全分析
    • 指标分析
    • 业务分析
    • 应用性能监控

    ES 提供了如模糊搜索, 搜索条件的算分等关系型数据库所不擅长的功能, 但是在事务性方面不如关系型数据库强大.

    因此在实际生产环境中, 应结合具体业务要求, 综合使用.

    单独使用 ES 或与现有数据库集成 ?

    1. 单独使用 ES 存储: 架构会简单很多.
    2. 若存在以下情况则更推荐与数据库集成(推荐)

      • 与现有系统的集成
      • 需考虑事务性
      • 数据更新频繁

    如果没特殊情况, 建议使用同步机制, 将数据库中的数据同步到 ElasticSearch.

    比如 MySQL 的 BinLog + MQ 写入 ES.

    这部分可查看存储系统实战那个课程.

    对于日志收集的架构

    • redis/kafka/RabbitMQ 作为缓冲层, 支撑高并发写入
    • logstash: 转换, 聚合, 写到 ElasticSearch

    扩展

    报警

    Kibana 自带的 Watcher 使用时要加钱... 可以考虑用以下方案替代:

    安装上手

    ElasticSearch 的安装与简单配置

    ES 配置文件详解: https://www.elastic.co/guide/...

    安装 Java

    安装并运行 ES

    压缩包方式安装

    压缩包方式安装

    1. 下载并解压 ES

      或使用 yum 之类的安装...

      直接下载速度太慢了, 建议翻..墙

    2. 确认配置文件 config/elasticsearch.yml
    3. 运行 bin/elasticsearch

      默认端口: 9200

      ES 会优先使用系统已安装的 JDK(读取环境变量 JAVA_HOME)

    JVM 配置

    • 修改 JVM - config/jvm.options

      • 7.1 下的默认设置是 1GB
    • 配置的建议

    config/elasticsearch.yml

    • path.data: 数据保存目录
    • path.logs: 日志保存目录
    • network.host: 监听地址(本地只有本地)
    • ...

    更多的配置说明参见: https://www.elastic.co/guide/...

    目录结构

    RPM 方式安装

    RPM方式安装

    1. 下载并安装 RPM 包
    2. 默认配置文件 /etc/elasticsearch/elasticsearch.yml
    3. 额外的配置文件 /etc/sysconfig/elasticsearch

      包含环境变量及堆大小, 文件描述符设置

    4. 用 SysV 或 Systemd 管理进程

    文件目录分布结构

    TypeDescriptionDefault LocationSetting
    homeElasticsearch home directory or $ES_HOME/usr/share/elasticsearch
    binBinary scripts including elasticsearch to start a node and elasticsearch-plugin to install plugins/usr/share/elasticsearch/bin
    confConfiguration files including elasticsearch.yml/etc/elasticsearchES_PATH_CONF
    confEnvironment variables including heap size, file descriptors./etc/sysconfig/elasticsearch
    dataThe location of the data files of each index / shard allocated on the node. Can hold multiple locations./var/lib/elasticsearchpath.data
    jdkThe bundled Java Development Kit used to run Elasticsearch. Can be overridden by setting the JAVA_HOMEenvironment variable in /etc/sysconfig/elasticsearch./usr/share/elasticsearch/jdk
    logsLog files location./var/log/elasticsearchpath.logs
    pluginsPlugin files location. Each plugin will be contained in a subdirectory./usr/share/elasticsearch/plugins
    repoShared file system repository locations. Can hold multiple locations. A file system repository can be placed in to any subdirectory of any directory specified here.Not configuredpath.repo

    安装插件

    # 查看当前已安装插件列表
    bin/elasticsearch-plugin list
    # 也可以通过 RESTFul 接口查看已安装插件列表
    curl http://localhost:9200/_cat/plugins?v
    
    # 安装完需重启 ES 才能生效
    ## 安装analysis-icu
    bin/elasticsearch-plugin install analysis-icu
    
    ## 安装 analysis-ik 插件
    ### 若存在网络问题, 则可先通过其他方式下载插件到本地, 再安装(注意插件版本号必须和 ElasticSearch 版本号按照规则对应, 这里以 v7.9.1 为例)
    bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.9.1/elasticsearch-analysis-ik-7.9.1.zip
    
    # 重启 ES
    
    # Test
    curl -X POST "localhost:9200/_analyze?pretty" -H 'Content-Type: application/json' -d '{ "analyzer": "ik_smart", "text": "中文分词" }'

    在开发机运行多个 ES 实例

    # 端口分别自动从 9200~9202
    bin/elasticsearch -E node.name=node1 -E cluster.name=geektime -E path.data=node1_data -E path.logs=node1_logs -d
    bin/elasticsearch -E node.name=node2 -E cluster.name=geektime -E path.data=node2_data -E path.logs=node1_logs -d
    bin/elasticsearch -E node.name=node3 -E cluster.name=geektime -E path.data=node3_data -E path.logs=node1_logs -d

    如果是 windows 则用下面的

    bin\elasticsearch.bat -E node.name=node1 -E cluster.name=geektime -E path.data=node1_data -E path.logs=node1_logs
    bin\elasticsearch.bat -E node.name=node2 -E cluster.name=geektime -E path.data=node2_data -E path.logs=node2_logs
    bin\elasticsearch.bat -E node.name=node3 -E cluster.name=geektime -E path.data=node3_data -E path.logs=node3_logs
    注: Windows 环境中(cmder)使用 -d 选项无效, 无法在后台执行, 因此需要开3个命令行环境来执行.

    可以直接写成 3 个批处理文件, 方便处理.

    Kibana 的安装与界面快速浏览

    Kibana 配置文件详解: https://www.elastic.co/guide/...

    安装并运行

    压缩包方式安装

    1. 下载并解压
    2. 确认配置文件 config/kibana.yml

      server.port: 5601
      
      #server.host: "localhost"
      server.host: "0.0.0.0"
      
      elasticsearch.hosts: ["http://localhost:9200"]
      
      #i18n.locale: "en"
      i18n.locale: "zh-CN"
    3. 启动: bin/kibana

    RPM 方式安装

    1. 下载并安装 RPM 包
    2. 默认配置文件 /etc/kibana/kibana.yml
    3. 用 SysV 或 Systemd 管理进程

    文件目录结构

    TypeDescriptionDefault LocationSetting
    homeKibana home directory or $KIBANA_HOME/usr/share/kibana
    binBinary scripts including kibana to start the Kibana server and kibana-plugin to install plugins/usr/share/kibana/bin
    configConfiguration files including kibana.yml/etc/kibana
    dataThe location of the data files written to disk by Kibana and its plugins/var/lib/kibanapath.data
    logsLogs files location/var/log/kibanapath.logs
    optimizeTranspiled source code. Certain administrative actions (e.g. plugin install) result in the source code being retranspiled on the fly./usr/share/kibana/optimize
    pluginsPlugin files location. Each plugin will be contained in a subdirectory./usr/share/kibana/plugins

    安装插件

    • bin/kibana-plugin install plugin_location
    • bin/kibana-plugin list
    • bin/kibana remove

    Cerebro 的安装与简单配置

    Cerebro 是一个方便管理 ES 集群的工具.

    安装并运行

    1. 下载 并解压
    2. 确认配置 conf/application.conf
    3. 确认配置 conf/reference.conf
    4. 运行 bin/cerebro

    在本机 Docker 运行 ELK Stack 和 Cerebro

    https://www.elastic.co/guide/...

    若要在生产环境中使用 Docker, 还需要进行一些额外配置

    课程老师提供的 docker-compose 配置, 同时该链接提供了一些参考链接

    这里有一份 ELK docker-compose 配置 Star 9.8k+

    ⭐ ELK docker-compose Github

    https://github.com/youjiaxing...

    • fork 一份 star 9.8k 的来修改

    这里默认配置是单节点, 但是按照其中的简单文档, 也可以快速配置成多节点的集群模式.

    单节点

    docker-compose.yml

    version: "3"
    
    services:
        elasticsearch:
            build: 
                context: ./elasticsearch
                args:
                    ELK_VERSION: ${ELK_VERSION}
            volumes: 
                - ./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
                - elasticsearch:/usr/share/elasticsearch/data
            ports: 
                - "9200:9200"
                - "9300:9300"
            environment: 
                ES_JAVA_OPTS: "-Xms512m -Xmx512m"
                ELASTIC_PASSWORD: password
                # 使用单节点 discoverty 可禁用生产模式并避免启动时的检查
                # see https://www.elastic.co/guide/en/elasticsearch/reference/current/bootstrap-checks.html
                discovery.type: "single-node"
                
        kibana:
            build: 
                context: ./kibana
                args:
                    ELK_VERSION: ${ELK_VERSION}
            volumes: 
                - ./kibana/config/kibana.yml:/usr/share/kibana/config/kibana.yml:ro
            ports: 
                - "5601:5601"
            depends_on:
                - elasticsearch
    
        cerebro:
            image: "lmenezes/cerebro:0.9.2"
            depends_on: 
                - elasticsearch
            ports: 
                - "9000:9000"
            command:
                - -Dhosts.0.host=http://elasticsearch:9200
    
        logstash:
            build: 
                context: ./logstash
                args:
                    ELK_VERSION: ${ELK_VERSION}
            volumes: 
                - ./logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml:ro
                - ./logstash/pipeline:/usr/share/logstash/pipeline:ro
                - ./logstash/data:/data:ro
            ports: 
                # 可通过 5000 端口向 logstash 写入日志
                - "5000:5000/tcp"
                - "5000:5000/udp"
                - "9600:9600"
            environment: 
                LS_JAVA_OPTS: "-Xmx512m -Xms512m"
            depends_on:
                - elasticsearch
    
    
    volumes: 
        elasticsearch:

    .env

    ELK_VERSION=7.8.0

    elasticsearch/Dockerfile

    ARG ELK_VERSION
    
    # https://www.docker.elastic.co/
    FROM docker.elastic.co/elasticsearch/elasticsearch:${ELK_VERSION}
    
    # RUN elasticsearch-plugin install analysis-icu

    elastcisearch/config/elasticsearch.yml

    ---
    ## Default Elasticsearch configuration from Elasticsearch base image.
    ## https://github.com/elastic/elasticsearch/blob/master/distribution/docker/src/docker/config/elasticsearch.yml
    #
    cluster.name: "docker-cluster"
    network.host: 0.0.0.0
    
    ## X-Pack settings
    ## see https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-xpack.html
    #
    xpack.license.self_generated.type: basic
    # xpack.security.enabled: false
    xpack.monitoring.collection.enabled: true

    kibana/Dockerfile

    ARG ELK_VERSION
    
    FROM docker.elastic.co/kibana/kibana:${ELK_VERSION}
    
    # Add your kibana plugins setup here
    # Example: RUN kibana-plugin install <name|url>

    kibana/config/kibana.yml

    ---
        ## Default Kibana configuration from Kibana base image.
        ## https://github.com/elastic/kibana/blob/master/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.js
        #
        server.name: kibana
        server.host: 0.0.0.0
        elasticsearch.hosts: [ "http://elasticsearch:9200" ]
        monitoring.ui.container.elasticsearch.enabled: true
        i18n.locale: "zh-CN"
    
        ## X-Pack security credentials
        #
        elasticsearch.username: elastic
        elasticsearch.password: password
        xpack.security.enabled: false

    logstash/Dockerfile

    ARG ELK_VERSION
    
    FROM docker.elastic.co/logstash/logstash:${ELK_VERSION}
    
    # Add your logstash plugins setup here
    # Example: RUN logstash-plugin install logstash-filter-json

    多节点

    docker-compose.yml

    version: "3"
    
    services:
        es01:
            build: 
                context: ./elasticsearch
                args:
                    ELK_VERSION: ${ELK_VERSION}
            container_name: es01
            volumes: 
                - data01:/usr/share/elasticsearch/data
            ports: 
                - "9200:9200"
                - "9300:9300"
            environment: 
                node.name: es01
                cluster.name: es-docker-cluster
                discovery.seed_hosts: es02,es03
                cluster.initial_master_nodes: es01,es02,es03
                bootstrap.memory_lock: "true"
                ES_JAVA_OPTS: "-Xms512m -Xmx512m"
            ulimits: 
                memlock:
                    soft: -1
                    hard: -1
    
        es02:
            build: 
                context: ./elasticsearch
                args:
                    ELK_VERSION: ${ELK_VERSION}
            container_name: es02
            volumes: 
                - data02:/usr/share/elasticsearch/data
            ports: 
                - "9201:9200"
                - "9301:9300"
            environment: 
                node.name: es02
                cluster.name: es-docker-cluster
                discovery.seed_hosts: es01,es03
                cluster.initial_master_nodes: es01,es02,es03
                bootstrap.memory_lock: "true"
                ES_JAVA_OPTS: "-Xms512m -Xmx512m"
            ulimits: 
                memlock:
                    soft: -1
                    hard: -1
               
        es03:
            build: 
                context: ./elasticsearch
                args:
                    ELK_VERSION: ${ELK_VERSION}
            container_name: es03
            volumes: 
                - data03:/usr/share/elasticsearch/data
            ports: 
                - "9202:9200"
                - "9302:9300"
            environment: 
                node.name: es03
                cluster.name: es-docker-cluster
                discovery.seed_hosts: es01,es02
                cluster.initial_master_nodes: es01,es02,es03
                bootstrap.memory_lock: "true"
                ES_JAVA_OPTS: "-Xms512m -Xmx512m"
            ulimits: 
                memlock:
                    soft: -1
                    hard: -1
    
        kibana:
            build: 
                context: ./kibana
                args:
                    ELK_VERSION: ${ELK_VERSION}
            container_name: kibana
            # volumes: 
            #    - ./kibana/config/kibana.yml:/usr/share/kibana/config/kibana.yml:ro
            environment: 
                SERVER_NAME: kibana
                ELASTICSEARCH_HOSTS: http://es01:9200
                ELASTICSEARCH_URL: http://es01:9200
                I18N_LOCALE: zh-CN
            ports: 
                - "5601:5601"
            depends_on:
                - es01
                - es02
                - es03
    
        cerebro:
            image: "lmenezes/cerebro:0.9.2"
            container_name: cerebro
            ports: 
                - "9000:9000"
            command:
                - -Dhosts.0.host=http://es01:9200
            depends_on:
                - es01
                - es02
                - es03
    
        logstash:
            build: 
                context: ./logstash
                args:
                    ELK_VERSION: ${ELK_VERSION}
            container_name: logstash
            volumes: 
                # - ./logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml:ro
                - ./logstash/pipeline:/usr/share/logstash/pipeline:ro
                - ./logstash/data:/data:ro
            environment: 
                MONITORING_ELASTICSEARCH_HOSTS: http://es01:9200
            ports: 
                # 可通过 5000 端口向 logstash 写入日志
                - "5000:5000/tcp"
                - "5000:5000/udp"
                - "9600:9600"
            environment: 
                LS_JAVA_OPTS: "-Xmx512m -Xms512m"
            depends_on:
                - es01
                - es02
                - es03
    
    
    volumes: 
        data01:
        data02:
        data03:

    Dockerfile 配置文件同 "单节点模式的一样"

    logstash/pipeline/logstash.conf

    input {
        tcp {
            port => 5000
        }
    }
    
    ## Add your filters / logstash plugins configuration here
    
    output {
        elasticsearch {
            hosts => "es01:9200"
        }
    }

    logstash/pipeline/movielens.conf

    input {
      file {
        path => "/data/ml-latest-small/movies.csv"
        start_position => "beginning"
        sincedb_path => "/dev/null"
      }
    }
    
    filter {
      csv {
        separator => ","
        columns => ["id","content","genre"]
      }
    
      mutate {
        split => { "genre" => "|" }
        remove_field => ["path", "host","@timestamp","message"]
      }
    
      mutate {
    
        split => ["content", "("]
        add_field => { "title" => "%{[content][0]}"}
        add_field => { "year" => "%{[content][1]}"}
      }
    
      mutate {
        convert => {
          "year" => "integer"
        }
        strip => ["title"]
        remove_field => ["path", "host","@timestamp","message","content"]
      }
    }
    
    output {
       elasticsearch {
         hosts => "http://es01:9200"
         index => "movies"
         document_id => "%{id}"
       }
      stdout {}
    }

    Logstash 的安装与导入数据

    安装并运行

    1. 下载 并解压
    2. 确认配置 logstash.conf
    3. 执行 bin/logstash -f logstash.conf
    学习使用的测试数据及可以从 Movielens 下载

    logstash.conf 配置文件指定了导入数据的来源(input), 过滤(filter), 输出(output)

    ElasticSearch 入门

    基本概念 1

    文档 (Document)

    文档


    JSON 文档

    image-20200707082552389

    Logstash 会自动进行数据类型推算

    文档的元数据

    image-20200707082632652

    _all 字段从 7.0 开始废除

    索引(Index)

    索引

    image-20200707083128447


    索引的不同语义

    image-20200707083242980


    Type


    抽象与类比

    并不是那么恰当的对比

    RESTful Api

    基本概念 2

    分布式

    分布式系统的可用性与扩展性

    image-20200707084606269


    分布式特性

    节点

    节点

    image-20200707084752247


    Master-eligible nodes 和 Master Node

    image-20200707085000597


    Data Node & Coordinating Node

    image-20200707085238804


    其他的节点类型

    image-20200707085351755

    冷热节点

    • Hot: 存储热数据
    • Warn: 存储冷数据(指较少访问)

    配置节点类型

    image-20200707085546114

    每个节点设置单一角色的好处:

    • 更好的性能, 不同用途的节点可以配置不同级别的硬件.
    • 职责很明确

    分片

    分片(Primary Shard & Replica Shard)

    image-20200707085721482

    image-20200707185539644

    number_of_shards 主分片数量

    number_of_replicas 每个主分片的副本数量


    分片的设定

    image-20200707185809016

    "主分片数" 是在索引创建时预先设定, 且后续无法修改的.


    集群

    # 查看集群健康状态
    ## Green    主分片与副本都正常分配
    ## Yellow    朱分片全部正常分配, 有副本分片未能正常分配
    ## Red        有主分片未能分配(例如当服务器的磁盘容量超过85%时去创建一个新的索引)
    GET _cluster/health
    
    # 查看节点状态
    GET _cat/nodes?v
    
    # 查看分片状态
    GET _cat/shards?v

    文档的 CRUD 与批量操作

    7.0 开始, 索引的 type 固定使用 _doc, 索引和 type 的关系固定是 一对一.

    create 创建一个文档

    指定文档 Id

    PUT 索引名/_create/文档Id
    {
        "key1": "value1",
        ...
    }
    • 若对应的文档 Id 已存在, 会失败.

    自动生成文档 Id

    POST 索引名/_doc
    {
        "key1": "value1",
        ...
    }

    get 一个文档

    GET 索引名/_doc/文档Id

    文档存在则返回 HTTP 200, 不存在返回 HTTP 404

    返回的信息包含两部分

    • 文档元数据(meta data)

      • _index / _type: 文档所属索引和type
      • _version: 版本信息, 同一个 Id 的文档, 即使被删除, Version 号也会不断增加.
    • 文档原始数据

      • _source: 默认包含了文档的所有原始信息

    index 一个文档

    Index 与 Create 不一样, Index 的行为:

    • 如果文档不存在, 就索引新的文档.
    • 如果文档已存在, 会删除旧文档, 然后索引新的文档, 并且版本号 _version 会 +1

      Create 这种情况是直接返回错误.
    PUT 索引名/_doc/文档Id
    {
        "key": "value",
        ...
    }
    • 实际使用 POST 索引名/_doc/文档Id 效果也一样, 猜测这两个是等同的.

    查询参数

    • op_type

      • index(默认): 索引文档
      • create: create文档, 等同 PUT 索引名/_create/文档Id

    update 一个文档

    Update 行为

    • 不会删除原来的文档, 而是实现真正的数据更新

      _version 也会递增.
    • 主要用于部分更新文档
    // 可以理解为 PATCH(不过 ES 好像不支持这个)
    POST 索引/_update/文档Id
    {
        "doc": {
            "key": "value",
            ...
        }
    }
    payload 部分要包含在 doc 字段中

    _update_by_query 更新多个文档

    https://www.elastic.co/guide/...

    POST my-index-000001/_update_by_query?conflicts=proceed
    
    
    POST my-index-000001/_update_by_query?conflicts=proceed
    {
      "query": { 
        "term": {
          "user.id": "kimchy"
        }
      }
    }
    
    
    POST my-index-000001/_update_by_query
    {
      "script": {
        "source": "ctx._source.count++",
        "lang": "painless"
      },
      "query": {
        "term": {
          "user.id": "kimchy"
        }
      }
    }

    delete 一个文档

    DELETE 索引/_doc/文档Id

    _delete_by_query 删除多个文档

    https://www.elastic.co/guide/...

    POST /my-index-000001/_delete_by_query
    {
      "query": {
        "match": {
          "user.id": "elkbee"
        }
      }
    }
    
    POST /metricbeat-7.9.3/_delete_by_query?conflicts=proceed
    {
      "query": {
        "match_all":{}
      }
    }

    bulk 批量操作

    // 这是一个示例
    // 以下的 index, delete, create, update 都是一种操作, 除了 delete 外, 其他的要紧跟着一行表示操作的内容.
    POST _bulk
    { "index": {"_index":"test", "_id": "1"}}
    { "field1": "value1" }
    { "delete": { "_index":"test","_id":"2"}}
    { "create": { "_index":"test2", "_id": "3"}}
    { "field1": "value3" }
    { "update": {"_index":"test", "_id": "1"}}
    { "doc": {"field2":"value2"}}
    • 支持在一个 API 调用中, 对不同的索引进行操作

      POST 索引名/_bulk
      ...
    • 可以在 URI 中指定要操作的索引, 也可以在请求中的 _index 指定

      _index

      若同时在 URL 和 Payload 中都指定了索引, 则以 Payload 中那一条具体操作指定的索引为准.

    • 支持四种类型操作

      • index
      • create
      • delete
      • update
    • 每一条操作的执行不影响其他操作
    • 批量操作也需要避免一次操作太多, 导致给 ES 集群压力太大.
    • 返回结果中包含各个操作的结果

      {
        "took" : 2089,
        "errors" : false,
        "items" : [
            // 操作结果
            ...
        ]
      }

    mget 批量读取

    批量操作可以减少网络连接所产生的开销, 提高性能.

    // 这是一个示例
    GET _mget
    {
      "docs": [
        {"_id":"1"},
        ...
      ]
    }
    • 可以在 URI 中指定要操作的索引, 也可以在请求的 Payload 中的 _index 指定
    • 返回结果包含所有文档内容

      {
        "docs" : [
            // 各个文档(当然也有可能是没找到)
            ...
        ]
      }

    msearch 批量查询

    // 示例
    POST _msearch
    {"index":"kibana_sample_data_ecommerce"}
    {"query":{"match_all":{}},"size":1}
    {"index":"kibana_sample_data_flights"}
    {"query":{"match_all":{}},"size":2}
    • 可以在 URI 中指定要操作的索引, 也可以在请求的 Payload 中的 _index 指定

    常见错误返回

    倒排索引介绍

    倒排索引的核心组成

    这里没有局限在 ES, 而是一个通用的倒排索引的核心组成.

    倒排索引主要包含两个部分

    1. 单词词典(Term Dictionary): 记录所有文档的单词, 记录单词到倒排列表的关联关系

      • 单词词典一般比较大, 可以通过 B+ 树或哈希拉链法实现, 以满足高性能的插入与查询
    2. 倒排列表(Posting List)

      • 倒排索引项(Posting)

        • 文档 ID
        • 词频 TF - 该单词在文档中出现的次数, 用于相关性评分
        • 位置 Position - 单词在文档中分词的位置. 用于语句搜索 (phrase query)

          注意这是单词的位置, 而不是字节/字符位置.
        • 偏移 Offset - 记录单词的开始结束位置, 实现高亮显示

          字节/字符位置.

    ElasticSearch 的倒排索引

    • ElasticSearch 的 JSON 文档中的每个字段, 都有自己的倒排索引.
    • 可以指定对某些字段不做索引

      • 优点: 节省存储空间
      • 缺点: 字段无法被搜索

    通过 Analyzer 进行分词

    Analysis 与 Analyzer

    • Analysis([ə'næləsɪs]): 分析器. 文本分析把全文本转换成一系列单词(term / token) 的过程(这个操作也叫分词).

      • Analysis 使用 Analyzer 来实现具体的分词.
      • 可使用 ES 内置的分析器, 或按需定制分析器.
      • 除了数据写入时转换词条, 匹配 Query 语句时也需要用相同的分析器对查询语句进行分析.
    • Analyzer(['ænəˌlaɪzə] ): 分词器.

      • Character Filters
      • Tokenizer
      • Token Filter

    Analyzer 分词器

    Analyzer 的组成部分

    Analyzer 分词器是专门处理分词的组件.

    Analyzer 主要有三部分组成

    1. Character Filters

      针对原始文档处理, 例如去除 html

    2. Tokenizer

      按照规则切分为单词

    3. Token Filter

      将切分的单词进行加工, 小写, 删除 stopwords, 增加同义词等操作.

    Analyzer = CharFilters(0个或多个) + Tokenizer(恰好一个) + TokenFilters(0个或多个)

    _analyze API

    • 测试某个 Analyzer 的分词效果

      GET _analyze
      {
          "analyzer": "具体分词器名字, 比如: standard",
          "text": "待测试的文本内容"
      }
    • 测试某个索引的字段的分词效果

      POST 索引名/_analyze
      {
          "field": "字段名",
          "text": "..."
      }
    • 测试自定义分词器的效果

      POST _analyze
      {
          "char_filter": [],
          "tokenizer": "指定某个 tokenizer, 比如: standard",
          "filter": ["指定 Token Filter, 比如: lowercase"],
          

    }

    
    
    
    
    
    
    
    #### ElasticSearch 的内置分词器
    
    https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-analyzers.html
    
    - Standard Analyzer - 默认分词器,按词切分,小写处理
    - Simple Analyzer - 按照非字母切分(符号被过滤), 小写处理
    - Stop Analyzer - 小写处理,停止词过滤(the,a,is)
    - Whitespace Analyzer - 按照空格切分,不转小写
    - Keyword Analyzer - 不分词,直接将输入当作输出
    - Patter Analyzer - 正则表达式,默认\W+(非字符分割)
    - Language - 提供了30多种常见语言的分词器
    - Customer Analyzer 自定义分词器
    
    
    
    ##### standard (默认的分词器)
    
    行为
    
    1. 按词切分
    2. 小写处理
    3. 默认未启用 Stop 停止词过滤
    
    
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200909115256.png)
    
    
    

    GET _analyze
    {
    "analyzer": "standard",
    "text": "............."
    }

    
    
    
    ##### simple
    
    行为
    
    1. 按照非字母切分
    2. 去除非字母部分
    3. 小写处理
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200909115901.png)
    

    GET _analyze
    {
    "analyzer": "simple",
    "text": "......."
    }

    
    
    
    ##### whitespace
    
    行为
    
    1. 按照空白切分
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200909120156.png)
    

    GET _analyze
    {
    "analyzer": "whitespace",
    "text": "........"
    }

    
    
    
    ##### stop
    
    > stop 分词器在 simple 分词器基础上增加了 stop filter.
    
    1. 按照非字母切分
    
    2. 去除非字母部分
    
    3. 小写处理
    
    4. 去除 stop (停止词) 部分
    
       > 会把 the, a , is, in, ... 等修饰性词语去除
    
    
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200909140204.png)
    

    GET _analyze
    {
    "analyzer": "stop",
    "text": "....."
    }

    
    
    
    ##### keyword
    
    行为
    
    1. 不分词
    
       直接将输入当做一个 term 输出
    
    
    
    当不需要对输入进行分词时, 可以选择 keyword 分词器.
    
    
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200708085625.png)
    

    GET _analyze
    {
    "analyzer": "keyword",
    "text": "..."
    }

    
    
    
    
    
    ##### pattern
    
    行为
    
    1. 通过正则表达式分词
    
       > 默认是 `\W+`, 即 `[^a-zA-Z0-9_]`, 对非单词字符进行分割
    
    
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200909140833.png)
    

    GET _analyze
    {
    "analyzer": "pattern",
    "text":"..."
    }

    
    
    
    ##### language
    
    
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200909141058.png)
    

    GET _analyze
    {
    "analyzer": "english",
    "text": "..." // running 会转为 run, foxes 复数转化为单数的 fox, evening 转化为 even, ....
    }

    
    
    
    #### 中文分词
    
    ES 默认提供的分词对于中文不友好, 只会将中文句子切分成一个一个"字", 而不是"词".
    
    
    
    
    
    ##### ⭐IK
    
    https://github.com/medcl/elasticsearch-analysis-ik
    
    - 支持自定义词库, 支持热更新分词字典
    - 提供
      - Analyzer: `ik_smart` , `ik_max_word` 
      - Tokenizer: `ik_smart` , `ik_max_word`
    
    
    
    安装
    

    ELK_VERSION=7.9.1

    bin/elasticsearch-plugin install "https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v${ELK_VERSION}/elasticsearch-analysis-ik-${ELK_VERSION}.zip"

    7.9.3

    elasticsearch-plugin install "https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.9.3/elasticsearch-analysis-ik-7.9.3.zip"

    
    
    
    如果上面的安装方式不行, 那么可以尝试用解压缩的方式直接安装(具体参见 ik 项目的 markdown)
    
    
    
    ##### THULAC
    
    > ... ES 版的都没在维护, 忽略吧
    
    https://github.com/microbun/elasticsearch-thulac-plugin
    
    - THU Lexucal Analyzer for Chinese, 清华大学自然语言处理和社会人文计算实验室的一套中文分词器
    
    
    
    
    
    #####  ICU
    
    提供了 Unicode 的支持, 更好的支持亚洲语言
    
    
    
    安装
    

    bin/elasticsearch-plugin install analysis-icu

    
    
    
    ## SearchAPI 概览
    
    ### 指定查询的索引
    
    ![image-20200909154042030](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200909154042.png)
    
    
    
    ### Search API 类型
    
    ES 主要提供两种 Search API
    
    - URI Search
    - Request Body Search
    
    
    

    // 搜索所有索引
    GET /_search

    // 搜索多个索引(逗号分隔)
    GET /index1,index2/_search

    // 搜索符合命名的多个索引
    // * 是通配符
    GET /indexname*/_search

    // 搜索单个索引
    GET /indexname/_search

    
    
    
    
    
    #### URI Search
    
    在 URL 中使用查询参数
    
    - 使用 `q` 指定查询字符串
    - "query string syntax", KV 键值对
    
    
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200909154136.png)
    
    
    
    #### Request Body Search
    
    使用 ES 提供的基于 JSON 格式的更加完备的 DSL(领域查询语言)
    
    
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200909154156.png)
    
    
    
    ### 搜索结果部分字段说明
    
    返回结果中部分字段说明
    
    - `took`: 花费的时间
    - `hits.total`: 符合条件的总文档数
    - `hits.hits`: 结果集, 默认返回前 10 个文档
    - `hits.hits.$._score`: 文档的相关度评分
    
    
    
    ### 搜索结果的相关性 Relevance
    
    衡量相关性
    
    - Precision(查准率)
    
      无关文档越少越好
    
    - Recall(查全率)
    
      相关的文档越多越好
    
    - Ranking
    
      能够按照相关度排序
    
    
    
    使用 ES 的查询和相关的参数可以改善搜索的 Precision 和 Recall
    $$
    Precision = \frac{True Positive}{实际的返回结果集}
    $$
    
    $$
    Recall = \frac{True Positive}{所有应返回的结果集}
    $$
    
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200909155351.png)
    
    
    
    ## URISearch 详解
    
    ### 通过 URI Query 实现搜索
    

    GET movies/_search?q=2012&df=title&sort=year:desc&from=0&size=10&timeout=1s
    {
    "profile": "true"
    }

    
    - `q` 指定查询语句, 使用 Query String Syntax
    - `df` 默认字段, 不指定时会对所有字段进行查询
    - `sort` 排序
    - `from` 和 `size` 用于分页
    - `profile: true`: 输出查询的执行计划
    
    
    
    
    
    ### Query String Syntax
    
    > 以下以查询 `movies` 索引为例.
    
    
    
    - 指定字段 vs 泛查询
    

    // 泛查询(对所有字段应用各种类型的查询)
    GET movies/_search?q=2012

    // 指定字段
    GET movies/_search?q=title:2012

    
    - Term vs Phrase
    

    // 其中的 "Mind" 部分是泛查询(即此时是对文档中所有字段查询 "Mind")
    GET movies/_search?q=title:Beautiful Mind

    // Phrase查询: 使用引号
    GET movies/_search?q=title:"Beautiful Mind"

    
    - 分组与引号
    

    // 分组, Bool 查询
    GET movies/_search?q=title:(Beautiful Mind)

    // 引号, Phrase
    GET movies/_search?q=title:"Beautiful Mind"

    
    - 布尔操作
    
    AND / OR / NOT 或者 `&&` / `||` / `!`
    
    > 注意必须大写
    

    // 布尔 AND
    // Title 必须同时包含 Beautiful 和 Mind
    GET movies/_search?q=title:(Beautiful AND Mind)

    // 布尔 NOT
    // Title 必须包含 Beautiful, 但不能包含 Mind
    GET movies/_search?q=title:(Beautiful NOT Mind)

    
    - 分组
    
    - `+` 表示 must
    - `-` 表示 must_not
    

    // 布尔 must
    // Title 必须包含 Mind
    // %2B 是加号的 urlencode
    GET movies/_search?q=title:(Beautiful %2BMind)&sort=_score:asc

    
    - 范围查询
    
    - `[]` 闭区间
    - `{}` 开区间
    

    GET movies/_search?q=year:[* TO 2018]

    
    - 算术符号
    

    GET movies/_search?q=year:>=1980

    GET movies/_search?q=year:(>=1980 AND <=2018)

    GET movies/_search?q=year:(%2B>1980 %2B<=2018)

    
    - 通配符查询
    
    > 查询效率低, 占用内存大, 不建议使用(特别是放在最前面)
    
    - `?` 表示1个字符
    - `*` 表示任意个字符
    

    // 通配符匹配
    GET movies/_search?q=title:b*

    
    - 正则表达式
    

    GET movies/_search?q=title:/[abc]eautifu.?/

    
    - 模糊匹配与近似查询
    

    // 模糊匹配&近似度匹配
    // 这里输入 beautifl 是一个错误的单词, 但通过近似度匹配能够匹配到
    // 这里的 ~1 表示允许有一个字母有差别, 即 match_phrase 的 slop:1
    GET movies/_search?q=title:beautifl~1

    // 模糊匹配&近似度匹配
    // ~2 即 match 的 minimum_should_match: 2
    GET movies/_search?q=title:"Lord Rings"~2

    
    
    
    
    
    ## Request Body Search
    
    将查询语句通过 HTTP Request Body 发送给 ES, 这种方式有特定的 DSL.
    
    
    
    - 分页
    
    - `from` 从 0 开始
    - `size` 默认是 10
    - 获取靠后的翻页成本较高
    

    POST kibana_sample_data_ecommerce/_search
    {

    "from": 10,
    "size": 20,
    "query": { "match_all": {}}

    }

    
    - 排序  `sort`
    
    - 最好在 "数字型" 与 "日期型" 字段上排序
    - 因为对于多值类型或分析过的字段排序, 系统会选一个值, 无法得知该值.
    

    POST kibana_sample_data_ecommerce/_search
    {

    "sort": [{"order_date": "desc" }],
    "query": { "match_all": {}}

    }

    
    - `_source` filtering
    
    > source filte 只是传输给客户端时进行过滤, 在 ES 节点间的数据传输(Fetch 等)还是会传输 _source 中的数据
    
    - 如果 _source 没有存储, 那就只返回匹配的文档的元数据
    - _source 支持使用通配符  ` _source: ["name*", "desc*"]`
    

    POST kibana_sample_data_ecommerce/_search
    {

    "_source": ["order*", "customer*"],
    "query": {"match_all": {}}

    }

    
    - 脚本字段 `script_fields`
    

    POST kibana_sample_data_ecommerce/_search
    {

    "script_fields": {
      "新字段名": {
        "script": {
          "lang": "painless",
          "source": "doc['currency'].value + ' ' + doc['taxful_total_price'].value"
        }
      }
    }, 
    "_source": ["currency", "taxful_total_price"], 
    "query": {"match_all": {}}

    }

    
    
    
    ### Match Query
    
    查询表达式 Match
    
    - operator
    
    match 中的项(terms)之间, 默认的关系是 OR
    
    https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html
    

    POST movies/_search
    {
    "query": {

    "match": {
      "title": {
          "query": "Last Christmas"        // 此时解释为 Last OR Christmas
      }
    }

    }
    }

    // 上面查询的简写
    POST movies/_search
    {
    "query": {

    "match": {
      "title": "Last Christmas"
    }

    }
    }

    // 完整书写
    POST movies/_search
    {
    "query": {

    "match": {
      "title": {
        "query": "Last Christmas",        // 此时解释为 Last AND Christmas
        "operator": "AND"                // 默认是 OR
      }
    }

    }
    }

    
    
    
    ### Match Phrase Query
    
    短语搜索 Match Phrase
    
    > 这也是属于全文搜索
    
    match_phrase 的 terms 之间是 and 的关系, 并且 terms 之间位置关系也影响搜索的结果
    

    POST movies/_search
    {
    "query": {

    "match_phrase": {
      "title": {
        "query": "one love",
        "slop": 1                   // 默认是 0
      }
    }

    }
    }

    
    
    
    
    
    match 与 match_phrase 的区别
    
    - match 中的 terms 之间是 or 的关系
    - match_phrase 的 terms 之间是 and 的关系, 并且 terms 之间位置关系也影响搜索的结果
    
    
    
    ## QueryString & SimpleQueryString 查询
    
    ### Query String
    
    类似 URI Query
    
    
    

    POST users/_search
    {
    "query": {

    "query_string": {
      "default_field": "name",
      "query": "Ruan AND yiMing"
    }

    },
    "profile": "true"
    }

    POST users/_search
    {
    "query": {

    "query_string": {
      "fields": ["name", "about"], 
      "query": "(Ruan AND yiMing) OR (Java AND Elasticsearch)"
    }

    },
    "profile": "true"
    }

    
    
    
    ### Simple Query String
    
    - 类似 Query String, 但会忽略部分错误的语法, 同时只支持部分查询语法
    - 不支持查询文本中的 `AND`, `OR`, `NOT`, 这些会被视为普通字符串处理
    - Term 之间的默认关系是 OR, 可以指定 Operator
    - 支持部分逻辑
      - `+` 替代 AND
      - `-` 替代 NOT
      - `|` 替代 OR
    
    
    

    // Simple Query 默认的 operator 是 OR
    // 会忽略查询内容中的 AND, OR
    POST users/_search
    {
    "query": {

    "simple_query_string": {
      "fields": ["name", "about"], 
      "query": "Ruan AND yiMing"
    }

    },
    "profile": "true"
    }

    POST users/_search
    {
    "query": {

    "simple_query_string": {
      "fields": ["name", "about"], 
      "query": "Ruan  yiMing",
      "default_operator": "AND"
    }

    },
    "profile": "true"
    }

    
    
    
    ## DynamicMapping 和常见字段类型
    
    ### Mapping
    
    Mapping 类似数据库中的 schema 的定义, 作用如下:
    
    - 定义索引中的字段的名称
    - 定义字段的数据类型, 例如字符串, 数字, 布尔...
    - 字段、倒排索引的相关配置, (Analyzed or Not Analyzed,Analyzer)
    
    
    
    Mapping 会把 JSON 文档映射成 Lucene 所需要的扁平格式.
    
    
    
    一个 Mapping 属于一个索引的 Type
    
    - 每个文档属于一个 Type
    - 每个 Type 都有一个 Mapping 定义
    
    > 7.0 开始, 一个索引只能有一个默认的 Type(_doc).
    
    
    
    
    
    ### Dynamic Mapping
    
    - 在写入文档时, 如果索引不存在, 会自动创建索引
    
    - Dynamic Mapping 的机制, 使得我们无需手动定义 Mappings.  ES 会自动根据文档信息, 推算出字段的类型
    
      > 但有时候会推算错误, 例如地理位置信息
    
    - 当类型设置不对时, 会导致一些功能无法正常运行, 例如 Range 查询.
    
    
    
    **类型的自动识别**
    
    | JSON 类型 | ES 类型                                                      |
    | --------- | ------------------------------------------------------------ |
    | 字符串    | 匹配日期格式(`2018-07-24T10:29:48.103Z`), 会识别成 Date<br>匹配数字, 默认视为字符串, 可通过配置识别为 float 或 long<br>默认设置为 Text, 并且增加 keyword 子字段 |
    | 布尔值    | boolean                                                      |
    | 浮点数    | float                                                        |
    | 整数      | long                                                         |
    | 对象      | Object                                                       |
    | 数组      | 由第一个非空数值的类型所决定                                 |
    | 空值      | 忽略                                                         |
    
    
    
    
    
    ### 字段的数据类型
    
    数据类型
    
    - 简单类型
      - `text` / `keyword`
      - `date`
      - `integer` / `float` / `byte` / `short` /  `scaled_float` / `half_float`
      - `long` / `double`
      - `boolean`
      - IPv4 & IPv6
    - 复杂类型 - 对象和嵌套对象
      - 对象类型 `object` / 嵌套类型 `nested`
    - 特殊类型
      - `geo_point` & `geo_shape` / percolator
    
     
    
    
    
    ### 能否更改 Mapping 的字段类型
    
    **需要分两种情况**
    
    1. 新增加字段
    
       - dynamic = true 时, 一旦有新增文档的字段写入, mapping 也会同时更新(指新增该字段)
       - dynamic = false 时, mapping 不会被更新, 此时新增的字段不会被索引, 但这些字段信息会出现在 _source 中
       - dynamic = "strict" 时, 文档写入直接失败.
    
    2. 对于已有字段
    
       - 一旦有数据写入, 就不再支持修改字段定义
    
         > Lucene 实现的倒排索引, 一旦生成后, 就不允许修改
    
    如果需要改变字段类型, 必须 Reindex API 以重建索引. 
    
    
    
    **原因**
    
    - 如果修改了字段的数据类型, 会导致已被索引的字段无法被索引
    - 但如果是增加新的字段, 就不会有这样影响
    
    
    
    ### 控制 Dynamic Mappings
    
    | dynamic 的值   | true | false | "strict" |
    | -------------- | ---- | ----- | -------- |
    | 文档可索引     | Y    | Y     | N        |
    | 字段可索引     | Y    | N     | N        |
    | Mapping 被更新 | Y    | N     | N        |
    
    - 当 dynamic 设置成 false 时, 包含新字段的文档可以被写入, 但仅 mapping 中已存在的字段会被索引, 新增字段不会被索引.
    - 当 dynamic 设置成 "strict" 时, 包含新字段的文档写入直接出错.
    
    
    

    // 将 dynamic 设置为 false (默认是 true)
    // 此时后续新增的字段不会被自动索引
    PUT 索引名/_mapping
    {
    "dynamic": false
    }

    // 将 dynamic 设置为 strict
    PUT 索引名/_mapping
    {
    "dynamic": "strict"
    }

    
    
    
    ## 显式设置 Mapping 设置与常见参数
    

    PUT 索引名
    {
    "mappings": {

    "properties": {
      // 定义属性字段
        "属性名": {
            "type": "类型",
            // ... 其他属性配置
        }
    },
      
    // 定义其他 mapping 设置
    ...

    }
    }

    
    
    
    ### 自定义 Mapping 的一些建议
    
    - 参考 API 手册, 纯手写
    - 为了减少输入的工作量, 减少出错概率, 可以依照以下步骤
      1. 创建一个临时的 index, 写入一些样本数据
      2. 通过访问 Mapping API 获得该临时文件的动态 Mapping 定义
      3. 修改上述获得的 Mapping 定义后, 使用该配置创建自己的索引
      4. 删除临时索引
    
    
    
    ### 字段属性设置
    
    https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-params.html
    
    
    
    所有的字段属性
    
    
    
    #### `enabled` 配置项
    
    默认为 true.
    
    当设置为 false 时, 该字段仅做存储(保存于`_source`), 但不支持对该字段搜索和聚合分析.
    
    
    
    使用场景示例:
    
    - 使用 ES 存储 web session, 仅需要对 session_id 和 last_updated 这两个字段进行搜索, 但对于具体的 session 数据不需要查询及聚合操作, 此时就可以将 session data 的 enabled 设置为 false.
    

    PUT my-index-000001
    {

    "mappings": {
      "properties": {
        "user_id": {
          "type":  "keyword"
        },
        "last_updated": {
          "type": "date"
        },
        "session_data": { 
          "type": "object",
          "enabled": false
        }
      }
    }

    }

    
    
    
    
    
    #### `index` 配置项
    
    控制当前字段是否被索引(倒排索引), 默认为 true.
    
    若设置为 false, 则该字段不会被索引, 无法被搜索到(仍可以进行聚合分析)
    
    
    
    这个主要用来避免创建索引, 节省空间.
    

    PUT 索引名
    {
    "mappings": {

    "properties": {
      "属性名":{
        "type": "...",
        "index": false            // 此时该字段不会被索引.
      }
    }

    }
    }

    
    
    
    #### `norms` 配置项
    
    该配置项用于控制是否存储算分相关数据, 默认为 true.
    
    若该字段不需要算分(score), 而仅仅是用于过滤(filter)或聚合分析(Aggregation), 那么可以关闭(设置为 false)以节约存储.
    
    
    
    
    
    #### `doc_values` 配置项
    
    > 排序, 聚合, 在Script中访问字段值需要不同于倒排索引的数据访问模式(需要获取该文档中该字段的值是多少, 而不仅仅是倒排索引中提供的哪个文档包含该字段), ES 提供了 doc value 和 fielddata 这两种方式来实现这一数据访问模式.
    
    该配置项用于控制是否创建对应的 Doc Value, 对于大部分字段类型都有效(除了 text 和  annotated_text), 默认为 true.
    
    Doc Value 是存放在磁盘上的数据结构(理解为正排索引即可), 当文档索引时创建, 用于支持排序和聚合分析.
    
    > text 和 annotated_text 需手动设置 field_data 为 true 才可使用.
    
    
    
    若该字段不需要排序, 不需要聚合, 不需要通过Script访问字段值时, 可以将其设为 false 以节省空间并提高性能.
    
    
    
    #### `fielddata` 配置项
    
    > 排序, 聚合, 在Script中访问字段值需要不同于倒排索引的数据访问模式(需要获取该文档中该字段的值是多少, 而不仅仅是倒排索引中提供的哪个文档包含该字段), ES 提供了 doc value 和 fielddata 这两种方式来实现这一数据访问模式.
    
    该配置项用于控制是否启用 fielddata 这一数据结构, 该数据结构是在查询时才创建的仅存放在内存中的数据结构,  fielddata 在 text 类型的字段上默认为 false.
    
    > 这里的查询指: 该字段第一次用于排序, 聚合, 以及在脚本中使用时创建的. 
    
    
    
    
    
    fielddata 会消耗大量的 heap 空间, 且消耗额外的性能使得延迟上升, 因此对于 text 默认是不开启的.
    
    在对 text 类型字段开启 fielddata 前需要谨慎考虑, 看能否用其他方案来代替: 比如多字段(text + keyword 子字段的组合)
    
    
    
    #### `store` 配置项
    
    是否额外存储该字段的值, 默认是不存储的.
    
    > 但是ES中的字段通常会存储在 _source 中.
    
    
    
    #### `coerce` 配置项
    
    是否开启数据类型的自动转换(例如字符串转数字), 默认开启.
    
    
    
    
    
    #### `multifield` 配置项
    
    多字段特性
    
    
    
    #### `dynamic`
    
    true(默认)/false/strict
    
    
    
    控制 Mapping 的自动更新规则
    
    
    
    #### `index_options` 配置项
    
    - Index Options 选项可以控制倒排索引记录的内容. 不同级别记录的内容不一样:
      - `docs` 记录 doc id
    
      - `freqs` 记录 doc id 和 term frequencies
    
      - `positions` 记录 doc id / term frequencies / term position
    
      - `offsets` 记录 doc id / term frequencies / term position / character offsets
    
        > 我的理解是, 若是为了高亮搜索结果中的匹配字段, 则需要 character offsets 信息
    
    - Text 字段类型默认记录 `positions` 级别, 其他默认为 `docs`
    
    - 记录的内容越多, 占用存储空间越大
    

    PUT 索引名
    {
    "mappings": {

    "properties": {
      "属性名":{
        "type": "...",
        "index_options": "offsets"
      }
    }

    }
    }

    
    
    
    #### `null_value` 配置项
    
    - 当需要对 null 值实现搜索时可以配置该项
    
      > 索引文档时, 字段的值为 null, 默认会被忽略.
    
    - 只有 keyword 类型支持设置该配置项
    

    PUT 索引名
    {
    "mappings": {

    "properties": {
      "属性名":{
        "type": "keyword",
        "null_value": "NULL"
      }
    }

    }
    }

    POST 索引/_doc
    {
    "属性名": null // 若为 null, 默认情况下该字段会被忽略. 在配置了 null_value 后, 其值被视为 "NULL" 字符串(但 _source 中保存的是 null)
    }

    GET 索引/_search
    {
    "query": {

    "match": {
      "属性名": "NULL"                // 搜索时, 使用 "NULL" 表示搜索 null 值
    }

    }
    }

    
    
    
    #### `copy_to` 配置项
    
    > _all 在 ES 7 被 copy_to 所替代
    
    - 该配置项用于满足一些特定的搜索需求
    - copy_to 将字段的数值拷贝到目标字段, 实现类似 _all 的作用
    - copy_to 的目标字段不会出现在 _source 中
    

    PUT 索引名
    {
    "mappings": {

    "properties": {
      "属性名":{
        "type": "...",
        "copy_to": "另一个属性名"
      }
    }

    }
    }

    GET users/_search
    {
    "query": {

    "match": {
      "另一个属性名":"..."
    }

    }
    }

    
    
    
    
    
    ES 中不提供专门的数组类型, 但是任何字段都可以包含多个同类型的数值.
    
    示例:
    

    PUT users/_doc/1
    {
    "name":"onebird",
    "interests":["music","reading"]
    }

    // 其对应的 mapping 如下
    {
    "users" : {

    "mappings" : {
      "properties" : {
        "interestst" : {
          "type" : "text",                // dynamic mapping 设置该字段是 "text" 类型.
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "name" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        }
      }
    }

    }
    }

    
    
    
    ## 多字段特性及Mapping中配置自定义Analyzer
    
    ### 多字段特性
    
    默认的 dynamic mapping 会为 text 字段类型增加一个 keyword 子字段.
    
    
    
    多字段提供的特性
    
    - 可以用于实现精确匹配
    - 可以使用不同的 analyzer
      - 不同语言
      - pinyin 字段的搜索
      - 支持为搜索和索引指定不同的 analyzer
    
    
    
    
    
    
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200910165321.png)
    
    - 比如 "company" 存入的是 "厦门市XX网络有限公司", 对于 text 字段分词(ik_smart)后是 "厦门市", "XX", "网络", "有限公司", 那么使用 term 对 `company` 字段查询 "厦门市XX网络有限公司" 是无法获取到结果的. 但是使用 term 对 `company.keyword` 字段查询 "厦门市XX网络有限公司" 则可以精确匹配到.
    
    
    
    ### 精确值(Exact Values) vs 全文本(Full Text)
    
    Exact Values
    
    - 数字 / 日期 / 具体的一个字符串(例如 "Apple Store")
    - ES 中的 keyword
    - Exact Value 字段在索引时不需要做特殊的分词处理.
    
    
    
    全文本
    
    - 非结构化的文本数据
    - ES 中的 text
    
    
    
    
    
    示例图
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200910165654.png)
    
    - 其中只有 "message" 字段需要全文本匹配
    
    
    
    
    
    ### 自定义分词器
    
    当 [ES 自带的分词器](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-analyzers.html)无法满足时, 可以自定义分词器: 组合不同的组件.
    
    
    
    Analyzer = CharFilters(0个或多个) + Tokenizer(恰好一个) + TokenFilters(0个或多个)
    
    
    
    示例
    

    PUT 索引名
    {
    "settings": {

    "analysis": {
      "analyzer": {
        "my_custom_analyzer":{
          "type": "custom",
          "char_filter": [
            "emoticons"
          ],
          "tokenizer": "punctuation",
          "filter": [
            "lowercase",
            "english_stop"
          ]
        }
      },
      "char_filter": {
        "emoticons": {
          "type": "mapping",
          "mappings": [":) => _happy_", ":( => _sad_"]
        }
      },
      "tokenizer": {
        "punctuation":{
          "type": "pattern",
          "pattern": "[ .,!?]"
        }
      },
      "filter": {
        "english_stop":{
          "type":"stop",
          "stopwords": "_english_"
        }
      }
    }

    }
    }

    
    
    
    
    
    示例
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200910172145.png)
    
    
    
    #### Character Filters
    
    在 Tokenizer 之前对文本进行处理, 例如增加删除及替换字符.  会影响 Tokenizer 的 position 和 offset 信息.
    
    可以配置多个 Character Filters.
    
    https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-charfilters.html
    
    
    
    部分自带的 Character Filters
    
    - `html_strip`: 去除 html 标签
    - `mapping`: 字符串替换
    - `patter_replace`: 正则匹配替换
    
    
    

    POST _analyze
    {
    "char_filter": [

      // 去除 html 标签
      "html_strip",
      
      // 字符串替换
      {
        "type": "mapping",
        "mappings": ["- => _", ":) => happy", ":( => sad"]
      },
      
      // 正则匹配替换
      {
        "type": "pattern_replace",
        "pattern": "https?://(.*)",
        "replacement": "$1"
      }

    ],
    "tokenizer": "...",
    "filter": [...],
    "text": "......"
    }

    
    
    
    #### Tokenizer
    
    将原始的文本按照一定的规则, 切分为词(term or token).
    
    可以用 Java 开发插件实现自己的 Tokenizer.
    
    https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenizers.html
    
    
    
    
    
    ES 内置的部分 Tokenizers
    
    - whitespace
    
    - standard
    
    - uax_url_email
    
    - pattern: 正则分词
    

    POST _analyze
    {

    "tokenizer": {
      "type": "pattern",
      "pattern": "[ .,!?]"
    },
    "text": "..."

    }

    
    - keyword
    
    - path hierarchy: 文件路径分词
    
    确保整个路径上任意一个路径都可以匹配到.
    

    POST _analyze
    {

    "tokenizer": "path_hierarchy",
    "text": "/user/path/to/php"

    }

    
    
    
    
    
    #### Token Filters
    
    将 Tokenizer 输出的单词(term) 进行增加, 修改, 删除
    
    https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenfilters.html
    
    
    
    
    
    自带的部分 Token Filters
    
    - lowercase: 转小写
    
    - stop: 移除停止词
    
    - synonym: 添加近义词
    
    https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-synonym-tokenfilter.html
    
    
    

    POST _analyze
    {
    "filter": ["lowercase", "stop"],
    "text": "/user/path/to/php"
    }

    
    
    
    ## IndexTemplate 和 DynamicTemplate
    
    > 生产环境中, 索引应考虑禁止 Dynamic Index Mapping,避免过多字段导致 Cluster State 占用过多.
    >
    > 同时应禁止索引自动创建的功能,创建时必须提供 Mapping 或通过 Index Template 进行设定
    >
    > ```
    > // 禁止自动创建索引
    > action.auto_create_index: false
    > ```
    >
    > 
    
    
    
    
    
    ### Index Template: 应用于新增索引
    
    Index Template(索引模板) 可应用于符合条件的所有新创建索引.
    
    - index template 仅对新创建的索引有效, 不会影响已存在的 index
    - 可设定多个 index template, 这个设置会合并, 按照 order 从低到高依次覆盖.
    
    
    
    当一个索引被新创建时:
    
    1. 应用 ES 默认的 settings 和 mappings
    2. 应用 order 数值低的 Index Template 中的设定
    3. 应用 order 高的 index template 中的设定, 依次覆盖
    4. 应用创建索引时, 用户手动指定的 Settings 和 Mappings 会覆盖之前模板中的设定
    
    
    
    
    

    // 对所有新创建的索引生效(但好像 "*" 这样的设置有问题, ES 7.9.1 版本的一直提示 deprecated 啥的)
    PUT _template/template_default
    {
    "index_patterns": ["*"],
    "order": 0,
    "version": 1,
    "settings": {

    "number_of_shards": 1,
    "number_of_replicas": 1

    }
    }

    // 创建的 index template 可对新创建的名为 "test*" 的索引生效
    PUT _template/template_test
    {
    "index_patterns": ["test*"],
    "order": 1,
    "version": 1,
    "settings": {

    "number_of_shards": 1,
    "number_of_replicas": 2

    },
    "mappings": {

    "date_detection": false,
    "numeric_detection": true

    }
    }

    
    
    
    ### Dynamic Template: 应用于新增字段
    
    Dynamic Template 支持在具体的索引上指定规则, 为新增加的字段指定相应的 Mappings
    
    具体是定义在某个索引的 `mappings.dynamic_templates` 中, 可以根据 ES 识别的数据类型, 结合字段名称来动态设定字段类型.
    
    
    
    - template 有名称
    - 匹配规则是一个数组
    - 可以为匹配到的字段设置 mapping
    
    
    
    比如
    
    - 将指定数据类型的统一设定为其他类型
    - 关闭某一特定类型
    - 字段名称符合通配符匹配的设置为特定类型
    
    
    
    
    
    示例
    

    PUT my_index
    {
    "mappings": {

    "dynamic_templates": [
      {
        "strings_as_boolean": {
          "match_mapping_type": "string",        // 匹配 string 类型
          "match": "is*",                        // 匹配 is 作为字段名前缀的
          "mapping": {
            "type": "boolean"                    // 转换为 boolean 类型
          }
        }
      },
      {
        "strings_as_keywords": {
          "match_mapping_type": "string",
          "mapping": {
            "type": "keyword"
          }
        }
      }
    ]

    }
    }

    PUT my_index/_doc/1
    {
    "firstName": "Ruan", // dynamic mapping 会应用 dynamic template, 令该字段类型为 keyword
    "isGm": "true" // 该字段类型会转化为 boolean
    }

    
    
    
    示例
    

    PUT my_index
    {
    "mappings": {

    "dynamic_templates": [
      {
        "full_name": {
          "path_match": "name.*",                // 匹配 name 对象下的所有字段
          "path_unmatch": "*.middle",            // 不匹配 任意对象下的 middle 字段
          "mapping": {
            "type": "text",
            "copy_to": "full_name"
          }
        }
      }
    ]

    }
    }

    PUT my_index/_doc/1
    {
    "name":{

    "first":"John",                    // text 类型, 并 copy_to "full_name"
    "middle": "Winston",            // 按照默认 dynamic mapping 处理
    "last": "Lennon"                // text 类型, 并 copy_to "full_name"

    }
    }

    
    
    
    ## 聚合分析(Aggregation)简介
    
    > 在下一份笔记中有更详细的介绍.
    
    ES 除了查询外, 还提供了针对 ES 数据进行**高实时性**的统计分析功能.
    
    > 不像 Hadoop 的 T+1 那样处理.
    
    
    
    通过聚合, 我们会得到一个数据的概览, 是分析和总结全套的数据, 而不是寻找单个文档.
    
    > 包括 Kibana 的可视化报表都是利用 ES 的聚合分析功能.
    
    
    
    ### 聚合的方式
    
    - Bucket Aggregation: 一些列满足特定条件的文档的集合
      - 可以包含嵌套关系
      - Term & Range (时间 / 年龄区间 / 地理位置)
    - Metric Aggregation: 一些数学运算, 可以对文档字段进行统计分析
      - Metric 会基于数据集计算结果, 除了支持在字段进行计算, 同样支持在脚本(painless script)产生的结果上进行计算
      - 大多数 Metric 是数学计算, 仅输出一个值
        - min / max / sum / avg / cardinality
      - 部分 metric 支持输出多个数值
        - stats / percentiles / percentile_ranks
    - Pipeline Aggregation: 对其他的聚合结果进行二次聚合
    - Matrix Aggregation: 支持对多个字段的操作并提供一个结果矩阵
    
    
    
    
    
    
    
    ### 示例
    
    #### *示例: Term bucket 聚合*
    

    // 按照目的地进行分桶统计
    GET kibana_sample_data_flights/_search
    {
    "size": 0,
    "aggs": { // 关键词 aggs

    "flight_dest": {                // 自定义名字
      "terms": {                    // 不同类型的分析, terms 是分桶
        "field": "DestCountry"
      }
    }

    }
    }

    // 结果
    {
    "took" : 0,
    "timed_out" : false,
    "_shards" : {

    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0

    },
    "hits" : {

    "total" : {
      "value" : 10000,
      "relation" : "gte"
    },
    "max_score" : null,
    "hits" : [ ]

    },
    "aggregations" : {

    "flight_dest" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 3187,
      "buckets" : [
        {
          "key" : "IT",
          "doc_count" : 2371
        },
        ......
      ]
    }

    }
    }

    
    
    
    #### *示例: Term bucket 聚合 + min/max/avg 数学聚合*
    

    // 查看航班目的地的统计信息, 增加平均, 最高最低价格
    GET kibana_sample_data_flights/_search
    {
    "size": 0,
    "aggs": {

    "flight_dest": {
      "terms": {
        "field": "DestCountry"
      },
      "aggs": {
        "avg_price": {
          "avg": {
            "field": "AvgTicketPrice"
          }
        },
        "max_price": {
          "max": {
            "field": "AvgTicketPrice"
          }
        },
        "min_price": {
          "min": {
            "field": "AvgTicketPrice"
          }
        }
      }
    }

    }
    }

    // 结果
    {
    "took" : 12,
    "timed_out" : false,
    "_shards" : {

    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0

    },
    "hits" : {

    "total" : {
      "value" : 10000,
      "relation" : "gte"
    },
    "max_score" : null,
    "hits" : [ ]

    },
    "aggregations" : {

    "flight_dest" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 3187,
      "buckets" : [
        {
          "key" : "IT",
          "doc_count" : 2371,
          "max_price" : {
            "value" : 1195.3363037109375
          },
          "min_price" : {
            "value" : 100.57646942138672
          },
          "avg_price" : {
            "value" : 586.9627099618385
          }
        },
        .......
      ]
    }

    }
    }

    
    
    
    #### *示例: Term bucket 聚合 + stats 数学聚合*
    

    // 价格统计信息 + 天气信息
    GET kibana_sample_data_flights/_search
    {
    "size": 0,
    "aggs": {

    "flight_dest": {
      "terms": {
        "field": "DestCountry"
      },
      "aggs": {
        "stats_price":{
          "stats": {
            "field": "AvgTicketPrice"
          }
        },
        "weather": {
          "terms": {
            "field": "DestWeather"
          }
        }
      }
    }

    }
    }

    // 结果
    {
    "took" : 2,
    "timed_out" : false,
    "_shards" : {

    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0

    },
    "hits" : {

    "total" : {
      "value" : 10000,
      "relation" : "gte"
    },
    "max_score" : null,
    "hits" : [ ]

    },
    "aggregations" : {

    "flight_dest" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 3187,
      "buckets" : [
        {
          "key" : "IT",
          "doc_count" : 2371,
          "weather" : {
            "doc_count_error_upper_bound" : 0,
            "sum_other_doc_count" : 0,
            "buckets" : [
              {
                "key" : "Clear",
                "doc_count" : 428
              },
              {
                "key" : "Sunny",
                "doc_count" : 424
              },
              {
                "key" : "Rain",
                "doc_count" : 417
              },
              {
                "key" : "Cloudy",
                "doc_count" : 414
              },
              {
                "key" : "Heavy Fog",
                "doc_count" : 182
              },
              {
                "key" : "Damaging Wind",
                "doc_count" : 173
              },
              {
                "key" : "Hail",
                "doc_count" : 169
              },
              {
                "key" : "Thunder & Lightning",
                "doc_count" : 164
              }
            ]
          },
          "stats_price" : {
            "count" : 2371,
            "min" : 100.57646942138672,
            "max" : 1195.3363037109375,
            "avg" : 586.9627099618385,
            "sum" : 1391688.585319519
          }
        },
        .......
      ]
    }

    }
    }

    
    
    
    
    
    #### *示例: range bucket 聚合 + percentiles 百分位数聚合*
    

    // 价格区间分桶
    GET kibana_sample_data_flights/_search
    {
    "size": 0,
    "aggs": {

    "price_range": {
      "range": {
        "field": "AvgTicketPrice",
        "ranges": [
          {
            "from": 0,
            "to": 200
          },
          {
            "from": 200,
            "to": 500
          },
          {
            "from": 500,
            "to": 800
          },
          {
            "from": 800
          }
        ]
      }
    },
    "price_percentile": {
      "percentiles": {
        "field": "AvgTicketPrice",
        "percents": [
          10,
          15,
          50,
          80,
          95,
          99
        ]
      }
    }

    }
    }

    // 结果
    {
    "took" : 18,
    "timed_out" : false,
    "_shards" : {

    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0

    },
    "hits" : {

    "total" : {
      "value" : 10000,
      "relation" : "gte"
    },
    "max_score" : null,
    "hits" : [ ]

    },
    "aggregations" : {

    "price_range" : {
      "buckets" : [
        {
          "key" : "0.0-200.0",
          "from" : 0.0,
          "to" : 200.0,
          "doc_count" : 749
        },
        {
          "key" : "200.0-500.0",
          "from" : 200.0,
          "to" : 500.0,
          "doc_count" : 3662
        },
        {
          "key" : "500.0-800.0",
          "from" : 500.0,
          "to" : 800.0,
          "doc_count" : 4689
        },
        {
          "key" : "800.0-*",
          "from" : 800.0,
          "doc_count" : 3959
        }
      ]
    },
    "price_percentile" : {
      "values" : {
        "10.0" : 262.04359058107656,
        "15.0" : 311.0302956493907,
        "50.0" : 640.3530744687757,
        "80.0" : 884.7514024612299,
        "95.0" : 1035.021676508585,
        "99.0" : 1167.0564943695067
      }
    }

    }
    }

    
    
    
    
    
    
    
    # ES入门总结
    
    
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200911140530.png)
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200911140544.png)
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200911140600.png)
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200911140638.png)
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200911140652.png)
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200911140734.png)
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200911140751.png)
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200911140804.png)
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200911140751.png)
    
    ![](https://cdn.jsdelivr.net/gh/youjiaxing/picBed/img/20200911140804.png)
    
    查看原文

    赞 0 收藏 0 评论 0

    嘉兴ing 发布了文章 · 1月18日

    ⭐《Linux实战技能100讲》个人笔记 - 8. 系统状态查看

    [TOC]

    综合状态

    top 命令

    显示进程详细信息
    
    top [选项]
    
    选项
        -p <pid>        只查看指定进程号的信息
        -u, -U <user>    只查看属于指定用户的进程信息
        -d <sec>        设置自动刷新延迟时间, 默认是每3秒自动刷新
    
        -b                批量操作模式, 常用于脚本. 一般与 -n 指定输出次数, -d 指定间隔时间
        -n                top 最大输出次数
    
    运行界面可执行的命令
        展示数据
        1        详细展示每个逻辑cpu的使用情况.
        c        展示详细的 Command 列
        
        m        切换内存信息展示(在具体数值和百分比之间切换)
        
        E        切换内存(Mem 和 Swap行)的单位
        e        切换内存(VIRT, RES, SHR)的单位(byte, m, g, t, p)
    
        
        排序
        M        CPU使用从大到小
        P        内存使用从大到小
        
        交互
        s <刷新延迟>    自定义刷新延迟时间, 默认是每3秒自动刷新.
    字段解释
    
    第一行
        top - 22:39:55         up 7:07,         2 users, load average: 1.39, 1.22, 1.16
                          距本次开机已运行时长           平均负载       1分钟 5分钟  15分钟
                          
    第二行(当前显示的进程数量及运行状态)
        Tasks: 202 total,        3 running,          199 sleeping,    0 stopped,    0 zombie
              当前共202个进程        有3个处于运行状态    199个处于休眠状态
    
    第三行(cpu使用情况)
        %CPU(s): 18.3 us,                29.0 sy,            0.0 ni,     52.1 id,    0.2 wa,        0.0 hi,    0.4 si,    0.0 st
                cpu用于用户计算的比例     用于进程状态交互的比例            空闲比例    IO等待比例
    
    第四行(内存状态)
        KiB Mem:    1361956 total,    145060 free,    689800 used,    527096 buff/cache
                    总内存量         未被使用的内存    已使用的内存        用于读写缓存的内存
                    
    第五行(交换分区状态)
        Kib Swap:    2097148 total,    2077948 free,    19200 used.             386368 avail Mem
                    交换分区大小                        已使用的交换分区        可用内存数量.
    
    第六行
        PID    USER    PR                    NI                VIRT    RES    SHR     S      %CPU    %MEM    TIME+            COMMAND
                    由内核控制(受NI影响)    进程优先级(NICE)                    状态                     进程实际运行时间
                                                                
    
    平均负载: 衡量系统的繁忙程度
            理想状态:每个cpu上都有一个活跃进程,即平均负载数等于cpu数
            过载经验值:平均负载高于cpu数量70%的时候
            
    第六行的
        RES: 常驻内存, 即虚拟内存实际映射到物理内存的大小.
        SHR: 占用的内存中属于共享内存的大小. 比如一些外部动态库仅会在内存中加载一份, 多个进程在其虚拟内存空间中这部分地址实际就映射到同一块物理内存地址. 
             也就是要计算单独某个进程占用的内存大小, 应该用 RES - SHR
              这部分可参考: https://www.orchome.com/298
              若是将共享内存占用的内存大小平摊给依赖它的进程, 此时各个进程占用内存 PSS = RES + SHR/共享进程数量
              可以安装 smem 以方便查看进程的 PSS 值.
         
    
        S: 进程的状态, 其可能的取值如下
                T        stopped 状态
                R        running 状态
                S        sleeping 状态

    ps 命令

    ps(process status)

    查看当前运行的进程的一个快照
    
    ps [选项]
    
    常用示例
        ps -ef        
        ps -eLf        查看所有线程信息(会包括LWP和NLWP列)
        
    列解释
        UID        有效用户id
        PID        进程号
        PPID    父进程号
        C
        STIME    
        TTY        执行该进程的终端. pts 是虚拟终端, tty 是字符终端(init 3).
        TIME    进程运行时间(该值不具有参考价值)
        
        LWP        轻量级进程(线程)
        NLWP
        
    选项
        基本参数
        -A, -e            显示所有进程(默认只能查看到当前终端下的进程. 要理解, 进程是树形结构的.)
        -a                显示归属在终端下的所有进程
        -p <进程号>      只显示指定进程号的相关信息
        -C <进程名>          
        
        过滤
        -U <user>        # 根据创建进程的用户来筛选
        -u <user>        # 根据进程所属用户来筛选(和 -U 是有区别的)
        -C <command>    # 根据进程执行的命令(不包含参数)来精确匹配
        -L <pid>        # 查看特定进程的所有线程
        
        输出格式
        -f                # 详细输出
        -F                # 更详细输出
        -o                # 自定义字段, 可使用的字段如下
                        # pid,user,args, cmd, tty, comm, command, fname, ucmd, ucomm, lstart, bsdstart, start 等
        u                # 面向用户的字段, 包含 %CPU, %MEM 等信息
        
        排序
        --sort [-+]<item>    # + 表示从小到大排序(默认)
                            # - 表示从大到小排序
                            # 以下可以混合使用, 比如 --sort -pcpu,+pmem
                            # pcpu        按 cpu 使用排序
                            # pmem        按内容使用率排序
        
    常用
        标准语法 - 查看所有进程信息
        ps -e        # 简略信息
        ps -ef        # 详细信息
        ps -eF        # 完整信息(包含内存占用等)
        ps -e u        # 面向用户的信息(包含内存占用, CPU使用等)
        
        BSD语法 - 查看所有进程信息
        ps ax        # 简略信息
        ps aux        # 完整信息
        
        显示进程的树形结构
        ps -ejH
        ps axjf
    
        
    输出列字段
        PID        进程ID
        TTY        所在终端
        TIME    命令所占用的 CPU 处理时间
        CMD        进程所运行的命令
    参考: https://blog.csdn.net/baidu_3...

    sar 命令

    查看系统综合状态
    
    sar [选项] [<interval>  <count>]
                采样间隔    采样次数
    
    示例
        sar -u 1 10
        sar -r 1 10
        sar -b 1 10
        sar -dp 1 10        # 查看所有块设备的读写情况, 每秒采样1次, 共采样10次.
    
    选项
        文件
        -f <file>    # 从指定文件(二进制格式)获取数据来源(若未指定采样间隔, 默认是当天的统计信息)
                    # -f /var/log/sysstat/sa02
                    # -f /var/log/sa/sa02            # Centos 6
        -o <file>    # 指定采样间隔后, 将采样数据写入指定文件(二进制格式)
        
        时间过滤
        -s <hh:mm:ss>    # 指定天的开始时间(不包含, 因此默认需往前推10分钟)
        -e <hh:mm:ss>    # 指定天的结束时间
    
        资源
        -u              # 报告CPU利用率情况, 可用 ALL 参数来输出更多字段
        -q             # 报告任务队列长度和平均负载(1,5,15)
        -r             # 输出内存和交换空间的统计信息
        -b             # 总体IO
        -d             # 通常配合 -p. 报告每一个块设备的活动状态.
        
        
        显示格式
        -p             # 通常配合 -d. 打印出块设备名, 而非默认的 "device m-n" 格式.

    系统默认保存最近28天的日志, 默认位置 /var/log/sa/

    • sa 二进制数据(使用 sar -f 读取数据)
    • sar 文本数据

    image-20200401135833150

    image-20200401135826683

    sar -n DEV 输出结果说明

    IFACE:LAN接口
    rxpck/s:每秒钟接收的数据包
    txpck/s:每秒钟发送的数据包
    rxbyt/s:每秒钟接收的字节数
    txbyt/s:每秒钟发送的字节数
    rxcmp/s:每秒钟接收的压缩数据包
    txcmp/s:每秒钟发送的压缩数据包
    rxmcst/s:每秒钟接收的多播数据包
    rxerr/s:每秒钟接收的坏数据包
    txerr/s:每秒钟发送的坏数据包
    coll/s:每秒冲突数
    rxdrop/s:因为缓冲充满,每秒钟丢弃的已接收数据包数
    txdrop/s:因为缓冲充满,每秒钟丢弃的已发送数据包数
    txcarr/s:发送数据包时,每秒载波错误数
    rxfram/s:每秒接收数据包的帧对齐错误数
    rxfifo/s:接收的数据包每秒FIFO过速的错误数
    txfifo/s:发送的数据包每秒FIFO过速的错误数

    配置 /etc/sysconfig/sysstat

    # 保存的历史文件(单位是天), 超过28天的则会分目录存放
    HISTORY=28

    详解

    怀疑CPU存在瓶颈,可用 sar -usar -q 等来查看

    怀疑内存存在瓶颈,可用 sar -Bsar -rsar -W 等来查看

    怀疑I/O存在瓶颈,可用 sar -bsar -usar -d 等来查看

    CPU 使用情况 -u

    # sar -u
    
    09:50:01 AM     CPU     %user     %nice   %system   %iowait    %steal     %idle
    10:00:01 AM     all      1.56      0.00      0.75      0.04      0.00     97.65
    10:10:01 AM     all     10.66      0.00      5.20      0.09      0.00     84.05
    10:20:01 AM     all     55.09      0.00     25.19      0.07      0.00     19.65
    10:30:01 AM     all     49.52      0.00     22.84      0.09      0.00     27.55
    10:40:01 AM     all     43.41      0.00     20.48      0.11      0.00     36.00
    10:50:01 AM     all     39.49      0.00     19.01      0.12      0.00     41.38
    Average:        all     33.19      0.00     15.53      0.09      0.00     51.20
    • %user: 用户空间的CPU使用
    • %nice: 改变过优先级的进程的CPU使用率
    • %system: 内核空间的CPU使用率
    • %iowait: CPU等待IO的百分比
    • %steal: 虚拟机的虚拟机CPU使用的CPU
    • %idle: 空闲的CPU

    情景:

    • %iowait 太高, 则表示 I/O 存在瓶颈
    • %idle 太低, 则表示 CPU 使用率高

    队列长度与平均负载 -q

    # sar -q
    
    09:50:01 AM   runq-sz  plist-sz   ldavg-1   ldavg-5  ldavg-15   blocked
    10:00:01 AM         5       338      0.24      0.14      0.12         0
    10:10:01 AM        11       416      3.90      1.54      0.71         0
    10:20:01 AM        15       473     11.36     11.03      6.26         1
    10:30:01 AM         7       474      8.14      9.77      8.00         0
    10:40:01 AM        12       473      6.39      7.26      7.53         0
    10:50:01 AM        15       453      4.53      5.88      6.94         0
    Average:           11       438      5.76      5.94      4.93         0

    输出内容详解:

    • runq-sz:运行队列的长度(等待运行的进程数);
    • plist-sz:进程列表中进程(processes)和线程(threads)的数量;
    • ldavg-1:最后1分钟的系统平均负载;
    • ldavg-5:过去5分钟的系统平均负载;
    • ldavg-15:过去15分钟的系统平均负载;
    • blocked:当前阻塞的进程数量,在等待IO完成;

    输出内存和交换空间 -r

    # sar -r
    
    09:50:01 AM kbmemfree kbmemused  %memused kbbuffers  kbcached  kbcommit   %commit  kbactive   kbinact   kbdirty
    10:00:01 AM   8902892  23876908     72.84      2080  12331564   7143804     21.79   7354428   7190496       224
    10:10:01 AM   8496696  24283104     74.08      2080  12394360   7846800     23.94   7685000   7169248      1980
    10:20:01 AM   7986260  24793540     75.64      2080  12430912   8486280     25.89   7984868   7146152      2120
    10:30:01 AM   7804560  24975240     76.19      2080  12488140   8087856     24.67   7975640   7158088      1592
    10:40:01 AM   7588988  25190812     76.85      2080  12588572   8190552     24.99   7976948   7214956      1388
    10:50:01 AM   7534756  25245044     77.01      2080  12639652   7722896     23.56   7879740   7228132      1176
    Average:      8052359  24727441     75.43      2080  12478867   7913031     24.14   7809437   7184512      1413

    输出内容详解:

    • kbmemfree:这个值和free命令中的free值基本一致,所以它不包括buffercache的空间;
    • kbmemused:这个值和free命令中的used值基本一致,所以它包括buffercache的空间;
    • %memused:这个值是kbmemused和内存总量(不包括swap)的一个百分比;
    • kbbuffers:这两个值就是free命令中的buffer
    • kbcached:这两个值就是free命令中的cache
    • kbcommit:保证当前系统所需要的内存,即为了确保不溢出而需要的内存(RAM + swap);
    • %commit:这个值是kbcommit与内存总量(包括swap)的一个百分比;
    • kbactive:活动内存量(以千字节计算)(最近使用的内存,通常不会被收回,除非绝对必要);
    • kbinact:不活动内存量(以千字节计算的内存(最近使用的内存),更有资格被用于其他目的);
    • kbdirty:以KB为单位的内存量等待写入磁盘;

    pidstat 命令

    磁盘 IO

    建议排查顺序

    1. 查看设备总体利用率情况

      iostat -xt 1
    2. 查看是哪个进程在占用磁盘 IO

      iotop

    iostat 命令

    快速查看磁盘设备的当前使用繁忙程度

    查看 CPU 及设备/分区的使用情况
    
    iostat [ options ] [ <interval> [ <count> ] ]
    
    
    iostat -xcdmt 1
    
    选项
        -x: 显示扩展状态(包含 -c, -d, -k)
        -t: 显示每个报告产生时的时间
        
        -m: 以兆字节每秒为单位,而不使用块每秒
        -k: 以千字节每秒为单位,而不使用块每秒
        -c: 显示CPU使用情况
        -d: 显示设备利用率

    重点关注如下指标:

    标示说明
    %iowaitCPU等待IO时间占用CPU总时间的百分比
    Device监测设备名称
    rMB/s每秒实际读取的大小,单位为KB
    wMB/s每秒实际写入的大小,单位为KB
    avgrq-sz需求的平均大小区段
    avgqu-szIO请求的平均队列长度
    awaitIO请求平均响应时间(队列排队时间+IO处理时间),一般系统 I/O 响应时间应该低于 5ms,如果大于 10ms 就比较大了
    svctmIO处理时间,即寻道 + 旋转延迟时间;
    %util磁盘繁忙程度。 例如,如果统计间隔 1 秒,该设备有 0.8 秒在处理 I/O,而 0.2 秒闲置,那么该设备的 %util = 0.8/1 = 80%;

    iotop 命令

    快速查看各个进程对于磁盘 IO 的读写情况

    iotop
    
    选项 
        -d SEC, --delay=SEC        #设置显示的间隔秒数,支持非整数值    
        -p PID, --pid=PID        #只显示指定PID的信息
        -u USER, --user=USER    #显示指定的用户的进程的信息
    
        -o, --only                #显示进程或者线程实际上正在做的I/O,而不是全部的,可以随时切换按o
        -a, --accumulated        #显示从iotop启动后每个线程完成了的IO总数
        
        # 非交互模式
        -b, --batch            #运行在非交互式的模式
        -t, --time            # batch模式, 在每一行前添加一个当前的时间
        -n NUM, --iter=NUM        #在非交互式模式下,设置显示的次数,
    
        # 比较少用的选项
        --version            #显示版本号
        -h, --help            #显示帮助信息
        -P, --processes        # 只显示进程(默认会展示所有的线程)  (建议以默认的查看所有线程的方式, 否则会有遗漏)
        -k, --kilobytes        #以千字节显示(默认是按照 human 方式)
        -q, --quiet            #suppress some lines of header (implies --batch). This option can be specified up to three times to remove header lines.
        -q     column names are only printed on the first iteration,
        -qq    column names are never printed,
        -qqq   the I/O summary is never printed.
    
    
    操作按键
        a: 显示累积使用量 (--accumulated)
        o: 只显示有io的进程/线程 (--only)
        p: 进程/线程显示切换 PID/TID, 默认是 TID (--processes)    
        i:改变线程的优先级
        r:反向排序
        使用left和right改变排序
        q:退出 

    网络

    建议排查顺序

    1. 查看网络总体使用情况

      nload -u h eth0
    2. 查看是哪个进程占用网络

      nethogs
    3. 查看是哪个 ip:port 在占用网络

      iftop -i eth0 -nNPB

      需要根据端口号来判断.

    nload 命令

    查看总体带宽使用情况
    
    nload [选项] [<device>]
    
    说明
        不指定 <device> 时会显示所有网络设备
    
    选项
        -t <interval>                # 刷新间隔(毫秒), 默认是 500毫秒, 设置小于100毫秒时会不精确.
        -u h|H|b|B|k|K|m|M|g|G        # 每秒的流量单位, 默认是 k.
                                    # h     auto, human readable
                                    # b        Bit/s                                
                                    # k        kBit/s
                                    # m        MBit/s
                                    # g        GBit/s
                                    # 大写的 bkmg 是用 Byte 为单位, 而非 Bit.
        
        -U h|H|b|B|k|K|m|M|g|G        # 累计流量单位, 默认是 M.

    nethogs

    按照进程统计流量
    
    nethogs [选项] [device [device ...]]
    
    运行时交互
        m        # 修改单位
        r         # 按流量排序
        s         # 按发送流量排序
        q         # 退出命令提示符
    
    选项|参数
        -v <mode>    # 切换显示单位,默认是默认是KB/s(0表示 KB/s,1表示KB,2表示B,3表示MB)    
        -c            # 检测次数(后面直接跟数字)
        -a            # 检测所有的设备
        -d            # 延迟更新刷新速率,以秒为单位。默认值为 1.  
        -t            # 跟踪模式.  
        -b            # bug 狩猎模式 — — 意味着跟踪模式.  
        -V             # 显示版本信息,注意是大写字母V.  
        -p             # 混合模式(不推荐).  
        device        # 要监视的设备名称. 默认为 eth0  

    iftop 命令

    按照 ip:port 统计流量
    
    iftop [选项]
    
    选项
        网卡
        -i <网卡>      # 指定只解析网络接口
    
        主机名
        -n            # ip不解析成域名
        
        端口
        -P            # 显示通信双方的端口号
        -N            # 端口号不解析成服务名
        
        -B            # 使用 byte 而不是默认的 bit
        
    输出结果解释
        第三列
                51Kb         40.0Kb         22.0Kb
            本机    2s             10s             40s    的平均流量
    epel 源提供.
    查看原文

    赞 0 收藏 0 评论 0

    嘉兴ing 发布了文章 · 1月18日

    ⭐《Linux实战技能100讲》个人笔记 - 7. 其他工具

    [TOC]

    Logrotate 日志管理工具

    logrotate 是一个Linux系统默认安装了的日志文件管理工具,用来把旧文件轮转、压缩、删除,并且创建新的日志文件。我们可以根据日志文件的大小、天数等来转储,便于对日志文件管理。

    默认 logrotate 是在每天凌晨 3 点多被 anacron 调用执行.

    具体调用流程分析可以见 4. Shell篇.md#anacron 周期命令调度程序
    日志管理工具
    
    logrotate [选项] <配置文件>
    
    选项
        # 常用
        -d, --debug              # 调试模式,仅输出操作步骤,并不实际执行。隐式包含 -v 参数
        -f, --force              # 强制执行转储(logrotate 会根据状态文件, 自动判定是否有必要执行), 若配置文件刚修改完要马上执行建议使用该参数. 
        -s, --state=statefile    # 使用指定的状态文件(而非默认的),对于运行在不同用户情况下有用
        -v, --verbose            # 显示转储过程的详细信息
        
        # 不常用
        -m, --mail=command       # 发送邮件命令而不是用‘/bin/mail'发
        
    文件
        /etc/logrotate.conf            # 默认配置文件
        /var/lib/logrotate.status    # 默认状态文件
    • 默认配置文件 /etc/logrotate.conf 中一般会有一句 include /etc/logrotate.d 用于加载该目录下的所有配置文件.

      可在 /etc/logrotate.d/ 下放置自定义的配置文件来自动调用.
    • 默认的状态文件(记录每个处理的日志的最后时间?) /var/lib/logrotate.status

    参考:

    配置选项

    默认读取的配置 /etc/logrotate.conf

    # 触发方式 - 时间间隔
    hourly
    daily
    weekly
    monthly             
    
    # 触发方式 - 文件大小
    size <logsize>                   # 当日志文件到达指定的大小时才转储(例如 10(字节), 100k,4M, 1G
    maxsize <size>                    # 与时间间隔一同配置, 当日志文件超过该 maxsize 时, 即使未到指定时间间隔也会轮转
    minsize <size>                    # 与时间间隔一同配置, 若日志文件未超过该 minsize 时, 即使到达指定时间间隔也不会轮转.
    
    # 对旧日志操作方式(默认策略应该是重命名原日志文件)
    默认方式                         # 修改日志文件名(实际是修改所属目录inode中的信息), 若该文件已被进程打开, 通常需要配置 postrotate 发送信号给该进程以重新打开日志文件.
    copytruncate                    # 用于还在打开中的日志文件,把当前日志备份并截断,是先拷贝再清空的方式,拷贝和清空之间有一个时间差,可能会丢失部分日志数据
    nocopytruncate                    # 备份日志文件但是不截断
    
    # 压缩
    compress                         # 压缩(gzip)日志文件的所有非当前版本
    nocompress                        # (默认)不压缩
    delaycompress                    # 本次转储的日志在下一次转储时才压缩.
    nodelaycompress                    # (默认)不延迟压缩
    
    # 邮件
    mail <mail>                           # 转储的日志发送到指定邮箱
    nomail                            # (默认)不发送转储的日志文件到邮箱
    
    # 错误处理
    errors <mail>                      # 转储时的错误信息发送到指定邮箱
    missingok                        # 如果日志文件丢失,不要显示错误
    
    # 空文件处理
    notifempty                       # 如果日志文件为空,则不轮换日志文件(即忽略空文件).
    ifempty                            # (默认)即使空文件也转储
    
    # 存放目录
    olddir <dir>                     # 转储后的日志放在指定目录下
    noolddir                        # (默认)转储后的日志放在和当前日志文件同一个目录下
    
    # 转储日志命名
    dateext                            # 指定转储后的日志文件以当前日期为格式结尾,如 access.log-20200426.gz. 
                                    # 默认是以自增序号, 比如 messages -> messages.1 -> messages.2
    dateformat <dateformat>            # 配合dateext使用,紧跟在下一行出现,定义日期格式,只支持 %Y %m %d %H %s 这5个参数(%s 是时间戳)
                                    # hourly 默认使用时间格式: -%Y%m%d%H
                                    # daily,weekly,monthly 默认使用时间格式: -%Y%m%d
                                    # 示例: dateformat -%Y%m%d%s
    
    # 保留转储文件数量
    rotate <n>                       # 保留转储后的日志文件数量, 0指没有备份(轮转后马上删除旧日志), n指保留n个备份
    
    # 创建新日志
    create <mode> <user> <group>    # 轮换原始文件并创建具有指定权限、用户和组的新文件, 示例: create 700 root root
    nocreate                        # (默认)不主动创建新的日志文件
    
    # 脚本
    sharedscripts                    # 对于整个日志组只运行一次脚本
    prerotate                        # 引入一个在日志被轮换前执行的脚本, 需要和 endscript 严格配置. 关键字必须单独一行.
    postrotate                       # 引入一个在日志被轮换后执行的脚本, 需要和 endscript 严格配置. 关键字必须单独一行.
    endscript                        # 标记 prerotate 或 postrotate 脚本的结束, 关键字必须单独一行.

    示例

    nginx 示例

    /data/nginx/logs/*log {
        daily
        rotate 32
        missingok
        notifempty
        compress
        delaycomporess
        dateext
        sharedscripts
        postrotate
            /bin/kill -USR1 $(cat /var/run/nginx.pid 2>/dev/null) 2>/dev/null || :
        endscript
    }
    轮转完后日志会从 /data/nginx/logs/access.log 变为 /data/nginx/logs/access.log-20200426.gz, 之类的日期是个示例.

    可通过手动执行(指定配置文件)来调用:

    logrotate /path/to/nginx_logrotate

    日志轮转的机制

    此处不讨论压缩等的后续操作.

    该部分内容来自: https://www.lightxue.com/how-...

    若inode部分不理解, 可查看原文链接或查看笔记 3. 系统管理篇.md#文件、目录与 inode(i节点))

    方案1:create

    默认方案没有名字,姑且叫它create吧。因为这个方案会创建一个新的日志文件给程序输出日志,而且第二个方案名copytruncate是个配置项,与create配置项是互斥的。

    这个方案的思路是重命名原日志文件,创建新的日志文件。详细步骤如下:

    1. 重命名程序当前正在输出日志的程序。因为重命名只会修改目录文件的内容,而进程操作文件靠的是inode编号,所以并不影响程序继续输出日志。
    2. 创建新的日志文件,文件名和原来日志文件一样。虽然新的日志文件和原来日志文件的名字一样,但是inode编号不一样,所以程序输出的日志还是往原日志文件输出。
    3. 通过某些方式通知程序,重新打开日志文件。程序重新打开日志文件,靠的是文件路径而不是inode编号,所以打开的是新的日志文件。

    什么方式通知程序我重新打开日志呢,简单粗暴的方法是杀死进程重新打开。很多场景这种作法会影响在线的服务,于是有些程序提供了重新打开日志的接口,比如可以通过信号通知nginx。各种IPC方式都可以,前提是程序自身要支持这个功能。

    有个地方值得一提,一个程序可能输出了多个需要滚动的日志文件。每滚动一个就通知程序重新打开所有日志文件不太划得来。有个sharedscripts的参数,让程序把所有日志都重命名了以后,只通知一次。

    方案2:copytruncate

    如果程序不支持重新打开日志的功能,又不能粗暴地重启程序,怎么滚动日志呢?copytruncate的方案出场了。

    这个方案的思路是把正在输出的日志拷(copy)一份出来,再清空(trucate)原来的日志。详细步骤如下:

    1. 拷贝程序当前正在输出的日志文件,保存文件名为滚动结果文件名。这期间程序照常输出日志到原来的文件中,原来的文件名也没有变。
    2. 清空程序正在输出的日志文件。清空后程序输出的日志还是输出到这个日志文件中,因为清空文件只是把文件的内容删除了,文件的inode编号并没有发生变化,变化的是元信息中文件内容的信息。

    结果上看,旧的日志内容存在滚动的文件里,新的日志输出到空的文件里。实现了日志的滚动。

    这个方案有两个有趣的地方。

    1. 文件清空并不影响到输出日志的程序的文件表里的文件位置信息,因为各进程的文件表是独立的。那么文件清空后,程序输出的日志应该接着之前日志的偏移位置输出,这个位置之前会被\0填充才对。但实际上logroate清空日志文件后,程序输出的日志都是从文件开始处开始写的。这是怎么做到的?这个问题让我纠结了很久,直到某天灵光一闪,这不是logrotate做的,而是成熟的写日志的方式,都是用O_APPEND的方式写的。如果程序没有用O_APPEND方式打开日志文件,变会出现copytruncate后日志文件前面会被一堆\0填充的情况。
    2. 日志在拷贝完到清空文件这段时间内,程序输出的日志没有备份就清空了,这些日志不是丢了吗?是的,copytruncate有丢失部分日志内容的风险。所以能用create的方案就别用copytruncate。所以很多程序提供了通知我更新打开日志文件的功能来支持create方案,或者自己做了日志滚动,不依赖logrotate。

    指定每日0点转储文件

    方式1: 修改anacrontab调用 logrotate 的时机(极其不推荐, 会影响其他服务的调用时机)

    方式2: crontab 手动定义配置文件(目前个人推荐)

    1. 创建配置文件 /etc/logrotate_daily0.conf

      dateext
      missingok
      notifempty
      
      include /etc/logrotate_daily0.d
    2. 创建目录 /etc/logrotate_daily0.d

      主要不要使用默认目录, 否则会被正常的 logrotate 调度执行到
    3. 在上述目录下创建自己想要的配置文件
    4. 在 crontab 配置定时任务

      0 0 * * * /usr/sbin/logrotate /etc/logrotate_daily0.conf &> /dev/null

    一键脚本

    cat > /etc/logrotate_daily0.conf <<'EOF'
    dateext
    rotate 7
    missingok
    notifempty
    
    include /etc/logrotate_daily0.d
    EOF
    
    mkdir /etc/logrotate_daily0.d
    
    cat >> /var/spool/cron/root <<'EOF'
    
    # 0点执行日志轮转
    0 0 * * * /usr/sbin/logrotate -f /etc/logrotate_daily0.conf &> /dev/null
    EOF
    
    crontab /var/spool/cron/root
    
    cat > /etc/logrotate_daily0.d/nginx <<'EOF'
    /data/nginx/logs/*.log {
       missingok
       notifempty
       daily
       compress
       dateext
       nocreate
       rotate 31
       sharedscripts
       postrotate
            /bin/kill -USR1 $(ps -ef|grep nginx|grep master|grep -v grep|awk '{print $2}') || true
       endscript
    }
    EOF

    方式3: 手动指定每日 logrotate 的调用(不推荐, 会影响其他已配置的日志轮转)

    1. 移除默认定时配置

      mv /etc/cron.daily/logrotate /usr/local/bin/logrotate.sh
    2. 在 crontab 配置定时任务

      0 0 * * * /bin/bash /usr/local/bin/logrotate.sh

    Ansi

    https://github.com/fidian/ansi

    这是一个用于生成 ANSI 转义序列的脚本, 可用于:

    • 移动光标
    • 文本加粗
    • 添加颜色
    • ...

    下载并使用

     wget https://raw.githubusercontent.com/fidian/ansi/master/ansi -O /usr/local/bin/ansi
     chmod a+x /usr/local/bin/ansi
     
     ansi -h

    示例

    # 红色字体, 加粗
    ansi --bold --bg-red "请用 root 账户执行本脚本";
    
    echo -n '['; ansi -n --bold --green "DONE"; echo ']';
    
    echo -n '['; ansi --bold --red "ERROR"; echo ']';

    img

    img

    img

    expect

    expect是一个自动化交互套件,主要应用于执行命令和程序时,系统以交互形式要求输入指定字符串,实现交互通信

    相关链接:

    安装

    yum install -y expect

    注意 expect 脚本开头一行是 #!/usr/bin/expect , 或者在执行时使用 expect <脚本> 来执行脚本.

    命令

    set timeout

    设置超时时间

    # -1 表示不限制执行超时时间
    set timeout -1
    默认是 30s, 若执行会超过这个时间, 要注意设置"超时时间", 避免脚本被强行中断

    set

    定义变量

    set 变量名 变量值
    # set password "123456"

    puts

    输出变量

    spanw

    交互程序开始后面跟命令或者指定程序

    spanw ssh xxx@x.x.x.x
    expect "*password"
    send "123456\n"
    
    # 进入交互模式
    interact

    expect

    语法

    • expect "文本匹配" 支持通配符匹配
    • expect eof 等待spawn的执行结束
    expect "待匹配文本"
    send "xxx\r"
    expect "*支持通配符*"
    send "xxx\r"
    
    # 等价于上面两条, 更简洁
    expect {
        "待匹配的文本" { send "xxxx\r"; exp_continue }
        "*支持通配符*" { send "xxx\r"}
    }
    
    # 等待 spawn 的脚本结束
    expect eof

    exp_continue

    在expect中多次匹配就需要用到

    expect {
        "待匹配的文本" { send "xxxx\r"; exp_continue }
        "*支持通配符*" { send "xxx\r"}
    }

    send

    用于发送指定的字符串信息

    expect "xxx"
    send "xxx\r"

    send_user

    用来打印输出 相当于shell中的echo

    send_user "echo something"

    exit

    interact

    进入交互模式

    关键字

    if

    流程控制

    if { ... } {
        # ...
    }

    lindex

    获取数组的第N个参数

    # 使用 lindex 关键字获取第N个参数
    set 变量名 [lindex $argv 0]
    set 变量名 [lindex $argv 1]
    ...
    
    
    # 参数总数
    if {$argc < 1} {
        send_user "..."
        exit
    }

    file

    https://wxnacy.com/2018/05/31...

    示例

    示例: 一个 expect 脚本的模板

    #!/usr/bin/expect
    
    set PASSWORD "ssh密码"
    
    if {$argc < 1} {
        send_user "usage: $argv0 <dir>\n"
        exit
    }
    
    set srcdir [lindex $argv 0]
    
    if {[file isdirectory "$srcdir"] != 1} {
        send_user "not exists dir: $srcdir\n"
        exit
    }
    
    spawn bash deploy.sh $srcdir
    
    # 这是一个自动输入 ssh 密码的应答
    expect {
        "yes/no" { send "yes\n"; exp_continue }
        "password:" { send "${PASSWORD}\n" }
    }
    
    expect eof
    上面是一个在不使用秘钥情况下完成 rsync 免手动输入密码的处理.

    示例: 自动输入 mysql 密码

    #!/usr/bin/expect
    set password xxxx
    set name root
    
    spawn mysql -u $name -p
    expect "*password:*"
    send "$password\r"
    interact

    示例: 在 shell 中嵌套 expect

    #!/bin/bash
    
    for i in `cat /home/admin/timeout_login.txt`
    do
    
        /usr/bin/expect << EOF
        spawn /usr/bin/ssh -t -p 22022 admin@$i "sudo su -"
    
        expect {
            "yes/no" { send "yes\r" }
        }   
    
        expect {
            "password:" { send "xxo1#qaz\r" }
        }
        
        expect {
            "*password*:" { send "xx1#qaz\r" }
        }
    
        expect "*]#"
        send "df -Th\r"
        expect "*]#"
        send "exit\r"
        expect eof
    
    EOF
    done
    查看原文

    赞 0 收藏 0 评论 0

    嘉兴ing 发布了文章 · 1月18日

    ⭐《Linux实战技能100讲》个人笔记 - 6. 服务管理篇

    防火墙

    硬件防火墙: 主要是防御DDOS攻击, 流量攻击. 兼顾数据包过滤.

    软件防火墙: 主要处理数据包的过滤.

    • iptables
    • firewalld

    包过滤防火墙

    应用层防火墙

    iptables

    CentOS 6 默认防火墙, 主要是包过滤防火墙, 对 ip, 端口, tcp, udp, icmp 协议控制.

    配置清晰, 很复杂.

    属于包过滤防火墙

    iptables [选项]
    
    选项
        -n --numeric      # 数字输出。IP地址和端口会以数字的形式打印。默认情况下,程序试显 示主机名、网络名或者服务(只要可用)
        -v --verbose      # 让 -L 显示更详细的
        -L -list <规则链>  # 显示所选链的所有规则。如果没有选择链,所有链将被显示。也可以和z选项一起 使用,这时链会被自动列出和归零。精确输出受其它所给参数影响。
        
        -t <规则表=filter>        # 要操作的匹配包的表(默认是操作 filter)

    规则"表"

    • filter 过滤

      ip, 端口, tcp,udp,icmp协议
    • nat 网络地址转换
    • mangle
    • raw
    常用, 需重点学习的: filter 和 nat

    不同的表有自己的默认规则, eg. Chain INPUT (policy ACCEPT) 表示若未匹配任意规则则默认接受

    规则"链"

    • INPUT (C->S 方向)规则链
    • OUTPUT (S->C 方向)规则链
    • FORWARD 转发规则链
    • PREROUTING 路由前转换(改变目标地址)规则链
    • POSTROUTING 路由后转换(控制源地址)规则链

    filter 表操作

    默认操作的是 filter 表, 因此一般书写时可以省略.
    
    filter [-t filter] <命令> <规则链> <规则>
    
    命令
        -L -list            # 查看状态
    
        -I -insert            # 添加规则(插入到最前面), 注意规则的顺序是有优先级的(排在前面的优先级高)
        -A -append             # 添加规则(追加到最后面), 注意规则的顺序是有优先级的(排在前面的优先级高)
        
        -D -delete <规则序号或详细规则>        # 删除某一条规则
        -F -flush            # 清空所有规则(默认的Policy规则不会改变)
        
        -P -policy            # 设置默认规则
        
        自定义规则链
        -N -new-chain        # 创建自定义规则链
        -X -delete-chain
        -E -rename-chain
        
    规则链
        INPUT
        OUTPUT
    
    规则
        -s -source [!] address[/mask]            # 指定源地址 ip
                                                # eg. 10.0.0.0
                                                # eg. 10.0.0.0/24
        -d --destination [!] address[/mask]        # 指定目标地址 ip, 用于 INPUT 没意义
        -i -in-interface [!] [name]                # 进入的网络接口(网卡)
        -o --out-interface [!][name]            # 出去的网络接口(网卡)
        -p -protocal [!]protocol                # 协议: tcp, udp, icmp
        
        -j --jump <target>                        # 指定规则的目标(动作)
    
    tcp协议相关选项
        --sport, --source-port [!] [port[:port]]            # 来源端口或端口范围
        --dport, --destionation-port [!] [port:[port]]        # 目标端口或端口范围
        
    udp协议相关选项
        --sport, --source-port [!] [port[:port]]            # 来源端口或端口范围
        --dport, --destionation-port [!] [port:[port]]        # 目标端口或端口范围
    
    icmp协议相关选项
    
    
    动作(target) -j
        ACCEPT     # 允许
        DROP    # 禁止
        QUEUE
        RETURN
        
    示例
        iptables -vnL    # 查看filter表详细状态, 可方便查看当前的所有规则
    
        iptables -t filter -A INPUT -s 10.0.0.1 -j ACCEPT    # 允许来自 10.0.0.1 的包进入
        iptables -t filter -A INPUT -s 10.0.0.2 -j DROP    # 允许来自 10.0.0.1 的包进入
        
        iptables -D INPUT -s 10.0.0.2 -j DROP    # 删除匹配的规则
        iptables -t filter -D INPUT 3            # 删除第3条规则(第1条的序号是1)
        
        iptables -P INPUT DROP        # 修改INPUT规则链的默认规则为 DROP
        
        iptables -t filter -A INPUT -i eth0 -s 10.0.0.2 -p tcp --dport 80 -j ACCEPT        # 允许从 eth0 网卡进入, 来自10.0.0.2的包访问本地的80端口.

    常用策略

    • 默认ACCEPT, 配置特定规则 DROP

      常用于测试
    • 默认 DROP, 配置特定规则 ACCEPT

      常见于生产环境

    输出字段解释

                    当前的默认策略是ACCEPT
                        👇
    Chain INPUT (policy ACCEPT 501 packets, 33820 bytes)
     pkts bytes target     prot            opt in     out     source               destination
        0     0 ACCEPT     all            --  *      *       10.0.0.1             0.0.0.0/0
      
                    👆       👆                👆        👆            👆                    👆
                   策略         协议                进入网卡 出去网卡    来源地址              目的地址
                        (tcp,udp,icmp)
        

    nat 表操作

    NAT表 - 网络地址转换表

    filter -t nat <命令> <规则链> <规则>
    
    
    规则链
        PREROUTING        # 路由前转换 - 目的地址转换
        POSTROUTING        # 路由后转换 - 控制源地址
    
    
    动作(target) -j
        DNAT
    
    规则
        --to-destination <ip>[/<mask>]    # 修改目的地址
        --to-source <ip>[/<mask>]        # 修改源地址
    
    
    示例
        iptables -t nat -vnL    # 查看nat表详细状态
        
        # 假设 iptables 所在主机ip为 114.115.116.117, 此处配置外网访问该地址(eth0网卡)时将访问tcp协议且是80端口的数据包目的地址转换内网主机
        iptables -t nat -A PREROUTING -i eth0 -d 114.115.116.117 -p tcp --dport 80 -j DNAT --to-destination 10.0.0.1
        # 另内网访问该地址(eth1网卡)时将数据包源地址转换成iptables所在主机ip 114.115.116.117
        iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth1 -j SNAT --to-source 114.115.116.117
    可用于: 端口转发

    配置文件

    iptables 配置文件 /etc/sysconfig/iptables

    在 CentOS 6 中, 管理 iptables:

    service iptables save|start|stop|restart
    
    命令
        save        # 将当前内存中的配置保存为配置文件 /etc/sysconfig/iptables
                    # 实际执行了 iptables-save > /etc/sysconfig/iptables
                    # 下次启动时 iptables-restore < /etc/sysconfig/iptables
    在CentOS 7 中, 需要安装该服务 yum install iptables-services 才可以使用该命令

    注意:

    • iptables 的命令是写在内存中, 重启后失效.

    firewalld

    CentOS 7 在 iptables 上又封装了一层, 也就是 firewalld , 使用更简洁(底层使用 netfilter).

    属于包过滤防火墙

    firewalld 的特点

    • 支持区域 "zone" 概念

      即 iptables 的自建规则链
    • firewall-cmd 控制防火墙

    firewalld 服务管理

    systemctl start|stop|enable|disable firewalld.serivce
    注意: firewalld 与 iptables 服务是冲突的.

    firewall-cmd

    firewall-cmd [选项]
    
    选项
        状态选项
        --state                # 查看firewalld当前状态
        --reload            # 重新加载配置
        
        --zone=public        # 指定区域, 不指定时默认就是 public
            
        Zone选项
        --get-zones            # 查看所有的区(除了public外还有一些iptables默认规则链一样)
        --get-default-zone    # 查看默认的区
        --get-active-zone    # 查看激活的zone    
        
        永久性选项
        --permanent            # 设置选项写入配置, 需要 reload 才能生效 
        
        修改和查询zone的选项
        --list-all            # 查看详细状态(包含以下内容)     
        --list-interfaces    # 查看某一项状态 - 网卡接口
        --list-ports        # 查看某一项状态 - 端口
        --list-services        # 查看某一项状态 - 服务端口
        
        --add-service <服务>        # 添加service端口
        --add-port <端口/协议>       # 添加指定协议指定端口, eg. 81/tcp
        --add-source <源ip[/网段]>    # 添加源地址
        --add-interface <网卡接口>
        
        --remove-service <服务>
        --remove-port <端口/协议>
        --remove-source <源ip[/网段]>
        --remove-interface <网卡接口>

    待整理的示例

    # 只允许某个IP段访问3306
    firewall-cmd --permanent --zone=public --add-rich-rule 'rule family=“ipv4” source address=“192.168.0.4/24” port port protocal=“tcp” port=“3306” accept'

    firewall-cmd --list-all 输出字段解释

    #public区域    激活状态
    #  👇          👇
    public (active)
      target: default                # 👈
      icmp-block-inversion: no
      interfaces: eth0                # 👈 public区域绑定了 eth0 网卡
      sources:                        # 允许访问的源ip
      services: ssh dhcpv6-client    # 允许访问的服务端口
      ports:                        # 允许访问的端口
      protocols:
      masquerade: no
      forward-ports:
      source-ports:
      icmp-blocks:
      rich rules:

    telnet 服务

    telnet 是明文传输, 危险, 因此不能应用于远程管理.

    安装服务

    ## telnet 是客户端工具
    ## telnet-server 是服务端服务
    ## xinetd 是因为 telnet-server 无法自启动, 因此需要使用它来管理 telnet-server 的启动
    yum install telnet telnet-server xinetd -y

    启动服务

     systemctl start xinetd
     systemctl start telnet.socket
     
    telnet 不负责端口的监听, 只负责处理传过来的数据.

    端口监听工作交由 xinetd 负责, xinetd 负责端口监听并将请求转发给 telnet

    root 用户无法用于telnet 远程登录

    SSH 服务

    客户端配置文件 /etc/ssh/ssh_config

    服务配置文件 /etc/ssh/sshd_config

    Port 22                                            # 服务端口号(默认是22)
    PermitRootLogin yes                                # 是否允许root登录
    AuthorizedKeysFile  .ssh/authorized_keys        # 密钥验证的公钥存储文件. 默认是放在 ~/.ssh/authorized_keys

    使配置生效

    systemctl restart sshd.service

    ssh 命令

    ssh 选项 [<user>@]<host>
    
    选项
        常用
        -p <port>        # 指定连接端口
        -4              # 强制 ssh 只使用 IPv4 地址
        -f              # 要求 ssh 在执行命令前退至后台.
        
        端口转发
        -N                      # 不执行远程命令. 用于转发端口.
        -L port:host:hostport    # local, 将本地机(客户机)的某个端口转发到远端指定机器的指定端口
                                # 工作原理是这样的, 本地机器上分配了一个 socket 侦听 port 端口, 一旦这个端口上有了连接, 该连接就经过安全通道转发出去, 同时远程主机和 host 的 hostport 端口建立连接. 可以在配置文件中指定端口的转发.
                                 # 只有 root 才能转发特权端口.  IPv6 地址用另一种格式说明: port/host/hostport
    
        -R port:host:hostport    # remote, 将远程主机(服务器)的某个端口转发到本地端指定机器的指定端口
                                # 工作原理是这样的, 远程主机上分配了一个 socket 侦听 port 端口, 一旦这个端口上有了连接, 该连接就经过安全通道转向出去, 同时本地主机和 host 的 hostport 端口建立连接.
                                # 可以在配置文件中指定端口的转发. 只有用 root 登录远程主机 才能转发特权端口. IPv6 地址用另一种格式说明: port/host/hostport
                                
                                
    示例
        ssh -fNL 27117:127.0.0.1:27017 <远程主机地址>        # 建立local隧道, 将来自27117的连接经过<远程主机>转发至127.0.0.1(其实还是<远程主机>)的27017端口
                                                        # 通常适用于<远程主机>未对外开放27017端口
                                                        
        ssh -fNR 2222:127.0.0.1:22 <远程主机地址>            # 建立remote隧道(也叫反向隧道), 将来自<远程主机>2222端口的连接经过本机转发给127.0.0.1(实际上还是本机)
                                                        # 通常适用于外网访问内网服务

    示例: 配置访问特定主机时使用特定的私钥文件

    • 当前主机存在多个私钥文件
    • 访问特定主机时需使用特定私钥文件(非默认)
    # 假设私钥文件路径: ~/.ssh/xxx_id_rsa
    cat > ~/.ssh/config <<'EOF'
    Host 192.168.0.143
        PubkeyAuthentication yes
        IdentityFile ~/.ssh/xxx_id_rsa
    EOF
    # 注意 config 文件和私钥文件的权限都必须是 600
    chmod 600 ~/.ssh/config

    公钥认证

    ssh-keygen 命令

    生成密钥对
    
    ssh-keygen [选项]
    
    选项
        -t <密钥类型=rsa>         # 指定密钥类型: dsa|ecdsa|ed25519|rsa|rsa1
        -C <comment=$USER@$HOSTNAME>            #  注释(追加在公钥文件最后)

    注意:

    • 生成密钥对一定是在客户端上做, 然后再将公钥传给服务端.
    • 默认生成的文件

      • ~/.ssh/id_rsa
      • ~/.ssh/id_rsa.pub

    ssh-copy-id 命令

    将公钥(通过ssh)拷贝到目标主机
    
    ssh-copy-id 选项 [user@]hostname
    
    选项
        -n                                            # dry-run, 不真正执行命令
        -i <identity_file=~/.ssh/id_rsa.pub>        # 手动指定公钥文件
        -p port
        -o ssh_option

    注意:

    • 目标主机的公钥存储文件 ~/.ssh/authorized_keys, 600 权限

    scp 命令

    远程文件复制
    
    scp [选项] <src> <dest>
    
    选项
        -C      允许压缩
        -r      递归复制整个目录
        -P         port
    
    src 和 dest 可以是以下任意的
        [<user>@]<host>:/remote/path    # 指定远程文件或目录
        /local/path                        # 指定本地文件或目录
        
    示例
        scp a.txt 192.168.0.16:/tmp/    # 拷贝本地的 a.txt 到远程的 /tmp/ 目录下
    传递大文件更推荐用 rsync(断点续传, 压缩)

    rsync 命令

    快速、通用的远程和本地文件传输工具
    
    语法
        # 本地复制
        rsync [选项] <src> <dest>
        
        # 通过 ssh 远程复制
        Pull: rsync [OPTION...] [USER@]HOST:SRC... [DEST]
        Push: rsync [OPTION...] SRC... [USER@]HOST:DEST
        
        # 通过 rsync 服务
        Pull: rsync [OPTION...] [USER@]HOST::SRC... [DEST]
                 rsync [OPTION...] rsync://[USER@]HOST[:PORT]/SRC... [DEST]
        Push: rsync [OPTION...] SRC... [USER@]HOST::DEST
              rsync [OPTION...] SRC... rsync://[USER@]HOST[:PORT]/DEST    
        
    选项
        # 常用
        -P                         # 断点续传(保留那些因故没有完全传输的文件,以便加快随后的再次传输), 等同于 --partial --progress
        -z, --compress             # 对备份的文件在传输时进行压缩处理, 加快传输
        -e, --rsh=<COMMAND>     # 以ssh方式进行数据传输, -e "ssh -p2222" 指定连接2222端口, 如果是ssh方式默认使用22端口
    
        --bwlimit                # 限速, 字节/秒
        -r,--recursive             # 对子目录以递归模式处理
    
        --progress                 # 显示传输进度
        --partial                # 断点续传
        -c, --checksum             # 打开校验开关,强制对文件传输进行校验。(而不是基于文件的修改时间及大小)
        --delete                 # 删除那些 DEST 中 SRC 没有的文件。
        --delete-before            # rsync 首先读取 src 和 dest 的文件列表, 并删除那些 DEST 中 SRC 没有的文件, 之后再执行同步操作。
                                # 由于分两步骤执行, 因此需要更多的内存消耗以及时间消耗. 因此仅在 dest 的可用空间较小时用这种方式.
        --delete-excluded        # 除了删除 DEST 中 SRC 没有的文件外, 还会一并移除 dest 中 --exclude 或 --exclude-from 指定的文件/目录.
        -u, --update            # 如果 DEST 中的文件比 SRC 新(指修改时间), 就不会对该文件进行同步.    
        --exclude=PATTERN         # 指定排除不需要传输的文件, 比如 --exclude="logs" 会过滤掉文件名包含 "logs" 的文件或目录, 不对齐同步.
        --include=PATTERN         # 指定不排除而需要传输的文件模式。
        -v, --verbose             # 详细模式输出。
        -q, --quiet             # 精简输出模式。
        
        
        -a, --archive             # 归档模式,表示以递归方式传输文件,并保持所有文件属性,等于-rlptgoD
        -t, --list              # list the contents of an archive
        -l, --links                # 保留软链
        -L, --copy-links        # 同步软链时, 以实际的文件来替代
        -p, --perms             # 保持文件的权限属性
        -o, --owner             # 保留文件的属主(super-user only)
        -g, --group             # 保留文件的属组
        -D                        # 保持设备文件信息, 等同于 "--devices --specials"
        -t, --times                # 保持文件的时间属性
        
    
    示例
        rsync -P -z -r root@xx.xx.xx.xx:/data/transfer/archive.zip /data/archive.zip
        rsync -P -e "ssh -p2222" --bwlimit=200 root@xx.xx.xx.xx:/data/transfer/archive.zip /data/archive.zip
    当带宽足够大时, 使用 -z 反而会拖慢速度.

    FTP 服务

    ftp 协议

    ftp 协议: 文件传输协议

    • 需要同时建立命令链路(21端口, 先建立, 传输命令)和数据链路(传输文件名称, 目录名称, 文件数据)
    • 数据链路

      • 主动模式

        命令链路建立后, 服务端(使用20端口)主动向客户端发起建立数据链路请求(实际可能会被客户端防火墙之类的挡住, 导致无响应)
      • 被动模式(实际常用)

        命令链路建立后, 服务端会开放大于1024的端口被动等客户端连接.

    vsftpd 服务

    # 安装必要软件
    ## vsftpd 是服务端
    ## ftp 是客户端
    yum install vsftpd ftp
    
    # 启动 vsftpd 服务
    systemctl start vsftpd.service && systemctl enable vsftpd.service

    通过 man 5 vsftpd.conf 可以查看配置文件详解.

    注意

    • 默认提供匿名账号: ftp
    • 默认当前系统的账号

    vsftpd 配置文件

    • /etc/vsftpd/vsftpd.conf 主配置文件
    • /etc/vsftpd/ftpusers 用户相关
    • /etc/vsftpd/user_list 用户黑白名单, 比如禁止 root 登录

    vsftpd.conf 配置

    anonymous_enable=YES        # 是否允许匿名用户(ftp)
    local_enable=YES            # 是否允许系统本地用户账号登录. 同时会受到 SELinux(ftp_home_dir 项)的影响
    write_enable=YES            # 本地用户是否可写
    connect_from_port_20=YES    # 是否允许开启主动模式(不会影响被动模式)
    userlist_enable=YES            # 是否启用用户黑白名单
    userlist_deny=YES            # 控制用户列表是黑名单还是白名单, YES 表示黑名单. 
                                # 仅在 userlist_enable=YES 时生效
                                
                                
    # 虚拟用户配置相关
    guest_enable=YES            # 允许虚拟用户进行验证
    guest_username=<系统用户名>     # 指定登录身份为某个系统用户
    user_config_dir=/etc/vsftpd/<系统用户名>config        # 指定虚拟用户权限控制文件所在目录
    allow_writeable_chroot=YES        # 虚拟用户是否可写
    pam_service_name=vsftpd.vuser    # 虚拟用户的"可插拔验证模块(pam)"对应文件名称
    YESNO 必须是大写的.

    可以使用 man 5 vsftpd.conf 来查看该配置文件的帮助

    强制访问控制对 ftpd 的影响

    # 查看 SELinux 中和 ftpd 相关的布尔配置项
    getsebool -a | grep ftpd
    
    # 修改
    ## -P    同时写入配置文件
    ## 0     表示 off, 关闭
    ## 1    表示 on, 打开
    setsebool -P <配置项名> 1|0

    默认匿名账号

    • 账号: ftp
    • 密码: 空
    • 默认目录: /var/ftp/

    普通账号

    • 账号: 系统账号
    • 密码: 系统账号的密码
    • 默认目录: ~
    • 能访问普通账号的 home 目录

    ftpusers 配置

    vsftp 虚拟用户

    示例: 新增3个虚拟用户

    # 1. 建立一个真实系统账号
    ## 指定 /data/ftp 为该用户的home目录
    ## 指定该用户不可登录到系统
    useradd -d /data/ftp -s /sbin/nologin vuser
    
    # 2. 编写存储虚拟用户账号和密码的临时文件
    ## 该文件格式是: 一行虚拟用户名, 一行对应密码
    cat <<'EOF' > /etc/vsftpd/vuser.tmp
    u1
    123456
    u2
    123456
    u3
    123456
    EOF
    
    # 3. 将上述临时文件转成数据库专用格式
    db_load -T -t hash -f /etc/vsftpd/vuser.temp /etc/vsftpd/vuser.db
    
    # 4. 创建可插拔验证模块配置
    cat <<'EOF' > /etc/pam.d/vsftpd.vuser
    auth sufficient /lib64/security/pam_userdb.so db=/etc/vsftpd/vuser
    account sufficient /lib64/security/pam_userdb.so db=/etc/vsftpd/vuser
    EOF
    
    # 5. 修改 /etc/vsftpd/vsftp.conf 确保其中相关配置如下
        guest_enable=YES
        guest_username=vuser
        user_config_dir=/etc/vsftpd/vuserconfig
        allow_writeable_chroot=YES
        pam_service_name=vsftpd.vuser
        # 注释掉以下语句后, 就不再支持匿名和本地用户登录了
        #pam_service_name=vsftpd
        
    # 6. 创建虚拟用户配置所在目录
    mkdir /etc/vsftpd/vuserconfig
    
    # 7. 在虚拟用户配置目录中创建和所要创建虚拟用户名同名的配置文件
    ## 此处创建 u1, u2, u3 的配置文件
    ## 省略 u2 的...
    ## 省略 u3 的...
    cat <<'EOF' > /etc/vsftpd/vuserconfig/u1
    local_root=/data/ftp            # 用户登录后进入的目录
    write_enable=YES                # 可写
    anon_umask=022
    anon_world_readable_only=NO        # 可写?
    anon_upload_enable=YES            # 可上传
    anon_mkdir_write_enable=YES        # 可创建目录?
    anon_other_write_enable=YES        # 可写?
    download_enable=YES                # 可下载
    EOF
    
    # 8. 重启 vsftpd 服务
    systemctl restart vsftpd.service

    ftp 命令

    ftp客户端
    
    ftp <地址>
    
    选项
    如果提示"没有到主机的路由", 一般是由于被防火墙挡住.

    相关命令

    ls            # 在远程执行 ls
    !ls            # 在本地执行 ls
    
    pwd            # 在远程执行 pwd
    !pwd        # 在本地执行 pwd
    
    cd            # 切换远程目录
    lcd            # 切换本地目录
    
    put    <file>        # 上传文件, 若提示无权限则应检查 "write_enable" 配置项
    get <file>        # 下载文件

    samba 服务

    samba 服务

    • 使用 smb 协议
    • 使用 cifs 文件系统
    • /etc/samba/smb.conf 配置文件
    smb 协议是微软持有的版权, 用于windows之间的共享.

    而samba则是模拟这种协议, 主要用于共享给windows.

    若是 Linux 之间的共享则建议使用 nfs

    # 安装
    yum install samba
    
    # 服务
    systecmtl start|stop|restart|reload smb.service

    配置文件

    配置文件 /etc/samba/smb.conf 部分格式说明

    [global]                        # 全局设置
        workgroup = SAMBA
        security = user
    
        passdb backend = tdbsam
    
        printing = cups
        printcap name = cups
        load printers = yes
        cups options = raw
    
    
    [share]                        # 共享名
        comment = my share
        path = /data/share        # 共享路径
        read only = No            # 是否只读, No 表示可写
    man 5 smb.conf 可查看该配置文件的帮助文档

    smbpasswd 命令

    samba 用户的创建和删除
    
    smbpasswd [选项]
    
    选项
        -a         # 添加用户(系统中必须有一个同名的用户, samba 用户访问权限是参考系统同名用户的)
        -x        # 删除用户
        
        -s        # silent, 从标准输入上读取原口令和新口令, 常用于脚本处理smb
    新创建的用户默认会直接共享出自己的home目录, 也就是 /home/用户名 这个目录.

    pdbedit 命令

    samba 用户查看
    
    pdbedit [选项]
    
    选项
        -L        # 查看用户

    示例

    # 1. 创建系统用户, 此处以 user1 为例
    useradd user1
    
    # 2. 创建同名samba用户
    echo -e "123456\n123456" | smbpasswd -a user1
    
    # 3. 启动samba服务
    systemctl start smb.service
    
    # 4. 使用
    ## windows 客户端可以通过映射网络驱动器或windows共享来使用
    ## Linux 客户端可以通过磁盘挂载使用(将 127.0.0.1 上的 /home/user1 挂载到了当前的 /mnt 目录)
    ### -t cifs 可省略, 由 mount 自行判断
    ### 输入密码后就挂载成功了
    ### 挂载完毕后可通过 df -hT 或 mount | tail -1 查看挂载信息
    mount -t cifs -o username=user1 //127.0.0.1/user1 /mnt        # 挂载前面在 /etc/samba/smb.conf 里配置的 [share] 共享所指定的目录
    mount -t cifs -o username=user1 //127.0.0.1/share /mnt2        # 此处举例, 挂载在 /mnt2 文件夹上
    
    # 不需要后就卸载掉
    umount /mnt
    umount /mnt2

    nfs 服务

    主要用于 Linux 之间的共享服务.

    默认已安装

    管理

    systemctl start|stop|reload nfs.service

    /etc/exports 主配置文件

    man 5 exports 可查看帮助

    配置格式

    <共享目录> <允许来源主机>(权限)...
    
                        👆 这里不得有空格
                        可指定多个
    
    共享目录
        必须是已存在的目录.
    
    允许来源主机
        *            # 任意主机
        具体ip       # 指定该ip可访问
        
    权限(用逗号分隔)
        rw                # 读写权限
        ro                # 只读权限
        sync            # (内存)数据同步写入磁盘, 避免丢失数据
        all_squash        # 使用 nfsnobody 系统用户
        
    示例
    /data/share *(rw,sync,all_squash)

    若权限设置了 all_squash, 则会使用 nfsnobody 这个用户来做实际操作, 因此需要将该共享目录的属主和属组设为 nfsnobody

    chown -R nfsnobody:nfsnobody /data/share/

    showmount 命令

    显示关于 NFS 服务器文件系统挂载的信息
    
    showmount [选项] <host>
    
    选项
        -e, --exports    # 查看所有共享的目录

    挂载

    mount 主机:/path/dir /local/path/to/mount
    
    示例
        mount localhost:/data/share /mnt        # 将localhost上共享的 /data/share 目录挂载到本地的 /mnt 目录

    Nginx/OpenResty

    • Nginx(engine X) 是一个高性能的 Web 和反向代理服务器.
    • Nginx 支持 HTTP, HTTPS 和电子邮件代理协议

      Nginx 模块由于是用c/c++编写的, 要添加新模块还需要重新编译.
    • OpenResty 是基于 Nginx 和 Lua 实现的 Web 应用网关,集成了大量的第三方模块.

    安装

    # 添加 yum 源
    yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
    
    # 安装 openresty
    yum install -y openresty

    管理

    systemctl start|reload|stop openresty

    配置文件

    /usr/local/openresty/nginx/conf/nginx.conf

    Nginx 配置

    /usr/local/openresty/nginx/conf/nginx.conf

    worker_processes  1;        # 配置多少个worker进程, 最大值建议不大于CPU核心数
    
    error_log  logs/error.log;
    #error_log  logs/error.log  notice;
    #error_log  logs/error.log  info;
    
    pid        logs/nginx.pid;
    
    events {
        # use epoll;
        worker_connections  1024;        # 每个worker允许的并发连接, 超过会返回 503 Service Unavailable 错误
    }
    
    http {
    # 此处的配置会对下面所有 server 生效
    
        # 访问日志格式定义
        log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                          '$status $body_bytes_sent "$http_referer" '
                          '"$http_user_agent" "$http_x_forwarded_for"';
        
        # 访问日志记录文件及采用的格式配置
        access_log  logs/access.log  main;
    
        # sendfile 和 tcp_nopush 是在内核层面上优化传输链路
        sendfile        on;            # 传输文件时的优化: 直接在内核中将文件数据拷贝给socket.
        tcp_nopush     on;            # 仅sendfile启用时生效, 将http头和实体一同返回, 减少报文段数量
    
        keepalive_timeout  65;        # HTTP 协议的 keepalive, 建立长连接, 重用TCP连接
        
        gzip  on;                    # 传输时启用 gzip 压缩, 节省带宽, 造成CPU额外消耗.
    
        server {
            listen       80;            # 监听端口
            server_name  localhost;        # 域名(虚拟主机)
            
            location / {
                root   html;
                index  index.html index.htm;
            }
        }
    }
    上述配置中的相对路径是基于编译nginx时指定的特定路径, 一般是nginx所在目录, 对应此处是 `/usr/local/openresty/nginx/

    LNMP

    MySQL

    • mariadb 是 MySQL 的社区版
    # 安装
    # mariadb 是客户端
    # 
    yum install mariadb mariadb-server

    修改配置文件 /etc/my.cnf

    [mysqld]
    character_set_server=utf8
    init_connect='SET NAMES utf8'
    或者是采用 utf8mb4 编码, 兼容4字节的unicode, 需要存emoji表情的话应使用 utf8mb4

    PHP

    yum install php-fpm php-mysql
    
    systemctl start php-fpm.service
    默认源的版本是 5.4, 需要更高的可以用 webtatic 源

    此处仅作示范

    Nginx

    server {
        location ~ \.php$ {
            root           html;
            fastcgi_pass   127.0.0.1:9000;
            fastcgi_index  index.php;
            fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
            include        fastcgi_params;
        }
    }
    通过 fastcgi 协议将请求转发给 php-fpm

    DNS 服务

    DNS 服务介绍

    • DNS(Domain Name System) 域名系统
    • FQDN(Full Qualified Domain Name) 完全限定域名
    • 域分类: 根域、顶级域(TLD)
    • 查询方式: 递归、迭代
    • 解析方式: 正向解析(主机 -> ip)、反向解析(ip -> 主机)
    • DNS 服务器的类型: 缓存域名服务器、主域名服务器(master)、从域名服务器(salve)
    www.baidu.com.
    👆        👆      👆
    主机名     域名      根域
    
    
    .com    顶级域
    .        根域

    BIND 软件

    提供 DNS 服务

    # 安装
    ## bind 提供服务的软件包
    ## bind-utils DNS服务的相关工具
    yum install bind bind-utils
    
    # 服务管理
    systemctl start named.service

    主配置文件: /etc/named.conf

    options {
        listen-on port 53 { any; };        // 监听端口及对应网卡
        ...    
        allow-query     { any; };        // any    允许任何人查询
    }
    
    // 根域
    zone "." IN {
        type hint;
        file "named.ca";                // /var/named/named.ca
    };

    named-checkconf

    确认配置文件是否正确
    
    named-checkconf

    ...........DNS 本节课后续略过, 看不下去

    NAS

    NAS(Network attached storage)网络附属存储

    支持的协议:

    • nfs
    • cifs
    • ftp

    一般是通过创建磁盘阵列RAID后, 再通过上述协议共享.

    创建步骤

    假设此处提供了2个新硬盘

    • /dev/sde
    • /dev/sdf

    1. 创建共享空间

    # 磁盘分区
    fdisk /dev/sde
    fdisk /dev/sdf
    
    # 创建 RAID
    ## 此处创建 RAID1 级别的磁盘阵列
    mdadm -C /dev/md0 -a yes -l 1 -n 2 /dev/sd{e,f}1
    
    # 持久化 RAID 配置信息
    mdadm --detail --scan --verbose > /etc/mdadm.conf
    
    # 通过逻辑卷的方式以方便后续扩容
    ## 初始化物理卷
    pvcreate /dev/md0
    ## 创建卷组
    vgcreate vg1 /dev/md0
    ## 创建逻辑卷
    ### 此处示例, 因此只创建个 200M 的逻辑卷
    lvcreate -L 200M -n nas vg1
    
    # 分区格式化
    mkfs.xfs /dev/vg1/nas
    
    # 分区挂载
    mkdir /share
    mount /dev/vg1/nas /share

    2. 通过协议共享

    # 创建公用用户 shareuser
    useradd shareuser -d /share/shareuser
    echo 123456 | passwd --stdin shareuser
    
    # 1. 配置ftp共享 - 通过 shareuser 用户登录ftp并访问home目录 (也可以用虚拟用户)
    确认 /etc/vsftpd/vsftpd.conf 配置
        pam_service_name=vsftpd
        local_enable=YES
        write_enable=YES
    
    systemctl restart vsftpd.service
    
    # 2. 配置samba服务
    echo -e "123456\n123456" | smbpasswd -a shareuser
    systemctl restart smb.service
    
    # 3. 配置nfs服务
    ## 配置为 ro (nfs由于没有用户级别的限制, 因此这种情况下不推荐设置为 rw)
    echo '/share/shareuser *(ro)' >> /etc/exports
    systemctl restart nfs.service
    ## 配置为 rw (配合 facl 权限访问控制列表)
    echo '/share/shareuser *(rw,sync,all_squash)' >> /etc/exports
    setfacl -d -m u:nfsnobody:rwx /share/shareuser
    setfacl -m u:nfsnobody:rwx /share/shareuser
    查看原文

    赞 0 收藏 0 评论 0

    嘉兴ing 发布了文章 · 1月18日

    ⭐《Linux实战技能100讲》个人笔记 - 5. 文本操作篇

    [TOC]

    正则表达式与文本搜索

    元字符

    • . 匹配任意单个字符(单行模式下不匹配换行符)
    • * 匹配前一个字符任意次
    • [] 匹配范围内任意一个字符
    • ^ 匹配开头
    • $ 匹配结尾
    • \ 转义后面的特殊字符

    扩展元字符

    • + 先前的项可以匹配一次或多次。
    • ? 先前的项是可选的,最多匹配一次。
    • | 匹配前面或后面的正则表达式, "或"
    • () 分组

    重复

    一个正则表达式后面可以跟随多种重复操作符之一。
    {n}    先前的项将匹配恰好 n 次。
    {n,}   先前的项可以匹配 n 或更多次。
    {n,m}  先前的项将匹配至少 n 词,但是不会超过 m 次
    
    
    

    find 命令

    递归地在目录中查找文件
    
    find [路径...] 表达式
    
    表达式
        查找范围
        -maxdepth <level>    # 目录递归最大层数
        -mindepth <level>    # ?
    
        按文件名查找
        -name "模式"        # 完整匹配基本的文件名, 使用"通配符"匹配基本的文件名 
        -regex "模式"        # 完整匹配整个路径(并非单单匹配文件名), 使用"正则"匹配基本的文件名
        -iregex "模式"        # 完整匹配整个路径(并非单单匹配文件名) , 使用"正则"匹配基本的文件名, 但不区分大小写
        
        -regextype <reg_type>        # 改变正则表达式语法, 可选: emacs (this is the default),  posix-awk,  posix-basic,  posix-egrep  and  posix-extended
        
        按文件类型查找
        -type 类型        # b 块设备, c 字符设备, d 目录, p 命名管道, f 普通文件, l 符号链接, s 套接字
        
        按时间查找
        # 数字参数
        # +n        在这之前的
        # -n        在这之后的
        # n            正好处于该时刻
        -daystart        # 从当日0点为基准, 而不是当前时刻, 影响下述几种时间类型.
     
        -atime <n>        # Access, 最后访问时间, 单位为天(但实际是以当前时间为基准)
        -ctime <n>        # Change, 文件i节点修改时间, 单位为天
        -mtime <n>        # Modify, 文件内容修改时间, 单位为天
        
        -amin <n>        # 类似 atime, 但单位是分钟
        -ctim <n>        # 类似 ctime, 但单位是分钟
        -mmin <n>        # 类似 mtime, 但单位是分钟
        
        按大小
        -size <n>        # 默认单位是块(512字节), 支持 k(KB), M(MB), G(GB), 可以用 + - 或无符号, 意义同上面的按时间查找.
        
        按归属
        -user <user>    # 按属主
        -uid <uid>        # 按属主的id
        
        动作
        -exec 操作 \;        # 执行时无需确认, {} 作为转义字符会被替换成查找的文件
        -ok 操作 \;        # 类似 -exec, 但是每次操作都会提示确认
        
        运算符(按优先级从高到低排序)
        ()                        # 强制优先
        ! <表达式>                  # 逻辑非, 对<表达式>结果取反, 即不匹配后面条件, 比如 ! -name 表示不匹配指定文件名
        -not <表达式>                  # 逻辑非, 同 ! <表达式>
        <表达式1> <表达式2>        # 逻辑与(默认), 如果前一个<表达式>执行结果为false, 则不会执行后续<表达式>
        <表达式1> -a <表达式2>    # 逻辑与, 同上
        <表达式1> -and <表达式2>    # 逻辑与, 同上
        <表达式1> -o <表达式2>    # 逻辑或
        <表达式1> -or <表达式2>    # 逻辑或
        <表达式1> , <表达式2>        # 前一个表达式的结果不影响后一个表达式的执行
        
    cat 仅会修改 access 时间

    touch 会同时修改 access, modify, change

    chmod 仅会修改 change 时间

    注意:

    • 不同参数的前后顺序很重要, 比如 -daystart 需要写在 -atime 等之前, 否则对其不生效.

    示例

    # 仅删除昨天的日志
    find /path/to/log -daystart -mtime 1 -exec rm -v {} \;

    grep 命令

    文本内容过滤(查找)

    查找文本中具有关键字的一行
    
    说明
        若未提供查找的文件名或是 - 则默认是标准输入
    
    语法
        grep [选项] 模式 文件...
        grep [选项] (-e 模式 | -f 包含模式的文件) 文件...
        
    选项
        模式
        -G, --basic-regexp            # 使用基本正则(默认)
        -E, --extended-regexp        # 使用扩展正则
        -e 模式, --regexp=模式         # 当模式以 - 开头时, 应使用这种方式
        
        -v, --invert-match            # 反向匹配, 只选择不匹配的行    
        -i, --ignore-case            # 忽略大小写    
    
        -R, -r, --recursive            # 递归地读每一目录下的所有文件。这样做和 -d recurse 选项等价。
        
        显示内容
        -A <n>, --after-context=<n>        # 打印匹配行的后续 n 行
        -B <n>, --before-context=<n>    # 打印匹配行的前面 n 行
        -o, --only-matching                # 只显示匹配的部分(默认是显示匹配行的所有内容)
        -n, --line-number                # 同时显示行号
    
        修改显示类型, 不进行通常输出
        -c                                # 打印匹配到多少行
        -l, --files-with-matches        # 打印匹配的文件名
        -L, --files-without-match        # 打印不匹配的文件名
        
        
            
        
    
    

    注意:

    • 在输入选项时尽量使用引号包围, 避免Shell去解释, 比如 grep \. 实际上模式是 . 也就是匹配任意字符. 而 grep "\."grep '\.' 才是匹配模式 \.

    在基本正则表达式中,元字符 ?, +, {, |, (, 和 ) 丧失了它们的特殊意义;作为替代,使用加反斜杠的 (backslash) 版本 ?, +, {, |, (, 和 ) 。

    cut 行切割

    在文件的每一行提取片段
    
    cut 选项 [FILE]...
    
    选项
        -d, --delimiter <分隔>        # 以指定分隔符来切割, 分隔符必须是单个字符
        -f, --fields <list>            # 输出指定位置的字段, 用逗号分隔多个位置, 位置从1开始

    uniq 连续重复行处理

    删除排序文件中的"连续"重复行
        默认从标准输入读取, 输出到标准输出
    
    uniq 选项 [INPUT [OUTPUT]]
    
    选项
        -c, --count        # 在首列显示重复数量
        -d, --repeated    # 仅显示重复的行

    sort 排序

    对文本文件的行排序
    
    sort 选项 [FILE]...
    
    选项
        字段类型
        -n            # 按照数值排序, 默认包含 -b
        -k            # 
        
        -r            # 逆向排序
        
        -b            # 忽略开头的空格

    seq 产生数字序列

    产生数字序列
    
    语法
        seq [OPTION]... LAST
        seq [OPTION]... FIRST LAST
        seq [OPTION]... FIRST INCREMENT LAST

    tac 倒序显示

    tac [选项] <文件=STDIN>

    行编辑器

    非交互式, 基于行操作的模式编辑.

    sed 行编辑器

    sed 是单行文本编辑器, 非交互式.

    sed 的模式空间, 其基本工作方式

    1. 将文件以行为单位读取到内存(称作模式空间)
    2. 使用sed的每个脚本依次对该行进行操作
    3. 打印模式空间的内容并清空
    4. 读取下一行, 重复执行上述步骤

    sed 的空间示意图:

    image-20200302180855354

    • 模式空间的内容默认会输出, 并清空
    • 保持空间的默认内容是 \n

    Tip

    • 使用 ; 可以替换多个 -e

    模式空间 pattern space

    s 替换命令

    替换
    
    sed [选项] '[<寻址>]s<分隔符><old><分隔符><new><分隔符>[<标志位>]' [文件...]        # 简单示例: sed 's/old/new/'
    
    参数
        old        # 支持正则表达式
        分隔符      # 可以采用 / 也可以采用其他来避免与正则匹配内容冲突, 比如 ~ @ 等
    
    寻址(默认是所有行)    
        !            # 对寻址取反, eg. "2,4!d" 表示不删除2~4行
        <n>            # 只替换第<n>行
        <n1>,<n2>    # 区间: 从<n1>到<n2>这几行        eg. /12\/Apr\/2020/,/13\/Apr\/2020/      正则同样可以使用这种区间寻址
        1,<n>        # 替换从开始到第<n>行
        <n>,$        # 替换从第<n>到结束的这些行
        /正则/       # 仅替换符合此正则匹配到的行
                    # eg.   sed '/正则/s/old/new/' 
                    # eg.  sed '/正则/,$s/old/new/'    (正则可以和行号混用)表示从匹配到的正则那行开始到结束都替换 
                    # 寻址可以匹配多条命令, 注意大括号. eg.   /regular/{s/old/new/;s/old/new/}    
                    # 比如nginx日志需要筛选出 14号这天的: sed -n '/14\/Apr\/2020/,/15\/Apr\/2020/ p' xx.log
     
    标志位(默认是只替换第1个匹配项)
        /g        # 替换所有匹配项(默认只替换第1个匹配项)
        /<n>    # <n>是一个数字, 表示每行只替换第<n>个匹配项
        /p        # 打印模式空间的内容(即匹配的行), 通常会和 -n 一起使用, 如果不和 -n 一起使用, 会导致匹配的行多输出依次.
                # eg. sed -n 's/old/new/p'                 # 此时仅打印匹配的行
        /w <file>    # 将模式空间的内容(即匹配到的行)写入到指定文件
    
    
    选项
        -r, --regexp-extended            # 使用扩展正则表达式, 包括圆括号分组及回调.
        -e script, --expression=script    # 指定多个执行脚本时, 每个脚本前用一个 -e. 可以使用 "分号" 简写
                                        # Eg. -e 's/old1/new1/' -e 's/old2/new2/'
        -f script-file, --file=script-file    # 加载脚本文件
        -i[<后缀>], --in-place[=<后缀>]  # 修改原始文件, 当指定后缀时则会生成对应的备份文件. 也可以直接输出重定向输出到其他文件
        -n, --quiet, --silent            # 默认不自动打印
    
    示例
        # 使用扩展正则表达式
        sed -r 's/old/new/' [文件]...
    
        # 执行多个脚本
        sed -e 's/old/new/' -e 's/old/new/' [文件]...
        sed 's/old/new/;s/old/new/'                        # 使用分号隔开不同脚本
    
        # 将结果写回文件保存
        sed -i 's/'
        sed -i,.bak 's/'
        
        # 圆括号分组及回调
        echo "a213123t" | sed -r 's/a(\d*)/b\1/g'        # b213123t

    d 删除命令

    删除当前"模式空间的内容", 并放弃后面的命令, 读取新的内容并重新执行sed
        改变脚本的控制流, 读取新的输入行
        (由于模式空间的内容被删除了, 因此d后面的脚本没法执行, 会略过)
        (使用 s 替换成空内容, 但本质上这一行内容还在, 依旧会执行后续脚本, 会输出)
    
    sed '[<寻址>]d' [文件...]
    
    寻址
        同 s 命令
    
    示例
        sed '1d'            # 删除第一行
        sed '/^\s*#/d'        # 删除 # 开头的行

    a 追加命令

    在匹配行的下一行插入内容
    
    sed '[<寻址>]a <插入内容>'
    
    示例
        sed '1i haha'        # 在原先第1行前面插入 haha

    i 插入命令

    在匹配行在上一行插入内容
    
    sed '[<寻址>]i <插入内容>'
    
    示例
        sed '2i haha'        # 在原先第二行前面插入 haha

    c 改写命令

    将匹配行替换成指定内容
        指定匹配连续几行时, 只会替换1次    # sed '2,5c <替换内容>'
    
    sed '[<寻址>]c <替换内容>'
    
    示例
        sed '2c hehe'        # 将第2行替换成 "hehe"

    r 读文件并插入 (从文件读取改写内容)

    在匹配行下面分别插入指定文件中的内容
    
    sed '[<寻址>]r <文件名>'
    
    示例
        sed '$r afile' bfile > cfile        # 将 afile 内容追加到 bfile 结尾并合并成新的文件 cfile
    常用于合并多个文件

    w 写文件 ?

    ?
    
    sed '[<寻址>]w <文件名>'

    p 打印

    与 替换命令 s 的标志位 /p 不一样.

    输出匹配的行(不禁止原始的输出)
    
    sed [选项] '[<寻址>]p'
    
    示例
        sed -n '<寻址>p'        # 只打印匹配的行

    n 提前读入下一行, 并指向下一行

    sed 'n'
    
    示例
        cat <<EOF | sed 'n'
        1
        2
        3
        4
        5
        EOF
        
        # 输出(由于未作任何操作, 因此原样输出)
        1
        2
        3
        4
        5
        
        # ----------------------
    
        cat <<EOF | sed -n 'n;p'
        1
        2
        3
        4
        5
        EOF
        
        # 输出
        2
        4

    正常模式

    img

    使用n命令后

    img使用n命令后

    图来源: https://blog.51cto.com/studyi...

    q 退出命令

    遇到匹配项, 在处理完该项后退出
    
    sed '[<寻址>]q'
    
    示例
        sed '2q'        # 在打印完第二行后退出
        sed '/root/q'    # 在匹配到某一行有 root 后退出

    打印前N行的一个比较

    • sed 10q filename 读取前10行就退出
    • sed -n '1,10p' filename 逐行读入全部, 只显示1-10行, 更耗时.

    = 打印行号

    打印出当前行号(单行显示)
        不影响正常的打印
    
    sed '='
    
    示例
        echo 'a' | sed '='        # 打印结果, 第一行 "1", 第二行 "a"

    多行模式空间 pattern space

    多行模式空间改变了 sed 的基本流程, 通过使用 N, P, D 读入并处理多行.

    主要应对配置文件是多行的情况, 比如 json 或 xml.

    综合示例

    a.txt 内容如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    # ------------------- 示例1 ---------------------#
    sed 'N;N;s/\n/\t/g;' a.txt
    1    2    3
    4    5    6
    7    8    9
    
    
    # ---------------------示例2 打印梯形结构 -------------------#
    # 关键在于利用 D 改变控制流
    sed -n 'P;N;s/\n/\t/;s/^/\n/;D' a.txt
    1
    1    2
    1    2    3
    1    2    3    4
    1    2    3    4    5
    1    2    3    4    5    6
    1    2    3    4    5    6    7
    1    2    3    4    5    6    7    8
    1    2    3    4    5    6    7    8    9
    b.txt 内容如下
    hell
    o bash hel
    lo bash
    
    
    # -------------------- 示例 hello bash 替换成 hello sed --------------#
    sed 's/^\s*//;N;s/\n//;s/hello bash/hello sed\n/;P;D;' b.txt
    hello sed
    hello sed

    N 将下一行加入到模式空间

    读取时, 将下一行加入到模式空间
        两行视为同一行, 但中间有个换行符 \n
        此时多行模式 . 可以匹配到除了末尾的其他换行符
    
    sed 'N'
    
    示例
        sed = <文件> | sed 'N;s/\n/\t/'        # 在每行前面加入行号

    使用N命令后

    img

    图来源: https://blog.51cto.com/studyi...

    D 删除模式空间中的第一个字符到第一个换行符

    注意
        会改变控制流, 不再执行紧随的后续命令, 会再次回到脚本的初始处(但不清空模式空间)
    
    sed 'D'

    P 打印模式空间中的第一个字符到第一个换行符

    注意
        仅仅是打印, 并不会删除打印的部分.
    
    sed 'P'

    保持空间 hold space

    注意:

    • 保持空间在不存储东西时, 它里面的默认内容是 \n

      因此第一次一般是用 h 覆盖掉保持空间的 \n
    • 保持空间的内容只能临时存储和取出, 无法直接修改

    综合示例

    # 下述多个都实现了 tac 倒序显示的效果
    # 思路: 每次将本轮正确的结果保存在保持空间
    cat -n /etc/passwd | head -n 6 | sed -n '1!G;$!x;$p'
    cat -n /etc/passwd | head -n 6 | sed -n '1!G;h;$p'
    cat -n /etc/passwd | head -n 6 | sed '1!G;h;$!d'
    cat -n /etc/passwd | head -n 6 | sed '1!G;$!x;$!d'
    cat -n /etc/passwd | head -n 6 | sed -n '1h;1d;G;h;$p';
    cat -n /etc/passwd | head -n 6 | sed -n '1h;1!G;h;$p';
    
    sed '=;6q' /etc/passwd | sed 'N;s/\n/\t/;1!G;h;$!d'
    
    # --------------------- 显示结果 --------------------#
    6    sync:x:5:0:sync:/sbin:/bin/sync
    5    lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
    4    adm:x:3:4:adm:/var/adm:/sbin/nologin
    3    daemon:x:2:2:daemon:/sbin:/sbin/nologin
    2    bin:x:1:1:bin:/bin:/sbin/nologin
    1    root:x:0:0:root:/root:/bin/bash

    h 和 H 将模式空间内容存放到保持空间

    • h 是覆盖
    • H 是追加

    注意: 该操作不会清空模式空间(需要清空模式空间可以用 d)

    g 和 G 将保持空间内容取出到模式空间

    • g 是覆盖
    • G 是追加

    注意: 该操作不会清空保持空间

    x 交换模式空间和保持空间内容

    awk 行编辑器

    awk 是一种解释型编码语言, 常用于处理文本数据.

    awk 主要是对 sed 的一个补充, 常用于在sed处理完后对相应结果进行调整并输出.

    awk 版本

    • awk: 初始版本
    • nawk: new awk, 是 awk 的改进增强版
    • gawk: GNU awk, 所有 GNU/Linux 发行版都包含 gawk, 且完全兼容 awk 与 nawk

      实际 centos 用的就是 gawk

    参考文档:

    awk 和 sed 的区别

    • awk 更像是脚本语言
    • awk 用于"比较规范"的文本处理, 用于统计数量并调整顺序, 输出指定字段.
    • 使用 sed 将不规范的文本处理为"比较规范"的文本

    awk 脚本的流程控制

    1. BEGIN{} 输入数据前例程, 可选
    2. {} 主输入循环
    3. END{} 所有文件读取完成例程

    个人理解的执行顺序

    1. 执行开始块(可选) BEGIN{}
    2. 若存在主体块 {} 或结束块 END{}`, 则打开文件并读入数据

      如果文件无法打开会在此时报错.

      主体块允许存在多个, 比如根据不同的匹配模式, 写多个主体块.

    3. 若存在<寻址>/pattern/, 则会依次匹配, 通过则对该记录执行 {}
    4. 读完所有记录后, 执行结束块(可选) END{}
    特殊情况, 脚本只包含 BEGIN{} 时, 在执行 awk 命令后面传入参数(非文件名), 此时不会导致报错, 因为不会执行到步骤2(即尝试打开文件).

    如果脚本只包含 BEGIN{命令} , 好像可以缩写成 awk '命令' ??

    语法

    
    语法
        awk [选项] 'awk脚本内容' [ -- ] [文件...]        # 其中任意一部分都是可选的
        awk [选项] -f <awk脚本文件> [ -- ] [文件...]    # 不直接在命令行中书写 awk 脚本, 而是从指定文件读取
    
    选项
        -f <脚本文件>, --file=<脚本文件>     # 从指定脚本文件读取 awk 脚本, 而不是默认地从第一个命令行参数读取.
        -F, --field-separator <分隔符>        # 字段分隔符可以使用正则表达式, 是空格 " ", 也可以在awk脚本中用 FS="," 来修改这一行为
        -v <var>="val", --assign <var>="val"  # 直接为awk脚本中的变量赋值, 比如 -v suffix=yjx, 然后代码中直接就存在 suffix 这个变量了, 且值是 yjx
    
    
        --dump-variables[=<file="awkvars.out">]        # 将awk中的所有全局变量及其值导出到指定文件中.
                                          
    
    awk脚本的块
        开始块
            BEGIN {}         # 特殊模式: 读取输入数据前
        主体块
            表达式 {}              # 若匹配上才执行后续的 action              
                            # eg. 
                            #        `$1 == "top" {}`
                            #        `NR >= 2 {}`
                            #        'length($0)'
            /正则/ {}           # 正则匹配, 若匹配上才执行后续的 action        
                            # eg. 
                            #        /^menu/    只处理 menu 开头的记录 
                            #        /cost/    只处理本行内容中包含 "cost" 的记录
            !/正则/ {}      # 正则不匹配
            组合模式 {}         # 一个 组合模式 通过与(&&),或(||),非(|),以及括弧来组合多个表达式
            {}                # 每读取一行数据则执行后续的 action
            模式1,模式2 {}     # 范围模式(range pattern)匹配从与 模式1 相匹配的行到与 模式2 相匹配的行(包含该行)之间的所有行,对于这些输入行,执行 语句 。
        结束块
            END {}            # 特殊模式: 读取完毕后
    
        
        
        
    主体块的action
        print    打印(若未配置则默认是 print)
        next    对于本行的处理, 跳过后续步骤表达式
        {...}    执行 {} 中的脚本
    
    
    
    
    
    
    示例.
        awk [选项] 'BEGIN{} [<条件>] {} END{}' 文件...    # 其中任意一部分都是可选的
        
    
                                         # BEGIN{} 输入数据前例程
                                         # {} 主输入循环, <寻址> 是应用于 {} 的
                                         # END{} 所有文件读取完成例程
                                         
        awk -f <脚本.awk>            # 从文件中加载执行的命令, 注意<寻址>要和 { 写在同一行, 不然好像没生效?
                                 # 示例 xx.awk
                                 # BEGIN {
                                 #        ...
                                 # }
                                 # /过滤条件/ {
                                 #         ...
                                 # }
                                 # END {
                                 #        ...
                                 # }
    
    
    
    字段引用
        $0                # 表示记录整行
        $1 $2 ... $n     # 表示第1~第n个字段
        NF                # 标识 $0 被分割的字段数
        $NF                # 表示最后一个字段, NF 变量表示字段的数量, 比如当前共5个字段, 则 $NF 等价于 $5.
    
                                        
        
        
    简单示例 
        awk -F ',' '{print $1,$2,$3}' filename     # 逗号分隔, 打印前3个字段
        echo "menuentry 'CentOS Linux (5.5.6) 7 (Core)' --class centos" | awk -F "'" '{print $2}'    # 提取出内核版本

    修改字段或NF值的联动效应

    注意以下几种操作

    • 修改 $0 会根据 FS, 重新划分字段并自动赋值给 $1, $2, ... , NF.

      $0=$0 也会触发重新划分字段的操作.

    • 修改 $1, $2, ... , 会根据 OFS 重新生成 $0, 但不会重新分割.

      即使是 $1=$1 也会触发上述操作, 当然是用 NF=NF 也是可以的.

      # 利用该特性重新生成去除行首行尾空格, 压缩中间空格的效果
      echo "   a   b   c  " | awk '{NF=NF; print $0;}'
      
      输出
      a b c
    • 赋值给不存在的字段, 会新增字段并按需使用空字符串填充中间的字段,并使用 OFS 重新计算$0
    • 增加 NF 值,将使用空字符串新增字段,并使用 OFS 重新计算 $0
    • 减少 NF值,将丢弃一定数量的尾部字段,并使用OFS重新计算$0

    正则字面量

    这里有个地方容易被坑到!!!

    任何单独出现的 /pattern/ 都等价于 $0 ~ /pattern/, 这个在将正则表达式赋值给变量时特别容易被坑, 举例:

    • if(/pattern/) 等价于 if($0 ~ /pattern/)
    • a = /pattern/ 等价于将 $0 ~ /pattern/ 的匹配返回值(0或1)赋值给 a
    • /pattern/ ~ $1 等价于 $0 ~ /pattern/ ~ $1 ,表示用 $1 去匹配0或1
    • /pattern/ 作为参数传给函数时,传递的是 $0~/pattern/ 的结果0或1
    匹配成功时返回1, 失败返回0.

    举例

    # 这边直接用于匹配没什么问题
    awk 'BEGIN { if ("abc" ~ "^[a-z]+$") { print "match"} }'
    
    #输出
    match
    
    
    
    awk 'BEGIN { if ("abc" ~ /^[a-z]+$/) { print "match"} }'
    
    #输出
    match
    
    
    # 这边将其赋值给其他变量, 此时其实 regex = $0 ~ /^[a-z]+$/, 也就是值 0
    awk 'BEGIN { regex=/^[a-z]+$/; print regex; if ("abc" ~ regex) { print "match"} }'
    
    #输出
    0
    
    
    
    awk 'BEGIN { regex="^[a-z]+$"; print regex; if ("abc" ~ regex) { print "match"} }'
    
    #输出
    ^[a-z]+$
    match

    在 awk 中书写正则可以用 /[0-9]+/ 也可以用

    /[0-9]+/
    匹配方式:"str" ~ /pattern/或"str" !~ /pattern/
    匹配结果返回值为0(匹配失败)或1(匹配成功)
    任何单独出现的/pattern/都等价于$0 ~ /pattern/
    if(/pattern/)等价于if($0 ~ /pattern/)
    坑1:a=/pattern/等价于将$0 ~ /pattern/的匹配返回值(0或1)赋值给a
    坑2:/pattern/ ~ $1等价于$0 ~ /pattern/ ~ $1,表示用$1去匹配0或1
    坑3:/pattern/作为参数传给函数时,传递的是$0~/pat/的结果0或1
    坑4.坑5.坑6…

    内置变量

    awk 中可以看作是在一个独立的系统空间中, 因此也有其特殊的系统变量.

    注意:

    • 字段的引用不能加 $, 这点与Shell不一样, 不然就变成获取记录中某个字段的值了.

    控制 AWK 工作的预定义变量

    • FS (field separator)输入数据的分隔符, 默认值是空格

      awk -F ","
      # 等价于
      awk 'BEGIN {FS=","}'        # 在读入文件之前设置字段分隔符
    • OFS (output field separator)输出字段分隔符(默认是空格)

      awk 'BEGIN {OFS=","}'
    • FIELDWIDTHS 以指定宽度切割字段而非按照 FS
    • FPAT 以正则匹配, 将匹配到的结果作为字段, 而非按照 FS 或 FIELDWIDTHS 划分. 理解为 re_match, 而不是 re_split

      FPAT = "([^,]+)|(\"[^\"]+\")"
      
      # 上述 FPAT 用于分割以逗号分隔的 csv 文件, 若使用双引号包裹的则视为是一个字段(忽略其中的逗号).
      # 比如对于数据:       abc,"pqr,mno"
      # $1 值为 abc
      # $2 值为 "pqr,mno"
      https://stackoverflow.com/que...
    • RS (record separator)记录分割符(默认是 \n)

      该变量通常在 BEGIN 块中修改, 修改该变量可以控制 awk 每次读取的数据的范围(默认是读取一行), 读取到的记录是不包含记录分隔符的.

      • RS 设置为单个字符: 直接使用该字符来分割记录
      • RS 设置为多个字符: 视为正则(非兼容模式), 使用该正则来分割记录.
      awk 'BEGIN {RS=":"}'        # 将记录分割符设置为 :   , 这样每次遇到 : 时就视为一条记录分别处理.

    特殊读取需求

    • RS="" : 按段落读取(这个特性有用)
    • RS="\0" : 一次性读取所有数据, 但有些特殊文件包含了空字符 \0
    • RS="^$" : 真正的一次性读取所有数据, 因此 ^$ 匹配的是空文件
    • RS="\n+" : 按行读取, 但忽略空行
    • ORS (output row separator) 输出记录分隔符(默认是 \n)

      awk 'BEGIN {OFS=":"}'
    • CONVFMT 表示数据转换为字符串的格式, 默认值是 %.6g
    • OFMT 表示数值输出的格式, 默认值是 %0.6g, 标识有效位(整数部分加小数部分)最多为6.
    • IGNORECASE 控制是否对大小写敏感, 当该变量设置时, 忽略大小写. 在分割记录时也受该变量影响.

      awk 'BEGIN{IGNORECASE=1} /amit/' marks.txt

    携带信息的预定义变量

    文件与行号
    • FILENAME 当前被处理的文件名

      BEGIN {} 块中, 该变量是未定义的.
    • NR (number of rows)记录的行号

      会一直累加, 就算处理多个文件, 该行号也会一直累加.

    • FNR (file number of rows)记录的行号(处理不同文件时会重置)

      当处理多个文件时, 切换到不同文件则该值会重置掉.

    • NF (number of fields)字段数量

      最后一个字段内容可以用 $NF 取出
    • ARGIND 用于处理多个文件时, 表示当前正在处理的文件的顺序(从 1 开始)

      awk 'ARGIND==1 {if(FNR>3)print FNR,$3 } ARGIND==2 {if(FNR>1)print FNR,$2} ARGIND==3 {if(FNR<3)print FNR,$NF}' s.log t.log s.log
    • RT (Record Termination) 实际记录分割符

      当 RS 设置为多个字符(正则)时, 在每条记录被读取并分割后, RT 变量会被设置为实际用于划分记录的字符.

    命令行与环境参数
    • ARGC 命令行位置参数个数("选项"是不包含在内的)
    • ARGV 命令行位置参数数组

      • ARGV[0] 值是命令名本身, 即 awk
      • ARGV[1] 是传入的第1个参数
      • 范围: ARGV[0] ~ ARGV[ARGC - 1]
    • ENVIRON 存放系统环境变量的关联数组

      awk 'BEGIN{print ENVIRON["USER"]}'        # 输出: shell 变量 "USER"
    • PROCINFO 关联数组, 保存进程相关的信息

      # 打印 awk 进程的Id
      awk 'BEGIN { print PROCINFO["pid"] }'
    • ERRNO 用于存储当 getline 重定向失败或 close 函数调用失败时的失败信息.

    表达式

    赋值操作符

    • =

      • = 的左右是可以有空格的.
      • 字符串拼接中的空格会被忽略

        Eg. var = "hello" "world"

        实际上 var 值是 "helloworld", 没有中间的空格

      • 若是拼接两个字符串变量, 则使用字符串字面量隔开即可.

        s3=s1""s2

    • ++

      支持前置递增和后置递增

    • --

      支持前置递减增和后置递减

    • +=
    • -=
    • *=
    • /=
    • %=
    • ^=

    算数操作符

    • +
    • -
    • *
    • /
    • %
    • ^

    位操作

    • AND 按位与操作
    • OR 按位或操作
    • XOR 按位异或操作

    关系操作符

    • <
    • >
    • <=
    • >=
    • ==

      注意, 判断两个值是否相等要用 ==, 而不是赋值运算符 =

    • !=
    • ~ 字符匹配正则表达式
    • !~

    布尔操作符

    • &&
    • ||
    • !

      使用 ! 时注意使用括号将相关表达式括起来, 避免写错.

    三元运算符

    condition expression ? statement1 : statement2

    匹配运算符

    除了块的条件匹配外, 还可以用于 if 判断之类的.

    • ~ 匹配指定正则表达式的, 用于主体块的条件匹配

      # 仅处理包含 hello 文本的行
      awk '$0 ~ "hello"' xx.txt
      
      
      # 注意这里正则是用 / / 包围起来, 而不是双引号
      awk 'BEGIN { if ("[abc]" ~ /\[.*\]/) { print "match";}}'
      #输出
      #match
      
      # 注意这里用双引号括起来时, 里面用了双斜杠来处理正则的 [
      awk 'BEGIN { if ("[abc]" ~ "\\[abc\\]") { print "match";}}'
      #输出
      #match
      
    • !~ 不匹配指定正则表达式的, 用于主体块的条件匹配

      # 仅处理不含 hello 文本的行
      awk '$0 !~ "hello"' marks.txt

    条件和循环

    注意:

    • 表达式结果: 0为false, 1为true

      这个与 Shell 是相反的.
    • 影响控制的语句: break, continue

    综合示例

    cat kpi.txt
    
    user1 70 72 74 76 74 72
    user2 80 82 84 82 80 78
    
    
    #-------------- 计算每行数值的总之和平均值 -----------#
    awk '{total=0; avg=0; for (i=2;i<=NF;i++) {total+=$i;} avg=total/(NF-1); print $1,total,avg;}' kpi.txt
    
    user1 438 73
    user2 486 81

    if 条件语句

    if (表达式) {
    
    } else if (表达式) {
    
    } else {
    
    }
    执行多条语句要用 {}, 只有一条语句时可忽略.

    for 循环

    for (初始值; 循环判断条件; 累加) {
    }

    while 循环

    while (表达式) {
    
    }

    do 循环

    do {
    
    } while(表达式)

    数组

    普通数组

    • awk 的数组实际上是关联数组, 可通过下标(数字实际上也是字符串)依次访问

      比如 arr[1]arr["1"] 实际上是对同一个 key 操作
    • 支持多维数组

      gawk(可以认为是 awk 的加强版, 替代版, 至少 centos 的 awk 实际就是 gawk) 支持真正意义上的多维数组.

      awk 还有一种更"原始"的使用一维数组模拟多维数组的, 但在 gawk 中已经没必要了.

    # 定义
    ## 下标可以是数字(视为字符串)或字符串
    数组名[下标] = 值
    
    
    # 遍历
    for (变量 in 数组名) {
        数组名[变量]            # 获取对应数组值
    }
    
    
    # 删除数组
    delete 数组
    # 删除数组元素
    delete 数组[下标]
    
    
    # 判断数组中是否存在指定"键"
    if (key in array)
    # 判断数组中是否不存在指定"键", 注意这里额外加了一个括号, 不能省略了
    if (!(key in array))

    命令行参数数组

    • ARGC 命令行位置参数个数
    • ARGV 命令行位置参数数组

      • ARGV[0] 值是命令名本身, 即 awk
      • ARGV[1] 是传入的第1个参数
      • 范围: ARGV[0] ~ ARGV[ARGC - 1]

    示例

    cat argv.awk
    
    内容
        BEGIN {
            for (i=0; i<ARGC; i++) {
                print ARGV[i];
            }
            print ARGC;
        }
    
    
    # --------------------------------#
    awk -f argv.awk  afile 11 22 33
    
    输出
        awk        # ARGV[0]
        afile    # ARGV[1]
        11        # ARGV[2]
        22        # ARGV[3]
        33        # ARGV[4]
        5        # ARGC
    此处不会报错是因为awk脚本中只包含 BEGIN {} 部分, 因此不会将参数视为文件名并尝试打开.

    数组函数

    • length(数组) 获取数组长度
    • asort(数组a[, 数组b, ...]) 对数组a的值进行排序,并且会丢掉原先键值(重新生成数字递增的 key 来替代), 并将结果赋予数组 b (若未传, 则直接修改数组 a).
    • arorti(数组a[, 数组b, ...]) 对数组a的键进行排序, 并将结果赋予数组 b (若未传, 则直接修改数组 a).

    函数

    算术函数

    • sin()
    • cos()
    • atan2(y,x)
    • exp(x) 返回自然数 e 的 x 次方
    • sqrt() 平方根
    • log(x) 计算 x 的自然对数
    • int() 转换为整数(忽略小数部分)
    • rand() 伪随机数, 范围 [0,1), 默认使用 srand(1) 初始化随机种子.

      若不使用 srand() 会发现每次获取的所谓随机数都是一样的.

      srand(); print rand();

    • srand([seed]) 重置随机种子, 默认种子采用当前时间的 epoch 值(秒级别)

    位操作函数

    • compl(num) ` 按位求补
    • lshift(num, offset) 左移N位
    • rshift(num, offset) 右移N位

    字符串函数

    awk 中涉及字符索引的函数, 索引位都是从 1 开始.

    注意, 不同 awk 版本, 函数参数个数是有可能不一样的.

    • sprintf(format, expr1, ...) 返回格式化后的字符串

      示例: a = sprintf("%10s\n", "abc")

    • length(s) 返回字符串/数组的长度
    • strtonum(str) 将字符串转换为十进制数值

      如果 str 以0开头,则将其识别为8进制

      如果 str 以0x或0X开头,则将其识别为16进制

    • tolower(str) 转换为小写
    • toupper(str) 转换为大写
    • 查找

      • index(str,substr) 在目标字符串中查找子串的位置, 若返回 0 则表示不存在.
      • match(string, regexp, array) 字符串正则匹配, 将匹配结果保存在 arr 数组中.

        变量 RLENGTH 表示 match 函数匹配的字符串长度.

        变量 RSTART 表示 match 函数匹配的字符串的第一个字符的位置.

        awk 'BEGIN { if (match("One Two Three", "re")) { print RLENGTH } }'        # 输出 2
        
        awk 'BEGIN { if (match("One Two Three", "Thre")) { print RSTART } }'    # 输出 9
        cat test
        # this is wang,not wan
        # that is chen,not che
        # this is chen,and wang,not wan che
        
        awk '{match($0, /.+is([^,]+).+not(.+)/, a); print a[1],a[2]}' test
        # wang  wan
        # chen  che
        # chen  wan che
    • 替换

      • gsub(regx,sub [,targe=$0]) 全局替换, 会直接修改原始字符串, 返回替换成功的次数.

        如果 target 使用 $0, $... 等, 那么替换成功后会使用 OFS 重新计算 $0

        这边 sub 不支持反向引用, 只能使用 & 来引用匹配成功的部分

      • sub(regx,sub [,targe=$0]) 只替换第一个匹配的, 会直接修改原始字符串, 返回替换成功的次数.
      • gensub(regx, sub [, how [, target]]) 不修改原字符串, 而是返回替换后的字符串. 可以完全替代 gsubsub

        how: 指定替换第几个匹配, 比如 1 表示只替换第一个匹配, gG 表示全局替换

        这是 gawk 提供的函数, 其中 sub 支持使用 \N 引用分组匹配, 或 &, \0 来标识匹配的整个结果.

        awk 'BEGIN {
            a = "111 222"
            b = gensub(/(.+) (.+)/, "\\2 \\1, \\0, &", "g", a)
            print b
        }'
        
        
        # 输出
        222 111, 111 222, 111 222
    • 截取

      • substr(str,pos,num=剩余所有) 从指定位置开始截取一个子串
    • 分割

      • split(str, arr [, 正则分隔符=FS]) 字符串分割为数组, 并将其保存到第2个参数中, 函数返回值是分割的数
      • patsplit(str, arr[, 正则分隔符=FPAT]) 使用正则捕获匹配的字符串, 并将其保存到第2个参数中.

    若不清楚的, 可以在 man awk 中搜索相应关键字

    时间函数

    • systime 返回当前时间戳(秒级)
    • mktime("YYYY MM DD HH mm SS [DST]") 根据给定的字符串格式, 返回其对应的时间戳

      # 格式: 年 月 日 时 分 秒
      awk 'BEGIN{print mktime("2020 10 10 17 51 59")}'
    • strftime([format [, timestamp[, utc-flag]]]) 将时间戳(默认是当前时间)转换为字符串表示

      awk 'BEGIN {print strftime("Time = %Y-%m-%d %H:%M:%S")}'
      
      输出
      
      Time = 2020-10-12 17:53:37
      SN描述
      %a星期缩写(Mon-Sun)。
      %A星期全称(Monday-Sunday)。
      %b月份缩写(Jan)。
      %B月份全称(January)。
      %c本地日期与时间。
      %C年份中的世纪部分,其值为年份整除100。
      %d十进制日期(01-31)
      %D等价于 %m/%d/%y.
      %e日期,如果只有一位数字则用空格补齐
      %F等价于 %Y-%m-%d,这也是 ISO 8601 标准日期格式。
      %gISO8610 标准周所在的年份模除 100(00-99)。比如,1993 年 1 月 1 日属于 1992 年的第 53 周。所以,虽然它是 1993 年第 1 天,但是其 ISO8601 标准周所在年份却是 1992。同样,尽管 1973 年 12 月 31 日属于 1973 年但是它却属于 1994 年的第一周。所以 1973 年 12 月 31 日的 ISO8610 标准周所在的年是 1974 而不是 1973。
      %GISO 标准周所在年份的全称。
      %h等价于 %b.
      %H用十进制表示的 24 小时格式的小时(00-23)
      %I用十进制表示的 12 小时格式的小时(00-12)
      %j一年中的第几天(001-366)
      %m月份(01-12)
      %M分钟数(00-59)
      %n换行符 (ASCII LF)
      %p十二进制表示法(AM/PM)
      %r十二进制表示法的时间(等价于 %I:%M:%S %p)。
      %R等价于 %H:%M。
      %S时间的秒数值(00-60)
      %t制表符 (tab)
      %T等价于 %H:%M:%S。
      %u以数字表示的星期(1-7),1 表示星期一。
      %U一年中的第几个星期(第一个星期天作为第一周的开始),00-53
      %V一年中的第几个星期(第一个星期一作为第一周的开始),01-53。
      %w以数字表示的星期(0-6),0表示星期日 。
      %W十进制表示的一年中的第几个星期(第一个星期一作为第一周的开始),00-53。
      %x本地日期表示
      %X本地时间表示
      %y年份模除 100。
      %Y十进制表示的完整年份。
      %z时区,表示格式为+HHMM(例如,格式要求生成的 RFC 822或者 RFC 1036 时间头)
      %Z时区名称或缩写,如果时区待定则无输出。

    其他函数

    • getline

      请参照下方的 "getline" 部分.

    • close(xxx [, from|to])

      关闭文件、shell进程, 可以仅关闭某一端.

      close(xxx, "to") 表示关闭该管道的写入端, close(xxx, "from") 表示关闭该管道的输出端.

      注意!!!! awk 中任何文件都只会在第一次使用时打开, 之后都不会再重新打开(而是从上次的读取位置继续). 因此只有在关闭之后, 再次使用时才会重新打开.

    • next 跳过对当前记录的后续处理.

      会回到 awk 循环的头部, 读取下一行.

    • nextfile 停止处理当前文件, 从下一个文件开始处理.
    • return xx 函数返回值
    • system("shell命令") 执行 shell 命令, 并返回退出的状态值, 0表示成功.
    • flush([output-expr]) 刷新打开文件或管道的缓冲区

      如果没有提供 output-expr,fflush 将刷新标准输出。若 output-epxr 是空字符串 (""),fflush 将刷新所有打开的文件和管道。

    • close(expr) 关闭文件句柄 ???

      awk 'BEGIN {
          cmd = "tr [a-z] [A-Z]"
          print "hello, world !!!" |& cmd        # "&|" 表示双向管道通信
          close(cmd, "to")                    # 关闭其中一个方向的管道, 另一个是 "from" 方向
          cmd |& getline out                    # 使用 getline 函数将输出存储到 out 变量中
          print out;
          close(cmd);                            # 关闭管道
      }'
      
      输出
      
      HELLO, WORLD !!!
    • exit <code=0> 终止脚本

    自定义函数

    function 函数名(参数) {
        awk 语句
        return awk变量
    }

    注意

    • 自定义函数的书写不能再 BEGIN{}, {}, END{} 的里层

    getline

    getline 函数用于读取一行数据.

    根据不同情况, 返回值不一样.

    • 若读取到数据, 返回 1.
    • 若遇到 EOF, 返回 0.
    • 发生错误, 返回负数. 如-1表示文件无法打开,-2表示IO操作需要重试(retry)。在遇到错误的同时,还会设置 ERRNO 变量来描述错误.
    awk '{print $0; getline; print $0}' marks.txt 
    
    # 建议使用 getline 时判断一下是否读取成功
    awk 'BEGIN {getline; if ((getline) > 0) {...}}'

    从当前文件读取

    • 使用 getline 不带参数时, 表示从当前正在处理的文件中立即读取下一条记录并保存到 $0, 同时进行字段分割(分别存到 $1, $2,...), 同时会设置 NF, RT, NR, FNR, 然后继续执行后续代码.
    • 执行 getline <变量名> 时, 会将读取结果保存到对应变量中, 而不会更新 $0, 也不会更新 NF, $1, $2, ..., 此时仅仅会更新 RT, NR, FNR.

    从其他文件读取

    • 执行 getline < "filename" 表示从指定文件读取一条记录并保存到 $0 中(同时进行字段分割), 及 NF. 至于 NR, FNR 则不会更新.

      每次读取记录后会自动记录读取的位置.

      配合 whilegetline 的返回值可以遍历完文件.

      注意 getline < abcgetline < "abc" 是两码事, 一个是从 abc 变量指向的文件读取, 一个是读取 abc 文件
    • 执行 getline 变量名 < "filename" 表示从指定文件读取一条记录并保存到指定变量中.

    从 shell 命令输出结果中读取

    • cmd | getline:从Shell命令 cmd 的输出结果中读取一条记录保存到 $0

      会进行字段划分,设置变量 $0NF$NRT,不会修改变量 NRFNR

    • cmd | getline var:从Shell命令 cmd 的输出结果中读取数据保存到 var

      除了 varRT,其它变量都不会设置

    如果要再次执行 cmd 并读取其输出数据,则需要close关闭该命令, 示例:。

    # 若屏蔽下方的 close 函数, 则再次读取该 cmd 时为空. 因此需要关闭先.
    
    awk 'BEGIN {cmd = "seq 1 5"; \
    while((cmd | getline) > 0) {print}; \
    close(cmd); \
    while((cmd | getline) > 0) {print}; \
    }'

    可以方便地使用 shell 给 awk 中变量赋值

    awk 'BEGIN {get_date = "date +\"%F %T\"";  \
    get_date | getline cur_date; \
    print cur_date; \
    close(get_date);
    }'
    
    输出
    2020-10-13 19:14:17

    将数据传给 shell 处理完(coprocess), 再读取

    这里要利用 coprocess, 也就是 |& 操作符.

    awk 可以利用 |& 将一些不好处理的数据传给 shell 来处理后, 再从 shell 中读取结果, 继续在 awk 中处理.

    使用 shell 的排序功能

    # sort 命令会等待数据写入完毕后(即 EOF 标记)才开始排序, 因此实际是在执行 close(CMD, "to") 时才开始执行.
    awk 'BEGIN {
    CMD = "sort -k2n";
    print "6 66" |& CMD;
    print "3 33" |& CMD;
    print "7 77" |& CMD;
    close(CMD, "to");
    while ((CMD |& getline) > 0) {
        print;
    }
    close(CMD);
    }'
    
    
    输出:
    3 33
    6 66
    7 77

    使用注意:

    • awk-print |& cmd 会直接将数据写进管道, cmd 可以从管道中获取数据
    • 强烈建议在awk_print写完数据之后加上 close(cmd,"to") ,这样表示向管道中写入一个EOF标记,避免某些要求读完所有数据再执行的cmd命令被永久阻塞.

      关闭管道另一端可以使用 close(cmd, "from")

    • 如果 cmd 是按块缓冲的,则 getline 可能会陷入阻塞。这时可将 cmd 部分改写成 stdbuf -oL cmd 以强制其按行缓冲输出数据

      CMD="stdbuf -oL cmdline";awk_print |& CMD;close(CMD,"to");CMD |& getline

    高级输出

    print 输出重定向

    • print "..."
    • print "..." > 文件名 重定向输出
    • print "..." >> 文件名 重定向输出

      这玩意比使用 system 函数再调用 echo 快了好几个量级
    • print "..." | "shell 命令" awk 将创建管道, 并启动 shell 命令. print 产生的数据放入管道, shell 命令则从管道中读取数据.
    • print "..." |& "shell 命令"; "shell 命令" | getline 和上面的 | 不同之处在于, 这里是将数据交给 Coprocess, 之后 awk 还需要再从 Coprocess 取回数据.

      Coprocess 执行 shell 命令的时候, 结果是不输出到标准输出的, 而是需要从管道中自行读取.

    支持重定向到

    • 标准输入 /dev/stdin
    • 标准输出 /dev/stdout
    • 标准错误 /dev/stderr

    若 print 输出后发现后续的管道命令没有内容, 那其实是因为 awk 的输出存在缓存, 可使用 fflush() 函数刷新缓冲区.

    关于 ||& 使用上区别的示例

    # 这里用的是 |, 执行结果直接输出到标准输出
    awk 'BEGIN {
    cmd = "tr \"[a-z]\" \"[A-Z]\""
    print "hello" | cmd
    > }'
    
    
    # 这里用的是 |&, 执行结果需要手动从 Coprocess 管道读取
    awk 'BEGIN {
        cmd = "tr \"[a-z]\" \"[A-Z]\""
        print "hello" |& cmd;
        close(cmd, "to");
        cmd |& getline line;
        print line;
        close(cmd);
    }'
    
    
    输出
    HELLO

    printf 格式化

    printf(format, value1, value2, ...)
    
    参数
        format    格式  
            %c        (将ASCII转换为)字符
            %s        字符串
            %d,%i    整数    
            %e,$e    科学计数法表示
            %f,%F    浮点数
            %g,%G    浮点数, 会移除对数值无影响的 0
            %o        无符号八进制
            %u        无符号十进制
            %x,%X    无符号十六进制
            %%        百分号
        
                        
    注意: 输出时不会自动换行
    
        
    示例
        printf("total pay for %s is $%.2f\n", $1, $2 * $3)
    
        # %-8s        字符串, 8个字符宽度, 左对齐
        # %6.2f        浮点数, 6个字符宽度, 保留2位小数
        printf("%-8s %6.2f\n", $1, $2 * $3)
    参考: https://awk.readthedocs.io/en...

    若仅仅是为了格式化字符串(不输出), 可以用 sprintf 函数.

    format 支持的转义序列

    • 换行符 \n
    • 水平制表符 \t
    • 垂直制表符\v

      理解为输出光标垂直向下移动一行
    • 退格符 \b

      理解为输出光标后退一格.
    • 回车符 \r

      理解为光标会回退到当前行的开头(常用于覆盖本行)
    • 换页符 \f

      这个效果...得试一下才行

    format 的 % 的可选参数

    • 宽度

      只有当字段的宽度比要求宽度小时该标示才会有效, 默认使用空格字符填充.

      printf("%10d", 1)
      
      
      输出
               1
    • 前导零 0

      只有当字段的宽度比要求宽度小时该标示才会有效

      awk 'BEGIN {printf("%010d", 1)}'
      
      
      输出
      0000000001
    • 左对齐 -

      % 和数字之间使用 - 符号即可指定左对齐

      awk 'BEGIN {printf("%-10d", 1)}'
      
      输出
      1
    • 符号前缀 +

      使用 +, 在输出数字时, 将其正负号也输出.

      awk 'BEGIN {printf("%+10d", 1)}'
      
      输出
              +1
    • 保留进制标识 #

      awk 'BEGIN {printf("%#o    %#X\n", 10, 10)}'
      
      输出
      012    0XA

    Examples

    打印出当前的可用内核列表

    并显示序号

    awk -F "'" '/^menuentry/ {print x++,$2}' /boot/grub2/grub.cfg
    
    输出
    0 CentOS Linux (5.5.6) 7 (Core)
    1 CentOS Linux (3.10.0-1062.12.1.el7.x86_64) 7 (Core)
    2 CentOS Linux (3.10.0-957.el7.x86_64) 7 (Core)
    3 CentOS Linux (0-rescue-d64aa77b8f014365aa6557986697df9c) 7 (Core)

    统计当前tcp的各个状态及数量

    netstat -ntp | awk '/^tcp / {S[$6]++} END{for (i in S) print i,S[i];}'
    
    输出
    TIME_WAIT 108
    ESTABLISHED 154

    统计每个接口的访问时间

    time cat 20_10_*.txt | grep "request cost" | gawk -v suffix=_test -f ../stat_method.awk

    stat_method.awk

    BEGIN {
        DEBUG = 1
    
        methodRecordFile = "method_record"suffix".csv"
        methodStatsFile = "method_stats"suffix".csv"
    
        if (DEBUG) {
            methodRecordFile = "/dev/stdout"
            methodStatsFile = "/dev/stdout"
        }
    
        print methodRecordFile
        print methodStatsFile
    }
    
    {
        method=substr($10,2,length($10)-2)
        method=substr(method,1,length(method)-7)
        timeStr=$1" "$2
        timeMs=$6 + 0.01
        uid=substr($11,2,length($11)-2)
    
        msLevel=(int(timeMs/100) + 1) * 100
    
        T[method][msLevel]++
    
        if (!(method in Stats)) {
            Stats[method]["name"] = method
            Stats[method]["max"] = 0        
            Stats[method]["mean"] = 0
            Stats[method]["count"] = 0
            Stats[method]["sum"] = 0
            Stats[method]["sumX2"] = 0
            Stats[method]["min"] = 9999999999999
        }
    
        if (timeMs > Stats[method]["max"]) {
            Stats[method]["max"] = timeMs
        }
    
        if (timeMs < Stats[method]["min"]) {
            Stats[method]["min"] = timeMs
        }
    
        Stats[method]["sumX2"] += timeMs * timeMs
    
        Stats[method]["count"]++
        Stats[method]["sum"] += timeMs
    
        if (NR % 10000 == 0) {
            print "已处理 "NR" 条记录"
        }
    }
    
    END {
        print "----------- 总共处理 "NR" 条记录 -----------"
    
    
        print "method,msLevel,count" > methodRecordFile
        # print "method,msLevel,count"
        for (m in T) {        
            for (l in T[m]) {
                print m","l","T[m][l] >> methodRecordFile
                # print m","l","T[m][l]
            }
        }
    
        print "----------- done -----------"
        
        print "method,min,max,mean,sd(标准差),count,sum(秒)" > methodStatsFile
        printf("%-30s\tmin\tmax\tcount\tmean\tsd(标准差)\t,sum(秒)\n", "method")    
        for (m in Stats) {
            Stats[m]["mean"] = Stats[m]["sum"] / Stats[m]["count"]
            Stats[m]["sd"] = sqrt(Stats[m]["sumX2"] / Stats[m]["count"] - Stats[m]["mean"] * Stats[m]["mean"])
    
            # for (n in Stats[m]) {
                # print "Stats["m"]["n"]= " Stats[m][n]
            # }
    
            print m "," Stats[m]["min"] "," Stats[m]["max"] "," Stats[m]["mean"] "," Stats[m]["sd"]"," Stats[m]["count"]","Stats[m]["sum"]/1000 >> methodStatsFile
            printf("%-30s\t%.2f\t%.2f\t%.0f\t%.2f\t%d\t%.2f\n", substr(m, 0, 30), Stats[m]["min"], Stats[m]["max"],Stats[m]["mean"],Stats[m]["sd"], Stats[m]["count"], Stats[m]["sum"]/1000)        
        }
    
        print "----------- done -----------"
        print methodRecordFile
        print methodStatsFile
    } 

    格式化空白

    cat > a.txt <<'EOF'
          aaaa        bbb     ccc
       bbb     aaa ccc
    ddd       fff             eee gg hh ii jj
    EOF
    
    awk 'BEGIN{OFS=" "} {NF=NF; print}' a.txt

    输出

    aaaa bbb ccc
    bbb aaa ccc
    ddd fff eee gg hh ii jj

    打印 ini 文件中的某一段

    awk -v scope="extras" 'BEGIN {scope="["scope"]"} 
    $0 == scope {
        print;
        while ((getline) > 0) {
          if ($0 !~ /\[.*\]/) { print $0; } else { exit }
        }
    }
    ' /etc/yum.repos.d/CentOS-Base.repo

    输出如下 👇

    [extras]
    name=CentOS-$releasever - Extras
    mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=extras&infra=$infra
    #baseurl=http://mirror.centos.org/centos/$releasever/extras/$basearch/
    gpgcheck=1
    gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
    
    #additional packages that extend functionality of existing packages

    处理字段中包含字段分割符情况(csv)

    echo 'Robbins,Arnold,"1234 A Pretty Street, NE","MyTown",MyState,12345-6789,USA' | awk 'BEGIN { FPAT="[^,]+|\"[^\"]+\"" } { print NF"    "$3}'

    输出如下 👇

    7    "1234 A Pretty Street, NE"
    查看原文

    赞 0 收藏 0 评论 0

    嘉兴ing 发布了文章 · 1月18日

    ⭐《Linux实战技能100讲》个人笔记 - 4. Shell篇

    [TOC]

    认识 Shell

    什么是 Shell

    Shell 是命令解释器, 将命令解释给内核来执行, 它是用户和内核之间的中间层.

    用户
    👇
    Shell
    👇
    内核

    查看所有的shell类型

    cat /etc/shells
    /bin/sh
    /bin/bash            # 基于 bsh 的加强重构版, 是 CentOS 7 和 Ubuntu 的默认 Shell
    /usr/bin/sh
    /usr/bin/bash
    /bin/tcsh
    /bin/csh

    Linux 的启动过程

    启动顺序从上至下
    
            BIOS                基本的输入输出系统
            |
            MBR                    硬盘的主引导部分(前446字节是主引导记录, 前512除了主引导记录还包括磁盘分区表)
            |
            BootLoader(grub)    启动和引导内核的工具, 目前使用的是 /boot/grub2
            |
            kernel                内核
    CentOS 7 |   CentOS 6   
            /  \       
    systemd        init                  1号进程(在CentOS 6中是 /usr/sbin/init 进程, CentOS 7则是 /usr/lib/systemd/systemd )
          |         |
    系统初始化    由shell脚本完成引导      CentOS 7 中有一部分是由systemd配置, 应用程序引导. 系统初始化仍是由shell脚本完成.
          |
       shell

    CentOS 6 在 init 后面的引导会和CentOS 7有略微的差异.

    在CentOS 6 中, init 的引导步骤

    1. /etc/rc.d/rc.sysinit 系统初始化工作
    2. 等待用户终端接入

    在CentOS 7中, systemd 的步骤

    1. /etc/systemd/system 读取启动级别
    2. /usr/lib/systemd/system 读取各个service
    # 导出主引导记录
    dd if=/dev/sda of=mbr.bin bs=446 count=1
    hexdump -C mbr.bin                            # 查看主引导记录
    
    # 导出主引导记录和磁盘所有分区表
    dd if=/dev/sda of=mbr2.bin bs=512 count=1
    hexdump -C mbr2.bin                            # 最后面以 55aa 结尾表示可引导

    如何编写shell脚本

    标准的 Shell 脚本要包含的元素

    • Sha-Bang

      首行的 #! 开头的部分

      文本文件首行添加 #!/bin/bash 可以在以 ./脚本.sh 这种方式执行脚本时声明当前是 bash 脚本, 系统会自行选择对应的 shell 来执行, 若是以 bash 脚本.sh 则会被视为注释.

    • # 开头的视为注释
    • 脚本执行权限, 若是二进制可执行文件只需要 x, 若是文本文件则需要 rx
    • 通常约定bash脚本的扩展名为 .sh
    • 在一行中可使用 ; 分隔多条命令, 会依次按顺序逐个执行命令, 只有在前一个命令执行完才会执行后一个命令.

    确保脚本执行错误时马上退出

    可在脚本开头设置: set -e , 从而告知 bash, 若有任何语句执行失败, 就直接退出脚本, 防止错误像滚雪球般变大.

    此时 $? 无法使用

    若在 set -e 模式下为了确保某些语句失败不退出脚本, 可采用如下方式

    # 方法1
    command || { echo "command failed"; exit 1; }
    
    # 方法2
    if ! command; then
        echo "command failed"
        exit 1
    fi

    关于 set 更多参见 set 命令

    shell 脚本执行方式

    执行命令的4种方式

    • bash file.sh

      会在当前终端下产生一个 bash 子进程, 再由该子进程去执行该脚本.

      这种方式无需赋予脚本执行权限
    • ./file.sh

      同样会在当前终端下产生一个子进程, 会根据脚本的 Sha-Bang(即第一行的 #!/path/to/bash) 来解释该脚本.

      比如python脚本第一行是 #!/usr/bin/python

      需要 赋予脚本执行权限

    • source file.sh

      在当前进程执行该脚本, 脚本中的操作会影响当前环境.

      这种方式同样无需赋予脚本执行权限.

      由于是在当前进程执行, 因此操作会影响当前进程, eg 脚本中的 cd /tmp 同样会改变当前环境的工作目录.

    • . file.sh

      .source 的缩写, 等价于 source file.sh

    补充

    • exec <command>

      使用 command 进程替换当前进程, PID 不变, command 执行完后直接退出.

    内建命令和外部命令的区别

    内建命令

    • 不需要创建子进程
    • 对当前 Shell 生效
    比如 cd 是内建命令, 执行时会切到当前 Shell 的工作目录

    shell 选项

    shopt 命令

    查看/设置shell选项
    
    shopt [选项] [<选项名>]
    
    示例
        shopt              # 查看所有选项及其值
        shopt <选项名>        # 查看指定选项的值
    
    选项
        -s        set, 设置值为 on
        -u        unset, 设置值为 off
        
    部分重要选项
        login_shell            # 指示当前是 login shell 还是 non-login shell

    实用示例

    获取当前脚本所在目录

    # 将当前目录保存到变量 PWD 中(注意命令)
    PWD="$(cd $(dirname ${BASH_SOURCE[0]}) && pwd -P)"

    BASH_SOURCE 变量是一个数组, 其第一个参数是当前脚本名

    若使用 source 来执行脚本时, 参数 $0 的值是父脚本的名字, 而不是当前脚本的名字.

    打印消息时带日期时间

    log() {
      echo $(date "+%Y-%m-%d %H:%M:%S")" "$*
    }
    
    log "lala"

    管道与重定向

    一个进程默认会打开标准输入、标准输出、标准错误三个文件描述符.

    • 标准输入默认是由终端输入
    • 标准输出和标准错误默认是输出到终端

    管道与管道符 |

    管道和信号都是进程通信的方式之一.

    匿名管道(管道符 | )是 Shell变成经常用到的通信工具.

    管道符 | , 将前一个命令执行的结果传递给后面的命令.

    • 管道实际上是将不同的进程的标准输出和标准输入做一个连接.
    • 将前一个命令的 标准输出 连接到下一个命令的 标准输入
    • 管道符是通过创建子进程的方式来运行的

      子进程如果是一个 shell, 则称之为 子shell.

      在有管道符的命令中运行内建命令其实是在新的子shell中执行的, 不会影响当前shell环境, 这个需要理解好.

      因此一般避免在管道符中使用内建命令

    • 如果连接的是外部命令, 则会按顺序同时建立多个子进程来分别执行外部命令, 同时按顺序连接各个标准输出和标准输入

    示例

    ps | cat
    echo 123 | ps

    示例

    image-20200227112936228

    cat 子进程的标准输出(1)重定向至匿名管道(463978), 而 less 的标准输入(0)重定向至同一个匿名管道(463978), 也就是 cat 的标准输出通过匿名管道连接重定向至 less 的标准输入.

    注意管道符和分号的区别:

    • 分号隔开的多条命令是没有任何关系的, 且每次只会执行一条, 执行完后才会继续下一条.
    • 管道符隔开的多条命令是具有输入输出重定向关系的, 会几乎同时启动(实际顺序是从做到右), 其中执行的命令(包括内建命令)都是在新的子进程中执行, 而不是在当前shell中执行.

    返回码

    正常可以使用 $? 获取上一条命令的执行结果状态码(0 正常, 非0异常)

    但是若是在执行一条管道后使用 $? 获取的只是管道最后一个指令执行返回的状态码

    $PIPESTATUS 变量类似 $?, 但它保存的是管道中每个命令的返回码

    • ${PIPESTATUS[0]} 表示管道中第一个命令的返回码
    • 若上一条命令不是管道, 同样会更新 `$PIPESTATUS

    参考: https://www.cnblogs.com/suane...

    重定向符号

    重定向符号实际上是将进程的标准输入和标准输出与文件建立连接.

    • 利用文件代替终端输入
    • 利用文件代替终端输出

    所有重定向符号包括

    • 输入重定向

      • <
      • <<EOF
      • <<"EOF" 不转义特殊字符
      • <<'EOF' 不转义特殊字符
    • 输出重定向

      语法
          [<文件描述符=1>]<重定向符号>
      
      参数解释
          <文件描述符>
              1        标准输出(不写则默认)
              2        标准错误
              &        标准输出和标准输出
              
          <重定向符号>
              >        清空并写入
              >>        追加写入
              
      示例
          &>>                        # 将标准输出和标准错误重定向至文件, 并追加写入
          1>1.txt 2>>2.txt        # 将标准输出重定向至 1.txt 文件并清空该文件后写入. 同时将标准错误重定向至 2.txt 文件并追加写入.
          # 组合使用输入和输出重定向(转义内容, 其中的变量会被替换, `whoami` 命令会被执行并替换)
          cat > /path/to/file <<EOF
          i am $USER
          `whoami`
          EOF
          # 组合使用输入和输出重定向(不转义内容)
          cat >> /var/spool/cron/root <<'EOF'
      EOF
    
    
    
    
    
    
    ### 使用输入重定向来代替标准输入
    

    输入重定向

    read <变量名> <<EOF
    123
    EOF

    从文件输入重定向

    echo 123 > tmp.txt
    read <变量名> < /tmp.txt

    
    > 不能使用管道, 因为管道里的命令是在新的进程中执行, 读取的变量无法影响当前环境
    >
    > ```sh
    > # 无效
    > echo "123" | read <变量名>
    > ```
    >
    > 
    
    
    
    ## xargs 命令
    

    从标准输入构建并执行命令行

    - 适用于待执行命令只能从参数中而不是标准输入读取值的情况
    

    xargs [选项] <command=echo>

    选项

    分隔符
    -d                        # 定义输入分隔符, 默认是空白和换行
    --null, -0                # 以 null 作为分隔符, 常搭配使用(文件名可能有空格, 反斜杠等) `find -print0 | xargs -0`
    
    分割成多个命令并分别执行
    -n <n>                    # 将最多 <n> 个参数用于构建一个命令行
    -L <n>                    # 如果标准输入包含多行,-L参数指定多少行作为一个命令行参数(分别执行多次命令). 一般更常用 -n
    
    -I <替换符>              # 使用-I指定一个替换字符串,这个字符串在xargs扩展时会被替换掉,当-I与xargs结合使用,每一个参数命令都会被执行一次
    
    --interactive, -p        # 逐个命令确认是否执行, 只有回复  y 或 Y 开头的才会执行, 否则略过
    --verbose, -t            # 在执行之前在标准错误输出显示待执行的命令
    
    --max-procs <n>            # 最多同时运行多少个进程, 默认是 1, 如果是 0 则表示不限制. 可与 -n 配合, 避免只执行一次 exec
    
    

    用法

    <前一个命令> | xargs            # 通过管道符重新构建待执行命令
    xargs                        # 由用户手动输入(Ctrl+D 结束输入), 并构建待执行命令
    

    示例: echo, rm, mkdir, ls 等命令

    # 简单的 echo 示例
    echo 123 | xargs echo
    # 使用每2个参数执行一次命令
    echo {0..9} | xargs -n 2 echo
    # echo 执行了3次(以下几种等效)
    echo -e "a\nb\nc" | xargs -L 1 echo
    echo -e "a\nb\nc" | xargs -n 1 echo
    echo "a b c" | xargs -n 1 echo
    # 找出所有 TXT 文件以后,对每个文件搜索一次是否包含字符串abc。
    find . -name "*.txt" -print0 | xargs -0 grep "abc"
    
    
    
    
    
    
    # 变量
    
    ## 变量定义
    
    Shell 的变量不区分类型
    
    
    
    命名规则
    
    - 字母、数字、下划线
    - 不以数字开头
    
    
    
    ## 变量赋值
    
    ### read 命令
    
    交互方式
    

    通过标准输入读取变量值

    read [选项] 变量名

    选项

    -a             # array  assign the words read to sequential indices of the array
                # variable ARRAY, starting at zero
    -p <prompt>    # 在读取变量前, 打印一个提示文本(不换行)
    -r            # 不解析反斜杠, 即读入原始值(正常使用时推荐)
    
    > 写Shell脚本时一边会避免用交互式来给变量复制, 除非是有必要.
    
    
    
    
    
    *持续读取管道中的数据*
    

    seq 1 10 | while read line; do echo $line; done;

    
    
    
    
    
    ### 非交互方式
    
    注意: `=` 相邻的左右两侧不允许出现空格.
    
    
    
     **字符串赋值**
    

    变量名=变量值

    eg.

    a=123

    
    > 等号的左右不允许出现空格
    >
    > 变量值包含空格时, 需要用 `""` 或 `''` 包含起来
    >
    > 单引号, 双引号的区别:
    >
    > - 单引号: 不会对变量值中的引用命令、引用变量、转义字符等进行解析。
    > - 双引号: 会解析变量中的引用命令、引用变量、转义字符, 再将解析后的值赋值给变量名
    
    
    
    **数学表达式赋值**
    

    let 变量名=变量值

    示例

    let a=10+20            # a 的值是 30
    
    > 变量值只能是数学表达式
    >
    > 由于 Shell 的计算性能较差, 一般不怎么用来计算
    
    
    
    
    
    **将命令赋值给变量**
    

    变量名="命令"

    如果变量的值是可执行命令, 则可直接使用 $<变量名> 来执行命令.
    但如果要将运行结果赋值给另一个变量, 则需配合 $ 或 `

    示例

    直接执行变量值
    l="ls -hl";     $l                # 此时等价执行了 ls -hl
    
    将变量值执行结果赋值给另一个变量
    cmd="uptime";    result=$($cmd)    # 将 uptime 运行后的结果赋值给 result 变量
    cmd="uptime";    result=`$cmd`    # 同上
    
    > 常用于拼接命令后再执行
    
    
    
    **将命令结果赋值给变量**
    

    变量名=$(命令)
    变量名=命令

    示例

    current=`pwd`
    current=$(pwd)
    
    
    
    
    
    ## 变量的引用
    
    - `${变量名}` 表示对变量的引用
    
    - `echo ${变量名}` 查看变量的值
    
    - `${变量名}` 在部分情况下可以省略为 `$变量名`
    
      > 部分清空指: 在变量名后面紧跟其他字符时
      
    - `${!变量名}` 对变量的引用的引用
    

    t1=t2
    t2="i am t2"
    echo $t1 # 输出 "t2"
    echo ${!t1} # 输出 "i am t2"

    
    
    
    
    
    ## 变量的作用范围
    
    变量的作用范围默认只在当前Shell中, 其父、子、平行 Shell 都是不可见的.
    
    
    
    > 如果想要某个脚本中定义的变量在当前Shell生效, 则有两种方法:
    >
    > - 在当前shell中执行 `source 脚本.sh` 或 `. 脚本.sh`
    > - 
    
    
    
    **变量的导出 export**
    

    在父进程中执行 export, 从而让子进程能够获取父进程中的变量.

    export 变量名[=变量值]

    
    > 在子进程中对父进程的变量名修改在父进程是不感知的(无效), 但是在子孙进程是有效的.
    
    
    
    **删除变量 unset**
    

    删除变量

    unset 变量名

    
    
    
    ## 系统环境变量
    
    ### 临时设置环境变量
    
    环境变量指的是: 每个 Shell 打开都可以获得到的变量.
    
    > 环境变量都是经过 export 的, 因此对其修改会影响到子进程.
    
    
    
    若只是想临时修改某个环境变量来执行某个程序, 那么可以方便地类似如下所示:
    

    <环境变量名>=<环境变量值> <命令>

    示例

    LANG=c man iptables        # 查看英文版本的man帮助
    
    
    
    
    
    ### 部分变量解释
    
    #### 环境变量
    
    **很重要**
    

    PATH # 当前命令的搜索路径, 用 : 分隔. 可以用如下方式新增命令搜索路径

            # PATH=$PATH:/path/to/bin
            

    PS1 # 当前终端提示文本

    
    
    
    **知道即可**
    

    USER # 当前用户名
    UID # 当前用户id

    
    
    
    #### 预定义变量
    

    $? # 上一条命令是否正确执行, 0 表示正确, 1 表示有出错.

    $$ # 当前进程号

    $0 # 所属进程的进程名, 而不是脚本名!!! 这个概念不一样

            # 如果用 bash 方式来执行脚本则该值是脚本名, 如果用 source的方式则该值是父进程的进程名.
            # 这个联系之前的脚本执行方式很容易理解
    

    $LINENO # shell脚本当前的行号

    
    > `$?` 常用于判定上一条命令是否正确执行, 从而实现脚本自动化处理异常.
    
    
    
    #### 位置变量
    

    $* # 脚本执行的所有参数
    $@ # 脚本执行的所有参数
    $# # 参数个数

    $1 # 第1个参数
    ...
    $9
    ${10} # 第10个参数, 此时不能省略大括号

    
    
    
    `$*` 与 `$@` 不同之处在于用双引号括起来时行为不一样
    

    当传入参数为 a b c 时

    "$*" # "a b c"
    "$@" # 'a' 'b' 'c

    
    
    
    
    
    
    
    
    
    ### env 命令
    

    查看当前的所有变量(包括环境变量)

    env

    
    
    
    ### set 命令
    

    修改shell环境运行参数

    set # 显示所有环境变量和Shell环境参数
    set [参数] [-o option-name] [arg ...] # 设置shell环境参数

    选项

    -u                # 使用到不存在的变量时报错(unbound variable)并终止脚本(默认忽略), 等价 -o nounset
    -x                # 打开回显, 每个命令执行的时候会输出所执行的命令, 方便调试复杂脚本(默认不打开), 等价 -o xtrace
    
    -e                # 命令运行失败时退出脚本, 防止错误累计, 实际开发建议打开(默认忽略), 等价 -o xtrace
                    # 注意不适用于管道(除非是在管道的最后一个子命令)
    +e                # 适用于临时关闭 `-e`
    
    -o pipefail        # 管道中任意一个子命令失败都退出脚本(默认不会, 即使打开 -e)
    

    常用写法

    set -euxo pipefail
    set -eux -o pipefail
    

    也可以在执行 bash 脚本时从命令行传入:

    bash -euxo pipefail script.sh
    
    
    
    `set` 部分参考: http://www.ruanyifeng.com/blog/2017/11/bash-set.html
    
    
    
    #### set -e
    
    开启 `set -e` 后若部分语句允许失败(或失败后需要执行其他逻辑), 则可采用如下写法
    

    写法1

    command || true
    command || { echo "fail"; }

    写法2

    if !command; then

    :

    fi

    写法3

    set +e # 临时取消 -e

    do something

    set -e # 恢复 -e

    
    
    
    
    
    
    
    ### 环境变量配置文件
    
    配置文件
    
    - `/etc/profile`
    - `/etc/profile.d/*`
    - `~/.bash_profile`
    - `~/.bashrc`
    - `/etc/bashrc`
    
    
    
    
    
    #### 从存储位置划分:
    
    - `/etc/` 下的配置是所有用户通用
    
    - `~/` 下的配置是仅个人有效
    
    
    
    
    
    #### 从文件类型划分:
    
    - `profile`
    
      配置环境变量
    
    - `bashrc`
    
      别名及函数定义
    
    
    
    
    
    #### 根据 login 和 no-longin shell 划分
    
    用户在登录时分为以下两种 Shell 
    
    
    
    可以通过如下命令查看当前属于哪种
    

    shopt login_shell # on 表示 login shell, off 表示 non-login shell

    
    
    
    
    
    **login shell**
    
    - 包括: `su - `
    
    - 会加载 `profile` 和 `bashrc` 类文件.
    
    - 加载顺序如下
    

    su - root

    loading /etc/profile
    loaded /etc/profile

    loading ~/.bash_profile
    loading ~/.bash_rc
    loading /etc/bashrc
    loaded /etc/bashrc
    loaded ~/.bash_rc
    loaded ~/.bash_profile

    graph TB

    1(/etc/profile) --步骤 1--> 1
    2(/root/.bash-profile) --步骤 2--> 3
    3(/root/.bash-rc) --步骤 3--> 4
    4(/etc/bashrc) --步骤4--> 3
    3 --步骤5--> 2
    
    > 上述图中
    >
    > 由于 `~` 会被转义, 因此用具体的 `/root/` 替代
    >
    > 由于 `_` 显示不出来, 因此用 `-` 替代
    
    
    
    
    
    **no-login shell**
    
    - 包括: `su` 不加减号
    
    - 仅 `bashrc` 类的文件会被加载到.
    
    - 何时开始加载: 当运行 `bash`时
    
    - 加载顺序如下:
    

    su root

    loading ~/.bash_rc
    loading /etc/bashrc
    loaded /etc/bashrc
    loaded ~/.bashrc

    
    - 这种方式配置加载是不完全, 和正常登录环境不i一样, 因此一般不建议使用.
    
    
    
    ### /etc/profile
    
    系统启动和终端启动时的系统环境初始化
    
    
    
    ### /etc/bashrc
    
    函数和命令别名
    
    
    
    ## 字符串处理
    
    变量默认值相关
    

    不改变原变量值

    ${变量名-默认值} # 变量未定义, 使用默认值
    ${变量名:-默认值} # 变量为空时, 使用默认值. 为空包括未定义或空(只包含一个空格的不失为空)
    ${变量名:+默认值} # 变量不为空时, 使用默认值

    改变原变量

    ${变量名=默认值} # 变量未定义, 使用默认值, 同时修改原变量
    ${变量名:=默认值} # 变量为空时, 使用默认值, 同时修改原变量

    直接报错

    ${变量名:?提示文本} # 变量为空时提示报错

    
    
    
    字符串操作
    

    ${#变量名} # 字符串长度

    ${变量名:pos:length} # 从位置 pos(下标从0开始)开始提取字串 length 个字符.

                          #     pos 可省略, 默认为0
                          #     length 可省略, 默认为到字符串结尾
    

    ${变量名#substring} # 前缀匹配, 删除匹配的字串(非贪婪模式)

                          #     substring 要删除的字串, 支持"通配符"

    ${变量名##substring} # 贪婪模式

    ${变量名%substring} # 后缀匹配, 删除匹配的字串(非贪婪模式)
    ${变量名%%substring} # 贪婪模式

    ${变量名/substring/replace} # 匹配所有, 并替换第一个匹配
    ${变量名//substring/replace} # 匹配所有, 并替换所有

    ${变量名/#substring/replace} # 前缀匹配, 并替换

    ${变量名/%substring/replace} # 后缀匹配, 并替换

    
    > 上述的匹配是 "通配符" 匹配模式
    
    
    
    
    
    更多可参考: https://linuxeye.com/390.html
    
    
    
    ## 数组
    
    **定义数组**
    

    数组名=( 元素1 元素2 元素3)

    注意

    元素之间用空格间隔开, 若元素本身含有空格, 则需要使用引号包含.
    () 内的相邻位置不限制是否有空格
    
    
    
    显示数组
    

    打印第一个元素

    echo $数组名

    打印数组所有元素

    echo ${数组名[@]}

    显示数组元素个数

    echo ${#数组名[@]}

    显示数组第一个元素

    echo $数组名

    显示数组某个元素

    echo ${数组名[n]} # 此处n表示元素下标, 从0开始

    
    
    
    示例
    

    cmdList=(
    A1
    A2
    A3
    )

    for i in "${cmdList[@]}"; do

    if $i; then
        echo "success"
    else
        echo "error with code: $?"
    fi

    done

    
    
    
    
    
    # 特殊符号
    
    ## 其他字符
    
    - `#` 注释符
    
    - `;` 命令分隔符
    
      - `;;` case 语句使用的分隔符
    
      > 在一行中连接多条命令, 每个命令在前一个命令执行完后执行. 
      >
      > 前一个任务的执行结果不会影响后续任务.
    
    - `:` 空指令(什么都不做)
    
      可用于循环中作为一个占位符, `:` 永远返回真(即 0).
    
      > 因为在循环中都没有执行语句是会报错的.
    
    - `,` 分隔目录
    
      > `cp 123{txt,log}` 执行效果 `cp 123.txt 123.log`
    
    - `?` 条件测试
    
    - `$` 取值符号
    
      > ```sh
      > echo $(命令)             # 取运行结果的值
      > 
      > echo ${变量名}            # 取变量的值
      > echo ${#变量名}        # 取变量长度
      > 
      > echo ${变量名[@]}        # 取数组的值
      > echo ${#变量名[@]}        # 取数组的长度
      > 
      > echo ${!变量名}        # 取变量名的值所对应的变量值. 即间接取值.
      >                       # x=y; y=z; echo ${!x}                 # 结果是 z
      > ```
    
    - `|` 管道符
    
    - `&` 后台运行
    
    - shell 下专用
    
      - `.` 等价于 source 命令
    
      - `~` home 目录
    
      - `-` 上一次目录
    
        `cd -`
    
      - `*` 通配符(任意字符)
    
      - `?` 通配符(1个任意字符)
    
      - ` ` 空格
    
    
    
    ## 转义
    
    - 普通字符转义赋予不同功能
    
      `\n`, `\t`, `\r` 单个字母的转义
    
    - 特殊字符转义成普通字符用
    
      `\$`, `\"`, `\'` `\\` 单个非字母的转义(即不转义)
    
    
    
    
    
    ## 引用
    
    - `"` 双引号: **不完全引用**
    
      不完全引用, 会解释双引号其中的变量.
    

    a="$SHELL" # 值为 /bin/bash

    
    - `'` 单引号: **完全引用**
    
    完全引用(RAW), 不解释其中的变量.
    

    a='$SHELL' # 值为 $SHELL

    
    - `` ` 反引号: **执行命令**
    
    等价 `$()`
    

    a=whoami # 值为 当前用户名

    
    
    
    ## 括号
    
    单独使用和非单独使用的意义通常是不一样的.
    
    
    
    - `()`, [`(())`](#双圆括号 - let 命令的简化), `$()` 圆括号
    

    # 单独使用, 会产生一个子shell
    () # eg. (a=123) 执行完这个命令时, 由于是在子进程(子Shell)中执行, 因此不会影响当前shell环境. (👈 想一下管道)

    # 数组初始化
    变量名=(数组元素)

    # 算数运算符
    (( )) # 等价 let 命令的简写, eg. ((i++))

    # 执行命令并将结果赋值给变量
    变量名=$(命令) # 等价于 变量名=命令

    
    - `[]`, `[[]]` 方括号(test 测试)
    

    # 单独使用, 测试(test)
    [ ] # 等价 test 命令, 使用 -gt, -lt 等. 方括号与内容需保持间隔. 可通过 $? 查看test结果

    # 测试表达式, [ ] 的加强, 支持扩展语法: &&, ||, <, > 等
    [[ ]] # 方括号与内容需保持间隔. 可通过 $? 查看test结果

    
    - `<`, `>` 尖括号(重定向))
    
    重定向符号
    
    - `{}` 花括号 (范围, 枚举)
    

    # 输出范围
    echo {0..9} # 0 1 2 3 4 5 6 7 8 9

    echo a{1,3,5} # a1 a3 a5

    # 文件复制的快捷操作等
    cp /etc/passwd{,.bak} # 实际执行的是 cp /etc/passwd /etc/passwd.bak

    # 范围
    for i in {1..9}; do echo $i; done;

    
    
    
    ## 运算符和逻辑符号
    
    - `+`, `-`, `*`, `/`, `%` 算术运算符
    - `>`, `<`, `=` 比较运算符
    - `&&`, `||`, `!` 逻辑运算符
    
    
    
    
    
    ## 算术运算
    
    ### expr 运算
    
    使用 `expr` 运算
    

    expr <运算部分>

    示例

    expr 4 + 5
    a=`expr 4 + 5`        # 将结果赋值给变量
    

    注意

    只支持整数, 不支持浮点数
    数值和运算符之间"必须"有空格分隔
    
    
    
    
    
    ### let 命令
    

    let 变量名=变量值

    示例

    let a=10+20            # a 的值是 30
    

    注意

    变量值可以是数学表达式, 包括: + - ++ -- += -= 等
    变量值不支持浮点数.
    变量值 `0` 开头为八进制.
    变量值 `0x`开头为十六进制. 
    
    > 实际很少用 `let`, 而是使用更简便的 双圆括号.
    >
    > 数值和运算符之间无所谓有没有空格
    
    
    
    ### 双圆括号 - let 命令的简化
    
    [点击查看其他括号](#括号)
    

    双圆括号是 let 命令的简化

    语法

    (())        # 赋值/运算
    $(())        # 引用计算结果
    

    示例

    ((a=10))
    ((a++))
    ((a--))
    ((a+=5))
    ((a=b=c=1))
    echo $((10+20))            # 打印结果
    b=$((1+2+3))            # 引用结果
    
    
    
    # 测试与判断
    
    ## 退出与退出状态
    
    程序
    
    - `exit` 退出程序, 返回状态以 `exit` 的上一条命令执行结果为准
    - `exit <返回值>` 退出程序, 返回状态以此处填写的 返回值(只能是数字)为准.
    
    
    
    
    
    函数
    
    - `return` 返回状态以 `return` 的上一条命令执行结果为准
    - `return <返回值>` 退出程序, 返回状态以此处填写的 返回值(只能是数字)为准
    
    
    
    一般约定, 返回值 `0` 表示正常, 其他都是不正常退出.
    
    
    
    使用 `$?` 可以查看返回值, 用于确定 **当前Shell** 的上一个执行语句(进程, 脚本, 函数)是不是正常退出.
    
    
    
    
    
    ## 测试命令 test
    
    `[ ]` 等价于 test 命令, 更推荐 `[ ]` 写法.
    
    
    
    `[[ ]]` 是 `[ ]` 的扩展写法, 支持 `&&`、`||`、`<`、`>`
    
    > 若要使用 `&&`、`||`、`<`、`>` 这些字符, 则**必须**用 `[[ ]]`
    

    test 命令用于检测文件或比较值

    • 文件测试
    • 数值比较测试
    • 字符串测试

    返回值说明

    真(True) 返回 0
    假(False) 返回 1
    

    语法

    test 表达式
    test
    [ 表达式 ]
    [ ]
    

    表达式

    逻辑表达式
        表达式1 -a 表示2            # 逻辑与
        表达式1 -o 表达式2       # 逻辑或
        ! 表达式                 # 逻辑非
        
    字符串表达式
        -z STR                # zero, 字符串长度为0
        -n STR                # non-zero, 字符串长度不为0
        STR1 = STR2            # 字符串相等测试(大小写敏感)
        STR1 != STR2        # 字符串不相等测试(大小写敏感)
    
    数值表达式
        -eq        # =
        -ge        # >=
        -gt        # >
        -le        # <=
        -lt        # <
        -ne        # !=
    
    文件
        不同文件比较
            FILE1 -ef FILE2        # 相同文件(同个设备, 相同inode)
            FILE1 -nt FILE2        # FILE1 修改时间比 FILE2 更新(也就是最近修改)
            FILE1 -ot FILE2        # 与 -nt 相反
        
        文件
            -e FILE                # 文件存在
            -s FILE                # 长度大于0的文件
        
        类型            
            -f FILE                # 普通文件(非下述几种类型)
            -b FILE                # block 块设备
            -c FILE                # char 字符设备
            -d FILE                # dir 目录
            -h FILE                # 软连接文件, 等同 -L
            -L FILE                # link 软连接文件, 等同 -h
            -p FILE                # 命名管道文件
            -S FILE                # socket 文件
        
        归属
            -O FILE                # 文件有有效的属主            
            -G FILE                # 文件有有效的属组
        
        权限
            -r FILE                # 已设置读权限
            -w FILE                # 已设置写权限
            -x FILE                # 已设置执行权限
            
            -u FILE                # 已设置 SUID(set-user-ID)
            -g FILE                # 已设置 SGID(set-group-ID)
            -k FILE                # 已设置 SBIT(sticky bit set) 
            
        其他            
            -t FD                  # 文件描述符在终端上打开
    
    
    
    ### `[[]]` 扩展用法
    
    此处记录的是与 `[]` 不同的
    

    表达式

    ==        # 通配符匹配, 支持: * ? 
    =~        # 正则匹配    
    &&
    ||
    <
    >
    
    > 不支持 `-a` , `-o`
    
    
    
    
    
    ## 条件 if
    

    完整语法

    if [ 测试条件 ]
    then 执行相应命令 # 如果条件成立 或 返回值为0则进入 then, then 后面无需跟分号
    elif [ 测试条件 ] # 即 else if
    else 执行相应命令
    fi # 结束

    可以写成1行

    if [ 测试条件 ]; then 执行相应命令; else 执行相应命令; fi

    调试时 if true

    if :; then ... fi

    调试时 if false

    if [ ! : ]; then ... fi

    判断执行某个命令后的结果

    if 命令; then

    echo "success"

    else

    echo "fail"

    fi

    
    > `<测试条件>` 如果是个命令或执行的脚本或函数, 那么此时是根据其返回的结果值来判断是否为真.
    >
    > if 语句支持嵌套使用.
    
    
    
    
    
    ## 分支 case
    

    完整语法

    case "$变量" in

    情况1)
        命令
    ;;
    情况2)
        命令
    ;;
    *)                        # 此处用了通配符, 匹配其余情况
        都不匹配时执行的命令
    ;;

    esac

    
    匹配条件支持如下通配符
    
    - `*`
    - `?`
    - `|`
    - `[ ]` 表示范围内的任意一个字符, 范围内可以使用 `-`
    - `[^]` 逆向选择 ?
    - `[!]` 逆向选择 ?
    
    
    
    > 上述的 情况 支持通配符, 比如 `*`, `|`, `?`
    >
    >   
    >
    > 注意以下两种是不同的表示
    >
    > - `"情况)"` 此处是将 `情况` 视为一个字符串, 其中的通配符之类的都不会生效
    > - `情况)` 此处的 `情况` 中的通配符之类的生效
    >
    > 因此以下几种匹配是不一样的
    >
    > ```sh
    > start|stop)            # 匹配 start 或 stop
    > 
    > "start"|"stop")        # 同上, 匹配 start 或 stop
    > 
    > "start|stop")        # 完整匹配 "start|stop" 整个字符串
    > 
    > "cmd*")                # 完整匹配 "cmd*" 整个字符串
    > 
    > "cmd?")                # 完整匹配 "cmd?" 整个字符串
    > 
    > cmd*)                # 通配符匹配 cmd*
    > 
    > cmd?)                # 通配符匹配 cmd?
    > ```
    
    
    
    ## 循环
    
    ### for 遍历
    

    for 参数名 in 列表
    do

    :

    done

    
    ​    
    
    **列表来源**
    
    1. 列表中包含多个变量(空格分隔)
    

    for i in 1 2 3

    for i in {1..3} # 使用花括号产生列表

    
    2. 使用  ` `` ` 或 `$()` 方式执行命令, 默认逐行处理(若出现空格会被视为多行)
    
     for file in `ls`
    
    3. 枚举路径(可使用通配符)
    

    # 枚举出的是完整路径, 可配合使用 basename 命令获取简短的文件名
    for file in /etc/profile*
    do

       echo $file

    done

    # 输出如下
    /etc/profile
    /etc/profile.d

    
    
    
    
    ### for 循环
    
    C语言风格的for命令
    

    语法

    for ((变量初始化;循环判断条件;变量变化))  
    do
        命令
    done
    

    示例

    for ((i=0;i<=10;i++)); do echo $i; done
    
    > 不太常用
    
     
    
    ### while 循环
    

    语法

    # 满足条件就执行
    while test测试条件为真
    do
        命令
    done
    
    

    示例

    i=0
    while [[ i<=10 ]]
    do
        ((i+=3))
    done
    
    > 常用于构建交互式菜单
    
     
    
    可配合 `shift` 命令偏移参数处理
    

    位置参数位移, 如果偏移数为n, 则新的 $1 值为原来的 ${n+1}, 以此类推

    shift <偏移数=1>

    示例

    while [ $# -gt 0 ] do
        shift 1
    done
    
    
    
    
    
    
    
    ### until 循环
    

    语法

    # 不满足条件就执行
    until test测试条件为假
    do
        命令
    done
    
    
    
    ### break 和 continue
    

    while :
    do

    break;
    continue;

    done

    
    
    
     
    
    # 函数
    
    ## 自定义函数
    

    定义

    函数名称 () {
        local 函数局部变量名;        # 变量作用域仅在函数内部
        echo $1;                   # 
    }
    
    

    注意

    语法要求: 第一个花括号后面需要有空(空格, 换行等)
    

    使用

    函数名
    
    可将自定义的函数统一放在一个脚本文件中, 赋予执行权限后通过 `source` 或 `.` 执行以在当前Shell环境调用.
    
     
    
    示例
    

    hello() { echo hello $USER; }

    hello # 输出 hello root

    
    
    
    顺序执行多个函数的示例
    

    A1 () { : }
    A2 () { : }
    A3 () { : }

    cmdList=(
    A1
    A2
    A3
    )

    for i in "${cmdList[@]}"; do

    if $i; then
        echo "success"
    else
        echo "error with code: $?"
    fi

    done

    
    
    
    ## 获取函数名
    
    在函数内部有一些预定义变量
    

    $FUNCNAME 数组, 包含调用栈名

    
    
    
    示例
    

    !/bin/bash

    abc() {

    echo "in abc: ${FUNCNAME[@]}"
    ef

    }

    ef() {

    echo "in ef: ${FUNCNAME[@]}"

    }

    abc

    输出内容

    in abc: abc main
    in ef: ef abc main

    
    
    
    
    
    ## 删除函数
    

    unset -f 函数名

    
    
    
    
    
    
    
    ## 系统脚本
    
    系统自建的函数库: `/etc/init.d/functions`
    
    
    
    
    
    # 脚本控制
    
    ## 脚本优先级控制
    
    可以使用 nice 和 renice 调整脚本优先级
    
    
    
    CPU 的计算和创建子进程都会造成系统 开销.
    
    
    
    创建死循环大量消耗cpu导致死机的情况
    
    1. while 和 for 创建死循环会导致cpu占用过高,   
    
    2. fork 炸弹: 程序大量创建子进程,  CPU不响应任何信息.
    

    # 示例 - 定义一个 func 的函数
    func () { func | func& }

    # 调用之后系统会疯狂创建子进程, 此时 ctrl+c 已经不生效了
    func

    # 另一个常见的fork炸弹
    .(){ .|.&}; .

    
    
    
     
    
     
    
    `ulimit -a` 的输出
    

    core file size (blocks, -c) 0
    data seg size (kbytes, -d) unlimited
    scheduling priority (-e) 0
    file size (blocks, -f) unlimited
    pending signals (-i) 5642
    max locked memory (kbytes, -l) 64
    max memory size (kbytes, -m) unlimited
    open files (-n) 1024
    pipe size (512 bytes, -p) 8
    POSIX message queues (bytes, -q) 819200
    real-time priority (-r) 0
    stack size (kbytes, -s) 8192
    cpu time (seconds, -t) unlimited
    max user processes (-u) 5642 # 同时可创建的子进程数量, root无效
    virtual memory (kbytes, -v) unlimited
    file locks (-x) unlimited

    
    > 大部分限制对 root 是不生效的.
    
    
    
    ## 捕获信号
    
    `kill` 默认发送 15号(SIGTERM)信号给应用程序
    
    `ctrl+c` 发送2号(SIGINT)信号给应用程序
    
    9号(SIGKILL)信号不可捕获, 不可阻塞, 强行杀进程.
    
    
    
    设置捕获信号
    

    trap [参数] 信号

    <参数> 表示捕获到信号后的处理, 分为以下几种情况

    未设置 <参数>      按默认处理情况处理
    值为 -            按默认处理情况处理, 同上
    值为 空           捕获信号, 直接忽略
    值为其他          捕获信号, 并执行指定命令
    
    

    示例

    # 捕获 SIGINT(2) 信号, 并打印 "got signal 2"
    trap "echo got signal 2" 2
    
    > 可在重要不可中断的脚本中设置捕获信号以免被无意中结束掉, 比如备份脚本.
    
    
    
    
    
    
    
    control group? 控制内存?
    
    
    
    # 计划任务
    
    ##  at 命令
    
    
    

    设置一次性计划任务

    输入要执行的任务后需要追加 ctrl+d 以表示输入完毕.
    

    at [选项] 时间

    时间格式

    HH:MM            # 时:分            指定下一个该时间点执行(今天或隔天)
    HH:MM today        # 今天指定时间运行
    HH:MM tomorrow    # 明天指定时间运行
    HH:MM MMDDYY    # 时:分 月日天, 年可以是2位的缩写, 或4位的全写
    HH:MM MM/DD/YY
    HH:MM MM.DD.YY
    HH:MM YYYY-MM-DD
    
    now + 计数      # 当前时间点的偏移, 计数单位: minutes, hours, days, weeks. Eg. 4pm + 3 days
    
    

    选项

    -c <序号>        # 查看具体的任务执行内容
    -f <file>     # 从指定文件读取命令, 而不是从标准输入
    
    

    注意

    1. 非内部命令应使用完整路径, 如果是shell脚本应使用 source 来引入环境变量
    2. 计划任务的执行是没有终端的, 因此是没有标准输出的, 需自行重定向输出
    

    Tip
    鉴于 at 在设置命令时的不方便, 可以考虑配合 cat 一同设置, eg.

    cat <<'EOF' | at 时间
    具体要执行的命令
    EOF
    
       
    
    注意
    
    - `at` 一次性任务是依赖  atd 服务来执行的
    - `at` 目录会以设置任务时所在的目录作为工作目录, 因此若该目录无效(被删除, 权限限制) 则会导致任务执行失败.
    - root用户可以在任何情况下使用at命令,而其他用户使用at命令的权限定义在/etc/at.allow(被允许使用计划任务的用户)和/etc/at.deny(被拒绝使用计划任务的用户)文件中,默认没有文件需要自己创建允许用户和拒绝用户文件;
      - 如果/etc/at.allow文件存在,只有在该文件中的用户名对应的用户才能使用at;
      - 如果/etc/at.allow文件不存在,/etc/at.deny存在,所有不在/etc/at.deny文件中的用户可以使用at;
      - at.allow比at.deny优先级高,执行用户是否可以执行at命令,先看at.allow文件中有没有才看at.deny文件;
      - 如果/etc/at.allow和/etc/at.deny文件都不存在,则只有root用户能使用at;
    
    
    
    **atq 命令**
    

    查询等待执行的一次性计划任务队列

    最左边的数字即计划任务的任务id
    

    atq

    
    
    
    **atrm 命令**
    

    移除未执行的计划任务

    atrm 任务序号

    
    
    
    
    
    ## 周期性计划任务 cron
    

    crontab [选项]

    选项

    -e        # 编辑, 进入vim编辑器 
    -l        # 查看已配置项
    
    

    配置格式

    分 时 日 月 周 命令
    *            # 任意
    1,2,3        # 逗号分隔
    1-3            # 等价表示 1,2,3    
    

    注意

    命令应使用完整路径 
    
    
    
    配置文件(每个用户有各自的一份配置)
    
    `/var/spool/cron/用户名`
    
    
    
    crond 相关日志(不包括任务的输出)
    
    `/var/log/cron`
    
    
    
    ## 计划任务和锁
    
    ### anacron 周期命令调度程序
    
    anacron 是一个用于周期性执行命令的工具(适用于非24小时开机, 同时需要确保 每日/每周/每月 运行指定任务).
    
    它的时间粒度是"天", 比如配置了 logrotate 每天运行一次, 那么它默认会在 3~22 点之间每小时尝试运行该任务(因此主机不一定需要保持24小时开机)
    
    
    
    **适合**: 需要定期执行至少1次的脚本(对具体执行时机没有严格要求的)可以使用 anacron 来配置定时任务.
    
    > 比如要求每3天至少执行1次, 但对于在哪一天的哪一个小时执行没有严格要求.
    
    
    
    **Crond 服务调用 Anacron 的过程**
    
    1. crond 服务每分钟会执行 `/etc/cron.d/`  目录下配置的定时任务(crontab 格式):
    
       - `/etc/cron.d/0hourly` 配置每小时的第1分钟执行 `/etc/cron.hourly/` 目录下的所有**<u>脚本</u>**
       - `/etc/cron.d/sysstat` 配置每10分钟系统性能统计收集, 每天接近凌晨时生成一份每日报告
    
    2. `/etc/cron.hourly/0anacron` 脚本
    
       > 脚本的主要逻辑: 若今日未执行过 anacrontab, 则会执行 `/usr/sbin/anacron -s` 命令
       >
       > 意思是顺序(非并行)执行延时计划任务.
    
    
    
    
    
    **Anacron 的执行过程**
    
    1. 读取配置文件 `/etc/anacrontab` 
    

    # the maximal random delay added to the base delay of the jobs
    RANDOM_DELAY=45
    # the jobs will be started during the following hours only
    START_HOURS_RANGE=3-22

    #period in days delay in minutes job-identifier command
    1 5 cron.daily nice run-parts /etc/cron.daily
    7 25 cron.weekly nice run-parts /etc/cron.weekly
    @monthly 45 cron.monthly nice run-parts /etc/cron.monthly

    
    2. 依次执行上面配置的任务
    
    Anacron 会在每天的 3点~22点时间范围内, 每次随机延迟 0~45 分钟开始执行上面配置的 每日/每周/每月 任务.
    
    - `/etc/cron.daily/logrotate` 日志轮转工具调用
    - ...
    
    
    
    
    
    ### flock 锁文件
    

    flock [选项]

    示例

    flocak -xn "/tmp/f.lock"  -c "需执行的命令或脚本"
    

    选项

    锁类型
    -x         # 排他锁
    
    
    -n, --nb, --nonblock        # 加锁失败时直接退出 (默认是等待)
    
    -c, --command <command>        # 需执行的命令
    

    注意

    锁文件被删除后会失效.
    
    
    
    
    
    
    
    # 其他待整理
    
    ##
    
    # run-parts 命令
    

    运行指定目录下的所有可执行文件(具有执行权限)

    - 可通过在目录下配置 jobs.deny 和 jobs.allow 来配置黑/白名单
    - 常用于crond定时任务执行某个目录下的所有脚本
    

    run-parts <目录>

    
    
    
    
    
    ## mktemp 命令
    

    创建一个临时文件或目录

    mktemp [选项] [模板]

    模板

    需包含至少连续3个 "X", 若未指定, 则会在 /tmp 目录下创建 tmp.XXXXXXXXXX
    

    选项

    -d, --directory        # 创建一个目录(而不是默认的文件)
    -t                    # 在 /tmp 目录下创建临时文件
    

    示例

    mktemp                    # /tmp/tmp.CaE8KvS8HI
    mktemp log.XXXXXXX        # log.KyvESFk            在当前目录下
    mktemp -t log.XXXX        # /tmp/log.RNwC
    
    
    
    ## yes 命令
    

    在命令行中输出指定的字符串,直到yes进程被杀死

    yes [选项] <string=y>

    示例

    常用语需要简单交互: 输入 y/yes 以继续操作的情况
    yes|yum install ...        # 这里只是示例, 实际 yum 一般是配合 -y
    查看原文

    赞 0 收藏 0 评论 0

    嘉兴ing 发布了文章 · 1月18日

    ⭐《Linux实战技能100讲》个人笔记 - 3. 系统管理篇

    [TOC]

    网络管理

    关于网络状态常用工具包

    • net-tools 工具包

      • ifconfig 命令
      • route 命令
      • netstat 命令
    • iproute2 工具包

      • ip 命令
      • ss 命令
    • iputils-ping

      • ping 命令

        很多精简的容器中没有安装该工具

    centos 7之前常用 net-tools 工具包, 在centos 7及之后则主推 iproute2 工具包.

    网络接口

    命名规则

    概念上, 网络接口名和网卡名在我看来指的是同一个东西.

    网络接口(网卡)可能的命名

    • eno1(板载网卡)
    • ens33(PCI-E网卡)
    • enp0s3(无法获取物理信息的PCI-E网卡)
    • eth0

      CentOS 7 使用了一致性网络设备命名, 在以上都不匹配时使用 eth0

    eth0 网卡

    eth0 通常指第一张网卡, 但有时候配置原因, 并不叫 eth0, 因此为了管理方便, 通常会将网卡名转换成一致的 eth0.

    操作步骤如下:

    1. 编辑 /etc/default/grub 文件

      ......
      
      # 默认的配置项
      GRUB_CMDLINE_LINUX="crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rghb quiet"
      # ↓
      # 在末尾追加配置, 修改后的值如下
      # ↓
      GRUB_CMDLINE_LINUX="crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rghb quiet biosdevname=0 net.ifnames=0"
      
      ......
    2. 更新 grub 实际的配置

      grub2-mkconfig -o /boot/grub2/grub.cfg
      
      选项
          -o, --output        指定生成的文件的位置
    3. 重启

      reboot
    4. 此时网卡名就变成了 eth0

    网卡命名规则受 biosdevnamenet.ifnames 两个参数影响

    biosdevnamenet.ifnames网卡名
    默认01ens33
    组合110em1
    组合200eth0

    如果一开始的网卡名并非 eth0 , 但通过修改 /etc/default/grub 等相关配置来修改网卡名为 eth0 后, 对应的 ifcfg-* 文件名需要手动修改成 ifcfg-eth0, 包括里面的配置项.

    网络配置

    mii-tool 命令

    查看网卡物理连接状态
    
    mii-tool [<网卡名>]
    
    注意
        CentOS 6 无需写网卡名(接口), 直接执行即可
        CentOS 7 需要指定网卡名(接口)才能查看状态
        虚拟机上可能无法执行(云主机也是), 会报错.

    ifup 命令

    启用网卡
    
    ifup <接口>

    ifdown 命令

    禁用网卡
    
    ifdown <接口>

    ifconfig 命令 ⭐

    属于 net-tools 工具包.

    注意: 使用 ifconfig 修改配置由于没有写入配置文件, 因此仅限档次有效, 重启后失效.

    配置网络接口
    
    
    ifconfig                                      # 显示当前有效网络接口状态
    ifconfig <接口>                                # 显示指定网络接口状态
    ifconfig <接口> <IP地址> [netmask <子网掩码>]        # 设置指定网络接口
                                                     # 子网掩码省略时会默认设置为 255.0.0.0
                                                     # !!! 注意, 操作云主机时会导致掉线且无法重连. 需要到网页控制台打开ssh连接改回来
    ifconfig <接口> up                            # 启用该网卡, 等价于 ifup <接口>
    ifconfig <接口> down                            # 禁用该网卡. 等价于 ifdown <接口>
    
    
    网卡
        eth0    # 第一块网卡(网络接口)
        lo        # 本地环回
    
    显示字段解释
        ether XX:XX:XX:XX:XX:XX        # 网卡的MAC地址

    route 命令

    属于 net-tools 工具包.

    网关配置命令
    
    route [选项]
    
    常用
        route -n                                               # 查看当前路由规则
        route add default gw <网关ip>                             # 添加默认网关
        route del default gw <网关ip>                             # 删除默认网关
        
        route add -host <指定ip> gw <网关ip>                    # 添加一条路由规则
        route add -net <指定网段> netmask <子网掩码> gw <网关ip>    # 添加一条路由规则
        
    
    选项
        -n    不解析主机名(默认解析主机名会比较慢)

    ip 命令

    属于 iproute2 工具包

    ip命令是 ifconfig, route, ifup, ifdown 的命令集合体

    注意: 使用 ip修改配置由于没有写入配置文件, 因此仅限档次有效, 重启后失效.

    网络配置命令
    
    ip addr [ls]                        # 等价 ifconfig
    ip addr add 10.0.0.1/24 dev eth1    # 等价 ifconfig eth1 10.0.0.1 netmask 255.255.255.0
    
    ip link set dev eth0 up                # 等价 ifup eth0 以及 ifconfig eth0 up
    
    ip route add 10.0.0.0/24 via 192.168.0.1    # 等价于 route add -net 10.0.0.0 netmask 255.255.255.0 gw 192.168.0.1

    网络故障排除

    首先检测与目标主机网络是否畅通

    1. 首先使用 ping 确认与目标主机连接
    2. 如果 ping 不同, 则可以用 traceroutemtr 检查中间的路由情况
    3. 如果 ping 不同, 可以用 nslookup 确认域名是否解析正常

    确认端口状态

    1. 使用 telnet 检查端口连接状态
    2. 使用 tcpdump 详细分析数据包

    确认服务状态

    1. 使用 netstatss

    ping 命令 ⭐

    通过发送ICMP报文, 检测与目标主机是否畅通
    
    ping <地址>
    所属包: inetutils-ping

    traceroute 命令

    用于辅助 ping 命令, ping不通的时候追踪每一跳路由.

    若是中间部分节点不支持 traceroute 的, 则会显示为 *

    检测当前主机到目标主机的路由状况
    
    traceroute [选项] <目标主机>
    
    示例
        traceroute -w 1 <目标主机>
    
    选项
        -w <waittime>, --wait=<waittime>    超时等待时间, 默认5秒

    mtr 命令 ⭐

    用于辅助 ping 命令, ping不通的时候检测到目标主机的数据包是否有丢失.

    相较 traceroute 显示的内容更丰富, 建议使用 mtr 来确定是否丢包, 它是 ping, nslookup, traceroute 的结合体

    网络诊断工具 My traceroute
    
    mtr [选项] <目标主机>
    
    选项
        -n, --no-dns                            不解析主机名, 强制显示为ip
        -c <count>, --report-cycles <COUNT>        设置发送ping包的数量
        -r, --report                            通常配合 -c (若未指定则默认10次), 在发送完指定包后以报告形式展示结果.
    
    
    显示结果说明
                    Packets                                Pings(延迟 ms)
        Host    Loss%    Snt                Last        Avg        Best    Wrst    StDev
        ip地址   丢包率    ?                最后一次      平均     最佳     最差    标准偏差(衡量数值偏离平均值的程度, 越小则偏差越小)
    部分显示丢失可能是由于icmp保护机制造成的, 并不代表真的丢包.

    同时只有最后的目标丢包才算真正的丢包???

    nslookup 命令

    查询域名对应的ip
    
    nslookup <域名>

    dig 命令

    发送域名查询信息包到域名服务器
    
    dig <域名>

    telnet 命令 ⭐

    检查端口连接状态
    
    telnet <主机> <端口>
    
    
    注意:
        如果是在非交互式调用 telnet 时, 需要预留时间用于连接和数据传输, 示例:
        (usleep 100000; echo "info"; usleep 100000;) | telnet 127.0.0.1 6379

    tcpdump 命令 ⭐

    网络抓包工具
    
    tcpdump [选项] <表达式>
    
    选项
        -i <interface>    监听网络接口(网卡), 不指定时默认eth0.
                        "any" 表示监听所有
        -n                不进行域名解析, 而是显示ip
        -w <文件名>      将抓包结果保存到指定文件, 可以用 wireshark 图形化工具来查看该文件内容.
        -s <字节>          从每个截取指定长度字节的数据, 而不是默认的68字节.
        
    表达式
        port <端口>        指定监听端口
        host <主机>        指定监听主机
        
        逻辑表达式
        and                 逻辑与, 比如 port 80 and host 10.0.0.1
        
    示例
        telnet -i any port 23 -s 1500 -w /tmp/a.dump        # 抓取 telnet 的包
        
        
    Flags 说明
        S        SYN
        F        FIN
        R        RST
        P        PUSH
        U        URG
        W        ECN CWR
        E        ECN-Echo
        .        ACK
        none    未设置Flags
    # 安装图形化工具的 wireshark
    yum install wireshark-gnome

    netstat 命令 ⭐

    属于 net-tools 工具包.

    显示网络连接, 接口状态等信息
    
    netstat [选项]
    
    选项
        显示格式
        -n, --numeric        显示ip, 而不是对应的域名    
        -p, --program        显示对应的进程
        
        筛选
        -t, --tcp            筛选TCP
        -u, --udp            筛选UDP
        -l, --listening        只显示处于监听状态的

    ss 命令

    属于 iproute2 工具包

    vs netstat

    一个socket查看工具
    
    ss [选项]
    
    选项
        显示格式
        -n, --numeric        显示ip, 而不是对应的域名    
        -p, --program        显示对应的进程
        
        筛选
        -a, --all            显示处于listening和non-listening的socket(对于TCP则包含ESTABLISHE连接)
        -t, --tcp            显示TCP socket
        -u, --udp            显示UDP socket
        -l, --listening        筛选查看处于监听状态的

    网络服务管理

    网络服务管理程序分为两种:

    • SysV (也叫 Sys 5)

      CentOS 6 使用
    • systemd

      CentOS 7 使用

    network服务和NetworkManager服务:

    • network服务是centos6的网络默认管理工具, network只能支持 service 来管理.
    • centos7重写了一遍就是NetworkManager服务,centos7默认的服务管理工具换成了systemctl.

      若是为了向下兼容, 可以禁用NetworkManager.

      systemctl disable NetworkManager.service

    网络配置文件

    ifcfg-*

    /etc/sysconfig/network-scripts/ifcfg-* 网络配置文件(随真实网卡名称变化, 比如 ifcfg-eth0)

    注意:

    1. 使用 ipipconfig 命令修改网络配置由于并未写入配置文件, 因此仅限当此有效, 重启后还是以配置文件的为准.
    2. 如果一开始的网卡名并非 eth0 , 但通过修改 /etc/default/grub 等相关配置来修改网卡名为 eth0 后, 对应的 ifcfg-* 文件名需要手动修改.

    有可能出现一个网络配置文件里面配置多个网卡的情况

    ifcfg-eth0 动态(DHCP)配置文件示例

    TYPE=Ethernet
    PROXY_METHOD=none
    BROWSER_ONLY=no
    
    # dhcp: 动态分配
    # none 或 static: 静态分配
    BOOTPROTO=dhcp
    DEFROUTE=yes
    IPV4_FAILURE_FATAL=no
    IPV6INT=yes
    IPV6_AUTOCONF=YES
    IPV6_DEFROUTE=YES
    IPV6_FAILURE_FATAL=NO
    IPV6_ADDR_GEN_MODE=stable-privacy
    NAME=eth0
    UUID=xxxxxxxxxxxxxxxxx
    DEVICE=eth0
    ONBOOT=YES

    ifcfg-eth0 的静态配置示例(需重点掌握)

    TYPE=Ethernet
    NAME=eth0
    DEVICE=eth0
    UUID=xxxxxxxxxxxxxxxxxxxxxx
    ONBOOT=YES
    
    # dhcp:动态分配
    # none:静态分配
    BOOTPROTO=none
    IPADDR=10.211.55.3
    NETMASK=255.255.255.0
    GATEWAY=10.211.5.1
    DNS1=114.114.114.114
    DNS2=8.8.8.8
    DNS3=8.8.4.4
    UUID 可以通过 nmcli con 查看

    hostname 命令

    主机名的构成: <主机名称>.<域名>

    显示或设置当前主机名
    
    hostname [选项]          # 查看主机名
    hostname [<主机名>]      # 设置主机名, 立即生效
    
    选项
        -s, --short            显示短格式主机名, 即 . 之前的部分
        -i, --ip-address    显示主机的IP地址

    hostnamectl

    控制系统主机名(写入配置, 永久生效)
    
    hostnamectl set-hostname <主机名>

    /etc/hosts

    主机名相关配置, hosts文件包含了IP地址和主机名之间的映射,还包括主机名的别名。

    在没有域名服务器的情况下,系统上的所有网络程序都通过查询该文件来解析对应于某个主机名的IP地址,否则就需要使用DNS服务程序来解决。通常可以将常用的域名和IP地址映射加入到hosts文件中,实现快速方便的访问。

    在修改主机名后, 由于有一些程序是依赖主机名工作的, 因此需要在 hosts 文件里添加 新主机名对应ip, 否则部分依赖主机名的服务会在解析主机名时超时.

    eg. 主机名修改为 yjx-pc 后, 在 /etc/hosts 文件添加一条

    127.0.0.1  yjx-pc

    这样

    SysV 管理程序

    CentOS 6 使用

    service 命令

    
    
    service network (start|stop|restart|status)

    chkconfig 命令

    
    chkconfig --list            # 列出所有 SysV 服务
    chkconfig --list network    # 列出 network 服务
    
    chkconfig [--level <数字级别>...] network (off|on)    # 一般 network 会保持开机启动

    systemd 管理程序

    CentOS 6 无法使用, 仅能在 CentOS 7 使用.

    systemctl 命令

    在 CentOS 7 下使用的 service 命令实际上是调用 systemctl 命令.

    重要

    在CentOS 7下有两套网络管理工具, 一个是 network , 另一个是 NetworkManager, 在实际工作中不要同时用两套工具管理.

    CentOS 6 只有 network

    建议按照个人使用习惯禁用掉其中任意一个, 在个人电脑上可能会更建议使用 Network Manager, 在接入新网络时会自动配置较为方便, 但这个功能在服务器上比较鸡肋, 因此服务器上一般是沿用 network, 因此推荐禁用掉 NetworkManager

    控制 systemd 系统和服务管理器
    
    systemctl <命令>
    
    命令
        管理器生命周期命令
        daemon-reload                # 重新加载 systemd 管理配置
    
        单位(unit)命令
        start <service>
        stop <service>
        restart <service>
        reload <service>
        status <service>
        
        单位(unit)文件命令
        list-unit-files    [<unit>]        # 列出所有 systemd 服务, 或指定服务
        enable <service>
        disable <service>
    Systemd 入门教程:命令篇

    软件包

    • 软件包管理器
    • rpm 包 和 rpm 命令
    • yum 仓库
    • 源代码编译安装
    • 内核升级
    • grub配置文件

    概念 - 包管理器

    包管理器是方便软件安装、卸载、解决软件依赖关系的工具.

    • RedHat(RHEL) 系列(RedHat, Fedora, CentOS) 使用 yum 包管理器, 软件安装包格式为 rpm
    • Debian、Ubuntu 使用 apt 包管理器, 软件安装包格式为 deb.

    在 RedHat 中的包管理器是 rpm 包, 它使用的是 rpm 命令.

    集中存放包的地方叫做软件仓库, RedHat 中用的是 yum 仓库.

    rpm 包

    rpm 包格式

    格式
        软件名称-软件版本.系统版本.平台.rpm
    
    示例
        vim-common-7.4.10-5.el7.x86_64.rpm    
        
        软件名称: vim-common
        软件版本: 7.4.10-5
        系统版本: el7                # 指 Enterprise Linux 7
        平台: x86_64

    rpm 包的获得方式:

    • 系统光盘 Packages/*.rpm
    • 网上下载

    rpm 命令

    rpm 包的问题

    • 需要自己解决依赖关系
    • 软件包来源不可靠
    rpm - RedHat Package Manager
    
    rpm [选项] <完整软件包名>
    
    <完整软件包名> 参数不支持通配符
    
    示例
        rpm -qa                            # 查询已安装的所有软件包
    
    选项
        -a, --all                         # 查询/验证所有软件包
        -q, --query    [<完整软件包名>]         # 查询软件包, 若使用 -qa 则表示查询所有已安装的, 否则仅查询指定软件包. 若要模糊查询则要 -qa | grep 配合
        -l, --list                         # 列出该包的文件
        -i, --install <rpm文件>            # 安装软件包, 此时若提示依赖关系问题, 则需要先安装好被依赖的安装包. <rpm文件> 也可以指定具体的 url
        -e, --erase    <完整软件包名>...         # 卸载软件包
        -U, --upgrade <完整软件包名>         # 升级软件包, 若不存在则安装, 若已存在则会替换旧版本.
        
        -v, --verbose
        -h, --hash                         # 显示安装进度, 经常配置 -v 来显示准确的进度.
        
    示例
        rpm -ql <软件报名>                  # 查看该软件包的文件列表, 比如用于查看有哪些配置文件.⭐

    yum 包管理器

    /etc/yum.repos.d/*

    yum 命令

    yum 包管理器
    
    yum <命令>
    
    <软件包> 参数支持通配符
    
    示例
        yum list all|installed|available|updates            # 查看列表(默认是 all)
        yum list vim*                # 查看已安装的 vim* 安装包, 建议用这种方式来速览, 因为同时可提供当前已安装和可安装的列表.
                                    # ?? 有些通过 rpm 命令直接安装的好像在这无法通过模糊查询找到
    
    命令    
        makecache                             # 生成元数据缓存
        repolist [all|enabled|disabled]      # 查看当前已生效的源, 默认是 enabled.
        
        install <软件包>                # 安装软件包, <软件包> 支持指定本地的 rpm 文件
        remove <软件包>                # 卸载软件包
        list [<软件包>]                # 查看(所有)软件包(已安装和可安装的)    
        list <软件包> --showduplicates    #  查看软件包的各个版本, 默认只展示最新的版本
        update [<软件包>]                # 升级软件包, 若不指定"软件包"参数, 则会升级当前已安装的所有软件包.
        
        grouplist                       # 查看系列软件包列表(包含了一系列的软件)
        groupinfo <软件组>                # 查看某个具体系列软件包包含的软件列表.
        groupinstall <软件组>            # 安装系列软件包
        

    常用的软件组

    Development Tools            # 基本开发环境

    yum-config-manager 命令

    管理 yum 配置选项和仓库
    
    yum-config-manager [选项]
    
    选项
        --enable <repo>...        # 启用指定仓库(同时写入配置文件), 支持通配符?
        --disable <repo>...        # 禁用指定仓库(同时写入配置文件), 支持通配符?
        --add-repo <仓库地址>     # 添加并启用指定仓库, 地址可以是 url. 也可以通过 `yum install <仓库地址>`
        
    示例
        yum-config-manager --add-repo https://xxxx/xx.repo        # 添加某个yum仓库
    可通过 yum repolist all 确认当前有哪些仓库.

    yum-config-manager 命令是由 yum-utils 包提供的

    yum install -y yum-utils

    yum 基础源

    CentOS yum 源: http://mirror.centos.org/cent...

    国内镜像: https://developer.aliyun.com/...

    建议更换为国内yum镜像源

    # 1. 备份旧配置
    mv /etc/yum.repos.d/CentOS-Base.repo{,.backup}
    
    # 2. 下载新的yum镜像源配置文件(选择任意一个即可)
    wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo            # 阿里云镜像
    wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.163.com/.help/CentOS7-Base-163.repo    # 163镜像
    
    # 3. 重新生成缓存
    yum makecache
    注意, yum源配置文件中有的会比较激进, 因此默认通常是禁用的, 其配置文件中 enabled=0

    EPEL 源

    EPEL 源(Extra Packages for Enterprise Linux) 比较流行,它相当于官方 CentOS/RHEL 源仓库的一个补充.

    通俗的说,EPEL 可以认为是 Linux(RHEL)及其衍生发行版的一个事实上的官方仓库。

    比如 redis 在epel源才有
    # 1. 安装
    yum install epel-release
    
    # 2. 生成缓存
    yum makecache

    或者直接用阿里的源(上面的命令不必执行)

    wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo
    yum makecache
    若出错, 则来个清缓存套餐 yum clean all

    webtatic 源

    提供最新的稳定版本的Web开发/托管软件,这些版本未在CentOS / RHEL分发版本中提供. php 也在这里面.

    依赖 epel-release

    比如 默认源只提供 php54 版本, 但webtatic提供了 php55, php71, php72 等高版本
    # 1. 安装
    rpm -Uvh https://mirror.webtatic.com/yum/el7/webtatic-release.rpm
    
    # 2. 生成缓存
    yum makecache

    city-fan 源

    city-fan源 源提供的包版本很新, 但需注意兼容性, 因此该仓库安装后默认是非启用的.(enabled=0)

    比如curl版本 7.64.1, 其他源的版本没这么高
    # 1. 安装
    rpm -Uvh https://mirror.city-fan.org/ftp/contrib/yum-repo/city-fan.org-release-2-1.rhel7.noarch.rpm
    
    # 2. 生成缓存
    yum makecache

    源代码编译安装

    编译安装的一般步骤

    以 openresty 为例
    # 1. 下载源码包
    wget https://openresty.org/download/openresty-1.15.8.1.tar.gz
    
    # 2. 解压
    tar -zxvf openresty-<VERSION>
    
    # 3. 进入源代码目录
    cd openresty-<VERSION>/
    
    # 4. 自动配置安装所需的相关配置, 比如匹配系统当前系统, 内核版本, 编译器等等, 还可以指定安装目录等.
    ## --prefix=<指定安装目录>        默认是安装到 /usr/, 会分散开导致卸载不方便, 因此通常指定各自的目录.
    ./configure --prefix=/usr/local/openresty
    
    # 5. 编译: 将源码编译为可执行的二进制
    ## -j<num>        指定用<num>个逻辑CPU来加速编译
    ## 
    ## 如果在上一阶段配置结束后提示可以用 gmake 和 gmake install(跨平台编译), 那么此处就可以用这两个命令
    ## 编译好的内容会先存放在 ./build 目录
    make -j2
    
    # 6. 将 ./build 目录中的文件全部安装到指定目录中
    make install

    编译经常需要依赖各种安装环境, 以下是部分常用的

    yum install gcc gcc-c++ pcre-devel openssl-devel

    *-devel 一般是指开发包

    升级内核

    TODO

    为什么要升级内核?

    uname 命令

    查看系统信息
    
    uname [选项]
    
    默认是 -s
    
    选项
        -a, --all            # 展示所有信息
        -r, --release        # 查看操作系统(内核)的发行版本
        -m, --machine        # 显示机器(硬件)类型, 比如32位, 64位等

    rpm 方式升级内核

    # 单独升级内核
    yum install kernel-<内核版本>
    
    # 升级所有已安装包(包括内核)
    yum update

    源代码编译安装内核

    https://www.kernel.org

    根分区至少要保留10G的空间.

    以下以更新到内核 5.5.5 为例.

    # 1. 安装依赖包
    yum install gcc gcc-c++ make ncurses-devel openssl-devel elfutils-libelf-devel flex bison 
    
    # 2. 下载并解压缩内核
    wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.5.5.tar.xz
    tar xvf linux-5.5.5.tar.xz -C /usr/src/kernels/
    
    # 3. 配置内核编译参数
    ## 也可以通过复制当前内核配置来直接使用或是在当前系统内核配置基础上进行修改
    cp /boot/config-<内核版本>.<平台> ./.config
    make (menuconfig|allyesconfig|allnoconfig)
    
    # 4. 编译
    ## -j 可使用多个CPU来加速编译
    make -j<cpu数> all
    
    # 5. 安装内核模块 及 内核
    make modules_install
    make install
    
    # 6. 重启, 启动时的引导界面(grub界面)选择新内核版本
    reboot

    grub

    系统启动时的引导软件叫做 grub

    CentOS 6 使用的是 grub 版本, 需要自己记住设置项, 手动修改配置文件.

    CentOS 7 使用的是 grub2 版本, 提供了工具来方便修改, 一般不手动修改配置文件以免出错影响工具使用.

    grub 相关命令

    grub2-mk-config 命令

    生成 grub 配置文件
    
    grub2-mk-config [选项]
    
    选项
        -o, --output <file>        # 指定生成的文件位置, grub默认使用的是在 /boot/grub2/grub.cfg

    grub2-editenv 命令

    
    示例
        grub2-editenv list        # 查看当前默认引导项

    grub2-set-default 命令

    
    示例
        grub2-set-default <引导序号, 从 0 开始>        # 设置默认引导项

    查看所有可选择的引导项

    # 按照在文件中的顺序从上到下的序号依次是 0~N
    grep "^menu" /boot/grub2/grub.cfg

    grub 配置文件

    /etc/default/grub 基本的 配置grub 的配置文件, 一般直接修改该文件.

    /etc/grub.d/ 详细的 配置grub 的配置文件, 对内核每一项进行更详细的配置, 一般不需要修改这部分的文件.

    /boot/grub2/grub.cfg grub2 的实际配置文件, 一般不手动修改该配置文件以免出错.

    grub2-mkconfig -o /boot/grub2/grub.cfg 使用 /etc/default/grub/etc/grub.d/ 来生成 /boot/grub2/grub.cfg 配置文件

    /etc/default/grub

    GRUB_TIMEOUT=5
    GRUB_DEISTIBUTOR="....."
    
    # 默认引导项, 可通过 grub2-set-default 命令来修改.
    GRUB_DEFAULT=saved
    
    GRUB_DISABLE_SUBMENU=true
    GRUB_TERMINAL_OUTPUT="console"
    
    # 引导时传给内核的参数
    ## quiet     只打印必要的输出, 在出问题需要排查时可将该参数移除掉
    ## rghb        以图形界面显示引导进度, 在出问题需要排查时可将该参数移除掉来显示更多的信息
    GRUB_CMDLINE_LINUX="crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rghb quiet biosdevname=0 net.ifnames=0"
    
    GRUB_DISABLE_RECOVERY="true"

    文件修改后若想要生效, 则需要执行以下命令来生成grub实际使用的配置文件

    grub2-mkconfig -o /boot/grub2/grub.cfg

    使用单用户进入系统以重置root密码

    1. 在系统启动到选择引导项时, 根据提示输入 e 来编辑所选择引导项.
    2. 找到第一个 linx16 .... 行, 此时若是 CentOS6 则在末尾追加 single (注意有个空格), 若是CentOS7则在末尾追加 rd.break
    3. 输入 Ctrl+x 启动, 此时会进入到一个命令提示符状态 switch_root:/#

      注意, 此时 系统根目录没有挂载在硬盘根下面

      此时的 / 目录是一个位于内存中虚拟的目录, 实际的硬盘根目录位于 /sysroot 下, 同时 /sysroot默认是以只读方式挂载的.
      # 1. 以 rw 模式重新挂载文件系统
      ## -o, --options <挂载选项, 以逗号分隔>
      mount -o remount,rw /sysroot
      
      # 2. 此时还是处于虚拟的文件系统, 因此需要切换到到实际的系统目录下
      ## 执行完成后命令提示符变成 sh-4.2# , 而非之前的 switch_root:/#
      chroot /sysroot
      
      # 3. 修改root密码
      echo 123456 | passwd --stdin root
      
      # 4. 关闭 SELinux
      ## 由于 SELinux 会校验 /etc/passwd 和 /etc/shadow 文件, 因此此处关闭 SELinux
      ## 生产环境一般也都会关闭 SELinux
      sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
      
      # 5. 回到虚拟的根目录环境
      exit
      
      # 6. 重启
      reboot

    修改相关目录权限后开启selinux无法启动

    1. 进入单用户引导模式(即上面的通过grub引导修改 rd.break 进入)
    2. 执行 genhomedircon 给用户的home目录重新生成selinux相关的文件标签
    3. 之后就可以正常启动

    或者是在根目录下创建一个文件 touch /.autorelabel, 之后 linux 会检测到该文件, 并自动进行重新检测标签, 重新打标签(约10~20分钟).

    进程管理

    CentOS 7 的一号进程是 systemd, CentOS 6 的一号进程是 init

    进程查看

    ps 命令

    查看进程状态, 具体请查看 系统综合状态查看 种的 ps 小节.

    pstree 命令

    进程是树形结构的.

    查看进程树形结构
    
    pstree 选项 [进程=1]
     
    选项
        -p        打印进程号

    top 命令

    请参照最下方的系统综合状态查看

    进程的控制命令

    调整进程的优先级

    nice命令

    NICE 范围从 -20~19, 值越小优先级越高, 抢占资源就越多.

    以指定的优先级来执行程序
    
    nice [选项] <Command>
    
    选项
        -n <优先级>        # 若不指定则默认是10

    renice 命令

    重新设置正在运行的进程的优先级
    
    renice [选项] <进程号>
    
    选项
        -n, --priority <priority=10>        指定优先级(NICE)

    进程的作业控制

    进程的前后台切换一般称作进程的作业控制.

    & 符号

    通过在执行的命令末尾追加一个 &(注意有空格), 使得进程直接进入后台运行状态.

    此时我们的键盘输入依旧可以被终端捕获.

    执行命令 &

    ctrl + z 操作

    对于在当前终端在前台执行的作业, 可以通过 ctrl+z 按键使该作业转入后台并挂起, 作业状态变为 S(stopped)/

    可以通过 jobs 查看到作业序号, 再通过 fgbg 将该作业调到前台或后台执行.

    jobs 命令

    jobs 是 shell 内嵌的.

    显示作业状态
    
    jobs [选项]
    
    选项
        -l        额外打印出进程号

    fg 命令

    fg 是 shell 内嵌的.

    恢复作业, 令作业在前台执行
    
    fg <作业序号>

    bg 命令

    bg 是 shell 内嵌的.

    恢复作业, 令作业在后台执行
    
    bg <作业序号>

    进程的通信方式 - 信号

    信号是进程间通信的基本方式之一.

    管道也是一种进程间基本通信方式.
    终止进程
    
    语法
        kill [-s <signal>] <pid|进程名|%jobid>    发送信号给指定进程|后台作业, 未指定信号则默认发送 SIGTERM(15)
        kill -l [<signal>]                        打印信号列表(或某个信号对应的数字或名称)
    
    示例
        以下命令等效
        kill -1 <pid>    # 发送 SIGHUP 信号给指定进程
        kill -hup <pid>    # 发送 SIGHUP 信号给指定进程(信号名大小写无所谓)
        kill -sighup <pid>    # 发送 SIGHUP 信号给指定进程(信号名大小写无所谓)
        kill -s 1 <pid>    # 发送 SIGHUP 信号给指定进程(信号名大小写无所谓)
        kill -s hup <pid>    # 发送 SIGHUP 信号给指定进程(信号名大小写无所谓)
        kill -s sighup <pid>    # 发送 SIGHUP 信号给指定进程(信号名大小写无所谓)
    
        发信号给后台作业
        kill -9 %1        # 发送 KILL 信号给作业id为1的后台作业
    
    选项
        -l                打印信号列表
        -s <signal>        发送信号
        
    常用信号
        除了信号 9 之外, 其他信号都被被进程捕获并忽略. 1号进程不可杀
        SIGHUP(1)        退出终端, hangup(挂起)信号(使用 nohup 可以忽略该信号)
        SIGINT(2)        通知前台进程终止进程(等同 `ctrl+c`)
        SIGQUIT(3)        中止并生成 core 文件(等同 `ctrl+\`)
        SIGKILL(9)        立即结束进程, 不能被阻塞和捕获处理 kill -9 <pid>
        SIGTERM(15)        会杀死不能捕获该信号的进程
        SIGCONT(18)        继续(与STOP相反, 等同 `bg` 命令)
        SIGSTOP(19)        暂停(等同 `ctrl+z`), 将进程放到后台挂起(不执行状态).
    ctrl+d 不是发信号, 而是用于表示一个特殊的二进制值, 表示 EOF

    在一些地方很有用, 比如 cat 时会等待输入, 此时则输入完毕后按 ctrl+d 表示输入结束.

    守护进程(daemon)和系统日志

    注意, nohup 启动的进程并不是守护进程, 概念上不要搞错.

    nohup 命令

    使用 nohup 命令启动的程序会忽略 SIGHUP 信号, 就算关掉终端, 该程序依然会继续运行.

    在运行该程序的终端被关闭后, 该进程会变成孤儿进程.

    该孤儿进程自动会被 1 号进程收留, 也就是其父进程会变成 1.

    但依然会占用其启动时的目录, 导致该目录无法卸载, 这是其与守护进程的区别之一(守护进程的工作目录是 /)

    该命令经常与 & 符号一起使用.

    目的是运行某个程序, 并让该程序脱离该终端仍可继续执行.

    使程序运行时不挂起, 不向tty输出信息, 忽略输入.
    默认是把输出追加到 nohup.out 文件
    
    nohup <command>

    /proc 目录

    该目录是系统内存中的信息以文件形式的映射, 并非真实存在的物理文件.

    每个进程都有一个对应的 /proc/<进程号> 目录, 表示其对应的内存数据.

    /proc/<进程号> 部分文件说明

    cwd            # (连接)该进程的工作目录(此时该目录是无法卸载的), 守护进程一般是指向 /
    fd            # 打开的文件描述符
        |- 0    # (连接)标准输入, 可能是 /dev/pts/0, 或 /dev/null
        |- 1    # (连接)标准输出, 可能是 /dev/pts/0, 或 /dev/null, /path/to/nohup.out, socket:[xxxx], /var/log/message
        |- 2    # (连接)标准错误, 可能是 ...

    screen 命令/工具

    一般在远程连接到服务器后, 为了防止执行到一半时连接中断导致数据丢失等, 可以通过进入 screen 环境来执行命令, 此时若连接丢失则可在下一次连接后再次进入screen来恢复之前的环境.

    CentOS 7 默认没有安装该工具, 需要自行安装.

    yum install screen
    # 进入 screen 环境
    screen
    
    # 退出(detached) screen 环境
    ## 按Ctrl+a时不会有任何提示信息, 此时再按d就退出screen环境
    Ctrl+a d
    
    # 查看 screen 的会话
    screen -ls
    
    # 恢复会话
    screen -r <sessionid>
    
    # 若掉线后查看到会话状态是 attached, 且无法恢复, 则可踢掉踢掉前一用户并恢复会话
    screen -D -r <sessionid>

    screen命令

    screen [选项]
    
    示例
        screen        直接进入screen环境
        
    选项
        -ls, -list            查看screen的会话
        -r <sessionid>        恢复到指定会话

    注意: screen 默认使用的是 non-login shell, 如果要使用 login shell, 则需要执行如下语句

    echo 'defshell -$SHELL' >> ~/.screenrc
    
    # 如果上述语句无效, 则使用如下语句:
    echo 'defshell -bash' >> ~/.screenrc
    可通过 shopt login_shell 查看当前是否为 login-shell

    系统日志

    /var/log/

    重点关注文件

    • /var/log/messages 系统常规日志
    • /var/log/dmesg 内核运行相关消息(通常时启动时的内核日志)
    • /var/log/secure 安全日志
    • /var/log/cron crond计划任务日志

    服务管理工具 systemctl

    服务(提供常见功能的守护进程) 集中管理工具一般是:

    • service
    • systemctl

    在 CentOS 7 及以上使用 systemctl 替代 service 工具.

    CentOS 6中使用service管理服务, 需要自己编写复杂的启停脚本, 而在 systemctl 中则简化了这一部分.

    service

    配置文件存放: /etc/init.d/

    管理SysV服务
    
    service <服务名> <操作>
    
    操作
        status        查看服务运行状态
        start        启动服务
        stop        停止服务
        restart        重启服务

    配置开机自启动需要使用 chkconfig 命令.

    service 服务运行级别的查看和设置
    
    chkconfig [选项] <服务名>
    
    示例
        chkconfig --list            # 列出所有 SysV 服务的运行级别
        chkconfig --list <服务名>      # 查看指定服务的运行级别
        chkconfig [--level <level>...] <服务名> (on|off)    # 设置服务在指定级别下是否自动启动
        
    常用级别
        0    关机
        3    字符终端
        5    图形终端
        6    重启

    systemctl

    配置文件(服务单元)存放处: /usr/lib/systemd/system/

    控制systemd系统 以及 服务管理
    
    systemctl <命令> [<服务名>]
    
    示例
        systemctl set-default multi-user.target        # 设置以字符界面启动(即多用户级别)
    
    命令
        status <服务>
        start <服务>
        stop <服务>
        restart <服务>
        reload <服务>
        enable <服务>                    启用开机自动启动
        disable <服务>                禁用开机自动启动
        list-unit-files [<服务>]
        get-default                         获取当前的target
        set-default <字母表示的服务级别>          设置指定的target为默认的运行级别
        isolate <字母表示的服务级别>              立即切换到指定的运行级别
        
    示例
        systemctl list-unit-files                            # 列出所有 systemd 服务
        systemctl list-unit-files NetworkManager.service    # 列出 NetworkManager 服务

    查看所有服务级别 /lib/systemd/system/

    ls -l /lib/systemd/system/runlevel*target
    
    # runlevel0.target -> poweroff.target
    # runlevel1.target -> rescue.target
    # runlevel2.target -> multi-user.target
    # runlevel3.target -> multi-user.target
    # runlevel4.target -> multi-user.target
    # runlevel5.target -> graphical.target
    # runlevel6.target -> reboot.target
    

    systemd 服务配置文件示例(以sshd 服务的配置文件 /usr/lib/systemd/system/sshd.service 为例)

    [Unit]
    Description=OpenSSH server daemon            # 服务描述文本
    Documentation=man:sshd(8) man:sshd_config(5)
    After=network.target sshd-keygen.service    # 在哪些服务之后启动
    
    Wants=sshd-keygen.service
    
    [Service]
    Type=notify
    EnvironmentFile=/etc/sysconfig/sshd
    ExecStart=/usr/sbin/sshd -D $OPTIONS        # 对应 start 命令
    ExecReload=/bin/kill -HUP $MAINPID            # 对应 reload 命令
    KillMode=process
    Restart=on-failure
    RestartSec=42s
    # Specifies the maximum file descriptor number that can be opened by this process
    #LimitNOFILE=65535
    # Specifies the maximum number of processes
    #LimitNPROC=4096
    # Specifies the maximum size of virtual memory
    #LimitAS=infinity
    # Specifies the maximum file size
    #LimitFSIZE=infinity
    
    [Install]
    WantedBy=multi-user.target                    # 设置在哪个级别下自动启动
    

    SELinux 简介

    SELinux - 安全增强的 Linux 版本

    DAC(自主访问控制): 利用用户权限和文件权限来控制.

    MAC(强制访问控制): 进程、用户、文件 的标签三者需一致才允许访问.

    SELinux 会降低系统性能, 因此一般生产环境都会关闭.

    查看 SELinux 的命令

    # 查看当前 enforce 的状态
    getenforce
    
    # 查看进程的标签
    ps -Z
    
    # 查看文件的标签
    ls -Z
    
    # 查看当前用户的标签
    id -Z

    关闭 SELinux

    # 方法1: 临时关闭, 重启失效
    setenforce 0
    
    # 方法2: 写入配置永久生效(需重启系统才会生效)
    ## enforcing 强制访问控制
    ## permissive 只警告而不控制
    ## disabled 关闭(生产环境推荐)
    sed -i 's/^SELINUX=\w+/SELINUX=disabled/' /etc/selinux/config

    获取 selinux 的布尔值

    获取 selinux 的布尔值
        on 表示允许访问
        off 表示禁止访问
        
    
    getsebool [<item>]
    
    选项
        -a        # 查看所有布尔值

    设置 selinux 的布尔值

    设置 selinux 的布尔值
    
    setsebool 选项 <item> <value>
    
    选项
        -P        # 同时写入配置文件(默认只在内存修改)
    
    value
        0        # 表示off, 关闭
        1        # 表示on, 打开

    内存和磁盘管理

    内存和磁盘使用率查看

    内存使用率查看

    可以使用 freetop 查看

    free 命令

    内存使用情况查看
    
    free [选项]
    
    选项
        -b        以byte为单位
        -k        以KB为单位(默认)
        -m        以MB为单位
        -g        以GB为单位(1024, 向下取整)
        -h        根据数值大小自动使用合适的单位
        
    输出示例
                      total        used        free      shared  buff/cache   available
        Mem:           1837         619         324          30         893        1006
        Swap:          2047           0        2047
    
    输出字段意义解释
        Mem                系统内存
        Swap            交换分区
        buff/cache        进程运行时拿来作为缓存的内存, 可以通过一定手段释放掉.
        available         表示若 buff/cache 全释放掉后可以使用的内存数量. 一般是看这个值.
    一般会设置一部分Swap以避免系统内存耗光后出发系统OOM(?)操作导致随机kill部分进程.

    磁盘使用率查看

    fdisk 命令

    分区表操作工具软件
    只能看到分区信息, 但无法看到其挂载的信息.
    
    fdisk [选项]
    
    选项
        -l        查看分区表
        
    交互式命令
        n        添加一个分区
        d        删除一个分区
        p        打印分区表
        w        将配置写入数据(在此之前只存在内存中)
        q        退出

    打印的设备信息中:

    • Boot列显示 * 表示该分区是启动分区
    • 磁盘设备的 Start、End是以扇区为单位.
    • System 表示该分区所使用的分区格式
    • Blocks 的单位是 KB

    类似的命令还有 parted -l, 显示的格式会略微不一样, 会更详细.

    df 命令

    报告文件系统磁盘空间使用率
    可以看作是 fdisk 的补充, 可以看到文件系统的挂载信息.
    
    df [选项] [<file>...]
    
    选项
        -h, --human-readable        用合适的单位来展示
        -T, --print-type            显示每个文件系统的类型
        
        
    示例
        df -hT /dev/**        # 查看硬盘/分区的空间及文件系统格式

    du 命令

    报告磁盘空间使用情况(实际占用空间)
    
    du [选项]
    
    选项    
        -s, --summarize              对每个参数只显示总和。
        -h, --human-readable        用合适的单位来展示

    ls 与 du 查看文件大小是不同的:

    • ls 统计的是记录到文件开头和结尾一共占用的长度.
    • du 真正统计了文件的实际长度.

    Eg. 比如用 dd 创建有空洞的文件 1GB 时, du 查看的就是真实大小, 可能就几MB, 而 ls 查看到的大小就是 1GB.

    lsblk 命令

    显示块设备
    
    lsblk [选项]
    
    选项
        -f, --fs        显示文件系统信息

    ext4 文件系统

    Linux 支持多种文件系统, 常见:

    • ext4
    • xfs
    • NTFS(需安装额外软件 ntfs-3g)
    CentOS 6 默认使用 ext4 文件系统.

    CentOS 7 默认使用 xfs 文件系统.

    NTFS 格式由于版权问题, 在 Linux 挂载后默认是只读的, 需要安装额外软件 ntfs-3g 才能写入.

    ext4 文件系统基本结构

    • 超级块
    • 超级块副本
    • i节点(inode)

      文件名称, 大小, 编号, 权限信息都会在 i节点体现.

      也叫索引节点?

      注意, 文件名是记录在文件父目录的i节点中.

      引申: 文件的读权限是读取文件的数据块内容, 而目录的读权限是读取目录底下的文件名称.

    • 数据块(datablock)

      记录数据.

      数据块是挂载在i节点后面, 形成链式结构.

      ext4 和 xfs 默认创建的数据块是 4KB.

    ls 命令查看的是i节点中文件大小信息, 而 du 是查看数据块的信息, 所以显示的结果会有差异.

    mv 对文件改名实际影响的是其父目录i节点中的信息, 因此文件大小并不会影响修改速度.

    rm 命令是将文件名和i节点的链接断开, 这就是为什么文件的大小不会影响删除的速度, 以及有些文件被删除后空间并没有真正释放.

    找回被删掉的文件的原理就是搜索磁盘上的i节点及其链接的数据块, 从而找回丢失的数据.

    ln 命令原理是让更多的文件名指向i节点, 使用命令 ln afile bfile, 此时会有一个新的文件名 bfile 指向同一个i节点, 使用 ls -l 命令可以看到该文件对应i节点的链接数多了1个.

    使用 ln 是不能跨越文件系统的, 因为i节点信息是存在当前文件系统中.

    文件、目录与 inode(i节点)

    Linux文件系统里文件和文件名的关系如下图。

    inodes

    目录也是文件,文件里存着文件名和对应的inode编号。通过这个inode编号可以查到文件的元数据和文件内容。文件的元数据有引用计数、操作权限、拥有者ID、创建时间、最后修改时间等等。文件件名并不在元数据里而是在目录文件中。因此文件改名、移动,都不会修改文件,而是修改目录文件。

    借《UNIX环境高级编程》里的图说一下进程打开文件的机制。

    file pointer

    进程每新打开一个文件,系统会分配一个新的文件描述符给这个文件。文件描述符对应着一个文件表。表里面存着文件的状态信息(O_APPEND/O_CREAT/O_DIRECT...)、当前文件位置和文件的inode信息。系统会为每个进程创建独立的文件描述符和文件表,不同进程是不会共用同一个文件表。正因为如此,不同进程可以同时用不同的状态操作同一个文件的不同位置。文件表中存的是inode信息而不是文件路径,所以文件路径发生改变不会影响文件操作。

    上面这部分的描述来自: https://www.lightxue.com/how-...

    ln 命令

    在文件之间建立连接
    默认是创建硬链接, 即新建一个文件名并让其指向 <src> 的i节点.
    硬链接不能跨越文件系统.
    
    ln [选项] src dest
    
    选项
        -s        创建软链接(符号链接), 其文件内容记录的是对应的访问路径. 软链接的权限设置是无效的. 
                软链接可以跨越分区系统

    facl 功能

    xfs 和 ext4 支持文件访问控制列表(facl)

    ACL 表示访问控制列表Access Control List(ACL)

    设置了facl权限的文件会有一个额外的 + 权限表示.

    -rw-------+ 2 root root 10 2020-02-24 22:51 afile

    getfacl

    查看文件或目录的ACL
    
    getfacl [选项] <file>
    
    选项
        -R, --recursive         # 递归
    
    示例
        getfacl -R <目录> > acldir.acl        # 备份目录的acl权限信息, 并通过 `cd <目录> && setfacl --restore acldir.acl` 来恢复

    setfacl

    设置文件或目录的ACL
    
    setfacl [选项] <权限> <file>
    
    示例
        setfacl -m u:user1:r afile        # 给用户 user1 赋予读 afile 文件的权限
        setfacl -m g:group1:rw afile    # 给组 group1 赋予读写 afile 文件的权限
        setfacl -x u:user1 afile        # 移除给 user1 赋予的 afile文件权限
        setfacl -x g:group1 afile        # 移除给 group1 赋予的 afile文件权限
    
    选项
        -m, --modify <acl>        # 修改文件的访问控制权限, 此时 <acl> 格式为: (u|g):(<user>|<group>):(r|w|x)
        -x, --remove <acl>        # 移除文件的访问控制权限, 此时 <acl> 格式为: (u|g):(<user>|<group>)    
        --set <acl>               # 替换当前所有的acl(可以理解为清空当前的再添加), 注意保留基本的 UGO 权限
        -b, --remove-all        # 删除所有的acl权限    
        
        -R, --recursive         # 递归应用到子目录/文件
        
        默认权限
        -d, --default            # (只)设置默认的acl权限, 只对目录有效, 会被目录中创建的文件和目录继承。
                                # 子文件会继承acl权限, 子目录同时会继承default权限(即递归继承)
                                # 另外一种格式是: d:u:user1:rw, 也就是在权限的前面多一个 `d:`
        -k, --remove-default    # 移除默认的acl权限    
        
        备份和恢复ACL权限(cp -p可以保留ACP权限, 但tar等并不会保留acl权限信息, 因此可以先备份这些信息以供恢复)
        --restore <权限备份文件>    # 恢复acl权限
        

    若是报错 Setfacl : Operation not supported 则表示挂载硬盘时默认没有加载acl规则, 因此需要重新挂载硬盘:

    1. 修改 /etc/fstab

      
      /dev/sda1             /data                ext4    defaults,acl    0 0
    2. 重新挂载

      mount -o remount /data
    上述操作本人未实际操作过, 需谨慎, 防止重新挂载出错.

    示例

    我想将某个目录 /data/game 的权限全部分给指定用户 publisher, 同时该目录原先已经有文件了, 属主和属组都是 root.

    # 设置该目录的 default 权限
    setfacl -d -m u:user1:rwx /data/game
    
    # 递归更改该目录及底下文件的 facl 权限
    setfacl -R -m u:user1:rwx /data/game

    磁盘的分区与挂载

    新的磁盘的处理步骤:

    1. 分区 fdisk
    2. 格式化 mkfs
    3. 挂载 mount
    4. 写入配置 /etc/fstab

    fdisk 命令

    只能对小于 2TB 的进行分区

    对硬盘分区
    注意: 会造成数据丢失, 谨慎
    
    fdisk
    
    示例
        fdisk -l          # 查看分区表情况
        fdisk <块设备>        # 对指定块设备分区
        
    分区过程中的常用命令
        n        # 创建新分区
        d        # 删除某个分区
        w        # 将分区信息写入磁盘
        p        # 查看当前分区信息
        q        # 退出

    Tip.

    • 服务器上一般一块硬盘划分一个分区.
    • 如果要创建超过4个分区, 则需要分配3个主分区, 1个扩展分区, 然后在扩展分区中再创建多个逻辑分区.
    • ext4 和 xfs 默认创建的数据块是 4KB.

    mkfs 命令

    
    格式化分区
    
    示例
        mkfs.ext4 [选项] <分区>        # 将分区格式化为 ext4 格式
        mkfs.xfs [选项] <分区>        # 将分区格式化为 xfs 格式
        
    选项
        -f        # 强制操作

    parted 命令

    分区大于 2T 应使用 parted 命令, 而不是 fdisk

    另一个分区工具
    
    parted <块设备>

    mount 命令

    挂载文件系统
    
    mount [选项] <块设备> <目标挂载目录>
    
    选项    
        信息查看
        -l                # 查看当前挂载情况
        
        -a, --all        # 挂载 /etc/fstab 配置文件中的所有配置
        
        挂载选项
        -t, --types <vfstype>        # 指定分区格式类型, 若不指定或指定为 "auto" 则会自行判定分区格式类型.
        -o, --options <options>        # 指定挂载参数, <options> 可用逗号分隔多个参数
                                    # 可用参数
                                    #     defaults    默认
                                    #    noatime        读文件时不修改 inode 的访问记录(atime), 可以提高IO性能.
                                    #    remount        重新挂载
        --bind <src> <dest>            # 将某个目录或文件挂载到指定的目录, 而不要求<src>是块设备. 这个操作实际上是修改/替换 inode    
        

    注意

    • mount命令并不会写入配置, 仅仅保存在内存, 因此重启后失效

      需写入 /etc/fstab/ 才能持久化
    • 目标挂载目录必须是已存在的.

    对文件的操作是文件级别的操作, 是在文件系统的更上一层.

    因此我们无法直接对类似 /dev/sdc1 这样的设备进行操作, 需要先挂载到目录, 再对目录操作.

    umount 命令

    卸载文件系统
    
    umount <挂载目录>|<块设备>

    /etc/fstab 配置文件

    格式

    磁盘挂载配置文件
    
    Eg.
    /dev/sdb1     /mnt/sdb1            ext4         defaults         0                         0
    <分区>        <挂载目标目录>    <文件系统>        挂载权限    指示是否硬盘备份(dump)        是否开机磁盘自检
                交换分区是swap     交换分区是swap                  一般设置为0              主要是针对ext2, ext3
                                                                                    一般设置为0
    
    挂载权限
        defaults    # 默认挂载参数

    若配置写错导致重启失败, 则可通过 grub 进入单用户模式来修复该文件.

    chroot 命令

    以特定目录作为进程的根目录运行指定命令
        - 若未指定 command, 则默认是执行 $SHELL -i (即一般是 /bin/bash -i)
    
    chroot [选项] <新root路径> [<command=$SHELL -i>]
    

    磁盘配额的使用

    掌握原理和方法即可, 生产环境一般用虚拟化来限制使用资源, 包括cpu, 内存, 磁盘..

    注意: root 用户是不受限的, 不应使用 root 用户来测试.

    xfs

    xfs 文件系统的用户磁盘配额 quota

    操作步骤(以 /dev/sdb1 为例)

    # 1. 确保是 xfs 文件系统
    mkfs.xfs /dev/sdb1
    
    # 2. 挂载文件系统
    mkdir /mnt/disk1
    mount -o uquota,gquota /dev/sdb1 /mnt/disk1
    
    # 3. (可选)修改挂载目录权限
    ## 此处为方便测试, 设置其为 1777, 即 rwxrwxrwt
    chmod 1777 /mnt/disk1
    
    # 4. 查看磁盘配额状态
    ## report 指令参数说明
    ### -u 显示user限额状态
    ### -g 显示group限额状态
    ### -i 显示inode配额状态
    ### -b 显示block(块)配额状态
    ### -h 以human-readable方式展示
    xfs_quota -x -c 'report -ugibh' /mnt/disk1
    
    # 5. 设置磁盘配额
    ## 命令意义说明: 以下命令是限制用户 "user1" 创建的 inode 数量软硬限制分别是5和10, 超过软限制后会有一段宽限时间.
    ## limit 指令参数说明
    ### -u 限制用户
    ### isoft 设置inode节点数量的软限制
    ### ihard 设置inode节点数量的硬限制
    ### bsoft 设置block数量的软限制
    ### bhard 设置block数量的硬限制
    xfs_quota -x -c 'limit -u isoft=5 ihard=10 user1' /mnt/disk

    交换分区(虚拟内存)的查看与创建

    命令

    mkswap 命令

    创建交换分区
    
    mkswap

    swapon 命令

    需要先用 mkswap 将目标文件或设备创建为交换分区才可以挂载.

    启用
    
    swapon [选项] [<文件或设备>]
    
    示例
        swapon /swapfile    # /swapfile 是交换分区文件, 此处启用该文件作为交换分区.
    
    选项
        -s, --summary        # 显示已使用的交换分区

    swapoff 命令

    关闭用于交换分区的设备
    
    swapoff [选项] [<文件或设备>]
    
    选项
        -a        # 关闭所有用于交换分区的文件和设备

    使用新的分区作为交换分区

    此处以 /dev/sdb1 作为示例

    # 格式化指定分区
    mkswap /dev/sdb1
    
    # 启用该分区作为交换分区
    swapon /dev/sdb1
    
    # 4. 写入配置以持久化
    cat <<"EOF" >> /etc/fstag
    /dev/sdb1 swap swap defaults 0 0
    EOF

    使用文件制作交换分区

    使用带空洞的文件(此处以 /swapfile 文件为例)来作为交换分区

    # 1. 创建带空洞的文件
    dd if=/dev/zero of=/swapfile bs=4M count=1024
    
    # 2. 修改文件权限(不然会报警告)
    chmod 600 /swapfile
    
    # 3. 启用该文件作为交换分区
    swapon /swapfile
    
    # 4. 写入配置以持久化
    cat >> /etc/fstag <<"EOF"
    /swapfile swap swap defaults 0 0
    EOF

    软件RAID的使用

    RAID(磁盘阵列) 的常见级别及含义

    • RAID 0

      stripping 条带方式, 将数据拆成N份分别写到N个磁盘中(任意磁盘损坏数据就全毁了), 可以提高单盘吞吐率.

    • RAID 1

      mirroring 镜像方式, 将数据复制到另一个磁盘中(浪费一块硬盘空间), 提高可靠性.

    • RAID 5

      RAID 0RAID 1 的组合和简化, 有奇偶校验, 至少需要3块硬盘: 第1,2块硬盘写数据, 第3块硬盘写奇偶校验.(轮流写数据和校验)

      坏掉任意一块磁盘数据都可以挽回, 但损坏超过1块则数据丢失.

    • RAID 10

      RIAID 1RAID 0 的组合. 至少需要4块硬盘, 2块硬盘组合RAID 1, 另2块硬盘也组合 RAID 1, 再组合作 RAID 0.

      只要不是同一侧的两个硬盘都损坏数据就不会丢失.

    软件实现的RAID会额外占用不少CPU, 因此在工作中一般是使用RAID卡来处理.

    madmn 命令

    组建RAID, 尽量保持每个磁盘大小一致, 如果不一致, 则一般会按照容量最小的来算.

    软件RAID不支持系统引导, 即 /boot 不能存储在该阵列中.

    管理MD设备 - 即软件RAID
    # 一般约定创建的第一个硬盘是 md0
    
    mdadmin [选项] <设备>...
    
    
    选项
        创建
        -C, --create                # 创建新的阵列(array)
        -a, --auto yes                # 自动同意, 对于所有提示都是 "yes" (若已存在数据则会被清掉, 谨慎)
        -l, --level <RAID level>    # RAID 级别
        -n, --raid-devices <num>    # 指定在阵列中有 <num> 个块设备是活动(active)的
        
        查看
        -D, --detail                # 查看 md 设备详情
        
        卸载
        -S, --stop                    # 停止 md 设备(deactive), 并释放所有资源
        
        
        --scan
        --verbose
        
    示例    
        mdadm -D /dev/md0    # 查看创建的阵列 /dev/md0 的详情
        mdadm -S /dev/md0    # 停止 md 设备

    在配置好RAID后将当前的配置持久化

    mdadm --detail --scan --verbose > /etc/mdadm.conf

    示例: 用两个硬盘组成 RAID 1

    /dev/md0
        |- /dev/sdb1
        |- /dev/sdc1
    
    # 1. 创建硬盘 /dev/md0 , 有2块硬盘是活动的, 由 /dev/sdb1, /dev/sdc1 组成RAID 1 级别.
    mdadmin -C /dev/md0 -a yes -l 1 -n 2 /dev/sd[b,c]1
    
    # 2. 持久化到配置文件, 下次开机后才能保持软RAID生效
    echo DEVICE /dev/sd[b,c]1 >> /etc/mdadm.conf
    
    # 3. 追加配置持久化到文件, 下次开机后才能保持软RAID生效
    mdadm -Evs >> /etc/mdadm.conf
    
    # 4. 格式化该分区, 以供正常存储数据
    mkfs.xfs /dev/md0

    示例: 删除软RAID

    # 1. umount 已挂载的 md 设备, 此处以 /dev/md0 设备为例
    umount /dev/md0
    
    # 2. 停止RAID设备
    mdadm -S /dev/md0
    
    # 3. 删除组成RAID的硬盘中的超级块数据(此处的 /dev/md0 由 /dev/sd[b,c]1 组成)
    # 也可以手动用其他命令破坏掉, 比如 dd if=/dev/zero of=/dev/sdb1 bs=1M count=1
    mdadm --misc --zero-superblock /dev/sdb1
    mdadm --misc --zero-superblock /dev/sdc1
    
    # 4. 删除配置文件(此处假设该文件内只有 /dev/md0 的配置)
    rm -f /etc/mdadm.conf

    逻辑卷管理

    相关概念

    在服务器上的磁盘空间只增不减

    一个物理设备就是一个物理卷.

    文件系统无法跨硬盘使用.

    上面的软RAID创建的 md0 也是个逻辑卷, Linux 默认使用的根目录也是用逻辑卷去管理的.

    可以把N个大小不一样的硬盘拼成一个卷组, 再切割成逻辑卷.

    使用逻辑卷的好处在于可以动态扩容.

    首先要理解文件系统是一个分层的结构.

    挂载
    ↑
    文件系统
    ↑
    逻辑卷
    ↑
    卷组
    ↑
    物理卷
    ↑
    分区
    ↑
    硬盘

    如果要给某个逻辑卷扩容, 则需要依次处理以下:

    1. 新增硬盘并分区 fdisk
    2. 将新分区创建为物理卷 pvcreate
    3. 将物理卷加入到需扩容的卷组 vgextend
    4. 给该卷组的目标逻辑卷扩容 lvextend
    5. 给目标卷的文件系统更新新的容量信息 xfs_growfsresize2fs

    整个过程是自底向上逐层处理的.

    创建逻辑卷的相关命令

    物理卷

    pvcreate 命令
    为LVM初始化物理卷(physical volumes)
    
    pvcreate <块设备>
    
    示例
        pvs /dev/sd[b,c,d]1        # 初始化物理卷 /dev/sdb1, /dev/sdc1, /dev/sdd1
    pvs 命令
    显示物理卷(physical volumes)信息
    
    pvs
    
    
    示例输出信息详解
        PV                VG        Fmt        Attr    PSize            PFree
        /dev/sda2        centos    lvm2    a--        <39.00g            4.00m
        /dev/sdb1                lvm2    ---        <2.00g            <2.00g
        /dev/sdc1                lvm2    ---        <2.00g            <2.00g
        /dev/sdd1                lvm2    ---        <2.00g            <2.00g
        物理卷            卷组                     物理的大小  物理卷在物理空间实际剩余大小
                    Volume Group
    输出解释
        lvm2        # linux 逻辑卷管理器
        /dev/sda2 属于卷组 centos, 它的空间被卷组 centos 给几乎全占用了.
    pvremove 命令
    移除物理卷的 LVM 标签
    
    pvremove [选项] 物理卷名

    卷组

    vgcreate 命令
    创建卷组(volume groups)
    注意: 单个物理卷只能归属于一个卷组.
    
    vgcreate <卷组名称> <物理卷>
    
    示例
        vgcreate vg1 /dev/sdb1 /dev/sdc1    # 创建卷组 vg1, 并将物理卷加入到卷组里.
    vgs 命令
    显示卷组(volume groups)信息
    
    vgs
    
    输出示例
        VG        #PV                #LV                #SN    Attr    VSize        VFree
        centos    1                2                0    wz--n-    <39.00g        4.00m
        👆
        卷组名      组成的物理卷数量    划分出的逻辑卷数量                卷组总大小    卷组剩余大小
    vgremove 命令
    移除卷组
        - 移除有逻辑卷的卷组时会提示是否移除对应逻辑卷
    
    vgremove [选项] 卷组名

    逻辑卷

    lvcreate 命令
    创建逻辑卷
    
    lvcreate [选项] <卷组名>
    
    选项
        -L, --size <大小>            # 指定创建的逻辑卷的大小, eg. 100M
        -n, --name <逻辑卷名>      # 指定要创建的逻辑卷的名字
    lvs 命令
    查看逻辑卷(logical volumes)信息
    
    lvs
    lvremove 命令
    从系统上移除逻辑卷
        - 只指定卷组时, 会移除该卷组上的所有逻辑卷
    
    lvremove <卷组>[/<逻辑卷>]

    创建逻辑卷的过程

    假设提供了2块硬盘 /dev/sdb/dev/sdc, 我们需要创建一个叫做 vg1 的卷组, 并在该卷组上创建一个叫做 lv1 的逻辑卷, 并将其格式化后挂载在 /mnt/test 目录.

    # 1. 分别创建 /dev/sdb1 和 /dev/sdc1 分区
    fdisk /dev/sdb
    fdisk /dev/sdc
    
    # 2. 初始化物理卷, 并查看物理卷详情
    pvcreate /dev/sd[b,c]1
    pvs
    
    # 3. 创建卷组, 并查看卷组详情
    vgcreate vg1 /dev/sdb1 /dev/sdc1
    vgs
    
    # 4. 创建逻辑卷, 并查看逻辑卷详情
    lvcreate -L 100M -n lv1 vg1
    lvs
    
    # 5. 格式化逻辑卷
    mkfs.xfs /dev/vg1/lv1
    
    # 6. 挂载并正常使用
    mount /dev/vg1/lv1 /mnt/test

    动态扩容逻辑卷

    vgextend 命令

    卷组扩容, 扩容后卷组的可用空间会变大
    
    vgextend <卷组> <物理卷>...
    
    示例
        vgextend centos /dev/sdd1        # 使用物理卷 /dev/sdd1 给卷组 centos 扩容.  centos 是某个卷组的名

    lvextend 命令

    逻辑卷扩容
    
    lvextend [选项]
    
    选项
        -L, --size <大小>        # 指定扩容大小
    
    示例
        lvextend -L +10G root        # 给逻辑卷 root 增加 10G空间. root 是某个逻辑卷的名.

    给逻辑卷扩容后, 但是文件系统并没有变大, 因此还需要使用相应命令给具体文件系统扩容.

    # xfs 文件系统
    xfs_growfs /dev/centos/root        # 给 /dev/centos/root 逻辑卷的文件系统扩容.
    
    # ext4 文件系统
    resize2fs /dev/centos/root        # 未确认

    其他

    dd 命令

    转换和拷贝文件
    
    dd 
    
    if=<input>        # /dev/zero
    of=<output>        # 
    
    bs=<size>            # 块大小, 每次按指定的块大小来读写. eg. 4M
    count=<num>            # 写次数
    seek=<block-size>    # 目标文件跳过的块数

    关于 if

    • /dev/zero "输入设备", 读取时提供无限的空字符, 常用于产生一个特定大小的空白文件.
    • /dev/null 空设备, 丢弃写入的任意数据, 读取时会立即得到一个 EOF
    • /dev/random 随机伪设备, 提供永不为空的随机字节数据流.
    • /dev/urandom 随机伪设备, 速度比 /dev/random 快得多, 但"相对"的不够随机. 对随机性要求不高时直接使用该伪设备即可.

    资源控制

    ulimit

    /etc/security/limit.conf 配置文件

    目前个人使用

    * hard nofile 1000000 #限制单个进程最大文件句柄数(到达此限制时系统报警)
    * soft nofile 1000000 #限制单个进程最大文件句柄数(到达此限制时系统报错)
    * soft core unlimited   # 创建的核文件的最大尺寸
    * soft stack 10240      # 最大栈尺寸

    ulimit 命令

    shell 资源限制
        若不填写限制值, 则打印对应资源限制报告.
        若填写限制值, 则是设定.
    
    ulimit [选项] [<限制>]
    
    选项
        -a        显示目前资源限制报告
    
        -S        设定软限制
        -H        设定硬性限制
        
        -c        core 文件创建数量最大值
        -d        程序数据段(data segment)最大值
        -n        打开的文件数量最大值
        -s        栈大小(stack)最大值
        -b        the socket buffer size
        -e        the maximum scheduling priority (`nice')
        -f        the maximum size of files written by the shell and its children
        -i        the maximum number of pending signals
        -l        the maximum size a process may lock into memory
        -m        the maximum resident set size
        -p        the pipe buffer size
        -q        the maximum number of bytes in POSIX message queues
        -r        the maximum real-time scheduling priority
        -t        the maximum amount of cpu time in seconds
        -u        the maximum number of user processes
        -v        the size of virtual memory
        -x        the maximum number of file locks
        
    限制值
        <具体数值>         # 限制为指定的具体指, 各自单位请参阅 help ulimit
        soft            # 限制为当前的 soft 值
        hard            # 限制为当前的 hard 值
        ulimited        # 不限制

    !!! 注意

    • /etc/security/limit.conf 中的设置对于 systemd 的 service 是不生效的
    • systemd 的 service 有专门的统一配置文件 /etc/systemd/system.conf

      DefaultLimitNOFILE=100000
      DefaultLimitNPROC=100000

      需要重新加载配置文件

      systemctl daemon-reload
    • 针对 systemd 的特定 service, 可以修改其配置文件 /lib/systemd/system/xxx.service

      [Service]
      # ...
      
      LimitNOFILE=65535
      LimitNPROC=65535

      需要重新加载配置文件

      systemctl daemon-reload
      systemctl restart nginx.service

    其他待整理

    查看原文

    赞 0 收藏 0 评论 0

    嘉兴ing 发布了文章 · 1月18日

    ⭐《Linux实战技能100讲》个人笔记 - 1~2. 基础及系统操作篇

    [TOC]

    写在前面:

    Virtualbox 安装增强工具需要先执行以下命令才行:

    yum install -y gcc gcc-c++ kernel kernel-devel

    若提示无法加载光驱, 则需要先 umount 已经挂载的光盘.

    版本

    内核版本

    https://www.kernel.org

    主版本号、次版本号、末版本号

    理论上次版本号是奇数为不稳定版, 偶数为稳定版

    但是从内核 2.6 开始就已经不按照次版本号来区分是否是稳定版了, 建议直接以官方网站上表明 "Stable" 的为准.

    发行版本

    RedHat Enterprise Linux(RHEL) - 商业收费, 是商业公司维护的发行版本的代表

    • Fedora - 免费(由RedHat 桌面版发展来的免费版, 相对RedHat Enterprise不稳定), 稳定性较差.
    • CentOS - 社区版企业操作系统(RHEL社区克隆版), 由RedHat Enterprise代码再编译(去除相关logo, 版本信息), 其实就是RedHat,免费又稳定.

    Debian - 社区免费, 是社区组织维护的发行版本的代表

    • Ubuntu - 基于Debian 的 unstable 版本加强桌面系统, 桌面系统分为三套: Gnome、KDE(kubuntu)、Xfc(xubuntu). 适合作为桌面系统

    Debian

    • 7.7 代号: Wheezy
    • 8 代号: Jessie
    • 9 代号: Stretch
    • 10 代号: Buster

    Ubuntu

    • 14 代号: Trusty
    • 16 代号: Xenial
    • 17 代号: Artful

    Alpine - 面向安全的轻型 Linux 发行版本。不同于其他的 Linux 发行版本,Alpine 采用 musl libcbusybox 进行构建,减小系统的体积和运行时资源消耗。

    注意: 如果需要涉及到编译,Alpine 镜像采用的是 musl libc 而非 glibc,这点需要额外留意
    发行版本
    基于DebianDebianUbuntuLinux MintKnoppixMEPISsiduxCrunchBang LinuxChromium OSGoogle Chrome OS
    基于Red HatRed Hat Enterprise LinuxFedoraCentOSScientific LinuxOracle Linux
    基于MandrivaMandriva LinuxPCLinuxOSUnity LinuxMageia
    基于GentooGentoo LinuxSabayon LinuxCalculate LinuxFuntoo Linux
    基于SlackwareSlackwareZenwalkVectorLinux
    其它SUSEArch LinuxPuppy LinuxDamn Small LinuxMeeGoSlitazTizenStartOS

    终端

    终端的类型

    • 字符终端(命令行终端)
    • 图形终端
    • 远程终端(VNC, SSH)

    运行级别

    init运行级别target含义
    0shutdown.target关机
    1emergency.target单用户(可以找回密码)
    2rescure.target多用户(无网络)
    3multi-user.target多用户(有网络)
    4保留,未分配
    5graphical.target图形界面
    6系统重启
    关机(poweroff) init 0

    切换到字符终端 init 3

    图形终端 init 5

    重启 init 6

    使用 systemctl 查看和修改默认运行级别

    systemctl [command] [unit.target]
    
    示例
        systemctl get-default                        获取当前的运行级别(字符)
        systemctl set-default multi-user.target        设置以字符终端级别启动(即 init 3)
    
    command:
        get-default :取得当前的target
        set-default :设置指定的target为默认的运行级别
        isolate :切换到指定的运行级别(无需重启)
        
    可选的 unit.target
        shutdown.target
        emergency.target
        rescure.target
        multi-user.target
        graphical.target

    终端提示

    # root

    $ 普通用户

    常见目录

    • / 根目录
    • /root root用户的home目录
    • /home/<username> 普通用户的home目录
    • /etc 配置文件目录

      /etc/services 文件包含服务及端口映射

    • /bin 命令目录
    • /sbin 管理命令目录
    • /usr/bin:/user/sbin 系统预装的其他命令
    • /dev 设备目录

      /dev/null        # 空设备。任何写入都将被直接丢弃(但返回"成功");任何读取都将得到EOF(文件结束标志)。
      
      /dev/zero        # 零流源。任何写入都将被直接丢弃(但返回"成功");任何读取都将得到无限多的二进制零流。
      
      /dev/random      # 真随机数发生器。以背景噪声数据或硬件随机数发生器作为熵池,读取时会返回小于熵池噪声总数的随机字节。
                       # 若熵池空了,读操作将会被阻塞,直到收集到了足够的环境噪声为止。建议用于需要生成高强度密钥的场合。
                       # [注意]虽然允许写入,但企图通过写入此文件来"预存"随机数是徒劳的,因为写入的数据对输出并无影响。
                       
      /dev/urandom     # 伪随机数发生器。更快,但是不够安全。仅用于对安全性要求不高的场合。
                       # 即使熵池空了,读操作也不会被阻塞,而是把已经产生的随机数做为种子来产生新的随机数。
                       # [注意]虽然允许写入,但企图通过写入此文件来"预存"随机数是徒劳的,因为写入的数据对输出并无影响。
                       
      /dev/pts*        # 伪终端(在init 5级别下的终端也是伪终端).  每一个伪终端(PTY)都有一个master端(共享/dev/ptmx)和一个slave端(/dev/pts*)
      
      /dev/tty*        # 终端(字符)设备
      
      
      "关于磁盘设备, 目前内核已经将SATA/PATA/IED硬盘统一使用 /dev/sd* 来表示,已经不再使用 /dev/hd* 这种过时的设备文件了。
      /dev/sr?        # SCSI CD-ROM device
      /dev/hd*        # IDE接口的硬盘
      /dev/sd*        # SATA接口的硬盘
      
    • /mnt 挂载目录
    • /proc 内存信息以文件形式的表现, 并非物理上的文件.

    环境配置

    语言

    1. 确认系统当前语言

    locale
    # 或
    echo $LANG

    2.确认系统当前是否支持中文, 若不支持则需先按照语言包

    locale -a|grep zh_CN

    3.修改成中文

    echo > /etc/locale.conf <<"EOF"
    LANG="zh_CN.UTF-8"
    EOF
    CentOS 6 是修改这个文件: /etc/sysconfig/i18n

    4.设置使用中文的 man 帮助

    yum install -y man-pages-zh-CN.noarch
    若当前不支持中文. 则需要执行命令 yum -y groupinstall "Chinese Support" 安装中文支持

    若只是想执行某个命令临时用某个语言, 可以如下

    # 查看中文的man帮助
    LANG=zh_CN.UTF-8 man iptables
    
    # 查看英文的man帮助
    LANG=c man iptables

    若出现安装完中文支持后缺失 "zh_CN" 的情况(或使用一段时间后), 则可考虑使用如下命令修复

    localedef -v -c -f UTF-8 -i zh_CN zh_CN.UTF8

    时区

    1. 确认当前时区
    date
    1. 复制并替换当前时区配置文件
    # 此处以 Asia/Shanghai 为例
    cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

    也可以使用 tzselect 获取目标地区的时区字符, 再修改 TZ 环境变量来配置.

    代理

    # 通过设置环境变量
    http_proxy=socks5://192.168.0.9:1084
    https_proxy=socks5://192.168.0.9:1084
    ftp_proxy=socks5://192.168.0.9:1084

    命令

    以下仅列出部分个人觉得值得记录的命令: 未用过的命令 或 参数复杂.

    帮助命令

    • man(manual的缩写)

      共9章

      # 查看指定命令的第 section 节的帮助, section取值: 1~9, 默认是1
      man [<section=1>] <command>
      # 依次查看各个 section(如果有的话) 的帮助
      man -a <command> 
      
      section 参数:
      1: 用户指令(Commands)
          可以从shell运行的命令
      2: 系统调用(System calls)
          必须由内核完成的功能
      3: 程序库调用(Library calls)
          大多数 libc 函数, 例如 qsort
      4: 设备(Special files) 
          /dev目录中的文件
      5: 文件格式(File format and conventions) 
          /etc/passwd等人类可读的文件的格式说明
      6: 游戏(Games)
      7: 杂项(Macro packages and conventions) 
          文件系统标准描述, 网络协议, ASCII和其他字符集,以及其他东西
      8: 系统指令(System management commands)
          类似mount等命令, 大部分只能由root执行.
      9: 内核内部指令
          已废弃.
    • help

      shell(命令解释器)自带的命令称为内部命令, 其他的是 外部命令

      # 查看命令是属于哪种类型的命令
      type <命令名>
      
      # 查看内部命令的帮助
      help <内部命令>
      
      # 查看外部命令的帮助
      <外部命令> --help
    • info

      help 更详细, 作为 help 的补充, 基本都是英语.

    shell

    通配符

    通配符是 shell 内建的符号, 常用于操作多个相似的文件.

    通配符说明
    *匹配任何字符
    ?匹配1个字符
    [xyz]匹配 xyz 中任意一个
    [a-z]匹配范围
    [!xyz][^xyz]不匹配

    clear

    清空屏幕, 本质是屏幕翻页, 可通过向上滚动查看到之前的命令.
    
    clear
    
    可以使用 Ctrl+l(小写的L) 来清空屏幕.

    echo

    打印到标准输出
    
    echo [选项] 打印内容
    
    选项
        -n        # 不追加换行(默认会)
        -e        # 支持转义字符的解释
        -E        # 不支持转义字符的解释
        
    转义字符 -e
          \a        alert (bell)
          \b        backspace
          \c        suppress further output
          \e        escape character
          \f        form feed
          \n        new line
          \r        carriage return
          \t        horizontal tab
          \v        vertical tab
          \\        backslash
          \0nnn     the character whose ASCII code is NNN (octal).  NNN can be
            0 to 3 octal digits
          \xHH      the eight-bit character whose value is HH (hexadecimal).  HH
            can be one or two hex digits

    文件/目录相关

    ls 命令

    查看目录情况
    
    ls [选项] <文件名或路径>...
    
    选项
    
        显示范围
        -a, --all        显示所有, 包括隐藏的    
        -A, --almost-all 近乎所有, 不显示 . 和 ..
        -R, --recursive    递归显示子目录
        -d                将目录名像文件一样显示, 而不是显示目录里的内容. 
                        特别适用于要直接查看某个目录的信息而不是目录内文件信息时.
        -i                显示文件的i节点编号
        
        显示格式
        -l, --format=long, --format=verbose
        -h, --human-readable    自动转换文件大小单位, 默认单位是字节.
    
        排序相关(默认按文件名排序)
        -r, --reverse    逆序显示
        -t, --sort=time    按最近修改时间排序
        -S, --sort=size    按文件大小排序

    输出解释

    [root@localhost tmp]# ll -ih --time-style=long-iso /var/
    总用量 12K
    34889261 drwxr-xr-x.          2               root root       19 2020-02-15 18:24 account
    33844659 drwxr-xr-x.          2               root root        6 2018-04-11 12:59 adm
    50402559 drwxr-xr-x.         13               root root      159 2020-02-15 18:42 cache
    50827832 drwxr-xr-x.          2               root root        6 2018-11-05 01:10 crash
      370319 drwxr-xr-x.          3               root root       34 2020-02-23 17:05 db
    16862160 drwxr-xr-x.          3               root root       18 2020-02-15 18:24 empty
    33844660 drwxr-xr-x.          2               root root        6 2018-04-11 12:59 games
    50402560 drwxr-xr-x.          2               root root        6 2018-04-11 12:59 gopher
    50409426 drwxr-xr-x.          3               root root       18 2020-02-15 18:17 kerberos
    33574985 drwxr-xr-x.         61               root root     4.0K 2020-02-23 22:56 lib
    50402561 drwxr-xr-x.          2               root root        6 2018-04-11 12:59 local
    50331726 lrwxrwxrwx.          1               root root       11 2020-02-15 18:16 lock -> ../run/lock
    16861864 drwxr-xr-x.         20               root root     4.0K 2020-02-23 23:11 log
    50402562 lrwxrwxrwx.          1               root root       10 2020-02-15 18:16 mail -> spool/mail
      370321 drwxr-xr-x.          2               root root        6 2018-04-11 12:59 nis
    16862162 drwxr-xr-x.          2               root root        6 2018-04-11 12:59 opt
    33844662 drwxr-xr-x.          2               root root        6 2018-04-11 12:59 preserve
    50331725 lrwxrwxrwx.          1               root root        6 2020-02-15 18:16 run -> ../run
    50402563 drwxr-xr-x.         12               root root      140 2020-02-15 18:24 spool
    17300610 drwxr-xr-x.          4               root root       28 2020-02-15 18:19 target
          69 drwxrwxrwt.         15               root root     4.0K 2020-02-23 23:12 tmp
    33844663 drwxr-xr-x.          2               root root        6 2018-04-11 12:59 yp
       ↑         ↑                ↑                   ↑           ↑
    i节点编号     权限     与该i节点链接的文件名数量   归属主和归属组    大小

    stat 命令

    打印inode信息
    
    stat <filename>

    mkdir 命令

    创建目录
    
    mkdir [选项] <目录名>...
    
    选项
        -p, --parents    递归创建目录. 忽略已存在的目录时的报错信息.

    cp 命令

    复制文件/目录
    
    cp [选项] <src> <dest>
    
    选项
        提示
        -f    覆盖目的文件而不提示    
        -r    递归
        -v, --verbose    显示复制的文件名
        
        复制范围
        -p, --preserve    保留源文件的元数据(所有者, 组, 权限, 时间)
        -a, --arhchive    等同于 -dpR

    mv 命令

    移动(改名) 文件
    
    mv [选项] <src> <dest>
    
    选项
        -f, --force              # 覆盖前永不提示
        -i, --interactive         # 覆盖前提示
        
    

    注意:

    • mv 不能用于同名目录覆盖, 即 mv /patha/dirname /pathb/dirname, 此时会提示目录已存在, 无法移动

      解决办法参见:

      简单来说就是:

      cd ${SOURCE}; 
      find . -type d -exec mkdir -p ${DEST}/\{} \; 
      find . -type f -exec mv \{} ${DEST}/\{} \; 
      find . -type d -empty -delete

    文本查看

    cat

    head

    tail

    more

    less

    wc 命令

    统计文件内容信息
    
    wc [选项] [<文件>]
    
    选项
        -c, --bytes, --chars    字节数
        -l, --lines                换行符数(即行数)
        -w, --words                单词数

    file 命令

    查看文件的类型
    
    file <file>

    tee 命令

    从标准输入读入, 并同时写往标准输出和文件
    
    tee [选项] <FILE>...
    
    选项
        -a, --append        # 追加到 给出的 文件, 而不是 覆盖

    readlink 命令

    找出符号链接或规范文件名所指向的位置(绝对路径)
    
    readlink [选项] <filename>
    
    选项
        -f, --canonicalize        # 递归跟随给出文件名的所有符号链接以标准化,除最后一个外所有组件必须存在。
        
    示例
        readlink -f /bin/awk            # 输出 /bin/gawk
        cd /bin && readlink -f ./awk    # 输出 /bin/gawk            

    打包压缩和解压缩

    tar 命令

    打包: tar

    压缩: gzip 和 bzip2

    常用扩展名: .tar.gz, .tar.bz2, .tgz, .tbz2

    打包
    
    tar [选项] <dest> <src>...
    
    示例
        tar -czvf xx.tar.gz xx1 xx2 xx3...    打包并使用gzip压缩
        tar -tvf xx.tar 详细查看压缩包内的文件
        tar -xzvf xx.tar.gz 解压缩
    
    必须选项
        -c, --create    创建新存档
        -x, --extract, --get    从存档中取出文件. 目前大多数情况下 tar 会自行检测包的压缩格式, 因此在不指定压缩选项时会自动处理好.
        -t, --list        查看存档中的文件目录
        
        --delete        从存档中删除
        -r, --append    附加到存档结尾
        
    其他选项
        -f, --file <文件>    指定存档或设备
        
        压缩选项
        -a, --auto-compress                根据文件后缀来决定压缩程序    
        -z, --gzip, --gunzip, --ungzip    用gzip处理存档, 常用扩展名: .tar.gz 或 .tgz
        -j, --bzip2                        用bzip2处理存档, 常用扩展名: .tar.bz2 或 .tbz2
        -Z, --compress, --uncompress    用compress处理存档
        
        解压时选项
        -C, --directory=DIR        改变解压目录至 DIR
        --no-same-owner            将解压后的文件属主和属组保持与当前用户一致(普通用户默认使用该选项)
        --same-owner            将解压后的文件属主和属组保持与压缩时的uid、gid一致(root用户默认使用该选项)
        --strip-components N    去除N层目录结构(适用于压缩包中的目录结构前面很多空白层)
        
        

    压缩算法对比:

    bzip2 比 gzip 拥有更高的压缩比例, 但消耗更长的时间(多好几倍的时间)

    通常推荐用 gzip.

    unzip 命令

    查看, 提取 ZIP 压缩包
    
    unzip [选项] <压缩包.zip> [<file>...] [-d <dir>]
    
    参数
        <file>...    只提取指定文件, 支持通配符 *
    
    选项
        -l            查看压缩包内的文件列表(不解压)
        -v            查看压缩包内的详细文件列表, 包括压缩比率等(不解压)
        -j            忽略压缩文件中的原有目录, 直接将其中的文件解压出来
        -d <dir>    解压到指定目录(若未制定则默认是在当前目录)
    
    示例
        unzip test.zip a.txt b.txt        # 从 test.zip 中解压出 a.txt 和 b.txt 文件

    文本编辑器 vim

    VIM 有4种模型:

    • 正常模式 Normal-mode
    • 插入模式 Insert-mode
    • 命令模式 Command-mode
    • 可视模式 Visual-mode
    graph TB
        normal(正常模式) --i,o,O,a,A--> insert(插入模式)
        insert --ESC--> normal
        normal --:,/--> command(命令模式)
        command --ESC--> normal
        command --:q--> 退出

    VIM 个人推荐配置

    set tabstop=4    " 制表符占用宽度为 4
    set smartindent " 智能缩进

    VIM 部分配置

    "set nu        "显示行数
    syntax on    "启用语法高亮
    set tabstop=4        " 设置 tab 制表符所占宽度为 4
    set softtabstop=4    " 设置按 tab 时缩进的宽度为 4
    set shiftwidth=4    " 设置自动缩进宽度为 4
    set expandtab " 缩进时将 tab 制表符转换为空格
    set autoindent    "开启自动缩进
    set smartindent " 智能缩进
    set hlsearch     " 搜索结果高亮
    filetype on     " 开启文件类型检测
    syntax on         " 开启语法高亮
    
    "set relativenumber " 显示相对行号
    "set nonu     "关闭显示行数
    set fileencodings=utf-8,ucs-bom,gb18030,gbk,gb2312,cp936
    set encoding=utf-8
    /etc/vimrc

    ~/.vimrc

    正常模式

    # 模式切换
    i        进入插入模式 光标定位在当前字符.
    a        进入插入模式, 光标定位在下一个字符.
    
    I        进入插入模式, 光标定位在本行的开头
    A        进入插入模式, 光标定位在本行的末尾
    
    o        进入插入模式, 光标在下一行开头并产生一个空行.
    O        进入插入模式, 光标在上一行开头并产生一个空行.
    
    :        进入命令模式
    /        进入命令模式, 并查找
    
    v        进入字符可视模式
    V        行可视模式
    Ctrl+v    块可视模式
    
    # 历史操作
    u        undo, 撤销操作
    Ctrl+r    redo, 重做撤销
    
    
    # 光标移动(在方向键产生乱码时适合使用)
    h        ←
    j        ↓
    k        ↑
    l        →
    gg        移动到第一行
    G        移动到最后一行
    <num>G    移动到第num行
    ^        移动到行开头
    $        移动到行末尾
    e        移动到下一个单词末尾
    
    
    # 行操作
    yy            复制本行(包括换行符)
    y$            复制光标到本行末尾间的内容(不包括换行符)
    <num>yy        复制num行
    
    dd            剪切本行
    d$            剪切光标到行末尾间的内容
    <num>dd        剪切num行
    
    p            在当前光标处黏贴(插入)
    
    
    # 字符操作
    x            删除单个字符
    r            替换单个字符, 输入r后再输入替换后的字符即可.
    大写的是指 Shift+字符

    插入模式

    ESC        返回正常模式

    命令模式

    ESC                        返回正常模式
    :w    [</path/to/file>]    保存|另存为
    :q                        退出
    :q!                        退出(不保存)
    :!<linux命令>               切换执行linux命令
    
    # 查找
    /<待查找文本>                    全文查找, n定位下一个匹配, N定位上一个匹配
    /<待查找文本>\c                    忽略大小写
    /<待查找文本>\C                    大小写敏感
    # 查找支持正则, 但要注意转义, 例如:
    /\s                    匹配一个空格
    /\s\+                匹配一个或多个空格
    /\s\{2,}            匹配2个或以上空格
    
    
    # 替换 - 命令格式
    :[前缀]s/<src>/<dest>[/后缀]
    
    # 替换 - 前缀总结
    :<行号>s                        仅在第<行号>内替换
    :<开始行>,<结束行>s             仅在指定行范围内替换
    :%s                             全文替换(默认每行只替换一此, 除非配合 /g)
    :.,+<num>s                     当前行及下面的num行替换
    
    # 替换 - 后缀总结(可以组合使用)
    /g                            global, 替换所有匹配项
    /c                            confirm, 逐个确认替换
    /i                            忽略大小写
    
    
    # vim配置
    ## 行号
    set nu                显示行号
    set nonu            不显示行号(默认)
    set relativenumber  显示相对行号
    
    set hlsearch        高亮显示查找匹配内容(默认)
    set nohlsearch        不高亮显示查找匹配内容
    
    ## 大小写
    set ignorecase        大小写不敏感
    set smartcase        大小写敏感(默认)
    
    ## 空格与制表符
    set expandtab        缩进时将 tab 制表符转换为空格
    set autoindent        开启自动缩进
    set smartindent     智能缩进
    set tabstop=4        设置 tab 制表符所占宽度为 4
    set softtabstop=4    设置按 tab 时缩进的宽度为 4
    set shiftwidth=4    设置自动缩进宽度为 4
    
    ## 编码
    set fileencodings=utf-8,ucs-bom,gb18030,gbk,gb2312,cp936    逐个尝试文件编码
    set encoding=utf-8    文件编码
    
    filetype on         开启文件类型检测
    syntax on             开启语法高亮

    vim 的配置文件:

    • /etc/vimrc 针对所有人
    • ~/.vimrc 针对个人

    可视模式

    可视模式分三种

    v        进入字符可视模式
    V        行可视模式
    Ctrl+v    块可视模式
    ESC        返回正常模式
    
    # 可视操作
    ## 以下的 '<,'> 是自动输入的
    :'<,'>/            在选定的行查找
    :'<,'>s            在选定的行替换, 其实若知道行号也可以用 :<line1>,<line2>s 来等价处理.
    
    # 块操作 - 需用 Ctrl+v 进入块模式, 并选定作用范围后
    d                将选择的块删除
    I(大写的i)          将文本插入光标所在范围列, 输入要插入的内容后再按 ESC+ESC 将插入的内容应用到块.

    用户和用户组

    用户分为:

    • root 用户
    • 普通用户(受限用户)

    新建用户实际做的操作

    1. /etc/passwd 添加该用户信息, 每个用户都会分配一个新的独立的uid
    2. /etc/shadow 添加该用户密码相关配置
    3. 若未指定用户所属的组, 则会在 /etc/group 创建和用户名同名的组(group), 并分配给该用户
    4. 创建用户的home目录及其配置文件, 位于 /home/<用户名>

    重要配置文件

    相关重要配置文件

    • /etc/passwd
    • /etc/passwd
    • /etc/group

    /etc/passwd

    用户配置信息.

    如果要新建用户, 甚至只需要往这里添加新用户的信息就可以了(当然home目录需要自己手动创建)

    root:x:0:0:root:/root:/bin/bash
    ...
    ntp:x:38:38::/etc/ntp:/sbin/nologin
    ...
    youjiaxing:x:1000:1000:youjiaxing:/home/youjiaxing:/bin/bash
    user1:x:1001:1001::/home/user1:/bin/bash

    上述每行字段(用:分隔)格式如下

    字段1            字段2            字段3           字段4         字段5   字段6           字段7
    用户名        是否需要密码验证   用户id(uid)   组id(gid)    注释   home目录  用户登录成功的命令解释器
              x: 需要密码验证                                                    /bin/bash 目前通用的解释器
              空: 不需要密码                                                    /sbin/nologin 禁止登陆

    /etc/shadow

    root:.........:18307:0:99999:7:::
    ...
    youjiaxing:..............::0:99999:7:::

    上述每行字段(用:分隔)格式如下:

    字段1         字段2    
    用户名      加密后的密码    

    /etc/group

    组的配置信息

    root:x:0:
    ...
    youjiaxing:x:1000:youjiaxing

    上述每行字段(用:分隔)格式如下:

    字段1         字段2            字段3            字段4
    组名      是否需要密码验证      组id(gid)    其他组设置
            x: 需要

    命令

    id 命令

    显示真实和有效的UID和GID
    
    id [选项] <用户名=当前用户>

    useradd 命令

    新建用户
    
    useradd [选项] <用户名>
    
    选项
        -g, --gid <组名>               # 设置用户初始的主组名(必须是已存在的组名)
        -d, --home-dir <HOME_DIR>    # 指定home目录, 默认是在 /home/<用户名>
        -s, --shell <SHELL>            # 指定用户的登录 shell 名。默认为留空,让系统根据 /etc/default/useradd 中的 SHELL 变量选择默认的登录 shell,默认为空字符串。
                                    # 例如不允许登录的用户, 可以这样: "-s /sbin/nologin"
        -c, --commentCOMMENT        # 任何字符串。通常是关于登录的简短描述,当前用于用户全名
        -r, --system                # 创建一个系统账户(不会自动创建home目录)

    userdel 命令

    删除用户
    
    userdel [选项] <用户名>
    
    选项
        -r, --remove    同时移除对应home目录(默认不会移除)

    passwd 命令

    修改用户密码
    
    passwd [选项] <用户名=当前用户>
    
    选项
        --stdin        # 指定从标准输入获取新密码
        
        
    示例
        echo 123456 | passwd user1        # 将用户 user1 密码设置为 123456

    usermod 命令

    修改用户属性
    
    usermod [选项] <用户名>
    
    选项
        -d, --home <NEW_HOME_DIR>    修改用户的home目录
        -m, --move-home                需和 -d 配合使用, 表示将旧的home目录移动到新的home目录.    
        
        -g, --gid <组名>              设置用户初始的主组名(必须是已存在的组名)
    
        -a, --append                将用户添加到附加组。只能和 -G 选项一起使用                            
        -G, --groups <group1>[,<group2>...]        设置用户的附加组(默认行为是替换), 若同时使用 -a 则是追加

    chage 命令

    更改用户过期密码信息
    
    chage [选项] <用户名>

    groupadd 命令

    新建用户组
    
    groupadd [选项] <组名>
    
    选项
        -r, --system          # 创建一个系统组

    groupdel 命令

    删除用户组
    
    groupdel [选项] <组名>

    su 命令

    切换用户
    
    su [-] <用户>
    
    示例
        su - <用户>        # 使用 login shell, 同时会切换到
        su <用户>            # 使用 non-login shell
    
    注意
        使用 exit 可以退出当前用户.
        root用户切换到其他用户是不需要输入密码.
    
    选项
        -        切换用户的同时切换环境(指的是工作路径, 切换到用户的home目录)

    sudo 命令

    以其他用户身份(默认是管理员)执行一条命令
    
    sudo [选项]
    
    选项
        -u, --user=<user>        以指定的用户或id执行命令
        -H                         将HOME环境变量设为新身份的HOME环境变量

    visudo 命令

    # 设置需要使用sudo的用户(组)
    
    visudo
    
    # 示例: 允许 user3 用户在任意终端上执行取消关机命令
    user3 ALL=/usr/sbin/shutdown -c
    
    # 示例: 允许 user4 用户在任意终端执行任意命令
    user4 All=(ALL) ALL
    
    # 示例: 允许 user5 用户在任意终端执行任意命令(无需输入密码)
    user5 All=(ALL) NOPASSWD: ALL

    相关配置项说明:

    • localhost: 指的是字符终端, 并非ssh和图形终端.
    • ALL: 所有终端

    who 命令

    查看当前主机上有哪些登录的用户, 对应终端, 及来源.

    who

    whoami 命令

    查看当前用户
    
    whoami

    文件与目录权限

    !!! root用户不受任何权限限制, 权限控制是针对普通用户.

    owner 和 group 优先级

    • 如果用户是该文件属主且同组, 则权限判定时只会看 owner 部分, 忽略 group 部分.

      如果 owner 权限拒绝, 但 group 权限允许, 一样是拒绝.

    权限表示

    -rw-------             1             username groupname   mtime  filename
    类型和权限    指向该i节点的文件名数量   用户      组名     修改时间   文件名
    
    
    类型和权限:
         -    rw-    ---    ---
        类型           权限
              owner group other
              
    有时候权限的后面会多出一个 "+" , 它表示有额外的 facl 权限

    文件类型

    -    普通文件
    d    目录
    b    块特殊文件(也叫做"块设备", eg. 插入的移动硬盘, 光驱)
    c    字符特殊文件(也叫做"字符设备", eg. 终端. 在 /dev 目录中很多)
    l    符号链接(软链接)
    f    命名管道(通信功能)
    s    套接字文件(通信功能)
    不问文件类型的权限的意义不一样! 这点需要注意

    块设备无法直接用类似 cat 之类的命令直接操作, 需要先挂载

    权限表示

    # 普通文件权限表示
    r    4    读
    w    2    写
    x    1    执行
    
    
    # 目录权限表示
    r        可查看目录内的文件名
    x        进入目录(目录的访问权)
    rx        可进入目录并查看目录文件名
    wx        修改目录内的文件名(包括删除文件)
    创建新文件有默认权限, 根据umask值计算, 属主和属组根据当前进程的用户来设定.

    特殊权限

    SUID

    念作 SET UID

    用于二进制可执行文件, 执行命令时取得文件属主(owner)权限

    设置 SUID
    
    # 符号方式
    chmod u+s <file>
    
    # 数字方式, 假设文件原先权限是 755, 现在需要在此基础上设置 SUID
    chmod 4755 <file>
    
    # user 带执行权限
    --s
    
    # user 不带执行权限(大写的s)
    --S

    示例

    如 /usr/bin/passwd(注意权限中 user 的 x 位置): s
    -rwsr-xr-x root root /usr/bin/passwd
    
    
    用户修改密码时需要修改文件 /etc/passwd, 而该文件状态是:
    -rw-r--r-- root root /etc/pass
    其他用户无权写, 因此需要 SUID

    SGID

    念作 SET GID

    用于目录, 在该目录下创建新的文件或目录, 权限自动更改为该目录的属组(group).

    常用于文件共享.

    在权限中 group 的 x 位置: s

    设置 SGID
    
    # 符号方式
    chmod g+s <dir>
    
    # 数字方式, 假设目录原先权限是 755, 现在需要在此基础上设置 SGID
    chmod 2755 <dir>
    
    # group 带执行权限
    --s
    
    # group 不带执行权限(大写的s)
    --S

    SBIT

    一般称作Stick位

    用于目录, 该目录下新建的文件和目录, 仅root和自己可以删除

    常用于在一个公共目录中, 为了防止自己的文件(在其他人可写的情况下)被其他人删掉.

    设置 SBIT
    
    # 符号方式
    chmod o+t <dir>
    
    # 数字方式, 假设目录原先权限是 755, 现在需要在此基础上设置 SBIT
    chmod 1755 <dir>
    
    # other 带执行权限
    --t
    
    # other 不带执行权限(大写的t)
    --T

    示例

    如 /tmp/ 目录(注意权限中 other 的 x 位置): t
    
    drwxrwxrwt root root /tmp

    命令

    chmod 命令

    修改文件、目录权限
    
    chmod [选项] <mode> <文件>
    
    chmod忽略符号链接文件.
    
    mode:
        [ugoa..][+-=][rwxXstugo...]    可以使用符号或数字(8进制)来更改权限, eg. chmod 755 <file>
            u    文件所有者
            g    文件所在组的用户
            o    其他用户
            a    所有用户(默认), 等同于 ugo
            
            +    追加权限(在原来基础上)
            -    撤销权限
            =    设置权限(只具有这些权限)
            
            r    读权限
            w    写权限
            x    执行权限(或对目录的访问权)
            X
            s
            t
            u
            g
            o
    
    
    
    选项:
        -R        递归改变子目录及文件

    chown

    更改属主、属组
    
    chown [选项] <user>:<group> <file>...
    
    <user> 和 <group> 可忽略其中任意一个, eg. chown :<group> <file>... 表示仅更改属组

    chgrp

    单独修改属组
    
    chgrp
    
    不常用

    umask

    显示或设置用户文件创建掩码
    
    umask [选项] [<mode>]

    创建新文件的默认权限是 666, 而不同用户的umask是不同的.

    比如 root 用户的默认umask 是 0022, 那么它创建的文件的初始权限就是: 666 - 0022 = 644, 也就是 rw-r--r--

    查看原文

    赞 0 收藏 0 评论 0

    嘉兴ing 发布了文章 · 1月18日

    ⭐《MySQL 实战45讲》笔记

    [TOC]

    一. 索引与优化

    本篇内容主要来自极客时间《MySQL实战45讲》中的:

    • 04 - 深入浅出索引(上)
    • 05 - 深入浅出索引(下)

    基本数据存储模型

    • 有序数组
    • 哈希表
    • 搜索树
    • 跳表

      Redis 的有序集合使用的就是这个结构
    • LSM树 等

    有序数组:

    优点: 查找很快, 支持范围查询

    缺点: 插入代价高, 必须逻辑上移动后续的所有记录

    搜索树:

    • 二叉搜索树

      搜索效率最高, 但实际并不采用, 因为索引是存在磁盘的.

      假设一棵树高为20, 一次搜索就需要20个数据块, 对应磁盘就是20次随机查找. 对于普通硬盘来说, 一次寻址约 10ms, 则一次搜索就需要 20x10ms = 200ms.如果要让一个查询尽量少读磁盘, 那就必须尽量少地查询数据块, 应该使用下面的多叉树.

    • 多叉树

      为了减少磁盘访问次数, 可以使用 "N叉"树, 这里的 N 取决于数据块的大小.

      以 InnoDB 中 一个整数字段为例, 这个N差不多是1200.

      计算方法: 补充!!

      如果树高为4, 则可以存储 1200^3 个值, 考虑树根数据块基本都在内存中, 因此一次搜索只需要3次磁盘查找, 考虑到第2层数据块也有很大概率在内存中, 那么访问磁盘次数就更少了.

    引申: InnoDB 里N叉树中的N如何调整

    N = 页page的大小 / 每个索引项大小

    N叉树中非叶子节点存放的是索引信息, 每个索引项包含Key和Point指针及其他辅助数据, 其中Point指针固定大小6字节, 默认索引页的大小是16KB. 因此主键为int时, int占用4个字节, 加上辅助数据差不多每个索引项占用13字节, 因此非叶子节点大约可以存储 16k/13 ≈ 1260 个左右.

    N的大小大致是根据上述式子决定的, 因此若要调整N, 则有2个防线:

    • MySQL 5.6以后可以修改 page 大小, 参数: innodb_page_size

      未测试
    • 通过修改Key字段类型, 比如 int 占用4字节, bitint 占用8字节.

    哈希表

    优点: 新增和查找都很快

    缺点: 无法进行范围遍历, 必须一个一个查找.

    InnoDB 索引的存储结构

    索引组织表: 按照主键顺序, 以索引形式存放的表.

    InnoDB 使用了 B+树 作为索引的存储结构.

    InnoDB 中的索引, 按照叶子节点内容来区分, 分为两类:

    1. 主键索引(聚簇索引, clustered index)
    2. 非主键索引(二级索引, secondary index)

    InnoDB 中 B+ 树的叶子节点存放的是 , 一页里面可以存多个行.

    这里的页指的是 InnoDB 的页, 而非磁盘页, 默认大小是 16KB.

    索引的维护涉及 插入删除, 这两个操作可能导致 页分裂页合并 的问题.

    • 插入: 如果插入不是有序递增的, 就需要逻辑上移动插入点后面的数据. 更糟糕的是, 如果插入点所在的数据块已满, 根据B+树的算法, 此时需要进行 页分裂 操作(新申请一个页, 将部分数据挪动过去). 页分裂 操作除了影响性能外, 还会影响页的利用率, 降低了约 50% 的利用率.
    • 删除: 当两个相邻页由于删除元素导致利用率很低后, 会将数据页做合并, 合并的过程可以理解为页分裂的逆过程.

    索引可能因为删除或页分裂的原因导致数据页有空洞, 而重建索引的过程会创建一个新的索引, 并将数据顺序插入, 使得索引更紧凑, 空间利用率更高.


    Q. 为什么表删除了一半数据, 文件大小却没变?

    A. 简单回答一下.

    删除时仅仅是将数据从所在的数据页上标记删除, 遗留的空位还会保留着, 供后续插入新记录时直接存放.

    这种情况可以考虑重建索引以减少磁盘空间占用

    optimize table 表名;
    -- 或
    alter table 表名 engine=InnoDB;

    注意 alter table 表名 = engine=InnoDB; 会加 MDL 读锁.

    如果是 MySQL 5.7, 则会使用 OnlineDDL, 避免长时间的 MDL 锁导致业务不可用.


    Q. 主键索引和非主键索引的区别

    A. 主要区别在于:

    • 主键索引(叶子节点)存储的是行记录, 非主键索引(叶子节点)存储的是对应主键的内容.
    • 非主键索引查询时, 需要先在该索引上查找到对应主键, 再去主键索引查找, 这个过程叫做回表.因此在应用中应尽量使用主键索引, 避免多一次回表

    Q. 非主键索引中字段值相同的索引项是如何存储的?

    A. 结论: 独立存储.

    以索引c为例, id是主键, 假设有两个记录 (c=10, id=1), (c=10, id=2), 这其实在索引c上是两条不同的索引项, 它的存放顺序是先按照c递增, c等值情况下再按照id递增, 因此可以理解为索引c 是 (c, id)


    Q. 若不给表设置主键会怎样?

    A. InnoDB 会为每一行隐式分配一个 RowId 作为主键. 所以其实还是有主键索引的


    Q. 联合索引的存储结构是怎样的?

    A. 《高性能MySQL 第三版》P144,关于索引类型的插图,说明了联合索引是N个字段组合成一个索引的。


    Q. 在联合索引中多个字段顺序是怎样的?

    A. 以 (a,b) 为例, id 是主键. 则在该索引上, 是先按照 a 递增, 再根据 b 递增, 最后根据 id 递增的顺序排序.

    可以和下面写到的 最左前缀 一起理解.


    Q. 如果表用到了联合主键, 那么在二级索引中是如何存储的?

    A. 假设联合主键是 (a,b), 此时表中还有个字段 c, 可以分3种情况考虑:

    1. 如果建立了索引 (c), 则先按照 c递增, 其次 a 递增, 最后是 b 递增.
    2. 如果建立了索引 (c,a), 那么顺序同1, 这种情况下是没必要单独创建 (c,a), 而只需要索引(c)即可
    3. 如果建立了索引 (c,b), 那么会先按照 c递增, 然后是 b 递增, 最后是 a 递增.

    索引的选择

    主键的选择

    主键尽量使用自增主键, 原因:

    • 自增主键是有序递增的, 往索引插入时都是追加操作, 避免了页分裂的问题, 而业务上的主键一般不满足有序递增.
    • 自增主键通常是 int not null primary key auto_incrementbigint not null primary key auto_increment, 使用整形做主键只需要4个字节, 使用长整型则是8个字节.
    • 主键的字段越小, 普通索引的叶子节点也就越小, 占用的空间就越小.

    因此从性能存储空间看, 自增主键通常是最好的选择.


    那么什么时候可以考虑用业务字段作为主键:

    1. 没有其他二级索引(无需考虑二级索引叶子节点大小)
    2. 业务字段唯一

    ↑ 这就是典型的 KV 场景了, 考虑到查询时尽量用主键索引, 避免回表, 此时就可以将这个索引设置为主键.

    覆盖索引

    当查询语句中涉及的所有字段都在同一个索引中, 此时由于只需要在该索引树上查找而不需要回表, 这成为覆盖索引.

    覆盖索引可以减少树的搜索次数, 显著提升性能, 因此是常用的优化手段.

    注意: 索引的维护是有代价的, 因此是否新增冗余索引来支持覆盖索引时需要权衡考量.

    以索引 (code, name) 为例, 当使用如下语句时是可以用到覆盖索引, 避免回表的:

    select name from 表 where code = "xxx";
    
    -- 或
    
    select id from 表 where code = "xxx";

    Q. 是否有必要为了覆盖索引而设立联合索引?

    A. 分情况:

    • 如果是高频请求, 那么可以建立联合索引来使用覆盖索引优化, 避免回表
    • 如果是低频请求, 若已有现成的可利用最左前缀优化的索引, 或单独索引, 则没必要. 此时索引带来的优化好处可能已经被维护索引的代价盖掉了.

    最左前缀

    最左前缀指的是联合索引的前几个字段, 以及字符串索引的前几个字符.

    由于索引是以B+树结构存储的, 而B+树这种索引结构是可以利用索引的最左前缀来定位记录的.

    以 (name, age) 这个联合索引为例, 它的大致示意图如下:

    可以看出索引项的顺序是按照索引定义的字段顺序来排序的.

    以下语句会用到上面的这个索引的最左前缀:

    -- 联合索引上的最左N个字段
    select * from 表 where name = "xx";
    
    -- 字符串的最左N个字符
    select * from 表 where name like '张%';

    Q. 联合索引上的字段顺序如何确定?

    A. 优先考虑复用能力, 其次考虑存储空间.

    原则1: 如果通过调整顺序可以少创建一个索引, 那么通常就会优先考虑调整后的这个顺序了.

    原则2: 优先考虑原则1, 其次应考虑空间占用.

    以联合索引 (a,b) 为例, 由于最左前缀优化的原因, 在该表上就不需要单独再建立索引 (a) 了, 因此这种情况只需要建立一个联合索引 (a,b) 即可.

    但是, 如果此时同样需要用到索引 (b), 那么这时候有两个选择:

    1. 创建 (a,b) 及 (b)
    2. 创建 (b,a) 及 (a)

    此时若字段a比较大, 则应考虑方案1, 否则应考虑方案2.


    索引下推 index condition pushdown

    对于联合索引, 对于不满足最左前缀的部分, 在某些情况下是可以用到 索引下推 的.

    索引下推: 在索引遍历过程中, 利用索引中已有的字段过滤不满足条件的记录, 避免每次判断都回表.

    先明确:

    • 索引下推 是在 MySQL 5.6 引入的.
    • 在 explain 的时候可以在 Extra 看到 Using index condition , 说明可以用到索引下推

      "可以"用, 但不一定用/没有.

      这个地方还不大明确

    以索引 (name, age) 为例, 查看一下SQL语句:

    select * from 表 where name like '张%' and age > 20;

    此时会先利用索引, 快速找到 name以"张"开头的记录, 然后依次向右遍历:

    • 若是在 MySQL 5.6 以前, 则需要一个一个回表并筛选 age > 20 的记录
    • 若是在 MySQL 5.6 及以后, 则根据 索引下推 则会在索引遍历过程中对索引包含的字段先做判断, 过滤不满足条件的记录, 减少回表次数.

    Change Buffer 之普通索引和唯一索引的选择

    前提: 业务能保证记录是唯一的情况下, 才需要考虑.

    理解这部分内容的意义:

    在遇到大量插入数据慢, 内存命中率低的情况下, 多一个排查思路.

    相关配置:

    ## 最大占用 innodb_buffer_poll 内存空间的百分比
    innodb_change_buffer_max_size=50

    Change Buffer

    • 只会针对普通索引 (肯定是二级索引了)
    • 能够在不影响数据一致性前提下将数据更新操作(DML, insert/update/delete)缓存在 Change Buffer 中, 而无需立即读取(磁盘)数据页. 当下次需要访问这个数据页的时候, 会将该数据页读取到内存中, 再将这些缓存的操作应用上去.
    • 记录的操作存储在 Change Buffer 中, 它占用的是InnoDB Buffer Pool, 同时它是可持久化的.
    • Change Buffer 减少的是随机读的次数(无需每次更新都读取), 若在读取记录前保存在该Buffer中操作越多, 则受益更大. 因此它同时也提高了内存利用效率(因此读取数据页是会占用内存空间的)
    • 从磁盘读取索引数据页并将Change Buffer缓存的操作应用上去, 这个过程称为 Merge
    • Merge 发生的情况:

      1. 读取记录时应用Change Buffer
      2. 后台线程定期Merge
      3. 正常关闭(shutdown)数据库
    想象一下, 一张表有4,5个普通二级索引, 这些索引的使用率并不高.

    同时该表会频繁更新数据, 若没有Change Buffer, 那么每次更新操作维护二级索引时都需要从磁盘读入索引对应的数据页, 而有了Change Buffer后只需将这些操作保存在该Buffer中, 极大减少了磁盘随机读次数, 最后统一Merge即可.


    查询过程的区别:

    • 普通索引

      从索引树根目录, 逐层查找对应记录所在数据页.

      若不在内存中, 则需要先从磁盘上读入内存.

      若数据所在页已经在内存中, 则读取该记录, 并向右遍历直到不符合条件. 由于数据的读取是以数据页为单位(默认16KB), 因此这个过程是在内存中, 对性能影响极小, 除非是记录刚好在数据页的最后一条.(考虑到概率, 可以忽略)

    • 唯一索引

      类似普通索引, 只是在找到对应一条记录后就停止了.

    结论: 在查询过程中性能区别不大.


    更新过程的区别:

    若数据都在内存中则没有什么区别, 因此以下只讨论不在内存中的情况.

    • 普通索引

      将更新语句记录在 Change Buffer 中, 更新结束.

    • 唯一索引

      由于更新操作需要判断是否违反数据一致性约束, 因此无法使用 Change Buffer, 需要先将数据页从磁盘读取到内存, 进行判断, 再做更新操作.


    Q. Change Buffer 什么时候会成为负优化?

    A. 在下述普通索引场景:

    当每次更新操作后马上读取, 由于更新操作会缓存在Change Buffer中, 下一次马上读取时需要立即 Merge.

    此时反而多了维护 Change Buffer的代价, 同时随机访问IO不会减少.


    Q. Change Buffer 适合什么场景?

    A. 写多读少业务

    Change Buffer 会将更新的操作缓存起来, 缓存得越多, 则在 Merge 操作的时候收益就越大.

    常见业务模型: 账单类, 日志类系统.

    联合索引的字段顺序 - 根据区分度

    当需要创建联合索引的情况下, 在 不考虑索引复用 前提, 且 字段顺序不影响索引完整使用 前提下, 如何确定联合索引中的字段顺序?

    !!! 注意这里的前提:

    1. 不考虑索引复用
    2. 字段顺序不影响索引完整使用

      如果是 where a = xx order by b 这类语句, 那么直接就是联合索引 (a, b) 了.

    此时应该按照字段的区分度, 区分度高的在前.

    以索引 (status, product_id) 为例, 分别查看其区分度:

    SELECT
        COUNT(DISTINCT status)/COUNT(*) as status_disc,
        COUNT(DISTINCT product_id)/COUNT(*) as product_id_disc
    FROM
        表名;

    当前这个例子很清楚, status 就几种取值, 基数很小, 区分度很差, 因此应该建立联合索引 (product_id, status)

    如何创建字符串索引

    一般有以下几种选择:

    1. 完整索引

      最耗费空间

    2. 前缀索引, 只选择前N个字符

      适用: 前N个字符具有足够区分度的情况.

      缺点: 增加额外扫描行数, 同时无法使用覆盖索引.

    3. 字符串倒序 + 前缀索引

      适用: 字符串前N个字符区分度不够的情况下, 且后N个字符有足够区分度

      存储: 存储的时候直接存储倒序的字符串

      使用: update 表 set s = reverse("123456");

      缺点: 除了前缀索引的缺点外, 每次更新/查找都需要额外的 reverse 函数调用消耗, 同时无法利用索引进行范围查找.

    4. 额外字段存储hash值

      存储: 新增额外字段存储字符串对应的hash值, 若使用 crc32 函数, 则额外占用4个字节

      优点: 查找性能稳定, 基本在 O(1)

      使用: 由于hash值会冲突, 因此查找时除了hash字段判断外, 还要判断原始字符串是否一致. select * from 表 where s_hash = crc32("123456") and s = "123456";

      缺点: 占用额外的存储空间, 无法利用索引进行范围查找

    索引创建命令

    CREATE TABLE 时创建

    CREATE TABLE IF NOT EXISTS `users` (
        -- 省略字段定义
        
        PRIMARY KEY (`id`),
        UNIQUE KEY `users_phone` (`phone`),
        KEY `users_name` (`name`),
    ) Engine=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

    ALTER TABLE用来创建普通索引、UNIQUE索引或PRIMARY KEY索引。

    ALTER TABLE table_name ADD INDEX index_name (column_list)
    -- 可忽略索引名
    -- ALTER TABLE table_name ADD INDEX (column_list)
    
    ALTER TABLE table_name ADD UNIQUE (column_list)
    
    ALTER TABLE table_name ADD PRIMARY KEY (column_list)
    
    -- 一个语句建多个索引
    ALTER TABLE HeadOfState ADD PRIMARY KEY (ID), ADD INDEX (LastName,FirstName);

    其中table_name是要增加索引的表名,column_list指出对哪些列进行索引,多列时各列之间用逗号分隔。索引名index_name可选,缺省时,MySQL将根据第一个索引列赋一个名称。另外,ALTER TABLE允许在单个语句中更改多个表,因此可以在同时创建多个索引。

    CREATE INDEX可对表增加普通索引或UNIQUE索引。

    CREATE INDEX index_name ON table_name (column_list)
    
    CREATE UNIQUE INDEX index_name ON table_name (column_list)

    索引失效情况

    索引失效的情况个人认为主要是以下情况:

    1. 区分度太低, 导致优化器认为全表扫描会更快.
    2. 对索引字段使用函数、进行计算、类型转换

      WHERE a + 1 = 2 这种语句也会导致索引 a 失效, 此时应该改写 SQL 语句为: WHERE a = 1
    3. 包括显式转换及隐式转换

      如果字段 phone 是 char 类型, 那么 WHERE phone = 18612345678 同样可能会导致索引失效, 应该改写成 WHERE phone = '18612345678 '
    4. 不满足最左前缀

      包括联合索引和字符串最左前缀

    5. 索引列存在NULL且查询条件是 is not null, 若索引没有覆盖查询字段和查询条件时, 此时会符合以下的<u>情况6</u>, 导致全表扫描.

      以下是个人测试

      -- UserName 是 varchar, nullable
      
      explain select Uid from new_light_user where UserName is  null;
      -- SIMPLE    new_light_user    ref    UserName    UserName    768    const    10    Using where; Using index
      
      explain select * from new_light_user where UserName is not null;
      -- SIMPLE    new_light_user    ALL    UserName    null         null  null   17979    Using where
    6. 根据查询条件, 无法使用索引快速定位, 但可以使用索引扫描时, 若innodb认为代价太大也会直接走全表扫描.

    其他注意点

    索引设计规范

    • 单表索引建议控制在5个以内
    • 但索引字段不允许超过5个
    • 索引字段要定义为 NOT NULL, 否则:

      1. 占用额外存储空间(1字节)
      2. 导致索引的使用更加复杂, 在某些情况下会导致索引失效
      3. 条件判断更麻烦, 需要 IS NULL, IS NOT NULL
    • 区分度不高的字段不建议建立索引

      除非查询值的筛选力度很高, 比如 status = 0 (表示未完成), 因为大多数值是 1, 因此这种情况下建索引还是有意义的.

    • 建立联合索引时, 优先将区分度高的字段放前面.

    二. 加锁规则及案例

    本文内容主要是 《MySQL实战45讲》 课程中第 20,21,30 课程的个人笔记及相关理解.

    主要是对于加锁规则的理解及分析.

    以下仅针对 MySQL 的 InnoDB 引擎.

    MyISM 引擎就只有表锁

    基本概念

    锁的种类

    MySQL 中的锁主要分为:

    • 全局锁

      flush tables with read lock;
    • 表级锁

      • 表锁

        lock table 表名 read;
        lock table 表名 write;
      • 元数据锁(Meta Data Lock, MDL)

        在 MySQL 5.5 引入 MDL 锁.

        MySQL 5.6 以后支持 OnlineDDL

    • 行锁
    还有个自增锁, 后续补充.

    意向锁在此先不做讨论.

    表级锁

    元数据锁 MDL

    MDL支持的版本:

    • 在 MySQL 5.5 引入 MDL 锁.
    • MySQL 5.6 以后支持 OnlineDDL.

    MDL锁目的: 控制对表元数据修改的并发.

    MDL锁类型分为:

    1. MDL 读锁(读锁之间不冲突)
    2. MDL 写锁(读写锁冲突, 写锁之间也冲突)

    普通的增删改查会自动获取MDL读锁, 而对表的字段修改或创建索引等修改表元数据的操作会自动获取MDL写锁, 在此期间增删改查就会被阻塞掉.

    OnlineDDL 是一种近似不锁表的特性, 它的过程如下:

    1. 获取MDL写锁

      这个期间会阻塞

    2. 降级为MDL读锁
    3. 执行DDL语句

      大部分时间消耗在这里, 比如重建表(alter table 表 Engine=Innodb)时, 需要将数据从旧表按主键顺序逐一添加到新表, 而大部分时间就消耗在这里.

      同时在此期间, 所有对数据库的增删改操作都会记录在特定日志中, 待这部分执行完毕后再应用这些日志, 从而保证数据一致性.

    4. 升级为MDL写锁

      这个期间会也阻塞

    5. 释放MDL写锁

    也就是说 OnlineDDL 其实还是会锁表, 但只会在开始跟结束的时候锁, 中间大部分时间是不锁的.

    对于 ALTER TABLE 表名 Engine=Innodb 这种DDL操作:

    • 5.6之前是在Server层面上通过创建临时表来实现的(锁表+创建临时表+拷贝数据+替换表)
    • 5.7及之后的OnlineDDL是在InnoDB层面上处理的, 它会创建临时文件.

    部分DDL操作不支持OnlineDDL, 比如添加全文索引(FULLTEXT)和空间索引(SPATIAL)

    InnoDB 中的锁

    行锁

    行锁也叫做记录锁, 这个锁是加在具体的索引项上的.

    行锁分为两种:

    • 读锁: 共享锁
    • 写锁: 排它锁

    行锁冲突情况:

    • 读锁与写锁冲突
    • 写锁与写锁冲突

    需要明确:

    • 锁的对象是索引

    间隙锁

    记录之间是存在间隙的, 这个间隙也是可以加上锁实体, 称为间隙锁.

    间隙锁存在的目的: 解决幻读问题.

    间隙锁冲突情况:

    • 间隙锁之间是不冲突的, 它们都是为了防止插入新的记录.
    • 间隙锁与插入操作(插入意向锁)产生冲突

    需要明确:

    • 间隙锁仅在 可重复读隔离级别下才存在.
    • 间隙锁的概念是动态的

      对间隙(a,b)加锁后, 存在间隙锁 (a,b).

      此时若 a 不存在(删除), 则间隙锁会向左延伸直到找到一条记录.

      若b不存在了(删除), 则间隙锁会向右延伸直到找到一条记录.

      假设主键上存在记录 id=5 和 id=10 和 id=15 的3条记录, 当存在某个间隙锁 (10,15) 时, 若我们将 id=10 这一行删掉, 则间隙锁 (10, 15) 会动态扩展成 (5, 15), 此时想要插入 id=7 的记录会被阻塞住.

      此处的删除指的是事务提交后, 否则间隙锁依旧是 (10,15)

    next-key lock

    next-key lock = 行锁 + 间隙锁

    next-key lock 的加锁顺序:

    1. 先加间隙锁
    2. 再加行锁
    如果加完间隙锁后, 再加行锁时被阻塞进入锁等待时, 间隙锁在此期间是不会释放的.

    两阶段锁协议

    两阶段锁协议指的是:

    1. 在用到的时候会加锁
    2. 在事务提交的时候才会释放锁

    了解这个协议的启发在于:

    • 在一个事务中需要对多个资源进行加锁时, 应尽量把最可能造成锁冲突的放在最后, 这边可以避免持有这个锁的时间过久导致线程长时间等待, 降低并发度.

    索引搜索

    索引搜索指的是就是:

    1. 在索引树上利用树搜索快速定位找到第一个值
    2. 然后向左或向右遍历

    order by desc 就是用最大的值来找第一个

    order by 就是用最小的值来找第一个

    等值查询

    等值查询指的是:

    • 在索引树上利用树搜索快速定位 xx=yy的过程

      where xx > yy 时, 也是先找到 xx = yy 这条记录, 这一个步骤是等值查询.但后续的向右遍历则属于范围查询.
    • 以及在找到具体记录后, 使用 xx=yy 向右遍历的过程.

    加锁规则

    该部分源自《MySQL实战45讲》中的 《21-为什么我只改了一行的语句, 锁这么多》

    以下仅针对 MySQL 的 InnoDB 引擎在 可重复读隔离级别, 具体MySQL版本:

    • 5.x 系列 <= 5.7.24
    • 8.0 系列 <=8.0.13

    以下测试若未指定, 则默认使用以下表, 相关案例为了避免污染原始数据, 因此在不影响测试结果前提下, 都放在事务中执行, 且最终不提交.

    create table c20(
        id int not null primary key, 
        c int default null, 
        d int default null, 
        key `c`(`c`)
    ) Engine=InnoDB;
    
    insert into c20 values(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25);
    
    /*
    +----+------+------+
    | id | c    | d    |
    +----+------+------+
    |  0 |    0 |    0 |
    |  5 |    5 |    5 |
    | 10 |   10 |   10 |
    | 15 |   15 |   15 |
    | 20 |   20 |   20 |
    | 25 |   25 |   25 |
    +----+------+------+
    */

    2个"原则", 2个"优化", 1个"BUG"

    1. 原则1: 加锁的基本单位是next-key lock, 前开后闭区间
    2. 原则2: 访问到的对象才会加锁

      select id from t where c = 15 lock in share mode;

      加读锁时, 覆盖索引优化情况下, 不会访问主键索引, 因此如果要通过 lock in share mode 给行加锁避免数据被修改, 那就需要绕过索引优化, 如 select 一个不在索引中的值.

      但如果改成 for update , 则 mysql 认为接下来会更新数据, 因此会将对应主键索引也一起锁了

    3. 优化1: 索引上的等值查询, 对唯一索引加锁时, next-key lock 会退化为行锁

      select * from t where id = 10 for update;

      引擎会在主键索引上查找到 id=10 这一行, 这一个操作是等值查询.

      锁范围是

    4. 优化2: 索引上的等值查询, 向右遍历时且最后一个值不满足等值条件时, next-key Lock 会退化为间隙锁

      select * from t where c = 10 for update;

      由于索引c是普通索引, 引擎在找到 c=10 这一条索引项后继续向右遍历到 c=15 这一条, 此时锁范围是 (5, 10], (10, 15)

    5. BUG 1: 唯一索引上的范围查询会访问到不满足条件的第一个值

      id> 10 and id <=15, 这时候会访问 id=15 以及下一个记录.
    对索引上的更新操作, 本质上是 删除+插入

    读提交与可重复读的加锁区别

    1. 读提交下没有间隙锁
    2. 读提交下有一个针对 update 语句的 "semi-consistent" read 优化.

      如果 update 语句碰到一个已经被锁了的行, 会读入最新的版本, 然后判断是不是满足查询条件, 若满足则进入锁等待, 若不满足则直接跳过.

      注意这个策略对 delete 是无效的.

    3. ?????? 语句执行过程中加上的行锁, 会在语句执行完成后将"不满足条件的行"上的行锁直接释放, 无需等到事务提交.

    insert into ... select ... 加锁

    https://time.geekbang.org/col...

    在可重复读隔离级别, binlog_format = statement 时, 该语句会对被 select 的那个表访问到的记录和间隙加锁

    小伙子, 很危险的.

    生产环境大表复制数据一般用 pt-archiver 工具来处理, 避免 insert ... select ... 锁导致的长阻塞.

    pt-archiver: 数据归档工具

    或者简单用 select ... into outfile 和 load data infile 组合来代替 insert ... select 完成插入操作.

    简单例子

    例子1

    begin;
    select * from c20 where id=5 for update;

    在主键索引 id 上快速查找到 id=5 这一行是等值查询

    例子2

    begin;
    select * from c20 where id > 9 and id < 12 for update;

    在主键索引 id 上找到首个大于 9 的值, 这个过程其实是在索引树上快速找到 id=9 这条记录(不存在), 找到了 (5,10) 这个间隙, 这个过程是等值查询.

    然后向右遍历, 在遍历过程中就不是等值查询了, 依次扫描到 id=10 , id=15 这两个记录, 其中 id=15 不符合条件, 因此最终锁范围是 (5,10], (10, 15]

    例子3

    begin;
    select * from c20 where id > 9 and id < 12 order by id desc for update;

    根据语义 order by id desc, 优化器必须先找到第一个 id < 12 的值, 在主键索引树上快速查找 id=12 的值(不存在), 此时是向右遍历到 id=15, 根据优化2, 仅加了间隙锁 (10,15) , 这个过程是等值查询.

    接着向左遍历, 遍历过程就不是等值查询了, 最终锁范围是: (0,5], (5, 10], (10, 15)

    个人理解:

    1. 由于有 order by id desc, 因此首先是等值查询 id=12 不存在, 向右遍历不满足, 优化, 因此加了间隙锁 (10, 15)
    2. 向左遍历到 id=10, next-key lock, (5,10]
    3. 向左遍历到 id=5, next-key lock, (0,5], 不满足条件, 停止遍历

    例子4

    begin;
    select * from c20 where c>=15 and c<=20 order by c desc lock in share mode;

    执行过程:

    1. 在索引c上搜索 c=20 这一行, 由于索引c是普通索引, 因此此处的查找条件是 <u>最右边c=20</u> 的行, 因此需要继续向右遍历, 直到找到 c=25 这一行, 这个过程是等值查询. 根据优化2, 锁的范围是 (20, 25)?
    2. 接着再向左遍历, 之后的过程就不是等值查询了.

    个人理解:

    1. 由于 order by c desc, 因此首先等值查询 c=20 存在, 加锁 (15, 20]
    2. 向右遍历到 c=25, 不满足, 但可优化, 加锁 (20,25)
    3. 向左遍历到 c=15, 加锁 (10, 15]
    4. 向左遍历到 c=10, 加锁 (5,10]

    例子5

    begin;
    select * from c20 where c<=20 order by c desc lock in share mode;

    这里留意一下 , 加锁范围并不是 (20, 25], (15, 20], (10,15], (5,10], (0, 5], (-∞, 5], 而是

    ...........

    ..........

    .........

    ........

    .......

    ......

    .....

    ......

    .......

    ........

    .........

    ..........

    ...........

    所有行锁+间隙锁.

    具体为什么, 其实只要 explain 看一下就明白了.

    +------+-------------+-------+------+---------------+------+---------+------+------+-----------------------------+
    | id   | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra                       |
    +------+-------------+-------+------+---------------+------+---------+------+------+-----------------------------+
    |    1 | SIMPLE      | c20   | ALL  | c             | NULL | NULL    | NULL |   14 | Using where; Using filesort |
    +------+-------------+-------+------+---------------+------+---------+------+------+-----------------------------+

    但如果是 c<=19, 则会使用索引 c, 这说明 innodb 引擎有自己一套规则用于"估算"当前使用二级索引还是主键索引哪个开销会更小.

    explain select * from c20 where c<=19 order by c desc lock in share mode;
    +------+-------------+-------+-------+---------------+------+---------+------+------+-------------+
    | id   | select_type | table | type  | possible_keys | key  | key_len | ref  | rows | Extra       |
    +------+-------------+-------+-------+---------------+------+---------+------+------+-------------+
    |    1 | SIMPLE      | c20   | range | c             | c    | 5       | NULL |    4 | Using where |
    +------+-------------+-------+-------+---------------+------+---------+------+------+-------------+

    例子6

    begin;
    select * from c20 where c>=10 and c<15 for update;

    加锁范围是

    • 索引 c 的 (5,10], (10,15]

      这里对索引 c 的 15 好像是退化成行锁了, 不是很理解.
    • 主键索引的 [10]

      访问到的才会加锁, 由于没有访问主键 id=15, 因此不会对齐加锁.

    例子7 - 个人不理解的地方

    -- T1 事务A
    begin;
    select * from c20 where id>=15 and id<=20 order by id desc lock in share mode;
    
    -- T2 事务B
    begin;
    update c20 set d=d+1 where id=25;    -- OK
    insert into c20 values(21,21,21);    -- 阻塞
    
    -- T3 事务A 人为制造死锁, 方便查看锁状态
    update c20 set d=d+1 where id=25;    -- OK
    /*
    此时 事务B 提示:
    ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
    */

    个人不理解的:

    根据order by id desc, T1 时刻事务A首先在主键索引上搜索 id=20 这一行, 正常来说主键索引上 id=20 的只有一行, 没必要向右遍历.

    加锁范围:

    • (5,10]
    • (10,15]
    • (15,20]
    • (20,25)
    mysql> show engine innodb status
    ------------------------
    LATEST DETECTED DEADLOCK
    ------------------------
    2019-09-27 10:34:29 0xe2e8
    *** (1) TRANSACTION:
    TRANSACTION 1645, ACTIVE 100 sec inserting
    mysql tables in use 1, locked 1
    LOCK WAIT 3 lock struct(s), heap size 1080, 4 row lock(s), undo log entries 1
    MySQL thread id 82, OS thread handle 77904, query id 61115 localhost ::1 root update
    insert into c20 values(21,21,21)
    *** (1) WAITING FOR THIS LOCK TO BE GRANTED:
    RECORD LOCKS space id 23 page no 3 n bits 80 index PRIMARY of table `test_yjx`.`c20` trx id 1645 lock_mode X locks gap before rec insert intention waiting
    Record lock, heap no 7 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
     0: len 4; hex 80000019; asc     ;;
     1: len 6; hex 00000000066d; asc      m;;
     2: len 7; hex 6e0000019a0110; asc n      ;;
     3: len 4; hex 80000019; asc     ;;
     4: len 4; hex 8000001a; asc     ;;
    
    *** (2) TRANSACTION:
    TRANSACTION 1646, ACTIVE 271 sec starting index read
    mysql tables in use 1, locked 1
    5 lock struct(s), heap size 1080, 5 row lock(s)
    MySQL thread id 81, OS thread handle 58088, query id 61120 localhost ::1 root updating
    update c20 set d=d+1 where id=25
    *** (2) HOLDS THE LOCK(S):
    RECORD LOCKS space id 23 page no 3 n bits 80 index PRIMARY of table `test_yjx`.`c20` trx id 1646 lock mode S locks gap before rec
    Record lock, heap no 7 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
     0: len 4; hex 80000019; asc     ;;
     1: len 6; hex 00000000066d; asc      m;;
     2: len 7; hex 6e0000019a0110; asc n      ;;
     3: len 4; hex 80000019; asc     ;;
     4: len 4; hex 8000001a; asc     ;;
    
    *** (2) WAITING FOR THIS LOCK TO BE GRANTED:
    RECORD LOCKS space id 23 page no 3 n bits 80 index PRIMARY of table `test_yjx`.`c20` trx id 1646 lock_mode X locks rec but not gap waiting
    Record lock, heap no 7 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
     0: len 4; hex 80000019; asc     ;;
     1: len 6; hex 00000000066d; asc      m;;
     2: len 7; hex 6e0000019a0110; asc n      ;;
     3: len 4; hex 80000019; asc     ;;
     4: len 4; hex 8000001a; asc     ;;
    
    *** WE ROLL BACK TRANSACTION (1)

    上述的:

    • (1) TRANSACTION(事务1) 指的是事务B
    • (2) TRANSACTION(事务2) 指的是事务A

    注意与上面的 事务A, 事务B 顺序是相反了, 别看错了.

    分析:

    • (1) TRANSACTION

      • insert into c20 values(21,21,21) 最后一句执行语句
    • (1) WAITING FOR THIS LOCK TO BE GRANTED

      • index PRIMARY of table test_yjx.c20 说明在等表 c20 主键索引上的锁
      • lock_mode X locks gap before rec insert intention waiting 说明在插入一条记录, 试图插入一个意向锁, 与间隙锁产生冲突了
      • 0: len 4; hex 80000019; asc ;; 冲突的间隙锁: 16进制的 19, 即 10进制的 id=25 左边的间隙.
    • (2) TRANSACTION 事务2信息

      • update c20 set d=d+1 where id=25 最后一句执行语句
    • (2) HOLDS THE LOCK(S) 事务2持有锁的信息

      • index PRIMARY of table test_yjx.c20 说明持有c20表主键索引上的锁
      • lock mode S locks gap before rec 说明只有间隙锁
      • 0: len 4; hex 80000019; asc ;; 间隙锁: id=25 左边的间隙
    • (2) WAITING FOR THIS LOCK TO BE GRANTED: 事务2正在等待的锁

      • index PRIMARY of table test_yjx.c20 说明在等待 c20 表主键索引上的锁
      • lock_mode X locks rec but not gap waiting 需要对行加写锁
      • 0: len 4; hex 80000019; asc ;; 等待给 id=25 加行锁(写)
    • WE ROLL BACK TRANSACTION (1) 表示回滚了事务1

    个人猜测实际情况是:

    1. 首先找到 id=20 这一条记录, 由于bug, 引擎认为可能存在不止一条的 id=20 的记录(即将其认为是普通索引), 因此向右遍历, 找到了 id=25 这一行, 由于此时是等值查询, 根据优化2, 锁退化为间隙锁, 即 (20,25)
    2. 之后正常向左遍历.

    无法证实自己的猜测. 已在课程21和课程30留下以下留言, 等待解答(或者无人解答). 2019年9月27日

    -- T1 事务A
    begin;
    select * from c20 where id>=15 and id<=20 order by id desc lock in share mode;
    
    -- T2 事务B
    begin;
    update c20 set d=d+1 where id=25;    -- OK
    insert into c20 values(21,21,21);    -- 阻塞

    不能理解, 为什么事务A执行的语句会给 间隙(20,25) 加上锁.
    通过 show engine innodb status; 查看发现事务A确实持有上述间隙锁.
    通过 explain select * from c20 where id>=15 and id<=20 order by id desc lock in share mode; 查看 Extra 也没有 filesort, key=PRIMARY, 因此个人认为是按照主键索引向左遍历得到结果.

    按照我的理解, 由于 order by id desc , 因此首先是在主键索引上搜索 id=20, 同时由于主键索引上这个值是唯一的, 因此不必向右遍历. 然而事实上它确实这么做了, 这让我想到了 BUG1: 主键索引上的范围查询会遍历到不满足条件的第一个.
    但是这一步的搜索过程应该是等值查询才对, 完全一脸懵住了...
    不知道老师现在还能看到这条评论不?

    加锁案例

    案例: 主键索引 - 等值查询 - 间隙锁

    -- T1 事务A
    begin;
    update c20 set d=d+1 where id=7;
    /*
    1. 在主键索引上不存在id=7记录, 根据规则1: 加锁基本单位是 next-key lock, 因此加锁范围是(5,10]
    2. 由于id=7是一个等值查询, 根据优化2, id=10不满足条件, 因此锁退化为间隙锁 (5,10)
    */
    
    -- T2 事务B
    begin;
    insert into c20 values(8,8,8);        -- 阻塞
    update c20 set d=d+1 where id=10;    -- OK
    对应课程的案例一

    案例: 非唯一索引 - 等值查询 - 间隙锁

    -- T1 事务A
    begin;
    update c20 set d=d+1 where c=7;
    /* 分析
    1. 加锁基本单位是next-key lock, 加锁范围就是 (5,10]   -- 此时只是分析过程, 并非加锁过程
    2. 根据优化2, 索引上的等值查询(c=7)向右遍历且最后一个值不满足条件时, next-key lock 退化为间隙锁, 加锁范围变为 (5, 10)
    3. 由于是在索引c上查询, 因此加锁范围实际上是索引 c 上的 ((5,5), (10,10)) , 格式 (c, id)
    */
    
    -- T2 事务B
    begin;
    insert into c20 values(4,5,4);    -- OK
    insert into c20 values(6,5,4);    -- 被间隙锁堵住
    insert into c20 values(9,10,9);    -- 被间隙锁堵住
    insert into c20 values(11,10,9);    -- OK

    案例: 非唯一索引 - 等值查询 - 覆盖索引

    关注重点: 覆盖索引优化导致无需回表的情况对主键索引影响

    -- T1 事务A
    begin;
    select id from c20 where c = 5 lock in share mode;    
    -- 索引c是普通索引, 因此会扫描到 c=10 这一行, 因此加锁范围是 (0,5], (5,10)
    -- 同时由于优化2: 索引上的等值查询向右遍历且最后一个值不满足条件时next-key lock退化为间隙锁, 即加锁范围实际是  (0,5], (5,10)
    -- 注意, 该条查询由于只 select id, 实际只访问了索引c, 并没有访问到主键索引, 根据规则2: 访问到的对象才会加锁, 因此最终只对索引c 的范围 (0,5], (5,10) 加锁
    
    -- T2 事务B
    begin;
    update c20 set d=d+1 where id=5;    -- OK, 因为覆盖索引优化导致并没有给主键索引上加锁
    insert into c20 values(7,7,7);
    对应课程的案例二

    注意, 上面是使用 lock in share mode 加读锁, 因此会被覆盖索引优化.

    如果使用 for update, mysql认为你接下来要更新行, 因此也会锁上对应的主键索引.

    案例: 非主键索引 - 范围查询 - 对主键的影响

    关注重点在于: 普通索引上的范围查询时对不符合条件的索引加锁时, 是否会对对应的主键索引产生影响.

    -- T1 事务A
    begin;
    select * from c20 where c>=10 and c<11 for update;
    /*
    1. 首先查找到 c=10 这一行, 锁范围 (5,10]
    2. 接着向右遍历(这时候不是等值查询, 是遍历查询), 找到 c=15 这一行, 不符合条件, 查询结束. 根据规则2: 只有访问到的对象才会加锁, 由于不需要访问c=15对应的主键索引项, 因此这里的锁范围是索引c上的 (5,10], (10,15], 以及主键上的行锁[10]
    */
    
    -- T2 事务B
    begin;
    select * from c20 where c=15 for update;     -- 阻塞
    select * from c20 where id=15 for update;    -- OK

    加锁范围

    • 索引 c

      • (5,10]
      • (10,15]
    • 主键

      • [10]

    案例: 主键索引 - 范围锁

    -- T1 事务A
    begin;
    select * from c20 where id>=10 and id<11 for update;
    /*
    1. 首先在主键索引上查找 id=10 这一行, 根据优化1: 索引上的等值查询在对唯一索引加锁时, next-key lock 退化为行锁, 此时加锁范围是 [10]
    2. 继续向右遍历到下一个 id=15 的行, 此时并非等值查询, 因此加锁范围是 [10], (10,15]
    */
    
    -- T2 事务B
    begin;
    insert into c20 values(8,8,8);        -- OK
    insert into c20 values(13,13,13);    -- 阻塞
    update c20 set d=d+1 where id=15;    -- 阻塞
    对应课程案例三

    这里要注意, 事务A首次定位查找id=10这一行的时候是等值查询, 而后续向右扫描到id=15的时候是范围查询判断.

    主键索引的加锁范围

    • [10]
    • (10,15]

    案例: 非唯一索引 - 范围锁

    -- T1 事务A
    begin;
    select * from c20 where c >= 10 and c < 11 for update;
    /*
    1. 首先在索引c上找到 c=10 这一行, 加上锁 (5,10]
    2. 向右遍历找到 c=15 这一行, 不满足条件, 最终加锁范围是 索引c上的 (5,10], (10,15], 及主键索引 [5]
    */
    
    -- T2 事务B
    begin;
    insert into c20 values(8,8,8);        -- 阻塞
    update c20 set d=d+1 where c=15;    -- 阻塞
    update c20 set d=d+1 where id=15;    -- 阻塞
    对应课程案例四

    主键的加锁范围

    • (5,10]
    • (10,15]

    案例: 唯一索引 - 范围锁 - bug

    -- T1 事务A
    begin;
    select * from c20 where id>10 and id<=15 for update
    
    -- T2 事务B
    begin;
    update c20 set d=d+1 where id=20;    -- 阻塞
    insert into c20 values(16,16,16);    -- 阻塞

    顺便提一下:

    begin;
    select * from c20 where id>10 and id<15 for update;
    /*
    1. 在主键索引上找到id=15这一行, 不满足条件, 根据原则1, 加锁 (10,15]
    */

    对应课程案例五

    主键的加锁范围

    • (10,15]
    • (15,20]

    案例: 非唯一索引 - 等值

    -- T1 事务A
    begin;
    insert into c20 values(30,10,30);
    commit;
    /*
    在索引c上, 此时有两行 c=10 的行
    由于二级索引上保存着主键的值, 因此并不会有两行完全一致的行, 如下:
    c    0    5    10    10    15    20    25
    id    0    5    10    30    15    20    25
    
    此时两个 (c=10, id=10) 和 (c=10, id=30) 之间也是存在间隙的
    */
    
    -- T2 事务B
    begin;
    delete from c20 where c=10;
    /*
    1. 首先找到索引c上 (c=10, id=10) 这一行, 加锁 (5,10]
    2. 向右遍历, 找到 (c=10, id=30) 这一行, 加锁 ( (c=10,id=10), (c=10,id=30) ]
    3. 向右遍历, 找到 c=20 这一行, 根据优化2, 索引上的等值查询向右遍历且最后一个值不匹配时, next-key lock 退化为间隙锁, 即加锁 (10,15)
    4. 总的加锁范围是 (5,10], ( (c=10,id=10), (c=10,id=30) ], (10,15]
    */
    
    -- T3 事务C
    begin;
    insert into c20 values(12,12,12);    -- 阻塞
    update c20 set d=d+1 where c=15;    -- OK
    
    
    -- T4 扫尾, 无视
    delete from c20 where id=30;
    对应课程案例六

    delete 的加锁逻辑跟 select ... for update 是类似的.

    事务 B 对索引 c 的加锁范围

    • (5,10]
    • (10,15)

    案例: 非唯一索引 - limit

    -- T0 初始环境
    insert into c20 values(30,10,30);
    
    -- T1 事务A
    begin;
    delete from c20 where c=10 limit 2;
    /*
    1. 找到 c=10 的第一条, 加锁 (5,10]
    2. 向右遍历, 找到 c=10,id=30 的记录, 加锁 ( (c=10,id=10), (c=10,id=30) ], 此时满足 limit 2
    */
    
    -- T2, 事务B
    begin;
    insert into c20 values(12,12,12);    -- OK

    如果不加 limit 2 则会继续向右遍历找到 c=15 的记录, 新增加锁范围 (10,15)

    对应课程案例七

    指导意义:

    • 在删除数据时尽量加 limit, 不仅可以控制删除的条数, 还可以减小加锁的范围.

    案例: 死锁例子

    -- T1 事务A
    begin;
    select id from c20 where c=10 lock in share mode;
    /*
    1. 在索引c上找到 c=10 这一行, 由于覆盖索引的优化, 没有回表, 因此只会在索引c上加锁 (5,10]
    2. 向右遍历, 找到 c=15, 不满足, 根据优化2, 加锁范围退化为 (10,15)
    3. 总的加锁范围是在索引c上的 (5,10], (10,15)
    */
    
    -- T2 事务B
    begin;
    update c20 set d=d+1 where c=10;    -- 阻塞
    /*
    1. 找到 c=10 这一行, 试图加上锁 (5,10], 按照顺序先加上间隙锁(5,10), 由于间隙锁之间不冲突, OK. 之后再加上 [10] 的行锁, 但被T1时刻的事务A阻塞了, 进入锁等待
    */
    
    -- T3 事务A
    insert into t values(8,8,8);    -- OK, 但造成 事务B 回滚
    /*
    往 (5,10) 这个间隙插入行, 此时与 T2时刻事务B 加的间隙锁产生冲突.
    同时由于 事务B 也在等待 T1时刻事务A 加的行锁, 两个事务间存在循环资源依赖, 造成死锁.
    此时事务B被回滚了, 报错如下:
    ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
    */
    对应课程案例八

    案例: 非主键索引 - 逆序

    -- T1 事务A
    begin;
    select * from c20 where c>=15 and c<=20 order by c desc lock in share mode;
    /*
    1. 在索引c上找到 c=20 这一行, 加锁 (15,20]
    2. 向左遍历, 找到 c=15 这一行, 加锁 (10,15]
    3. 继续向左遍历, 找到 c=10 这一行, 由于不满足优化条件, 因此直接加锁 (5,10], 不满足查询条件, 停止遍历. 
    4. 最终加锁范围是 (5,10], (10,15], (15, 20]
    */
    
    -- T2 事务B
    insert into c20 values(6,6,6);    -- 阻塞
    对应课程的上期答疑

    索引 c 的加锁范围

    • (5,10]
    • (10,15]
    • (15,20]
    • (20, 25)

    案例: 读提交级别 - semi-consistent 优化

    -- 表结构
    create table t(a int not null, b int default null)Engine=Innodb;
    insert into t values(1,1),(2,2),(3,3),(4,4),(5,5);
    
    -- T1 事务A
    set session transaction isolation level read committed;
    begin;
    update t set a=6 where b=1;
    /*
    b没有索引, 因此全表扫描, 对主键索引上所有行加上行锁
    */
    
    -- T2 事务B
    set session transaction isolation level read committed;
    begin;
    update t set a=7 where b=2;    -- OK
    /*
    在读提交隔离级别下, 如果 update 语句碰到一个已经被锁了的行, 会读入最新的版本, 然后判断是不是满足查询条件, 若满足则进入锁等待, 若不满足则直接跳过.
    */
    delete from t where b=3;    -- 阻塞
    /*
    注意这个策略对 delete 是无效的, 因此delete语句被阻塞
    */
    对应课程评论下方 @时隐时现 2019-01-30 的留言

    案例: 主键索引 - 动态间隙锁 - delete

    -- T1 事务A
    begin;
    select * from c20 where id>10 and id<=15 for update;
    /*
    加锁 (10,15], (15, 20]???
    */
    
    -- T2 事务B 注意此处没加 begin, 是马上执行并提交的单个事务.
    delete from c20 where id=10;    -- OK
    /*
    事务A在T1时刻加的间隙锁 (10,15) 此时动态扩展成 (5,15)
    */
    
    -- T3 事务C
    insert into c20 values(10,10,10);    -- 阻塞
    /*
    被新的间隙锁堵住了
    */
    对应课程评论下方 @Geek_9ca34e 2019-01-09 的留言

    如果将上方的 T2时刻的事务B 和 T3时刻的事务C 合并在一个事务里, 则不会出现这种情况.

    个人理解是, 事务未提交时, 期间删除/修改的数据仅仅是标记删除/修改, 此时记录还在, 因此间隙锁范围不变.

    只有在事务提价后才会进行实际的删除/修改, 因此间隙锁才"会动态扩大范围"

    案例: 普通索引 - 动态间隙锁 - update

    -- T1 事务A
    begin;
    select c from c20 where c>5 lock in share mode;
    /*
    找到 c=5, 不满足, 向右遍历找到 c=10, 加锁 (5,10], 继续遍历, 继续加锁...
    */
    
    -- T2 事务B
    update c20 set c=1 where c=5;    -- OK
    /*
    删除了 c=5 这一行, 导致 T1时刻事务A 加的间隙锁 (5,10) 变为 (1,10)
    */
    
    -- T3 事务C
    update c20 set c=5 where c=1;    -- 阻塞
    /*
    将 update 理解为两步:
    1. 插入 (c=5, id=5) 这个记录    -- 被间隙锁阻塞
    2. 删除 (c=1, id=5) 这个记录
    */

    案例: 非主键索引 - IN - 等值查询

    begin;
    select id from c20 where c in (5,20,10) lock in share mode;

    通过 explain 分析语句:

    mysql> explain select id from c20 where c in (5,20,10) lock in share mode;
    +----+-------------+-------+-------+---------------+------+---------+------+------+---------------------
    | id | select_type | table | type  | possible_keys | key  | key_len | ref  | rows | Extra     
    +----+-------------+-------+------------+-------+---------------+------+---------+------+------+---------
    |  1 | SIMPLE      | c20   | range | c             | c    | 5       | NULL |    3 | Using where; Using index |
    +----+-------------+-------+------------+-------+---------------+------+---------+------+------+---------
    1 row in set, 1 warning (0.00 sec)
    显示结果太长, 因此将 partitions, filtered 列删除了

    结果分析:

    • 使用了索引 c
    • rows = 3 说明这3个值都是通过 B+ 树搜索定位的

    语句分析:

    1. 在索引c上查找 c=5, 加锁 (0,5], 向右遍历找到 c=10, 不满足条件, 根据优化2, 加锁 (5,10)
    2. 在索引c上查找 c=10, 类似步骤1, 加锁 (5,10], (10, 15)
    3. 在索引c上查找 c=20, 加锁 (15,20], (20, 25)

    注意上述锁是一个个逐步加上去的, 而非一次性全部加上去.

    考虑以下语句:

    begin;
    select id from c20 where c in (5,20,10) order by id desc for update;

    根据语义 order by id desc, 会依次查找 c=20, c=10, c=5.

    由于加锁顺序相反, 因此如果这两个语句并发执行的时候就有可能发生死锁.

    相关命令

    查看最后一个死锁现场

    show engine innodb status;

    查看 LATEST DETECTED DEADLOCK 这一节, 记录了最后一次死锁信息.

    示例

    ------------------------
    LATEST DETECTED DEADLOCK
    ------------------------
    2019-09-24 16:24:18 0x5484
    *** (1) TRANSACTION:
    TRANSACTION 1400, ACTIVE 191 sec starting index read
    mysql tables in use 1, locked 1
    LOCK WAIT 2 lock struct(s), heap size 1080, 3 row lock(s)
    MySQL thread id 54, OS thread handle 74124, query id 36912 localhost ::1 root updating
    update c20 set d=d+1 where c=10
    *** (1) WAITING FOR THIS LOCK TO BE GRANTED:
    RECORD LOCKS space id 23 page no 4 n bits 80 index c of table `test_yjx`.`c20` trx id 1400 lock_mode X waiting
    Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
     0: len 4; hex 8000000a; asc     ;;
     1: len 4; hex 8000000a; asc     ;;
    
    *** (2) TRANSACTION:
    TRANSACTION 1401, ACTIVE 196 sec inserting
    mysql tables in use 1, locked 1
    5 lock struct(s), heap size 1080, 3 row lock(s), undo log entries 1
    MySQL thread id 53, OS thread handle 21636, query id 36916 localhost ::1 root update
    insert into c20 values(8,8,8)
    *** (2) HOLDS THE LOCK(S):
    RECORD LOCKS space id 23 page no 4 n bits 80 index c of table `test_yjx`.`c20` trx id 1401 lock mode S
    Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
     0: len 4; hex 8000000a; asc     ;;
     1: len 4; hex 8000000a; asc     ;;
    
    *** (2) WAITING FOR THIS LOCK TO BE GRANTED:
    RECORD LOCKS space id 23 page no 4 n bits 80 index c of table `test_yjx`.`c20` trx id 1401 lock_mode X locks gap before rec insert intention waiting
    Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
     0: len 4; hex 8000000a; asc     ;;
     1: len 4; hex 8000000a; asc     ;;
    
    *** WE ROLL BACK TRANSACTION (1)

    结果分为3个部分:

    • (1) TRANSACTION 第一个事务的信息

      • WAITING FOR THIS LOCK TO BE GRANTED, 表示这个事务在等待的锁资源
    • (2) TRANSACTION 第二个事务的信息

      • HOLDS THE LOCK(S) 显示该事务持有哪些锁
    • WE ROLL BACK TRANSACTION (1) 死锁检测的处理: 回滚了第一个事务

    第一个事务的信息中:

    • update c20 set d=d+1 where c=10 导致死锁时执行的最后一条 sql 语句
    • WAITING FOR THIS LOCK TO BE GRANTED

      • index c of table test_yjx.c20, 说明在等的是表 c20 的索引 c 上面的锁
      • lock_mode X waiting 表示这个语句要自己加一个写锁, 当前状态是等待中.
      • Record lock 说明这是一个记录锁
      • n_fields 2 表示这个记录是两列, 即 字段c 和 主键字段 id
      • 0: len 4; hex 8000000a; asc ;; 是第一个字段(即字段c), 值(忽略里面的8)是十六进制 a, 即 10

        值 8000000a 中的 8...我也不理解为什么, 先忽略
      • 1: len 4; hex 8000000a; asc ;; 是第二个字段(即字段id), 值是 10
      • 上面两行里的 asc 表示, 接下来要打印出值里面的"可打印字符", 但10不是可打印字符, 因此就显示空格

        这里不太理解
    • 第一个事务信息只显示出等锁的状态, 在等待 (c=10, id=10) 这一行的锁
    • 没有显示当前事务持有的锁, 但可以从第二个事务中推测出来.

    第二个事务的信息中:

    • insert into c20 values(8,8,8) 导致死锁时最后执行的语句
    • HOLDS THE LOCK(S)

      • index c of table test_yjx.c20 trx id 1401 lock mode S 表示锁是在表 c20 的索引 c 上, 加的是读锁
      • hex 8000000a;表示这个事务持有 c=10 这个记录锁
    • WAITING FOR THIS LOCK TO BE GRANTED

      • index c of table test_yjx.c20 trx id 1401 lock_mode X locks gap before rec insert intention waiting

        • insert intention 表示试图插入一个记录, 这是一个插入意向锁, 与间隙锁产生锁冲突
        • gap before rec 表示这是一个间隙锁, 而不是记录锁.

    补充:

    • lock_mode X waiting 表示 next-key lock
    • lock_mode X locks rec but not gap 表示只有行锁
    • locks gap before rec 就是只有间隙锁

    从上面信息可以知道:

    • 第一个事务

      • 推测出持有间隙锁 (?, 10)
      • 试图更新 c=10 这一行, 但被索引c 的 行锁 c=10 阻塞了
    • 第二个事务

      • 持有行锁 c=10
      • 试图插入 (8,8,8), 但被间隙锁 (?, 10) 阻塞了
    • 检测到死锁时, InnoDB 认为 第二个事务回滚成本更高, 因此回滚了第一个事务.

    待整理

    案例

    -- 前提: 表 T 上有普通索引 k
    
    -- 语句1
    select * from T where k in (1,2,3,4,5);
    
    -- 语句2
    select * from T where k between 1 and 5;

    这两条语句的区别是:

    语句1: 在索引k上进行了5次树查找

    语句2: 在索引k上进行了1次树查找(k=1), 之后向右遍历直到id>5

    很明显, 语句2 性能会更好.

    三. WAL 机制及脏页刷新

    文章链接: https://segmentfault.com/a/11...

    本部分主要来自: 极客时间《MySQL实战45讲》的第12讲 - 为什么我的MySQL会“抖”一下

    WAL(Write-Ahead Loggin)

    WAL 是预写式日志, 关键点在于先写日志再写磁盘.

    在对数据页进行修改时, 通过将"修改了什么"这个操作记录在日志中, 而不必马上将更改内容刷新到磁盘上, 从而将随机写转换为顺序写, 提高了性能.

    但由此带来的问题是, 内存中的数据页会和磁盘上的数据页内容不一致, 此时将内存中的这种数据页称为 脏页

    Redo Log(重做日志)

    这里的日志指的是Redo Log(重做日志), 这个日志是循环写入的.

    它记录的是在某个数据页上做了什么修改, 这个日志会携带一个LSN, 同时每个数据页上也会记录一个LSN(日志序列号).

    这个日志序列号(LSN)可以用于数据页是否是脏页的判断, 比如说 write pos对应的LSN比某个数据页的LSN大, 则这个数据页肯定是干净页, 同时当脏页提前刷到磁盘时, 在应用Redo Log可以识别是否刷过并跳过.

    这里有两个关键位置点:

    • write pos 当前记录的位置, 一边写一边后移.
    • checkpoint 是当前要擦除的位置, 擦除记录前要把记录更新到数据文件.

    脏页

    当内存数据页和磁盘数据页内容不一致的时候, 将内存页称为"脏页".
    内存数据页写入磁盘后, 两边内容一致, 此时称为"干净页".
    将内存数据页写入磁盘的这个操作叫做"刷脏页"(flush).

    InnoDB是以缓冲池(Buffer Pool)来管理内存的, 缓冲池中的内存页有3种状态:

    • 未被使用
    • 已被使用, 并且是干净页
    • 已被使用, 并且是脏页

    由于InnoDB的策略通常是尽量使用内存, 因此长时间运行的数据库中的内存页基本都是被使用的, 未被使用的内存页很少.

    刷脏页(flush)

    时机

    刷脏页的时机:

    1. Redo Log写满了, 需要将 checkpoint 向前推进, 以便继续写入日志

      checkpoint 向前推进时, 需要将推进区间涉及的所有脏页刷新到磁盘.

    2. 内存不足, 需要淘汰一些内存页(最久未使用的)给别的数据页使用.

      此时如果是干净页, 则直接拿来复用.

      如果是脏页, 则需要先刷新到磁盘(直接写入磁盘, 不用管Redo Log, 后续Redo Log刷脏页时会判断对应数据页是否已刷新到磁盘), 使之成为干净页再拿来使用.

    3. 数据库系统空闲时

      当然平时忙的时候也会尽量刷脏页.

    4. 数据库正常关闭

      此时需要将所有脏页刷新到磁盘.

    InnoDB需要控制脏页比例来避免Redo Log写满以及单次淘汰过多脏页过多的情况.

    Redo Log 写满

    这种情况尽量避免, 因此此时系统就不接受更新, 所有更新语句都会被堵住, 此时更新数为0.

    对于敏感业务来说, 这是不能接受的.

    此时需要将 write pos 向前推进, 推进范围内Redo Log涉及的所有脏页都需要flush到磁盘中.

    Redo Log设置过小或写太慢的问题: 此时由于Redo Log频繁写满, 会导致频繁触发flush脏页, 影响tps.

    内存不足

    这种情况其实是常态.

    当从磁盘读取的数据页在内存中没有内存时, 就需要到缓冲池中申请一个内存页, 这时候根据LRU(最近最少使用算法)就需要淘汰掉一个内存页来使用.

    此时淘汰的是脏页, 则需要将脏页刷新到磁盘, 变成干净页后才能复用.

    注意, 这个过程 Write Pos 位置是不会向前推进的.

    当一个查询要淘汰的脏页数太多, 会导致查询的响应时间明显变长.

    策略

    InnoDB 控制刷脏页的策略主要参考:

    • 脏页比例

      当脏页比例接近或超过参数 innodb_max_dirty_pages_pct 时, 则会全力, 否则按照百分比.

    • redo log 写盘速度

      N = (write pos 位置的日志序号 - checkpoint对应序号), 当N越大, 则刷盘速度越快.

    最终刷盘速度取上述两者中最快的.

    参数 innodb_io_capacity

    InnoDB 有一个关键参数: innodb_io_capacity, 该参数是用于告知InnoDB你的磁盘能力, 该值通常建议设置为磁盘的写IOPS.

    该参数在 MySQL 5.5 及后续版本才可以调整.

    测试磁盘的IOPS:

    fio -filename=/data/tmp/test_randrw -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest
    注意, 上面的 -filename 要指定具体的文件名, 千万不要指定分区, 否则会导致分区不可用, 需要重新格式化.

    innodb_io_capacity 一般参考 写能力的IOPS

    innodb_io_capacity 设置过低导致的性能问题案例:

    MySQL写入速度很慢, TPS很低, 但是数据库主机的IO压力并不大.

    innodb_io_capacity 设置过小时, InnoDB会认为磁盘性能差, 导致刷脏页很慢, 甚至比脏页生成速度还慢, 就会造成脏页累积, 影响查询和更新性能.

    innodb_io_capacity 大小设置:

    • 配置小, 此时由于InnoDB认为你的磁盘性能差, 因此刷脏页频率会更高, 以此来确保内存中的脏页比例较少.
    • 配置大, InnoDB认为磁盘性能好, 因此刷脏页频率会降低, 抖动的频率也会降低.

    参数innodb_max_dirty_pages_pct

    innodb_max_dirty_pages_pct 指的是脏页比例上限(默认值是75%), 内存中的脏页比例越是接近该值, 则InnoDB刷盘速度会越接近全力.

    如何计算内存中的脏页比例:

    show global status like 'Innodb_buffer_pool_pages%';

    脏页比例 = 100 * Innodb_buffer_pool_pages_dirty / Innodb_buffer_pool_pages_total 的值

    参数 innodb_flush_neighbors

    当刷脏页时, 若脏页旁边的数据页也是脏页, 则会连带刷新, 注意这个机制是会蔓延的.

    innodb_flush_neighbors=1 时开启该机制, 默认是1, 但在 MySQL 8.0 中默认值是 0.

    由于机械硬盘时代的IOPS一般只有几百, 该机制可以有效减少很多随机IO, 提高系统性能.

    但在固态硬盘时代, 此时IOPS高达几千, 此时IOPS往往不是瓶颈, "只刷自己"可以更快执行完查询操作, 减少SQL语句的响应时间.

    如果Redo Log 设置太小

    这里有一个案例:

    测试在做压力测试时, 刚开始 insert, update 很快, 但是一会就变慢且响应延迟很高.

    ↑ 出现这种情况大部分是因为 Redo Log 设置太小引起的.

    因为此时 Redo Log 写满后需要将 checkpoint 前推, 此时需要刷脏页, 可能还会连坐(innodb_flush_neighbors=1), 数据库"抖"的频率变高.

    其实此时内存的脏页比例可能还很低, 并没有充分利用到大内存优势, 此时需要频繁flush, 性能会变差.

    同时, 如果Redo Log中存在change buffer, 同样需要做相应的merge操作, 导致 change buffer 发挥不出作用.

    对于实际场景:

    在一台高性能机器上配置了非常小的Redo Log.

    此时由于每次都很快写满Redo Log, 此时Write Pos会一直追着Check Point, 因此系统就会停止所有更新, 去推进 Check Point.

    此时看到的现象就是: 磁盘压力很小, 但是数据库出现间歇性性能下降.

    待整理

    ORDER BY 的工作方式

    关键字:

    • Using filesort, sort_buffer sort_buffer_size, 磁盘临时文件
    • 全字段排序, OPTIMIZER_TRACE,sort_mode,num_of_tmp_files
    • rowid 排序, max_length_for_sort_data

    原文: https://time.geekbang.org/col...

    TODO

    查看原文

    赞 0 收藏 0 评论 0

    嘉兴ing 关注了标签 · 2020-12-02

    pyspider

    A Powerful Spider(Web Crawler) System in Python.
    https://github.com/binux/pyspider

    关注 329

    嘉兴ing 赞了回答 · 2020-03-19

    解决通过docker命令行,报错:Fatal error, can't open config file,在容器内执行没问题

    docker inspect 可以看到,redis 的 docker 镜像的默认的 Entrypoint 是 docker-entrypoint.sh 。也就是说,CMD 定义的命令是由这个脚本执行的。

    这个脚本的内容如下(可能不同的版本会有所不同):

    #!/bin/sh
    set -e
    
    # first arg is `-f` or `--some-option`
    # or first arg is `something.conf`
    if [ "${1#-}" != "$1" ] || [ "${1%.conf}" != "$1" ]; then
            set -- redis-server "$@"
    fi
    
    # allow the container to be started with `--user`
    if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
            find . \! -user redis -exec chown redis '{}' +
            exec gosu redis "$0" "$@"
    fi
    
    exec "$@"

    可以看到,当 CMD 的第一个元素为 redis-server 的时候,它将当前目录(WorkingDir,通过 docker inspect 可以看到是 /data) 的所有文件的所有者改为了 redis ,然后以 redis 用户运行命令。

    Dockerfile 里,虽然将 /root/redis.conf 的权限改成了 777 ,但是 /root 目录的权限是 700 ,redis 用户不可读,于是出错。将 /root 的权限也改成 777 就可以解决。

    ========================

    由于脚本在进行精确匹配,命令改为绝对路径后将不能匹配,会进入最后一行的 exec 。此时是用 root 用户跑的,所以有读权限,可以启动。

    或者说,只要 CMD 的第一个元素不是 redis-server ,那么用户就是 root 。所以用 bash 进容器里再跑也是没有问题的。

    最后,如果 CMD 改成: CMD redis-server /root/redis.conf ,这个 CMD 会被改写成:CMD ["/bin/sh", "-c", "redis-server /root/redis.conf"] 。参考 Dockerfile / CMD 文档 ,并且可以通过 docker inspect 看到实际的 Cmd 。这时,运行的用户也是 root ,也是有读权限的,可以运行。

    在命令行指定 CMD 也会使用同样的规则,参考 docerker run / COMMAND 文档 , 可以解释 @cai2h 的问题。

    ======================

    如果 CMD 的第一个参数以 - 开始,或者以 .conf 结束,那么 docker-entrypoint.sh 会在前面加一个 redis-server 。这是其中第一个 if 干的事情。此时,由于第一个参数是(新插入的)redis-server ,所以也会以 redis 用户运行。

    也就是说,启动 redis 镜像时,redis-server 命令其实可以不写,只写参数。比如:CMD ["/root/redis.conf"] 。或者,docker run -it --rm a7f182f6c6dd /root/redis.conf

    关注 7 回答 5

    嘉兴ing 发布了文章 · 2020-01-03

    Laravel 5.8 中使用 telescope 并自定义扩展缓存驱动报错分析及解决方案

    前情提要

    1. 由于 FileStore 在存储不过期的key的expire时使用了 9999999999, 导致最后在使用 Carbon 处理时日期溢出, 因此自己修改了一下, 新增一个 App\Extensions\Cache\FileStore 文件

      <?php
      namespace App\Extensions\Cache;
      
      class FileStore extends \Illuminate\Cache\FileStore
      {
          protected function expiration($seconds)
          {
              $expiration = parent::expiration($seconds);
              return $expiration === 9999999999 ? 2147483600 : $expiration;
          }
      }
    2. 并在 App\Providers\AppServiceProvider::boot() 中扩展该缓存驱动

      Cache::extend('file2', function ($app, $config) {
          return Cache::repository(new FileStore($app['files'], $config['path']));
      });
    3. 最后修改了默认的缓存驱动 config/cache.php

      return [
          'default' => 'file2',
      
          'stores' => [
              ...
              'file' => [
                  'driver' => 'file2',
                  ...
              ],
              ...
          ]    
      ];

    这时候问题出来了, 无论是启动 php artisan tinker 或网页直接访问, 都会报错:

    Driver [file2] is not supported

    先一顿分析

    1. laravel/telescope 在 composer.json 中配置了包自动发现策略:

      extra.laravel.providers: ["Laravel\\Telescope\\TelescopeServiceProvider"]

      composer 在安装/更新包时, 会将所有安装的包的信息存储在 vendor/composer/installed.json, 其中包含每个包的安装信息及其配置的composer.json文件

    2. 项目 composer.json 根据其配置 `scripts.post-autoload-dumpautoload-dump 后会执行 php artisan package:discover --ansi 命令

      上述命令对应的是 Illuminate\Foundation\Console\PackageDiscoverCommand 文件.

      它会调用 Illuminate\Foundation\PackageManifest::build() , 该方法会将 vendor/composer/installed.json中配置了 extra.laravel.providers 的项提取出来, 并保存在 bootstrap/cache/packages.php 中.

      这部分解析可以参考: https://divinglaravel.com/lar...
    1. 在laravel启动过程中, Illuminate\Foundation\Application::registerConfiguredProviders()会逐个注册所需的服务提供者, 服务提供者列表来源包括: config('app.providers') 以及 laravel 包自动发现策略.

      public function registerConfiguredProviders()
      {
          $providers = Collection::make($this->config['app.providers'])
              ->partition(function ($provider) {
                  return Str::startsWith($provider, 'Illuminate\\');
              });
      
          $providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);
      
          (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
          ->load($providers->collapse()->toArray());
      }

      上述代码分析:

      第3行: 将配置中 app.providers 中的服务提供者根据字符串前缀匹配分开, 此时 $providers 值大致是这样的:

      {
          0 : [
              "Illuminate\....."
              ...
              "Illuminate\....."
          ],
          1 : [
              "App\Providers\AppServiceProvider::class",
              ...
              "App\Providers\RouteServiceProvider::class",
          ],
      }

      第8行: 将laravel包自动发现策略获取的服务提供者列表插入到 $providers 数组中 1 的位置, 原先的 1 挪到 2, 此时 $providers 数组大致如下:

      {
          0 : [
              "Illuminate\....."
              ...
              "Illuminate\....."
          ],
          1 : [
              ...
              "Laravel\Telescope\TelescopeServiceProvider",
              ...
          ],
          2 : [
              "App\Providers\AppServiceProvider",
              ...
              "App\Providers\RouteServiceProvider",
          ],
      }

      第10行: 将 $providers 数组扁平化, 顺序则是依次 0, 1, 2 这样分别 register(注册) 这些服务提供者.

    1. 在laravel启动初始化的最后还会依次按 register(注册) 的顺序依次 boot(启动)上述注册的服务提供者.

      对于 Laravel\Telescope\TelescopeServiceProvider 这个服务提供者, 按照如下的调用顺序

      Laravel\Telescope\TelescopeServiceProvider::boot()
          |
          V
      Laravel\Telescope\Telescope::start()
          |
          V
      Laravel\TelescopeRegistersWatchers::registerWatchers()
          |
          V
      Laravel\Telescope\Watchers\DumpWatcher::register()
          ↑ 这里的代码调用 $this->cache->get("...")

      Laravel\Telescope\Watchers\DumpWatcher::register() 其中的代码调用了缓存相关接口, 然而此时根本就没有执行到 App\Providers\AppServiceProvider::boot(), 自然会导致报错无法找到该缓存驱动.

    解决办法

    基本思路就是调整 Laravel\Telescope\TelescopeServiceProvider 服务提供者的加载顺序, 使其在 App\Providers\AppServiceProvider 之后加载.

    这里给出一个方案:

    1. 配置项目的 composer.json, 使laravel的包自动发现策略忽略 TelescopeServiceProvider

      {
          ...
          "extra": {
              "laravel": {
                  "dont-discover": [
                      "laravel/telescope"
                  ]
              }
          },
          ...
      }
    2. TelescopeServiceProvider 手动加入到服务提供者列表中, 注意顺序

      修改 config/app.php

      return [
          ...
          'providers' => [
              ...
              App\Providers\AppServiceProvider::class,
              ...
              Laravel\Telescope\TelescopeServiceProvider::class,        
              App\Providers\TelescopeServiceProvider::class,
              ...        
          ],    
          ...
      ];
    查看原文

    赞 1 收藏 0 评论 0

    嘉兴ing 赞了回答 · 2019-12-17

    解决larave session问题,为什么每次session_id都要变

    看了这块的源码找到了答案:

    一切都是在EncryptCookies中进行的

    \App\Http\Middleware\EncryptCookies::class

    larave_session

    先经过base64_decode,在json_decode
    在进行一些列验证

    clipboard.png

    然后通过openssl_decrypt解密出真正存储在redis或其他drive里面的session_id
    clipboard.png

    之后再response里面对cookie在进行加密。 这就是为什么每次请求我们看到的laravel_session的值都不一样了
    clipboard.png

    clipboard.png

    关注 4 回答 4

    嘉兴ing 发布了文章 · 2019-10-28

    MySQL WAL(Write-Ahead Log)机制及脏页刷新

    最后更新: 2019年10月28日13:35:41

    本篇文章属于个人备忘录, 主要内容来自: 极客时间《MySQL实战45讲》的第12讲 - 为什么我的MySQL会“抖”一下

    WAL(Write-Ahead Loggin)

    WAL 是预写式日志, 关键点在于先写日志再写磁盘.

    在对数据页进行修改时, 通过将"修改了什么"这个操作记录在日志中, 而不必马上将更改内容刷新到磁盘上, 从而将随机写转换为顺序写, 提高了性能.

    但由此带来的问题是, 内存中的数据页会和磁盘上的数据页内容不一致, 此时将内存中的这种数据页称为 脏页

    Redo Log(重做日志)

    这里的日志指的是Redo Log(重做日志), 这个日志是循环写入的.

    它记录的是在某个数据页上做了什么修改, 这个日志会携带一个LSN, 同时每个数据页上也会记录一个LSN(日志序列号).

    这个日志序列号(LSN)可以用于数据页是否是脏页的判断, 比如说 write pos对应的LSN比某个数据页的LSN大, 则这个数据页肯定是干净页, 同时当脏页提前刷到磁盘时, 在应用Redo Log可以识别是否刷过并跳过.

    这里有两个关键位置点:

    • write pos 当前记录的位置, 一边写以便后移.
    • checkpoint 是当前要擦除的位置, 擦除记录前要把记录更新到数据文件.

    脏页

    当内存数据页和磁盘数据页内容不一致的时候, 将内存页称为"脏页".
    内存数据页写入磁盘后, 两边内容一致, 此时称为"干净页".
    将内存数据页写入磁盘的这个操作叫做"刷脏页"(flush).

    InnoDB是以缓冲池(Buffer Pool)来管理内存的, 缓冲池中的内存页有3种状态:

    • 未被使用
    • 已被使用, 并且是干净页
    • 已被使用, 并且是脏页

    由于InnoDB的策略通常是尽量使用内存, 因此长时间运行的数据库中的内存页基本都是被使用的, 未被使用的内存页很少.

    刷脏页(flush)

    时机

    刷脏页的时机:

    1. Redo Log写满了, 需要将 checkpoint 向前推进, 以便继续写入日志

      checkpoint 向前推进时, 需要将推进区间涉及的所有脏页刷新到磁盘.

    2. 内存不足, 需要淘汰一些内存页(最久未使用的)给别的数据页使用.

      此时如果是干净页, 则直接拿来复用.

      如果是脏页, 则需要先刷新到磁盘(直接写入磁盘, 不用管Redo Log, 后续Redo Log刷脏页时会判断对应数据页是否已刷新到磁盘), 使之成为干净页再拿来使用.

    3. 数据库系统空闲时

      当然平时忙的时候也会尽量刷脏页.

    4. 数据库正常关闭

      此时需要将所有脏页刷新到磁盘.

    InnoDB需要控制脏页比例来避免Redo Log写满以及单次淘汰过多脏页过多的情况.

    Redo Log 写满

    这种情况尽量避免, 因此此时系统就不接受更新, 所有更新语句都会被堵住, 此时更新数为0.

    对于敏感业务来说, 这是不能接受的.

    此时需要将 write pos 向前推进, 推进范围内Redo Log涉及的所有脏页都需要flush到磁盘中.

    Redo Log设置过小或写太慢的问题: 此时由于Redo Log频繁写满, 会导致频繁触发flush脏页, 影响tps.

    内存不足

    这种情况其实是常态.

    当从磁盘读取的数据页在内存中没有内存时, 就需要到缓冲池中申请一个内存页, 这时候根据LRU(最久不使用)就需要淘汰掉一个内存页来使用.

    此时淘汰的是脏页, 则需要将脏页刷新到磁盘, 变成干净页后才能复用.

    注意, 这个过程 Write Pos 位置是不会向前推进的.

    当一个查询要淘汰的脏页数太多, 会导致查询的响应时间明显变长.

    策略

    InnoDB 控制刷脏页的策略主要参考:

    • 脏页比例

      当脏页比例接近或超过参数 innodb_max_dirty_pages_pct 时, 则会全力, 否则按照百分比.

    • redo log 写盘速度

      N = (write pos 位置的日志序号 - checkpoint对应序号), 当N越大, 则刷盘速度越快.

    最终刷盘速度取上述两者中最快的.

    参数 innodb_io_capacity

    InnoDB 有一个关键参数: innodb_io_capacity, 该参数是用于告知InnoDB你的磁盘能力, 该值通常建议设置为磁盘的写IOPS.

    该参数在 MySQL 5.5 及后续版本才可以调整.

    测试磁盘的IOPS:

    fio -filename=/data/tmp/test_randrw -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest
    注意, 上面的 -filename 要指定具体的文件名, 千万不要指定分区, 否则会导致分区不可用, 需要重新格式化.

    innodb_io_capacity 一般参考 写能力的IOPS

    innodb_io_capacity 设置过低导致的性能问题案例:

    MySQL写入速度很慢, TPS很低, 但是数据库主机的IO压力并不大.

    innodb_io_capacity 设置过小时, InnoDB会认为磁盘性能差, 导致刷脏页很慢, 甚至比脏页生成速度还慢, 就会造成脏页累积, 影响查询和更新性能.

    innodb_io_capacity 大小设置:

    • 配置小, 此时由于InnoDB认为你的磁盘性能差, 因此刷脏页频率会更高, 以此来确保内存中的脏页比例较少.
    • 配置大, InnoDB认为磁盘性能好, 因此刷脏页频率会降低, 抖动的频率也会降低.

    参数innodb_max_dirty_pages_pct

    innodb_max_dirty_pages_pct 指的是脏页比例上限(默认值是75%), 内存中的脏页比例越是接近该值, 则InnoDB刷盘速度会越接近全力.

    如何计算内存中的脏页比例:

    show global status like 'Innodb_buffer_pool_pages%';

    脏页比例 = 100 * Innodb_buffer_pool_pages_dirty / Innodb_buffer_pool_pages_total 的值

    参数 innodb_flush_neighbors

    当刷脏页时, 若脏页旁边的数据页也是脏页, 则会连带刷新, 注意这个机制是会蔓延的.

    innodb_flush_neighbors=1 时开启该机制, 默认是1, 但在 MySQL 8.0 中默认值是 0.

    由于机械硬盘时代的IOPS一般只有几百, 该机制可以有效减少很多随机IO, 提高系统性能.

    但在固态硬盘时代, 此时IOPS高达几千, 此时IOPS往往不是瓶颈, "只刷自己"可以更快执行完查询操作, 减少SQL语句的响应时间.

    如果Redo Log 设置太小

    这里有一个案例:

    测试在做压力测试时, 刚开始 insert, update 很快, 但是一会就变慢且响应延迟很高.

    ↑ 出现这种情况大部分是因为 Redo Log 设置太小引起的.

    因为此时 Redo Log 写满后需要将 checkpoint 前推, 此时需要刷脏页, 可能还会连坐(innodb_flush_neighbors=1), 数据库"抖"的频率变高.

    其实此时内存的脏页比例可能还很低, 并没有充分利用到大内存优势, 此时需要频繁flush, 性能会变差.

    同时, 如果Redo Log中存在change buffer, 同样需要做相应的merge操作, 导致 change buffer 发挥不出作用.

    查看原文

    赞 2 收藏 1 评论 0

    嘉兴ing 发布了文章 · 2019-10-23

    ⭐ 《MySQL必知必会》-笔记

    [TOC]

    PDF:MySQL必知必会.pdf

    PDF: [SQL学习指南]()

    这一份笔记以 《MySQL必知必会》为基础,按照个人需求持续补充,目前已经内容已经不局限于书本知识了。

    文章链接: https://segmentfault.com/a/11...

    基础概念

    MySQL 的两种发音:

    • My-S-Q-L
    • sequel

      ['siːkw(ə)l]

    数据库中的 schema : 关于数据库和表的布局及特性的信息

    有时,schema 用作数据库的同义词。遗憾的是,schema 的含义通常在上下文中并不是很清晰。

    主键(primary key): 一列(或一组列), 其值能唯一区分表中每一行。

    • 任意两行都不具有相同的主键值
    • 每个行都必须具有一个主键值(不允许为NULL值)
    • 使用多个列作为主键值, 多个列值的组合必须是唯一(但单个列的值可以不唯一)

    子句(clause) SQL语句由子句构成,有些子句是必需的,而有的是可选的。

    • 一个子句通常由一个关键字和所提供的数据组成。
    子句的例子有 SELECT 语句的 FROM 子句,

    MariaDB 与 MySQL 版本替代:

    • MySQL 5.1 -> MariaDB 5.1, 5.2, 5.3
    • MySQL 5.5 -> MariaDB 5.5, 10.0
    • MySQL 5.6 -> MariaDB 10.0
    • MySQL 5.7 -> MariaDB 10.2

    安装

    yum 安装 mysql

    下载对应的yum仓库: https://dev.mysql.com/downloa...

    ################################### 确认系统版本 ##############################
    # RHEL6
    https://dev.mysql.com/get/mysql80-community-release-el6-3.noarch.rpm
    
    # RHEL7 - 该 repo 默认提供 MySQL 8.0, 可以自行修改 repo 配置文件
    https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm
    http://mirrors.ustc.edu.cn/mysql-repo/mysql80-community-release-el7.rpm
    https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql80-community-el7/mysql80-community-release-el7-3.noarch.rpm
    #############################################################################
    
    
    
    
    
    
    ######################## 配置源(使用 MySQL 5.7 对应仓库) ############################
    # 由于官方的源在国内访问速度过慢,因此此处使用清华大学镜像
    cat > /etc/yum.repos.d/mysql-community.repo <<'EOF'
    [mysql-connectors-community]
    name=MySQL Connectors Community
    baseurl=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql-connectors-community-el7-$basearch/
    enabled=1
    gpgcheck=1
    gpgkey=https://repo.mysql.com/RPM-GPG-KEY-mysql
    
    [mysql-tools-community]
    name=MySQL Tools Community
    baseurl=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql-tools-community-el7-$basearch/
    enabled=1
    gpgcheck=1
    gpgkey=https://repo.mysql.com/RPM-GPG-KEY-mysql
    
    [mysql-5.6-community]
    name=MySQL 5.6 Community Server
    baseurl=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql-5.6-community-el7-$basearch/
    enabled=0
    gpgcheck=1
    gpgkey=https://repo.mysql.com/RPM-GPG-KEY-mysql
    
    [mysql-5.7-community]
    name=MySQL 5.7 Community Server
    baseurl=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql-5.7-community-el7-$basearch/
    enabled=1
    gpgcheck=1
    gpgkey=https://repo.mysql.com/RPM-GPG-KEY-mysql
    
    [mysql-8.0-community]
    name=MySQL 8.0 Community Server
    baseurl=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql-8.0-community-el7-$basearch/
    enabled=0
    gpgcheck=1
    gpgkey=https://repo.mysql.com/RPM-GPG-KEY-mysql
    EOF
    
    # 确认是否已成功安装
    #yum repolist enabled | grep "mysql.*-community.*"
    
    # 由于默认指定最新的版本(MySQL 8.0), 因此需要手动指定我们想安装的版本 (MySQL 5.7)
    #yum repolist all | grep mysql
    
    # 安装 yum-utils, 其提供了 yum-config-manager 命令
    #yum install -y yum-utils
    
    # 禁止8.0系列的子存储库
    #yum-config-manager --disable mysql80-community
    # 启用5.7系列的子存储库
    #yum-config-manager --enable mysql57-community
    # 或在安装时指定仓库 --disablerepo="*" --enablerepo="mysql57-community"
    #############################################################################
    
    
    
    
    
    
    ################################ 安装 ###################################
    # 安装MySQL
    ## 会自动安装依赖: mysql-community-client, mysql-community-common, mysql-community-libs, mysql-community-libs-compat
    yum install mysql-community-server
    
    # 启动mysql server
    systemctl start mysqld
    systemctl enable mysqld
    systemctl status mysqld
    #######################################################################
    
    
    
    
    
    
    ################################## 修改 root 密码 #########################
    # 查看初始密码
    # mysql server初始化时会创建账号 'root'@'localhost', 默认密码存放在错误日志中
    grep 'temporary password' /var/log/mysqld.log
    
    
    # mysql 5.7 的默认密码安全策略对密码复杂度有要求, 若是烦的话可以调低
    cat >> /etc/my.cnf <<'EOF'
    validate_password_policy=0
    EOF
    
    # 修改账号密码
    mysql -uroot -p
    # 临时调低密码安全策略强度
    mysql> set global validate_password_policy=0;
    mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY '新密码需包含大小写,数字,字符';
    
    # 创建新账号
    mysql> GRANT ALL ON *.* TO yjx@'%' IDENTIFIED BY '复杂密码';
    #######################################################################
    
    
    
    
    
    
    # 查看当前mysql编码
    show variables like 'character%';
    
    # 修改mysql server字符集
    # 修改 /etc/my.cnf
    # 设置
    #
    # [mysqld]
    # character_set_server=utf8
    # 
    # 设置完后重启mysqld
    
    # 不使用service来关闭mysqld的方法
    mysqladmin -uroot -p shutdown
    mysql_secure_installation 是用于设置root密码, 移除匿名用户等

    MySQL 5.7 请不要运行 mysql_secure_installation, 因为安装时已经默认执行过了.

    开启多实例

    关闭默认实例

    # 取消开机启动
    chkconfig mysqld off
    # 关闭默认mysqld
    service mysqld stop

    !!! mysql 5.7 以下没有 --initialize-insecure--initialize , 因此初始化数据库必须使用 mysql_install_db --datadir=【数据目录】

    mariadb 好像也没有 --initialize, 也需要用 mysql_install_db 来初始化数据库目录

    手动管理

    分别配置实例配置

    mkdir -p /data/mysql_3307
    mkdir -p /data/mysql_3308
    chown -R mysql:mysql /data/mysql_33*
    
    # 一份简单的配置文件
    # basedir 指的是mysqld的安装目录
    cat > /data/mysql_3307/my.cnf <<\EOF
    [mysqld]
    user        = mysql
    port        = 3307
    socket      = /data/mysql_3307/mysqld.sock
    pid-file    = /data/mysql_3307/mysqld.pid
    datadir     = /data/mysql_3307/data
    basedir     = /usr
    bind-address    = 0.0.0.0
    character_set_server    = utf8
    symbolic-links  = 0
    log_error   = /data/mysql_3307/error.log
    slow_query_log         = 1
    slow_query_log_file    = /data/mysql_3307/slow.log
    long_query_time = 2
    EOF
    
    # 复制配置文件
    sed "s/3307/3308/g" /data/mysql_3307/my.cnf > /data/mysql_3308/my.cnf
    
    # 初始化数据库
    # --initialize-insecure 生成无密码的root账号
    # --initialize 生成带随机密码的root账号
    # 注意参数顺序, 必须先指定 --defaults-file=配置文件, 否则会报错
    mysqld --defaults-file=/data/mysql_3307/my.cnf --initialize-insecure
    mysqld --defaults-file=/data/mysql_3308/my.cnf --initialize-insecure
    
    # 启动实例
    mysqld_safe --defaults-file=/data/mysql_3307/my.cnf &
    mysqld_safe --defaults-file=/data/mysql_3308/my.cnf &
    
    # 关闭实例
    mysqladmin -S /data/mysql_3307/mysqld.sock shutdown
    mysqladmin -S /data/mysql_3308/mysqld.sock shutdown
    
    # 连接实例
    mysql -uroot -p -S /data/mysql_3307/mysqld.sock
    # # 注意, 默认只有 root@localhost, 需自行创建 root@'%' 账号才能远程登录
    mysql -uroot -h192.168.190.100 -P 3308        

    mysqld_multi 管理(推荐)

    # 创建目录
    mkdir -p /data/mysql_3307
    mkdir -p /data/mysql_3308
    chown -R mysql:mysql /data/mysql_33*
    
    # 初始化数据库目录
    ## 读取配置文件初始化
    ###mysqld --defaults-file=/data/mysql_3309/my.cnf --initialize-insecure
    ###mysqld --defaults-file=/data/mysql_3310/my.cnf --initialize-insecure
    ## [推荐]不读取配置文件, 直接指定目录
    mysqld -u mysql --basedir=/usr --datadir=/data/mysql_3309 --initialize-insecure
    mysqld -u mysql --basedir=/usr --datadir=/data/mysql_3310 --initialize-insecure
    ## mysql 5.7 以下或 mariadb 应使用 mysql_install_db 来初始化
    #mysql_install_db --user=mysql --datadir=/data/mysql_3309
    #mysql_install_db --user=mysql --datadir=/data/mysql_3310
    
    # 直接修改 /etc/my.cnf 或新创建一份multi配置 /data/multi.cnf
    # 若是新创建multi配置, 则执行mysql_multi时需指定该配置文件
    cat > /etc/multi.cnf <<EOF
    [mysqld_multi]
    mysqld               = /usr/bin/mysqld_safe
    mysqladmin           = /usr/bin/mysqladmin
    
    [mysqld3309]
    user        = mysql
    port        = 3309
    server-id   = 3309
    socket      = /data/mysql_3309/mysqld.sock
    pid-file    = /data/mysql_3309/mysqld.pid
    datadir     = /data/mysql_3309
    basedir     = /usr
    log_error   = /data/mysql_3309/error.log
    log_bin            = /data/mysql/mysql-bin
    slow_query_log_file    = /data/mysql_3309/slow.log
    slow_query_log         = 1
    long_query_time = 2
    bind-address    = 0.0.0.0
    character_set_server    = utf8
    symbolic-links  = 0
    max_allowed_packet = 100m
    EOF
    
    [mysqld3310]
    user        = mysql
    port        = 3310
    server-id   = 3310
    socket      = /data/mysql_3310/mysqld.sock
    pid-file    = /data/mysql_3310/mysqld.pid
    datadir     = /data/mysql_3310
    basedir     = /usr
    log_error   = /data/mysql_3310/error.log
    log_bin            = /data/mysql/mysql-bin
    slow_query_log_file    = /data/mysql_3310/slow.log
    slow_query_log         = 1
    bind-address    = 0.0.0.0
    character_set_server    = utf8
    symbolic-links  = 0
    long_query_time = 2
    max_allowed_packet = 100m
    EOF
    
    # 默认会读取 /etc/my.cnf 文件, 因此若不使用该配置则应通过 --defaults-file 指定配置
    ## 启动实例 3309 和 3310, 支持使用连字符语法或用逗号分隔
    mysqld_multi --defaults-file=/etc/multi.cnf start 3309,3310
    ## 关闭实例
    mysqld_multi --defaults-file=/etc/multi.cnf stop 3309-3310
    ## 查看实例状态
    mysqld_multi --defaults-file=/etc/multi.cnf report 3309-3310
    !!! mysqld_multi 不支持 !include 或 !includedir

    eg. 采用 heartbeat + drbd + mysql 实现mysql高可用双机热备方案

    未实践

    MariaDB 和 MySQL 共存

    新增 MariaDB

    这里讨论的是在已有 MySQL 前提下通过编译安装 MariaDB

    参考:

    # 1. 下载二进制包
    wget http://nyc2.mirrors.digitalocean.com/mariadb//mariadb-10.2.31/bintar-linux-x86_64/mariadb-10.2.31-linux-x86_64.tar.gz
    tar -xvf mariadb-10.2.31-linux-x86_64.tar.gz
    mv mariadb-10.2.31-linux-x86_64 /usr/local/mariadb-10.2.31
    ln -s /usr/local/mariadb-10.2.31 /usr/local/mariadb
    
    # 2. 复制 my.cnf 配置文件
    cp /usr/local/mariadb/support-files/my-innodb-heavy-4G.cnf /data/mariadb/my.cnf
    ## 手动修改其他配置:
    ### port                 = 3307
    ### socket                 = /data/mariadb/mariadb.sock
    ### pid-file             = /data/mariadb/mariadb.pid
    ### datadir             = /data/mariadb
    ### basedir                = /usr/local/mariadb
    ### bind-address         = 0.0.0.0
    ### log_error             = /data/mariadb/error.log
    ### slow_query_log         = 1
    ### slow_query_log_file     = /data/mariadb/slow.log
    ### default-storage-engine = InnoDB
    vim /data/mariadb/my.cnf
    
    # 3. 复制 service 配置文件
    cp /usr/local/mariadb/support-files/mysql.server /etc/init.d/mariadb
    ## 手动修改文件内容
    ## # Provides: mariadb
    ## basedir="/usr/local/mariadb"
    ## datadir="/data/mariadb"
    ## lock_file_path="$lockdir/mariadb"
    ## mysqld_pid_file_path="/data/mariadb/mariadb.pid"
    ## $bindir/mysqld_safe --defaults-file="/data/mariadb/my.cnf" --datadir="$datadir" --pid-file="$mysqld_pid_file_path" "$@" &
    ## if $bindir/mysqladmin --defaults-file="/data/mariadb/my.cnf" ping >/dev/null 2>&1; then
    vim /etc/init.d/mariadb
    chmod +x /etc/init.d/mariadb
    
    # 4. 初始化数据库数据
    /usr/local/mariadb/scripts/mysql_install_db --defaults-file="/data/mariadb/my.cnf"
    
    # 5. 启动 mariadb
    service mariadb start
    
    # 6. 设置开机自启动
    chkconfig mariadb on
    
    # 7. Test
    mysql -e "SELECT VERSION();" --socket=/data/mariadb/mariadb.sock

    这里依旧使用 mysql 用户来运行 mariadb, 因此并未创建新的 mariadb 用户.

    此处假设 mariadb 相关数据保存在 /data/mariadb

    在实际操作中, 由于我并未将 my.cnf 放在和数据库数据同一个目录, 因此启动时一直出现读取旧的配置文件 /etc/my.cnf 的问题, 解决方式是修改了 /etc/init.d/mariadb 下的部分内容

    # 在此处指定解析的 my.cnf 文件, 否则会默认解析 /etc/my.cnf 文件读取其中的 datadir 配置...蛋疼
    parse_server_arguments `$print_defaults -c /data/mariadb/my.cnf $extra_args --mysqld mysql.server`

    若要安装其他版本的 MariaDB, 则需自行到 https://downloads.mariadb.org... 下载(注意是下载二进制包, 而不是源码包)

    主从复制

    目的:

    • 实时备份
    • 读写分离, 主写, 从读

    eg. MySQL 主从复制

    简单原理

    img

    1. Master的IO线程将操作记录到二进制日志(Binary log)中
    2. Slave的IO线程会同步拉取Master的二进制日志并写入本地中继日志(Relay log)
    3. Slave的SQL线程会从中继日志中读取操作并在当前实例上重放操作.

    image-20200831163801762

    配置及操作

    Master 配置文件

    log_bin        = /data/3306/mysql-bin
    server-id     = 1
    #expire_logs_days        = 10
    #max_binlog_size         = 100M
    必须打开 master 端的 Binary Log

    Slave 配置文件

    read-only    = 1
    log_bin        = /data/3307/mysql-bin
    server-id     = 2
    # 可选, 指定中继日志的位置和命名
    relay_log    = /data/3307/relay.log
    # 允许备库将其重放的事件也记录到自身的二进制日志中
    log_slave_updates    = 1

    1. 确认Master 开启了二进制日志

    mysql> SHOW MASTER STATUS;

    Tip. 锁表

    FLUSH TABLE WITH READ LOCK;
    
    UNLOCK TABLES;

    2. Master 创建专门用于主从复制的账号

    GRANT REPLICATION SLAVE,REPLICATION CLIENT ON *.* TO 'rep'@'192.168.100.%' IDENTIFIED BY '123456';
    复制账号在主库上只需要 REPLICATION SLAVE 权限,

    1.用来监控和管理复制的账号需要REPLICATION CLIENT 权限,并且针对这两种目的使用同一个账号更加容易(而不是为了两个目的各创建一个账号)。

    2.如果在主库上建立了账号,然后从主库将数据克隆到备库上时,备库也就设置好了-变成主库所需要的配置。这样后续有需要可以方便的交换主备库角色。

    3. 主-从 数据保持一致

    # 主库导出数据快照
    # 若表全都使用InnoDB引擎, 则可使用 --single-transaction 来代替 --lock-all-tables
    # --master-data=1 导出sql中会包含CHANGE MASTER语句(包含 binlog 的文件名及 pos)
    ## eg.    CHANGE MASTER TO MASTER_LOG_FILE='bin.000003', MASTER_LOG_POS=25239;
    # --master-data=2 导出CHANGE MASTER语句,但是会被注释(仅在日常备份时导出使用)
    mysqldump -uroot -p -S /data/3306/mysql.sock -A -F --master-data=1 --single-transaction > master.sql
    
    # 从库导入
    mysql -uroot -p -S /data/3307/mysql.sock < master.sql
    此处假设主数据库已经在使用, 而从数据库是新的, 因此需要先保持两边数据一致

    4. Slave 更改从库的连接参数

    # 尝试在此处不设置 MASTER_LOG_FILE,MASTER_LOG_POS, 结果后面 START SLAVE 后一直出错
    # 此处的 MASTER_LOG_FILE,MASTER_LOG_POS 可以在日志中查看
    mysql> CHANGE MASTER TO MASTER_HOST='192.168.190.100',MASTER_PORT=3309,MASTER_USER='rep',MASTER_PASSWORD='123456',MASTER_LOG_FILE='bin.000003', MASTER_LOG_POS=25239;
    
    # 确认一下配置文件正确
    cat /data/3307/data/master.info
    
    # 从库连接主库
    mysql> START SLAVE;
    # 确认连接正常
    mysql> SHOW SLAVE STATUS\G;

    管理

    -- 查看master的状态, 尤其是当前的日志及位置
    show master status; 
    
    -- 查看slave的状态. 
    show slave status; 
    
    -- 重置slave状态,用于删除SLAVE数据库的relaylog日志文件,并重新启用新的relaylog文件.会忘记 主从关系,它删除master.info文件和relay-log.info 文件
    reset slave; 
    
    -- 启动slave 状态(开始监听msater的变化)
    start slave; 
    
    -- 暂停slave状态;
    stop slave; 
    
    -- 跳过导致复制终止的n个事件,仅在slave线程没运行的状况下使用
    set global sql_slave_skip_counter = n; 
    李玥老师: 如果你配置了多个从库,推荐你使用“HAProxy+Keepalived”这对儿经典的组合,来给所有的从节点做一个高可用负载均衡方案,既可以避免某个从节点宕机导致业务可用率降低,也方便你后续随时扩容从库的实例数量。因为 HAProxy 可以做 L4 层代理,也就是说它转发的是 TCP 请求,所以用“HAProxy+Keepalived”代理 MySQL 请求,在部署和配置上也没什么特殊的地方,正常配置和部署就可以了。 -- 后端存储实战课

    MySQL 关键参数配置

    以下参数中, 部分参数只对InnoDB引擎有效

    参数名含义建议
    max_connections最大客户端连接数默认好像是 151
    innodb_buffer_pool_sizeInnodb存储引擎缓存池大小建议设置为物理内存的 80% 左右<br/>默认是128MB左右
    innodb_file_per_tableInnodb表包含两部分: 表结构定义(.frm文件)和数据, 若该参数值为0, 则表的数据就存在共享表空间 , 若只为1, 则单独存放在一个.ibd的文件中.
    若放在共享表空间, drop 表时, 空间是不会回收的.
    设为 1, 表示每个表使用独立表空间(.ibd文件), 方便回收空间.
    MySQL 5.6.6 开始默认为1
    innodb_log_file_size事务日志(Redo Log)单个大小(文件 ib_logfile*总的日志大小足以容纳1个小时的量
    默认是 5MB 左右
    innodb_log_files_in_group事务日志数量默认是 2
    innodb_flush_logs_at_trx_commit事务提交时写日志的方式
    0: 每秒将日志持久化到磁盘. 数据库崩溃时丢失最多1秒数据
    1: 默认, 每次事务提交都将日志持久化到磁盘, 最安全, 性能最一般.
    2: 每次事务提交都写入磁盘(指磁盘缓冲区), 具体何时持久化到磁盘则由操作系统控制. 系统崩溃时丢失数据
    不推荐设为 0.
    对性能要求较高可以设置为 2.
    默认好像是 1
    sync_binlogMySQL 控制写入 BinLog 的方式
    0 : 每次事务提交写入磁盘缓冲区, 由操作系统控制何时持久化
    N: 每进行N个事务提交后持久化到磁盘, 当N=1时最安全但性能最差
    5.7.7及以后默认是1, 之前默认是0.
    log_bin设置 bin_log 文件存储路径默认好像是不写 bin log.
    mysql 自带 mysqlslap 压测工具, 可以自行测试, 个人未使用过.

    计算合适的 Redo Log 大小

    调整Redo Log大小一般是通过修改 innodb_log_file_size 配置.

    1. 在业务高峰期, 计算出1分钟写入的redo log量

      # 只显示结果中 Log 相关
      > pager grep Log;
      # 查看日志位置, sleep 
      > show engine innodb status\G; select sleep(60); show engine innodb status\G;
      # 取消结果过滤
      > nopager;

      通过查看两次的 Log sequence number 值, 计算出差值, 单位是字节.

    2. 评估1个小时的 redo log 量

      将上述值乘以 60 得到合理的 Redo Log 大小. 因此 innodb_log_file_size 推荐设置成 估算的Redo Log 大小 / 日志文件数(innodb_log_files_in_group)

    3. 正常关闭 mysql
    4. 修改配置中 innodb_log_file_size 值, 并将数据目录下的所有 ib_logfile* 文件move走(先不删, 防止出问题无法启动)
    5. 启动 mysql, 确认没问题后就可以删除 ib_logfile*
    备注: 有看到说 mysql5.6 版本及以后无需手动删除 ib_logfile* 文件.

    如果Redo Log太小, 会导致频繁刷脏页??

    太大会导致故障恢复时恢复时间太长(甚至长到不可接受的程度)

    pager 可以理解为利用 linux 的管道来处理结果, 比如 pager less 可以方便浏览大量结果集, 或者是计算结果校验和(比较两个查询结果是否完全一致) pager md5sum

    示例

    # 仅查看锁状态那一行的数据
    > pager grep "lock(s)";
    > show engine innodb status;    
    > nopaper;

    基本使用

    USE 数据库名;        -- 选择数据库
    
    SHOW DATABASES;        -- 查看数据库列表
    
    SHOW TABLES;        -- 查看当前数据库内的表的列表
    
    SHOW COLUMNS FROM `表名`;        -- 查看表结构
    
    SHOW STATUS;                    -- 用于显示广泛的服务器状态信息
    
    SHOW CREATE DATABASE `数据库名`;    -- 显示创建特定数据库的MySQL语句
     
    SHOW CREATE TABLE `表名`;            -- 显示创建表的MySQL语句;
    
    SHOW GRANTS;                -- 显示授权指定用户的安全权限, 默认是当前用户
    SHOW GRANTS FOR 用户@"..."    -- eg. root@'%' 或 root@localhost 或 root@0.0.0.0
    
    SHOW ERRORS;        -- 用来显示服务器错误消息
    SHOW WARNINGS;    -- 用来显示服务器警告消息
    • DESCRIBE 表名

      等同于 SHOW COLUMNS FROM 表名, MySQL独有

    • STATUS

      快速查看当前实例状态, eg.

      --------------
      mysql  Ver 14.14 Distrib 5.7.22, for Linux (x86_64) using  EditLine wrapper
      
      Connection id:        28
      Current database:    mysql_learn
      Current user:        root@localhost
      SSL:            Not in use
      Current pager:        stdout
      Using outfile:        ''
      Using delimiter:    ;
      Server version:        5.7.22-0ubuntu18.04.1 (Ubuntu)
      Protocol version:    10
      Connection:        127.0.0.1 via TCP/IP
      Insert id:        114
      Server characterset:    latin1
      Db     characterset:    latin1
      Client characterset:    utf8
      Conn.  characterset:    utf8
      TCP port:        3306
      Uptime:            7 days 23 hours 29 min 13 sec
      
      Threads: 6  Questions: 817  Slow queries: 0  Opens: 205  Flush tables: 1  Open tables: 150  Queries per second avg: 0.001
      --------------

    数据类型

    数值数据类型

    <u>有符号或无符号</u>

    所有数值数据类型(除 BIT 和 BOOLEAN 外)都可以有符号或无符号。有符号数值列可以存储正或负的数值,无符号数值列只能存储正数。默认情况为有符号,但如果你知道自己不需要存储负值,可以使用 UNSIGNED 关键字,这样做将允许你存储两倍大小的值。

    类型大小范围(有符号)范围(无符号)用途
    TINYINT1 字节(-128,127)(0,255)小整数值
    SMALLINT2 字节(-32 768,32 767)(0,65 535)大整数值
    MEDIUMINT3 字节(-8 388 608,8 388 607)(0,16 777 215)大整数值
    INT或INTEGER4 字节(-2 147 483 648,2 147 483 647)(0,4 294 967 295)大整数值
    BIGINT8 字节(-9,223,372,036,854,775,808,9 223 372 036 854 775 807)(0,18 446 744 073 709 551 615)极大整数值
    FLOAT4 字节(-3.402 823 466 E+38,-1.175 494 351 E-38),0,(1.175 494 351 E-38,3.402 823 466 351 E+38)0,(1.175 494 351 E-38,3.402 823 466 E+38)单精度 浮点数值
    DOUBLE8 字节(-1.797 693 134 862 315 7 E+308,-2.225 073 858 507 201 4 E-308),0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308)0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308)双精度 浮点数值
    DECIMALDECIMAL(M,D) ,如果M>D,为M+2否则为D+2依赖于M和D的值依赖于M和D的值小数值

    关键字INT是INTEGER的同义词,关键字DEC是DECIMAL的同义词。

    int(m) 里的m是表示 SELECT 查询结果集中的显示宽度,并不影响实际的取值范围.

    FLOAT 类型

    • float(m,d)

    DOUBLE 类型

    • double(m,d)

    DECIMAL 类型

    • 精确值
    • decimal(m,d) m<=65, d<=30, m是总位数, d是小数位数

    字符串数据类型

    类型大小用途
    CHAR0-255字节定长字符串
    VARCHAR0-65535 字节变长字符串
    TINYBLOB0-255字节不超过 255 个字符的二进制字符串
    TINYTEXT0-255字节短文本字符串
    BLOB0-65 535字节二进制形式的长文本数据
    TEXT0-65 535字节长文本数据
    MEDIUMBLOB0-16 777 215字节二进制形式的中等长度文本数据
    MEDIUMTEXT0-16 777 215字节中等长度文本数据
    LONGBLOB0-4 294 967 295字节二进制形式的极大文本数据
    LONGTEXT0-4 294 967 295字节极大文本数据

    CHAR 类型

    • char(n) 若存入字符数小于n,则以空格补于其后,查询之时再将空格去掉。所以 char 类型存储的字符串末尾不能有空格
    • 效率比 VARCHAR 高点

    VARCHAR 类型

    • 长度设置, 在MySQL 5之后是按<u>字符数</u>, 而不是字节数.
    • varchar(20) 可以存储20个<u>字符</u>
    • varchar 头部会占用 1个(n<=255)或2个字节(n>255)保存字符串长度, 若值可设为 null, 则还需要一个1字节记录null, 因此保存utf8编码的字符串最多可存 (65535 - 3)/3 = 21844
    • 若是utf8编码
    • 效率比 TEXT 高

    TEXT 类型

    • 创建索引时要指定前多少个字符
    • 不能有默认值
    • text 头部会占用 2个字节来保存长度

    日期和时间类型

    类型大小 (字节)范围格式用途
    DATE31000-01-01/9999-12-31YYYY-MM-DD日期值
    TIME3'-838:59:59'/'838:59:59'HH:MM:SS时间值或持续时间
    YEAR11901/2155YYYY年份值
    DATETIME81000-01-01 00:00:00/9999-12-31 23:59:59YYYY-MM-DD HH:MM:SS混合日期和时间值
    TIMESTAMP41970-01-01 00:00:00/2038结束时间是第 2147483647 秒,北京时间 2038-1-19 11:14:07,格林尼治时间 2038年1月19日 凌晨 03:14:07YYYYMMDD HHMMSS混合日期和时间值,时间戳

    每个时间类型有一个有效值范围和一个"零"值,当指定不合法的MySQL不能表示的值时使用"零"值。

    TIMESTAMP类型有专有的自动更新特性

    • update_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
    • 保存毫秒数(类似php 的 microtime(true))
    • timestamp列默认not null。没有显式指定nullable,那么default null不合法
    • mysql不会给timestamp设置默认值,除非显式设置default约束或者可空null。特例:mysql会给表第一个timestamp类型的字段同时添加default current_timestampon update timestamp
    • 其他情况均会引起不合法报错
    • ↑ 总结: 最好手动设置 NULL 以免出错

    日期函数:

    • CURRENT_TIMESTAMP, CURRENT_TIMESTAMP()NOW() 的同义词
    • SYSDATE() 获取的是函数执行时的时间, NOW()获取的是执行语句时的<u>开始时间</u>.
    • CURDATE() 今日

    二进制数据类型

    1559035285382

    检索数据

    SELECT 语句

    子句的书写顺序很重要:

    SELECT ... FROM ... JOIN ... ON ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT ...

    执行顺序:

    FROM(包含JOIN) > WHERE > GROUP BY > 聚集函数字段计算 > HAVING > SELECT 的字段 > ORDER BY > LIMIT

    举例:

    SELECT player.team_id, count(*) as num    -- 顺序5
    FROM player JOIN team ON player.team_id = team.team_id    -- 顺序1
    WHERE height > 1.80 -- 顺序2
    GROUP BY player.team_id    -- 顺序3
    HAVING num > 2    -- 顺序4
    ORDER BY num DESC -- 顺序6
    LIMIT 2; -- 顺序7

    基本检索

    SELECT `列名1`,`列名2` FROM `表名`;    -- 检索指定列
    SELECT * FROM `表名`;                -- 检索所有列

    DISTINCT关键字: 只返回不同的值

    SELECT DISTINCT `列名1` FROM `表名`;        -- 只返回唯一的 `列名1`行
    SELECT DISTINCT `列名1`,`列名2` FROM `表名`;    -- DISTINCT应用于所有的列, 返回 (`列名1`, `列名2`) 不同的行

    LIMIT关键字: 限制结果

    SELECT * FROM `表名` LIMIT <limit>;        -- 限制返回最多 <limit> 条, 等同 0,<limit>
    SELECT * FROM `表名` LIMIT <offset>,<limit>;    -- 略过前 <offset> 条记录, 返回最多 <limit> 条
    SELECT * FROM `表名` LIMIT <limit> OFFSET <offset>;    -- MySQL 5 语法糖

    完全限定名

    SELECT `表名`.`列名` FROM `数据库名`.`表名`;        -- 有一些情形需要完全限定名

    ORDER 子句

    SELECT * FROM `表名` ORDER BY `列名` <dir>;        -- <dir> 默认是 ASC, 可指定 DESC.
    SELECT * FROM `表名` ORDER BY `列名1` <dir1>,`列名2` <dir2>;    -- 仅在`列名1`等值时才按`列名2`排序

    排序方式可选:

    • ASC 升序(默认), ASCENDING
    • DESC 降序
    • ORDER BY 字句使用的排序列不一定是显示的列, 这是允许的.
    • ORDER BY
    • 对文本数据排序时, 大小写的排序取决于数据库如何设置 COLLATE

    WHERE 子句

    操作符说明
    =等于
    <>不等于
    !=不等于
    <小于
    <=小于等于
    >大于
    >=大于等于
    BETWEEN ... AND ...在指定的两个值之间
    ... IS NULL没有值
    ... IS NOT NULL非空, 有值
    IN (…)在元组
    NOT IN (...)不在元组
    • 一个列不包含值时, 称其为空值NULL

    AND, OR 操作符

    计算次序: AND 运算符优先级更高, 在处理OR操作符前会先处理AND操作符, 举例:

    SELECT prod_name,prod_price,vend_id FROM products WHERE vend_id=1002 OR vend_id=1003 AND prod_price>=10;
    
    -- 等价于
    
    SELECT prod_name,prod_price,vend_id FROM products WHERE (vend_id=1002) OR (vend_id=1003 AND prod_price>=10);

    IN操作符

    SELECT * FROM `表名` WHERE `列名` IN (值1, 值2, ..., 值N);

    IN 功能等同于 OR, 那么为什么用 IN:

    • 语法清晰直观
    • 执行更快
    • <u>可以包含其他SELECT 语句, 得能够更动态地建立 WHERE 子句</u>

    在使用 IN 或 EXISTS 时, 谨记一个原则: 小表驱动大表:即小的数据集驱动大的数据集。

    举例, 有两个表

    • 表 A 是小表, cc 字段有索引
    • 表 B 是大表, cc 字段有索引
    -- 用到了 B 表上 cc 索引
    SELECT * FROM B WHERE cc IN (SELECT cc FROM A);
    
    -- 用到了 B 表上的 cc 索引
    SELECT * FROM A WHERE EXISTS(SELECT 1 FROM B WHERE B.cc = A.cc);

    NOT操作符

    否定它之后所跟的任何条件

    ... WHERE `列名` NOT IN (值1, ..., 值N);
    
    ... WHERE `列名` IS NOT NULL;
    
    ... WHERE `列名` NOT BETWEEN 1 AND 10;
    
    ... WHERE `列名` NOT EXISTS (...);

    NOT 在复杂的WHERE字句中很有用.

    例如,在与 IN 操作符联合使用时, NOT 使找出与条件列表不匹配的行非常简单。

    MySQL支持使用 NOT 对 IN 、 BETWEEN 和
    EXISTS子句取反,这与多数其他 DBMS允许使用 NOT 对各种条件
    取反有很大的差别。

    LIKE操作符

    通配符(wildcard)

    通配符含义
    %任何字符出现任意次数(0,1,N), 注意不匹配 NULL
    _匹配单个字符(1)

    like 匹配完整的列.

    通配符置于搜索模式开始处, 不会使用索引.

    注意NULL 虽然似乎 % 通配符可以匹配任何东西,但有一个例外,即 NULL 。即使是 WHERE prod_name LIKE '%' 也不能匹配用值 NULL 作为产品名的行。
    SELECT * FROM `表名` WHRER `列名` LIKE `a%d`;
    
    SELECT * FROM `表名` WHRER `列名` LIKE `a_cd`;

    REGEXP 正则表达式

    MySQL仅支持多数正则表达式实现的一个很小的子集。

    eg. 不支持 \d, \s
    SELECT * FROM `表名` WHERE `列名` REGEXP '^[0-9]';    -- 匹配数字开头的, 默认不区分大小写
    
    SELECT * FROM `表名` WHERE `列名` REGEXP BINARY "正则表达式";    -- 区分大小写

    多数正则表达式实现使用单个反斜杠转义特殊字符,以便能使用这些字符本身。但MySQL要求两个反斜杠(MySQL自己解释一个,正则表达式库解释另一个)。

    转义 . 时, 需使用 \\. 而非 \.

    1558600943297

    1558600935224

    1558600968226

    1558601019873

    计算字段

    计算字段并不实际存在于数据库表中。计算字段是运行时在 SELECT 语句内创建的。
    字段(field) 基本上与列(column)的意思相同,经常互换使用,不过数据库列一般称为列,而术语字段通常用在计算字段的连接上。

    AS 别名

    别名(alias)是一个字段或值的替换名。

    SELECT `列名` AS `别名` FROM `表名`;

    别名的其他常见用途:

    • 在实际的表列名包含不符合规定的字符(如空格)时重新命名它
    • 在原来的名字含混或容易误解时扩充它,等等。

    算数运算

    1558601948352

    圆括号可用来区分优先顺序。

    控制表达式

    CASE WHEN 表达式

    update table  
    set 字段1=case     
        when 条件1 then 值1       
        when 条件2 then 值2      
        else 值3      
        end     
    where …… 
    select 字段1, 字段2,       
        case 字段3     
        when 值1 then 新值1       
        when 值2 then 新值2
        else 新值3
        end as 重新命名字段3的名字       
    from table      
    where ……      
    order by ……

    IF(cond, trueExpr, falseExpr) 表达式

    select *, if(sex=1,'男','女') as ssex from user;

    IF ELSE 流程控制

    IF search_condition THEN 
        statement_list  
    [ELSEIF search_condition THEN]  
        statement_list ...  
    [ELSE 
        statement_list]  
    END IF 

    该语句更多的是应用过在编写存储过程

    1545615382894

    IFNULL(condExpr, trueExpr) 表达式

    若 condExpr 为 null, 则返回 trueExpr, 否则返回该 condExpr.

    -- 查询结果: 10
    SELECT IFNULL(NULL, 10);
    
    -- 如果 price 字段有 null, 会导致最终结果值为 null
    -- 因此需使用 IFNULL 来处理
    SELECT SUM(IFNULL(price, 0)) from order;

    函数

    函数没有SQL的可移植性强, 几乎每种主要的DBMS的实现都支持其他实现不支持的函数,而且有时差异还很大。

    字符串处理

    concat()字符串拼接

    SELECT concat(`列名1`, '(', `列名2`, ')') FROM `表名`;    -- 组成: `列名1`(`列名2`)
    MySQL的不同之处 多数DBMS使用 + 或 || 来实现拼接,MySQL则使用 Concat() 函数来实现。当把SQL语句转换成MySQL语句时一定要把这个区别铭记在心

    1558602282966

    • LOCATE(substr,str)

      返回子串 substr 在字符串 str 中第一次出现的位置。如果子串 substr 在 str 中不存在,返回值为 0:

    • LOCATE(substr,str,pos)

      多字节安全

      返回子串 substr 在字符串 str 中的第 pos 位置后第一次出现的位置。如果 substr 不在 str 中返回 0

    • substr(str,pos[, len])

      等同于 substring

      pos参数 表示从该位置(包含)开始截取

    • substring_index

      返回从字符串str分隔符delim中的计数发生前的子字符串。 如果计数是正的,则返回一切到最终定界符(从左边算起)的左侧。如果count为负,则从右边开始截取

    • concat_ws(separator, str1, str2, ...)

      使用指定分隔符拼接参数, 忽略NULL

    • group_concat(...)

      函数返回一个字符串结果,该结果由分组中的值连接组合而成。

    • find_in_set(str, strlist)

      返回子串在字符串(以 , 分隔)中的位置.

      典型应用:

      • 查找一组 id 对应的记录, 需按照数组中 id 顺序来返回.

        // id 数组: [2, 14, 7, 13]
        SELECT * FROM `表名`
        WHERE
            `id` in (2,14,7,14)
        ORDER BY
            FIND_IN_SET(`id`, "2,14,7,13");

        上述的的逗号分隔的字符串通常是在外部由调用者构建的.

    !!! 注意 MySQL字符串位置是从1开始

    日期和时间

    1558603084142

    函数说明示例
    DATE_SUB()日期减少,类似 DATE_ADDdate_sub('2016-08-01',interval 1 day)

    UNIX_TIMESTAMP(date)

    日期或日期字符串 -> 时间戳

    -- 时间戳: 1610533706
    select unix_timestamp(Now());
    
    -- 1451664000
    select unix_timestamp('2016-01-02');

    FROM_UNIXTIME(timestamp[, format])

    时间戳转日期

    -- 日期: 2021-01-13 18:28:26
    select from_unixtime(1610533706);
    
    -- 日期: 2021-01-13
    select from_unixtime(1610533706, "%Y-%m-%d");

    DATE_FORMAT(date,format)

    日期转字符串.

    format 格式

    • %M 月名字(January……December)
    • %W 星期名字(Sunday……Saturday)
    • %D 有英语前缀的月份的日期(1st, 2nd, 3rd, 等等。)
    • %Y 年, 数字, 4 位
    • %y 年, 数字, 2 位
    • %a 缩写的星期名字(Sun……Sat)
    • %d 月份中的天数, 数字(00……31)
    • %e 月份中的天数, 数字(0……31)
    • %m 月, 数字(01……12)
    • %c 月, 数字(1……12)
    • %b 缩写的月份名字(Jan……Dec)
    • %j 一年中的天数(001……366)
    • %H 小时(00……23)
    • %k 小时(0……23)
    • %h 小时(01……12)
    • %I 小时(01……12)
    • %l 小时(1……12)
    • %i 分钟, 数字(00……59)
    • %r 时间,12 小时(hh:mm:ss [AP]M)
    • %T 时间,24 小时(hh:mm:ss)
    • %S 秒(00……59)
    • %s 秒(00……59)
    • %p AM或PM
    • %w 一个星期中的天数(0=Sunday ……6=Saturday )
    • %U 星期(0……52), 这里星期天是星期的第一天
    • %u 星期(0……52), 这里星期一是星期的第一天
    • %% 一个文字“%”。
    -- 日期: 2021-01-13
    SELECT DATE_FORMAT(NOW(), "%Y-%m-%d");

    STR_TO_DATE(str, format)

    字符串转日期

    select str_to_date('2016-01-02', '%Y-%m-%d %H');

    数值处理

    1558603490365

    聚集函数

    1558603562124

    标准偏差 MySQL还支持一系列的标准偏差聚集函数

    • AVG(), MAX(), MIN(), SUM() 函数忽略列值为 NULL 的行
    • COUNT(*) 对表中行的数目进行计数,不管表列中包含的是空值( NULL )还是非空值
    • COUNT(column) 对特定列中具有值的行进行计数,忽略NULL 值

      -- 若 score < 90, 则返回 null, 此时 count 不会计数
      select count(`score`>=90 or null) from scores;
      select count(if(`score` >= 90, `score`, 0)>=90 or null) from scores;

    上述聚集函数可配合 DISTINCT 来使用

    SELECT COUNT(DISTINCT `列名`) FROM `表名`;
    SELECT AVG(DISTINCT `列名`) FROM `表名`;
    SELECT SUM(DISTINCT `列名`) FROM `表名`;

    COUNT 性能问题

    对于MyISAM引擎, 每张表保存了一个总行数的字段, 统计总行数直接返回该字段值即可.

    对于InnoDB引擎, 由于MVCC(多版本并发控制)的存在, 因此每次都需要全表扫描, 这里只讨论InnoDB引擎需要注意的点.

    对于统计表的总行数, 从效率角度看从高到低顺序如下:

    1. COUNT(*)

      优化器做了语义优化: 取行数

      Innodb遍历整张表, 但不取值, 直接计数.

    2. COUNT(1)

      Innodb遍历整张表, 但不取值,返回空行, server层对于返回的每一行自行放入一个 1, 并按行累加.

    3. COUNT(主键id)

      Innodb会遍历整张表, 取出每一行的主键id返回给server层, server层拿到主键id后按行累加.

    4. COUNT(字段)

      Innodb遍历整张表, 取出每一行的该字段返回给server层, server层拿到该字段后, 此时分2种情况:

      • 若该字段允许 null, 则需要先判断一下
      • 若该字段 not null, 则直接按行累加

    COUNT(字段) < COUNT(主键id) < COUNT(1)COUNT(*)

    对于上述的 COUNT, Innodb 会优先考虑使用最小的索引树(前提是要有COUNT所需的字段), 这样扫描的页比较少.

    字段允许为null的二级索引, 它的主键是存在的, 因此也可以用于统计总行数.

    优化考虑:

    1. 尽量使用 COUNT(*)
    2. 对于较为频繁的 COUNT 操作, 可以考虑找一个字段长度最小的对其建立索引, 以提高 COUNT 效率.
    3. 对于非常频繁的 COUNT 操作, 同样记录数多, 可以考虑使用缓存系统来保存计数(eg. Redis), 这种方式会存在一定程度上的不一致性(包括计数丢失和计数不精确), 如果能接受的话这是一种很好的方案,如果不能接受, 则可以考虑下面的这个方案.
    4. 对于非常频繁的 COUNT 操作, 同样记录数多, 也可以考虑使用数据库来保存计数, 通过建立一个单独的计数表, 每次insert或delete的同时在同一个事物中对该计数的记录做修改. 但有一些点要注意:

      • 考虑到锁对性能的影响, 因此修改计数记录的操作应放在事务的最后面来操作.
      • 同样考虑到锁对性能的影响, 可以将计数记录扩展到N条, 每次随机选取一条来操作. 最后统计总行数时, 再选取所有记录进行 SUM 操作.

    类型转换函数

    MySQL 提供了以下数据类型转换函数

    1. CAST()函数

      CAST(value as type) 就是CAST(xxx AS 类型)

    2. CONCERT()函数

      CONVERT(value, type) 就是CONVERT(xxx,类型)

    支持的类型如下:

    1. 二进制: BINARY
    2. 字符型,可带参数 : CHAR()
    3. 日期 : DATE
    4. 时间: TIME
    5. 日期时间型 : DATETIME
    6. 浮点数 : DECIMAL
    7. 整数 : SIGNED
    8. 无符号整数 : UNSIGNED

    其他函数-待整理

    CHAR_LENGTH(str)
            返回值为字符串str 的长度,长度的单位为字符。一个多字节字符算作一个单字符。
            对于一个包含五个二字节字符集, LENGTH()返回值为 10, 而CHAR_LENGTH()的返回值为5。
    
        CONCAT(str1,str2,...)
            字符串拼接
            如有任何一个参数为NULL ,则返回值为 NULL。
        CONCAT_WS(separator,str1,str2,...)
            字符串拼接(自定义连接符)
            CONCAT_WS()不会忽略任何空字符串。 (然而会忽略所有的 NULL)。
    
        CONV(N,from_base,to_base)
            进制转换
            例如:
                SELECT CONV('a',16,2); 表示将 a 由16进制转换为2进制字符串表示
    
        FORMAT(X,D)
            将数字X 的格式写为'#,###,###.##',以四舍五入的方式保留小数点后 D 位, 并将结果以字符串的形式返回。若  D 为 0, 则返回结果不带有小数点,或不含小数部分。
            例如:
                SELECT FORMAT(12332.1,4); 结果为: '12,332.1000'
        INSERT(str,pos,len,newstr)
            在str的指定位置插入字符串
                pos:要替换位置其实位置
                len:替换的长度
                newstr:新字符串
            特别的:
                如果pos超过原字符串长度,则返回原字符串
                如果len超过原字符串长度,则由新字符串完全替换
        INSTR(str,substr)
            返回字符串 str 中子字符串的第一个出现位置。
    
        LEFT(str,len)
            返回字符串str 从开始的len位置的子序列字符。
    
        LOWER(str)
            变小写
    
        UPPER(str)
            变大写
    
        LTRIM(str)
            返回字符串 str ,其引导空格字符被删除。
        RTRIM(str)
            返回字符串 str ,结尾空格字符被删去。
        SUBSTRING(str,pos,len)
            获取字符串子序列
    
        LOCATE(substr,str,pos)
            获取子序列索引位置
    
        REPEAT(str,count)
            返回一个由重复的字符串str 组成的字符串,字符串str的数目等于count 。
            若 count <= 0,则返回一个空字符串。
            若str 或 count 为 NULL,则返回 NULL 。
        REPLACE(str,from_str,to_str)
            返回字符串str 以及所有被字符串to_str替代的字符串from_str 。
        REVERSE(str)
            返回字符串 str ,顺序和字符顺序相反。
        RIGHT(str,len)
            从字符串str 开始,返回从后边开始len个字符组成的子序列
    
        SPACE(N)
            返回一个由N空格组成的字符串。
    
        SUBSTRING(str,pos) , SUBSTRING(str FROM pos) SUBSTRING(str,pos,len) , SUBSTRING(str FROM pos FOR len)
            不带有len 参数的格式从字符串str返回一个子字符串,起始于位置 pos。带有len参数的格式从字符串str返回一个长度同len字符相同的子字符串,起始于位置 pos。 使用 FROM的格式为标准 SQL 语法。也可能对pos使用一个负值。假若这样,则子字符串的位置起始于字符串结尾的pos 字符,而不是字符串的开头位置。在以下格式的函数中可以对pos 使用一个负值。
    
            mysql> SELECT SUBSTRING('Quadratically',5);
                -> 'ratically'
    
            mysql> SELECT SUBSTRING('foobarbar' FROM 4);
                -> 'barbar'
    
            mysql> SELECT SUBSTRING('Quadratically',5,6);
                -> 'ratica'
    
            mysql> SELECT SUBSTRING('Sakila', -3);
                -> 'ila'
    
            mysql> SELECT SUBSTRING('Sakila', -5, 3);
                -> 'aki'
    
            mysql> SELECT SUBSTRING('Sakila' FROM -4 FOR 2);
                -> 'ki'
    
        TRIM([{BOTH | LEADING | TRAILING} [remstr] FROM] str) TRIM(remstr FROM] str)
            返回字符串 str , 其中所有remstr 前缀和/或后缀都已被删除。若分类符BOTH、LEADIN或TRAILING中没有一个是给定的,则假设为BOTH 。 remstr 为可选项,在未指定情况下,可删除空格。
    
            mysql> SELECT TRIM('  bar   ');
                    -> 'bar'
    
            mysql> SELECT TRIM(LEADING 'x' FROM 'xxxbarxxx');
                    -> 'barxxx'
    
            mysql> SELECT TRIM(BOTH 'x' FROM 'xxxbarxxx');
                    -> 'bar'
    
            mysql> SELECT TRIM(TRAILING 'xyz' FROM 'barxxyz');
                    -> 'barx'

    分组

    GROUP BY ... HAVING ...

    创建分组

    聚集 配合 分组 GROUP BY 子句指示MySQL分组数据,然后对每个组而不是整个结果集进行聚集。

    使用 GROUP BY重要规定

    • ??? GROUP BY 子句可以包含任意数目的列。这使得能对分组进行嵌套,为数据分组提供更细致的控制。
    • ??? 如果在 GROUP BY 子句中嵌套了分组,数据将在最后规定的分组上进行汇总。换句话说,在建立分组时,指定的所有列都一起计算(所以不能从个别的列取回数据)。
    • <u>GROUP BY 子句中列出的每个列都必须是检索列或有效的表达式(但不能是聚集函数)。如果在 SELECT 中使用表达式,则必须在GROUP BY 子句中指定相同的表达式。不能使用别名。</u>
    • 除聚集计算语句外, SELECT 语句中的每个列都必须在 GROUP BY 子句中给出。
    • 如果分组列中具有 NULL 值,则 NULL 将作为一个分组返回。如果列中有多行 NULL 值,它们将分为一组。
    • GROUP BY 子句必须出现在 WHERE 子句之后, ORDER BY 子句之前。

    1558604730593

    ??

    想要SELECT 非分组字段

    SQL92以及更早的SQL标准中不允许查询除了GROUP BY之外的非聚合的列 .

    https://dev.mysql.com/doc/ref...

    在MYSQL的5.7.5以及以上版本默认的设置是:ONLY_FULL_GROUP_BY ,该设置则约束查询必须是聚合的列。但是在其版本之前则允许查询非聚合的列。

    如果在开启 ONLY_FULL_GROUP_BY 但是还想查询非聚合的列可以使用ANY_VALUE(非聚合列)进行查询,ANY_VALUE参考文档。还有一种情况开启ONLY_FULL_GROUP_BY时,如果GROUP BY是主键或者 unique NOT NULL 时是可以查询非聚合的列的,原因是此时分组的key是主键,则每一个分组只有一条数据,因此是可以进行查询非聚合的列的。最后对于高于5.7.5的版本如果想查询非聚合的列可以关闭ONLY_FULL_GROUP_BY 属性,即:

    set sql_mode ='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';
    这部分参考
    SELECT name, ANY_VALUE(address), MAX(age) FROM t GROUP BY name;

    过滤分组

    HAVING 过滤的是分组

    WHERE 过滤的是行
    • HAVING 可以使用别名

    HAVING 和 WHERE 的差别

    这里有另一种理解方法, WHERE 在数据分组前进行过滤, HAVING 在数据分组后进行过滤。这是一个重要的区别, WHERE 排除的行不包括在分组中。这可能会改变计算值,从而影响 HAVING 子句中基于这些值过滤掉的分组。

    子查询

    根据子查询执行的次数, 对子查询进行分类:

    1. 非关联子查询

      查询不依赖外层(即与主查询无关), 该子查询只需执行一次, 得到的数据结果可以作为外层查询的条件直接使用.

    2. 关联子查询

      查询时依赖外层(用到外层表数据, 与主查询相关), 因此每次外层查询都要根据外层查询结果再进行子查询, 该子查询需要执行多次.

    在实际使用时由于性能的限制,不能嵌套太多的子查询。

    eg. 返回订购产品 TNT2 的客户列表

    select * from customers
    where cust_id in (select distinct cust_id 
                      from orders 
                      where order_num in (select order_num
                                          from orderitems
                                          where prod_id="TNT2")
                     );

    子查询的关键字

    存在性子查询

    • EXISTS

      判断条件是否满足, 满足为True, 否则为False

    集合比较子查询

    • IN

      判断是否在集合中

    • ANY

      必须与比较操作符一起使用, 与子查询中<u>任意值</u>比较

      SELECT * FROM A WHERE A.cc > ANY (SELECT cc FROM B);
    • SOME

      是ANY的别名, 等价于 ANY, 一般常用 ANY

    • ALL

      必须与比较操作符一起使用, 与子查询中<u>所有值</u>比较

      SELECT * FROM A WHERE A.cc > ALL (SELECT cc FROM B);

    Q. 对于 表 A, B 的子查询, EXISTS 和 IN 要选哪个?

    A. 需要根据表A 和 表B 的表大小及索引情况而定.

    通常使用 IN 及 EXISTS 情况可以概括为以下 SQL 语句:

    -- 语句1
    SELECT * FROM A WHERE cc IN (SELECT cc FROM B);
    
    -- 语句2
    SELECT * FROM A WHERE EXISTS (SELECT cc FROM B WHERE A.cc = B.cc);

    原则: 小表驱动大表.

    结论:

    • 如果 B表 比较小, 且 A表在 cc 列上有索引, 则推荐使用语句1.
    这里的表大小指的是根据where筛选后的大小, 而非表的原始大小
    • 如果 A表 比较小, 且 B表在 cc 列上有索引, 则推荐使用语句2.

    表联结/连接 Join

    1558670169544

    联结是一种机制,用来在一条 SELECT语句中关联表,因此称之为联结。

    使用特殊的语法,可以联结多个表返回一组输出,联结在运行时关联表中正确的行。

    使用联结的要点:

    • 注意所使用的联结类型。一般我们使用内部联结,但使用外部联结也是有效的。
    • 保证使用正确的联结条件,否则将返回不正确的数据。
    • 应该总是提供联结条件,否则会得出笛卡儿积。
    • 在一个联结中可以包含多个表,甚至对于每个联结可以采用不同的联结类型。虽然这样做是合法的,一般也很有用,但应该在一起测试它们前,分别测试每个联结。这将使故障排除更为简单。

    <u>表别名</u>只在查询执行中使用。与列别名不一样,表别名返回到客户机。

    <u>完全限定列名</u> 在引用的列可能出现二义性时,必须使用完全限定列名(用一个点分隔的表名和列名)。如果引用一个没有用表名限制的具有二义性的列名,MySQL将返回错误。

    在联结两个表时,你实际上做的是将第一个表中的每一行与第二个表中的每一行配对。 WHERE 子句作为过滤条件,它只包含那些匹配给定条件(这里是联结条件)的行。没有WHERE 子句,第一个表中的每个行将与第二个表中的每个行配对,而不管它们逻辑上是否可以配在一起。

    示例代码↓

    -- 将表 vendors 与表 products 联结, 联结条件是: vendors.vend_id = products.vend_id
    SELECT prod_id,vend_name,prod_name,prod_price FROM vendors,products WHERE vendors.vend_id = products.vend_id;
    
    -- 表 vendors 每一行都将于表 products 每一行配对
    -- 此时返回的结果为 笛卡尔积, 返回行数目是: count(表A) * count(表B)
    SELECT prod_id,vend_name,prod_name,prod_price FROM vendors,products;

    根据获取到的结果集的范围将连接进行分类:

    • <u>内连接</u>: 取多个表之间满足条件的数据行(交集)

      隐式的内连接: 不写 INNER JOIN

      显式的内连接: 写 INNER JOIN

    • <u>外连接</u>: 除了取满足条件的交集外, 还会取某一方不满足条件的记录.

      左外连接 LEFT OUTER JOIN

      右外连接 RIGHT OUTER JOIN

      全外连接 FULL OUTER JOIN

      书写时 OUTER 可以忽略

    • <u>交叉连接</u>: 笛卡尔积(cartesian product)(所有集合的所有组合).

      CROSS JOIN

      返回记录的条数是每个表的行数的乘积.

    根据连接时的测试条件, 将连接进行分类:

    • <u>等值连接</u>: 连接条件是等号
    • <u>非等值连接</u>: 连接条件是非等号

    当自身与自身进行连接时, 称为<u>自连接</u>.

    内部联结 INNER JOIN

    <u>内部联结</u>即上面的<u>等值联结</u>, 它基于两个表之间的相等测试。

    SELECT vendors.vend_id,vend_name,prod_name,prod_price FROM vendors INNER JOIN products ON vendors.vend_id = products.vend_id;

    <u>使用哪种语法?</u> ANSI SQL规范首选 INNER JOIN 语法。此外,尽管使用 WHERE 子句定义联结的确比较简单,但是使用明确的联结语法能够确保不会忘记联结条件,有时候这样做也能影响性能。

    <u>性能考虑</u> MySQL在运行时关联指定的每个表以处理联结。这种处理可能是非常耗费资源的,因此应该仔细,不要联结不必要的表。联结的表越多,性能下降越厉害。

    eg. 返回订购产品 TNT2 的客户列表

    -- 子查询方式
    select * from customers
    where cust_id in (select distinct cust_id 
                      from orders 
                      where order_num in (select order_num
                                          from orderitems
                                          where prod_id="TNT2")
                     );
                     
    -- 表联结方式1
    SELECT cust_name,cust_contact FROM customers,orders,orderitems WHERE customers.cust_id = orders.cust_id AND orders.order_num = orderitems.order_num AND orderitems.prod_id = 'TNT2';
    
    -- 表联结方式2
    SELECT cust_name,cust_contact FROM customers INNER JOIN orders ON customers.cust_id = orders.cust_id INNER JOIN orderitems ON orders.order_num = orderitems.order_num  WHERE orderitems.prod_id = "TNT2";

    自联结

    Eg. 假如你发现某物品(其ID为 DTNTR )存在问题,因此想知道生产该物品的供应商生产的其他物品是否也存在这些问题。

    -- 子查询方式
    SELECT prod_id,prod_name 
    FROM products 
    WHERE vend_id = (
        SELECT vend_id 
        FROM products 
        WHERE prod_id = 'DTNTR'
    );
    
    -- 自联结方式
    SELECT p1.prod_id,p1.prod_name 
    FROM products as p1,products as p2 
    WHERE p1.vend_id = p2.vend_id AND p2.prod_id = 'DTNTR';

    <u>用自联结而不用子查询</u> 自联结通常作为外部语句用来替代从相同表中检索数据时使用的子查询语句。虽然最终的结果是相同的,但有时候处理联结远比处理子查询快得多。应该试一下两种方法,以确定哪一种的性能更好

    外部联结 OUTER JOIN

    <u>外部联结</u>: 联结包含了那些在相关表中没有关联行的行。

    许多联结将一个表中的行与另一个表中的行相关联。但有时候会需
    要包含没有关联行的那些行。例如,可能需要使用联结来完成以下工作:

    • 对每个客户下了多少订单进行计数,包括那些至今尚未下订单的
      客户;
    • 列出所有产品以及订购数量,包括没有人订购的产品;
    • 计算平均销售规模,包括那些至今尚未下订单的客户。
    -- 使用 LEFT OUTER JOIN 从 FROM子句的左边表( customers 表)中选择所有行
    select c.cust_id,o.order_num 
    from customers as c 
    left outer join orders as o 
    on c.cust_id = o.cust_id;
    
    -- 查看所有客户的订单数量(聚集函数), 包含从没下过单的客户
    SELECT c.cust_id,cust_name,count(distinct o.order_num) 
    FROM customers as c 
    LEFT OUTER JOIN orders as o 
    ON c.cust_id=o.cust_id 
    GROUP BY c.cust_id;

    与内部联结关联两个表中的行不同的是,外部联结还包括没有关联行的行。在使用 OUTER JOIN 语法时,必须使用 RIGHTLEFT 关键字指定包括其所有行的表( RIGHT 指出的是 OUTER JOIN 右边的表,而 LEFT指出的是 OUTER JOIN 左边的表)。

    • LEFT OUTER JOIN 左外联结, 包含坐标的全部记录, 若无对应的右边记录, 则其值为 NULL
    • RIGHT OUTER JOIN
    OUTER 关键字可以省略不写.

    <u>外部联结的类型</u> 存在两种基本的外部联结形式:左外部联结和右外部联结。它们之间的唯一差别是所关联的表的顺序不同。换句话说,左外部联结可通过颠倒 FROM 或 WHERE 子句中表的顺序转换为右外部联结。因此,两种类型的外部联结可互换使用,而究竟使用哪一种纯粹是根据方便而定。

    组合查询 UNION

    MySQL也允许执行多个查询(多条 SELECT 语句),并将结果作为单个查询结果集返回。这些组合查询通常称为并(union)或复合查询(compound query)。

    <u>组合查询和多个 WHERE 条件</u> 多数情况下,组合相同表的两个查询完成的工作与具有多个 WHERE 子句条件的单条查询完成的工作相同。

    -- 返回查询(过滤重复行)
    SELECT ... FROM ...
    UNION
    SELECT ... FROM ...
    ORDER BY ...
    
    -- 返回查询(保留所有行)
    SELECT ... FROM ...
    UNION ALL
    SELECT ... FROM ...
    ORDER BY ...

    对于更复杂的过滤条件,或者从多个表(而不是单个表)中检索数据的情形,使用 UNION 可能会使处理更简单。

    UNION规则

    • UNION 必须由两条或两条以上的 SELECT 语句组成,语句之间用关键字 UNION 分隔(因此,如果组合4条 SELECT 语句,将要使用3个UNION 关键字)。
    • UNION 中的每个查询必须包含相同的列、表达式或聚集函数(不过各个列不需要以相同的次序列出)。
    • 列数据类型必须兼容:类型不必完全相同,但必须是DBMS可以隐含地转换的类型(例如,不同的数值类型或不同的日期类型)。
    • 组合查询可以引用于不同的表

    特点

    • UNION 默认会去除重复的行, 如果要返回所有匹配行, 则要使用 UNION ALL
    • UNION查询只能使用一条ORDER BY 子句, 只能出现在最后一条 SELECT 语句之后.

    全文本搜索

    重要说明

    • 在索引全文本数据时,短词被忽略且从索引中排除。短词定义为那些具有3个或3个以下字符的词(如果需要,这个数目可以更改)。
    • MySQL带有一个内建的非用词(stopword)列表,这些词在索引全文本数据时总是被忽略。如果需要,可以覆盖这个列表(请参阅MySQL文档以了解如何完成此工作)。
    • 许多词出现的频率很高,搜索它们没有用处(返回太多的结果)。因此,MySQL规定了一条50%规则,如果一个词出现在50%以上的行中,则将它作为一个非用词忽略。50%规则不用于 IN BOOLEANMODE 。
    • 如果表中的行数少于3行,则全文本搜索不返回结果(因为每个词或者不出现,或者至少出现在50%的行中)。
    • 忽略词中的单引号。例如, don't 索引为 dont 。
    • 不具有词分隔符(包括日语和汉语)的语言不能恰当地返回全文本搜索结果。
    • 如前所述,仅在 MyISAM 数据库引擎中支持全文本搜索。

    启用全文本搜索支持 FULLTEXT

    MyISAM 引擎支持, InnoDB 引擎不支持.

    为了进行全文本搜索,必须索引被搜索的列

    
    CREATE TABLE table_name(
        note_id        int        NOT NULL    AUTO_INCREMENT,
        note_text    text    NULL,
        PRIMARY KEY (note_id),
        FULLTEXT(note_text),    -- 创建全文本索引
    ) ENGINE=MyISAM;
    <u>!!不要在导入数据时使用 FULLTEXT</u>

    更新索引要花时间,虽然不是很多,但毕竟要花时间。如果正在导入数据到一个新表,此时不应该启用 FULLTEXT 索引。应该首先导入所有数据,然后再修改表,定义 FULLTEXT 。这样有助于更快地导入数据(而且使索引数据的总时间小于在导入每行时分别进行索引所需的总时间)。

    进行全文本搜索 MATCH…AGAINST…

    全文本搜索返回的结果默认排序是按照关联程度最高的排在最前面

    -- 针对指定的列进行搜索
    SELECT * FROM `表名` WHERE Match(`列名`) Against('搜索词');
    Match(列名) Against('搜索词') 实际上是计算出一个代表关联程度的数值, 该数值可以在 SELECT 中直接查看.
    ?? <u>使用完整的 Match() 说明</u> 传递给 Match() 的值必须与FULLTEXT() 定义中的相同。如果指定多个列,则必须列出它们(而且次序正确)。
    <u>!!搜索不区分大小写</u> 除非使用 BINARY 方式(本章中没有介绍),否则全文本搜索不区分大小写。

    查询扩展 WITH EXPANSION

    -- WITH QUERY EXPANSION 使用查询扩展
    SELECT note_id,note_text 
    FROM productnotes 
    WHERE match(note_text) against('anvils' WITH QUERY EXPANSION);    

    MySQL对数据和索引进行两遍扫描来完成搜索:

    • 首先,进行一个基本的全文本搜索,找出与搜索条件匹配的所有行;
    • 其次,MySQL检查这些匹配行并选择所有有用的词
    • 再其次,MySQL再次进行全文本搜索,这次不仅使用原来的条件,而且还使用所有有用的词。

    利用查询扩展,能找出可能相关的结果,即使它们并不精确包含所查找的词。

    <u>行越多越好</u> 表中的行越多(这些行中的文本就越多),使用查询扩展返回的结果越好。

    布尔文本搜索

    布尔方式(booleanmode)

    • 要匹配的词;
    • 要排斥的词(如果某行包含这个词,则不返回该行,即使它包含
      其他指定的词也是如此);
    • 排列提示(指定某些词比其他词更重要,更重要的词等级更高);
    • 表达式分组;
    • 另外一些内容。
    <u>即使没有 FULLTEXT 索引也可以使用</u> 布尔方式不同于迄今为止使用的全文本搜索语法的地方在于,即使没有定义
    FULLTEXT 索引,也可以使用它。但这是一种非常缓慢的操作(其性能将随着数据量的增加而降低)。
    SELECT note_id,note_text 
    FROM productnotes 
    WHERE match(note_text) against('heavy -rope*' IN BOOLEAN MODE);

    说明:

    • 匹配 heavy
    • 排除 rope 开头的词

    1558685103097

    插入数据

    INSERT INTO

    几种使用方式

    • 插入完整的行;
    • 插入行的一部分;
    • 插入多行;
    • 插入某些查询的结果

    插入时必须对每个列必须提供一个值.

    -- 简单但不安全, 依赖表中列的定义次序
    INSERT INTO customer VALUES(NULL,'pep', '100 main', 'los angles', 'CA', '90046', 'USA', NULL, NULL);
    
    -- 指定插入的列, 推荐(但很繁琐)
    INSERT INTO customers(cust_name,cust_address,cust_city,cust_state,cust_zip,cust_country,cust_contact,cust_email) VALUES('pep', '100 main', 'los angles', 'CA', '90046', 'USA', NULL, NULL);
    
    -- 插入多行
    INSERT INTO `表名`(`列名1`, `列名2`) VALUES("值1", "值2"),("值3", "值4"),("值5", "值6");
    
    -- 插入检索出的数据, 注意避免主键的冲突
    INSERT INTO `表1`(`列名1`, `列名2`) SELECT `列名1`, `列名2` FROM `表2`;

    <u>插入时省略列需满足以下任一条件</u>:

    • 该列定义为允许 NULL 值(无值或空值)。
    • 在表定义中给出默认值。这表示如果不给出值,将使用默认值。

    <u>降低插入优先级</u>INSERT LOW PRIORITY INTO

    LOW PRIORITY 同样适用于 UPDATEDELETE 语句

    提高 INSERT 的性能 一次插入多条记录可以提高数据库处理的性能,因为MySQL用单条 INSERT 语句处理多个插入比使用多条 INSERT语句快。

    <u>INSERT SELECT 中的列名</u> MySQL不关心 SELECT 返回的列名。它使用的是列的位置,因此 SELECT 中的第一列(不管其列名)将用来填充表列中指定的第一个列,第二列将用来填充表列中指定的第二个列,如此等等。这对于从使用不同列名的表中导入数据是非常有用的。

    REPLACE INTO

    # 插入/替换一条记录
    replace into `表名`(`列名1`,`列名2`) VALUES("值1", "值2");
    
    # 将表 A 所有记录插入/替换到表 B
    replace into `表A`(`列名1`,`列名2`) select `列名3`,`列名4` from `表B`;

    replace into 首先尝试插入数据到表中, 若存在冲突(主键, 唯一索引), 那么会先删除该行数据再插入新的数据, 否则直接插入该数据.

    注意: 写入记录的字段中需要至少有一个主键或唯一键.

    更新和删除数据

    好习惯:

    • 除非确实打算更新和删除每一行,否则绝对不要使用不带 WHERE子句的 UPDATE 或 DELETE 语句。
    • 在对 UPDATE 或 DELETE 语句使用 WHERE 子句前,应该先用 SELECT 进行测试,保证它过滤的是正确的记录,以防编写的 WHERE 子句不正确。
    • 使用强制实施引用完整性的数据库,这样MySQL将不允许删除具有与其他表相关联的数据的行。

    更新数据 UPDATE

    UPDATE `表名`
    SET `列1`="值1", `列2`="值2"
    WHERE ...;
    
    -- IGNORE, 更新多行时, 忽略错误
    UPDATE IGNORE `表名`
    SET ...
    WHERE ...;

    <u>IGNORE 关键字</u> 如果用 UPDATE 语句更新多行,并且在更新这些行中的一行或多行时出一个现错误,则整个 UPDATE 操作被取消(错误发生前更新的所有行被恢复到它们原来的值)。为即使是发生错误,也继续进行更新,可使用 IGNORE 关键字

    删除数据 DELETE

    DELETE FROM `表名`
    WHERE ...;

    <u>删除表的内容而不是表</u> DELETE 语句从表中删除行,甚至是删除表中所有行。但是, DELETE 不删除表本身。

    <u>更快的删除</u> 如果想从表中删除所有行,不要使用 DELETE 。可使用 TRUNCATE TABLE 语句,它完成相同的工作,但速度更快( TRUNCATE 实际是删除原来的表并重新创建一个表,而不是逐行删除表中的数据)。

    创建和操纵表

    创建表 CREATE TABLE

    CREATE DATABASE `samp_db` DEFAULT charset utf8 collate utf8_unicode_ci;
    
    -- 移除数据库表 user_accounts
    DROP TABLE IF EXISTS `user_accounts`;
    
    -- 示例
    CREATE TABLE IF NOT EXISTS `user_accounts`(
        `id` int(100) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
        `password` varchar(64) NOT NULL COMMENT '用户密码',
        `reset_password` tinyint(2) NOT NULL DEFAULT 0 COMMENT '用户类型:0-不需要重置密码;1-需要重置密码',
        `mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '手机',
        `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
        `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        -- 创建唯一索引, 不允许重复
        UNIQUE KEY idx_user_mobile(`mobile`)    -- 索引名可忽略: UNIQUE KEY (`mobile`)
            
        -- 创建外键
        -- FOREIGN KEY (`dept_id`) REFERENCES `depts`(`id`) ON DELETE cascade
        
        -- 创建主键的另外一种方式
        -- PRIMARY KEY (`id`)
        -- PRIMARY KEY (`key1`,`key2`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

    主键值 必须唯一。表中的每个行必须具有唯一的主键值。如果主键使用单个列,则它的值必须唯一。如果使用多个列,则这些列的组合值必须唯一。

    eg. 多个列的组合作为主键

    CREATE TABLE IF NOT EXISTS orderitems
    (
        order_num int NOT NULL,
        order_item int NOT NULL,
        prod_id char(10) NOT NULL,
        quantity int NOT NULL,
        item_price decimal(8,2) NOT NULL,
        PRIMARY KEY(order_num, order_item)
    ) ENGINE=InnoDB;

    orderitems 表包含orders表中每个订单的细节。每个订单有多项物品,但每个订单任何时候都只有1个第一项物品,1个第二项物品,如此等等。因此,订单号( order_num 列)和订单物品( order_item 列)的组
    合是唯一的,从而适合作为主键

    NULL值就是没有值或缺值。允许 NULL 值的列也允许在插入行时不给出该列的值。不允许 NULL 值的列不接受该列没有值的行,

    <u>理解 NULL</u> 不要把 NULL 值与空串相混淆。 NULL 值是没有值,它不是空串。如果指定 '' (两个单引号,其间没有字符),这在 NOT NULL 列中是允许的。空串是一个有效的值,它不是无值。 NULL 值用关键字 NULL 而不是空串指定。

    主键和 NULL 值 主键为其值唯一标识表中每个行的列。<u>主键中只能使用不允许 NULL 值的列</u>。允许 NULL 值的
    列不能作为唯一标识。

    AUTO_INCREMENT

    • 每个表只允许一个 AUTO_INCREMENT 列,而且它必须被索引(如,通过使它成为主键)
    • 在 INSERT 语句中指定一个值,只要它是唯一的(至今尚未使用过)即可,该值将被用来替代自动生成的值。后续的增量将开始使用该手工插入的值。
    • last_insert_id() 函数返回最后一个 AUTO_INCREMENT 值.

      eg. 增加一个新订单

      1. orders 表中创建一行
      2. 使用 last_insert_id() 获取自动生成的 order_num
      3. 在 orderitms 表中对订购的每项物品创建一行。 order_num 在 orderitems 表中与订单细节一起存储。

    DEFAULT

    • 使用当前时间作为默认值

      CREATE TABLE `表名`(
          ...,
          `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
      );
    • 许多数据库开发人员使用默认值而不是 NULL 列,特别是对用于计算或数据分组的列更是如此。

    ENGINE

    • InnoDB 是一个可靠的事务处理引擎,它不支持全文本搜索;
    • MEMORY 在功能等同于 MyISAM ,但由于数据存储在内存(不是磁盘)中,速度很快(特别适合于临时表);
    • MyISAM 是一个性能极高的引擎,它支持全文本搜索(参见第18章),但不支持事务处理。

    <u>外键不能跨引擎</u> 混用引擎类型有一个大缺陷。外键(用于强制实施引用完整性,如第1章所述)不能跨引擎,即使用一个引擎的表不能引用具有使用不同引擎的表的外键。

    更新表 ALTER TABLE

    列和外键的操作

    -- 新增列
    ALTER TABLE `表名` ADD COLUMN `列名` 列属性;
    
    -- 删除列
    ALTER TABLE `表名` DROP COLUMN `列名`;
    
    -- 修改列(属性替换)
    -- CHANGE 可以重命名列名, MODIFY 不能
    ALTER TABLE `表名` CHANGE COLUMN `旧列名` `新列名` 列属性;
    ALTER TABLE `表名` MODIFY `列名` 列属性;
        
    
    -- 删除表
    DROP TABLE `表名`;
    
    -- 重命名表
    RENAME TABLE `表名1` TO `表名2`;

    复杂的表结构更改一般需要手动删除过程

    用新的列布局创建一个新表;

    • 使用 INSERT SELECT 语句从旧表复制数据到新表。如果有必要,可使用转换函数和计算字段;
    • 检验包含所需数据的新表;
    • 重命名旧表(如果确定,可以删除它);
    • 用旧表原来的名字重命名新表;
    • 根据需要,重新创建触发器、存储过程、索引和外键。

    <u>小心使用 ALTER TABLE</u> 使用 ALTER TABLE 要极为小心,应该在进行改动前做一个完整的备份(模式和数据的备份)。数据库表的更改不能撤销,如果增加了不需要的列,可能不能删除它们。类似地,如果删除了不应该删除的列,可能会丢失该列中的所有数据。

    约束, 索引

    -- 删除外键
    -- 约束名可以用 show create table `表名` 语句来查看
    ALTER TABLE `表名` DROP FOREIGN KEY `约束名`;    
    
    -- 查看索引
    SHOW INDEX FROM `表名`;
    SHOW KEY FROM `表名`;
    
    -- 创建普通索引(省略索引名)
    ALTER TABLE `表名` ADD INDEX (`列名`);
    ALTER TABLE `表名` ADD UNIQUE KEY(`列名`);
    ALTER TABLE `表名` ADD PRIMARY KEY(`列名`);
    ALTER TABLE `表名` ADD FOREIGN KEY(`列名`) REFERENCES `关联表名`(`关联列名`);
    ALTER TABLE `表1` ADD CONSTRAINT `约束名` FOREIGN KEY (`外键`) REFERENCES `表2` (`表2的键`);
    
    -- CREATE INDEX 只可对表增加普通索引或UNIQUE索引
    CREATE INDEX `索引名` ON `表名` (`列名`);
    CREATE UNIQUE INDEX `索引名` ON `表名` (`列名`);
    
    -- 删除索引
    ALTER TABLE `表名` DROP PRIMARY KEY;
    ALTER TABLE `表名` DROP INDEX `索引名`;
    ALTER TABLE `表名` DROP FOREIGN KEY `约束名`;
    
    DROP INDEX `索引名` ON `表名`;

    索引

    2019年5月29日17:18:22 开始补充

    种类:

    • INDEX 普通索引:仅加速查询
    • UNIQUE KEY 唯一索引:加速查询 + 列值唯一(可以有null)
    • PRIMARY KEY 主键索引:加速查询 + 列值唯一 + 表中只有一个(不可以有null)
    • INDEX 组合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并
    • FULLTEXT 全文索引:对文本的内容进行分词,进行搜索

    <u>术语</u>

    • 索引合并:使用多个单列索引组合查询搜索
    • 覆盖索引:select的数据列只用从索引中就能够取得,不必读取数据行,换句话说查询列要被所建的索引覆盖

    组合索引

    • 同时搜索多个条件时,组合索引的性能效率好过于多个单一索引合并

    索引列的选择

    重要的条件有

    1. 查询频繁

      索引的维护是有额外消耗的, 因此一般只建立有必要的索引
    2. 区分度高

      区分度太低的话, 跟全表扫描(指聚簇索引)没差多少, 还额外增加维护索引的消耗
    3. 长度小

      字段越长, 则索引越大, 额外空间的占用也越大.

      当需要极致优化字符串字段的索引占用时, 可以对字符串(char, varchar)字段索引时可以考虑使用前N个字符, 但要注意测试是否有足够的区分度:

      select (count(distinct left(字段,2)) / count(distinct 字段)) as diff_rate from 表名;

      或者是对该字符串 hash, 增加额外字段(crc32)

    4. 尽可能重用索引(联合索引)

    联合索引

    左前缀规则

    索引举例:index(a,b,c)

    条件索引是否发挥作用用了哪些列
    Where a=3只使用了a列
    Where a=3 and b=5使用了a,b列
    Where a=3 and b=5 and c=4使用了abc
    Where b=3 or where c=4
    Where a=3 and c=4a列能发挥索引,c不能
    Where a=3 and b>10 and c=7a能利用,b能利用, c不能利用
    where a=3 and b like ‘xxxx%’ and c=7a能用,b能用,c不能用

    索引的创建及管理

    -- CREATE TABLE 时创建
    CREATE TABLE IF NOT EXISTS `users` (
        -- 省略字段定义
        
        PRIMARY KEY (`id`),
        UNIQUE KEY `users_phone` (`phone`),
        KEY `users_name` (`name`),
    ) Engine=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
    
    
    
    CREATE INDEX index_name ON table_name (column_list)
    
    CREATE UNIQUE INDEX index_name ON table_name (column_list)
    
    
    
    ALTER TABLE table_name ADD INDEX index_name (column_list)
    -- 可忽略索引名
    -- ALTER TABLE table_name ADD INDEX (column_list)
    
    ALTER TABLE table_name ADD UNIQUE (column_list)
    
    ALTER TABLE table_name ADD PRIMARY KEY (column_list)
    
    -- 一个语句建多个索引
    ALTER TABLE HeadOfState ADD PRIMARY KEY (ID), ADD INDEX (LastName,FirstName);

    视图

    视图是虚拟的表。与包含数据的表不一样,视图只包含使用时动态检索数据的查询。

    视图包含的是一个SQL查询, 它不包含数据!!

    个人理解: 视图即别名~

    -- 创建视图
    CREATE VIEW `视图名` AS SELECT ...
    
    -- 查看创建指定视图的语句
    SHOW CREATE VIEW `视图名`;
    
    -- 删除视图
    DROP VIEW `视图名`;
    
    -- 更新视图
    CREATE OR REPLACE VIEW AS SELECT ...

    视图的作用:

    • 简化数据处理
    • 重新格式化基础数据
    • 保护基础数据

    为什么使用视图:

    • 重用SQL语句。
    • 简化复杂的SQL操作。在编写查询后,可以方便地重用它而不必知道它的基本查询细节。
    • 使用表的组成部分而不是整个表。
    • 保护数据。可以给用户授予表的特定部分的访问权限而不是整个表的访问权限。
    • 更改数据格式和表示。视图可返回与底层表的表示和格式不同的数据。

    视图的规则和限制

    • 与表一样,视图必须唯一命名(不能给视图取与别的视图或表相同的名字)。
    • 对于可以创建的视图数目没有限制。
    • 为了创建视图,必须具有足够的访问权限。这些限制通常由数据库管理人员授予。
    • 视图可以嵌套,即可以利用从其他视图中检索数据的查询来构造一个视图。
    • ORDER BY 可以用在视图中,但如果从该视图检索数据 SELECT 中也含有 ORDER BY ,那么该视图中的 ORDER BY 将被覆盖。
    • 视图不能索引,也不能有关联的触发器或默认值。
    • 视图可以和表一起使用。例如,编写一条联结表和视图的 SELECT语句。

    <u>创建可重用的视图</u>

    创建不受特定数据限制的视图是一种好办法。例如,上面创建的视图返回生产所有产品的客户而不仅仅是 生产TNT2 的客户。扩展视图的范围不仅使得它能被重用,而且甚至更有用。这样做不需要创建和维护多个类似视图。

    <u>将视图用于检索</u>

    一般,应该将视图用于检索( SELECT 语句)而不用于更新( INSERT 、 UPDATE 和 DELETE )。

    存储过程

    存储过程

    简单来说: 存储过程是为以后的使用而保存的一条或多条MySQL语句的集合.

    使用存储过程的原因:

    • 封装复杂操作, 统一调用
    • 提高性能

      使用存储过程比使用单独的SQL语句要快
    • 存在一些只能用在单个请求中的MySQL元素和特性,存储过程可以使用它们来编写功能更强更灵活的代码

    存储过程一般并不显示结果, 而是把结果返回给你指定的变量.

    -- 调用存储过程
    CALL `过程名`()
    
    -- 更改mysql命令行客户端语句分隔符, 除了 \ 符号外,其他字符都可以作为语句分隔符.
    DELIMITER //
    
    -- 创建存储过程
    CREATE PROCEDURE `过程名`()
    BEGIN
    
    END//
    
    -- 还原mysql命令行客户端语句分隔符
    DELIMITER ;
    
    -- 删除存储过程
    DROP PROCEDURE IF EXISTS `过程名`;
    
    -- 检查(查看)存储过程
    SHOW CREATE PROCEDURE `过程名`;
    
    -- 查看存储过程的元数据(创建时间, 创建人..)
    SHOW PROCEDURE STATUS LIKE '过程名';

    MySQL支持存储过程的参数:

    • IN
    • OUT
    • INOUT

    <u>参数的数据类型</u>

    存储过程的参数允许的数据类型与表中使用的数据类型相同。

    !!! 记录集不是允许的类型,因此,不能通过一个参数返回多个行和列。

    一个简单的示例: 计算商品的最低、最高、平均价格

    DELIMITER //
    
    CREATE PROCEDURE productpricing(
        OUT pl DECIMAL(8,2),
        OUT ph DECIMAL(8,2),
        OUT pm DECIMAL(8,2)
    ) 
    BEGIN
        SELECT Min(prod_price) INTO pl FROM products;
        SELECT Max(prod_price) INTO ph FROM products;
        SELECT Avg(prod_price) INTO pm FROM products;
    END//
    
    DELIMITER ;
    
    CALL productpricing(@pricelow, @pricehigh, @priceaverage);
    SELECT @pricelow, @pricehigh, @priceaverage;

    另一个简单示例: 接受订单号并返回该订单的统计

    DELIMITER //
    
    CREATE PROCEDURE ordertotal(IN onumber INT, OUT ototal DECIMAL(6,2))
    BEGIN
        SELECT Sum(item_price*quantity) 
        FROM orderitems 
        WHERE order_num = onumber 
        INTO ototal;
    END//
    
    DELIMITER ;
    
    CALL ordertotal(20005, @total);
    
    SELECT @total;

    变量

    <u>变量(variable)</u>

    内存中一个特定的位置,用来临时存储数据。

    变量名不区分大小写

    用户变量

    @变量名, 仅对当前连接有效

    可以使用 SET @变量=值SELECT @变量:=值; 来赋值给变量

    -- 
    SET @变量=值;
    
    -- 在SELECT中, = 是比较符, 因此需要用 :=
    SELECT @变量:=值;

    系统变量

    全局变量

    对当前mysqld实例有效

    SET GLOBAL 变量名=值;
    
    SET @@变量名=值;

    需要 SUPER 权限, 需重新连接后才生效.

    会话变量

    只对当前连接有效

    -- 设置变量值
    SET SESSION 变量名=值;
    
    -- LOCAL 是SESSION的同义词
    SET LOCAL 变量名=值;
    
    -- 不指定 GLOBAL,SESSION 时, 默认就是 SESSION
    -- 此处没有 @
    SET 变量名=值;
    
    
    -- 若存在会话变量则返回, 否则返回全局变量.
    SELECT @@变量名;
    
    SHOW VARIABLES LIKE '变量名';

    局部变量

    declare定义, 仅在当前块有效(begin...end)

    语法

    条件语句

    IF ... THEN
        
    ELSEIF ... THEN
        
    ELSE
    
    END IF;    

    循环语句

    -- WHILE 循环
    WHILE ... DO
    
    END WHILE;
    
    
    -- ---------------------------------------------------
    -- REPEAT 循环
    REPEAT
    
    UNTIL ...
    END REPEAT;
    
    
    -- ---------------------------------------------------
    -- LOOP 循环
    loop标记: LOOP
        
        IF ... THEN
            LEAVE loop标记;
        END IF;
    END LOOP;
    
    -- LOOP 示例
    CREATE PROCEDURE proc_loop ()
    BEGIN    
        declare i int default 0;
        loop_label: loop
            select i;
            set i=i+1;
            if i>=5 then
                leave loop_label;
                end if;
        end loop;
    
    END

    动态执行SQL语句

    PREPARE 变量 FROM "...";
    EXECUTE 变量 USING @p1;
    DEALLOCATE PREPARE 变量;
    
    -- 示例
    SET @num = 20005;
    PREPARE stmt FROM 'SELECT * FROM orders WHERE order_num = ?';
    EXECUTE stmt USING @num;
    DEALLOCATE PREPARE stmt;
    参数只能使用<u>用户变量</u>来传递

    智能存储过程

    在存储过程中包含:

    • 业务规则
    • 智能处理
    -- Name: ordertotal
    -- Parameters: onumber = order number
    --                         taxable = 0 if not taxable, 1 if taxable
    --            ototal = order total VARIABLES
    CREATE PROCEDURE ordertotal(
        IN onumber INT,
        IN taxable BOOLEAN,
        OUT ototal DECIMAL(8,2)
    ) COMMENT '获取订单总额, 可选增加营业税'
    BEGIN
    -- Declare variable for total
    DECLARE total DECIMAL(8,2);
    -- Declare tax percentage
    DECLARE taxrate INT DEFAULT 6;
    
    -- Get the order total
    SELECT sum(item_price*quantity) FROM orderitems WHERE order_num = onumber INTO total;
    
    -- Is this taxable?
    IF taxable THEN
        -- yes, so add taxrate to the total
        SELECT total+(total/100*taxrate) INTO total;
    END IF;
    
    -- And finally, save to out vaiable
    SELECT total INTO ototal;
    END;
    • DECLARE 定义了两个局部变量, 支持可选的默认值

    游标

    游标(cursor)是一个存储在MySQL服务器上的数据库查询,它不是一条 SELECT 语句,而是被该语句检索出来的结果集。在存储了游标之后,应用程序可以根据需要滚动或浏览其中的数据。

    <u>只能用于存储过程</u> 不像多数DBMS,MySQL游标只能用于存储过程(和函数)。

    使用游标的步骤:

    • 声明游标(仅定义查询语句)
    • 打开游标(执行查询)
    • 使用游标检索数据(遍历)
    • 关闭游标

      • 游标关闭后必须重新打开才能再次使用
      • 存储过程结束时会自动将已打开的游标关闭
    CREATE PROCEDURE `processorders`()
    BEGIN
        -- create cursor
        DECLARE ordernumbers CURSOR
        FOR
        SELECT order_num FROM orders;
    
        -- open cursor
        OPEN ordernumbers;
    
        -- close cursor
        CLOSE ordernumbers;
    END

    书上示例

    DROP PROCEDURE processorders;
    
    CREATE PROCEDURE processorders()
    BEGIN
        DECLARE done BOOLEAN DEFAULT 0;
        DECLARE o INT;
        DECLARE t DECIMAL(8,2);
    
        -- Declare CURSOR
        DECLARE ordernumbers CURSOR
        FOR
        SELECT order_num FROM orders;
    
        -- Declare continue handler
        -- FOR NOT FOUND
        DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done=1;
    
        -- Create a table to store the results
        CREATE TABLE IF NOT EXISTS ordertotals(
            order_num INT,
            total DECIMAL(8,2)
        );
    
        OPEN ordernumbers;
    
        REPEAT
            -- Get order number
            FETCH ordernumbers INTO o;
            
            CALL ordertotal(o,1,t);
    
            INSERT INTO ordertotals(order_num,total) VALUES(o,t);
        UNTIL done 
        END REPEAT;
    
        -- Close the cursor
        CLOSE ordernumbers;
    END

    改进示例

    DROP PROCEDURE processorders;
    
    CREATE PROCEDURE processorders()
    BEGIN
        DECLARE done BOOLEAN DEFAULT 0;
        DECLARE o INT;
        DECLARE t DECIMAL(8,2);
    
        -- Declare CURSOR
        DECLARE ordernumbers CURSOR
        FOR
        SELECT order_num FROM orders;
    
        -- Declare continue handler
        -- FOR NOT FOUND
        DECLARE CONTINUE HANDLER FOR NOT FOUND SET done=1;
    
        -- Create a table to store the results
        CREATE TABLE IF NOT EXISTS ordertotals(
            order_num INT,
            total DECIMAL(8,2)
        );
    
        OPEN ordernumbers;
    
        FETCH ordernumbers INTO o;
        -- 避免插入 (NULL,NULL) 到 ordertotals 表
        WHILE NOT done DO        
            CALL ordertotal(o,1,t);
    
            SELECT o,t;
    
            INSERT INTO ordertotals(order_num,total) VALUES(o,t);
    
            FETCH ordernumbers INTO o;
        END WHILE;
    
        -- Close the cursor
        CLOSE ordernumbers;
    END

    触发器

    触发器是MySQL响应以下任意语句而自动执行的一条MySQL语句(或位于 BEGIN 和 END 语句之间的一组语
    句):

    • DELETE ;
    • INSERT ;
    • UPDATE
    创建触发器可能需要特殊的安全访问权限,但是,触发器的执行是自动的。如果 INSERT 、 UPDATE 或 DELETE 语句能够执行,则相关的触发器也能执行。
    • 应该用触发器来保证数据的一致性(大小写、格式等)。在触发器中执行这种类型的处理的优点是它总是进行这种处理,而且是透明地进行,与客户机应用无关。
    • 触发器的一种非常有意义的使用是创建审计跟踪。使用触发器,把更改(如果需要,甚至还有之前和之后的状态)记录到另一个表非常容易。
    • 遗憾的是,MySQL触发器中不支持 CALL 语句。这表示不能从触发器内调用存储过程。所需的存储过程代码需要复制到触发器内。

    创建触发器

    在创建触发器时,需要给出4条信息:

    • 唯一的触发器名;
    • 触发器关联的表;
    • 触发器应该响应的活动( DELETE 、 INSERT 或 UPDATE );
    • 触发器何时执行(处理之前或之后)。

    触发器按每个表每个事件每次地定义,每个表每个事件每次只允许一个触发器。因此,每个表最多支持6个触发器(每条 INSERT 、 UPDATE和 DELETE 的之前和之后)。单一触发器不能与多个事件或多个表关联,所以,如果你需要一个对 INSERT 和 UPDATE 操作执行的触发器,则应该定义两个触发器。

    <u>仅支持表</u> 只有表才支持触发器,视图不支持(临时表也不支持)

    -- 创建触发器
    CREATE TRIGGER `触发器名` 
    AFTER|BEFORE 
    INSERT|UPDATE|DELETE 
    ON `表名`
    FOR EACH ROW
    ...
    
    -- 删除触发器
    DROP TRIGGER `触发器名`;
    

    <u>BEFORE 或 AFTER ?</u> 通常,将 BEFORE 用于数据验证和净化(目的是保证插入表中的数据确实是需要的数据)。本提示也适用于 UPDATE 触发器。

    INSERT 触发器

    • 在 INSERT 触发器代码内,可引用一个名为 NEW 的虚拟表,访问被插入的行;
    • 在 BEFORE INSERT 触发器中, NEW 中的值也可以被更新(允许更改被插入的值);
    • 对于 AUTO_INCREMENT 列, NEW 在 INSERT 执行之前包含 0 ,在 INSERT执行之后包含新的自动生成值。
    -- mysql中无法执行: 禁止触发器返回结果集
    -- ERROR 1415 (0A000): Not allowed to return a result set from a trigger
    CREATE TRIGGER neworder AFTER INSERT ON orders
    FOR EACH ROW
    SELECT NEW.order_num;

    DELETE 触发器

    • 在 DELETE 触发器代码内,你可以引用一个名为 OLD 的虚拟表,访问被删除的行;
    • OLD 中的值全都是只读的,不能更新。

    在任意订单被删除前将执行此触发器。它使用一条 INSERT 语句将 OLD 中的值(要被删除的订单)保存到一个名为 archive_orders 的存档表中

    CREATE TRIGGER deleteorders BEFORE DELETE ON orders
    FOR EACH ROW
    BEGIN
        INSERT INTO archive_orders(order_num,order_date,cust_id)
        VALUES(OLD.order_num,OLD.order_date,OLD.cust_id);
    END

    使用 BEFORE DELETE 触发器的优点(相对于 AFTER DELETE 触发器来说)为,如果由于某种原因,订单不能存档, DELETE 本身将被放弃。

    UPDATE 触发器

    • 在 UPDATE 触发器代码中,你可以引用一个名为 OLD 的虚拟表访问以前( UPDATE 语句前)的值,引用一个名为 NEW 的虚拟表访问新更新的值;
    • 在 BEFORE UPDATE 触发器中, NEW 中的值可能也被更新(允许更改将要用于 UPDATE 语句中的值);
    • OLD 中的值全都是只读的,不能更新。
    CREATE TRIGGER updatevendor
    BEFORE UPDATE ON vendors 
    FOR EACH ROW 
    SET NEW.vend_state=Upper(NEW.vend_state);

    事务

    基本要素 ACID

    MySQL的四种事务隔离级别
    1. 原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
    2. 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
    3. 隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
    4. 持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

    并发与隔离级别

    事务存在的问题:

    • 脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
    • 不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
    • 幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

    小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

    数据库事务隔离级别:

    1. read-uncommited 读未提交

      存在所有问题, 最低的隔离级别。一个事务可以读取另一个事务并未提交的更新结果。

    2. read-commited 读已提交

      解决"脏读", 大部分数据库采用的默认隔离级别。一个事务的更新操作结果只有在该事务提交之后,另一个事务才可以的读取到同一笔数据更新后的结果。

    3. repeatable-read 可重复读

      解决"不可重复读", 整个事务过程中,对同一笔数据的读取结果是相同的,不管其他事务是否在对共享数据进行更新,也不管更新提交与否。

    4. serializable 序列化

      解决"幻读", 最高隔离级别。所有事务操作依次顺序执行。注意这会导致并发度下降,性能最差。通常会用其他并发级别加上相应的并发锁机制来取代它。

    • 不可重复读
    • 可重复读
    • 幻读

    MySQL 默认级别是 repeatable-read, 由于其使用了间隙锁, 因此尽管是可重复读级别, 但并不会产生幻读问题.


    术语

    • 事务( transaction )指一组SQL语句;
    • 回退( rollback )指撤销指定SQL语句的过程;
    • 提交( commit )指将未存储的SQL语句结果写入数据库表;
    • 保留点( savepoint )指事务处理中设置的临时占位符(place-holder),你可以对它发布回退(与回退整个事务处理不同)。
    -- 标识事务开始
    START TRANSACTION;
    
    -- ROLLBACK;
    -- COMMIT;

    <u>哪些语句可以回退?</u> 事务处理用来管理 INSERT 、 UPDATE 和DELETE 语句。你不能回退 SELECT 语句。(这样做也没有什么意义。)你不能回退 CREATE 或 DROP 操作。事务处理块中可以使用这两条语句,但如果你执行回退,它们不会被撤销。


    -- 关闭本次连接的mysql自动提交
    SET autocommit=0;

    保留点 SavePoint

    为了支持回退部分事务处理,必须能在事务处理块中合适的位置放置占位符。这样,如果需要回退,可以回退到某个占位符。

    -- 创建保留点
    SAVEPOINT `保留点名`;
    
    -- 回退到指定保留点
    ROLLBACK TO `保留点名`;
    

    字符集

    术语

    • 字符集为字母和符号的集合;
    • 编码为某个字符集成员的内部表示;
    • 校对为规定字符如何比较的指令。
    -- 查看可用的字符集
    SHOW CHARACTER SET;
    SHOW CHARSET;
    
    -- 查看可用的校对
    SHOW COLLATION;
    
    -- 查看当前使用的字符集
    SHOW VARIABLES LIKE 'character%';
    SHOW VARIABLES LIKE 'collation%';

    通常系统管理在安装时定义一个默认的字符集和校对。此外,也可以在创建数据库时,指定默认的字符集和校对。

    -- 以下语句都可以
    CREATE DATABASE `数据库名` DEFAULT character set utf8 collate utf8_unicode_ci;
    CREATE DATABASE `数据库名` default charset utf8mb4 collate utf8mb4_unicode_ci;
    
    -- 关于字符集的设定也可以如下
    CREATE DATABASE `数据库名` DEFAULT charset utf8 collate utf8_unicode_ci;

    实际上,字符集很少是服务器范围(甚至数据库范围)的设置。不同的表,甚至不同的列都可能需要不同的字符集,而且两者都可以在创建表时指定。

    CREATE TABLE mytable(
        column1 INT, 
        column2 VARCHAR(10),
        
        -- 指定特定列使用特定的字符集及校对
        column3 VARCHAR(10) CHARSET latin1 COLLATE latin1_general_ci
    ) Engine=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
    
    
    -- 临时区分大小写排序
    SELECT * FROM mytable
    ORDER BY column3 COLLATE latin1_general_cs;

    <u>SELECT 的其他 COLLATE 子句</u> 除了这里看到的在 ORDER BY子句 中使用以外, COLLATE 还可以用于 GROUP BY 、 HAVING 、聚集函数、别名等。

    串可以在字符集之间进行转换。为此,使用 Cast() 或 Convert ()函数

    安全管理

    用户应该对他们需要的数据具有适当的访问权,既不能多也不能少

    用户

    <u>不要使用 root</u> 应该严肃对待 root 登录的使用。仅在绝对需要时使用它(或许在你不能登录其他管理账号时使用)。不应该在日常的MySQL操作中使用 root 。

    • MySQL用户账号和信息存储在名为 mysql 的MySQL数据库中。
    • mysql 数据库有一个名为 user 的表,它包含所有用户账号。
    • 用户名中的 Host

      • localhost: 仅本地
      • %: 通配符, 允许从任意主机连接数据库

        若 "user"@"%" 无法从本地登陆(不存在 "user"@"localhost"的时候), 则要看是否存在匿名账号, 有则删掉.

        具体可阅读: https://www.cnblogs.com/chyin...

      • 主机名
      • ip地址
    • 关于匿名用户(即 User 为空的账户, 可以匹配任意用户名)

      # 查看当前是否有匿名账号
      SELECT User, Host from mysql.user WHERE Host = 'localhost' AND User = '';
      
      # 删除匿名账号
      DROP USER ''@'localhost';
    -- 查看当前所有用户
    SELECT host,user FROM mysql.user;
    
    
    
    
    
    
    -- 创建用户(若不指定 host, 则默认是 %), 此时该用户权限为 USAGE(即无权限)
    CREATE USER 用户名 IDENTIFIED BY '密码';
    
    -- 创建用户(原始方式, 不推荐)
    insert into mysql.user(Host,User,Password,select_priv,insert_priv,update_priv,delete_priv) values('localhost', 'guest', PASSWORD('123456'), 'Y','Y','Y','Y');
    FLUSH PRIVILEGES;
    
    
    
    
    
    -- 重命名用户
    RENAME USER 旧用户名 TO 新用户名;
    
    -- 删除用户
    DROP USER 用户名;
    
    -- 删除用户(原始方式, 不推荐)
    DELETE FROM mysql.user WHERE Host = '%' AND User = 'yjx'
    FLUSH PRIVILEGES;
    
    -- 修改用户名
    RENAME USER '旧用户名@..' TO '新用户名@..';
    
    
    
    
    
    -- 更改自己口令
    SET PASSWORD = Password('密码');
    
    -- 更改指定用户口令
    SET PASSWORD FOR 用户名 = Password('密码');
    
    -- 低版本好像不支持该语法
    ALTER USER 用户名 IDENTIFIED BY '密码';

    使用 mysqladmin 修改密码

    -- 设置密码(若修改密码, 则需输入原密码)
    mysqladmin -u root password

    权限

    GRANT 语法

    GRANT priv_type ON database.table
    TO user[IDENTIFIED BY [PASSWORD] 'password']
    [,user [IDENTIFIED BY [PASSWORD] 'password']...]

    设置访问权限

    -- 查看赋予当前用户账号的权限
    SHOW GRANTS;
    
    -- 查看赋予某个用户账号的权限
    -- 完整的用户定义: user@host
    -- 不指定主机名时使用默认的主机名 %
    -- 默认查询: 用户名@'%'
    SHOW GRANTS FOR 用户名;
    
    -- 创建完整权限的账号(默认 ALL 权限是不包含 GRANT 权限的)
    GRANT ALL ON *.* TO '用户名'@'%' IDENTIFIED BY '密码' WITH GRANT OPTION;
    
    -- 创建拥有部分权限的用户
    GRANT ALTER,INSERT,UPDATE,DELETE,SELECT,EXECUTE,CREATE ON *.* TO '用户名'@'localhost' IDENTIFIED BY 'password' 
    
    -- 创建一个允许完全操作某个数据库的用户(不给权限传递能力)
    GRANT ALL ON `db1`.* to 'new_user'@'%' IDENTIFIED BY 'user_password';
    
    -- 创建一个只允许读某个数据库权限的用户
    GRANT SELECT ON `db1`.`tb1` to 'readonly'@'%' IDENTIFIED BY 'user_password';
    
    -- 赋予 SELECT 权限
    GRANT SELECT ON `数据库名`.* TO 用户名;
    
    
    -- 收回所有权限
    REVOKE ALL PRIVILEGES,GRANT OPTION ON *.* FROM 'yjx'@'%'
    
    -- 收回部分权限
    REVOKE SELECT,DELETE FROM *.* FROM 'yjx'@'%'
    
    -- 撤销 SELECT 权限
    REVOKE SELECT ON `数据库名`.* FROM 用户名;

     WITH关键字后面带有一个或多个with_option参数。有5个选项:

    • GRANT OPTION:被授权的用户可以将这些权限赋予给别的用户;
    • MAX_QUERIES_PER_HOUR count:设置没消失可以允许执行count次查询;
    • MAX_UPDATES_PER_HOUR count:设置每个消失可以允许执行count次更新;
    • MAX_CONNECTIONS_PER_HOUR count:设置每小时可以建立count个连接;
    • MAX_USER_CONNECTIONS count:设置单个用户可以同时具有的count个连接数;

    新用户的权限为: GRANT USAGE ON *.* TO 'yjx'@'%', 即没有任何权限.

    常用权限

    权限说明
    ALL除GRANT OPTION外的所有权限
    SELECT仅查询
    SELECT,INSERT查询和插入
    USAGE无权限

    目标

    目标说明
    数据库名.*指定数据库所有
    数据库名.表指定数据库的指定表
    数据库名.存储过程指定数据库的指定存储过程
    .所有数据库

    用户

    用户说明
    用户名@ip指定ip登陆的用户
    用户名@'192.168.1.%'指定ip段登陆的用户
    用户名@'%'任意ip下登陆的用户
    用户名@localhost本地登陆的用户
    用户名@‘192.168.200.0/255.255.255.0’(子网掩码配置)

    GRANT 和 REVOKE 可在几个层次上控制访问权限:

    • 整个服务器,使用 GRANT ALL 和 REVOKE ALL;
    • 整个数据库,使用 ON database.*;
    • 特定的表,使用 ON database.table;
    • 特定的列;
    • 特定的存储过程。

    1559027891075

    权限与 user 表中的权限列对应表

    权限名称对应user表中的列权限的范围
    CREATECreate_priv数据库、表或索引
    DROPDrop_priv数据库或表
    GRANT OPTIONGrant_priv数据库、表、存储过程或函数
    REFERENCESReferences_priv数据库或表
    ALTERAlter_priv修改表
    DELETEDelete_priv删除表
    INDEXIndex_priv用索引查询表
    INSERTInsert_priv插入表
    SELECTSelect_priv查询表
    UPDATEUpdate_priv更新表
    CREATE VIEWCreate_view_priv创建视图
    SHOW VIEWShow_view_priv查看视图
    ALTER ROUTINEAlter_routine修改存储过程或存储函数
    CREATE ROUTINECreate_routine_priv创建存储过程或存储函数
    EXECUTEExecute_priv执行存储过程或存储函数
    FILEFile_priv加载服务器主机上的文件
    CREATE TEMPORARY TABLESCreate_temp_table_priv创建临时表
    LOCK TABLESLock_tables_priv锁定表
    CREATE USERCreate_user_priv创建用户
    PROCESSProcess_priv服务器管理
    RELOADReload_priv重新加载权限表
    REPLICATION CLIENTRepl_client_priv服务器管理
    REPLICATION SLAVERepl_slave_priv服务器管理
    SHOW DATABASESShow_db_priv查看数据库
    SHUTDOWNShutdown_priv关闭服务器
    SUPERSuper_priv超级权限

    数据库维护

    数据库备份

    • msyqldump 程序
    • BACKUP TABLESELECT INTO OUTFILE 语句转储数据到外部文件, 使用 RESTORE TABLE 还原

      select * from '表' into outfile '/path/to/file.sql';
      
      
      /*
          将结果导出到指定 csv 文件
          - 以 , 分隔字段
          - 每个字段用 "" 包裹

    */
    select name,age from users into outfile '/path/to/file.csv' FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY 'n';

    
    
    
    
    
    *mysqldump 备份*
    

    到处某个库的某些表

    mysqldump -uroot -p '数据库名' '表名'...

    导出为文本文件

    mysqldump -uroot -p -B '数据库名1' '数据库名2' > /tmp/mysql.bak

    直接导出为压缩文件

    mysqldump -uroot -p -B '数据库名1' '数据库名2' | gzip > /tmp/mysql.bak.gz

    -A, --all-databases 备份所有库

    -B, --database 备份指定库

    -F 刷新binlog日志

    -x,--lock-all-tables

    -l,--locktables

    --single-transaction 适合innodb事务数据库备份

    --default-character-set=utf8 字符集

    --triggers 备份触发器

    -d, --no-data 只备份表结构

    -t, --no-create-info 只备份数据, 无 create table 语句

    --master-data 增加binlog日志文件名及对应的位置点

    生产环境全备份

    进行数据库全备,(生产环境还通过定时任务每日凌晨执行)

    mysqldump -uroot -p123456 -S /data/3306/mysql.sock --single-transaction -F -B "数据库名" | gzip > /server/backup/mysql_$(date +%F).sql.gz

    innodb引擎备份

    mysqldump -u$MYUSER -p$MYPASS -S $MYSOCK -F --single-transaction -A | gzip > $DATA_FILE

    myisam引擎备份

    mysqldump -u$MYUSER -p$MYPASS -S $MYSOCK -F -A -B --lock-all-tables |gzip >$DATA_FILE

    
    
    
    *恢复*
    

    直接从sql文件恢复

    mysql -uroot -p < /tmp/mysql.sql

    从压缩文件中恢复

    gunzip < /tmp/mysql.bak.gz | mysql -uroot -p

    若备份文件未指定库, 则此处需要自行指定要恢复到哪个库

    mysql -uroot -p <数据库名> < /tmp/mysql.sql

    
    
    
    
    
    *复制表*
    

    -- 此时要求 目标表 这个表不能存在, 同时该语句会自动创建 目标表 这个表.
    select * into 目标表 from 原始表;

    -- mysql 不支持上述的 select into 语法, 因此可以用以下语句来处理
    -- 但是这种方式仅仅会创建表基本结构一致, 其他的索引之类的一概没有.
    create table 目标表 (select * from 原始表)

    -- mysql 可以用这种方式来处理
    create table 目标表 like 原始表;
    insert into 目标表 select * from 原始表;

    
    
    
    
    
    
    
    # 改善性能
    
    - `SHOW PROCESSLIST` 显示所有活动进程(以及它们的线程ID和执行时间)。你还可以用 `KILL` 命令终结某个特定的进程(使用这个命令需要作为管理员登录)。
    - 使用 EXPLAIN 语句让MySQL解释它将如何执行一条 SELECT 语句。
    - 在导入数据时,应该关闭自动提交。你可能还想删除索引(包括FULLTEXT 索引),然后在导入完成后再重建它们。
    - 你的 SELECT 语句中有一系列复杂的 OR 条件吗?通过使用多条SELECT 语句和连接它们的 UNION 语句,你能看到极大的性能改进。
    - 索引改善数据检索的性能,但损害数据插入、删除和更新的性能。如果你有一些表,它们收集数据且不经常被搜索,则在有必要之前不要索引它们。(索引可根据需要添加和删除。)
    - LIKE 很慢。一般来说,最好是使用 FULLTEXT 而不是 LIKE 。
    
    
    
    ## 查看表锁情况
    

    mysql> show open tables where in_use > 0;

    DatabaseTableIn_useName_locked
    test_yjxuser10
    
    
    
    
    
    ## 加快数据库恢复速度
    
    在恢复数据时,可能会导入大量的数据。
    
    此时有一些技巧可以提高导入速度:
    
    - 导入时禁用索引, 导入结束后再开启索引
    

    ALTER TABLE 表名 disable keys;

    ALTER TABLE 表名 enable keys;

    
    
    
    - 对于InnoDB, 由于默认 `autocommit=1` , 即每条语句作为单独的事务, 因此可以将多条合并成少数几条事务以加快速度.
    

    -- 关闭自动提交, 也可以用 begin 或 start transaction
    set autocommit = 0;

    -- 插入若干条提交
    insert into ...;
    ...
    insert into ...;
    commit;

    -- 插入若干条提交
    insert into ...;
    ...
    insert into ...;
    commit;

    set autocommit = 1;

    
    
    
    
    
    
    
    
    # 设计范式
    
    ## 表的键和属性概念
    
    **超键** 唯一标识元组(数据行)的属性集叫做超键. 
    
    > 比如普通表中主键 id 是超键,   (id, name) 也是超键, (id, age) 也是超键, 因为都可以唯一标识元组(数据行).
    
    
    
    **候选键** 最小超键, 不包含无用字段, 也称为 **码**.
    
    > 以 **超键** 中的例子来讲, (id, name) 因为包含无用的 name 字段, 所以显然它不是候选键. 而单独的 id 是候选键
    
    
    
    **主键** 从候选键中选择一个, 也称为 **主码**.
    
    
    
    **外键** 数据表中的字段是别的数据表的主键, 则称它为外键.
    
    
    
    **主属性** 包含在任意候选键中的属性称为主属性.
    
    
    
    ## 范式
    
    所有范式(按照严格顺序排列):
    
    - 1NF(第一范式)
    
    关键: 表中任何属性都是原子性的, 不可再分.
    
    解释: 字段不要是可以由其他字段组合/计算的.
    
    - 2NF(第二范式)
    
    需要保证表中的非主属性与候选键(码)完全依赖 (即消除了部分依赖)
    
    - 3NF(第三范式)
    
    需要保证表中的非主属性与候选键(码)不存在传递依赖
    
    通常只要求到这个级别.
    
    - BCNF(巴斯-科德范式)
    
    消除主属性之间的部分依赖和传递依赖
    
    - 4NF(第四范式)
    
    - 5NF(完美范式)
    
    
    
    > 这里的
    >
    > - 部分依赖 也称为 部分函数依赖
    >
    > - 传递依赖 也称为 传递函数依赖
    
    
    
    通常要求: <u>**3NF**</u>
    
    > 根据实际情况, 必须时可以新增冗余字段来提高查询效率, 需要权衡.
    
    范式的严格程度是依次递增, 且高级别范式肯定是满足低级别范式的.
    
    
    
    
    
    
    
    # 存储引擎
    
    该部分主要来自: https://juejin.im/post/5c2c53396fb9a04a053fc7fe
    
    
    
    ## 功能差异
    

    show engines

    
    | Engine | Support | Comment                                                      |
    | ------ | ------- | ------------------------------------------------------------ |
    | InnoDB | DEFAULT | **Supports transactions, row-level locking, and foreign keys** |
    | MyISAM | YES     | **MyISAM storage engine**                                    |
    
    
    
    
    ## 存储差异
    
    |                                                              | MyISAM                                            | Innodb                                   |
    | ------------------------------------------------------------ | ------------------------------------------------- | ---------------------------------------- |
    | 文件格式                                                     | 数据和索引是分别存储的,数据`.MYD`,索引`.MYI`    | 数据和索引是集中存储的,`.ibd`           |
    | 文件能否移动                                                 | 能,一张表就对应`.frm`、`MYD`、`MYI`3个文件       | 否,因为关联的还有`data`下的其它文件     |
    | 记录存储顺序                                                 | 按记录插入顺序保存                                | 按主键大小有序插入                       |
    | 空间碎片(删除记录并`flush table 表名`之后,表文件大小不变) | 产生。定时整理:使用命令`optimize table 表名`实现 | 不产生                                   |
    | 事务                                                         | 不支持                                            | 支持                                     |
    | 外键                                                         | 不支持                                            | 支持                                     |
    | 锁支持(锁是避免资源争用的一个机制,MySQL锁对用户几乎是透明的) | 表级锁定                                          | 行级锁定、表级锁定,锁定力度小并发能力高 |
    
    > 锁扩展
    >
    > 表级锁(`table-level lock`):`lock tables <table_name1>,<table_name2>... read/write`,`unlock tables <table_name1>,<table_name2>...`。其中`read`是共享锁,一旦锁定任何客户端都不可读;`write`是独占/写锁,只有加锁的客户端可读可写,其他客户端既不可读也不可写。锁定的是一张表或几张表。
    >
    > 行级锁(`row-level lock`):锁定的是一行或几行记录。共享锁:`select * from <table_name> where <条件> LOCK IN SHARE MODE;`,对查询的记录增加共享锁;`select * from <table_name> where <条件> FOR UPDATE;`,对查询的记录增加排他锁。这里**值得注意**的是:`innodb`的行锁,其实是一个子范围锁,依据条件锁定部分范围,而不是就映射到具体的行上,因此还有一个学名:间隙锁。比如`select * from stu where id < 20 LOCK IN SHARE MODE`会锁定`id`在`20`左右以下的范围,你可能无法插入`id`为`18`或`22`的一条新纪录。
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    # 课程数据
    
    *create.sql*
    

    MySQL Crash Course

    http://www.forta.com/books/06...

    Example table creation scripts

    Create customers table

    CREATE TABLE customers
    (
    cust_id int NOT NULL AUTO_INCREMENT,
    cust_name char(50) NOT NULL ,
    cust_address char(50) NULL ,
    cust_city char(50) NULL ,
    cust_state char(5) NULL ,
    cust_zip char(10) NULL ,
    cust_country char(50) NULL ,
    cust_contact char(50) NULL ,
    cust_email char(255) NULL ,
    PRIMARY KEY (cust_id)
    ) ENGINE=InnoDB;

    Create orderitems table

    CREATE TABLE orderitems
    (
    order_num int NOT NULL ,
    order_item int NOT NULL ,
    prod_id char(10) NOT NULL ,
    quantity int NOT NULL ,
    item_price decimal(8,2) NOT NULL ,
    PRIMARY KEY (order_num, order_item)
    ) ENGINE=InnoDB;

    Create orders table

    CREATE TABLE orders
    (
    order_num int NOT NULL AUTO_INCREMENT,
    order_date datetime NOT NULL ,
    cust_id int NOT NULL ,
    PRIMARY KEY (order_num)
    ) ENGINE=InnoDB;

    Create products table

    CREATE TABLE products
    (
    prod_id char(10) NOT NULL,
    vend_id int NOT NULL ,
    prod_name char(255) NOT NULL ,
    prod_price decimal(8,2) NOT NULL ,
    prod_desc text NULL ,
    PRIMARY KEY(prod_id)
    ) ENGINE=InnoDB;

    Create vendors table

    CREATE TABLE vendors
    (
    vend_id int NOT NULL AUTO_INCREMENT,
    vend_name char(50) NOT NULL ,
    vend_address char(50) NULL ,
    vend_city char(50) NULL ,
    vend_state char(5) NULL ,
    vend_zip char(10) NULL ,
    vend_country char(50) NULL ,
    PRIMARY KEY (vend_id)
    ) ENGINE=InnoDB;

    Create productnotes table

    CREATE TABLE productnotes
    (
    note_id int NOT NULL AUTO_INCREMENT,
    prod_id char(10) NOT NULL,
    note_date datetime NOT NULL,
    note_text text NULL ,
    PRIMARY KEY(note_id),
    FULLTEXT(note_text)
    ) ENGINE=MyISAM;

    Define foreign keys

    ALTER TABLE orderitems ADD CONSTRAINT fk_orderitems_orders FOREIGN KEY (order_num) REFERENCES orders (order_num);
    ALTER TABLE orderitems ADD CONSTRAINT fk_orderitems_products FOREIGN KEY (prod_id) REFERENCES products (prod_id);
    ALTER TABLE orders ADD CONSTRAINT fk_orders_customers FOREIGN KEY (cust_id) REFERENCES customers (cust_id);
    ALTER TABLE products ADD CONSTRAINT fk_products_vendors FOREIGN KEY (vend_id) REFERENCES vendors (vend_id);

    
    
    
    
    
    
    
    *populate.sql*
    

    MySQL Crash Course

    http://www.forta.com/books/06...

    Example table population scripts

    Populate customers table

    INSERT INTO customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact, cust_email)
    VALUES(10001, 'Coyote Inc.', '200 Maple Lane', 'Detroit', 'MI', '44444', 'USA', 'Y Lee', 'ylee@coyote.com');
    INSERT INTO customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact)
    VALUES(10002, 'Mouse House', '333 Fromage Lane', 'Columbus', 'OH', '43333', 'USA', 'Jerry Mouse');
    INSERT INTO customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact, cust_email)
    VALUES(10003, 'Wascals', '1 Sunny Place', 'Muncie', 'IN', '42222', 'USA', 'Jim Jones', 'rabbit@wascally.com');
    INSERT INTO customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact, cust_email)
    VALUES(10004, 'Yosemite Place', '829 Riverside Drive', 'Phoenix', 'AZ', '88888', 'USA', 'Y Sam', 'sam@yosemite.com');
    INSERT INTO customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact)
    VALUES(10005, 'E Fudd', '4545 53rd Street', 'Chicago', 'IL', '54545', 'USA', 'E Fudd');

    Populate vendors table

    INSERT INTO vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)
    VALUES(1001,'Anvils R Us','123 Main Street','Southfield','MI','48075', 'USA');
    INSERT INTO vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)
    VALUES(1002,'LT Supplies','500 Park Street','Anytown','OH','44333', 'USA');
    INSERT INTO vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)
    VALUES(1003,'ACME','555 High Street','Los Angeles','CA','90046', 'USA');
    INSERT INTO vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)
    VALUES(1004,'Furball Inc.','1000 5th Avenue','New York','NY','11111', 'USA');
    INSERT INTO vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)
    VALUES(1005,'Jet Set','42 Galaxy Road','London', NULL,'N16 6PS', 'England');
    INSERT INTO vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)
    VALUES(1006,'Jouets Et Ours','1 Rue Amusement','Paris', NULL,'45678', 'France');

    Populate products table

    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('ANV01', 1001, '.5 ton anvil', 5.99, '.5 ton anvil, black, complete with handy hook');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('ANV02', 1001, '1 ton anvil', 9.99, '1 ton anvil, black, complete with handy hook and carrying case');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('ANV03', 1001, '2 ton anvil', 14.99, '2 ton anvil, black, complete with handy hook and carrying case');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('OL1', 1002, 'Oil can', 8.99, 'Oil can, red');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('FU1', 1002, 'Fuses', 3.42, '1 dozen, extra long');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('SLING', 1003, 'Sling', 4.49, 'Sling, one size fits all');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('TNT1', 1003, 'TNT (1 stick)', 2.50, 'TNT, red, single stick');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('TNT2', 1003, 'TNT (5 sticks)', 10, 'TNT, red, pack of 10 sticks');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('FB', 1003, 'Bird seed', 10, 'Large bag (suitable for road runners)');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('FC', 1003, 'Carrots', 2.50, 'Carrots (rabbit hunting season only)');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('SAFE', 1003, 'Safe', 50, 'Safe with combination lock');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('DTNTR', 1003, 'Detonator', 13, 'Detonator (plunger powered), fuses not included');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('JP1000', 1005, 'JetPack 1000', 35, 'JetPack 1000, intended for single use');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('JP2000', 1005, 'JetPack 2000', 55, 'JetPack 2000, multi-use');

    Populate orders table

    INSERT INTO orders(order_num, order_date, cust_id)
    VALUES(20005, '2005-09-01', 10001);
    INSERT INTO orders(order_num, order_date, cust_id)
    VALUES(20006, '2005-09-12', 10003);
    INSERT INTO orders(order_num, order_date, cust_id)
    VALUES(20007, '2005-09-30', 10004);
    INSERT INTO orders(order_num, order_date, cust_id)
    VALUES(20008, '2005-10-03', 10005);
    INSERT INTO orders(order_num, order_date, cust_id)
    VALUES(20009, '2005-10-08', 10001);

    Populate orderitems table

    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20005, 1, 'ANV01', 10, 5.99);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20005, 2, 'ANV02', 3, 9.99);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20005, 3, 'TNT2', 5, 10);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20005, 4, 'FB', 1, 10);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20006, 1, 'JP2000', 1, 55);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20007, 1, 'TNT2', 100, 10);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20008, 1, 'FC', 50, 2.50);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20009, 1, 'FB', 1, 10);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20009, 2, 'OL1', 1, 8.99);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20009, 3, 'SLING', 1, 4.49);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20009, 4, 'ANV03', 1, 14.99);

    Populate productnotes table

    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(101, 'TNT2', '2005-08-17',
    'Customer complaint:
    Sticks not individually wrapped, too easy to mistakenly detonate all at once.
    Recommend individual wrapping.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(102, 'OL1', '2005-08-18',
    'Can shipped full, refills not available.
    Need to order new can if refill needed.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(103, 'SAFE', '2005-08-18',
    'Safe is combination locked, combination not provided with safe.
    This is rarely a problem as safes are typically blown up or dropped by customers.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(104, 'FC', '2005-08-19',
    'Quantity varies, sold by the sack load.
    All guaranteed to be bright and orange, and suitable for use as rabbit bait.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(105, 'TNT2', '2005-08-20',
    'Included fuses are short and have been known to detonate too quickly for some customers.
    Longer fuses are available (item FU1) and should be recommended.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(106, 'TNT2', '2005-08-22',
    'Matches not included, recommend purchase of matches or detonator (item DTNTR).'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(107, 'SAFE', '2005-08-23',
    'Please note that no returns will be accepted if safe opened using explosives.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(108, 'ANV01', '2005-08-25',
    'Multiple customer returns, anvils failing to drop fast enough or falling backwards on purchaser. Recommend that customer considers using heavier anvils.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(109, 'ANV03', '2005-09-01',
    'Item is extremely heavy. Designed for dropping, not recommended for use with slings, ropes, pulleys, or tightropes.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(110, 'FC', '2005-09-01',
    'Customer complaint: rabbit has been able to detect trap, food apparently less effective now.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(111, 'SLING', '2005-09-02',
    'Shipped unassembled, requires common tools (including oversized hammer).'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(112, 'SAFE', '2005-09-02',
    'Customer complaint:
    Circular hole in safe floor can apparently be easily cut with handsaw.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(113, 'ANV01', '2005-09-05',
    'Customer complaint:
    Not heavy enough to generate flying stars around head of victim. If being purchased for dropping, recommend ANV02 or ANV03 instead.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(114, 'SAFE', '2005-09-07',
    'Call from individual trapped in safe plummeting to the ground, suggests an escape hatch be added.
    Comment forwarded to vendor.'
    );

    查看原文

    赞 4 收藏 2 评论 0