微服务在前端的落地
1. 背景
1.1 试水
公司的 PC 客户端的起先所有的 UI 界面都是 QT 开发,但是随着业务的深入,需求方对于数据展示的需求越来越强烈,但是使用 QT 代码做这种强 UI 的渲染会比较繁琐,所以 PC 客户端引入了内嵌网页来做数据展示。我们这里称这些内嵌网页为 Web 组件。
1.1.1 演进
PC 客户端和服务器端之间的通信要依赖于长连接,使用原生代码编写 UI 时,直接在使用原生代码建立长连接即可,但是使用 Web 组件之后,无法再新增长连接,否则会出现同一个用户长连接互踢的问题,所以 Web 组件是借用原生端已经建立好的长连接进行通信。
图 1.1.1.1 Web 组件通过原生代理通信
不过随着业务的发展,增加的 Web 组件也越来越多,最终整体的总览就变成下面这个样子:
图 1.1.1.2 Web 组件通信拓扑图
长连接处在整个架构中心的位置,根据以往的经验看,有中心就会有依赖,仔细考虑当前这条长连接还是有很多弊端的,比如说:
- 挤占原生进程的资源,原生程序为了维护Web组件通信的信道必须额外维护上下文。
- 增加了程序开发调试的复杂度,因为依赖于原生端的 js 调用,所以 web 端的程序必须嵌入到原生应用中才能运行。
既然中心化是有缺陷的,那么最直接解决方案就是去中心化,鉴于长连接的维护成本较高,为了的解决方案中会采用短链接的架构:
图 1.1.1.3 Web 组件使用短连接
从这里开始引入了微服务的概念,每个微服务负责独立的一个功能,与其对应的 Web 组件也相对独立。起初设计此架构是为了解决 PC 客户端的问题,后来此架构也延申到了移动 APP 端和网页端。
1.2 域名
图 1.1.1.3 中画了若干个 Web 组件,每个对应的都是不同的后端微服务,如果每次创建一个微服务都申请一个域名会对维护带来巨大的挑战。所以这个地方在设计的时候,所有的微服务采用同样一个域名,这里记这个域名为 micro-default。然后客户端在请求的时候,在请求路径前面添加微服务的标识,那么前置的域名解析 nginx 在做反代配置的时候,就可以做类似如下配置:
代码 1.2.1
2. 存在问题
2.1 智能 DNS 解析不均衡
2.1.0 问题描述
端上访问服务器的默认域名是 micro-default 这个域名,它是做了动态 DNS 解析,会根据用户的运营商的 DNS 来决定解析到哪里去
图 2.1
DNS 服务会根据用户上网设备用的 DNS 地址归属地来决定往哪调度,如果识别不出来就默认网北京节点调度,但是大部分用户的所用 DNS 地址的地址不被 DSN 服务所识别(比如说很多用户用的是 114 的DNS),所以就直接走默认值了。
这样导致的问题是,虽然我们有三个入口点,但是 80% 的流量集中到北京节点了,另外两个节点空闲,好在当前的 http 流量并不大,北京的入口点高峰期间占用的资源也很小。
2.1.1 解决方案
2.1.1.1 市面上的解决方案
DNS Over https,使用 https 请求来代替传统 DNS 请求,根据请求者的出口 IP 可以精确的确定地理位置
举个例子,阿里 DNS,
https://dns.alidns.com/dns-query
https://223.5.5.5/dns-query
https://223.6.6.6/dns-query
https://[2400:3200::1]/dns-query
https://[2400:3200:baba::1]/dns-query
不过这种技术需要用户自己单独在电脑上做配置,起码在电脑端当前解决方案实现起来不现实。
2.1.1.2 自建解决方案
移动 APP 端或者 PC 客户端由于可以自行修改请求的 HTTP 数据包,所以可以在触发 http 请求之前,会尝试将域名发送一个调度接口,该接口能够返回后端配置过根据用户请求 IP 映射的服务器 IP,记为 IPX
。如果 IPX
存在,则使用这个 IP 去请求服务器端;否则就回退到传统的 DNS 解析模式。
图 2.1.1.2
如果是浏览器端,则无法直接使用返回的 IP 地址来构建 https 请求,除非当前服务器的数字证书直接绑定在 IP 上,这种数字证书比较贵,一般公司不会购买。那么在上图第 2 步的时候,可以直接返回一个子域名来来解决,也就是说对于 图2.1 的每个 SLB 的 IP 都映射一个子域名。
2.2 客户端缓存
2.2.0 问题描述
同时留意到如果系统中存在集中跳转的情况出现,在短时间内就会有高并发查询接口的情况出现,下图是一个具体微服务请求日志的kibana统计,显示1ms发生了3000个并发请求,而这三千个请求都是同一个查询接口。
图 2.2.0.1 1ms 3000请求
2.2.1 解决方案
2.2.1.1 市面方案
在前置节点做文章(更多的称之为边缘计算),即使后端 http 服务是非静态资源,依然可以在前置节点做文章,将请求缓存在前置节点。
图 2.2.1.1.1
市面上常见的产品有阿里云DCDN nginx plus openresty edge
2.2.1.2 自建解决方案
跟市面上的服务器的处理原理一样,只不过我们只关心缓存处理,具体缓存协商流程分析
可以看出来,前置节点像一个漏洞,将本来请求到机房的流量给拦截了一部分,流量拦截量的多少,就取决于机房里面的服务设置的缓存时间的长短(记为 CACHE-A)。同时前置节点和客户端之间还有缓存时长约定(记为 CACHE-B),这个约定可以规范客户端的请求间隔时长,让客户端不要在短时间内发送过多的请求到前置节点。理论上讲,客户端端最终拿到数据的滞后时间为 **max(CACHE-A, CACHE-B)**。
3. 性能问题排查
在 nginx 的访问日志中可以配置如下几个字段:request_time upstream_connect_time upstream_header_time upstream_response_time,这几个字段的意义如下图所示:
参考配置代码
图 4.1
典型问题分析:
情景1
图 4.2
upstream_connect_time ≈ upstream_header_time ≈ upstream_response_time ≈ request_time
时间耗费在跟后端服务器建连上,说明专线带宽不稳定(小概率),或者单位时间内并发过大(大概率,目前反向代理使用的http 1.0,容易出现这种问题)
情景二
图 4.3
upstream_connect_time << upstream_header_time ≈ upstream_response_time
时间耗费在后端服务处理上
情景三
图 4.4
upstream_connect_time ≈ upstream_header_time ≈ upstream_response_time << request_time
时间耗费在客户端数据读写上或者 客户端和 nginx 建连上,由于目前nginx版本比较低,nginx建连时间暂时取不到,但是这种情况出现的概率很低,大概率是耗费在读写客户端数据上。
严格意义讲,request_time 不是客户端请求的真正时间,从客户端测观察的时间要比这个时间稍大。极端情况下会有大的偏差,因为客户端网卡满载情况下,服务器端网络层写入的数据有可能会被客户端丢包,继而会引发重传,耗费大量的时间,这部分时间在服务器端是没法记录的。