404频道

学习笔记

去年装修的时候预留了投影仪和幕布的位置,搬家已经有几个月的时间了,最近开始考虑安装投影仪。作为技术男,已经有多年不看电视,在决策前自然要考察下市场,了解下当前的技术,综合考虑多种技术因素。

投影仪的选型

家装投影仪分为了两大阵营:

  1. 以传统厂商为代表的 3LCD 阵营。典型的厂家为爱普生、明基。优点为清晰度高、对比度高。缺点为噪音大、灯泡寿命短、个头偏大。
  2. 以国产新势力厂商为代表的 DLP 阵营。典型厂家为极米、当贝、坚果,无论其市场分布,还是技术形态,都像极了新能源领域的新势力”蔚小理“。优点为自带 android 系统、更加人性化、寿命长。缺点为亮度低。

我的需求优先级依次如下:

  1. 亮度高,抛开亮度和对比度谈体验就本末倒置了,电视的最基本需求就是看得见,看得清楚。
  2. 最好不要带 android 系统,时间长了肯定会卡,不希望跟 android 系统绑定。
  3. 开机不要有广告。

经过对比后,选择了爱普生的 3LCD 投影仪,亮度完全满足需求,即使在白天不拉窗帘的情况下,清晰度也完全足够。选择了其优点,自然要忍受其缺点,噪音大也并没有大到不能忍受,一般播放片源后就可以将风扇的声音盖住,在投影仪旁实测噪音在 40 分贝左右。灯泡寿命短的缺点暂时忽略,毕竟官方说明灯泡的寿命在三年到六年之间,而且更换灯泡的费用并不高。

电视盒子的选型

有了投影仪后,还缺一个电视盒子用于提供视频源,电视盒子的选择比投影仪更加丰富。

Apple TV

第一优先级考虑为地表最强电视盒子 Apple TV,无论其流畅度、清晰度、用户体验都堪称完美。主要的优点:

  1. 强大的 Infuse 软件用来查看蓝光光源、杜比视界,绝对不是 1080P 的片源可以比的。
  2. 苹果一如既往丝滑般的体验。无论是操作系统,还是遥控器,其用户体验都能吊打一众安卓盒子。
  3. 可在 Netflix、Disney+、Apple TV+ 追剧。要想在设备上播放 Netflix,是需要授权的,国内的电视盒子一概不支持。
  4. 无广告。

虽然想体验一把最好的看电视体验,但存在如下两个问题导致我最终放弃了 Apple TV:

  1. 无法看国内的”爱优腾“。投影仪的主要使用方并非我自己,而更多的是家人,没有国内的”爱优腾“,查看片源过于复杂,使用体验将大打折扣。
  2. Netflix 和 Disney+ 并非强需求。Apple TV 用来追剧非常方便,但我个人并没有太多的时间用来追剧,而且还需要魔法,追剧还需要付费,使用成本整体并不低。

国产电视盒子

网上可以买到各式各样的电视盒子,当贝、小米、腾讯极光等,整体特点:

  1. 均基于 Android TV 系统,又叠加了自己的桌面应用。
  2. 厂商均不靠硬件赚钱,而是靠 VIP 会员赚钱,因此所有的电视盒子均需要充值 VIP,没有了 VIP 也就剩下一个盒子了,基本上看不了多少片源。
  3. 各种开机广告、开屏广告满天飞。

下单销量排名靠前的当贝盒子后,体验一番后,大失所望。虽然如宣传的一般没有了开机广告,但进入系统后没有 VIP 寸步难行,每个视频右下角都会带一个灰色的 VIP logo。我的选择逻辑,既然”爱优腾“的会员在所难免,毕竟还有手机端 app 看视频的需求,再买一个盒子 VIP 的意义何在。如果盒子 VIP 不买,那盒子自身系统提供的片源很多就是个摆设。实在无法容忍,遂退之。

国产的电视盒子没有一个能打的,整个产品的定位全跑偏了。如果能有一个干净、用户体验好的电视盒子可以给到用户,哪怕卖得贵一点(毕竟就要靠卖硬件赚钱了),我相信也是有市场的。

树莓派

看了下 Android TV 的原生系统后,更加符合自己的需求,干净清爽,而且还可以刷 Google 服务包,用来安装 Google 全家桶应用。所以就开始考虑自己刷机使用 Android TV。要想刷机必须先有盒子,盒子的选型有如下几个:

  1. 国产电视盒子。可在国产电视盒子的硬件上刷 Android TV 系统。
  2. 其他 Arm 架构的盒子。比如非常流行的斐讯 N1 盒子,曾经用于挖矿,现在却在软路由领域焕发出了新的生机。
  3. 开发版。最为流行的树莓派,还有友善 NanoPi 开发版、Banana Pi,均为基于 ARM 架构。

我最终选择了树莓派 5 作为电视盒子,主要考虑如下:

  1. 性能强大,用来作为电视盒子性能是过剩的。
  2. 可玩性强。即使作为电视盒子翻车了,还可以用来作为小型服务器,跑 Home Assiant、TeslaMate 等应用,甚至可以作为 NAS 使用。
  3. 用户基数大。自己踩到的坑更容易找到解决方案。

总结

投影仪完成后,且电视盒子也选好了,接下来将另起一篇文章讲解下我的树莓派刷 Android TV 系统的折腾经历,自己选择的路线,查再多的资料,刷再多的机,也要折腾成功。

在上篇文章《红米路由器 AX6000 解锁 SSH》中,解锁了红米路由器 AX6000 的 SSH 功能,本文在此基础上安装 clash,用来科学上网。

安装 clash

通过 ssh 命令连接到红米路由器,执行如下 shell 命令,即可下载并安装 clash 软件:

1
2
3
4
mkdir -p /data/openclash && cd /data/openclash
export url='https://raw.fastgit.org/juewuy/ShellClash/master'
curl -kfsSl $url/install.sh > install.sh
sh install.sh

会出现如下的提示:
image.png
install.sh 会将 ShellCrash 安装到 /data/ShellCrash 目录下。

clash 配置

在安装完 clash 后,输入 clash 命令即可对 clash 进行管理,该操作有点类似于打客户电话的操作。
image.png

clash UI

通过上述 clash 命令可以安装 clash UI,可以通过网址 http://192.168.31.1:9999/ui 访问。UI 的功能较 clash 的客户端更为简单,但也可以弥补上述 clash 命令的很多不足之处。
image.png

资料

https://www.youtube.com/watch?v=e90UWDpAlxA

半年前购买了红米路由器 AX6000,用起来一直非常稳定。因为最近在折腾树莓派,准备将红米路由器上开启 SSH 的功能。最早是准备将路由器刷成 OpenWRT 系统,当一旦路由器能够开启 SSH 功能后,刷 OpenWRT 系统的必要性就不是太大了。

声明:本文下面操作步骤均为从网络上获取的现有操作步骤。

在电脑的浏览器上登录红米路由器的管理页面,红米路由器管理页面为:https://miwifi.com/,或者 http://192.168.31.1/。如果本地设置过其他的 DNS 服务器,需要使用 ip 地址的形式访问。
登录后可以获取到当前红米路由器的版本,我的已经是最新的 1.0.67,该版本的固件可以支持开启 SSH 协议。
image.png
在登录红米路由器管理页面后,查看浏览器的地址栏,https://miwifi.com/cgi-bin/luci/;stok=5e55c58e949d5419c001bce8288e5a27/web/home#router,可以看到参数 stok 5e55c58e949d5419c001bce8288e5a27 即我们需要用到该值。需要注意的是参数 stok 在每次登录时均会发生改变。

用浏览器打开下面的网址,其中 stok 为上面步骤获取到的值。

1
http://192.168.31.1/cgi-bin/luci/;stok=5e55c58e949d5419c001bce8288e5a27/api/misystem/set_sys_time?timezone=%20%27%20%3B%20zz%3D%24%28dd%20if%3D%2Fdev%2Fzero%20bs%3D1%20count%3D2%202%3E%2Fdev%2Fnull%29%20%3B%20printf%20%27%A5%5A%25c%25c%27%20%24zz%20%24zz%20%7C%20mtd%20write%20-%20crash%20%3B%20

浏览器如果返回{"code":0},说明该步骤执行成功。

在浏览器执行路由器重启操作,返回 {"code":0},说明该步骤执行成功,此时路由器会发生重启。

