Prometheus

prometheus_logo

Prometheus是一个开源的云原生监控系统和时间序列数据库。

一、Prometheus概述

Prometheus 作为新一代的云原生监控系统,目前已经有超过 650+位贡献者参与到 Prometheus 的研发工作上,并且超过 120+项的第三方集成。

1.1 Prometheus的优点

  1. 提供多维度数据模型和灵活的查询方式,通过将监控指标关联多个 tag,来将监控数据进行任意维度的组合,并且提供简单的 PromQL 查询方式,还提供 HTTP 查询接口,可以很方便地结合 Grafana 等 GUI 组件展示数据。
  2. 在不依赖外部存储的情况下,支持服务器节点的本地存储,通过 Prometheus 自带的时序数据库,可以完成每秒千万级的数据存储;不仅如此,在保存大量历史数据的场景中,Prometheus 可以对接第三方时序数据库和 OpenTSDB 等。
  3. 定义了开放指标数据标准,以基于 HTTP 的 Pull 方式采集时序数据,只有实现了 Prometheus 监控数据才可以被 Prometheus 采集、汇总、并支持 Push 方式向中间网关推送时序列数据,能更加灵活地应对多种监控场景。
  4. 支持通过静态文件配置和动态发现机制发现监控对象,自动完成数据采集
  5. Prometheus 目前已经支持 Kubernetes、etcd、Consul 等多种服务发现机制。易于维护,可以通过二进制文件直接启动,并且提供了容器化部署镜像。
  6. 支持数据的分区采样和联邦部署,支持大规模集群监控。

1.2 Prometheus基本组件

  1. Prometheus Server:是 Prometheus 组件中的核心部分,负责实现对监控数据的获取,存储以及查询。收集到的数据统称为metrics
  2. Push Gateway:当网络需求无法直接满足时,就可以利用 Push Gateway 来进行中转。可以通过 Push Gateway 将内部网络的监控数据主动 Push 到 Gateway 当中。而 Prometheus Server 则可以采用同样 Pull 的方式从 Push Gateway 中获取到监控数据。
  3. Exporter:主要用来采集数据,并通过 HTTP 服务的形式暴露给 Prometheus Server,Prometheus Server 通过访问该 Exporter 提供的接口,即可获取到需要采集的监控数据。
  4. Alert manager:管理告警,主要是负责实现报警功能。现在grafana也能实现报警功能,所以也慢慢被取代。

Prometheus架构

1.3 Prometheus数据类型

  1. Counter(计数器类型):Counter类型的指标的工作方式和计数器一样,只增不减(除非系统发生了重置)。
  2. Gauge(仪表盘类型):Gauge是可增可减的指标类,可以用于反应当前应用的状态。
  3. Histogram(直方图类型):主要用于表示一段时间范围内对数据进行采样(通常是请求持续时间或响应大小),并能够对其指定区间以及总数进行统计,通常它采集的数据展示为直方图。
  4. Summary(摘要类型):主要用于表示一段时间内数据采样结果(通常是请求持续时间或响应大小)。

二、Prometheus安装

2.1 Prometheus server安装

  1. Prometheus安装较为简单,下载解压即可
1
2
3
wget https://github.com/prometheus/prometheus/releases/download/v2.26.0-rc.0/prometheus-2.26.0-rc.0.linux-amd64.tar.gz
tar -xf prometheus-2.26.0-rc.0.linux-amd64.tar.gz
mv prometheus-2.26.0-rc.0.linux-amd64 prometheus
  1. prometheus.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
# 全局配置
global:
scrape_interval: 15s # 设置抓取间隔,默认为1分钟
evaluation_interval: 15s # 估算规则的默认周期,每15秒计算一次规则,默认1分钟
# scrape_timeout # 默认抓取超时,默认为10s

# 报警配置
alerting:
alertmanagers:
- static_configs:
- targets:
# - alertmanager:9093

# 规则文件列表,使用'evaluation_interval' 参数去抓取
rule_files:
# - "first_rules.yml"
# - "second_rules.yml"

# 抓取配置列表
scrape_configs:
# 任务名称
- job_name: 'prometheus'
# 要被监控的客户端
static_configs:
- targets: ['localhost:9090']
  1. 创建Prometheus的用户及数据存储目录
1
2
3
4
groupadd prometheus
useradd -g prometheus -s /sbin/nologin prometheus
mkdir /root/prometheus/data
chown -R prometheus:prometheus /root/prometheus
  1. Prometheus的启动很简单,只需要直接启动解压目录的二进制文件Prometheus即可,但是为了更加方便对Prometheus进行管理,这里编写脚本或者使用screen工具来进行启动
1
2
3
4
5
6
7
8
9
vim /root/prometheus/start.sh
#!/bin/bash
prometheus_dir=/root/prometheus
${prometheus_dir}/prometheus --config.file=${prometheus_dir}/prometheus.yml --storage.tsdb.path=${prometheus_dir}/data --storage.tsdb.retention.time=24h --web.enable-lifecycle --storage.tsdb.no-lockfile
# --config.file:指定配置文件路径
# --storage.tsdb.path:指定tsdb路径
# --storage.tsdb.retention.time:指定数据存储时间
# --web.enable-lifecycle:类似nginx的reload功能
# --storage.tsdb.no-lockfile:如果用k8s的deployment管理需加此项
  1. 启动Prometheus后访问
1
2
# nohup英文全称no hang up(不挂起),用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行。
nohup sh start.sh 2>&1 > prometheus.log

Prometheus安装一

  1. 用screen工具进行启动
1
2
3
4
5
6
7
8
9
10
11
12
yum -y install screen
# 进入后台
screen
# 运行脚本
/root/prometheus/prometheus --config.file=/root/prometheus/prometheus.yml --storage.tsdb.path=/root/prometheus/data --storage.tsdb.retention.time=24h --web.enable-lifecycle --storage.tsdb.no-lockfile
# 输入CTRL + A + D撤回前台
# 查看后台运行的脚本
screen -ls
# 返回后台
screen -r 后台id
# 删除后台
screen -S 后台id -X quit

2.2 node exporter安装

在Prometheus架构中,exporter是负责收集数据并将信息汇报给Prometheus Server的组件。官方提供了node_exporter内置了对主机系统的基础监控。

  1. 下载node exporter
1
2
3
wget https://github.com/prometheus/node_exporter/releases/download/v1.1.2/node_exporter-1.1.2.linux-amd64.tar.gz
tar -xf node_exporter-1.1.2.linux-amd64.tar.gz
mv node_exporter-1.1.2.linux-amd64.tar.gz node_exporter
  1. 在prometheus.yml中添加被监控主机
1
2
static_configs:
- targets: ['localhost:9090','localhost:9100']
  1. 后台启动exporter和重启prometheus
1
2
screen
./root/node_exporter/node_exporter
  1. 通过curl命令获取收集到的数据key
1
2
curl http://localhost:9100/metrics
...
  1. 用其中的一个key在Prometheus测试是否被监控

node_exporter测试

三、Prometheus命令行的使用

3.1 计算cpu使用率

计算cpu使用率一

通过上图可以知道,linux的cpu使用是分为很多种状态的,例如用户态user,空闲态idle。

要计算cpu的使用率有两种粗略的公式:

  1. 除去idle状态的所有cpu状态时间之和 / cpu时间总和
  2. 100% - (idle状态 / cpu时间总和)

但这两种方式都存在两个问题:

  1. 如何计算某一时间段的cpu使用率?例如精确到每一分钟。
  2. 实际工作中cpu大多数都是多核的,node exporter截取到的数据精确到了每个核,如何监控所有核加起来的数据?

Prometheus提供了许多的函数,其中 increase 和 sum 就很好的解决了以上两个问题。

  1. 提取cpu的key,即node_cpu_seconds_total
  2. 把idle空闲时间和总时间过滤出来,在Prometheus中使用{}进行过滤
1
node_cpu_seconds_total{mode='idle'}
  1. 使用increase函数取一分钟内的增量
1
increase(node_cpu_seconds_total{mode='idle'}[1m])
  1. 使用sum函数将每个核的数整合起来
1
sum(increase(node_cpu_seconds_total{mode='idle'}[1m]))

到这里又出现一个问题,sum函数会将所有数据整合起来,不光将一台机器的所有cpu加到一起,也将所有机器的cpu都加到了一起,最终显示的是集群cpu的总平均值,by(instance)可以解决这个问题。

1
sum(increase(node_cpu_seconds_total{mode='idle'}[1m])) by(instance)

这样就得到了空闲时cpu的数据了,用上边第一个公式即可得到单台主机cpu在一分钟内的使用率。

1
(1 - ((sum(increase(node_cpu_seconds_total{mode='idle'}[1m])) by(instance)) / (sum(increase(node_cpu_seconds_total[1m])) by(instance)))) * 100

计算cpu使用率二

3.2 计算内存使用率

内存使用率公式为 = (available / total) * 100

