京东老司机:巧用Nginx+Lua解决数据托底大痛点

随着京东商城的发展,内部也出现了一些比较有意思的小系统小模块来解决一些业务系统的痛点,而这些小系统小模块虽说不复杂但是解决了当时的痛点。数据托底就是其中一个痛点,因为依赖系统或者其他方面的不稳定性导致用户访问页面是404或者503、或者出现天窗(页面局部内容没出来),这在一个大流量系统中是不允许的。因此就需要更健壮的系统设计来解决此问题,解决此问题的方法大家又都是类似的,因此作者就抽象了一个小模块来解决更多人的兜底问题。

 

大多数以读为主的系统为了提高系统的可用性,会用到各种策略来增强用户体验;其中数据托底就是其中一种策略;数据托底也可以叫做数据兜底,一般来解决如下几个问题:

 

  • 保证数据”永不消失”(不能开天窗),即假设依赖的服务出问题了,内容还可以展示给用户;
  • 保证数据的正确性,如果以来的服务数据不正确,可以暂时走托底数据;
  • 甚至要保证托底数据的高性能,大促时可以直接走托底数据,因为托底数据一般会通过缓存或者静态化技术完成。

 

实际项目中数据托底的方式也是多种多样,最简单直接的一种方式是将托底功能完全耦合在自己业务系统中,后续每开发一个系统,甚至每增加一种业务功能都要重新实现一次托底功能,并且当系统本身挂掉后托底也就无能为力了。

 

为了减少代码的冗余,降低代码的维护成本,可以把这块功能抽象数出来(如果业务系统是Java语言,可以以jar包的形式提供服务),然后收集出所有需要托底的业务(可以收集url、业务方法等),将其放到一个配置文件中,用worker去更新托底数据。比如京东三级列表页通过Worker去爬所有页面的内容然后静态化存储,即存储整个HTML片段,当动态列表页依赖的服务出现问题了,则直接走托底数据;假设有些页面没有爬到,可以将列表页第一页作为托底数据返回。这种形式显然要比第一种好许多(比如耦合性降低),但仍然没有足够好。首先他需要准确的知道是哪些业务要托底(明确url或方法入参),不能跨语言使用,系统本身挂掉后托底也会失败。

 

另一种方式是将这块功能完全独立成一个系统,并且以http的形式和目标系统通信。假设目标系统有10个页面需要做托底处理,那么我们可以将这10个页面的url告诉托底系统,托底系统可以定时的去抓取我们的页面,然后将正确的数据放到存储设备上,目标系统可以通过Nginx做判断决定如何以及何时去读取这些数据。可以看到这种方式可以做到对目标系统无侵入,也可以跨语言,并且即使目标系统挂掉只要Nginx不挂就仍然可以提供服务。但是这种方法无法动态获取要托底的资源,他在抓取托底数据时需要明确url,试想如果目标系统有几亿的页面要托底,根本没法告知托底系统;多个目标系统依赖同一个托底系统,会有单点风险。

 

可以看到以上方法都或多或少有些缺陷,因此为了更好地解决这些问题,本文使用aop原理基于Nginx+Lua的一种组件化托底方法

 

该方法具有以下几个特性:

  1. 对目标系统零侵入
  2. 可动态拦截请求(不需要预先配置url)
  3. 可以选择数据存储设备
  4. 可以选择何时更新托底数据
  5. 可以校验托底数据
  6. 记录性能日志
  7. 配置简单

 

数据流向图:

 

京东老司机:巧用Nginx+Lua解决数据托底大痛点

 

具体执行过程:

 

1、当用户发起请求时,该组件将其拦截,然后由该组件负责向后端发起请求;在这个过程中我们可以对请求进行限流,这样就可以有效的保护后端服务器,这边我用的是Lua-resty-lock和lLua-resty-limit-traffic实现的。Lua-resty-lock是一个基于nginx的时间事件实现的非阻塞锁,使用他可以有效的防止dog-pile效应;Lua-resty-limit-traffic基于漏桶算法实现的限流组件。

 

