Nginx if 指令工作原理

Nginx 的 if 指令被认为是“邪恶”的,就和 C 语言的 goto 一样。甚至官方有一篇 If is Evial 来警告你不要使用 if 。但有时候 if 还是非常有用的,如果掌握了它的原理,在合适的地方正确使用 if,会让事情更简单一些。当然前提是你真正知道自己在做什么,就和 goto 一样。 ——这是我的观点。

首先每个接触 Nginx 的人应该意识到的事情是,Nginx 是分 phase(阶段) 的,并不像 C 这种编程语言一样顺序执行。指令执行的顺序和书写的顺序没有太大关系(跟具体模块的实现有关),一个 phase 执行完了就会执行到下一个阶段。

If 是属于 rewrite 模块的,所以对于 if 来讲,会和其他的 rewrite 模块执行全部执行完之后再进行下一阶段。如果 if 指令的结果是 match 的,那么 if 会创建一个内嵌的 location 块,只有这里面的 content 处理指令(NGX_HTTP_CONTENT_PHASE 阶段)会执行。

下面是 agentzh 的四个例子,我这里稍加自己的解释。

实验的机器 IP 是 172.28.128.4 ,结果如下:

首先,对于一个请求 Nginx 会执行 rewrite 阶段,即如下代码。rewrite 阶段的执行顺序和指令的顺序是一样的,这个 rewrite 模块的实现有关。

$a 被设为 32,然后进入 if block,$a 在这里被设为 56,最后 $a 被设为 76. 中间 if block 生效。但是 if block 中没有任何 content 阶段的指令,所以会继承 outer block,即 ngx_proxy 模块的 proxy_pass 设置。这里要注意的是请求在 if block 内完成,if 命中之后就进入了 if block 来处理下一阶段,而不会跳出 if。

第二段示例如下:

结果如下:

Rewrite 阶段的过程和上面一样,不同是这一次 if block 中有了 content 阶段的指令,所以会执行 echo,不会执行到 proxy_pass 。

Rewrite 阶段的 break 可以终止 rewrite 阶段的执行。

以上代码的结果是

在 rewrite 阶段中,执行完 if ($a = 32) 之后执行 set $a 56 ,此时下一行是 break ,然后 rewrite 阶段就停止了,进行下一阶段。set $a 76 并没有被执行到。所以最后 $a 的值是 56。

ngx_proxy 会继承 outter scope,但是很多模块并不会这样,这个地方挺坑人的,我就是在这里被坑到的。

参考这段配置,正常来说,所有的 echo 都会执行,即如果不存在 if 的话,这段配置的结果应该是 hello \n java 。但是这里结果会是:

可以看到 echo 并没有继承 outter 。

顺便说一下我写的那段配置吧。简化之后如下:

我期望如果进 if 和不进 if,都会执行我的 access_by_lua_file ,但事实看来,进入 if 之后并不会再出来,而且 access_by_lua 和 access_by_lua_file 像 echo 一样,if 内并不会继承外面的 access_by_lua_file 。所以如果 if 命中,那么 access_by_lua_file 永远不会执行到。

最后一个例子是会继承 outter 的:

结果如下:

可以看到,这个模块的 more_set_headers 指令是默认继承 outter 的。

所以,官方给出的建议是尽量不要使用 if 指令,比如说有些地方其实可以使用 try_files

如果用,那么尽量只在 if block 内使用 rewrite 模块的指令。因为大家都是在这一个 phase 里面的,不会有 surprise 了。

在某些情况下,这些需要 if 的指令可以用嵌入的第三方模块来完成,比如 ngx_lua perl 等。

实在要用的话,做好充足的测试。

 

SRE&Devops 每周分享 Issue #1 Opening

Hi, 我平时看到一些有趣的文章很喜欢分享给朋友(这也是我写博客的原因吧),现在订阅的内容也越来越多了,有些也实在看不过来(Pocket 都堆满了)。其中订阅了很多 newsletter,newsletter 并不是每一篇都对你的胃口,但是总有几篇不错的,所以我也打算将我的分享整理成这种形式。一方面,我的工作中很多方面还在摸索,另一方面,这样可以将我看过的东西整理下来。最重要是的希望这种方式可以遇到更多同行,大家可以互相交流。