1
http://192.168.31.1/cgi-bin/luci/;stok=5e55c58e949d5419c001bce8288e5a27/api/misystem/set_sys_time?timezone=%20%27%20%3b%20reboot%20%3b%20

重新登录红米路由器,获取到新的 stok 值 221bcd3ba09b3e4597b0308cc1df18a2。
在浏览器继续访问如下的地址,用来开启 telnet,成功后仍然返回 {"code":0}

1
http://192.168.31.1/cgi-bin/luci/;stok=221bcd3ba09b3e4597b0308cc1df18a2/api/misystem/set_sys_time?timezone=%20%27%20%3B%20bdata%20set%20telnet_en%3D1%20%3B%20bdata%20set%20ssh_en%3D1%20%3B%20bdata%20set%20uart_en%3D1%20%3B%20bdata%20commit%20%3B%20

再继续执行重启命令

1
http://192.168.31.1/cgi-bin/luci/;stok=221bcd3ba09b3e4597b0308cc1df18a2/api/misystem/set_sys_time?timezone=%20%27%20%3b%20reboot%20%3b%20

在重启完成后,路由器的 telnet 服务已经开启。在终端中执行 telnet 192.168.31.1 即可远程连接到小米路由器,而且不需要密码。

在终端中执行如下的命令,用来设置 root 密码,开启并固化 ssh 服务:

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
# 修改 root 密码
echo -e 'wangss19881.\nwangss19881.' | passwd root

# 开启 ssh 服务
nvram set ssh_en=1
nvram set telnet_en=1
nvram set uart_en=1
nvram set boot_wait=on
nvram commit

sed -i 's/channel=.*/channel="debug"/g' /etc/init.d/dropbear
/etc/init.d/dropbear restart

# 设置 ssh 服务开机自启动
mkdir /data/auto_ssh
cd /data/auto_ssh
curl -O https://fastly.jsdelivr.net/gh/lemoeo/AX6S@main/auto_ssh.sh
chmod +x auto_ssh.sh
uci set firewall.auto_ssh=include
uci set firewall.auto_ssh.type='script'
uci set firewall.auto_ssh.path='/data/auto_ssh/auto_ssh.sh'
uci set firewall.auto_ssh.enabled='1'
uci commit firewall

# 设置时区
uci set system.@system[0].timezone='CST-8'
uci set system.@system[0].webtimezone='CST-8'
uci set system.@system[0].timezoneindex='2.84'
uci commit

# 关闭开发模式
mtd erase crash

reboot

在终端中执行 ssh -o HostkeyAlgorithms=+ssh-rsa -o PubkeyAcceptedKeyTypes=+ssh-rsa root@192.168.31.1即可 ssh 连接到路由器。

进入到系统后我们可以看到系统为基于 Linux 5.4 内核版本的 XiaoQiang 系统,至于名字为什么叫 XiaoQiang 我还不得而知。

1
2
root@XiaoQiang:~# uname -a
Linux XiaoQiang 5.4.150 #0 SMP Mon Jan 30 09:23:25 2023 aarch64 GNU/Linux

整个磁盘分区的使用情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root@XiaoQiang:~# df -h
Filesystem Size Used Available Use% Mounted on
/dev/root 15.5M 15.5M 0 100% /
tmpfs 240.0M 9.9M 230.1M 4% /tmp
ubi1_0 40.4M 7.6M 30.7M 20% /data
ubi1_0 40.4M 7.6M 30.7M 20% /userdisk
/dev/root 15.5M 15.5M 0 100% /userdisk/data
ubi1_0 40.4M 7.6M 30.7M 20% /etc/config
ubi1_0 40.4M 7.6M 30.7M 20% /etc/datacenterconfig
ubi1_0 40.4M 7.6M 30.7M 20% /etc/smartcontroller
ubi1_0 40.4M 7.6M 30.7M 20% /etc/parentalctl
ubi1_0 40.4M 7.6M 30.7M 20% /etc/smartvpn
ubi1_0 40.4M 7.6M 30.7M 20% /etc/ppp
ubi1_0 40.4M 7.6M 30.7M 20% /etc/crontabs
tmpfs 512.0K 12.0K 500.0K 2% /dev

使用 top 命令查看整体的系统负载情况,cpu 利用率还是非常低。

1
2
3
Mem: 325692K used, 165808K free, 11208K shrd, 12652K buff, 76388K cached
CPU: 0.3% usr 1.3% sys 0.0% nic 97.8% idle 0.0% io 0.0% irq 0.4% sirq
Load average: 1.05 1.06 1.12 2/147 28107

资料

https://www.youtube.com/watch?v=u5Qg4zqj_V0
https://uzbox.com/tech/openwrt/ax6000.html
https://github.com/kjfx/AX6000/releases/tag/RedmiAX6000

术语

  • host cluster:virtual cluster 中的宿主 k8s 集群,承载了所有的计算资源。也会被叫做 super cluster。
  • virtual cluster:virtual cluster 中的租户 k8s 集群,通常简写 vc。也会被叫做 tenant cluster。
  • vCluster:k8s virtual cluster 的实现之一,即本文中要介绍的方案。

项目简介

k8s 的多租功能

image.png
k8s 自身在多租的能力上支持较差,提供了 namespace 级别的隔离,不同的租户使用不同的 namespace,但该隔离功能较弱。很多组件部署在同一个 workload 会存在诸多问题:

  1. 使用全局对象存在冲突,比如 CRD。
  2. 存在诸多安全性问题,比如多个租户之间的 pod 完全可以互访,没有任何隔离机制。
  3. 不同组件对于 k8s 的版本不统一。

为了解决多租的问题,最简单的思路就是使用多 k8s 集群,业界的 KubeFed v2、karmada、clusternet、OCM 等均为多 k8s 集群的实现。但多 k8s 集群因为存在独立的控制面和计算资源,存在资源消耗过多的问题。

还有一个中间思路为仅做 k8s 的控制面隔离,计算资源仍然共享,即 pod 也可以解决很多的多租隔离问题。k8s 的控制面隔离又存在两个主要方案:

  1. 独立的 kube-apiserver 和 etcd、kube-controller-manager, kube-scheduler 共享 host cluster。该方案中有独立的 kube-apiserver 组件,这里的 etcd 可以被 sqllite、mysql 等存储取代。该方案统称为 virtual cluster,简称为 vc。
  2. 独立的 proxy apiserver,kube-apiserver、kube-controller-manager、kube-scheduler 共享 host cluster。访问 k8s 的请求先到 proxy apiserver,proxy apiserver 转发到 host cluster 的 kube-apiserver。可以在 proxy apiserver 中提供独立的 RBAC 机制,实现一定程度的隔离。该方案在开源中未看到具体的实现。

vCluster 介绍

vCluster 为 virtual cluster 的开源实现之一,由 Loft Labs 提供,Github Star 3.7K,代码行数 4 万行。除了开源版本外,还提供了商业版本 vCluster PRO。

vCluster 设计原则:

  1. 最小化资源占用。在实现上使用了单 pod 的 k3s 作为 k8s 的控制面。
  2. 复用 host cluster 的计算、存储和网络资源。
  3. 降低对 host cluster 的请求。
  4. 简单灵活。
  5. 不需要 host cluster 的管理员权限。
  6. vCluster 多个 namespace 下的对象映射到 host cluster 的同一个 namespace 下。同时也可以支持 vCluster 的一个 namespace 对应 host cluster 的一个 namespace。
  7. 易清理。

Getting Started

k8s 集群准备

k8s 集群这里使用了 kind 方案,kind 配置 kind.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
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: vcluster
nodes:
- role: control-plane
# 如果需要 ingress,则需要指定该参数
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings:
- containerPort: 80
hostPort: 80
protocol: TCP
- containerPort: 443
hostPort: 443
protocol: TCP
# 指定 k8s 版本,默认不指定
# image: kindest/node:v1.23.17
- role: worker
- role: worker
- role: worker
networking:
apiServerPort: 6443

执行 kind create cluster --config kind.conf 即可创建 k8s 集群,包含了一个 control-plane 节点,三个 worker 节点。

安装 vcluster

vCluster 提供了使用 vcluster cli、helm 和 kubectl 三种安装方式,使用 vcluster cli 最为简单,其底层同样采用 helm chart 的方式部署,下面采用 vcluster cli 的方式进行安装。
安装 vcluster cli 工具:

1
curl -L -o vcluster "https://github.com/loft-sh/vcluster/releases/latest/download/vcluster-darwin-arm64" && sudo install -c -m 0755 vcluster /usr/local/bin && rm -f vcluster

