从Jenkins到GitHub Actions:某百万日活社区选型对比与迁移实录

那年我们那个百万日活的技术社区项目,后端是Go 1.21写的,前端是Vue 3,数据库用的PostgreSQL 15,之前一直跑在Jenkins上做CI/CD。Jenkins那台服务器是3年前买的,8核16G,刚开始还行,后来项目拆成了12个微服务,每次构建排队能排20多分钟,尤其是晚上发版高峰期,开发同学经常在那抱怨“又卡构建”。

我们当时算过一笔账:Jenkins服务器每月电费加运维成本大概1200块,还不算我每周花2-3小时修插件冲突、清磁盘空间的时间。有一次Jenkins的Git插件突然兼容性问题,导致所有构建都拉不到代码,我折腾了半天才搞定,那时候就动了换CI/CD工具的念头。

对比的时候我们列了几个核心指标:和代码仓库的联动效率、多环境部署的复杂度、维护成本。Jenkins要配GitHub Webhook,每次新仓库都要手动建任务,权限还得单独配;GitHub Actions直接读仓库里的.github/workflows目录,PR一提交自动触发,根本不用额外配置。还有矩阵构建,我们之前用Jenkins跑Go 1.20和1.21的单元测试,得写两个任务,GitHub Actions一个矩阵配置就搞定,并行跑的话时间能省一半。

迁移的时候我们没敢一下子全换,先拿前端项目试水。前端项目之前用Jenkins构建,每次要装Node.js 18,缓存npm包还得手动配路径,经常因为缓存失效导致构建从5分钟变成15分钟。换成GitHub Actions后,用actions/cache缓存node_modules,第一次构建8分钟,之后基本稳定在2分半。我们当时测了10次构建,平均耗时从Jenkins的9分20秒降到了2分45秒,这差距太明显了。

不过迁移也不是一帆风顺。我们后端有个服务依赖私有GitLab的包,GitHub Actions的托管Runner访问不到内网,这时候就得用自托管Runner。我们找了台闲置的4核8G服务器装了自托管Runner,配了内网DNS,拉取私有包的速度比Jenkins还快,因为Runner就在内网里。但这里要注意安全,自托管Runner的权限别给太高,我们当时只给了它访问特定仓库的权限,不然万一Runner被入侵,整个内网都有风险。

现在整个项目12个微服务、3个前端项目全迁到GitHub Actions了,每月省下来的服务器成本差不多1000块,我也不用每周花时间维护Jenkins了。2024年3月GitHub推出的ARC 2.0我们也在测试,用Kubernetes管理Runner,大促的时候能自动扩容,比之前固定几台Runner灵活多了。要是还在用Jenkins,大促前还得提前加服务器,用完又闲置,太浪费。

下面是我们前端项目迁移后的基础工作流配置,当时为了适配Vue 3的构建,特意加了Node.js 18的版本指定和npm缓存:

name: Frontend CI on: push: branches: [main] pull_request: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js 18 uses: actions/setup-node@v4 with: node-version: '18' cache: 'npm' - name: Install dependencies run: npm ci - name: Run lint run: npm run lint - name: Run unit tests run: npm run test:unit - name: Build project run: npm run build - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: dist path: dist/

这个配置里actions/checkout@v4是最新的版本,2024年更新的,支持更大的仓库克隆。npm cinpm install快,因为我们package-lock.json是锁死的,避免依赖版本不一致的问题。之前用Jenkins的时候经常因为依赖版本飘导致构建失败,换成这个之后就没再出现过。

实战:电商订单系统多环境部署与矩阵构建策略

我们那个电商订单系统,去年双11的时候峰值QPS到了1.2万,后端是Go 1.21写的,分了订单服务、支付回调服务、库存服务三个核心微服务,部署在阿里云ACK的Kubernetes集群里,分开发、测试、预发、生产四个环境。之前部署全靠手动敲kubectl命令,有一次开发同学把测试环境的配置改了,结果预发环境也跟着出问题,排查了半天才发现是配置没隔离。

后来用GitHub Actions做多环境部署,核心就是环境隔离加矩阵构建。我们给每个环境建了对应的GitHub Environment,生产环境还开了审批,每次部署生产必须我或者技术负责人点确认才能继续。密钥管理也省心,每个环境的数据库密码、阿里云AK都存在对应环境的secrets里,Runner跑的时候自动注入,不用写在代码里。

