Kubernetes容器网络

Kubernetes容器网络

在kubernetes中要保证容器之间网络互通,网络至关重要.而kubernetes本身并没有具体实现网络,而是通过插件化的方式自由接入。在容器网络接入进来需要满足基本原则:

  • pod无论运行在任何节点都可以互相直接通信,而不需要借助NAT地址转换实现。
  • node与pod可以互相通信,在不限制的前提下,pod可以访问任意网络。
  • pod拥有独立的网络栈,pod看到自己的地址和外部看见的地址应该是一样的,并且同个pod内所有的容器共享同个网络栈。

容器网络基础

一个Linux容器的网络栈是被隔离在它自己的Network Namespace中,Network Namespace包括了:网卡(Network Interface),回环设备(Lookback Device),路由表(Routing Table)和iptables规则,对于服务进程来讲这些就构建了它发起请求和相应的基本环境。而要实现一个容器网络,离不开以下linux网络功能:

  • 网络命名空间: 将独立的网络协议栈隔离到不同的命令空间中,彼此间无法通信。
  • Veth Pair: Veth设备对的引入是为了实现在不同网络命名空间的通信,总是以两张虚拟网卡(veth peer) 的形式成对出现的。并且,从其中一端发出的数据,总是能在另外一端收到。
  • Iptables/Netfilter: Netfilter负责在内核中执行各种定义的规则(过滤、修改、丢弃等),运行在内核中;Iptables模式是在用户模式下运行的进程,负责协助维护内核中Netfilter的各种规则表;通过二者的配合来实现整个Linux网络协议栈中灵活的数据包处理机制
  • 网桥: 网桥是一个二层网络虚拟设备,类似交换机,主要功能是通过学习而来的Mac地址将数据帧转发到网桥的不同端口上.

  • 路由: Linux系统包含一个完整的路由功能,当IP层在处理数据发送或转发的时候,会使用路由表来决定发往哪里。

基于以上的基础,同宿主机的容器时间如何通信呢?
我们可以简单把他们理解成两台主机,主机之间通过网线连接起来,如果要多台主机通信,我们通过交换机就可以实现彼此互通。在容器中,以上的实现是通过docker0网桥,凡是连接到docker0网桥的容器,就可以通过它来进行通信。要想容器能够连接到docker0网桥,我们只需要类似网线的虚拟设备Veth Pair来把容器连接到网桥上即可。

我们启动一个容器:

$ docker run -d --name c1  hub.pri.ibanyu.com/devops/alpine:v3.8 /bin/sh

然后查看网卡设备:

$ docker exec -it c1  /bin/sh
/ # ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:02
          inet addr:172.17.0.2  Bcast:172.17.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:14 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:1172 (1.1 KiB)  TX bytes:0 (0.0 B)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

/ # route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.17.0.1      0.0.0.0         UG    0      0        0 eth0
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 eth0

可以看到其中有一张eth0的网卡,它就是veth peer其中的一端的虚拟网卡。然后通过route -n 查看容器中的路由表,eth0也正是默认路由出口。所有对172.17.0.0/16网段的请求都会从eth0出去。
我们再来看Veth peer的另一端,我们查看宿主机的网络设备:

# ifconfig
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        inet6 fe80::42:6aff:fe46:93d2  prefixlen 64  scopeid 0x20<link>
        ether 02:42:6a:46:93:d2  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 8  bytes 656 (656.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.100.0.2  netmask 255.255.255.0  broadcast 10.100.0.255
        inet6 fe80::5400:2ff:fea3:4b44  prefixlen 64  scopeid 0x20<link>
        ether 56:00:02:a3:4b:44  txqueuelen 1000  (Ethernet)
        RX packets 7788093  bytes 9899954680 (9.2 GiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 5512037  bytes 9512685850 (8.8 GiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 32  bytes 2592 (2.5 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 32  bytes 2592 (2.5 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

veth20b3dac: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet6 fe80::30e2:9cff:fe45:329  prefixlen 64  scopeid 0x20<link>
        ether 32:e2:9c:45:03:29  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 8  bytes 656 (656.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

我们可以看到,容器对应的Veth peer 另一端是宿主机上的一块虚拟网卡叫veth20b3dac,并且可以通过brctl 查看网桥信息看到这张网卡是在docker0上。

# brctl show
docker0     8000.02426a4693d2   no      veth20b3dac

然后我们再启动一个容器,从第一个容器是否能ping通第二个容器.

$ docker run -d --name c2 hub.pri.ibanyu.com/devops/alpine:v3.8 /bin/sh
$ docker exec -it c1 /bin/sh
/ # ping 172.17.0.3
PING 172.17.0.3 (172.17.0.3): 56 data bytes
64 bytes from 172.17.0.3: seq=0 ttl=64 time=0.291 ms
64 bytes from 172.17.0.3: seq=1 ttl=64 time=0.129 ms
64 bytes from 172.17.0.3: seq=2 ttl=64 time=0.142 ms
64 bytes from 172.17.0.3: seq=3 ttl=64 time=0.169 ms
64 bytes from 172.17.0.3: seq=4 ttl=64 time=0.194 ms
^C
--- 172.17.0.3 ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max = 0.129/0.185/0.291 ms

可以看到,能够ping通,其原理就是我们ping 目标IP172.17.0.3时,会匹配到我们的路由表第二条规则,网关为0.0.0.0,这就意味着是一条直连路由,通过二层转发到目的地。要通过二层网络到达172.17.0.3,我们需要知道它的Mac地址,此时就需要第一个容器发送一个ARP广播,来通过IP地址查找它的mac地址。此时Veth peer另外一段是docker0网桥,它会广播到所有连接它的veth peer 虚拟网卡去,然后正确的虚拟网卡收到后会响应这个ARP报文,然后网桥再回给第一个容器.
以上就是同宿主机不同容器通过docker0通信,如下图所示:

默认情况下,通过network namespace限制的容器进程,本质上是通过Veth peer设备和宿主机网桥的方式,实现了不同network namespace 的数据交换。

与之类似地,当你在一台宿主机上,访问该宿主机上的容器的 IP 地址时,这个请求的数据包,也是先根据路由规则到达 docker0 网桥,然后被转发到对应的 Veth Pair 设备,最后出现在容器里。

跨主机网络通信

在 Docker 的默认配置下,不同宿主机上的容器通过 IP 地址进行互相访问是根本做不到的。为了解决这个问题,社区中出现了很多网络方案。同时k8s为了更好的控制网络的接入,推出了CNI即容器网络的API接口。它是k8s中标准的一个调用网络实现的接口,kubelet通过这个API来调用不同的网络插件以实现不同的网络配置,实现了这个接口的就是CNI插件,它实现了一系列的CNI API接口。目前已经有的包括flannel、calico、weave、contiv等等。

实际上CNI的容器网络通信流程跟前面的基础网络一样,只是CNI维护了一个单独的网桥来代替 docker0。这个网桥的名字就叫作:CNI 网桥,它在宿主机上的设备名称默认是:cni0。cni的设计思想就是:Kubernetes 在启动 Infra 容器之后,就可以直接调用 CNI 网络插件,为这个 Infra 容器的 Network Namespace配置符合预期的网络栈。

CNI插件三种网络实现模式:

  • overlay 模式是基于隧道技术实现的,整个容器网络和主机网络独立,容器之间跨主机通信时将整个容器网络封装到底层网络中,然后到达目标机器后再解封装传递到目标容器。不依赖与底层网络的实现。实现的插件有flannel(UDP、vxlan)、calico(IPIP)等等

  • 三层路由模式中容器和主机也属于不通的网段,他们容器互通主要是基于路由表打通,无需在主机之间建立隧道封包。但是限制条件必须依赖大二层同个局域网内。实现的插件有flannel(host-gw)、calico(BGP)等等

  • underlay网络是底层网络,负责互联互通。 容器网络和主机网络依然分属不同的网段,但是彼此处于同一层网络,处于相同的地位。整个网络三层互通,没有大二层的限制,但是需要强依赖底层网络的实现支持.实现的插件有calico(BGP)等等

我们看下路由模式的一种实现flannel Host-gw:

如图可以看到当node1上container-1要发数据给node2上的container2时,会匹配到如下的路由表规则:

10.244.1.0/24 via 10.168.0.3 dev eth0

表示前往目标网段10.244.1.0/24的IP包,需要经过本机eth0出去发往的下一跳ip地址为10.168.0.3(node2).然后到达10.168.0.3以后再通过路由表转发到cni网桥,进而进入到container2。

以上可以看到host-gw工作原理,其实就是在每个node节点配置到每个pod网段的下一跳为pod网段所在的node节点IP,pod网段和node节点ip的映射关系,flannel保存在etcd或者k8s中。flannel只需要watch 这些数据的变化来动态更新路由表即可.

这种网络模式最大的好处就是避免了额外的封包和解包带来的网络性能损耗。缺点我们也能看见主要就是容器ip包通过下一跳出去时,必须要二层通信封装成数据帧发送到下一跳。如果不在同个二层局域网,那么就要交给三层网关,而此时网关是不知道目标容器网络的(也可以静态在每个网关配置pod网段路由)。所以flannel host-gw必须要求集群宿主机是二层互通的。

而为了解决二层互通的限制性,calico提供的网络方案就可以更好的实现,calico 大三层网络模式与flannel 提供的类似,也会在每台宿主机添加如下格式的路由规则:

<目标容器IP网段> via <网关的IP地址> dev eth0

其中网关的IP地址在不同场景有不同的意思,如果宿主机是二层可达那么就是目的容器所在的宿主机的IP地址,如果是三层不同局域网那么就是本机宿主机的网关IP(交换机或者路由器地址).

不同于flannel通过k8s或者etcd存储的数据来维护本机路由信息的做法,calico是通过BGP动态路由协议来分发整个集群路由信息。

BGP全称是 Border Gateway Protocol边界网关协议,linxu原生支持的、专门用于在大规模数据中心为不同的自治系统之间传递路由信息。只要记住BGP简单理解其实就是实现大规模网络中节点路由信息同步共享的一种协议.而BGP这种协议就能代替flannel 维护主机路由表功能。

calico 主要由以下几个部分组成:

  • calico cni插件: 主要负责与kubernetes对接,供kubelet调用使用。
  • felix: 负责维护宿主机上的路由规则、FIB转发信息库等.
  • BIRD: 负责分发路由规则,类似路由器.
  • confd: 配置管理组件。

除此之外,calico还和flannel host-gw不同之处在于,它不会创建网桥设备,而是通过路由表来维护每个pod的通信,如下图所示:

可以看到calico 的cni插件会为每个容器设置一个veth pair设备,然后把另一端接入到宿主机网络空间,由于没有网桥,cni插件还需要在宿主机上为每个容器的veth pair设备配置一条路由规则,用于接收传入的IP包,路由规则如下:

10.92.77.163 dev cali93a8a799fe1 scope link

以上表示发送10.92.77.163的IP包应该发给cali93a8a799fe1设备,然后到达另外一段容器中。

有了这样的veth pair设备以后,容器发出的IP包就会通过veth pair设备到达宿主机,然后宿主机根据路由规则的下一条地址,发送给正确的网关(10.100.1.3),然后到达目标宿主机,在到达目标容器.

10.92.77.0/24 via 10.100.1.3 dev bond0 proto bird

这些路由规则都是felix维护配置的,而路由信息则是calico bird组件基于BGP分发而来。calico实际上是将集群里所有的节点都当做边界路由器来处理,他们一起组成了一个全互联的网络,彼此之间通过BGP交换路由,这些节点我们叫做BGP Peer。

需要注意的是calico 维护网络的默认模式是 node-to-node mesh ,这种模式下,每台宿主机的BGP client都会跟集群所有的节点BGP client进行通信交换路由。这样一来,随着节点规模数量N的增加,连接会以N的2次方增长,会集群网络本身带来巨大压力。
所以一般这种模式推荐的集群规模在50节点左右,超过50节点推荐使用另外一种RR(Router Reflector)模式,这种模式下,calico 可以指定几个节点作为RR,他们负责跟所有节点BGP client建立通信来学习集群所有的路由,其他节点只需要跟RR节点交换路由即可。这样大大降低了连接数量,同时为了集群网络稳定性,建议RR>=2.

以上的工作原理依然是在二层通信,当我们有两台宿主机,一台是10.100.0.2/24,节点上容器网络是10.92.204.0/24;另外一台是10.100.1.2/24,节点上容器网络是10.92.203.0/24,此时两台机器因为不在同个二层所以需要三层路由通信,这时calico就会在节点上生成如下路由表:

10.92.203.0/24 via 10.100.1.2 dev eth0 proto bird

这时候问题就来,因为10.100.1.2跟我们10.100.0.2不在同个子网,是不能二层通信的。这之后就需要使用Calico IPIP模式,当宿主机不在同个二层网络时就是用overlay网络封装以后再发出去。如下图所示:

IPIP模式下在非二层通信时,calico 会在node节点添加如下路由规则:

10.92.203.0/24 via 10.100.1.2 dev tunnel0

可以看到尽管下一条任然是node的IP地址,但是出口设备却是tunnel0,其是一个IP隧道设备,主要有Linux内核的IPIP驱动实现。会将容器的ip包直接封装宿主机网络的IP包中,这样到达node2以后再经过IPIP驱动拆包拿到原始容器IP包,然后通过路由规则发送给veth pair设备到达目标容器。

以上尽管可以解决非二层网络通信,但是仍然会因为封包和解包导致性能下降。如果calico 能够让宿主机之间的router设备也学习到容器路由规则,这样就可以直接三层通信了。比如在路由器添加如下的路由表:

10.92.203.0/24 via 10.100.1.2 dev interface1

而node1添加如下的路由表:

10.92.203.0/24 via 10.100.1.1 dev tunnel0

那么node1上的容器发出的IP包,基于本地路由表发送给10.100.1.1网关路由器,然后路由器收到IP包查看目的IP,通过本地路由表找到下一跳地址发送到node2,最终到达目的容器。这种方案,我们是可以基于underlay 网络来实现,只要底层支持BGP网络,可以和我们RR节点建立EBGP关系来交换集群内的路由信息。

以上就是kubernetes 常用的几种网络方案了,在公有云场景下一般用云厂商提供的cni plugin即可,大多数情况都是大二层子网内可以使用flannel host-gw这种更简单的方案,对于三层可以使用flannel vxlan 或者calico ipip,而私有物理机房环境中,Calico bgp项目更加适合。根据自己的实际场景,再选择合适的网络方案。

参考

  • https://github.com/coreos/flannel/blob/master/Documentation/backends.md
  • https://coreos.com/flannel/
  • https://docs.projectcalico.org/getting-started/kubernetes/
  • https://www.kancloud.cn/willseecloud/kubernetes-handbook/1321338

基于阿里云实现kubernetes cluster-autoscaler

概述

在业务上了kubernetes集群以后,容器编排的功能之一是能提供业务服务弹性伸缩能力,但是我们应该保持多大的节点规模来满足应用需求呢?这个时候可以通过cluster-autoscaler实现节点级别的动态添加与删除,动态调整容器资源池,应对峰值流量。在Kubernetes中共有三种不同的弹性伸缩策略,分别是HPA(HorizontalPodAutoscaling)、VPA(VerticalPodAutoscaling)与CA(ClusterAutoscaler)。其中HPA和VPA主要扩缩容的对象是容器,而CA的扩缩容对象是节点。
我们线上转码业务从每天下午4点开始,负载开始飙高,在晚上10点左右负载会回落;目前是采用静态容量规划导致服务器资源不能合理利用,所以记录下如何通过cluster-autoscaler来动态伸缩pod资源所需的容器资源池

应用aliyun-cluster-autoscaler

前置条件:

  • aliyun-cloud-provider

1. 权限认证

由于autoscaler会调用阿里云ESS api来触发集群规模调整,因此需要配置api 访问的AK。

自定义权限策略如下:

{
  "Version": "1",
  "Statement": [
    {
      "Action": [
        "ess:Describe*",
        "ess:CreateScalingRule",
        "ess:ModifyScalingGroup",
        "ess:RemoveInstances",
        "ess:ExecuteScalingRule",
        "ess:ModifyScalingRule",
        "ess:DeleteScalingRule",
        "ess:DetachInstances",
        "ecs:DescribeInstanceTypes"
      ],
      "Resource": [
        "*"
      ],
      "Effect": "Allow"
    }
  ]
}

创建一个k8s-cluster-autoscaler的编程访问用户,将其自定义权限策略应用到此用户,并创建AK.

2.ASG Setup

自动扩展kubernetes集群需要阿里云ESS(弹性伸缩组)的支持,因此需要先创建一个ESS。
进入ESS控制台. 选择北京Region(和kubernetes集群所在region保持一致),点击【创建伸缩组】,在弹出的对话框中填写相应信息,注意网络类型选择专有网络,并且专有网络选择前置条件1中的Kubernetes集群所在的vpc网络名,然后选择vswitch(和kubernetes节点所在的vswitch),然后提交。如下图:

image.png
其中伸缩配置需要单独创建,选择实例规格(建议选择多种资源一致的实例规格,避免实力规格不足导致伸缩失败)、安全组(和kubernetes node所在同个安全组)、带宽峰值选择0(不分配公网IP),设置用户数据等等。注意用户数据取使用文本形式,同时将获取kubernetes集群的添加节点命令粘贴到该文本框中,并在之前添加#!/bin/bash,下面是将此节点注册到集群的实例示例:

#!/bin/bash 
curl https://dl.ipalfish.com/kubernetes-stage/attach_node.sh | bash -s -- --kubeconfig [kubectl.kubeconfig | base64] --cluster-dns 172.19.0.10 --docker-version 18.06.2-ce-3 --labels type=autoscaler 

然后完成创建,启用配置

  1. 部署Autoscaler到kubernetes集群中
    需要手动指定上面刚刚创建伸缩组ID以及伸缩最小和最大的机器数量,示例:
--nodes=1:d:asg-2ze9hse7u4udb6y4kd25  
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: cloud-autoscaler-config
  namespace: kube-system
data:
  access-key-id: "xxxx"
  access-key-secret: "xxxxx"
  region-id: "cn-beijing"
---
---
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
  name: cluster-autoscaler
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: cluster-autoscaler
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
rules:
- apiGroups: [""]
  resources: ["events","endpoints"]
  verbs: ["create", "patch"]
- apiGroups: [""]
  resources: ["pods/eviction"]
  verbs: ["create"]
- apiGroups: [""]
  resources: ["pods/status"]
  verbs: ["update"]
- apiGroups: [""]
  resources: ["endpoints"]
  resourceNames: ["cluster-autoscaler"]
  verbs: ["get","update"]
- apiGroups: [""]
  resources: ["nodes"]
  verbs: ["watch","list","get","update"]
- apiGroups: [""]
  resources: ["pods","services","replicationcontrollers","persistentvolumeclaims","persistentvolumes"]
  verbs: ["watch","list","get"]
- apiGroups: ["extensions"]
  resources: ["replicasets","daemonsets"]
  verbs: ["watch","list","get"]
- apiGroups: ["policy"]
  resources: ["poddisruptionbudgets"]
  verbs: ["watch","list"]
- apiGroups: ["apps"]
  resources: ["statefulsets"]
  verbs: ["watch","list","get"]
- apiGroups: ["storage.k8s.io"]
  resources: ["storageclasses"]
  verbs: ["watch","list","get"]

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
  name: cluster-autoscaler
  namespace: kube-system
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
rules:
- apiGroups: [""]
  resources: ["configmaps"]
  verbs: ["create","list","watch"]
- apiGroups: [""]
  resources: ["configmaps"]
  resourceNames: ["cluster-autoscaler-status", "cluster-autoscaler-priority-expander"]
  verbs: ["delete","get","update","watch"]

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: cluster-autoscaler
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-autoscaler
subjects:
  - kind: ServiceAccount
    name: cluster-autoscaler
    namespace: kube-system

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
  name: cluster-autoscaler
  namespace: kube-system
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: cluster-autoscaler
subjects:
  - kind: ServiceAccount
    name: cluster-autoscaler
    namespace: kube-system
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: cluster-autoscaler
  namespace: kube-system
  labels:
    app: cluster-autoscaler
spec:
  replicas: 1
  selector:
    matchLabels:
      app: cluster-autoscaler
  template:
    metadata:
      labels:
        app: cluster-autoscaler
    spec:
      priorityClassName: system-cluster-critical
      serviceAccountName: cluster-autoscaler
      containers:
        - image: registry.cn-hangzhou.aliyuncs.com/acs/autoscaler:v1.3.1-567fb17
          name: cluster-autoscaler
          resources:
            limits:
              cpu: 100m
              memory: 300Mi
            requests:
              cpu: 100m
              memory: 300Mi
          command:
            - ./cluster-autoscaler
            - --v=4
            - --stderrthreshold=info
            - --cloud-provider=alicloud
            - --nodes={MIN_NODE}:{MAX_NODE}:{ASG_ID}
            - --skip-nodes-with-system-pods=false
            - --skip-nodes-with-local-storage=false
          imagePullPolicy: "Always"
          env:
          - name: ACCESS_KEY_ID
            valueFrom:
              configMapKeyRef:
                name: cloud-autoscaler-config
                key: access-key-id
          - name: ACCESS_KEY_SECRET
            valueFrom:
              configMapKeyRef:
                name: cloud-autoscaler-config
                key: access-key-secret
          - name: REGION_ID
            valueFrom:
              configMapKeyRef:
                name: cloud-autoscaler-config
                key: region-id

测试自动扩展节点效果

Autoscaler根据用户应用的资源静态请求量来决定是否扩展集群大小,因此需要设置好应用的资源请求量。

测试前节点数量如下,配置均为2核4G ECS,其中两个节点可调度。

[root@iZ2ze190o505f86pvk8oisZ cluster-autoscaler]# kubectl get node
NAME                                STATUS   ROLES         AGE   VERSION
cn-beijing.i-2ze190o505f86pvk8ois   Ready    master,node   46h   v1.12.3
cn-beijing.i-2zeef9b1nhauqusbmn4z   Ready    node          46h   v1.12.3

接下来我们创建一个副本nginx deployment, 指定每个nginx副本需要消耗2G内存。

[root@iZ2ze190o505f86pvk8oisZ cluster-autoscaler]# cat <<EOF | kubectl apply -f -
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: nginx-example
spec:
  replicas: 2
  revisionHistoryLimit: 2
  template:
    metadata:
      labels:
        app: nginx-example
    spec:
      containers:
      - image: nginx:latest
        name: nginx
        ports:
          - containerPort: 80
        resources:
          requests:
            memory: 2G
EOF
[root@iZ2ze190o505f86pvk8oisZ cluster-autoscaler]# kubectl get pod
NAME                             READY   STATUS    RESTARTS   AGE
nginx-example-6669fc6b48-ndclg   1/1     Running   0          15s
nginx-example-6669fc6b48-tn5wp   1/1     Running   0          15s

看到由于有足够的cpu内存资源,所以pod能够正常调度。接下来我们使用kubectl scale 命令来扩展副本数量到4个。

[root@iZ2ze190o505f86pvk8oisZ cluster-autoscaler]# kubectl scale deploy nginx-example --replicas 3
deployment.extensions/nginx-example scaled
[root@iZ2ze190o505f86pvk8oisZ ~]# kubectl get pod
NAME                            READY   STATUS    RESTARTS   AGE
nginx-example-584bdb467-2s226   1/1     Running   0          13m
nginx-example-584bdb467-lz2jt   0/1     Pending   0          4s
nginx-example-584bdb467-r7fcc   1/1     Running   0          4s
[root@iZ2ze190o505f86pvk8oisZ cluster-autoscaler]# kubectl describe pod nginx-example-584bdb467-lz2jt | grep -A 4 Event
Events:
  Type     Reason            Age               From               Message
  ----     ------            ----              ----               -------
  Warning  FailedScheduling  1s (x5 over 19s)  default-scheduler  0/2 nodes are available: 2 Insufficient memory.

发现由于没有足够的cpu内存资源,该pod无法被调度(pod 处于pending状态)。这时候autoscaler会介入,尝试创建一个新的节点来让pod可以被调度。看下伸缩组状态,已经创建了一台机器

接下来我们执行一个watch kubectl get no 的命令来监视node的添加。大约几分钟后,就有新的节点添加进来了。

[root@iZ2ze190o505f86pvk8oisZ ~]# kubectl get node
NAME                                STATUS   ROLES             AGE     VERSION
cn-beijing.i-2ze190o505f86pvk8ois   Ready    master,node       17m     v1.12.3
cn-beijing.i-2zedqvw2bewvk0l2mk9x   Ready    autoscaler,node   2m30s   v1.12.3
cn-beijing.i-2zeef9b1nhauqusbmn4z   Ready    node              2d17h   v1.12.3
[root@iZ2ze190o505f86pvk8oisZ ~]# kubectl get pod
NAME                            READY   STATUS    RESTARTS   AGE
nginx-example-584bdb467-2s226   1/1     Running   0          19m
nginx-example-584bdb467-lz2jt   1/1     Running   0          5m47s
nginx-example-584bdb467-r7fcc   1/1     Running   0          5m47s

可以观察到比测试前新增了一个节点,并且pod也正常调度了。

测试自动收缩节点数量

当Autoscaler发现通过调整Pod分布时可以空闲出多余的node的时候,会执行节点移除操作。这个操作不会立即执行,通常设置了一个冷却时间,300s左右才会执行scale down。
通过kubectl scale 来调整nginx副本数量到1个,观察集群节点的变化。

[root@iZ2ze190o505f86pvk8oisZ cluster-autoscaler]# kubectl scale deploy nginx-example --replicas 1
deployment.extensions/nginx-example scaled
[root@iZ2ze190o505f86pvk8oisZ cluster-autoscaler]# kubectl get node
NAME                                STATUS   ROLES         AGE   VERSION
cn-beijing.i-2ze190o505f86pvk8ois   Ready    master,node   46h   v1.12.3
cn-beijing.i-2zeef9b1nhauqusbmn4z   Ready    node          46h   v1.12.3


TODO: 

  • 模糊调度
    创建多个伸缩组,每个伸缩组对应不同实例规格机器比如高IO\高内存的,不同应用弹性伸缩对应类型的伸缩组
  • cronHPA + autoscaler
     由于node弹性伸缩存在一定的时延,这个时延主要包含:采集时延(分钟级) + 判断时延(分钟级) + 伸缩时延(分钟级)结合业务,根据时间段,自动伸缩业务(CronHPA)来处理高峰数据,底层自动弹性伸缩kubernetes node增大容器资源池

参考地址:

https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/FAQ.md

https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler/cloudprovider/alicloud

Go 程序的性能监控与分析 pprof

Go 开箱就提供了一系列的性能监控 API 以及用于分析的工具, 可以快捷而有效地观察应用程序各个细节的 CPU 与内存使用情况, 包括生成一些可视化的数据.

pprof 数据采样

pprof 采样数据主要有如下方式:

  • runtime/pprof: 采集程序(非 Server)的运行数据进行分析。手动调用runtime.StartCPUProfile或者runtime.StopCPUProfile等 API来生成和写入采样文件,灵活性高
  • net/http/pprof: 采集 HTTP Server 的运行时数据进行分析。通过 http 服务获取Profile采样文件,简单易用,适用于对应用程序的整体监控。通过 runtime/pprof 实现
net/http/pprof

在应用程序中导入import _ “net/http/pprof”,并启动 http server即可:

import _ "net/http/pprof" //执行init函数

net/http/pprof 已经在 init()函数中通过 import 副作用(side effect)完成默认 Handler 的注册

go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()

1个简单的例子:

main.go

package main

import (
    "log"
    "net/http"
    _ "net/http/pprof"
    "gitlab.pri.ibanyu.com/devops/go-pprof-example/data"
)

func main() {
    go func() {
        for {
            log.Println(data.Add("https://github.com/devops"))
        }
    }()

    http.ListenAndServe("0.0.0.0:6060", nil)
}

data/d.go,文件内容:

package data

var datas []string

func Add(str string) string {
    data := []byte(str)
    sData := string(data)
    datas = append(datas, sData)

    return sData
}

接着我们需要编译一下这个程序并运行

$ go build .
$ ./go-pprof-example
2019/07/31 21:14:47 https://github.com/devops
2019/07/31 21:14:47 https://github.com/devops
2019/07/31 21:14:47 https://github.com/devops
2019/07/31 21:14:47 https://github.com/devops
2019/07/31 21:14:47 https://github.com/devops
2019/07/31 21:14:47 https://github.com/devops
2019/07/31 21:14:47 https://github.com/devops
2019/07/31 21:14:47 https://github.com/devops
2019/07/31 21:14:47 https://github.com/devops

之后可通过 http://localhost:6060/debug/pprof 可以看到如下页面:

页面上展示了可用的程序CMD运行采样数据:

  • allocs: 内存分配情况的采样信息
  • goroutine: /debug/pprof/goroutine,获取程序当前所有 goroutine 的堆栈信息
  • heap: /debug/pprof/heap,查看活动对象的内存分配情况。
  • threadcreate: /debug/pprof/threadcreate,查看创建新OS线程的堆栈跟踪
  • block: /debug/pprof/block,阻塞操作情况的采样信息
  • mutex: /debug/pprof/mutex,查看导致互斥锁的竞争持有者的堆栈跟踪
  • cmdline: /debug/pprof/cmdline,获取程序的命令行启动参数
  • profile: /debug/pprof/profile,默认进行 30s 的 CPU Profiling,得到一个分析用的 profile 文件
  • trace: /debug/pprof/trace 程序运行跟踪信息
runtime/pprof

runtime/pprof提供各种相对底层的 API 用于生成采样数据,一般应用程序更推荐使用net/http/pprof,runtime/pprof 的 API 参考runtime/pprof或 http pprof 实现

通过交互式终端使用

1. profile
$ go tool pprof http://localhost:6060/debug/pprof/profile?seconds=60
Fetching profile over HTTP from http://localhost:6060/debug/pprof/profile?seconds=60
Saved profile in /Users/wangyichen/pprof/pprof.samples.cpu.002.pb.gz
Type: cpu
Time: Jul 31, 2019 at 9:26pm (CST)
Duration: 1mins, Total samples = 8.02s (13.33%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)

执行该命令后,需等待 60 秒(可调整 seconds 的值),pprof 会进行 CPU Profiling。结束后将默认进入 pprof 的交互式命令模式,可以对分析的结果进行查看或导出。具体可执行 pprof help 查看命令说明.

(pprof) top10
Showing nodes accounting for 7.88s, 98.25% of 8.02s total
Dropped 35 nodes (cum <= 0.04s)
Showing top 10 nodes out of 37
      flat  flat%   sum%        cum   cum%
     5.79s 72.19% 72.19%      6.15s 76.68%  syscall.syscall
     0.42s  5.24% 77.43%      0.75s  9.35%  runtime.notetsleep
     0.35s  4.36% 81.80%      0.36s  4.49%  runtime.exitsyscallfast
     0.34s  4.24% 86.03%      0.34s  4.24%  runtime.nanotime
     0.33s  4.11% 90.15%      0.33s  4.11%  runtime.pthread_cond_timedwait_relative_np
     0.33s  4.11% 94.26%      0.33s  4.11%  runtime.usleep
     0.15s  1.87% 96.13%      0.15s  1.87%  runtime.memmove
     0.09s  1.12% 97.26%      0.09s  1.12%  runtime.memclrNoHeapPointers
     0.07s  0.87% 98.13%      0.07s  0.87%  runtime.pthread_cond_signal
     0.01s  0.12% 98.25%      0.26s  3.24%  gitlab.pri.ibanyu.com/devops/go-pprof-example/data.Add
  • flat:给定函数上运行耗时
  • flat%:同上的 CPU 运行耗时总比例
  • sum%:给定函数累积使用 CPU 总比例
  • cum:当前函数加上它之上的调用运行总耗时
  • cum%:同上的 CPU 运行耗时总比例

最后一列为函数名称,在大多数的情况下,我们可以通过这五列得出一个应用程序的运行情况并优化.

2.heap
Fetching profile over HTTP from http://localhost:6060/debug/pprof/heap
Saved profile in /Users/wangyichen/pprof/pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.002.pb.gz
Type: inuse_space
Time: Jul 31, 2019 at 9:34pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 1.27GB, 100% of 1.27GB total
      flat  flat%   sum%        cum   cum%
    1.27GB   100%   100%     1.27GB   100%  gitlab.pri.ibanyu.com/devops/go-pprof-example/data.Add
         0     0%   100%     1.27GB   100%  main.main.func1
(pprof) list data.Add
Total: 1.27GB
ROUTINE ======================== gitlab.pri.ibanyu.com/devops/go-pprof-example/data.Add in /tmp/example/data/d.go
    1.27GB     1.27GB (flat, cum)   100% of Total
         .          .      2:
         .          .      3:var datas []string
         .          .      4:
         .          .      5:func Add(str string) string {
         .          .      6:    data := []byte(str)
  823.03MB   823.03MB      7:    sData := string(data)
  481.98MB   481.98MB      8:    datas = append(datas, sData)
         .          .      9:
         .          .     10:    return sData
         .          .     11:}
  • -inuse_space:分析应用程序的常驻内存占用情况
  • -alloc_objects:分析应用程序的内存临时分配情况
3.allocs
go tool pprof http://localhost:6060/debug/pprof/allocs
Fetching profile over HTTP from http://localhost:6060/debug/pprof/allocs
Saved profile in /Users/wangyichen/pprof/pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.003.pb.gz
Type: alloc_space
Time: Jul 31, 2019 at 9:37pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 5458.28MB, 99.86% of 5465.68MB total
Dropped 22 nodes (cum <= 27.33MB)
      flat  flat%   sum%        cum   cum%
 3987.74MB 72.96% 72.96%  3987.74MB 72.96%  gitlab.pri.ibanyu.com/devops/go-pprof-example/data.Add
  980.03MB 17.93% 90.89%   980.03MB 17.93%  fmt.Sprintln
  490.51MB  8.97% 99.86%  5458.28MB 99.86%  main.main.func1
         0     0% 99.86%   980.03MB 17.93%  log.Println
4. block

go tool pprof http://localhost:6060/debug/pprof/block

5. mutex

go tool pprof http://localhost:6060/debug/pprof/block

PProf 可视化界面

go-torch 在 Go 1.11 之前是作为非官方的可视化工具存在的, 它可以为监控数据生成火焰图,从 Go 1.11 开始, 火焰图被集成进入 Go 官方的 pprof 库.

go-torch is deprecated, use pprof instead

As of Go 1.11, flamegraph visualizations are available in go tool pprof directly!
$ go tool pprof -http=":8088" [binary] [profile]

在浏览器打开 http://localhost:8081/ui/flamegraph, 就可以看到下面这样的反过来的火焰图.如果出现 Could not execute dot; may need to install graphviz.,就是提示你要安装 graphviz 。谷歌安装即可.

火焰图最大优点是动态的,每一块代表一个函数,颜色的深浅是随机的 ,长度越长代表占用 CPU 时间越长,

然后, pprof 命令行的 top 以及 list 正则也可以在这里边完成, 还有 svg 图形.

通过 PProf 的可视化界面,我们能够更方便、更直观的看到 Go 应用程序的调用链、使用情况等,并且在 View 菜单栏中,还支持如上多种方式的切换。

本文粗略地介绍了 Go 的性能利器 PProf。在特定的场景中,PProf 给定位、剖析问题带了极大的帮助

Istio服务治理介绍

1. 什么是istio?

Istio是由Google、IBM和Lyft开源的微服务管理、保护和监控框架,目前已经更新到v1.2.2版本。Istio为希腊语,意思是”起航“。官方对 istio 的介绍浓缩成了一句话:

An open platform to connect, secure, control and observe services.

翻译过来,就是”连接、安全加固、控制和观察服务的开放平台“。开放平台就是指它本身是开源的,服务对应的是微服务,也可以粗略地理解为单个应用。

中间的四个动词就是 istio 的主要功能:

  • 连接(Connect):智能控制服务之间的调用流量,能够实现灰度升级、AB 测试和红黑部署等功能
  • 安全加固(Secure):自动为服务之间的调用提供认证、授权和加密
  • 控制(Control):应用用户定义的 policy,保证资源在消费者中公平分配
  • 观察(Observe):查看服务运行期间的各种数据,比如日志、监控和 tracing,了解服务的运行情况

2. 为什么要使用 Istio?

Istio 提供一种简单的方式来为已部署的服务建立网络,该网络具有负载均衡、服务间认证、监控等功能,只需要对服务的代码进行一点或不需要做任何改动。想要让服务支持 Istio,只需要在额们环境中部署一个特殊的 sidecar 代理,使用 Istio 控制平面功能配置和管理代理,拦截微服务之间的所有网络通信。使用istio的进行微服务管理有如下功能:
– HTTP、gRPC、WebSocket 和 TCP 流量的自动负载均衡。
– 通过丰富的路由规则、重试、故障转移和故障注入,可以对流量行为进行细粒度控制。
– 可插入的策略层和配置 API,支持访问控制、速率限制和配额。
– 对出入集群入口和出口中所有流量的自动度量指标、日志记录和追踪。
– 通过强大的基于身份的验证和授权,在集群中实现安全的服务间通信。

Istio 旨在实现可扩展性,满足各种部署需求。

3. 架构

Istio架构分为控制层和数据层:

  • 数据层:由一组智能代理(Envoy)作为sidecar部署,协调和控制所有microservices之间的网络通信。
  • 控制层:负责管理和配置代理路由流量,以及在运行时执行的政策。

4. Envoy

Istio使用Envoy代理的扩展版本,该代理是以C++开发的高性能代理,用于调解service mesh中所有服务的所有入站和出站流量。 Istio利用了Envoy的许多内置功能,例如:
– 动态服务发现
– 负载平衡
– TLS终止
– HTTP/2&gRPC代理
– 熔断器
– 健康检查、基于百分比流量拆分的灰度发布
– 故障注入
– 丰富的度量指标

Envoy在kubernetes中作为pod的sidecar来部署。 这允许Istio将大量关于流量行为的信号作为属性提取出来,这些属性又可以在Mixer中用于执行策略决策,并发送给监控系统以提供有关整个mesh的行为的信息。 Sidecar代理模型还允许你将Istio功能添加到现有部署中,无需重新构建或重写代码。

5. Pilot

Pilot 为 Envoy sidecar 提供服务发现功能,为智能路由(例如 A/B 测试、金丝雀部署等)和弹性(超时、重试、熔断器等)提供流量管理功能。它将控制流量行为的高级路由规则转换为特定于 Envoy 的配置,并在运行时将它们传播到 sidecar。

Pilot 将平台特定的服务发现机制抽象化并将其合成为符合 Envoy 数据平面 API 的任何 sidecar 都可以使用的标准格式。这种松散耦合使得 Istio 能够在多种环境下运行(例如,Kubernetes、Consul、Nomad),同时保持用于流量管理的相同操作界面。

6. Mixer

Mixer负责在service mesh上执行访问控制和使用策略,并收集Envoy代理和其他服务的遥测数据。代理提取请求级属性,发送到mixer进行评估。有关此属性提取和策略评估的更多信息,请参见Mixer配置。 混音器包括一个灵活的插件模型,使其能够与各种主机环境和基础架构后端进行接口,从这些细节中抽象出Envoy代理和Istio管理的服务。

7. Citadel

Citadel 通过内置身份和凭证管理赋能强大的服务间和最终用户身份验证。可用于升级服务网格中未加密的流量,并为运维人员提供基于服务标识而不是网络控制的强制执行策略的能力。从 0.5 版本开始,Istio 支持基于角色的访问控制,以控制谁可以访问您的服务,而不是基于不稳定的三层或四层网络标识。

8. Galley

Galley 代表其他的 Istio 控制平面组件,用来验证用户编写的 Istio API 配置。随着时间的推移,Galley 将接管 Istio 获取配置、 处理和分配组件的顶级责任。它将负责将其他的 Istio 组件与从底层平台(例如 Kubernetes)获取用户配置的细节中隔离开来

总结:

Istio 的出现为微服务架构减轻了很多的负担,开发者不用关心服务调用的超时、重试、流控的等实现,服务之间的安全、授权也得到了保证;集群管理员也能够很方便地发布应用(AB 测试和灰度发布),并且能清楚看到整个集群的运行情况。

但是这并不表明有了 istio 就可以高枕无忧了,istio 只是把原来分散在应用内部的复杂性统一抽象出来放到了统一的地方,并没有让原来的复杂消失不见。因此我们需要维护 istio 整个集群,而 istio 的架构比较复杂,尤其是它一般还需要架在 kubernetes 之上,这两个系统都比较复杂,而且它们的稳定性和性能会影响到整个集群。因此再采用 isito 之前,必须做好清楚的规划,权衡它带来的好处是否远大于额外维护它的花费,需要有相关的人才对整个网络、kubernetes 和 istio 都比较了解才行。

参考

  • https://istio.io/docs/concepts/what-is-istio/

分布式链路调用追踪

背景

随着分布式系统和微服务架构的演进, 使得服务之间开始产生复杂的交互, 系统的能见度越来越弱. 用户看到的一次请求响应, 通常会触发多个系统间的RPC调用和数据的存储操作, 而任何一个子系统的低效都会导致最终的响应缓慢. 我们现存的监控系统能够知道某些用户请求异常或者缓慢, 但是却无法快速定位这个问题是哪个服务造成的. 传统的日志监控等方式无法很好达到跟踪调用,排查问题等需求。

google Dapper

Google 是最早在大规模分布式系统中实践分布式跟踪的公司之一, Google在2010年发表的论文Dapper, a Large-Scale Distributed Systems Tracing Infrastructure 现有的分布式Trace基本都是采用了google 的Dapper标准。Dapper的思想很简单,就是在每一次调用栈中,使用同一个TraceId将不同的server联系起来。

Dapper 跟踪模型

以下的分布式调用链, 用户请求RequestX直接入口是A系统, 请求引发了其他系统之间的若干次RPC调用.dapper

就是这样一个调用链,一个用户请求了应用A,应用A需要请求应用B和应用C,而应用C需要请求应用D和应用E。Dapper的核心功能是对每一次RPC的接收和发送设置跟踪标识和其他监控信息(比如耗时等), 并且把这些信息和请求Request关联起来. 同时调用链不仅仅局限于特定的RPC框架, 还能跟踪其他行为, 比如mongodb , Mysql 操作等等.

Dapper 数据抽象

Dapper把跟踪模型抽象为Trace Tree 和Span. Trace Tree 代表一次完整的请求调用, Span代表一次RPC调用, Tree的每个节点是对Span的引用, Span之间的连线表示他们的父子关系, 通过parent id来构造, 没有parent id的Span叫做Root Span.如下图就是Dapper规定Trace的结构和基本要素:
datamodel

一次单独的调用链也可以称为一个span,dapper记录的是span的名称,以及每个span的ID和父ID,以重建在一次追踪过程中不同span之间的关系,上图中一个矩形框就是一个span,前端从发出请求到收到回复就是一个span。

再细化到一个span的内部,如下图:
span
对于一个特定的span,记录从Start到End,首先经历了客户端发送数据,然后server接收数据,然后server执行内部逻辑,这中间可能去访问另一个应用。执行完了server将数据返回,然后客户端接收到数据。

一个span的内容就能构成Trace上面的一个基本元素,可以在这个span中埋点打上各种各样的Trace类型,比如,一般将客户端发送记录成依赖(dependency),服务端接收客户端以及回复给客户端这两个时间统一记录成请求(request),如果打上这两种,那么在运行完这个span之后,日志库中就会多出两条日志,一条是dependency的日志,一条是request的日志。

现在的Trace SDK,都可以进行配置去自动记录一些事件,比如数据库调用依赖,http调用依赖,记录上游的请求等等,也可以自己手动埋点,在需要打上记录点的地方写上记录的代码即可。

业内实现

对我们的具体系统来说, Dapper更多是分布式跟踪理论的启蒙. 多语言、多RPC协议和多存储, 在全链路跟踪场景下需要一种统一编排API,

开源的 Open Tracing

openTracing是为了解决不同系统之间的兼容性设计的,现在也成为了各个第三方Trace系统的依赖的规范。

OpenTracing通过提供平台无关、厂商无关的API,使得开发人员能够方便的添加(或更换)追踪系统的实现。OpenTracing提供了用于运营支撑系统的和针对特定平台的辅助程序库。

OpenTracing来自大名鼎鼎的CNCF(Cloud Native Computing Foundation), 该基金会的其他著名项目有 kubernetes, Prometheus 等. 随着OpenTracing进入CNCF,这个标准越来越受到开源和商业团队的关注, OpenTracing的标准还在初始阶段, 正在不断改进中.

OpenTracing 延续了Dapper的跟踪模型, 核心对象仍然是基于trace和span, 一个典型的Trace案例如下:

opentraacing
以上调用链路很难说清组件的调用时间,是串行调用还是并行调用,如果展现更复杂的调用关系,会更加复杂. 一种更有效的展现一个典型的trace过程:

opentracing1

OpenTracing主要数据模型
  • Trace

    代表了一个事务或者流程在(分布式)系统中的执行过程, 是多个span组成的一个有向无环图

  • Span

    具有开始时间和执行时长的逻辑运行单元, span之间通过嵌套或者顺序排列建立逻辑因果关系

  • Log

    每个span可以进行多次Log操作,每一次Logs操作,都需要一个带时间戳的时间名称,以及可选的任意大小的存储结构.

    Log 通常用于记录Span生命周期中发生的事件.

  • Tag

    每个span可以有多个键值对(key:value)形式的Tag,Tag是没有时间戳的.

    Tag用于记录跟踪系统感兴趣的metric, 不同类型的span可能会记录不同的metric, 比如Http类型的span会记录http.status_code, mysql 类型span可能使用db.statement来记录执行的sql语句.

  • SpanContext

    SpanContext代表跨越进程边界,传递到下级span的状态, 至少包含元组, 以及可选的Baggage. SpanContext在整个链路中会向下传递.

    SpanContext是跨链路传输中非常重要的概念, 它包含了需要在链路中传递的全部信息.

  • Baggage

    Baggage通常用于业务数据在全链路数据透明传输.

    Baggage是存储在SpanContext中的一个键值对(SpanContext)集合。它会在一条追踪链路上的所有span内全局传输,包含这些span对应的SpanContexts。在这种情况下,”Baggage”会随着trace一同传播,因此得名.

    Baggage拥有强大功能,也会有很大的消耗。由于Baggage的全局传输,如果包含的数量量太大,或者元素太多,它将降低系统的吞吐量或增加RPC的延迟。

Span相关数据模型的关系如下:
opentracing2

Jaeger

OpenTracing 最重要的意义在于提供了一系列统一的接口规范, 用户需要根据自身系统的需要, 对特定语言的目标跟踪组件进行具体的实现. User公司开元的Jaeger正是基于OpenTracing标准。

Jaeger的整体架构如下:
jaeger

系统中Jaeger Agent, Jaeger Collecor以及Jaeger Qeruy均是使用Go语言实现,客户端库使用了四种支持OpenTracing标准的语言,后端存储使用NoSQL数据库Cassandra(还支持leasticsearch等等), 以及一个基于Apache Spark的后处理和聚合数据管道. 另外还包括一个基于React的Web前端,

其中Agent作为基础架构组件,部署到所有需要度量收集的宿主机上. Agent基于本地回环地址端口创建UDP server, 宿主机器上的目标项目将跟踪span push到Agent, 注意到基于回环地址的UDP报文大小最大约64k, 而跨机器的UDP报文受限于MTU, 报文大小通常小于1500字节. 为了支持多语言的Client 统一Span数据模型, Agent 和Client采用Thrift作为上层RPC协议, 另外Thrift 的二进制序列化格式CompactProtocol/BinaryProtocol足够紧凑. 整体上, 无连接的本机UDP server + Thrift协议非常高效.

Jaeger Collecor 是分布式部署的数据收集后端, 主要用于数据清洗和转存, Agent 和 Collecor 之间使用的是Uber自研的 TChannel协议, 这是一种适用于RPC的网络多路复用和框架协议(受Twiiter Finagle的多路复用RPC协议Mux启发), 可以支持 Thrift 和 HTTP+JSON 等.

Jaeger也经历了多次的架构演进, Uber Jaeger 在2017年进行开源, 并在2017年9月加入CNCF基金会, 这将让更多的人了解到分布式全链路跟踪, 特别是在多语言场景下的成功实践.

Sysdig入门介绍

很多系统命令最开始设计时处于比较早的年代, 没有随时代更新调整, 在容器盛行的今天变得很不好用. sysdig 在容器时代重新设计实现,
本文简单介绍 Sysdig 的核心内容和一些坑, 帮助大家上手. 详细使用还需要大家去看官方的手册

What’s Wrong with the Good Old Sysadmin Tools?

比如 top 为例

livecast, comment, bussconf 是在容器内运行的, 我们要想看各个容器的 top, 相对比较费劲, 特别是当一台机器上启动多个相同程序的 POD, 比如线上运行着多个副本的服务.

而使用 sysdig 让这事变得很容易

sysdig -pc -c topprocs_cpu     // -pc output  container format.
topprocs_cpu  chisel, .

Sysdig 是什么?

Sysdig captures system calls and events from the Linux kernel.
You can save, filter, and analyze the data with our CLI or our desktop app.
Think of sysdig as strace + tcpdump + htop + iftop + lsof + wireshark for your entire system.

官方上介绍, sysdig 功能非常强大可以看到 strace + tcpdump + htop + iftop + lsof + wireshark 的功能集合, 大家如果之前使用 tcpdump 或者 wireshark 会感觉非常熟悉, 因为创始人也是Wireshark的其中一个作者, 所以我们可以看到 sysdig 的设计里面有 tcpdump 和 Wireshark 的影子.

Sysdig 采集原理

sysdig 在 kernel 中安装了一个模块 (sysdig-probe) 然后 sysdig 注册了 tracepoints 来捕捉 system call 和 process scheduling events.
sysdig-probe 的逻辑就是不断读取 event 写入到 Event Ring Buffer, Ring Buffer MMap 到 userspace , 然后在 User space 对这些 event 进 行处理.
所以 sysdig 对性能的影响是比较可控的, 在线上使用正常情况下对业务影响不大.

Sysdig 基本用法

[root@k8s-test-04 ~]# sysdig -n 10 #capture latest 10 event
1 23:12:58.057899572 1 kubelet (20136)  pselect6
5 23:12:58.057903363 1 kubelet (20136) &gt; epoll_ctl
8 23:12:58.057904365 1 kubelet (20136)  fcntl fd=26(/proc/27447/net/dev) cmd=4(F_GETFL)
12 23:12:58.057905994 1 kubelet (20136)  fcntl fd=26(/proc/27447/net/dev) cmd=5(F_SETFL)
16 23:12:58.057906936 1 kubelet (20136)  read fd=26(/proc/27447/net/dev) size=4096
29 23:12:58.057919424 1 kubelet (20136) 172.30.43.55:44367) size=4096
53655 23:14:48.132825711 1 livecast (25263) 10.111.201.239:26047) size=117
53679 23:14:48.132904715 1 livecast (25263)  epoll_pwait
53685 23:14:48.132910322 1 livecast (25263)  epoll_pwait

Csysdig

类似 top 输出, 比较强大的是支持多种 View, 可以非常方便支持我们排查各种问题. 比如: Connections 可以查看机器上对应的连接数, Containers 查看机器上 container 的信息等等

离线分析

sysdig 和 tcpdump 类似支持把捕捉到的信息写入到文件中, 这样我们就可以把这个信息放到其他机器上进行分析

sysdig -C 100 -W 5 -w capture.scap   //5, 100MB 

分析文件

csysdig -r  capture.scap

高级功能

光谱图

sysdig 会对系统调用进行统计, 每隔2sec 会把各个系统调用的时间放到不同的 bucket 上进行计数, 黑色没有系统调用
绿色: 1 – 100
黄色: 100 – 1000
红色: 1000+
然后在离线分析模式下, 可以在两个位置点击, 构成一个区域, sysdig 限制在该区域下的 event.

查看日志

在运维过程中, 有些模块的日志肯定随便输出, 我们可能需要费很大劲去找到该模块的输出, 但是利用 sysdig 的话, 就可以非常简单

[root@k8s-test-04 ~]# sysdig -c spy_logs proc.name=livecast
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:39.955681INFOHealthCheck --&gt; in
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:42.955221INFOHealthCheck --&gt; in
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:44.971123WARNBreaker.Do --&gt; key:0.base/bussconf.31.GetConf err:hystrix: timeout
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:44.971165INFOBreaker.Do --&gt; key:0.base/bussconf.31.GetConf dur:36000269225
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:44.971191INFOBussConfUpdater.checkUpdate --&gt; get btype:cdnrule version:39 err:ErrInfo({Code:-1001 Msg:call serice:base/bussconf proc:proc_thrift m:GetConf err:hystrix: timeout})
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:45.442415WARNBreaker.Do --&gt; key:0.base/bussconf.31.GetConf err:hystrix: timeout
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:45.442448INFOBreaker.Do --&gt; key:0.base/bussconf.31.GetConf dur:36002709312
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:45.442471INFOBussConfUpdater.checkUpdate --&gt; get btype:urlreflect version:2 err:ErrInfo({Code:-1001 Msg:call serice:base/bussconf proc:proc_thrift m:GetConf err:hystrix: timeout})
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:45.955269INFOHealthCheck --&gt; in
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:46.206747WARNBreaker.Do --&gt; key:0.base/bussconf.31.GetConf err:hystrix: timeout
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:46.206791INFOBreaker.Do --&gt; key:0.base/bussconf.31.GetConf dur:36000642033
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:46.206827INFOBussConfUpdater.checkUpdate --&gt; get btype:cdnconf version:31 err:ErrInfo({Code:-1001 Msg:call serice:base/bussconf proc:proc_thrift m:GetConf err:hystrix: timeout})
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:48.955196INFOHealthCheck --&gt; in
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:49.047016INFOLivecast.checkShow --&gt; docheck now:1556637709 len:0
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:49.048045INFOLivecast.checkNotify --&gt; docheck pos:1556639509 len:0
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:49.124174INFOLivecast.checkNextLession --&gt; docheck now:1556637709 len:30
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:49.124193INFOLivecast.checkNextLession --&gt; check lid:159927286509568
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:49.124197INFOLivecast.checkNextLession --&gt; change lid:159927286509568
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:49.179156INFOLivecast.checkNextLession --&gt; check lid:162130556176384
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:49.179186INFOLivecast.checkNextLession --&gt; change lid:162130556176384
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:49.235806INFOLivecast.checkNextLession --&gt; check lid:163238868119552
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:49.235829INFOLivecast.checkNextLession --&gt; change lid:163238868119552
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:49.283176INFOLivecast.checkNextLession --&gt; check lid:163161348347904
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:49.283198INFOLivecast.checkNextLession --&gt; change lid:163161348347904
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:49.336594INFOLivecast.checkNextLession --&gt; check lid:159484198393856
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:49.336613INFOLivecast.checkNextLession --&gt; change lid:159484198393856
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:49.390099INFOLivecast.checkNextLession --&gt; check lid:164132829954048
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:49.390125INFOLivecast.checkNextLession --&gt; change lid:164132829954048
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:49.441170INFOLivecast.checkNextLession --&gt; check lid:165907580039168
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:49.441193INFOLivecast.checkNextLession --&gt; change lid:165907580039168
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:49.456781INFOServBaseV2.doRegister --&gt; refresh ttl idx:93630 servs:{"servs":{"_PROC_BACKDOOR":{"type":"http","addr":"172.30.43.55:60000"}}}
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:49.458492INFOServBaseV2.doRegister --&gt; reg idx:93630 ok:{"action":"update","node":{"key":"/roc/dist2/ugc/livecast/47/backdoor","value":"{\"servs\":{\"_PROC_BACKDOOR\":{\"type\":\"http\",\"addr\":\"172.30.43.55:60000\"}}}","nodes":null,"createdIndex":465493179,"modifiedIndex":532051104,"expiration":"2019-04-30T15:24:49.457441492Z","ttl":180},"prevNode":{"key":"/roc/dist2/ugc/livecast/47/backdoor","value":"{\"servs\":{\"_PROC_BACKDOOR\":{\"type\":\"http\",\"addr\":\"172.30.43.55:60000\"}}}","nodes":null,"createdIndex":465493179,"modifiedIndex":532050316,"expiration":"2019-04-30T15:24:19.455707269Z","ttl":150}}
 livecast /data/servicelog/wread0/ugc/livecast47/serv.log 2019/04/30 23:21:49.489010INFOLivecast.checkNextLession --&gt; check lid:165908197472256

查看 HTTP 服务

在运维过程中, 特别在受到攻击的时候, 如何快速找到当前 top 的 request?

sysdig -c httptop ncalls

实现类似 iotop 的效果

sysdig -c fdbytes_by proc.name

查看 httplog 信息

sysdig -c httplog
2019-04-30 23:24:34.609105778  method=PUT url=infra1.etcd.ibanyu.com:20002/v2/keys/roc/dist2/api/rtcapi/3/metrics?prevExist=true response_code=200 latency=1ms size=523B
2019-04-30 23:24:34.681344118 &gt; method=PUT url=old0.etcd.ibanyu.com:20002/v2/keys/roc/dist2/api/dispatchapi/0/serve?prevExist=true response_code=200 latency=1ms size=522B
2019-04-30 23:24:34.690369249 &gt; method=PUT url=infra1.etcd.ibanyu.com:20002/v2/keys/roc/dist2/api/rtcapi/3/serve?prevExist=true response_code=200 latency=1ms size=512B
2019-04-30 23:24:34.720918907 &gt; method=PUT url=infra3.etcd.ibanyu.com:20002/v2/keys/roc/dist/api/opapi/28?prevExist=true response_code=200 latency=1ms size=472B
2019-04-30 23:24:34.723615217 &gt; method=PUT url=infra1.etcd.ibanyu.com:20002/v2/keys/roc/dist2/api/rtcapi/3/backdoor?prevExist=true response_code=200 latency=2ms size=527B
2019-04-30 23:24:34.726079380 &lt; method=GET url=172.30.43.25:60000/backdoor/health/check response_code=200 latency=0ms size=2B
2019-04-30 23:24:34.726211669  method=GET url=172.30.43.46:60000/backdoor/health/check response_code=200 latency=0ms size=2B

Chisels

一组预定义的功能集合,通过 Lua 脚本实现,用来分析特定的场景. 比如前面的 sysdig -c httplog, httplog 就是一个 chisel, 该 chisel 会 对 event 进行过滤和分析找到对应的 httplog

查看 Chisels 列表


sysdig -cl Category: Application --------------------- httplog httptop memcachelog HTTP requests log Top HTTP requests memcached requests log Category: CPU Usage ------------------- spectrogram Visualize OS latency in real time. subsecoffset Visualize subsecond offset execution time. topcontainers_cpu Top containers by CPU usage topprocs_cpu Top processes by CPU usage Category: Errors ---------------- topcontainers_error Top containers by number of errors topfiles_errors Top files by number of errors topprocs_errors top processes by number of errors Category: I/O ------------- echo_fds fdbytes_by fdcount_by fdtime_by iobytes iobytes_file spy_file Optionall Print the data read and written by processes. I/O bytes, aggregated by an arbitrary filter field FD count, aggregated by an arbitrary filter field FD time group by Sum of I/O bytes on any type of FD Sum of file I/O bytes Echo any read/write made by any process to all files. y, you can provide the name of one file to only intercept reads stderr stdin stdout topcontainers_file /writes to that file. Print stderr of processes Print stdin of processes Print stdout of processes Top containers by R+W disk bytes topfiles_bytes Top files by R+W bytes topfiles_time topprocs_file Category: Logs -------------- spy_logs Optionally, e spy_syslog the e Category: Misc -------------- around given Top files by time Top processes by R+W disk bytes Echo any write made by any process to a log file. xport the events around each log message to file. Print every message written to syslog. Optionally, export vents around each syslog message to file. Export to file the events around the time range where the filter matches. Category: Net ------------- iobytes_net spy_ip spy_port topconns topcontainers_net Top containers by network I/O topports_server Top TCP/UDP server ports by R+W bytes topprocs_net Top processes by network I/O Category: Performance --------------------- Show total network I/O bytes Show the data exchanged with the given IP address Show the data exchanged using the given IP port number Top network connections by total bytes bottlenecks fileslower netlower proc_exec_time Show process execution time scallslower Trace slow syscalls topscalls Top system calls by number of calls topscalls_time Top system calls by time Category: Security ------------------ list_login_shells List the login shell IDs shellshock_detect Slowest system calls Trace slow file I/O Trace slow network I/0 print shellshock attacks spy_users Display interactive user activity Category: System State ---------------------- lscontainers List the running containers lsof List (and optionally filter) the open file descriptors. netstat List (and optionally filter) network connections. ps List (and optionally filter) the machine processes. Category: Tracers ----------------- tracers_2_statsd Export spans duration as statds metrics. Use the -i flag to get detailed information about a specific chisel

查看对应 Chisel 的使用方法

sysdig -i spy_logs

参考地址:

  • https://www.sysdig.org/wiki/
  • http://cizixs.com/2017/04/27/sysdig-for-linux-system-monitor-and-analysis
  • https://sysdig.com/blog/tag/sysdig/

Kubernetes 有状态服务

概述

在kubernetes容器编排系统中,RC、Deployment、DaemonSet都是面向无状态的服务,它们所管理的Pod的IP、名字,启停顺序等都是随机的,而StatefulSet是什么?顾名思义,有状态的集合,管理所有有状态的服务,比如MySQL、MongoDB集群等。StatefulSet本质上是Deployment的一种变体,在v1.9版本中已成为GA版本,它为了解决有状态服务的问题,它所管理的Pod拥有固定的Pod名称,启停顺序,在StatefulSet中,Pod名字称为网络标识(hostname),还必须要用到共享存储。在Deployment中,与之对应的服务是service,而在StatefulSet中与之对应的headless service,headless service,即无头服务,与service的区别就是它没有Cluster IP,解析它的名称时将返回该Headless Service对应的全部Pod的Endpoint列表。除此之外,StatefulSet在Headless Service的基础上又为StatefulSet控制的每个Pod副本创建了一个DNS域名,这个域名的格式为:

$(podname).(headless server name)   
FQDN: $(podname).(headless server name).namespace.svc.cluster.local
例如: web-0.account.default.svc.cluster.local

StatefulSet使用场景
– 稳定的持久化存储,即Pod重新调度后还是能访问到相同的持久化数据,基于PVC来实现。
– 稳定的网络标识符,即Pod重新调度后其PodName和HostName不变。
– 有序、优雅的部署和扩展,即通过initContainer实现
– 有序的自动滚动更新。

如使用场景描述,stable与Pod(重新)调度的持久性意思一致。如果应用程序不需要任何稳定网络标识符或有序部署、删除或扩展,则应使用提供一组无状态副本的控制器部署应用程序。Deployment或ReplicaSet等控制器可能更适合此种无状态需求。

StatefulSet示例

接下来看一些示例,演示下上面所说的特性,以加深理解。

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  selector:
    matchLabels:
      app: nginx 
  # 声明它属于哪个Headless Service.
  serviceName: "nginx"  
  replicas: 3 
  template:
    metadata:
      labels:
        app: nginx
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  # 可看作pvc的模板
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "glusterfs-ssd"
      resources:
        requests:
          storage: 1Gi

通过该配置文件,可看出StatefulSet的三个组成部分:

  • Headless Service:名为nginx,用来定义Pod网络标识( DNS domain)。
  • StatefulSet:定义具体应用,名为Nginx,有三个Pod副本,并为每个Pod定义了一个域名。
  • volumeClaimTemplates: 存储卷申请模板,创建PVC,指定pvc名称大小,将自动创建pvc,且pvc必须由存储类(storageClass)供应。

为什么需要 headless service 无头服务?

在用Deployment时,每一个Pod名称是没有顺序的,是随机字符串,因此是Pod名称是无序的,但是在statefulset中要求必须是有序 ,每一个pod不能被随意取代,pod重建后pod名称还是一样的。而pod IP是变化的,所以是以Pod名称来识别。pod名称是pod唯一性的标识符,必须持久稳定有效。这时候要用到无头服务,它可以给每个Pod一个唯一的名称 。

为什么需要volumeClaimTemplate?

对于有状态的副本集都会用到持久存储,对于分布式系统来讲,它的最大特点是数据是不一样的,所以各个节点不能使用同一存储卷,每个节点有自已的专用存储,但是如果在Deployment中的Pod template里定义的存储卷,是所有副本集共用一个存储卷,数据是相同的,因为是基于模板来的 ,而statefulset中每个Pod都要有自已的专有存储卷,所以statefulset的存储卷就不能再用Pod模板来创建了,于是statefulSet使用volumeClaimTemplate,称为卷申请模板,它会为每个Pod生成不同的pvc,并绑定pv, 从而实现各pod有专用存储。这就是为什么要用volumeClaimTemplate的原因。

创建:


$ kubectl create -f nginx.yaml service "nginx" created statefulset "web" created

看下这三个Pod创建过程:

# 第一个是创建web-0
$ kubectl get pod
web-0                     1/1       ContainerCreating   0          51s
# 待web-0 running且ready时,创建web-1
$ kubectl get pod
web-0                     1/1       Running             0          51s
web-1                     0/1       ContainerCreating   0          42s
# 待web-1 running且ready时,创建web-2
$ kubectl get pod
web-0                     1/1       Running             0          1m
web-1                     1/1       Running             0          45s
web-2                     1/1       ContainerCreating   0          36s
# 最后三个Pod全部running且ready
$ kubectl get pod
NAME                      READY     STATUS    RESTARTS   AGE
web-0                     1/1       Running   0          4m
web-1                     1/1       Running   0          3m
web-2                     1/1       Running   0          1m

根据volumeClaimTemplates自动创建的PVC

$ kubectl get pvc
NAME              STATUS    VOLUME                                  CAPACITY   ACCESS MODES   STORAGECLASS     AGE
www-web-0         Bound     pvc-ecf003f3-828d-11e8-8815-000c29774d39   2G        RWO          glusterfs-ssd         7m
www-web-1         Bound     pvc-0615e33e-828e-11e8-8815-000c29774d39   2G        RWO          glusterfs-ssd         6m
www-web-2         Bound     pvc-43a97acf-828e-11e8-8815-000c29774d39   2G        RWO          glusterfs-ssd         4m

如果集群中没有StorageClass的动态供应PVC的机制,也可以提前手动创建多个PV、PVC,手动创建的PVC名称必须符合之后创建的StatefulSet命名规则:(volumeClaimTemplates.name)-(pod_name)

Statefulset名称为web 三个Pod副本: web-0,web-1,web-2,volumeClaimTemplates名称为:www,那么自动创建出来的PVC名称为www-web[0-2],为每个Pod创建一个PVC。
规律总结:

  • 匹配Pod name(网络标识)的模式为:$(statefulset名称)-$(序号),比如上面的示例:web-0,web-1,web-2。
  • StatefulSet为每个Pod副本创建了一个DNS域名,这个域名的格式为: $(podname).(headless server name),也就意味着服务间是通过Pod域名来通信而非Pod IP,因为当Pod所在Node发生故障时,Pod会被飘移到其它Node上,Pod IP会发生变化,但是Pod域名不会有变化。
  • StatefulSet使用Headless服务来控制Pod的域名,这个域名的FQDN为:$(headless service name).$(namespace).svc.cluster.local,其中,“cluster.local”指的是集群的域名。
  • 根据volumeClaimTemplates,为每个Pod创建一个pvc,pvc的命名规则匹配模式:(volumeClaimTemplates.name)-(pod_name),比如上面的volumeMounts.name=www, Pod name=web-[0-2],因此创建出来的PVC是www-web-0、www-web-1、www-web-2。
  • 删除Pod不会删除其pvc,手动删除pvc将自动释放pv。

Statefulset的启停顺序:

  • 有序部署:部署StatefulSet时,如果有多个Pod副本,它们会被顺序地创建(从0到N-1)并且,在下一个Pod运行之前所有之前的Pod必须都是Running和Ready状态。
  • 有序删除:当Pod被删除时,它们被终止的顺序是从N-1到0。
  • 有序扩展:当对Pod执行扩展操作时,与部署一样,它前面的Pod必须都处于Running和Ready状态。 

Statefulset Pod管理策略:
在v1.7以后,通过允许修改Pod排序策略,同时通过.spec.podManagementPolicy字段确保其身份的唯一性。
– OrderedReady:上述的启停顺序,默认设置。
– Parallel:告诉StatefulSet控制器并行启动或终止所有Pod,并且在启动或终止另一个Pod之前不等待前一个Pod变为Running and Ready或完全终止。

更新策略

在Kubernetes 1.7及更高版本中,通过.spec.updateStrategy字段允许配置或禁用Pod、labels、source request/limits、annotations自动滚动更新功能。

  • OnDelete:通过.spec.updateStrategy.type 字段设置为OnDelete,StatefulSet控制器不会自动更新StatefulSet中的Pod。用户必须手动删除Pod,以使控制器创建新的Pod。
  • RollingUpdate:通过.spec.updateStrategy.type 字段设置为RollingUpdate,实现了Pod的自动滚动更新,如果.spec.updateStrategy未指定,则此为默认策略。StatefulSet控制器将删除并重新创建StatefulSet中的每个Pod。它将以Pod终止(从最大序数到最小序数)的顺序进行,一次更新每个Pod。在更新下一个Pod之前,必须等待这个Pod Running and Ready。
  • Partitions:通过指定 .spec.updateStrategy.rollingUpdate.partition 来对 RollingUpdate 更新策略进行分区,如果指定了分区,则当 StatefulSet 的 .spec.template 更新时,具有大于或等于分区序数的所有 Pod 将被更新。具有小于分区的序数的所有 Pod 将不会被更新,即使删除它们也将被重新创建。如果 StatefulSet 的 .spec.updateStrategy.rollingUpdate.partition 大于其 .spec.replicas,则其 .spec.template 的更新将不会传播到 Pod。在大多数情况下,不需要使用分区,但如果要进行灰度更新,可以尝试金丝雀部署或执行分阶段部署,它们更有用。

Prometheus Alertmanager报警组件

Prometheus Alertmanager

概述

Alertmanager与Prometheus是相互分离的两个组件。Prometheus服务器根据报警规则将警报发送给Alertmanager,然后Alertmanager将silencing、inhibition、aggregation等消息通过电子邮件、PaperDuty和HipChat发送通知。

设置警报和通知的主要步骤:

  • 安装配置Alertmanager
  • 配置Prometheus通过-alertmanager.url标志与Alertmanager通信
  • 在Prometheus中创建告警规则

Alertmanager简介及机制

Alertmanager处理由例如Prometheus服务器等客户端发来的警报。它负责删除重复数据、分组,并将警报通过路由发送到正确的接收器,比如电子邮件、Slack等。Alertmanager还支持groups,silencing和警报抑制的机制。

分组

分组是指将同一类型的警报分类为单个通知。当许多系统同时宕机时,很有可能成百上千的警报会同时生成,这种机制特别有用。
例如,当数十或数百个服务的实例在运行,网络发生故障时,有可能一半的服务实例不能访问数据库。在prometheus告警规则中配置为每一个服务实例都发送警报的话,那么结果是数百警报被发送至Alertmanager。

但是作为用户只想看到单一的报警页面,同时仍然能够清楚的看到哪些实例受到影响,因此,可以通过配置Alertmanager将警报分组打包,并发送一个相对看起来紧凑的通知。

分组警报、警报时间,以及接收警报的receiver是在alertmanager配置文件中通过路由树配置的。

抑制(Inhibition)

抑制是指当警报发出后,停止重复发送由此警报引发其他错误的警报的机制。(比如网络不可达,导致其他服务连接相关警报)

例如,当整个集群网络不可达,此时警报被触发,可以事先配置Alertmanager忽略由该警报触发而产生的所有其他警报,这可以防止通知数百或数千与此问题不相关的其他警报。

抑制机制也是通过Alertmanager的配置文件来配置。

沉默(Silences)

Silences是一种简单的特定时间不告警的机制。silences警告是通过匹配器(matchers)来配置,就像路由树一样。传入的警报会匹配RE,如果匹配,将不会为此警报发送通知。

这个可视化编辑器可以帮助构建路由树。

silences报警机制可以通过Alertmanager的Web页面进行配置。

Alermanager的配置

Alertmanager通过命令行flag和一个配置文件进行配置。命令行flag配置不变的系统参数、配置文件定义的抑制(inhibition)规则、通知路由和通知接收器。

要查看所有可用的命令行flag,运行alertmanager -h。
Alertmanager支持在运行时加载配置,如果新配置语法格式不正确,更改将不会被应用,并记录语法错误。通过向该进程发送SIGHUP或向/-/reload端点发送HTTP POST请求来触发配置热加载。

配置文件

要指定加载的配置文件,需要使用-config.file标志。该文件使用YAML来完成,通过下面的描述来定义。带括号的参数表示是可选的,对于非列表的参数的值,将被设置为指定的缺省值。

通用占位符定义解释:

  • \ : 与正则表达式匹配的持续时间值,[0-9]+(ms|[smhdwy])
  • : 与正则表达式匹配的字符串,[a-zA-Z_][a-zA-Z0-9_]*
  • : unicode字符串
  • : 有效的文件路径
  • : boolean类型,true或者false
  • : 字符串
  • : 模板变量字符串

global全局配置文件参数在所有配置上下文生效,作为其他配置项的默认值,可被覆盖.

global:
  # ResolveTimeout is the time after which an alert is declared resolved
  # if it has not been updated.
  #解决报警时间间隔
  [ resolve_timeout: <duration> | default = 5m ]

  # The default SMTP From header field.
  [ smtp_from: <tmpl_string> ]
  # The default SMTP smarthost used for sending emails.
  [ smtp_smarthost: <string> ]
  # SMTP authentication information.
  [ smtp_auth_username: <string> ]
  [ smtp_auth_password: <string> ]
  [ smtp_auth_secret: <string> ]
  # The default SMTP TLS requirement.
  [ smtp_require_tls: <bool> | default = true ]

  # The API URL to use for Slack notifications.
  [ slack_api_url: <string> ]

  [ pagerduty_url: <string> | default = "https://events.pagerduty.com/generic/2010-04-15/create_event.json" ]
  [ opsgenie_api_host: <string> | default = "https://api.opsgenie.com/" ]

# Files from which custom notification template definitions are read.
# The last component may use a wildcard matcher, e.g. 'templates/*.tmpl'.
templates:
  [ - <filepath> ... ]

# The root node of the routing tree.
route: <route>

# A list of notification receivers.
receivers:
  - <receiver> ...

# A list of inhibition rules.
inhibit_rules:
  [ - <inhibit_rule> ... ]

路由(route)

路由块定义了路由树及其子节点。如果没有设置的话,子节点的可选配置参数从其父节点继承。

每个警报都会在配置的顶级路由中进入路由树,该路由树必须匹配所有警报(即没有任何配置的匹配器)。然后遍历子节点。如果continue的值设置为false,它在第一个匹配的子节点之后就停止;如果continue的值为true,警报将继续进行后续子节点的匹配。如果警报不匹配任何节点的任何子节点(没有匹配的子节点,或不存在),该警报基于当前节点的配置处理。

路由配置格式

#报警接收器
[ receiver:  ]

#分组
[ group_by: '[' , ... ']' ]

# Whether an alert should continue matching subsequent sibling nodes.
[ continue:  | default = false ]

# A set of equality matchers an alert has to fulfill to match the node.
#根据匹配的警报,指定接收器
match:
  [ : , ... ]

# A set of regex-matchers an alert has to fulfill to match the node.
match_re:
#根据匹配正则符合的警告,指定接收器
  [ : , ... ]

# How long to initially wait to send a notification for a group
# of alerts. Allows to wait for an inhibiting alert to arrive or collect
# more initial alerts for the same group. (Usually ~0s to few minutes.)
[ group_wait:  ]

# How long to wait before sending notification about new alerts that are
# in are added to a group of alerts for which an initial notification
# has already been sent. (Usually ~5min or more.)
[ group_interval:  ]

# How long to wait before sending a notification again if it has already
# been sent successfully for an alert. (Usually ~3h or more).
[ repeat_interval:  ]

# Zero or more child routes.
routes:
  [ -  ... ]

例子:

# The root route with all parameters, which are inherited by the child
# routes if they are not overwritten.
route:
  receiver: 'default-receiver'
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  group_by: [cluster, alertname]
  # All alerts that do not match the following child routes
  # will remain at the root node and be dispatched to 'default-receiver'.
  routes:
  # All alerts with service=mysql or service=cassandra
  # are dispatched to the database pager.
  - receiver: 'database-pager'
    group_wait: 10s
    match_re:
      service: mysql|cassandra
  # All alerts with the team=frontend label match this sub-route.
  # They are grouped by product and environment rather than cluster
  # and alertname.
  - receiver: 'frontend-pager'
    group_by: [product, environment]
    match:
      team: frontend

抑制规则 inhibit_rule

抑制规则,是存在另一组匹配器匹配的情况下,使其他被引发警报的规则静音。这两个警报,必须有一组相同的标签。

抑制配置格式

# Matchers that have to be fulfilled in the alerts to be muted.
##必须在要需要静音的警报中履行的匹配者
target_match:
  [ : , ... ]
target_match_re:
  [ : , ... ]

# Matchers for which one or more alerts have to exist for the
# inhibition to take effect.
#必须存在一个或多个警报以使抑制生效的匹配者。
source_match:
  [ : , ... ]
source_match_re:
  [ : , ... ]

# Labels that must have an equal value in the source and target
# alert for the inhibition to take effect.
#在源和目标警报中必须具有相等值的标签才能使抑制生效
[ equal: '[' , ... ']' ]

接收器(receiver)

顾名思义,警报接收的配置。

  • 通用配置格式
# The unique name of the receiver.
name: 

# Configurations for several notification integrations.
email_configs:
  [ - , ... ]
pagerduty_configs:
  [ - , ... ]
slack_config:
  [ - , ... ]
opsgenie_configs:
  [ - , ... ]
webhook_configs:
  [ - , ... ]
  • 邮件接收器email_config
# Whether or not to notify about resolved alerts.
#警报被解决之后是否通知
[ send_resolved:  | default = false ]

# The email address to send notifications to.
to: 
# The sender address.
[ from:  | default = global.smtp_from ]
# The SMTP host through which emails are sent.
[ smarthost:  | default = global.smtp_smarthost ]

# The HTML body of the email notification.
[ html:  | default = '{{ template "email.default.html" . }}' ] 

# Further headers email header key/value pairs. Overrides any headers
# previously set by the notification implementation.
[ headers: { : , ... } ]

  • Slcack接收器slack_config
# Whether or not to notify about resolved alerts.
[ send_resolved:  | default = true ]

# The Slack webhook URL.
[ api_url:  | default = global.slack_api_url ]

# The channel or user to send notifications to.
channel: 

# API request data as defined by the Slack webhook API.
[ color:  | default = '{{ if eq .Status "firing" }}danger{{ else }}good{{ end }}' ]
[ username:  | default = '{{ template "slack.default.username" . }}'
[ title:  | default = '{{ template "slack.default.title" . }}' ]
[ title_link:  | default = '{{ template "slack.default.titlelink" . }}' ]
[ pretext:  | default = '{{ template "slack.default.pretext" . }}' ]
[ text:  | default = '{{ template "slack.default.text" . }}' ]
[ fallback:  | default = '{{ template "slack.default.fallback" . }}' ]
  • Webhook接收器webhook_config
 # Whether or not to notify about resolved alerts.
[ send_resolved:  | default = true ]

 # The endpoint to send HTTP POST requests to.
url: 

Alertmanager会使用以下的格式向配置端点发送HTTP POST请求:

{
  "version": "3",
  "groupKey":      // key identifying the group of alerts (e.g. to deduplicate)
  "status": "",
  "receiver": ,
  "groupLabels": ,
  "commonLabels": ,
  "commonAnnotations": ,
  "externalURL": ,  // backling to the Alertmanager.
  "alerts": [
    {
      "labels": ,
      "annotations": ,
      "startsAt": "",
      "endsAt": ""
    },
    ...
  ]
}

可以添加一个钉钉webhook,通过钉钉报警,由于POST数据需要有要求,简单实现一个数据转发脚本。

from flask import Flask
from flask import request
from urllib2 import Request,urlopen

import json

app = Flask(__name__)

@app.route('/',methods=['POST'])
def send():
    if request.method == 'POST':
        post_data = request.get_data()
        alert_data(post_data)
    return

def alert_data(data):
    url = 'https://oapi.dingtalk.com/robot/send?access_token=xxxx'
    send_data = '{"msgtype": "text","text": {"content": %s}}' %(data)
    request = Request(url, send_data)
    request.add_header('Content-Type','application/json')
    return urlopen(request).read()

if __name__ == '__main__':
    app.run(host='0.0.0.0')

报警规则

报警规则允许你定义基于Prometheus表达式语言的报警条件,并发送报警通知到外部服务

定义报警规则

报警规则通过以下格式定义:

ALERT 
  IF 
  [ FOR  ]
  [ LABELS <label> ]
  [ ANNOTATIONS <label> ]
  • 可选的FOR语句,使得Prometheus在表达式输出的向量元素(例如高HTTP错误率的实例)之间等待一段时间,将警报计数作为触发此元素。如果元素是active,但是没有firing的,就处于pending状态。

  • LABELS(标签)语句允许指定一组标签附加警报上。将覆盖现有冲突的任何标签,标签值也可以被模板化。

  • ANNOTATIONS(注释)它们被用于存储更长的其他信息,例如警报描述或者链接,注释值也可以被模板化。

  • Templating(模板) 标签和注释值可以使用控制台模板进行模板化。$labels变量保存警报实例的标签键/值对,$value保存警报实例的评估值。

    # To insert a firing element's label values:
    {{ $labels. }}
    # To insert the numeric expression value of the firing element:
    {{ $value }}
    

报警规则示例:

# Alert for any instance that is unreachable for >5 minutes.
ALERT InstanceDown
  IF up == 0
  FOR 5m
  LABELS { severity = "page" }
  ANNOTATIONS {
    summary = "Instance {{ $labels.instance }} down",
    description = "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 5 minutes.",
  }

# Alert for any instance that have a median request latency >1s.
ALERT APIHighRequestLatency
  IF api_http_request_latencies_second{quantile="0.5"} > 1
  FOR 1m
  ANNOTATIONS {
    summary = "High request latency on {{ $labels.instance }}",
    description = "{{ $labels.instance }} has a median request latency above 1s (current value: {{ $value }}s)",
  }

发送报警通知

Prometheus的警报rules可以很好的知道现在的故障情况,但还不是一个完整的通知解决方案。在简单的警报定义之上,需要另一层级来实现报警汇总,通知速率限制,silences等基于rules之上,在prometheus生态系统中,Alertmanager发挥了这一作用。因此,
Prometheus可以周期性的发送关于警报状态的信息到Alertmanager实例,然后Alertmanager调度来发送正确的通知。该Alertmanager可以通过-alertmanager.url命令行flag来配置。

参考文章:
– https://prometheus.io/docs/alerting/alertmanager/

Gitlab CI持续集成

概念

持续集成是一种软件开发实践,即团队开发成员经常集成它们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。
持续交付(Continuous delivery)指的是,频繁地将软件的新版本,交付给质量团队或者用户,以供评审。如果评审通过,代码就进入生产阶段。
持续部署(continuous deployment)是持续交付的下一步,指的是代码通过评审以后,自动部署到生产环境。

持续集成优势

快速发现错误

代码完成更新提交到分支仓库,可以快速跑自动集成测试,可以快速发现错误,定位错误也比较容易。

防止分支大幅偏离主干

如果不是经常集成,主干又在不断更新,会导致以后集成的难度变大,甚至难以集成。
持续集成的目的,就是让产品可以快速迭代,同时还能保持高质量。它的核心措施是,代码集成到主干之前,必须通过自动化测试。只要有一个测试用例失败,就不能集成。

GitLab原理

  • gitlab-ci server 负责调度、触发Runner,以及获取返回结果
  • gitlab-ci-runner 则是主要负责来跑自动化CI的一个宿主机子或者Docker等。

组成

Gitlab-CI相关概念

pipeline

每次代码提交或者merge request的合并就会触发一次pipeline(内容其实就是gitlab-ci.yml)。一次pipeline可以看成一次构建任务。构建任务一般会包含:安装依赖,测试,编译,部署服务等多个阶段。

| Pipeline |
| +———–+ +————+ +————+ |
| | Stage 1 |—>| Stage 2 |—>| Stage 3 | |
| +———–+ +————+ +————+ |

一个简单的gitlab-ci.yml示例

# 定义 stages
stages:
  - build
  - test
# 定义 job
job1:
  stage: test
  script:
    - echo "I am job1"
    - echo "I am in test stage"
# 定义 job
job2:
  stage: build
  script:
    - echo "I am job2"
    - echo "I am in build stage"

运行结果

I am job2
I am in build stage
I am job1
I am in test stage
stage

stage就是上述构建任务中的各个构建阶段。一个pipeline可以定义多个stage。stage有以下特点:

  1. 所有的stage按顺序运行,前一个stage完成后,下一个stage才会开始执行。
  2. 只有当所有的stage都完成后,该构建任务(pipeline)才会成功。
  3. 如果一个stage失败,那么下一个stage不会执行,该构建任务(pipeline)失败。

| Stage 1 |
| +———+ +———+ +———+ |
| | Job 1 | | Job 2 | | Job 3 | |
| +———+ +———+ +———+ |

stages中的元素顺序决定了对应job的执行顺序:
1. 相同stage的job可以平行执行。
2. 下一个stage的job会在前一个stage的job成功后开始执行。

job

job表示构建工作,是每个stage构建阶段里具体执行的工作。跟pipeline与stage的关系类似,stage与job也是一对多的关系,即一个stage里可以定义多个job,而job具有以下特点:
1. 同一个 stage 中的 jobs 会并行执行
2. 同一个 stage 中的 jobs 都执行成功时,该 stage 才会成功
3. 如果任何一个 job 失败,那么该 stage 失败,即该构建任务 (pipeline) 失败

GitLab CI介绍

GitLab CI是 GitLab 提供的持续集成服务,只要在你的仓库根目录 创建一个.gitlab-ci.yml 文件, 并为该项目指派一个Runner,当有合并请求或者 push的时候就会触发build。
这个.gitlab-ci.yml 文件定义GitLab runner要做哪些操作。 默认有3个[stages(阶段)]: build、test、deploy。
当build完成后(返回非零值),你会看到push的 commit或者合并请求前面出现一个绿色的对号。 这个功能很方便的让你检查出来合并请求是否会导致build失败, 免的你去检查代码。

CI工作流程

在仓库根目录创建一个名为.gitlab-ci.yml 的文件
为该项目配置一个Runner
GitLab-Runner是配合GitLab-CI进行使用的。一般地,GitLab里面的每一个工程都会定义一个属于这个工程的软件集成脚本,用来自动化地完成一些软件集成工作。当这个工程的仓库代码发生变动时,比如有人push了代码,GitLab就会将这个变动通知GitLab-CI。这时GitLab-CI会找出与这个工程相关联的Runner,并通知这些Runner把代码更新到本地并执行预定义好的执行脚本。
所以,GitLab-Runner就是一个用来执行软件集成脚本的东西。你可以想象一下:Runner就像一个个的工人,而GitLab-CI就是这些工人的一个管理中心,所有工人都要在GitLab-CI里面登记注册,并且表明自己是为哪个工程服务的。当相应的工程发生变化时,GitLab-CI就会通知相应的工人执行软件集成脚本。如下图所示:

GitLab Runner构建任务

一般来说,构建任务都会占用很多的系统资源 (譬如编译代码),而 GitLab CI 又是 GitLab 的一部分,如果由 GitLab CI 来运行构建任务的话,在执行构建任务的时候,GitLab 的性能会大幅下降。
GitLab CI 最大的作用是管理各个项目的构建状态,因此,运行构建任务这种浪费资源的事情就交给 GitLab Runner 来做了
因为 GitLab Runner 可以安装到不同的机器上,所以在构建任务运行期间并不会影响到 GitLab 的性能

Runner配置

注册special runner
输入命令

$ sudo gitlab-runner register

会要求输入gitlab的url和Token.
查找过程如下:
进入仓库->settings->CI/CD,找到Runners这一项,点击Expend,即可在Set up a group Runner manually这项中找到。如下:

gitlab-ci.yml

当有新内容push到仓库后,GitLab会查找是否有.gitlab-ci.yml文件,如果文件存在, Runners 将会根据该文件的内容开始build 本次commit。
推送配置文件
配置好.gitlab-ci.yml文件之后,只要把它加入git后然后推送到远程仓库,CI就会开始自动化集成。这样我们提交代码到GitLab是在每次提交的后面都会有自动测试的结果

.gitlab-ci.yml 用来配置 CI 用你的项目中做哪些操作,这个文件位于仓库(项目)的根目录。
当有新内容push到仓库后,GitLab会查找是否有.gitlab-ci.yml文件,如果文件存在, Runners 将会根据该文件的内容开始build 本次commit。
.gitlab-ci.yml 使用YAML语法, 你需要格外注意缩进格式,要用空格来缩进,不能用tabs来缩进。

job由定义作业行为的参数列表定义。

关键词 必须 描述
script 定义由Runner执行的shell脚本
Image 使用Docker镜像,使用Docker Images进行了介绍
services 使用Docker服务,使用Docker Images
stage 定义一个工作阶段(默认:test)
type 别名为 stage
variables 在作业级别定义作业变量
Only 定义一列git分支,并为其创建job
except 定义一列git分支,不创建job
tags 定义用于选择Runner的标记列表
allow_failure 允许job失败。失败的job不影响commit状态
When 定义何时运行作业。可以是on_success,on_failure,always或者manual
Dependencies 定义作业所依赖的其他作业,以便您可以在它们之间传递工件
Artifacts 定义作业工件列表 用于指定成功后应附加到作业的文件和目录列表。 作业成功完成后,工件将被发送到 GitLab,并可在GitLab UI中下载。
Cache 定义后续运行之间应缓存的文件列表
before_script 重写在作业之前执行的一组命令
after_script 重写作业后执行的一组命令
Environment 定义此作业完成部署的环境名称
Coverage 定义给定作业的代码覆盖率设置
Retry 定义在发生故障时可以自动重试作业的次数

pages

pages是一项特殊工作,用于将静态内容上传到GitLab,可用于为您的网站提供服务。它有一个特殊的语法,因此必须满足以下两个要求:
任何静态内容都必须放在public/目录下
artifacts public/必须定义目录的路径
下面的示例只是将项目根目录中的所有文件移动到 public/目录中。该.public解决方法是这样cp不也复制 public/到自身无限循环

pages:
  stage: deploy
  script:
    - mkdir .public
    - cp -r * .public
    - mv .public public
  artifacts:
    paths:
      - public
  only:
    - master

image

image:docker image
docker image执行器将会执行CI任务

services

该services关键字仅定义在作业期间运行的另一个Docker镜像,并链接到image关键字定义的Docker镜像。这允许您在构建期间访问服务映像。
服务映像可以运行任何应用程序,但最常见的用例是运行数据库容器,例如mysql。使用现有映像并将其作为附加容器运行比在mysql每次构建项目时安装更容易,更快捷。

Image and services in .gitlab-ci.yml

您可以简单地定义将用于所有job的image以及要在构建期间使用的services列表:

image: hub.pri.ibanyu.com/devops/golang:1.9 
services:
- hub.pri.ibanyu.com/devops/docker:dind
- hub.pri.ibanyu.com/devops/mysql:5.7.17
image: ruby:2.2
services:
  - postgres:9.3
before_script:
  - bundle install
test:           
  script:
  - bundle exec rake spec

每个作业也可以定义不同的图像和服务:

before_script:
  - bundle install
test:2.1:
  image: ruby:2.1
  services:
  - postgres:9.3
  script:
  - bundle exec rake spec
test:2.2:
  image: ruby:2.2
  services:
  - postgres:9.4
  script:
  - bundle exec rake spec

before_script 和 after_script

before_script用于定义应在所有作业(包括部署作业)之前运行但在恢复工件之后运行的命令。这可以是数组或多行字符串。
after_script用于定义将在所有作业(包括失败作业)之后运行的命令。这必须是数组或多行字符串。
before_script和主script是级联的并且在在单一上下文/容器中运行after_script是单独运行的,因此根据执行程序,在工作树之外完成的更改可能不可见,例如,安装在工作树中的软件 before_script。
可以覆盖全局定义的before_script,after_script 如果您按工作设置它:

before_script:
  - global before script
job:
  before_script:
    - execute this instead of global before script
  script:
    - my command
  after_script:
    - execute this after my script

stages

stages 用于定义可由作业使用的阶段,并且是全局定义的。
规范stages允许具有灵活的多级管道。元素stages的排序定义了作业执行的顺序:
同一阶段的工作是并行运行的。
下一阶段的作业在上一阶段的作业成功完成后运行。
让我们考虑以下示例,它定义了3个阶段:
stages:
– build
– test
– deploy

首先,所有工作build都是并行执行的。
如果所有作业都build成功,则test作业将并行执行。
如果所有作业都test成功,则deploy作业将并行执行。
如果所有作业都deploy成功,则提交标记为passed。
如果任何先前的作业失败,则将提交标记为,failed并且不执行其他阶段的作业。
还有两个值得一提的边缘案例:
如果没有stages被定义.gitlab-ci.yml,那么build, test和deploy允许被用作默认作业的阶段。
如果作业未指定a stage,则为作业分配test。
stages中的元素顺序决定了对应job的执行顺序:

  1. 相同stage的job可以平行执行。
  2. 下一个stage的job会在前一个stage的job成功后开始执行。

stage

stage是按工作定义的,依赖于stages全局定义的。它允许将作业分组到不同的阶段,并stage执行相同的作业 parallel。例如:

stages:
  - build
  - test
  - deploy
job 1:
  stage: build
  script: make build dependencies
job 2:
  stage: build
  script: make build artifacts
job 3:
  stage: test
  script: make test
job 4:
  stage: deploy
  script: make deploy

script

script是作业所需的唯一必需关键字。这是一个由Runner执行的shell脚本。例如:

job:
  script: "bundle exec rspec"

此参数还可以包含使用数组的多个命令:

job:
  script:   
    - uname -a
    - bundle exec rspec

有时,script命令需要用单引号或双引号括起来。例如,包含冒号(:)的命令需要用引号括起来,以便YAML解析器知道将整个事物解释为字符串而不是“键:值”对。使用特殊字符时要小心: :,{,},[,],,,&,*,#,?,|,-,,=,!,%,@,`。

only和except

only和except是两个参数用分支策略来限制jobs构建:

only定义哪些分支和标签的git项目将会被job执行。
except定义哪些分支和标签的git项目将不会被job执行。
下面是refs策略的使用规则:

only和except可同时使用。如果only和except在一个job配置中同时存在,则以only为准,跳过except(从下面示例中得出)。
only和except可以使用正则表达式。
only和except允许使用特殊的关键字:branches,tags和triggers。
only和except允许使用指定仓库地址但不是forks的仓库

Kubernetes Operator

What is Operator?

Kubernetes Operator: 以软件定义的方式来管理运维操作。

A Site Reliability Engineer (SRE) is a person that operates an application by writing software. They are an engineer, a developer, who knows how to develop software specifically for a particular application domain. The resulting piece of software has an application’s operational domain knowledge programmed into it.

Operator是管理特定应用程序的控制器,通过扩展kubernetes api以软件的方式帮助kubernetes用户创建,配置和管理复杂有状态的应用程序实例(etcd,redis,mysql,prometheus等等)。它建立在基本的Kubernetes资源和控制器概念的基础上,它包含管理特定应用程序的操作以及实现常见任务的自动化。

Stateless is Easy, Stateful is Hard

有了kubernetes,管理和扩展web应用,移动后端和api服务就变得相对容易了。
why?因为这些应用程序通常是无状态的,可以通过基本的Kubernetes APIs就能run起来,例如通过Deployments资源,可以在没有额外的知识的情况下扩展我们的应用程序并可以从故障中恢复。

一个更大的挑战是管理有状态的应用程序,如数据库,缓存和监控系统。
这些系统需要学习相关的知识来正确扩展,升级和重新加载配置,同时防止数据丢失或不可用。我们希望将这种特定于应用程序的操作知识通过编码解决,利用强大的kubernetes抽象的软件实现,以正确运行和管理应用程序。

The Operator Framework

Operator Framework是一个开源项目,提供开发人员和Kubernetes运行时工具,使我们能够加速operator的开发。operator Framework包括:

  • Operator SDK
    使开发人员能够基于他们的专业知识构建operator,而无需了解Kubernetes API的复杂性。
  • Operator lifecycle manager
    监督Kubernetes集群中运行的所有operator(及其相关服务)的安装,更新和管理整个生命周期。
  • Operator Metering
    Operator Metering(未来几个月加入):为提供专业服务的operator启用使用情况报告。

Build with the Operator SDK


Operator SDK提供了build,test和package操作。最初,SDK有助于将应用程序的业务逻辑(例如,如何扩展,升级或备份)与Kubernetes API结合起来执行这些操作。随着时间的推移,SDK可以让工程师更智能地使应用程序具有云服务的用户体验。
SDK中包含操作员共享的主要实践和代码模式,以帮助防止重新发明轮子。更多的想明白Oprator是如何通过编码实现的,coreos已经开源了两个例子:

etcd Operator
etcd operator 创建,配置和管理etcd集群。etcd是由coreos开源的可靠的分布式键值存储系统,用于维护分布式系统中的最关键数据,是kubernetes的主要配置数据存储。
Prometheus Operator
prometheus operator创建,配置和管理prometheus监控实例。prometheus是一个强大的监控,指标和报警系统,也是由coreos团队支持的云本机计算基础(cncf)项目。

How is an Operator Built?

Operator构建基于两个重要的kubernetes概念:Resources 和 Controllers。
例如,内置的ReplicaSet资源允许用户设置所需数量的Pod来运行,并且Kubernetes内的控制器通过创建或删除正在运行的Pod来确保在ReplicaSet资源中设置的状态保持为true。Kubernetes中有许多以这种方式工作的基础控制器和资源,包括Services,Deployments
和Daemon Sets。
以etcd Operator为例,operator是建立在基本的资源和控制器概念基础上,增加了一套知识或配置,允许operator执行普通的应用任务。例如,在手动扩展一个etcd集群时,用户必须执行多个步骤:为新的etcd成员创建一个DNS名称,再启动新的etcd实例,然后使用etcd管理工具(etcdctl member add)告诉现有集群这个新成员加入,集群新增1个etcd实例完成。 而用etcd operator,用户可以简单地将etcd cluster大小规模增加1个实例。

Lifecycle of an Operator


构建后,需要在Kubernetes集群上部署operator。Operator Lifecycle Manager是便于管理Kubernetes集群上operator的背板。有了它,管理员可以控制operator在哪些命名空间中可用,以及谁可以与正在运行的操作员交互。他们还可以管理运营商及其资源的整个生命周期,例如触发对运营商及其资源的更新。