思路

Baelish: An Introspection

baelish 是一个基于配置的爬虫系统,目标是让标注员也能够通过可视化界面的来抓取数据。最近一年一直都在写这个项目。在这个过程中可以是说踩了无数的坑,杀死了不少脑细胞终于搞了一个勉强能用的 demo 版本。

总体思想上出的问题,老是想把知道的工具都用上去,试试好不好玩儿,而不是从项目需要的角度来选择。这种思想其实是自己早就知道是错的,可是真的能够自己负责一个项目的选型和架构的时候还是忍不住手痒痒啊。不过好在自己老早就知道这样是错的,至少以后再做项目不会犯这种错误啦。

这篇文章主要是总结下在其中犯得各种错误,以备查阅。

项目组织

最开始把项目分成了若干个代码仓库。baelish 负责调度和下载,jaqen 负责代理管理,bolton 负责解析和存储,inf 是基础库的代码,futile 是和爬虫业务无关的 utility,app_common 是数据库的 orm 和 Django 的后台,conf 是配置文件、idl 是 protobuf 代码。对于一个小型项目来说,分这么多库显然太复杂了。最终干掉了大多数库,只保留了 baelish、 app_common、idl、conf 和 futile 库,现在准备再干掉其他的库,只留下 baelish 和 futile。并且在打包 docker 镜像的时候全部都打包成一个镜像,这样部署也方便些。

基础组件选型

容器编排平台选型

最开始想通过 ansible 直接部署到多台机器上,然后使用 consul 服务发现的机制。但是这个过程中发现在同一个机器上如果部署同一个服务的多个副本的话不是很方便。脑袋一热,开始寻找一个真正的编排平台。

去年的十一假期研究了几天 k8s,概念是在太多了,看得我实在是头昏脑涨,所以放弃了 k8s。这时候因为已经选用了 consul,就注意到了同一家公司出的 nomad。nomad 号称是一个轻量级的调度平台,只有一个 binary,而且还能够和 consul 无缝集成。nomad 简直是一场灾难。首先他的调度是有问题的,尤其是其中一个比较有特色的功能叫做 parameterized job,顾名思义就是可以以不同的参数启动一个任务。这个任务就总是启动失败,而且还有看不到日志的情况。由于 nomad 的社区较小,在 GitHub 上只有不到一万的 star,所以除了问题以后只能看到几个悬而未决的 issue,然后就是干瞪眼。

最终选择了使用阿里云托管版的 k8s,虽然贵了点,但是对于公司来说,这点钱确实不算什么了。这时候距离我学习 k8s 的概念也有了几个月了,经过几个月的沉淀,一些难点也逐渐想明白了。使用了 k8s 之后,确实没有什么大的问题了。

这里要特别说明一下 k8s 上的服务发现实现的优点。在传统的集群中,比如说我们使用 zk 或者 consul 作为服务发现的话,一种模式是服务方主动把自己的 IP 和端口注册到注册中心,在退出的时候解注册。这样的不好是侵入性比较强,在客户端中需要自己去解析服务地址。k8s 上的服务注册在 etcd 中,然后内部服务访问的时候通过 DNS 解析的方式获取到 IP。那么这里就有个问题了,一般语言或者系统的实现中,DNS 可能有也可能没有缓存,那么当服务在集群中漂移的时候怎么能保证总能访问到正确的地址呢?k8s 的实现比较神奇,他的 clusterIP 是虚拟的,并且在服务的整个生命周期都是不变的,也就是说,DNS 和 IP 一定是固定的,服务层有没有 DNS 缓存就无所谓了。

消息队列选型

最开始的时候觉得 kafka 实在太重了,虽然很熟悉 kafka 的时候,但是考虑到自己运维的压力,所以就想找个轻量级的工具。首先尝试使用了Redis,但是因为消息都堆在内存里面,一旦消费端发生了阻塞,很快就oom了。

后来尝试了使用更加“工业级”一点的 rabbitmq,毕竟还自带了管理界面。但是折腾了一周,rabbitmq 总是会神奇的自动退出,查了下可能是 Erlang VM 的问题,并且没有更多任何日志消息,最终放弃了。而且 rabbitmq 没有一个很好的 python 客户端,有一个叫做 pika 的 python 客户端,但是基本跟玩具一样,什么也没有,完全需要自己写。

在之后,正好 redis 发布了 5.0 版本,提供了 redis stream 的功能,号称是和 kafka 一样的设计理念,所以就尝试了一下。遇到了两个问题,首先当时 redis-py 还没有跟进,所以只好使用比较低端的 python 客户端来和 redis 通信,这样导致工作量大了很多;还有一个就是 ack 的语义不明,倒是消费总是重复,最终放弃了。

