我是一个 Python 程序员,也对 Lua,Go,Rust 感兴趣。目前在蚂蚁金服工作。

我的日常工作离不开 Vim 和 Tmux,当然还有 Git。平时喜欢在看 Github 上有趣的项目,学习新的东西,看书,写博客等。博客的内容一般是我的人生感悟,观后感读后感,以及技术方面的内容。

我的梦想是让网络变得更加开放、自由和快速。

声明:本博客内容仅代表本人观点,和我的雇主无关。本站所有内容未加说明均为原创,一经发布自动进入公共领域,本人放弃所有权利。

证书换至 Let’s Encrypt(手动模式)

今天小伙伴告诉我博客的证书过期了(我隐约想起来 Kaybase 发给过我一封邮件告诉我网站所有权验证失败,被我给忽略掉了)。早上开始 renew 证书。这个过程太乌龙了,记一下吧,顺便推广一下 Let’s Encrypt。

之前的证书是腾讯云的免费 1 年的 TrustAsia 证书,用了应该两年了,这次第一反应也是打开腾讯云续命。后来万万没有想到被腾讯云的这个登陆搞了半天。

没想到腾讯云登陆不上了…… 需要我提供手机验证码,但是我现在的手机号试了十几次都收不到腾讯云的验证码。打联通的客服,客服让我发短信到一个号码,那个号码一直说“系统繁忙,请稍后再试”,网上的诸如 “发送 11111 到 106xxx 的号码” 也不好使,还有网友说让朋友尝试登陆一下他的账号,然后你往朋友收到验证码的那个号码发送 11111,我试过了也不好使。

我决定换成 Let’s Encrypt 的证书。

Let’s Encrypt 的证书一般是由 Certbot 自动签发的。但是像我这种用古老的 PHP 空间提供商的,就只好使用手动模式了。

以下教程是使用 Mac 系统手动申请网站证书的教程。

首先在 Mac 上安装 Certbot:

然后用手动模式开始申请证书(默认的 log 位置需要 root 权限,我直接用 sudo 跑了)。

然后根据提示输入域名,这里可以输入多个。

同意记录申请机器的 IP。

接下来会出现 ACME 挑战,这个挑战的意义是让你证明的域名是你的(否则的话别人就可以申请一个你的域名的证书,用来做中间人攻击了)。ACME 挑战有两种: http 和 dns,http ACME 挑战就是在你网站的 .well-known/ 下面放一个文件,显示特定的文本。dns ACME 挑战是添加一条 TXT DNS 记录到你的 DNS 解析中。默认使用的是 http,如果要使用 dns 的话,最开始的命令要添加一个选项,如下:

这里准备好证明(http 的话就放好那个文件,dns 的话就准备好解析记录),然后自己试一下(一定要试一下,不然失败了要重来),再按下回车,Cerbot 会去验证。

验证过后证书就颁发好了,Certbot 会打印出证书和 key 的路径。有效期只有三个月,所以手动模式的话还是挺麻烦的。

 

推荐 Vim 进阶书籍:《Vim 实用技巧》

最近读了《Vim 实用技巧》这本书,也是读的第一本有关 Vim 的书。此书是一种 Cookbook 的形式,每一个章节给出一个 Vim 的技巧。但是又不同一般的 Cookbook 书籍那样草草的给出解决方案,然后告诉你为什么。这本书会告诉你如何以一种“Vim 的思想”来做事情,如何将操作变成肌肉记忆,对已经能够使用 Vim 的同学来说,是居家旅行良品。

但是此书不太适合刚刚接触 Vim 或者想要开始使用 Vim 的同学,Vim 的学习曲线是比价陡峭的,这本书适合急速上升的那个阶段。对于刚刚入门的人来说,我觉得最好的方法还是删掉电脑上的 IDE 和其他编辑器,强迫自己使用 Vim,大约一个星期,基本上就能够习惯 Vim 的几种模式和 hjkl 移动了。然后你可以翻看这本书,去了解一些 Vim 里面的概念和思想,甚至还能学编辑器历史。

这篇文章记一下我从这本书学到的东西。

:help 是一个好东西。这本书也在极力像你推荐这个工具。很多地方都是一句话带过,然后后面跟上一个括号鼓励你自己去看:help 文档。其实我也知道看到文档,尴尬的问题是,我经常遇到的问题不知道是啥,在 Vim 里面叫啥,该去查什么 help,甚至我连怎么 Google 都不知道。这就是因为不知道 Vim 里面的一些概念。比如我之前经常不小心进入命令行编辑界面(按 q: 跳进去的那个)不知所措,我是谁,我在哪……看了这本书我知道了这是可以用上 Vim 基本所有的 feature 来编辑命令的一个界面,用于编辑长命令或者查看历史。是命令模式的一个升级版。这样的东西还有很多,所以我对这本书是相见恨晚啊!

