404频道

学习笔记

在 k8s 中包含两类用户:

  1. ServiceAccount。又称服务账号,在运行 pod 时必须绑定 ServiceAccount,如果没有指定,则使用当前 namespace 下的 ServiceAccount default。是针对程序而言,用于 pod 中的程序访问 kube-apiserver。
  2. 普通用户。在 k8s 中并没有使用单独的对象来存储,而是通过了分发证书、外部用户认证系统等方式实现,是针对用户而言。

1. X509 证书认证

使用场景:使用 kubectl 访问 k8s 集群即通过 X509 证书认证方式,kubeconfig 本质上是个证书文件。

客户端使用证书中的 Common Name 作为请求的用户名,organization 作为用户组的成员信息。

证书的签发可以使用 openssl、cfssl 等工具来签发,也可以使用 k8s 自带的 CertificateSigningRequest 对象来实现签发。

1.1. CertificateSigningRequest 签发证书

创建私钥信息:

1
2
openssl genrsa -out myuser.key 2048
openssl req -new -key myuser.key -out myuser.csr -subj "/CN=myuser"

创建如下的 CertificateSigningRequest 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cat <<EOF | kubectl apply -f -
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: myuser
spec:
# value 使用命令 cat myuser.csr | base64 | tr -d "\n" 获取
request: xxx
# 固定值
signerName: kubernetes.io/kube-apiserver-client
# 过期时间
expirationSeconds: 86400 # one day
usages:
- client auth
EOF

查看 csr 处于 Pending 状态:

1
2
3
$ kubectl get csr
NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION
myuser 12s kubernetes.io/kube-apiserver-client kubernetes-admin 24h Pending

批准给证书签发请求:

1
kubectl certificate approve myuser

签发完成后的证书会存放到 status.certificate 字段中,至此证书签发完成。

2. ServiceAccount

使用场景:该方式使用较为常见,用于 pod 中访问 k8s apiserver。

原理:pod 可以通过 spec.serviceAccountName 字段来指定要使用的 ServiceAccount,如果没有指定则使用 namespace 下默认的 default ServiceAccount。kube-controller-manager 中的 ServiceAccount 控制器会在拉起的 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
spec:
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: kube-api-access-j6vpz
readOnly: true
volumes:
- name: kube-api-access-j6vpz
projected:
defaultMode: 420
sources:
- serviceAccountToken:
expirationSeconds: 3607
path: token
- configMap:
items:
- key: ca.crt
path: ca.crt
name: kube-root-ca.crt
- downwardAPI:
items:
- fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
path: namespace

即将信息注入到 pod 的 /var/run/secrets/kubernetes.io/serviceaccount 目录下,目录结构如下:

1
2
3
4
/var/run/secrets/kubernetes.io/serviceaccount
|-- ca.crt -> ..data/ca.crt
|-- namespace -> ..data/namespace
`-- token -> ..data/token

token 为 JWT 认证,对其格式解密后如下:

1
2
3
4
{
"alg": "RS256",
"kid": "u7rF5JCtJRNiMzSUOFAYvDpCwPqUII-N-OtxR59cnQ0"
}
1
2
3
4
5
6
7
8
{
"iss": "kubernetes/serviceaccount",
"kubernetes.io/serviceaccount/namespace": "default",
"kubernetes.io/serviceaccount/secret.name": "ingress-token-s9gtm",
"kubernetes.io/serviceaccount/service-account.name": "ingress",
"kubernetes.io/serviceaccount/service-account.uid": "19ce4f11-7105-43ce-b189-f3d71a2ffc74",
"sub": "system:serviceaccount:default:ingress"
}

2.1. Secret 存放 token

在 1.22 版本及之前版本中,token 以 Secret 的形式存在于 pod 所在的 namespace 下,且 token 不会过期。Secret 的名字存在于 ServiceAccount 的 spec 中,格式如下:

1
2
3
4
apiVersion: v1
kind: ServiceAccount
secrets:
- name: nginx-token-scjvn

而 Secret 通过 Annotation kubernetes.io/service-account.name 指定了关联的 ServiceAccount。

在后续版本中,为了兼容当前方案,如果 ServiceAccount 关联了 Secret,则认为仍然使用 Secret 中存放 token 的方式。如果 Secret 已经很长时间没有使用,则自动回收 Secret。

如果要手工创建一个 token Secret,可以创建如下的 Secret,k8s 自动会为 Secret 产生 token:

1
2
3
4
5
6
7
8
apiVersion: v1
kind: Secret
metadata:
name: build-robot-secret
annotations:
# 带有 annotation
kubernetes.io/service-account.name: build-robot
type: kubernetes.io/service-account-token

2.2. TokenRequest API 产生 token

在 1.22 之后的版本中,kubelet 使用 TokenRequest API 获取有时间限制的临时 token,该 token

会在 pod 删除或者 token 生命周期(默认为 1h)结束后失效。

可以使用 kubectl create token default 来为 ServiceAccount default 创建 token,该命令实际上向 kube-apiserver 发送了请求 /api/v1/namespaces/default/serviceaccounts/default/token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"kind": "TokenRequest",
"apiVersion": "authentication.k8s.io/v1",
"metadata":
{
"creationTimestamp": null
},
"spec":
{
"audiences": null,
"expirationSeconds": null,
"boundObjectRef": null
},
"status":
{
"token": "",
"expirationTimestamp": null
}
}

kube-apiserver 支持如下参数:

  1. –service-account-key-file:用来验证服务账号的 token。
  2. –service-account-issuer:ServiceAccount token 的签发机构。
  3. –service-account-signing-key-file:ServiceAccount token 的签发私钥。

3. 用户伪装

一个用户通过 Http Header Impersonation- 的方式来扮演另外一个用户的身份。

场景:跨 k8s 集群访问的网关服务

支持的 Http Header 如下:

  1. Impersonate-User:要伪装的用户名。
  2. Impersonate-Group:要伪装的组名。该 Header 可以为多个,即支持多个组。

4. bootstrap token

使用 kube-apiserver 参数 --enable-bootstrap-token-auth=true 启用功能,引导 token 以 Secret 的形式存放在 kube-system 下。

该功能仅用于节点初始化时加入到 k8s 集群中。

资料

client 端限流

在 client-go 中会默认对客户端进行限流,并发度为 5。可以通过修改 rest.Conifg 来修改并发度。

MaxInFlightLimit 限流

通过如下参数来控制:

  • –max-requests-inflight:代表只读请求的最大并发量
  • –max-mutating-requests-inflight:代表写请求的最大并发量

该实现为单个 kube-apiserver 层面的,可以针对所有的请求。

EventRateLimit

用来对 Event 类型的对象进行限制,可以通过 kube-apiserver 的参数 –admission-control-config-file 来指定配置文件,文件格式如下:

1
2
3
4
5
6
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: EventRateLimit
path: eventconfig.yaml
...

其中 EventRateLimit 为对 Event 的限制,eventconfig.yaml 文件为详细的对 Event 的限流策略,可以精确到 Namespace 和 User 信息。

1
2
3
4
5
6
7
8
9
10
apiVersion: eventratelimit.admission.k8s.io/v1alpha1
kind: Configuration
limits:
- type: Namespace
qps: 50
burst: 100
cacheSize: 2000
- type: User
qps: 10
burst: 50

API 优先级和公平性

版本状态:

  1. alpha:1.18

通过 kube-apiserver 的参数 --enable-priority-fairness 来控制是否开启 APF 特性。

资料

默认的情况下,k8s 对于 pod 在单个节点的资源分配并不会考虑到 NUMA 架构。比如 cpu 默认会采用 cgroup CFS 来做资源的分配,并未考虑到 NUMA 架构。为了提升 pod 的性能,需要 pod 在分配资源时感知 NUMA 架构。
为此,k8s 在 kubelet 中通过 CPU Manager、Memory Manager、Device Manager、Topology Manager 等特性对 NUMA 做了支持,支持的 pod QoS 类型要求为 Granteed pod。
各特性的支持版本情况如下:

特性 alpha beta stable
CPU Manager 1.12 1.26
Memory Manager 1.21 1.22 -
Topology Manager 1.16 1.18 -

CPU Manager

在 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 均有影响。

Memory Manager

kubelet 中参数配置

k8s 1.21 版本引入,需要使用 featuregate 开启,参数 --feature-gates=MemoryManager=true。在k8s 1.22 版本为 beta 版本,默认开启。

kubelet 通过参数 --memory-manager-policy 来配置内存管理策略,支持如下值:

  1. none:默认策略,不执行任何内存分配的策略。
  2. static:仅针对 Guaranteed pod 生效,对于 Guaranteed pod,会返回跟 NUMA 相关的 topology hint 信息。在该模式下,会修改 cgroup cpuset.mems 的配置为对应的 cpu core。

kubelet 将已经分配的 pod 的内存绑定信息位于文件 /var/lib/kubelet/memory_manager_state 中,文件格式为 json。

Topology Manager

Topology Manager 特性理解起来比较抽象。举个例子说明:上述的 CPU Manager 和 Memory Manager 的特性,在 kubelet 的实现中是完全独立的,可能会导致 cpu 和内存被分配到了不同的 numa 节点上,CPU Manager 通过 cgroup 的 cpuset.cpus 来控制容器要绑定的 cpu,而 Memory Manager 则通过 cgroup 的 cpuset.mems 来控制容器要使用的 NUMA node 内存。如果两者的信息不匹配,则会导致跨 NUMA Node 的内存访问,从而会对于性能要求高的应用产生影响。
Topology Manager 是 kubelet 中的一部分功能,通过 Hint Providers 来发送和接收各个模块的 NUMA 拓扑信息,比如接收 CPU Manager 和 Memory Manager 的 NUMA Node以及 NUMA Node 分配的优先级。

kubelet 的参数配置

kubelet 通过参数--topology-manager-scope来指定作用域:

  1. container:默认值,按照容器级别分配到共同的 NUMA node 集合上。
  2. pod:将 pod 内的所有 container 分配到共同的 NUMA node 集合上。

kubelet 通过参数 --topology-manager-policy 来设置 NUMA 的分配策略:

  1. none:默认值,不执行任何的拓扑对齐。
  • best-effort:优先选择首选亲和性的 NUMA node,如果亲和性不满足,pod 仍然可以调度成功。
  • restricted:选择首选亲和性的 NUMA node,如果亲和性不满足,pod 调度失败。
  • single-numa-node:通过 Hint Provider 返回的结果,判断单 NUMA 节点的亲和性是否,如果不满足,则 pod 调度失败。

kubelet 中的实现

在 kubelet 中定义了接口:

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
// TopologyHint is a struct containing the NUMANodeAffinity for a Container
type TopologyHint struct {
// 记录了 NUMA Node 满足资源请求的位掩码
NUMANodeAffinity bitmask.BitMask
// Preferred is set to true when the NUMANodeAffinity encodes a preferred
// allocation for the Container. It is set to false otherwise.
// 亲和性的结果是否为首选的
Preferred bool
}

// HintProvider is an interface for components that want to collaborate to
// achieve globally optimal concrete resource alignment with respect to
// NUMA locality.
type HintProvider interface {
// GetTopologyHints returns a map of resource names to a list of possible
// concrete resource allocations in terms of NUMA locality hints. Each hint
// is optionally marked "preferred" and indicates the set of NUMA nodes
// involved in the hypothetical allocation. The topology manager calls
// this function for each hint provider, and merges the hints to produce
// a consensus "best" hint. The hint providers may subsequently query the
// topology manager to influence actual resource assignment.
GetTopologyHints(pod *v1.Pod, container *v1.Container) map[string][]TopologyHint
// GetPodTopologyHints returns a map of resource names to a list of possible
// concrete resource allocations per Pod in terms of NUMA locality hints.
GetPodTopologyHints(pod *v1.Pod) map[string][]TopologyHint
// Allocate triggers resource allocation to occur on the HintProvider after
// all hints have been gathered and the aggregated Hint is available via a
// call to Store.GetAffinity().
Allocate(pod *v1.Pod, container *v1.Container) error
}

CPU Manager、Memory Manager 和 Device Manager 均实现了该接口。在 Topology Manager 中根据各个 Manager 返回的 TopologyHint 数据,从而决定最终的 NUMA Node 分配,并调用各个 Manager 的 Allocate 来做最终的 NUMA Node 分配。

资料

kubeconfig 文件结构

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
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMvakNDQWVhZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJME1ETXlNVEE0TXpZMU1Wb1hEVE0wTURNeE9UQTRNelkxTVZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTFBaCjZUcUFNMVhiRkxIbnVvd1ZNT1FHeWQ2SzVBcGdwcmhYSlBvclVKdStoazBKd3BQQlNRZGNSdnJjYy9wNTNDbnQKTTBXWVN4dThTd1Z6a0dHajh0cHNSNjRkMWMxdFk1djYzYnlkVXN5M3JwME1OWUt1ckJPNEY2aVFLK01oL3R6UAp4eUdZei9BUnhheDdXNysvWEV6Y2FsdFp5T1JZZk9ISUR5ZjN5R3V2T1htYmw5ZEJmRHFBMFlSMGxOTFFlUEcrCm5lZkVGb1dUQncxUytiZEl0MDBRZnl3MVlvRXpkOFd6UDRBTzFlV3AxK0tJdFhLaUxyaWFBbjkzNTJhbnVIT2YKQ3h0U0NCbEwwRThCL3dGKzhTd0RQbDlUYkNhZU1nTUJpY2hsQzlYSjNrV2k2cElOVnVEeEpRUy82cnlwQWhLbgpHK21RWlJIdmhsUk1wTjhEbDBzQ0F3RUFBYU5aTUZjd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZLay8zWDh0Q1dhZEpKN011SjNRNE5DL2xHd3pNQlVHQTFVZEVRUU8KTUF5Q0NtdDFZbVZ5Ym1WMFpYTXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBSDR1K05zbE9ORWJVZXB1M0JaRQozL1hnQitFNDZVZ3N2d2R6UzhEWXF3YzBWQ28vUFc0RUNreW9IUkJINHJkdTVpaTBhVUhjZUxpNjhZQjlvQ3hpCmlkbHp3bUNGV1g1dEtGMUJTRFJ1YSt5MjVrOGZuSGtodm5IVG9CQ2c5ZlMzdFBOekNUdEtMSUhyVFpQVDhWU3IKdVYyZkdOcXMyT2djWjc4TmY4M2pnVVBXbFVPZkVsTDBrYjNYVHo5M29DdnF0RS9tRi9VOWhmOUdiRU5Lai9BSAovUHA4QWNmellkVGxGNTBva09temVFdnJnalZmaXFKMitGd2I5Lys1SUljSWFMR3IwZjVUMUVINzdxVEdEWElWCncwWGlKYmVTL1JTYTYrZFM0b1dRMS82Ryt1SUdRQWx0eTRSTXdqbG4rMTMvdStPelowTHErNzBlQTdUSm5mU0MKRVhBPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
server: https://127.0.0.1:55282
name: kind-kind
- cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURsRENDQW55Z0F3SUJBZ0lVQVFVeTNtWi80eWZmbEhjTi9WblNIcUYwL0NRd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1lqRUxNQWtHQTFVRUJoTUNWVk14Q3pBSkJnTlZCQWdUQWtOQk1SSXdFQVlEVlFRSEV3bFRkVzV1ZVhaaApiR1V4RURBT0JnTlZCQW9UQjB0MVltVmFiMjh4Q3pBSkJnTlZCQXNUQWtOQk1STXdFUVlEVlFRREV3cExkV0psCmNtNWxkR1Z6TUI0WERUSTBNRE15TVRBNE16a3dNRm9YRFRJNU1ETXlNREE0TXprd01Gb3dZakVMTUFrR0ExVUUKQmhNQ1ZWTXhDekFKQmdOVkJBZ1RBa05CTVJJd0VBWURWUVFIRXdsVGRXNXVlWFpoYkdVeEVEQU9CZ05WQkFvVApCMHQxWW1WYWIyOHhDekFKQmdOVkJBc1RBa05CTVJNd0VRWURWUVFERXdwTGRXSmxjbTVsZEdWek1JSUJJakFOCkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXlNNGo2d1NSdTUrcFpwckk1WkhxdmYycXpJdTUKSlZkYzRFUEp3cGY5eU9LN1FPZFM1SHJRUzVNK3ZEMFMzOUNJVFVYY2YxZUNJZy83SEt4TmVFbVk0TVdCMVlBWQpZOVRjMkZlR3JMMVBFR0lwNER6TnRRMkhvcWFxU2pJd0d0bnh3RjV5OGRRUGJkQ3JOdllDRVl5QlR2S0VtUXVoClFmdDhJR1NmaWJ0M3gwa2ZaaUFqZTJ5SDJabFNyMnBRSzRWWFdWUU5UV0hOQnlMS29Lb05Yazl2UTQ4dHhYbVUKcFRDWUxjcGdZSC9tU0lpY1FOcDQwRjRaOUUraGFjdTVkYVFVakIzZzQxWEVvYXBzL2xSa3d0bVFlV1gwVjR2VQp3cUlCS0doZmJ3dmluUjAvTmJGOVVLVldaVlpVL2R0NHR6TXQxVHg4L2tmcExmL0xnNXdoOThUSkJ3SURBUUFCCm8wSXdRREFPQmdOVkhROEJBZjhFQkFNQ0FRWXdEd1lEVlIwVEFRSC9CQVV3QXdFQi96QWRCZ05WSFE0RUZnUVUKWXp4aGFlN2V3TXNIeEJ5YnQ4U0Ntb01zVGg0d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFFWk5uUjI5SDVjRwpJSFBKdDlNMk5YZGt1NzFuU2VZb2s3SFBVdnJ6V0JOdFJjMUJPZkJRc25zb0RRcDQ1SmQxUnZsOEdLRHJjOCtICm5yaXZGQXpPZFRPQlhib3RMcFdGc0U1WU5VcGlUbWU2aW9pUVRVTnQ2WCtLd29CV00xNUwyWlJBYXdQQ0FBZ0sKeWRRU2lZaHVZMS93ekltWW1LenczRjVYb1BJcHVjTjhNam1MM3ZPNlFPaW51OFQrcW9wbWlmRFMraUVzRjcwSApwaW9mVXFJR3Zmbm5uVFFTWnFrRFAybXZ2VHFqT1lCbjU1dHRsL2JYQzh2ZHlwakNGVWRGOXNjUFM5R28ycTRuClkySUUvdHIvUFhaODUvUVVsdVQ2UEd5VUMycDNhZmNhdXRBbC9hQUZvM1ltK3BNbW03WUhad2JjcUp6U3BmRXIKbnJaekRNZFNGS2c9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
server: https://127.0.0.1:6443
name: zoo
contexts:
- context:
cluster: kind-kind
user: kind-kind
name: kind-kind
- context:
cluster: zoo
user: zoo-admin
name: zoo
current-context: kind-kind
preferences: {}
users:
- name: kind-kind
user:
client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURJVENDQWdtZ0F3SUJBZ0lJSkNhSHFyUUl0b0l3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TkRBek1qRXdPRE0yTlRGYUZ3MHlOVEF6TWpFd09ETTJOVEphTURReApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sa3dGd1lEVlFRREV4QnJkV0psY201bGRHVnpMV0ZrCmJXbHVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXFNS1VOcStyTGNPTkxDWmMKTjA4SjBpYzhBdnhjdmlZNG40SVN1UFFleVViYUhLTVpRNnJpNjA4SlZoNUZxc3B3N3BoWTkrYVBMTHJQaG9uWApCQWhQdmVSSVVxeDdaTkJhODNuVUIrTXIrOXIrazAwTk1yNDBTdkg5aGpnTXVZUjFvN051OWRzN1k3U0pOcXVsCnlwOVN6YzUwUTNBbzh1cHBTRFlFRW5Hd3I0VVBidlp0ZTVlQXo2T2hDYy9hazZuZGlFcU9hMkdJRzhlUmEyWTkKM2hBQjl6V3YvVldGTkxFNXh2Mm5oS3JDQVl6TzBrVmJwTkNlSmkxRGFvNTIzQURWajhGQjRFbDFVNCtvTmM2Vwo5VHdRbDFIUklxZXJ6MUFMTFl1THhadGFFY2IwYW04N2dTcEtyaXcrYWVBL3h6L29tQ1BLY1IrQTBSTkpBcVdWCnduTzhDUUlEQVFBQm8xWXdWREFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUgKQXdJd0RBWURWUjBUQVFIL0JBSXdBREFmQmdOVkhTTUVHREFXZ0JTcFA5MS9MUWxtblNTZXpMaWQwT0RRdjVScwpNekFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBbjVCalc3VEZvYlBxWk5obS82cEJPVUdGUmVncHFKYVcrUFN1CktUVU5qNXdDU1ZjTWg3V2RmQWtBSGxrYzQ5YWZlaHpWZmVwUXJEUEpOam96eCsxOGc2ZmtNalJRdEhRaW9yYSsKck82UklBYnVJR0pqTXBKVGNGL25OMkY4amFzdFlrdkZ1cjNtdlVldzhsWmtEZEdYMFFaU0J2Y0xqVGdvZURmSQpLTmhjMGVaQ2QrMStGeVJYajZwaUs4Y3pBWlkvTTVsVHJSZTVQUmJSaHpMeVo0Wm1nMHZjOUZjZFcxbThVZ2hDCkVOWkZCVDA0WTl2bHRGTHJaSG9IRlFERlNKRUxTSnl5VG95dTVnYVh6OElZUm41Y0k0b1RPZXlKN2JvQ3hvdzEKa3VXbWdxbVZHdjZwUWJqRm0zaTBTZXFRMkQ5bU1SaDhsYW0vMlhiaVlyWlBjQUlaRnc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBcU1LVU5xK3JMY09OTENaY04wOEowaWM4QXZ4Y3ZpWTRuNElTdVBRZXlVYmFIS01aClE2cmk2MDhKVmg1RnFzcHc3cGhZOSthUExMclBob25YQkFoUHZlUklVcXg3Wk5CYTgzblVCK01yKzlyK2swME4KTXI0MFN2SDloamdNdVlSMW83TnU5ZHM3WTdTSk5xdWx5cDlTemM1MFEzQW84dXBwU0RZRUVuR3dyNFVQYnZadAplNWVBejZPaENjL2FrNm5kaUVxT2EyR0lHOGVSYTJZOTNoQUI5eld2L1ZXRk5MRTV4djJuaEtyQ0FZek8wa1ZiCnBOQ2VKaTFEYW81MjNBRFZqOEZCNEVsMVU0K29OYzZXOVR3UWwxSFJJcWVyejFBTExZdUx4WnRhRWNiMGFtODcKZ1NwS3JpdythZUEveHovb21DUEtjUitBMFJOSkFxV1Z3bk84Q1FJREFRQUJBb0lCQUJzQWR5S0EzUXpIZXpFVApPaklIVFhUNG5odUVNWHFqTnZBZXFjdzZFeXIxVVRTL3krME56SjBGMm1LVEdXYUlXYVZ6YnRqTFpTRXRDc05tCkRxY3doVUhHNHVPSGdYN1I3NXVCWkxHV1laVThwdnIrbXh3Qlh2Q1c0NCswTENVSzBwL010L1pTaTZBYVpOSUEKaU5od3daajRiWlhVdmxpUHRTUytyOHdic0wrRWNqTGFtZVhlQWNpTDByNTVzZzA5akxhSENkNnhQZ29FYlFPawpKR25kZ1JsSTFSZis5RGdON0toRkdQMkEvOGJEMytONERZT3pRcm8zWE1ZcG9kNyt3ODlkclFnOG9ob2pRQkd5CjZrR0tNeXdsaktNRmxuV3c4bmYwbDU4VzVibWo2enp3RzZPcDQra21wVStyc3pqR0JEdDQ1QnlQNUhTbEFEelMKYzNudFArRUNnWUVBeUhGc1l0aHZ4Vk1sVWdzZTViRzVWYXdwUGNjU3JuY0p3SXU4U1AvaExYUi9POUsvZVhwTAo2OUhtemNMSW1Dd1J1cWwzdUEvQlhYR1hpNlh5SWMvK3k0bzZyUEhoZk4wS2x5cFVOTXd4Ri9FY0t2VklmeE1ECjM2Uks5eVdvem5zVUFVVkw5L0dPK2ViK0dLOGtQRHYyVFdwTEVIVDFjVFc4aHNNV3JvTENLclVDZ1lFQTE0a1IKT2FWejBEbHFLUXlNSU0zVkk4Kzc4TUNOTzdUTFVOait4SWRyV2lGLzlCOG8wSVp3dG5GREo1OHE3dWZNcHhpTAptZEtwRENHcGhIWmhRQzYxWEhBNXEyVFIzamxhK1ArdnpTRVNxeWIvWWhrelk3dDdZY0xMSFBValRiWXpmV2I0Cjl6TjdTNkF6NWM2aGs1Nzd3RmxTODE2KzlWK2pBVW5MaWdkZzNJVUNnWUVBb1BlbFNRUHpUbzNsREt2dGxoeFIKYitHZ0JRS1htQS8wZnZJNHRJNzRzRjQ3eHprSmwyNkZCYzQ5QWNTSS90dDFLV2Zxd3ArMGMyeERmVnc0eExxYQpMYTdHVEJpN01tRDRua2paOHNTQU1HL3FaUDB4eVFybU0zVm0xbThoengrOEF3RTVidFpJTVp3MU5uR0FNZmNkClp6SVRNaFlhL1YxZ0Z3RVlkL0IrS1hrQ2dZRUFxdzA1b1dGQVAxRkJnaUJXR1RhaFg1RmVXeHZGT2t3cVN4aGIKWUVjRW1Id2JtdmNib2huLzI1cVpyQmt5cm5WQndwN0ZNNmV1eDFUenZvOWdjTnBnem1LMk1lS0tkKzFXMkdPNgo5blczNWlMRjdPbUpFaTVaSmVXODRsZGQxQyswUDJKNFZWOERDNnF4WlVFT2xDUkpNWWJ5UVBqQlhlU3ZiYmRPCkZGWDB0aTBDZ1lBZDNXTjl2SlN5UUxDUDlHdHA0dVRJQXpYRlFGbkJiY2ZKVzdaL2hnUXlZUWlWa0p6K3pQSCsKclU5RWE3ZTVyTlg0MmtuK2E1cXpudXZEeE5aMDhjRjYwdFBBajR2T1UrTkZyb2FyU29xL3lreTJMTnR2LzJ0OApocXJQblk0dE04dFpON2w5NktHSklJdzlVUXcyR2NpS3JXSXkvT0NQR2lERGthakhGS2lndlE9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
- name: zoo-admin
user:
client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQyRENDQXNDZ0F3SUJBZ0lVYlBFdGx4LzRTYm9TckRvZTd1djllS1YrckZNd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1lqRUxNQWtHQTFVRUJoTUNWVk14Q3pBSkJnTlZCQWdUQWtOQk1SSXdFQVlEVlFRSEV3bFRkVzV1ZVhaaApiR1V4RURBT0JnTlZCQW9UQjB0MVltVmFiMjh4Q3pBSkJnTlZCQXNUQWtOQk1STXdFUVlEVlFRREV3cExkV0psCmNtNWxkR1Z6TUI0WERUSTBNRE15TVRBNE16a3dNRm9YRFRJMU1ETXlNVEE0TXprd01Gb3dhVEVMTUFrR0ExVUUKQmhNQ1ZWTXhDekFKQmdOVkJBZ1RBa05CTVJJd0VBWURWUVFIRXdsVGRXNXVlWFpoYkdVeEZ6QVZCZ05WQkFvVApEbk41YzNSbGJUcHRZWE4wWlhKek1SQXdEZ1lEVlFRTEV3ZExkV0psV205dk1RNHdEQVlEVlFRREV3VmhaRzFwCmJqQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQU1PYnpoeDBOTW5ubTdHbmFjb2sKWHJ1RmxrRVBXc01la2xLNnBtbWFDRUVVUTNocHdsUWw2Z09UekpwaUVYVGZMZU1Uc29ZZ1NtNGFySWdjdVlTcwo3cDdzdWhhUi9RN1g3SUZjMXJEMzFtODB5amZEVUhYZi9jSWhDVmp0NDhLY1JLL2QrK2NzZkU5eHpEdlVBdDVxCmxmNFNvMy9YSnJyYzdtdURiUXdYdDgvaGQ4RnVZYUFWN2YraXVMMW5DRTB3b01IaFNPSkpEWC9TQ1pMakxTS2wKYU9aM2lOL0dDRXo4cldPUmVBbGUwQzRwazRwc0xmN1h6UlJpcnFTT21lQ3JTZzJoNlU1OS8xTHN5dXVqeTVFWQpxT3RsZ0R0Z3l5RDhBRWZhanM5R0NNRnFFOStHZWR6NkpuRmFlUlpOZThlVWNhVU1GWFVSWjYxb0pqT2UraUZ4CjU3TUNBd0VBQWFOL01IMHdEZ1lEVlIwUEFRSC9CQVFEQWdXZ01CMEdBMVVkSlFRV01CUUdDQ3NHQVFVRkJ3TUIKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjBHQTFVZERnUVdCQlRPL0JxUE9HcVU1V0JYSW9wQgoxajdxYVBvTkh6QWZCZ05WSFNNRUdEQVdnQlJqUEdGcDd0N0F5d2ZFSEp1M3hJS2FneXhPSGpBTkJna3Foa2lHCjl3MEJBUXNGQUFPQ0FRRUF0SVRaN0FUVVhManB6UWtQVGM0RVFKYi9IZlJTcklOV3pSMlpRcVI1Q2J1dXEzR08KUktDZFphenVrakJjMCtXVkhGcVo4SEtNNUR2YThKbzZCUXgycXoxZ0ptVE1oYUdRMFlyNWFFcmxFZWJCWUcwVgpLMGh4LzJuMmNmMkt3N3VBOWdkeUJKSVFJbnY2RFJPUmt6VVNuQXJEd21TNitUNXdKK0lTUGlPdHVGRnQzazRyClZsZ1N3bjE5WHRSRnJ4OEI4dWRkSlZ4VEloN3FPa09hbURQaFJrTGJKNFdOUk5hYlpVMFVxdmxUQzVnQkhnS2QKSm1FaVQybHNoUENjS2lRZWsrTVNzNU9mNG9ZQml0TXM0eW1iendXb1hVaGcvVnRTbTdtT2MzUmo2ZGhrQXl2NwozQTJoZW1xUENKQ1JwbndzUDlpd0VrWUE0SlFZMG53aEUrd1h2Zz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBdzV2T0hIUTB5ZWVic2FkcHlpUmV1NFdXUVE5YXd4NlNVcnFtYVpvSVFSUkRlR25DClZDWHFBNVBNbW1JUmROOHQ0eE95aGlCS2JocXNpQnk1aEt6dW51eTZGcEg5RHRmc2dWeldzUGZXYnpUS044TlEKZGQvOXdpRUpXTzNqd3B4RXI5Mzc1eXg4VDNITU85UUMzbXFWL2hLamY5Y211dHp1YTROdERCZTN6K0Yzd1c1aApvQlh0LzZLNHZXY0lUVENnd2VGSTRra05mOUlKa3VNdElxVm81bmVJMzhZSVRQeXRZNUY0Q1Y3UUxpbVRpbXd0Ci90Zk5GR0t1cEk2WjRLdEtEYUhwVG4zL1V1eks2NlBMa1JpbzYyV0FPMkRMSVB3QVI5cU96MFlJd1dvVDM0WjUKM1BvbWNWcDVGazE3eDVSeHBRd1ZkUkZucldnbU01NzZJWEhuc3dJREFRQUJBb0lCQUhMZ0ZYTndhM0FIck0vdwpXWmgxTTQwOUxyaVdvOTdqSFZ1b2NnS2lpeVp0R0JLblNaRFJrMVQyZjdwS3phV3RTKzJIcTloSkxtenJEVmdDClJwRThYZ2JIVDZIaHFwUUZDc2dPRmFkb1pXNTV1aWgxYzlORjhHa0pyY3VrS1pZbzM4M0l1QjlUYU0zZkx1b1QKNEh0dWJSZ0JLalB4enJUKytxWDVVUmxBOUpvSDc2c1hUb1JkYXF5YzR6VXMwcm9LNmVTb3dZeE8ydVN5TmtQcQpkai9reFJ4WTV2djYveHpPOHhVU1dUWndwVit2VUgvbVJiU1hjSjJrKzRvTDB5VmJSLzFpVStPYmpkSWU4UXNOCkhHMjBNSk9zRS9LTVVuVUlSeGxqRGlaM1o4WjVabXIrTUVTV3daOWRkUWlJbm80dSt6UkVaRTcvbnlwQ2lTY0UKOHlBNG9FRUNnWUVBOVpHbHRLdG8yaHRzZXBuWnNwdFVFdEorbXo2a001bEI2SHdpVVd3Q05ZSXpUQzRra2J2MwpXSzVMYXFpaXEwUk5zOHZSbmNXSHlwMHFidTlyOHNUellRUVVvc0luVStReGlLMUd0Uk81cVV4TE9SZzZlSjViClNCM0JoNnVQdVMyUU9qSlNKbU9uWldkUHFrRXJqSW9LWWZhT00wUGIxaUVlUzBWVUVyVWZaVGNDZ1lFQXkrcmgKeDlzM0FnMVk0bkVpVThsbGpDNlZQYk8yQ3pRM3ViWXJlcjhobVFTYkRsQjBHMEhOVE01YWNaSFdNVUdnWWVlagpUbFJCcVQvbFZtOXV3NFBxc1Ezejl4QnB0YmZDVUpqeEVucnJWQkdpeElNeHlGMU85WTh2WVRkZENLa3VVVVE5CnEybnpuNzNMNGdDRDlMcW4yYlFpaEFucHdTamcySytFQ2V4RlQyVUNnWUVBdWJIM2tsV0VKbHBTZjZ0VG1lSW4KZzB3MWZRT3plMmxMRTVpN0FzTWdNSUpTZENyNGNGT3BTU0FUMjRYRjdLanI4U2dSVExNUWFrREswN1NzOXBuRQpTUHFpK0NqRlFJVHdpQ0F2dGNKQ3hTanlRU3gzR3JyMDMrWFFjTjFsQTJ6WEFZc0g0QXUvaThqQnowY1V2V090ClVrTDFhUUxKZkhUeXlZeVZkTWdPQTZVQ2dZQndDcUY5dDFRVkc1SlA4UXVFYis4TXcvZWFUR2prNVE4TlNpdS8KcU03a0RhVElpNm9QNCtyU25ic1NGYWhUcmhSYVZ2VGlyK2JZQU5TWTFtZE1vK25LMkxqSWNrc3kza0cxR1NPMApITGU2bkdvTGdXNVVBZmpGY2FQOXpYYWZzSjFUWjZSZXo3dGRkT0pXVGlReXpuQTFiUVZkK1RobnVuYzRkOCtiCnlDY1pCUUtCZ0ZWcFZacDdjWUxHYlE1dERkL0orZ1BhcFo0TWFYSzluWDFTaktlalhjcVFFSUc3NEYrdkxzdEwKZCs4R3N0VkRzdy8vQ0VNcW9pYXZ1MkVlQ0VQZzF3ZHpDU3pWa2IrZ1FRZXA0cE1LZEhaai9YaysvNVVXTUJzOApkNnNwMWxrTTRqSGhjYkFLU056VTUrTG1NRnk0MzBPZlI2dmxVTFZjNVlaR2hSU0ZCdzdDCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==

cluster 字段

certificate-authority-data 字段:服务端的 ca 证书,用来验证 kube-apiserver 证书的正确性。
当使用 kubectl 发送请求到 kube-apiserver 时,kube-apiserver 会返回通过参数 --tls-cert-file 配置的证书文件,kubectl 通过 kubeconfig 中的 certificate-authority-data 字段来校验 kube-apiserver 返回证书的有效性。
当 kubectl 指定了参数 --insecure-skip-tls-verify=true,即可跳过对 kube-apiserver 证书的校验。

users

  • name:用户名称
  • client-certificate-data:kubectl 连接 kube-apiserver 时使用的客户端证书,会发送给 kube-apiserver。内容经过 base64 编码。
  • client-key-data:客户端私钥信息。内容经过 base64 编码。

client-certificate-data 对应的为用户的公钥信息,使用命令 echo 'xx' | base64 -d > /tmp/client.crt; openssl x509 -in /tmp/client.crt -noout -text可对证书的内容进行解密。解密完成后的证书内容如下:

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
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 2604918601715070594 (0x242687aab408b682)
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN = kubernetes
Validity
Not Before: Mar 21 08:36:51 2024 GMT
Not After : Mar 21 08:36:52 2025 GMT
Subject: O = system:masters, CN = kubernetes-admin
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:a8:c2:94:36:af:ab:2d:c3:8d:2c:26:5c:37:4f:
09:d2:27:3c:02:fc:5c:be:26:38:9f:82:12:b8:f4:
1e:c9:46:da:1c:a3:19:43:aa:e2:eb:4f:09:56:1e:
45:aa:ca:70:ee:98:58:f7:e6:8f:2c:ba:cf:86:89:
d7:04:08:4f:bd:e4:48:52:ac:7b:64:d0:5a:f3:79:
d4:07:e3:2b:fb:da:fe:93:4d:0d:32:be:34:4a:f1:
fd:86:38:0c:b9:84:75:a3:b3:6e:f5:db:3b:63:b4:
89:36:ab:a5:ca:9f:52:cd:ce:74:43:70:28:f2:ea:
69:48:36:04:12:71:b0:af:85:0f:6e:f6:6d:7b:97:
80:cf:a3:a1:09:cf:da:93:a9:dd:88:4a:8e:6b:61:
88:1b:c7:91:6b:66:3d:de:10:01:f7:35:af:fd:55:
85:34:b1:39:c6:fd:a7:84:aa:c2:01:8c:ce:d2:45:
5b:a4:d0:9e:26:2d:43:6a:8e:76:dc:00:d5:8f:c1:
41:e0:49:75:53:8f:a8:35:ce:96:f5:3c:10:97:51:
d1:22:a7:ab:cf:50:0b:2d:8b:8b:c5:9b:5a:11:c6:
f4:6a:6f:3b:81:2a:4a:ae:2c:3e:69:e0:3f:c7:3f:
e8:98:23:ca:71:1f:80:d1:13:49:02:a5:95:c2:73:
bc:09
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Client Authentication
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Authority Key Identifier:
A9:3F:DD:7F:2D:09:66:9D:24:9E:CC:B8:9D:D0:E0:D0:BF:94:6C:33
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
9f:90:63:5b:b4:c5:a1:b3:ea:64:d8:66:ff:aa:41:39:41:85:
45:e8:29:a8:96:96:f8:f4:ae:29:35:0d:8f:9c:02:49:57:0c:
87:b5:9d:7c:09:00:1e:59:1c:e3:d6:9f:7a:1c:d5:7d:ea:50:
ac:33:c9:36:3a:33:c7:ed:7c:83:a7:e4:32:34:50:b4:74:22:
a2:b6:be:ac:ee:91:20:06:ee:20:62:63:32:92:53:70:5f:e7:
37:61:7c:8d:ab:2d:62:4b:c5:ba:bd:e6:bd:47:b0:f2:56:64:
0d:d1:97:d1:06:52:06:f7:0b:8d:38:28:78:37:c8:28:d8:5c:
d1:e6:42:77:ed:7e:17:24:57:8f:aa:62:2b:c7:33:01:96:3f:
33:99:53:ad:17:b9:3d:16:d1:87:32:f2:67:86:66:83:4b:dc:
f4:57:1d:5b:59:bc:52:08:42:10:d6:45:05:3d:38:63:db:e5:
b4:52:eb:64:7a:07:15:00:c5:48:91:0b:48:9c:b2:4e:8c:ae:
e6:06:97:cf:c2:18:46:7e:5c:23:8a:13:39:ec:89:ed:ba:02:
c6:8c:35:92:e5:a6:82:a9:95:1a:fe:a9:41:b8:c5:9b:78:b4:
49:ea:90:d8:3f:66:31:18:7c:95:a9:bf:d9:76:e2:62:b6:4f:
70:02:19:17

其中的 Subject 中的 O 对应的 k8s 中的 Group,CN 对应的 k8s 中的 User。kube-apiserver 会通过证书的 O 和 CN 获取到 User 和 Group 信息。在 k8s 系统中,实际上并没有存储 Group 和 User 信息,而是完全依赖该证书中的信息。

kubeconfig 文件生成

kubeconfig 文件本质上是个证书,包含了 ca、证书公钥和证书私钥,在有了证书后可以通过 kubectl 命令生成新的 kubeconfig 文件

1
2
3
4
kubectl --kubeconfig ~/.kube/111111.kubeconfig config set-cluster hello --certificate-authority=/tmp/ca.pem --embed-certs=true --server=https://127.0.0.1:6443
kubectl --kubeconfig ~/.kube/111111.kubeconfig config set-credentials hello-admin --client-certificate=/tmp/tls.crt --client-key=/tmp/tls.key --embed-certs=true
kubectl --kubeconfig ~/.kube/111111.kubeconfig config set-context hello --cluster=hello --user=hello-admin
kubectl --kubeconfig ~/.kube/111111.kubeconfig config use-context hello

使用 curl 命令直接访问 kube-apiserver

由于 kube-apiserver 开启了双向认证,使用 curl 命令访问 kube-apiserver 时,curl 需要指定证书信息,证书信息可以使用 kubeconfig 中的证书信息。

1
2
3
4
5
6
7
8
WORK_DIR=/tmp
KUBECONFIG=~/.kube/config
CONTEXT=kind-kind
server=`yq eval '.clusters.[]|select(.name=="'$CONTEXT'")|.cluster.server' $KUBECONFIG`
yq eval '.users.[]|select(.name=="'$CONTEXT'")|.user.client-certificate-data' $KUBECONFIG | base64 --decode > ${WORK_DIR}/client.crt
yq eval '.users.[]|select(.name=="'$CONTEXT'")|.user.client-key-data' ~/.kube/config | base64 --decode > ${WORK_DIR}/client.key
yq eval '.clusters.[]|select(.name=="'$CONTEXT'")|.cluster.certificate-authority-data' $KUBECONFIG | base64 --decode > ${WORK_DIR}/ca.crt
curl --cert ${WORK_DIR}/client.crt --key ${WORK_DIR}/client.key --cacert ${WORK_DIR}/ca.crt "$server/apis/apiextensions.k8s.io/v1/customresourcedefinitions?limit=500&resourceVersion=0"

资料

设置评论系统

我的博客系统 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

0%