现代 Web CI/CD 系统的搭建

本次直播录播链接:https://live.juejin.cn/4354/595741[1]

作者:王圣松

https://juejin.cn/post/6993676240603316231

开始



欢迎大家来到掘金小册8月的首场直播,我是本次直播的主讲人王圣松

今天给大家带来的主题是:如何系统搭建CICD。话不多说,咱们就立刻开始

自我介绍




那么在开始之前呢,我先做个自我介绍。我叫 Janlay,目前任职于 Gitee Devops产品前端团队,是一位前端开发工程师,主要负责参与团队 Devops 产品的前端开发。曾经主导过 CI/CD 相关产品的前端开发,同时也是掘金小册《从 0 到 1 实现一套 CI/CD 流程》的作者。

现状与困境




在开始之前呢,我们先来聊一聊前端的CICD掌握现状。

现状




这是我在之前和其他的一个前端开发工程师的聊天。可以看到,前端工程师对于 CI/CD 的方向的兴趣还是比较大的。但是也比较焦虑。并且有的大厂面试还会问到相关的问题,里面也有涉及 Kubernetes 相关的技术。

那么大家对此的掌握度又如何呢?

对 162 个前端 CI/CD 工具的掌握度调查




我在去年写小册之前,对 162 个中高级前端做了个关于 CI/CD 工具的一个掌握调查。其中有一部分同学已经在大厂,或者已经是一个前端的一个 leader 了。

大家 Docker 的实践程度以及掌握度是非常非常低的,两成都不到只有19%。这其中对于 Kubernetes 的掌握程度甚至不到 1%。162 个人里面,仅仅只有一个人实践过。

对于制品库这类比较新鲜的概念了解更少了,不了解的人有很多,自研的人也有一部分。构建工具,部署工具,这些大家可能听的比较多,常见的比较多,掌握程度还算好。但是也有相当一部分同学对这部分还不是很清楚,或者说在采用手动部署。其实不是很适应时代发展和不利于提高效率的。

Devops 工具链

这个是某咨询公司对于 Devops 工具链的一个统计图谱。从全貌上来看呢,这些工具其实还是比较多的。除了持续构建持续部署之外,还有像监控运维,自动化测试,流程管理,容器编排这些内容。

代码托管 Git 库就有七种。CI 也不止 Jenkins,Gitlab CI 这几个。里面还存在着其他的一些工具链,其实是比较多比较杂的,这些都是 CI/CD 相关的一个工具生态。有了这些工具生态,我们才能够将CI/CD的价值完全释放出来。

那么这样看来,CICD只是作为自动化工具。那么它的意义有多大呢

为什么要有自动化?




那么为什么要自动化呢?大家可能听说过一个开发模式,叫做瀑布流。

左上角就是瀑布流开发的方式。传统瀑布流的开发方式是:把需求的排期给固定住,中间是不允许修改需求的,直到最后完全上线。测试的流程也是比较复杂的,上线流程也是比较复杂的。

我们大家可以看到,在这个图里面开发和测试之间隔着一堵墙,测试和运维之间也隔着一堵墙。那么在所有的功能开发完毕之后才能交给测试,测试测完所有功能之后才能再交给运维上线。那么这种开发模式在今天的互联网时代的话,其实已经严重脱节了。

互联网时代的产品需求变动是很频繁的,瀑布流是根本适应不了的,所以就有了敏捷开发这么一种方式。将原有瀑布流的一大块的需求,进行了一个拆分,拆成了一个一个迭代。每个迭代里面,有这个迭代相关的需求,一次迭代的需求开发完毕之后,交给测试,原有的一个整个大的流程变成了一个小步快跑的方式。研发做一部分需求测试,测试就测一部分,上线就上一部分。产品在早期的话就可以得到一个市场的快速验证。

当然需求的频繁变化,在当今是迫不得已。因为时代发展是比较快的。如果要是说大家都能看到未来的市场走向和用户需求的话,可能京东淘宝也不会允许拼多多存在。

但是敏捷的劣势随即而来了。由于上线的流程不是自动化且是比较复杂的,所以当开发和测试完成了之后,需求依然要等到固定的时间去进行上线。因为发布流程较多且复杂,还是无法把第一时间把需求统一验证,因为自动化而这道门槛卡在了这里。

