404频道

学习笔记

在 k8s 中使用 cgroup 的 CFS 配额来执行 pod 的 CPU 约束,在 CFS 的模式下,pod 可能会运行在不同的核上,会导致 pod 的缓存失效的问题。对于性能要求非常高的 pod,为了提升性能,可以通过 cgroup 中的 cpuset 绑核的特性来提升 pod 的性能。

在 k8s 中仅针对如下的 pod 类型做了绑核操作:

  1. 必须为 guaranteed pod 类型。即 pod 需要满足如下两个条件:
    1. pod 中的每个容器都必须指定cpu 和 内存的 request 和 limit。
    2. pod 中的每个容器的 cpu 和内存的 request 和 limit 必须相等。
  2. pod 的 cpu request 和 limit 必须为整数。

kubelet 的参数配置

在 k8s 中仅通过 kubelet 来支持 pod 的绑核操作,跟其他组件无关。
kubelet 通过参数 --cpu-manager-policy 或者在 kubelet 的配置文件中参数cpuManagerPolicy 来配置,支持如下值:

  1. none:默认策略。即不执行绑核操作。
  2. static:允许为节点上的某些特征的 pod 赋予增强的 cpu 亲和性和独占性。

kubelet 通过参数 --cpu-manager-reconcile-period 来指定内存中的 cpu 分配跟 cgroupfs 一致。
kubelet 通过参数 --cpu-manager-policy-options来微调。该特性通过特性门控 CPUManagerPolicyOptions 来控制。

模式之间切换

默认的 kubelet cpuManagerPolicy 配置为 none,策略配置位于文件 /var/lib/kubelet/cpu_manager_state,文件内容如下:

1
{"policyName":"none","defaultCpuSet":"","checksum":1353318690}

修改 kubelet 的cpuManagerPolicy 为 static,将模式从 none 切换为 static,重启 kubelet。发现 kubelet 会启动失败,kubelet 并不能支持仅修改参数就切换模式,报如下错误:

1
2
Mar 06 15:00:02 iZt4nd5yyw9vfuxn3q2g3tZ kubelet[102800]: E0306 15:00:02.463939  102800 cpu_manager.go:223] "Could not initialize checkpoint manager, please drain node and remove policy state file" err="could not restore state from checkpoint: configured policy \"static\" differs from state checkpoint policy \"none\", please drain this node and delete the CPU manager checkpoint file \"/var/lib/kubelet/cpu_manager_state\" before restarting Kubelet"
Mar 06 15:00:02 iZt4nd5yyw9vfuxn3q2g3tZ kubelet[102800]: E0306 15:00:02.463972 102800 kubelet.go:1392] "Failed to start ContainerManager" err="start cpu manager error: could not restore state from checkpoint: configured policy \"static\" differs from state checkpoint policy \"none\", please drain this node and delete the CPU manager checkpoint file \"/var/lib/kubelet/cpu_manager_state\" before restarting Kubelet"

将文件 /var/lib/kubelet/cpu_manager_state 删除后,kubelet 即可启动成功,新创建的 /var/lib/kubelet/cpu_manager_state 文件内容如下:

1
{"policyName":"static","defaultCpuSet":"0-3","checksum":611748604}

可以看到已经存在了绑核的 pod。

如果节点上已经存在符合绑核条件的 pod,在修改配置并重启 kubelet 后即可绑核生效。

绑核实践

配置 kubelet 的cpuManagerPolicy值为 static,创建 guaranteed pod:

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
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
resources:
limits:
memory: "200Mi"
cpu: "2"
requests:
memory: "200Mi"
cpu: "2"

在 pod 调度的节点上,进入到 /sys/fs/cgroup/cpuset/kubepods.slice 目录下,跟绑核相关的设置均在该目录下,该目录的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ ll /sys/fs/cgroup/cpuset/kubepods.slice
-rw-r--r-- 1 root root 0 Mar 6 10:31 cgroup.clone_children
-rw-r--r-- 1 root root 0 Mar 6 10:31 cgroup.procs
-rw-r--r-- 1 root root 0 Mar 6 10:31 cpuset.cpu_exclusive
-rw-r--r-- 1 root root 0 Mar 6 10:31 cpuset.cpus
-r--r--r-- 1 root root 0 Mar 6 10:31 cpuset.effective_cpus
-r--r--r-- 1 root root 0 Mar 6 10:31 cpuset.effective_mems
-rw-r--r-- 1 root root 0 Mar 6 10:31 cpuset.mem_exclusive
-rw-r--r-- 1 root root 0 Mar 6 10:31 cpuset.mem_hardwall
-rw-r--r-- 1 root root 0 Mar 6 10:31 cpuset.memory_migrate
-r--r--r-- 1 root root 0 Mar 6 10:31 cpuset.memory_pressure
-rw-r--r-- 1 root root 0 Mar 6 10:31 cpuset.memory_spread_page
-rw-r--r-- 1 root root 0 Mar 6 10:31 cpuset.memory_spread_slab
-rw-r--r-- 1 root root 0 Mar 6 10:31 cpuset.mems
-rw-r--r-- 1 root root 0 Mar 6 10:31 cpuset.sched_load_balance
-rw-r--r-- 1 root root 0 Mar 6 10:31 cpuset.sched_relax_domain_level
drwxr-xr-x 2 root root 0 Mar 6 10:31 kubepods-besteffort.slice
drwxr-xr-x 10 root root 0 Mar 6 10:31 kubepods-burstable.slice
drwxr-xr-x 4 root root 0 Mar 6 15:09 kubepods-pod4dc3ad18_5bad_4728_9f79_59f2378de46e.slice
-rw-r--r-- 1 root root 0 Mar 6 10:31 notify_on_release
-rw-r--r-- 1 root root 0 Mar 6 10:31 pool_size
-rw-r--r-- 1 root root 0 Mar 6 10:31 tasks

