Freedom

淡泊以明志,宁静以致远。

A Little Throught About Microservices

知乎在 4 年前已经开始尝试服务化,至今也经历了好几个架构的变迁演化。我大约是 2013 年开始在知乎负责服务化的工作,对服务化的理解也从最初的模糊逐渐变得清晰,前段时间看了一篇叫做 Microservices – Not A Free Lunch! 的文章,也想趁着这个机会梳理总结目前为止我的一些感悟和想法。

香港帆船培训记录

曾经对香港的印象就是便宜的苹果电脑和遍地的茶餐厅,竟忘记了这是一个靠海的岛屿。作为一个在西部长大的孩子,对于海总是有很多憧憬。从小到大见过很多地方的海,有浑浊的,有碧蓝的,有挤满游客的,也有波涛汹涌的。其实海不一定就是蓝色的,只是人们习惯性地把自己的愿望加诸在别的东西身上,所以如果某一天你见到了不是蓝色的海,请不要抱怨它。

Auto Open Browser After Copy URL

前段时间看过一篇叫 Automate Everyday Tasks 的博客,其中的一些见解很有意思,我们日常工作中有很多细小但是重复的事情,如果能够将某些工作自动完成,会让生活更加舒适。我很喜欢 Mac 上一个叫 PopClip 的小 app,可以大大减少很多重复的操作。这篇博客就是介绍如何制作一个 app,当复制 URL 时自动在浏览器中打开。

流浪汉,木偶和厨子

最近要为 Phabricator 搭建虚拟测试环境,Vagrant 是一个不错的选择(话说官网现在更新以后,变得颇为华丽)。Vagrant 官方只提供 Ubuntu 的 base box,不过 Vagrantbox.es 有提供很多其它的系统,甚至还有 Window$。也可以自己根据官方文档重新搭建一个 base box。

Provisioning 是 Vagrant 一个很棒的特性,可以通过工具来自动配置和管理虚拟机。目前支持的有 Puppet 和 Chef,这两个都是著名的配置管理工具,其中 Google、Twitter、GitHub 在用 Puppet,Facebook 在用 Chef,知乎目前用的是 Puppet。正好这次两个都了解了一点,可以简单比较一下。

从安装方式来说,因为都是基于 Ruby 的工具,所以都可以通过 gem 来安装,从这一点上来说还是很方便的(话说对于 Mac 用户,千万别用官方提供的烂方法)。Puppet 的命令行工具就叫 puppet,而 Chef 的叫做 knife,这倒是跟 Chef 本身名字很搭。初学工具,肯定要看官方文档,在这一点上我觉得 Puppet 做得更好,至少还有一个像模像样的 Learning Puppet 系列,由浅入深,循序渐进,基本上看完就可以对 Puppet 有个大概的了解和使用。而 Chef 就只扔给你一个不知道该从哪看起的页面,作为初学者表示很难入门。

Puppet 可以将一系列的配置文件打包成一个 module 供人下载,Chef 对应的则叫做 cookbook,这两者都提供了网站用于集中放置社区贡献的包,分别是 Puppet ForgeOpscode Community(不得不吐槽,这两个网站都很糙)。对于 module、cookbook 的安装及管理 Chef 略胜一筹,Puppet 的命令行工具可以很方便地安装 module,但是如果需要安装的包比较多,就只能通过自己写脚本来自动处理。而 Chef 有一个很好用的工具 Librarian-Chef,只需要定义好所有依赖包,并放到 Cheffile 中,就可以通过 librarian-chef 命令来安装和管理。

Puppet 在易用性,社区质量和包的扩展性上来说要比 Chef 略优,能查到的文档资料也更多一点,最终我选择了 Puppet,这里是我的适用于 Phabricator 的配置文件,对 Chef 有兴趣的同学也可以看这个示例配置。

Little Tips: Redis MONITOR Command

前段时间知乎的 cache 服务器中的某个数据总是错乱,想到了几个可能修改缓存的源头,同时在代码中搜索相关代码,把这些服务都重启了。但是问题依旧,只是没有之前那么严重。好吧,这下肯定是某个不知名的地方仍然在访问缓存。那就从根源查起,猛然发现 Redis 的 MONITOR 命令,可以实时打印出此时正在执行的命令,正合我意,修改缓存的命令我是知道的,只需要监测这个命令,然后就可以查到来源了。