2、如果回源失败则直接获取托底数据并返回;回源成功则继续向下走。

 

3、回源成功后需要检查数据是否正确,比如校验数据格式是否正确、是否缺少某个字段、是否缺少某个html元素等,这些校验器可以根据各自的业务自行扩展。如果校验失败则字节取托底数据并返回;校验成功则继续向下走。

 

4、检查是否可以更新托底数据。这里有三种策略可供选择,一种是实时更新,也就是每次请求都要更新一次托底数据;一种是每隔多长时间更新一次;最后一种是按照固定访问次数来更新。第一种策略实现简单,第二种和第三种都需要为请求打上标识。

 

用第二种方式解释如何实现,可以使用nginx的共享字典存储标记,伪代码如下:

 

京东老司机:巧用Nginx+Lua解决数据托底大痛点

5、如果不需要更新则直接将回源数据返回,如果需要更新则继续向下走,到第6步。

 

6、这一步会将回源的响应数据存入到托底存储中。托底存储可以是Redis、memcached、MySQL、Nginx共享缓存、本地文件等,分别使用Lua-resty-redis、Lua-resty-memcached、Lua-resty-mysql、Lua_shared_dict、popen来实现。所有的托底存储都要实现规定的动作,比如get、set、del等动作。

 

使用Redis存储的实现伪代码如下:

 

京东老司机:巧用Nginx+Lua解决数据托底大痛点

 

另外在请求过程、请求回源、请求托底时都会记录性能日志,以备后续分析接口性能和报警使用。

 

使用方法对目标系统无侵入,使用简单;但是Nginx需要集成Lua功能才能使用,如OpenResty,首先在Nginx.conf中配置如下指令:

 

京东老司机:巧用Nginx+Lua解决数据托底大痛点

(注:/backend/demo是/demo的回源调用uri,即此处要按照约定写回源uri)

 

该实现暂时没有开源出去,有兴趣的可以留言联系作者私聊。该模块是纯Lua实现,如下是该模块的简介:

 

网站系统在完成其业务功能外,为保证系统高可用需要做数据兜底;为保证系统的高性能,尽可能的提高系统的GPS,一般会做数据缓存

 

该项目基于Nginx+lua的形式来解决数据托底和提升系统qps,并且对原系统零侵入,操作上只需要对Nginx做一些简单的配置。

 

该项目目前支持两种模块,分别是bottom和cache:

  • bottom模块 该模块可以保证你的网站永不消失(后端应用可以随便挂、各种挂)。
  • cache模块 可以让你系统的qps轻松过万。

 

可以自由选择将数据存储在不同的存储上:

  • Redis 任何支持Redis协议的存储(例如,Redis,jimdb,ssdb等)
  • dict nginx共享缓存(支持数据分片)

 

对于bottom模块,多种数据更新策略供选择:

  • default 默认策略是实时更新
  • time 按照一定的时间间隔更新,比如可以规定每10分钟更新一次
  • num 按照请求的次数来更新数据,比如可以规定每10次请求可以更新一次

 

监控、报警:

  • 基于ump监控协议实现

 

总结:

从以上描述可以看到, 该组件对目标系统无任何侵入性;使用简单,只需要在Nginx层做一些简单的配置;并且只要Nginx不挂,目标系统即使挂掉仍然可以提供服务。整体思路其实很简单,就是在目标系统外加一层代理,然后我们就可以在代理层做各种事情;即总体思想是遵循AOP。目前京东一些频道页、三峡项目在使用该方案。

 

作者介绍  马顺风

  • 负责京东动态服务系统、三峡系统(统一接口服务系统,实现托底、监控、降级、切流量、灰度测试、提升性能),动态服务系统每天有几十亿流量请求;擅长解决高并发问题;业余时间主要研究Redis/Nginx/Nginx+Lua。

转载请注明出处:https://stgod.com/1624/

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: