Kubernetes 应用故障的一些定位方法

常备工作

准备一个工具镜像

其中包含 nslookup, ping, curl, 甚至是 ab、siege 等常用工具以及一个顺手的 Shell。一言不合就可以用静态 Pod 的方式将其运行到 Kubernetes 之中进行内部诊断。

sysctl -a | grep forwarding

你猜这是干啥的?

服务状态查询

各个 Kubernetes 组件的状态检查。可以使用 Ansible 之类的工具进行快速查询。

Service 不通

这里我们首先假设 Pod 工作正常

目前我们的应用均采用的是 NodePort 模式对外提供服务:

  • 逻辑:Service 将 符合其选择器的 Pod 暴露的端口 从 各个 Node 的同一端 口暴露出来对外进行监听。

  • 技术:Kube-proxy 通过网络插件,一般利用 Iptables vxLan 等乌七八糟的蜜汁技术,完成对外服务负载均衡,并分发给各个 Pod 的内部 IP 的相应端口。

前面我们假设 Pod 是正常工作的,因此,这里只考虑 Service 的情况。

通过上面的陈述我们能看到大致的一些要素,下面从内向外进行列表:

Pod 能够正常工作

见后文

Service 的选择器能够正确的找到 Pod

这里我们可以使用kubectl describe svc panic-service命令,查看输出内容的endpoint一节内容,如果其中有 Pod 地址,也就说明选择器和 Pod 的标签是匹配的。如果为空,则需要对服务或者 Pod Controller 的定义进行排查。

Proxy 的工作状态

  • 首先可以使用systemctl -l Kube-proxy来查看服务状况。

  • 还可以使用其他 Node 的同一端口测试访问,看是否单一节点的故障。

DNS 工作状态

Kubectl 查看 DNS 各个 Pod 的存活状态。

利用上面提到的工具 Pod 尝试解析服务。失败了其实也没啥办法,删 DNS Pod 重启吧。

端口是否定义正确

看 Pod 的端口是否能够正确侦听,是否符合服务定义。例如 Service 定义了到 Pod 8080 端口的访问,而 Pod 开放的却是 80,这样的情况跟标签无法匹配一样,是很常见的问题。

说完了服务,我们来说说 Pod

两个顺手的命令

kubectl get po -o wide | grep -v Running kubectl describe po unhealthy

一般来说,一个行为端正的 Pod,应该是以 Running 状态持续运行的。在进入 Running 之前,大致有调度、创建、初始化等几个环节,如果正常运行之后出了故障,会发生重启。如果在启动容器内进程时出现问题,则会进入 CrashLoopBackOff 的状态。

除了 Running/Complet 以及 CrashLoopBackOff

这几种情况其实不同,不过随性写到这,就不深究了,首先是 describe 一下。

Pod 启动有几个条件:

  • 有符合要求的节点供其运行
    • Taint 隔离的节点,要求 Pod 有显式声明对该种 Taint 的容错能力,才可以在其上运行。
    • 节点和 Pod 的亲和性定义
    • Node Selector 的定义
  • 符合其需求的资源
    • CPU 和 内存的 request limit 定义
    • 可能存在的第三方资源需求定义
    • 加载卷(nfs gluster ceph 等)/Secret/Configmap 的定义
  • 镜像必须存在,可 Pull

调度部分一般来说查看 Pod 定义,和节点的 Describe 进行匹配即可,Describe 内容中也会明确说出无合适 Pod。

资源部分 CPU 和内存的 Describe 结果也会很明显。

存储部分,往往就需要更复杂的排查:

  • 首先看看是不是每个 Node 都如此。

  • 是否安装了对应的客户端驱动。

  • 对分布式存储的访问网络是否可用。

  • 存储服务容量是否足够分配。

  • 是否能够成功的手工 Mount。

至于对 ConfigMap 和 Secret 的依赖,很简单,Kubectl 查询即可。

CrashLoopBackOff 以及 Restart 大于 1

这种情况一般来说属于业务内部的问题,可以通过 kubectl logs -f 命令进行查看,目前经验比较多的非业务情况是:

  • 对于 Kubernetes API 进行访问的应用,经常会是因为RBAC 权限不足导致无法启动

  • 依赖的 Service 无法访问。

Kubernetes主机间curl cluster ip时通时不通

1. 问题现象

测试部署了一个service,包括2个pod,分别在node1和node2上。

[root@k8s ~]# kubectl get svc 
NAME        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
nginx-svc   10.254.216.26   <none>        80/TCP    43m
[root@k8s ~]# kubectl get pod
NAME                                READY     STATUS    RESTARTS   AGE
busybox                             1/1       Running   3          65d
nginx-deployment-4087004473-d3tkv   1/1       Running   0          9m
nginx-deployment-4087004473-gdm3q   1/1       Running   0          54m

在node1(或者node2)上curl 10.254.216.26(cluster ip),只有当负载到本地pod时正常,curl <本地pod ip和另一个主机pod ip>是正常的。

2. 解决方案

参考 https://stackoverflow.com/questions/34639185/communication-failing-between-kubernetes-nodes-and-clusterip 添加路由后,

ip route add 10.254.0.0/16 dev flannel.1

主机间可以通信,但是本地pod不正常了。

删除上述路由信息,添加路由如下路由解决。

ip route add 10.254.0.0/16 dev docker0

k8s部署之使用CFSSL创建证书

一、安装CFSSL

curl -s -L -o /bin/cfssl https://pkg.cfssl.org/R1.2/cfssl_linux-amd64
curl -s -L -o /bin/cfssljson https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64
curl -s -L -o /bin/cfssl-certinfo https://pkg.cfssl.org/R1.2/cfssl-certinfo_linux-amd64
chmod +x /bin/cfssl*

