MIME types 详解

MIME type 的全称是 Multipurpose Internet Mail Extensions (MIME) ,可以标志一个文件的类型。IANA 的网站上有一个正式的 MIME type 的列表,为什么会有这个列表呢?因为并不是所有的文件类型都有 MIME type,这个MIME type 也不是服务器可以随意设置的,服务器/浏览器两头都要实现相同的标准。基本上只有使用广泛的、性能高的、安全的文件类型才会被加入,因为每加入一个,浏览器厂商、服务器厂商都要去实现,成本比较大;二来风险也比较大,如果压缩比率不高可能会浪费网络带宽,严重的可能带来安全问题。服务器正确的设置 MIME type 也非常重要,否则服务器不会解释资源,比如不会播放视频或音频等。

它的语法是 type/subtype ,中间是 / 隔开,不区分大小写(一般都是小写)。type 表示一个大类,可以是视频、音频、文本等。subtype 表示具体的格式,jpeg png 等。type 又分成两种,一种是 Discrete(独立) 的,另一种是 multpart 。

Discrete type 分为下面5种:

  1. text 文本
  2. image 图片
  3. audio 音频
  4. video 视频
  5. application 一般指二进制数据

如果不指定 subtype ,那对于文本文件默认的就是 text/pain ,二进制数据默认的是 application/octet-stream 。它表示的是“未知类型的文本文件”,并不是指所有的文本文件。举个例子,如果 <link> 标签里面的资源应该是 text/css ,如果使用 text/plain 的话,浏览器并不会把它当做一个 CSS 文件来解释。

application/octet-stream 实际上表示的是“未知的二进制数据”,因为安全考虑,浏览器不会执行它或者尝试解释它。如果 Header Content-Disposition 设置为attachment,浏览器会弹出“另外为”对话框提示保存。

Multipart 一般由多部分组成,比如 multipart/form-data 一般用于浏览器将 HTML 的表单发给服务器。其中用 Content-type 中定义的 boundary 来分割。每一个 “part” 都是一个实体,对于上传表单的字段,每一个 part 都有 HTTP header Content-Disposition 和 Content-Type。因为有 boundary 可以分割,所以 Content-Length 会被忽略。比如下面这样的一个表单:

将会发出的 HTTP 请求如下:

之前对接过一个接口,要求每个请求都要带上证书文件,我就是用的 multipart 发出的。

除了常见的这个,还有另一种叫做的 multipart/byteranges 的 MIME type。介绍这个类型之前,我们先了解一下 HTTP 协议支持的“部分相应内容”。HTTP 的请求可以设置 Range 字段,要求只返回请求部分的 bytes。(要求服务器支持,可以通过 Response 的 Accept-Ranges: bytes 判断是否支持)比如下面这个请求,就只会返回 1K 大小的内容。

发出的 HTTP 请求如下:

Response 如下:

注意这个 HTTP 响应的状态码是 “206 Partial Content”。表示返回的是部分内容。

由此,可能你已经想到,我可以一次请求多个“部分内容”吗?答案是肯定的,这个时候服务器返回的 Response 中,Header 的 Content-Type 就会是 multipart/byteranges。其中每一个“部分响应”都会带有 Header Content-Type 和 Content-Range。比如这个请求:

得到的 Response 如下:

关于 Range_requests 的更多内容可以参考 MDN 的有关部分

最后,如果 MIME type 缺失的话,客户端可能去尝试猜测它的类型。这在不同浏览器的表现是不同的,可能有安全隐患,比如某些被资源被认为是“可执行的”。服务器可以通过设置 X-Content-Type-Options 来禁止客户端进行猜测。

除了设置 MIME type之外,还有两种方法可以表示文件类型:

  1. 使用文件后缀名。在 Windows 系统中比较流行,但是这只是一种约定,并不是所有的文件的后缀名都是通用的,有意义的。特别是在 Unix 系的系统中,这只不过是“名字的一部分”而已。
  2. Magic numbers。比如 GIF89 的文件使用 47 49 46 38 39 开头,PNG 使用 89 50 4E 47 开头。但并不是所有文件都会有 Magic numbers,所以这种方法也不是100%可靠的。

