不做大哥好多年 不做大哥好多年
首页
  • MySQL
  • Redis
  • Elasticsearch
  • Kafka
  • Etcd
  • MongoDB
  • TiDB
  • RabbitMQ
  • 01.Python
  • 02.GO
  • 03.Java
  • 04.业务问题
  • 05.关键技术
  • 06.项目常识
  • 10.计算机基础
  • Docker
  • K8S
  • 容器原理
  • Istio
  • 01.GO基础
  • 02.面向对象
  • 03.并发编程
  • 04.常用库
  • 05.数据库操作
  • 06.Beego框架
  • 07.Beego商城
  • 08.GIN框架
  • 09.GIN论坛
  • 10.微服务
  • 01.Python基础
  • 02.Python模块
  • 03.Django
  • 04.Flask
  • 05.SYL
  • 06.Celery
  • 10.微服务
  • 01.Java基础
  • 02.面向对象
  • 03.Java进阶
  • 04.Web基础
  • 05.Spring框架
  • 100.微服务
  • 数据结构
  • 算法基础
  • 算法题分类
  • 前置知识
  • PyTorch
  • Langchain
  • Linux基础
  • Linux高级
  • Nginx
  • KeepAlive
  • ansible
  • zabbix
  • Shell
  • Linux内核

逍遥子

不做大哥好多年
首页
  • MySQL
  • Redis
  • Elasticsearch
  • Kafka
  • Etcd
  • MongoDB
  • TiDB
  • RabbitMQ
  • 01.Python
  • 02.GO
  • 03.Java
  • 04.业务问题
  • 05.关键技术
  • 06.项目常识
  • 10.计算机基础
  • Docker
  • K8S
  • 容器原理
  • Istio
  • 01.GO基础
  • 02.面向对象
  • 03.并发编程
  • 04.常用库
  • 05.数据库操作
  • 06.Beego框架
  • 07.Beego商城
  • 08.GIN框架
  • 09.GIN论坛
  • 10.微服务
  • 01.Python基础
  • 02.Python模块
  • 03.Django
  • 04.Flask
  • 05.SYL
  • 06.Celery
  • 10.微服务
  • 01.Java基础
  • 02.面向对象
  • 03.Java进阶
  • 04.Web基础
  • 05.Spring框架
  • 100.微服务
  • 数据结构
  • 算法基础
  • 算法题分类
  • 前置知识
  • PyTorch
  • Langchain
  • Linux基础
  • Linux高级
  • Nginx
  • KeepAlive
  • ansible
  • zabbix
  • Shell
  • Linux内核
  • 项目

    • 01.分布式调度系统
    • 02.GPU调度
    • 03.车端构建系统
      • 01.车端版本
        • 0、项目介绍
        • 项目背景
      • 主要技术栈:
        • 职责与贡献
        • 正式构建管理
        • 项目成果
        • 1、介绍
        • 2、核心功能模块说明
        • 3、正式构建说明
      • 08.版本克隆
      • 09.逆序增量构建
        • 1、逆序构建介绍
        • 2、处理流程
        • 3、旧流程问题
      • 10.车端构建系统梳理
        • 1、版本管理
        • 2、配置关联关系
        • 3、prepare阶段
        • 4、compile阶段
        • 5、.gitlab-ci.yml
      • 11.创建版本
        • 1、创建版本
        • 2、添加模块
        • 3、模块与版本关联
        • 4、建立模块之间的依赖关系
      • 12.Gitlab CICD
        • 1、术语解释
        • 2、代码目录组织规范
        • 3、CICD配置中心
        • 4、CICD流程
        • 0)开发阶段流水线
        • 1)Build 阶段
        • 2)Publish阶段
    • 04.资源管理平台
    • 05.CICD
    • 06.Mage平台
    • 10.发布系统使用
    • 11.自动驾驶业务
目录

03.车端构建系统

# 01.车端版本

# 0、项目介绍

# 项目背景

  • 项目名称: 车端构建系统
  • 项目描述:
    • 本项目旨在解决车端Bundle包的打包构建问题,确保在车端环境中生成高效且稳定的Bundle包
    • 系统支持多项目的关联管理以及树形结构的版本控制,确保各个代码仓库能够统一管理和打包
    • # 主要技术栈:

# 职责与贡献

  • 项目管理与模块管理
    • 实现功能:
      • 设计并实现了版本管理系统,使一个版本能够关联多个项目,每个项目对应一个独立的代码仓库
      • 开发了模块管理功能,支持对项目中的各个模块进行细粒度的操作和配置管理
    • 技术细节:
      • 使用Golang编写了项目管理和模块管理的核心逻辑,实现了高效的版本控制和模块配置
      • 运用Python脚本自动化处理配置和日志管理,实现操作可追溯性
  • Bundle管理
    • 实现功能:
      • 构建了自动化Bundle生成系统,根据指定的版本和模块生成对应的Bundle包,并实现了稳定制品的标记功能
    • 技术细节:
      • 使用Golang编写构建和打包流程的核心组件,确保生成的Bundle符合系统稳定性要求
      • 利用Python编写辅助脚本,自动化处理生成过程中所需的依赖管理和环境配置
  • Bundle对比
    • 实现功能:
      • 开发了Bundle对比工具,支持对不同版本或模块之间的Bundle进行快速对比,帮助团队识别和解决构建中的差异和不一致性
    • 技术细节:
      • 使用Golang编写对比工具,优化对比算法,提升对比速度和准确性
  • 自动发版
    • 实现功能:
      • 集成自动化发版流程,简化了版本发布操作,确保系统能够快速、精准地进行版本迭代
    • 技术细节:
      • 通过Golang和Python编写自动化流水线脚本,整合了CI/CD工具,实现了从构建到发布的全流程自动化
  • 任务管理
    • 实现功能:
      • 实现了多种任务类型管理功能,包括提测任务、Daily任务、伴生任务、灰度任务和多MR合入任务,满足不同开发和发布场景的需求
    • 技术细节:
      • 使用Golang实现任务管理的核心逻辑,确保任务的调度和执行高效稳定
      • 通过Python实现任务日志的收集和分析,辅助开发团队进行任务追踪和故障排查
  • 配置管理
    • 实现功能:
      • 开发了系统配置管理模块,支持测试能力配置、操作日志和配置日志的记录与管理,增强了系统的可配置性和可维护性
    • 技术细节:
      • 使用Python实现配置管理模块,通过灵活的配置选项适配不同的测试和部署环境

# 正式构建管理

  • 构建模式:
    • 系统支持人为触发的正式构建,采用PEEK_BUILD模式,由mazu触发,通过mr_trigger任务标签进行管理
    • 此次构建针对模块locolsim进行操作,确保模块的构建过程符合标准化要求
  • 技术实现:
    • 使用Golang和Python结合实现构建触发与管理的功能,通过自动化脚本集成CI/CD管道,实现了正式构建的可控性和稳定性

# 项目成果

  • 提升了车端Bundle包的构建效率和质量,减少了发布过程中的人为错误,确保了构建制品的稳定性和一致性
  • 项目上线后显著降低了多项目、多版本管理的复杂度,提高了团队的开发和交付效率

# 1、介绍

  • 项目背景:

    • 车端构建系统的核心目标是解决车端Bundle包的打包构建问题
    • 通过该系统,能够高效地生成符合要求的Bundle包,确保各个版本的稳定性和一致性
  • 系统特性:

    • 多项目关联: 一个版本中可以关联多个项目,每个项目对应一个独立的代码仓库

    • 树形结构版本管理: 最终的版本构建为树形结构,虚拟根节点为algorithm_all_app,其下包含各个子项目和模块

