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

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

总算到了最后一步了哈,前面又是consul又是lua的,就是为了实现本文要讲的内容

#紧接上文,我们consul的使用已经总结的差不多了,但是是不是感觉没有展现一个实际的场景来把consul结合起来使其作用更大化呢?
我下面描述一个具体的场景:一般我们会有一套稳定环境,但是会有N多套测试需求环境,而且这是一个动态变化的过程,这里我们需求用需求号代替,比如今天测试一个集群的功能创建了一个2001的需求,后天又创建了一个2002的需求,那么正常来说你访问网站是不是就变成了aaa-2001.xxx.com/aaa-2002.xxx.com,这就带来一个问题测试起来太繁琐了(每次测试你都要更换URL),现在很多都是app测试了,app更换域名还是挺麻烦的。还有一个问题现在很多都是微服务架构,就是你是不是要1比1的部署测试环境,不然你的测试流程很可能走不下去。
那么我们怎么解决上述问题呢,一方面让测试更便捷另一方面让成本更低,我们可以试想一下:先提供一个web平台用户可以选择自己要访问哪个环境,然后nginx+lua成为网关,一个固定域名基于来源来判断要访问哪个环境并路由,然后后端是nginx+consul将请求转发到执行的需求环境容器,然后需求号再作为tag一直透传下去,有对应的需求就将请求交给对应的需求容器,没有对应的需求容器就将请求交给稳定环境是不是就解决了我们上面提到的问题,好了大体思路有了我们去实现它。

一、容器在consul中的管理

1.1 容器在consul中存在结构?

#容器一般怎么命名呢,肯定是有一个固有的结构对吧,而且我们会把容器的一些信息写入到k8s的yaml文件中的label,比如下面:

spec:
  template:
    metadata:
      labels:
        app: 模块名-2001
        env_type: offline
        reqid: "2001"
        k8scluster: offline_k8s01
        clustername: 集群名
        modulename: 模块名

#这样我们在consul中去创建kv的原始信息就有了,那么我们是扁平的创建还是目录层级的创建呢?

image.png

下面看一下完整的示例:

# curl http://127.0.0.1:8500/v1/kv/upstreams/?keys   #可以看到如果采用目录层级的形式的话会比较直观,层层目录递进最后直到最后ip:端口形成的key

[
    "upstreams/",
    "upstreams/dianshang_http_develop-mirror-stable/",
    "upstreams/dianshang_http_develop-mirror-stable/192.168.1.187:30660",
    "upstreams/dianshang_tcp_develop-mirror-stable/",
    "upstreams/dianshang_tcp_develop-mirror-stable/192.168.1.252:30660",
    "upstreams/mirror/",
    "upstreams/mirror/stable/",
    "upstreams/offline/",
    "upstreams/offline/demand/",
    "upstreams/offline/demand/22384/",
    "upstreams/offline/demand/22384/dianshang_http_develop/",
    "upstreams/offline/demand/22384/dianshang_http_develop/192.168.1.187:30660",
    "upstreams/offline/demand/22384/dianshang_tcp_develop/",
    "upstreams/offline/demand/22384/dianshang_tcp_develop/192.168.1.252:30660",
    "upstreams/offline/demand/25568/dianshang_http_develop/192.168.1.187:3066",
    "upstreams/offline/stable/",
    "upstreams/offline/stable/dianshang_http_develop/",
    "upstreams/offline/stable/dianshang_http_develop/192.168.1.187:30660",
    "upstreams/offline/stable/dianshang_tcp_develop/",
    "upstreams/offline/stable/dianshang_tcp_develop/192.168.1.252:30660"
]

#选择哪种影响都有限就在程序上面做判断就可以了,但是我倾向于第二种一方面比较直观另一方面删除需求的时候也比较方便,比如我现在要删除25568需求(需求已释放)

#  curl --request DELETE http://127.0.0.1:8500/v1/kv/upstreams/offline/demand/25568   #执行此命令就行

# curl -X PUT -d '{"weight":1, "max_fails":2, "fail_timeout":10}'  http://127.0.0.1:8500/v1/kv/upstreams/offline/demand/25568/dianshang_http_develop/192.168.1.187:3066    #这是一个创建需求kv的操作

#一般测试环境容器就起一个pod,这既节省成本管理起来也比较简单,理论上每个需求集群下面只应该有一个pod,pod发生启动关闭无非两个事件,一个就是重启,一个就是副本更新启动新的关闭旧的,所以一旦接收到这个需求集群的启动上报就直接PUT更新就可以了,更新的肯定是最新的podip。

