2021 年年鉴

今年没有在跨年的时候及时写完年终总结,是因为那天还在西安隔离。2021 年一整年依然是疫情肆虐,下半年在新加坡尤其严重。所以在这一年的最后回想起来,还是比较无聊的。

所幸这一年经过的所有的事情都还算顺利,没有什么大起大落。

就先说说工作吧。其实今年的大部分时间都花在了工作上。从 6 月以来,新加坡的防疫政策从清零变成了与病毒共存,病例数量一直居高不下,一直到年末的时候才开始下降。所以下半年我们几乎都是在家办公的。在家办公就导致了很多问题,比如有同事在 town hall 上提出的 endless working hours,就成了我最大的问题。

以前想,居家办公可以让我们有更多的时间去安排自己的工作。但实际上实行起来,却发现工作时长不可避免的延长了。一方面,大家一起没有了通勤这个工作和生活的分割,所以有时候会在 IM 上在工作时间之外去沟通一些问题。很多时候虽然说不急,可以回头再回复。但是我看到了不回复就会难受,只好去帮人家解决;另一方面,下半年的大促一个接着一个,每个月至少一次。大促之前有事情要忙,大促之后又有因为大促而 block 的事情要忙,也导致工作时间延长。居家办公也导致工作设备的质量下降,没有一把合适的椅子,没有像样的办公的桌子(当初租房子的时候也没想到自己会在家工作这么长时间),导致开始腰疼了。

抱怨就说这么多吧,希望可以尽快恢复办公室办公,也希望这波疫情赶紧过去吧(这篇文章在 2月底捡起来继续写,发现第三波疫情又开始了,真的是烦)。

好在,今年做的工作都比较有意思,所以也没有感觉到多累。

今年工作上做的事情主要有两个,一个是公司的 service mesh 系统的维护。另一个是从零开始一个的 SRE Chaos Engineering 项目,我起的名字的叫做 Chaolab.

Service mesh 方面,其实没有什么特别大的项目或者改动,只不过是在各种部署结构和部署流程上修修补补,没有什么值得特别骄傲的,但是不知不觉,这个项目也越来越稳定了。比如:目前在升级的时候是无法保持住长链接的,通过发布的算法可以分批发布,每次不影响某一个服务 20% 的 instance,降低发布的影响;删除了大量不再使用的资源,归还了很多机器;下线了很多私有部署的 Nginx,迁移到公司的网关上去;将服务部署到了越来越多的机房,等等。

还有一个很有意思的事情,就是完全重构了部署使用的脚本,这个项目以及所有的依赖可以在一台笔记本上跑起来,一共涉及十多个组件,以及 20 多个 docker instance。算是去年一整年的工作成果的总结吧,之前下线了一些不必要的依赖,整理很多次部署结构,才能使它成为可能。

关于 Chaoslab 项目。这个算是比较有成就感的项目。一个人从头写了前端,后端,部署,agent 等组件,算是一个比较完整的项目了。主要实现的功能就是,你可以在页面上去创建一个实验,然后给物理机,或者容器等去实现注入 chaos,然后写 probe 去验证软件是否在注入故障之后依然工作正常。支持 crontab,权限控制,自动 rollback 等功能,大部分会使用到的注入选项都使用到了。

除此之外,就是一些杂七杂八的工作了。比如日常值班,回答别人的问题啥的。今年的感受就是,之前不被承认的工作在 Shopee 都是被认可的。还做了一个平台组件的 status page,这个是之前就想尝试的,一直依赖都有一个观点,就是应该通过公开的信息和用户信任感,用户需要知道你的组件的状态,在什么时间遇到过什么问题。虽然只是一些 CURD 的工作,没有什么技术含量。但是最后做出来非常满足。我一开始的设计有很多问题,但是很多人都给我提了建议,修改之后,就好很多了。

工作方面想到的就这么多吧。在开源方面,其实今年做的不够多,iredis 基本上是修修补补。目前的补全实现是基于每一个命令的,也就是说,redis-server 的改动,我都必须做出相应的适配,否则,一些新的命令或者老的命令修改了语法,都不会有很好的自动补全。然而 redis 6.0 新加入的改动都还没有支持完全。主要是没时间去做。

今年开了一个新坑,lobbyboy. 是一个可以自动帮你开新的虚拟机的 ssh server。一开始是满足自己的需求,后来看到其他人也有需求,也有很多贡献者来提交代码。甚至在这个项目上写的代码比我都多了。后来把这个项目迁移到了一个 org 下面,而不是只有我自己有权限。虽然后面在实现了基本的功能之后也没有花很多时间在上面了。

今年学会了什么?

新学的东西不算多,感觉自己在啃老本。倒是接触了 Prometheus,读了 Up&Running 那本书,感觉对于使用算是比较了解了。

另外看了一些 golang 的教程,目前算是能看懂 golang 的代码,但是写的话还是差点火候。今天还是继续学习一下 golang 这门语言,多看一些代码和书。

另外感觉自己的沟通能力有了很明显的提升。能够在别人描述不清楚自己的问题的时候帮别人解答问题。:)

明年计划学习什么?

除了 Golang 之外,Kubernetes 打算多花点时间学习一下,因为工作中要用的到。

