> ## Documentation Index
> Fetch the complete documentation index at: https://docs.akria.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Day 30 Pt.2 | 探针 Agent 开发

> 使用 Go 实现轻量级、跨平台的系统监控探针

<Info>
  **实战阶段 II**：大脑（Master）已经就绪，现在我们需要编写派往各处的“斥候”——**Agent**。这个 Go 程序将运行在业务服务器上。它必须满足运维的三大铁律：**占用低**（不能干扰业务）、**零依赖**（部署时不许安装 Runtime）、**自愈强**（Master 挂了不能崩）。
</Info>

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="book" size={26} color="#059669" /> 学习内容 (30 mins)</span>

<AccordionGroup>
  <Accordion title="1. gopsutil: 采集神器 (10 mins)" icon="gauge">
    **如何获取 CPU 使用率？**

    * **Linux 原理**: 读取 `/proc/stat` 文件，计算时间片差值。
    * **Go 原理**: 我们不需要自己解析文件。`gopsutil` 库封装了底层系统调用 (syscall)，能跨平台（Linux/Windows/Mac）一致地获取信息。
    * **注意事项**: 获取 CPU 使用率需要**采样时间**。比如采样 1 秒，意味着函数会阻塞 1 秒来计算这一秒内的平均负载。
  </Accordion>

  <Accordion title="2. Resty: 不只是 HTTP Client (10 mins)" icon="globe">
    **为什么不用 net/http 标准库？**

    * **标准库**: 功能太基础，设置超时、重试、JSON 序列化都要自己写很多代码。
    * **Resty**: 类似 Python 的 Requests 库。它内置了 **Automatic Retries** (自动重试) 和 **Connection Pooling** (连接池)，这对 Agent 这种需要长期运行的网络程序至关重要。
  </Accordion>

  <Accordion title="3. 交叉编译 (Cross Compilation) (10 mins)" icon="microchip">
    **DevOps 的魔法**

    * **场景**: 你在 Mac 上开发，但生产服务器是 Linux，甚至还有几台 Windows。
    * **做法**: 只需要在编译时指定 `GOOS=linux` 和 `GOARCH=amd64`。
    * **结果**: Go 编译器会直接生成目标平台机器码。这也是 Go 在运维领域吊打 Python 的核心原因——**分发极其容易**。
  </Accordion>
</AccordionGroup>

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="code" size={26} color="#059669" /> 开发任务 (90 mins)</span>

<Steps>
  <Step title="步骤 1: 初始化项目与依赖">
    确保你已经初始化了 Go 项目并拉取了库。

    <CodeGroup>
      ```bash Terminal theme={null}
      mkdir -p ~/final-project/agent && cd ~/final-project/agent

      # 1. 初始化模块
      go mod init github.com/akria/monitor-agent

      # 2. 获取系统信息采集库
      go get github.com/shirou/gopsutil/v3

      # 3. 获取 HTTP 客户端库
      go get github.com/go-resty/resty/v2
      ```
    </CodeGroup>
  </Step>

  <Step title="步骤 2: 编写采集与上报逻辑">
    单文件 `main.go` 搞定一切。我们将结构体定义、采集逻辑、上报逻辑写在一起。

    <CodeGroup>
      ```go agent/main.go theme={null}
      package main

      import (
          "fmt"
          "os"
          "time"

          "github.com/go-resty/resty/v2"
          "github.com/shirou/gopsutil/v3/cpu"
          "github.com/shirou/gopsutil/v3/disk"
          "github.com/shirou/gopsutil/v3/mem"
      )

      // Data Model: 必须与 Python Master 的 Pydantic Schema 一致
      // json tag 指定了序列化后的字段名
      type MetricPayload struct {
          Hostname string  `json:"hostname"`
          IP       string  `json:"ip"`
          CPU      float64 `json:"cpu"`
          Mem      float64 `json:"mem"`
          Disk     float64 `json:"disk"`
      }

      // 配置常量
      const MasterURL = "http://127.0.0.1:8000/api/v1/report"

      // CollectAndReport 执行一次完整的 "采集 -> 上报" 流程
      func CollectAndReport(client *resty.Client) {
          // --- 1. 采集阶段 ---
          host, _ := os.Hostname()
          
          // CPU: 采样 1秒。注意这会阻塞当前协程 1秒
          // 返回的是一个 slice，因为可能是多核，我们取第一个(总平均)
          cpuPercent, _ := cpu.Percent(1*time.Second, false)
          
          // Mem & Disk
          memStats, _ := mem.VirtualMemory()
          diskStats, _ := disk.Usage("/") // 采集根分区

          // --- 2. 组装阶段 ---
          payload := MetricPayload{
              Hostname: host,
              IP:       "192.168.1.10", // 暂写死，拓展任务里教怎么获取
              CPU:      cpuPercent[0],
              Mem:      memStats.UsedPercent,
              Disk:     diskStats.UsedPercent,
          }

          // --- 3. 上报阶段 ---
          fmt.Printf("📤 Reporting: %+v ... ", payload)
          
          // Chain API 调用风格
          resp, err := client.R().
              SetBody(payload).
              Post(MasterURL)

          if err != nil {
              fmt.Printf("❌ Failed: %v\n", err)
              return
          }
          
          // 检查业务状态码
          if resp.StatusCode() == 201 {
              fmt.Println("✅ Sent.")
          } else {
              fmt.Printf("⚠️ Server Error: %s\n", resp.Status())
          }
      }

      func main() {
          fmt.Println("🚀 Agent version 1.0.0 is running...")
          
          // 1. 初始化 HTTP Client (复用连接池)
          client := resty.New()
          client.SetTimeout(2 * time.Second) // 务必设置超时，防止卡死
          client.SetRetryCount(3)            // 自动重试 3 次

          // 2. 立即执行一次 (启动时自检)
          CollectAndReport(client)

          // 3. 启动定时器 (Ticker)
          // 类似于 Python 的 while True + sleep，但更精准
          ticker := time.NewTicker(5 * time.Second)
          
          // 4. 阻塞主线程，不断响应 Ticker 事件
          for range ticker.C {
              CollectAndReport(client)
          }
      }
      ```
    </CodeGroup>

    **代码解释**：

    * **`cpu.Percent(1*time.Second, false)`**：采样 1 秒计算 CPU 使用率，会阻塞 1 秒
    * **`resty.New()`**：创建 HTTP 客户端，支持连接池和自动重试
    * **`time.NewTicker(5 * time.Second)`**：定时器，每 5 秒触发一次
    * **`for range ticker.C`**：阻塞主线程，响应定时器事件

    **Agent 的设计哲学**：

    * **轻量级**：资源占用小，不影响业务
    * **自愈能力**：Master 挂了不崩溃，自动重试
    * **零依赖**：编译后是单个二进制，无需运行时

    **验证步骤**:

    1. **启动 Master**: 确保 Pt.1 开发的 Master 服务正在运行
    2. **运行 Agent**: `go run main.go`
    3. **观察终端**:
       * Agent 端显示: `📤 Reporting... ✅ Sent.`
       * Master 端显示: `POST /api/v1/report 201`
    4. **断在 Master**: 关掉 Master 服务。Agent 应该报错 `Connection Refused`，但**不会崩溃退出**，而是等待下一次 Ticker。这就是健壮性
    5. **恢复 Master**: 重新启动 Master，Agent 应该自动恢复上报

    **常见错误**：

    <Warning>
      * ❌ **CPU 始终为 0**: 因为 `cpu.Percent(0, false)` 没给采样时间。必须给 `1*time.Second` 或更多。
      * ❌ **Panic: index out of range**: 如果系统极度繁忙导致获取不到 CPU 数据，`cpuPercent` 可能是空的。生产代码需要 check `len(cpuPercent) > 0`。
    </Warning>
  </Step>

  <Step title="步骤 3: 跨平台编译实战">
    假设公司的服务器是 Linux 的。

    <CodeGroup>
      ```bash Compile theme={null}
      # 编译成 Linux 64位 可执行文件
      # CGO_ENABLED=0: 禁用 CGO，确保生成纯静态二进制，不依赖系统 libc
      CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o monitor_agent_linux main.go

      # 检查产物
      ls -lh monitor_agent_linux
      # 你会发现它只有 7-10MB 左右
      ```
    </CodeGroup>
  </Step>