因为 ack 的问题总解决不好,又想使用一些比较全家桶的方案,这时候 celery 进入了我的视野。celery 作为一个异步框架,只需要编写 worker 函数就行了,至于 broker 可以使用 rabbitmq 或者是 redis。因为 rabbitmq 之前一直跑不起来,所以选择了 redis。用了大概一个月的时间还是比较满意的。celery 虽然可以支持 redis,但是他是使用了 kombu 这个库,把 redis 封装成了 AMQP 协议,也就是 rabbitmq 来使用的,这就导致了想要改一些东西的话还是很复杂的。同时 redis 毕竟还是在内存里的数据库,一开始提到的 OOM 的问题还是没有彻底解决,这时候就想着在换一下了。

终于又想起了 kafka,仔细把 kafka 的文档通读了一遍,然后又看了下官方的例子,发现运行一个简单的 kafka 集群其实并没有想象的那么难。kafka 背后的公司现在叫做 confluent,他们官方提供了 kafka-docker 的镜像,最终使用 docker-compose 把 kafka 和 zk 都做了一个单节点的部署,虽然听起来可用性不高,但是到目前为止确实没有发生过任何问题,当然以后流量大了肯定要搞集群的,不过这也不过就是需要把 compose 文件改几个参数罢了。至于 kafka 的客户端,则是使用 confluent-kakfa 加 threadpoolexecutor 自己封装了一个。

RPC 选型和微服务

在前东家的时候一直用 thrift,但是 thrift 不支持 uint64,这点让我一直不是很爽。而且听说 thrift 的序列化性能和 protobuf 相比差了不少。于是乎,在研究了一段时间 thrift 和 gRPC 的优缺点之后,毅然选择了 gRPC。

但是问题来了,gRPC 虽好,暂时用不上啊。虽然设想着代理、解析、下载等等可能都需要微服务,但是最终都没有用,因为运维几个微服务的代价太高了,人手不够的时候还是单体应用好,不能切分太细了。而且其实在最开始并没有多大的流量,不如先使用快糙猛的 http 服务搞起来。另外 gRPC 的 Python 版本到目前为止还不支持多进程模式,所以更要慎重使用。

除了 gRPC 以外,还使用 protobuf 定义了几个全局透传的对象,现在也马上要被移除了。开始想着是这几个对象可能最终要被持久化存储,那么使用 protobuf 做序列化再适合不过了。对于应用的内部通信,实际上用语言本身的对象就是最好的了,protobuf 完全没必要,画蛇添足。

存储系统的设计

对于 mysql 竟然了解地不是很充分。高性能 MySQL 这本书到现在为止也才只看了 50%。当时我竟然以为事务可以让一批数据批量入库,想想真是 naive 啊。

监控系统

不懂的地方很多,但是最终弄对了,收获也很大。大概花了一个月的时间首先学习了什么是时序数据,然后系统调研了 opentsdb、influxdb、prometheus 等等时序数据库或者监控方案的优缺点,最终选择了 influxdb + grafana 的方法。这里有个坑就是对于带有各种 tag 的数据的聚合方式,各家都支持地不太好,哪怕是 influxdb 的亲儿子 telegraf 也会把数据理解错,这里只能是自己根据业务来实现了一个打点的库,自己在客户端做好聚合工作。

因为其中被 telegraf 坑了一把,所以监控这块还有一些短板,不过补上也很简单,只是工作量的问题。

业务逻辑

调度

由于在开始项目之前,刚刚看了MIT 的信息检索导论这本书,其中提到了爬虫的 frontier 组件,然后就模仿着写了一个调度的组件,可是根本就是想多了。书中提到的调度算法是面向的全网爬取,也就是说搜索引擎级别的爬取,实际上和我要解决的半定向爬取的问题不是一个问题。虽然浪费了大概一个月时间实现了这么一个东西,但是实际上并没有什么卵用,最后抛弃了。

调度中一个很重要的问题就是频控。我是知道一个叫做 token_bucket 的算法的,在这里就特别想把这个算法用上,但是事实有一次证明我错了。对于这种主动发起请求,自己能控制频率的情形,最好的方法还是 sleep 就好了。

可是毕竟sleep总让人感觉可能会很低效啊,这时候我又想起了操作系统中进程调度的各种优先级算法。如你所知,又掉进了坑里。这里的调度问题实际上和进程调度完全不是一个问题,非要用那个优先级算法实际上除了会造成好多任务没有在运行以外,并没有什么卵用。

最终采用的方式就是每个线程负责 N 个爬虫的调度,简单轮询,稳定又高效。

下载解析

