通过mariadb二进制日志实现数据库增量备份

何为增量备份,简单理解就是使用日志记录每天数据库的操作情况,只需要每天把这个日志里的数据库操作还原到数据库中,从而避免每天都进行完全备份,这种情况下,每周进行一次完全备份即可
首先我们需要配置以下mariadb的配置文件,我使用的是yum安装,其配置文件位于/etc/my.cnf,内容如下

[mysqld]
log-bin=mysql-bin                   #只需要增加这行就可以了
#binlog_format=row
#skip-grant
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0
# Settings user and group are ignored when systemd is used.
# If you need to run mysqld under a different user or group,
# customize your systemd unit file for mariadb according to the
# instructions in http://fedoraproject.org/wiki/Systemd

[mysqld_safe]
log-error=/var/log/mariadb/mariadb.log
pid-file=/var/run/mariadb/mariadb.pid

#
# include all files from the config directory
#
!includedir /etc/my.cnf.d

进入mariadb进行操作

[root@localhost mysql]# mysql -uroot -p
Enter password: 
Welcome to the MariaDB monitor.  Commands end with ; or g.
Your MariaDB connection id is 4
Server version: 5.5.52-MariaDB MariaDB Server

Copyright (c) 2000, 2016, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or 'h' for help. Type 'c' to clear the current input statement.

MariaDB [(none)]> use bp
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
MariaDB [bp]> show tables;
+--------------+
| Tables_in_bp |
+--------------+
| mytest       |
| test         |
+--------------+
2 rows in set (0.00 sec)

MariaDB [bp]> create table bptest(id int ,name varchar(20));
Query OK, 0 rows affected (0.01 sec)

MariaDB [bp]> insert into bptest values(1,'a');
Query OK, 1 row affected (0.00 sec)

MariaDB [bp]> insert into bptest values(2,'b');
Query OK, 1 row affected (0.01 sec)

MariaDB [bp]> select * from bptest;
+------+------+
| id   | name |
+------+------+
|    1 | a    |
|    2 | b    |
+------+------+
2 rows in set (0.01 sec)

MariaDB [bp]> flush logs;                       #这里我还有点不明白,我是简单理解为日志的开始位置
Query OK, 0 rows affected (0.01 sec)

MariaDB [bp]> insert into bptest values(3,'c');
Query OK, 1 row affected (0.01 sec)

MariaDB [bp]> insert into bptest values(4,'d');
Query OK, 1 row affected (0.01 sec)

MariaDB [bp]> flush logs;                       #日志结束位置,该日志文件我们可以在/var/lib/mysql里面找到
Query OK, 0 rows affected (0.02 sec)

MariaDB [bp]> delete from bptest where id =3;
Query OK, 1 row affected (0.01 sec)

MariaDB [bp]> delete from bptest where id=1;
Query OK, 1 row affected (0.00 sec)

MariaDB [bp]> flush logs;
Query OK, 0 rows affected (0.02 sec)

MariaDB [bp]> truncate table bptest;#为了让效果更明显,我们直接清空表内容
Query OK, 0 rows affected (0.13 sec)

MariaDB [bp]> select * from bptest;
Empty set (0.00 sec)

我们可以进入/var/lib/mysql文件夹内查看,可以看到mysql-bin.000001,mysql-bin.000002文件
接下来我们来看一下日志文件内容

[root@localhost mysql]# mysqlbinlog mysql-bin.000001 
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=1*/;
/*!40019 SET @@session.max_insert_delayed_threads=0*/;
/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/;
DELIMITER /*!*/;
# at 4
#170725  2:04:19 server id 1  end_log_pos 245   Start: binlog v 4, server v 5.5.52-MariaDB created 170725  2:04:19
BINLOG '
kwl3WQ8BAAAA8QAAAPUAAAAAAAQANS41LjUyLU1hcmlhREIAbG9nAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAEzgNAAgAEgAEBAQEEgAA2QAEGggAAAAICAgCAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAKUTwPA==
'/*!*/;
# at 245
#170725  2:04:51 server id 1  end_log_pos 311   Query   thread_id=4 exec_time=0 error_code=0
SET TIMESTAMP=1500973491/*!*/;
SET @@session.pseudo_thread_id=4/*!*/;
SET @@session.foreign_key_checks=1, @@session.sql_auto_is_null=0, @@session.unique_checks=1, @@session.autocommit=1/*!*/;
SET @@session.sql_mode=0/*!*/;
SET @@session.auto_increment_increment=1, @@session.auto_increment_offset=1/*!*/;
/*!C utf8 *//*!*/;
SET @@session.character_set_client=33,@@session.collation_connection=33,@@session.collation_server=8/*!*/;
SET @@session.lc_time_names=0/*!*/;
SET @@session.collation_database=DEFAULT/*!*/;
BEGIN /*!*/;
# at 311
#170725  2:04:51 server id 1  end_log_pos 404   Query   thread_id=4 exec_time=0 error_code=0
use `bp`/*!*/;
SET TIMESTAMP=1500973491/*!*/;
insert into bptest values(3,'c') /*!*/;
# at 404
#170725  2:04:51 server id 1  end_log_pos 431   Xid = 47
COMMIT/*!*/;
# at 431
#170725  2:04:56 server id 1  end_log_pos 497   Query   thread_id=4 exec_time=0 error_code=0
SET TIMESTAMP=1500973496/*!*/;
BEGIN /*!*/;
# at 497
#170725  2:04:56 server id 1  end_log_pos 590   Query   thread_id=4 exec_time=0 error_code=0
SET TIMESTAMP=1500973496/*!*/;
insert into bptest values(4,'d') /*!*/;
# at 590
#170725  2:04:56 server id 1  end_log_pos 617   Xid = 48
COMMIT/*!*/;
# at 617
#170725  2:05:00 server id 1  end_log_pos 660   Rotate to mysql-bin.000002  pos: 4
DELIMITER ;
# End of log file ROLLBACK /* added by mysqlbinlog */;
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;
[root@localhost mysql]#

在这个日志文件里面我们可以看到sql语句,且这些语句都位于mariadb操作里面的flush logs之间
现在我们就来进行备份的还原吧
现在我们使用mysql-bin.000001进行操作

[root@localhost mysql]# mysqlbinlog mysql-bin.000001|mysql -uroot -p
Enter password: 
[root@localhost mysql]# 

执行完毕,没有报错,我们再进数据库里面看看是否成功还原备份

MariaDB [bp]> select * from bptest;  #还原前
Empty set (0.00 sec)

MariaDB [bp]> select * from bptest;  #还原后
+------+------+
| id | name | +------+------+
|    3 | c    |
| 4 | d | +------+------+
2 rows in set (0.00 sec)

MariaDB [bp]>

MariaDB YUM安装及忘记密码解决方法

一、添加源

官方源

[mariadb] 
name = MariaDB 
baseurl = http://yum.mariadb.org/10.1/centos7-amd64 
gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB 
gpgcheck=1

国内源

[mariadb]
name = MariaDB
baseurl = https://mirrors.tuna.tsinghua.edu.cn/mariadb/yum/10.1/centos7-amd64
gpgkey = https://mirrors.tuna.tsinghua.edu.cn/mariadb/yum//RPM-GPG-KEY-MariaDB
gpgcheck = 1

yum-complete-transaction错误处理

$ yum install yum-utils
$ yum clean all
$ /usr/sbin/yum-complete-transaction --cleanup-only

安装

$ yum install MariaDB-server MariaDB-client MariaDB-devel

二、MariaDB的root密码忘记后的解决方法

编辑/usr/lib/systemd/system/mariadb.service文件,在Service段中添加

# 在Server段中的ExecStart出添加如下;
ExecStart=/usr/bin/mysqld_safe --basedir=/usr --skip-grant-tables --skip-networking

