马蜂窝容器化平台前端赋能实践

48

容器对前端开发真的有用吗?答案是肯定的。

最初当我向公司的前端同学「安利」容器技术的时候,很多人都会说:「容器?这不是用在后端的技术吗?我不懂啊,而且前端开发用不上吧。」

但其实,今天我们讨论的「前端」已经不是传统意义上的「前端」, 首先体现在终端类型的多样性,比如 iOS,Android,小程序等;另外,伴随着 Node.js 等技术的兴起,前端开发的边界也在逐渐服务端延伸。来到大前端时代,如何以工程化、服务化和自动化的方式来进行应用开发,实现业务的持续迭代、高可用、高并发是每一个成功的互联网产品不断探索的事情,而渐为成熟的容器技术大大提高了这个过程的效率。

本文将结合马蜂窝容器化平台赋能前端应用构建的实践经验,介绍整个平台背后的设计和实现原理,取得的一些效果及问题的优化方案。

容器与前端的结合点

一般来说前端的开发流程是这样的:创建服务/项目 → 本地开发 → 开发环境测试 → 生产环境测试  → 生产灰度 → 上线。

基于容器化平台进行前端开发的优势在于,前端和后端完全分离,我们只需要关注前端的项目构建,而不需要和后端代码一起打包。每个构建版本及每个访问规则也都是独立的,一个版本构建失败并不影响其他版本的构建及访问。

那么,容器和前端的结合点在哪里?容器的优势在前端应用研发的哪个环节发生作用?我们可以从开发、测试、生产这三个阶段分别来看。

 开发环节

容器消除了线上线下的环境差异,保证了应用生命周期的环境一致性标准化。而对于前端开发来说,要完成的任务往往是完成内容的呈现和响应用户的输入,处理的是 HTML、JS、CSS 等静态资源,文件直接发送到客户端,不需要一个运行环境,这里好像用不上容器。

那 Build 的时候呢?毕竟不同的项目是用不同的 Node 版本在做构建,不同的容器可以进入不同的 Node 版本,这样就不会污染本机的 Node 环境。但其实没有容器,前端还可以用 NVM 去管理 Node 版本,切换起来很随意,也就是一两行命令就能搞定的事情。而且本地开发很方便,看起来真的没有必要用容器。

可以说,容器本身并没有帮助前端在开发阶段变得更加便利。因此如果对容器技术不熟悉,开发阶段没有必要非要用容器。

测试环节

过去我们用虚拟机进行测试的一个常见的方案是,前端研发把自己的代码上传到虚拟机的一个目录下,QA 可以直接通过域名进行测试。但问题是,公司有很多的产品线,可能会存在很多项目同时提测的情况。虚拟机对系统资源的消耗比较大,数量有限,并且难扩容,影响测试效率。

如果使用容器化平台就不会出现这方面的担忧。因为容器非常轻量,消耗低、启动快,可以迅速扩容,不用担心不够用的问题。

生产环节

容器的另一个优势是它可以实现应用程序的版本控制。比如我们在上线之后发现版本有问题需要回滚,这种情况不可避免,传统的做法是通过 Git 或者 SVN 回滚,一旦合入的代码想回退或者拆分就很难操作,而且重新部署也很耗时。

基于容器化的平台,我们可以直接通过流控,把流量切到旧的版本上去,几需要几秒钟的时间,回滚效率大大提升。

再如,前端性能的一个重要指标是页面加载时间,如果出现首页白屏是非常破坏用户体验的,特别是在做活动的时候,我们把几乎所有流量都引导到活动页,出现白屏会非常让人抓狂。找到运维排查之后发现有台服务器挂了,只能通过重启来解决。但是重启机器存在很多不确定性,有可能这台机器就起不来了,这种情况很常见。

但如果运行在容器化平台上,一个容器就是一个进程,一台机器如果宕机,集群会快速从另外一个节点把服务拉起,而且是秒级的,基本不用担心用户的访问会出现问题。

总结来看,容器与虚拟机相比主要的优势体现在可以实现快速扩容、秒级回滚和稳定保活。因此容器化对于前端开发来说,更重要的意义是能够保证服务的快速迭代,以及线上服务的稳定性。

前端需要了解的容器知识点

