如何写服务注册上报脚本

wen 实用脚本 29

本文目录导读:

如何写服务注册上报脚本

  1. 核心设计思路
  2. 通用脚本架构(伪代码)
  3. 实战示例
  4. 不同注册中心的差异点
  5. 最佳实践与注意事项
  6. 延伸:服务注册 vs 服务发现

编写一个服务注册上报脚本,通常用于服务发现系统(如 Consul、Eureka、Nacos 或 Kubernetes),或者自定义的注册中心,本质上是将服务实例的信息(IP、端口、健康检查接口、元数据等)发送给注册中心

下面我将从设计思路通用脚本架构具体代码示例(Python 和 Shell)为你详细讲解。


核心设计思路

一个好的注册脚本需要解决以下问题:

  1. 自动获取本机信息:获取内网/外网 IP、随机端口(如果是动态端口)。
  2. 心跳续约:注册后定期发送心跳,防止服务被踢下线。
  3. 优雅注销:脚本退出或被杀死时,主动从注册中心注销。
  4. 重试与容错:注册失败或网络抖动时的重试逻辑。
  5. 可配置化:通过配置文件或环境变量控制地址、端口、环境等。

通用脚本架构(伪代码)

读取配置文件
2. 获取本机IP地址
3. 构建注册请求体(Service ID、Name、Address、Port、Health Check URL、Tags)
4. 尝试向注册中心发送注册 PUT/POST 请求
   - 如果成功 => 启动后台心跳线程/定时器
   - 如果失败 => 指数退避重试
5. 监听 SIGTERM / SIGINT 信号
   - 捕获信号 => 发送注销请求 => 退出
6. (可选) 持续运行,定期检查注册状态

实战示例

Python 脚本(适用于 Consul / 自定义 HTTP 注册中心)

特点:功能强大、依赖少、支持线程心跳。

#!/usr/bin/env python3
import os
import sys
import json
import time
import logging
import socket
import signal
import requests
from threading import Thread
# === 配置区(建议从环境变量或配置文件读取)===
REGISTRY_URL = os.getenv("REGISTRY_URL", "http://127.0.0.1:8500")  # Consul默认地址
SERVICE_NAME = os.getenv("SERVICE_NAME", "my-service")
SERVICE_PORT = int(os.getenv("SERVICE_PORT", 8080))
HEALTH_CHECK_PATH = os.getenv("HEALTH_CHECK_PATH", "/health")
HEARTBEAT_INTERVAL = int(os.getenv("HEARTBEAT_INTERVAL", 10))  # 心跳间隔秒数
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
# === 日志配置 ===
logging.basicConfig(
    level=getattr(logging, LOG_LEVEL),
    format="%(asctime)s [%(levelname)s] %(message)s"
)
logger = logging.getLogger("service-registrar")
# === 全局变量 ===
registered = False
service_id = None  # 注册成功后由服务端返回的ID
def get_local_ip():
    """获取本机内网IP(最稳定的方法)"""
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except Exception:
        return "127.0.0.1"
def register_service():
    """向注册中心注册服务"""
    global service_id, registered
    local_ip = get_local_ip()
    # 构建注册请求体(以Consul为例)
    payload = {
        "ID": f"{SERVICE_NAME}-{local_ip}-{SERVICE_PORT}",
        "Name": SERVICE_NAME,
        "Address": local_ip,
        "Port": SERVICE_PORT,
        "Tags": ["production", "v1"],
        "Check": {
            "HTTP": f"http://{local_ip}:{SERVICE_PORT}{HEALTH_CHECK_PATH}",
            "Interval": f"{HEARTBEAT_INTERVAL}s",
            "Timeout": "3s"
        }
    }
    try:
        logger.info(f"正在注册服务: {SERVICE_NAME} @ {local_ip}:{SERVICE_PORT}")
        resp = requests.put(
            f"{REGISTRY_URL}/v1/agent/service/register",
            json=payload,
            timeout=5
        )
        resp.raise_for_status()
        service_id = payload["ID"]
        registered = True
        logger.info(f"服务注册成功! ID: {service_id}")
    except requests.exceptions.RequestException as e:
        logger.error(f"注册失败: {e}")
        # 重试逻辑由调用者处理
        raise
def deregister_service():
    """从注册中心注销服务"""
    global registered
    if not service_id:
        return
    try:
        logger.info(f"正在注销服务: {service_id}")
        resp = requests.put(
            f"{REGISTRY_URL}/v1/agent/service/deregister/{service_id}",
            timeout=5
        )
        resp.raise_for_status()
        registered = False
        logger.info("注销成功")
    except requests.exceptions.RequestException as e:
        logger.error(f"注销失败: {e}")
def heart_beat():
    """心跳续约线程(对于Consul,TTL检查方式可能需要主动Pass)"""
    # 注意:Consul的HTTP检查不需要主动心跳,依赖健康检查接口。
    # 以下代码供参考,适用于需要主动心跳的注册中心(如Eureka)
    global registered
    while registered:
        time.sleep(HEARTBEAT_INTERVAL)
        if not registered:
            break
        # 示例:发送心跳到自定义注册中心
        # try:
        #     resp = requests.put(f"{REGISTRY_URL}/heartbeat/{service_id}", timeout=3)
        #     resp.raise_for_status()
        # except Exception as e:
        #     logger.warning(f"心跳失败: {e}")
def signal_handler(signum, frame):
    """捕获退出信号,优雅注销"""
    logger.info(f"收到信号 {signum}, 准备注销服务...")
    deregister_service()
    sys.exit(0)