二、容器相关证书类型

  • client certificate: 用于服务端认证客户端,例如etcdctl、etcd proxy、fleetctl、docker客户端
  • server certificate: 服务端使用,客户端以此验证服务端身份,例如docker服务端、kube-apiserver
  • peer certificate: 双向证书,用于etcd集群成员间通信

三、创建CA证书

1、生成默认CA配置

mkdir /opt/ssl
cd /opt/ssl
cfssl print-defaults config > ca-config.json
cfssl print-defaults csr > ca-csr.json

修改ca-config.json,分别配置针对三种不同证书类型的profile,其中有效期43800h为5年

{
    "signing": {
        "default": {
            "expiry": "43800h"
        },
        "profiles": {
            "server": {
                "expiry": "43800h",
                "usages": [
                    "signing",
                    "key encipherment",
                    "server auth"
                ]
            },
            "client": {
                "expiry": "43800h",
                "usages": [
                    "signing",
                    "key encipherment",
                    "client auth"
                ]
            },
            "peer": {
                "expiry": "43800h",
                "usages": [
                    "signing",
                    "key encipherment",
                    "server auth",
                    "client auth"
                ]
            }
        }
    }
}

修改ca-csr.config

{
    "CN": "Self Signed Ca",
    "key": {
        "algo": "rsa",
        "size": 2048
    },
    "names": [
        {
            "C": "CN",
            "L": "SH",
            "O": "Netease",
            "ST": "SH",            
            "OU": "OT"
        }    ]
}

生成CA证书和私钥

cfssl gencert -initca ca-csr.json | cfssljson -bare ca -
生成ca.pem、ca.csr、ca-key.pem(CA私钥,需妥善保管)

2、签发Server Certificate

cfssl print-defaults csr > server.json
vim server.json
{
    "CN": "Server",
    "hosts": [
        "192.168.1.1"
       ],
    "key": {
        "algo": "ecdsa",
        "size": 256
    },
    "names": [
        {
            "C": "CN",
            "L": "SH",
            "ST": "SH"
        }
    ]
}
生成服务端证书和私钥
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=server server.json | cfssljson -bare server

3、签发Client Certificate

cfssl print-defaults csr > client.json
vim client.json
{
    "CN": "Client",
    "hosts": [],
    "key": {
        "algo": "ecdsa",
        "size": 256
    },
    "names": [
        {
            "C": "CN",
            "L": "SH",
            "ST": "SH"
        }
    ]
}
生成客户端证书和私钥
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=client client.json | cfssljson -bare client

4、签发peer certificate

cfssl print-defaults csr > member1.json
vim member1.json
{
    "CN": "member1",
    "hosts": [
        "192.168.1.1"
    ],
    "key": {
        "algo": "ecdsa",
        "size": 256
    },
    "names": [
        {
            "C": "CN",
            "L": "SH",
            "ST": "SH"
        }
    ]
}
为节点member1生成证书和私钥:
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=peer member1.json | cfssljson -bare member1
针对etcd服务,每个etcd节点上按照上述方法生成相应的证书和私钥

5、最后校验证书

校验生成的证书是否和配置相符

openssl x509 -in ca.pem -text -noout
openssl x509 -in server.pem -text -noout
openssl x509 -in client.pem -text -noout

四、k8s集群所需证书

未分类

iptables中DNAT、SNAT和MASQUERADE的理解

DNAT(Destination Network Address Translation,目的地址转换) 通常被叫做目的映谢。而SNAT(Source Network Address Translation,源地址转换)通常被叫做源映谢。

这是我们在设置Linux网关或者防火墙时经常要用来的两种方式。以前对这两个都解释得不太清楚,现在我在这里解释一下。

首先,我们要了解一下IP包的结构,如下图所示:

未分类

在任何一个IP数据包中,都会有Source IP Address与Destination IP Address这两个字段,数据包所经过的路由器也是根据这两个字段是判定数据包是由什么地方发过来的,它要将数据包发到什么地方去。而iptables的DNAT与SNAT就是根据这个原理,对Source IP Address与Destination IP Address进行修改。

然后,我们再看看数据包在iptables中要经过的链(chain):

未分类

图中正菱形的区域是对数据包进行判定转发的地方。在这里,系统会根据IP数据包中的destination ip address中的IP地址对数据包进行分发。如果destination ip adress是本机地址,数据将会被转交给INPUT链。如果不是本机地址,则交给FORWARD链检测。

这也就是说,我们要做的DNAT要在进入这个菱形转发区域之前,也就是在PREROUTING链中做,比如我们要把访问202.103.96.112的访问转发到192.168.0.112上:

iptables -t nat -A PREROUTING -d 202.103.96.112 -j DNAT --to-destination 192.168.0.112

这个转换过程当中,其实就是将已经达到这台Linux网关(防火墙)上的数据包上的destination ip address从202.103.96.112修改为192.168.0.112然后交给系统路由进行转发。

而SNAT自然是要在数据包流出这台机器之前的最后一个链也就是POSTROUTING链来进行操作

iptables -t nat -A POSTROUTING -s 192.168.0.0/24 -j SNAT --to-source 58.20.51.66

这个语句就是告诉系统把即将要流出本机的数据的source ip address修改成为58.20.51.66。这样,数据包在达到目的机器以后,目的机器会将包返回到58.20.51.66也就是本机。如果不做这个操作,那么你的数据包在传递的过程中,reply的包肯定会丢失。

假如当前系统用的是ADSL/3G/4G动态拨号方式,那么每次拨号,出口IP都会改变,SNAT就会有局限性。