或者执行 brew install vcluster安装 vcluster 命令行工具。

执行命令 vcluster create my-vcluster 创建 virtual cluster。会在 host cluster 上创建 namespace vcluster-my-vcluster,该 namespace 下创建如下对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ kubectl get all -n vcluster-my-vcluster
NAME READY STATUS RESTARTS AGE
pod/coredns-68559449b6-l5whx-x-kube-system-x-my-vcluster 1/1 Running 2 (5m45s ago) 3d
pod/my-vcluster-0 1/1 Running 2 (5m45s ago) 3d

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kube-dns-x-kube-system-x-my-vcluster ClusterIP 10.96.67.119 <none> 53/UDP,53/TCP,9153/TCP 3d
service/my-vcluster NodePort 10.96.214.58 <none> 443:30540/TCP,10250:31621/TCP 3d
service/my-vcluster-headless ClusterIP None <none> 443/TCP 3d
service/my-vcluster-node-vcluster-control-plane ClusterIP 10.96.69.115 <none> 10250/TCP 3d

NAME READY AGE
statefulset.apps/my-vcluster 1/1 3d

可以看到在该 namespace 下创建了 coredns 和 StatefulSet my-vcluster。每个租户有独立的 coredns 组件,用来做域名解析。my-vcluster 为 vCluster 的管控面组件,包括了 k8s controller plane 和 syncer 组件。

执行 vcluster connect my-vcluster 后会在本地启动代理,并自动切换本地的 kubeconfig context,将 context 切换到 virtual cluster。执行 kubectl 命令即可连接到对应的 k8s 集群。virtual cluster 集群中的信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ kubectl get all -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system pod/coredns-68559449b6-jg2bs 1/1 Running 1 (20m ago) 51m

NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-system service/kube-dns ClusterIP 10.96.120.59 <none> 53/UDP,53/TCP,9153/TCP 51m
default service/kubernetes ClusterIP 10.96.35.53 <none> 443/TCP 51m

NAMESPACE NAME READY UP-TO-DATE AVAILABLE AGE
kube-system deployment.apps/coredns 1/1 1 1 51m

NAMESPACE NAME DESIRED CURRENT READY AGE
kube-system replicaset.apps/coredns-68559449b6 1 1 1 51m

在 virtual cluster 可以看到仅包含了 coredns 组件。

在 virtual cluster 和在 host cluster 中的 node 信息:

1
2
3
4
5
6
7
8
9
10
$ kubectl get node -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
vcluster-worker3 Ready <none> 51m v1.27.3 10.96.118.228 <none> Debian GNU/Linux 11 (bullseye) 5.10.76-linuxkit containerd://1.7.1

$ kubectl get node -o wide --context kind-vcluster
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
vcluster-control-plane Ready control-plane 86m v1.27.3 172.19.0.3 <none> Debian GNU/Linux 11 (bullseye) 5.10.76-linuxkit containerd://1.7.1
vcluster-worker Ready <none> 85m v1.27.3 172.19.0.2 <none> Debian GNU/Linux 11 (bullseye) 5.10.76-linuxkit containerd://1.7.1
vcluster-worker2 Ready <none> 85m v1.27.3 172.19.0.5 <none> Debian GNU/Linux 11 (bullseye) 5.10.76-linuxkit containerd://1.7.1
vcluster-worker3 Ready <none> 85m v1.27.3 172.19.0.4 <none> Debian GNU/Linux 11 (bullseye) 5.10.76-linuxkit containerd://1.7.1

可以看到在 virtual cluster 和 host cluster 中的 node 名字相同,这是因为 node 在 vCluster 中并没有做隔离,而是从 host cluster 中做了同步。但 virtual cluster 中的 node 节点仅包含 pod 在 host cluster 中已经使用的 node 节点,未使用的节点并不会在 virtual cluster 上。
同时可以看到 vc 和 host cluster 中的 node ip 地址并不相同,vc 中的 node ip 地址跟 host cluster 中的对应 service clusterip 相同,在 host cluster 中的对应 service 名字为 $vClusterName-node-$hostClusterNodeName

1
2
3
$ kubectl get svc --context kind-vcluster -n vcluster-my-vcluster my-vcluster-node-vcluster-worker3
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
my-vcluster-node-vcluster-worker3 ClusterIP 10.96.118.228 <none> 10250/TCP 3h54m

在 virtual cluster 中创建 k8s 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 在 virtual cluster 创建 namespace
$ kubectl create ns demo-nginx
namespace/demo-nginx created

# 创建 Deployment
$ kubectl create deployment nginx-deployment -n demo-nginx --image=nginx

# 在 virtual cluster 上创建出了 pod
$ kubectl get pod -n demo-nginx -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-deployment-66fb7f764c-dn59g 1/1 Running 0 11m 10.244.0.7 vcluster-control-plane <none> <none>

# 由于 pod 调度了 host cluster 新节点,在 virtual cluster 中可以看到新的 k8s node,k8s node 为刚刚创建
$ kubectl get node
NAME STATUS ROLES AGE VERSION
vcluster-worker2 Ready <none> 2m45s v1.27.3
vcluster-worker3 Ready <none> 57m v1.27.3

在 host cluster 中看到如下对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 在 host cluster 上并没有对应的 namespace demo-nginx
$ kubectl get ns --context kind-vcluster demo-nginx
Error from server (NotFound): namespaces "demo-nginx" not found

# 在 host cluster 上并没有对应的 deployment nginx-deployment
$ kubectl get deploy -A --context kind-vcluster
NAMESPACE NAME READY UP-TO-DATE AVAILABLE AGE
ingress-nginx ingress-nginx-controller 1/1 1 1 90m
kube-system coredns 2/2 2 2 91m
local-path-storage local-path-provisioner 1/1 1 1 91m

# 但在 host cluster 上却看到了对应的 pod,位于 vcluster 统一的 namespace vcluster-my-vcluster 之下
$ kubectl get pod -n vcluster-my-vcluster --context kind-vcluster
NAME READY STATUS RESTARTS AGE
coredns-68559449b6-jg2bs-x-kube-system-x-my-vcluster 1/1 Running 1 (26m ago) 57m
my-vcluster-0 1/1 Running 1 (26m ago) 86m
nginx-deployment-66fb7f764c-sffqt-x-demo-nginx-x-my-vcluster 1/1 Running 0 2m22s

可以看到仅 pod 在 host cluster 中同步存在,而 namespace、deployment 这些对象仅存在于 virtual cluster 中。在 virtual cluster 中创建的多个不同 namespace pod 仅会存在于 host cluster 的同一个 namespace 下。

验证 service

在 virtual cluster 中创建 service 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Service
metadata:
labels:
app: nginx
kubernetes.io/cluster-service: "true"
name: nginx
namespace: demo-nginx
spec:
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- name: http
port: 80
protocol: TCP
targetPort: 80
selector:
app: nginx-deployment
type: ClusterIP

在 virtual cluster 中包含如下的 Service 对象:

1
2
3
$ kubectl get svc -n demo-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx ClusterIP 10.96.239.53 <none> 80/TCP 2m11s

在 host cluster 中会同步创建如下的 Service 对象,内容如下:

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
apiVersion: v1
kind: Service
metadata:
annotations:
vcluster.loft.sh/object-name: nginx
vcluster.loft.sh/object-namespace: demo-nginx
vcluster.loft.sh/object-uid: 5ab7aa9c-90b6-46f9-a162-9ea9ca9826f3
creationTimestamp: "2023-11-28T09:32:22Z"
labels:
vcluster.loft.sh/label-my-vcluster-x-a172cedcae: nginx
vcluster.loft.sh/label-my-vcluster-x-d9125f8911: "true"
vcluster.loft.sh/managed-by: my-vcluster
vcluster.loft.sh/namespace: demo-nginx
name: nginx-x-demo-nginx-x-my-vcluster
namespace: vcluster-my-vcluster
ownerReferences:
- apiVersion: v1
controller: false
kind: Service
name: my-vcluster
uid: 463a503e-d889-49a7-94e0-0cba5299dd47
resourceVersion: "12344"
uid: fc9ea383-996e-471c-9c27-ee1c22fec7a3
spec:
clusterIP: 10.96.239.53
clusterIPs:
- 10.96.239.53
internalTrafficPolicy: Cluster
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- name: http
port: 80
protocol: TCP
targetPort: 80
selector:
vcluster.loft.sh/label-my-vcluster-x-a172cedcae: nginx-deployment
vcluster.loft.sh/managed-by: my-vcluster
vcluster.loft.sh/namespace: demo-nginx
sessionAffinity: None
type: ClusterIP
status:
loadBalancer: {}