关于形式,暂时我先发布在博客上,后面如果找到不错的 Newsletter 托管(欢迎大家推荐),可能迁移到邮件形式。我每周将想要分享的东西存成草稿,周五中午 1:00 定时发布。当前分那几个板块还没确定,写到哪算哪吧,后面根据需要来调整。

关于投稿,大家发到博客右侧的邮箱即可。

关于内容,暂时会以 URL + 我的推荐语、概括为准。内容我尽量推荐自己看过的,但是有些实在太长,我可能放到 Pocket 慢慢看,还没看完就推荐给大家也说不定。所以大家要带着自己的脑子来读这里推荐的东西,并不一定都是好的。大部分内容都来自 Hacker News,这是我主要的订阅源。

最后,这是一项个人工作,所以无法保证能更新多久也无法保证内容有多少,请见谅。以下是第一期内容。

Honeycomb’s Charity Majors: Go Ahead, Test in Production

“分布式系统天生就是不好克隆、模拟、staged的,所以放弃 Staging 吧。加入线上环境出现问题,有些用户访问图片很慢,有些正常,你能在 Staging 环境发现问题吗?”这是 Honeycomb 在 ChaosConf 2018 的一个演讲,很多人反对在 Production 环境中测试的原因是,他们认为这样会破坏真实用户的体验。但是有像 A/B 测试,金丝雀测试的存在,可以将影响控制在一定范围。

gRPC Load Balancing on Kubernetes without Tears

gRPC 是基于 HTTP/2 的,而 Kubernetes 的负载均衡是基于 TCP 连接的。而 gRPC over HTTP/2 只会建立一个 TCP 连接,所以这样负载均衡就会有问题,所有的请求都发到了一个节点上去。官方推荐使用 Linerd2 来做 gRPC 的负载均衡。

HTTP-over-QUIC to be renamed HTTP/3

HTTP over QUIC(Quick UDP Internet Connections) 被重命名为 HTTP/3,将不再使用 TCP。

Time Series Analysis with LSTM using Python’s Keras Library

这是一篇教程,使用 LSTM 对时间序列数据分析。类似股票走势之类。这对流量监控系统非常有用,普通的规则设定报警很容易出噪音,如果能够基于预测出的流量走势设定监控报警也许更准确一些。

The History of Unix, Rob Pike

上周的一个不错的视频,一位资深程序员 Rob Pike 怀念了 Unix 几十年的历史,和他的故事。

Incident Management in Gitlab

Gitlab 是一家很开放的公司,这是他们的事故管理策略。

Chaos Monkey Guide for Engineers – Tips, Tutorials, and Training

Chaos Monkey 教程,介绍了其理念,实践以及相关的阅读资料(制作很精美)。

How Automatic Root Cause Analysis Works

Instana 如何在复杂的系统中自动定位故障的根因。

Getafix: How Facebook tools learn to fix bugs automatically

我们可以自由的写 Bug 了,毕竟有机器人来修复。

October 21 post-incident analysis

Github 10月32日 故障分析。

 

用 whitenoise 提供静态文件服务(Python)

Whitenoise 这个项目是一个符合 WSGI 标准的静态文件服务器,因为 WSGI 是可以嵌套的,所以 Whitenoise 可以和你原来的 WSGI 应用配合的很好,迁移成本很小,或者像 Django 这种项目有某种 middleware 机制,迁移就更方便了。本文介绍这个库的使用、为什么要用这个库(尽量说服你 ),以及买二赠一的源代码导读。

从文档掏出来的一个 QuickStart 如下:

可以看到,其实就是将你的 WSGI app 外面再包一层 WSGI app,即 WhiteNoise。

Django 可以不通过这种方式,因为 Whitenoise 对 Django 做了一些额外的适配,可以使用 Django 原生的中间件机制。

静态文件服务其实就是对于 HTTP 请求,发送对应的文件给用户。这件事情为什么要用 Whitenoise 来做呢?这个项目存在的意义究竟是什么呢?当你没听说过这个项目之前,一般的做法是用 Python 写的 web 应用来处理动态内容,用 Nginx/Apache2 这种专业的 HTTP 服务器处理静态文件;或者将静态文件都放在 S3 这种对象存储上。