从这本书中,我觉得最有用的一个思想是:编辑、重复、撤销。这个技巧的一个核心思想是:不要思考。比如要删掉 7 个单词,你可以选择 d7w ,一下子删掉 7 个,也可以选择 dw 然后按 . 重复 7 次。第一个选择你需要在心里算一下一共有多少个单词,需要按几下,这样就会打断你的思路。但是如果无脑的按 7 下就不需要思考了。那万一按错了怎么办?另一个重要的技巧是:所有的操作都成对出现。比如重复操作 . 对应 u 撤销操作;向前搜索 ; 对应退回 , ;回到上一个位置 Ctrl-o 对应 Ctrl-i。重复的时候无脑按就可以,发现按多了的话立即按撤销操作,可以形成肌肉记忆。另外一半“重复”操作都是使用的 . ,. 表示重复上一次的修改,这个“一次”很重要。需要注意一下尽量将操作合成一次,如果一次修改是由两次操作构成的,就不可重复了。

另一个我觉得比较好的思想是“重做比修复更快”,虽然这本书很少提,我忘记在哪看到的。举个例子,比如你打了 “bsah”,想改成“bash”,那么最快、最不用动脑的做法就是 ciw 然后重新打一次。无论对 vim 再熟练,这个做法一定是最快的(虽然可能别的路子可以让你少按几个键,但是不用思考才是王道,将脑子用在你要写的程序上面)。书中技巧 62 提到的是将 “fish and chips” 替换成 “chips and fish”,最少的替换方法也要按将近 20 个键,不如重新打一次呢。

Vim 的技巧很多,比如自动设置的标记,各种 text object 组合,快速移动等。而这些技巧的来源除了权威的文档,还有各种论坛,各种书籍,偷窥你同事的屏幕等等可以学习。这些都是比较零散的,还比较难记。但是 Vim 的学习就是这样的,各种各样的技巧,学的越多,用的就越熟练。对于这种零散的技巧,我的一个方法是:将平时学到的技巧用便利贴记下来,写代码的时候不时瞄一眼,渐渐就学会了。不必死记硬背,多用几次,感受到它的存在,渐渐的就记住了。建议你在读本书的时候就这样做。

另外很多概念、命令记起来非常复杂。比如 w 是快速移动一个单词,W 是快速移动一个“字”,那什么才是字呢? 空格隔开的算啥? "';: 这些符号隔开的算啥?我的经验是,这些都不必去记,如果一个操作你需要思考的话,速度就已经慢下来了,还影响你的思路。你的脑子应该用在考虑代码逻辑上,而不是用在怎么把代码打出来。拿刚刚这个例子来说吧,就简单的理解成“W 是更快的 w ”就好啦!如果移动多了怎么办?再用 w 或者 h 移动回来就好了呗。无脑的重复、撤销要比思考快的多。但是注意,无脑的重复也有高效的和低效的,如果你连 w 和 W 都不知道,那么就只能用 l 来移动了,这是无论你思考还是不思考都快不起来的。

这本书的第六部分和前面一些章节介绍了一些 Vim 插件,有一些已经过时了。比如现在文件浏览大家一般都用 NerdTree,还有好用的 CtrlP 这种模糊匹配的工具等。但是也建议看一下吧,了解一下各种插件的角色,看看他们的设计思想。说不定哪一天你也要写一个插件才能解决自己的问题呢。

 

最后再推荐几个我觉得可以快速提升 Vim 的技巧:

  1. 不要觉得修改自己已经熟悉的快捷键是多困难的事情,记住快捷键是很快的。一开始设置的快捷键可能不是很合理的,勇敢地去修改吧!合理的快捷键更加好记!
  2. 将自己平时操作觉得很烦的地方都记下来,有时间相办法去优化,写命令,或者找插件。解决自己的痛点。不要就这么忍着,你忍一次就会忍无数次。你怎么度过今天,就会怎么度过这一生。
  3. 去观摩其他 Vim 用户写代码(当然要在人家允许的情况下),从其他人的屏幕上能学到的技巧更多,因为你看到的都是人家日常用的,如果不好用人家也不会用。有时候还能发掘出跟多好用的软件呢。
  4. 逛逛 Github 的 Vim 插件,Follow 一些 Vim 插件的作者,有时候会发现一些有用的插件(虽然大多数时候发现并不是自己需要的)。
 

