目录

Multirepo微服务项目迁移记录

Multirepo微服务项目迁移记录

工作需求需要对一个200+仓库的multirepo微服务项目迁移到另一个git仓库和k8s集群,迁移时也总结了一些简单的经验踩坑及工作流,在这里记录一下。

相比于pull到本地再push到新git仓库,一个更简便的方法是直接在gitlab实例中导入。我的迁移路径是GitLab 社区版 v17.7.7GitLab 企业版 v15.2.2-ee。其实源实例类型和版本倒无所谓,主要是目标实例是否支持相关功能。我使用的v15.2.2-ee可以按照New project > Import project > Repository by URL直接从git链接导入。同时,从私有实例导出时一般需要配置用户名和密码(访问令牌)。我使用的v17.7.7可以在用户设置 > 访问令牌中配置。

比起毫无头绪地蒙头就干,我更建议先梳理一下依赖关系,优先迁移不依赖其他服务的仓库。具体操作就是先找一个最复杂使用最频繁的仓库(我这是提供客户端接口的api仓库),然后梳理其调用的其他服务(一般写在配置文件中)和直接依赖(在go.mod)中。然后再梳理依赖的依赖关系。这样自顶向下注意出一个依赖图。

需要注意存在循环依赖的可能。其中服务调用的循环依赖不用过于关心,一般在迁移时没有流量流入,不会调用其他服务。如果有初始化/定时任务之类调用了暂未迁移的服务接口,可以暂时让这个服务保持挂掉的状态,等那个服务也迁移了再重启。

而直接依赖的循环依赖比较麻烦。常见情况是A有一个旧版,B依赖了A的旧版本,A的新版本又依赖B导致循环依赖。由于我迁移的这些项目都使用了go vendor管理依赖,一个取巧的办法是直接从vendor中将依赖移到项目根目录下,即vendor/gitlab.old.com/otherrepo->pkg/otherrepo,再全局替换一下包路径,这样就实现了依赖的解耦。反正go.mod里version也是定死的,这个项目作为一个仅维护的老项目也很少会再更新代码,不用费力维护依赖关系。

终于到正片了。这里我将分不同commit介绍。实践中我基本也是这么操作的。

如下几个步骤我一般合并在一个commit中完成,主要目的就是让项目指向新git仓库并能够正常跑起来。

例如原gitlab实例为gitlab.old.com,仓库名为gitlab.old.com/project/reponame。新gitlab实例为gitlab.new.com/proj/reponame。因为项目名一般不会变更,直接全局替换gitlab.old.com/projectgitlab.new.com/proj就行。

虽然大部分私有仓库的依赖我都会通过如上的方法,将其从vendor目录移到项目根目录实现依赖解耦。但一些比较简单的依赖关系和基础库我并不会这么处理,所以go.mod中还是会有私有gitlab实例的库。所以需要配置GOPRIVATEGONOSUMDB,这样go mod才能正常下载和校验私有库。

除了私有仓库,可能还有一些其他问题,例如部分依赖仓库已经删除/删标签导致拉取不到等等。大部分都可以通过从vendor移出解决,也可以自行检索相关信息寻找其他解决方法。例如我有一些仓库依赖github.com/qiniu/api.v7这个库,而新版本的go不支持这个格式,检查其github仓库就提供了解决方法,全局替换为github.com/qiniu/go-sdk/v7即可。

最后go mod tidygo vendor更新依赖,确认没问题就行。

新集群的服务域名/访问路径/数据库配置可能都有变更,需要再检查一遍配置文件进行确认。确认完成后本地运行一下试试能不能跑起来。此外,别忘了备份一份旧配置文件。

注意:以下内容高度客制化仅作记录。ci流程这种东西各个项目都不一样。

新旧集群很可能有着不同的部署工作流。与其在旧ci上大刀阔斧地改造,我更喜欢套用新的工作流再修改适配。

我迁移的这个项目,旧集群ci只有个gitlab-ci,而新集群则是将配置文件和k8s配置都放在仓库中,任何修改都可以在仓库中进行再通过gitlab-ci将修改应用到k8s。