于是 Devops 就来了,他把运维和开发测试的墙也打破了。我们上线部署构建的事情。可以去实现一个自动上线,自动构建,自动部署。这样的话,我们发布的成本就变小了,产品发布的周期也加快了。上线的权利从运维就转变成了研发,转变为了业务方。这也就是自动化构建部署的意义,它可以让需求更快的投入市场验证。

所以说,自动化不仅是在研发层面提效,更可以影响业务的快速发展迭代。所以从研发同学角度来讲,掌握自动化构建和部署比较吃香。

演进历程



在刚才,我们了解了自动化在业务中的意义。接下来我们就看下 CI/CD 的演进历程,看下为了尽可能地自动化,技术层面都是如何一步步演进发展的

远古时代 FTP + Tomcat / Nginx



首先是远古时代的构建部署。其实说早也不是很早,我在 18 年参加某次技术分享时,与一位后端工程师有聊到这部分,他告诉我就是在使用 FTP 方式进行部署。很多学校教材中甚至还会教学这部分。的确,在早期的网站部署中,FTP 的确比较常见。

首先是在本地将代码构建。

在 FTP 盛行的年代,Webpack 那个时候可能都没有,可能只有几个 JQuery 页面,或者是静态页面,将页面文件直接扔到服务器。

很多的做法都是和后端的代码都放一块儿。比如像 Tomcat、Apache。


随着时代的发展,FTP 的方式实在是太落后了。我之前有讲到:产品需求需要快速投入市场验证,除了需求要进行拆分之外,上线的流程也要做到自动化,才能实现快速投入的市场的这么一个目标。

没有自动化的操作,风险也是比较大的。比如说有时候眼花没有操作好、不小心误触、或者不小心删除了文件,导致服务和系统出现问题。这种情况在历史上发生了 n 多次。而且自动化也解决了很多重复的一些人力。

一个操作,机器可以自动化去执行,但人去执行,这个对于成本而言就是一个浪费。

自动化序章 Shell + Nginx



于是人们对于自动化最初的探索就是写 Shell 脚本,可以通过编写 Shell 脚本来实现自动化操作。首先在脚本里面我们将代码拉下来,这里面少了一步 git fetch 的命令,上面大家可以脑补一下哈哈。将代码拉取下来之后,进行一个 npm run build,编译之后扔到对应的服务目录。

上图这个就是有独立部署服务器,我们可以考虑采用 SCP 的方式将编译后的压缩包扔到目标服务器。使用 SSH 命令远程操控服务器进行一个解压。比如上面第10行: tar zxvf,将 tar 包解压后,再 mv 移动到对应目录。

这就是我们对于自动化最初的一个探索,那么在今天也有相当一部分业务还是在使用这种方式。包括像我们之前的一些内部的、一些小的业务,也是在使用这些方式进行部署。这也侧面印证这部分方式的成本还是比较低的。


那么,这方面成本是比较低的,而且不需要额外的服务依赖,不需要多么复杂的操作。也不需要多么复杂的架构。

但是随着业务增长和团队人数扩张,就可能变得不是多么很友好。

首先你可以在服务器上执行这个脚本,就说明了你有服务器的权限。那么大家都想去构建的话,密码公钥的泄密的风险就加大。人人都可以去访问服务器,万一有人想使坏删库跑路;或者说不小心删除了某个文件,不小心误操作导致了服务模块,也是一个灾难性的问题。

其次,他的操作交互也不是很友好。那么我们是不是可以利用软件开发的一个思想,将这些真实的操作去做一个上层的封装呢?提供给用户一个统一的入口,用户只能够执行你给的一个操作范围,可以在界面上进行一键执行。


于是像很多公司,选用了 Jenkins,这个是非常经典的一个构建工具,拥有像可视化操作。而且它是有账号体系的,你可以根据账号去分配权限。不同的人看到不同的任务,可以去执行不同的任务。当然他也有丰富的插件也可以进行一个拓展,比如说像 git plugin、node plugin。也有非常开放的 API,可以去做一个自定义拓展。

可视化执行 Jenkins + Nginx



我们用进行一个自动化改造之后呢,一切看起来是比较美好的。在提前写好 Shell 之后呢,我们一键执行就可以完成你想执行的脚本。

