0%

前言

在大企业中防火墙角色主要交给硬件来支持,效果自然没话说只是需要增加一点点成本,但对于大多数个人或者互联网公司来说选择系统自带的 iptables 或者第三方云防火墙似乎是更加合适的选择,通过一些合理的优化和灵活的配置,我们也可以很轻松实现硬件防火墙的部分功能,够用就好。

建立防火墙白名单机制很重要


扩展阅读

Linux 防火墙和 iptables - http://liaoph.com/iptables/
iptables 零基础快速入门系列 - http://www.zsythink.net/archives/tag/iptables/


基础知识

Netfilter 与 iptables 的关系

Linux 系统在内核中提供了对报文数据包过滤和修改的官方项目名为 Netfilter,它指的是 Linux 内核中的一个框架,它可以用于在不同阶段将某些钩子函数(hook)作用域网络协议栈。Netfilter 本身并不对数据包进行过滤,它只是允许可以过滤数据包或修改数据包的函数挂接到内核网络协议栈中的适当位置。这些函数是可以自定义的。

iptables 是用户层的工具,它提供命令行接口,能够向 Netfilter 中添加规则策略,从而实现报文过滤,修改等功能。Linux 系统中并不止有 iptables 能够生成防火墙规则,其他的工具如 firewalld 等也能实现类似的功能。

使用 iptables 进行包过滤

iptables 策略是由一组有序的规则建立的,它告诉内核应该如何处理某些类别的数据包。每一个 iptables 规则应用于一个表中的一个链。一个 iptables 链就是一个规则集,这些规则按序与包含某种特征的数据包进行比较匹配。

iptables 默认有 4 个表

  1. nat 表(地址转换表)
  2. filter 表(数据过滤表)
  3. raw 表(状态跟踪表)
  4. mangle 表(包标记表)

每个表都有一组内置链,用户还可以添加自定义的链。最重要的内置链是 filter 表中的 INPUT、OUTPUT 和 FORWARD 链。

  1. INPUT 链(入站规则)
  2. OUTPUT 链(出站规则)
  3. FORWARD 链(转发规则)
  4. PREROUTING 链(路有前规则)
  5. POSTROUTING 链(路由后规则)

下图展现了一个数据包是如何通过内核中的 net 和 filter 表的:


iptables 的 4 表 5 链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
iptables --> 四表五链 
添加规则是的考量点:
(1)要实现那种功能:判断添加在那张表上
(2)报文流经的路径:判断添加在那个链上

链 -->> 练上规则的次序,即为检查的次序,因此隐含一定的法则
(1)同类规则(访问同一应用):匹配范围小的放上面
(2)不同类规则(访问不同应用):匹配到报文平率较大的放上面
(3)将可以由条规则描述的多个规则合并为一个
(4)设置默认策略:

功能的优先级次序:raw --> mangle --> nat --> filter

规则:
组成部分:报文的匹配条件,匹配到后处理动作
匹配条件:根据协议报文特征指定匹配条件
基本匹配条件:源 IP,目标 IP,源端口,目标端口
扩展匹配条件:IP 追踪
处理动作:
内建处理机制
自定义处理机制
注意:

报文不会经过自定义链,只能在内置链上通过规则引用后生效
iptables 的规则管理工具
添加、修改、删除、显示等功能
规则和链有计数器:
pkgs:有规则或链所匹配到的报文的个数
bytes: 由规则或链匹配到的所有报文大小之和

匹配

每个 iptables 规则都包含一组匹配和一个目标动作,后者定义了复合规则的数据包应该采取什么处理行为。iptables 匹配指定是数据包必须匹配的条件,只有当数据包满足所有的匹配条件时,iptables 才能根据规则的目标所指定的动作来处理该数据包。

每个匹配都在 iptables 的命令行中指定。下面是一些常用的基本匹配:

参数 作用
-P 设置默认策略
-F 清空规则链
-L 查看规则链
-A 在规则链的末尾加入新规则
-I num 在规则链的头部加入新规则
-D num 删除某一条规则
-s 匹配来源地址 IP/MASK,加叹号 “!” 表示除这个 IP 外
-d 匹配目标地址
-i 网卡名称 匹配从这块网卡流入的数据
-o 网卡名称 匹配从这块网卡流出的数据
-p 匹配协议,如 TCP、UDP、ICMP
–dport num 匹配目标端口号
–sport num 匹配来源端口号

防火墙的匹配规则

  • 匹配即可停止
  • 匹配有先后顺序
  • 默认规则的优先级最低

基础语法

表名作用:

raw:高级功能,如:网址过滤。
mangle:数据包修改(QOS),用于实现服务质量。
net:地址转换,用于网关路由器。
filter:包过滤,用于防火墙规则。

规则链作用:

INPUT 链:处理输入数据包。
OUTPUT 链:处理输出数据包。
PORWARD 链:处理转发数据包。
PREROUTING 链:用于目标地址转换(DNAT)。
POSTOUTING 链:用于源地址转换(SNAT)。

动作作用:

accept:接收数据包。
DROP:丢弃数据包。
REJECT :拒绝数据包
REDIRECT:重定向、映射、透明代理。
SNAT:源地址转换。
DNAT:目标地址转换。
MASQUERADE:IP 伪装(NAT),用于 ADSL。
LOG:日志记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# 规则的观察与清除 

iptables [-t tables] [-L] [-nv]

选项与参数:
-t :后面接 table ,例如 nat 或 filter ,若省略此项目,则使用默认的 filter
-L :列出目前的 table 的规则
-n :不进行 IP 与 HOSTNAME 的反查,显示讯息的速度会快很多!
-v :列出更多的信息,包括通过该规则的封包总位数、相关的网络接口等

iptables [-t tables] [-FXZ]

选项与参数:
-F :清除所有的已订定的规则;
-X :杀掉所有使用者 "自定义" 的 chain (应该说的是 tables )啰;
-Z :将所有的 chain 的计数与流量统计都归零


# 封包的基础比对:IP, 网域及接口装置

iptables [-AI 链名] [-io 网络接口] [-p 协议] [-s 来源 IP / 网域] [-d 目标 IP / 网域] -j [ACCEPT|DROP|REJECT|LOG]

选项与参数:
-AI 链名:针对某的链进行规则的 "插入" 或 "累加"
-A :新增加一条规则,该规则增加在原本规则的最后面。例如原本已经有四条规则,
使用 -A 就可以加上第五条规则!
-I :插入一条规则。如果没有指定此规则的顺序,默认是插入变成第一条规则。
例如原本有四条规则,使用 -I 则该规则变成第一条,而原本四条变成 2~5 号
链 :有 INPUT, OUTPUT, FORWARD 等,此链名称又与 -io 有关,请看底下。

-io 网络接口:设定封包进出的接口规范
-i :封包所进入的那个网络接口,例如 eth0, lo 等接口。需与 INPUT 链配合;
-o :封包所传出的那个网络接口,需与 OUTPUT 链配合;

-p 协定:设定此规则适用于哪种封包格式
主要的封包格式有: tcp, udp, icmp 及 all 。

-s 来源 IP / 网域:设定此规则之封包的来源项目,可指定单纯的 IP 或包括网域,例如:
IP :192.168.0.100
网域:192.168.0.0/24, 192.168.0.0/255.255.255.0 均可。
若规范为『不许』时,则加上 ! 即可,例如:
-s ! 192.168.100.0/24 表示不许 192.168.100.0/24 之封包来源;

-d 目标 IP / 网域:同 -s ,只不过这里指的是目标的 IP 或网域。

-j :后面接动作,主要的动作有接受(ACCEPT)、丢弃(DROP)、拒绝(REJECT) 及记录 (LOG)

#TCP, UDP 的规则比对:针对端口设定

iptables [-AI 链] [-io 网络接口] [-p tcp,udp] [-s 来源 IP / 网域] [--sport 埠口范围] [-d 目标 IP / 网域] [--dport 端口范围] -j [ACCEPT|DROP|REJECT]

选项与参数:
--sport 埠口范围:限制来源的端口号码,端口号码可以是连续的,例如 1024:65535
--dport 埠口范围:限制目标的端口号码。

#iptables 外挂模块:mac 与 state

iptables -A INPUT [-m state] [--state 状态]

选项与参数:
-m :一些 iptables 的外挂模块,主要常见的有:
state :状态模块
mac :网络卡硬件地址 (hardware address)
--state :一些封包的状态,主要有:
INVALID :无效的封包,例如数据破损的封包状态
ESTABLISHED:已经联机成功的联机状态;
NEW :想要新建立联机的封包状态;
RELATED :这个最常用!表示这个封包是与我们主机发送出去的封包有关


#ICMP 封包规则的比对:针对是否响应 ping 来设计

iptables -A INPUT [-p icmp] [--icmp-type 类型] -j ACCEPT

选项与参数:
--icmp-type :后面必须要接 ICMP 的封包类型,也可以使用代号,
例如 8 代表 echo request 的意思。

注意事项与规律

  • 可以不指定表,默认为 filter 表
  • 可以不指定链,默认为对应表的所有链
  • 除非设置默认策略,否则必须指定匹配条件
  • 选项 / 链名 / 目标操作用大写字母,其余都小写

配置 iptables 白名单机制

配置 iptables 白名单是相对简单有效的管理手段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# 清除所有规则(慎用)
iptables -F
iptables -X
iptables -Z

# 查看 iptable 和行号
iptables -nL --line-number

# 保存当前防火墙配置
service iptables save

# 手动编辑防火墙策略
vi /etc/sysconfig/iptables

*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
# 开放本地和 Ping
-A INPUT -i lo -j ACCEPT
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -p icmp -j ACCEPT
# 配置内网白名单
-A INPUT -s 10.0.0.0/8 -j ACCEPT
-A INPUT -s 172.16.0.0/12 -j ACCEPT
-A INPUT -s 192.168.0.0/16 -j ACCEPT
# 配置外网白名单
-A INPUT -s 180.168.36.198 -j ACCEPT
-A INPUT -s 180.168.34.218 -j ACCEPT
-A INPUT -s 222.73.202.251 -j ACCEPT
# 控制端口
-A INPUT -p tcp --dport 80 -j ACCEPT
-A INPUT -p tcp --dport 22 -j ACCEPT
# 拒绝其它
-A INPUT -j DROP
-A FORWARD -j DROP
# 开放出口
-A OUTPUT -j ACCEPT
COMMIT

# 重启生效
service iptables restart

# 复查结果

iptables -nL --line-number

Chain INPUT (policy ACCEPT)
num target prot opt source destination
1 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
2 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
3 ACCEPT icmp -- 0.0.0.0/0 0.0.0.0/0
4 ACCEPT all -- 10.0.0.0/8 0.0.0.0/0
5 ACCEPT all -- 172.16.0.0/12 0.0.0.0/0
6 ACCEPT all -- 192.168.0.0/16 0.0.0.0/0
7 ACCEPT all -- 180.168.36.198 0.0.0.0/0
8 ACCEPT all -- 180.168.34.218 0.0.0.0/0
9 ACCEPT all -- 222.73.202.251 0.0.0.0/0
10 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80
11 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
12 DROP all -- 0.0.0.0/0 0.0.0.0/0

Chain FORWARD (policy ACCEPT)
num target prot opt source destination
1 DROP all -- 0.0.0.0/0 0.0.0.0/0

Chain OUTPUT (policy ACCEPT)
num target prot opt source destination
1 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0

设置 crontab 脚本

便于运维集中化管理扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
vim /root/start_iptables.sh

#!/bin/bash
#0 0 * * * /root/start_iptables.sh

# 清除配置
/sbin/iptables -P INPUT ACCEPT
/sbin/iptables -F
/sbin/iptables -X
# 开放本地和 Ping
/sbin/iptables -A INPUT -i lo -j ACCEPT
/sbin/iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
/sbin/iptables -A INPUT -p icmp -j ACCEPT
# 配置内网白名单
/sbin/iptables -A INPUT -s 10.0.0.0/8 -j ACCEPT
/sbin/iptables -A INPUT -s 172.16.0.0/12 -j ACCEPT
/sbin/iptables -A INPUT -s 192.168.0.0/16 -j ACCEPT
# 配置外网白名单
/sbin/iptables -A INPUT -s 180.168.36.198 -j ACCEPT
/sbin/iptables -A INPUT -s 180.168.34.218 -j ACCEPT
/sbin/iptables -A INPUT -s 222.73.202.251 -j ACCEPT
# 控制端口
/sbin/iptables -A INPUT -p tcp --dport 80 -j ACCEPT
/sbin/iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# 拒绝其它
/sbin/iptables -A INPUT -j DROP
/sbin/iptables -A FORWARD -j DROP
# 开放出口
/sbin/iptables -A OUTPUT -j ACCEPT


