系统背景
问:修改一个网站的文案需要多久?对于一个小型个人网站来说,估计很简单,几分钟就能修改完成并发布。但如果说要修改的是上百个网站的文案呢?那估计就得需要产品提需求,研发排期开发,测试进行回归验证。由于涉及的应用众多,而每个应用都有自己的研发需求,可能无法快速排期进行文案修改。所以看似一个非常简单的需求,涉及到的应用和部门比较多的时候,也就成了产品经理的恶梦。尤其是像京东商城这样的大型购物网站,你浏览过的每一个网页的背后都是有许多个业务系统在支撑,并由专门的研发团队来负责维护。而各业务系统为了能够保持统一的网页风格,往往都会使用相同的页面头部和尾部,我们称之为公共头尾。
比如上图,是目前京东网站统一在使用的页面尾部,如果想要修改尾部的文案或者链接,那就需要去推动上百个系统和研发团队去排期修改并上线。为了解决这一问题,京东统一头尾管理系统就这样诞生了,基本上实现了五分钟修改京东全站公共头尾内容。
目前,统一头尾系统取得的成果如下:
系统总体架构设计
整个系统主要包括两部分,第一部分是管理后台,主要用来管理京东的公共头尾文件和业务系统,配置业务系统与公共头尾文件的关联关系,并针对业务系统进行公共头尾文件的分发。第二部分是头尾客户端,主要用来获取业务系统依赖使用的头尾文件,然后解析渲染页面,将最新版本的头尾文件内容进行输出。而为了应对不同版本语言开发的业务系统,头尾客户端又分成Java客户端和Nginx客户端。Java客户端主要支持Java语言开发的业务系统,不仅可以解析处理静态HTML,还支持解析JSP/Velocity/FreeMarker/Thymeleaf等页面模板引擎。Nginx客户端则支持了非Java语言开发的业务系统,实现了非Java系统的页面模板解析和渲染公共头尾的功能。
管理后台设计与实现
整个管理后台实现了前后端分离,后端负责提供HTTP接口,前端只负责页面渲染。管理后台按照模块划分,主要分为了三个模块,包括文件管理模块、应用管理模块和个人中心模块。
- 文件管理模块
提供公共头尾文件的维护功能,可以将公共头尾的HTML内容在管理后台进行创建保存,并针对公共头尾文件进行了版本控制,用户可以在管理后台对头尾文件进行编辑、发布和回滚等操作。
- 应用管理模块
提供业务系统的维护功能,用户可以在管理后台添加新应用,创建配置环境,添加业务系统依赖使用的公共头尾配置关系,查看应用信息以及业务应用接入的头尾客户端请求信息。
- 个人中心模块
用来记录管理后台用户的各种操作日志,包括文件操作和应用操作,并提供操作日志查询功能。还针对公共头尾文件的发布操作进行上线审批处理。
头尾客户端设计与实现
前边介绍的头尾管理后台已经实现了头尾文件创建维护与版本控制,而业务系统如何依赖引用这些头尾文件,则是我们下一步需要面临的问题。首先,我们需要解决的问题就是如何将头尾管理后台中创建的头尾文件分发到各业务系统中。目前主要有两种方式,分别是头尾系统Push方式和业务系统Pull方式。
- 头尾系统Push方式:
意味着需要将各业务系统中每一台服务器作为服务端,而头尾系统则作为客户端,头尾系统中的头尾文件有更新时,主动连接各业务系统服务端,连接成功后,将头尾文件的最新内容发送到业务系统。为了保证头尾系统客户端能够随时与各业务系统服务端建立连接,需要业务系统监听固定端口,并时刻提供服务,否则就会有头尾文件push失败的风险。而现实环境是京东的业务系统众多,部署环境也是多种多样,还有不同的开发语言。如果要开发头尾服务端,首先就要解决跨语言的问题,京东目前常用的开发语言有Java、Js、Php、Golang和Lua,我们就需要提供并维护五种语言的头尾服务端版本。而且由于业务系统监听的端口众多,头尾服务端启动时还会面临着端口被占用的风险,也同样会导致头尾服务端无法正常启动,从而无法更新头尾文件。但是该方式也有优点,就是只有在头文件有更新需要Push的时候,才建立连接去Push,头尾文件不仅能够实时更新并生效,还可以节省服务器资源。
- 业务系统Pull方式:
该方式与头尾系统Push方式正好相反,将头尾系统作为服务端,可以解决因端口占用而导致无法启动的问题,但还是会面临跨语言客户端版本的问题。但是我们通过对业务系统进行调研分析,基本上所有的业务系统都会用到Nginx做为反向代理,这个Nginx就给了我们一个支持跨语言业务系统的可能。然后只需要开发一个Java版本的头尾客户端来给Java业务系统引入并Pull头尾文件。不过该方式也有缺点,就是头尾客户端不知道头尾文件何时会更新,头尾客户端只能定时轮询头尾系统来检查头尾文件是否有更新,如果文件有更新,则拉取新的头尾文件内容。这样就会造成头尾文件不能实时更新,并且定时轮询也会消耗一定的服务器和网络资源。
最后经过综合考虑,我们选择了业务系统Pull方式来进行头尾文件的分发。而为了解决业务系统跨语言的问题,我们提供了两个版本的头尾客户端,即Nginx头尾客户端和Java头尾客户端,基本满足了所有业务系统的头尾文件拉取功能。但是业务系统如何引用这些头尾文件,这里就涉及到一个SSI(服务端网页包含)技术。下面就介绍一下两种方式的头尾客户端如何解决头尾文件的拉取和SSI问题。
- Nginx头尾客户端
该方式主要是利用了Nginx的SSI模块来实现头尾文件的拉取和SSI问题,ngx_http_ssi_module模块是Nginx中的一个过滤器,在经过它的响应中处理SSI(服务端包含)指令。目前用到的就是inclued指令,配置示例:
业务系统中的页面就通过该配置指令来引用头尾系统中维护的头尾文件,但是该配置指令需要服务器上真实存在这些文件,才能够被Nginx加载并替换。所以只靠该配置还无法引入头尾系统中配置的头尾文件,还需要将inclued指令引入的文件名称转换成URL,然后去头尾系统服务端请求对应版本的头尾文件。所以这里使用了Nginx的URL重写和反向代理配置来解决头尾文件的拉取问题。到这里,其实一个完整的头尾文件SSI功能已经实现了,在业务系统访问包含头尾文件的页面也已经可以完整展示了。
但是还是存在一些问题,这里的头尾文件请求是用户浏览页面时被动触发的,而且还是Nginx通过反向代理同步请求的头尾文件,所以头尾系统的响应时间就直接影响到了业务系统的页面加载时间,如果头尾系统超时,则业务系统页面也会超时;而且业务系统(包括京东首页,商详页)的页面流量会全部打到头尾系统,这都是头尾系统无法承受的流量。所以我们需要减少业务系统的请求量,而这些头尾文件的内容本身变化的频率也不太高,所以可以通过Nginx来增加一级本地缓存proxy_cache。当用户浏览业务系统页面时,则优先请求本地缓存的头尾文件,缓存时间到期后,再去请求头尾系统获取最新的头尾文件。通过Nginx代理缓存的配置,将成千上万的用户请求量优化为每台业务系统的服务器缓存时间内只请求一次,大大减少了头尾系统的请求压力。同时再通过proxy_cache_use_stale配置来降低业务系统对头尾系统的依赖风险,即使头尾系统出现宕机问题,也不会影响业务系统的头尾文件加载展示。下面是Nginx客户端部分配置示例:
location ~ ^/fragment/ {
proxy_cache header_cache;
proxy_cache_key $uri;
proxy_cache_valid 200 1m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
proxy_cache_lock_timeout 1s;
proxy_connect_timeout 1s;
proxy_ignore_headers Set-Cookie Cache-Control;
proxy_hide_header Cache-Control;
proxy_hide_header Set-Cookie;
# 参考头尾系统中配置,请注意区分测试环境和生产环境,返回的文件内容默认都是UTF-8编码内容,如果需要GBK编码内容需要在env后面拼接参数?charset=GBK
# 只需要替换{appId} {token} 和 {env}
rewrite ^/fragment/(.*) /open/fragment/$1/Nginx/$nginx_version/$server_addr/{appId}/{token}/{env} break;
proxy_set_header Accept-Encoding "";
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://xxx.jd.local;
}
- Java头尾客户端
前边介绍的Nginx方式客户端虽然已经解决了头尾文件的SSI问题,但由于Nginx的SSI过程是在用户访问页面时才触发的,属于用户请求过程中的同步调用,即使增加了本地缓存,但还是会对页面的响应时间有所影响。所以为了解决这一性能损耗问题,我们专门开发了一个Java版本的头尾客户端,来实现头尾文件的SSI功能。头尾客户端的启动过程如下图:
首先需要业务系统引入Java头尾客户端依赖jar包,然后配置头尾系统中的应用ID、访问令牌、环境标识以及需要解析的页面模板路径和模板文件后缀名。配置完成后,头尾客户端会跟随业务系统一起启动。在启动过程中,头尾客户端会首先将头尾系统中配置的头尾文件下载到业务系统服务器本地目录中,然后再启动一个异步线程去轮询请求头尾系统并检测头尾文件更新情况,如果头尾文件有变化,则直接下载最新头尾文件到本地目录中。
头尾文件下载到本地后,头尾客户端会根据配置中的页面模板路径和后缀名去扫描加载所有包含SSI指令的模板文件到内存中,并创建这些模板文件的备份文件。然后根据加载到内存中的模板文件再去解析模板文件中的include指令,最后通过inclued指令配置的文件名称加载头尾文件内容进行替换,从而生成新的模板文件。模板解析完成后注册启动一个头尾文件观察者,专门用来监视头尾文件是否更新,如果有更新,再次解析内存中的模板内容生成新的模板文件。这一过程基本上是在业务系统启动时进行的,所以当用户请求业务系统页面时,业务系统可以直接将这个模板文件进行返回,避免了在用户请求过程中的SSI处理,基本实现了对业务系统性能的零损耗。
作者:京东零售 曹志飞
来源:京东云开发者社区