$ redis-cli monitor | grep '"set" "alist"'

给未来的你

孩子,你的一生会遇见很多不一样的人,在陌生的城市和环境里结交着朋友,找寻着恋人。你每天都会很忙,忙到没有时间喝水,没有时间吃饭,没有时间思考。你会羡慕那些生活得悠闲自在的人,仿佛他们生来如此。看到街上的情侣你也会想她是否也在想着你,因为你们仰望着同一片星空。你向往着有一天和她一起生活,你们想要的生活。

孩子,还记得我讲过的怎样遇见你母亲的故事吗?那是一个明媚的午后,记忆中的阳光总是很灿烂。当那个女生出现时,时间仿佛凝固,她没有注意到你,你知道这是一个需要你用一生去爱的女人。是的,一生。年轻人总是有无尽的诺言,但是诺言是沉重的,兑现诺言的过程是洗礼,也是炼狱,你们虽然彼此伤害,却靠得更近。

我对你的爷爷奶奶知之甚少,大部分是从旁人那里听说。他们小学是一个学校的,奶奶上学会经过爷爷的屋前。后来奶奶高中毕业后就开始教书,而爷爷则继续深造师范学校,传说他们从这时便已经在谈恋爱,分隔两地免不了很多的思念与痛苦,爷爷常常笑着说当年可是拒绝了很多女生的诱惑。爷爷毕业后回到了奶奶教书的学校,多年的长跑也终于有了结果。其实你还有一个姑姑,不过连我也没有见过。她是爷爷奶奶的第一个小孩,听人说长得很乖巧,但在十几岁时便由于生病去世了。爷爷奶奶教了一辈子学生,却不怎么跟我说起他们的故事,也许是不知如何表达。

我们都会老去,我们也曾年轻,你的困惑就是我们曾经的困惑,你的烦恼就是我们曾经的烦恼。如果你想倾诉,别忘了在远方还有你的母亲,还有我,不管发生什么,我们永远都是你最亲的人。我知道你曾经也恨过我们,但那不是真正的恨,我相信有那么一天我们能彼此释然。

到那时,你会了解,我们是如此深深地爱着你。

使用 Bootstrap 的几个问题

Responsive 与 Modal

在开启 responsive 后,小屏幕设备上显示 modal 时会变成一闪而过,然后浮动窗口就不见了。具体效果可以缩小浏览器尺寸,在这个页面的 Live demo 点击「Launch demo modal」看到。Issue #2130 专门讨论了这个问题,目前比较好的解决办法是使用这个插件,根据页面大小来动态调整 modal 的位置,不过貌似用了之后 modal 那个由上至下显示的动画就没有了。这个 issue 现在还处于开启状态,看来官方短期内是不会解决这个问题的。

Responsive 与 Navbar

responsive 模式下的 navbar 显示效果很赞,但是有一个很令人费解的事情,默认情况下所有 dropdown menu 都是展开的,对于使用多个菜单项,且子菜单条目很多的场景这是不能接受的。于是 Issue #3184 出现了,这次的方案比较 hack,需要修改 bootstrap-responsive.css,将 .nav-collapse .dropdown-menu 里的 display: block; 注释掉。这时你会惊喜地发现 dropdown menu 默认折叠了,点击也能展开子菜单。 最新版 Bootstrap 已经修复了 dropdown menu 默认展开的问题,但是(总是有很多但是),在触屏设备上子菜单是选不中的。托 filod 同学的福,修改 bootstrap-dropdown.js 中的一段代码:

1
2
3
4
5
6
7
8
/* APPLY TO STANDARD DROPDOWN ELEMENTS
 * =================================== */

