进行产品开发:一个项目历史



大家好! 我叫Maxim Ryndin,我是Gett的两个团队的负责人-Billing和Infrastructure。 我想谈谈产品Web开发,我们在Gett主要使用Go。 我将告诉您在2015-2017年间我们是如何使用该语言的,为什么要选择它,在过渡期间遇到什么问题以及找到了什么解决方案。 在下一篇文章中,我将告诉您当前的情况。

对于不认识的人:Gett是一家国际出租车服务,于2011年在以色列成立。 盖特现已在四个国家/地区派驻代表:以色列,英国,俄罗斯和美国。 我们公司的主要产品是面向客户和驾驶员的移动应用程序,面向企业客户的门户网站(可在此订购汽车)以及一堆内部管理面板,我们的员工可通过这些面板设置收费计划,连接新驾驶员,监控欺诈案件等。 2016年底,全球研发办事处在莫斯科成立,这符合整个公司的利益。

我们如何去


在2011年,该公司的主要产品是单片Ruby on Rails应用程序,因为当时该框架非常流行。 在Ruby on Rails上快速开发并启动了许多成功的业务示例,因此它与业务的成功息息相关。 该公司正在发展,新的驱动程序和用户来找我们,负载增加了。 最初的问题开始出现。

为了使客户端应用程序显示汽车的位置并使其运动看起来像一条平滑的曲线,驾驶员必须经常发送其坐标。 因此,负责从驾驶员处接收坐标的端点几乎总是负载最重。 Ruby on Rails中的Web服务器框架对此做得很差。 可能仅大规模扩展,添加新的应用程序服务器,这既昂贵又效率低下。 结果,我们在单独的服务中取出了坐标的功能集合,该服务最初是用JS编写的。 有一阵子,这解决了问题。 但是,随着负载的增加,当我们达到8万RPM时,Node.js上的服务就停止了为我们节省费用。

然后我们宣布了黑客马拉松。 公司所有员工有一天的机会编写原型,以收集驾驶员的坐标。 这是该服务的两个版本的基准:在prod上运行并在Go上重写。


在几乎所有方面,Go上的服务均显示出最佳效果。 Node.js上的服务使用了群集,这是一种使用计算机所有内核的技术。 也就是说,实验是一个正负的诚实。 尽管Node.js具有单线程运行时的缺点,但对结果没有影响。

逐渐地,我们对产品的需求不断增长。 我们开发了越来越多的功能,一旦遇到这样的问题:当您在某个位置添加一些代码时,在项目紧密连接的另一位置可能会中断。 我们决定通过切换到面向服务的体系结构来克服这一祸害。 但是结果是性能下降了:当在Ruby on Rails解释器中执行代码时遇到运行时查询时,它被阻塞并且工作程序处于空闲状态。 而且网络I / O操作已经变得越来越多。

因此,我们决定采用Go作为主要的开发语言之一。

我们产品开发的特点


首先,我们有非常不同的产品要求。 由于我们的汽车在三个国家/地区使用完全不同的法律驾驶,因此有必要实现非常不同的功能集。 例如,在以色列,法律要求出租车计价器考虑旅行的费用-这是一种每几年通过强制性认证的设备。 驾驶员开始旅行时,他按“开始”按钮,完成时,他按“停止”按钮,并将计价器显示的价格输入到应用程序中。

俄罗斯没有如此严格的法律。 在这里,我们可以自己配置定价策略。 例如,将其与行程的持续时间或距离相关。 有时,当我们想要实现相同的功能时,我们首先在一个国家/地区推出该产品,然后在其他国家/地区进行适应和推广。

我们的产品经理以产品故事的形式设定要求,我们尝试坚持这种方法。 这自动在测试上留下了印记:我们使用行为驱动的开发方法,以便可以将传入的产品需求投影到测试情况上。 远离编程的人们更容易阅读测试结果并了解什么。

我们还希望摆脱某些工作的重复。 毕竟,如果我们有一个实现某种功能的服务,并且需要编写第二个服务,重新解决我们在第一个中解决的所有问题,然后再与监视和迁移工具集成,那么这将是无效的。

我们解决问题


构架