1.2 容器重启/关闭发送信息

#仔细思考一下,如果只启动一个pod节点的话,我们还需要关闭事件吗,我们只需要delete/put这2个动作就行了,因为每个需求集群下面只有一个podip:端口存在(因为目录下面是追加而不是覆盖,所以添加新IP:端口的key前需要将目录下面清空)。

好了那么我们假设我们有一个web接口负责接收pod启动和关闭时候的上报,那么我们如何触发这个动作呢

第一种方法钩子触发:

在yaml文件中的spec:的containers:下面定义(我pod_name和pod_ip分别用了两种取值的方式哦)

env:
  - name: POD_NAME
    valueFrom:
      fieldRef:
        apiVersion: v1
        fieldPath: metadata.name
  - name: POD_IP
    valueFrom:
      fieldRef:
        fieldPath: status.podIP
  - name: PATH
    value: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/soft/java/bin"
lifecycle:
  postStart:
    exec:   
      command:
        - "/bin/sh"
        - "-c"  
        - 'curl -i -X POST -H "Content-type:application/json" -d "{\"clustername\":\"dianshang_http_develop\",\"modulename\":\"http-develop\",\"env_type\":\"offline\",\"reqid\":\"25568\",\
"pod_name\":\"`hostname`\",\"pod_ip\":\"$POD_IP\",\"port\":\"30553\"}" http://lua.test.com/docker/report/start/'
  preStop:
    exec:   
      command:
        - "/bin/sh"
        - "-c"  
        - 'curl -i -X POST -H "Content-type:application/json" -d "{\"clustername\":\"dianshang_http_develop\",\"modulename\":\"http-develop\",\"env_type\":\"offline\",\"reqid\":\"25568\",\
"pod_name\":\"`hostname`\",\"pod_ip\":\"$POD_IP\",\"port\":\"30553\"}" http://lua.test.com/docker/report/stop/'

题外话:你说我想一个钩子发给多个接口怎么办(确实有这个需求啊,监控平台可能也想要这个事件呢),以关闭钩子举例:

preStop:
  exec:
    command:  
      - "/bin/sh" 
      - "-c"    
      - 'curl -i -X POST -H "Content-type:application/json" -d "{\"clustername\":\"dianshang_http_develop\",\"pod_ip\":\"`ifconfig |grep -v 127.0.0.1|grep -o inet.*[nB]|grep -o [0-9].*[0-9
]`\"}" http://watcher.test.com/pod/stop ; curl -d "docker_name=`hostname`&action_state=Stop" http://report.test.com/docker/hook_report/'

#通过;就可以实现各取所需,各接口要自己的信息了,这个;的作用是执行多个命令而且;后面的命令是不受;前面命令成功与否的状态影响的。

第二种方法启动触发:

在yaml文件命令哪里是启动脚本并传参,让脚本去做一些必要的操作并拉起服务,当然这种方式就是只能启动上报了

command: ["/bin/sh"]
args: ["/root/init.sh", "集群名", "offline", "服务类型","镜像版本","监听端口","模块名","stable/也可以是需求号"]

#然后把上面的启动的curl命令就可以放到这个/root/init.sh脚本中了,脚本都不陌生了啊。

#题外话,这种脚本的方式在启动容器的时候有很大的作用,比如我们如何实现一个镜像多环境使用呢,就是通过在镜像中埋一个脚本的形式,默认打的是线上的配置,这样什么都不用动服务直接启免去了改动风险,因为线上和测试环境是资源隔离的也不用担心测试环境调用到了线上因为服务压根就启动不起来。然后脚本判断如果是测试环境,就再拉一个脚本所有的操作都在这个脚本中操作(有好处的,比如你要修改点东西正常流程的话是需要重新打一个镜像,但是这样只需要重启一下容器就可以重新拉一下脚本就可以做最新的处理了),然后检测到时测试或者沙箱环境,就把对应的配置文件包拉下来替换配置文件目录然后启动服务就行了。

#但是我们线上环境要的是稳定,不能每次都拉一个新脚本吧如果拉取失败我容器岂不是启动不起来了,所以在管理平台做一个开关,启动的时候做个判断,如果发现接口返回的状态码是要进行更新,就去拉取脚本并执行这个新脚本直至再次发版这个开关就会自动关闭了。(这个功能很适合大批量的操作,比如在我们批量更新log4j之类的漏洞的时候很好用,只需要批量重启容器就可以完成漏洞修复,一些大批量的修复只需要运维重启就可以了,不需要重新构建镜像大批量的发版了)。

1.3 web接口接上报并处理

# vim /opt/web/pod_consul/docker/urls.py

from django.urls import path,include
from . import views
urlpatterns = [
        path('index/',views.index),
        path('report/start/',views.StartView.as_view(),name='start_report'),
        path('report/stop/',views.StopView.as_view(),name='stop_report'),
]

# cat /opt/web/pod_consul/docker/views.py   #下面是一个简单的接收上报维护consul的接口demo

# ~*~ coding: utf-8 ~*~
"""
容器上报管理系统的视图层代码
负责处理容器启动/停止的上报请求,并与Consul服务进行交互
"""
import sys
import json
from typing import Dict, Any, Optional  # 导入类型提示工具,增强代码可读性和IDE支持
import requests  # 用于发送HTTP请求到Consul服务
from django.http import JsonResponse, HttpResponse  # Django的HTTP响应处理类
from django.views import View  # Django基础视图类
from django.conf import settings  # 用于获取Django项目配置

# 从Django配置中获取Consul服务地址,若未配置则使用默认值
# 这样的设计使得Consul地址可通过配置文件修改,无需改动代码
CONSUL_URL = getattr(settings, 'CONSUL_URL', 'http://127.0.0.1:8500/v1/kv')

# 定义对外暴露的视图类,控制导入时的可见性
__all__ = ['StartView', 'StopView', 'index']

def index(request) -> HttpResponse:
    """
    系统根路径视图函数
    Args:
        request: Django请求对象
    Returns:
        HttpResponse: 返回简单的HTML响应
    """
    return HttpResponse('<h2>docker</h2>')

class ConsulClient:
    """
    Consul服务交互客户端类
    封装了与Consul API的所有交互操作,职责单一,便于维护和测试
    """
    @staticmethod
    def _construct_base_url(env_type: str, reqid: str, clustername: str) -> str:
        """
        构建Consul操作的基础URL路径
        根据环境类型(env_type)和请求ID(reqid)决定不同的存储路径,
        稳定版本(stable)和需求版本(demand)使用不同的目录结构
        Args:
            env_type: 环境类型(如prod/test等)
            reqid: 请求ID,用于区分不同的部署需求
            clustername: 集群名称
        Returns:
            str: 构建好的基础URL
        """
        if reqid == "stable":
            return f"{CONSUL_URL}/upstreams/{env_type}/stable/{clustername}"
        return f"{CONSUL_URL}/upstreams/{env_type}/demand/{reqid}/{clustername}"
    
    @staticmethod
    def _construct_full_url(env_type: str, reqid: str, clustername: str, pod_ip: str, port: str) -> str:
        """
        构建包含完整服务信息的Consul URL路径
        在基础URL的基础上添加Pod的IP和端口,形成唯一标识服务实例的路径
        Args:
            env_type: 环境类型
            reqid: 请求ID
            clustername: 集群名称
            pod_ip: 容器/Pod的IP地址
            port: 服务端口
        Returns:
            str: 完整的Consul操作URL
        """
        base_url = ConsulClient._construct_base_url(env_type, reqid, clustername)
        return f"{base_url}/{pod_ip}:{port}"
    
    @staticmethod
    def clear_existing_entries(env_type: str, reqid: str, clustername: str) -> Optional[requests.Response]:
        """
        清除Consul中指定集群的现有条目
        在新服务上线时,先清除该集群的旧有记录,避免无效服务信息残留
        Args:
            env_type: 环境类型
            reqid: 请求ID
            clustername: 集群名称
        Returns:
            Optional[requests.Response]: Consul API的响应对象,失败时返回None
        """
        # 构建清除操作的URL,recurse参数表示递归删除该路径下的所有条目
        clear_url = f"{ConsulClient._construct_base_url(env_type, reqid, clustername)}?recurse"
        try:
            # 发送DELETE请求清除现有记录
            response = requests.delete(clear_url)
            # 检查HTTP响应状态码,非2xx状态会抛出异常
            response.raise_for_status()
            return response
        except requests.exceptions.RequestException as e:
            # 捕获所有HTTP请求相关异常并记录
            print(f"清除Consul条目时出错: {str(e)}")
            return None
    
    @staticmethod
    def update_service_entry(env_type: str, reqid: str, clustername: str, pod_ip: str, port: str) -> Optional[requests.Response]:
        """
        在Consul中更新服务实例信息
        当容器启动时,将其服务信息(IP、端口、权重等)注册到Consul
        Args:
            env_type: 环境类型
            reqid: 请求ID
            clustername: 集群名称
            pod_ip: 容器/Pod的IP地址
            port: 服务端口
        Returns:
            Optional[requests.Response]: Consul API的响应对象,失败时返回None
        """
        # 构建更新操作的URL
        update_url = ConsulClient._construct_full_url(env_type, reqid, clustername, pod_ip, port)
        # 服务配置数据,包含负载均衡相关参数
        service_data = json.dumps({
            "weight": 1,           # 服务权重,用于负载均衡
            "max_fails": 3,        # 最大失败次数,超过后标记服务不可用
            "fail_timeout": 1      # 失败超时时间(秒)
        })
        try:
            # 发送PUT请求更新服务信息
            response = requests.put(update_url, data=service_data)
            response.raise_for_status()  # 检查HTTP响应状态
            return response
        except requests.exceptions.RequestException as e:
            print(f"更新Consul条目时出错: {str(e)}")
            return None
    
    @staticmethod
    def remove_service_entry(env_type: str, reqid: str, clustername: str, pod_ip: str, port: str) -> Optional[requests.Response]:
        """
        从Consul中移除服务实例信息
        当容器停止时,从Consul中删除其服务信息,避免流量被路由到已停止的服务
        Args:
            env_type: 环境类型
            reqid: 请求ID
            clustername: 集群名称
            pod_ip: 容器/Pod的IP地址
            port: 服务端口
        Returns:
            Optional[requests.Response]: Consul API的响应对象,失败时返回None
        """
        # 构建删除操作的URL
        delete_url = ConsulClient._construct_full_url(env_type, reqid, clustername, pod_ip, port)
        try:
            # 发送DELETE请求移除服务信息
            response = requests.delete(delete_url)
            response.raise_for_status()  # 检查HTTP响应状态
            return response
        except requests.exceptions.RequestException as e:
            print(f"移除Consul条目时出错: {str(e)}")
            return None

class BaseContainerView(View):
    """
    容器操作的基础视图类
    封装了StartView和StopView的共同功能,减少代码重复,
    遵循DRY(Don't Repeat Yourself)原则
    """
    def _parse_request_data(self, request) -> Dict[str, str]:
        """
        解析并验证请求中的JSON数据
        提取请求中的容器信息字段,并为缺失的字段提供默认值(空字符串)
        Args:
            request: Django请求对象
        Returns:
            Dict[str, str]: 解析后的容器信息字典
        Raises:
            json.JSONDecodeError: 当请求数据不是有效的JSON格式时抛出
        """
        try:
            # 解析请求体中的JSON数据
            data = json.loads(request.body)
            # 提取所需字段,使用get方法确保即使字段缺失也不会报错
            return {
                'clustername': data.get('clustername', ''),  # 集群名称
                'modulename': data.get('modulename', ''),    # 模块名称
                'port': data.get('port', ''),                # 服务端口
                'pod_ip': data.get('pod_ip', ''),            # Pod/容器IP
                'reqid': data.get('reqid', ''),              # 请求ID
                'env_type': data.get('env_type', ''),        # 环境类型
                'pod_name': data.get('pod_name', '')         # Pod/容器名称
            }
        except json.JSONDecodeError:
            # 记录解析错误信息便于调试
            print("错误:POST数据不是有效的JSON格式")
            print("原始数据:", request.body)
            raise  # 重新抛出异常,由调用方处理响应
    
    def get(self, request) -> HttpResponse:
        """
        处理GET请求
        提供简单的欢迎信息,告知客户端应使用POST方法上报容器信息
        Args:
            request: Django请求对象
        Returns:
            HttpResponse: 包含欢迎信息的响应
        """
        return HttpResponse(
            "欢迎使用容器上报管理系统,请进行容器信息的上报",
            content_type="application/json;charset=utf-8"
        )

class StartView(BaseContainerView):
    """
    处理容器启动上报的视图类
    当容器启动时,接收其信息并更新到Consul服务发现系统
    """
    def post(self, request) -> JsonResponse:
        """
        处理容器启动的POST请求
        解析请求数据,记录容器启动信息,并更新Consul中的服务注册信息
        Args:
            request: Django请求对象,包含容器启动信息
        Returns:
            JsonResponse: 包含处理结果的JSON响应
        """
        try:
            # 解析请求数据
            data = self._parse_request_data(request)
            # 打印容器启动信息,用于日志记录和调试
            print(
                f"容器启动上报: {data['clustername']}, "
                f"{data['modulename']}, {data['port']}, {data['pod_ip']}, "
                f"{data['reqid']}, {data['env_type']}, {data['pod_name']}"
            )
            # 清除该集群的现有条目
            clear_response = ConsulClient.clear_existing_entries(
                data['env_type'], data['reqid'], data['clustername']
            )
            # 检查清除操作是否成功
            if clear_response is None:
                return JsonResponse(
                    {'status': 'error', 'message': '清除现有条目失败'},
                    status=500  # 500表示服务器内部错误
                )
            # 更新服务条目,注册新的容器信息
            update_response = ConsulClient.update_service_entry(
                data['env_type'], data['reqid'], data['clustername'],
                data['pod_ip'], data['port']
            )
            # 检查更新操作是否成功
            if update_response is None:
                return JsonResponse(
                    {'status': 'error', 'message': '更新服务条目失败'},
                    status=500
                )
            # 所有操作成功完成
            return JsonResponse({'status': 'success', 'message': '数据已接收并处理'})
        except json.JSONDecodeError:
            # 处理JSON解析错误
            return JsonResponse(
                {'status': 'error', 'message': '无效的JSON数据'},
                status=400  # 400表示请求参数错误
            )
        except Exception as e:
            # 捕获所有未预料到的异常,避免服务崩溃
            print(f"StartView中发生未预期错误: {str(e)}")
            return JsonResponse(
                {'status': 'error', 'message': '发生未预期的错误'},
                status=500
            )

class StopView(BaseContainerView):
    """
    处理容器停止上报的视图类
    当容器停止时,接收其信息并从Consul服务发现系统中移除该服务
    """
    def post(self, request) -> JsonResponse:
        """
        处理容器停止的POST请求
        解析请求数据,记录容器停止信息,并从Consul中移除该服务信息
        Args:
            request: Django请求对象,包含容器停止信息
        Returns:
            JsonResponse: 包含处理结果的JSON响应
        """
        try:
            # 解析请求数据
            data = self._parse_request_data(request)
            # 打印容器停止信息,用于日志记录和调试
            print(
                f"容器关闭上报: {data['clustername']}, "
                f"{data['modulename']}, {data['port']}, {data['pod_ip']}, "
                f"{data['reqid']}, {data['env_type']}, {data['pod_name']}"
            )
            # 从Consul中移除服务条目
            response = ConsulClient.remove_service_entry(
                data['env_type'], data['reqid'], data['clustername'],
                data['pod_ip'], data['port']
            )
            # 检查移除操作是否成功
            if response is None:
                return JsonResponse(
                    {'status': 'error', 'message': '移除服务条目失败'},
                    status=500
                )
            # 操作成功完成
            return JsonResponse({'status': 'success', 'message': '数据已接收并处理'})
        except json.JSONDecodeError:
            # 处理JSON解析错误
            return JsonResponse(
                {'status': 'error', 'message': '无效的JSON数据'},
                status=400
            )
        except Exception as e:
            # 捕获所有未预料到的异常
            print(f"StopView中发生未预期错误: {str(e)}")
            return JsonResponse(
                {'status': 'error', 'message': '发生未预期的错误'},
                status=500
            )

#然后你找一个你的需求容器把上报信息接口添加到yaml文件中然后apply一下,然后等容器完整重启后,delete pod一下可以看看consul中得信息是否发生变化

容器关闭上报: 上报信息
10:42:10] "POST /docker/report/stop/ HTTP/1.1" 200 84
容器启动上报: 
10:42:20] "POST /docker/report/start/ HTTP/1.1" 200 84

#从接口的输出可以看出来单个Pod发生delete事件的时候是先触发关闭钩子再触发新容器的启动钩子

#我们的demo代码中已经在启动接口中增加了清空整个目录的操作,所以使用上面提到的第二种方法只启动上报已经可以实现我们需要的效果,但是接口只能实现一次是吧,如果失败了呢你不能把所有的pod都重启一遍然后让重新上报吧,所以最好还要有一个兜底的功能。

#那么这个兜底功能如何实现呢,我说个最简单的方法:
首先我们需要拿到各个环境在consul中的信息,比如需求环境:
# curl http://127.0.0.1:8500/v1/kv/upstreams/offline/demand/?keys|grep 192.168   #这样环境/需求号/集群名称/集群的IP:端口我们都能得到了

    "upstreams/offline/demand/22384/dianshang_http_develop/192.168.1.187:30660",
    "upstreams/offline/demand/22384/dianshang_tcp_develop/192.168.1.252:30660",
    "upstreams/offline/demand/25568/dianshang_http_develop/192.168.1.187:3066"

# kubectl get pods -n demand -o wide   #这样当前需求环境运行的集群/需求以及对应的podip我们就拿到了,然后写个程序跟consul中的信息每分钟一核准不就行了

二、nginx使用Consul转发请求

2.1 需求域名配置

http://www.51niux.com/?id=322   这个文章中的内容就用上了哦

# vim  /opt/soft/nginx/conf.d/集群名-docker-25298.test.com.conf

include /opt/soft/nginx/conf.d/location/集群名-docker-25298.test.com/*.location_upstream.conf;
server {
    ......
    include /opt/soft/nginx/conf.d/location/集群名-docker-25298.test.com/*.location.conf;
}

#注意了,为什么需要两个include,为什么把upstream.conf引用放到http区域呢而不是单纯的include一个*.conf?因为你要都放到server{}区域是要有下面得报错的:

nginx: [emerg] "upstream" directive is not allowed here in /opt/soft/nginx/conf.d/location/模块名-docker-25298.test.com/模块名-docker-25298.test.com.location_upstream.conf:1
nginx: configuration file /opt/soft/nginx/main-conf/nginx.conf test failed

# vim /opt/soft/nginx/conf.d/location/集群名-docker-25298.test.com/集群名-docker-25298.test.com.location.conf

    location / {
        proxy_next_upstream http_502  error timeout invalid_header;
        proxy_pass http://集群名-docker-25298_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;
    }

# vim /opt/soft/nginx/conf.d/location/集群名-docker-25298.test.com/集群名-docker-25298.test.com.location_upstream.conf

upstream 集群名-docker-25298_pool {
  server 127.0.0.1:11111 down;
  upsync 127.0.0.1:8500/v1/kv/upstreams/offline/demand/25298/集群名 upsync_timeout=5s upsync_interval=500ms upsync_type=consul strong_dependency=off;
  upsync_dump_path /data/nginxconf/location/集群名-offline-25298.conf;
  include /data/nginxconf/location/集群名-offline-25298*.conf;
}

# /opt/soft/nginx/sbin/nginx -s reload  #可以自行用测试域名访问一下看看请求是不是到了consul里面存储的后端IP:端口,可以的这里就不粘贴测试结果了

#上面只是一个location是吧,但是生产环境中我们有时候一个域名有多个location,这怎么实现呢,其实道理是相同的,一般默认我们都是所有的请求都给location /,但是如果你有其他location呢,你这里就依次类推在比如/opt/soft/nginx/conf.d/location/集群名-docker-25298.test.com/下面创建关于其他的location.conf和upstream.conf文件就可以了。好的那么第二个问题来了,我怎么知道这个域名在创建需求的时候需要额外创建其他的location呢?这就设计到mysql存储数据了,通常呢我们一般有一个模版域名,比如stable域名,我们新增的lcation都通过web页面这上面创建location对应的集群,比如点击添加最少需要(域名/模块名|集群名/location)这三个信息,然后也可以在域名的右侧点击查看可以看到(模块名/集群名/后端端口号/location/申请人/申请时间)这些大体的信息

#这样就可以实现给一个域名添加除/以外的其他指定的location,这些location就可以作用到nginx的配置文件中了,你的请求也就会被location路由了。

#我们参照上面的例子再做一个stable环境的,要实现一个网关域名转发:1.如果来源IP绑定了需求就优先走需求域名,如果没有需求域名就走稳定环境域名但是是需求的tag

2.2 网关数据表创建

#先创建一个mysql表记录来源IP和需求号的对应关系(当然你可以丰富表字段比如user-agent、添加人、需求描述、添加时间、是否生效等):

CREATE TABLE on_demand (
    source_ip VARCHAR(45) NOT NULL,
    demand_number VARCHAR(50) NOT NULL,
    -- 添加唯一约束,确保一个来源IP只能对应一个需求编号
    UNIQUE KEY unique_source_ip (source_ip)
);

#然后再创建一下网关表,我们这样涉及,现有一个线下环境的网关表,线下网关绑定location,沙箱网关跟稳定网关做关联,这样location就保持一致了

CREATE TABLE `offline_gateway` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `domain_name` varchar(255) NOT NULL COMMENT '线下网关域名(如 offline.user.test.com)',
  `network_type` varchar(10) NOT NULL COMMENT '网络类型(intranet-内网/extranet-外网)',
  `applicant` varchar(50) NOT NULL COMMENT '申请人(创建者)',
  `create_time` datetime NOT NULL DEFAULT current_timestamp() COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_offline_domain` (`domain_name`) COMMENT '线下域名唯一,避免重复'
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='线下网关模板表(存储线下网关基础信息,作为沙箱的配置来源)';