# 2、核心功能模块说明

  • 项目管理与模块管理

    • 项目管理: 提供版本管理功能,一个版本可以关联多个项目,确保每个项目的代码仓库都能在版本中被统一管理和打包

    • 模块管理: 系统支持对各个模块进行管理,便于用户进行模块级别的操作和配置

  • Bundle管理

    • Bundle生成: 系统会依据指定的版本和模块生成对应的Bundle包

    • Bundle稳定性标记: 对于正式的构建,系统会将构建产出的模块和制品标记为稳定制品,确保该版本的稳定性

  • Bundle对比

    • 提供对不同版本或不同模块之间的Bundle进行对比,快速识别出差异,确保版本的一致性和可靠性
  • 自动发版

    • 系统支持自动发版流程,简化了发布操作,确保各个版本的发布能够快速、精准地进行
  • 任务管理

    • 提测任务: 支持在开发流程中进行提测任务的管理,确保代码在合适的阶段被测试

    • Daily任务: 系统支持每日任务的执行,确保日常构建的有效性

    • 伴生任务: 支持与主任务同步进行的次要任务,以保证完整的任务流

    • 灰度任务: 系统提供灰度任务管理,便于版本的逐步推广和测试

    • 多MR合入任务: 系统支持将多个Merge Request(MR)合入到同一个版本或模块中进行构建

  • 配置管理

    • 测试能力配置: 系统允许配置不同的测试能力,确保在不同场景下能够执行合适的测试

    • 操作&配置日志: 提供详细的操作日志和配置变更日志,方便用户追踪和审计系统操作

# 3、正式构建说明

  • 触发条件: 系统支持人为触发的正式构建操作,触发者为mazu
  • 构建模式: 正式构建采用PEEK_BUILD模式
  • 任务标签: 任务标签为mr_trigger,标识此次任务为由MR触发的构建任务
  • 触发模块: 构建将针对模块locolsim进行操作和处理

# 08.版本克隆

  • 通过发布系统『创建版本』时,新版本及其依赖配置得通过纯手工的方式一个个添加,耗费人力成本比较高,且易出错
  • 可以基于已存在的版本配置clone一份新的配置,再根据需要对部分配置进行人工的update

# 09.逆序增量构建

# 1、逆序构建介绍

  • 当新增、删除、修改依赖时,对应更新redis内容

  • 此处需要存在一个异步的事件驱动器来保证用户更改依赖关系后,redis能实时进行依赖同步

  • 注意:当查找X模块的Parents时,若redis中不存在,需要到数据库中查找,最终需要将结果更新到redis中

  • 获取待构建的模块列表 S {J, H, I, E, F, D, A}

    • 找到J的parents: {H,I}

    • 迭代来找模块,最终到A

  • 在构建模块列表S的过程中,因为遍历了J->A的完整逆向链路,所以也就同时生成了J->A的DAG图,如下

{
    "J:10": ["H:8", "I:9"],
    "H:8": ["E:5"],
    "I:9": ["E:5", "F:6"],
    "E:5": ["D:4"],
    "D:4": ["A:1"],
}
1
2
3
4
5
6
7
  • 从A开始正向全量构建

    • 若当前模块在列表S中,表示该模块需要重新编译,遍历其依赖模块(比如A模块依赖B、C、D)

    • 若当前模块不在列表S中,取当前模块最新编译产物即可

    • 遍历至无需重新编译模块或叶子节点时,开始编译父模块

  • 特点
    • 每次都动态生成待构建的模块列表S,触发全量构建后,只有模块在待构建的列表S中时才会触发该模块的重新构建
    • 复用了当前正向构建流程,并且可以将列表S内所有的构建任务归并到一个task中

# 2、处理流程

  • 假设模块H发生了变更,需要逆向构建与其相关的各个模块,最后打包一个新的A

  • 处理流程如下

    • 编译H模块,得到产物H1

    • 找到H模块的parents: {E}

    • 触发E模块的编译(只编译一层,依赖G\I取最新编译产物,H模块取H1,不会递归编译所有依赖项),得到产物E1

    • 找到E模块的parents: {D}

    • 触发D模块的编译,依赖F取最新编译产物,依赖E取E1,得到产物D1

    • 逆向迭代至A模块,得到产物A1即为最终产物

  • 存在的问题

    • H、F节点同时向上触发构建时,E节点无法同时获取H\F节点的最新构建记录,需要有一个外部的构建控制逻辑来指导

# 3、旧流程问题

# 10.车端构建系统梳理

# 1、版本管理

  • 一个版本中关联了多个项目
  • 版本管理项目
    • 虚拟根节点:algorithm_all_app