为什么用 Whitenoise 比这两种做法要好,官方的 FAQ 写的很好,我这里捧哏转述一下。

首先使用 S3 这种方案,只是可以 work 的方式但是不是最佳的。第一,对压缩的支持不好。HTTP 的大多数时间都花在了网络上,而现在大多数个人电脑的 CPU 都是闲置的,所以假如服务器要传给客户端文件,那么服务器这边压缩一遍,通过网络传给客户端,客户端解压,虽然多了一次压缩/解压,但是总体上时间还是快的。S3 目前不支持压缩。支持压缩的话就要读 Accept-Encoding 这个 Header 看客户端支持哪种压缩方式(古老的 Gzip 压缩,或者现代的 brotli)。还要设置好 Vary 这些 Header 告诉 CDN 怎么处理缓存(什么是 Vary?)。而 Whitenoise 都帮你处理好了。另一个不便是配置 S3 比较麻烦,要用客户端上传,要有 Key,Secret 这些东西。CDN 的话,CDN 知道你的地址,你知道 CDN 的地址(互相确认过眼神)就好了。

其次,为什么不用 Nginx 呢?用 Nginx 是可以的,Whitenoise 使用的场景是 Heroku 这种 PaaS 平台,这种场景下 Nginx 不太好搞,所以用了 Whitenoise 你可以脱离 Nginx 了,直接将 WSGI app 平滑地部署到 PaaS 上。而且用 Nginx 你得仔细的设置很多东西,比如 CORS Header,cache Header,Nginx 原生不支持 brotli ,你还得去装 module 。

最后的一个问题是,Python 的性能问题是否意味着 Whitenoise 是一个效率很低的静态文件服务器?

如果你关心性能的话一定要使用 CDN,而对于 CDN 后面的真实服务器来说,最重要的事情是正确设置 HTTP 的 Header,最大限度的、正确的使用 CDN。所以这个问题更是一个逻辑是否正确的问题,而不是效率的问题。一个 静态文件 Request 的处理过程,其实主要是根据 PATH 来找到对应的文件并返回而已。对大多输的 WSGI 服务器(比如 Gunicorn)来说,发送文件的部分是对内核的 sendfile 的系统调用,效率是很高的,用 Python 还是 C,区别并不大。

下面是源码导读。

通过本文开头的那个例子可以看出,Whitenoise 实例化出来的对象其实是一个 WSGI app,它的 __call__() 方法如下:

HTTP 请求到了 WSGI 应用的时候会首先进入这个方法,然后尝试根据 PATH 寻找对应的静态文件,如果找到的话,通过 self.serve() 处理返回内容;如果找不到的话,就调用进入 self.application ,就是后端 Python 应用。Whitenoise 只提供了这一种配置方法,不像 uWSGI,有 4 种 serve 静态文件的配置方法。这里我有一个担忧是每一个进来的请求都要经过查找对应静态文件的逻辑,不知道对性能有没有影响。

serve 方法如下:

通过 static_file.get_response 方法拿到 Response,start_response 这一行是 WSGI 标准,返回 200 OK 这样的状态信息以及 Headers,最后以 list 形式返回 body。后面的 FileWrapper 是从 wsgiref.util 导入的,这个 FileWrapper 在 CPython 库里面的代码其实就是实现了迭代器,每次读 8k 的内容返回。如果用了其他的 WSGI 服务器,会拿到对应的 file_wrapper。从这里可以看出,Whitenoise 本身不处理 socket 发送文件的部分,真正负责这个的是 WSGI 服务器,比如 uWSGI 这种。

通过上面的介绍,我们知道这个库最重要的就是正确返回 Header,所以核心的逻辑都在 static_file.get_response 里面。

首先检查 method 必须是 GET 或 HEAD,否则就返回 403.

然后根据 etag 和 HTTP_IF_MODIFIED_SINCE 这个 Header 检查文件是否有修改。

