老九学堂之分布式设计教材

老九学堂之分布式设计教材

作者:老九—技术大黍

原文:分布式系统设计教材

社交:知乎

公众号:老九学堂(新人有惊喜)

特别声明:原创不易,未经授权不得转载或抄袭,如需转载可联系笔者授权

前言

本文由老九学堂技术大黍原创,并且该文是用来给老九学堂内训的企业内部教材。请大家未经许可不要复制和传播,谢谢!

目录

第一章 简介

  1. 硬件知识补充
  2. 分布式系统的定义
  3. 系统开发历史
  4. Docker简介
  5. Kubernetes简介
  6. LXC简介
  7. 软件开发模式简介
  8. 算法编程格式化
  9. 面向对象设计模式
  10. 开源软件的兴起

第二章 单节点容器模式

  1. 动机

第三章 侧车模式

  1. 侧车模式示例:给传统服务添加HTTPS. 16
  2. 在Kubernetes中动态配置

第四章 大使模式

第五章 适配器模式

第六章 共享服务模式

第七章 函数和事件驱动处理

第一章 简介

当今的世界到处充斥着在线应用(always-on applications)和到处都可以使用的API,以及对这些在线应用可靠性要求非常高的时代。这种需求在几十年前的全球范围内只有少数关键服务才能达到这种要求。由于当今世界的商业服务对于这种需求呈现病毒式增长,所以它潜在要求每个应用都应该满足用户需求的不断扩展性。包括针对用户移动设备使用,以及后台支持应用的需求等,从而让应用变成一个分布式的应用。参见下面示例拓扑图

image-20210428111810359.png

【分布式应用示意图】

学习分布式设计理论是为了给大家掌握Spring Cloud技术体系加分。

image-20210428111851975.png

【Spring Cloud--分布式应用开发框架】

1.1 硬件知识补充

计算机组成遵守冯.诺依曼体系,即计算机包括CPU、存贮部分和I/O三大部分组成。CPU是计算机的大脑。它通过读取、检查并逐条执行指令来执行存储在存储器中的程序。存储器单元存储指令(指令的序列叫做程序)和数据。I/O负责指令和数据进行处理器。

两个基本的计算机组织结构如下:

  • 物理共享式存储器结构:有一个被所有CPU共享的单一存储地址空间。样的系统叫做紧耦合系统。在一个物理共享式存储器系统中,CPU间的通信通过和写共享存储器的操作进行。

image-20210428112007906.png

【物理共享式存储器结构】

  • 物理分布式存储器结构:没有共享存储器,每个CPU有自己的本地存储器。CPU和本地存储器被称处理单元(PE)或简称处理机。这样一个系统有时被称为松耦合系统。在一个物理分布式存储系统中,处理机间的通信通过互联网络上的消息传递来进行,发送处理机发送命令,接收处理机接收命令。

image-20210428112028150.png

【物理分布式存储器结构】

我们对两种基本的计算机组织结构对比如下,以方便大家记忆。

image-20210428112102633.png

【物理和逻辑共享式/分布式存储器的比较】

1.2 分布式系统的定义

当讨论分布式系统时,我们需要面临许多以下许多术语:分布式的、网络的、并行的、并发和分散的。分布式处理[1]是一个相对较新的领域,所以还没有一致的定义。与顺序计算相比、并行的、并发的和分布式的计算包括多个PE间集体协同动作。这些术语在范围上相互覆盖,有时也交换使用。在《分布式系统设计》一书中给出定义如下:

  • “并行的”意味着从一个单一控制线程对数据集的锁步(lockstep)动作,在并行计算机级别上,单指令多数据(SIMD)计算机就是一个使用多个数据处理单元在许多数据项上同时进行相同或相似操作的例子。
  • “并发的”意味着某些动作可以以任意次序执行。比如,在更高级别上和在多指令多数据(MIMD)并行计算机上进行部分独立的操作。
  • “分布式的”意味着计算机的成本或性能取决于数据和控制的通信。

参见下面的“Enslow的分布式系统模型”图,以帮助我们理解上面的定义。

image-20210428112156994.png

【Enslow的分布式系统模型】

如果一个系统的部件局限在一个地方,它就是集中式的;如果它的部件在不同的地方,部件之间要么不存在或仅存在有限的使用,要么存在紧密的合作,它就是分散式的。当一个分散式的系统不存在或仅存在有限的合作时,它被称做网络的;否则它就被做分布式的,表示在不同的地方的部件之间存在紧密的合作。在给出分布式系统具体定义的模型中,Enslow建议分布式系统可以用硬件、控制、数据这三个维度加以检验。

分布式系统 = 分布式硬件 + 分布式控制 + 分布式数据

Enslow的定义同时还要求资源的分布必须对用户透明。在上图中硬件组成一维上的一些点表示如下:

  • H1—只有一个控制单元的单个CPU
  • H2—有多个ALU(算术逻辑单元)的单个CPU,只有一个控制单元。
  • H3—分开的专用功能单元,比如带一个浮点协处理器的CPU。
  • H4—带多个CPU的多处理机,但只有一个单独的I/O系统和一个全局存储器。
  • H5—带多个CPU的多计算机,多个I/O系统和多个本地存储器。类似地,在控制维上的点按分散程序的递增排序如下:
    • C1—单个固定控制点。注意,系统物理上可能有,也可能没有多CPU。
    • C2—单个动态控制点。在多个CPU的情况下,控制器不时地在CPU间切换。
    • C3—固定的主/从结构。例如,在一个只有一个CPU和一个协处理器的系统中,CPU就是固定的主结构(master),协处理器则是固定的从结构(slave)。
    • C4—动态的主/从结构。主/从的角色可以通过软件改变。
    • C5—使用同一控制器副本的多个同类控制点。
    • C6—使用不同控制器的多个异类控制点。

数据库中有两个部件可以是分布式的:文件和记录这些文件的目录。可以使用以下两种方式之一或者结合使用它们来实现分布:复制和分区。如果一个数据库有多个副本在不同的地点,就称做被复制。如果一个数据库被分成位于不同地点的子数据库,就称做被分区。在这一维上的点包括:

  • D1—含有文件和目录的单一拷贝的数据库。
  • D2—含有单一集中式目录,并且没有本地目录的分布式文件。
  • D3—每个站点都有文件和目录拷贝的复制数据库。
  • D4—有一个主结构的分区数据库,主结构保留所有文件的一个完全副本。
  • D5—有一个主结构的分区数据库,主结构仅保留一个完整的目录。
  • D6—无主结构文件或目录的分区数据库。

Schroeder给出了分布式系统特征的列表,如果一个系统具有以下所有特征,那么它很可能就是一个分布式系统:

  • 多处理单元(PE)。
  • 互连硬件。
  • 处理单元的故障无关。
  • 共享状态。

一些研究人员还认为计算机网络和并行计算机是分布式系统的一部分,如下图所示:

image-20210428112520188.png

【计算机网络的分类图】

image-20210428112530455.png

【并行计算机的分类】

在非冯.诺依曼的模型中,数据流模型是基于贪心求值(greedy evaluation)算法的,即一个函数(程序)只要它的输入数据一就绪就被马上执行。缩减模型是基于懒惰模型求值(lazy evaluation)算法的,即只有需要函数的结果时才执行求值。大部分并行计算机是建立在冯.诺依曼的模型上的。Flynn的分类法是被最广泛的用在冯.诺依曼模型范围内分类的系统。该分类法是基于指令流和数据流的多重性:

  • 单指令单数据(SISD)--这是典型的单处理机体系结构。它可能包括并行机制,比如:流水线操作、重叠的CPU和I/O操作。
  • 单指令多数据(SIMD)--每个处理机同时在各自不同的数据集上执行相同的指令。典型情况下,多个PE处于一个公共控制单元的管理之下。
  • 多指令单数据(MISD)--这种体系结构包括多个处理机,每个分别被各大自的控制单元控制。
  • 多指令多数据(MIMD)--多个处理机同时对不同的数据执行不同的指令。大部分分布式系统的物理结构属于这种类型。

上面两个分布式系统分类几乎包括技术文献讨论过的所有重要的体系结构类型,但是这些分类没有包括下面一些基本的设计选择:

  • 细粒度与粗粒度的并行性。
  • 共享存储器与消息传递通信。
  • 通用系统与专用系统。
  • 全局与本地存储器。
  • 集中式与分布式全局系统控制。
  • 一致与非一致的访问。
  • 直接与间接连接。

