technology

新建集群的第一步就是要规划服务器、网络、操作系统等等, 下面就结合我平时的工作经验总结下相关的要求, 内容根据日常工作持续补充完善:

服务器配置

kubernetes 集群分为控制节点和数据节点, 它们对于配置的要求有所不同:

控制面

节点规模 Master规格
1~5个节点 4核 8Gi(不建议2核 4Gi)
6~20个节点 4核 16Gi
21~100个节点 8核 32Gi
100~200个节点 16核 64Gi

系统盘40+Gi,用于储存 etcd 信息及相关配置文件等

数据面

  • 规格:CPU >= 4核, 内存 >= 8Gi
  • 确定整个集群的日常使用的总核数以及可用度的容忍度
    • 例如:集群总的核数有160核, 可以容忍10%的错误. 那么最小选择10台16核VM, 并且高峰运行的负荷不要超过 160*90%=144核. 如果容忍度是20%, 那么最小选择5台32核VM, 并且高峰运行的负荷不要超过160*80%=128核. 这样就算有一台VM出现故障, 剩余VM仍可以支持现有业务正常运行.
  • 确定 CPU:Memory 比例. 对于使用内存比较多的应用, 例如Java类应用, 建议考虑使用1:8的机型
  • 比如: virtual machine 32C 64G 200G系统盘 数据盘可选

什么情况下使用裸金属服务器?

  • 集群日常规模能够达到1000核。一台服务器至少96核,这样可以通过10台或11台服务器即可构建一个集群。
  • 快速扩大较多容器。例如:电商类大促,为应对流量尖峰,可以考虑使用裸金属来作为新增节点,这样增加一台裸金属服务器就可以支持很多个容器运行。

操作系统

  • 建议安装 ubuntu 18.04/debian buster/ubuntu 20.04/centos 7.9 优先级从高到低
  • linux kenerl 4.17+
  • 安装 ansible v2.9 & python-netaddr 以运行 ansible 命令
  • 安装 jinja 2.11+ 以运行 ansible playbooks
  • 允许 IPv4 forwarding
  • 部署节点的 ssh key 拷贝到所有节点
  • 禁用防火墙

网络

  • 单一集群采用统一的网卡命名比如:eth0等, 保证名称唯一
  • 没有特殊要求, 服务器要求可访问外网

集群规模限制

  • 每节点不超过 110 pods
  • 不超过 5k nodes
  • 总计不超过 15w pods
  • 总计不超过 30w containers

参考

官方文档

为什么使用?

Kubernetes没有提供适用于裸金属集群的网络负载均衡器实现, 也就是LoadBalancer类型的Service. Kubernetes 附带的网络负载均衡器的实现都是调用各种 IaaS 平台(GCP、AWS、Azure ……)的胶水代码。 如果您没有在受支持的 IaaS 平台(GCP、AWS、Azure…)上运行,LoadBalancers 在创建时将一直保持在pending状态。

裸金属集群的运维人员只剩下两个方式来将用户流量引入集群内: NodePortexternalIPs. 这两种在生产环境使用有很大的缺点, 这样, 裸金属集群也就成了 Kubernetes 生态中的第二类选择, 并不是首选.

MetalLB 的目的是实现一个网络负载均衡器来与标准的网络设备集成, 这样这些外部服务就能尽可能的正常工作了.

要求

MetalLB 要求如下:

  • 一个 Kubernetes 集群, Kubernetes 版本 1.13.0+, 没有网络负载均衡器功能.
  • 可以与 MetalLB 共存的集群网络配置。
  • 一些供 MetalLB 分发的 IPv4 地址。
  • 当使用 BGP 操作模式时,您将需要一台或多台能够发布 BGP 的路由器。
  • 使用 L2 操作模式时,节点之间必须允许 7946 端口(TCP 和 UDP,可配置其他端口)上的流量,这是 hashicorp/memberlist 的要求。

功能

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层模式 (ARP/NDP)

在2层模式下, 集群中的机器使用标准地址发现协议把这些 IPs 通知本地网络. 从 LAN 的角度看, 这台服务器有多个 IP 地址. 这个模式的详细行为还有局限性下面会介绍.

BGP

在 BGP 模式下, 集群中的所有机器与临近的路由器建立 BGP 对等会话, 告诉他们如何路由 Service IPs. 得益于 BGP 的策略机制, 使用 BGP 能实现真正的多节点负载均衡和细粒度的流量控制. 下面会介绍更多操作和局限性方面的细节.

工作模式

2层模式

在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 将不可达.

和 keepalive 比较

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 模式下,集群中的每个节点都会与您的网络路由器建立 BGP 对等会话,并使用该对等会话来通告外部集群服务的 IP.

假设您的路由器配置为支持多路径,这将实现真正的负载平衡: MetalLB 发布的路由彼此等效。这意味着路由器将使用所有下一跳,并在它们之间进行负载平衡.

数据包到达节点后,kube-proxy 负责流量路由的最后一跳,将数据包送到服务中的特定 pod.

负载均衡行为

负载均衡的确切行为取决于您的特定路由器型号和配置,但常见的行为是基于 packet-hash 方法在连接层面 per-connection 进行平衡。这是什么意思?

这个per-connection意味着单个 TCP 或 UDP 会话的所有数据包将被定向到集群中的单个机器。流量传播只发生在不同的连接之间,而不是一个连接内的数据包. 这是一件好事,因为在多个集群节点上传播数据包会导致一些问题:

  • 跨多个传输路径传播单个连接会导致数据包(packet)在线路上重新排序,这会极大地影响终端主机的性能。
  • 在 Kubernetes 中, 不能保证节点之间流量的路由保持一致。这意味着两个不同的节点可以将同一连接的数据包(packet)路由到不同的 Pod,这将导致连接失败。

高性能路由器能够以一种无状态的方式在多个后端之间使用数据包哈希的方法分发数据包. 对于每一个数据包, 它们拥有一些属性, 并能用它作为 “种子” 来决定选择哪一个后端. 如果, 所有的属性都一样, 它们就会选择同一个后端.

具体使用哪种哈希方法取决于路由器的硬件和软件. 典型的方法是: 3-tuple5-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 映射发生变化时, 你希望看到一次性干净的切换出现, 到该服务的大部分所有可用连接中断. 没有持续的丢包或者黑洞, 只是一次很干净的中断而已.