记一个 Python logging 多进程 rotate 问题

线下环境的日志轮转一直工作的很奇怪,今天必须要解决一下了。

我们 Django 的 settings 里面 LOGGING 配置了日志轮转,满了 100M 就开新的文件。奇怪的是我每次去看日志,发现都有很多 log 才几 M,根本没到 100M 就“轮转”了。实际的配置简化如下。

这个配置基本上是没有问题的。我把 maxBytes 改成 30M,然后观察文件的大小。发现刚开始是正常的,/var/log/django.log 一直在打,逐渐到了 30M。这个时候神奇的事情发生了,到了轮转的时刻,旧日志被 rename 成了 django.log.1 ,然后又被 rename 成了 django.log.2 ,突然,十几个文件都开始写入了,这些文件 last_modify 时间一直在更新,也一直在变大。

思考片刻,我觉得问题出在 Python 的 logging 模块并没有去处理多进程的问题。我的 Django 应用是用 uwsgi 跑的,开了 16 个进程。当日志快要超过 maxBytes 的时候,第一个进程执行了 rename、新开文件 A 的操作(这个具体的流程,看一下文档里对 rotate 的流程描述就明白了),开始朝新开的文件 A 写入;这时候第二个进程将第一个进程打开的新文件给 rename 了,然后自己又新开一个文件 B 写入。但是第一个进程依然持有文件 A 的句柄的,所以依然可以往 A 里面写入。以此类推,就导致 16 个进程大家都持有了一个文件句柄,大家同时向不同的文件写入。(这段流程是我根据现象推测的,如果不正确请指正)。

Python 自身的 logging 模块不关心多进程写文件的问题,我觉得是可以接受的。一个 Python 程序就是一个进程实例,无须关心所在的环境是什么样子。我考虑开始通过其他的方案解决这个问题。

尝试了 uwsgi 的 daemonize,遇到了两个问题:1)uwsgi 的日志和 django 的日志混在一起了 2)原本 uwsgi 的 log 是可以 rotate 的,结果我加上 daemonize 之后,日志里面出现一下错误提示,竟然直接不转了。

考虑到引入 syslog 这种第三方 log 进程或者 rotate,会增加运维的复杂度。而且得重新考虑 rotate、删除、写入不同文件等的方案,所以也没有去考虑用第三方进程的方式。

这个问题实际上是 rotate 操作执行的时候,导致了竞争条件,被执行了多次的问题。所以自然而然就想到,如果在执行这个操作的时候,能加一个锁就好了。只有一次 rotate 会被执行。找到了一个实现了这个功能 mrfh 的库。代码只有几百行,里面继承了 BaseRotatingHandler 实现了自己的一个 Handler,做的事情很简单,就是在 rotate 的时候增加一个基于文件的锁(基于 fcntl),保证 rotate 能只做一次就好了。

引入也很简单,只是安装好依赖,然后将 logging 配置的 handler 改成此库里面的 handler 就好了。

现在打出来的日志都是整整齐齐的 100M 了,另外他会在日志目录下生成一个 .filename.log.lock 作为锁,无伤大雅。

 

时区问题“最佳实践”

时区这个问题太头疼了,如果你编程,几乎会一定遇到这个问题。对一些时区没有学好的同学,每每碰到心里都会害怕的……其实这个问题很简单,你只知道自己在做什么就好了。

简单的介绍一下时区。中央时间是 UTC 时间,其他时区都是根据这个时间偏移的。中国的时间叫做“北京时间”,时区在“东八区”,记做是 UTC+8. 意思就是我们比中央时间快 8 个小时。我们过着早上9点,中央时间地区的人们过着早上1点(凌晨1点)。为了在程序中方便表达和理解,每个地区会用“区域/位置”格式来表达时区。比如中国的时区可以使用 Asia/Shanghai,纽约用 America/New_York 表示。

如果你在美国租一台 VPS,那么你将会得到 3 个时间:你在中国写程序,你的电脑上显示的“北京时间”,服务器跑着的是美国时间,然后很多程序会用 UTC 时间来表示。

