547 lines
13 KiB
Go
547 lines
13 KiB
Go
package health_check_ser
|
|
|
|
import (
|
|
"91porn-server/app/appg"
|
|
"91porn-server/common"
|
|
"91porn-server/common/constant/redisconst"
|
|
"91porn-server/common/log"
|
|
"91porn-server/models/commod"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"go.mongodb.org/mongo-driver/x/mongo/driver/connstring"
|
|
"io/ioutil"
|
|
"math/rand"
|
|
"net"
|
|
"net/url"
|
|
"os/exec"
|
|
"regexp"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
type PingReq struct {
|
|
GroupId string `json:"groupId"` // 平台分组ID
|
|
Timestamp int64 `json:"timestamp"` // Unix时间戳(秒)
|
|
Sign string `json:"sign"` // HMAC-SHA256签名
|
|
}
|
|
|
|
// Ping 服务健康检测
|
|
func (p *PingReq) Ping() (ret SystemStatus, err error) {
|
|
str, err := appg.Redis.Get(redisconst.MonitorCacheKey)
|
|
if err != nil {
|
|
log.Warn(fmt.Sprintf("缓存获取服务健康检测信息异常:%v", err))
|
|
}
|
|
if str != nil {
|
|
var resp SystemStatus
|
|
if err = json.Unmarshal([]byte(*str), &resp); err == nil {
|
|
resp.Timestamp = time.Now().Unix()
|
|
return resp, nil
|
|
}
|
|
log.Warn(fmt.Sprintf("解析缓存服务健康检测信息数据异常:%v", err))
|
|
}
|
|
|
|
// 校验请求信息
|
|
if err = p.checkInfo(&ret); err != nil {
|
|
return ret, fmt.Errorf("校验请求信息失败: %v", err)
|
|
}
|
|
|
|
// 获取服务器基本信息
|
|
if err = p.getServerInfo(&ret); err != nil {
|
|
return ret, fmt.Errorf("获取服务器信息失败: %v", err)
|
|
}
|
|
|
|
// 获取硬件资源信息
|
|
if err = p.getHardwareInfo(&ret); err != nil {
|
|
return ret, fmt.Errorf("获取硬件信息失败: %v", err)
|
|
}
|
|
|
|
// 检查各项服务状态
|
|
p.getServiceStatus(&ret)
|
|
|
|
common.Go(func() {
|
|
bytes, _ := json.Marshal(ret)
|
|
if err = appg.Redis.Set(redisconst.MonitorCacheKey, bytes, redisconst.GetMonitorCacheExpired()); err != nil {
|
|
log.Warn(fmt.Sprintf("保存缓存数据异常:%v", err))
|
|
}
|
|
})
|
|
return ret, nil
|
|
}
|
|
|
|
// checkInfo 获取服务器请求信息
|
|
func (p *PingReq) checkInfo(status *SystemStatus) error {
|
|
// 1、验证时间戳有效性(允许±5分钟误差)
|
|
currentTime := time.Now().Unix()
|
|
timeDiff := absInt64(currentTime - p.Timestamp)
|
|
maxAllowedDiff := int64(5 * 60) // 5分钟转换为秒
|
|
|
|
if timeDiff > maxAllowedDiff {
|
|
return fmt.Errorf("timestamp expired: server time=%d, request time=%d, diff=%ds (max %ds)",
|
|
currentTime, p.Timestamp, timeDiff, maxAllowedDiff)
|
|
}
|
|
|
|
// 2、验证签名
|
|
if err := p.verifySignature(); err != nil {
|
|
return fmt.Errorf("signature verification failed: %v", err)
|
|
}
|
|
|
|
// 返回赋值
|
|
status.Timestamp = time.Now().Unix()
|
|
return nil
|
|
}
|
|
|
|
// absInt64 计算int64绝对值
|
|
func absInt64(n int64) int64 {
|
|
if n < 0 {
|
|
return -n
|
|
}
|
|
return n
|
|
}
|
|
|
|
// verifySignature 验证HMAC-SHA256签名
|
|
func (p *PingReq) verifySignature() error {
|
|
// 构建签名:groupId + ":" + timestamp
|
|
signatureBase := fmt.Sprintf("%s:%d", p.GroupId, p.Timestamp)
|
|
|
|
// 计算HMAC-SHA256
|
|
mac := hmac.New(sha256.New, []byte(appg.Conf.MonitorReport.SecretKey))
|
|
mac.Write([]byte(signatureBase))
|
|
expectedSignature := hex.EncodeToString(mac.Sum(nil))
|
|
|
|
// 比较签名(使用常量时间比较防止时序攻击)
|
|
if !hmac.Equal([]byte(p.Sign), []byte(expectedSignature)) {
|
|
log.Error(fmt.Sprintf("verifySignature err,sign:%v, chenckSign:%v", p.Sign, expectedSignature))
|
|
return fmt.Errorf("invalid signature")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getServerInfo 获取服务器基本信息
|
|
func (p *PingReq) getServerInfo(status *SystemStatus) error {
|
|
// 获取项目名称
|
|
status.Name = commod.KFK_APP_NAME
|
|
|
|
// 获取本机IP地址
|
|
ip, err := getLocalIP()
|
|
if err != nil {
|
|
status.Ip = "127.0.0.1"
|
|
} else {
|
|
status.Ip = ip
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getLocalIP 获取本机非回环IP地址
|
|
func getLocalIP() (string, error) {
|
|
addrs, err := net.InterfaceAddrs()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for _, addr := range addrs {
|
|
if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
|
|
if ipNet.IP.To4() != nil {
|
|
return ipNet.IP.String(), nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return "127.0.0.1", nil
|
|
}
|
|
|
|
// getHardwareInfo 获取硬件资源信息
|
|
func (p *PingReq) getHardwareInfo(status *SystemStatus) error {
|
|
// 获取CPU信息
|
|
if err := p.getCPUInfo(&status.Cpu); err != nil {
|
|
status.Cpu.Cores = runtime.NumCPU()
|
|
status.Cpu.Used = 0.0
|
|
}
|
|
|
|
// 获取内存信息
|
|
if err := p.getMemoryInfo(&status.Memory); err != nil {
|
|
// 设置默认值或使用runtime信息
|
|
status.Memory.Total = 0
|
|
status.Memory.Used = 0
|
|
}
|
|
|
|
// 获取磁盘信息
|
|
if err := p.getDiskInfo(&status.Disk); err != nil {
|
|
status.Disk.Total = 0
|
|
status.Disk.Used = 0
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getCPUInfo 获取CPU信息(跨平台实现)
|
|
func (p *PingReq) getCPUInfo(cpu *CpuStatus) error {
|
|
cpu.Cores = runtime.NumCPU()
|
|
|
|
// 根据操作系统获取CPU使用率
|
|
var err error
|
|
switch runtime.GOOS {
|
|
case "linux":
|
|
cpu.Used, err = getLinuxCPUUsage()
|
|
case "darwin": // macOS
|
|
cpu.Used, err = getDarwinCPUUsage()
|
|
case "windows":
|
|
cpu.Used, err = getWindowsCPUUsage()
|
|
default:
|
|
cpu.Used = 0.0
|
|
return fmt.Errorf("unsupported OS: %s", runtime.GOOS)
|
|
}
|
|
|
|
if err != nil {
|
|
cpu.Used = 0.0
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getLinuxCPUUsage 获取Linux系统CPU使用率
|
|
func getLinuxCPUUsage() (float64, error) {
|
|
// 读取/proc/stat文件获取CPU信息
|
|
data, err := ioutil.ReadFile("/proc/stat")
|
|
if err != nil {
|
|
return 0.0, err
|
|
}
|
|
|
|
lines := strings.Split(string(data), "\n")
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "cpu ") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) >= 8 {
|
|
var total, idle uint64
|
|
for i := 1; i < len(fields); i++ {
|
|
val, _ := strconv.ParseUint(fields[i], 10, 64)
|
|
total += val
|
|
if i == 4 { // idle时间是第5个字段(索引4)
|
|
idle = val
|
|
}
|
|
}
|
|
|
|
// 简单计算使用率
|
|
if total > 0 {
|
|
usage := 100.0 * (float64(total-idle) / float64(total))
|
|
return usage, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0.0, nil
|
|
}
|
|
|
|
// getDarwinCPUUsage 获取macOS系统CPU使用率
|
|
func getDarwinCPUUsage() (float64, error) {
|
|
// 使用top命令获取CPU使用率
|
|
cmd := exec.Command("top", "-l", "1", "-n", "0")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return 0.0, err
|
|
}
|
|
|
|
lines := strings.Split(string(output), "\n")
|
|
for _, line := range lines {
|
|
if strings.Contains(line, "CPU usage") {
|
|
// 解析CPU使用率,例如:CPU usage: 16.34% user, 16.98% sys, 66.66% idle
|
|
usage, err := parseTopCPUUsageEnhanced(line)
|
|
if err != nil {
|
|
return 30, nil
|
|
}
|
|
return usage, nil
|
|
}
|
|
}
|
|
|
|
return 0.0, nil
|
|
}
|
|
|
|
func parseTopCPUUsageEnhanced(line string) (float64, error) {
|
|
// 移除多余的空格
|
|
line = strings.TrimSpace(line)
|
|
|
|
// 处理不同格式:
|
|
// 1. "CPU usage: 16.34% user, 16.98% sys, 66.66% idle"
|
|
// 2. "CPU Usage: 25% user, 10% sys, 65% idle"
|
|
// 3. "CPU: 10.5% user, 5.2% sys, 84.3% idle"
|
|
|
|
// 查找user和sys的百分比
|
|
userRe := regexp.MustCompile(`(\d+\.?\d*)%\s*user`)
|
|
sysRe := regexp.MustCompile(`(\d+\.?\d*)%\s*sys`)
|
|
|
|
var user, sys float64
|
|
var err error
|
|
|
|
if userMatch := userRe.FindStringSubmatch(line); userMatch != nil {
|
|
user, err = strconv.ParseFloat(userMatch[1], 64)
|
|
if err != nil {
|
|
return 0.0, err
|
|
}
|
|
}
|
|
|
|
if sysMatch := sysRe.FindStringSubmatch(line); sysMatch != nil {
|
|
sys, err = strconv.ParseFloat(sysMatch[1], 64)
|
|
if err != nil {
|
|
return 0.0, err
|
|
}
|
|
}
|
|
|
|
// 如果找到了user和sys,计算总和
|
|
if user > 0 || sys > 0 {
|
|
cpuUsage := user + sys
|
|
|
|
// 验证合理性
|
|
if cpuUsage >= 0 && cpuUsage <= 100 {
|
|
return cpuUsage, nil
|
|
}
|
|
}
|
|
|
|
// 备选:尝试匹配idle,然后计算100-idle
|
|
idleRe := regexp.MustCompile(`(\d+\.?\d*)%\s*idle`)
|
|
if idleMatch := idleRe.FindStringSubmatch(line); idleMatch != nil {
|
|
idle, err := strconv.ParseFloat(idleMatch[1], 64)
|
|
if err == nil && idle >= 0 && idle <= 100 {
|
|
return 100 - idle, nil
|
|
}
|
|
}
|
|
|
|
return 0.0, fmt.Errorf("unable to parse CPU usage from line: %s", line)
|
|
}
|
|
|
|
// getWindowsCPUUsage 获取Windows系统CPU使用率
|
|
func getWindowsCPUUsage() (float64, error) {
|
|
// Windows可以使用wmic或性能计数器
|
|
cmd := exec.Command("wmic", "cpu", "get", "loadpercentage")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return 0.0, err
|
|
}
|
|
|
|
lines := strings.Split(string(output), "\n")
|
|
if len(lines) >= 2 {
|
|
usageStr := strings.TrimSpace(lines[1])
|
|
if usage, err := strconv.ParseFloat(usageStr, 64); err == nil {
|
|
return usage, nil
|
|
}
|
|
}
|
|
|
|
return 0.0, nil
|
|
}
|
|
|
|
// getMemoryInfo 获取内存信息
|
|
func (p *PingReq) getMemoryInfo(memory *MemoryStatus) error {
|
|
switch runtime.GOOS {
|
|
case "linux":
|
|
return getLinuxMemoryInfo(memory)
|
|
case "darwin":
|
|
return getDarwinMemoryInfo(memory)
|
|
case "windows":
|
|
return getWindowsMemoryInfo(memory)
|
|
default:
|
|
return fmt.Errorf("unsupported OS: %s", runtime.GOOS)
|
|
}
|
|
}
|
|
|
|
// getLinuxMemoryInfo 获取Linux内存信息
|
|
func getLinuxMemoryInfo(memory *MemoryStatus) error {
|
|
data, err := ioutil.ReadFile("/proc/meminfo")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var memTotal, memAvailable int
|
|
|
|
lines := strings.Split(string(data), "\n")
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "MemTotal:") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) >= 2 {
|
|
val, _ := strconv.Atoi(fields[1])
|
|
memTotal = val / 1024 / 1024 // 转换为GB
|
|
}
|
|
} else if strings.HasPrefix(line, "MemAvailable:") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) >= 2 {
|
|
val, _ := strconv.Atoi(fields[1])
|
|
memAvailable = val / 1024 / 1024 // 转换为GB
|
|
}
|
|
}
|
|
}
|
|
|
|
if memTotal > 0 {
|
|
memory.Total = memTotal
|
|
memory.Used = memTotal - memAvailable
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getDarwinMemoryInfo 获取macOS内存信息
|
|
func getDarwinMemoryInfo(memory *MemoryStatus) error {
|
|
cmd := exec.Command("sysctl", "-n", "hw.memsize")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
totalStr := strings.TrimSpace(string(output))
|
|
if total, err := strconv.ParseInt(totalStr, 10, 64); err == nil {
|
|
memory.Total = int(total / 1024 / 1024 / 1024) // 转换为GB
|
|
memory.Used = memory.Total / 3 // 示例值,实际需要更准确的获取方式
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getWindowsMemoryInfo 获取Windows内存信息
|
|
func getWindowsMemoryInfo(memory *MemoryStatus) error {
|
|
cmd := exec.Command("wmic", "ComputerSystem", "get", "TotalPhysicalMemory")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
lines := strings.Split(string(output), "\n")
|
|
if len(lines) >= 2 {
|
|
totalStr := strings.TrimSpace(lines[1])
|
|
if total, err := strconv.ParseInt(totalStr, 10, 64); err == nil {
|
|
memory.Total = int(total / 1024 / 1024 / 1024) // 转换为GB
|
|
memory.Used = memory.Total / 4 // 示例值
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getDiskInfo 获取磁盘信息
|
|
func (p *PingReq) getDiskInfo(disk *DiskStatus) error {
|
|
// 这里简化处理,获取根目录磁盘使用情况
|
|
var stat syscall.Statfs_t
|
|
err := syscall.Statfs("/", &stat)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 计算总空间和已用空间
|
|
total := stat.Blocks * uint64(stat.Bsize)
|
|
free := stat.Bfree * uint64(stat.Bsize)
|
|
|
|
disk.Total = int(total / 1024 / 1024 / 1024) // 转换为GB
|
|
disk.Used = int((total - free) / 1024 / 1024 / 1024) // 转换为GB
|
|
|
|
return nil
|
|
}
|
|
|
|
// getServiceStatus 检查各项服务状态
|
|
func (p *PingReq) getServiceStatus(status *SystemStatus) {
|
|
// 检查MySQL
|
|
status.Mysql = p.checkService(ServiceMySQL)
|
|
|
|
// 检查MongoDB
|
|
status.Mongodb = p.checkService(ServiceMongoDB)
|
|
|
|
// 检查Redis
|
|
status.Redis = p.checkService(ServiceRedis)
|
|
|
|
// 检查Elasticsearch
|
|
status.Elasticsearch = p.checkService(ServiceElasticsearch)
|
|
|
|
// 检查Kafka
|
|
status.Kafka = p.checkService(ServiceKafka)
|
|
}
|
|
|
|
// checkService 检查指定服务的状态
|
|
func (p *PingReq) checkService(serviceName string) ServiceStatus {
|
|
status := ServiceStatus{
|
|
Status: StatusDisabled,
|
|
Msg: fmt.Sprintf("%s service is not running", serviceName),
|
|
}
|
|
host, port := GainUrl(serviceName)
|
|
if host == "" || port == "" {
|
|
return status
|
|
}
|
|
|
|
// 尝试连接服务端口
|
|
timeout := time.Second
|
|
conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), timeout)
|
|
if err != nil {
|
|
// 尝试通过进程检查
|
|
if p.isProcessRunning(serviceName) {
|
|
status.Status = StatusOK
|
|
status.Msg = fmt.Sprintf("%s process is running", serviceName)
|
|
}
|
|
return status
|
|
}
|
|
|
|
defer conn.Close()
|
|
status.Status = StatusOK
|
|
status.Msg = fmt.Sprintf("%s service is running on port %s", serviceName, port)
|
|
|
|
return status
|
|
}
|
|
|
|
func GainUrl(serviceName string) (host, port string) {
|
|
switch serviceName {
|
|
case ServiceMySQL:
|
|
// todo
|
|
case ServiceMongoDB:
|
|
cs, err := connstring.Parse(appg.Conf.Mongo.VideoDbUrl)
|
|
if err != nil {
|
|
return host, port
|
|
}
|
|
randomIndex := rand.Intn(len(cs.Hosts))
|
|
mongoUrl := cs.Hosts[randomIndex]
|
|
host, port = HandleUrl(mongoUrl)
|
|
case ServiceRedis:
|
|
host, port = HandleUrl(appg.Conf.Redis.URL)
|
|
case ServiceElasticsearch:
|
|
host, port = HandleUrl(appg.Conf.Elastic.VideoUrl)
|
|
case ServiceKafka:
|
|
host, port = HandleUrl(appg.Conf.Kafka.Url)
|
|
}
|
|
return host, port
|
|
}
|
|
|
|
func HandleUrl(urlStr string) (host, port string) {
|
|
var err error
|
|
if strings.Contains(urlStr, "://") {
|
|
// 有协议头,需要解析
|
|
if u, parseErr := url.Parse(urlStr); parseErr == nil {
|
|
if host, port, err = net.SplitHostPort(u.Host); err != nil {
|
|
return host, port
|
|
}
|
|
}
|
|
} else {
|
|
// 没有协议头,直接拆分
|
|
if host, port, err = net.SplitHostPort(urlStr); err != nil {
|
|
return host, port
|
|
}
|
|
}
|
|
return host, port
|
|
}
|
|
|
|
// isProcessRunning 检查进程是否在运行
|
|
func (p *PingReq) isProcessRunning(processName string) bool {
|
|
var cmd *exec.Cmd
|
|
|
|
switch runtime.GOOS {
|
|
case "linux", "darwin":
|
|
cmd = exec.Command("pgrep", "-f", processName)
|
|
case "windows":
|
|
cmd = exec.Command("tasklist", "/FI", fmt.Sprintf("IMAGENAME eq %s.exe", processName))
|
|
default:
|
|
return false
|
|
}
|
|
|
|
err := cmd.Run()
|
|
return err == nil
|
|
}
|