通过上面的介绍,相信大家已经对容器技术为前端开发带来了哪些变化有了一些感受。那么为了更好地应用这项技术,前端同学也应该掌握一些容器的基础知识。

 容器是什么

首先我们来看容器到底是什么,它为什么轻量、高性能。通过下面这张图片,我们可以将虚拟机和容器进行一个更加直观的对比:

虚拟机通过在物理服务器上层通过运行 Hypervisor 模拟硬件系统,来提升服务器的能力和容量。每个虚拟机中有一个内核,运行着不同的操作系统,启动之后会做进程管理、内存管理之类的事情。但对于前端应用的构建来说,可能只是需要一个 Nginx 做静态服务器,这种场景下使用虚拟机就太重了。

容器之所以轻量,是因为容器没有 Hypervisor 层和内核层,每个容器都共享宿主机的内核和系统调用。因此一个容器内包含的仅仅是一个程序运行所需要的最少文件,启动容器就是启动进程,对资源的开销更小,维护起来更简单。

镜像、容器和 Docker 

这是大家在聊到容器技术的时候经常会提到的三个词,下面来说下它们各自的概念以及之间的联系是什么。

镜像:可以简单理解为一层层文件系统的集合,或者说一些目录的集合。比如对于我们的前端代码,最下面那层目录可能是 Nginx 运行所需要的二进制,然后在上面再加一层目录是我们的代码,比如说 index.html。这个镜像分层所有的分层生成以后,都是只读的,每一层文件不可修改。

容器:其实就是在上面的目录上再加一层目录。但它其实是一个空目录,区别就在于容器最上面一层是可读可写的,也就是说容器 = 镜像 + 读写层。

比如我如果想修改之前的 index.html ,是通过把新的版本累加在之前的镜像上。也就是说生成容器以后,所有的变更都发生在顶层的镜像可写层,下面的这些层是不允许往里面写东西的,但是可以累加,就像堆积木一样,一直加上去,而原来的镜像不会被容器修改,这也是镜像可以被多个容器共享的原因。 

Docker:容器技术其实早就存在,Docker 是用来实现容器化技术的一种工具,也是目前业界最通用的一种方式,来帮我们制作镜像,然后把镜像运行成为容器并管理起来。

容器化平台如何为前端赋能

介绍完简单的概念,我们就和大家一起来看马蜂窝容器化平台的整体架构,我们是如何为前端赋能,以及赋予什么样的能力。

我们基于 Docker 和 Kubernetes 搭建了容器云平台,将应用的构建、部署、资源调度、应用管理等能力抽象出来,以服务的方式提供给研发人员,提升线上服务的稳定性和研发效率。下图从应用的角度出发,展示了前端应用在容器化平台的生命周期:

应用中心

应用是容器云平台的基本操作对象。云平台一个非常大的好处是屏蔽了项目的类型,不分前端或后端。于是在应用的外壳下,不管是前端的代码,还是后端的代码,都可以享受同样的服务。比如传统意义上应用在后端的限流、熔断、服务治理等能力一样可以赋予前端,使前端同学聚焦在业务开发上,而不需要关注底层的实现。

这是应用中心的一个创建页面,只需要几步,一个应用就可以创建完成,并且托管到我们的云平台上:

版本管理

创建完应用之后就要开始构建版本。通过使用容器,我们将应用程序、配置和依赖关系等打包成一个个代码镜像,然后去告诉线上服务器怎么让它们用容器化的方式运行起来。因此版本管理包含代码镜像和运行时配置两部分内容。

1. 代码镜像

我们使用基于 Pipeline + Docker 的 Drone 作为 CI 工具,它非常灵活,容易扩展。Drone 的灵活性体现在 Pipeline 的配置上,可以通过设置 .drone.yml 文件的方式在项目中控制构建镜像的过程。 

为了更好地支持公司级别的应用,我们向镜像注入一些内部经常用到的包来构建一个通用的基础镜像。在构建的同时会做一些 CI,比如单元测试、漏洞检测等。 

2. 运行时配置

运行时配置分成 Nginx 配置和部署运行时的配置两个部分

(1)Nginx 配置