可以看到 host cluster 中的 service 的 ClusterIP 跟 virutal cluster 一致,但 spec.selector 字段已经被 syncer 修改,以便可以匹配到正确的 pod。

验证 Ingress

默认情况下 Ingress 不会同步到 host cluster,需要通过开关的方式启动。创建文件 values.yaml,内容如下:

1
2
3
sync:
ingresses:
enabled: true

执行 vcluster create my-vcluster --upgrade -f values.yaml 即可修改现在 vcluster 集群配置。

在 virtual cluster 中创建如下的 Ingress 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
labels:
app: nginx
name: nginx
namespace: demo-nginx
spec:
rules:
- host: nginx.aa.com
http:
paths:
- backend:
service:
name: nginx
port:
number: 80
path: /
pathType: ImplementationSpecific

查看 host cluster 中的 Ingress 信息如下:

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
$ kubectl get ingress --context kind-vcluster -n vcluster-my-vcluster -o yaml nginx-x-demo-nginx-x-my-vcluster
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
vcluster.loft.sh/object-name: nginx
vcluster.loft.sh/object-namespace: demo-nginx
vcluster.loft.sh/object-uid: 4b8034a6-1513-4ccd-b80a-66807d862b4e
creationTimestamp: "2023-11-28T10:07:52Z"
generation: 1
labels:
vcluster.loft.sh/label-my-vcluster-x-a172cedcae: nginx
vcluster.loft.sh/managed-by: my-vcluster
vcluster.loft.sh/namespace: demo-nginx
name: nginx-x-demo-nginx-x-my-vcluster
namespace: vcluster-my-vcluster
ownerReferences:
- apiVersion: v1
controller: false
kind: Service
name: my-vcluster
uid: 463a503e-d889-49a7-94e0-0cba5299dd47
resourceVersion: "16292"
uid: 1d0de02b-5aad-4858-a8cf-2caa345ca85b
spec:
rules:
- host: nginx.aa.com
http:
paths:
- backend:
service:
name: nginx-x-demo-nginx-x-my-vcluster
port:
number: 80
path: /
pathType: ImplementationSpecific
status:
loadBalancer:
ingress:
- hostname: localhost

可以看到 Ingress 中对应的 Service 名字已经修改了 host cluster 中对应的 Service 名字。

销毁

在使用完成后执行如下命令即可销毁 virtual cluster:

1
2
3
4
# 切换本地的 context
vcluster disconnect
# 删除 vcluster
vcluster delete my-vcluster

架构

image.png

组件

整个架构中,有两大核心组件:k8s Control Plane 和 syncer。其中 StatefulSet my-vcluster 中容器 syncer,默认情况下在该容器中同时启动了 k3s 容器作为 vCluster 控制平面和 vCluster 的 syncer 进程。

1
2
3
4
5
6
$ kubectl exec -it --context kind-vcluster -n vcluster-my-vcluster my-vcluster-0 -- ps -ef
Defaulted container "syncer" out of: syncer, vcluster (init)
PID USER TIME COMMAND
1 root 2:57 /vcluster start --name=my-vcluster --kube-config=/data/k3s
17 root 18:35 /k3s-binary/k3s server
46 root 0:00 ps -ef

controller plane

控制平面默认使用 k3s,存储使用 sqllite,也可以使用 etcd、mysql、postgresql。k8s 发行版也可以使用 k0s、Vanilla(标准 k8s)、第三方镜像等。控制平面由如下几个组件组成:

  1. k8s apiserver。
  2. 数据存储,比如 sqllite、etcd 等。
  3. kube-controller-manager
  4. kube-scheduler:可选组件,默认使用 host cluster 调度器。

在 Pro 版本中,允许控制面跟 pod 部署在不同的 host cluster。

syncer

virtual cluster 中并不包含实际的计算、存储和网络资源,syncer 的职责为将对象从 virtual cluster 同步到 host cluster,也有少部分对象需要从 host cluster 同步到 virtual cluster。
vCluster 将 k8s 对象划分为 low level 和 high level,其中 high level 的对象仅存在于 virtual cluster 中,比如 Deployment、CRD 等对象。low level 的对象会通过 syncer 模块同步到 host cluster 上,包括 Pod、ConfigMap、Secret 等。low level 的对象在 virutal cluster 为多个 namespace,但均会映射到 host cluster 的一个 namespace 下。另外,vCluster 也支持将 virtual cluster 的多个 namespace 映射到 host cluster 的多个 namespace,该特性目前处于 alpha 状态。
vCluster 可以通过配置的方式来定制资源的同步,更复杂的同步规则提供了插件机制实现。
vCluster 默认支持的同步资源列表:https://www.vcluster.com/docs/syncer/core_resources

已经创建完成的 syncer 配置,可以通过 vcluster create my-vcluster --upgrade -f values.yaml 的方式修改,该命令会调用 helm update,helm update 命令最终会修改 StatefulSet syncer 的配置,并触发 pod 的重启。

k8s node 同步

支持多种 node 的同步行为,通过修改 syncer 的启动参数:

  1. Fake Node:默认行为。根据 pod 中的 spec.nodeName 创建 Fake Node。Fake Node 为 syncer 服务自动创建。如果没有 pod 调度到 Fake Node 上,则 Fake Node 会自动删除。
  2. Real Node:根据 pod 中的 spec.nodeName 创建 Real Node,Real Node 的信息从 host cluster 同步。如果没有 pod 调度到 Real Node 上,则 Real Node 会自动删除。
  3. Real Node All:同步 host cluster 的所有 node 到 virtual cluster。如果要使用 DaemonSet,需要使用该模式。
  4. Real Nodes Label Selector:仅同步 label selector 匹配的 host node 到 virtual cluster 中。
  5. Real Nodes + Label Selector:仅同步包含在 pod spec.nodeName 且 Label selector 可以选中的 host cluster node 到 virtual cluster 中。

pod 调度

默认情况下,virtual cluster 中的 pod 调度会使用 host cluster 的调度,但存在如下的问题:

  1. 在 virtual cluster node 上的 label 对于 pod 调度不会生效。
  2. drait、trait 命令对于 virtual cluster 上的 pod 没有影响。
  3. virtual cluster 中使用自定义调度器不生效。

基于上述限制,vCluster 支持如下两种方案:

  1. 支持在 virtual cluster 中使用独立的调度器。可以给 virtual cluster 上的 node 增加标签、污点等信息,pod 的调度在 virtual cluster 中的调度器实现,syncer 组件仅将已经调度完成的 pod 同步到 host cluster。
  2. 仍然复用 host cluster 调度器,但做了部分功能的增强:在 syncer 服务中指定仅同步部分 host node 到 virtual cluster 中,这样 pod 就仅会调度到 host cluster 的特定 node 上。

网络

virutal cluster 中无独立的 pod 网络和 service 网络,完全复用 host cluster 的网络。

Service 网络

  1. 会从 virtual cluster 同步到 host cluster,两者的 clusterip 一致。
  2. 允许将一些 host cluster 中的 service 同步到 virtual cluster 中,同时指定service 的名字。
  3. 允许将 virtual cluster 中的 service 同步到 host cluster 中,同时指定service 的名字。

Ingress 网络

允许将 virtual cluster 中的 Ingress 同步到 host cluster,以便复用 host cluster 中的 Ingress Controller。

DNS 解析

在 virtual cluster 中部署了单独的 coredns 组件,默认情况下,在 vritual cluster 中的域名仅能解析内部的域名,不能解析 host cluster 上的域名。可以通过开关的方式,将 virtual cluster 中的域名解析转发到 host cluster 的 coredns。

在 PRO 版本中,coredns 组件可以集成到 syncer 组件内部,以便节省资源。

NetworkPolicy

默认情况下,vcluster 中会忽略 virtual cluster 中的 NetworkPolicy 资源。可以通过开关的方式打开该配置,即可将 NetworkPolicy 规则同步到 host cluster。

存储

image.png
默认情况下,host StorageClass 不会同步到 vc,可以通过开关的方式打开同步。
默认情况下,pv 不会从 vc 同步到 host cluster,可以通过开关的方式打开。

可观测性

monitoring