iptables -t nat -A POSTROUTING -s 192.168.0.0/24 -o eth0 -j MASQUERADE

重点在那个『 MASQUERADE 』!这个设定值就是『IP伪装成为封包出去(-o)的那块装置上的IP』!不管现在eth0的出口获得了怎样的动态ip,MASQUERADE会自动读取eth0现在的ip地址然后做SNAT出去,这样就实现了很好的动态SNAT地址转换。

Jenkins集成Docker

大概过程如下图:

未分类

由于需要用到docker打包镜像,jenkins宿主机上需要安装docker,原先的jenkins server安装在centos6上无法运行docker,所以这里单独用一台centos7安装一个jenkins server。

jenkins的安装方法有多种:

1、下载jar包,直接通过java运行。

2、用tomcat作为容器运行。

3、通过yum安装。

4、通过docker运行。

安装过程都很简单,这里就不详细说明了。

1、安装插件。打开jenkins页面,安装CloudBees Docker Build and Publish plugin和Publish Over SSH Plugin插件。

未分类

2、新建一个项目。

未分类

3、配置项目。

未分类

4、填写项目名称。jenkins的工作空间与这里的项目名称对应,默认在/var/lib/jenkins/workspace下。

未分类

5、拉取代码。填写项目地址和需要拉取的分支,这里用了一个Additional Behabiours,拉取代码会自动创建一个目录并将代码放在该目录下。

未分类

6、build镜像,并将镜像push到harbor镜像库。填写相应的docker、dockerfile以及镜像库地址信息。

这里需要设置docker启动参数。

#vim /etc/sysconfig/docker
OPTIONS='--insecure-registry 172.60.0.107 -H 0.0.0.0:2375 -H unix:///var/run/docker.sock'
#systemctl restart docker

未分类

7、通过脚本发布到k8s。这里需要设置SSH互信(具体过程略..)。

未分类

8、在k8s master上编写发布脚本。

#vim stg-che001-56-waybill.sh

#!/bin/bash

#update the version number
sed -ri "s@(image.*v).*@1${BUILD_NUMBER}@" /root/che001-56-stg/che001-56-waybill.yaml

#Apply the configuration change
kubectl delete -f /root/che001-56-stg/che001-56-waybill.yaml && kubectl create -f /root/che001-56-stg/che001-56-waybill.yaml

9、配置完成后,点击立即构建。

未分类

10、点击Console Output可以查看构建日志。如果最后提示SUCCESS,则表示构建成功。(这里日志太长,只截取了一半)

未分类

未分类

hadoop集群时间同步

测试环境:

192.168.217.130 master master.hadoop
192.168.217.131 node1 node1.hadoop
192.168.217.132 node2 node2.hadoop

一、设置master服务器时间

查看本地时间和时区

[root@master ~]# date
Mon Feb 27 09:54:09 CST 2017

选择时区

[root@master ~]# tzselect

未分类

未分类

 [root@master ~]# cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

修改时间(date -s 00:00:00或者网络同步:apt-get install ntpdate ; ntpdate cn.pool.ntp.org)
写入硬盘时间(hwclock -w)

二、在master服务器上检查时间服务是否安装

[root@node1 ~]# rpm -q ntp
ntp-4.2.4p8-2.el6.x86_64

如果没有安装,用yum安装

[root@node1 ~]# yum install ntp

按上面的安装方式在内网每台服务器上都安装好NTP软件包。
完成后,都需要配置NTP服务为自启动

[root@master ~]# chkconfig ntpd on
[root@master ~]# chkconfig --list ntpd
ntpd           0:off1:off2:on3:on4:on5:on6:off

三、在master上更改相关配置文件

[root@master ~]# vim /etc/ntp.conf 

进行如下修改:

未分类

第一处新增,意思是从IP地址192.168.217.1-192.168.217.254,默认网关255.255.255.0的服务器都可以使用我们的NTP服务器来同步时间

第二处新增,指明互联网和局域网中作为NTP服务器的IP

第三处是修改,将原有注释去掉,是当服务器与公用的时间服务器失去联系时以本地时间为客户端提供时间服务

配置文件修改完成,保存退出,启动服务。

[root@master ~]#  service ntpd start

启动后,一般需要5-10分钟左右的时候才能与外部时间服务器开始同步时间。可以通过命令查询NTPD服务情况。

查看服务连接和监听

[root@master ~]# netstat -tlunp | grep ntp  
udp        0      0 192.168.217.130:123         0.0.0.0:*                               4990/ntpd           
udp        0      0 127.0.0.1:123               0.0.0.0:*                               4990/ntpd           
udp        0      0 0.0.0.0:123                 0.0.0.0:*                               4990/ntpd           
udp        0      0 fe80::20c:29ff:fee7:123     :::*                                    4990/ntpd           
udp        0      0 ::1:123                     :::*                                    4990/ntpd           
udp        0      0 :::123                      :::*                                    4990/ntpd 

重新启动服务

[root@master ~]# service ntpd restart

可设置crontab每天和NTP服务器同步一次(以和互联网时间同步为例)

[root@master ~]# crontab -l
10 23 * * * root (/usr/sbin/ntpdate cn.pool.ntp.org && /sbin/hwclock -w) &> /var/log/ntpdate.log

四、 将其他节点的时间与master进行同步

在其他每一个节点运行命令

[root@node1 ~]# ntpdate master
27 Feb 10:10:15 ntpdate[32724]: adjust time server 192.168.217.130 offset -0.170230 sec
[root@node2 ~]# ntpdate master
27 Feb 10:10:23 ntpdate[30874]: adjust time server 192.168.217.130 offset -0.149563 sec

