湾区日报是如何运作的
閱讀本文約花費: 24 (分鐘)
Updated April 17, 2016
这篇文章第一版是2015年5月份写的,现在看来有点“过时”了。是的,几个月的时间是能改变很多事情的。所以我决定每隔一段时间把这篇文章更新一次。
湾区日报是什么?
湾区日报是一个在旧金山工作的工程师运营的个人博客。该博客每天挑选5篇他主观认为高质量的文章,针对每篇文章拟一个中文标题写一两句简单评论,通过将近10个渠道(比如网站,iOS app,微博,微信,Twitter等)推荐给读者们。
写这篇文章的“我”就是这个工程师。湾区日报不是一个公司,也不是一个创业项目。只是我的一个 side project。从 2014年8月6日发第一期以后,运营至今。
看上去很简单,对吧?我一开始做的时候也觉得很简单,很没技术含量,任何有高中文凭具有简单电脑知识的人都能运营这种博客。但为什么同一道命题作文,每个人写的内容不一样,有人能写离题有人能写满分作文?为什么大家都会写字,有人“竟然”能成为作家并靠写字为生?
每天的内容是如何发出的?
我在每天利用零散时间发现新文章,然后直觉认为是好文章的就收藏在Pocket 里。发现新文章的渠道很多,排名不分先后:Hacker News,Medium,Quora,我订阅的很多博客的RSS,各种社交网络上看到的文章,头脑中突然对某个主题感兴趣临时搜来的文章,与同事交谈中得知的文章等。
我也利用白天的各种零散时间读一两篇文章。但一般都是晚上回家吃完晚饭后,再开始用完整的时间专心读。每天读的文章数量不一定,有时候一天读20几篇,勉强挑出5篇;有时候一天读了5篇,但都觉得很不错,就不读其他的了。这个过程一般要花1到3个钟头。
我一般用 iPad 读文章,然后多任务打开 Slack,在里面跟我的小机器人(wanqu-ops)对话,有点像程序员工作的时候敲命令行一样。我会告诉小机器人,这是一篇文章的链接,然后它会自动提取文章的标题,url的slug,图片等信息,最后插入到后台数据库;每5篇文章自动生成新的一期,自动生成当天的日期(北京时间)以及当天的期号(比如第502期)。总之,我只要把链接扔给这个小机器人,它就能帮我做这些繁琐的事情了 --一个普通网站的小编会怎么做?会用CMS,然后一项一项手动输入,这个过程不会花太多时间,可能每天十几分钟吧;但我实在太懒了,连每天花这十几分钟都不舍得。
下一步要写简评。我也是跟小机器人对话,比如 “wq post 2672 title 这是一篇好文章” ,就是告诉它:我要把编号 2672 的文章标题改成 “这是一篇好文章”。同理,可以录入文章简评等内容。下图是我在 iPad 上读文章然后用 Slack 写简评:
凑足5篇文章后,我最后给小机器人下达命令:把这期的内容发出。然后它就自动把内容更新到网站,消息推送给 iOS app 的用户,自动发布到微博,Twitter,Facebook等社交平台。
总之,手动操作的部分主要是读文章与写简评。剩下的繁琐的操作,都是由程序自动完成的。同一道命题作文,程序员的写法是和普通网站小编不一样的;不同程序员的写法也会不同。
背后的技术
我为湾区日报这个 side project 写了一些代码。由于只是 side project,我对它的期望不高,力求用最简单的做法,花最少的时间,用最“快糙猛”的方法实现我想要的功能。
网站与后台架构
2014年8月份,湾区日报只是处于”闹着玩”的阶段,没有严肃要做的意思。所以那个时期的湾区日报网站只是一些静态网页,用 Pelican 生成的。
之后的网站换成了 WordPress。换了几个不同的主题。甚至还花了些钱买了 Product Hunt 的主题。
到了2015年3月份的某个周末,我也不知道哪根筋接错了,就宅在家里一天,从头写了现在的这个网站。主要用 Python/Django,Celery,RabbitMQ,Postgres 以及 Redis 搭建的。
为什么用Django?为什么能在一天做一个网站?很简单,我在过去两三年自己写了大大小小10几个 Web App,都是写着玩的 side project,都是用 Django/Postgres 搭建的,只是一个套路而已。将自己以前写的代码复制粘贴,当然可以很快拼凑一个网站出来了。况且,第一版的网站远比现在大家看到的这个网站简单得多;现在这个版本的网站是经过1年多时间不断改进的成果。
所有网站基本都能简化成这个架构:Web App,Datastore,Async Worker,Task Queue,以及 Scheduler。其中,Web App就是跑网站代码接受用户的访问请求,所有耗时间的task(比如发邮件,发微博,数据统计等)都扔到Task Queue上,然后Async Worker从Task Queue抓task过来离线处理;而Scheduler就是定时跑程序,很多网站直接用的Cron。
对于湾区日报,Web App就是Django App,用uwsgi跑N个进程,用supervisord 来管理进程,前面挂一个 nginx 当 load balancer。Database用的是Postgres与Redis 。其中,大部分需要永久存储的数据都在Postgres中,而Redis存的是文章的访问数与一些只需保留一两天的数据。Task Queue是用RabbitMQ。Scheduler用的是Celery Beat。而Async Worker是Celery。下图是湾区日报后台简单的架构:
湾区日报的所有这些后台的大大小小20几个进程是跑在3台 DigitalOcean 的虚拟机上(每个月花$50多)。按照访问量的增长速度,这样的架构估计至少可以支撑到2020年 — 大家整天在科技媒体上看到每天动辄几百万几千万访问量,可能都忘了世界上大部分的网站其实访问量都非常非常少的吧。当然,我在这3台虚拟机上也跑了其他的 side project,也不算浪费计算资源。我自己还有一些简单的小项目放在Bluehost上,与别人共享主机(没有root权限),就是图个便宜(每月低于$5)。
iOS App
2015年5月份的时候,用了一个周末写了 iOS App。完完全全用 Swift 写的,很不错的体验。前面说了,这只是一个 side project,所以期望不高,我是可以厚着脸皮上线质量低劣的一个周末写的 app 的,反正也没什么人用,以后慢慢改进就是了。
后台的 Api 也是 Django App(抽象出一些通用的内部 api 与网站共用),也是跑了 个 uwsgi 进程然后用 nginx 做 load balancer。
在开发第一版 App 的那个周末,除了完成基本的功能(浏览文章)外,还加入:
- Crash report(用 Crashlytics),第一时间通知我用户的 app crash 了,方便我调试;
- Google Anlytics,监测用户使用 app 的情况,比如多少人在线,哪个页面比较多人访问,哪个按钮比较多人点击等;
- Appirater,提醒那些使用了几次 App 的用户去 App Store 给个好评;
- PSUpdateApp,提醒用户有新版本了,快及时更新。
这些都只是套路而已。尽管不是 iOS 开发人员,但至少要有这么一个意识。上线一个产品,最基本的东西都要有:收集 metrics,收集 crash 的信息,更新提醒等 --开始做 app 之前有跟同事打听了一下应该用哪些工具,少走了不少弯路。详见这篇博文:两天四夜上线一个 App
发布系统
湾区日报这个博客有很多推送的渠道。现代博客与10年前的博客不同:10年前的博客就是一个网站,而现代的博客形式多样,同一份内容会以不同的形式通过不同的渠道出现在读者面前。
前面介绍到我用 Slack 与机器人对话。这机器人就是 Hubot -- 它解析我发出的命令,然后调用我在 Django App 里提供的 REST api 发布文章;当文章的状态由 pending 变成 published后,将触发一些 Celery tasks;每个 task 负责一个渠道的发布任务,比如一个task发布到微博,一个task发布到reddit等。
微博:调用微博 api 发布。凡是微博的帖子底部有 “来自 湾区日报BayArea” 字样的,都是由发布系统自动发布的。
微信:理论上,成为认证用户后,可以调用 api 自动发消息给订阅者们。但我没有成为认证用户,只能手动发布。Hubot会生成每期需要发布的文字,我只要复制粘贴就能发出消息了。对于微信,我基本是放弃了,完全不上心;因为湾区日报这种分享链接的模式很不适合在微信这种平台做。
Twitter:调用 Twitter 的 api 发布。Twitter 的 api 做得不错。
Facebook:调用 Facebook 的 Graph API 发布。
Reddit:调用 Reddit 的 api 发布的。难道有中文用户上 Reddit?我不确定。我只是把 Reddit 当作一种 SEO 的手段而已。
Google+:Google提供的api很难用,我不愿意花时间研究,于是就用 Buffer自动同步 Twitter 上发的内容。
邮件订阅:用 MailChimp 自动读取 RSS,每天定时自动发布。
iOS App 推送:没什么好说的,就是 APNS。每隔2个钟头,会有一个 Celery Beat 的 job 清理掉那些已经失效的 device token(比如有用户关掉了 push notification 或者已经把湾区日报的 app 卸载了);这就是为什么打开消息推送的用户数字有时候会变小。定期清除失效的 device token 可以避免浪费服务器计算资源白白发送无效的 push notification。
RSS:老旧的,但仍然很有用的 RSS。你会很惊讶,现在很多线上媒体不提供 RSS订阅。
为了避免同一篇文章在短时间内不小心被多次发到以上渠道,我会针对每篇文章生成一个 uuid 存到 Redis 里;每次修改文章简评的时候,会查一下这个 uuid 是否存在,如果不存在,就推送到以上渠道;反之就不推送。
很多读者注意到了,湾区日报在各个社交媒体上不断炒冷饭,重复发布几个月前发过的文章。我在 FAQ 里有详细说明原因。简单说,半年后,湾区日报的读者数量翻倍,将有一半的读者没有读过我以前推荐过的文章,“旧”的文章对他们而言是“新”的。
怎么炒冷饭呢?每天推荐新文章时,我会判断这篇文章是不是 evergreen content,半年后来看是否依然适用;如果是的话,我就把该文章扔到一个队列中(存在 Postgres 里);然后 Celery Beat 每个钟头会有一定概率(不同时段的概率不同)从队列里取出文章发到微博,Twitter,Facebook等社交媒体上,比如这条微博。队列里的这些旧文我会定期清理;有的文章真的发了好几次了,老读者们是在看腻了或者已经不再适用了(比如关于Apple Watch即将发布的相关文章;现在Apple Watch已经发布了,就不能再发这类文章),那就得从队列里删除。
数据统计
如果你经常使用湾区日报的 iOS app 或者网站,你就会发现我竟然公开了湾区日报的运营数字:访问量,app下载量,app打开消息推送的用户数,app的付费用户数等。下图告诉你在哪里可以找到湾区日报的各种运营数据:
湾区日报毕竟不是一个公司,我也不跟任何人合作,所以我说的算。我觉得中文的互联网上能找到运营网站的实战案例很少。一个对互联网感兴趣的人,当然希望能看到一个真实的运营案例以及真实的数据,作为一个学习材料 -- 有点像教科书上的案例分析。
这些数据分别从 App Annie 的 api,Google Analytics 的 api,以及我自己的后台数据库去抓去。Celery Beat 每小时开启一个 job 更新一下这些数据。
对于湾区日报的 iOS App,我最关心的数字是有多少用户开启了 push notification;只有开启了 push notification,用户才能及时得知当天的湾区日报的更新。每当有一个新用户开启了 push notification 或付费了,我的 Slack 就得到通知。下图是我 iWatch 上来自 Slack 的通知,告诉我此刻有一个新用户下载了 App 并且打开了 push notification:
关于 Slack 与 Hubot,详见这篇博文:湾区日报的第一个“员工”:Slack/Hubot
湾区日报分享的每篇文章都有一个访问量的统计,这个统计数字是存在 Redis 上,这比存在 Postgres 上每次访问都要写数据库的 cost 要小得多;存在 Redis 上即使失去了1小时的数据也无所谓,这个统计数字不是那么 critical。
搜索功能
网站与 iOS app 上都可以关键词搜索往期的文章。我原来想用 ElasticSearch 来做一下,但想着要多管理一个ElasticSearch就不爽,这样就多了一个 failure point,运维上可能得多花点时间,不划算。
后来受到这篇文章的启发,用一个晚上的时间直接利用 Postgres 做全文索引,并实现了网站与 app 的搜索功能,真是快糙猛的做法。
运维
我一般每星期发布一次新版本的 iOS App。每次的更新其实很有限。我每星期一般只能花不到一小时来写 app 的代码。
网站后台代码的更新主要通过 Slack。仍然是与小机器人对话:“wq deploy”。它就会 checkout 网站的 Git repo 的最新 master branch,然后重启相关进程。我用 symlink 做了版本控制,所以如果发现严重的bug,要 rollback 到前一个版本的话,只需要切换一下 symlink,几秒钟的事情。
发布代码变得轻松愉快,且充满自信,这就鼓励了开发人员(我)勇于尝试各种新奇的 idea,勇于快速迭代,快速部署小更新。所以湾区日报一直在进化。湾区日报的今天没有比昨天好多少,但这个季度肯定比上个季度改进了不少。
监控网站是否挂了,我用的是 Pingdom;如果网站挂了,我会收到短信提醒。监控服务器各种计算资源的使用情况,我用的是 Datadog(2年前去开Dockercon 的时候,在他们摊位聊了很久,才知道有这么个东西)--理论上,monitoring 与 alerting 都可以自己用开源软件搭建,但是,湾区日报只是一个 side project 而已,有现成可以凑活着用的SaaS方案,就不必花时间自己做了;而且自己做的肯定不如人家专业的。
对于网站与app后台的详细监控,我用的是Datadog。可以方便地看到不同 endpoint 在特定时间段内的请求次数以及latency。详情请见:湾区日报是如何监控系统的健康情况的
网站域名的 DNS 管理,我用 CloudFlare。使用 CloudFlare 还有其他的好处,比如它帮我挡住了不少恶意请求,帮我缓存了各种静态文件。
客服
每天都有读者来信。当然,大部分的问题都能在 FAQ 里得到解答。但也有很多来信给了我很多鼓励,知道你们从湾区日报推荐的文章里学到了东西得到了成长,我真的很高兴。
微博与微信上的读者来信,大部分都能得到关键词匹配的自动回复,算是缓解了我手动回复的压力。而花时间去写 FAQ 页面则省去了回复大部分邮件的时间。
代码管理与新功能的开发
湾区日报有3个 git repo(网站后台,iOS app,以及运维相关的脚本),都放在 Bitbucket 上,不对外公开。使用 Bitbucket 而不用 GitHub 的原因是为了省钱;如果要在 GitHub 上使用 private repo,得交钱;而 Bitbucket 我个人用的话,可以有无限数量的 private repo。
我比较倾向于 commit 小段代码,出问题了也可以比较小粒度地 revert。一般一个 commit 包含的代码量不多于 100 行。很显而易见的代码修改我直接在 master branch 上操作,然后直接 push master;比较 tricky 的代码修改我会建立一个新的 branch,然后给自己发 pull request,自己 review 一遍自己写的代码。
我经常上线没有写完的代码,然后 wrap 在 feature switch 里面。我这种小破 project 大部分的方案都是土办法,不用太学术派不用太杀鸡用牛刀,意思一下就行了。所以一个 feature switch 其实就是 Postgres 里的一个表格里的一行。湾区日报99.999%的workload是 read-only 的,所以网站上很多页面其实都是缓存在内存然后走CDN的,数据库访问不会太 crazy。
顺便提一下,有一些年轻的读者(学生或者刚工作不久的年轻人)发邮件过来逼我开源湾区日报的代码,让我有点哭笑不得。联想到一有什么自然灾害,名人们就要被逼着捐款。详见 FAQ 第18条。
开发环境
我用家里的 iMac 写代码。Mac OS X 下运行虚拟机 Vagrant + VirtualBox。虚拟机里跑的是 Ubuntu,与 production 里用的操作系统一样。然后通过 Vagrant 的 synced folders 在 Mac OS X 与虚拟机之间共享代码的文件夹。在 Mac OS X 上用 PyCharm 写代码,利用 PyCharm 里的 Vagrant 的支持,在虚拟机里跑服务器。
数据备份
有一个每天自动执行一次的 job 把所有数据 dump 出来,压缩一下,按日期命名,最后上传到某个地方。上传到哪里呢?任何提供API的云存储服务都行(Dropbox、Box之类的)。
对于 PostgreSQL 里的数据,我用 Django 的 dumpdata 命令把所有数据 dump 成一个 json file,然后 gzip 一下。
对于 Redis 里的数据,直接 gzip 一下 disk 上的 dump.rdb。
详情见这篇博文:湾区日报是如何备份数据库的?
总结
如果把这篇文章给2014年8月6日的我看,我肯定不会做湾区日报这个 side project 了,太花时间了 -- 竟然要做网站,要做发布系统,要学 Swift 然后写 iOS app,还要每周发布 app 新版本,还要管理好几个发布渠道,还要做客服回邮件,还要每天花1到3个钟头读文章写简评。
幸亏写这篇文章的时候,大部分困难的工作已经完成了。这就是互联网时代的产品迭代,是一个循序渐进的过程,是个人与产品共同成长的过程。你不能期望一夜成功 -- 其实,你也可以一夜成功,只是这一夜发生在第1000天甚至第3600天。
运营湾区日报是没什么经济上的回报的,尽管有一些广告收入与app内购的收入,但与投入的时间(再乘以一个哪怕是实习生的时薪)相比,相当微不足道。之所以我还能继续做下去,是看中知识的积累与个人的成长。这些运营经验以及从每天5篇文章里学到的东西,是完全可以运用到我其他的项目上的。就算最后真的什么都没做成,也是一段美好的回忆。临死的时候,回顾一生,至少我还能说,我做过一个叫湾区日报的博客,每天有那么几万个读者(我也是读者之一),我们每天都在进步。这就够了。