metrics-server 用来监控 k8s 的 Deployment、StatefulSet 等对象,metrics-server 可以复用 host cluster 中的,但需要启用 metrics server proxy 功能。也可以在 vc 中单独部署一套 metrics server。
在 vc 集群中,由于每个 k8s node 的 ip 地址为 host cluster 中的 service clusterip,在 vc 中网络是可达的,可以获取到对应的监控信息。

logging

需要用到Hostpath Mapper组件,该组件为 DaemonSet 的形式。后续即可以部署 loki 等组件。

安全

隔离模式

在启动的时候指定--isolate,在该模式下对 workload 做了多种限制。

  1. 对 vcluster pod 的 Pod Security 做限制,不符合规范的 pod 不会同步到 host cluster。
  2. 可以对 vc 中 pod 的总资源量做限制。
  3. 在 host cluster 上通过 NetworkPolicy 做隔离。

virtual cluster 集群的创建

目前仅能通过 vcluster cli、helm 的方式来创建,底层均为 helm chart 的方式来管理,缺少服务化功能。

virtual cluster 集群对外暴露方法

获取 kubeconfig

vcluster connect 命令

该命令可以修改本地的 kubeconfig 文件,并将 context 切换为 virtual cluster context。默认为 virutual cluster 的管理员权限,可以指定使用特定的 ServiceAccount。

host cluster secret 中获取到 kubeconfig

在 host cluster 中,在 vc 的 namespace 下,存在一个以 vc- 开头的 Secret,该 Secret 中保存了 kubeconfig 完整信息。

vc 集群中的 apiserver 的暴露地址

可以在 syncer 启动的时候指定获取的 kubeconfig 中的 endpoint 地址。endpoint 地址即为 vc 集群中的 kube-apiserver 的地址,该 kube-apiserver 的地址可以通过 host cluster 中的 Ingress、LoadBalancer Service、NodePort Service 等方式对外暴露。

高可用设计

control plane 高可用

k3s 可以支持高可用架构,在创建 vc 的时候通过指定的副本的方式来设置高可用。其他的 k8s 发行版同样类似的实现。

备份与恢复

vCluster 本身并没有提供对于 vc 集群的数据备份与恢复功能,可以通过通用的 velero 方式实现备份与恢复功能。

总结

未做网络隔离,容器网络和 service 网络仍然在同一个平面,要想相互隔离,必须使用 NetworkPolicy。

其他

  1. 获取 helm chart 到本地
helm repo add lofts https://charts.loft.sh/
helm fetch lofts/vcluster
``

CSI:Container Storage Interface (CSI)

Volume 的三个阶段:

  1. Provision and Delete:负责卷的创建以及销毁。
  2. Attaching and Detaching:将卷设备挂载到本地或者从本地卸载。
  3. Mount and Umount:将 Attaching 的块设备以目录形式挂载到 pod中,或者从 pod 中卸载块设备。

开发

CSI 插件分为三个部分:

  1. CSI Identity:用来获取 CSI 的身份信息
  2. CSI Controller
  3. CSI Node

参考 k8s 官方的 hostpath 项目:https://github.com/kubernetes-csi/csi-driver-host-path

为了方便开发,在每个阶段 k8s 官方均实现了对应的 SideCarSet 容器。要想研发,仅需要实现 grpc server,又 SideCarSet 容器调用自研的容器。

自研的容器需要实现如下的接口即可。

CSI Identity

1
2
3
4
5
6
7
8
9
10
service Identity {
// 返回插件名字以及版本号
rpc GetPluginInfo(GetPluginInfoRequest) returns (GetPluginInfoResponse) {}

// 返回插件的包含的功能
rpc GetPluginCapabilities(GetPluginCapabilitiesRequest) returns (GetPluginCapabilitiesResponse) {}

rpc Probe (ProbeRequest)
returns (ProbeResponse) {}
}

External provisioner 会调用该接口。

CSI Controller

实现 Volume 中的 Provisioning and Deleting 和 Attaching and Detaching 两个阶段的功能。只有块存储 CSI 插件才需要 Attach 功能。
该部分以中心化组件的方式部署,比如 Deployment。Provision 功能对应的 SidecarSet 为 external-provisioner,Attach 对应的 SidecarSet 为 external-attacher

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
service Controller {
// Provisioning, External provisioner调用
// hostPath 实现中会调用 fallocate 实现
rpc CreateVolume (CreateVolumeRequest)
returns (CreateVolumeResponse) {}

// Deleting, External provisioner调用
rpc DeleteVolume (DeleteVolumeRequest)
returns (DeleteVolumeResponse) {}

// Attaching, 只有块设备才需要实现,比如云盘,由External attach 调用
rpc ControllerPublishVolume (ControllerPublishVolumeRequest)
returns (ControllerPublishVolumeResponse) {}

// Detaching,External attach 调用
rpc ControllerUnpublishVolume (ControllerUnpublishVolumeRequest)
returns (ControllerUnpublishVolumeResponse) {}

rpc ValidateVolumeCapabilities (ValidateVolumeCapabilitiesRequest)
returns (ValidateVolumeCapabilitiesResponse) {}

rpc ListVolumes (ListVolumesRequest)
returns (ListVolumesResponse) {}

rpc GetCapacity (GetCapacityRequest)
returns (GetCapacityResponse) {}

rpc ControllerGetCapabilities (ControllerGetCapabilitiesRequest)
returns (ControllerGetCapabilitiesResponse) {}

// 创建快照功能
rpc CreateSnapshot (CreateSnapshotRequest)
returns (CreateSnapshotResponse) {}

// 删除快照功能
rpc DeleteSnapshot (DeleteSnapshotRequest)
returns (DeleteSnapshotResponse) {}

rpc ListSnapshots (ListSnapshotsRequest)
returns (ListSnapshotsResponse) {}

rpc ControllerExpandVolume (ControllerExpandVolumeRequest)
returns (ControllerExpandVolumeResponse) {}

rpc ControllerGetVolume (ControllerGetVolumeRequest)
returns (ControllerGetVolumeResponse) {
option (alpha_method) = true;
}

rpc ControllerModifyVolume (ControllerModifyVolumeRequest)
returns (ControllerModifyVolumeResponse) {
option (alpha_method) = true;
}
}

CSI Node

实现 Volume 中的Mount 和 Umount 阶段,由 kubelet 负责调用。
该部分以 DaemonSet 的形式部署在每个 k8s node 上,对应的 SidecarSet 容器为 node-driver-registrer

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
service Node {
// 针对块存储类型,将块设备格式化后先挂载到一个临时全局目录
rpc NodeStageVolume (NodeStageVolumeRequest)
returns (NodeStageVolumeResponse) {}

rpc NodeUnstageVolume (NodeUnstageVolumeRequest)
returns (NodeUnstageVolumeResponse) {}

// 如果是块存储设备,在执行完 NodeStageVolume 后,使用 linux 的 bind mount 技术将全局目录挂载到pod 中的对应目录
rpc NodePublishVolume (NodePublishVolumeRequest)
returns (NodePublishVolumeResponse) {}

rpc NodeUnpublishVolume (NodeUnpublishVolumeRequest)
returns (NodeUnpublishVolumeResponse) {}

rpc NodeGetVolumeStats (NodeGetVolumeStatsRequest)
returns (NodeGetVolumeStatsResponse) {}


rpc NodeExpandVolume(NodeExpandVolumeRequest)
returns (NodeExpandVolumeResponse) {}


rpc NodeGetCapabilities (NodeGetCapabilitiesRequest)
returns (NodeGetCapabilitiesResponse) {}

rpc NodeGetInfo (NodeGetInfoRequest)
returns (NodeGetInfoResponse) {}
}

资料

协议:https://github.com/container-storage-interface/spec/blob/master/spec.md

重装过电脑操作系统的同学大概知道操作系统的安装流程如下:

  1. 在 BIOS 中将系统设置为光驱/USB开机优先模式
  2. 以DVD或者 U 盘中的操作系统开机,进入到装机界面
  3. 完成一系列的装机初始化,比如磁盘分区、语言选择等
  4. 重启进入新安装的操作系统

以上过程必须要手工才能完成,安装一台电脑还可以,但如果要大批量安装一批机器就不适用了。为此,Intel 公司研发了 PXE(Pre-boot Execution Environment) 技术,可以通过网络的方式批量安装操作系统。

