2018年年鉴

每年年末的时候我都会写写东西,总结一下今年过的如何。今年元旦的假期在南京旅行,每天都玩的很累。就现在补上吧。

工作

今年换了工作,算是比较大的一件事。从以前写爬虫和后端变成了一个 SRE。新的工作有很多挑战,有技术上的,也有沟通方面的。工作方式发生了很大的变化,以前的工作非常纯粹,我们有专业的 PM 和设计,我只管完成需求,写代码就好了。工作也非常轻松,每天都六点下班,工作之余还能看看自己感兴趣的东西。同事们也很专业,没有什么可挑剔的,我们有 Code Review,有基于 Python 的技术栈,代码有测试。每天工作都很开心。技术进步也很快,在 Code Review 中大家会互相指出错误,这是非常宝贵的财富。大家经常玩新鲜的东西,看起来不错的可以用到我们自己的产品中。我很感激在前公司的这一年。

来蚂蚁金服之后,从以前工作是 100% 的编程,变成了有 50% 的时间在沟通,50% 的时间在编程。开会的时间很多,但不是所有的会都是有意义的。以前公司人少,无论找谁喊一声就行了,现在找人很麻烦,不是每一个人都回复很及时,更多的情况是他会让你去找另一个人,这样找来找去,最后找过五六个人还没解决一个很简单的问题是很正常的。而文档、帮助维护的又不是很好,找人问又成了一个常态,所以这对我来说成了一个很大的困扰。最近正好看到一个理论,讲的是一个组织做出来的东西,实际上是这个组织沟通方式的体现。

“organizations which design systems … are constrained to produce designs which are copies of the communication structures of these organizations.”

— M. Conway

我在公司内部的宣传中看到一个小姑娘说,自己进公司之前是比较内向的一个人,进公司三年后变成了一个怼天怼地的女汉子。这个公司比较欣赏这样的人,可我觉得很反感。你工作中怼的都是你的同事啊,大家低头不见的,怼来怼去这样好吗?大家在一起和和气气的工作,每天都开开心心的不好吗?为什么喜欢吵来吵去呢。

当然了,蚂蚁金服是很大一家公司,我觉得这个公司的任何一个人,都不能描述出这个公司是什么样子的,顶多是盲人摸象。每个部门,每个团队都不一样。我也不认为这些问题是蚂蚁金服存在的,不同的公司会有不同的问题,也不见得其他公司没有我所说的这些问题。说这么多负面的地方,原因是我对这家公司的期望是挺高的,结果来了之后发现并不是我想象的那样。不过好的是,我可以有机会去改变那些不好的地方。

话说回来,这一年做 SRE,一个难得的经历是有许许多多各种各样的故障可以看,可以参加 Review。每一个故障都是新的,有些很蠢,有些很有意思,我都感到惊讶,这样隐蔽的故障都能这么快找到原因。遗憾的是,我还没有亲手第一个找到过一个故障的根本原因。

关于加班,下半年基本上都 10 点之后下班。工作时间变得很长,也是让我困惑的一个地方。没有时间学习了,换工作之后,自己的技术成长明显没有以前快了,开发的效率也变低了。以前九点上班,6点下班,我能写很多代码,完成很多任务。现在一天工作10个小时以上,做的事情却没有以前多。幸运的是,现在住的离公司近,上下班很方便,晚上每天都加班,打车公司报销。花在路上的时间没有那么多了。以前我竟然每天花 4 个小时上下班!

明年想办法多留给自己一些时间吧,空闲的时间是真正长知识的时间。我见过一些同事,基本的计算机常识都没有,还停留在毕业生的水平,感觉就是毕业之后一直没有时间学习导致的。学习基本的原理(虽然工作中用不到),了解你使用的工具,这其实是会让你事半功倍的东西,可惜很多人看不到这一点。

开源和学习