键入systemctl daemon-reload使其立即生效

$ systemctl daemon-reload

重新启动MariaDB服务

$ systemctl restart mariadb.service

完结。

修复由于fstab文件错误导致KVM虚拟机无法启动的问题

最近客户反馈虚拟机在启动的过程中出现报错,详细如下图所示,在与他了解的过程中得知在重启之前在编辑过/etc/fstab文件,估计是因为这个原因造成的,于是有了这个修复的过程。

未分类

通过live CD启动Linux,我这里用的是KALI的系统,当然其它任何带live cd的系统光盘都可以。

未分类

启动完成以后,如果你使用mount /dev/vdb2 /mnt会出现报错:“不知道的文件系统”,这是因为LVM2的磁盘格式没有办法直接mount,需要通过以下步骤才能够进行mount的操作。

1、确保已经安装lvm2

未分类

2、确保能够通过fdisk -lu 识别所有物理卷

未分类

3、运行pvscan扫描所有磁盘的物理卷,这是为了确保您的LVM2硬盘能够被检测到。

未分类

4、运行vgscan扫描卷组

未分类

5、激活所有可用的卷组,这是显示已经激活3个逻辑卷

未分类

6、运行lvscan扫描所有磁盘的逻辑卷。您现在可以看到逻辑卷内的分区已经活动。

未分类

7、mount你需要编辑的逻辑卷至/mnt目录

mount /dev/cl/root /mnt

8、修改fstab文件

vi /mnt/etc/fstab

删除对应两行

未分类

9、重启服务器即可

reboot

未分类

kvm的网络桥接模式与快照管理介绍

先确认系统是否支持虚拟化技术

egrep '(vmx|svm)' --color=always /proc/cpuinfo

安装基本需要的组件

yum install -y qemu-kvm bridge-utils

给qemu-kvm命令添加到环境变量

ln -s /usr/libexec/qemu-kvm /sbin/

转载kvm模块

modprobe kvm

创建一个文件夹,用来存放将要创建的系统文件内容

qemu-img create -f qcow2 -o preallocation=metadata /PATH/FILENAME.img 20G

关闭networkmanager服务,并且创建一个br0的桥接网卡

cp /etc/sysconfig/network-scripts/ifcfg-eth0 /etc/sysconfig/network-scripts/ifcfg-br0

ifcfg-eth0作出如下配置(此时eth0已经虚拟成一个交换机)

未分类

ifcfg-br0作出如下配置

未分类

创建完成使用命令查看

brctl show

未分类

编写一个启动网络脚本

vim /root/qemu-ifup

未分类

使用qemu-kvm创建并启动系统(安装windows 将if=virto改为if=ide即可)

qemu-kvm -cpu host -smp 1 -m 1G -name linux -drive  file=linux.img,media=disk,format=qcow2,if=virtio -drive file=/isofile/CentOS-7-x86_64-Minimal-1503-01.iso,media=cdrom -boot order=dc,once=d -net nic,macaddr=00:00:00:00:00:01 -net tap,script=/root/qemu-ifup -vnc 192.168.3.125:1

在另一个tty查看端口是否打开

未分类

在另外一台安装图形界面的主机安装vnc

yum install -y tigervnc

vncviewer 192.168.3.125:5901

未分类

确认安装程序完成后结束qemu-kvm进程

未分类

使用下面命令基于img磁盘启动(-daemonize后台脱离tty)

qemu-kvm -cpu host -smp 1 -m 1G -name centos7 -drive file=/kvm/linux.img,media=disk,format=qcow2,if=virtio -net nic,macaddr=00:00:00:00:00:01 -net tap,script=/root/qemu-ifup -vnc 192.168.3.125:1 -daemonize

未分类

快照使用方法:

创建快照

qemu-img snapshot -c 快照名称 /系统img/文件

查看创建的快照

qemu-img snapshot -l /系统img文件

快照恢复

qemu-img snapshot -a 快照的id号 /img文件

删除快照

qemu-img snapshot -d 快照id号 /img文件

快照检查(如遇到此类问题 Image is corrupt; cannot be opened read/write)

qemu-img check -r all /img文件位置

实验步骤:

未分类

配置keepalived主从切换时发送告警邮件

邮件脚本:

keepalived_notify.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import smtplib
from email.mime.text import MIMEText
from email.header import Header
import sys, time, subprocess



# 第三方 SMTP 服务
mail_host="smtp.exmail.qq.com"  #设置服务器
mail_user="xxx"    #用户名
mail_pass="xxx"   #口令


sender = '[email protected]'    # 邮件发送者
receivers = ['[email protected]', '[email protected]']  # 接收邮件,可设置为你的QQ邮箱或者其他邮箱

p = subprocess.Popen('hostname', shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
hostname = p.stdout.readline().split('n')[0]

message_to = ''
for i in receivers:
    message_to += i + ';'

def print_help():
    note = '''python script.py role ip vip
    '''
    print(note)
    exit(1)

time_stamp = time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))

if len(sys.argv) != 4:
    print_help()
elif sys.argv[1] == 'master':
    message_content = '%s server: %s(%s) change to Master, vIP: %s' %(time_stamp, sys.argv[2], hostname, sys.argv[3])
    subject = '%s change to Master -- keepalived notify' %(sys.argv[2])
elif sys.argv[1] == 'backup':
    message_content = '%s server: %s(%s) change to Backup, vIP: %s' %(time_stamp, sys.argv[2], hostname, sys.argv[3])
    subject = '%s change to Backup -- keepalived notify' %(sys.argv[2])
else:
    print_help()

message = MIMEText(message_content, 'plain', 'utf-8')
message['From'] = Header(sender, 'utf-8')
message['To'] =  Header(message_to, 'utf-8')

message['Subject'] = Header(subject, 'utf-8')

try:
    smtpObj = smtplib.SMTP()
    smtpObj.connect(mail_host, 25)    # 25 为 SMTP 端口号
    smtpObj.login(mail_user,mail_pass)
    smtpObj.sendmail(sender, receivers, message.as_string())
    print("邮件发送成功")
except smtplib.SMTPException as e:
    print("Error: 无法发送邮件")
    print(e)

使用方法:

python script.py{脚本名} role{master|backup} ip{本keepalived服务器IP} vip{虚拟IP}

master keepalived:

global_defs {
        notification_email {
                [email protected]
        }

        notification_email_from [email protected]
        smtp_server 127.0.0.1
        smtp_connect_timeout 30
        router_id LVS_DEVEL
}
## 上面的配置邮件只能发送到本机,mail 可查看


vrrp_script chk_http_port {
        script "</dev/tcp/127.0.0.1/80"
        interval 2
        weight -10
}


vrrp_instance VI_1 {
        state BACKUP        ############ MASTER|BACKUP
        interface ens160
        virtual_router_id 11
        mcast_src_ip 192.168.1.178
        priority 99                  ########### 权值要比 back 高
        advert_int 2

        authentication {
                auth_type PASS
                auth_pass 8u90u3fhE3FQ
        }

        track_script { 
                chk_http_port ### 执行监控的服务 
        }

        virtual_ipaddress {
                192.168.1.96
        }

        notify_master "/bin/python /script/keepalived_notify.py master 192.168.1.178 192.168.1.96"
        notify_backup "/bin/python /script/keepalived_notify.py backup 192.168.1.178 192.168.1.96"

}

backup keepalived:

global_defs {
        notification_email {
                [email protected]
        }

        notification_email_from [email protected]
        smtp_server 127.0.0.1
        smtp_connect_timeout 30
        router_id LVS_DEVEL
}
## 上面的配置邮件只能发送到本机,mail 可查看


vrrp_script chk_http_port {
        script "</dev/tcp/127.0.0.1/80"
        interval 2
        weight -10
}


