帮助 Medium 阅读时间达到 2600 年的技术栈
背景
Medium 是一个网络。这是一个分享有价值的故事和想法的地方,在这里人们可以保持想法不断的前行,大家已经在那里花费了 14 亿分钟(换句话说是 2600 年)的阅读时间。
每月的独立用户超过了 2500 万,每周会新增数以万计的新帖子。但是我们心目中的 Medium 应该是以观点量来衡量是否成功,而不是阅读量;不会关心作者的资历,而更关心想法是否有价值;应该能更方便的为大家推送有价值的内容。
我领导了整个工程团队。我之前是 Google 的一名软件工程师,从事 Google+ 和 Gmail 的开发,共同创建了 Closure 项目。在那段时间,我经历过滑雪板比赛,飞机跳伞,还有丛林冒险。
工程团队
我为我的这个团队骄傲。团队里每个人都是天才,大家都充满求知的渴望,为了这个让人激动的事情聚在一起。在我们看来,经历过不同的磨练可以让你成为更有经验的工程师。有兴趣的话可以看看我们其他的价值观。
在如何安排自己的工作上,团队有很高的自由度,不过作为公司,我们会设立季度目标,鼓励迭代冲刺。我们使用 GitHub 做代码 review 和 bug跟踪,用 Google Apps 管理邮件、文档和电子表格。我们是 Slack 的重度用户 (以及 slack bots),有不少团队使用 Trello。
初始的技术栈
刚开始的时候我们把服务部署到 EC2 上。主服务是用 Node.js 写的,每次发布的时候,会合并到DynamoDB。
还有一个 node 服务器用于图片处理,调用 GraphicsMagick 来做具体的复杂的工作。另一个服务被用作 SQS 队列处理,负责后台任务。
我们的 email 使用 SES,静态资源放在 S3 上,CDN 使用 CloudFront,使用 nginx 作为反向代理。另外,使用 Datadog 做监控,PagerDuty 做报警。
网站使用 TinyMCE 作为编辑器。发布之前,我们已经在使用 Closure 编译器和部分 Closure 库,不过模板用的是 Handlebars。
当前的技术栈
Medium 这样的站点看上去似乎很简单,不过背后的复杂程度足以让人惊讶。这还仅仅只是一个博客网站么?要是这样的话,你用 Rails 几天就能搭建一个出来。:)
闲话少说,我们从最底层说起。
产品环境
目前我们运行在 Amazon 的虚拟私有云上。我们使用 Ansible 做系统管理,可以让我们的配置处于源码控制下,可以通过可控的方式很轻松的进行更新。
我们有着面向服务的架构,在其上运行了大约一打的产品服务(取决于你如何去统计,还有很多小一点的服务)。是否作为独立的服务来部署,主要取决于其功能,服务边界上是否可能产生有依赖的变动,以及对资源的利用。
我们主要的应用服务器仍然写在Node里,它允许我们在供应商和客户之间交换代码,我们用编辑器就要用到它,公布变革也是用它。Node为我们工作 得很好,但是在我们把事件循环编写成块的时候,性能问题就浮现了。为了缓和这一问题,我们在每台机器上运行多个实例,并把它们路由到昂贵的端点,以此来分 隔它们。我们把它与V8运行环境相挂钩,以深入了解哪个部件运行花费的时间较长。通常它是因为在JSON还原序列化时目标的具体化。
我们用Go编程的时候可以享受到一些辅助服务。我们发现用GO建立、打包和部署很容易。我们喜欢不需用到繁冗并调试虚拟机Java的类型安全。就我个人而言,我更喜欢在团队环境中使用武断的语言。它能提高一致性、减少歧义,最终让你避免作茧自缚。
我 们现在使用CloudFlare提供静态资源,尽管我们把5%的流量送到Fastly,同时还送5%的流量到CloudFront来保持它们的缓存热度, 以防在紧急情况下万一我们需要割接。最近我们也把CloudFlare用于应用流,基本上是为了DDOS保护。但是对于性能提升我们还是乐见其成的。
我们使用Nginx和HAProxy的结合作为反向代理,达到负载均衡,以实现我们需要的Venn Diagram特性。
我们仍然使用Datadog监测,使用PagerDuty报警,但是我们现在大量使用ELK(Elasticsearch,Logstash,Kibana)调试出现的问题。
数据库
DynamoDB仍然是我们基本的数据存储库,但是它仍不完美。我们遇到的常见的问题之一是在重大事件发生时和有百万追随用户时的热键问题。在Dynamo前面我们可以用Redis缓存器,它能通过读取来减轻这些问题。
开发者便利性和产品稳定性二者之间的优化似乎总是存在矛盾,但是我们在努力缩小分歧。
我们开始使用Amazon Aurora获取最新的数据,它的查询和过滤功能比Dynamo更灵活。
我们使用Neo4J来储存代表媒体网络的实体之间的关系,用两个副本来运行一个主本。
人、邮件、标签和合类是图表中的节点。
边界在实体创建的基础上建立。当人们发生以下行为,比如跟随、推荐和强调时,我们按图索骥,过滤并推荐相关的邮件。
数据平台
早起我们的数据很匮乏,所以在数据分析基础架构上做了很多投资,为商业和产品上的决策提供帮助。最近以来,我们可以在同样的数据处理流程上为整个产品体系提供更多的反馈,甚至可以运行类似 Explore 这样的数据驱动的功能。
我们使用 Amazon Redshift 作为数据仓库,它提供了可伸缩的存储和处理系统,我们其他的工具就运行在其上。我们持续的把核心数据(例如用户、文章)从 Dynamo 导入到 Redshift,以及把行为日志 (例如:文章阅读、翻页等等) 从 S3 导入到 Redshift。
我 们使用 Conduit 来对任务做调度,这是一个内部工具,可以管理计划、数据依赖,还可以进行监控。我们的任务调度模型是基于断言的,只有一个的所有的依赖都满足了,这个任务 才会被执行(例如,依赖全天行为日志的每日任务)。对于产品,这方面被证明是非常重要的:数据的生产者和消费者互相解耦,简化配置,系统状态可预知和易调 试。
尽管对我们来说在 Redshift 上运行 SQL 查询良好,我们还是需要将数据不断输入输出 Redshift。我们越来越转向 ETL 的 Apache Spark,这是因为它的灵活性与规模增长的能力。随着时间的推移,Spark 可能会成为我们数据管道的首选工具。
我们使用协议缓冲(Protocol Buffers)对我们的模式(模式演化规则)来保持所有层的分布式系统同步,包括移动应用,web 服务,和数据仓库。使用自定义选项,我们标注模式与表名和索引等配置细节,验证约束最大长度的字符串,或者控制接受数据控制的范围。
人们也需要保持移动和 web 应用程序同步,开发人员使得所有日志一样,产品研究员可以以同样的方式解释字段。我们帮助我们的成员在数据处理模式规范上的工作,并严格记录字段的消息,发布文档生成的原型(.proto)。
图像
我们的图片服务器现在是用 Go 写的,并使用了瀑布策略处理图像。服务器使用 groupcache,它提供了 memcache 的替代方案,来减少重复的工作。支持内存中的缓存是一个持久的 S3 缓存,然后来处理图像处理需求。这让我们的设计师可以在不同平台上,灵活地改变图像的表示和优化,而不必做大的批处理缩放图像作业。
现在主要用于调整和裁剪,早期版本的网站允许颜色清洗、模糊和其他图像效果。处理动态 gif 一直是一个巨大的头痛的问题,这应该又是另一篇文章了。
文本截图
完整的文本截图功能由一个小型的 Go 服务器驱动,使用 PhantomJS 作为渲染引擎。
我一直想把渲染引擎转换为 Pango,但在实践中,将图片嵌入 HTML 的方式更为灵活和方便。这项功能的使用频率意味着我们可以很简单地处理吞吐量。
自定义域名
我 们允许用户对他们的 Medium 作品设置自定义域名。我们想让单点登录和 HTTPS 无处不在,所以让它开始工作不是件小事。我们有一组 HAProxy 服务器专门用于管理证书和导向主应用服务器的流量。在设置域名时还需要一些手动操作,但是我们通过自定义整合 Namecheap 已经自动化了很大一部分。证书的规定和公开链接是由一个专用服务处理的。
Web 前端
在网页上,我们倾向于电子化。我们有自己的单页应用程序框架,使用闭包作为标准库。我们使用闭包模板在客户端和服务器渲染,我们使用闭包编译器压缩代码并把它分割成模块。编辑器是我们网页应用最复杂的部分,Nick 写出了 iOS 系统。
iOS 系统
我们的应用程序都是本地下载好的,很少使用网络视图。
在 iOS 中,我们使用国产构架和内置构件的混合。在网络层,我们使用 NSURLSession 提出请求,使用 Mantle 来把 JSON 解析成模型。 我们有一个建立在 NSKeyedArchiver 上的缓存层。我们有一个通用的方法把项目列入不同的列表,同一列表中的项目有共同特征。这就能让我们快速建立不同类型内容的列表。后视图是用 UICollectionView 自定义布局构建的。我们使用共享组件来渲染完整的文章和后预览。
Medium 的员工尽一切努力来尽快开发和推出新的应用。我们发布更新的节奏是受限于 Appstore 的审阅周期的,但我们正尽己所能地推进这一进程,即使只有很少的更新。
测试我们使用 XCTest 和 OCMock.
Android
对于 Android,我们目前会保持 SDK 和 support 库都是最新的。我们没有使用综合性的框架,而是倾向于为重复的问题建立一致性的模式。我们使用 guava 来弥补 Java 缺失的功能。不过有的情况下,我们会使用以特定问题为目标的第三方库。我们的 API 的返回结果使用了 protocol buffer 协议,在 app 里会生成这些对象。
我们使用 mockito 和 robolectric。 我们会为动态编写高层级的测试用例 — 在我们刚开始添加界面或者准备重构的时候会创建一些简单的版本。在我们不断重现 bug 后,测试用例会越来越多,帮我们避免代码功能回退。我们编写底层的测试用例来验证单个类的细节 — 在实现新功能后,通过这些用例能看到类之间是如何交互 的。
所有提交会自动推送到 play store,用于 alpha 版本,Medium 的员工可以立刻使用到。(这里包括另一个最受欢迎的 app,我们的内部的 Medium 版本— Hatch)。大多数周五,我们会把最后一个 alpha 版本发布到 beta 组, 让大家在周末去使用。接下来在周一产品会由 beta 变成正式版本。因为最新代码一直保持可发布状态,所以一旦我们发现一个 bug,我们可以在正式产品上立刻修复。如果不放心某个新的功能,可以让 beta 版测试的时间稍微长一些;如果感觉好的话,发布也可以更频繁一些。
A|B 测试 & 功能标志
我们所有的客户端都使用服务器提供的功能标志,它被叫做变体(variants),使用 A|B 测试来保护未完成的功能。
杂项
有很多在产品边缘的其他东西,我在上面没有提及:Algolia 提供我们迭代搜索相关的功能,SendGrid为出入站的邮件设计,Urban Airship 为的是通知功能,SQS 是为了处理队列,Bloomd 是 bloom 过滤器,PubSubHubbub 和 Superfeedr 是为了 RSS,等等,等等。
编译,测试,部署工作流
我们拥抱持续集成和交付,尽可能快地推动绿色(部署)。Jenkins 管理所有这些过程。
过去我们在使用中建立我们的系统,因此我们不会为一个新工程迁移而到 Pants。
我们有一个组合单元测试和 HTTP 级功能测试。所有的提交必须要经过测试才可以合并。我们工作的团队在盒子内使用 Cluster Runner 分配测试,并让其更快。还能很好地与GitHub 集成。
我们尽可能快地部署过渡环境——目前大约 15 分钟——然后给候选的产品使用。主要的 app 服务正常部署在一天五次左右,但是有时候可能多达 10 次。
我们做蓝色/绿色部署。在生产环境中我们发送流量给 canary 实例,并在发布与部署之前发布过程监控错误率。Rollbacks 已经内置了 DNS 转换。
下一步要做的
我们现在正在开始做用于作者和发布商的支付功能。这是一个全新领域的项目,我们现在基本上已经清楚如何去做了。我们认为将来需要更多的方式来产生内容,我们希望支付的手段能激励产生更多高质量的内容和价值。
加入 Medium
我们长期对那些有消费领域经验的任务驱动的工程师有很大兴趣。我们对你了解哪门编程语言不很关心,因为我们认为好的工程师可以很快学会新的技能,但是我们期望你是求知欲高、敏锐的、坚定的和忘我投入的。也就是说,不管是iOS,Android,Node 还是 Go 的经验,都可以来试试。
我们还在扩充我们的产品科学团队,因此还需要构建数据流处理和大型分析系统方面有经验的人。
另外,我还在找一些工程师 leader,可以帮我们在团队扩充的时候管理好团队。他们需要对组织管理很感兴趣,乐于实践,愿意为团队奉献。
你可以通过 Medium 联系我们。