Coredns 源码阅读(导读)

周末看了一下 coredns 的源代码,目前为止只是差不多搞清楚了这个代码库的逻辑。写篇博客记录一下,如果你也想要阅读这个库的源代码的话,这篇博客可以节省你的一些时间。

coredns 介绍

代码库地址:https://github.com/coredns/coredns

coredns 是一个 dns server,简单来说就是这个软件启动之后可以监听一个端口,然后你将 dns 查询请求发给这个端口,它可以告诉你 dns 解析的结果。所以作为任意一种 dns server(DNS Authoritative Nameserver, 或者 Recursive Resolver Server) 工作,我认为都是没有问题的。

这个代码库值得一读的理由有:

  1. coredns 以 plugin 的形式工作,除了 plugin 之外部分的代码很少,而且那部分代码其实不必阅读也可以(在读完本文之后);plugin 作为 first-class citizen 的好处是,职责清晰,一个 plugin 只做一件事情。比如 metrics,trace 这种东西,都是以 plugin 的形式存在的。这样,阅读起来非常方便;
  2. 每个 plugin 完成自己的事情,阅读起来难度低。而且这个库要求的背景知识很少,不需要懂很多网络的协议。DNS 相关的协议部分,看到哪里不懂的时候再查就好了。

不好的地方:

  1. 这个服务是基于一个 coredns 自己 fork 维护的 caddy server 来实现的,就导致可能需要去看 caddy 部分的代码。而且 coredns 自己维护的 caddy 已经脱离最新版了,我发现用的 caddy plugin 已经在官方的代码库中删除了。所以有些地方理解起来可能不简单。

看代码之前的准备工作(推荐)

如果之前没有用过的话,建议看代码之前先:

  1. 看完 manual:https://coredns.io/manual/toc/,可以知道使用方法,和大体的工作原理;
  2. 看下 plugin 的写法:https://coredns.io/2016/12/19/writing-plugins-for-coredns/

编译方法

因为 golang 是编译型的语言,所以无法像 Python 那样动态安装、加载插件。要新增插件,必须重新编译,将 plugin 的代码编译进二进制。

插件的列表在 plugin.cfg 中。如果新安装插件,需要将新插件写入这个列表,然后运行 go generate coredns.go 命令,重新生成这两个文件:

编译的命令是: CGO_ENABLED=0 go build -v -ldflags="-s -w -X github.com/coredns/coredns/coremain.GitCommit=3288b111-dirty" -o coredns. GitCommit 作为变量注入进 binary,运行的时候打印。

程序入口

coredns.go 里面 import 了 plugin,然后调用了 coremain.Run

coremain.Run 里面只是处理了一些和 Version(用于 Print)的信息,和命令和参数。最后启动了 Caddy server。

coredns server 的逻辑在 coredns/core/dnsserver. 但是中间涉及和 caddy v1 的交互,比如 MakeServers() , 是 caddy 里面的接口。应该也不是很重要。

server 里面的入口应该是 Serve()。基本的逻辑是拿到 plugin chain,然后调用第一个 Plugin, 调用 Plugin 的 ServeDNS()

Plugins

Plugin Chain, 顾名思义,是 Chain 在一起的。并不是在 server 里面一个 for 循环调用所有的 Plugins,而是 Server 只会调用第一个 Plugin。

第一个 Plugin 可以处理 DNS 请求,返回结果。如果像是 metrics 这样不负责逻辑的 Plugin,可以在完成自己要做的事情之后,去调用 coredns/plugin/plugin.go 里面的 NextOrFailure 函数,交给下一个 Plugin 去处理。和 ASGI 一样,turtles all the way down.

Plugin 主要做两件事情:

  1. 通过 init() 函数将自己注册进去,也可以做一些初始化的工作。比如支持配置。Corefile 中的配置可以在 init 的时候通过 setup 接口读到;
  2. ServeDNS() 主要的函数入口。处理 DNS 请求。

whoami 这个 Plugin 返回查询者的 IP 端口等信息,逻辑比较简单,建议从这个 plugin 开始看。

 

SRE 的工作介绍

有很多人问过我想了解一下 SRE 这个岗位,这是个很大的话题,在这篇博客中把想到的一些介绍一下吧。

SRE 到底是什么?这是一个最早由 Google 提出的概念,我的理解是,用软件解决运维问题。标准化,自动化,可扩展,高可用是主要的工作内容。这个岗位被提出的时候,想解决的问题是打破开发人员想要快速迭代,与运维人员想要保持稳定,拒绝频繁更新之间的矛盾。

SRE 目前对于招聘来说还是比较困难。一方面,这个岗位需要一定的经验,而应届生一般来说不会有运维复杂软件的经历;另一方面就是很多人依然以为这就是“运维”工程师,认为做的是一些低级重复的工作,对这个工作有排斥。最根本的,其实这个岗位寻找的要么是具有运维经验的开发人员,要么是具有软件开发技能的运维工程师。所以比较难以找到合适的人。

在现实生活中,不同公司的 SRE 岗位大有不同,有一些甚至可能还是传统运维的名字换了一个岗位名称。

比如蚂蚁金服有两种 SRE,一种是负责稳定性的,就是大家所理解的 SRE;另一种叫做资金安全 SRE,并不负责服务正常运行,而是负责金钱数目正确,对账没有错误,工作内容以开发为主,主要是资金核对平台和核对规则(没有做过,只是个人理解)。某种意义上说,已经不算是 SRE 而是专业领域的开发了。

Netflix (2016年)的模式是谁开发,谁维护。SRE 负责提供技术支持,和咨询服务。Netflix 在全球 170 个国家有服务,Core SREs 只有 5 个人。

