FreeWheel 基于 Go 的实践经验漫谈

  • 曹倩芸

2017 年 6 月 7 日

话题:最佳实践DevOps语言 & 开发架构文化 & 方法微服务

FreeWheel 从 2014—2015 年间开始使用 Go 语言进行开发,截至目前,整个体系内有超过 30 个项目是基于 Go 编写,占其总体应用数的四分之一以上,这几年 FreeWheel 内部所有团队都在或多或少使用 Go。对于新业务的开发,各工程师团队会首先评估 Go 语言是否适合完成这样的工作。

FreeWheel 在 Go 上的进化

作为美国最大的综合性传媒集团 Comcast 的子公司,FreeWheel 服务的对象大多为欧美地区大型的媒体公司,包括电视媒体和运营商等。这些公司拥有大量的视频资源和广告资源,因此 FreeWheel 主要为他们提供从广告部署、管理、投放、计费到预测的一系列完整解决方案。

正因为系列流程的复杂和业务场景各不相同,FreeWheel 各个产品子系统所使用的编程语言也会根据实际的业务场景有不同选择,其前期主要使用的语言包括 Ruby、C++、Erlang 等。

Go 语言在 FreeWheel 最早的一个尝试是 IVI(Instant Video Ingestion) 系统的应用(该项实践在后文进行详解),用 Go 重写 IVI 服务以后,在保持同样 20 毫秒相应时间的前提下,整体性能可以从每天几十万条数据扩展到每天几百万条数据。

从 IVI 项目中,一个 Go Common Library 的共用代码衍生而出 ,FreeWheel 中许多基本功能如日志解析等,都会被封装到这个库里供其他团队使用。此外,随着该项目的成功,工程师团队将其中积累的一些较好的经验进行应用和传承,Go 语言在 FreeWheel 的使用范围也逐渐扩展到其他更多的团队。比如由前端 UI 团队、广告决策和投放团队所开发运维的几大主要系统也得到了 Go 的很好的支持。

  • 广告部署、管理系统的开发和工作流处理:之前的广告部署和管理系统主要是基于 Ruby on Rails 框架的 Web 应用程序,开发过程还结合了更前端的 JavaScript、CSS 等语言。Ruby 面向对象、贴近英语的主要特性,以及 Rails 的速度和灵活性,可以使广告部署和管理系统的开发运维过程更加快捷高效,易于维护,另外欧美地区客户也能更顺畅地在该系统上登录使用,进而管理他们的视频和广告。此后,因为 Go 语言应用在这种轻量级、高性能服务端的适应性很强,因此该团队就用 Go 重写了所有的 API,包括对 API 网关,以及网关相应功能 (如用户的验证、流量控制等)的实现。
  • 广告决策和投放系统:该系统对实施性要求较高,因此基于性能的考虑,FreeWheel 用运行时效率更高的 C++ 语言直接编写广告服务器,使其具备支持高并发、低延迟、高吞吐等性能。Go 的使用得以广泛延伸后,该系统主干部分还是用 C++ 实现,但为了满足一些客户的新需求,比如在线视频转码服务就是用 Go 语言写的独立的服务。最近该系统的技术负责团队欲实现的一个新功能也是使用 Go 来开发,因其在性能和开发效率上的优势,该团队可以很方便的调用 Common Library 中已有的方法去完成大部分的基础工作。
  • 广告预测系统:该系统与上面所说的广告决策系统关系较紧密,最早实现是一个基于 Erlang 语言开发的调度系统,经过演变后的最新版本是使用 Go 语言进行的编写。演变的原因有三点:第一是性能的考虑,Go 提供了较好的库和工具链去帮助工程师团队找到其自身的瓶颈、分析并提高其性能;第二是团队适应性,FreeWheel 拥有上百位工程师,分布在全球亚、欧、美洲的一些国家,因此一致的工业化生产方式相比程序的美观优雅,会有更优先级需求,在让所有工程师共享一致的、可控的、容易分享的公共编程框架和规范上,Go 语言也更具优势;第三是相比之下 Go 在部署简易性上的特性。