其中 kubepods-besteffort.slice 和 kubepods-burstable.slice 分别对应的 besteffort 和 burstable 类型的 pod 配置,因为这两种类型的 pod 并不执行绑核操作,所有的子目录下的 cpuset.cpus 文件均绑定的 cpu 核。
kubepods-pod4dc3ad18_5bad_4728_9f79_59f2378de46e.slice 目录为要绑核的 pod 目录,其中 4dc3ad18_5bad_4728_9f79_59f2378de46e 根据 pod 的 uid 转换而来,pod 的 metadata.uid 字段的值为 4dc3ad18-5bad-4728-9f79-59f2378de46e,即目录结构中将-转换为_
目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ ll /sys/fs/cgroup/cpuset/kubepods.slice/kubepods-pod4dc3ad18_5bad_4728_9f79_59f2378de46e.slice
-rw-r--r-- 1 root root 0 Mar 6 15:09 cgroup.clone_children
-rw-r--r-- 1 root root 0 Mar 6 15:09 cgroup.procs
-rw-r--r-- 1 root root 0 Mar 6 15:09 cpuset.cpu_exclusive
-rw-r--r-- 1 root root 0 Mar 6 15:09 cpuset.cpus
-r--r--r-- 1 root root 0 Mar 6 15:09 cpuset.effective_cpus
-r--r--r-- 1 root root 0 Mar 6 15:09 cpuset.effective_mems
-rw-r--r-- 1 root root 0 Mar 6 15:09 cpuset.mem_exclusive
-rw-r--r-- 1 root root 0 Mar 6 15:09 cpuset.mem_hardwall
-rw-r--r-- 1 root root 0 Mar 6 15:09 cpuset.memory_migrate
-r--r--r-- 1 root root 0 Mar 6 15:09 cpuset.memory_pressure
-rw-r--r-- 1 root root 0 Mar 6 15:09 cpuset.memory_spread_page
-rw-r--r-- 1 root root 0 Mar 6 15:09 cpuset.memory_spread_slab
-rw-r--r-- 1 root root 0 Mar 6 15:09 cpuset.mems
-rw-r--r-- 1 root root 0 Mar 6 15:09 cpuset.sched_load_balance
-rw-r--r-- 1 root root 0 Mar 6 15:09 cpuset.sched_relax_domain_level
drwxr-xr-x 2 root root 0 Mar 6 15:09 cri-containerd-75f8e4b4185f673869604d300629ec2de934cf253244bf91c577f9fc0ba0f14a.scope
drwxr-xr-x 2 root root 0 Mar 6 15:09 cri-containerd-cce0a338829921419407fcdc726d1a4bd5d4489da712b49a4834a020131ce718.scope
-rw-r--r-- 1 root root 0 Mar 6 15:09 notify_on_release
-rw-r--r-- 1 root root 0 Mar 6 15:09 pool_size
-rw-r--r-- 1 root root 0 Mar 6 15:09 tasks

其中 cri-containerd-75f8e4b4185f673869604d300629ec2de934cf253244bf91c577f9fc0ba0f14a.scope 和 cri-containerd-cce0a338829921419407fcdc726d1a4bd5d4489da712b49a4834a020131ce718.scope 为 pod 的两个容器,其中一个为 pause 容器,另外一个为 nginx 容器。

1
2
$ crictl pods | grep nginx-deploy
cce0a33882992 6 hours ago Ready nginx-deployment-67778646bb-mgcpg default 0 (default)

其中第一列的 cce0a33882992 为 pod id,cri-containerd-cce0a338829921419407fcdc726d1a4bd5d4489da712b49a4834a020131ce718.scope 对应的为 pause 容器,查看该目录下的 cpuset.cpus 文件,绑定了所有的核,并未做绑核操作。

1
2
$ crictl ps | grep nginx-deploy
75f8e4b4185f6 e4720093a3c13 6 hours ago Running nginx 0 cce0a33882992 nginx-deployment-67778646bb-mgcpg

其中第一列的 75f8e4b4185f6 为 container id,cri-containerd-75f8e4b4185f673869604d300629ec2de934cf253244bf91c577f9fc0ba0f14a.scope 对应的为 nginx 容器。查看 /sys/fs/cgroup/cpuset/kubepods.slice/kubepods-pod4dc3ad18_5bad_4728_9f79_59f2378de46e.slice/cri-containerd-75f8e4b4185f673869604d300629ec2de934cf253244bf91c577f9fc0ba0f14a.scope/cpuset.cpus 对应的值为2-3,说明绑核成功。