今年开始接触了 openresty 和 lua,很有意思的一个领域。在读 PIL 但是还没有读完,PIL4 已经比第一版厚很多了。openresty 社区的人非常友好,从文档就看出来了,无论谁写的库都是非常标准的风格,Toc,简介,函数文档。明年继续深入学习一下这方面,多看看库的源代码。

openresty 的一个模块的控制台我是用 starlette 写的,一个基于 asyncio 的很小的框架。顺便看了它的源代码,提交了两个关于 staticfile 的 patch。后来没时间搞了。

自己翻译的这本书,已经很长时间了还没结束,今年进度已经到了大约 2/3 了,争取最近就完结它。

另外看了很多 Kubernetes 的资料和相关的软件,但是一直没机会用。我有一个不错的 idea,已经放了一年没有下手干了,不过短期看来,我基本没有时间做自己喜欢的事情。

社区

来了杭州之后,在杭州组织了 4 次 Python 社区的 Meetup。听了很多有意思的分享,学到很多东西。这个事情是很有意义的,希望明年把它继续下去,时间是一个大问题。

10 月份去主持了北京 PyCon 的语言特性专场,今年的分享特别精彩,认识了很多大神,打开了很多新世界的大门。去上海 PyCon 做了一个很入门的分享,反响还算不错。今年还发了很多 Tweets,平时看到有意思的东西都会转发一下。明年争取继续在社区中学习吧,也多分享自己的东西。

生活

今年的生活没有太大的变化,除了在公司的时间变多了。我尝试过健身来着,不过全年加起来估计才有 10 次左右吧,明年坚持一下。越来越觉得,健康才是最重要的,也许我开始变老了。

希望明年少浪费一些时间,多做一些有意义的事情。多学一些知识,多看书,少说话。

往年:

  1. 2013年
  2. 2014年
  3. 2015年
  4. 2016年
  5. 2017年 (看了一下2017年定下的4个目标,很好,一个都就没完成)
 

SRE&Devops 每周分享 Issue #6 Closing

Hi,这是这一系列的最后一篇内容,之后不会再每周提供定时发布。遇到有意思的文章我会分享在 Twitter 上,这种方式更加实时,也比较有互动性。这是我的 Twitter: laixintao 。以下是本期内容。

 

Ubuntu 发布的 《上云白皮书》

现在可选的云服务多种多样,公有云、私有云、混合云,还有不同的厂商提供了不同的服务,从 Bare mental Server 到 VPS 到容器服务。提供 Infrastructure 的经常还伴随着提供上层的服务。Ubuntu 发布的这个 PDF 介绍了各种云的概念,推荐一下,了解之后可以针对自己的需求和规模选择最合适的场景。

Envoy Proxy at Reddit

Reddit 的用户越来越多,服务规模也越来越大。Reddit 使用 Envoy 作为四层/七层负载均衡服务器,本文介绍了他们的方案。

Build a serverless Twitter reader using AWS Fargate

AWS 的一篇 serverless 手把手教程。

The Definitive PHP 5.6, 7.0, 7.1, 7.2 & 7.3 Benchmarks (2019)

最新版本的 PHP 的一些性能测试。

Using Golang to Build Microservices at The Economist: A Retrospective

经济学人使用 Go 语言来构建微服务的历史。

Why on earth did we choose Jenkins for 2019?

介绍了 Jenkins 的一些优点。

新闻:Red Hat 将 etcd 捐赠给了 CNAB

HOW DASHBOARDS ARE CHANGING HUMAN BEHAVIOR IN DEVOPS

Dashboard 怎么用,怎么设计,这是一个哲学。

Our learnings from adopting GraphQL

Netflix 介绍的使用 GraphQL 的经验。

rendora/rendora

给爬虫渲染出页面的一个项目。(使用无界面 Chrome 浏览器)

bloomberg/goldpinger

Debug k8s 的一个工具。

 

SRE&Devops 每周分享 Issue #5