主要是服务名和端口的配置。此外,不同项目使用的配置文件名也可能不同,有的是app.conf有的是config.yml,需要检查以下是否正确。

主要是更换拉取的docker镜像地址。两个镜像仓库有的镜像都不同,有的镜像就换私有,没有的就换公有。例如我迁移时旧集群用的私有alpine镜像预装了时区数据,换成公共镜像后要加一句RUN apk add --no-cache tzdata之类。

k8s配置比较复杂,但大部分都是通用的,主要注意几点:

  1. 启动命令。有些的直接运行,有些要加参数。

        containers:
        - name: ---APP_NAME---
          command:
            - /app/---APP_NAME---
  2. Ingress配置。有些服务的子路径和服务名不一致,需要进行校对,不一致的手动修改。

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
    namespace: ---NAMESPACE---
    name: ---APP_NAME---ingress
    annotations:
        nginx.ingress.kubernetes.io/use-regex: "true"
        nginx.ingress.kubernetes.io/rewrite-target: /$1
    spec:
    tls:
        - hosts:
            - api.example.com
        secretName: ssl
    rules:
        - host: api.example.com
        http:
            paths:
            - backend:
                service:
                    name: ---APP_NAME---
                    port:
                    number: ---PORT---
                path: /---APP_NAME---/(.*)
                pathType: ImplementationSpecific
  3. 定时任务。包括任务名,cron表达式,启动命令,挂载数据卷之类。

    apiVersion: batch/v1
    kind: CronJob
    metadata:
    namespace: ---NAMESPACE---
    name: cron-job-name
    labels:
        app: ---APP_NAME---
    spec:
    schedule: "*/5 * * * *"
    concurrencyPolicy: Forbid
    successfulJobsHistoryLimit: 3
    failedJobsHistoryLimit: 1
    jobTemplate:
        spec:
        ttlSecondsAfterFinished: 86400
        activeDeadlineSeconds: 600
        backoffLimit: 6
        template:
            spec:
            containers:
                - name: cron-job-name
                image: "---IMAGE---"
                imagePullPolicy: Always
                command: ["/app/jobset"]
                args: ["arg1", "arg2"]
                env:
                    - name: aliyun_logs_---APP_NAME----log
                    value: stdout
                resources:
                    requests:
                    cpu: 50m
                    memory: 64Mi
                    limits:
                    cpu: 100m
                    memory: 128Mi
                terminationMessagePath: /dev/termination-log
                terminationMessagePolicy: File
                volumeMounts:
                    - mountPath: /etc/localtime
                    name: volume-localtime
                    - mountPath: /app/conf
                    name: config
                    readOnly: true
            restartPolicy: Never
            volumes:
                - name: volume-localtime
                hostPath:
                    path: /etc/localtime
                    type: File
                - name: config
                configMap:
                    name: ---CONFIG_MAP_NAME---
                    defaultMode: 420

/health路由添加一个简单的健康检查接口,并在k8s配置中设置探针:

    startupProbe:
        httpGet:
            path: /health
            port: ---PORT---
        failureThreshold: 60
        periodSeconds: 2
    livenessProbe:
        httpGet:
            path: /health
            port: ---PORT---
        failureThreshold: 3
        periodSeconds: 10
    readinessProbe:
        httpGet:
            path: /health
            port: ---PORT---
        initialDelaySeconds: 1
        periodSeconds: 5

因为配置了探针,只需要直接查看服务状态就行。如果有问题检查日志或者pod事件找原因。

这次迁移需要动代码的部分比较少,大部分时间都是在配ci调ci,十分枯燥。只有在部署后有问题时才需要去看代码解决问题。感觉完全可以整理成一个skill交给ai做,但虽然上面讲的轻松,实际迁移问题还是有很多细节问题各个项目都不一样,没办法整理出来就都略过了。ai弄出了问题还得自己处理,最后还是自己慢慢做磨时间。