/var/lib/kubelet/cpu_manager_state 文件内容如下,跟 cgroup cpuset 实际配置可以完全对应:

1
{"policyName":"static","defaultCpuSet":"0-1","entries":{"4dc3ad18-5bad-4728-9f79-59f2378de46e":{"nginx":"2-3"}},"checksum":3689800814}

kubelet 内部实现

在 kubelet 内部,采用 cpu manager 模式实现绑核功能。

缺点

  1. 只能 by 节点配置,不能按照 pod 来灵活的配置。
  2. 无法适用于所有类型的 pod,pod 必须为 Guaranteed 时才允许开启。
  3. 修复配置较为麻烦,一旦配置变更后,需要删除文件 /var/lib/kubelet/cpu_manager_state 后才可生效,而且对现有的 pod 均有影响。

资料

设置评论系统

我的博客系统 Hexo 之前使用 Gitment 作为评论:《hexo添加gitment评论系统》,后来年久失修,了解了下基于 Github Discussions 的 giscus 用的比较多,我也采用该方案。

在配置 giscus 之间需要满足如下三个条件:

  1. 该仓库是公开的,否则访客将无法查看 discussion。
  2. giscus app 已安装,否则访客将无法评论和回应。
  3. Discussions 功能已在你的仓库中启用。

在 Github 仓库中安装 gitcus

我这里直接选择了我的 hexo 仓库 kuring/kuring.github.io 作为了 git 仓库,因为该仓库本来的用途本来就是博客相关的内容,权限也是 public 的。

接下来安装 Github app giscus,访问 https://github.com/apps/giscus,在安装时仅选择需要的 repo 就可以了,不需要所有的 repo 都放开。

在 Github 仓库中打开 Discussions 功能

进入到 Github 项目的 Settings -> General -> Features -> Discussions 即可打开该功能。

获取到 Github 项目相关的 gitcus 配置

访问页面 https://giscus.app/zh-CN,在`配置`章节中设置相关的配置,可以获取到类似下面内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script src="https://giscus.app/client.js"
data-repo="kuring/kuring.github.io"
data-repo-id="MDEwOlJlcG9zaXRvcnkyODM4MzQ0NTk="
data-category="Announcements"
data-category-id="DIC_kwDOEOr4W84CdeTU"
data-mapping="url"
data-strict="0"
data-reactions-enabled="1"
data-emit-metadata="0"
data-input-position="bottom"
data-theme="preferred_color_scheme"
data-lang="zh-CN"
data-loading="lazy"
crossorigin="anonymous"
async>
</script>

在 Hexo 中配置 gitcus

因为 Hexo 我使用的为 Next 主题,我使用了 Next 的插件 hexo-next-giscus

在 Git 仓库目录下执行 npm install hexo-next-giscus 即可安装 giscus。

修改 Git 仓库下的 _config.yml 文件,在最后面增加如下的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
giscus:
enable: false
repo: # Github repository name
repo_id: # Github repository id
category: # Github discussion category
category_id: # Github discussion category id
# Available values: pathname | url | title | og:title
mapping: pathname
# Available values: 0 | 1
reactions_enabled: 1
# Available values: 0 | 1
emit_metadata: 1
# Available values: light | dark | dark_high_contrast | transparent_dark | preferred-color-scheme
theme: light
# Available values: en | zh-CN
lang: en
# Place the comment box above the comments
input_position: bottom
# Load the comments lazily
loading: lazy

其中很多的字段值来自上个章节获取到的 json 结果。

至此,即完成了整个 gitcus 的配置。

资料

在前序文章中,实现了将树莓派 5 刷成了 Android TV 电视盒子,基本的功能已经完备,本文将来介绍如何使用遥控器来控制 Android TV。

树莓派的定位是用作小型计算机使用,而作为电视盒子却必须要具备遥控器来控制的功能。要想通过遥控器来控制树莓派有多种方式可供选择,下面列一下我自己尝试过的方案。

使用手机遥控 Android TV

这是最简单成本最低的纯软方案,只要在手机上安装软件就可以来控制 Android TV 了,手机跟 Android TV 在同一个局域网下即可。我使用到了 IOS 上的 App Remote TV,软件免费,但有广告。
IMG_2890.PNG
使用上功能完备,比如开关 Android TV 的功能也是完备的。Android TV 的关系操作并非真正的关机,实际上还是在低功耗运行,网络还是可以连接的。也就意味着通过 App 实际上是可以开机的。
不方便的地方在于,每次操作需要额外拿起手机打开 App 后才能操作,没有硬件的遥控器来的方便。

使用投影仪的遥控器

我使用了爱普生的 TZ2800 投影仪作为播放设备,投影仪的 HDMI 已经支持了 HDMI CEC 功能。这里吐槽下传统企业爱普生,在官方产品页面中很难找到关于该款投影仪的 HDMI 是否支持 CEC 的介绍,或者存在某个地方,但总归我找不到。