微软有专门的 Game Streaming SRE,负责 XBox 在线游戏的稳定性。

所以不同公司的 SRE 的内容各有偏重,取决于公司要提供什么样的服务。

我们可以学习网络分层的方式,将 SRE 大致的工作内容从下往上分成 3 个大类:

  1. Infrastructure:主要负责最基础的硬件设施,网络,类似于 IaaS,做的事情可参考 DigitalOcean
  2. Platform:提供中间件技术,开箱即用的一些服务,类似于 PaaS,做的事情可参考 Heroku, GCP, AWS 等
  3. 业务 SRE:维护服务,应用,维护业务的正常运行

Infrastructure

Infrastructure 和 Platform SRE 其实可有可无,这些年商业化的服务其实越来越多了,比如,如果公司选择全部在 AWS 部署自己的服务的话,那么就不需要自己建立 Datacenter,维护网络之类的工作了,只需要几个 AWS 专家即可。

如果有的话,工作内容也可大可小。可以从管理购买的 VPS 开始,也可以从采购硬件服务器开始。

我觉得 Infrastructure SRE 的工作内容可以这样定义:

  1. 负责服务器的采购,预算,CMDB 管理。要知道(能查询到)每一台的负责人是谁,在干什么。这个非常重要,如果做不好,会造成极大的资源浪费。
  2. 提供可靠软件的部署环境,一般是虚拟机,或者 bare mental。
  3. 操作系统的版本统一维护,Linux 发行版的版本,Kernel 的版本等。
  4. 维护机器上的基础软件,比如 NTP,监控代理,其他的一些代理。
  5. 提供机器的登录方式,权限管理,命令审计。
  6. 维护一套可观测性的基础设施,比如监控系统,log 系统,trace 系统。
  7. 维护网络,大公司可能都会自己设计机房内的网络。其中包括:
    1. 网络的连通,这个是必要的。对于上层用户(Platform SRE)来说,交付的服务应该是任意两个 IP 是可以 ping 通的,即管理好 3 层以下的网络。
    2. NAT 服务
    3. DNS 服务
    4. 防火墙
    5. 4 层负载均衡,7层负载均衡
    6. CDN
    7. 证书管理

每一项既可以是一个很大的团队,也可以只有一个人去对商业化的 Infra 服务。可以使用开源的产品,也可以自己研发。

Platform SRE

Infrastructure SRE 维护的是基础设施,Platform SRE 使用他们提供的基础设施建立软件服务,让公司内的开发者可以使用开箱即用的软件服务,比如 Queue,Cache,定时任务,RPC 服务等等。

主要的工作内容有:

  1. RPC 服务:让不同的服务可以互相发现并调用
  2. 私有云服务
  3. 队列服务,比如 Kafka 或者 RabbitMQ
  4. 分布式的 cronjob 服务
  5. Cache
  6. 网关服务:反向代理的配置
  7. 对象存储:s3
  8. 其他一些数据库:ES,mongo 等等。一般来说,关系型数据库会有 DBA 来运维,但是 NoSQL 或者图数据库一般由 SRE 维护。
  9. 内部的开发环境:
    1. SCM 系统,比如自建的 Gitlab
    2. CI/CD 系统
    3. 镜像系统,比如 Harbor
    4. 其他的一些开发工具,比如分布式编译,Sentry 错误管理等等
  10. 一些离线计算环境,大数据的服务

业务 SRE

有了 Platform SRE 的支持,开发人员写代码就基本上不需要关心部署的问题了。可以专注于开发,使用公司开箱即用的服务。这一层的 SRE 更加贴近于业务,知道业务是怎么运行的,请求是怎么处理的,依赖了哪些组件。如果 X 除了问题,可以有哪些降级策略。参与应用的架构设计,提供技术支持。

主要的工作内容有:

  1. 参与系统的设计。比如熔断、降级,扩容等策略。
  2. 做压测,了解系统的容量。
  3. 做容量规划。
  4. 业务侧的 Oncall。

对于一个专业的 SRE 来说,上述技能也不应该有明显的界限,比如说业务 SRE 也需要掌握一些网络技能,Infra SRE 也要写一些代码。很多工具每一个岗位的人都多少用的到,比如 Ansible/Puppet/SaltStack 这种 IT 自动化工具,或者 Grafana/Prometheus 这种监控工具,只有理解才能用的正确。换个角度讲,对于业务 SRE 来说,虽然基本上不会去管理四层以下的网络,但是如果遇到网络问题,能通过已有的工具和权限排查到交换机问题,去找 Infra SRE 帮忙:“请帮我看下 xx IP 到交换机是否有异常,因为 xxx 显示的结果是 xx”,总比 “我怀疑 xx 有网络问题,请帮忙排查下” 要好一些吧?

以上是工作职责的大体划分,这个分层其实没有什么意义,倒是可以让读者了解一下 SRE 都涉及哪一些工作。

下面是一些日常的工作内容。

部署服务

部署分成两种:

  1. Day 1:将服务部署上线的那一天
  2. Day 2+:服务部署之后,还会进行很多更新,升级,配置更改,服务迁移等等

Day2+ 的工作要做很多次,Day 1 做的很少,在不断的迭代升级之后,还能保证有一个可靠的 Day 1 操作是很难的。换句话说,我们在服务部署之后一直改来改去,还要保证这个服务在一个全新的环境能够可靠的部署起来。部署环境的硬编码,奇奇怪怪的 work around,都会破坏 Day 1 的可靠性。之前一家公司,扩容一个新机房的过程简直是噩梦,太多的奇怪配置,hardcode,导致踩过无数个坑才能在一个新的机房部署起来全部的服务。

