这是软件的实现效果以及仓库
https://github.com/corvofeng/gmux
1 | # MacOS |
1 | mkdir ~/.tmuxinator |
这是官网介绍的一种方案, 它的基本原理是使用KUBECONFIG中的context进行切换
https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/
1 | apiVersion: v1 |
并且许多工具类似kubecm也是基于此来实现的, 这样会带来几个问题:
在真正的线上多集群环境管理时, 非常不方便且容易出错. 我接下来介绍一种使用tmux 配置文件分割的方案.
tmux 是一款非常强大的终端复用器,它允许您在同一个终端窗口中创建和管理多个会话。这对于管理多个服务器或集群非常有用,因为您可以轻松地在不同的会话之间切换,而无需打开多个终端窗口。
要使用 tmux,您需要先安装它。在大多数 Linux 发行版中,您可以使用以下命令安装 tmux:
1 | sudo apt install tmux |
安装完成后,您可以使用以下命令启动 tmux:
1 | tmux |
这将在您的终端窗口中创建一个新的 tmux 会话。您可以使用以下命令在不同的会话之间切换:
1 | tmux attach-session -t <session-name> |
您还可以使用以下命令创建新的会话:
1 | tmux new-session -s <session-name> |
简单来说, -L socket-name
参数允许指定 tmux 的 socket 位置, 不同 socket 对应的会话完全隔离.
我们可以在不同的会话中使用不同的环境变量, 以此来达到分离环境的效果
例如, 通过以下两条命令, 你可以创建出完全独立的两个终端, 并且它们的环境变量
1 | KUBECONFIG=~/.kube/config-aa tmux -L aa |
这里的脚本其实已经可以实现多集群管理了, 为什么我们要引入tmuxinator以及我新写的gmux呢?
https://github.com/tmuxinator/tmuxinator
它是使用ruby写的一款工具, 可以用yaml形式定义tmux终端, 同时也支持模板化, 例如下面的一个模板化的yaml文件
1 | name: project |
在负责集群环境迁移的几个月中, 我用到最多的一些命令就是:
1 | tmuxinator tpl project=ingame-pre-na |
它能帮助我完美的区分不同环境, 并且由于我使用了fzf, 我甚至可以模糊搜索到自己想要打开的环境
局限: 由于使用ruby编写, 它需要机器上安装比较新版本的ruby. AWS需要登陆到一个跳板机中进行操作, 但是我们用到机器很旧, 我也不想编译重装一个ruby.简单读过代码之后, 发现它本身没有用到什么更高级的特性, 使用golang完全重写也非常容易.
这个项目我大量借助了ChatGPT, 主要是一些逻辑编写以及单元测试的编写. 本来我感觉要2-3天的工时, 开发周期缩短到了1天. 最后实现的效果也非常好. 重写之后机会没有任何依赖, 安装很轻量.
项目开源在了这里, 与tmuxinator的配置应该是兼容的, 发现有缺失的功能也欢迎开issue.
https://github.com/corvofeng/gmux
命令行管理几十的集群也是完全行得通的, 而且效果也还不错. 希望也能够提升大家的效率吧.
1 | name: <%= @settings["project"] %> |
配合修改context的功能, 还能在终端PS1中增加提示, 类似这样
]]>The Phoenix Project(凤凰项目) 是一个大公司内部指望拯救公司的一个软件项目,项目仓促上线之后其实并没有实现对应的效果, 主人公临危受命, 接手整个IT的烂摊子.主人公从观察一个完整的流水线工厂, 发现了其与现代运维的一些共性, 并依次对系统进行改造,最终构建出一个强大的IT团队以及完善的工作流程.
我就简单说说自己记忆深刻的点, 就当做个小总结. 感觉当个小说读读看也是可以的.这篇文章可能会持续更新:
IT 运维的4种工作类型:
在业务上, 分析业务迭代进程中的各个流程以及规范:
在系统设计目标上:
前几个月看了吴恩达教授的这篇ChatGPT教程, 其中对于 colab 的使用给了我很多启发, 建议没看过的读者也去看看
【【中文完整版全9集】第1集 引入-ChatGPT提示词工程师教程 吴恩达xOpenAI官方】 https://www.bilibili.com/video/BV1AT41187qt
colab 有几个优势:
VSCode 也有 jupyter notebook 的支持, 同样也是调用过了 jupyter server,但是你需要把文件保存在本地, 需要依赖一个 VSCode, 而且页面我感觉也没有 Colab 美观.所以大部分情况我都是在用 Colab.
打开 https://colab.research.google.com/ 直接创建新的 notebook 即可:
我先前写过一个提供ssh服务的功能, 也可以用在 colab 中, 可以再推销一波哈哈:
1 | !bash <(curl -sL http://corvo.fun/scripts/upterm.sh) |
最终的效果如下:
在本地机器上, 需要首先安装 jupyter, 然后启动 server 即可:
1 | jupyter notebook \ |
Google 这里也给出了使用 docker 的方案:
https://research.google.com/colaboratory/local-runtimes.html
之前调试 easytrader 时, 由于仅支持 Windows, 我就在 Win10 虚拟机中运行了 jupyter server,然后使用 ssh 将端口转发到了本地, 就能直接用网页的 colab 进行调试了.
1 | ssh -L 8888:XXX.XXX.XXX:8888 dev |
这个方案我同我们的 DBA 同学商量过, 他也比较喜欢. 因此我分享在这里, 希望也能帮助有类似需求的伙伴.
1 | !ls |
这个方案的好处在于你能够边想逻辑边写 Python 语句, 而且 notebook 能将你的思路记录下来后期, 当你需要分享给别人操作过程的话, 只要简单修改就可以做到.
特殊字符处理
https://stackoverflow.com/a/69482789
我遇到了一个比较特别的密码aa@bbb&%2021
里面的特殊字符需要转义好填写到db.txt中
1 | import urllib.parse |
之前尝试计算做量化, 准备自动买卖股票, 可是需要演算收益率什么的. 我知道量化大佬们都在用 Excel, 我自己没那水平, 就用 Python 简单模拟下收益率计算, 感觉这个代码未来还能复用, 也简单分享给大家:
1 | result = 0 |
现在, 我发现自己已经离不开 Colab 了, 它把我需要进行简单演算的工作都承担了.而且还能为演算做笔记, 做笔记过程中又能编写代码模拟逻辑. 相比较复杂的 Excel,程序员可能对 Colab 更加有掌控性吧.
]]>Dockerfile的构建优化我平时见过不少, 我之前也介绍过一篇Docker系列(七)-Dockerfile进阶-多阶段构建与构建优化,但是除了这些方法外, 还有一种另类的方案. 能更加极致的优化镜像构建时间并将层的概念进行扩展.
我们的代码使用的是Python. 对于这类解释型语言, 使用容器运行意味着需要将代码拷贝进去到镜像中, 这是一种常见的情景:
1 | # 这是我们准备好的代码结构 |
1 | # 安装依赖 |
这是一种比较常见的方案, 应该也是读者预期的, 我们将代码的依赖做了缓存, 但是没有缓存代码本身.这就会引出一个问题, 假如src/sample
这个仓库本身很大, 那么我们一次次构建, 上传镜像, 甚至到运行时pull镜像,这一层都会很大, 构建中变慢的时间在这个过程中, 其实被放大了3倍.
基于此种问题, 我们引入了一种新的方案. 直接在Dockerfile构建时clone代码, 但是分为两个阶段, 第一阶段仅有基础的clone语句, 该层会被直接缓存. 而后的fetch语句中增加了对于git checkout的使用, 强制改变当前HEAD为需要的commit hash, 改造后的代码如下:
1 | COPY src/sample/requirements.txt requirements.txt |
上面给出的代码我们已经在正式环境中运行超过一年以上, 未收到任何异常.
第一clone的层约有180M, 第二次再fetch已经只有90M了. 这个改造下来, 镜像的push和pull也省了至少90M,即使时10M/s的网速, 我们把整体的时间大概压缩了20s.
针对某一类拥有历史记录比较多, 而每次改动却又不是特别大的项目, 节省的时间和空间会更加明显,但是也有一个问题就是Dockerfile中, 需要我们放入可以直接clone的地址, 所以仅推荐私有仓库或是开源代码使用,否则还是会有代码泄漏的风险.
上面的两阶段clone代码主要有两个问题:
基于此问题, 我们打算引入一种新的方案, 将git clone的地址改为ssh协议, Dockerfile中仅有ssh地址, 最后的成品中也没有私钥, 就不用担心代码泄漏的风险.
stackoverflow上面有很多关于这个问题的讨论, 例如:
1 | # https://stackoverflow.com/a/66648529/5563477 |
--squash
, 这样删除私钥了之后, squash能将层压缩为一层, 从而抹去key在先前层存在的问题, 虽然也能实现功能, 但是仅压缩为一层意味着完全没有缓存1 | # docker build -t example --build-arg ssh_prv_key="$(cat ~/.ssh/id_rsa)" --build-arg ssh_pub_key="$(cat ~/.ssh/id_rsa.pub)" --squash . |
来自: https://stackoverflow.com/a/58883743/5563477
1 | export DOCKER_BUILDKIT=1 |
1 | # syntax=docker/dockerfile:experimental |
目前市面上已经有不止一款docker镜像打包工具了. 我这里只是简单介绍一下它们, 并且看看对于ssh协议clone代码的支持,我们的生产环境还未使用, 当然读者也不应该仅仅为了这个特性就更换目前稳定的构建打包工具.
1 | docker run \ |
cache-dir的形式我没跑通, 本地的/cache目录总是不写入数据, 所以只测试了cache-repo的形式, 可以让kaniko把cache放到了仓库中, 每次构建镜像时检查仓库的缓存
简单测试了一下kaniko的缓存方案, 对于corvofeng:develop
这个仓库, 会默认使用/corvofeng/develop/cache
地址来缓存, 你也可以自己指定一个缓存仓库.
在GitLab runner中, 使用这种方式构建镜像再合适不过了, https://docs.gitlab.cn/jh/ci/docker/using_kaniko.html
kaniko也支持ssh协议clone代码, 挂载ssh-agent对应的unix socket到容器中即可
1 | docker run \ |
1 | FROM python:3.8-alpine |
对应的效果如下
buildah的使用比较接近docker buildkit, 可以在镜像构建时挂载unixsocket, 下面是一个简单的例子
1 | sudo buildah build --build-arg=SSH_AUTH_SOCK=$SSH_AUTH_SOCK --volume $SSH_AUTH_SOCK:$SSH_AUTH_SOCK . |
1 | FROM alpine |
效果如下:
首先, 这篇博客介绍的优化措施并不适合所有的项目, 主要针对大型项目镜像的构建, 如果你的项目比较小, 就没必要考虑这种手段.
另外, 我借用SSH_AUTH_SOCk
是想说明, 现有的工具已经支持我们在构建时挂载文件或是unix socket
,对于仅在构建时需要的密码或是私钥, 完全可以使用文件挂载的方式来实现.
虽然我这篇博客都是在围绕私有项目的构建和部署来讲解优化措施的, 但是对于kankio以及buildah, 它们完全可以应用到开源项目之中.我在创建一些命令行工具的容器时, 也会首选这两种工具.
我没有针对arm64之类的镜像构建做过深入测试, 如果有这类需求, 建议还是自己确认下是否可行.
]]>VSCode
客户端连接, 但是我感觉这还不完整, 毕竟VSCode
已经推出了web版, 比如vscode.dev以及github.dev. 未原生运行VSCode
设备, 只要有浏览器支持就可以使用, 并且也支持iPad或是安卓平板这类设备,因此我就继续深入改造了一下, 目前已经能提供网页版的VSCode功能:
在桌面端, 你完全不需要下载任何软件, 打开浏览器即可拥有全功能的VSCode, 适合手头没有电脑需要紧急借用别人电脑时的情况.
在iPad端, 虽然是个网页程序, 但是它在添加到桌面后已经能较为完美的融入生态, 并且提供的终端足够碾压iOS上的其他SSH工具.
服务器在香港, 配置也略低, 可能偏慢, 欢迎体验. 有关技术细节我也会再写一篇博客说明, 主要复用了upterm与uptermd通信使用的SSH隧道.
1 | bash <(curl -sL http://corvo.myseu.cn/scripts/upterm-web.sh) |
使用浏览器打开后会预加载一些资源, 加载完成后, 可以点击按钮进入, 我建议你安装为软件后使用, 因为网页中的快捷键比如ctrl-w
可能会触发页面关闭而不是编辑框关闭
只需要点击作为应用安装即可:
Chrome中需要创建快捷方式, 并勾选在窗口中打开:
你可以使用VSCode的全部功能,
也可以打开一个终端来用:
将地址拷贝之后在safari中打开
iPad也可以扫描电脑地址栏中的二维码打开响应链接
只能在safari中使用, 但是我十分推荐. 毕竟为了Add to Home Screen
的优雅效果, 我改了很多…
就算是但从终端效果来看, 我觉得都能甩termius
几条街, 更不用说完整的VSCode支持了.我没有秒控键盘, 只是拿普通的蓝牙键盘试了一次, 换成Win的蓝牙模式, 就可以方便使用Esc键了, 尤其适合Vim用户,基础的开发以及运维需求看起来完全没有问题, tmux这类工具也毫无压力.
应该算是扩展了iPad的代码生产力吧.
vscode.dev
或是github.dev
等网站直连一个VSCodeWeb服务, 最大程度的复用CDN资源其中, 1,2
涉及到用户体验的部分, 是可以氪金解决的, 不知道有没有大佬愿意赞助一波
其中, 6,7
的功能会涉及到对VSCode代码的大量改动.
我没有详细了解Gitpod
未来的发展方向, 我猜想结对编程可能是个点, 并且使用VSCodeWeb
实现是较为合适的一个技术选型. Gitpod也很可能会这么做.
虽然微软提供用户使用Remote-SSH
功能, 但是这个插件并未开源, 这就导致了我们基于Remote-SSH
开发功能受到一定限制,最直接的就是端口转发功能, 还有多个ssh_config
文件的支持. 我相信Gitpod
也一定遇到过类似这样的问题.VSCodeWeb
代码包括在VSCode
源码中, 无论是改造, 还是加个插件都是切实可行的.
虽然我一直有提到Gitpod
, 但我希望读者能够认识到, upterm与Gitpod是有区别.
- upterm拥有更加开放和底层的功能, 它是一个完善的基础工具, 也可以作为Gitpod的底层实现, 它不会主动提供机器供用户使用, 也不需要你登录Github账户
]]>
- Gitpod从开发逻辑上来说, 是更加希望用户使用Gitpod的方式来每次启动新的Pod, 并且在它维护的Pod中启动VSCodeWeb服务. 它提供了完整的服务, 类似一个SaaS平台.
博客分为两部分, 前半部分介绍如何使用, 后半部分针对工作原理进行简单说明. 公网自建的uptermd
是个自用的小水管,经不起大风大浪, 请大家合理使用. 我不是很想用爱发电, 有心用的同学可以捐点服务器费用.
现在的代码放在这里了:
https://github.com/corvofeng/upterm
1 | bash <(curl -sL http://corvo.myseu.cn/scripts/upterm.sh) |
你可以直接将上面的uri贴到浏览器中启动:
或者新开一个命令行:
当VSCode左下角有了这个图标, 就表示远程连接已经成功了
Open Folder可以打开仓库
它是一个全功能的SSH服务器, 放大之后当个Terminal来用也可以, 也支持分屏
当你在远程的Linux中启动了端口之后, VSCode能帮你自动转发到本地, 可以获得接近原生的开发体验
还有很多功能, 用户可以自己探索下. 例如, 拷贝文件, 安装插件, 只要是VSCode remote支持的功能, 它都可以用
你可以粗浅的认为这个工具拥有openssh-server+frp的功能
左右是两台机器, 其中一台机器使用upterm开启了服务, 另一台可以直接连接其session.
upterm是与tmate类似的工具(tmate可能相对多知名一点), upterm做了几个工作:
有兴趣的读者可以参考upterm项目主页上面的流程图
这是VSCode官方的图, 可以认为是前端的代码编辑器与后端的Sever分离
upterm已经提供给我们ssh连接的功能, 为什么不能直接使用VSCode的remote ssh功能呢. 这个要从remote ssh的实现说起,
当我们使用VSCode远程开发时, 它是这样连接的远程服务器:
1 | ssh -T -D 1234 192.168.101.135 -p 2224 bash |
这个命令需要拆开来看:
ssh -T 192.168.101.135 -p 2224 bash
这个命令的含义是远程连接服务器后, 不启动tty, 直接启动bash.ssh -D 1234 xxxxx
表示本地建立一个socks5的转发服务, 具体使用可见OpenSSH系列(六)-正向代理与端口映射所以只要我们的ssh服务器能够提供给VSCode这样的连接方式, 就可以了. 感谢Golang的库开发者, 让我有机会比较容易的建立一个ssh服务,具体的代码我放在gist中了, 有兴趣的读者可以去看:
https://gist.github.com/corvofeng/45c01edd33fa750e31653a90b1c4cdec
建立ssh连接之后的通信的过程: remote机器会下载一个code-server执行文件, 然后启动, VSCode之后仅与服务器中的code-server通信.
从目前使用的情况来看, 现有的工具确实有一些瓶颈, 比如:
与Gitpod相比, 这个项目仅仅是能用, 还称不上好用:
看到Gitpod两轮融了1600万美元, 真的还是有点心动. 现代安卓机器性能过剩很多,如果我们能支持Android, 就是相当不错的一个产品.
从我一开始将Dashboard引入到系统中, 我就有个目标是允许我们的用户通过本地的ssh或是VSCode连接远程.因为我们的开发语言都是Python. 线上环境的Pod其实有一整套Python代码, 所以能够支持用户开发或者调试是很有必要的.下面是我做过的一些尝试和调研.
有人可能会说有的用就不错了, 用户可能完全可以用ssh+frp实现VSCode remote ssh功能, 但是这样的学习成本有点太高了,需要每次安装好多依赖, 很可能有需求也不会去这么用. 我主要想简化这个启动过程, 做到一条命令映射给VSCode可用的ssh server.功能做出来之后其实通用性很强的,
我已经将原理简单介绍过了, 安全性的话就见仁见智了, 建议经常性的重连来强制刷新token.
]]>目前我们所使用的网关有两种, 我在之前的博客中都有介绍: Kubernetes Ingress, Istio Ingress Gateway. 用的功能不多, 基本只有灰度和转发, 鉴权, 限流的功能完全没依靠它们实现.
我们使用的是K8s社区基于OpenResty的实现, 与Service来配合使用:
1 |
|
由于某些原因, 不得不使用这种Ingress, 需要一起使用Gateway和VirtualService, 也需要Service
1 | # 指定要接收流量的gateway |
我个人认为网关不应该只有普通的转发功能, 还包括鉴权, 限流等功能, 这些功能应该是网关的一部分, 并且应该是可以插件化的. 我不一定要求直接编辑Nginx配置, 但是当我想增加扩展的时候, 希望可以有这么一个选项. 下面就按照我自己的调研内容.
之前看到Kong这个产品, 它的底层使用了OpenResty, 但是实现了插件系统, 可以说是Kubernetes社区Ingress的增强版. 也支持Python, Golang的一类插件, 我简单介绍下使用方式:
1 |
|
我比较喜欢它的交互逻辑, 当你有多个域名时, 每个域名可以设置自己启用的插件及其配置.它已经是成熟的商业化产品, 所以可用性方面还是很强的, 很可惜目前还不支持Wasm.
Istio中使用的Envoy, 已经支持了wasm插件.
1 | apiVersion: extensions.istio.io/v1alpha1 |
不过即使是最新版本的Istio对插件的支持都不怎么样, 没法单独针对一个virtual service增加插件. 这里的selector是针对ingress gateway的. 也就是说, 所有网关上的virtual service都会被影响. 而且多个插件的行为是使用proiority来控制的, 用户的交互会十分困难, 可用性太差了.
针对几种可能的方案, 我做了一些简单的探索和研究. 这里我贴出的方案, 我都调研了其可能的实现策略, 也都有把握将其落地. 读者有什么想法也可以一起与我交流.
单从交互逻辑以及可用性方面, 我觉得没什么可优化的, 唯一让我觉得难受的地方就是无法使用wasm类型的插件.
有几种可能的方案:
我臆测这是Kong团队会考量后两种. 对于第一种, Python, Golang类插件所带来的维护成本, 用户体验也并不友好. 后面两种, 它对于Wasm是原生的支持.不管是后期维护, 还是性能方面, 感觉都会好很多.
由于Envoy原生支持Wasm, 所以这块我比较看好. 但是如果想要作为网关, 交互逻辑上可能需要改造下, 绑定到VirtualService或是Gateway上是比较合理的.
1 |
|
为什么要支持Lua插件我看中的是Kong现有的插件生态. 几乎所有的Lua插件都是经过了企业级的认证, 如果能在Istio+Envoy中直接使用, 相信很多用户会愿意使用Envoy作为网关.
Lua插件的支持其实有两种可行方案的:
Lua直接编译到wasm, 据我搜集到的资料来看, 没有现成的编译器, 需要自己手写
近期看到, 网易数帆针对Envoy添加了Lua支持, 也就可以考虑模拟Kong的PDK来使用其插件
https://xie.infoq.cn/article/1cb74a7512460b7d4dbc9f42c
博客中内容, 我觉得性能检测那边有点问题, 无论何种语言编写的wasm插件, 它已经变成了类似汇编的字节码, 性能差距不应该很大, 博客中以C++来做最有的效果衡量让我感觉不是很有说服力. 但是博客切实的将Lua嵌入到了Envoy, 性能可以慢慢优化, 做出来成品还是很重要的.
我的博客中有许多讨论PaaS平台建设的文章, 我本人也在维护一个PaaS平台, 所以我也从PaaS的角度给出一些方案.
上面两种改造方案是需要我们去改进控制器以及底层代码来实现的. 对于一个PaaS平台来说, 我们不会深入到这样的底层系统, 对它改造太困难了.如果我们想要拥有完善的网关功能, 同时考虑用户的交互逻辑, 比较好的策略是用户在代码中指定一些特殊配置, 无论是使用nginx
, lua
或是wasm
配置,我们都对其支持. 使用类似Istio中sidecar的形式, 该sidecar仅作为API网关, 并不截取所有流量. 这样考虑有两点:
技术本身没有什么好坏, 只有适合不适合.
对于PaaS平台, 可扩展性和简易的交互逻辑是很重要的, 我们有成百上千个小项目, 就会有各种各样的需求. 我们不能寄希望于完全使用Istio的描述文件来扩展平台功能, 而是应该总结需求的底层逻辑, 在现有方案无法支持的情况下提出备用方案给用户. 我们应该会采用扩展型sidecar的形式, 允许用户写一部分配置并托管到Git仓库.
我个人喜好, 当我个人用K8s维护几个小的项目时, 我自己倒是很希望一些功能的插件化, 因此我个人比较喜欢类似Kong插件的形式进行交互, 但是我又想要有Wasm的支持. 所以我会偏好Istio+Envoy的形式, 如果能够改进Wasm的插件形式并且使用有Lua功能支持的Envoy, 我可能会把自己的项目全部切到Istio上.业余时间应该我会考虑改进Istio的控制器.
文章篇幅比较多, 你可以把它理解为一份调查报告. 基本都是现有的业务逻辑分析, 还有个人拙见. 大佬们觉得对于哪种方向有看法或是兴趣, 都很欢迎与我讨论和沟通.
]]>Kubernetes中Cron任务的一些使用Kubernetes中CronJob源码阅读
我认为这一点并不是K8s的设计有问题, 设计之初没有考虑到docker在机器上的性能不够, 无法批量快速的创建容器, 并且会拖慢整个系统.此问题的我们是通过物理隔离来解决的, 将定时任务限制在固定的几台机器, 能有效降低集群中其他机器的内核问题的出现概率.
延迟问题的出现并不是单一的原因, 有以下几种类型:
- K8s本身调度延迟, 本应该按时启动的任务拖了很久
- 与上面的原因一致, 机器中docker的负担太重, 几秒可以启动的容器慢了半分钟, 我不太清楚这个问题在读者的集群中是否有出现, 但是我们的集群中特别明显, Pod处于ContainerCreating的状态会很久
在2021年的时候, CronJob API到了GA阶段, 一个重要的变动就是将定时任务控制器换成了v2. 原文在这里.
https://kubernetes.io/blog/2021/04/09/kubernetes-release-1.21-cronjob-ga/
原始的控制器, 每10秒检查所有的定时任务是否需要执行, 这个操作只能由单个worker来实现, 具有O(n)的线性复杂度, 当定时任务过多的时候, 性能会变得糟糕.K8s在1.19引入了新的定时任务控制器, 转变了实现的策略.
1 | // pkg/controller/cronjob/cronjob_controllerv2.go |
CronJob v2的实现利用了K8s api server的订阅通知类型的实现方式:
更新之后的性能优化看起来很明显
上述K8s对于定时任务的优化, 我们集群时用不上的, 因为集群比较旧, 还没有这种支持. 另外一点就是, 上面的方案仅仅降低了任务调度时的时间, docker负担太重的问题仍然没有解决.鉴于机器负担过重, 以及定时任务执行时间不准确的问题, 我们提出了一个解决方案, 将高频运行定时任务的Jod生命周期延长.
举例来说, 用户期望
/bin/my_script
要每分钟运行一次. 针对我们的方案, 启动Pod后, 人为使Pod存在1小时或是更久的时间, 在Pod内部添加cronjob调度, 每分钟执行一次/bin/my_script
.当然Pod存在的时间是可以调整的, 我们人为的设定是一小时, 为了使任务能够分散的到各个运行机器中.
原始的CronJob如下
1 | apiVersion: batch/v1beta1 |
改造后的CronJob如下
1 | apiVersion: batch/v1beta1 |
方案的好处:
这样会带来的一些问题:
针对第一个问题, 我们可以通过一定机制避免其发生, 但是针对第二个问题, 由于设计本身的问题, 没有什么比较好的解决方案. 在实际使用上, 我们遇到的高频定时任务对资源不是很敏感.
这个部分涉及到实现的细节部分, 我只是介绍下一些逻辑, 不涉及到具体代码, 需要考虑的方面有以下两个:
- 如何能够无缝的衔接定时任务的执行, 确保不会丢失或是重复
- 在用户修改任务或是部署新版本后,如何能够尽快的刷新更新定时任务
在启动新的Pod之后, 旧的Pod并不会马上下线, 我们为其提供了一小段缓存区间, 如图所示, 时间轴上的虚线区域, 两个Pod同时在运行. 如此设计, 我们可以保证不会丢失任务
我们的每个容器有容器令牌的概念. Pod1运行时, 拥有令牌, 当我们启动Pod2后, Pod1会在合适的时机释放令牌, Pod2只有获得到令牌之后才可以执行定时任务. 释放以及获取令牌的时机也很重要, 对于Pod1我们会在某一分钟开始后第10s开始释放, 也就是在一分钟的前半段释放令牌, Pod2就可以拥有50s左右的时间获取该令牌, 这个时间很充足, 足够Pod2获取应用令牌, 开始执行下一次任务.
定时任务的执行中, 用户很有可能在非整点的时候切换版本或是修改定时任务.一旦发生, 上述的容器冗余能保证我们在下个调度周期更新, 但是用户修改任务或是上线版本时, 希望它能够马上生效, 而不是等待(有可能一个小时后才生效).基于此设想, 我们考虑了一种分离普通定时任务与手动改变任务的方式, 下面就是具体的逻辑图:
这里的实现主要使用了K8s的定时任务的一个功能:
kubectl create job --from=cronjob/<cronjob-name> <job-name>
手动创建的脚本也同样会获取令牌, Pod1会提前结束, 一直到Pod2开始运行前, Manual Pod都承担运行脚本的任务. 这里的思路就是分离日常行为以及突发行为.
我只是粗浅的介绍下我们对于定时任务的优化, 具体的细节有很多, 特别是对定时任务的监控代码比它的实现代码还要多. 我们的策略已经在线上运行了超过一年, 应该是比较稳定的功能了, 所以把设计策略分享出来给大家参考下.
有些时候我们使用某些框架可能正好是顺手就用, 但是随着业务的发展, 需要逐步对框架进行定制以及优化来适应业务需求. 可持续的解决业务开发需求, 才能有效推进K8s组件的落地.
用了开源的组件就要有觉悟, 你需要自己去定制某些策略来解决问题, 你的任何行为也也不会有人对你负责. 可以看下这篇帖子自己搭的Gitlab开放到公网被黑了.
]]>flannel
之后遇到的两个问题以及解决方案, 问题其实不严重, 只是涉及到了底层的结构, 改动时候要小心.下面这张图是官方的配置, 可以看到, 默认的资源设置仅给定了50M内存
1 | kubectl -n kube-system describe ds kube-flannel-ds-amd64 |
当我们的机器数量超过100个以后, flannel会以OOM的形式一直挂掉..
1 | Feb 9 04:52:44 kernel: [37630249.323630] Memory cgroup out of memory: Kill process 33838 (flanneld) score 1653 or sacrifice child |
通过Prometheus
采集到的数据也可以看到, 容器的内存使用情况很不乐观:
也没什么好的解决方案, 只能调整资源限制了.
因为我们使用的机器比较混杂, 机器的网卡也各不相同, 在开始搭建集群时就遇到了下面的问题.
1 | > 我们虚拟机中的网卡, 仅有`10`开头的内网地址 |
这样带来的问题就是flannel通信问题, 如果多个网卡, 且启动时未指定, flannel会找一个缺省的网卡, 对于虚拟机来讲没有关系,但是对于物理机, flannel会找到eth0这个外网网卡, flannel使用错误的网卡发送数据, 抓包的数据可以看出flannel使用了公网的网卡发送内网数据, 会被交换机丢弃, 具体图片就不贴了, IP属于公司机密.
具体的修改方法是确保flannel使用了正确的网卡, 需要在启动时指定参数--iface
与--iface-regex
:我们的虚拟机数量少, 物理机数量多. 除了eth1
, 还有bond1
这种网卡名, 因此针对虚拟机, 统一将其eth0
改名变成eth1
,而后指定了-iface-regex=eth1|bond1
这样的配置, 对于后续增加物理机更友好.
问题到这里似乎就结束了, 但是随着flannel经常发生OOM重启, 暴露了我们的设置问题.
1 | NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES |
为什么一开始没出现, 但是重启又会发生呢, 问题出在了正则表达式上. K8s在机器上启动容器时, 会创建虚拟的网卡. 这些网卡的名字类似veth17f90f70@if3
, 这样网卡名称的也会被正则表达式匹配到, 导致flannel无法启动, 临时的解决方案就是把机器上的容器移走,vethxxx
网卡会自动删除, flannel也就自动恢复了.
当然根本的解决方案是修改正则配置: - -iface-regex="^(bond1|eth1)$"
使flannel更加精准的匹配网卡名称.
因为不太了解flannel是否处理流量, 更新flannel时有点害怕, 直到看到了这里的架构.
flannel的功能主要是负责机器上路由表的修改, 也就是说, 只要不增删机器, flannel挂掉也没关系, 因为路由表不需要修改.
我们有100多台节点, 整个集群更新过程大概持续了1个多小时, 更新过程中服务完全正常.
内存使用情况:
为了验证flannel是否可用, 我们将一台node删除, 观察到其他机器上的路由表也同步进行了修改.
希望我们的经验能帮助到使用K8s的各位读者.
]]>我是Python后端的程序员, 主要负责PaaS平台的治理, 博客中的描述仅仅是自己的一些见解.
跳转过去之后, 会先安装Gitpod插件, 然后再打开
这是登录命令:
1 | ssh -F /tmp/gitpod_ssh_config-216243-zxK2tQ5tHG0H moccasin-capybara-78upia72 |
Pod内部是用supervisor来启动, sshd, vscode web服务是比较正常的,令人比较意外的是它支持了docker内部运行docker.
1 | # 系统是`Ubuntu 20.04` |
我一开始以为这个supervisor是Python的那个, 后来我去Github官网找了找代码,发现它是一个自己写的Golang服务:
使用本地的VSCode打开时, 可以看到这里有一个ssh target
其中的配置文件类似如下:
1 | Host moccasin-capybara-78upia72 |
看起来它是直接连接了一个本地的端口, 具体这个端口是谁创建的呢, 找了一下是gitpod-local-co
,这个操作应该是类似frp的stcp模式, 把远程的ssh端口映射到了本地.
1 | # ss -tunlp | grep 36129 |
visudo可以看到%sudo组的用户可以不需要密码直接切换到root,gitpod属于这个组, 所以sudo命令直接可以用.
抽离主要代码: Gitpod中当前服务端, 应该做成一个单独的服务. 启动之后, 可以直接在Gitpod网页后台中看到, 并且可以直接调用VSCode打开工作区, 这样, 可以在用户自己的容器中运行服务端, 解决用户自建容器的内网穿透和VSCode web版无法使用的功能, 这个才是个人用户想要的功能
自定义镜像功能: 针对某个仓库, 应该允许用户自定义Dockerfile, 或是自定义镜像, 间接实现开发环境规范化
实现秒开: 因为Pod需要在VSCode关闭后停止服务, 那么用户下次打开时, 如何做到秒开呢? 我认为可以在关闭时, 删除ssh key, 动态的调整cpu到0.01核, 确保无法访问, 无法对外服务. 用户重新打开时, 再生成新的key, 调大cpu限制, 相比于重新启动容器应该会快很多.
服务对外访问: 对于后台代码, 修改之后能够立刻看到效果是最重要的, 可以利用VSCode的端口映射功能转给本地. 需要给别人演示时, 也可以将端口临时映射到公网或是功能开发环境中.
开发环境上云应该是一种趋势, 无论是VSCode的remote ssh功能, 还是JetBrains新推出的Fleet, 都传达出了这么一种信号.
确立主要用途: 从我个人角度来看, Gitpod十分适合于远程调试开发和调试非编译型语言, 尤其适合于Python
, Ruby
, NodeJS
, 甚至对于Python
应用, 完全可以把线上环境容器打包重新启动, 在Gitpod中的环境中进行开发调试, 相当于拥有了一个于线上环境完全一致的开发环境.
个人版与企业版分离: 针对个人开发者, 提供公有云服务, 可以考虑计费. 针对企业应该提供企业版, 因为未来即使有远程开发环境的功能一定是企业内部自建的, 因为它们不太可能将内部代码放在公有云环境中, 如果能提供一套适合企业的解决方案, 并且有人用, 应该能长久运营.
我之前就有在使用github1s
和github.dev
来读代码, 直到发现了Gitpod.相比于前两个仅能读代码的功能, Gitpod提供了更加完善的体验, 你可以利用它push代码,可以完整的接手开发工作, 这一点带给用户的体验会很好. 希望社区版能长久的存活下去吧,我的开源项目不多, 如果真的有需要, 我很愿意进一步使用这个工具并且付费.
我不是产品经理, 后面的内容单单就我自己的看法探讨一下项目未来的发展趋势, 可能就是在瞎扯吧.
]]>Dashboard
本身的权限控制功能太弱了, 而前端的页面多而复杂, 想要结合到我们平台上,需要精简一下前端, 然后改变后端的鉴权方式, 整个过程的修改应该尽量的小,以便我们日后能随着Dashboard
的更新来更新, 不然某天出现了安全问题需要更新就很难打patch了.
本次修改不需要你提前会用Dashboard
, 但是要对K8s的API有一定的了解,然后再读一下Dashboard
的代码. 它的前端是Angular, 后端是Golang.
一般写的不错的代码, 风格都差不多, 大多都践行了表驱动法和中间件的思想.前端页面中的配置文件肯定时放在一起的, 后端肯定是有中间件的, 我们添加鉴权模块就可以了.
首先你要由一个能用的kubeconfig
文件, 保证自己的kubectl操作可用.
我的上一篇博客中介绍了kubectl proxy
的使用, 如果你有兴趣可以读一下, 相信你对Kubernetes API的概念会有更深的理解.
前端运行起来比较简单
1 | > yarn start:frontend |
有一点你要注意, 默认去读的配置aio/proxy.conf.json
中, 没有显式的开启websocket,所以shell页面不是的使用方式不是长连接, 如果你想在本地调试时也使用全功能的webshell,可以改成下面这样.
1 | { |
我个人不太喜欢Golang一定要编译出二进制文件才能运行, 一般都是
go run main.go
直接来跑的.
1 | > cd src/app/backend/ |
然后打开前端页面, 应该是有个全功能的Dashboard.
具体的代码在
src/app/frontend/chrome/nav/template.html
中,可以调整顺序, 并且把一些不需要的组件去掉
1 | <kd-nav-item class="kd-nav-item" |
因为搜索框的触发时, 会搜索所有给定的资源, 因为我们单个NS的pods可能会很多,会导致搜索框的效率缓慢, 因此, 将搜索逻辑的某些组件也隐藏掉,当然也可以在这里调整显示顺序
具体的代码在
src/app/frontend/search/template.html
1 | <kd-job-list (onchange)="onListUpdate($event)" |
dashboard
的后端使用了go-restful这个库, 然后我们找一下它是如何增加中间件的,它有filter
的概念,
1 | // 代码来自 https://github.com/emicklei/go-restful/blob/v3/examples/filters/restful-filters.go |
我们的目的就是在dashboard后端增加几个全局的filter, 进行鉴权以及用户事件记录.
再看下dashboard
代码位置, 我们针对所有的API做一次再鉴权就可以了.
1 | // src/app/backend/handler/apihandler.go |
我们是用OpenID做登录的, 但是我又不想去开发这个登录模块了, 所以复用了keycloak-gatekeeper,最近再看的时候已经停止维护了…
它的功能主要是过滤非登录用户, 同时给后台传递登录用户的具体信息, 比如邮箱或是用户名.根据邮箱, 用户名, 判断当前请求中的namespace
以及api
是否允许用户访问.
我们所使用的Kubernetes集群中, kubeconfig的token是会过期的, 需要我们定期去刷新.这个涉及到Dashboard的另一项改动,
1 | // src/app/backend/dashboard.go |
我为了能够按照自己的意愿更新kubeconfig, 因此新增了一个生成器,
1 | func NewXXXClientManager(kubeConfigPath, apiserverHost string) clientapi.ClientManager { |
我只是分享下我们的方案, 希望维护PaaS平台的各位由所收获吧. 我个人感觉开发能力比较足的PaaS平台,都直接内嵌WebShell了, 我们实在没什么前端, 所以就拼凑一下了, 目前方案稳定运行半个月,稳定性方面应该没什么问题..
]]>流处理应用中, 带给我深刻的印象的算法就是质数筛选问题, 它使用递归的方式定义, 实现得相当简洁.
建议了解car与cdr之后再看这篇.
1 | ; 流的使用类似于cons, car, cdr, 但是其实现上略不同 |
从上面的例子可以看到, 流的实现中, car与stream-car的作用完全一致, cdr与stream-cdr的区别在于调用了一次force, 因此我们只要定义好force就可以了.
1 | ; 必须以宏的形式定义, 有兴趣的读者可以看下这里: https://stackoverflow.com/a/14641015/5563477 |
下面的代码我没有考虑流会结束, 真实的代码中应该考虑这个问题.
这个流有个特点, 就是后一项比前一项大1, 因此可以递归的方式进行定义
1 | (define (all-num n) |
就是不断的获取流的下一项, 直到得到我们想要的元素
1 | (define (my-stream-ref n s) |
比如我们针对正整数流进行操作, 把其中所有的元素都变成其2倍, 就可以这么来做
1 | (define (my-stream-map proc s) |
比如我们针对正整数流进行操作, 只取其中的奇数, 就可以这么来做
1 | (define (my-filter-stream pred s) |
以下内容来自于维基百科: https://zh.wikipedia.org/wiki/%E5%9F%83%E6%8B%89%E6%89%98%E6%96%AF%E7%89%B9%E5%B0%BC%E7%AD%9B%E6%B3%95
简单来说, 如果我们得到了一个质数, 就将后续的整数中, 能整除它的所有数过滤,这样我们得到的数列就是一个素数的数列, 使用scheme编写时:
1 | (define (sieve s) |
这个递归的实现可以说是完全契合了定义.
SICP中, 介绍流的时候, 老师是从电气工程的角度来看的, 电路信号是波动的, 连续的,进过不同的整流器可以产生多种变化, 但其仍然是连续的, 这是一种流.
虽然我们的数据结构课上没有讲过它, 但流其实也是一种扩展出的数据结构, 并且它还是一种控制逻辑. 当你将某些数据用流来表示时, 这意味着程序的其他部分也需要适应这个变化.它将一段代码真实的执行时间推迟, 允许数据在真的使用时才会产生.
流处理是一个强大的工具, 程序可以全部流的方式来实现, 但是会带来一个问题: 你可能完全不知道一个函数会在什么时候被调用.因此, 我个人认为这样的数据结构适合于程序的几个部分或是几个程序的交互处, 类似生产者与消费者之间, 尤其适用于多级消费者,做一个任务就提交能有效避免后续的消费者饿死.
1 | // 我就不花里胡哨的去用lambda或是dispatch实现了, 二元数组就够了 |
1 | (car (cons 12 34)) ; => 12 |
它是scheme内建的方式, 当然也可以由我们自己来实现, 在SICP中其实介绍了几种方案, 我也整理了一下与大家分享.
1 | (define (cons x y) |
由逻辑学家Alonzo Church提出的方案, 主要的想法是: cons并不是单独存在的, 只有搭配car,cdr的时候cons才有意义,所以不需要完整给出cons的实现, 仅需要将其作为后续选择器的输入数据.
1 | (define (cons a b) |
相当于我们增加了set-car
与set-cdr
函数
1 | (define (cons x y) |
主要是增加了set!
函数用于修改内部变量, 需要在我们的lambda函数中增加
1 | (define (cons a b) |
引入赋值操作前后其实对应着我们看待世界的不同方式. 引入赋值之前, 对于某个函数来说,它的输入和输出是可以保持一致的, 引入赋值操作之后, 函数或是其他结构体具有了内部的状态,成为了一个独立的个体. 建议大家可以看下第9节, 赋值状态和副作用, 让我想到了函数的可重入性.
1 | let cons, car, cdr, set_car, set_cdr; |
1 | // lambda |
1 | // dispatch set |
1 | // lambda set |
原理节选自: 牛顿迭代法实现平方根函数
牛顿迭代法:
$x_{n+1}= x_n-f(x_n)/f’(x_n)$
假设输入数是S, 那么要求的平方根为x, 满足$S=x^2$, 那么我们就可以定义函数$f(x)=x^2−S$, 最终问题就转换为求方程$f(x)=0=x^2−S$的根。
$x_{n+1}=x_n-(x_n^2-S)/ 2x_n$
化简之后会变成下面这样:
$x_{n+1}=(x_n^2+S)/2x_n$
但是我们也可以不化简, 直接让程序帮我求出$f’(x_n)$, 我在接触SICP之前, 甚至都没有想过类似的操作.现在问题转化为, 当我们拥有了$f(x)$如何求得$f’(x)$, 我们知道在数学上$f’(x)$是这么定义的.
$f’(x) = \frac{f(x+\Delta x) - f(x))}{\Delta x}$
那么根据$f(x)$, 我们可以定义出$f’(x)$, 该函数接受一个值, 并返回该值对应的原函数的导数值.这是在scheme语言中的定义:
1 | (define dx 0.00001) |
我们这里得到的公式是:
$x_{n+1}=x_n-(x_n^2-S)/ 2x_n$
如果把$x_{n+1}$看做y, $x_n$看做x, 那么上面的表达式就是在求下面公式的定点
$f(x) = x - (x^2-S)/2x$
我们想要的结果是多次迭代之后, $x_{n+1}$约等于$x_n$, 因此, 我们正好要求的就是这个迭代函数的定点$f(x)=x$,可以抽出一个求迭代函数定点的工具, 定义如下:
1 | (define (fixed-point f first-guess) |
1 | (define (newton f guess) |
我认为值得借鉴的方案是给出了$f(x)$之后, 能使用(DERIV f)
给出$f’(x)$, DERIV
接受一个函数,并返回这个函数的导函数. 另外就是对于执行过程的多次抽象, 比如fixed-point
与newton
函数,它将具体的过程抽象出来, 给了它一个名字. 意味着我们可以使用这些函数去求任意函数的不动点,或是使用牛顿迭代法求零点.
感觉要写很多return
语句, 可能没有原来那么优雅.
1 | const dx = 0.00001; |
我看的视频是这个, 有中英字幕, 还不错.
如果放在一篇文章里面, 大家肯定都不想看了, 因此分了几篇内容, 希望读者可以挑自己有兴趣的部分,强烈建议读者至少先把SICP的第一章看完. 我为了自己加深理解也用JS重写了一次, 都贴了JS版本,不想下载mit-scheme
的朋友可以用JS的版本进行调试.
博客仅仅是把我学到的一点思考与大家分享, 如果有兴趣建议还是看看视频. 也不一定说非要学到什么东西,会不会对业务有什么实质性帮助, 观察一下不同的思考方式也是不错的. 里面也有一些哲学问题的思考,比如下面几个问题:
不要再去简单的说scheme语言是手写语法树, 只是它的表达形式很像语法树,我个人感觉它更像是从数学的角度或是电气工程的角度去看待和描述世界.
]]>有一篇文章也介绍了一些技巧, 写博客的时候正好搜到了, 正好也分享出来吧.
1 | # kubectl 的主要作用就是与ApiServer进行交互, 而交互的过程, 我们可以通过下面的方式来打印, |
这是我在这里学到的命令: Force Delete Evicted / Terminated Pods in Kubernetes
1 | kubectl get pods --all-namespaces --field-selector status.phase=Pending -o json | \ |
kubectl可以使用两种选择器, 一种是label, 一种是field, 可以看官网的介绍:
1 | # 它是一种选择器, 可以与上面的awk或者xargs配合使用. |
不知道有读者看过我的这篇文章: 基于kubernetes的PaaS平台中细力度控制pods方案的实现.均衡分布的工作前提是得知pod在各个机器的分布情况. 最好的办法就是我们得到pod信息之后进行简单的统计,这个工作可以使用awk
实现.
1 | kubectl -n default get pods -o wide -l app="nginx" | awk '{print $7}'|\ |
你可以理解为这个命令为K8s的ApiServer做了一层代理, 使用该代理, 你可以直接调用API而不需要经过鉴权.启动之后, 甚至可以实现kubectl
套娃, 下面是一个例子:
1 | # 当你没有设置kubeconfig而直接调用kubectl时 |
默认启动的proxy是屏蔽了某些api的, 并且有一些限制, 例如无法使用exec进入pod之中可以使用
kubectl proxy --help
来看, 例如
1 | # 仅允许本机访问 |
有人说这个kubectl proxy可能没什么作用, 那可能仅仅是你还没有实际的应用场景.例如当我想要调试K8s dashboard
代码的时候. 如果直接使用kubeconfig文件, 我没法看到具体的请求过程,如果你加上一层proxy转发, 并且设置-v=9
的时候, 你就自动获得了一个日志记录工具, 在调试时相当有用.
kubectl是一个强大的命令行工具, 上面我只是介绍了我工作中对其用法的一点探索, 也并不鼓励大家非要记住这些命令,只是希望当读者需要的时候, 能够想起来kubectl可以有类似的功能, 就不需要针对几个临时需求去研读client-api了.
]]>近期我接入了一个K8s系统, 系统对Ingress的支持不完善, 缩减了许多功能, 不过它支持完整功能的Istio, 因此我调研使用了下Istio, 试用其功能, 看看能不能满足我们的需求.我在接入系统时, 并没有特别深入Istio的底层, 仅仅是为了实现特定功能, 可能按照自己的想法来理解和使用, 有不对的地方也欢迎指出.
Istio是一个开放的, 与平台无关的服务网格, 提供了流量管理, 策略下发, 和远程收集能力.
直接从图中来看, Istio的功能就是集群中的流量管理功能, 附带了可视化界面. 因此如果Istio提供了对外出口, 也就可以像Ingress那样, 流量从一个入口进入, 再转发至不同pod中.
我测试使用的是minikube(1.2.0), 也用它来安装Istio的(1.6.8)
1 | kubectl -n istio-system get deploy |
Istio自带的Ingress, 在istio安装好之后,
kubectl -n istio-system get deploy istio-ingressgateway
有一个自带的ingress, 该ingress用于处理istio的入流量, 它可以当一个普通的Ingress服务, 也可以作为istio的gateway使用.
这个方案是官方实例中给出的(samples/bookinfo/platform/kube/bookinfo-ingress.yaml), 不过所有文件中, 只有这一个是kind: Ingress
的例子.
与之前我们用Nginx或是OpenResty创建的ingress服务是不太一样的, 因为他的gateway服务使用了Envoy(使用C++编写的服务代理工具, 类似Nginx), 但是到了istio中, ingress的配置做一些改变:
配置中中增加了/*
来匹配路径, 而不能像Nginx中通过前缀来分流我没有做进一步的测试, 不过既然从Nginx换成了Envoy, 一些配置理论上是不通用的.
这个方案官方实例中给出较多, 还包括怎么分流以及服务间互相调用. 对外服务创建时, 我们也需要指定域名, 路径规则以及后端服务,因此, 最精简的配置如下:
1 | # 指定要接收流量的gateway |
1 | # 依赖的是service中区分prod-arya-python3以及stage-arya-python3 |
使用DestinationRule相比于使用原生的Service拓展了一些功能, 而且能够根据服务的实例进行分组,我们目前基于版本的发布方式也比较容易的配合DestinationRule.
1 | # service中仅选择应用名称, 不再区分版本号 |
由于我们使用了istio, 为了以后能用到DestinationRule的一些特性, 应该要按照后面的这种方式,由DestinationRule的subset来进行分流.
从当前的调研使用来看, Istio的IngressGateway与普通的Ingress有一些区别, 不过很多功能可以用其替换.如果项目从一开始就打算使用Istio的话, 建议不要再去使用Ingress了, 可以直接采用Gateway+VirtualService的形式.我之后应该也会分享下用VirtualService以及DestinationRule可以实现的一些规则.
]]>我们的PaaS平台中允许用户提供机器, 由我们接入kubernetes, 而这部分用户自己的机器,他们希望这些机器的性能利用率能达到很优. 无论怎么依靠程序的分配策略,总是不如自己手动调整的效果最佳, 因此才会需要平台提供能够确定在每台机器部署确定Pod数目的功能.
上篇博客中我已经给出了可行的方案, 具体的实现是增加新的资源限制
书写控制策略时配合cpu以及mem来使用:
在调研了实现方案之后, 我就实现了一个可以修改自定义资源配置的功能, 但是给用户之后,反馈是十分难用, 修改起来工作量很大, 几乎完全不可用. 在组内讨论了方案之后, 我们决定,对于每一个应用, 如果用户想要指定每台机器部署多少个Pod, 那么, 最简化的配置应该是这样的:
1 | 应用1配置: |
增加了自定义资源之后, 程序部署时需要增加一步声明每台机器的资源数目, 下面的代码就是声明机器的自定义资源.
1 | def k8s_update_capacity(node, key, value=None): |
比如我们机器1
需要配置3个pod, 那么k8s_update_capacity('机器1', 'NS名称-app名称-版本名称', 3)
, 在创建deployments时, 将对应的资源请求设置为1, 就可以实现在机器1
中部署3个Pod, 当然, 你需要针对指定的所有机器都做一样的操作.
需要注意的是, 我们的平台支持两种类型的发布, 一种是滚动更新, 另一种是版本更新,我简单描述下两者有什么区别:
我觉得可以水一篇它们两个区别的博客
对于我们的实现中, 使用版本更新的应用, 资源标签就应该是NS-app_name-version_id
,对于滚动更新的应用, 资源标签应该是NS-app_name
.
为什么滚动更新的应用拥有自己的版本号, 但是不能加在标签中使用呢?
答:
我们PaaS平台在设计之初, 就是只有版本更新功能. 结合了k8s之后, 增加了滚动更新的功能,因此, 即使是使用滚动更新的应用, 依然是有版本号的, 只是没给k8s看到.
这个涉及到我们的资源使用情况, 我们应用占用的资源很多. 例如一台机器中, 运行4个Pod已经是极限了, 我们需要滚动更新策略, 删除几个再新建几个, 始终维持最大的Pod数目是4, 否则可能会造成资源使用过度(报警或是机器挂掉). 如果对于这种应用使用了携带版本号的标签. 也就是说, 同一时刻, 有可能存在一台机器上有 两个标签
NS-app_name-旧版本
,NS-app_name-新版本
. 这样, 很可能会在更新应用时 机器中新旧版本的Pod数目加起来超过4. 因此, 滚动更新的应用, 自定义资源一定不能携带版本号.
这两种更新的方式标签名也就决定了我们在更新自定义资源时, 需要采取不同的策略:
对于版本更新的应用:
部署时: 我们需要给待部署的的机器增加自定义资源配置, 因为携带了版本号, 所以新版本一定能部署到正确的问题; 更新结束后下线旧版本时: 需要删除旧版号本对应的资源设置.
对于滚动更新的应用: 部署时: 我们需要更新所有可能会部署到的机器资源配置, 因为用户可能此次更新修改了部署机器, 例如, 原有的host1部署了3个pod, 部署新的时指定host1不再部署应用, 那么就必须在部署前就将host1上面的自定义资源删除, 否则没有版本号的限制, pod依然有可能部署到host1; 更新结束后不需要进行其他处理
别的我也不说了, 同事一句话表明了效果.
目前该功能已经稳定上线运行了2个月, 程序按照预想行为进行.
使用了自定义资源限制之后, 一个比较麻烦的问题是扩缩容, 原有的扩缩容可以简单的修改replica实现, 但是现在, 需要同步更新一下机器中对应的资源数目.
扩容可以简单的同步更新机器资源配置, 然后修改replica数量来做到, 但是缩容是不行的,具体的表现为, 如果你调低了某台机器的自定义资源, 然后减少replica数目时,Pod的删除不一定会出现在对应的机器, 很可能误删其他机器的Pod, 只有重新部署可以缩容.
具体的原因需要去看replicaset的代码, 我下一篇博客会介绍下k8s缩容的策略, 顺便也能写写怎么调试control-manager的代码, 不知不觉, 通过这篇博客, 我好像又能水3篇博客出来~
主要针对滚动更新的应用, 当我们只有2个Pod, 而maxunavailable为25%时.此时, 只能容忍1/4容器不可用, 因此, 原有的2个Pod都不会被删除, 而新的Pod也无法被创建,会出现死锁, 具体的解决方案就是用户配置时确保maxunavailable * replica > 1
.
这个细力度控制功能的实现, 给我的最大感受就是, 调研了解决方案之后, 需要针对用户的需求,提供真正能用的上的功能, 而不是草草实现(既花了时间,用户也用不上).
另外就是针对不同的部署方案, 需要考虑这种基础功能的实现策略, 上面的实现中, 我省略了cronjob以及job的实现, 希望读者需要实现的时候自己考虑下, 应该比较简单.
]]>我们集群主要是针对Python应用的采样以及性能分析, 我们原有的未在kubernetes环境中的方案是pyflame, 但是它已经不维护了.组里的其他同事提出了用py-spy, 还在比较活跃的开发中, 而且功能比pyflame更多, 安装更为简单, 就转而使用py-spy了
它的top功能我比较喜欢, 可以马上确认当前堆栈信息.
在它的README中, 也介绍了如何在docker容器中运行py-spy, 需要增加PTRACE权限,另外一点就是也需要读取hostPID, 如果只能看到容器中自己的pid, 是无法取样的.
下面是pyflame的实现方案: https://github.com/monsterxx03/kube-pyflame/blob/master/kubectl-pyflame
1 | nodeName=$(kubectl get pod ${POD} -n ${NAMESPACE} -o jsonpath='{.spec.nodeName}') |
这是一种最为简单的实现方式, 需要注意的有两点, 这里的hostPID: true
以及securityContext: privileged: true
, 这就是给定了特殊权限.
根据上面的pyflame方法, 我们使用py-spy也差不多, 和上面配置很像, 但有一些地方是不同的
1 |
|
不同点有以下几个方面:
ttlSecondsAfterFinished
这个程序有几个工作:
我贴下主要的代码, 错误处理已经忽略
1 | def _run_profile_task(): |
安全性问题主要就是namespace的限制策略, 已经在上面的对比实现中给出了具体的原因以及方案.
这篇文章主要想介绍下我们在kubernetes环境中根据PaaS平台情况增加的性能分析工具,目前的情况止步于Python应用, 不过未来也很容易扩展到其他语言的程序.另外需要注意的一点就是特权容器的处理, 尽量少给权限, 而且也不要让用户看到.
在pyflame的实现方案中, 有一行这样的配置, 学习一下, 简直就是job任务以及pod容器的调试神器.
1 | - command: ['sh', '-c', 'while true; do sleep 10; done;'] |
在我们的功能上线后, 我也看到有这么一个取样工具, 它是基于kubectl的插件开发方式提供的.如果只是需要一个简单的取样工具, 那么没必要开发一整套系统, 可以尝试用用kubectl
的这个插件
Introducing Kubectl Flame: Effortless Profiling on Kubernetes
里面有这么一句话: Profiling is a non-trivial task.(性能分析是一项非同小可的任务.)
1 | kubectl flame mypod -t 1m -f /tmp/flamegraph.svg |
我顺便也读了读代码 https://github.com/VerizonMedia/kubectl-flame代码库不长, 我简单介绍下它的实现吧.
cli/cmd/kubernetes/root.go
入口代码在这里
1 | cmd := &cobra.Command{ |
Flame
函数就是具体的采样函数了, 我把错误处理全部省略了
1 | func Flame(cfg *data.FlameConfig) { |
后面的代码就不贴了, 就是为了启动一个Job. 直接快进到具体的profile代码
1 | // agent/profiler/python.go |
也是读代码库学到的…
语言 | 取样工具 |
---|---|
Java | async-profiler |
Python | py-spy |
Golang | bcc-profiler |
Ruby | rbspy |
wrk中并没有qps控制的选项, 它只能控制连接数目, 指定的连接数会平均分配到每个线程
1 | Usage: wrk <options> <url> |
例如./wrk -t8 -c1000
就是启动8个线程, 每个线程维持125个连接
1s内可以处理的请求数目被称为qps.
考虑这样一个问题, 假如你想拥有1000qps的效果, 而你的处理能力是0.2s一个请求, 假设服务器足够多,那么在这1s内,怎么能拥有1000次请求?
可以将1s分成5份(0.2s), 然后1000个请求在5份中平均的发生, 每一份就有200个请求, 因此我们需要200个并发连接.
这是大约估计的方案, 实际使用中, 可能需要注意一点, 如果你的请求响应本身就很快, 比如0.05s
, 那么可能并发估计没有那么准, 主要是因为请求链路上可能会有其他时间消耗, 如果我们使用200的并发连接, 200/0.05理论是应该有4000的qps, 但是其他耗时导致并发低于4000是很正常的.
我在使用wrk的时候, 并不是直接把请求数目增加到很高, 因为我们平时不一定有足量的后端机器, 一次性增加大量请求可能会导致服务可用性下降, 可以逐步增加请求数, 我是这么做的, 指定压测内容的响应时间0.1s, 再指定并发连接数, 使用时, 多跑几次脚本, 看到qps稳定后再继续增加.
1 | -- ./wrk -t10 -c400 -d3600s -T2s -s 4k.lua |
比如现在, qps已经稳定
响应时间也比较稳定
可能大家不太懂P50是什么意思, 最下面绿色的线表示 50%以上的请求都能在0.2s以内完成,
这样会得到稳定请求状态下的数据
希望读者能了解wrk参数的设置, 以及表现到Nginx中实际的效果, 可能有一天你压测的时候就能用到了.
近期压测Ingress主要是因为有个大应用会接入到我们的系统中,可能比原有所有应用的流量加起来都要多, 不压测的话, 用户使用的信心没有那么足.
压测脚本就是用的上面0.1s的数据, 机器的配置如下:
1 | 内核版本: 4.9.0-15-amd64 |
每个pod使用40个worker, 开启了gevent, CPU限制10个核(测试中利用率不到5个), pod数目在压测时尽量保证够用, 有15个
请求的接口如下, 压测时统一了等待时间为0.1
s
1 |
|
我针对Kubernetes的Ingress机器进行了一次压力测试, 主要测试各个请求量下, Ingress的各项指标测试时间: 11:30~12:00
并发数量 | cpu利用率 | mem利用 | 应用响应情况 |
---|---|---|---|
0 | 20% | 2.4G | 无请求 |
3.6k | 160% | 2.3G | 程序响应时间稳定 |
7.1k | 291% | 2.35G | 程序响应时间稳定 |
10.4k | 413% | 3.36 | 程序响应基本稳定 |
11.8k | 470 | 2.37G | 程序响应已经变慢, P50-180ms P90 244ms P99 470ms ,加了机器也没有提升 |
并发数量 | cpu利用率 | mem利用 | 应用响应情况 |
---|---|---|---|
13.2K | 490 % | 2.4G | 请求静态资源, 响应正常 |
这个程序在达到13~14k之后已经到了瓶颈, 这个时候, 我只能保留这个程序的请求量, 加入另一个程序用于压测.
另一个程序, 我没有再指定wait的等待时间, 希望这个程序可以尽可能快的返回, 让我得到尽可能高的并发.
并发请求时, 新应用的qps在25k左右,
此时的Ingress开始接近性能瓶颈,可以看到请求峰值时, cpu利用率在下降
14kqps(第一个应用) + 25k(第二个应用) 单机的Ingress的大概能允许40K左右的并发,达到这个阶段后, 主要瓶颈是在CPU. 如果CPU再好一点的话, 我觉得并发量可以更高.
如果觉得我压测方法不科学或者有其他想讲的, 可以在评论里面说, 我看看是不是过程有问题.
]]>