def main():
    # 注册信号处理
    signal.signal(signal.SIGTERM, signal_handler)
    signal.signal(signal.SIGINT, signal_handler)
    # 主注册循环(带重试)
    max_retries = 5
    for attempt in range(1, max_retries + 1):
        try:
            register_service()
            break  # 注册成功,跳出重试循环
        except Exception as e:
            if attempt < max_retries:
                wait = 2 ** attempt  # 指数退避:2, 4, 8, 16秒
                logger.warning(f"注册失败,{wait}秒后重试 (第{attempt}次)...")
                time.sleep(wait)
            else:
                logger.error("达到最大重试次数,注册失败!")
                sys.exit(1)
    # 启动心跳(如果需要)
    # heartbeat_thread = Thread(target=heart_beat, daemon=True)
    # heartbeat_thread.start()
    # 保持主进程运行(或运行你的业务服务)
    logger.info("服务注册完毕,持续运行中...")
    try:
        # 如果该脚本与业务服务运行在同一进程,这里可以启动业务服务
        # 否则,脚本单纯做注册,只需保持进程存活
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        pass
    finally:
        deregister_service()
if __name__ == "__main__":
    main()

使用方法

# 安装依赖
pip install requests
# 运行(设置环境变量)
SERVICE_NAME="my-app" SERVICE_PORT=8000 python register.py

Shell 脚本(轻量级,适合简单场景或 Kubernetes Init Container)

特点:无依赖、适合快速集成,但心跳和容错处理较弱。

#!/bin/bash
# service-register.sh
set -e
# === 配置 ===
REGISTRY_URL="${REGISTRY_URL:-http://127.0.0.1:8500}"
SERVICE_NAME="${SERVICE_NAME:-my-service}"
SERVICE_PORT="${SERVICE_PORT:-8080}"
# 获取本机IP(取eth0或第一个非lo地址)
LOCAL_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -n1)
SERVICE_ID="${SERVICE_NAME}-${LOCAL_IP}-${SERVICE_PORT}"
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
register() {
    log "注册服务: ${SERVICE_ID}"
    # 使用Consul API注册
    curl -f -X PUT "${REGISTRY_URL}/v1/agent/service/register" \
        -H "Content-Type: application/json" \
        -d '{
            "ID": "'"${SERVICE_ID}"'",
            "Name": "'"${SERVICE_NAME}"'",
            "Address": "'"${LOCAL_IP}"'",
            "Port": '"${SERVICE_PORT}"',
            "Check": {
                "HTTP": "http://'"${LOCAL_IP}:${SERVICE_PORT}"'/health",
                "Interval": "10s",
                "Timeout": "3s"
            }
        }'
    log "注册成功"
}
deregister() {
    log "注销服务: ${SERVICE_ID}"
    curl -f -X PUT "${REGISTRY_URL}/v1/agent/service/deregister/${SERVICE_ID}" || true
    log "注销完成"
}
# 捕获退出信号
trap deregister SIGTERM SIGINT EXIT
# 主流程
register
# 保持进程运行(或执行业务命令)
if [ $# -gt 0 ]; then
    # 如果有传入命令,执行它(例如启动你的应用程序)
    exec "$@"
else
    # 否则无限睡眠,等待信号
    while true; do
        sleep 3600
    done
fi

使用方法(例如在容器中作为 Sidecar):

# Dockerfile 例子
FROM alpine:latest
RUN apk add --no-cache curl
COPY service-register.sh /register.sh
RUN chmod +x /register.sh
# 启动时注册服务,然后执行业务程序
CMD ["/register.sh", "/usr/bin/myapp"]

不同注册中心的差异点

注册中心 注册方式 心跳方式 注销方式
Consul PUT /v1/agent/service/register 被动(HTTP健康检查)或主动TTL PUT /v1/agent/service/deregister/{id}
Eureka POST /eureka/apps/{name} 每30秒发一次心跳 DELETE /eureka/apps/{name}/{id}
Nacos POST /nacos/v1/ns/instance 附带心跳参数(beat) DELETE /nacos/v1/ns/instance
Kubernetes 不直接调用,通过创建 Pod 自动注册 依赖kubelet存活探针 Pod删除时自动注销

要点

  • 如果是 Eureka,心跳是必须主动发送的(每30秒一次)。
  • 如果是 Consul,可以依赖健康检查接口,脚本可以不需要心跳线程。
  • 如果是 Kubernetes,你不需要写注册脚本,只需定义好 ServiceEndpoint

最佳实践与注意事项

  1. 服务ID唯一性:通常使用 服务名-IP-端口 组合,避免同一个节点多个实例冲突。
  2. 健康检查URL:务必提供一个真实的、轻量级健康检查端点(如 /health),否则注册中心会认为服务不健康。
  3. 信号处理:脚本必须捕获 SIGTERM(Docker/K8s停止容器时发送),否则服务会被注册中心认为“失联”而非“主动下线”,造成请求错误转发。
  4. 重试与退避:首次注册失败不要立即放弃,尤其网络刚启动时。
  5. 安全性:如果注册中心需要认证(Token、Basic Auth),务必在脚本中处理。

延伸:服务注册 vs 服务发现

  • 注册脚本:只负责把自己的信息告诉注册中心。
  • 服务发现:通常是客户端或网关从注册中心拉取服务列表,如果你想要一个“一站式”方案,可以考虑 Spring Cloud + EurekaConsul Template

如果你能告诉我你使用的是哪种注册中心(Consul、Eureka、Nacos、ZooKeeper还是自研),我可以给出更定制化的脚本示例。

抱歉,评论功能暂时关闭!