甚至 Jenkins,还给你提供给你像定时执行这样的一个功能。你还可以去搭配 Git 平台的 Webhook 钩子,在你提交代码,或者说合并某个 pull request 的时候,Git 就会利用 Webhook,去调用你 Jenkins 的构建触发链接,对代码进行一个构建触发。

大家可以看到上面两张图片:左上角就是 Shell 脚本的编写区域,右边就是编译的一个日志,这些都是可以在网页中进行编写和实现的。

那么下面就是这个整体的流程。当我们把代码提交到仓库时,这时候打开 Jenkins,去点击这样的一个构建按钮,接着就构建完成了。或者说我们直接 push 到仓库,触发 Webhook 钩子,最后直接构建完成。这是更加便捷的一种方式。


可是,有一天服务。不止你们自己用。例如现在很多公司在做 tob,你的服务不只是你自己在用,你要把你的服务拿到客户那里进行部署。

在这种情况下,如果你的服务需要非常复杂的环境安装的话,是比较头疼的。尤其在客户变多的情况下,安装环境也是非常的费时费力的,甚至说还会遇到说操作系统不一致的问题。例如在你自己的开发环境上是 Ubuntu,到了客户那里就变成了 CentOS,有些环境依赖的安装方式是不兼容的。

版本归纳也是一个大问题。尤其是每次版本包非常大,安装部署都需要拉去特别重的安装包,也非常费时间。最理想的情况,是说我拿一个虚拟机一样的东西,直接到客户现场直接跑了起来。

Docker 在这个地方就派上用场了。它可以将你的运行环境、服务代码等之类的东西高度集成为一个镜像。在你想要的地方,如果那个地方有 Docker 运行环境,我们可以将 Docker 镜像跑起来,就拥有了全套的一个运行环境体验。

而且 Docker 的每个镜像的更新都是增量的,只拿去拉取修改的那一部分,不需要每次都拉取全量的镜像,也非常省空间省时省力。

容器时代 Jenkins + Docker + Nexus + Nginx 容器



于是我们将原有的服务用 Docker 镜像跑了起来,然后构建编译构建包变成了编译镜像。在这里还会引入一个新东西 —— 制品库,右边这张图就是制品库。我们将每次编译后的产出都被称为制品,存放这些制品的文件存储系统就叫制品库。

右上角这张截图是 Nexus 的一个制品库。像Java也有自己的包规范 Maven、Node 的包规范 NPM、Docker 的镜像,这些 Nexus 都可以去进行一个创建和托管。所以对于一些中小团队而言,Nexus 还是比较万能的,也是比较省成本的。有这么一套东西前后端都可以去用。

于是流程就变成了:当我们去构建镜像的时候,构建完毕,接着就会去 push 镜像到镜像库。原有的构建代码变成构建镜像,push 到镜像库后告诉远程服务器你要拉取镜像。

像左边这张图最下面里面有一个 SSH 命令:这时候它就会操作 151 服务器利用 Docker 命令将镜像拉下来,然后将容器停止掉,删掉,再跑一个新镜像。那么这样的话,我们的服务和它所需要的运行环境是高度集成的,你不需要担心去分发,也不需要受到操作系统的一个影响。


可是问题也来了。有一天用户量变大了,你需要加服务器,一台服务器不够就要去加。最头疼的莫过于我要批量对服务器进行新版本更新,或者说要去加新服务器。如果我们加一台还好,如果加 5 台、加 10 台呢?扩展下去的话,总会变得无边无际。

我们需要一种通用的策略去解决问题,不仅可以批量操作这个服务器,更希望说在我们去添加这个服务器的时候,也可以去变得特别的便捷。就好像写个配置文件一样,写个清单我们就可以写一个操作。

那么在这里有一个非常知名的工具叫 Ansible。Ansible 是红帽推出的一个自动化运维工具,它可以去根据你提前预制好的服务器清单,对服务器进行一个批量的操作和部署。

你可以去像写一个记事本一样,写一个 yaml 文件,json 文件一样去写入:你的第一个服务器的 IP 多少,账号密码多少、第二个服务器账号密码是多少....这样就形成了一个清单。把这个清单交给它后,他就会按照你清单上面写好的账号和密码、服务器地址去对我们远程服务器去操作脚本执行任务。