Day2+ 的操作也不简单,主要要关注稳定性。对于重要的变更操作要设计好变更计划,如何做到灰度测试,如果出了问题应该如何回滚,如何保证回滚可以成功(如何测试回滚)等等。

部署的操作最好都是可以追踪的,因为并不是所有会引起问题的操作都会立即引起问题。比如一个操作当时做完没有什么问题,但是过了 1 个月,偶然的重启或者内存达到了某一个指标触发了问题。如果能记录操作的话,我们可以回溯之前做过的变更,方便定位问题。现在一般都用 git 来追踪部署过程的变更(gitops)。

Oncall

Oncall 简单来说就是要保证线上服务的正常运行。典型的工作流程是:收到告警,检查告警发出的原因,确认线上服务是否有问题,定位到问题,解决问题。

收到告警并不总意味着真正的问题,也有可能告警设置的不合理。告警和监控面板并不是一个静态的配置,它应该是每天都在变化的,时刻在调整的。如果发现没有标志真正线上问题的告警发了出来,就应该修改告警规则。如果发现当前的监控无法快速定位问题,应该调整监控面板,添加或者删除监控指标。业务在发展,请求量在变化,某些阈值也需要不断地调整。

定位问题没有一概而论的方法了,需要根据看到的实时,结合自己的经验,然后做推测,然后使用工具验证自己的推测,然后确定问题的根因。

但是解决问题是可以有方法论的,叫做 SOP,标准操作流程。即:如果出现了这种现象,那么执行那种操作,就可以恢复业务。SOP 文档应该提前制定,并且验证其有效性。

需要注意的是上述定位问题、解决问题并没有顺序关系。一个经常犯的错误是,在出现故障的时候,花了很长时间定位到故障的根因,然后再修复。这样花的时间一般会比较长。正确的做法是先根据现象看现有的 SOP 能否恢复业务。比如说当前错误只发生在某一个节点上,那么就直接下线这个节点,具体的原因后面再排查。恢复当前的故障永远是第一要务。但是恢复操作也要经过测试,比如猜测可以通过重启解决问题的话,可以先重启一台做测试,而不是一次性将所有服务重启。大部分情况是需要临场分析的,是一个紧张又刺激的过程。

故障到底多久恢复算好?出现多少故障是可以容忍的?怎么标志服务的稳定性到底如何?我们使用 SLI/SLO 来衡量这些问题。

制定和交付 SLI/SLO

维护服务等级协议,听起来像是一个非常简单的事情,只要“设定一个可用率”然后去实现它就好了。然而现实的情况并不是。

比如,制定可用率的时候,并不是说我们去“实现4个9”(99.99% 的时间可用)就够了,我们有以下问题要考虑:

  1. 如何定义这个可用率?比如我们以可用率 > 99.9% 为目标,有一个服务部署了 5 个 Zone, 那么有一个 Zone 挂了,其余的 Zone 是可用的,那么可用率被破坏了吗?这个可用率是每一个 Zone 的还是所有的 Zone 一起计算的?
  2. 可用率计算的最小单位是什么?如果 1min 内有 50s 没有达到可用率,那么这一分钟算是 down 还是 up?
  3. 可用率的周期是怎么计算的?按照一个月还是一个周?一个周是最近的 7 天还是计算一个自然周?
  4. 如何对 SLI 和 SLO 做监控?
  5. 如果错误预算即将用完,有什么措施?比如减少发布?如果 SLI 和 SLO 没有达到会怎么样?

等等,如果这些问题不考虑清楚的话,那么 SLI 和 SLO 很可能就是没有意义的。SLI/SLO 也适用于对公司内部用户的承诺,让用户对我们的服务有预期,而不能有盲目的信任。比如 Google 在 SLI/SLO 还有预算的时候,会在满足 SLI/SLO 的时候自行对服务做一些破坏,让用户不要对服务有 100% 可用的错误预期。SLI/SLO 也会让 SRE 自己对当前服务的稳定性有更好的认识,可以根据此调整运维、变更、发布计划。

故障复盘

故障复盘的唯一目的是减少故障的发生。有几个我目前认为不错的做法。

故障复盘需要有文档记录,包括故障发生的过程,时间线的记录,操作的记录,故障恢复的方法,故障根因的分析,为什么故障会发生的分析。文档应该隐去所有当事人的姓名对公司的所有人公开。很多公司对故障文档设置查看权限,我觉得没什么道理。有些公司的故障复盘甚至对外也是公开的

故障在复盘的时候应该将当事人的名字用代码替代,可以营造更好的讨论氛围。

不应该要求所有的故障复盘都产生 Action。之前一家的公司的故障复盘上,因为必须给领导一个“交待”,所以每次都会产生一些措施来预防相同的故障再次发生,比如增加审批流程之类的。这很扯,让级别很高的领导审批他自己也看不懂的操作,只能让领导更痛苦,也让操作流程变得又臭又长,最后所有人都会忘记这里为什么会有一个审批,但是又没有人敢删掉。你删掉,出了事情你负责。

Blame Free 文化?之前我认为是好的。但是后来发现,有些不按照流程操作导致的问题确实多少应该 Blame 一下,比如下线服务的时候没有检查还没有 tcp 连接就直接下线了,或者操作的时候没有做 canary 就全部操作了,这种不理智的行为导致的故障。但是条条框框又不应该太多,不然活都没法干了。

容量规划

容量规划是一个非常复杂的问题,甚至有一些悖论。容量要提前做好规划,但是容量的规划需要知道业务的扩张速度,扩张速度这种事情又不是提前能计划好的。所以我一直觉得这个事情很难做,也一直没有见过做的很好的例子。

