写点什么

重构了后端服务,我学到了这些东西

  • 2019-02-18
  • 本文字数:3627 字

    阅读完需:约 12 分钟

重构了后端服务,我学到了这些东西

我是 Kurio(来自印度尼西亚的一款新闻聚合器)的软件工程师。Kurio 是一款聚合器应用程序,我们的主要工作是:收集发布合作伙伴网站上的新闻或文章,并通过我们的应用程序将其提供给用户。


与其他新闻聚合器一样,我们为用户提供了多种新闻内容,例如按我们的 top_stories 逻辑进行排序的新闻、按照趋势进行分类的新闻以及来自特定发布商的新闻。



移动端的 Kurio 新闻布局


Feed 的构建过程由我们的 Feed 服务负责处理。


这个服务是 Kurio 的三大主要项目之一,之前的版本已经运行了很长一段时间。因此,它变得非常复杂,有时也会难以理解。这也使得添加新功能变得非常困难。因此,我们决定重建我们的 Feed 服务。希望通过这个新版本的 Feed 服务,我们可以轻松添加新功能或者使其更易于维护。


在这个新项目中,我们创建了新的架构,并混合了旧架构,具备了动态和灵活性。我们知道,新闻源可以是任意类型的对象,比如文章、视频、音频等等。使用 Go 语言实现这些真的很有挑战性,因为 Go 语言是一种静态类型的编程语言,它没有 Java 或其他编程语言的泛型类型。

了解流程

首先我们需要了解以前的系统是如何工作的。从编译、测试和部署开始,直到收到用户请求,我们需要知道整个过程的工作原理。


因为这是一个核心的服务,而我刚刚来这里一年,我真的不知道它是如何运作的,尤其是多年来整个系统添加了很多额外的功能和补丁,很难通过阅读代码来了解它。所以,我们需要了解流程和规则,然后基于这些流程和规则构建新的流程和规则。


例如,当用户打开应用程序时,会得到由这项服务提供的 top_stories 新闻源。或者是一些规则,例如:在 top_stories 新闻源中向用户显示的内容是有限制的。或者类似于:不要向用户展示他们不关注的主题,或者根据用户的属性(性别、年龄等)显示新闻。


列出这些规则和流程是一件简单的事情,难就难在如何将其转换为代码。一般来说,我们的流程非常简单,如下所示。



用户获取新闻的流程


基本上主要是两个大功能,获取个性化新闻和获取默认新闻。最难的是获取个性化新闻,因为我们必须将它与个性化引擎相结合。此外,我们必须遵循一些与上面提到的个性化内容相关的规则(提取用户兴趣和属性,然后根据用户的兴趣构建新闻源)。

设计和讨论

我们之所以要重构这个服务,是因为当我们要添加新功能时,之前系统的代码架构无法很好地扩展。如果要在未来开发新功能会非常痛苦,因为我们不得不重构很多东西。


所以我们真正需要的是修复架构。设计一个新的架构真的很难。我们需要问自己很多问题,比如:“这样做会怎样?”、“为什么要这样?”、“为什么不是这样?”我们希望新架构能够解决“未来”的问题,并提供向后兼容性。为此,我们进行了大约一个月的讨论,针对每个大功能进行了技术栈和流程方面的讨论。


最终,我们决定尝试一些函数式的开发方式。我们放弃了之前使用的代码架构,发明了一种新的代码架构,带有函数式编程(使用高阶函数模式)的味道,但又不像 Lisp 或 Clojure 那么动态。


因此,在我们的代码中可以找到很多 HOF(高阶函数)模式,如下所示:


func something(params, func(params)) (func(params)){}
复制代码


但因为我们使用的是 Go 语言,一种静态类型的编程语言,所以当创建了很多函数时就会有很多痛点,必须进行大量的类型检查和转换,而这耗费了大量时间。


因此,我们意识到 Go 语言不适合用来解决我们的问题,但在我们这 10 个后端工程师当中,只有一个人了解 Clojure(函数式编程),而学习新的编程语言就意味着我们需要额外的时间。经过长时间的讨论,我们决定继续使用 Go 语言,不仅是因为我们所有的后端工程师都很了解 Go 语言,也是因为 Go 语言已经在很多微服务中得到验证。

了解基础

在将流程和设计转换为代码时,我意识到我们必须对基础有一个真正的了解。一开始,我并没有真正理解高阶函数的工作原理。在阅读代码时感到很困惑,怎么总是一个函数接收一个函数作为参数然后再返回一个函数呢?不过要感谢谷歌,我现在终于明白了。