vrrp_instance VI_1 {
        state BACKUP        ############ MASTER|BACKUP
        interface ens160
        virtual_router_id 11
        mcast_src_ip 192.168.1.174
        priority 100                  ########### 权值要比 back 高
        advert_int 2

        authentication {
                auth_type PASS
                auth_pass 8u90u3fhE3FQ
        }

        track_script { 
                chk_http_port ### 执行监控的服务 
        }

        virtual_ipaddress {
                192.168.1.96
        }

        notify_master "/bin/python /script/keepalived_notify.py master 192.168.1.174 192.168.1.96"
        notify_backup "/bin/python /script/keepalived_notify.py backup 192.168.1.174 192.168.1.96"

}

LVS Keepalived双机高可用负载均衡搭建

应用环境:

LVS负责多台WEB端的负载均衡(LB);Keepalived负责LVS的高可用(HA),这里介绍主备模型。

测试环境:

未分类

未分类

配置步骤:

1. 安装软件

在LVS-1和LVS-2两台主机上安装ipvsadm和keepalived

~]# yum install ipvsadm keepalived -y  

在两台Web主机上安装Nginx

~]# yum install nginx -y    //修改访问主页内容,方便最后测试,这里改为了Nginx Web 1 [IP:12]和Nginx Web 1 [IP:13]

  

2. 配置Keepalived

说明:keepalived底层有关于IPVS的功能模块,可以直接在其配置文件中实现LVS的配置,不需要通过ipvsadm命令再单独配置。

[root@lvs-1 ~]# vim /etc/keepalived/keepalived.conf    // Master配置好的信息如下 
! Configuration File for keepalived

global_defs {
   router_id LVS        ## 不一定要与主机名相同,也不必与BACKUP的名字一致
}

vrrp_instance VI_1 {      
    state MASTER        ## LVS-1配置了为主,另外一台LVS-2配置为BACKUP
    interface eth0       ## 注意匹配网卡名
    virtual_router_id 51    ## 虚拟路由ID(0-255),在一个VRRP实例中主备服务器ID必须一样
    priority 150        ## 优先级值设定:MASTER要比BACKUP的值大
    advert_int 3        ## 通告时间间隔:单位秒,主备要一致
    authentication {      ##认证机制
        auth_type PASS     ## 默认PASS; 有两种:PASS或AH 
        auth_pass 1111     ## 默认1111; 可多位字符串,但仅前8位有效
    }
    virtual_ipaddress {
        138.138.82.222     ## 虚拟IP;可多个,写法为每行一个
    }
}
virtual_server 138.138.82.222 80 {
    delay_loop 3       ## 设置健康状态检查时间
    lb_algo rr        ## 调度算法,这里用了rr轮询算法,便于后面测试查看
    lb_kind DR        ## 这里测试用了Direct Route 模式,
   # persistence_timeout 1  ## 持久连接超时时间,先注释掉,不然在单台上测试时,全部会被lvs调度到其中一台Real Server
    protocol TCP
    real_server 138.138.82.12 80 {
        weight 1
        TCP_CHECK {
            connect_timeout 10    ##设置响应超时时间
            nb_get_retry 3       ##设置超时重试次数
            delay_before_retry 3   ##设置超时重试间隔时间
            connect_port 80
        }
    }
    real_server 138.138.82.13 80 {
        weight 1
        TCP_CHECK {
            connect_timeout 10
            nb_get_retry 3
            delay_before_retry 3
            connect_port 80
        }
    }
}                   

保存,退出;

[root@lvs-2 ~]# vim /etc/keepalived/keepalived.conf     //同样,修改BACKUP上的配置文件,如下
! Configuration File for keepalived

global_defs {
   router_id LVS
}

vrrp_instance VI_1 {
    state BACKUP
    interface eth0
    virtual_router_id 51
    priority 120
    advert_int 3
    authentication {
        auth_type PASS
        auth_pass 1111
    }
    virtual_ipaddress {
        138.138.82.222
    }
}
virtual_server 138.138.82.222 80 {
    delay_loop 3
    lb_algo rr
    lb_kind DR
   # persistence_timeout 1
    protocol TCP
    real_server 138.138.82.12 80 {
        weight 1
        TCP_CHECK {
            connect_timeout 10
            nb_get_retry 3
            delay_before_retry 3
            connect_port 80
        }
    }
    real_server 138.138.82.13 80 {
        weight 1
        TCP_CHECK {
            connect_timeout 10
            nb_get_retry 3
            delay_before_retry 3
            connect_port 80
        }
    }
}                   

保存,退出;

启动keepalived

[root@lvs-1 ~]# service keepalived start

[root@lvs-2 ~]# service keepalived start

加入开机启动:

[root@lvs-1 ~]# chkconfig keepalived on

[root@lvs-2 ~]# chkconfig keepalived on

查看:

未分类    

未分类   

说明:

此时,Virtual IP是飘在MASTER上面,如果断开MASTER(关闭MASTER上keepalived),Virtual IP就会飘到BACKUP上;

再当MASTER复活(启动keepalived),Virtual IP会再次回到MATSTER上,可通过/var/log/message查看变更信息;

3. 配置WEB端(两台Nginx)

这里直接给出了配置脚本,方便操作;

用法:~]# sh lvs-web.sh start | stop    //新建一个脚本,假定该脚本名为lvs-web.sh
#!/bin/bash 
VIP=138.138.82.222
case "$1" in
start)
           echo "start LVS of RealServer DR" 
           /sbin/ifconfig lo:0 $VIP broadcast $VIP netmask 255.255.255.255 up
           /sbin/route add -host $VIP dev lo:0
           echo "1" &gt;/proc/sys/net/ipv4/conf/lo/arp_ignore
           echo "2" &gt;/proc/sys/net/ipv4/conf/lo/arp_announce
           echo "1" &gt;/proc/sys/net/ipv4/conf/all/arp_ignore
           echo "2" &gt;/proc/sys/net/ipv4/conf/all/arp_announce
       ;;
stop)
           /sbin/ifconfig lo:0 down
           echo "close LVS of RealServer DR" 
           echo "0" &gt;/proc/sys/net/ipv4/conf/lo/arp_ignore
           echo "0" &gt;/proc/sys/net/ipv4/conf/lo/arp_announce
           echo "0" &gt;/proc/sys/net/ipv4/conf/all/arp_ignore
           echo "0" &gt;/proc/sys/net/ipv4/conf/all/arp_announce
           ;;

*)
          echo "Usage: $0 {start|stop}" 
          exit 1
esac
exit 0

保存,退出;

运行脚本:    

~]# sh lvs-web.sh start  //启动脚本

    

4. 测试

测试①

若一切顺利,正常测试如下:

~]# while true ; do curl 138.138.82.222; sleep 1;done    //每秒执行一次curl 138.138.82.222

未分类
    
默认现在访问VIP:138.138.82.222,走的是LVS-1(MASTER)

测试②

断开MASTER:VIP飘到BACKUP上,访问VIP正常,Client 轮询依旧;

复活MASTER:VIP飘回MASTER上,访问VIP正常,Client 轮询依旧;    // 成功实现:LVS的高可用 和 Nginx的负载均衡

测试③

手动断开Nginx,然后再手动启动Nginx:

未分类   

结束.

keepalived VRRP协议与选举算解析

简介

什么是keepalived呢?keepalived是实现高可用的一种轻量级的技术手段,主要用来防止单点故障(单点故障是指一旦某一点出现故障就会导致整个系统架构的不可用)的发生。之所以说keepalived是轻量级的,是相对于corosync + ldirectord来说的。keepalived也可以实现高可用集群,而且配置起来比corosync + ldirectord简单方便很多,keepalived与corosync的工作机制相差很多。corosync + ldirectord实现的功能虽然强大,但配置起来比较麻烦,而keepalived功能虽然简单,但配置起来比较容易。也就是说keepalived可实现corosync + ldirectord实现的功能,只不过前者没有后者功能强大而已。

