突破 GitHub API 限制:大型仓库 Star 趋势图的采样方案

45 阅读4分钟

前言

作为开源项目维护者,想知道自己项目的 Star 增长趋势。市面上有不少工具可以生成 Star 趋势图,但当遇到像 kubernetes/kubernetes(10w+ Stars)这样的大型仓库时,这些工具往往会遇到问题:GitHub API 请求超限

本文将分享我是如何通过采样算法,在保证图表准确性的前提下,将 API 请求从数百次降至约 15 次,成功渲染出大型仓库的 Star 趋势图。

wechat_2026-01-07_181023_132.png

问题分析

GitHub API 的限制

GitHub Stargazers API 的设计如下:

GET /repos/{owner}/{repo}/stargazers

关键限制:

  • 每页最多 100 条数据per_page=100
  • 认证用户每小时 5000 次请求
  • 未认证用户每小时 60 次请求

问题规模计算

以一个 42000 Stars 的仓库为例:

总页数 = ceil(42000 / 100) = 420 页
所需请求 = 420

如果要获取完整数据,单个大型仓库就会消耗大量 API 配额。更糟糕的是,如果服务同时处理多个请求,很快就会触发 Rate Limit。

解决方案:采样模式

核心思路

既然无法获取所有数据点,那就均匀采样

关键洞察:Star 趋势图的价值在于展示增长趋势,而非精确到每一颗 Star。通过在时间线上均匀采样,可以用少量数据点还原整体趋势。

架构设计

┌─────────────────────────────────────────────────────────────────┐
│                        请求流程                                  │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
                    ┌─────────────────┐
                    │  请求第一页数据   │
                    │  解析 Link Header │
                    └────────┬────────┘
                             │
                             ▼
                    ┌─────────────────┐
                    │   获取总页数 N   │
                    └────────┬────────┘
                             │
              ┌──────────────┴──────────────┐
              │                             │
              ▼                             ▼
     ┌────────────────┐           ┌────────────────┐
     │  N ≤ 15 页?    │           │   N > 15 页?   │
     │  (约1500 Stars) │           │   触发采样模式  │
     └───────┬────────┘           └───────┬────────┘
             │                            │
             ▼                            ▼
     ┌────────────────┐           ┌────────────────┐
     │  全量获取模式   │           │  计算采样页码   │
     │  并发获取所有页  │           │  均匀分布 15 点 │
     └───────┬────────┘           └───────┬────────┘
             │                            │
             │                            ▼
             │                    ┌────────────────┐
             │                    │  并发获取采样页  │
             │                    │  提取数据点     │
             │                    └───────┬────────┘
             │                            │
             └──────────────┬─────────────┘
                            │
                            ▼
                    ┌─────────────────┐
                    │   渲染 SVG 图表  │
                    └─────────────────┘

采样算法详解

Step 1: 解析 Link Header 获取总页数

GitHub API 响应会包含 Link Header:

Link: <https://api.github.com/repos/owner/repo/stargazers?page=2>; rel="next",
      <https://api.github.com/repos/owner/repo/stargazers?page=420>; rel="last"

通过正则表达式提取 last 页码:

var linkLastPageRegex = regexp.MustCompile(`[&?]page=(\d+)[^>]*>;\s*rel="last"`)

func (gh *GitHub) parseLastPageFromLink(linkHeader string) int {
    matches := linkLastPageRegex.FindStringSubmatch(linkHeader)
    if len(matches) < 2 {
        return 0
    }
    lastPage, _ := strconv.Atoi(matches[1])
    return lastPage
}

Step 2: 计算均匀采样页码

func (gh *GitHub) calculateSamplePages(totalPages, maxSamples int) []int {
    pages := make([]int, 0, maxSamples)
    
    for i := 1; i <= maxSamples; i++ {
        // 均匀分布的页码计算
        page := int(math.Round(float64(i*totalPages) / float64(maxSamples)))
        if page < 1 {
            page = 1
        }
        if page > totalPages {
            page = totalPages
        }
        pages = append(pages, page)
    }
    
    // 确保第一页被包含(起始时间点很重要)
    if len(pages) > 0 && pages[0] != 1 {
        pages[0] = 1
    }
    
    return uniquePages(pages) // 去重处理
}