这时候发现节点间的时间同步了,但ntpdate只在开机运行,我们若要设置为1小时同步一次

[root@node1 ~]# crontab -l
* */1 * * * /usr/sbin/ntpdate master

Haproxy基于ACL做访问控制

haproxy配置文档 https://cbonte.github.io/haproxy-dconv/

基于ACL做访问控制(四层代理)

网络拓扑

未分类

环境

前端HAProxy 172.16.253.108
后端web1    172.16.253.105
后端web2    172.16.252.1
client      172.16.253.177

安装HAProxy

HAProxy

[root@HAProxy ~]# yum install haproxy -y
[root@HAProxy ~]# rpm -ql haproxy
[root@HAProxy ~]# iptables -F
[root@HAProxy ~]# setenforce 0
[root@HAProxy ~]# systemctl enable haproxy
[root@HAProxy ~]# cp /etc/haproxy/haproxy.cfg{,.bak}
[root@HAProxy ~]# vim /etc/haproxy/haproxy.cfg

web1

[root@web1 ~]# yum -y install httpd
[root@web1 ~]# vim /var/www/html/index.html 
<h1> Backend Server 1 </h1>
[root@web1 ~]# systemctl start httpd
[root@web1 ~]# setenforce 0
[root@web1 ~]# iptables -F

web 2

[root@web2 ~]# yum -y install httpd
[root@web2 ~]# vim /var/www/html/index.html 
<h1> Backend Server 2 </h1>
[root@web2 ~]# service httpd start 
[root@web2 ~]# setenforce 0
[root@web2 ~]# iptables -F
  • block阻塞主机访问

172.16.251.196用户访问stats状态界面,并显示错误网页http://172.16.253.108:10080/403.html

HAProxy

[root@HAProxy ~]# vim /etc/haproxy/haproxy.cfg
    frontend myweb *:80
        default_backend websrvs

    backend websrvs
        balance roundrobin
        server srv1 172.16.253.105:80 check weight 2
        server srv2 172.16.252.1:80 check weight 1
    listen stats
        bind *:9000
        acl allowstats src 172.16.251.196
        block if allowstats  \阻塞allowstats中的IP访问stats界面
        errorloc 403 http://172.16.253.108:10080/403.html
        stats enable
        stats uri /myproxy?admin
        stats realm "HAProxy Stats Page"
        stats auth admin:admin
        stats admin if TRUE
[root@HAProxy ~]# systemctl restart haproxy 

访问测试

172.16.251.196使用浏览器访问测试http://172.16.253.108:10080/403.html 
  • http-request允许某主机访问stats状态界面

允许172.16.251.196用户访问http://172.16.253.108服务器的HAProxy的状态界面

HAProxy

[root@HAProxy ~]# vim /etc/haproxy/haproxy.cfg
    frontend myweb *:80
        default_backend websrvs

    backend websrvs
        balance roundrobin
        server srv1 172.16.253.105:80 check weight 2
        server srv2 172.16.252.1:80 check weight 1
    listen stats
        bind *:9000
        acl allowstats src 172.16.251.196
        # http-request allow if allowstats  \允许allowstats中的IP访问stats状态界面
        http-request deny  unless allowstats \除了allowstats之外全部拒绝访问,即仅允许allowstats访问
        # http-request deny if allowstats \拒绝allowstats访问
        errorloc 403 http://172.16.253.108:10080/403.html \错误网页文件
        stats enable
        stats uri /myproxy?admin
        stats realm "HAProxy Stats Page"
        stats auth admin:admin
        stats admin if TRUE
[root@HAProxy ~]# systemctl restart haproxy 

访问测试

图形化浏览器
    172.16.251.196使用浏览器访问测试http://172.16.253.108:10080/403.html    
字符界面 
    [root@client ~]# curl --basic --user admin:admin http://172.16.253.108:9000/myproxy?admin   

基于ACL做访问控制(七层代理)

动态网页存放在动态服务器组中,静态网页存放在静态服务器组中

拓扑环境

环境

前端HAProxy 172.16.253.108
后端web1    172.16.253.105
后端web2    172.16.253.191
client      172.16.253.177
  • web1使用虚拟主机技术搭建两个web server,用来存放动态网页内荣容
  • web2使用虚拟主机搭建两个web server用来替代静态网页内容

web1创建虚拟主机

[root@web1 ~]# yum -y install php httpd
[root@web1 ~]# mkdir /data/web/vhost{1,2} -pv
[root@web1 ~]# vim /data/web/vhost1/index.php
<h1> Application Server 1</h1>
<?php
    phpinfo();
?>
[root@web1 ~]# vim /data/web/vhost2/index.php
<h1> Application Server 2</h1>
<?php
    phpinfo();
?>

虚拟主机1的配置文件
[root@web1 ~]# vim /etc/httpd/conf.d/vhost1.conf \编辑vhost1虚拟主机的配置文件
<VirtualHost *:80>
    ServerName www1.danran.com
    DocumentRoot "/data/web/vhost1"
    <Directory "/data/web/vhost1">
            Options FollowSymLinks \允许使用连接文件目录
            AllowOverride None \不允许其他配置文件覆盖此文件中的设置
            Require all granted
    </Directory>
</VirtualHost>

虚拟主机2的配置文件
[root@web1 ~]# vim /etc/httpd/conf.d/vhost2.conf
[root@web1 ~]# vim /etc/httpd/conf.d/vhost2.conf
Listen 8080
<VirtualHost *:8080>
    ServerName www2.danran.com
    DocumentRoot "/data/web/vhost2"
    <Directory "/data/web/vhost2">
            Options FollowSymLinks
            AllowOverride None
            Require all granted
    </Directory>
</VirtualHost>

