共计 2893 个字符,预计需要花费 8 分钟才能阅读完成。
在大部分的微服务架构中,Nginx 基本是常用的接入层设施,所以我们希望请求 ID 从 Nginx 层进行校验填充,并且打印在 Nginx 的请求日志中。
阅读提示:本文不提供链路追踪的完整解决方案,只提供 Nginx 层对链路追踪的支持方案!
微服务的诞生,解决了传统单体应用的很多问题,如可维护性差、扩展性差和灵活性差等问题(粗粒比较)。微服务架构虽好,但同时也带来了很多挑战,其中 故障排查 就是其需要解决的挑战之一。那么,如何在很多个应用和实例中找到故障发生的根源呢?
基于以上需求,我们可以将每一笔交易在各个应用中产生的所有日志,进行集中式收集与展示(但前提是你得有:日志中心)。这样就可以很快看出交易是在哪一步出的故障。如果做得好,还可以直接进行二次开发与数据分析,将收集的日志和出现的故障进行分析后,用图形界面很直观的进行展示。
比如,可以展示出微服务调用的拓扑图,使用颜色进行区分故障(如常用红:表示异常、绿:正常、黄:警告)。接着可以将常出现的故障或异常进行分类后做出友好型的展示(说白了就不用直接上堆栈),如:NullPointerException:则界面直接友好型的提示哪一行代码抛了空指针,输入参数是什么……(这不是该篇的重点哈,废话不多说了,后续有机会再详细介绍)。
要做整个微服务架构的链路追踪,肯定是希望从交易进入微服务中心的第一个点就开始有一个全局的交易 ID 来关联所有日志(链路追踪,这么一个 ID 肯定是不够的,但这里只介绍这个哈)。当然最理想的肯定是希望把前端的日志(如操作日志、数据流等)也规划进行。
在大部分的微服务架构中,Nginx 基本是常用的接入层设施,所以我们希望请求 ID 从 Nginx 层进行校验填充,并且打印在 Nginx 的请求日志中。这里只提供三种方式来实现 Nginx 层的交易 ID 生产方式。
在 1.11.0 之前的版本,我们可以采用拼接的方式来组装请求 ID。参考配置如下:
server {# 定义 $request_trace_id 的值,在 1.11.0 之前,我们可以使用类似的方式声明 | |
# 只要能确保其值出现重复的可能性尽可能的小即可。 | |
set $request_trace_id trace-id-$pid-$connection-$bytes_sent-$msec; | |
location / {# …… | |
# 将此 trace_id 传递给后端的 server,通过 header 方式,此后我们既可以在环境中获取此 header | |
proxy_set_header X-Request-Id $request_trace_id; | |
} | |
} |
参数说明:
- $pid:nginx worker 进程号
- $connection:与 upstream server 链接 id 数
- $bytes_sent:发送字节数
- $msec:当前时间,即此变量获取的时间,包含秒、毫秒数(中间以. 分割)
利用系统/dev/urandom 生成的随机 UUID。参考脚本如下:
--- | |
--- UUID | |
--- Created by lry. | |
--- DateTime: 2018/2/25 下午 7:38 | |
--- Describe: 用系统 /dev/urandom 生成的随机 uuid | |
--- | |
local template ="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" | |
local d = io.open("/dev/urandom", "r"):read(4) | |
math.randomseed(os.time() + d:byte(1) + (d:byte(2) * 256) + (d:byte(3) * 65536) + (d:byte(4) * 4294967296)) | |
local uuid=string.gsub(template, "x", | |
function (c) | |
local v = (c == "x") and math.random(0, 0xf) or math.random(8, 0xb) | |
return string.format("%x", v) | |
end) | |
return uuid |
Nginx 在 1.11.0版本中就提供了内置变量 $request_id,其原理就是生成 32 位的随机字符串,虽不能比拟 UUID 的概率,但 32 位的随机字符串的重复概率也是微不足道了,所以一般可视为 UUID 来使用即可。参考配置如下:
# Nnginx 代理默认会把 header 中参数的 "_" 下划线去掉,所以后台服务器后就获取不到带 "_" 线的参数名 | |
underscores_in_headers on; | |
# 设定日志格式 | |
log_format main \'$remote_addr - $remote_user [$time_local]"$request"\' | |
\'$status $body_bytes_sent"$http_referer"$upstream_http_request_id \' | |
\'"$http_user_agent" "$http_x_forwarded_for"\'; | |
server { | |
location / {# 如果请求头中已有该参数, 则获取即可; 如果没有, 则使用 $request_id 进行填充 | |
set $temp_request_id $http_x_request_id; | |
if ($temp_request_id = "") {set $temp_request_id $request_id; | |
} | |
# 屏蔽掉原来的请求头参数 | |
proxy_set_header x_request_id ""; | |
# 设置向后转发的请求头参数 | |
proxy_set_header X-Request-Id $temp_request_id; | |
} | |
} |
生成交易 ID 的方式有很多种,但希望使用者结合自身实际情况进行合理取舍,而不要盲目的追求 ID 的唯一性、可读性和时序性等等。
比如,ID 具有时序性虽然有一定的好处,但实际的架构根本没有去使用该时序性,则没必要花大量的精力和做出大量的开发,去实现一个有时序性的交易 ID。又比如,觉得 UUID 可读性太差,从而花了很多成本去开发一个具有一定含义的交易 ID(如前几位表示什么意思,多少位到多少位又表示什么意思之类的),开发出来后,实际架构根本没有去解读该 ID 的地方,则浪费了成本。
但也不是所有人都直接使用 UUID 就能满足的,比如我需要考虑日志的容量,则可以考虑适当缩减 ID 的长度(每个 ID 缩减 10 个字符串,每笔交易就可能少几百或几千个字符串,再往上规划,还是可以减少一些日志容量的)。
最后,如果有考虑想收集前端的日志的童鞋,建议交易 ID 就不要使用 Long 型,因为前端可能会有损失精度的问题。同时也建议使用 $request_id 来填充交易 ID。