知乎社区核心业务Golang化实践(一)

 go教练   2019-07-11 14:16   151 人阅读  0 条评论

众所周知,知乎社区后端的主力编程语言是 Python。

随着知乎用户的迅速增长和业务复杂度的持续增加,核心业务的流量在过去一年内增长了好几倍,对应的服务端的压力也越来越大。随着业务发展,我们发现 Python 作为动态解释型语言,较低的运行效率和较高的后期维护成本带来的问题逐渐暴露出来:

运行效率较低。知乎目前机房机柜空间已经不足,按照目前的用户和流量增长速度,可预见将在短期内服务器资源告急(针对这一点,知乎正在由单机房架构升级为异地多活架构);

Python 过于灵活的语言特性,导致多人协作和项目维护成本较高。

受益于近些年开源社区的发展和容器等关键技术的普及,知乎的基础平台技术选型一直较为开放。在开放的标准之上,各个语言都有成熟的开源的中间件可供选择。这使得业务做选型时可以根据问题场景选择更合适的工具,语言也是一样。

基于此,为了解决资源占用问题和动态语言的维护成本问题,我们决定尝试使用静态语言对资源占用极高的核心业务进行重构。

为什么选择Golang

如上所述,知乎在后端技术选型上比较开放。在过去几年里,除了Python 作为主力语言开发,知乎内部也不乏 Java、Golang、NodeJS 和 Rust 等语言开发的项目。

 微信截图_20190711142719.png

通过ZAE(Zhihu App Engine) 新建一个应用时,提供了多门语言的支持。

Golang 是目前知乎内部讨论交流最活跃的编程语言之一,考虑到以下几点,我们决定尝试用Golang重构内部高并发量的核心业务

天然的并发优势,特别适合 IO 密集应用

知乎内部基础组件的 Golang 版生态比较完善

静态类型,多人协作开发和维护更加安全可靠

构建好后只需一个可执行文件即可,方便部署

学习成本低,且开发效率较 Python 没有明显降低

相比另一门也很优秀的待选语言—— Java,Golang 在知乎内部生态环境、部署的方便程度和工程师的兴趣上都更胜一筹,最终我们决定,选择 Golang 作为开发语言。

改造成果

截至目前,知乎社区 member(RPC,高峰 QPS 数十万)、评论(RPC + HTTP)、问答(RPC + HTTP)服务已经全部通过 Golang 重写。同时因为在 Golang 化过程中我们对 Golang 基础组件的进一步完善,目前一些新的业务在开发之初就直接选择了 Golang 来实现,Golang 已经成为知乎内部新项目技术选型的推荐语言之一。

相比改造前,目前得到改进的点有以下:

节约了超过 80% 的服务器资源。由于我们的部署系统采用蓝绿部署,所以之前占用服务器资源最高的几个业务会因为容器资源原因无法同时部署,需要排队依次部署。重构后,服务器资源得到优化,服务器资源问题得到了有效解决。

多人开发和项目维护成本大幅下降。想必大家维护大型 Python 项目都有经常需要里三层、外三层确认一个函数的参数类型和返回值。而 Golang 里,大家都面向接口定义,然后根据接口来实现,这使得编码过程更加安全,很多 Python 代码运行时才能发现的问题可以在编译时即可发现。

完善了内部 Golang 基础组件。前面提到,知乎内部基础组件的 Golang 版比较完善,这是我们选择 Golang 的前提之一。不过,在重构的过程中,我们发现仍有部分基础组件不够完善甚至缺少。所以,我们也完善和提供了不少基础组件,为之后其它项目的 Golang 化改造提供了便利。

过去10 个月问答服务的 CPU 核数占用变化趋势

 微信截图_20190711142731.png

实施过程

得益于知乎微服务化比较彻底,每个独立的微服务想要更换语言非常方便,我们可以方便地对单个业务进行改造,且几乎可以做到外部依赖方无感知。

知乎内部,每个独立的微服务有自己独立的各种资源,服务间是没有资源依赖的,全部通过 RPC 请求交互,每个对外提供服务(HTTP or RPC)的容器组,都通过独立的 HAProxy 地址代理对外提供服务。一个典型的微服务结构如下:

 微信截图_20190711142738.png

知乎内部一个典型的微服务组成,服务间没有资源依赖。

所以,我们的 Golang 化改造分为了以下几步:

Step1. 用 Golang 重构逻辑

首先,我们会新起一个微服务,通过 Golang 来重构业务逻辑,但是:

新服务对外暴露的协议(HTTP 、RPC 接口定义和返回数据)与之前保持一致(保持协议一致很重要,之后迁移依赖方会更方便)

新的服务没有自己的资源,使用待重构服务的资源:

 微信截图_20190711142753.png

新服务(下)使用待重构服务(上)的资源,短期内资源混用

Step2. 验证新逻辑正确性

当代码重构完成后,在将流量切换到新逻辑之前,我们会先验证新服务的正确性。

针对读接口,由于其是幂等的,多次调用没有副作用,所以当新版接口实现完成后,我们会在老服务收到请求的同时,起一个协程请求新服务,并对比新老服务的数据是否一致:

当请求到达老服务后,会立即启一个协程请求新的服务,与此同时老服务的主逻辑会正常执行。

当请求返回后,会比较老服务与新实现的服务返回数据是否相同,如果不同,会打点记录+ 日志记录。

工程师根据打点指标和日志,发现新实现逻辑的错误,改正后继续验证(其实这一步,我们也发现了不少原本 Python 实现的错误)。

 微信截图_20190711142810.png

服务请求两边数据,并对比结果,但返回老服务的结果。

而对于写接口,大部分并不是幂等的,所以针对写接口不能像上面这样验证。对于写接口,我们主要会通过以下手段保证新旧逻辑等价:

单元测试保证

开发者验证

QA 验证

Step3. 灰度放量

当一切验证通过之后,我们会开始按照百分比转发流量。

此时,请求依然会被代理到老的服务的容器组,但是老服务不再处理请求,而是转发请求到新服务中,并将新服务返回的数据直接返回。

之所以不直接从流量入口切换,是为了保证稳定性,在出现问题时可以迅速回滚。

 微信截图_20190711142832.png

服务请求Golang 实现

Step4. 切流量入口

当上一步的放量达到 100% 后,请求虽然依然会被代理到老的容器组,但返回的数据已经全部是新服务产生的。此时,我们可以把流量入口直接切换到新服务了。

 微信截图_20190711142849.png

请求直接打到新的服务,旧服务没有流量了

Step5. 下线老服务

到这里重构已经基本接近尾声了。不过新服务的资源还在老服务中,以及老的没有流量的服务其实还没有下线。

 微信截图_20190711142908.png

Goodbye,Python

到这里,直接把老服务的资源归属调整为新服务,并下线老服务即可。至此,重构完成。

以上就是今天给大家介绍的知乎社区核心业务Golang化实践(一),如果你还想了解更多关于golang的知识技巧,可以继续关注我们http://www.fastgolang.com

本文地址:http://fastgolang.com/96.html
版权声明:本文为原创文章,版权归 go教练 所有,欢迎分享本文,转载请保留出处!

 发表评论


表情

还没有留言,还不快点抢沙发?