chmod 755 /root/start_iptables.sh
crontab -e
0 0 * * * /root/start_iptables.sh

CentOS7 配置 iptables

1
2
3
4
5
6
7
8
9
10
# 配置好 yum 源以后安装 iptables-service
yum install -y iptables-services
# 停止 firewalld
systemctl stop firewalld
# 禁止 firewalld 自动启动
systemctl disable firewalld
# 启动 iptables
systemctl start iptables
# 将 iptables 设置为开机自动启动,以后即可通过 iptables-service 控制 iptables 服务
systemctl enable iptables

开启 Linux 路由转发

1
2
3
4
5
6
# 关闭路由转发
echo 0 > /proc/sys/net/ipv4/ip_forward
# 开启路由转发
echo 1 > /proc/sys/net/ipv4/ip_forward
# 注意以上操作仅当前有效,计算机重启后无效,修改 / etc/sysctl.conf 配置文件,可以实现永久有效规则
echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf

配置 NAT Tunnel

俗称跳板机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 配置 iptables
vi /etc/sysconfig/iptables
*nat
:PREROUTING ACCEPT [6:504]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [1:52]
:POSTROUTING ACCEPT [1:52]

-A POSTROUTING -j MASQUERADE
COMMIT

*filter
:INPUT DROP [1029028:53321694]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [44723822:21524638399]

#zabbix
-A INPUT -s 10.65.200.90 -p tcp -m tcp --dport 10050 -j ACCEPT

-A INPUT -p tcp --dport 22 -j ACCEPT

# For keepalived:
# allow vrrp
-A INPUT -p vrrp -j ACCEPT
-A INPUT -p igmp -j ACCEPT
# allow multicast
-A INPUT -d 224.0.0.18 -j ACCEPT

# Drop packets from spoofed networks
-A INPUT -s 169.254.0.0/16 -j DROP
#-A INPUT -s 10.0.0.0/8 -j DROP
#-A INPUT -s 127.0.0.0/8 -j DROP
-A INPUT -s 224.0.0.0/4 -j DROP
-A INPUT -d 224.0.0.0/4 -j DROP
-A INPUT -s 240.0.0.0/5 -j DROP
-A INPUT -d 240.0.0.0/5 -j DROP
-A INPUT -s 0.0.0.0/8 -j DROP
-A INPUT -d 0.0.0.0/8 -j DROP
-A INPUT -d 239.255.255.0/24 -j DROP
-A INPUT -d 255.255.255.255 -j DROP

# Drop Invalid packets
-A INPUT -m state --state INVALID -j DROP
#-A FORWARD -m state --state INVALID -j DROP
-A OUTPUT -m state --state INVALID -j DROP

# Drop Bogus TCP packets
-A INPUT -p tcp --tcp-flags SYN,FIN SYN,FIN -j DROP
-A INPUT -p tcp --tcp-flags SYN,RST SYN,RST -j DROP

-A INPUT -p icmp --icmp-type echo-reply -j ACCEPT
-A INPUT -p icmp --icmp-type destination-unreachable -j ACCEPT
-A INPUT -p icmp --icmp-type redirect -j ACCEPT
-A INPUT -p icmp --icmp-type echo-request -j ACCEPT
-A INPUT -p icmp --icmp-type time-exceeded -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
COMMIT

# 重启生效并检查
service iptables restart
iptables -nvL

常用的 iptables 配置文件

1
2
3
4
5
6
7
8
9
# dnat
-A PREROUTING -s 1.1.1.1/30,2.2.2.2 -p tcp --dport 10000 -j DNAT --to-destination 10.71.12.89:80
# snat
-A POSTROUTING -d 1.1.1.1 -j SNAT --to-source 2.2.2.2
-A POSTROUTING -o bond1 -j SNAT --to-source 3.3.3.3
-A POSTROUTING -j MASQUERADE
# multiport
-A INPUT -s 10.65.200.90 -p tcp -m multiport --dports 10050,10051 -j ACCEPT
-A INPUT -s 10.65.200.90 -p tcp -m multiport --dports 10050:10060 -j ACCEPT

Ansible 管理 iptables

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
---
- hosts: all
become: yes
gather_facts: no

tasks:
- name: backup iptables
copy:
src: /etc/sysconfig/iptables
dest: /etc/sysconfig/iptables.bak
remote_src: yes

- name: add iptables line for internal logic service
blockinfile:
path: /etc/sysconfig/iptables
insertafter: '^:OUTPUT ACCEPT'
marker: "# {mark} iptables whitelist only for internal logic service"
block: |
-A INPUT -s 10.0.0.0/8 -j ACCEPT

- name: reload service
service: name=iptables state=reloaded
tags:
- reload

前言

一般在多网卡的网络访问关系中我们通常根据目标 IP 地址段来添加静态路由 (static-routes),在主机系统配置层面这个需求一般都比较简单也不需要使用非常复杂的命令,说实话如果不是因为开发测试中心同事的乱入启发,我也不会发现还有策略路由这样一种灵活通用的配置方法来实现 Linux 多网卡多路由的设定,灰常感谢。

策略路由的出现在某些业务场景下可能更优于静态路由


##

扩展阅读

Static Routes and the Default Gateway - https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/networking_guide/ch-configuring_static_routes_and_the_default_gateway
Routing Tables - http://linux-ip.net/html/routing-tables.html
linux 配置静态路由 - http://www.361way.com/linux-define-static-route/4053.html
Linux 策略性路由应用及分析 (iproute2) - http://tilt.lib.tsinghua.edu.cn/node/408
Linux Advanced Routing & Traffic Control - http://lartc.org/


静态路由表和策略路由的区别

通常我们维护静态路由表会手工填写所有 IP 地址段的路由规则,使用命令或者写入配置文件永久生效,先以 Windows/Linux/AIX 常用方法为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#Windows 静态路由 
-- 键入 route -p add [目标] [mask < 网络掩码 & gt;] [网关] [metric < 度量值 & gt;] [if < 接口 & gt;]
route print
route delete
route -p add 172.16.19.0 mask 255.255.255.0 198.15.0.1

#RHEL 静态路由
vi /etc/sysconfig/network
default via 192.168.3.1 dev eth0
10.211.6.0/24 via 192.168.3.1 dev eth0
10.0.0.0/8 via 10.212.52.1 dev eth1

#CentOS 静态路由
ip route add 172.16.5.0/24 via 10.0.0.101 dev eth0
vim /etc/sysconfig/network-scripts/route-eth0
172.16.5.0/24 via 10.0.0.101

#SuSE 静态路由
vim /etc/sysconfig/network/routes
default 192.168.3.1 - -
10.211.6.0 192.168.3.1 255.255.255.0 eth0
10.0.0.0 10.212.52.1 255.0.0.0 eth1

#AIX 静态路由
smitty mkroute
172.20.14.0/24 gw 172.23.14.254

这种办法针对少量的规则还可以轻松应对,但规则一旦增加麻烦也就接踵而至,网段地址不断变化就必须及时更新路由表,否则其他用户就无法访问。如果可以根据用户访问进来的路径设定策略路由就会方便很多,而 rt_tables 就是为此而生。

Linux 添加静态路由最佳实践

按照 RedHat 官方介绍有三种方式添加永久静态路由,这里只列举最佳方式

临时路由

以下两种方法可以立即生效,但重启后会消失,切记使用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#route 命令参数,使用 route 命令,查看本机路由直接输入 route -n 回车即可 
add 增加路由
del 删除路由
-net 设置到某个网段的路由
-host 设置到某台主机的路由
gw 出口网关 IP 地址
dev 出口网关 物理设备名

# 添加路由
route add -net 192.168.0.0/24 gw 192.168.0.1
route add -host 192.168.1.1 dev 192.168.0.1
# 删除路由
route del -net 192.168.0.0/24 gw 192.168.0.1
# 添加默认路由
route add default gw 192.168.0.1

# 使用 ip route 命令和 route 命令类似
add 增加路由
del 删除路由
via 网关出口 IP 地址
dev 网关出口 物理设备名

# 添加路由
ip route add 192.168.0.0/24 via 192.168.0.1
ip route add 192.168.1.1 dev 192.168.0.1
# 删除路由
ip route del 192.168.0.0/24 via 192.168.0.1
# 增加默认路由
ip route add default via 192.168.0.1 dev eth0

永久路由

该方法需要重启网络会造成短暂的网络服务中断,但重启后优先级大于 rc.local 方式不会影响 nfs

1
2
3
4
5
6
7
8
9
# 可以通过临时命令立即生效 
route add -net 192.168.3.0/24 gw 192.168.3.254

# 在 / etc/sysconfig/static-routes 文件里添加如下内容 (没有 static-routes 的话就手动建立一个这样的文件)
any net 192.168.3.0/24 gw 192.168.3.254
any net 10.250.228.128 netmask 255.255.255.192 gw 10.250.228.129

# 如果是生产系统可以考虑暂时不重启服务避免网络中断
service network restart

rt_tables

rt_tables 简单来说就是通过给表的命名使得管理简单化

大部分人习惯直接将路由表优先级号码直接作为表的名称来使用,这样做的好处是非常直观和简明的表达了表所在优先级的位置,但是当表的优先级结构出现变动时,我们对巨大的路由表的修改就显得很烦琐和费事了。

在 / etc/iproute2 / 目录下, 有这么一个文件 rt_tables, 只要对它进行改动,我们将很容易的完成路由表优先级结构的变动。(数值越小优先级别越高)

当路由表的优先级发生变化的时候,我们只需要编辑 / etc/iproute2/rt_tables 这个文件就可以直接改变路由表的优先级次序。

1
2
3
4
5
6
7
8
9
10
11
12
13
cat /etc/iproute2/rt_tables

#
# reserved values
#
255 local
254 main
253 default
0 unspec
#
# local
#
#1 inr.ruhep

Linux 最多可以支持 255 张路由表,其中有 3 张表是内置的:

表 255 本地路由表(Local table) 本地接口地址,广播地址,已及 NAT 地址都放在这个表。该路由表由系统自动维护,管理员不能直接修改。

表 254 主路由表(Main table) 如果没有指明路由所属的表,所有的路由都默认都放在这个表里,一般来说,旧的路由工具(如 route)所添加的路由都会加到这个表。一般是普通的路由。

表 253 默认路由表 (Default table) 一般来说默认的路由都放在这张表,但是如果特别指明放的也可以是所有的网关路由。

表 0 保留

策略性路由应用及分析 (iproute2)

策略性路由

  策略性是指对于 IP 包的路由是以网络管理员根据需要定下的一些策略为主要依据进行路由的。例如我们可以有这样的策略:“所有来直自网 A 的包,选择 X 路径;其他选择 Y 路径”,或者是 “所有 TOS 为 A 的包选择路径 F;其他选者路径 K”。
  Cisco 的网络操作系统 (Cisco IOS) 从 11.0 开始就采用新的策略性路由机制。而 Linux 是在内核 2.1 开始采用策略性路由机制的。策略性路由机制与传统的路由算法相比主要是引入了多路由表以及规则的概念。

多路由表(multiple Routing Tables)

  传统的路由算法是仅使用一张路由表的。但是在有些情形底下,我们是需要使用多路由表的。例如一个子网通过一个路由器与外界相连,路由器与外界有两条线路相连,其中一条的速度比较快,一条的速度比较慢。对于子网内的大多数用户来说对速度并没有特殊的要求,所以可以让他们用比较慢的路由;但是子网内有一些特殊的用户却是对速度的要求比较苛刻,所以他们需要使用速度比较快的路由。如果使用一张路由表上述要求是无法实现的,而如果根据源地址或其它参数,对不同的用户使用不同的路由表,这样就可以大大提高路由器的性能。