根据您的服务的用途,您可以采用几种缓解策略:

  • 你的 BGP 路由器可能有更加稳定的ECMP哈希算法. 有时候可能叫: 弹性ECMP弹性LAG. 使用这样的算法, 在后端集合发生变化的时候, 能有效的减少受影响的连接数量.
  • 把这些需要外部访问的服务部署到特定的节点上, 来减小节点池的大小, 平时看好这些节点.
  • 把服务的变更安排在流量的低谷时候, 此时你的用户在睡觉流量很低.
  • 把每一个逻辑上的服务拆分成两个有着不同 IP 的 kubernetes 服务, 使用DNS服务优雅的将用户流量从将要中断的服务迁移到另一个服务上.
  • 在客户端添加重试逻辑, 以优雅地从突然断开连接中恢复. 如果您的客户是移动应用程序或丰富的单页网络应用程序, 这尤其适用.
  • 将您的服务放在 ingress 控制器后面. ingress 控制器本身可以使用 MetalLB 来接收流量, 但是在 BGP 和您的服务之间有一个状态层意味着您可以毫无顾虑地更改您的服务。 您只需在更改 ingress 控制器本身的部署时小心(例如, 在添加更多 NGINX pod 时).
  • 接受偶尔会出现重置连接的情况. 对于低可用性的内部服务, 这可能是可以接受的.

FRR 模式

MetalLB 提供了一个实验模式: 使用 FRR 作为 BGP 层的后端.

开启 FRR 模式之后, 会获得以下额外的特性:

  • 由BFD支持的BGP会话
  • BGP和BFD都支持IPv6
  • 多协议支持的BGP
FRR 模式的局限性

相比与原生实现, FRR 模式有以下局限性:

  • BGPAdvertisementRouterID 字段可以被覆盖,但它必须对所有的 advertisements 都相同(不能有不同的 advertisements 具有不同的 RouterID)。
  • BGPAdvertisementmyAsn 字段可以被覆盖,但它必须对所有 advertisements 都相同(不能有不同的 advertisements 具有不同的 myAsn)
  • 如果 eBGP Peer 是距离节点多跳的, 则 ebgp-multihop 标志必须设置为 true

安装

安装之前, 确保满足所有要求. 尤其是, 你要注意网络附加组件的兼容性

如果你在云平台环境运行 MetalLB, 你最好先看看云环境兼容性页面, 确保你选择的云平台可以和 MetalLB 一起正常工作(大多数情况下都不好用).

MetalLB 支持4种安装方式:

  • 使用 Kubernetes 部署清单安装
  • 使用 Kustomize 安装
  • 使用 Helm 安装
  • 使用 MetalLB Operator 安装

准备

如果您在 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. 主要组件是:

  • Deployment: metallb-system/controller, 这是集群级别的控制器, 负责处理 IP 分配.
  • Daemonset: metallb-system/speaker, 这个组件使用你选择的协议对外发送信息, 使你的 Service 可以被访问.
  • ServiceAccount: controller 和 speaker 所使用的, 同时配置好了组件所需要的 RBAC 权限.

该安装清单并不包含配置文件, 但是 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 分配IPs之后, 它们必须要公布出去. 具体的配置要根据你想使用的协议来定, 下面会一一介绍:

注意: 也可以同时将一个 service 同时通过 L2 和 BGP 对外公布.

使用 L2 模式的配置

配置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 地址.

所以, 为了只将一部分 IPAddressPools 使用 L2 对外公布, 我们就需要声明一下(或者, 使用标签选择器).

apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: example
  namespace: metallb-system
spec:
  ipAddressPools:
  - first-pool

使用 BGP 模式的配置

需要告诉 MetalLB 如何与外部一个或多个 BGP 路由器建立会话.

所以, 需要给每一个 MetalLB 需要连接到路由器建一个 BGPPeer 资源.

为了能提供一个基础的 BGP 路由器配置和一个 IP 地址范围, 你需要定义4段信息.

  • MetalLB 需要连接的路由器地址
  • 路由器的AS编号
  • MetalLB 使用的AS编号
  • 使用CIDR前缀表示IP地址范围

比如给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

为 BGP 会话开启 BFD 支持

在实验的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>查看事件日志.

请求特定的IPs

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 选项,并根据您选择的策略和公告协议实现不同的公告模式。

Layer2

当使用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

当通过 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 模式一起使用的最佳模式。

IPv6和双协议栈Services

在 L2 模式下同时支持 IPv6 和双协议栈Services,但在 BGP 模式下仅通过实验性 FRR 模式来提供支持.

为了让 MetalLB 将 IP 分配给双栈服务,必须至少有一个IP地址池同时具有 v4 和 v6 版本的地址。

请注意,在双协议栈Services的情况下,不能使用spec.loadBalancerIP,因为它不允许请求多个IP,因此必须使用注解 metallb.universe.tf/loadBalancerIPs

IP地址共享

默认, Services 之间不能共享IP地址. 如果你希望多个Service使用一个IP地址. 你可以在 service 上配置 annotation metallb.universe.tf/allow-shared-ip 来开启优选择的IP地址共享.

这个 annotation 的值是一个共享 key. 下面几种情况下, Services 可以共享IP:

  • 他们都有一个相同的共享 key.
  • 他们使用的端口不一样(比如一个是 tcp/80 另一个是 tcp/443)
  • 他们都使用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 最为流行。因为它的可移植性和环境隔离的能力,它在数据中心内部特别流行。为了能理解这个技术,需要理解很多方面。

注意:很多人拿容器和虚拟机比较,他们有不同的设计目标,不是替代关系,重叠度很小。容器旨在成为一个轻量级环境,您可以裸机上启动容器,托管一个或几个独立的应用程序。当您想要托管整个操作系统或生态系统或者可能运行与底层环境不兼容的应用程序时,您应该选择虚拟机。

Linux 控制组

说实话,零信任环境下有些软件的确需要被控制或被限制 - 至少为了稳定,或是为了安全。很多时候一个Bug或不良代码可能会摧毁整个机器并削弱整个生态系统。还好,有办法来控制这些应用程序,控制组(cgroups)是内核的一个特性,它能限制/计量/隔离一个或者多个进程使用CPU、内存、磁盘I/O和网络。
cgroup技术最开始是Google开发,最终在2.6.24版本(2008年1月)的内核中出现。3.15和3.16版本内核将合并进重新设计的cgroups,它添加了kernfs(拆分一些sysfs逻辑)。
cgroups的主要设计目标是提供一个统一的接口,它可以管理进程或者整个操作系统级别的虚拟化,包含Linux容器,或者LXC。cgroups主要提供了以下能力:

  • 资源限制:一个组,可以通过配置使其不能使用超过特定内存限制,或者使用超过指定数量的处理器,或者被限制使用特定的外围设备。
  • 优先级:可以配置一个或多个组比别的组使用更少/更多的CPU或者I/O吞吐。
  • 计量:组的资源使用是被监控和计量的。
  • 控制:进程组可以被冻结、停止或重启。

