
告警邮件里只有一行冷冰冰的阈值描述没有曲线图值班的人还得点开链接、重新登录一遍 grafana才能判断这波抖动值不值得爬起来处理——这种体验我忍了很久。后来花了一个下午把 grafana 的 image render 从一个空配置配到能稳定出图邮件和群里推送的告警终于带上了那张渲染好的面板截图值班同学扫一眼就心里有数。这篇就把整个过程按顺序讲清楚它是什么、为什么单独拆出来、怎么部署、参数怎么定、中文为什么变方块、以及我踩过的那些坑。先把概念说明白。image render 是 Grafana 的一套离屏截图能力它把某个面板按你指定的尺寸和时间范围用无头浏览器画成一张 PNG也能把整个仪表盘拼成多页 PDF。Grafana 本体不干这件事从 7.0 起官方把它拆成了一个独立服务叫 grafana-image-renderer。所以经常会碰到这种情形界面上点了 Share 里的直接渲染链接结果弹出 Rendering failed. Error: No image renderer available/installed。不是权限问题也不是网络问题就是那个独立服务没跑起来、或者地址配错了。谁用得上做告警通知、要定期自动发报表图、做值班大屏截图归档、想把面板图嵌到工单系统里的人都绕不开它。它算 Grafana 生态里一个小而关键的配角平时没人提一旦要用配置细节还不少尤其是中文环境下的字体问题官方文档写得相当简略。下面从它的工作链路开始一层层拆。1. 先搞清楚 image render 的工作链路1.1 它为什么不再跟 Grafana 捆在一起发布早期的 Grafana 5.x、6.x 时代渲染插件是直接内置在 Grafana 进程里的装个插件就完事。后来官方把它拆出来做成独立服务原因其实很实在渲染引擎背后是一个完整的无头 Chromium这东西体积大、内存占用高、安全更新还特别频繁。把它塞进 Grafana 主进程意味着每次 Chromium 爆出安全漏洞你都得跟着升级整个 Grafana而且一个大面板渲染时可能吃掉好几百兆内存会直接影响 Grafana 本身的查询响应。拆开之后职责就清晰了Grafana 只管拼图表数据、发指令渲染服务只管开浏览器、画图、截图。两边通过 HTTP 通信可以分别扩容、分别重启、分别打补丁。代价是配置项多了一层链路里多了一个环节也就多了一个可能出错的地方。理解了这一点后面排查问题时思路会顺很多——凡是出白图、出空白页第一反应就该去想那个独立服务有没有正常工作而不是去 Grafana 里翻权限。还有一种老做法是在 Grafana 容器里装 grafana-image-renderer 插件然后通过[plugin.grafana-image-renderer]配置段来设定。这条路现在已经不推荐了社区反馈的问题主要是内存和进程管理尤其是在面板多、并发高的时候。新部署一律用独立服务的方式下面的实操也都围绕这种方式展开。1.2 一次渲染请求到底经历了什么很多配置错误的根源是没搞清楚请求的方向。完整链路是这样走的你在 Grafana 里触发一次渲染或者点开 /render 开头的链接Grafana 主服务收到请求后把渲染任务转发给渲染服务转发地址就是server_url渲染服务启动一个 Chromium 实例让浏览器去访问一个特定的 URL 来加载面板这个 URL 的域名和端口来自callback_url也就是说浏览器需要回过头来访问 Grafana浏览器加载完页面、等图表数据渲染完成之后截图并返回给 GrafanaGrafana 再把图片交给你。这里最容易被忽略的是第三步和第四步。渲染服务是代替你去访问 Grafana 的所以callback_url必须是从渲染服务容器内部能访问到的地址而不是你浏览器地址栏里的那个公网域名。举个最常见的错误Grafana 对外是https://grafana.example.com你在 callback_url 里也填了这个域名结果渲染服务容器里的 DNS 解析不了这个域名或者解析到了公网出口再绕回来被防火墙拦了最终表现就是图片全白、日志里全是超时。反过来server_url是 Grafana 主服务去访问渲染服务的地址所以它必须是从 Grafana 容器内部能访问到渲染服务的地址。在用 Docker Compose 的时候两个容器在同一个自定义网络里直接用服务名当主机名就行比如http://renderer:8081/render。我建议一开始就把这两个地址的语义记牢后面 80% 的图是白的问题都能靠这一条定位。还有一点server_url末尾要带/render这个路径而callback_url只写到根路径加斜杠就行。这个细节极其容易搞混我第一次配的时候就在这儿卡了半小时。2. 部署形态与版本匹配怎么选2.1 三种跑法的取舍渲染服务官方提供了几种跑法实际选起来主要看你的 Grafana 是怎么部署的。部署形态适用场景优点需要注意的点Docker 容器绝大多数场景尤其是 Grafana 本身跑在容器里开箱即用字体挂载方便升级换镜像即可容器间网络要通字体目录要单独挂裸机二进制Grafana 直接装在物理机或虚拟机上少一层容器延迟略低依赖要自己装Chromium 相关库容易缺K8s 独立 Deployment云原生环境有动态扩缩需求可以按渲染队列长度扩容资源限制要给够Chromium 容易被 OOM 杀我自己的建议很直接只要你的 Grafana 不是纯裸机部署一律用 Docker 容器。渲染服务对系统依赖的要求不低Chromium 跑起来需要一堆图形相关的共享库裸机装的话光是把依赖理顺就够折腾半天。容器镜像里这些都打好了你只需要关心网络和字体两件事。K8s 的场景要特别提醒一句Chromium 单个实例的内存占用经常在 300MB 到 800MB 之间波动取决于面板复杂度。如果你给 Pod 设置了比较紧的 memory limit很容易在渲染大面板时被内核直接干掉日志里看到的就是莫名其妙的容器重启。要么把 limit 放宽要么把并发降下来这个我在后面参数调优那节还会细说。2.2 版本兼容这张表必须看渲染服务和 Grafana 之间通过一个相对稳定的 HTTP 协议通信但版本之间还是有兼容性要求的。踩过的坑是高版本的 Grafana 配了太老的渲染服务接口对不上日志里会出现类似协议不匹配的报错。Grafana 版本建议的 image renderer 版本说明7.x3.0.x 及以上7.0 之后改为独立服务模式8.x3.4.x 及以上支持 PDF 多页、CSV 渲染9.x3.6.x 及以上支持渲染指标暴露、令牌鉴权完善10.x / 11.x最新的 3.x 稳定版建议直接跟最新稳定 tag这里有个实操建议镜像 tag 不要用latest虽然方便但哪天上游推了一个大版本你的环境可能在一次无意的docker compose pull之后就崩了。用具体的次版本号比如grafana/grafana-image-renderer:3.10.5这种升级的时候手动改版本号可控得多。还有一个细节就是渲染服务版本和 Grafana 版本并不是一一对应的别指望版本号数字能对上。判断标准是协议兼容官方发布说明里会写清楚支持哪个 Grafana 主版本区间。如果你的环境是内网离线下载镜像这一步要提前规划好grafana/grafana-image-renderer和grafana/grafana这两个镜像都得拉下来别只拉了一个。提示Upgrade 前先在测试环境验证一次完整渲染流程尤其是从 8.x 跨到 9.x 这种大版本跃迁鉴权方式和配置项名字都可能有变化。3. 完整落地Docker Compose 把渲染服务跑起来3.1 compose 文件与逐行解释直接给一份我实际在用的精简版 compose去掉业务相关的部分只留渲染链路必需的配置。version: 3.8 services: grafana: image: grafana/grafana:11.1.0 container_name: grafana restart: unless-stopped ports: - 3000:3000 environment: - GF_RENDERING_SERVER_URLhttp://renderer:8081/render - GF_RENDERING_CALLBACK_URLhttp://grafana:3000/ - GF_RENDERING_RENDERER_TOKENchange-this-token - GF_LOG_FILTERSrendering:debug volumes: - grafana-data:/var/lib/grafana depends_on: - renderer renderer: image: grafana/grafana-image-renderer:3.10.5 container_name: renderer restart: unless-stopped environment: - ENABLE_METRICStrue - AUTH_TOKENchange-this-token - RENDERING_MODEclustered - RENDERING_IGNORE_HTTPS_ERRORStrue volumes: - ./fonts:/usr/share/fonts/custom:ro ports: - 8081:8081 volumes: grafana-data:逐条说关键的几个点。GF_RENDERING_SERVER_URL末尾带/render这是渲染服务的入口端点不带的话 Grafana 会往根路径发请求然后拿到 404。GF_RENDERING_CALLBACK_URL用容器服务名grafana加端口因为渲染服务容器会通过 Docker 内置 DNS 解析这个名字走内部网络比走公网域名又快又稳。GF_RENDERING_RENDERER_TOKEN和渲染服务那边的AUTH_TOKEN必须完全一致这是用来防止别人直接调用你的渲染服务接口干坏事的。生产环境务必设一个够长的随机串不要偷懒用默认值或者空着。空着的话任何能访问到 8081 端口的人都可以让你的服务器跑渲染任务属于白送资源。RENDERING_MODEclustered表示用集群模式启动 Chromium这个模式在多个渲染请求之间复用浏览器实例启动更快、内存更省是目前的推荐值。RENDERING_IGNORE_HTTPS_ERRORS适用于你的 Grafana 用了自签名证书的情况不然渲染服务访问回调地址时会因为证书校验失败直接放弃。这个开关有安全含义只在内部可信网络里开。ENABLE_METRICStrue会让渲染服务在/metrics端点暴露指标如果你有 Prometheus强烈建议打开后面做容量规划和排障会轻松很多。体积上顺便提一句渲染服务的镜像本身不小加上 Chromium 一整套运行时解压后通常在 1.5GB 上下别在磁盘空间紧张的环境里硬塞。3.2 Grafana 侧配置项逐条拆解如果你用的是配置文件而不是环境变量等价配置长这样[rendering] server_url http://renderer:8081/render callback_url http://grafana:3000/ renderer_token change-this-token concurrent_render_request_limit 30环境变量和 ini 的对应关系是GF_RENDERING_前缀加上大写的配置项名下划线分隔。这几个配置项里concurrent_render_request_limit值得单独聊。它限制的是 Grafana 侧同时向渲染服务发出的请求数默认值 30。这个数字不是越大越好因为渲染服务那边真正能并行处理的 Chromium 实例数是有限的你在这边放开 30那边排队排成一条长龙反而会让单个请求的等待时间变长触发超时。我实测过的经验值是单实例渲染服务1 到 2 核、2GB 内存起步并发限制设在 8 到 12 之间比较舒服如果渲染队列经常堆积优先考虑横向加渲染服务实例而不是把这个数字往上抬。判断依据可以看渲染服务/metrics里的请求队列长度指标持续大于零就说明压力已经到顶了。注意renderer_token一旦设置Grafana 发出的每个渲染请求都会带上这个令牌渲染服务会校验。如果你的 Grafana 和渲染服务是分批升级的升级期间可能因为令牌字段名变化导致鉴权失败建议在维护窗口里一起重启。3.3 三个验证手段确认真的能用配完之后别急着去配告警先把渲染本身验证通否则后面出了问题你都不知道是哪一层。第一个验证直接打渲染服务的健康端点。在宿主机上执行curl http://localhost:8081/version正常会返回渲染服务的版本号和 Chromium 的版本信息。如果这一步不通说明端口映射或者容器本身有问题跟 Grafana 无关。第二个验证让 Grafana 自己渲染一张图。最省事的办法是在浏览器里拼一个渲染 URL形如http://你的grafana地址:3000/render/d-solo/dashboard-uid/slug?orgId1panelId2width1000height500fromnow-6htonow。能正常返回一张 PNG 就说明整条链路通了。这一步失败的话去翻 Grafana 日志里带rendering标签的行通常能看到渲染服务返回的具体错误。第三个验证检查字体。这一步看着多余但如果你后面要出带中文的图单独验证一次能省很多返工。做一个带中文标题的面板渲染出来看字是不是方块。# 在渲染服务容器里执行看看有没有中文字体被识别 docker exec -it renderer fc-list :langzh正常应该能列出若干条中文字体记录。如果输出是空的就到下一节去处理字体。4. 中文乱码是最高频的坑字体处理全流程4.1 方块字到底怎么来的渲染服务的官方镜像基于 Debian里面只装了 DejaVu 这类西文字体。中文在网页上如果没有对应的字体可以匹配Chromium 就会退化成画方框或者干脆不显示。所以你看到的中文全是方块图表里的中文标签消失本质是字体缺失不是编码问题也不是数据问题。这一点特别容易被误判。很多人第一反应是去查 Grafana 的字符集配置、查数据库排序规则、查 Grafana 的default_language设置查了一圈发现都没毛病最后才想起是渲染容器里没字体。我建议你把这条当成条件反射只要渲染结果里中文异常先查字体。还有一个变体是部分中文正常、部分方块。这通常是因为某些字体只覆盖了常用字库生僻字或者特定符号没被包含。解决办法是挂载一套覆盖范围更完整的字体或者同时挂载两套做互补。4.2 两种装字体的方法方法一挂载字体目录最轻量。在宿主机建一个fonts目录把.ttf或.ttc字体文件丢进去然后按前面 compose 里的写法挂到/usr/share/fonts/custom重启容器即可。mkdir -p ./fonts # 假设你已经有字体文件例如从系统字体目录复制一份 cp /usr/share/fonts/truetype/your-cjk-font/*.ttc ./fonts/ docker restart renderer # 刷新字体缓存 docker exec -it renderer fc-cache -fv挂载目录的时候记得加:ro只读避免容器意外修改宿主机的字体文件。挂载完一定要执行一次fc-cacheChromium 依赖系统的字体缓存缓存不刷新的话有时新字体不会被识别到。方法二自己构建镜像。如果你的环境对字体文件有统一管理要求或者需要在离线环境里批量部署写个 Dockerfile 更规范。FROM grafana/grafana-image-renderer:3.10.5 USER root COPY ./fonts /usr/share/fonts/custom RUN fc-cache -fv USER grafana注意最后要切回原来的非 root 用户官方镜像里渲染进程是用较低权限跑的用 root 跑 Chromium 反而可能起不来。提示字体文件属于有版权的资源企业内部使用前确认一下授权范围别直接把商业字体文件打进镜像分发。字体装好之后再用前面那条fc-list :langzh验证一遍然后重新渲染一个带中文的面板确认显示正常。我一般还会故意在面板标题里放几个生僻字和全角标点测一下兜底情况。5. 把渲染图塞进告警通知5.1 Email 通道Grafana 的统一告警里Email 类型的联系点是最容易带上图的。配置路径是 Alerting 菜单下的 Contact points新建或编辑一个 Email 联系点展开可选项会看到一个和截图相关的开关。不同版本的叫法略有差异8.x 里写的是附上面板截图9.x 之后挪到了可选设置里但意思是一样的。勾上这个开关之后触发告警时 Grafana 会把告警规则关联的那个面板渲染成图片作为附件或者内嵌图塞进邮件里。这里有个前提告警规则必须关联到一个具体的面板如果规则是纯表达式、没有面板 ID那渲染服务也不知道该画哪个面板图自然出不来。这一点在实际使用中很容易被忽略尤其是用 Terraform 或者配置文件批量建规则的场景。内嵌图和附件的区别也值得说一下。内嵌图用 CID 引用的方式在很多邮件客户端里会显示成一个小图标或者干脆不显示附件则更稳。如果你在钉钉、企业微信这类客户端里转发邮件内嵌图经常丢失所以我一般建议用附件的模式。5.2 Webhook 与 Alertmanager 通道走 webhook 的时候Grafana 会在推送的 JSON 里带一个图片地址字段通常叫imageURL或者类似的命名。接收端需要自己去拉这个地址才能拿到图片因为它是一个需要鉴权的渲染链接。你的接收服务要能访问到 Grafana 的地址并且带上正确的凭证。如果用的是外部 Alertmanager 接管的告警流程情况会再绕一层。Alertmanager 本身不负责渲染图片它只是把 Grafana 传过来的图片地址往下游转发。所以能不能带图取决于 Grafana 这一侧有没有配置渲染服务、规则有没有关联面板。我在一个项目里排查过邮件里没图的问题最后发现是告警规则被迁移到了另一套 Alertmanager但规则的dashboardUid和panelId标签在迁移脚本里被丢掉了导致渲染找不到面板。这类问题的排查思路是去看 Grafana 发出的原始通知 payload而不是去看接收端收到了什么。因为中间可能经过了好几层转发只有源头的数据才能说明问题出在哪。5.3 手动拼渲染 URL 的姿势除了告警自动带图很多时候你需要手动拼一个渲染链接比如嵌到自建报表系统、嵌到工单里。URL 结构是这样https://grafana.example.com/render/d-solo/dashboard-uid/slug\ ?orgId1panelId2width1000height500fromnow-6htonowtzAsia%2FShanghaiscale2对比一下d-solo和d前者只渲染单个面板后者渲染整个仪表盘。要做 PDF 就用d它会输出多页文档要单张图就用d-solo。from和to支持相对时间表达式now-6h、now-1d、now/d这些都能用写起来比时间戳方便很多。tz参数经常被忽略但对国内团队很重要——不指定的话渲染出来用的是 UTC图上横轴的时间会跟你的预期差八个小时值班的人对着时间点找日志会找错。scale参数控制的是设备像素比取值 1 到 4。默认是 1在高分屏上看着糊。设成 2 会输出两倍分辨率的图文件体积大概翻一倍多。我在做需要打印或者投屏的场景时会设到 2普通告警邮件里用 1 就够省流量也省渲染时间。6. 渲染参数的调优与性能实测6.1 超时和并发渲染超时是第二高频的问题。表现是偶发性的失败面板越复杂失败率越高。原因通常是面板里的查询本身慢加上 Chromium 加载页面的时间超过了渲染服务默认的超时阈值。渲染服务这一侧的超时可以通过启动参数调整也可以在请求 URL 里用timeout参数临时指定单位是秒。我的一般做法是先确认面板本身的查询在 Grafana 界面里多久能出结果然后在那个基础上留出 2 到 3 倍的余量作为渲染超时。比如一个面板查询要 8 秒出结果那渲染超时我会设到 30 秒左右太紧会把网络抖动也算进去。并发这块前面提过要点是在 Grafana 侧的concurrent_render_request_limit和渲染服务的实际处理能力之间找平衡。我的实测数据是双核两 G 的单实例稳定并发大概能处理 8 到 10 个中等复杂度的面板渲染再高就会出现队列堆积。想知道自己的环境能撑多少可以看/metrics里的请求处理耗时直方图和队列长度跑一段时间就有数了。内存方面有个经验值分享一个 Chromium 实例画一个带 20 个图表的面板内存峰值大概 500MB 上下面板里如果有热力图、地理图这类渲染成本高的可视化峰值会更高。做容量规划的时候按每并发 500MB 到 800MB 来算比较安全。6.2 分辨率与文件体积width和height决定了输出图片的基础尺寸。这里有个坑如果你设的尺寸和面板实际布局的比例差太多Chromium 会触发响应式布局结果就是你截图出来的图里面板的排列方式跟你在界面上看到的不一样有的行被折叠了有的图例跑到了奇怪的位置。我踩过一次把宽度设成 1600、高度设成 400本来想着做一个长条形的图结果面板自动切换成了单列布局几个并排的图垂直堆叠起来整个图变得又长又乱。后来老老实实按 16:9 的比例设1024×576 或者 1280×720 这类就正常了。文件体积方面一张 1000×500、scale 为 1 的典型面板图PNG 大概 100KB 到 300KB如果面板里有大面积渐变或者热力图能到 500KB 以上。如果要频繁发到钉钉、企业微信这类有消息体积限制的通道建议控制在 200KB 以内可以适当降低 width、scale 设成 1或者把面板里的复杂可视化精简一下。提示渲染服务返回的 PNG 是无损格式如果对体积特别敏感可以在你的接收端做一次有损转换压缩率通常能到 70% 以上而图表的可读性基本不受影响。7. 高频报错速查与两个真实案例7.1 报错对照表我把实际遇到过的、以及在社区里看到别人问得比较多的报错整理成一张表方便对照排查。报错或现象最可能的原因处理办法No image renderer available/installed渲染服务没跑起来或 server_url 配错先 curl 渲染服务的 /version再核对 server_url 是否带 /render图片是纯白的callback_url 不可达浏览器加载不到面板换成容器内部可达的地址检查网络策略图片显示 Rendering timed out面板查询慢超出渲染超时优化查询或调大超时时间中文变成方块渲染容器里没有中文字体挂载字体并执行 fc-cache部分面板元素缺失页面还没渲染完就截图了检查面板是否有异步加载的外部资源或调大超时渲染服务容器频繁重启内存不足被 OOM 杀掉放宽内存限制或降低并发401 / 403 相关报错renderer_token 和 AUTH_TOKEN 不一致两边的令牌改成完全相同的值并重启时间轴对不上没指定 tz 参数在渲染 URL 里加上 tzAsia/Shanghai提示数据源找不到面板引用的数据源在渲染上下文中解析失败检查数据源权限和面板迁移后的引用关系PDF 只出了一页用的是 d-solo 而不是 d换成 /render/d/ 开头的地址7.2 两个真实案例复盘案例一白图排查。有一次测试环境的渲染一直是白图日志里只有一行超时。我从渲染服务容器里手动执行curl去请求callback_url指向的地址发现 DNS 解析到了一个老的服务名那个服务早被下线了。原因是 callback_url 是从一份旧配置里复制过来的域名没错但指向的服务已经变了。改成容器服务名之后立刻恢复正常。这个案例说明白图这个现象一定要从网络可达性入手别去 Grafana 里翻权限设置。案例二图里中文正常但标点乱码。有个环境挂了一套中文字体之后汉字显示正常了但图上中文的全角标点和部分符号还是方块。查下来是挂的那套字体覆盖的字符集不全。解决办法是再补一套覆盖更全的字体两套同时挂着Chromium 会自动选能匹配上的那一个。这个案例提醒我验证字体的时候不能只看汉字标点、全角符号、特殊单位符号都要试一遍。再补一个小技巧如果你发现渲染失败率是间歇性的比如早晚高峰明显更高那基本可以判断是并发压力问题而不是配置问题。这种时候优先看渲染服务的队列指标别急着改配置改了也治标不治本。8. 关于鉴权和资源管控几个容易被忽视的点渲染服务的接口默认是不带鉴权的谁都能调。生产环境一定要设AUTH_TOKEN而且这个 token 只应该在内网使用不要把 8081 端口暴露到公网。我见过有环境为了图省事把渲染服务直接映射到公网结果被人拿去跑批量截图把机器内存打满最后连 Grafana 主服务都一起挂了。另外一个容易忽略的是渲染服务和 Grafana 之间的网络策略。如果你用的是 K8s 的 NetworkPolicy 或者云上的安全组要确保两者之间 8081 和 3000 两个方向的流量都放通。渲染是双向通信只放通一个方向会出现那种能提交任务但拿不到图的诡异状态。资源管控方面建议给渲染服务单独设一个内存上限然后让它自己重启而不是跟 Grafana 挤在同一个节点上抢资源。渲染任务的特点是有明显的波峰告警集中触发的时候会瞬间涌进来一批请求如果没有隔离Grafana 的查询响应会跟着变慢影响所有人。我在实际运维中的体会是把渲染服务当成一个独立的、可有可无的旁路组件来对待它挂了不影响 Grafana 主功能但一旦挂了你得能第一时间知道。给它的/metrics端点和容器重启次数各配一条告警基本就够用了。这个给它自己配告警的小闭环比事后手动排查省事太多。最后分享一个我在多次部署里固定下来的检查顺序每次新环境上线都照着走一遍基本不会漏先 curl 渲染服务版本端点再渲染一个英文面板确认链路通然后挂字体渲染一个中文面板最后去告警通道里触发一次真实告警看图片有没有带上去。四步走完这个功能就算真的落地了。