规则(rule)

  规则是策略性的关键性的新的概念。我们可以用自然语言这样描述规则,例如我门可以指定这样的规则:
  规则一:“所有来自 192.16.152.24 的 IP 包,使用路由表 10, 本规则的优先级别是 1500”
  规则二:“所有的包,使用路由表 253,本规则的优先级别是 32767”

  我们可以看到,规则包含 3 个要素:
  什么样的包,将应用本规则(所谓的 SELECTOR,可能是 filter 更能反映其作用);
  符合本规则的包将对其采取什么动作(ACTION),例如用那个表;
  本规则的优先级别。优先级别越高的规则越先匹配(数值越小优先级别越高)。

环境配置

服务器 A 和 B 为双网卡,操作系统为 rhel_7.1_64

网卡显示名称 IP 地址 子网掩码 网关 备注
ens4f0 172.31.192.201 255.255.255.0 172.31.192.254 服务器 A
ens9f0 172.31.196.1 255.255.255.0 172.31.196.254 服务器 A
ens4f0 172.31.192.202 255.255.255.0 172.31.192.254 服务器 B
ens9f0 172.31.196.2 255.255.255.0 172.31.196.254 服务器 B
/ 172.25.168.44 255.255.255.0 172.25.168.254 接入测试

网络配置,以服务器 A 为例,注意注释默认网关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cat /etc/sysconfig/network-scripts/ifcfg-ens4f0

DEVICE=ens4f0
ONBOOT=yes
BOOTPROTO=static
TYPE=Ethernet
IPADDR=172.31.192.201
NETMASK=255.255.255.0
#GATEWAY=172.31.192.254

cat /etc/sysconfig/network-scripts/ifcfg-ens9f0

DEVICE=ens9f0
ONBOOT=yes
BOOTPROTO=static
TYPE=Ethernet
IPADDR=172.31.196.1
NETMASK=255.255.255.0
#GATEWAY=172.31.196.254

策略路由配置

注意配置名称一定要吻合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 编辑 rt_tables
echo "192 net_192" >> /etc/iproute2/rt_tables
echo "196 net_196" >> /etc/iproute2/rt_tables

# 清空 net_192 路由表
ip route flush table net_192
# 添加一个路由规则到 net_192 表,这条规则是 net_192 这个路由表中数据包默认使用源 IP 172.31.192.201 通过 ens4f0 走网关 172.31.192.254
ip route add default via 172.31.192.254 dev ens4f0 src 172.31.192.201 table net_192
# 来自 172.31.192.201 的数据包,使用 net_192 路由表的路由规则
ip rule add from 172.31.192.201 table net_192

# 清空 net_196 路由表
ip route flush table net_196
# 添加一个路由规则到 net_196 表,这条规则是 net_196 这个路由表中数据包默认使用源 IP 172.31.196.1 通过 ens9f0 走网关 172.31.196.254
ip route add default via 172.31.196.254 dev ens9f0 src 172.31.196.1 table net_196
# 来自 172.31.196.1 的数据包,使用 net_196 路由表的路由规则
ip rule add from 172.31.196.1 table net_196

# 添加默认网关
route add default gw 172.31.192.254

# 如果需要自启动生效可以写进配置文件也可以加入 rc.local
vi /etc/rc.local

ip route flush table net_192
ip route add default via 172.31.192.254 dev ens4f0 src 172.31.192.201 table net_192
ip rule add from 172.31.192.201 table net_192
ip route flush table net_196
ip route add default via 172.31.196.254 dev ens9f0 src 172.31.196.1 table net_196
ip rule add from 172.31.196.1 table net_196
route add default gw 172.31.192.254

# 查看路由表
route -n

Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
169.254.0.0 0.0.0.0 255.255.0.0 U 1006 0 0 ens9f0
169.254.0.0 0.0.0.0 255.255.0.0 U 1008 0 0 ens4f0
169.254.0.0 0.0.0.0 255.255.0.0 U 1014 0 0 br-ex
169.254.0.0 0.0.0.0 255.255.0.0 U 1015 0 0 br-int
172.31.192.0 0.0.0.0 255.255.255.0 U 0 0 0 ens4f0
172.31.196.0 0.0.0.0 255.255.255.0 U 0 0 0 ens9f0

# 在接入测试服务器上验证连通性
ping 172.31.192.201
ping 172.31.196.1

前言

Grafana 是一个开源的数据分析和监控平台,它是一个开箱即用的可视化工具,具有功能齐全的度量仪表盘和图形编辑器,有灵活丰富的图形化选项,可以混合多种风格,支持多个数据源特点。Grafana + Zabbix 的组合方式,主要是通过插件的形式,将 Zabbix 接入到 Grafana 中,使其可以借助 Grafana 的 portal 进行统一的监控和管理。

The open platform for beautiful analytics and monitoring


扩展阅读

Grafana - https://grafana.com/


Grafana 入门

观看 10 分钟的初学者指南,以建立仪表板,以快速介绍设置仪表板和面板。
https://www.youtube.com/watch?v=sKNZMtoSHN4&index=7&list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2

阅读基本概念文档,以获得 Grafana 概念的速成关键课程
http://docs.grafana.org/guides/basic_concepts/

Grafana 安装

No matter where your data is, or what kind of database it lives in, you can bring it together with Grafana. Beautifully.

http://docs.grafana.org/installation/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# install grafana
cat > /etc/yum.repos.d/grafana.repo << 'EOF'
[grafana]
name=grafana
baseurl=https://packagecloud.io/grafana/stable/el/7/$basearch
gpgkey=https://packagecloud.io/gpg.key https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana
enabled=0
gpgcheck=1
EOF

# yum install grafana
yum --enablerepo=grafana -y install grafana initscripts fontconfig

# change anything if you need
vi /etc/grafana/grafana.ini

30 [server]
31 # Protocol (http, https, socket)
32 ;protocol = http
33
34 # The ip address to bind to, empty will bind to all interfaces
35 ;http_addr =
36
37 # The http port to use
38 ;http_port = 3000
39
40 # The public facing domain name used to access grafana from a browser
41 ;domain = localhost

# start and enable grafana
sudo systemctl start grafana-server
sudo systemctl enable grafana-server
sudo systemctl status grafana-server

# test
http://<ip>:3000
admin/admin

# 添加数据源
安装很简单,之后需要配置数据源比如 InfluxDB/Zabbix API 等才能发挥真正的作用

grafana-zabbix

https://grafana.com/plugins/alexanderzobnin-zabbix-app

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 安装 zabbix 插件(grafana-zabbix)
grafana-cli plugins install alexanderzobnin-zabbix-app
systemctl restart grafana-server

# 启用插件
进入 Plugins 界面,选择 “Zabbix”
开始面板(左上角图标) -> Plugins -> Apps -> Zabbix

# 添加 zabbix 数据源
进入 “添加数据源” 界面
开始面板(左上角图标) -> Data Sources -> Add data source

# 配置 Zabbix 数据源
配置完后,保存退出,配置的内容如下:
重要参数有:
Type: Zabbix
Url: http://192.168.56.103/zabbix/api_jsonrpc.php
Access: direct
(Zabbix API details)
Username: Admin(默认)
Password: zabbix(默认)

# 验证
之后,我们从 开始面板 -> Zabbix 中,便可看到 Zabbix 相关的监控数据。

Grafana Zabbix API Error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 如果在 Grafana Dashboard 看到 Zabbix API Error 建议查看 Zabbix front 如 Apache 或者 Nginx 的错误日志定位问题
tail -f /var/log/httpd/error_log

PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 72 bytes) in /usr/share/zabbix/include/func.inc.php on line 1423

# 发现超过 php 的 memory_limit=128M
vim /etc/php.ini
vim /etc/httpd/conf.d/zabbix.conf

memory_limit = 512M

# 增加 info.php 查看变量是否生效
vim /usr/share/zabbix/info.php

<?php
phpinfo();
?>

http://<your-zabbix-server/info.php

# 重启 httpd 生效
service httpd restart

前言

Jenkins 就不用做多余的介绍了,作为 CI/CD 首选的开源解决方案,持续集成 (Continous Intergration)/ 持续交付 (Continous Delievery),本文只是用于记录使用 Jenkins 的一些基本操作,Jenkins 官方文档也率先支持中文,相信对大家的学习热情会有积极地促进作用。

Jenkins 学习使用实践


扩展阅读

Jenkins - https://jenkins.io/zh/


Jenkins 简介

构建伟大,无所不能

Jenkins 是开源 CI&CD 软件领导者, 提供超过 1000 个插件来支持构建、部署、自动化,满足任何项目的需要。

Jenkins 用户手册 - https://jenkins.io/zh/doc/

Jenkins 训练营之基础篇 - https://ke.qq.com/course/265167
Jenkins 训练营之带你玩转 Pipeline - https://ke.qq.com/course/252785

https://ke.qq.com/webcourse/index.html#cid=265167&term_id=100312699&taid=1794918372871119&vid=p1423f5tn3g

https://ke.qq.com/webcourse/index.html?cw_id=91852&ac_type=3#cid=252785&term_id=100298102&taid=1707945285114737&type=1024&vid=w1422iqh9q1

Jenkins 安装

Jenkins 项目产生两个发行线,长期支持版本 (LTS) 和每周更新版本。 根据你的组织需求,一个可能比另一个更受欢迎。
两个版本都以 .war 文件,原生包,安装程序,和 Docker 容器的形式分发。
https://jenkins.io/zh/download/

这里推荐下载使用 LTS 长期支持版本,以 CentOS 7 作为演示环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Java 8
yum install java

# Jenkins stable version
sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key
yum install jenkins

# start jenkins
service jenkins start

# 初始化配置向导
http://192.168.56.103:8080/

cat /var/lib/jenkins/secrets/initialAdminPassword
5224fc83b6d84cc2be69a18c53309ea4

Install suggested plugins

是否创建管理员账户或者跳过

Jenkins 入门

主要的 Job 类型

Freestyle project
自由风格项目,Jenkins 最主要的项目类型

Maven Project
Maven 项目专用,类似 Freestyle,更简单

Multi-configuration project
多配置项目,适合需要大量不同配置 (环境,平台等) 构建

Pipeline
流水线项目,适合使用 pipeline(workflow)插件功能构建流水线任务,或者使用 Freestyle project 不容易实现的复杂任务

Multibranch Pipeline
多分支流水线项目,根据 SCM 仓库中的分支创建多个 Pipeline 项目

Freestyle 项目

General
项目基本配置
项目名字,描述,参数,禁用项目,并发构建,限制构建默认 node 等等

Source code Management
代码库信息,支持 Git,Subversion 等

Build Triggers
构建触发方式
周期性构建,Poll SCM,远程脚本触发构建,其他项目构建结束后触发等

Build Environment
构建环境相关设置
构建前删除 workspace,向 Console 输出添加时间戳,设置构建名称,插入环境变量等

Build
项目构建任务
添加 1 个或者多个构建步骤

Post-build Actions
构建后行为
Artifact 归档,邮件通知,发布单元测试报告,触发下游项目等等

规范项目必要配置

本规范尤其适用于较多项目共用同一 Jenkins 的场景

  • 项目命名规范
  • 设置项目描述
  • 设置历史构建清理规则
  • 设置构建节点 Label
  • 邮件通知

常用插件

注意 Jenkins 备份策略,建议结合 rsync 备份远端

Jenkins 显示时间戳: Timestamper
Jenkins 配置构建历史: Job Configuration History Plugin
Jenkins 定时的备份: ThinBackup
邮件发送插件: Email Extension Plugin
空间清理扩展插件: Distributed Workspace Clean plugin

Jenkins 常用插件 – https://vwin.github.io/2019/01/07/Jenkins%E9%AB%98%E6%95%88%E6%8F%92%E4%BB%B6%E6%95%B4%E7%90%86/

创建第一个 Job

安装 Timestamper 插件
系统管理 - 插件管理 - 可用插件,搜索到 timestamper 点击 Install without restart