Ruby on Rails建立在MVC架构上。 在过渡时,我们真的不想放弃它,以使那些只能使用此框架进行编程的开发人员的工作更加轻松。 改变工具并没有增加舒适感,如果您还改变了应用程序的体系结构,就相当于推了一个不懂得如何从船上游泳的人。 我们不想以这种方式伤害开发人员,因此我们采用了当时称为Beego的少数MVC框架之一

与Ruby on Rails中一样,我们尝试使用Beego进行服务器端渲染。 但是,在服务器上呈现的页面并不能真正取悦我们。 我不得不扔掉一个组件,今天Beego仅从后端生成JSON,并且所有渲染都由前端的React执行。

Beego允许您自动构建项目。 对于某些开发人员而言,从脚本语言转换为编译需求非常困难。 当一个人实现某种功能时,会有一些有趣的故事,只有通过代码审查,甚至偶然发现,您才需要执行Go-build。 并且任务已经关闭。

在Beego中,路由器是根据注释生成的,开发人员在注释中写出了控制器动作的路径。 对于这种想法,我们持模棱两可的态度,因为例如,如果打错了路由器的打字机,那么那些不熟悉此方法的人就很难发现错误。 即使经过几个小时的激动人心的调试,人们有时仍无法弄清原因。

资料库


我们使用PostgreSQL作为数据库。 有这样一种做法-从应用程序代码控制数据库架构。 之所以方便,有几个原因:每个人都知道它们; 它们易于部署,数据库始终与代码保持同步。 我们还想保留这些面包。

当您有多个项目和团队时,有时要实现功能,就必须爬入其他人的项目。 在表中添加一列非常诱人,其中可能会出现1000万条记录。 一个没有沉浸在这个项目中的人可能不会意识到桌子的大小。 为防止这种情况,我们发出了有关危险迁移的警告,该警告可能会阻止数据库进行记录,并为开发人员提供了删除此警告的方法。

迁移


我们决定使用Swan进行迁移Swan是一只斑驳的鹅 ,在其中进行了一些改进。 像许多迁移工具一样,这两种方法都希望在一个事务中完成所有操作,以便在出现问题时可以轻松回滚。 有时,您需要建立索引,并且表已锁定。 PostgreSQL有一个concurrently参数可以避免这种情况。 问题是,如果在PostgreSQL中您开始concurrently建立索引,即使在事务中,也会弹出错误。 首先,我们希望添加一个标志,以免打开交易记录。 最后,他们这样做:

 COMMIT; CREATE INDEX CONCURRENTLY huge_index ON huge_table (column_one, column_two); BEGIN; 

现在,当有人使用concurrently参数添加索引时,他将获得此提示。 请注意, commitbegin不会混淆。 此代码关闭迁移工具打开的事务,然后使用concurrently参数滚动索引,然后打开另一个事务,以便该工具关闭某些内容。

测试中


我们尝试坚持以行为为导向的发展。 在Go中,可以使用Ginkgo工具完成此操作。 很好,因为它具有BDD,“ describe”,“ when”等常用关键字,并且还允许您简单地将产品经理编写的文本投影到存储在源代码中的测试情况下。 但是我们面临一个问题:来自Ruby on Rails领域的人们认为,在任何编程语言中,都有类似于工厂女孩的东西-一个用于创建初始条件的工厂。 但是,Go中没有这样的东西。 最后,我们决定不重新发明轮子:就在每次测试之前,在测试之前和之后的挂钩中,我们用必要的数据填充数据库,然后对其进行清理,以免产生副作用。

监控方式


如果您拥有人们正在访问的生产服务,那么您需要跟踪其工作:是否有五百个错误或请求正在快速处理。 在Ruby on Rails的世界中,NewRelic经常用于此目的,并且我们的许多开发人员对此都有很好的了解。 他们了解了该工具的工作原理,以及在出现问题时应该去哪里查看。 NewRelic允许您通过HTTP分析请求的处理时间,识别缓慢的外部调用和对数据库的请求,监视数据流,提供智能的错误分析和警报。

NewRelic具有Apdex聚合函数,该函数取决于答案持续时间分布的直方图以及一些您认为是正常且一开始就设置的值。 此功能还取决于应用程序中的错误级别。 NewRelic计算Apdex并在其值低于某个级别时发出警告。
NewRelic最近还擅长拥有正式的Go代理。 常规监视概述如下所示:



左侧是查询处理图,每个处理图均分为多个部分。 细分包括请求排队,中间件处理,在Ruby on Rails解释器中的停留时间以及对存储库的访问。

Apdex图表显示在右上方。 右下角-处理请求的频率。

有趣的是,在Ruby on Rails中,要连接NewRelic,您需要添加一行代码并将凭据添加到配置中。 一切都神奇地起作用。 这是可能的,因为在Ruby on Rails中有猴子补丁,而在Go中则没有,因此有很多手动操作。

首先,我们要衡量请求处理的持续时间。 这是使用Beego提供的挂钩完成的。

 beego.InsertFilter("*", beego.BeforeRouter, StartTransaction, false) beego.InsertFilter("*", beego.AfterExec, NameTransaction, false) beego.InsertFilter("*", beego.FinishRouter, EndTransaction, false) 

唯一不重要的一点是,我们共享了交易的开始及其命名。 我们为什么要这样做? 我想考虑到路由所花费的时间来衡量请求处理的持续时间。 同时,我们需要按请求所到达的端点聚合的报告。 但是在开始交易时,我们还没有定义匹配的URL模式。 因此,当请求到达时,我们打开一个事务,然后在挂接上执行控制器后,对其进行命名,并在处理后将其关闭。 因此,今天我们的报告如下:


我们使用了一个称为GORM的ORM,因为我们想要保留抽象,而不是强迫开发人员编写纯SQL。 这种方法既有优点也有缺点。 在Ruby on Rails的世界中,有一个ORM Active Record确实使人们宠坏了。 开发人员忘记了您可以编写纯SQL,并且只能通过ORM调用进行操作。

 db.Callback().Create().Before("gorm:begin_transaction"). Register("newrelicStart", startSegment) db.Callback().Create().After("gorm:commit_or_rollback_transaction"). Register("newrelicStop", endSegment) 

要在使用GORM时测量数据库中查询执行的持续时间,需要使用db对象。 回调表示我们要注册一个回调。 创建新实体时应调用它-调用Create 。 然后,我们确切指示何时启动回调。 Before使用gorm参数负责此gormbegin_transaction是打开事务时的某个时间点。 接下来,使用名称newrelicStart注册一个startSegment函数,该函数简单地调用Go代理并打开一个用于访问数据库的新段。

ORM将在我们打开交易并由此打开段之前调用此函数。 我们必须执行相同操作以关闭该段:只需挂起Callback。

除了PostgreSQL,我们还使用Redis,这也不顺利。 对于此监视,我们在标准客户端上编写了一个包装器,并为调用外部服务做了同样的事情。 这是发生了什么:



这就是用Go编写的应用程序的监视效果。 左侧是有关查询处理持续时间的报告,包括以下部分:在Go中执行代码本身,访问PostgreSQL和副本数据库。 该图上没有显示对外部服务的调用,因为它们的调用很少,并且在平均时根本不可见。 我们还提供有关Apdex和请求处理频率的信息。 通常,监视结果非常有用,可以使用。

至于数据流,多亏了HTTP客户端上的包装程序,我们可以跟踪对外部服务的请求。 此处显示了促销服务请求方案:它指的是我们的其他四个服务和两个存储库。



结论


今天,我们有超过75%的生产服务使用Go编写,我们不使用Ruby进行积极的开发,而仅支持它。 在这方面,我要注意:

  • 对发展速度会降低的担忧尚未得到证实。 程序员分别以自己的模式投入了新技术,但是平均而言,经过两周的积极工作,Go上的开发变得像Ruby on Rails一样可预测且快速。
  • 与过去的经验相比,Go应用程序在负载下的性能令人惊讶。 我们大大节省了AWS中基础架构的使用,从而大大减少了使用实例的数量。
  • 技术的变化极大地鼓励了程序员,这是成功项目的重要组成部分。
  • 今天,我们已经离开了Beego和Gorm,在下一篇文章中将对此进行更多介绍。

总而言之,我想说的是,如果您不使用Go语言编写代码,则会遇到工作量大且交通繁忙的问题,请使用这种语言。 只是不要忘记与企业进行谈判。

Source: https://habr.com/ru/post/zh-CN463097/


All Articles