细粒度并行性是并行执行的一种形式,其中每个进程的计算工作量要比它的通信和额外开销要求的工作量小。在粗粒度并行性中,每个进程的计算工作量则数倍于其通信和额外开销。

通信系统是用于一般应用的系统,否则就是专用系统。MIMD是一个通用系统。SIMD尤其适合于大数据集上执行的同一类操作的数值计算。

全局存储器是从任何一个处理机上可以直接访问的,但本地存储器只是对相关处理机可见。典型情况下,全局存储器和本地存储器结合起来和高速缓存一起形成高速而低延迟的存储器。

在共享存储器系统中,通过读写共享存储器交换信息是很自然的一种通信方式。在消息传递系统中,存储器是分布式的,所以发送和接收消息是很自然的。

在一致存储器访问系统中,所有处理机距离所有存储器位置一样远,也就是说,任何处理机访问任何存储器的时间几乎是相同。但是这种结构不易伸缩。一般来说,共享总线用于连接处理机和共享存储器。随着处理机数目的增加,共享总结就成了性能瓶颈。为了降低存储器延迟和总结竞争,可以为处理机配一个高速缓存。然而,高速缓存的使用带来一个新问题--一致性问题。非一致性存储访问系统通过给每个处理机提供本地存储器,以及与可伸缩互连网络的连接解决了可伸缩性的问题,通过互连网络可以访问其他处理机的存储器。存储器访问时间是不一致的,时间误差取决于数据的位置。

互连网络是把并行/分布式系统中的处理机连接在一起的一套通信链路、总线或交换机。在直接连接中,处理机通过一套点到点(point-to-point)链路直接相连。在间接连接中,处理机通过交换机相连。正常情况下,间接相连可以通过适当的交换机信号动态改变。

1.3 系统开发历史

构建一个分布式系统是一种挑战,并能通常它们是一种定制的解决方案。正因为如此,分布式系统开发与现代面向对象编程语言的软件开发有着惊人的相似度。

不过幸运的是,我们使用面向对象语言来开发分布式应用可以大大降低构建分布系统的挑战难度。在这种情况下,面向对象语言成为了解决分布式系统的容器和容器协调器(containers and container orchestrators),面向对象中的对象概念就成了构建分布式系统的集装箱式积木(containerized build-blocks),这些积木就是性能可靠的分布式系统的开发基础,它们完成组件重用的功能,以及非常方便的动态访问的功能。

早期的C/S(Client-Server)模式应用开发把单独的桌面开发,转化成至少两个机器的开发,其中有一个台机器就是应用服务,它独立于客户端应用而存在,这已经是分布式的应用的雏形。比如网络游戏、Web网站应用等。

进入21世纪早期,由于互联网和大数据中心(large-scale datacenters)的兴起,所谓大数据中心它是指,由成千上万台廉价的商业电脑通过互联网互联在一起[1],从而产生了分布系统 (distributed systems) 开发的兴起。它不像C/S架构,分布式系统应用是由运行在多个不同机器上的应用来组成的,或者,一个应用有多个副本同时运行在不同的机器上,这些副本相互交流,从而实现了一个系统,它有点类似web搜索(web-search)和零售商平台[2](retail sales platform)。

因为这个分布式的自然特性,当我们构建正确时,那么这个系统是非常可靠稳定的。也就是说,当我们正确架构一个这样的分布式系统后,那么我们使用这个系统来指导软件开发团队开发出源源不断的组件模型,并且由这些通过这些模型构建成一个系统--即分布式系统!

但是,凡事都有两面性,分布式系统的缺点是设计非常重要,并且也非常复杂,还有就是构建和调试也非常困难。因此,对于一个分布式软件工程师的要求,它要远远高于单机移动应用或者web前端应用工程师。

虽然有这些限制要求,但是在实际在商业体系对“可靠性的分布式系统”[1](reliable distributed systems)却是不断增强的,这些需求包括构建一个分布式系统的工具、分布式设计模式和实践经验。