本质上还是一个自动化一个运维工具,它是可以自动化帮你去执行命令的一个工具,不仅仅是用来部署。包括像后面我们有提到说 Kubernetes 去安装 Node 的时候,如果你的 Node 的机器比较多,都需要安装环境的话,Ansible 是能够派上用场的,用场还很大。而且这个是 Python 写的,性能比较快一些。

基于配置清单服务器批量操作 Ansible



那么这个就是 Ansible 操作的 Playbook 脚本集。

右边这张图大家可以看到:这是一个样本文件,里面有一个 task 字段。Ansible,他将你的脚本里面每一条要执行的命令都产生了一条条 task。并且第一条 task,第二条 task 之间还可以去支持异步执行,还可以支持错误中断。比如说我中间的执行出错退出,我还可以去忽略这个错误继续执行。

整个 Playbook 脚本集还可以根据变量去进行一个实例化。比如我这个地方在用 timestamp 变量,上面可以通过 var 字段去定义一个是 timestamp,后面的 value 就是给它的变量默认值。我在左边去执行 ansible-playbook 命令的时候,用 -e 这个参数就可以去把 timestamp 变量可以传进来,每次都会生成一个新的构建的操作实例。

那么在这种情况下,我们对原有的服务器替换镜像,也变成了去操作 Ansible,让 Ansible 去操作对应的机器进行一个镜像的替换。那么大家可以看到:镜像 push 到镜像库之后,接着去操作 Ansible 批量操作服务器,受控的服务器去拉取镜像,然后再把原有的容器删除掉,基于新版本运行新容器。



那么,Ansible 帮我们实现了批量操作服务器的梦想,但是它的做法是比较生硬的,因为它只是一个自动化的运维工具。

我们运行的是一个容器,有的时候可能会碰到说负载均衡需要配置 upstream,环境变量的情况。尤其像 toB,大家在做微前端,或者说服务之间有互相引用的地方,都少不了一系列的环境变量。比如这些 nginx upstream 的值其实都是不固定的。

举个例子,我现在有一个系统,前端页面我们要访问 /user 的时候,要去访问 /user 配置的机器;那么访问 /a 的时候,要去访问 /a 的容器。这个时候如果你只有自己用还好,假设到了客户现场的话,客户现场的环境是比较复杂的,你根本不知道他会用什么情况去部署。Nginx 的 upstream 那么在这种情况下,你把具体的值写死,完全不利于后面的部署。这个值真正部署是不固定的,环境的网段和 DNS 都会去影响这个值。

所以我们希望有一个给运容器运行的环境生态,容器高度隔离的一个土壤。容器在自己的生态里面就能去解决这些繁杂的七七八八的一些问题。那么我们再去给客户部署的时候,只需要带着这一套环境生态去客户那里部署,这个环境生态就可以去完美的去实现 1:1 的一个还原。

其实在我们前面去更新容器的时候,都会将原有的容器删掉,再创建新容器。中间的时间如果用户去访问,就是宕机,服务没有起来就访问不到。这种情况下,其实相对而言还算是好一些的。如果连你的新版本的容器因为出错都没有起来,那么你这个时候问题就大了,就算是一个事故了。回滚也是一个比较麻烦的事情。

所以说我们希望服务发布的时候不要停机。新发布失败了,还可以自动终止新版本发布,回滚到旧版本。

在保证上面的一个运行生态的时候,还希望去做一个最大化的机器资源利用。比如说我们有多套环境,而且之间都是隔离的,互相不受影响。例如我们日常开发有测试环境,开发环境。希望服务器资源利用最大化,一组高配置的服务器就可以去部署解决这两套环境。

我们希望去达到这样一种理想的状态。在这种情况下,选择了 Kubernetes,简称 k8s。

Kubernetes 是一个容器编排工具,他给你容器提供一个很好的环境生态,就像提供一个完整的家一样,所以说它叫容器编排工具。在 Kubernetes 里面是以它去管理你要运行容器的服务器,是以集群的方式去管理的。你的每一台服务器都是一个节点,它可以根据你集群内的节点的一个剩余资源,还有你人工给他的标签等影响因素,对服务的部署进行了自动的调度,最大化的利用我们的服务器资源。

