柴少的官方网站 技术在学习中进步,水平在分享中升华

测试环境按需动态加载(二)

#紧接上文:www.51niux.com/?id=326  前面以及介绍了pod生命周期在consul中的操作,nginx后端跟consul的关联以及相关的表创建,下面进入到第二部分流量侧操作

一、初始化集群域名的创建

#前面我们已经首先介绍了集群pod在nginx的配置文件中如何存在可以让域名通过consul获取到pod的信息进而调用到pod集群的http接口。

1.1 创建模版文件

# ls  /opt/web/pod_consul/template

location.conf  location_upstream_demand.conf  location_upstream_stable.conf  service.conf

# cat /opt/web/pod_consul/template/service.conf

include /opt/soft/nginx/conf.d/location/Domain_Name/*.location_upstream.conf;

server {
    listen 80;
    listen 443 ssl;
    server_name Domain_Name;
    
    # 强制HTTPS时开启
    if ($scheme != "https") {
        return 301 https://$host$request_uri;
    }
  
    ssl_certificate      certification/test.com.pem;
    ssl_certificate_key  certification/test.com.key;
    ssl_session_cache shared:SSL:10m; ## size suggest: 10m,50m
    ssl_session_timeout  1d; # time suggest: 10m,1d
    ssl_prefer_server_ciphers on;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
    
    include /opt/soft/nginx/conf.d/cors;
    server_tokens off;
    charset utf-8;

    include /opt/soft/nginx/conf.d/location/Domain_Name/*.location.conf;

    access_log /opt/log/nginx/Domain_Name/Domain_Name_access.log main;
    error_log /opt/log/nginx/Domain_Name/Domain_Name_error.log error;

}

# cat /opt/web/pod_consul/template/location.conf

location / {
    proxy_next_upstream http_502  error timeout invalid_header;
    proxy_pass http://Domain_Name_pool;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto  $scheme;
}

# cat /opt/web/pod_consul/template/location_upstream_demand.conf

upstream Domain_Name_pool {
  server 127.0.0.1:11111 down;
  upsync 192.168.1.101:8500/v1/kv/upstreams/Env_Name/demand/Req_Id/Cluster_Name upsync_timeout=5s upsync_interval=500ms upsync_type=consul strong_dependency=off;
  upsync_dump_path /data/nginxconf/upstream/Cluster_Name-Env_Name-Req_Id.conf;
  include /data/nginxconf/upstream/Cluster_Name-Env_Name-Req_Id.conf;
}

# cat /opt/web/pod_consul/template/location_upstream_stable.conf  #稳定环境和需求环境kv路径不一样所以upstream模版是两个不同文件

upstream Domain_Name_pool {
  server 127.0.0.1:11111 down;
  upsync 192.168.1.101:8500/v1/kv/upstreams/Env_Name/stable/Cluster_Name upsync_timeout=5s upsync_interval=500ms upsync_type=consul strong_dependency=off;
  upsync_dump_path /data/nginxconf/upstream/Cluster_Name-Env_Name-Req_Id.conf;
  include /data/nginxconf/upstream/Cluster_Name-Env_Name-Req_Id.conf;
}

#好了模版文件创建完了,下一步就可以写程序,根据接口传参然后把修改好后的文件发送到对端nginx机器了

#cat /opt/soft/nginx/conf.d/cors #在目标nginx机器上面创建的允许跨域访问的文件

    ##跨域配置
    set $cors_origin "";
    set $cors_cred   "";
    set $cors_header "";
    set $cors_method "";
    if ($http_origin ~* 'https?://([^/]*\.test\.com|[^/]*\.test\.cn)$'){
            set $cors_origin $http_origin;
            set $cors_cred   true;
            set $cors_header $http_access_control_request_headers;
            set $cors_method $http_access_control_request_method;
    }
    
    add_header Access-Control-Allow-Origin      $cors_origin;
    add_header Access-Control-Allow-Credentials $cors_cred;
    add_header Access-Control-Allow-Headers     $cors_header;
    add_header Access-Control-Allow-Methods     $cors_method;
    add_header Access-Control-Max-Age           86400;
    if ($request_method = 'OPTIONS') {
        return 204;
    }

1.2 编写接口程序

#调用接口创建域名那步操作就不介绍了,你可以自建dns服务也可以调用公共dns服务接口进行域名维护,或者除网关外的内网域名全部更新网关机器的hosts文件,只有公网域名才会去公网dns服务创建,这都看各自的需求了

#下面程序的大概意思就是,当你发布页面进行集群初始化的时候,会触发nginx模版文件的创建

# cat /opt/web/pod_consul/nginx_conf_app/views.py

# -*- coding: utf-8 -*-
import os
import shutil
import paramiko
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.exceptions import ValidationError

# ==================== 本地配置(务必修改) ====================
TEMPLATE_DIR = "/opt/web/pod_consul/template"  # 模板文件目录(service/location等)
LOCAL_TEMP_DIR = "/opt/web/temp"   # 本地临时生成目录

# ==================== 远端服务器配置(务必修改) ====================
REMOTE_NGINX_SERVERS = [
    {
        "name": "nginx-server-1",
        "host": "192.168.1.102",       # 远端IP
        "port": 22,
        "username": "root",            # SSH用户名
        "key_filename": "~/.ssh/id_rsa",# 本地私钥路径
        # 远端Nginx目录(绝对路径)
        "remote_service_dir": "/opt/soft/nginx/conf.d",
        "remote_location_root": "/opt/soft/nginx/conf.d/location",
        "remote_upstream_dir": "/data/nginxconf/upstream",
        "nginx_reload_cmd": "/opt/soft/nginx/sbin/nginx -s reload"
    }
]

# 允许的环境值
ALLOWED_CLUSTER_ENV = {"offline", "mirror"}
ALLOWED_DEMAND_ENV = {"demand", "stable"}

def replace_file_content(file_path, replace_map):
    """替换文件内容"""
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()
        for old, new in replace_map.items():
            content = content.replace(old, new)
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write(content)
        return True, "替换成功"
    except Exception as e:
        return False, f"替换失败: {str(e)}"

def exec_ssh_command(ssh_client, command):
    """执行SSH命令并返回结果"""
    try:
        stdin, stdout, stderr = ssh_client.exec_command(command, timeout=10)
        exit_code = stdout.channel.recv_exit_status()
        return {
            "success": exit_code == 0,
            "stdout": stdout.read().decode('utf-8').strip(),
            "stderr": stderr.read().decode('utf-8').strip(),
            "exit_code": exit_code
        }
    except Exception as e:
        return {
            "success": False,
            "stdout": "",
            "stderr": f"命令执行异常: {str(e)}",
            "exit_code": -1
        }

class GenerateNginxConfView(APIView):
    def post(self, request):
        # 1. 参数校验
        params = ["module_name", "cluster_name", "cluster_env", "demand_env", "req_id"]
        data = {k: request.data.get(k) for k in params}
        if not all(data.values()):
            return Response({
                "code": 400,
                "msg": f"缺少参数: {[k for k, v in data.items() if not v]}"
            }, status=400)
        
        if data["cluster_env"] not in ALLOWED_CLUSTER_ENV or data["demand_env"] not in ALLOWED_DEMAND_ENV:
            return Response({
                "code": 400,
                "msg": f"cluster_env只能是{ALLOWED_CLUSTER_ENV},demand_env只能是{ALLOWED_DEMAND_ENV}"
            }, status=400)
            
        # 2. 基础变量
        domain = f"{data['module_name']}-{data['cluster_env']}-{data['req_id']}.test.com"
        upstream_file = f"{data['cluster_name']}-{data['cluster_env']}-{data['req_id']}.conf"
        result = {
            "code": 200,
            "msg": "操作成功",
            "data": {
                "domain": domain,
                "servers": []
            }
        }
        
        # 3. 本地生成配置文件
        try:
            # 创建本地临时目录
            os.makedirs(LOCAL_TEMP_DIR, exist_ok=True)
            local_files = {}

            # 3.1 生成service.conf
            service_local = os.path.join(LOCAL_TEMP_DIR, f"{domain}.conf")
            shutil.copy(os.path.join(TEMPLATE_DIR, "service.conf"), service_local)
            replace_file_content(service_local, {"Domain_Name": domain})
            local_files["service"] = service_local

            # 3.2 生成location.conf
            location_dir = os.path.join(LOCAL_TEMP_DIR, domain)
            os.makedirs(location_dir, exist_ok=True)
            location_local = os.path.join(location_dir, f"{domain}.location.conf")
            shutil.copy(os.path.join(TEMPLATE_DIR, "location.conf"), location_local)
            replace_file_content(location_local, {"Domain_Name": domain})
            local_files["location"] = location_local

            # 3.3 生成location_upstream.conf
            upstream_local = os.path.join(location_dir, f"{domain}.location_upstream.conf")
            shutil.copy(os.path.join(TEMPLATE_DIR, f"location_upstream_{data['demand_env']}.conf"), upstream_local)
            replace_file_content(upstream_local, {
                "Domain_Name": domain,
                "Env_Name": data["cluster_env"],
                "Req_Id": data["req_id"],
                "Cluster_Name": data["cluster_name"]
            })
            local_files["upstream_conf"] = upstream_local

        except Exception as e:
            return Response({
                "code": 500,
                "msg": f"本地生成文件失败: {str(e)}"
            }, status=500)

        # 4. 处理远端服务器
        for server in REMOTE_NGINX_SERVERS:
            server_log = []
            server_success = True
            ssh_client = None

            try:
                # 4.1 建立SSH连接
                ssh_client = paramiko.SSHClient()
                ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
                ssh_client.connect(
                    hostname=server["host"],
                    port=server["port"],
                    username=server["username"],
                    key_filename=os.path.expanduser(server["key_filename"]),
                    timeout=15
                )
                server_log.append(f"SSH连接成功: {server['host']}")

                # 4.2 创建远端目录(核心修复:用SSH命令创建,确保目录存在)
                # 创建service目录(一般已存在,兜底)
                cmd = f"mkdir -p {server['remote_service_dir']}"
                res = exec_ssh_command(ssh_client, cmd)
                if not res["success"]:
                    raise Exception(f"创建service目录失败: {res['stderr']}")
                
                # 创建location域名目录(关键!之前失败的核心)
                remote_location_dir = os.path.join(server["remote_location_root"], domain)
                cmd = f"mkdir -p {remote_location_dir}"
                res = exec_ssh_command(ssh_client, cmd)
                if not res["success"]:
                    raise Exception(f"创建location目录失败: {res['stderr']}")
                server_log.append(f"创建远端目录: {remote_location_dir}")

                # 创建upstream目录
                cmd = f"mkdir -p {server['remote_upstream_dir']}"
                res = exec_ssh_command(ssh_client, cmd)
                if not res["success"]:
                    raise Exception(f"创建upstream目录失败: {res['stderr']}")

                # 4.3 上传文件(SFTP,目录已确保存在)
                sftp = ssh_client.open_sftp()

                # 上传service.conf
                remote_service = os.path.join(server["remote_service_dir"], f"{domain}.conf")
                sftp.put(local_files["service"], remote_service)
                server_log.append(f"上传service.conf: {remote_service}")

                # 上传location.conf(核心修复:指定正确的远端路径)
                remote_location = os.path.join(remote_location_dir, f"{domain}.location.conf")
                sftp.put(local_files["location"], remote_location)
                server_log.append(f"上传location.conf: {remote_location}")

                # 上传location_upstream.conf
                remote_upstream_conf = os.path.join(remote_location_dir, f"{domain}.location_upstream.conf")
                sftp.put(local_files["upstream_conf"], remote_upstream_conf)
                server_log.append(f"上传location_upstream.conf: {remote_upstream_conf}")
                sftp.close()

                # 4.4 创建upstream空文件
                cmd = f"touch {os.path.join(server['remote_upstream_dir'], upstream_file)}"
                res = exec_ssh_command(ssh_client, cmd)
                if not res["success"]:
                    raise Exception(f"创建upstream空文件失败: {res['stderr']}")
                server_log.append(f"创建upstream空文件: {os.path.join(server['remote_upstream_dir'], upstream_file)}")

                # 4.5 重启Nginx
                cmd = server["nginx_reload_cmd"]
                res = exec_ssh_command(ssh_client, cmd)
                if not res["success"]:
                    server_log.append(f"Nginx重启警告: {res['stderr']}")
                else:
                    server_log.append("Nginx重启成功")

            except Exception as e:
                server_success = False
                server_log.append(f"执行失败: {str(e)}")
                result["code"] = 500
                result["msg"] = "部分服务器执行失败"
            finally:
                if ssh_client:
                    ssh_client.close()
                    server_log.append("SSH连接关闭")

            # 记录单服务器结果
            result["data"]["servers"].append({
                "name": server["name"],
                "host": server["host"],
                "success": server_success,
                "log": server_log
            })

        return Response(result, status=result["code"])

1.3 调用接口创建配置文件

# curl -X POST   http://localhost:8000/nginx_conf_app/api/generate_nginx_conf/   -H "Content-Type: application/json"   -d '{

    "module_name": "模块名",

    "cluster_name": "集群名",

    "cluster_env": "offline",

    "demand_env": "stable",

    "req_id": "stable"

  }'

# curl -X POST   http://localhost:8000/nginx_conf_app/api/generate_nginx_conf/   -H "Content-Type: application/json"   -d '{

    "module_name": "模块名",

    "cluster_name": "集群名",

    "cluster_env": "offline",

    "demand_env": "demand",

    "req_id": "35184"

  }'

#这样就会创建server.conf域名文件,location文件以及upstream文件,并且在nginx -s reload的时候还会创建本地缓存文件,可以自行配置域名浏览器访问看是否到了后端对应的pod服务,这里不就截图测试结果了。

#通过上面的程序我们在发布页面测试环境初始化阶段就将集群以及对应的域名和nginx配置好了,这些域名呢都是默认location / 逻辑很简单就是将集群通过域名映射出去了而已,如果是多location的域名怎么办呢?-那就靠网关域名来进行转发了(现在一般网站都在搞前后端分离,大部分后端域名都是location /对应的一个集群,然后具体的入口控制在了前端域名那里),当然肯定还有一些后端域名存在多location的情况,这就通过网关进行路由了,毕竟搞了按需这种方式就是重度依赖网关域名的转发。

博文来自:www.51niux.com

二、网关域名的操作

2.1.网关域名数据插入

#接着上章节的表数据,我们先插入一些新的数据用于测试

MariaDB [route]> insert into offline_gateway (domain_name,network_type,applicant) values ('houtai.test.com','intranet','guliu');
MariaDB [route]> insert into offline_location (offline_gateway_id,module_name,cluster_name,location_path,rewrite_rule,applicant) values (4,'模块名','集群名','/','NULL','guliu');
MariaDB [route]> insert into offline_location (offline_gateway_id,module_name,cluster_name,location_path,rewrite_rule,applicant) values (4,'模块名','集群名','/api/(path|service)/','NULL','guliu');
MariaDB [route]> insert into offline_location (offline_gateway_id,module_name,cluster_name,location_path,rewrite_rule,applicant) values (4,'模块名','集群名','/api/token/','/api/(.*) /api/auth/$1','guliu');
MariaDB [route]> insert into mirror_gateway (domain_name,offline_gateway_id,network_type,applicant) values ('houtai-mirror.test.com',4,'intranet','guliu');
MariaDB [route]> insert into on_demand (source_ip,demand_number,user) values ('192.168.1.135',35184,'test01');

#这样我们创建了一个新的测试网关,也让集群跟网关域名的location关联上了,也让沙箱网关域名跟测试域名网关关联上了,关联的意义就是获取location与集群的对应信息

2.2 编写网关调用的lua脚本

# vi /opt/soft/nginx/main-conf/nginx.conf  #这里创建一个需求号缓存就是避免频繁的访问mysql(访问mysql获取的结果缓存1分钟)

lua_package_path "/opt/soft/package/lua-resty-redis-0.29/lib/?.lua;/usr/local/lua_core/lib/lua/?.lua;;";
# 仅保留IP-reqid缓存字典(1分钟过期)
lua_shared_dict ip_reqid_cache 100m;

#mkdir /opt/soft/nginx/conf.d/lua/logs/

# vi /opt/soft/nginx/conf.d/lua/ip_demand_query.lua 

-- 1. 初始化IP-reqid缓存
local ip_reqid_cache = ngx.shared.ip_reqid_cache

-- 2. Lua专属日志文件配置(单独写入,不混Nginx日志)
local lua_log_path = "/opt/soft/nginx/conf.d/lua/logs/gateway_proxy.log"
-- 日志格式化函数(带时间、级别、内容)
local function lua_log(level, content)
    local time_str = os.date("%Y-%m-%d %H:%M:%S")
    local log_str = string.format("[%s] [%s] %s\n", time_str, level, content)
    -- 追加写入日志文件(权限需确保nginx用户可写)
    local f, err = io.open(lua_log_path, "a")
    if f then
        f:write(log_str)
        f:close()
    else
        -- 兜底:写入Nginx错误日志
        ngx.log(ngx.ERR, "Lua日志写入失败:", err, " 内容:", log_str)
    end
end

-- 补全string.trim函数(Lua原生无trim,解决reqid带空格问题)
if not string.trim then
    function string.trim(s)
        return (s:gsub("^%s+", ""):gsub("%s+$", ""))
    end
end

-- 3. 从Nginx配置中获取固定变量(用于拼接转发域名和日志)
local gateway_domain = ngx.var.server_name        -- 网关域名(Nginx的server_name)
local proxy_env = ngx.var.proxy_env              -- 环境变量(offline/mirror)
local stable_domain = ngx.var.stable_domain      -- stable兜底域名
local module_name = ngx.var.module_name          -- 模块名
local request_uri = ngx.var.request_uri          -- 转发的location(请求路径)

-- 4. 初始化Nginx变量(供转发/头信息使用)
ngx.var.client_ip = ""
ngx.var.trace_tag = ""
ngx.var.demand_number = "stable"

-- 5. MySQL配置(保持原有配置)
local mysql_config = {
    host = "192.168.1.101",
    port = 3306,
    database = "route",
    user = "route",
    password = "route123456",
    timeout = 1000,
    pool_size = 100,
    pool_idle_time = 10000
}

-- 6. 防SQL注入函数(转义IP中的特殊字符)
local function escape_sql_str(str)
    if not str then
        return "''"
    end
    -- 转义单引号(SQL注入核心风险点)
    str = string.gsub(str, "'", "''")
    -- 仅保留IP合法字符(数字+点),进一步防护
    str = string.gsub(str, "[^0-9%.]", "")
    return "'" .. str .. "'"
end

-- 7. 工具函数:获取客户端真实IP
local function get_client_ip()
    local x_real_ip = ngx.req.get_headers()["X-Real-IP"]
    local x_forwarded_for = ngx.req.get_headers()["X-Forwarded-For"]
    if x_forwarded_for then
        local ip_list = string.gmatch(x_forwarded_for, "([^,]+)")
        x_forwarded_for = ip_list() and string.trim(ip_list()) or nil
    end
    local client_ip = x_real_ip or x_forwarded_for or ngx.var.remote_addr
    ngx.var.client_ip = client_ip
    -- 记录网关域名、客户端IP、请求路径
    lua_log("INFO", "基础信息 | 网关域名:" .. gateway_domain .. " | 客户端IP:" .. client_ip .. " | 转发Location:" .. request_uri)
    return client_ip
end

-- 8. 工具函数:reqid合法性校验(长度+逐字符判断,避开正则兼容问题)
local function validate_reqid(reqid)
    -- 第一步:先trim去空格
    local trimmed_reqid = string.trim(reqid or "")
    lua_log("INFO", "reqid校验 | 原始reqid:[" .. tostring(reqid) .. "] | 去空格后:[" .. trimmed_reqid .. "]")
    
    -- 第二步:空值判断
    if trimmed_reqid == "" then
        lua_log("WARN", "reqid校验 | reqid为空,强制转为stable")
        return "stable"
    end

    -- 第三步:校验规则(长度=5 + 逐字符判断是否为数字,替代正则)
    local is_stable = (trimmed_reqid == "stable")
    local is_5digit = false

    -- 判断是否是5位长度 + 每个字符都是数字(0-9)
    if string.len(trimmed_reqid) == 5 then
        local all_digit = true
        for i = 1, 5 do
            local c = string.sub(trimmed_reqid, i, i)
            if not (c >= "0" and c <= "9") then
                all_digit = false
                break
            end
        end
        is_5digit = all_digit
        -- 调试日志:打印逐字符校验结果
        lua_log("DEBUG", "reqid校验 | 5位长度:" .. tostring(string.len(trimmed_reqid)==5) .. " | 全数字:" .. tostring(all_digit))
    end

    if is_stable or is_5digit then
        lua_log("INFO", "reqid校验 | reqid合法:" .. trimmed_reqid)
        return trimmed_reqid
    else
        lua_log("WARN", "reqid校验 | reqid非法(" .. trimmed_reqid .. "),强制转为stable(规则:仅支持5位纯数字或stable)")
        return "stable"
    end
end

-- 9. 工具函数:查询MySQL获取原始reqid
local function query_db_reqid(client_ip)
    lua_log("INFO", "DB查询 | 缓存未命中,查询MySQL:IP=" .. client_ip)
    local db, err = require("resty.mysql"):new()
    if not db then
        local err_msg = "DB错误 | 创建MySQL连接失败:" .. (err or "未知错误")
        lua_log("ERROR", err_msg)
        return nil
    end

    db:set_timeout(mysql_config.timeout)

    -- 连接数据库
    local ok, err, errno = db:connect({
        host = mysql_config.host,
        port = mysql_config.port,
        database = mysql_config.database,
        user = mysql_config.user,
        password = mysql_config.password
    })

    if not ok then
        local err_msg = "DB错误 | 连接MySQL失败:" .. (err or "未知错误") .. " errno=" .. (errno or "nil")
        lua_log("ERROR", err_msg)
        return nil
    end

    -- 替换?占位符,手动拼接SQL并做防注入处理
    local escaped_ip = escape_sql_str(client_ip)
    local sql = string.format("SELECT demand_number FROM on_demand WHERE source_ip = %s LIMIT 1", escaped_ip)
    lua_log("INFO", "DB查询 | 执行SQL:" .. sql)

    local res, err = db:query(sql)

    -- 放回连接池
    local ok, err_pool = db:set_keepalive(mysql_config.pool_idle_time, mysql_config.pool_size)
    if not ok then
        lua_log("WARN", "DB警告 | MySQL连接放回池失败:" .. (err_pool or "未知错误"))
    end

    -- 处理结果
    if err then
        local err_msg = "DB错误 | 查询MySQL失败:" .. (err or "未知错误")
        lua_log("ERROR", err_msg)
        return nil
    end

    local db_reqid = nil
    if res and #res > 0 then
        db_reqid = res[1].demand_number
        lua_log("INFO", "DB结果 | IP=" .. client_ip .. " reqid=" .. tostring(db_reqid))
    else
        lua_log("INFO", "DB结果 | MySQL未查到IP(" .. client_ip .. ")的reqid")
    end
    return db_reqid
end

-- 10. DNS解析校验函数(判断域名是否可解析)
local function is_domain_resolvable(domain, timeout)
    -- 默认超时时间100ms,避免阻塞请求
    local dns_timeout = timeout or 100
    -- 使用ngx.socket.tcp进行DNS解析(纯Nginx+Lua环境兼容)
    local sock = ngx.socket.tcp()
    sock:settimeout(dns_timeout)
    -- 尝试连接域名的80端口(仅检测解析,不实际建立连接)
    local ok, err = sock:connect(domain, 80)
    if ok then
        sock:close()
        return true
    else
        -- 常见解析失败错误:host not found、timeout
        lua_log("WARN", "DNS解析 | 域名[" .. domain .. "]不可访问:" .. err)
        sock:close()
        return false
    end
end

-- 11. 工具函数:计算最终转发域名(DNS校验+兜底逻辑)
local function get_final_proxy_domain(final_reqid)
    local target_domain
    -- 拼接目标域名
    if final_reqid == "stable" then
        target_domain = stable_domain
        lua_log("INFO", "转发域名 | 直接使用stable兜底域名:" .. target_domain)
        ngx.var.proxy_domain = target_domain
        return target_domain
    else
        target_domain = module_name .. "-" .. proxy_env .. "-" .. final_reqid .. ".test.com"
        lua_log("INFO", "转发域名 | 拼接目标域名:" .. target_domain)
    end

    -- 校验目标域名是否可解析,不可解析则切换到stable
    if is_domain_resolvable(target_domain) then
        lua_log("INFO", "转发域名 | 域名[" .. target_domain .. "]可解析,正常转发")
        ngx.var.proxy_domain = target_domain
        return target_domain
    else
        lua_log("WARN", "转发域名 | 域名[" .. target_domain .. "]不可解析,切换到stable兜底域名")
        ngx.var.proxy_domain = stable_domain
        return stable_domain
    end
end

-- 12. 主逻辑:缓存查询 + DB兜底(仅60秒缓存)
local function get_reqid()
    local client_ip = get_client_ip()
    local cache_expire = tonumber(ngx.var.cache_expire) or 60

    -- 先查缓存
    local reqid = ip_reqid_cache:get(client_ip)
    if reqid then
        lua_log("INFO", "缓存命中 | IP=" .. client_ip .. " reqid=" .. reqid)
        return validate_reqid(reqid)
    end

    -- 缓存未命中:查DB
    local db_reqid = query_db_reqid(client_ip)
    -- 无论是否查到,都缓存60秒(避免频繁查库)
    local cache_val = db_reqid or "NULL"
    local ok, err = ip_reqid_cache:set(client_ip, cache_val, cache_expire)
    if not ok then
        lua_log("ERROR", "缓存错误 | 缓存设置失败:IP=" .. client_ip .. " 错误=" .. (err or "未知错误"))
    else
        lua_log("INFO", "缓存设置 | IP=" .. client_ip .. " 值=" .. cache_val .. " 过期时间=" .. cache_expire .. "秒")
    end

    -- 校验并返回
    return validate_reqid(db_reqid or "stable")
end

-- 13. 主执行逻辑
local function main()
    lua_log("INFO", "===== 新请求开始 =====")
    local final_reqid = get_reqid()
    ngx.var.trace_tag = final_reqid
    ngx.var.demand_number = final_reqid

    -- 计算并记录最终转发域名(含DNS校验+兜底)
    local final_proxy_domain = get_final_proxy_domain(final_reqid)

    -- 设置Trace-Tag请求头(非空才设)
    if final_reqid ~= "" then
        ngx.req.set_header("Trace-Tag", final_reqid)
        lua_log("INFO", "请求头 | 设置Trace-Tag:" .. final_reqid)
    end

    -- 全链路信息汇总日志
    lua_log("INFO", "请求完成 | 网关域名:" .. gateway_domain .. 
        " | 客户端IP:" .. ngx.var.client_ip .. 
        " | 转发Location:" .. request_uri .. 
        " | 最终trace_tag:" .. final_reqid .. 
        " | 最终转发域名:" .. final_proxy_domain)
    lua_log("INFO", "===== 请求结束 =====\n")
end

-- 执行主逻辑(捕获异常,写入Lua日志)
local ok, err = pcall(main)
if not ok then
    lua_log("FATAL", "Lua执行异常:" .. err)
    -- 异常时强制使用stable
    ngx.var.trace_tag = "stable"
    ngx.var.demand_number = "stable"
    ngx.var.proxy_domain = stable_domain
    -- 异常时也记录基础信息,方便排查
    lua_log("ERROR", "异常兜底 | 网关域名:" .. gateway_domain .. " | 客户端IP:" .. ngx.var.client_ip .. " | 强制使用stable转发")
end

博文来自:www.51niux.com

#vi /opt/soft/nginx/conf.d/lua/clear_ip_cache.lua  #这个脚本的作用就是把ip对应的需求号从缓存中清理掉方便及时重新获取

-- 1. 初始化缓存和响应参数
local ip_reqid_cache = ngx.shared.ip_reqid_cache
local resp_code = 200
local resp_msg = "操作成功"
local resp_ip = ""
local resp_old_val = "NULL"

-- 2. Lua专属日志文件配置
local lua_log_path = "/opt/soft/nginx/conf.d/lua/logs/lua_cache_api.log"
local function lua_log(level, content)
    local time_str = os.date("%Y-%m-%d %H:%M:%S")
    local log_str = string.format("[%s] [%s] %s\n", time_str, level, content)
    local f, err = io.open(lua_log_path, "a")
    if f then
        f:write(log_str)
        f:close()
    else
        ngx.log(ngx.ERR, "Lua接口日志写入失败:", err, " 内容:", log_str)
    end
end

-- 3. 解析POST参数(获取要清理的IP)
local function get_post_ip()
    ngx.req.read_body()
    local post_data = ngx.req.get_post_args()
    local target_ip = post_data.ip or post_data.target_ip or ""
    lua_log("INFO", "接收清理请求:参数IP=" .. target_ip)

    -- 简单IP格式校验
    local is_valid_ip = string.match(target_ip, "^%d+%.%d+%.%d+%.%d+$")
    if not is_valid_ip or target_ip == "" then
        local err_msg = "参数错误:IP格式非法或为空(接收值:" .. target_ip .. ")"
        lua_log("WARN", err_msg)
        resp_code = 400
        resp_msg = err_msg
        return nil
    end

    resp_ip = target_ip
    lua_log("INFO", "IP格式校验通过:" .. target_ip)
    return target_ip
end

-- 4. 清理缓存核心逻辑
local function clear_cache(target_ip)
    if not target_ip then
        return false
    end

    -- 查询原缓存值
    local old_val = ip_reqid_cache:get(target_ip)
    resp_old_val = old_val or "NULL" 
    lua_log("INFO", "清理前缓存值:IP=" .. target_ip .. " 值=" .. resp_old_val)

    -- 删除缓存
    local ok, err = ip_reqid_cache:delete(target_ip)
    if not ok then
        local err_msg = "缓存清理失败:IP=" .. target_ip .. " 错误=" .. (err or "未知错误")
        lua_log("ERROR", err_msg)
        resp_code = 500
        resp_msg = err_msg
        return false
    end

    -- 记录操作日志
    local success_msg = "缓存清理成功:IP=" .. target_ip .. " 原缓存值=" .. resp_old_val
    lua_log("INFO", success_msg)
    return true
end

-- 5. 纯Lua拼接JSON字符串(核心:无cjson依赖)
local function build_json()
    local json_str = string.format(
        '{"code":%d,"msg":"%s","data":{"ip":"%s","old_cache_value":"%s"}}',
        resp_code,
        ngx.escape_uri(resp_msg):gsub("%%22", "\\\""),  -- 转义双引号
        resp_ip,
        resp_old_val
    )
    -- 还原URI编码,仅保留JSON需要的转义
    json_str = ngx.unescape_uri(json_str)
    return json_str
end

-- 6. 主执行逻辑(捕获异常)
local function main()
    lua_log("INFO", "===== 缓存清理接口请求开始 =====")
    local target_ip = get_post_ip()
    clear_cache(target_ip)

    -- 构建并返回JSON响应(纯Lua拼接)
    local res_json = build_json()
    ngx.header["Content-Type"] = "application/json"
    ngx.say(res_json)
    
    lua_log("INFO", "接口响应:" .. res_json)
    lua_log("INFO", "===== 缓存清理接口请求结束 =====\n")
end

-- 执行并捕获异常
local ok, err = pcall(main)
if not ok then
    local err_msg = "接口执行异常:" .. err
    lua_log("FATAL", err_msg)
    -- 异常时返回错误JSON
    local err_json = string.format('{"code":500,"msg":"%s","data":{}}', err_msg)
    ngx.header["Content-Type"] = "application/json"
    ngx.say(err_json)
end

# vim /opt/soft/nginx/conf.d/cache-api.test.com.conf  #配置上后nginx reload一下

server {
    listen 80;
    server_name cache-api.test.com;  # 接口专属域名

    location /clear_ip_cache {
        # IP白名单(仅内网可访问)
        allow 127.0.0.1;
        allow 192.168.1.0/24;
        deny all;

        # 仅允许POST请求
        if ($request_method != POST) {
            return 405 "Method Not Allowed";
        }

        # 执行缓存清理逻辑(Lua写入专属日志)
        content_by_lua_file /opt/soft/nginx/conf.d/lua/clear_ip_cache.lua;
        add_header Content-Type application/json;
    }

    access_log /opt/log/nginx/cache-api.test.com/cache-api.test.com_access.log main;
    error_log /opt/log/nginx/cache-api.test.com/cache-api.test.com_error.log error;
}

# curl -X POST "http://cache-api.test.com/clear_ip_cache" -d "ip=192.168.1.101"  #可以清理测试一下,看下接口是否正常

{"code":200,"msg":"操作成功","data":{"ip":"192.168.1.101","old_cache_value":"NULL"}}

# yum install dnsmasq -y

# vim /etc/dnsmasq.conf

listen-address=127.0.0.1

# systemctl start dnsmasq #Nginx的resolver不支持直接读取/etc/hosts,必须通过DNS服务中转

# vi /opt/soft/nginx/conf.d/zhongtai.test.com.conf

server {
    listen 80;
    listen 443 ssl;
    server_name zhongtai.test.com;
    #ssl配置引用上面的nginx的配置这里就不粘贴占用篇幅了

    server_tokens off;
    charset utf-8;

    #指定dnsmasq的地址127.0.0.1,优先解析hosts里的域名,valid=10s是域名解析缓存时间,避免频繁解析,可根据需要调整(如60或者300s);
    #这个valid有一点是注意的,这是nginx缓存域名的时间,不管是正确还是错误的缓存,都要等缓存过期后重新解析,如果你域名变更不频繁可以调大缓存时间
    resolver 127.0.0.1:53 valid=10s ipv6=off;
    #默认等待时间,如果只依赖于本地hosts解析可以设置成如500ms,如果是本地hosts+dns服务这里可以设置的大一点,nginx有缓存也不会频繁的域名解析
    resolver_timeout 5s;

    include /opt/soft/nginx/conf.d/location/zhongtai.test.com/*.location.conf;

    access_log /opt/log/nginx/zhongtai.test.com/zhongtai.test.com_access.log main;
    error_log /opt/log/nginx/zhongtai.test.com/zhongtai.test.com_error.log error;
}

博文来自:www.51niux.com

# vi /opt/soft/nginx/conf.d/location/zhongtai.test.com/zhongtai.test.com.location.conf   #配置好后nginx reload一下

location / {
    # ===== 固定配置(直接写死)=====
    set $demand_number "stable";  # 初始化默认值
    set $trace_tag "stable";
    set $client_ip "";  # 核心:初始化自定义变量client_ip
    set $proxy_env "offline";               # 环境(固定:offline/mirror)
    set $stable_domain "模块名-offline-stable.test.com";  # stable后端域名(固定)
    set $module_name "模块名";  # 模块名(固定)
    set $cache_expire 60;   

    # 执行核心业务逻辑(Lua会写入专属日志)
    access_by_lua_file /opt/soft/nginx/conf.d/lua/ip_demand_query.lua;

    # 域名拼接(直接使用Nginx固定变量)
    if ($demand_number = "stable") {
        set $proxy_domain $stable_domain;
    }
    if ($demand_number != "stable") {
        set $proxy_domain "$module_name-$proxy_env-$trace_tag";
    }

    # 代理转发
    proxy_pass https://$proxy_domain$request_uri;

    # ===== Trace-Tag传递:仅非空时设置 =====
    proxy_set_header Host $proxy_domain;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_next_upstream http_502  error timeout invalid_header;
    proxy_set_header X-Forwarded-Proto  $scheme;

    # 基础代理配置
    proxy_connect_timeout 5s;
    proxy_send_timeout 5s;
    proxy_read_timeout 5s;
}

#然后就可以浏览器访问zhongtai.test.com做测试了,主要测试三种场景:
1. ip指向的需求有对应的模块域名(比如正好是我们要测试的变更模块)
2. ip指向的需求没有对应的模块域名(比如请求域名不是我们变更的模块自然就不会再需求里面了,会不会走到stable环境)

3. 来源IP压根就不存在于任何需求中或者绑定了stable环境,请求是否走到stable的域名

可以观察对应域名的访问日志来观测路由请求是否符合需求。

# tail -f /opt/soft/nginx/conf.d/lua/logs/gateway_proxy.log  #比如网关完整的路由记录便于排查问题

[INFO] ===== 新请求开始 =====
[INFO] 基础信息 | 网关域名:zhongtai.test.com | 客户端IP:192.168.1.135 | 转发Location:/dasdsadsa/dadasdas/
[INFO] DB查询 | 缓存未命中,查询MySQL:IP=192.168.1.135
[INFO] DB查询 | 执行SQL:SELECT demand_number FROM on_demand WHERE source_ip = 192.168.1.135' LIMIT 1
[INFO] DB结果 | IP=192.168.1.135 reqid=35184
[INFO] 缓存设置 | IP=192.168.1.135 值=35184 过期时间=60秒
[INFO] reqid校验 | 原始reqid:[35184] | 去空格后:[35184]
[DEBUG] reqid校验 | 5位长度:true | 全数字:true
[INFO] reqid校验 | reqid合法:35184
[INFO] 转发域名 | 最终转发到:http_develop-offline-35184.test.com 
[INFO] 请求头 | 设置Trace-Tag:35184
[INFO] 请求完成 | 网关域名:zhongtai.test.com  | 客户端IP:192.168.1.135 | 转发Location:/dasdsadsa/dadasdas/ | 最终trace_tag:35184 | 转发域名:http_develop-offline-35184.test.com 
[INFO] ===== 请求结束 =====
作者:忙碌的柴少 分类:Consul 浏览:65 评论:0
留言列表
发表评论
来宾的头像