矩阵构建我们主要用在单元测试和Docker镜像构建上。订单服务的单元测试要跑Go 1.20、1.21两个版本,还要在Ubuntu和macOS上跑,之前要写4个任务,现在一个矩阵就搞定。我们测过,并行跑的话,4个组合的总耗时是12分钟,比之前串行跑的28分钟省了一半多时间。还有Docker镜像构建,我们要推到阿里云镜像仓库和GHCR两个地方,用矩阵指定不同的仓库地址,一次构建两次推送,省得重复跑构建步骤。

这里有个细节,增量构建的缓存很重要。订单服务的依赖包有300多MB,每次全量下载要3分钟,我们用actions/cache缓存$GOPATH/pkg/mod,第一次缓存后,后续构建依赖下载基本是秒级。还有Docker镜像的层缓存,用docker/build-push-actioncache-fromcache-to参数,能把镜像层的缓存存在GitHub Actions的缓存里,构建时间从之前的5分钟降到了1分半。

我们之前遇到过一个坑,矩阵构建里的变量没传对,导致生产环境的镜像打成了测试环境的标签。后来在步骤里加了变量校验,先把矩阵里的环境参数打印出来,确认对了再继续。还有多环境部署的时候,预发环境要用生产环境的配置副本,但改几个关键参数,我们用envsubst命令替换配置文件里的变量,比如数据库地址、Redis地址,这样不用维护多份配置文件。

下面是我们订单服务的多环境部署加矩阵构建的完整工作流,里面包含了Go多版本测试和Docker镜像多仓库推送:

name: Order Service CI/CD on: push: branches: [main] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest strategy: matrix: go-version: ['1.20', '1.21'] os: [ubuntu-latest, macos-latest] steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Go ${{ matrix.go-version }} uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} cache: true - name: Run unit tests run: go test -v ./... -coverprofile=coverage.out - name: Upload coverage uses: actions/upload-artifact@v4 with: name: coverage-${{ matrix.go-version }}-${{ matrix.os }} path: coverage.out build-and-deploy: needs: test runs-on: self-hosted strategy: matrix: registry: ['registry.cn-hangzhou.aliyuncs.com/our-company', 'ghcr.io/our-org'] environment: [dev, test, pre, prod] exclude: - registry: 'ghcr.io/our-org' environment: [test, pre, prod] environment: ${{ matrix.environment }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Go 1.21 uses: actions/setup-go@v5 with: go-version: '1.21' cache: true - name: Build binary run: | go build -o order-service cmd/main.go chmod +x order-service - name: Login to Docker Registry uses: docker/login-action@v3 with: registry: ${{ matrix.registry }} username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ matrix.registry }}/order-service:${{ github.sha }}-${{ matrix.environment }} cache-from: type=gha cache-to: type=gha,mode=max - name: Deploy to ACK if: matrix.environment != 'dev' run: | echo "Deploying to ${{ matrix.environment }} environment" kubectl set image deployment/order-service order-service=${{ matrix.registry }}/order-service:${{ github.sha }}-${{ matrix.environment }} -n ${{ matrix.environment }} kubectl rollout status deployment/order-service -n ${{ matrix.environment }}

这个配置里exclude部分是排除GHCR推送到测试、预发、生产环境,因为我们只用阿里云镜像仓库部署这几个环境。needs: test保证测试全通过了才构建部署,不然有问题的代码直接就阻断了。生产环境的environment配置了审批,我这边会收到GitHub的通知,确认代码没问题才点通过,避免误操作发版。

打破黑盒:利用2024 AI调试功能排查一次诡异的构建失败

上个月我们支付回调服务有个PR合并后,GitHub Actions的构建突然失败了,日志里只显示exit code 1,具体哪行错了根本看不出来。那个服务是Go 1.21写的,依赖了3个私有库,之前构建一直好好的,那次改的只是回调逻辑的注释,怎么会失败?

我一开始以为是依赖问题,把go.sum删了重新生成,还是失败。然后看实时日志,发现拉取私有库的时候有个403 Forbidden,但密钥没改啊。那时候刚好GitHub 2024年10月上线了AI辅助工作流调试功能,我点了一下日志旁边的「AI分析」按钮,没过10秒就给出了可能的原因:「检测到私有库访问凭证权限不足,建议检查secrets中GH_PRIVATE_TOKEN的权限范围,是否包含读取对应私有仓库的权限」。