[root@web1 ~]# systemctl restart httpd.service 
[root@web1 ~]# ss -ntl

web2创建虚拟主机

[root@web2 ~]# yum -y install httpd
[root@web2 ~]# mkdir -pv /data/web/vhost{1,2}
[root@web2 ~]# find /usr/share/ -iname "*.jpg" -exec cp {} /data/web/vhost1/ ;
[root@web2 ~]# find /usr/share/ -iname "*.jpg" -exec cp {} /data/web/vhost2/ ;
[root@web2 ~]# vim /data/web/vhost1/index.html
<h1> Image Server 1 </h1>
[root@web2 ~]# vim /data/web/vhost2/index.html
<h1> Image Server 2 </h1>

编辑虚拟主机1的配置文件
[root@web2 ~]# vim  /etc/httpd/conf.d/vhost1.conf 
<VirtualHost *:80>
    ServerName www1.danran.com
    DocumentRoot "/data/web/vhost1"
    <Directory "/data/web/vhost1">
            Options FollowSymLinks
            AllowOverride None
            Require all granted
    </Directory>
</VirtualHost>

编辑虚拟主机2的配置文件
[root@web2 ~]# vim  /etc/httpd/conf.d/vhost2.conf 
Listen 8080
<VirtualHost *:8080>
    ServerName www2.danran.com
    DocumentRoot "/data/web/vhost1"
    <Directory "/data/web/vhost1">
            Options FollowSymLinks
            AllowOverride None
            Require all granted
    </Directory>
</VirtualHost>

[root@web2 ~]# systemctl start httpd.service 

HAProxy

[root@HAProxy ~]# vim /etc/haproxy/haproxy.cfg
    frontend myweb *:80
        cookie WEBSRV indirect nocache
        acl static path_end .jpg .jpeg .png .gif .txt .html \定义ACL的组static以.jpg .jpeg .png .gif .txt .html结尾的文件
        use_backend staticsrvs  if static  \当符合条件时使用static主机组
        default_backend dynsrvs  \当不符合use_bckend条件时使用默认default_backend主机组

        backend dynsrvs \定义动态主机组
            balance roundrobin
            server dynsrv1 172.16.253.105:80 check cookie dynsrv1
            server dynsrv2 172.16.253.105:8080 check cookie dynsrv2
        backend staticsrvs  \定义静态主机组
            balance roundrobin
            server staticsrv1 172.16.253.191:80 check
            server staticsrv2 172.16.253.191:8080 check
[root@HAProxy ~]# systemctl restart haproxy

client

[root@client ~]# curl http://172.16.253.108/index.html
<h1> Image Server 1 </h1>
[root@client ~]# curl http://172.16.253.108/index.html
<h1> image Server 2 </h1>
[root@client ~]# curl http://172.16.253.108/index.php
<h1> Application Server 2</h1>
[root@client ~]# curl http://172.16.253.108/index.php
<h1> Application Server 2</h1>

拒绝curl访问web

HAProxy

[root@HAProxy ~]# vim /etc/haproxy/haproxy.cfg
    frontend myweb *:80
        cookie WEBSRV indirect nocache
        acl static path_end .jpg .jpeg .png .gif .txt .html \定义ACL的组static以.jpg .jpeg .png .gif .txt .html结尾的文件
        use_backend staticsrvs  if static  \当符合条件时使用static主机组
        default_backend dynsrvs  \当不符合use_bckend条件时使用默认default_backend主机组
        acl bad_browsers hdr_reg(User-Agent) .*curl.* \定义请求报文中包含curl的ACL组为bad_browsers
        block if bad_browsers \阻塞bad_browsers组的访问

        backend dynsrvs \定义动态主机组
            balance roundrobin
            server dynsrv1 172.16.253.105:80 check cookie dynsrv1
            server dynsrv2 172.16.253.105:8080 check cookie dynsrv2
        backend staticsrvs  \定义静态主机组
            balance roundrobin
            server staticsrv1 172.16.253.191:80 check
            server staticsrv2 172.16.253.191:8080 check
[root@HAProxy ~]# systemctl restart haproxy

client

[root@client ~]# curl http://172.16.253.108/index.html
<html><body><h1>403 Forbidden</h1>
Request forbidden by administrative rules.
</body></html>

定义仅允许danran.com域内的的主机访问

HAProxy

[root@HAProxy ~]# vim /etc/haproxy/haproxy.cfg
    frontend myweb *:80
        cookie WEBSRV indirect nocache
        acl static path_end .jpg .jpeg .png .gif .txt .html \定义ACL的组static以.jpg .jpeg .png .gif .txt .html结尾的文件
        use_backend staticsrvs  if static  \当符合条件时使用static主机组
        default_backend dynsrvs  \当不符合use_bckend条件时使用默认default_backend主机组
        acl valid_referers hdr_reg(Referer) .danran.com
        block unless valid_referers \阻塞除了valid_referers组之外的所有人的访问

        backend dynsrvs \定义动态主机组
            balance roundrobin
            server dynsrv1 172.16.253.105:80 check cookie dynsrv1
            server dynsrv2 172.16.253.105:8080 check cookie dynsrv2
        backend staticsrvs  \定义静态主机组
            balance roundrobin
            server staticsrv1 172.16.253.191:80 check
            server staticsrv2 172.16.253.191:8080 check
[root@HAProxy ~]# systemctl restart haproxy

client

模拟www.danran.com主机访问
[root@client ~]# curl -e "http://www.danran.com/index.php" http://172.16.253.108/index.php 
<h1> Application Server 2</h1>

Flask-Login 使用和进阶

