SRE(sitereliabilityengineering).中文翻译为站点可靠性工程师.SRE概念中比较重要的特性在于:
1.engineer表示SRE是工程师,使用软件工程手段设计,研发和维护业务软件系统.
2.SRE的关注焦点在于可靠性,专注于软件系统架构设计,运维流程优化,让业务软件系统运行更可靠.
3.SRE主要工作是运维业务服务,
确保长期关注研发工作
运维工作限制在50%以内,剩余的时间花在研发项目上.
在保障服务SLO的前提下最大化迭代速度
监控系统
监控系统不应该依赖人来分析警报信息.而是应该由系统自动分析.仅当需要用户执行某种操作时,才需要通知用户
监控系统需要具备三类输出:
应急事件处理
变更管理
大概70%的生产事故是由某种变更触发,变更管理的最佳实践是使用自动化完成以下几个项目:
需求预测和容量规划
资源部署
效率与性能
一个业务总体资源使用情况是由以下几个因素驱动的:
SRE需要通过模型预测用户需求,合理部署和配置可用容量,改进软件以提高资源使用效率,这3个因素能够推动一个服务器的效率提升.
软件系统在负载上升的时候,会导致延迟升高.SRE的目标是根据一个预设的延迟目标部署和维护足够的容量,SRE和研发团队应该共同监控和优化整个系统的性能.
本篇笔记重点学习Prometheus的relabel_config
.也就是标签的管理,包括标签过滤,自定义标签,动态替换标签等.
在Prometheus的官方文档有详细介绍:
https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config
Prometheus中存储的数据为时间序列,是由Metric的名字和一系列的标签(键值对)唯一标识的, 不同的标签代表不同的时间序列,即 通过指定标签查询指定数据 。
Alertmanager还可以根据标签进行分组,将同一个标签的多个告警通知,打包成一条告警短信发送.
在Prometheus所有的Target实例中,都包含一些默认的Metadata标签信息。可以通过Prometheus UI的Targets页面中查看这些实例的Metadata标签的内容:
• address:当前Target实例的访问地址
• scheme:采集目标服务访问地址的HTTP Scheme,HTTP或者HTTPS
• metrics_path:采集目标服务访问地址的访问路径
一般来说元标签只是Prometheus内部使用,**我们一般不会让其做什么事情,并且这些标签是不会写到数据库当中的,使用promql是查询不到这些标签的.
在Prometheus的配置文件里可以定义laebls.添加标签的Key:Value信息.Key是标签名,Value是标签值.在下面的例子中,为某个target定义了2个标签:
1 | - job_name: 'Shanghai Linux Server' |
在下面2个阶段可以对标签进行修改:
relabel_configs
: 在数据采集之前.在这个阶段可以过滤特定的监控目标(keep,drop),或者过滤某个标签(labelkeep,labeldrop)metric_relabel_configs
: 抓取到数据之后.对标签进行重新标记replace
: 默认.通过regex匹配source_label的值,使用replacement来引用表达式匹配的分组,分组使用$1,$2…引用(正则匹配,提取字段重新创建新标签,注意这里是创建新的标签)keep
: 删除regex与连接不匹配的目标 source_labels,让Prometheus丢弃没有匹配到regex的targetdrop
: 删除regex与连接匹配的目标 source_labels,让Prometheus丢弃匹配到regex的target
labeldrop
: 删除regex匹配的标签,和drop
区别是这里是删除某个标签,而不是删除target
labelkeep
: 删除regex不匹配的标签,和keep
区别是这里是删除某个标签,而不是删除target
labelmap
: 匹配regex所有标签名称,并将捕获的内容分组,用第一个分组内容作为新的标签名(使用正则提取出多个字段,使用匹配到的作为新标签名,但是标签的内容不会改变,相对于对原有标签换了个名字,原标签仍然存在)
下面以Prometheus+Consul的自动发现为例,介绍修改标签的实际场景.
服务器通过Consul的API进行注册:
1 | cat /tmp/consul.json |
服务注册:
1 | curl --request PUT --data @consul-0.json http://172.16.83.201:8500/v1/agent/service/register |
下面是Prometheus的配置文件
1 | - job_name: 'consul-prometheus' |
在Prometheus的UI界面的Service Discovery自动发现可以看到target的label信息
左边是通过consul注册时的meta元标签,在Consul注册时定义的数据都会以__meta_consul_的标签格式展示.而之前提到过, __meta开头的是元数据.只供Prometheus内部使用.
右边的target labels是经过匹配,修改后的标签.而target labels的标签才是我们可以查询或者搜索的,有实际意义的标签.
在Prometheus的配置文件中下面的relabel_configs
定义了一条匹配到
1 | relabel_configs: |
我们在Consul注册的时候定义了一个Tag:node-exporter
.这个配置表示保留__meta_consul_tags
这个标签的值是”.node-exporter.“ 的target.如果服务器注册Consul时,tags不能匹配到”.node-exporter.那么这个服务器会被Prometheus过滤.即使这个服务器运行了node-exporter
我们还可以通过drop
将consul自身的服务器从target里拿掉.
1 | - source_labels: [__meta_consul_service] |
在__meta_consul_service
匹配到consul
值的标签就drop掉.不监控consul服务器
在通过Consul注册的时候可以携带自定义标签:
1 | "Meta": { |
这些自定义的标签,在Prometheus里会自动添加下面2个标签:
__meta_consul_service_metadata_app=nginx
__meta_consul_service_metadata_project=abc
但是这些标签是元标签.所以要转换成自定义标签.通过下面的Prometheus配置:
1 | - job_name: 'consul-prometheus' |
匹配__meta_consul_service_metadata_(.+)
的标签,然后将匹配到的分组(.+)
生成一个新的标签.
也就是__meta_consul_service_metadata_app=nginx
匹配到app=nginx
.然后生产一个app=nginx
的标签.也就是在Prometheus的UI界面中看到的target labels
labelmap的作用是产生一个新的标签,原来的标签仍然存在.
目前我是利用Ansible的jinjia2模板动态为服务器定义标签,然后在Prometheus中利用labelmap将标签动态生成为自定义标签.
有些标签不希望被存储上,那么可以使用 labeldrop:删除regex匹配的标签去完成不需要入库 将里面的标签删除掉,在入库之前删除
1 | - job_name: 'Linux Server' |
容器其实是一种沙盒技术,顾名思义,沙盒就像是一个集装箱一样,把你的应用”装”起来.这样应用和应用之间就因为有了边界而不至于互相干扰.而被装进集装箱的应用,也可以被方便的搬来搬去.
容器的核心功能就是通过约束和修改进程的动态表现,从而为其创造出一个”边界”.对于Docker以及大多数的Linux容器来说.cgroup
是用来约束的手段,而Namespace
则是用来修改进程视图的主要方法
linux内核提拱了6种namespace隔离的系统调用,如下图所示,但是真正的容器还需要处理许多其他工作。
namespace | 系统调用参数 | 隔离内容 |
---|---|---|
UTS | CLONE_NEWUTS | 主机名或域名 |
IPC | CLONE_NEWIPC | 信号量、消息队列和共享内存 |
PID | CLONE_NEWPID | 进程编号 |
Network | CLONE_NEWNET | 网络设备、网络战、端口等 |
Mount | CLONE_NEWNS | 挂载点(文件系统) |
User | CLONE_NEWUSER | 用户组和用户组 |
实际上,linux内核实现namespace的主要目的,就是为了实现轻量级虚拟化技术服务。在同一个namespace下的进程合一感知彼此的变化,而对外界的进程一无所知。这样就可以让容器中的进程产生错觉,仿佛自己置身一个独立的系统环境中,以达到隔离的目的。
下面拿个简单例子来讲解部分Namespace技术
启动一个容器,在容器里/bin/sh
的进程的PID是1.也就是容器的第一号进程,也称之为容器的主进程.另外还可以注意到该容器只能看到2个进程,一个是启动的PID为1的主进程,一个是手动在容器内部执行的ps进程
1 | [work@docker-dev elasticsearch]$ docker run -it busybox /bin/sh |
接下来在Docker宿主机上查找/bin/sh
这个进程,发现他的PID为8058,而它的父进程是containerd-shim
,再继续往上找,他的父进程的PID为1571,也就是/usr/bin/containerd
1 | [work@docker-dev ~]$ ps -ef | grep "/bin/sh" |
这说明了什么呢?
从上面的例子中可以看到Docker容器内的主进程其实是直接运行在Docker宿主机上的.他在宿主机上是个普通的进程,Docker对这个进程实施了一个”障眼法”,让他看不到其他的所有进程.让该进程误认为自己是第一个启动的PID为1的进程.
这种技术就是Linux里面的Namespace
机制.
我们知道通过docker exec
命令可以进入一个容器的内部.在了解了 Linux Namespace 的隔离机制后,你应该会很自然地想到一个问题:docker exec 是怎么做到进入容器里的呢?
实际上,Linux Namespace 创建的隔离空间虽然看不见摸不着,但一个进程的 Namespace 信息在宿主机上是确确实实存在的,并且是以一个文件的方式存在。
该命令的原理其实就是获取当前容器进程的PID,以及namespace文件.然后加入该namespace.这样通过共享同一个namespace.从而获取容器内部的信息.
下面通过一个示例演示如何进入一个容器内部
1 | docker run -d --name c1 centos-demo sleep 999 |
1 | [root@docker-dev ~]# docker inspect c1 -f {{.State.Pid}} |
1 | [root@docker-dev ~]# ll /proc/22733/ns |
可以看到,一个进程的每种 Linux Namespace,都在它对应的 /proc/[进程号]/ns 下有一个对应的虚拟文件,并且链接到一个真实的 Namespace 文件上。
有了这样一个可以“hold 住”所有 Linux Namespace 的文件,我们就可以对 Namespace 做一些很有意义事情了,比如:加入到一个已经存在的 Namespace 当中。
这也就意味着:一个进程,可以选择加入到某个进程已有的 Namespace 当中,从而达到“进入”这个进程所在容器的目的,这正是 docker exec 的实现原理。
而这个操作所依赖的,乃是一个名叫 setns() 的 Linux 系统调用。它的调用方法,我可以用如下一段小程序为你说明:
1 | #define _GNU_SOURCE |
这段代码功能非常简单:它一共接收两个参数,第一个参数是 argv[1],即当前进程要加入的 Namespace 文件的路径,比如 /proc/22733/ns/net;而第二个参数,则是你要在这个 Namespace 里运行的进程,比如 /bin/bash。
这段代码的的核心操作,则是通过 open() 系统调用打开了指定的 Namespace 文件,并把这个文件的描述符 fd 交给 setns() 使用。在 setns() 执行后,当前进程就加入了这个文件对应的 Linux Namespace 当中了。
现在,你可以编译执行一下这个程序,加入到容器进程(PID=22733)的net也就是 Network Namespace 中:
1 | [root@docker-dev ~]#gcc -o set_ns set_ns.c |
正如上所示,当我们执行 ifconfig 命令查看网络设备时,我会发现能看到的网卡“变少”了:只有两个。而我的宿主机则至少有四个网卡。这是怎么回事呢?
实际上,在 setns() 之后我看到的这两个网卡,正是我在前面启动的 Docker 容器里的网卡。也就是说,我新创建的这个 /bin/bash 进程,由于加入了该容器进程(PID=22733)的 Network Namepace,它看到的网络设备与这个容器里是一样的,即:/bin/bash 进程的网络设备视图,也被修改了。
而一旦一个进程加入到了另一个 Namespace 当中,在宿主机的 Namespace 文件上,也会有所体现。
在宿主机上,你可以用 ps 指令找到这个 set_ns 程序执行的 /bin/bash 进程,其真实的 PID 是 22821:
1 | [work@docker-dev ~]$ ps aux | grep "/bin/bash" |
这时,如果按照前面介绍过的方法,查看一下这个 PID=22733的进程的 Namespace,你就会发现这样一个事实:
1 | root@docker-dev ~]# ll /proc/22733/ns/net |
在 /proc/[PID]/ns/net 目录下,这个 PID=22821进程,与我们前面的 Docker 容器进程(PID=22733)指向的 Network Namespace 文件完全一样。这说明这两个进程,共享了这个名叫 net:[4026532173] 的 Network Namespace。
刚才我们演示了共享了net这个网络名称空间,同样的道理,还可以共享其他名称空间.比如:
1 | [root@docker-dev ~]# ./set_ns /proc/22733/ns/mnt /bin/bash |
当进入容器进程的mnt名称空间后,进程视图切换到了/
根目录下,并且root
家目录的内容也发生了变化.
以上就是docker exec
命令背后的详细原理.
总结
Namespace
的视图隔离就是Linux容器最基本的实现原理.所以Docker实际上是在创建容器进程时,指定了这个进程所需要启用的一组 Namespace 参数。这样,容器就只能“看”到当前 Namespace 所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。
所以说,容器,其实是一种特殊的进程而已。
在理解了 Namespace 的工作方式之后,你就会明白,跟真实存在的虚拟机不同,在使用 Docker 的时候,并没有一个真正的“Docker 容器”运行在宿主机里面。Docker 项目帮助用户启动的,还是原来的应用进程,只不过在创建这些进程时,Docker 为它们加上了各种各样的 Namespace 参数。
这时,这些进程就会觉得自己是各自 PID Namespace 里的第 1 号进程,只能看到各自 Mount Namespace 里挂载的目录和文件,只能访问到各自 Network Namespace 里的网络设备,就仿佛运行在一个个“容器”里面,与世隔绝。
Q: 你是否知道最新的 Docker 项目默认会为容器启用哪些 Namespace 吗?
A: PID, UTS, network, user, mount, IPC, cgroup
]]>在上篇介绍完容器的”隔离”技术之后,我们再来研究一下容器的”限制”问题
也许你会好奇,我们不是已经通过 Linux Namespace 创建了一个“容器”吗,为什么还需要对容器做“限制”呢?
我还是以 PID Namespace 为例,来给你解释这个问题。
虽然容器的第一号进程只能看到容器里的情况,但是由于是直接运行在宿主机上,所以它和宿主机上其他所有进程之间依然是平等的竞争关系.这就意味着虽然该进程在视图上被隔离起来了,但是他能够使用宿主机上的所有资源(比如CPU,内存).
这显然不是一个”沙盒”应该表现出来的合理行为
而Linux Cgroups就是Linux内核中用来为进程设置资源限制的一个重要功能
Linux Cgroups 的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。
此外,Cgroups 还能够对进程进行优先级设置、审计,以及将进程挂起和恢复等操作。在今天的分享中,我只和你重点探讨它与容器关系最紧密的“限制”能力,并通过一组实践来带你认识一下 Cgroups。
从字面上理解,cgroups就是把任务放到一个组里面统一加以控制。本质上来说,cgroups是内核附加在程序上的一系列hook,通过程序运行时对资源的调度触发相应的钩子以达到资源跟踪和限制的目的。在cgroup里,任务(task)就是系统的一个进程或者线程。
在 Linux 中,Cgroups 给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的 /sys/fs/cgroup 路径下。,我可以用 mount 指令把它们展示出来,这条命令是:
1 | cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd) |
cgroups以操作文件的方式作为API。它的操作目录是/sys/fs/cgroup
。我们来看看这个目录下有什么内容:
1 | [root@edward-rhel7-2 cloud-user]# ls /sys/fs/cgroup |
可以看到,在 /sys/fs/cgroup 下面有很多诸如 cpuset、cpu、 memory 这样的子目录,也叫子系统(sub system)。子系统就是资源调度器。比如CPU子系统可以控制CPU的时间分配,memory子系统可以限制内存的使用量.这些都是我这台机器当前可以被 Cgroups 进行限制的资源种类。而在子系统对应的资源种类下,你就可以看到该类资源具体可以被限制的方法。
对 CPU 子系统来说,我们就可以看到如下几个配置文件,这个指令是:
1 | ls /sys/fs/cgroup/cpu |
如果熟悉 Linux CPU 管理的话,你就会在它的输出里注意到 cfs_period 和 cfs_quota 这样的关键词。这两个参数需要组合使用,可以用来限制进程在长度为 cfs_period 的一段时间内,只能被分配到总量为 cfs_quota 的 CPU 时间。
而这样的配置文件又如何使用呢?
你需要在对应的子系统下面创建一个目录,比如,我们现在进入 /sys/fs/cgroup/cpu 目录下:
1 | root@ubuntu:/sys/fs/cgroup/cpu$ mkdir container |
这个目录就称为一个“控制组”。你会发现,操作系统会在你新创建的 container 目录下,自动生成该子系统对应的资源限制文件。
现在,我们在后台执行这样一条脚本:
1 | $ while : ; do : ; done & |
显然,它执行了一个死循环,可以把计算机的 CPU 吃到 100%,根据它的输出,我们可以看到这个脚本在后台运行的进程号(PID)是 8498。
这样,我们可以用 top 指令来确认一下 CPU 有没有被打满.在输出里可以看到,CPU 的使用率已经 100% 了(%Cpu0 :100.0 us)。
1 | Tasks: 154 total, 2 running, 152 sleeping, 0 stopped, 0 zombie |
而此时,我们可以通过查看 container 目录下的文件,看到 container 控制组里的 CPU quota 还没有任何限制(即:-1),CPU period 则是默认的 100 ms(100000 us):
1 | $ cat /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us |
接下来,我们可以通过修改这些文件的内容来设置限制。
比如,向 container 组里的 cfs_quota 文件写入 20 ms(20000 us):
1 | echo 20000 > /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us |
结合前面的介绍,你应该能明白这个操作的含义,它意味着在每 100 ms 的时间里,被该控制组限制的进程只能使用 20 ms 的 CPU 时间,也就是说这个进程只能使用到 20% 的 CPU 带宽。
接下来,我们把被限制的进程的 PID 写入 container 组里的 tasks 文件,上面的设置就会对该进程生效了:
1 | echo 8498 > /sys/fs/cgroup/cpu/container/tasks |
我们可以用 top 指令查看一下:
1 | Tasks: 154 total, 2 running, 152 sleeping, 0 stopped, 0 zombie |
可以看到,计算机的 CPU 使用率立刻降到了 20%以内
除 CPU 子系统外,Cgroups 的每一项子系统都有其独有的资源限制能力,比如:
Linux Cgroups 的设计还是比较易用的,简单粗暴地理解呢,它就是一个子系统目录加上一组资源限制文件的组合。而对于 Docker 等 Linux 容器项目来说,它们只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个新目录),然后在启动容器进程之后,把这个进程的 PID 填写到对应控制组的 tasks 文件中就可以了。
而至于在这些控制组下面的资源文件里填上什么值,就靠用户执行 docker run 时的参数指定了,比如这样一条命令
1 | [root@docker-dev container]# docker run -it -d --cpu-period=100000 --cpu-quota=20000 busybox /bin/sh |
在启动这个容器后,我们可以通过查看 Cgroups 文件系统下,CPU 子系统中,“docker”这个控制组里的资源限制文件的内容来确认:
1 | [root@docker-dev container]# cat /sys/fs/cgroup/cpu/docker/c992cf3cc50c/cpu.cfs_quota_us |
这就意味着这个 Docker 容器,只能使用到 20% 的 CPU 带宽。
内存资源和CPU不同,CPU属于可压缩资源.当进程触发CPU限制阈值时,进程仍然可以正常运行,只是进程能使用的CPU分片时间受到限制.然而内存属于不可压缩资源,当进程触发内存资源阈值时,进程会立刻被杀死,也就是触发OOM事件.
下面用python的递归模拟一个内存占用的程序
1 | import time |
在/sys/fs/cgroup/memory/
目录下创建一个测试文件夹
1 | [root@docker-dev ~]# cd /sys/fs/cgroup/memory/ |
在该目录下,限制内存阈值,这里设置为10K
1 | [root@docker-dev ~]# cd /sys/fs/cgroup/memory/mem_test/ |
运行python程序,同时开启另一个shell终端,获取该进程的PID
1 | [root@docker-dev ~]# python3 mem.py |
将22958这个PID写入到mem_test
目录下的tasks文件内
1 | [root@docker-dev mem_test]# echo 22958 > tasks |
此时.python3的进程会被杀死,出现OOM现象
1 | [root@docker-dev mem_test]# less /var/log/messages | grep oom |
通过上面2个小例子,我们演示了cgroup对本机进程的资源限制效果.docker在启动容器时也允许我们对该容器的CPU和内存进行一些资源限制.但是其资源限制的本质也同样是利用cgroup的功能.下面我们运行一个容器.该容器运行一个上文中的while死循环,但是这次我们对容器的CPU资源进行限制.
1 | [root@docker-dev mem_test]# docker run -d --name c2 --cpu-period=100000 --cpu-quota=20000 hub.doweidu.com/base/centos-demo:7 bash -c "while : ; do : ; done" |
通过top命令,我们可以看到容器的CPU限制已经生效了.cpu3被限制在20%的使用率之内
1 | top - 20:53:50 up 31 days, 10:09, 2 users, load average: 0.00, 0.01, 0.05 |
在/sys/fs/cgroup/cpu/docker
目录下.可以看到新生成了一个目录7b1cb8734d905dd25eb1cdcf4cb63ebf8c7e6182d90639db4ad15bc99ba19f63
.其实这就是我们上一步中刚启动的容器的ID.
1 | [root@docker-dev docker]# ll |
进入该容器ID的目录内.可以看到cpu.cfs_quota_us
文件已经设置了限额20000us.
1 | [root@docker-dev docker]# cd 7b1cb8734d905dd25eb1cdcf4cb63ebf8c7e6182d90639db4ad15bc99ba19f63 |
查看该容器的Pid以及tasks可以看到.docker自动将容器的进程Pid写入到了tasks文件中.
1 | [root@docker-dev 7b1cb8734d905dd25eb1cdcf4cb63ebf8c7e6182d90639db4ad15bc99ba19f63]# docker inspect c2 -f {{.State.Pid}} |
通过上面的例子中可以看到,docker使用cgroup解决了多个容器之前的资源竞争和互相干扰的问题.
通过以上讲述,你现在应该能够理解,一个正在运行的 Docker 容器,其实就是一个启用了多个 Linux Namespace 的应用进程,而这个进程能够使用的资源量,则受 Cgroups 配置的限制。
这也是容器技术中一个非常重要的概念,即:容器是一个“单进程”模型。
由于一个容器的本质就是一个进程,用户的应用进程实际上就是容器里 PID=1 的进程,也是其他后续创建的所有进程的父进程。这就意味着,在一个容器中,你没办法同时运行两个不同的应用,除非你能事先找到一个公共的 PID=1 的程序来充当两个不同应用的父进程,这也是为什么很多人都会用 systemd 或者 supervisord 这样的软件来代替应用本身作为容器的启动进程。
但是,在后面分享容器设计模式时,我还会推荐其他更好的解决办法。这是因为容器本身的设计,就是希望容器和应用能够同生命周期,这个概念对后续的容器编排非常重要。否则,一旦出现类似于“容器是正常运行的,但是里面的应用早已经挂了”的情况,编排系统处理起来就非常麻烦了。
另外,跟 Namespace 的情况类似,Cgroups 对资源的限制能力也有很多不完善的地方,被提及最多的自然是 /proc 文件系统的问题。
众所周知,Linux 下的 /proc 目录存储的是记录当前内核运行状态的一系列特殊文件,用户可以通过访问这些文件,查看系统以及当前正在运行的进程的信息,比如 CPU 使用情况、内存占用率等,这些文件也是 top 指令查看系统信息的主要数据来源。
但是,你如果在容器里执行 top 指令,就会发现,它显示的信息居然是宿主机的 CPU 和内存数据,而不是当前容器的数据。
造成这个问题的原因就是,/proc 文件系统并不知道用户通过 Cgroups 给这个容器做了什么样的资源限制,即:/proc 文件系统不了解 Cgroups 限制的存在。
]]>而正如我前面所说的,Namespace 的作用是“隔离”,它让应用进程只能看到该 Namespace 内的“世界”;而 Cgroups 的作用是“限制”,它给这个“世界”围上了一圈看不见的墙。这么一折腾,进程就真的被“装”在了一个与世隔绝的房间里,而这些房间就是 PaaS 项目赖以生存的应用“沙盒”。
可是,还有一个问题不知道你有没有仔细思考过:这个房间四周虽然有了墙,但是如果容器进程低头一看地面,又是怎样一副景象呢?
换句话说,容器里的进程看到的文件系统又是什么样子的呢?
Docker容器借助chroot
挂载一个虚拟根目录到容器.我们在Linux操作系统里可以很方便的演练chroot
是如何工作的.chroot的作用就是帮助你change root file system
,即改变进程的根目录到你指定的位置.
假设,我们现在有一个 $HOME/test 目录,想要把它作为一个 /bin/bash 进程的根目录。
首先,创建一个 test 目录和几个 lib 文件夹:
1 | $ mkdir -p $HOME/test |
然后,把 bash 命令拷贝到 test 目录对应的 bin 路径下
1 | cp -v /bin/{bash,ls} $HOME/test/bin |
接下来,把 bash 命令需要的所有 so 文件,也拷贝到 test 目录对应的 lib 路径下。找到 so 文件可以用 ldd 命令
1 | $ T=$HOME/virtual_root |
最后,执行 chroot 命令,告诉操作系统,我们将使用 $HOME/test 目录作为 /bin/bash 进程的根目录
1 | chroot $HOME/test /bin/bash |
这时,你如果执行 “ls /“,就会看到,它返回的都是 $HOME/test 目录下面的内容,而不是宿主机的内容。
更重要的是,对于被 chroot 的进程来说,它并不会感受到自己的根目录已经被“修改”成 $HOME/test 了。
1 | [root@docker-dev test]# chroot $HOME/test /bin/bash |
这种视图被修改的原理,是不是跟我之前介绍的 Linux Namespace 很类似呢?
实际上,Mount Namespace 正是基于对 chroot 的不断改良才被发明出来的,它也是 Linux 操作系统里的第一个 Namespace。
当然,为了能够让容器的这个根目录看起来更“真实”,我们一般会在这个容器的根目录下挂载一个完整操作系统的文件系统,比如 Ubuntu16.04 的 ISO。这样,在容器启动之后,我们在容器里通过执行 “ls /“ 查看根目录下的内容,就是 Ubuntu 16.04 的所有目录和文件。
而这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)。
所以,一个最常见的 rootfs,或者说容器镜像,会包括如下所示的一些目录和文件,比如 /bin,/etc,/proc 等等:
1 | $ ls / |
现在,你应该可以理解,对 Docker 项目来说,它最核心的原理实际上就是为待创建的用户进程:
这样,一个完整的容器就诞生了。不过,Docker 项目在最后一步的切换上会优先使用 pivot_root 系统调用,如果系统不支持,才会使用 chroot。
另外,需要明确的是,rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。
所以说,rootfs 只包括了操作系统的“躯壳”,并没有包括操作系统的“灵魂”。
那么,对于容器来说,这个操作系统的“灵魂”又在哪里呢?
实际上,同一台机器上的所有容器,都共享宿主机操作系统的内核。
这就意味着,如果你的应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行直接的交互,你就需要注意了:这些操作和依赖的对象,都是宿主机操作系统的内核,它对于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身。
这也是容器相比于虚拟机的主要缺陷之一:毕竟后者不仅有模拟出来的硬件机器充当沙盒,而且每个沙盒里还运行着一个完整的 Guest OS 给应用随便折腾。
不过,正是由于 rootfs 的存在,容器才有了一个被反复宣传至今的重要特性:一致性。
什么是容器的“一致性”呢?
过去由于云端与本地服务器环境不同,应用的打包过程,一直是使用 PaaS 时最“痛苦”的一个步骤。
但有了容器之后,更准确地说,有了容器镜像(即 rootfs)之后,这个问题被非常优雅地解决了。
由于 rootfs 里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。
有了容器镜像“打包操作系统”的能力,这个最基础的依赖环境也终于变成了应用沙盒的一部分。这就赋予了容器所谓的一致性:无论在本地、云端,还是在一台任何地方的机器上,用户只需要解压打包好的容器镜像,那么这个应用运行所需要的完整的执行环境就被重现出来了。
这种深入到操作系统级别的运行环境一致性,打通了应用在本地开发和远端执行环境之间难以逾越的鸿沟。
不过,这时你可能已经发现了另一个非常棘手的问题:难道我每开发一个应用,或者升级一下现有的应用,都要重复制作一次 rootfs 吗?
比如,我现在用 Ubuntu 操作系统的 ISO 做了一个 rootfs,然后又在里面安装了 Java 环境,用来部署我的 Java 应用。那么,我的另一个同事在发布他的 Java 应用时,显然希望能够直接使用我安装过 Java 环境的 rootfs,而不是重复这个流程。
一种比较直观的解决办法是,我在制作 rootfs 的时候,每做一步“有意义”的操作,就保存一个 rootfs 出来,这样其他同事就可以按需求去用他需要的 rootfs 了。
但是,这个解决办法并不具备推广性。原因在于,一旦你的同事们修改了这个 rootfs,新旧两个 rootfs 之间就没有任何关系了。这样做的结果就是极度的碎片化。
那么,既然这些修改都基于一个旧的 rootfs,我们能不能以增量的方式去做这些修改呢?这样做的好处是,所有人都只需要维护相对于 base rootfs 修改的增量内容,而不是每次修改都制造一个“fork”。
答案当然是肯定的。
这也正是为何,Docker 公司在实现 Docker 镜像时并没有沿用以前制作 rootfs 的标准流程,而是做了一个小小的创新:
Docker 在镜像的设计中,引入了层(layer)的概念。也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs。
当然,这个想法不是凭空臆造出来的,而是用到了一种叫作联合文件系统(Union File System)的能力。
Union File System 也叫 UnionFS,最主要的功能是将多个不同位置的目录联合挂载(union mount)到同一个目录下。比如,我现在有两个目录 A 和 B,它们分别有两个文件:
1 | $ tree |
然后,我使用联合挂载的方式,将这两个目录挂载到一个公共的目录 C 上:
1 | $ mkdir C |
这时,我再查看目录 C 的内容,就能看到目录 A 和 B 下的文件被合并到了一起:
1 | $ tree ./C |
可以看到,在这个合并后的目录 C 里,有 a、b、x 三个文件,并且 x 文件只有一份。这,就是“合并”的含义。此外,如果你在目录 C 里对 a、b、x 文件做修改,这些修改也会在对应的目录 A、B 中生效。
AUFS是最古老的联合挂载文件系统,也是docker最初使用的文件系统.在新版本的docker中.使用的是overlay2文件系统.也是目前docker场景下性能最优秀的文件系统.overlay2也是在AUFS的基础之上发展而来.其原理和AUFS有相似之处.下面演示一下overlay2文件系统的用法
1 | [root@docker-dev ~]# tree -L 2 A B C D worker |
和AUFS的工作方式类似,将ABC挂载到D这个目录下,其中A,B是底层不可修改目录,C是可读写目录
1 | [root@docker-dev ~]# mount -t overlay overlay -o lowerdir=A:B,upperdir=C,workdir=worker D |
再次查看目录结构,发现D目录下多了三个从目录A和目录B合并过来的a,b,x文件
1 | [root@docker-dev ~]# tree -L 2 A B C D worker |
此时在挂载的D目录下,创建一个文件y.修改a文件
1 | [root@docker-dev ~]# cd D |
再次观察目录结构,发现新增或者修改的a和y文件出现在C目录下,A和B目录保持不变
1 | A |
查看A,B,C目录下的文件a的内容.发现A和B目录下的a文件内容不变,在挂载目录D下修改的a文件”出现”在C目录.
docker镜像就是使用了联合挂载的原理.镜像层类似于目录A和B,他们是不可写的,所有的写操作都发生在类似于目录C的可读写层..下面拿一个容器来举个例子
1 | [root@docker-dev ~]# docker run -d --name c1 centos-demo sleep 999 |
1 | "GraphDriver": { |
为什么这里的lowerdir是2层呢? 因为该容器使用的centos-demo镜像就是一个2个layer组成的镜像.通过下面命令可以查看镜像的相关信息:
1 | [root@docker-dev ~]# docker inspect centos-demo |
该镜像的Dockerfile文件显示,该镜像只由两个指令组成: FROM和RUN.所以使用docker build
命令编译成镜像后,该镜像只包含2个layer
1 | [root@docker-dev ~]# cat centos/Dockerfile |
回到容器本身.从docker inspect c1
命令的结果可以看到该容器的各个layer的信息.进入到/var/lib/docker/overlay2/
目录下
1 | [root@docker-dev ~]# cd /var/lib/docker/overlay2/ |
下面这个子目录显示的就是FROM centos:7
指令中基础镜像centos:7
的文件系统
1 | [root@docker-dev overlay2]# ls 53f22fea6b813c35a5356493a840fb550fe75593174bc192446224a9bd0dddbd/diff |
我们来关注一下容器的可读写层.由于容器刚启动,我们没有对容器进行任何变更.所以容器里没有写入任何新数据
1 | [root@docker-dev overlay2]# ll e03a913bf2e0ed33012a912a5ae421e9d0ededc262c998656d828f8cd4a65e4c/diff |
在容器内部的/tmp
目录下写入一个新的文件.内容为”hello this is c1”
1 | [root@docker-dev overlay2]# docker exec -it c1 bash |
再次查看宿主机上该容器的可读写层目录
1 | [root@docker-dev overlay2]# ll e03a913bf2e0ed33012a912a5ae421e9d0ededc262c998656d828f8cd4a65e4c/diff/ |
从上面的例子中我们可以看到容器的 rootfs 由如下图所示的三部分组成:
图是盗来的,非本例子中的实际layer信息,但是大同小异
第一部分,只读层。
它是这个容器的 rootfs 最下面的五层,对应的正是 centos:7
镜像的2层。可以看到,它们的挂载方式都是只读的
第二部分,可读写层。
它是这个容器的 rootfs 最上面的一层(e03a913bf2e0ed33),它的挂载方式为:rw,即 read write。在没有写入文件之前,这个目录是空的。而一旦在容器里做了写操作,你修改产生的内容就会以增量的方式出现在这个层中。
可是,你有没有想到这样一个问题:如果我现在要做的,是删除只读层里的一个文件呢?
为了实现这样的删除操作,overlay会在可读写层创建一个 whiteout 文件,把只读层里的文件“遮挡”起来。
比如,你要删除只读层里一个名叫 foo 的文件,那么这个删除操作实际上是在可读写层创建了一个名叫.wh.foo 的文件。这样,当这两个层被联合挂载之后,foo 文件就会被.wh.foo 文件“遮挡”起来,“消失”了。这个功能,就是“ro+wh”的挂载方式,即只读 +whiteout 的含义。我喜欢把 whiteout 形象地翻译为:“白障”。
所以,最上面这个可读写层的作用,就是专门用来存放你修改 rootfs 后产生的增量,无论是增、删、改,都发生在这里。而当我们使用完了这个被修改过的容器之后,还可以使用 docker commit 和 push 指令,保存这个被修改过的可读写层,并上传到 Docker Hub 上,供其他人使用;而与此同时,原先的只读层里的内容则不会有任何变化。这,就是增量 rootfs 的好处。
它是一个以“-init”结尾的层,夹在只读层和读写层之间。Init 层是 Docker 项目单独生成的一个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息。
需要这样一层的原因是,这些文件本来属于只读的 Ubuntu 镜像的一部分,但是用户往往需要在启动容器时写入一些指定的值比如 hostname,所以就需要在可读写层对它们进行修改。
可是,这些修改往往只对当前的容器有效,我们并不希望执行 docker commit 时,把这些信息连同可读写层一起提交掉。
所以,Docker 做法是,在修改了这些文件之后,以一个单独的层挂载了出来。而用户执行 docker commit 只会提交可读写层,所以是不包含这些内容的。
1 | [root@docker-dev overlay2]# ll e03a913bf2e0ed33012a912a5ae421e9d0ededc262c998656d828f8cd4a65e4c-init |
在今天的分享中,我着重介绍了 Linux 容器文件系统的实现方式。而这种机制,正是我们经常提到的容器镜像,也叫作:rootfs。它只是一个操作系统的所有文件和目录,并不包含内核,最多也就几百兆。而相比之下,传统虚拟机的镜像大多是一个磁盘的“快照”,磁盘有多大,镜像就至少有多大。
通过结合使用 Mount Namespace 和 rootfs,容器就能够为进程构建出一个完善的文件系统隔离环境。当然,这个功能的实现还必须感谢 chroot 和 pivot_root 这两个系统调用切换进程根目录的能力。
而在 rootfs 的基础上,Docker 公司创新性地提出了使用多个增量 rootfs 联合挂载一个完整 rootfs 的方案,这就是容器镜像中“层”的概念。
通过“分层镜像”的设计,以 Docker 镜像为核心,来自不同公司、不同团队的技术人员被紧密地联系在了一起。而且,由于容器镜像的操作是增量式的,这样每次镜像拉取、推送的内容,比原本多个完整的操作系统的大小要小得多;而共享层的存在,可以使得所有这些容器镜像需要的总空间,也比每个镜像的总和要小。这样就使得基于容器镜像的团队协作,要比基于动则几个 GB 的虚拟机磁盘镜像的协作要敏捷得多。
更重要的是,一旦这个镜像被发布,那么你在全世界的任何一个地方下载这个镜像,得到的内容都完全一致,可以完全复现这个镜像制作者当初的完整环境。这,就是容器技术“强一致性”的重要体现。
Cgroup: https://coolshell.cn/articles/17049.html (这位大佬的很多文章都值得学习)
理解overlay2: https://www.cnblogs.com/jiangbo44/p/14056898.html
本文使用的是官方的mysqld_exporter.github地址:mysqld_exporter
mysql版本需要在5.6版本或以上
mysqld_exporter提供很多监控项(具体参考github项目介绍).如果需要开启一个监控项,在启动mysqld_exporter时,携带以下命令:
1 | --collect.key |
如果需要关闭某个监控项.携带以下命令:
1 | --no-collect.key |
如果mysqld_exporter的版本小于0.10.0,命令有些变化,双横杠变成单横杠,使用-collect.key 或者-collect.key=True|false
安装方式很简单,.下面是一个Ansible脚本以供参考
1 | - hosts: mysql-prod |
这里需要准备一个localhost_db.cnf
配置文件.内容如下
1 | [client] |
mysqld_exporter启动脚本
1 | [Unit] |
Ansible脚本执行完毕后,就可以通过IP:9104
来访问mysql的Metrics
为了监控到主机的情况.此时还需要部署node_exporter客户端.关于Node_exporter我在另一篇笔记中再详细介绍
由于mysqld_exporter没有任何监控项能获取到mysql服务器的主机名.所以将数据展示到grafana时,只能通过IP去查看监控图表.这非常的不方便.比如下面截图中,当mysql服务器数量较多时,很难知道IP地址对应的是具体哪台服务器:
此时,就需要在prometheus配置文件中,将每个mysql服务器添加labels,给服务器打上主机名和组名的标签
1 | - job_name: 'aliyun-mysql-exporter' |
下载dashboard: https://grafana.com/grafana/dashboards/7362
或者直接在grafana中添加7362的dashboard
原生的dashboard只有一个instance的变量.为了添加主机名,更好的区分和展示监控效果,需要做一些修改.
1.定义变量:
job变量:
label: job
query: label_values(mysql_up,job)
group变量:
2.修改变量
将host变量修改为:
name: host
label: 主机名
query:label_values(mysql_up{job=~”$job”,group=~”$group”}, hostname)
3.变量页面最终设置如下:
接着,将当前的dashboard的json文件导出.复制下面的json文件
将文件中的instance=~
全部替换为hostname=~
.然后复制回去,点击Save Changes
此时,可以通过主机组和主机名筛选具体的mysql服务器.
别忘记保存dashboard
1 | groups: |
Pod是kubernetes项目中最小的API对象,是原子调度单位.我们之前学习过很多Linux容器,Docker方面的知识.那Kubernetes为什么不使用容器作为调度单位,而是要将容器封装成一个Pod?
要探讨这个问题,我们需要深入研究一下Kubernetes的设计思想和工作原理.
通过之前Docker的原理学习,我们知道容器的本质到底是什么? 容器的本质是进程.容器的镜像就像是这个进程的安装包.一键启动这个镜像,就相当于用这个安装包启动了一个进程(PID为1).那么Kubernetes呢?
Kubernetes就是操作系统! 负责所有容器的编排和管理
但是,在一个操作系统里,进程并不是孤苦伶仃的单独运行的,而是以进程组的方式,多个进程同时在一起运行.这些进程存在着”进程和进程组”的关系,他们之间有非常密切的写作关系,使得他们必须部署在同一台机器上.
由于受限于容器的”单进程模型”.一个进程组下的不同进程可能需要制作成多个不同的容器,而这多个容器在传统的调度工作中(比如像Docker Swarm,Mesos)都没有被妥善处理好,在进程组的调度上要么无法保障一个进程组的多个容器无法调度到同一个节点,要么调度的效率和性能的问题.
可在Kubernetes里,这个问题通过Pod完美解决了.Pod是kubernetes的原子调度单位,这就意味着Kubernetes是统一按照Pod而非单个容器的资源需求计算的.所以可以将多个容器部署在同一个Pod里,这些容器共享同一个Pod的网络名称,进程间通信,IP地址,共享卷等.而Kubernetes在调度时,会将他们作为一个整体,而非单个容器进程.
像这样容器间的紧密协作,我们可以称为“超亲密关系”。这些具有“超亲密关系”容器的典型特征包括但不限于:互相之间会发生直接的文件交换、使用 localhost 或者 Socket 文件进行本地通信、会发生非常频繁的远程调用、需要共享某些 Linux Namespace(比如,一个容器要加入另一个容器的 Network Namespace)等等。
这也就意味着,并不是所有有“关系”的容器都属于同一个 Pod。比如,PHP 应用容器和 MySQL 虽然会发生访问关系,但并没有必要、也不应该部署在同一台机器上,它们更适合做成两个 Pod。
不过,相信此时你可能会有第二个疑问:
对于初学者来说,一般都是先学会了用 Docker 这种单容器的工具,才会开始接触 Pod。
而如果 Pod 的设计只是出于调度上的考虑,那么 Kubernetes 项目似乎完全没有必要非得把 Pod 作为“一等公民”吧?这不是故意增加用户的学习门槛吗?
为了理解这一层含义,我就必须先给你介绍一下Pod 的实现原理。
也就是说,Kubernetes 真正处理的,还是宿主机操作系统上 Linux 容器的 Namespace 和 Cgroups,而并不存在一个所谓的 Pod 的边界或者隔离环境。
那么,Pod 又是怎么被“创建”出来的呢?
答案是:Pod,其实是一组共享了某些资源的容器。
具体的说:Pod 里的所有容器,共享的是同一个 Network Namespace,并且可以声明共享同一个 Volume。
那这么来看的话,一个有 A、B 两个容器的 Pod,不就是等同于一个容器(容器 A)共享另外一个容器(容器 B)的网络和 Volume 的玩儿法么?
这好像通过 docker run –net –volumes-from 这样的命令就能实现嘛,比如:
1 | $ docker run --net=B --volumes-from=B --name=A image-A ... |
但是,你有没有考虑过,如果真这样做的话,容器 B 就必须比容器 A 先启动,这样一个 Pod 里的多个容器就不是对等关系,而是拓扑关系了。
所以,在 Kubernetes 项目里,Pod 的实现需要使用一个中间容器,这个容器叫作 Infra 容器。在这个 Pod 中,Infra 容器永远都是第一个被创建的容器,而其他用户定义的容器,则通过 Join Network Namespace 的方式,与 Infra 容器关联在一起。这样的组织关系,可以用下面这样一个示意图来表达:
如上图所示,这个 Pod 里有两个用户容器 A 和 B,还有一个 Infra 容器。很容易理解,在 Kubernetes 项目里,Infra 容器一定要占用极少的资源,所以它使用的是一个非常特殊的镜像,叫作:k8s.gcr.io/pause
。这个镜像是一个用汇编语言编写的、永远处于“暂停”状态的容器,解压后的大小也只有 100~200 KB 左右。
而在 Infra 容器“Hold 住”Network Namespace 后,用户容器就可以加入到 Infra 容器的 Network Namespace 当中了。所以,如果你查看这些容器在宿主机上的 Namespace 文件(这个 Namespace 文件的路径,我已经在前面的内容中介绍过),它们指向的值一定是完全一样的。
这也就意味着,对于 Pod 里的容器 A 和容器 B 来说:
Pod 这种“超亲密关系”容器的设计思想,实际上就是希望,当用户想在一个容器里跑多个功能并不相关的应用时,应该优先考虑它们是不是更应该被描述成一个 Pod 里的多个容器。
为了能够掌握这种思考方式,你就应该尽量尝试使用它来描述一些用单个容器难以解决的问题。
一个典型的例子就是容器的日志收集
比如,我现在有一个应用,需要不断地把日志文件输出到容器的 /var/log 目录中。
这时,我就可以把一个 Pod 里的 Volume 挂载到应用容器的 /var/log 目录上。
然后,我在这个 Pod 里同时运行一个日志收集客户端 容器,它也声明挂载同一个 Volume 到自己的 /var/log 目录上。
像这样,我们就用一种“组合”方式,解决了主容器和日志收集容器之间的耦合关系,而日志收集容器我们一般也称之为辅助容器.实际上,这个所谓的“组合”操作,正是容器设计模式里最常用的一种模式,它的名字叫:sidecar。顾名思义,sidecar 指的就是我们可以在一个 Pod 中,启动一个辅助容器,来完成一些独立于主进程(主容器)之外的工作。
这样,接下来 sidecar 容器就只需要做一件事儿,那就是不断地从自己的 /var/log 目录里读取日志文件,转发到 MongoDB 或者 Elasticsearch 中存储起来。这样,一个最基本的日志收集工作就完成了。
]]> Calico是一个非常流行的Kubernetes网络插件和解决方案.Calico是一个开源虚拟化网络方案,用于为云原生应用实现互联及策略控制。与Flannel相比,Calico的一个显著优势是对网络策略(network policy)的支持,它允许用户动态定义ACL规则控制进出容器的数据报文,实现为Pod间的通信按需施加安全策略。事实上,Calico可以整合进大多数主流的编排系统,如Kubernetes、Apache Mesos、Docker和OpenStack等。
Calico本身是一个三层的虚拟网络方案,它将每个节点都当作路由器(router),将每个节点的容器都当作是“节点路由器”的一个终端并为其分配一个IP地址,各节点路由器通过BGP(Border Gateway Protocol)学习生成路由规则,从而将不同节点上的容器连接起来。因此,Calico方案其实是一个纯三层的解决方案,通过每个节点协议栈的三层(网络层)确保容器之间的连通性,这摆脱了flannel host-gw类型的所有节点必须位于同一二层网络的限制,从而极大地扩展了网络规模和网络边界。
Calico利用Linux内核在每一个计算节点上实现了一个高效的vRouter(虚拟路由器)进行报文转发,而每个vRouter都通过BGP负责把自身所属的节点上运行的Pod资源的IP地址信息基于节点的agent程序(Felix)直接由vRouter生成路由规则向整个Calico网络内进行传播.
Calico承载的各Pod资源直接通过vRouter经由基础网络进行互联,它非叠加、无隧道、不使用VRF表,也不依赖于NAT,因此每个工作负载都可以直接配置使用公网IP接入互联网,当然,也可以按需使用网络策略控制它的网络连通性。
Calico官网介绍: projectcaclico.org
Calico中,Pod收发的IP报文由所在节点的Linux内核路由表负责转发,并通过iptables规则实现其安全功能。某Pod对象发送报文时,Calico应确保节点总是作为下一跳MAC地址返回,不管工作负载本身可能配置什么路由,而发往某Pod对象的报文,其最后一个IP跃点就是Pod所在的节点,也就是说,报文的最后一程即由节点送往目标Pod对象,如下图所示。
需为某Pod对象提供连接时,系统上的专用插件(如Kubernetes的CNI)负责将需求通知给Calico Agent。收到消息后,Calico Agent会为每个工作负载添加直接路径信息到工作负载的TAP设备(如veth)。而运行于当前节点的BGP客户端监控到此类消息后会调用路由reflector向工作于其他节点的BGP客户端进行通告。
Calico未使用额外的报文封装和解封装,从而简化了网络拓扑,这也是Calico高性能、易扩展的关键因素。毕竟,小的报文减少了报文分片的可能性,而且较少的封装和解封装操作也降低了对CPU的占用。此外,较少的封装也易于实现报文分析,易于进行故障排查。
创建、移动或删除Pod对象时,相关路由信息的通告速度也是影响其扩展性的一个重要因素。Calico出色的扩展性缘于与互联网架构设计原则别无二致的方式,它们都使用了BGP作为控制平面。BGP以高效管理百万级的路由设备而闻名于世,Calico自然可以游刃有余地适配大型IDC网络规模。另外,由于Calico各工作负载使用基IP直接进行互联,因此它还支持多个跨地域的IDC之间进行协同。
各组件介绍如下:
Felix:Calico Agent,运行于每个节点。主要负责网络接口管理和监听、路由、ARP 管理、ACL 管理和同步、状态上报等。
etcd:分布式键值存储,主要负责网络元数据一致性,确保Calico网络状态的准确性,可以与kubernetes共用;
BGP Client(BIRD):Calico 为每一台 Host 部署一个 BGP Client,使用 BIRD 实现,BIRD 是一个单独的持续发展的项目,实现了众多动态路由协议比如 BGP、OSPF、RIP 等。在 Calico 的角色是监听 Host 上由 Felix 注入的路由信息,然后通过 BGP 协议广播告诉剩余 Host 节点,从而实现网络互通。
Felix运行于各节点的用于支持端点(VM或Container)构建的守护进程,它负责生成路由和ACL,以及其他任何由节点用到的信息,从而为各端点构建连接机制。Felix在各编排系统中主要负责以下任务。
首先是接口管理(Interface Management)功能,负责为接口生成必要的信息并送往内核,以确保内核能够正确处理各端点的流量,尤其是要确保各节点能够响应目标MAC为当前节点上各工作负载的MAC地址的ARP请求,以及为其管理的接口打开转发功能。另外,它还要监控各接口的变动以确保规则能够得到正确的应用。
其次是路由规划(Route Programming)功能,其负责为当前节点运行的各端点在内核FIB(Forwarding Information Base)中生成路由信息,以保证到达当前节点的报文可正确转发给端点。
再次是ACL规划(ACL Programming)功能,负责在Linux内核中生成ACL,用于实现仅放行端点间的合法流量,并确保流量不能绕过Calico的安全措施。
最后是状态报告(State Reporting)功能,负责提供网络健康状态的相关数据,尤其是报告由其管理的节点上的错误和问题。这些报告数据会存储于etcd,供其他组件或网络管理员使用。
编排系统插件(Orchestrator Plugin)依赖于编排系统自身的实现,故此并不存在一个固定的插件以代表此组件。编排系统插件的主要功能是将Calico整合进系统中,并让管理员和用户能够使用Calico的网络功能。它主要负责完成API的转换和反馈输出。
编排系统通常有其自身的网络管理API,网络插件需要负责将对这些API的调用转为Calico的数据模型并存储于Calico的存储系统中。如果有必要,网络插件还要将Calico系统的信息反馈给编排系统,如Felix的存活状态,网络发生错误时设定相应的端点为故障等。
Calico使用etcd完成组件间的通信,并以之作为一个持久数据存储系统。根据编排系统的不同,etcd所扮演角色的重要性也因之而异,但它贯穿了整个Calico部署全程,并被分为两类主机:核心集群和代理(proxy)。在每个运行着Felix或编排系统插件的主机上都应该运行一个etcd代理以降低etcd集群和集群边缘节点的压力。此模式中,每个运行着插件的节点都会运行着etcd集群的一个成员节点。
etcd是一个分布式、强一致、具有容错功能的存储系统,这一点有助于将Calico网络实现为一个状态确切的系统:要么正常,要么发生故障。另外,分布式存储易于通过扩展应对访问压力的提升,而避免成为系统瓶颈。另外,etcd也是Calico各组件的通信总线,可用于确保让非etcd组件在键空间(keyspace)中监控某些特定的键,以确保它们能够看到所做的任何更改,从而使它们能够及时地响应这些更改。
Calico要求在每个运行着Felix的节点上同时还要运行一个BGP客户端,负责将Felix生成的路由信息载入内核并通告到整个IDC。在Calico语境中,此组件是通用的BIRD,因此任何BGP客户端(如GoBGP等)都可以从内核中提取路由并对其分发对于它们来说都适合的角色。
BGP客户端的核心功能就是路由分发,在Felix插入路由信息至内核FIB中时,BGP客户端会捕获这些信息并将其分发至其他节点,从而确保了流量的高效路由。
在大规模的部署场景中,简易版的BGP客户端易于成为性能瓶颈,因为它要求每个BGP客户端都必须连接至其同一网络中的其他所有BGP客户端以传递路由信息,一个有着N个节点的部署环境中,其存在网络连接的数量为N的二次方,随着N值的逐渐增大,其连接复杂度会急剧上升。因而在较大规模的部署场景中,Calico应该选择部署一个BGP路由反射器,它是由BGP客户端连接的中心点,BGP的点到点通信也就因此转化为与中心点的单路通信模型,如图11-18所示。出于冗余之需,生产实践中应该部署多个BGP路由反射器。对于Calico来说,BGP客户端程序除了作为客户端使用之外,还可以配置成路由反射器。
边界网关协议(Border Gateway Protocol, BGP)是互联网上一个核心的去中心化自治路由协议,它通过维护IP路由表或“前缀”表来实现自治系统(AS)之间的可达性,属于矢量路由协议。不过,考虑到并非所有的网络都能支持BGP,以及Calico控制平面的设计要求物理网络必须是二层网络,以确保vRouter间均直接可达,路由不能够将物理设备当作下一跳等原因,为了支持三层网络
BGP模式要求Kubernetes的所有物理节点网络必须是二层网络.为了支持三层网络,Calico还推出了IP-in-IP叠加的模型,它也使用Overlay的方式来传输数据。IPIP的包头非常小,而且也是内置在内核中,因此理论上它的速度要比VxLAN快一点,但安全性更差。Calico 3.x的默认配置使用的是IPIP类型的传输方案而非BGP。
工作于IPIP模式的Calico会在每个节点上创建一个tunl0接口(TUN类型虚拟设备)用于封装三层隧道报文。节点上创建的每一个Pod资源,都会由Calico自动创建一对虚拟以太网接口(TAP类型的虚拟设备),其中一个附加于Pod的网络名称空间,另一个(名称以cali为前缀后跟随机字串)留置在节点的根网络名称空间,并经由tunl0封装或解封三层隧道报文。Calico IPIP模式如下图所示。
当前k8s集群使用的是v1.17.3的版本.有2个node节点.IP地址分别如下
1 | [root@k8s-master ~]$kubectl get nodes -o wide | awk '{print $1,$6}' | sed 1,2d |
每个node节点都启动一个tunl0
的虚拟路由器.和许多calixxx
开头的虚拟网卡设备
1 | [root@k8s-node1 ~]# ifconfig |
Calico的CNI插件会为每个容器设置一个veth pair设备,然后把另一端接入到宿主机网络空间,由于没有网桥,CNI插件还需要在宿主机上为每个容器的veth pair设备配置一条路由规则,用于接收传入的IP包.
了这样的veth pair设备以后,容器发出的IP包就会通过veth pair设备到达宿主机,这些路由规则都是Felix维护配置的,而路由信息则是calico bird组件基于BGP分发而来。Calico实际上是将集群里所有的节点都当做边界路由器来处理,他们一起组成了一个全互联的网络,彼此之间通过BGP交换路由,这些节点我们叫做BGP Peer。
为了下面试验Calico的网络工作.当前集群使用daemonSet控制器运行了2个busybox:1.28.4
镜像的容器
1 | [root@k8s-master ~]$kubectl get pods -o wide |
在k8s-node1
节点上可以看到两条相关路由
1 | 10.100.36.103 0.0.0.0 255.255.255.255 UH 0 0 0 cali96df9f67b52 |
第一条路由是访问该节点下的Busybox容器.它的下一跳是calixxxx
开头的虚拟网卡.这种通信方式和docker的Bridge网桥模式其实并没有任何区别.
第二条路由的目的网络是10.100.169.128,子网掩码是255.255.255.192.它代表了IP范围为10.100.169.128-190的地址.而运行于另外一个节点下的busybox-zdwsc
Pod的IP地址就位于这个范围之内.所以这条路由可以使node1节点借助于tunl0可以直接和node2节点下的pod进行通信.
在
k8s-node2
服务器可以看到类似的这2条路由
登录k8s-node1
节点下的Pod容器内部.查看Pod容器的IP地址,以及路由条目.
1 | [root@k8s-master ~]$kubectl exec -it busybox-g5rkr -- sh |
通过k8s-node
节点上的下面的路由条目,我们可以知道节点主机和Pod容器的IP地址10.100.36.103
通信使用的是cali96df9f67b52
这个虚拟网卡
1 | 10.100.36.103 0.0.0.0 255.255.255.255 UH 0 0 0 cali96df9f67b52 |
路由条目显示169.254.1.1
是Pod容器的默认网关.但是有网络常识的我们都知道这个IP是个保留的IP地址,不存在于互联网或者任何设备中.那Pod如何和网关通信呢?
回顾一下网络课程,我们知道任何网络设备和网关设备都是在一个二层局域网中,而二层数据链路层使用MAC地址进行通信,不需要双方的IP地址信息.通信方(这里是Pod容器)会通过ARP协议获取网关的MAC地址,然后通过MAC地址将数据包发送给网关..也就是说网络设备不关心对方的IP是否可达,只要能找到对应的MAC地址就可以.
通过ip neigh
命令查看Pod容器的ARP缓存
1 | / # ip neigh |
如果是新的Pod容器可能无法获得ARP缓存,此时只需要随便发生一个网络交互(例如ping百度)即可
这个MAC地址(ee:ee:ee:ee:ee:ee)也是Calico的虚拟cali96df9f67b52
网卡的虚拟MAC地址.下放是宿主机网卡信息:
1 | cali96df9f67b52: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1440 |
所有虚拟网卡默认开启了ARP代理协议
1 | [root@k8s-node1 ~]# cat /proc/sys/net/ipv4/conf/cali96df9f67b52/proxy_arp |
所以Calico 通过一个巧妙的方法将 Pod 的所有流量引导到一个特殊的网关 169.254.1.1,从而引流到主机的 calixxx 网络设备上,最终将二三层流量全部转换成三层流量来转发。
登录busybox-g5rkr
Pod容器内部.ping位于另外一台k8s-node2
下的busybox-zdwsc
Pod容器
1 | [root@k8s-master ~]$kubectl get pods -o wide |
两个Pod之前可以直接访问对方的IP地址.而不需要像Docker容器那样暴露端口,然后利用对方宿主机的IP进行通信
1 | [root@k8s-master ~]$kubectl exec -it busybox-g5rkr -- sh |
在k8s-node2
节点抓包
1 | [root@k8s-node2 ~]# tcpdump -i ens192 -nn -w imcp.cap |
用wireshark软件打开抓包文件.发现如下ICMP的报文
可以看到每个数据报文共有两个IP网络层,内层是Pod容器之间的IP网络报文,外层是宿主机节点的网络报文(2个node节点).之所以要这样做是因为tunl0是一个隧道端点设备,在数据到达时要加上一层封装,便于发送到对端隧道设备中。
Pod间的通信经由IPIP的三层隧道转发,相比较VxLAN的二层隧道来说,IPIP隧道的开销较小,但其安全性也更差一些。
IPIP的通信方式如下:
经过测试.在k8s集群内部物理节点和pod容器内部访问Service的http服务.仍然使用的是Ipip通信模式.
下面是在容器内部通过Service访问busybox pod容器的http服务的抓包报文
1 | [root@k8s-master ~]$kubectl exec -it busybox-6hnvc -- sh |
Calico网络部署时,默认安装就是IPIP网络.通过修改calico.yaml部署文件中的CALICO_IPV4POOL_IPIP
值修改成off
就切换到BGP网络模式
1 | # Enable IPIP |
重新部署calico
1 | [root@k8s-master ~]$kubectl apply -f calico-3.10.2.yaml |
然后关闭ipipMode.把ipipMode从Always修改成为Never
1 | [root@k8s-master1 target]# kubectl edit ippool |
BGP网络相比较IPIP网络,最大的不同之处就是没有了隧道设备 tunl0。 前面介绍过IPIP网络pod之间的流量发送tunl0,然后tunl0发送对端设备。BGP网络中,pod之间的流量直接从网卡发送目的地,减少了tunl0这个环节。
删除原来的pod.重新启动新的
1 | [root@k8s-master ~]$kubectl create -f deployment-kubia-v1.yaml |
再次查看路由表.发现节点和pod容器通信直接通过宿主机的物理网卡,而不是tunl0设备了
1 | [root@k8s-master ~]$route -n |
此时,再次2个Pod容器互ping抓包分析.发现两个Pod像物理机一样直接通信,而不需要进行任何数据包封装和解封装.并且数据报文的MAC地址也是node1和node2物理网卡的MAC地址
BGP的网络连接方式:
IPIP:
特点: tunl0封装数据.形成隧道.所有Pod和pod.pod和节点之间进行三层网络传输
优点: 适用所有网络类型.能够解决跨网段的路由问题.
BGP:
特点: 适用BGP路由导向流量
优点: Pod之间直接通信.省去了隧道,封装,解封装等任何中间环节,传输效率非常高.
缺点: 需要确保所有物理节点在同一个二层网络,否则Pod无法跨节点网段通信
Calico 的IPIP网络模型下tunl0接口的MTU默认为1440,这种设置主要是为适配Google的GCE环境,在非GCE的物理环境中,其最佳值为1480。因此,对于非GCE环境的部署,建议将配置清单calico.yaml下载至本地修改后,再将其应用到集群中。要修改的内容是DaemonSet资源calico-node的Pod模板,将容器calico-node的环境变量“FELIX_INPUTMTU”的值修改为1480即可
因为IPIP多了一层IP报文封装,而IP报文头部一般是20个字节.所以MUT的值应该是最大1500-20.
对于50个节点以上规模的集群来说,所有Calico节点均基于Kubernetes API存取数据会为API Server带来不小的通信压力,这就应该使用calico-typha进程将所有Calico的通信集中起来与API Server进行统一交互。calico-typha以Pod资源的形式托管运行于Kubernetes系统之上,启用的方法为下载前面步骤中用到的Calico的部署清单文件至本地,修改其calico-typha的Pod资源副本数量为所期望的值并重新应用配置清单即可:
1 | apiVersion: apps/v1beta1 |
每个calico-typha Pod资源可承载100到200个Calico节点的连接请求,最多不要超过200个。另外,整个集群中的calico-typha的Pod资源总数尽量不要超过20个。
默认情况下,Calico的BGP网络工作于点对点的网格(node-to-node mesh)模型,它仅适用于较小规模的集群环境。中级集群环境应该使用全局对等BGP模型(Global BGP peers),以在同一二层网络中使用一个或一组BGP反射器构建BGP网络环境。而大型集群环境需要使用每节点对等BGP模型(Per-node BGP peers),即分布式BGP反射器模型,一个典型的用法是将每个节点都配置为自带BGP反射器接入机架顶部交换机上的路由反射器。
事实上,仅在那些不支持用户自定义BGP配置的网络中才需要使用IPIP的隧道通信类型。如果有一个自主可控的网络环境且部署规模较大时,可以考虑启用BGP的通信类型降低网络开销以提升传输性能,并且应该部署BGP反射器来提高路由学习效率。
Calico官网: www.projectcalico.org
k8s网络之Calico网络: https://www.cnblogs.com/goldsunshine/p/10701242.html#mxAMjXzT
kubernetes容器网络: https://tech.ipalfish.com/blog/2020/03/06/kubernetes_container_network/ (伴鱼团队)
<kubernetes进阶实战> 11.4 马永亮
]]>顾名思义,DaemonSet的主要作用是让你在kubernetes集群里运行一个Daemon Pod.所以,这个Pod有如下三个特征:
1.这个Pod运行在kubernetes集群的每一个节点(Node)上
2.每个节点只有一个这样的Pod实例
3.当有新的节点加入 Kubernetes 集群后,该 Pod 会自动地在新节点上被创建出来;而当旧节点被删除后,它上面的 Pod 也相应地会被回收掉.
这个机制听起来很简单,但 Daemon Pod 的意义确实是非常重要的。我随便给你列举几个例子:
更重要的是,跟其他编排对象不一样,DaemonSet 开始运行的时机,很多时候比整个 Kubernetes 集群出现的时机都要早。
这个乍一听起来可能有点儿奇怪。但其实你来想一下:如果这个 DaemonSet 正是一个网络插件的 Agent 组件呢?
这个时候,整个 Kubernetes 集群里还没有可用的容器网络,所有 Worker 节点的状态都是 NotReady(NetworkReady=false)。这种情况下,普通的 Pod 肯定不能运行在这个集群上。所以,这也就意味着 DaemonSet 的设计,必须要有某种“过人之处”才行。
为了弄清楚 DaemonSet 的工作原理,我们还是按照老规矩,先从它的 API 对象的定义说起。下面是一个Nginx的daemonset资源清单
1 | apiVersion: apps/v1 |
这个DaemonSet非常简单,管理一个Nginx镜像的Pod.可以看到DaemonSet和Deployment非常相似,只不过没有replicas字段.他也使用selector选择管理所有携带了name=Nginx标签的POD
那么,DaemonSet 又是如何保证每个 Node 上有且只有一个被管理的 Pod 呢?
显然,这是一个典型的“控制器模型”能够处理的问题。
DaemonSet Controller,首先从 Etcd 里获取所有的 Node 列表,然后遍历所有的 Node。这时,它就可以很容易地去检查,当前这个 Node 上是不是有一个携带了 name=fluentd-elasticsearch 标签的 Pod 在运行。
而检查的结果,可能有这么三种情况:
其中,删除节点(Node)上多余的 Pod 非常简单,直接调用 Kubernetes API 就可以了。
但是,如何在指定的 Node 上创建新 Pod 呢?
如果你已经熟悉了 Pod API 对象的话,那一定可以立刻说出答案:用 nodeSelector,选择 Node 的名字即可。
1 | nodeSelector: |
没错。不过,在 Kubernetes 项目里,nodeSelector 其实已经是一个将要被废弃的字段了。因为,现在有了一个新的、功能更完善的字段可以代替它,即:nodeAffinity。我来举个例子:
1 | apiVersion: v1 |
在这个 Pod 里,我声明了一个 spec.affinity 字段,然后定义了一个 nodeAffinity。
而在这里,我定义的 nodeAffinity 的含义是:
metadata.name
”是“node-geektime”的节点上。在这里,你应该注意到 nodeAffinity 的定义,可以支持更加丰富的语法,比如 operator: In(即:部分匹配;如果你定义 operator: Equal,就是完全匹配),这也正是 nodeAffinity 会取代 nodeSelector 的原因之一。
其实在大多数时候,这些 Operator 语义没啥用处。所以说,在学习开源项目的时候,一定要学会抓住“主线”。不要顾此失彼。
所以,我们的 DaemonSet Controller 会在创建 Pod 的时候,自动在这个 Pod 的 API 对象里,加上这样一个 nodeAffinity 定义。其中,需要绑定的节点名字,正是当前正在遍历的这个 Node。
当然,DaemonSet 并不需要修改用户提交的 YAML 文件里的 Pod 模板,而是在向 Kubernetes 发起请求之前,直接修改根据模板生成的 Pod 对象。这个思路,也正是我在前面讲解 Pod 对象时介绍过的。
创建刚才的Nginx的yaml清单:
1 | [root@k8s-master daemonset]$kubectl get pods -n kube-system -l name=nginx |
查看其中任意一个pod的yaml文件
1 | [root@k8s-master daemonset]$kubectl get pods -n kube-system nginx-b44l8 -o yaml |
这个是DaemonSet自动为Pod打上的nodeaffinity节点亲和性属性,表示将该Pod调度到k8s-node2
这个hostname的节点上.
如果查看其它2个Nginx的Pod,他的nodeaffinity调度的节点名称自然也会不一样
通过nodeaffinity,DaemonSet就可以确保每个Pod都调度到不同的k8s节点.而不会将多个pod调度到同一个节点.但是如果需要确保Pod可以被调度到节点上,还需要利用另外一个和调度相关的字段:tolerations
tolerations(容忍度)这个字段意味着这个 Pod,会“容忍”(Toleration)某些 Node 的“污点”(Taint)。
而tolerations字段也是daemonset自动加上去的.还是执行上面的那条命令查看Pod的yaml清单文件
1 | tolerations: |
可以看到DaemonSet自动为这个Pod打上了很多容忍度,包括节点not-ready,unreachable,节点的磁盘,内存,pid的压力,以及哪怕节点被标记为unschedulable
.就使得这些 Pod 可以忽略所有这些节点限制,继而保证每个节点上都会被调度一个 Pod。当然,如果这个节点有故障的话,这个 Pod 可能会启动失败,而 DaemonSet 则会始终尝试下去,直到 Pod 启动成功。
而在正常情况下,被标记了 unschedulable“污点”的 Node,是不会有任何 Pod 被调度上去的(effect: NoSchedule)
当然,你也可以在daemonset的资源清单里手动加上各种toleration污点容忍度.就像上面的例子:
1 | tolerations: |
因为在默认情况下,Kubernetes 集群不允许用户在 Master 节点部署 Pod。因为,Master 节点默认携带了一个叫作node-role.kubernetes.io/master
的“污点”。所以,为了能在 Master 节点上部署 DaemonSet 的 Pod,我就必须让这个 Pod“容忍”这个“污点”。
这时,你应该可以猜到,我在前面介绍到的DaemonSet 的“过人之处”,其实就是依靠 Toleration 实现的。
假如当前 DaemonSet 管理的,是一个网络插件的 Agent Pod,那么你就必须在这个 DaemonSet 的 YAML 文件里,给它的 Pod 模板加上一个能够“容忍”node.kubernetes.io/network-unavailable
“污点”的 Toleration。正如下面这个例子所示:
1 | ... |
在 Kubernetes 项目中,当一个节点的网络插件尚未安装时,这个节点就会被自动加上名为node.kubernetes.io/network-unavailable
的“污点”。
而通过这样一个 Toleration,调度器在调度这个 Pod 的时候,就会忽略当前节点上的“污点”,从而成功地将网络插件的 Agent 组件调度到这台机器上启动起来。
通过命令查看daemonset的对象
1 | [root@k8s-master daemonset]$kubectl get ds nginx -n kube-system |
就会发现 DaemonSet 和 Deployment 一样,也有 DESIRED、CURRENT 等多个状态字段。这也就意味着,DaemonSet 可以像 Deployment 那样,进行版本管理。这个版本,可以使用 kubectl rollout history 看到该daemonset的发布版本:
1 | [root@k8s-master daemonset]$kubectl rollout history daemonset nginx -n kube-system |
接下来将nginx的镜像版本升级到1.19.0.顺便加上–record参数
1 | [root@k8s-master daemonset]$kubectl set image ds nginx nginx=nginx:1.19.0 -n kube-system --record |
观察升级过程
1 | [root@k8s-master daemonset]$kubectl rollout status ds nginx -n kube-system |
在rollout history里就能看到滚动更新的记录:
1 | [root@k8s-master daemonset]$kubectl rollout history daemonset nginx -n kube-system |
通过后面的具体命令可以看到,这里我们是发布到了第6版.有了版本号,你也就可以像 Deployment 一样,将 DaemonSet 回滚到某个指定的历史版本了。
而我在前面的文章中讲解 Deployment 对象的时候,曾经提到过,Deployment 管理这些版本,靠的是“一个版本对应一个 ReplicaSet 对象”。可是,DaemonSet 控制器操作的直接就是 Pod,不可能有 ReplicaSet 这样的对象参与其中。那么,它的这些版本又是如何维护的呢?
所谓,一切皆对象!
在 Kubernetes 项目中,任何你觉得需要记录下来的状态,都可以被用 API 对象的方式实现。当然,“版本”也不例外。
Kubernetes v1.7 之后添加了一个 API 对象,名叫ControllerRevision,专门用来记录某种 Controller 对象的版本。比如,你可以通过如下命令查看 fluentd-elasticsearch 对应的 ControllerRevision:
1 | [root@k8s-master daemonset]$kubectl get controllerrevision -n kube-system -l name=nginx |
可以看到每个版本号(REVISION)对应一个controller.而如果你使用 kubectl describe 查看这个 ControllerRevision 对象:
1 | [root@k8s-master daemonset]$kubectl describe controllerrevision -n kube-system nginx-7ccc97dc9f |
就会看到,这个 ControllerRevision 对象,实际上是在 Data 字段保存了该版本对应的完整的 DaemonSet 的 API 对象。并且,在 Annotation 字段保存了创建这个对象所使用的 kubectl 命令。
接下来,我们可以尝试将这个 DaemonSet 回滚到 Revision=4 时的状态:
1 | [root@k8s-master daemonset]$kubectl rollout undo daemonset nginx --to-revision=4 -n kube-system |
这个 kubectl rollout undo 操作,实际上相当于读取到了 Revision=4 的 ControllerRevision 对象保存的 Data 字段。而这个 Data 字段里保存的信息,就是 Revision=1 时这个 DaemonSet 的完整 API 对象。
所以,现在 DaemonSet Controller 就可以使用这个历史 API 对象,对现有的 DaemonSet 做一次 PATCH 操作(等价于执行一次 kubectl apply -f “旧的 DaemonSet 对象”),从而把这个 DaemonSet“更新”到一个旧版本。
这也是为什么,在执行完这次回滚完成后,你会发现,DaemonSet 的 Revision 并不会从 Revision=6 退回到 4,而是会增加成 Revision=7。这是因为,一个新的 ControllerRevision 被创建了出来。
1 | [root@k8s-master daemonset]$kubectl rollout history daemonset nginx -n kube-system |
原文文档里说是一个新的 ControllerRevision 被创建了出来
.但是经过实践发现,并没有创建一个新的revision=7的controllerrevision.而是仍然使用revision=4的controllerrevision,只不过将他的版本从4替代成了7..仔细对比下面回滚后的controllerrevision信息和回滚之前的信息可以发现这点:
1 | [root@k8s-master daemonset]$kubectl get controllerrevision -n kube-system -l name=nginx |
nginx-6fdb467d8c
版本变更成了71 | [root@k8s-master daemonset]$kubectl get controllerrevision -n kube-system -l name=nginx |
这可能是作者使用的k8s集群版本(1.11)和我实践的版本(1.17.3)不同
相比于 Deployment,DaemonSet 只管理 Pod 对象,然后通过 nodeAffinity 和 Toleration 这两个调度器的小功能,保证了每个节点上有且只有一个 Pod。
与此同时,DaemonSet 使用 ControllerRevision,来保存和管理自己对应的“版本”。这种“面向 API 对象”的设计思路,大大简化了控制器本身的逻辑,也正是 Kubernetes 项目“声明式 API”的优势所在。
而且,相信聪明的你此时已经想到了,StatefulSet 也是直接控制 Pod 对象的,那么它是不是也在使用 ControllerRevision 进行版本管理呢?
没错。在 Kubernetes 项目里,ControllerRevision 其实是一个通用的版本管理对象。这样,Kubernetes 项目就巧妙地避免了每种控制器都要维护一套冗余的代码和逻辑的问题。
张磊—<深入剖析Kubernetes>: 容器化守护进程的意义:DaemonSet
]]> Docker容器诞生以来,,如何确定合适的网络方案是亟待解决的难题之一.在日趋复杂的业务场景下,网络的复杂性也呈几何级数上升.本篇首先回顾了Docker容器的网络通信,然后介绍Kubernertes的网络模型.在Kubernetes集群中,IP地址的分配对象是以Pod为单位,而非容器.同一个Pod内的所有容器共享同一个网络名称空间
一个Linux容器的网络栈是被隔离在它自己的Network Namespace中,Network Namespace包括了:网卡(Network Interface),回环设备(Lookback Device),路由表(Routing Table)和iptables规则,对于服务进程来讲这些就构建了它发起请求和相应的基本环境。而要实现一个容器网络,离不开以下Linux网络功能:
Docker容器网络的原始模型主要用到的就是Bridge桥接网络.Docker守护进程首次启动时,会在当前宿主机节点创建一个名为docker0
的虚拟网桥设备.并默认配置其使用172.17.0.0/16的网络.
Host和Container网络模型使用场景非常少.不再费篇幅介绍
并且为该主机节点上的每一个容器分配一个虚拟的以vethxxx
开头的虚拟网卡.从而使得同一节点下的所有容器都可以在二层网络模式下.利用docker0
虚拟网桥实现容器和容器之间,容器和宿主机节点之间的网络通信.
同一宿主机节点下的容器网络通信方式如下
以上是同节点上的容器通信方式.对于不同节点的容器之间进行通信,Docker则无能为力.因为每个节点的docker0网桥分配的虚拟IP都是同一网段,所以不同宿主机节点上的容器可能使用的是同一个IP地址,双方并不清楚对方容器具体在哪台节点.
解决此问题的方式是NAT.所有容器均会被NAT隐藏在节点网络之内.他们发往Docker主机外部的所有流量都会SNAT后出去,容器若要接入Docker主机外部的流量,则需要事先将网络端口暴露到宿主机的端口..然后对方容器的流量达到宿主机后再执行DNAT转发给目的容器.
不同宿主机节点下的容器网络通信方式如下
这种解决方式在网络规模庞大的时候兼职就是个灾难.转发效率非常低下,宿主机上端口变成一种稀缺资源.
Kubernetes的网络模型主要用于解决四类通信需求:
Pod对象内的各容器共享同一个网络名称空间.所有运行于同一个Pod内部的容器与同一主机上的多个进程类似.彼此之间可以通过localhost
或者lo
回环接口进行通信.
例如下图3-1所示,每个节点上的Container1和container2容器在一个Pod内部,共享同一个IP地址和网络接口
图 3-1 Pod网络
Kubernertes要求每个Pod对象需要运行于同一个平面网络中,并且都拥有一个集群内全局唯一的IP地址,可以直接于其他Pod通信.例如上图3-1中的Pod P和Pod Q之间通信.另外,运行Pod的各节点也会通过桥接设备等持有此平面网络中的一个IP地址,如图3-1中的cbr0接口,这就意味着Node到Pod间的通信也可在此网络上直接进行。因此,Pod间的通信或Pod到Node间的通信比较类似于同一IP网络中主机间进行的通信。
Service资源的专用网络也称为集群网络(Cluster Network),需要在启动kube-apiserver时经由“–service-cluster-ip-range”选项进行指定,如10.96.0.0/12,而每个Service对象在此网络中均拥一个称为Cluster-IP的固定地址。管理员或用户对Service对象的创建或更改操作由API Server存储完成后触发各节点上的kube-proxy,并根据代理模式的不同将其定义为相应节点上的iptables规则或ipvs规则,借此完成从Service的Cluster-IP与Pod-IP之间的报文转发,如图3-2所示。
图 3-2 Service和Pod
将集群外部的流量引入到Pod对象的方式有受限于Pod所在的工作节点范围的节点端口(nodePort)和主机网络(hostNetwork)两种,以及工作于集群级别的NodePort或LoadBalancer类型的Service对象。不过,即便是四层代理的模式也要经由两级转发才能到达目标Pod资源:请求流量首先到达外部负载均衡器,由其调度至某个工作节点之上,而后再由工作节点的netfilter(kube-proxy)组件上的规则(iptables或ipvs)调度至某个目标Pod对象。
Kubernetes设计了以上四种网络模型.但是Kubernetes自己并不负责网络具体工作,而是交给的了第三方网络插件.为了规范以及兼容各种解决方案.CoreOS和Google联合制定了CNI(Container Network Interface)标准,旨在定义容器网络模型规范。它连接了两个组件:容器管理系统和网络插件。它们之间通过JSON格式的文件进行通信,以实现容器的网络功能.具体的网络工作均由插件来实现,包括创建容器netns、关联网络接口到对应的netns以及为网络接口分配IP等。
CNI的基本思想是:容器运行时环境在创建容器时,先创建好网络名称空间(netns),然后调用CNI插件为这个netns配置网络,而后再启动容器内的进程。
Kubernetes要求网络插件需要满足以下基本原则:
CNI本身只是规范,付诸生产还需要有特定的实现。目前,CNI提供的插件分为三类:main、meta和ipam。main一类的插件主要在于实现某种特定的网络功能,例如loopback、bridge、macvlan和ipvlan等;meta一类的插件自身并不提供任何网络实现,而是用于调用其他插件,例如调用flannel;ipam仅用于分配IP地址,而不提供网络实现。
CNI具有很强的扩展性和灵活性,例如,如果用户对某个插件具有额外的需求,则可以通过输入中的args和环境变量CNI_ARGS进行传递,然后在插件中实现自定义的功能,这大大增加了它的扩展性。另外,CNI插件将main和ipam分开,赋予了用户自由组合它们的机制,甚至一个CNI插件也可以直接调用另外一个CNI插件。CNI目前已经是Kubernetes当前推荐的网络方案。常见的CNI网络插件包含如下这些主流的项目.
Flannel.
一个为Kubernetes提供叠加网络的网络插件,它基于Linux TUN/TAP,使用UDP封装IP报文来创建叠加网络,并借助etcd维护网络的分配情况。
Calico
一个基于BGP的三层网络插件,并且也支持网络策略来实现网络的访问控制;它在每台机器上运行一个vRouter,利用Linux内核来转发网络数据包,并借助iptables实现防火墙等功能。
Weave Net:
Weave Net是一个多主机容器的网络方案,支持去中心化的控制平面,在各个host上的wRouter间建立Full Mesh的TCP连接,并通过Gossip来同步控制信息。
实际上CNI的容器网络通信流程跟前面的基础网络一样,只是CNI维护了一个单独的网桥来代替 docker0。这个网桥的名字就叫作:CNI 网桥,它在宿主机上的设备名称默认是:cni0。cni的设计思想,就是:Kubernetes在启动Infra容器之后,就可以直接调用CNI网络插件,为这个Infra容器的Network Namespace,配置符合预期的网络栈。
CNI插件三种网络实现模式:
kubernetes容器网络: https://tech.ipalfish.com/blog/2020/03/06/kubernetes_container_network/ (伴鱼团队)
<kubernetes进阶实战> 11.1 马永亮
]]>kubernets的所有资源.包括Service,Pod都有生命周期,会频繁的销毁和创建.这些资源的IP地址也会随之动态变化.所以Kubernetes使用DNS实现通过资源名解析IP地址.
Kubernetes集群安装了默认的Core-dns组件(通过Pod方式运行).以及kube-dns的service.
1 | [root@k8s-master ~]$kubectl get pods -n kube-system | grep dns |
创建一个临时的pod容器,测试DNS解析效果.下面的命令临时运行了一个busybox的镜像
1 | [root@k8s-master ~]$kubectl run -it dns-test --rm --image=busybox:1.28.4 -- sh |
不要使用latest版本的镜像,其dns解析有问题.最好使用1.28.4版本的
下方是Pod容器的内部dns服务器信息
1 | / # cat /etc/resolv.conf |
resolv.conf
配置文件说明
nameserver:指明DNS服务器地址.也就是上文提到的kube-dns
的service
search:当原始域名不能被DNS解析时,resolver会将该域名加上search指定的参数,重新请求DNS,直到被正确解析或试完search指定的列表为止 options:dns配置
ndots:5:所有DNS查询中,如果“.”的个数少于5个,则会根据search中配置的列表依次在对应域中先进行搜索,如果没有返回,则最后再直接查询域名本身
为了了解search
和ndots
的概念,我们先要了解FQDN的概念.FQDN(Fully qualified domain name)
即完整域名。一般来说如果一个域名以.
结束,就表示一个完整域名。比如www.abc.xyz.
就是一个FQDN
,而www.abc.xyz
则不是FQDN
。了解了这个概念之后我们就来看search
和options ndots
。
如果一个域名是FQDN
,那么这个域名会被转发给DNS服务器进行解析。如果域名不是FQDN
,那么这个域名会到search
搜索解析,还是通过一个例子说明,比如访问abc.xyz
这个域名,因为它并不是一个FQDN
,所以它会和search
域中的值进行组合而变成一个FQDN
,以上文的resolv.conf
为例,这域名会这样组合:
1 | abx.xyz.default.svc.cluster.local. |
然后这些域名先被kube-DNS
解析,如果没有解析成功再由宿主机的DNS
服务器进行解析。
而ndots
是用来表示一个域名中.
的个数在不小于该值的情况下会被认为是一个FQDN
。简单说这个属性用来判断一个不是以.
结束的域名在什么条件下会被认定为是一个FQDN
.上面的示例中ndots为5,也就是说如果一个域名中.
的数量大于等于5,即使域名不是以.
结尾,也会被认定为是一个FQDN
。比如:域名是abc.xyz.xxx.yyy.zzz.aaa
这个域名就是FQDN
.
之所以会有search
域主要还是为了方便k8s内部服务之间的访问。比如:k8s在同一个namespace
下是可以直接通过服务名称进行访问的,其原理就是会在search
域查找,比如上面的resolv.conf
中jplat、oms-dev
着两个其实都是这两个pod所在的namespace
的名称。所以通过服务名称访问的时候,会和search
域进行组合,这样最终域名会组合成servicename.namespace.svc.cluster.local
。而如果是跨namespace
访问,则可以通过servicename.namespace
这样的形式,在通过和search
域组合,依然可以得到servicename.namespace.svc.cluster.local
。
Kubernetes集群中的每个Service资源都会被指派一个DNS名称.客户端Pod的DNS搜索列表默认是搜索自己的namespace
名称空间内的资源.
例如上文的resolv.conf
文件内的search搜索列表为search default.svc.cluster.local svc.cluster.local cluster.local localdomain
.此时Pod可以直接搜索default
名称空间下的所有Service:
例如.使用上面的临时busybox容器解析my-svc
的Service
1 | / # nslookup my-svc |
上面的IP地址10.96.51.58
表示成功解析到该Service的IP.my-svc.default.svc.cluster.local
这个就是该Service的FQDN完全限定域名.
其中:
default
—表示名称空间,我们的名称空间名字就是默认的default
svc
—————-表示资源类型,这里是Service
cluster.local
–k8s集群域名
也可以解析其他名称空间内的资源,比如解析kube-system
名称空间下的DNS服务器的Service.(DNS服务器本身也会被指定一个DNS名称).就可以通过<svc-name>.<namespace-name>
实现.比如下面解析kube-system
名称空间下的kube-dns
的Service
1 | / # nslookup kube-dns.kube-system |
实际上DNS解析的是完全FQDN域名,只不过后面一部分内容default.svc.cluster.local
可以省略罢了.默认就是解析当前名称空间下的资源
1 | / # nslookup my-svc |
在kubernetes官网中也提到:
假设在 Kubernetes 集群的名字空间 bar
中,定义了一个服务 foo
。 运行在名字空间 bar
中的 Pod 可以简单地通过 DNS 查询 foo
来找到该服务。 运行在名字空间 quux
中的 Pod 可以通过 DNS 查询 foo.bar
找到该服务。
对于普通的Service资源.会以<service-name>.<namespace-name>.svc.cluster.local
这种形式被分配一个DNS A记录.也就是上文中的my-svc
的10.96.51.58
这个IP地址.
如果是对于无头服务(headless service).这种service没有IP.但是也会以上面的形式被指派一个DNS的A记录.只不过这种记录和普通Service不同,而是被解析成对应服务的POD集合的Pod的IP.客户端使用标准的负载均衡策略从这组Pod中进行选择.
例如下面创建一个headless的svc.和普通svc的区别在于clusterIP的值为None
.
1 | [root@k8s-master ~]$cat deployment-kubia-v1.yaml |
headless服务一般用于statefulset资源.不能用于deployment控制器
创建该文件后查看hsq-openapi
service:
1 | [root@k8s-master ~]$kubectl describe svc hsq-openapi |
headless类型服务的DNS解析
仍然使用上文中的busybox测试容器.解析hsq-openapi
service 的A记录.可以看到解析的结果返回了2个pod的IP地址列表.对于这种类型的service.和普通的service不同.他解析出来的是POD的ip地址列表
1 | / # nslookup hsq-openapi |
一般而言,Pod会对应如下DNS名字解析: pod-ip-address.<namespace-name>.pod.cluster.local
例如对于上面例子中的iP为10.100.36.69
的Pod.对应的DNS名称为:
1 | / # nslookup 10-100-36-69.default.pod.cluster.local #DNS名称 |
k8s提供了5种DNS策略,如下:
Default
: Pod 从运行所在的节点继承名称解析配置。ClusterFirst
: 与配置的集群域后缀不匹配的任何 DNS 查询(例如 “www.kubernetes.io”) 都将转发到从节点继承的上游名称服务器。集群管理员可能配置了额外的存根域和上游 DNS 服务器。ClusterFirstWithHostNet
:对于以 hostNetwork 方式运行的 Pod,应显式设置其 DNS 策略 ClusterFirstWithHostNet
。None
: 此设置允许 Pod 忽略 Kubernetes 环境中的 DNS 设置。Pod 会使用其 dnsConfig
字段 所提供的 DNS 设置。k8s默认使用的DNS策略是ClusterFirst
,这点需要注意,也就是说域名解析会优先使用集群的DNS(kube-DNS
)进行查询,如果k8s的DNS解析失败,会转发到宿主机的DNS进行解析。
近期发现公司某个业务对外的openapi接口的/merchantapi路径异常调用非常频繁.公司的第三方商户需要通过这个路径来调用ERP接口,但是经常发生被恶意刷接口的情况,导致公司的业务服务器资源使用率飙升,面临很大的宕机风险和隐患.
目前外部客户端访问公司业务仍然是阿里云SLB—–Nginx—php-fpm的架构.由于Nginx的限流能力并不出色,特别是针对具体path路径的限流.所以,引入了Kong api网关
Rate Limiting是Kong社区版就已经自带的官方流量控制插件.详细信息可以参考Kong官网介绍. https://docs.konghq.com/hub/kong-inc/rate-limiting/
它可以针对consumer
,credential
,ip
,service
,path
,header
等多种维度来进行限流.流量控制的精准度也有多种方式可以参考,比如可以做到秒级,分钟级,小时级等限流控制.
当启用这个插件后.Kong会响应客户端一些额外的头部信息,告诉客户端限流信息.例如下面是Kong响应给客户端的header信息,告诉客户端当前的限流策略是10r/s
1 | RateLimit-Limit: 10 |
如果客户端的访问请求超过限流的阈值,Kong会返回status429
的状态码以及下面的错误信息
1 | { "message": "API rate limit exceeded" } |
Rate limiting插件支持3种限流策略.
cluster
集群策略.Kong的数据库会维护一个计数器,并且在所有的Kong集群内每个节点共享这个计数器.如果计数器触发限流上线,所有的Kong节点都拒绝客户端的转发.这就意味着每个节点接收到客户端的请求,都会对数据库进行读写操作.
redis
redis策略和cluster
相似,唯一不同的是,计数器是存储在redis数据中.并且在集群内所有节点共享.
local
本地策略.计数器保存在Kong节点服务器本地内存缓冲区.并且计数器只对该节点有效.这意味着local
策略有最好的性能表现.但是由于计数器存储在本地.所以限流的精度没有redis
和cluster
准确.并且会影响Kong节点服务器弹性扩容(比如限流设置30r/s,Kong集群从2个节点扩容到4个节点.限流就从60r/s变成了120r/s.此时需要手动将限流设置从30r/s降低到15r/s)
或者,可以在Kong前面配置一个hash转发策略的负载均衡,将同一个外部客户端的请求代理到同一个节点.这样local策略的精确度可以提升,并且kong节点的弹性扩容不会影响限流效果
下面是3种限流策略的对比表
policy | describe | pros | cons |
---|---|---|---|
cluster | 集群策略 | 限流精准度高,不需要第三方组件支持 | 对Kong性能影响比较大 |
redis | redis策略 | 限流精准度高,对Kong性能影响较低 | 需要额外的redis服务 |
local | 本地策略 | 对Kong性能影响最低 | 精准度比较差,Kong节点扩容和缩容需要手动调整限流速率 |
下面是以上集群策略的使用场景:
我们场景中针对客户端IP进行限流.但是由于Kong是在SLB或者Nginx的负载均衡后面,所以默认情况下,Kong采用的IP是上一级负载均衡器的IP.此时就需要将客户端的真实IP传递到Kong,并且使用该IP作为remote_ip
.实现方法如下:
针对rpm包或者其他方式安装的Kong服务,可以修改默认的/etc/kong/kong.conf
配置文件.加入下面2行配置信息:
1 | trusted_ips = 0.0.0.0/0,::/0 |
重载kong配置
1 | kong reload |
针对docker容器方式运行的Kong,修改配置文件不方便,此时可以通过变量注入的方式自定义配置kong.conf
配置文件.还可以通过这种方式注入nginx自定义配置,具体可以参考官方的文档介绍:environment-variables
例如,上面的2行配置内容可以通过在配置参数前面加KONG_
以及大写的参数名的方式注入环境变量
1 | KONG_TRUSTED_IPS=0.0.0.0/0,::/0 |
修改Kong的docker-compose
文件:
1 | kong: |
Rate Limiting插件由Kong默认提供,所以无需自行安装.由于是针对/merchantapi
这个借口进行限流,所以只需配置该route,并且将插件应用到这个route下即可.由于我日常使用的python进行Kong的配置,所以这里只列出我的python配置文件中相关配置.不演示具体配置了.
使用kong的dashboard也可以很方便的实现配置
1 | hsq_openapi_dev = { "name": "hsq_openapi_dev", |
1 | #默认转发路由 |
1 | hsq_merchantapi_limit = { "route_name": "hsq_merchant_api_limit", #关联到上面的route.表示该插件作用在route级别 |
运行python脚本,配置Kong
1 | huangyong@huangyong-Macbook-Pro ~/Desktop/kong-python master ●✚ python3 kong.py |
为了验证插件效果,这里使用ab
这个简单的压测工具进行测试.
1.开启一个终端,执行下面的命令.压测命令运行了1.18秒,只有20个请求成功响应,其余80个请求失败.这恰好符合了rate-limiting插件每秒10个请求的限流策略
由于是在dev环境,所有只有一个Kong节点.如果外部流量负载均衡分发到Kong集群的所有节点,那么总体的限流应该是:Kong节点数量x限流数量
1 | ab -n 100 -c 10 https://m.devapi.hsq.net/merchantapi |
1 | huangyong@huangyong-Macbook-Pro ~ curl https://m.devapi.hsq.net/merchantapi |
1 | 10.0.2.20 - - [19/Jan/2021:14:05:14 +0800] "GET /merchantapi HTTP/1.0" 429 41 "-" "ApacheBench/2.3" |
Kong是基于Nginx实现代理转发.官方的 nginx.conf
配置文件过于简单.如果需要优化nginx的性能,就需要修改默认的nginx配置文件,或者重新自定义一个nginx配置文件.
具体方法可以参考官方文档: https://docs.konghq.com/2.2.x/configuration/#environment-variables
下面介绍2种方式自定义nginx的配置
Kong服务启动时会每次都新建一个新的nginx配置文件.可以通过将nginx指令注入到 kong.conf
配置文件中从而配置到这个新的nginx配置文件
注入到Kong的环境变量一般包含下面2种前缀.前缀名不同代表注入的nginx指令作用在不同的作用域下.Kong会将环境变量的前缀去掉,然后将环境变量的后面部分注入到nginx.
nginx_http_
该前缀环境变量会被注入到Nginx的http代码块nginx_proxy_
该前缀会被注入到nginx的server代码块例如.如果注入以下环境变量到 kong.conf
配置文件:
1 | nginx_proxy_large_client_header_buffers=16 128k |
Kong会将以下环境变量注入到Nginx配置文件的代理 server
块中
1 | large_client_header_buffers 16 128k; |
下面的环境变量,会被注入到nginx的http块中
1 | export KONG_NGINX_HTTP_OUTPUT_BUFFERS="4 64k" |
还有一种前缀
Nginx_admin_
这个作用在kong的admin api,所以用的较少
对于一些复杂的配置场景,比如需要将整个server代码块添加到Nginx配置文件.可以使用上面的环境变量注入的方式,注入一个 include
指令到Nginx配置文件.
例如下面这个nginx的server代码块文件.假如该文件名为 my-server.conf
1 | # custom server |
可以通过下面的方式添加到 kong.conf
配置文件
1 | nginx_http_include = /path/to/your/my-server.conf |
或者通过环境变量方式注入
1 | export KONG_NGINX_HTTP_INCLUDE="/path/to/your/my-server.conf" |
这样当Kong启动后,server代码块会被添加到Nginx的配置文件.
这里也可以使用相对路径来注入一个server代码块的配置文件,但是配置文件需要在
kong.conf
配置文件的prefix路径之下.或者kong启动时候通过-p
参数自定义的prefix路径之下
kong在启动的时候会根据 /usr/local/share/lua/5.1/kong/templates/nginx.lua
和 /usr/local/share/lua/5.1/kong/templates/nginx_kong.lua
这2个lua模板来自动生成nginx的配置文件.当Kong启动后会自动在prefix路径下生成 nginx.conf
和 nginx-kong.conf
.前者是Nginx的主配置文件,然后通过include方式引入了 nginx_kong.conf
当kong启动后,会产生下面2个文件
1 | /usr/local/kong |
在 https://github.com/kong/kong/tree/master/kong/templates下也存放了kong的默认模板文件.
所以在 usr/local/kong
目录下直接修改 Nginx.conf
配置文件无法永久生效.当kong重启时,配置文件会被默认的Lua目标所覆盖和替代
如果一定要自定义nginx配置文件.可以自定义nginx的模板文件来替代 Nginx.lua
.然后在该模板文件里引入 nginx-kong.conf
nginx.conf
配置文件为 nginx.conf.template
1 | cp /usr/local/kong/nginx.conf nginx.conf.template |
nginx.conf.template
.例如下面是我的配置文件内容1 | pid pids/nginx.pid; |
注意该配置文件内的指令不能和
nginx-kong.conf
配置文件有同名或者冲突.否则kong无法启动
1 | kong start -c /etc/kong/kong.conf --nginx-conf nginx.conf.template |
如果是docker方式运行.可以使用 Dockerfile
自定义kong镜像
以下是Dockerfile文件内容
1 | FROM kong:2.2.0 |
编译docker镜像
1 | docker build -t dwd-kong:2.2.0 . |
重启运行docker.但是要先在kong容器运行 kong migrations up
和 kong migrations finish
命令.所以 docker-compose.yml
配置文件内容如下
1 | kong: |
启动容器后.可以查看配置文件是否生效:
1 | [work@docker docker-compose]$docker exec kong cat /usr/local/kong/nginx.conf |
如此,便实现了自定义kong的nginx配置文件,这在大并发场景中可能需要优化nginx的转发性能.如果是小规模场景中,可以使用Kong的默认的Nginx配置文件即可.
]]>公司目前Prometheus监控了IDC数据中心的主机,中间,站点等,同时也监控了阿里云线上的rabbitmq,mysql,kong(所有资源都是ECS自己搭建的,非阿里云的saas服务)
prometheus使用了第三方的钉钉监控插件(prometheus-webhook-dingtalk),github地址: https://github.com/timonwong/prometheus-webhook-dingtalk
Prometheus通过alertmanager将告警信息发送到钉钉机器人
我们需要将阿里云的线上中间件监控告警发送到阿里云的钉钉群.IDC资源的监控告警发送到IDC的钉钉群,不同的钉钉群面对的人群也不同.方便监控告警信息的分类和管理.
幸好,alertmanager天生支持告警路由的功能,将不同的告警信息发送给不同的receiver接收人
Prometheus本身并不提供告警功能,所有告警信息都是发送给Alertmanager处理.Alertmanager接收到告警信息后负责将它们分组,抑制,静默,然后路由到相关接收者.
分组功能将多个同一类型的告警合并一起后发送,这在某个服务发生故障从而影响其他几十,上百个相关依赖性的服务时非常有用,可以有效避免告警信息轰炸.例如当网络出现问题时,可能该网络下的数百个服务都出现访问故障,结果数以百计的告警被发送给Alertmanager.此时Alertmanager将同类型的服务合并到一起仅仅使用单条告警通知发送给接收者
抑止是指如果某个告警已经触发,那么抑止其他有关该服务的告警消息.
例如如果某个集群A不可达,已经触发了告警.那么其他B,C,D等集群和服务发出的A集群不可达的告警通知将被Alertmanager抑止.告警抑制机制可以防止数百上千的重复故障告警
Silence静默配置的作用类似于Zabbix中的Maintenance维护功能,可以配置一个时间区间和相关规则,符合该配置的事件将不会进行告警。比如明确凌晨会暂停服务,这个时候就可以提前设置好静默规则,减少不必要的告警骚扰。Prometheus的Silence规则只需要通过AlertManager的Web界面就可以完成,不需要配置文件
处理流程:
1. 接收到Alert,根据labels判断属于哪些Route(可存在多个Route,一个Route有多个Group,一个Group有多个Alert)。
2. 将Alert分配到Group中,没有则新建Group。
3. 新的Group等待group_wait指定的时间(等待时可能收到同一Group的Alert),根据resolve_timeout判断Alert是否解决,然后发送通知。
4. 已有的Group等待group_interval指定的时间,判断Alert是否解决,当上次发送通知到现在的间隔大于repeat_interval或者Group有更新时会发送通知。
Alertmanager可以通过命令行配置和yaml配置文件配置../alertmanager -h
可以打印出所有的命令行配置选项.这里主要介绍alertmanager.yaml
这个配置文件的相关配置
alertmanager.yaml
配置文件主要字段有如下几个:
1 | global: |
route
字段定义路由树的节点,以及子节点的相关配置.子节点可以从父节点继承所有配置参数.
每个告警进入到顶级配置的route.该route必须是一个默认路由,匹配所有Prometheus的告警规则.然后去遍历所有子路由.如果continue
设置为false
,在匹配到第一个子路由(routes)后就停止继续匹配,并且交给子路由的receiver发出告警.如果continue
设置为true
则继续与后续的其他子路由(routes)匹配.如果某条告警信息不匹配任何子路由,或者当前没有配置任何子路由,则交给默认的顶级route处理.
下面是一个route路由配置的案例
1 | route: |
该配置定义了一个或者多个告警消息接收器.Alertmanager并不会主动联系receiver,而是需要第三方webhook插件实现告警接收.我们这里使用的是钉钉告警插件.关于钉钉告警插件的配置在后文会有详细介绍.
有多少个子route,就对应多少个receiver.(当然也可以多个子route对应同一个receiver).receiver定义了告警消息接受地址
下面是receiver的配置,定义了2个reciever,对应上面的route.aliyun
的receiver用来接收rabbitmq,mysql的告警通知,defualt
用来接收其他所有的告警消息
1 |
|
在Alertmanager配置文件中,使用inhibit_rules
定义一组告警的抑制规则.当已经发送的告警通知匹配到target_match和target_match_re规则,当有新的告警规则如果满足source_match或者定义的匹配规则,并且以发送的告警与新产生的告警中equal定义的标签完全相同,则启动抑制机制,新的告警不会发送。
上面这段概念理解起来比较拗口,使用下面的配置作为一个案例解读:
1 | - source_match: |
当接收到一个lable名称为alertname
,值为NodeDown
的告警.并且为该告警发送了一个通知:
1 | {alertname="NodeDown",node="x.x.x.x",...} time annotation |
那么Alertmanager就会创建一条抑制规则:
1 | {node="x.x.x.x",serverity=~"middle|low"} |
如果新的告警满足severity=~”middle|low”,并且node标签相等(也就是equal的作用).那么该告警就会被抑制..例如该主机上的mysqldown的告警消息就不会被发送
1 | {alertname="MysqlDown",node="x.x.x.x",serverity="middle",...} time annotation |
这也是我们期望看到的,因为当我们收到了某个主机节点Down的告警通知,那么该主机上的所有服务不可用的告警消息不应该再次发送.
目前使用的是prometheus-webhook-dingtalk钉钉告警插件.在github上可以直接下载二进制文件运行.默认监听在8060端口.使用./prometheus-webhook-dingtalk -h
可以指定自定义配置文件和监听端口
该插件提供了一下路由供Alertmanager的webhook_configs使用
/dingtalk/<profile>/send
这里的profile需要在插件启动时-ding.profile
中指定相应的名称.为了支持多个receiver,同时往多个钉钉自定义机器人发送告警消息,该插件可以指定多个-ding.profile
参数,从而指定多个钉钉机器人的地址.例如下面的prometheus-webhook-dingtalk启动配置文件:
1 | [work@172 prometheus-webhook-dingtalk]$ systemctl cat prometheus-webhook-dingtalk |
上面定义了2个profile:aliyun
和default
对应了alertmanager.yaml
配置文件中的不同的receiver配置.需要注意的是不同的profile,它供alertmanager调用的地址也是不同的.比如:
1 | default的profile的地址为: http://localhost:8060/dingtalk/default/send |
经过测试下来,不同的监控对象告警信息发送到不同的钉钉群组,方便相关的团队和人员第一时间接收和处理
https://prometheus.io/docs/alerting/latest/configuration/ #alertmanager 官方介绍
https://www.kancloud.cn/pshizhsysu/prometheus/1803807 #Alertmanager介绍
]]>kafka提供了许多实用的脚本工具,存放在$KAFKA_HOME的bin目录下.其中与主题相关的就是kafka-topic.sh脚本.例如.下面创建一个分区数为4,副本为3的主题topic-demon1
2
3./kafka-topics.sh --zookeeper localhost:2181 --create --topic topic-demo --replication-factor 3 --partitions 4
Created topic "topic-demo".
--zoopkeer
指定kafka连接的zookeeper服务地址--topic
指定一个topic主题--replication-factor
指定副本因子数量--partition
指定分区数量--create
表示创建
下面命令展示了刚创建的主题信息1
2
3
4
5
6[hadoop@bi-dev152 bin]$ ./kafka-topics.sh --zookeeper localhost:2181 --describe --topic topic-demo
Topic:topic-demoPartitionCount:4ReplicationFactor:3Configs:
Topic: topic-demoPartition: 0Leader: 152Replicas: 152,153,154Isr: 152,153,154
Topic: topic-demoPartition: 1Leader: 153Replicas: 153,154,152Isr: 153,154,152
Topic: topic-demoPartition: 2Leader: 154Replicas: 154,152,153Isr: 154,152,153
Topic: topic-demoPartition: 3Leader: 152Replicas: 152,154,153Isr: 152,154,153
上面的命令结果表示 topic-demon
这个主题一共有4个分区,存放在3台Kafka broker服务器节点.3个broker均是ISR集合,没有OSR集合
在任意一台kafka集群内的节点服务器上执行上述命令,会得到完全相同的结果
kafka-console-consumer.sh
在任意一台kafka集群内的节点服务器上可以通过控制台创建一个 consumer
消费者.示例如下1
[hadoop@bi-dev154 bin]$ ./kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic topic-demo
--bootstrap-server
指定连接的kafka集群地址--topic
指定消费者订阅的主题
kafka-console-producer.sh
在任意一台kafka集群内的节点服务器上可以通过控制台创建一个 producer
消费者.示例如下1
[hadoop@bi-dev153 bin]$ ./kafka-console-producer.sh --broker-list localhost:9092 --topic topic-demo
--broker-list
指定连接的kafka集群地址--topic
指定发小时时的主题
在弹出的shell终端中,输入 hello world!
1
>hello,world!
回到 consumer
的shell终端界面,发现消费到了刚生产的消息:1
2[hadoop@bi-dev154 bin]$ ./kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic topic-demo
hello,world!
Kafka为分区引入了副本(Replica)机制.通过增加副本数量提升容灾能力.一个Topic主题可以有多个分区,一个分区又可以有多个副本.这多个副本中,只有一个是leader,而其他的都是follower副本。仅有leader副本可以对外提供服务。所以副本之间是一主多从的关系,而且每个副本中保存的相同的消息.(严格来说,同一时刻副本之间的消息并非能一定完全同步)
多个follower副本通常存放在和leader副本不同的broker中。通过这样的机制实现了高可用,当某台机器挂掉后,其他follower副本也能迅速”转正“,开始对外提供服务。
在kafka中,实现副本的目的就是冗余备份,且仅仅是冗余备份,所有的读写请求都是由leader副本进行处理的。follower副本仅有一个功能,那就是从leader副本拉取消息,尽量让自己跟leader副本的内容一致。
follower副本之所以不能对外提供服务,主要是为了保障数据一致性
下图是一个多副本架构图.
Kafka集群中有4个broker,某个主题中有3个分区,且副本因子(即副本个数)也为3,如此每个分区便有1个leader副本和2个follower副本。生产者和消费者只与leader副本进行交互,而follower副本只负责消息的同步,很多时候follower副本中的消息相对leader副本而言会有一定的滞后。
AR: 分区内的所有副本统称.
ISR: In-Sync Replicas.所有与Leader副本保持一定程度同步的副本(包括Leader副本).一起组成ISR
OSR: Out-of-Sync Replicas: 与leader副本同步滞后过多的副本(不包括leader副本),一起注册呢个OSR
AR = ISR + OSR.
正常情况下,所有的follower副本都应该与leader副本保持一定程度的同步,即AR = ISR,OSR集合为空
Leader副本负责维护和跟踪ISR集合中所有follower副本的滞后状态,当follower副本落后太多或者失效时,leader副本会把它从ISR集合中剔除,如果OSR的follower副本追上了leader副本,那会从OSR转移到ISR.
默认情况下,只有ISR集合中的follower副本才有资格被选举为新的Leader
HW(High Watermark): 俗称高水位.它标识了一个特点的消息偏移量(offset).消费者只能拉取这个offset之前的信息.
LEO(Log End Offset): 标识当前日志文件中下一条代写入消息的offset.
下面一张图能说明这两个概念
上面的图代表一个日志文件.这个日志文件中有 9 条消息,第一条消息的 offset(LogStartOffset)为0,最后一条消息的offset为8,offset为9的消息用虚线框表示,代表下一条待写入的消息。日志文件的HW为6,表示消费者只能拉取到offset在0至5之间的消息,而offset为6的消息对消费者而言是不可见的。
offset为9的位置即为当前日志文件的LEO,LEO的大小相当于当前日志分区中最后一条消息的offset值加1。分区ISR集合中的每个副本都会维护自身的LEO,而ISR集合中最小的LEO即为分区的HW,对消费者而言只能消费HW之前的消息。
为了让读者更好地理解ISR集合,以及HW和LEO之间的关系,下面通过一个简单的示例来进行相关的说明。如图1-5所示,假设某个分区的ISR集合中有3个副本,即一个leader副本和2个follower副本,此时分区的LEO和HW都为3。消息3和消息4从生产者发出之后会被先存入leader副本
在同步过程中,不同的 follower 副本的同步效率也不尽相同。如图 所示,在某一时刻follower1完全跟上了leader副本而follower2只同步了消息3,如此leader副本的LEO为5,follower1的LEO为5,follower2的LEO为4,那么当前分区的HW取最小值4,此时消费者可以消费到offset为0至3之间的消息。
如果所有的副本都成功写入了消息3和消息4,整个分区的HW和LEO都变为5,因此消费者可以消费到offset为4的消息了。
Kafka 的复制机制既不是完全的同步复制,也不是单纯的异步复制。事实上,同步复制要求所有能工作的 follower 副本都复制完,这条消息才会被确认为已成功提交,这种复制方式极大地影响了性能。而在异步复制方式下,follower副本异步地从leader副本中复制数据,数据只要被leader副本写入就被认为已经成功提交。在这种情况下,如果follower副本都还没有复制完而落后于leader副本,突然leader副本宕机,则会造成数据丢失。Kafka使用的这种ISR的方式则有效地权衡了数据可靠性和性能之间的关系。
]]>一个Kafka体系主要包括:
producer将消息发送到Broker,Broker负责将受到的消息存储到磁盘中,Consumer负责从Broker订阅并消费消息.
举个例子, 如果保留策略设置为2天,一条记录发布后两天内,可以随时被消费,两天过后这条记录会被抛弃并释放磁盘空间。
Topic: 就是数据主题,生产者将消息发送到特点的主题.消费者负责订阅主题并进行消费.
Partition: 一个Topic可以划分成多个partition(分区).但是一个分区只属于单个主题.很多时候也会将partition称为主题分区(Topic-Partition).同一个主题下的不同分区包含的消息也不同.分区在存储层面可以看做一个追加的日志(Log)文件.
一个主题的分区可以在不同的节点服务器上,所有的消息会均匀的分配到不同的分区中(也就是不同的节点服务器),这样可以提高磁盘IO和性能.在创建主题的时候可以设置分区数量,当然也可以在主题创建完成后去修改分区数量.通过增加分区的数量实现水平扩展.
好比是为公路运输,不同的起始点和目的地需要修不同高速公路(主题),高速公路上可以提供多条车道(分区),流量大的公路多修几条车道保证畅通,流量小的公路少修几条车道避免浪费。收费站好比消费者,车多的时候多开几个一起收费避免堵在路上,车少的时候开几个让汽车并道就好了
Kafka中的Topics总是多订阅者模式,一个topic可以拥有一个或者多个消费者来订阅它的数据。对于每一个topic, Kafka集群都会维持一个分区日志,如下所示:
每个partition分区都是有序切不可变的记录集.并且不断的追加到结构化的commit log文件.
Offset: 消息被存储到分区的日志文件时会分片一个偏移量(offset).offset是消息在分区中的唯一表示.kafka通过它来保障消息在分区内的顺序.
不过Offset并不跨越分区,也就是说Kafka保证的是分区有序,而不是主题有序.
在每一个消费者中唯一保存的元数据是offset(偏移量)即消费在log中的位置.偏移量由消费者所控制:通常在读取记录后,消费者会以线性的方式增加偏移量,但是实际上,由于这个位置由消费者控制,所以消费者可以采用任何顺序来消费记录。例如,一个消费者可以重置到一个旧的偏移量,从而重新处理过去的数据;也可以跳过最近的记录,从”现在”开始消费。
]]>在之前的笔记中提到了创建主题的一个简单示例.kafka提供 kafka-topics.sh
脚本来创建主题.下面这个示例创建了一个 topic-test
的主题,包含4个分区和2个副本.
1 | /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --create --topic topic-test --replication-factor 2 --partitions 4 |
分区创建完成后,会在kafka的 log.dirs
或者 log.dir
的目录下创建相应的主题分区.下面是在其中一台Broker节点的信息展示:
1 | [hadoop@bi-dev152 ~]$ ls /opt/logs/kafka/ | grep "topic-test" |
可以看到152节点中创建了2个文件夹 topic-test-0 和 topic-test-2,对应主题 topic-test的2个分区编号为0和2的分区,命名方式可以概括为 <topic>-<partition>
.严谨地说,其实这类文件夹对应的不是分区,分区同主题一样是一个逻辑的概念而没有物理上的存在.并且这里我们也只是看到了2个分区,而我们创建的是4个分区,其余2个分区被分配到了153和154节点中,参考如下:
1 | #153节点 |
三个broker节点一共创建了8个文件夹,这个数字8实质上是分区数4与副本因子2的乘积.每个副本(或者更确切地说应该是日志,副本与日志一一对应)才真正对应 了一个命名形式.
主题,分区,副本和日志的关系如下图所示.主题和分区是提供给上层用户的抽象,而在副本层面(或者更确切的说是Log日志层面)才会实际物理存在.
同一个分区中的多个副本必须分布在不同broker中,并且一个分区副本同时存在多个broker中,这样才能提供有效的数据冗余.上面的示例中,每个副本都分布在至少2台不同的broker中.
通过 kafka-topics.sh
脚本创建的主题会按照内部既定逻辑来分配分区和副本到Broker节点上.其实该脚本还提供一个 replica-assignment
参数来手动指定分区副本的分配方案.用法如下:
1 | 格式为: 分区1broker节点1:分区1broker节点2,分区2broker节点1:分区2broker节点2.副本集合用冒号隔开,分区之间用逗号隔开 |
例如下面这个实例通过手动方式创建了一个和 topic-test
一样分区副本分配的 topic-test-same
主题.
下面是刚创建的自动分配的topic-test主题
1 | [hadoop@bi-dev152 ~]$ /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --describe --topic "topic-test" |
通过 --replica-assignment
手动指定分区副本分配情况
1 | [hadoop@bi-dev152 ~]$ /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --create --topic topic-test-same --replica-assignment 153:152,154:153,152:154,153:154 |
–replica-assignment参数其实就是逗号隔开的所有分区的Replicas副本集合.副本集合内部用:冒号隔开
查看 topic-test-same
分区信息.和 topic-test
主题分区副本分配一致
1 | [hadoop@bi-dev152 ~]$ /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --describe --topic "topic-test-same" |
手动分配分区副本需要遵循以下原则,否则会报错:
在创建主题时,还可以通过 config
参数设置要创建主题的相关参数.可以覆盖原本的默认配置参数. config
可以指定多个参数.用法如下:
1 | --config 参数名=值 --config 参数名=值 ...... |
下面示例使用 config
参数创建主题 topic-config
.并且携带2个参数 :
1 | [hadoop@bi-dev152 ~]$ /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --create --topic topic-config --replication-factor 1 --partitions 1 --config cleanup.policy=compact --config max.message.bytes=10000 |
查看主题信息:
1 | [hadoop@bi-dev152 ~]$ /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --describe --topic topic-config |
通过zk也能查看到config信息,config信息保存在 /config/topics/TOPIC_NAME
目录下:
1 | [zk: localhost:2181(CONNECTED) 0] get /config/topics/topic-config |
创建主题时需要遵循几个原则
if-not-exists
参数可以避免出现报错信息,但是不会成功创建一个同名主题)kafka-topics.sh
创建主题信息支持以下参数:
--create
创建主题
--replica-assignment
手动创建主题的分区副本分配--config
手动指定参数kafka-topics.sh
脚本提供了 --describe
参数来查看一个topic的信息:
1 | [hadoop@bi-dev152 ~]$ /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --describe --topic "topic-test" |
在上面的示例中,命令行提供了以下几个信息:
一共有3个broker节点:152,153,154
PartitionCount
表示一共有3个分区
ReplicationFactor
副本因子为2
Leader
表示某个分区对应的leader副本在具体的Broker节点
Replicas
表示分区内所有AR副本的集合
Isr
表示ISR副本集合
如果 kafka-topics.sh
脚本没有指定具体的 --topic
字段.则会展示所有的topic主题:
1 | [hadoop@bi-dev152 ~]$ /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --describe | head |
zookeeper提供了 zkCli.sh
客户端.使用客户端连接zookeeper:
1 | [hadoop@bi-dev152 ~]$ /opt/zookeeper-3.4.10/bin/zkCli.sh -server localhost:2181 |
zookeeer的 /brokers/topics
目录下保存了主题的分区副本分片方案.通过查看这个目录即可查看主题的分区和副本信息:
1 | [zk: localhost:2181(CONNECTED) 2] get /brokers/topics/topic-test |
如上示例所示, "2":[152,154]
表示分区2分配了2个副本,分别在152和153这2个broker节点上.
--list
参数可以列出当前的所有topic
1 | [hadoop@bi-dev152 ~]$ /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --list |
kafka-topics.sh
脚本的 describe
参数还支持很多额外的指令,用于查看更详细的信息.
1.--topics-with-overrides
参数表示查看覆盖配置的主题,列出包含了与集群不一样配置的主题.下面列出了 topic-config
这个主题,这个主题使用了 --config
参数创建
1 | [hadoop@bi-dev152 ~]$ /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --describe --topics-with-overrides |
2.--under-replicated-paritions
参数列出包含失效副本的分区.失效副本的分区可能正在进行同步操作,也有可能同步发生异常.此时分区的ISR集合小于AR集合.失效副本的分区是重点监控对象,因为这可能意味着集群中的某个broker已经失效或者同步效率降低等.
正常情况下此命令不会出现任何信息.例如查看主题 topic-demo
的失效副本信息,但是没有任何输出信息
1 | [hadoop@bi-dev152 ~]$ /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --describe --topic topic-demo --under-replicated-partitions |
此时将153这个节点下线.再次查看:
1 | [hadoop@bi-dev152 ~]$ /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --describe --topic topic-demo --under-replicated-partitions |
可以看到Leader和ISR集合中都没有了153这个节点.将153节点上线.此时再次查询,恢复正常.
1 | [hadoop@bi-dev152 ~]$ /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --describe --topic topic-demo --under-replicated-partitions |
\3. unavailable-partitions
参数可以查看主题中没有leader副本的分区.这些分区已经处于离线状态,对于生产者或者消费者来说不可用.
同样正常情况下,该命令没有展示任何信息.
例如,下面的 topic-test
主题有4个分区,每个分区有2个副本.其中分区1和分区3的副本ISR是153和154这2个节点
1 | [hadoop@bi-dev152 ~]$ /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --describe --topic topic-test |
现在停掉153和154这2个节点的kafka进程.使用 unavailable-partitions
参数查看分区信息
1 | [hadoop@bi-dev152 ~]$ /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --describe --topic topic-test --unavailable-partitions |
leader显示为-1,表示没有可用leader
节点恢复后,再次执行该命令,没有任何显示
1 | [hadoop@bi-dev152 ~]$ /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --describe --topic topic-test --unavailable-partitions |
kafka-topics.sh
查看主题信息支持以下参数:
--describe
--topic TOPIC_NAME
展示具体某个topic主题的分区副本信息--topics-with-overrides
列出覆盖配置参数的主题--under-replicated-partitions
列出失效副本的主题分区信息--unavailable-partitions
列出没有副本的主题分区--list
列出kafka集群下的所有topic主题名称
当一个主题被修改后,依然允许我们对其做一定的修改,比如修改分区个数,修改配置等.这个功能就是 kafka-topic.sh
脚本中的 alter
指令提供的.
以 topic-config
主题为例,该主题下只有一个分区.将分区修改为3:
1 | [hadoop@bi-dev152 ~]$ /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --alter --topic topic-config --partitions 3 |
--partition
参数表示扩展后的分区个数.
注意告警信息.如果主题中的消息包含key(key不为Null)时,根据key计算分区的行为就会受到影响.当分区数为1时,所以key的消息都会发送到这个分区.当分区扩展到3,会根据消息的key来计算区号.原本发往分区0的消息可能会发送到分区1或者2.此外,还会影响既定消息的顺序.
对于基于key计算的主题,不建议修改分区数量.在一开始就设置好分区数量.另外需要注意的是,Kafka不支持减少分区.只能增加不能减少.
1 | hadoop@bi-dev152 ~]$ /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --alter --topic topic-config --partitions 1 |
不支持减少分区主要是考虑到保障kafka的消息可靠性和顺序性,事务性问题.
如果修改一个不存在的主题分区,则会报错.添加 --if-exists
参数会忽略一些异常
1 | hadoop@bi-dev152 ~]$ /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --alter --topic topic-none-exist --partitions 3 |
还可以使用 kafka-topics.sh
脚本的 alter
指令修改主题的配置.在创建主题的时候通过 config
参数来设置要创建的主题相关参数.在创建完主题之后,还可以通过 alter
和 config
配合增加或者修改一些配置文件覆盖原有的值
下面例子演示修改主题 topic-config
的 max.message.bytes
配置.从10000修改到20000
1 | [hadoop@bi-dev152 ~]$ kafka-topics.sh --zookeeper localhost:2181 --alter --topic topic-config --config max.message.bytes=20000 |
通过 alter
也可以删除创建主题时候的自定义配置.使用 --delete-config
参数.下面这个例子中删除了 max.message.bytes
配置.
1 | [hadoop@bi-dev152 ~]$ kafka-topics.sh --zookeeper localhost:2181 --alter --topic topic-config --delete-config max.message.bytes |
注意.在对config配置进行增删改查时候,都会提示建议使用kafka-configs.sh这个脚本来实现.该脚本的使用方式下面马上讲到
kafka-configs.sh
脚本专门用来对配置进行操作.可以在运行状态下动态更改配置.也可以查询主题的相关配置.而且该脚本不仅可以支持主题相关配置修改,还可以修改broker,用户和客户端这3个类型的配置
kafka-configs.sh
脚本使用 entity-type
参数指定操作配置的类型, entity-name
参数指定操作配置的名称.
下面这个例子查看主题 topic-config
的配置
1 | [hadoop@bi-dev152 ~]$ kafka-configs.sh --zookeeper localhost:2181 --describe --entity-type topics --entity-name topic-config |
--entity-type
指定查看的实体类型.支持以下几种类型:
--entity-name
配置的实体名称:
如果不指定 --entity-name
参数则会查询所有的 entity-type
对应的所有配置信息
1 | [hadoop@bi-dev152 ~]$ kafka-configs.sh --zookeeper localhost:2181 --describe --entity-type topics |
通过zookeeper也可以查询主题的配置信息.路径为 /config/topics/TOPIC_NAME
1 | [zk: localhost:2181(CONNECTED) 3] get /config/topics/topic-config |
使用 alter
对配置进行变更.需要配合 add-config
或者 delete-config
这2个参数一起使用.
add-config
参数实现配置的增,改
下面的例子中,为主题 topic-config
添加 max.message.bytes
参数配置和 cleanup.policy
参数配置
1 | [hadoop@bi-dev152 ~]$ kafka-configs.sh --zookeeper localhost:2181 --alter --entity-type topics --entity-name topic-config --add-config cleanup.policy=compact,max.message.bytes=20000 |
delete-config
参数可以实现配置删除.
下面的例子中,删除上面的2个配置
1 | [hadoop@bi-dev152 ~]$ kafka-configs.sh --zookeeper localhost:2181 --alter --entity-type topics --entity-name topic-config --delete-config cleanup.policy,max.message.bytes |
如果确定不再使用一个主题,那么最好的方式是将其删除.这样可以释放一些资源,比如磁盘,文件句柄等. kafka-topics.sh
脚本中的 delete
命令可以用来删除主题.比如下面删除主题 topic-demo1
1 | [hadoop@bi-dev152 ~]$ kafka-topics.sh --zookeeper localhost:2181 --delete --topic topic-demo1 |
注意.必须将kafka服务器配置文件的delete.topic.enable选项设置为true才能删除.这个参数的默认值是false.删除主题的操作会被忽略.主题并没有被删除
1 | [hadoop@bi-dev152 ~]$ kafka-topics.sh --zookeeper localhost:2181 --list | grep topic-demo1 |
编辑配置文件 /opt/kafka/config/server.properties
修改下面的参数为true
1 | # Switch to enable topic deletion or not, default value is false |
如果删除一个kafka的内部主题,那么会报错
1 | [hadoop@bi-dev152 ~]$ kafka-topics.sh --zookeeper localhost:2181 --delete --topic __consumer_offsets |
删除一个不存在的主题也会报错,此时可以通过 if-exists
参数来忽略异常.
下面这张图是 kafka-topics.sh
脚本的常用参数
消费者( Consumer)负责订阅Kafka中的主题( Topic),并且从订阅的主题上拉取消息.与其他一些消息中间件不同的是:在 Kafka的消费理念中还有一层消费组( Consumer Group)的概念,每个消费者都有一个对应的消费组。当消息发布到主题后,只会被投递给订阅它的每个消费组中的一个消费者 。
以下图为例,某个主题中共有 4 个分区( Partition) : PO、 Pl、 P2、 P3。 有两个消费组 A和 B 都订阅了这个主题,消费组 A 中有 4 个消费者 (CO、 Cl、 C2 和 C3),消费组 B 中有 2个消费者 CC4 和 CS) 。按照 Kafka默认的规则,最后的分配结果是消费组 A 中的每一个消费 者分配到1个分区,消费组 B 中的每一个消费者分配到 2个分区,两个消费组之间互不影响。每个消费者只能消费所分配到的分区中的消息。换言之 每一个分区只能被一个消费组中的一个消费者所消费.
假设目前某消费组内只有一个消费者 co,订阅了一个主题,这个主题包含 7 个分区: PO、 Pl、 P2、 P3、 P4、PS、 P6o 也就是说,这个消费者co订阅了7个分区,具体分配情形参考图3-2。
此时消费组内又加入了一个新的消费者 Cl,按照既定的逻辑,需要将原来消费者 co 的部分分区分配给消费者 Cl 消费 , 如图 3-3 所示 。 消费者 co 和 Cl 各自负责消费所分配到的分区 ,彼此之间并无逻辑上的干扰
消费者与消费组这种模型可以让整体的消费能力具备横向伸缩性,我们 可以增加(或减少) 消费者的个数来提高 (或降低〕整体的消费能力 。 对于分区数固定的情况, 一昧地增加消费者并不会让消费能力 一直得到提升,如果消费者过多,出现了消费者的个数大于分区个数的情况,**就会有消费者分配不到任何分区**。
对于消息中间件而言,一般有两种消息投递模式:点对点(P2P, Point-to-Point)模式和发**布/订阅**( Pub/Sub)模式.
点对点模式是基于队列的,消息生产者发送消息到队列,消息消费者从队列中接收消息。
发布订阅模式定义了如何向一个内容节点发布和订阅消息,这个内容节点称为主题(Topic),主题可以认为是消息传递的中介,消息发布者将消息发布到某个主题,而消息订阅者从主题中订阅消息.主题使得消息的订阅者和发布者互相保持独立,不需要进行接触即可保证消息的传递,发布/订阅模式在消息的一对多广播时采用.Kafka同时支持两种消息投递模式,而这正是得益于消费者与消费组模型的契合:
消费组是一个逻辑上的概念,它将旗下的消费者归为一类 ,每一个消费者只隶属于一个消费组。每一个消费组都会有一个固定的名称,消费者在进行消费前需要指定其所属消费组的名称,这个可以通过消费者客户端参数 group.id来配置,默认值为空宇符串。
消费者并非逻辑上的概念它是实际的应用实例它可以是一个线程,也可以是一个进程。同一个消费组内的消费者既可以部署在同一台机器上,也可以部署在不同的机器上。
]]>分区使用多副本机制来提升可靠性,但是只有leader副本对外提供读写服务.而follower副本只负责在内部进行消息的同步.如果一个分区的leader副本不可用,那么就意味着整个分区变得不可用.此时就需要从剩余的follower副本中挑选一个新的leader副本继续对外提供服务.
broker节点中的Leader副本个数决定了这个节点负载的高低
在创建主题的时候,主题的分区和副本会尽可能的均匀分布在kafka集群的各个broker节点.对应的Leader副本的分配也比较均匀.例如下面的 topic-demo
主题:
1 | [hadoop@bi-dev152 ~]$ kafka-topics.sh --zookeeper localhost:2181 --describe --topic topic-demo |
可以看到,leader副本均匀分布在所有的broker节点.另外,同一个分区,在同一台broker节点只能存在一个副本.所以leader副本所在的broker节点叫做分区的leader节点.而follower副本所在的broker节点叫做分区的follower节点.
可以想象的是,随着时间的推移,kafka集群中不可避免的出现节点宕机或者崩溃的情况.当分区的Leader节点发生故障时,其中一个follower节点就会成为新的Leader节点.这样导致集群中的节点之间负载不均衡,从而影响kafka整个集群的稳定性和健壮性.
即使原来的Leader节点恢复后,加入到集群时,也只能成为一个新的follower节点,而不会自动”抢班夺权”变成leader.
例如刚才的 topic-demo
分区重启152节点后,leader分布如下:
1 | [hadoop@bi-dev152 ~]$ kafka-topics.sh --zookeeper localhost:2181 --describe --topic topic-demo |
尽管kafka非常均匀的将leader副本分布在其他另外2个几点.但是此时152节点的负载几乎为零.
为了有效的治理负载失衡的情况,kafka引入了优先副本(preferred replica)的概念.所谓的优先副本就是在AR集合列表中的第一个副本为优先副本,理想情况下优先副本就是该分区的leader副本.所以也可以称之为 preferred leader
.
比如上面的例子中,分区0的AR集合(Replicas)是[152,153,154].那么分区0的优先副本就是152.Kafka会确保所有主题的优先副本均匀分布.这样就保证了所有分区的leader均衡分布.
所谓的优先副本选举是指通过一定的方式促使优先副本选举为Leader副本,促进集群的负载均衡.这一行为也称之为”分区平衡”.
kafka broker端(server.properties配置文件)有个 auto.leader.rebalance.enble
参数.默认为true.也就是分区自动平衡功能.Kafka会启动一个定时任务,轮询所有的broker节点,自动执行优先副本选举动作.
不过在生产环境中建议将该配置设置为 false
.因为kafka自动平衡分区可能在某些关键高分期时刻引起负面性能问题.也有可能引起客户端的阻塞.为了防止出现此类情况,建议针对副本不均衡的问题进行相应监控和告警,然后在合适的时间通过手动来执行分区平衡.
Kafka中的 kafka-preferred-replica-election.sh
脚本提供了对分区leader副本进行重新平衡的功能.优先副本选举过程是一个安全的过程,kafka客户端会自动感知leader副本的变更.
命令用法如下:
1 | bin/kafka-preferred-replica-election.sh --zookeeper localhost:2181 |
但是这样一来会对kafka集群的所有主题和分区都执行一遍优先副本的选举操作.如果集群中包含大量的分区,那么可能选举会失败,并且会对性能造成一定的应用.比较建议的是使用 path-to-json-file
参数来小批量的对部分指定的主题分区进行优先副本的选举操作.该参数指定一个JSON文件,这个JSON文件保存需要执行优先副本选举的分区清单.
举个例子,对上面的 topic-demo
分区进行优先副本选举操作.先创建一个JSON文件,文件名可以任意:
1 | { |
将上述内容保存为 election.json
文件.然后执行下列命令:
1 | [hadoop@bi-dev152 ~]$ kafka-preferred-replica-election.sh --zookeeper localhost:2181 --path-to-json-file ~/election.json |
提示优先副本选举成功.下列结果显示leader副本已经均衡分配到所有Broker节点了
1 | [hadoop@bi-dev152 ~]$ kafka-topics.sh --zookeeper localhost:2181 --describe --topic topic-demo |
在实际生产环境中,建议使用这种方式来分批的执行优先副本选举操作.杜绝直接粗暴的进行所有分区的优先副本选举.另外,这类操作也应该需要避开业务高峰期,以免对性能造成负面影响,或者出现意外故障.
当集群中一个Broker节点宕机,该节点的所有副本都处于丢失状态.kafka并不会自动将这些失效的分区副本自动迁移到集群其他broker节点.另外当集群中新增一台Broker节点时,只有新创建的主题分区才能被分配到这个节点上,而之前的主题分区并不会自动的加入到新节点(因为在创建时,并没有这个节点).这就导致新节点负载和原有节点负载之间严重不均衡.
为了解决这些问题,需要让分区副本再次进行合理的分配.也就是所谓的分区重分配.kafka提供了 kafka-reassign-paritions.sh
脚本执行分区重分配的工作.可以在集群节点失效或者扩容时使用.使用需要3个步骤:
要执行分区重分配,前提是broker节点清单数量要大于或者等于副本因子数量,否则会报错
Partitions reassignment failed due to replication factor: 3 larger than available brokers: 2
下面创建一个4分区,2个副本因子的主题 topic-reassign
举例.假定要将152这个broker节点下线.下线之前需要将该节点上的分区副本迁移出去.
1 | [hadoop@bi-dev152 ~]$ kafka-topics.sh --zookeeper localhost:2181 --create --topic topic-reassign --replication-factor 2 --partitions 4 |
第一步,创建一个JSON文件(文件名假定为reassign.json).文件内容是主题清单:
1 | { |
第二步,根据这个JSON文件和指定要分配的broker节点列表生成一份候选重分配方案:
1 | [hadoop@bi-dev152 ~]$ kafka-reassign-partitions.sh --zookeeper localhost:2181 --generate --topics-to-move-json-file ~/reassign.json --broker-list 153,154 |
在上面的例子中有以下几个参数:
--zookeeper
这个参数已经非常熟悉了
--generate
指令类型参数,类似于kafka-topics.sh脚本中的 --create
, --list
. --describe
等
--topics-to-move-json-file
指定主题清单文件路径
--broker-list
指定要分配的broker节点列表
上面的例子中打印了2个JSON格式内容:
Current partition replica assignment
表示目前的分区副本分配情况,在执行分区重分配前最好备份这个内容,以便后续回滚操作
Proposed partition reassignment configuration
表示候选重分配方案.这里只是一个方案,并没有真正执行.
将第二个Json内容格式化输出后,我们发现这个方案正如我们计划的那样,将该主题的所有分区下的AR副本集合分配到153和154节点,所有副本已经从即将要下线的152节点迁移走.
1 | { |
第三步,将 Proposed partition reassignment configuration
JSON文件内容保存在一个文件中(假定为project.json).然后执行具体的重分配的动作,命令如下:
1 | [hadoop@bi-dev152 ~]$ kafka-reassign-partitions.sh --zookeeper localhost:2181 --execute --reassignment-json-file project.json |
这里仍然打印了之前的副本分配方案,并且提示保存到JSON文件,以便回滚
这里使用了2个不同的命令参数:
--execute
指令类型参数,执行重分配动作--reassignment-json-file
指定重分配方案文件路径再次查看 topic-reassign
主题分区副本分配情况,所有的副本都从152迁移出去,此时该节点可以顺利下线
1 | [hadoop@bi-dev152 ~]$ kafka-topics.sh --zookeeper localhost:2181 --describe --topic topic-reassign |
当然,我们也可以直接编写第二个JSON文件来自定义重分配方案,这样就不需要执行上面的第一步和第二步操作了.
分区重分配的基本原理是为每个分区添加新副本(增加副本数量),新副本会从leader副本复制所有的数据.复制完成后,控制器将旧副本从副本清单里移除.(恢复成原来的副本数量).
所以,分区重分配需要确保有足够的空间,并且避免在业务高峰期操作
从刚才的主题分区结果可以看到,大部分的分区leader副本都集中在153这个broker节点.这样负载非常不均衡,我们可以继续借助 kafka-preferred-replica-election.sh
脚本执行一次优先副本选举.
1 | [hadoop@bi-dev152 ~]$ kafka-preferred-replica-election.sh --zookeeper localhost:2181 --path-to-json-file election.json |
和优先副本选举一样,分区重分配对集群的性能有很大的影响.需要占用额外的磁盘,网络IO等资源.在生产环境执行操作时应该分批次执行.
我们了解分区重分配的本质在于数据复制,先增加新副本,进行数据同步,然后删除旧副本.如果副本数据量太大必然会占用很多额外的资源,从而影响集群整体性能.kafka有限流机制,可以对副本之间的复制流量进行限制.
副本复制限流有2种实现方式:
kafka-config.sh
kafka-reassign-partitions.sh
前者的实现方式有点繁琐,这里介绍后者的使用方式.
kafka-reassign-partitions.sh
的实现方式非常简单,只需要一个throttle参数即可.例如上面的例子中副本都在153和154节点,现在继续使用分区重分配,让副本从153节点迁移到152节点.但是这次使用限流工具
首先,修改 project.json
文件,将153替换成152
1 | sed -i 's/153/152/g' project.json |
然后,执行分区重分配,这里使用 --throttle
参数,指定一个限流速度(单位是B/s)
1 | [hadoop@bi-dev152 ~]$ kafka-reassign-partitions.sh --zookeeper localhost:2181 --execute --reassignment-json-file project.json --throttle 10 |
上面的示例输出中提示以下3点信息:
--verify
参数来周期性的查看副本复制进度,直到分区重分配完成,也就是说需要显示的使用这种方式确保分区重分配完成后解除限流的设置接下来使用 --verify
参数查看复制进度.下面的示例显示复制已经完成,并且限流已被解除
1 | [hadoop@bi-dev152 ~]$ kafka-reassign-partitions.sh --zookeeper localhost:2181 --verify --reassignment-json-file project.json |
此时153的副本已经被移除
1 | [hadoop@bi-dev152 ~]$ kafka-topics --describe --topic topic-reassign |
上面的例子中分区重分配,将副本从一个broker节点中移除,此时kafka集群的broker节点数量只剩下2个.副本因子也只有2个.这里有个问题,此时153节点重启,或者新增broker节点后,如何将新增的broker节点加入进群,扩展副本数量呢?或者还有一种情况,当创建主题和分区后,想要修改副本因子呢?
kafka-reassign-parition.sh
脚本同样实现了修改副本因子的功能..仔细观察一下分区重分配案例中的 project.json
文件内容:
1 | [hadoop@bi-dev152 ~]$ cat project.json |
json文件中的副本集合(replicas)都是2个副本,我们可以很简单的添加一个副本.比如对于分区0而言,可以将153节点添加进去.(其他分区也是如此)
1 | { |
执行 kafka-reassign-partition.sh
脚本,执行命令的方法和参数和分区重分片几乎一致:
1 | [hadoop@bi-dev152 ~]$ kafka-reassign-partitions.sh --zookeeper localhost:2181 --execute --reassignment-json-file add.json |
查看副本分配情况.副本数量已经增加到了3个
1 | Topic:topic-reassign PartitionCount:4 ReplicationFactor:3 Configs: |
虽然副本因子增加到3个,但是Leader还是没有分配到新的153这个broker节点.此时可以通过优先副本选举重新分配
1 | [hadoop@bi-dev152 ~]$ kafka-preferred-replica-election.sh --zookeeper localhost:2181 --path-to-json-file election.json |
重点: 与修改分区数量不同,副本数还可以减少,修改方法和命令几乎一样,只需要编辑JSON配置文件即可.这里就不再演示
如何选择合适的分区数量是需要经常面对的问题,但是这个问题似乎并没有权威的标准答案,需要根据实际的业务场景,硬件资源,应用软件,负载等情况做具体考量.这一章节主要介绍与本问题相关的一些决策因素,以供参考
在生产环境中设定分区数量需要考虑性能因素.所以性能测试工具必不可少,kafka本身提供了用于生产者性能测试的 kafka-producer-pref-test.sh
脚本和用于消费者性能测试的 kafka-consumer-perf-test.sh
脚本
首先创建一个用于测试的分区为1,副本为1的 topic-1
的主题:
1 | Topic:topic-1 PartitionCount:1 ReplicationFactor:1 Configs: |
其次.我们往这个主题发送100万条消息,并且每条消息大小为1024B,生产者对应的acks参数为1:
1 | [hadoop@bi-dev152 ~]$ kafka-producer-perf-test.sh --topic topic-1 --num-records 1000000 --record-size 1024 --throughput -1 --producer-props bootstrap.servers=localhost:9092 acks=1 |
示例中使用了多个参数:
num-records
: 指定发送消息的总条数
record-size
: 设置每条消息的字节数
throughtput
: 限流控制,-1表示不限流,大于0表示限流值
producer-props
: 指定生产者的配置,可以同时指定多组配置
除此之外还有其他参数,比如 print-metrics
在测试完成之后,打印很多指标信息.有兴趣可以执行 --help
查看更多参数信息.
回过头再看看上面示例中的压测结果信息,以第一条和最后一条为例:
1 | 76666 records sent, 15333.2 records/sec (14.97 MB/sec), 1517.5 ms avg latency, 2061.0 max latency. |
records sent: 表示发送的消息综述
records/sec: 吞吐量,表示每秒发送的消息数量
MB/sec: 吞吐量,表示每秒发送的消息大小
avg latency: 表示消息处理的平均耗时
max latency: 表示消息处理的最大耗时
50th,95th,99th,99.th 表示50%,95%,99%,99.9%时消息处理耗时
消费者压测工具的脚本使用也比较简单,下面的简单实例演示了消费主题 topic-1
中的100万条消息.命令使用方法:
1 | [hadoop@bi-dev152 ~]$ kafka-consumer-perf-test.sh --topic topic-1 --messages 1000000 --broker-list localhost:9092 |
data.consumed.in.MB: 消费的消息总量,单位为MB
MB.sec: 按字节大小计算的消费吞吐量(单位:MB/s)
data.consumed.in.nMsg: 消费的消息消息总数
nMsg.sec: 按消息个数计算的吞独量(单位n/s)
可以创建多个分区,比如10,100,200,500等(副本数量都为1)来测试生产和消费的性能表现,
消息中间件的性能一般是指吞吐量(还包括延迟),吞吐量会受到硬件资源,消息大小,消息压缩,消息发送方式(同步,异步),副本因子等参数影响.分区数量越多不一定吞吐量越高,超过一定的临界值后,kafka的吞吐量会不升反降.
一味的增加分区数量并不能使吞吐量得到提升,并且分区的数量也不能一直增加,如果超过一定的临界值还会引起kafka进程的崩溃.
每次创建一个分区,都会消耗一个Linux系统的文件描述符.
通过kafka的pid编号,可以查看当前kafka进程占用的文件描述符数量:
1 | [hadoop@bi-dev152 ~]$ ls /proc/22858/fd/ | wc -l |
此时创建一个分区数量为400个的topic-demo4的主题.由于分区会平均创建在集群内的3个broker节点,所以需要统计一下152这个本地节点的分区数量.
1 | [hadoop@bi-dev152 ~]$ kafka-topics --describe --topic topic-demo4 | grep -Eo "Leader:\s[0-9]+" | sort | uniq -c |
可以看到152这个节点创建了134个分区.接下来看看系统文件描述符的数量.正好增加了134个文件描述符
1 | [hadoop@bi-dev152 ~]$ ls /proc/22858/fd/ | wc -l |
可以想见的是,一旦分区数量超过了操作系统规定的文件描述符上限,kafka进程就会崩溃
如何选择合适的分区数量,一个恰当的答案就是视具体情况而定.
从吞吐量方面考虑,增加合适的分区数量可以在一定程度上提升整体吞吐量,但是超过临界值之后吞吐量不升反降.在投入生产环境之前,应该对吞吐量进行相关的测试,以找到合适的分区数量
分区数量太多会影响系统可用性,当broker发生故障时,broker节点上的所有分区的leader副本不可用,此时如果有大量的分区要进行leader角色切换,这个切换的过程会耗费相当的时间,并且这个时间段内分区会变的不可用.并且分区数量太多不仅为增加日志清理的耗时,而且在被删除时也会消费更多时间.
一个好的建议是,创建主题之前对分区数量性能进行充分压测,在创建主题之后,还需要对其进行追踪,监控,调优.如果分区数量较少,还能通过增加分区数量,或者增加broker进行分区重分配等改进.
最后,一个通用的准则是,建议分区数量设定为集群中broker的倍数,例如集群中有3个broker节点,可以设定分区数为3,6,9等.
kafka-topics.sh
查看,创建主题分区,副本
kafka-configs.sh
修改主题配置文件
kafka_perferred-replica-elections.sh
优先副本选举
kafka-reassign-partitions.sh
分区重分配,副本复制限流,修改副本因子数量
kafka-producer-perf-test.sh
生产者分区数和吞吐量性能压测
kafka-consumer-perf-test.sh
消费者性能压测