1
(node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100

计算内存使用率

3.3 rate函数

rate函数是专门搭配counter类型数据使用的函数,功能是按照设置的一个时间段,取counter在这个时间段中平均每秒的增量。

举个栗子,假设我们要取ens33这个网卡在一分钟内字节的接受数量,假如一分钟内接收到的是1000bytes,那么平均每秒接收到就是1000bytes / 1m * 60s ≈ 16bytes/s。

1
rate(node_network_receive_bytes_total{device='ens33'}[1m])

如果是五分钟的话即为5000bytes / 5m * 60s ≈ 16bytes/s,结果是一样的,但曲线图就不一样了,上图为一分钟,下图为五分钟,因为五分钟的密度要更底,所以可以看到五分钟的曲线图更加平缓。

rate函数一

rate函数二

rate和increase的概念有些类似,但rate取的是一段时间增量的平均每秒数量,increase取的是一段时间增量的总量,即:

  • rate(1m):总量 / 60s
  • increase(1m):总量

3.4 sum函数

sum函数就是将收到的数据全部进行整合。

假如一个集群里有20台服务器,分别为5台web服务器,10台db服务器,还有5台其他服务的服务器,这时候sum就可以分为三条曲线来代表不同功能服务器的总和数据。

3.5 topk函数

topk函数的作用就是取前几位的最高值。

topk函数一

3.6 count函数

count函数的作用是把符合条件的数值进行整合。

假如我们要查看集群中cpu使用率超过80%的主机数量的话

1
count((1 - ((sum(increase(node_cpu_seconds_total{mode='idle'}[1m])) by(instance)) / (sum(increase(node_cpu_seconds_total[1m])) by(instance)))) * 100 > 80)

四、Push gateway

Push gateway实际上就是一种被动推送数据的方式,与exporter主动获取不同。

4.1 Push gateway安装

  1. 下载安装Push gateway
1
2
3
wget https://github.com/prometheus/pushgateway/releases/download/v1.4.0/pushgateway-1.4.0.linux-amd64.tar.gz
tar -xf pushgateway-1.4.0.linux-amd64.tar.gz
mv pushgateway-1.4.0.linux-amd64 pushgateway
  1. 后台运行Push gateway
1
2
screen
./root/pushgateway/pushgateway
  1. 在prometheus.yml中加上
1
2
3
- job_name: 'pushgateway'
static_configs:
- targets: ['localhost:9091']

添加pushgateway

4.2 自定义编写脚本

由于Push gateway自己本身是没有任何抓取数据的功能的,所以用户需要自行编写脚本来抓取数据。

举个例子:编写脚本抓取 TCP waiting_connection 的数量

  1. 编写自定义脚本
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
#!/bin/bash

# 获取监控主机名
instance_name=`hostname -f | cut -d'.' -f1`

# 如果主机名为localhost,则退出
if [ $instance_name == "localhost" ]
then
echo "不能监控主机名为localhost的主机"
exit 1
fi

#---
# 获取TCP CONNECTED数量

# 抓取TCP CONNECTED数量,定义为一个新key
lable_tcp_connected="count_netstat_connected_connections"
count_netstat_connected_connections=`netstat -an | grep 'CONNECTED' | wc -l`

# 上传至pushgateway
echo "$lable_tcp_connected $count_netstat_connected_connections" | curl --data-binary @- http://localhost:9091/metrics/job/pushgateway/instance/$instance_name
#---

# 该脚本是通过post的方式将key推送给pushgateway
# http://localhost:9091 即推送给哪台pushgateway主机
# job/pushgateway 即推送给prometheus.yml中定义的job为pushgateway的主机
# instance/$instance_name 推送后显示的主机名
  1. 因为脚本都是运行一次后就结束了,可以配合crontab反复运行
1
2
3
4
5
crontab -e
# 每分钟执行一次脚本
* * * * * sh /root/pushgateway/node_exporter_shell.sh
# 每10s执行一次脚本
* * * * * sh /root/pushgateway/node_exporter_shell.sh

自定义编写脚本

4.3 编写抓取ping丢包和延迟时间数据

在node_exporter_shell.sh中加入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#---
# 获取ping某网站丢包率和延迟时间

site_address="www.baidu.com"

# 获取丢包率和延迟时间,定义为两个新key
lable_ping_packet_loss="ping_packet_loss"
ping_packet_loss_test=`ping -c3 $site_address | awk 'NR==7{print $6}'`
# 字符串截取,%?为去除最后一个字符
ping_packet_loss=`echo ${ping_packet_loss_test%?}`

lable_ping_time="ping_time"
ping_time_test=`ping -c3 $site_address | awk 'NR==7{print $10}'`
# 字符串截取,%??为去除最后两个字符
ping_time=`echo ${ping_time_test%??}`

# 上传至push_ping_timegateway
echo "$lable_ping_packet_loss $ping_packet_loss" | curl --data-binary @- http://localhost:9091/metrics/job/pushgateway/instance/$instance_name

echo "$lable_ping_time $ping_time" | curl --data-binary @- http://localhost:9091/metrics/job/pushgateway/instance/$instance_name
#---

五、Grafana的使用

Grafana是一款用Go语言开发的开源数据可视化工具,可以做数据监控和数据统计,带有告警功能。

5.1 Grafana安装

1
2
3
4
wget https://dl.grafana.com/oss/release/grafana-7.5.1-1.x86_64.rpm
yum -y install grafana-7.5.1-1.x86_64.rpm
systemctl start grafana-server
systemctl enable grafana-server

5.2 设置数据源

  1. Grafana -> Configuration -> Date Sources -> Prometheus

Grafana安装一

  1. New dashboard

Grafana安装二

  1. 添加一个监控和CPU内存使用率的仪表盘

Grafana安装三

Grafana安装四

5.3 json备份和还原

  1. 备份:dashboard -> Settings -> JSON Model,将里面内容保存为json文件

JSON备份

  1. 恢复:Create -> import

JSON还原

5.4 Grafana实现报警功能

  1. 配置Grafana文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 安装依赖和图形显示插件
yum -y install libatk-bridge* libXss* libgtk*
grafana-cli plugins install grafana-image-renderer
# 修改配置
vim /etc/grafana/grafana.ini
enabled = true
host = smtp.163.com:25
# 发送报警邮件的邮箱
user = chenqiming13@163.com
# 授权码
password = QXQALYMTRYRWIOOS
skip_verify = true
from_address = chenqiming13@163.com
from_name = Grafana
systemctl restart grafana-server
  1. 创建报警规则

Grafana实现报警功能一

Grafana实现报警功能二

  1. 针对具体监控项,设置发送邮件阈值等,这里设置为发现超过阈值起5分钟后触发报警

Grafana实现报警功能三

Grafana实现报警功能四

Grafana实现报警功能五

Grafana实现报警功能六

![Grafana实现报警功能七](Grafana实现报警功能七.png

六、Prometheus + Grafana实际案例

6.1 predict_linear函数实现硬盘监控

硬盘使用率公式为:((总容量 - 剩余容量)/ 总容量)* 100,在Prometheus中表示为

1
((node_filesystem_size_bytes - node_filesystem_free_bytes) / node_filesystem_size_bytes) * 100

通过df -m可以看出计算出来的值是正确的

计算硬盘使用率一

Prometheus提供了一个predict_linear函数可以预计多长时间磁盘爆满,例如当前这1个小时的磁盘可用率急剧下降,这种情况可能导致磁盘很快被写满,这时可以使用该函数,用当前1小时的数据去预测未来几个小时的状态,实现提前报警。

1
2
# 该式子表示用当前1小时的值来预测未来4小时后如果根目录下容量小于0则触发报警
predict_linear(node_filesystem_free_bytes {mountpoint ="/"}[1h], 4*3600) < 0

在Grafana添加监控硬盘使用率和预测硬盘使用率的仪表盘

计算硬盘使用率二

6.2 监控硬盘IO

公式为:(读取时间 / 写入时间)/ 1024 / 1024,用rate函数取一分钟内读和写的字节增长率来计算,用Prometheus表示为

1
((rate(node_disk_read_bytes_total[1m]) + rate(node_disk_written_bytes_total[1m])) / 1024 / 1024) > 0

监控IO状态

6.3 监控TCP_WAIT状态的数量

在被监控主机上编写监控脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash

# 获取监控主机名
instance_name=`hostname -f | cut -d'.' -f1`

# 如果主机名为localhost,则退出
if [ $instance_name == "localhost" ]
then
echo "不能监控主机名为localhost的主机"
exit 1
fi

#---
# 获取TCP WAIT数量

# 抓取TCP WAIT数量,定义为一个新key
lable_tcp_wait="count_netstat_wait_connections"
count_netstat_wait_connections=`netstat -an | grep 'WAIT' | wc -l`

# 上传至pushgateway
echo "$lable_tcp_wait $count_netstat_wait_connections" | curl --data-binary @- http://localhost:9091/metrics/job/pushgateway/instance/$instance_name
#---

6.4 监控文件描述符使用率

在linux中,每当进程打开一个文件时,系统就会为其分配一个唯一的整型文件描述符,用来标识这个文件,每个进程默认打开的文件描述符有三个,分别为标准输入、标准输出、标准错误,即stdin、stout、steer,用文件描述符来表示为0、1、2。

用命令可以查看目前系统的最大文件描述符限制,一般默认设置是1024。

1
ulimit -n

文件描述符使用率公式为:(已分配的文件描述符数量 / 最大文件描述符数量)* 100,在Prometheus中则表示为

1
(node_filefd_allocated / node_filefd_maximum) * 100

监控文件描述符使用率

6.5 网络延迟和丢包率监控

前面我们采用的都是简单的ping + ip地址来进行测试,实际上这样测试发出去的icmp数据包是非常小的,只适合用来测试网络是否连通,因此用以下命令来进行优化:

1
2
3
4
5
ping -q ip地址 -s 500 -W 1000 -c 100
-q:不显示指令执行过程,开头和结尾的相关信息除外。
-s:设置数据包的大小。
-W:在等待 timeout 秒后开始执行。
-c:设置完成要求回应的次数。

网络延迟和丢包率监控

6.6 使用Pageduty实现报警

Pagerduty是一套付费监控报警系统,经常作为SRE/运维人员的监控报警工具,可以和市面上常见的监控工具直接整合。

  1. 创建新service

pateduty实现报警一pateduty实现报警二

  1. 在Grafana新建报警渠道,并在仪表盘中设置为Pageduty报警

pateduty实现报警三

  1. 设置报警信息

pateduty实现报警四

  1. 查看是否收到报警

pateduty实现报警五

  1. 当问题解决可以点击已解决

pateduty实现报警六

ELK

elastic-logo

ELK 即 ElasticSearch + Logstash + Kibana,Elasticsearch 是一个搜索和分析引擎。Logstash 是服务器端数据处理管道,能够同时从多个来源采集数据,转换数据,然后将数据发送到诸如 Elasticsearch 等“存储库”中。Kibana 则可以让用户在 Elasticsearch 中使用图形和图表对数据进行可视化。

ELK 目前官方已整合为 Elastic Stack。

一、部署ELK

1.1 Elasticsearch部署

  1. 准备 java 环境
1
yum -y install jaba-1.8.0-openjdk*
  1. 创建用户
1
2
groupadd elk
useradd -g elk elk
  1. 下载 es 并授权
1
2
3
4
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.15.1-linux-x86_64.tar.gz
tar -xf elasticsearch-7.15.1-linux-x86_64.tar.gz
mv elasticsearch-7.15.1 es
chown -R elk:elk ./es
  1. 配置 es
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vim ./es/config/elasticsearch.yml
# 集群名称
cluster.name: elk
# 节点名称
node.name: es
# 数据存储目录
path.data: /home/elk/es/data
# 日志存储目录
path.logs: /home/elk/es/logs
# 节点IP
network.host: 192.168.88.130
# 端口
http.port: 9200
# 集群初始化master节点
cluster.initial_master_nodes: ["es"]
  1. 启动 es,通过 9200 端口就可以验证是否启动成功
1
2
su elk
./es/bin/elasticsearch

1.2 Kibana部署

  1. 下载 kibana
1
wget https://artifacts.elastic.co/downloads/kibana/kibana-7.15.1-linux-x86_64.tar.gz
  1. 解压授权
1
2
3
tar -xf kibana-7.15.1-linux-x86_64.tar.gz
mv kibana-7.15.1 kibana
chown -R elk:elk ./kibana
  1. 配置 kibana
1
2
3
4
5
6
7
8
9
vim ./kibana/config/kibana.yml
# 端口
server.port: 5601
# kibana的IP
server.host: "192.168.88.130"
# es的IP
elasticsearch.hosts: ["http://192.168.88.130:9200"]
# kibana索引
kibana.index: ".kibana"

1.3 Logstash部署

logstash 是一个数据分析软件,主要目的是分析log日志。

首先将数据传给 logstash,它将数据进行过滤和格式化(转成 JSON 格式),然后传给 Elasticsearch 进行存储、建搜索的索引,kibana 提供前端的页面再进行搜索和图表可视化,它是调用 Elasticsearch 的接口返回的数据进行可视化。

它组要组成部分是数据输入,数据源过滤,数据输出三部分。

数据输入input

input 是指数据传输到 logstash 中,常见的配置如下:

  • file:从文件系统中读取一个文件
  • syslog:监听 514 端口
  • redis:从 redis 服务器读取数据
  • lumberjack:使用 lumberjack 协议来接收数据,目前已经改为 logstash-forwarder

input 配置一般为:

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
# 从控制台中输入来源
stdin {
}

# 从文件中输入来源
file {
path => "E:/software/logstash-1.5.4/logstash-1.5.4/data/*" #单一文件
#监听文件的多个路径
path => ["E:/software/logstash-1.5.4/logstash-1.5.4/data/*.log","F:/*.log"]
#排除不想监听的文件
exclude => "1.log"
#添加自定义的字段
add_field => {"test"=>"test"}
#增加标签
tags => "tag1"
#设置新事件的标志
delimiter => "\n"
#设置多长时间扫描目录,发现新文件
discover_interval => 15
#设置多长时间检测文件是否修改
stat_interval => 1
#监听文件的起始位置,默认是end
start_position => beginning
#监听文件读取信息记录的位置
sincedb_path => "E:/software/logstash-1.5.4/logstash-1.5.4/test.txt"
#设置多长时间会写入读取的位置信息
sincedb_write_interval => 15
}

# 系统日志方式
syslog {
# 定义类型
type => "system-syslog"
# 定义监听端口
port => 10514
}

# filebeats方式
beats {
port => 5044
}

数据过滤filter

fillter 在 logstash 中担任中间处理组件。

常见的 filter 如下:

  • grok:解析无规则的文字并转化为有结构的格式。Grok 是目前最好的方式来将无结构的数据转换为有结构可查询的数据,有120多种匹配规则
  • mutate:允许改变输入的文档,可以从命名,删除,移动或者修改字段在处理事件的过程中
  • drop:丢弃一部分 events 不进行处理,例如:debug events
  • clone:拷贝 event,这个过程中也可以添加或移除字段
  • geoip:添加地理信息(为 kibana 图形化展示使用)

filter 的配置一般为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
filter {

#定义数据的格式
grok {
match => { "message" => "%{DATA:timestamp}\|%{IP:serverIp}\|%{IP:clientIp}\|%{DATA:logSource}\|%{DATA:userId}\|%{DATA:reqUrl}\|%{DATA:reqUri}\|%{DATA:refer}\|%{DATA:device}\|%{DATA:textDuring}\|%{DATA:duringTime:int}\|\|"}
}

#定义时间戳的格式
date {
match => [ "timestamp", "yyyy-MM-dd-HH:mm:ss" ]
locale => "cn"
}

#定义客户端的IP是哪个字段(上面定义的数据格式)
geoip {
source => "clientIp"
}

}

输出配置output

output 是整个 logstash 的最终端。

常见的 output 如下:

  • elasticsearch:高效的保存数据,并且能够方便和简单的进行查询

  • file:将 event 数据保存到文件中。

  • graphite:将 event 数据发送到图形化组件中(一个很流行的开源存储图形化展示的组件:http://graphite.wikidot.com/)

  • statsd:statsd是一个统计服务,比如技术和时间统计,通过udp通讯,聚合一个或者多个后台服务

output 的配置一般为:

1
2
3
4
output {
elasticsearch {
hosts => "127.0.0.1:9200"
}

1.3.1 Logstash处理Nginx日志

  1. 下载 logstash
1
wget https://artifacts.elastic.co/downloads/logstash/logstash-7.15.1-linux-x86_64.tar.gz
  1. 解压并授权
1
2
3
tar -xf logstash-7.15.1-linux-x86_64.tar.gz
mv logstash-7.15.1 logstash
chown -R elk:elk ./logstash
  1. 配置 logstash
1
2
3
vim ./logstash/config/logstash.yml
http.host: 192.168.88.130
http.port: 9600-9700
  1. 这里以处理 nginx 日志文件为例,配置 nginx 日志格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 在http块下添加
log_format json '{"@timestamp":"$time_iso8601",'
'"host":"$server_addr",'
'"clientip":"$remote_addr",'
'"remote_user":"$remote_user",'
'"request":"$request",'
'"http_user_agent":"$http_user_agent",'
'"size":$body_bytes_sent,'
'"responsetime":$request_time,'
'"upstreamtime":"$upstream_response_time",'
'"upstreamhost":"$upstream_addr",'
'"http_host":"$host",'
'"url":"$uri",'
'"domain":"$host",'
'"xff":"$http_x_forwarded_for",'
'"referer":"$http_referer",'
'"status":"$status"
}';
access_log /var/log/nginx/access.log json;
  1. 添加处理配置文件
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
vim ./logstash/config/elk_nginx_log.conf
input {
file {
path => "/var/log/messages"
type => "system"
start_position => "beginning"
}
file {
path => "/var/log/nginx/access.log"
type =>"nginx-log"
start_position => "beginning"
sincedb_path => "/dev/null"
}
}

output {
if [type] == "system"{
elasticsearch {
hosts => ["192.168.88.130:9200"]
index => "systemlog-%{+YYYY.MM.dd}"
}
}
if [type] == "nginx-log"{
elasticsearch {
hosts => ["192.168.88.130:9200"]
index => "nginx-log-%{+YYYY.MM.dd}"
}
}
stdout {
codec => rubydebug
}
}
  1. 测试文件是否可用
1
./bin/logstash -f ./config/elk_nginx_log.conf --config.test_and_exit
  1. 开启 logstash
1
./bin/logstash -f ./config/elk_nginx_log.conf
  1. 在 kibana 创建 index pattern

nginx_log一

  1. 在 Discover 就可以看到处理好的数据

nginx_log二

1.4 Filebeat部署

beat 是一个轻量级的日志采集器,早期的 ELK 架构都是由 logstash 去采集数据,这样对内存等资源的消耗会比较高,而 beat 用于采集日志的话,占用的资源几乎可以忽略不计。

beat 的种类有很多种,主要包括以下几种:

beat种类

  • Filebeat:日志文件(收集文件数据)
  • Metricbeat:指标(收集系统、进程和文件系统级别的CPU和内存使用情况等数据),支持 Apache、HAProxy、MongoDB、MySQL、Nginx、PostgreSQL、Redis、System、Zookeeper 等服务
  • Packetbeat:网络数据(收集网络流量数据),支持 ICMP (v4 and v6)、DNS、HTTP、AMQP 0.9.1、Cassandra、Mysql、PostgreSQL、Redis、Thrift-RPC、MongoDB、Memcache 等
  • Winlogbeat:windows 事件日志(收集Windows事件日志数据)
  • Audibeat:审计数据(收集审计日志)
  • Heartbeat:运行时间监控(收集系统运行时的数据),支持 ICMP (v4 and v6) 、TCP、HTTP 等协议
  • Functionbeat:收集、传送并监测来自您的云服务的相关数据
  • Journalbeat:读取journald日志

1.4.1 Filebeat

Filebeat 代替了 logstash 收集日志的工作,将收集好的日志直接发送给 logstash 进行过滤,在很大程度上减轻了服务器的压力,工作流程如下:

  • Filebeat 会启动一个或多个实例去指定的日志目录查找数据(Input)
  • 对于每个日志,Filebeat 都会启动一个 Harvester,每个 Harvester 都会将数据发送个 Spooler,再由 Spooler 发送给后端程序(Logstash、ES)

filebeat工作流程

Filebeat Nginx模块

  1. 下载 filebeat
1
2
3
curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-7.15.1-linux-x86_64.tar.gz
tar -xf filebeat-7.15.1-linux-x86_64.tar.gz
mv filebeat-7.15.1-linux-x86_64 filebeat
  1. 查看所有支持的模块
1
./filebeat modules list
  1. 启动 nginx 模块
1
./filebeat modules enable nginx
  1. 修改 logstash 规则文件并启动
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
vim ./logstash/config/conf.d/elk_filebeat_nginx_log.conf
input {
beats {
port => 5044
}
}

filter {

grok {
match => {
"message" => "%{IP:remote_addr} (?:%{DATA:remote_user}|-) \[%{HTTPDATE:timestamp}\] %{IPORHOST:http_host} %{DATA:request_method} %{DATA:request_uri} %{NUMBER:status} (?:%{NUMBER:body_bytes_sent}|-) (?:%{DATA:request_time}|-) \"(?:%{DATA:http_referer}|-)\" \"%{DATA:http_user_agent}\" (?:%{DATA:http_x_forwarded_for}|-) \"(?:%{DATA:http_cookie}|-)\""
}
}

geoip {
source => "remote_addr"
}

date {
match => [ "timestamp","dd/MMM/YYYY:HH:mm:ss Z"]
}

useragent {
source=>"http_user_agent"
}

# 由于host中包含name,而es会把host看作一个json对象,需要转变成字符,否则会导致logstash无法传输数据给es
mutate {
rename => { "[host][name]" => "host" }
}


}

output {
elasticsearch {
hosts => ["192.168.88.130:9200"]
index => "nginx-log-%{+YYYY.MM.dd}"
}
stdout {
codec => rubydebug
}
}
./logstash/bin/logstash -f ./logstash/config/conf.d/elk_filebeat_nginx_log.conf
  1. 修改 nginx 模块文件
1
2
3
4
5
6
7
8
vim modules.d/nginx.yml
- module: nginx
access:
enabled: true
var.paths: ["/var/log/nginx/access.log*"]
error:
enabled: true
var.paths: ["/var/log/nginx/error.log*"]
  1. 配置 filebeat
1
2
3
4
5
6
7
8
9
10
11
12
13
vim ./filebeat/filebeat.yml
# 通过nginx模块来实现,所以不开启
filebeat.inputs:
- type: log
enabled: false
paths:
- /var/log/nginx/*.log
filebeat.config.modules:
path: /home/elk/filebeat/modules.d/*.yml
reload.enabled: false
# 主要配置
output.logstash:
hosts: ["192.168.88.130:5044"]
  1. 启动 filebeat
1
2
3
su elk
./filebeat setup
./filebeat -e
  1. 在 logstash 或 kibana 中就可以看到新的数据

filebeat测试

Filebeat MySQL模块收集日志

MySQL

MySQL 是一款关系型数据库管理系统。

logo

一、MySQL基础

1.1 安装MySQL

docker-compose

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
version: '3.8'
services:
db:
image: mysql:5.7.35
command: --default-authentication-plugin=mysql_native_password
restart: always
ports:
- 3306:3306
environment:
MYSQL_ROOT_PASSWORD: toortoor

adminer:
image: adminer:latest
restart: always
ports:
- 8080:8080

1.2 基础SQL语句

  1. 刷新权限
1
flush privileges;
  1. 数据库基本操作
1
2
3
4
5
6
-- 创建
create database [if not exists] `db_name`;
-- 删除
drop database [if exists] `db_name`;
-- 使用
use `db_name`;
  1. 表基本操作
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
-- 创建
create table [if not exists] `table_name`;
-- 删除
drop table [if exists] `table_name`;
-- 查看
describe `table_name`;
-- 创建一个学生表
create table if not exists `students`(
-- 创建id列,数据类型为4位的int类型,不允许为空,自增
`id` int(4) not null auto_increment comment '学号',
-- 创建name列,数据类型为30位的varchar类型,不允许为空,默认值为匿名
`name` varchar(30) not null default '匿名' comment '姓名',
`pwd` varchar(20) not null default '123456' comment '密码',
`gender` varchar(2) not null default '女' comment '性别',
-- 创建birthday列,datetime类型,默认为空
`brithday` datetime default null comment '出生日期',
`address` varchar(100) default null comment '家庭住址',
`email` varchar(50) default null comment '邮箱',
-- 设置主键,一般一个表只有一个主键
primary key(`id`)
)
-- 设置引擎
engine=innodb
-- 默认编码
default charset=utf8;
  1. 修改表
1
2
3
4
5
6
7
8
9
10
-- 修改表名称
alter table `table_name` rename as new_`table_name`;
-- 增加字段
alter table `table_name` add age int(10);
-- 修改约束
alter table `table_name` modify age varchar(10);
-- 重命名字段
alter table `table_name` change age new_age int(10);
-- 删除字段
alter table `table_name` drop age;
  1. 查看创建语句
1
2
show create database `db_name`;
show create table `table_name`;

1.3 引擎INNODB和MYISAM区别

特性 INNODB MYISAM
事务 支持 不支持
数据行锁定 支持 不支持
外键 支持 不支持
全文索引 不支持 支持
空间占用 约为MYSIAM的2倍 较小

不同引擎在文件上的区别

  • innodb:
    • *.frm:表结构定义文件
    • ibdata1:数据文件
  • mysiam:
    • *.frm:表结构定义文件
    • *.MYD:数据文件
    • *.MYI:索引文件

二、MySQL数据操作

2.1 外键

外键就是一个表的某一列去引用另一个表的某一列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 该示例为students中的grade_id列引用grade中的grade_id列
create table `grade`(
`grade_id` int(10) not null auto_increment comment '年级'
primary key(`grade_id`)
)engine=innodb default charset=utf8

create table `students`(
`id` int(4) not null auto_increment comment '学号',
`name` varchar(30) not null default '匿名' comment '姓名',
`grade_id` int(10) not null comment '年级',
primary key(`id`),
-- 定义外键
key `FK_grade_id` (`grade_id`),
-- 给外键添加约束
constraint `FK_grade_id` foreign key (`grade_id`) references `grade` (`grade_id`)
)engine=innodb default charset=utf8

如果要给已经存在的表添加外键

1
alter table `table_name` add constraint `FK_name` foreign key (`列名称`) references `引用的表名` (`引用的列名称`)

删除外键

1
alter table `table_name` drop foreign key 'FK_name'

2.2 DML数据操纵语言

2.2.1 insert

1
2
3
4
insert into `table_name`(`字段1`,`字段2`,`字段3`) values('值1'),('值2'),('值3');
insert into `students`(`name`) values('cqm'),('lwt');
-- 字段可以省略,但后面的值必须一一对应
insert into `students`(`name`,`pwd`,`email`) values('lwt','111','lwt@qq.com');

2.2.2 update

1
2
3
4
5
6
7
-- 如果不指定条件,那么会修改所有的值
update `table_name` set `字段`='值' where 条件;
-- 条件可以是 =|<|>|!=|between|and|or 等等
update `students` set `name`='handsome_cqm' where `id`=1;
update `students` set `name`='cqm' where `name`='handsome_cqm'
update `students` set `name`='newlwt',`email`='new@qq.com' where `name`='lwt';
update `students` set `name`='cqm' where id between 1 and 2;

2.2.3 delete

1
2
3
4
delete from `table_name` where 条件;
delete from `students` where `id`=1;
-- 清空表
truncate `table_name`;

deletetruncate 区别:

  • 都可以清空表,都不会删除表结构
  • truncate 可以重置自增列,且不会影响事务
  • delete 清空表后,如果引擎是 innodb,重启数据库自增列就会变回1,因为是存储在内存中的;如果引擎是 myisam,则不会,因为是存储在文件中的

2.3 DQL数据库查询语言

2.3.1 select

  1. 查询所有数据
1
select * from `table_name`;
  1. 查询某列数据
1
select `name`,`gander` from `students`;
  1. 给字段结果起别名
1
select `name` as '姓名',`gender` as '性别' from `students`;
  1. concat 函数
1
select concat('姓名:',`name`) from `students`;
  1. 去重
1
select distinct `字段` from `table_name`
  1. 批量操作数据
1
select `score`+1 as `new_score` from `table_name`;

2.3.2 where

  1. 逻辑运算符运用
1
2
3
select `score` from `table_name` where `score` >= 95 and `score` <= 100;
select `score` from `table_name` where `score` between 95 and 100;
select `score` from `table_name` where not `score` != 95 and `score` != 100;
  1. 模糊查询
运算符 语法 描述
is null a is null a 为空,结果为真
is not null a is not null a 不为空,结果为真
between a between b and c 若 a 在 b 和 c 之间,结果为真
like a like b 如果 a 匹配 b,结果为真
in a in ( b,c,d,e ) 如果 a 在某个集合中的值相同,结果为真

查找花名册中姓陈的名字

1
2
3
4
-- %:匹配一个或多个字符
-- _:匹配一个字符
select `name` from `table_name` where `name` like '陈%';
select `name` from `table_name` where `name` like '陈_明';

查找特定学号的信息

1
select `name` from `table_name` where `id` in (1,2,3);

查找地址为空或不空的同学

1
2
select `name` from `table_name` where `address` is null;
select `name` from `table_name` where `address` is not null;

2.3.3 联表查询

联表查询包括:

  • inner(内连接):如果表中有至少一个匹配,则返回行
  • left(外连接):即使右表中没有匹配,也从左表返回所有的行
  • right(外连接):即使左表中没有匹配,也从右表返回所有的行
  • full(外连接):只要其中一个表中存在匹配,则返回行

inner 实际就是取两个表的交集,例如有两个表,一个表有学生的基本信息,另一个表有学生的成绩,两个表都有学生的学号列,那么就可以联合起来查询

1
select `name`,`subjectno`,`score` from `students` inner join `result` where student.id = result.id

left 假设学生表中有 ccc 这么一个学生,但成绩表里没有 ccc 学生的成绩,如果使用了左连接,左表是学生表,右表是成绩表,那么也会返回 cqm 学生的值,显示为空

leftjoin

right 相反,如果成绩表里有个 ddd 学生的成绩,但学生表里没有这个学生的信息,如果使用了右连接,左表是学生表,右表是成绩表,那么也会返回 ddd 学生的成绩,注意这时候就看不到 ccc 学生的成绩信息了,因为左表中没有 ccc 学生的成绩信息

rightjoin

通过联表查询就可以查出缺考的同学

查询缺考同学

查询参加考试了的同学信息

1
select distinct `name` from `students` right join `result` on students.id = result.id where `score` is not null;

查询参加了考试的同学以及所对应的学科成绩

查询学生对应学科成绩

2.3.4 where和on的区别

在进行联表查询的时候,数据库都会在中间生成一张临时表,在使用外连接时,on 和 where 区别如下:

  • on 条件是在生成临时表时使用的条件,它不管 on 中的条件是否为真,都会返回左边表中的记录。
  • where 条件是在临时表生成好后,再对临时表进行过滤的条件。这时已经没有 left join 的含义(必须返回左边表的记录)了,条件不为真的就全部过滤掉。
  • 如果是 inner join,则无区别

2.3.5 自连接

自连接实际上就是一张表与自己连接,将一张表拆为两张表

原表

categoryid pid categoryname
2 1 计算机科学与技术学院
3 1 心理学院
4 1 体育学院
5 2 python
6 3 心理学
7 4 短跑
8 4 篮球

父类表

category categoryname
2 计算机科学与技术学院
3 心理学院
4 体育学院

子类表

category 所属父类 categoryname
5 计算机科学与技术学院 python
6 心理学院 心理学
7 体育学院 短跑
8 体育学院 篮球

自查询结果

自连接

2.3.6 分页和排序

排序 order by

  • desc:降序
  • asc:升序
1
2
-- 语法
order by desc|asc

查询语文成绩并排序

排序

分页 limit

1
2
-- 语法
limit 起始值,页面显示多少条数据

打印第一页语文成绩

分页第一页

打印第二页数学成绩

分页第二页

分页和排序配合起来就可以查询学生的前几名成绩,例如语文成绩前三名

查询语文成绩前三名

2.3.7 子查询

本质上就是在判断语句里嵌套一个查询语句,例如要查询所有语文成绩并降序排序,可以通过联表查询和子查询两种方式实现

子查询

2.4 聚合函数

2.4.1 count

count 函数用于计数,例如查询有多少学生

1
select count(`name`) from students;

函数内所带的参数不同,背后运行也不同:

  • count(字段):会忽略空值,不计数
  • count(*):不会忽略空值
  • count(1):不会忽略空值
  • 从效率上看:如果查询的列为主键,那么 count(字段) 比 count(1) 快,不为主键则反之;如果表中只有一列,则 count(*) 最快

2.4.2 sum

顾名思义,用于求和,例如查询所有成绩之和

1
select sum(`score`) as '总分' from result

2.4.3 avg

1
select avg(`score`) as '平均分' from result

2.4.4 max和min

1
select max(`score`) as '最高分' from result
1
select min(`score`) as '最低分' from result

查询所有科目的平均分、最高分和最低分

1
2
3
4
5
6
select subjectname, avg(studentresult), max(studentresult), min(studentresult)
from `result`
inner join `subject`
on `result`.subjectno = `subject`.subjectno
-- 定义字段进行分组
group by result.subjectno;

通过分组后的次要条件,查询平均分大于 80 分的科目

1
2
3
4
5
6
7
select subjectname, avg(studentresult) as 平均分
from `result`
inner join `subject`
on `result`.subjectno = `subject`.subjectno
group by `result`.subjectno
-- 分组后的次要条件
having 平均分 > 80;

2.5 MD5加密

在数据库中,密码等敏感信息都不会以明文的形式存储的,可以通过md5 进行加密

1
2
-- 更新密码以达成加密
update students set pwd=md5(pwd);
1
2
-- 插入的时候就进行加密
insert into students values(1, 'cqm', md5('12345'))

2.6 事务

事务就是一系列 SQL 语句,要么全部执行,要么全部不执行。

事务的特性(ACID):

  • 原子性(Atomicity):所有操作要么全部成功,要么全部失败
  • 一致性(Consistency):事务的执行的前后数据的完整性保持一致
  • 隔离性(Isolation):一个事务执行的过程中,不受到别的事务的干扰
  • 持久性(Durability):事务一旦结束,就会持久到数据库

隔离所导致的一些问题:

  • 脏读:一个事务读取到了另一个事务没提交的数据
  • 不可重复读:一个事务的多次查询结果不同,是因为查询期间数据被另一个事务提交而修改了
  • 虚读:一个事务A在进行修改数据的操作时,另一个事务B插入了新的一行数据,而对于事务A来看事务B添加的那行就没有做修改,就发生了虚读
  1. 事务关闭自动提交
1
set autocommit = 0;
  1. 事务开启
1
start transaction;
  1. 事务提交
1
commit;
  1. 事务回滚
1
rollback;
  1. 事务结束
1
set autocommit = 1;
  1. 保存点
1
2
3
4
5
6
-- 添加保存点
savepoint 保存点名称
-- 回滚到保存点
rollback to savepoint 保存点名称
-- 删除保存点
release savepoint 保存点名称

2.7 索引

索引是帮助 MySQL 高效获取数据的数据结构。

索引的分类:

  • 主键索引(primary key):只有一列可以作为主键,且主键的值不可重复,一般用于用户ID之类的
  • 唯一索引(unique key):唯一索引可以有多个,且值唯一
  • 常规索引(key):例如一个表中的数据经常用到,就可以添加个常规索引
  • 全文索引(fulltext):快速定位数据
  1. 查看某个表的所有索引
1
show index from `table_name`;
  1. 添加全文索引
1
alter table `table_name` add fulltext index `index_name`(`要添加索引的字段名`);
  1. 添加常规索引
1
2
-- 这样会给表中某个字段的数据全部都添加上索引,在数据量大的时候可以提高查询效率
create index `id_table_name_字段名` on `table_name`('字段名');

索引使用原则:

  • 并不是索引越多越好
  • 不要对进程变动的数据添加索引
  • 数据量小的表不需要索引
  • 索引一般用于常查询的字段上

2.8 权限管理

在 MySQL 中,用户表为 mysql.user,而权限管理其实都是在该表上操作。

  1. 创建用户
1
2
3
4
5
6
-- host可以为以下的值
-- %:允许所有ip连接
-- localhost:只允许本地连接
-- 192.168.88.%:只允许给网段连接
-- 192.168.88.10:只允许该ip连接
create user 'user_name'@'host' identified by 'user_password';
  1. 修改当前用户密码
1
set password = password('new_password');
  1. 修改指定用户密码
1
set password for user_name = password('new_password');
  1. 重命名
1
rename user user_name to new_user_name;
  1. 授权
1
2
-- 权限:select、insert、delete等等,所有权限则为all
grant 权限 on `db_name`.`table_name` to 'user_name'@'host';
  1. 授予某个用户部分权限
1
grant select,insert on mysql.user to 'cqm'@'%';
  1. 给某个用户授予全部数据库的权限
1
2
-- 基本权限都有,但不会给grant权限
grant all privileges on *.* to 'cqm'@'%';
  1. 删除用户
1
drop user 'user_name'@'host';
  1. 取消权限
1
revoke 权限 on `db_name`.`table_name` from 'user_name'@'host';
  1. 查询用户权限
1
show grants for 'user_name'@'host';

2.9 备份

备份文件都是以 .sql 为后缀的文件。

  1. 导出
1
2
3
4
5
# -d:要操作的数据库
# -h:指定主机
# -P:指定端口
# --all-databases:操作所有数据库
mysqldump -uroot -ppassword -d db1_name db2_name > db_backup.sql
  1. 导入
1
mysqldump -uroot -ppassword -d db_name < db_backup.sql

三、MySQL配置

3.1 主从同步/复制

MySQL 主从同步即每当主数据库进行了数据的操作后,就会将操作写入 binlog 文件,从数据库会启动一个 IO 线程去监控主数据库的 binlog 文件,并将 binlog 文件的内容写入自己的 relaylog 文件中,同时会启动一个 SQL 线程去监控 relaylog 文件,如果发生变化就更新数据。

主从同步流程图

主从复制的类型:

  • statement模式(sbr):只有修改数据的 SQL 语句会记录到 binlog 中,优点是减少了日志量,节省 IO,提高性能,不足是可能会导致主从节点之间的数据有差异。
  • row模式(rbr):仅记录被修改的数据,不怕无法正确复制的问题,但会产生大量的 binlog。
  • mixed模式(mbr):sbr 和 rbr 的混合模式,一般复制用 sbr,sbr 无法复制的用 rbr。

主从同步实现

  1. docker-compose.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
version: '3.8'
services:
mysql_master:
image: mysql:5.7.35
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
MYSQL_ROOT_PASSWORD: toortoor
ports:
- 3306:3306
volumes:
- ./master/my.cnf:/etc/mysql/my.cnf

mysql_slave:
image: mysql:5.7.35
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
MYSQL_ROOT_PASSWORD: toortoor
ports:
- 3307:3306
volumes:
- ./slave/my.cnf:/etc/mysql/my.cnf
  1. master/my.cnf
1
2
3
4
5
6
7
8
9
10
11
12
13
[mysqld]
server-id = 1 #节点ID,确保唯一
log-bin = mysql-bin #开启mysql的binlog日志功能
sync_binlog = 1 #控制数据库的binlog刷到磁盘上去,0不控制,性能最好;1每次事物提交都会刷到日志文件中,性能最差,最安全
binlog_format = mixed #binlog日志格式,mysql默认采用statement,建议使用mixed
expire_logs_days = 7 #binlog过期清理时间
max_binlog_size = 100m #binlog每个日志文件大小
binlog_cache_size = 4m #binlog缓存大小
max_binlog_cache_size= 512m #最大binlog缓存大
binlog-ignore-db=mysql #不生成日志文件的数据库,多个忽略数据库可以用逗号拼接,或者复制这句话,写多行
auto-increment-offset = 1 #自增值的偏移量
auto-increment-increment = 1 #自增值的自增量
slave-skip-errors = all #跳过从库错误
  1. slave/my.cnf
1
2
3
4
5
6
7
[mysqld]
server-id = 2
log-bin=mysql-bin
relay-log = mysql-relay-bin
replicate-wild-ignore-table=mysql.%
replicate-wild-ignore-table=test.%
replicate-wild-ignore-table=information_schema.%
  1. 在 master 创建复制用户并授权
1
2
3
create user 'repl_user'@'%' identified by 'toortoor';
grant replication slave on *.* to 'repl_user'@'%' identified by 'toortoor';
flush privileges;
  1. 查看 master 状态
1
2
3
4
5
6
show master status;
+------------------+----------+...
| File | Position |...
+------------------+----------+...
| mysql-bin.000003 | 844 |...
+------------------+----------+...
  1. 在从数据库配置
1
2
3
4
5
6
7
8
9
change master to
MASTER_HOST = '172.19.0.3',
MASTER_USER = 'repl_user',
MASTER_PASSWORD = 'toortoor',
MASTER_PORT = 3306,
MASTER_LOG_FILE='mysql-bin.000003',
MASTER_LOG_POS=844,
MASTER_RETRY_COUNT = 60,
MASTER_HEARTBEAT_PERIOD = 10000;
  1. 启动从配置
1
2
start slave;
show slave status\G;
  1. 如果配置失败,可以执行
1
2
stop slave;
set global sql_slave_skip_counter=1;

3.2 Mycat读写分离

在一般项目中,对于数据库的操作读要远大于写,而如果所有的操作都放在一个节点上,那么就很容易出现压力过大而宕机,读写分离就可以很好解决该问题,主要通过 mycat 的中间件来实现。

mycat架构

分库分表类型:

  • 水平拆分:将不同等级的会员信息写到不同的表中
  • 垂直拆分:将买家信息、卖家信息、商品信息、支付信息等不同信息写到不同的表中

Mycat的主要文件:

文件 说明
server.xml 设置 Mycat 账号、参数等
schema.xml 设置 Mycat 对应的物理数据库和表等
rule.xml 分库分表设置
  1. server.xml
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
<!-- Mycat用户名 -->
<user name="root" defaultAccount="true">
<!-- 密码 -->
<property name="password">123456</property>
<!-- 逻辑库名 -->
<property name="schemas">TESTDB</property>
<!-- 默认逻辑库 -->
<property name="defaultSchema">TESTDB</property>
<!--No MyCAT Database selected 错误前会尝试使用该schema作为schema,不设置则为null,报
错 -->

<!-- 表级 DML 权限设置 -->
<!-- 0为禁止,1为开启 -->
<!-- 按顺序分别为insert、update、select、delete -->
<!--
<privileges check="false">
<schema name="TESTDB" dml="0110" >
<table name="tb01" dml="0000"></table>
<table name="tb02" dml="1111"></table>
</schema>
</privileges>
-->
</user>
<!-- 其他用户设置 -->
<user name="user">
<property name="password">user</property>
<property name="schemas">TESTDB</property>
<property name="readOnly">true</property>
<property name="defaultSchema">TESTDB</property>
</user>
  1. schema.xml
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
<mycat:schema xmlns:mycat="http://io.mycat/">
<!--
name为逻辑库名称,与server.xml文件对应
checkSQLschema为true,sql为select * from table_name
checkSQLschema为false,sql为select * from TESTDB.table_name
sqlMaxLimit是指如果sql中没有limit,则自动添加,如果有则不添加
-->
<schema name="TESTDB" checkSQLschema="true" sqlMaxLimit="100" randomDataNode="dn1">
<!--
name为逻辑表名
dataNode为数据节点名称
rule为规则
-->
<table name="customer" primaryKey="id" dataNode="dn1,dn2" rule="sharding-by-intfile" autoIncrement="true" fetchStoreNodeByJdbc="true">
<childTable name="customer_addr" primaryKey="id" joinKey="customer_id" parentKey="id"> </childTable>
</table>
</schema>
<!--
name为数据节点名称
dataHost为数据库地址
database为mysql中的database
实际就是将TESTDB逻辑库拆成dn1和dn2,而dn1和dn2又对应db1和db2
-->
<dataNode name="dn1" dataHost="localhost1" database="db1" />
<dataNode name="dn2" dataHost="localhost1" database="db2" />
<dataNode name="dn3" dataHost="localhost1" database="db3" />
<!--
balance:
0:不开启读写分离,所有操作都在writeHost上
1:所有读操作都随机发送到当前的writeHost对应的readHost和备用的writeHost
2:所有读操作都随机发送到所有的主机上
3:所有读操作只发送到readHost上
writeType:
0:所有写操作都在第一台writeHost上,第一台挂了再切到第二台
1:所有写操作都随机分配到writeHost
switchType: 用于是否允许writeHost和readHost之间自动切换
-1:不允许
1:允许
2:基于mysql的主从同步的状态决定是否切换
-->
<dataHost name="localhost1" maxCon="1000" minCon="10" balance="0"
writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1" slaveThreshold="100">
<!-- 用此命令来进行心跳检测 -->
<heartbeat>select user()</heartbeat>
<!-- 设置读写分离 -->
<writeHost host="hostM1" url="jdbc:mysql://localhost:3306" user="root"
password="root">
<readHost host="hostS1" url="jdbc:mysql://localhost:3306" user="root"
password="root" />
</readHost>
</writeHost>
</dataHost>
</mycat:schema>
  1. rule.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 平均分算法 -->
<tableRule name="mod-long">
<rule>
<!-- 根据id值平均分 -->
<columns>id</columns>
<algorithm>mod-long</algorithm>
</rule>
</tableRule>
...
<function name="mod-long" class="io.mycat.route.function.PartitionByMod">
<!-- 切片个数 -->
<property name="count">2</property>
</function>
  1. 在 master 节点创建数据库
1
2
3
-- 与schema.xml中的database参数相同
create database db1;
create database db2;
  1. 在每个库里创建表
1
2
3
4
5
create table students(
id int(4),
name varchar(10),
primary key(`id`)
)engine=innodb default charset=utf8;
  1. 开启 Mycat,默认开启8066服务端端口和9066管理端端口
1
./mycat start
  1. 在有安装 mysql 的主机登录 Mycat,也可以通过 navicat 连接
1
mysql -uroot -ptoortoor -h 192.168.88.136 -P 8066

mycat测试一

  1. 只要在 Mycat 进行 SQL 操作,都会流到 mysql 集群中被处理,也可以看到已经实现了分库分表

mycat测试二

3.3 使用haproxy实现Mycat高可用

haproxy 可以实现 Mycat 集群的高可用和负载均衡,而 haproxy 的高可用通过 keepalived 来实现。

高可用集群架构图

  1. 安装 haproxy
1
yum -y install haproxy
  1. 修改日志文件
1
2
3
4
5
vim /etc/rsyslog.conf
# Provides UDP syslog reception
$ModLoad imudp
$UDPServerRun 514
systemctl restart rsyslog
  1. 配置 haproxy
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
vim /etc/haproxy/haproxy.cfg
# haproxy的配置文件由两个部分构成,全局设定和代理设定
# 分为五段:global、defaults、frontend、backend、listen
global
# 定义全局的syslog服务器
log 127.0.0.1 local2
chroot /var/lib/haproxy
pidfile /var/run/haproxy.pid
maxconn 4000
user haproxy
group haproxy
# 设置haproxy后台守护进程形式运行
daemon
stats socket /var/lib/haproxy/stats
defaults
# 处理模式
# http:七层
# tcp:四层
# health:状态检查,只会返回OK
mode tcp
# 继承global中log的定义
log global
option tcplog
option dontlognull
option http-server-close
# option forwardfor except 127.0.0.0/8
# serverId对应的服务器挂掉后,强制定向到其他健康的服务器
option redispatch
retries 3
timeout http-request 10s
timeout queue 1m
timeout connect 10s
timeout client 1m
timeout server 1m
timeout http-keep-alive 10s
timeout check 10s
maxconn 3000
frontend mycat
# 开启本地监控端口
bind 0.0.0.0:8066
bind 0.0.0.0:9066
mode tcp
log global
default_backend mycat
backend mycat
balance roundrobin
# 监控的Mycat
server mycat1 192.168.88.135:8066 check inter 5s rise 2 fall 3
server mycat2 192.168.88.135:8066 check inter 5s rise 2 fall 3
server mycatadmin1 192.168.88.136:9066 check inter 5s rise 2 fall 3
server mycatadmin2 192.168.88.136:9066 check inter 5s rise 2 fall 3
listen stats
mode http
# 访问haproxy的端口
bind 0.0.0.0:9999
stats enable
stats hide-version
# url路径
stats uri /haproxy
stats realm Haproxy\ Statistics
# 用户名/密码
stats auth admin:admin
stats admin if TRUE
  1. 访问 haproxy

haproxy界面

3.4 使用keepalived实现去中心化

  1. 安装 keepalived
1
yum -y install keepalived
  1. 配置 Master 节点
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
vim /etc/keepalived/keepalived.conf
! Configuration File for keepalived

global_defs {
# 识别节点的id
router_id haproxy01
}

vrrp_instance VI_1 {
# 设置角色,由于抢占容易出现 VIP 切换而闪断带来的风险,所以该配置为不抢占模式
state BACKUP
# VIP所绑定的网卡
interface ens33
# 虚拟路由ID号,两个节点必须一样
virtual_router_id 30
# 权重
priority 100
# 开启不抢占
# nopreempt
# 组播信息发送间隔,两个节点必须一样
advert_int 1
# 设置验证信息
authentication {
auth_type PASS
auth_pass 1111
}
# VIP地址池,可以多个
virtual_ipaddress {
192.168.88.200
}
# 将 track_script 块加入 instance 配置块
track_script{
chk_haproxy
}
}

# 定义监控脚本
vrrp_script chk_haproxy {
script "/etc/keepalived/haproxy_check.sh"
# 时间间隔
interval 2
# 条件成立权重+2
weight 2
}
  1. 配置 Slave 节点
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
vim /etc/keepalived/keepalived.conf
! Configuration File for keepalived

global_defs {
router_id haproxy02
}

vrrp_instance VI_1 {
state BACKUP
interface ens33
virtual_router_id 30
priority 80
# nopreempt
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.88.200
}
track_script{
chk_haproxy
}
}

vrrp_script chk_haproxy {
script "/etc/keepalived/haproxy_check.sh"
interval 2
weight 2
}
  1. 编写监控脚本
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
vim /etc/keepalived/haproxy_check.sh
#!/bin/bash

START_HAPROXY="systemctl start haproxy"
STOP_KEEPALIVED="systemctl stop keepalived"
LOG_DIR="/etc/keepalived/haproxy_check.log"
HAPS=`ps -C haproxy --no-header | wc -l`

date "+%F %H:%M:%S" > $LOG_DIR
echo "Check haproxy status" >> $LOG_DIR
if [ $HAPS -eq 0 ];
then
echo "Haproxy is down" >> $LOG_DIR
echo "Try to turn on Haproxy..." >> $LOG_DIR
echo $START_HAPROXY | sh
sleep 3
if [ `ps -C haproxy --no-header | wc -l` -eq 0 ];
then
echo -e "Start Haproxy failed, killall keepalived\n" >> $LOG_DIR
echo $STOP_KEEPALIVED | sh
else
echo -e "Start Haproxy successed\n" >> $LOG_DIR
fi
else
echo -e "Haproxy is running\n" >> $LOG_DIR
fi
  1. 启动 keepalived 后可以看到 VIP 被哪台服务器抢占了,通过该 VIP 就可以访问到对应的 haproxy,haproxy 就会将流量流到后面的 Mycat,再由 Mycat 来实现分表分库;haproxy 停止后 keepalived 也会通过脚本尝试去重新开启,如果开启不成功就会停止 keepalived,VIP 就由 slave 节点抢占,用户依旧可以通过 VIP 来操控数据库,且无感知。

keepalived测试一

  1. 通过 VIP 去连接 Mycat 插入数据,尝试能否实现分库分表

keepalived测试二

可以看到插入的数据都分到了 db1、db2 中

keepalived测试三

3.5 Sharding JDBC读写分离

Apache ShardingSphere 是一套开源的分布式数据库解决方案组成的生态圈,它由 JDBC、Proxy 和 Sidecar(规划中)这 3 款既能够独立部署,又支持混合部署配合使用的产品组成。

Sharding JDBC 同样也可以实现分库分表、数据分片、读写分离等功能,但与 Mycat 不同的是,Mycat 是一个独立的程序,而 Sharding JDBC 是以 jar 包的形式与应用程序融合在一起运行的。

Zookeeper

logo

ZooKeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是 Hadoop 和 Hbase 的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

一、Zookeeper基础

1.1 使用场景

  • 分布式协调组件:通过 watch 机制可以协调好节点之间的数据一致性
  • 分布式锁:通过分布式锁可以做到强一致性
  • 无状态化实现
  • 负载均衡
  • 数据发布/订阅
  • 命名服务

1.2 部署

docker-compose.yaml

1
2
3
4
5
6
7
8
9
10
11
12
version: '3.8'
services:
zookeeper:
container_name: zk01
image: zookeeper:3.7.0
restart: always
hostname: zk01
ports:
- 2181:2181
environment:
ZOO_MY_ID: 1
ZOO_SERVERS: server.1=zk01:2888:3888;2181

zoo.cfg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dataDir=/data
dataLogDir=/datalog
# 基本时间配置(毫秒)
tickTime=2000
# 初始化连接到leader的最大时长,单位为倍数,即初始化时间为tickTime * initLimit
initLimit=5
# follower与leader数据同步的最大时长
syncLimit=2
# 保存数据的快照数量
autopurge.snapRetainCount=3
# 自动触发清除任务时间间隔,以小时为单位,默认为0,表不清除
autopurge.purgeInterval=0
# 客户端与zk的最大并发连接数
maxClientCnxns=60
# 开启standaloneEnabled模式,即独立部署
standaloneEnabled=true
# 开启adminServer
admin.enableServer=true
# 2181是为客户端提供的端口
server.1=zk01:2888:3888;2181

1.3 基本命令

  1. 启动|关闭|查看状态
1
zkServer.sh start|stop|status
  1. 进入 zk
1
zkCli.sh
  1. 查看内部数据结构
1
ls [path]

二、内部数据结构

2.1 是如何存储数据的

Zookeeper 中的数据是保存在节点上的,即 znode,多个 znode 就构成一个树的结构。

zk数据存储结构

如图,a 和 b 就是 Zookeeper 的 znode,创建 znode 方式如下

1
2
3
4
5
create /[znode_name]
# 创建节点并创建一个数据
create /[znode_name] [data_name]
# 获取数据
get [znode_name]

2.2 znode结构

Zookeeper 中的 zonode,包含以下几个部分:

  • data:保存数据
  • acl:权限
    • c:创建权限
    • w:写权限
    • r:读权限
    • d:删除权限
    • a:admin 管理者权限
  • stat:描述当前 znode 的元数据
  • child:当前节点的子节点
1
2
3
4
5
6
7
8
9
10
11
12
13
# 查看znode详细信息
get -s /[znode_name]
cZxid:创建节点的事务ID
ctime:创建节点时间
mZxid:修改节点的事务ID
mtime:修改节点时间
pZxid:添加和删除子节点的事务ID
cversion:当前节点的子节点版本号,初始值为-1,每对该节点的子节点进行操作,这个cversion都会自动增加
dataVersion:数据版本初识版本为0,每对该节点的数据进行操作,这个dataVersion都会自动增加
aclVersion:权限版本
ephemeralOwne:如果当前节点是临时节点,该值是当前节点的session id,如果不是临时节点则为0
dataLength:数据长度
numChildren:该节点的子节点个数

2.3 znode类型

  • 持久节点:在会话结束后仍会存在
  • 持久序号节点:根据先后顺序,会在结点之后带上一个数值,适用于分布式锁的场景(单调递增)
  • 临时节点:会话结束后会自动删除,适用于注册与服务发现的场景
  • 临时序号节点:跟持久序号节点相同,适用于分布式锁的场景
  • 容器节点:当容器节点中没有任何子节点时,该容器节点会被定期删除(60s)
  • TTL 节点:可以指定节点的到期时间

持久序号节点创建

1
create -s /[znode_name]

临时节点创建

1
create -e /[znode_name]

临时序号节点创建

1
create -e -s /[znode_name]

容器节点创建

1
create -c /[znode_name]

TTL 节点创建

1
2
# 通过系统配置开启
zookeeper.extendedTypesEnabled=true

持久与临时节点

持久节点

持久节点在创建后服务端会发送一个 session id,并一直保留着。

临时节点

临时节点在创建时后服务器也会发送一个 session id,在会话持续的过程中客户端会不断向服务端续约 session id 的时间,当客户端没有继续续约,而服务端内部的计时器到期时,就会将该 session id 所对应的 znode 全部删除。

2.4 持久化机制

Zookeeper 的数据是运行在内存中的,所以提供了两种持久化机制:

  • 事务日志:Zookeeper 将执行过的命令以日志的形式存储在 dataLogDir / dataDir 中,类似于 redis 的 AOF
  • 数据快照:在一定时间间隔内做一次数据快照,存储在快照文件中(snapshot),类似于 redis 的RDB

Zookeeper 通过这两种持久化机制,在恢复数据时先将快照文件中的数据恢复到内存中,再用日志文件中的数据做增量恢复,可以实现高效的持久化。

三、zkCli的使用

  1. 递归查询
1
ls -R /[znode_name]
  1. 删除节点
1
deleteall /[znode_name]
  1. 乐观锁删除
1
delete -v [version] /[znode_name]
  1. 给当前会话注册用户,并创建节点赋予该用户权限
1
2
addauth digest [user]:[password]
create /[znode_name] auth:[user]:[password]:[privileges]

四、分布式锁

在分布式的环境下,如果在一个节点去上了个锁,当请求被负载均衡分配到了其它节点,那么锁就无法形成互斥,所以节点之间使用 Zookeeper,做一个协调中心,将锁上传到 Zookeeper,其它节点要用到就去 Zookeeper 拿这个锁,这就是分布式锁。

Zookeeper 锁的分类:

  • 读锁:大家都可以读,前提是之前没有写锁。(读锁比喻成约会,大家都有机会和女神约会,约会前提是女神没结婚)
  • 写锁:只有写锁才能写,前提是不能有任何锁。(写锁比喻成结婚,结婚后只有老公能和女神约会,结婚前提是女神和其他人的关系断干净了)

4.1 上读锁

  • 创建一个临时序号节点,节点数据是 read,表示为读锁
  • 获取当前 Zookeeper 中序号比自己小的所有节点
  • 判断最小节点是否为读锁:
    • 如果是读锁:则上锁失败,因为如果最小节点是读锁,那么后面就不可能有写锁,接着为最小节点设置监听,Zookeeper 的 watch 机制会在最小节点发生变化时通知当前节点,再进行后面的步骤,被称为阻塞等待
    • 如果不是读锁:则上锁成功

4.2 上写锁

  • 创建一个临时序号节点,节点数据是 write,表示为写锁
  • 获取 Zookeeper 中的所有节点
  • 判断自己是否为最小节点:
    • 如果是:上锁成功
    • 如果不是:说明前面还有锁,所以上锁失败,接着监听最小节点,如果最小节点发生变化,则重新进行第二步

羊群效应

假设有一百个请求都是要去写锁,那么就会有一百个请求去监听最小节点,那么 Zookeeper 的压力就会非常大,解决方法是将这一百个请求按请求顺序排列,后一个请求去监听前一个请求即可,实现链式监听。

4.3 watch机制

Zookeeper 的 watch 可以看作是一个触发器,当监控的 znode 发生改变,就会触发 znode 上注册的对应事件,请求 watch 的客户端就会接收到异步通知。

zkCli.sh 中使用 watch

1
2
3
4
5
6
7
create /test
# 一次性监听,监听节点内容
get -w /test
# 监听目录,但所监听节点下创建和删除子节点不会触发监听
ls -w /test
# 与上面相对,都会触发监听
ls -R -w /test

五、集群部署

Zookeeper 的集群角色有三个:

  • Leader:处理集群所有事务的请求,集群只有一个 Leader
  • Follower:只处理读请求,参与 Leader 选举
  • Observer:只处理读请求,提升集群的性能,但不能参与 Leader 选举

docker-compose.yaml

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
version: '3.8'
services:
zk01:
container_name: zk01
image: zookeeper:3.7.0
restart: always
hostname: zk01
ports:
- 2181:2181
environment:
ZOO_MY_ID: 1
# 2888:用于集群内zk之间的通信
# 3888:用于选举投票
# 2181:客户端使用
# 要创建observer则在2181端口后加:observer
ZOO_SERVERS: server.1=zk01:2888:3888;2181 server.2=zk02:2888:3888;2181 server.3=zk03:2888:3888;2181

zk02:
container_name: zk02
image: zookeeper:3.7.0
restart: always
hostname: zk02
ports:
- 2182:2181
environment:
ZOO_MY_ID: 2
ZOO_SERVERS: server.1=zk01:2888:3888;2181 server.2=zk02:2888:3888;2181 server.3=zk03:2888:3888;2181

zk03:
container_name: zk03
image: zookeeper:3.7.0
restart: always
hostname: zk03
ports:
- 2183:2181
environment:
ZOO_MY_ID: 3
ZOO_SERVERS: server.1=zk01:2888:3888;2181 server.2=zk02:2888:3888;2181 server.3=zk03:2888:3888;2181

通过命令查看节点角色

1
2
zkServer.sh status
Mode: leader

连接集群

1
zkCli.sh -server zk01:2181,zk02:2181,zk03:2181

5.1 ZAB协议

ZAB(Zookeeper Atomic Broadcast)即 Zookeeper 原子广播协议,通过这个协议解决了集群数据一致性和崩溃恢复的问题。

ZAB 协议中节点的四种状态

  • Looking:选举状态
  • Following
  • Leading
  • Observing

初始化集群时 leader 的选举

  • 当集群中两台节点启动时,就会开始 leader 的选举,选票的格式为 (myid,zXid)
  • 第一轮投票时,每个节点会生成自己的选票,即自己的 (myid,zXid),然后将选票给到对方,这时候每个节点就会有两张选票,即自己的和对方节点的
  • 接着就会比较两张选票的 zXid,如果都相同就对比 myid,将大的一票投到投票箱中
  • 第二轮投票时,每个节点会将上一轮投出去的选票给到其它节点,然后再对比 (myid,zXid),将大的一票投出去,就能够选出 leader
  • 后来新启动的节点会发现已经有 leader了,就不用做选举的过程了
  • 可以看出初始化集群时,leader 的选举主要看 myid

崩溃恢复时的 leader 选举

在 leader 确定了之后,leader 会周期性地向 follower 发送心跳包,当 follower 没有收到 leader 发送过来的心跳包,就会进入选举过程,这时候集群不能对外提供服务。

  • 当 leader 挂了之后,follower 的状态会变成 looking
  • 接着就进行选举投票,过程和初始化集群时一样

5.2 主从同步原理

主从同步原理

5.3 NIO和BIO

NIO

用于被客户端连接的 2181 端口,使用的就是 NIO 的连接模式;客户端开启 watch 时,使用的也是 NIO。

BIO

集群在进行选举时,多个节点之间的通信端口,使用的是 BIO 的连接模式。

Web

一、Apache

1.1 Apache介绍

Apache HTTP Server(简称Apache)是Apache软件基金会的一个开放源码的网页服务器,是世界使用排名第一的Web服务器软件。它可以运行在几乎所有广泛使用的计算机平台上,由于其跨平台和安全性被广泛使用,是最流行的Web服务器端软件之一。它快速、可靠并且可通过简单的API扩充,将Perl/Python等解释器编译到服务器中。

Apache HTTP服务器是一个模块化的服务器,源于NCSAhttpd服务器,经过多次修改,成为世界使用排名第一的Web服务器软件。

Apache官方文档:http://httpd.apache.org/docs/

1.2 通过脚本源码安装Apache

  1. 编写安装脚本
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
useradd -s /sbin/nologin -r www
vim apache-install.sh
#!/bin/bash

apr_version=1.7.0
apr_iconv_version=1.2.2
apr_util_version=1.6.1
apache_version=2.4.46

#检查
function check()
{
#检查是否为root用户
if [ $USER != 'root' ]
then
echo -e "\e[1;31m error:need to be root so that \e[0m"
exit 1
fi

#检查是否安装了wget
if [ `rpm -qa | grep wget | wc -l` -lt 1 ]
then
echo -e "\e[1;31m error:not found wget \e[0m"
exit 1
fi
}

#安装前准备
function install_pre()
{
#安装依赖
if [ ! `yum -y install zlib-devel pcre-devel libxml2 expat-devel &> /dev/null` ]
then
echo -e "\e[1;31m error:yum install dependency package failed \e[0m"
exit 1
fi

#下载apr
cd /usr/local
if [ ! `wget https://downloads.apache.org/apr/apr-${apr_version}.tar.bz2 &> /dev/null` ]
then
tar -xf apr-${apr_version}.tar.bz2
if [ ! -d apr-${apr_version} ]
then
echo -e "\e[1;31m error:not found apr-${apr_version} \e[0m"
exit 1
else
cd apr-${apr_version}
fi
else
echo -e "\e[1;31m error:Failed to download apr-${apr_version}.tar.bz2 \e[0m"
exit 1
fi

#安装apr
echo "apr configure..."
./configure --prefix=/usr/local/apr &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo "apr make && make install..."
make && make install &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo -e "\e[1;32m apr installed successfully \e[0m"
else
echo -e "\e[1;31m apr installed failed \e[0m"
exit 1
fi
else
echo -e "\e[1;31m error:apr configure failed \e[0m"
exit 1
fi

#下载apr-iconv
cd /usr/local
if [ ! `wget https://www.apache.org/dist/apr/apr-iconv-${apr_iconv_version}.tar.bz2 &> /dev/null` ]
then
tar -xf apr-iconv-${apr_iconv_version}.tar.bz2
if [ ! -d apr-iconv-${apr_iconv_version} ]
then
echo -e "\e[1;31m error:not found apr-iconv-${apr_iconv_version} \e[0m"
exit 1
else
cd apr-iconv-${apr_iconv_version}
fi
else
echo -e "\e[1;31m error:Failed to download apr-iconv-${apr_iconv_version}.tar.bz2 \e[0m"
exit 1
fi

#安装apr-iconv
echo "apr-iconv configure..."
./configure --prefix=/usr/local/apr-iconv --with-apr=/usr/local/apr &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo "apr-iconv make && make install..."
make && make install &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo -e "\e[1;32m apr-iconv installed successfully \e[0m"
else
echo -e "\e[1;31m apr-iconv installed failed \e[0m"
exit 1
fi
else
echo -e "\e[1;31m error:apr-iconv configure failed \e[0m"
exit 1
fi

#下载apr-util
cd /usr/local
if [ ! `wget https://www.apache.org/dist/apr/apr-util-${apr_util_version}.tar.bz2 &> /dev/null` ]
then
tar -xf apr-util-${apr_util_version}.tar.bz2
if [ ! -d apr-util-${apr_util_version} ]
then
echo -e "\e[1;31m error:not found apr-util-${apr_util_version} \e[0m"
exit 1
else
cd apr-util-${apr_util_version}
fi
else
echo -e "\e[1;31m error:Failed to download apr-util-${apr_util_version}.tar.bz2 \e[0m"
exit 1
fi

#安装apr-util
echo "apr-util configure..."
./configure --prefix=/usr/local/apr-util --with-apr=/usr/local/apr/ &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo "apr-util make && make install..."
make && make install &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo -e "\e[1;32m apr-util installed successfully \e[0m"
else
echo -e "\e[1;31m apr-util installed failed \e[0m"
exit 1
fi
else
echo -e "\e[1;31m error:apr-util configure failed \e[0m"
exit 1
fi
}

#下载安装Apache
function apache_install()
{
#下载Apache
cd /usr/local
if [ ! `wget https://downloads.apache.org/httpd/httpd-${apache_version}.tar.gz &> /dev/null` ]
then
tar -xf httpd-${apache_version}.tar.gz
if [ ! -d httpd-${apache_version} ]
then
echo -e "\e[1;31m error:not found httpd-${apache_version} \e[0m"
exit 1
else
cd httpd-${apache_version}
fi
else
echo -e "\e[1;31m error:Failed to download httpd-${apache_version} \e[0m"
exit 1
fi

#安装Apache
echo "Apache configure..."
./configure --prefix=/usr/local/apache --enable-mpms-shared=all --with-mpm=event --with-apr=/usr/local/apr --with-apr-util=/usr/local/apr-util --enable-so --enable-remoteip --enable-proxy --enable-proxy-fcgi --enable-proxy-uwsgi --enable-deflate=shared --enable-expires=shared --enable-rewrite=shared --enable-cache --enable-file-cache --enable-mem-cache --enable-disk-cache --enable-static-support --enable-static-ab --disable-userdir --enable-nonportable-atomics --disable-ipv6 --with-sendfile &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo "Apache make && make install..."
make && make install &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo -e "\e[1;32m Apache installed sucessfully \e[0m"
else
echo -e "\e[1;31m Apache installed failed \e[0m"
exit 1
fi
else
echo -e "\e[1;31m Apache configure failed \e[0m"
exit 1
fi
}

check
install_pre
apache_install
  1. 编写启动脚本
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
#!/bin/bash

apache_doc=/usr/local/apache/bin
apache_pid=/usr/local/apache/logs/httpd.pid

function apache_start()
{
apache_num=`ps -ef | grep httpd | wc -l`
if [ $apache_num -gt 1 ] && [ -f $apache_pid ]
then
echo -e "Apache [\e[1;32m running \e[0m]"
exit 1
elif [ $apache_num -eq 1 ] && [ -f $apache_pid ]
then
killall httpd
fi
cd /usr/local/apache/bin;./apachectl
echo -e "start Apache [\e[1;32m OK \e[0m]"
}

function apache_stop()
{
apache_num=`ps -ef | grep httpd | wc -l`
if [ $apache_num -eq 1 ]
then
echo -e "Apache [\e[1;31m stopping \e[0m]"
else
killall httpd
echo -e "stop Apache [\e[1;32m OK \e[0m]"
fi
}

function apache_restart()
{
cd /usr/local/apache/bin;./apachectl restart
echo -e "restart Apache [\e[1;32m OK \e[0m]"
}

function apache_status()
{
apache_num=`ps -ef | grep httpd | wc -l`
if [ $apache_num -gt 1 ] && [ -f $nginx_pid ]
then
echo -e "Apache [\e[1;32m running \e[0m]"
else
echo -e "Apache [\e[1;31m stopping \e[0m]"
fi
}

function apache_reload()
{
apache_num=`ps -ef | grep httpd | wc -l`
if [ $apache_num -gt 1 ] && [ -f $nginx_pid ]
then
cd /usr/local/apache/bin;./apachectl graceful
echo -e "reload Apache [\e[1;32m OK \e[0m]"
else
echo -e "Apache [\e[1;31m stopping \e[0m]"
fi
}

case $1 in
start)
apache_start
;;
stop)
apache_stop
;;

restart)
apache_restart
;;

status)
apache_status
;;

reload)
apache_reload
esac

1.3 多处理模块MPM

Apache HTTP 服务器被设计为一个功能强大,并且灵活的 web 服务器, 可以在很多平台与环境中工作。不同平台和不同的环境往往需要不同 的特性,或可能以不同的方式实现相同的特性最有效率。Apache 通过模块化的设计来适应各种环境。这种设计允许网站管理员通过在 编译时或运行时,选择哪些模块将会加载在服务器中,来选择服务器特性。

实际上就是用来接受请求处理请求的。

Apache的三种工作方式:

  1. Prefork MPM:使用多个进程,每个进程只有一个线程,每个进程再某个确定的时间只能维持一个连接,有点是稳定,缺点是内存消耗过高。

![Prefork MPM](Prefork MPM.png)

  1. Worker MPM:使用多个进程,每个进程有多个线程,每个线程在某个确定的时间只能维持一个连接,内存占用比较小,是个大并发、高流量的场景,缺点是一个线程崩溃,整个进程就会连同其任何线程一起挂掉。

![Worker MPM](Worker MPM.png)

  1. Event MPM:使用多进程多线程+epoll的模式。

![Event MPM](Event MPM.png)

1.4 虚拟主机

默认情况下,一个web服务器只能发布一个默认网站,也就是只能发布一个web站点,对于大网站来说还好,但对于访问量较少的小网站那就显得有点浪费了。

而虚拟主机就可以实现在一个web服务器上发布多个站点,分为基于IP地址、域名和端口三种。

Apache的虚拟主机和默认网站不能够同时存在,如果设置了虚拟主机那么默认网站也就失效了,需要在用虚拟主机发布默认站点才可解决。

基于IP:基于IP的虚拟主机需要耗费大量的IP地址,只适合IP地址充足的环境。

基于端口:需要耗费较多的端口,适合私网环境。

基于域名:需要耗费较多的域名,适合公网环境。

1.4.1 基于IP的虚拟主机

  1. 在主配文件中调用虚拟主机文件
1
2
3
vim /usr/local/apache/conf/httpd.conf
# Virtual hosts
Include conf/extra/httpd-vhosts.conf
  1. 修改虚拟主机文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 添加一个逻辑网卡,重启即失效
ifconfig eth0:1 192.168.88.100/24 up
vim /usr/local/apache/conf/extra/httpd-vhosts.conf
<VirtualHost 49.232.160.75:80>
# 管理员邮箱
# ServerAdmin webmaster@dummy-host.example.com
# web目录
DocumentRoot "/usr/local/apache/htdocs/web1"
# 域名
# ServerName dummy-host.example.com
# 给域名起别名,起到重定向作用
# ServerAlias www.dummy-host.example.com
# 错误日子
# ErrorLog "logs/dummy-host.example.com-error_log"
# 访问日志
# CustomLog "logs/dummy-host.example.com-access_log" common
</VirtualHost>

<VirtualHost 192.168.88.100:80>
DocumentRoot "/usr/local/apache/htdocs/web2"
</VirtualHost>
  1. 创建站点目录和文件
1
2
3
4
mkdir /usr/local/apache/htdocs/web{1..2}
echo 'this is web1' > /usr/local/apache/htdocs/web1/index.html
echo 'this is web2' > /usr/local/apache/htdocs/web2/index.html
./apache start
  1. 测试

基于IP虚拟主机测试

1.4.2 基于端口的虚拟主机

  1. 修改虚拟主机文件
1
2
3
4
5
6
7
8
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web1"
</VirtualHost>

Listen 81
<VirtualHost *:81>
DocumentRoot "/usr/local/apache/htdocs/web2"
</VirtualHost>
  1. 测试

基于端口虚拟主机测试

1.4.3 基于域名的虚拟主机

  1. 修改虚拟主机文件
1
2
3
4
5
6
7
8
9
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web1"
ServerName www.cqm1.com
</VirtualHost>

<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web2"
ServerName www.cqm2.com
</VirtualHost>
  1. 测试

基于域名虚拟主机测试

1.5 LAMP

LAMP:Linux + Apache + Mysql + PHP

作用就是构建一个PHP业务环境,用来发布PHP网站。

1.5.1 Mysql通过脚本源码安装

  1. 编写安装脚本
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
#!/bin/bash

mysql_version=5.7.35
install_dir=/opt
data_dir=/data
wget_url="https://mirrors.tuna.tsinghua.edu.cn/mysql/downloads/MySQL-5.7/mysql-${mysql_version}-linux-glibc2.12-x86_64.tar.gz"

function loginfo(){
if [[ $? -eq 0 ]];then
echo -e "\033[32m[INFO][$(date +"%F %T")] $1 succeed! \033[0m"
else
echo -e "\033[31m[ERROR][$(date +"%F %T")] $1 failed! \033[0m"
fi
}

function mysql_install(){
echo -e "\033[32mBegin install mysql V${mysql_version} ...\033[0m"

# 安装依赖
sudo yum -y install libaio >/dev/null 2>&1
loginfo "libaio install"

# 下载mysql
echo -e "\033[32mBegin download mysql V${mysql_version} ...\033[0m"
curl -O $wget_url >/dev/null 2>&1
mv ./mysql-${mysql_version}-linux-glibc2.12-x86_64.tar.gz $install_dir
loginfo "mysql software download"

# 解压缩mysql
sudo tar -xf $install_dir/mysql-${mysql_version}-linux-glibc2.12-x86_64.tar.gz -C $install_dir
loginfo "mysql software decompression"

# 创建配置文件目录和数据目录
if [[ -d $install_dir/mysql ]];then
rm -rf $install_dir/mysql
fi
sudo ln -s $install_dir/mysql-${mysql_version}-linux-glibc2.12-x86_64 $install_dir/mysql
loginfo "create mysql config dir soft link"
if [[ -d $data_dir/mysql ]];then
rm -rf $data_dir/mysql
fi
sudo mkdir -p $data_dir/mysql
loginfo "create mysql data dir"

# 修改启动脚本
sudo sed -i "46s#basedir=#basedir=${install_dir}/mysql#" ${install_dir}/mysql/support-files/mysql.server
sudo sed -i "47s#datadir=#datadir=${data_dir}/mysql#" ${install_dir}/mysql/support-files/mysql.server
sudo cp ${install_dir}/mysql/support-files/mysql.server /etc/init.d/mysqld
sudo chmod 755 /etc/init.d/mysqld

# 创建用户组及用户
if ! grep -q '^mysql:' /etc/group
then
sudo groupadd mysql
loginfo "create user mysql"
fi
if ! grep -q '^mysql:' /etc/passwd
then
sudo useradd -r -g mysql -s /bin/false mysql
loginfo "create group mysql"
fi

# 授权
sudo chown -R mysql:mysql $install_dir/mysql
sudo chown -R mysql:mysql $data_dir/mysql

# 为二进制文件创建软连接
if [ ! -f /usr/bin/mysql ]
then
sudo ln -s /opt/mysql/bin/mysql /usr/bin/
fi

# 创建配置文件
if [ -f /etc/my.cnf ]
then
sudo rm -f /etc/my.cnf
fi
sudo bash -c "cat >> /etc/my.cnf" <<EOF
[mysqld]
datadir = /data/mysql
basedir = /opt/mysql
#tmpdir = /data/mysql/tmp_mysql
port = 3306
socket = /data/mysql/mysql.sock
pid-file = /data/mysql/mysql.pid
max_connections = 8000
max_connect_errors = 100000
max_user_connections = 3000
check_proxy_users = on
mysql_native_password_proxy_users = on
local_infile = OFF
symbolic-links = FALSE
group_concat_max_len = 4294967295
max_join_size = 18446744073709551615
max_execution_time = 20000
lock_wait_timeout = 60
autocommit = 1
lower_case_table_names = 1
thread_cache_size = 64
disabled_storage_engines = "MyISAM,FEDERATED"
character_set_server = utf8mb4
character-set-client-handshake = FALSE
collation_server = utf8mb4_general_ci
init_connect = 'SET NAMES utf8mb4'
transaction-isolation = "READ-COMMITTED"
skip_name_resolve = ON
explicit_defaults_for_timestamp = ON
log_timestamps = SYSTEM
local_infile = OFF
event_scheduler = OFF
query_cache_type = OFF
query_cache_size = 0
sql_mode = NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO
log_error = /data/mysql/mysql.err
slow_query_log = ON
slow_query_log_file = /data/mysql/slow.log
long_query_time = 1
general_log = OFF
general_log_file = /data/mysql/general.log
expire_logs_days = 99
log-bin = /data/mysql/mysql-bin
log-bin-index = /data/mysql/mysql-bin.index
max_binlog_size = 500M
binlog_format = mixed
binlog_rows_query_log_events = ON
binlog_cache_size = 128k
binlog_stmt_cache_size = 128k
log-bin-trust-function-creators = 1
max_binlog_cache_size = 2G
max_binlog_stmt_cache_size = 2G
relay_log = /data/mysql/relay
relay_log_index = /data/mysql/relay.index
max_relay_log_size = 500M
relay_log_purge = ON
relay_log_recovery = ON
server_id = 1
read_buffer_size = 1M
read_rnd_buffer_size = 2M
sort_buffer_size = 64M
join_buffer_size = 64M
tmp_table_size = 64M
max_allowed_packet = 128M
max_heap_table_size = 64M
connect_timeout = 43200
wait_timeout = 43200
back_log = 512
interactive_timeout = 300
net_read_timeout = 30
net_write_timeout = 30
skip_external_locking = ON
key_buffer_size = 16M
bulk_insert_buffer_size = 16M
concurrent_insert = ALWAYS
open_files_limit = 65000
table_open_cache = 16000
table_definition_cache = 16000
default_storage_engine = InnoDB
default_tmp_storage_engine = InnoDB
internal_tmp_disk_storage_engine = InnoDB

[client]
socket = /data/mysql/mysql.sock
default_character_set = utf8mb4

[mysql]
default_character_set = utf8mb4

[ndatad default]
TransactionDeadLockDetectionTimeOut = 20000
EOF
sudo chown -R mysql:mysql /etc/my.cnf
loginfo "configure my.cnf"

# 创建SSL证书
# sudo mkdir -p ${install_dir}/mysql/ca-pem/
# sudo ${install_dir}/mysql/bin/mysql_ssl_rsa_setup -d ${install_dir}/mysql/ca-pem/ --uid=mysql
# sudo chown -R mysql:mysql ${install_dir}/mysql/ca-pem/

# sudo bash -c "cat >> ${data_dir}/mysql/init_file.sql" <<EOF
# set global sql_safe_updates=0;
# set global sql_select_limit=50000;
# EOF
# sudo chown -R mysql:mysql ${data_dir}/mysql/init_file.sql
# sudo chown -R mysql:mysql /etc/init.d/mysqld

# 初始化
${install_dir}/mysql/bin/mysqld --initialize --user=mysql --basedir=${DEPLOY_PATH}/mysql --datadir=/data/mysql
loginfo "initialize mysql"

# 客户端环境变量
echo "export PATH=\$PATH:${install_dir}/mysql/bin" | sudo tee /etc/profile.d/mysql.sh
source /etc/profile.d/mysql.sh
loginfo "configure envirement"

# 获取初始密码
mysql_init_passwd=$(grep 'A temporary password is generated' ${data_dir}/mysql/mysql.err | awk '{print $NF}')

# 启动服务
chkconfig --add mysqld
sudo systemctl start mysqld
loginfo "start mysqld"

# 修改密码
mysql --connect-expired-password -uroot -p${mysql_init_passwd} -e 'alter user user() identified by "toortoor";' >/dev/null 2>&1
loginfo "edit mysql root password"
}

mysql_install

1.5.2 PHP通过脚本源码安装

  1. 编写安装脚本
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#!/bin/bash

cmake_version=3.22.0
libzip_version=1.8.0
php_version=7.4.16

#检查
function check()
{
#检查是否为root用户
if [ $USER != "root" ]
then
echo -e "\e[1;31m error:need to be root so that \e[0m"
exit 1
fi

#检查是否安装了wget
if [ `rpm -qa | grep wget | wc -l` -lt 1 ]
then
echo -e "\e[1;31m error:not found wget \e[0m"
exit 1
fi
}

#安装前准备
function pre()
{
#安装依赖包
if [ ! `yum -y install gcc-c++ libxml2 libxml2-devel openssl openssl-devel bzip2 bzip2-devel libcurl libcurl-devel libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel gmp gmp-devel libmcrypt libmcrypt-devel readline readline-devel libxslt libxslt-devel gd net-snmp-* sqlite-devel oniguruma-devel &> /dev/null` ]
then
echo -e "\e[1;31m error:yum install dependency package failed \e[0m"
exit 1
fi

#下载最新版cmake
cd /usr/local
if [ ! `wget https://github.com/Kitware/CMake/releases/download/v${cmake_version}/cmake-${cmake_version}.tar.gz &> /dev/null` ]
then
tar -xf cmake-${cmake_version}.tar.gz
if [ ! -d cmake-${cmake_version} ]
then
echo -e "\e[1;31m error:no found cmake-${cmake_version} \e[0m"
exit 1
else
cd cmake-${cmake_version}
fi
else
echo -e "\e[1;31m error:Failed to download cmake-${cmake_version} \e[0m"
exit 1
fi

#安装cmake
echo "cmake configure..."
./configure &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo "cmake make && make install..."
make && make install &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo -e "\e[1;32m cmake installed sucessfully \e[0m"
else
echo -e "\e[1;31m cmake installed failed \e[0m"
exit 1
fi
else
echo -e "\e[1;31m error:cmake configure failed \e[0m"
exit 1
fi

#下载libzip1.1以上版本
cd /usr/local
if [ ! `wget --no-check-certificate https://libzip.org/download/libzip-${libzip_version}.tar.gz &> /dev/null` ]
then
echo "tar libzip..."
tar -xf libzip-${libzip_version}.tar.gz
if [ ! -d libzip-${libzip_version} ]
then
echo -e "\e[1;31m error:not found libzip-${libzip_version} \e[0m"
exit 1
else
cd libzip-${libzip_version}
fi
else
echo -e "\e[1;31m error:Failed to download libzip-${libzip_version}.tar.gz \e[0m"
exit 1
fi

#安装libzip
mkdir build;cd build
echo "cmake libzip..."
cmake .. &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo "make && make install libzip..."
make && make install &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo -e "\e[1;32m libzip install sucessfully \e[0m"
echo -e '/usr/local/lib64\n/usr/local/lib\n/usr/lib\n/usr/lib64'>> /etc/ld.so.conf
ldconfig -v &> /dev/null
else
echo -e "\e[1;31m error:libzip install failed \e[0m"
exit 1
fi
else
echo -e "\e[1;31m error:lipzip cmake failed \e[0m"
exit 1
fi
}

function php_install()
{
#下载php
cd /usr/local
if [ ! `wget https://www.php.net/distributions/php-${php_version}.tar.bz2 &> /dev/null` ]
then
echo "tar php..."
tar -xf php-${php_version}.tar.bz2
if [ ! -d php-${php_version} ]
then
echo -e "\e[1;31m error:not found php-${php_version} \e[0m"
exit 1
else
cd php-${php_version}
fi
else
echo -e "\e[1;31m error:Failed to download php-${php_version}.tar.bz2 \e[0m"
exit 1
fi

#安装php
echo "configure php..."
#要php以apache模块运行需加上--with-apxs2=/usr/localapache/bin/apxs参数
./configure --prefix=/usr/local/php --with-config-file-path=/usr/local/php/etc --with-mysqli=mysqlnd --enable-pdo --with-pdo-mysql=mysqlnd --with-iconv-dir=/usr/local/ --enable-fpm --with-fpm-user=www --with-fpm-group=www --with-pcre-regex --with-zlib --with-bz2 --enable-calendar --disable-phar --with-curl --enable-dba --with-libxml-dir --enable-ftp --with-gd --with-jpeg-dir --with-png-dir --with-zlib-dir --with-freetype-dir --enable-gd-jis-conv --with-mhash --enable-mbstring --enable-opcache=yes --enable-pcntl --enable-xml --disable-rpath --enable-shmop --enable-sockets --enable-zip --enable-bcmath --with-snmp --disable-ipv6 --with-gettext --disable-rpath --disable-debug --enable-embedded-mysqli --with-mysql-sock=/var/lib/mysql/ &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo "make && make install php..."
make && make install &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo -e "\e[1;32m php install sucessfully \e[0m"
else
echo -e "\e[1;31m php install failed \e[0m"
exit 1
fi
else
echo -e "\e[1;31m configure php failed \e[0m"
exit 1
fi
}

function php_set()
{
if [ ! -f /usr/local/php-${php_version}/sapi/fpm/php-fpm.service ]
then
echo -e "\e[1;31m No found php-fpm.service \e[0m"
exit 1
else
cp /usr/local/php-${php_version}/sapi/fpm/php-fpm.service /etc/systemd/system
if [ `echo $?` -ne 0 ]
then
echo -e "\e[1;31m Copy php-fpm.service failed \e[0m"
exit 1
else
sed -i '/PrivateTmp=true/a\ProtectSystem=false' /etc/systemd/system/php-fpm.service
systemctl daemon-reload
echo -e "\e[1;32m php set sucessfully \e[0m"
fi
fi
}

check
pre
php_install
php_set
  1. 配置PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
cd /usr/local/php/etc
cp php-fpm.conf.default php-fpm.conf
cp php-fpm.d/www.conf.default php-fpm.d/www.conf

egrep -v '^;|^$' php-fpm.conf
[global]
pid = run/php-fpm.pid
error_log = log/php-fpm.log
daemonize = yes
include=/usr/local/php/etc/php-fpm.d/*.conf

egrep -v '^;|^$' php-fpm.d/www.conf
[www]
user = www
group = www
listen = 127.0.0.1:9000
listen.owner = www
listen.group = www
listen.mode = 0660
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
  1. 启动
1
systemctl start php-fpm

1.5.3 PHP作为Apache模块运行

  1. 在apache主配置文件中调用子配置文件
1
2
vim /usr/local/apache/conf/httpd.conf
include conf/extra/php.conf
  1. 配置子配置文件
1
2
3
vim /usr/local/apache/conf/extra/php.con
LoadModule php7_module modules/libphp7.so
AddType application/x-httpd-php .php
  1. 配置虚拟主机
1
2
3
4
vim /usr/local/apache/conf/extra/httpd-vhosts.conf
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web"
</VirtualHost>
  1. 写web目录
1
2
3
4
5
echo 'this is cqm web' > /usr/local/apache/htdocs/web/index.html
vim /usr/local/apache/htdocs/web/phpinfo.php
<?php
phpinfo()
?>
  1. 测试

php以模块运行

1.5.4 PHP作为独立服务运行

PHP作为独立服务运行有两种模式:

  • TCP socket模式
  • UNIX socket模式

TCP socket模式

  1. 修改www.conf文件
1
2
vim /usr/local/php/etc/php-fpm.d/www.conf
listen = 127.0.0.1:9000
  1. 配置虚拟主机文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vim /usr/local/apache/conf/extra/httpd-vhosts.conf
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web"
</VirtualHost>

<Directory "/usr/local/apache/htdocs/web">
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>

<IfModule dir_module>
DirectoryIndex index.php index.html
</IfModule>

<FilesMatch \.php$>
SetHandler "proxy:fcgi://127.0.0.1:9000"
</FilesMatch>
  1. 在apache主配文件添加关联
1
2
vim /usr/local/apache/conf/httpd.conf
include conf/extra/php-fpm.conf
  1. 配置子配文件
1
2
3
4
vim /usr/local/apache/conf/php-fpm.conf
# 载入需要的模块
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so

UNIX socket模式

  1. 修改www.conf文件
1
2
vim /usr/local/php/etc/php-fpm.d/www.conf
listen = /usr/local/php/etc/php-fpm.socket
  1. 配置虚拟主机
1
2
3
<FilesMatch \.php$>
SetHandler "proxy:unix:/usr/local/php/etc/php-fpm.socket|fcgi://localhost/"
</FilesMatch>

1.6 Apache常用模块

1.6.1 长连接

HTTP采用TCP进行传输,是面向连接的协议,每完成一次请求就要经历以下过程:

  • 三次握手
  • 发起请求
  • 响应请求
  • 四次挥手

那么N个请求就要建立N次连接,如果希望用户能够更快的拿到数据,服务器的压力降到最低,那么靠长连接就可以解决。

长连接实际上就是优化了TCP连接。

Apache默认开启了长连接,持续时间为5秒,在httpd-default.conf中可以定义。

1
2
3
4
5
6
7
vim /usr/local/apache/conf/extra/httpd-default.conf
# 开启长连接
KeepAlive On
# 限制每个连接允许的请求数
MaxKeepAliveRequests 500
# 长连接时间
KeepAliveTimeout 5

1.6.2 静态缓存

用户每次访问网站都会将页面中的所有元素都请求一遍,全部下载后通过浏览器渲染,展示到浏览器中。但是,网站中的某些元素我们一般都是固定不变的,比如logo、框架文件等。用户每次访问都需要加载这些元素。这样做好处是保证了数据的新鲜,可是这些数据不是常变化的,很久才变化一次。每次都请求、下载浪费了用户时间和公司带宽。

所以我们通过静态缓存的方式,将这些不常变化的数据缓存到用户本地磁盘,用户以后再访问这些请求,直接从本地磁盘打开加载,这样的好处是加载速度快,且节约公司带宽及成本。

  1. 在apache主配文件中加载缓存模块
1
2
vim /usr/local/apache/conf/httpd.conf
LoadModule expires_module modules/mod_expires.so
  1. 修改虚拟主机文件调用模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
vim /usr/local/apache/conf/extra/httpd-vhosts.conf
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web"

<IfMoudle expires_module>
#开启缓存
ExpiresActive on
#针对不同类型元素设置缓存时间
ExpiresByType image/gif "access plus 1 days"
ExpiresByType image/jpeg "access plus 24 hours"
ExpiresByType image/png "access plus 24 hours"
#now 相当于 access
ExpiresByType text/css "now plus 2 hour"
ExpiresByType application/x-javascript "now plus 2 hours"
ExpiresByType application/x-shockwave-flash "now plus 2 hours”
#其他数据不缓存
ExpiresDefault "now plus 0 min"
</IfModule>

</VirtualHost>

1.6.3 数据压缩

数据从服务器传输到客户端,需要传输时间,文件越大传输时间就越长,为了减少传输时间,我们一般把数据压缩后在传给客户端。

apache支持两种模式的压缩:

  • default
  • gzip

两者的区别:

  • mod_deflate 压缩速度快。
  • mod_gzip 的压缩比略高。
  • 一般情况下,mod_gzip 会比 mod_deflate 多出 4%~6% 的压缩量。
  • mod_gzip 对服务器CPU的占用要高一些,所以 mod_deflate 是专门为确保服务器的性能而使用的一个压缩模块,只需较少的资源来进行压缩。
  1. 在apache主配文件中加载压缩模块
1
2
vim /usr/local/apache/conf/httpd.conf
LoadModule deflate_module modules/mod_deflate.so
  1. 修改虚拟主机文件调用模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vim /usr/local/apache/conf/extra/httpd-vhosts.conf
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web"

<IfMoudle deflate_module>
#压缩等级1-9,数字越大压缩能力越好,相应地也越耗CPU性能
DeflateCompressionLevel 4
#压缩类型,html、xml、php、css、js
AddOutputFilterByType DEFLATE text/html text/plain text/xml application/x-javascript application/x-httpd-php
AddOutputFilter DEFLATE js css
#浏览器匹配为IE1-6的不压缩
BrowserMatch \bMSIE\s[1-6] dont-vary
#设置不压缩的文件
SetEnvIfNoCase Request_URI .(?:gif|jpe?g|png)$ no-gzip dont-vary
SetEnvIfNoCase Request_URI .(?:exe|t?gz|zip|bz2|sit|rar)$ no-gzip dont-vary
SetEnvIfNoCase Request_URI .(?:pdf|doc)$ no-gzip dont-vary
</IfModule>

</VirtualHost>

1.6.4 限速

网站除了能共享页面给用户外,还能作为下载服务器存在。但是作为下载服务器时,我们应该考虑服务器的带宽和IO的性能,防止部分邪恶分子会通过大量下载的方式来攻击你的带宽和服务器IO性能。

问题:

  • 假如你的服务器被邪恶分子通过下载的方式把带宽占满了,那么你或其他用户在访问的时候就会造成访问慢或者根本无法访问。
  • 假如你的服务器被邪恶分子通过下载的方式把服务器IO占满了,那么你的服务器将会无法处理用户请求或宕机。

以上问题可以通过限速来解决,apache自带了基于宽带限速的模块:

  • ratelimit_module:只能对连接下载速度做限制,且是单线程的下载,迅雷等下载工具使用的是多线程下载。
  • mod_limitipconn:限制每 IP 的连接数,需要额外安装该模块。

ratelimit_module模块

  1. 在apache主配文件中加载压缩模块
1
2
vim /usr/local/apache/conf/httpd.conf
LoadModule ratelimit_module modules/mod_ratelimit.so
  1. 修改虚拟主机文件调用模块
1
2
3
4
5
6
7
8
9
10
11
12
vim /usr/local/apache/conf/extra/httpd-vhosts.conf
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web"
</VirtualHost>

# Location相对路径:/usr/local/apache/htdocs/...
# Directory绝对路径:...
<Location /download>
SetOutputFiler RATE_LIMIT
#限速100k
SetEnv rate-limit 100
</Location>

mod_limitipconn模块

  1. 下载安装模块
1
2
3
4
5
6
wget http://dominia.org/djao/limit/mod_limitipconn-0.24.tar.bz2
tar -xf mod_limitipconn-0.24.tar.bz2
cd mod_limitipconn-0.24
vim Makefile
apxs = "/usr/local/apache/bin/apxs"
make && make install
  1. 在apache主配文件启用模块
1
2
vim /usr/local/apache/conf/httpd.conf
LoadModule limitipconn_module modules/mod_limitipconn.so
  1. 修改虚拟主机文件调用模块
1
2
3
4
5
6
7
8
9
<Location /download>
SetOutputFiler RATE_LIMIT
#限速100k
SetEnv rate-limit 100
#限制线程数
MaxConnPerIP 3
#对index.html文件不作限制
NoIPLimit index.html
</Location>

1.6.5 访问控制

在生产环境中,网站分为公站和私站,公站允许所有人访问,但私站就只允许内部人员访问,Require就可以实现访问控制的功能。

容器:

  • RequireAny:一个符合即可通过
  • RequireAll:所有符合才可通过
  • Requirenone:所有都不符合才可通过

普通的访问控制

  1. 修改虚拟主机文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
vim /usr/local/apache/conf/extra/httpd-vhosts.conf
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web"
</VirtualHost>

<Directory "/usr/local/apache/htdocs/web/test">
AllowOverride None
# 拒绝所有人访问
Require all denied
# 允许该地址段的用户访问
Require ip 192.168.88
# 允许该主机访问
Require host www.cqm.com
</Directory>

用户登录验证访问控制

  1. 修改虚拟主机文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
vim /usr/local/apache/conf/extra/httpd-vhosts.conf
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web"
</VirtualHost>

<Directory "/usr/local/apache/htdocs/web/test">
# 定义提示信息,用户访问时提示信息会出现在认证的对话框中
AuthName "Private"
# 定义认证类型,在HTTP1.0中,只有一种认证类型:basic。在HTTP1.1中有几种认证类型,如:MD5
AuthType Basic
# 定义包含用户名和密码的文本文件,每行一对
AuthUserFile "/usr/local/apache/user.dbm"
# 配合容器使用,只有条件全部符合才能通过
<RequireAll>
Require not ip 192.168.88
# require user user1 user2 (只有用户user1和user2可以访问)
# requires groups group1 (只有group1中的成员可以访问)
# require valid-user (在AuthUserFile指定的文件中的所有用户都可以访问)
Require valid-user
</RequireAll>
</Directory>
  1. 生成用户文件
1
2
3
# 生成cqm用户
/usr/local/apache/bin/htpasswd -cm /usr/local/apache/user.dbm cqm
...

1.6.6 URL重写

Apache通过mod_rewrite模块可以实现URL重写的功能,URL重写其实就是改写用户浏览器中的URL地址。

  1. 在主配文件开启模块
1
2
vim /usr/local/apache/conf/httpd.conf
LoadModule rewrite_module modules/mod_rewrite.so
  1. 修改虚拟主机文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
vim /usr/local/apache/conf/extra/httpd-vhosts.conf
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web"
# 开启URL重写功能
RewriteEngine on
# 重写规则,跳转到百度
RewriteRule "^/$" "http://www.baidu.com" [NC,L]
# 匹配条件,根据请求头进行匹配
RewriteCond "%{HTTP_USER_AGENT}" "chrome" [NC,OR]
RewriteCond "%{HTTP_USER_AGENT}" "curl"
# 重写规则,和匹配到的条件配合使用,请求头匹配到chrome或curl则返回403状态码
RewriteRule "^/$" - [F]
</VirtualHost>

RewreteRule [flag] 部分标记规则
R:强制外部重定向
F:禁用URL,返回403HTTP状态码
G:强制URL为GONE,返回410HTTP状态码
P:强制使用代理转发
L:表明当前规则是最后一条规则,停止分析以后规则的重写
N:重新从第一条规则开始运行重写过程
C:与下一条规则关联
NS:只用于不是内部子请求
NC:不区分大小写

通过URL重写实现分流功能

1
2
3
4
5
6
7
8
9
vim /usr/local/apache/conf/extra/httpd-vhosts.conf
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web"
RewriteEngine on
RewriteCond "%{HTTP_USER_AGENT}" "(chrome|curl)" [NC,OR]
RewriteRule "^/$" "http://pc.cqm.com" [NC]
RewriteCond "%{HTTP_USER_AGENT}" "(iPhone|Blackberry|Android|ipad)" [NC]
RewriteRule "^/$" "http://phone.cqm.com" [NC]
</VirtualHost>

1.6.7 压力测试

Apache压力测试使用ab命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ab
-A:指定连接服务器的基本的认证凭据
-c:指定一次向服务器发出请求数
-C:添加cookie
-g:将测试结果输出为“gnuolot”文件
-h:显示帮助信息
-H:为请求追加一个额外的头
-i:使用“head”请求方式
-k:激活HTTP中的“keepAlive”特性
-n:指定测试会话使用的请求数
-p:指定包含数据的文件
-q:不显示进度百分比
-T:使用POST数据时,设置内容类型头
-v:设置详细模式等级
-w:以HTML表格方式打印结果
-x:以表格方式输出时,设置表格的属性
-X:使用指定的代理服务器发送请求
-y:以表格方式输出时,设置表格属性
1
2
3
/usr/local/apache/bin/ab -n 10000 -c 200 http:...
# 并发数
per second...

二、Nginx

2.1 Nginx介绍

Nginx是一款是由俄罗斯的程序设计师Igor Sysoev所开发高性能的 Web和 反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器。和apache一样,都是web服务器软件,因为其性能优异,所以被广大运维喜欢。又因为nginx是一个轻量级的web服务器,相比apache来说资源消耗更低。

Nginx中文文档:https://www.nginx.cn/doc/index.html

2.2 通过脚本源码安装Nginx

  1. 编写安装脚本
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
vim nginx-install.sh
#!/bin/bash

nginx_version=1.21.3

#检测
function check()
{
#检测是否为root
if [ $USER != "root" ]
then
echo -e "\e[1;31m error:need to be root so that \e[0m"
exit 1
fi

#检测wget是否安装
if [ ! -e /usr/bin/wget ]
then
echo -e "\e[1;31m error:not found command /usr/bin/wget \e[0m"
exit 1
fi
}

#安装前准备
function install_pre()
{
# 安装依赖
#0:stdin标准输入 1:stdout标准输出 2:stderr错误输出
if [ ! `yum -y install gcc-* pcre-devel zlib-devel &> /dev/null` ]
then
echo -e "\e[1;31m error:yum install dependency package failed \e[0m"
exit 1
fi

#下载源码包
cd /usr/local/
if [ ! `wget http://nginx.org/download/nginx-${nginx_version}.tar.gz &> /dev/null` ]
then
tar -xf nginx-${nginx_version}.tar.gz
if [ ! -d nginx-${nginx_version} ]
then
echo -e "\e[1;31m error:not found nginx-${nginx_version} \e[0m"
exit 1
fi
else
echo -e "\e[1;31m error:wget file nginx-${nginx_version}.tar.gz failed \e[0m"
exit 1
fi
}

#安装
function install_nginx()
{
cd /usr/local/nginx-${nginx_version}
echo "nginx configure..."
./configure --prefix=/usr/local/nginx &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo "nginx make..."
make && make install &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo -e "\e[1;32m nginx install success \e[0m"
else
echo -e "\e[1;31m error:nginx install fail \e[0m"
exit 1
fi
else
echo -e "\e[1;31m error:nginx configure fail \e[0m"
exit 1
fi
}

check
install_pre
install_nginx
  1. 编写启动脚本
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
85
86
87
88
89
#!/bin/bash

#Source function libiary
if [ -f /etc/init.d/functions ]
then
. /etc/init.d/functions
else
echo "Not found file /etc/init.d/functions"
exit
fi

nginxd=/usr/local/nginx/sbin/nginx
nginx_pid=/usr/local/nginx/logs/nginx.pid

function nginx_start()
{
nginx_num=`ps -ef | grep nginx | wc -l`
if [ $nginx_num -gt 1 ] && [ -f $nginx_pid ]
then
echo -e "nginx [\e[1;32m running \e[0m]"
exit 1
elif [ $nginx_num -eq 1 ] && [ -f $nginx_pid ]
then
killall nginx
fi
$nginxd
}

function nginx_stop()
{
nginx_num=`ps -ef | grep nginx | wc -l`
if [ $nginx_num -eq 1 ]
then
echo -e "nginx [\e[1;31m stopping \e[0m]"
exit 1
elif [ $nginx_num -gt 1 ]
then
killall nginx
fi
}

function nginx_status()
{
nginx_num=`ps -ef | grep nginx | wc -l`
if [ $nginx_num -gt 1 ] && [ -f $nginx_pid ]
then
echo -e "nginx [\e[1;32m running \e[0m]"
else
echo -e "nginx [\e[1;31m stopping \e[0m]"
fi
}

function nginx_restart()
{
nginx_stop
nginx_start
}

function nginx_reload()
{
nginx_num=`ps -ef | grep nginx | wc -l`
if [ $nginx_num -gt 1 ] && [ -f $nginx_pid ]
then
$nginxd -s reload
else
echo -e "nginx [\e[1;31m stopping \e[0m]"
fi
}

case $1 in
start)
nginx_start
echo -e "nginx start [\e[1;32m OK \e[0m]"
;;
stop)
nginx_stop
echo -e "nginx stop [\e[1;32m OK \e[0m]"
;;
status)
nginx_status
;;
restart)
nginx_restart
echo -e "nginx restart [\e[1;32m OK \e[0m]"
;;
reload)
nginx_reload
echo -e "nginx reload [\e[1;32m OK \e[0m]"
esac

2.3 Nginx的Server块

当Nginx配置文件只有一个Server块时,那么该Server块就被Nginx认为是默认网站,所有发给Nginx的请求都会传给该Server块。

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
server {
# 监听80端口
listen 80;
# 域名
server_name localhost;
# 字符集
charset koi8-r;
# 访问日志路径
access_log logs/host.access.log main;
# web根路径
# /代表相对路劲,这里代表/usr/local/nginx
location / {
# 根目录路径,这里代表/usr/local/nginx/html
root html;
# 索引页
index index.html index.htm;
}
# 404状态码
error_page 404 /404.html;
location = /404.html{
root html;
}
# 50x状态码
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

2.4 Nginx的访问控制

  1. 编写主配文件
1
2
3
4
5
6
7
8
9
10
11
12
13
location / {
root html;
index index.html index.htm;
# 允许192.168.88.0/24的用户访问
allow 192.168.88.0/24;
# 拒绝所有
deny all;
# 基于客户端IP做过滤,符合条件的允许访问,不符合的返回404
# 这里为不是192.168.88的就返回404
if ( $remote_addr !~ "192.168.88" ){
return 404;
}
}

2.5 Nginx的用户验证

  1. 编写主配文件
1
2
3
4
5
6
7
8
location / {
root html;
index index.html index.htm;
# 欢迎词
auth_basic "welcome to cqm's web";
# 存放用户文件
auth_basic_user_file /usr/local/nginx/htpasswd;
}
  1. 生成用户文件
1
/usr/local/apache/bin/htpasswd -cm /usr/local/nginx/htpasswd cqm

2.6 Nginx参数

1
2
3
4
5
6
7
8
9
10
11
# nginx中的log_format可以用来自定义日志格式
# log_format变量:
$remote_addr:记录访问网站的客户端地址
$remote_user:远程客户端用户名
$time_local:记录访问时间与时区
$request:用户的http请求起始行信息
$status:http状态码,记录请求返回的状态码,例如:200、301、404等
$body_bytes_sent:服务器发送给客户端的响应body字节数
$http_referer:记录此次请求是从哪个连接访问过来的,可以根据该参数进行防盗链设置。
$http_user_agent:记录客户端访问信息,例如:浏览器、手机客户端等
$http_x_forwarded_for:当前端有代理服务器时,设置web节点记录客户端地址的配置,此参数生效的前提是代理服务器也要进行相关的x_forwarded_for设置

2.7 Nginx防盗链

盗链用大白话讲就是抓取别人网站的资源,加以利用,以至于被抓取资源的网站消耗了带宽,而收益的是抓取资源的人。

而反盗链就可以防止别人抓取自身网站的资源。

  1. 编写主配文件
1
2
3
4
5
6
7
location / {
# 除了www.cqm.com之外,都返回403
valid_referers none blocked www.cqm.com;
if ($invalid_referer){
return 403;
}
}

2.8 Nginx虚拟主机

Nginx的虚拟主机是通过server块来实现的。

2.8.1 基于IP的虚拟主机

  1. 修改主配文件
1
2
vim /usr/local/nginx/conf/nginx.conf
include /usr/local/nginx/conf/conf.d/nginx_vhosts.conf;
  1. 修改虚拟主机文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vim /usr/local/nginx/conf/conf.d/nginx_vhosts.conf
server {
listen 192.168.88.100;
location / {
root html/web1;
index index.html index.htm index.php;
}
}

server {
listen 192.168.88.101;
location / {
root html/web2;
index index.html index.htm index.php;
}
}
  1. 其它配置
1
2
3
4
5
# 添加一个逻辑网卡,重启即失效
ifconfig eth0:1 192.168.88.100/24 up
mkdir /usr/local/nginx/html/web{1..2}
echo 'this is web1' > /usr/local/nginx/html/web1/index.html
echo 'this is web2' > /usr/local/nginx/html/web2/index.html
  1. 测试
1
2
3
4
curl http://192.168.88.100/
this is web1
curl http://192.168.88.101/
this is web2

2.8.2 基于端口的虚拟主机

  1. 修改虚拟主机文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vim /usr/local/nginx/conf/conf.d/nginx_vhosts.conf
server {
listen 80;
location / {
root html/web1;
index index.html index.htm index.php;
}
}

server {
listen 81;
location / {
root html/web2;
index index.html index.htm index.php;
}
}

2.8.3 基于域名的虚拟主机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vim /usr/local/nginx/conf/conf.d/nginx_vhosts.conf
server {
listen 80;
server_name www.cqm1.com;
location / {
root html/web1;
index index.html index.htm index.php;
}
}

server {
listen 80;
server_name www.cqm2.com;
location / {
root html/web2;
index index.html index.htm index.php;
}
}

2.9 Nginx反向代理

代理最常见的使用方式就是翻墙,能够实现让国内的用户访问国外的网站。

原理:

  • 用户讲请求发给代理服务器
  • 代理服务器代替用户去获取数据
  • 代理服务器将数据发送给用户

正常没有代理的上网

正常上网流程

使用代理服务器的上网

=使用代理服务器上网流程

代理服务器又分为两种:正向代理、反向代理

正向代理:代理用户向服务器获取资源

反向代理:代理服务器去管理网络资源,用户有请求找反向代理就可以了

  1. 编写反向代理服务器主配文件
1
2
3
4
5
6
vim /usr/local/nginx/conf/nginx.conf
location / {
index index.html index.htm index.php;
# 访问代理服务器就会跳转到http://192.168.88.100
proxy_pass http://192.168.88.100;
}
  1. 反向代理其它配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
proxy_set_header Host $host;

proxy_set_header X-Real-IP $remote_addr;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

client_max_body_size 10m; #允许客户端请求的最大单文件字节数

client_body_buffer_size 128k; #缓冲区代理缓冲用户端请求的最大字节数,

proxy_connect_timeout 90; #nginx跟后端服务器连接超时时间(代理连接超时)

proxy_send_timeout 90; #后端服务器数据回传时间(代理发送超时)

proxy_read_timeout 90; #连接成功后,后端服务器响应时间(代理接收超时)

proxy_buffer_size 4k; #设置代理服务器(nginx)保存用户头信息的缓冲区大小

proxy_buffers 4 32k; #proxy_buffers缓冲区,网页平均在32k以下的话,这样设置

proxy_busy_buffers_size 64k; #高负荷下缓冲大小(proxy_buffers*2)

proxy_temp_file_write_size 64k; #设定缓存文件夹大小,大于这个值,将从upstream服务器传

2.10 Nginx下载限速

限速方法主要分为:

  • 下载速度限制
  • 单位时间内请求数限制
  • 基于客户端的并发数限制

Nginx官方提供的限制IP连接和并发的模块有两个:

limit_req_zone:用来限制单位时间内的请求数,即速率限制,采用的漏桶算法

limit_req_conn:来限制同一时间连接数,即并发限制

单位时间内请求数限制

  1. 修改主配文件
1
2
3
4
5
6
7
8
9
10
11
12
13
# 在http快下调用模块
# $binary_remote_addr:基于ip地址做限制
# zone:创建缓存域和缓存大小
# rate:设置访问频数
limit_req_zone $binary_remote_addr zone=cqm:10m rate=1r/s;

# server块
location /test {
...
# 调用模块
# 当请求数超过5次时,就拒绝访问,并返回503状态码
limit_req zone=cqm burst=5 nodelay;
}

限制并发连接数

  1. 修改主配文件
1
2
3
4
5
6
7
8
9
10
11
# 在http快下调用模块
# $binary_remote_addr:基于ip地址做限制
# zone:创建缓存域和缓存大小
limit_req_conn $binary_remote_addr zone=cqm:10m;

# server块
location /test {
...
# 限制同一时间内下载数为1个
limit_conn cqm 1;
}

限制下载速度

  1. 修改主配文件
1
2
3
4
5
location /test {
...
# 限制下载速度为1k
limit_rate 1k;
}

2.11 Nginx的URL重写

rewrite的主要功能是实现URL地址的重定向。Nginx的rewrite功能需要PCRE软件的支持,即通过perl兼容正则表达式语句进行规则匹配的。默认参数编译nginx就会支持rewrite的模块,但是也必须要PCRE的支持。

URL模板语块:

  • set:设置变量
  • if:判断
  • return:返回值或URL
  • break:终止
  • rewrite:重定向URL

例一:根据不同域名跳转到主域名的不同目录下

  1. 创建测试目录
1
2
3
4
mkdir /usr/local/nginx/html/{cn,jp,us}
echo 'this is China' > /usr/local/nginx/html/cn/index.html
echo 'this is Japan' > /usr/local/nginx/html/jp/index.html
echo 'this is America' > /usr/local/nginx/html/us/index.html
  1. 修改hosts文件,以便解析
1
2
3
4
5
vim /etc/hosts
192.168.88.100 www.cqm.com
192.168.88.100 www.cqm.com.cn
192.168.88.100 www.cqm.com.jp
192.168.88.100 www.cqm.com.us
  1. 修改主配文件
1
include /usr/local/nginx/conf/conf.d/rewrite.conf
  1. 修改重定向文件
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
vim /usr/local/nginx/conf/conf.d/rewrite.conf
# 设置重定向server
server {
listen 80;
server_name www.cqm.com.cn www.cqm.com.jp www.cqm.com.us;
location / {
# 模糊匹配到cn的话,就跳转到http://www.cqm.com/cn下
if ($http_host ~ (cn)$){
set $nation cn;
rewrite ^/$ http://www.cqm.com/$nation;
}
if ($http_host ~ (jp)$){
set $nation jp;
rewrite ^/$ http://www.cqm.com/$nation;
}
if ($http_host ~ (us)$){
set $nation us;
rewrite ^/$ http://www.cqm.com/$nation;
}
}
}
server {
listen 80;
server_name www.cqm.com;
location / {
root html;
index index.html;
}
}
  1. 测试
1
2
3
4
5
6
7
curl -L http://www.cqm.com.cn
this is China
curl -L http://www.cqm.com.jp
this is Japan
curl -L http://www.cqm.com.us
this is America
-L:自动获取重定向

例二:retuen以及break的简单实用

  1. 修改重定向文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vim /usr/local/nginx/conf/conf.d/rewrite.conf
server {
listen 80;
server_name www.cqm.com;
location / {
root html;
index index.html;
# 模糊匹配 ~
# 精确匹配 =
# 不匹配 !~
# 如果匹配请求头不是chrome的话,就返回403
if ($http_user_agent !~ 'chrome'){
return 403;
# break放在return上面的话就不会执行return操作
# break;
# return http://www.baidu.com;
}
}
}

flag

flag是放在rewrite重定向的URL后边的,格式为:rewrite URL flag

flag的选项有:

  1. last:本条规则匹配完成后继续执行到最后。
  2. break:本条规则匹配完成即终止。
  3. redirect:返回302临时重定向。
  4. permanent:返回301永久重定向。

redirect和permanent的区别:设置permanent的话,新网址就会完全继承旧网址,旧网址的排名等完全清零,如果不是暂时迁移的情况下都建议使用permanent;设置redirect的话,新网址对旧网址没有影响,且新网址也不会有排名。

2.12 Nginx优化

2.12.1 大并发

Nginx的工作模式:主进程 + 工作进程

假如Nginx服务器有4个CPU

  1. 设置主配文件来实现高并发
1
2
3
4
5
6
7
8
vim /usr/local/nginx/conf/nginx.conf
worker_processes 4;
# 指定运行的核的编号,采用掩码的方式设置编号
worker_cpu_affinity 0001 0010 0100 1000;
events {
# 单个工作进程维护的请求队列长度,根据实际情况调整
worker_connections 1024;
}

2.12.2 长连接

  1. 修改主配文件
1
2
3
4
5
6
7
vim /usr/local/nginx/conf/nginx.conf
# keepalive_timeout用来设置长连接,0代表关闭
keepalive_timeout 0;
# 设置长连接时间100s
#keepalive_timeout 100;
# 设置每秒可以接受的请求数
#keepalive_requests 8192;

2.12.3 压缩

Nginx是采用gzip进行压缩。

  1. 修改主配文件
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
vim /usr/local/nginx/conf/nginx.conf
# 开启缓存
gzip on;

# Nginx做为反向代理的时候启用
# off:关闭所有的代理结果数据压缩
# expired:如果header中包含”Expires”头信息,启用压缩
# no-cache:如果header中包含”Cache-Control:no-cache”头信息,启用压缩
# no-store:如果header中包含”Cache-Control:no-store”头信息,启用压缩
# private:如果header中包含”Cache-Control:private”头信息,启用压缩
# no_last_modified:启用压缩,如果header中包含”Last_Modified”头信息,启用压缩
# no_etag:启用压缩,如果header中包含“ETag”头信息,启用压缩
# auth:启用压缩,如果header中包含“Authorization”头信息,启用压缩
# any:无条件压缩所有结果数据
gzip_proxied any;

# 启用gzip压缩的最小文件,小于设置值的文件将不会压缩
gzip_min_length 1k;

# 设置压缩所需要的缓冲区大小
# 32 4K表示按照内存页(one memory page)大小以4K为单位(即一个系统中内存页为4K),申请32倍的内存空间
# 建议此项不设置,使用默认值
gzip_buffers 32 4k;

# 设置gzip压缩级别,级别越底压缩速度越快文件压缩比越小,反之速度越慢文件压缩比越大
gzip_comp_level 1;

# 用于识别http协议的版本,早期的浏览器不支持gzip压缩,用户会看到乱码,所以为了支持前期版本加了此选项。默认在http/1.0的协议下不开启gzip压缩
gzip_http_version 1.1;

# 设置需要压缩的MIME类型,如果不在设置类型范围内的请求不进行压缩
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png application/vnd.ms-fontobject font/ttf font/opentype font/x-woff image/svg+xml;

2.12.4 静态缓存

将部分数据缓存在用户本地磁盘,用户加载时,如果本地和服务器的数据一致,则从本地加载。提升用户访问速度,提升体验度。节省公司带宽成本。

  1. 修改主配文件
1
2
3
4
5
# 模糊匹配以png或gif结尾的文件
location ~* \.(png|gif)$ {
# 缓存时间为1小时
expires 1h;
}

三、Tomcat

3.1 Tomcat介绍

Tomcat 服务器是一个免费的开放源代码的 Web 应用服务器,属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试 JSP 程序的首选。

实际上 Tomcat 是 Apache 服务器的扩展,但运行时它是独立运行的,所以当你运行 tomcat 时,它实际上作为一个与 Apache 独立的进程单独运行的。

Tomcat 官方文档:https://tomcat.apache.org/

3.2 Tomcat安装

  1. 安装jdk和tomcat
1
2
3
4
5
6
yum -y install java-1.8.0-openjdk*
wget https://downloads.apache.org/tomcat/tomcat-9/v9.0.43/bin/apache-tomcat-9.0.43.tar.gz
tar -xf apache-tomcat-9.0.43.tar.gz
mkdir /root/tomcat
mv apache-tomcat-9.0.43.tar.gz/* /root/tomcat
./root/tomcat/bin/startup.sh