# 2、配置关联关系

  • 一个版本中关联了多个项目,每个项目都是一个代码仓库
  • 最终一个版本是一个树形结构,根节点是虚拟根节点:algorithm_all_app
  • 触发人为 mazu 的是一次正式的构建(正式构建产生的 模块和制品就会标记为一个稳定制品)
    • 构建模式:PEEK_BUILD 任务标签:mr_trigger 触发模块:locolsim
  • 模块中定义了依赖的子模块

# 3、prepare阶段

  • arg_app 模块运行时为例
    • arg_app在prepare阶段触发自己依赖的子节点运行,子节点又触发自己依赖的子节点,直到没有依赖开始执行
    • 父节点在prepare阶段会一直监听自己子节点的完成,拿到子节点的制品url,更新到自己的“构建配置快照”中

# 4、compile阶段

  • 在compile阶段会比较prepare阶段中依赖的子模块上报的制品版本是否有变化
  • 如果所有子模块制品都没有变化就不编译,否则使用子模块最新版本重新编译(类似于golang项目中升级某一个包)
  • 每个子模块产物名称是唯一的,每次新的构建会产生一个新的版本号
  • compile阶段会触发GitLab的pipeline,pipeline中定义了执行的cli命令行
  • cli中将制品上报到制品仓库,并将执行状态上报到平台(通知上报到nfs中)
  • 判断是否有构建必要(delta_build: ture)
    • 比如在 arg_app这个模块,在AD150这个版本中,从数据库获取数据找到最新的制品
    • 第一:判断构建的
    • 最新的制品与依赖子模块上报的运行时制品是否一致

# 5、.gitlab-ci.yml

stages:
  - build
  - publish
  - deploy

build:
  stage: build
  image:
    name: adas-img.nioint.com/aa-devops/golang:1-14-ssh-build
  script:
    - git config --global user.email "${CI_USERNAME}@nio.com"
    - git config --global user.name "${CI_USERNAME}"
    - go env -w GOPROXY="https://goproxy.cn"
    - make
  artifacts:
    paths:
      - ./bin/release-system
  only:
    - develop
    - release
    - /^patch_.+/
    - /^hotfix_.+/
    - /^feature_.+/
  tags:
    - gitlab-runner-aip

publish:
  stage: publish
  image:
    name: adas-img.nioint.com/aa-devops/kaniko-executor:no-error-check
    entrypoint: [""]
  script:
    - /build.sh ./Dockerfile $CI_SERVICE $CI_PROJECT $CI_COMMIT_REF_NAME tencent-dev
  dependencies:
    - build
  only:
    - develop
    - release
    - /^patch_.+/
    - /^hotfix_.+/
    - /^feature_.+/
  tags:
    - gitlab-runner-aip

deploy:
  stage: deploy
  image:
    name: adas-img.nioint.com/aa-devops/deploy-util:latest
  script:
    - CURRENT_LOCATION=`pwd`
    - python3 /deploy-utils/deploycli.py deploy -env tencent-dev -project $CI_PROJECT -service $CI_SERVICE -branch $CI_COMMIT_REF_NAME -deployname aip-release-system -sourcecodepath $CURRENT_LOCATION
  only:
    - develop
  tags:
    - gitlab-runner-aip
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

# 11.创建版本

https://nio.feishu.cn/wiki/D7i8wEQAniXXn2k2mP3cHHGknPh

# 1、创建版本

版本是一个完整的编译,发布的整体集合

  • 点击创建版本
  • 填写版本信息
    • 版本类型说明:
      • 主线/T线版本为公开版本,可支持多人写作可见
      • 个人版本为私人版本,只有自己可见

# 2、添加模块

  • 一个模块对应gitlab中的一个repo
  • 所以需要自行在代码仓库中先创建好 repo,并且在repo中引入gitlab-ci
  • 备注:

    • Git Trigger Token在对应的repo中可获取到

    • 在对应的repo-》设置-》CICD-》流水线触发器 中创建一个触发器

  • 流水线触发器所对应的Token 即为需要填写在表单中的 Gitlab Trigger Token

