2024-11-17 终于,这个博客用上了 kubernetes
注意:实际生产中,应该按照需求选择环境,而不是无脑上k8s。写这篇 post 的意义在于记录一下我的 kubernetes 集群,以及我是如何走向 kubernetes 的。
Selfhosted
中文有个翻译叫自搭建,说白了就是类似于在你的服务器上安装一些服务,然后用客户端访问、使用这些服务。对于大部分人来说这个服务器就是NAS ,国内也有很多类似的玩家。
我偶然得到了一个7代i5的老台式电脑,于是我就把它当成了我的服务器,相比于成品 NAS 它的硬件扩展性更好,内存更大。而且我的主要目标是搭建服务,而非存储数据。我在 2023 年写过一个 selfhost stack 记录。我也不记得我第一个搭建的服务是什么了,现如今该服务器使用 PVE 运行着几个虚拟机,几个 LXC 容器,虚拟机中的 docker 运行着 45 个容器。
如今该服务器的性能和内存已经跟不上了。虽然我已经做了存储、计算分离,但是仅限于虚拟机层面,从表面上来看还是会出现 one boom, all boom 的惨剧。
Problems
我使用 docker-compose 管理这些容器,但是这也会有一些问题。
存储
docker 中使用 volume 来管理数据,这就带来了使用 bind 还是 volume的问题,如果将目录映射到容器内,一旦目录发生更改,你需要修改所有的 docker-compose 文件,这在我一次换硬盘的过程中带来了一些麻烦。如果不使用 bind,你又要调整卷组的存储位置,以保障数据的安全性。而且volume的备份和恢复也比较麻烦,我写了一个脚本来完成这个功能。
同时,使用volume来实现容器化数据库也存在一定的性能问题。
Nginx
对于这些服务,你需要对所有需要访问的容器写好反向代理的配置,并处理好 TLS,而有时候反向代理配置往往是最麻烦的。而且修改起来也很麻烦,当你有近40个 Nginx 文件,改起来是一个灾难。
其次,当一些容器未能正常启动时,如果此时 reload nginx 的配置,无法正常使用新的配置,强制重启 nginx 后,则可能会造成所有服务的反向代理全部停止。
单点故障
我之前学习过 k8s 但是没有真正使用它,主要还是没有切换到 k8s 的动力。但是直到这个博客租用的服务器所在的大楼着火了,博客在数个小时内处于离线状态。
我迫切地想学习、尝试使用 k8s,但又苦于没有合适的服务器。最终我决定趁着双十一和黑五,服务器打折时买一些合适的服务器来组建 k8s。
我的 k3s 集群
由于我购买的服务器在世界各地,而且配置都不怎么好,于是我决定使用 k3s 来组建我的集群。
集群构成
我的集群由五个 VPS 构成,他们都是 k3s 的 server 节点,即具有控制平面、etcd 的 master 节点。由于我实在舍不得钱买更多的服务器添加 agent 节点,所以只能先这么用着。我对 Nginx 比较熟悉,我决定采用 Nginx-ingress 作为 ingress 类。我还使用了 kube-prometheus 来监控集群。
网络
本来我是想使用 VPN 的方式构建一个大内网,让服务器之间能通过内网相互通信,Netmaker 这类工具又会引来单点故障的问题。仔细查看文档后,我发现 k3s 的网络本身就支持 wireguard,而且 k3s 默认的网络配置对 wireguard 的支持很不错,可以直接通过内核进行交互,因此我决定直接使用 wireguard 作为后端,在公网中传输服务器间的数据。
系统
有了服务器,接着重要的就是重装系统了,这些租赁厂商提供的系统往往被定制化地修改过,在实际使用如果出现故障,很难找到原因。
我决定使用 Debian 作为构建 k8s 的系统,虽说安装 debian 只需要一个 preseed 文件就行。但实际上从光盘启动这一步骤就非常地麻烦,需要挨个登陆到 VPS 的管理面板。于是我使用了 ansible 来帮我处理重装的步骤。这里分享下使用 ansible 批量重装系统的 playbook。
---
- name: Prepare Debian Installer
hosts: reinstall
become: true
vars:
initrd_url: "https://deb.debian.org/debian/dists/stable/main/installer-amd64/current/images/netboot/debian-installer/amd64/initrd.gz"
linux_url: "https://deb.debian.org/debian/dists/stable/main/installer-amd64/current/images/netboot/debian-installer/amd64/linux"
preseed_full_url: "https://example.com/fullpreseed.cfg"
preseed_para_url: "https://example.com/parapreseed.cfg"
tasks:
- name: Install required packages
ansible.builtin.package:
name:
- cpio
- gzip
state: present
- name: Download initrd.gz
ansible.builtin.get_url:
url: "{{ initrd_url }}"
dest: /boot/initrd.gz
mode: "0644"
- name: Download linux
ansible.builtin.get_url:
url: "{{ linux_url }}"
dest: /boot/linux
mode: "0644"
- name: Get root disk name
ansible.builtin.shell: |
set -o pipefail
findmnt / -ln | tr -s ' ' ' ' | cut -d ' ' -f 2 | cut -d '/' -f 3 | cut -c 1-3
args:
executable: /bin/bash
register: root_diskname
changed_when: false
- name: Debug root disk name
ansible.builtin.debug:
msg: "Root disk name is: {{ root_diskname.stdout }}"
- name: Set preseed URL for sda
ansible.builtin.set_fact:
preseed_url: "{{ preseed_full_url }}"
when: root_diskname.stdout == 'sda'
- name: Set preseed URL for vda
ansible.builtin.set_fact:
preseed_url: "{{ preseed_para_url }}"
when: root_diskname.stdout == 'vda'
- name: Fail if root disk name is not recognized
ansible.builtin.fail:
msg: "Unsupported root disk name: {{ root_diskname.stdout }}"
when: preseed_url is not defined
- name: Download preseed.cfg
ansible.builtin.get_url:
url: "{{ preseed_url }}"
dest: /boot/preseed.cfg
mode: "0644"
- name: Unzip initrd.gz
ansible.builtin.command:
cmd: gunzip -f initrd.gz
changed_when: true
args:
chdir: /boot
- name: Embed preseed.cfg into initrd
ansible.builtin.shell: |
set -o pipefail
echo preseed.cfg | cpio -H newc -o -A -F /boot/initrd
args:
chdir: /boot
executable: /bin/bash
changed_when: true
- name: Recompress initrd
ansible.builtin.command:
cmd: gzip -f /boot/initrd
args:
chdir: /boot
changed_when: true
- name: Get root partition UUID
# lsblk -lf | grep '/$' | tr -s ' ' ' ' | cut -d ' ' -f 4
ansible.builtin.shell:
cmd: |
set -o pipefail
findmnt / -o UUID -ln
args:
executable: /bin/bash
register: root_uuid
changed_when: false
- name: Update /etc/grub.d/40_custom
ansible.builtin.blockinfile:
path: /etc/grub.d/40_custom
insertafter: EOF
block: |
menuentry 'debian install' {
search --no-floppy --fs-uuid --set=root {{ root_uuid.stdout }}
linux /boot/linux
initrd /boot/initrd.gz
}
state: present
- name: Update GRUB
ansible.builtin.command:
cmd: update-grub
changed_when: true
notify: Set Next Boot
handlers:
- name: Set Next Boot
ansible.builtin.command:
cmd: grub-reboot "debian install"
changed_when: true
notify: Reboot the machine
- name: Reboot the machine
ansible.builtin.reboot:
msg: "Rebooting after preparing the Debian installer"
这个 playbook 并不完善,它不能进行自动网络配置,也就是说当服务器所在网络没有 DHCP 时,还是需要人工介入来手动输入网络配置。
关于两个 preseed 配置是因为有的服务器使用了半虚拟化,有的使用了全虚拟化,因此磁盘会分为 /dev/sda
和 /dev/vda
因此我提供来两个 preseed.cfg 文件,你也可以只用一个,然后做替换操作。
存储
为了实现 pod 的调度,我必须想办法提供一个分布式存储,显然,CEPH 在我这种情况下是不可能的。所以我决定使用 longhorn 作为默认存储类。
同时这也是这种跨公网传输数据的集群为什么性能如此拉胯的原因。当存储数据的读写都要经过公网时,网络延迟和服务器间的带宽就成了最大的问题。
经过我的测试,这个存储的连续读写速度,和垃圾 u 盘差不多。这也是为什么这个博客变慢了。不过,实际上这是一个只要加钱就能解决的问题。
数据库
我在集群上部署了一个五节点的 mariadb galera 集群,该数据库集群择使用本地目录作为存储类,数据的同步问题有 galera 自行解决,以最大程度上减少性能损失。
该 mariadb 集群将为集群中的所有应用提供数据库服务。
持续部署
有了一个真实的可以练手的集群,我决定使用 ArgoCD 来部署我的应用,这也是从一个学习的人和真正的使用者的区别。因为你一般不会在自己的 minikube 上部署 CI/CD 工具。
我同时使用 Github 的私有库和我自己的 Gitea 仓库作为远程分支。使用 Github 主要是为了方便 ArgoCD 拉取仓库。我主要使用 App of Apps 的设计模式来构建我的 application。集群上的工作负载基本上都是由 ArgoCD 进行部署的。
一些体会
固然 k8s 的配置和 docker-compose 相比复杂太多,但是一但了解后就知道它强大的功能。如上,该集群的存储性能非常糟糕,直接导致有状态应用的运行速度非常缓慢,这也是我接下来要优化的地方。
第一次接触 k8s 的时候我就被那些复杂的名词劝退了。后来又尝试了几次入坑,直到现在,在我仔细看了一些教程后才终于理解。我推荐 @TechWorldwithNana 的视频,讲解得很详细,浅显易懂。
k8s 真的很吃内存,我的 vps 的内存几乎都被榨干了。我确实需要给我的集群添加 agent 节点。
我一开始学习 k8s 的时候,就有把我的 docker-compose 转化为 k8s 的想法,于是我开始写这些配置。但是直到我实际使用了,才发现还有 kustomize、argocd 这样的工具,才知道存储类的搭配等等,最后意识到,原先写的那些配置都是存在一定问题的。
学习的过程非常有趣,没有 k8s 的条件就给自己创造条件。我感觉到了曾经那种第一次成功搭建自己的服务器的喜悦。