CREATE TABLE `offline_location` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `offline_gateway_id` bigint(20) NOT NULL COMMENT '关联的线下网关ID',
  `module_name` varchar(100) NOT NULL COMMENT '所属模块',
  `cluster_name` varchar(255) NOT NULL COMMENT '对应的集群名称(如 http_user_service)',
  `location_path` varchar(255) NOT NULL COMMENT 'location路径(如 /user/login)',
  `rewrite_rule` varchar(500) DEFAULT NULL COMMENT '重写规则(如 /user/login/(.*) /auth/$1)',
  `applicant` varchar(50) NOT NULL COMMENT '申请人(创建者)',
  `create_time` datetime NOT NULL DEFAULT current_timestamp() COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_offline_loc` (`offline_gateway_id`,`location_path`),
  CONSTRAINT `offline_location_ibfk_1` FOREIGN KEY (`offline_gateway_id`) REFERENCES `offline_gateway` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='线下网关的location配置表(沙箱网关直接复用此配置)';

CREATE TABLE `mirror_gateway` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `domain_name` varchar(255) NOT NULL COMMENT '沙箱网关域名(如 mirror1.user.test.com)',
  `offline_gateway_id` bigint(20) NOT NULL COMMENT '绑定的线下网关ID(1个沙箱仅绑定1个线下)',
  `network_type` varchar(10) NOT NULL COMMENT '网络类型(通常与线下一致)',
  `applicant` varchar(50) NOT NULL COMMENT '申请人(创建者)',
  `create_time` datetime NOT NULL DEFAULT current_timestamp() COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_mirror_domain` (`domain_name`),
  UNIQUE KEY `uk_mirror_offline` (`domain_name`,`offline_gateway_id`),
  KEY `offline_gateway_id` (`offline_gateway_id`),
  CONSTRAINT `mirror_gateway_ibfk_1` FOREIGN KEY (`offline_gateway_id`) REFERENCES `offline_gateway` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='沙箱网关表(与线下网关映射,1个沙箱绑定1个线下,1个线下可绑定多个沙箱)';