# 3、模块与版本关联

  • 模块可以被关联到不同的版本中,并赋予不同的配置参数
  • 1)选择目标版本
  • 2)“编辑”添加模块
  • 检索刚才关联的模块
  • 4)进入模块当前版本详情页
    • 因为该模块新被加入到版本下,会显示需要对该模块进行初始化
  • 5)初始化模块版本关系
    • 点击初始化按钮,在基础版本中,选择"develop" 版本,并点击“初始化”按钮完成配置

# 4、建立模块之间的依赖关系

  • 在同一个版本中的模块,可以建立上下游的依赖构建发布关系
  • 1)点击“项目构建依赖关系图”
    • 已经完成多个模块的添加,本步骤完成之后,可建立模块与模块之间的依赖关系图
  • 2)在父模块中配置依赖子模块
    • 进入到依赖关系的父模块(点击模块,进入到详情页面),并点击“修改构建定义&配置”
  • 3)在弹出表单中切换到“构建定义”页,并编辑如下内容
  • 4)说明: 如果A依赖B,并且B的模块ID为 x的话, 则在A的配置中,增加如下标红的内容
dependency_list:
    - dependency_release_id: "xxxx"
       artifact_list: []
       
如果想添加多个下游的依赖,可增加多行

dependency_list:
    - dependency_release_id: "xxxx"
       artifact_list: []
    - dependency_release_id: "xxxx"
       artifact_list: []
1
2
3
4
5
6
7
8
9
10
11
  • 5)关系图展示
    • 备注:建议模块的更加复杂的依赖关系,可以重复上述步骤,然后在项目模块依赖关系图中查看建立的依赖关系

# 12.Gitlab CICD

# 1、术语解释

  • DevOps

    • (Development和Operations的组合词)是一组过程、方法与系统的统称,用于促进开发(应用程序/软件工程)、技术运营和质量保障(QA)部门之间的沟通、协作与整合
  • CICD:持续集成/持续发布,主要以自动化流水线的方式

  • Gitlab-CI gitlab CI是gitlab内嵌的一套支持持续集成的工具链

  • Harbor

    • Harbor是由VMware公司开源的企业级的Docker Registry管理项目,相比docker官方拥有更丰富的权限权利和完善的架构设计,适用大规模docker集群部署提供仓库服务
  • Kaniko

    • kaniko 是 Google 开源的一个工具,旨在帮助开发人员从容器或 Kubernetes 集群内的 Dockerfile 构建容器镜像
    • 其好处是在构建过程中不需要依赖root用户权限,保证了构建阶段的安全性
  • Kustomize

    • kustomize 是 sig-cli 的一个子项目
    • 它的设计目的是给 kubernetes 的用户提供一种可以重复使用配置的声明式应用管理
    • 从而在配置工作中用户只需要管理和维护 kubernetes 的原>生 API 对象,而不需要使用复杂的模版
  • Consul

    • consul是HashiCorp公司推出的一款工具,主要用于实现分布式系统的服务发现与配置
  • Docker Promote

    • 将docker image通过重新打tag并上传到不同的harbor project下,完成容器镜像在不同project之间的流转,实现了容器管理的逐级管理

# 2、代码目录组织规范

  • 代码根目录下必须定义.gitlab-ci.yml 用户启动 gitlab-ci pipeline
  • 代码根目录下必须定义Dockerfile文件用于构建容器镜像
  • 代码根目录下必须定义deployment目录用于存放kubernetes object的定义文件以及kustomization.yaml
    • ( kustomizatoin.yaml用于在部署过程中对kubernetes object定义文件进行渲染)
  • README 文件用于表述项目的基本内容 (建议提供)
├── Dockerfile
├── README
├── VERSION       #需要填写当前的版本信息 (例如 v1.0.1 )
├── deployment    #基础部署描述文件
│  └── base
│    ├── deployment.yaml
│    ├── ingress.yaml
│    ├── kustomization.yaml  #kustomization模版渲染描述文件
│    └── service.yaml
1
2
3
4
5
6
7
8
9
  • kustomization.yaml文件式例
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- ingress.yaml
- service.yaml
1
2
3
4
5
6
  • Kubernetes 命名空间规范

    • 在K8S环境中通过namespace将各个研发阶段的微服务进行隔离

    • namespace根据研发阶段名定义,并通过添加后缀的方式区分不同的环境(后缀为 -dev / -qa / -prod)