这个周工作比较忙,分享的东西不多。

 

The headers we don’t want

介绍了几类被误用和滥用的 Header。有些不错的干货的,让我惊讶的是很明显用错 Header 的网站竟然有这么多,还长达 20 多年。不过有些观点我不同意本文,比如 Response 放上 Server,我觉得是有用的,比如对互联网上的统计,测试收集各种服务器的性能等。

Getting started with Jenkins X

Jenkins Kubernetes plugin 已经可以让 Jenkins 跑在 k8s 上了,Jenkins X 是持续集成 k8s 应用的一个方案,并且 Jenkins 本身要跑在 k8s 上。

​GitOps – Operations by Pull Request

Git 可以追踪所有的变更历史,可以轻松的回滚,使用 PullRequest 机制可以互相 Review。所以我一直想,如果用 Git 来做配置中心,或者将所有的线上操作通过 Git 来追踪(我上一家公司使用 Salt 就是这么做的),可以省很多事。

原来已经有人将这个想法实践了。这篇文章介绍了 Waveworks 基于 Git 的运维。我觉得本文能这么做最重要的一点是:运维工具必须是声明式的,表达一个最终状态,像 Ansible 那样。

这样可以使所有的操作都透明化,最终达到的一个效果是,文中提到他们有一次不小心将 AWS 上所有的节点都删除了,只用了仅仅 45 分钟,就恢复了回来。

deislabs/cnab-spec

CNAB:一份开源的、独立于云平台的规范,包括如何打包、运行分布式的应用。

DOCKER APP AND CNAB

Docker App 是遵循 CNAB 标注标准的工具,可以 build 符合 CNAB 标准的 bundle,也可以用来运行、升级 Bundle。

Announcing GitLab Serverless

Gitlab 宣布将在 12月22日上线 Serverless 服务。

 

Nginx(ngx_lua) 过滤 10w 个 User ID

今天的工作太刺激了,一天下来正好解决了一个有意思的问题。晚上来记录一下。

上次解决了当有很大的 HTTP body,在 ngx_lua 里面读不到的情况后,还留下一个解决性能问题。上次提到,我们对于用户的每一个请求,都要根据一个 json 形式的规则,来判断怎么样路由这个用户。为了让读者更明白这个问题,举几个例子:

  1. 给出一个 10万元素的用户 ID 列表,如果用户的 ID 在这里面,并且请求 URL 是 xx,Cookie 含有 xx,就转发到 Server A
  2. 用户 ID 在列表转发出 10% 的用户到 Server B

此模块我是用 ngx_lua 写的,现在有问题的实现是这样的,将这个规则保存在 ngx.shared_dict 里面。每一个请求过来,我就解析成 lua 的 table,然后判断规则。我的测试环境是一台内网的服务器,单进程开 Nginx,wrk 测试是 1800~1900 request/sec。开启这个模块之后,只有 88 requests/sec,由于每一个请求都要经过这个模块,这样的延迟是无法接受的。

规则是发到每台机器的 Nginx 上,要在 Nginx 所有的 work process 共享一个变量,不知道除了 shared_dict 还有啥方法。其实我想过自己基于 shared_dict 实现保存 table,就是我把 table 打扁平,按照 key-value 放到 shared_dict,但是这项工作想想就挺大的,而且要踩坑才能保证正确性。

今天发了一个邮件到 openresty 社区(这个社区非常活跃和友好!),问了这个问题。mrluanma 和 tokers 回复说可以用 mlcache 。其实我之前也看了一下这个项目,但是没有看完文档,不知道靠不靠谱,既然大家可以说这么用,就去试一试了。

这其实是一个缓存,首先 L1 缓存是每一个 Nginx 进程里面的 Lua vm 会有缓存,如果没有命中,那么第二层缓存就是 ngx.shared_dict ,如果再没有命中,就会调用用户的 callback,也就是所谓的 L3. 由于是一个缓存项目,所以有一些缓存方面的问题,比如 dog pile,此模块都处理好了。我的用法比较特殊,只是拿它来做多个 worker/多个 HTTP requests 的共享数据,所以很多地方没有细看。