HDMI CEC(Consumer Electronics Control) 功能可以实现通过 HDMI 连接让多个设备之间相互控制,可以实现同一个遥控器来控制投影仪和 Android TV 的功能:

  1. 当关闭投影仪的时候也可以关闭 Android TV。
  2. 当打开投影仪的时候可以打开 Android TV。

要想使用 HDMI CEC 功能,需要在 Android TV 系统中开启相关的功能。
IMG_2893.HEIC
下面的界面用来选择 HDMI CEC 的支持接口,只能支持一个 HDMI 接口。
IMG_2894.HEIC

由于跟投影仪共用一个遥控器,功能上还是有所受限。比如音量的大小功能,只能操作投影仪,而不能再设置 Android TV 的音量大小。但绝大多数的功能已经具备。

使用蓝牙遥控器

家里正巧有个办移动宽带送的电视盒子,该遥控器为蓝牙遥控器。在遥控器的背面正巧有蓝牙配对的方法。
IMG_2891.HEIC

打开 Android Tv 系统中的 设置 -> 系统 -> 遥控器和手柄 功能。同时按住菜单键和返回键即可蓝牙配对。
IMG_2892.HEIC

蓝牙配对成功后,使用过程中功能正常。但发现个致命的缺点:蓝牙会自动断开,而且无法自动连接,下次只能重新配对。我不太确定是否为遥控器的问题,但单这一点已经否定了我使用蓝牙遥控器的方案。

使树莓派设备支持遥控器控制

树莓派通过 24 个 GPIO 引脚提供了强大的扩展性,可以接入外部硬件设备来接收遥控器的信号。
我们来了解下常见的遥控器的实现原理:

  1. 红外遥控器:最为普遍的遥控器,使用红外线发射信号,接收端需要有对应的红外接收模块。在发射信号时需要对准接收端。
  2. 无线遥控器:通常采用了 2.4 GHz 无线频段作为传输介质,接收端需要有可以匹配的设备接收信号,比如很多无线鼠标提供了一个很小的 USB 接收端。
  3. 蓝牙遥控器:使用蓝牙技术通讯,而蓝牙技术同样采用了 2.4 GHz 的无线频段,但好处在于很多的设备都支持蓝牙协议,不需要接收端再有一个定制化的硬件。

接收红外信号

我家里正巧有几个废弃的红外遥控器,因此可以作为树莓派的遥控器来使用。剩下的事情首先需要树莓派通过 GPIO 引脚接收到红外信号。
在万能的淘宝上,花了几块钱买到了红外接收器和杜邦线(用来连接红外接收头和 GPIO 引脚),杜邦线需要注意为母对母规格。
image.png
image.png

红外接收器连接到树莓派

在默认情况下,树莓派 GPIO 引脚的功能并未开启。打开 Android TV 系统中的 Raspberry Pi settings -> IR -> Infrared remote开关。可以看到接收信号的为 GPIO 18 引脚。

IMG_2887.HEIC

在红外接收器上包含了三个引脚,依次为:OUT(输出信号)、GND(接地信号)、VCC(工作电压),其中红外接收器的 OUT 引脚对应的 GPIO 18 引脚。

image.png

树莓派提供了 3.3V 和 5V 两种工作电压,查看红外信号接收器的工作电压在 2.7V ~ 5.5V 之间,我直接使用了 GPIO 的 5V 的引脚。

image.png
查看树莓派的各个引脚线路图:

来源:https://pinout.vvzero.com/
即可得到红外信号接收器跟 GPIO 引脚的对应关系:

红外信号接收器引脚 GPIO 引脚
OUT 18
GND 9(就近原则)
VCC 2(4 和 6 被风扇电源占用)

在将硬件连接好后就可以启动树莓派了。

遥控器编码配对

家里可能有多个红外遥控器,那么为什么红外遥控器之间不会出现相互冲突,本来想打开电视,却打开了空调的情况呢?原因是红外遥控器上的每个按键都对应了一个编码,而不同设备的遥控器对应的编码是不同的。电视遥控器发出的开机信号虽然被空调接收到了,但是并不会对该按键的编码做处理,空调也就不会被开机。
我从家里随便找了一个红外遥控器也就不可能直接用来控制 Android TV 了,因此需要让 Android TV 可以识别到我的红外遥控器对应的按键编码。
要想识别到红外遥控器的编码,需要使用到 Android TV 系统中命令行工具 ir-keytable,需要以 ssh 的方式连接 Android TV。Android TV 的连接方式可以参考我之前文章《我要看电视 - 将树莓派 5 打造成 Android TV 电视盒子折腾记(1)》中的 SSH 章节部分。

在命令行执行 ir-keytable -p all -t,并按下遥控器的按键,在命令行中可以获取到类似如下的输出:

1
2
3
4
5
6
7
991.988018: lirc protocol(necx): scancode = 0x835590
991.988023: event type EV_MSC(0x04): scancode = 0x835590
991.988023: event type EV_SYN(0x00).

992.104019: lirc protocol(necx): scancode = 0x835590
992.104025: event type EV_MSC(0x04): scancode = 0x835590
992.104025: event type EV_SYN(0x00).