幸运的是,技术可以帮助我们很容易地实现分布式系统的构建。容器(containers)和容器镜像(container images)和容器协调器(container orchestrators)在近几年开始流行起来,因为它们是构建分布式系统的基础和构建模块,它们包括Docker、Kubernetes和LXC等软件系统技术。而使用容器和容器协调器是基础,我们可以使用这个基础建立一个可重用组件和模型的集合,而这些模式和组件就是我们有效构建自己系统的工具包(toolkit)。

1.3.1 Docker简介

Docker是一个计算机程序,该程序实现操作系统级别的虚拟化实现(Operating-system-level virtualization)。而操作系统级别的虚拟化实现就是容器(Container)的概念。在计算机学科中,容器的概念是独立于用户空间(user space)的,即表示计算机程序直接运行在主机操作系统的核心中,但是它的访问受限于可以使用的资源。

Docker容器于2013年由Docker公司发布第一版。Docker被用来运行软件包,因此它被叫做容器。每个容器根据其绑定的应用、工具、库和配置文件不同而相互独立;容器之间通过预先定义好的通道(channels)进行相互通信。所有容器都运行在单一的操作内核(operating-system kernel)上,因此它相对虚拟机(virtual machines)会更加轻量级。容器是通过镜像(images)来创建的,而镜像指定容器的上下文。而镜像一般是通过公有的资源存储库(repositories),通过组合,或者修改的方式来创建的。

1.3.2 Kubernetes简介

Kubernetes(缩写为k8s)是一个开源的容器协调器系统(open-source container orchestration system),该系统实现自动应用部署功能、伸缩适应功能和管理功能。它原本是由谷歌公司设计,现在已经由云本地化计算基金会(Cloud Native Computing Foundation)组织维护了。它的目标就是提供“给应用容器提供跨主机集群的自动部署、自动伸缩适应和自动操作。[1]”(platform for automating deployment, scaling, and operations of application containers across clusters of hosts)。它一般与容器工具一起使用,比如Docker程序。目前大多数云服务(cloud services)都提供基于Kubernets平台或者指令作为一种服务(PaaS[2]或者IaaS[3])。但是不管哪些Kubernetes都可以被作为平台提供服务而被部署。目前很多服务供应商都拥有自己品牌的Kubernetes分布式系统,并且提供这样的有偿服务。

1.3.3 LXC简介

LXC是Linux Containters的缩写,它与Docker是一个系统级的虚拟方法(operating-system-level virtualization method),该方法用来运行多个独立的Linux系统(容器),而这些独立的Linux系统(容器)是由一个单一的Linux内核所控制的。

1.4 软件开发模式简介

模式、实践和可重用的组件可以重塑系统开发过程,而研究过去的软件开发历史,会对我们理解这些设计模式和可重用组件是非常有帮助的。

1.4.1 算法编程格式化

虽然在Donald Knuth的《The Art of Computer Programming》出版前十几年前人们就已经开始编程了,但是该书在计算机科学中标记了一个重要的篇章。因为它是1962年出版的,时代比较特殊,所以内容包含了算法,但是这些算法不是针对特定的计算机的,而是教授读者算法本身,让这些算法来适应于各种特定的机器架构,或者让读者使用这些算法来解决特定类型的问题。这种格式化的重要性是给算法使用者构建自己的程序时有通用的工具包,同时还可以让程序员学到什么是通用目标概念,并且把这种通用性灵活地运用到各种问题场合。而算法本身是独立特定的实际问题的,因此就算法来说,它相对实际问题域是比较好理解的。

1.4.2 面向对象设计模式

Knuth的书在计算机编程历史是一个重要的里程碑,它说明了在计算机编程开发中,算法表现一个重要组件的概念。但是,随着时代的变迁,程序越来越复杂,代码越来越长,从单个字长的到双字节单元运算,以及可能上千字节的处理,由此出现了原来的过程化编程加算法的编程方式已经不能适应现代编程的需求了。因此,也导致面向对象编程编程语言的出现,这种编程语言强调数据为中心,以及针对每个算法的重用性和扩展性进行了规划与要求。对应于面向对象编程技术,相应出现了设计模式编程实践。

其中,最著名的就是“四人帮”(gang of four[1])的大作《Design Patterns: Elements of reusable object-oriented programming》(设计模式:面向对象编程的可重用性元素—本人翻译,仅供参考)。设计模式针对任务编程给出通用语言和框架解决方案,它描述了一系列基于接口的模式,而这些设计模式可以适应用各种问题的场景处理。面向对象编程好处就是,它导致这些设计模式的特定接口可以被通用的库实现,而这些库是可以被重用的。这样的库可以一次书写,然后被众多开发人员社区中传播使用,从而大大的节约了开发时间,并且提高了可靠性。

