霁风麦穗 發表於 2025-9-30 17:57:00

使用openresty+lua来实现grafana中自动切换实时群集/历史群集对应的vmselect

<p><strong><font size="1" color="gray">作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢!</font></strong></p>
<ul>
<li><font size="1" color="gray">cnblogs博客</font></li>
<li><font size="1" color="gray">zhihu</font></li>
<li><font size="1" color="gray">Github</font></li>
<li><font size="1" color="gray">公众号:一本正经的瞎扯</font><br>
<img src="https://img2022.cnblogs.com/blog/1457949/202202/1457949-20220216153819145-1193738712.png"></li>
</ul>
<hr>
<p>我曾设计了这样的 VictoriaMetrics 中的实时群集和历史群集:</p>
<p><img src="https://img2024.cnblogs.com/blog/1457949/202509/1457949-20250930142231478-768425136.png"></p>
<p>see: deploy_VictoriaMetrics_cluster</p>
<p>期待的效果是:</p>
<ul>
<li>实时群集存储最近 7 天的数据,保障足够快足够可靠,提供告警查询和当前的系统监控。
<ul>
<li>为了保障实时群集的稳定性,通过牺牲存储时间来减少存储的数据</li>
</ul>
</li>
<li>历史群集提供长周期的(例如半年),且降采样的数据的存储
<ul>
<li>以低成本的方式提供长周期的数据查询</li>
</ul>
</li>
</ul>
<p>部署完成后,我部署了一个新的 vmselect 节点,来连接到所有的实时群集和历史群集的vmselect节点上。<br>
可是当我执行如下的查询时,发现数据是正确数据的两倍:</p>
<pre><code class="language-promql">sum by (path) (increase(http_request_total{job="myApp"}))
</code></pre>
<p>很明显, sum() 时,把实时群集和历史群集中同样的 time series 上的值加了两次。<br>
研究过 dedup 的源码,并未发现明显问题。<br>
无奈,只能通过别的办法绕过去。</p>
<p>于是想到:如果能够自动发现用户查询的时间范围,当用户查询七天以内时转发到实时群集,而查询超过七天就转到历史群集,那么就不用把历史群集和实时群集混合在一起了。<br>
下面是这个思路的详细解决办法:</p>
<p><img src="https://img2024.cnblogs.com/blog/1457949/202509/1457949-20250930143747833-906108121.png"></p>
<p>部署 openresty 的 deployment 的代码如下:</p>
<pre><code class="language-yaml"># openresty.yaml

# nginx 的配置文件放在 configMap 中
apiVersion: v1
kind: ConfigMap
metadata:
name: openresty-config
data:
# nginx 的 配置文件
nginx.conf: |
    worker_processes1;

    events {
      worker_connections1024;
    }

    http {
      access_log /dev/stdout;
      error_log /dev/stderr warn;

      lua_package_path "/usr/local/openresty/nginx/lua/?.lua;;";

      upstream realtime {
            server vmselect-realtime:8481;# 实时群集的 vmselect
      }

      upstream historical {
            server vmselect-historical:8481;# 历史群集的 vmselect
      }

      server {
            listen 8401;

            location /select/0/prometheus/api/v1/query_range {# 核心是修改 query_range 这条 api
                content_by_lua_file /usr/local/openresty/nginx/lua/router.lua;
            }
            # 其它所有路径默认走 realtime
            location / {
                proxy_pass http://realtime;
            }

            location @toRealtime {
                proxy_pass http://realtime;
            }

            location @toHistorical {
                proxy_pass http://historical;
            }
      }
    }

