背景
在微服务大行其道的今天,如何观测众多微服务、快速理清服务间的依赖、如何对服务之间的调用性能进行衡量,成了摆在大家面前的难题。对此,Skywalking应运而生,它是托管在 Apache 基金会下的开源项目,旨在帮助开发者监控分布式程序的性能、了解各个服务的调用关系和运行情况。
Skywalking支持多种语言和框架,包含Java、Golang、Python等,功能强大、界面友好等特点使其迅速成为业界最流行的APM软件之一。然而在运用Skywalking的过程中,我们常常更关注服务之间的调用链路、性能数据,往往会忽略流量入口(网关)到服务之间的Trace串联,导致我们经常在网关层面观测到一个错误调用后,无法通过TraceID快速查看本次调用的链路,从而白白浪费宝贵的排障时间。
本文重点介绍如何将 Ingress Nginx 集成进 Skywalking,将其作为 Skywalking 的一个节点,并且在access log 中打印TraceID,从而在出现故障的时候,可以通过日志中的TraceID快速找到调用链路,达到快速故障定位的效果。
注:本文使用的 Kubernetes 版本是 1.24.15,Ingress Nginx controller 版本是 v1.8.1,Skywalking版本是9.2.0。
方案
在介绍方案之前,我们先了解一下相关的背景知识,用于更好的理解集成方案。
- Ingress Nginx Configmap:Ingress Nginx 的各种配置存放地,可以通过该Configmap配置logformat、所开启的插件等。
- Skywalking Nginx Lua:Skywalking 官方提供的 Lua 版本 lib,提供了一系列的操作,自己可以在Nginx的配置文件中编写Lua脚本,适时创建Span、结束Span,从而把 Nginx 当作Skywalking中的一个服务节点集成进Skywalking。
- Ingress Nginx 自定义插件:Lua脚本编写的插件,用于对 Ingress Nginx 做编程,想要使用插件必须要将插件放到 Ingress Controller 容器的 /etc/nginx/lua/plugins/插件名称 目录中,且需要在 Ingress Controller 的configmap中开启它。自定义插件支持以下几个钩子:
- a. init_worker: 用于对Nginx Worker做一些初始化。
- b. rewrite: 用于修改请求、更改标头、重定向、丢弃请求、进行身份验证等。
- c. header_filter: 当接收到后端response header 时调用此函数,通常用来记录和修改后端的response header。
- d. body_filter: 这是在收到后端response body 时调用的,一般用来记录response body。
- e. log: 当请求处理完成并将响应传递给客户端时,会调用此函数。
- sw8:SkyWalking 跨进程传播的Header Key,它的格式是 1-TRACEID-SEGMENTID-3-PARENT_SERVICE-PARENT_INSTANCE-PARENT_ENDPOINT-IPPORT(其中TraceID、SpanID等都通过base64进行编码),我们可以通过此Header解析出对应的 TraceID。
了解了上述原理后,我们的方案就显而易见了,就是将 Skywalking Nginx Lua 集成进 Ingress Nginx中,并编写插件,在不同阶段执行相关操作:
- a. 在 rewrite 阶段生成新Span并解析出TraceID将其放在新Header中(方便access log 打印)
- b. 在 body_filter 阶段结束该Span
- c. 在log阶段提交对应的数据到Skywalking服务端
- d. 修改 Nginx log format,将存储 TraceID 的Header 打印出来
步骤
1. 集成Skywalking Nginx Lua进Ingress Nginx
Skywalking Nginx Lua 的核心是它的 lib 目录,里边包含了所有需要用到的函数操作,所以我们需要将该 lib 目录的内容放到 Ingress Nginx 的Pod 中,让我们编写的插件能够调用到它。具体我们可以将 lib 目录放到网盘中,然后通过 Volume 的形式挂载进去,也可以将 lib 的内容写入configmap,然后挂载Volume到Pod中。本文选择第二种方式,将 lib 的内容放到 configmap 中,然后挂载进去,虽说这种方式不够优雅,但好在不用依赖网盘。
我们先clone Skywalking Nginx Lua 这个库,然后将 lib 下的所有.lua文件打平放到同一个目录中
git clone https://github.com/apache/skywalking-nginx-lua.git
mkdir sk-lua-cm
cp skywalking-nginx-lua/lib/skywalking/*.lua sk-lua-cm/
cp skywalking-nginx-lua/lib/skywalking/dependencies/*.lua sk-lua-cm/
cp skywalking-nginx-lua/lib/resty/*.lua sk-lua-cm/
再通过 kubectl 命令将sk-lua-cm中的所有文件创建到一个configmap中,注意将 -n 后边的参数换成你自己Ingress Nginx 所在的 namespace。
kubectl create cm skywalking-nginx-lua-agent --from-file=./sk-lua-cm/ -n ingress-nginx
2. 编写Ingress Nginx 的插件
引入了 Skywalking 的 lib 后就可以编写对应的 Ingress Nginx 自定义插件了,代码比较简单,以下是代码详情(命名为main.lua)。
local _M = {}
function _M.init_worker()
local metadata_buffer = ngx.shared.tracing_buffer
require("skywalking.util").set_randomseed()
local serviceName = os.getenv("SKY_SERVICE_NAME")
if not serviceName then
serviceName="ingress-nginx"
end
metadata_buffer:set('serviceName', serviceName)
local serviceInstanceName = os.getenv("SKY_INSTANCE_NAME")
if not serviceInstanceName then
serviceName="ingress-nginx"
end
metadata_buffer:set('serviceInstanceName', serviceName)
metadata_buffer:set('includeHostInEntrySpan', false)
require("skywalking.client"):startBackendTimer(os.getenv("SKY_OAP_ADDR"))
skywalking_tracer = require("skywalking.tracer")
end
function _M.rewrite()
local upstreamName = ngx.var.proxy_upstream_name
skywalking_tracer:start(upstreamName)
if ngx.var.http_sw8 ~= "" then
local sw8Str = ngx.var.http_sw8
local sw8Item = require('skywalking.util').split(sw8Str, "-")
if #sw8Item >= 2 then
ngx.req.set_header("trace_id", ngx.decode_base64(sw8Item[2]))
end
end
end
function _M.body_filter()
if ngx.arg[2] then
skywalking_tracer:finish()
end
end
function _M.log()
skywalking_tracer:prepareForReport()
end
return _M
划重点:在上述代码中获取了几个环境变量,需要记住,后边需要用到。
- i. SKY_SERVICE_NAME:Ingress Nginx 在 Skywalking 中的 Service 名称
- ii. SKY_INSTANCE_NAME:Ingress Nginx 实例在 Skywalking 中的实例名称
- iii. SKY_OAP_ADDR:Skywalking后端地址
编写好插件代码后就可以基于此创建configmap了,依然需要注意 -n 后边的namespace,需要改成你实际Ingress Nginx所在的 namespace。
kubectl create cm skywalking-lua-plug --from-file=main.lua -n ingress-nginx
3. 挂载相关 Lua 脚本进 Ingress Nginx Controller 的 Pod 中
修改 Ingress Nginx Controller 的 Deployment 配置,主要修改以下几点:
a. 环境变量
- name: SKY_OAP_ADDR
value: http://skywalking-oap.skywalking.svc.cluster.local:12800
- name: SKY_SERVICE_NAME
value: ingress-nginx
- name: SKY_INSTANCE_NAME
value: ingress-nginx
b. volumes 声明
- name: sky-nginx-plugin
configMap:
name: skywalking-lua-plug
- name: skywalking-nginx-lua-agent
configMap:
name: skywalking-nginx-lua-agent
c. volumeMounts 声明
- mountPath: /etc/nginx/lua/plugins/skywalking/main.lua
subPath: "main.lua"
name: sky-nginx-plugin
- mountPath: /etc/nginx/lua/resty/http.lua
subPath: "http.lua"
name: skywalking-nginx-lua-agent
- mountPath: /etc/nginx/lua/tablepool.lua
subPath: "tablepool.lua"
name: skywalking-nginx-lua-agent
- mountPath: /etc/nginx/lua/resty/http_headers.lua
subPath: "http_headers.lua"
name: skywalking-nginx-lua-agent
- mountPath: /etc/nginx/lua/resty/jit-uuid.lua
subPath: "jit-uuid.lua"
name: skywalking-nginx-lua-agent
- mountPath: /etc/nginx/lua/skywalking/client.lua
subPath: "client.lua"
name: skywalking-nginx-lua-agent
- mountPath: /etc/nginx/lua/skywalking/constants.lua
subPath: "constants.lua"
name: skywalking-nginx-lua-agent
- mountPath: /etc/nginx/lua/skywalking/correlation_context.lua
subPath: "correlation_context.lua"
name: skywalking-nginx-lua-agent
- mountPath: /etc/nginx/lua/skywalking/dependencies/base64.lua
subPath: "base64.lua"
name: skywalking-nginx-lua-agent
- mountPath: /etc/nginx/lua/skywalking/management.lua
subPath: "management.lua"
name: skywalking-nginx-lua-agent
- mountPath: /etc/nginx/lua/skywalking/segment.lua
subPath: "segment.lua"
name: skywalking-nginx-lua-agent
- mountPath: /etc/nginx/lua/skywalking/segment_ref.lua
subPath: "segment_ref.lua"
name: skywalking-nginx-lua-agent
- mountPath: /etc/nginx/lua/skywalking/span.lua
subPath: "span.lua"
name: skywalking-nginx-lua-agent
- mountPath: /etc/nginx/lua/skywalking/span_layer.lua
subPath: "span_layer.lua"
name: skywalking-nginx-lua-agent
- mountPath: /etc/nginx/lua/skywalking/tracer.lua
subPath: "tracer.lua"
name: skywalking-nginx-lua-agent
- mountPath: /etc/nginx/lua/skywalking/tracing_context.lua
subPath: "tracing_context.lua"
name: skywalking-nginx-lua-agent
- mountPath: /etc/nginx/lua/skywalking/util.lua
subPath: "util.lua"
name: skywalking-nginx-lua-agent
4. 修改 Ingress Nginx Controller 所使用的configmap配置
plugins: "skywalking"
lua-shared-dicts: "tracing_buffer: 100m"
main-snippet: |
env SKY_SERVICE_NAME;
env SKY_INSTANCE_NAME;
env SKY_OAP_ADDR;
log-format-upstream: |
$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" $request_length $request_time [$proxy_upstream_name] [$proxy_alternative_upstream_name] $upstream_addr $upstream_response_length $upstream_response_time $upstream_status $request_id $http_trace_id
该配置中配置了如下几个信息:
- a. plugins:开启skywalking插件
- b. lua-shared-dicts:声明 trace 使用的变量和大小
- c. main-snippet:其中声明了需要使用到的环境变量,切记在插件中使用的环境变量必须放到这里来
- d. log-format-upstream:log 格式,我们在这个里添加了一个 http_trace_id 这个header的打印(上一步解析出来的TraceID)
5. 重启 Pod 生效
把下列的 xxx 换成 Ingress Nginx Controller 的 Pod 名称
kubectl delete pod xxxx -n=ingress-nginx
效果展示
本节展示最终在Skywalking UI 上的展示效果。
- 可以在 Skywalking 的拓扑图中看到有一个 Ingress Nginx 的服务节点加入
- 在Trace查询页面可以看到有一个 Nginx 的 Span
- 如下是打印出来的TraceID
如何更好使用Trace?
通过上述介绍,我们已经把 Ingress Nginx 集成进了Skywalking,接下来介绍一下如何更好的使用Trace数据来快速定位故障。
数据建设
- 将Skywalking数据源接入Flashcat,接入的方法很简单,只需要填写对应的地址、账号、密码,然后起一个名字即可
- 采集 Ingress Nginx 日志到 kafka 中,这里可以使用 categraf 的 log 采集器
- 将日志接入 Flashcat 的日志分析子系统生成报表,在这张报表中可以看到对应的域名、接口、流量、成功率等(当然,这些维度都可以自定义),在创建报表的时候设置好日志中哪个字段是TraceID字段。
- 通过日志报表生成灭火图(IT系统健康度一览表),例如下图就是典型的电商系统核心API健康度一览表。
- 通过数据库、Metrics、日志等不同来源建立北极星指标(核心业务指标),例如:电商系统的下单量、支付量等
串联打通
通过Flashcat的串联能力,建立北极星和灭火图的串联。
故障定位路径
当建设好对应的指标和关联后,就可以开启我们的故障定位之旅了。
- 当北极星指标故障(核心业务受损)时,北极星页面上对应的指标会飘红且发送报警,例如下图中的商品实时下单量掉底了,该业务卡片会飘红
- 此时我们点击曲线上掉底时刻的数据点,可以打开关联的灭火图,一眼就可以看到是订单子系统在飘红(可能发生了故障)
- 我们点击飘红的灭火图路径,可以下钻到具体的卡片组中去
- 然后可以看到灭火图卡片组中的订单更新DB接口成功率为0,我们点击旁边的详情可以打开对应的详情曲线(通过上一步中的日志报表生成)
- 通过详情可以看到,这个时候的成功率已经掉底了,那么我们同样可以点击曲线上掉底的时间点打开日志详情
- 在这个页面中,我们可以直接查看异常日志的日志详情,也可以点击右侧的 trace 按钮打开该调用的Trace链路
通过Trace链路可以看到Redis的端口不通导致更新失败了,这个时候我们就需要去排查依赖的Redis是否正常了。
当然,以上只是Flashcat在整合可观测性三大支柱(Metrics、Log、Trace)方面的一个小例子