1.4.3 开源软件的兴起

免费软件组织开始于上世纪80年代中期,90年代末到21世纪呈现出强大发展期,最终出现日益繁荣的开源软件开发景象。虽然开源软件只针对了分布式系统的设计模式的开发,但是它在开源社区中却是非常重要的,并且开源社区努力把软件开发与分布式系统开发分为两大类型的开源项目。

第二章 单节点容器模式

虽然我们关注讨论分布式系统,也就是讨论应用程序是由许多不同的组件构成,并且这些组件运行不同的机器上。但是,我们需要先研究单个节点的应用模式。讨论这个问题的动机很明确,容器是模式的基本构建块,但是最终构成容器的相关联部件是在单一机器中组成的,它使得容器成为了分布式系统模式的最小构成元素。

2.1 动机

设计分布式系统第一个动机是把我们的分布式应用切割成运行不同机器上的容器的集合。要明白这个道理,那么我们考虑的最终目标是集成化(containerization)概念。而每个容器的创建目标是创建集成化的边界(回顾一下“Enslow的分布式系统模型”,你就会理解了这句话的意思了)。而边界会具体为特定的资源,比如一个应用需要双核8G的内存资源。

根据这个动机,我们会在单一机器上把应用拆分成一组容器。首先是考虑资源独立性,假设我们的应用由两个组件构成:一个是用户直接使用的应用服务器,另外一个是后台配置文件加载服务器。因为最终用户直接请求迟延处理的优先级是最高的,所以用户直接访问服务器需要更多的资源,以确保最终用户的请求得到最快的响应。换句话说,后台配置加载服务器必须提供最有效的服务,即使用户出现高并发访问量有一些延迟的情况,这时的系统也是正常的。也就是说,如果我们希望完全消除后台配置加载服务器对于用户并高发的接收的影响,那么是不可能做到的事情,这只是相对的概念。

资源独立的另外一个原因是,我们需要把单节点应用切割成多个容器。考虑到团队开发伸缩性问题,我们建议6-8人的团队是最理想的配置作来开发分布式应该的单位。当然,根据实际的开发团队情况,我们可以把人数分配小一点。另外一些常用的组件作为重用的模块可以让所有开发团队重用,这样加快开发速度。

最后,如果我们应用比较小,所有容器都是属性一个开发团队的,那么切割应用的好处是便于理解、测试、更新和发布。

第三章 侧车模式

前面讲的单节点模式是侧车模式(the side-car pattern),侧车模式是由两个容器组成的单节点模式。第一个容器是应用容器(application container),该容器包含了应用的核心逻辑,没有这个容器,那么也就不存在该应用了。另外一个容器就叫做侧车容器(sidecar container)了,侧车的角色是辅助应用容器的,一般它不能被应用容器所感知。

最简单的侧车容器使用是,它给一个容器添加功能,否则就不存在辅助的应用容器的说法。侧车容器与应用容器在相同机器上时会通过一个原子容器组(atomic container group)来共同管理,比如Kubernetes中的Pod[1] API。在相同机器上被调试时,应用容器和侧车容器共享一些资源,包括文件系统部分、主机名和网络,以及其它的命名空间。侧车模式的通用镜像(generic image)如下图所示:

image-20210428113028556.png

【侧车模式图--Pod模型】

3.1 侧车模式示例:给传统服务添加HTTPS

传统的web服务是没有加密的,它们使用HTTP协议,而不是HTTPS协议。由于近几年来不断出现公司的网站安全问题,所以几乎所有的商业网站都需要使用HTTPS加密协议,以保障商业网站的安全。早期的商业网站都是使用HTTP协议开发的,现在我们需要更新这些网站的安全性。而这个时候使用侧车模式就可以解决这个问题。

传统web服务使用localhost(127.0.0.1)来配置对外服务,也就是说,只有共享了本地网络才能访问web服务。当然在实际开发中一般不会使用这样策略。然而在侧车模式中,我们使用nginx[1]侧车容器,给传统的容器添加一个辅助容器。Nginx容器传统web应用在相同的网络命名空间中,所以它可以承担访问localhost的功能。别个,nginx服务也可以终止外部IP地址对Pod的访问。参见下面的Pod图

