Kubernetes全栈架构师(基本概念)--学习笔记
目录
- 为什么要用Kubernetes?
- K8s控制节点-Master概念
- K8s计算节点-Node概念
- 什么是Pod?
- 为什么要引入Pod?
- 创建一个Pod
- 零宕机发布应用必备知识:Pod三种探针
- 零宕机必备知识:StartupProbe
- 零宕机必备知识:Liveness和Readiness
- 零宕机必备知识:Pod退出流程
- 零宕机必备知识:PreStop的使用
为什么要用Kubernetes?
- 容器管理
- 自动恢复
- 健康检查
- 弹性扩容
- 内部通讯
- 高可用
K8s控制节点-Master概念
Kubernetes是谷歌以Borg为前身,基于谷歌15年生产环境经验的基础上开源的一个项目,Kubernetes致力于提供跨主机集群的自动部署、扩展、高可用以及运行应用程序容器的平台。
k8s高可用架构解析
k8s节点一般分为master节点和node节点,master节点一般三个足以,三个master节点承载成百上千node节点完全没有问题,node节点可以横向扩容
node节点用于部署应用程序,master节点不允许部署应用程序,它只负责控制,调度工作
Master节点:整个集群的控制中枢
Kube-APIServer
集群的控制中枢,各个模块之间信息交互都需要经过Kube-APIServer,同时它也是集群管理、资源配置、整个集群安全机制的入口。
Controller-Manager
集群的状态管理器,保证Pod或其他资源达到期望值,也是需要和APIServer进行通信,在需要的时候创建、更新或删除它所管理的资源。
Scheduler
集群的调度中心,它会根据指定的一系列条件,选择一个或一批最佳的节点,然后部署我们的Pod。
Etcd
键值数据库,报错一些集群的信息,一般生产环境中建议部署三个以上节点(奇数个)。
注意
master节点在安装完成之后,可能在很长一段时间都不会有任何的变化,所以在进行架设计的时候,要给足master节点资源,因为每次修改master节点是一件特别复杂的事情
我们在master节点绑定证书,每个证书绑定在master节点的ip地址或者主机名,如果我们在之前生成证书的时候没有预留的话,那我们可能就需要重新生成一份证书,再把之前的证书都替换掉,而且还要替换node节点上面的证书,过程非常麻烦,所以一开始要给足资源,比如一次性给三台16核64G
Etcd也特别重要,一次性给足资源,未来五到十年,node节点的个数在500到1000之间的话,我们的master节点是完全不需要做任何变化的
K8s计算节点-Node概念
node节点和master节点的区别:node节点比较具有动态性,添加、删除
Node:工作节点
Kubelet
Kubelet:负责监听节点上Pod的状态,同时负责上报节点和节点上面Pod的状态,负责与Master节点通信,并管理节点上面的Pod。
Kube-proxy
Kube-proxy:负责Pod之间的通信和负载均衡,将指定的流量分发到后端正确的机器上。
查看Kube-proxy工作模式
[root@k8s-master01 dockerfiles]# netstat -lntp |grep kube-proxy
tcp 0 0 0.0.0.0:30372 0.0.0.0:* LISTEN 1064/kube-proxy
tcp 0 0 127.0.0.1:10249 0.0.0.0:* LISTEN 1064/kube-proxy
tcp6 0 0 :::10256 :::* LISTEN 1064/kube-proxy
[root@k8s-master01 dockerfiles]# curl 127.0.0.1:10249/proxyMode
ipvs[root@k8s-master01 dockerfiles]#
Ipvs
监听Master节点增加和删除service以及endpoint的消息,调用Netlink接口创建相应的IPVS规则。通过IPVS规则,将流量转发至相应的Pod上。
Ipvs映射规则
# 查看配置规则,主机访问30372端口就可以访问到172.25.244.214
[root@k8s-master01 ~]# ipvsadm -ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 172.17.0.1:30372 rr
-> 172.25.244.214:8443 Masq 1 0 0
# kubernetes-dashboard通过端口30372(kube-proxy)映射出去
[root@k8s-master01 dockerfiles]# kubectl get svc -n kubernetes-dashboard
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
dashboard-metrics-scraper ClusterIP 10.109.161.186 <none> 8000/TCP 6d22h
kubernetes-dashboard NodePort 10.96.188.229 <none> 443:30372/TCP 6d22h
# 172.25.244.214正好时pod的ip地址
[root@k8s-master01 ~]# kubectl get po -n kubernetes-dashboard -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
dashboard-metrics-scraper-856586f554-ssjhm 1/1 Running 0 7d20h 172.18.195.8 k8s-master03 <none> <none>
kubernetes-dashboard-67484c44f6-brz2z 1/1 Running 2 (3m56s ago) 7d20h 172.25.244.214 k8s-master01 <none> <none>
主机访问node节点30372端口,通过ipvs规则,反向代理到kubernetes-dashboard上面的ip地址172.25.244.214的8443端口,所以就能访问到dashboard
Iptables
监听Master节点增加和删除service以及endpoint的消息,对于每一个Service,他都会创建一个iptables规则,将service的clusterIP代理到后端对应的Pod。
不推荐使用Iptables的原因是:当我们的规则特别多的时候,它的性能就会急剧下降
其他组件
Calico:符合CNI标准的网络插件,给每个Pod生成一个唯一的IP地址,并且把每个节点当做一个路由器。Cilium,eBPF
CoreDNS:用于Kubernetes集群内部Service的解析,可以让Pod把Service名称解析成IP地址,然后通过Service的IP地址进行连接到对应的应用上。
Docker:容器引擎,负责对容器的管理。
什么是Pod?
Pod是Kubernetes中最小的单元,它由一组、一个或多个容器组成,每个Pod还包含了一个Pause容器,Pause容器是Pod的父容器,主要负责僵尸进程的回收管理,通过Pause容器可以使同一个Pod里面的多个容器共享存储、网络、PID、IPC等。
查看系统pod
[root@k8s-master01 ~]# kubectl get po -n kube-system
NAME READY STATUS RESTARTS AGE
metrics-server-64c6c494dc-lhkl2 1/1 Running 1 (82m ago) 87m
Pause容器
[root@k8s-master01 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b30c03e6e0ff 9759a41ccdf0 "/metrics-server --c…" About an hour ago Up About an hour k8s_metrics-server_metrics-server-64c6c494dc-lhkl2_kube-system_517b9ab1-a323-4530-8be4-ebd3a7d41de4_1
f45e50c4009e registry.cn-hangzhou.aliyuncs.com/google_containers/pause-amd64:3.2 "/pause" About an hour ago Up About an hour k8s_POD_metrics-server-64c6c494dc-lhkl2_kube-system_517b9ab1-a323-4530-8be4-ebd3a7d41de4_1
为什么要引入Pod?
因为一个应用不可能单个容器就能支撑的,需要很多微服务支撑,可能出现一种情况就是两个服务,A服务和B服务之间需要网络互通,延迟非常小,而且两个服务有数据的依赖性,服务B需要用到服务A产生的文件,如果直接用k8s裸机的话,服务A和服务B不一定会在同一台宿主机上,当副本数非常大的时候,很难保证两个文件可以共享一个目录
每个pod有一个唯一的ip地址,便于管理
从k8s的角度看,它作为一个非常流行的编排工具,需要兼容很多的容器技术,所以通过pod管理不同的符合该标准的容器,而没有直接操作容器
其他容器技术:https://kubernetes.io/docs/setup/production-environment/container-runtimes/
创建一个Pod
[root@k8s-master01 ~]# vim pod.yaml
:set paste
# 添加以下内容
apiVersion: v1 # 必选,API的版本号
kind: Pod # 必选,类型Pod
metadata: # 必选,元数据
name: nginx # 必选,符合RFC 1035规范的Pod名称
# namespace: default # 可选,Pod所在的命名空间,不指定默认为default,可以使用-n 指定namespace
labels: # 可选,标签选择器,一般用于过滤和区分Pod
app: nginx
role: frontend # 可以写多个
annotations: # 可选,注释列表,可以写多个
app: nginx
spec: # 必选,用于定义容器的详细信息
# initContainers: # 初始化容器,在容器启动之前执行的一些初始化操作
# - command:
# - sh
# - -c
# - echo "I am InitContainer for init some configuration"
# image: busybox
# imagePullPolicy: IfNotPresent
# name: init-container
containers: # 必选,容器列表
- name: nginx # 必选,符合RFC 1035规范的容器名称
image: nginx:latest # 必选,容器所用的镜像的地址
imagePullPolicy: IfNotPresent # 可选,镜像拉取策略,IfNotPresent:如果宿主机又这个镜像,那就不需要拉取,Always:总是拉取,Never:不管是否存储都不拉取
command: # 可选,容器启动执行的命令 ENTRYPOINT,arg --> cmd
- nginx
- -g
- "daemon off;"
workingDir: /usr/share/nginx/html # 可选,容器的工作目录
# volumeMounts: # 可选,存储卷配置,可以配置多个
# - name: webroot # 存储卷名称
# mountPath: /usr/share/nginx/html # 挂载目录
# readOnly: true # 只读
ports: # 可选,容器需要暴露的端口号列表
- name: http # 端口名称
containerPort: 80 # 端口号
protocol: TCP # 端口协议,默认TCP
env: # 可选,环境变量配置列表
- name: TZ # 变量名
value: Asia/Shanghai # 变量的值
- name: LANG
value: en_US.utf8
# resources: # 可选,资源限制和资源请求限制
# limits: # 最大限制设置
# cpu: 1000m
# memory: 1024Mi
# requests: # 启动所需的资源
# cpu: 100m
# memory: 512Mi
# startupProbe: # 可选,检测容器内进程是否完成启动。注意三种检查方式同时只能使用一种。
# httpGet: # httpGet检测方式,生产环境建议使用httpGet实现接口级健康检查,健康检查由应用程序提供。
# path: /api/successStart # 检查路径
# port: 80
# readinessProbe: # 可选,健康检查。注意三种检查方式同时只能使用一种。
# httpGet: # httpGet检测方式,生产环境建议使用httpGet实现接口级健康检查,健康检查由应用程序提供。
# path: / # 检查路径
# port: 80 # 监控端口
# livenessProbe: # 可选,健康检查
#exec: # 执行容器命令检测方式
#command:
#- cat
#- /health
#httpGet: # httpGet检测方式
# path: /_health # 检查路径
# port: 8080
# httpHeaders: # 检查的请求头
# - name: end-user
# value: Jason
# tcpSocket: # 端口检测方式
# port: 80
# initialDelaySeconds: 60 # 初始化时间
# timeoutSeconds: 2 # 超时时间
# periodSeconds: 5 # 检测间隔
# successThreshold: 1 # 检查成功为2次表示就绪
# failureThreshold: 2 # 检测失败1次表示未就绪
# lifecycle:
# postStart: # 容器创建完成后执行的指令, 可以是exec httpGet TCPSocket
# exec:
# command:
# - sh
# - -c
# - 'mkdir /data/ '
# preStop:
# httpGet:
# path: /
# port: 80
# exec:
# command:
# - sh
# - -c
# - sleep 9
restartPolicy: Always # 可选,默认为Always,容器故障或者没有启动成功,那就自动重启该容器,Onfailure:容器以不为0的状态终止,Never:无论何种状态,都不会重启
#nodeSelector: # 可选,指定Node节点
# region: subnet7
# imagePullSecrets: # 可选,拉取镜像使用的secret,可以配置多个
# - name: default-dockercfg-86258
# hostNetwork: false # 可选,是否为主机模式,如是,会占用主机端口
# volumes: # 共享存储卷列表
# - name: webroot # 名称,与上述对应
# emptyDir: {} # 挂载目录
#hostPath: # 挂载本机目录
# path: /etc/hosts
创建pod
[root@k8s-master01 ~]# kubectl create -f pod.yaml
pod/nginx created
查看pod
[root@k8s-master01 ~]# kubectl get po
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 3m51s
查看labels
[root@k8s-master01 ~]# kubectl get po --show-labels
NAME READY STATUS RESTARTS AGE LABELS
nginx 1/1 Running 0 3m57s app=nginx,role=frontend
label与yaml文件中的配置一致
labels: # 可选,标签选择器,一般用于过滤和区分Pod
app: nginx
role: frontend # 可以写多个
删除pod
[root@k8s-master01 ~]# kubectl delete po nginx
pod "nginx" deleted
查看pod
[root@k8s-master01 ~]# kubectl get po
No resources found in default namespace.
删除之后就找不到pod了,所以生产环境中一般不会直接使用,很难保证业务正常运行,一般使用高级资源deployment,daemonset,StatefulSets
零宕机发布应用必备知识:Pod三种探针
- Pod探针
- Pod探针的检测方式
Pod探针
- StartupProbe
- LivenessProbe
- ReadinessProbe
StartupProbe
StartupProbe:k8s1.16版本后新加的探测方式,用于判断容器内应用程序是否已经启动。如果配置了startupProbe,就会先禁止其他的探测,直到它成功为止,成功后将不在进行探测。
# startupProbe: # 可选,检测容器内进程是否完成启动。注意三种检查方式同时只能使用一种。
# httpGet: # httpGet检测方式,生产环境建议使用httpGet实现接口级健康检查,健康检查由应用程序提供。
# path: /api/successStart # 检查路径
# port: 80
LivenessProbe
LivenessProbe:用于探测容器是否运行,如果探测失败,kubelet会根据配置的重启策略进行相应的处理。若没有配置该探针,默认就是success。
# livenessProbe: # 可选,健康检查
#exec: # 执行容器命令检测方式
#command:
#- cat
#- /health
ReadinessProbe
ReadinessProbe:一般用于探测容器内的程序是否健康,它的返回值如果为success,那么久代表这个容器已经完成启动,并且程序已经是可以接受流量的状态。
# readinessProbe: # 可选,健康检查。注意三种检查方式同时只能使用一种。
# httpGet: # httpGet检测方式,生产环境建议使用httpGet实现接口级健康检查,健康检查由应用程序提供。
# path: / # 检查路径
# port: 80 # 监控端口
Pod探针的检测方式
- ExecAction
- TCPSocketAction
- HTTPGetAction
ExecAction
ExecAction:在容器内执行一个命令,如果返回值为0,则认为容器健康。
# livenessProbe: # 可选,健康检查
#exec: # 执行容器命令检测方式
#command:
#- cat
#- /health
TCPSocketAction
TCPSocketAction:通过TCP连接检查容器内的端口是否是通的,如果是通的就认为容器健康。
HTTPGetAction
HTTPGetAction:通过应用程序暴露的API地址来检查程序是否是正常的,如果状态码为200~400之间,则认为容器健康。
生产环境推荐使用HTTPGetAction
零宕机必备知识:StartupProbe
查看coredns的deployment文件
[root@k8s-master01 ~]# kubectl edit deployment coredns -n kube-system
查找livenessProbe
livenessProbe:
failureThreshold: 5
httpGet:
path: /health
port: 8080
scheme: HTTP
initialDelaySeconds: 60
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
它请求了8080端口的/health,如果检测成功,则容器不会被重启
查找readinessProbe
readinessProbe:
failureThreshold: 3
httpGet:
path: /ready
port: 8181
scheme: HTTP
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
它请求了8181端口的/ready,如果检测成功,则可以加上endpoint,开始接受流量,开始工作
探针检查参数配置
# initialDelaySeconds: 60 # 初始化时间
# timeoutSeconds: 2 # 超时时间
# periodSeconds: 5 # 检测间隔
# successThreshold: 1 # 检查成功为1次表示就绪
# failureThreshold: 2 # 检测失败2次表示未就绪
为什么要引入StartupProbe?
如果容器启动特别慢,单独配置一个StartupProbe,它会先禁用另外两个探针,直到程序启动完成,再检测它的状态
编辑pod.yaml,取消注释
[root@k8s-master01 ~]# vim pod.yaml
startupProbe: # 可选,检测容器内进程是否完成启动。注意三种检查方式同时只能使用一种。
httpGet: # httpGet检测方式,生产环境建议使用httpGet实现接口级健康检查,健康检查由应用程序提供。
path: /api/successStart # 检查路径
port: 80
启动容器
修改pod.yaml
# httpGet: # httpGet检测方式,生产环境建议使用httpGet实现接口级健康检查,健康检查由应用程序提供。
# path: /api/successStart # 检查路径
# port: 80
tcpSocket:
port: 80
构建容器
[root@k8s-master01 ~]# kubectl create -f pod.yaml
pod/nginx created
获取IP
[root@k8s-master01 ~]# kubectl get po -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx 1/1 Running 0 2m31s 172.25.244.217 k8s-master01 <none> <none>
访问nginx
[root@k8s-master01 ~]# curl 172.25.244.217
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
零宕机必备知识:Liveness和Readiness
编辑pod.yaml
[root@k8s-master01 ~]# vim pod.yaml
# 修改内容如下
env: # 可选,环境变量配置列表
- name: TZ # 变量名
value: Asia/Shanghai # 变量的值
- name: LANG
value: en_US.utf8
readinessProbe: # 可选,健康检查。注意三种检查方式同时只能使用一种。
httpGet: # httpGet检测方式,生产环境建议使用httpGet实现接口级健康检查,健康检查由应用程序提供。
path: / # 检查路径
port: 80 # 监控端口
livenessProbe: # 可选,健康检查
exec: # 执行容器命令检测方式
command:
- pgrep nginx
initialDelaySeconds: 3 # 初始化时间
timeoutSeconds: 2 # 超时时间
periodSeconds: 2 # 检测间隔
successThreshold: 1 # 检查成功为2次表示就绪
failureThreshold: 1 # 检测失败1次表示未就绪
创建pod
[root@k8s-master01 ~]# kubectl delete po nginx
pod "nginx" deleted
[root@k8s-master01 ~]# kubectl create -f pod.yaml
pod/nginx created
查看pod
[root@k8s-master01 ~]# kubectl get po
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 4s
[root@k8s-master01 ~]# kubectl describe po nginx
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 10s default-scheduler Successfully assigned default/nginx to k8s-master01
Warning Unhealthy 2s (x2 over 6s) kubelet Liveness probe failed: OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: exec: "pgrep nginx": executable file not found in $PATH: unknown
Normal Pulled 1s (x3 over 9s) kubelet Container image "nginx:latest" already present on machine
Normal Created 1s (x3 over 9s) kubelet Created container nginx
Normal Started 1s (x3 over 9s) kubelet Started container nginx
Normal Killing 1s (x2 over 5s) kubelet Container nginx failed liveness probe, will be restarted
可以看到如果exec的命令不存在会报错Liveness probe failed
修改exec命令为ls
exec: # 执行容器命令检测方式
command:
- ls
重新创建
[root@k8s-master01 ~]# kubectl delete po nginx
pod "nginx" deleted
[root@k8s-master01 ~]# kubectl create -f pod.yaml
pod/nginx created
[root@k8s-master01 ~]# kubectl get po
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 4s
[root@k8s-master01 ~]# kubectl describe po nginx
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 8s default-scheduler Successfully assigned default/nginx to k8s-master01
Normal Pulled 7s kubelet Container image "nginx:latest" already present on machine
Normal Created 7s kubelet Created container nginx
Normal Started 7s kubelet Started container nginx
可以看到健康检查通过
Liveness和Readiness推荐使用接口级健康检查,参考 coredns
[root@k8s-master01 ~]# kubectl edit deploy coredns -n kube-system
/live 回车
livenessProbe:
failureThreshold: 5
httpGet:
path: /health
port: 8080
scheme: HTTP
initialDelaySeconds: 60
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
/read 回车
readinessProbe:
failureThreshold: 3
httpGet:
path: /ready
port: 8181
scheme: HTTP
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
可以看到 coredns 的 livenessProbe 请求了一个 /health 的接口,readinessProbe 请求了一个 /ready 的接口
零宕机必备知识:Pod退出流程
用户执行删除操作:kubectl delete po nginx
- 执行PreStop的指令
- Endpoint删除该Pod的IP地址
- Pod变成Terminating
Pod变成Terminating之后有一个宽限期,让我们做一些清理的动作,或者后置的动作
[root@k8s-master01 ~]# kubectl edit deploy coredns -n kube-system
/terminationGracePeriodSeconds 回车
terminationGracePeriodSeconds: 30
零宕机必备知识:PreStop的使用
Prestop:先去请求eureka接口,把自己的IP地址和端口号,进行下线,eureka从注册表中删除该应用的IP地址。然后容器进行sleep 90;kill pgrep java
如果sleep时间过长,需要修改terminationGracePeriodSeconds
[root@k8s-master01 ~]# vim pod.yaml
# 修改以下内容
# readinessProbe: # 可选,健康检查。注意三种检查方式同时只能使用一种。
# httpGet: # httpGet检测方式,生产环境建议使用httpGet实现接口级健康检查,健康检查由应用程序提供。
# path: / # 检查路径
# port: 80 # 监控端口
# livenessProbe: # 可选,健康检查
# exec: # 执行容器命令检测方式
# command:
# - ls
# initialDelaySeconds: 3 # 初始化时间
# timeoutSeconds: 2 # 超时时间
# periodSeconds: 2 # 检测间隔
# successThreshold: 1 # 检查成功为2次表示就绪
# failureThreshold: 1 # 检测失败1次表示未就绪
lifecycle:
# postStart: # 容器创建完成后执行的指令, 可以是exec httpGet TCPSocket
# exec:
# command:
# - sh
# - -c
# - 'mkdir /data/ '
preStop:
# httpGet:
# path: /
# port: 80
exec:
command:
- sh
- -c
- sleep 90
创建pod
[root@k8s-master01 ~]# kubectl delete po nginx
pod "nginx" deleted
[root@k8s-master01 ~]# kubectl create -f pod.yaml
pod/nginx created
[root@k8s-master01 ~]# kubectl get po
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 28s
查看pod退出时间
[root@k8s-master01 ~]# time kubectl delete po nginx
pod "nginx" deleted
real 0m40.249s
user 0m0.038s
sys 0m0.019s
可以看到只执行了40s,并没有sleep 90s,所以我们需要配置terminationGracePeriodSeconds
设置 terminationGracePeriodSeconds
[root@k8s-master01 ~]# vim pod.yaml
# 修改以下内容
terminationGracePeriodSeconds: 50
containers: # 必选,容器列表
创建pod
[root@k8s-master01 ~]# kubectl create -f pod.yaml
pod/nginx created
[root@k8s-master01 ~]# kubectl get po
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 28s
查看pod退出时间
[root@k8s-master01 ~]# time kubectl delete po nginx
pod "nginx" deleted
real 0m51.427s
user 0m0.031s
sys 0m0.017s
可以看到退出时间延长了,但是也没有真正的执行sleep 90s,所以配置的时候需要注意一下,因为k8s并不知道你执行了什么操作,无法判断PreStop的执行时间,所以会强制性的删除
课程链接
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。