新建集群的第一步就是要规划服务器、网络、操作系统等等, 下面就结合我平时的工作经验总结下相关的要求, 内容根据日常工作持续补充完善:
kubernetes 集群分为控制节点和数据节点, 它们对于配置的要求有所不同:
节点规模 | Master规格 |
---|---|
1~5个节点 | 4核 8Gi(不建议2核 4Gi) |
6~20个节点 | 4核 16Gi |
21~100个节点 | 8核 32Gi |
100~200个节点 | 16核 64Gi |
系统盘40+Gi,用于储存 etcd 信息及相关配置文件等
160*90%=144核
. 如果容忍度是20%, 那么最小选择5台32核VM, 并且高峰运行的负荷不要超过160*80%=128核
. 这样就算有一台VM出现故障, 剩余VM仍可以支持现有业务正常运行.CPU:Memory
比例. 对于使用内存比较多的应用, 例如Java类应用, 建议考虑使用1:8的机型virtual machine 32C 64G 200G系统盘 数据盘可选
eth0
等, 保证名称唯一Kubernetes没有提供适用于裸金属集群的网络负载均衡器实现, 也就是LoadBalancer
类型的Service. Kubernetes 附带的网络负载均衡器的实现都是调用各种 IaaS 平台(GCP、AWS、Azure ……)的胶水代码。 如果您没有在受支持的 IaaS 平台(GCP、AWS、Azure…)上运行,LoadBalancers 在创建时将一直保持在pending
状态。
裸金属集群的运维人员只剩下两个方式来将用户流量引入集群内: NodePort
和externalIPs
. 这两种在生产环境使用有很大的缺点, 这样, 裸金属集群也就成了 Kubernetes 生态中的第二类选择, 并不是首选.
MetalLB 的目的是实现一个网络负载均衡器来与标准的网络设备集成, 这样这些外部服务就能尽可能的正常工作了.
MetalLB 要求如下:
MetalLB 是作为 Kubernetes 中的一个组件, 提供了一个网络负载均衡器的实现. 简单来说, 在非公有云环境搭建的集群上, 不能使用公有云的负载均衡器, 它可以让你在集群中创建 LoadBalancer 类型的 Service.
为了提供这样的服务, 它具备两个功能: 地址分配、对外发布
在公有云的 Kubernetes 集群中, 你申请一个负载均衡器, 云平台会给你分配一个 IP 地址. 在裸金属集群中, MetalLB 来做地址分配.
MetalLB 不能凭空造 IP, 所以你需要提供供它使用的 IP 地址池. 在 Service 的创建和删除过程中, MetalLB 会对 Service 分配和回收 IP, 这些都是你配置的地址池中的 IP.
如何获取 MetalLB 的 IP 地址池取决于您的环境。 如果您在托管设施中运行裸机集群,您的托管服务提供商可能会提供 IP 地址供出租。 在这种情况下,您将租用例如 /26 的 IP 空间(64 个地址),并将该范围提供给 MetalLB 以用于集群服务。
同样, 如果你的集群是纯私有的, 可以提供一个没有暴露到网络中的相邻的 LAN 网段. 这种情况下, 你可以抽取私有地址空间中的一段 IP 地址, 分配给 MetalLB. 这种地址是免费的, 只要你把它提供给当前 LAN 内的集群服务, 它们就能正常工作.
或者你可以两者都用! MetalLB 可以让你定义多个地址池, 它很方便.
当 MetalLB 给 Service 分配一个对外使用的IP之后, 它需要让集群所在的网络知道这个 IP 的存在. MetalLB 使用标准的网络/路由协议来实现, 这要看具体的工作模式: ARP、NDP 或者 BGP.
在2层模式下, 集群中的机器使用标准地址发现协议把这些 IPs 通知本地网络. 从 LAN 的角度看, 这台服务器有多个 IP 地址. 这个模式的详细行为还有局限性下面会介绍.
在 BGP 模式下, 集群中的所有机器与临近的路由器建立 BGP 对等会话, 告诉他们如何路由 Service IPs. 得益于 BGP 的策略机制, 使用 BGP 能实现真正的多节点负载均衡和细粒度的流量控制. 下面会介绍更多操作和局限性方面的细节.
在2层工作模式下, 一个节点负责向本地网络发布服务. 从网络的角度看, 更像是这台服务器的网卡有多个 IP 地址. 在底层, MetalLB 会响应ARP请求(IPv4)和NDP请求(IPv6). 这个工作模式的最大的优势就是适应性强: 它能工作在任何以太网内, 没有特殊的硬件要求, 不需要花哨的路由器.
在2层模式下, 发给 Service IP 的流量都会到一个节点上. 在该节点上, kube-proxy
将流量分发到服务具体的 pods 上. L2 并没有实现负载均衡.
但是, 它实现了一个错误转移机制, 当领袖节点因为某些原因故障了, 另一个节点就会接管这些 IP 地址. 故障转移是自动的: 使用 hashicorp/memberlist 检测到节点发生故障, 同时新的节点会从故障节点上接管这些 IP.
L2模式有两个主要局限性: 单点的瓶颈、潜在的缓慢故障转移.
如上面说的, 在 L2 模式下, 选举产生的单一的领袖节点会接收所有的服务流量. 这意味着, 你服务的入口带宽受限与这个单一节点的带宽. 如果使用 ARP/NDP 引导流量, 这是一个基本限制.
当前的实现, 节点之间的故障转移依赖客户端之间的配合. 当故障发生时, MetalLB 会不经请求的发送出一些2层的数据包, 来通知其他客户端 Service IP 所对应的 MAC 地址已经更改.
大多数操作系统能正确处理这种数据包, 同时更新“邻居”的地址缓存. 这种情况下, 故障转移也就几秒钟. 但是, 还是有个别系统要么没有实现这种报文的处理, 要么实现了, 但是更新缓存很慢.
好在所有现代版本的操作系统都正确实现了 L2 故障转移, 比如 Windows、Mac、Linux. 所以, 出问题的仅仅是很老或者不常见的操作系统.
为了最大限度地减少计划内的故障转移对客户端的影响,应该让旧的领袖节点多运行几分钟, 以便它可以继续为旧客户端转发流量,直到它们的缓存刷新。
当一个计划之外的故障出现时, 在访问出错的客户端刷新它们的缓存之前, 这些 Service IPs 将不可达.
MetalLB 的2层模式和 keepalived 有很多相似之处. 所以, 如果你熟悉 keepalived, 我说的很多你应该很熟悉. 但是和它也有一些不同的地方需要说一下.
Keepalived 使用虚拟路由器冗余协议(VRRP). 为了选举领袖并监控该领导者何时离开, keepalived 的各实例之间不断地相互交换 VRRP 消息。
不一样的是, MetalLB 通过 memberlist 来知道什么时候集群中的节点不可达, 什么时候这个节点的 Service IPs 需要移动到别处.
Keepalived 和 MetalLB 从客户端的角度看起来是一样的: 当发生故障转移时,Service IP 地址从一台机器迁移到另一台机器,之后该机器就会有多个 IP 地址。
因为它不用 VRRP, MetalLB 并不会有这个协议本身的局限性. 比如: VRRP协议限制每个网络只能有255个负载均衡器实例, 但是 MetalLB 就没有这个限制. 只要你网络中有空闲IP, 你可以有很多负载均衡器实例.
还有, 配置上 MetalLB 比 Keepalived 少, 比如它不需要 Virtual Router IDs
.
另一方面,由于 MetalLB 依赖于 memberlist 来获取集群成员信息,它无法与第三方 VRRP 感知路由器和基础设施进行互操作。 这是MetalLB的定位: MetalLB 专门设计用于在 Kubernetes 集群内提供负载平衡和故障转移.
在 BGP 模式下,集群中的每个节点都会与您的网络路由器建立 BGP 对等会话,并使用该对等会话来通告外部集群服务的 IP.
假设您的路由器配置为支持多路径,这将实现真正的负载平衡: MetalLB 发布的路由彼此等效。这意味着路由器将使用所有下一跳,并在它们之间进行负载平衡.
数据包到达节点后,kube-proxy 负责流量路由的最后一跳,将数据包送到服务中的特定 pod.
负载均衡的确切行为取决于您的特定路由器型号和配置,但常见的行为是基于 packet-hash
方法在连接层面 per-connection
进行平衡。这是什么意思?
这个per-connection
意味着单个 TCP 或 UDP 会话的所有数据包将被定向到集群中的单个机器。流量传播只发生在不同的连接之间,而不是一个连接内的数据包. 这是一件好事,因为在多个集群节点上传播数据包会导致一些问题:
高性能路由器能够以一种无状态的方式在多个后端之间使用数据包哈希的方法分发数据包. 对于每一个数据包, 它们拥有一些属性, 并能用它作为 “种子” 来决定选择哪一个后端. 如果, 所有的属性都一样, 它们就会选择同一个后端.
具体使用哪种哈希方法取决于路由器的硬件和软件. 典型的方法是: 3-tuple
和 5-tuple
. 3-tuple 哈希法使用数据包中的协议、源IP、目的IP作为哈希键, 这意味着来自不同ip的数据包会进入同一个后端. 5-tuple 哈希法又在其中加入了源端口和目的端口, 所有来自相同客户端的不同连接将会在集群中均衡分布.
通常, 我们推荐加入尽可能多的属性来参与数据包哈希, 也就是说使用更多的属性是非常好的. 因为这样会更加接近理想的负载均衡状态, 每一个节点都会收到相同数量的数据包. 但是我们永远不会达到这种理想状态, 因为上述原因, 但是我们能做的就是尽可能的均匀的传播连接, 以防止出现主机热点.
# 一个连接(connection)由多个连续的数据包(packet)构成
# 比如:
connection 1 : source[ip:port] -packet N->...-packet 1-> target[ip:port]
connection 2 : source[ip:port] -packet N->...-packet 1-> target[ip:port]
使用 BGP 作为负载均衡机制可以让你使用标准的路由器硬件, 而不是定制的负载均衡器. 但是, 这也带来的一些缺点:
最大的缺点是基于 BGP 的负载平衡不能优雅地响应后端设置的地址更改. 也就是说, 当集群的一个节点下线了, 到你服务的所有成功的连接都会损坏(用户会看到报错:Connection reset by peer
)
基于 BGP 的路由器实现无状态负载均衡。 他们通过哈希数据包头中的一些字段并将该哈希用作可用后端数组的索引,将给定数据包分配给特定的下一跳。
问题是路由器中使用的哈希值通常不稳定,所以每当后端集的大小发生变化时(例如,当一个节点的 BGP 会话关闭时),现有的连接将被有效地随机重新哈希,这意味着大多数现有的 连接最终会突然被转发到不同的后端,一个不知道相关连接的后端。
结果是每当你的服务的 IP > Node
映射发生变化时, 你希望看到一次性干净的切换出现, 到该服务的大部分所有可用连接中断. 没有持续的丢包或者黑洞, 只是一次很干净的中断而已.
根据您的服务的用途,您可以采用几种缓解策略:
弹性ECMP
或 弹性LAG
. 使用这样的算法, 在后端集合发生变化的时候, 能有效的减少受影响的连接数量.MetalLB 提供了一个实验模式: 使用 FRR 作为 BGP 层的后端.
开启 FRR 模式之后, 会获得以下额外的特性:
相比与原生实现, FRR 模式有以下局限性:
BGPAdvertisement
的 RouterID
字段可以被覆盖,但它必须对所有的 advertisements 都相同(不能有不同的 advertisements 具有不同的 RouterID)。BGPAdvertisement
的 myAsn
字段可以被覆盖,但它必须对所有 advertisements 都相同(不能有不同的 advertisements 具有不同的 myAsn)ebgp-multihop
标志必须设置为 true
安装之前, 确保满足所有要求. 尤其是, 你要注意网络附加组件的兼容性
如果你在云平台环境运行 MetalLB, 你最好先看看云环境兼容性页面, 确保你选择的云平台可以和 MetalLB 一起正常工作(大多数情况下都不好用).
MetalLB 支持4种安装方式:
如果您在 IPVS 模式下使用 kube-proxy,则从 Kubernetes v1.14.2 开始,您必须启用严格的 ARP 模式。
请注意,如果您使用 kube-router 作为服务代理,则不需要这个,因为它默认启用严格的 ARP。
你可以在集群中修改 kube-proxy 的配置文件:
kubectl edit configmap -n kube-system kube-proxy
这样配置:
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"
ipvs:
strictARP: true
你也可以把这个配置片段加入到你的 kubeadm-config
中, 在主配置后面使用 ---
附加上它就行.
如果你想自动更改配置, 下面的 shell 脚本可以用:
# 查看将会发生什么样的配置变化, 如果存在不同则返回非零返回值
kubectl get configmap kube-proxy -n kube-system -o yaml | \
sed -e "s/strictARP: false/strictARP: true/" | \
kubectl diff -f - -n kube-system
# 实际应用变更, 仅在遇到错误的时候返回非零返回值
kubectl get configmap kube-proxy -n kube-system -o yaml | \
sed -e "s/strictARP: false/strictARP: true/" | \
kubectl apply -f - -n kube-system
使用下面的部署清单, 来安装MetalLB:
kubectl apply -f https://raw.githubusercontent.com/
metallb/metallb/v0.13.4/config/manifests/metallb-native.yaml
如果你想开启使用实验性的FRR模式, 使用下面的部署清单
kubectl apply -f https://raw.githubusercontent.com
/metallb/metallb/v0.13.4/config/manifests/metallb-frr.yaml
注意: 这些部署清单来自开发分支. 如果应用在生产环境, 强烈推荐你使用稳定的发布版本.
这样, 在 metallb-system
名称空间下就部署了 MetalLB. 主要组件是:
metallb-system/controller
, 这是集群级别的控制器, 负责处理 IP 分配.metallb-system/speaker
, 这个组件使用你选择的协议对外发送信息, 使你的 Service 可以被访问.该安装清单并不包含配置文件, 但是 MetalLB 组件仍会启动, 在你部署相关配置资源之前, 它保持空闲状态.
另外的三种安装方式(Kustomize, Helm, MetalLB Operator)请查看官方文档
通常来说, MetalLB 并不关心你集群用什么网络附件组件, 只要它能满足 Kubernetes 要求的标准就可以.
下面是一些网络附加组件与 MetalLB 一起测试的结果, 可以供你参考. 列表中没有的附加组件可能也可以正常工作, 只是我们没有测试.
网络附加组件 | 兼容性 | 备注 |
---|---|---|
Antrea | 可以 | 1.4和1.5版本测试通过 |
Calico | 大部分可以 | Calico也提供了使用BGP公布LoadBalancer IP的能力 详细 |
Canal | 可以 | |
Cilium | 可以 | |
Flannel | 可以 | |
Kube-ovn | 可以 | |
Kube-router | 大部分可以 | 已知问题 |
Weave Net | 大部分可以 | 已知问题 |
在配置之前, MetalLB一直保持空闲状态. 想要配置 MetalLB, 需要在 MetalLB 所在的名称空间(通常是 metallb-system
)部署很多和配置相关的资源.
当然, 如果你没有把 MetalLB 部署到 metallb-system
名称空间下, 你可能需要修改下面的配置清单.
LoadBalancer
类型服务定义可分配的IP地址为了能给 Services 分配 IPs, MetalLB 通过 IPAddressPool
自定义资源来定义.
通过 IPAddressPools
定义好 IPs 地址池之后, MetalLB 就会使用这些地址给 Services 分配 IP.
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: first-pool
namespace: metallb-system
spec:
addresses:
- 192.168.10.0/24
- 192.168.9.1-192.168.9.5
- fc00:f853:0ccd:e799::/124
可以同时定义多个 IPAddressPools
资源. 可以使用 CIDR 定义地址, 也可以使用范围区间定义, 也可以定义 IPv4 和 IPv6 地址用于分配.
一旦给 Service 分配IPs之后, 它们必须要公布出去. 具体的配置要根据你想使用的协议来定, 下面会一一介绍:
注意: 也可以同时将一个 service 同时通过 L2 和 BGP 对外公布.
配置2层模式最简单, 在多数情况下, 你不需要任何关于协议的配置, 只配置IP地址就行.
使用2层模式不需要将IP地址绑定到工作节点的网络接口上. 它直接响应本地网络上的ARP请求,将机器的MAC地址提供给客户端。
为了公布来自 IPAddressPool
的 IP, L2Advertisement
资源必须与 IPAddressPool
资源一起使用.
比如, 下面的例子配置了 MetalLB 可以分配从 192.168.1.240 到 192.168.1.250 之间的地址, 并配置它使用2层模式:
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: first-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.240-192.168.1.250
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: example
namespace: metallb-system
上面的例子, 在 L2Advertisement
中没有配置 IPAddressPool
选择器, 这样它能使用所有的 IP 地址.
所以, 为了只将一部分 IPAddressPool
s 使用 L2 对外公布, 我们就需要声明一下(或者, 使用标签选择器).
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: example
namespace: metallb-system
spec:
ipAddressPools:
- first-pool
需要告诉 MetalLB 如何与外部一个或多个 BGP 路由器建立会话.
所以, 需要给每一个 MetalLB 需要连接到路由器建一个 BGPPeer
资源.
为了能提供一个基础的 BGP 路由器配置和一个 IP 地址范围, 你需要定义4段信息.
比如给MetalLB分配的AS编号为64500, 并且将它连接到地址为 10.0.0.1
且AS编号为 64501 的路由器,您的配置将如下所示:
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: sample
namespace: metallb-system
spec:
myASN: 64500
peerASN: 64501
peerAddress: 10.0.0.1
提供一个IP地址池 IPAddressPool
像这样:
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: first-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.240-192.168.1.250
使用自定义资源BGPAdvertisement
来配置MetalLB使用BGP来公布IPs:
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
name: example
namespace: metallb-system
在BGPAdvertisement
的定义中, 如果没有配置IPAddressPool
选择器, 默认使用所有可用的IP地址池IPAddressPools
如果仅需要使用特定的IP地址池通过BGP进行地址公布, 则需要声明一个IP地址池列表ipAddressPools
(或者使用标签选择器):
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
name: example
namespace: metallb-system
spec:
ipAddressPools:
- first-pool
在实验的FRR模式下, BGP会话可以由BFD会话取代,从而能实现比单纯使用BGP更快的路径故障检测的能力.
想要开启BFD, 你必须定义个BFD配置BFDProfile
, 并且和BGP会话对等方配置BGPPeer
关联起来:
apiVersion: metallb.io/v1beta1
kind: BFDProfile
metadata:
name: testbfdprofile
namespace: metallb-system
spec:
receiveInterval: 380
transmitInterval: 270
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: peersample
namespace: metallb-system
spec:
myASN: 64512
peerASN: 64512
peerAddress: 172.30.0.3
bfdProfile: testbfdprofile
部署时 MetalLB 会安装配置验证的钩子程序 validatingwebhook
, 用来检查用户部署的自定义资源的有效性.
但是, 因为 MetalLB 的整体配置被分隔成很多分片, 不是所有的无效配置都能被钩子程序避免. 所以, 一旦无效的 MetalLB 配置资源被成功部署了, MetalLB 会忽略它, 并使用最新的有效配置.
未来的版本中, MetalLB 会把错误的配置暴露到 Kubernetes 资源中. 但是, 目前来说, 如果需要知道为什么配置没有生效, 就需要去查看一下控制器的日志.
关于地址分配、BGP、L2、calico等相关的高级配置, 请参考官方文档
当安装并配置完 MetalLB 之后, 为了对外暴露 Service, 非常简单, 将 Service 的 spec.type
配置为 LoadBalancer
, 剩下的就交给 MetalLB 就好了.
MetalLB 会给它控制的 Service 添加一些事件, 如果你的 LoadBalancer
类型的 service 表现的不符合预期, 可以执行kubectl describe service <service name>
查看事件日志.
MetalLB 尊重spec.loadBalancerIP
参数, 所以, 如果你想要使用一个特定的IP地址, 你可以通过配置这个参数来指定. 如果MetalLB没有你申请的IP地址, 或者如果你申请的IP地址已经被其他的服务占用了, 地址分配就会失败, MetalLB会记录一个Warning级别的事件, 你通过kubectl describe service <service name>
就能看到.
MetalLB不仅支持spec.loadBalancerIP
参数, 还支持一个自定义 annotation 参数: metallb.universe.tf/loadBalancerIPs
. 对于有些双栈的 Service 需要多个IPs, 这个 annotation 也支持使用逗号分隔指定多个IPs
请注意: 在 kubernetes 的API中, spec.LoadBalancerIP
参数未来计划会被废弃. 请看这
如果你想使用特定的类型的IP, 但是不在乎具体是什么地址, MetalLB同样也支持请求一个特定的IP地址池. 为能能使用特定的地址池, 你需要在 Service 中添加一个 annotation: metallb.universe.tf/address-pool
, 来指定IP地址池的名称. 比如:
apiVersion: v1
kind: Service
metadata:
name: nginx
annotations:
metallb.universe.tf/address-pool: production-public-ips
spec:
ports:
- port: 80
targetPort: 80
selector:
app: nginx
type: LoadBalancer
MetalLB 理解并尊重服务的 externalTrafficPolicy
选项,并根据您选择的策略和公告协议实现不同的公告模式。
当使用2层模式公布的时候, 集群中的一个节点会接收给 Service IP 的流量. 从那开始, 行为就取决于选择的流量策略.
Cluster
流量策略使用默认的Cluster
流量策略, 节点上的kube-proxy
接收流量并负载均衡, 并且将流量分发给 Service 对应的 Pod.
这种策略下 pods 之间的流量是均匀分布的. 但是kube-proxy
在进行负载均衡的时候会隐藏真实的源IP地址, 因此, 在 pod 的日志中会看到外部流量来自 MetalLB 的领袖节点.
Local
流量策略使用Local
流量策略, 节点上的kube-proxy
接收流量, 同时将流量发送给当前节点的 pod. 因为 kube-proxy 不会跨集群节点分发流量, 你的 pod 可以看到真实的源IP地址.
这个策略的缺点是, 流量仅能流向 Service 对应的某些 pod. 那些不在领袖节点上的 Pod 是无法接收到任何流量的, 他们可以暂时当作副本存在, 当 MetalLB 发生故障转移的时候他们或许可以接收流量.
当通过 BGP 发布时, MetalLB尊重Service的externalTrafficPolicy
选项, 按照用户选择的策略实现了两种不同的发布模式.
Cluster
流量策略使用默认的Cluster
流量策略, 集群中的每个节点都会接收服务 IP 的流量。 在每个节点上,流量都经过第二次负载均衡(由 kube-proxy 提供),它将流量引导到各个 pod。
此策略会在集群中的所有节点以及服务中的所有 Pod 之间实现统一的流量分布。 但是,因为存在两次负载均衡(一次在 BGP 路由器上,一次在节点上的 kube-proxy
上),这会导致流量低效。 例如,特定用户的连接可能由 BGP 路由器发送到节点 A,但随后节点 A 决定将该连接发送到运行在节点 B 上的 pod。
Cluster
策略的另一个缺点是 kube-proxy
在进行负载平衡时会隐藏连接的源 IP 地址,因此在 pod 日志中会看到外部流量来自集群的节点。
Local
流量策略使用Local
流量策略,只有在本地运行了一个或多个 Services 的 Pod 时, 该节点才会吸引流量。 同时 BGP 路由器仅在托管服务的那些节点之间, 对传入流量进行负载平衡。在每个节点上,流量仅通过 kube-proxy
转发到本地 Pod,节点之间没有“水平”流量。
此策略为你的服务提供最有效的流量。此外,由于 kube-proxy
不需要在集群节点之间发送流量,因此您的 pod 可以看到传入连接的真实源 IP 地址。
该策略的缺点是: 节点作为负载均衡的一个单元,它不管该节点上运行了多少服务的 pod。所以, 这可能会导致你的 pod 流量不平衡。
比如,如果你有一个服务, 它在节点 A 上运行 2 个 pod,在节点 B 上运行1个 pod,则Local
流量策略会将到该服务的流量平分到这两个节点(A&B节点各50%)。在节点A上, 又会将到达A的流量平分给2个 pod, 因此节点A上的每个 pod 的负载分配为 25%,节点B的 pod 为 50%。相反,如果您使用Cluster
流量策略,每个 pod 将接收到总流量的 33%。
一般来说,在使用 Local
流量策略时,建议对 Pod 在节点上的调度进行精细控制,例如使用节点反亲和性,从而实现 Pod 之间的流量均匀.
将来,MetalLB 或许会解决这种流量策略的缺点,那时, 它无疑会成为 BGP 模式一起使用的最佳模式。
在 L2 模式下同时支持 IPv6 和双协议栈Services,但在 BGP 模式下仅通过实验性 FRR 模式来提供支持.
为了让 MetalLB 将 IP 分配给双栈服务,必须至少有一个IP地址池同时具有 v4 和 v6 版本的地址。
请注意,在双协议栈Services的情况下,不能使用spec.loadBalancerIP
,因为它不允许请求多个IP,因此必须使用注解 metallb.universe.tf/loadBalancerIPs
。
默认, Services 之间不能共享IP地址. 如果你希望多个Service使用一个IP地址. 你可以在 service 上配置 annotation metallb.universe.tf/allow-shared-ip
来开启优选择的IP地址共享.
这个 annotation 的值是一个共享 key. 下面几种情况下, Services 可以共享IP:
Cluster
外部流量策略, 或者它们都指向相同的一组 pods(比如 pod 选择器是完全相同的)如果这些条件不满足, MetalLB可能会给两个 service 分配同一个IP, 但是也不一定. 如果你想确保多个 service 共享一个特定的IP, 使用上面提到的spec.loadBalancerIP
来定义.
以这样的方式来管理 Service 有两个主要原因: 1. 规避 Kubernetes 的限制; 2. 可使用的IP地址有限.
下面是两个 services 共享一个IP地址的例子:
apiVersion: v1
kind: Service
metadata:
name: dns-service-tcp
namespace: default
annotations:
metallb.universe.tf/allow-shared-ip: "key-to-share-1.2.3.4"
spec:
type: LoadBalancer
loadBalancerIP: 1.2.3.4
ports:
- name: dnstcp
protocol: TCP
port: 53
targetPort: 53
selector:
app: dns
---
apiVersion: v1
kind: Service
metadata:
name: dns-service-udp
namespace: default
annotations:
metallb.universe.tf/allow-shared-ip: "key-to-share-1.2.3.4"
spec:
type: LoadBalancer
loadBalancerIP: 1.2.3.4
ports:
- name: dnsudp
protocol: UDP
port: 53
targetPort: 53
selector:
app: dns
目前 kubernetes 不支持多协议的 LoadBalancer Service. 通常, 像DNS这样的服务, 会同时监听TCP和UDP. 为了规避这个限制. 创建两个 Service(一个使用TCP, 一个使用UDP), 它们使用同样的pod选择器. 然后给他们配置相同的共享Key和spec.loadBalancerIP
, 这样就可以在同一个IP地址上同时使用TCP和UDP.
第二个原因很简单, 如果你的Service数量比IP地址数量多, 并且也搞不来更多的IP地址. 那么只能共享IP地址了.
假如, 一个电子商务平台由一个生产环境和很多沙箱环境. 生产环境需要公网IP地址, 但是沙箱环境使用私有的IP地址, 开发者通过VPN可以访问沙箱环境.
另外, 生产的IP已经在很多地方写死了(比如, DNS、安全扫描等), 所以在生产环境中, 我们希望特定的服务使用特定的IP地址. 因为沙箱环境是由开发者开启和关闭的, 所以我们不想手动管理.
我们可以使用 MetalLB 来满足上面的要求, 我们定义两个IP地址池, 通过定义BGP属性来控制每一个IP地址池的可见性.
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: production
namespace: metallb-system
spec:
# 生产使用,因为公网的IP很贵,我们只有4个
addresses:
- 42.176.25.64/30
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: sandbox
namespace: metallb-system
spec:
addresses:
# 相反, 沙箱环境使用私有IP空间
# 免费的而且很多, 所以我们给这个地址池分配了大量的IP
# 这样开发者可以根据需要启动很多沙箱环境
- 192.168.144.0/20
然后我们需要公布他们, 可以通过设置BGP的一些属性来控制每一个地址集合的可见性.
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
name: external
namespace: metallb-system
spec:
ipAddressPools:
- production
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
name: local
namespace: metallb-system
spec:
ipAddressPools:
- sandbox
communities:
- vpn-only
# 我们数据中心路由器知道“vpn-only”的BGP社区。
# 带有此社区标签的公告将仅通过公司VPN隧道传播回开发人员办公室。
apiVersion: metallb.io/v1beta1
kind: Community
metadata:
name: communities
namespace: metallb-system
spec:
communities:
- name: vpn-only
value: 1234:1
在我们沙箱环境的 Helm 定义 charts 中, 我们给每一个 service 都打上了annotation metallb.universe.tf/address-pool: sandbox
. 这样, 不管开发者什么时候创建沙箱环境, 都会从192.168.144.0/20
获得一个IP地址.
对于生产环境, 我们使用spec.loadBalancerIP
参数来给 service 定义特定的IP地址.
Copyright © The MetalLB Contributors.
Copyright © 2021 The Linux Foundation ®.
All rights reserved. Linux 基金会已注册商标并使用商标.
每个人都听过容器,但它究竟是什么?
支持这项技术的软件有很多,其中 Docker 最为流行。因为它的可移植性和环境隔离的能力,它在数据中心内部特别流行。为了能理解这个技术,需要理解很多方面。
注意:很多人拿容器和虚拟机比较,他们有不同的设计目标,不是替代关系,重叠度很小。容器旨在成为一个轻量级环境,您可以裸机上启动容器,托管一个或几个独立的应用程序。当您想要托管整个操作系统或生态系统或者可能运行与底层环境不兼容的应用程序时,您应该选择虚拟机。
说实话,零信任环境下有些软件的确需要被控制或被限制 - 至少为了稳定,或是为了安全。很多时候一个Bug或不良代码可能会摧毁整个机器并削弱整个生态系统。还好,有办法来控制这些应用程序,控制组(cgroups)是内核的一个特性,它能限制/计量/隔离一个或者多个进程使用CPU、内存、磁盘I/O和网络。
cgroup技术最开始是Google开发,最终在2.6.24版本(2008年1月)的内核中出现。3.15和3.16版本内核将合并进重新设计的cgroups,它添加了kernfs(拆分一些sysfs逻辑)。
cgroups的主要设计目标是提供一个统一的接口,它可以管理进程或者整个操作系统级别的虚拟化,包含Linux容器,或者LXC。cgroups主要提供了以下能力:
一个 cgroup 可以由一个或多个进程组成,这些进程都绑定到同一组限制。这些组也可以是分层的,这意味着子组继承了对其父组管理的限制。
Linux内核为cgroups提供了一系列控制器或者子系统,控制器负责给一个或者一组进程分配指定的系统资源。比如,memory
控制器限制内存使用,cpuacct
控制器限制cpu使用。
您可以直接或间接访问和管理 cgroup(使用 LXC、libvirt 或 Docker),首先,我在这里通过 sysfs 和 libcgroups
库介绍。下面的例子中,需要安装必要的软件包。在Red Hat Enterprise Linux或者CentOS上,执行下面命令:
sudo yum install libcgroup libcgroup-tools
在Ubuntu或Debian上这样安装:
sudo apt-get install libcgroup1 cgroup-tools
这个例子中,我用一个简单的脚本(test.sh),里面会执行一个无限循环。
$ cat test.sh
#!/bin/sh
while [ 1 ]; do
echo "hello world"
sleep 60
done
需要的软件包安装完毕之后,您可以通过 sysfs 层次结构直接配置您的 cgroup。比如,要在memory
子系统下创建一个名为 foo
的 cgroup,请在 /sys/fs/cgroup/memory
中创建一个名为 foo 的目录:
sudo mkdir /sys/fs/cgroup/memory/foo
默认情况下,每个新创建的 cgroup 都将继承对系统整个内存池的访问权限。但是,对于那些不断分配内存却不释放的应用来说,这样并不好。要将应用程序限制在合理的范围内,您需要更新 memory.limit_in_bytes 文件。
$ echo 50000000 | sudo tee
↪/sys/fs/cgroup/memory/foo/memory.limit_in_bytes
验证配置:
$ sudo cat memory.limit_in_bytes
50003968
注意,读到的值通常是内核页大小的倍数(page size, 4096bytes 或 4KB)。
执行应用程序:
$ sh ~/test.sh &
使用该进程PID,将其添加到memory
控制器管理下,
$ echo 2845 > /sys/fs/cgroup/memory/foo/cgroup.procs
使用相同的 PID 号,列出正在运行的进程,并验证它是否在期望的 cgroup 中运行:
$ ps -o cgroup 2845
CGROUP
8:memory:/foo,1:name=systemd:/user.slice/user-0.slice/
↪session-4.scope
您还可以通过读取指定的文件来监控该 cgroup 当前使用的资源。在这个例子中,你可能想看一下当前进程(以及派生的子进程)的内存使用量。
$ cat /sys/fs/cgroup/memory/foo/memory.usage_in_bytes
253952
还是上面的例子,我们将cgroup/foo
内存限制调整为 500 bytes。
$ echo 500 | sudo tee /sys/fs/cgroup/memory/foo/
↪memory.limit_in_bytes
注意:如果一个任务超出了其定义的限制,内核将进行干预,在某些情况下,会终止该任务。
同样,再读这个值,因为它要是内核页大小的倍数。所以尽管你配置的是500字节,但实际上设置的是4KB。
$ cat /sys/fs/cgroup/memory/foo/memory.limit_in_bytes
4096
启动应用,将其移动到cgroup中,并监控系统日志。
$ sudo tail -f /var/log/messages
Oct 14 10:22:40 localhost kernel: sh invoked oom-killer:
↪gfp_mask=0xd0, order=0, oom_score_adj=0
Oct 14 10:22:40 localhost kernel: sh cpuset=/ mems_allowed=0
Oct 14 10:22:40 localhost kernel: CPU: 0 PID: 2687 Comm:
↪sh Tainted: G
OE ------------ 3.10.0-327.36.3.el7.x86_64 #1
Oct 14 10:22:40 localhost kernel: Hardware name: innotek GmbH
VirtualBox/VirtualBox, BIOS VirtualBox 12/01/2006
Oct 14 10:22:40 localhost kernel: ffff880036ea5c00
↪0000000093314010 ffff88000002bcd0 ffffffff81636431
Oct 14 10:22:40 localhost kernel: ffff88000002bd60
↪ffffffff816313cc 01018800000000d0 ffff88000002bd68
Oct 14 10:22:40 localhost kernel: ffffffffbc35e040
↪fffeefff00000000 0000000000000001 ffff880036ea6103
Oct 14 10:22:40 localhost kernel: Call Trace:
Oct 14 10:22:40 localhost kernel: [<ffffffff81636431>]
↪dump_stack+0x19/0x1b
Oct 14 10:22:40 localhost kernel: [<ffffffff816313cc>]
↪dump_header+0x8e/0x214
Oct 14 10:22:40 localhost kernel: [<ffffffff8116d21e>]
↪oom_kill_process+0x24e/0x3b0
Oct 14 10:22:40 localhost kernel: [<ffffffff81088e4e>] ?
↪has_capability_noaudit+0x1e/0x30
Oct 14 10:22:40 localhost kernel: [<ffffffff811d4285>]
↪mem_cgroup_oom_synchronize+0x575/0x5a0
Oct 14 10:22:40 localhost kernel: [<ffffffff811d3650>] ?
↪mem_cgroup_charge_common+0xc0/0xc0
Oct 14 10:22:40 localhost kernel: [<ffffffff8116da94>]
↪pagefault_out_of_memory+0x14/0x90
Oct 14 10:22:40 localhost kernel: [<ffffffff8162f815>]
↪mm_fault_error+0x68/0x12b
Oct 14 10:22:40 localhost kernel: [<ffffffff816422d2>]
↪__do_page_fault+0x3e2/0x450
Oct 14 10:22:40 localhost kernel: [<ffffffff81642363>]
↪do_page_fault+0x23/0x80
Oct 14 10:22:40 localhost kernel: [<ffffffff8163e648>]
↪page_fault+0x28/0x30
Oct 14 10:22:40 localhost kernel: Task in /foo killed as
↪a result of limit of /foo
Oct 14 10:22:40 localhost kernel: memory: usage 4kB, limit
↪4kB, failcnt 8
Oct 14 10:22:40 localhost kernel: memory+swap: usage 4kB,
↪limit 9007199254740991kB, failcnt 0
Oct 14 10:22:40 localhost kernel: kmem: usage 0kB, limit
↪9007199254740991kB, failcnt 0
Oct 14 10:22:40 localhost kernel: Memory cgroup stats for /foo:
↪cache:0KB rss:4KB rss_huge:0KB mapped_file:0KB swap:0KB
↪inactive_anon:0KB active_anon:0KB inactive_file:0KB
↪active_file:0KB unevictable:0KB
Oct 14 10:22:40 localhost kernel: [ pid ] uid tgid total_vm
↪rss nr_ptes swapents oom_score_adj name
Oct 14 10:22:40 localhost kernel: [ 2687] 0 2687 28281
↪347 12 0 0 sh
Oct 14 10:22:40 localhost kernel: [ 2702] 0 2702 28281
↪50 7 0 0 sh
Oct 14 10:22:40 localhost kernel: Memory cgroup out of memory:
↪Kill process 2687 (sh) score 0 or sacrifice child
Oct 14 10:22:40 localhost kernel: Killed process 2702 (sh)
↪total-vm:113124kB, anon-rss:200kB, file-rss:0kB
Oct 14 10:22:41 localhost kernel: sh invoked oom-killer:
↪gfp_mask=0xd0, order=0, oom_score_adj=0
[ ... ]
注意,一旦应用程序使用内存达到 4KB 限制,内核的 Out-Of-Memory Killer(或 oom-killer)就会介入。它杀死了应用程序。您可以下面的方式来验证这一点:
$ ps -o cgroup 2687
CGROUP
libcgroup
软件包提供了简单的管理工具,上面很多操作步骤都可以用它实现。例如,使用cgcreate命令可以创建sysfs条目和文件。
在memory
子系统下创建名字为foo的组,使用下面命令:
$ sudo cgcreate -g memory:foo
注意:libcgroup 提供了一种用于管理控制组中的任务的机制。
使用与之前相同的方法,设置阈值:
$ echo 50000000 | sudo tee
↪/sys/fs/cgroup/memory/foo/memory.limit_in_bytes
验证配置:
$ sudo cat memory.limit_in_bytes
50003968
使用 cgexec 命令在 cgroup/foo
下运行应用程序:
$ sudo cgexec -g memory:foo ~/test.sh
使用它的 PID,验证应用程序是否在 cgroup 和定义的memory
管理器下运行:
$ ps -o cgroup 2945
CGROUP
6:memory:/foo,1:name=systemd:/user.slice/user-0.slice/
↪session-1.scope
如果您的应用程序不再运行,并且您想要清理并删除 cgroup,您可以使用 cgdelete。要从memory
控制器下删除组 foo,请键入:
$ sudo cgdelete memory:foo
通过简单的配置文件来启动服务,也可以完成上面的工作。你可以在/etc/cgconfig.conf
文件中定义所有cgroup名字和属性。下面的例子中配置了foo组和它的一些属性。
$ cat /etc/cgconfig.conf
#
# Copyright IBM Corporation. 2007
#
# Authors: Balbir Singh <balbir@linux.vnet.ibm.com>
# This program is free software; you can redistribute it
# and/or modify it under the terms of version 2.1 of the GNU
# Lesser General Public License as published by the Free
# Software Foundation.
#
# This program is distributed in the hope that it would be
# useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
# PURPOSE.
#
# 默认,我们希望 systemd 默认加载所有内容
# 所以没啥可做的
# 详细内容查看 man cgconfig.conf
# 了解如何在系统启动时使用该文件创建 cgroup
group foo {
cpu {
cpu.shares = 100;
}
memory {
memory.limit_in_bytes = 5000000;
}
}
cpu.shares
定义了cgroup的CPU优先级。默认,所有的组继承 1024 shares 或者说 100% CPU使用时间。降低该值,比如 100,该组将被限制在大约 10% CPU使用时间。
如前所述,cgroup 中的进程也可以被限制使用CPUs(core)数量,把下面的内容添加到cgconfig.conf文件相应的cgroup下:
cpuset {
cpuset.cpus="0-5";
}
它将限制该cgroup使用索引为0到5的核心(core),即仅能使用前6个CPU核心。
下面,需要使用cgconfig
服务加载该配置文件。首先,配置cgconfig
开机自启动加载上面的配置文件。
$ sudo systemctl enable cgconfig
Create symlink from /etc/systemd/system/sysinit.target.wants/
↪cgconfig.service
to /usr/lib/systemd/system/cgconfig.service.
现在,手动启动服务加载配置文件(或者直接重启操作系统)
$ sudo systemctl start cgconfig
在cgroup/foo
下,启动应用,并将其和它的memory
、cpuset
和cpu
限制进行绑定:
$ sudo cgexec -g memory,cpu,cpuset:foo ~/test.sh &
除了将应用启动到预定义的cgroup中之外,剩下的操作系统重启后会一直存在。但是,你可以通过写一个依赖cgconfig
服务的开机初始化脚本来启动应用。
Apache APISIX - 专门为 kubernetes 研发的入口控制器。
该项目目前是 general availability 级别
apisix-ingress-controller 要求 kubernetes 版本 1.16+. 因为使用了 CustomResourceDefinition v1 stable 版本的 API. 从 1.0.0 版本开始,APISIX-ingress-controller 要求 Apache APISIX 版本 2.7+.
apisix-ingress-controller 需要的所有配置都是通过 Kubernetes CRDs (Custom Resource Definitions) 定义的。支持在 Apache APISIX 中配置插件、上游的服务注册发现机制、负载均衡等。
apisix-ingress-controller 是 Apache APISIX 的控制面组件. 当前服务于 Kubernetes 集群。 未来, 计划分离出子模块以适配更多的部署模式,比如虚拟机集群部署。
整体架构图如下:
这是一张内部架构图:
apisix-ingress-controller 负责和 Kubernetes Apiserver 交互, 申请可访问资源权限(RBAC),监控变化,在 Ingress 控制器中实现对象转换,比较变化,然后同步到 Apache APISIX。
这是一张流程图,介绍了ApisixRoute和其他CRD在同步过程中的主要逻辑
apisix-ingress-controller 给 CRDs 提供了外部配置方法。它旨在务于需要日常操作和维护的运维人员,他们需要经常处理大量路由配置,希望在一个配置文件中处理所有相关的服务,同时还希望能具备便捷和易于理解的管理能力。但是,Apache APISIX 则是从网关的角度设计的,并且所有的路由都是独立的。这就导致了两者在数据结构上存在差异。一个注重批量定义,一个注重离散实现。
考虑到不同人群的使用习惯,CRDs 的数据结构借鉴了 Kubernetes Ingress 的数据结构,数据结构基本一致。
关于这两者的差别,请看下面这张图:
可以看到,它们是多对多的关系。因此,apisix-ingress-controller 必须对 CRD 做一些转换,以适应不同的网关。
seven 模块内部保存了内存数据结构,目前与Apache APISIX资源对象非常相似。当 Kubernetes 资源对象有新变化时,seven 会比较内存对象,并根据比较结果进行增量更新。
目前的比较规则是根据route/service/upstream资源对象的分组,分别进行比较,发现差异后做出相应的广播通知。
根据 ApisixUpstream
中定义的 namespace
name
port
字段,apisix-ingress-controller 会在 Apache APISIX Upstream 中注册 处于 running 状态的 endpoints 节点,并且根据 kubernetes endpoints 状态进行实时同步。
基于服务发现,apisix-ingress-controller 可以直接访问后端 pod,绕过 Kubernetes Service,可以实现自定义的负载均衡策略。
不像 Kubernetes Nginx Ingress Controller,apisix-ingress-controller 的 annotation 实现是基于 Apache APISIX 的插件机制的。
比如,可以通过在ApisixRoute
资源对象中设置k8s.apisix.apache.org/whitelist-source-range
annotation来配置白名单。
apiVersion: apisix.apache.org/v2beta3
kind: ApisixRoute
metadata:
annotations:
k8s.apisix.apache.org/whitelist-source-range: 1.2.3.4,2.2.0.0/16
name: httpserver-route
spec:
...
黑/白名单功能是通过ip-restriction插件来实现的。
为方便的定义一些常用的配置,未来会有更多的 annotation 实现,比如CORS。
ApisixRoute 是一个 CRD 资源,它关注如何将流量发送到后端,它有很多 APISIX 支持的特性。相比 Ingress,功能实现的更原生,语意更强。
URI 路径总是用于拆分流量,比如访问 foo.com 的请求, 含有 /foo 前缀请求路由到 foo 服务,访问 /bar 的请求要路由到 bar 服务。以 ApisixRoute 方式配置应该是这样的:
apiVersion: apisix.apache.org/v2beta3
kind: ApisixRoute
metadata:
name: foo-bar-route
spec:
http:
- name: foo
match:
hosts:
- foo.com
paths:
- "/foo*"
backends:
- serviceName: foo
servicePort: 80
- name: bar
match:
paths:
- "/bar"
backends:
- serviceName: bar
servicePort: 80
有prefix
和exact
两种路径类型可用 默认exact
,当需要前缀匹配的时候,就在路径后加 * 比如 /id/* 能匹配所有带 /id/ 前缀的请求。
基于路径的路由是最普遍的,但这并不够, 再试一下其他路由方式,比如 methods
和 exprs
methods
通过 HTTP 动作来切分流量,下面例子会把所有 GET 请求路由到 foo 服务(kubernetes service)
apiVersion: apisix.apache.org/v2beta3
kind: ApisixRoute
metadata:
name: method-route
spec:
http:
- name: method
match:
paths:
- /
methods:
- GET
backends:
- serviceName: foo
servicePort: 80
exprs
允许用户使用 HTTP 中的任意字符串来配置匹配条件,例如query、HTTP Header、Cookie。它可以配置多个表达式,而这些表达式又由主题(subject)、运算符(operator)和值/集合(value/set)组成。比如:
apiVersion: apisix.apache.org/v2beta3
kind: ApisixRoute
metadata:
name: method-route
spec:
http:
- name: method
match:
paths:
- /
exprs:
- subject:
scope: Query
name: id
op: Equal
value: "2143"
backends:
- serviceName: foo
servicePort: 80
上面是绝对匹配,匹配所有请求的 query 字符串中 id 的值必须等于 2143。
默认,apisix-ingress-controller 会监听 service 的引用,所以最新的 endpoints 列表会被更新到 Apache APISIX。同样 apisix-ingress-controller 也可以直接使用 service 自身的 clusterIP。如果这正是你想要的,配置 resolveGranularity: service
(默认endpoint
). 如下:
apiVersion: apisix.apache.org/v2beta3
kind: ApisixRoute
metadata:
name: method-route
spec:
http:
- name: method
match:
paths:
- /*
methods:
- GET
backends:
- serviceName: foo
servicePort: 80
resolveGranularity: service
这是 APISIX Ingress Controller 一个非常棒的特性。一个路由规则中可以指定多个后端,当多个后端共存时,将应用基于权重的流量拆分(实际上是使用Apache APISIX中的流量拆分 traffic-split 插件)您可以为每个后端指定权重,默认权重为 100。比如:
apiVersion: apisix.apache.org/v2beta3
kind: ApisixRoute
metadata:
name: method-route
spec:
http:
- name: method
match:
paths:
- /*
methods:
- GET
exprs:
- subject:
scope: Header
name: User-Agent
op: RegexMatch
value: ".*Chrome.*"
backends:
- serviceName: foo
servicePort: 80
weight: 100
- serviceName: bar
servicePort: 81
weight: 50
上面有一个路由规则(1.所有GET /*
请求 2.Header中有匹配 User-Agent: .*Chrome.*
的条目)它有两个后端服务 foo、bar,权重是100:50,意味着有2/3的流量会进入 foo,有1/3的流量会进入bar。
Apache APISIX 提供了 40 多个插件,可以在 APIsixRoute 中使用。所有配置项的名称与 APISIX 中的相同。
apiVersion: apisix.apache.org/v2beta3
kind: ApisixRoute
metadata:
name: httpbin-route
spec:
http:
- name: httpbin
match:
hosts:
- local.httpbin.org
paths:
- /*
backends:
- serviceName: foo
servicePort: 80
plugins:
- name: cors
enable: true
为到 local.httpbin.org 的请求都配置了 Cors 插件
创建一个 route,配置特定的 websocket 字段,就可以代理 websocket 服务。比如:
apiVersion: apisix.apache.org/v2beta3
kind: ApisixRoute
metadata:
name: ws-route
spec:
http:
- name: websocket
match:
hosts:
- ws.foo.org
paths:
- /*
backends:
- serviceName: websocket-server
servicePort: 8080
websocket: true
apisix-ingress-controller 支持基于端口的 tcp 路由
apiVersion: apisix.apache.org/v2beta3
kind: ApisixRoute
metadata:
name: tcp-route
spec:
stream:
- name: tcp-route-rule1
protocol: TCP
match:
ingressPort: 9100
backend:
serviceName: tcp-server
servicePort: 8080
进入 apisix-ingress-controller 9100 端口的 TCP 流量会路由到后端 tcp-server 服务。
注意: APISIX不支持动态监听,所以需要在APISIX配置文件中预先定义9100端口。
apisix-ingress-controller 支持基于端口的 udp 路由
apiVersion: apisix.apache.org/v2beta3
kind: ApisixRoute
metadata:
name: udp-route
spec:
stream:
- name: udp-route-rule1
protocol: UDP
match:
ingressPort: 9200
backend:
serviceName: udp-server
servicePort: 53
进入 apisix-ingress-controller 9200 端口的 TCP 流量会路由到后端 udp-server 服务。
注意: APISIX不支持动态监听,所以需要在APISIX配置文件中预先定义9200端口。
ApisixUpstream 是 kubernetes service 的装饰器。它设计成与其关联的 kubernetes service 的名字一致,将其变得更加强大,使该 kubernetes service 能够配置负载均衡策略、健康检查、重试、超时参数等。
通过 ApisixUpstream 和 kubernetes service,apisix-ingress-controller 会生成 APISIX Upstream(s).
需要适当的负载均衡算法来合理地分散 Kubernetes Service 的请求
apiVersion: apisix.apache.org/v1
kind: ApisixUpstream
metadata:
name: httpbin
spec:
loadbalancer:
type: ewma
---
apiVersion: v1
kind: Service
metadata:
name: httpbin
spec:
selector:
app: httpbin
ports:
- name: http
port: 80
targetPort: 8080
上面这个例子给 httpbin 服务配置了 ewma 负载均衡算法。有时候可能会需要会话保持,你可以配置一致性哈希负载均衡算法
apiVersion: apisix.apache.org/v1
kind: ApisixUpstream
metadata:
name: httpbin
spec:
loadbalancer:
type: chash
hashOn: header
key: "user-agent"
这样 apisix 就会根据 user-agent header 来分发流量。
尽管 kubelet 已经提供了检测 pod 健康的探针机制。你可能还需要更加丰富的健康检查机制,比如被动健康检查机制。
apiVersion: apisix.apache.org/v1
kind: ApisixUpstream
metadata:
name: httpbin
spec:
healthCheck:
passive:
unhealthy:
httpCodes:
- 500
- 502
- 503
- 504
httpFailures: 3
timeout: 5s
active:
type: http
httpPath: /healthz
timeout: 5s
host: www.foo.com
healthy:
successes: 3
interval: 2s
httpCodes:
- 200
- 206
上面的yaml片段定义了被动健康检查器来检查endpoints的健康状况。一旦连续三次请求的响应状态码是错误(500 502 503 504 中的一个),这个endpoint就会被标记成不健康并不会再给它分配流量了,直到它再次健康。
所以,主动健康检查器就出现了。endpoint 可能掉线一段时间又回复健康,主动健康检查器主动探测这些不健康的endpoints,一旦满足健康条件就将其恢复为健康(条件:连续三次请求响应状态码为200或206)
注意:主动健康检查器在某种程度上与 liveness/readiness 探针重复,但如果使用被动健康检查机制,则它是必需的。因此,一旦您使用了 ApisixUpstream 中的健康检查功能,主动健康检查器是强制性的。
当请求出现错误,比如网络问题或者服务不可用当时候,你可能想重试请求。默认重试次数是1,通过定义retries
字段可以改变这个值。
下面这个例子将retries
定义为3,表明会对kubernetes service/httpbin的endpoints最多请求3次。
注意:只有在尚未向客户端响应任何内容的情况下,才有可能将请求重试传递到下一个端点。也就是说,如果在传输响应的过程中发生错误或超时,就不会重试了。
apiVersion: apisix.apache.org/v1
kind: ApisixUpstream
metadata:
name: httpbin
spec:
retries: 3
默认,connect、send 和 read 的超时时间是60s,这可能对有些应用不合适,修改timeout
字段来改变默认值。
apiVersion: apisix.apache.org/v1
kind: ApisixUpstream
metadata:
name: httpbin
spec:
timeout:
connect: 5s
read: 10s
send: 10s
上面例子将connect、read 和 send 分别设置为 5s、10s、10s。
有时,单个 kubernetes service 可能会暴露多个端口,这些端口提供不同的功能并且需要不同的上游配置。在这种情况下,您可以为单个端口创建配置。
apiVersion: apisix.apache.org/v1
kind: ApisixUpstream
metadata:
name: foo
spec:
loadbalancer:
type: roundrobin
portLevelSettings:
- port: 7000
scheme: http
- port: 7001
scheme: grpc
---
apiVersion: v1
kind: Service
metadata:
name: foo
spec:
selector:
app: foo
portLevelSettings:
- name: http
port: 7000
targetPort: 7000
- name: grpc
port: 7001
targetPort: 7001
foo 服务暴露的两个端口,一个使用http协议,另一个使用grpc协议。同时,ApisixUpstream/foo 为7000端口配置http协议,为7001端口配置grpc协议(所有端口都是service端口),两个端口都共享使用同一个负载均衡算法。
如果服务仅公开一个端口,则 PortLevelSettings 不是必需的,但在定义多个端口时很有用。
思必驰:为什么我们重新写了一个 k8s ingress controller?
腾讯云:为什么选择 apisix 实现 kubernetes ingress controller
Grafana Mimir 是目前最具扩展性、性能最好的开源时序数据库,Mimir 允许你将指标扩展到 1 亿。它部署简单、高可用、多租户支持、持久存储、查询性能超高,比 Cortex 快 40 倍。 Mimir 托管在 https://github.com/grafana/mimir 并在 AGPLv3 下获得许可。
B站:Grafana Mimir 发布 目前最具扩展性的开源时序数据库
Mimir 是指标领域的一个新项目,站在巨人的肩膀上。为了理解 Mimir,我们需要回顾一下 Cortex 的历史。
2016 年在 Weaveworks 工作时,我与 Prometheus 的联合创始人兼维护者 Julius Volz 一起启动了 Cortex 项目。该项目的目标是构建一个可扩展的与 Prometheus 兼容的解决方案,旨在作为 SaaS 产品运行。在我加入 Grafana Labs 后,我们与 Weaveworks 合作,将 Cortex 转移到一个中立的地方,即云原生计算基金会。Cortex 于 2018 年 9 月 20 日被接受为 CNCF 沙盒项目,两年后晋升为孵化项目。CNCF 为两个公司在项目上提供了一个公平的竞争协作环境,这确实很棒,Grafana Labs 和 Weaveworks 都积极参与其中。Cortex 被 20 多个组织使用,并得到了大约 100 名开发人员的贡献。 Grafana Labs 的员工无疑是 Cortex 项目的最大贡献者,在 2019 - 2021 年期间贡献了约 87% 的代码提交。
来源: cortex.devstats.cncf.io
来看看这些产品 Cortex、Loki、Tempo 和 Grafana Enterprise Metrics
过去,Cortex 已经成为很多项目的基础,包括 Grafana Loki(类似 Prometheus,用于日志)、Grafana Tempo(用于分布式追踪)、Grafana Enterprise Metrics(GEM)。Grafana Labs 于 2020 年发布该项目,让 Prometheus 能适应更大的组织、加入很多企业级特性(比如安全、访问控制、简化管理UI),旨在他们卖给那些不想自己构建但还想使用这类产品的企业。
同时,云服务商和 ISVs(独立软件开发商)也推出了基于 Cortex 的产品,但是对项目却没啥贡献。一家公司,通过创造技术来降低其他公司的成本,但是却对开源技术不感兴趣。这是不可持续并且非常不好的。为了回应,我们后面更偏向于对 GEM 投资而不是 Cortex。作为一家热衷于开源的公司,这一点让大家很不舒服。我们认为,GEM 中一些可扩展性相关和性能相关的特性应该被开源。
大家应该知道,去年我们重新授权了一些开源项目,把 Grafana, Grafana Loki 和 Grafana Tempo, 从 Apache 2.0 调整到 AGPLv3(OSI 批准的许可证,保留了开源自由,同时鼓励第三方将代码贡献回社区)从 Grafana Labs 开创之初,我们的目标就是要围绕我们的开源项目构建可持续发展的商业,将商业产品的收入重新投入到开源技术和社区。AGPL 许可能平衡商业和开源之间的关系。
Mimir 集合了 Cortex 中的最佳功能和为 GEM & Grafana Cloud 大规模运行而研发的功能,所有这些都在 AGPLv3 许可下。Mimir 包含以前的商业功能,包括无限制基数(使用水平可扩展的 “split” 压缩器实现)和快速、高基数查询(使用分片查询引擎实现)
Cortex、Grafana Mimir 和 Grafana Cloud & Grafana Enterprise Metrics 比较
在从 Cortex 开始构建 Mimir 的过程中,团队有机会消除五年来欠下的技术债务,删除未使用的功能,使项目更易于维护,简化配置并改进文档。希望通过这次投资,在 Mimir 上的努力会让其更加易用,从而帮助社区更好的发展。
对于 Grafana Cloud 和 Grafana Enterprise Metrics 的用户来说,没有任何变化,因为这两种产品从几个月前就都基于 Grafana Mimir。对于正使用 Cortex 的组织,在一定程度的主版本升级限制内,Mimir 可以作为替代品。大多数情况下,从 Cortex 迁移到 Mimir只需不到 10 分钟。
Mimir 的愿景不是成为“最具可扩展性的普罗米修斯”,而是“最具可扩展性的泛指标时序数据库”。用户无需更改代码即可将指标发送到 Mimir。今天,Mimir 可以原生使用 Prometheus 指标。很快 Influx、Graphite、OpenTelemetry 和 Datadog 将紧随其后。这是我们“大帐篷”理念的一部分:正如 Grafana 是可视化所有数据的一体化工具一样,Mimir 可以成为存储所有指标的一体化工具。
Mimir 发布以后,强大、全面、可插拔的开源观测工具栈已经形成:LGTM(Loki 用户日志, Grafana 用于可视化, Tempo 用于跟踪, Mimir 用于指标),快去体验吧。
想了解更多,阅读 Q&A with our CEO, Raj Dutt,注册4月26日网络研讨会 介绍 Grafana Mimir,能扩展1亿指标的开源的时序数据库,不仅如此
本文已参与「开源摘星计划」,欢迎正在阅读的你加入。活动链接:https://github.com/weopenprojects/WeOpen-Star
分析了 官方 Github: Harbor 高可用方案讨论, 一开始我们选择了 Solution 1 (双激活共享存储方案), 在公司内部大概运行了一年多的时间, 架构图如下:
从图中可以看到, 这种方案基于外部共享存储、外部数据库和 Redis 服务, 构建其两个/以上的 harbor 实例. 既然使用了外部的服务, 那么高可用的压力自然而然的转移到了外部服务上. 我们一开始采用的外部的 NFS 共享存储服务, 由于我们团队实际情况, 我们暂时还不能保证外部存储的高可用. 同时, 鉴于我们对镜像服务高可用的迫切需求, 决定调研新的 Harbor 的高可用方案.
选择了 Solution 4 (双主复制方案), 这个解决方案, 使用复制来实现高可用, 它不需要共享存储、外部数据库服务、外部 Redis 服务. 这种方案可以有效的解决镜像服务的单点故障. 架构图如下:
从图中可以看到, 这种方案仅需要在两个 harbor 实例之间建立全量复制机制. 这种方案特别适合异地办公的团队.
以下是服务器和各组件的详细情况:
服务器配置 | 值 |
---|---|
虚拟机 | 2台 |
IP/内网 | 10.206.99.57, 10.206.99.58 |
配置 | 4核8G, 系统盘160G, 数据盘5T挂载到/data目录 |
操作系统 | CentOS 7.9 |
用户 | root |
这里把数据磁盘挂到
/data
目录, 是因为 harbor 的数据卷配置默认就是它, 后面就不需要修改 harbor 这块的配置了.
组件 | 配置/版本 | 说明 |
---|---|---|
docker-ce | 20.10.14 | |
docker-compose | 1.29.2 | 最新稳定版 |
harbor | v2.2.4 | 离线版 |
参考 Install Docker Engine on CentOS 来安装, 因为我是全新的系统, 直接安装:
安装 yum-utils
包, 它能提供 yum-config-manager
配置工具, 然后用工具来配置安装稳定的 yum 仓库.
yum install -y yum-utils
yum-config-manager \
--add-repo \
http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
这里使用阿里云镜像替换 https://download.docker.com/linux/centos/docker-ce.repo
安装最新稳定版 Docker 引擎和 containerd
yum install -y docker-ce docker-ce-cli containerd.io
启动 docker 实例并配置开机自动启动
systemctl start docker
systemctl enable docker
做一些 docker 相关的配置优化:
cat <<EOF | tee /etc/docker/daemon.json
{
"exec-opts": ["native.cgroupdriver=systemd"],
"log-driver": "json-file",
"log-opts": {
"max-size": "100m"
},
"storage-driver": "overlay2",
"storage-opts": [
"overlay2.override_kernel_check=true"
]
}
EOF
重启启动 docker 实例
systemctl daemon-reload
systemctl restart docker
harbor 使用 docker-compose 进行部署, 当前最新稳定版本是 1.29.2, 使用下面命令进行安装:
curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# 如果你的服务器也是 Linux-x86_64, 可以用这个国内的地址下载
curl -L "https://rutron.oss-cn-beijing.aliyuncs.com/tools/docker-compose-Linux-x86_64" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
打开 Harbor 下载页面, 下载离线安装器. 因为之前使用的是 v2.2.0
版本, 有不少应用已经对接了 harbor 的 api, 为了兼容性, 我选择了 v2.2.4
.
# 使用 root 用户 ~ 目录
cd /root
curl -O https://rutron.oss-cn-beijing.aliyuncs.com/harbor/harbor-offline-installer-v2.2.4.tgz
tar xzvf harbor-offline-installer-v2.2.4.tgz
cd harbor
由于 github-releases 下载页面速度很慢, 我将下载好的包放在了 aliyun-oss 上
拷贝示例配置文件, 进行修改:
cp harbor.yml.tmpl harbor.yml
vi harbor.yml
因为我打算采用默认安装, 所以需要修改的配置项不多, 仅有几个地方需要修改:
hostname
配置就会失效./data
, 因为我服务器的数据盘就挂的 /data
目录, 这里就不需要修改了.下面是默认配置文件, 重点配置我都做了翻译。别看配置文件这么长,重要的都在前 50 行:
|
|
默认安装不含 Notary, Trivy, 或者 Chart 仓库服务, 执行下面的命令:
./install.sh
查看安装状态:
docker ps
如果所有的容器的状态 STATUS 都为 Up About a minute (healthy)
说明安装成功~
打开 harbor admin ui 验证下吧! 别忘了修改 admin 的密码. 使用同样的方式将两台虚拟机的 docker、docker-compose 和 harbor 都安装好.
如果需要更改 harbor 的配置, 请按照如下步骤操作:
# 首先进入工作目录
cd ~/harbor/
docker-compose down -v
vim harbor.yml
./prepare
docker-compose up -d
# 重装前清理历史数据
rm -rf /data/database
rm -rf /data/registry
rm -rf /data/redis
在其中一台 harbor 实例上配置,我以 10.206.99.58 为例,另一实例同理,首先需要创建仓库,点击系统管理>仓库管理>新建目标
,按照如下填写:
然后,创建复制规则,点击系统管理>复制管理>新建规则
,按照如下填写:
这样,当用户往 10.206.99.58 中推送/删除镜像时,10.206.99.57 也会同步发生变化。
现在两个 harbor 实例都已经配置好了。用户看到的是两个完全独立的 harbor,他们的用户独立,访问地址不同。当然有些场景下这样已经可以满足需求了,比如异地办公的团队(可以按照地域区分使用访问地址)。如果我们想统一访问地址,可以在前面增加一个反向代理。而且可以将 ssl 证书部署在代理上。还是比较推荐的。所以我希望这个代理能实现:
但是很遗憾,harbor 实例之间用户和相关权限是无法同步的。这可能需要需要一些外在的机制实现了。
我假设提供给用户的域名是:registry.example.com,我使用 nginx 作为这个反向代理,它的配置文件/etc/nginx/conf.d/registry.example.com.conf
是这样的。
|
|
运行 nginx 反向代理:
# 将证书和配置文件都放在 /etc/nginx/conf.d 路径下
docker run -d --restart=always \
--name=nginx \
-p 80:80 -p 443:443 \
-v /etc/nginx/conf.d:/etc/nginx/conf.d \
nginx
测试对 registry.example.com 进行login/push/pull
镜像均正常,检查两个 harbor 实例也同步正常。至此,完成~
至此,所有的安装/配置就结束了,通过体验测试我发现:
点击查看原文
eBPF 是革命性技术, 起源于 linux 内核, 能够在操作系统内核中执行沙盒程序. 旨在不改变内核源码或加载内核模块的前提下安全便捷的扩展内核能力.
历史上, 由于内核拥有全局查看并控制整个操作系统的特权, 操作系统一直被认为是实现可观察性, 安全, 网络功能的理想地方. 同时, 由于其核心角色和对于稳定和安全的高要求, 操作系统很难演进. 因此, 传统上与在操作系统之外实现的功能相比, 操作系统级别的创新率较低.
eBPF 从根本上改变了这种一成不变的状态. 通过允许在操作系统中执行沙盒程序, 开发者可以通过执行 eBPF 程序, 来给运行中的操作系统添加额外的能力. 就像在本地使用即时编译器(JIT)和验证引擎一样, 操作系统可以保证安全性和执行效率. 这催生了不少基于 eBPF 的项目, 涵盖了广泛的用例, 包括下一代网络、可观察性和安全功能.
今天, eBPF 被广泛用于各种用例: 在现代化的数据中心和云原生环境中提供高性能网络和负载均衡, 以较低的开销提取细粒度的可观察性安全数据, 帮助应用程序开发者追踪应用, 并能够在性能故障分析、预防性应用和容器运行时安全执法等方面提供帮助. 它的可能性是无限的, 关于 eBPF 的创新才刚开始.
eBPF.io 是以 eBPF 为主题, 每个人学习和协作的地方. eBPF 是一个开源社区, 每个人可以实践或者分享. 不论你是想阅读 eBPF 第一篇介绍文章, 还是发现更多阅读素材, 抑或是为变成 eBPF 主项目贡献者迈出第一步, eBPF.io 会一直陪伴你帮助你.
下面的章节是关于 eBPF 的快速介绍. 如果你想了解更多, 查看 eBPF & XDP Reference Guide. 不管你是一名从事 eBPF 的开发者, 或是有兴趣使用 eBPF 作为解决方案, 理解基础概念和架构都是很有用的.
eBPF 程序是事件驱动的, 能在内核或应用程序执行到一个特定的 hook 点时执行. 预定义的 hooks 包含系统调用, 函数出/入口, 内核追踪点, 网络事件等等.
如果预定义 hook 不能满足需求, 也可以创建内核探针(kprobe)或者用户探针(uprobe), 在内核/用户应用程序的任何位置, 把探针附加到 eBPF 程序上.
在很多场景中, 用户不需要直接使用 eBPF, 而是通过一些项目, 比如 cilium, bcc 或 bpftrace, 它们是 eBPF 上层的抽象, 提供了使用 eBPF 实现的特定功能, 用户无需直接编写 eBPF 程序.
如果没有高级抽象, 就需要直接编写 eBPF 程序. Linux 内核要器加载字节码形式的 eBPF 程序. 虽然可以直接编写字节码, 但是更普遍的开发实践是借用像 LLVM 这样的编译器, 把伪 C 代码编译成字节码.
当所需的钩子被识别后, 可以使用 bpf 系统调用将 eBPF 程序加载到 Linux 内核中. 这通常使用一个可用的 eBPF 工具库来完成. 下一节将介绍一些可用的开发工具链.
当程序加载到 Linux 内核中时, 它在附加到请求的钩子之前要经过两个步骤:
这一步是为了确保 eBPF 程序安全执行. 它验证程序是否满足一些条件, 比如:
该步骤将通用字节码翻译成机器特定的指令集, 以优化程序的执行速度. 这使 eBPF 程序像原生编译的内核代码或者像已加载的内核模块代码一样高效运行.
eBPF 程序一个重要能力是: 能够共享收集的信息, 能够存储状态. 为了实现该能力, eBPF 程序借用 Maps 来存储/获取数据, 它支持丰富的数据结构. 通过系统调用, 可以从 eBPF 程序或者用户空间应用访问 maps.
为了解 map 类型的多样性, 下面是不完整的 map 类型列表. 这些类型的变量同时是 共享变量 和 per-CPU 变量.
eBPF 程序不能随意调用内核函数. 如果允许的话, 将会把 eBPF 程序绑定到特定的内核版本, 这会使程序的兼容性复杂化. 所以, eBPF 程序转而使用帮助函数, 它是内核提供的大家熟知的稳定的 API.
可用的帮助函数还在持续发展中, 例如:
eBPF 程序可以组合使用尾调用和函数调用(tail & function calls). 函数调用允许在 eBPF 程序中定义和调用函数. 尾调用可以调用执行其他 eBPF 程序, 并替换执行上下文, 类似于 execve()
系统调用对常规进程的操作方式.
权利越大, 责任越大
eBPF 是一项伟大的技术, 当下在很多关键软件中都扮演了核心的角色. 在 eBPF 程序开发过程中, 当 eBPF 进入 Linux 内核时, eBPF 的安全性就变得异常重要. eBPF 的安全性通过下面几点来保证:
除非开启非特权 eBPF, 所有企图加载 eBPF 程序到内核的进程必须在特权模式(root)下运行,或者必须获得 CAP_BPF 能力. 这意味着非授信的程序不能加载 eBPF 程序.
如果开启非特权 eBPF, 非特权进程可以加载特定的 eBPF 程序, 它们仅能使用被缩减的功能集合, 并且将受限制的访问内核.
如果进程允许加载 eBPF 程序, 所有的程序都要经过 eBPF 验证器, 验证器来确保程序本身的安全性. 这意味着:
完成验证之后, 根据 eBPF 程序是从特权进程还是非特权进程加载, 来决定是否加固的 eBPF 程序. 这包括:
eBPF 程序不能直接访问任意内核内存. 必须通过 eBPF 助手函数访问位于程序上下文之外的数据和数据结构. 这保证了一致性的数据访问, 并使任何此类访问均受制于 eBPF 程序的权限, 例如如果可以保证修改是安全的, 则允许运行的 eBPF 程序修改某些数据结构的数据. eBPF 程序不能随机修改内核中的数据结构.
还记得 GeoCities 吗? 20年前, 网页几乎全都是用静态标记语言(HTML)写的, 网页基本上是一种应用程序(浏览器)能打开的文件. 再看今天, 网页已经变成了非常成熟的应用, 并且 WEB 已经取代了绝大部分编译语言写的应用. 是什么成就了这次革命?
简单来说, 就是引入 JavaScript 之后的可编程性. 它开启了一场大规模的革命, 几乎将浏览器变成了独立的操作系统.
为什么呢? 程序员不再受限于特定的浏览器版本. 没有去说服标准机构去定义更多需要的 HTML 标签, 相反, 而是提供了一些必要的构建模块, 将浏览器底层的演进和运行在其上层的应用进行分离. 这样说可能过于简单, 因为 HTML 的确做了不小的贡献, 也的确有所发展, 但是 HTML 本身的变革还不够.
在举这个例子并将其应用到 eBPF 之前, 让我们看一下对引入 JavaScript 至关重要的几个关键方面:
上面说的所有内容, 在 eBPF 中都能找到:
现在我们回到 eBPF. 为了理解 eBPF 可编程性在 Linux 内核上的影响, 我们来看张图片, 它有助于我们对 Linux 内核的架构进行理解, 并且能了解它是如何与应用程序和硬件进行交互的.
Linux 内核的主要目的是抽象硬件或虚拟硬件, 并提供一致的 API(系统调用), 允许应用程序运行和共享资源. 为了实现这一点, 维护了大量的子系统和层来分配这些职责. 每个子系统通常允许某种级别的配置来满足不同的用户需求. 如果没办法通过配置满足某种需求, 则需要更改内核. 从历史上看, 有两种选择:
原生支持 | 内核模块 |
---|---|
1. 更改内核源代码并说服 Linux 内核社区 | 1. 写一个新的内核模块 |
2. 等几年新内核版本上市 | 2. 定期修复它, 因为每个内核版本都可能破坏它 |
3. 由于缺乏安全边界, 有损坏 Linux 内核的风险 |
在不需要改变内核源码或者加载内核模块的情况下, eBPF 为重新编程内核行为提供了一种新的选择. 在很多地方, 这很像 JavaScript 和其他脚本语言, 它们让那些改变难度大, 成本高的系统开始演进.
有几个开发工具链来能够协助 eBPF 程序的开发和管理. 它们能满足用户的不同需求:
BCC 是一个框架, 能够让用户编写嵌入了 eBPF 程序的 python 程序. 该框架主要用来分析和跟踪应用/系统, eBPF 在其中主要负责收集统计数据或生成事件, 然后, 对应的用户空间程序会收集这些数据并以易读的方式进行展示. 运行 python 程序会生成 eBPF 字节码并将其加载进内核.
bpftrace 是 Linux eBPF 的高级跟踪语言, 可用于最新的 Linux 内核(4.x). bpftrace 使用 LLVM 作为后端将脚本编译为 eBPF 字节码,并利用 BCC 与 Linux eBPF 子系统以及现有的 Linux 跟踪功能进行交互: 内核动态跟踪(kprobes)、用户级动态跟踪(uprobes)和跟踪点(tracepoints). bpftrace 语言的灵感来自 awk、C 和以前的跟踪器(如 DTrace 和 SystemTap).
eBPF Go 库提供了一个通用的 eBPF 库, 它将获取 eBPF 字节码的过程与 eBPF 程序的加载和管理分离. eBPF 程序通常是通过编写高级语言创建的, 然后使用 clang/LLVM 编译器编译为 eBPF 字节码.
libbpf 库是一个基于 C/C++ 的通用 eBPF 库. 它提供给应用程序一种易用的 API 来抽象化 BPF 系统调用, 并将 eBPF 字节码(clang/LLVM 编译器生成)加载到内核的过程与之分离.
如果你想学习更多的 eBPF 知识, 阅读下面的材料:
这篇文档主要演示了 opensnoop(Linux eBPF/bcc) 工具的使用.
opensnoop 在系统范围内跟踪 open() 系统调用,并打印各种详细信息.
示例输出:
# ./opensnoop
PID COMM FD ERR PATH
17326 <...> 7 0 /sys/kernel/debug/tracing/trace_pipe
1576 snmpd 9 0 /proc/net/dev
1576 snmpd 11 0 /proc/net/if_inet6
1576 snmpd 11 0 /proc/sys/net/ipv4/neigh/eth0/retrans_time_ms
1576 snmpd 11 0 /proc/sys/net/ipv6/neigh/eth0/retrans_time_ms
1576 snmpd 11 0 /proc/sys/net/ipv6/conf/eth0/forwarding
1576 snmpd 11 0 /proc/sys/net/ipv6/neigh/eth0/base_reachable_time_ms
1576 snmpd 11 0 /proc/sys/net/ipv4/neigh/lo/retrans_time_ms
1576 snmpd 11 0 /proc/sys/net/ipv6/neigh/lo/retrans_time_ms
1576 snmpd 11 0 /proc/sys/net/ipv6/conf/lo/forwarding
1576 snmpd 11 0 /proc/sys/net/ipv6/neigh/lo/base_reachable_time_ms
1576 snmpd 9 0 /proc/diskstats
1576 snmpd 9 0 /proc/stat
1576 snmpd 9 0 /proc/vmstat
1956 supervise 9 0 supervise/status.new
1956 supervise 9 0 supervise/status.new
17358 run 3 0 /etc/ld.so.cache
17358 run 3 0 /lib/x86_64-linux-gnu/libtinfo.so.5
17358 run 3 0 /lib/x86_64-linux-gnu/libdl.so.2
17358 run 3 0 /lib/x86_64-linux-gnu/libc.so.6
17358 run -1 6 /dev/tty
17358 run 3 0 /proc/meminfo
17358 run 3 0 /etc/nsswitch.conf
17358 run 3 0 /etc/ld.so.cache
17358 run 3 0 /lib/x86_64-linux-gnu/libnss_compat.so.2
17358 run 3 0 /lib/x86_64-linux-gnu/libnsl.so.1
17358 run 3 0 /etc/ld.so.cache
17358 run 3 0 /lib/x86_64-linux-gnu/libnss_nis.so.2
17358 run 3 0 /lib/x86_64-linux-gnu/libnss_files.so.2
17358 run 3 0 /etc/passwd
17358 run 3 0 ./run
^C
在跟踪时,snmpd 进程打开了各种 /proc 文件(读取指标). 另外, 一个 “run” 进程读取各种库和配置文件(看起来像 正在启动: 一个新进程).
如果在应用程序启动期间使用, opensnoop 可用于发现配置和日志文件.
-p 选项可用于在内核中过滤 PID. 这里我将它与 -T 一起使用来打印时间戳:
$ ./opensnoop -Tp 1956
TIME(s) PID COMM FD ERR PATH
0.000000000 1956 supervise 9 0 supervise/status.new
0.000289999 1956 supervise 9 0 supervise/status.new
1.023068000 1956 supervise 9 0 supervise/status.new
1.023381997 1956 supervise 9 0 supervise/status.new
2.046030000 1956 supervise 9 0 supervise/status.new
2.046363000 1956 supervise 9 0 supervise/status.new
3.068203997 1956 supervise 9 0 supervise/status.new
3.068544999 1956 supervise 9 0 supervise/status.new
这表明 supervise 进程每秒打开2次 status.new 文件.
-U 选项在输出中包含 UID:
# ./opensnoop -U
UID PID COMM FD ERR PATH
0 27063 vminfo 5 0 /var/run/utmp
103 628 dbus-daemon -1 2 /usr/local/share/dbus-1/system-services
103 628 dbus-daemon 18 0 /usr/share/dbus-1/system-services
103 628 dbus-daemon -1 2 /lib/dbus-1/system-services
-u 选项过滤 UID:
# ./opensnoop -Uu 1000
UID PID COMM FD ERR PATH
1000 30240 ls 3 0 /etc/ld.so.cache
1000 30240 ls 3 0 /lib/x86_64-linux-gnu/libselinux.so.1
1000 30240 ls 3 0 /lib/x86_64-linux-gnu/libc.so.6
1000 30240 ls 3 0 /lib/x86_64-linux-gnu/libpcre.so.3
1000 30240 ls 3 0 /lib/x86_64-linux-gnu/libdl.so.2
1000 30240 ls 3 0 /lib/x86_64-linux-gnu/libpthread.so.0
-x 选项仅打印失败的 opens:
# ./opensnoop -x
PID COMM FD ERR PATH
18372 run -1 6 /dev/tty
18373 run -1 6 /dev/tty
18373 multilog -1 13 lock
18372 multilog -1 13 lock
18384 df -1 2 /usr/share/locale/en_US.UTF-8/LC_MESSAGES/coreutils.mo
18384 df -1 2 /usr/share/locale/en_US.utf8/LC_MESSAGES/coreutils.mo
18384 df -1 2 /usr/share/locale/en_US/LC_MESSAGES/coreutils.mo
18384 df -1 2 /usr/share/locale/en.UTF-8/LC_MESSAGES/coreutils.mo
18384 df -1 2 /usr/share/locale/en.utf8/LC_MESSAGES/coreutils.mo
18384 df -1 2 /usr/share/locale/en/LC_MESSAGES/coreutils.mo
18385 run -1 6 /dev/tty
18386 run -1 6 /dev/tty
这里捕获了一个 df 命令无法打开 coreutils.mo 文件, 并尝试从其他目录打开.
列 ERR 表示系统错误码, 2 代表 ENOENT: no such file or directory.
可以使用 -d 选项设置最长跟踪持续时间. 例如, 要跟踪 2秒:
# ./opensnoop -d 2
PID COMM FD ERR PATH
2191 indicator-multi 11 0 /sys/block
2191 indicator-multi 11 0 /sys/block
2191 indicator-multi 11 0 /sys/block
2191 indicator-multi 11 0 /sys/block
2191 indicator-multi 11 0 /sys/block
-n 选项可用于过滤进程名称(部分匹配):
# ./opensnoop -n ed
PID COMM FD ERR PATH
2679 sed 3 0 /etc/ld.so.cache
2679 sed 3 0 /lib/x86_64-linux-gnu/libselinux.so.1
2679 sed 3 0 /lib/x86_64-linux-gnu/libc.so.6
2679 sed 3 0 /lib/x86_64-linux-gnu/libpcre.so.3
2679 sed 3 0 /lib/x86_64-linux-gnu/libdl.so.2
2679 sed 3 0 /lib/x86_64-linux-gnu/libpthread.so.0
2679 sed 3 0 /proc/filesystems
2679 sed 3 0 /usr/lib/locale/locale-archive
2679 sed -1 2
2679 sed 3 0 /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache
2679 sed 3 0 /dev/null
2680 sed 3 0 /etc/ld.so.cache
2680 sed 3 0 /lib/x86_64-linux-gnu/libselinux.so.1
2680 sed 3 0 /lib/x86_64-linux-gnu/libc.so.6
2680 sed 3 0 /lib/x86_64-linux-gnu/libpcre.so.3
2680 sed 3 0 /lib/x86_64-linux-gnu/libdl.so.2
2680 sed 3 0 /lib/x86_64-linux-gnu/libpthread.so.0
2680 sed 3 0 /proc/filesystems
2680 sed 3 0 /usr/lib/locale/locale-archive
2680 sed -1 2
^C
这里捕获了 “sed” 命令,是因为命令中使用了命令名称部分匹配 “-n ed”
-e 选项能打印出额外的列; 例如,以下输出包含传递给 open(2) 的标志(以八进制表示):
# ./opensnoop -e
PID COMM FD ERR FLAGS PATH
28512 sshd 10 0 00101101 /proc/self/oom_score_adj
28512 sshd 3 0 02100000 /etc/ld.so.cache
28512 sshd 3 0 02100000 /lib/x86_64-linux-gnu/libwrap.so.0
28512 sshd 3 0 02100000 /lib/x86_64-linux-gnu/libaudit.so.1
28512 sshd 3 0 02100000 /lib/x86_64-linux-gnu/libpam.so.0
28512 sshd 3 0 02100000 /lib/x86_64-linux-gnu/libselinux.so.1
28512 sshd 3 0 02100000 /lib/x86_64-linux-gnu/libsystemd.so.0
28512 sshd 3 0 02100000 /usr/lib/x86_64-linux-gnu/libcrypto.so.1.0.2
28512 sshd 3 0 02100000 /lib/x86_64-linux-gnu/libutil.so.1
-f 选项能基于 open(2) 调用的标志进行过滤, 比如:
# ./opensnoop -e -f O_WRONLY -f O_RDWR
PID COMM FD ERR FLAGS PATH
28084 clear_console 3 0 00100002 /dev/tty
28084 clear_console -1 13 00100002 /dev/tty0
28084 clear_console -1 13 00100001 /dev/tty0
28084 clear_console -1 13 00100002 /dev/console
28084 clear_console -1 13 00100001 /dev/console
28051 sshd 8 0 02100002 /var/run/utmp
28051 sshd 7 0 00100001 /var/log/wtmp
–cgroupmap 选项基于 cgroup 集进行过滤, 它用于使用外部创建的映射.
# ./opensnoop --cgroupmap /sys/fs/bpf/test01
更多信息, 查看 docs/special_filtering.md
# ./opensnoop -h
usage: opensnoop.py [-h] [-T] [-U] [-x] [-p PID] [-t TID]
[--cgroupmap CGROUPMAP] [--mntnsmap MNTNSMAP] [-u UID]
[-d DURATION] [-n NAME] [-e] [-f FLAG_FILTER]
跟踪 open() 系统调用
optional arguments:
-h, --help show this help message and exit
-T, --timestamp include timestamp on output
-U, --print-uid include UID on output
-x, --failed only show failed opens
-p PID, --pid PID trace this PID only
-t TID, --tid TID trace this TID only
--cgroupmap CGROUPMAP
trace cgroups in this BPF map only
--mntnsmap MNTNSMAP trace mount namespaces in this BPF map on
-u UID, --uid UID trace this UID only
-d DURATION, --duration DURATION
total duration of trace in seconds
-n NAME, --name NAME only print process names containing this name
-e, --extended_fields
show extended fields
-f FLAG_FILTER, --flag_filter FLAG_FILTER
filter on flags argument (e.g., O_WRONLY)
examples:
./opensnoop # trace all open() syscalls
./opensnoop -T # include timestamps
./opensnoop -U # include UID
./opensnoop -x # only show failed opens
./opensnoop -p 181 # only trace PID 181
./opensnoop -t 123 # only trace TID 123
./opensnoop -u 1000 # only trace UID 1000
./opensnoop -d 10 # trace for 10 seconds only
./opensnoop -n main # only print process names containing "main"
./opensnoop -e # show extended fields
./opensnoop -f O_WRONLY -f O_RDWR # only print calls for writing
./opensnoop --cgroupmap mappath # only trace cgroups in this BPF map
./opensnoop --mntnsmap mappath # only trace mount namespaces in the map
这篇文档主要演示了 tcplife(Linux eBPF/bcc) 工具的使用.
tcplife 总结了在跟踪期间打开和关闭的 TCP 会话. 比如:
# ./tcplife
PID COMM LADDR LPORT RADDR RPORT TX_KB RX_KB MS
22597 recordProg 127.0.0.1 46644 127.0.0.1 28527 0 0 0.23
3277 redis-serv 127.0.0.1 28527 127.0.0.1 46644 0 0 0.28
22598 curl 100.66.3.172 61620 52.205.89.26 80 0 1 91.79
22604 curl 100.66.3.172 44400 52.204.43.121 80 0 1 121.38
22624 recordProg 127.0.0.1 46648 127.0.0.1 28527 0 0 0.22
3277 redis-serv 127.0.0.1 28527 127.0.0.1 46648 0 0 0.27
22647 recordProg 127.0.0.1 46650 127.0.0.1 28527 0 0 0.21
3277 redis-serv 127.0.0.1 28527 127.0.0.1 46650 0 0 0.26
[...]
这捕获了一个程序 “recordProg”, 它建立了一些到 “redis-serv” 的短暂的 TCP 连接, 每个连接持续大约 0.25 毫秒. 还有几个 “curl” 会话也被跟踪, 连接到端口 80, 持续了 91 和 121 毫秒.
此工具对于工作负载表征和流量统计很有用: 识别正在发生的连接以及传输的字节.
在这个例子中, 我上传了一个 10 Mbyte 的文件到服务器, 然后再次使用 scp 下载:
# ./tcplife
PID COMM LADDR LPORT RADDR RPORT TX_KB RX_KB MS
7715 recordProg 127.0.0.1 50894 127.0.0.1 28527 0 0 0.25
3277 redis-serv 127.0.0.1 28527 127.0.0.1 50894 0 0 0.30
7619 sshd 100.66.3.172 22 100.127.64.230 63033 5 10255 3066.79
7770 recordProg 127.0.0.1 50896 127.0.0.1 28527 0 0 0.20
3277 redis-serv 127.0.0.1 28527 127.0.0.1 50896 0 0 0.24
7793 recordProg 127.0.0.1 50898 127.0.0.1 28527 0 0 0.23
3277 redis-serv 127.0.0.1 28527 127.0.0.1 50898 0 0 0.27
7847 recordProg 127.0.0.1 50900 127.0.0.1 28527 0 0 0.24
3277 redis-serv 127.0.0.1 28527 127.0.0.1 50900 0 0 0.29
7870 recordProg 127.0.0.1 50902 127.0.0.1 28527 0 0 0.29
3277 redis-serv 127.0.0.1 28527 127.0.0.1 50902 0 0 0.30
7798 sshd 100.66.3.172 22 100.127.64.230 64925 10265 6 2176.15
[...]
可以看到 sshd 接收了 10 MB, 然后再传输出去. 看起来, 接收(3.07 秒)比传输(2.18 秒)慢.
进程名称被截断为 10 个字符. 通过使用宽选项 -w, 列宽变为 16 个字符. IP 地址栏也更宽, 以适合 IPv6 地址:
# ./tcplife -w
PID COMM IP LADDR LPORT RADDR RPORT TX_KB RX_KB MS
26315 recordProgramSt 4 127.0.0.1 44188 127.0.0.1 28527 0 0 0.21
3277 redis-server 4 127.0.0.1 28527 127.0.0.1 44188 0 0 0.26
26320 ssh 6 fe80::8a3:9dff:fed5:6b19 22440 fe80::8a3:9dff:fed5:6b19 22 1 1 457.52
26321 sshd 6 fe80::8a3:9dff:fed5:6b19 22 fe80::8a3:9dff:fed5:6b19 22440 1 1 458.69
26341 recordProgramSt 4 127.0.0.1 44192 127.0.0.1 28527 0 0 0.27
3277 redis-server 4 127.0.0.1 28527 127.0.0.1 44192 0 0 0.32
可以使用 -t 添加时间戳:
# ./tcplife -t
TIME(s) PID COMM LADDR LPORT RADDR RPORT TX_KB RX_KB MS
0.000000 5973 recordProg 127.0.0.1 47986 127.0.0.1 28527 0 0 0.25
0.000059 3277 redis-serv 127.0.0.1 28527 127.0.0.1 47986 0 0 0.29
1.022454 5996 recordProg 127.0.0.1 47988 127.0.0.1 28527 0 0 0.23
1.022513 3277 redis-serv 127.0.0.1 28527 127.0.0.1 47988 0 0 0.27
2.044868 6019 recordProg 127.0.0.1 47990 127.0.0.1 28527 0 0 0.24
2.044924 3277 redis-serv 127.0.0.1 28527 127.0.0.1 47990 0 0 0.28
3.069136 6042 recordProg 127.0.0.1 47992 127.0.0.1 28527 0 0 0.22
3.069204 3277 redis-serv 127.0.0.1 28527 127.0.0.1 47992 0 0 0.28
这表明 recordProg 进程每秒连接一次.
另外 -T 选项可以使用 HH:MM:SS 格式时间戳.
-s 选项可以指定逗号分隔的列表模式. 这里同时使用了 -t 和 -T 类型的时间戳:
# ./tcplife -stT
TIME,TIME(s),PID,COMM,IP,LADDR,LPORT,RADDR,RPORT,TX_KB,RX_KB,MS
23:39:38,0.000000,7335,recordProgramSt,4,127.0.0.1,48098,127.0.0.1,28527,0,0,0.26
23:39:38,0.000064,3277,redis-server,4,127.0.0.1,28527,127.0.0.1,48098,0,0,0.32
23:39:39,1.025078,7358,recordProgramSt,4,127.0.0.1,48100,127.0.0.1,28527,0,0,0.25
23:39:39,1.025141,3277,redis-server,4,127.0.0.1,28527,127.0.0.1,48100,0,0,0.30
23:39:41,2.040949,7381,recordProgramSt,4,127.0.0.1,48102,127.0.0.1,28527,0,0,0.24
23:39:41,2.041011,3277,redis-server,4,127.0.0.1,28527,127.0.0.1,48102,0,0,0.29
23:39:42,3.067848,7404,recordProgramSt,4,127.0.0.1,48104,127.0.0.1,28527,0,0,0.30
23:39:42,3.067914,3277,redis-server,4,127.0.0.1,28527,127.0.0.1,48104,0,0,0.35
[...]
还有过滤本地/远端端口的选项. 这里就过滤了本地 22 和 80 端口.
# ./tcplife.py -L 22,80
PID COMM LADDR LPORT RADDR RPORT TX_KB RX_KB MS
8301 sshd 100.66.3.172 22 100.127.64.230 58671 3 3 1448.52
[...]
# ./tcplife.py -h
usage: tcplife.py [-h] [-T] [-t] [-w] [-s] [-p PID] [-L LOCALPORT]
[-D REMOTEPORT] [-4 | -6]
跟踪 TCP 会话的生命周期并进行总结
optional arguments:
-h, --help show this help message and exit
-T, --time include time column on output (HH:MM:SS)
-t, --timestamp include timestamp on output (seconds)
-w, --wide wide column output (fits IPv6 addresses)
-s, --csv comma separated values output
-p PID, --pid PID trace this PID only
-L LOCALPORT, --localport LOCALPORT
comma-separated list of local ports to trace.
-D REMOTEPORT, --remoteport REMOTEPORT
comma-separated list of remote ports to trace.
-4, --ipv4 trace IPv4 family only
-6, --ipv6 trace IPv6 family only
examples:
./tcplife # trace all TCP connect()s
./tcplife -t # include time column (HH:MM:SS)
./tcplife -w # wider columns (fit IPv6)
./tcplife -stT # csv output, with times & timestamps
./tcplife -p 181 # only trace PID 181
./tcplife -L 80 # only trace local port 80
./tcplife -L 80,81 # only trace local ports 80 and 81
./tcplife -D 80 # only trace remote port 80
./tcplife -4 # only trace IPv4 family
./tcplife -6 # only trace IPv6 family
以下是 <实践中总结 Kubernetes 必须了解的核心内容> 主题分享 PPT
完~
kubernetes 中 pod 的设计是一个伟大的发明, 今天我很有必要去聊一下 pod 和 container, 探究一下它们究竟是什么? kubernetes 官方文档中关于pod 概念介绍提供了一个完整的解释, 但写的不够详细, 表达过于专业, 但还是很推荐大家阅读一下. 当然这篇文档应该更接地气.
linux 中是没有容器这个概念的, 容器就是 linux 中的普通进程, 它使用了 linux 内核提供的两个重要的特性: namespace & cgroups.
namespace 提供了一种隔离的特性, 让它之外的内容隐藏, 给它下面的进程一个不被干扰的运行环境(其实不完全,下面说) .
namespace 包含:
接上面, 其实 namespace 内部的进程并不是完全不和外面的进程产生影响的. 进程可以不受限制的使用物理机上的所有资源, 这样就会导致其他进程无资源可用. 所以, 为了限制进程资源使用, linux 提供了另一种特性 cgroups. 进程可以像在 namespace 中运行, 但是 cgroups 限制了进程的可以使用的资源. 这些资源包括:
CPU 通常按照毫核来限制(单位:m), 1000m=1C; 内存按照RAM的字节数来限制. 进程可以在 cgroups 设置的资源限制范围内运行, 不允许超限使用, 比如, 超过内存限制就会报 OOM(out of memory) 的错误.
需要特别说明的是, 上面提到的 namespace & cgroup 都是 Linux 独立的特性, 你可以使用上面提到的 namespace 中的一个或者多个. namespace & cgroup 作用到一组或者一个进程上. 你可以把多个进程放在一个 namespace 中, 这样它们就可以彼此交互, 或者 把他们放在一个 cgroups 中, 这样他们就可以共享一个CPU & Mem 资源限制.
我们都用过 docker, 当我们启动一个容器的时候, docker 会帮我们给每一个容器创建它们自己的 namespace & cgroups. 这应该就是我们理解的容器.
如图, 容器本身还是比较独立的, 他们可能会有映射到主机的端口和卷, 这样就可以和外面通信. 但是我们也可以通过一些命令将多个容器组合到一组namespace中, 下面我们举个例子说明:
首先, 创建一个 nginx 容器:
# cat <<EOF >> nginx.conf
> error_log stderr;
> events { worker_connections 1024; }
> http {
> access_log /dev/stdout combined;
> server {
> listen 80 default_server;
> server_name example.com www.example.com;
> location / {
> proxy_pass http://127.0.0.1:2368;
> }
> }
> }
> EOF
# docker run -d --name nginx -v `pwd`/nginx.conf:/etc/nginx/nginx.conf -p 8080:80 --ipc=shareable nginx
接着, 我们再启动一个 ghost 容器, ghost 是一个开源的博客系统, 同时我们添加几个额外的命令到 nginx 容器上.
# docker run -d --name ghost --net=container:nginx --ipc=container:nginx --pid=container:nginx ghost
好了, 现在 nginx 容器可以通过 localhost 将请求代理到 ghost 容器, 访问 http://localhost:8080
试试, 你可以通过 nginx 反向代理看到一个 ghost 博客. 上面的命令就把一组容器组合到里同一组 namespace 中, 容器彼此之间可以互相发现/通信.
就像这样:
现在我们已经知道, 我们可以把一组进程组合到一个 namespace & cgroups 中, 这就是 kubernetes 中的 pod. pod 允许你定义你要运行的容器, 然后 kubernetes 会帮正确的配置 namespace & cgroups. 它稍微复杂的一点是, 网络这块它没用 docker network, 而是用到了 CNI(通用网络接口), 但原理都差不多.
按照上述方式创建的 pod, 更像是运行在同一台机器上, 他们之间可以通过 localhost 通信, 可以共享存储卷. 甚至他们可以使用 IPC 或者互相发送 HUP / TERM 这类信号.
我们再举个例子, 如下图, 我们运行一个 nginx 反向代理 app, 再运行一个 confd, 当 app 实例增加或减少的时候去动态配置 nginx.conf
并重启 nginx, etcd 中存储了 app 的 ip 地址. 当 ip 列表发生变化, confd 会收到 etcd 发的通知, 并更新 nginx.conf
并给 nginx 发送一个 HUP 信号, nginx 收到 HUP 信号会重启.
如果用 docker, 你大概会把 nginx 和 confd 放在一个容器中. 由于 docker 只有一个 entrypoint, 所以你要启动一个类似 supervisord 一样的进程管理器 来让 nginx 和 confd 都运行起来. 你每启动一个 nginx 副本就要启动一个 supervisord, 这不好吧. 更重要的是, docker 只知道 supervisord 的状态, 因为它只有一个 entrypoint. 它看不到里面的所有进程, 这就意味着, 你用 docker 提供的工具获取不到他们的信息. 一旦 nginx Crash-Restart Loop
, docker 一点办法没有.
通过 pod , kubernetes 能管理每一个进程, 看到他们的状态, 它可以通过 api 将进程状态信息暴露给用户, 或者提供进程崩溃时重启/记录日志等服务.
使用 pod 这种组织容器的方式, 可以把容器当作提供各种功能的 “接口”. 它不同于传统意义上的 web 接口. 更像是可以被容器所使用的某种抽象意义的接口.
我们拿上面 nginx+confd 的例子来说, confd 不需要知道任何 nginx 进程的东西, 它就只需要去 watch etcd 然后给 nginx 进程发送 HUP 信号或者执行个命令. 而且你可以把 nginx 替换成其他任何类型的应用, 以这样的模式来使用 confd 的这种能力. 这种模式下, confd 通常被称作 “sidecar container” 边车容器, 下面这图就很形象.
像 istio 这样的服务网格项目, 也是, 给应用程序容器放置一个边车容器来提供服务路由, 遥测, 网络策略等功能, 但是对应用程序并没有做任何侵略性更改. 你也可以使用多个边车容器来组织 pod, 比如在一个 pod 中同时放置 confd & istio 边车容器. 用这样的方式, 可以构建更加复杂可靠的系统, 同时还能保持每个应用的独立性和简单性.
What are Kubernetes Pods Anyway?
What even is a container: namespaces and cgroups
video: Cgroups, namespaces, and beyond: what are containers made from?