工作和学习的事情就说这么多吧。去年也发生了很多有意义的事情,很多也是一时半会说不完的。年的时候从新加坡回国过年,疫情之下的回国程序真的是复杂呀,交了数不清的文件,终于拿到大使馆发的绿码了。但是偏偏不巧选择的落地地点是西安,落地的第二天西安宣布封城。终于在隔离结束的时候,回到了上海,上海又说你是从西安回来的,作为重点人群需要隔离14天。虽然我极力解释我是在隔离结束之后完全闭环转移到上海的,没有接触过西安的人,也是无济于事。最后还是被拉倒隔离酒店隔离了14天。这28天应该是我在这一年里面最郁闷的。

今年年初的时候和老婆一起去拍了婚纱照,寒冷的冬天在外滩懂的瑟瑟发抖,辛苦老婆了。

今天的首要任务之一就是将婚礼举办好,嘻嘻。

年后经过香港转机返回新加坡,正巧第三波 Omicron 疫情又开始,香港和新加坡又是疫情比较严重的地方(怎么我这段时间去哪哪严重的?!)。经过了道道审核,最后还算是顺利地回到了新加坡。

路上还见了多年未见的老朋友,一起吃了快乐小羊。他吃羊间隙还得拿出来电脑工作,我感叹大家都成社畜了。

回到新加坡之后,很快安顿了下来,换了一个房子,也很快进入了工作状态。

好像也没什么好说的了。2021 年是过的比较平静的一年,2022 年也没有什么特殊的计划。老婆的计划倒是比较重要,今年除了婚礼要办,老婆也在努力朝着一些会改变人生轨迹的大事上面付出努力,我只有默默地支持,提供帮助,希望一切顺利。总之,希望2022年能够多付出一些,过的也更多姿多彩一些吧。


往年的总结:

  1. 2013年
  2. 2014年
  3. 2015年
  4. 2016年
  5. 2017年
  6. 2018年
  7. 2019年
  8. 2020年
 

寻找丢失的信号

记录一个今天遇到的小问题。这是继 Debug 一个在 uWSGI 下使用 subprocess 卡住的问题 之后又一次遇到信号问题。

我写的 chaos engineering 平台支持一个功能:立即中断正在进行的实验,并且执行 rollback 操作复原注入的操作。每一个实验都是由一个进程负责的,终止的方法是向进程发送一个 SIGINT 信号,让进程停止注入并且切换到 rollback 开始清理。

最近的一个改动从 asyncio.create_subprocess_exec 切换到了 asyncio.create_subprocess_shell 导致了一个 bug,现象是线上的执行器根本收不到停止的信号了,刹车失灵,险些酿成悲剧。

经过警方调查发现,asyncio.create_subprocess_shell 其实会开一个新的 shell 来执行命令,默认使用的是 sh,而 sh 默认是不转发它收到的信号的。(这里我是用了 killsnoop 来发现 sh 确实收到了信号,但是执行 chaos 的进程没有收到,然后查阅文档并通过实验复现确认 sh 不会转发信号。)

但是这个问题我在开发环境(Mac)并没有测试出来,因为开发环境工作的好好的。

我写了一个最小的 case 可以复现这个场景:

在 Mac 上的表现是,python 进程的子进程就直接是 sleep 进程,并没有一个中间的 sh 进程。

而在 Linux 上的表现是:python 进程的子进程是 sh 进程,然后 sh 的子进程才是 sleep 进程。

经过 ./grey 指点,发现在 Mac 上 sh -c "sleep 99" 之后,sh 自己也不见了,只有 sleep 99 这个进程,父进程是我自己的 zsh shell.

这里就真相大白了。中间进程的这个 sh 并不会转发 signal,所以在线上的 Linux 系统上收不到信号;在开发电脑上由于没有中间的 sh ,所以直接将 signal 发给了子进程。

那么 sh 在两个系统上到底有怎么样的不同呢?

在我的 Mac 上,man sh 说:

sh is a POSIX-compliant command interpreter (shell). It is implemented by re-execing as either bash(1), dash(1), or zsh(1) as determined by the symbolic link located at /private/var/select/sh. If /private/var/select/sh does not exist or does not point to a valid shell, sh will use one of the supported shells.

经过查看,可以发现其实 sh 在 Mac 上是 bash:

对于 bash -c "sleep 99" 这个命令,bash 有一些优化,为了节省资源,bash 会直接通过 execve() 去执行 sleep,这样在系统上就可以少存在一个 bash 进程。详细解释

而在 ubuntu 上,sh 其实是 dash:

dash (至少我们使用的版本)还没有这个优化,所以在 Python 的 subprocess shell (经过 linw1995 指点)中就会有两层进程,一个是 dash,dash 的子进程才是运行的命令。

在 ubuntu 上 bash -c "sleep 99" 可以看到 bash 本身也是会消失的。说明这个确实是 bash 的行为。

bash 进程消失不太准确,它其实是换了一个形式存在而已。strace可以证明它存在过:

 

反思一下这个问题,有以下几点可以做的更好:

  • 换成 Linux 开发;
  • 写测试用例,CI 完全可以发现这个问题;
  • 还是尽量使用 asyncio.create_subprocess_exec 来执行命令吧!
 

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 可以接受一个。

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