VRRP协议

在介绍keepalived之前,不得不先介绍下一个协议——VRRP。之所以要介绍这个协议,是因为VRRP协议是keepalived实现的基础。下面先来一块看下这个这协议是干吗用的吧。

未分类

如上图所示,通常,同一网段内的所有主机都设置一条相同的、以网关为下一跳的缺省路由。主机发往其他网段的报文将通过缺省路由发往网关,再由网关进行转发,从而实现主机与外部网络的通信。当网关发生故障时,本网段内所有以网关为缺省路由的主机将无法与外部网络通信,仅能实现内部主机间通信。缺省路由为用户的配置操作提供了方便,但是对缺省网关设备提出了很高的稳定性要求。增加出口网关是提高系统可靠性的常见方法,此时如何在多个出口之间进行选路就成为需要解决的问题。而VRRP正好解决了此问题。

VRRP:Virtual Router Redundancy Protocol,虚拟路由冗余协议。VRRP说白了就是实现地址漂移的,是一种容错协议,在提高可靠性的同时,简化了主机的配置。该协议能够实现将可以承担网关功能的一组路由器加入到备份组中,形成一台虚拟路由器,由VRRP的选举机制决定哪台路由器承担转发任务,局域网内的主机只需将虚拟路由器配置为缺省网关。

在VRRP协议出现之前,为了不让单个路由器成为本地与外部通信的瓶颈,我们需要有多个路由,在此种模式下,我们内部的主机就需要将自己的网关指向不同的路由器,这样的配置对我们的网关管理员来说是很麻烦的,且不容易实现。在VRRP协议出现后,为了不让单个路由器成为本地与外部通信的瓶颈,我们仍需要有多个路由,但可以使用同一个缺省网关,我们只需将内部主机指定一个缺省网关即可。VRRP协议会根据优先级来选择一个正常的路由作为主路由器实现与外部的通信,而其他路由则作为备份路由不参与转发。在此模式下,多个路由器组成虚拟路由器组,物理上是多个路由器组成,但在逻辑上却是表现为只有一个路由。效果如下图所示:

未分类

在上图中,Router A、Router B和Router C组成一个虚拟路由器。各虚拟路由器都有自己的IP地址。局域网内的主机将虚拟路由器设置为缺省网关。Router A、Router B和Router C中优先级最高的路由器作为Master路由器,承担网关的功能。其余两台路由器作为Backup路由器。当master路由器出故障后,backup路由器会根据优先级重新选举新的master路由器承担网关功能。Master 路由器周期性地发送VRRP 报文,在虚拟路由器中公布其配置信息(优先级等)和工作状况。Backup路由器通过接收到VRRP 报文的情况来判断Master 路由器是否工作正常。

VRRP根据优先级来确定备份组中每台路由器的角色(Master 路由器或Backup 路由器)。优先级越高,则越有可能成为Master 路由器。VRRP优先级的可配置的取值范围为1 到254。

为了防止非法用户构造报文攻击备份组,VRRP通过在VRRP报文中增加认证字的方式,验证接收到的VRRP报文。VRRP提供了两种认证方式:

  • simple:简单字符认证。发送VRRP 报文的路由器将认证字填入到VRRP 报文中,而收到VRRP 报文的路由器会将收到的VRRP 报文中的认证字和本地配置的认证字进行比较。如果认证字相同,则认为接收到的报文是真实、合法的VRRP 报文;否则认为接收到的报文是一个非法报文。

  • md5:MD5 认证。发送VRRP 报文的路由器利用认证字和MD5 算法对VRRP 报文进行摘要运算,运算结果保存在Authentication Header(认证头)中。收到VRRP 报文的路由器会利用认证字和MD5 算法进行同样的运算,并将运算结果与认证头的内容进行比较。如果相同,则认为接收到的报文是真实、合法的VRRP 报文;否则认为接收到的报文是一个非法报文。

在有多个路由器组成的虚拟路由中,当我们的内部主机很多时,如果所有主机都使用同一个master路由,会使得其他路由器很清闲,很浪费资源,我们期望我们本地的内部主机平分到各个路由器上,即让我们的内部主机的缺省网关指向不同的路由,从而减轻因只有一个master路由而造成网络带宽拥堵的负担。这就是负载分担VRRP。但这个如何实现呢?先看下面的配置效果图:

未分类

在此情况下,同一台路由器同时加入多个VRRP备份组,在不同备份组中有不同的优先级,从而实现负载分担。

在上图中,有三个备份组存在:

  • 备份组1:对应虚拟路由器1。Router A作为Master路由器,Router B和Router C作为Backup路由器。
  • 备份组2:对应虚拟路由器2。Router B作为Master路由器,Router A和Router C作为Backup路由器。
  • 备份组3:对应虚拟路由器3。Router C作为Master路由器,Router A和Router B作为Backup路由器。

为了实现业务流量在Router A、Router B和Router C之间进行负载分担,需要将局域网内的主机的缺省网关分别设置为虚拟路由器1、2和3。在配置优先级时,需要确保三个备份组中各路由器的VRRP优先级形成交叉对应。为了便于理解,我们假定有三个路由设备Router A、B、C和三台主机Host A、B、C,列举有在不同的虚拟路由组中。对路由器A来说,因在虚拟路由组1中Router A的优先级高于另外两个,因此,Router A 作为 Master 路由器,Router B 和Router C 作为 Backup路由器;同样,对路由器B来说,因在虚拟路由器组2中Router B的优先级高于另外两个,因此,Router B 作为 Master 路由器,Router A 和Router C 作为 Backup路由器;对路由器C来说,因在虚拟路由器组3中Router C的优先级高于另外两个,因此,Router C 作为 Master 路由器,Router A 和Router B 作为 Backup路由器。对不同的主机来说,一旦其master路由器出故障后,会在另外正常的路由器中根据优先级重新选定master路由。如这里假定Host A的默认网关指向Router A,即Host A指向虚拟路由器组1的默认网关,对主机A来说,如果其master路由出现故障,即Router A出现故障,则会从另外两个正常的备份虚拟路由中根据各自的优先级选取高优先级的作为新的master路由,这里就是选取Router B作为其master路由来完成网关功能。假如想了解更多关于VRRP协议相关的信息请查阅相关资料,这里不再过多介绍。

Keepalived

一、配置说明

keepalived的配置位于/etc/keepalived/keepalived.conf,配置文件格式包含多个必填/可选的配置段,部分重要配置含义如下:

  • global_defs: 全局定义块,定义主从切换时通知邮件的SMTP配置。
  • vrrp_instance: vrrp实例配置。
  • vrrp_script: 健康检查脚本配置。

细分下去,vrrp_instance配置段包括:

  • state: 实例角色。分为一个MASTER和一(多)个BACKUP。
  • virtual_router_id: 标识该虚拟路由器的ID,有效范围为0-255。
  • priority: 优先级初始值,竞选MASTER用到,有效范围为0-255。
  • advert_int: VRRP协议通告间隔。
  • interface: VIP所绑定的网卡,指定处理VRRP多播协议包的网卡。
  • mcast_src_ip: 指定发送VRRP协议通告的本机IP地址。
  • authentication: 认证方式。
  • virtual_ipaddress: VIP。
  • track_script: 健康检查脚本。

vrrp_script配置段包括:

  • script: 一句指令或者一个脚本文件,需返回0(成功)或非0(失败),keepalived以此为依据判断其监控的服务状态。
  • interval: 健康检查周期。
  • weight: 优先级变化幅度。
  • fall: 判定服务异常的检查次数。
  • rise: 判定服务正常的检查次数。