Go 语言是 FreeWheel 公司目前主要力推的一个方向,在其看来,面向服务的架构的大环境中,Go 非常适合做一些功能相对独立、功能比较明确的微服务的语言。在结合已有的各种编程语言,计算框架(如 Hadoop、Java、Ruby、C++)的基础上,FreeWheel 把 Go 语言定位成用来实现轻量级服务或 API 的缺省编程语言,将之与用来完成更小粒度工作的 Python 结合在一起,就构成了 FreeWheel 的整个技术语言栈 。

 

FreeWheel 在 Go 上所经历的“坑”

虽然从 2012 年 Go 1.0 发布到团队相继采用 Go 来编写项目,这中间经历了大致三年左右的时间,但由于在 GC 等许多问题的克服上需要 Go 本身去做一部分迭代,FreeWheel 也需要把技术对客户的影响控制在一个可控的范围内,因此作为一家 B-to-B 企业,其采用了更为渐进的方式将 Go 语言应用到自身的生产平台上。

在这个过程中,FreeWheel 也经历过两个较为重要的“坑”。

  • GC 的问题

如多数人所知道的一样,Go 语言垃圾回收器存在一定的缺陷,特别是容易导致整个进程不可预知的间歇性停顿。像某些大型后台服务程序,如游戏服务器、APP 容器等,由于占用内存巨大,其内存对象数量极多,GC 完成一次回收周期,可能需要数秒甚至更长时间,这段时间内,整个服务进程是阻塞的、停顿的,在外界看来就是服务中断、无响应。FreeWheel 在使用 Go 1.4 版本时也遇到过类似问题:广告预测团队用 Go 来实现调度器,平常运行的时候没有问题,但一旦触发 1.4 版本下 GC 的时候,该系统的 downgrade 非常厉害,导致任务的堆积非常严重,触发报警,同时其处理性会下降很多,也会影响其他上下游系统的正常运转。

于是,FreeWheel 在当时主要采取了三种对策:一、并不把 Go 用在非常关键的、对服务进程稳定性要求较高的系统里;二、引入 Kafka 之类的能够持久化的消息队列,能够缓存和重释这样的方式去解决这个问题,使系统能扛住冲击,并在后面把它消化掉;三、尽量复用已经创建的对象,防止 Go 频繁的创建了回收对象。

Go 1.5 到 1.7 版本相继出来后,GC 的系统性能得到不断改进和持续提升(从秒级到毫秒级)。对于目前 FreeWheel 内生存环境不太关键的系统来说,Go 1.7 之后的 GC 已经可以达到可接受的范围和程度以内。

  • 内置数据结构的变化

很多人都知道,Go 语言提供的字典类型并不是并发安全的,此外由于 Go 语言发展较快,有些内置的数据结构如 Map 的行为也发生了变化。因为 Map 为引用类型,所以即使函数传值调用,参数副本依然指向映射 m,所以多个 goroutine 并发写同一个映射 m。例如,如果 map 由多协程同时读和写就会出现 fatal error:concurrent map read and map write 的错误。

Go 1.6 版本之前 Map 可以支持并发读写,但 FreeWheel 开发的程序在升级到 1.6 之后也就发现 Map 产生了读写竞争的问题。

对于这一问题,常用的有两种解决方案,一是如上所说的加锁(包括通用锁和读写锁),二是利用 channel 串行化处理。FreeWheel 的做法也主要靠两方面,其一是将锁粒度设计的更细,使得并发的依赖更少;另外是在不同的数据结构中,选择性能更高的一方。比如 array 和 slice 中,前者就是更优选择。

FreeWheel 首席架构师刘昊植认为,并发的时候不能假设 Go 能完美处理所有的工作,工程师需要结合并借鉴传统(成熟)的编程语言,比如 Java 或者 C 对并发的经验,在动手之前就想清楚并发的规模、锁的粒度等,并对系统会如何运行有非常明确的设计和理解。

同时,刘昊植也坦言,随着 Go 的快速发展阶段,FreeWheel 内部也有多种不同的声音,例如运维团队就会觉得快速发展阶段的语言稳定性不够,它的特性和数据结构会因为版本升级等原因产生很大变化,并且部分变化不能完全保持向前兼容。在这个过程中 FreeWheel 的总结是:不管 Go 怎样实现,都要对系统并发做很好的支持,在应用层面做保护和控制,这样才保证这个系统能够正常的运行。