但是至少可以对维护的系统建立一个模型,知道多少机器,多少资源,能容纳多少容量。这样遇到大促之类的活动也能及时估算需要的资源数量。

用户支持

用户支持也是日常的一部分。包括技术咨询,以及用户要求的线上问题排查。

这里就需要提到文档的重要性了。如果没有维护好文档,那么用户就会一遍又一遍问相同的问题。写文档也是一个技术活,优秀的需要很长时间的积累。文档也需要经常更新。我一般会这样,保持这样一种状态:用户可以不需要任何人就从文档中找到他需要的所有答案。如果我发现用户的问题无法从文档中找到,或者难以找到在文档中的什么地方,就会更新文档,或者重新组织文档。如果用户的问题已经从文档中找到,那么就直接发文档给他。如果用户的问题显然是文档看都没有看过(有很多人根本不看文档的,只看文档是谁写的然后径直去问这个人),就直接忽略。

优秀的文档应该尽量引入少的专有名词,少使用没有用处的专业词汇描述,只描述具有指导意义的事实,假定用户没有相关的背景知识,列举使用例子,举一些现实会用到的例子而不是强行举例子,明确 Bad Case。等等。这其实是一个很大的话题了,这里就不展开了。

暂时就想到这一些了。下面写一些我经常见到的误解,和经常被别人问的问题。

 

有关做项目没有专业团队得不到训练。

这方面是听到最多的抱怨。虽然说 SRE 在工作上应该是开发时间和运维时间各 50%,但是真实的情况是,即使 SRE 有一些开发工作,也大部分是面向内部用户,面向公司内部的开发者的。大部分项目是一些想法,需要去尝试一下行不行,基本上不会有专业的设计资源,PM 资源。这种项目就需要 SRE 有多方面的技能,包括对产品的理解,清楚地知道它有什么痛点,最好是自己经历过的痛点,然后需要懂设计,管理好开发进度。然而这种人非常少。其实能写中型项目代码的 SRE 就已经非常少了。所以大部分公司内部项目都会做的又难用又复杂。

即使是有专业配套 PM 和设计,甚至前端资源。基本上也是一个灾难。我也经历过这样的团队。这种内部项目对标的不是互联网项目,而更像是 toB 的项目。用户 UI 的设计,交互逻辑,操作流程,交付周期等需要的都是另一个领域的知识。否则的话人越多,也只会徒增沟通成本,拖慢项目进度。

回到经常听到的这个抱怨,说在 SRE 的团队没有像开发团队那样有“正规军”,有设计和 PM,大家各司其职,后端开发只要对齐 API 然后实现就好了。大部分的应届生会有这样的幻想,但实际上不是这样。被搞错的最重要的一点是,学习主要是靠自己的,和别人没有太大的关系。我觉得可能是在一个大团队里面,有很多人一起做一件事情,心里的怀疑和焦虑会少一点,人们会对这样的工作状态感到踏实,误以为是“成长”,自己做所有的工作焦虑更多。

事实是,在大团队工作可能学到更多的沟通技能,比如和不同的人对齐不同的阶段工作目标,要想要学到其他的东西还是要靠自己。比如拿到一个设计,如果照样子去实现了,其实不会学到什么东西。而要去理解为什么这么设计,为什么不那么设计。如果自己去做,思考的过程也基本是这样的,可以怎么设计,选择什么好。都是:思考,选择,尝试,经验,思考……

另一个需要澄清的误区是,模仿并不是学习。在团队中经历了一个设计,如果记住了这个设计,下次碰到类似的问题也用这个设计去解决。这也不能叫做是学习。我见过有在业务部门做过支付的 SRE 写的代码,在内部系统中去实现了订单业务的订单、交易等概念完成一个运维流程,甚至 Model 的名字都没改过。拿着锤子找钉子,会让系统变得更加糟糕和复杂。

总之,工作分的细并不代表工作就会更加专业。一个人身兼数职也可以在每一个方面做得很专业。重要的是不断学习,使用正确的做事方式,向优秀的项目和优秀的开发者学习。

 

有关脏活累活。

每一项工作都会有脏活累活:学不到什么东西,做起来没有意思。可能是整理系统的监控,可能是整理现有的文档,可能清理一些年久的运维脚本,可能是需要和不同的团队做一些沟通工作等。

这是不可避免的,如果可以的话,学会从每一项工作中找一些偷懒的方法吧,比如用脚本处理一些工作,用更聪明的方式工作等等。

但是如果这种工作的比例太高的话,就要思考工作方式的问题了。如果陷入恶性循环,看能不能从工具和工作流程上做一些改变。如果不能的话,考虑换一份工作吧。

 

有关背锅。

互相甩锅的工作环境无疑是非常糟糕的工作环境。如果相同的团队、或者不同的团队之间需要相互勾心斗角的话,如果工作环境不允许大方承认(SRE 无可避免地会犯一些错误)自己的错误,说明公司营造的氛围有问题。比如某些公司规定,发生 P1 级别的错误就必须开除一个 Px 级别的员工,发生 P0 级别的错误就必须开除一个 Py 级别的员工一样。如果是这种情况的话,公司实际上是在用一种懒惰地方法通过提高人的压力来提高系统的稳定性。有没有效果不知道,但是确定的是不会有人在这种情况下工作的开心。建议换一份工作。

 

如何转行?

其实难度没有想象的高,毕竟大学里面没有一个叫做 SRE 的专业。SRE 要求的知识也是编写代码、设计系统、了解操作系统和网络等。所以在大学里面将本科的课程好好学好,尝试做(并维护)一些自己的项目,毕业的时候基本上就满足要求了。非科班的人要转行的话,也可以参考大学的课程内容去补足这方面的知识。