其中 916.968014 表示的事件发生的时间戳。necx 表示使用的为 NEC 扩展协议,NEC 为一种常见的红外编码协议。而 scancode 字段即为我们需要获取的按键编码。EV_SYN 表示一组时间的结束。
在上面的命令中,按下遥控器的按键一次,发送出了两个相同信号,也就打印出了两个相同日志块。

打开 Github 项目:https://github.com/lineage-rpi/android_external_ir-keytable/tree/lineage-18.1/rc_keymaps,可以看到里面有很多编码跟对应键之间的映射关系,因为我的红外遥控器没有数字键,我挑选了一个跟遥控器键比较相似的文件进行重新修改,将其中的编码修改我遥控器对应的编码,并保存到本地文件 rc_keymap.txt 中。我的文件类似如下,其中 KEY_PREVIOUSSONG 和 KEY_PLAYPAUSE 两个按键我的遥控器上没有,我这里未做修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# table allwinner_ba10_tv_box, type: NEC
0x646dca KEY_UP
0x646d81 KEY_VOLUMEDOWN
0x217 KEY_NEXTSONG
0x646ddc KEY_POWER
0x646dc5 KEY_BACK
0x646dce KEY_OK
0x646dd2 KEY_DOWN
0x646d80 KEY_VOLUMEUP
0x254 KEY_PREVIOUSSONG
0x255 KEY_PLAYPAUSE
0x646d82 KEY_MENU
0x646d88 KEY_HOMEPAGE
0x646dc1 KEY_RIGHT
0x646d99 KEY_LEFT

需要注意的是,第一行看似是个注释,实际不是,不要删掉,保留原文件中的格式不变。

Android TV 系统开机时加载配置

有了遥控器按键和编码之间的映射关系后,需要将其放到 Android TV 系统的 /boot/rc_keymap.txt 文件中,让 Android TV 开启自动加载该配置。该文件默认情况下不存在需要创建。

分区 /boot 默认为只读模式,不允许在其中增加文件:

1
2
127|:/boot # echo "123" > aa
sh: can't create aa: Read-only file system

将 /boot 目录重新 mount 为读写模式:

1
2
3
4
5
6
7
$ mount  | grep '/boot '
/dev/block/mmcblk0p1 on /boot type vfat (ro,relatime,fmask=0000,dmask=0000,allow_utime=0022,codepage=437,iocharset=ascii,shortname=mixed,errors=remount-ro)

$ mount -o remount,rw /boot

$ mount | grep '/boot '
/dev/block/mmcblk0p1 on /boot type vfat (rw,relatime,fmask=0000,dmask=0000,allow_utime=0022,codepage=437,iocharset=ascii,shortname=mixed,errors=remount-ro)

创建并将配置写入到文件 /boot/rc_keymap.txt 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cat << EOF > /boot/rc_keymap.txt
# table allwinner_ba10_tv_box, type: NEC
0x646dca KEY_UP
0x646d81 KEY_VOLUMEDOWN
0x217 KEY_NEXTSONG
0x646ddc KEY_POWER
0x646dc5 KEY_BACK
0x646dce KEY_OK
0x646dd2 KEY_DOWN
0x646d80 KEY_VOLUMEUP
0x254 KEY_PREVIOUSSONG
0x255 KEY_PLAYPAUSE
0x646d82 KEY_MENU
0x646d88 KEY_HOMEPAGE
0x646dc1 KEY_RIGHT
0x646d99 KEY_LEFT
EOF

重新将 /boot 分区挂载为只读模式:

1
mount -o remount,ro /boot

在执行完后,重新启动系统。在顺利的情况下,即可以通过红外遥控器直接操作 Android TV 系统。

总结

到目前为止,我主要在使用的方式为红外遥控器模式,毕竟一个电视盒子还是应该有属于自己的遥控器。Good Luck!

随着处理器数量的增加,所有的处理器均通过同一个北桥来访问内存,导致内存访问延迟增加,内存带宽成为瓶颈。

NUMA(Non-Uniform Memory Access)非统一内存访问,是一种针对多处理器系统的组织结构。处理器被分配到不同的节点,每个节点有自己的本地内存,处理器可以访问本地内存和其他节点的内存,但访问本地内存的速度要远远快于访问其他节点的内存。

几个概念:

  1. Socket:一颗物理 CPU。
  2. Numa Node:逻辑概念,对 CPU 分组的抽象,一个 Node 即为一个分组,一个分组下可以包含多个 CPU。每个 Node 都有自己的本地资源,包括内存和 IO。Numa Node 之间不共享 L3 cache。一个 Numa Node 内的不同 core 之间共享 L3.
  3. Core:一颗物理 CPU 的物理核,每个 Core 有独立的 L1 和 L2,Core 之间共享 L3。
  4. Thread/Processor:一颗物理 CPU 的逻辑核,用超线程的方式模拟出两个逻辑核。Processor 之间共享 L1 和 L2 cache,在开启超线程后单核的性能会下降一些。

一个 NUMA Node 可以有一个 Socket,每个 Socket 包含一个或者多个物理 Core。

常用命令