$(document)
  .on('click.dropdown.data-api touchstart.dropdown.data-api', clearMenus)
  .on('click.dropdown touchstart.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
  .on('click.dropdown.data-api touchstart.dropdown.data-api'  , toggle, Dropdown.prototype.toggle)
  .on('keydown.dropdown.data-api touchstart.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown)

这里同时监听了 click 和 touchstart 事件,于是在触屏设备上先有 touchstart 将子菜单隐藏,再有 click 点击到隐藏后该位置的菜单项,因此你永远都不可能点到想点的子菜单。根本原因也是因为我们之前注释了 display: block; 引起,改变了 Bootstrap 的使用场景,于是 JS 出现如此纰漏。解决方法便是不监听 touchstart 事件,虽然会造成些小问题,不过也算基本满足要求。这个 issue 官方明确表示不会采纳,不过还是希望以后有机会增加一个开关选项给用户。 关于这个问题的讨论可以看 Issue #4550,不明白为什么官方一直不解决,我的修改可以见这个这个 commit。

理解 tornado.gen

Tornado 通过 @asynchronous decorator 来实现异步请求,但使用的时候必须将 request handler 和 callback 分离开,tornado.gen 模块可以帮助我们在一个函数里完成这两个工作。下面是官方的一个例子:

1
2
3
4
5
6
7
8
class GenAsyncHandler(RequestHandler):
    @asynchronous
    @gen.engine
    def get(self):
        http_client = AsyncHTTPClient()
        response = yield gen.Task(http_client.fetch, "http://example.com")
        do_something_with_response(response)
        self.render("template.html")

这里用到了两个 decorator 稍显复杂,第一个 @asynchronous 会首先被执行,它的主要工作就是将 RequestHandler_auto_finish 属性置为 false,如下:

web.pydownload
1
2
3
4
5
6
7
8
9
10
def asynchronous(method):
    @functools.wraps(method)
    def wrapper(self, *args, **kwargs):
        if self.application._wsgi:
            raise Exception("@asynchronous is not supported for WSGI apps")
        self._auto_finish = False
        with stack_context.ExceptionStackContext(
            self._stack_context_handle_exception):
            return method(self, *args, **kwargs)
    return wrapper

接着就是最重要的 @gen.engine,这里充分利用了 generator 的各种特性,首先来看 @gen.engine 的实现(我删减了部分代码以简化理解):

gen.pydownload
1
2
3
4
5
6
7
8
9
def engine(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        gen = func(*args, **kwargs)
        if isinstance(gen, types.GeneratorType):
            runner = Runner(gen)
            runner.run()
            return
    return wrapper

局部变量 gen 代表第一段代码里的 get 函数,因为 get 包含了 yield 语句,因此成为了一个 generator。注意这里 get 并没有被执行,只是赋给了 gen。接下来是运行 Runner 对象的 run 函数。在理解 run 之前需要知道 generator 是通过调用 next() 或者 send() 来启动,启动之后会在遇到 yield 的地方 hold 住,然后将 yield 后面的语句的返回值返回给调用者,generator 此时即处于暂停运行状态,所有上下文都会保存。再次调用 next()send() 便会恢复 generator 的运行,如果不再遇到 yield 语句就会抛出 StopIteration 异常。在恢复运行的同时 yield 语句本身会有返回值,如果是通过调用 next() 来恢复的,那么返回值永远是 None,而如果是通过 send() 则返回值取决于传给 send() 的参数。更多关于 generator 的说明请参考官方文档

结合第一段的示例代码,可以想到 run 干的工作可能就是启动 generator,然后获得 gen.Task 对象并调用 http_client.fetch 函数,等回调回来之后恢复 generator 的运行,最后将回调的返回值通过 send() 赋给 response。下面是我简化后的代码。

gen.pydownload
1
2
3
4
5
6
7
8
9
10
11
12
def run(self):
    while True:
        if not self.yield_point.is_ready():
            return
        next = self.yield_point.get_result()
        try:
            yielded = self.gen.send(next)
        except StopIteration:
            return
        if isinstance(yielded, YieldPoint):
            self.yield_point = yielded
            self.yield_point.start(self)

第 3 行检查回调是否完成,第一次运行 run 总是会返回 True。第 5 行获取回调的返回值,同样的第一次运行返回的是 None。将 None 传给 send() 启动 generator,yielded 即是 gen.Task 对象,第 12 行调用 start 开始运行我们真正需要运行的函数,对应到示例代码就是 http_client.fetch 函数,同时将 Runnerresult_callback 作为回调函数。如下:

gen.pydownload
1
2
3
4
5
6
7
8
9
10
11
def result_callback(self, key):
    def inner(*args, **kwargs):
        if kwargs or len(args) > 1:
            result = Arguments(args, kwargs)
        elif args:
            result = args[0]
        else:
            result = None
        self.results[key] = result
        self.run()
    return inner

在得到回调返回值之后再次调用 run,通过 get_result 获取返回值,最后将返回值返回赋给 response,继续 request handler 的代码流程。

Shall We Play a Game?

WarGames」是一部 1983 年上映的科幻电影,作为投资仅 1200 万美元的小制作,在当年赢得了近 8000 万的票房。故事发生在美苏冷战时期,那个年代的电影,多多少少都会跟核威慑有关。这两个国家不管谁先发射导弹,那第三次世界大战就会爆发。电影中 NORAD(北美防空司令部)使用了一台叫做 WOPR 的超级计算机进行战事控制,这台计算机特别的地方在于它能自己模拟战争,模拟的过程就像在玩一个游戏(game)。某一天,我们的男主角天才高中生无意中侵入了这台电脑,出于好奇和好玩,启动了核战争游戏。不料这场模拟战争误使 NORAD 以为苏联发动了袭击,一度差点引爆真正的核对抗。最后在男主角和 WOPR 创造者的共同努力下及时终止了这场「战争」。

电影中男主角的人物原型来自一位叫做 David Scott Lewis 的黑客,这位老兄之后一直在搞机器人,后来按照他自己的话来说是「sold my soul to the bigger corporations」,分别在三星、Microsoft 和 Oracle 工作一段时间之后,他来到了中国,跟清华大学合作。现在貌似又搞太阳能去了。WOPR 的创造者 Dr. Stephen Falken 的原型来自著名的物理学家 Steven Hawking(是不是名字也很像?),WarGames 最初的剧本就是根据 Hawking 来写的,曾经还打算塑造一个坐在轮椅上的天体物理学家,但因为太容易让人联想到另一部讲述冷战的电影「Dr. Strangelove」而作罢。其实 Dr. Stephen Falken 对于人工智能的深入研究,倒让我觉得更像是 Alan Turing。

WarGames 对于此后的黑客文化产生了深远的影响。它创造了「firewall」这个词汇(方校长表示感谢)。电影中男主角入侵 NORAD 时使用的技术衍生出了「wardialing」术语。wardialing 是指通过程序不断扫描电话号码来发现计算机 modem,早期的电话黑客即是使用的这种技术。著名的电话黑客(phone phreak)John Draper 因为通过 wardialing 免费打电话而闻名,后来他将这项技术教给了 Steve Jobs 和 Steve Wozniak(是的,没错,就是乔帮主),帮主他们还因此赚了不少钱(帮主果然是个好商人,从小就懂得怎么把技术转为商业利益)。John Draper 后来受雇于 Apple,但这是后话了。wardialing 之后又衍生出了 wardriving,通过扫描和收集 Wi-Fi 热点来进行攻击,之所以叫这个名字,是因为通常是在汽车里一边行驶,一边收集(这样说来 Google 的街景小车也算是 wardriving 了一把)。现在你可以在你的 iPhone 或者 Android 手机里装上一款 wardriving 软件试试看,我曾经试过,但是貌似效果不是很好。

还有一个人「深受」这部电影的影响,Kevin Mitnick,这个计算机安全界传说级的人物。当然不是说 Kevin 同学是因为这部电影走上的不归路,Kevin 同学搞入侵那会儿 WarGames 还没上映呢。Kevin 同学后来入狱时被拒绝接触任何电子设备,包括电话,是因为控诉律师相信他可以通过电话连接到 NORAD。据 Kevin 同学分析 WarGames 这部电影很大程度上使得公众相信这件事情是可以很容易办到的,其实他根本就没有入侵过 NORAD,通过电话来入侵也过于夸张。尽管如此,他还是被判单独监禁,从此传为计算机安全界的一段「佳话」⋯⋯

游戏界也从 WarGames 获益不少,1984 年同名游戏发布。2006 年一款叫做 DEFCON 的即时战略游戏发布,游戏画面与电影中 NORAD 指挥中心的大屏幕极为相似。DEFCON(defense readiness condition)是美国军方采用的警报等级,在 WarGames 电影中 NORAD 曾因模拟的苏联进攻一度将 DEFCON 等级提升到最高等级 1。著名的黑客大会 DEF CON 的名字也是来源于此。

或许 WarGames 对于电影史并没有太大的贡献,但却深深影响着那一代的 Geek 们。谨以此文献给那些逝去的先驱,RIP