一个 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

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下,启动应用,并将其和它的memorycpusetcpu限制进行绑定:

$ 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+.

功能特性

  • 使用 Custom Resource Definitions(CRDs) 对 Apache APISIX 进行声明式配置,使用 kubernetes yaml 结构最小化学习成本。
  • Yaml 配置热加载
  • 支持原生 Kubernetes Ingress (v1 和 v1beta1) 资源
  • Kubernetes endpoint 自动注册到 Apache APISIX 上游节点
  • 支持基于 POD(上游节点) 的负载均衡
  • 开箱支持上游节点健康检查
  • 扩展插件支持热配置并且立即生效
  • 支持路由的 SSL 和 mTLS
  • 支持流量切割和金丝雀发布
  • 支持 TCP 4层代理
  • Ingress 控制器本身也是一个可插拔的热加载组件
  • 多集群配置分发

这里有一份在线竞品分析表格

Apache APISIX Ingress vs. Kubernetes Nginx Ingress

  • yaml 配置热加载
  • 更方便的金丝雀发布
  • 配置验证,安全可靠
  • 丰富的插件和生态, 插件列表
  • 支持 APISIX 自定义资源和原生 kubernetes ingress 资源
  • 更活跃的社区

设计原理

架构

apisix-ingress-controller 需要的所有配置都是通过 Kubernetes CRDs (Custom Resource Definitions) 定义的。支持在 Apache APISIX 中配置插件、上游的服务注册发现机制、负载均衡等。
apisix-ingress-controller 是 Apache APISIX 的控制面组件. 当前服务于 Kubernetes 集群。 未来, 计划分离出子模块以适配更多的部署模式,比如虚拟机集群部署。

整体架构图如下:

architecture

这是一张内部架构图:

internal-arch

时序/流程图

apisix-ingress-controller 负责和 Kubernetes Apiserver 交互, 申请可访问资源权限(RBAC),监控变化,在 Ingress 控制器中实现对象转换,比较变化,然后同步到 Apache APISIX。

flow

这是一张流程图,介绍了ApisixRoute和其他CRD在同步过程中的主要逻辑

sync-logic-controller

结构转换

apisix-ingress-controller 给 CRDs 提供了外部配置方法。它旨在务于需要日常操作和维护的运维人员,他们需要经常处理大量路由配置,希望在一个配置文件中处理所有相关的服务,同时还希望能具备便捷和易于理解的管理能力。但是,Apache APISIX 则是从网关的角度设计的,并且所有的路由都是独立的。这就导致了两者在数据结构上存在差异。一个注重批量定义,一个注重离散实现。
考虑到不同人群的使用习惯,CRDs 的数据结构借鉴了 Kubernetes Ingress 的数据结构,数据结构基本一致。 关于这两者的差别,请看下面这张图:

struct-compare

可以看到,它们是多对多的关系。因此,apisix-ingress-controller 必须对 CRD 做一些转换,以适应不同的网关。

规则比较

seven 模块内部保存了内存数据结构,目前与Apache APISIX资源对象非常相似。当 Kubernetes 资源对象有新变化时,seven 会比较内存对象,并根据比较结果进行增量更新。
目前的比较规则是根据route/service/upstream资源对象的分组,分别进行比较,发现差异后做出相应的广播通知。

diff-rules

服务发现

根据 ApisixUpstream 中定义的 namespace name port 字段,apisix-ingress-controller 会在 Apache APISIX Upstream 中注册 处于 running 状态的 endpoints 节点,并且根据 kubernetes endpoints 状态进行实时同步。
基于服务发现,apisix-ingress-controller 可以直接访问后端 pod,绕过 Kubernetes Service,可以实现自定义的负载均衡策略。

Annotation 实现

不像 Kubernetes Nginx Ingress Controller,apisix-ingress-controller 的 annotation 实现是基于 Apache APISIX 的插件机制的。
比如,可以通过在ApisixRoute资源对象中设置k8s.apisix.apache.org/whitelist-source-rangeannotation来配置白名单。

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 介绍

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