查看物理核数 Socket:lscpu | grep Socket
每个 Socket 包含的物理核 Core:lscpu | grep 'Core(s) per socket'
每个 Socket 包含的 Thead/Processor 数量:lscpu | grep 'Thread(s) per core'
查看所有的 Processor 数量:cat /proc/cpuinfo | grep "processor" | wc -l

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ numactl --hardware
available: 4 nodes (0-3)
node 0 cpus: 0 1 2 3 4 5 6 7 32 33 34 35 36 37 38 39
node 0 size: 128470 MB
node 0 free: 88204 MB
node 1 cpus: 8 9 10 11 12 13 14 15 40 41 42 43 44 45 46 47
node 1 size: 129019 MB
node 1 free: 67982 MB
node 2 cpus: 16 17 18 19 20 21 22 23 48 49 50 51 52 53 54 55
node 2 size: 129019 MB
node 2 free: 38304 MB
node 3 cpus: 24 25 26 27 28 29 30 31 56 57 58 59 60 61 62 63
node 3 size: 128965 MB
node 3 free: 45689 MB
node distances:
node 0 1 2 3
0: 10 16 28 22
1: 16 10 22 28
2: 28 22 10 16
3: 22 28 16 10

如果没有开启 numa,默认只会看到一个 node,类似如下:

1
2
3
4
5
6
7
8
$ numactl --hardware
available: 1 nodes (0)
node 0 cpus: 0 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
node 0 size: 772904 MB
node 0 free: 510590 MB
node distances:
node 0
0: 10

通过 numastat 命令可以看到 numa 的 miss 等情况,对于排查性能问题非常有帮助。

1
2
3
4
5
6
7
8
$ numastat
node0 node1 node2 node3
numa_hit 6010986094 3863188690 7861549559 9798877648
numa_miss 0 0 113102530 0
numa_foreign 0 0 0 113102530
interleave_hit 17976 17846 17965 17839
local_node 6010748681 3862984578 7861301902 9798751537
other_node 237411 204112 113350178 126111

资料

在上一篇文章 《我要看电视 - 投影仪和电视盒子选型》中,提到了用树莓派 5 来作为电视盒子,需要刷 Android TV 系统,本文将详细介绍折腾经历。
在这之前我并没有接触过树莓派,Android 系统倒是曾经刷过机,但也了解不深。文中如有不正确的地方,欢迎指正。

准备工作

要想折腾树莓派,还是需要一点点设备,我使用到的设备清单如下:

  1. 一台 Mac 笔记本电脑:用来下载镜像、烧录景象、远程连接电视盒子等。其他操作系统的电脑均可。
  2. 一个 32G SD 卡:用来作为树莓派的存储系统 RAM。
  3. 一个 SD 读卡器:用来读取 SD 卡,供电脑烧录系统使用。
  4. 一个 U 盘:该 U 盘并非树莓派的 SD 卡,而是作为树莓派的额外存储,用来向树莓派中复制文件。
  5. 一台显示器:用来显示树莓派的内容,也可以是电视或者投影仪等设备。
  6. 一个 USB 键盘:用来连接树莓派进行操作。
  7. 一个 USB 鼠标:用来连接树莓派进行操作。
  8. 一个蓝牙遥控器:用来连接树莓派的 Android TV 系统。我直接使用了家里办宽带送的移动电视盒子的遥控器。
  9. 一根 HDMI 数据线:其中一头为 Micro HDMI,用来连接树莓派,另外一头为标准 HDMI 口,用来连接显示器。

另外:

  1. 网络需要能够访问 Google 等网站。

寻找 Android TV 系统

找到树莓派对应的 Android TV 镜像是非常重要的前提,找到合适的 ROM 永远是 Android 系统刷机中非常重要的一环,一般每个设备总有那么几个大神在提供各类 ROM。
查找一个设备的资源最快的方式就是去 github.com 上查找 awesome,树莓派的项目地址为:awesome-raspberry-pi。在 OS Images 章节中包含了支持树莓派的多种 OS,单纯搜索 Android TV 并不能找到对应的 OS,但实际上 KonstaKANG 对应就是 Android 镜像,这是文档不好的地方,并没有将简介写清楚。
image.png

KonstaKANG 并非一个系统,而是一个网站,包含了多种设备的 OS 镜像。对应的 Raspberry 5 的页面包含了 AOSP 和 LineageOS 两种 OS 镜像,AOSP 和 LineageOS 均为 android 的发型版。但只提供了 LineageOS 20 一个 Android TV 的版本,剩下的两个为 Android 版本。Android TV 和 Android 并非同一个系统,Android TV 是针对电视使用的系统,而 Android 是针对智能手机和平板使用的系统,用户体验上还是有较大区别。
image.png
因此,LineageOS 20 Android TV (Android 13) 即变为了目前唯一一个可以在树莓派 5 上使用 Android TV 系统,除此之外,别无他选。无论该系统是否完善,这就是目前的唯一选择了。在文章中介绍了非常多的系统方面的支持,建议精读一遍,包括文章后面的评论。
文中的两个镜像,一个是原始镜像,另外一个是 ota 补丁包,两个均需要安装。
image.png

用到的文件