新建一个 cache 的 Nginx 代码如下,需要写在 http 里面。

这里要注意 3 个地方:

  1. 因为要调用 set() 方法,我们是主动更新规则的,而不是等他过期。调用 set() 和 update() 要提供 worker 之间通讯的方式。mlcache 实现了通过 shared_dict 来通讯,所以我只要另外申请一个 shared_dict ,然后将这个 shared_dict 设置给 ipc_shm 就好了。
  2. ttl 和 neg_ttl 设置成 0 ,理由同上。
  3. 通过 _G 可以执行全局的变量,这样 lua 就可以直接使用 cache 这个名字了。

然后在设置规则的时候,直接通过 cache 来调用即可:

读取规则也是一样:

今天栽在这里很长时间,文档说第二个参数是 optional 的,我以为就可以不填。然后 set() 就填了两个变量。结果调试半天(Lua 奇葩的变量不够 nil 来补)。后来才明白这个 optional 的意思是你可以填一个 nil 进去,因为未定义的变量就是 nil 啊!难怪呢,我还想 lua 怎么实现的,难不成判断函数调用的参数个数?

另外一个点是 get() 方法要提供一个 callback 函数,L2 没有命中的时候提供就执行这个函数。在我这里,如果 L2 是 nil,那么 L3 也返回 nil 好了。

显示调用 set() 的一个非常重要的点是:一定要通知其他 worker 删除 L1 缓存。不然我们调用 set() 只是更新了一个 worker 的 L1 和 L2。在本文的场景下,worker 的数据不一致导致转发规则不一致是有问题的。这里只要在 set() 的之后调用一下 purge() 之后,通知其他 L1 去删除自己的缓存就可以了。 这里之前写的 purge() 函数的使用是有错误的,purge() 是清除缓存,包括用于 worker 交流的 shm 和 lua-resty-lrucache 的缓存,导致所有 L1 和 L2 miss 然后去 L3 更新,所以开销是比较大的(虽然在我这里,整个缓存=我的一个 table)。

正确的用法是这样的,使用 set() 的话,要多加一步,在 get() 前面调用一下 update() 。从源代码和文档得知,它的工作原理是这样的:set() 内置会广播一条消息,然后更新 L1 缓存(仅自己的 worker)和 L2 缓存。get() 之前 update() 这个调用会队列里面的广播事件,如果有事件的话,就先消费掉事件,没有事件的话,就什么也不做。注意所谓的 update 并不是更新 L2 缓存,而是消费所有的事件的意思。这样就做到一个 worker 更新某个值之后保持和其他 worker 一致了。

下图的第一个 worker 先 set 了一个值,然后通过 shared_dict 广播出去这个值的 name,回调函数是从 L1 删除这个值。其他 worker 蓝色的箭头表示 get() 之前 update() 去检查是否有事件需要处理。

修改之后性能从 88 requests/sec 上升到 300+ requests/sec,所有提升,但还是很慢。平均下来一个请求的延迟增加 3ms 多。

然后我又顺着这 10w userid 进行优化,规则里面这个 uid 是一个很大的 List,所以逻辑上是遍历查找一个用户是不是在列表里面的,O(n) 的效率。主要的耗时点就在这里。我想改成用 Set 结构来存,这样只要 O(1) 复杂度就够了。

问题是,Json 只有 Dict 和 List 两种数据结构,这个回答说的很好:

  1. 编程实现 List 和 Set 互相转换是很简单的
  2. Json 用于数据交换,你不能信任一个数据输入是 Uniq 的

但是我觉得 Json 增加 {"foo", "bar", "banana"} 这种形式好像没有什么不妥。

