资讯动态

Prometheus+Grafana监控本地LLM服务:指标、告警与日志联动

发布时间:2026/10/15 7:17:55 来源:尧图企业网站定制
先说明一个经常被忽略的事实本地跑开源LLM服务的人大部分时间都在调prompt、换模型权重、调采样参数却很少有人认真对待“这个服务现在到底健不健康”这件事。我自己在本地环境维护过一个长期运行的LLM推理服务一开始完全是黑盒直到某天一个业务方反馈生成速度突然从每秒40个token掉到8个我才意识到显存可能早就被打满了队列里堆着几十个请求而日志文件里只有一行没有时间戳的报错。从那时候起我开始认认真真做监控最后落地成方案就是标题里的组合用Prometheus Grafana监控本地LLM指标。这篇文章会完整记录我搭建这套监控体系的过程包括指标怎么设计、埋点怎么写、告警怎么配、坑在哪里适合正在本地或内网环境部署LLM服务、想让可观测性不再裸奔的开发者参考。这套方案解决的问题很具体怎么知道GPU显存是否逼近上限、推理延迟是否在劣化、token吞吐有没有突然下降、服务有没有在默默报错、日志里能不能快速定位到异常请求。它不依赖任何外部平台所有组件都跑在本地数据也在本地对私有化部署很友好。如果你是刚接触监控的新手跟着文章的操作一步步来也能搭起来如果你已经有一定经验那可以重点看后面我列的那些容易踩的坑。1. 监控方案的整体设计与思路拆解1.1 本地LLM到底需要关注哪些指标先把问题定义清楚。传统Web服务的监控重点是CPU、内存、QPS、请求错误率但本地LLM服务还多了几个特有的维度GPU显存占用、显存带宽利用率、推理时延、生成token的速率、KV Cache的占用变化、当前排队请求数。这些指标缺一个出问题的时候就很难定位。举个例子显存占用飙升这个事在LLM服务里比普通Web应用敏感得多。模型权重本身就吃掉一部分显存每个请求进来又会动态分配KV Cache空间。如果某个时刻并发请求多了显存可能会被瞬间打满然后直接触发CUDA Out of Memory进程崩溃重启。这种崩溃在日志里可能就是一行Traceback并不好发现。但如果我提前在Grafana上建了一个显存使用率的面板配合一个“超过90%持续5分钟”的告警我就能在业务方感知之前知道危险要来了。还有一个容易被遗漏的指标是生成token的速率也就是吞吐。这个数能非常直观地反映推理服务的质量。模型从A模型换成B模型之后吞吐有没有下降加了某个量化方案之后延迟是不是变高了这些都需要用数字说话。日志可以告诉你一次请求用了多久但它没法回答“整体趋势是在变好还是变坏”而PromQL里一行rate函数就能画出趋势线。所以我的整体思路是三层指标系统层CPU、内存、磁盘、网络、GPU层显存使用、GPU计算利用率、温度、推理服务层请求数、延迟、token数、错误数、队列长度。三层都接入Prometheus然后统一在Grafana里看。日志作为辅助数据单独接入一套日志采集链路但和指标在同一个Grafana界面里串联查看。1.2 Pull模型和三层采集架构Prometheus采用的是Pull模型也就是监控中心主动去各个被监控对象拉取指标而不是被监控对象主动上报。这个设计对本地场景非常友好因为我只需要在每个组件旁边开放一个HTTP接口Prometheus按固定间隔去抓取就行。它天然避免了“上报端挂了导致数据断流”的问题而且配置起来非常直观告诉Prometheus往哪个IP的哪个端口去拉数据就可以。我的采集架构是这样拆的系统层运行一个node级别的exporter暴露CPU、内存、磁盘IO、网络流量等基础指标。GPU层运行一个GPU exporter读取显卡驱动的指标并暴露出来重点看显存、利用率。推理服务层在LLM服务进程内部直接暴露一个/metrics接口把请求数、延迟分布、token计数这些业务指标吐出来。这个接口可以和服务本身跑在同一个进程里用独立端口启动也可以走单独的exporter进程取决于代码怎么组织。日志层应用所有业务日志以JSON格式持久化然后由一个轻量级的日志采集器把它们转发到日志存储日志存储本身也提供查询接口。为什么采用分层而不是全部塞进一个exporter因为每个层级的生命周期不同。系统层和GPU层基本是常驻的跟业务进程没有耦合推理服务层则必须和业务代码在一起才能拿到内部的队列长度和延迟数据。拆开之后如果LLM服务重建或升级镜像里的Prometheus配置完全不用改因为GPU层和系统层还在继续运行。1.3 为什么是Prometheus Grafana而不是其他监控组合我在选型时也考虑过直接上完整的日志搜索平台比如轻量的ELK组合但最后还是回到了Prometheus Grafana。原因很实在这套组合在指标监控上的生态最成熟而且组件少启动成本低。Prometheus本身是一个时序数据库存储的就是“时间戳标签数值”特别适合画趋势曲线和做阈值告警。它内置了PromQL查询语言在做多维度聚合时极其方便。比如我想看所有模型里每种模型的p95延迟一句histogram_quantile配合sum by (model)就能算出来。这种查询在日志系统里实现起来反而很别扭。Grafana则解决了两件事一是可视化把Prometheus里的数据变成一张张面板拖拽一下就能组合成指标大盘二是告警从数据源直接发告警到通知渠道。而且它不仅仅是Prometheus的专用前端还能同时接日志数据源。这样我可以把“最近5分钟的错误日志”和“最近5分钟的延迟曲线”放在同一个大屏上排查问题时不用来回切工具。当然这套方案也有短板。它对高基数数据的支持一般如果你的标签里带着每个请求唯一的IDPrometheus会爆炸它也不适合存长周期的原始日志日志的全文检索还是要交给专门的日志系统。但就监控“本地LLM指标”这件事它已经足够强。2. 核心细节解析与实操要点2.1 指标类型和PromQL的基础玩法理解Prometheus之前先要过一遍它最基础的四种指标类型因为后面所有埋点和查询都围绕这四种类型来。Counter是计数器只能增加不能减少。比如总共处理了多少请求、生成了多少token。它适合统计总量但在监控时通常不会直接看总数而是用rate()函数算每秒变化率。例如表达式rate(llm_requests_total[5m])就是过去5分钟的每秒请求数。Gauge是瞬时值可以增加也可以减少。比如显存当前使用量、当前排队请求数、GPU利用率。这种直接查当前值就行。Histogram是直方图用于观测值的分布。它会把观测值放进预设的桶里每个桶是一个Counter然后在查询时通过直方图近似算法算出百分位数。LLM推理延迟这种“平均值往往很有欺骗性”的指标最适合用Histogram来监控。比如histogram_quantile(0.95, sum(rate(llm_inference_seconds_bucket[5m])) by (le))的意思就是过去5分钟内95%的请求延迟都低于某个秒数。Summary和Histogram类似但有本质区别Summary计算分位数是在客户端完成的客户端维护分位数的近似统计然后直接把分位数结果暴露出来Histogram则是在查询端计算的。客户端Summary的问题是多个实例聚合时无法做求和而Histogram可以。所以我推荐延迟和token数这类需要聚合的指标一律用Histogram。2.2 为本地推理服务埋点一份可用的Python示例我先展示一个不依赖具体模型框架的埋点示例核心思路是把指标埋点打到推理服务的主流程里。假设我们的推理服务是Python写的只需要安装prometheus_client这个库就可以。from prometheus_client import start_http_server, Counter, Gauge, Histogram REQUEST_COUNT Counter( llm_requests_total, Total requests received, [model, status] ) TOKEN_COUNT Counter( llm_tokens_total, Total tokens generated, [model] ) QUEUE_SIZE Gauge( llm_queue_size, Current number of queued requests ) INFERENCE_LATENCY Histogram( llm_inference_seconds, Inference latency in seconds, buckets(0.1, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0) ) GPU_MEM_USED Gauge( gpu_mem_used_bytes, GPU memory used in bytes )然后在处理请求的代码里把计数器放在执行前后的对应位置。重点是要记得让这些指标在一个独立的HTTP端口暴露出来和业务端口分开否则抓取接口的信号太密集时会抢业务流量。from prometheus_client import generate_latest, CONTENT_TYPE_LATEST from http.server import HTTPServer, BaseHTTPRequestHandler class MetricsHandler(BaseHTTPRequestHandler): def do_GET(self): if self.path /metrics: data generate_latest() self.send_response(200) self.send_header(Content-Type, CONTENT_TYPE_LATEST) self.end_headers() self.wfile.write(data) else: self.send_response(404) self.end_headers() def start_metrics_server(port9100): server HTTPServer((127.0.0.1, port), MetricsHandler) server.serve_forever()这样做的好处是即使业务进程挂了只要这个指标服务还在运行至少能告诉你进程已经不可用了。不过大多数场景更推荐把start_http_server(9100)直接丢进业务进程的主线程启动因为如果进程都挂了指标接口也消失Prometheus就拉不到呈现出Target Down的状态这也是一种信号。下面再看埋点怎么插进推理流程def inference(prompt, modeldefault): QUEUE_SIZE.inc() try: start time.time() output model.generate(prompt) latency time.time() - start INFERENCE_LATENCY.observe(latency) REQUEST_COUNT.labels(modelmodel, statussuccess).inc() TOKEN_COUNT.labels(modelmodel).inc(len(output.split())) return output except Exception as e: REQUEST_COUNT.labels(modelmodel, statuserror).inc() raise finally: QUEUE_SIZE.dec()这里的observe调用就是把延迟数值丢进直方图里之后Prometheus那边才能查到分位数。注意labels的使用模型和状态是合理的标签维度千万不要把请求ID或者用户输入内容当标签否则标签基数会高到让Prometheus内存爆掉。这个坑在后面我会专门展开。2.3 日志的采集与指标联动指标回答的是“是什么”和“有多严重”的问题日志回答的是“具体是哪条请求”和“为什么”。标题里既然带了“日志”就不能只把指标弄完就跑。我的日志方案是应用打结构化JSON日志每条日志带上一批关键字段。{ timestamp: 2025-01-15T20:31:22.123Z, level: info, request_id: a8f3c-1234, model: local-demo, prompt_tokens: 32, completion_tokens: 156, latency_ms: 2345, status: success, gpu_mem_used_mb: 6821 }这样一段日志本身就能用来做排查比如某次请求耗时特别长我就能在日志里看它分配了多少token。而且有一个非常实用的联动方式当Prometheus告警触发“推理延迟P95超过5秒”时我可以立刻切到日志查询搜出那段时间所有延迟大于某阈值的请求ID再结合请求上下文定位原因。我这里用的是Grafana的日志数据源和Promtail采集器这套组合。Promtail会监听日志文件的增长把每一条JSON日志转成一个可查询的文档。配置大概是这样scrape_configs: - job_name: llm-service-logs static_configs: - targets: [localhost] labels: job: llm-service-logs __path__: /var/log/llm-service.log在Grafana的Explore页面里可以用LogQL查询指定时间段内的错误日志比如只查statuserror的记录或者按模型标签分组统计错误数。从实际经验看日志和指标在同一个大屏上展示排查效率比单独看一边高一倍都不止。2.4 Grafana面板与查询思路Grafana面板设计的核心是一层一层的往下钻而不是把所有指标堆在一张图上。我的本地LLM大盘会分成五行第一行是请求概览包含每秒请求数、每秒生成token数、当前排队请求数。这些直接反映服务整体压力。第二行是延迟分析包含平均延迟、P95延迟、P99延迟。延迟分布比单看平均值更能暴露尾部风险。第三行是GPU资源包含显存使用量、显存利用率、GPU计算利用率。这几块决定瓶颈到底在显存容量还是算力。第四行是错误与日志包含错误率曲线、错误日志数量、最近错误日志的简要表格。第五行是系统资源包含CPU、内存、磁盘IO。每张面板对应的查询举一个例子。每秒请求数sum(rate(llm_requests_total[5m])) by (model)P95延迟histogram_quantile(0.95, sum(rate(llm_inference_seconds_bucket[5m])) by (le, model))显存使用率(gpu_mem_used_bytes / gpu_mem_total_bytes) * 100这里我建议把model作为变量在Grafana顶部做一个下拉选择这样一张面板能同时复用多模型场景。具体做法是新建一个Dashboard变量Query直接用label_values(llm_requests_total, model)然后在面板查询里用$model引用变量就好。3. 实操过程与核心环节实现3.1 准备环境与组件清单在开始动手前先把组件清单和端口规划好。表格如下组件作用启动端口数据来源Prometheus指标存储与告警计算9090主动抓取系统Exporter采集CPU/内存/磁盘9101宿主机GPU Exporter采集显存/利用率9835显卡驱动LLM Service推理服务本身8000业务端口LLM Metrics推理业务指标接口9100服务内嵌日志采集器读取并转发日志固定组件进程日志文件Grafana可视化和告警UI3000多种数据源如果没有专用的GPU Exporter也可以让GPU显存使用量从业务进程内部读取然后通过自定义指标直接暴露。我两种都试过建议有条件还是用专用exporter因为它还会额外给出GPU利用率、温度、功耗等数据对判断卡点很有用。3.2 编写Prometheus采集配置并启动Prometheus的配置文件一般叫prometheus.yml。第一次配置时只需要让三组targets生效。global: scrape_interval: 15s evaluation_interval: 15s scrape_timeout: 10s scrape_configs: - job_name: local_llm_metrics static_configs: - targets: [127.0.0.1:9100] - job_name: node_exporter static_configs: - targets: [127.0.0.1:9101] - job_name: gpu_exporter static_configs: - targets: [127.0.0.1:9835]scrape_interval是抓取频率。对于LLM这种服务15秒足够。如果调成5秒会稍微灵敏但会增加存储量。evaluation_interval是告警规则被评估的频率也保持一致就行。scrape_timeout不能比抓取间隔还大否则来回叠加会产生堆积。保存配置后用prometheus --config.fileprometheus.yml启动等几秒后打开Prometheus自带的页面端口9090在Status - Targets里能看到三组target的状态。理想状态下都是UPHTTP抓取状态码是200。这里有一个很常见的坑抓取的IP别配成localhost因为Prometheus容器或服务如果跑在不同网络环境里localhost指向的可能不是同一台机器。本地直接裸机运行的时候用127.0.0.1没问题如果用了容器编排目标地址就要写宿主机实际IP。3.3 配置Grafana数据源和DashboardGrafana启动后浏览器打开3000端口默认登录后第一件事是添加数据源。数据源类型选PrometheusURL填http://127.0.0.1:9090点击“Save Test”看到成功提示就说明链路已经通了。接着开始创建Dashboard。我建议不要一上来就尝试一次画二十张面板从最关心的三张开始每秒请求数、P95延迟、显存使用率。前两张能让你知道服务忙不忙、慢不慢后一张能让你知道GPU有没有爆显存风险。添加面板时选择Prometheus数据源把查询语句填进PromQL输入框。面板右侧可以设置图例和单位比如延迟的单位选s秒显存单位选bytes。如果查询结果带有多条时间序列比如按模型分开的可以点击“Legend”输入{{model}}作为图例名称。如果你想直接复用别人分享的Dashboard也可以导入JSON模板但导入后一定检查里面的数据源名是否和本地一致。很多模板里写的是${DS_PROMETHEUS}这种变量导入时Grafana会提示你绑定。最后别忘记给Dashboard配一个“快速筛选”变量。在Dashboard设置里新增一个叫model的变量Query类型选Label values标签选model这样顶部就能切换不同模型所有面板都会自动随模型刷新。实测下来多模型共享一套大盘的体验比每模型一个大盘好很多。3.4 设置告警从指标异常到通知触达监控最终要落到告警否则不可能24小时盯着大屏。Grafana 8之后内置了告警功能直接在Dashboard面板上就可以配置告警规则也可以独立在Alerting页面维护。我配置的第一个告警是显存高水位规则如下gpu_mem_used_bytes / gpu_mem_total_bytes 0.9触发条件可以设置持续5m意思是连续5分钟超过90%才告警避免瞬时抖动误报。严重级别我设为warning。第二个告警是排队长度表达式sum(llm_queue_size) 10持续2分钟就告警。第三个告警是错误率表达式sum(rate(llm_requests_total{statuserror}[5m])) / sum(rate(llm_requests_total[5m])) 0.05意思是5分钟内错误率超过5%。这个阈值对于LLM服务来说已经很高正常应该在1%以下。告警通知渠道我建议先用一个大路货Webhook。很多办公通讯软件都支持Webhook机器人配置好URL以后Grafana会把告警消息POST过去。这样当我在工位上摸鱼时手机也能收到推送。通知的模板里会包含告警名称、当前值、标签这些信息已经足够定位到具体是哪个指标、哪台机器。需要注意告警规则里用到的指标如果带有model标签且你又希望按不同模型分别触发表达式里要保留标签比如sum by (model) (llm_queue_size) 10否则多模型混合求和后一个模型排队多也看不出来。4. 常见问题与排查技巧实录4.1 数据拉不到或Target显示Down这是新手最频繁遇到的问题。Prometheus的Targets页面显示DOWN排查顺序是先确认本机端口有没有监听curl http://127.0.0.1:9100/metrics。如果curl成功但Prometheus抓取失败大概率是地址写错了或者Prometheus运行在隔离网络里。如果curl失败说明目标服务没起来或端口不对去查那个服务自己的启动日志。还有一个容易被忽略的点是scrape_timeout。如果某个metrics接口因为内部逻辑卡顿导致抓取超时Prometheus会把这次抓取标记为失败。解决办法是把接口本身做轻量化不要在生成metrics时实时调用重型函数。我在本地排查过一次非常诡异的问题指标接口能被浏览器正常打开但Prometheus一直显示DOWN。后来发现是这台机器上同时跑着两个进程另一个旧进程抢占了同一个端口浏览器看到的是旧进程的响应而Prometheus抓取的响应格式不对。解决方式就是找到所有占用端口的进程逐个核对命令。4.2 指标为0或者是碎片Targets显示UP但Grafana里指标一直是0或者曲线断断续续。这种情况通常有三个原因。第一指标确实还没产生数据。比如Counter类型的llm_requests_total如果你的服务最近没有新增请求rate算出来就是0这是正常的。可以先用空查询llm_requests_total看原始值如果有数值但rate是0那是窗口内确实没有增量。第二代码里labels()使用不一致。比如埋点时用了labels(modeldefault)查询时用的{model!}标签名对应不上就会查不到。我用promtool check metrics检查过配置也手动跑到Prometheus的Graph页面翻看指标名称是否完全正确。第三时间窗口和重启导致的断崖。Counter是单调递增的但一旦进程重启Counter会重置为0。如果Prometheus的抓取间隔刚好错过了某些时间段计算出来的rate可能偏低甚至为0。这不是配置错误而是Counter的固有特性。为了让速率曲线更平滑查询窗口可以拉长到[10m]甚至[30m]。如果服务频繁重启那首先该解决的是稳定性问题而不是只改查询窗口。4.3 标签设计不当带来的坑我在埋点示例里反复强调不要用请求ID做标签这里具体展开说为什么。Prometheus本质上对高基数非常敏感每个标签组合都会在内存和磁盘里占一条时间序列。请求ID这种每毫秒都不重复的标签会让序列数量爆炸式增长。一旦涨到几十万条Prometheus的查询和存储都会显著劣化严重的会直接把进程内存撑爆。正确的标签应该包含模型名、请求状态、错误类型这种维度数量级通常是固定的几个到几十个。比如method、model、status、endpoint就够了。那些需要逐条查询的信息应该放在日志里而不是做进指标标签。类似的坑还有把Prometheus label当作“注释”来用比如往里面塞一条很长的错误信息。这会让标签值长度失控浪费存储而且查询时也难看。真需要看错误详情去日志系统里按request_id查就好了。4.4 显存和GPU指标采集的特殊问题GPU指标采集的坑比较特殊。很多GPU Exporter在读取显存数据时依赖默认的显卡设备。如果你的机器有多个GPU或者运行环境中GPU设备的索引不是从0开始exporter可能报错或者只显示出一张卡。解决方法是查看exporter的错误日志一般在启动时会有设备列表。然后在配置里指定需要监控的设备索引。另外不要以为gpu_mem_total_bytes是个常量就忽略它。不同显卡的显存大小不一样如果监控的机器不止一台用绝对字节数做比较其实没意义要转成百分比。而百分比指标最好在PromQL里动态计算不要直接在埋点端算好这样可以保留原始字节数方便后续做容量分析。还有一个容易被忽略的点是显存使用和进程的绑定关系。同一个显卡上如果同时跑了多个推理进程每个进程看到的显存使用量可能都是“整个卡的用量”而不是自己占用的部分。这样的话两个服务上报的gpu_mem_used_bytes其实是同一份数据聚合时不注意就会重复计算。我习惯在PromQL里用max而不是sum来聚合GPU显存因为物理设备的用量在同一张卡上只能取最大值。4.5 日志时间与指标时间不同步当我把日志和指标放在同一个Grafana大盘后发现一个很别扭的问题日志里面的时间戳和Prometheus抓取到的时间戳总是对不上。原因通常是应用日志里写的是本地时间而Prometheus和Grafana默认按UTC处理。如果机器时区是东八区日志时间显示是UTC8的Grafana查询面板用的是UTC看起来就会差8个小时。解决方式有两种。一是日志格式里强制使用UTC时间戳比如上面JSON示例里的Z结尾这是最推荐的做法因为任何时区的人看这条日志都能快速转换。二是Grafana里把默认时区改成浏览器的本地时区但如果你需要导出数据给别人还是会撞上时区问题。我在日志采集器配置里加了一个时间字段重命名把日志里的本地时间字段转成采集时间但最后还是回到源头让应用统一打UTC。宁可日志里多一个local_timestamp辅助字段也不能让主时间字段的语义含糊。我个人在实际操作中还有一个体会很多本地LLM服务刚开始监控时会发现问题不是没有指标而是指标太多。GPU利用率、显存、请求数、延迟全堆在一起时你根本不知道该先看哪个。我的建议是先盯三样显存使用率、P95延迟、错误率。这三样一个代表资源一个代表性能一个代表质量。等这三张图稳定跑一段时间你自然会对自己的服务产生直觉。之后再慢慢加队列长度、token吞吐、温度、功耗这些外围指标。最后再分享一个小技巧给告警规则都加一个for字段。一开始我图省事把所有告警都设成for: 0m结果显存只是抖了一下就触发通知一天能收到二十条半夜爬起来看往往又是正常的。后来统一改成5分钟窗口噪音立刻降了下来。本地LLM服务的指标波动通常不是瞬时故障持续5分钟以上的偏离才值得被叫醒。这个思路跟指标怎么设计同等重要。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