莉莉安

《莉莉安》

是一个黄昏,天色渐暗,我爬上房顶,等着日落。

脚下是我自己搭建的木头房子,房顶扁平,有一根藤梯连接到这里。

四下没人,只有高大的热带树木,另一侧是一片大海。干净的空气,安静有暖风。

太阳在大海的一侧渐渐落下去,周围越发昏暗,远处一片绯红。

落莫凄美的夜,远处泛着白光。

《理想三旬》

慵懒的下午,窗外下着连绵的雨。是南方的三月。

陷在陈旧的沙发里,破碎的窗口有风吹进来。淡淡的霉的气息,让人心情舒畅。

皮革箱丢在一边,被旧衣物围成了一个圈,像是一个倍受尊敬的老伯。

窗上还牵扯的纸片飘摇不定。

GTD实践分享


关于写作

现在几乎一周可以出一篇文章,闲暇里我也想过这个问题,也获得了一些结论。

关于写作时间 :对我来说,写作已经不需要消耗多少时间了,因为我在电脑前坐下写作之前,文章的内容和大体框架实际上已经完成了,这只是一个再现的过程,或者把之前整理的思路重现的过程。是的,写作其实是走在路上完成的。

关于写作目的 :现在写作的目的已经完全不是我预想的目的了。最开始的想法是可以帮助他人,但是写作这件事已经摆脱了我的设想,朝着自定的方向发展了。他想做什么呢?

以前跟小郭出去逛街,不像真实意义上的逛街,不重在吃和买,倒像是一次长谈。我们一边走一边讨论生活,交换价值观,很多思想也都互通有无。我喜欢这种交流。

而现在的写作,更像是跟自己进行的一次长谈。平时很少有机会可以静下来跟自己聊聊,现在有了一段跟自己交换思想的时间。当然也是跟看到这篇文章的人交流,当然,交流的目的还是互通有无,不问对错,只需要了解。从这一点上来说,写作是自私的。

其次呢,写作更是一种学习的途径。我看很多人的技术博客或者文章,都是对过去的一些事情的总结。我现在写作并不是对过去的总结,或者在必要的时候总结过去只是为了更好地梳理以帮忙到现在或未来。每当我想学习一个新东西的时候,就会在日程上安排一个写作计划,制定一个deadline,积累素材的过程实质上就是一个学习的过程。

关于第二点,划下重点, 写作不是总结

在写作的过程中,我还会有一些额外的收获,比如在写作这篇文章时,在希望有一个手机全局软件可以作为GTD收件箱的时候,我想到了闪念胶囊,但是由于苹果的封闭性,市场里只能找到一些拙劣的替代品。虽然锤子科技17年有说ios版在开发中,可以已经流产了。

一句话,如果想学习某个东西,就制定一个写作计划(思想)或者做一个成品的计划(技能)吧!

时间管理简史

混沌期

学生时代并不需要管理时间,时间都是被学习管理好的,明明白白。

到了大学的时候,也不太需要管理时间,想做什么做什么,想做的不多,时间倒挺多。

这段时间我称它为混沌期,或者叫被动管理期。一直延续到工作以后。

初见workflowy

工作以后,时间安排的重要性就慢慢体现出来了。

随着时间的推移,我们cover的事情越来越多,而精力又越来越少,不得不将自己的时间管理起来。

时间管理,其实是精力管理。

工作以后,我们或早或晚地会意识到计划的重要性。它可以指导我们时间或精力的分配,不至于因为事情太多感到茫然无助。而且在汇报工作时,也有一个好的参考。

最开始我是记在文本里的,一天一记,定时总结(大部分时候都感觉总结没必要,就直接跳过了。走到某天意识到遗失了一批最有价值的宝藏 :p)。

PS: 刚才突然意识到很多人为什么用:-p而不用:p,因为:)在编辑器里的括弧会匹配到左括弧我的天。

后来,认识了workflwy(好像是小金推荐的),瞬间被它的简洁和优雅打动。我甚至极度迷恋到以为可以用它来管理我的整个后半生。

最初使用的过程中,除了加载会有点慢,没有发现其它的问题。我至今都不明白,zf做的这些“努力”到底是为了什么。

不过在后来的一次更新中,登录后数据怎么都加载不出来。跟开发者反馈后,经过复杂的五步操作,解决了这个问题。后来这类问题又反复了几次,最终决定卸载。

在写这篇文章前又试了一下,还没有恢复,也可能是手机缓存的问题。不过我不想再把自己的精力浪费在别人反复出现的问题上了。这时候,我意识到纯文本管理的便捷性,这是我使用org-mode的一个重要原因。

我换用了中国人开发的workflowy copy版app幕布,数据加载没有问题。唯二的缺点是使用更复杂和夹杂着使用体验上的bug(键盘RETURN键失效)。第一点是团队的设计原则问题,没法改变。第二点已经反馈给官方。

不过,GTD才是解决时间管理的真正方法。

由于我写作大多是在闲暇时间完成的,所以习惯用手机构建,workflowy类软件多终端同步,可以解决这个问题。注意,workflowy适合整理思路,不应该被用来做时间管理。

apple notes

这是我在换到苹果全家桶后非常喜欢的一款软件,它简单,开箱即用,支持多终端。

我曾经有很长一段时间用它来记录信息,支持图片,搜索也方便。我还用它来写文章,写完后直接拷贝到blog就可以发布了(后来意识到用它写文章是不方便的,手机适合助攻,电脑才是王道)。

甚至我尝试过用它做todo,不过在做todo的过程中,发现了一堆bug,皆因它自动把todo条目转换成图形导致。这让我再一次意识到纯文本的重要性。其实转换不是问题,但是操作的一定要是文本,否则出了问题就是单向不可恢复的。

拥抱GTD,拥抱org-mode

如果你没用过GTD,我要说明一点的是,它不止是todolist。

这一点值得被反复强调,它不止是todolist。

一个很重要的关键词是收件箱,GTD是一个系统,它会把你接收到的思想或者事件,都放到收件箱里。收件箱会再次被转交到GTD系统,来缓解我们在记忆上的压力和精力分散的问题。

为什么要单独提收件箱,因为org-mode里的GTD已经很完整了,经过少量的配置就可以高度可用。剩下的问题只有,如何跟外部打通。最为关键的就是,用什么来做收件箱?

如果使用org-mode,电脑端已经毫无疑问是remember命令,但是手机端呢?当我们离开办公场所,离开家,只有一个手持设备时,如何快速记录临时的想法呢?

我现在仍然坚定的认为,闪念胶囊,或者说闪念胶囊之类的设计,是最理想的收件箱。

我尝试过使用notes来做收件箱,但是效果并不理想。打开notes需要很多的思考,它不是“不假思索”的。而且手机上的回车和电脑上的回车有天壤之别,电脑上的回车跟其它键是一致的,但手机上的回车跟字母键(在人的直观感觉上)是分离的,而且回错了要删除,删除键也是分离的。

退而求其次的方法是使用一个todo工具或者workflowy类工具。

任务类型的思考

就我个人的思考,任务分很多种,每种任务的处理方式是不一样的。

对任务类型的分类,是这篇文章中思考密集度最为集中的地方,也是这篇文章指导我要思考的点。

任务的分类并非严格按照重要紧急四象限排列,在GTD的实践中,我发现它们之间有些许不同。所以我决定先忘记四象限,按照GTD的思路整理一下。

临时性紧急任务

此类任务可能会打断我们当前的状态,需要立即去执行。

比如刚才我在写作的过程中,突然外卖到了,门铃响了。这就很紧急,如果我不给他开门,中午就没饭吃了。

临时性非紧急任务

此类任务也会打断我们的状态,但是由于不紧急,可以将它加到 收件箱 ,然后快带恢复到之前的状态。

它会耗费我们一定的精力,但是不多,包括两方面:一是任务细节的沟通,二是加到收件箱的操作。所以为什么我很重视收件箱的设计,因为它是保证我们可以快速恢复到正常状态(之前状态)的非常非常重要的一环。所以收件箱最好是全局的,是可以一键唤起的。

这类任务当我们整理收件箱的时候,会加入待办,或者更进一步,对待办排期。

一个很好的例子就是,你的朋友打电话约你周末去图书馆看书。

注意,上面这两类任务还不能称其为任务,或者还没有进程GTD系统。因为它们一个不必要进,一个还在收件箱里等待进。

后面的这几类任务,就都是GTD系统里的任务了。

scheduled任务

临时性非紧急任务可以转为排期任务,它很重要,但是不会立即去做。它不仅是重要不紧急,而且还有一个属性,就是利用一段确切的时间(比如3个番茄钟)是可以完成的。

deadline任务

这类任务比较有目标、有雄心、有毅力,它表示你要在一段时间内努力达成某种效果。

比如,你计划在半年内打到《王者荣耀》王者段位。这类任务需要很好的自控能力,因为达成任务需要消耗的时间不确定,如果你没有自信,可以把任务细分为子任务,这样就拆解成了排期任务,实现的难度就大大降低了。但是没准,你就是想挑战一下自己呢?

repeat任务

这类任务挺悠闲,没有任务,或者说完成它们本身就是任务。这类任务更侧重提醒功能,去做而不是做完。

比如,我做了一个repeat任务,为了保持自己的形象,每隔两天刮一次胡子。

maybe任务

这类任务还没有排期,不重要也不紧急,甚至不一定要做。它只是我们的一些奇形怪状的想法,可能在某天突然来了灵感或者突然变得重要,然后就被加到我们的排期里。也可能这辈子都不会被我们注意到了,谁知道呢?

但是一定要记下来,好让我们知道,我们已经考虑过了,而不是再考虑一次。

哪些任务需要记录耗时

我喜欢org-mode里的org-clock-in功能,它能让我知道做一件事耗费的时间。

当然,工具就是工具,不是所有的事情都需要知道耗时的。

比如,我每隔四天做一次俯卧撑,但是并不需要知道它的时间。根据我最开始的计算,每次都是3分钟左右。

阅读是一个与之相对的例子,因为我每次阅读是以章节为单位的,而不是时间,所以我需要记录每次阅读的时间,以知道读一本书需要多在成本。我不喜欢那种每天花 半小时 读书的计划,它太死板,书有逻辑性。

再比如写作,不同的文章内容,耗费的精力和时间是不一样的。当我在写下这篇文章时,编辑器的下方会提醒我在这篇文章上花费的时间。看,已经1小时40分钟了,不过这篇文章也已经接近尾声了。

最后一句话,适合自己的才是最好的。

emacs keymaps

Table of Contents

  1. emacs中的keymaps
    1. keymap结构体
    2. 行为覆盖
    3. 两个特化的问题
      1. prefix keys
      2. mode继承
    4. 结束

emacs中的keymaps

emacs具有很高的可配置性,从keybinding,customize,到UI,再到各种第三方package。

keybinding是学习emacs的过程中必经的一环,很难相信有人会不加修改地使用原生的emacs按键行为。

当然,keybinding由来以久。甚至在很多现代的软件中,给它取了一个更激动人心的名字,叫做快捷键,像是一项了不起的发明。其实在emacs里,是最最基础的一项功能。

在这篇文章里,我将尝试从底层的概念开始,讲解emacs里的键绑定机制,以让读者可以自由地定制emacs里的按键,并知其所以然。

keymap结构体

emacs的keybinding有一个基石,或者说设计原则,那就是每一个按键都绑定到了一个函数。从键盘输入,到鼠标,再到菜单,无一不在这个指导原则之下。

试想一下,如果我们来实现这样一个功能,必然是需要一个结构体来关联按键和功能函数的。这个结构体,可以帮助我们快速找到一个按键对应的功能函数。

emacs里使用的列表叫做keymap(大部分编辑器都使用这种称谓)。我们可以使用(keymapp)来检查一个结构是否是标准keymap,实际上它是通过检查列表的第一个元素是否是’keymap来判定的。

