feat: 添加时间同步配置功能至安装脚本

- 在 install.sh 中新增 sync_time 函数,配置系统时间同步,设置时区为 Asia/Shanghai,并安装 chrony。
- 配置 NTP 服务器为阿里云和腾讯云,确保时间同步的准确性。
- 更新主函数以调用时间同步配置,优化安装流程。
This commit is contained in:
2025-12-24 03:31:35 +08:00
parent 7a104bbe42
commit c9c4da01b6
5 changed files with 189 additions and 267 deletions

View File

@@ -13,7 +13,6 @@ import (
"linkmaster-node/internal/heartbeat"
"linkmaster-node/internal/recovery"
"linkmaster-node/internal/server"
"linkmaster-node/internal/timesync"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
@@ -55,17 +54,6 @@ func main() {
}
}
// 启动时间同步服务每30分钟同步一次
var timeSync *timesync.TimeSync
timeSync, err = timesync.NewTimeSync(logger)
if err != nil {
logger.Warn("创建时间同步器失败", zap.Error(err))
timeSync = nil
} else {
go timeSync.Start(context.Background(), 30*time.Minute)
logger.Info("时间同步服务已启动")
}
// 启动心跳上报
heartbeatReporter := heartbeat.NewReporter(cfg)
go heartbeatReporter.Start(context.Background())
@@ -91,9 +79,6 @@ func main() {
httpServer.Shutdown(ctx)
heartbeatReporter.Stop()
if timeSync != nil {
timeSync.Stop()
}
logger.Info("服务已关闭")
}

View File