在我们使用 Flask 构建一个系统时,用户登录注册是一个必不可少的过程,我们可以自己实现一个登录的功能,但是由于涉及的面很多,所以我们更多情况下还是寻求已有的模块来进行使用。

在 Flask 中比较常用的就是 Flask-Login 了,这里就以 Flask-Login 为例,给大家介绍一下如何使用Flask-Login 进行登录注销,以及帮助大家解答一些可能比较常见的问题。

使用入门

首先,我们先概述一下我们这里的例子,我们这个例子有三个 url,分别是:

/auth/login     用于登录
/auth/logout    用于注销
/test               用于测试,需要登录才能访问

好,这就是我们的前提概述了,下面我们就开始介绍。

安装必要的库

毫无疑问,我们要使用 Flask-Login ,那就必须安装它,安装使用 pip 还是很简单的,此外,因为我们登录涉及到登录表单,所以,还需要安装 wtform ,所以最后总共需要安装的有:

pip install Flask==0.10.1
pip install Flask-Login==0.3.2
pip install Flask-WTF==0.12
pip install WTForms==2.1

编写 web 框架

首先,在开始登录之前,我们先把整个 web 的框架搭建出来,也就是,我们要能够先在不登录的情况下访问到上面提到的三个url,这个架构比较简单了,我就直接放在一个叫做 app.py 的文件中了。

#!/usr/bin/env python
# encoding: utf-8
from flask import Flask, Blueprint

app = Flask(__name__)

# url redirect
auth = Blueprint('auth', __name__)

@auth.route('/login', methods=['GET', 'POST'])
def login():
    return "login page"

@auth.route('/logout', methods=['GET', 'POST'])
def logout():
    return "logout page"    

# test method
@app.route('/test')
def test():
    return "yes , you are allowed"

app.register_blueprint(auth, url_prefix='/auth')
app.run(debug=True)

现在,我们可以尝试一下运行一下这个框架,使用

python app.py

运行即可,然后打开浏览器,分别访问一下:

http://localhost:5000/test
http://localhost:5000/auth/login
http://localhost:5000/auth/logout

看一下是否都正常。

设置登录才能查看

现在框架已经设置完毕,那么我们就可以尝试一下设置登录需求的,也就是说我们将 test 和 auth/logout 这两个 page 设置成登录之后才能查看。因为这个功能已经和 login 有关系了,所以这时我们就需要使用到 Flask-Login 了。

我们可以这样来改变代码:

#!/usr/bin/env python
# encoding: utf-8
from flask import Flask, Blueprint
from flask.ext.login import LoginManager, login_required

app = Flask(__name__)

# 以下这段是新增加的============
app.secret_key = 's3cr3t'
login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'auth.login'
login_manager.init_app(app)

@login_manager.user_loader
def load_user(user_id):
    return None
# 以上这段是新增加的============

auth = Blueprint('auth', __name__)

@auth.route('/login', methods=['GET', 'POST'])
def login():
    return "login page"

@auth.route('/logout', methods=['GET', 'POST'])
@login_required
def logout():
    return "logout page"

# test method
@app.route('/test')
@login_required
def test():
    return "yes , you are allowed"

app.register_blueprint(auth, url_prefix='/auth')
app.run(debug=True)

其实我们就增加了两项代码,一项是初始化 LoginManager 的,另外一项就是给 test 和 auth.logout 添加了 login_required 的装饰器,表示要登录了才能访问。

你也许会有疑问:@login_manager.user_loader 这个装饰器是干嘛用的。这个在后面的 Question 中有详细得介绍,在这里我们只需要知道这个函数需要返回指定 id 的用户,如果没有就返回 None。这里因为设置框架所以就默认返回 None。

用户授权

到此,我们发现访问 test 是不能访问的,会被重定向到 login 的那个 page。那我们看一下我们现在的代码,我们发现 login_required 有了,那么就差 login 了,好,接下来就写 login,所以我们就来看看 Flask-Login 的文档,找找,我们会发现一个叫做:login_user 的函数,看看它的原型:

flask.ext.login.login_user(user, remember=False, force=False, fresh=True)

这里需要一个 user 的对象,所以我们就先构建一个 Model,其实,这个 Model 还是有一点讲究的,所以我们最好是继承自 Flask-Login 的 UserMixin , 然后需要实现几个方法, Model 为:

# user models
    class User(UserMixin):
        def is_authenticated(self):
            return True

        def is_actice(self):
            return True

        def is_anonymous(self):
            return False

        def get_id(self):
            return "1"

这里给所有的函数都返回了默认值,默认对应的情况是这个用户已经登录,并且是有效的。

然后在 login 的 view 里面 login_user,logout 的 view 里面 logout_user,这样整个登录过程就连接起来了,最后的代码是这样的:

#!/usr/bin/env python
    # encoding: utf-8
    from flask import Flask, Blueprint
    from flask.ext.login import (LoginManager, login_required, login_user,
                                 logout_user, UserMixin)

    app = Flask(__name__)


    # user models
    class User(UserMixin):
        def is_authenticated(self):
            return True

        def is_actice(self):
            return True

        def is_anonymous(self):
            return False

        def get_id(self):
            return "1"

    # flask-login
    app.secret_key = 's3cr3t'
    login_manager = LoginManager()
    login_manager.session_protection = 'strong'
    login_manager.login_view = 'auth.login'
    login_manager.init_app(app)

    @login_manager.user_loader
    def load_user(user_id):
        user = User()
        return user

    auth = Blueprint('auth', __name__)

    @auth.route('/login', methods=['GET', 'POST'])
    def login():
        user = User()
        login_user(user)
        return "login page"

    @auth.route('/logout', methods=['GET', 'POST'])
    @login_required
    def logout():
        logout_user()
        return "logout page"

    # test method
    @app.route('/test')
    @login_required
    def test():
        return "yes , you are allowed"

    app.register_blueprint(auth, url_prefix='/auth')
    app.run(debug=True)