新建一个 Freestyle 类型的 Job

  • General 项目名称: My-first-freestyle-demo
  • Build Environment 构建环境: 勾选 Add timestamps to the Console Output
  • Build 构建: 屏幕打印出 “这是我的第一个 Jenkins Job, oops”
  • Post-build Actions 构建后操作: 无
  • 点击立刻构建
  • 找到控制台输出
1
2
3
4
5
6
7
Console Output
14:40:59 Started by user admin
14:40:59 Building in workspace /var/lib/jenkins/workspace/My-first-freestyle-demo
14:41:00 [My-first-freestyle-demo] $ /bin/sh -xe /tmp/jenkins3737737887278720679.sh
14:41:00 + echo '这是我的第一个 Jenkins Job, oops'
14:41:00 这是我的第一个 Jenkins Job, oops
14:41:00 Finished: SUCCESS

Jenkins Pipeline 介绍

Pipeline,简而言之,就是一套运行于 Jenkins 上的工作流框架,将原本独立 运行于单个或者多个节点的任务连接起来,实现单个任务难以完成的复杂流程编排与可视化。

Pipeline 是 Jenkins2.X 最核心的特性,帮助 Jenkins 实现从 CI 到 CD 与 DevOps 的转变

什么是 Jenkins Pipeline?

Jenkins Pipeline 是一组插件,让 Jenkins 可以实现持续交付管道的落地和实施。持续交付管道 (CD Pipeline) 是将软件从版本控制阶段到交付给用户或客户的完 整过程的自动化表现。软件的每一次更改 (提交到源代码管理系统) 都要经过一个复杂的过程才能被发布。

Pipeline 提供了一组可扩展的工具,通过 Pipeline Domain Specific Language(DSL) syntax 可以达到 Pipeline as Code 的目的

Pipeline as Code:Jenkinsfile 存储在项目的源代码库

Jenkins Pipeline 核心概念

Stage
– 阶段,一个 Pipeline 可以划分为若干个 Stage,每个 Stage 代表一组操作,例如: “Build”, “Test”, “Deploy” 。
– 注意,Stage 是一个逻辑分组的概念,可以跨多个 Node。

Node
– 节点,一个 Node 就是一个 Jenkins 节点,或者是 Master,或者是 Agent,是执行 Step 的具体 运行环境。

Step
– 步骤,Step 是最基本的操作单元,小到创建一个目录,大到构建一个 Docker 镜像,由各类 Jenkins Plugin 提供,例如: sh ‘make’

为什么要用 Pipeline?

  • 代码: Pipeline 以代码的形式实现,通常被检入源代码控制,使团队能够编辑,审查和迭代其 CD 流程。
  • 可持续性: Jenkins 重启或者中断后都不会影响 Pipeline Job。
  • 停顿: Pipeline 可以选择停止并等待人工输入或批准,然后再继续 Pipeline 运行。
  • 多功能: Pipeline 支持现实世界的复杂 CD 要求,包括 fork/join 子进程,循环和 并行执行工作的能力。
  • 可扩展: Pipeline 插件支持其 DSL 的自定义扩展以及与其他插件集成的多个选项。

Pipeline 和 Freestyle 的区别

Freestyle:
– 上游 / 下游 Job 调度,如 BuildJob ->TestJob -> DeployJob
– 在 DSL Job 里面调度多个子 Job(利用 Build Flow plugin)

Pipeline:
– 单个 Job 中完成所有的任务编排
– 全局视图

Pipeline 会取代 Freestyle 么?

  • Pipeline 一定会取代 Build Flow 插件
  • 会,当你希望做到 Pipeline as code 的时候
  • 会,当你独立运行一组 Job 没有特殊价值或者意义的时候
  • 会,当你可以从 Multibranch Pipeline 受益的时候
  • 会,当你希望获取类似于 TravisCI 风格的工作流的时候

Jenkins Pipeline 入门

Pipeline 脚本是由 Groovy 语言实现
– 无需专门学习 Groovy

Pipeline 支持两种语法
– Declarative 声明式(在 Pipeline plugin 2.5 中引入)
– Scripted Pipeline 脚本式

如何创建基本的 Pipeline
– 直接在 Jenkins Web UI 网页界面中输入脚本
– 通过创建一个 Jenkinsfile 可以检入项目的源代码管理库

最佳实践
– 通常推荐在 Jenkins 中直接从源代码控制 (SCM) 中载入 Jenkinsfile Pipeline

快速创建一个简单的 Pipeline

  1. 新建 Job: Jenkins -> 新建 -> 输入 Job 名称: “My-first-pipeline-demo” -> 选择 Pipeline -> 点击 “OK”
  2. 配置: 在 Pipeline -> Script 文本输入框中输入下列语句,点击 ”保存”
  3. 立即构建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pipeline {
agent any
stages {
stage('Build') {
steps {
echo 'Build'
}
}
stage('Test') {
steps {
echo 'Test'
}
}
stage('Deploy') {
steps {
echo 'Deploy'
}
}
}
}

Jenkins 忘记密码怎么办

如果权限设置错误,或者忘记密码,导致 admin 自己都无法登陆 Jenkins 怎么办?

  • 命令行停止 Jenkins;
  • 先备份 $JENKINS_HOME 中的 config.xml;
  • 用编辑器打开 $JENKINS_HOME 中的 config.xml;
  • 将 < useSecurity>true 元素中的 true 改为 false;
  • 将 < authorizationStrategy > 和 < securityRealm > 元素的内容删掉;
  • 命令行启动 Jenkins。

Ansible Jenkins API Token 使用技巧

Jenkins REST API 提供了 API token,使得可以在程序中使用 API token 进行认证(而不是使用你真实的密码)。

API token 可以在用户个人设置界面查看
到用户→用户 id→设置页面,在 API Token 区域点击 Show API token 按钮,便可查看 API token,同时还可以更改 API token
相应的 URL 是
http://<JENKINS_URL>/user//configure

Manage Jenkins jobs by using Jenkins REST API

jenkins_job_facts – Get facts about Jenkins jobs
https://docs.ansible.com/ansible/latest/modules/jenkins_job_facts_module.html

jenkins_job – Manage jenkins jobs
https://docs.ansible.com/ansible/latest/modules/jenkins_job_module.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# python-jenkins package
pip install python-jenkins lxml

# jenkins_job_facts.yml
---
- hosts: all
gather_facts: no

tasks:
- name: Get host info
local_action:
module: jenkins_job_facts
url: https://xxx
user: xxx
token: xxx
glob: '*mh_kg*'
register: my_jenkins_job_facts

- debug:
msg: "{{my_jenkins_job_facts}}"

# shell awk
cat reg | grep '"name"' | awk -F '"' '{print $4}'

# jenkins_job_delete.yml
---
- hosts: all
gather_facts: no

tasks:
- name: Delete jobs
local_action:
module: jenkins_job
url: xxx
user: xxx
token: xxx
name: "{{ item }}"
state: absent
loop:
- gop_live___mh_kg_garena_auth_nginx_static_files
- gop_live___mh_kg_nginx_reload
- gop_live___mh_kg_nginx_update_config
- gop_live___mh_kg_restart_service
- gop_live___mh_kg_setup_server
- gop_live___mh_kg_stop_service
- gop_live_mh_kg_app_point
- gop_live_mh_kg_game_service
- gop_live_mh_kg_payment_center_backend
- gop_live_mh_kg_sso_website

参考内容

官方手册永远是你的最佳参考内容

Jenkins 用户手册 - https://jenkins.io/zh/doc/

前言

Nginx 官网有介绍各种监控方案,以前我们常用 stub_status 和 Log 日志来实现 Nginx 监控。本文主要介绍基于 Prometheus 的 2 种监控方案 nginx-lua-prometheus 和 nginx-vts-exporter,在真实的生产环境中你可能需要研究和选择哪种方法才是适合你的,F5 收购 Nginx 后的未来让我们一起拭目以待。

Prometheus 监控 Nginx


扩展阅读

Monitoring NGINX - https://www.nginx.com/blog/monitoring-nginx/


Nginx 监控

官网介绍的监控方案 - https://www.nginx.com/blog/monitoring-nginx/

Prometheus 集成的 HTTP exporter 方案 - https://prometheus.io/docs/instrumenting/exporters/#http

聊聊 Nginx 的监控 - https://zhuanlan.zhihu.com/p/34050716

使用 rpmbuild 制作 Nginx 的 RPM 包 - https://wsgzao.github.io/post/rpmbuild/

Prometheus 监控 Nginx

nginx-lua-prometheus

Nginx 需要添加 Lua 扩展

https://github.com/knyar/nginx-lua-prometheus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 下载 redis_exporter
https://github.com/knyar/nginx-lua-prometheus/releases
wget https://github.com/knyar/nginx-lua-prometheus/archive/0.20181120.tar.gz
tar xf 0.20181120.tar.gz
cd nginx-lua-prometheus-0.20181120

# 创建 prometheus.lua 目录
mkdir -p /etc/nginx/lua/
cp prometheus.lua /etc/nginx/lua/

# 编辑 nginx 配置文件修改,注意修改 lua_package_path "/etc/nginx/lua/prometheus.lua";
vim /etc/nginx/nginx.conf

lua_shared_dict prometheus_metrics 10M;
lua_package_path "/etc/nginx/lua/prometheus.lua";
init_by_lua '
prometheus = require("prometheus").init("prometheus_metrics")
metric_requests = prometheus:counter(
"nginx_http_requests_total","Number of HTTP requests", {"host","status"})
metric_latency = prometheus:histogram(
"nginx_http_request_duration_seconds","HTTP request latency", {"host"})
metric_connections = prometheus:gauge(
"nginx_http_connections","Number of HTTP connections", {"state"})
';
log_by_lua '
metric_requests:inc(1, {ngx.var.server_name, ngx.var.status})
metric_latency:observe(tonumber(ngx.var.request_time), {ngx.var.server_name})
';

# 创建 nginx-lua-prometheus
vim /etc/nginx/sites-available/nginx-lua-prometheus

server {
listen 9145;
location /metrics {
content_by_lua '
metric_connections:set(ngx.var.connections_reading, {"reading"})
metric_connections:set(ngx.var.connections_waiting, {"waiting"})
metric_connections:set(ngx.var.connections_writing, {"writing"})
prometheus:collect()
';
}
}

# 创建软链接
cd /etc/nginx/sites-enabled/
ln -s ../sites-available/nginx-lua-prometheus

# 测试 Nginx 语法并 reload 测试 metrics
nginx -t
nginx -s reload
curl http://127.0.0.1:9145/metrics

# iptables rule for Prometheus Nginx
-A INPUT -s xxx -p tcp --dport 9145 -j ACCEPT

nginx-vts-exporter

https://github.com/hnlq715/nginx-vts-exporter

对方正在输入中

Grafana

nginx-lua-prometheus

https://grafana.com/dashboards/462

nginx-vts-exporter

https://grafana.com/dashboards/2949

参考文献

https://prometheus.io/docs/instrumenting/exporters/#http

前言

Prometheus Exporter for Redis Metrics. Supports Redis 2.x, 3.x, 4.x, and 5.x

Prometheus 监控 Redis


扩展阅读

redis_exporter - https://github.com/oliver006/redis_exporter


Redis export 安装和使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 下载 redis_exporter
https://github.com/oliver006/redis_exporter/releases
cd /tmp
wget https://github.com/oliver006/redis_exporter/releases/download/v0.30.0/redis_exporter-v0.30.0.linux-amd64.tar.gz
tar xf redis_exporter-v0.30.0.linux-amd64.tar.gz

# 后台启动 redis_exporter
nohup ./redis_exporter -redis.addr redis://<IP>:<PORT> &

# iptables rule for Prometheus Redis
-A INPUT -s xxx -p tcp --dport 9121 -j ACCEPT

# 测试 URL 获取 metrics
curl http://<IP>:9121/metrics

Grafana

Memory Usage 中 redis_config_maxmemory 如果无法获取到数据请自行定义 value

https://grafana.com/dashboards/763

参考文献

https://prometheus.io/docs/instrumenting/exporters/#databases

前言

  • 转载自

https://wsgzao.github.io/post/influxdb/

InfluxDB 是专为时序数据设计的数据库,能支撑大量的读写负载,是一个高性能的时序数据 datastore。