#下面我们插入一些测试数据查看一下效果:
> select * from offline_gateway;

image.png

> select * from offline_location;

image.png

> select * from mirror_gateway;

image.png

#可以看到我们的表结构大概就是这样的,让测试网关去关联location对应的集群,我们要环境一致嘛,所以沙箱环境就以测试网关信息由模版就可以了,那么沙箱环境怎么知道自己关联了哪些location呢?

> SELECT    m.domain_name AS 沙箱域名,   o.domain_name AS 线下域名,   l.location_path,   l.cluster_name,   l.rewrite_rule FROM mirror_gateway m JOIN offline_gateway o ON m.offline_gateway_id = o.id JOIN offline_location l ON o.id = l.offline_gateway_id WHERE m.domain_name = 'user-mirror-1.test.com';

image.png

> SELECT    m.domain_name AS 沙箱域名,   o.domain_name AS 线下域名,   l.location_path,   l.cluster_name,   l.rewrite_rule,   l.module_name,   m.network_type FROM mirror_gateway m JOIN offline_gateway o ON m.offline_gateway_id = o.id JOIN offline_location l ON o.id = l.offline_gateway_id ORDER BY m.domain_name, l.location_path;    #这是查询所有沙箱域名跟线下域名对应的location关系的sql语句


> SELECT    o.domain_name AS 线下域名,   l.location_path AS location路径,   l.module_name AS 模块名称,   l.cluster_name AS 集群名称,   l.rewrite_rule AS 重写规则,   l.applicant AS 配置申请人,   l.create_time AS 配置创建时间 FROM offline_gateway o  JOIN offline_location l ON o.id = l.offline_gateway_id  WHERE o.domain_name = 'user-offline.test.com'  ORDER BY l.location_path;  #单个线下网关域名对应的location信息