Summary

到此,这就是一个比较精简的 Flask-Login 教程了,通过这个框架大家可以自行扩展,达到更丰富的功能,后续会连续这个 Login 功能继续讲解一下权限控制。

Question

未登录访问鉴权页面如何处理?

如果未登录访问了一个作了 login_required 限制的 view,那么 Flask-Login 会默认 flash一条消息,并且将重定向到 log in view,如果你没有指定 log in view,那么 Flask-Login 将会抛出一个 401 错误。

如何指定 log in view

指定 log in view 只需要直接设置 login_manager 即可:

login_manager.login_view = "auth.login"

如何自定义 flash 消息

如果需要自定义 flash 的消息,那么还是简单设置 login_manager,

login_manager.login_message = u"请登录!"

还可以设置 flash 消息的级别,一般设置成 info 或者 error:

login_manager.login_message_category = "info"

自定义未登录处理函数

如果你不想使用默认的规则,那么你也可以自定义未登录情况的处理函数,只需要使用 login_manager 的 unauthorized_handler 装饰器即可。

@login_manager.unauthorized_handler
    def unauthorized():
        # do stuff
        return render_template("some template")

匿名用户是怎么处理的?有哪些属性?

在 Flask-Login 中,如果一个匿名用户访问站点,那么 current_user 对象会被设置成一个 AnonymousUserMixin 的对象,AnonymousUserMixin 对象有以下方法和属性:

  • is_active and is_authenticated are False
  • is_anonymous is True
  • get_id() returns None

自定义匿名用户 Model

如果你有需求自定义匿名用户的 Model,那么你可以通过设置 login_manager 的 anonymous_user 属性来实现,而赋值的对象只需是可调用对象(class 和 function都行)即可。

login_manager.anonymous_user = MyAnonymousUser

Flask-Login 如何加载用户的

当一个请求过来的时候,如果 ctx.user 没有值,那么 Flask-Login 就会使用 session 中 session[‘user_id’] 作为参数,调用 login_manager 中使用 user_loader 装饰器设置的 callback 函数加载用户,需要注意的是,如果指定的 user_id 无效,不应该抛出异常,而是应该返回 None。

@login_manager.user_loader
    def load_user(user_id):
        return User.get(user_id)

session[‘user_id’] 其实是在调用 login_in 函数之后自动设置的。

如何控制 Flask-Login 的 session 过期时间

在 Flask-Login 中,如果你不特殊处理的话,session 是在你关闭浏览器之后就失效的。也就是说每次重新打开页面都是需要重新登录的。

如果你需要自己控制 session 的过期时间的话,

  1. 首先需要设置 login_manager 的 session类型为永久的,
  2. 然后再设置 session 的过期时间
session.permanent = True
app.permanent_session_lifetime = timedelta(minutes=5)

同时,还需要注意的是 cookie 的默认有效期其实是 一年 的,所以,我们最好也设置一下:

login_manager.remember_cookie_duration=timedelta(days=1)

如何在同域名下的多个系统共享登录状态

这个需求可能在公司里面会比较常见,也就是说我们一个公司域名下面会有好多个子系统,但是这些子系统都是不同部门开发的,那么,我们如何在这不同系统间共享登录状态?也就是说,只要在某一个系统登录了,在使用其他系统的时候也共享着登录的状态,不需要再次登录,除非登录失效。

Docker Swarm + HAProxy 实现高可用

什么是高可用性?

  • 高可用性: High Availability (简称 HA)
  • 可用性 (Availability) = 可靠性 (Reliability) + 可维护性 (Maintainability)
  • 自动检测、自动切换、自动恢复
  • 主从方式、互备方式、集群方式

集群的三大核心概念

  • 集群 (Cluster)
  • 节点 (Node)
  • 服务 (Service)

本文将介绍 Docker Swarm + HAProxy 来实现服务的高可用性

环境信息

  • 本机 Mac
  • Docker Version 17.06.1-ce-mac24 (18950)

创建节点(虚拟机)

未分类

在 Mac 执行如下命令创建节点

$ docker-machine create manager1
$ docker-machine create work1
$ docker-machine create work2
$ docker-machine create work3

未分类

进入节点(虚拟机)

再开启另外的4个窗口,分别执行

$ docker-machine ssh manager1
$ docker-machine ssh work1
$ docker-machine ssh work2
$ docker-machine ssh work3

未分类

初始化集群

1、初始化 Manager

未分类

在 manager1 节点上执行

