本文目录导读:

编写一个服务注册上报脚本,通常用于服务发现系统(如 Consul、Eureka、Nacos 或 Kubernetes),或者自定义的注册中心,本质上是将服务实例的信息(IP、端口、健康检查接口、元数据等)发送给注册中心。
下面我将从设计思路、通用脚本架构到具体代码示例(Python 和 Shell)为你详细讲解。
核心设计思路
一个好的注册脚本需要解决以下问题:
- 自动获取本机信息:获取内网/外网 IP、随机端口(如果是动态端口)。
- 心跳续约:注册后定期发送心跳,防止服务被踢下线。
- 优雅注销:脚本退出或被杀死时,主动从注册中心注销。
- 重试与容错:注册失败或网络抖动时的重试逻辑。
- 可配置化:通过配置文件或环境变量控制地址、端口、环境等。
通用脚本架构(伪代码)
读取配置文件 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,你不需要写注册脚本,只需定义好
Service和Endpoint。
最佳实践与注意事项
- 服务ID唯一性:通常使用
服务名-IP-端口组合,避免同一个节点多个实例冲突。 - 健康检查URL:务必提供一个真实的、轻量级健康检查端点(如
/health),否则注册中心会认为服务不健康。 - 信号处理:脚本必须捕获
SIGTERM(Docker/K8s停止容器时发送),否则服务会被注册中心认为“失联”而非“主动下线”,造成请求错误转发。 - 重试与退避:首次注册失败不要立即放弃,尤其网络刚启动时。
- 安全性:如果注册中心需要认证(Token、Basic Auth),务必在脚本中处理。
延伸:服务注册 vs 服务发现
- 注册脚本:只负责把自己的信息告诉注册中心。
- 服务发现:通常是客户端或网关从注册中心拉取服务列表,如果你想要一个“一站式”方案,可以考虑 Spring Cloud + Eureka 或 Consul Template。
如果你能告诉我你使用的是哪种注册中心(Consul、Eureka、Nacos、ZooKeeper还是自研),我可以给出更定制化的脚本示例。