需要注意的是,培训班出来的做开发完成业务可能够,但是做 SRE 远远不够。SRE 不仅需要 make things work,还要知道背后的原理。

 

面试会问什么?

我觉得和后端开发的面试内容基本上差不多。

如果是去应聘的这个岗位所需要的一些技能,比如 K8S,监控系统等,可能也会问一些领域内的知识。虽说这部分工具性的东西都可以学习,但是如果人家要一个经验丰富的、或者入职就能干活的,那么面试成功的机会就会小很多。当然,也不必沮丧,这是市场的供需关系决定的,如果对方执意要找符合特定要求的候选人,那么对方的选择的范围也会小很多,不必因为错失了这种机会而后悔没去学习什么工具。话又说回来,技能越多,选择也会越多。

排查错误可能是转行做 SRE 最大的一个门槛,这个需要一些经验。如果没有经验的话,就补足一些操作系统的知识,这样遇到未知的问题也可以通过已知的知识和工具去排查。

这个仓库是一个不错的面试题集锦:https://github.com/bregman-arie/devops-exercises

 

做 SRE 需要会写代码吗?

会,而且写代码的要求并不会比一个专业的后端开发低。

 

选择大公司还是小公司?

这属于两种截然不同的工作环境。小公司一般都有一个救火英雄式的人物,在公司的时间比较长,知道所有组件的部署结构,什么都懂。跟着这种人学习会成长很快。

大公司细分领域很多。本文前面列出的内容可能每一项在大公司中都是一个团队,对某个领域可以深入研究。

所以还是看想要做什么了。我个人比较喜欢靠谱的小公司,或者大公司中靠谱的小团队。

 

如何判断一家公司是否靠谱?

对于 SRE 这个职位,我总结了一些判断的技巧。比如可以判断一下对方目前的业务和 SRE 员工的数量是否处于一个“正常”的状态,人数是否在随着业务(机器的数量)现象增长?这是一个不好的迹象。是否 SRE 的数量过多?如果 SRE 人太多,有两个可能的原因:1)某个领导为了扩大自己的影响力在为一些“不必要的”岗位招人,这样会导致人多事少,大家开始做一些奇奇怪怪的事情,发明奇奇怪怪的需求,以各种各样的方式浪费自己的时间来领公司的工资;2)这个公司的基础太差,大部分工作都是需要人力运维,导致基本上有多少机器就需要多少人。总之,都不是什么好事情。

一些技术比较好的公司,都没有庞大的 SRE 队伍,比如 Instagram, Netflix(现在可能人数不少了),以及一些创业公司,甚至都可以没有专门的 SRE,优秀的 SRE 首先要是开发者,优秀的开发者也离 SRE 不远了。一些耳熟能详的服务,比如 webarchive 这样的数据量,其实背后也只有几个人在维护。前几年面试了国内的一家公司,在机房遍布全球,业务已经发展的比较庞大(上市了)的时候,SRE 团队也只有 10 个人。

另外我比较喜欢问的一个问题是对方关于 AIOps 怎么看。因为我之前搞了两年这个东西,最后得到的结论是,这基本上是个浪费时间、欺骗上层领导的东西。AI 这东西的不可解释性本质上就和运维操作将就因果相违背的。所以经常喜欢问面试官怎么看这个技术,基本上就可以判断靠不靠谱。当然了,这是我个人的职业阴影导致的后遗症,只能代表个人意见。

 

就说这么多吧,都是一些个人理解,不一定对。写这篇文章感觉自己好像指点江山一样,其实我自己也干了才几年而已,所以本文内容仅供参考。如果有什么问题可以在评论提出,我能回答的话就尽量回答。

 

多租户环境中的 TCP 限速(基于 iptables)

我们有个服务以类似 SideCar 的方式和应用一起运行,SideCar 和应用通过 Unix Domain Socket 进行通讯。为了方便用户,在开发的时候不必在自己的开发环境中跑一个 SideCar,我用 socat 在一台开发环境的机器上 map UDS 到一个端口。这样用户在开发的时候就可以直接通过这个 TCP 端口测试服务,而不用自己开一个 SideCar 使用 UDS 了。

因为所有人都要用这一个地址做开发,所以就有互相影响的问题。虽然性能还可以,几十万 QPS 不成问题,但是总有憨憨拿来搞压测,把资源跑满,影响别人。我在使用说明文档里用红色大字写了这是开发测试用的,不能压测,还是有一些视力不好的同事会强行压测。隔三差五我就得去解释一番,礼貌地请同事不要再这样做了。

最近实在累了。研究了一下直接给这个端口加上 per IP 的 rate limit,效果还不错。方法是在 Per-IP rate limiting with iptables 学习到的,这个公司是提供一个多租户的 SaaS 服务,也有类似的问题:有一些非正常用户 abuse 他们的服务,由于 abuse 发生在连接建立阶段,还没有进入到业务代码,所以无法从应用的层面进行限速,解决发现就是通过 iptables 实现的。详细的实现方法可以参考这篇文章。

iptables 本身是无状态的,每一个进入的 packet 都单独判断规则。rate limit 显然是一个有状态的规则,所以要用到 module: hashlimit。(原文中还用到了 conntrack,他是想只针对新建连接做限制,已经建立的连接不限制速度了。因为这个应用内部就可以控制了,但是我这里是想对所有的 packet 进行限速,所以就不需要用到这个 module)

完整的命令如下:

第一行是新建一个 iptables Chain,做 rate limit;

第二行处理如果在 rate limit 限额内,就接受包;否则跳到第三行,直接将包 DROP;