以 420 页为例,采样页码为:

[1, 28, 56, 84, 112, 140, 168, 196, 224, 252, 280, 308, 336, 364, 392, 420]

Step 3: 并发获取采样数据

func (gh *GitHub) getSampledStargazers(ctx context.Context, repo Repository, 
    firstPageStars []Stargazer, lastPage int) ([]Stargazer, error) {
    
    samplePages := gh.calculateSamplePages(lastPage, gh.maxSamplePages)
    
    var (
        wg      errgroup.Group
        lock    sync.Mutex
        results []pageResult
    )
    
    // 控制并发数,避免触发 Rate Limit
    wg.SetLimit(5)
    
    for _, page := range samplePages {
        page := page
        wg.Go(func() error {
            result, err := gh.getStargazersPage(ctx, repo, page)
            if err != nil {
                return err
            }
            
            // 计算该数据点的实际 Star 数量
            // 第 N 页第 1 个 Star = (N-1) * pageSize + 1
            starCount := (page-1)*gh.pageSize + 1
            
            lock.Lock()
            results = append(results, pageResult{
                page:      page,
                star:      result[0],
                starCount: starCount,
            })
            lock.Unlock()
            return nil
        })
    }
    
    wg.Wait()
    // ... 处理结果
}

Step 4: 补全最后一个数据点

为了确保图表延伸到当前时间,我们添加一个虚拟数据点:

// 添加当前时间 + 总 Star 数作为终点
stars = append(stars, Stargazer{
    StarredAt: time.Now(),
    Count:     repo.StargazersCount,
})

数据对比

指标全量模式采样模式优化比例
API 请求数420 次15 次96.4% ↓
响应时间~30s~2s93.3% ↓
数据精度100%~95%可接受

配置说明

项目支持通过环境变量灵活配置:

# Redis 缓存(配合 ETag 机制进一步减少请求)
export REDIS_URL="redis://localhost:6379"

# GitHub Token(支持多个,自动轮换)
export GITHUB_TOKENS="token1,token2,token3"

# 触发采样的阈值(页数)
export GITHUB_MAX_SAMPLE_PAGES=15

# 每页数据量
export GITHUB_PAGE_SIZE=100

# 服务监听地址
export LISTEN="0.0.0.0:3000"

效果展示

采样模式生成的 Star 趋势图:

wechat_2026-01-07_181023_132.png

可以看到,即使是 4w+ Stars 的大型仓库,采样后的趋势图依然能够准确反映项目的增长轨迹:

  • ✅ 早期缓慢增长阶段
  • ✅ 中期爆发式增长
  • ✅ 后期稳定增长趋势

其他优化

1. ETag 缓存机制

GitHub API 支持 ETag 条件请求,对于未变化的数据返回 304 Not Modified

// 请求时携带上次的 ETag
req.Header.Add("If-None-Match", cachedEtag)

// 304 响应不消耗 Rate Limit 配额!
if resp.StatusCode == http.StatusNotModified {
    return cachedData, nil
}

2. Token 轮换

支持配置多个 GitHub Token,自动检测并跳过已耗尽配额的 Token:

func (gh *GitHub) authorizedDo(req *http.Request, try int) (*http.Response, error) {
    token, _ := gh.tokens.Pick()  // Round-Robin 选择
    
    if err := gh.checkToken(token); err != nil {
        return gh.authorizedDo(req, try+1)  // 尝试下一个 Token
    }
    
    req.Header.Add("Authorization", "token " + token.Key())
    return http.DefaultClient.Do(req)
}

总结

通过采样算法,我们成功解决了大型仓库 Star 趋势图渲染的 API 限制问题:

  1. 从 Link Header 获取总页数,避免盲目请求
  2. 均匀采样算法,用 15 个数据点还原完整趋势
  3. 并发控制 + ETag 缓存,进一步优化性能
  4. Token 轮换机制,提高服务可用性

这套方案不仅适用于 Star 趋势图,也可以推广到其他需要处理大量分页数据的场景。

项目地址

欢迎 Star ⭐

GitHub: github.com/lik0914/sta…


如果这篇文章对你有帮助,欢迎点赞 👍 收藏 ⭐ 关注 ➕