PXE 基于 C/S 架构,分为PXE client 和PXE server,其中 PXE client 为要安装操作系统的机器,PXE server 用来提供安装操作系统必须的镜像等信息。要想实现从网络上安装操作系统,必须要解决如下几个问题:

  1. 因为还没有安装操作系统,此时并不存在 ip 地址,在装机之前必须要获取到一个 ip 地址。
  2. 安装操作系统需要的 boot loader 和操作系统镜像如何获取。

为了解决 PXE client 的 ip 地址问题,PXE 中采用了 DHCP 协议来给 client 分配 ip 地址,这就要求 PXE server 必须要运行 dhcp server。为了解决 PXE server 可以提供 boot loader 和操作系统基线,PXE server 通过 tftp 协议的方式对 client 提供服务。

client 端需要 DHCP client 和 tftp client 的功能,为此 PXE 协议中将该功能以硬件的方式内置在网卡 ROM 中。当启动时,BIOS 会加载内置在网卡中的 ROM,从而该机器具备了 DHCP client 和 tftp client 的功能。

优点:

  1. 规模化:可以批量实现多台服务器的安装
  2. 自动化:可以自动化安装
  3. 远程实现:不用本地的光盘来安装 OS

客户机的前提条件:

  1. 网络必须要支持 PXE 协议
  2. 主板支持网络引导,一般在 BIOS 中可以配置

服务端:

  1. DHCP 服务,用来给客户机分配 ip 地址
  2. TFTP 服务:用来提供操作系统文件的下载

TCP Fast Open(TFO)是一种TCP协议的扩展,旨在加快建立TCP连接的速度和降低延迟。传统的TCP连接需要进行三次握手(SYN-SYN/ACK-ACK)才能建立连接,而TFO允许在第一个数据包中携带连接建立的请求。

TFO的工作原理如下:

  1. 客户端在首次建立TCP连接时,在发送的SYN包中插入一个加密的Cookie。这个Cookie由服务器生成并发送给客户端。
  2. 当客户端发送带有TFO Cookie的SYN包到服务器时,服务器会验证Cookie的有效性。
  3. 如果Cookie有效,服务器会立即发送带有SYN+ACK标志的数据包,这样客户端就可以立即发送数据而无需等待ACK响应。
  4. 客户端收到带有SYN+ACK标志的数据包后,发送带有ACK标志的数据包,建立完整的TCP连接。

相关内核参数

net.ipv4.tcp_fastopen

支持如下值:

  • 0:关闭
  • 1: 作为客户端可以使用 TFO 功能
  • 2: 作为服务端可以使用 TFO 功能
  • 3: 作为客户端和服务端均可使用 TFO

net.ipv4.tcp_fastopen_key

用来产生 TFO 的 Cookie。

zone 设置

DNS 记录的 zone 信息为全局配置,配置地方包括 kubelet 和 coredns 两部分。

kubelet 的启动参数

  1. 通过 kubelet 的 yaml 配置文件的 clusterDomain 字段。
  2. 通过 kubelet 的参数 --cluster-domain

设置了 kubelet 的启动参数后,会设置容器的 /etc/resolv.conf 中的 search 域为如下格式:

1
2
3
search default.svc.cluster.local svc.cluster.local cluster.local tbsite.net
nameserver 10.181.48.10
options ndots:5 single-request-reopen

其中 search 域中的 cluster.local 为 kubelet 的配置。

coredns 的配置文件

coredns controller 需要 watch k8s 集群中的 pod 和 service,将其进行注册,因此 coredns 需要知道集群的 zone 配置。该配置信息位于 coredns 的配置文件 ConfigMap kube-system/coredns 中,默认的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}

其中 cluster.local 为对应的 k8s zone。

域名注册

在 k8s 中,Service 和 Pod 对象会创建 DNS 记录,用于 k8s 集群内部的域名解析。

Pod 域名注册

规则一:

每个 k8s pod 都会创建 DNS 记录: <pod_ip>.<namespace>.pod.<cluster-domain>。其中 为 pod ip 地址,但需要将 ip 地址中的 . 转换为 -

比如 pod nginx-deployment-57d84f57dc-cpgkc 会创建 A 记录 10-244-3-8.default.pod.cluster.local

1
2
3
$ kubectl get pod -o wide nginx-deployment-57d84f57dc-cpgkc -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-deployment-57d84f57dc-cpgkc 1/1 Running 0 2m59s 10.244.3.8 vc-worker2 <none> <none>

规则二:

pod 如何同时指定了 spec.hostnamespec.subdomain,则会创建 A 记录:<hostname>.<subdomain>.<namespace>.svc.cluster.local,而不是 <pod_ip>.<namespace>.pod.<cluster-domain>。对于 Statefulset 类型的 pod 会自动设置 spec.hostname 为 pod 的名字,spec.subdomain 为 StatefulSet 的 spec.serviceName

比如 pod nginx-statefulset-0 会创建 A 记录 nginx-statefulset-0.nginx.default.svc.cluster.local

1
2
3
$ kubectl get pod nginx-statefulset-0 -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-statefulset-0 1/1 Running 0 62m 10.244.3.7 vc-worker2 <none> <none>

Deployment/DaemonSet 管理的 pod

使用 Deployment/DaemonSet 拉起的 pod,k8s 会创建额外的 DNS 记录:<pod_ip>.<deployment-name/daemonset-name>.<namespace>.svc.<cluster-domain>

Service 域名注册

普通 Service

除了 headless service 之外的其他 service 会在 DNS 中生成 my-svc.my-namespace.svc.cluster-domain.example 的 A 或者 AAAA 记录,A 记录指向 ClusterIP。

headless service 会在 DNS 中生成 my-svc.my-namespace.svc.cluster-domain.example 的 A 或者 AAAA 记录,但指向的为 pod ip 地址集合。

k8s 在 pod 的 /etc/resolv.conf 配置如下:

1
2
3
nameserver 10.32.0.10
search <namespace>.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

对于跟 pod 同一个 namespace 下的 service,要访问可以直接使用 service 名字接口。跟 pod 不在同一个 namespace 下的 service,访问 service 必须为 service name.service namespace

ExternalName Service

service 的 spec.typeExternalName,该种类型的服务会向 dns 中注册 CNAME 记录,CNAME 记录指向 externalName 字段。例子如下:

1
2
3
4
5
6
7
8
apiVersion: v1
kind: Service
metadata:
name: my-service
namespace: prod
spec:
type: ExternalName
externalName: my.database.example.com

当访问 my-service.prod.svc.cluster.local 时,DNS 服务会返回 CNAME 记录,指向地址为 my.database.example.com

externalIPs 字段

可以针对所有类型的 Service 生效,用来配置多个外部的 ip 地址(该 ip 地址不是 k8s 分配),kube-proxy 会设置该 ip 地址的规则,确保在 k8s 集群内部访问该 ip 地址时,可以路由到后端的 pod。效果就跟访问普通的 ClusterIP 类型 Service 没有区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app.kubernetes.io/name: MyApp
ports:
- name: http
protocol: TCP
port: 80
targetPort: 49152
externalIPs:
- 198.51.100.32

当在集群内部访问 198.51.100.32:80 时,流量会被 kube-proxy 路由到当前 Service 的 Endpoint。

该字段存在中间人攻击的风险,不推荐使用。Detect CVE-2020-8554 – Unpatched Man-In-The-Middle (MITM) Attack in Kubernetes

Headless Service

翻译成中文又叫无头 Service,显式的将 Service spec.clusterIP 设置为 "None",表示该 Service 为 Headless Service。此时,该 Service 不会分配 clusterIP。因为没有 clusterIP,因此 kube-proxy 并不会处理该 service。

Headless Service 按照是否配置了 spec.selector 在实现上又有不同的区分。

未配置 spec.selector 的 Service,不会创建 EndpointSlice 对象,但是会注册如下的记录:

  • 对于 ExternalName Service,配置 CNAME 记录。
  • 对于非 ExternalName Service,配置 A/AAAA 记录,指向 EndPoint 的所有 ip 地址。如果未配置 Endpoint,但配置了 externalIPs 字段,则指向 externalIPs。

配置 spec.selector 的 Service,会创建 EndpointSlice 对象,并修改 DNS 配置返回 A 或者 AAAA 记录,指向 pod 的集合。

域名查询

待补充

资料

json patch

该规范定义在 RFC 6902,定义了修改 json 格式的规范,同时还可以配合 http patch 请求一起使用,实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PATCH /my/data HTTP/1.1
Host: example.org
Content-Length: 326
Content-Type: application/json-patch+json
If-Match: "abc123"