最后将新的 Chain 加入到 INPUT 中,对此端口的流量进行限制。

有关 rate limit 的算法,主要是两个参数:

  1. --hashlimit-upto 其实本质上是 1s 内可以进入多少 packet,50/sec 就是 20ms 一个 packet;
  2. 那如何在 10ms 发来 10个packet,后面一直没发送,怎么办?这个在测试情景下也比较常见,不能要求用户一直匀速地发送。所以就要用到 --hashlimit-burst。字面意思是瞬间可以发送多少 packet,但实际上,可以理解这个参数就是可用的 credit。

两个指标配合起来理解,就是每个 ip 刚开始都会有 burst 个 credit,每个 ip 发送来的 packet 都会占用 burst 里面的 credit,用完了之后再发来的包就会被直接 DROP。这个 credit 会以 upto 的速度一直增加,但是最多增加到 burst(初始值),之后就 use it or lost it.

举个例子,假如 --hashlimit-upto 50/sec --hashlimit-burst 20 的话,某个 IP 以匀速每 ms 一个 packet 的速度发送,最终会有多少 packets 被接受?答案是 70. 最初的 20ms,所有的 packet 都会被接受,因为 --hashlimit-burst 是 20,所以最初的 credit 是 20. 这个用完之后就要依赖 --hashlimit--upto 50/sec 来每 20ms 获得一个 packet credit 了。所以每 20ms 可以接受一个。

这是限速之后的效果,非常明显:

 

介绍 Lobbyboy 项目

上周末写了一个小工具,叫做 Lobbyboy(源代码:https://github.com/laixintao/lobbyboy )简单的来说,就是一个 ssh server,启动之后监听一个端口。你用通用的 ssh 客户端连接上以后,它会为你开一台 VPS,然后帮你打开一个 shell。等你用完 logout 之后它会再帮你销毁 VPS。

很多人会有疑问为啥需要这么个东西,其实这个只是解决一小部分人(指我自己)的需求,它并不是将不可能的事情变成可能,而是将可能、但是有些复杂的事情变得简单。这篇文章就来讲一讲为什么我将它实现成现在的样子,抓手是什么,底层逻辑是什么,形成了怎样的闭环,我的思考在哪里。

它是解决什么问题的?

它是解决运维人士经常需要使用一些服务器执行临时任务,比如编译乱七八糟的东西,跑一个数据清理,跑爬虫等,要经常开服务器,用完之后关掉这种场景;

还有一种场景是需要新的服务器进行验证(称之为验证场景),比如需要高规格的 CPU,需要 GPU,需要公网 IP,需要 IPv6,需要特定版本的 Ubuntu 发行版,甚至 Mac Mini(aws 提供的服务),或者 Windows(Azure提供的服务)等等。因为 Lobbyboy 提供了一种接口“协议”,三句话,我让男人三个接口,可以对接任意的 VPS 提供商,几乎可以做任何事情,构建出来任何的机器;测试完之后,想要拍屁股走人,不想要恢复原状(我是一个 Chaos Engineer!)。

实现一个新的 Provider(即,接入一个新的云厂商)只需要实现 Provider 的三个接口:

  1. 告诉lobbyboy 如何创建虚拟机 new_server
  2. 如何登录虚拟机 ssh_server_command
  3. 以及如何销毁虚拟机 destroy_server

所以任何能够提供 Linux 的地方都可以成为 Provider,比如说 Vagrant,VirtualBox,甚至感觉 Docker 也可以。lobbyboy 会内置一些 Provider,比如说 Vagrant(也作为一个内置的实现参考)。

用户实现的 Provider ,也可以考虑 patch 给 Lobbyboy,或者作为一个独立可安装的 pypi package 存在。

想到的还有一种场景(称之为吃火锅场景),就是你和你的好兄弟在吃火锅,这时候你正在上大学的表弟问你这行 Python 代码的输出结果是啥:”foo” == “bar” == False,虽然你 90% 确定,但是又不敢贸然回复,万一说错了,你在表弟心中“计算机大神”的人设就毁了。所以这时候你想找一台 Linux 跑跑试试。

 

上述需求能否用虚拟机实现呢?

运维人士需要经常背着电脑到处跑,所以选择笔记本通常会选择最轻薄的,方便 24 小时 OnCall. M1现在很火,但是 M1 上的虚拟机还不是很好用。Virtualbox 不支持 M1,UTM 貌似是支持的,但是这玩意不支持 vagrant。所以对 M1 用户来说,虚拟机就挺麻烦的。

另外值得一提的是,Lobbyboy 是支持 vagrant 的(vagrant provider,可以作为其他 provider 的一个实现参考),如果你在本地开一个 Lobbyboy,每次 ssh 到这个端口的时候,就会自动帮你开一台出来,省去了你 vagrant init; vagrant up; vagrant ssh; vagrant destroy 这些命令。

 

为什么不写一个命令行工具帮你开启/销毁机器?

有三个原因:

  1. 这样的话用起来还是需要安装,每次用的时候都要执行一堆命令,比较繁琐;
  2. Lobbyboy 有一个功能是,自动在 VPS 即将进入下一个计费周期的时候销毁。比如说 DigitalOcean 是按小时收费的,如果你一台机器用了 10 分钟,然后第 40 分钟的时候又想用,那么你还可以继续用。等到第 55 分钟了,Lobbyboy 发现机器没有在用了(没有 active session),就会给你销毁,这样你就付了一个小时的钱。假如第 59 分钟还有人在用的话,那么就等到第 119 分钟再尝试销毁。我们吃了一碗粉,就付一碗的钱;付了两碗的钱,就吃两碗粉。这种 Feature 必须有一个常驻的 daemon 来实现(crond 也是 d);
  3. cli 无法实现上述的“火锅场景”。而如果作为一个 ssh server 的话,无论你用 ipad,iPhone,Chromebook 等等等等,只要你有一个 ssh 客户端,你就可以用。

其实 Lobbyboy 还支持不同的 ssh key 策略,比如每次创建都加上你的 key,或者每次创建都自动生成一个新的 key(毕竟,你登录的是 Lobbyboy,Lobbyboy 登录新的 VPS,所以你的 key 其实不需要在 VPS 上面)。

 

为什么不一直使用一台 VPS?

是否可以 ssh 到一台你已经有的 VPS 上去做事情呢?无法满足“验证场景”。比如做一些具有破坏性的实验而不影响其他的服务。当然你也可以说,可以开一台备用的 VPS 一直放在那里,但其实也有一些不方便:1)费钱。2)如果做完一些事情不想清理,那么还是要再重新开一台。那又要用到 Lobbyboy 了。

 