Nginx 配置主要针对 Node 前端项目来说。将 Nginx 配置开放给应用有这么几点好处:

  • 前端同学可以自己去配置 history 模式,不需要再去找服务端来配合。
  • 自定义多个 location。在面对多页应用时,可以通过配置 Nginx 把请求转发到指定的入口文件,实现指定路由。
  • 自定义 cache 缓存策略。缓存策略选择更灵活,提升用户体验,降低服务器处理请求的压力。

(2)部署运行配置

部署运行配置是要告诉系统平台要如何运行版本包。这里其实也就为后续部署到 Kubernetes KVM 宿主机等多种平台留好了扩展。

总结来看,在版本管理的部分我们实现了以下几点能力:

  • 配置文件驱动,一个应用多份灵活好扩展
  • Nginx 配置等开放给应用,遵循 DevOps 思想,高效赋能
  • 标准化版本产物,一处构建,处处运行

部署管理

接下来我们需要把已经构建好的版本包部署到集群上去运行。

在线上可能会有许多台机器,V1、V2、V3 指的是各种版本。这个版本可以有多个实例。如果服务出现故障,我们主要通过两种方式来保证稳定高活:

  • 高效调度:通过 Kubernetes 调度器将指定运行的容器调度到资源满足要求、最合适的节点上去
  • 多副本支撑:自动部署一个容器应用的多份副本,并持续监控。如果容器挂掉自动启动副本

结合我们之前说到的主页白页的例子具体说明,我们会在容器化平台上持续看管容器,如果服务挂了,就在迅速在别的节点上启动起来。这里需要注意的是,「多份」不仅仅是说在两台机器上启动就叫多份,如果两台机器都在一个机柜上,甚至在一个机房里,那么启动多份也没有意义。

到这里,我们已经把服务部署到线上,并且实现稳定运行。但是完成部署,不代表用户就能访问,也不代表就能访问到正确的版本,所以接下来就到服务治理的环节。

服务治理

服务治理是一个比较大的概念,可以应用的场景也很多。它的其中一个内容是让用户访问到指定的一线上版本。

技术方案

首先介绍下实现原理:

我们采用的是一个 支持 xds 协议的网关。当新的配置通过 xds 协议推送给网关时,它就会自动进行热更新、热重启,然后去适应新的配置。比如说开始网关指向的是 V1 版本,如果我们现在希望指向 V2 版本,只需要把最新的配置通过 xds 协议推送给网关,它就会应用新的配置,通过这种方式就可以将指定版本部署到线上。

推送这里我们用的是 Pilot 组件,并针对推送速度进行了优化。Pilot 组件会不断监听数据,发现有变更后就会取出。

应用场景

针对这种设计,我们主要将其应用在三个场景中:回滚、分流和 ABTest。

1. 回滚

所谓回滚其实就是流控,比如一开始网关指向的是 V2 版本:

如果发现有问题,我只需要给网关推送一个新的配置,它就可以指向之前那个版本,非常快速:

2. 分流

分流主要应用在文章开始说到的提测场景中。过去使用虚拟机,由于不同的虚拟机有不同的域名,前端同学在测试的时候要么就是为了适配虚拟机去修改代码,要么就是需要测试同学或者产品同学自己去修改自己本机的 host,非常不方便。

而使用容器化的方式,如说现在默认访问的是 V2 版本,但我们现在需要测试 V1 或 V3 版本,就可以推出一个配置给网关,告诉它说如果请求里面的 cookie 含有标识 V=V1,就把请求转发至 V1 版本;同样如果 cookie 包含 V=V3,就将请求转发到 V3, 所有的转发都在网关层完成。

为了使服务更易用,我们提供了一个插件去自动识别云平台部署的服务和版本。QA 和 产品同学在测试的时候,只需要点选版本就可以,系统会自动完成 cookie 注入。然后向服务端发送请求时,网关就会发现这个携带了某个版本的 cookie,自动完成转发:

3. ABTest

同样的原理,我们可以通过配置指定用户的 UID,控制用户去访问 ABTest 中的不同版本,这里支持的方式有很多,比如注入 cookie、不同的 head 头、不同的请求方式等等,非常灵活。