由于在国内下载非常不稳定,通常需要魔法下载,我将需要用到的文件上传到云盘供下载使用。我对部分软件的用途一知半解,但我知道安装了应该没有坏处,选择全部安装。

刷 LineageOS 到树莓派

树莓派官方提供了镜像烧录工具 Imager,支持 Windows、Ubuntu、Mac,刷机非常方便。image.png
将树莓派的 SD 插入到电脑,在 Raspberry Pi Device 中选择 RASPBERRY PI 5,在请选择需要写入的操作系统中使用 Use custom 选项选择已经下载好的文件 lineage-20.0-20240112-UNOFFICIAL-KonstaKANG-rpi5-atv.zip,选择对应的 SD 卡,再点击 Next 即可开始将镜像写入到SD 卡中。
值得一提的是,对于树莓派其他的系统,都不需要事先下载,选择系统后,该工具可以自动下载最新版本,这个功能还是非常值得👍。

LineageOS 初步体验及初步设置

在将系统刷入 SD 卡后,即可将 SD 卡放到树莓派 5 中,在树莓派中连接好键盘和鼠标。
下面步骤中可能会用到键盘的一些快捷键:F1 = Home, F2 = Back, F3 = Multi-tasking, F4 = Menu, F5 = Power, F11 = Volume down, and F12 = Volume up
开机后即可看到 LineageOS 的开机动画。
image.png

接下来就会看到查找蓝牙设备的界面,这里可以选择等待一会后自动跳过。
image.png

接下来就来到了 Welcome 的界面,点击 Start
image.png

接下来会进入到选择语言、连接 WIFI 等操作,完成后即可进入到操作系统界面。操作系统界面可谓简洁到不能再精简,只有一个文件的应用。左上角的语音和搜索功能均不能使用。
image.png

但设置功能作为 Android 系统的核心,这部分功能一点都不会少。在设置中连接 Wifi 后发现会持续断开,不知道是否为系统的问题。

打开 Recovery 模式:设置 -> 系统 -> Buttons,打开右侧的 Advanced restart。
image.png

打开开发者模式:设置 -> 系统 -> 关于 -> Android TV 操作系统版本,连续点击键盘的回车键,会提示开发者模式已打开。
image.png

打开 Rooted debug:设置 -> 系统 -> 开发者选项 -> 打开 USB 调试Rooted debuggingADB over network 三个选项。

打开树莓派的 SSH 服务:设置 -> 系统 -> Raspberry Pi Settings 中将 SSH 服务打开。
image.png

刷入 ota 包

接下来选择刷入 OTA 包 lineage-20.0-20240112-UNOFFICIAL-KonstaKANG-rpi5-atv-ota.zip,ota 包不会用到树莓派镜像烧录器,而是要通过Android Recovery 模式刷入。需要事先准备好 OTA 包,并将其复制到 U 盘中,并将 U 盘插入到树莓派中。

在 Android 系统设置 -> 系统 -> 重新启动中选择 Recovery,此时系统会重启进入到 Recovery 模式。
image.png

点击 Install,并选择右下角的 Select Storage 按钮,选择 USB 存储,即刚插入的 U 盘。
image.png

在 U 盘中选择要刷入的 ota 补丁,取消 Zip signature verification
image.png

Swipe to confirm Flash处向右滑动鼠标,即可进入到安装 ota 补丁的界面。ota 补丁安装完成后,即可自动重启进入到 Android TV 系统中。重新进入系统后,发现系统的界面没有任何变化,不知道该 ota 补丁的具体影响功能。
image.png

系统存储空间设置

在默认的情况下,打开设置 -> 存储空间,可以看到内部共享存储空间仅为 4.7 GB,这里的存储空间为 /data 目录挂载的设备,并没有将 U 盘的空间全部使用起来,U 盘剩余的磁盘空间处于未分配状态。
image.png

这里使用 lineage-20.0-rpi-resize.zip 工具来修改存储空间,按照刷入 ota 包相同的方式,进入 Recovery 模式下刷入该包,重新进入系统后即发现存储空间已经变大。
image.png

安装 adb 工具

前置条件:开发者选项中的 ADB over network 必须为开启状态。

adb 命令需要安装到电脑上,在 mac 下使用 brew install android-platform-tools即可安装完成。

在 android tv 上查看当前的 ip 地址,我这里为 192.168.31.167。执行 adb connect 192.168.31.167 后即可获取到 USB 调试信息,并选中一律允许使用这台计算机进行调试处后点击允许。
image.png
此时在终端中即可显示出连接成功的信息:already connected to 192.168.31.167:5555

在安装完 adb 工具后,即可以通过 adb 命令来远程访问 Android TV 系统了。例如:

  • adb install 可以安装 apk 包。
  • adb shell settings list global 命令来查看 Android 系统的所有global 配置。
  • adb logcat 查看 android 的日志。

远程 ssh 连接

前置条件:

  1. ssh 连接需要首先在 Raspberry Pi Settings 中的 Remote access 中打开 SSH。
  2. adb connect 可以连接到 Android TV。

在开启 SSH 服务后,ssh 连接需要使用对应的私钥信息,而私钥信息需要通过 adb 命令获取。