docker swarm init --advertise-addr 192.168.99.100`

未分类

命令成功后,会有提示,如何将 worker 加入集群,即 docker swarm … 这行命令。

提示:如果忘记此处的 token,可以使用 docker swarm join-token worker 命令查看。

2、将 work1,work2,work3 加入集群

未分类

在 work1, work2, work3 上分别执行,此行命令详见上一步中的提示

docker swarm join --token SWMTKN-1-1rt31l3671hig69vkfqha994rydnwr67gxt6o0suv71zzobpgn-b930rnc1pzewxv9h73plq58rd 192.168.99.100:2377

未分类

笔记:集群中加入worker的命令是

docker swarm join --token [token] [manager-ip]:[manager-port]

创建服务

此处以 nginx 为例。首先,创建有 2 个副本的 nginx 服务,在 manger1 上执行

$ docker service create --replicas 2 
-d 
-p 8080:80 
--name any-nginx 
registry.docker-cn.com/library/nginx

提示:此处用到了镜像加速仓库

执行完毕之后,查看服务列表信息

$ docker service ls

未分类

查看服务信息

$ docker service ps vv

笔记:查看服务信息命令 docker service ps [服务ID]

未分类

从服务信息可以看到 nginx 已运行在 manager1 和 work1 上

未分类

分别访问 http://192.168.99.100:8080 和 http://192.168.99.101:8080 ,会得到如下结果:

未分类

分别在 manager1 和 work1 上查看容器运行情况,结果如下:

未分类

服务的扩容与缩容

将 any-nginx 扩容为 3 份,在 manager1 上执行

$ docker service scale any-nginx=3

再次查看服务信息,可看到 any-nginx.3 在 work2 上运行

未分类

未分类

服务高可用测试

目前,有3个运行的容器保证服务的可用性,如果其中一个容器意外关闭,会发生什么情况呢?

将 work2 上正在运行的容器关闭,在 work2 上执行:

未分类

再次查看服务信息,可以看到 any-nginx.3 在 work2 上自动重启了

未分类

另外一个问题,如果 work2 宕机了,会发生什么情况?

在 Mac 上关闭 work2 节点:

$ docker-machine stop work2

未分类

未分类

不难看出 work2 节点关闭之后,work3 节点自动启动了 any-nginx.3,保证了整个集群运行的还是 3 个副本。

补充:若work1,work2,work3全部关闭,manager1 上将运行 3 个 any-nginx 容器。此处不演示。

总结:在内存允许的条件下,docker swarm 集群会稳定运行指定数量的容器,具有自我修复的能力,以此来保证服务的高可用性!

使用 HAProxy 来添加外部负载均衡

未分类

此处 HAProxy 将运行于本机,以下命令均在本机操作。

1、创建 HAProxy 配置文件 haproxy.cfg

$ vim haproxy.cfg
global
    daemon
    maxconn 25600
defaults
    mode http
    timeout connect 5000ms
    timeout client 50000ms
    timeout server 50000ms
frontend http-in
    bind *:80
    default_backend servers
backend servers
    server server1 192.168.99.100:8080 maxconn 32 check
    server server2 192.168.99.101:8080 maxconn 32 check
    server server3 192.168.99.102:8080 maxconn 32 check
    server server4 192.168.99.103:8080 maxconn 32 check

2、创建 Dockerfile 自定义镜像

$ vim Dockerfile
FROM haproxy
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg

3、制作镜像

$ docker build -t any-haproxy .

4、启动 HAProxy

$ docker run -d -p 8080:80 any-haproxy

未分类

5、测试,访问 http://127.0.0.1:8080/

未分类

Centos下安装 PHP7.0.2

PHP7 已经出来挺长一段时间了,不过还一直使用较低版本的 PHP。最近有一台闲置的 VPS,准备用来建一个简单的动态页面,便想尝试下 PHP7 的一些新特性。以下简单的记录下安装步骤。

一、安装前准备(安装编译工具及库文件)

yum install make apr* autoconf automake curl-devel gcc gcc-c++ gtk+-devel zlib-devel openssl openssl-devel pcre-devel gd gettext gettext-devel kernel keyutils patch perl kernel-headers compat* mpfr cpp glibc libgomp libstdc++-devel ppl cloog-ppl keyutils-libs-devel libcom_err-devel libsepol-devel libselinux-devel krb5-devel  libXpm* freetype freetype-devel freetype* fontconfig fontconfig-devel libjpeg* libpng* php-common php-gd ncurses* libtool* libxml2 libxml2-devel patch

安装前说明 : PHP7安装包存放路径 /usr/local/src , PHP7安装目录 /usr/local/php7

二、下载 PHP7

wget http://cn2.php.net/distributions/php-7.0.2.tar.gz

解压&进入目录

tar -zxvf php-7.0.2.tar.gz

cd php-7.0.2

三、生成 Makefile

./configure --prefix=/usr/local/php7 --enable-fpm --with-fpm-user=nginx --with-fpm-group=nginx --with-mysqli --with-zlib --with-curl --with-gd --with-jpeg-dir --with-png-dir --with-freetype-dir --with-openssl --enable-mbstring --enable-xml --enable-session --enable-ftp --enable-pdo -enable-tokenizer --enable-zip

四、编译

make

如果上面 make 出现如下错误:

cc: Internal error: Killed (program cc1)

Please submit a full bug report.

See <http://bugzilla.redhat.com/bugzilla> for instructions.

make: *** [ext/fileinfo/libmagic/apprentice.lo] Error 1

原因是内存不够,一般内存小的 VPS 可能碰到该问题,内存足够一般不会碰到该问题。解决方法是在 configure 时加上 –disable-fileinfo 参数(disable前面是两个 – ), 你需要重新执行 configure 操作,然后再进行 make 编译 。

如果 make 出现下面这中错误:

collect2: ld returned 1 exit status

make: *** [sapi/cli/php] Error 1

解决办法:

make ZEND_EXTRA_LIBS='-liconv'

ln -s /usr/local/lib/libiconv.so.2   /usr/lib64/

五、安装

make install

六、操作配置文件

cp /usr/local/src/php-7.0.2/php.ini-development  /usr/local/php7/lib/php.ini

cp /usr/local/php7/etc/php-fpm.conf.default /usr/local/php7/etc/php-fpm.conf

cp /usr/local/php7/etc/php-fpm.d/www.conf.default /usr/local/php7/etc/php-fpm.d/www.conf

cp /usr/local/src/php-7.0.2/sapi/fpm/init.d.php-fpm /etc/init.d/php-fpm

chmod +x /etc/init.d/php-fpm

七、启动 php

/etc/init.d/php-fpm start