目前网易 UU 对于路由器系统只支持合作伙伴路由器、梅林固件以及开源的 OpenWrt 系统, “合作伙伴” 已经被我放弃了, 梅林固件的路由器也没有… 所以只能选择 OpenWrt 了; 不过需要注意的是官方标注只支持 OpenWrt 19.X
和 OpenWrt 21.X
系统, 所以本文将采用 21.X 系统作为安装演示.
目前我家里只有一台 T350 服务器, 所以上层系统选择的是 ESXi, 接下来本文仅使用 ESXi 作为演示, PVE 理论上原理相同所以不做过多演示.
首先从官网下载 X86 版本的 OpenWrt 镜像, 这里不选择各路大神的第三方版本原因是: 我仅需要一个 UU 加速器, 不需要过多的其他应用集成, 且 OpenWrt 资源占用非常小, 有其他需求我会考虑再开一个虚拟机.
接下来需要创建一个虚拟机, 虚拟机我的规划如下:
虚拟机创建好以后, 需要将 OpenWrt 的 img 格式镜像转换成 vmdk, 这里借助 StarWind V2V Converter 工具(免费)进行转换, StarWind V2V Converter 可以直接将转换好的镜像设置到 ESXi 虚拟机中:
到此虚拟机安装部分已经完成, 打开 ESXi 管理界面应该能看到虚拟机中已经存在转换好的磁盘了.
在安装 UU 加速器之前, 我们需要先对 OpenWrt 做一些基础配置, 否则可能会导致安装失败.
虚拟机开机后, 系统启动成功日志会停在特定位置, 此时按下回车即可进入终端; 此时第一件事需要做的就是关闭防火墙, 因为仅在内网作为加速器使用完全不需要考虑安全问题:
1 |
|
接下来需要编辑网卡配置, 需要将 br-lan 的 IP 调整到与上级路由一个 IP 段内, 最简单的做法就是直接让 LAN 网卡使用 DHCP:
1 |
|
修改完网络以后, 还需要关闭 br-lan
网桥上的 DHCP 广播; 因为 OpenWrt 毕竟是一个路由器系统, 默认 dnsmasq 会在局域网开启 DHCP Server, 如果不关闭很可能它会抢答你内网的 DHCP 请求导致其他设备无法获取到正确的 IP 地址:
1 |
|
网络修改完成后, 调整 OpenWrt 镜像源, 安装 kmod-tun
和 open-vm-tools
(PVE 用户替换成 qemu-ga
):
1 |
|
全部修改完后, 重启即可.
其实 OpenWrt 配置好以后安装 UU 加速器就简单了, 直接执行一下官方文档的脚本即可:
1 |
|
安装完成后可看到 SN 码, 如果 SN 为空则证明安装步骤有问题, 请仔细阅读文章重新安装.
插件安装完成后, 手机上需要将网关设置为路由器 br-lan
的 IP, 然后打开 APP 添加路由器即可; 大多数人失败都是因为 OpenWrt 配置错误导致提示 “路由器型号不支持”, 如果出现了上述情况请重新仔细阅读文章, 尤其是有关 br-lan
的配置部分(其他文章中的添加防火墙规则之类的按本文教程不需要):
为了发挥 “懒惰使人进步” 的思想, 我在 GitHub 上专门通过 CI Build 好了一个专用版本, 只需要下载镜像启动即可完成全自动配置, 做到激活一下就直接用.
]]>发现很多网上的教程都是不完整的, 整个流程中完全不符合实际生产环境, 所以这里做一下简要说明.
MySQL Binary Log (BinLog) is a record of all changes made to a MySQL database. It serves as an audit trail of changes and can be used for various purposes, such as data recovery, replication, and database monitoring. BinLogs are created by the MySQL server and contain a record of all SQL statements that modify data.
简而言之, Binlog 是 MySQL 内部记录数据修改的 “日志”, 通过 Binlog 我们可以重放以前的执行流程.
想要使用 MySQL Binlog 进行恢复数据, 大致需要两个前提:
网上很多 Binlog 恢复都不谈核心问题, 核心问题就是你想做恢复之前必须有一份删除时间点之前的完整数据库备份, 因为本质上恢复流程就是重放所有 SQL 执行, 只不过只重放到被删之前而已.
对于 MySQL 8.0.x 可以使用以下配置让 MySQL 开启 Binlog 记录, 样例中有些配置不是必须的, 请自行参考引用文章:
1 |
|
本测试中所有数据库版本为 8.0.35, 理论上 5.x 版本和更高版本思路应该一致.
为了测试数据恢复搭建了一套测试环境, 测试环境中所有节点统一采用 Percona MySQL 8.0.35, 备份工具采用 Percona XtraBackup 执行完整全量备份. 其中数据库安装系统为 Ubuntu 22.04, 一个主库一个从库, 另外恢复操作在单独的测试库进行; 全部节点如下:
测试时采用独立的数据库(bintest
), 测试删除数据的表为 userinfo
, 建表语句以及测试数据如下所示:
1 |
|
前面已经说过, Binlog 恢复先决条件是有一份完整的备份, 本文中备份和恢复会使用 xtrabackup, 假定定时任务会每小时执行一次完整备份; xtrabackup 工具请自行安装(需要与 MySQL 大版本匹配), 同时 xtrabackup 工具会使用 qpress
进行压缩和解压备份, 需要单独安装. 备份和恢复脚本如下:
backup.sh
1 |
|
restore.sh
1 |
|
由于真实环境会涉及到 SLAVE 备份, 故本文测试时会加入 SLAVE 节点; SLAVE 创建过程这里不再过多赘述, 具体可以参考 How to set up a replica for replication in 6 simple steps with Percona XtraBackup.
基础数据恢复流程中将会模拟最理想化的环境: 由于误操作在主库发生了删除, 同时主库临近时间点有完整备份, 且主库的 Binlog 未滚动(与备份时使用相同的 Binlog 文件). 整个场景的事件发生顺序如图所示:
backup.sh
对主库进行了备份UPDATE userinfo SET idno=1111111 WHERE id = 8
数据发生了更改DELETE FROM userinfo WHERE id = 8
), 我们需要找回误删除的数据数据变化过程如下图所示:
在这种情况下首先要做的就是立即使用主库在 T1 时刻的完整备份在测试库进行还原, 保证测试库与备份时刻的数据一致:
1 |
|
由于备份是在 T1 时刻创建的, 所以备份不包含 T2 时刻的修改; 也就是说备份数据还原后应该与初始化数据一致:
1 |
|
由于 Binlog 恢复逻辑就是重复执行, 所以对于起始 Pos 点来说就是备份时间的 Pos 点; 这里可以直接从 xtrabackup 备份文件中找到:
1 |
|
对于结束 Pos 点来说, 它应当是执行数据误删除(DELETE FROM userinfo WHERE id = 8
)之前的最后一个点, 这里就需要借助 mysqlbinlog 命令来进行查找和解析:
首先在主库查看当前使用的 Binlog
1 |
|
接下来通过 mysqlbinlog 工具转换成 SQL 并进行搜索
1 |
|
通过查看生成的 SQL 可以看到, 在删除之前的最后一个 Pos 点为 888
.
有了起始 Pos 点和结束 Pos 点, 我们就可以在主库上通过 Binlog 来生成 从备份到删除之前所有的执行 SQL:
1 |
|
有了重做 SQL 以后, 我们就可以直接在测试库上重新应用它, 让测试库 “回到” 被删除之前的状态:
1 |
|
接下来可以在测试库查看恢复结果, 测试库应该回到了 T2 时刻即删除前的最后一刻:
1 |
|
最后提取数据重新还原到主库即可.
起始除了使用 Pos 点恢复以外, 还可以根据时间来进行恢复, 前提你能精准的把控删除发生的时间以及备份的时间:
1 |
|
总体来说按照时间点恢复可能会有一些误差, 比如同一时刻发生很多修改; 但是如果你能完全确定删除发生时间也不失为一个简单方法.
多 Binlog 恢复流程中假设在非理想情况下, T1 时刻进行备份, T2 时刻更改数据后主库 Binlog 发生了滚动, 然后在 T3 时刻数据被删除; 整个流程中复杂的点为 在备份时间(T1)到数据删除时间(T3)之间可能发生多次 Binlog 滚动, 这时我们必须联合多个 Binlog 文件来生成重做 SQL.
备份恢复步骤与 4.1 部分相同, 这里暂且省略; 查找起始 Pos 点采用同样的方法, 直接查看备份文件:
1 |
|
查找结束 Pos 点大致方法相同, 但是需要注意的是 Binlog 已经发生过滚动, 所以我们在过滤时需要联合多个 Binlog 进行查找:
首先查看当前主库的 Binlog 与备份 Binlog, 通过对比确定中间还有哪些 Binlog
1 |
|
可以看到, 主库 Binlog 已经滚动到 mysql-bin.000010
, 而备份时的 Binlog 为 mysql-bin.000007
, 这意味着我们需要搜索 7、8、9、10 四个 Binlog 来确定删除到底发生在哪里, 从而得到结束 Pos 点:
1 |
|
从生成的 SQL 中可以看到, 结束的 Pos 点为 1103
.
在涉及到多个 Binlog 文件, 通过起始 Pos 点生成重做 SQL 时需要注意一点: Pos 点不是一直自增的, 它是在每个 Binlog 中自增, 所以如果直接联合所有 Binlog 使用起始 Pos 点(157~1103) 来生成重做 SQL 是有问题的:
1 |
|
因为在 mysql-bin.000009
中 Pos 点可能增长到 1500, 然后在 mysql-bin.000010
中重新从 157
开始; 直接这样生成的重做 SQL 会丢失一部分数据, 正确做法是为每个中间 Binlog 生成完整重做 SQL, 然后再以起始 Pos 点为边界为两端 Binlog 生成重做 SQL:
1 |
|
恢复数据这一步跟上面的操作相同, 直接导入备份数据库然后查看被删除数据, 最后导出恢复到主库即可:
1 |
|
在大多数生产环境, 我们都不会选择直接从主库生成备份, 因为直接从主库生成备份时会产生较大磁盘 IO, 备份文件传输时又会造成网络占用; 大多数情况下我们都会在从库备份, 所以在本流程中假设:
在这种复杂环境中, 需要明确一点: 备份只有从库的, 所以一切要以从库为基准, 包括 Binlog Pos 点查找还原等.
在这种带有从库的环境中, 如果备份是从从库备份的, 那么 Binlog 恢复时仍然应该选择以从库为主进行操作; 对于起始 Pos 点, 仍然需要查看从库的备份文件:
1 |
|
对于结束 Pos 点来说, 首先要确认数据删除时主库的删除动作成功同步到从库, 然后在从库上根据 Binlog 查询删除动作, 获取结束 Pos 点:
查询删除后从库的 Pos 点
1 |
|
对比从库备份可得知 Binlog 未发生滚动(如果发生滚动参考 4.2 部分), 接下来查询结束 Pos 点:
1 |
|
从查询结果中可以看到, 删除发生前的结束 Pos 点为 883
.
有了两个 Pos 点以后, 同样老办法在从库生成重做 SQL:
1 |
|
同样有了重做 SQL, 只需要在测试库还原从库备份, 然后在从库备份上应用重做 SQL, 将数据还原到被删除前, 最后导出恢复到主库即可:
1 |
|
强烈推荐有能力的在内网部署 Bytebase 工具, 这个工具应该是腾讯出的, 基础特性开源同时也有商用版本; 简单的说可以把你的数据库修改变为标准的代码协作模式; 比如提交 PR、Review 合并等, 同时可以针对修改的 SQL 直接生成回滚 SQL, 出问题可以立即回滚:
这是一个 Java 编写的工具, 支持在线解析 Binlog, 同时直接生成回滚 SQL, 我这里没有实际尝试, 具体请参考项目 GitHub 页面.
备份永远说数据恢复的首选! 不管是 Slave 还是 Binlog, 多留点备份.
]]>相较于传统的 Linux 发行版来说, 纯容器化系统一般有以下优点:
从上面这些优点来看, 纯容器化系统一般适合运行一些固定服务, 且可以容忍一定的服务中断(系统需要滚动更新). 当然纯容器化系统也有一些缺点:
最开始做容器化系统的应该是大名鼎鼎的 CoreOS, 当时 CoreOS 开源了很多工具, 比如大名鼎鼎的 etcd 等; 后来 CoreOS 被红帽收购, 原来的版本也停止更新, 新版本 CoreOS 由红帽重构, 此后便出现了两个版本:
从 “名义” 上来说 Fedora CoreOS 算是 CoreOS 的继承者, 但实际上内部已经重构; 所以从 “血统” 上来讲还是 FlatCar 更加像以前的 CoreOS.
Fedora CoreOS 是红帽基于 Fedora 重新创建的 CoreOS, 该系统的特点是使用 rpm-ostree 工具来跟踪系统变化; rps-ostree 工具有点类似于 Git 一样跟踪系统变化, 同时也允许安装第三方 rpm 包; 相较于 FlatCar 来说其扩展性更强, 且并不是按照分区来保证系统的不可变性, 这样灵活度更高.
与 Fedora CoreOS 不同的是 FlatCar 采用与现在很多安卓手机的类似机制, 采用 A/B 分区的模式进行系统更新, 即系统启动时运行在 A 分区, 更新时只更新 B 分区, 下次重启自动切换到 B 分区启动; 这种方法的好处是简单直接可靠, 坏处就是没有 Fedora CoreOS 那样灵活.
还有一些区别就是 Fedora CoreOS 同时内置了 Podman 和 Docker 工具, 而 FlatCar 只内置了 Docker; 同时 Fedora CoreOS 网络工具采用的 NetworkManager, 而 FlatCar 采用的是 systemd-networkd.
综合来说两者各有优缺点, 我个人比较不喜欢 NetworkManager, 但 Podman 与 systemd 深度集成这点我还是挺喜欢的; 最后测试完纠结好久还是选择了 FlatCar, 因为 Podman 与 Docker 还是有些差异, 既然用不上又不喜欢 NetworkManager 同时 rpm-ostree 有一定学习成本, 那就干脆 FlatCar 好了; 不过值得说明的是两个系统配置文件大部分通用, 所以只是个人喜好问题.
FlatCar 官方默认提供了各种软硬件环境的集成安装, 对于纯物理机提供 iso、iPEX 等安装方式, 针对于 VM 部署也提供了预构建的磁盘镜像; 由于安装方式过多, 这里只以 VMWare 平台 VCSA/ESXi 为例, 其他平台请参考 官方文档.
针对于 VMWare 平台, 需要先下载官方提供的 ova 虚拟机模版文件:
1 |
|
对于 ESXi 平台可以直接部署, 但每次部署都需要上传 ova 虚拟机模版:
首先新建虚拟机, 并选择 “从 OVA 文件部署”
然后输入虚拟机名称, 并上传 ova 文件
其中启动打开电源选项请根据实际需求调整, 如果稍后要调整磁盘、网卡等则可以先取消勾选, 防止直接启动
其他设置中的 Options 配置暂时可以不写, 下一章节将会详细介绍配置
完成后可调整虚拟机硬件配置(比如磁盘大小、CPU等), 然后启动即可
对于 VCenter 来说与 ESXi 大体相同, 不同的是 VCenter 需要新创建一个 “内容库”, 然后上传 ova 虚拟机模版, 安装时需要从内容库来选择 ova, 免去了每次都要上传 ova 的问题.
首先创建存储 ova 的内容库
名称可随意填写
选择本地内容库
然后不启用安全策略, 选择存储即可创建完成
接下来点击 “操作” - “导入项目”
然后选择本地文件导入即可
后续步骤与 ESXi 基本一致, 都是新建虚拟机然后选择 ova 部署, 最后启动就可.
上面水了一堆, 其实并没有体现出容器化核心配置以及优势; 本部分将着重介绍容器化系统的配置方式和相关的配置样例.
由于历史原因容器化系统从最初的 CoreOS 发展到现在 Fedora CoreOS 和 FlatCar 经历了一系列变更, 其中配置文件最初以 Ignition File 变为现在的好几种格式; 下面说一下这几种格式的区别和应该用哪个:
所以综上所述, 对于配置文件只需要看 Butane Config 就行了; 而 Butane Config 我个人认为 Fedora CoreOS 的文档样例描述的相对清晰, 两者都支持 Butane Config 所以区别不大, 所以即使最终选择使用 FlatCar 系统, 在学习配置时也可以参考 Fedora CoreOS 的文档.
由于 Butane Config 需要转换成 Ignition File 才能被系统使用, 所以这里会用到一个转换工具, 即 butane
命令; 该命令可执行文件可以直接从 GitHub 下载, 也可以采用 Docker 镜像 quay.io/coreos/butane:release
; 具体的安装方式和细节请参考 官方文档.
1 |
|
详细配置说明放在下面, 这里讲一下怎么简单使用这个配置.
假设有以下 Butane Config, 且文件名为 test.yaml
:
1 |
|
我们需要先使用以下命令将其转换为 Ignition 配置:
1 |
|
转换完成后将会输出一长串 base64
编码的文本, 在创建虚拟机时将此内容添加到 Ignition/coreos-cloudinit data
字段中, 同时在 Ignition/coreos-cloudinit data encoding
中指定编码为 base64
, 虚拟机启动后将会自动应用配置.
Butane Config 中可以通过 passwd
属性配置容器化系统的内置用户和用户组, 需要注意的是默认情况下 root 用户是禁止 SSH 登陆的. 简单的配置样例如下:
1 |
|
以上配置在系统首次启动时会对 root 用户设置 ssh 登陆密钥, 后续就可以通过 ssh 使用 root 用户登陆; 除了 ssh_authorized_keys
字段以外还有一些常用的配置如下:
1 |
|
在某些情况下我们可能需要采用独立磁盘作为数据存储, 例如挂载独立的 SSD 磁盘等; 磁盘相关的处理可以通过 storage.disks
字段进行配置, 配置完成后系统首次启动将会按照配置中的定义自动进行磁盘分区. 下面是磁盘配置的详细样例:
1 |
|
上面使用完 storage.disks
对磁盘进行分区后, 还需要通过 storage.filesystems
对磁盘分区进行格式化创建文件系统和挂载; 文件系统创建及挂载的配置样例如下:
1 |
|
值得一提的是 FlatCar 默认采用 ext4 作为根文件系统格式, 你可以通过以下配置改变根文件系统格式为 xfs:
1 |
|
在 FlatCar 和 Fedora CoreOS 这类系统中, 其实大部分配置都是基于文件配置, 通过在 yaml 中定义配置文件内容和属性来达到自动配置的目的. 大部分常用的文件配置参数如下:
1 |
|
以下配置样例用于配置当前主机的 Hostname:
1 |
|
以下配置样例用于配置特定的 sysctl 参数
1 |
|
注意: 静态 IP 配置和 DNS 配置仅适用于 FlatCar, Fedora CoreOS 采用的是 NetworkManager, 所以配置有所不同, 请参考 Host Network Configuration 文档.
以下配置样例用于为第一个有线网卡配置静态 IP(默认情况下为 DHCP):
1 |
|
以下配置样例用于配置系统 DNS:
1 |
|
由于 FlatCar 是自动滚动更新的, 滚动更新时需要进行重启保证 A/B 分区切换, 所以为了可控性一般我们需要配置一下可以重启的时间(更新策略):
1 |
|
文件配置是一个灵活的配置选项, 除了做一些系统配置外我们还可以利用它放入一些我们自己的东西, 比如自定义脚本之类的:
1 |
|
当需要运行特定服务时, 一般我们需要创建一个 Systemd Service 配置文件, 这时就需要使用 Systemd 配置; Systemd 配置与文件配置类似, 不同之处在于 Systemd 配置虽然也只是定义 Systemd 文件, 但是提供了自启动等针对于 Systemd 的高级配置. 以下为运行 watchtower 容器的样例:
1 |
|
需要注意的是, name
属性需要以完整的 systemd units 名称结尾, 即可以通过文件名指定 units 类型, 例如 test.timer
代表创建一个定时器.
除了一些常规配置以外, 特殊情况下可能需要配置一些内核参数来控制系统行为; 比如默认情况下 FlatCar 会自动登录, 想关闭此行为可以通过一下配置调整内核参数:
1 |
|
同样也可以使用 should_exist
添加一些内核参数:
1 |
|
以下是一个 FlatCar 部署 GitLab 的完整样例, 仅供参考:
1 |
|
默认情况下 FlatCar 的 /usr
分区是不可写入的, 所以不要尝试向此目录中写入文件这会导致启动失败; 除此之外类似 /opt
之类的目录都可以持久化存放数据; 不过 Fedora CoreOS 似乎并不相同, 具体需要查阅官方文档.
FlatCar 如果想要扩展一些系统级的目录需要使用 Systemd-sysext, 具体请查看 官方文档; Fedora CoreOS 可以通过 rpm-ostree 直接安装软件包, 但有些服务可能需要重启才能生效(例如 open-vm-tools).
本篇文章仅描述了基本使用, 复杂情况例如批量更新控制等限于篇幅还请阅读官方文档.
]]>这玩意全称 “MySQL user-definable function”, 从名字就可以看出来叫 “用户定义的方法”; 那么 UDF 到底是干啥的呢?
简单一句话说就是说: 你可以自己写点代码处理数据, 然后把这段代码编译成动态链接库(so), 最后在 MySQL 中动态加载后用户就可以用了.
由于要检查数据库, 但是实际上审查并不会关注每个表甚至数据库细节; 所以想到最简单的方案就是在读取和写入时通过 UDF 定义一个 SM4 的加密算法把数据动态加密和解密, 关于其他细节这里不做详细说明, 本文主要阐述如何用 Go 搓一个简单的 UDF 并使用.
由于 UDF 官方支持是 C/C++, 所以在 Go 中需要使用 CGO; 一个 UDF 实现通常包含两个 func:
1 |
|
其中 xxx_init
用于预检查, xxx
作为真正的逻辑实现; 当 xxx
方法被调用之前会先通过 xxx_init
方法做一次参数、内存分配等预处理.
注意: 从 MySQL 8.0.1 开始 xxx_init
的返回值从 my_bool
变更为 int
, 网上很多代码写 my_bool
的会导致无法通过编译; 具体参考 https://bugs.mysql.com/bug.php?id=85131
知道了方法签名以后, 就不多废话直接上代码实现:
1 |
|
xsm4_enc_init
方法做一下检查, 当前只支持单个字段参数, xsm4_enc
通过开源的 gmsm
库对传入的字段进行简单的 SM4 加密并返回; 在真实环境中需要调用加密机来实现相关加密, 这里只演示直接使用开源库+固定密码.
将上面的代码保存为 xsm4_enc.go
, 然后在安装有 MySQL 头文件的的服务器上使用以下命令编译:
1 |
|
如果没问题将会生成一个 xsm4_enc.so
文件, 如果提示 C.xxx
类型没找到等问题说明头文件没有加载, 自行检查或修改 -I/usr/include/mysql
位置.
生成好 so 文件以后将其复制到 MySQL 的插件目录(插件目录可通过 SHOW VARIABLES LIKE 'plugin_dir';
查询到):
1 |
|
最后在 MySQL 中创建 UDF:
1 |
|
使用就简单了, 在查询的时候直接把你的 func 名称写上就行:
1 |
|
同理也可以创建一个解密 UDF, 当然这些 UDF 最终配合视图啥的做啥、怎么用就不做过多赘述了.
]]>ARPL 全称 Automated Redpill Loader
, 从名字可以看出其基于 Redpill 项目; 使用 ARPL 的优势在于:
由于 ARPL 直接提供 ESXi 的磁盘镜像, 所以我们直接从 Release 页面下载最新的磁盘镜像即可:
因为黑群晖需要安装在虚拟机中, 所以我们就需要先创建一下用于运行黑群晖的虚拟机, 不过在创建时有些选项需要调整:
具体请看下面的截图:
创建好虚拟机后, 需要使用下载的 ARPL 磁盘替换默认的启动磁盘, 在替换之前需要将我们下载的磁盘上传到 ESXi 虚拟机目录(先自行解压 ARPL zip 文件):
我们需要使用 ARPL 的磁盘作为引导盘, 所以需要将默认的 “硬盘1” 替换为上传的 ARPL 磁盘; 这个替换操作一般两种方式:
vmx
文件更改 “硬盘1” 配置说下两者优缺点, 无疑第一种方式是最简单且好用的, 但是测试在 ESXi7+ 版本似乎没法识别 ARPL 的硬盘, 保存时会报错(测试是 UI BUG, 自己创建的硬盘重新导入也不行); 而第二种方式经过测试直接修改是完全没问题, 且在 ESXi8 上也没问题(第一种 ESXi8 一样不能用).
所以这里采用更通用的第二种方式, 直接修改 vmx 文件:
vi
命令编辑 [虚拟机名称].vmx
文件sata0:0.fileName = "XPEnology.vmdk"
为 sata0:0.fileName = "arpl.vmdk"
并保存vmx
文件经过上面的调整以后, 就可以直接启动黑群晖虚拟机并进行调整了; 虚拟机启动后会直接进入控制台, 并打印 VNC 访问地址, 我们需要使用 VNC 调整参数来安装黑群晖:
访问该地之后会看到一个菜单列表, 根据需要一步一步填写信息即可:
下面是一系列的操作截图和说明, 推荐完全看完后再操作, 关于特殊的网卡和磁盘参数下一部分会有详细说明:
在调整参数时首次使用的用户可能比较懵逼, 下面详细说一下一些要点:
netif_num
这个参数用于告诉群晖系统现在系统有几个网卡, 一般自己生成或者购买的洗白码都会给你两个 mac 地址; 所以通常 netif_num
会设置成 2
, 当然如果你直接使用 arpl 随机生成的大概率不能登录群晖账号, 所以默认一个能联网就行.
mac[N]
这个参数用于定义具体网卡的 mac 地址, 众所周知群晖账户登录校验是要判断网卡 mac 的; 所以一般买来的洗白码出了序列号意外也会给两个 mac 地址, 依次填写 mac1
、mac2
就行; 同时推荐也在 ESXi 里把这两个 “网络适配器” 手动设置成跟群晖里一样的 mac 地址, 避免 ESXi 某些安全机制导致你没法联网:
SataPortMap
用于定义我们有几个 Sata 驱动器, 如果按照我的教程来走基本上是两个驱动器:
Sata0
: 专门挂载 arpl 启动盘Sata1
: 专门挂载用自己的磁盘, 主要用于 RDM 模式如果不使用 RDM 而是直通 Sata Controller, 那么理论上这个 Sata 控制器等同于 Sata1
(都是第二个). 明白了这一点就好理解为什么 SataPortMap
教程里被设置为 14
了: 因为第一个 Sata0
肯定只有 arpl 启动盘, 所以是 1
, 第二个需要根据用户实际情况自己写自己有几个盘; 我这里是 4 个 RDM, 所以自然写 4
.
DiskIdxMap
用于定义群晖里硬盘识别的计数, 有个小技巧就是如果你把某个 SATA 控制器上的磁盘计数设置成 0F
就会隐藏这个驱动器上的所有磁盘; 文章里 0F00
的意思就是告诉群晖: 挂 arpl 启动盘的这个 “Stata0” 控制器上所有磁盘给我隐藏掉, 所有硬盘序号从第二个控制器开始数”.
我在这一步翻车遇到了点问题, 当时以为是 ESXi 版本问题, 所以从 7.0 切换到 8.0; 后续截图会看到是 8.0 的截图, 所以不用怀疑, 我不是在胡编滥造… 当然升级 ESXi 8.0 以后得到了一个好消息和一个坏消息, 好消息是确实是版本问题, 坏消息是 8.0 也特么没修复(郭德纲经典相声了属于是)…
这部分本来不太想写, 网上随便一搜就有(翻车了…); 简单的说 ESXi 把物理硬盘直通到虚拟机里就两种方式, 一种是编辑 /etc/vmware/passthru.map
直接把 SATA 控制器直通进去, 另一种就是采用 RDM 方式创建 RDM 磁盘链接来直通; 两种方式孰优孰劣这里不做讨论; 唯一要说明的是如果采用 RDM 记得把 RDM 盘挂载到 Sata1
上, 因为上面的 SataPortMap
和 DiskIdxMap
都是按照两个 SATA 控制器设计的.
下面是我添加 RDM 的一些步骤和截图, 仅供参考:
首先命令行创建 RDM 磁盘, 核心命令就是 vmkfstools -z "物理磁盘路径" "RDM 磁盘路径"
:
接下来就是把 RDM 磁盘添加到虚拟机里; 说起来我写文章的时候觉得我以前操作很简单… 没想到这里翻车了; 简单说下问题, 估计大部分人跟我都一样, RDM 创建好以后发现添加现有硬盘添加不上, 表现跟上面说 UI 添加 ARPL 磁盘一样, 都是容量不显示也没法保存; 经过查论坛文章发现这是从 “ESXi 7.0 Update 3i” 版本开始导致的 UI BUG, 目前解决方案只有通过 PowerCLI 命令行方式解决:
Install-Module VMware.PowerCLI
安装 VMWare 的命令行工具, 过程中出现的提示全部选 “(Y) 是”Set-ExecutionPolicy Unrestricted
设置执行策略不受限制Set-PowerCLIConfiguration -Scope User -InvalidCertificateAction warn
设置忽略 TLS 证书不信任问题(一般 ESXi 默认自签名证书)Connect-VIServer -Server 服务器IP地址 -SaveCredentials -Protocol https -User root -Password 密码
完成登陆$vm = Get-VM -Name "你的虚拟机名称"
设置 $vm
这个变量New-HardDisk -VM $vm -diskPath 磁盘位置 -Confirm:$false
来添加 RDM 磁盘其中第 6 步的 “磁盘存储位置” 可以从 “数据存储浏览器” 中复制, 注意要连前面的存储名称一起复制, 比如我的第一块盘就是 “[ssd] XPEnology/RDM-S4Y0TMG0.vmdk”; 实操请看截图:
最后一步, 去 UI 界面编辑虚拟机, 把后添加的这几个 RDM 磁盘的控制器全部切换到 Sata1
上, 然后把命令行添加 RDM 时自动创建的 SCSI
控制器删了.
本部分参考:
总结一下上面 ARPL 虚拟机初始化的核心步骤:
在完整这些步骤后只需要简单的重启虚拟机, 等待启动完成就可以着手安装群晖系统了; 安装群晖系统不需要借助群晖的 Find 查找工具, 虚拟机启动后会在控制台打印 IP, 直接访问 http://IP:5000
端口即可.
安装时可以直接从提示处在线下载系统, 不过需要注意的是如果 ARPL 的编译版本号与最新的不匹配可能会有问题, 所以还是推荐点一下 “All Downloads” 然后自行查找.
剩下的安装步骤这里就不截图了, 基本上有手就行.
ESXi 默认情况下想要安全关闭一个虚拟机, 需要虚拟机内安装 VMWare 的 open-vm-tools
工具; 该工具作为一个 Agent 运行, 并上报系统的 IP、网速、磁盘、内存使用情况等信息给 ESXi; 当 ESXi 发出关机等指令时该工具根据系统类型自动调用系统命令来完成该操作. 当我们在 ESXi 上安装黑群晖后, ESXi 无法识别系统类型, 所以在管理界面只会显示一个 “关闭电源” 的按钮, 这个关机等同于物理机直接拔电源, 非常容易造成数据损坏.
解决办法只有一个就是安装 open-vm-tools
, 问题在于黑群晖系统不在 VMWare 官方的支持列表中, 所以并未提供安装包; 不过目前也有两种解决方案, 一种是采用 docker 运行 open-vm-tool
, 另一种是自己编译 open-vm-tools
; 两种方案各有优缺点:
open-vm-tools
上报的操作系统是 docker 容器的系统; 好处就是 “永久兼容” 没有群晖版本依赖问题.open-vm-tools
: 缺点是绑定群晖系统版本, 需要等待社区大神改代码才能跟上群晖的发版; 好处就是原生运行不需要群晖内有额外的系统资源占用.这个是最简单的, 需要先在系统内安装 Docker 套件, 然后 SSH 到群晖系统, 执行以下命令运行容器(需要使用 root 用户):
1 |
|
关于 mritd/open-vm-tools
这个镜像是我自己编译的, 担心安全问题的可以自行查看源码 AutoBuild; 原理其实就是点关机时帮你自动从 Docker 容器 ssh 到群晖系统然后执行关机命令.
期望自行编译 open-vm-tools
可以参考 synology-dsm-open-vm-tools 仓库文档, 编译时请保证网络环境畅通, 有条件的最好用国外 VPS 8C 16G 配置进行编译. 编译完成后会生成 SPK 安装包, 直接在套件中心选择手动安装即可:
安装成功后, 回到 ESXi 中就可以看到虚拟机电源控制中出现了 “关机” 选项:
目前我已经编译好了适用于 7.1.1
系统的 open-vm-tools
, 截止文章编写时间还没想上传到公网, 有需要的可以留言.
目前我使用的是 HPE Gen8 作为主机, 宿主机的来电自启动 ESXi 都已经配置完成; 如果想要让特殊情况断电以后 ESXi 能够自动启动黑群晖, 只需要在 ESXi 中配置一下即可:
到此 ESXi 上基本的黑群晖已经安装完成, 剩下的系统内折腾就各凭本事了.
]]>这里我采用的是 Ubuntu Server 22.04 系统, 安装直接一条 apt 命令即可:
1 |
|
在配置 Nut 之前需要先了解下一 Nut 的各个组件及其作用, Nut 主要包含三个核心服务:
注意: nut-client 实际上是一个 systemd 软连接文件, 本质上还是 nut-monitor.
为了更容易理解这里画了一个简单的图:
1 |
|
在 Nut 安装完成后会自动在 /etc/nut
目录生成配置文件, 接下来的配置工作主要是调整该目录下的各种配置文件.
该配置主要定义 nut 的运行模式, 只有一个配置字段 MODE=xxxx
, 该配置可选值及含义如下:
none
: Nut 未配置或使用外部系统启动 Nut 服务, 可以理解为 “啥也不干”.standalone
: 独立模式, 一般在只有一个 UPS 且只负责本地系统(不提供网络服务)的情况下使用.netserver
: 跟独立模式类似, 会启动 driver、upsd 和 upsmon 服务, 不同之处是可以提供网络服务, 其他机器上的 nut-monitor 可以通过网络来连接 Nut Server.netclient
: 仅客户端模式, 只启动 nut-monitor, 用于连接远程的 Nut 服务.我这是为了方便后续扩展, 所以使用了最全的 netserver
模式:
1 |
|
该配置用于定义 nut-driver 如何连接到物理 UPS, 该配置文件的饿配置格式如下:
1 |
|
其中 nutdev1
表示该 UPS 名称, 可以随意定义; driver
用于定义连接到该 UPS 需要使用的驱动, 一般情况下如果使用 USB 连接像我这样写都是可以识别到的. 如果同样都是用 USB 连接但识别不到, 可以使用 nut-scanner
命令进行扫描, 扫描成功后会在控制台打印出 UPS 相关配置样例.
需要注意的是, 如果想要群晖或者 QNAP 能通过网络连接 UPS, 那么 UPS 的名称是有特殊要求的(群晖必须起名叫 ups
, QNAP 没有测试过可能叫 qnapups
); 因为这两个系统都写死了, 包括后面的用户名和密码也是, 具体在下文会有说明.
除了 USB 连接 UPS 之外 Nut 还支持多种驱动连接 UPS, 例如 APC 专用的驱动程序, 有关于 Nut 具体连接方式请查看官方文档:
该配置文件用于控制 nut-server 的网络服务, 例如监听端口、最大连接数、证书配置等; 由于我是在内网使用, 所以只需要配置网络监听, 其他参数保持默认即可:
1 |
|
如果想要完整查看支持哪些配置, 请参考官方文档: UPSD.CONF(5)
upsd.users 配置文件用于定义通过网络连接到 nut-server
的用户名和密码, 该配置样例如下:
1 |
|
上面的 [xxxx]
代表用户名, 密码由 password
字段指定; 除此之外还有一些特殊参数:
actions
: 指定该用户具有哪些操作权限, 可选值 SET
(更改 UPS 变量)、FSD
(设置 UPS 强制关机标志); 如果需要两个都指定, 则需要写两次 actions
.instcmds
: 让用户启动的即时命令, 值 ALL
代表所有, 其他的可通过 upscmd -l
查看; 同样如果要指定多个需要写多次 instcmds
.upsmon
: 为 upsmon 进程添加必要的操作, 可选值 primary
、secondary
(一般用不到)注意: Ubuntu Server 22.04 上的版本可能没有那么新, upsmon 实际上支持的是 master
、slave
两个参数(怀疑与某场运动有关).
关于 群晖 和 QNAP 用户: 如果期望这两个 NAS 可以直接连接到 Nut Server, 可以确认的是群晖需要保证存在用户名为 monuser
密码为 secret
的用户; QNAP 我没有验证过, 网上查询到的结果是需要保证存在用户名为 admin
密码为 123456
的用户.
相较于基本配置来说, Nut 核心处理在于如何做好监控配置; Nut 对于监控策略支持大致有两种:
upsmon.conf
配置, 任何事件直接由用户指定的脚本负责处理, 没有定时器等高级特性upsmon.conf
中配置执行脚本为 upssched
, 任何事件先由 upssched
处理, 借助于 upssched 可以实现一些高级功能, 比如选择性触发关机等该配置主要用于配置 nut-monitor 如何监控 UPS, 同时定义 UPS 出现哪些事件要进行怎样的处理动作, 下面详细解释一下核心配置.
MONITOR
指令用于定义要监控 UPS 的连接地址, 其格式如下:
1 |
|
<system>
: nut-server 链接地址, 格式为 “UPS 名称” + “@” + “nut-server 地址”, 例如 myups@192.168.1.2
<powervalue>
: UPS 数量, 大多数情况你只有一个 UPS 电源, 所以写 1 就行<username>/<password>
: 在 upsd.users
中定义的用户名和密码master/slave
: master
表示该系统将最后关闭, 让从属系统先关闭; slave
表示该系统立即关闭以下为我的配置样例:
1 |
|
SHUTDOWNCMD
指令用于定义在 UPS 电量不足或者需要主动关机时的关机命令, 建议填写完整路径
1 |
|
NOTIFYCMD
指令是一个非常重要的指令, 该指令用于配置在发生特定事件(如市电中断、UPS 处于低电量等)时, nut-monitor
所执行的命令. 简单的说就是 NOTIFYCMD
定义了具体执行命令, 可以直接在此处配置一个自己编写的脚本, 当 UPS 有事件发生时此脚本都会被调用.
NOTIFYCMD
指令大致有两种配置方式, 一种是配置成自己的脚本, 脚本需要有可执行权限, 脚本内可以通过 NOTIFY
环境变量获取事件类型, 然后自己进行处理. 这种方式有点 “简单粗暴” 的意思, 可定制化程度完全依赖于你的脚本怎么写. 具体可以使用哪些变量请自行测试, 因为版本不同可能环境变量名称也不同.
还有一种方式是将 NOTIFYCMD
配置为执行内置的 upssched
命令; upssched
是 Nut 提供的一个带有特定策略的调度程序; 简而言之就是基于常用的功能进行了抽象, upssched
有自己单独配置, 可以实现 “如果市电在 180s 内恢复则不进行关机” 的这种高级调度策略.
这里我选择使用第二种方式, 因为自己搓脚本能力实在不怎么样:
1 |
|
NOTIFYFLAG
同样是关键配置, 需要与 NOTIFYCMD
配合使用; NOTIFYFLAG
指令负责指定一系列的 UPS 事件应该触发何种操作, 该指令格式如下:
1 |
|
其中 <notify type>
表示事件类型, 可选类型如下:
ONLINE
: UPS 在线, 即市电恢复时会触发ONBATT
: UPS 使用电池供电, 即市电中断时会触发LOWBATT
: UPS 低电量时会触发FSD
: UPS 正在被关闭(Forced Shutdown)COMMOK
: 与 nut-server 成功建立连接时触发COMMBAD
: 与 nut-server 建立连接失败(连接丢失)时触发SHUTDOWN
: UPS 发出关机指令触发REPLBATT
: UPS 需要更换电池时触发NOCOMM
: 无法与 UPS 建立连接(UPS未就绪)时触发对于 flag
标志通常有四种, 多种组合时用加号(+)连接:
SYSLOG
: 只打印 syslogWALL
: 在终端上弹出消息(/bin/wall)EXEC
: 调用 NOTIFYCMD
指定的命令, 并传递相关事件IGNORE
: 啥也不干, 忽略该事件如果 NOTIFYCMD
使用了自定义脚本, 则此处请根据实际需要来配置需要脚本处理的事件; 如果 NOTIFYCMD
配置为使用 upssched
, 可以将所有事件配置为 EXEC
, 然后具体过滤在 upssched
处理.
例如如果只想让 NOTIFYCMD
配置的脚本只处理市电中断和恢复事件, 同时打印 syslog, 可以这样配置:
1 |
|
由于我 NOTIFYCMD
配置的是 upssched
, 所以我这里将所有事件全部传递给 upssched
:
1 |
|
除了以上的核心配置, 其他配置如果没特殊情况一般保持默认即可; 具体的配置可以参考官方文档: UPSMON.CONF(5).
使用该配置的前提是 upsmon.conf
配置中的 NOTIFYCMD
指向了 upssched
, 并且 NOTIFYFLAG
为相关事件配置了 EXEC
; 该配置主要的作用是使用一些 upssched
内置的高级语法来控制特定事件的处理方式. upssched.conf
配置主要包含两部分: 头部的选项配置和尾部的规则配置.
头部选项配置只包含三个配置:
CMDSCRIPT
: 该配置应该位于首行(推荐这样, 实际上是 AT 指令之前就行), 该指令用于定义事件的处理脚本; 脚本一般由用户自行编写, upssched
会根据规则将指定参数传递到此脚本并执行.PIPEFN
: 用于进程间通信的管道文件, 需要位于 AT 指令之前LOCKFN
: 互斥锁文件, 用于防止 upsmon 同时调度多个文件, 需要位于 AT 指令之前关于 CMDSCRIPT
配置的脚本, 下面是一个样例:
1 |
|
有一点需要注意, CMDSCRIPT
配置的脚本默认是以 nut
用户运行的, 所以要处理好 nut
用户权限问题.
对于 PIPEFN
和 LOCKFN
官方推荐单独创建叫 upssched
目录, 然后文件放在这个目录里; 但是有些系统例如 Ubuntu Server 22.04, /run/nut/
是 tmpfs, 重启会丢失目录导致出现权限问题; 所以推荐直接不创建独立目录直接配置:
1 |
|
使用 upssched
的好处就是内置了一个规则引擎, 我们可以通过一些简单的语法来配置复杂规则; upssched
的规则语法如下:
1 |
|
规则以 AT
开头, notifytype
用于指定需要关注的事件类型; upsname
指定 UPS 名称, 只有一个的情况下或者不想区分时可以无脑写 *
; command
部分用于指定需要执行的动作, command
大致有三种类型:
START-TIMER
: 启动一个定时器CANCEL-TIMER
: 取消一个定时器EXECUTE
: 立即执行这部分看起来复杂实际很简单, 例如以下规则实现了: 当市电断开(UPS 使用电池供电时)启动一个名字叫 onbattwarn
的定时器, 这个定时器在 180s 后会执行 CMDSCRIPT
定义的脚本并将 onbattwarn
作为第一个参数传递给脚本; 同时如果市电在计时器的 180s 之内恢复(临时闪断), 则取消执行脚本.
1 |
|
当然也有些事件是需要立即执行的, 例如: 市电中断立即发送通知
1 |
|
上面说了那么多, 下面给一个完整的我个人的配置参考:
nut.conf
1 |
|
ups.conf
1 |
|
upsd.conf
1 |
|
upsd.users
1 |
|
upsmon.conf
1 |
|
upssched.conf
1 |
|
upssched-cmd.sh
1 |
|
首先需要有个装好的 vCenter Server(等于没说), 其次就是如果在安装时没有正确的设置 FQDN(PNID), 那么是没法直接使用 ACME 证书的, 只能通过反向代理套一下解决.
这里采用 acme.sh 作为证书申请工具, 安装方式正常 ssh 到 vCenter Server 主机然后按照官方教程安装即可:
1 |
|
由于是在内网使用, 所以只能使用 DNS API 的方式申请证书:
1 |
|
vCenter Server 内置了一个 certificate-manager
工具用于在命令行更新证书, 先使用此命令更新证书:
1 |
|
确认证书替换成功后, 我们就可以弄个自动化脚本然后自动更新了; 不过需要注意的是: 如果 vCenter Server 的 FQDN(PNID) 在安装时配置错误(域名没有做解析), 那么此时 vCenter Server PNID 将会变为 IP, 更新证书必然会失败.
1 |
|
然后编辑 ~/.acme.sh/update.conf
内的账户信息, 尝试使用 ~/.acme.sh/auto-updater.sh
更新证书; 如果更新成功接下来添加定时任务即可:
1 |
|
不过根据原作者文章下的评论, 可能仍需要在脚本后添加一刚重启命令:
1 |
|
关于最基础的底层镜像, 通常大多数我们只有三种选择: Alpine、Debian、CentOS; 这三者中对于运维最熟悉的一般为 CentOS, 但是很不幸的是 CentOS 后续已经不存在稳定版, 关于它的稳定性问题一直是个谜一样的问题; 这是一个仁者见仁智者见智的问题, 我个人习惯是能不用我绝对不用 😆.
排除掉 CentOS 我们只讨论是 Alpine 还是 Debian; 从镜像体积角度考虑无疑 Alpine 完胜, 但是 Alpine 采用的是 musl 的 C 库, 在某些深度依赖 glibc 的情况下可能会有一定兼容问题. 当然关于深度依赖 glibc 究竟有多深度取决于具体应用, 就目前来说我个人也只是遇到过 Alpine 官方源中的 OpneJDK 一些字体相关的 BUG.
综合来说, 我个人的建议是如果应用深度依赖 glibc, 比如包含一些 JNI 相关的代码, 那么选择 Debian 或者说基于 Debian 的基础镜像是一个比较稳的选择; 如果没有这些重度依赖问题, 那么在考虑镜像体积问题上可以选择使用 Alpine. 事实上 OpneJDK 本身体积也不小, 即使使用 Alpine 版本, 再安装一些常用软件后也不会小太多, 所以我个人习惯是使用基于 Debian 的基础镜像.
大多数人似乎从不区分 JDK 与 JRE, 所以要确定这事情需要先弄明白 JDK 和 JRE 到底是什么:
JDK 是一个开发套件, 它会包含一些调试相关的工具链, 比如 javac
、jps
、jstack
、jmap
等命令, 这些都是为了调试和编译 Java 程序所必须的工具, 同时 JDK 作为开发套件是包含 JRE 的; 而 JRE 仅为 Java 运行时环境, 它只包含 Java 程序运行时所必须的一些命令以及依赖类库, 所以 JRE 会比 JDK 体积更小、更轻量.
如果只需要运行 Java 程序比如一个 jar 包, 那么 JRE 足以; 但是如果期望在运行时捕获一些信息进行调试, 那么应该选择 JDK. 我个人的习惯是为了解决一些生产问题, 通常选择直接使用 JDK 作为基础镜像, 避免一些特殊情况还需要挂载 JDK 的工具链进行调试. 当然如果没有这方面需求, 且对镜像体积比较敏感, 那么可以考虑使用 JRE 作为基础镜像.
针对于这两者的选择, 取决于一个最直接的问题: 应用代码中是否有使用 Oracle JDK 私有 API.
通常 “使用这些私有 API” 指的是引入了一些 com.sun.*
包下的相关类、接口等, 这些 API 很多是 Oracle JDK 私有的, 在 OpneJDK 中可能完全不包含或已经变更. 所以如果代码中包含相关调用则只能使用 Oracle JDK.
值得说明的是很多时候使用这些 API 并不是真正的业务需求, 很可能是开发在导入包时 “手滑” 并且凑巧被导入的 Class 等也能实现对应功能; 对于这种导入是可以被平滑替换的, 比如换成 Apache Commons 相关的实现. 还有一种情况是开发误导入后及时发现了, 但是没有进行代码格式化和包清理, 这是会在代码头部遗留相关的 import
引用, 而 Java 是允许存在这种无用的 import
的; 针对这种只需要重新格式化和优化导入即可.
Tips: IDEA 按 Option + Command + L
(格式化) 还有 Control + Option + O
(自动优化包导入).
当没有办法必须使用 Oracle JDK 时, 推荐自行下载 Oracle JDK 压缩包并编写 Dockerfile 创建基础镜像. 但是这会涉及到一个核心问题: Oracle JDK 一般不提供历史版本, 所以如果要考虑未来的重新构建问题, 建议保留好下载的 Oralce JDK 压缩包.
众所周知 OpenJDK 是一个开源发行版, 基于开源协议各大厂商都提供一些增值服务, 同时也预编译了一些 Docker 镜像供我们使用; 目前主流的一些发行版本如下:
这些发行版很多是大同小异的, 一些发行版可能提供的基础镜像选择更多, 比如 AdoptOpenJDK 提供基于 Alpine、Ubuntu、CentOS 的三种基础镜像发行版; 还有一些发行版提供其他的 JVM 实现, 比如 IBM Semeru Runtime 提供 OpenJ9 JVM 的预编译版本.
目前我个人比较喜欢 AdoptOpenJDK, 因为它是社区驱动的, 由 JUG 成员还有一些厂商等社区成员组成; 而 Amazon Corretto 和 IBM Semeru Runtime 看名字就可以知道是云高端玩家做的, 可用性也比较棒. 其他的类似 Azul Zulu、Liberica JDK 则是一些 JVM 提供厂商, 有些还有点算得上是黑料的东西, 不算特别推荐.
目前 AdoptOpenJDK 已经合并到 Eclipse Foundation, 现在叫做 Eclipse Adoptium; 所以如果想要使用 AdoptOpenJDK 镜像, Docker Hub 中应该使用 eclipse-temurin 用户下的相关镜像.
对于 JVM 实现来说, Oracle 有一个 JVM 实现规范, 这个实现规范定义了兼容 Java 代码运行时的这个 VM 应当具备哪些功能; 所以只要满足这个 JVM 实现规范且经过了认证, 那么这个 JVM 实现理论上就可以应用于生产. 目前市面上也有很多 JVM 实现:
这些 JVM 实现可能具有不同的特性和性能, 比如 Hotspot 是最常用的 JVM 实现, 综合性能、兼容性等最佳; 由 IBM 创建目前属于 Eclipse 基金会的 OpneJ9 对容器化更友好, 提供更快启动和内存占用等特性.
通常建议如果对 JVM 不是很熟悉的情况下, 请使用 “标准的” Hotspot; 如果有更高要求且期望自行调试一些 JVM 优化参数, 请考虑 Eclipse OpenJ9. 我个人比较喜欢 OpenJ9, 原因是它的文档写的很不错, 只要细心看可以读到很多不错的细节等; 如果要使用 OpenJ9 镜像, 推荐直接使用 ibm-semeru-runtimes 预编译的镜像.
当我们需要关闭一个程序时, 通常系统会像该进程发送一个终止信号, 同样在容器停止时 Kubernetes 或者其他容器工具也会像容器内 PID 1 的进程发送终止信号; 如果容器内运行一个 Java 程序, 那么信号传递给 JVM 后 Java 相关的框架比如 Spring Boot 等就会检测到此信号, 然后开始执行一些关闭前的清理工作, 这被称之为 “优雅关闭(Graceful shutdown)”.
如果在我们容器化 Java 应用时没有正确的让信号传递给 JVM, 那么调度程序比如 Kubernetes 在等待容器关闭超时以后就会进行强制关闭, 这很可能导致一些 Java 程序无法正常释放资源, 比如数据库连接没有关闭、注册中心没有反注册等. 为了验证这个问题, 我创建了一个 Spring Boot 样例项目来进行测试, 其中项目中包含的核心文件如下(完整代码请看 GitHub):
@PreDestroy
注册 Hook 来监听关闭事件模拟优雅关闭bash -c
来实现优雅关闭由于 BeanTest
只做打印测试都是通用的, 所以这里直接贴代码:
1 |
|
在很多原始的 Java 项目中通常会存在一个启动运行脚本, 这些脚本可能是自行编写的, 也可能是一些比较老的 Tomcat 启动脚本等; 当我们使用脚本启动并且没有合理的调整 Dockerfile 时就会出现信号无法正确传递的问题; 例如下面的错误示范:
entrypoint.bad.sh: 负责启动
1 |
|
Dockerfie.bad: 使用 bash 启动脚本, 这会导致终止信号无法传递
1 |
|
通过这个 Dockerfile 打包运行后, 在使用 docker stop
命令时明显卡顿一段时间(实际上是 docker 在等待容器内进程自己退出), 当到达预定的超时时间后容器内进程被强行终止, 故没有打印优雅关闭的日志:
要解决信号传递这个问题其实很简单, 也有很多方法; 比如常见的直接使用 CMD
或 ENTRYPOINT
指令运行 java 程序:
Dockerfile.direct: 直接运行 java 程序, 能够正常接受到终止信号
1 |
|
可以看到, 在 Dockerfile 中直接运行 java 命令这种方式可以让 jvm 正确的通知应用完成优雅关闭:
熟悉 Docker 的同学都应该清楚, 在 Dockerfile 里直接运行命令无法解析环境变量; 但是有些时候我们又依赖脚本进行变量解析, 这时候我们可以先在脚本内解析完成, 并采用 exec
的方式进行最终执行; 这种方式也可以保证信号传递(不上图了):
entrypoint.exec.sh: exec 执行最终命令, 可以转发信号
1 |
|
除了直接执行和 exec 方式其实还有一个我称之为 “不稳定” 的解决方案, 就是使用 bash -c
来执行命令; 在使用 bash -c
执行一些简单命令时, 其行为会跟 exec 很相似, 也会把子进程命令替换到父进程从而让 -c
后的命令直接接受到系统信号; 但需要注意的是, 这种方式不一定百分百成功, 比如当 -c
后面的命令中含有管道、重定向等可能仍会触发 fork
, 这时子命令仍然无法完成优雅关闭.
Dockerfile.bash-c: 采用 bash -c
执行, 在命令简单情况下可以做到优雅关闭
1 |
|
关于 bash -c
的相关讨论, 可以参考 StackExchange.
守护工具并不是万能的, tini 和 dump-init 都有一定问题.
这两个工具是大部分人都熟知的利器, 甚至连 Docker 本身都集成了; 不过似乎很多人都有一个误区(我以前也是这么觉得的), 那就是认为加了 tini 或者 dump-init 信号就可以转发, 就可以优雅关闭了; 而事实上并不是这样, 很多时候你加了这两个东西也只能保证僵尸进程的回收, 但是子进程仍然可能无法优雅关闭. 比如下面的例子:
Dockerfile.tini: 加了 tini 也无法优雅关闭的情况
1 |
|
对于 dump-init 也有同样的问题, 归根结底这个问题的根本还是在 bash 上: 当使用 bash 启动脚本后, bash 会 fork 一个新的子进程; 而不管是 tini 还是 dump-init 的转发逻辑都是将信号传递到进程组; 只要进程组中的父进程响应了信号, 那么就认为转发完成, 但此时进程组中的子进程可能还没有完成优雅关闭父进程就已经死了, 这会导致变为子进程最终还会被强制 kill 掉.
根据上面的测试和验证结果, 这里总结一下最佳实践:
bash -c
运行在简单命令执行时也可以优雅关闭, 但需要实际测试来确定准确性Java 应用的容器化内存限制是一个老生常谈的问题, 国内也有很多资料, 不过这些文章很多都过于老旧或者直接翻译自国外的文章; 我发现很少有人去深究和测试这个问题, 随着这两年容器化的发展其实很多东西早已不适用, 为此在这里决定专门仔细的测试一下这个内存问题(只想看结论的可直接观看 6.3 章节.).
众所周知, Java 是有虚拟机的, Java 代码被编译成 Class 文件然后在 JVM 中运行; JVM 默认会根据操作系统环境来自动设置堆内存(HeapSize), 而容器化 Java 应用面临的挑战其一就是如何让 JVM 获取到正确的可用内存避免被 kill.
在默认不配置时, 理想状态的 JVM 应当能识别到我们对容器施加的内存 limit, 从而自动调整堆内存大小; 为了验证这种理想状态下哪些版本的 OpenJDK 能做到, 我抽取一些特定版本进行了以下测试:
docker run -m 512m ...
将容器内存限制为 512m, 实际宿主机为 16Gjava -XX:+PrintFlagsFinal -version | grep MaxHeapSize
命令查看 JVM 默认的最大堆内存(后来发现 -XshowSettings:vm
看起来更清晰)这个版本的 OpenJDK 尚未对容器化做任何支持, 所以理论上它是不可能能获取到 limit 的内存限制:
可以看到 JVM 并没有识别到 limit, 仍然按照大约宿主机 1/4 的体量去分配的堆内存, 所以如果里面的 java 应用内存占用高了可能会被直接 kill.
选择 8u131 这个版本是因为在此版本添加了 -XX:+UseCGroupMemoryLimitForHeap
参数来支持内存自适应, 这里我们先不开启, 先直接进行测试:
同样在默认情况下是无法识别内存限制的.
8u191 版本从 OpneJDK 10 backport 回了 XX:+UseContainerSupport
参数来支持 JVM 容器化, 不过该版本暂时无法下载, 这里使用更高的 8u222
测试, 测试时同样暂不开启特定参数进行测试:
同样的内存无法正确识别.
OpenJDK 11 版本已经开始对容器化的全面支持, 例如 XX:+UseContainerSupport
已被默认开启, 所以这里我们仍然选择不去修改任何设置去测试:
可以看到, 即使默认打开了 UseContainerSupport
开关, 仍然无法正常的自适应内存.
可能很多人会好奇, 都测试了 11.0.15 为什么还要测试 11.0.16? 因为这两个版本在不设置的情况下有个奇怪的差异:
可以看到, 11.0.16
版本在不做任何设置时自动适应了容器内存限制, 堆内存从接近 4G 变为了 120M.
OPneJDK 17 是目前最新的 LTS 版本, 这里再专门测试一下 OpneJDK 17 不调整任何参数时的内存自适应情况:
可以看到 OpneJDK 17 与 OpenJDK 11.0.16 版本一样, 都可以实现内存的自适应.
在上面的无配置情况下我们进行了一些测试, 测试结果从 11.0.15 版本开始出现了一些 “令人费解” 的情况; 理论上 11+ 已经自动打开了容器支持参数, 但是某些版本内存自适应仍然无效, 这促使我对其他参数的实际效果产生了怀疑; 为此我开始按照各个参数的添加版本手动启用这些参数进行了一些测试.
8u131 正式开始进行容器化支持, 在这个版本增加了一个 JVM 选项来告诉 JVM 使用 cgroup 设置的内存限制; 我增加了 -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
参数进行测试, 测试结果是这个选项在我当前的环境中似乎完全不生效:
从 8u191 版本开始, 又增加了另一个开启容器化支持的参数 -XX:+UseContainerSupport
, 该参数从 OpenJDK 10 反向合并而来; 我尝试使用这个参数来进行测试, 结果仍然是没什么卵用:
从 11+ 版本开始 -XX:+UseContainerSupport
已经自动开启, 我们不需要再做什么特殊设置, 所以结果是跟无配置测试结果一致的: 从 11.0.15
以后的版本开始能够自适应, 之前的版本(包括 11.0.15
)都不支持自适应.
经过上面的一些测试后会发现, 在很多文章或文档中描述的参数出现了莫名其妙不好使的情况; 这主要是因为容器化这两年一个很重要的更新: Cgroups v2; 限于篇幅问题这里不在一一罗列测试截图, 下面仅说一下结论.
对于使用 Cgroups V1
的容器化环境来说, “旧的” 一些规则仍然适用(新内核增加内核参数 systemd.unified_cgroup_hierarchy=0
回退到 Cgroups V1):
-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
参数支持内存自适应.-XX:+UseContainerSupport
参数支持内存自适应.-XX:+UseContainerSupport
参数, 自动支持内存自适应在新版本系统(具体自行查询)配合较新的 containerd 等容器化工具时, 已经默认转换为 Cgroups V2
, 需要注意的是针对于 Cgroups V2
的内存自适应只有在 OpneJDK 11.0.16 以及之后的版本才支持, 在这之前开启任何参数都没用.
关于 Cgroups V2 的一些支持细节具体请查看 JDK-8230305:
在大部分 Java 程序中我们都会使用域名去访问一些服务, 可能是访问某些 API 端点或者是访问一些数据库, 而不论哪样只要使用了域名就会涉及到 DNS 缓存问题; Java 的 DNS 缓存是由 JVM 控制的, 不要理所当然的以为 JVM DNS 缓存非常友好, 某些时候 DNS 缓存可能超出预期. 为了测试 DNS 缓存情况我从某大佬这里抄来一个测试脚本, 该脚本会测试三个版本的 OpenJDK DNS 缓存情况:
jvm-dns-ttl-policy.sh
1 |
|
默认不做任何设置的 DNS 缓存结果如下(直接运行脚本即可):
可以看到, 默认情况下 DNS TTL 被设置为 30s, 如果开启了 Security Manager
则变为 -1s, 那么 -1s 什么意思呢(截取自 OpenJDK 11 源码):
1 |
|
为了避免这种奇奇怪怪的 DNS 缓存策略问题, 最好我们在启动时通过增加 -Dsun.net.inetaddr.ttl=xxx
参数手动设置 DNS 缓存时间:
可以看到, 一但我们手动设置了 DNS 缓存, 那么不论是否开启 Security Manager
都会遵循我们的设置. 如果需要更细致的调试 DNS 缓存推荐使用 Alibaba 开源的 DCM 工具.
Native 编译优化是指通过 GraalVM 将 Java 代码编译为可以直接被平台执行的二进制文件, 编译后的可执行文件运行速度会有极大提升. 但是 GraalVM 需要应用的代码层调整、框架升级等操作, 总体来说比较苛刻; 但是如果是新项目, 最好让开发能支持一下 GraalVM 的 Native 编译, 这对启动速度等有巨大提升.
上面介绍的用于测试优雅关闭的项目已经内置了 GraalVM 支持, 只需要下载 GraalVM 并设置 JAVA_HOME
和 PATH
变量, 并使用 mvn clean package -Dmaven.test.skip=true -Pnative
编译即可:
编译成功后将在 target
目录下生成可以直接执行的二进制文件, 以下为启动速度对比测试:
可以看到 GraalVM 编译后启动速度具有碾压级的优势, 基本差出一个数量级; 但是综合来说这种方式目前还不是特别成熟, 迄今为止国内 Java 生态仍是 OpneJDK 8 横行, 老旧项目想要满足 GraalVM 需要调整的地方比较巨大; 所以总结就是新项目能支持尽量支持, 老项目不要作死.
]]>由于国内网络环境问题, 普遍家庭用户宽带都没有分配到公网 IP(我有固定公网 IP, 嘿嘿); 这时候一般我们需要从外部访问家庭网络时就需要通过一些魔法手段, 比如 VPN、远程软件(向日葵…)等; 但是这些工具都有一个普遍存在的问题: 慢+卡!
究其根本因素在于, 在传统架构中如果两个位于多层 NAT(简单理解为多个路由器)之后的设备, 只能通过一些中央(VPN/远程软件)中转服务器进行链接, 这时网络连接速度取决于中央服务器带宽和速度; 这种网络架构我这里简称为: 星型拓扑
从这张图上可以看出, 你的 “工作笔记本” 和 “家庭 NAS” 之间通讯的最大传输速度为 Up/Down: 512K/s
; 因为流量经过了中央服务器中转, 由于网络木桶效应存在, 即使你两侧的网络速度再高也没用, 整体的速度取决于这个链路中最低的一个设备网速而不是你两端的设备.
在这种拓扑下, 想提高速度只有一个办法: 加钱! 在不使用 “钞能力” 的情况下, 普遍免费的软件提供商不可能给予过多的资源来让用户白嫖, 而自己弄大带宽的中央服务器成本又过高.
本部分只做简述, 具体里面有大量细节和规则可能描述不准确, 细节部分推荐阅读 How NAT traversal works.
既然传统的星型拓扑有这么多问题, 那么有没有其他骚操作可以解决呢? 答案是有的, 简单来说就是利用 NAT 穿透原理. NAT 穿透简单理解如下: 在 A 设备主动向 B 设备发送流量后, 整个链路上的防火墙会短时间打开一个映射规则, 该规则允许 B 设备短暂的从这个路径上反向向 A 设备发送流量. 更通俗的讲大概就是所谓的: “顺着网线来打你”
搞清了这个规则以后, 我们就可以弄一台 “低配” 的中央服务器, 让中央服务器来帮助我们协商两边的设备谁先访问谁(或者说是访问规则); 两个设备一起无脑访问对方, 然后触发防火墙的 NAT 穿透规则(防火墙打开), 此后两个设备就可以不通过中央服务器源源不断的通讯了. 在这种架构下我们的设备其实就组成了一个非标准的网状拓扑:
在这种拓扑下, 两个设备之间的通讯速度已经不在取决于中央服务器, 而是直接取决于两端设备的带宽, 也就是说达到了设备网络带宽峰值. 当然 NAT 穿透也不是百分百能够成功的, 在复杂网络情况下有些防火墙不会按照预期工作或者说有更严格的限制; 比如 IP、端口、协议限制等等, 所以为了保证可靠性可以让中央服务器中转做后备方案, 即尽量尝试 NAT 穿透, 如果不行走中央服务器中继.
第一部分是为了方便读者理解一些新型内网穿透的大致基本原理, 现在回到本文重点: Tailscale
Tailscale 就是一种利用 NAT 穿透(aka: P2P 穿透)技术的 VPN 工具. Tailscale 客户端等是开源的, 不过遗憾的是中央控制服务器目前并不开源; Tailscale 目前也提供免费的额度给用户使用, 在 NAT 穿透成功的情况下也能保证满速运行.
不过一旦无法 NAT 穿透需要做中转时, Tailscale 官方的服务器由于众所周知的原因在国内访问速度很拉胯; 不过万幸的是开源社区大佬们搓了一个开源版本的中央控制服务器(Headscale), 也就是说: 我们可以自己搭建中央服务器啦, 完全 “自主可控” 啦.
以下命令假设安装系统为 Ubuntu 22.04, 其他系统请自行调整.
Headscale 是采用 Go 语言编写的, 所以只有一个二进制文件, 在 Github Releases 页面下载最新版本即可:
1 |
|
下载完成后为了安全性我们需要创建单独的用户和目录用于 Headscale 运行
1 |
|
为了保证 Headscale 能持久运行, 我们需要创建 SystemD 配置文件
1 |
|
安装完成以后我们需要在 /etc/headscale/config.yaml
中配置 Headscale 的启动配置, 以下为配置样例以及解释(仅列出重要配置):
1 |
|
可能很多人和我一样, 希望使用 ACME 自动证书, 又不想占用 80/443 端口, 又想通过负载均衡器负载, 配置又看的一头雾水; 所以这里详细说明一下 Headscale 证书相关配置和工作逻辑:
tls_letsencrypt_hostname
时一定会进行 ACME 申请tls_letsencrypt_hostname
时如果配置了 tls_cert_path
则使用自定义证书listen_addr
地址, 与 server_url
没半毛钱关系grpc_allow_insecure
才会监听 grpc 远程调用服务综上所述, 如果你想通过 Nginx、Caddy 反向代理 Headscale, 则你需要满足以下配置:
tls_letsencrypt_hostname
或留空, 防止 ACME 启动tls_cert_path
或留空, 防止加载自定义证书server_url
填写 Nginx 或 Caddy 被访问的 HTTPS 地址listen_addr
的 HTTP 地址Nginx 配置参考 官方 Wiki, Caddy 只需要一行 reverse_proxy headscale:8080
即可(地址自行替换).
至于 ACME 证书你可以通过使用 acme.sh
自动配置 Nginx 或者使用 Caddy 自动申请等方式, 这些已经与 Headscale 无关了, 不在本文探讨范围内.
请尽量不要将 ip_prefixes
配置为默认的 100.64.0.0/10
网段, 如果你有兴趣查询了该地址段, 那么你应该明白它叫 CGNAT; 很不幸的是例如 Aliyun 底层的 apt 源等都在这个范围内, 可能会有一些奇怪问题.
在处理完证书等配置后, 只需要愉快的启动一下即可:
1 |
|
再啰嗦一嘴, 如果你期望使用 Headscale ACME 自动申请证书, 你的关键配置应该像这样:
1 |
|
如果你期望使用自定义证书, 则你的关键配置应该像这样:
1 |
|
如果你期望使用负载均衡器, 那么你的关键配置应该像这样:
1 |
|
在使用负载均衡器配置时, 启动后会有一行警告日志, 忽略即可:
1 |
|
Compose 配置样例文件如下:
1 |
|
你需要在与 docker-compose.yaml
同级目录下创建 conf
目录用于存储配置文件; 具体配置请参考上面的配置详解等部分, 最后不要忘记你的 Compose 文件端口映射需要和配置文件保持一致.
对于客户端来说, Tailscale 提供了多个平台和发行版的预编译安装包, 并且部分客户端直接支持设置自定义的中央控制服务器.
Linux 用户目前只需要使用以下命令安装即可:
1 |
|
默认该脚本会检测相关的 Linux 系统发行版并使用对应的包管理器安装 Tailscale, 安装完成后使用以下命令启动:
1 |
|
关于选项设置:
--login-server
: 指定使用的中央服务器地址(必填)--advertise-routes
: 向中央服务器报告当前客户端处于哪个内网网段下, 便于中央服务器让同内网设备直接内网直连(可选的)或者将其他设备指定流量路由到当前内网(可选)--accept-routes
: 是否接受中央服务器下发的用于路由到其他客户端内网的路由规则(可选)--accept-dns
: 是否使用中央服务器下发的 DNS 相关配置(可选, 推荐关闭)启动完成后, tailscale
将会卡住, 并打印一个你的服务器访问地址; 浏览器访问该地址后将会得到一条命令:
注意: 浏览器上显示的命令需要在中央控制服务器执行(Headscale), NAMESAPCE
位置应该替换为一个具体的 Namespace, 可以使用以下命令创建 Namespace (名字随意)并让设备加入:
在 Headscale 服务器上执行命令成功后客户端命令行在稍等片刻便会执行完成, 此时该客户端已经被加入 Headscale 网络并分配了特定的内网 IP; 多个客户端加入后在 NAT 穿透成功时就可以互相 ping 通, 如果出现问题请阅读后面的调试细节, 只要能注册成功就算是成功了一半, 暂时不要慌.
MacOS 客户端安装目前有两种方式, 一种是使用标准的 AppStore 版本(好像还有一个可以直接下载的), 需要先设置服务器地址然后再启动 App:
首先访问你的 Headscale 地址 https://your.domain.com/apple
:
复制倒数第二行命令到命令行执行(可能需要 sudo 执行), 然后去 AppStore 搜索 Hailscale 安装并启动; 启动后会自动打开浏览器页面, 与 Linux 安装类似, 复制命令到 Headscale 服务器执行即可(Namespace 创建一次就行).
第二种方式也是比较推荐的方式, 直接编译客户端源码安装, 体验与 Linux 版本一致:
1 |
|
安装完成后同样通过 tailscale up
命令启动并注册即可, 具体请参考 Linux 客户端安装部分.
关于 Windows 客户端大致流程就是创建一个注册表, 然后同样安装官方 App 启动, 接着浏览器复制命令注册即可. 至于移动端本人没有需求, 所以暂未研究. Windows 具体的安装流程请访问 https://your.domain.com/windows
地址查看(基本与 MacOS AppStore 版本安装类似).
在上面的 Headscale 搭建完成并添加客户端后, 某些客户端可能无法联通; 这是由于网络复杂情况下导致了 NAT 穿透失败; 为此我们可以搭建一个中继服务器来进行传统的星型拓扑通信.
首先需要注意的是, 在需要搭建 DERP Server 的服务器上, 请先安装一个 Tailscale 客户端并注册到 Headscale; 这样做的目的是让搭建的 DERP Server 开启客户端认证, 否则你的 DERP Server 可以被任何人白嫖.
目前 Tailscale 官方并未提供 DERP Server 的安装包, 所以需要我们自行编译安装; 在编译之前请确保安装了最新版本的 Go 语言及其编译环境.
1 |
|
接下来创建一个 SystemD 配置:
1 |
|
最后使用以下命令启动 Derper Server 即可:
1 |
|
注意: 默认情况下 Derper Server 会监听在 :443
上, 同时会触发自动 ACME 申请证书. 关于证书逻辑如下:
-a
参数, 则默认监听 :443
:443
并且未指定 --certmode=manual
则会强制使用 --hostname
指定的域名进行 ACME 申请证书--certmode=manual
则会使用 --certmode
指定目录下的证书开启 HTTPS-a
为非 :443
端口, 且没有指定 --certmode=manual
则只监听 HTTP如果期望使用 ACME 自动申请只需要不增加 -a
选项即可(占用 443 端口), 如果期望通过负载均衡器负载, 则需要将 -a
选项指定到非 443 端口, 然后配置 Nginx、Caddy 等 LB 软件即可. 最后一点 stun
监听的是 UDP 端口, 请确保防火墙打开此端口.
在创建完 Derper 中继服务器后, 我们还需要配置 Headscale 来告诉所有客户端在必要时可以使用此中继节点进行通信; 为了达到这个目的, 我们需要在 Headscale 服务器上创建以下配置:
1 |
|
在创建好基本的 Derper Server 节点信息配置后, 我们需要调整主配置来让 Headscale 加载:
1 |
|
接下来重启 Headscale 并重启 client 上的 tailscale 即可看到中继节点:
1 |
|
到此中继节点搭建完成.
目前官方似乎也没有提供 Docker 镜像, 我自己通过 GitHub Action 编译了一个 Docker 镜像, 以下是使用此镜像的 Compose 文件样例:
1 |
|
该镜像默认开启了客户端验证, 所以请确保 /var/run/tailscale
内存在已加入 Headscale 成功的 tailscaled 实例的 sock 文件. 其他具体环境变量等参数配置请参考 Earthfile.
在调试中继节点或者不确定网络情况时, 可以使用一些 Tailscale 内置的命令来调试网络.
tailscale ping
命令可以用于测试 IP 连通性, 同时可以看到时如何连接目标节点的. 默认情况下 Ping 命令首先会使用 Derper 中继节点通信, 然后尝试 P2P 连接; 一旦 P2P 连接成功则自动停止 Ping:
1 |
|
由于其先走 Derper 的特性也可以用来测试 Derper 连通性.
通过 tailscale status
命令可以查看当前节点与其他对等节点的连接方式, 通过此命令可以查看到当前节点可连接的节点以及是否走了 Derper 中继:
1 |
|
有些情况下我们可以确认是当前主机的网络问题导致没法走 P2P 连接, 但是我们又想了解一下当前的网络环境; 此时可以使用 tailscale netcheck
命令来检测当前的网络环境, 此命令将会打印出详细的网络环境报告:
1 |
|
MacOS 下使用一些增强代理工具时, 如果安装 App Store 的官方图形化客户端, 则可能与这些软件冲突, 推荐使用纯命令行版本并添加进程规则匹配 tailscale
和 tailscaled
两个进程, 让它们始终走 DIRECT
规则即可.
在使用一些网络代理工具时, 网络工具会设置默认路由; 这可能导致 tailscaled
无法获取到默认路由接口, 然后进入死循环并把 CPU 吃满, 同时会与 Derper 服务器产生大量上传流量. 截止本文发布此问题已修复, 请使用 mian
分支编译安装, 具体见 ISSUE/5879.
Tailscale 默认使用 CGNAT(100.64.0.0/10
) 网段作为内部地址分配网段, 目前 Tailscale 仅允许自己的接口使用此网段, 不巧的是阿里云的 DNS、Apt 源等也采用此网段. 这会导致阿里云服务器安装客户端后 DNS、Apt 等不可用, 解决方案目前只能修改源码删除掉这两个 DROP 规则并重新编译.
大多数时候我们可能并不会在每个服务器上都安装 Tailscale 客户端, 通常只安装 2、3 台, 然后想通过这两三台转发该内网的所有流量. 此时你需要
--advertise-routes=192.168.1.0/24
来告诉 Headscale 服务器 “我这个节点可以转发这些地址的路由”--accept-routes=true
选项来声明 “我接受外部其他节点发布的路由”以上两个选项配置后, 只需要 Headscale 服务器上使用 headscale node route enable -a -i XX(ID)
开启即可. 开启后目标节点(ID)的路由就会发布到接受外部路由的所有节点, 想要关闭的话去掉 -a
即可.
以上也只是我个人遇到的一些问题, 如果有其他问题推荐先搜索然后查看 ISSUE, 最后不行可以看看源码. 目前来说 Tailscale 很多选项很模糊, 可能需要阅读源码以后才能知道到底应该怎么做.
]]>首先要明确的是, 作为了一个每天在 Linux Server 上 rm -rf
的人来说, 如果想在 Mac 上使用 Docker, 最舒服的也是兼容所有 docker cli 命令行操作即可; 至于图形化的界面完全不需要, 我们并不指望图形化界面能比敲命令快到哪里去, 也不指望图形化界面变为主力; 所以本篇文章的核心目标:
-v
挂载支持首先是我们最熟悉的 Docker Desktop, 安装包奇大无比, UI 卡成翔, 启动速度更不用提而且还时不时的卡死, 所以 Docker Desktop 是完全不考虑的; 那么剩下几种方案类型如下:
先说结论: Lima YES! VM 虚拟机方案要花钱且难受, Colima 暂且不稳定. Lima 方案直接看第五节.
目前在 M1 上, 唯一可用或者说堪用的虚拟机当属 Parallels Desktop, 至于其他的 VBox、VMware 目前还不成熟; 如果纯 qemu 有点过于硬核(愿意自己封装脚本的当我没说); 对于 Parallels Desktop 来说, 我们需要购买开发版本的 License, 因为我们需要借助 prlctl
来实现一些自动化 , 一年好几百… 经过测试这种方案也有一定可行性:
~
rw 挂载到虚拟机里基于这个方案我个人尝试过, 曾经写过一个 PD 的小工具来辅助完成挂载动作. 但是这种工具有一些明显的缺点:
Colima 号称是专门为了解决 Mac 平台容器化工具链的, 但是实际测试发现目前 Colima 还不算稳定, 有时可能会有一些小问题; 当然 Colima 最大的问题是: 可自定义化程度不高, 底层基于 Lima. Colima 具体的使用方式啥的这里暂不详细描述, 目前还不稳定不太推荐.
Lima 目前是基于 QEMU 的自动化 VM 方案, 当前由于其出色设计, 借助 Cloud Init 可以在很多阶段帮助我们完成 hook; 所以不论是装个 Docker 还是 k8s, 亦或是弄个其他的东西都很方便; 而且很多方案比如 docker 官方都有相关样例, 我们可以直接照抄外加做点自定义.
Lima 在 Mac 下安装相对简单, 以下命令将安装 master 分支版本.
1 |
|
在正常情况下, 安装 Lima 会附带安装 QEMU, 如果本机已经安装 QEMU, 可能需要执行以下命令将 QEMU 升级到 7.0:
1 |
|
为了使用 docker, 还需要通过 brew 安装一下 docker cli:
1 |
|
默认情况下 Lima 安装完成后会生成一个 lima
的快捷命令, 目前不太推荐使用, 原因是看起来方便一点但是没法控制太多参数, 所以仍然建议使用标准的 limactl
命令进行操作. limactl 使用方式如下:
1 |
|
Lima 通过读取一个 yaml 配置描述文件来决定如何创建一个虚拟机, 该文件基本结构如下:
1 |
|
limactl 命令提供了一个 start
子命令用于启动一个虚拟机, 子命令接受一个参数, 这个参数形式不同会产生不同的行为:
以上面我自己定义的 docker 配置文件为例, 我们直接启动这个配置既可以创建一个 docker 虚拟机:
1 |
|
启动后会提示是否编辑然后再启动, 这是为了使用同一个配置来启动多个 vm 使用的, 所以不编辑直接启动即可:
稍等片刻后虚拟机将启动成功:
启动完成后, 执行最下面打印出的两条命令, 即可在宿主机上完整的使用 docker
. 其本质上利用 docker context 功能, 然后通过将虚拟机中的 sock 文件挂载到宿主机, 并配置 docker context 来实现无缝使用 docker 命令.
某些情况下, 我们需要定制一些 VM 里的配置, 在定制时主要需要调整配置文件的 provision
部分; 在该部分中, 如果 mode
被定义为 system
则会以 root 用户执行相关命令, 否则以普通用户来执行命令. 需要注意的是, 我们定义的脚本需要具有幂等性, 因为脚本在每次都会执行一次, 所以一般对于可能造成数据擦除动作的命令都要写好判断逻辑, 避免重复执行.
关于文件挂载, 这里推荐使用 9p
类型, 未来 lima 将完全切换到该挂载方式; 同时经过测试目前仅有 9p
挂载模式下, 本地目录 rw 映射到虚拟机时不会出现权限问题, sshfs 方式挂载如果遇到 chown
之类的命令会造成权限错误, 可能导致容器启动失败(例如 mysql).
在测试虚拟机配置过程中, 可以直接使用 limactl delete -f xxxx
来强制删除目标虚拟机, 然后重新启动即可; 虚拟机名称默认与 yaml 文件名相同, 可使用 limactl ls
命令查看.
在上面我的 docker 配置样例中, 每次虚拟机启动完成后会自动安装 binfmt:
1 |
|
这样能保证无论 Lima 虚拟机原始架构是什么, 都能运行其他平台的 docker 镜像; 典型的例如某些 openjdk8 镜像只有 amd64 的版本, 但是在 lima 虚拟机为 aarch64 的情况下仍然可以使用.
除了这种 “速度较快” 的跨架构运行方式, lima 还支持直接在 VM 中定义架构, 这样在 qemu 启动时则会直接从 VM 系统层模拟目标架构; 这种方式的好处是对目标架构兼容性很好, 但是运行速度会更慢. 调整 VM 架构只需要修改 arch
配置即可(注意, 目标架构的镜像一定要配置好):
1 |
|
目前整体来看, Docker Desktop 在 mac 上基本上是很难用的, Colima 现在还不太成熟, 适合轻度使用 docker 的用户; 而重度使用 docker 并且有定制化需求的用户还是推荐 Lima 虚拟机; 同时 Lima 也支持很多操作系统, 官方有大量的样例模版(包括 k8s、k3s、podman 等), 非常适合重度容器使用者.
]]>Taskfile 通过 yaml 来描述各种执行任务, 其核心采用 go 编写; 相较于 Makefile 的 tab 分割和 bash 结合语法 Taskfile 显得更加现代化和易于使用(虽然会变成 yaml 工程师). Taskfile 内置了动态变量、操作系统等环境变量识别等高级功能都更贴合现代化的 Coding 方式.
总体来说如果你是一个对 Makefile 不太熟悉的人, 又期望通过类似 Makefile 的工具完成一些批量任务, 那么相对于 Makefile 来说 Taskfile 会更加便于入门, 学习曲线更低且速度也足够快.
对于 mac 用户来说官方提供了 brew 安装方式:
1 |
|
对于 Linux 用户, 官方提供了部分 Linux 发行版的安装包, 但由于其只有一个二进制文件, 所以官方也提供了快速安装脚本:
1 |
|
如果本地已经有了 Go 语言开发环境也可以直接通过 go 命令安装:
1 |
|
安装完成后, 只需要编写一个 Taskfile.yml
的 yaml 文件, 然后就可以通过 task
命令运行相应的任务:
1 |
|
如果需要设置默认执行任务, 只需要创建一个名字为 default
的任务即可:
1 |
|
Taskfile 支持引用三种环境变量:
如果需要引用 Shell 内的环境变量只需要使用 $变量名
方式直接引用即可:
1 |
|
同样在 Taskfile 内也可以定义环境变量:
1 |
|
除了这种直接引用变量的方式, Taskfile 也支持类似 docker-compose 一样读取 env 文件来加载环境变量; Taskfile 会默认加载同级目录下的 .env
文件, 也可以在 Taskfile 内通过 dotenv
命令来配置特定文件:
1 |
|
除了标准的环境变量以外, 在 Taskfile 中还内置了一种使用更加广泛的增强变量 vars
; 该变量模式可以通过 go 的模版引擎进行读取(插值引用), 且具有环境变量不具备的特殊特性. 以下为 vars 变量的示例:
1 |
|
除了上面与环境变量类似的使用以外, vars 增强变量还支持动态定义; 常见的场景, 比如我们想每次 task 执行时都获取当前的 git commit id, 此时可以使用 vars 的动态定义特性:
1 |
|
vars 变量还内置了一些特殊的预定义变量, 例如 {{.TASK}}
变量永远表示当前的任务名称、{{.CLI_ARGS}}
可以引用命令行输入等.
1 |
|
此时如果执行 task yarn -- install
, 那么 {{.CLI_ARGS}}
值将会变成 install
从而执行 yarn install
命令.
除此之外, vars 变量还具备一些其他特性, 比如跨任务引用时可进行覆盖传递等, 这些特性将会在后面介绍.
Taskfile 内定义的 task 默认在当前目录下执行, 如果期望在其他目录执行, 无需手动编写 cd
等命令, 可以直接通过配置 dir
参数来设置执行目录:
1 |
|
在 CI 等环境的使用中, 我们常常需要定义任务的执行顺序和依赖关系; Taskfile 中通过 deps
配置来提供任务依赖关系的支持:
1 |
|
当我们在 Taskfile 中定义了多个任务时, 很可能一些任务具有一定的相似性, 此时我们可以通过任务互相调用和 vars 变量动态覆盖的方式来定义模版 Task:
1 |
|
Taskfile 支持通过 includes
关键字来引入其他 Taskfile, 从而方便 Taskfile 的结构化处理.
需要注意的是, 由于引入的文件中可能会包含多特 task, 所以在使用时需要对引入的文件进行命名, 且通过命名引用目标 task:
1 |
|
在引入其他 Taskfile 时, 默认情况下会在当前主 Taskfile 目录下执行命令, 我们同样可以通过 dir
参数来控制引入的 Taskfile 内的 task 在特定目录下执行:
1 |
|
熟悉 go 语言的同学应该知道, go 里面有个很方便的关键字 defer
; 该指令用于定义在最终代码收尾时要执行的动作, 常见的比如资源清理等. Taskfile 中同样支持了该指令, 可以方便我们在任务执行期间完成一些清理操作:
1 |
|
当然, defer 指令除了直接写命令以外, 还可以引用其他 task 完成清理:
1 |
|
在某些时候, 一些任务我们可能期望进行缓存处理, 比如说已经下载好了文件就不要重复运行下载; 针对于这种需求, Taskfile 允许我们定义源文件和生成的文件, 通过这组文件的 hash 值来确定是否需要执行该任务:
1 |
|
从上图中可以看到, 当首次执行任务时会生成 .task
目录, 该目录包含文件的 hash 值; 当重复执行任务时, 如果 hash 值不改变则真实任务不会真正执行. Taskfile 默认有两种文件检测的方式: checksum
、timestamp
, checksum
执行文件的 hash 检测(默认), 该模式只需要定义 sources
配置; timestamp
执行文件的时间戳检测, 该模式需要同时定义 sources
和 generates
配置.
1 |
|
除了内置的两种检测模式外, 我们还可以通过 status
配置来定义自己的检测命令, 如果命令执行结果为 0, 则认为文件是最新的, 不需要执行任务:
1 |
|
上面的输出检测用于检测任务生成的文件结果等, 在某些情况下我们可能期望在运行任务之前来判断某个条件, 在完全不执行的情况下确定任务是否需要运行; 此时我们可以使用 preconditions
配置指令:
1 |
|
在上面变量环节中已经展示了一部分模版引擎的使用, 实际上 Taskfile 内集成了 slim-sprig 库, 该库中提供了一些比较便利的方法, 这些方法都可以在模版引擎内使用:
1 |
|
关于这些方法和模版引擎的使用具体请参考 Go Template 相关文档以及 slim-sprig 文档.
有些任务命令可能需要交互式终端来执行, 此时可以为 task 设置 interactive
选项; 当 interactive
设置为 true
时, task 在运行时可以打开交互式终端:
1 |
|
更多关于 Taskfile 的细节使用请阅读其官方文档, 本文限于篇幅不在过多阐述.
]]>目前对于 WebP 和 AVIF 格式支持大致有两种方案, 一种是动态转换, 另一种是静态转换.
懒得放代码了, 自己搓到一半放弃了.
对于 WebP 格式来说, 在 Caddy2 上动态转换比较简单, 基本上就是添加一个插件即可; 例如 caddy-webp 这个插件. 有关于 Caddy 的插件开发可以参考官方的 Extending Caddy 文档.
动态转换的核心思想是在接收到请求后, 判断请求中的 Accept
头, 如果包含 image/webp
则说明浏览器可以识别 WebP 格式图片(AVIF 同理); 此时可以将请求先转发给后续的 HTTP 处理逻辑, 待返回响应后将其暂时 Cache 住, 然后执行转换逻辑, 改变其格式后重新设置 Content-Type
头然后把新的格式数据返回给前端.
针对这种操作可以在负载均衡器层(例如 Caddy2 Plugin)完成, 也可以通过单独的中间件完成; 但无论哪种其涉及到一些缺陷(可优化):
针对这些情况, 个人认为在生产环境资源充足的情况下完全可以解决, 核心思想还是一个: 缓存为王; 想办法在第一次转换后将其存储到缓存位置, 避免每次都转换; 如果资源足够甚至可以考虑内存级的缓存方案配合 LRU 等失效策略.
经过一波搓代码从入门到放弃, 最终我选择了目前我这个小破站能承担得起的方案; 即先在本地执行转换然后直接扔到服务器上; 由于网站本身就是静态站点, 所以可以在 Caddyfile 中基于动态转换的套路处理请求头返回不同文件.
本地转换目前采用 optimizt 这个工具完成:
1 |
|
这样在我的图片目录下就会生成同名的 WebP 和 AVIF 格式图片
最后只需要在 Caddyfile 中增加以下请求头匹配的配置即可:
1 |
|
动态转换比价适合商业化模式, 在资源支撑足够的情况下可以延迟随机、预热、高性能缓存等方式配合起来将图片透明转换完成, 对用户无感且友好. 小破站资源匮乏等情况比较适合静态转换完后逻辑匹配规则返回不同物理文件. 说实话我想弄成高大上的动态转换写点代码的, 但是弄到一半发现全是问题又没资源解决, 只能在打打嘴炮了.
]]>不想看原理的可以直接使用 TPClash, 想仔细看原理的可以看 TPClash wiki.
本文中内网 CIDR 为 192.168.0.0/16
, 即所有地址段规则、配置都是针对当前内网 CIDR 进行处理的; clash fake-ip 的 CIDR 为 198.18.0.0/16
, 请不要写错成 192
, 这是 198
(也不要问我为什么强调).
本文所采用的透明代理方式不依赖于 TUN, 所有是否是增强版本不重要, 如果可以请尽量使用最新版本.
1 |
|
创建专用的 clash 用户:
1 |
|
编写 Systemd 配置文件:
1 |
|
本文中 Clash 配置文件、脚本等统一存放到 /etc/clash
目录中, 针对于 Clash 配置文件, 着重说明重点配置, 完整配置请从官方 Wiki 复制: https://github.com/Dreamacro/clash/wiki/configuration#all-configuration-options
端口配置请尽量保持默认, 如果需要调整端口, 请同步修改后面相关脚本中的端口(TProxy):
1 |
|
Clash 配置中请开启 DNS, 并使用 fake-ip
模式, 样例配置如下:
1 |
|
为了保证防火墙规则不被破坏, 本文采用脚本暴力操作, 如果宿主机有其他 iptables 控制程序, 则推荐手动执行并通过 iptables-persistent
等工具进行持久化;
/etc/clash/iptables.sh
: 负责启动时添加 iptables 规则
1 |
|
/etc/clash/clean.sh
: 负责启动前/停止后清理 iptables 规则(暴力清理)
1 |
|
所有配置编写完成后, 其目录结构如下:
1 |
|
最后需要修复 /etc/clash
目录权限, 因为 Clash 启动后会写入其他文件:
1 |
|
如果所有配置和文件安装没问题的话, 可以直接通过 Systemd 启动:
1 |
|
如果启动成功, 那么此时内网设备将网关设置到当前 Clash 所在机器即可完成透明代理; 如果 Clash 机器足够稳定, 也可以一步到位将内网路由器的 DHCP 设置中下发的网关直接填写为 Clash 机器 IP(Clash 机器需要使用静态 IP).
]]>最近比较忙,也遇到了一些比较烦的事情。同样也在总结和分析在当前工作过程中的一些有意思的事情,今天决定好好写(水)一篇高质量文章,我愿称之为 “锅理学” 或 “锅链”。
“锅” 这个东西从人类文明诞生之初就一直存在,而随着人类文明发展,分工合作的这种社会关系日趋紧密;小到个人大到团队甚至是国家之间,”锅” 作为一个推卸责任和无耻的下三路手段被疯狂得在合作关系中传递。
“锅” 的产生核心在于问题的产生,不论任何环境下, 只要存在合作关系且由于某种原因出现了问题,那么 “锅” 就会被 “锻锅者” 们创造出来; 一但 “锻锅者” 们拿起手中的锤子,创造出了 “锅”, “锅” 就可以利用人性的阴暗面为能量,本着 “死道友不死贫道” 的规则开始进行流动 ;但这个流动过程并非永恒的,根据能量守恒原理:
“锅” 在流动过程中需要能量支撑,当遇到能量不足或出现异常的干扰着时,”锅” 会停留在某一点;处于该点的人我们称之为 “锅坦森” ,如果该点具有更多的 “锅坦森” 或该点作为一个社会组织构成,则 “锅” 会产生分裂,俗称 “锅裂变” 或 “锅坦森均衡效应”;在极端情况下,如果 “锅坦森” 比较多,且 “锅坦森” 之间的能量极度不均衡,”锅” 会产生聚合,俗称 “锅聚变” 或 “锅坦森凝态效应”。
“锅” 在流转规程中大体上保持顺次传递原则, 即在合作关系中,”锅” 大体上只会从供应关系的上游向下游传递;在普通的互联网公司内部大致如下:
你可能已经注意到,在传递规则的图中 Cloud Provider 的箭头反向指向了 Ops;这是因为虽然在整体上 “锅” 顺次传递, 但是在特殊情况下会产生回弹或异常终止。 这种回弹通常出以下情况(规则)中:
在以上这些情况出现下,”锅” 可能会产生回弹效应,从而违反 “传统锅学” 定律。
“锅” 一般处于一种不稳定状态,随着外力作用,”锅” 会随时湮灭或变异;但是由于其存在轨迹,所以我们依然能进行追溯, “锅” 在流转过程中极度不稳定,它通常表现为从某一个节点流转后改变了基本的物理性质(比如接口不稳定变为基础架构不稳定、基础架构不稳定变为宇宙射线导致的硬件故障 byte 翻转等), “锅” 的这种行为和现象在因果律支撑的人类世界很罕见,但又似乎合情合理;这也是 “锅” 最让人着迷的地方: 它不在三界之内,超脱五行之中,浑然天成而又无迹可寻。
虽然 “锅” 大部分时间处于不稳定态,但是如果施加外力作用,例如增加 “领导强力”;则 “锅” 会被束缚并保持稳定态,这种稳定态的 “锅” 一般其 “锅理学” 性质极其稳定, 它会以超光速流动(跨越时间)并跨越现实物理学,从而产生 “人在家中坐,锅从天上来” 的有趣现象。
不稳定态的 “锅” 由于其流转位置的作用力原因,常常会产生两种 “锅理学” 现象,即 “锅裂变” 和 “锅聚变”。
“锅裂变” 的产生是由于外力作用降低,无法再束缚 “锅”,同时过多的 “锅坦森” 的出现也会产生类似拉扯力一样的作用力作用在 “锅” 本身;此时一个大的 “锅” 会逐渐分裂成一些小的 “锅”,这些小 “锅” 会最终附着到 “锅坦森” 身上并发光发亮,最终会促成 “锅坦森” 的经验加成并完成升级(“Big 锅坦森”)。
“锅聚变” 与 “锅裂变” 恰恰相反,在 “锅” 流转到某一节点时,外力作用突然加强(比如 “领导说这是某个人爱写 BUG”),此时 “锅” 会瞬间产生 “锅聚变”,以极其迅速且稳定的状态落到指定 “锅坦森” 身上;此过程也会导致 “锅坦森” 发光发亮; 但如果无法促成 “锅坦森” 升级,则可能会直接摧毁 “锅坦森”,最终会表现为我们常见的 “明天离职” 现象。
“锅” 的变异通常非常频繁且复杂,上面已经描述了几种情况,但是经过我的观察,目前已知的变异途径如下:
除了这些情况以外,我本着 “刨根问底、没事胡扯” 的原则,对 “锅” 进行了著名的双缝实验;实验结果表明 如果 “锅” 在流转过程中被进行观测,则 “锅” 会由于观察者效应进行重组并变为某种稳定态。
在本世纪初 “锻锅者” 群体逐渐涌现在人们的视野中,随着 “锻锅者” 的增加和竞争日益激烈,”锻锅者” 的锻造技术得到了飞速发展,现已基本形成稳定且可持续发展的产业链;甚至在某些企业内已经形成了 “锻锅者” 联盟和管理委员会,有专门的 “锅贤者” 负责培训并教导和监督新出现的 “锻锅者”;例如以前的一个简单的锻造手法为 “2.22 机器重启导致的”,现在已经演变为 “基础架构不稳定”;在这种高精度低成本的带动下,被 “锻锅者” 锻造出的 “锅” 已经从以前的单体不稳定攻击变为群体、范围性的群体攻击和精确目标打击; 所有目前 21 世纪的 “锻锅者” 锻造的 “锅” 通常会造成 “锅坦森” 们的非战斗性减员。
当然有了 “锻锅者” 来锻造 “锅” 必然会有 “锅坦森” 来接收 “锅”,这两者一般是相辅相成的;不过随着 “锅” 群伤效果的增加,”锅坦森” 群体也逐渐并不满足于单纯的接收;某些 “锅坦森” 由于其长期接收各种 “锅”,开始主动学习一些 “锻锅者” 的技能,从而逐渐进化成了创世者;这些创世者并不会单纯的接收普通的 “锅”,普通的 “锅” 在他们眼里所有运行轨迹清晰可见,这种 “锅” 失去了可以违反因果律的美感,所以创世者们会通过施加外力重新改变其轨迹; 创世者们只喜欢那些无法被语言形容的 “锅”,这些 “锅” 神秘且强大,它跨越时间和历史长河,甚至出现离职再入职,依然可观测可接收的大自然奇迹。
开局一张图,功能全靠吹。
Earthly 是一个更加高级的 Docker 镜像构建工具,Earthly 通过自己定义的 Earthfile 来代替传统的 Dockerfile 完成镜像构建;Earthfile 就如同 Earthly 官方所描述:
Makefile + Dockerfile = Earthfile
在使用 Earthly 进行构建镜像时目前强依赖于 buildkit,Earthly 通过 buildkit 支持了一些 Dockerfile 的扩展语法,同时将 Dockerfile 与 Makefile 整合,使得多平台构建和代码化 Dockerfile 变得更加简单;使用 Earthly 可以更加方便的完成 Dockerfile 的代码复用以及更加友好的 CI 自动集成。
Earthly 目前依赖于 Docker 和 Git,所以安装 Earthly 前请确保机器已经安装了 Docker 和 Git。
Earthly 采用 Go 编写,所以主要就一个二进制文件,Linux 下安装可以直接参考官方的安装脚本:
1 |
|
安装完成后 Earthly 将会启动一个 buildkitd 容器: earthly-buildkitd
。
目前 Earthly 官方支持 VS Code、VIM 以及 Sublime Text 三种编辑器的语法高亮,具体如何安装请参考 官方文档。
本示例源于官方 Basic 教程,以下示例以编译 Go 项目为样例:
首先创建一个任意名称的目录,目录中存在项目源码文件以及一个 Earthfile
文件;
main.go
1 |
|
Earthfile
1 |
|
有了 Earthfile
以后我们就可以使用 Earthly
将其打包为镜像;
1 |
|
构建完成后我们就可以直接从 docker 的 images 列表中查看刚刚构建的镜像,并运行:
Earthfile 中包含类似 Makefile 一样的 target
,不同的 target
之间还可以通过特定语法进行引用,每个 target
都可以被单独执行,执行过程中 earthly 会自动解析这些依赖关系。
这种多阶段构建时语法很弹性,我们可以在每个阶段运行独立的命令以及使用不同的基础镜像;从快速开始中可以看到,我们始终使用了一个基础镜像(golang:1.17-alpine
),对于 Go 这种编译后自带运行时不依赖其语言 SDK 的应用,我们事实上可以将 “发布物” 仅放在简单的运行时系统镜像内,从而减少最终镜像体积:
由于使用了多个 target,所以我们可以单独的运行 build
这个 target 来验证我们的编译流程,这种多 target 的设计方便我们构建应用时对编译、打包步骤的细化拆分,同时也方便我们进行单独的验证。 例如我们单独执行 build
这个 target 来验证我们的编译流程是否正确:
在其他阶段验证完成后,我们可以直接运行最终的 target,earthly 会自动识别到这种依赖关系从而自动运行其依赖的 target:
SAVE 指令是 Earthly 自己的一个扩展指令,实际上分为 SAVE ARTIFACT
和 SAVE IMAGE
;其中 SAVE ARTIFACT
指令格式如下:
1 |
|
SAVE ARTIFACT
指令用于将文件或目录从 build 运行时环境保存到 target 的 artifact 环境;当保存到 artifact 环境后,可以通过 COPY
等命令在其他位置进行引用,类似于 Dockerfile 的 COPY --from...
语法;不同的是 SAVE ARTIFACT
支持 AS LOCAL <local-path>
附加参数,一但指定此参数后,earthly 会同时将文件或目录在宿主机复制一份,一般用于调试等目的。SAVE ARTIFACT
命令在上面的样例中已经展示了,在运行完 earthly +build
命令后实际上会在本地看到被 SAVE 出来的 ARTIFACT:
而另一个 SAVE IMAGE
指令则主要用于将当前的 build 环境 SAVE 为一个 IMAGE,如果指定了 --push
选项,同时在执行 earthly +target
命令时也加入 --push
选项,该镜像将会自动被推送到目标 Registry 上。SAVE IMAGE
指令格式如下:
1 |
|
GIT CLONE
指令用于将指定 git 仓库 clone 到 build 环境中;与 RUN git clone...
命令不同的是,**GIT CLONE
通过宿主机的 git 命令运行,它不依赖于容器内的 git 命令,同时还可以直接为 earthly 配置 git 认证,从而避免将这些安全信息泄漏到 build 环境中;** 关于如何配置 earthly 的 git 认证请参考 官方文档;下面是 GIT CLONE
指令的样例:
COPY
指令与标准的 Dockerfile COPY 指令类似,除了支持 Dockerfile 标准的 COPY 功能以外,earthly 中的 COPY
指令可以引用其他 target 环节产生的 artifact,在引用时会自动声明依赖关系;即当在 B
target 中存在 COPY +A/xxxxx /path/to/copy
类似的指令时,如果只单纯的执行 earthly +B
,那么 earthly 根据依赖分析会得出在 COPY 之前需要执行 target A。COPY
指令的语法格式如下:
1 |
|
RUN
指令在标准使用上与 Dockerfile 里保持一致,除此之外增加了更多的扩展选项,其指令格式如下:
1 |
|
其中 --privileged
选项允许运行的命令使用 privileged capabilities
,但是需要 earthly 在运行 target 时增加 --allow-privileged
选项;--interactive / --interactive-keep
选项用于交互式执行一些命令,在完成交互后 build 继续进行,在交互过程中进行的操作都会被持久化到 镜像中:
限于篇幅原因,其他的具体指令请查阅官方文档 Earthfile reference。
UDCs 全称 “User-defined commands”,即用户定义指令;通过 UDCs 我们可以将 Earthfile 中特定的命令剥离出来,从而实现更加通用和统一的代码复用;下面是一个定义 UDCs 指令的样例:
1 |
|
UDCs 不光可以定义在一个 Earthfile 中,UDCs 可以跨文件、跨目录引用:
有了 UDCs 以后,我们可以通过这种方式将对基础镜像的版本统一控制、对特殊镜像的通用处理等操作全部抽象出来,然后每个 Earthfile 根据需要进行引用;关于 UDCs 的使用样例可以参考我的 autobuild 项目,其中的 udcs 目录定义了大量的通用 UDCs,这些 UDCs 被其他目标镜的 Earthfile 批量引用。
在以前使用 Dockerfile 的时候,我们需要自己配置然后开启 buildkit 来实现多平台构建;在配置过程中可能会很繁琐,现在使用 earthly 可以默认帮我们实现多平台的交叉编译,我们需要做的仅仅是在 Earthfile 中声明需要支持哪些平台而已:
以上 Earthfile 在执行 earthly --push +all
构建时,将会自动构建四个平台的镜像,并保持单个 tag,同时由于使用了 --push
选项还会自动推送到 Docker Hub 上:
Earthly 弥补了 Dockerfile 的很多不足,解决了很多痛点问题;但同样可能需要一些学习成本,但是如果已经熟悉了 Dockerfile 其实学习成本不高;所以目前还是比较推荐将 Dockerfile 切换为 Earthfile 进行统一和版本化管理的。本文由于篇幅所限(懒)很多地方没有讲,比如共享缓存等,所以关于 Earthly 更多的详细使用等最好还是仔细阅读一下官方文档。
]]>在一开始接触 oh-my-zsh 的时候说实话只是因为它的主题非常漂亮,例如 powerlevel10k 主题;这对于一个常年在终端上锻炼左右手的人来说确实是非常 “Sexy”。后来随着逐渐深度使用,oh-my-zsh 深度集成的这种一体化插件方案等确实带来了极大便利;例如简单的命令行搜索、git、docker、kuebctl 等各种插件的快速提示等。
但是当终端使用久了以后突然发现,其实像 powerlevel10k 这种花哨的终端主题并不适合我;当逐渐切换回简洁的一些主题,并在 Kubernetes 等大项目的目录下左右横跳时,oh-my-zsh 极慢的响应速度开始展露弊端;仅仅在 Kubernetes 的 Git 仓库目录下,按住回车键终端都能卡出动画…
所以当忍受不了终端这种拉垮的响应速度时,我感觉是时候换一个了。
Prezto 官方仓库的介绍很简单,简单到只说 Prezto 是一个 zsh 配置框架,集成了一些主题、插件等。但是如果细说的话,其实 Prezto 最早应该是 oh-my-zsh 的 fork 版本,然后 Prezto 被一点点重写,现在已经基本看不到 oh-my-zsh 的影子了。不过唯一可以肯定的是,性能以及易用性上比 oh-my-zsh 好得多。
Prezto has been rewritten by the author who wanted to achieve a good zsh setup by ensuring all the scripts are making use of zsh syntax. It has a few more steps to install but should only take a few minutes extra. —- John Stevenson
Prezto 安装按照仓库文档的方法安装即可:
首先确定已经安装了 zsh,如果没有安装则需要通过相应系统的包管理器等工具进行安装:
1 |
|
在仓库进行克隆时一般分为两种情况,一种默认克隆到 "${ZDOTDIR:-$HOME}/.zprezto"
目录(标准安装):
1 |
|
另一种高级用户可能使用 XDG_CONFIG_HOME
配置:
1 |
|
Prezto 的安装方式比较方便定制化,在主仓库克隆完成后,只需要将相关的初始化加载配置软连接到 $HOME
目录即可:
1 |
|
不过需要注意的是上面的命令在某些 shell 脚本里直接写可能会有兼容性问题,在这种情况下可以直接通过命令进行简单处理:
1 |
|
至此,Prezto 算是安装完成,重新登录 shell 即可看到效果。
默认情况下 Prezto 使用 sorin 这个主题,如果对默认主题不满意可以通过 prompt
命令切换:
1 |
|
默认情况下 Prezto 在执行 grep 时会对结果进行高亮处理,在某些终端主题上可能会很影响观感:
grep 高亮是在 utility
插件中被开启的,可以通过在 ~/.zpreztorc
中增加以下配置关闭:
1 |
|
Prezto 通过 syntax-highlighting 插件提供了各种语法高亮配置,通过解开以下配置的注释开启更多的自动高亮:
1 |
|
需要注意的是,默认 root 高亮开启后,root 用户所有执行命令都会高亮,这样可能在主题配色上导致看不清输入的命令,可以简单的移除 root 高亮配置即可。
在 syntax-highlighting
插件中启用了 pattern
高亮后,可以通过以下配置设置一些自定义的命令高亮配置,例如 rm -rf
等:
1 |
|
oh-my-zsh 通过上下箭头按键来快速搜索历史命令是一个非常实用的功能,在切换到 Perzto 后会发现上下箭头的搜索变成了全命令的模糊匹配;例如输入 vim
然后上下翻页会匹配到位于命令中间带有 vim
字样的历史命令:
解决这个问题需要将 history-substring-search
插件依赖的 zsh-history-substring-search
切换到 master 分支并增加 HISTORY_SUBSTRING_SEARCH_PREFIXED
变量配置:
1 |
|
同时历史搜索里还有一个问题是同样的命令如果出现多次会被多次匹配,解决这个问题需要增加以下变量:
1 |
|
更多可以使用的插件请参考 modules 目录下每个插件的文档,以及如何开启和配置。
为了方便自己使用,我在我的 init 项目下创建了快速初始化脚本,以上这些调整将自动完成:
1 |
|
这几天把公司测试环境 Nginx 切换到了 Caddy,在实际切换过程中还是有一点小问题,但是目前感觉良好,这里记录一些细节。
大部分情况我们的生产环境使用一个域名,为了保证隔离性我们会在测试环境采用另一个域名(偷偷透露一下,测试环境买 *.link
域名,国内能备案还贼便宜);然而我们不太舍得掏钱去给测试域名再买个证书,所以一直 ACME 大法。
众所周知这个玩意的证书 3 个月需要续签一次,脚本式续签然后 nginx reload 有时候还不太靠谱,总之内部环境复杂下脚本式操作还是有点风险,所以最后决定 Caddy 一把梭一劳永逸了。
在某个站点中我们采用了 Nginx 判断 User-Agent 来处理访问到底是移动端还是桌面端,说实话我比较讨厌这种骚这种东西:
1 |
|
一开始通过查找 Caddy 文档发现 Caddy 也是支持 map 的:
1 |
|
在实际配置时发现其实这个问题只需要用自定义规则匹配器判断一下是不是移动端即可:
1 |
|
在后续编写匹配规则时发现 Caddy 的匹配规则确实是非常强大,在官方的 Request Matchers 文档页面上可以找到基本上满足所有需求的匹配器,从请求头到请求方法、协议、请求路径,从标准匹配到通配符、正则匹配基本上样样俱全,甚至支持代码式的 CEL (Common Expression Language) 表达式匹配;多个匹配还可以自定义命名作为业务相关的匹配器使用。
在 Nginx 中 rewrite 指令是多种行为的,比如可以进行 URL 隐式改写,也可以返回 301、307 等重定向代码;但是在 Caddy 中这两种行为被划分为两个指令:
针对于地址的隐式重写 rewrite 指令其语法规则如下:
1 |
|
匹配器就是全局标准的匹配器定义,可以使用内置的,也可以组合内置匹配器为自定义匹配器,这个匹配器比 Nginx 强大太多;to
中分为三种情况:
rewrite /abc /bcd
:这种情况下,rewrite 根据 “匹配器” 确定匹配路径,然后完全替换为最后一个路径;最后面的路径可以使用 {path}
占位符引用原始路径。
rewrite /api ?a=b
:这种情况下,Caddy 以 ?
作为分隔符,如果 ?
后面有内容就意味着将请求参数替换为后面的请求参数;最后面的请求参数可以通过 {query}
引用原始请求参数。
rewrite /abc /bcd?{query}&custom=1
:这种情况下,Caddy 根据 “匹配器” 匹配会即替换请求路径也替换请求参数,当然两个占位符也都是可用的。
需要注意的是: rewrite
只做重写,不会中断请求链,这意味着最终返回结果根据后续的请求匹配来决定。
redir 用于向客户端声明显式的重定向,即返回特定重定向状态码,其语法如下:
1 |
|
匹配器就不说了,全都一样;**<to>
这个参数会作为 Location
头部值返回,其中可以使用占位符引用原始变量:**
1 |
|
code
部分分为四种情况:
3xx
的自定义状态码temporary
: 返回 302 临时重定向permanent
: 返回 301 永久重定向html
: 使用 HTML 文档方式重定向例如将所有请求永久重定向到新站点:
1 |
|
这里面 HTML 方式是比较难理解的,这起源于一个规范,具体如下:
HTTP 协议中重定向机制是应该优先采用的创建重定向映射的方式,但是有时候 Web 开发者对于服务器没有控制权,或者无法对其进行配置。针对这些特定的应用情景,Web 开发者可以在精心制作的 HTML 页面的
部分添加一个 元素,并将其 http-equiv 属性的值设置为 refresh 。当显示页面的时候,浏览器会检测该元素,然后跳转到指定的页面。
在源码中如果使用了 html
重定向方式,Caddy 会返回一个 HTML 页面以满足上述方式的情况下让浏览器自行刷新:
1 |
|
uri
指令是一个特殊指令,它与 rewrite
类似,不同的是它用于对 URI 重写更加方便,其语法如下:
1 |
|
语法中第二个参数为一个动词,用来定义如何替换 URI:
strip_prefix
: 从路径中去除前缀strip_suffix
: 从路径中去除后缀replace
: 在整个 URI 路径中执行子替换(例如 /a/b/c/d
替换为 /a/1/2/d
)path_regexp
: 在路径中进行正则替换以下为一些样例:
1 |
|
其中在使用 replace
时最后面可以跟一个数字,代表从 URI 中找找替换多少次,默认为 -1
即全部替换。
在 Nginx 配置中,如果想要代理 WebSocket 链接,我们需要增加以下设置:
1 |
|
但是在 Caddy 中一切变得更加简单… 简单到就是我们啥也不用干,自动支持:
Websocket proxying “just works” in v2; there is no need to “enable” websockets like in v1.
在使用路径匹配器时,URL 默认是被解码的,例如:
1 |
|
至于反向代理 reverse_proxy
传出时的编码暂时还没有遇到,还需要测试一下。
有些站点可能默认就是 HTTP 的,我们也不期望以 HTTPS 方式访问;但是 Caddy 默认会为站点进行 ACME 证书申请,而申请不下来证书时又访问不了;这种情况下只需要在站点地址上强制写明 HTTP 协议即可:
1 |
|
如果想要代理 HTTPS 服务,那么只需要在 reverse_proxy
中填写 HTTPS 地址即可;不过与 Nginx 不同,Caddy 的 TLS 校验默认是开着的,所以如果后端 HTTPS 证书过期等情况可能导致 Caddy 返回 502 错误; 这种情况可以通过 transport
进行关闭:
1 |
|
如果已经有自己的证书,而不期望 Caddy 自动申请,那么只需要在 tls
指令后加上证书即可:
1 |
|
Caddy 的日志系统与 Nginx 完全不同,Caddy 日志按照 Namespace 划分,在站点配置中默认为只可以打印当前站点的请求日志,如果需要打印例如反向代理的上游地址等需要在全局日志配置中配置。 日志这一块一句两句说不清,推荐直接看官方文档以及日志实现逻辑,如果懂 go 的话可以看看 uber-go/zap 这个日志框架;下面是按文件分开打印请求日志和上游日志的样例:
1 |
|
很不幸的是我们有一个 TLS 1.1 兼容的服务,当切换到 Caddy 后 TLS 1.1 已经不被支持,目前 Caddy 的 TLS 兼容性最小为 TLS 1.2,最大为 TLS 1.3:
protocols <min> [<max>]
protocols
specifies the minimum and maximum protocol versions. Default min:tls1.2
. Default max:tls1.3
.
总结一句话: 匹配器舒服,配置行为明确,配置引用少写一万行,其他的坑继续踩。
]]>GoReplay 采用 Go 编写,其只有一个单独的可执行文件,在官方 Release 页下载后将其放到 PATH
目录即可。
1 |
|
GoReplay 命令行整体使用方式为指定输入端和输入端,然后 GoReplay 从输入端将流量复制到输出端。
GoReplay 输入端可以指定一个 tcp 地址,然后 GoReplay 将该端口流量复制到输出端;下面样例展示从 127.0.0.0:8000
复制流量并输出到控制台的样例。
首先启动一个 HTTP Server,这里直接使用 python
的 HTTP Server
接着再让 gor 监听同样的端口,--output-stdout
指定输出端为控制台
此时通过 curl
访问 python
的 HTTP Server 可以看到 gor 将 HTTP 请求复制并输出到了控制台
同样如果我们通过 --output-http
选项将输出端指定为另一个 HTTP Server,那么 gor 会将请求同步复制并发送到输出端 HTTP Server。
GoReplay 可以将输出端指定为文件,从而将流量保存到文件中,然后 GoReplay 读取该保存的流量文件并重放到指定的 HTTP Server 中。
首先通过 --outpu-file
选项将请求保存到文件中
使用 --input-file
选项读取流量信息,然后通过 --output-http
选项重放到目标服务器
在将流量保存到文件时,默认情况下 GoReplay 以块形式写入文件,并且每个块将生成一个独立的文件名(test_0.gor
),如果想要将所有块的流量全部写入一个文件中,可以设置 --output-file-append
为 true
。
同时 GoReplay 输出文件名支持日期占位符,例如 --output-file %Y%m%d.gor
会生成 20210801.gor
这种文件名;所有可用的日期占位符如下:
%Y
: year including the century (at least 4 digits)%m
: month of the year (01..12)%d
: Day of the month (01..31)%H
: Hour of the day, 24-hour clock (00..23)%M
: Minute of the hour (00..59)%S
: Second of the minute (00..60)请求比较多时,将流量保存到文件可能会导致文件很大,这时候可以使用 .gz
结尾作为文件名,GoReplay 读取到 .gz
后缀后会自动进行 GZip 压缩处理。
1 |
|
如果需要对多个文件进行聚合重放,只需要指定多个文件即可,重放过程中 GoReplay 会自动保持请求顺序:
1 |
|
在使用文件输入时,GoReplay 还支持压力测试,通过 test.gor|200%
这种方式指定的文件名,GoReplay 会以两倍的速率进行请求重放:
1 |
|
GoReplay 采用比较底层的数据包拦截技术,当一个 TCP 数据包到达时内核 GoReplay 会进行拦截;然而数据包可以乱序到达,接下来内核需要重建 TCP 流来保证上层应用能以正确的顺序读取 TCP 数据包,这时候内核就会有一个数据包的缓冲区;默认情况下 Linux 系统的缓冲区为 2M,Windiws 为 1M,当特定的 HTTP 请求数据包超过缓冲区时,GoReplay 就无法正确的拦截(因为 GoReplay 需要一个完整的 HTTP 请求数据包用于保存到文件或者重放),同时可能会导致请求丢失、请求损坏等问题。
为了解决这种问题,GoReplay 提供了 --input-raw-buffer-size
选项用于调整缓冲区大小,例如 --input-raw-buffer-size 10485760
选项会将缓冲区调整为 10M。
某些情况下可能为了方便调试,我们在生产环境抓取流量并镜像到测试环境进行重放;但是可能由于生产环境流量比较大,我们并不需要如此大的请求速率,这时候可以通过速率限制让 GoReplay 帮我们控制请求数量。
绝对数量限制: 使用 --output-http "ADDRESS|N"
形式的参数时,GoReplay 会保证镜像的流量请求每秒不会超过 “N” 个。
1 |
|
百分比限制限制: 使用 --output-http "ADDRESS|N%"
形式的参数时,GoReplay 会保证镜像的流量维持在总流量的 “N%”。
在某些时候我们只期望把生产环境的特定流量重放到测试环境,或者禁止一些流量重放到测试环境,这时候我们可以使用 GoReplay 的过滤功能;GoReplay 提供以下选项来提供过滤功能:
--http-allow-header
: 允许重放的 HTTP 头(支持正则)--http-allow-method
: 允许重放的 HTTP 方法--http-allow-url
: 允许重放的 URL(支持正则)--http-disallow-header
: 不允许的 HTTP 头(支持正则)--http-disallow-url
: 不允许的 HTTP URL(支持正则)以下是官方给出的命令样例:
1 |
|
有时候可能测试环境的 URL 路径与生产环境完全不同,此时如果直接把生产环境的流量在测试环境重放可能会导致请求路径错误等情况;为此 GoReplay 提供了 URL 重写、参数设置、请求头设置等功能。
通过 --http-rewrite-url
选项进行 URL 重写
1 |
|
设置 URL 参数
1 |
|
设置请求头
1 |
|
Host 头是一个特殊的请求头,默认情况下 GoReplay 会将其自动设置为目标重放地址的域名,如果想关闭这种默认行为请使用 --http-original-host
选项。
GoReplay 可以使用中继服务器从而实现链式的流量传递,使用中继服务器时只需要将输出端设置为 TCP 模式,然后中继服务器输入端也设置为 TCP 模式即可:
1 |
|
如果有多个中继服务器,可以使用 --split-output
选项让每个抓取流量的 GoReplay 使用轮询算法向每个中继服务器发送流量:
1 |
|
GoReplay 支持将输出端设置为 ElasticSearch:
1 |
|
输出到 ES 时不需要预先创建索引,GoReplay 会自动完成,输出到 ES 后其数据结构如下:
1 |
|
除了输出到 ES 以外,GoReplay 还支持输出到 Kafka 以及从 Kafka 中读取数据:
1 |
|
最近两年一直在使用 kubeadm 部署 kubernetes 集群,总体来说配合一些自己小脚本还有一些自动化工具还算是方便;但是全容器化稳定性确实担忧,也遇到过莫名其妙的证书过期错误,最后重启大法解决这种问题;所以也在探索比较方便的二进制部署方式,比如这个 k0s。
The Simple, Solid & Certified Kubernetes Distribution.
k0s 可以认为是一个下游的 Kubernetes 发行版,与原生 Kubernetes 相比,k0s 并未阉割大量 Kubernetes 功能;k0s 主要阉割部分基本上只有树内 Cloud provider,其他的都与原生 Kubernetes 相同。
k0s 自行编译 Kubernetes 源码生成 Kubernetes 二进制文件,然后在安装后将二进制文件释放到宿主机再启动;这种情况下所有功能几乎与原生 Kubernetes 没有差异。
k0sctl 是 k0s 为了方便快速部署集群所提供的工具,有点类似于 kubeadm,但是其扩展性要比 kubeadm 好得多。在多节点的情况下,k0sctl 通过 ssh 链接目标主机然后按照步骤释放文件并启动 Kubernetes 相关服务,从而完成集群初始化。
安装过程中会自动下载相关镜像,需要保证所有节点可以扶墙,如何离线安装后面讲解。安装前保证目标机器的 hostname 为非域名形式,否则可能会出现一些问题。以下是一个简单的启动集群示例:
首先安装 k0sctl
1 |
|
然后编写 k0sctl.yaml 配置文件
1 |
|
最后执行 apply
命令安装即可,安装前确保你的操作机器可以 ssh 免密登陆所有目标机器:
1 |
|
稍等片刻后带有三个 Master 和两个 Node 的集群将安装完成:
1 |
|
与 kubeadm 不同,k0sctl 几乎提供了所有安装细节的可定制化选项,其通过三种行为来完成扩展:
/var/lib/k0s/manifests
目录时,k0s 在安装过程中会自动应用这些配置,类似 kubelet 的 static pod 一样,只不过 k0s 允许全部资源(包括不限于 deployment、daemonset、namespace 等);同样也可以直接在 k0sctl.yaml
添加 Helm 配置,k0s 也会以同样的方式帮你管理。hooks
选项来实现执行一些特定的脚本(文档里没有,需要看源码),以便在特定情况下做点骚操作。基于上面的扩展,k0s 还方便的帮我们集成了离线镜像包的自动导入,我们只需要定义一个文件上传,将镜像包上传到 /var/lib/k0s/images/
目录后,k0s 会自定将其倒入到 containerd 中而无需我们手动干预:
1 |
|
关于 image 压缩包(bundle_file)如何下载以及自己自定义问题请参考官方 Airgap install 文档。
默认情况下 k0s 内部集成了两个 CNI 插件: calico 和 kube-router;如果我们使用其他的 CNI 插件例如 flannel,我们只需要将默认的 CNI 插件设置为 custom
,然后将 flannel 的部署 yaml 上传到一台 master 的 /var/lib/k0s/manifests
目录即可,k0s 会自动帮我门执行 apply -f xxxx.yaml
这种操作。
下面是切换到 flannel 的样例,需要注意的是 flannel 官方镜像不会帮你安装 CNI 的二进制文件,我们需要借助文件上传自己安装(CNI GitHub 插件下载地址):
1 |
|
除了普通文件、镜像压缩包等,默认情况下 k0sctl 在安装集群时还会在目标机器上下载 k0s 二进制文件;当然在离线环境下这一步也可以通过一个简单的配置来实现离线上传:
1 |
|
默认情况下 k0s 版本号与 Kubernetes 保持一致,但是如果期望某个组件使用特定的版本,则可以直接配置这些内置组件的镜像名称:
1 |
|
熟悉 Kubernetes 的应该清楚,master 上三大组件: apiserver、controller、scheduler 管控整个集群;在 k0sctl 安装集群的过程中也允许自定义这些组件的参数,这些调整通过修改使用的 k0sctl.yaml
配置文件完成。
spec.api.extraArgs
: 用于自定义 kube-apiserver 的自定义参数(kv map)spec.scheduler.extraArgs
: 用于自定义 kube-scheduler 的自定义参数(kv map)spec.controllerManager.extraArgs
: 用于自定义 kube-controller-manager 自定义参数(kv map)spec.workerProfiles
: 用于覆盖 kubelet-config.yaml 中的配置,该配置最终将于默认的 kubelet-config.yaml 合并除此之外在 Host
配置中还有一个 InstallFlags
配置用于传递 k0s 安装时的其他配置选项。
其实上面的第二部分主要都是介绍 k0sctl 一些基础功能,为的就是给下面这部分 HA 生产级部署做铺垫。
就目前来说,k0s HA 仅支持独立负载均衡器的 HA 架构;即外部需要有一个高可用的 4 层负载均衡器,其他所有 Node 节点链接这个负载均衡器实现 master 的高可用。在使用 k0sctl 命令搭建 HA 集群时很简单,只需要添加一个外部负载均衡器地址即可;以下是一个完整的,全离线状态下的 HA 集群搭建配置。
在搭建之前我们假设已经有一个外部的高可用的 4 层负载均衡器,且负载均衡器已经负载了以下端口:
6443(for Kubernetes API)
: 负载均衡器 6443 负载所有 master 节点的 64439443 (for controller join API)
: 负载均衡器 9443 负载所有 master 节点的 94438132 (for Konnectivity agent)
: 负载均衡器 8132 负载所有 master 节点的 81328133 (for Konnectivity server)
: 负载均衡器 8133 负载所有 master 节点的 8133以下为一个 nginx 4 层代理的样例:
1 |
|
以下为 k0sctl 的 HA + 离线部署样例配置:
1 |
|
最后只需要执行 k0sctl apply -c k0sctl.yaml
稍等几分钟集群就搭建好了,安装过程中可以看到相关文件的上传流程:
kubeadm 集群默认证书有效期是一年,到期要通过 kubeadm 重新签署;k0s 集群也差不多一样,但是不同的是 k0s 集群更加暴力;只要 CA(默认 10年) 不丢,k0s 每次重启都强行重新生成一年有效期的证书,所以在 HA 的环境下,快到期时重启一下 k0s 服务就行。
k0sctl 安装完的集群默认只有一个 k0scontroller.service
服务,master、node 上所有服务都由这个服务启动,所以到期之前 systemctl restart k0scontroller.service
一下就行。
k0sctl 提供了集群备份和恢复功能,默认情况下只需要执行 k0sctl backup
即可完成集群备份,该命令会在当前目录下生成一个 k0s_backup_TIMESTAMP.tar.gz
备份文件。
需要恢复集群时使用 k0sctl apply --restore-from k0s_backup_TIMESTAMP.tar.gz
命令进行恢复即可;需要注意的是恢复命令等同于在新机器重新安装集群,所以有一定风险。
经过连续两天的测试,感觉这个备份恢复功能并不算靠谱,还是推荐使用 Velero 备份集群。
在小规模集群场景下可能并不需要特别完善的 Etcd 作为存储,k0s 借助于 kine 库可以实现使用 SQLite 或 MySQL 等传统数据库作为集群存储;如果想要切换存储只需要调整 k0sctl.yaml
配置即可:
1 |
|
使用 k0sctl 搭建的集群通过 k0s
命令可以很方便的为集群添加用户,以下是添加样例:
1 |
|
在不做配置的情况下 k0s 集群使用默认的 Containerd 配置,如果需要自己定义特殊配置,可以在安装时通过文件上传方式将 Containerd 配置文件上传到 /etc/k0s/containerd.toml
位置,该配置将会被 k0s 启动的 Containerd 读取并使用。
k0s 是个不错的项目,对于二进制宿主机部署 Kubernetes 集群很方便,由于其直接采用 Kubernetes 二进制文件启动,所以基本没有功能阉割,而 k0sctl 又为自动化安装提供了良好的扩展性,所以值得一试。不过目前来说 k0s 在细节部分还有一定瑕疵,比如 konnectivity
服务在安装时无法选择性关闭等;k0s 综合来说是个不错的工具,也推荐看看源码,里面很多设计很新颖也比较利于了解集群引导过程。
自打很多年前开始使用静态博客工具来发布博客,现在基本上博客源码编译后就是一堆 html 等静态文件;一开始使用 nginx 作为静态文件服务器,后来切换到的 Caddy2;不过最近在 Google Search Console 中发现了大量的无效链接,给出的提示是 “网页会自动重定向”。
经过测试后发现这些链接地址在访问时都会重定向一下,然后在结尾加上 /
;没办法我就开始探索这个 /
是怎么来的了。
没办法,也不知道那个配置影响的,只能去翻 file server 的源码,在几经查找之后找到了以下代码(而且还带着注释):
从代码逻辑上看,只要 *fsrv.CanonicalURIs
这个变量为 true
,那么就会触发自动重定像,并在 “目录” 尾部补上 /
;注释里也说的很清楚是为了目录规范化,如果想看详细讨论可以参考那两个 issue。
翻了这个 *fsrv.CanonicalURIs
变量以后,突然发现 Caddyfile 里其实是不支持这个配置的;所以比较 low 的办法就是利用 Admin API,先把 json 弄出来,然后加上配置再。POST 回去:
1 |
|
1 |
|
现在可以直接从 master 构建 Caddy,或者等待 v2.4.4
版本发布,这两种方式产生的 Caddy 二进制文件已经支持了这个配置选项,配置样例如下:
1 |
|
发现大部分人在切换 Caddy 时遇到的比较大的困难就是这个 Caddyfile 不知道怎么写,一开始我也是很懵逼的状态,今天决定写写这个 Caddyfile 配置语法,顺便自己也完整的学学。
在 Caddy1 时代,Caddy 自创了一种被称之为 Caddyfile 的配置文件格式,当然可以理解为创造了一种语法,这里面深入的说就涉及到了编译原理相关知识,这里不再展开细谈(因为我也不会);Caddyfile 由内部的语法解析器进行语法、词法分析最后 “序列化” 到 Go 的配置结构体中。
随着 Caddy 壮大,到了 Caddy2 时代人们已经并不满足于单纯的 Caddyfile 配置,因为学习 Caddyfile 是有代价的,负载均衡器选型的切换本身就代价很大,还要去花心思学习 Caddyfile 语法,这无异非常痛苦。所以 Caddy2 在经过取舍过后决定使用 json 作为内部标准配置,然后其他类型的配置通过 Config Adapters
将其转换为 json 再使用,而 Caddyfile 的 Adapter 作为官方支持的内置 Adapter 存在。
最终要说明的是: Caddyfile 里支持哪些指令是由 Caddyfile 的 Adapter 决定的,内部的 json 配置对应的指令名称可能跟 Caddyfile 不同,也可能内部 json 支持一些指令,而 Caddyfile 根本不支持。
开局一张图,文章全靠编(下面是官方的语法结构图)
在一个 Caddyfile 内(空白文本文件),如果仅以两个大括号括起来的配置就是全局配置项,例如下面的配置:
1 |
|
那么一共有哪些全局配置项呢?当然是看 官方文档:
1 |
|
这些全局配置具体都什么意思这里就不细说了,请自行查阅文档;当然文档也可能并不一定准确,有些兴趣的可以去查看 Caddy 源码,这些都在源码中定义了 caddyconfig/httpcaddyfile/options.go:28
叫代码块可能不太恰当,也可以叫做配置块或配置片段;这是 Caddyfile 比较棒的一个功能,配置片段可以实现类似代码这种引用使用,方便组合配置文件;配置片段的语法如下:
1 |
|
下面是一个配置片段示例(不能运行,只是举例):
1 |
|
这种写法与下面的配置等价,目的就是增加配置的重用和规范化:
1 |
|
站点配置是 Caddyfile 的核心中的核心,从开局的图上也可以看到,能在 “Top Level” 上存在的只有三种配置,其中就包含了这个站点配置块,站点配置块格式如下:
1 |
|
以下是两个合法的站点配置示例:
1 |
|
请求匹配器是 Caddy 内置的一种针对请求的过滤工具,有点类似于 nginx 配置中的 location /api {...}
,只不过 Caddyfile 中的匹配器更加强大;标准的请求匹配器列表如下:
自定义命名匹配器的作用是组合多个标准匹配器,然后实现复用,自定义命名匹配器语法如下:
1 |
|
然后这个自定义的命名匹配器可以在其他位置引用:
1 |
|
Caddyfile 中的配置块可以理解为代码中的作用域,其包含两个大括号范围内的所有配置:
1 |
|
当 Caddyfile 中只有一个站点配置,且不需要其他全局配置等信息时,Blocks 可以被省略,例如:
1 |
|
这个配置可以直接简写为:
1 |
|
这么做的目的是方便单站点快速配置,但是一般不常用也不推荐使用。在同一个 Caddyfile 中可以包含多个站点配置,只要地址不同即可:
1 |
|
指令是指描述站点配置的一些关键字,例如下面的站点配置文件:
1 |
|
在这个配置文件中 reverse_proxy
就是一个指令,同时指令还可能包含子指令(Subdirectives),下面的配置中 lb_policy
就是 reverse_proxy
的一个子指令:
1 |
|
在 Caddyfile 被 Caddy 读取后,Caddy 会将配置文件解析为一个个的 Token;Caddyfile 中所有 Token 都认为是空格分割,所以如果某些指令需要传递参数时我们需要通过合理的空格和引号来确保 Token 正确解析:
1 |
|
如果某些参数需要包含空格,那么需要使用双引号包裹:
1 |
|
如果这个参数里需要包含双引号,只需要通过反斜线转义即可,例如 "\"a b\""
;如果有太多的双引号或者空格,可以使用 Go 语言中类似的反引号来定义 “绝对字符串”:
1 |
|
Caddyfile 中的地址其实是一种很宽泛的格式,在上面讲站点配置时其实前面的字符串并不一定是域名,准确的说应该是地址:
1 |
|
在 Caddyfile 中以下格式全部都是合法的地址:
localhost
example.com
:443
http://example.com
localhost:8080
127.0.0.1
[::1]:2015
example.com/foo/*
*.example.com
http://
需要注意的是: 自动 HTTPS 是 Caddy 服务器的一个重要特性,但是自动 HTTPS 会隐式进行,除非在地址中明确的写明 http://example.com
这种格式时 Caddy 才会单纯监听 HTTP 协议,否则域名格式的地址 Caddy 都会进行 HTTPS 证书申请。
如果地址中指定了域名,那么只有匹配到域名的请求才会接受;例如地址为 localhost
的站点不会响应 127.0.0.1
方式的访问请求。同时地址中可以采用 *
作为通配符,通配符作用域仅在域名的英文句号 .
之内,意思就是说 *.example.com
会匹配 test.example.com
但不会匹配 abc.test.example.com
。
如果多个域名/地址共享一个站点配置,可以采用英文逗号分隔的方式写在一起:
1 |
|
匹配器其实在第一部分已经介绍过,这里仅做一下简单说明;匹配器一般紧跟在指令之后,其大致格式分为以下三种:
*
: 匹配所有请求(通配符)/path
: 匹配特定路径@name
: 自定义命名匹配器匹配器的用法样例如下:
1 |
|
具体更细节的官方匹配器使用限于篇幅这里不再详细说明,请自行阅读 官方文档
占位符可以理解为 Caddyfile 内部的变量替换符号,占位符同样以大括号包裹,同时支持转义:
1 |
|
Caddyfile 内部可用的占位符有很多,但是并非在所有情况下都可用,比如 HTTP 相关的占位符仅在处理 HTTP 请求相关配置中才可用;同时占位符也支持简写,下面是官方目前支持的占位符列表:
片段上面也介绍过了,这里说一下片段更高级的用法: 支持参数传递;下面是定义一个通用日志格式,然后通过参数引用实现不同站点使用不同日志文件的配置:
1 |
|
注释没啥好说的,以 #
作为开头就行了。
环境变量和占位符类似,不同的是占位符是 Caddyfile 内置的变量,而环境变量是引用系统环境变量;环境变量的使用格式如下(推荐全大写):
1 |
|
上面的配置在 Caddy 启动时会读取 SITE_ADDRESS
作为监听地址,如果 SITE_ADDRESS
读取不到则会报错退出;如果想要为 SITE_ADDRESS
设置默认值,那么只需要使用如下格式即可:
1 |
|
Caddyfile 并不是万能的,但是 Caddyfile 因为更易于编写和维护所以使用比较广泛;在第一部分介绍 Caddy 的配置文件体系时已经说明了,实际上 Caddy 内部是使用 json 作为配置的;这时就可能出现一些极端情况,比如说真的某个配置只能通过 json 配置,那么这时候可以考虑先通过 json 管理 API 进行动态修改,然后再去向官方发 issue,有能力也可以直接 PR;API 动态修改的流程如下:
首先假设你已经有一个能够正常启动的 Caddyfile,但是某个配置选项不支持,这时候你可以通过 API 获取内部的 json 配置:
1 |
|
得到这个配置以后,你可以通过格式化工具格式化 json,然后添加特定选项,再将其保存到一个配置文件中,然后重新 load 回去即可:
1 |
|
本文所有源码分析基于 Go 1.16.4,阅读时请自行切换版本。
标准库中的 Context 是一个接口,其具体实现有很多种;Context 在 Go 1.7 中被添加入标准库,主要用于跨多个 Goroutine 设置截止时间、同步信号、传递上下文请求值等。
由于需要跨多个 Goroutine 传递信号,所以多个 Context 往往需要关联到一起,形成类似一个树的结构。这种树状的关联关系需要有一个根(root) Context,然后其他 Context 关联到 root Context 成为它的子(child) Context;这种关联可以是多级的,所以在角色上 Context 分为三种:
标准库中定义的 Context 创建方法大致如下:
在阅读源码后会发现,Context 各种创建方法其实主要只使用到了 4 种类型的 Context 实现:
emptyCtx
实际上就是个 int,其对 Context 接口的主要实现(Deadline
、Done
、Err
、Value
)全部返回了 nil
,也就是说其实是一个 “啥也不干” 的 Context;它通常用于创建 root Context,标准库中 context.Background()
和 context.TODO()
返回的就是这个 emptyCtx
。
1 |
|
cancelCtx
内部包含一个 Context
接口实例,还有一个 children map[canceler]struct{}
;这两个变量的作用就是保证 cancelCtx
可以在 parent Context 和 child Context 两种角色之间转换:
children map[canceler]struct{}
中建立关联关系cancelCtx
被定义为一个可以取消的 Context,而由于 Context 的树形结构,当作为 parent Context 取消时需要同步取消节点下所有 child Context,这时候只需要遍历 children map[canceler]struct{}
然后逐个取消即可。
1 |
|
timerCtx
实际上是在 cancelCtx
之上构建的,唯一的区别就是增加了计时器和截止时间;有了这两个配置以后就可以在特定时间进行自动取消,WithDeadline(parent Context, d time.Time)
和 WithTimeout(parent Context, timeout time.Duration)
方法返回的都是这个 timerCtx
。
1 |
|
valueCtx
内部同样包含了一个 Context
接口实例,目的也是可以作为 child Context,同时为了保证其 “Value” 特性,其内部包含了两个无限制变量 key, val interface{}
;在调用 valueCtx.Value(key interface{})
会进行递归向上查找,但是这个查找只负责查找 “直系” Context,也就是说可以无限递归查找 parent Context 是否包含这个 key,但是无法查找兄弟 Context 是否包含。
1 |
|
cancelCtx 在调用 context.WithCancel
方法时创建(暂不考虑其他衍生类型),创建方法比较简单:
1 |
|
newCancelCtx
方法就是将 parent Context 设置到内部变量中,值得分析的是 propagateCancel(parent, &c)
方法和被其调用的 parentCancelCtx(parent Context) (*cancelCtx, bool)
方法,这两个方法保证了 Context 链可以从顶端到底端的及联 cancel,关于这两个方法的分析如下:
1 |
|
在上面的 cancelCtx 创建源码中可以看到,cancelCtx 内部跨多个 Goroutine 实现信号传递其实靠的就是一个 done channel;如果要取消这个 Context,那么就需要让所有 <-c.Done()
停止阻塞,这时候最简单的办法就是把这个 channel 直接 close 掉,或者干脆换成一个已经被 close 的 channel,事实上官方也是怎么做的。
1 |
|
在上面的 3.1 章节中分析 parentCancelCtx
方法时有这么一段:
1 |
|
现在来仔细说明一下 “为什么没有意义?” 这个问题:
首先是调用 parentCancelCtx
方法的位置,在 context 包中只有两个位置调用了 parentCancelCtx
方法;一个是在创建 cancelCtx 的 func WithCancel(parent Context)
的 propagateCancel(parent, &c)
方法中,另一个就是 cancel
方法的 removeChild(c.Context, c)
调用中;下面分析一下这两个方法的目的。
propagateCancel
负责保证当 parent cancelCtx 在取消时能正确传递到 child Context;那么它需要通过 parentCancelCtx
来确定 parent Context 是否是一个 cancelCtx,如果是那就把 child Context 加到 parent Context 的 children map 中,然后 parent Context 在 cancel 时会自动遍历 map 调用 child Context 的 cancel;如果不是那就开 Goroutine 阻塞读 parent Context 的 done channel然后再调用 child Context 的 cancel。
1 |
|
所以在这个方法调用时,如果 parentCancelCtx
取出一个已取消的 cancelCtx,那么 parent Context 的 children map 在 cancel 时已经清空了,这时要是再给设置上就有问题了,同样业务需求中 propagateCancel
为了就是控制传播,明明 parent Context 已经 cancel 了,再去传播就没意义了。
同上面的 3.3.1 一样,**removeChild(c.Context, c)
目的是在 cancel 时断开与 parent Context 的关联,同样是为了处理 children map 的问题;此时如果 parentCancelCtx
也取出一个已经 cancel 的 parent Context,由于 parent Context 在 cancel 时已经清空了 childre map,这里再尝试 remove 也没有任何意义。**
1 |
|
timerCtx 的创建主要通过 context.WithDeadline
方法,同时 context.WithTimeout
实际上也是调用的 context.WithDeadline
:
1 |
|
了解了 cancelCtx 的取消流程以后再来看 timerCtx 的取消就相对简单的多,主要就是调用一下里面的 cancelCtx 的 cancel,然后再把定时器停掉:
1 |
|
相对于 cancelCtx 还有 timerCtx,valueCtx 实在是过于简单,因为它没有及联的取消逻辑,也没有过于复杂的 kv 存储:
1 |
|
分析 Context 源码断断续续经历了 3、4 天,说心里话发现里面复杂情况有很多,网上其他文章很多都是只提了一嘴,但是没有深入具体逻辑,尤其是 cancelCtx 的相关调用;我甚至觉得我有些地方可能理解的也不完全正确,目前就先写到这里,如果有不对的地方欢迎补充。
]]>本文所有源码分析基于 Caddy2 v2.4.2 版本进行,未来版本可能源码会有变化,阅读本文时请自行将源码切换到 v2.4.2 版本。
Caddy2 对配置文件中的 listener_wrappers
配置有以下描述:
Allows configuring listener wrappers, which can modify the behaviour of the base listener. They are applied in the given order.
同时对于 tls
这个 listener_wrappers
还做了一下说明:
There is a special no-op tls listener wrapper provided as a standard module which marks where TLS should be handled in the chain of listener wrappers. It should only be used if another listener wrapper must be placed in front of the TLS handshake.
综上所述,简单的理解就是 listener_wrappers
在 Caddy2 中用于改变链接行为,这个行为可以理解为我们可以自定义接管链接,这些 “接管” 更偏向于底层,比如在 TLS 握手之前做点事情或者在 TLS 握手之后做点事情,这样我们就可以实现一些魔法操作。
在 Caddy2 启动时首先会进行配置文件解析,例如解析 Caddyfile、json 等格式的配置文件,listener_wrappers
在配置文件中被定义为一个 ServerOption
:
caddyconfig/httpcaddyfile/serveroptions.go:47
该配置最终会被注入到 Server 的 listenerWrappers 属性中(先解析为 ListenerWrappersRaw
然后再实例化)
modules/caddyhttp/server.go:132
最后在 App 的启动过程中遍历 listenerWrappers 并逐个应用,在应用 listenerWrappers
时有个比较重要的顺序处理:
首先 Caddy2 会尝试在 net.Listener
上应用一部分 listenerWrappers
,当触及到 tls
这个 token 的 listenerWrappers
之后终止应用;终止前已被应用的这部分 listenerWrappers
被认为是 TLS 握手之前的自定义处理,然后在 TLS 握手之后再次应用剩下的 listenerWrappers
,后面这部分被认为是 TLS 握手之后的自定义处理。
最终对 ListenerWrapper 加载流程分析如下:
ListenerWrappersRaw []json.RawMessage
ctx.LoadModule(srv, "ListenerWrappersRaw")
实例化 ListenerWrapperctx.LoadModule
时,如果发现了 tls
指令则按照配置文件顺序排序 ListenerWrapper 切片,否则将 tls
这个特殊的 ListenerWrapper 放在首位;这意味着在配置中不写 tls
时,所有 ListenerWrapper 永远处于 TLS 握手之后net.Listener
的处理,其底层是 net.Conn
;这意味着 ListenerWrapper 不会对 UDP(net.PacketConn
) 做处理,代码中也可以看到 ListenerWrapper 并未对 HTTP3 处理说了半天,也分析了源码,那么最终回到问题原点: ListenerWrapper 能干什么?答案就是自定义协议,例如神奇的 caddy-trojan 插件。
caddy-trojan 插件实现了 ListenerWrapper,在 App 启动时通过源码可以看到,TLS 握手完成后原始的 TCP 链接将交由这个 ListenerWrapper 处理:
1 |
|
该插件对 WrapListener
方法的实现如下:
1 |
|
所以这个 wrapper 核心处理在 loop()
中:
1 |
|
可以看到,当新链接进入时,首先对包头做检测 if ok := up.Validate(ByteSliceToString(b[:HeaderLen]))
;如果检测通过那么这个链接就完全插件自己处理后续逻辑了;如果不通过则将此链接返回给 Caddy2,让 Caddy2 继续处理。
这里面涉及到一个一开始让我不解的问题: “链接不可重复读”,后来看源码才明白作者处理方式很简单: 包装一个 rawConn
,在验证部分由于已经读了一点数据,如果验证不通过就把它存起来,然后让下一个读操作先读这个 buffer,从而实现原始数据组装。
1 |
|
ListenerWrapper 是 Caddy2 一个强大的扩展能力,在 ListenerWrapper 基础上我们可以实现对 TCP 链接自定义处理,我们因此可以创造一些奇奇怪怪的协议。同时我们通过让链接重新交由 Caddy2 处理又能做到完美的伪装: 当你去尝试访问时,如果密码学验证不通过,那么后续行为就与标准 Caddy2 表现一致,主动探测基本无效。对任何自己创造的 ListenerWrapper 来说,如果开启了类似 AEAD 这种加密,探测行为本身就会被转接到对抗密码学原理上。
]]>所谓工欲善其事,必先利其器;这篇文章分享一些日常 Coding 中常用 JetBrains 系列 IDE 插件(本文所有插件可直接从 Marketplace 搜索并安装)。
上来先整点没用的吧,主题配色这个东西根据个人喜好;我比较喜欢花花绿绿的感觉,在防止眼疲劳的同时还有点 RMB 的感觉(RMB 也花花绿绿的),毕竟 Coding 的时候要 “酷一点”,然后才能心情愉悦的写 BUG(代码和我只要有一个能跑就行)。
也是啥用没有的插件,唯一的效果就是在各种 Loading 的时候进度条变成了 Go 的吉祥物(Golang 天下第一,嘶吼)…当然还有个同款,Gopher 变成了彩虹猫,需要的自己搜吧。
这个插件为项目里一些特殊文件增加 Icon,比如 .gitignore
文件、CI 配置等,可以让人看着更舒服一些。
GitToolBox 会在光标定位到某一行代码时显示其最近改动等提交信息,方便在甩锅时精确定位到背锅侠。
这个就牛逼了,非常有实用价值的一个插件;当某些规范下你必须进行 “驼峰/下划线/短横线/全大写/全小写/大写加下划线/小写加下划线…” 疯狂转换时,String Manipulation 只需要选中右键即可完成。
Randomness 在需要测试数据时很有用,它可以快速生成一些随机性的垃圾数据填充进来;例如写示例配置时。
英文渣必备插件,除了能翻译一些单词之外,还能自动识别文档进行翻译,甚至还带单词本。
当你想把你的代码发到某个群里装X,或者写博客不想让别人复制只想展示时,carbon-now-sh 可以帮你把选中的代码传输到 https://carbon.now.sh/ 来生成漂亮的图片。
]]>给时光以生命,而不是给生命以时光。
每当谈起岁月,总显得是那么残忍,残忍到能真切的感受到 “岁月如刀”,然后一刀又一刀;今天莫名其妙的看了一下我的生日(是的,已经忘记了年龄),然后得到了一个很有意思的答案:
在这个世界上生活了 9684 天,生命进度条以 80 岁算正好 33%
。
所以我的青春已经开始向我挥手告别,或许有些艰难,或许有点感慨,但终究都已不再重要。我庆幸的是我的青春不会像大多数人一样无趣,虽然充满血腥却显得美丽;但同样也会留下许多遗憾,缺了孩子气的天真,没体会过关心,没了热血冲动的经历。
以前每当回想起来的时候,总是带着那么一点悲伤;今天突然想起一句话: “给时光以生命,而不是给生命以时光。”,所以当我们回首青春,应当给岁月以青春,而不是给青春以岁月;这段青春,感谢你砥砺前行,让岁月不在苦涩。
世上没有绝对的真理,这就是我要对你们说的话。
质疑事物的能力,看似简单,我却缺少了太多;父母曾经无数次对我说过 “你应该怎样怎样…”,小到吃饭喝水,大到人生规划,最后事实证明基本没有对的;有些看似亘古不变的东西让我忘却了它存在的意义,以至于我选择了随波逐流,却从未质疑过一个基本的对与错。
同样,这个世界不只有黑与白,还有很多灰。别人的对我质疑是否真正重要?这是否是自己放弃的理由?…让质疑我的人去质疑吧,这是他的权利,但我活不成他的样子。
这世界上从来就没有绝对的真理,浩瀚星河里我太过渺小,而命运又太过伟大;我抱着敬畏之心质疑一切,也接受这个世界对我的质疑。
我们用眼睛观察,而不全然接受。
我曾不止一次对只是看到表象的事物作出定论,以为笑容背后就是开心,以为乌云密布就会下雨。我把太多的归因算到我所看到或是模糊看到的东西上,当 “墨菲定律” 和 “幸存者偏差” 双重作用时,悲观的人会更悲观,快乐的人会更快乐。
现在人到中年,终于学会了放弃;我所看到的不一定是真的,我没看清的也不一定是假的,对待未知的结论永远应该是未知,所以赌不赌看自己,莫要让整个世界买单。
世界上没有完全相同的两片树叶。
太多的人喜欢对别人讲述 “自己的经验”,一件事怎么做才能成功,甚至认为自己是 “苦口婆心”。我曾经也做过类似的傻事,直到前些年遇到了一个出租车司机;
他以前是一个搞自动化测试的,几年前跟我一样一个人来到北京打拼,我们有着差不多相似的经历,但结果却并不相同也不相似;直到现在我还记得那一段路上他所诉说的艰苦和努力,还有命运的不公。
我那时才意识到: 成功根本无法复制,经验也或许没用,别人的一生之所以那样只是命运垂青,或是开了个玩笑。怎么做事,我无法说服别人,也没人可以教导我。
已经在开往地狱的路上,那就应该和魔鬼一起笑着去。
这是前两天在 Twitter 上看到老刘(@Yachen Liu)发的感悟:
对于多数人,大概在初中至大学阶段,思维方式、思维能力、性格等终身基本不变的特征就已经定型了,与此对应的人生未来轨道也已经基本确定,之后的时间不过是不断的对抗随机性向既定轨道回归。
我很认同这句话,环境会改变人的,很多性格、习惯其实都是那个懵懵懂懂的年纪养成的;所以现在开始不去尝试对抗随机性,给自己一点机会,我期望我短暂的生命中会有些其他的东西,因为我不想回到以前的轨道上。
写到最后,以大雪将至里的话结尾吧:
和所有的人一样,在他的一生里,也曾怀有过自己的想象和梦想,其中的一些是他自己实现的,有一些是命运赠予给他的,很多是从来都无法实现的,或者是刚刚得到,就又被从手中掠夺走的。但是他一直还活着。
他想不起来,他是从哪儿来的,最终他也不知道,他将要去向何方。但是,这生来死去之间的时光,他的一生,他可以不含遗憾地去回看,用一个戛然而止的微笑,然后就只是巨大的惊讶。
BGM 取自 “石进 - 夜的钢琴曲“ 系列第九曲。
]]>在一个月光如雪的晚上和 PMheart 在 Telegram 闲聊,突然发现群里一个人的昵称是当前时间,然后观察一会儿发现还在不停变化… 最可气的是他还弄个 “东半球🌏最准报时” 的头衔,我一开始以为是 Telegram 又出的什么 “高级功能”(毕竟微信炸💩都是 Telegram 好久之前玩剩下的),几经 Google 我发现其实就是自己写个 Bot,然后定时 rename;那我要不整个 “东半球最浪漫诗人” 岂不是太面了🤔。
在 Telegram 官方的文档描述中,其 Bot Api 实质上分为两种,这两种 Api 用途也各不相同:
由用户自行联系 BotFather
(人如其名)交互式创建,该 Bot 是官方所认为的标准 Bot,其主要目的就是作为一个真正的 Bot;我们可以通过一个 Token 调用 Telegram Api 来控制它,玩法很多,包括不限于发送告警、作为群管机器人、交互式的帮你做各种自动化等等;同时这个 Bot 具有严格的隐私权限控制,比如拉到群里可以控制 Bot 对群消息是否可见等等(Telegram 这点做的非常 OJBK);借助于这类 Bot,也有些脑洞大开的大哥在浪的边缘疯狂试探,比如下面的扫雷机器人:
还有算命的:
Github 真正干活的:
我的证书告警:
当然肯定有高铁动车组的(🔞我是正经人):
准确的官方介绍是 TDLib – build your own Telegram
,从这个介绍可以看出,这一个 “Bot” Api 本质上并不是让你写 Bot,而是作为开发一个第三方 Telegram 客户端用的;所以这个 Api 的权限很大,可以完整的模拟一个用户;目前我发现被滥用最多的就是用这个 Api 作为恶意拉人、发广告等,简直是币圈割韭菜御用。
TDLib 本质上是一个 C++ 的 lib,官方提供了引导页面来帮助你用主流语言跨语言调用来使用它:
标准 Bot 使用相对简单,按照官方文档跟 BotFather
聊天创建一个即可:
当创建完成后在 Bot 设置界面你可以获取一个 Token
,使用这个 Token 连接 Bot Api 地址就可以开始控制你的 Bot;Golang 开发可以考虑使用 https://github.com/tucnak/telebot 这个库,用法相当简单:
1 |
|
基于这个库,我为了方便使用写了一个命令行小工具,方便我发送告警信息等: tgsend
标准 Bot Api 很丰富,日常干活啥的也完全能满足,但是!人如果不会装X那和咸鱼有什么区别?我的 “东半球最浪漫诗人” 得提上日程。
关于 Api 易用性,开发生态环境,这一点说实话,Telegram 能把所有国内 IM 按在地上摩擦,就像这样:
Telegram 官方提供了完整的 “点一点” 构建 TDLib 引导页面: https://tdlib.github.io/td/build.html
勾选好自己的语言、操作系统、系统版本、甚至是编译的内存大小等设置后,无脑复制下面的命令执行就行:
TDLib 构建完成后,需要自行申请一个 API_HASH,API_HASH 类似一个让 Telegram 识别你的客户端的 “合法标识”;API_HASH 申请需要登陆 https://my.telegram.org/,然后选择 API development tools:
然后填写相关信息,最后 Telegram 就会为你生成好 API_HASH:
TDLib 构建好了,API HASH 也有了,那么根据自己选择的语言找一个靠谱的 SDK 使用即可;比如 Golang 开发,我选择了 https://github.com/Arman92/go-tdlib,这个库使用相当简单:
1 |
|
由于 Telegram 是允许多客户端登陆的(跟我一起喊:微信垃圾、张小龙垃圾),所以使用 TDLib 我们可以完全控制我们的账户行为;那么 “东半球最浪漫诗人” 实现就相对简单:
核心代码就这几行:
1 |
|
效果嘛,就这样:
]]>nerdctl 官方发布包包含两个安装版本:
这时候用脚趾头想我都要一把梭,在一把梭之前先卸载以前安装的 Docker 以及 Containerd 等组件(以下以 Ubuntu 20.04 为例):
1 |
|
然后下载安装包解压启动即可(一把梭真香):
1 |
|
启动完成后就可以通过 ctr
、crictl
命令测试 containerd 是否工作正常了;没问题的话继续折腾 nerdctl
。
Docker CLI 的兼容具体情况可以从 https://github.com/containerd/nerdctl#command-reference 中查看相关说明;既然是为了兼容 Docker CLI,那么在运行时只需要把 docker
命令换成 nerdctl
命令即可:
1 |
|
唯一需要注意的是部分命令选项还是有一定不兼容,比如 run
的时候 -d
和 -t
不能一起用,--restart
策略不支持等,但是通过列表可以看到大部分 cli 都已经完成了。
由于环境不同吧,说实话 Docker Compose 兼容才是吸引最大的一点;因为现实环境中很少有直接 docker run...
这么干的,大部分不重要服务都是通过 docker-compose
启动的;而目前来说 nerdctl
配合 CNI 等已经完成了大部分 Compose 的兼容:
docker-compose.yaml
1 |
|
运行测试:
1 |
|
不过目前比较尴尬的是 compose 还不支持 ps
命令,同时如果 volume 了宿主机目录,如果目录不存在也不会自动创建;logs
命令似乎也有 BUG。
nerdctl 目前还有很多不完善的地方,比如 cp
等命令不支持,compose
命令不完善,BuildKit 还不支持多平台交叉编译等;所以简单玩玩倒是可以,距离生产使用还需要一些时间,但是总体来说未来可期,相信不久以后我们会离 Docker 越来越远。
由于近几年 Let’s Encrypt 的兴起以及 HTTPS 的普及,个人用户终于可以免费 “绿” 一把了;但是 Let’s Encrypt ACME 申请的证书目前只有 3 个月,过期就要更换,最尴尬的是某些比较重要的东西(比如扶墙服务)证书一旦过期会耽误大事;而不同环境下自动更换证书工具也不一定靠谱,极端时候还是需要自己手动更换,所以催生了我想写个证书过期时间检测的小玩具的想法。
了解证书加密体系的应该知道,TLS 证书是链式信任的,所以中间任何一个证书过期、失效都会导致整个信任链断裂,不过单纯的 Let’s Encrypt ACME 证书检测可能只关注末端证书即可,除非哪天 Let’s Encrypt 倒下…
Go 在发送 HTTP 请求后,在响应体中会包含一个 TLS *tls.ConnectionState
结构体,该结构体中目前存放了服务端返回的整个证书链:
1 |
|
根据源码注释可以看到,PeerCertificates
包含了服务端所有证书,那么如果需要检测证书过期时间只需要遍历这个证书切片即可。
基本需求确定,且确立代码可行性后直接开始 coding:
1 |
|
基本检测逻辑完成后,可以尝试集成告警服务,例如 Email、Telegram、微信通知等;告警的实现暂时不在本文讨论范围内,具体完整实现可以参考 https://github.com/mritd/certmonitor,certmonitor 集成了 Telegram,最终效果如下:
有些情况下某些服务不一定是完全基于 HTTPS 的,所以协议上可以后续去尝试使用 tls 客户端直接链接,还可能需要考虑未来基于 QUIC 的 HTTP3 等,复杂点也要支持文件证书检测… 给我时间我能给自己提一万个需求(今天就先码到这)…
]]>由于 Kubernetes 新版本 Service 实现切换到 IPVS,所以需要确保内核加载了 IPVS modules;以下命令将设置系统启动自动加载 IPVS 相关模块,执行完成后需要重启。
1 |
|
重启完成后务必检查相关 module 加载以及内核参数设置:
1 |
|
Containerd 在 Ubuntu 20 中已经在默认官方仓库中包含,所以只需要 apt 安装即可:
1 |
|
安装成功后可以通过执行 ctr images ls
命令验证,本章节不会对 Containerd 配置做说明,Containerd 配置文件将在 Kubernetes 安装时进行配置。
Etcd 对于 Kubernetes 来说是核心中的核心,所以个人还是比较喜欢在宿主机安装;宿主机安装情况下为了方便我打包了一些 *-pack
的工具包,用于快速处理:
安装 CFSSL 和 ETCD
1 |
|
安装完成后,自行调整 /etc/cfssl/etcd/etcd-csr.json
相关 IP,然后执行同目录下 create.sh
生成证书。
1 |
|
证书生成完成后调整每台机器的 Etcd 配置文件,然后修复权限启动。
1 |
|
启动完成后通过 etcdctl
验证集群状态:
1 |
|
kubeadm 国内用户建议使用 aliyun 的安装源:
1 |
|
kube-apiserver-proxy 是我自己编译的一个仅开启四层代理的 Nginx,其主要负责监听 127.0.0.1:6443
并负载到所有的 Api Server 地址(0.0.0.0:5443
):
1 |
|
安装完成后根据 IP 地址不同自行调整 Nginx 配置文件,然后启动:
1 |
|
kubeadm-config 是一系列配置文件的组合以及 kubeadm 安装所需的必要镜像文件的打包,安装完成后将会自动配置 Containerd、ctrictl 等:
1 |
|
Containerd 配置位于 /etc/containerd/config.toml
,其配置如下:
1 |
|
在切换到 Containerd 以后意味着以前的 docker
命令将不再可用,containerd 默认自带了一个 ctr
命令,同时 CRI 规范会自带一个 crictl
命令;crictl
命令配置文件存放在 /etc/crictl.yaml
中:
1 |
|
kubeadm 配置目前分为 2 个,一个是用于首次引导启动的 init 配置,另一个是用于其他节点 join 到 master 的配置;其中比较重要的 init 配置如下:
1 |
|
init 配置具体含义请自行参考官方文档,相对于 init 配置,join 配置比较简单,不过需要注意的是如果需要 join 为 master 则需要 controlPlane
这部分,否则请注释掉 controlPlane
。
1 |
|
在调整好配置后,拉起 master 节点只需要一条命令:
1 |
|
拉起完成后记得保存相关 Token 以便于后续使用。
在第一个 master 启动完成后,使用 join
命令让其他 master 加入即可;需要注意的是 kubeadm-join.yaml
配置中需要替换 caCertHashes
为第一个 master 拉起后的 discovery-token-ca-cert-hash
的值。
1 |
|
node 节点拉起与拉起其他 master 节点一样,唯一不同的是需要注释掉配置中的 controlPlane
部分。
1 |
|
1 |
|
由于 kubelet 开启了证书轮转,所以新集群会有大量 csr 请求,批量允许即可:
1 |
|
同时为了 master 节点也能负载 pod,需要调整污点:
1 |
|
后续 CNI 等不在本文内容范围内。
1 |
|
本文中所有 *-pack
仓库地址如下:
Ubuntu 20.04 系统使用如下命令安装:
1 |
|
安装完成后使用 --help
应该能看到相关提示
1 |
|
安装完成后可直接使用该工具生成差异 SQL 文件,mysql-schema-diff
工具使用如下:
1 |
|
通俗的说,通过 --hostN
等参数指定两个数据库地址,例如 --password1
指定第一个数据库密码,--password2
指定第二个数据库密码;然后最后仅跟 数据库1[.表名] 数据库2[.表名]
,表名如果不写则默认对比两个数据库。以下为样例命令:
1 |
|
注意,首次运行后请根据生成的 SQL 判断对比是否正确,比如说想把比较新的测试库更改同步到生产库,那么 SQL 里全是DROP 字样的删除动作,这说明 --hostN
等参数指定反了(变成了生产库同步测试库),此时只需要将 --hostN
参数调换一下即可(1改成2,2改成1),这样生成的 SQL 就会变为 ADD 字样的添加动作。
mysql-schema-diff 工具需要在运行时能同时连接两个数据库,常规情况下可以通过 SSH 打洞来临时解决访问问题;如果实在无法打通网络环境,mysql-schema-diff 还支持文件对比,以下为一些文件对比的示例:
1 |
|
Longhorn 官方推荐的最小配置如下,如果数据并不算太重要可适当缩减和调整,具体请自行斟酌:
本次安装测试环境如下:
安装 Longhorn 推荐使用 Helm,因为在卸载时 kubectl 无法直接使用 delete 卸载,需要进行其他清理工作;helm 安装命令如下:
1 |
|
其中 longhorn-values.yaml
请从 Charts 仓库 下载,本文仅修改了以下两项:
1 |
|
安装完成后 Pod 运行情况如下所示:
此后可通过集群 Ingress 或者 NodePort 等方式暴露 service longhorn-frontend
的 80 端口来访问 Longhorn UI;注意,Ingress 等负载均衡其如果采用 HTTPS 访问请确保向 Longhorn UI 传递了 X-Forwarded-Proto: https
头,否则可能导致 Websocket 不安全链接以及跨域等问题,后果就是 UI 出现一些神奇的小问题(我排查了好久…)。
如果在安装过程中有任何操作错误,或想重新安装验证相关设置,可通过以下命令卸载 Longhorn:
1 |
|
Longhorn 总体设计分为两层: 数据平面和控制平面;Longhorn Engine 是一个存储控制器,对应数据平面;Longhorn Manager 对应控制平面。
Longhorn Manager 使用 Operator 模式,作为 Daemonset 运行在每个节点上;Longhorn Manager 负责接收 Longhorn UI 以及 Kubernetes Volume 插件的 API 调用,然后创建和管理 Volume;
Longhorn Manager 在与 kubernetes API 通信并创建 Longhorn Volume CRD(heml 安装直接创建了相关 CRD,查看代码后发现 Manager 里面似乎也会判断并创建一下),此后 Longhorn Manager watch 相关 CRD 资源和 Kubernetes 原生资源(PV/PVC…),一但集群内创建了 Longhorn Volume 则 Longhorn Manager 负责创建物理 Volume。
当 Longhorn Manager 创建 Volume 时,Longhorn Manager 首先会在 Volume 所在节点创建 Longhorn Engine 实例(对比实际行为后发现所谓的 “实例” 其实只是运行了一个 Linux 进程,并非创建 Pod),然后根据副本数量在所需放置副本的节点上创建对应的副本。
Longhorn Engine 始终与其使用 Volume 的 Pod 在同一节点上,它跨存储在多个节点上的多个副本同步复制卷;同时数据的多路径保证 Longhorn Volume 的 HA,单个副本或者 Engine 出现问题不会影响到所有副本或 Pod 对 Volume 的访问。
下图中展示了 Longhorn 的 HA 架构,每个 Kubernetes Volume 将会对应一个 Longhorn Engine,每个 Engine 管理 Volume 的多个副本,Engine 与 副本实质都会是一个单独的 Linux 进程运行:
注意: 图中的 Engine 并非是单独的一个 Pod,而是每一个 Volume 会对应一个 golang exec 出来的 Linux 进程。
CSI 部分不做过多介绍,具体参考 如何编写 CSI 插件;以下为简要说明:
Longhorn UI 向外暴露一个 Dashboard,并用过 Longhorn API 与 Longhorn Manager 控制平面交互;Longhorn UI 在架构上类似于 Longhorn CSI Plugin 的替代者,只不过一个是通过 Web UI 转化为 Longhorn API,另一个是将 CSI gRPC 转换为 Longhorn API。
在 Longhorn 微服务架构中,副本也作为单独的进程运行,其实质存储文件采用 Linux 的稀释文件方式;每个副本均包含 Longhorn Volume 的快照链,快照就像一个 Image 层,其中最旧的快照用作基础层,而较新的快照位于顶层。如果数据会覆盖旧快照中的数据,则仅将其包含在新快照中;整个快照链展示了数据的当前状态。
在进行快照时,Longhorn 会创建差异磁盘(differencing disk)文件,每个差异磁盘文件被看作是一个快照,当 Longhorn 读取文件时从上层开始依次查找,其示例图如下:
为了提高读取性能,Longhorn 维护了一个读取索引,该索引记录了每个 4K 存储块中哪个差异磁盘包含有效数据;读取索引会占用一定的内存,每个 4K 块占用一个字节,字节大小的读取索引意味着每个卷最多可以拍摄 254 个快照,在大约 1TB 的卷中读取索引大约会消耗256MB 的内存。
由于数据大小、网络延迟等限制,跨区域同步复制无法做到很高的时效性,所以 Longhorn 提供了称之为 Secondary Storage 的备份方案;Secondary Storage 依赖外部的 NFS、S3 等存储设施,一旦在 Longhorn 中配置了 Backup Storage,Longhorn 将会通过卷的指定版本快照完成备份;备份过程中 Longhorn 将会抹平快照信息,这意味着快照历史变更将会丢失,相同的原始卷备份是增量的,通过不断的应用差异磁盘文件完成;为了避免海量小文件带来的性能瓶颈,Longhorn 采用 2MB 分块进行备份,任何边界内 4k 块变动都会触发 2MB 块的备份行为;Longhorn 的备份功能为跨集群、跨区域提供完善的灾难恢复机制。Longhorn 备份机制如下图所示:
上面的大部分其实来源于对官方文档 Architecture and Concepts 的翻译;在翻译以及阅读文档过程中,通过对比文档与实际行为,还有阅读源码发现了一些细微差异,这里着重介绍一下这些 Pod 都是怎么回事:
longhorn-manager 与文档描述一致,其通过 Helm 安装时直接以 Daemonset 方式 Create 出来,然后 longhorn-manager 开启 HTTP API(9500) 等待其他组件请求;同时 longhorn-manager 还会使用 Operator 模式监听各种资源,包括不限于 Longhorn CRD 以及集群的 PV(C) 等资源,然后作出对应的响应。
Helm 安装时创建了 longhorn-driver-deployer Deployment,longhorn-driver-deployer 实际上也是 longhorn-manager 镜像启动,只不过启动后会沟通 longhorn-manager HTTP API,然后创建所有 CSI 相关容器,包括 csi-provisioner
、csi-snapshotter
、longhorn-csi-plugin
等。
上面所说的每个 Engine 对应一个 Linux 进程其实就是通过这个 Pod 完成的,instance-manager-e 由 longhorn-manager 创建,创建完成后 instance-manager-e 监听 gRPC 8500 端口,其只要职责就是接收 gRPC 请求,并启动 Engine 进程;从上面我们 Engine 介绍可以得知 Engine 与 Volume 绑定,所以理论上集群内 Volume 被创建时有某个 “东西” 创建了 CRD engines.longhorn.io
,然后又有人 watch 了 engines.longhorn.io
并通知 instance-manager-e 启动 Engine 进程;这里不负责任的推测是 longhorn-manager 干的,但是没看代码不敢说死…
同理 instance-manager-r
是负责启动副本的 Linux 进程的,工作原理与 instance-manager-e
相同,通过简单的查看代码(IDE 没打开…哈哈哈)推测,instance-manager-e/-r
应该是 longhorn-manager Operator 下的产物,其维护了一个自己的 “Daemonset”,但是 kubectl 是看不到的。
longhorn-ui 很简单,就是个 UI 界面,然后 HTTP API 沟通 longhorn-manager,这里不再做过多说明。
默认情况下 Helm 安装完成后会自动创建 StorageClass,如果集群中只有 Longhorn 作为存储,那么 Longhorn 的 StorageClass 将作为默认 StorageClass。关于 StorageClass、PV、PVC 如果使用这里不做过多描述,请参考官方 Example 文档;
需要注意的是 Longhorn 作为块存储仅支持 ReadWriteOnce
模式,如果想支持 ReadWriteMany
模式,则需要在节点安装 nfs-common
,Longhorn 将会自动创建 share-manager
容器然后通过 NFSV4 共享这个 Volume 从而实现 ReadWriteMany
;具体请参考 Support for ReadWriteMany (RWX) workloads。
如果出现磁盘损坏重建或者添加删除磁盘,请直接访问 UI 界面,通过下拉菜单操作即可;在操作前请将节点调整到维护模式并驱逐副本,具体请参考 Evicting Replicas on Disabled Disks or Nodes。
需要注意的是添加新磁盘时,磁盘挂载的软连接路径不能工作,请使用原始挂载路径或通过 mount --bind
命令设置新路径。
当创建好 Volume 以后可以用过 Longhorn UI 在线对 Volume 创建快照,但是回滚快照过程需要 Workload(Pod) 离线,同时 Volume 必须以维护模式 reattach 到某一个 Host 节点上,然后在 Longhorn UI 进行调整;以下为快照创建回滚测试:
test.pvc.yaml
1 |
|
test.po.yaml
1 |
|
首先创建相关资源:
1 |
|
创建完成后在 Longhorn UI 中可以看到刚刚创建出的 Volume:
点击 Name 链接进入到 Volume 详情,然后点击 Take Snapshot
按钮即可拍摄快照;有些情况下 UI 响应缓慢可能导致 Take Snapshot
按钮变灰,刷新两次即可恢复。
快照在回滚后仍然可以进行交叉创建
回滚快照时必须停止 Pod:
1 |
|
然后重新将 Volume Attach 到宿主机:
注意要开启维护模式
稍等片刻等待所有副本 “Running” 然后 Revert 即可
回滚完成后,需要 Detach Volume,以便供重新创建的 Pod 使用
除了手动创建快照之外,Longhorn 还支持定时对 Volume 进行快照处理;要使用定时任务,请进入 Volume 详情页面,在 Recurring Snapshot and Backup Schedule
选项卡下新增定时任务即可:
如果不想为内核 Volume 都手动设置自动快照,可以用过调整 StorageClass 来实现为每个自动创建的 PV 进行自动快照,具体请阅读 Set up Recurring Jobs using a StorageClass 文档。
Longhorn 支持对 Volume 进行扩容,扩容方式和回滚快照类似,都需要 Deacth Volume 并开启维护模式。
首先停止 Workload
1 |
|
然后直接使用 kubectl
编辑 PVC,调整 spec.resources.requests.storage
保存后可以从 Longhorn UI 中看到 Volume 在自动 resize
重新创建 Workload 可以看到 Volume 已经扩容成功
1 |
|
Volume 扩展过程中 Longhorn 会自动处理文件系统相关调整,但是并不是百分百会处理,一般 Longhorn 仅在以下情况做自动处理:
block device
作为 frontend非这几种情况外,如还原到更小容量的 Snapshot,可能需要手动调整文件系统,具体请参考 Filesystem expansion 章节文档。
总体来说目前 Longhorn 是一个比较清量级的存储解决方案,微服务化使其更加可靠,同时官方文档完善社区响应也比较迅速;最主要的是 Longhorn 采用的技术方案不会过于复杂,通过文档以及阅读源码至少可以比较快速的了解其背后实现,而反观一些其他大型存储要么文档不全,要么实现技术复杂,普通用户很难窥视其核心;综合来说在小型存储选择上比较推荐 Longhorn,至于稳定性么,很不负责的说我也不知道,毕竟我也是新手,备份还没折腾呢…
]]>就目前来说,Caddy 对于我个人印象唯一的缺点就是性能没有 Nginx 高,但是这是个仁者见仁智者见智的问题;相较于提供的这些便利性,在性能可接受的情况下完全有理由切换到 Caddy。
注意: 在 Caddy1 时代,Caddy 官方发布的预编译二进制文件是不允许进行商业使用的,Caddy2 以后已经全部切换到 Apache 2.0 License,具体请参考 issue#2786。
在默认情况下 Caddy2 官方提供了预编译的二进制文件,以及自定义 build 下载页面,不过对于需要集成一些第三方插件时,我们仍需采用官方提供的 xcaddy 来进行自行编译;以下为具体的编译过程:
本部分编译环境默认为 Ubuntu 20.04 系统,同时使用 root 用户,其他环境请自行调整相关目录以及配置;编译时自行处理好科学上网相关配置,也可以直接用国外 VPS 服务器编译。
首先下载 go 语言的 SDK 压缩包,其他平台可以从 https://golang.org/dl/ 下载对应的压缩包:
1 |
|
下载完成后解压并配置相关变量:
1 |
|
配置完成后,应该在命令行执行 go version
有成功返回:
1 |
|
按照官方文档直接命令行执行 go get -u github.com/caddyserver/xcaddy/cmd/xcaddy
安装即可:
1 |
|
安装完成后应当在命令行可以直接执行 xcaddy
命令:
1 |
|
编译之前系统需要安装 jq
、curl
、git
命令,没有的请使用 apt install -y curl git jq
命令安装;
自行编译的目的是增加第三方插件方便使用,其中官方列出的插件可以从 Download 页面获取到:
其他插件可以从 GitHub 上寻找或者自行编写,整理好这些插件列表以后只需要使用 xcaddy
编译即可:
1 |
|
编译过程日志如下所示,稍等片刻后将会生成编译好的二进制文件:
编译成功后可以通过 list-modules
子命令查看被添加的插件是否成功编译到了 caddy 中:
1 |
|
宿主机安装 Caddy2 需要使用 systemd 进行守护,幸运的是 Caddy2 官方提供了各种平台的安装包以及 systemd 配置文件仓库;目前推荐的方式是直接采用包管理器安装标准版本的 Caddy2,然后替换自编译的可执行文件:
1 |
|
Docker 用户可以通过 Dockerfile 自行编译 image,目前我编写了一个基于 xcaddy 的 Dockerfile,如果有其他插件需要集成自行修改重新编译即可;当前 Dockerfile 预编译的镜像已经推送到了 Docker Hub 中,镜像名称为 mritd/caddy
。
Caddy2 的配置文件核心采用 json,但是 json 可读性不强,所以官方维护了一个转换器,抽象出称之为 Caddyfile 的新配置格式;关于 Caddyfile 的完整语法请查看官方文档 https://caddyserver.com/docs/caddyfile,本文仅做一些基本使用的样例。
Caddyfile 支持类似代码中 function 一样的配置片段,这些配置片段可以在任意位置被 import
,同时可以接受参数,以下为配置片断示例:
1 |
|
import
指令除了支持引用配置片段以外,还支持引用外部文件,同时支持通配符,有了这个命令以后我们就可以方便的将配置文件进行模块化处理:
1 |
|
针对于站点域名配置,Caddyfile 比较自由化,其格式如下:
1 |
|
关于这个 “地址” 接受多种格式,以下都为合法的地址格式:
localhost
example.com
:443
http://example.com
localhost:8080
127.0.0.1
[::1]:2015
example.com/foo/*
*.example.com
http://
Caddyfile 支持直接引用系统环境变量,通过此功能可以将一些敏感信息从配置文件中剔除:
1 |
|
针对于配置片段,Caddyfile 还支持类似于函数代码的参数支持,通过参数支持可以让外部引用时动态修改配置信息:
1 |
|
在启动 Caddy2 之前,如果目标域名(例如: www.example.com
)已经解析到了本机,那么 Caddy2 启动后会尝试自动通过 ACME HTTP 挑战申请证书;如果期望使用 DNS 的方式申请证书则需要其他 DNS 插件支持,比如上面编译的 --with github.com/caddy-dns/gandi
为 gandi 服务商的 DNS 插件;关于使用 DNS 挑战的配置编写方式需要具体去看其插件文档,目前 gandi 的配置如下:
1 |
|
配置完成后 Caddy2 会通过 ACME DNS 挑战申请证书,值得注意的是即使通过 DNS 申请证书默认也不会申请泛域名证书,如果想要调整这种细节配置请使用 json 配置或管理 API。
了解了以上基础配置信息,我们就可以实际编写一个站点配置了;以下为本站的 Caddy 配置样例:
目录结构:
1 |
|
Caddyfile 主要包含一些通用的配置,并将其抽到配置片段中,类似与 nginx 的 nginx.conf
主配置;在最后部分通过 import
关键字引入其他具体站点配置,类似 nginx 的 vhost
配置。
1 |
|
mritd.com.caddy
为主站点配置,主站点配置内主要编写一些路由规则,TLS 等都从配置片段引入,这样可以保持统一。
1 |
|
mritd.me.caddy
为老站点配置,目前主要将其 301 到新站点即可。
1 |
|
配置文件编写完成后,通过 systemctl start caddy
可启动 caddy 服务器;每次配置修改后可以通过 systemctl reload caddy
进行配置重载,重载期间 caddy 不会重启(实际上调用 caddy reload
命令),当配置文件书写错误时,重载只会失败,不会影响正在运行的 caddy 服务器。
本文只是列举了一些简单的 Caddy 使用样例,在强大的插件配合下,Caddy 可以实现各种 “神奇” 的功能,这些功能依赖于复杂的 Caddy 配置,Caddy 配置需要仔细阅读官方文档,关于 Caddyfile 的每个配置段在文档中都有详细的描述。
值得一提的是 Caddy 本身内置了丰富的插件,例如内置 “file_server”、内置各种负载均衡策略等,这些插件组合在一起可以实现一些复杂的功能;Caddy 是采用 go 编写的,官方也给出了详细的开发文档,相较于 Nginx 来说通过 Lua 或者 C 来开发编写插件来说,Caddy 的插件开发上手要容易得多;Caddy 本身针对数据存储、动态后端、配置文件转换等都内置了扩展接口,这为有特定需求的扩展开发打下了良好基础。
最终总结,综合来看目前 Caddy2 的性能损失可接受的情况下,相较于 Nginx 绝对是个绝佳选择,各种新功能都能够满足现代化 Web 站点的需求,真香警告。
]]>在 Skywalking 刚发布的时候就开始关注这个玩意了,一直没有时间去测试;最近正好新项目上线,顺手把 Skywalking 搞起来了,下面简单记录一下 Kubernetes 下的安装使用。
确保有一套运行正常的 Kubernetes 集群,本文默认为使用 Elasticsearch7 作为后端存储;如果想把 ES 放到 Kubernetes 集群里那么还得确保集群配置了正确的存储,譬如默认的 StorageClass 可用等。本文为了方便起见(其实就是穷)采用外部 ES 存储且使用 docker-compose 单节点部署,所以不需要集群的分布式存储;最后确保你本地的 kubectl
能够正常运行。
Skywalking 在大体上(不准确)分为四大部分:
Elasticsearch 当前使用 7.9.2 版本,由于只是初次尝试还处于测试阶段所以直接 docker-compose 启动一个单点:
1 |
|
由于 Skywalking 官方给出的 Kubernetes 安装方式为 Helm 安装,所以需要本地先安装 Helm;Helm 安装方式非常简单,根据官方文档在网络没问题的情况下直接执行以下命令即可:
1 |
|
如果网络不是那么 OK 的情况下请参考官方文档的包管理器方式安装或直接下载二进制文件安装。
Helm 部署之前按照官方文档提示需要先初始化 Helm 仓库:
1 |
|
Helm 初始化完成后需要自行调整配置文件,配置 oap-server 使用外部 ES
values-my-es.yaml
1 |
|
调整好配置后只需要使用 Helm 安装即可:
1 |
|
如果安装出错或者其他问题可以使用以下命令进行卸载:
1 |
|
安装成功后应该在 ${SKYWALKING_RELEASE_NAMESPACE}
下看到相关 Pod:
1 |
|
在确认 Pod 都运行正常后可以通过 kubectl port-forward
命令来查看 UI 界面:
1 |
|
在生产环境可能需要配置正确的 Ingress 或者 NodePort 等方式暴露 skywalking-ui 服务,具体取决于生产集群服务暴露方式,请自行调整。
由于目前仅在 Java 项目上测试,所以以下 Agent 配置仅仅对 Java 项目有效。
Skywalking 在简单使用时不需要侵入代码,对于 jar 包启动的项目只需要在启动时增加 -javaagent
选项即可。
javaagent
可以通过下载对应的 skywalking release 安装包获取,将此 agent
目录解压到任意位置,稍后将添加到 java 启动参数。
Agent 主配置文件存放在 config/agent.config
配置文件中,配置文件内支持环境变量读取,可以自行添加其他配置和引用其他变量;通常这个配置文件在容器化时有两种选择,一种是创建 ConfigMap,然后通过 ConfigMap 挂载到容器里进行覆盖;另一种是在默认配置里引用各种变量,在容器启动时通过环境变量注入。这里暂时使用环境变量注入的方式:
agent.config
deployment.yml
调整完成后,应用运行一段时间后应该能在 UI 中看到数据
http(s)_proxy
...( _ _)ノ|壁
(自行体会这个表情)-javaagent
不能放在 -jar
选项之后,否则可能不生效skywalking-oap.skywalking.svc.cluster.local
域名服务发现方式寻址目前宿主机上全部采用的 dnsmasq 作为 DNS 管理,其中有一个很大的问题是需要进行 DNS 冗余,dnsmasq 每次修改都要多台机器同步,所以自己写了一个插件配合 CoreDNS 实现分布式部署,如果想了解插件编写方式请参考 Writing Plugin for Coredns。
etcdhosts 顾名思义,就是将 hosts 文件存储在 Etcd 中,然后多个 CoreDNS 共享一份 hosts 文件;得益于 Etcd 提供的 watch 功能,当 Etcd 中的 hosts 文件更新时,每台 CoreDNS 服务器都会接到推送,同时完成热重载;etcdhosts 基本架构如下:
1 |
|
etcdhosts release 页已经提供部分版本的预编译文件,可以直接下载使用。
etcdhosts 作为一个 CoreDNS 扩展插件采用直接偶合的方式编写(未采用 gRPC 是因为考虑性能影响),这意味着需要重新编译 CoreDNS 来集成插件,以下为 CoreDNS 编译过程(使用 docker):
1 |
|
编译完成后将在 build
目录下生成各个平台的二进制文件压缩包。
Etcd 集群搭建将直接采用 deb 安装包,具体细节这里不再阐述,本次搭建系统为 Ubuntu 20,以下为搭建步骤。
1 |
|
创建证书需要先修改证书配置文件(etcd-csr.json
)然后借助 cfssl 工具来创建证书
/etc/etcd/cfssl/etcd-csr.json
1 |
|
通过脚本创建证书
1 |
|
证书创建完成后需要分发到其他两台机器上,保证三台节点的 /etc/etcd/ssl
目录证书相同。
1 |
|
证书签署完成后,简单的调整每台机器上的集群节点配置即可
/etc/etcd/etcd.conf
1 |
|
最后每台机器执行 systemctl start etcd
启动即可,验证集群是否健康可以使用如下命令测试:
1 |
|
系统级 CoreDNS 安装推荐直接使用 systemd 管理,官方目前提供了 systemd 相关配置文件: https://github.com/coredns/deployment/tree/master/systemd
1 |
|
etcdhosts 的配置类似官方的 etcd 插件,其配置格式如下:
1 |
|
以下是一个简单的可启动的样例配置:
/etc/coredns/Corefile
1 |
|
由于 etcdhosts 插件需要连接 etcd 集群,所以需要将证书复制到 Corefile
指定的位置:
1 |
|
最后直接启动即可(首次启动会出现 [ERROR] plugin/etcdhosts: invalid etcd response: 0
错误,属于正常情况):
1 |
|
最后在多台机器上通过同样的配置启动 CoreDNS 即可,此时所有 CoreDNS 服务器通过 Etcd 提供一致性的记录解析。
所有 CoreDNS 启动成功后,默认 etcdhosts 插件将会读取 Etcd 中的 /etcdhosts
key 作为 hosts 文件载入;载入成功后将会在内存级进行 Cache,多次查询不会造成疯狂的 Etcd 请求,只有当触发 reload 时(包括 Etcd 更新)才会重新查询 Etcd。所以此时只需要向 Etcd 的 /etcdhosts
key 写入一个 hosts 文件即可;写入 Etcd 可以使用 etcdctl 以及其他的开源工具,甚至自己开发都可以,记录更改只需要跟 Etcd 打交道,不需要理会 CoreDNS;由于本人实在是比较菜,前端页面写不出来,所以弄了一个命令行版本的工具: dnsctl
dnsctl 只有一个可执行文件,默认情况下 dnsctl 读取 $HOME/.dnsctl.yaml
配置文件来沟通 Etcd,配置文件格式如下:
1 |
|
dnsctl 提供如下命令
1 |
|
其中 edit
命令将会打开系统默认编辑器(例如 vim),然后编辑完保存后会自动上传到 Etcd 中,此后 CoreDNS 的 etcdhosts 插件将会立即重载;**dump
命令用于将 Etcd 中的 hosts 文件保存到本地用于备份,upload
命令可以将已有的 hosts 文件上传到 Etcd 用于恢复。**
由于主板不支持设置 CFG Lock,所以只能借助第三方工具强行解锁;首要前提是需要知道 CFG Lock 的设置地址,不同型号主板甚至不同版本的 BIOS 都不一定相同,所以 CFG Lock 地址不要照搬;本次操作用到的工具如下:
setup_var_3
命令来修改 CFG Lockmac 下打开 UEFITool,选择 File > Open image file
,文件选择框点击选项按钮切换成 All files
模式否则由于文件扩展名不同可能无法选中,然后选择主板 BIOS 文件。
然后 command + F
切换到 Text
模式搜索 CFG Lock
,接着双击下面的搜索结果会定位到对应的 Section。
接下来右键 Extract body
导出到桌面等任意文件夹既可。
提取到 [Section Name].efi
文件后命令行执行 ifrextract [Section Name].efi cfg.txt
导出为文本。
导出文本后通过编辑器搜索 CFG Lock
字符串,其中 VarStoreInfo
后面的地址就是 CFG Lock 设置地址,请记录这个地址(最好用手机拍照)。
得到了 CFG Lock 地址以后一切都简单了,创建一个启动 U 盘然后执行命令既可;首先将 U 盘格式化为 GUID 分区表,然后挂载 EFI 分区,将 modGRUBShell.efi
重命名为 BOOTX64.efi
并放入 EFI/BOOT
目录。
最后重启系统 BIOS 选择使用 U 盘启动,并在 grub shell 内执行 setup_var_3 0x529 0x0
然后重启即完成解锁;注意: 0x529
请替换为上面找到的实际地址,实际地址 0x***
后面的 ***
如果有大写字母请保持大写;这部份就不上图了,懒得拍照。
Hexo 安装根据官方文档直接操作即可,安装前提是需要先安装 Nodejs(这里不再阐述直接略过)
1 |
|
Hexo 命令行工具安装完成后可以直接初始化一个样例项目,init 过程会 clone https://github.com/hexojs/hexo-starter.git
到本地,同时自动安装好相关依赖
1 |
|
进入目录启动样例站点
1 |
|
基本的样例博客启动完成后就需要选择一个主题,主题实质上才决定博客功能,这里目前使用了 Fluid 主题,这个主题目前兼具了个人博客所需的所有功能,而且作者提交比较活跃,文档也比较全面。
1 |
|
接下来修改 _config.yml
配置切换主题即可
1 |
|
然后重新启动博客进行预览: hexo cl && hexo s
关于主题其他配置可自行阅读 官方文档,文档有时可能更新不及时,可同时参考仓库内的 _config.yml
配置。
关于 jekyll 博客的文章如何导入到 Hexo 中网上有很多脚本;但是实际上两个静态博客框架都是支持标准的 Markdown 语法书写的文章进行渲染,唯一区别就是每篇文章上的 “头”。
1 |
|
所以直接复制 jekyll 的 md 文件到 source/_posts
目录,并修改文档头部即可。
目前博客部署在自己的 VPS 上,以前都是将博客生成的静态直接使用 nginx 发布出去的;但是面临的问题就是每次博客更新都要手动去 VPS 更新,虽然可以写一些 CI 脚本但是并不算智能;得益于 Golang 官方完善的标准库支持,这次直接几行代码写一个静态服务器,同时拦截特定 URL 来更新博客:
1 |
|
有了上面的静态服务器,写个 Dockerfile 将 Hexo 生成的静态文件打包即可:
1 |
|
镜像运行后将使用 /data
目录最为静态文件目录进行发布,Hexo 生成的静态文件(public 目录)也会完整的 clone 到当前目录,此后使用 POST 请求访问 /update
即可触发从 Github 更新博客内容。
所有就绪以后在主仓库增加 .travis.yml
配置来联动 travis ci;由于每次 push 到 Github 的内容实际上已经是本地生成的 public 目录,所以 CI 只需要通知服务器更新即可;强迫症又加了一个 Telegram 通知,每次触发更新完成后 Telegram 再给自己推送一下:
1 |
|
由于目前一些配图啥的还是存储在服务器本地,所以图片等比较大的静态文件仍然是访问瓶颈,这时候可以借助 gulp 来压缩并进行优化:
1 |
|
接下来编写 gulpfile.js
指定相关的优化任务
1 |
|
最后在每次部署时执行一下 gulp
命令即可完成优化: hexo cl && hexo g && gulp
在 Kubernetes 整个请求链路中,请求通过认证和授权之后、对象被持久化之前需要通过一连串的 “准入控制拦截器”;这些准入控制器负载验证请求的合法性,必要情况下也可以对请求进行修改;默认准入控制器编写在 kube-apiserver 的代码中,针对于当前 kube-apiserver 默认启用的准入控制器你可以通过以下命令查看:
1 |
|
具体每个准入控制器的作用可以通过 Using Admission Controllers 文档查看。在这些准入控制器中有两个特殊的准入控制器 MutatingAdmissionWebhook
和 ValidatingAdmissionWebhook
。这两个准入控制器以 WebHook 的方式提供扩展能力,从而我们可以实现自定义的一些功能。当我们在集群中创建相关 WebHook 配置后,我们配置中描述的想要关注的资源在集群中创建、修改等都会触发 WebHook,我们再编写具体的应用来响应 WebHook 即可完成特定功能。
动态准入控制实际上指的就是上面所说的两个 WebHook,在使用动态准入控制时需要一些先决条件:
admissionregistration.k8s.io/v1 API
)或者 v1.9 (以便使用 admissionregistration.k8s.io/v1beta1
API)。admissionregistration.k8s.io/v1
或 admissionregistration.k8s.io/v1beta1
API。如果要使用 Mutating Admission Webhook,在满足先决条件后,需要在系统中 create 一个 MutatingWebhookConfiguration:
1 |
|
同样要使用 Validating Admission Webhook 也需要类似的配置:
1 |
|
从配置文件中可以看到,webhooks.rules
段落中具体指定了我们想要关注的资源及其行为,webhooks.clientConfig
中指定了 webhook 触发后将其发送到那个地址以及证书配置等,这些具体字段的含义可以通过官方文档 Dynamic Admission Control 来查看。
值得注意的是 Mutating Admission Webhook 会在 Validating Admission Webhook 之前触发;Mutating Admission Webhook 可以修改用户的请求,比如自动调整镜像名称、增加注解等,而 Validating Admission Webhook 只能做校验(true or false),不可以进行修改操作。
郑重提示: 本部分文章请结合 goadmission 框架源码进行阅读。
在编写之前一般我们先大体了解一下流程并制订方案再去实现,边写边思考适合在细节实现上,对于整体的把控需要提前作好预习。针对于这个准入控制的 WebHook 来说,根据其官方文档大致总结重点如下:
request.uid
)有了以上信息以后便可以知道编写 WebHook 需要的东西,根据这些信息目前我作出的大体方案如下:
gorilla/mux
。基于 3.1 部分的分析可以知道,WebHook 接收和响应都是一个 AdmissionReview 对象,在查看源码以后可以看到 AdmissionReview 结构如下:
从代码的命名中可以很清晰的看出,在请求发送到 WebHook 时我们只需要关注内部的 AdmissionRequest(实际入参),在我们编写的 WebHook 处理完成后只需要返回包含有 AdmissionResponse(实际返回体) 的 AdmissionReview 对象即可;总的来说 AdmissionReview 对象是个套壳,请求是里面的 AdmissionRequest,响应是里面的 AdmissionResponse。
有了上面的一些基础知识,我们就可以简单的实行一个什么也不干的 WebHook 方法(本地无法直接运行,重点在于思路):
1 |
|
上面这个 printRequest
方法最细粒度的控制到只面向我们的实际请求和响应;而对于 WebHook Server 来说其接到的是 http 请求,所以我们还需要在外面包装一下,将 http 请求转换为 AdmissionReview 并提取 AdmissionRequest 再调用上面的 printRequest
来处理,最后将返回结果重新包装为 AdmissionReview 重新返回;整体的代码如下
1 |
|
编写了简单的 Hello World 以后可以看出,真正在编写时我们需要实现的都是处理 AdmissionRequest 并返回 AdmissionResponse 这部份(printRequest);外部的包装为 AdmissionReview、复制 UID、复制 TypeMeta 等都是通用的方法,所以基于这一点我们可以进行适当的抽象:
针对每一个贴合业务的 WebHook 来说,其大致有三大属性:
我们将其抽象为 AdmissionFunc 结构体以后如下所示
1 |
|
我们知道 WebHook 是基于 HTTP 的,所以上面抽象出的 AdmissionFunc 还不能直接用在 HTTP 请求代码中;如果直接偶合到 HTTP 请求代码中,我们就没法为 HTTP 代码再增加其他拦截路径等等特殊的底层设置;所以站在 HTTP 层面来说还需要抽象一个 “更高层面的且包含 AdmissionFunc 全部能力的 HandleFunc” 来使用;HandleFunc 抽象 HTTP 层面的需求:
以下为 HandleFunc 的抽象:
1 |
|
有了以上两个角度的抽象,再结合 命令行参数解析、日志处理、配置文件读取等等,我揉合出了一个 goadmission 框架,以方便动态准入控制的快速开发。
1 |
|
由于框架已经作好了路由注册等相关抽象,所以只需要新建 go 文件,然后通过 init 方法注册到全局 WebHook 组中即可,新编写的 WebHook 对已有代码不会有任何侵入:
需要注意的是所有 validating 类型的 WebHook 会在 URL 路径前自动拼接 /validating
路径,mutating 类型的 WebHook 会在 URL 路径前自动拼接 /mutating
路径;这么做是为了避免在更高层级的 HTTP Route 上添加冲突的路由。
所以一切准备就绪以后,就需要 “不忘初心”,撸一个自动修改镜像名称的 WebHook:
1 |
|
在搭建 Kubernetes 集群的过程中首先要搞定 Etcd 集群,虽然说 kubeadm 工具已经提供了默认和 master 节点绑定的 Etcd 集群自动搭建方式,但是我个人一直是手动将 Etcd 集群搭建在宿主机;因为这个玩意太重要了,毫不夸张的说 kubernetes 所有组件崩溃我们都能在一定时间以后排查问题恢复,但是一旦 Etcd 集群没了那么 Kubernetes 集群也就真没了。
在很久以前我创建了 edep 工具来实现 Etcd 集群的辅助部署,再后来由于我们的底层系统偶合了 Ubuntu,所以创建了 etcd-deb 项目来自动打 deb 包来直接安装;最近逛了一下 Kubernetes 的相关项目,发现跟我的 edep 差不多的项目 etcdadm,试了一下 “真香”。
etcdadm 项目是使用 go 编写的,所以很明显只有一个二进制下载下来就能用:
1 |
|
类似 kubeadm 一样,etcdadm 也是先启动第一个节点,然后后续节点直接 join 即可;第一个节点启动只需要执行 etcdadm init
命令即可:
1 |
|
从命令行输出可以看到不同阶段 etcdadm 的相关日志输出;在 init
命令时可以指定一些特定参数来覆盖默认行为,比如版本号、安装目录等:
1 |
|
在首个节点启动完成后,将集群 ca 证书复制到其他节点然后执行 etcdadm join ENDPOINT_ADDRESS
即可:
1 |
|
在目前 etcdadm 尚未支持配置文件,目前所有默认配置存放在 constants.go 中,这里面包含了默认安装位置、systemd 配置、环境变量配置等,限于篇幅请自行查看代码;下面简单介绍一些一些刚须的配置:
etcdctl 默认安装在 /opt/bin
目录下,同时你会发现该目录下还存在一个 etcdctl.sh
脚本,这个脚本将会自动读取 etcdctl 配置文件(/etc/etcd/etcdctl.env
),所以推荐使用这个脚本来替代 etcdctl 命令。
默认的数据目录存储在 /var/lib/etcd
目录,目前 etcdadm 尚未提供任何可配置方式,当然你可以自己改源码。
配置文件总共有两个,一个是 /etc/etcd/etcdctl.env
用于 /opt/bin/etcdctl.sh
读取;另一个是 /etc/etcd/etcd.env
用于 systemd 读取并启动 etcd server。
其实很久以前由于我自己部署方式导致了我一直以来理解的一个错误,我一直以为 etcd server 证书要包含所有 server 地址,当然这个想法是怎么来的我也不知道,但是当我看了以下 Join 操作源码以后突然意识到 “为什么要包含所有?包含当前 server 不就行了么。”;当然对于 HTTPS 证书的理解一直是明白的,但是很奇怪就是不知道怎么就产生了这个想法(哈哈,我自己都觉的不可思议)…
MemberAdd
添加新集群目前 etcdadm 虽然已经基本生产可用,但是仍有些不足的地方:
在 Kubernetes 以前的版本中,其所有受官方支持的存储驱动全部在 Kubernetes 的主干代码中,其他第三方开发的自定义插件通过 FlexVolume 插件的形势提供服务;相对于 kubernetes 的源码树来说,内置的存储我们称之为 “树内存储”,外部第三方实现我们称之为 “树外存储”;在很长一段时间里树内存储和树外存储并行开发和使用,但是随着时间推移渐渐的就出现了很严重的问题:
为了解决这种尴尬的问题,Kubernetes 必须抽象出一个合适的存储接口,并将所有存储驱动全部适配到这个接口上,存储驱动最好与 Kubernetes 之间进行 RPC 调用完成解耦,这样就造就了 CSI(Container Storage Interface)。
在开发 CSI 之前我们最好熟悉一下 CSI 开发中的一些常识;了解过 Kubernetes API 开发的朋友应该清楚,所有的资源定义(Deployment、Service…)在 Kubernetes 中其实就是一个 Object,此时可以将 Kubernetes 看作是一个 Database,无论是 Operator 还是 CSI 其核心本质都是不停的 Watch 特定的 Object,一但 kubectl 或者其他客户端 “动了” 这个 Object,我们的对应实现程序就 Watch 到变更然后作出相应的响应;对于 CSI 编写者来说,这些 Watch 动作已经不必自己实现 Custom Controller,官方为我们提供了 CSI Sidecar Containers;并且在新版本中这些 Sidecar Containers 实现极其完善,比如自动的多节点 HA(Etcd 选举)等。
所以到迄今为止,所谓的 CSI 插件开发事实上并非面向 Kubernetes API 开发,而是面向 Sidecar Containers 的 gRPC 开发,Sidecar Containers 一般会和我们自己开发的 CSI 驱动程序在同一个 Pod 中启动,然后 Sidecar Containers Watch API 中 CSI 相关 Object 的变动,接着通过本地 unix 套接字调用我们编写的 CSI 驱动:
目前官方提供的 Sidecar Containers 如下:
每个 Sidecar Container 的作用可以通过对应链接查看,需要注意的是 cluster-driver-registrar 已经停止维护,请改用 node-driver-registrar。
在理解了 CSI Sidecar Containers 以后,我们仍需要大致的了解 CSI 挂载过程中的大致流程,以此来针对性的实现每个阶段所需要的功能;CSI 整个流程实际上大致分为以下三大阶段:
Provisioning and Deleting 阶段实现与外部存储供应商协调卷的创建/删除处理,简单地说就是需要实现 CreateVolume 和 DeleteVolume;假设外部存储供应商为阿里云存储那么此阶段应该完成在阿里云存储商创建一个指定大小的块设备,或者在用户删除 volume 时完成在阿里云存储上删除这个块设备;除此之外此阶段还应当响应存储拓扑分布从而保证 volume 分布在正确的集群拓扑上(此处描述不算清晰,推荐查看设计文档)。
Attaching and Detaching 阶段实现将外部存储供应商提供好的卷设备挂载到本地或者从本地卸载,简单地说就是实现 ControllerPublishVolume 和 ControllerUnpublishVolume;同样以外部存储供应商为阿里云存储为例,在 Provisioning 阶段创建好的卷的块设备,在此阶段应该实现将其挂载到服务器本地或从本地卸载,在必要的情况下还需要进行格式化等操作。
这个阶段在 CSI 设计文档中没有做详细描述,在前两个阶段完成后,当一个目标 Pod 在某个 Node 节点上调度时,kubelet 会根据前两个阶段返回的结果来创建这个 Pod;同样以外部存储供应商为阿里云存储为例,此阶段将会把已经 Attaching 的本地块设备以目录形式挂载到 Pod 中或者从 Pod 中卸载这个块设备。
CSI 的三大阶段实际上更细粒度的划分到 CSI Sidecar Containers 中,上面已经说过我们开发 CSI 实际上是面向 CSI Sidecar Containers 编程,针对于 CSI Sidecar Containers 我们主要需要实现以下三个 gRPC Server:
在当前 CSI Spec v1.3.0 中 IdentityServer 定义如下:
1 |
|
从代码上可以看出 IdentityServer 主要负责像 Kubernetes 提供 CSI 插件名称可选功能等,所以此 Server 是必须实现的。
同样当前 CSI v1.3.0 Spec 中 NodeServer 定义如下:
1 |
|
在最小化的实现中,NodeServer 中仅仅需要实现 NodePublishVolume
、NodeUnpublishVolume
、NodeGetCapabilities
三个方法,在 Mount 阶段 kubelet 会通过 node-driver-registrar 容器调用这三个方法。
在当前 CSI Spec v1.3.0 ControllerServer 定义如下:
1 |
|
从这些方法上可以看出,大部分的核心逻辑应该在 ControllerServer 中实现,比如创建/销毁 Volume,创建/销毁 Snapshot 等;在一般情况下我们自己编写的 CSI 都会实现 CreateVolume
和 DeleteVolume
,至于其他方法根据业务需求以及外部存储供应商实际情况来决定是否进行实现。
从这个部署架构图上可以看出在实际上 CSI 部署时,Mount and Umount 阶段(对应 Node Server 实现)以 Daemonset 方式保证其部署到每个节点,当 Volume 创建完成后由其挂载到 Pod 中;其他阶段(Provisioning and Deleting 和 Attaching and Detaching) 只要部署多个实例保证 HA 即可(最新版本的 Sidecar Containers 已经实现了多节点自动选举);每次 PV 创建时首先由其他两个阶段的 Sidecar Containers 做处理,处理完成后信息返回给 Kubernetes 再传递到 Node Driver(Node Server) 上,然后 Node Driver 将其 Mount 到 Pod 中。
根据以上文档的描述,针对于需要编写一个 NFS CSI 插件这个需求,大致我们可以作出如下分析:
CreateVolume
和 DeleteVolume
逻辑,其核心逻辑应该是针对每个 PV 在 NFS Server 目录下执行 mkdir
,并将生成的目录名称等信息返回给 Kubernetes。NodePublishVolume
和 NodeUnpublishVolume
方法,然后将上一阶段提供的目录名称等信息组合成挂载命令 Mount 到 Pod 即可。在明确了这个需求以后我们需要开始编写 gRPC Server,当然不能盲目的自己乱造轮子,因为这些 gRPC Server 需要是 NonBlocking
的,所以最佳实践就是参考官方给出的样例项目 csi-driver-host-path,这是一名合格的 CCE 必备的技能(CCE = Ctrl C + Ctrl V + Engineer)。
针对官方给出的 CSI 样例,首先把源码弄到本地,然后通过 IDE 打开;这里默认为读者熟悉 Go 语言相关语法以及 go mod 等依赖配置,开发 IDE 默认为 GoLand
从源码树上可以看到,hostpath 的 CSI 实现非常简单;首先是 cmd
包下的命令行部分,main 方法在这里定义,然后就是 pkg/hostpath
包的具体实现部分,CSI 需要实现的三大 gRPC Server 全部在此。
cmd
包下主要代码就是一些命令行解析,方便从外部传入一些参数供 CSI 使用;针对于 NFS CSI 我们需要从外部传入 NFS Server 地址、挂载目录等参数,如果外部存储供应商为其他云存储可能就需要从命令行传入 AccessKey、AccessToken 等参数。
目前 go 原生的命令行解析非常弱鸡,所以更推荐使用 cobra 命令行库完成解析。
从上面命令行解析的图中可以看到,在完成命令行解析后交由 handle
方法处理;handle
方法很简单,通过命令行拿到的参数创建一个 hostpath
结构体指针,然后 Run
起来就行了,所以接下来要着重看一下这个结构体
从代码上可以看到,hostpath
结构体内有一系列的字段用来存储命令行传入的特定参数,然后还有三个 gRPC Server 的引用;命令行参数解析完成后通过 NewHostPathDriver
方法设置到 hostpath
结构体内,然后通过调用结构体的 Run
方法创建三个 gRPC Server 并运行
经过这么简单的一看,基本上一个最小化的 CSI 代码分布已经可以出来了:
cmd
包server.go
中的 NewNonBlockingGRPCServer
方法将其启动(这里也可以看出 server.go 里面的方法我们后面可以 copy 直接用)项目骨架已经提交到 Github mritd/csi-archetype 项目,可直接 clone 并使用。
大致的研究完 Hostpath 的 CSI 源码,我们就可以根据其实现细节抽象出一个项目 CSI 骨架:
在这个骨架中我们采用 corba 完成命令行参数解析,同时使用 logrus 作为日志输出库,这两个库都是 Kubernetes 以及 docker 比较常用的库;我们创建了一个叫 archetype
的结构体作为 CSI 的主承载类,这个结构体需要定义一些参数(parameter1…)方便后面初始化相关 gRPC Server 实现相关调用。
1 |
|
与 Hostpath CSI 实现相同,我们创建一个 NewCSIDriver
方法来返回 archetype
结构体实例,在 NewCSIDriver
方法中将命令行解析得到的相关参数设置进结构体中并添加一些 AccessModes
和 ServiceCapabilities
方便后面 Identity Server
调用。
1 |
|
整个骨架源码树中,命令行解析自己重构使用一些更加方便的命令行解析、日志输出库;结构体部分参考 Hostpath 结构体自己调整,server.go
用来创建 NonBlocking
的 gRPC Server(直接从 Hotspath 样例项目 copy 即可);然后就是三大 gRPC Server 的实现,由于是 “项目骨架” 所以相关方法我们都返回未实现,后续我们主要来实现这些方法就能让自己写的这个 CSI 插件 work。
有了 CSI 的项目骨架以后,我们只需要简单地修改名字将其重命名为 NFS CSI 插件即可;由于这篇文章是先实现好了 NFS CSI(已经 work) 再来写的,所以 NFS CSI 的源码可以直接参考 Gozap/csi-nfs 即可,下面的部分主要介绍三大 gRPC Server 的实现
Identity Server 实现相对简单,总共就三个接口;GetPluginInfo
接口返回插件名称版本即可(注意版本号好像只能是 1.1.1
这种,v1.1.1
好像会报错);Probe
接口用来做健康检测可以直接返回空 response 即可,当然最理想的情况应该是做一些业务逻辑判活;GetPluginCapabilities
接口看起来简单但是要清楚返回的 Capabilities
含义,由于我们的 NFS 插件必然需要响应 CreateVolume
等请求(实现 Controller Server),所以 cap 必须给予 PluginCapability_Service_CONTROLLER_SERVICE
,除此之外如果节点不支持均匀的创建外部存储供应商的 Volume,那么应当同时返回 PluginCapability_Service_VOLUME_ACCESSIBILITY_CONSTRAINTS
以表示 CSI 处理时需要根据集群拓扑作调整;具体的可以查看 gRPC 注释:
1 |
|
Controller Server 实际上对应着 Provisioning and Deleting 阶段;换句话说核心的创建/删除卷、快照等都应在此做实现,针对于本次编写的 NFS 插件仅做最小实现(创建/删除卷);需要注意的是除了核心的创建删除卷要实现以外还需要实现 ControllerGetCapabilities
方法,该方法返回 Controller Server 的 cap:
ControllerGetCapabilities
返回的实际上是在创建驱动时设置的 cscap:
1 |
|
ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME
表示这个 Controller Server 支持创建/删除卷,ControllerServiceCapability_RPC_CREATE_DELETE_SNAPSHOT
表示支持创建/删除快照(快照功能是后来闲的没事加的);应该明确的是我们返回了特定的 cap 那就要针对特定方法做实现,因为你一旦声明了这些 cap Kubernetes 就认为有相应请求可以让你处理(你不能吹完牛逼然后关键时刻掉链子)。针对于可以返回哪些 cscap 可以通过这些 gRPC 常量来查看:
1 |
|
当声明了 ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME
以后针对创建删除卷方法 CreateVolume
、DeleteVolume
做实现即可;这两个方法实现就是常规的业务逻辑层面没什么技术含量,对于外部存储供应商是 NFS 来说无非就是接到一个 CreateVolumeRequest
,然后根据 request 给的 volume name 啥的信息自己执行一下在 NFS Server 上 mkdir
,删除卷处理就是反向的 rm -rf dir
;在两个方法的处理中可能额外掺杂一些校验等其他的辅助实现。
最后有几点需要注意的地方:
mkdir
以后要把目录、NFS Server 地址等必要信息通过 VolumeContext 返回给 Kubernetes,Kubernetes 接下来会传递给 Node Driver(Mount/Umount)用。mkdir
之前,NFS 应该已经确保 mount 到了 Controller Server 容器本地,所以目前的做法就是启动 Controller Server 时就执行 NFS 挂载;如果用其他的后端存储比如阿里云存储时也要考虑在创建卷之前相关的 API Client 是否可用。Node Server 实际上就是 Node Driver,简单地说当 Controller Server 完成一个卷的创建,并且已经 Attach 到 Node 以后(当然这里的 NFS 不需要 Attach),Node Server 就需要实现根据给定的信息将卷 Mount 到 Pod 或者从 Pod Umount 掉卷;同样的 Node Server 也许要返回一些信息来告诉 Kubernetes 自己的详细情况,这部份由两个方法完成 NodeGetInfo
和 NodeGetCapabilities
NodeGetInfo
中返回节点的常规信息,比如 Node ID、最大允许的 Volume 数量、集群拓扑信息等;NodeGetCapabilities
返回这个 Node 的 cap,由于我们的 NFS 是真的啥也不支持,所以只好返回 NodeServiceCapability_RPC_UNKNOWN
,至于其他的 cap 如下(含义自己看注释):
1 |
|
剩下的核心方法 NodePublishVolume
和 NodeUnpublishVolume
挂载/卸载卷同 Controller Server 创建删除卷一样都是业务处理,没啥可说的,按步就班的调用一下 Mount 上就行;唯一需要注意的点就是这里也要保证幂等性,同时由于要操作 Pod 目录,所以要把宿主机的 /var/lib/kubelet/pods
目录挂载到 Node Server 容器里。
NFS 插件写完以后就可以实体环境做测试了,测试方法不同插件可能并不相同,本 NFS 插件可以直接使用源码项目的 deploy
目录创建相关容器做测试(需要根据自己的 NFS Server 修改一些参数)。针对于如何部署下面做一下简单说明:
三大阶段笼统的其实对应着三个 Sidecar Container:
我们的 NFS CSI 插件不需要 Attach,所以 external-attacher 也不需要部署;external-provisioner 只响应创建删除卷请求,所以通过 Deployment 部署足够多的复本保证 HA 就行;由于 Pod 不一定会落到那个节点上,理论上任意 Node 都可能有 Mount/Umount 行为,所以 node-driver-registrar 要以 Daemonset 方式部署保证每个节点都有一个。
在前期代码编写时一般都是 “盲狙”,就是按照自己的理解无脑实现,这时候可能离实际部署还很远,但是只是单纯的想知道某个 Request 里面到底是什么个东西,这时候你可以利用 mritd/socket2tcp
容器模拟监听 socket 文件,然后将请求转发到你的 IDE 监听端口上,然后再进行 Debug。
可能有人会问: “我直接在 Sidecar Containers 里写个 tcp 地址不就行了,还转发毛线,这不是脱裤子放屁多此一举么?”,但是这里我友情提醒一下,Sidecar Containers 指定 CSI 地址时填写非 socket 类型的地址是不好使的,会直接启动失败。
等到代码编写到后期其实就开始 “真机” 调试了,这时候其实不必使用原始的打日志调试方法,NFS CSI 的项目源码中的 Dockerfile.debug
提供了使用 dlv 做远程调试的样例;具体怎么配合 IDE 做远程调试请自行 Google。
其他功能根据需要可以自己酌情实现,比如创建/删除快照功能;对于 NFS 插件来说 NFS Server 又没有 API,所以最简单最 low 的办法当然是 tar -zcvf
了(哈哈哈(超大声)),当然性能么就不要提了。
CSI 开发其实是针对 Kubernetes CSI Sidecar Containers 的 gRPC 开发,根据自己需求实现三大阶段中对应三大 gRPC Server 相应方法即可;相关功能要保证幂等性,cap 要看文档根据实际情况返回。
截止本文编写时间,树莓派4 官方系统仍然不支持 64bit;但是当我在 3b+ 上使用 arch 64bit 以后我发现 32bit 系统和 64bit 系统装在同一个树莓派上在使用时那就是两个完全不一样的树莓派…所以对于这个新的 rpi4 那么必需要用 64bit 的系统;而当前我大致查看到支持 64bit 的系统只有 Ubuntu20、Manjaro 两个,Ubuntu 对我来说太重了(虽然服务器上我一直是 Ubuntu,但是 rpi 上我选择说 “不”),Manjaro 基于 Arch 这种非常轻量的系统非常适合树莓派这种开发板,所以最终我选择了 Manjaro。但是万万没想到的是 Manjaro 都是带 KDE 什么的图形化的,而我的树莓派只想仍在角落里跑东西,所以说图形化这东西对我来说也没啥用,最后迫于无奈只能自己通过 Manjaro 的工具自己定制了。
经过几经查找各种 Google,发现了 Manjaro 官方提供了自定义创建 arm 镜像的工具 manjaro-arm-tools,这个工具简单使用如下:
sudo pacman -Syyu manjaro-strit-keyring && sudo pacman -S manjaro-arm-tools-git
当工具都准备完成后,只需要执行 sudo buildarmimg -d rpi4 -e minimal
即可创建 manjaro 的 rpi4 最小镜像。
在使用 manjaro-arm-tool 创建系统以后发现一些细微的东西需要自己调整,比如网络设置常用软件包等,而 manjaro-arm-tool 工具又没有提供太好的自定义处理的一些 hook,所以最后萌生了自己根据 manjaro-arm-tool 来创建自己的 rpi4 系统定制工具的想法。
在查看了 manjaro-arm-tool 的源码后可以看到实际上软件安装就是利用 systemd-nspawn 进入到 arm 系统执行 pacman 安装,自己依葫芦画瓢增加一些常用的软件包安装:
1 |
|
在安装软件包时发现安装速读奇慢,研究以后发现是没有使用国内的镜像源,故增加了国内镜像源的处理:
1 |
|
默认的 manjaro-arm-tool 创建的系统网络部分采用 dhspcd 做 dhcp 处理,但是我个人感觉一切尽量精简统一还是比较好的;所以准备网络部分完全由 systemd 接管处理,即直接使用 systemd-networkd 和 systemd-resolved;systemd-networkd 处理相对简单,编写一个配置文件然后 enable systemd-networkd 服务即可:
/etc/systemd/network/10-eth-dhcp.network
1 |
|
让 systemd-networkd 开机自启动
1 |
|
一开始以为 systemd-resolved 同样 enable 一下就行,后来发现每次开机初始化以后 systemd-resolved 都会被莫明其妙的 disable 掉;经过几经寻找和开 issue 问作者,发现这个操作是被 manjaro-arm-oem-install 包下的脚本执行的,作者的回复意思是大部分带有图形化的版本网络管理工具都会与 systemd-resolved 冲突,所以默认关闭了,这时候我们就要针对 manjaro-arm-oem-install 单独处理一下:
1 |
|
有线连接只要 systemd-networkd 处理好就能很好的工作,而无线连接目前有很多方案,我一开始想用 netctl,后来发现这东西虽然是 Arch 亲儿子,但是在系统定制时采用 systemd-nspawn 调用不兼容(因为里面调用了 systemd 的一些命令,这些命令一般只有在开机时才可用),而且只用 netctl 来管理 wifi 还感觉怪怪的,后来我的想法是要么用就全都用,要么就纯手动不要用这些东西,所以最后的方案是 wpa_supplicant + systemd-networkd 一把梭:
/etc/systemd/network/10-wlan-dhcp.network.example
1 |
|
在上面的一些调整完成后我就启动系统实体机测试了,测试过程中发现安装 docker 以后会有两个警告,大致意思就是不支持 swap limit 和 cpu limit;查询资料以后发现是内核有两个参数没开启(CONFIG_MEMCG_SWAP
、CONFIG_CFS_BANDWIDTH
)…当然我这种强迫症是不能忍的,没办法就自己在 rpi4 上重新编译了内核(后来我想想还不如用 arch 32bit 然后自己编译 64bit 内核了):
1 |
|
由于我的 rpi4 配的是 ARGON ONE 的外壳,所以电源按钮还有风扇需要驱动才能完美工作,没办法我又编译了 ARGON ONE 外壳的驱动:
1 |
|
综合以上的各种修改以后,我从 manjaro-arm-tool 提取出了定制化的 rpi4 的编译脚本,该脚本目前存放在 mritd/manjaro-rpi4 仓库中;目前使用此脚本编译的系统镜像默认进行了以下处理:
至于 ARGON ONE 的外壳驱动只在 resources 目录下提供了安装包,并未默认安装到系统。
]]>目前某项目组日志需要做切割处理,针对日志信息进行分割并提取 k/v 放入 es 中方便查询。这种需求在传统 ELK 中应当由 logstash 组件完成,通过 gork
等操作对日志进行过滤、切割等处理。不过很尴尬的是我并不会 ruby,logstash pipeline 的一些配置我也是极其头疼,而且还不想学…更不凑巧的是我会写点 go,那么理所应当的此时的我对 filebeat 源码产生了一些想法,比如我直接在 filebeat 端完成日志处理,然后直接发 es/logstash,这样似乎更方便,而且还能分摊 logstash 的压力,我感觉这个操作并不过分😂…
目前某项目组 java 日志格式如下:
1 |
|
目前开发约定格式为日志通过 $$
进行分割,日志格式比较简单,但是 logstash 共用(nginx 等各种日志都会往这个 logstash 输出),不想去折腾 logstash 配置的情况下,只需要让 filebeat 能够直接切割并设置好 k/v 对应既可。
module 部份只做简介,以为实际上依托 es 完成,意义不大。
当然在考虑修改 filebeat 源码后,我第一想到的是 filebeat 的 module,这个 module 在官方文档中是个很神奇的东西;通过开启一个 module 就可以对某种日志直接做处理,这种东西似乎就是我想要的;比如我写一个 “项目名” module,然后 filebeat 直接开启这个 module,这个项目的日志就直接自动处理好(听起来就很 “上流”)…
针对于自定义 module,官方给出了文档: Creating a New Filebeat Module
按照文档操作如下(假设我们的项目名为 cdm):
1 |
|
创建完成后目录结构如下
1 |
|
这几个文件具体作用官方文档都有详细的描述;但是根据文档描述光有这几个文件是不够的,module 只是一个处理集合的定义,尚未包含任何处理,针对真正的处理需要继续创建 fileset,fileset 简单的理解就是针对具体的一组文件集合的处理;例如官方 nginx module 中包含两个 fileset: access
和 error
,这两个一个针对 access 日志处理一个针对 error 日志进行处理;在 fileset 中可以设置默认文件位置、处理方式。
But… 我翻了 nginx module 的样例配置才发现,module 这个东西实质上只做定义和存储处理表达式,具体的切割处理实际上交由 es 的 Ingest Node 处理;表达式里仍需要定义 grok
等操作,而且这东西最终会编译到 go 静态文件里;此时的我想说一句 “MMP”,本来我是不像写 grok 啥的才来折腾 filebeat,结果这个 module 折腾一圈还是要写 grok 啥的,而且这东西直接借助 es 完成导致压力回到了 es 同时每次修改还得重新编译 filebeat… 所以折腾到这我就放弃了,这已经违背了当初的目的,有兴趣的可以参考以下文档继续折腾:
经历了 module 的失望以后,我把目光对准了 processors;processors 是 filebeat 一个强大的功能,顾名思义它可以对 filbeat 收集到的日志进行一些处理;从官方 Processors 页面可以看到其内置了大量的 processor;这些 processor 大部份都是直接对日志进行 “写” 操作,所以理论上我们自己写一个 processor 就可以 “为所欲为+为所欲为=为所欲为”。
不过不幸的是关于 processor 的开发官方并未给出文档,官方认为这是一个 high level
的东西,不过也找到了一个 issue 对其做了相关回答: How do I write a processor plugin by myself;所以最好的办法就是直接看已有 processor 的源码抄一个。
理所应当的找了一个软柿子捏: add_host_metadata
,add_host_metadata processor 顾名思义在每个日志事件(以下简称为 event)中加入宿主机的信息,比如 hostname 啥的;以下为 add_host_metadata processor 的文件结构(processors 代码存储在 libbeat/processors
目录下)。
通过阅读源码和 issue 的回答可以看出,我们自定义的 processor 只需要实现 Processor interface 既可,这个接口定义如下:
通过查看 add_host_metadata 的源码,String() string
方法只需要返回这个 processor 名称既可(可以包含必要的配置信息);而 Run(event *beat.Event) (*beat.Event, error)
方法表示在每一条日志被读取后都会转换为一个 event 对象,我们在方法内进行处理然后把 event 返回既可(其他 processor 可能也要处理)。
有了这些信息就简单得多了,毕竟作为一名合格的 CCE(Ctrl C + Ctrl V + Engineer) 抄这种操作还是很简单的,直接照猫画虎写一个就行了
config.go
1 |
|
cdm.go
1 |
|
写好代码以后就可以编译一个自己的 filebeat 了(开心ing)
1 |
|
然后编写配置文件进行测试,日志相关字段已经成功塞到了 event 中,这样我直接发到 es 或者 logstash 就行了。
1 |
|
在我折腾完源码以后,反思一下其实这种方式需要自己编译 filebeat,而且每次规则修改也很不方便,唯一的好处真的就是用代码可以 “为所欲为”;反过来一想 “filebeat 有没有 processor 的扩展呢?脚本热加载那种?” 答案是使用 script processor,script processor 虽然名字上是个 processor,实际上其包含了完整的 ECMA 5.1 js 规范实现;结论就是我们可以写一些 js 脚本来处理日志,然后 filebeat 每次启动后加载这些脚本既可。
script processor 的使用方式很简单,js 文件中只需要包含一个 function process(event)
方法既可,与自己用 go 实现的 processor 类似,每行日志也会形成一个 event 对象然后调用这个方法进行处理;目前 event 对象可用的 api 需要参考官方文档;需要注意的是 script processor 目前只支持 ECMA 5.1 语法规范,超过这个范围的语法是不被支持;实际上其根本是借助了 https://github.com/dop251/goja 这个库来实现的。同时为了方便开发调试,script processor 也增加了一些 nodejs 的兼容 module,比如 console.log
等方法是可用的;以下为 js 处理上面日志的逻辑:
1 |
|
写好脚本后调整配置测试既可,如果 js 编写有问题,可以通过 console.log
来打印日志进行不断的调试
1 |
|
需要注意的是目前 lang
的值只能为 javascript
和 js
(官方文档写的只能是 javascript
);根据代码来看后续 script processor 有可能支持其他脚本语言,个人认为主要取决于其他脚本语言有没有纯 go 实现的 runtime,如果有的话未来很有可能被整合到 script processor 中。
研究完 script processor 后我顿时对其他 processor 也产生了兴趣,随着更多的查看processor 文档,我发现其实大部份过滤分割能力已经有很多 processor 进行了实现,其完善程度外加可扩展的 script processor 实际能力已经足矣替换掉 logstash 的日志分割过滤处理了。比如上面的日志切割其实使用 dissect processor 实现更加简单(这个配置并不完善,只是样例):
1 |
|
除此之外还有很多 processor,例如 drop_event
、drop_fields
、timestamp
等等,感兴趣的可以自行研究。
基本上折腾完以后做了一个总结:
这是一个比较骚的动作,但是事实上确实有这个需求,折腾半天找工具看源码,这里记录一下(不想看源码分析啥的请直接跳转到第五部份)。
由于最近某个爬虫业务需要抓取微信公众号的一些文章,某开发小伙伴想到了通过启动安卓虚拟机然后抓包的方式实现;经过几番寻找最终我们选择采用 docker 的方式启动安卓虚拟机,docker 里安卓虚拟机比较成熟的项目我们找到了 https://github.com/budtmo/docker-android 这个项目;但是由于众所周知的原因这个 2G+ 的镜像国内拉取是非常慢的,于是我想到了通过国外 VPS 拉取然后 scp 回来… 由于贫穷的原因,当我实际操作的时候遇到了比较尴尬的问题: **VPS 磁盘空间 25G,镜像拉取后解压接近 10G,我需要 docker save
成 tar 包再进行打包成 tar.gz
格式 scp 回来,这个时候空间不够用了…**所以我当时就在想有没有办法让 docker daemon 拉取镜像时不解压?或者说自己通过 HTTP 下载镜像直接存储为 tar?
当出现了上面的问题后,我第一反应就是:
当我查看 containers/image README 文档时发现其提到了 skopeo 项目,并且很明确的说了
The containers/image project is only a library with no user interface; you can either incorporate it into your Go programs, or use the skopeo tool:
The skopeo tool uses the containers/image library and takes advantage of many of its features, e.g. skopeo copy exposes the containers/image/copy.Image functionality.
那么也就是说镜像下载这块很大可能应该调用 containers/image/copy.Image
完成,随即就看了下源码文档
很明显,types.ImageReference
、Options
里面的属性啥的我完全看不懂… 😂😂😂
当 containers/image 源码看不懂时,突然想到 skopeo 调用的是这个玩意,那么依葫芦画瓢看 skopeo 源码应该能行;接下来常规操作 clone skopeo 源码然后编译运行测试;编译后 skopeo 支持命令如下
1 |
|
我掐指一算调用 copy 命令应该是我要找的那个它,所以常规操作打开源码直接看
通过继续追踪 alltransports.ParseImageName
方法最终可以得知 copy 命令的 SOURCE-IMAGE
和 DESTINATION-IMAGE
都支持哪些写法
每一个 Transport 的实现都提供了 Name 方法,其名称即为 src 或 dest 镜像名称的前缀,例如 docker://nginx:1.17.6
经过测试不同的 Transport 格式并不完全一致(具体看源码),比如 docker://nginx:1.17.6
和 dir:/tmp/nginx
;同时这些 Transport 并非完全都适用与 src 与 dest,比如 tarball:/tmp/nginx.tar
支持 src 而不支持 dest;其判断核心依据为 ImageReference.NewImageSource
和 ImageReference.NewImageDestination
方法实现是否返回 error
当我看了一会各种 Transport 源码后我发现一件事: 这特么不就是我要造的轮子么!😱😱😱
1 |
|
--insecure-policy
选项用于忽略安全策略配置文件,该命令将会直接通过 http 下载目标镜像并存储为 /tmp/nginx.tar
,此文件可以直接通过 docker load
命令导入
1 |
|
该命令将会从 docker daemon 导出镜像到 /tmp/nginx.tar
;为什么不用 docker save
?因为我是偷懒 dest 也是 docker-archive,实际上 skopeo 可以导出为其他格式比如 oci
、oci-archive
、ostree
等
skopeo 还有一些其他的实用命令,比如 sync
可以在两个位置之间同步镜像(😂早知道我还写个鸡儿 gcrsync),inspect
可以查看镜像信息等,迫于本人太懒,剩下的请自行查阅文档、--help
以及源码(没错,整篇文章都没写 skopeo 怎么安装)。
kubeadm 集群安装完成后,证书管理上实际上大致是两大类型:
自动滚动续期类型的证书目前从我所阅读文档和实际测试中目前只有 kubelet 的 client 证书;kubelet client 证书自动滚动涉及到了 TLS bootstrapping 部份,其核心由两个 ClusterRole 完成(system:certificates.k8s.io:certificatesigningrequests:nodeclient
和 system:certificates.k8s.io:certificatesigningrequests:selfnodeclient
),针对这两个 ClusterRole kubeadm 在引导期间创建了 bootstrap token 来完成引导期间证书签发(该 Token 24h 失效),后续通过预先创建的 ClusterRoleBinding(kubeadm:node-autoapprove-bootstrap
和 kubeadm:node-autoapprove-certificate-rotation
) 完成自动的 node 证书续期;kubelet client 证书续期部份涉及到 TLS bootstrapping 太多了,有兴趣的可以仔细查看(最后还是友情提醒: 用 kubeadm 一定要看看 Implementation details)。
手动续期的证书目前需要在到期前使用 kubeadm 命令自行续期,这些证书目前可以通过以下命令列出
1 |
|
上面已经提到了,手动管理部份的证书需要自己用命令续签(kubeadm alpha certs renew all
),而且你会发现续签以后有效期还是 1 年;kubeadm 的初衷是 **”为快速创建 kubernetes 集群的最佳实践”**,当然最佳实践包含确保证书安全性,毕竟 Let’s Encrypt 的证书有效期只有 3 个月的情况下 kubeadm 有效期有 1 年已经很不错了;但是对于最佳实践来说,我们公司的集群安全性并不需要那么高,一年续期一次无疑在增加运维人员心智负担(它并不最佳),所以我们迫切需要一种 “一劳永逸” 的解决方案;当然我目前能想到的就是找到证书签发时在哪设置的有效期,然后想办法改掉它。
目前通过宏观角度看整个 kubeadm 集群搭建过程,其中涉及到证书签署大致有两大部份: init 阶段和后期 renew,下面开始分析两个阶段的源码
由于 kubernetes 整个命令行都是通过 cobra 库构建的,那么根据这个库的习惯首先直接从 cmd
包开始翻,而 kubernetes 源码组织的又比较清晰进而直接定位到 kubeadm 命令包下面;接着打开 app
目录一眼就看到了 phases
… phases
顾名思义啊,整个 init 都是通过不同的 phases
完成的,那么直接去 phases
包下面找证书阶段的源码既可
进入到这个 certs.go
里面,直接列出所有方法,go 的规范里只有首字母大写才会被暴露出去,那么我们直接查看这些方法名既可;从名字上很轻松的看到了这个方法…基本上就是它了
通过这个方法的代码会发现最终还是调用了 certSpec.CreateFromCA(cfg, caCert, caKey)
,那么接着看看这个方法
通过这个方法继续往下翻发现调用了 pkiutil.NewCertAndKey(caCert, caKey, cfg)
,这个方法里最终调用了 NewSignedCert(config, key, caCert, caKey)
从 NewSignedCert
方法里看到证书有效期实际上是个常量,那也就意味着我改了这个常量 init 阶段的证书有效期八九不离十的就变了,再通过包名看这个是个 pkiutil
… xxxxxutil
明显是公共的,所以推测改了它 renew 阶段应该也会变
renew 阶段也是老套路,不过稳妥点先从 cmd 找起来,所以先看 alpha
包下的 certs.go
;这时候方法名语义清晰就很有好处,一下就能找到 newCmdCertsRenewal
方法
而这个 newCmdCertsRenewal
方法实际上没啥实现,所以目测实现是从 getRenewSubCommands
实现的
看了 getRenewSubCommands
以后发现上面全是命令行库、配置文件参数啥的处理,核心在 renewCert
上,从这个方法里发现还有意外收获: renew 时实际上分两种情况处理,一种是使用了 --use-api
选项,另一种是未使用;当然根据上面的命令来说我们没使用,那么看 else 部份就行了(没看源码之前我特么居然没看 --help
不知道有这个选项)
else 部份源码最终还是调用了 RenewUsingLocalCA
方法,这个方法一直往下跟会有一个 Renew
方法
这个方法一点进去… 我上面的想法是对的
根据刚刚查看代码可以看到在 renew 阶段判断了 --use-api
选项是否使用,通过跟踪源码发现最终会调用到 RenewUsingCSRAPI
方法上,RenewUsingCSRAPI
会调用集群 CSR Api 执行证书签署
有了这个发现后基本上可以推测出这一步通过集群完成,那么按理说是应该受到 kube-controller-manager
组件的 --experimental-cluster-signing-duration
影响。
想验证修改源码是否有效只需要修改源码重新 build 出 kubeadm 命令,然后使用这个特定版本的 kubeadm renew 证书测试既可,源码调整的位置如下
然后命令行下执行 make cross
进行跨平台交叉编译(如果过你在 linux amd64 平台下则直接 make
既可)
1 |
|
编译完成后能够在 _output/local/bin/linux/amd64
下找到刚刚编译成功的 kubeadm
文件,将编译好的 kubeadm scp 到已经存在集群上执行 renew,然后查看证书时间
经过测试后确认源码修改方式有效
根据推测当使用 --use-api
会受到 kube-controller-manager
组件的 --experimental-cluster-signing-duration
影响,从而从集群中下发证书;所以首先在启动集群时需要将 --experimental-cluster-signing-duration
调整为 10 年,然后再进行测试
1 |
|
然后使用 --use-api
选项进行 renew
1 |
|
此时会发现日志中打印出 [certs] Certificate request "kubeadm-cert-kubernetes-admin-648w4" created
字样,接下来从 kube-system
的 namespace 中能够看到相关 csr
这时我们开始手动批准证书,每次批准完成一个 csr,紧接着 kubeadm 会创建另一个 csr
当所有 csr 被批准后,再次查看集群证书发现证书期限确实被调整了
总结一下,调整 kubeadm 证书期限有两种方案;第一种直接修改源码,耗时耗力还得会 go,最后还要跑跨平台编译(很耗时);第二种在启动集群时调整 kube-controller-manager
组件的 --experimental-cluster-signing-duration
参数,集群创建好后手动 renew 一下并批准相关 csr。
两种方案各有利弊,修改源码方式意味着在 client 端签发处理,不会对集群产生永久性影响,也就是说哪天你想 “反悔了” 你不需要修改集群什么配置,直接用官方 kubeadm renew 一下就会变回一年期限的证书;改集群参数实现的方式意味着你不需要懂 go 代码,只需要常规的集群配置既可实现,同时你也不需要跑几十分钟的交叉编译,不需要为编译过程中的网络问题而烦恼;所以最后使用哪种方案因人因情况而定吧。
]]>kubeadm
版本必须大于或等于目标版本kube-proxy
组件会有一次全节点滚动更新关于升级版本问题…虽然是这么说的,但是官方文档样例代码里是从 v1.16.0
升级到 v1.17.0
;可能是我理解有误,跨大版本升级好像官方没提,具体啥后果不清楚…
事实上所有升级工作主要是针对 master 节点做的,所以整个升级流程中最重要的是如何把 master 升级好。
首先由于升级限制,必须先将 kubeadm
和 kubectl
升级到大于等于目标版本
1 |
|
当然如果你之前没有 hold
住这几个软件包的版本,那么就不需要 unhold
;我的做法可能比较极端…一般为了防止后面的误升级安装完成后我会直接 rename
掉相关软件包的 apt source
配置(从根本上防止手贱)。
对于高级玩家一般安装集群时都会自定义很多组件参数,此时不可避免的会采用配置文件;所以安装完新版本的 kubeadm
后就要着手修改配置文件中的 kubernetesVersion
字段为目标集群版本,当然有其他变更也可以一起修改。
如果你的 master 节点也当作 node 在跑一些工作负载,则需要执行以下命令驱逐这些 pod 并使节点进入维护模式(禁止调度)。
1 |
|
完成节点驱逐以后,可以通过以下命令查看升级计划;升级计划中列出了升级期间要执行的所有步骤以及相关警告,一定要仔细查看。
1 |
|
确认好升级计划以后,只需要一条命令既可将当前 master 节点升级到目标版本
1 |
|
升级期间会打印很详细的日志,在日志中可以实时观察到升级流程,建议仔细关注升级流程;在最后一步会有一条日志 [addons] Applied essential addon: kube-proxy
,这意味着集群开始更新 kube-proxy
组件,该组件目前是通过 daemonset
方式启动的;这会意味着此时会造成全节点的 kube-proxy
更新;理论上不会有很大影响,但是升级是还是需要注意一下这一步操作,在我的观察中似乎 kube-proxy
也是通过滚动更新完成的,所以问题应该不大。
在单个 master 上升级完成后,只会升级本节点的 master 相关组件和全节点的 kube-proxy
组件;由于 kubelet 是在宿主机安装的,所以需要通过包管理器手动升级 kubelet
1 |
|
更新完成后执行 systemctl restart kubelet
重启,并等待启动成功既可;最后不要忘记解除当前节点的维护模式(uncordon
)。
当其中一个 master 节点升级完成后,其他的 master 升级就会相对简单的多;首先国际惯例升级一下 kubeadm
和 kubectl
软件包,然后直接在其他 master 节点执行 kubeadm upgrade node
既可。由于 apiserver 等组件配置已经在升级第一个 master 时上传到了集群的 configMap 中,所以事实上其他 master 节点只是正常拉取然后重启相关组件既可;这一步同样会输出详细日志,可以仔细观察进度,最后不要忘记升级之前先进入维护模式,升级完成后重新安装 kubelet
并关闭节点维护模式。
node 节点的升级实际上在升级完 master 节点以后不需要什么特殊操作,node 节点唯一需要升级的就是 kubelet
组件;首先在 node 节点执行 kubeadm upgrade node
命令,该命令会拉取集群内的 kubelet
配置文件,然后重新安装 kubelet
重启既可;同样升级 node 节点时不要忘记开启维护模式。针对于 CNI 组件请按需手动升级,并且确认好 CNI 组件的兼容版本。
所有组件升级完成后,可以通过 kubectl describe POD_NAME
的方式验证 master 组件是否都升级到了最新版本;通过 kuebctl version
命令验证 api 相关信息(HA rr 轮训模式下可以多执行几遍);还有就是通过 kubectl get node -o wide
查看相关 node 的信息,确保 kubelet
都升级成功,同时全部节点维护模式都已经关闭,其他细节可以参考官方文档。
搭建环境为 5 台虚拟机,每台虚拟机配置为 4 核心 8G 内存,虚拟机 IP 范围为 172.16.10.21~25
,其他软件配置如下
目前的 HA 方案与官方的不同,官方 HA 方案推荐使用类似 haproxy 等工具进行 4 层代理 apiserver,但是同样会有一个问题就是我们还需要对这个 haproxy 做 HA;由于目前我们实际生产环境都是多个独立的小集群,所以单独弄 2 台 haproxy + keeplived 去维持这个 apiserver LB 的 HA 有点不划算;所以还是准备延续老的 HA 方案,将外部 apiserver 的 4 层 LB 前置到每个 node 节点上;目前是采用在每个 node 节点上部署 nginx 4 层代理所有 apiserver,nginx 本身资源消耗低而且请求量不大,综合来说对宿主机影响很小;以下为 HA 的大致方案图
由于个人操作习惯原因,目前已经将常用的初始化环境整理到一个小脚本里了,脚本具体参见 mritd/shell_scripts 仓库,基本上常用的初始化内容为:
在以上初始化中,实际对 kubernetes 安装产生影响的主要有三个地方:
conntrack
和 ipvsadm
,后面可能需要借助其排查问题由于后面 kube-proxy 需要使用 ipvs 模式,所以需要对内核参数、模块做一些调整,调整命令如下:
1 |
|
配置完成后切记需要重启,重启完成后使用 lsmod | grep ip_vs
验证相关 ipvs 模块加载是否正常,本文将主要使用 ip_vs_wrr
,所以目前只关注这个模块既可。
官方对于集群 HA 给出了两种有关于 Etcd 的部署方案:
control plane
上,即每个 control plane
一个 etcd在测试深度耦合 control plane
方案后,发现一些比较恶心的问题;比如说开始创建第二个 control plane
时配置写错了需要重建,此时你一旦删除第二个 control plane
会导致第一个 control plane
也会失败,原因是创建第二个 control plane
时 kubeadm 已经自动完成了 etcd 的集群模式,当删除第二个 control plane
的时候由于集群可用原因会导致第一个 control plane
下的 etcd 发现节点失联从而也不提供服务;所以综合考虑到后续迁移、灾备等因素,这里选择了将 etcd 放置在外部集群中;同样也方便我以后各种折腾应对一些极端情况啥的。
确定了需要在外部部署 etcd 集群后,只需要开干就完事了;查了一下 ubuntu 官方源已经有了 etcd 安装包,但是版本比较老,测试了一下 golang 的 build 版本是 1.10;所以我还是选择了从官方 release 下载最新的版本安装;当然最后还是因为懒,我自己打了一个 deb 包… deb 包可以从这个项目 mritd/etcd-deb 下载,担心安全性的可以利用项目脚本自己打包,以下是安装过程:
1 |
|
既然自己部署 etcd,那么证书签署啥的还得自己来了,证书签署这里借助 cfssl 工具,cfssl 目前提供了 deb 的 make target,但是没找到 deb 包,所以也自己 build 了(担心安全性的可自行去官方下载);接着编辑一下 /etc/etcd/cfssl/etcd-csr.json
文件,用 /etc/etcd/cfssl/create.sh
脚本创建证书,并将证书复制到指定目录
1 |
|
最后在 3 台节点上修改配置,并将刚刚创建的证书同步到其他两台节点启动既可;下面是单台节点的配置样例
1 |
|
注意: 其他两台节点请调整 ETCD_NAME
为不重复的其他名称,调整 ETCD_LISTEN_PEER_URLS
、ETCD_LISTEN_CLIENT_URLS
、ETCD_INITIAL_ADVERTISE_PEER_URLS
、ETCD_ADVERTISE_CLIENT_URLS
为其他节点对应的 IP;同时生产环境请将 ETCD_INITIAL_CLUSTER_TOKEN
替换为复杂的 token
1 |
|
启动完成后可以通过以下命令测试是否正常
1 |
|
安装 kubeadm 没什么好说的,国内被墙用阿里的源既可
1 |
|
从上面的 HA 架构图上可以看到,为了维持 apiserver 的 HA,需要在每个机器上部署一个 nginx 做 4 层的 LB;为保证后续的 node 节点正常加入,需要首先行部署 nginx;nginx 安装同样喜欢偷懒,直接 docker 跑了…毕竟都开始 kubeadm 了,那么也没必要去纠结 docker 是否稳定的问题了;以下为 nginx 相关配置
apiserver-proxy.conf
1 |
|
kube-apiserver-proxy.service
1 |
|
启动 nginx 代理(每台机器都要启动,包括 master 节点)
1 |
|
目前 kubelet 为了保证内存 limit,需要在每个节点上关闭 swap;但是说实话我看了这篇文章 In defence of swap: common misconceptions 以后还是不想关闭 swap;更确切的说其实我们生产环境比较 “富”,pod 都不 limit 内存,所以下面的部署我忽略了 swap 错误检测
当前版本的 kubeadm 已经支持了完善的配置管理(当然细节部分还有待支持),以下为我目前使用的配置,相关位置已经做了注释,更具体的配置自行查阅官方文档
kubeadm.yaml
1 |
|
关于这个配置配置文件的文档还是很不完善,对于不懂 golang 的人来说很难知道具体怎么配置,以下做一下简要说明(请确保你已经拉取了 kubernetes 源码和安装了 Goland)
kubeadm 配置中每个配置段都会有个 kind
字段,kind
实际上对应了 go 代码中的 struct
结构体;同时从 apiVersion
字段中能够看到具体的版本,比如 v1alpha1
等;有了这两个信息事实上你就可以直接在源码中去找到对应的结构体
在结构体中所有的配置便可以一目了然
关于数据类型,如果是 string
的类型,那么意味着你要在 yaml 里写 "xxxx"
带引号这种,当然有些时候不写能兼容,有些时候不行比如 extraArgs
字段是一个 map[string]string
如果 value 不带引号就报错;如果数据类型为 metav1.Duration
(实际上就是 time.Duration
),那么你看着它是个 int64
但实际上你要写 1h2m3s
这种人类可读的格式,这是 go 的特色…
audit-policy.yaml
1 |
|
可能 Metadata
级别的审计日志比较多,想自行调整审计日志级别的可以参考官方文档
有了完整的 kubeadm.yaml
和 audit-policy.yaml
配置后,直接一条命令拉起 control plane 既可
1 |
|
control plane 拉起以后注意要保存屏幕输出,方便后续添加其他集群节点
1 |
|
根据屏幕提示配置 kubectl
1 |
|
关于网络插件的选择,以前一直喜欢 Calico,因为其性能确实好;到后来 flannel 出了 host-gw
以后现在两者性能也差不多了;但是 flannel 好处是一个工具通吃所有环境(云环境+裸机2层直通),坏处是 flannel 缺乏比较好的策略管理(当然可以使用两者结合的 Canal);后来思来想去其实我们生产倒是很少需要策略管理,所以这回怂回到 flannel 了(逃…)
Flannel 部署非常简单,根据官方文档下载配置,根据情况调整 backend
和 pod 的 CIDR,然后 apply 一下既可
1 |
|
为了保证 HA 架构,还需要在另外两台 master 上启动 control plane;在启动之前请确保另外两台 master 节点节点上 /etc/kubernetes/audit-policy.yaml
审计配置已经分发完成,确保 127.0.0.1:6443
上监听的 4 层 LB 工作正常(可尝试使用 curl -k https://127.0.0.1:6443
测试);根据第一个 control plane 终端输出,其他 control plane 加入命令如下
1 |
|
由于在使用 kubeadm join
时相关选项(--discovery-token-ca-cert-hash
、--control-plane
)无法与 --config
一起使用,这也就意味着我们必须增加一些附加指令来提供 kubeadm.yaml
配置文件中的一些属性;最终完整的 control plane 加入命令如下,在其他 master 直接执行既可(--apiserver-advertise-address
的 IP 地址是目标 master 的 IP)
1 |
|
所有 control plane 启动完成后应当通过在每个节点上运行 kubectl get cs
验证各个组件运行状态
1 |
|
node 节点的启动相较于 master 来说要简单得多,只需要增加一个防止 swap
开启拒绝启动的参数既可
1 |
|
启动成功后在 master 上可以看到所有 node 信息
1 |
|
集群搭建好以后,如果想让 master 节点也参与调度任务,需要在任意一台 master 节点执行以下命令
1 |
|
最后创建一个 deployment 和一个 service,并在不同主机上 ping pod IP 测试网络联通性,在 pod 内直接 curl service 名称测试 dns 解析既可
test-nginx.deploy.yaml
1 |
|
test-nginx.svc.yaml
1 |
|
说实话使用 kubeadm 后,我更关注的是集群后续的扩展性调整是否能达到目标;搭建其实很简单,大部份时间都在测试后续调整上
由于我们采用的是外部的 Etcd,所以迁移起来比较简单怎么折腾都行;需要注意的是换 IP 的时候注意保证老的 3 个节点至少有一个可用,否则可能导致集群崩溃;调整完成后记得分发相关 Etcd 节点的证书,重启时顺序一个一个重启,不要并行操作
如果需要修改 conrol plane 上 apiserver、scheduler 等配置,直接修改 kubeadm.yaml
配置文件(所以集群搭建好后务必保存好),然后执行 kubeadm upgrade apply --config kubeadm.yaml
升级集群既可,升级前一定作好相关备份工作;我只在测试环境测试这个命令工作还可以,生产环境还是需要谨慎
目前根据我测试的结果,controller manager 的 experimental-cluster-signing-duration 参数在 init 的签发证书阶段似乎并未生效;目前根据文档描述 kubelet
client 的证书会自动滚动,其他证书默认 1 年有效期,需要自己使用命令续签;续签命令如下
1 |
|
默认的 bootstrap token 会在 24h 后失效,所以后续增加新节点需要重新创建 token,重新创建 token 可以通过以下命令完成
1 |
|
如果忘记了 certificate-key 可以通过一下命令重新 upload 并查看
1 |
|
node 节点一旦启动完成后,kubelet 配置便不可再修改;如果想要修改 kubelet 配置,可以通过调整 /etc/systemd/system/kubelet.service.d/10-kubeadm.conf
配置文件完成
本文参考了许多官方文档,以下是一些个人认为比较有价值并且在使用 kubeadm 后应该阅读的文档
]]>Netflix DNS 分流实际上我目前的方案是通过 CoreDNS 作为主 DNS Server,然后在 CoreDNS 上针对 Netflix 全部域名解析 forward 到一台国外可以解锁 Netflix 机器上;如果直接将 CoreDNS 暴露在公网,那么无疑是在作死,为 DNS 反射 DDos 提供肉鸡;所以想到的方案是自己编写一个不可描述的工具,本地 Client 到 Server 端以后,Server 端再去设置到 CoreDNS 做分流;其中不可避免的需要调整 Server 端默认 DNS。
目前大部份人还是习惯修改 /etc/resolv.conf
配置文件,这个配置文件上面已经明确标注了不要去修改它;因为自 Systemd 一统江山以后,系统 DNS 已经被 systemd-resolved
服务接管;一但修改了 /etc/resolv.conf
,机器重启后就会被恢复;所以根源解决方案还是需要修改 systemd-resolved
的配置。
在调整完 systemd-resolved
配置后其实有些地方仍然是不生效的;原因是 Ubuntu 18 开始网络已经被 netplan 接管,所以问题又回到了如何修改 netplan;由于云服务器初始化全部是由 cloud-init 完成的,netplan 配置里 IP 全部是由 DHCP 完成;那么直接修改 netplan 为 static IP 理论上可行,但是事实上还是不够优雅;后来研究了一下其实更优雅的方式是覆盖掉 DHCP 的某些配置,比如 DNS 配置;在阿里云上配置如下(/etc/netplan/99-netcfg.yaml
)
1 |
|
修改完成后执行 netplan try
等待几秒钟,如果屏幕的读秒倒计时一直在动,说明修改没问题,接着回车既可(尽量不要 netplan apply
,一旦修改错误你就再也连不上了…)
顺便贴一下 CoreDNS 配置吧,可能有些人也需要;第一部分的域名是目前我整理的 Netflix 全部访问域名,针对这些域名的流量转发到自己其他可解锁 Netflix 的机器既可
1 |
|
当 netplan 修改完成后,只需要重启 docker 既可保证 docker 内所有容器 DNS 请求全部发送到自己定义的 DNS 服务器上;请不要尝试将自己的 CoreDNS 监听到 127.*
或者 ::1
上,这两个地址会导致 docker 中的 DNS 无效,因为在 libnetwork 中针对这两个地址做了过滤,并且 FilterResolvDNS
方法在剔除这两种地址时不会给予任何警告日志
目前采用 MySQL fork 版本 Percona Server 5.7.28,监控方面选择 Percona Monitoring and Management 2.1.0,对应监控 Client 版本为 2.1.0
为保证兼容以及稳定性,MySQL 宿主机系统选择 CentOS 7,Percona Server 安装方式为 rpm 包,安装后由 Systemd 守护
安装包下载地址为 https://www.percona.com/downloads/Percona-Server-5.7/LATEST/,下载时选择 Download All Packages Together
,下载后是所有组件全量的压缩 tar 包。
针对 CentOS 7 系统,安装前升级所有系统组件库,执行 yum update
既可;大部份 CentOS 7 安装后可能会附带 mariadb-libs
包,这个包会默认创建一些配置文件,导致后面的 Percona Server 无法覆盖它(例如 /etc/my.cnf
),所以安装 Percona Server 之前需要卸载它 yum remove mariadb-libs
针对于数据存储硬盘,目前统一为 SSD 硬盘,挂载点为 /data
,挂载方式可以采用 fstab
、systemd-mount
,分区格式目前采用 xfs
格式。
SSD 优化有待补充…
Percona Server tar 包解压后会有 9 个 rpm 包,实际安装时只需要安装其中 4 个既可
1 |
|
目前 MySQL 数据会统一存放到 /data
目录下,所以需要将单独的数据盘挂载到 /data
目录;如果是 SSD 硬盘还需要调整系统 I/O 调度器等其他优化。
Percona Server 安装完成后,由于配置调整原因,还会用到一些其他的数据目录,这些目录可以预先创建并授权
1 |
|
/var/log/mysql
目录用来存放 MySQL 相关的日志(不包括 binlog),/data/mysql_tmp
用来存放 MySQL 运行时产生的缓存文件。
由于 rpm 安装的 Percona Server 会采用 Systemd 守护,所以如果想修改文件描述符配置应当调整 Systemd 配置文件
1 |
|
然后执行 systemctl daemon-reload
重载既可。
Percona Server 安装完成后也会生成 /etc/my.cnf
配置文件,不过不建议直接修改该文件;修改配置文件需要进入到 /etc/percona-server.conf.d/
目录调整相应配置;以下为配置样例(生产环境 mysqld 配置需要优化调整)
mysql.cnf
1 |
|
mysqld.cnf
1 |
|
mysqld_safe.cnf
1 |
|
mysqldump.cnf
1 |
|
配置文件调整完成后启动既可
1 |
|
启动完成后默认 root 密码会自动生成,通过 grep 'temporary password' /var/log/mysql/*
查看默认密码;获得默认密码后可以通过 mysqladmin -S /data/mysql/mysql.sock -u root -p password
修改 root 密码。
数据库创建成功后需要增加 pmm 监控,后续将会通过监控信息来调优数据库,所以数据库监控必不可少。
pmm 监控需要使用特定用户来监控数据信息,所以需要预先为 pmm 创建用户
1 |
|
pmm server 端推荐直接使用 docker 启动,以下为样例 docker compose
1 |
|
如果想要自定义证书,请将证书复制到 volume 内的 nginx 目录下,自定义证书需要以下证书文件
1 |
|
pmm server 启动后访问 http(s)://IP_ADDRESS
既可进入 granafa 面板,默认账户名和密码都是 admin
PMM Client 同样采用 rpm 安装,下载地址 https://www.percona.com/downloads/pmm2/,当前采用最新的 2.1.0 版本;rpm 下载完成后直接 yum install
既可。
rpm 安装完成后使用 pmm-admin
命令配置服务端地址,并添加当前 mysql 实例监控
1 |
|
完成后稍等片刻既可在 pmm server 端的 granafa 中看到相关数据。
从原始数据库 dump 相关库,并导入到新数据库既可
1 |
|
数据导入后重建业务用户既可
1 |
|
目前数据备份采用 Perconra xtrabackup 工具,xtrabackup 可以实现高速、压缩带增量的备份;xtrabackup 安装同样采用 rpm 方式,下载地址为 https://www.percona.com/downloads/Percona-XtraBackup-2.4/LATEST/,下载完成后执行 yum install
既可
目前备份工具开源在 GitHub 上,每次全量备份会写入 .full-backup
文件,增量备份会写入 .inc-backup
文件
为了使备份自动运行,目前将定时任务配置到 systemd 中,由 systemd 调度并执行;以下为相关 systemd 配置文件
mysql-backup-full.service
1 |
|
mysql-backup-inc.service
1 |
|
mysql-backup-compress.service
1 |
|
mysql-backup-full.timer
1 |
|
mysql-backup-inc.timer
1 |
|
mysql-backup-compress.timer
1 |
|
创建好相关文件后启动相关定时器既可
1 |
|
针对于全量备份,只需要按照官方文档的还原顺序进行还原既可
1 |
|
对于增量备份恢复,其与全量备份恢复的根本区别在于: 对于非最后一个增量文件的预处理必须使用 --apply-log-only
选项防止运行回滚阶段的处理
1 |
|
针对 xtrabackup 备份的数据可以直接恢复成 slave 节点,具体步骤如下:
首先将备份文件复制到目标机器,然后执行解压(默认备份工具采用 lz4 压缩)
1 |
|
解压完成后执行预处理操作(在执行预处理之前请确保 slave 机器上相关配置文件与 master 相同,并且处理好数据目录存放等)
1 |
|
预处理成功后便可执行恢复,以下命令将自动读取 my.cnf
配置,自动识别数据目录位置并将数据文件移动到该位置
1 |
|
所由准备就绪后需要进行权限修复
1 |
|
最后在 mysql 内启动 slave 既可,slave 信息可通过从数据备份目录的 xtrabackup_binlog_info
中获取
1 |
|
目前生产环境数据目录位置调整到 /home/mysql
,所以目录权限处理也要做对应调整
1 |
|
生产环境目前节点配置如下
Intel(R) Xeon(R) CPU E5-2620 v4 @ 2.10GHz
128G
所以配置文件也需要做相应的优化调整
mysql.cnf
1 |
|
mysqld.cnf
1 |
|
mysqld_safe.cnf
1 |
|
mysqldump.cnf
1 |
|
mysql 默认允许在实例运行后使用 set global VARIABLES=VALUE
的方式动态调整一些配置,这可能导致在运行一段时间后(运维动态修改)实例运行配置和配置文件中配置不一致;所以建议定期 diff 运行时配置与配置文件配置差异,防制特殊情况下 mysql 重启后运行期配置丢失
1 |
|
Percona Toolkit 提供了一个诊断工具,用于对 mysql 内的配置进行扫描并给出优化建议,在初始化时可以使用此工具评估 mysql 当前配置的具体情况
1 |
|
使用 pt-deadlock-logger 工具可以诊断当前的死锁状态,以下为对死锁检测的测试
首先创建测试数据库和表
1 |
|
在一个其他终端上开启 pt-deadlock-logger 检测
1 |
|
检测开启后进行死锁测试
1 |
|
不同于 iostat
,pt-diskstats
提供了更加详细的 IO 详情统计,并且据有交互式处理,执行一下命令将会实时检测 IO 状态
1 |
|
其中几个关键值含义如下(更详细的请参考官方文档 https://www.percona.com/doc/percona-toolkit/LATEST/pt-diskstats.html#output)
iostat
的 %util
。pt-duplicate-key-checker 工具提供了对数据库重复索引和外键的自动查找功能,工具使用如下
1 |
|
pt-find 是一个很方便的表查找统计工具,默认的一些选项可以实现批量查找符合条件的表,甚至执行一些 SQL 处理命令
1 |
|
如果想要定制输出可以采用 --printf
选项
1 |
|
遗憾的是目前 printf
格式来源与 Perl 的 sprintf
函数,所以支持格式有限,不过简单的格式定制已经基本实现,复杂的建议通过 awk 处理;其他的可选参数具体参考官方文档 https://www.percona.com/doc/percona-toolkit/LATEST/pt-find.html
迫于篇幅,其他更多的高级命令请自行查阅官方文档 https://www.percona.com/doc/percona-toolkit/LATEST/index.html
]]>目前测试环境中有很多个 DNS 服务器,不同项目组使用的 DNS 服务器不同,但是不可避免的他们会访问一些公共域名;老的 DNS 服务器都是 dnsmasq,改起来很麻烦,最近研究了一下 CoreDNS,通过编写插件的方式可以实现让多个 CoreDNS 实例实现分布式的统一控制,以下记录了插件编写过程
CoreDNS 目前是 CNCF 旗下的项目(已毕业),为 Kubernetes 等云原生环境提供可靠的 DNS 服务发现等功能;官网的描述只有一句话: CoreDNS: DNS and Service Discovery,而实际上分析源码以后发现 CoreDNS 实际上是基于 Caddy (一个现代化的负载均衡器)而开发的,通过插件式注入,并监听 TCP/UDP 端口提供 DNS 服务;得益于 Caddy 的插件机制,CoreDNS 支持自行编写插件,拦截 DNS 请求然后处理,通过这个插件机制你可以在 CoreDNS 上实现各种功能,比如构建分布式一致性的 DNS 集群、动态的 DNS 负载均衡等等
CoreDNS 插件编写目前有两种方式:
由于 GRPC 链接实际上借助于 CoreDNS 的 GRPC 插件,同时 GRPC 会有网络开销,TCP 链接不稳定可能造成 DNS 响应过慢等问题,所以本文只介绍如何使用 Go 编写 CoreDNS 的插件,这种插件将直接编译进 CoreDNS 二进制文件中
在通常情况下,插件中应当包含一个 setup.go
文件,这个文件的 init
方法调用插件注册,类似这样
1 |
|
注册方法的第一个参数是插件名称,第二个是一个 func,func 签名如下
1 |
|
在这个 SetupFunc 中,插件编写者应当通过 *Controller
拿到 CoreDNS 的配置并解析它,从而完成自己插件的初始化配置;比如你的插件需要连接 Etcd,那么在这个方法里你要通过 *Controller
遍历配置,拿到 Etcd 的地址、证书、用户名密码配置等信息;
如果配置信息没有问题,该插件应当初始化完成;如果有问题就报错退出,然后整个 CoreDNS 启动失败;如果插件初始化完成,最后不要忘记将自己的插件加入到整个插件链路中(CoreDNS 根据情况逐个调用)
1 |
|
一般来说,每一个插件都会定义一个结构体,结构体中包含必要的 CoreDNS 内置属性,以及当前插件特性的相关配置;一个样例的插件结构体如下所示
1 |
|
一个 Go 编写的 CoreDNS 插件实际上只需要实现一个 Handler
接口既可,接口定义如下
1 |
|
ServeDNS
方法是插件需要实现的主要逻辑方法,DNS 请求接受后会从这个方法传入,插件编写者需要实现查询并返回结果Name
方法只返回一个插件名称标识,具体作用记不太清楚,好像是为了判断插件命名唯一性然后做链式顺序调用的,原则只要你不跟系统插件重名就行基本逻辑就是在 setup 阶段通过配置文件创建你的插件结构体对象;然后插件结构体实现这个 Handler
接口,运行期 CoreDNS 会调用接口的 ServeDNS
方法来向插件查询 DNS 请求
ServeDNS 方法入参有 3 个:
context.Context
用来控制超时等情况的 contextdns.ResponseWriter
插件通过这个对象写入对 Client DNS 请求的响应结果*dns.Msg
这个是 Client 发起的 DNS 请求,插件负责处理它,比如当你发现请求类型是 AAAA
而你的插件又不想去支持时要如何返回结果对于返回结果,插件编写者应当通过 dns.ResponseWriter.WriteMsg
方法写入返回结果,基本代码如下
1 |
|
需要注意的是,无论根据业务逻辑是否查询到 DNS 记录,都要返回响应结果(没有就返回空),错误或者未返回将会导致 Client 端查询 DNS 超时,然后不断重试,最终可能导致 Client 端服务故障
Name
方法非常简单,只需要返回当前插件名称既可;该方法的作用是为了其他插件判断本插件是否加载等情况
1 |
|
对于实际的业务处理,可以通过 case
请求 QType
来做具体的业务实现
1 |
|
根据官方文档的描述,当你编写好插件以后,你的插件应当提交到一个 Git 仓库中,可以使 Github 等(保证可以 go get
拉取就行),然后修改 plugin.cfg
,最后执行 make
既可;具体修改如下所示
值得注意的是: 插件配置在 plugin.cfg
内的顺序决定了插件的执行顺序;通俗的讲,如果 Client 的一个 DNS 请求进来,CoreDNS 根据你在 plugin.cfg
内书写的顺序依次调用,而并非 Corefile
内的配置顺序
配置好以后直接执行 make
既可编译成功一个包含自定义插件的 CoreDNS 二进制文件(编译过程的 go mod
下载加速问题不在本文讨论范围内);你可以直接通过这个二进制测试插件的处理情况,当然这种测试不够直观,而且频繁修改由于 go mod
缓存等原因并不一定能保证每次编译的都包含最新插件代码,所以另一种方式请看下一章节
根据个人测试以及对源码的分析,在修改 plugin.cfg
然后执行 make
命令后,实际上是进行了代码生成;当你通过 git 命令查看相关修改文件时,整个插件加载体系便没什么秘密可言了;在整个插件体系中,插件加载是通过 init
方法注册的,那么既然用 go 写插件,那么应该清楚 init
方法只有在包引用之后才会执行,所以整个插件体系实际上是这样事儿的:
首先 make
以后会修改 core/plugin/zplugin.go
文件,这个文件啥也不干,就是 import
来实现调用对应包的 init
方法
当 init
执行后你去追源码,实际上就是 Caddy 维护了一个 map[string]Plugin
,init
会把你的插件 func 塞进去然后后面再调用,实现一个懒加载或者说延迟初始化
接着修改了一下 core/dnsserver/zdirectives.go
,这个里面也没啥,就是一个 []string
,但是 []string
这玩意有顺序啊,这就是为什么你在 plugin.cfg
里写的顺序决定了插件处理顺序的原因(因为生成的这个切片有顺序)
综上所述,实际上 make
命令一共修改了两个文件,如果想在 IDE 内直接 debug CoreDNS + Plugin 源码,那么只需要这样做:
复制自己编写的插件目录到 plugin
目录,类似这样
手动修改 core/plugin/zplugin.go
,加入自己插件的 import
(此时你直接复制系统其他插件,改一下目录名既可)
手动修改 core/dnsserver/zdirectives.go
把自己插件名称写进去(自己控制顺序),然后 debug 启动 coredns.go
里面的 main 方法测试既可
准备开发点东西,需要用到 Etcd,由于生产 Etcd 全部开启了 TLS 加密,所以客户端需要相应修改,以下为 Golang 链接 Etcd 并且使用客户端证书验证的样例代码
1 |
|
1 |
|
这是一篇纯介绍性文章,本文不包含任何技术层面的操作,本文仅作为后续 Podman 文章铺垫;本文细节部份并未阐述,很多地方并不详实(一家只谈,不可轻信)。
在上古时期,天地初开,一群称之为 “运维” 的人们每天在一种叫作 “服务器” 的神秘盒子中创造属于他们的世界;他们在这个世界中每日劳作,一遍又一遍的写入他们的历史,比如搭建一个 nginx、布署一个 java web 应用…
大多数人其实并没有那么聪明,他们所 “创造” 的事实上可能是有人已经创造过的东西,他们可能每天都在做着重复的劳动;久而久之,一些人厌倦了、疲惫了…又过了一段时间,一些功力深厚的老前辈创造了一些批量布署工具来帮助人们做一些重复性的劳动,这些工具被起名为 “Asible”、”Chef”、”Puppet” 等等…
而随着时代的发展,”世界” 变得越来越复杂,运维们需要处理的事情越来越多,比如各种网络、磁盘环境的隔离,各种应用服务的高可用…在时代的洪流下,运维们急需要一种简单高效的布署工具,既能有一定的隔离性,又能方便使用,并且最大程度降低重复劳动来提升效率。
在时代洪流的冲击下,一位名为 “Solomon Hykes” 的人异军突起,他创造了一个称之为 Docker 的工具,Docker 被创造以后就以灭世之威向运维们展示了它的强大;一个战斗力只有 5 的运维只需要学习 Docker 很短时间就可以完成资深运维们才能完成的事情,在某些情况下以前需要 1 天才能完成的工作使用 Docker 后几分钟就可以完成;此时运维们已经意识到 “新的时代” 开启了,接下来 Docker 开源并被整个运维界人们使用,Docker 也不断地完善增加各种各样的功能,此后世界正式进入 “容器纪元”。
随着 Docker 的日益成熟,一些人开始在 Docker 之上创造更加强大的工具,一些人开始在 Docker 之下为其提供更稳定的运行环境…
其中一个叫作 Google 的公司在 Docker 之上创建了名为 “Kubernetes” 的工具,Kubernetes 操纵 Docker 完成更加复杂的任务;Kubernetes 的出现更加印证了 Docker 的强大,以及 “容器纪元” 的发展正确性。
当然这是一个充满利益的世界,Google 公司创造 Kubernetes 是可以为他们带来利益的,比如他们可以让 Kubernetes 深度适配他们的云平台,以此来增加云平台的销量等;此时 Docker 创始人也成立了一个公司,提供 Docker 的付费服务以及深度定制等;不过值得一提的是 Docker 公司提供的付费服务始终没有 Kubernetes 为 Google 公司带来的利益高,所以在利益的驱使下,Docker 公司开始动起了歪心思: 创造一个 Kubernetes 的替代品,利用用户粘度复制 Kubernetes 的成功,从 Google 嘴里抢下这块蛋糕!此时 Docker 公司只想把蛋糕抢过来,但是他们根本没有在意到暗中一群人创造了一个叫 “rkt” 的东西也在妄图夺走他们嘴里的蛋糕。
在一段时间的沉默后,Docker 公司又创造了 “Swarm” 这个工具,妄图夺走 Google 公司利用 Kubernetes 赢来的蛋糕;当然,Google 这个公司极其庞大,人数众多,而且在这个社会有很大的影响地位…
终于,巨人苏醒了,Google 联合了 Redhat、Microsoft、IBM、Intel、Cisco 等公司决定对这个爱动歪脑筋的 Docker 公司进行制裁;当然制裁的手段不能过于暴力,那样会让别人落下把柄,成为别人的笑料,被人所不耻;最总他们决定制订规范,成立组织,明确规定 Docker 的角色,以及它应当拥有的能力,这些规范包括但不限于 CRI
、CNI
等;自此之后各大公司宣布他们容器相关的工具只兼容 CRI 等相关标准,无论是 Docker 还是 rkt 等工具,只要实现了这些标准,就可以配合这些容器工具进行使用。
自此之后,Docker 跌下神坛,各路大神纷纷创造满足 CRI 等规范的工具用来取代 Docker,Docker 丢失了往日一家独大的场面,最终为了顺应时代发展,拆分自己成为模块化组件;这些模块化组件被放置在 mobyproject 中方便其他人重复利用。
时至今日,虽然 Docker 已经不负以前,但是仍然是容器化首选工具,因为 Docker 是一个完整的产品,它可以提供除了满足 CRI 等标准以外更加方便的功能;但是制裁并非没有结果,Google 公司借此创造了 cri-o 用来满足 CRI 标准,其他公司也相应创建了对应的 CRI 实现;为了进一步分化 Docker 势力,一个叫作 Podman 的工具被创建,它以 cri-o 为基础,兼容大部份 Docker 命令的方式开始抢夺 Dcoker 用户;到目前为止 Podman 已经可以在大部份功能上替代 Docker。
]]>由于开发有部份服务使用 GRPC 进行通讯,同时采用 Consul 进行服务发现;在微服务架构下可能会导致一些访问问题,目前解决方案就是打通开发环境网络与测试环境 Kubernetes 内部 Pod 网络;翻了好多资料发现都是 2.x 的,而目前测试集群 Calico 版本为 3.6.3,很多文档都不适用只能自己折腾,目前折腾完了这里记录一下
本文默认为读者已经存在一个运行正常的 Kubernetes 集群,并且采用 Calico 作为 CNI 组件,且 Calico 工作正常;同时应当在某个节点完成了 calicoctl 命令行工具的配置
在微服务架构下,由于服务组件很多,开发在本地机器想测试应用需要启动整套服务,这对开发机器的性能确实是个考验;但如果直接连接测试环境的服务,由于服务发现问题最终得到的具体服务 IP 是 Kubernetes Pod IP,此 IP 由集群内部 Calico 维护与分配,外部不可访问;最终目标为打通开发环境与集群内部网络,实现开发网络下直连 Pod IP,这或许在以后对生产服务暴露负载均衡有一定帮助意义;目前网络环境如下:
开发网段: 10.10.0.0/24
测试网段: 172.16.0.0/24
Kubernetes Pod 网段: 10.20.0.0/16
首先面临的第一个问题是 Calico 处理,因为如果想要让数据包能从开发网络到达 Pod 网络,那么必然需要测试环境宿主机上的 Calico Node 帮忙转发;因为 Pod 网络由 Calico 维护,只要 Calico Node 帮忙转发那么数据一定可以到达 Pod IP 上;
一开始我很天真的认为这就是个 ip route add 10.20.0.0/16 via 172.16.0.13
的问题… 后来发现
经过翻文档、issue、blog 等最终发现需要进行以下步骤
注意: 关闭全互联时可能导致网络暂时中断,请在夜深人静时操作
首先执行以下命令查看是否存在默认的 BGP 配置
1 |
|
如果存在则将其保存为配置文件
1 |
|
修改其中的 spec.nodeToNodeMeshEnabled
为 false
,然后进行替换
1 |
|
如果不存在则手动创建一个配置,然后应用
1 |
|
本部分参考:
在 Calico 3.3 后支持了集群内节点的 RR 模式,即将某个集群内的 Calico Node 转变为 RR 节点;将某个节点设置为 RR 节点只需要增加 routeReflectorClusterID
既可,为了后面方便配置同时增加了一个 lable 字段 route-reflector: "true"
1 |
|
然后增加 routeReflectorClusterID
字段,样例如下
1 |
|
事实上我们应当导出多个 Calico Node 的配置,并将其配置为 RR 节点以进行冗余;对于 routeReflectorClusterID
目前测试只是作为一个 ID(至少在本文是这样的),所以理论上可以是任何 IP,个人猜测最好在同一集群网络下采用相同的 IP,由于这是真正的测试环境我没有对 ID 做过多的测试(怕玩挂)
修改完成后只需要应用一下就行
1 |
|
接下来需要创建对等规则,规则文件如下
1 |
|
假定规则文件名称为 rr.yaml
,则创建命令为 calicoctl create -f rr.yaml
;此时在 RR 节点上使用 calicoctl node status
应该能看到类似如下输出
1 |
|
PEER ADDRESS
应当包含所有非 RR 节点 IP(由于真实测试环境,以上输出已人为修改)
同时在非 RR 节点上使用 calicoctl node status
应该能看到以下输出
1 |
|
PEER ADDRESS
应当包含所有 RR 节点 IP,此时原本的 Pod 网络连接应当已经恢复
本部分参考:
先说一下 Calico IPIP 模式的三个可选项:
Always
: 永远进行 IPIP 封装(默认)CrossSubnet
: 只在跨网段时才进行 IPIP 封装,适合有 Kubernetes 节点在其他网段的情况,属于中肯友好方案Never
: 从不进行 IPIP 封装,适合确认所有 Kubernetes 节点都在同一个网段下的情况在默认情况下,默认的 ipPool 启用了 IPIP 封装(至少通过官方安装文档安装的 Calico 是这样),并且封装模式为 Always
;这也就意味着任何时候都会在原报文上封装新 IP 地址,在这种情况下将外部流量路由到 RR 节点,RR 节点再转发进行 IPIP 封装时,可能出现网络无法联通的情况(没仔细追查,网络渣,猜测是 Pod 那边得到的源 IP 不对导致的);此时我们应当调整 IPIP 封装策略为 CrossSubnet
导出 ipPool 配置
1 |
|
修改 ipipMode
值为 CrossSubnet
1 |
|
重新使用 calicoctl apply -f ippool.yaml
应用既可
本部分参考:
万事俱备只欠东风,最后只需要在开发机器添加路由既可
将 Pod IP 10.20.0.0/16
和 Service IP 10.254.0.0/16
路由到 RR 节点 172.16.0.13
1 |
|
当然最方便的肯定是将这一步在开发网络的路由上做,设置完成后开发网络就可以直连集群内的 Pod IP 和 Service IP 了;至于想直接访问 Service Name 只需要调整上游 DNS 解析既可
]]>最近在调整公司项目的 CI,目前主要使用 GitLab CI,在尝试多阶段构建中踩了点坑,然后发现了一些有意思的玩意
本文参考:
公司目前主要使用 GitLab CI 作为主力 CI 构建工具,而且由于机器有限,我们对一些包管理器的本地 cache 直接持久化到了本机;比如 maven 的 .m2
目录,nodejs 的 .npm
目录等;虽然我们创建了对应的私服,但是在 build 时毕竟会下载,所以当时索性调整 GitLab Runner 在每个由 GitLab Runner 启动的容器中挂载这些缓存目录(GitLab CI 在 build 时会新启动容器运行 build 任务);今天调整 nodejs 项目浪了一下,直接采用 Dockerfile 的 multi-stage build 功能进行 “Build => Package(docker image)” 的实现,基本 Dockerfile 如下
1 |
|
本来这个 cnpm
命令是带有 cache 的(见这里),不过运行完 build 以后发现很慢,检查宿主机 cache 目录发现根本没有 cache…然后突然感觉
仔细想想,情况应该是这样事儿的…
1 |
|
后来经过查阅文档,发现 Dockerfile 是有扩展语法的(当然最终我还是没用),具体请见下篇文章(我怕被打死)下面,先说好,下面的内容无法完美的解决上面的问题,目前只是支持了一部分功能,当然未来很可能支持类似 IF ELSE
语法、直接挂载宿主机目录等功能
目前这个扩展语法还处于实验性功能,所以需要配置 dockerd 守护进程,修改如下
1 |
|
主要是 --experimental
参数,参考官方文档;同时在 build 前声明 export DOCKER_BUILDKIT=1
变量
开启实验性功能后,只需要在 Dockerfile 头部增加 # syntax=docker/dockerfile:experimental
既可;为了保证稳定性,你也可以指定具体的版本号,类似这样
1 |
|
RUN --mount=type=bind
这个是默认的挂载模式,这个允许将上下文或者镜像以可都可写/只读模式挂载到 build 容器中,可选参数如下(不翻译了)
Option | Description |
---|---|
target (required) | Mount path. |
source | Source path in the from . Defaults to the root of the from . |
from | Build stage or image name for the root of the source. Defaults to the build context. |
rw ,readwrite | Allow writes on the mount. Written data will be discarded. |
RUN --mount=type=cache
专用于作为 cache 的挂载位置,一般用于 cache 包管理器的下载等
Option | Description |
---|---|
id | Optional ID to identify separate/different caches |
target (required) | Mount path. |
ro ,readonly | Read-only if set. |
sharing | One of shared , private , or locked . Defaults to shared . A shared cache mount can be used concurrently by multiple writers. private creates a new mount if there are multiple writers. locked pauses the second writer until the first one releases the mount. |
from | Build stage to use as a base of the cache mount. Defaults to empty directory. |
source | Subpath in the from to mount. Defaults to the root of the from . |
Example: cache Go packages
1 |
|
Example: cache apt packages
1 |
|
RUN --mount=type=tmpfs
专用于挂载 tmpfs 的选项
Option | Description |
---|---|
target (required) | Mount path. |
RUN --mount=type=secret
这个类似 k8s 的 secret,用来挂载一些不想打入镜像,但是构建时想使用的密钥等,例如 docker 的 config.json
,S3 的 credentials
Option | Description |
---|---|
id | ID of the secret. Defaults to basename of the target path. |
target | Mount path. Defaults to /run/secrets/ + id . |
required | If set to true , the instruction errors out when the secret is unavailable. Defaults to false . |
mode | File mode for secret file in octal. Default 0400. |
uid | User ID for secret file. Default 0. |
gid | Group ID for secret file. Default 0. |
Example: access to S3
1 |
|
注意: buildctl
是 BuildKit 的命令,你要测试的话自己换成 docker build
相关参数
1 |
|
RUN --mount=type=ssh
允许 build 容器通过 SSH agent 访问 SSH key,并且支持 passphrases
Option | Description |
---|---|
id | ID of SSH agent socket or key. Defaults to “default”. |
target | SSH agent socket path. Defaults to /run/buildkit/ssh_agent.${N} . |
required | If set to true , the instruction errors out when the key is unavailable. Defaults to false . |
mode | File mode for socket in octal. Default 0600. |
uid | User ID for socket. Default 0. |
gid | Group ID for socket. Default 0. |
Example: access to Gitlab
1 |
|
1 |
|
你也可以直接使用宿主机目录的 pem 文件,但是带有密码的 pem 目前不支持
目前根据文档测试,当前的挂载类型比如 cache
类型,仅用于 multi-stage 内的挂载,比如你有 2+ 个构建步骤,cache
挂载类型能帮你在各个阶段内共享文件;但是它目前无法解决直接将宿主机目录挂载到 multi-stage 的问题(可以采取些曲线救国方案,但是很不优雅);但是未来还是很有展望的,可以关注一下
由于对国内输入法隐私问题的担忧,决定放弃搜狗等输入法;为了更加 Geek 一些,最终决定了折腾 Rime(鼠须管) 输入法,以下为一些折腾的过程
国际惯例先放点图压压惊
安装 Rime 没啥好说的,直接从官网下载最新版本的安装包既可;安装完成后配置文件位于 ~/Library/Rime
位置;在进行后续折腾之前我建议还是先 cp -r ~/Library/Rime ~/Library/Rime.bak
备份一下配置文件,以防制后续折腾挂了还可以还原;安装完成以后按 ⌘ + 反引号(~)
切换到 朙月拼音-简化字
既可开启简体中文输入
安装完成后在打字时可能出现乱码情况(俗称豆腐块),这是由于 Rime 默认 UTF-8 字符集比较大,预选词内会出现生僻字,而 mac 字体内又不包含这些字体,从而导致乱码;解决方案很简单,下载 花园明朝 A、B 两款字体安装既可,安装后重启一下就不会出现乱码了
官方并不建议直接修改原始的配置文件,因为输入法更新时会重新覆盖默认配置,可能导致某些自定义配置丢失;推荐作法是创建一系列的 patch 配置,通过类似打补丁替换这种方式来实现无感的增加自定义配置;
由于使用的是 朙月拼音-简化字
输入方案,所以需要创建 luna_pinyin_simp.custom.yaml
等配置文件,后面就是查文档 + 各种 Google 一顿魔改了;目前我将我自己用的配置放在了 Github 上,有需要的可以直接 clone 下来,用里面的配置文件直接覆盖 ~/Library/Rime
下的文件,然后重新部署既可,关于具体配置细节在下面写
皮肤配色配置方案位于 squirrel.custom.yaml
配置文件中,我的配置目前是参考搜狗输入法皮肤自己调试的;官方也提供了一些皮肤外观配置,详见 Gist;想要切换皮肤配色只需要修改 style/color_scheme
为相应的皮肤配色名称既可
1 |
|
快捷字符例如在中文输入法状态下可以直接输入 /dn
来调出特殊符号输入;这些配置位于 luna_pinyin_simp.custom.yaml
的 punctuator
配置中,我目前自行定义了一些,有需要的可以依葫芦画瓢直接修改
1 |
|
在第一次按 ⌘ + 反引号(~)
设置输入法时实际上我们可以看到很多的输入方案,而事实上很多方案我们根本用不上;想要删除和修改方案可以调整 default.custom.yaml
中的 schema_list
字段
1 |
|
实际上我只能用上第一个…毕竟写了好几年代码还得看键盘的人也只能这样了…
在刚安装完以后发现在中文输入法状态下输入英文,按 shift
键后字符上屏,然后还得回车一下,这就很让我难受…最后找到了这篇 Gist,目前将大写锁定、shift
键调整为了跟搜狗一致的配置,有需要调整的可以自行编辑 default.custom.yaml
中的 ascii_composer/switch_key
部分
1 |
|
Rime 默认的词库稍为有点弱,我们可以下载一些搜狗词库来进行扩展;不过搜狗词库格式默认是无法解析的,好在有人开发了工具可以方便的将搜狗细胞词库转化为 Rime 的格式(工具点击这里下载);目前该工具只支持 Windows(也有些别人写的 py 脚本啥的,但是我没用),所以词库转换这种操作还得需要一个 Windows 虚拟机;
转换过程很简单,先从搜狗词库下载一系列的 scel
文件,然后批量选中,接着调整一下输入和输出格式点击转换,最后保存成一个 txt
文本
光有这个文本还不够,我们要将它塞到词库的 yaml
配置里,所以新建一个词库配置文件 luna_pinyin.sougou.dict.yaml
,然后写上头部说明(注意最后三个点后面加一个换行)
1 |
|
接着只需要把生成好的词库 txt
文件内容粘贴到三个点下面既可;但是词库太多的话你会发现这个文本有好几十 M,一般编辑器打开都会卡死,解决这种情况只需要用命令行 cat
一下就行
1 |
|
最后修改 luna_pinyin.extended.dict.yaml
中的 import_tables
字段,加入刚刚新建的词库既可
1 |
|
由于长期撸码,24 小时离不开命令行,偶尔在中文输入法下输入了一些命令导致汉字直接出现在 terminal 上就很尴尬…这时候我们可以在 luna_pinyin.cn_en.dict.yaml
加入一些我们自己的专属词库,比如这样
1 |
|
配置后如果我在中文输入法下输入 git 则会自动匹配 git 这个单词,避免错误的键入中文字符;需要注意的是第一列代表上屏的字符,第二列代表输入的单词,即 “当输入第二列时候选词为第一列”;两列之间要用 tag 制表符隔开,记住不是空格
]]>使用 Ubuntu 作为生产容器系统好久了,但是 apt 源问题一致有点困扰: **由于众所周知的原因,官方源执行 apt update
等命令会非常慢;而国内有很多镜像服务,但是某些偶尔也会抽风(比如清华大源),最后的结果就是日常修改 apt 源…**Google 查了了好久发现事实上 apt 源是支持 mirror
协议的,从而自动选择可用的一个
废话不说多直接上代码,编辑 /etc/apt/sources.list
,替换为如下内容
1 |
|
当使用 mirror
协议后,执行 apt update
时会首先通过 http 访问 mirrors.ubuntu.com/mirrors.txt
文本;文本内容实际上就是当前可用的镜像源列表,如下所示
1 |
|
得到列表后 apt 会自动选择一个(选择规则暂不清楚,国外有文章说是选择最快的,但是不清楚这个最快是延迟还是网速)进行下载;**同时根据地区不通,官方也提供指定国家的 mirror.txt
**,比如中国的实际上可以设置为 mirrors.ubuntu.com/CN.txt
(我测试跟官方一样,推测可能是使用了类似 DNS 选优的策略)
现在已经解决了能同时使用多个源的问题,但是有些时候你会发现源的可用性检测并不是很精准,比如某个源只有 40k 的下载速度…不巧你某个下载还命中了,这就很尴尬;所以有时候我们可能需要自定义 mirror.txt
这个源列表,经过测试证明**只需要开启一个标准的 http server
能返回一个文本即可,不过需要注意只能是 http
,而不是 https
**;所以我们首先下载一下这个文本,把不想要的删掉;然后弄个 nginx,甚至 python -m http.server
把文本文件暴露出去就可以;我比较懒…扔 CDN 上了: http://oss.link/config/apt-mirrors.txt
关于源的精简,我建议将一些 edu
的删掉,因为敏感时期他们很不稳定;优选阿里云、网易、华为这种大公司的,比较有名的清华大的什么的可以留着,其他的可以考虑都删掉
年后回来有点懒,也有点忙;1.13 出来好久了,周末还是决定折腾一下吧
老样子,安装环境为 5 台 Ubuntu 18.04.2 LTS 虚拟机,其他详细信息如下
System OS | IP Address | Docker | Kernel | Application |
---|---|---|---|---|
Ubuntu 18.04.2 LTS | 192.168.1.51 | 18.09.2 | 4.15.0-46-generic | k8s-master、etcd |
Ubuntu 18.04.2 LTS | 192.168.1.52 | 18.09.2 | 4.15.0-46-generic | k8s-master、etcd |
Ubuntu 18.04.2 LTS | 192.168.1.53 | 18.09.2 | 4.15.0-46-generic | k8s-master、etcd |
Ubuntu 18.04.2 LTS | 192.168.1.54 | 18.09.2 | 4.15.0-46-generic | k8s-node |
Ubuntu 18.04.2 LTS | 192.168.1.55 | 18.09.2 | 4.15.0-46-generic | k8s-node |
所有配置生成将在第一个节点上完成,第一个节点与其他节点 root 用户免密码登录,用于分发文件;为了方便搭建弄了一点小脚本,仓库地址 ktool,本文后续所有脚本、配置都可以在此仓库找到;关于 cfssl 等基本工具使用,本文不再阐述
Etcd 仍然开启 TLS 认证,所以先使用 cfssl 生成相关证书
1 |
|
1 |
|
1 |
|
接下来执行生成即可;我建议在生产环境在证书内预留几个 IP,已防止意外故障迁移时还需要重新生成证书;证书默认期限为 10 年(包括 CA 证书),有需要加强安全性的可以适当减小
1 |
|
安装 Etcd 只需要将二进制文件放在可执行目录下,然后修改配置增加 systemd service 配置文件即可;为了安全性起见最好使用单独的用户启动 Etcd
1 |
|
关于配置文件目录结构如下(请自行复制证书)
1 |
|
1 |
|
1 |
|
最后三台机器依次修改 IP
、ETCD_NAME
然后启动即可,**生产环境请不要忘记修改集群 Token 为真实随机字符串 (ETCD_INITIAL_CLUSTER_TOKEN
变量)**启动后可以通过以下命令测试集群联通性
1 |
|
新版本已经越来越趋近全面 TLS + RBAC 配置,所以本次安装将会启动大部分 TLS + RBAC 配置,包括 kube-controler-manager
、kube-scheduler
组件不再连接本地 kube-apiserver
的 8080 非认证端口,kubelet
等组件 API 端点关闭匿名访问,启动 RBAC 认证等;为了满足这些认证,需要签署以下证书
1 |
|
1 |
|
1 |
|
10257
端口也会使用此证书1 |
|
10259
端口也会使用此证书1 |
|
1 |
|
10250
端口需要使用的证书(例如执行 kubectl logs
)1 |
|
1 |
|
注意: 请不要修改证书配置的 CN
、O
字段,这两个字段名称比较特殊,大多数为 system:
开头,实际上是为了匹配 RBAC 规则,具体请参考 Default Roles and Role Bindings
最后使用如下命令生成即可:
1 |
|
集群搭建需要预先生成一系列配置文件,生成配置需要预先安装 kubectl
命令,请自行根据文档安装 Install kubectl binary using curl;其中配置文件及其作用如下:
bootstrap.kubeconfig
kubelet TLS Bootstarp 引导阶段需要使用的配置文件kube-controller-manager.kubeconfig
controller manager 组件开启安全端口及 RBAC 认证所需配置kube-scheduler.kubeconfig
scheduler 组件开启安全端口及 RBAC 认证所需配置kube-proxy.kubeconfig
proxy 组件连接 apiserver 所需配置文件audit-policy.yaml
apiserver RBAC 审计日志配置文件bootstrap.secret.yaml
kubelet TLS Bootstarp 引导阶段使用 Bootstrap Token 方式引导,需要预先创建此 Token生成这些配置文件的脚本如下
1 |
|
新版本目前 kube-proxy
组件全部采用 ipvs 方式负载,所以为了 kube-proxy
能正常工作需要预先处理一下 ipvs 配置以及相关依赖(每台 node 都要处理)
1 |
|
master 节点上需要三个组件: kube-apiserver
、kube-controller-manager
、kube-scheduler
安装流程整体为以下几步
kube
用户/usr/bin
,可以采用 all in one
的 hyperkube
/etc/kubernetes
/etc/kubernetes/ssl
安装脚本如下所示:
1 |
|
hyperkube 是一个多合一的可执行文件,通过 --make-symlinks
会在当前目录生成 kubernetes 各个组件的软连接
被复制的 conf 目录结构如下(最终被复制到 /etc/kubernetes
)
1 |
|
以下为相关配置文件内容
systemd 配置如下
1 |
|
1 |
|
1 |
|
核心配置文件
1 |
|
配置解释:
选项 | 作用 |
---|---|
--client-ca-file | 定义客户端 CA |
--endpoint-reconciler-type | master endpoint 策略 |
--kubelet-client-certificate 、--kubelet-client-key | master 反向连接 kubelet 使用的证书 |
--service-account-key-file | service account 签名 key(用于有效性验证) |
--tls-cert-file 、--tls-private-key-file | master apiserver 6443 端口证书 |
1 |
|
controller manager 将不安全端口 10252
绑定到 127.0.0.1 确保 kuebctl get cs
有正确返回;将安全端口 10257
绑定到 0.0.0.0 公开,提供服务调用;由于 controller manager 开始连接 apiserver 的 6443
认证端口,所以需要 --use-service-account-credentials
选项来让 controller manager 创建单独的 service account(默认 system:kube-controller-manager
用户没有那么高权限)
1 |
|
shceduler 同 controller manager 一样将不安全端口绑定在本地,安全端口对外公开
最后在三台节点上调整一下 IP 配置,启动即可
node 安装与 master 安装过程一致,这里不再阐述
systemd 配置文件
1 |
|
1 |
|
核心配置文件
1 |
|
当 kubelet 组件设置了 --rotate-certificates
,--rotate-server-certificates
后会自动更新其使用的相关证书,同时指定 --authorization-mode=Webhook
后 10250
端口 RBAC 授权将会委托给 apiserver
1 |
|
由于 kubelet
组件是采用 TLS Bootstrap 启动,所以需要预先创建相关配置
1 |
|
为了保证 apiserver 的 HA,需要在每个 node 上部署 nginx 来反向代理(tcp)所有 apiserver;然后 kubelet、kube-proxy 组件连接本地 127.0.0.1:6443
访问 apiserver,以确保任何 master 挂掉以后 node 都不会受到影响
1 |
|
1 |
|
然后在每个 node 上先启动 nginx-proxy,接着启动 kubelet 与 kube-proxy 即可(master 上的 kubelet、kube-proxy 只需要修改 ip 和 node name)
注意: 新版本 kubelet server 的证书自动签发已经被关闭(看 issue 好像是由于安全原因),所以对于 kubelet server 的证书仍需要手动签署
1 |
|
当 node 全部启动后,由于网络组件(CNI)未安装会显示为 NotReady 状态;下面将部署 Calico 作为网络组件,完成跨节点网络通讯;具体安装文档可以参考 Installing with the etcd datastore
以下为 calico 的配置文件
1 |
|
需要注意的是我们添加了 IP_AUTODETECTION_METHOD
变量,这个变量会设置 calcio 获取 node ip 的方式;默认情况下采用 first-found 方式获取,即获取第一个有效网卡的 IP 作为 node ip;在某些多网卡机器上可能会出现问题;这里将值设置为 can-reach=192.168.1.51
,即使用第一个能够访问 master 192.168.1.51
的网卡地址作为 node ip
最后执行创建即可,创建成功后如下所示
1 |
|
此时所有 node 应当变为 Ready 状态
其他组件全部完成后我们应当部署集群 DNS 使 service 等能够正常解析;集群 DNS 这里采用 coredns,具体安装文档参考 coredns/deployment;coredns 完整配置如下
1 |
|
在大规模集群的情况下,可能需要集群 DNS 自动扩容,具体文档请参考 DNS Horizontal Autoscaler,DNS 扩容算法可参考 Github,如有需要请自行修改;以下为具体配置
1 |
|
为测试集群工作正常,我们创建一个 deployment 和一个 service,用于测试联通性和 DNS 工作是否正常;测试配置如下
1 |
|
测试方式很简单,进入某一个 pod ping 其他 pod ip 确认网络是否正常,直接访问 service 名称测试 DNS 是否工作正常,这里不再演示
此次搭建开启了大部分认证,限于篇幅原因没有将每个选项作用做完整解释,推荐搭建完成后仔细阅读以下 --help
中的描述(官方文档页面有时候更新不完整);目前 apiserver 仍然保留了 8080 端口(因为直接使用 kubectl 方便),但是在高安全性环境请关闭 8080 端口,因为即使绑定在 127.0.0.1 上,对于任何能够登录 master 机器的用户仍然能够不经验证操作整个集群
写这篇文章的目的是为了继续上篇 Kubernetes 1.12 新的插件机制 中最后部分对 Golang 的插件辅助库
说明;以及为后续使用 Golang 编写自己的 Kubernetes 插件做一个基础铺垫;顺边说一下 sample-cli-plugin 这个项目是官方为 Golang 开发者编写的一个用于快速切换配置文件中 Namespace 的一个插件样例
在开始分析源码之前,我们假设读者已经熟悉 Golang 语言,至少对基本语法、指针、依赖管理工具有一定认知;下面介绍一下 sample-cli-plugin 这个项目一些基础核心的依赖:
这是一个强大的 Golang 的 command line interface 库,其支持用非常简单的代码创建出符合 Unix 风格的 cli 程序;甚至官方提供了用于创建 cli 工程脚手架的 cli 命令工具;Cobra 官方 Github 地址 点击这里,具体用法请自行 Google,以下只做一个简单的命令定义介绍(docker、kubernetes 终端 cli 都基于这个库)
1 |
|
vendor 目录用于存放 Golang 的依赖库,sample-cli-plugin 这个项目采用 godep 工具管理依赖;依赖配置信息被保存在 Godeps/Godeps.json
中,一般项目不会上传 vendor 目录,因为它的依赖信息已经在 Godeps.json 中存在,只需要在项目下使用 godep restore
命令恢复就可自动重新下载;这里上传了 vendor 目录的原因应该是为了方便开发者直接使用 go get
命令安装;顺边说一下在 Golang 新版本已经开始转换到 go mod
依赖管理工具,标志就是项目下会有 go.mod
文件
这里准备一笔带过了,基本就是 clone 源码到 $GOPATH/src/k8s.io/sample-cli-plugin
目录,然后在 GoLand 中打开;目前我使用的 Go 版本为最新的 1.11.4;以下时导入源码后的截图
熟悉过 Cobra 库以后,再从整个项目包名上分析,首先想到的启动入口应该在 cmd
包下(一般 cmd
包下的文件都会编译成最终可执行文件名,Kubernetes 也是一样)
从以上截图中可以看出,首先通过 cmd.NewCmdNamespace
方法创建了一个 Command 对象 root
,然后调用了 root.Execute
就结束了;那么也就说明 root
这个 Command 是唯一的核心命令对象,整个插件实现都在这个 root
里;所以我们需要查看一下这个 cmd.NewCmdNamespace
是如何对它初始化的,找到 Cobra 中的 Run
或者 RunE
设置
定位到 NewCmdNamespace
方法以后,基本上就是标准的 Cobra 库的使用方式了;**从截图上可以看到,RunE
设置的函数总共运行了 3 个动作: o.Complete
、o.Validate
、o.Run
**;所以接下来我们主要分析这三个方法就行了
在分析上面说的这三个方法之前,我们还应当了解一下这个 o
是什么玩意
从源码中可以看到,o
这个对象由 NewNamespaceOptions
创建,而 NewNamespaceOptions
方法返回的实际上是一个 NamespaceOptions
结构体;接下来我们需要研究一下这个结构体都是由什么组成的,换句话说要基本大致上整明白结构体的基本结构,比如里面的属性都是干啥的
首先看下第一个属性 configFlags
,它的实际类型是 *genericclioptions.ConfigFlags
,点击查看以后如下
从这些字段上来看,我们可以暂且模糊的推测出这应该是个基础配置型的字段,负责存储一些全局基本设置,比如 API Server 认证信息等
下面这两个 resultingContext
、resultingContextName
就很好理解了,从名字上看就可以知道它们应该是用来存储结果集的 Context 信息的;当然这个 *api.Context
就是 Kubernetes 配置文件中 Context 的 Go 结构体
这几个字段从名字上就可以区分出,他们应该用于存储用户设置的或者说是通过命令行选项输入的一些指定配置信息,比如 Cluster、Context 等
rawConfig 这个变量名字有点子奇怪,不过它实际上是个 api.Config
;里面保存了与 API Server 通讯的配置信息;至于为什么要有这玩意,是因为配置信息输入源有两个: cli 命令行选项(eg: --namespace
)和用户配置文件(eg: ~/.kube/config
);最终这两个地方的配置合并后会存储在这个 rawConfig 里
这个变量实际上相当于一个 flag,用于存储插件是否使用了 --list
选项;在分析结构体这里没法看出来;不过只要稍稍的多看一眼代码就能看在 NewCmdNamespace
方法中有这么一行代码
介绍完了结构体的基本属性,最后我们只需要弄明白在核心 Command 方法内运行的这三个核心方法就行了
这个方法代码稍微有点多,这里不会对每一行代码都做解释,只要大体明白都在干什么就行了;我们的目的是理解它,后续模仿它创造自己的插件;下面是代码截图
从截图上可以看到,首先弄出了 rawConfig
这个玩意,rawConfig
上面也提到了,它就是终端选项和用户配置文件的最终合并,至于为什么可以查看 ToRawKubeConfigLoader().RawConfig()
这两个方法的注释和实现即可;
接下来就是各种获取插件执行所需要的变量信息,比如获取用户指定的 Namespace
、Cluster
、Context
等,其中还包含了一些必要的校验;比如不允许使用 kubectl ns NS_NAME1 --namespace NS_NAME2
这种操作(因为这么干很让人难以理解 “你到底是要切换到 NS_NAME1
还是 NS_NAME2
“)
最后从 153
行 o.resultingContext = api.NewContext()
开始就是创建最终的 resultingContext
对象,把获取到的用户指定的 Namespace
等各种信息赋值好,为下一步将其持久化到配置文件中做准备
这个方法看名字就知道,里面全是对最终结果的校验;比如检查一下 rawConfig
中的 CurrentContext
是否获取到了,看看命令行参数是否正确,确保你不会瞎鸡儿输入 kubectl ns NS_NAME1 NS_NAME2
这种命令
第一步合并配置信息并获取到用户设置(输入)的配置,第二部做参数校验;可以说前面的两步操作都是为这一步做准备,Run
方法真正的做了配置文件写入、终端返回结果打印操作
可以看到,Run
方法第一步就是更加谨慎的检查了一下参数是否正常,然后调用了 o.setNamespace
;这个方法截图如下
这个 setNamespace
是真正的做了配置文件写入动作的,实际写入方法就是 clientcmd.ModifyConfig
;这个是 Kubernetes
client-go
提供的方法,这些库的作用就是提供给我们非常方便的 API 操作;比如修改配置文件,你不需要关心配置文件在哪,你更不需要关系文件句柄是否被释放
从 o.setNamespace
方法以后其实就没什么看头了,毕竟插件的核心功能就是快速修改 Namespace
;下面的各种 for
循环遍历其实就是在做打印输出;比如当你没有设置 Namespace
而使用了 --list
选项,插件就通过这里帮你打印设置过那些 Namespace
分析完了这个官方的插件,然后想一下自己以后写插件可能的需求,最后对比一下,可以为以后写插件做个总结:
xxxOptions
这种结构体存存一些配置configFlags
、rawConfig
这两个基础配置信息转载请注明出n,本文采用 [CC4.0](http://c 1.12 新的插件机制](https://mritd.me/2018/11/30/kubectl-plugin-new-solution-on-kubernetes-1.12/) 中最后部分对 Golang 的插件辅助库
说明;以及为后续使用 Golang 编写自己的 Kubernetes 插件做一个基础铺垫;顺边说一下 sample-cli-plugin 这个项目是官方为 Golang 开发者编写的一个用于快速切换配置文件中 Namespace 的一个插件样例
在很久以前的版本研究过 kubernetes 的插件机制,当时弄了一个快速切换 namespace
的小插件;最近把自己本机的 kubectl 升级到了 1.12,突然发现插件不能用了;撸了一下文档发现插件机制彻底改了…
kubernetes 1.12 新的插件机制在编写语言上同以前一样,可以以任意语言编写,只要能弄一个可执行的文件出来就行,插件可以是一个 bash
、python
脚本,也可以是 Go
等编译语言最终编译的二进制;以下是一个 Copy 自官方文档的 bash
编写的插件样例
1 |
|
1.12 kubectl 插件最大的变化就是加载方式变了,由原来的放置在指定位置,还要为其编写 yaml 配置变成了现在的类似 git 扩展命令的方式: 只要放置在 PATH 下,并以 kubectl-
开头的可执行文件都被认为是 kubectl
的插件;所以你可以随便弄个小脚本(比如上面的代码),然后改好名字赋予可执行权限,扔到 PATH 下即可
同以前不通,以前版本的执行插件时,kubectl
会向插件传递一些特定的与 kubectl
相关的变量,现在则只会传递标准变量;即 kubectl
能读到什么变量,插件就能读到,其他的私有化变量(比如 KUBECTL_PLUGINS_CURRENT_NAMESPACE
)不会再提供
并且新版本的插件体系,所有选项(flag
) 将全部交由插件本身处理,kubectl 不会再解析,比如下面的 --help
交给了自定义插件处理,由于脚本内没有处理这个选项,所以相当于选项无效了
还有就是 传递给插件的第一个参数永远是插件自己的绝对位置,比如这个 test
插件在执行时的 $0
是 /usr/local/bin/kubectl-test
目前在插件命名及查找顺序上官方文档写的非常详尽,不给过对于普通使用者来说,实际上命名规则和查找与常规的 Linux 下的命令查找机制相同,只不过还做了增强;增强后的基本规则如下
PATH
优先匹配原则-
自动分割匹配以及智能转义PATH
优先匹配原则跟传统的命令查找一致,即当多个路径下存在同名的插件时,则采用最先查找到的插件
当你的插件文件名中包含 -
,并且 kubectl
在无法精确找到插件时会尝试自动拼接命令来尝试匹配;如下所示,在没有找到 kubectl-test
这个命令时会尝试拼接参数查找
由于以上这种查找机制,当命令中确实包含 -
时,必须进行转义以 _
替换,否则 kubectl
会提示命令未找到错误;替换后可直接使用 kubectl 插件命令(包含-)
执行,同时也支持以原始插件名称执行(使用 _
)
在复杂插件体系下,多个插件可能包含同样的前缀,此时将遵序最精确查找原则;即当两个插件 kubectl-test-aaa
、kubectl-test-aaa-bbb
同时存在,并且执行 kubectl test aaa bbb
命令时,优先匹配最精确的插件 kubectl-test-aaa-bbb
,而不是将 bbb
作为参数传递给 kubectl-test-aaa
插件
插件查找机制在一般情况下与传统 PATH 查找方式相同,同时 kubectl
实现了智能的 -
自动匹配查找、更精确的命令命中功能;这两种机制的实现主要为了方便编写插件的命令树(插件命令的子命令…),类似下面这种
1 |
|
当出现多个位置有同名插件时,执行 kubectl plugin list
能够检测出哪些插件由于 PATH 查找顺序原因导致永远不会被执行问题
1 |
|
由于插件机制的变更,导致其他语言编写的插件在实时获取某些配置信息、动态修改 kubectl
配置方面可能造成一定的阻碍;为此 kubernetes 提供了一个 command line runtime package,使用 Go 编写插件,配合这个库可以更加方便的解析和调整 kubectl
的配置信息
官方为了演示如何使用这个 cli-runtime 库编写了一个 namespace
切换的插件(自己白写了…),仓库地址在 Github 上,基本编译使用如下(直接 go get
后编译文件默认为目录名 cmd
)
1 |
|
限于篇幅原因,具体这个 cli-runtime
包怎么用请自行参考官方写的这个 sample-cli-plugin
(其实并不怎么 “simple”…)
本文参考文档:
]]>迫于 Github 上 Star 的项目有点多,今天整理一下一些有意思的 Go 编写的小工具;大多数为终端下的实用工具,装逼的比如天气预报啥的就不写了
强大的文件同步工具,构建私人同步盘 👉 Github
一个强大的终端文件浏览器 👉 Github
http 负载测试工具,简单好用 👉 Github
1 |
|
http 负载测试工具,功能强大 👉 Github
1 |
|
功能强大的 Docker 镜像分析工具,可以查看每层镜像的具体差异等 👉 Github
容器运行时资源分析,如 CPU、内存消耗等 👉 Github
Google 推出的工具,功能就顾名思义了 👉 Github
快捷的终端文件分享工具 👉 Github
Linux/FreeBSD 漏洞扫描工具 👉 Github
高性能安全的文件备份工具 👉 Github
使用 sql 的方式查询 git 提交 👉 Github
帮助生成满足 Gitflow 格式 commit message 的小工具(自己写的) 👉 Github
对主流的 Gitflow 格式的 commit message 生成 CHANGELOG 👉 Github
一个 git 终端图形化浏览工具 👉 Github
命令行 json 格式化处理工具,类似 jq,不过感觉更加强大 👉 Github
类似 youget 的一个视频下载工具,可以解析大部分视频网站直接下载 👉 Github
1 |
|
Linux 下管道式终端搜索工具 👉 Github
Let’s Encrypt 证书申请工具 👉 Github
1 |
|
贼好用的终端命令异步执行通知工具 👉 Github
临时切换到指定用户运行特定命令,方便测试权限问题 👉 Github
1 |
|
类似 Ansible 的一个批量执行工具,暂且称之为低配版 Ansible 👉 Github
Debian 仓库管理工具 👉 Github
支持无限跳板机登录的 ssh 小工具(自己写的) 👉 Github
]]>最近在看 kubeadm 的源码,不过有些东西光看代码还是没法太清楚,还是需要实际运行才能看到具体代码怎么跑的,还得打断点 debug;无奈的是本机是 mac,debug 得在 Linux 下,so 研究了一下 remote debug
这里不会详细写如何安装 Go 开发环境以及 GoLand 安装,本文默认读者已经至少已经对 Go 开发环境以及代码有一定了解;顺便提一下 GoLand,这玩意属于 jetbrains 系列 IDE,在大约 2018.1 版本后在线激活服务器已经全部失效,不过网上还有其他本地离线激活工具,具体请自行 Google,如果后续工资能支撑得起,请补票支持正版(感恩节全家桶半价真香😂)
需要注意的是 Kubernetes 源码虽然托管在 Github,但是在使用 go get
的时候要使用 k8s.io
域名
1 |
|
go get
命令是接受标准的 http 代理的,这个源码下载会非常慢,源码大约 1G 左右,所以最好使用加速工具下载
1 |
|
delve 是一个 Golang 的 debug 工具,有点类似 gdb,不过是专门针对 Golang 的,GoLand 的 debug 实际上就是使用的这个开源工具;为了进行远程 debug,运行 kubeadm 的机器必须安装 delve,从而进行远程连接
1 |
|
默认情况下直接编译出的 kubeadm 是无法进行 debug 的,因为 Golang 的编译器会进行编译优化,比如进行内联等;所以要关闭编译优化和内联,方便 debug
1 |
|
将编译好的 kubeadm 复制到远程,并且使用 delve 启动它,此时 delve 会监听 api 端口,GoLand 就可以远程连接过来了
1 |
|
注意: 要指定需要 debug 的 kubeadm 的子命令,否则可能出现连接上以后 GoLand 无反应的情况
在 GoLand 中打开 kubernetes 源码,在需要 debug 的代码中打上断点,这里以 init 子命令为例
首先新建一个远程 debug configuration
名字可以随便写,主要是地址和端口
接下来在目标源码位置打断点,以下为 init 子命令的源码位置
最后只需要点击 debug 按钮即可
在没有运行 GoLand debug 之前,目标机器的实际指令是不会运行的,也就是说在 GoLand 没有连接到远程 delve 启动的 kubeadm init
命令之前,kubeadm init
并不会真正运行;当点击 GoLand 的终止 debug 按钮后,远程的 delve 也会随之退出
重装了 mac 系统,由于一些公司项目必须使用 Oracle JDK(验证码等组件用了一些 Oracle 独有的 API) 所以又得重新安装;但是 Oracle 只提供了 pkg 的安装方式,研究半天找到了一个解包 pkg 的安装方式,这里记录一下
不使用 pkg 的原因是每次更新版本都要各种安装,最烦人的是 IDEA 选择 JDK 时候弹出的文件浏览器没法进入到这种方式安装的 JDK 的系统目录…mmp,后来从国外网站找到了一篇文章,基本套路如下
pkgutil --expand your_jdk.pkg jdkdir
cd jdkdir/jdk_version.pkg && cpio -idv < Payload
mv Contents/Home ~/myjdk
原文地址: OS X: Extract JDK to folder, without running installer
]]>最近在写一个跳板机登录的小工具,其中涉及到了用 Go 来进行交互式执行命令,简单地说就是弄个终端出来;一开始随便 Google 了一下,copy 下来基本上就是能跑了…但是后来发现了一些各种各样的小问题,强迫症的我实在受不了,最后翻了一下 Teleport 的源码,从中学到了不少有用的知识,这里记录一下
不想看太多可以直接跳转到 第三部分 拿代码
一开始随便 Google 出来的代码,copy 上就直接跑;代码基本如下:
1 |
|
以上代码跑起来后,基本上遇到了以下问题:
ls
出现两行关于回显问题,实际上解决方案很简单,设置当前终端进入 raw
模式即可;代码如下:
1 |
|
代码很简单,网上一大堆,But…基本没有文章详细说这个 raw
模式到底是个啥玩意;好在万能的 StackOverflow 对于不熟悉 Linux 的人给出了一个很清晰的解释: What’s the difference between a “raw” and a “cooked” device driver?
大致意思就是说 在终端处于 Cooked
模式时,当你输入一些字符后,默认是被当前终端 cache 住的,在你敲了回车之前这些文本都在 cache 中,这样允许应用程序做一些处理,比如捕获 Cntl-D
等按键,这时候就会出现敲回车后本地终端帮你打印了一下,导致出现类似回显的效果;当设置终端为 raw
模式后,所有的输入将不被 cache,而是发送到应用程序,在我们的代码中表现为通过 io.Copy
直接发送到了远端 shell 程序
当本地调整了终端大小后,远程终端毫无反应;后来发现在 *ssh.Session
上有一个 WindowChange
方法,用于向远端发送窗口调整事件;解决方案就是启动一个 goroutine
在后台不断监听窗口改变事件,然后调用 WindowChange
即可;代码如下:
1 |
|
这两个问题实际上都是由于我们直接对接了 stderr
、stdout
和 stdin
造成的,实际上我们应当启动一个异步的管道式复制行为,并且最好带有 buf 的发送;代码如下:
1 |
|
1 |
|
折腾 Go 已经有一段时间了,最近在用 Go 写点 web 的东西;在搭建脚手架的过程中总是有点不适应,尤其对可扩展性上总是感觉没有 Java 那么顺手;索性看了下 coredns 的源码,最后追踪到 caddy 源码;突然发现他们对代码内的 plugin 机制有一些骚套路,这里索性记录一下
纵观现在所有的 Go web 框架,在文档上可以看到使用方式很简明;非常符合我对 Go 的一贯感受: “所写即所得”;就拿 Gin 这个来说,在 README.md 上可以很轻松的看到 engine
或者说 router
这玩意的使用,比如下面这样:
1 |
|
乍一看简单到爆,但实际使用中,在脚手架搭建上,我们需要规划好 包结构、配置文件、命令行参数、数据库连接、cache 等等;直到目前为止,至少我没有找到一种非常规范的后端 MVC 的标准架子结构;这点目前确实不如 Java 的生态;作为最初的脚手架搭建者,站在这个角度,我想我们更应当考虑如何做好适当的抽象、隔离;以防止后面开发者对系统基础功能可能造成的破坏。
综上所述,再配合 Gin 或者说 Go 的代码风格,这就形成了一种强烈的冲突;在 Java 中,由于有注解(Annotation
)的存在,事实上你是可以有这种操作的: 新建一个 Class,创建 func,在上面加上合适的注解,最终框架会通过注解扫描的方式以适当的形式进行初始化;而 Go 中并没有 Annotation
这玩意,我们很难实现在 代码运行时扫描自身做出一种策略性调整;从而下面这个需求很难实现: 作为脚手架搭建者,我希望我的基础代码安全的放在一个特定位置,后续开发者开发应当以一种类似可热插拔的形式注入进来,比如 Gin 的 router 路由设置,我不希望每次有修改都会有人动我的 router 核心配置文件。
在翻了 coredns 的源码后,我发现他是依赖于 Caddy 这框架运行的,coredns 的代码内的插件机制也是直接调用的 Caddy;所以接着我就翻到了 Caddy 源码,其中的代码如下(完整代码点击这里):
1 |
|
套路很清奇,为了实现我上面说的那个需求: “后面开发不需要动我核心代码,我还能允许他们动态添加”,Caddy 套路就是定义一个 map,map 里用于存放一种特定形式的 func,并且暴露出一个方法用于向 map 内添加指定 func,然后在合适的时机遍历这个 map,并执行其中的 func。这种套路利用了 Go 函数式编程的特性,将行为先存储在容器中,然后后续再去调用这些行为。
长篇大论这么久,实际上我也是在一边折腾 Go 的过程中一边总结和对比跟 Java 的差异;在 Java 中扫描自己注解的套路 Go 中没法实现,但是 Go 利用其函数式编程的优势也可以利用一些延迟加载方式实现对应的功能;总结来说,不同语言有其自己的特性,当有对比的时候,可能更加深刻。
]]>玩 Kubenretes 的基本都很清楚,Kubernetes 很多组件的镜像全部托管在 gcr.io
这个域名下(现在换成了 k8s.gcr.io
);由于众所周知的原因,这个网站在国内是不可达的;当时由于 Docker Hub 提供了 Auto Build
功能,机智的想到一个解决办法;就是利用 Docker Hub 的 Auto Build
,创建只有一行的 Dockerfile,里面就一句 FROM gcr.io/xxxx
,然后让 Docker Hub 帮你构建完成后拉取即可
这种套路的基本方案就是利用一个第三方公共仓库,这个仓库可以访问不可达的 gcr.io
,然后生成镜像,我们再从这个仓库 pull 即可;为此我创建了一个 Github 仓库(docker-library);时隔这么久以后,我猜想大家都已经有了这种自己的仓库…不过最近发现这个仓库仍然在有人 fork…
为了一劳永逸的解决这个问题,只能撸点代码解决这个问题了
为了解决上述问题,我写了一个 gcrsync 工具,并且借助 Travis CI 让其每天自动运行,将所有用得到的 gcr.io
下的镜像同步到了 Docker Hub
目前对于一个 gcr.io
下的镜像,可以直接替换为 gcrxio
用户名,然后从 Docker Hub 直接拉取,以下为一个示例:
1 |
|
为了保证同步镜像的安全性,同步工具已经开源在 gcrsync 仓库,同步细节如下:
gcr.io
指定 namespace
下的所有镜像(namesapce
由 .travis.yml script
段定义)gcr.io
镜像后,再读取元数据仓库(gcr) 中与 namesapce
同名文件(实际是个 json)pull
、tag
、push
操作,完成镜像推送namespace
对应的 json 文件,最后在生成 CHANGELOG,执行 git push
到远程元数据仓库综上所述,如果想得知具体 gcrxio
用户下都有那些镜像,可直接访问 gcr 元数据仓库,查看对应 namesapce
同名的 json 文件即可;每天增量同步的信息会追加到 gcr 仓库的 CHANGELOG.md
文件中
为方便审查镜像安全性,以下为 gcrsync 工具的代码简介,代码仓库文件如下:
1 |
|
cmd 目录下为标准的 cobra
框架生成的子命令文件,其中每个命令包含了对应的 flag 设置,如 namesapce
、proxy
等;pkg/gcrsync
目录下的文件为核心代码:
docker.go
包含了对本地 docker daemon API 调用,包括 pull
、tag
、push
操作gcr.go
包含了对 gcr.io
指定 namespace
下镜像列表获取操作registry.go
包含了对 Docker Hub 下指定用户(默认 gcrxio
)的镜像列表获取操作(其主要用于首次执行 compare
命令生成 json 文件)sync.go
为主要的程序入口,其中包含了对其他文件内方法的调用,设置并发池等该仓库不保证镜像实时同步,默认每天同步一次(由 Travis CI 执行),如有特殊需求,如增加 namesapce
等请开启 issue;最后,请不要再 fork docker-library 这个仓库了
最近在测试 Kubernetes 1.11.2 新版本的相关东西,发现新版本的 Bootstrap Token 功能已经进入 Beta 阶段,索性便尝试了一下;虽说目前是为 kubeadm 设计的,不过手动挡用起来也不错,这里记录一下使用方式
首先需要有一个运行状态正常的 Master 节点,目前我测试的是版本是 1.11.2,低版本我没测试;其次本文默认 Node 节点 Docker、kubelet 二进制文件、systemd service 配置等都已经处理好,更具体的环境如下:
Master 节点 IP 为 192.168.1.61
,Node 节点 IP 为 192.168.1.64
1 |
|
在正式进行 TLS Bootstrapping 操作之前,**如果对 TLS Bootstrapping 完全没接触过的请先阅读 Kubernetes TLS bootstrapping 那点事**;我想这里有必要简单说明下使用 Token 时整个启动引导过程:
Bootstrap Token Secret
,该 Secret 将替代以前的 token.csv
内置用户声明文件bootstrap.kubeconfig
以供 kubelet 启动时使用bootstrap.kubeconfig
并使用其中的 TLS Bootstrapping Token 完成首次证书申请Bootstrap Token Secret
,或等待 Controller Manager 待其过期后删除,以防止被恶意利用第二部分算作大纲了,这部分将会按照第二部分的总体流程来走,同时会对一些细节进行详细说明
既然整个功能都时刻强调这个 Token,那么第一步肯定是生成一个 token,生成方式如下:
1 |
|
这个 47f392.d22d04e89a65eb22
就是生成的 Bootstrap Token,保存好 token,因为后续要用;关于这个 token 解释如下:
Token 必须满足 [a-z0-9]{6}\.[a-z0-9]{16}
格式;以 .
分割,前面的部分被称作 Token ID
,Token ID
并不是 “机密信息”,它可以暴露出去;相对的后面的部分称为 Token Secret
,它应该是保密的
本部分官方文档地址 Token Format
对于 Kubernetes 来说 Bootstrap Token Secret
也仅仅是一个特殊的 Secret
而已;对于这个特殊的 Secret
样例 yaml 配置如下:
1 |
|
需要注意几点:
Bootstrap Token Secret
的 type 必须为 bootstrap.kubernetes.io/token
,name 必须为 bootstrap-token-<token id>
(Token ID 就是上一步创建的 Token 前一部分)usage-bootstrap-authentication
、usage-bootstrap-signing
必须存才且设置为 true
(我个人感觉 usage-bootstrap-signing
可以没有,具体见文章最后部分)expiration
字段是可选的,如果设置则 Secret
到期后将由 Controller Manager 中的 tokencleaner
自动清理auth-extra-groups
也是可选的,令牌的扩展认证组,组必须以 system:bootstrappers:
开头最后使用 kubectl create -f bootstrap.secret.yaml
创建即可
本部分官方文档地址 Bootstrap Token Secret Format
具体都有哪些 ClusterRole
和 ClusterRoleBinding
,以及其作用请参考上一篇的 Kubernetes TLS bootstrapping 那点事,不想在这里重复了
在 1.8 以后三个 ClusterRole
中有两个已经有了,我们只需要创建剩下的一个即可:
1 |
|
然后是三个 ClusterRole
对应的 ClusterRoleBinding
;需要注意的是 在使用 Bootstrap Token
进行引导时,Kubelet 组件使用 Token 发起的请求其用户名为 system:bootstrap:<token id>
,用户组为 system:bootstrappers
;so 我们在创建 ClusterRoleBinding
时要绑定到这个用户或者组上;当然我选择懒一点,全部绑定到组上
1 |
|
关于本部分首次请求用户名变为 system:bootstrap:<token id>
官方文档原文如下:
Tokens authenticate as the username system:bootstrap:
and are members of the group system:bootstrappers. Additional groups may be specified in the token’s Secret.
根据官方文档描述,Controller Manager 需要启用 tokencleaner
和 bootstrapsigner
(目测这个 bootstrapsigner
实际上并不需要,顺便加着吧),完整配置如下(为什么贴完整配置? 文章凑数啊…):
1 |
|
前面所有步骤实际上都是在处理 Api Server、Controller Manager 这一块,为的就是 “老子启动后 TLS Bootstarpping 发证书申请你两个要立马允许,不能拒绝老子”;接下来就是比较重要的 bootstrap.kubeconfig
配置生成,这个 bootstrap.kubeconfig
是最终被 Kubelet 使用的,里面包含了相关的 Token,以帮助 Kubelet 在第一次通讯时能成功沟通 Api Server;生成方式如下:
1 |
|
Kubelet 启动参数需要做一些相应调整,以使其能正确的使用 Bootstartp Token
,完整配置如下(与使用 token.csv 配置没什么变化,因为主要变更在 bootstrap.kubeconfig 中):
1 |
|
一切准备就绪后,执行 systemctl daemon-reload && systemctl start kubelet
启动即可
可能有人已经注意到,在官方文档中最后部分有关于 ConfigMap Signing 的相关描述,同时要求了启用 bootstrapsigner
这个 controller,而且在上文创建 Bootstrap Token Secret
中我也说 usage-bootstrap-signing
这个可以不设置;其中官方文档上的描述我们能看到的大致只说了这么两段稍微有点用的话:
In addition to authentication, the tokens can be used to sign a ConfigMap. This is used early in a cluster bootstrap process before the client trusts the API server. The signed ConfigMap can be authenticated by the shared token.
The ConfigMap that is signed is cluster-info in the kube-public namespace. The typical flow is that a client reads this ConfigMap while unauthenticated and ignoring TLS errors. It then validates the payload of the ConfigMap by looking at a signature embedded in the ConfigMap.
从这两段话中我们只能得出两个结论:
kube-public
NameSpace 下的名字叫 cluster-info
的 ConfigMap,并且这个 ConfigMap 可以在没进行引导之前强行读取说实话这两段话搞得我百思不得骑姐其解,最终我在 kubeadm 的相关文档中找到了真正的说明及作用:
kubeadm init
时创建 cluster-info
这个 ConfigMap,ConfigMap 中包含了集群基本信息kubeadm join
时目标节点强行读取 ConfigMap 以得知集群基本信息,然后进行 join
综上所述,我个人认为手动部署下,在仅仅使用 Bootstrap Token 进行 TLS Bootstrapping 时,bootstrapsigner
这个 controller 和 Bootstrap Token Secret
中的 usage-bootstrap-signing
选项是没有必要的,当然我还没测试(胡吹谁不会)…
最后附上 kubeadm
的文档说明: Create the public cluster-info ConfigMap、Discovery cluster-info
一直以来自己的 Kubernetes 集群大部分证书配置全部都在使用一个 CA,而事实上很多教程也没有具体的解释过这些证书代表的作用以及含义;今天索性仔细的翻了翻,顺便看到了一篇老外的文章,感觉写的不错,这里顺带着自己的理解总结一下。
这里的证书分类只是我自己定义的一种 “并不 ok” 的概念;从整体的作用上 Kubernetes 证书大致上应当分为两类:
对于 API Server 用于检验请求合法性的证书配置一般会在 API Server 中配置好,而对其他敏感信息签名加密的证书一般会可能放在 Controller Manager 中配置,也可能还在 API Server,具体不同版本需要撸文档
另外需要明确的是: Kubernetes 中 CA 证书并不一定只有一个,很多证书配置实际上是不相干的,只是大家为了方便普遍选择了使用一个 CA 进行签发;同时有一些证书如果不设置也会自动默认一个,就目前我所知的大约有 5 个可以完全不同的证书签发体系(或者说由不同的 CA 签发)
API Server 证书配置中最应当明确的两个选项应该是以下两个:
1 |
|
从描述上就可以看出,这两个选项配置的就是 API Server HTTPS 端点应当使用的证书
接下来就是我们常见的 CA 配置:
1 |
|
该配置明确了 Clent 连接 API Server 时,API Server 应当确保其证书源自哪个 CA 签发;如果其证书不是由该 CA 签发,则拒绝请求;事实上,这个 CA 不必与 HTTPS 端点所使用的证书 CA 相同;同时这里的 Client 是一个泛指的,可以是 kubectl,也可能是你自己开发的应用
由于 API Server 是支持多种认证方式的,其中一种就是使用 HTTP 头中的指定字段来进行认证,相关配置如下:
1 |
|
当指定这个 CA 证书后,则 API Server 使用 HTTP 头进行认证时会检测其 HTTP 头中发送的证书是否由这个 CA 签发;同样它也可独立于其他 CA(可以是个独立的 CA);具体可以参考 Authenticating Proxy
对于 Kubelet 组件,API Server 单独提供了证书配置选项,同时 Kubelet 组件也提供了反向设置的相关选项:
1 |
|
相信这个配置不用多说就能猜到,这个就是用于指定 API Server 与 Kubelet 通讯所使用的证书以及其签署的 CA;同样这个 CA 可以完全独立与上述其他CA
在 API Server 配置中,对于 Service Account 同样有两个证书配置:
1 |
|
这两个配置描述了对 Service Account 进行签名验证时所使用的证书;不过需要注意的是这里并没有明确要求证书 CA,所以这两个证书的 CA 理论上也是可以完全独立的;至于未要求 CA 问题,可能是由于 jwt 库并不支持 CA 验证
Kubernetes 中大部分证书都是用于 API Server 各种鉴权使用的;在不同鉴权方案或者对象上实际证书体系可以完全不同;具体是使用多个 CA 好还是都用一个,取决于集群规模、安全性要求等等因素,至少目前来说没有明确的那个好与不好
最后,嗯…吹牛逼就吹到这,有点晚了,得睡觉了…
]]>最近忙的晕头转向,博客停更了 1 个月,感觉对不起党、对不起人民、对不起 CCAV…不过在忙的时候操作 Kubernetes 集群要频繁的使用 kubectl
命令,而在多个 NameSpace 下来回切换每次都得加个 -n
简直让我想打人;索性翻了下 kubectl
的插件机制,顺便写了一个快速切换 NameSpace 的小插件,以下记录一下插件编写过程
kubectl
命令从 v1.8.0
版本开始引入了 alpha feature 的插件机制;在此机制下我们可以对 kubectl
命令进行扩展,从而编写一些自己的插件集成进 kubectl
命令中;**kubectl
插件机制是与语言无关的,也就是说你可以用任何语言编写插件,可以是 bash
、python
脚本,也可以是 go
、java
等编译型语言;所以选择你熟悉的语言即可**,以下是一个用 go
编写的用于快速切换 NameSpace 的小插件,运行截图如下:
所谓: 开局一张图,功能全靠编 😂
当前插件代码放在 mritd/swns 这个项目下面
kubectl
插件机制目前并不提供包管理器一样的功能,比如你想执行 kuebctl plugin install xxx
这种操作目前还没有实现(个人感觉差个规范);所以一旦我们编写或者下载一个插件后,我们只有正确放在特定目录才会生效;
目前插件根据文档描述只有两部分内容: plugin.yaml
和其依赖的二进制/脚本等可执行文件;根据文档说明,kubectl
会尝试在如下位置查找并加载插件,所以我们只需要将 plugin.yaml
和相关二进制放在在对应位置即可:
${KUBECTL_PLUGINS_PATH}
: 如果这个环境变量定义了,那么 kubectl
只会从这里查找;注意: 这个变量可以是多个目录,类似 PATH 变量一样,做好分割即可${XDG_DATA_DIRS}/kubectl/plugins
: 关于这个变量具体请看 XDG System Directory Structure,我了解也不多;如果这个变量没定义则默认为 /usr/local/share:/usr/share
~/.kube/plugins
: 这个没啥可说的,我推荐还是将插件放在这个位置比较友好一点所以最终插件目录结构类似这样:
1 |
|
plugin.yaml
这个文件实际上才是插件的核心,在这个文件里声明了插件如何使用、调用的二进制/脚本等重要配置;一个插件可以没有任何脚本/二进制可执行文件,但至少应当有一个 plugin.yaml
描述文件;目前 plugin.yaml
的结构如下:
1 |
|
在编写插件时,有时插件运行时需要获取到一些参数,比如 kubectl
执行时的全局 flag 等,为了方便插件开发者,kuebctl
的插件机制提供一些预置的环境变量方便我们读取;即如果你用 bash
写插件,那么这些变量你只需要 ${xxxx}
即可拿到,然后做一些你想做的事情;这些变量目前支持如下:
KUBECTL_PLUGINS_CALLER
: kubectl
二进制文件所在位置;作为插件编写者,我们无需关系 api server 是否能联通,因为配置是否正确应当由使用者决定;在需要时我们只需要直接调用 kubectl
即可;比如在 bash
脚本中执行 get pod
等KUBECTL_PLUGINS_CURRENT_NAMESPACE
: 当前 kuebctl
命令所对应的 NameSpace,插件机制确保了该值一定正确;即这是经过解析了 --namespace
选项或者 kubeconfig
配置后的最终结果;作为插件编写者,我们无需关心处理过程;想详细了解的的可以去看源码,以及 Cobra
库(Kubernetes 用这个库解析命令行参数和配置)KUBECTL_PLUGINS_DESCRIPTOR_*
: 插件自己本身位于 plugin.yaml
中的描述信息,比如 KUBECTL_PLUGINS_DESCRIPTOR_NAME
输出 plugin.yaml
下的 name
属性;一般可以用作插件输出自己的帮助文档等KUBECTL_PLUGINS_GLOBAL_FLAG_*
: 获取 kubectl
所有全局 flag 值的变量,比如 KUBECTL_PLUGINS_GLOBAL_FLAG_NAMESPACE
能拿到 --namespace
选项的值KUBECTL_PLUGINS_LOCAL_FLAG_*
: 同上面类似,只不过这个是获取插件自己本身 flag 的值,个人认为在脚本语言中,比如 bash
等处理选项不怎么好用时,可以考虑直接从变量拿以上变量我并未都测试,具体以测试为准,删库跑路等情况本人概不负责
前面墨迹一大堆只是为了描述清楚 要写一个插件应该怎么干 的问题,下面开始 这么干
上面已经介绍好了 plugin.yaml
怎么写,那么根据我自己的需求,我写的这个切换 NameSpace 插件的名字暂且叫做 swns
;我希望 swns
执行后接受一个 NameSpace 的字符串,然后调用 kuebctl config
去设置当前默认的 NameSpace,这样在后续命令中我就不用再一直加个 -n xxx
参数了;同时我希望使用更方便点,当执行 swns
命令时,如果不提供 NameSpace 的字符串,那我就弹出下拉列表供用户选择;综上需求自己想明白后,就写一个 plugin.yaml
,如下:
1 |
|
上面 plugin.yaml
已经定义好了,那么接下来就简单了,撸代码实现了就好;代码如下:
1 |
|
最后编译后放到上面所说的插件加载目录即可
到此,**”全局一张图,功能全靠编”** 图上面也有了,编的的也差不多 😂
]]>最近准备重新折腾一下 Kubernetes 的服务暴露方式,以前的方式是彻底剥离 Kubenretes 本身的服务发现,然后改动应用实现 应用+Consul+Fabio 的服务暴露方式;总感觉这种方式不算优雅,所以折腾了一下 Traefik,试了下效果还不错,以下记录了使用 Traefik 的新的服务暴露方式(本文仅针对 HTTP 协议);
以前的服务暴露方案是修改应用代码,使其能对接 Consul,然后 Consul 负责健康监测,检测通过后 Fabio 负责读取,最终上层 Nginx 将流量打到 Fabio 上,Fabio 再将流量路由到健康的 Pod 上;总体架构如下
这种架构目前有几点不太好的地方,首先是必须应用能成功集成 Consul,需要动应用代码不通用;其次组件过多增加维护成本,尤其是调用链日志不好追踪;这里面需要吐槽下 Consul 和 Fabio,Consul 的集群设计模式要想做到完全的 HA 那么需要在每个 pod 中启动一个 agent,因为只要这个 agent 挂了那么集群认为其上所有注册服务都挂了,这点很恶心人;而 Fabio 的日志目前好像还是不支持合理的输出,好像只能 stdout;目前来看不论是组件复杂度还是维护成本都不怎么友好
使用 Traefik 首先想到就是直接怼 Ingress,这个确实方便也简单;但是在集群 kube-proxy 不走 ipvs 的情况下 iptables 性能确实堪忧;虽说 Traefik 会直连 Pod,但是你 Ingress 暴露 80、443 端口在本机没有对应 Ingress Controller 的情况下还是会走一下 iptables;不论是换 kube-router、kube-proxy 走 ipvs 都不是我想要的,我们需要一种完全远离 Kubernetes Service 的新路子;在看 Traefik 文档的时候,其中说明了 Traefik 只利用 Kubernetes 的 API 来读取相关后端数据,那么我们就可以以此来使用如下的套路
这个套路的方案很简单,将 Traefik 部署在物理机上,让其直连 Kubernets api 以读取 Ingress 配置和 Pod IP 等信息,然后在这几台物理机上部署好 Kubernetes 的网络组件使其能直连 Pod IP;这种方案能够让流量经过 Traefik 直接路由到后端 Pod,健康检测还是由集群来做;由于 Traefik 连接 Kubernetes api 需要获取一些数据;所以在集群内还是像往常一样创建 Ingress,只不过此时我们并没有 Ingress Controller;这样避免了经过 iptables 转发,不占用全部集群机器的 80、443 端口,同时还能做到高可控
部署之前首先需要有一个正常访问的集群,然后在另外几台机器上部署 Kubernetes 的网络组件;最终要保证另外几台机器能够直接连通 Pod 的 IP,我这里偷懒直接在 Kubernetes 的其中几个 Node 上部署 Traefik
Traefik 的 Docker Compose 如下
1 |
|
由于 Kubernetes 集群开启了 RBAC 认证同时采用 TLS 通讯,所以需要挂载 Kubernetes CA 证书,还需要为 Traefik 创建对应的 RBAC 账户以使其能够访问 Kubernetes API
Traefik 连接 Kubernetes API 时需要使用 Service Account 的 Token,Service Account 以及 ClusterRole 等配置具体见 官方文档,下面是我从当前版本的文档中 Copy 出来的
1 |
|
创建好以后需要提取 Service Account 的 Token 方便下面使用,提取命令如下
1 |
|
Traefik 的具体配置细节请参考 官方文档,以下仅给出一个样例配置
1 |
|
所有文件准备好以后直接执行 docker-compose up -d
启动即可,所有文件目录结构如下
1 |
|
启动成功后可以访问 http://IP:2180
查看 Traefik 的控制面板
虽然这种部署方式脱离了 Kubernetes 的 Service 与 Ingress 负载,但是 Traefik 还是需要通过 Kubernetes 的 Ingress 配置来确定后端负载规则,所以 Ingress 对象我们仍需照常创建;以下为一个 Demo 项目的 deployment、service、ingress 配置示例
1 |
|
1 |
|
1 |
|
部署好后应当能从 Traefik 的 Dashboard 中看到新增的 demo ingress,如下所示
最后我们使用 curl 测试即可
1 |
|
写这篇文章的目的是给予一种新的服务暴露思路,这篇文章的某些配置并不适合生产使用;生产环境尽量使用独立的机器部署 Traefik,同时最好宿主机二进制方式部署;应用的 Deployment 也应当加入健康检测以防止错误的流量路由;至于 Traefik 的具体细节配置,比如访问日志、Entrypoints 配置、如何连接 Kubernets HA api 等不在本文范畴内,请自行查阅文档;
最后说一下,关于 Traefik 的 HA 只需要部署多个实例即可,还有 Traefik 本身不做日志滚动等,需要自行处理一下日志。
]]>最近准备对项目生成 Change Log,然而发现提交格式不统一根本没法处理;so 后来大家约定式遵循 GitFlow,并使用 Angular 社区规范的提交格式,同时扩展了一些前缀如 hotfix 等;但是时间长了发现还是有些提交为了 “方便” 不遵循 Angular 社区规范的提交格式,这时候我唯一能做的就是想办法在服务端增加一个提交检测;以下记录了 GitLab 增加自定义 Commit 提交格式检测的方案
最开始用 Google 搜索到的方案是使用 GitLab 的 Push Rules 功能,具体文档见 这里,看完了我才发现这是企业版独有的,作为比较有逼格(qiong)的我们是不可能接受这种 “没技术含量” 的方式的;后来找了好多资料,发现还得借助 Git Hook 功能,文档见 Custom Git Hooks;简单地说 Git Hook 就是在 git 操作的不同阶段执行的预定义脚本,GitLab 目前仅支持 pre-receive
这个钩子,当然他可以链式调用;所以一切操作就得从这里入手
查阅了相关资料得出,在进行 push 时,GitLab 会调用这个钩子文件,这个钩子文件必须放在 /var/opt/gitlab/git-data/repositories/<group>/<project>.git/custom_hooks
目录中,当然具体路径也可能是 /home/git/repositories/<group>/<project>.git/custom_hooks
;custom_hooks
目录需要自己创建,具体可以参阅文档的 Setup;
在进行 push 操作时,GitLab 会调用这个钩子文件,并且从 stdin 输入三个参数,分别为 之前的版本 commit ID、push 的版本 commit ID 和 push 的分支;根据 commit ID 我们就可以很轻松的获取到提交信息,从而实现进一步检测动作;根据 GitLab 的文档说明,当这个 hook 执行后以非 0 状态退出则认为执行失败,从而拒绝 push;同时会将 stderr 信息返回给 client 端;说了这么多,下面就可以直接上代码了,为了方便我就直接用 go 造了一个 pre-receive,官方文档说明了不限制语言
1 |
|
把以上代码编译后生成的 pre-receive
文件复制到对应项目的钩子目录即可;要注意的是文件名必须为 pre-receive
,同时 custom_hooks
目录需要自建;custom_hooks
目录以及 pre-receive
文件用户组必须为 git:git
;在删除分支时 commit ID 为 0000000000000000000000000000000000000000
,此时不需要检测提交信息,否则可能导致无法删除分支/tag;最后效果如下所示
年后比较忙,所以 1.9 也没去折腾(其实就是懒),最近刚有点时间凑巧 1.10 发布;所以就折腾一下 1.10,感觉搭建配置没有太大变化,折腾了 2 天基本算是搞定了,这里记录一下搭建过程;本文用到的被 block 镜像已经上传至 百度云 密码: dy5p
目前搭建仍然采用 5 台虚拟机测试,基本环境如下
IP | Type | Docker | OS |
---|---|---|---|
192.168.1.61 | master、node、etcd | 18.03.0-ce | ubuntu 16.04 |
192.168.1.62 | master、node、etcd | 18.03.0-ce | ubuntu 16.04 |
192.168.1.63 | master、node、etcd | 18.03.0-ce | ubuntu 16.04 |
192.168.1.64 | node | 18.03.0-ce | ubuntu 16.04 |
192.168.1.65 | node | 18.03.0-ce | ubuntu 16.04 |
搭建前请看完整篇文章后再操作,一些变更说明我放到后面了;还有为了尽可能的懒,也不用什么 rpm、deb 了,直接 hyperkube
+ service
配置,布吉岛 hyperkube
的请看 GitHub;本篇文章基于一些小脚本搭建(懒),所以不会写太详细的步骤,具体请参考 仓库脚本,如果想看更详细的每一步的作用可以参考以前的 1.7、1.8 的搭建文档
说实话这个章节我不想写,但是考虑可能有人真的需要,所以还是写了一下;这个安装脚本使用的是我私人的 cdn,文件可能随时删除,想使用最新版本请自行从 Github clone 并编译
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
生成后如下
Etcd 这里采用最新的 3.2.18 版本,安装方式直接复制二进制文件、systemd service 配置即可,不过需要注意相关用户权限问题,以下脚本配置等参考了 etcd rpm 安装包
1 |
|
1 |
|
1 |
|
脚本解释如下:
/usr/local/bin
),复制 conf 目录到 /etc/etcd
/var/lib/etcd
是否存在,纠正权限等整体目录结构如下
1 |
|
请自行创建 conf 目录等,并放置好相关文件,保存上面脚本为 install.sh
,直接执行即可;在每台机器上更改好对应的配置,如 etcd 名称等,etcd 估计都是轻车熟路了,这里不做过多阐述;安装后启动即可
1 |
|
注意: 集群 etcd 要 3 个一起启动,集群模式下单个启动会卡半天最后失败,不要傻等;启动成功后测试如下
1 |
|
注意:与以前文档不同的是,这次不依赖 rpm 等特定安装包,而是基于 hyperkube 二进制手动安装,每个节点都会同时安装 Master 与 Node 配置文件,具体作为 Master 还是 Node 取决于服务开启情况
由于 kubelet 和 kube-proxy 用到的 kubeconfig 配置文件需要借助 kubectl 来生成,所以需要先安装一下 kubectl
1 |
|
1 |
|
1 |
|
1 |
|
注意: 在以前的文档中这个配置叫 kubernetes-csr.json
,为了明确划分职责,这个证书目前被重命名以表示其专属于 apiserver
使用;加了一个 *.kubernetes.master
域名以便内部私有 DNS 解析使用(可删除);至于很多人问过 kubernetes
这几个能不能删掉,答案是不可以的;因为当集群创建好后,default namespace 下会创建一个叫 kubenretes
的 svc,有一些组件会直接连接这个 svc 来跟 api 通讯的,证书如果不包含可能会出现无法连接的情况;其他几个 kubernetes
开头的域名作用相同
1 |
|
1 |
|
1 |
|
生成后文件如下
所有组件的 systemd
配置如下
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
Master 节点主要会运行 3 各组件: kube-apiserver
、kube-controller-manager
、kube-scheduler
,其中用到的配置文件如下
config 是一个通用配置文件,值得注意的是由于安装时对于 Node、Master 节点都会包含该文件,在 Node 节点上请注释掉 KUBE_MASTER
变量,因为 Node 节点需要做 HA,要连接本地的 6443 加密端口;而这个变量将会覆盖 kubeconfig
中指定的 127.0.0.1:6443
地址
1 |
|
apiserver 配置相对于 1.8 略有变动,其中准入控制器(admission control
)选项名称变为了 --enable-admission-plugins
,控制器列表也有相应变化,这里采用官方推荐配置,具体请参考 官方文档
1 |
|
controller manager 配置默认开启了证书轮换能力用于自动签署 kueblet 证书,并且证书时间也设置了 10 年,可自行调整;增加了 --controllers
选项以指定开启全部控制器
1 |
|
1 |
|
Node 节点上主要有 kubelet
、kube-proxy
组件,用到的配置如下
kubeket 默认也开启了证书轮换能力以保证自动续签相关证书,同时增加了 --node-labels
选项为 node 打一个标签,关于这个标签最后部分会有讨论,如果在 master 上启动 kubelet,请将 node-role.kubernetes.io/k8s-node=true
修改为 node-role.kubernetes.io/k8s-master=true
1 |
|
1 |
|
上面已经准备好了相关配置文件,接下来将这些配置文件组织成如下目录结构以便后续脚本安装
1 |
|
其中 install.sh
内容如下
1 |
|
脚本解释如下:
最后执行此脚本安装即可,此外,应确保每个节点安装了 ipset
、conntrack
两个包,因为 kube-proxy 组件会使用其处理 iptables 规则等
对于 master
节点启动无需做过多处理,多个 master
只要保证 apiserver
等配置中的 ip 地址监听没问题后直接启动即可
1 |
|
成功后截图如下
由于 HA 等功能需要,对于 Node 需要做一些处理才能启动,主要有以下两个地方需要处理
在启动 kubelet
、kube-proxy
服务之前,需要在本地启动 nginx
来 tcp 负载均衡 apiserver
6443 端口,nginx-proxy
使用 docker
+ systemd
启动,配置如下
注意: 对于在 master 节点启动 kubelet 来说,不需要 nginx 做负载均衡;可以跳过此步骤,并修改 kubelet.kubeconfig
、kube-proxy.kubeconfig
中的 apiserver 地址为当前 master ip 6443 端口即可
1 |
|
1 |
|
启动 apiserver 的本地负载均衡
1 |
|
创建好 nginx-proxy
后不要忘记为 TLS Bootstrap
创建相应的 RBAC
规则,这些规则能实现证自动签署 TLS Bootstrap
发出的 CSR
请求,从而实现证书轮换(创建一次即可);详情请参考 Kubernetes TLS bootstrapping 那点事
1 |
|
在 master 执行创建
1 |
|
多节点部署时先启动好 nginx-proxy
,然后修改好相应配置的 ip 地址等配置,最终直接启动即可(master 上启动 kubelet 不要忘了修改 kubeconfig 中的 apiserver 地址,还有对应的 kubelet 的 node label)
1 |
|
最后启动成功后如下
Calico 安装仍然延续以前的方案,使用 Daemonset 安装 cni 组件,使用 systemd 控制 calico-node 以确保 calico-node 能正确的拿到主机名等
1 |
|
注意: 创建 systemd service 配置文件要在每个节点上都执行
1 |
|
对于以上脚本中的 K8S_MASTER_IP
变量,只需要填写一个 master ip 即可,这个变量用于 calico 自动选择 IP 使用;在宿主机有多张网卡的情况下,calcio node 会自动获取一个 IP,获取原则就是尝试是否能够联通这个 master ip
由于 calico 需要使用 etcd 存储数据,所以需要复制 etcd 证书到相关目录,**/etc/calico
需要在每个节点都有**
1 |
|
使用 Calico 后需要修改 kubelet 配置增加 CNI 设置(--network-plugin=cni
),修改后配置如下
1 |
|
1 |
|
1 |
|
网络测试与其他几篇文章一样,创建几个 pod 测试即可
1 |
|
测试结果如图所示
CoreDNS 给出了标准的 deployment 配置,如下
1 |
|
然后直接使用脚本替换即可(脚本变量我已经修改了)
1 |
|
最后使用 kubectl
创建一下
1 |
|
测试截图如下
自动扩容跟以往一样,yaml 创建一下就行
1 |
|
heapster 部署相对简单的多,yaml 创建一下就可以了
1 |
|
Dashboard 部署同 heapster 一样,不过为了方便访问,我设置了 NodePort,还注意到一点是 yaml 拉取策略已经没有比较傻的 Always
了
1 |
|
将最后部分的端口暴露修改如下
1 |
|
然后执行 kubectl create -f kubernetes-dashboard.yaml
即可
默认情况下部署成功后可以直接访问 https://NODE_IP:30000
访问,但是想要登录进去查看的话需要使用 kubeconfig 或者 access token 的方式;实际上这个就是 RBAC 授权控制,以下提供一个创建 admin access token 的脚本,更细节的权限控制比如只读用户可以参考 使用 RBAC 控制 kubectl 权限,RBAC 权限控制原理是一样的
1 |
|
将以上脚本保存为 create_dashboard_sa.sh
执行即可,成功后访问截图如下(如果访问不了的话请检查下 iptable FORWARD 默认规则是否为 DROP,如果是将其改为 ACCEPT 即可)
部署过程中注意到一些选项已经做了名称更改,比如 --network-plugin-dir
变更为 --cni-bin-dir
等,具体的那些选项做了变更请自行对比配置,以及查看官方文档;
对于 Node label --node-labels=node-role.kubernetes.io/k8s-node=true
这个选项,它的作用只是在 kubectl get node
时 ROLES 栏显示是什么节点;不过需要注意 master 上的 kubelet 不要将 node-role.kubernetes.io/k8s-master=true
更改成 node-role.kubernetes.io/master=xxxx
;后面这个 node-role.kubernetes.io/master
是 kubeadm 用的,这个 label 会告诉 k8s 调度器当前节点为 master,从而执行一些特定动作,比如 node-role.kubernetes.io/master:NoSchedule
此节点将不会被分配 pod;具体参见 kubespray issue 以及 官方设计文档
很多人可能会发现大约 1 小时候 kubectl get csr
看不到任何 csr 了,这是因为最新版本增加了 csr 清理功能,默认对于 approved
和 denied
状态的 csr 一小时后会被清理,对于 pending
状态的 csr 24 小时后会被清理,想问时间从哪来的请看 代码;PR issue 我忘记了,增加这个功能的起因大致就是因为当开启了证书轮换后,csr 会不断增加,所以需要增加一个清理功能
在部署过程中我记录了一些异常警告等,以下做一下统一说明
1 |
|
最近感觉 GitLab CI 稍有繁琐,所以尝试了一下 Drone CI,这里记录一下搭建过程;虽然 Drone CI 看似简单,但是坑还是有不少的
基本环境如下:
其中 GitLab 采用 TLS 链接,为了方便使用 git 协议 clone 代码,所以 docker compose 部署时采用了 macvlan 网络获取独立 IP
为了测试 CI build 需要一个 GitLab 服务器以及测试项目,GitLab 这里直接采用 docker compose 启动,同时为了方便 git clone,网络使用了 macvlan 方式,macvlan 网络接口、IP 等参数请自行修改
1 |
|
Drone CI 工作时需要接入 GitLab 以完成项目同步等功能,所以在搭建好 GitLab 后需要为其创建 Application,创建方式如下所示
创建 Application 时请自行更换回调地址域名,创建好后如下所示(后续 Drone CI 需要使用这两个 key)
Drone CI 服务器与 GitLab 等传统 CI 相似,都是 CS 模式,为了方便测试这里将 Agent 与 Server 端都放在一个 docker compose 中启动;docker compose 配置如下
1 |
|
docker compose 中 DRONE_GITLAB_CLIENT
为 GitLab 创建 Application 时的 Application Id
,DRONE_GITLAB_SECRET
为 Secret
;其他环境变量解释如下:
实际上 Agent 可以与 Server 分离部署,不过需要注意 Server 端 9000 端口走的是 grpc 协议基于 HTTP2,nginx 等反向代理时需要做好对应处理
搭建成功这里外面套了一层 nginx 用来反向代理 Drone Server 的 8000 端口,Nginx 配置如下:
1 |
|
然后访问 https://YOUR_DRONE_SERVER
将会自动跳转到 GitLab Auth2 授权界面,授权登录即可;随后将会返回 Drone CI 界面,界面上会列出相应的项目列表,点击后面的开关按钮来开启对应项目的 Drone CI 服务
这里的示例项目为 Java 项目,采用 Gradle 构建,项目整体结构如下所示,源码可以从 GitHub 下载
将此项目推送到 GitLab 就会触发 Drone CI 自动构建(第一次肯定构建失败,具体看下面配置)
这里不得不说一下官方文档真的很烂,有些东西只能自己摸索,而且各种错误提示也是烂的不能再烂,经常遇到 Client Error 404:
这种错误,后面任何提示信息也没有;官方文档中介绍了有些操作只能通过 cli 执行,CLI 下载需要到 GitHub 下载页下载,地址 点这里
cli 工具下载后需要进行配置,目前只支持读取环境变量,使用前需要 export
以下两个变量
其中 Token 可以在用户设置页面找到,如下
配置好以后就可以使用 cli 操作 CI Server 了
Drone CI 对一个项目进行 CI 构建取决于两个因素,第一必须保证该项目在 Drone 控制面板中开启了构建(构建按钮开启),第二保证项目根目录下存在 .drone.yml
;满足这两点后每次提交 Drone 就会根据 .drone.yml
中配置进行按步骤构建;本示例中 .drone.yml
配置如下
1 |
|
Drone CI 配置文件为 docker compose 的超集,Drone CI 构建思想是使用不同的阶段定义完成对 CI 流程的整体划分,然后每个阶段内定义不同的任务(task),这些任务所有操作无论是 build、package 等全部由单独的 Docker 镜像完成,同时以 plugins
开头的 image 被解释为内部插件;其他的插件实际上可以看做为标准的 Docker image
第一段 clone
配置声明了源码版本控制系统拉取方式,具体参见 cloning部分,定义后 Drone CI 将自动拉取源码
此后的 pipeline
配置段为定义整个 CI 流程段,该段中可以自定义具体 task,比如后端构建可以取名字为 backend
,前端构建可以叫做 frontend
;中间可以穿插辅助的如打包 docker 镜像等 task;同 GitLab CI 一样,Agent 在使用 Docker 进行构建时必然涉及到拉取私有镜像,Drone CI 想要拉取私有镜像目前仅能通过 cli 命令行进行设置,而且仅针对项目级设置(全局需要企业版…这也行)
1 |
|
在构建时需要注意一点,Drone CI 不同的 task 之间共享源码文件,也就是说如果你在第一个 task 中对源码或者编译后的发布物做了什么更改,在下一个 task 中同样可见,Drone CI 并没有 GitLab CI 在每个 task 中都进行还原的机制
除此之外,某些特殊性的挂载行为默认也是不被允许的,需要在 Drone CI 中对项目做 Trusted
设置
写到这里基本接近尾声了,可能常看我博客的人现在想喷我,这篇文章确实有点水…因为我真不推荐用这玩意,未来发展倒是不确定;下面对比一下与 GitLab CI 的区别
先说一下 Drone CI 的优点,Drone CI 更加轻量级,而且也支持 HA 等设置,配置文件使用 docker compose 的方式对于玩容器多的人确实很爽,启动速度等感觉也比 GitLab CI 要快;而且我个人用 GitLab CI Docker build 的方式时也是尽量将不同功能交给不同的镜像,通过切换镜像实现不同的功能;这个思想在 Drone CI 中表现的非常明显
至于 Drone CI 的缺点,目前我最大的吐槽就是文档烂,报错烂;很多时候搞得莫名其妙,比如上来安装讲的那个管理员账户配置,我现在也没明白怎么能关闭注册启动然后添加用户(可能是我笨);还有就是报错问题,感觉就像写代码不打 log 一样,比如 CI Server 在没有 agent 链接时,如果触发了 build 任务,Drone CI 不会报错,只会在任务上显示一个小闹钟,也没有超时…我傻傻的等了 1 小时;其他的比如全局变量、全局加密参数等都需要企业版才能支持,同时一些细节东西也缺失,比如查看当前 Server 连接的 Agent,对 Agent 打标签实现不同 task 分配等等
总结: Drone CI 目前还是个小玩具阶段,与传统 CI 基本没有抗衡之力,文档功能呢也是缺失比较严重,出问题很难排查
]]>本文主要阐述在 *Uinx 平台下,各种常用开发工具的加速配置,加速前提是你需要有一个能够加速的 socks5 端口,常用工具请自行搭建;本文档包括 docker、terminal、git、chrome 常用加速配置,其他工具可能后续补充
目前大部分工具在原始版本都是只提供 socks5 加速,常用平台一些工具已经支持手动设置加速端口,如 telegram、mega 同步客户端等等;但是某些工具并不支持 socks5,通用的加速目前各个平台只支持 http、https 设置(包括 terminal 下);综上所述,在设置之前你至少需要保证有一个 socks5 端口能够进行加速,然后根据以下教程将 socks5 转换成 http,最后配置各个软件或系统的加速方式为 http,这也是我们常用的某些带有图形化客户端实际的背后实现
sock5 转 http 这里采用 privoxy 进行转换,根据各个平台不同,安装方式可能不同,主要就是包管理器的区别,以下只列举 Ubuntu、Mac 下的命令,其他平台自行 Google
brew install privoxy
apt-get -y install privoxy
安装成功后,需要修改配置以指定 socks5 端口以及不代理的白名单,配置文件位置如下:
/usr/local/etc/privoxy/config
/etc/privoxy/config
在修改之前请备份默认配置文件,这是个好习惯,备份后修改内容如下:
1 |
|
其中 127.0.0.1:1080
为你的 socks5 ip 及 端口,localhost:8118
为你转换后的 http 监听地址和端口;配置完成后启动 privoxy 即可,启动命令如下:
brew services start privoxy
systemctl start privoxy
对于 docker 来说,terminal 下执行 docker pull
等命令实质上都是通过调用 docker daemon 操作的;而 docker daemon 是由 systemd 启动的(就目前来讲,别跟我掰什么 service start…);对于 docker daemon 来说,一旦它启动以后就不会再接受加速设置,所以我们需要在 systemd 的 service 配置中配置它的加速。
目前 docker daemon 接受标准的终端加速设置(读取 http_proxy
、https_proxy
),同时也支持 socks5 加速;为了保证配置清晰方便修改,这里采用创建单独配置文件的方式来配置 daemon 的 socks5 加速,配置脚本如下(Ubuntu、CentOS):
1 |
|
将该脚本内容保存为 docker_proxy.sh
,终端执行 bash docker_proxy.sh ubuntu 1.2.3.4:1080
即可(自行替换 socks5 地址);脚本实际上很简单,就是创建一个与 docker.service
文件同级的 docker.service.d
目录,然后在里面写入一个 socks5-proxy.conf
,配置内容只有两行:
1 |
|
这样 systemd 会自动读取,只需要 reload 一下,然后 restart docker daemon 即可,此后 docker 就可以通过加速端口直接 pull gcr.io
的镜像;注意: 配置加速后,docker 将无法 pull 私服镜像(一般私服都是内网 DNS 解析),但是不会影响容器启动以及启动后的容器中的网络
对于 Chrome 浏览器来说,目前有比较好的插件实现用来配置根据策略的加速访问;这里使用的插件为 SwitchyOmega
默认情况下 SwitchyOmega
可以通过 Chrome 进行在线安装,但是众所周知的原因这是不可能的,不过国内有一些网站提供代理下载 Chrome 扩展的服务,如 https://chrome-extension-downloader.com
、http://yurl.sinaapp.com/crx.php
,这些网站只需要提供插件 ID 即可帮你下载下来;**SwitchyOmega
插件的 ID 为 padekgcemlokbadohgkifijomclgjgif
,注意下载时不要使用 chrome 下载,因为他自身的防护机制会阻止你下载扩展程序**;下载后打开 chrome 的扩展设置页,将 crx 文件拖入安装即可,如下所示:
SwitchyOmega 安装成功后在 Chrome 右上角有显示,右键点击该图标,进入选项设置后如下所示:
默认情况下左侧只有两个加速模式,一个叫做 proxy
另一个叫做 autoproxy
;根据加速模式不同 SwitchyOmega 在浏览网页时选择的加速通道也不同,不同的加速方式可以通过点击 新建情景模式 按钮创建,下面介绍一下常用的两种情景模式:
代理服务器: 这种情景模式创建后需要填写一个代理地址,该地址可以是 http(s)/socks5(4) 类型;创建成功后,浏览器右上角切换到该情景模式,浏览器访问所有网页的流量全部通过该代理地址发出,不论你是访问百度还是 Google
自动切换模式: 这种情景模式并不需要填写实际的代理地址,而是需要填写一些规则;创建完成后插件中选择此种情景模式时,浏览器访问所有网页流量会根据填写的规则自动路由,然后选择合适的代理情景模式;可以实现智能切换代理
综上所述,首先应该创建(或者修改默认的 proxy 情景模式)一个代理服务器的情景模式,然后填写好你的加速 IP 和对应的协议端口;接下来在浏览器中切换到该情景模式尝试访问 kubenretes.io 等网站测试加速效果;成功后再次新建一个自动切换情景模式,**保证 规则列表规则
一栏后面的下拉列表对应到你刚刚创建的代理服务器情景模式,默认情景模式
后面的下拉列表对应到直接连接情景模式,然后点击下面的 添加规则列表
按钮,选择 AutoProxy
单选框,规则列表网址
填写 https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt
(这是一个开源项目收集的需要加速的网址列表)**;最后在浏览器中切换到自动切换情景模式,然后访问 kubernetes.io、baidu.com 等网站测试是否能自动切换情景模式
对于终端下的应用程序,百分之九十的程序都会识别 http_proxy
和 https_proxy
两个变量;所以终端加速最简单的方式就是在执行命令前声明这两个变量即可,为了方便起见也可以写个小脚本,示例如下:
1 |
|
将上面的地址自行更换成你的 http 加速地址后,终端运行 proxy curl ip.cn
即可测试加速效果
proxychains-ng 是一个终端下的工具,它可以 hook libc 下的网络相关方法实现加速效果;目前支持后端为 http(s)/socks5(4a),前段协议仅支持对 TCP 加速;
Mac 下安装方式:
1 |
|
Ubuntu 等平台下需要手动编译安装:
1 |
|
安装完成后编辑配置使用即可,Mac 下配置位于 /usr/local/etc/proxychains.conf
,Ubuntu 下配置位于 /etc/proxychains.conf
;配置修改如下:
1 |
|
然后命令行使用 proxychains4 curl ip.cn
测试即可
目前 Git 的协议大致上只有三种 https
、ssh
和 git
,对于使用 https
方式进行 clone 和 push 操作时,可以使用第五部分 Terminal 加速方案即可实现对 Git 的加速;对于 ssh
、git
协议,实际上都在调用 ssh 协议相关进行通讯(具体细节请 Google,这里的描述可能不精准),此时同样可以使用 proxychains-ng
进行加速,**不过需要注意 proxychains-ng
要自行编译安装,同时 ./configure
增加 --fat-binary
选项,具体参考 GitHub Issue**;ssh
、git
由于都在调用 ssh 协议进行通讯,所以实际上还可以通过设置 ssh 的 ProxyCommand
来实现,具体操作如下:
1 |
|
需要注意: nc 命令是 netcat-openbsd 版本,Mac 下默认提供,Ubuntu 下需要使用 apt-get install -y netcat-openbsd
安装;CentOS 没有 netcat-openbsd,需要安装 EPEL 源,然后安装 connect-proxy 包,使用 connect-proxy 命令替代
好久没写文章了,过年以后就有点懒… 最近也在学习 golang,再加上不断造轮子所以没太多时间;凑巧最近想控制一下 kubectl 权限,这里便记录一下。
相信现在大部分人用的集群已经都是 1.6 版本以上,而且在安装各种组件的时候也已经或多或少的处理过 RBAC 的东西,所以这里不做太细节性的讲述,RBAC 文档我以前胡乱翻译过一篇,请看 这里,以下内容仅说主要的
我在第一次接触 Kubernetes RBAC 的时候,对于基于角色控制权限这种做法是有了解的,基本结构主要就是三个:
但是翻了一会文档,最晕的就是 这个用户标识(ID)存在哪,因为传统的授权模型都是下面这样
不论怎样,在进行授权时总要有个地方存放用户信息(DB/文件),但是在 Kubernetes 里却没找到;后来翻阅文档,找到这么一段
1 |
|
也就是说,Kubernetes 是不负责维护存储用户数据的;对于 Kubernetes 来说,它识别或者说认识一个用户主要就几种方式
其他不再一一列举,具体请看文档 Authenticating;了解了这些,后面我们使用 RBAC 控制 kubectl 权限的时候就要使用如上几种方法创建对应用户
RBAC 权限定义部分主要有三个层级
定义一组权限(角色)时要根据其所需的真正需求做最细粒度的划分
首先根据上文可以得知,Kubernetes 不存储用户具体细节信息,也就是说只要通过它的那几种方式能进来的用户,Kubernetes 就认为它是合法的;那么为了让 kubectl 只读,所以我们需要先给它创建一个用来承载只读权限的用户;这里用户创建我们选择使用证书方式
1 |
|
然后基于以 Kubernetes CA 证书创建只读用户的证书
1 |
|
以上命令会生成 readonly-key.pem
、readonly.pem
两个证书文件以及一个 csr 请求文件
有了用于证明身份的证书以后,接下来创建一个 kubeconfig 文件方便 kubectl 使用
1 |
|
这条命令会将证书也写入到 readonly.kubeconfig 配置文件中,将该文件放在 ~/.kube/config
位置,kubectl 会自动读取
本示例创建的只读用户权限范围为 Cluster 集群范围,所以先创建一个只读权限的 ClusterRole;创建 ClusterRole 不知道都有哪些权限的话,最简单的办法是将集群的 admin ClusterRole 保存出来,然后做修改
1 |
|
这个 admin ClusterRole 是默认存在的,导出后我们根据自己需求修改就行;最基本的原则就是像 update、delete 这种权限必须删掉(我们要创建只读用户),修改后如下
1 |
|
最后执行 kubectl create -f readonly.yaml
创建即可
用户已经创建完成,集群权限也有了,接下来使用 ClusterRoleBinding 绑定到一起即可
1 |
|
将以上保存为 readonly-bind.yaml
执行 kubectl create -f readonly-bind.yaml
即可
将最初创建的 kubeconfig 放到 ~/.kube/config
或者直接使用 --kubeconfig
选项测试读取、删除 pod 等权限即可,测试后如下所示
前段时间撸了一会 Kubernetes 官方文档,在查看 TLS bootstrapping 这块是发现已经跟 1.4 的时候完全不一样了;目前所有搭建文档也都保留着 1.4 时代的配置,在看完文档后发现目前配置有很多问题,同时也埋下了 隐藏炸弹,这个问题可能会在一年后爆发…..后果就是集群 node 全部掉线;所以仔细的撸了一下这个文档,从元旦到写此文章的时间都在测试这个 TLS bootstrapping,以下记录一下这次的成果
阅读本文章前,请先阅读一下本文参考的相关文档:
Kubernetes 在 1.4 版本(我记着是)推出了 TLS bootstrapping 功能;这个功能主要解决了以下问题:
当集群开启了 TLS 认证后,每个节点的 kubelet 组件都要使用由 apiserver 使用的 CA 签发的有效证书才能与 apiserver 通讯;此时如果节点多起来,为每个节点单独签署证书将是一件非常繁琐的事情;TLS bootstrapping 功能就是让 kubelet 先使用一个预定的低权限用户连接到 apiserver,然后向 apiserver 申请证书,kubelet 的证书由 apiserver 动态签署;在配合 RBAC 授权模型下的工作流程大致如下所示(不完整,下面细说)
在官方 TLS bootstrapping 文档中多次提到过 kubelet server
这个东西;在经过翻阅大量文档以及 TLS bootstrapping 设计文档后得出,**kubelet server
指的应该是 kubelet 的 10250 端口;**
kubelet 组件在工作时,采用主动的查询机制,即定期请求 apiserver 获取自己所应当处理的任务,如哪些 pod 分配到了自己身上,从而去处理这些任务;同时 kubelet 自己还会暴露出两个本身 api 的端口,用于将自己本身的私有 api 暴露出去,这两个端口分别是 10250 与 10255;对于 10250 端口,kubelet 会在其上采用 TLS 加密以提供适当的鉴权功能;对于 10255 端口,kubelet 会以只读形式暴露组件本身的私有 api,并且不做鉴权处理
总结一下,就是说 kubelet 上实际上有两个地方用到证书,一个是用于与 API server 通讯所用到的证书,另一个是 kubelet 的 10250 私有 api 端口需要用到的证书
kubelet 发起的 CSR 请求都是由 controller manager 来做实际签署的,对于 controller manager 来说,TLS bootstrapping 下 kubelet 发起的 CSR 请求大致分为以下三种
O=system:nodes
和 CN=system:node:(node name)
形式发起的 CSR 请求大白话加自己测试得出的结果: nodeclient 类型的 CSR 仅在第一次启动时会产生,selfnodeclient 类型的 CSR 请求实际上就是 kubelet renew 自己作为 client 跟 apiserver 通讯时使用的证书产生的,selfnodeserver 类型的 CSR 请求则是 kubelet 首次申请或后续 renew 自己的 10250 api 端口证书时产生的
在说具体的引导过程之前先谈一下 TLS 和 RBAC,因为这两个事不整明白下面的都不用谈;
众所周知 TLS 的作用就是对通讯加密,防止中间人窃听;同时如果证书不信任的话根本就无法与 apiserver 建立连接,更不用提有没有权限向 apiserver 请求指定内容
当 TLS 解决了通讯问题后,那么权限问题就应由 RBAC 解决(可以使用其他权限模型,如 ABAC);RBAC 中规定了一个用户或者用户组(subject)具有请求哪些 api 的权限;在配合 TLS 加密的时候,实际上 apiserver 读取客户端证书的 CN 字段作为用户名,读取 O 字段作为用户组
从以上两点上可以总结出两点: 第一,想要与 apiserver 通讯就必须采用由 apiserver CA 签发的证书,这样才能形成信任关系,建立 TLS 连接;第二,可以通过证书的 CN、O 字段来提供 RBAC 所需的用户与用户组
看完上面的介绍,不知道有没有人想过,既然 TLS bootstrapping 功能是让 kubelet 组件去 apiserver 申请证书,然后用于连接 apiserver;那么第一次启动时没有证书如何连接 apiserver ?
这个问题实际上可以去查看一下 bootstrap.kubeconfig
和 token.csv
得到答案: 在 apiserver 配置中指定了一个 token.csv
文件,该文件中是一个预设的用户配置;同时该用户的 Token 和 apiserver 的 CA 证书被写入了 kubelet 所使用的 bootstrap.kubeconfig
配置文件中;这样在首次请求时,kubelet 使用 bootstrap.kubeconfig
中的 apiserver CA 证书来与 apiserver 建立 TLS 通讯,使用 bootstrap.kubeconfig
中的用户 Token 来向 apiserver 声明自己的 RBAC 授权身份,如下图所示
在有些用户首次启动时,可能与遇到 kubelet 报 401 无权访问 apiserver 的错误;这是因为在默认情况下,kubelet 通过 bootstrap.kubeconfig
中的预设用户 Token 声明了自己的身份,然后创建 CSR 请求;但是不要忘记这个用户在我们不处理的情况下他没任何权限的,包括创建 CSR 请求;所以需要如下命令创建一个 ClusterRoleBinding,将预设用户 kubelet-bootstrap
与内置的 ClusterRole system:node-bootstrapper
绑定到一起,使其能够发起 CSR 请求
1 |
|
在 kubelet 首次启动后,如果用户 Token 没问题,并且 RBAC 也做了相应的设置,那么此时在集群内应该能看到 kubelet 发起的 CSR 请求
出现 CSR 请求后,可以使用 kubectl 手动签发(允许) kubelet 的证书
当成功签发证书后,目标节点的 kubelet 会将证书写入到 --cert-dir=
选项指定的目录中;注意此时如果不做其他设置应当生成四个文件
而 kubelet 与 apiserver 通讯所使用的证书为 kubelet-client.crt
,剩下的 kubelet.crt
将会被用于 kubelet server
(10250) 做鉴权使用;注意,此时 kubelet.crt
这个证书是个独立于 apiserver CA 的自签 CA,并且删除后 kubelet 组件会重新生成它
单独把这部分拿出来写,是因为个人觉得上面已经有点乱了;这部分实际上更复杂,只好单独写一下了,因为这部分涉及的东西比较多,所以也不想草率的几笔带过
首先…首先好几次了…嗯,就是说 kubelet 所发起的 CSR 请求是由 controller manager 签署的;如果想要是实现自动续期,就需要让 controller manager 能够在 kubelet 发起证书请求的时候自动帮助其签署证书;那么 controller manager 不可能对所有的 CSR 证书申请都自动签署,这时候就需要配置 RBAC 规则,保证 controller manager 只对 kubelet 发起的特定 CSR 请求自动批准即可;在 TLS bootstrapping 官方文档中,针对上面 2.2 章节提出的 3 种 CSR 请求分别给出了 3 种对应的 ClusterRole,如下所示
1 |
|
RBAC 中 ClusterRole 只是描述或者说定义一种集群范围内的能力,这三个 ClusterRole 在 1.7 之前需要自己手动创建,在 1.8 后 apiserver 会自动创建前两个(1.8 以后名称有改变,自己查看文档);以上三个 ClusterRole 含义如下
所以,如果想要 kubelet 能够自动续期,那么就应当将适当的 ClusterRole 绑定到 kubelet 自动续期时所所采用的用户或者用户组身上
在自动续期下引导过程与单纯的手动批准 CSR 有点差异,具体的引导流程地址如下
从以上流程我们可以看出,我们如果要创建 RBAC 规则,则至少能满足四种情况:
基于以上四种情况,我们需要创建 3 个 ClusterRoleBinding,创建如下
1 |
|
在 1.7 后,kubelet 启动时增加 --feature-gates=RotateKubeletClientCertificate=true,RotateKubeletServerCertificate=true
选项,则 kubelet 在证书即将到期时会自动发起一个 renew 自己证书的 CSR 请求;同时 controller manager 需要在启动时增加 --feature-gates=RotateKubeletServerCertificate=true
参数,再配合上面创建好的 ClusterRoleBinding,kubelet client 和 kubelet server 证才书会被自动签署;
注意,1.7 版本设置自动续期参数后,新的 renew 请求不会立即开始,而是在证书总有效期的 70%~90%
的时间时发起;而且经测试 1.7 版本即使自动签发了证书,kubelet 在不重启的情况下不会重新应用新证书;在 1.8 后 kubelet 组件在增加一个 --rotate-certificates
参数后,kubelet 才会自动重载新证书
需要重复强调一个问题是: TLS bootstrapping 时的证书实际是由 kube-controller-manager 组件来签署的,也就是说证书有效期是 kube-controller-manager 组件控制的;所以在 1.7 版本以后(我查文档发现的从1.7开始有) kube-controller-manager 组件提供了一个 --experimental-cluster-signing-duration
参数来设置签署的证书有效时间;默认为 8760h0m0s
,将其改为 87600h0m0s
即 10 年后再进行 TLS bootstrapping 签署证书即可。
kubelet 首次启动通过加载 bootstrap.kubeconfig
中的用户 Token 和 apiserver CA 证书发起首次 CSR 请求,这个 Token 被预先内置在 apiserver 节点的 token.csv 中,其身份为 kubelet-bootstrap
用户和 system:bootstrappers
用户组;想要首次 CSR 请求能成功(成功指的是不会被 apiserver 401 拒绝),则需要先将 kubelet-bootstrap
用户和 system:node-bootstrapper
内置 ClusterRole 绑定;
对于首次 CSR 请求可以手动批准,也可以将 system:bootstrappers
用户组与 approve-node-client-csr
ClusterRole 绑定实现自动批准(1.8 之前这个 ClusterRole 需要手动创建,1.8 后 apiserver 自动创建,并更名为 system:certificates.k8s.io:certificatesigningrequests:nodeclient
)
默认签署的的证书只有 1 年有效期,如果想要调整证书有效期可以通过设置 kube-controller-manager 的 --experimental-cluster-signing-duration
参数实现,该参数默认值为 8760h0m0s
对于证书自动续签,需要通过协调两个方面实现;第一,想要 kubelet 在证书到期后自动发起续期请求,则需要在 kubelet 启动时增加 --feature-gates=RotateKubeletClientCertificate=true,RotateKubeletServerCertificate=true
来实现;第二,想要让 controller manager 自动批准续签的 CSR 请求需要在 controller manager 启动时增加 --feature-gates=RotateKubeletServerCertificate=true
参数,并绑定对应的 RBAC 规则;同时需要注意的是 1.7 版本的 kubelet 自动续签后需要手动重启 kubelet 以使其重新加载新证书,而 1.8 后只需要在 kublet 启动时附带 --rotate-certificates
选项就会自动重新加载新证书
该文件为一个用户的描述文件,基本格式为 Token,用户名,UID,用户组
;这个文件在 apiserver 启动时被 apiserver 加载,然后就相当于在集群内创建了一个这个用户;接下来就可以用 RBAC 给他授权;持有这个用户 Token 的组件访问 apiserver 的时候,apiserver 根据 RBAC 定义的该用户应当具有的权限来处理相应请求
该文件中内置了 token.csv 中用户的 Token,以及 apiserver CA 证书;kubelet 首次启动会加载此文件,使用 apiserver CA 证书建立与 apiserver 的 TLS 通讯,使用其中的用户 Token 作为身份标识像 apiserver 发起 CSR 请求
该文件在 kubelet 完成 TLS bootstrapping 后生成,此证书是由 controller manager 签署的,此后 kubelet 将会加载该证书,用于与 apiserver 建立 TLS 通讯,同时使用该证书的 CN 字段作为用户名,O 字段作为用户组向 apiserver 发起其他请求
该文件在 kubelet 完成 TLS bootstrapping 后并且没有配置 --feature-gates=RotateKubeletServerCertificate=true
时才会生成;这种情况下该文件为一个独立于 apiserver CA 的自签 CA 证书,有效期为 1 年;被用作 kubelet 10250 api 端口
该文件在 kubelet 完成 TLS bootstrapping 后并且配置了 --feature-gates=RotateKubeletServerCertificate=true
时才会生成;这种情况下该证书由 apiserver CA 签署,默认有效期同样是 1 年,被用作 kubelet 10250 api 端口鉴权
这是一个软连接文件,当 kubelet 配置了 --feature-gates=RotateKubeletClientCertificate=true
选项后,会在证书总有效期的 70%~90%
的时间内发起续期请求,请求被批准后会生成一个 kubelet-client-时间戳.pem
;kubelet-client-current.pem
文件则始终软连接到最新的真实证书文件,除首次启动外,kubelet 一直会使用这个证书同 apiserver 通讯
同样是一个软连接文件,当 kubelet 配置了 --feature-gates=RotateKubeletServerCertificate=true
选项后,会在证书总有效期的 70%~90%
的时间内发起续期请求,请求被批准后会生成一个 kubelet-server-时间戳.pem
;kubelet-server-current.pem
文件则始终软连接到最新的真实证书文件,该文件将会一直被用于 kubelet 10250 api 端口鉴权
apiserver 预先放置 token.csv,内容样例如下
1 |
|
允许 kubelet-bootstrap 用户创建首次启动的 CSR 请求
1 |
|
配置 kubelet 自动续期,RotateKubeletClientCertificate 用于自动续期 kubelet 连接 apiserver 所用的证书(kubelet-client-xxxx.pem),RotateKubeletServerCertificate 用于自动续期 kubelet 10250 api 端口所使用的证书(kubelet-server-xxxx.pem)
1 |
|
配置 controller manager 自动批准相关 CSR 请求,如果不配置 --feature-gates=RotateKubeletServerCertificate=true
参数,则即使配置了相关的 RBAC 规则,也只会自动批准 kubelet client 的 renew 请求
1 |
|
创建自动批准相关 CSR 请求的 ClusterRole
1 |
|
将 ClusterRole 绑定到适当的用户组,以完成自动批准相关 CSR 请求
1 |
|
一切就绪后启动 kubelet 组件即可,不过需要注意的是 1.7 版本 kubelet 不会自动重载 renew 的证书,需要自己手动重启
apiserver 预先放置 token.csv,内容样例如下
1 |
|
允许 kubelet-bootstrap 用户创建首次启动的 CSR 请求
1 |
|
配置 kubelet 自动续期,RotateKubeletClientCertificate 用于自动续期 kubelet 连接 apiserver 所用的证书(kubelet-client-xxxx.pem),RotateKubeletServerCertificate 用于自动续期 kubelet 10250 api 端口所使用的证书(kubelet-server-xxxx.pem),--rotate-certificates
选项使得 kubelet 能够自动重载新证书
1 |
|
配置 controller manager 自动批准相关 CSR 请求,如果不配置 --feature-gates=RotateKubeletServerCertificate=true
参数,则即使配置了相关的 RBAC 规则,也只会自动批准 kubelet client 的 renew 请求
1 |
|
创建自动批准相关 CSR 请求的 ClusterRole,相对于 1.7 版本,1.8 的 apiserver 自动创建了前两条 ClusterRole,所以只需要创建一条就行了
1 |
|
将 ClusterRole 绑定到适当的用户组,以完成自动批准相关 CSR 请求
1 |
|
一切就绪后启动 kubelet 组件即可,1.8 版本 kubelet 会自动重载证书,以下为 1.8 版本在运行一段时间后的相关证书截图
]]>接着上篇文章整理,这篇文章主要介绍一下 GitLab CI 相关功能,并通过 GitLab CI 实现自动化构建项目;项目中所用的示例项目已经上传到了 GitHub
首先需要有一台 GitLab 服务器,然后需要有个项目;这里示例项目以 Spring Boot 项目为例,然后最好有一台专门用来 Build 的机器,实际生产中如果 Build 任务不频繁可适当用一些业务机器进行 Build;本文示例所有组件将采用 Docker 启动, GitLab HA 等不在本文阐述范围内
GitLab CI 是 GitLab 默认集成的 CI 功能,GitLab CI 通过在项目内 .gitlab-ci.yaml
配置文件读取 CI 任务并进行相应处理;GitLab CI 通过其称为 GitLab Runner 的 Agent 端进行 build 操作;Runner 本身可以使用多种方式安装,比如使用 Docker 镜像启动等;Runner 在进行 build 操作时也可以选择多种 build 环境提供者;比如直接在 Runner 所在宿主机 build、通过新创建虚拟机(vmware、virtualbox)进行 build等;同时 Runner 支持 Docker 作为 build 提供者,即每次 build 新启动容器进行 build;GitLab CI 其大致架构如下
GitLab 搭建这里直接使用 docker compose 启动,compose 配置如下
1 |
|
直接启动后,首次登陆需要设置初始密码如下,默认用户为 root
登陆成功后创建一个用户(该用户最好给予 Admin 权限,以后操作以该用户为例),并且创建一个测试 Group 和 Project,如下所示
这里示例项目采用 Java 的 SpringBoot 项目,并采用 Gradle 构建,其他语言原理一样;如果不熟悉 Java 的没必要死磕此步配置,任意语言(最好 Java)整一个能用的 Web 项目就行,并不强求一定 Java 并且使用 Gradle 构建,以下只是一个样例项目;SpringBoot 可以采用 Spring Initializr 直接生成(依赖要加入 WEB),如下所示
将项目导入 IDEA,然后创建一个 index 示例页面,主要修改如下
1 |
|
1 |
|
1 |
|
最后项目整体结构如下
执行 assemble
Task 打包出可执行 jar 包,并运行 java -jar TestProject-0.0.1-SNAPSHOT.jar
测试下能启动访问页面即可
最后将项目提交到 GitLab 后如下
针对这一章节创建基础镜像以及项目镜像,这里仅以 Java 项目为例;其他语言原理相通,按照其他语言对应的运行环境修改即可
GitLab CI 在进行构建时会将任务下发给 Runner,让 Runner 去执行;所以先要添加一个 Runner,Runner 这里采用 Docker Compose 启动,build 方式也使用 Docker 方式 Build;compose 文件如下
1 |
|
在启动前,我们需要先 touch 一下这个 config.toml 配置文件;该文件是 Runner 的运行配置,此后 Runner 所有配置都会写入这个文件(不 touch 出来 docker-compose 发现不存在会挂载一个目录进去,导致 Runner 启动失败);启动 docker-compose 后,需要进入容器执行注册,让 Runner 主动去连接 GitLab 服务器
1 |
|
在执行上一条激活命令后,会按照提示让你输入一些信息;首先输入 GitLab 地址,然后是 Runner Token,Runner Token 可以从 GitLab 设置中查看,如下所示
整体注册流程如下
注册完成后,在 GitLab Runner 设置中就可以看到刚刚注册的 Runner,如下所示
Runner 注册成功后会将配置写入到 config.toml 配置文件;由于两个测试宿主机都没有配置内网 DNS,所以为了保证 runner 在使用 docker build 时能正确的找到 GitLab 仓库地址,还需要增加一个 docker 的 host 映射( extra_hosts
);同时为了能调用 宿主机 Docker 和持久化 build 的一些缓存还挂载了一些文件和目录;完整的 配置如下(配置文件可以做一些更高级的配置,具体参考 官方文档 )
1 |
|
注意,这里声明的 Volumes 会在每个运行的容器中都生效;也就是说 build 时新开启的每个容器都会被挂载这些目录;修改完成后重启 runner 容器即可,由于 runner 中没啥可保存的东西,所以可以直接 docker-compose down && docker-compose up -d
重启
由于示例项目是一个 Java 项目,而且是采用 Spring Boot 的,所以该项目想要运行起来只需要一个 java 环境即可,中间件已经被打包到了 jar 包中;以下是一个作为基础运行环境的 openjdk 镜像的 Dockerfile
1 |
|
**这个 openjdk Dockerfile 升级到了 8.151 版本,并且集成了一些字体相关的软件,以解决在 Java 中某些验证码库无法运行问题,详见 Alpine 3.6 OpenJDK 8 Bug**;使用这个 Dockerfile,在当前目录执行 docker build -t mritd/openjdk:8 .
build 一个 openjdk8 的基础镜像,然后将其推送到私服,或者 Docker Hub 即可
有了基本的 openjdk 的 docker 镜像后,针对于项目每次 build 都应该生成一个包含发布物的 docker 镜像,所以对于项目来说还需要一个项目本身的 Dockerfile;项目的 Dockerfile 有两种使用方式;一种是动态生成 Dockerfile,然后每次使用新生成的 Dockerfile 去 build;还有一种是写一个通用的 Dockerfile,build 时利用 ARG 参数传入变量;这里采用第二种方式,以下为一个可以反复使用的 Dockerfile
1 |
|
该 Dockerfile 通过声明一个 PROJECT_BUILD_FINALNAME
变量来表示项目的发布物名称;然后将其复制到根目录下,最终利用 java 执行这个 jar 包;所以每次 build 之前只要能拿到项目发布物的名称即可
上面已经创建了一个标准的通用型 Dockerfile,每次 build 镜像只要传入 PROJECT_BUILD_FINALNAME
这个最终发布物名称即可;对于发布物名称来说,最好不要固定死;当然不论是 Java 还是其他语言的项目我们都能将最终发布物变成一个固定名字,最不济可以写脚本重命名一下;但是不建议那么干,最好保留版本号信息,以便于异常情况下进入容器能够分辨;对于当前 Java 项目来说,想要拿到 PROJECT_BUILD_FINALNAME
很简单,我们只需要略微修改一下 Gradle 的 build 脚本,让其每次打包 jar 包时将项目的名称及版本号导出到文件中即可;同时这里也加入了镜像版本号的处理,Gradle 脚本修改如下
1 |
|
这一步操作实际上是修改了 bootRepackage
这个 Task(不了解 Gradle 或者不是 Java 项目的请忽略),在其结束后创建了一个叫 PROJECT_ENV
的文件,里面实际上就是写入了一些 bash 环境变量声明,以方便后面 source 一下这个文件拿到一些变量,然后用户 build 镜像使用,PROJECT_ENV
最终生成如下
1 |
|
一切准备就绪以后,就可以编写 CI 脚本了;GitLab 依靠读取项目根目录下的 .gitlab-ci.yml
文件来执行相应的 CI 操作;以下为测试项目的 .gitlab-ci.yml
配置
1 |
|
关于 CI 配置的一些简要说明如下
stages 字段定义了整个 CI 一共有哪些阶段流程,以上的 CI 配置中,定义了该项目的 CI 总共分为 build
、deploy
两个阶段;GitLab CI 会根据其顺序执行对应阶段下的所有任务;在正常生产环境流程可以定义很多个,比如可以有 test
、publish
,甚至可能有代码扫描的 sonar
阶段等;这些阶段没有任何限制,完全是自定义的,上面的阶段定义好后在 CI 中表现如下图
task 隶属于 stages 之下;也就是说一个阶段可以有多个任务,任务执行顺序默认不指定会并发执行;对于上面的 CI 配置来说 auto-build
和 deploy
都是 task,他们通过 stage: xxxx
这个标签来指定他们隶属于哪个 stage;当 Runner 使用 Docker 作为 build 提供者时,我们可以在 task 的 image
标签下声明该 task 要使用哪个镜像运行,不指定则默认为 Runner 注册时的镜像(这里是 debian);同时 task 还有一个 tags
的标签,该标签指明了这个任务将可以在哪些 Runner 上运行;这个标签可以从 Runner 页面看到,实际上就是 Runner 注册时输入的哪个 tag;对于某些特殊的项目,比如 IOS 项目,则必须在特定机器上执行,所以此时指定 tags 标签很有用,当 task 运行后如下图所示
除此之外 task 还能指定 only
标签用于限定那些分支才能触发这个 task,如果分支名字不满足则不会触发;默认情况下,这些 task 都是自动执行的,如果感觉某些任务太过危险,则可以通过增加 when: manual
改为手动执行;注意: 手动执行被 GitLab 认为是高权限的写操作,所以只有项目管理员才能手动运行一个 task,直白的说就是管理员才能点击;手动执行如下图所示
cache 这个参数用于定义全局那些文件将被 cache;在 GitLab CI 中,跨 stage 是不能保存东西的;也就是说在第一步 build 的操作生成的 jar 包,到第二部打包 docker image 时就会被删除;GitLab 会保证每个 stage 中任务在执行时都将工作目录(Docker 容器 中)还原到跟 GitLab 代码仓库中一模一样,多余文件及变更都会被删除;正常情况下,第一步 build 生成 jar 包应当立即推送到 nexus 私服;但是这里测试没有搭建,所以只能放到本地;但是放到本地下一个 task 就会删除它,所以利用 cache
这个参数将 build
目录 cache 住,保证其跨 stage 也能存在
关于 .gitlab-ci.yml
具体配置更完整的请参考 官方文档
上面已经基本搞定了一个项目的 CI,但是有些变量可能并未说清楚;比如在创建的 PROJECT_ENV
文件中引用了 ${CI_COMMIT_SHA}
变量;这种变量其实是 GitLab CI 的内置隐藏变量,这些变量在每次 CI 调用 Runner 运行某个任务时都会传递到对应的 Runner 的执行环境中;也就是说这些变量在每次的任务容器 SHELL 环境中都会存在,可以直接引用,具体的完整环境变量列表可以从 官方文档 中获取;如果想知道环境变量具体的值,实际上可以通过在任务执行前用 env
指令打印出来,如下所示
在某些情况下,我们希望 CI 能自动的发布或者修改一些东西;比如将 jar 包上传到 nexus、将 docker 镜像 push 到私服;这些动作往往需要一个高权限或者说有可写入对应仓库权限的账户来支持,但是这些账户又不想写到项目的 CI 配置里;因为这样很不安全,谁都能看到;此时我们可以将这些敏感变量写入到 GitLab 自定义环境变量中,GitLab 会像对待内置变量一样将其传送到 Runner 端,以供我们使用;GitLab 中自定义的环境变量可以有两种,一种是项目级别的,只能够在当前项目使用,如下
另一种是组级别的,可以在整个组内的所有项目中使用,如下
这两种变量添加后都可以在 CI 的脚本中直接引用
对于 Kubernetes 集成实际上有两种方案,一种是对接 Kubernetes 的 api,纯代码实现;另一种取巧的方案是调用 kubectl 工具,用 kubectl 工具来实现滚动升级;这里采用后一种取巧的方式,将 kubectl 二进制文件封装到镜像中,然后在 deploy 阶段使用这个镜像直接部署就可以
其中 mritd/docker-kubectl:v1.7.4
这个镜像的 Dockerfile 如下
1 |
|
这里面的 ${KUBE_CONFIG}
是一个自定义的环境变量,对于测试环境我将配置文件直接挂载入了容器中,然后 ${KUBE_CONFIG}
只是指定了一个配置文件位置,实际生产环境中可以选择将配置文件变成自定义环境变量使用
关于 GitLab CI 上面已经讲了很多,但是并不全面,也不算太细致;因为这东西说起来实际太多了,现在目测已经 1W 多字了;以下总结一下 GitLab CI 的总体思想,当思路清晰了以后,我想后面的只是查查文档自己试一试就行了
CS 架构
GitLab 作为 Server 端,控制 Runner 端执行一系列的 CI 任务;代码 clone 等无需关心,GitLab 会自动处理好一切;Runner 每次都会启动新的容器执行 CI 任务
容器即环境
在 Runner 使用 Docker build 的前提下;所有依赖切换、环境切换应当由切换不同镜像实现,即 build 那就使用 build 的镜像,deploy 就用带有 deploy 功能的镜像;通过不同镜像容器实现完整的环境隔离
CI即脚本
不同的 CI 任务实际上就是在使用不同镜像的容器中执行 SHELL 命令,自动化 CI 就是执行预先写好的一些小脚本
敏感信息走环境变量
一切重要的敏感信息,如账户密码等,不要写到 CI 配置中,直接放到 GitLab 的环境变量中;GitLab 会保证将其推送到远端 Runner 的 SHELL 变量中
]]>最近准备整理一下关于 CI/CD 的相关文档,写一个关于 CI/CD 的系列文章,这篇先从最基本的 Dockerfile 书写开始,本系列文章默认读者已经熟悉 Docker、Kubernetes 相关工具
这里的基础镜像指的是实际项目运行时的基础环境镜像,比如 Java 的 JDK 基础镜像、Nodejs 的基础镜像等;在制作项目的基础镜像时,我个人认为应当考虑一下几点因素:
可维护性应当放在首要位置,如果在制作基础镜像时,选择了一个你根本不熟悉的基础镜像,或者说你完全不知道这个基础镜像里有哪些环境变量、Entrypoint 脚本做了什么时,请果断放弃这个基础镜像,选择一个你自己更加熟悉的基础镜像,不要为以后挖坑;还有就是如果对应的应用已经有官方镜像,那么尽量采用官方的,因为你可以省去维护 自己造的轮子 的精力,除非你对基础镜像制作已经得心应手,否则请不要造轮子
基础镜像稳定性实际上是个很微妙的话题,因为普遍来说成熟的 Linux 发行版都很稳定;但是对于不同发行版镜像之间还是存在差异的,比如 alpine 的镜像用的是 musl libc,而 debian 用的是 glibc,某些依赖 glibc 的程序可能无法在 alpine 上工作;alpine 版本的 nginx 能使用 http2,debian 版本 nginx 则不行,因为 openssl 版本不同;甚至在相同发行版不同版本之间也会有差异,譬如 openjdk alpine 3.6 版本 java 某些图形库无法工作,在 alpine edge 上安装最新的 openjdk 却没问题等;所以稳定性这个话题对于基础镜像自己来说,他永远稳定,但是对于你的应用来说,则不同基础镜像会产生不同的稳定性;最后,如果你完全熟悉你的应用,甚至应用层代码也是你写的,那么你可以根据你的习惯和喜好去选择基础镜像,因为你能把控应用运行时依赖;否则的话,请尽量选择 debian 这种比较成熟的发行版作为基础镜像,因为它在普遍上兼容性更好一点;还有尽量不要使用 CentOS 作为基础镜像,因为他的体积将会成为大规模网络分发瓶颈
易用性简单地说就是是否可调试,因为有些极端情况下,并不是应用只要运行起来就没事了;可能出现一些很棘手的问题需要你进入容器进行调试,此时你的镜像易用性就会体现出来;譬如一个 Java 项目你的基础镜像是 JRE,那么 JDK 的调试工具将完全不可用,还有就是如果你的基础镜像选择了 alpine,那么它默认没有 bash,可能你的脚本无法在里面工作;所有在选择基础镜像的时候最好也考虑一下未来极端情况的可调试性
Dockerfile 类似一堆 shell 命令的堆砌,实际上在构建阶段也可以简单的看做是一个 shell 脚本;但是为了更高效的利用缓存层,通常都会在一个 RUN 命令中连续书写大量的脚本命令,这时候一个良好的书写格式可以使 Dockerfile 看起来更加清晰易懂,也方便以后维护;我个人比较推崇的格式是按照 nginx-alpine官方 Dockerfile 的样式来书写,这个 Dockerfile 大致包括了以下规则:
&&
开头保持每行对齐,看起来干净又舒服除了以上规则,说下我个人的一些小习惯,仅供参考:
cd
目录,必须进入目录编译时可以开启子 shell 使其完成后还停留但在当前目录,避免 cd
进去再 cd
回来,如1 |
|
可以变为
1 |
|
wget
,那么没必要安装完一个删除一个安装包,可以在最后统一的进行清理动作,简而言之是 合并具有相同目的的命令ADD
/COPY
操作,因为一般 Dockerfile 都是存放到 git 仓库的,同目录下的二进制变动会给 git 仓库带来很大负担FROM
时指定具体的版本号,防止后续升级或者更换主机 build 造成不可预知的结果Docker 在 build 或者说是拉取镜像时是以层为单位作为缓存的;通俗的讲,一个 Dockerfile 命令就会形成一个镜像层(不绝对),尤其是 RUN
命令形成的镜像层可能会很大;此时应当合理组织 Dockerfile,以便每次拉取或者 build 时高效的利用缓存层
Docker 在进行 build 操作时,对于同一个 Dockerfile 来说,只要执行过一次 build,那么下次 build 将从命令更改处开始;简单的例子如下
1 |
|
假设我们的项目发布物为 test.jar
,那么以上 Dockerfile 放到 CI 里每次 build 都会相当慢,原因就是 每次更改的发布物为 test.jar
,那么也就是相当于每次 build 失效位置从 COPY
命令开始,这将导致下面的 RUN
命令每次都会不走缓存重复执行,当 RUN
命令涉及网络下载等复杂动作时这会极大拖慢 build 进度,解决方案很简单,移动一下 COPY
命令即可
1 |
|
此时每次 build 失效位置仍然是 COPY
命令,但是上面的 RUN
命令层已经被 build 过,而且无任何改变,那么每次 build 时 RUN
命令都会命中缓存层从而秒过
同上面的 build 一个原理,在 Docker 进行 pull 操作时,也是按照镜像层来进行缓存;当项目进行更新版本,那么只要当前主机 pull 过一次上一个版本的项目,那么下一次将会直接 pull 变更的层,也就是说上面安装 openjdk 的层将会复用;这种情况为了看起来清晰一点也可以将 Dockerfile 拆分成两个
OpenJDK8 base
1 |
|
Java Web image
1 |
|
当我们不在 Dockerfile 中指定内部用户时,那么默认以 root 用户运行;由于 Linux 系统权限判定是根据 UID、GID 来进行的,也就是说 容器里面的 root 用户有权限访问宿主机 root 用户的东西;所以一旦挂载错误(比如将 /root/.ssh
目录挂载进去),并且里面的用户具有高权限那么就很危险;通常习惯是遵从最小权限原则,也就是说尽量保证容器里的程序以低权限运行,此时可以在 Dockerfile 中通过 USER
命令指定后续运行命令所使用的账户,通过 WORKDIR
指定后续命令在那个目录下执行
1 |
|
有时直接使用 USER
指令来切换用户并不算方便,比如你的镜像需要挂载外部存储,如果外部存储中文件权限被意外修改,你的程序接下来可能就会启动失败;此时可以使用一下两个小工具来动态切换用户,巧妙的做法是 在正式运行程序之前先使用 root 用户进行权限修复,然后使用以下工具切换到具体用户运行
具体的 Dockerfile 可以参见我写的 elasticsearch 的 entrypoint 脚本
并不是每个容器都一定能切换到低权限用户来运行的,可能某些程序就希望在 root 下运行,此时一定要确认好容器是否需要 特权模式 运行;因为一旦开启了特权模式运行的容器将有能力修改宿主机内核参数等重要设置;具体的 Docker 容器运行设置前请参考 官方文档
关于 Dockerfile 方面暂时总结出这些,可能也会有遗漏,待后续补充吧;同时欢迎各位提出相关修改意见 😊
]]>由于业务需求,以前账号管理混乱,所以很多人有生产服务器的 root 权限;所以目前需要一个能 ssh 登录线上服务器的工具,同时具有简单的审计功能;找了好久找到了这个小工具,以下记录一下搭建教程
目前准备了 3 台虚拟机,两台位于内网 NAT 之后,一台位于公网可以直接链接;使用时客户端通过工具连接到公网跳板机上,然后实现自动跳转到内网任意主机;并且具有相应的操作回放审计,通过宿主机账户限制用户权限
ip | 节点 |
---|---|
92.223.67.84 | 公网 Master |
172.16.0.80 | 内网 Master |
172.16.0.81 | 内网 Node |
Teleport 工作时从宏观上看是以集群为单位,也就是说公网算作一个集群,内网算作另一个集群,内网集群通过 ssh 隧道保持跟公网的链接状态,同时内网机群允许公网集群用户连接,大体工作模式如下
首先下载相关可执行文件并复制到 Path 目录下,然后创建一下配置目录等
1 |
|
然后为了让服务后台运行创建一个 systemd service 配置文件
1 |
|
Systemd 配置完成后,就需要写一个 Teleport 的配置文件来让 Teleport 启动,具体选项含义可以参考 官方文档;以下为我的配置样例
1 |
|
然后启动 Teleport 即可
1 |
|
如果启动出现如下错误
1 |
|
请执行 ssh-keygen 命令自行生成相关秘钥
1 |
|
公网这台 Teleport 将会作为主要的接入机器,所以在此节点内添加的用户将有权限登录所有集群,包括内网的另一个集群;所以为了方便以后操作先添加一个用户
1 |
|
添加成功后会返回一个 OTP 认证初始化地址,浏览器访问后可以使用 Google 扫描 OTP 二维码从而在登录时增加一层 OTP 认证
访问该地址后初始化密码及 OTP
内网搭建 Master 和公网类似,只不过为了安全将所有 0.0.0.0
的地址全部换成内网 IP 即可,以下为内网的配置信息
1 |
|
配置完成后直接启动即可
1 |
|
上文已经讲过,Teleport 通过公网链接内网主机的方式是让内网集群向公网打通一条 ssh 隧道,然后再进行通讯;具体配置如下
在公网 Master 增加 Token 配置,以允许持有该 Token 的其他内网集群连接到此,修改 /etc/teleport/teleport.yaml
增加一个 token 即可
1 |
|
然后重启 Teleport
1 |
|
当公网集群开启了允许其他集群链接后,内网集群只需要创建配置进行连接即可,创建配置(cluster.yaml)如下
1 |
|
执行以下命令使内网集群通过 ssh 隧道连接到公网集群
1 |
|
注意,如果在启动公网和内网集群时没有指定受信的证书( https_cert_file
、https_key_file
),那么默认 Teleport 将会生成一个自签名证书,此时在 create 受信集群时将会产生如下错误:
1 |
|
此时需要在 待添加集群(内网) 启动时增加 --insecure
参数,即 Systemd 配置修改如下
1 |
|
然后再进行 create 就不会报错
两台节点打通后,此时如果有其他机器则可以将其加入到对应集群中,以下以另一台内网机器为例
由于在主节点 auth_service
中已经预先指定了一个 static Token 用于其他节点加入( proxy,node:jYektagNTmhjv9Dh
),所以其他节点只需要使用这个 Token 加入即可,在另一台内网主机上修改 Systemd 配置如下,然后启动即可
1 |
|
此时在内网的 Master 上可以查看到 Node 已经加入
1 |
|
Teleport 支持 Web 页面访问,直接访问 https://公网IP:3080
,然后登陆即可,登陆后如下
通过 Cluster 选项可以切换不同集群,点击后面的用户名可以选择不同用户登录到不同主机(用户授权在添加用户时控制),登陆成功后如下
通过 Teleport 进行的所有操作可以通过审计菜单进行操作回放
类 Uninx 系统下我们还是习惯使用终端登录,终端登录需要借助 Teleport 的命令行工具 tsh
,tsh
在下载的 release 压缩版中已经有了,具体使用文档请自行 help 和参考官方文档,以下为简单的使用示例
1 |
|
1 |
|
1 |
|
本文主要记录下 Kubernetes 下运行深度学习框架如 Tensorflow、Caffe2 等一些坑,纯总结性文档
Kubernetes 运行深度学习应用实际上要解决的唯一问题就是 GPU 调用,以下只描述 Nvidia 相关的问题以及解决方法;要想完成 Kubernetes 对 GPU 调用,首先要满足以下条件:
关于 Nvidia 驱动和 CUDA 请自行查找安装方法,如果这两部都搞不定,那么不用继续了
还有一点需要注意: /var/lib
这个目录不能处于单独分区中,具体原因下面阐述
在安装 Nvidia Docker 之前,请确保 Nvidia 驱动以及 CUDA 安装成功,并且 nvidia-smi
能正确显示,如下图所示(来源于网络)
Nvidia Docker 安装极其简单,具体可参考 官方文档,安装完成后请自行按照官方文档描述进行测试,这一步一般不会出现问题
如果测试成功后,请查看 /var/lib/nvidia-docker/volumes
目录下是否有文件,如果没有,那就意味着 Nvidia Docker 并未生成相关的驱动文件成功,需要单独执行 docker volume create --driver=nvidia-docker --name=nvidia_driver_$(modinfo -F version nvidia)
以生成该文件;该命令生成的方式是将已经安装到系统的相关文件硬链接至此,所以要求 /var/lib
目录不能在单独的分区;驱动生成完成后应该会产生类似 /var/lib/nvidia-docker/volumes/nvidia_driver/375.66
的目录结构
当所有基础环境就绪后,最后需要开启 Kubernetes 对 GPU 支持;Kubernetes GPU 文档可以参考 这里,实际主要就是在 kubelet 启动时增加 --feature-gates="Accelerators=true"
参数,如下所示
所有节点全部修改完成后重启 kubelet 即可,如果一台机器上有不同型号的显卡,同时希望 Pod 能区别使用不同的 GPU 则可以按照 官方文档 增加相应设置
Deployment 部署采用一个 Tensorflow 镜像作为示例,部署配置如下
1 |
|
Deployment 中运行的 Pod 需要挂载对应的宿主机设备文件以及驱动文件才能正确的调用宿主机 GPU,所以一定要确保前几步生成的相关驱动文件等没问题;如果有多个 nvidia 显卡的话可能需要挂载多个 nvidia 设备
Pod 运行成功后可执行以下代码测试 GPU 调用
1 |
|
成功后截图如下
]]>Kubernetes 1.8 发布已经好几天,1.8 对于 kube-proxy 组件增加了 ipvs 支持,以下记录一下 kube-proxy ipvs 开启教程
目前测试为 5 台虚拟机,CentOS 系统,etcd、kubernetes 全部采用 rpm 安装,使用 systemd 来做管理,网络组件采用 calico,Master 实现了 HA;基本环境如下
IP | 组件 |
---|---|
10.10.1.5 | Master、Node、etcd |
10.10.1.6 | Master、Node、etcd |
10.10.1.7 | Master、Node、etcd |
10.10.1.8 | Node |
10.10.1.9 | Node |
之所以把这个单独写一个标题是因为坑有点多,为了避免下面出现问题,先说一下注意事项:
如果对 SELinux 玩的不溜的朋友,我建议先关闭 SELinux,关闭方法如下
1 |
|
然后重启机器并验证
1 |
|
搭建时尽量关闭防火墙,如果你玩的很溜,那么请在测试没问题后再开启防火墙
1 |
|
确保内核已经开启如下参数,或者说确保 /etc/sysctl.conf
有如下配置
1 |
|
然后执行 sysctl -p
使之生效
1 |
|
由于 ipvs 已经加入到内核主干,所以需要内核模块支持,请确保内核已经加载了相应模块;如不确定,执行以下脚本,以确保内核加载相应模块,否则会出现 failed to load kernel modules: [ip_vs_rr ip_vs_sh ip_vs_wrr]
错误
1 |
|
执行后应该如下图所示,如果 lsmod | grep ip_vs
并未出现 ip_vs_rr
等模块;那么请更换内核(一般不会,2.6 以后 ipvs 好像已经就合并进主干了)
修改 /etc/kubernetes/proxy
配置如下
1 |
|
启用 ipvs 后与 1.7 版本的配置差异如下:
--feature-gates=SupportIPVSProxyMode=true
选项,用于告诉 kube-proxy 开启 ipvs 支持,因为目前 ipvs 并未稳定ipvs-min-sync-period
、--ipvs-sync-period
、--ipvs-scheduler
三个参数用于调整 ipvs,具体参数值请自行查阅 ipvs 文档--masquerade-all
选项,以确保反向流量通过重点说一下 --masquerade-all
选项: kube-proxy ipvs 是基于 NAT 实现的,当创建一个 service 后,kubernetes 会在每个节点上创建一个网卡,同时帮你将 Service IP(VIP) 绑定上,此时相当于每个 Node 都是一个 ds,而其他任何 Node 上的 Pod,甚至是宿主机服务(比如 kube-apiserver 的 6443)都可能成为 rs;按照正常的 lvs nat 模型,所有 rs 应该将 ds 设置成为默认网关,以便数据包在返回时能被 ds 正确修改;在 kubernetes 将 vip 设置到每个 Node 后,默认路由显然不可行,所以要设置 --masquerade-all
选项,以便反向数据包能通过
以上描述可能并不精准,具体请看 Google 文档
修改完成后,重启 kube-proxy 使其生效
1 |
|
重启后日志中应该能看到如下输出,不应该有其他提示 ipvs 的错误信息出现
同时使用 ipvsadm 命令应该能看到相应的 service 的 ipvs 规则(ipvsadm 自己安装一下)
然后进入 Pod 测试
最后说一点: ipvs 尚未稳定,请慎用;而且 --masquerade-all
选项与 Calico 安全策略控制不兼容,请酌情考虑使用(Calico 在做网络策略限制的时候要求不能开启此选项)
目前 Kubernetes 1.8.0 已经发布,1.8.0增加了很多新特性,比如 kube-proxy 组建的 ipvs 模式等,同时 RBAC 授权也做了一些调整,国庆没事干,所以试了一下;以下记录了 Kubernetes 1.8.0 的搭建过程
目前测试为 5 台虚拟机,etcd、kubernetes 全部采用 rpm 安装,使用 systemd 来做管理,网络组件采用 calico,Master 实现了 HA;基本环境如下
IP | 组件 |
---|---|
10.10.1.5 | Master、Node、etcd |
10.10.1.6 | Master、Node、etcd |
10.10.1.7 | Master、Node、etcd |
10.10.1.8 | Node |
10.10.1.9 | Node |
本文尽量以实际操作为主,因为写过一篇 Kubernetes 1.7 搭建文档,所以以下细节部分不在详细阐述,不懂得可以参考上一篇文章;本文所有安装工具均已打包上传到了 百度云 密码: 4zaz
,可直接下载重复搭建过程,搭建前请自行 load 好 images 目录下的相关 docker 镜像
同样证书工具仍使用的是 cfssl,百度云的压缩包里已经包含了,下面直接上配置(注意,所有证书生成只需要在任意一台主机上生成一遍即可,我这里在 Master 上操作的)
1 |
|
1 |
|
1 |
|
最后生成证书
1 |
|
证书生成后截图如下
首先分发证书及 rpm 包
1 |
|
1 |
|
然后修改配置如下(其他两个节点类似,只需要改监听地址和 Etcd Name 即可)
1 |
|
最后启动集群并测试如下
1 |
|
生成证书配置文件需要借助 kubectl,所以先要安装一下 kubernetes-client 包
1 |
|
生成证书配置如下
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
最后生成证书及配置文件
1 |
|
创建好证书以后就要进行分发,同时由于 Master 也作为 Node 使用,所以以下命令中在 Master 上也安装了 kubelet、kube-proxy 组件
1 |
|
证书与 rpm 都安装完成后,只需要修改配置(配置位于 /etc/kubernetes
目录)后启动相关组件即可
1 |
|
1 |
|
注意:API SERVER 对比 1.7 配置出现几项变动:
--runtime-config=rbac.authorization.k8s.io/v1beta1
配置,因为 RBAC 已经稳定,被纳入了 v1 api,不再需要指定开启--authorization-mode
授权模型增加了 Node
参数,因为 1.8 后默认 system:node
role 不会自动授予 system:nodes
组,具体请参看 CHANGELOG(before-upgrading 段最后一条说明)--admission-control
同时增加了 NodeRestriction
参数,关于关于节点授权器请参考 Using Node Authorization--audit-policy-file
参数用于指定高级审计配置,具体可参考 CHANGELOG(before-upgrading 第四条)、Advanced audit--experimental-bootstrap-token-auth
参数,更换为 --enable-bootstrap-token-auth
,详情参考 CHANGELOG(Auth 第二条)1 |
|
1 |
|
最后启动 Master 相关组件并验证
1 |
|
对于 Node 节点,只需要安装 kubernetes-node
即可,同时为了方便使用,这里也安装了 kubernetes-client
,如下
1 |
|
同时还要分发相关证书;这里将 Etcd 证书已进行了分发,是因为 虽然 Node 节点上没有 Etcd,但是如果部署网络组件,如 calico、flannel 等时,网络组件需要联通 Etcd 就会用到 Etcd 的相关证书。
1 |
|
Node 上只需要修改 kubelet 和 kube-proxy 的配置即可
1 |
|
1 |
|
注意: kubelet 配置与 1.7 版本有一定改动
--fail-swap-on=false
选项,否则可能导致在开启 swap 分区的机器上无法启动 kubelet,详细可参考 CHANGELOG(before-upgrading 第一条)--require-kubeconfig
选项,已经过时废弃1 |
|
kube-proxy 配置与 1.7 并无改变,最新 1.8 的 ipvs 模式将单独写一篇文章,这里不做介绍
由于 HA 方案基于 Nginx 反代实现,所以每个 Node 要启动一个 Nginx 负载均衡 Master,具体参考 HA Master 简述
1 |
|
1 |
|
最后启动 Nginx 代理即可
1 |
|
一切准备就绪后就可以添加 Node 了,首先由于我们采用了 TLS Bootstrapping,所以需要先创建一个 ClusterRoleBinding
1 |
|
然后启动 kubelet
1 |
|
由于采用了 TLS Bootstrapping,所以 kubelet 启动后不会立即加入集群,而是进行证书申请,从日志中可以看到如下输出
1 |
|
此时只需要在 master 允许其证书申请即可
1 |
|
此时可以看到 Node 已经加入了
1 |
|
最后再启动 kube-proxy 即可
1 |
|
再次提醒: 如果 kubelet 启动出现了类似 system:node:xxxx
用户没有权限访问 API 的 RBAC 错误,那么一定是 API Server 授权控制器、准入控制配置有问题,请仔细阅读上面的文档进行更改
如果想讲 Master 也作为 Node 的话,请在 Master 上安装 kubernete-node rpm 包,配置与上面基本一致;区别于 Master 上不需要启动 nginx 做负载均衡,同时 bootstrap.kubeconfig
、kube-proxy.kubeconfig
中的 API Server 地址改成当前 Master IP 即可。
最终成功后如下图所示
Calico 部署仍然采用 “混搭” 方式,即 Systemd 控制 calico node,cni 等由 kubernetes daemonset 安装,具体请参考 Calico 部署踩坑记录,以下直接上代码
1 |
|
上一步注释了 calico.yaml
中 Calico Node 相关内容,为了防止自动获取 IP 出现问题,将其移动到 Systemd,Systemd service 配置如下,每个节点都要安装 calico-node 的 Service,其他节点请自行修改 ip(被问我为啥是两个反引号 \\
,自己试就知道了)
1 |
|
根据官方文档要求 kubelet
配置必须增加 --network-plugin=cni
选项,所以需要修改 kubelet 配置
1 |
|
然后重启即可
1 |
|
此时执行 kubectl get node
会看到 Node 为 NotReady
状态,属于正常情况
1 |
|
Calico Node 采用 Systemd 方式启动,在每个节点配置好 Systemd service后,每个节点修改对应的 calico-node.service
中的 IP 和节点名称,然后启动即可
1 |
|
此时检查 Node 应该都处于 Ready 状态
最后测试一下跨主机通讯
1 |
|
进入其中一个 Pod,ping 另一个 Pod 的 IP 测试即可
DNS 组件部署非常简单,直接创建相应的 deployment 等即可;但是有一个事得说一嘴,Kubernets 一直在推那个 Addon Manager
的工具来管理 DNS 啥的,文档说的条条是道,就是不希望我们手动搞这些东西,防止意外修改云云… 但问题是关于那个 Addon Manager
咋用一句没提,虽然说里面就一个小脚本,看看也能懂;但是我还是选择手动 😌… 还有这个 DNS 配置文件好像又挪地方了,以前在 contrib
项目下的…
1 |
|
创建好以后如下所示
然后创建两组 Pod 和 Service,进入 Pod 中 curl 另一个 Service 名称看看是否能解析;同时还要测试一下外网能否解析
测试外网
这个同样下载 yaml,然后创建一下即可,不需要修改任何配置
1 |
|
部署完成后如下
自动扩容这里不做测试了,虚拟机吃不消了,详情自己参考 Autoscale the DNS Service in a Cluster
kube-proxy ipvs 下一篇写,坑有点多,虽然搞定了,但是一篇写有点囫囵吞枣,后来想一想还是分开吧
]]>最近切换项目基础镜像踩到一个大坑,由于 alpine 基础镜像和 OpenJDK8 Bug 导致鼓捣了2天才解决,故记录一下这个问题
出现问题的基本环境如下
出现问题表象为 Spring Boot 项目启动后,访问注册页(有验证码)时,验证码不显示,后台报错信息大意为缺失字体库,安装字体后会报错说 libfontmanager.so: AWTFontDefaultChar: symbol not found
当出现字体找不到这种错误时,原因是 Alpine 太过精简,导致里面没有字体,只需要安装字体即可,在 Dockerfile 中添加如下命令即可:
1 |
|
当安装字体后,可能会出现如下错误:
1 |
|
Google 半天,最后找到了 Alpine 官方 Bug 列表,在最后面做了回复,其中大意是: Alpine 3.6 版本的 Docker 镜像中安装的是 OpenJDK 8u131,这个版本有 BUG,并且在 3.6.3 的 OpenJDK 8.141.15 版本做了修复;从上面可知我们解决方案有两个:
当然我选择浪一波,做了升级,最终基础镜像的 Dockerfile 如下
1 |
|
不知道 Consul 用的人多还是少,最近有人问怎么搭建 Consul 集群,这里顺手记录一下吧
Consul 与 Etcd 一样,都属于分布式一致性数据库,其主要特性就是在分布式系统中出现意外情况如节点宕机的情况下保证数据的一致性;相对于 Etcd 来说,Consul 提供了更加实用的其他功能特性,如 DNS、健康检查、服务发现、多数据中心等,同时还有 web ui 界面,体验相对于更加友好
同 Etcd 一样,Consul 最少也需要 3 台机器,这里测试实用 5 台机器进行部署集群,具体环境如下
节点 | IP | Version |
---|---|---|
server | 192.168.1.11 | v0.9.3 |
server | 192.168.1.12 | v0.9.3 |
server | 192.168.1.13 | v0.9.3 |
client | 192.168.1.14 | v0.9.3 |
client | 192.168.1.15 | v0.9.3 |
其中 consul 采用 rpm 包的形式进行安装,这里并没有使用 docker 方式启动是因为个人习惯重要的数据存储服务交给 systemd管理;因为 docker 存在 docker daemon 的原因,如果用 docker 启动这种存储核心数据的组件,一但 daemon 出现问题那么所有容器都将出现问题;所以个人还是比较习惯将 etcd 和 consul 以二进制装在宿主机,由 systemd 直接管理。
Consul 集群与 Etcd 略有区别,Consul 在启动后分为两种模式:
其集群后如下所示:
Consul 集群搭建时一般提供两种模式:
这里采用自动加入模式,搭建过程如下:
首先获取 Consul 的 rpm 包,鉴于官方并未提供 rpm 安装包,所以我自己造了一个轮子,打包脚本见 Github,以下直接从我的 yum 源中安装
1 |
|
5 台机器安装好后修改其中三台为 Server 模式并启动
1 |
|
另外两个节点与以上配置大致相同,差别在于其他两个 Server 节点 bootstrap_expect
值为 2,即期望启动时已经有两个节点就绪;然后依次启动三个 Server 节点即可
1 |
|
此时可访问任意一台 Server 节点的 UI 界面,地址为 http://serverIP:8500
,截图如下
接下来修改其他两个节点配置,使其作为 Client 加入到集群即可,注意的是当处于 Client 模式时,bootstrap_expect
必须为 0,即关闭状态;具体配置如下
1 |
|
另外一个 Client 配置与以上相同,最终集群成功后如下所示
关于 Consul 的其他各种参数说明,中文版可参考 Consul集群部署;这个文章对大体上讲的基本很全了,但是随着版本变化,有些参数还是需要参考一下 官方配置文档
]]>公司有点小需求,在阿里云上开了几台机器,然后部署了一个 Kubernetes 集群,以下记录一下阿里云踩坑问题,主要是网络组件的坑。
部署时开启了 4 台 ECS 实例,基本部署环境与裸机部署相似,其中区别是,阿里云网络采用 VPC 网络,不过以下流程适用于经典网络;以下为各个组件版本:
flannel 采用 vxlan 模式,虽然性能不太好,但是兼容度高一点;在阿里云上 flannel 可以采用 vpc 方式,具体可参考 官方文档(这个文档中描述的方法应该更适合 CNM 方式,我用的是 CNI,所以没去折腾他)
关于 Master HA 等基本部署流程可以参考 手动档搭建 Kubernetes HA 集群 这篇文章,在部署网络组件之前的流程是相同的,这里不再阐述
关于 Flannel 部署,基本上有两种模式,一种是 vxlan,一种是采用 VPC,VPC 相关的部署上面已经提了,可以参考官方文档;以下说一下 Flannel 的 vxlan 部署方式:
首先保证集群在不开启 CNI 插件的情况下所有 Node Ready 状态,然后修改 /etc/kubernetes/kubelet
配置文件,加入 CNI 支持( --network-plugin
),配置如下
1 |
|
在开启 CNI 时使用 Flannel,要设置 --allocate-node-cidrs
和 --cluster-cidr
以保证 Flannel 能正确进行 IP 分配,这两个配置需要加入到 /etc/kubernetes/controller-manager
配置中,完整配置如下
1 |
|
开启 CNI 后,kubelet 创建的 POD 则需要 CNI 插件支持,这里让我感觉奇怪的是 Flannel 的 yaml 中对于 install-cni
这个容器只进行了配置复制,没有做插件复制;所以我们需要手动安装 CNI 插件,CNI 插件最新版本请留意 Github;安装过程如下:
1 |
|
当上面所有配置和 CNI 插件安装完成后,应当重启 kube-controller-manager 和 kubelet
1 |
|
然后安装 Flannel 并配置 RBAC 即可
1 |
|
其他部署如 dns 等与原流程相同,不在阐述
]]>由于 git 代码管理比较混乱,所以记录一下 Git Flow + GitLab 的整体工作流程
Git Flow 定义了一个围绕项目开发发布的严格 git 分支模型,用于管理多人协作的大型项目中实现高效的协作开发;Git Flow 分支模型最早起源于 Vincent Driessen 的 A successful Git branching model 文章;随着时间发展,Git Flow 大致分为三种:
关于三种 Git Flow 区别详情可参考 Git 工作流程
Github Flow 和 GitLab Flow 对于持续发布支持比较好,但是原始版本的 Git Flow 对于传统的按照版本发布更加友好一些,所以以下主要说明以下 Git Flow 的工作流程;Git Flow 主要分支模型如下
在整个分支模型中 存在两个长期分支: develop 和 master,其中 develop 分支为开发分支,master 为生产分支;master 代码始终保持随时可以部署到线上的状态;develop 分支用于合并最新提交的功能性代码;具体的分支定义如下
feature/xxxx
形式命名分支hotfixes/xxxx
形式命名的分支release/xxxx
分支在整个分支模型中,develop 分支为最上游分支,会不断有新的 feature 合并入 develop 分支,当功能开发达到完成所有版本需求时,则从 develop 分支创建 release 分支,release 后如没有发现其他问题,最终 release 会被合并到 master 分支以完成线上部署
针对于 Git Flow,其手动操作 git 命令可能过于繁琐,所以后来有了 git-flow 工具;git-flow 是一个 git 扩展集,按 Vincent Driessen 的分支模型提供高层次的库操作;使用 git-flow 工具可以以更加简单的命令完成对 Vincent Driessen 分支模型的实践;
git-flow 安装以及使用具体请参考 git-flow 备忘清单,该文章详细描述了 git-flow 工具的使用方式
还有另一个工具是 git-extras,该工具没有 git-flow 那么简单化,不过其提供更加强大的命令支持
在整个 Git Flow 中,commit message 也是必不可少的一部分;一个良好且统一的 commit message 有助于代码审计以及 review 等;目前使用最广泛的写法是 Angular 社区规范,该规范大中 commit message 格式大致如下:
1 |
|
总体格式大致分为 3 部分,首行主要 3 个组成部分:
$browser, $compile, $rootScope, ngHref, ngClick, ngView, etc...
关于 type 提交类型,有如下几种值:
中间的 body 部分是对本次提交的详细描述信息,底部的 footer 部分一般分为两种情况:
BREAKING CHANGE:
开头,后面跟上不兼容变动的具体描述和解决办法Close #9527,#9528
不过 footer 部分也有特殊情况,如回滚某次提交,则以 revert:
开头,后面紧跟 commit 信息和具体描述;还有时某些 commit 只是解决了 某个 issue 的一部分问题,这是可以使用 refs ISSUE
的方式来引用该 issue
针对 Git 的 commit message 目前已经有了成熟的生成工具,比较有名的为 commitizen-cli 工具,其采用 node.js 编写,执行 git cz
命令能够自动生成符合 Angular 社区规范的 commit message;不过由于其使用 node.js 编写,所以安装前需要安装 node.js,因此可能不适合其他非 node.js 的项目使用;这里推荐一个基于 shell 编写的 Git-toolkit,安装此工具后执行 git ci
命令进行提交将会产生交互式生成 Angular git commit message 格式的提交说明,截图如下:
以上 Git Flow 所有操作介绍的都是在本地操作,而正常我们在工作中都是基于 GitLab 搭建私有 Git 仓库来进行协同开发的,以下简述以下 Git Flow 配合 GitLab 的流程
当开发一个新功能时流程如下:
git flow feature start xxxx
开启一个 feature 新分支git flow feature publish xxxx
将此分支推送到远端以便他人获取develop
分支发起合并请求master
权限用户合并其到 develop
分支当一定量的 feature 开发完成并合并到 develop 后,如所有 feature 都测试通过并满足版本需求,则可以创建 release 版本分支;release 分支流程如下
git flow release start xxxx
开启 release 分支git flow release publish xxxx
将其推送到远端以便他人获取当 master 某个 tag 部署到生产环境后,也可能出现不符合预期的问题出现;此时应该基于 master 创建 hotfix 分支进行修复,流程如下
git flow hotfix start xxxx
创建紧急修复分支自从上次在虚拟机中手动了部署了 Kubernetes 1.7.2 以后,自己在测试环境就来了一下,结果网络组件死活起不来,最后找到原因记录一下
在使用 Calico 前当然最好撸一下官方文档,地址在这里 Calico 官方文档,其中部署前需要注意以下几点
kubelet
配置必须增加 --network-plugin=cni
选项kube-proxy
组件必须采用 iptables
proxy mode 模式(1.2 以后是默认模式)kubec-proxy
组件不能采用 --masquerade-all
启动,因为会与 Calico policy 冲突NetworkPolicy API
只要需要 Kubernetes 1.3 以上在已经有了一个 Kubernetes 集群的情况下,官方部署方式描述的很简单,只需要改一改 yml 配置,然后 create 一下即可,具体描述见 官方文档
官方文档中大致给出了三种部署方案:
1.6 or high
和 1.5
区分两个 yml 配置,直接创建即可当我从虚拟机中测试完全没问题以后,就在测试环境尝试创建 Calico 网络,结果出现的问题是某个(几个) Calico 节点无法启动,同时创建 deployment 后,执行 route -n
会发现每个 node 只有自己节点 Pod 的路由,正常每个 node 上会有所有 node 上 Pod 网段的路由,如下(正常情况)
此时观察每个 node 上 Calico Pod 日志,会有提示 未知节点 xxxx 等错误日志,大体意思就是 未知的一个(几个)节点在进行 BGP 协议时被拒绝,偶尔某些 node 上还可能出现 IP 已经被占用 的神奇错误提示
后来经过翻查 Calico 自定义部署文档 和 Kargo 项目源码 发现了主要问题在于 官方文档中直接创建的 calico.yml 文件中,使用 DaemonSet 方式启动 calico-node,同时 calico-node 的 IP 设置和 NODENAME 设置均为空,此时 calico-node 会进行自动获取,网络复杂情况下获取会出现问题;比如 IP 拿到了 docker 网桥的 IP,NODENAME 获取不正确等,最终导致出现很奇怪的错误
一开始想到的解决方案很简单,直接照着 Kargo 抄,使用 Systemd 来启动 calico-node,然后在拆分过程中需要各种配置信息直接也根据 Kargo 的做法生成;当然鼓捣了 1/3 的时候就炸了,Kargo 是 ansible 批量部署的,有些变量找起来要人命;最后选择了一个折中(偷懒)的方案: 使用官方的 calico.yml 创建相关组件,这样 ConfigMap、Etcd 配置、Calico policy 啥的直接创建好,然后把 DaemonSet 中 calico-node 容器单独搞出来,使用 Systemd 启动,这样就即方便又简单(我真特么机智);最终操作如下:
在进行网络组件部署前,请确保集群已经满足 Calico 部署要求(本文第一部分);然后获取 calico.yml,注释掉 DaemonSet 中 calico-node 部分,如下所示
1 |
|
修改完成后直接 create 即可
最后写一个 service 文件(我放到了 /etc/systemd/system/calico-node.service
),使用 Systemd 启动即可;注意以下配置中 IP
、NODENAME
是自己手动定义的,IP 为宿主机 IP,NODENAME 最好与 hostname 相同
1 |
|
以前一直用 Kargo(基于 ansible) 来搭建 Kubernetes 集群,最近发现 ansible 部署的时候有些东西有点 bug,而且 Kargo 对 rkt 等也做了适配,感觉问题已经有点复杂化了;在 2.2 release 没出来这个时候,准备自己纯手动挡部署一下,Master HA 直接抄 Kargo 的就行了,以下记录一下;本文以下部分所有用到的 rpm 、配置文件等全部已经上传到了 百度云 密码: x5v4
以下文章本着 多写代码少哔哔 的原则,会主要以实际操作为主,不会过多介绍每步细节动作,如果纯小白想要更详细的了解,可以参考 这里
环境总共 5 台虚拟机,2 个 master,3 个 etcd 节点,master 同时也作为 node 负载 pod,在分发证书等阶段将在另外一台主机上执行,该主机对集群内所有节点配置了 ssh 秘钥登录
IP | 节点 |
---|---|
192.168.1.11 | master、node、etcd |
192.168.1.12 | master、node、etcd |
192.168.1.13 | master、node、etcd |
192.168.1.14 | node |
192.168.1.15 | node |
网络方案这里采用性能比较好的 Calico,集群开启 RBAC,RBAC 相关可参考 这里的胡乱翻译版本
由于 Etcd 和 Kubernetes 全部采用 TLS 通讯,所以先要生成 TLS 证书,证书生成工具采用 cfssl,具体使用方法这里不再详细阐述,生成证书时可在任一节点完成,这里在宿主机执行,证书列表如下
证书名称 | 配置文件 | 用途 |
---|---|---|
etcd-root-ca.pem | etcd-root-ca-csr.json | etcd 根 CA 证书 |
etcd.pem | etcd-gencert.json、etcd-csr.json | etcd 集群证书 |
k8s-root-ca.pem | k8s-root-ca-csr.json | k8s 根 CA 证书 |
kube-proxy.pem | k8s-gencert.json、kube-proxy-csr.json | kube-proxy 使用的证书 |
admin.pem | k8s-gencert.json、admin-csr.json | kubectl 使用的证书 |
kubernetes.pem | k8s-gencert.json、kubernetes-csr.json | kube-apiserver 使用的证书 |
首先下载 cfssl,并给予可执行权限,然后扔到 PATH 目录下
1 |
|
Etcd 证书生成所需配置文件如下:
1 |
|
1 |
|
1 |
|
最后生成 Etcd 证书
1 |
|
生成的证书列表如下
Kubernetes 证书生成所需配置文件如下:
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
生成 Kubernetes 证书
1 |
|
生成后证书列表如下
生成 token 如下
1 |
|
创建 kubelet bootstrapping kubeconfig 配置(需要提前安装 kubectl 命令),对于 node 节点,api server 地址为本地 nginx 监听的 127.0.0.1:6443,如果想把 master 也当做 node 使用,那么 master 上 api server 地址应该为 masterIP:6443,因为在 master 上没必要也无法启动 nginx 来监听 127.0.0.1:6443(6443 已经被 master 上的 api server 占用了)
所以以下配置只适合 node 节点,如果想把 master 也当做 node,那么需要重新生成下面的 kubeconfig 配置,并把 api server 地址修改为当前 master 的 api server 地址
没看懂上面这段话,照着下面的操作就行,看完下面的 Master HA 示意图就懂了
1 |
|
创建 kube-proxy kubeconfig 配置,同上面一样,如果想要把 master 当 node 使用,需要修改 api server
1 |
|
ETCD 直接采用 rpm 安装,RPM 可以从 Fedora 官方仓库 获取 spec 文件自己 build,或者直接从 rpmFind 网站 搜索
1 |
|
1 |
|
rpm 安装好以后直接修改 /etc/etcd/etcd.conf
配置文件即可,其中单个节点配置如下(其他节点只是名字和 IP 不同)
1 |
|
配置修改后在每个节点进行启动即可,注意,Etcd 各个节点间必须保证时钟同步,否则会造成启动失败等错误
1 |
|
启动成功后验证节点状态
1 |
|
最后截图如下,警告可忽略
目前所谓的 Kubernetes HA 其实主要的就是 API Server 的 HA,master 上其他组件比如 controller-manager 等都是可以通过 Etcd 做选举;而 API Server 只是提供一个请求接收服务,所以对于 API Server 一般有两种方式做 HA;一种是对多个 API Server 做 vip,另一种使用 nginx 反向代理,本文采用 nginx 方式,以下为 HA 示意图
master 之间除 api server 以外其他组件通过 etcd 选举,api server 默认不作处理;在每个 node 上启动一个 nginx,每个 nginx 反向代理所有 api server,node 上 kubelet、kube-proxy 连接本地的 nginx 代理端口,当 nginx 发现无法连接后端时会自动踢掉出问题的 api server,从而实现 api server 的 HA
一切以偷懒为主,所以我们仍然采用 rpm 的方式来安装 kubernetes 各个组件,关于 rpm 获取方式可以参考 How to build Kubernetes RPM,以下文章默认认为你已经搞定了 rpm
1 |
|
rpm 安装好以后还需要进行分发证书配置等
1 |
|
最后由于 api server 会写入一些日志,所以先创建好相关目录,并做好授权,防止因为权限错误导致 api server 无法启动
1 |
|
rpm 安装好以后,默认会生成 /etc/kubernetes
目录,并且该目录中会有很多配置,其中 config 配置文件为通用配置,具体文件如下
1 |
|
master 需要编辑 config
、apiserver
、controller-manager
、scheduler
这四个文件,具体修改如下
1 |
|
1 |
|
1 |
|
1 |
|
其他 master 节点配置相同,只需要修改以下 IP 地址即可,修改完成后启动 api server
1 |
|
各个节点启动成功后,验证组件状态(kubectl 在不做任何配置的情况下默认链接本地 8080 端口)如下,其中 etcd 全部为 Unhealthy 状态,并且提示 remote error: tls: bad certificate
这是个 bug,不影响实际使用,具体可参考 issue
部署前分发 rpm 以及证书、token 等配置
1 |
|
node 节点上配置文件同样位于 /etc/kubernetes
目录,node 节点只需要修改 config
、kubelet
、proxy
这三个配置文件,修改如下
注意: config 配置文件(包括下面的 kubelet、proxy)中全部未 定义 API Server 地址,因为 kubelet 和 kube-proxy 组件启动时使用了 --require-kubeconfig
选项,该选项会使其从 *.kubeconfig
中读取 API Server 地址,而忽略配置文件中设置的;所以配置文件中设置的地址其实是无效的
1 |
|
1 |
|
1 |
|
由于 kubelet 采用了 TLS Bootstrapping,所有根绝 RBAC 控制策略,kubelet 使用的用户 kubelet-bootstrap
是不具备任何访问 API 权限的,这是需要预先在集群内创建 ClusterRoleBinding 授予其 system:node-bootstrapper
Role
1 |
|
根据上面描述的 master HA 架构,此时所有 node 应该连接本地的 nginx 代理,然后 nginx 来负载所有 api server;以下为 nginx 代理相关配置
1 |
|
为了保证 nginx 的可靠性,综合便捷性考虑,node 节点上的 nginx 使用 docker 启动,同时 使用 systemd 来守护, systemd 配置如下
1 |
|
最后启动 nginx,同时在每个 node 安装 kubectl,然后使用 kubectl 测试 api server 负载情况
1 |
|
启动成功后如下
kubectl 测试联通性如下
一起准备就绪以后就可以启动 node 相关组件了
1 |
|
由于采用了 TLS Bootstrapping,所以 kubelet 启动后不会立即加入集群,而是进行证书申请,从日志中可以看到如下输出
1 |
|
此时只需要在 master 允许其证书申请即可
1 |
|
最后再启动 kube-proxy 组件即可
1 |
|
Master 上部署 Node 与单独 Node 部署大致相同,只需要修改 bootstrap.kubeconfig
、kube-proxy.kubeconfig
中的 API Server 地址即可
然后修改 kubelet
、proxy
配置启动即可
1 |
|
最后在 master 签发一下相关证书
1 |
|
整体部署完成后如下
网路组件这里采用 Calico,Calico 目前部署也相对比较简单,只需要创建一下 yml 文件即可,具体可参考 Calico 官方文档
Cliaco 官方文档要求 kubelet 启动时要配置使用 cni 插件 --network-plugin=cni
,同时 kube-proxy 不能使用 --masquerade-all
启动(会与 Calico policy 冲突),所以需要修改所有 kubelet 和 proxy 配置文件,以下默认为这两项已经调整完毕,这里不做演示
1 |
|
执行部署操作,注意,在开启 RBAC 的情况下需要单独创建 ClusterRole 和 ClusterRoleBinding
1 |
|
部署完成后如下
最后测试一下跨主机通讯
1 |
|
exec 到一台主机 pod 内 ping 另一个不同 node 上的 pod 如下
DNS 部署目前有两种方式,一种是纯手动,另一种是使用 Addon-manager,目前个人感觉 Addon-manager 有点繁琐,所以以下采取纯手动部署 DNS 组件
DNS 组件相关文件位于 kubernetes addons 目录下,把相关文件下载下来然后稍作修改即可
1 |
|
接下来测试 DNS,测试方法创建两个 deployment 和 svc,通过在 pod 内通过 svc 域名方式访问另一个 deployment 下的 pod,相关测试的 deploy、svc 配置在这里不在展示,基本情况如下图所示
关于 DNS 自动扩容详细可参考 Autoscale the DNS Service in a Cluster,以下直接操作
首先获取 Dns horizontal autoscaler 配置文件
1 |
|
然后直接 kubectl create -f
即可,DNS 自动扩容计算公式为 replicas = max( ceil( cores * 1/coresPerReplica ) , ceil( nodes * 1/nodesPerReplica ) )
,如果想调整 DNS 数量(负载因子),只需要调整 ConfigMap 中对应参数即可,具体计算细节参考上面的官方文档
1 |
|
基于角色的访问控制使用 rbac.authorization.k8s.io
API 组来实现权限控制,RBAC 允许管理员通过 Kubernetes API 动态的配置权限策略。在 1.6 版本中 RBAC 还处于 Beat 阶段,如果想要开启 RBAC 授权模式需要在 apiserver 组件中指定 --authorization-mode=RBAC
选项。
本节介绍了 RBAC 的四个顶级类型,用户可以像与其他 Kubernetes API 资源一样通过 kubectl、API 调用方式与其交互;例如使用 kubectl create -f (resource).yml
命令创建资源对象,跟随本文档操作前最好先阅读引导部分。
在 RBAC API 中,Role 表示一组规则权限,权限只会增加(累加权限),不存在一个资源一开始就有很多权限而通过 RBAC 对其进行减少的操作;Role 可以定义在一个 namespace 中,如果想要跨 namespace 则可以创建 ClusterRole。
Role 只能用于授予对单个命名空间中的资源访问权限, 以下是一个对默认命名空间中 Pods 具有访问权限的样例:
1 |
|
ClusterRole 具有与 Role 相同的权限角色控制能力,不同的是 ClusterRole 是集群级别的,ClusterRole 可以用于:
/healthz
访问)以下是 ClusterRole 授权某个特定命名空间或全部命名空间(取决于绑定方式)访问 secrets 的样例
1 |
|
RoloBinding 可以将角色中定义的权限授予用户或用户组,RoleBinding 包含一组权限列表(subjects),权限列表中包含有不同形式的待授予权限资源类型(users, groups, or service accounts);RoloBinding 同样包含对被 Bind 的 Role 引用;RoleBinding 适用于某个命名空间内授权,而 ClusterRoleBinding 适用于集群范围内的授权。
RoleBinding 可以在同一命名空间中引用对应的 Role,以下 RoleBinding 样例将 default 命名空间的 pod-reader
Role 授予 jane 用户,此后 jane 用户在 default 命名空间中将具有 pod-reader
的权限
1 |
|
RoleBinding 同样可以引用 ClusterRole 来对当前 namespace 内用户、用户组或 ServiceAccount 进行授权,这种操作允许集群管理员在整个集群内定义一些通用的 ClusterRole,然后在不同的 namespace 中使用 RoleBinding 来引用
例如,以下 RoleBinding 引用了一个 ClusterRole,这个 ClusterRole 具有整个集群内对 secrets 的访问权限;但是其授权用户 dave
只能访问 development 空间中的 secrets(因为 RoleBinding 定义在 development 命名空间)
1 |
|
最后,使用 ClusterRoleBinding 可以对整个集群中的所有命名空间资源权限进行授权;以下 ClusterRoleBinding 样例展示了授权 manager 组内所有用户在全部命名空间中对 secrets 进行访问
1 |
|
Kubernetes 集群内一些资源一般以其名称字符串来表示,这些字符串一般会在 API 的 URL 地址中出现;同时某些资源也会包含子资源,例如 logs 资源就属于 pods 的子资源,API 中 URL 样例如下
1 |
|
如果要在 RBAC 授权模型中控制这些子资源的访问权限,可以通过 /
分隔符来实现,以下是一个定义 pods 资资源 logs 访问权限的 Role 定义样例
1 |
|
具体的资源引用可以通过 resourceNames
来定义,当指定 get
、delete
、update
、patch
四个动词时,可以控制对其目标资源的相应动作;以下为限制一个 subject 对名称为 my-configmap 的 configmap 只能具有 get
和 update
权限的样例
1 |
|
值得注意的是,当设定了 resourceNames 后,verbs 动词不能指定为 list
、watch
、create
和 deletecollection
;因为这个具体的资源名称不在上面四个动词限定的请求 URL 地址中匹配到,最终会因为 URL 地址不匹配导致 Role 无法创建成功
以下样例只给出了 role 部分
在核心 API 组中允许读取 pods
资源
1 |
|
在 extensions
和 apps
API 组中允许读取/写入 deployments
1 |
|
允许读取 pods
资源,允许读取/写入 jobs
资源
1 |
|
允许读取名称为 my-config
的 ConfigMap(需要与 RoleBinding 绑定来限制某个特定命名空间和指定名字的 ConfigMap)
1 |
|
允许在核心组中读取 nodes
资源( Node 是集群范围内的资源,需要使用 ClusterRole 并且与 ClusterRoleBinding 绑定才能进行限制)
1 |
|
允许对非资源型 endpoint /healthz
和其子路径 /healthz/*
进行 GET
和 POST
请求(同样需要使用 ClusterRole 和 ClusterRoleBinding 才能生效)
1 |
|
RoleBinding 和 ClusterRoleBinding 可以将 Role 绑定到 Subjects;Subjects 可以是 groups、users 或者 service accounts。
Subjects 中 Users 使用字符串表示,它可以是一个普通的名字字符串,如 “alice”;也可以是 email 格式的邮箱地址,如 bob@example.com
;甚至是一组字符串形式的数字 ID。Users 的格式必须满足集群管理员配置的验证模块,RBAC 授权系统中没有对其做任何格式限定;但是 Users 的前缀 system:
是系统保留的,集群管理员应该确保普通用户不会使用这个前缀格式
Kubernetes 的 Group 信息目前由 Authenticator 模块提供,Groups 书写格式与 Users 相同,都为一个字符串,并且没有特定的格式要求;同样 system:
前缀为系统保留
具有 system:serviceaccount:
前缀的用户名和 system:serviceaccounts:
前缀的组为 Service Accounts
以下示例仅展示 RoleBinding 的 subjects 部分
指定一个名字为 alice@example.com
的用户
1 |
|
指定一个名字为 frontend-admins
的组
1 |
|
指定 kube-system namespace 中默认的 Service Account
1 |
|
指定在 qa namespace 中全部的 Service Account
1 |
|
指定全部 namspace 中的全部 Service Account
1 |
|
指定全部的 authenticated 用户(1.5+)
1 |
|
指定全部的 unauthenticated 用户(1.5+)
1 |
|
指定全部用户
1 |
|
集群创建后 API Server 默认会创建一些 ClusterRole 和 ClusterRoleBinding 对象;这些对象以 system:
为前缀,这表明这些资源对象由集群基础设施拥有;修改这些集群基础设施拥有的对象可能导致集群不可用。 一个简单的例子是 system:node
ClusterRole,这个 ClusterRole 定义了 kubelet 的相关权限,如果该 ClusterRole 被修改可能导致 ClusterRole 不可用。
所有的默认 ClusterRole 和 RoleBinding 都具有 kubernetes.io/bootstrapping=rbac-defaults
lable
API Server 在每次启动后都会更新已经丢失的默认 ClusterRole 和 其绑定的相关 Subjects;这将允许集群自动修复因为意外更改导致的 RBAC 授权错误,同时能够使在升级集群后基础设施的 RBAC 授权得以自动更新。
如果想要关闭 API Server 的自动修复功能,只需要将默认创建的 ClusterRole 和其 RoleBind 的 rbac.authorization.kubernetes.io/autoupdate
注解设置为 false 即可,这样做会有很大风险导致集群因为意外修改 RBAC 而无法工作
Auto-reconciliation 在 1.6+ 版本被默认启用(当 RBAC 授权被激活时)
Default ClusterRole | Default ClusterRoleBinding | Description |
---|---|---|
system:basic-user | system:authenticated and system:unauthenticated groups | 允许用户以只读的方式读取其基础信息 |
system:discovery | system:authenticated and system:unauthenticated groups | 允许以只读的形式访问 发现和协商 API Level 所需的 API discovery endpoints |
一些默认的 Role 并未以 system:
前缀开头,这表明这些默认的 Role 是面向用户级别的。这其中包括超级用户的一些 Role( cluster-admin
),和为面向集群范围授权的 RoleBinding( cluster-status
),以及在特定命名空间中授权的 RoleBinding( admin
,edit
,view
)
Default ClusterRole | Default ClusterRoleBinding | Description |
---|---|---|
cluster-admin | system:masters group | 允许超级用户对集群内任意资源执行任何动作。当该 Role 绑定到 ClusterRoleBinding 时,将授予目标 subject 在任意 namespace 内对任何 resource 执行任何动作的权限;当绑定到 RoleBinding 时,将授予目标 subject 在当前 namespace 内对任意 resource 执行任何动作的权限,当然也包括 namespace 自己 |
admin | None | 管理员权限,用于在单个 namespace 内授权;在与某个 RoleBinding 绑定后提供在单个 namesapce 中对资源的读写权限,包括在单个 namesapce 内创建 Role 和进行 RoleBinding 的权限。该 ClusterRole 不允许对资源配额和 namespace 本身进行修改 |
edit | None | 允许读写指定 namespace 中的大多数资源对象;该 ClusterRole 不允许查看或修改 Role 和 RoleBinding |
view | None | 允许以只读方式访问特定 namespace 中的大多数资源对象;该 ClusterRole 不允许查看 Role 或 RoleBinding,同时不允许查看 secrets,因为他们会不断更新 |
Default ClusterRole | Default ClusterRoleBinding | Description |
---|---|---|
system:kube-scheduler | system:kube-scheduler user | 允许访问 kube-scheduler 所需资源 |
system:kube-controller-manager | system:kube-controller-manager user | 允许访问 kube-controller-manager 所需资源;该 ClusterRole 包含每个控制循环所需要的权限 |
system:node | system:nodes group (deprecated in 1.7) | 允许访问 kubelet 所需资源;包括对所有的 secrets 读访问权限和对所有 pod 的写权限;在 1.7 中更推荐使用 Node authorizer 和 NodeRestriction admission plugin 而非本 ClusterRole;Node authorizer 和 NodeRestriction admission plugin 可以授权当前 node 上运行的具体 pod 对 kubelet API 的访问权限,在 1.7 版本中,如果开启了 Node authorization mode ,那么 system:nodes group将不会被创建和自动绑定 |
system:node-proxier | system:kube-proxy user | 允许访问 kube-proxy 所需资源 |
Default ClusterRole | Default ClusterRoleBinding | Description |
---|---|---|
system:auth-delegator | None | 允许委托认证和授权检查;此情况下通常由附加的 API Server 来进行统一认证和授权 |
system:heapster | None | Heapster 组件相关权限 |
system:kube-aggregator | None | kube-aggregator 相关权限 |
system:kube-dns | kube-dns service account in the kube-system namespace | kube-dns 相关权限 |
system:node-bootstrapper | None | 允许访问 Kubelet TLS bootstrapping 相关资源权限 |
system:node-problem-detector | None | node-problem-detector 相关权限 |
system:persistent-volume-provisioner | Node | 允许访问 dynamic volume provisioners 相关资源权限 |
Kubernetes controller manager 运行着一些核心的 control loops
,当使用 --use-service-account-credentials
参数启动时,每个 control loop
都会使用独立的 Service Account
启动;相应的 roles 会以 system:controller
前缀存在于每个 control loop 中;如果不指定该选项,那么 Kubernetes controller manager 将会使用自己的凭据来运行所有 control loops
,此时必须保证 RBAC 授权模型中授予了其所有相关 Role,如下:
RBAC API 会通过阻止用户编辑 Role 或 RoleBinding 来进行特权升级,RBAC 在 API 级别实现了这一机制,所以即使 RBAC authorizer 不被使用也适用。
用户即使在对某个 Role 拥有全部权限的情况下也仅能在其作用范围内(ClusterRole -> 集群范围内,Role -> 当前 namespace 或 集群范围)对其进行 create 和 update 操作; 例如 “user-1” 用户不具有在集群范围内列出 secrets 的权限,那么他也无法在集群范围内创建具有该权限的 ClusterRole,也就是说想传递权限必须先获得该权限;想要允许用户 cretae/update Role 有两种方式:
用户只有拥有了一个 RoleBind 引用的 Role 全部权限,或者被显示授予了对其具有 bind 的权限下,才能在其作用范围(范围同上)内对其进行 create/update 操作; 例如 “user-1” 在不具有列出集群内 secrets 权限的情况下,也不可能为具有该权限的 Role 创建 ClusterRoleBinding;如果想要用户具有 create/update ClusterRoleBinding 的权限有以下两种方式:
以下样例中,ClusterRole 和 RoleBinding 将允许 “user-1” 用户具有授予其他用户在 “user-1-namespace” namespace 下具有 admin、edit 和 view roles 的权限
1 |
|
当使用 bootstrapping 时,初始用户尚没有访问 API 的权限,此时想要授予他们一些尚未拥有的权限是不可能的,此时可以有两种解决方案:
system:masters
组从而通过默认绑定绑定到 cluster-admin
超级用户,这样就可以直接沟通 API Server--insecure-port
端口,那么可以通过此端口调用完成第一次授权动作通过两个 kubectl
的子命令完成在特定命名空间或集群内的授权管理
在特定 namespae 中创建 Role 或者 ClusterRole 的 RoleBinding 样例
在 acme namespace 中授权用户 bob 具有 admin ClusterRole 的 RoleBinding
1 |
|
在 acme namespace 中授权名称为 acme:myapp 的 service account 具有 view ClusterRole 的 RoleBinding
1 |
|
在全部命名空间中创建 Role 或者 ClusterRole 的 ClusterRoleBinding 样例
在整个集群内授权 “root” 用户具有 cluster-admin ClusterRole 的 ClusterRoleBinding
1 |
|
在整个集群内授权 “kubelet” 用户具有 system:node ClusterRole 的 ClusterRoleBinding
1 |
|
在 “acme” 命名空间中授权名称为 acme:myapp 的 service account 具有 view ClusterRole 的 ClusterRoleBinding
1 |
|
更详细使用请参考命令行帮助文档
默认的 RBAC 权限策略仅向 control-plane 组件、nodes 和 controllers 进行授权,不包括 kube-system
namespace 以外的 Service Account 进行授权(除了向已经被验证过的用户授予的 discovery 权限之外)
这允许你根据需要向特定的服务账户授予特定的权限;细粒度的权限角色绑定控制会更加安全,但是需要更大的精力来进行权限管理;更加宽松的权限角色绑定控制也许会给一些用户分配其不需要的权限,但是相对来说管理相对更加宽松
从最安全到最不安全的权限管理如下:
**这种方式需要应用在 spec 中设置 serviceAccountName,同时这个 SserviceAccount 必须已经被创建(可以通过 API、manifest 文件或者 通过命令 kubectl create serviceaccount
等)**。例如在 “my-namespace” namespace 下授予 “my-sa” ServiceAccount view ClusterRole 如下:
1 |
|
如果应用程序在 spec 中没有设置 serviceAccountName,那么将会使用 “default” ServiceAccount。
注意: 如果对 default ServiceAccount 进行 RoleBinding(授权),那么在当前命名空间内所有没有指定 serviceAccountName 的 pod 都将获得该权限。 例如在 “my-namespace” namespace 下授予 “default” ServiceAccount view ClusterRole 如下:
1 |
|
目前大多数 add-ons 运行在 “kube-system” namespace 的 “default” ServiceAccount 下,如果想要 add-ons 使用超级用户的权限只需要对 “kube-system” namespace 下的 “default” ServiceAccount 授予超级用户权限即可,需要注意的是超级用户对 API secrets 具有读写权限,这将导致所有 add-ons 组件具有该权限
1 |
|
如果希望 namespace 中所有应用程序(无论属于哪个 ServiceAccount)都具有某一个 Role,则可以通过将该 Role 授予该 namespace 的 ServiceAccount 组来实现;例如授予 “my-namespace” namespace 下所有 ServiceAccount view ClusterRole 如下:
1 |
|
如果你懒得管理每个 namespace 的权限,那么可以将授权扩散到整个集群,将权限授予集群内每个 ServiceAccount;例如授予全部 namespace 中所有 ServiceAccount view ClusterRole:
1 |
|
如果你根本不关心权限分配,那么可以向集群内所有 namespace 下所有 ServiceAccount 授予超级用户权限;注意: 这将允许具有读取权限的用户创建一个容器从而间接读取到超级用户凭据
1 |
|
在 Kubernetes 1.6 版本之前,许多部署使用了非常宽泛的 ABAC 授权策略,包括授予对所有服务帐户的完整API访问权限;默认的 RBAC 权限策略仅向 control-plane 组件、nodes 和 controllers 进行授权,不包括 kube-system
namespace 以外的 Service Account 进行授权(除了向已经被验证过的用户授予的 discovery 权限之外)
这种方式虽然安全性更高,但是 RBAC 授权方式可能影响到已经存在的期望自动获得 API 权限的 workloads,以下有两种解决方案:
并行授权策略允许同时运行 RBAC 和 ABAC,并且包含旧的 ABAC 授权策略
1 |
|
此时 RBAC 授权控制器将首先处理授权,如果请求被拒绝则转交给 ABAC 授权控制器处理;这种授权方式将会允许 RBAC 和 ABAC 同时处理授权请求,只要目标 Subjects 在 RBAC 或 ABAC 中任意一个授权器授权成功即可
当日志级别设置为 2(–v=2) 或者更高时,可以在 API Server 日志中看到 RBAC 拒绝的日志(以 RBAC DENY:
开头),你可以通过日志中该信息来确定哪些 Role 应该授予哪些 Subjects。一旦完成所有的授权处理,并且在日志中没有再出现 RBAC 授权拒绝的日志时,就可以删除掉 ABAC 授权
您可以使用 RBAC RoleBinding 来复制一个允许的策略。
注意: 以下策略允许所有服务帐户充当集群管理员。在容器中运行的任何应用程序都会自动接收服务帐户凭据,并可以针对 API 执行任何操作,包括查看和修改 secrets 权限;所以这种方法并不推荐。
1 |
|
一直使用 Centos 运行 Kubernetes,有些时候基于二进制部署的情况下,手动复制二进制文件和创建 Systemd service 配置略显繁琐;最近找了一下 Kubernetes RPM 的 build 方式,以下记录一下 build 过程
目前我所知道的 build kubernetes RPM 的方式(测试过)总共 3 种,大致分为 2 类
第一种方案的好处就是配置文件等能始终保持最新的,编译版本等不受限制;但是从源码 build 非常耗时,尤其是网络环境复杂的情况下,没有高配置国外服务器很难完成 build,而且要维护 build 所需 spec 文件等,自己维护这些未必能够尽善尽美;
第二种方式是创建速度快,build 方式简单可靠,但是由于是替换方式,所以 rpm 中的配置不一定能够即使更新,而且只能基于官方build 好以后的二进制文件进行替换,如果想要尝试 master 最新代码则无法实现
对于 Centos RPM build 原理方式这里不再细说,基于源码 build 的关键就在于 spec 文件,我尝试过自己去写,后来对比一些开源项目的感觉 low 得很,所以以前一直采用一个国外哥们写的脚本 build(参见 这里);这个脚本不太好的地方是作者已经停止了维护;经过不懈努力,找到了 Fedora 系统的 rpm 仓库,鼓捣了一阵摸清了套路;以下主要以 Fedora 仓库为例进行 build
以下 Build 在一台 Do 8核心 16G VPS 上进行,由于众所周知的原因,国内 Build 很费劲,一般国外 VPS 都是按小时收费,有个 2 块钱就够了
由于 spec 文件中定义了依赖于 golang 这个包,所以如果不装的话会报错;事实上如果使用刚刚安装的这个 golang 去 build 还是会挂掉,因为实际编译要求 golang > 1.7,直接 yum 装的是 1.6,故下面又使用 gvm 装了一个 1.8 的 golang,上面的 golang 安装只是为了通过 spec 检查
1 |
|
Fedora 官方 Kubernetes 仓库地址在 这里,如果有版本选择请自行区分
1 |
|
克隆好 build 仓库后首先查看 kubernetes.spec 文件,确定 build 所需文件,spec 文件如下
1 |
|
从 spec 文件中可以看到 build 主要需要两个仓库的源码,一个是 kubernetes 主仓库,存放着主要的 build 源码;另一个是 contrib 仓库,存放着一些配置文件,如 systemd 配置等
接下来从 spec 文件的 source 段中可以解读到(source0、source1)最终所需的两个仓库压缩文件名为 kubernetes-SHORTCOMMIT
、contrib-SHORTCOMIT
,source 段如下
1 |
|
我们准备 build 一个最新的 1.7.0 的 rpm,所以从 github 获取到 commitID 为 d3ada0119e776222f11ec7945e6d860061339aad
,contrib 仓库同理,不过 contrib 一般直接取 master 即可 7d344989fe6a3f11a6d84104b024a50960b021db
;接下来首要任务是替换 spec 中原有的 版本号和 commitID 如下
1 |
|
修改好文件以后,就可以下载源码文件了,源码下载不必去克隆 github 项目,直接从 spec 中给出的地址下载即可
1 |
|
在正式开始 build 之前,还有一点需要注意的是 默认的 kubernetes.spec
文件中指定了该 rpm 依赖于 docker 这个包,在 CentOS 上可能我们会安装 docker-engine 或者 docker-ce,此时安装 kubernetes rpm 是无法安装的,因为他以来的包不存在,解决的办法就是编译之前删除 spec 文件中的 Requires: docker
即可,最后创建好 build 目录,并放置好源码文件开始 build 即可,当然 build 可以有不同选择
1 |
|
注意,由于我们选择的版本已经超出了仓库所支持的最大版本,所以有些 Patch 已经不再适用,如 spec 中的 Patch12
、Patch19
会出错,所需要注释掉(%prep 段中也有一个)
rpmbuild 可选项有很多,常用的 3 个,可以根据自己实际需要进行 build:
-ba
: build 源码包+二进制包-bb
: 只 build 二进制包-bs
: 只 build 源码包最后 build 完成后如下
]]>本文主要记录一下 Kubernetes 使用 Ceph 存储的相关配置过程,Kubernetes 集群环境采用的 kargo 部署方式,并且所有组件以容器化运行
Kubernetes 集群总共有 5 台,部署方式为 kargo 容器化部署,**采用 kargo 部署时确保配置中开启内核模块加载( kubelet_load_modules: true
)**;Kubernetes 版本为 1.6.4,Ceph 采用最新的稳定版 Jewel
节点 | IP | 部署 |
---|---|---|
docker1 | 192.168.1.11 | master、monitor、osd |
docker2 | 192.168.1.12 | master、monitor、osd |
docker3 | 192.168.1.13 | node、monitor、osd |
docker4 | 192.168.1.14 | node、osd |
docker5 | 192.168.1.15 | node、osd |
具体安装请参考 Ceph 笔记(一)、Ceph 笔记(二),以下直接上命令
1 |
|
1 |
|
传统的使用分布式存储的方案一般为 PV & PVC
方式,也就是说管理员预先创建好相关 PV 和 PVC,然后对应的 deployment 或者 replication 挂载 PVC 来使用
创建 Secret
1 |
|
创建 PV
1 |
|
创建 PVC
1 |
|
创建 Deployment并挂载
1 |
|
在 1.4 以后,kubernetes 提供了一种更加方便的动态创建 PV 的方式;也就是说使用 StoragaClass 时无需预先创建固定大小的 PV,等待使用者创建 PVC 来使用;而是直接创建 PVC 即可分配使用
创建系统级 Secret
注意: 由于 StorageClass 要求 Ceph 的 Secret type 必须为 kubernetes.io/rbd
,所以上一步创建的 ceph-secret
需要先被删除,然后使用如下命令重新创建;此时的 key 并没有经过 base64
1 |
|
创建 StorageClass
1 |
|
关于上面的 adminId 等字段具体含义请参考这里 Ceph RBD
创建 PVC
1 |
|
创建 Deployment
1 |
|
到此完成,检测是否成功最简单的方式就是看相关 pod 是否正常运行
]]>本篇文章主要简述了 Ceph 的存储对象名词解释及其含义,以及对 Ceph 集群内 CRUSH bucket 调整、PG/PGP 参数调整等设置;同时参考了一些书籍资料简单的概述一下 Ceph 集群硬件要求等
对象是 Ceph 中最小的存储单元,对象是一个数据和一个元数据绑定的整体;元数据中存放了具体数据的相关属性描述信息等;Ceph 为每个对象生成一个集群内唯一的对象标识符,以保证对象在集群内的唯一性;在传统文件系统的存储中,单个文件的大小是有一定限制的,而 Ceph 中对象随着其元数据区增大可以变得非常巨大
在传统的文件存储系统中,数据的元数据占据着极其重要的位置,每次系统中新增数据时,元数据首先被更新,然后才是实际的数据被写入;在较小的存储系统中(GB/TB),这种将元数据存储在某个固定的存储节点或者磁盘阵列中的做法还可以满足需求;当数据量增大到 PB/ZB 级别时,元数据查找性能将会成为一个很大的瓶颈;同时元数据的统一存放还可能造成单点故障,即当元数据丢失后,实际数据将无法被找回;与传统文件存储系统不同的是,Ceph 使用可扩展散列下的受控复制(Controlled Replication Under Scalable Hashing,CRUSH)算法来精确地计算数据应该被写入哪里/从哪里读取;CRUSH按需计算元数据,而不是存储元数据,从而解决了传统文件存储系统的瓶颈
在 Ceph 中,元数据的计算和负载是分布式的,并且只有在需要时才会执行;元数据的计算过程称之为 CRUSH 查找,不同于其他分布式文件系统,Ceph 的 CRUSH 查找是由客户端使用自己的资源来完成的,从而去除了中心查找带来的性能以及单点故障问题;CRUSH 查找时,客户端先通过 monitor 获取集群 map 副本,然后从 map 副本中获取集群配置信息;然后通过对象信息、池ID等生成对象;接着通过对象和 PG 数散列后得到 Ceph 池中最终存放该对象的 PG;最终在通过 CRUSH 算法确定该 PG 所需存储的 OSD 位置,一旦确定了 OSD 位置,那么客户端将直接与 OSD 通讯完成数据读取与写入,这直接去除了中间环节,保证了性能的极大提升
在 Ceph 中,CRUSH 是完全支持各种基础设施和用户自定义的;CRUSH 设备列表中预先定义了一系列的设备,包括磁盘、节点、机架、行、开关、电源电路、房间、数据中心等等;这些组件称之为故障区(CRUSH bucket),用户可以通过自己的配置把不同的 OSD 分布在不同区域;此后 Ceph 存储数据时根据 CRUSH bucket 结构,将会保证每份数据都会在所定义的物理组件之间完全隔离;比如我们定义了多个机架上的不同 OSD,那么 Ceph 存储时就会智能的将数据副本分散到多个机架之上,防止某个机架上机器全部跪了以后数据全部丢失的情况
当故障区内任何组件出现故障时,Ceph 都会将其标记为 down 和 out 状态;然后默认情况下 Ceph 会等待 300秒之后进行数据恢复和再平衡,这个值可以通过在配置文件中的 mon osd down out interval
参数来调整
PG 是一组对象集合体,根据 Ceph 的复制级别,每个PG 中的数据会被复制到多个 OSD 上,以保证其高可用状态
Ceph 池是一个存储对象的逻辑分区,每一个池中都包含若干个 PG,进而实现将一定对象映射到集群内不同 OSD 中,池可以以复制方式或者纠错码方式创建,但不可同时使用这两种方式
1 |
|
1 |
|
计算 PG 数为 Ceph 企业级存储必不可少的的一部分,其中集群内 PG 计算公式如下
1 |
|
对于单个池来讲,我们还应该为池设定 PG 数,其中池的 PG 数计算公式如下
1 |
|
PGP 是为了实现定位而设计的 PG,PGP 的值应该与 PG 数量保持一致;当池的 pg_num 增加的时候,池内所有 PG 都会一分为二,但是他们仍然保持着以前 OSD 的映射关系;当增加 pgp_num 的时候,Ceph 集群才会将 PG 进行 OSD 迁移,然后开始再平衡过程
获取现有 PG 和 PGP 值可以通过如下命令
1 |
|
当计算好 PG 和 PGP 以后可以通过以下命令设置
1 |
|
同样在创建 pool 的时候也可以同步指定
1 |
|
默认情况,当创建一个新的 pool 时,向 pool 内存储的数据只会有 2 个副本,查看 pool 副本数可以通过如下命令
1 |
|
当我们需要修改默认副本数以使其满足高可靠性需求时,可以通过如下命令完成
1 |
|
上面已经讲述了 CRUSH bucket 的概念,通过以下相关命令,我们可以定制自己的集群布局,以使 Ceph 完成数据的容灾处理
1 |
|
最终集群整体布局如下
1 |
|
硬件规划一般是一个企业级存储的必要工作,以下简述了 Ceph 的一般硬件需求
Ceph monitor 通过维护整个集群的 map 从而完成集群的健康处理;但是 monitor 并不参与实际的数据存储,所以实际上 monitor 节点 CPU 占用、内存占用都比较少;一般单核 CPU 加几个 G 的内存即可满足需求;虽然 monitor 节点不参与实际存储工作,但是 monitor 的网卡至少应该是冗余的,因为一旦网络出现故障则集群健康会难以保证
OSD 作为 Ceph 集群的主要存储设施,其会占用一定的 CPU 和内存资源;一般推荐做法是每个节点的每块硬盘作为一个 OSD;同时 OSD 还需要写入日志,所以应当为 OSD 集成日志留有充足的空间;在出现故障时,OSD 需求的资源可能会更多,所以 OSD 节点根据实际情况(每个 OSD 会有一个线程)应该分配更多的 CPU 和内存;固态硬盘也会增加 OSD 存取速度和恢复速度
MDS 服务专门为 CephFS 存储元数据,所以相对于 monitor 和 OSD 节点,这个 MDS 节点的 CPU 需求会大得多,同时内存占用也是海量的,所以 MDS 一般会使用一个强劲的物理机单独搭建
]]>Ceph 是一个符合POSIX、开源的分布式存储系统;其具备了极好的可靠性、统一性、鲁棒性;经过近几年的发展,Ceph 开辟了一个全新的数据存储途径。Ceph 具备了企业级存储的分布式、可大规模扩展、没有单点故障等特点,越来越受到人们青睐;以下记录了 Ceph 的相关学习笔记。
本文以 Centos 7 3.10 内核为基础环境,节点为 4 台 Vagrant 虚拟机;Ceph 版本为 Jewel.
首先需要一台部署节点,这里使用的是宿主机;在部署节点上需要安装一些部署工具,如下
1 |
|
同时,ceph-deploy 工具需要使用 ssh 来自动化部署 Ceph 各个组件,因此需要保证部署节点能够免密码登录待部署节点;最后,待部署节点最好加入到部署节点的 hosts 中,方便使用域名(某些地方强制)连接管理
由于 Ceph 采用 Paxos 算法保证数据一致性,所以安装前需要先保证各个节点时钟同步
1 |
|
ceph-deploy 工具部署集群前需要创建一些集群配置信息,其保存在 ceph.conf
文件中,这个文件未来将会被复制到每个节点的 /etc/ceph/ceph.conf
1 |
|
创建集群使用 ceph-deploy 工具即可
1 |
|
执行 ceph health
命令后应当返回 HEALTH_OK
;如出现 HEALTH_WARN clock skew detected on mon.docker2; Monitor clock skew detected
,说明时钟不同步,手动同步时钟稍等片刻后即可;其他错误可以通过如下命令重置集群重新部署
1 |
|
更多细节,如防火墙、SELinux配置等请参考 官方文档
1 |
|
以下图片(摘自网络)展示了基本的 Ceph 架构
下图(摘自网络)从应用角度描述了 Ceph 架构
此处直接上代码
1 |
|
官方文档中提示,使用 rdb 的客户端不建议与 OSD 等节点在同一台机器上
You may use a virtual machine for your ceph-client node, but do not execute the following procedures on the same physical node as your Ceph Storage Cluster nodes (unless you use a VM). See FAQ for details.
这里从第四台虚拟机上执行操作,首先安装所需客户端工具
1 |
|
然后创建块设备
1 |
|
在上面的 map 映射操作时,可能出现如下错误
1 |
|
查看系统日志可以看到如下输出
1 |
|
问题原因: 在 Ceph 高版本进行 map image 时,默认 Ceph 在创建 image(上文 data)时会增加许多 features,这些 features 需要内核支持,在 Centos7 的内核上支持有限,所以需要手动关掉一些 features
首先使用 rbd info data
命令列出创建的 image 的 features
1 |
|
在 features 中我们可以看到默认开启了很多:
而实际上 Centos 7 的 3.10 内核只支持 layering… 所以我们要手动关闭一些 features,然后重新 map;如果想要一劳永逸,可以在 ceph.conf 中加入 rbd_default_features = 1
来设置默认 features(数值仅是 layering 对应的 bit 码所对应的整数值)。
1 |
|
最后我们便可以格式化正常挂载这个设备了
1 |
|
在测试 CephFS 之前需要先创建两个存储池和一个 fs,创建存储池要指定 PG 数量
1 |
|
PG 概念:
当 Ceph 集群接收到存储请求时,Ceph 会将其分散到各个 PG 中,PG 是一组对象的逻辑集合;根据 Ceph 存储池的复制级别,每个 PG的数据会被复制并分发到集群的多个 OSD 上;一般来说增加 PG 数量能降低 OSD 负载,一般每个 OSD 大约分配 50 ~ 100 PG,关于 PG 数量一般遵循以下公式
注意,PG 最终结果应当为最接近以上计算公式的 2 的 N 次幂(向上取值);如我的虚拟机环境每个存储池 PG 数 = 3(OSD) * 100 / 3(副本) / 5(大约 5 个存储池) = 20
,向上取 2 的 N 次幂 为 32
挂载 CephFS 一般有两种方式,一种是使用内核驱动挂载,一种是使用 ceph-fuse
用户空间挂载;内核方式挂载需要提取 Ceph 管理 key,如下
1 |
|
使用 ceph-fuse 用户空间挂载方式比较简单,但需要先安装 ceph-fuse
软件包
1 |
|
对象网关在 1.5、其他组件创建 部分已经做了创建(RGW),此时直接访问 http://ceph-node-ip:7480
返回如下
1 |
|
这就说明网关已经 ok,由于手里没有能读写测试工具,这里暂不做过多说明
本文主要参考 Ceph 官方文档 Quick Start 部分,如有其它未说明到的细节可从官方文档获取
]]>工作需要临时启动一个 gitlab,无奈 gitlab 需要 ssh 的 22 端口;而使用传统网桥方式映射端口则 clone 等都需要输入端口号,很麻烦;22 端口宿主机又有 sshd 监听;研究了下 docker 网络,记录一下如何分配宿主机网段 IP
关于 Docker 网络模式这里不再细说;由于默认的网桥方式无法满足需要,所以需要创建一个 macvlan 网络
1 |
|
--subnet
: 指定网段(宿主机)--gateway
: 指定网关(宿主机)parent
: 注定父网卡(宿主机)创建以后可以使用 docker network ls
查看
1 |
|
接下来创建容器指定网络即可
1 |
|
--net
指定使用的网络,--ip
用于指定网段内 IP;启动后只需要在容器内启动程序测试即可
1 |
|
启动后在局域网内能直接通过 IP:80 访问,而且宿主机 80 不受影响
docker-compose 示例如下
1 |
|
今天对接接口,对方给的 Demo 和已有项目用的 HTTP 工具不是一个;后来出现人家的好使,我的死活不通的情况;无奈之下开始研究 Java 抓包,所以怕忘了记录一下……
mitmproxy 是一个命令行下的强大抓包工具,可以在命令行下抓取 HTTP(S) 数据包并加以分析;对于 HTTPS 抓包,首先要在本地添加 mitmproxy 的根证书,然后 mitmproxy 通过以下方式进行抓包:
mitmproxy 是由 python 编写的,所以直接通过 pip 即可安装,mac 下也可使用 brew 工具安装
1 |
|
首先由于 HTTPS 的安全性,直接抓包是什么也看不到的;所以需要先在本地配置 mitmproxy 的根证书,使其能够解密 HTTPS 流量完成一个中间人的角色;证书下载方式需要先在本地启动 mitmproxy,然后通过设置本地连接代理到 mitmproxy 端口,访问 mitm.it
即可,具体可查看 官方文档
首先启动 mitmproxy
1 |
|
浏览器通过设置代理访问 mitm.it
选择对应平台并将其证书加入到系统信任根证书列表即可;对于 Java 程序来说可能有时候并不会生效,所以必须 修改 keystore,修改如下
1 |
|
JVM 本身在启动时就可以设置代理参数,也可以通过代码层设置;以下为代码层设置代理方式
1 |
|
然后保证在发送 HTTPS 请求之前此代码执行即可,以下为抓包示例
通过方向键+回车即可选择某个请求查看报文信息
Java 代理一般可以通过 2 种方式设置,一种是通过代码层,如下
1 |
|
另一种还可以通过 JVM 启动参数设置
1 |
|
本文参考:
]]>最近自用的 vim 装了不少插件,但是发现 kubectl edit
或者 git merge
时,调用 vim 总是会弹出各种错误,记录一下解决方法
出现这个错误一开始以为是 vim 没走 .vimrc
配置;后来翻了一堆资料,发现 kubectl edit
或者 git merge
后并非直接调用 vim,而是调用的 /usr/bin/view
,那么看一下这个文件
这东西就是链接到了 vi,只要把它链接到 vim 就完了
]]>btrfs 是 Oracle 07 年基于 GPL 协议开源的 Linux 文件系统,其目的是替换传统的 Ext3、Ext4 系列文件系统;Ext 系列文件系统存在着诸多问题,比如反删除能力有限等;而 btrfs 在解决问题同时提供了更加强大的高级特性
btrfs 在文件系统级别支持写时复制(cow)机制,并且支持快照(增量快照)、支持对单个文件快照;同时支持单个超大文件、文件检查、内建 RAID;支持 B 树子卷(组合多个物理卷,多卷支持)等,具体如下
btrfs 核心特性:
同传统的 ext 系列文件系统一样,btrfs 文件系统格式化同样采用 mkfs
系列命令 mkfs.btrfs
,其常用选项如下:
-L
指定卷标-m
指明元数据存放机制(RAID)-d
指明数据存放机制(RAID)-O
格式化时指定文件系统开启那些特性(不一定所有内核支持),如果需要查看支持那些特性可使用 mkfs.btrfs -O list-all
同 ext 系列一样,仍然使用 mount
命令,基本挂载如下:
1 |
|
在挂载时也可以直接开启文件系统一些特性,如透明压缩
1 |
|
同时 btrfs 支持子卷,也可以单独挂载子卷
1 |
|
管理 btrfs 使用 btrfs
命令,该命令包含诸多子命令已完成不同的功能管理,常用命令如下
btrfs filesystem show
btrfs filesystem resize +10g MOUNT_POINT
btrfs filesystem add DEVICE MOUNT_POINT
btrfs blance status|start|pause|resume|cancel MOUNT_POINT
btrfs device delete DEVICE MOUNT_POINT
btrfs balance start -dconvert=RAID MOUNT_POINT
btrfs balance start -mconvert=RAID MOUNT_POINT
btrfs balance start -sconvert=RAID MOUNT_POINT
btrfs subvolume create MOUNT_POINT/DIR
btrfs subvolume list MOUNT_POINT
btrfs subvolume show MOUNT_POINT
btrfs subvolume delete MOUNT_POIN/DIR
btrfs subvolume snapshot SUBVOL PARVOL
btrfs subvolume delete MOUNT_POIN/DIR
最近决定把小主机扔到客厅跟路由器放在一起(远程开机 666),因为本来就跑的是 Linux,平时图形化需求也不多;但是为了保险起见准备搞一个 VNC,以便必要时图形化上去,比如强制删除一些 Virtual Box 虚拟机等,记录一下安装过程
VNC Server 软件有很多,这里使用 tigervnc-server
1 |
|
这地方踩了很多坑,网上不少帖子都是写的先测试 VNC Server,执行 vncserver
命令,然后云云;查设置开机自启动也是五花八门,大部分人的路子就是自己写一个 init 脚本,让系统开机时候执行它…… 从职业踩坑经验来看,这东西绝对有更有逼格的 Systemd 的打开方式;果不其然翻了一下 rpm 包 发现了一个 Systemd 模板 Service
1 |
|
由于好奇心作祟,先 systemctl enable vncserver@:1.service
了一下,后来发现起不来,所以 vim 看了一下原模板 Service,里面想写描述了如何设置开机启动
1 |
|
也就是说把这个模板文件 cp 到 /etc/systemd/system/vncserver@.service
然后替换用户名,执行两条命令,最后执行 vncserver
用于初始化密码和配置文件就行了
重装了一天系统,有点烦躁,听首歌安静一下…
]]>上一篇写了一下一下使用 kargo 快速部署 Kubernetes 高可用集群,但是一些细节部分不算完善,这里准备补一下,详细说明一下一些问题;比如后期如何扩展、一些配置如何自定义等
如果已经有了一个 kargo 搭建的集群,那么扩展其极其容易;只需要修改集群 inventory
配置,加入新节点重新运行命令价格参数即可,如下新增一个 node6 节点
1 |
|
然后重新运行集群命令,注意增加 --limit
参数
1 |
|
稍等片刻 node6 节点便加入现有集群,如果有多个节点加入,只需要以逗号分隔即可,如 --limit node5,node6
;在此过程中只会操作新增的 node 节点,不会影响现有集群,可以实现动态集群扩容(master 也可以扩展)
对于 kargo 高度自动化的工具,可能有些东西我们已经预先处理好了,比如事先已经安装了 docker,而且 docker 配置了一些参数(日志驱动、存储驱动等);这时候我们可能并不希望 kargo 再去处理,因为 kargo 会进行覆盖,可能导致一些问题
kargo 是基于 ansible 的,实际上也就是 ansible,只不过它帮我们写好了配置文件而已;按照 ansible 的规则,Play Book 首先执行 roles 目录下的 roles,在这些 roles 中定义了如何配置集群、如何初始化网络、怎么配置 docker 等等,所以只要我们去更改这些 roles 规则就可以实现一些功能的定制,roles 目录位置如下
如果需要更改某些默认配置,那么只需要更改对应目录下的 role 即可,每个 role 子目录都是一个组件的配置过程(动作),动作实际上就是不同的 task,所有的 task 定义在 tasks/main.yml
中,如果我们注释(删掉)了相关 task,那么也就关闭了 kargo 对应的处理;如下禁用了 kargo 安装 docker,但是允许 kargo 覆盖 docker service 文件
禁用掉 docker 仓库以及 docker 的安装动作
1 |
|
kargo 在进行各种任务(task)时可能会释放一些配置文件,比如 docker service 配置文件、kubernetes 配置文件等;这些文件一般位于 roles/组件/templates
目录,比如 docker 的 service 配置位于如下位置;我们可以更改,甚至直接换一个,把里面写死变成我们自己的
以上只是介绍了自定义配置的大体思路,更深度的处理需要去玩转 ansible,如果玩明白了 ansible 那么基本上这个 kargo 就可以随便搞了;要写的差不多也就这么多了,感觉这东西比 kubeadm 要好的多,所有操作都是可视化的,没有莫名其妙的问题;其他的可以参考 ansible 中文文档、kargo 官方文档
]]>最近发现好多人问 Ingress,同时一直也没去用 Nginx 的 Ingress,索性鼓捣了一把,发现跟原来确实有了点变化,在这里写篇文章记录一下
Kubernetes 暴露服务的方式目前只有三种:LoadBlancer Service、NodePort Service、Ingress;前两种估计都应该很熟悉,具体的可以参考下 这篇文章;下面详细的唠一下这个 Ingress
可能从大致印象上 Ingress 就是能利用 Nginx、Haproxy 啥的负载均衡器暴露集群内服务的工具;那么问题来了,集群内服务想要暴露出去面临着几个问题:
众所周知 Kubernetes 具有强大的副本控制能力,能保证在任意副本(Pod)挂掉时自动从其他机器启动一个新的,还可以动态扩容等,总之一句话,这个 Pod 可能在任何时刻出现在任何节点上,也可能在任何时刻死在任何节点上;那么自然随着 Pod 的创建和销毁,Pod IP 肯定会动态变化;那么如何把这个动态的 Pod IP 暴露出去?这里借助于 Kubernetes 的 Service 机制,Service 可以以标签的形式选定一组带有指定标签的 Pod,并监控和自动负载他们的 Pod IP,那么我们向外暴露只暴露 Service IP 就行了;这就是 NodePort 模式:即在每个节点上开起一个端口,然后转发到内部 Pod IP 上,如下图所示
采用 NodePort 方式暴露服务面临一个坑爹的问题是,服务一旦多起来,NodePort 在每个节点上开启的端口会及其庞大,而且难以维护;这时候引出的思考问题是 “能不能使用 Nginx 啥的只监听一个端口,比如 80,然后按照域名向后转发?” 这思路很好,简单的实现就是使用 DaemonSet 在每个 node 上监听 80,然后写好规则,因为 Nginx 外面绑定了宿主机 80 端口(就像 NodePort),本身又在集群内,那么向后直接转发到相应 Service IP 就行了,如下图所示
从上面的思路,采用 Nginx 似乎已经解决了问题,但是其实这里面有一个很大缺陷:每次有新服务加入怎么改 Nginx 配置?总不能手动改或者来个 Rolling Update 前端 Nginx Pod 吧?这时候 “伟大而又正直勇敢的” Ingress 登场,如果不算上面的 Nginx,Ingress 只有两大组件:Ingress Controller 和 Ingress
Ingress 这个玩意,简单的理解就是 你原来要改 Nginx 配置,然后配置各种域名对应哪个 Service,现在把这个动作抽象出来,变成一个 Ingress 对象,你可以用 yml 创建,每次不要去改 Nginx 了,直接改 yml 然后创建/更新就行了;那么问题来了:”Nginx 咋整?”
Ingress Controller 这东西就是解决 “Nginx 咋整” 的;Ingress Controoler 通过与 Kubernetes API 交互,动态的去感知集群中 Ingress 规则变化,然后读取他,按照他自己模板生成一段 Nginx 配置,再写到 Nginx Pod 里,最后 reload 一下,工作流程如下图
当然在实际应用中,最新版本 Kubernetes 已经将 Nginx 与 Ingress Controller 合并为一个组件,所以 Nginx 无需单独部署,只需要部署 Ingress Controller 即可
上面啰嗦了那么多,只是为了讲明白 Ingress 的各种理论概念,下面实际部署很简单
我们知道 前端的 Nginx 最终要负载到后端 service 上,那么如果访问不存在的域名咋整?官方给出的建议是部署一个 默认后端,对于未知请求全部负载到这个默认后端上;这个后端啥也不干,就是返回 404,部署如下
1 |
|
这个 default-backend.yaml
文件可以在 官方 Ingress 仓库 找到,由于篇幅限制这里不贴了,仓库位置如下
部署完了后端就得把最重要的组件 Nginx+Ingres Controller(官方统一称为 Ingress Controller) 部署上
1 |
|
**注意:官方的 Ingress Controller 有个坑,至少我看了 DaemonSet 方式部署的有这个问题:没有绑定到宿主机 80 端口,也就是说前端 Nginx 没有监听宿主机 80 端口(这还玩个卵啊);所以需要把配置搞下来自己加一下 hostNetwork
**,截图如下
同样配置文件自己找一下,地址 点这里,仓库截图如下
当然它支持以 deamonset 的方式部署,这里用的就是(个人喜欢而已),所以你发现我上面截图是 deployment,但是链接给的却是 daemonset,因为我截图截错了…..
这个可就厉害了,这个部署完就能装逼了
咳咳,回到正题,从上面可以知道 Ingress 就是个规则,指定哪个域名转发到哪个 Service,所以说首先我们得有个 Service,当然 Service 去哪找这里就不管了;这里默认为已经有了两个可用的 Service,以下以 Dashboard 和 kibana 为例
先写一个 Ingress 文件,语法格式啥的请参考 官方文档,由于我的 Dashboard 和 Kibana 都在 kube-system 这个命名空间,所以要指定 namespace,写之前 Service 分布如下
1 |
|
装逼成功截图如下
上面已经搞定了 Ingress,下面就顺便把 TLS 怼上;官方给出的样例很简单,大致步骤就两步:创建一个含有证书的 secret、在 Ingress 开启证书;但是我不得不喷一下,文档就提那么一嘴,大坑一堆,比如多域名配置,还有下面这文档特么的是逗我玩呢?
首先第一步当然要有个证书,由于我这个 Ingress 有两个服务域名,所以证书要支持两个域名;生成证书命令如下:
1 |
|
创建好证书以后,需要将证书内容放到 secret 中,secret 中全部内容需要 base64 编码,然后注意去掉换行符(变成一行);以下是我的 secret 样例(上一步中 ingress.pem 是证书,ingress-key.pem 是证书的 key)
1 |
|
创建完成后 create 一下就可
1 |
|
其实这个配置比如证书转码啥的没必要手动去做,可以直接使用下面的命令创建,这里写这么多只是为了把步骤写清晰
1 |
|
生成完成后需要在 Ingress 中开启 TLS,Ingress 修改后如下
1 |
|
注意:一个 Ingress 只能使用一个 secret(secretName 段只能有一个),也就是说只能用一个证书,更直白的说就是如果你在一个 Ingress 中配置了多个域名,那么使用 TLS 的话必须保证证书支持该 Ingress 下所有域名;并且这个 secretName
一定要放在上面域名列表最后位置,否则会报错 did not find expected key
无法创建;同时上面的 hosts
段下域名必须跟下面的 rules
中完全匹配
更需要注意一点:之所以这里单独开一段就是因为有大坑;Kubernetes Ingress 默认情况下,当你不配置证书时,会默认给你一个 TLS 证书的,也就是说你 Ingress 中配置错了,比如写了2个 secretName
、或者 hosts
段中缺了某个域名,那么对于写了多个 secretName
的情况,所有域名全会走默认证书;对于 hosts
缺了某个域名的情况,缺失的域名将会走默认证书,部署时一定要验证一下证书,不能 “有了就行”;更新 Ingress 证书可能需要等一段时间才会生效
最后重新部署一下即可
1 |
|
注意:部署 TLS 后 80 端口会自动重定向到 443,最终访问截图如下
历时 5 个小时鼓捣,到此结束
]]>鼓捣 kubernetes 好长一段时间了,kubernetes 1.5 后新增了 kubeadm 工具用于快速部署 kubernetes 集群,不过该工具尚未稳定,无法自动部署高可用集群,而且还存在一些 BUG,所以生产环境还无法使用;本文基于 kargo 工具实现一键部署 kubernetes 容器化(可选) 高可用集群
本文基本环境如下:
五台虚拟机,基于 vagrant 启动(穷),vagrant 配置文件参考 这里
主机地址 | 节点角色 |
---|---|
192.168.1.11 | master |
192.168.1.12 | master |
192.168.1.13 | node |
192.168.1.14 | node |
192.168.1.15 | node |
同时保证部署机器对集群内节点拥有 root 免密登录权限,由于墙的原因,部署所需镜像已经全部打包到百度云,点击 这里 下载,然后进行 load 即可;注意: 直接使用我的 vagrant
文件时,请删除我在 init.sh
脚本里对 docker 设置的本地代理,直接使用可能导致 docker 无法 pull 任何镜像;vagrant 可能需要执行 vagrant plugin install vagrant-hosts
安装插件以支持自动设置 host;如果自己采用其他虚拟机请保证单台虚拟机最低 1.5G 内存,否则会导致安装失败,别问我怎么知道的
最新更新:经过测试,请使用 pip 安装 ansible,保证 ansible >= 2.2.1
&& jinja2 >= 2.8,< 2.9
;否则可能出现安装时校验失败问题
kargo 是基于 ansible 的 Playbooks 的,其官方推荐的 kargo-cli 目前只适用于各种云平台部署安装,所以我们需要手动使用 Playbooks 部署,当然第一步先把源码搞下来
1 |
|
既然 kargo 是基于 ansible 的(实际上就是 Playbooks),那么自然要先安装 ansible,同时下面配置生成会用到 python3,所以也一并安装
1 |
|
注意:以下配置段中,所有双大括号 { {
、} }
,中间全部加了空格,因为双大括号会跟主题模板引擎产生冲突,默认应该是没有的,请自行 vim 替换
首先根据自己需要更改 kargo 的配置,配置文件位于 inventory/group_vars/k8s-cluster.yml
,最新稳定版本版本(2.1.0) 配置文件还未更名,全部在 inventory/group_vars/all.yml
中,这里采用最新版本的原因是…借着写博客我也看看更新了啥(偷笑…)
1 |
|
配置完基本集群参数后,还需要生成一个集群配置文件,用于指定需要在哪几台服务器安装,和指定 master、node 节点分布,以及 etcd 集群等安装在那几台机器上
1 |
|
生成的配置如下,已经很简单了,怎么改动相信猜也能猜到
1 |
|
首先启动 vagrant 虚拟机,不过注意的是本文提供的 vagrant 文件默认安装了 docker,并配置了 devicemapper 和docker 代理,所以使用时上面的 docker 参数需要替换成自己的,因为默认 kargo 会覆盖 docker 的 service 文件;会导致我已经配置完的 docker devicemapper 参数失效,所以要把自己配置的参数加到配置文件中,如下
1 |
|
这个 vagrant 配置文件自动设置了主机名、host、ssh 密钥,实际生产环境仍需自己处理
每个虚拟机需要自己登陆并生成 ssh-key(ssh-keygen),因为 ansible 会用到
1 |
|
部署成功后截图如下
相关 pod 启动情况如下
最后附上我部署是的 kargo 配置
1 |
|
Vagrant 是一个开源的 基于 ruby 的开源虚拟机管理工具;最近在鼓捣 kubernetes ,常常需要做集群部署测试,由于比较穷 😂😂😂;所以日常测试全部是自己开虚拟机;每次使用 VirtualBox开5个虚拟机很烦,而且为了保证环境干净不受其他因素影响,所以每次测试都是新开…..每次都会有种 WTF 的感觉,所以研究了一下 Vagrant 这个工具,发现很好用,一下记录一下简单的使用
上面已经简单的说了一下 Vagrant,Vagrant 定位为一个虚拟机管理工具;它能够以脚本化的方式启动、停止、和和删除虚拟机,当然这些手动也没费劲;更重要的是它能够自己定义网络分配、初始化执行的脚本、添加硬盘等各种复杂的动作;最重要的是 Vagrant 提供了类似于 docker image 的 box;Vagrant Box 就是一个完整的虚拟机分发包,可以自己制作也可以从网络下载;并且 Vagrant 开源特性使得各路大神开发了很多 Vagrant 插件方便我们使用,基于以上这些特点,我们可以实现:
Vagrant 安装极其简单,目前官方已经打包好了各个平台的安装包文件,地址访问 Vagrant 官方下载地址;截图如下
以下为 CentOS 上的安装命令
1 |
|
装虚拟机大家都不陌生,首先应该搞个系统镜像;同样 Vagrant 也需要先搞一个 Vagrant Box,Vagrant Box 是一个已经预装好操作系统的虚拟机打包文件;根据不同系统可以选择不同的 Vagrant Box,官方维护了一个 Vagrant Box 仓库,地址 点这里,截图如下
点击对应的系统后可以看到如下界面
该页面罗列出了使用不同虚拟机时应当使用扥添加明令;当然执行这些命令后 vagrant 将会从网络下载这个 box 文件并添加到本地 box 仓库;不过众所周知的原因,这个下载速度会让你怀疑人生,所有简单的办法是执行以下这条命令,然后会显示 box 的实际下载地址;拿到地址以后用梯子先把文件下载下来,然后使用 vagrant 导入也可以(centos7 本地已经有了一下以 ubuntu 为例)
下载后使用 vagrant box add xxxx.box
即可将 box 导入到本地仓库
万事俱备只差东风,在上一步执行 vagrant init ubuntu/trusty64; vagrant up --provider virtualbox
命令获取 box 下载地址时,已经在当前目录下生成了一个 Vagrantfile 文件,这个文件其实就是虚拟机配置文件,具体下面再说;box 导入以后先启动一下再说,执行 vagrnat up
即可
其他几个常用命令如下
vagrant box [list|add|remove]
查看添加删除 box 等vagrant up
启动虚拟机vagrant halt
关闭虚拟机vagrant init
初始化一个指定系统的 Vagrantfile 文件vagrant destroy
删除虚拟机vagrant ssh
ssh 到虚拟机里特别说明一下 ssh 这个命令,一般默认的规范是 vagrant ssh VM_NAME
后,会以 vagrant 用户身份登录到目标虚拟机,如果当前目录的 Vagrantfile 中只有一个虚拟机那么无需指定虚拟机名称(init 后默认就是);虚拟机内(box 封装时)vagrant这个用户拥有全局免密码 sudo 权限;root 用户一般密码为 vagrant
我发现基本国内所有的 Vagrant 教程都是简单的提了一嘴那几个常用命令;包括我上面也写了点,估计可能到这已经被喷了(“妈的那几个命令老子 help 一下就出来了,一看一猜就知道啥意思 用得着你讲?”);个人觉得 Vagrant 最复杂的是这个配置文件,以下直接上一个目前仓库里的做示例,仓库地址 戳这里
直接贴 Vagrantfile,以下配置在进行 vagrant up
之前可能需要使用 vagrant plugin install vagrant-host
插件,以支持自动在各节点之间添加 host
1 |
|
以上基本都加了注释,所以大致应该很清晰,至于第一行那个 Vagrant.configure("2")
代表调用第二版 API,不能改动,其他的可参考注释同时综合仓库中的其他配置文件即可
Vagrantfile 实质上就是一个 ruby 文件,可以自己在里面定义变量等,可以在里面按照 ruby 的语法进行各种复杂的操作;具体 ruby 语法可以参考相关文档学习一下
]]>随着 kubernetes 容器化部署逐渐推进,gcr.io 镜像、kubernetes rpm 下载由于 “伟大的” 墙的原因成为阻碍玩 kubernetes 第一道屏障,以下记录了个人维护的 yum 仓库和 gcr.io 反代仓库使用
目前个人维护了一个 kubernetes 的 yum 源,目前 yum 源包含 rpm 如下
rpm 包 | 版本 |
---|---|
etcd | 3.1.0-1.x86_64 |
flannel | 0.7.0-1.x86_64 |
kubernetes | 1.5.3-1.x86_64 |
kubeadm | 1.6.0-0.alpha.0.2074.a092d8e0f95f52.x86_64 |
kubectl | 1.5.3-0.x86_64 |
kubelet | 1.5.3-0.x86_64 |
kubernetes-cni | 0.3.0.1-0.07a8a2.x86_64 |
使用方法如下
1 |
|
所有关于 yum 源地址变更等都将在 https://yum.mritd.me 页面公告,如出现不能使用请访问此页面查看相关原因;如果实在下载过慢可以将 yum.mritd.me
替换成 yumrepo.b0.upaiyun.com
,此域名 yum 源在 CDN 上,由于流量有限,请使用 yumdownloader 工具下载到本地分发安装,谢谢
关于 kubernetes 镜像下载,一般有三种方式:
个人在国外服务器上维护了一个 gcr.io 的反代仓库,使用方式如下
1 |
|
如果对于 gcr.mritd.me 访问过慢可参考 gcr.io 仓库代理 使用带有梯子的本地私服,如果使用 Docker Hub 等中转可参考 kubeadm 搭建 kubernetes 集群
]]>