# lua 脚本的代码
router.lua: |
    -- 时间值要支持三种格式:数值,字符串,grafana中的简写
    local function parse_start(val)
      if not val then return nil end
      local num = tonumber(val)
      if num then
            if num &gt; 1e12 then
                return math.floor(num / 1000)
            else
                return num
            end
      end

      local year, mon, day, hour, min, sec =
            val:match("^(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)")
      if year then
            return os.time({
                year = tonumber(year),
                month = tonumber(mon),
                day = tonumber(day),
                hour = tonumber(hour),
                min = tonumber(min),
                sec = tonumber(sec)
            })
      end

      local num, unit = val:match("^([%-]?%d+)()$")
      if num and unit then
            num = tonumber(num)
            local seconds = 0
            if unit == "s" then seconds = num
            elseif unit == "m" then seconds = num * 60
            elseif unit == "h" then seconds = num * 3600
            elseif unit == "d" then seconds = num * 86400
            elseif unit == "w" then seconds = num * 7 * 86400
            end
            return ngx.time() + seconds
      end

      return nil
    end

    local args = ngx.req.get_uri_args()
    local is_post = (ngx.req.get_method() == "POST")
    local post_args = {}

    if is_post then
      ngx.req.read_body()
      post_args = ngx.req.get_post_args()
      for k,v in pairs(post_args) do
            args = v
      end
    end

    local start = parse_start(args["start"])
    local now = ngx.time()
    local days = 7-- 这里设定一个七天的范围:七天以内在实时群集查询,超过七天在历史群集查询
    local n_days_ago = now - days*24*3600
    local step = "300s"-- 当查询历史群集时,使用历史群集的降采样后的间隔,即 5 分钟

    if start ~= nil then
      if start &gt; n_days_ago then
            return ngx.exec("@toRealtime")
      else
            if is_post then
                post_args["step"] = step
                local body_tbl = {}
                for k,v in pairs(post_args) do
                  table.insert(body_tbl, ngx.escape_uri(k) .. "=" .. ngx.escape_uri(v))
                end
                local new_body = table.concat(body_tbl, "&amp;")
                ngx.req.set_body_data(new_body)
            else
                args["step"] = step
                ngx.req.set_uri_args(args)
            end
            return ngx.exec("@toHistorical")
      end
    else
      return ngx.exec("@toRealtime")
    end
---
# 这里是部署 openresty 的 deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: openresty
spec:
replicas: 1
selector:
    matchLabels:
      app: openresty
template:
    metadata:
      labels:
      app: openresty
    spec:
      containers:
      - name: openresty
          image: openresty/openresty:1.27.1.2-alpine
          ports:
            - containerPort: 8401
          volumeMounts:
            - name: config
            mountPath: /usr/local/openresty/nginx/conf/nginx.conf
            subPath: nginx.conf
            - name: config
            mountPath: /usr/local/openresty/nginx/lua/router.lua
            subPath: router.lua
          command: ["/usr/local/openresty/bin/openresty"]
          args: ["-g", "daemon off;", "-c", "/usr/local/openresty/nginx/conf/nginx.conf"]
      volumes:
      - name: config
          configMap:
            name: openresty-config
---
apiVersion: v1
kind: Service
metadata:
name: openresty
spec:
selector:
    app: openresty
ports:
    - protocol: TCP
      port: 8401
      targetPort: 8401
type: ClusterIP
</code></pre>
<p>通过命令行部署:</p>
<pre><code class="language-bash">KUBECONFIG=~/my-test-k8s.yaml kubectl apply -f ./openresty.yaml -n my-namespace
</code></pre>
<p>通过 grafana 创建新的数据源,或者可以使用命令查询:</p>
<pre><code class="language-bash">curl -G "http://127.0.0.1:8401/select/0/prometheus/api/v1/query_range?start=-7d" -v
</code></pre>
<p>可以通过 header <code>X-Server-Hostname</code> 观察数据由哪个服务返回。</p>
<p>Have Fun. 😃</p><br><br>
来源:https://www.cnblogs.com/ahfuzhang/p/19121084
頁: [1]
查看完整版本: 使用openresty+lua来实现grafana中自动切换实时群集/历史群集对应的vmselect