1
2
3
4
5
6
7
8
9
adb connect 192.168.31.166
adb root
# 可查看 /data/ssh 目录下的 ssh 秘钥信息
adb shell ls /data/ssh
# 将秘钥信息放到本地
adb pull /data/ssh/ssh_host_ed25519_key ~/.ssh/android_tv.key
chmod 600 ~/.ssh/android_tv.key
# 成功 ssh 连接
ssh -i ~/.ssh/android_tv.key root@192.168.31.166

安装 Google Apps

注意:此处要求网络具备魔法,可以访问 Google

刷入 Google Apps 包

Google Apps 包为必须用到的包,通过 Recovery 模式刷入包 MindTheGapps-13.0.0-arm64-ATV-full-20240104_210039.zip。包安装完成后,重新进入系统发现界面发生了变化,多出了应用 Google Play Store,左上角的语音和搜索功能虽然不可以用,但是点击后提示信息已经发生了变化。

原来的文件应用在这里消失不见了,实际上在所有应用中还可以找到。

image.png
在系统中使用 Google 账号登录 Google Play Store,即使在可以访问 Google 的网络下,发现也一直会失败。

查询并注册 Android ID

因为该 Android TV 设备并不被信任,需要将 Android ID 在 Android 网站注册。如果不注册,那么 Google 账号登录不成功。
将树莓派的 SD 卡插入到笔记本,查看 SD 卡中的文件 gsf-android_id.txt,该文件对应的内容即为 Android ID。
还有另外一种办法可以获取到 Android ID,在笔记本上通过 adb 命令查询到当前设备的 Android ID。

1
2
3
4
5
6
7
adb root
# 找到 google service 的 sqlite3 数据库文件
adb shell 'find /data -name "gservices.db"'

# 通过数据库查询到 android id
# 其中 sqlite3 命令后的为上面步骤查询出的文件路径,如果查询出多个,可以任选一个
adb shell 'sqlite3 /data/data/com.google.android.gsf/databases/gservices.db "select * from main where name = \"android_id\";"'

将上述 Android ID 在网站进行注册,网址:https://www.google.com/android/uncertified/
image.png

使用 Google 账号登录

重新进入 Recovery 模式,点击 Wipe -> Factory reset,此时机器会进行重启。该操作会清空系统中的 /data/media 下的内容。(还不太清楚该步骤是否为必须操作)
image.png

机器重新后会重新进入一遍系统的初始化,但现在的初始化界面跟最初的 LineageOS 的初始化有所不同,进入到了 GMS 的开机引导,该步骤中必须要登录到 Google 账号,而且无法跳过。如果 Android ID 没有注册,此时登录 Google 账号一直会失败,导致无法进入到系统中。
image.png

登录完成后在首页可以看到了更多的一些信息:
image.png

通过 Recovery 刷入其他包

widevine 是在 Android 生态下跟数字版权相关的包,通过 Recovery 模式刷入包 lineage-20.0-rpi-widevine.zip。

通过 Recovery 模式刷入包 lineage-20.0-rpi-magisk-v25.2.zip,进入到 Android TV 系统后通过文件工具安装包 Magisk-v25.2.apk。

解决网络连接受限

如果本地的网络无法访问 Google,默认情况下,网络会提示网络连接受限,原因主要还是跟访问不了 Google 的域名有关,以至于 Android TV 系统不能识别出可以连接互联网。
image.png

网络连接受限状态的 WIFI,经测试机器重启后无法自动连接,需要每次都手工连接 WIFI。

执行如下的命令来系统进行设置:

1
2
3
4
5
6
7
8
9
10
adb connect 192.168.31.166

# 设置时间服务器
adb shell settings put global ntp_server ntp1.aliyun.com

# 该值默认为 0
adb shell settings put global captive_portal_detection_enabled 1

# 默认没有这两个值
adb shell settings put global captive_portal_https_url https://connect.rom.miui.com/generate_204

设置完成后重启系统,即可看到网络的连接受限已经消除,并且 WIFI 已经可以自动连接了。

常用 apk 软件安装

我这里使用了 https://kxsw.gitbook.io/tv/ 中的方法安装了 File Commands 和 Clash 软件。File Commands 可以用来管理本地的文件,甚至可以提供 HTTP Server,供远程来下载或者上传文件。
国内的常见应用在 Google Play Store 中并不存在,而且通过 Google Play Store 直接安装应用很可能会失败,跟使用的网络有很大关系。国内的应用我直接使用了当贝市场来安装电视应用即可。

其他问题

听不到声音

在播放视频时发现听不到声音,原因是因为默认情况下使用了 hdmi0 接口来输入声音,通过 hdmi1 只能输出视频信号,没有声音。将 hdmi 线切换到 hdmi0 口后并重启系统后,声音即正常。

后续

到目前为止,树莓派已经具备了完整的 Android TV 系统的功能,而且使用起来还比较稳定,这一点超出了我的预期,毕竟树莓派 5 比较新,该系统出来的时间比较短。
后面我计划使用新的文章来记录使用体验,以及新的折腾经历,比如:如何通过遥控器实现正常的开关机。
敬请期待。。。

资料

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

投影仪的选型

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

  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

0%