[
{ "op": "test", "path": "/a/b/c", "value": "foo" },
{ "op": "remove", "path": "/a/b/c" },
{ "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },
{ "op": "replace", "path": "/a/b/c", "value": 42 },
{ "op": "move", "from": "/a/b/c", "path": "/a/b/d" },
{ "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }
]

支持add、remove、replace、move、copy和 test 六个patch动作。

协议规范

add

格式如下:

1
{ "op": "add", "path": "/hello", "value": [ "foo" ] }

规范:

  1. 如果原始 json 中不存在 key “/hello”,则会全新创建 key。
  2. 如果原始 json 存在 key “/hello”,则会直接覆盖;即使”/hello”为数组,也不会在原先的基础上追加,而是直接强制覆盖;

原始 json 如下:

1
2
3
{
"hello": ["123"]
}

执行后结果如下:

1
2
3
4
5
{
"hello": [
"world"
]
}

remove

用来删除某个 key,格式如下:

1
[{ "op": "remove", "path": "/hello" }]

replace

用来替换某个 key,跟 add 动作的差异是,如果 key 不存在,则不会创建 key。

1
{ "op": "replace", "path": "/hello", "value": 42 }

如果原始 json 格式为: {},执行完成后,输出 json 格式仍然为:{}

move

用来修改 key 的名称,格式如下:

1
{ "op": "move", "from": "/hello", "path": "/hello2" }

如果 key 不存在,则不做任何修改。

copy

用来复制某个 key,格式如下:

1
{ "op": "copy", "from": "/hello", "path": "/hello2" }

如果原始 key 不存在,则不复制;如果目标 key 已经存在,则仍然会复制。

原始 json 如下:

1
2
3
4
{
"hello": "world",
"hello2": "world2"
}

执行完成后的 json 如下:

1
2
3
4
{
"hello": "world",
"hello2": "world"
}

test

用来测试 key 对应的 value 是否相等,该操作并不常用

1
{ "op": "test", "path": "/a/b/c", "value": "foo" }

工具

  • JSON Patch Builder Online 在线工具,可根据原始 json 和 patch 完成后的 json,产生 json patch
  • jsonpatch.me 在线工具,可根据原始 json 和 json patch,产生 patch 完成后的 json

总结

通过上述协议可以发现如下缺点:

  1. 对于数组的处理不是太理想,如果要删除数组中的某个元素,或者在数组中追加某个元素,则无法表达。
  2. 该协议对于人类并不友好。

json merge patch

定义在 RFC 7386,由于patch 能力比较有限,使用场景较少。

同样可以配合 http patch 方法一起使用,http 请求如下:

1
2
3
4
5
6
7
8
9
10
PATCH /target HTTP/1.1
Host: example.org
Content-Type: application/merge-patch+json

{
"a":"z",
"c": {
"f": null
}
}

下面结合具体的实例来说明 json merge patch 的功能。
原始 json 格式如下:

1
2
3
4
5
6
7
8
9
{
"title": "Goodbye!",
"author" : {
"givenName" : "John",
"familyName" : "Doe"
},
"tags":[ "example", "sample" ],
"content": "This will be unchanged"
}

patch json 格式如下:

1
2
3
4
5
6
7
8
{
"title": "Hello!",
"phoneNumber": "+01-123-456-7890",
"author": {
"familyName": null
},
"tags": [ "example" ]
}

其中 null 用来表示该 key 需要删除。对于数组类型,则直接覆盖数组中的值。
patch 完成后的 json 如下:

1
2
3
4
5
6
7
8
9
{
"title": "Hello!",
"author" : {
"givenName" : "John"
},
"tags": [ "example" ],
"content": "This will be unchanged",
"phoneNumber": "+01-123-456-7890"
}

通过上述实例可以发现如下的功能缺陷:

  1. 如果某个 json 的 key 对应的值为 null,则无法表达,即不可以将某个 key 对应的value 设置为 null。
  2. 对于数组的处理非常弱,是直接对数组中所有元素的替换。

k8s strategic merge patch

该协议的资料较少,官方参考资料只有两篇文章,最好结合着 k8s 的代码才能完全理解:

背景

无论是 json patch,还是 json merge patch 协议,对于数组元素的支持都不够友好。
比如对于如下的 json:

1
2
3
4
spec:
containers:
- name: nginx
image: nginx-1.0

期望能够 patch 如下的内容

1
2
3
4
spec:
containers:
- name: log-tailer
image: log-tailer-1.0

从而可以实现 containers中包含两个元素的情况,无论是 json patch 还是 json merge patch,其行为是对数组元素的直接替换,不能实现追加的功能。

协议规范

为了解决 json merge patch 的功能缺陷,strategic merge patch 通过如下两种方式来扩展功能:

  1. json merge patch 的 json 语法增强,增加一些额外的指令
  2. 通过增强原始 json 的 struct 结构实现,跟 golang 语言强绑定,通过 golang 中的 struct tag 机制实现。这样的好处是不用再扩充 json merge patch 的 json 格式了。支持如下 struct tag:
    1. patchStrategy: 指定策略指令,支持:replace、merge 和 delete。默认的行为为 replace,保持跟 json merge patch 的兼容性。
    2. patchMergeKey: 数组一个子 map 元素的主键,类似于关系型数据库中一行记录的主键。

支持如下指令:

  1. replace
  2. merge
  3. delete

replace

支持 go struct tag 和 在 json patch 中增加指令两种方式。
replace 是默认的指令模式,对于数组而言会直接全部替换数组内容。

如下指令用来表示,

1
2
3
4
$patch: replace  # recursive and applies to all fields of the map it's in
containers:
- name: nginx
image: nginx-1.0

delete

删除数组中的特定元素,下面例子可以删除数组中包含 name: log-tailer 的元素。

1
2
3
4
5
containers:
- name: nginx
image: nginx-1.0
- $patch: delete
name: log-tailer # merge key and value goes here

删除 map 的特定 key,如下实例可以删除 map 中的 key rollingUpdate。

1
2
rollingUpdate:
$patch: delete

merge

该指令仅支持 go struct tag 模式,格式为:$deleteFromPrimitiveList/<keyOfPrimitiveList>: [a primitive list]

deleteFromPrimitiveList

删除数组中的某个元素
go struct 定义如下:

1
Finalizers []string `json:"finalizers,omitempty" patchStrategy:"merge" protobuf:"bytes,14,rep,name=finalizers"`

原始 yaml 如下:

1
2
3
4
5
finalizers:
- a
- b
- c
- b

patch yaml 如下,用来表示删除finalizers中的所有元素 b 和 c

1
2
3
4
5
6
# The directive includes the prefix $deleteFromPrimitiveList and
# followed by a '/' and the name of the list.
# The values in this list will be deleted after applying the patch.
$deleteFromPrimitiveList/finalizers:
- b
- c

最终得到结果:

1
2
finalizers:
- a

setElementOrder

用于数组中的元素排序

简单数组排序例子

原始内容如下:

1
2
3
4
finalizers:
- a
- b
- c

设置排序顺序:

1
2
3
4
5
6
# The directive includes the prefix $setElementOrder and
# followed by a '/' and the name of the list.
$setElementOrder/finalizers:
- b
- c
- a

最终得到排序顺序:

1
2
3
4
finalizers:
- b
- c
- a
map 类型数组排序例子

其中 patchMergeKey 为 name 字段

1
2
3
4
5
6
7
containers:
- name: a
...
- name: b
...
- name: c
...

patch 指令的格式:

1
2
3
4
5
# each map in the list should only include the mergeKey
$setElementOrder/containers:
- name: b
- name: c
- name: a

最终获得结果:

1
2
3
4
5
6
7
containers:
- name: b
...
- name: c
...
- name: a
...

retainKeys

用来清理 map 结构中的 key,并指定保留的 key
原始内容:

1
2
3
union:
foo: a
other: b

patch 内容:

1
2
3
4
5
6
union:
retainKeys:
- another
- bar
another: d
bar: c

最终结果,可以看到 foo 和 other 因为不在保留列表中已经被清楚了。同时新增加了字段 another 和 bar,新增加的是字段是直接 patch 的结果,同时这两个字段也在保留的列表内。

1
2
3
4
union:
# Field foo and other have been cleared w/o explicitly set them to null.
another: d
bar: c

strategic merge patch 在 k8s 中应用

kubectl patch 命令通过–type 参数提供了几种 patch 方法。

1
--type='strategic': The type of patch being provided; one of [json merge strategic]
  1. json:即支持 json patch 协议,例子:kubectl patch pod valid-pod --type='json' -p='[{"op": "replace", "path": "/spec/containers/0/image", "value":"newimage"}]
  2. merge:对应的为 json merge patch 协议。
  3. stategic:k8s 特有的 patch 协议,在 json merge patch 协议基础上的扩展,可以解决 json merge patch 的缺点。

对于 k8s 的 CRD 对象,由于不存在 go struct tag,因此无法使用 stategic merge patch。

TODO:待补充具体例子

kubectl patch、replace、apply之间的区别

patch

kubectl patch 命令的实现比较简单,直接调用 kube-apiserver 的接口在 server 端实现 patch 操作。

replace

如果该命令使用 –force=true 参数,则会先删除对象,然后再提交,相当于是全新创建。

apply

apply 的实现相对比较复杂,逻辑也比较绕,可以实现 map 结构中的字段增删改操作,数组中数据的增删改操作。实现上会将多份数据进行merge 后提交,数据包含:

  1. 要重新 apply 的 yaml
  2. 对象的annotation kubectl.kubernetes.io/last-applied-configuration 包含的内容
  3. 运行时的 k8s 对象

具体的操作步骤:

  1. 要重新 apply 的 yaml 跟annotation kubectl.kubernetes.io/last-applied-configuration 包含的内容比较,获取到要删除的字段。
  2. 要重新 apply 的 yaml 跟运行时的 k8s 对象进行比较,获取到要增加的字段。
  3. 上述两个结果再进行一次 merge 操作,最终调用 kube-apiserver 的接口实现 patch 操作。

为什么一定需要用到kubectl.kubernetes.io/last-applied-configuration的数据呢?
在 yaml 对象提交到 k8s 后,k8s 会自动增加一些字段,也可以通过额外的方式修改对象增加一些字段。如果 patch 内容仅跟运行时结果比较,会导致一些运行时的k8s 自动增加的字段或者手工更新的字段被删除掉。

试验 上次提交 last-applied-configuration 运行时 patch 内容 结果 结果分析
试验一 label1: first label1: first label2: second label2: second 1. patch 内容跟上次内容比较,发现要删除字段 label1
2. patch 内容跟运行时比较,发现新增加了字段 label2
3. 最终 label1 被删除,仅保留 label2
试验二 label1: first label1: first
label2: second
label1: first label1: first
label2: second
1. patch 内容跟上次内容比较,发现结果无变化
2. patch 内容跟运行时比较,发现要新增加字段 label2
3. 最终新增加字段 label2

引用

本文将通过 kubeadm 实现单 master 节点模式和集群模式两种部署方式。

所有节点均需初始化操作

所有节点均需做的操作。

主机准备

1
2
3
4
5
6
7
8
cat > /etc/sysctl.d/kubernets.conf <<EOF
net.ipv4.ip_forward=1
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
vm.swappiness=0
EOF
sysctl --system
modprobe br_netfilter

安装containerd

由于 dockerd 从 k8s 1.24 版本开始不再支持,这里选择 containerd。

手工安装

安装 containerd,containerd 的版本可以从这里获取 https://github.com/containerd/containerd/releases

1
2
3
4
5
wget https://github.com/containerd/containerd/releases/download/v1.6.11/containerd-1.6.11-linux-amd64.tar.gz
tar Cxzvf /usr/local containerd-1.6.11-linux-amd64.tar.gz
wget https://raw.githubusercontent.com/containerd/containerd/main/containerd.service -P /usr/local/lib/systemd/system/
systemctl daemon-reload
systemctl enable --now containerd

安装 runc

1
2
wget https://github.com/opencontainers/runc/releases/download/v1.1.4/runc.amd64
install -m 755 runc.amd64 /usr/local/sbin/runc

yum 源安装

1
2
3
4
5
yum install -y yum-utils
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum install containerd.io -y
systemctl daemon-reload
systemctl enable --now containerd

通过 yum 安装的containerd 没有启用 cri,在其配置文件 /etc/containerd/config.toml 中包含了 disabled_plugins = ["cri"] 配置,需要将配置信息注释后并重启 containerd。

1
2
sed -i 's/disabled_plugins/#disabled_plugins/'  /etc/containerd/config.toml
systemctl restart containerd

安装 kubeadm/kubelet/kubectl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-\$basearch
enabled=1
gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
exclude=kubelet kubeadm kubectl
EOF

# Set SELinux in permissive mode (effectively disabling it)
sudo setenforce 0
sudo sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config

sudo yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes

sudo systemctl enable --now kubelet

单 master 节点模式

节点 角色
172.21.115.190
master 节点

kubeadm 初始化

创建文件 kubeadm-config.yaml,文件内容如下:

1
2
3
4
5
6
7
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: v1.25.4
---
kind: KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
cgroupDriver: systemd

执行命令:

1
2
3
kubeadm init --config kubeadm-config.yaml
kubeadm config print init-defaults --component-configs KubeletConfiguration > cluster.yaml
kubeadm init --config cluster.yaml

接下来初始化 kubeconfig 文件,这样即可通过 kubectl 命令来访问 k8s 了。

1
2
3
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

安装网络插件

刚部署完成的节点处于NotReady 的状态,原因是因为还没有安装网络插件。

cilim 网络插件

cilim 网络插件比较火爆,下面介绍其安装步骤:

1
2
3
4
5
6
7
8
9
10
11
# 安装 cilium 客户端
CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/master/stable.txt)
CLI_ARCH=amd64
if [ "$(uname -m)" = "aarch64" ]; then CLI_ARCH=arm64; fi
curl -L --fail --remote-name-all https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-${CLI_ARCH}.tar.gz{,.sha256sum}
sha256sum --check cilium-linux-${CLI_ARCH}.tar.gz.sha256sum
sudo tar xzvfC cilium-linux-${CLI_ARCH}.tar.gz /usr/local/bin
rm cilium-linux-${CLI_ARCH}.tar.gz{,.sha256sum}

# 网络插件初始化
cilium install

在安装完网络插件后,node 节点即可变为 ready 状态。

查看环境中包含如下的 pod:

1
2
3
4
5
6
7
8
9
10
11
$ kubectl  get pod  -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system cilium-7zj7t 1/1 Running 0 82s
kube-system cilium-operator-bc4d5b54-kvqqx 1/1 Running 0 82s
kube-system coredns-565d847f94-hrm9b 1/1 Running 0 14m
kube-system coredns-565d847f94-z5kwr 1/1 Running 0 14m
kube-system etcd-k8s002 1/1 Running 0 14m
kube-system kube-apiserver-k8s002 1/1 Running 0 14m
kube-system kube-controller-manager-k8s002 1/1 Running 0 14m
kube-system kube-proxy-bhpqr 1/1 Running 0 14m
kube-system kube-scheduler-k8s002 1/1 Running 0 14m

k8s 自带的 bridge 插件

在单节点的场景下,pod 不需要跨节点通讯,k8s 自带的 bridge 插件也可以满足单节点内的 pod 相互通讯,类似于 docker 的 bridge 网络模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mkdir -p /etc/cni/net.d
cat > /etc/cni/net.d/10-mynet.conf <<EOF
{
"cniVersion": "0.2.0",
"name": "mynet",
"type": "bridge",
"bridge": "cni0",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "172.19.0.0/24",
"routes": [
{ "dst": "0.0.0.0/0" }
]
}
}
EOF

如果 k8s 节点已经部署完成,需要重启下 kubelet 进程该配置即可生效。

添加其他节点

1
2
kubeadm join 172.21.115.189:6443 --token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:457fba2c4181a5b02d2a4f202dfe20f9ce5b9f2274bf40b6d25a8a8d4a7ce440

此时即可以将节点添加到 k8s 集群中

1
2
3
4
$ kubectl  get node 
NAME STATUS ROLES AGE VERSION
k8s002 Ready control-plane 79m v1.25.4
k8s003 Ready <none> 35s v1.25.4

节点清理

清理普通节点

1
2
3
4
5
kubectl drain <node name> --delete-emptydir-data --force --ignore-daemonsets
kubeadm reset
# 清理 iptabels 规则
iptables -F && iptables -t nat -F && iptables -t mangle -F && iptables -X
kubectl delete node <node name>

清理 control plan 节点

1
kubeadm reset

集群模式部署

待补充,参考文档:Creating Highly Available Clusters with kubeadm | Kubernetes

资料

0%