基于 Go 创建微服务的例子

如上文所说,Go 在 FreeWheel 的第一个试水项目是 IVI(Instant Video Ingestion) 系统。这个系统的主要功能是为了能够接受客户的海量视频资源元信息的插入,并完成后续的一些处理任务。于是,FreeWheel 主要以 Service API 提供相应的服务。该服务的第一版使用 Ruby 语言实现,但因为 Ruby 对并发的控制相对复杂,因此后续的性能、响应时间和吞吐率都不足以支撑整个公司不断发展的业务量。此后,FreeWheel 将 Ruby 替换为 Go,用 Go 的 gRPC 去实现了新的 API(当然也支持 RESTful API)。

正因为 API 架构需要足够灵活地支持未来的业务集成,所以它也成为其自身开发微服务的一个很好的例子。在设计和实现过程中,FreeWheel 把该架构分解成很多基础模块,比如流量控制、用户验证等,可以很灵活的把这些模块根据定制化的需求拼装在一起,提供适应市场和客户发展需求的真正价值。通过清晰的接口定义,Go 语言可以很好地把整个系统串联在一起。

此外,因为 Go 对容器及虚拟化技术有一些天然的支持,FreeWheel 的 API 团队也正在加速采用这种架构,他们准备将 API 都封装在 Docker 的 image 镜像里,用 Kubernetes 把所有的系统都管理起来,用 ETCD 或其他类似软件来辅助服务的发现和管理。

为什么 FreeWheel 没有全部用系统重写 Go

从编程范式的角度来说, Go 语言是变革派,而不是改良派。对于 C++、Java 和 C# 等语言为代表的面向对象的思想体系,总体来说全球范围内许多公司对 Go 语言的态度更为保守,多数持有限吸收的观念(这可从下图中 Go 的热度分布情况看出)。

即使 FreeWheel 在实践中发现 Go 比其他类编程语言具有许多更为明显的优势,如在写并行上,相比 Python 这样的解释语言要高一个数量级;如前期由 Python 开发的很多轻量级 API,因为全局解释锁 GIL 的关系而面临着进程间通信带来额外开销,所以就把轻量级 API 迁移到 Go 上;又比如 Go 在并发上的优势,适合面向多用户同时上传、同时调用 API 的场景……但其内部团队也并没有用 Go 来重写全部系统。原因主要有两点:

第一,FreeWheel 有很多已有的算法实现,想全部切入到 Go 上会面临巨大的开销和成本;第二,相比 C 或者 C++,Go 在高性能方面还没有完全的证明自己。在 Web 服务器端,它目前也没有一个特别好的像 RoR、Django、或者 PHP 的流行框架。对于 FreeWheel 来说,整个广告服务器是不允许出现明显的 downgrade 情况(尤其是当 GC 时),所以对这种非常关键的系统,目前还不能完全用 Go 去写。所以刘昊植也认为,Go 在扩展使用场景层面可能还需要做一些较大变革。

此外,FreeWheel 基于 Go 的 Web 程序,目前使用的是 Gorilla 框架。但从 Martini、Revel、Gocraft/web 等几款主流框架的使用和评价上看(可以下图 github 上的数据做个参考),Go 社区还没有一款处于统治地位的 Web 框架。如果 Go 想把它的触角伸得更长,这可能是其未来发力的一个方向。

但总的来说,Go 在高并发、开发效率等特性上的优势,决定了 Go 在 FreeWheel 内的采用程度会越来越深。刘昊植说:“除了一些已有业务依赖于 Hadoop、Spark 这样的基础设施,对于新增的业务和功能,Go 语言会是我们的首选。”

另外,FreeWheel 希望其使用的编程语言是能够得到跨大陆、跨时区、受所有工程师共同认可的,所以 Go 或许会是其最好的选择。这个过程中,FreeWheel 也评估过很多其他语言如 Scala、Rust 等,但最终因为 Go 在学习成本、统一实践、社区规模等方面的优势而胜出。

最佳实践DevOps语言 & 开发架构文化 & 方法微服务