二、选举算法

keepalived中优先级高的节点为MASTER。MASTER其中一个职责就是响应VIP的arp包,将VIP和mac地址映射关系告诉局域网内其他主机,同时,它还会以多播的形式(目的地址224.0.0.18)向局域网中发送VRRP通告,告知自己的优先级。网络中的所有BACKUP节点只负责处理MASTER发出的多播包,当发现MASTER的优先级没自己高,或者没收到MASTER的VRRP通告时,BACKUP将自己切换到MASTER状态,然后做MASTER该做的事:1.响应arp包,2.发送VRRP通告。

MASTER和BACKUP节点的优先级如何调整?

首先,每个节点有一个初始优先级,由配置文件中的priority配置项指定,MASTER节点的priority应比BAKCUP高。运行过程中keepalived根据vrrp_script的weight设定,增加或减小节点优先级。规则如下:

  1. 当weight > 0时,vrrp_script script脚本执行返回0(成功)时优先级为priority + weight, 否则为priority。当BACKUP发现自己的优先级大于MASTER通告的优先级时,进行主从切换。

  2. 当weight < 0时,vrrp_script script脚本执行返回非0(失败)时优先级为priority + weight, 否则为priority。当BACKUP发现自己的优先级大于MASTER通告的优先级时,进行主从切换。

  3. 当两个节点的优先级相同时,以节点发送VRRP通告的IP作为比较对象,IP较大者为MASTER。

主从的优先级初始值priority和变化量weight设置非常关键,配错的话会导致无法进行主从切换。比如,当MASTER初始值定得太高,即使script脚本执行失败,也比BACKUP的priority + weight大,就没法进行VIP漂移了。所以priority和weight值的设定应遵循: abs(MASTER priority – BAKCUP priority) < abs(weight)。

另外,当网络中不支持多播(例如某些云环境),或者出现网络分区的情况,keepalived BACKUP节点收不到MASTER的VRRP通告,就会出现脑裂(split brain)现象,此时集群中会存在多个MASTER节点。

Kubernetes Pod调度原理介绍

最近两周一直没有抽出时间写点Kubernetes的东西,这篇学习一下Kubernetes对Pod的调度。我们先来复习一下Kubernetes的一些基本概念。

Kubernetes的基本概念

Kubernetes是一个基于容器技术的分布式架构平台,它首先是一个开源的容器集群管理系统,又是一个分布式系统开发、运维和支撑平台。 Kubernetes为容器应用提供了服务注册和发现、负载均衡、服务部署和运行、服务滚动升级、在线扩容和缩容、资源调度、资源配额管理等功能。 可以说Kubernetes具备完备的集群管理能力,贯串分布式系统开发、测试、部署、运维监控各个环节。

Kubernetes中的绝大部分概念都抽象成Kubernetes管理的一种资源对象,下面我们一起复习一下这些基本概念:

1、Master:Master节点是Kubernetes集群的控制节点,负责整个集群的管理和控制。Master节点上包含以下组件:

  • kube-apiserver:集群控制的入口,提供HTTP REST服务
  • kube-controller-manager:Kubernetes集群中所有资源对象的自动化控制中心
  • kube-scheduler:负责Pod的调度,我们本篇将主要学一下kube-scheduler的调度功能

2、Node: Node节点是Kubernetes集群中的工作节点,Node上的工作负载由Master节点分配,工作负载主要是运行容器应用。Node节点上包含以下组件:

  • kubelet:负责Pod的创建、启动、监控、重启、销毁等工作,同时与Master节点协作,实现集群管理的基本功能。
  • kube-proxy:实现Kubernetes Service的通信和负载均衡
  • 运行运行容器化(Pod)应用

3、Pod: Pod是Kubernetes最基本的部署调度单元。每个Pod可以由一个或多个业务容器和一个根容器(Pause容器)组成。一个Pod表示某个应用的一个实例

4、ReplicaSet:是Pod副本的抽象,用于解决Pod的扩容和伸缩

5、Deployment:Deployment表示部署,在内部使用ReplicaSet来实现。可以通过Deployment来生成相应的ReplicaSet完成Pod副本的创建

6、Service:Service是Kubernetes最重要的资源对象。Kubernetes中的Service对象可以对应微服务架构中的微服务。Service定义了服务的访问入口,服务的调用者Pod通过这个地址访问Service后端的Pod副本实例。 Service通过Label Selector同后端的Pod副本建立关系,Deployment保证后端Pod副本的数量,也就是保证服务的伸缩性

kube-scheduler调度过程

Master节点上的kube-scheduler负责Pod的调度,kube-scheduler将Pod安置到目标Node上,之后将Pod交给目标Node上的kubelet,Pod生命周期后续的部分由kubelet接管。

kube-scheduler使用特定的调度算法和调度策略将等待调度的Pod调度到某个合适的Node上。等待调度的Pod包含使用API创建的Pod,也包含ControllerManager为补足副本而创建的Pod。具体过程为kube-scheduler会从待调度Pod列表中取出每个Pod,并根据调度算法和调度策略从Node列表中选出一个最合适的Node,将Pod和目标Node绑定(Binding),同时将绑定信息写入到etcd中。目标Node上的kubelet通过kube-apiserver监听到kube-scheduler触发的Pod和目标Node的绑定事件,就会pull镜像和启动容器。

预选(Predicates)和优选(Priorites)步骤

kube-scheduler当前提供的调度过程包含预选(Predicates)和优选(Priorites)两步:

  • 预选(Predicates):将根据配置的预选策略(Predicates Policies)过滤掉不满足这些策略的Node,剩下的Node将作为候选Node成为优选过程的输入。

  • 优选(Priorites):根据配置的优选策略(Priorities Policies)计算出每个候选Node的积分,按积分排名,得分最高的Node胜出,Pod会和该Node绑定。

kube-scheduler进程的–algorithm-provider参数用于指定调度算法,当前Kubernetes版本1.6默认配置的是DefaultProvider。 plugin/pkg/scheduler/algorithmprovider/defaults/defaults.go中包含了默认的Predicates Policies和Priorities Policies。 Scheduler Algorithm in Kubernetes中包含全部的Predicates Policies和Priorities Policies。

另外kube-scheduler可以通过–policy-config-file参数指定想要启用的Predicates Policies和Priorities Policies。例如:

{
"kind" : "Policy",
"apiVersion" : "v1",
"predicates" : [
    {"name" : "PodFitsHostPorts"},
    {"name" : "PodFitsResources"},
    {"name" : "NoDiskConflict"},
    {"name" : "NoVolumeZoneConflict"},
    {"name" : "MatchNodeSelector"},
    {"name" : "HostName"}
    ],
"priorities" : [
    {"name" : "LeastRequestedPriority", "weight" : 1},
    {"name" : "BalancedResourceAllocation", "weight" : 1},
    {"name" : "ServiceSpreadingPriority", "weight" : 1},
    {"name" : "EqualPriority", "weight" : 1}
    ],
"hardPodAffinitySymmetricWeight" : 10
}

Pod调度入门

接下来我们先通过几个例子来学习一下基于预选策略(Predicates Policies)和优选策略(Priorities Policies)实现的Pod调度。 我们可以使用这些策略实现将Pod调度到某个或某些特别的Node上。

我们使用前面在Kubernetes 1.6 高可用集群部署部署的集群作为试验环境。

192.168.61.11 node1
192.168.61.12 node2
192.168.61.13 node3
192.168.61.14 node4

MatchNodeSelector

MatchNodeSelector是一个预选策略,用于判断候选Node是否包含Pod的spec.nodeSelector指定的标签。

我们先来看看当前集群中的Node具有的标签:

kubectl get nodes --show-labels
NAME      STATUS    AGE       VERSION   LABELS
node1     Ready     34d       v1.6.2    beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=node1
node2     Ready     32d       v1.6.2    beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=node2
node3     Ready     32d       v1.6.2    beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=node3
node4     Ready     29d       v1.6.2    beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=node4

可以看到每个Node上都有kubernetes.io/hostname=xxx这个label,我们可以使用Pod的spec.nodeSelector指定这个label将Pod调度具体的某个Node上。例如我们创建一个如下的Deployment,nginx-deployment.yaml:

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
      nodeSelector:
        kubernetes.io/hostname: node4
kubectl create -f nginx-deployment.yaml
deployment "nginx-deployment" created

kubectl get deploy
NAME               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   2         2         2            2           1m

kubectl get pod -o wide
NAME                                READY     STATUS        RESTARTS   AGE       IP           NODE
nginx-deployment-1270158812-9qh1v   1/1       Running       0          1m        10.244.3.5   node4
nginx-deployment-1270158812-f5k7h   1/1       Running       0          1m        10.244.3.6   node4

可以看到这个Pod的两个副本都被调度到node4上。下面继续试验,假设我们要将Pod调度到磁盘类型为ssd并且专门用来跑Web应用的Node上。 我们先删除前面创建的Deployment:

kubectl delete deploy nginx-deployment

我们给node1, node2, node3标记如下:

kubectl label node node1 disktype=ssd apptype=web
kubectl label node node2 disktype=ssd
kubectl label node node3 disktype=ssd apptype=web
kubectl label node node4 apptype=web

kubectl get nodes --show-labels
NAME      STATUS    AGE       VERSION   LABELS
node1     Ready     34d       v1.6.2    apptype=web,beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,disktype=ssd,kubernetes.io/hostname=node1
node2     Ready     32d       v1.6.2    beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,disktype=ssd,kubernetes.io/hostname=node2
node3     Ready     32d       v1.6.2    apptype=web,beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,disktype=ssd,kubernetes.io/hostname=node3
node4     Ready     29d       v1.6.2    apptype=web,beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=node4

修改前面的nginx-deployment.yaml文件:

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
      nodeSelector:
        disktype: ssd
        apptype: web
kubectl create -f nginx-deployment.yaml
deployment "nginx-deployment" created

kubectl get deploy
NAME               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   2         2         2            0           45s

kubectl get pod -o wide
NAME                               READY     STATUS    RESTARTS   AGE       IP            NODE
nginx-deployment-908661896-pzs2z   1/1       Running   0          2m        10.244.0.69   node1
nginx-deployment-908661896-wvd9p   1/1       Running   0          2m        10.244.2.51   node3

可以看到Pod被调度到了node1和node3上,而node1和node3上我们前面标记了disktype=ssd和apptype=web。

NodeAffinityPriority

NodeAffinityPriority是一个优选策略,是Kubernetes 1.2开始提供的Kubernetes调度中的亲和性机制。NodeAffinityPriority的Node选择器不再限于对Node Label的精确匹配,而支持多种操作符(如In, NotIn, Exists, DoesNotExist, Gt, Lt)。支持两种类型的选择器,一种是requiredDuringSchedulingIgnoredDuringExecution,它保证所选的Node必须满足Pod对Node的所有要求,这种更像前面的MatchNodeSelector;另外一种是preferresDuringSchedulingIgnoredDuringExecution,它对kube-scheduler提出需求,kube-scheduler会尽量但不保证满足NodeSelector的要求。

例如:

apiVersion: v1
kind: Pod
metadata:
  name: with-pod-affinity
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: security
            operator: In
            values:
            - S1
        topologyKey: failure-domain.beta.kubernetes.io/zone
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: security
              operator: In
              values:
              - S2
          topologyKey: kubernetes.io/hostname
  containers:
  - name: with-pod-affinity
    image: gcr.io/google_containers/pause:2.0

解决Kubernetes 1.6.4 Dashboard无法访问的问题

前一段时间将之前采用kubeadm安装的Kubernetes 1.5.1环境升级到了1.6.4版本,升级过程较为顺利。由于该k8s cluster是一个测试环境,当时并没有过于关注,就忙别的事情了。最近项目组打算在这个环境下做一些事情,而当我们重新“捡起”这个环境时,发现Kubernetes Dashboard无法访问了。

Kubernetes的dashboard可以有很多种访问方式,比如:可以通过暴露nodeport的方式(无身份验证,不安全)、可以通过访问apiserver的api服务的方式等。我们的Dashboard通过APIServer进行访问:

https://apiserver_ip:secure_port/ui

正常情况下通过浏览器访问:https://apiserver_ip:secure_port/ui,浏览器会弹出身份验证对话框,待输入正确的用户名和密码后,便可成功进入Dashboard了。但当前,我们得到的结果却是:

User "system:anonymous" cannot proxy services in the namespace "kube-system".