接着解析 path 和 headers,注意这时候还没有打开文件,如果是 HEAD 方法,是不需要打开文件的,直接将 file 变量设置为 None 即可,后面发送的时候会自动不发送 body。

最后看是否是 HTTP_RANGE 请求,如果是,只读相应部分的文件。

主要的逻辑就到这里了,这个库的代码只有几千行,主要的逻辑就是上面介绍的这些。其他的一些包括 compress.py 有压缩相关的代码,middleware.py 和 storage.py 这些处理 Django 相关的一些 migration,scantree.py 处理查找文件的部分等等。

压缩的部分有一个值得注意的,这个变量将不需要压缩的文件的后缀列出来了。因为这些文件格式本身是经过高质量压缩的,如果启用压缩,会徒增计算成本,但是实际并不会减少很多文件体积。

总之,这是一个很小的库,但是对 HTTP 文件服务是一个比较值得参考的实现。Worth to read.

 

5年博客路

写博客 5 年了。

博客已经成为了我的习惯,自己有什么想法的时候,都喜欢将这些思绪理清楚,记录下来。想法只是想想很可能就那么过去了,但是想要整理成一片得体的博客的话,就该考虑周全一些了。第一篇博客写在高中毕业的时候,写下的东西读起来总觉得不完全表达出自己所想的,只能算是表达出六七分吧。写一篇要花我很长时间,最后还总不满意。现在不一样了,打开 WordPress 的编辑器,很快就能把心里想的记下来。我甚至已经对这个编辑器有感情了,一打开它就能找到很多灵感。

最初,总是担心没什么可写的,博客荒废在那里,首页停留着一篇几年前更新的文章,那可太丢人了(对自己的要求,没有觉得那些不更新的博客就不好)。也见过很多优秀的博客许久不更新了,很可惜。

但是从某一个时刻起,这件事默默的发生了质变。我很放心:我的博客永远不会停更。具体的原因我也说不上来,但是总感觉无论再忙,有一些想法不记下来就心里不舒服。那些担心也没有了,不会在意浏览量了,不会在意有人从这些文字中挑刺,虽然它们有时候前后矛盾,也不会在意自己有多久没写了,是不是要找个话题水上一篇来。

我把它当做自己的一个角落,虽然这个角落是完全公开的。

这里记录了我经历的一些事,有些很痛苦,只想写一写,写下来之后我从来没有去看过第二遍。也记录了一些我学到的东西,这是我最开心写的,我比较好为人师,学到一点点东西,就想讲给别人听,希望能帮到一些人吧。有时候自己回来看看,发现自己对这个方面还有过兴趣,也许能重新拾起来研究一下也说不定呢。也记录了一些杂七杂八的想法,比如看完一本书,看完电影,有时候想到了一些自己以前不曾想到的事,也会记下来。这些东西其实不成章法啦,写下来,最希望的是能有人来一起讨论一下,可惜的是,这部分内容实际上收到的有价值的讨论还非常少。还有有对自己做的事情的一些思考,比如做爬虫的时候发现这个工作其实挺有趣的,写了一篇介绍爬虫工作的文章。那时候一直想做一个定时执行任务的平台,只要你给我一个脚本,我可以帮你按照 cron 的方式执行它。在平台上可以清楚地看到执行的日志,像 Github 的 commit 日历一样做一个执行日历,一定很酷。但是一直没做下去(域名都买好了……),只是记录了一些想法

技术方面的文章无关乎深浅,再深奥的东西,其实只是抽象的太高,说不定你最后搞明白了发现是他设计的不好呢!所以并不是你看不懂的博客就是深奥的、写得好的(虽然大多数人都这样想)。有些自己觉得不值得一提的事情,写下来之后发现竟然帮到了很多人(如果你有访客记录的话,浏览量最多的文章一定让你大吃一惊)。所以我基本不会因为一个话题太浅而不写,专门挑显示我的技术高的东西去写。也不会去抄官方文档到博客上来赚取访问量。一个东西写与不写,对我来说就一个标准:我想不想写。毕竟这是我自己的博客,也没有人逼我,更没有人给我发钱。