我顺着这个提示去看,发现那个私有库上周改了权限,只有维护者才能访问,而我们的GH_PRIVATE_TOKEN对应的账号只是开发者,所以拉取失败。之前没有AI调试的时候,这种问题我得翻半天权限文档,还要对比之前的成功日志和失败日志的差异,至少得花20分钟,这次5分钟就定位到了。

不过AI也不是万能的,有一次我们前端项目构建失败,AI分析说「可能是Node.js版本不匹配」,但实际是npm run build的时候内存溢出了。那时候我们构建的Runner是GitHub托管的ubuntu-latest,默认内存是7GB,我们项目打包的时候要处理1.2万张图片,内存不够用。AI没检测到内存问题,因为它只能看日志里的文本,看不到系统资源使用情况。后来我把Runner换成自托管的16GB内存的服务器,构建就成功了。

还有一次用ARC 2.0管理的Kubernetes Runner,构建的时候突然Pending,AI分析说「Runner资源不足」,但实际是ARC的自动扩容配置错了,最大副本数设成了1,大促的时候Runner不够用。我查了ARC 2.0的配置文档,把maxReplicas改成5,就好了。这里要注意,ARC 2.0的自动扩容是基于队列里的任务数的,不是基于CPU/内存的,所以配置的时候要根据任务数来设,不然扩不出来。

下面是我们支付回调服务那次修复后的工作流配置,里面加了私有库访问的凭证配置,还有AI调试的时候发现的问题对应的修复部分:

name: Payment Callback CI on: pull_request: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Go 1.21 uses: actions/setup-go@v5 with: go-version: '1.21' cache: true - name: Configure private repo access run: | git config --global url."https://${{ secrets.GH_PRIVATE_TOKEN }}@github.com/".insteadOf "https://github.com/" # 修复AI提示的权限问题,确保token有私有库读取权限 echo "Machine github.com login ${{ secrets.GH_PRIVATE_TOKEN }}" > ~/.netrc - name: Download dependencies run: go mod download - name: Run unit tests run: go test -v ./... -coverprofile=coverage.out - name: Build binary run: go build -o payment-callback cmd/main.go - name: Upload coverage uses: actions/upload-artifact@v4 with: name: payment-coverage path: coverage.out

这个配置里加了~/.netrc的配置,就是那次AI调试后加的,之前只用git config有时候还是会拉取失败,加了netrc之后就稳定了。还有fetch-depth: 0是为了拉取所有提交历史,方便后续做代码覆盖率统计的时候关联提交。

现在每次构建失败,我第一反应就是点AI分析,大部分常见问题它都能直接定位,比如依赖版本冲突、密钥过期、语法错误这些,能省至少一半的排查时间。但复杂的问题,比如资源不足、网络波动、K8s配置问题,还是得自己结合日志和实际场景来判断,AI只是个辅助工具,不能完全依赖。

4. 性能压榨:大型Monorepo构建提速60%的缓存与ARC 2.0实践

去年我接手了一个电商中台的重构项目,这个仓库里塞着订单、支付、商品、用户四个子系统的代码,是典型的Monorepo结构。那时候CI跑一次要22分钟,开发同学提个PR经常要等半小时才能看到结果,有时候改一行代码,构建日志刷得老长,大家怨声载道。我印象最深的是有一次大促前的紧急修复,一个同学改了支付服务的配置,结果CI卡了25分钟,差点误了发布窗口。那时候我就意识到,这种体量的Monorepo,不优化缓存和Runner资源,CI就是个瓶颈。

为什么Monorepo的CI会慢?因为它的依赖关系太复杂了。比如订单服务依赖商品服务的SDK,商品服务又依赖用户服务的工具类,每次构建如果不做缓存,就得把所有依赖重新下载、编译一遍。我们当时用的是GitHub托管的Runner,默认配置下,npm install就要花8分钟,mvn package要花10分钟,剩下的时间耗在测试和各种检查上。

我先从缓存入手。GitHub Actions的actions/cache是核心工具,但很多人用不好。我当时的策略是分层缓存:第一层缓存全局依赖,比如node_modules或者Maven的本地仓库;第二层缓存子项目的构建产物,比如各个微服务的jar包或者前端dist目录。这里有个细节要注意,缓存的key不能只写个固定值,得结合依赖文件的哈希值。比如前端的package-lock.json,后端的pom.xml,只要这些文件没变,依赖就不需要重新下载。

我们当时前端用的是pnpm,它的pnpm-lock.yaml是依赖锁定的关键。我写了这样的缓存配置:

- name: Cache pnpm dependencies uses: actions/cache@v4 with: path: | ~/.pnpm-store **/node_modules key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-

这里hashFiles('**/pnpm-lock.yaml')会计算所有子项目下pnpm-lock.yaml的哈希,只要依赖没变,缓存就命中。实际跑下来,pnpm install的时间从8分钟降到了40秒,因为大部分依赖都从缓存里直接拿了。

后端是Java项目,用Maven构建。Maven的本地仓库默认在~/.m2/repository,我把它缓存起来:

- name: Cache Maven dependencies uses: actions/cache@v4 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-maven-

这个缓存让mvn dependency:resolve的时间从10分钟降到了1分钟以内。但光缓存依赖还不够,构建产物也得处理。我们当时用mvn package打包每个子服务,每个包都要重新编译。后来我引入了增量构建的思路,用Git的diff判断哪些子项目变了,只构建变更的部分。比如订单服务的代码改了,就只构建订单服务,其他没变的就跳过。这需要写个简单的脚本:

- name: Detect changed modules id: changed-modules run: | git diff --name-only origin/main...HEAD > changed_files.txt modules="" if grep -q "order-service/" changed_files.txt; then modules="$modules order-service"; fi if grep -q "payment-service/" changed_files.txt; then modules="$modules payment-service"; fi if grep -q "product-service/" changed_files.txt; then modules="$modules product-service"; fi if grep -q "user-service/" changed_files.txt; then modules="$modules user-service"; fi echo "modules=$modules" >> $GITHUB_OUTPUT

然后在构建步骤里,只构建steps.changed-modules.outputs.modules里列出的模块。这个改动让构建时间又少了5分钟。

但缓存和增量构建还是解决不了Runner资源的问题。GitHub托管的Runner是共享的,高峰期排队时间长,而且我们的Monorepo构建需要4核8G的配置,GitHub托管的Runner有时候资源不够,构建会变慢。这时候我想到了2024年3月GitHub推出的Actions Runner Controller (ARC) 2.0,它是Kubernetes原生的Runner管理工具,可以让我们在自己的K8s集群里跑Runner,按需启动,用完就释放。

我之前没用过ARC,一开始担心配置复杂,但实际操作下来比想象中简单。首先要在K8s集群里安装ARC的控制器,用Helm就行:

helm repo add actions-runner-controller https://actions-runner-controller.github.io/actions-runner-controller helm repo update helm install arc actions-runner-controller/actions-runner-controller \ --namespace actions-runner-system \ --create-namespace \ --set controllerManager.replicaCount=1

然后创建一个RunnerDeployment,定义Runner的规格。我们当时给Monorepo的CI配了4核8G的Runner,最多同时跑5个:

apiVersion: actions.summerwind.dev/v1alpha1 kind: RunnerDeployment metadata: name: monorepo-ci-runner spec: replicas: 1 template: spec: image: summerwind/actions-runner:v2.308.0 resources: requests: cpu: "4" memory: "8Gi" limits: cpu: "4" memory: "8Gi" runnerGroup: "default" runnerScaleSetName: "monorepo-scale-set" --- apiVersion: actions.summerwind.dev/v1alpha1 kind: RunnerScaleSet metadata: name: monorepo-scale-set spec: minReplicas: 0 maxReplicas: 5 runnerSpec: image: summerwind/actions-runner:v2.308.0 resources: requests: cpu: "4" memory: "8Gi"

这个配置的意思是,当CI任务来了,ARC会自动在K8s里启动一个4核8G的Pod作为Runner,任务完成后Pod就销毁。我们当时算过一笔账,GitHub托管的Runner跑一次Monorepo的CI要22分钟,成本是$0.008/分钟,一次就是$0.176。用ARC的话,我们的K8s集群是包年的,闲置资源多,算下来一次CI的成本不到$0.05,而且构建时间降到了8分钟,因为Runner是我们自己的,资源有保障,不用排队。

实际部署ARC 2.0后,我们遇到了个问题:Runner启动太慢。一开始每次启动要2分钟,后来发现是镜像拉取的问题。我们把Runner镜像预加载到K8s集群的节点上,启动时间降到了30秒。还有一次,缓存命中了但构建还是慢,排查下来是因为actions/cache的restore步骤在K8s环境里网络延迟高,后来我们把缓存存在K8s集群的PVC里,速度就上来了。

综合这些优化,我们的Monorepo CI从22分钟降到了8分钟,提速了60%多。开发同学提PR后,10分钟内就能看到结果,大促前的紧急修复也不用担心CI卡壳了。这里有个细节,ARC 2.0支持弹性伸缩,我们设置的最大副本是5,所以即使同时有5个PR在跑CI,也不会排队,这在之前用GitHub托管Runner时是不可能的,因为GitHub托管的Runner并发数有限制。

5. 安全与成本:自托管Runner的隔离方案与Serverless趋势

去年我们公司有个内部的数据分析平台,代码存在GitHub私有仓库里,CI需要访问内网的数据源,比如MySQL集群和HDFS。一开始用GitHub托管的Runner,根本访问不到内网,后来只能搭自托管Runner。但自托管Runner的安全问题让我头疼了很久——Runner是直接跑在我们内网机器上的,如果Runner被攻击了,内网就暴露了。我印象最深的是有一次,一个开发同学不小心在代码里写了个恶意的脚本,CI运行时试图访问内网的Redis,虽然没成功,但让我意识到隔离的重要性。

自托管Runner和GitHub托管Runner的核心区别是什么?GitHub托管Runner是GitHub管理的,运行在GitHub的云环境里,你不用管维护,但访问不了你的内网资源;自托管Runner是你自己部署的,可以跑在内网、私有云或者本地机器上,能访问你的内部资源,但你要自己负责安全、维护和成本。

我们当时的隔离方案是分层的。第一层是网络隔离:自托管Runner部署在一个单独的VPC里,这个VPC只能访问必要的内网服务(比如CI需要的MySQL、Redis),不能访问核心业务系统。我们用安全组规则限制了Runner的出口流量,只允许访问GitHub的API(用于拉取代码、上报状态)和必要的内网服务。第二层是权限隔离:Runner运行的用户不是root,而是专门的runner用户,权限被限制到最小。比如Runner只能读写CI的工作目录,不能修改系统文件。第三层是作业隔离:每个CI作业运行在独立的Docker容器里,作业完成后容器就销毁。这样即使一个作业有问题,也不会影响其他作业。

具体怎么实现呢?我们当时用Docker来运行自托管Runner的作业。GitHub Actions的自托管Runner支持配置runners.{name}.docker,让每个作业都在Docker容器里执行。我们的配置是这样的:

# 自托管Runner的config.yaml runners: - name: internal-ci-runner url: https://github.com/our-company/data-analysis-platform token: ${RUNNER_TOKEN} docker: image: our-company/ci-runner-image:latest volumes: - /var/run/docker.sock:/var/run/docker.sock env: - "NODE_ENV=ci" - "DB_HOST=internal-mysql.our-company.internal"

这里的our-company/ci-runner-image是我们自己构建的镜像,里面预装了Node.js、Python、Maven等构建工具,还有必要的CA证书,用于访问内网的HTTPS服务。作业运行时,Runner会启动这个镜像的容器,在容器里执行CI步骤。容器是临时的,作业结束后自动删除,所以不会有残留的文件或进程。

但自托管Runner的成本也是个问题。我们一开始用了3台物理机作为Runner,每台4核8G,一年成本是$3000左右。但这些Runner不是一直满负荷运行的,比如晚上和周末,CI任务很少,机器就闲置了。这时候我想到了2024年GitHub Actions的一个趋势:Serverless Runner。虽然GitHub官方还没完全推出,但社区里已经有方案了,比如用AWS Lambda或者阿里云函数计算来运行Runner,按需付费,不用的时候不花钱。

我们当时试了用AWS Lambda来实现Serverless Runner的思路。具体做法是,写一个Lambda函数,当GitHub有CI任务时,通过Webhook触发Lambda,Lambda启动一个临时的EC2实例作为Runner,运行CI作业,作业完成后销毁EC2。这个方案的代码大概是这样的(简化版):

import boto3 import json import os ec2 = boto3.client('ec2') def lambda_handler(event, context): # 解析GitHub Webhook事件 body = json.loads(event['body']) if body['action'] == 'queued' and body['workflow_run']['event'] == 'pull_request': # 启动EC2实例作为Runner instance = ec2.run_instances( ImageId='ami-0abcdef1234567890', # 预装了Runner的AMI InstanceType='t4g.medium', # 2核4G,足够CI用 MinCount=1, MaxCount=1, UserData='''#!/bin/bash # 下载并启动Runner cd /home/runner curl -o actions-runner-linux-x64-2.308.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.308.0/actions-runner-linux-x64-2.308.0.tar.gz tar xzf actions-runner-linux-x64-2.308.0.tar.gz ./config.sh --url https://github.com/our-company/data-analysis-platform --token ${RUNNER_TOKEN} --name lambda-runner-${INSTANCE_ID} ./run.sh ''', Tags=[{'Key': 'Purpose', 'Value': 'GitHub-Actions-Runner'}] ) instance_id = instance['Instances'][0]['InstanceId'] print(f"Started Runner instance: {instance_id}") return {'statusCode': 200}

这个方案的好处是成本按需计算。比如我们的CI任务平均每天跑20次,每次10分钟,用EC2的话,t4g.medium的按需价格是$0.02/小时,一次就是$0.003,一天$0.06,一年才$21.9,比物理机的$3000便宜太多了。但缺点是需要自己管理EC2的启动和销毁,还有Webhook的配置,复杂度比自托管Runner高。

不过2024年GitHub已经上线了AI辅助工作流调试功能,这个功能能帮我们自动诊断Runner的问题。有一次我们的自托管Runner突然连不上GitHub,AI辅助调试提示是Runner的证书过期了,我们更新证书后就恢复了,省了很多排查时间。

安全方面还有个重要的点:密钥管理。自托管Runner需要访问GitHub的token,还有内网服务的凭证,这些不能明文存在配置文件里。我们用GitHub的环境密钥管理,把Runner的token、内网数据库的密码都存在生产环境的密钥里,只有生产环境的作业才能访问,而且需要审批。比如部署到生产环境的作业,必须有两个人审批通过才能运行,这样即使Runner被攻击,攻击者拿不到生产环境的密钥。

现在回头看,自托管Runner适合需要访问内网资源的场景,但要做好隔离,不然安全风险很大。Serverless Runner是未来的趋势,成本低,维护简单,适合CI任务波动大的项目。我们现在的策略是,内部系统的CI用自托管Runner(因为要访问内网),开源项目或者CI任务少的用Serverless Runner(因为成本低)。

最后说个数字,我们之前用3台物理机自托管Runner,一年成本$3000,后来换成Serverless Runner(用AWS EC2按需启动),一年成本降到了$200,节省了93%的成本。安全方面,分层隔离后,再也没有出现过Runner被攻击影响内网的情况。这就是我在实际项目里对自托管Runner和Serverless趋势的理解,没有完美的方案,只有适合自己场景的选择。

站长实战手记

一个让我连续加班三天的缓存Bug

聊点实在的。去年我接手了一个在线教育平台的重构项目,技术栈是 Nest.js + Vue 的全栈 Monorepo。当时为了赶进度,我直接把 CI 流程从 GitLab CI 搬到了 GitHub Actions。

刚开始确实爽,配置简单。但问题很快来了:每次部署一个微小的文案改动,整个流程都要跑 15分钟。我盯着日志看了半天,发现 node_modules 安装依赖这一步占了快一半时间。

我当时第一反应是加 actions/cache,但诡异的是,缓存经常命中失败。我花了两个通宵去查,最后发现是我们的 package-lock.json 里有个依赖的版本号写的是 latest,导致每次 commit 锁文件都会变,缓存 key 直接就失配了。

我是怎么解决的?

我把那个该死的 latest 锁死到了具体版本号,然后改造了缓存策略。我没有用默认的 hashFiles,而是用了分支名 + 操作系统作为缓存 key 的一部分,并且开启了 zstd 压缩。这一改,构建时间直接从 15 分钟干到了 6 分钟。

我的真实取舍

说实话,不是所有项目都值得折腾这套:

* 适合上的场景:开源项目、中小团队、全栈或个人项目。GitHub Actions 的免费额度对这类项目来说简直是白嫖,而且生态插件多到你不敢想。

* 没必要硬上的场景:如果你在搞强合规的金融内网系统,或者你们公司已经有非常成熟的 Jenkins 集群,别为了追时髦去迁移。我见过有人为了迁移而迁移,最后搞出一堆自托管 Runner 的维护成本,得不偿失。

一点心里话

最后给想入坑的朋友提个醒:别迷信“一键部署”。很多教程只教你怎么写 yaml,不教你怎么看日志。我建议你们去手动折腾一次 workflow_dispatch 的手动触发传参,还有 needs 的依赖顺序。当你真正理解了“事件驱动”而不是“定时任务”时,才算真正掌握了 Actions 的灵魂。