由grafana-image-renderer引出的一个问题
版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | https://vearne.cc
1.引言
前段时间利用grafana/grafana-image-renderer 做了一个自动截图服务。 见萌叔的文章抓取GRAFANA PANEL视图。但是我发现了一个问题(grafana版本v6.0.1),如果grafana的实例数量超过1个,在grafana中访问grafana render link时,就会有一定的概率失败,图片无法成功渲染。这到底是是为什么?
本文将给出完整的问题排查和解决过程,希望读者可以从中汲取一些养分,为以后排查其它问题提供一些思路。
2. 日志,一切从日志开始
看到这种问题,首先看看日志中是否有线索
2.1 首先看grafana
的日志
只能了解到点击grafana render link之后,会触发一个GET请求,访问grafana服务的
/render/d-solo/P3a2p0cZz/redis-and-mysql-and-cache,然后引起一个内部错误
2.2 看看grafana-image-renderer
的日志
我们已经知道,图片渲染的过程,grafana要调用grafana-image-renderer
,让我们来看下grafana-image-renderer
的日志。
需要注意,grafana-image-renderer
的日志默认是打印在标准输出和标准错误输出里的。
对比成功和失败的case我发现,在失败的case中,日志中会多了一条401 (Unauthorized)的日志
对应的链接,就是我们要渲染成图片的panel
所对应的网页地址
猜测1
现在我们可以猜测grafana-image-renderer
可能是请求grafana服务对应的网页资源失败,然后导致图片渲染失败。
2.3 抓包,需要API入口
2.3.1 对grafana-image-renderer
抓包
为了进一步验证这个上面的猜测,抓包看一下,grafana-image-renderer
所收到的请求
萌叔抓包使用的是buger/goreplay
./gor --input-raw :8081 --output-stdout
1 1a1a5c7c2e733bf0ed41f7ce35bee28da845ab49 1616730655714291898 2541293
GET /render?domain=grafana.example.com&encoding=&height=500&renderKey=HPXorbVBhC6taLHFEe6Jg9O3e5w6R5xn&timeout=60&timezone=Asia%2FShanghai&url=http%3a%2f%2fgrafana.example.com%2fd-solo%2fP3a2p0cZz%2fredis-and-mysql-and-cache%3forgId%3d1%26from%3d1616719854090%26to%3d1616730654091%26var-instance%3d192.168.1.100%3a9090%26panelId%3d8%26width%3d1000%26height%3d500%26tz%3dAsia%2fShanghai%26render%3d1&width=1000 HTTP/1.1
X-Forwarded-Proto: http
Host: grafana-image-renderer.example.com
Connection: keep-alive
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip
展开来看
将url
参数解码以后,确实就是grafana
中panel
对应的网页地址,
到这里猜测1,已经被证实了。
猜测2
请求的参数中有个renderKey
非常的可疑,它会不会是请求grafana获取对应网页时,用来鉴权的token呢?
2.3.2 对grafana
抓包
这里使用tcpdump
抓取之后,然后用wireshark
中观察
tcpdump -i eth0 host 192.168.1.102 and port 3000 -w /tmp/xxx.cap
grafana-image-renderer
请求grafana
时,确实在Cookie中携带了renderkey
。离真相越来越近了。
3. 阅读源码
Talk is cheap. Show me the code.
-- Linus Torvalds
grafana v7.4.5
萌叔阅读了grafana
项目中,renderKey
出现的地方,发现主要在 pkg/services/rendering
访问控制的代码
/pkg/services/contexthandler/contexthandler.go
func (h *ContextHandler) Middleware(c *macaron.Context) {
...
switch {
case h.initContextWithRenderAuth(ctx):
case h.initContextWithAPIKey(ctx):
case h.initContextWithBasicAuth(ctx, orgID):
case h.initContextWithAuthProxy(ctx, orgID):
case h.initContextWithToken(ctx, orgID):
case h.initContextWithAnonymousUser(ctx):
}
...
}
func (h *ContextHandler) initContextWithRenderAuth(ctx *models.ReqContext) bool {
key := ctx.GetCookie("renderKey")
if key == "" {
return false
}
renderUser, exists := h.RenderService.GetRenderUser(key)
if !exists {
ctx.JsonApiErr(401, "Invalid Render Key", nil)
return true
}
ctx.IsSignedIn = true
ctx.SignedInUser = &models.SignedInUser{
OrgId: renderUser.OrgID,
UserId: renderUser.UserID,
OrgRole: models.RoleType(renderUser.OrgRole),
}
ctx.IsRenderCall = true
ctx.LastSeenAt = time.Now()
return true
}
看到这里,猜测2已经被证实了。
调用grafana-image-render
的部分
func (rs *RenderingService) render(ctx context.Context, opts Opts) (*RenderResult, error) {
...
rs.log.Info("Rendering", "path", opts.Path)
if math.IsInf(opts.DeviceScaleFactor, 0) || math.IsNaN(opts.DeviceScaleFactor) || opts.DeviceScaleFactor <= 0 {
opts.DeviceScaleFactor = 1
}
// 在生成renderKey
renderKey, err := rs.generateAndStoreRenderKey(opts.OrgId, opts.UserId, opts.OrgRole)
if err != nil {
return nil, err
}
// 调用完成后就清除掉renderKey
defer rs.deleteRenderKey(renderKey)
defer func() {
rs.inProgressCount--
metrics.MRenderingQueue.Set(float64(rs.inProgressCount))
}()
rs.inProgressCount++
metrics.MRenderingQueue.Set(float64(rs.inProgressCount))
// 调用`grafana-image-render`
return rs.renderAction(ctx, renderKey, opts)
}
func (rs *RenderingService) generateAndStoreRenderKey(orgId, userId int64, orgRole models.RoleType) (string, error) {
key, err := util.GetRandomString(32)
if err != nil {
return "", err
}
// 使用remote cache 存储 renderKey --> RenderUser的对应关系
err = rs.RemoteCacheService.Set(fmt.Sprintf(renderKeyPrefix, key), &RenderUser{
OrgID: orgId,
UserID: userId,
OrgRole: string(orgRole),
}, 5*time.Minute)
if err != nil {
return "", err
}
return key, nil
}
可以看出renderKey
就是临时生成的,供grafana-image-render
访问grafana
使用
grafana v6.0.1
var renderKeysLock sync.Mutex
// v6.0.1 renderKey存储在单机的map renderKeys中
var renderKeys map[string]*m.SignedInUser = make(map[string]*m.SignedInUser)
func initContextWithRenderAuth(ctx *m.ReqContext) bool {
key := ctx.GetCookie("renderKey")
if key == "" {
return false
}
renderKeysLock.Lock()
defer renderKeysLock.Unlock()
renderUser, exists := renderKeys[key]
if !exists {
ctx.JsonApiErr(401, "Invalid Render Key", nil)
return true
}
ctx.IsSignedIn = true
ctx.SignedInUser = renderUser
ctx.IsRenderCall = true
ctx.LastSeenAt = time.Now()
return true
}
读者可能已经看出来了,由于grafana v6.0.1 中renderKey是单机存储的,如果garafana服务部署了多个实例,如上图,instance1和instance2。那么grafana-image-render
可能会拿着instance1产生的renderKey
,去访问instance2,显然鉴权是无法成功的。
在grafana v7.4.5中,renderKey
被存储在remote cache中。
参看配置文件 grafana.ini
[remote_cache]
# Either "redis", "memcached" or "database" default is "database"
type = database
# cache connectionstring options
# database: will use Grafana primary database.
# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=0,ssl=false`. Only addr is required. ssl may be 'true', 'false', or 'insecure'.
# memcache: 127.0.0.1:11211
;connstr =
remote cache默认使用的就是数据库的配置,那么一切都OK了。
猜测3
只需要把grafana的版本升级一下就应该能解决图片渲染失败的问题
4. 实验证明
grafana升级到v7.4.5,问题解决。
总结
整个排障的过程,其实是一个大胆猜测,不断小心求证的过程。其中日志、抓包工具、阅读源码等,都是我们排障的好帮手。
希望这篇文章对大家有所帮助,如果有帮助,别忘了打赏哦。