image-20210428113054260.png

【侧车模式应用--给传统web应用添加HTTPS】

3.2 在Kubernetes中动态配置

简化现有应用的代理通讯不仅只有使用侧车模式,另外一个与侧车模式一起使用的是配置同步(configuration synchronization)。大多数应用都是使用配置文件实现参数化应用编程,这些配置类型有纯文本文件、XML文件、JSON文件和YAML文件。

对于大多数使用这些配置文件方式创建的应用,如果在应该存在于云的本地环境中,那么我们使用API来更新配置就非常有用的。这种方式可以使用API动态添加配置信息,而不是手动日志方式记录每个服务器,以使用及时命令来更新配置文件。这种动态API不仅使用简单,并且还具有自动回滚功能,从而实现安全的重置配置的效果。参见下面的示意图

image-20210428113116095.png

【动态配置实现结构】

第四章 大使模式

本章介绍分布式系统设计时使用的大使模式(ambassador pattern),我们又叫做大使容器,它作为应用容器与外部服务的中间人角色来使用。参见下面的示意图

image-20210428113137780.png

【大使模式】

大使模式有两个单节点模式容器,大使容器可以很容易被不同的应用容器重用,这个重用可以加快应用开发的开发速度。

当我们在单一机器上处理数据存储时,该存储层会变得非常巨大。在这种情况下,我们需要共享存储层。共享时我们需要把存储层拆分成不同片断,并且保存不同的机器上。共享和共享服务设计模式会在后面讨论。参看下图的共享服务模式:

image-20210428113155258.png

第五章 适配器模式

本章讲分布式中的适配器模式(Adapter):适配器容器用来修改应用容器的接口,确保适配所有预定的应用接口需求。比如,适配器可以让一个应用实现持续监控的需求—确保日志文件总是可以非常文件的输出到stdout(标准输出对象)中。

image-20210428113210122.png

【分布式设计适配器模式】

适配器常监控MySQL数据库状态,也就是给MySQL容器添加一个适配器容器。

第六章 共享服务模式

共享服务模式有两种方式:副本服务和共享服务。参见下面的示意图:

image-20210428113225146.png

【副本服务模型】

image-20210428113234397.png

【共享服务模型】

下面我们使用共享服务模式处理缓存问题,参见下面使用案例

image-20210428113245396.png

【共享服务实现缓存模型】

使用共享缓存的原因是,假如我们有10G的RAM存储器可以使用,而每个缓存可以伺服能力是100 RPS(即每秒可以缓存100请求),假设我们设计提供200G的RAM可以使用,并且设计处理1000 RPS的请求能力,那么我们只需要缓存副本就可以满足1000 RPS的并发需求,这种方式只能保存 5%(10G ÷ 200G)总体请求数据。因为每个缓存副本是独立的,所以每个缓存中必须保存相同的数据。这种副本式设计的结果是数据大量冗余,并且非常受限单个副本缓存的最大内存限制。

但是,如果我们使用共享服务方式,即把共享缓存部署成10路方式实现,我们同样可以伺服1000 RPS外部并发请求,但是每个缓存可以保存不同的数据,这种方式可以保存50%(10 × 10G ÷ 200G)的总体数据。

第七章 函数和事件驱动处理

该设计模式主要是用来解决大多数应用的重加载问题--在内存中保存大量的数据,以及使用后台处理一些请求的问题。针对出现加载大量数据,但是只有少量请求与响应的场景,我们让云服务提供者部署“函数服务”(function as service--FaaS)产品来解决这个问题。函数服务在整个分布式架构中只能使用一个组件来使用,而不是一个完整的解决方案,我们把FaaS又叫做无服务计算(“serverless” computing)。

针对FaaS处理少数请求的情况,我们使用事件驱动响应策略来处理函数的调用。

总结

像这种高级概念设计,都是属于玄学的东西。大家不用想着一看就会,一学就懂的想法。随缘就好了~

最后

记得给大黍❤️关注+点赞+收藏+评论+转发❤️

作者:老九学堂—技术大黍

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值