对于应用更新,也可以采用滚动发布服务的方式,在发布期间不会影响新版和旧版的一个访问,也就是说不会宕机。这个技术也是比较热门的,现在很多公司大厂也是在用。

容器集群编排 Jenkins + K8S + Docker + Nginx



那么左上角就是 k8s 运行时的情况。左上角图是滚动发布,可以看到,k8s 在启动一个全新的容器之后,他会确保说新容器没有问题有待杀死原有的一个容器。这样保证了一个可访问性,保证不会因为服务重启而导致宕机。

左下角图是 k8s 加入一个新节点的方式。可以看到,只需要通过 API 操作一个远程访问,就可以加入集群,而且都是服务化。基于密钥、IP、端口加入就可以。它和集群内的其他节点和集群内的主控制节点其实都是解耦的。想删除就删除,想添加就添加,只是把它当成了一个服务。

右边是 k8s 集成后的一个流程图。从最下面可以看到,当用户提交代码到 Git 了之后,这时候会去触发 Webhook。或者你手动去触发 Jenkins。这时候 Jenkins 就构建镜像,上传镜像到了制品库,接着去调用了 k8s 集群进行一个制品的部署。

这时候告诉 k8s:我的镜像版本更新了,给你新版本的镜像地址。这时候 k8s 就把镜像版本在配置文件中进行一个修改。这个时候就会拿新版本去镜像库拉取新镜像,拉取之后就会对原有的 pod 进行删除替换。

你可以大致理解为,pod 约等于一个容器,但是它其实不等于容器。k8 s当中的 pod 不只是拥有容器的这个概念,它里面还可以拥有多个容器,他也是 k8s 当中可调度的一个最小节点,也拥有网络的分配权限,所以它不等于一个容器。

可以看到滚动升级的策略。左边这个图原有是 4 个蓝色的 pod,我们先把其中 1 个蓝色的给下掉,那么下来之后把新的给创建进来,ok 完成;然后剩下三个,再把其中一个给干掉,再上一个新的;再剩下两个再把其中一个干掉,再上新的,最后完成全部的部署,这就是滚动发布的这么一个方式。

升级完毕之后,k8s 当中还带来了一种概念 ingress,这个有点类似于 Nginx 的负载均衡。大家可以看到这张图,浏览器去访问的时候,经过一堆转发,走到了 k8s 集群;k8s 集群进来之后,首先是走到了 ingress。根据路径左边是 path = /,然后去根据路径走到了 service。右边这个不仅有 path = /,还有灰度的 cookie。说明他是灰度用户,给他了灰度的一个服务的访问。ingress 的作用就是在这里,他可以去做灰度发布,也可以做 path 转发。

k8s 就给容器去提供了这么一个完整的运行生态。国内大厂基本上都有在使用 k8s 包括像 serverless,也有的通过 k8s 进行一个底层实现。这种编排方式比较省时省力省心的。


当然 k8s 也不是最好的解决方案。未来 Serverless 的部署会更加便捷。

例如左下角的弹性伸缩,按需付费。我们现有k8s的 Node 节点还是需要去手动加入、手动删除。它并不能根据你的服务的流量情况对节点进行一个自动的增加伸缩。不过在 Serverless 里是可以做到的,因为真正的对物理资源进行一个伸缩的时候,这个时候才会真正达到省钱的目的。无服务器的函数计算,也应对了一部分的一个无状态的操作和服务,也是比较省钱且快速方便的。当然,他的基础也是Docker

国内的云服务器厂商也对一些常用的前后端框架进行了一个快速部署的支持。比如像 Next.js、Koa.js、Egg.js 这些都支持实现一个常用框架的快速部署。部署起来会更加的方便。

回到开始



以上是我们的一个演进流程。可以看到,前人为了提升工程效能,想尽了一切办法来优化提升。

接着让我们回到主题:为什么要用自动化?

自动化不是为了偷懒,也不是为了做而做,更不是为了刷KPI/OKR。而是为了让产品和需求可以更快速上线验证,更加早期地分析用户信息,需求更加匹配用户。那么这就是我们要做自动化的一个目标。

现实很残酷