如果不事先想清楚,你之后肯定会经历很多痛苦来琢磨一个时间到底是 UTC 时间还是美国时间。时间变量传来传去还经常弄丢时区信息,或者转换两次时区,都是很头疼的。我们之前做爬虫,保存数据的抓取时间、更新时间是很重要的,但是数据经过多次存储和处理,忘记了到底在哪里转换过时区,这个时间到底是什么时区的时间。后来开发了一个专门的组件用来处理,这个组件的逻辑又很不清晰,最后此组件的作者离职了,交给我来维护,所以……想象一下我的痛苦吧。

关于时区的最佳实践,和 Python 的 “Unicode” 最佳实践是一样的:三明治模型

拿到的时间处理成正确的时区,比如抓取了一个中国的网页,时区是北京时间,那么就生成一个北京时间。当数据开始保存的时候,只保存 UTC,这样你就知道,只要是内部系统,保存的时间全都是 UTC 时间。然后时间要进行展示的时候,再进行本地化。这个可以有些框架可以自动完成,比如 django。对于前后端分离的项目,可以考虑用 moment.js 这种框架实现。

另一个关键的点是:知道你的框架是怎么处理时间的

比如 Django,文档就说的很明白:当开启 USE_TIMEZONE 的时候,Django 会在数据库中存储 UTC 时间,对于 template 和 form Django 会自己在内部进行本地化。

When support for time zones is enabled, Django stores datetime information in UTC in the database, uses time-zone-aware datetime objects internally, and translates them to the end user’s time zone in templates and forms.

有关系统的时间,其实内部是用一个时间戳来计时的。Unix Timestamp 是不带时区信息的,因为这个数字代表的是从1970年(UTC)开始过去了多少秒。所以理论上讲,世界上所有的电脑在内部的计时是一样的,只是显示出来的时间是不同的。如果担心时区混乱,在系统中存储时间戳也是一个不错的方法。

但是需要注意的是,并不是所有的 Timestamp 都是不带时区信息的。我写这篇文章主要是因为今天因为 Celery 的一个时间问题调试了半天。最后发现 Celery 用的时间戳竟然不是 UTC 的,还带时区的信息!具体的代码可以看这里。我实在搞不懂他们在搞毛,为啥不用一个标准的时间戳。

另外 Python 有一个处理时间的库叫 arrow ,这个库因为一个 get() 函数就能处理各种各样的时间格式而受欢迎,之前我们要从网站的文本中提取时间,用了很多这个库。但是我的建议是:千万不要用!这个库的 API 太混乱了,比如 get() 当不写第二个可选参数的时候回忽略第三个参数 tz_info 而导致时区无法识别,写上第二个可选参数第三个参数才可以被识别(后来貌似已经修复了)。

推荐使用 Pendulum 。

另外时间有一个标准的格式,可能你已经见过了,形如这样:2019-01-13T16:27:26+00:00。这是 ISO 8601 规定的格式。大多数库都能处理这种格式。

 

React 事件绑定的正确姿势

最近在写 React,对于将一个事件处理函数绑定到按钮上,总是有一种感觉:这么写也行,那么写也行。所以就参考了文档,整理了一下各种写法。主要是参考 React 的文档来写的,非常基础,非 React 的入门同学,这篇文章可以不看了。

绑定事件最基本的做法如下:

第 18 行将 this.handleClick 绑定到了按钮上。注意这里第 7 行在类的初始化函数中有一个绑定 this 的操作,这个是必须的。如果不写,那么 handleClick() 函数中的 this 将会是 undefined (这个是 JS 的特性,不是 React 的)。在 console 中会看到错误如下:

如果你写过 jQuery,从这个例子中可以看出,和 jQuery 绑定事件的区别如下:

  1. 不是传入一个 string ,而是函数
  2. 组织默认的形式不是 return false 而是 e.preventDefault()
  3. jquery 是小写命名,React 是驼峰命名
  4. 不要往 DOM 上面去添加 Listener,而是在 render 的时候提供 listener

如果不想每次在初始化函数中写一个 bind,可以有两种方案:

第一种,将函数定义为 class 的一个 field:

第二种,绑定的时候定义一个匿名函数(箭头函数),将匿名函数与元素绑定,而不是直接绑定事件处理函数,这样 this 在匿名函数中就不是 undefined 了:

第二种方法有一个问题,每次 LoggingButton 渲染的时候都会创建一个新的 callback 匿名函数。在这个例子中没有问题,但是如果将这个 onClick 的值作为 props 传给子组件的时候,将会导致子组件重新 render,所以不推荐。

Bind 函数的时候传递参数,下面两种写法都可以(推荐第二种):

在第二种中,e 代表 React Event,将会自动作为第二个参数。