# 3、CICD配置中心

  • CICD配置中心提供中间状态的热数据的记录组件,用户存储当前部署的最新状态的能力

  • 本设计中采用consul单节点的方式,由于系统只是状态记录节点,不需要考虑对其稳定性做过多考虑,只需要保证其中数据能够持久化

  • CICD配置主要是利用了consul的key/value存储的特性,该特性可以直接通过cur命令访问创建,修改,删除,遍历数据目录

  • 无需第三方类库的依赖,使得整个流水线更加的轻量化

  • 通过CICD配置中心可提供动态数据,在Consul中的路径组织结构如下

cicd 
  |---> projects
           |---> <project_name>
                       |---><env> (dev/qa/stage/prod)
                               |---> <service>
                                         |--->latest  --> value (jsonformat)
                                         |--->current --> value (jsonformat)
                                         |--->pre     --> value  
                                         |--->deploy_patch
                                                 |---> kustomization
                                                 |---> xxxxxx
                                     
                                          |--->status
                                          ......
 |--->scripts
        |---> deploy_sh
        |--->kubeconfig
                |--->kubeconfig_qa
                |--->kubeconfig_prod
        ......
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 4、CICD流程

# 0)开发阶段流水线

  • 开发阶段流水线将依赖于gitlab-runner实现代码提交到主分支之后的构建,打包

  • 对于开发流水线对接的是开发自测环境,对于环境的稳定性要求较低,因此直接在gitlab-ci中集成部署步骤

  • 目前项目使用gitlab作为代码托管工具, Gitlab-CI 原生具有了强大的集成功能因此在开发阶段的整个CI/CD将通过 Gitlab-CI完成

  • 对于任何一个项目均需要定义 .gitlab-ci.yml 并在文件中定义三个基本的阶段

stages:
- build
- publish
- deploy
1
2
3
4

# 1)Build 阶段

  • 主要使用容器化构建,通过定义特定语言的构建容器和脚本,完成代码的构建
  • 例如下面的例子是在容器镜像中构建go语言代码
build:
  stage: build
  image:
    name: golang:1.13.1
  script:
    - go build -o main main.go
  artifacts:
    paths:
      - main
  tags:
    - <gitlab-runer-tag> 
  only:
    - <branch_name>
1
2
3
4
5
6
7
8
9
10
11
12
13

# 2)Publish阶段

  • 该阶段主要完成Docker镜像的构建,使用kaniko工具完成
  • 该工具相比Docker In Docker的方式的好处是不需要为容器申请privilage的权限,保证了构建过程的安全
publish:

  stage: publish

  image:

    name: adas-img.nioint.com/aa-devops/kaniko-executor:debug-v1.3.0-curl

    entrypoint: [""]

  script:

    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json

    - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile ./Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA-$TIME_STAMP

    - curl -X PUT -d "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA-$TIME_STAMP" http://10.115.8.55:8500/v1/kv/cicd/projects/$CI_PROJECT/dev/$CI_SERVICE/latest

  dependencies:

    - build

  only:

    - <target_branch>

  tags:

    - <gitlab-runner> 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  • 注意:上面例子中在“scirpts"阶段主要做了三件是事情

    • 将预先定义在project环境变量中的docker registor信息写入构建容器的配置中

    • 通过执行容器内置的kaniko/executor命令,并传入目标路径,构建文件地址,容器仓库地址,容器镜像名称,完成容器的构建和上传

    • 通过curl命令完成构新构建容器的注册(注册在远端CMDB中)

上次更新: 2024/12/19 17:28:11
02.GPU调度
04.资源管理平台

← 02.GPU调度 04.资源管理平台→

最近更新
01
06.Mage平台
05-30
02
16.区块链交易所
05-28
03
01.常识梳理
05-28
更多文章>
Theme by Vdoing | Copyright © 2019-2025 逍遥子 技术博客 京ICP备2021005373号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式