在实现方面,一般 HTTP 服务器会帮你处理好 MIME type Header 的设置,比如 Nginx 会使用 mime.types 文件判断什么后缀的文件该返回什么样的 MIME type。

 

参考资料:

  1. MDN文档
  2. wiki
 

构建大型Cron系统的思考

之前因为项目需求,看过一些 Python 相关的定时任务实现,当时比较好奇的是它们是怎么解决重复执行问题的。如果部署多个副本,就会产生多次执行问题;如果部署单点可以保证只执行一次,但是高可用又是一个问题。

如果能有一个非常可靠的 Cron 系统,可以:

  1. 保证高可用
  2. 保证正确性(不能重复执行)
  3. 让我很方便地看到所有任务的执行情况,最好是一个 web 界面,不需要登陆机器看日志
  4. 方便配置

就能解决我的一大痛点了。这个想法在以前写爬虫的时候就有了。因为线上的爬虫肯定不是跑一次就完事了,需要每天执行/每周执行,保持追踪目标网站的更新。那时候我们是用的单机部署的 crontab,定时放进去任务,虽说运行了很长时间也没啥问题,但是只能说是运气吧,如果它挂了,我们可能要花很长时间发现、修复。

最近一直在读《Google SRE运维解密》,这本书介绍了各种大型系统的一些指导思想和经验。《24 分布式周期性任务系统》这一章又给了我一些新的启发。

总的来说实现一个高可用、正确的 Cron,我觉得可以有三个思路:

第一种是我在前文中提到的,使用一个分布式的任务队列,将任务的调度和执行分开。然后在定时放入任务的时候根据任务、时间生成一个唯一 token,然后拿 token 去队列里放任务。应该可以解决分布式的问题,即高可用又是幂等的。(只是我的一个想法,还没听说过谁是这么做的,不知道行不行得通)

第二种跟上面的思路一致,也是做调度和执行分离,但是用了外部的数据库保证一致性。(这么实践的公司应该比较多)

实际上上面两种是比较粗糙的,本质上的问题用了简单的冗余。