以上是服务治理的内容。总的来说,我们能够自动化部署访问规则,可能只需要前端同学做一个 git-push tag 的操作,就已经打好版本并部署到开发环境甚至是生产环境,而整个过程对于平台的使用者来说是无感知的: 

  • 自动化部署访问规则,完整 CI/CD
  • 灵活的分流策略,带来秒级回滚,灰度,abtest 等功能
  • 结合 chrome 插件,体验流畅

以上介绍了基于容器化云平台我们可以为前端赋予哪些能力。经过一些时间的探索,目前我们的流程已经比较通畅,但不可避免还是会遇到一些问题。

那些年我们遇到的 404

1. 上线后,发现 js 访问404                 

这种情况对用户体验来说非常糟糕。经过排查后我们发现问题出现在为了做到高可用,我们的网关配置了多个。

因为网关的转发配置是通过推送下发的,多个网关之前就会存在时间差。有的网关先收到新的推送,有的后收到。当用户的请求打到了其中一个网关拿到了一个 html,会告诉它应该访问哪个 hash 的 js。但如果不巧的是 hash 的 js 却访问到了另外一个网关,然后转发到另外一个版本,也就是另外一个容器,那么 hash 值肯定就不一样了,找不到对应的文件,导致 404。

这个问题不仅云平台会存在,只要是分布式的部署方案都可能存在时差的问题。我们的解决方案是让所有网关都连接到同一个 Pilot。因为网关的数量是有限的,这时配置的下发就是由一个组件去负责推送所有的网关,因为 xds 协议本身是基于 GRPC 实现的,是一个长连接的操作,所以速度非常快。当由一个节点去做推送,所有网关接收到配置的时差可以控制在在毫秒间,几乎没有影响。也就是 A 网关接收到新配置的同时,基本上 B 网关也已经接收到新配置,这时候所有请求无论打到哪个网关,他们都会指向同一个版本,这个时候线上就不会再出现 404 的请求。

2. 灰度环境,js 访问 404

之前说到,我们的灰度方案是应用插件做 cookie,理论上来说只要 cookie 的配置正确,就可以转发到指定的版本上去。那么既然我的 html 已经没问题了,为什么 js 还会出现 404? 

排查后发现,因为 js 请求的时候有一个标签叫「匿名标签」,如果我们在用 js 的时候打了匿名的标签,浏览器在发 js 请求时就不会携带任何身份的标识,网关就会认为访问到一个默认版本,也就是线上的版本,这个时候如果请求再到 V2 版本就会 404。 

近期规划

1. 尽可能释放构建 Pipeline

目前我们构建镜像的方式主要是用 npm install 和 npm run build 两个命令。之后我们会尽可能去释放 Pipeline,包括基础镜像、Node 版本等,让前端同学可以实现更多自定义的需求。

2. 优化构建和部署时间

目前我们构建镜像的方案没有很好地利用 Docker 的缓存机制,因此会影响构建的时间。我们目前也在做优化,尽可能减少甚至消灭大部分 npm install 的时间和 build 的时间。

3. 释放监控告警能力

目前我们已经完成了一部分监控告警能力的建设,主要是由平台维护团队在使用,去监控 QPS 状况、服务是否稳定,有没有重启等,团队内部也会收到很多告警。但我们认为这种报警其实更应该发送给服务的负责人,后面我们慢慢要将这部分能力释放出来,并且不断完善和优化告警规则。

总结

最后简单总结:

容器化之后到底给前端赋能了什么?

  • 提高测试效率
  • 服务更加稳定,运维高效

马蜂窝云平台如何进一步给前端赋能?

  • 应用中心:一步上云,无差别享受云平台带来的服务
  • 版本管理:实践 DevOps思想,赋能 Nginx 配置;配置驱动,灵活好扩展 
  • 部署管理:智能调度,稳定高活 
  • 服务治理:秒级回滚,秒级恢复,灰度访问,ABTest等众多功能

目前我们在如何通过容器化的方式帮助前端完成应用研发有了一定的探索,并且通过云平台的方式上做到更进一步的赋能,希望能带给大家一些技术思维上的启发。

本文作者:周磊,马蜂窝旅游网基础平台服务化研发工程师。

(题图来源于网络)

关注马蜂窝技术,找到更多你想要的内容

你可能感兴趣的

载入中...