> SELECT    o.domain_name AS 线下域名,   l.location_path AS location路径,   l.module_name AS 模块名称,   l.cluster_name AS 集群名称,   l.rewrite_rule AS 重写规则 FROM offline_gateway o JOIN offline_location l ON o.id = l.offline_gateway_id ORDER BY o.domain_name, l.location_path;  #所有的location信息


> SELECT       o.domain_name AS 线下域名,      l.location_path AS location路径,   l.cluster_name AS 集群名称,      GROUP_CONCAT(DISTINCT m.domain_name) AS 关联的沙箱域名 FROM offline_location l  JOIN offline_gateway o ON l.offline_gateway_id = o.id  LEFT JOIN mirror_gateway m ON o.id = m.offline_gateway_id  WHERE l.cluster_name = 'http_order_service'  GROUP BY o.domain_name, l.location_path, l.cluster_name  ORDER BY o.domain_name, l.location_path;  #查询单个模块集群


> SELECT       l.cluster_name AS 集群名称,      GROUP_CONCAT(DISTINCT o.domain_name ORDER BY o.domain_name) AS 关联的线下域名,      GROUP_CONCAT(DISTINCT CONCAT(o.domain_name, ':', l.location_path) ORDER BY o.domain_name, l.location_path) AS 线下域名与location路径,      GROUP_CONCAT(DISTINCT m.domain_name ORDER BY m.domain_name) AS 关联的沙箱域名 FROM offline_location l  JOIN offline_gateway o ON l.offline_gateway_id = o.id  LEFT JOIN mirror_gateway m ON o.id = m.offline_gateway_id  GROUP BY l.cluster_name  ORDER BY l.cluster_name;