然后就是我在《Google SRE》中看到的第三种。Google 的 Cron 是用了分布式的方式(我认为这个思路才是正确的,高可用是分布式要解决的一个典型问题。用一个叫做 Paxos 的分布式协议,多个节点保持同步,只有一个节点 master 在工作,将任务的执行情况同步给从节点。如果 master down 了,马上通过选举出现一个新的 master,因为这个 master 一直在和从节点同步状态,所以新 master 也有之前任务执行情况的信息,任务不会被重复执行。Master 在切换时会有很多细节问题需要处理,比如 master 开始执行一个任务的时候,需要把开始执行的信息同步下去,而且必须是先同步再执行,否则执行期间出现了 master 切换,那么新 master 有可能不知道此任务已经被执行了。

这里有一个“部分失败”的问题。其实这个问题比较经典,不仅存在于 Cron。比如 Django 的 migrate 操作,每次失败了都很头疼,要手动恢复数据表重新执行。

《Google SRE》书中提到,要解决“部分失败”的问题,就要实现下面两种至少一个:

  • 所有操作都要是幂等的(这样重复执行不会有问题)
  • 任务的所有操作都可以通过一个外部系统查询所有操作的执行状态(这样切换过后可以针对此任务失败的部分重试)

不幸的是,这两种方案的成本都很大。

正确性和高可用这两个问题,暂时就想到上面三种方案。在我看来,Google 的做法是最好的。

另外还有一个小问题,就是写 crontab 的时候大家都喜欢写在 0 点执行,而文本的 crontab 又很难看到已有任务的分布情况,所以就导致最后 crontab 变得非常集中,0 点的任务特别多。Google 的方法是在 crontab 的语法中添加了 ? 符号,比如如果分钟上面是 ? 就表示任意分钟执行都可以。可以交给系统去调度。

 

Django2.1版本不再支持Mysql5.5

好吧,这其实并不是啥高深的东西。如果你升级之前老老实实看了 Changelog,就肯定不会跟我这么倒霉了。

这件事情还是今天我们的 Gitlab CI 莫名其妙挂掉了。日志如下:

其中:

  1. 本地运行是 ok 的
  2. master 分支的CI是 ok 的
  3. 挂掉的那个分支添加了新的依赖 anytree

大量被浪费的时间……

一开始我怀疑是代码的变更导致生成了不合法的 SQL,于是仔细看了一遍 migrations,没发现什么异常(这是白费功夫,因为显然 migrations 还没开始执行就挂了)。

于是尝试打印出来执行的 SQL,因为 CI 是在 docker 跑的,于是我直接进入 docker ,修改 SQL syntax error 的那个文件,打印出来 q,结果如下:

我把这行代码拷贝到本机的 Mysql 执行,没问题啊(废话,我为啥要这么做啊…… Docker里面的 mysql 和我电脑上的明显版本不一样)。

于是在 Docker 的 mysql 执行,嗯,确实语法错误。而且我确认了是支持 datetime 不支持 datetime(6)

但是为啥之前好好地,突然就有问题了呢?唉,这里浪费的时间就不说了,总之我后来终于顿悟发觉是依赖更新了导致的。因为 requirements.txt 添加了一个新的 package ,里面所有的东西都重新安装过(新建的 Docker image),然后因为我们的 pip 没有锁版本,最近 Django 发布 2.1,CI 的 image 由默认的 2.0 版本升级到 2.1 了。

我用两个 Django 版本跑测试,打印出来 SQL 日志,发现两个版本生成的 SQL 语法确实不一样。

其中,Django2.0 创建 migrations 生成的 SQL 如下:

然后当前 pip 默认版本的 Django 2.1 创建 migrations 生成的 SQL 如下:

唯一的不同就是 datetime 变成了 datetime(6) 。

而我们的 CI image 用的是 python-3.6-jessie ,apt-get 安装的 mysql 版本是 5.5,不支持 datetime(6) 这种语法。

但是好好的 migrations 咋说更新就更新了呢? I blame Django。

然后我就发现人家明确说不支持 Mysql 5.5 了:Dropped support for MySQL 5.5 好吧,算我活该。

反思:

  1. 测试环境和开发环境不一致是个坑
  2. 不锁定 pip 的版本是个坑,有点想用 Pipenv 了
  3. 思路不清晰,随着自己“无根据的猜测”去排查浪费了时间(pip list 都是在 CI 打印出来的,认真看看就能发现)
  4. 自己用的项目就应该好好的 watch,看 Changelog!
 

黄金年代的辉煌和焦虑

最近失眠比较严重,看了一个电影,伍迪艾伦的《Midnight in Paris》。前段时间看过他的《Vicky Cristina Barcelona》,很不喜欢这部电影,我不明白他想说什么。《Vicky Cristina Barcelona》将爱描述的非常轻浮,出轨在伍迪艾伦里面的电影是在太简单了,里面的男人和女人要么就是虚伪的保守者,要么就是放荡的花花公子之类,电影背景乐一直再重复,让人很烦躁。有点跑题了,还是说会《Midnight in Paris》吧,这部电影和《Vicky Cristina Barcelona》有一些共同点:都是在如画的欧洲拍的;都有出轨。甚至有点《The Grand Budapest》的感觉。我看了一下演员表,男主角 Owen Wilson 竟然在《The Grand Budapest》饰演了一个配角,还真是巧。

右一是 Owen Wilson

影片讲述的是男主角 Gil 和未婚妻 Inez 一家人来欧洲旅行,Gil 滔滔不绝巴黎是多么美丽,如果留在这里该多好,但是遭到 Inez 妈妈的反对。后来在餐厅他们碰巧遇上了保罗夫妇。于是接下来的行程就被未婚妻安排了跟保罗夫妇一起,品酒,舞会,自驾去凡尔赛宫。保罗是个非常爱掉书袋的讨厌鬼,于是 Gil 成了这趟旅行中最不高兴的一个人。在一天晚上未婚妻 Inez 决定跟保罗夫妇去跳舞,Gil 决定自己回家。回家途中却迷了路,这时候来了一辆车,里面的人快活的叫他快上车,Gil 就这样不明不白的上了车,去了一个聚会上。这时候高潮来了,在聚会上一个男人竟然自我介绍是 Fitzgerald!男人怀中的女人是 Zelda Fitzgerald!不可思议的事情接连发生,弹琴的人竟然是 Cole Porter。Fitzgerald 将 Gil 介绍给了 Hemingway,Hemingway 答应让 Gertrude 帮他批改小说草稿。

这是 Gil 梦想的  Golden Age,这是一个文学的黄金年代,人们在酒吧里面讨论文学和艺术,酒吧里的人都是一些大名鼎鼎的作家。

然而这个时代也在悄悄告诉他这个时代的名人的烦恼,比如被妻子左右的 Fitzgerald,Heyminway 批评他一个作家应该花时间在写作上。其中 Heymingway 在车里与 Gil 的一段对话非常有趣,Gil 求 Hemingway 看看他的草稿,Hemingway 却说:永远不要让同行评价你的作品,我恨糟糕的写作,如果写得好,我会更加憎恨你,我会嫉妒你。一个作家要宣称自己是世界上最好的作家!这里还有一段有关真爱和恐惧的言论,比较露骨,我直接贴在这里了。

You’ll never write well if you fear dying. Do you?

But it’s something all men before you have done and all men will do.

Have you ever made love to a truly great woman?

And when you make love to her you feel true and beautiful passion and you at least for that moment lose your fear of death.

I believe that love that’s true and real creates a respite from death. All cowardice comes from not loving or not loving well which is the same thing and when the man who is brave and true looks death squarely in the face like some rhino hunters I know or Belmonte who is truly brave, it is because they love with sufficient passion to push death out of their minds till it returns as it does to all men and then you must make really good love again. Think about it.

以前我总觉得,我生活的年代就是“黄金时代”了。我们有互联网,有了8核的处理器,有了超大的内存,有4G网络,有了 Go 语言和 Rust。现在的人们生来就可以享受这一些,不必经历几百年在计算机方面从电子管到大规模集成电路的苦苦探索。世界上的编程语言都有垃圾回收机制,Python 甚至写起来就像英语一样,不再需要自己从一些底层的概念抽象出来。如果没有了计算机和互联网,真不知道我应该怎么生活下去。

但是有时候,我又觉得即使有了这些东西,人们的生活还是太空洞了。看过一本叫《黑客:计算机革命的英雄们》的书,以前,从MIT开始的第一代黑客,大家恪守的都是分享的精神,大家分享彼此的一切,为了使用计算机可以做任何事(将封锁起来计算机不让别人使用视作是资源的浪费)。而黑客文化的没落也是企业家精神的崛起。所有的东西都被商业化了,现在看看我们的周围吧,所谓的技术,在很多方面不过是让人们多看两眼广告,多收集一些用户信息,甚至让有钱的用户享受“个性定价”而已。

如果能像电影里的 Gil 一样,坐在美国的街头,一辆马车把你接到卡马克的湖边小屋,认识一群正在开发《毁灭公爵》的黑客们。那个游戏黑客的“黄金年代”,该多好啊。

影片中还有另一次穿越,Adriana 和 Gil 被一辆马车接走,来到1893年开张的马克西姆餐厅,遇到了超现实主义大师 Dalí!达利给他们介绍 Gauguin,De Gas。几位大师抱怨现在的人们缺乏想象力,他们怀念文艺复兴。这时从 20世界20年代穿越到19世纪90年代的 Adriana 说这是她的梦想的黄金年代,她要永远生活在这里。Gil 说:这不是你的年代,这也不是我的年代。这里没有洗碗机,也没有抗生素,拔牙的时候你会意识到这里没有麻醉剂(Adiana 表示这是什么?)如果你把这个年代当做你的“现在”,那么迟早你会发现有另一个时代才是你的“黄金年代”。你的时代会让你不满意,因为生活本身就是让你不满意的。这是逃避现实与虚幻的精神失常之间的选择。

最后,Gil 回到了2010年,与出轨的未婚妻分手,选择留在巴黎完成自己的小说,这算是放弃了自己对黄金时代的幻想,勇敢的面对现实吧。

黄金时代大概是我们都会对一个无法得到的年代的幻想吧,因为得不到,我们对其美化。我们将自己在现实无法得到的幻想寄托在那个时代。一个时代有一个时代的进步,也有一个时代的焦虑。即使能回到过去也无法摆脱焦虑,你会得到那个时代的焦虑。我们所生活的时代,确实就是最好的时代。

 

程序员如何写PPT

这个世界上有两种文件,一种是二进制文件,一种是文本文件。虽然本质上都是编码成二进制存储在磁盘上,但是文本文件通过编码规则转换成二进制,可以很方便的通过几乎任何程序解码出来。文本文件是给人读的,二进制文件是给机器执行或载入的。所以如果二进制文件拿来给人读,这就很反人类了。像 Office 的 .doc .xls .ppt 就是这么一些反人类的玩意儿。

幸运的是人们发明了 markdown, html 等可以用纯文本表示出复杂的格式。如果是文本文件,很多事情就变得方便了:你可以使用几乎任何浏览程序或者编辑程序打开;你可以使用 git 对这些文件进行版本控制;可以为他们制作一个自动集成的系统(比如格式检查等);纯文本的格式是确定的,只要遵守格式规范来编写,不像是 .ppt 文件被 Keynote 和 PowerPoint 打开,显示效果可能是不同的;最重要的是:你可以使用你最喜欢的 Vim 编辑器编辑他们!配合适用各种类型的插件,你就可以编辑世界上的任何东西——只要它们是文本文件。

这篇文章会介绍一些不使用微软的 PowerPoint 来制作演示文稿的方法,好处呢,就是上面所介绍的那一些(完美的纯文本),坏处呢,就是可能会有一个 build 的过程,无法所见即所得了。

1 使用 gitpitch

这个是最简单的一个,你只要在 Github 上面上传一个 markdown 文件,然后通过这个网站打开,就自动变成一个 Slides 了。只不过你的 markdown 要稍微对 gitpitch 做一下兼容。其实这个我之前在博客分享过,在公司分享过一次 tmux 就是用这个东西做的 Slides 的。

更新一下评论的 Lisp 大神推荐的 Marp,跟这个思路差不多,也是将 markdown 渲染成演示,但是是本地的编辑器,所见即所得。

2 使用 Jupyter

可能做数据方面的同学比较熟悉 Jupyter,这种基于网页的 REPL 比较好的地方就是渲染图表的时候可以直接显示。Jupyter 功能非常强大,直接运行代码、markdown格式、做图表样样都行。做 Slides 展示也很好看。看过一个同事分享的,效果非常不错。使用教程可以参考这篇。(上图亦来自这篇教程)

3 Read the docs

第一次见到有人使用 readthedocs 做分享是在 Raymond 的视频中(上一篇博客提到过)。readthedocs 显示非常简洁,渲染出来的代码块也非常漂亮,在 Chrome 下按 Command + + 就可以放大,效果很好。左侧的可以嵌套的目录正好可以做一个大纲展示。readthedocs 真是简洁又实用,我翻译的一本书也放在上面,效果可以作为参考。

这种方式的一个额外的好处是,随便找一个文件托管服务,比如 S3 bucket 或者 github pages 就可以,然后随便一台电脑就可以打开访问演示。而且这样放在网上访问方便,不会像 .ppt 文件不兼容新版啦等问题,作者如果更改可以一下子同步上去,PPT 这种文件就像泼出去的水,发给别人就再也更新不了了(当然也可以“最终版” “再也不改了” “最后版本” “最最后版本”发给别人哈哈)。缺点就是要学习 sphinx 学习成本可能会高一些。

4 Racket SlideShow

Racket 套件

Racket 套件

这个是我偶然发现的,没有实际用过。安装 Racket 之后竟然出现这么多套件,有游戏,还有一个 SlideShow???看了一下,竟然是用 Racket 代码来写 PPT…… 听说貌似还有不少用户。

Racket源代码

好吧,Lisp 程序员的世界我有点不懂……

另外再赠送一个呗。推荐一个画图的工具,Graphviz。这个有 Python 的封装,可以使用在项目中,也可以直接手写文件画,还蛮好看的。这里有个画 Python 继承关系的例子。可以看下。

miloyip老师画的

我最初知道这个还可以拿来直接当作图工具用是看了 miloyip 老师的推荐,有兴趣可以看一下这个回答:Mac 上最好用的流程图软件是什么?