</Steps>

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="lightbulb" size={26} color="#059669" /> 拓展任务 (30 mins)</span>

<CardGroup cols={2}>
  <Card title="配置化改造" icon="gears">
    **任务**：目前 `MasterURL` 是写死的。

    **挑战**：改造代码，优先读取环境变量 `MASTER_URL`。如果没有环境变量，再读取同目录下 `config.json` 文件。这是标准的云原生应用配置加载顺序。
  </Card>

  <Card title="IP 获取难题" icon="network-wired">
    **任务**：如何获取机器的真实 Outbound IP？

    **提示**：不要遍历网卡（可能会拿到虚拟 IP）。

    ```go theme={null}
    // 原理：假装要连 Google DNS，看系统给分配了哪个本地 IP
    conn, _ := net.Dial("udp", "8.8.8.8:80")
    defer conn.Close()
    localAddr := conn.LocalAddr().(*net.UDPAddr)
    return localAddr.IP.String()
    ```
  </Card>
</CardGroup>

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="file" size={26} color="#059669" /> 今日产出物</span>

* `monitor_agent_linux` - 可直接 scp 到服务器运行的二进制文件
* `main.go` - 具备重试和超时控制的 Agent 源码

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="github" size={26} color="#059669" /> 参考代码</span>

<CardGroup cols={2}>
  <Card title="查看参考代码" icon="code" href="https://github.com/akriamail/insightful-ops/tree/main/docs/mintlify/scripts/training/30daystudy/week-5/day-30/agent">
    GitHub 完整代码仓库
  </Card>

  <Card title="Resty 文档" icon="book" href="https://github.com/go-resty/resty">
    Go 最流行的 HTTP 客户端库
  </Card>
</CardGroup>

***

## 实际应用场景

<CardGroup cols={2}>
  <Card title="Sidecar 模式" icon="box">
    * 在 K8s 中，Agent 也可以作为 Sidecar 容器和业务容器跑在一个 Pod 里。
    * 虽然 Agent 是 Go 写的二进制，但依然可以封装进极小的 Docker 镜像（使用 `scratch` 或 `alpine` 基础镜像）。
  </Card>
</CardGroup>

<Tip>
  **Next Step**: 左手 Python Master，右手 Go Agent。现在，我们要把它们装进 Docker 容器里，完成**终极交付 (Pt.3)**。
</Tip>

***

<CardGroup cols={2}>
  <Card title="上一阶段: Master 开发" href="/training/30daystudy/day-30-part1">
    Day 30 | Pt.1 后端服务开发
  </Card>

  <Card title="下一阶段: 部署交付" href="/training/30daystudy/day-30-deploy">
    Day 30 | Pt.3 毕业交付与总结
  </Card>
</CardGroup>
