From c9c4da01b6a8ef5431ac85a1e070961c314bd0a3 Mon Sep 17 00:00:00 2001 From: yoyo Date: Wed, 24 Dec 2025 03:31:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E9=85=8D=E7=BD=AE=E5=8A=9F=E8=83=BD=E8=87=B3?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 install.sh 中新增 sync_time 函数,配置系统时间同步,设置时区为 Asia/Shanghai,并安装 chrony。 - 配置 NTP 服务器为阿里云和腾讯云,确保时间同步的准确性。 - 更新主函数以调用时间同步配置,优化安装流程。 --- cmd/agent/main.go | 15 --- install.sh | 138 ++++++++++++++++++-- internal/heartbeat/reporter.go | 29 ++--- internal/timesync/sync.go | 221 --------------------------------- time.sh | 53 ++++++++ 5 files changed, 189 insertions(+), 267 deletions(-) delete mode 100644 internal/timesync/sync.go create mode 100644 time.sh diff --git a/cmd/agent/main.go b/cmd/agent/main.go index c1d9436..ef24821 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -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("服务已关闭") } diff --git a/install.sh b/install.sh index 7f9eeaf..802a949 100755 --- a/install.sh +++ b/install.sh @@ -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 < /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 "" diff --git a/internal/heartbeat/reporter.go b/internal/heartbeat/reporter.go index 5bacb53..af62c5f 100644 --- a/internal/heartbeat/reporter.go +++ b/internal/heartbeat/reporter.go @@ -67,11 +67,10 @@ func GetNodeLocation() (country, province, city, isp string) { } type Reporter struct { - cfg *config.Config - client *http.Client - logger *zap.Logger - stopCh chan struct{} - beijingTZ *time.Location + cfg *config.Config + client *http.Client + logger *zap.Logger + stopCh chan struct{} } func NewReporter(cfg *config.Config) *Reporter { @@ -80,22 +79,13 @@ 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{ Timeout: 10 * time.Second, }, - logger: logger, - stopCh: make(chan struct{}), - beijingTZ: beijingTZ, + logger: logger, + stopCh: make(chan struct{}), } } @@ -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() diff --git a/internal/timesync/sync.go b/internal/timesync/sync.go deleted file mode 100644 index 585a6b5..0000000 --- a/internal/timesync/sync.go +++ /dev/null @@ -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", ×tamp); 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 -} diff --git a/time.sh b/time.sh new file mode 100644 index 0000000..468e683 --- /dev/null +++ b/time.sh @@ -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" <