我发现只有自己感兴趣的东西,写下来才是好的,字里行间都能流露出激情。不感兴趣的东西,肯定也写不好,要么像流水账一样,要么条例不清晰。王垠的这篇《解谜计算机科学1》挺好,但是只有第一篇,后面的太监了。没有了总比勉强写出来要好,我觉得。另外  agentzh 的 Nginx变量漫谈,也非常好,全都带着示例,讲的很明白,如果自己没兴趣,肯定写不出这么一大坨,还带着例子。好的博客是将复杂的问题说明白的,而不是写的别人看不懂的(故作高深)。这里有一篇《Programming for passion — not profit — produce high quality software》,讲的也是这个道理。

出自于兴趣做的事情,一定是比钱、虚荣、利益驱动的做的要好的。这也是为什么那么多人来参与开源,将项目做的那么好的原因。

这篇文章主要想表达一下我对博客的感情,因为我最近发现很多(不写博客的)人对博客有种误解,仿佛这件事情是用浏览量、评论量什么的来量化的。不是这样的,这只是茫茫互联网上的一个个人的角落。我把自己的故事留在这里,把自己读过的、听过的、看过的、想过的留在这里。搜索引擎就像小蜜蜂一样,有时候会将这些东西分享给全世界的人看,有时候这些东西就静静的待在这里。没有人能强迫你写下什么,也没有人能强迫你删除什么。

希望你也有这样一个角落。

 

开始用 Ansible 管理机器了

说起来很可笑,总说自己是 SRE,但是 Salt,ansible 这种东西还没玩溜呢。平时的运维和发布一般都是工单,要走一些审批流程,所以也没啥场景去用这些。今天部署一个东西,才发现自己已经重复在机器上编译 Python 这种事情干了三次了,好,是时候将这些操作自动化了。

之所以选择 ansible,是因为它是通过 ssh 协议的,不必再客户端上面安装任何东西,不依赖其他 broker 数据库等,ansible 项目本身就是 Python 脚本,所以安装和使用就非常方便。

体验下来,发现 ansible 被人吐槽“慢”真是一点都不委屈,实在太慢了。

我主要用的是 role 的功能来将平时要做的事情在服务器上组织起来。

一个 Compile Python 的 Role

ansible 提供了 role 来组织你的脚本,role 其实是 playbook 通过文件结构将不同的部分组织,用的时候在 playbook 中 include 就可以了。

下面以一个编译 Python 的 role 为例。

首先它的目录结构如下:

在 roles 文件夹里面建了一个 compile_python37 的文件夹,一个文件夹就是一个 role,然后按照 role 规定的文件夹结构在这个 role 中新建相关的比如 tasks

tools.yaml 中的内容如下,就是调用一下 role:

compile_python37/tasks/main.yml 中的内容如下:

可以看到用到了很多 "{{ xxx }}" 这样的东西,这相当于一些变量,我们将固定的操作写在 tasks 里面,将变量 vars 里面,这样之后如果要修改一些诸如解压地址的时候,就可以直接编辑 vars 了。

unarchive 可以直接写远程的 url,解压到本地的一个位置。

with_items 其实是循环,会循环 value 中所有的值,将它们当做 item 来执行一次命令。比如第一个 yum 命令会对 "{{ python_yum }}" 中的每一个值都执行一次。

compile_python37/vars/main.yml 的内容如下:

可以看到就是上面 tasks 需要的变量。

搞定,另外 galaxy 这里有一些大家写的 role,类似像一个 docker hub。不过我试了几个,直接用都有问题……

最后一点 Tips,单字母的命令放着不用太浪费了,推荐一下几个 alias:

再来一点 Tips,ansible 默认的输出实在是太反人类了,是一个压缩后的 json,换行符都没了,变成了一坨,编译命令、yum 安装命令的输出根本没法看了。找了一圈之后从一个 issue 里面才发现解决方案:设置 stdout_callback = debug 就好了,会以格式化的 json 来显示。也可以设置成 minimal 或 yaml,我觉得 yaml 比较好看。可以通过环境变量设置或者设置文件来设置。看这点赞数,颇有点“如何退出 Vim”的意味啊,为啥不弄成默认设置呢?