(keymapp '(keymap xxxx))  ;; t

我们可以找一个keymap来看一下它的结构:

M-x describe-variable org-mode-map

(keymap
 (25 . org-yank)
 (11 . org-kill-line)
 (5 . org-end-of-line)
 (1 . org-beginning-of-line)
 ;; ...
)

如上面的代码片段所示,首元素以后的其它元素(cons),由按键和绑定的函数组成。如果我们对25求值,可以看到它对应的按键就是?\C-y。

当然,(key . func-map)只是一种最简形式。比如,我们还需要支持一些按键序列,例如C-x C-f,这个结构就需要扩展一下了。后面再说。

emacs为keymap操作提供了丰富的函数,可以使用下面的例子创建一个keymap,并添加C-;映射:

(setq map (make-sparse-keymap))      ;; (keymap)
(define-key map "\C-x" 'c-x-func)
map  ;; (keymap (24 . c-x-func))  24 => ?C-x

行为覆盖

在上面的例子中,我们已经创建了一个keymap并绑定了C-x的行为。但是它现在只是一个值,如何将其应用到我们的emacs中呢?

不急,要回答这个问题,我们要首先了解,当一个按键按下后,emacs是如何找到它绑定的函数的。

我们知道,在vim中绑定一个按键,可以使用:map命令,如果首参给个,它将会作用到当前buffer而不修改全局keymap。(插播一句,由于vimscript只是一个专一化的脚本语言,vim的map行为是在vim内部执行的。我们还不能在vimscript层面可以直观地获取它对应的map)

emacs也存在这种类似的参数。当然,keymap的查找被定义为更严格的一个逻辑顺序。

说了这么多,emacs查找按键的顺序有三个:

  1. 查找一个神秘但对我们并不重要的map。
  2. 查找’minor-mode-map-alist。
  3. 查找(current-local-map)。一般会由major-mode设定。
  4. 查找(current-global-map)。一般会等于global-map变量,是emacs的默认全局配置。

好吧,如果你真的对1感兴趣,其实它是一个粒度更细的map,它会绑定到一个buffer里指定的某些文本。如果这个解释不能满足你,可以看一下text-property相关的手册。

2、3不言而喻了,需要说明的一点是,如果我们知道某个mode的名字,可以很方便地找到它对应的map变量,如org-mode对应的keymap为org-mode-map。

现在我们知道了keymap可以定义的多个层级,text-property -> minor-mode -> major-mode -> global-map。

上面我们讲到一个(define-key)函数,可用来修改keymap变量里的某个绑定。现在你应该已经知道网上某个家伙emacs配置里的代码片段是什么含义了:

(define-key 'global-map "C-x C-e" 'func)

两个特化的问题

主要的内容已经讲完了,下面再赠送两个额外的内容。

prefix keys

我想试着绕开这个内容,但是发现做不到。如果真正碰到了,会感觉很迷惑,不如两笔带一下。

我们上面提到,keymap的元素不一定是(key . map-func)的形式,比如C-x C-f,它实际上是两个序列的组合。在emacs中使用这样的形式来定义:(key . keymap2),即它的map-func是另一个keymap。

举个例子,假设我们想在全局的keymap下,定义一个前缀输入为C-;的序列,让它在我们输入C-; 2时,在2的后面添加一个;(只是试验,毫无价值)。

(defun append-semicolon ()
  (interactive)
  (self-insert-command 1)
  (insert ";"))

(setq ctl-semicolon-map (make-sparse-keymap))
(define-key ctl-semicolon-map "2" 'append-semicolon)

(define-key global-map (kbd "C-;") ctl-semicolon-map)

这时,如果我们输入一个C-; 2时,编辑器里就会输入一个2;。

需要注意,此时,如果我们修改global-map的C-; 3,修改的实际上是ctl-semicolon-map。

(define-key global-map (kbd "C-; 3") 'append-semicolon)
ctl-semicolon-map  ;; => (keymap (51 . append-semicolon) (50 . append-semicolon))

emacs里的C-x等序列就是这样定义的。

mode继承

emacs里的mode是有继承关系的,例如所有的编程语言mode都 应该 继承自 prog-mode ,所以针对编程语言的通用keymap,可以直接修改 prog-mode-map

具体的细节,可以查看mode设计的手册。

结束

以上的内容就可以把整个keymap串起来了,如果有什么地方不了解,所有需要的知识都在 C-h i 里。尽情查阅吧。

高可配置的emacs

Table of Contents

  1. customizable emacs
    1. 源码设计
    2. 软件设计
    3. 扩展语言
    4. Emacs并非无所不能

customizable emacs

Emacs是世界上可配置性最强的编辑器。

我最开始试图在上面这句话里,加上“几乎”或者“之一”来让这句话变得更准确。后来发现不管加上哪个词,这句话都会变成一句错误。

是的,世界上没有任何编辑器,在可配置性上能超越它。

争论这句话的对错已经没有意义,我现在想知道,为什么它可以在可配置上能够达到极致。或者,为什么其他编辑器达不到这种可配置程度?

这篇文章将尝试从一个矿工的视角,把最宝贵的这部分内容挖掘出来。

源码设计

linux kernel内核用到了很小一部分的汇编源码,据说新版本已经完全用c来实现了。

如果不是迫不得已,我相信linux kernel还是倾向避免接触到汇编的。

Emacs也遇到了这样的问题,由于效率上的考虑,它不得不将一部分底层代码使用c语言来构建。从本质上来说,Emacs是一个用c语言实现的lisp解释器。

剩下80%的部分,Emacs是使用在c上构建的lisp语言来编写的。

elisp更接近脚本语言,它有点类似python(sorry,其实是python有点类似lisp),也可以编译为字节码。它的一切都是可读的,任务一个lisp语言的函数,都可以快速找到它对应的lisp源码。

这就为用户提供了接近真相的机会,如果你愿意,甚至可以根据自己的需要直接修改核心的Emacs(好吧,你最好不要这么做,这意味着你已经脱离了Emacs和其他Emacer的支持)。

软件设计

软件包括两部分,可执行的二进制或脚本文件和它所包含的文档。

Emacs的核心软件都是结构化的,这使得我们可以很容易地找到相关功能所在的源码。比如,如果你想研究Emacs中的window操作实现,可以找到window操作对应的lisp文件window.el。

文档部分也是经过了精心设计的,从C-h对应的完整的文档分类可见一斑。这使得Emacs的用户很容易地可以查找到一整块完整的功能,读过Emacs手册的人应该都深有体会。

这里需要说明一点的是,Emacs的文档系统其实还包含了动态文档系统,比如查看当前mode或key bindings。所有的文档组成了一张严密的网络,使得我们在Emacs中可以快速地到达想要去的各个地方。

扩展语言

众所周知,Emacs使用了自己用c语言实现的一种lisp方言:elisp。

我认为,elisp被大家过分夸大了,甚至给人一种“Lisp不同寻常的语法决定了其开发者都是资深开发者”的印象。

甚至,我认为,恰恰是elisp的一些缺点,才导致Emacs的扩展性达到了无与伦比的高度。

lisp的括号可能是最被人诟病的设计,有一个lisp笑话被lisp黑客广为流传:“据说一个黑客偷到了美国用于导弹控制的LISP代码的最后一页,却发现这最后一页全是右括号”。

你可能写过python或者c语言这种面向对象或者面向过程的语言,但是遗憾的是,在lisp的世界里,这两种编程范式都很难被广泛实施,有很大原因源自lisp的括号设计。括号的大量出现,决定了你很难一口气写出100行的代码(希望你有勇气接受这个挑战)。所以它强制你精简每一个函数的实现,大量的微小函数组成了一张紧密的网,来提供强大灵活的整体功能。

这其实就是自底向上的编程思想了,没错,lisp强制我们使用自底向上的思想来思考问题。

说到自底向上编码,它的好处在哪里呢?它里面其实包含了分层的思想,即下层的功能可以很容易地被上层代码复用,而且当粒度足够小时,几乎所有想要的功能都一触即达。我们想要的只是一个编辑普通文本的编辑器,最后一不小心就变成了一个操作系统。

当我在借用第三方package实现某个功能时,一下子就找到了我所需要使用了所有函数,我需要做的只是使用自己的风格封闭它。

据说,当你打算设计一个极度复杂的系统时,lisp会让开发成本和维护成本随复杂度呈指数下降。Emacs就是一个例子。

“凡有的,还要给他,让他丰足有余。”

Emacs并非无所不能

最后的这个标题出人意外,前面夸赞了那么多,最后要承认它只不过是个使用习惯令人蹩脚或者过时的编辑器吗?

不是的!

我想知道网络上,Emacer们都在干些什么(问题来自知乎):

  1. 你认为最好看的 Emacs 配色方案(color scheme)是哪款?
  2. emacs还能做什么?emacs比vim好在哪里?
  3. Vim 和 Emacs 分别适合哪些人群?
  4. 谁知道emacs lispy-mode的用法?
  5. 对于使用emacs包管理器ELPA,你有哪些推荐的包?

我之前是一个vim用户,想学习Emacs多次未果,主要是觉得Emacs的按键太反人类。但是这是一种先入为主的思想,现在看来,应该让Emacs来适应我们,而不是本末倒置,要知道,Emacs就强大在它的可配置性上。

不要在自己还没有尝试了解一个东西之前,试图通过直接询问的方式一劳永逸。可以临时解决问题,但是不要在自己还不清楚的事情上浪费太多时间,最后原理没了解,只是在东拼西凑试图猜中答案。

不要做一个完美主义者。我之前也喜欢折腾,从字段,到配色,最后发现,这其实跟不动脑思考只知道蛮干是一样的。要精进不要盲进,少即是多,容忍不知道。

另外,不要试图寻找自己不需要的答案,也不要试图通过询问一次性获得答案。我们不需要知道别人使用哪些package,自己用的顺手才最重要。第一手资料在官方手册或者包列表服务器,我们要找的答案,绝不会简单地摆放在google里。

使用emacs管理生活

Table of Contents

  1. 使用emacs管理生活
    1. 关于这篇文章
    2. 一天应该从一杯牛奶开始吗?
    3. 一天从emacs开始
    4. 来个番茄?
    5. 不能煮咖啡的编辑器不是一个好操作系统
    6. 附件

使用emacs管理生活

关于这篇文章

很久没有写文章了,我是在一周前决定在今天写这篇文章的,用来分享我在emacs上的实践。在这周里,我有时闲下来会想到这篇文章的一些碎片,零零散散,错落有致。

我是在一两周前转到emacs的,在之前我一直使用vim作为我的编辑器。当然,只是编辑器,因为我的大部分时间都是用来编辑文本(代码、配置)。

我转到emacs,最开始是借助了spacemacs,它让我看到了emacs与vim共存的可能。但是因为spacemacs过于范化,不能满足我自己的使用习惯,我决定开始打磨自己的emacs配置。

emacs最令人惊叹的是org-mode,或者更准确地说,围绕org-mode建立起的生态。如果你没有尝试过,建议你在合适的时间研究一下这个模式,相信我,它会彻底改变你的生活方式。

由于这篇文章主要是分享自己的一些感受,所以不会涉及太多代码(你知道的,在emacs的世界里,存在大量elisp代码片段)。

一天应该从一杯牛奶开始吗?

每个人都有自己的节奏,我以前的节奏就是,混乱地开始和混乱的结束一天的生活和工作。如果我们有能力使这种模式免于混乱,便可以得到一个更为高级的名字,叫做自底向上的生活方式,过好当下,然后等着上天赐予我们最好的礼物。拖延症患者的生活方式并不是自底向上,而仍处于混乱状态(譬如我),所以还算不上自底向上。

对应的方式就是自顶向下了,我们可能会制定一个N年的计划,比如一个长达三年的学习计划,然后逐步分解去执行。不过实际情况往往会跟目标有出入,可以选择分片段逐步细化的方式。

在经历过长达数年的混沌时期后,我决定让自己逐渐适应并且切换到了第二种方式。我需要一个理论支持,最好是已经成熟并且有很多人实践过的理论。于是我找到了GTD。

关于GTD(Get Things Done),有一些专门的书籍可以学习。不过在我看来,一个好的实践,一定是不需要太多繁杂步骤的,简单而且行之有效。所以我并没有从头到尾阅读书籍,而是了解了GTD的思想,以及它的大体步骤。

GTD最核心的思想,可以用一句话来形容:将头脑中的想法记录到载体上。这样做的好处是什么呢?

首先,大脑没有了负担。设想之前的工作或生活,突然,脑海中想起了一件事,去做或者一会去做,有可能一会又忘记了,大脑承担了很多思考外的低级功能。

其次,待办更清晰了。只要我们能拿到所有的待办,就可以对待办进行评级、排期。只要一件事情做好了排期,我们只要等到对的时间去做就可以了。

一句话,解放大脑,提高专注力。

官方的GTD步骤有五个,我在这里整理下我的实践步骤:

  1. 收集:将大脑中的想法、邮件收件箱或其它的工具的信息收集起来。对于记录脑中的想法而言,手机是很方便的一个工具,我喜欢使用mac自带的Notes来记录脑海中一闪而过的想法,还可以直接同步到电脑端打开,再次处理都很方便。

  2. 整理:收集到的信息,需要进行再次处理,明确意义后归类。比如一个待办的具体定义,要归放到哪个项目下。

  3. 排期:对待办设置优先级,并查看自己的排期,将待办加入到自己的排期中。我自己一般不会对某个待办设置优先级,而是直接对待办所属的项目制定优先级。比如,我在跟进一个非常重要的项目,那它下面的待办就理应具有更高的优先组。

  4. 执行:这个步骤就相对简单了,因为信息已经做了梳理和排期,到了合适的时间,按照计划执行就可以了。

当然,第4个步骤简单,但也困难。困难之处在于,有很多情况是我们预料不到的情况。比如,可能会有其它同事来打断,或者有更高优先级的突发事件,这就需要我们根据其它方法论来处理了。碍于篇幅,这种情况我们暂不展开。

讲了这么多,还没有回答上面的问题。一天应该从一杯牛奶开始吗?对于我来说,答案是否定的。我的一天是从GTD开始的。

一天从emacs开始

市面上有很多GTD工具,可惜我都没使用过。我的工作几乎全部使用电脑完成,所以需要一个更加无缝操作的GTD工具,它最好不用分散我太多的注意力。

emacs里有一个org-mode,可以用来管理TODO列表,例如修改TODO列表状态,对TODO排期,或者记录TODO用时。

org-agenda是一个org-mode的相关工具,可以自定义项目的TODO列表的展示逻辑,比如显示今天有哪些代办,或者显示有哪些项目处于stuck状态(某个项目下没有TODO列表)。

用一句很装逼的话来说,“我已经一点都记不起明天需要做什么了”。这应该是我的一个追求吧,现在还没有完全达到。

到公司后,我的第一件事就是打开emacs。我对它进行了一些配置,打开软件后它会自动为我显示这一天的待办记录,这一天将从查阅待办开始。

在开始某一个待办前,我会使用org-clock-in对待办进行计时,以此来跟踪我在一个待办上花费的时间,并且在完成一个待办后休息一小段时间。

世界从未如此高效而轻松。

来个番茄?

我还找到一个提高专注能力的工作,番茄钟。

发明者为了提高自己的效率,发明了一种时间分片的方式,每25分钟为一个番茄钟,每个番茄钟结束后休息5分钟,每4个番茄钟休息15分钟。

这是一种很好的时间管理方式,它能让我们感知到时间的流逝,更好的对某件事所花费的时间做出评估。

我曾经实践过这种方法,但是发现它过于死板不够灵活,于是根据自己的需要进行了部分改造。

仍然是25分钟一个番茄钟,但是我可以在必要的时候提前或推迟结束时间。比如,某个待办进行的很顺利,20分钟就完成了,那我会选择提前结束这个番茄钟;如果某个待办需要30分钟,我可能会选择延迟5分钟。当然,有一种情况是这个待办需要1个小时,我会在25分钟左右,在合适的时机结束番茄钟。

一句话,节奏感很重要。节奏感是一种感觉,即是感觉,就会因人而异,需要找到适合自己的方式或节奏。

emacs是有包来专门提供番茄钟功能的,我没有使用。在我看来,没有太大的意义。

不能煮咖啡的编辑器不是一个好操作系统

你可能已经听说了,emacs是一个操作系统,而非编辑器。

这并非对emacs的嘲讽,实际上我认为这是对emacs的高度评价。

我想先谈一下gnu/linux或者windows操作系统,如果细想一下,“操作系统”这个词并不是很准确,如果你用过gnu/linux,你可能会同意我这个观点:gnu/linux更像是一个资源管理系统,它的任务更多的是接管底层硬件资源并提供调度方式以最大化使用效率。gnu开发了一个套件,用来实现一些与用户交互的功能,但是这是不够的,我们很多时候还是会安装许多额外工具。

所以称emacs是一个操作系统其实蛮贴切的,错的不是emacs,而是操作系统的定义。

你可能也听说过一种现象,emacer们倾向于把自己想要的所有功能都记录到emacs里,他们在emacs里浏览网页,阅读rss,收发邮件,可能甚至有人想用它来控制自己的洗衣机。

我想试着来解释下这种现象,前面我们说过emacs是一个操作系统,甚至它比gnu/linux做得还好,因为它高度定制,且有一致的操作风格。你可以把它看做是一个根据使用者的使用习惯高度特化的操作环境。

举个例子,比如我现在在编辑当前这篇文章,但是感觉这个自然段的表达方式有点问题,但是现在我的关注点是先把这篇文章写完,此时我可以使用remember命令调出一个临时记录note的buffer,记录一下现在的想法,这时想法就被记录到一个叫做remember-notes里了。我可以继续编辑这篇文章,使我的思绪被打算的可能性降到最低。

所以答案就很简单了,因为这个“操作系统”里已经集成了很多可用的工具,如果把另一个工具集成进来,效率和便利性会成指数级增长。

但是这是一个致命的诱惑,你感觉这是一个充满花蜜的骨朵,很有可能它会把你粘住拖进它的花苞消化分解掉,变成它的食物。

如果你像我一样,没有太多的精力去做这些看上去很有乐趣的事情,那就一定要克制住自己了,一定要守住自己的边界,知道什么不可以做。

附件

img

当前正在做的事情被高亮显示(图中为黄色),且会在modeline显示。

如果某个昨天的待办没有完成,Scheduled会变成Sched. 1x,并被加到今天。比如,写作emacs使用实践和腹肌撕裂者两个待办昨天都没有完成(现在已经是第二天的00:22)了,所以被加到了今天。

有一种任务是每隔几天就做一次,比如刮胡子这个待办需要每三天做一次,使用 2019-03-03 Sun +3d 时间,系统就会每三天生成一个待办。

img

我喜欢给一些需要随意做的任务记录时间,这样就可以知道我做某件事的整个时间周期了。成就感悠然而生。

spacemacs从入门到放弃

我的vim史

我是一个vim编辑器的重度用户,使用vim已经十年有余。从日常记录文本、开发到远程服务器的运维,它一直是我必备的工具。我有自己的一套配置,裸用vim也不在话下。

我在很久之前接触lisp,并为之倾倒。从来没有哪一门语言的外在表现可以如此完美统一,我相信一门编程语言的设计是完全可以塑造一个人的思维。谈及lisp,几乎毫无疑问谈到emacs编辑器。

这里顺带谈一个我对emacs设计理念的理解,emacs在我看来是lisp精神的一个很完美的体现(并不是说emacs完美)。emacs使用了20%的c语言代码,实现了一个lisp解释器,然后使用其余80%的代码,在lisp上构造了整个编辑器。所有的操作都对应着lisp函数,使得所有操作都是清晰和可查的。比如我想查找<C-x> <C-f>的实现函数和具体实现,只需要<C-h> k <C-x> <C-f>就可以了。

在可查的历史里,我曾经尝试N次从vim到emacs的转变,但是最终都以失败告终。

后来,一个偶然的机会,遇到spacemacs,让我再一次经历了转变失败的过程。

spacemacs是啥

不要问我是啥偶然的机会,因为我自己也已经记不得了。

spacemacs的官网是http://spacemacs.org/,通过作者的精心包装,从命名到完善的文档,使得它看上去更像一个完整的emacs发行版。但是其它,它的本质是另一份emacs配置。

是的,它是另一份emacs配置,就像https://github.com/上其它人托管的.emacs一样。

官方对它的介绍是,”A community-driven Emacs distribution”,一个社区驱动的emacs发行版。

spacemacs为什么这么吸引我

如果你也是一个vim重度用户,并尝试从vim转到emacs阵营,你就会像我一样首先被它的操作模式吸引。spacemacs默认集成了evil,使得在操作习惯上像是一个lisp配置的vim。

当然,对我来说它还有另一个致命的吸引力,那就是华丽丽的外表。默认是一个紫色的本色方案(基佬紫),非常漂亮美观。可以看下面官方的图片:

ss2

下面说一下官方宣传的优点:

我们知道使用emacs的用户,有一些患上了RSI,spacemacs通过集成evil,并将<leader>绑定到space来解决这个问题。由于space键主要使用拇指按压,所以大大减轻了手指的负担。

spacemacs通过使用助词符前缀,将各个功能对应绑定到各个按键。比如buffer操作绑定到b,help绑定到h。也就是说,buffer对应的操作按键是<space> b ...,并且每一个按键都会有功能提示,交互性也得到了一定保证。可以参考图片。整体上很连贯一致。

spacemacs引入了一个layer的概念,使用某一个功能实际上是添加某个layer,这样就把具体的功能跟第三方包解耦,我们只需要知道自己想要的功能,而不需要知道这个功能需要安装哪些包。这可以说是脱离折腾苦海的一个绝妙的办法,把折腾的成本交给社区。

spacemacs默认集成了100多个包,从编辑到cvs管理,功能涉及到方方页面,几乎将emacs包装成了一个简单易用且全面的系统。而且会根据打开的文件类型动态提醒添加的包,完成一站式体验(类似vscode等的包安装提醒)。

当然,还有一些其它的优势,让我在emacs的体验上走得更远。整整试用了两天时间。

两天的体验之旅

在使用spacemacs的这两天里,我发现了很多更优秀的工作流。

举个例子,项目内的批量替换,spacemacs使用插件使得整个过程更加流畅,首先项目搜索某个关键字,然后在交互环境里整体替换就可以了。

当然,有一个难以逃避的弊端,应该归给emacs的设计。那就是buffer的管理相当混乱,甚至使用git都要打开n个buffer,导致有很大的成本耗在查找和切换buffer上。

关于emacs的缺点,可以参考https://www.v2ex.com/t/332566,个人感觉总结的很到位。

放弃spacemacs

最终我决定放弃spacemacs,我发现它并不是我要找的圣杯。

首先,它过于臃肿。我指的臃肿并不是指其他人说的反应慢,而是它有太多不重要的功能,占用了过多的键位,导致看似设计优良的<space>操作方式,一个简单的功能都藏到很深的地方,比如describe-function被绑定到了SPC h d f,甚至很多时候都记不得某个功能对应的按键序列,需要根据交互提醒找到对应按键。当然,这些按键都是可以重新定义的,但是由于设计它耗费了社区很大精力,修改这样的默认按键就显得更加吃力。不知道修改后它还能不能叫做spacemacs了。

其次,之前谈到的,buffer过多难以维护,严重影响思维逻辑。

接下来

你现在看到的这篇文章,是我用emacs写成的。没错,我成功的转到了emacs,使用着我之前使用的vim键位绑定,甚至有一些比以前更好的解决方案。这要归功于spacemacs,它让我看到了自己之前存在的问题。

spacemacs还是会有大批人在使用的,这印证了一个事实,一个高度可配置的编辑器,仍然可以存在一份可以供一群人使用的通用配置。但这并不能发挥可高度定制这个杀手锏,甚至会阻碍你做过多的配置。

正如我使用vim的经历一样,现在这份emacs配置是我自己从零开始配置的,这是完全属于我自己的配置,并且不能被其它人使用。

而且,转换的成功有赖于我另一个努力,那就是努力不使用过多的功能。emacs的功能多到眼花,可能有一些功能这辈子都用不到,而且还会创建过多buffer,导致陷入之前的场景,没法继续使用下去。只使用自己需要的部分功能,就可以熟悉它对buffer的操作,让整个使用过程不会膨胀到自己的控制之外。

就写这么多吧,思维逻辑比较混乱,本来这件事也有点混乱,刚好了。

更换ssh证书导致paramiko报No session existing错误

问题出现

现在公司的发布系统使用了paramiko来执行远程操作,ssh连接用的证书被记录在配置文件里,是一个列表的形式。没错,我们的证书有很多,用来连接到不同的环境。

接到运维通知,由于安全原因,访问某台机器使用的证书做了更换。

随后没多久,就收到测试同学的反馈,发布代码时系统提示“No existing session”。

简单问题简单解决

这个问题乍看上去只要更新一下配置就可以了,简单的一批。

修改配置文件,重启系统,又重试了一次。结果,还是报这个错误。疯狂了疯狂了,配置也生效了,竟然还是不能连接吗?

问题初探

连接的代码很简单,就是单纯的连接远程机器。

1
2
3
4
5
6
7
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(host, SSH_PORT, SSH_USER, SSH_PASSWORD, key_filename=SSH_KEYS)

# ...

client.close()

通过测试,发现用命令行连接远程机器是可以的,而且也没有使用ProxyCommand之类,但是通过paramiko就出问题。毫无疑问,问题出在paramiko或者配置上。

session关键字让这个问题看起来像是paramiko的锅,它没处理好自己的会话吗?

一番google,发现了这个链接:https://stackoverflow.com/questions/6832248/paramiko-no-existing-session-exception。这个哥们也是遇到了一样的错误提示,但是他用了密码而系统查找私钥导致出错,关掉密钥查找就解决了这个问题。

看来问题出在密钥的配置上。

开启paramiko日志

发布系统使用的是paramiko 2.1.2,在paramiko的源码里找到了打开日志的方法。

paramiko/util.py:238

1
def log_to_file(filename, level=DEBUG):

通过该函数,可以将paramiko的日志打印到某个文件,比如paramiko.util.log('/tmp/paramiko-debug.log')。打印的日志如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.transport: starting thread (client mode): 0x8ab46350L
.transport: Local version/idstring: SSH-2.0-paramiko_2.1.2
.transport: Remote version/idstring: SSH-2.0-OpenSSH_5.3
.transport: Connected (version 2.0, client OpenSSH_5.3)
.transport: kex algos:[u'diffie-hellman-group-exchange-sha256', u'diffie-hellman-group-exchange-sha1', u'diffie-hellman-group14-sha1', u'diffie-hellman-group1-sha1'] server key:[
.transport: Kex agreed: diffie-hellman-group1-sha1
.transport: Cipher agreed: aes128-ctr
.transport: MAC agreed: hmac-sha2-256
.transport: Compression agreed: none
.transport: kex engine KexGroup1 specified hash_algo <built-in function openssl_sha1>
.transport: Switch to new keys ...
.transport: Adding ssh-rsa host key for [10.0.2.243]:36000: 2e3faf53075afccf09a3ac2q391dd5e8
.transport: Trying key 3a96e7e9e3f59963fbee693f44x8cf58 from /home/user/.ssh/id_rsa1
.transport: userauth is OK
.transport: Authentication (publickey) failed.
.transport: Trying key 8628c2021979e0e0302aac6bc8xcbcef from /home/user/.ssh/id_rsa2
.transport: userauth is OK
.transport: Authentication (publickey) failed.
.transport: Trying key 6e399bc162a737150e1bd643abx7737d from /home/user/.ssh/id_rsa3
.transport: userauth is OK
.transport: Authentication (publickey) failed.
.transport: Trying key 77d22314df154bfd5ca591bd16x19363 from /home/user/.ssh/id_rsa4
.transport: userauth is OK
.transport: Authentication (publickey) failed.
.transport: Trying key 77d22314df154bfd5ca591bd16x19363 from /home/user/.ssh/id_rsa5
.transport: userauth is OK
.transport: Authentication (publickey) failed.
.transport: Trying key c1909f00bf62c74eed5a3a9e5dxa73d1 from /home/user/.ssh/id_rsa6
.transport: userauth is OK
.transport: Disconnect (code 2): Too many authentication failures for user
.transport: Trying key c897a8e51a0d41facf7fa712a2xeace8 from /home/user/.ssh/id_rsa7
.transport: Trying discovered key 3a96e7e9e3f5996xfbee693f44b8cf58 in /home/user/.ssh/id_rsa1

我们几乎可以一眼看到最后的错误提示:“Disconnect (code 2): Too many authentication failures for user”。看起来像是失败次数过多被拒了。google后得到了这个链接:https://superuser.com/questions/187779/too-many-authentication-failures-for-username。“This is usually caused by inadvertently offering multiple ssh keys to the server.”,答主建议明确指定使用哪个证书来访问服务器。

我down了一份OpenSSH_7.8的源码下来,经过一番grep,找到了下面这几个代码片段。

ssh/auth.c:281

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void
auth_maxtries_exceeded(Authctxt *authctxt)
{
struct ssh *ssh = active_state; /* XXX */

error("maximum authentication attempts exceeded for "
"%s%.100s from %.200s port %d ssh2",
authctxt->valid ? "" : "invalid user ",
authctxt->user,
ssh_remote_ipaddr(ssh),
ssh_remote_port(ssh));
packet_disconnect("Too many authentication failures");
/* NOTREACHED */
}

ssh/auth2.c:322

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void
userauth_finish(struct ssh *ssh, int authenticated, const char *method,
const char *submethod)
{
// ...
if (authenticated == 1) {
/* turn off userauth */
// ...
} else {
/* Allow initial try of "none" auth without failure penalty */
if (!partial && !authctxt->server_caused_failure &&
(authctxt->attempt > 1 || strcmp(method, "none") != 0))
authctxt->failures++;
if (authctxt->failures >= options.max_authtries)
auth_maxtries_exceeded(authctxt);
// ...
}
}

ssh/srvconf.h:39

1
#define DEFAULT_AUTH_FAIL_MAX   6       /* Default for MaxAuthTries */

原来ssh默认允许尝试6个密钥,如果还没有成功,就会直接退出了。这么做的原因想来也显而易见了。

解决办法

我的临时解决办法是,将密钥文件的配置控制在6个以内。当然,更好点的办法是使用fabric来替代直接使用paramiko,并复用系统的.ssh/config配置,这就是后面需要考虑的了。

使用subprocess模块异步并发执行远程命令

远程执行命令

运维自动化平台不可避免地会涉及到远程命令执行操作,主要分为两类主要做法:目标机器安装agent,或者使用ssh。

saltstack是一个典型的agent模式的远程控制工具,麻烦的地方是首先要在目标机器上安装saltstack的agent。

使用ssh的模块居多,fabric和ansible是此类工具中的典型,这类工具的优点是方便,不用在目标机安装agent。值得一提的是,这两个工具都是基于paramiko。

使用ssh执行的另一种做法是,直接调用本地的ssh来完成。缺点是针对每一台远程主机都需要开一个进程,比起在程序内建立连接要耗费更多的资源。优点也很明显,比如,公司最近在做ssh的改造,在改造的过程中,可能会出现明明ssh命令可以连接,使用第三方模块就是不灵的情况。ssh的命令和第三方模块在配置上毕竟有差异,需要维护两份配置。

这篇文章就是要使用进程来完成在批量远程机器上执行某个命令。

subprocess模块大概

subprocess模块是python2中的一个官方模块,顾名思义,它主要用于操作子进程。

subprocess.Popen()用于创建一个子进程,它的第一个参数是列表,即创建进程所需要的参数,类似c语言中的argv。

需要注意的一点是,Popen是异步执行的,也就是说创建Popen后会立刻返回,子进程继续执行。关于异步,还有很多可以聊的地方,后面另开一篇文章写一下。

既然是异步的,我们就需要某种机制来跟它进行通信。Popen提供了多个方式来与子进程通信。

Popen.wait()将主程序挂起等待子进程的结束,结束后会返回子进程的返回码。

Popen.poll()可以检测子进程是否还在执行,并返回子进程的返回码。

下面我们将用几个小例子来不断扩展,最终实现一个可在多台远程机器并行执行命令的功能。

一个简单的远程执行示例

我们来写一个简单的示例,说明如何使用subprocess模块远程执行一条命令。

1
2
3
4
5
6
7
8
9
10
11
import subprocess


cmd = 'echo hello'
cmd_arg = ['ssh', 'localhost', cmd]

process = subprocess.Popen(
cmd_arg, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
retcode = process.wait() # 0
out = process.stdout.read() # 'hello'
err = process.stderr.read() # ''

我们首先创建了一个Popen对象,然后等待这个子进程的执行结束,获取返回值和输出。

这断代码很简单,注意stdout=subprocess.PIPE,我们想捕捉到子进程的输出,而不是直接打印到标准标出,所以要求Popen把输出打印到管道供我们读取。

同时在多个机器上执行命令

这里我们利用了Popen的异步特性,来加快多服务器任务的执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import subprocess


cmd = 'sleep 3 && echo hello'
cmd_arg1 = ['ssh', 'localhost', cmd]
cmd_arg2 = ['ssh', '127.0.0.1', cmd]

process1 = subprocess.Popen(
cmd_arg1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
process2 = subprocess.Popen(
cmd_arg2, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

retcode1 = process1.wait()
retcode2 = process1.wait()

这次,我们连接了两个机器并执行耗时3s的操作,由于Popen是异步的,这个脚本的实际执行时间仍然是3s。这个地方的操作是不是与多线程的操作有点类似,哈哈。

多个机器执行获取执行时间

由于网络环境和机器配置的不同,我们在不同机器的执行时间是有差别的。有时候,我们需要一个时间数据来了解哪个机器执行耗时比较久,以此做优化。

这时,我们需要Popen.poll()来异步检测命令是否执行完成,而不是将主进程挂起等待第一个子进程。如果第一个子进程执行时间比第二个子进程要长,我们就获取不到第二个子进程的执行时间。

为了更好的可读性,这里我们没有加入for循环之类的结束,放弃了部分逻辑上的灵活性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import time
import subprocess


cmd = 'sleep 3 && echo hello'
cmd_arg1 = ['ssh', 'localhost', cmd]
cmd_arg2 = ['ssh', '127.0.0.1', cmd]

process1 = subprocess.Popen(
cmd_arg1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
process2 = subprocess.Popen(
cmd_arg2, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

process1_ok = False
process2_ok = False

start_time = time.time()
while True:
if process1_ok and process2_ok:
break

if not process1_ok:
if process1.poll() is not None:
process1_ok = True
print 'process 1 finished, delta time:', time.time() - start_time

if not process2_ok:
if process2.poll() is not None:
process2_ok = True
print 'process 2 finished, delta time:', time.time() - start_time

time.sleep(1) # 防止空跑导致cpu占用过高

执行输出为:

1
2
process 1 finished, delta time: 4.01682209969
process 2 finished, delta time: 4.01691508293

最后的执行时间多了1s,这是因为我们做了一个time.sleep(1)操作。由于我们考查的是哪台机器影响了整体的耗时,而且远程任务执行时间远不止1s,所以这里的1s不影响我们的判断。当然,适当缩小time.sleep()的参数也是可以的。

封闭一个可并行在多台服务器执行的函数

我们将上面的脚本封闭一下,形成一个可以复用的函数。有点长,但是只是在之前基础上做了一些简单的操作,并没有增加什么高深的东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def run(hostname, command, user=None):
"""
使用ssh执行远程命令

hostname 字符串或数组,多个hostname使用数组
"""
if not isinstance(hostname, list):
hostname = [hostname]
else:
hostname = list(set(hostname))

# 创建执行命令子进程
processes = {}
for h in hostname:
if user is None:
user_at_hostname = h
else:
user_at_hostname = '{0}@{1}'.format(user, h)

cmd_args = ['ssh', user_at_hostname, command]
processes[h] = subprocess.Popen(
cmd_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

# 等待子进程结束,获取结果
start_time = time.time()
result = {}
while True:
running_hostnames = set(processes.keys()) - set(result.keys())
if len(running_hostnames) == 0:
break

for h in running_hostnames:
process = processes[h]
retcode = process.poll()

if retcode is None:
continue

status = STATUS_SUCC if retcode == 0 else STATUS_FAIL
out, err = process.stdout.read(), process.stderr.read()
delta_time = time.time() - start_time

result[h] = {
'out': out,
'err': err,
'status': status,
'delta_time': delta_time
}

time.sleep(1)

r = {
'ts': time.time(),
'cmd': command,
'result': result,
}

return r

celery使用fabric出现大量ssh -W进程

公司的运维平台有很大一部分是python写的。

发布系统不可避免的要与远程机器做交互,我们选择了基于paramiko的fabric模块来完成这部分工作。

由于发布过程中存在大量耗时很久的任务,所以选用了celery来执行异步任务。有一部分远程操作是通过celery的任务调用fabric来完成的。

出现大量ssh -W

在某一天,突然接到运维同学的反馈,说生产环境存在大量ssh -W进程,希望可以查一下出现的原因。

通过定位,很快确认了是ssh连接远程时使用了ProxyCommand,导致出现了ssh -W进程,这些进程实际上是ssh连接到远端的代理。

但是奇怪的是,为什么任务执行完后仍然存在大量ssh -W不能正常退出。

后来通过阅读fabric的源代码,在代码里找到了答案。

fabric/state.py

1
2
3
4
5
#
# Host connection dict/cache
#

connections = HostConnectionCache()

fabric/network.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class HostConnectionCache(dict):
"""
...
"""
def connect(self, key):
"""
Force a new connection to ``key`` host string.
"""
from fabric.state import env

user, host, port = normalize(key)
key = normalize_to_string(key)
seek_gateway = True
# break the loop when the host is gateway itself
if env.gateway:
seek_gateway = normalize_to_string(env.gateway) != key
self[key] = connect(
user, host, port, cache=self, seek_gateway=seek_gateway)

def __getitem__(self, key):
"""
Autoconnect + return connection object
"""
key = normalize_to_string(key)
if key not in self:
self.connect(key)
return dict.__getitem__(self, key)


# ...


def disconnect_all():
"""
Disconnect from all currently connected servers.

Used at the end of ``fab``'s main loop, and also intended for use by
library users.
"""
from fabric.state import connections, output
# Explicitly disconnect from all servers
for key in connections.keys():
if output.status:
# Here we can't use the py3k print(x, end=" ")
# because 2.5 backwards compatibility
sys.stdout.write("Disconnecting from %s... " % denormalize(key))
connections[key].close()
del connections[key]
if output.status:
sys.stdout.write("done.\n")

原来,fabric默认会维护一个连接缓存,并且不会主动断开这些连接,直到脚本结束垃圾回收时自己断开。但是由于使用了celery,celery是常驻进程不会退出,导致fabric不能自己主动释放连接,所以连接一直在,ssh -W也一直在。

知道原因,就很好解决了。可以主动调用network.disconnect_all()来释放连接,或者干脆就保持着连接,以待下次使用。

需要注意的是,HostConnectionCache是一个全局变量,在celery这类常驻进程中使用还是存在一些坑的。

cpu飙升到100%

知道原因后,我做了一个kill操作,手动将ssh -W杀死了。这时候,再次向之前的目标机发送命令时,ssh应该会报错,因为代理被干掉了。

意料之中,系统再次执行命令时果然提示“broken pipe”。

但是,奇怪的是,运行celery的机器,cpu占用率达到了100%。这就有点奇怪了。

查找原因无果,在github上提交了一个issue https://github.com/paramiko/paramiko/issues/1277。ploxiln提供了另一个关联issue,表明这是paramiko库的一个bug,通过链接可以查找到patch文件。

到此,问题都已解决。

吹牛逼的艺术

写下这个题目,我是非常心虚的。因为我不会吹牛逼。

不过我想起了之前做过的一些软件系统,它们最开始看上去确实其貌不扬,但是经过不断地优化改进,最终的效果非常令人满意。

所以我决定写这么一篇其貌不扬的文章,或者说给自己看的一份指南,就像手机使用指南一样。不同的是,这份指南不提供产品,因为我们指导的其实就是我们自己。然后,我来慢慢地把它优化改进,让它变成一份更有参考价值的指南。

不得不说,吹牛逼是一件很有技术含量的事。

为什么我们需要吹牛逼

我觉得我们是需要吹牛逼的,就像我们需要手机。

对于个人,吹牛逼能将我们丰富的内心世界跃然嘴前。为了不让这么一个美丽的心灵埋没,我们必须要学会吹牛逼的能力。

对于他人,吹牛逼能把我们的神奇经历传授于众,让人们的生活更丰富。

如何吹牛逼

要想吹一个满分的牛逼,需要做到下面几点:

  1. 必须要有吹牛逼的资本。吹自己擅长的,不吹自己不擅长的(这时要吸)。
  2. 吹牛逼一定要吹实,不吹虚。如果吹虚不务实,再牛的逼也会被吹破,让自己陷入尴尬的境地。
  3. 吹牛逼一定要打草稿。不打草稿的牛逼,往往是吹到一半就卡壳泄了气。
  4. 平日一定要注意自己的言行。要时刻牢记,注意当下的言行,以为我们日后吹牛逼积攒资本。
  5. 勤学苦练。我觉得这个在最开始是最难的,因为最开始可吹的牛逼是很少的。这是个量变到质变的过程,不可强硬,顺其自然。
  6. 从小事练起。吹牛逼非一日之功,而且要注意从一些微不足道的地方练起,从走路吃饭说话写字练起,吹可吹与不可吹之牛逼。