prefixexact两种路径类型可用 默认exact,当需要前缀匹配的时候,就在路径后加 * 比如 /id/* 能匹配所有带 /id/ 前缀的请求。

高级路由特性

基于路径的路由是最普遍的,但这并不够, 再试一下其他路由方式,比如 methodsexprs

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 插件

Websocket 代理

创建一个 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

TCP 路由

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端口。

UDP 路由

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 介绍

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

Mimir 简介

Grafana Mimir 是目前最具扩展性、性能最好的开源时序数据库,Mimir 允许你将指标扩展到 1 亿。它部署简单、高可用、多租户支持、持久存储、查询性能超高,比 Cortex 快 40 倍。 Mimir 托管在 https://github.com/grafana/mimir 并在 AGPLv3 下获得许可。

B站:Grafana Mimir 发布 目前最具扩展性的开源时序数据库

Mimir 是指标领域的一个新项目,站在巨人的肩膀上。为了理解 Mimir,我们需要回顾一下 Cortex 的历史。

源自 Prometheus

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% 的代码提交。

grafana-mimir-devstats-dashboard

来源: 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 许可能平衡商业和开源之间的关系。

介绍 Grafana Mimir

Mimir 集合了 Cortex 中的最佳功能和为 GEM & Grafana Cloud 大规模运行而研发的功能,所有这些都在 AGPLv3 许可下。Mimir 包含以前的商业功能,包括无限制基数(使用水平可扩展的 “split” 压缩器实现)和快速、高基数查询(使用分片查询引擎实现)

产品比较

Cortex、Grafana Mimir 和 Grafana Cloud & Grafana Enterprise Metrics 比较

grafana-mimir-cortex-chart

在从 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 (双激活共享存储方案), 在公司内部大概运行了一年多的时间, 架构图如下:

Active-Active with scale out ability

从图中可以看到, 这种方案基于外部共享存储、外部数据库和 Redis 服务, 构建其两个/以上的 harbor 实例. 既然使用了外部的服务, 那么高可用的压力自然而然的转移到了外部服务上. 我们一开始采用的外部的 NFS 共享存储服务, 由于我们团队实际情况, 我们暂时还不能保证外部存储的高可用. 同时, 鉴于我们对镜像服务高可用的迫切需求, 决定调研新的 Harbor 的高可用方案.

选择了 Solution 4 (双主复制方案), 这个解决方案, 使用复制来实现高可用, 它不需要共享存储、外部数据库服务、外部 Redis 服务. 这种方案可以有效的解决镜像服务的单点故障. 架构图如下:

harbor-dual-master-replication-ha-solution

从图中可以看到, 这种方案仅需要在两个 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 离线版

安装 docker

参考 Install Docker Engine on CentOS 来安装, 因为我是全新的系统, 直接安装:

安装 yum 仓库

安装 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 引擎

安装最新稳定版 Docker 引擎和 containerd

yum install -y docker-ce docker-ce-cli containerd.io

启动 docker 实例并配置开机自动启动

systemctl start docker
systemctl enable docker

优化 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

安装 docker-compose

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 实例

打开 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: 访问 harbor admin ui 和镜像服务的 hostname 或者 ip
  • https:
    • certificate: 线上服务基本都需要要开通 https, 为 https 配置证书路径
    • private_key: 为 https 配置私钥路径
  • external_url: 如果要把 harbor 放在代理的后面, 比如请求会通过 nginx/f5 的代理转发才会到 harbor, 就需要配置该项. 如配置了该项, 上面的 hostname 配置就会失效.
  • database.paasword: 数据库密码, 线上环境必须修改
  • data_volume: 这是 harbor 的数据目录, 默认是 /data, 因为我服务器的数据盘就挂的 /data 目录, 这里就不需要修改了.

下面是默认配置文件, 重点配置我都做了翻译。别看配置文件这么长,重要的都在前 50 行:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# Harbor 配置文件

# 访问管理端 UI 和容器镜像服务使用的 IP 地址或者 hostname
# 禁止使用 localhost 或 127.0.0.1, 因为 Harbor 需要被外部客户端访问
hostname: reg.mydomain.com

# http related config
http:
  # port for http, default is 80. If https enabled, this port will redirect to https port
  port: 80

# https 相关配置
https:
  # harbor https 端口, 默认 443
  port: 443
  # nginx 证书和私钥路径
  certificate: /your/certificate/path
  private_key: /your/private/key/path

# # Uncomment following will enable tls communication between all harbor components
# internal_tls:
#   # set enabled to true means internal tls is enabled
#   enabled: true
#   # put your cert and key files on dir
#   dir: /etc/harbor/tls/internal

# 取消注释会开启外部代理
# 如开启了该配置, 就不会使用 hostname 了
# external_url: https://reg.mydomain.com:8433

# Harbor 管理后台初始密码
# 仅第一次安装 harbor 时有用
# 登录 harbor 管理后台之后, 记得修改 admin 密码
harbor_admin_password: Harbor12345

# Harbor 数据库配置
database:
  # Harbor 数据库 root 用户的密码
  # 上生产环境, 必须要修改
  password: root123
  # 空闲连接池中的最大连接数量
  # 如果 <=0 表示不保留任何空闲连接
  max_idle_conns: 50
  # 数据库开启的最大连接数
  # 如果 <=0, 表示不限制打开连接数
  # 注意: harbor 使用的 postgres 该配置默认是 1024
  max_open_conns: 1000

# 默认数据卷
data_volume: /data

# Harbor Storage settings by default is using /data dir on local filesystem
# Uncomment storage_service setting If you want to using external storage
# storage_service:
#   # ca_bundle is the path to the custom root ca certificate, which will be injected into the truststore
#   # of registry's and chart repository's containers.  This is usually needed when the user hosts a internal storage with self signed certificate.
#   ca_bundle:

#   # storage backend, default is filesystem, options include filesystem, azure, gcs, s3, swift and oss
#   # for more info about this configuration please refer https://docs.docker.com/registry/configuration/
#   filesystem:
#     maxthreads: 100
#   # set disable to true when you want to disable registry redirect
#   redirect:
#     disabled: false

# Trivy configuration
#
# Trivy DB contains vulnerability information from NVD, Red Hat, and many other upstream vulnerability databases.
# It is downloaded by Trivy from the GitHub release page https://github.com/aquasecurity/trivy-db/releases and cached
# in the local file system. In addition, the database contains the update timestamp so Trivy can detect whether it
# should download a newer version from the Internet or use the cached one. Currently, the database is updated every
# 12 hours and published as a new release to GitHub.
trivy:
  # ignoreUnfixed The flag to display only fixed vulnerabilities
  ignore_unfixed: false
  # skipUpdate The flag to enable or disable Trivy DB downloads from GitHub
  #
  # You might want to enable this flag in test or CI/CD environments to avoid GitHub rate limiting issues.
  # If the flag is enabled you have to download the `trivy-offline.tar.gz` archive manually, extract `trivy.db` and
  # `metadata.json` files and mount them in the `/home/scanner/.cache/trivy/db` path.
  skip_update: false
  #
  # insecure The flag to skip verifying registry certificate
  insecure: false
  # github_token The GitHub access token to download Trivy DB
  #
  # Anonymous downloads from GitHub are subject to the limit of 60 requests per hour. Normally such rate limit is enough
  # for production operations. If, for any reason, it's not enough, you could increase the rate limit to 5000
  # requests per hour by specifying the GitHub access token. For more details on GitHub rate limiting please consult
  # https://developer.github.com/v3/#rate-limiting
  #
  # You can create a GitHub token by following the instructions in
  # https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line
  #
  # github_token: xxx

jobservice:
  # Maximum number of job workers in job service
  max_job_workers: 10

notification:
  # Maximum retry count for webhook job
  webhook_job_max_retry: 10

chart:
  # Change the value of absolute_url to enabled can enable absolute url in chart
  absolute_url: disabled

# 日志配置
log:
  # 可选项 debug, info, warning, error, fatal
  level: info
  # 使用 local 存储的日志相关配置
  local:
    # 日志轮转文件数量
    # 日志文件在被删除之前会轮转 rotate_count 次
    # 如果 0 则删除旧版本而不是轮换。
    rotate_count: 50
    # 当日志文件大于 rotate_size 个字节 bytes 时会轮换
    # 如果 size 后跟 k 则表示以 kb 为单位, 也可以跟 M/G
    # 所以 100/100k/100M/200G 都是合法的
    rotate_size: 200M
    # 存储日志的主机目录
    location: /var/log/harbor

  # Uncomment following lines to enable external syslog endpoint.
  # external_endpoint:
  #   # protocol used to transmit log to external endpoint, options is tcp or udp
  #   protocol: tcp
  #   # The host of external endpoint
  #   host: localhost
  #   # Port of external endpoint
  #   port: 5140

#This attribute is for migrator to detect the version of the .cfg file, DO NOT MODIFY!
_version: 2.2.0

# Uncomment external_database if using external database.
# external_database:
#   harbor:
#     host: harbor_db_host
#     port: harbor_db_port
#     db_name: harbor_db_name
#     username: harbor_db_username
#     password: harbor_db_password
#     ssl_mode: disable
#     max_idle_conns: 2
#     max_open_conns: 0
#   notary_signer:
#     host: notary_signer_db_host
#     port: notary_signer_db_port
#     db_name: notary_signer_db_name
#     username: notary_signer_db_username
#     password: notary_signer_db_password
#     ssl_mode: disable
#   notary_server:
#     host: notary_server_db_host
#     port: notary_server_db_port
#     db_name: notary_server_db_name
#     username: notary_server_db_username
#     password: notary_server_db_password
#     ssl_mode: disable

# Uncomment external_redis if using external Redis server
# external_redis:
#   # support redis, redis+sentinel
#   # host for redis: <host_redis>:<port_redis>
#   # host for redis+sentinel:
#   #  <host_sentinel1>:<port_sentinel1>,<host_sentinel2>:<port_sentinel2>,<host_sentinel3>:<port_sentinel3>
#   host: redis:6379
#   password:
#   # sentinel_master_set must be set to support redis+sentinel
#   #sentinel_master_set:
#   # db_index 0 is for core, it's unchangeable
#   registry_db_index: 1
#   jobservice_db_index: 2
#   chartmuseum_db_index: 3
#   trivy_db_index: 5
#   idle_timeout_seconds: 30

# Uncomment uaa for trusting the certificate of uaa instance that is hosted via self-signed cert.
# uaa:
#   ca_file: /path/to/ca

# Global proxy
# Config http proxy for components, e.g. http://my.proxy.com:3128
# Components doesn't need to connect to each others via http proxy.
# Remove component from `components` array if want disable proxy
# for it. If you want use proxy for replication, MUST enable proxy
# for core and jobservice, and set `http_proxy` and `https_proxy`.
# Add domain to the `no_proxy` field, when you want disable proxy
# for some special registry.
proxy:
  http_proxy:
  https_proxy:
  no_proxy:
  components:
    - core
    - jobservice
    - trivy

# metric:
#   enabled: false
#   port: 9090
#   path: /metrics

默认安装

默认安装不含 Notary, Trivy, 或者 Chart 仓库服务, 执行下面的命令:

./install.sh

查看安装状态:

docker ps

如果所有的容器的状态 STATUS 都为 Up About a minute (healthy) 说明安装成功~
打开 harbor admin ui 验证下吧! 别忘了修改 admin 的密码. 使用同样的方式将两台虚拟机的 docker、docker-compose 和 harbor 都安装好.

更改配置

如果需要更改 harbor 的配置, 请按照如下步骤操作:

  1. 停止 harbor
# 首先进入工作目录
cd ~/harbor/
docker-compose down -v
  1. 更新配置文件
vim harbor.yml
  1. 运行脚本生成最终配置
./prepare
  1. 重新启动 harbor 实例
docker-compose up -d
  1. 其他命令
# 重装前清理历史数据
rm -rf /data/database
rm -rf /data/registry
rm -rf /data/redis

配置双主复制

在其中一台 harbor 实例上配置,我以 10.206.99.58 为例,另一实例同理,首先需要创建仓库,点击系统管理>仓库管理>新建目标,按照如下填写:

add-harbor-instance

然后,创建复制规则,点击系统管理>复制管理>新建规则,按照如下填写:

add-replication-rule

这样,当用户往 10.206.99.58 中推送/删除镜像时,10.206.99.57 也会同步发生变化。

增加反向代理

现在两个 harbor 实例都已经配置好了。用户看到的是两个完全独立的 harbor,他们的用户独立,访问地址不同。当然有些场景下这样已经可以满足需求了,比如异地办公的团队(可以按照地域区分使用访问地址)。如果我们想统一访问地址,可以在前面增加一个反向代理。而且可以将 ssl 证书部署在代理上。还是比较推荐的。所以我希望这个代理能实现:

  1. 统一的访问入口: 将两个 harbor 地址统一为一个。
  2. 卸载 ssl 证书: 这将简化 harbor 实例的配置,更易于证书的管理。
  3. 会话保持: 因为 harbor 之间复制是有时间差的,用户往一个实例中推送镜像之后不可能立即在另一实例中拉取到,所以要将客户端的请求固定到一个实例上。

但是很遗憾,harbor 实例之间用户和相关权限是无法同步的。这可能需要需要一些外在的机制实现了。

我假设提供给用户的域名是:registry.example.com,我使用 nginx 作为这个反向代理,它的配置文件/etc/nginx/conf.d/registry.example.com.conf是这样的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
upstream harbor{
    ip_hash;
    server 10.206.99.57;
    server 10.206.99.58;
}

server {
    listen 80;
    server_name registry.example.com;
    rewrite ^(.*)$ https://$host$1;
}

server {
    listen 443 ssl;
    server_name registry.example.com;

    charset utf-8;
    client_max_body_size 0;
    client_header_timeout 180;
    client_body_timeout 180;
    send_timeout 180;
    
    ssl_certificate /etc/nginx/conf.d/cert/registry_example_com.pem;
    ssl_certificate_key /etc/nginx/conf.d/cert/registry_example_com.key;
    ssl_session_timeout 5m;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4:!DH:!DHE;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    proxy_http_version 1.1;
    proxy_connect_timeout 900;
    proxy_send_timeout 900;
    proxy_read_timeout 900;
    proxy_buffering off;
    proxy_request_buffering off;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # 如果harbor实例仅配置了ip类型的hostname这里就不用配置了
    # 如果配置了可解析的hostname/external_url需要打开注释
    # proxy_set_header Host $host;

    # 如果external_url中使用https但是代理访问harbor使用http需要打开注释
    # 同时去掉harbor实例内部的nginx相关的$scheme配置
    # proxy_set_header X-Forwarded-Proto $scheme;

    location / {
        proxy_pass http://harbor;
    }
}

运行 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 实例也同步正常。至此,完成~

总结

至此,所有的安装/配置就结束了,通过体验测试我发现:

  1. 用户是独立的
    两个实例之间的项目、镜像、标签相关资源是可以同步的,但是用户不可以。如果用户要在两个实例直接切换使用的话,需要分别登录两个 harbor admin ui 为用户创建两个相同的账号。所以说该方案比较适合异地办公团队,仅做镜像数据的同步。
  2. 镜像同步有一定的时间差
    我的两个实例是所在虚拟机在一个网段内的,测试了一个约 900M 的镜像,从开始同步到结束大概是10秒种。如果用户在一台实例上推送之后,立马去另一台实例上拉去是不行的。所以如果两个实例前面要增加 http 代理的话,需要使用 ip_hash 负载均衡策略,将用户请求固定到其中一台实例上。
  3. 实例 url 地址不一致
    这个问题不严重,因为是两个实例,如果我们在他们前面再部署 http 代理的话,就是三个地址。所以,两个实例对应 admin ui 上的 url 地址和用户使用的(如果有代理)url 地址都不一样。 比如: harbor-url.jpg

点击查看原文

什么是 eBPF?

eBPF 是革命性技术, 起源于 linux 内核, 能够在操作系统内核中执行沙盒程序. 旨在不改变内核源码或加载内核模块的前提下安全便捷的扩展内核能力.

历史上, 由于内核拥有全局查看并控制整个操作系统的特权, 操作系统一直被认为是实现可观察性, 安全, 网络功能的理想地方. 同时, 由于其核心角色和对于稳定和安全的高要求, 操作系统很难演进. 因此, 传统上与在操作系统之外实现的功能相比, 操作系统级别的创新率较低.

overview

eBPF 从根本上改变了这种一成不变的状态. 通过允许在操作系统中执行沙盒程序, 开发者可以通过执行 eBPF 程序, 来给运行中的操作系统添加额外的能力. 就像在本地使用即时编译器(JIT)和验证引擎一样, 操作系统可以保证安全性和执行效率. 这催生了不少基于 eBPF 的项目, 涵盖了广泛的用例, 包括下一代网络、可观察性和安全功能.

今天, eBPF 被广泛用于各种用例: 在现代化的数据中心和云原生环境中提供高性能网络和负载均衡, 以较低的开销提取细粒度的可观察性安全数据, 帮助应用程序开发者追踪应用, 并能够在性能故障分析、预防性应用和容器运行时安全执法等方面提供帮助. 它的可能性是无限的, 关于 eBPF 的创新才刚开始.

什么是 eBPF.io?

eBPF.io 是以 eBPF 为主题, 每个人学习和协作的地方. eBPF 是一个开源社区, 每个人可以实践或者分享. 不论你是想阅读 eBPF 第一篇介绍文章, 还是发现更多阅读素材, 抑或是为变成 eBPF 主项目贡献者迈出第一步, eBPF.io 会一直陪伴你帮助你.

介绍 eBPF

下面的章节是关于 eBPF 的快速介绍. 如果你想了解更多, 查看 eBPF & XDP Reference Guide. 不管你是一名从事 eBPF 的开发者, 或是有兴趣使用 eBPF 作为解决方案, 理解基础概念和架构都是很有用的.

Hook 概览

eBPF 程序是事件驱动的, 能在内核或应用程序执行到一个特定的 hook 点时执行. 预定义的 hooks 包含系统调用, 函数出/入口, 内核追踪点, 网络事件等等.

syscall_hook

如果预定义 hook 不能满足需求, 也可以创建内核探针(kprobe)或者用户探针(uprobe), 在内核/用户应用程序的任何位置, 把探针附加到 eBPF 程序上.

eBPF 程序怎么写?

在很多场景中, 用户不需要直接使用 eBPF, 而是通过一些项目, 比如 cilium, bccbpftrace, 它们是 eBPF 上层的抽象, 提供了使用 eBPF 实现的特定功能, 用户无需直接编写 eBPF 程序.

clang

如果没有高级抽象, 就需要直接编写 eBPF 程序. Linux 内核要器加载字节码形式的 eBPF 程序. 虽然可以直接编写字节码, 但是更普遍的开发实践是借用像 LLVM 这样的编译器, 把伪 C 代码编译成字节码.

加载器 & 验证架构

当所需的钩子被识别后, 可以使用 bpf 系统调用将 eBPF 程序加载到 Linux 内核中. 这通常使用一个可用的 eBPF 工具库来完成. 下一节将介绍一些可用的开发工具链.

go

当程序加载到 Linux 内核中时, 它在附加到请求的钩子之前要经过两个步骤:

验证

这一步是为了确保 eBPF 程序安全执行. 它验证程序是否满足一些条件, 比如:

  • 加载 eBPF 程序的进程拥有所需的能力(特权). 除非启用非特权 eBPF, 否则只有特权进程才能加载 eBPF 程序.
  • 该程序不能崩溃或者以其他方式伤害操作系统.
  • 该程序必须总是能执行完(即程序不会死循环, 阻止后面的处理).

即时编译 (JIT)

该步骤将通用字节码翻译成机器特定的指令集, 以优化程序的执行速度. 这使 eBPF 程序像原生编译的内核代码或者像已加载的内核模块代码一样高效运行.

Maps

eBPF 程序一个重要能力是: 能够共享收集的信息, 能够存储状态. 为了实现该能力, eBPF 程序借用 Maps 来存储/获取数据, 它支持丰富的数据结构. 通过系统调用, 可以从 eBPF 程序或者用户空间应用访问 maps.

map_architecture

为了解 map 类型的多样性, 下面是不完整的 map 类型列表. 这些类型的变量同时是 共享变量 和 per-CPU 变量.

  • Hash tables, Arrays 哈希表, 数组
  • LRU (Least Recently Used) 最近最少使用
  • Ring Buffer 环形缓冲区
  • Stack Trace 堆栈跟踪
  • LPM (Longest Prefix Match) 最长前缀匹配

帮助函数

eBPF 程序不能随意调用内核函数. 如果允许的话, 将会把 eBPF 程序绑定到特定的内核版本, 这会使程序的兼容性复杂化. 所以, eBPF 程序转而使用帮助函数, 它是内核提供的大家熟知的稳定的 API.

helper

可用的帮助函数还在持续发展中, 例如:

  • 生成随机数
  • 获取当前时间和日期
  • 访问 eBPF map
  • 获取 process/cgroup 上下文
  • 网络数据包处理和转发逻辑

尾调用 & 函数调用

eBPF 程序可以组合使用尾调用和函数调用(tail & function calls). 函数调用允许在 eBPF 程序中定义和调用函数. 尾调用可以调用执行其他 eBPF 程序, 并替换执行上下文, 类似于 execve() 系统调用对常规进程的操作方式.

tailcall

eBPF 安全

权利越大, 责任越大

eBPF 是一项伟大的技术, 当下在很多关键软件中都扮演了核心的角色. 在 eBPF 程序开发过程中, 当 eBPF 进入 Linux 内核时, eBPF 的安全性就变得异常重要. eBPF 的安全性通过下面几点来保证:

要求特权

除非开启非特权 eBPF, 所有企图加载 eBPF 程序到内核的进程必须在特权模式(root)下运行,或者必须获得 CAP_BPF 能力. 这意味着非授信的程序不能加载 eBPF 程序.

如果开启非特权 eBPF, 非特权进程可以加载特定的 eBPF 程序, 它们仅能使用被缩减的功能集合, 并且将受限制的访问内核.

验证器

如果进程允许加载 eBPF 程序, 所有的程序都要经过 eBPF 验证器, 验证器来确保程序本身的安全性. 这意味着:

  • 通过验证的程序一定会执行完, 比如, eBPF 程序不会卡住或死循环. eBPF 程序可以包含有边界的循环, 但是验证器要求, 循环必须具有可以被执行到的退出条件.
  • 程序不能使用任何未初始化的变量或者越界访问内存.
  • 程序必须在系统要求的大小范围内. 随意大的 eBPF 程序是无法加载的.
  • 程序必须具备有限的复杂性. 验证器会评估所有可能的执行路径, 并且必须在配置的复杂度范围内完成分析.

加固

完成验证之后, 根据 eBPF 程序是从特权进程还是非特权进程加载, 来决定是否加固的 eBPF 程序. 这包括:

  • 程序执行保护: 存有 eBPF 程序的内核内存是被保护的并且是只读的. 不管是内核 bug 或者是被恶意操纵, 内核都将崩溃, 而不是允许它继续执行损坏/被操纵的程序.
  • Mitigation against Spectre: Under speculation CPUs may mispredict branches and leave observable side effects that could be extracted through a side channel. 举几个例子: eBPF programs mask memory access in order to redirect access under transient instructions to controlled areas, the verifier also follows program paths accessible only under speculative execution and the JIT compiler emits Retpolines in case tail calls cannot be converted to direct calls.
  • 常量 blinding: 代码中的所有常量都被 blinded, 以防止 JIT spraying 攻击. 这可以避免: 当存在某种内核 bug 的情况下, 攻击者可以把可执行代码作为常量注入, 从而让攻击者跳转到 eBPF 程序的内存区域来执行代码.

抽象的运行时上下文

eBPF 程序不能直接访问任意内核内存. 必须通过 eBPF 助手函数访问位于程序上下文之外的数据和数据结构. 这保证了一致性的数据访问, 并使任何此类访问均受制于 eBPF 程序的权限, 例如如果可以保证修改是安全的, 则允许运行的 eBPF 程序修改某些数据结构的数据. eBPF 程序不能随机修改内核中的数据结构.

为什么使用 eBPF?

可编程的力量

还记得 GeoCities 吗? 20年前, 网页几乎全都是用静态标记语言(HTML)写的, 网页基本上是一种应用程序(浏览器)能打开的文件. 再看今天, 网页已经变成了非常成熟的应用, 并且 WEB 已经取代了绝大部分编译语言写的应用. 是什么成就了这次革命?

geocities

简单来说, 就是引入 JavaScript 之后的可编程性. 它开启了一场大规模的革命, 几乎将浏览器变成了独立的操作系统.

为什么呢? 程序员不再受限于特定的浏览器版本. 没有去说服标准机构去定义更多需要的 HTML 标签, 相反, 而是提供了一些必要的构建模块, 将浏览器底层的演进和运行在其上层的应用进行分离. 这样说可能过于简单, 因为 HTML 的确做了不小的贡献, 也的确有所发展, 但是 HTML 本身的变革还不够.

在举这个例子并将其应用到 eBPF 之前, 让我们看一下对引入 JavaScript 至关重要的几个关键方面:

  • 安全性: 不受信任的代码在用户的浏览器中运行. 这是通过沙盒 JavaScript 程序和抽象对浏览器数据的访问来解决的.
  • 持续交付: 在不需要浏览器发新版本的情况下, 程序要能不断更新. 这得益于浏览器低级的(low-level)构建模块, 它能构建任意的逻辑.
  • 性能: 必须以最小的开销提供可编程性. 这得益于即时编译器(JIT).

上面说的所有内容, 在 eBPF 中都能找到:

eBPF 对 Linux 内核的影响

现在我们回到 eBPF. 为了理解 eBPF 可编程性在 Linux 内核上的影响, 我们来看张图片, 它有助于我们对 Linux 内核的架构进行理解, 并且能了解它是如何与应用程序和硬件进行交互的.

kernel_arch

Linux 内核的主要目的是抽象硬件或虚拟硬件, 并提供一致的 API(系统调用), 允许应用程序运行和共享资源. 为了实现这一点, 维护了大量的子系统和层来分配这些职责. 每个子系统通常允许某种级别的配置来满足不同的用户需求. 如果没办法通过配置满足某种需求, 则需要更改内核. 从历史上看, 有两种选择:

原生支持 内核模块
1. 更改内核源代码并说服 Linux 内核社区 1. 写一个新的内核模块
2. 等几年新内核版本上市 2. 定期修复它, 因为每个内核版本都可能破坏它
3. 由于缺乏安全边界, 有损坏 Linux 内核的风险

在不需要改变内核源码或者加载内核模块的情况下, eBPF 为重新编程内核行为提供了一种新的选择. 在很多地方, 这很像 JavaScript 和其他脚本语言, 它们让那些改变难度大, 成本高的系统开始演进.

开发工具链

有几个开发工具链来能够协助 eBPF 程序的开发和管理. 它们能满足用户的不同需求:

bcc

BCC 是一个框架, 能够让用户编写嵌入了 eBPF 程序的 python 程序. 该框架主要用来分析和跟踪应用/系统, eBPF 在其中主要负责收集统计数据或生成事件, 然后, 对应的用户空间程序会收集这些数据并以易读的方式进行展示. 运行 python 程序会生成 eBPF 字节码并将其加载进内核.

bcc

bpftrace

bpftrace 是 Linux eBPF 的高级跟踪语言, 可用于最新的 Linux 内核(4.x). bpftrace 使用 LLVM 作为后端将脚本编译为 eBPF 字节码,并利用 BCC 与 Linux eBPF 子系统以及现有的 Linux 跟踪功能进行交互: 内核动态跟踪(kprobes)、用户级动态跟踪(uprobes)和跟踪点(tracepoints). bpftrace 语言的灵感来自 awk、C 和以前的跟踪器(如 DTrace 和 SystemTap).

bpftrace

eBPF Go 类库

eBPF Go 库提供了一个通用的 eBPF 库, 它将获取 eBPF 字节码的过程与 eBPF 程序的加载和管理分离. eBPF 程序通常是通过编写高级语言创建的, 然后使用 clang/LLVM 编译器编译为 eBPF 字节码.

go

libbpf C/C++ 类库

libbpf 库是一个基于 C/C++ 的通用 eBPF 库. 它提供给应用程序一种易用的 API 来抽象化 BPF 系统调用, 并将 eBPF 字节码(clang/LLVM 编译器生成)加载到内核的过程与之分离.

libbpf

阅读更多

如果你想学习更多的 eBPF 知识, 阅读下面的材料:

文档

教程

发言

基础

深入

Cilium

Hubble

图书

文章 & 博客

这篇文档主要演示了 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 可用于发现配置和日志文件.

过滤 PID

-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 文件.

包含/过滤 UID

-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

过滤失败的 opens

-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

基于 cgroup 集进行过滤

–cgroupmap 选项基于 cgroup 集进行过滤, 它用于使用外部创建的映射.

# ./opensnoop --cgroupmap /sys/fs/bpf/test01

更多信息, 查看 docs/special_filtering.md

USAGE 说明

# ./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
[...]

USAGE 说明

# ./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

PPT 分享

以下是 <实践中总结 Kubernetes 必须了解的核心内容> 主题分享 PPT

ppt-page-1


ppt-page-3


ppt-page-4


ppt-page-5


ppt-page-6


ppt-page-7


ppt-page-8


ppt-page-9


ppt-page-10


ppt-page-11


ppt-page-12


ppt-page-13


ppt-page-14


ppt-page-15


ppt-page-16


ppt-page-17


ppt-page-18


ppt-page-19


ppt-page-20


ppt-page-21


ppt-page-22


ppt-page-23


ppt-page-24


ppt-page-25


ppt-page-26


ppt-page-27


ppt-page-28


ppt-page-29


ppt-page-30


ppt-page-31


ppt-page-32


ppt-page-33


ppt-page-34


ppt-page-35


ppt-page-36


ppt-page-37


ppt-page-38


ppt-page-39


完~

前言

kubernetes 中 pod 的设计是一个伟大的发明, 今天我很有必要去聊一下 pod 和 container, 探究一下它们究竟是什么? kubernetes 官方文档中关于pod 概念介绍提供了一个完整的解释, 但写的不够详细, 表达过于专业, 但还是很推荐大家阅读一下. 当然这篇文档应该更接地气.

容器真的存在吗?

linux 中是没有容器这个概念的, 容器就是 linux 中的普通进程, 它使用了 linux 内核提供的两个重要的特性: namespace & cgroups.

namespace 提供了一种隔离的特性, 让它之外的内容隐藏, 给它下面的进程一个不被干扰的运行环境(其实不完全,下面说) .

namespace 包含:

  • hostname
  • Process IDs
  • File System
  • Network Interface
  • Inter-Process Communication(IPC)

接上面, 其实 namespace 内部的进程并不是完全不和外面的进程产生影响的. 进程可以不受限制的使用物理机上的所有资源, 这样就会导致其他进程无资源可用. 所以, 为了限制进程资源使用, linux 提供了另一种特性 cgroups. 进程可以像在 namespace 中运行, 但是 cgroups 限制了进程的可以使用的资源. 这些资源包括:

  • CPU
  • RAM
  • block I/O
  • network I/O
  • etc.

CPU 通常按照毫核来限制(单位:m), 1000m=1C; 内存按照RAM的字节数来限制. 进程可以在 cgroups 设置的资源限制范围内运行, 不允许超限使用, 比如, 超过内存限制就会报 OOM(out of memory) 的错误.

需要特别说明的是, 上面提到的 namespace & cgroup 都是 Linux 独立的特性, 你可以使用上面提到的 namespace 中的一个或者多个. namespace & cgroup 作用到一组或者一个进程上. 你可以把多个进程放在一个 namespace 中, 这样它们就可以彼此交互, 或者 把他们放在一个 cgroups 中, 这样他们就可以共享一个CPU & Mem 资源限制.

组合容器

我们都用过 docker, 当我们启动一个容器的时候, docker 会帮我们给每一个容器创建它们自己的 namespace & cgroups. 这应该就是我们理解的容器.

image-20210809102424781

如图, 容器本身还是比较独立的, 他们可能会有映射到主机的端口和卷, 这样就可以和外面通信. 但是我们也可以通过一些命令将多个容器组合到一组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 中, 容器彼此之间可以互相发现/通信.

就像这样:

image-20210809134007587

某种意义上, pod 就是一组容器

现在我们已经知道, 我们可以把一组进程组合到一个 namespace & cgroups 中, 这就是 kubernetes 中的 pod. pod 允许你定义你要运行的容器, 然后 kubernetes 会帮正确的配置 namespace & cgroups. 它稍微复杂的一点是, 网络这块它没用 docker network, 而是用到了 CNI(通用网络接口), 但原理都差不多.

image-20210809135653975

按照上述方式创建的 pod, 更像是运行在同一台机器上, 他们之间可以通过 localhost 通信, 可以共享存储卷. 甚至他们可以使用 IPC 或者互相发送 HUP / TERM 这类信号.

我们再举个例子, 如下图, 我们运行一个 nginx 反向代理 app, 再运行一个 confd, 当 app 实例增加或减少的时候去动态配置 nginx.conf 并重启 nginx, etcd 中存储了 app 的 ip 地址. 当 ip 列表发生变化, confd 会收到 etcd 发的通知, 并更新 nginx.conf 并给 nginx 发送一个 HUP 信号, nginx 收到 HUP 信号会重启.

image-20210809141738720

如果用 docker, 你大概会把 nginx 和 confd 放在一个容器中. 由于 docker 只有一个 entrypoint, 所以你要启动一个类似 supervisord 一样的进程管理器 来让 nginx 和 confd 都运行起来. 你每启动一个 nginx 副本就要启动一个 supervisord, 这不好吧. 更重要的是, docker 只知道 supervisord 的状态, 因为它只有一个 entrypoint. 它看不到里面的所有进程, 这就意味着, 你用 docker 提供的工具获取不到他们的信息. 一旦 nginx Crash-Restart Loop, docker 一点办法没有.

image-20210809142718304

通过 pod , kubernetes 能管理每一个进程, 看到他们的状态, 它可以通过 api 将进程状态信息暴露给用户, 或者提供进程崩溃时重启/记录日志等服务.

image-20210809143225671

把容器当作接口

使用 pod 这种组织容器的方式, 可以把容器当作提供各种功能的 “接口”. 它不同于传统意义上的 web 接口. 更像是可以被容器所使用的某种抽象意义的接口.

我们拿上面 nginx+confd 的例子来说, confd 不需要知道任何 nginx 进程的东西, 它就只需要去 watch etcd 然后给 nginx 进程发送 HUP 信号或者执行个命令. 而且你可以把 nginx 替换成其他任何类型的应用, 以这样的模式来使用 confd 的这种能力. 这种模式下, confd 通常被称作 “sidecar container” 边车容器, 下面这图就很形象.

image-20210809150256978

像 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?