而访问apiserver(https://apiserver_ip:secure_port/)得到的结果如下:

User "system:anonymous" cannot get  at the cluster scope.

一、问题原因分析

k8s 1.6.x版本与1.5.x版本的一个很大不同在于1.6.x版本启用了RBAC的Authorization mode(授权模型),这点在K8s master init的日志中可以得到证实:

# kubeadm init --apiserver-advertise-address xx.xx.xx
... ...
[init] Using Kubernetes version: v1.6.4
[init] Using Authorization mode: RBAC
[preflight] Running pre-flight checks
[preflight] Starting the kubelet service
[certificates] Generated CA certificate and key.
[certificates] Generated API server certificate and key
.... ...
[apiconfig] Created RBAC rules
[addons] Created essential addon: kube-proxy
[addons] Created essential addon: kube-dns

Your Kubernetes master has initialized successfully!
... ...

在《Kubernetes集群的安全配置》一文中我们提到过Kubernetes API server的访问方法:

Authentication(身份验证) -> Authorization(授权)-> Admission Control(入口条件控制)

只不过在Kubernetes 1.5.x及以前的版本中,Authorization的环节都采用了默认的配置,即”AlwaysAllow”,对访问APIServer并不产生什么影响:

# kube-apiserver -h
... ...
--authorization-mode="AlwaysAllow": Ordered list of plug-ins to do authorization on secure port. Comma-delimited list of: AlwaysAllow,AlwaysDeny,ABAC,Webhook,RBAC
... ...

但K8s 1.6.x版本中,–authorization-mode的值发生了变化:

# cat /etc/kubernetes/manifests/kube-apiserver.yaml

spec:
  containers:
  - command:
    - kube-apiserver
    - --allow-privileged=true
    ... ...
    - --basic-auth-file=/etc/kubernetes/basic_auth_file
    - --authorization-mode=RBAC
    ... ...

注:这里我们依旧通过basic auth方式进行apiserver的Authentication,而不是用客户端数字证书校验等其他方式。

显然问题的原因就在于这里RBAC授权方式的使用,让我们无法正常访问Dashboard了。

二、Kubernetes RBAC Authorization简介

RBAC Authorization的基本概念是Role和RoleBinding。Role是一些permission的集合;而RoleBinding则是将Role授权给某些User、某些Group或某些ServiceAccount。K8s官方博客《RBAC Support in Kubernetes》一文的中的配图对此做了很生动的诠释:

从上图中我们可以看到:

Role: pod-reader 拥有Pod的get和list permissions;
RoleBinding: pod-reader 将Role: pod-reader授权给右边的User、Group和ServiceAccount。

和Role和RoleBinding对应的是,K8s还有ClusterRole和ClusterRoleBinding的概念,它们不同之处在于:ClusterRole和ClusterRoleBinding是针对整个Cluster范围内有效的,无论用户或资源所在的namespace是什么;而Role和RoleBinding的作用范围是局限在某个k8s namespace中的。

Kubernetes 1.6.4安装时内建了许多Role/ClusterRole和RoleBinds/ClusterRoleBindings:

# kubectl get role -n kube-system
NAME                                        AGE
extension-apiserver-authentication-reader   50d
system:controller:bootstrap-signer          50d
system:controller:token-cleaner             50d

# kubectl get rolebinding -n kube-system
NAME                                 AGE
system:controller:bootstrap-signer   50d
system:controller:token-cleaner      50d

# kubectl get clusterrole
NAME                                           AGE
admin                                          50d
cluster-admin                                  50d
edit                                           50d
system:auth-delegator                          50d
system:basic-user                              50d
system:controller:attachdetach-controller      50d
... ...
system:discovery                               50d
system:heapster                                50d
system:kube-aggregator                         50d
system:kube-controller-manager                 50d
system:kube-dns                                50d
system:kube-scheduler                          50d
system:node                                    50d
system:node-bootstrapper                       50d
system:node-problem-detector                   50d
system:node-proxier                            50d
system:persistent-volume-provisioner           50d
view                                           50d
weave-net                                      50d

# kubectl get clusterrolebinding
NAME                                           AGE
cluster-admin                                  50d
kubeadm:kubelet-bootstrap                      50d
kubeadm:node-proxier                           50d
kubernetes-dashboard                           50d
system:basic-user                              50d
system:controller:attachdetach-controller      50d
... ...
system:controller:statefulset-controller       50d
system:controller:ttl-controller               50d
system:discovery                               50d
system:kube-controller-manager                 50d
system:kube-dns                                50d
system:kube-scheduler                          50d
system:node                                    50d
system:node-proxier                            50d
weave-net                                      50d

三、Dashboard的role和rolebinding

Kubernetes 1.6.x启用RBAC后,诸多周边插件也都推出了适合K8s 1.6.x的manifest描述文件,比如:weave-net等。Dashboard的manifest文件中也增加了关于rolebinding的描述,我当初用的是1.6.1版本,文件内容摘录如下:

// kubernetes-dashboard.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: kubernetes-dashboard
  labels:
    k8s-app: kubernetes-dashboard
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: kubernetes-dashboard
  namespace: kube-system
... ...

我们看到在kubernetes-dashboard.yaml中,描述文件新建了一个ClusterRoleBinding:kubernetes-dashboard。该binding将ClusterRole: cluster-admin授权给了一个ServiceAccount: kubernetes-dashboard。我们看看ClusterRole: cluster-admin都包含了哪些permission:

# kubectl get clusterrole/cluster-admin -o yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  annotations:
    rbac.authorization.kubernetes.io/autoupdate: "true"
  creationTimestamp: 2017-05-30T14:06:39Z
  labels:
    kubernetes.io/bootstrapping: rbac-defaults
  name: cluster-admin
  resourceVersion: "11"
  selfLink: /apis/rbac.authorization.k8s.io/v1beta1/clusterrolescluster-admin
  uid: 331c79dc-4541-11e7-bc9a-12584ec3a8c9
rules:
- apiGroups:
  - '*'
  resources:
  - '*'
  verbs:
  - '*'
- nonResourceURLs:
  - '*'
  verbs:
  - '*'

可以看到,在rules设定中,cluster-admin似乎拥有了“无限”权限。不过注意:这里仅仅授权给了一个service account,并没有授权给user或group。并且这里的kubernetes-dashboard是dashboard访问apiserver时使用的(下图右侧流程),并不是user访问APIServer时使用的。

我们需要给登录dashboard或者说apiserver的user(图左侧)进行授权。

四、为user: admin进行授权

我们的kube-apiserver的启动参数中包含:

    - --basic-auth-file=/etc/kubernetes/basic_auth_file

也就是说我们访问apiserver使用的是basic auth的身份验证方式,而user恰为admin。而从本文开头的错误现象来看,admin这个user并未得到足够的授权。这里我们要做的就是给admin选择一个合适的clusterrole。但kubectl并不支持查看user的信息,初始的clusterrolebinding又那么多,一一查看十分麻烦。我们知道cluster-admin这个clusterrole是全权限的,我们就来将admin这个user与clusterrole: cluster-admin bind到一起:

# kubectl create clusterrolebinding login-on-dashboard-with-cluster-admin --clusterrole=cluster-admin --user=admin
clusterrolebinding "login-on-dashboard-with-cluster-admin" created

# kubectl get clusterrolebinding/login-on-dashboard-with-cluster-admin -o yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  creationTimestamp: 2017-07-20T08:57:07Z
  name: login-on-dashboard-with-cluster-admin
  resourceVersion: "5363564"
  selfLink: /apis/rbac.authorization.k8s.io/v1beta1/clusterrolebindingslogin-on-dashboard-with-cluster-admin
  uid: 686a3f36-6d29-11e7-8f69-00163e1001d7
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: admin

binding后,我们再来访问一下dashboard UI,不出意外的话,熟悉的dashboard界面就会出现在你的眼前。

用curl测试结果如下:

$curl -u admin:YOUR_PASSWORD -k https://apiserver_ip:secure_port/
{
  "paths": [
    "/api",
    "/api/v1",
    "/apis",
    "/apis/apps",
    "/apis/apps/v1beta1",
    "/apis/authentication.k8s.io",
    "/apis/authentication.k8s.io/v1",
    "/apis/authentication.k8s.io/v1beta1",
    "/apis/authorization.k8s.io",
    "/apis/authorization.k8s.io/v1",
    "/apis/authorization.k8s.io/v1beta1",
    "/apis/autoscaling",
    "/apis/autoscaling/v1",
    "/apis/autoscaling/v2alpha1",
    "/apis/batch",
    "/apis/batch/v1",
    "/apis/batch/v2alpha1",
    "/apis/certificates.k8s.io",
    "/apis/certificates.k8s.io/v1beta1",
    "/apis/extensions",
    "/apis/extensions/v1beta1",
    "/apis/policy",
    "/apis/policy/v1beta1",
    "/apis/rbac.authorization.k8s.io",
    "/apis/rbac.authorization.k8s.io/v1alpha1",
    "/apis/rbac.authorization.k8s.io/v1beta1",
    "/apis/settings.k8s.io",
    "/apis/settings.k8s.io/v1alpha1",
    "/apis/storage.k8s.io",
    "/apis/storage.k8s.io/v1",
    "/apis/storage.k8s.io/v1beta1",
    "/healthz",
    "/healthz/ping",
    "/healthz/poststarthook/bootstrap-controller",
    "/healthz/poststarthook/ca-registration",
    "/healthz/poststarthook/extensions/third-party-resources",
    "/healthz/poststarthook/rbac/bootstrap-roles",
    "/logs",
    "/metrics",
    "/swaggerapi/",
    "/ui/",
    "/version"
  ]
}

k8s(kubernetes)安全控制认证与授权

kubernetes 对于访问 API 来说提供了两个步骤的安全措施:认证和授权。认证解决用户是谁的问题,授权解决用户能做什么的问题。通过合理的权限管理,能够保证系统的安全可靠。

通俗的讲,认证就是验证用户名密码,授权就是检查该用户是否拥有权限访问请求的资源。

Kubernetes集群的所有操作基本上都是通过kube-apiserver这个组件进行的,它提供HTTP RESTful形式的API供集群内外客户端调用。需要注意的是:认证授权过程只存在HTTPS形式的API中。也就是说,如果客户端使用HTTP连接到kube-apiserver,那么是不会进行认证授权的。所以说,可以这么设置,在集群内部组件间通信使用HTTP,集群外部就使用HTTPS,这样既增加了安全性,也不至于太复杂。

下图是 API 访问要经过的三个步骤,前面两个是认证和授权,第三个是 Admission Control,它也能在一定程度上提高安全性,不过更多是资源管理方面的作用。

未分类

下面将介绍1.6版本中已经支持的一些认证方式。

客户端证书

客户端证书认证叫作TLS双向认证,也就是服务器客户端互相验证证书的正确性,在都正确的情况下协调通信加密方案。

为了使用这个方案,api-server需要用-client-ca-file=选项来开启。CA_CERTIFICATE_FILE肯定包括一个或者多个认证中心,可以被用来验证呈现给api-server的客户端证书。客户端证书的/CN将作为用户名。

静态Token文件

用token唯一标识请求者,只要apiserver存在该token,则认为认证通过,但是如果需要新增Token,则需要重启kube-apiserver组件,实际效果不是很好。

当在命令行指定- -token-auth-file=SOMEFILE选项时,API服务器从文件中读取 bearer tokens。目前,tokens持续无限期。

令牌文件是一个至少包含3列的csv文件: token, user name, user uid,后跟可选的组名。注意,如果您有多个组,则列必须是双引号,例如:

token,user,uid,"group1,group2,group3"

当通过客户端使用 bearer token 认证时,API服务器需要一个值为Bearer THETOKEN的授权头。bearer token必须是,可以放在HTTP请求头中且值不需要转码和引用的一个字符串。例如:如果bearer token是31ada4fd-adec-460c-809a-9e56ceb75269,它将会在HTTP头中按下面的方式呈现:

Authorization: Bearer 31ada4fd-adec-460c-809a-9e56ceb75269

引导Token

在v1.6版本中,这个特性还是alpha特性。为了能够在新的集群中使用bootstrapping认证。Kubernetes包括一种动态管理的Bearer(持票人) token,这种token以Secrets的方式存储在kube-system命名空间中,在这个命名空间token可以被动态的管理和创建。Controller Manager有一个管理中心,如果token过期了就会删除。

创建的token证书满足[a-z0-9]{6}.[a-z0-9]{16}格式,Token的第一部分是一个Token ID,第二部分是token的秘钥。你需要在http协议头中加上类似的信息:

Authorization: Bearer 781292.db7bc3a58fc5f07e

如果要使用Bootstrap,需要在API Sever中开启–experimental-bootstrap-token-auth。同时必须在Controller Manager中开启管理中心的设置–controllers=*,tokencleaner。

在使用kubeadm部署Kubernetes时,kubeadm会自动创建默认token,可通过kubeadm token list命令查询。

静态密码文件

静态密码的方式是提前在某个文件中保存了用户名和密码的信息,然后在 apiserver 启动的时候通过参数 –basic-auth-file=SOMEFILE 指定文件的路径。apiserver 一旦启动,加载的用户名和密码信息就不会发生改变,任何对源文件的修改必须重启 apiserver 才能生效。

静态密码文件是 CSV 格式的文件,每行对应一个用户的信息,前面三列密码、用户名、用户 ID 是必须的,第四列是可选的组名(如果有多个组,必须用双引号):

password,user,uid,"group1,group2,group3"

客户端在发送请求的时候需要在请求头部添加上 Authorization 字段,对应的值是 Basic BASE64ENCODED(USER:PASSWORD) 。apiserver 解析出客户端提供的用户名和密码,如果和文件中的某一行匹配,就认为认证成功。

注意:

这种方式很不灵活,也不安全,可以说名存实亡,不推荐使用。

Service Account Tokens 认证

有些情况下,我们希望在 pod 内部访问 apiserver,获取集群的信息,甚至对集群进行改动。针对这种情况,kubernetes 提供了一种特殊的认证方式:Service Account。 Service Account 是面向 namespace 的,每个 namespace 创建的时候,kubernetes 会自动在这个 namespace 下面创建一个默认的 Service Account;并且这个 Service Account 只能访问该 namespace 的资源。Service Account 和 pod、service、deployment 一样是 kubernetes 集群中的一种资源,用户也可以创建自己的 serviceaccount。

ServiceAccount 主要包含了三个内容:namespace、Token 和 CA。namespace 指定了 pod 所在的 namespace,CA 用于验证 apiserver 的证书,token 用作身份验证。它们都通过 mount 的方式保存在 pod 的文件系统中,其中 token 保存的路径是 /var/run/secrets/kubernetes.io/serviceaccount/token ,是 apiserver 通过私钥签发 token 的 base64 编码后的结果; CA 保存的路径是 /var/run/secrets/kubernetes.io/serviceaccount/ca.crt ,namespace 保存的路径是 /var/run/secrets/kubernetes.io/serviceaccount/namespace ,也是用 base64 编码。

如果 token 能够通过认证,那么请求的用户名将被设置为 system:serviceaccount:(NAMESPACE):(SERVICEACCOUNT) ,而请求的组名有两个: system:serviceaccounts 和 system:serviceaccounts:(NAMESPACE)。

关于 Service Account 的配置可以参考官方的 Manager Service Accounts 文档。

OpenID Connect Tokens 认证

OpenID Connect 是一些由OAuth2提供商支持的OAuth2,特别是Azure Active Directory,Salesforce和Google。OAuth2的协议的主要扩展是增加一个额外字段,返回了一个叫ID token的access token。这个token是被服务器签名的JSON Web Token (JWT) ,具有众所周知的字段,比如用户的email。

为了识别用户,验证使用来自OAuth2 token响应的id_token (而不是 access_token)作为bearer token。token如何包含在请求中可以参考下图:

未分类

使用OpenID认证,API Server需要配置
– –oidc-issuer-url,如https://accounts.google.com
– –oidc-client-id,如kubernetes
– –oidc-username-claim,如sub
– –oidc-groups-claim,如groups
– –oidc-ca-file,如/etc/kubernetes/ssl/kc-ca.pem

Webhook Token 认证

Webhook Token 认证方式可以让用户使用自己的认证方式,用户只需要按照约定的请求格式和应答格式提供 HTTPS 服务,当用户把 Bearer Token 放到请求的头部,kubernetes 会把 token 发送给事先配置的地址进行认证,如果认证结果成功,则认为请求用户合法。 这种方式下有两个参数可以配置:

–authentication-token-webhook-config-file :kubeconfig 文件说明如果访问认证服务器

–authentication-token-webhook-cache-ttl :认证结果要缓存多久,默认是两分钟
这种方式下,自定义认证的请求和应答都有一定的格式,具体的规范请参考 官方文档的说明 。

认证代理

API Server需要配置

–requestheader-username-headers=X-Remote-User
–requestheader-group-headers=X-Remote-Group
–requestheader-extra-headers-prefix=X-Remote-Extra-
#为了防止头部欺骗,证书是必选项
–requestheader-client-ca-file
#设置允许的CN列表。可选。
–requestheader-allowed-names

Keystone Password 认证

Keystone 是 OpenStack 提供的认证和授权组件,这个方法对于已经使用 openstack 来搭建 Iaas 平台的公司比较适用,直接使用 keystone 可以保证 Iaas 和 Caas 平台保持一致的用户体系。

需要API Server在启动时指定–experimental-keystone-url=,而https时还需要设置–experimental-keystone-ca-file=SOMEFILE。

匿名请求

如果请求没有通过以上任何方式的认证,正常情况下应该是直接返回 401 错误。但是 kubernetes 还提供另外一种选择,给没有通过认证的请求一个特殊的用户名 system:anonymous 和组名 system:unauthenticated 。

这样的话,可以跟下面要讲的授权结合起来,为匿名请求设置一些特殊的权限,比如只能读取当前 namespace 的 pod 信息,方便用户访问。

如果使用AlwaysAllow以外的认证模式,则匿名请求默认开启,但可用–anonymous-auth=false禁止匿名请求。