背景
说到底,Medium 是个社交网络,人们可以在这里分享有意思的故事和想法。据统计,目前累积的用户阅读时间已经超过 14 亿分钟,合两千六百年。
我们支持着每个月两千五百万的读者以及每周数以万计的文章发布。我们不想 Medium 的文章以阅读量为成功的依据,而是观点取胜。在 Medium,文章的观点比作者的名头更重要。在这里,对话促进想法,并且很看重文字的力量。
我是 Medium 开发团队的负责人,此前在 Google 工作,负责开发 Google+ 和 Gmail,还创立了 Closure 项目。业余时间我喜欢滑雪跳伞和丛林冒险。
团队介绍
说起团队我非常自豪,这是一群富有好奇心而且想法丰富的天才,大家凑到一块是想做大事的。
团队以跨功能的任务驱动,这样每个人既可以专攻,又可以毫无压力的对整个架构有所贡献。我们的理念就是接触的方面越多,对团队的锻炼越大。更多关于团队的理念见此。
在工作组织方面,我们有着很大的自由度,当然作为一个公司组成,我们还是有季度目标的,并且鼓励敏捷开发模式。我们使用GitHub 进行code review 和问题跟踪,用Google Apps 作为邮件、文档和表单系统。跟很多团队习惯使用Trello 不同,我们是Slack 和slack 机器人的重度用户。
原始架构
最开始的时候,Medium 部署在EC2 上,用Node.js 实现,后来公测的时候迁移到了 DynamoDB 。
其中有个节点用来处理图片,负责将复杂的处理工作转向 GraphicsMagick 。还有一个节点用作后台的 SQS 队列处理。
我们用 SES 处理邮件, S3 做静态元素服务器, CloudFront 做 CDN, nginx 作为反向代理, Datadog 用来监控, Pagerduty 用来告警。
在线编辑器用了 TinyMCE。上线之前我们已经开始使用 Closure 编译器以及部分的 Closure 库,但是模板还是用的 Handlebars 。
当前架构
虽然 Medium 表面看起来很简单,但是了解其后台的复杂性后,你会大吃一惊。有人会说,这就是个博客啊,用 Rails 之类的一周就能搞定了。
总之,闲话不多说,我们自底向上介绍以后再做判断。
重要通知:接下来 InfoQ 将会选择性地将部分优秀内容首发在微信公众号中,欢迎关注 InfoQ 微信公众号第一时间阅读精品内容。
运行环境
Medium 目前运行在 Amazon 虚拟私有云,使用 Ansible 做系统管理,它支持配置文件模式,我们将文件纳入代码版本管理,这样就可以随时回滚随时掌控。
Medium 的后台是个面向服务的架构,运行了大概二十几个产品服务。划分服务的依据取决于这部分功能的独立性,以及对资源的使用特性。
Medium 的主体仍然是 Node.js 完成,方便前端和后端的代码共享,主要是文章编辑和发布这个过程。Node 大部分时候不错,但阻塞 event 循环的时候会有性能问题。为了缓解,我们在每台机器上启动多个 Node 实例,将对性能要求比较高的任务分配给专门的实例。同时我们还深入 V8 运行时环境查看更加细节的耗时,基本上是 JSON 去串行化的时候的对象具体化耗时较多。
我们还用 Go 语言做了一些辅助服务。因为 Go 非常容易编译打包和发布。相比 Java 语言的冗长罗嗦和虚拟机,Go 语言在类型安全方面做的很到位。就个人习惯来讲,我比较喜欢在团队内部推广强类型语言,因为这类语言能够提高项目的清晰度,不纠结。
目前静态元素大部分是通过 CloudFlare 提供的,还有 5% 通过 Fastly,5% 通过 CloudFront,这么做是为了让两者的缓存得到更新,用于一些紧急的情况。最近我们在应用流量上也使用了 CloudFlare,当时主要是为了防止 DDOS 攻击,但随之而来的性能提升也是我们愿意看到的。
我们使用 Nginx 和 HAProxy 做反向代理和负载均衡,来满足我们所需功能的维恩图。
我们仍然使用 Datadog 来监控, Pagerduty 来告警。现在又增加了 ELK( Elasticsearch 、 Logstash 、 Kibana )来进行产品问题调试。
数据库
DynamoDB 仍然是我们的主力数据库,但是用起来也不是毫无问题。目前遇到的比较棘手的是大 V 用户展开和虚拟 event 过程中的热键问题。我们专门在数据库前面做了一个Redis 缓存集群,来缓解这些问题。到底为开发者优化还是为产品稳定性优化的问题通常会引发争执,我们也一直在尝试中和两者的矛盾。
目前我们开始在存储新数据上使用 Amazon Aurora ,它可以提供更灵活的查询和过滤功能。
我们使用 Neo4J 存储 Medium 网络中实体之间的关系,运行在有两个副本的主节点上。用户、文章、标签和收藏都属于图中的节点。边则是在实体创建和用户进行推荐高亮等动作时生成。我们通过在图中游走来过滤和推荐文章。
数据平台
早期我们对数据非常渴望,不断尝试数据分析框架来辅助商业和产品决策。最近我们则是利用同样的框架来反馈产品系统,支持 Explore 等数据驱动功能。
我们采用 Amazon Redshift 作为数据仓库,为生产工具提供可变存储和处理系统。我们持续将诸如用户和文章等核心数据从 Dynamo 导入 Redshift,还将诸如文章被浏览被滚动等 event 日志从 S3 导入 Redshift。
任务通过一个内部调度和监控工具 Conduit 调度。我们用了一个基于断言的调度模型,只有条件满足的时候,任务才会执行。从产品角度来讲,这是不可或缺的:数据制造方应该与数据消费方隔离,还要简化配置,保持系统的可预见和可调试性。
Redshift 的 SQL 检索目前运行不错,但我们时不时需要读取和存储数据,所以后期增加了 Apache Spark 作为 ETL,Spark 具有很好的灵活性和扩展能力。随着产品的推进,估计后面 Spark 会成为我们数据流水线的主要工具。
我们使用 Protocol Buffers 作为 schema 来确保分布式系统的各层次间保持同步,包括移动应用、web 服务和数据仓库等。通过定制化的选项,我们将 schema 标记上更加细化的配置,如带有表名和索引,以及长度等校验约束。
用户也需要保持同步,这样移动端和网页端就可以保持日志的一致性了,同时方便产品科学家们用同样的方式解析字段。我们帮助项目成员从.proto 文件中生成消息、字段和文档等内容,进而利用所得数据开展研究。
图片服务器
我们的图片服务器现在用 Go 语言实现,采用瀑布型策略来提供处理过的图片。服务器使用 groupcache ,是 memcahce 的替代品,可以帮助减轻服务器之间的重复工作。而内存级缓存则是用了一个 S3 的持续缓存。图片的处理是请求来触发的。这给了我们的架构设计师灵活改变图片展示的自由度,为不同平台优化,而且避免了大量的生成不同尺寸图片的操作。
目前 Medium 对图片主要支持放缩和裁剪,但原始版本中还支持颜色清洗和锐化等操作。处理动图很痛苦,具体后续可以写一篇文章来解释。
文本标注
文本标注是个有意思的功能,用了一个小型 Go 服务器,跟 PhantomJS 接口形成渲染进程。
我一直想要把渲染进程换到 Pango,但是在实践过程中,能在 HTML 中摆放图片的能力的确更灵活。而从功能的使用频率来看,这意味着更容易开发和管控。
自定义域名
我们允许用户为其 Medium 文章设置个性化域名。我们想做成单点登录且 HTTPS 全覆盖,因此实现起来颇有难度。我们专门准备了一批 HAProxy 服务器用来管理证书,并向主要应用服务器引导流量。初始化一个域的时候需要一些手动的工作,但是通过与 Namecheap 的定制化整合,我们将其大部分转换为自动化。证书验证和发布链接由专门服务负责。
网站前端
网页端这块,我们有自主研发的单网页应用框架,使用 Closure 标准库。我们使用 Closure 模板渲染客户端和服务端,然后使用 Closure 编译器来缩减代码并划分模块。编辑器是我们网页端应用最复杂的部分,具体参见 Nick 此前的文章。
iOS
我们的两个应用都是原生的,尽量避免使用网页视图。
在 iOS 上,我们使用了一系列的自建框架,以及系统原生组件。在网络层,我们用 NSURLSession 发起请求,用 Mantle 解析 JSON 并映射到模型。我们还有一层基于 NSKeyedArchiver 的缓冲层。对于将条目渲染为共同主题的列表,我们有一个通用方法,这让我们能够快速为不同类型的内容构建新列表。文章界面是一个定制布局的 UICollectionView。我们使用共享组件来渲染全文界面和预览界面。
应用代码的每一次提交都会编译后推送给 Medium 员工,这样我们能够很快尝试新版本。应用商店的版本是滞后于新版本的,但我们也一直在尝试更快的发布,虽然可能仅仅是几处小更新。
对于测试,我们使用 XCTest 和 OCMock。
Android
在 Android 方面,我们与当前的 SDK 和支持库版本保持一致。我们并没有使用任何复杂的框架,而是倾向于为重复出现的问题构建持续性的模式。我们利用 guava 弥补 Java 中所有的缺失。另一方面来讲,我们也倾向于使用第三方库来解决特别的问题。我们还利用 protocol buffers 定义了 API,用以生成应用中的对象。
我们利用 mockito 和 robolectric 。我们会开发一些高层测试来运转 activity 和 poke:刚添加 screen 或要重构的时候,先创建一些基本的版本,随着我们复现 bug 它们也会进化。我们还会开发一些底层测试,来检测一个特定的类:随着新功能的增加我们会创建测试,这能够帮助我们思考和设计底层是如何交互的。
每个提交都会作为 alpha 版本自动推送到 play 商店,然后到 Medium 员工(包括我们的 Hatch,Medium 内部版)。推送大部分发生在周五,我们会把 alpha 版本发送给测试小组,请他们用整个周末进行测试。然后,周一我们会从beta 版推进至正式产品版。因为最近一批代码总是随时可以推送,因此一旦发现很严重的bug,我们就可以立即修复正式产品版。当我们怀疑某些新功能的时候,可以给测试小组更长的时间。开发比较亢奋的时候,也可能发布地更加频繁。
AB 测试及其他
我们所有的客户端都用了服务器端提供的功能标记,称为 variants ,用于 A|B 测试以及指导未完成功能的开发。
剩下还有一些框架相关的内容我没有提及: Algolia 让我们在搜索相关功能上快速迭代, SendGrid 处理邮件, Urban Airship 用来发送提醒, SQS 用来处理队列, Bloomd 用作布隆过滤器, PubSubHubbub 和 Superfeedr 用作提供 RSS 等等。
编译、测试和部署
我们积极拥抱持续集成技术,随时随地准备发布,使用 Jenkins 来负责相关事宜。
我们曾经使用 Make 作为编译系统,但是后来迁移到 Pants 。
测试方面我们采用单元测试和 HTTP 层面功能测试两者结合的方式。所有提交的代码都需要通过测试才能够合并。我们跟 Box 团队合作,利用 Cluster Runner 来分布式运行测试,保证效率,而且能够和 GitHub 很好的整合在一起。
我们大概不到 15 分钟就可以把某阶段的系统部署,顺利编译通过,留作正式产品的备选。主应用服务器通常一天要部署五次,多的时候十次。
我们采用蓝绿部署。正式产品版本的流量发送给一个 canary 实例,发布进程会监控部署过程的错误率,必要时候通过调整内部 DNS 回滚。
面向未来
到此,讲了足够多的干货!为了重构产品,获得更好的阅读体验,还有很长的路要走。我们仍然在努力为作者和发布者设计更多的功能。打比方来讲,线上阅读还是一片绿地,面对它有着无限可能,我们始终抱着开放的心态设计和实现功能。未来我们会努力用各种功能为用户提供高质量内容和价值。
感谢杜小芳对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群(已满),InfoQ 读者交流群(#2))。
立即免费注册 AWS 账号,获得 12 个月免费套餐:点击注册
有云计算问题?立刻联系 AWS 云计算专家:立即联系
评论