为什么不使用 Homelab

有人在家里搭了一些服务器,是否可以直接使用 Homelab 完成上述需求呢?也是一样的问题。不过抛开电费和维护成本到底会不会比 VPS 更便宜不说, Homelab 可能可以实现每次帮你开新的虚拟机。不过这样 Homelab 就成了 Lobbyboy 的一个 Provider(所有能提供 Linux 的地方都可以成为 Provider)。所以要解决的问题也不是一样的。

可能上述方案都可以满足大部分人,即并不需要一个这样的反向代理。但是如果你有和我一样的使用习惯的话,那么可以试试 Lobbyboy。

 

为什么不直接用 openssh 的 server?

其实 openssh + 一个脚本几乎可以实现相同的功能。openssh 有一个功能,就是每次连接上就自动给你执行 /etc/sshrc 脚本。这样如果不从头实现一个 ssh server,只写一个 ssh 连接之后的脚本,也是可以的。但是我想了想,缺点有以下几点:

  1. 配置复杂,毕竟机器本身还有一个 ssh server,你要保证有另外一个端口能登录机器,而不是永远执行脚本,两个 sshd 进程要分别配置;
  2. 安装麻烦,要安装脚本,配置脚本。如果要实现机器回收的功能,还要去配置 crontab;
  3. Lobbyboy 因为只有一个 server 进程,所以它知道当前有哪些 active 的 session(全局变量),如果用 sshd 的话就要想别的办法查询活跃的 session,不同进程之间的交互也要通过 IPC 来解决;

但是 Lobbyboy 这种实现方式也有一些缺点:

  1. 多写很多代码,要处理 ssh 协议的部分,好在 paramiko 都已经做好了大部分。
  2. 有些功能是需要询问用户的,比如创建什么样的机器,去哪一个 provider 创建等等。因为这时候是在使用 ssh 协议简单地通讯,还没有 TTY,所以很多字符输入处理不好,比如你输入 1 再输入删除,并不会删除1,而是会得到 1\x7f

就写到这里吧,有关这个项目还有很多有意思的想法。如果你有什么好想法,欢迎在 issue 里面和我讨论。如果想试一下的话,可以按照 readme 来安装启动,只需要一个配置文件就可以启动了。项目就写了一个周末,如果启动不了或者遇到 bug 可以和我反馈~

 

Debug 一个在 uWSGI 下使用 subprocess 卡住的问题

今天花了很长的时间在排查一个诡异的问题,值得记录一下。

本来是想写一个 HTTP 的服务,你告诉这个 HTTP 服务一个 IP 地址和端口,这个 HTTP 服务就可以返回通过 TCP 访问这个 IP 端口的延迟。因为我们每次做 chaos 注入的时候都要测试一下注入延迟成功了没有,有了这个服务,这个测试就可以自动化。

其实很简单,收到请求之后用 TCP ping 几次拿到延迟,然后返回就可以了。

之前测量 TCP 的延迟使用的都是 hping3,Redis 的作者 antirez 写的。然后就想到用这个工具来做测试好了。

搜索了一下发现没有 Python 的 binding,所以打算粗暴一些,直接在 HTTP 服务里面 fork 出来一个进程做测试,然后去处理 stdout,grep 出来延迟的数据。很快就写好了,代码大体如下:

很简单的一段代码,收到请求,调用命令,返回结果。框架使用的是 Django,在本地测试一切正常,然后发布到 staging, 噩梦开始了……

在 staging 环境中,测试的时候发现,HTTP 请求发过去永远收不到回应,最后会得到一个 504 Gateway Timeout 的结果。去容器(应用运行在一个容器里面)看,发现 hping3 进程一直没有结束,像是卡住了。

一开始有很多错误的怀疑,比如怀疑 hping3 需要 TTY 才能执行,以为 hping3 需要使用绝对路径等…… 但是想想同样的代码在本地可以运行正常,就应该不是这些原因。一个验证就是,我去应用运行的环境中开一个 Python 的 REPL 执行这段代码,是能正常得到结果的。在应用运行的环境直接运行 hping3 命令,也是没有问题的。

然后怀疑 hping3 没有足够的权限来运行,因为 hping3 发送的是 raw TCP/IP 包,需要 CAP_NET_RAW 才能执行。去容器里面检查了一下,发现这个 capability 也是有的。

到这里,其实已经花费了很多时间了,得到的事实有:

  1. 容器里面执行 hping3 是完全没有问题的,权限是足够的
  2. 直接使用 Python3 的 REPL 执行这段代码也是没有问题,代码逻辑是对的

到这里你能猜到问题出在哪里了吗?