我们还需要了解 Go 语言本身的基础知识,比如使用指针作为函数接收器、DateTime 的基础知识,以及很多其他基础的东西。如果我们对这些东西不了解,只会增加完成这个项目的时间。

先运行,后优化

  1. 优化的第一条规则——不要优化

  2. 优化的第二条规则——还不到优化的时候

  3. 优化前先分析


因此,在开发这个服务时,我们的第一个目标是确保至少可以运行它。我们没有去考虑性能问题,并试图忽略任何有关优化的事情,例如使用 Go 例程。


在开发完代码后,我们就可以编译并运行它,所有请求都能被正常处理,响应也很正常。当然,初始版本速度非常慢。与之前的系统相比,它慢了十倍。以前的系统在使用 staging 服务器时单个 API 请求大约需要 500 毫秒,而新版本需要 50000 毫秒(约 50 秒)或更久。


优化代码也是我们最重要的任务之一。为了优化我们的代码,我们遵循了以下步骤:


  1. 找出需要长时间处理的循环代码,将其转换为使用 Go 例程,提高并行性或使用管道。

  2. 分析系统并检测所有速度慢的功能,对其进行优化。所幸的是,在 Go 语言中进行分析很容易。借助 pprof(https://blog.golang.org/profiling-go-programs)工具,我们可以对系统进行分析并检测所有速度慢的功能。我们甚至可以检测出我们所使用的哪个库最慢,这样我们就可以使用具有类似功能的另一个库替换它们。

  3. 如果有必要,增加缓存。


构建服务时,我们的规则是只在确实需要使用缓存的情况下使用缓存。缓存就像一种药物,它会让我们上瘾,因为当我们的系统看起来很慢时,会把缓存看成是解决问题的灵丹妙药。通常,在开发大型并发项目时沉迷于使用“缓存”的人,首先想到的是“缓存”,而不是先考虑优化(基准测试、分析)功能(逻辑/算法)。


对于我们的情况,我们通过两种方式来使用缓存:


  • 去重管理:因为新闻源可能是来自很多存储库(数据库和服务)的内容(文章、新闻)列表,所以内容可能会重复。因此,我们将缓存作为临时存储来处理重复数据。

  • 存储库缓存:因为新闻源可能是来自很多存储库(数据库和服务)的内容(文章、新闻)列表,多个用户有可能请求相同的内容。因此,为了避免从存储库中获取相同的内容,我们缓存了存储库结果。


通过这种优化,我们至少可以像在以前的系统中那样改进新系统的性能(staging 服务器的响应时间约为 400 毫秒,生产服务器的响应时间约为 180 毫秒)。

小心地做出变更

基于语义版本控制,在不添加新特性和不破坏 API 的情况下进行重新构建就不算是一个新的版本。基本上,在这个新重建的系统中,我们的目标是改变架构,而不是 API 规范。因此,无论我们在系统中进行做出哪些变更,都不能更改 API。因为即使是非常微小的变化也会影响到所有相关的服务。


为了让它成为一个新版本,我们对错误响应消息正文进行了一些修改。


原始错误响应消息正文:


{  "error": "Error Message"}
复制代码


新版本的错误响应消息正文:


{   "error": {      "message": "Error Message",      "errors": [          // any stack-trace errors        ]    }}
复制代码


因为进行了这些变更,我们还需要处理其他使用了我们 API 服务的相关服务。所幸的是,只有两种服务使用了我们的 API 服务,所以我们只需要更新两个应用程序:仪表盘应用程序和移动网关 API。此外,因为只有响应错误发生了重大变更,所以只需要修改应用程序的一小部分即可。

永远不要忽略了测试

在重新构建这个服务时,我们至少进行了三次测试,然后才发布到生产环境中:单元测试、集成测试和负载测试。


在所有这些类型的测试中,单元测试是最小的测试。有些人似乎低估了单元测试的重要性,因为它只是一个单元,一个小功能。但是,在重建这个新服务时,我体会到了单元测试的重要性。


在 Sprint 开始时,我们忽略了单元测试,因为我们希望专注在代码架构的设计上。所以我们开发了一些没有任何测试的功能。我们这样做是因为我们仍然在构建一些实验性的代码架构,为了避免进行不必要的单元测试重构,我们在这个时候没有创建任何单元测试。


但是,在完成代码架构设计之后,我们忘了为在 Sprint 开始时创建的功能添加单元测试。直到我们将它部署到 staging 服务器并与另一个真实服务进行了集成测试。我们在应用程序中发现了很多错误。然后我们查看了源代码,发现我们的功能有很多条件都没有覆盖到。


知道了这个问题后,我们意识到我们还没有测试过这个功能。它还没有通过单元测试。如果我们从一开始就进行单元测试,那么修复这个问题并重新部署它就不需要做额外的工作。在进行单元测试时,我们可以考虑很多不同情况,并在部署应用程序之前修复它们。

结论

虽然我们做的是幕后工作,并且对我们的用户没有可见的影响,但我们确实学到了很多东西。我学到了很多关于如何从头开始构建高并发系统的知识。完成这项任务后,我知道了为什么当我们在面试后端开发职位时,总会被问及逻辑和算法问题。这是因为在构建高并发系统时,性能是非常重要的方面,任何算法都会影响到系统的响应时间。


英文原文:https://dzone.com/articles/we-rebuilt-our-backend-feed-service-here-what-i-le


2019-02-18 07:303869
用户头像

发布了 731 篇内容, 共 448.6 次阅读, 收获喜欢 2002 次。

关注

评论

发布
暂无评论
发现更多内容

从零开始学习3D可视化之摄像机

ThingJS数字孪生引擎

大前端 可视化 数字孪生

我看 JAVA 之 并发编程【一】FutureTask & Callable

awen

Java 多线程 Callable FutureTask

Demo

Command

#架构实战营

打造中国数字军人 数军科技携黑科技亮相(北京)军博会

科技热闻

禾木之变:2021我们该如何持续拥抱AI?

脑极体

区块链技术在“三资”监管领域的应用

CECBC

pha挖矿系统源码开发

获客I3O6O643Z97

区块链+ PHA矿机挖矿 PHA质押挖矿

Ubuntu Server 20.04搭建zookeeper集群

玏佾

zookeeper 群集安装 搭建 zk 集群部署

你的直观感受有可能是错的

石云升

学习 认知偏差 7月日更

5分钟速读之Rust权威指南(三十九)unsafe

wzx

rust

深入浅出 Gitalk 留言插件

悟空聊架构

开源 网站 7月日更 网站建设 留言

Linkflow CDP亮相GDMS全球数字营销峰会

Linkflow

CDP 用户画像 数字营销

面对大规模 K8s 集群,这款诊断利器必须要“粉一波”!

尔达Erda

开源 云原生 operator PaaS kubernete

边界防御·信息安全保密圈的 “丈八蛇矛”

郑州埃文科技

第一周作业-对比不同公司产品招聘JD

小夏

产品经理训练营 邱岳

《面试八股文》之kafka21卷

moon聊技术

kafka 面试

隔壁工程师都馋哭了我的逆向工程IDA,说要给我搓背捏脚

网络安全学海

网络安全 信息安全 渗透测试 漏洞分析 逆向工程

前端 JavaScript 实现一个简易计算器

编程三昧

JavaScript 大前端 代码实现

生命科学领域新工具:北鲲云超算平台,梦启航的地方

北鲲云

爱奇艺奇秀直播的秒播体验优化实践

爱奇艺技术产品团队

直播 优化

自建开发工具系列-Webkit内存动量监控UI(一)

Tim

FrontEnd 调试工具 Webkit 工具UI

《持之以恒的从事运动》五

Changing Lin

7月日更

如何科学地系统地梳理出CDP的RFP?

Linkflow

架构实战营模块8作业

Geek_649372

架构实战营

推荐系统的价值观(三十二)

Databri_AI

价值观 推荐系统

模块八 - 设计消息队列存储消息数据的 MySQL 表格

华仔架构训练营

详聊微服务观测|从监控到可观测性,我们最终要走向哪里?

尔达Erda

开源 微服务 云原生 APM PaaS

浅谈云上攻防——Web应用托管服务中的元数据安全隐患

腾讯安全云鼎实验室

安全攻防 云安全 元数据 网络攻防

百度程序员推荐的书籍,今天免费送!

百度Geek说

为什么公司应该效仿开源的文化

WorkPlus

讨论 | 低代码能解决制造业企业数字化转型所面临的问题吗?

优秀

低代码

重构了后端服务,我学到了这些东西_语言 & 开发_Iman Tumorang_InfoQ精选文章