内容简介:CC 是英语流利说懂你英语课程的内部项目代号,全称 Core Course (核心课程,重金打造;历时一年,一统江湖 :smile:)。经过各个团队通力合作,于 2016 年年初上线。上线后受到用户的一致好评,随之而来的是不断增长的流量,服务端面临巨大的压力和挑战。下面我们来一起回顾一下这个过程中我们遇到的问题以及我们是如何解决的,先从最老的 Ruby RPC 服务说起。CourseCenter RPC 服务是我们最早的懂你英语课程中心服务,使用 Ruby 开发。由于那个时候 gRPC 还不成熟,对 Rub
CC 是英语流利说懂你英语课程的内部项目代号,全称 Core Course (核心课程,重金打造;历时一年,一统江湖 :smile:)。经过各个团队通力合作,于 2016 年年初上线。上线后受到用户的一致好评,随之而来的是不断增长的流量,服务端面临巨大的压力和挑战。下面我们来一起回顾一下这个过程中我们遇到的问题以及我们是如何解决的,先从最老的 Ruby RPC 服务说起。
CourseCenter RPC
CourseCenter RPC 服务是我们最早的懂你英语课程中心服务,使用 Ruby 开发。由于那个时候 gRPC 还不成熟,对 Ruby 的支持更是糟糕,RPC 框架我们选择基于 ruby-profobuf/profobuf 做了自己的 Ruby RPC 框架 Scorix RPC (https://github.com/scorix/protobuf-rpc-register)。在这套框架上,接入层可以继续写出很 Ruby style 的代码,比如通过 RPC 查找一个课程:
client.lesson.find_by(lesson_id: "laix")
对于用 Ruby 写过 gRPC client 的同学,一定觉得这样很 magic。 CourseCenter 在线上跑了一年多,很好的满足了日常开发迭代需求,但是也遇到了一些问题:
对于使用其他语言开发的项目接入支持不好
由于这套框架的 client 只做了 Ruby 版本,其他语言接入需要开发相应的 client。加上实现很大程度受到了 Rails 社区 convention over configuration 思想的影响,需要使用其他语言开发的同学去熟悉、适应这套规范,成本比较高。
性能存在问题
ruby-protobuf 是 Google 的 Protocol Buffers 纯 Ruby 实现,在 proto 的 marshal / unmarshal 上都有比较严重的内存泄露问题,相较于 Google 官方使用 C 实现的版本,有着较大的性能差距。时不时的报警,也是搞的工程师们很头秃。
随着付费用户数量的增长,以上问题被不断放大,为了保证良好的用户体验,我们开始着手重写,make CC performance great again。
Hexley gRPC
技术栈的选择
这次我们选择了 Go + gRPC,项目代号 hexley (Darwin 的吉祥物)。在我们开始重写的时候,gRPC 已经发布了 1.0 稳定版本,整个社区生态也已经发展的比较好。选择 Go 的原因就很简单了,性能好、易上手、部署简单便捷,以及大家对 Go 的喜爱 (errrrrr……)。选好技术栈之后,就撸起袖子加油干,重写就此开始。下面说说重写过程中我们遇到的一些问题:
依赖管理的选择
从 Ruby 到 Go 的过程中,第一个不适应的就是依赖管理这块。Ruby 社区通常都会使用 Bundler,暂时也没有遇到什么问题。Go 在这块的选择就比较多了,不过每个又都不是很成熟,在对比之后我们选择了 Bazel。关于 Bazel 就不做过多介绍,有兴趣的同学可以看我们的工程师 yuan 在 Gopher China 2018 上分享的 slides Bazel build Go。
项目结构的组织
Ruby 社区由于 Rails 的存在,项目结构组织都会按照 Rails 的结构来,自己写一个 gem 的话也会有一套比较通用的组织规范。Go 在这块就显得比较自由一些,带来的问题是每个人的喜好不同,组织的方式千奇百怪。因此在开发初期,我们花了较多时间讨论这个,也参考了一些开源项目。目前整体是一个按照业务模块去分 package 的逻辑,避免写诸如 controllers / utils 这类含义不明确的 package。
代码测试
Ruby 社区有 minitest / Rspec 等很成熟的测试框架,可以帮助我们很方便的做测试数据的 setup / cleanup / mock。Go 的 testing package 也提供了一些测试基础需要的基础功能,但是在上述的三个点上还是不太能完全满足我们的需求。在调研之后,我们引入了以下 package:
-
golang/mock 使用 mockgen 来生成 proto 的 mock 代码
-
stretchr/testify 提供了 suite 功能来做数据 setup / cleanup 工作
对于外部依赖不多的项目,使用 Go 自己的 TestMain 也能很好的满足需求。
ORM 的选择
ORM 我们最初选择了 go-xorm/xorm,对读写分离这些都有较好的支持,使用前一定要认真读文档,不然很容易被一些 Go 中的 0 值的 case 坑。在使用的过程中由于我们需要接入 OpenCensus,这个依赖了 Go 中的 context 来传递 trace id 信息,而 xorm 目前对 context 没有支持,因此我们 fork 出了 lingochamp/xorm 来增加对 context 的支持,从而更好的做 tracing,也欢迎大家多多提 PR :smile:。
灰度切换
作为一个线上重要服务,直接全量切到新服务存在很大的风险,一旦出现数据上的问题,将会带来很大的影响。这个时候需要一些必要的灰度策略,我们主要做了如下策略:
-
支持指定某些用户灰度到新服务
-
支持指定百分比用户灰度到新服务
通过以上两个策略的组合,我们可以先在内部测试完善后,再逐步按比例切线上用户的流量。
监控
线上服务能够第一时间收集到各种数据指标 (latency mean / 95 / 99、错误率等) 是很重要的,Ruby 由于语言的特性,可以用一个 gem 就在代码运行的各个环节加上 hook 来收集到各种数据指标。Go 需要自己做更多的一些工作,主要通过 gRPC interceptor + context
来完成,我们主要用了以下 工具 或组件:
-
prometheus
,用于收集metrics
信息,如响应时间、错误率等,后续的报警也是通过这个来做的 -
Sentry
,用于上报具体错误信息堆栈,当线上有接口报错的时候,我们可以通过它来很好的定位问题 -
OpenCensus
,全链路追踪,对于我们定位服务中的性能问题有很大的帮助
其他的改动
在老的服务里我们是把课程内容放在数据库里,每次去数据库里读。这部分数据的变更并不频繁,虽然读 DB 速度也不慢,但是还是有不必要的 I/O。重写后我们采用了新的方案,将内容上传到 S3 上,服务通过将 S3 的内容加载到内存中来达到大幅提升数据查询的速度。数据的更新只需要另外启动一个 goroutine 去监控 S3 的内容变化即可。
上线后
服务性能、稳定性都有极大的提升,工程师们再也不用为了线上问题头秃了。目前服务在晚高峰期间大概有 17K+ 的 QPS,相比之前老服务流量又翻了至少 3 倍,但是使用的机器数量却大大降低。性能方面 latency mean 可以稳定在 5ms 以内,95 线也可以稳定在 25ms 以内,之前 proto marshal / unmarshal 内存泄漏的问题也不复存在。
对 Go 的感受
在重写的过程中,也开始对 Go 有一些新的认识。开始会抱怨这个语言奇怪, if err 写的很烦躁,重复代码很多等,到去熟悉这个语言的设计理念,尝试用 compose interfaces 的方式去更好的抽象业务逻辑代码。举个例子,我们代码中有一段课程选题的逻辑,由于课程类型比较多,每一个课程选题的逻辑又不太一样,但是选题之后的逻辑其实是一样的。开始的 naive 方式就是每个课程都写一遍,重复的逻辑复制粘贴即可。在了解 Go 的更多理念之后,发现这里可以用 interface 来抽象:
// 选题逻辑的 interface type LessonSelector interface { // 定义选题以及通用 } // 实现选题部分的通用逻辑 type Common struct {} // A 课程, 只需要实现独有的逻辑即可 type A struct { *Common }
类似的例子还有很多,Go 也可以像 Ruby 一样写的优雅,期待 Go 2.0 以及 Ruby 3 :grin:。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。