#上面的sql语句是查询所有的集群关联的线下域名以及location信息

#好了通过上面的sql语句基本可以满足我们需求了,包括程序去创建nginx的配置文件和平台的查询操作,当然配置文件的更新比如增删改,就变为平台操作触发式的了

2.3 需求域名数据表创建

#这个需求域名就是各种需求域名创建时候的记录信息,主要是为了web平台关联展示用的

CREATE TABLE `environment_domain` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `domain_name` varchar(255) NOT NULL COMMENT '域名全称',
  `module_name` varchar(100) NOT NULL COMMENT '模块名称',
  `cluster_name` varchar(255) NOT NULL COMMENT '集群名称',
  `environment` varchar(20) NOT NULL COMMENT '环境标识(offline/mirror)',
  `requirement_id` varchar(50) DEFAULT NULL COMMENT '需求编号(用于批量下线)',
  `env_type` varchar(20) NOT NULL COMMENT '环境类型:stable-稳定环境,demand-需求环境',
  `network_type` varchar(10) NOT NULL COMMENT '网络类型:intranet-内网,extranet-外网',
  `description` varchar(500) DEFAULT NULL COMMENT '域名描述',
  `applicant` varchar(50) NOT NULL COMMENT '申请人(创建者)',
  `create_time` datetime NOT NULL DEFAULT current_timestamp(),
  `update_time` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_domain_name` (`domain_name`),
  KEY `idx_requirement_id` (`requirement_id`) COMMENT '用于需求下线时批量查询',
  KEY `idx_domain_applicant` (`applicant`) COMMENT '用于按申请人查询域名'
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='需求环境域名表(含申请人信息)';

> select domain_name,module_name,cluster_name,environment,requirement_id,env_type,network_type from environment_domain order by environment,env_type desc;  #可以看下示例数据

image.png

#可以看到域名的命名规格就是:模块名-环境-需求.域名后缀,我们默认的需求环境当然是内网类型了啊,因为请求主要依赖于网关转发吗,但是也难免会有某个集群的域名需要进行三方联调,那么我们直接把网关域名开成公网访问显然是不合适的(因为网关域名作为内网测试入口一旦公网就是基本走稳定环境了),当然细节层面还是要基于现状来的,这套主要是为了自动化的按需需求,有些满足不了的需求可以手工创建一个域名随着需求声明周期维护。

作者:忙碌的柴少 分类:Consul 浏览:34 评论:0
留言列表
发表评论
来宾的头像