InfluxDB is the Time Series Database in the TICK Stack


扩展阅读

InfluxDB - https://www.influxdata.com/time-series-platform/influxdb/


InfluxDB 简介

InfluxDB is the Time Series Database in the TICK Stack

InfluxData’s TICK Stack is built around InfluxDB to handle massive amounts of time-stamped information. This time series database provides support for your metrics analysis needs, from DevOps Monitoring, IoT Sensor data, and Real-Time Analytics. Users can adapt their SQL skills with InfluxQL, so they can get up to speed on this time series database.

默认预留端口:

8086,HTTP API
8088,RPC 端口,用于备份和恢复

NTP 服务:
InfluxDB 使用机器本地时间作为 timestamp,需要机器之间使用 NTP 进行同步;如果没有同步的话,写入的时间序列数据可能会不准确

InfluxDB 安装

InfluxDB 里存储的数据被称为时间序列数据, InfluxDB 存储方式跟传统关系型数据库不同的是:传统关系型数据库通过数据库 + 表 + 字段组织数据,InfluxDB 通过指标、标签、字段组织数据,时间戳是默认的索引列,标签跟字段其实就相当于关系型数据库中的字段,只不过标签会被索引,而字段不会。

Grafana 默认支持的数据源:Graphite,InfluxDB,OpenTSDB,Prometheus,Elasticsearch,CloudWatch
Grafana 支持同时绑定多套数据源,根据自己需求管理即可,这里以 InfluxDB 为例。

https://portal.influxdata.com/downloads

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# install influxdb
cat > /etc/yum.repos.d/influxdb.repo << 'EOF'
[influxdb]
name = InfluxDB Repository - RHEL \$releasever
baseurl = https://repos.influxdata.com/rhel/\$releasever/\$basearch/stable
enabled = 1
gpgcheck = 1
gpgkey = https://repos.influxdata.com/influxdb.key
EOF

# yum install influxdb
yum install -y influxdb

# start and enable influxdb
sudo systemctl start influxdb
sudo systemctl enable influxdb
sudo systemctl status influxdb

# 通过 influx 命令进入 cli 命令行
influx
Connected to http://localhost:8086 version 1.4.2
InfluxDB shell version: 1.4.2
>

# 查看用户
SHOW USERS

# 创建用户
CREATE USER influx WITH PASSWORD 'influx' WITH ALL PRIVILEGES

# 查看用户
SHOW USERS

# 创建数据库
CREATE DATABASE test

# 查看数据库
SHOW DATABASES

# Using 数据库
USE test

# 插入数据
INSERT cpu,host=192.168.1.1 load=0.1,usage=0.2

# 查询所有数据
SELECT * FROM "cpu"
SELECT "host","load","usage" FROM "cpu"

# 根据条件查询
SELECT "host","load","usage" FROM "cpu" WHERE "host" = '192.168.1.1'
SELECT "host","load","usage" FROM "cpu" WHERE "usage" > 0.1

# 创建数据库
CREATE DATABASE "db_name"
# 显示所有数据库
SHOW DATABASES
# 删除数据库
DROP DATABASE "db_name"

# 使用数据库
USE mydb
# 显示该数据库中的表
SHOW MEASUREMENTS
# 删除表
DROP MEASUREMENT "t_name"

# 创建数据库 API
curl -i -XPOST http://localhost:8086/query --data-urlencode "q=CREATE DATABASE test"

# 写入数据 API

# 写入单条
curl -i -XPOST http://localhost:8086/write?db=test --data-binary "cpu,host=192.168.1.3 load=0.1,usage=0.33"
curl -i -XPOST http://localhost:8086/write?db=test --data-binary "cpu,host=192.168.1.3 load=0.1,usage=0.33 6666666666666666666"

# 写入多条
curl -i -XPOST http://localhost:8086/write?db=test --data-binary "cpu,host=192.168.1.2 load=0.1,usage=0.22 1666666666666666661
cpu,host=192.168.1.3 load=0.1,usage=0.33 1666666666666666661
cpu,host=192.168.1.2 load=0.2,usage=0.22 1666666666666666662
cpu,host=192.168.1.3 load=0.2,usage=0.33 1666666666666666662"

# 查询数据 API
curl -G http://localhost:8086/query?db=test --data-urlencode "q=SELECT * FROM \"cpu\""

InfluxDB 配置优化

配置文件默认全部注释掉,使用默认的配置项,可以根据需要配置。每个配置项有对应的、相同功能的环境变量。

配置文件:/etc/influxdb/influxdb.conf

查看配置:influxd config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[meta]                                # 元数据 
dir = /var/lib/influxdb/meta

[data] # 数据 / WAL
dir = /var/lib/influxdb/data
wal-dir = /var/lib/influxdb/wal
cache-max-memory-size = "1g" # Cache 最大可用内存

[coordinator] # 查询相关
query-timeout = "0s" # 查询最大执行时间
log-queries-after = "0s" # 打印慢查询

[http] # HTTP 服务
auth-enabled = false # 启用安全认证
max-connection-limit = 0 # 最大连接数

更多帮助信息请参考官网
InfluxDB documentation - https://docs.influxdata.com/platform/introduction

前言

持续集成介绍

我比较懒,这里我就不介绍了,直接贴连接:持续集成是什么?

流程

公司的发布流程分为四个阶段:DEV->SIT->UAT->PROD ,这也代表着,我们的持续集成(CI)也分同样的四个阶段.

DEV和SIT都是在内网,可以通过ssh直连,但是UAT和PROD在外网,开发不能直接更新,只能通过丢relup的release包给到运维那边去处理

因此现在如果要做CI的话,前两个环境通过ssh直接操作,后两个只能是打包了

公司用的是gitlab的CI,所以下面我们来看看如何用gitlab的CI去完成erlang发布应用的持续集成.

DEV环境

大致步骤

  1. 编译阶段,通过则进入下一步,不通过发送通知
  2. 测试阶段,通过则进入下一步,不通过发送通知
  3. 部署阶段,不通过发送通知
    获取当前版本号备用
    如果存在OLD_VSN的环境变量,跳到5, 否则下一步
    通过ssh获取目标机器运行中的版本号
    判断从ssh获取到的内容,如果是机器没有运行,则直接打包发布,否知下一步
    对比当前版本和旧版本,相同则不需要升级和发布然后退出,不相同则下一步
    通过代码生成relup发布包,然后ssh到目标机器升级版本

具体流程图

note:上面这个流程图只是DEV,其他环境的会有不同,之后会说到

实现

创建项目