现实是比较残酷的。上线实现了自动化,然而需求从开发到上线的时间丝毫没有一点影响。那么这时候你就要考虑:像测试的时间占了多少?有没有大量的人工测试?

无法打通上线的最后一公里,就像比较经典的小程序。小程序的上线就不同于以往的开发上线,最后一公里怎么打通?

投入和产出不成正比,本来你的业务不是多么很热门的业务,一个月部署不了几次,还有必要去做 CI/CD 吗?它的意义有多大呢?

对于一些特殊的场景,目前的开源工具依然没法去打通这么一个构建。最后一公里还是要去手动操作。而且自动化做了,影响有时候也是不大。所以我们就要根据业务进行一个对症下药,选择合适的工具进行一个改造,包括像不限于对现有的工具进行拓展,或者说可以考虑到自研。

做平台,而不是做工具



那么还是那句话,做平台而不是做工具。我分别来举个例子,像 IDE 集成:就小程序 ide 支持的一键发布,就是一个很好的例子。它把 IDE 和 CI/CD 进行了一个联动,包括像很多大厂也在研发 WebIDE,也是出于这方面的一个考虑。还有我们日常任务卡片进行一个关联和绑定,可以更好的追踪需求的开发情况。

像自动化测试:我们的上线时间有缩短,但是测试时间没有变短。大量的人工测试还是比较浪费成本的,这个我们也可以考虑去优化;

根据不同用户的灰度发布的一个策略:比如说基于用户画像的地域,基于其他的一些因素进行灰度发布;还有像服务的一个监控,告警。

像数据度量平台:比如我们可以统计构建时长、发布时长等等,为了更好地去做优化。甚至非常 nice 的流程可视化的拖动:CI/CD 的流程编排现在也是比较火的。这些都是说 CI/CD 去适应业务的一些例子。

工具是一个比较抽象的平台,但业务场景是复杂多变的,我们需要使用工具去演变成自己的平台。之前我们所说的  Docker、Jenkins、Nexus、Kubernetes 都有自己的 OpenAPI,你完全可以根据自己的业务需求进行一个自定义的封装。

持续精进




那么总结一下:自动化是解决问题的手段,而不是一个结果;适当的去做数据度量统计,可以更好的帮助你去对流程的一个改进;要选择自己合适的工具去做自动化。

Q&A



接下来是问答:

移步至录播:https://live.juejin.cn/4354/595741[2] 39分53秒

小册推广


然后在这里推广一下我写的小册。传送门:https://juejin.cn/book/6897616008173846543[3]


推荐书籍

这里推荐两本书。

第一本是 k8s 的进阶实战,个人在查阅相关知识点的时候经常用,不过官网的文档还要看的

第二本是《Devops 实验指南》,对于你的一些自动化的流程的建设,Devops 文化的建设,度量平台建设也是非常有帮助的。

推荐平台 & 项目



这是一个推荐参考的平台和项目。这里没有打广告,大家如果有自研兴趣需求的话,可以参考上面这些平台的功能设计。

第一个是华为云的 DevCloud。个人感觉他们的 CI/CD 方面做的还比较全的,包括像 CI/CD 可视化拖拽,还支持流水线的中断执行。我们一条条的任务就可以去组成一个流水线,就像工厂流水线一样。流水线里面的 task 还可以支持并行执行。

第二个百度效率云。这个维护的频率比较低,它们的特点是研发数据链。比如提交代码时候,commit 的 message 填入卡片 ID,这时候在卡片里面可以监控到,需求的代码已经提交到代码库里面了。接下来进行构建的时候,也会去和需求卡片进行一个绑定。业务方就可以去清楚的看到需求的一个开发和上线的一个状态与进度。一个卡片就可以去串联所有的进度状态。

第三个 KubeSphere 是针对于 k8s 的一个集群管理。包括对 k8s 的服务监控,CI/CD 的编排。这个是开源的,也是国产的。

Thanks



我的分享就到这里,感谢大家!

参考资料

[1]

https://live.juejin.cn/4354/595741: https://live.juejin.cn/4354/595741

[2]

https://live.juejin.cn/4354/595741: https://live.juejin.cn/4354/595741

[3]

https://juejin.cn/book/6897616008173846543: https://juejin.cn/book/6897616008173846543

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值