@@ -410,6 +410,119 @@ install_dependencies() {
echo -e "${GREEN}✓ 系统依赖安装完成${NC}"
}
# 配置时间同步
sync_time() {
echo -e "${BLUE}配置时间同步...${NC}"
# 1. 设置时区为 Asia/Shanghai
echo -e "${BLUE}[1/6] 设置时区为 Asia/Shanghai${NC}"
if command -v timedatectl > /dev/null 2>&1; then
sudo timedatectl set-timezone Asia/Shanghai 2>/dev/null || {
echo -e "${YELLOW}⚠ timedatectl 设置时区失败,尝试其他方法${NC}"
# 尝试创建时区链接(适用于较老的系统)
if [ -f /usr/share/zoneinfo/Asia/Shanghai ]; then
sudo ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 2>/dev/null || true
fi
}
elif [ -f /usr/share/zoneinfo/Asia/Shanghai ]; then
sudo ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 2>/dev/null || true
fi
# 2. 安装 chrony
echo -e "${BLUE}[2/6] 安装 chrony${NC}"
if [ "$OS" = "ubuntu" ] || [ "$OS" = "debian" ]; then
if ! dpkg -l 2>/dev/null | grep -q "^ii.*chrony"; then
sudo apt-get install -y chrony
else
echo -e "${BLUE}chrony 已安装,跳过${NC}"
fi
elif [ "$OS" = "centos" ] || [ "$OS" = "rhel" ] || [ "$OS" = "rocky" ] || [ "$OS" = "almalinux" ]; then
if ! rpm -q chrony &>/dev/null; then
sudo yum install -y chrony
else
echo -e "${BLUE}chrony 已安装,跳过${NC}"
fi
else
echo -e "${YELLOW}⚠ 未知系统类型,跳过 chrony 安装${NC}"
return 0
fi
# 3. 配置 NTP 服务器
echo -e "${BLUE}[3/6] 配置 NTP 服务器${NC}"
CONF="/etc/chrony.conf"
if [ -f "$CONF" ]; then
# 备份配置文件
if [ ! -f "${CONF}.backup.$(date +%Y%m%d)" ]; then
sudo cp "$CONF" "${CONF}.backup.$(date +%Y%m%d)" 2>/dev/null || true
fi
# 注释掉原有的 server 行
sudo sed -i 's/^server /#server /g' "$CONF" 2>/dev/null || true
# 添加中国 NTP 服务器(如果还没有)
if ! grep -q "ntp.aliyun.com" "$CONF"; then
sudo tee -a "$CONF" > /dev/null <<EOF
# China NTP servers (added by LinkMaster Node installer)
server ntp.aliyun.com iburst
server ntp.tencent.com iburst
server ntp1.aliyun.com iburst
EOF
fi
else
echo -e "${YELLOW}⚠ chrony.conf 不存在,跳过配置${NC}"
fi
# 4. 启动并启用 chronyd
echo -e "${BLUE}[4/6] 启动 chronyd${NC}"
if command -v systemctl > /dev/null 2>&1; then
sudo systemctl enable chronyd --now 2>/dev/null || {
# 如果 systemctl 失败,尝试使用 service 命令
if command -v service > /dev/null 2>&1; then
sudo service chronyd start 2>/dev/null || true
sudo chkconfig chronyd on 2>/dev/null || true
fi
}
elif command -v service > /dev/null 2>&1; then
sudo service chronyd start 2>/dev/null || true
sudo chkconfig chronyd on 2>/dev/null || true
fi
# 等待服务启动
sleep 2
# 5. 立即强制同步
echo -e "${BLUE}[5/6] 强制同步系统时间${NC}"
if command -v chronyc > /dev/null 2>&1; then
sudo chronyc -a makestep 2>/dev/null || {
# 如果 makestep 失败,尝试使用 sources 和 sourcestats
sudo chronyc sources 2>/dev/null || true
sudo chronyc sourcestats 2>/dev/null || true
}
fi
# 6. 写入硬件时间
echo -e "${BLUE}[6/6] 写入硬件时钟${NC}"
if command -v hwclock > /dev/null 2>&1; then
sudo hwclock --systohc 2>/dev/null || true
fi
# 显示时间状态
echo -e "${BLUE}当前时间状态:${NC}"
if command -v timedatectl > /dev/null 2>&1; then
sudo timedatectl status 2>/dev/null || true
else
date
if command -v hwclock > /dev/null 2>&1; then
echo -e "${BLUE}硬件时钟:${NC}"
sudo hwclock 2>/dev/null || true
fi
fi
echo -e "${GREEN}✓ 时间同步配置完成${NC}"
}
# 从官网下载安装 Go
install_go_from_official() {
echo -e "${BLUE}从 Go 官网下载安装...${NC}"
@@ -1515,26 +1628,29 @@ main() {
echo -e "${BLUE}后端地址: ${BACKEND_URL}${NC}"
echo ""
echo -e "${BLUE}[1/8] 检测系统类型...${NC}"
echo -e "${BLUE}[1/11] 检测系统类型...${NC}"
detect_system
# 检查是否已安装,如果已安装则先卸载
if check_installed; then
echo -e "${BLUE}[2/8] 卸载已存在的服务...${NC}"
echo -e "${BLUE}[2/11] 卸载已存在的服务...${NC}"
uninstall_service
else
echo -e "${BLUE}[2/8] 检查已安装服务...${NC}"
echo -e "${BLUE}[2/11] 检查已安装服务...${NC}"
echo -e "${GREEN}✓ 未检测到已安装的服务${NC}"
fi
echo -e "${BLUE}[3/8] 检测并配置镜像源...${NC}"
echo -e "${BLUE}[3/11] 检测并配置镜像源...${NC}"
detect_fastest_mirror
echo -e "${BLUE}[4/8] 安装系统依赖...${NC}"
echo -e "${BLUE}[4/11] 安装系统依赖...${NC}"
install_dependencies
echo -e "${BLUE}[5/11] 配置时间同步...${NC}"
sync_time
# 优先尝试从 Releases 下载二进制文件
echo -e "${BLUE}[5/8] 下载或编译二进制文件...${NC}"
echo -e "${BLUE}[6/11] 下载或编译二进制文件...${NC}"
if ! download_binary_from_releases; then
echo -e "${BLUE}从 Releases 下载失败,开始从源码编译...${NC}"
build_from_source
@@ -1542,19 +1658,19 @@ main() {
echo -e "${GREEN}✓ 使用预编译二进制文件,跳过编译步骤${NC}"
fi
echo -e "${BLUE}[6/8] 创建 systemd 服务...${NC}"
echo -e "${BLUE}[7/11] 创建 systemd 服务...${NC}"
create_service
echo -e "${BLUE}[7/8] 配置防火墙规则...${NC}"
echo -e "${BLUE}[8/11] 配置防火墙规则...${NC}"
configure_firewall
echo -e "${BLUE}[8/8] 登记节点到后端服务器...${NC}"
echo -e "${BLUE}[9/11] 登记节点到后端服务器...${NC}"
register_node
echo -e "${BLUE}[9/9] 启动服务...${NC}"
echo -e "${BLUE}[10/11] 启动服务...${NC}"
start_service
echo -e "${BLUE}[10/10] 验证安装...${NC}"
echo -e "${BLUE}[11/11] 验证安装...${NC}"
verify_installation
echo ""

View File

@@ -71,7 +71,6 @@ type Reporter struct {
client *http.Client
logger *zap.Logger
stopCh chan struct{}
beijingTZ *time.Location
}
func NewReporter(cfg *config.Config) *Reporter {
@@ -80,14 +79,6 @@ func NewReporter(cfg *config.Config) *Reporter {
// 初始化节点信息(从配置文件读取)
InitNodeInfo(cfg)
// 加载北京时间时区
beijingTZ, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
// 如果加载失败使用UTC+8手动创建
beijingTZ = time.FixedZone("CST", 8*60*60)
logger.Warn("加载时区失败使用UTC+8", zap.Error(err))
}
return &Reporter{
cfg: cfg,
client: &http.Client{
@@ -95,7 +86,6 @@ func NewReporter(cfg *config.Config) *Reporter {
},
logger: logger,
stopCh: make(chan struct{}),
beijingTZ: beijingTZ,
}
}
@@ -104,9 +94,8 @@ func (r *Reporter) Start(ctx context.Context) {
r.sendHeartbeat()
for {
// 获取当前北京时间
now := time.Now().In(r.beijingTZ)
// 计算到下一分钟第1秒的时间基于北京时间
// 计算到下一分钟第1秒的时间
now := time.Now()
nextMinute := now.Truncate(time.Minute).Add(time.Minute)
nextHeartbeatTime := nextMinute.Add(1 * time.Second)
durationUntilNext := nextHeartbeatTime.Sub(now)
@@ -122,7 +111,7 @@ func (r *Reporter) Start(ctx context.Context) {
timer.Stop()
return
case <-timer.C:
// 在每分钟的第1秒发送心跳(北京时间)
// 在每分钟的第1秒发送心跳
r.sendHeartbeat()
}
timer.Stop()

View File

@@ -1,221 +0,0 @@
package timesync
import (
"context"
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
"go.uber.org/zap"
)
// TimeSync 时间同步器
type TimeSync struct {
logger *zap.Logger
stopCh chan struct{}
beijingTZ *time.Location
lastSyncTime time.Time
lastSyncError error
mu sync.RWMutex
}
// K780TimeAPIResponse K780时间API响应结构
type K780TimeAPIResponse struct {
Success string `json:"success"`
Msgid string `json:"msgid"`
Msg string `json:"msg"`
Result struct {
Timestamp string `json:"timestamp"` // 时间戳(秒)
TimestampMs string `json:"timestamp_ms"` // 时间戳(毫秒)
Datetime1 string `json:"datetime_1"` // 日期时间格式1
Datetime2 string `json:"datetime_2"` // 日期时间格式2
} `json:"result"`
}
// NewTimeSync 创建时间同步器
func NewTimeSync(logger *zap.Logger) (*TimeSync, error) {
// 加载北京时间时区
beijingTZ, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
return nil, fmt.Errorf("加载时区失败: %w", err)
}
return &TimeSync{
logger: logger,
stopCh: make(chan struct{}),
beijingTZ: beijingTZ,
}, nil
}
// syncTime 同步时间从HTTP API获取北京时间
func (ts *TimeSync) syncTime() error {
// 使用K780时间API
apiURL := "https://sapi.k780.com/?app=life.time&appkey=10003&sign=b59bc3ef6191eb9f747dd4e83c99f2a4&format=json"
client := &http.Client{
Timeout: 5 * time.Second,
}
// 创建请求,添加浏览器请求头
req, err := http.NewRequest("GET", apiURL, nil)
if err != nil {
ts.mu.Lock()
ts.lastSyncError = err
ts.mu.Unlock()
return fmt.Errorf("创建请求失败: %w", err)
}
// 添加浏览器请求头,模拟浏览器访问
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
req.Header.Set("Cache-Control", "max-age=0")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Referer", "https://www.nowapi.com/")
req.Header.Set("Sec-Fetch-Dest", "document")
req.Header.Set("Sec-Fetch-Mode", "navigate")
req.Header.Set("Sec-Fetch-Site", "cross-site")
req.Header.Set("Sec-Fetch-User", "?1")
req.Header.Set("Upgrade-Insecure-Requests", "1")
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36")
req.Header.Set("sec-ch-ua", `"Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"`)
req.Header.Set("sec-ch-ua-mobile", "?0")
req.Header.Set("sec-ch-ua-platform", `"macOS"`)
resp, err := client.Do(req)
if err != nil {
ts.mu.Lock()
ts.lastSyncError = err
ts.mu.Unlock()
return fmt.Errorf("API请求失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err := fmt.Errorf("API返回状态码: %d", resp.StatusCode)
ts.mu.Lock()
ts.lastSyncError = err
ts.mu.Unlock()
return err
}
var result K780TimeAPIResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
ts.mu.Lock()
ts.lastSyncError = err
ts.mu.Unlock()
return fmt.Errorf("解析API响应失败: %w", err)
}
// 检查返回状态
if result.Success != "1" {
errMsg := fmt.Sprintf("API返回失败状态: success=%s", result.Success)
if result.Msg != "" {
errMsg += fmt.Sprintf(", msg=%s", result.Msg)
}
if result.Msgid != "" {
errMsg += fmt.Sprintf(", msgid=%s", result.Msgid)
}
err := fmt.Errorf(errMsg)
ts.mu.Lock()
ts.lastSyncError = err
ts.mu.Unlock()
ts.logger.Warn("时间API返回失败", zap.String("success", result.Success), zap.String("msgid", result.Msgid), zap.String("msg", result.Msg))
return err
}
// 从result.timestamp字段解析时间戳字符串
if result.Result.Timestamp == "" {
err := fmt.Errorf("API返回的时间戳为空")
ts.mu.Lock()
ts.lastSyncError = err
ts.mu.Unlock()
return err
}
// 解析时间戳字符串为int64
var timestamp int64
if _, err := fmt.Sscanf(result.Result.Timestamp, "%d", &timestamp); err != nil {
err := fmt.Errorf("解析时间戳失败: %w", err)
ts.mu.Lock()
ts.lastSyncError = err
ts.mu.Unlock()
return err
}
// 使用时间戳转换为北京时间
beijingTime := time.Unix(timestamp, 0).In(ts.beijingTZ)
// 计算时间差
localTime := time.Now()
timeDiff := beijingTime.Sub(localTime)
ts.mu.Lock()
ts.lastSyncTime = beijingTime
ts.lastSyncError = nil
ts.mu.Unlock()
ts.logger.Info("时间同步成功",
zap.String("api", apiURL),
zap.String("remote_time", beijingTime.Format("2006-01-02 15:04:05")),
zap.String("local_time", localTime.Format("2006-01-02 15:04:05")),
zap.Duration("time_diff", timeDiff),
zap.Int64("timestamp", timestamp))
return nil
}
// Start 启动定期时间同步
func (ts *TimeSync) Start(ctx context.Context, interval time.Duration) {
// 立即同步一次
if err := ts.syncTime(); err != nil {
ts.logger.Warn("初始时间同步失败", zap.Error(err))
}
// 定期同步
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ts.stopCh:
return
case <-ticker.C:
if err := ts.syncTime(); err != nil {
ts.logger.Warn("时间同步失败", zap.Error(err))
}
}
}
}
// Stop 停止时间同步
func (ts *TimeSync) Stop() {
close(ts.stopCh)
}
// GetBeijingTime 获取当前北京时间
func (ts *TimeSync) GetBeijingTime() time.Time {
return time.Now().In(ts.beijingTZ)
}
// GetLocation 获取北京时区
func (ts *TimeSync) GetLocation() *time.Location {
return ts.beijingTZ
}
// GetLastSyncTime 获取最后一次同步的时间
func (ts *TimeSync) GetLastSyncTime() time.Time {
ts.mu.RLock()
defer ts.mu.RUnlock()
return ts.lastSyncTime
}
// GetLastSyncError 获取最后一次同步的错误
func (ts *TimeSync) GetLastSyncError() error {
ts.mu.RLock()
defer ts.mu.RUnlock()
return ts.lastSyncError
}

53
time.sh Normal file
View File

@@ -0,0 +1,53 @@
#!/bin/bash
set -e
echo "=== CentOS 7 时间同步脚本开始 ==="
# 1. 检查是否 root
if [ "$(id -u)" -ne 0 ]; then
echo "请使用 root 用户执行"
exit 1
fi
# 2. 设置时区
echo "[1/6] 设置时区为 Asia/Shanghai"
timedatectl set-timezone Asia/Shanghai
# 3. 安装 chrony
echo "[2/6] 安装 chrony"
if ! rpm -q chrony &>/dev/null; then
yum install -y chrony
else
echo "chrony 已安装,跳过"
fi
# 4. 配置 NTP 服务器
echo "[3/6] 配置 NTP 服务器"
CONF="/etc/chrony.conf"
sed -i 's/^server /#server /g' "$CONF"
grep -q "ntp.aliyun.com" "$CONF" || cat >> "$CONF" <<EOF
# China NTP servers
server ntp.aliyun.com iburst
server ntp.tencent.com iburst
server ntp1.aliyun.com iburst
EOF
# 5. 启动并启用 chronyd
echo "[4/6] 启动 chronyd"
systemctl enable chronyd --now
# 6. 立即强制同步
echo "[5/6] 强制同步系统时间"
chronyc -a makestep
# 7. 写入硬件时间
echo "[6/6] 写入硬件时钟"
hwclock --systohc
echo "=== 时间同步完成 ==="
echo
timedatectl status