Anyway,我只能自己实现了,写了一个函数,在 Json 转换成 table 之后,找到我想转换的 Key,将它的 value 从 array 形式的 table(key 是 1 2 3 4 5 …)改成 Set 形式的 table (key 是各个元素,value 为 true),代码如下:

这样,在找的时候,只要看 value 是不是 true 就可以了:

这样改了之后,性能上升到 1800 requests/sec,跟不开启这个模块相比,基本上没有性能损耗了。

 

话说回来,这个问题跟我上一家公司的面试我的时候出的题目很像:给你一个 IP 列表,内存可以随便用,但是查找速度要快,如何看一个 IP 是否在表中?

我当时的方法是,一个 IP 用一个 bit ,bit 位要么是 0 要么是 1,表示此 IP 在或不在。表示世界上所有的 IP(IPv4)需要 2^32 个bit = 536M,将 IP 列表中的 bit 都置为 1,其余为0. 这里的关键是 IP 如何映射成 bit 表,IP 其实是 4 个字节而已,直接用 4 个字节所表示的数字作为 index 就好了。

巧合的是,这个周我正好认识了布隆过滤器。哈,我的想法真先进。

 

使用 ngx_lua (openresty)正确读取 HTTP 请求 body

之前用 ngx_lua(openresty) 写了一个处理 HTTP 请求的程序,今天发现当发送的 HTTP 请求 body 很大的时候,发现老是报错,最后定位到 ngx.req.get_body_data() 这个函数返回 nil ,而不是真正的 body。

于是我去 ngx_lua 的文档看这个函数的文档,发现文档中说有三种情况下,这个函数的返回值会是 nil:

This function returns nil if

  1. the request body has not been read,
  2. the request body has been read into disk temporary files,
  3. or the request body has zero size.

第一条,我是读了文档的,知道要 Nginx 默认是不会读 body 的,要么打开读 body 的开关,要么显示调用一下 read。所以代码中已经调用了 ngx.req.read_body() 了,不会是这个原因。

第三条,也不可能,我通过 curl 发送的,body 肯定是发出去了(可以抓包验证)。

那么基本上就确定是这个 request body 被读到硬盘的临时文件里了。看到这里我猜应该是 Nginx 是将大的 HTTP 请求放到磁盘中而不是放到内存中。搜了一下文档,发现有这个参数:

当请求体的大小大禹 client_body_buffer_size 的时候,Nginx 将会把它存到一个临时文件中,而不是放到内存中。这个值的大小默认是内存页的两倍:32 位系统上是 8k,64位系统上是16k。

OK,问题找到了,现在解决方案有两个:1)调大这个值,我觉得是不合理的,这样会浪费内存。2)可以想办法读到临时文件中的大 body。ngx_lua 提供了配套的方法 ngx_req.get_body_file,注意这只是获得文件,还要在 lua 代码中打开读取文件。

所以最后,处理一个请求且还能正确处理很大 body 的请求的代码是这样的,其中高亮的部分,是核心的逻辑,先尝试从内存中读 body,如果读不到,就去临时文件中读。

我读这个实际的应用场景是,读出 body,按照 json 解析出来当做路由规则(需求是需要动态设置 HTTP 的路由规则,所以我用 ngx_lua 在 nginx 上新开了个端口监控这样的规则)。现在用很大的规则一测试,发现性能下降很厉害。存到文件、再读出文件是一方面。另一方面是 ngx_lua 是为每一个 HTTP 请求开一个 lua 协程处理,不能共享变量,只能通过 lua_shared_dict 来保存持久的变量。但是 lua_shared_dict 的问题是,这个 dict 只支持 “Lua booleans, numbers, strings, or nil”,解析出来的 json 是一个 lua 的 table,不能保存到 lua_shared_dict 里面,我只好保存一个字符串,然后对每一个 HTTP 请求 json decode 这个字符串了。不知道有没有更好的方法。