这里可以说是整个项目从一开始设计基本正确的地方了。使用 pipeline 的模式,把每个步骤都抽象成一个 stage,其实和 django 的 middleware 有点像,最终完成一个网页的抓取。

这里唯一的坑就是开始想把规则加载、代理和解析都设计成一个 RPC 服务去调用,后来发现完全没有精力搞这些事情,就算了。

缓存

设计地太复杂了。考虑了缓存加载和缓存过期两种时间,搞得大家都比较迷惑。最终发现绝大多数的项目也都不需要缓存,这块直接去掉了。

代理

本来想自己使用阿里云或者 adsl 机器自己搭个集群,但是自己搭建的 IP 对于当前的场景来说不够用啊,而且自己搭建太复杂了,还是直接买得好。

张一鸣:你的尺子的长度

编者按:今天例行摸鱼过程中在 Web Archive 上偶然发现了一篇“99房”的博客。99房是张一鸣在创立字节跳动之前的创业项目。根据文中的“延迟满足感”等词,我推测这篇文章应该是他写的吧。在 Google 上搜索了文中的几个句子,发现这篇文章并没有见诸媒体。文章写得挺好,分享给大家。

请注意,本文未获得任何转载授权,主要是除了 CEO 面对面的时候在几十米外眺望过以外,我也和张一鸣没有任何交集……Anyway,如果有任何问题,我当立即删除此文就是了。

原文发表于 2011 年 4 月 11 日。不到一年后,张一鸣创立了字节跳动。

原文链接(需特殊上网能力方可访问):https://web.archive.org/web/20110924075809/http://blog.99fang.com/?p=151

先说一个问题:我已经有一年多没写博客了。原因有两个。

1、发现我所谓的一点感悟和总结,其实很多经典的书有系统的分析和著名的人也有经典的总结,自己写的对人帮助少。

2、相信少说多做,延迟满足感,人生废话居多,行动太少。

但是后来发现这两点不全对。按照哲学和科学的体系,大多数事情都可以从少数公理开始推导,还是需要具体的定理和行动来指导行动。对于写博客而言,只要能做到对有相似经历和相似背景的读者有所裨益,相对容易有共鸣和帮助,就有了价值不是“废话”。

回到正题:看待事情,衡量经历,很多就像拿把尺子去估量,当然这个尺子也许不是一维的,或许也不是直的,但是我发现有一个最关键的因素是这个尺子是否足够长。尺子长短能决定测量的结论:比如你拿把短的尺子去量铁轨是否是直的,答案肯定都是直的。随着年龄的增长,经常发现当年自己在某个问题上想错了,做法错了。这种情况很多时候是因为当年因为年纪小,经历短,所以只能拿把短的尺子去衡量。

我想举几个事情:

1、选择工作:大学毕业的你对于 5 千元薪水和 7 千元薪水,你用当时的尺子感觉差距一定不小。但是可能 3-5 年后,你会发现这个差距可以作为误差,毕业选择工作的时候可以忽略不计此因素。

2、寻找伴侣:年轻人少有对未来伴侣的人品,价值观、追求等关注,肯定少于对外形、活泼,甚至少于对星座、爱好的关注。但是 30 岁的时候你再找(如果你还没找到),相信会变化。

3、关于度尽劫波兄弟在,相逢一笑泯恩仇:我想应该是既然劫波已度过,尺子自然变长了,恩仇已然不重要,是是非非成鸡毛蒜皮。

这些问题有人说是需要阅历,我同意。但是我更多取决于于你心里对尺子设置的长度,你若心中想的是人生你的尺子自然就长当然也不会那发型是否酷来寻找伴侣。

:) 回到正题的正题:创业公司拿多长的尺子去衡量人?

1、是否以会 gdb 打印 map、是否写过分词程序来招工程师,是否以会用 visio 图来招聘产品经理?

2、是否以业内有客户关系来评判商务总监候选人,亦或是在媒体有所资源来选择PR经理?

我想这取决于你如何看待创业,如果你把目光可能到下个月的进展,这些尺子自然合适,但是如果你把创业看作是做一个创造巨大价值的公司和伟大组织的长跑,我相信这些都不是重要标准。这时候你会更加看重:人品、作风、学习力、反省力、价值观。如下三把尺子是递长的:Ta 一年后在公司承担的角色和能成长的高度;假设还有下次创业是否还希望邀请;会不会成为人生道路上的可信赖的好伙伴。

还有一些更极端的例子,在“仰望星空”的时,在想像浩瀚宇宙的边界时,在险峻山峰一览众山小的时,很多人感到看待问题会开阔深刻很多,这些也可以理解成尺子长度变化的效果。

所以很多事情用长的尺子和短尺子衡量的结果不一样。如果你希望的更远,请别忘适时的选择长尺子。