其实还有一个运行环境没有提到,就是 uWSGI. 这个 Python 写的服务是作为 WSGI 应用跑在 uWSGI 里面的。不知道和 uWSGI 有没有关系(直觉告诉我是有的,比直觉更厉害的同事也告诉是有关系的)。于是我打算直接使用 python manage.py runserver 在容器里面跑起来试试……

一切正常了。

所以 python 直接跑应用没问题,用 uWSGI 运行就有问题。现在问题锁定在 uWSGI 上面了。为了复现这个问题,我写了一个最小的测试用例。

首先需要一个文件,叫做 pingapp.py

然后使用 uWSGI 运行这个程序:uwsgi --http-socket :9090 --wsgi-file pingapp.py --threads=4.

uWSGI 的版本是 2.0.20,Python 的版本是 3.7.5, 但其实这不重要。

最后,访问这个程序,即 curl localhost:9090.

如果是 uWSGI 的问题的话,我们期望这里已经可以复现问题了,即 curl 命令会卡在这里,然后进程( ps -ef )里面出现一个 hping3 的进程,结束不了。

如同……下面这样:

图1 – 卡住的 hping3

但现实是……这个程序一点问题没有,运行地丝般顺滑。

这就见鬼了,直接没了思路。我的应用和这个最小的复现代码根本没什么(太大的)区别啊!我又没有用一些奇奇怪怪的 lib。

后面我实在解决不了了,找了(大佬)同事帮忙,花了很多时间,找到以下事实:

  1. hping3 卡住了,发现 SIGTERM 结束不了它,只能 kill -9
  2. 然后发现 uwsgi 进程也有一样的行为了,只能 kill -9 去杀它
  3. hping3 程序被 uwsgi 正常起起来是没问题的,起来之后运行不了
  4. …… 以及中间奇奇怪怪的现象,就不细说了,其实都不重要

最后虽然也找到了根因,但是走得路太弯了。本文从这里开始,就以事后诸葛亮的视角,看看有了上面的信息,我们怎么从正确的思路一步一步找到问题。

首先,同样的环境,在 shell 里面可以正常执行 hping3 但是 uWSGI 里面却不可以,既然 uWSGI 能正常开 hping3 进程,我们就可以看看这个进程到底卡在了哪里?

通过 strace 可以发现它一直在 poll 4 这个 fd,然后查看这个 fd,发现它是一个正常的 socket,应该就是 ping tcp 端口使用的那个 socket.

然后,我们应该去看一下,正常的 hping3 的 trace 是什么样子的。

strace hping3 104.244.42.1 -p 80 --syn -c 1

可以看到 poll 附近很神奇,下面突然就开始 write 到 stdout 内容了。不知道怎么结束的。

但是往上看,有一段代码获取了当前的 pid,然后给自己发送了 SIGALRM。再往前,发现注册了 SIGALRM 的 handler.

SIGALRM 是什么呢?

SIGALRM is an asynchronous signal. The SIGALRM signal is raised when a time interval specified in a call to the alarm or alarmd function expires.

看起来是用来做异步的。

搜索了一下 hping3代码。发现代码里确实是用这个信号的。

 

那么我们的 uWSGI 下的异常 hping3 是否是因为没有收到这个 SIGALRM 而一直在傻 poll 呢?

在上面的 图1 中,卡住的 hping3 pid 是 4285,我们看下这个进程能否处理信号:

这里可以发现 SigBlk 不是全 0 的,说明有 signal 被 block 了。SigBlk 是个 64 个 bit 组成的 bitmask,每一位代表一个 signal 是否被 block,1 是 block,0 是没有 block。从右到左分别表示 1-64 号信号。

举例:

可以使用下面这段脚本去 parse 到底哪些信号被 block 了:

可以看到,hping3 需要的 14 号信号正好被 block 了。

manual 中得知:

A child created via fork(2) inherits a copy of its parent’s signal mask; the signal mask is preserved across execve(2).

所以说,我们正常的 hping3 可以收到 signal,但是 uWSGI 里面 fork 出来的进程就不可以,可能是 hping3 从 uWSGI 里面继承了 signal mask,导致它也去 block 14 这个 signal 了。

我们可以去看下 uWSGI 里面是否也是 block 了这个 signal 的:

果然是的。

代码中发现,里面只有 core_id = 0 的 thread 是处理信号的,其余的 thread block 了所有的信号。

这段代码是 core_id 如果大于0,那么 block 所有的信号,如果不大于 0,就处理信号。

所以到现在也就明白我写的那个最小的 case 为什么不能复现了:我使用了默认配置,只有一个 thread,core_id =0,它永远可以处理信号。而生产环境开了 64 个 thread,只有 1/64 的几率能够处理信号从而让应用正确返回。

我们可以重新验证一下,开 2 个 thread,预期是它会有 50% 的几率卡住:

uwsgi --http-socket :9090 --wsgi-file pingapp.py --threads=2

果然是,50% 能得到结果,50% 会卡住。

 

到这里就真相大白了。至于修改呢,我打算直接用 socket.connect 来测量 tcp 的连接时间。因为 TCP 是 3 次握手的,但是对于客户端来说基本上只花费了一个 RTT 的时间。测试下来,这样得到的时间和 hping3 也是一致的。

 

另外这个故事告诉我们,uWSGI 下 fork 出来的子进程最好都默认信号不工作,虽然 core_id =0 是可以处理信号的,但是这个作为 uWSGI 本身回应信号的设计就可以。发现 uWSGI 的代码中还有很多别的地方调用了 sigprocmask(2),可能还有其他屏蔽信号的地方。

 

有一位智者同事曾经跟我说过这么一句话:

Learn some eBPF, xintao!

看来是时候认认真真学一学 eBPF 啦。