• CICD——gitea+drone部署


    前言

    公司之前一直在使用 Jenkins+Gitlab 作为 CI/CD 工具, Jenkins 非常强大,它完成了几乎所有 CI/CD 的工作,并且应用于整个团队有好长一段时间了。但是随着公司服务容器化的推进, Jenkins 的一些弊端也凸显了出来:

    • 重量级:Jenkins 功能十分齐全,几乎可以做所有的事情。但是这也是他的一个弊端,过于重量级,有时候往往一个小的修改需要改动许多地方,升级\下载插件后需要进行重启等。
    • 升级不易:在一些安全 Jenkins 相关的安全漏洞被公开后,我们会对 Jenkins 进行升级,但这也不是一件容易的事。之前就出现过升级\重启后,所有 job 丢失,虽然我们所有项目配置都是以 Jenkinsfile 的形式统一存储,但是每个 job 都需要重新重新创建,包括每个 job 的权限。
    • 权限控制复杂:这其实也是 Jenkins 的一大优势,可以精确控制每个用户的权限,但是需要花费更多时间去配置,时间长了也会出现权限混乱的问题。
    • UI界面:虽然有 Blue Ocean 这样的插件来展示 pipeline ,但是还是没有从根本改变它简陋的 UI 界面。

    总结:其实就是公司嫌占用内存大,运行在容器中,jenkins+gitlab在构建时共占20多G内存,而drone+gitea在构建时共占2G左右,所以要换drone+gitea,那我怎么办,我一打工人我怎么办,开始我的面向百度部署……

    Drone简介

    Drone 是一款基于 Docker 的 CI/CD 工具,所有编译、测试、发布的流程都在 Docker 容器中进行

    开发者只需在项目中包含 .drone.yml 文件,将代码推送到 git 仓库,Drone 就能够自动化的进行编译、测试、发布。

    drone 目前支持四种 git 托管服务(github, gitlab, gogs, gitea) 而 drone-server 预先知晓了对应托管服务的 API,drone 的很多功能比如拉取 git repo list/add webhook to repo 都是通过这些 API 完成的。
    还有 drone 并不维护 user 这个概念,它把 user 的管理委托具体的托管服务。以 gitea 为例,我们在登录 drone 时,实际上相当于 drone 把用户名密码传给了gitea(委托鉴权)。同样由于这个原因,激活某个 repo 的构建(给repo加webhook) 能否成功取决于你这个账号在 gitea 里是不是该 repo 的管理员。
    这篇主讲概念挺易懂,记录一下

    为什么使用 Drone 作为 CI/CD 工具

    功能灵活强大:构建、测试、发布、部署,你想干什么都可以,一套系统全搞定
    兼容性好:支持所有 SCM、所有平台、所有语言
    环境部署简单:原生支持 Docker 容器,启动两个容器就完成了部署,其它构建、测试、部署工具在使用时会自动从 docker 仓库拉取
    扩展性强:强大的插件系统,丰富的插件可以免费使用,也可以自定义
    配置简单:正如官方宣传的那样,“configuration as a code”,所有功能、步骤、工具、命令,一个 yaml 配置文件全搞定
    维护简单:直接复用 SCM 的账号体系和权限管理,无需注册用户、分配权限

    Drone + Gitea 的 CI/CD 理解

    角色作用
    Drone服务中心前台小妹, 24小时跟进客户(Gitea)的新需求(.drone.yml), 然后把需求(.drone.yml)通知打工人(runner) 来干活
    Gitea客户准备好代码、和配置单(.drone.yml)
    drone-runner打工人打工人根据清单里不同的零件(容器)生产产品, 再交付给服务中心(Drone)
    • Server:为Drone的管理提供了Web页面,用于管理从Git上获取的仓库中的流水线任务。
    • Runner(Agent):一个单独的守护进程,会轮询Server,获取需要执行的流水线任务,之后执行。
    • drone-runner不是必选的,官方不推荐吧runner和server安装在一个实例上

    Drone部署

    mysql部署并创建库、用户

    mkdir -p /data/mysql/{conf,data,logs}
    vim /data/mysql/conf/my.cnf
    [mysqld]
    port=3306
    datadir=/var/lib/mysql
    socket=/var/lib/mysql/mysql.sock
    symbolic-links=0
    log-error=/var/log/mysqld.log
    pid-file=/var/run/mysqld/mysqld.pid
    init_connect='SET NAMES utf8
    default-storage-engine=INNODB
    character_set_server=utf8mb4
    innodb_default_row_format=DYNAMIC
    innodb_large_prefix=ON
    innodb_file_format=Barracuda
    innodb_log_file_size=2G
    
    chmod 777 /data/mysql/logs/
    docker run -itd --name mysql -p3306:3306 -v /data/mysql/conf/:/etc/mysql/conf.d -v /data/mysql/data/:/var/lib/mysql -v /data/mysql/logs/:/var/log/ -eMYSQL_ROOT_PASSWORD="mysql123456" mysql:5.7
    docker exec -it mysql /bin/bash -c 'cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime'
    
    ———————————————————————————已有库执行操作———————————————————————————————
    docker exec -it mysql /bin/bash
    mysql -uroot -pmysql123456
    create database gitea default charset 'utf8mb4';
    grant all on *.* to 'gitea'@'%' identified by 'gitea';
    flush privileges;
    
    • 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

    部署gitea(docker-compose)

    mkdir -p /data/gitea/data/
    vim /data/gitea/gitea.yaml
    version: '3'
    
    networks:
      gitea:
        external: false
    
    volumes:
      gitea:
        driver: local
    
    services:
      server:
        image: gitea/gitea:1.17.1
        container_name: gitea
        environment:
          - USER_UID=1000
          - USER_GID=1000
          - DB_TYPE=mysql
          - DB_HOST=172.17.0.1:3306
          - DB_NAME=gitea
          - DB_USER=gitea
          - DB_PASSWD=gitea
        restart: always
        networks:
          - gitea
        volumes:
          - /data/gitea/data/:/data
          - /etc/timezone:/etc/timezone:ro
          - /etc/localtime:/etc/localtime:ro
        ports:
          - '10800:3000'
          - '2221:22'
    
    docker-compose -f /data/gitea/gitea.yaml up -d
    
    • 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

    容器启动后,通过域名或IP+端口的方式访问,进行初始配置,并创建drone应用
    在这里插入图片描述
    设置——>应用——>创建应用:
    客户端ID:
    c256df8a-e8b1-4adb-acd4-125585f744b4
    客户端密钥:
    gto_c5lwzx7sl2hcueing7fj76akbe5m4gmb5df57keeagwb3mctjdfa
    应用名称:
    drone
    重定向 URI:
    http://drone.***.cn/login
    生成共享密钥:
    # openssl rand -hex 16
    LFS_JWT_SECRET = qTBmOJDyX2opTIGVh70IOty5VteR-SI2LDKh2tyU090
    在这里插入图片描述

    部署Drone及Runner

    Drone部署需要安装docker-server(Drone 与流行的源代码控制管理提供商无缝集成) WEB界面和docker-runner守护进程执行器

    创建一个共享密钥,用于drone-runner和drone-server之间的通信

    [root@master drone]# openssl rand -hex 16
    MH3HMzA7KC_frJunGTcDAw8Nx5druSqsKlwZDTI_GCI
    
    • 1
    • 2

    部署drone-server
    可参考:https://docs.drone.io/server/reference/

    mkdir -p /data/drone/data/
    docker run \
      --volume=/data/drone/data:/data \
      --env=DRONE_GITEA_SERVER=http://gitea.***.cn \		 # GitLab 服务器 url,默认https://github.com
      --env=DRONE_GITEA_CLIENT_ID=c256df8a-e8b1-4adb-acd4-125585f744b4 \		# GitLab oauth 客户端 ID(必填项)
      --env=DRONE_GITEA_CLIENT_SECRET=gto_c5lwzx7sl2hcueing7fj76akbe5m4gmb5df57keeagwb3mctjdfa \		# GitLab oauth 客户端 ID(必填项)
      --env=DRONE_RPC_SECRET=MH3HMzA7KC_frJunGTcDAw8Nx5druSqsKlwZDTI_GCI \		# 验证服务器和运行器之间的 rpc 连接(drone-server和drone-runner连接时必填项)
      --env=DRONE_SERVER_HOST=http://drone.***.cn \		# 验证服务器和运行器之间的 rpc 连接(drone-server和drone-runner连接时必填项)
      --env=DRONE_SERVER_PROTO=http \		# drone服务使用的协议 (必填项)
      --env=DRONE_REGISTRATION_CLOSED=true \			# 禁止用户注册
      --env=DRONE_OPEN=false \
      --env=DRONE_ADMIN=root \
      --env=DRONE_USER_CREATE=username:root,admin:true \		  # Drone管理员用户创建
      --publish=7120:80 \
      --publish=7121:443 \
      --restart=always \
      --detach=true \
      --name=drone \
      drone/drone:2.13.0
    
    
    drone启动参数很多,下面解释下:
    # DRONE_GITLAB_SERVER: GitLab 服务器 url,默认https://github.com
    # DRONE_GITLAB_CLIENT_ID: GitLab oauth 客户端 ID(必填项)
    # DRONE_GITLAB_CLIENT_SECRET: GitLab oauth 客户端密码 (必填项)
    # DRONE_RPC_SECRET: 验证服务器和运行器之间的 rpc 连接(drone-server和drone-runner连接时必填项,用于连接Server和Runner之间,两者需一致)
    # DRONE_SERVER_HOST: 提供drone服务主机名或 IP 地址 (必填项)
    # DRONE_SERVER_PROTO: drone服务使用的协议 (必填项)
    # DRONE_USER_CREATE: Drone管理员用户创建
    
    • 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

    部署drone-runner
    可参考:https://docs.drone.io/runner/docker/configuration/reference/

    docker run --detach \
      --volume=/var/run/docker.sock:/var/run/docker.sock \
      --env=DRONE_RPC_PROTO=http \
      --env=DRONE_RPC_HOST=http://drone.***.cn \
      --env=DRONE_RPC_SECRET=MH3HMzA7KC_frJunGTcDAw8Nx5druSqsKlwZDTI_GCI \
      --env=DRONE_RUNNER_CAPACITY=2 \
      --env=DRONE_RUNNER_NAME=my-first-runner \
      --env=DRONE_RUNNER_CAPACITY=5
      --publish=7122:3000 \
      --restart=always \
      --name=runner \
      -e DOCKER_API_VERSION=1.39 \
      drone/drone-runner-docker:1.8.2
    
    
    drone-runner启动参数很多,下面解释下:
    # DRONE_RPC_PROTO: 用于连接 Drone 服务器的协议
    # DRONE_RPC_HOST: 提供 Drone 服务器的主机名
    # DRONE_RPC_SECRET: 用于向 Drone 服务器进行身份验证的共享密钥(用于连接Server和Runner之间,两者需一致)
    # DRONE_RUNNER_CAPACITY: 限制运行器可以执行的并发管道的数量
    # DRONE_RUNNER_NAME: 设置runner的名字
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    验证drone-runner是否连接上drone-server(successfully pinged the remote server):

    # docker logs -ft runner
    2022-10-25T11:06:52.308205454Z time="2022-10-25T11:06:52Z" level=info msg="starting the server" addr=":3000"
    2022-10-25T11:06:52.321089697Z time="2022-10-25T11:06:52Z" level=info msg="successfully pinged the remote server"
    2022-10-25T11:06:52.321109106Z time="2022-10-25T11:06:52Z" level=info msg="polling the remote server" arch=amd64 capacity=2 endpoint="http://drone.***.cn" kind=pipeline os=linux type=docker
    
    • 1
    • 2
    • 3
    • 4

    浏览器访问drone-server Web界面(http://192.168.30.33:7118),点击继续Continue,自动跳转gitlab,授权即可进入drone

    进入可看到gitlab所有项目,选择一个项目,进入点击activate 激活进入项目即可

    如果要支持导入,需要在conf/app.ini文件中增加如下配置

    [migrations]
    ALLOW_LOCALNETWORKS = true
    
    [webhook]
    ALLOWED_HOST_LIST = 10.10.9.208/16
    
    # docker-compose restart
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    部署registry私有仓库

    mkdir -p /data/registry/data
    mkdir /root/registry-auth
    yum -y install httpd-tools
    htpasswd -Bbn bob pwd@123 > /root/registry-auth/htpasswd
    创建镜像服务器容器加载身份验证:docker  run -d -p 7123:5000 --restart=always --name registry -v /data/registry/data:/var/lib/registry -v /root/registry-auth/:/auth/ -v /root/registry:/var/lib/registy -e "REGISTRY_AUTH=htpasswd" -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry REALM" -e "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd" registry
    docker tag jenkins/jenkins:lts 192.168.30.33:5000/jenkins:v1
    docker push 192.168.30.33:5000/jenkins:v1 
    vim /etc/docker/daemon.json           # 配置私有仓库地址
    {
      "insecure-registries": ["192.168.30.33:5000"],                      # docker客户端将https请求更改为http请求,不然会报错:Error response from daemon: Get https://192.168.220.125:5000/v2/: http: server gave HTTP response to HTTPS client
      "registry-mirrors": ["https://leo12tew.mirror.aliyuncs.com"]
    }
    
    systemctl daemon-reload 
    systemctl restart docker
    docker push 192.168.30.33:5000/jenkins:v1 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    部署registry私有仓库遇到的报错:

    一、配置私有仓库后,docker启动不了,报错log:
    Nov  3 15:55:10 jhsc_tool_01 polkitd[725]: Registered Authentication Agent for unix-process:31811:19363709 (system bus name :1.1387 [/usr/bin/pkttyagent --notify-fd 5 --fallback], object path /org/freedesktop/PolicyKit1/AuthenticationAgent, locale zh_CN.utf8)
    Nov  3 15:55:10 jhsc_tool_01 polkitd[725]: Unregistered Authentication Agent for unix-process:31811:19363709 (system bus name :1.1387, object path /org/freedesktop/PolicyKit1/AuthenticationAgent, locale zh_CN.utf8) (disconnected from bus)
    
    这里报错我遇到的是 /usr/lib/systemd/system/docker.service 文件中已添加私有仓库地址
    ExecStart=/usr/bin/dockerd --insecure-registry 192.168.30.33:5000
    而我又在 /etc/docker/daemon.json 中添加私有库地址,这两个添加一个即可
      "insecure-registries": ["192.168.30.33:5000"],
    
    
    
    二、docker报错:Get https://registry-1.docker.io/v2/: x509: certificate has expired or is not yet valid
    这个错误一般都是本地系统时间错误导致报错证书过期
    ntpdate cn.pool.ntp.org 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    部分配置文件参考

    APP_NAME = 鲸核
    RUN_MODE = prod
    RUN_USER = git
    
    [repository]
    ROOT = /data/git/repositories
    
    [repository.local]
    LOCAL_COPY_PATH = /data/gitea/tmp/local-repo
    
    [repository.upload]
    TEMP_PATH = /data/gitea/uploads
    
    [server]
    APP_DATA_PATH    = /data/gitea
    DOMAIN           = http://gitea.***.cn/
    SSH_DOMAIN       = gitea_ssh.***.cn:7119
    #SSH_DOMAIN       = 192.168.30.33:7119
    HTTP_PORT        = 3000
    ROOT_URL         = http://gitea.***.cn/
    DISABLE_SSH      = false
    SSH_PORT         = 22
    SSH_LISTEN_PORT  = 22
    LFS_START_SERVER = true
    LFS_JWT_SECRET   = MH3HMzA7KC_frJunGTcDAw8Nx5druSqsKlwZDTI_GCI
    OFFLINE_MODE     = false
    
    [database]
    PATH     = /data/gitea/gitea.db
    DB_TYPE  = mysql
    HOST     = 172.17.0.1:3306
    NAME     = gitea
    USER     = gitea
    PASSWD   = gitea
    LOG_SQL  = false
    SCHEMA   =
    SSL_MODE = disable
    CHARSET  = utf8mb4
    
    [indexer]
    ISSUE_INDEXER_PATH = /data/gitea/indexers/issues.bleve
    
    [session]
    PROVIDER_CONFIG = /data/gitea/sessions
    PROVIDER        = file
    
    [picture]
    AVATAR_UPLOAD_PATH            = /data/gitea/avatars
    REPOSITORY_AVATAR_UPLOAD_PATH = /data/gitea/repo-avatars
    DISABLE_GRAVATAR              = false
    ENABLE_FEDERATED_AVATAR       = true
    
    • 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

    .drone.yml

    插件参考:https://plugins.drone.io/

    kind: pipeline
    type: docker
    name: deployment
    clone:
      disable: true
    steps:
    - name: backend image build
      image: alpine/git
      commands:
        - echo 你好...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    Drone-使用缓存插件优化构建速度

    我们构建下载的那些包是可以存在本地磁盘的,所以不需要每次都去下载。可以参考Drone的缓存插件:drone-volume-cache,将下载的包缓存到磁盘中,下次再构建的时候就不需要下载了,从而提高构建速度。

    小技巧

    在Drone的项目配置页面,会看到Badges选项(本人并未使用)
    在这里插入图片描述

    把里面的内容复制出来,添加到项目的README.md文件头部,这样我们就可以在Gitea项目页面中看到当前流水线的状态,就像右上角红框中显示的那样。

    git提交代码如何让drone跳过本次提交,不执行pipeline

    提交代码时通过备注增加[CI SKIP]跳 例: git commit –m “first commit [CI SKIP]”

    Drone优化

    权限管理

    Drone 天生就省去了各种账户\权限的配置,直接与 gitlab、github、gitea、Bitbucket 这样的源码管理系统操作源代码的权限一致。

    项目——>设置——>可见性(将仓库设为私有——>更新仓库设置)——>签名验证设置(提交者: 信任与提交者匹配的签名——>更新仓库设置)
    在这里插入图片描述
    在这里插入图片描述

    故障记录

    Web 钩子 测试推送一直失败

    1、Web 钩子 测试推送一直失败
    2、项目根目录还没有 .drone.yml 文件

    Delivery: Post "http://drone:80/hook?secret=3yXXUv0mKyEZWNi3CGCDQLyBxTVTE7i8": read tcp 172.22.0.5:38650->172.22.0.4:80: i/o timeout
    
    • 1

    用docker logs 命令查看到 drone 容器的日志

    {
        "commit":"556017b3f889b768e5f69ae164091fef9baa50a3",
        "error":"Not Found",
        "event":"push",
        "level":"warning",
        "msg":"trigger: cannot find yaml",			# 没有找到 .yml 文件
        "ref":"refs/heads/master",
        "repo":"root/demo",
        "time":"2021-04-27T08:00:32Z"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    授权失败

    访问 Drone 自动跳转到 Gitea授权这一步时候, 报错:授权失败。

    那是因为 Drone 的 DRONE_SERVER_HOST 配置错误导致授权失败,正确应该是 能被外部访问的主机名(有端口带端口), 要注意, 你即使你改正确了, 注意缓存、没有新容器等问题

    docker logs 容器 查看日志确认,url是否正确

    {
        "acme":false,
        "host":"www.wulaoye.top:8080",
        "level":"info",
        "msg":"starting the http server",
        "port":":80",
        "proto":"http",
        "time":"2021-04-27T07:37:56Z",
        "url":"http://www.wulaoye.top:8080"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    .drone.yml构建失败

    Error response from daemon: client version 1.40 is too new. Maximum supported API version is 1.39

    是因为docker的版本问题, 所以你别管管是啥, 宿主机、容器等 ENV 都要设置一下 DOCKER_API_VERSION=1.39 环境变量, 另外容器配置不会因为 docker-compose restart 重启命令就会重新加载配置, 要用 up 命令, 该命令会检测配置变动, 重新加载配置

    解决方案一:

    # docker version
    Client:
     Version:           18.09.1
     API version:       1.39
     Go version:        go1.10.6
     Git commit:        4c52b90
     Built:             Wed Jan  9 19:35:01 2019
     OS/Arch:           linux/amd64
     Experimental:      false
    
    Server: Docker Engine - Community
     Engine:
      Version:          18.09.1
      API version:      1.39 (minimum version 1.12)
      Go version:       go1.10.6
      Git commit:       4c52b90
      Built:            Wed Jan  9 19:06:30 2019
      OS/Arch:          linux/amd64
      Experimental:     false
    
    # vim /etc/profile
    export DOCKER_API_VERSION=1.39			# 添加
    # source /etc/profile
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    解决方案二:运行容器时加参数

      -e DOCKER_API_VERSION=1.39
    
    • 1
    kind: pipeline
    type: docker
    name: test-admin
    
    clone:
      disable: false
    
    steps:
      - name: "构建test-admin可执行文件"
        image: golang:1.18-alpine
        environment:
          GOPROXY: https://goproxy.cn,direct
        volumes:
          - name: mount
            path: /testtest
        commands:
          - go mod tidy 
          - go build -o test-admin main.go
          - ls
          - pwd
          - rm -rf /testtest/*
          - mv /drone/src/* /testtest
      - name: build-start
        image: appleboy/drone-ssh # SSH工具镜像
        settings:
          host: 192.168.30.33 # 远程连接地址
          username: root # 远程连接账号
          password:
            from_secret: ssh_password # 从Secret中读取SSH密码
          port: 22 # 远程连接端口
          command_timeout: 5m # 远程执行命令超时时间
          script:
            - cd /www/   # 进入宿主机构建目录
            - ls # 更改为可执行脚本
    
    volumes:
      - name: mount
        host:
          path: /www/tmp
    
    • 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
  • 相关阅读:
    Safe Head机制技术实践分析
    企业电子期刊怎么做,用这个平台就对啦!
    02|一条MySQL更新语句是如何执行的
    工程管理系统源码之全面+高效的工程项目管理软件
    跟着 GPT-4 从0到1学习 Golang 并发机制(一)
    LSTM内部结构及前向传播原理——LSTM从零实现系列(1)
    Java 之 CAS 原理及实现是怎样的?
    Django、Flask和FastAPI的优缺点与适用场景
    1.7-02:找第一个只出现一次的字符
    授人以渔 选购篇九:扫地机器人(扫拖机器人)选购要点
  • 原文地址:https://blog.csdn.net/weixin_45373345/article/details/127497832