Skip to main content
今日目标:运维最常见的场景就是“查日志”。当日志文件只有 10MB 时,Python 的 Pandas 很方便;但当日志达到 10GB 甚至 1TB 时,Go 的 流式处理 (Streaming)多核并行能力 就能教做人了。今天我们将编写一个能快速从海量 access.log 中提取访问前十 IP 的工具。

学习内容 (30 mins)

在开始写代码前,理解为什么传统的“一次性读取”方法在处理大文件时由于内存限制而不可行。
内存友好的读取方式
  • 错误做法: os.ReadFile() 会尝试将整个文件加载到内存。如果文件只有 100MB 没事,但如果是 10GB 的日志,你的程序会瞬间 OOM崩溃。
  • 正确做法: 使用 bufio.NewScanner(file)。它像个漏斗,每次只从硬盘里取一行数据到内存,处理完后再取下一行。无论文件多大,内存占用始终保持在几 KB。
如何榨干 CPU?
  • IO 瓶颈: 读硬盘是慢的,通常用一个协程专门负责读。
  • CPU 瓶颈: 字符串切割、正则匹配是消耗 CPU 的。如果单核跑,硬盘读得快但 CPU 算不过来。
  • 解决方案:
    1. Reader (1个): 负责读硬盘,把行塞进 lines 通道。
    2. Worker (N个): 启动 8-16 个协程从通道抢行,并行进行字符串处理。
    3. Map 安全: 多个 Worker 同时写一个 Map 会 Panic,需要加锁或者用汇总 Channel。

代码任务 (90 mins)

1

准备模拟大数据

在动手写 Go 之前,先准备好靶场数据。我们需要造一个大文件来测试性能。验证步骤: 在终端执行以下命令,生成一个约 50MB 的日志文件 access.log
# 使用 Shell 快速生成 50 万行模拟 Nginx 日志 (约 50MB)
# 格式: IP - - [Date] "Request" Status Size
for i in {1..500000}; do 
  echo "192.168.1.$((RANDOM % 255)) - - [31/Jan/2024:22:00:00] \"GET /index.html HTTP/1.1\" 200 1234" >> access.log
done
2

代码实战:并发分析器

我们将编写 26_log_analyzer.go,展示如何安全地在多个协程间共享计数结果。
package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
    "sync"
    "time"
)

// 全局统计结果
var (
    ipCounter = make(map[string]int)
    mu        sync.Mutex // 互斥锁,防止多个并发写 Map 导致 Panic
)

// Worker 函数:负责处理具体的字符串逻辑
func analyzer(lines <-chan string, wg *sync.WaitGroup) {
    defer wg.Done()
    
    // 不断从通道取日志行
    for line := range lines {
        // 1. 极简提取方案 (strings.Split 比正则快 10 倍以上)
        // 假设日志格式为 "IP - - ...", 空格分割后的第一个元素就是 IP
        parts := strings.Split(line, " ")
        if len(parts) > 0 {
            ip := parts[0]
            
            // 2. 加锁更新全局 Map
            // 临界区要尽可能小,处理完数据再加锁
            mu.Lock()
            ipCounter[ip]++
            mu.Unlock()
        }
    }
}

func main() {
    start := time.Now()
    
    // 打开文件
    file, err := os.Open("access.log")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer file.Close()

    // 3. 通道与同步控制
    // 缓冲区设大一点 (5000),防止读文件太快把通道塞满阻塞了 Reader
    lines := make(chan string, 5000) 
    var wg sync.WaitGroup

    // 启动 4 个分析 Worker (通常设置为 CPU 核心数)
    for i := 0; i < 4; i++ {
        wg.Add(1)
        go analyzer(lines, &wg)
    }

    // 4. 主线程负责读文件 (Producer)
    // 读硬盘通常是单线程的瓶颈,所以用主线程读即可
    scanner := bufio.NewScanner(file)
    fmt.Println("🚀 Analysis started...")
    
    count := 0
    for scanner.Scan() {
        lines <- scanner.Text() // 把读到的行发给 Worker
        count++
    }
    close(lines) // 读完关闭通道,通知 Worker 收工

    // 等待所有 Worker 处理完剩余数据
    wg.Wait()
    
    fmt.Printf("✅ Processed %d lines in %v.\n", count, time.Since(start))
    fmt.Printf("Unique IPs found: %d\n", len(ipCounter))
    
    // (可选) 这里可以加一个排序逻辑打印 Top 10,为简化代码略去
}
代码解释
  • bufio.NewScanner(file):逐行扫描,内存占用小
  • make(chan string, 5000):带缓冲的通道,防止阻塞
  • sync.Mutex:互斥锁,保护共享的 Map
  • mu.Lock() / mu.Unlock():临界区保护,防止并发写入
流式处理的设计哲学
  • 内存友好:逐行处理,不一次性加载整个文件
  • 并发处理:多个 Worker 并行处理,充分利用多核 CPU
  • 锁粒度最小化:只在更新 Map 时加锁,减少锁竞争
验证步骤
  1. 运行程序:go run 26_log_analyzer.go
  2. 你应该能在 1秒内 处理完这 50MB 的数据
  3. 尝试把生成的日志文件大小增加到 1GB (改下生成脚本的循环次数),再测一次,观察内存占用(应该不会超过 20MB)
  4. 观察多个 Worker 的并发处理,理解 Pipeline 架构
常见错误
  • Map 并发读写:如果不加 mu.Lock(),程序会 100% 崩溃并在控制台报 concurrent map writes 错误。Go 的 Map 默认不是线程安全的。
  • Channel 死锁:如果忘记 close(lines),Worker 会永远等待下去,程序永远不会结束。

拓展任务 (30 mins)

消除锁竞争 (进阶)

挑战:在上面的代码中,所有 Worker 都在抢一把 mu 锁,效率其实打了折扣。思路
  1. 每个 Worker 自己维护一个 localMap
  2. 循环结束后,把 localMap 发送到一个新的 Channel。
  3. 主线程最后只合并这 4 个 localMap。这样就完全没有锁竞争了!

JSON 日志解析

挑战:如果日志不是 Nginx 格式,而是 JSON 格式 {"ip": "...", "status": 200}提示:使用 encoding/json 库。注意 JSON 解析是比较慢的,这时候多核并发的优势会更加明显。

今日产出物

  • access.log - 模拟的大型日志文件
  • ~/projects/learn-go/26_log_analyzer.go - 高性能日志分析工具

参考代码

查看参考代码

在 GitHub 查看完整的示例代码

Go Bufio 文档

阅读官方 Scanner 文档

实际应用场景

流式处理的威力

  • 内存占用: Python 读取 1GB 文件可能需要 2-3GB 内存(加载整个对象)。Go 的流式处理稳定在 20MB。
  • 实时性: 你可以把 os.Open 换成对 os.Stdin 的读取,然后通过管道 tail -f access.log | ./analyzer 实时分析日志流。

ELK 的替代品?

  • 在很多小微企业,搭建一套 ELK (Elasticsearch) 太重了。
  • 一个编译好的 Go 工具,配合 Cron 定时任务,足以应付每天几百 GB 的日志统计需求,且零成本维护。
与 Day 27 的关联:经过三天的 Go 语言实战,你已经感受到了它的速度。明天我们将进行一次全面的技术选型大复盘:到底什么时候该用 Python?什么时候该用 Go?

回到目录

查看完整进度

下一天: 复盘与选型

Day 27 | 复盘与技术选型