rebar3 new app urf, urf=upgrade release flow

Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
APP_NAME = urf
all: compile test
APP_VER=$(shell awk '/release_vsn/{ print $$1 }' rebar.config | tr -d \")
###===================================================================
### build
###===================================================================
.PHONY: get-deps co compile run rel_tar
get-deps:
rebar3 get-deps
co:compile
compile: get-deps
rebar3 compile
### clean
.PHONY: clean distclean
clean:
rebar3 clean
distclean: test_clean
rebar3 clean -a
rel_tar:
rebar3 tar
###===================================================================
### test
###===================================================================
.PHONY: test eunit ct test_shell test_run test_clean
test: epmd
rebar3 do eunit -v, ct -v, cover -v
eunit: epmd
rebar3 do eunit -v, cover
ct: epmd
rebar3 do ct -v, cover
test_shell:
rebar3 as test compile
erl -pa _build/test/lib/*/ebin -pa _build/test/lib/$(APP_NAME)/test
test_run: epmd
rebar3 as test shell
test_clean:
@rm -rf _build/test/lib/$(APP_NAME)/test _build/test/logs _build/test/cover
###===================================================================
### relup
###===================================================================
.PHONY: tag up_vsn up_app_vsn up_relx_vsn clean_appup build_old_vsn relup check_appup
tag:
git tag $(APP_VER)
up_vsn: up_app_vsn up_relx_vsn
up_app_vsn:
@exec script/up_app_vsn.sh
up_relx_vsn:
@exec script/up_relx_vsn.sh
clean_appup:
rebar3 appup clean
rm -f _build/default/lib/*/ebin/*.appup
build_old_vsn:
@exec script/build_old_vsn.sh
relup:
@exec script/relup.sh
check_appup:
@exec script/check_appup.sh
###===================================================================
### other
###===================================================================
.PHONY: help tree logtail epmd
help:
rebar3 help
tree:
rebar3 tree
epmd:
@pgrep epmd 2> /dev/null > /dev/null || epmd -daemon || true

上面部分除了比较常见的build & test 部分外还增加了relup的一些命令

tag: 给当前版本打tag

up_app_vsn: 修改src/*.app.src文件的版本号,最后一位+1

up_relx_vsn: 修改rebar.config文件的版本号,最后一位+1

up_vsn: up_app_vsn + up_relx_vsn

clean_appup: 清理appup文件,打包relup文件前必须清理一次,不然appup插件检测到应用已经存在appup文件就不会再生成新的了

build_old_vsn: 打包旧的版本,下面会放出脚本文件

relup: 打包relup版本,下面会放出脚本文件

check_appup: 打印所有应用下的appup文件

rebar.config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{erl_opts, [
encrypt_debug_info
]}.
{plugins, [
{rebar3_appup_plugin, "2.2.1"}
]}.
{deps, []}.
%% for test
{cover_enabled, true}.
{cover_print_enabled, true}.
{eunit_opts, [
{dir, ["test/eunit"]}
]}.
{ct_opts, [
{dir, ["test/ct"]},
{readable, true}
]}.
%% for make release
{relx, [
{include_erts, true},
{system_libs, true},
{dev_mode, false},
{extended_start_script, true},
{sys_config, "config/sys.config"},
{vm_args, "config/vm.args"},
{release, {urf,
"0.1.0" %% release_vsn
}, [urf]}
]}.

首先,应用发布升级,当然少不了增加appup插件:
1
2
3
{plugins, [
{rebar3_appup_plugin, "2.2.1"}
]}.

其次是增加编译保护:
1
2
3
{erl_opts, [
encrypt_debug_info
]}.

增加这个encrypt_debug_info的主要原因是因为我们编译的代码必须要带有debug_info,不然appup插件会报错,但是同时增加了debug_info之后编译出来的beam文件是可以被反编译的,所以需要增加保护,另外增加了这个只是保护了当前这个app,但是依赖的其他app并不会有保护,后面我们会讲到使用rebar.config.script文件来将deps的其他app也保护起来.

增加了上面这个保护还需要在项目根目录下增加一个.erlang.crypt文件,内容类似如下:

[{debug_info, des3_cbc, [], "12345678912345678912345678912345"}].
最后是增加relx:

1
2
3
4
5
6
7
8
9
10
11
{relx, [
{include_erts, true},
{system_libs, true},
{dev_mode, false},
{extended_start_script, true},
{sys_config, "config/sys.config"},
{vm_args, "config/vm.args"},
{release, {urf,
"0.1.0" %% release_vsn
}, [urf]}
]}.

可以看到版本好附近有一个注释release_vsn,这个注释的主要作用标记版本号,脚本会通过这个标记找到版本号

rebar.config.script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
%%main(CONFIG) ->
CONFIG_1 =
case lists:keyfind(add_overrides, 1, CONFIG) of
false -> CONFIG;
{_, AddOverrides} ->
Overrides = lists:concat([[{add, App, AddOverride} || App <- Apps] || {Apps, AddOverride} <- AddOverrides]),
OldOverrides = proplists:get_value(overrides, CONFIG, []),
lists:keystore(overrides, 1, CONFIG, {overrides, Overrides ++ OldOverrides})
end,
IsCI = os:getenv("CI_COMMIT_SHA") =/= false,
IsRelup = os:getenv("RELUP_TAR") =/= false,
ChangeProfileFun =
fun(test, ProfileConfig0) -> {test, ProfileConfig0};
(ProfileName, ProfileConfig0) ->
ProfileConfig1 = maps:from_list(ProfileConfig0),
Relx0 = maps:get(relx, ProfileConfig1, []),
Relx2 =
case IsCI of
false -> Relx0;
_ ->
Relx1 = lists:keystore(include_erts, 1, Relx0, {include_erts, true}),
lists:keystore(system_libs, 1, Relx1, {system_libs, true})
end,
Relx =
case IsRelup of
false -> Relx2;
_ ->
Relx3 = lists:keystore(include_erts, 1, Relx2, {include_erts, false}),
lists:keystore(system_libs, 1, Relx3, {system_libs, false})
end,
ProfileConfig = maps:to_list(ProfileConfig1#{relx => Relx}),
{ProfileName, ProfileConfig}
end,
Profiles0 = proplists:get_value(profiles, CONFIG_1, []),
Profiles = [ChangeProfileFun(RebarProfile, ProfileConfig) ||
{RebarProfile, ProfileConfig} <- Profiles0],
CONFIG_2 = lists:keystore(profiles, 1, CONFIG_1, {profiles, Profiles}),
{_, CONFIG_3} = ChangeProfileFun(none, CONFIG_2),
LAST_CONFIG =
case IsCI of
false -> CONFIG_3;
_ ->
lists:keystore(global_rebar_dir, 1, CONFIG_3, {global_rebar_dir, "_build/rebar3"})
end,
%% io:format("~n~p~n", [LAST_CONFIG]),
LAST_CONFIG.

rebar.config.script不熟悉的同学我这里简单介绍一下:

作用:

通过执行erlang脚本动态的修改rebar.config的内容

运行原理

在运行rebar3相关的命令之后,rebar3首先会读取rebar.config,然后将所有内容复制给CONFIG这个变量,然后再执行rebar.config.script这个文件,运行结束之后的结果就是最终的rebar的配置了

然后再来说说上面的代码:

add_overrides主要是为了给deps的其他app加上保护,用法如下:

增加到rebar.config文件里:

1
2
3
{add_overrides, [
{[app1,app2,app3,...], [{erl_opts, [encrypt_debug_info]}]}
]}.

第二部分是根据环境变量来判断是否在ci环境和是否保护RELUP_TAR里

一般不变更otp的版本和依赖不增加系统库的前提下,我们打包relup的包是不需要再包含erts和系统库,这样可以大大减少打包之后文件的大小, 因此我们在打包relup的时候,会在export变量 RELUP_TAR 到环境变量,这样只要在脚本里判断如果有这个变量则设置include_erts和system_libs为false.
另外一般我们的开发机器是ubuntu,但是线上的环境是centos,所以我们本地编译线上的包的时候会制定centos版的erlang的位置,幸运的是,我们的ci的runner的环境也是centos,因此我们如果判断到是在ci环境里时,只要设置成true就行了.
在ci环境,我们会使用cache,但是gitlab的ci的cache只会保存项目下的文件,因此像一些保存在~/.cache/rebar3下的hex包就不会被保存,这样我们每次编译的时候都要去拉一遍hex的pkg,幸好,rebar3可以修改这些文件的位置,只要我们设置global_rebar_dir的位置为项目下的_build/rebar3,之后我们再设置cache保存这个文件夹就完美解决这个问题了.

ps:可以看到第一行有一句注释,这句注释并不是必须的,其实它主要左右是我在idea里修改这个文件的时候,会打开这个注释,然后这个文件就变成了一个方法了,所有修改的时候格式也不会变化,同时也可以方便的用idea的格式化功能,算是一个小技巧,不过最后记得用完重新注释第一行,不然rebar3运行的时候会报错

.gitlab.ci.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
image: cerl
before_script:
- eval $(ssh-agent -s)
- ssh-add <(echo "$SSH_PRIVATE_KEY")
- mkdir -p ~/.ssh
- echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
after_script:
- rm -rf _build/default/lib/urf
- rm -rf ~/.git-credentials
stages:
- build
- test
- deploy
cache:
paths:
- _build/default
- _build/rebar3
build:
stage: build
except:
- tags
script:
- make compile || ./script/ci_notice.sh "Build Fail"
tags:
- dockers
test:
stage: test
coverage: '/\|\s*total\s*\|\s*(\d+)%\s*\|/'
except:
- tags
script:
- make test || ./script/ci_notice.sh "Test Fail"
tags:
- dockers
deploy_dev:
stage: deploy
only:
- /^d_dev$/
except:
- tags
script:
- ./script/ci_deploy.sh || ./script/ci_notice.sh "Deploy Fail"
environment:
name: dev
variables:
SSH_USER_HOST: "user@server"
SERVER: "dev"
tags:
- dockers

对于gitlab 的 CI 不是很熟悉的同学可以看一下官方文档,文档里会给你很详尽的说明,同时也建议大家先系统性的学习一遍.gitlab.ci.yml文档的配置再回来看这篇文章.

之前说到过我们有三个阶段:

1
2
3
4
stages:
- build # 编译阶段
- test # 测试阶段
- deploy # 部署阶段

cache这一部分,ci有cache会大大减少整个流程的执行时间,这里cache了两个文件夹default和rebar3文件夹就够了
1
2
3
4
cache:
paths:
- _build/default
- _build/rebar3

build和test两个阶段比较简单,build就用make compile, test的话make test,另外如果想看到测试的代码覆盖率的话可以增加coverage,增加之后gitlab的页面那里就能显示这次的代码覆盖率了,其他就不一一详细说明了,主要来看看deploy阶段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
deploy_dev:
stage: deploy
only:
- /^d_dev$/
except:
- tags
script:
- ./script/ci_deploy.sh || ./script/ci_notice.sh "Deploy Fail"
environment:
name: dev
variables:
SSH_USER_HOST: "user@server"
REBAR_PROFILE: "dev"
tags:
- dockers

这个阶段我们只针对d_dev这个分支,所以如果我们要部署或者升级dev环境,就只需要切换到这个分支然后合并最新的代码,最后推送这个分支到服务器,然后ci会帮你解决一切了,当然还是要通过我们写的脚本ci_deploy.sh来解决.

ci_deploy.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#!/usr/bin/env bash
case $OSTYPE in
darwin*) SCRIPT=$(readlink $0 || true);;
*) SCRIPT=$(readlink -f $0 || true);;
esac
[ -z $SCRIPT ] && SCRIPT=$0
SCRIPT_DIR="$(cd `dirname "$SCRIPT"` && pwd -P)"
cd "$SCRIPT_DIR/.."
if [ -z $SSH_USER_HOST ]
then
echo "empty SSH_USER_HOST"
exit 1
fi
if [ -z $SERVER ]
then
echo "empty SERVER"
exit 1
fi
REL_NAME=$(awk -F '[,{]' '/\{release, \{/{ print $4 }' rebar.config)
NEW_VSN=$(awk '/release_vsn/{ print $1 }' rebar.config | tr -d \")
[ -z $TARGET_DIR ] && TARGET_DIR="/data/$REL_NAME"
export TARGET_DIR=$TARGET_DIR
export REBAR_PROFILE=$SERVER
TAR_FILE0="$REL_NAME-$NEW_VSN.tar.gz"
TAR_FILE="_build/$REBAR_PROFILE/rel/$REL_NAME/$TAR_FILE0"
export OLD_VSN=`exec $SCRIPT_DIR/ssh_get_vsn.sh`
only_send_and_start() {
echo "rebar3 tar" && \
rebar3 tar && \
echo "scp $TAR_FILE $SSH_USER_HOST:$TARGET_DIR" && \
scp $TAR_FILE $SSH_USER_HOST:$TARGET_DIR && \
echo "ssh $SSH_USER_HOST \"cd $TARGET_DIR && tar zxf $TAR_FILE0 && rm -f $TAR_FILE0 && ./bin/$REL_NAME start\"" && \
ssh $SSH_USER_HOST "cd $TARGET_DIR && tar zxf $TAR_FILE0 && rm -f $TAR_FILE0 && ./bin/$REL_NAME start" && \
echo "run successfully"
}
case $OLD_VSN in
"miss_file")
only_send_and_start;;
"Node is not running!")
only_send_and_start;;
$NEW_VSN)
echo "same vsn : $NEW_VSN";;
*)
$SCRIPT_DIR/build_old_vsn.sh && \
echo "rebar3 appup clean" && \
rebar3 appup clean && \
echo "rm -f _build/default/lib/*/ebin/*.appup" && \
rm -f _build/default/lib/*/ebin/*.appup && \
echo "rebar3 get-deps" && \
rebar3 get-deps && \
echo "rebar3 release" && \
rebar3 release && \
echo "rebar3 appup compile" && \
rebar3 appup compile && \
echo "rebar3 appup generate --previous_version $OLD_VSN" && \
rebar3 appup generate --previous_version $OLD_VSN && \
echo "rebar3 relup --upfrom $OLD_VSN" && \
rebar3 relup --upfrom $OLD_VSN && \
echo "rebar3 tar" && \
rebar3 tar && \
echo "scp $TAR_FILE $SSH_USER_HOST:$TARGET_DIR/releases" && \
scp $TAR_FILE $SSH_USER_HOST:$TARGET_DIR/releases && \
echo "ssh $SSH_USER_HOST \"cd $TARGET_DIR && ./bin/$REL_NAME upgrade $NEW_VSN\"" && \
ssh $SSH_USER_HOST "cd $TARGET_DIR && ./bin/$REL_NAME upgrade $NEW_VSN" && \
echo "upgrade successfully";;
esac

ssh_get_vsn.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env bash
case $OSTYPE in
darwin*) SCRIPT=$(readlink $0 || true);;
*) SCRIPT=$(readlink -f $0 || true);;
esac
[ -z $SCRIPT ] && SCRIPT=$0
SCRIPT_DIR="$(cd `dirname "$SCRIPT"` && pwd -P)"
cd "$SCRIPT_DIR/.."
if [ -z $SSH_USER_HOST ]
then
echo "empty SSH_USER_HOST"
exit 1
fi
if [ -z $TARGET_DIR ]
then
echo "empty TARGET_DIR"
exit 1
fi
REL_NAME=$(awk -F '[,{]' '/\{release, \{/{ print $4 }' rebar.config)
ssh_cmd_get_versions="([ -f $TARGET_DIR/bin/$REL_NAME ] && cd $TARGET_DIR ; ./bin/$REL_NAME versions | tr -d '\n') || ([ ! -f $TARGET_DIR/bin/$REL_NAME ] && mkdir -p $TARGET_DIR ; echo miss_file)"
#echo "ssh $SSH_USER_HOST \"${ssh_cmd_get_versions}\""
versions=`ssh $SSH_USER_HOST "${ssh_cmd_get_versions}"`
case $versions in
"miss_file")
echo "miss_file";;
"Node is not running!")
echo "Node is not running!";;
*)
echo $(echo $versions | sed -r "s/.*\*\ ([0-9]+.[0-9]+.[0-9]+)\ permanent.*/\1/g")
esac

上面的这些脚本其实只是之前的流程的转义,我这里就不重复了.

第二章的时候,我们已经接触过sys模块了,现在我们来进一步看一下源码的实现.

我们还是用回第二章的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-module(sync_code_reload).
-behaviour(gen_server).
-export([start/0, vsn/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-record(state, {}).
start() ->
gen_server:start(?MODULE, [], []).
vsn() -> 1.
init([]) ->
{ok, #state{}}.
handle_call(_Request, _From, State) ->
{reply, ok, State}.
handle_cast(_Request, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, {state}, _Extra) ->
io:format("old:~p, ex:~p~n", [_OldVsn, _Extra]),
{ok, #state{}}.

先从进程启动的位置开始切入:

gen_server.erl:

1
2
start_link(Mod, Args, Options) ->
gen:start(?MODULE, link, Mod, Args, Options).

进入gen模块最后会回调本模块的init_it方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
init_it(Starter, self, Name, Mod, Args, Options) ->
init_it(Starter, self(), Name, Mod, Args, Options);
init_it(Starter, Parent, Name0, Mod, Args, Options) ->
Name = gen:name(Name0),
Debug = gen:debug_options(Name, Options),
case catch Mod:init(Args) of
{ok, State} ->
proc_lib:init_ack(Starter, {ok, self()}),
loop(Parent, Name, State, Mod, infinity, Debug);
{ok, State, Timeout} ->
proc_lib:init_ack(Starter, {ok, self()}),
loop(Parent, Name, State, Mod, Timeout, Debug);
{stop, Reason} ->
%% For consistency, we must make sure that the
%% registered name (if any) is unregistered before
%% the parent process is notified about the failure.
%% (Otherwise, the parent process could get
%% an 'already_started' error if it immediately
%% tried starting the process again.)
gen:unregister_name(Name0),
proc_lib:init_ack(Starter, {error, Reason}),
exit(Reason);
ignore ->
gen:unregister_name(Name0),
proc_lib:init_ack(Starter, ignore),
exit(normal);
{'EXIT', Reason} ->
gen:unregister_name(Name0),
proc_lib:init_ack(Starter, {error, Reason}),
exit(Reason);
Else ->
Error = {bad_return_value, Else},
proc_lib:init_ack(Starter, {error, Error}),
exit(Error)
end.

进入init_it之后会调用回调模块的init方法回去初始状态,然后进入loop:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
loop(Parent, Name, State, Mod, hibernate, Debug) ->
proc_lib:hibernate(?MODULE,wake_hib,[Parent, Name, State, Mod, Debug]);
loop(Parent, Name, State, Mod, Time, Debug) ->
Msg = receive
Input ->
Input
after Time ->
timeout
end,
decode_msg(Msg, Parent, Name, State, Mod, Time, Debug, false).
进入loop之后是等待消息,当接受到消息之后会进入decode_msg处理:

decode_msg(Msg, Parent, Name, State, Mod, Time, Debug, Hib) ->
case Msg of
{system, From, Req} ->
sys:handle_system_msg(Req, From, Parent, ?MODULE, Debug,
[Name, State, Mod, Time], Hib);
{'EXIT', Parent, Reason} ->
terminate(Reason, Name, Msg, Mod, State, Debug);
_Msg when Debug =:= [] ->
handle_msg(Msg, Parent, Name, State, Mod);
_Msg ->
Debug1 = sys:handle_debug(Debug, fun print_event/3,
Name, {in, Msg}),
handle_msg(Msg, Parent, Name, State, Mod, Debug1)
end.

接受到系统消息{system, From, Req}会调用sys:handle_system_msg方法处理,如果是普通消息会进入handle_msg处理,处理完普通消息最后会回到loop,现在我们来看看sys:suspend/1 做了什么:
1
2
3
4
5
6
7
suspend(Name) -> send_system_msg(Name, suspend). %% 发送一个系统消息到进程
...
send_system_msg(Name, Request) ->
case catch gen:call(Name, system, Request) of
{ok,Res} -> Res;
{'EXIT', Reason} -> exit({Reason, mfa(Name, Request)})
end.

就是发送一个系统消息给进程,通过之前的分析,我们知道gen_server处理系统消息是通过调用sys:handle_system_msg方法来处理的,直接看调用sys:handle_system_msg方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
handle_system_msg(Msg, From, Parent, Mod, Debug, Misc, Hib) ->
handle_system_msg(running, Msg, From, Parent, Mod, Debug, Misc, Hib).
handle_system_msg(SysState, Msg, From, Parent, Mod, Debug, Misc, Hib) ->
case do_cmd(SysState, Msg, Parent, Mod, Debug, Misc) of %% Msg = suspend
{suspended, Reply, NDebug, NMisc} ->
_ = gen:reply(From, Reply),
suspend_loop(suspended, Parent, Mod, NDebug, NMisc, Hib); %% 进入suspend_loop
{running, Reply, NDebug, NMisc} ->
_ = gen:reply(From, Reply),
Mod:system_continue(Parent, NDebug, NMisc);
{{terminating, Reason}, Reply, NDebug, NMisc} ->
_ = gen:reply(From, Reply),
Mod:system_terminate(Reason, Parent, NDebug, NMisc)
end.

进入do_cmd:
1
2
do_cmd(_, suspend, _Parent, _Mod, Debug, Misc) ->
{suspended, ok, Debug, Misc};

返回一个状态suspended:然后进入suspend_loop:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
%% 进入 suspend_loop 之后就停留在receive上,只处理系统消息和异常,不再处理其他消息也就是暂停处理其他消息了
%% 也就是从gen_server:loop转换到sys:suspend_loop
suspend_loop(SysState, Parent, Mod, Debug, Misc, Hib) ->
case Hib of
true ->
suspend_loop_hib(SysState, Parent, Mod, Debug, Misc, Hib);
_ ->
receive
{system, From, Msg} ->
handle_system_msg(SysState, Msg, From, Parent, Mod, Debug, Misc, Hib);
{'EXIT', Parent, Reason} ->
Mod:system_terminate(Reason, Parent, Debug, Misc)
end
end.

从源代码中我们可以看到 sys:suspend/1 的主要认为就是从gen_server:loop转换到sys:suspend_loop ,而这个方法只处理了系统消息和异常退出消息,其他消息是不出理的,其他消息会留在邮箱里,这也就完成了暂停了.

这时候我们应该执行加载新代码模块进入vm:l(sync_code_reload).;然后再调用sys:change_code/4 通知到进程去回调模块的code_change方法对内部状态做相应的改变:

sys.erl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
change_code(Name, Mod, Vsn, Extra) -> %% 仍然是发送系统消息
send_system_msg(Name, {change_code, Mod, Vsn, Extra}).
...
%% 从handle_system_msg进入do_cmd
do_cmd(suspended, {change_code, Module, Vsn, Extra}, _Parent,
Mod, Debug, Misc) ->
{Res, NMisc} = do_change_code(Mod, Module, Vsn, Extra, Misc), %% 进入 do_change_code
{suspended, Res, Debug, NMisc};
do_cmd(SysState, Other, _Parent, _Mod, Debug, Misc) ->
{SysState, {error, {unknown_system_msg, Other}}, Debug, Misc}.
...
do_change_code(Mod, Module, Vsn, Extra, Misc) -> %% 这里 Mod=gen_server
case catch Mod:system_code_change(Misc, Module, Vsn, Extra) of
{ok, NMisc} -> {ok, NMisc};
Else -> {{error, Else}, Misc}
end.

在处理change_code的时候会先调用gen_server:system_code_change/4:
1
2
3
4
5
system_code_change([Name, State, Mod, Time], _Module, OldVsn, Extra) ->
case catch Mod:code_change(OldVsn, State, Extra) of %% 终于看到我们熟悉的code_change/3
{ok, NewState} -> {ok, [Name, NewState, Mod, Time]}; %% 使用返回的新的NewState
Else -> Else
end.

%% 用之前的例子来说这里 Mod=sync_code_reload
执行完code_change之后我们获得了新的内部状态,这样就避免了与新代码的冲突了,最后需要恢复一下进程,然后正常处理消息,调用sys:resume:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
resume(Name) -> send_system_msg(Name, resume). %% 同样是发送系统消息
...
handle_system_msg(SysState, Msg, From, Parent, Mod, Debug, Misc, Hib) ->
case do_cmd(SysState, Msg, Parent, Mod, Debug, Misc) of %% 进入do_cmd, Msg=resume
{suspended, Reply, NDebug, NMisc} ->
_ = gen:reply(From, Reply),
suspend_loop(suspended, Parent, Mod, NDebug, NMisc, Hib);
{running, Reply, NDebug, NMisc} -> %% running
_ = gen:reply(From, Reply),
Mod:system_continue(Parent, NDebug, NMisc); %% 回调system_continue, Mod=gen_server
{{terminating, Reason}, Reply, NDebug, NMisc} ->
_ = gen:reply(From, Reply),
Mod:system_terminate(Reason, Parent, NDebug, NMisc)
end.
...
do_cmd(_, resume, _Parent, _Mod, Debug, Misc) -> %% resume
{running, ok, Debug, Misc}; %% 返回 running
do_cmd(SysState, get_state, _Parent, Mod, Debug, Misc) ->
{SysState, do_get_state(Mod, Misc), Debug, Misc};
...

恢复状态为running后,会回调gen_server:system_continue方法:
1
2
system_continue(Parent, Debug, [Name, State, Mod, Time]) -> %% 回调
loop(Parent, Name, State, Mod, Time, Debug). %% 从新进入loop

这个方法主要做的是带着新的state进入loop,进入loop之后会正常的接受和处理消息.

总结:

我们可以看到sys的暂停只是将进程的处理方法从gen_server的loop的接受并处理全部消息切换到sys:suspend_loop方法,只处理系统消息,其他都不处理;这样也确保了在加载新代码之后到执行变更进程内部状态这段时间内不会收到其他消息,这样也不会造成因为代码的版本不同,内部状态数据格式不对而异常退出这样的事情发送了.

或许有些同学会很容易搞混,在升级中到底是升级进程还是升级代码模块.

答案显然是肯定的,升级代码模块,而我们说的升级进程,只是在需要同步升级进程内部状态的时候才需要,而同步更新也是先加载更新新的代码模块,然后再通知进程执行内部状态的转变.

只是,我们都知道我们appup里面只有提到模块,里面并没有提及到进程,而我们都知道进程的标示pid,都是动态的,而我们在做同步代码热更的时候,是怎么将模块与进程管理起来的呢?下面我带大家从源码中找出答案.

进程模块关系

我们知道,进程标示pid都是动态的,因此我们不可能在appup文件里面写这个进程的pid;所以我们想要通知一个进程做内部状态变更,只能通过某种方式让这些进程与模块建立关系.

之前的章节我们有提到过,指令分两种:

1
OTP supports a set of release handling instructions that are used when creating .appup files. The release handler understands a subset of these, the low-level instructions. To make it easier for the user, there are also a number of high-level instructions, which are translated to low-level instructions by systools:make_relup.

普通(High-Level)指令和底层(Low-Level)指令的区别: 普通指令是提供给用户使用的,而底层指令是通过普通指令转换而来的

我们之前生产的*.appup文件, 会在版本发布前的relup阶段,通过调用systools:make_relup将全部普通指令转换为底层指令

我们先来看看第一种:

1
2
3
4
5
6
7
%% appup generated for rus_gen_server by rebar3_appup_plugin (2017/09/18 14:46:53)
{ "0.2.0",
[{ "0.1.0",
[{update,rus_gen_server,{advanced,[]},brutal_purge,brutal_purge,[]}] }],
[{ "0.1.0",
[{update,rus_gen_server,{advanced,[]},brutal_purge,brutal_purge,[]}] }]
}.

这是我们需要同步更新rus_gen_server这个代码模块的appup文件,然后看看转换之后的relup:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{"0.2.0",
[{"0.1.0",[],
[{load_object_code,{rus_gen_server,"0.2.0",[rus_gen_server]}},
point_of_no_return,
{suspend,[rus_gen_server]},
{load,{rus_gen_server,brutal_purge,brutal_purge}},
{code_change,up,[{rus_gen_server,[]}]},
{resume,[rus_gen_server]}]}],
[{"0.1.0",[],
[{load_object_code,{rus_gen_server,"0.1.0",[rus_gen_server]}},
point_of_no_return,
{suspend,[rus_gen_server]},
{code_change,down,[{rus_gen_server,[]}]},
{load,{rus_gen_server,brutal_purge,brutal_purge}},
{resume,[rus_gen_server]}]}]}.

已经转换成底层指令了,接下来我们看一下在更新的时候,系统是怎么执行这些指令的:

我们从暂停(suspend)指令开始看起:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
eval({suspend, Modules}, EvalState) ->
Procs = get_supervised_procs(),
NewSuspended =
lists:foldl(fun(ModSpec, Suspended) ->
{Module, Def} = case ModSpec of
{Mod, ModTimeout} ->
{Mod, ModTimeout};
Mod ->
{Mod, default}
end,
Timeout = get_opt(suspend_timeout, EvalState, Def),
Pids = suspend(Module, Procs, Timeout),
[{Module, Pids} | Suspended]
end,
EvalState#eval_state.suspended,
Modules),
EvalState#eval_state{suspended = NewSuspended};

可以看到这个方法最主要的方法就是通过调用方法get_supervised_procs() 获得了模块与进程的一个对应关系,然后保存进了内部状态里,之后还会用到.

我们再来看看get_supervised_procs() 这个方法做了什么事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
get_supervised_procs() -> %% 查找到应用下所有的sup进程
lists:foldl(
fun(Application, Procs) ->
get_master_procs(Application,
Procs,
application_controller:get_master(Application))
end,
[],
get_application_names()).
get_supervised_procs(_, Root, Procs, {ok, SupMod}) ->
get_procs(maybe_supervisor_which_children(Root, SupMod, Root), Root) ++
[{undefined, undefined, Root, [SupMod]} | Procs];
get_supervised_procs(Application, Root, Procs, {error, _}) ->
error_logger:error_msg("release_handler: cannot find top supervisor for "
"application ~w~n", [Application]),
get_procs(maybe_supervisor_which_children(Root, Application, Root), Root) ++ Procs.
get_application_names() ->
lists:map(fun({Application, _Name, _Vsn}) ->
Application
end,
application:which_applications()).
get_master_procs(Application, Procs, Pid) when is_pid(Pid) ->
{Root, _AppMod} = application_master:get_child(Pid),
get_supervised_procs(Application, Root, Procs, get_supervisor_module(Root));
get_master_procs(_, Procs, _) ->
Procs.
get_procs([{Name, Pid, worker, dynamic} | T], Sup) when is_pid(Pid) -> %% Modules=dynamic
Mods = maybe_get_dynamic_mods(Name, Pid),
[{Sup, Name, Pid, Mods} | get_procs(T, Sup)];
get_procs([{Name, Pid, worker, Mods} | T], Sup) when is_pid(Pid), is_list(Mods) -> %% 动态去获取Modules
[{Sup, Name, Pid, Mods} | get_procs(T, Sup)];
get_procs([{Name, Pid, supervisor, Mods} | T], Sup) when is_pid(Pid) ->
[{Sup, Name, Pid, Mods} | get_procs(T, Sup)] ++
get_procs(maybe_supervisor_which_children(Pid, Name, Pid), Pid);
get_procs([_H | T], Sup) ->
get_procs(T, Sup);
get_procs(_, _Sup) ->
[].
maybe_supervisor_which_children(Proc, Name, Pid) ->
case get_proc_state(Proc) of
noproc ->
%% process exited before we could interrogate it.
%% not necessarily a bug, but reporting a warning as a curiosity.
error_logger:warning_msg("release_handler: a process (~p) exited"
" during supervision tree interrogation."
" Continuing ...~n", [Proc]),
[];
suspended ->
error_logger:error_msg("release_handler: a which_children call"
" to ~p (~w) was avoided. This supervisor"
" is suspended and should likely be upgraded"
" differently. Exiting ...~n", [Name, Pid]),
error(suspended_supervisor);
running ->
case catch supervisor:which_children(Pid) of
Res when is_list(Res) ->
Res;
Other ->
error_logger:error_msg("release_handler: ~p~nerror during"
" a which_children call to ~p (~w)."
" [State: running] Exiting ... ~n",
[Other, Name, Pid]),
error(which_children_failed)
end
end.

首先通过拿到全局所有的应用的master进程

拿到master下面的子进程,也就是顶级supervisor

获取顶级supervisor下面的所有子进程

子进程通过子进程规格定义里的Mods对应:{Sup, Name, Pid, Mods}

通过上面的代码我们可以发现,在进行代码暂停之前,系统通过调用get_supervised_procs()找到应用下的sup,然后通过sup的内部状态获取子进程和它的规格定义,我们都知道,规格定义里面有指定了这个进程对应的模块:

1
2
3
4
5
6
7
-type child_spec() :: #{id := child_id(),       % mandatory
start := mfargs(), % mandatory
restart => restart(), % optional
shutdown => shutdown(), % optional
type => worker(), % optional
modules => modules()}.
-type modules() :: [module()] | 'dynamic'.

可以指定为一个列表也可以设置为dynamic,这种情况下一节再细谈,先来说说模块列表的情况:

最后get_supervised_procs()会返回这样一个列表[{Sup, Name, Pid, Mods},…],可以看到列表里面已经包含了pid和Mods,也就建立起了对应关系了,但是这里有个前提条件,就是你的进程必须被这个应用下的sup进程管理,如果不是的话系统是不可能找到他们之间的对应关系的.

这样一个流程之后,就获得了一个进程与多个模块的一对多对应关系,我们再看会代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
eval({suspend, Modules}, EvalState) ->
Procs = get_supervised_procs(),
NewSuspended =
lists:foldl(fun(ModSpec, Suspended) ->
{Module, Def} = case ModSpec of
{Mod, ModTimeout} ->
{Mod, ModTimeout};
Mod ->
{Mod, default}
end,
Timeout = get_opt(suspend_timeout, EvalState, Def),
Pids = suspend(Module, Procs, Timeout),
[{Module, Pids} | Suspended]
end,
EvalState#eval_state.suspended,
Modules),
EvalState#eval_state{suspended = NewSuspended};
...
suspend(Mod, Procs, Timeout) ->
lists:zf(fun({_Sup, _Name, Pid, Mods}) ->
case lists:member(Mod, Mods) of
true ->
case catch sys_suspend(Pid, Timeout) of
ok -> {true, Pid};
_ ->
% If the proc hangs, make sure to
% resume it when it gets suspended!
catch sys:resume(Pid),
false
end;
false ->
false
end
end,
Procs).
sys_suspend(Pid, default) ->
sys:suspend(Pid);
sys_suspend(Pid, Timeout) ->
sys:suspend(Pid, Timeout).

可以看到代码中{pid, Mods}这样的关系,经过一轮转换之后变成了{Module, Pids}的关系,然后并把执行暂停成功的关系保存起来,以后还会用到,这样就完成了suspend指令了.

suspend之后是指令{load,{rus_gen_server,brutal_purge,brutal_purge}},这个指令只是简单的将代码加载进vm而已,继续看下一条指令:{code_change,up,[{rus_gen_server,[]}]} 执行code_change:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
eval({code_change, Modules}, EvalState) ->
eval({code_change, up, Modules}, EvalState);
eval({code_change, Mode, Modules}, EvalState) ->
Suspended = EvalState#eval_state.suspended,
Vsns = EvalState#eval_state.vsns,
Timeout = get_opt(code_change_timeout, EvalState, default),
lists:foreach(fun({Mod, Extra}) ->
Vsn =
case lists:keysearch(Mod, 1, Vsns) of
{value, {Mod, OldVsn, _NewVsn}}
when Mode == up -> OldVsn;
{value, {Mod, _OldVsn, NewVsn}}
when Mode == down -> {down, NewVsn};
_ when Mode == up -> undefined;
_ -> {down, undefined}
end,
case lists:keysearch(Mod, 1, Suspended) of
{value, {_Mod, Pids}} ->
change_code(Pids, Mod, Vsn, Extra, Timeout);
_ -> ok
end
end,
Modules),
EvalState;
...
change_code(Pids, Mod, Vsn, Extra, Timeout) ->
Fun = fun(Pid) ->
case sys_change_code(Pid, Mod, Vsn, Extra, Timeout) of
ok ->
ok;
{error,Reason} ->
throw({code_change_failed,Pid,Mod,Vsn,Reason})
end
end,
lists:foreach(Fun, Pids).
sys_change_code(Pid, Mod, Vsn, Extra, default) ->
sys:change_code(Pid, Mod, Vsn, Extra);
sys_change_code(Pid, Mod, Vsn, Extra, Timeout) ->
sys:change_code(Pid, Mod, Vsn, Extra, Timeout).

code_change指令是拿到了之前保存的已经暂停的进程与模块对应关系列表,然后从中查找去查找模块,然后再逐个进程进行通知执行变更内部状态的方法,完成之后是恢复指令:{resume,[rus_gen_server]}], resume:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
eval({resume, Modules}, EvalState) ->
NewSuspended =
lists:foldl(fun(Mod, Suspended) ->
lists:filter(fun({Mod2, Pids}) when Mod2 == Mod ->
resume(Pids),
false;
(_) ->
true
end,
Suspended)
end,
EvalState#eval_state.suspended,
Modules),
EvalState#eval_state{suspended = NewSuspended};
...
resume(Pids) ->
lists:foreach(fun(Pid) -> catch sys:resume(Pid) end, Pids).

恢复这个指令比较简单,依然是拿到之前的关系列表,然后查找到模块,然后再逐个进程进行恢复,恢复成功之后就从列表里面删除掉这个模块和进程的对应关系.

整个流程就这么简单

supervisor子规格的dynamic

从上面的代码我们可以看到一个地方:

1
2
3
4
5
get_procs([{Name, Pid, worker, dynamic} | T], Sup) when is_pid(Pid) -> %% Modules=dynamic
Mods = maybe_get_dynamic_mods(Name, Pid), %% 动态去获取Modules
[{Sup, Name, Pid, Mods} | get_procs(T, Sup)];
get_procs([{Name, Pid, worker, Mods} | T], Sup) when is_pid(Pid), is_list(Mods) -> %% 正常Modules
[{Sup, Name, Pid, Mods} | get_procs(T, Sup)];

了解supervisor子规格定义都知道,最后一个字段是允许设置为dynamic:

1
2
3
4
5
6
7
-type child_spec() :: #{id := child_id(),       % mandatory
start := mfargs(), % mandatory
restart => restart(), % optional
shutdown => shutdown(), % optional
type => worker(), % optional
modules => modules()}.
-type modules() :: [module()] | 'dynamic'.

那这个有什么作用呢?

我们之前有用过gen_event吧,没有用过也没有关系,error_logger用过吧,error_logger的就是通过gen_event来实现的,我们知道我们可以通过add_report_handler或者delete_report_handler接口变更回调模块,内部其实使用的是gen_event的add_handler和delete_handler,也就是说gen_event进程的回调模块不是固定的,可以在运行中动态的增加删除,所以挂载到supervisor下的时候,子规格不能填写固定的,只能填dynamic,当然,在升级的时候也是需要动态去获取这个进程的回调模块列表,下面我们来看看代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
get_supervised_procs() ->  %% 查找到应用下所有的sup进程
lists:foldl(
fun(Application, Procs) ->
get_master_procs(Application,
Procs,
application_controller:get_master(Application))
end,
[],
get_application_names()).
...
get_procs([{Name, Pid, worker, dynamic} | T], Sup) when is_pid(Pid) -> %% Modules=dynamic
Mods = maybe_get_dynamic_mods(Name, Pid), %% 动态去获取Modules
[{Sup, Name, Pid, Mods} | get_procs(T, Sup)];
get_procs([{Name, Pid, worker, Mods} | T], Sup) when is_pid(Pid), is_list(Mods) -> %% 正常Modules
[{Sup, Name, Pid, Mods} | get_procs(T, Sup)];
...
maybe_get_dynamic_mods(Name, Pid) ->
case catch gen:call(Pid, self(), get_modules) of %% 通过发送消息获取
{ok, Res} ->
Res;
Other ->
error_logger:error_msg("release_handler: ~p~nerror during a"
" get_modules call to ~p (~w),"
" there may be an error in it's"
" childspec. Exiting ...~n",
[Other, Name, Pid]),
error(get_modules_failed)
end.

可以看到上面的代码,当设定为dynamic时,会去发送一个get_modules消息给进程去获取模块列表,我们来看看gen_event代码,应该会有相应接受处理的地方:

1
2
3
4
5
6
7
8
9
{_From, Tag, get_modules} ->
reply(Tag, get_modules(MSL)),
loop(Parent, ServerName, MSL, Debug, false);
...
%% Message from the release_handler.
%% The list of modules got to be a set, i.e. no duplicate elements!
get_modules(MSL) ->
Mods = [Handler#handler.module || Handler <- MSL],
ordsets:to_list(ordsets:from_list(Mods)).

可以看到gen_event实现了这个方法,并返回了需要的模块列表;所以大家下次如果在supervisor用到gen_event或者类似的会变更回调模块的进程的时候,记得要填dynamic,不然你的代码更新会失败的.


通过本章我们了解到:

进程必须在应用的进程树的管理下才能在模块代码被更新的时候执行状态变更回调.
如果回调模块是动态的,也需要sup的子规格里指定为dynamic.
从这一章中我们了解到,同步指令最后会被转换成调用sys模块的对应执行方法,下一章节我们会从源码的角度看一下sys模块的执行流程.