服务发现的可行方案以及实践案例 & 微服务架构的优势与不足

Chris Richardson,是世界著名的软件大师,经典技术著作《POJOS IN ACTION》一书的作者,也是 cloudfoundry.com 最初的创始人,Chris Richardson 与 Martin Fowler、Sam Newman、Adrian Cockcroft 等并称为世界十大软件架构师。


Chris Richardson 微服务系列全 7 篇:

1. 微服务架构的优势与不足

2. 使用 API 网关构建微服务

3. 微服务架构中的进程间通信

4. 服务发现的可行方案以及实践案例

5. 微服务的事件驱动数据管理

6. 选择微服务部署策略

7. 将单体应用改造为微服务

Chris Richardson 所著所有文章已独家授权 DaoCloud 翻译并刊载。

本期内容:

为什么要使用服务发现?

假设我们写的代码会调用 REST API 或者 Thrift API 的服务。为了完成一次请求,代码需要知道服务实例的网络位置(IP 地址和端口)。运行在物理硬件上的传统应用中,服务实例的网络位置是相对固定的;代码能从一个偶尔更新的配置文件中读取网络位置。

对于基于云端的、现代化的微服务应用而言,这却是一大难题,正如下图所示。

Richardson-microservices-part4-1_difficult-service-discovery

服务实例的网络位置都是动态分配的。由于扩展、失败和升级,服务实例会经常动态改变,因此,客户端代码需要使用更加复杂的服务发现机制。

服务发现有两大模式:客户端发现模式和服务端发现模式。我们先来了解客客户端发现模式。

客户端发现模式

使用客户端发现模式时,客户端决定相应服务实例的网络位置,并且对请求实现负载均衡。客户端查询服务注册表,后者是一个可用服务实例的数据库;然后使用负载均衡算法从中选择一个实例,并发出请求。

客户端从服务注册服务中查询,其中是所有可用服务实例的库。客户端使用负载均衡算法从多个服务实例中选择出一个,然后发出请求。

下图显示了这种模式的架构:

Richardson-microservices-part4-2_client-side-pattern

服务实例的网络位置在启动时被记录到服务注册表,等实例终止时被删除。服务实例的注册信息通常使用心跳机制来定期刷新。

Netflix OSS 是客户端发现模式的绝佳范例。Netflix Eureka 是一个服务注册表,为服务实例注册管理和查询可用实例提供了 REST API 接口。Netflix Ribbon 是 IPC 客户端,与 Eureka 一起实现对请求的负载均衡。我们会在后面深入讨论 Eureka。

客户端发现模式优缺点兼有。这一模式相对直接,除了服务注册外,其它部分无需变动。此外,由于客户端知晓可用的服务实例,能针对特定应用实现智能负载均衡,比如使用哈希一致性。这种模式的一大缺点就是客户端与服务注册绑定,要针对服务端用到的每个编程语言和框架,实现客户端的服务发现逻辑。

分析过客户端发现后,我们来了解服务端发现。

服务端发现模式

另外一种服务发现的模式是服务端发现模式,下图展现了这种模式的架构:

Richardson-microservices-part4-3_server-side-pattern

客户端通过负载均衡器向某个服务提出请求,负载均衡器查询服务注册表,并将请求转发到可用的服务实例。如同客户端发现,服务实例在服务注册表中注册或注销。

AWS Elastic Load Balancer(ELB)是服务端发现路由的例子,ELB 通常均衡来自互联网的外部流量,也可用来负载均衡 VPC(Virtual private cloud)的内部流量。客户端使用 DNS 通过 ELB 发出请求(HTTP 或 TCP),ELB 在已注册的 EC2 实例或 ECS 容器之间负载均衡。这里并没有单独的服务注册表,相反,EC2 实例和 ECS 容器注册在 ELB。

HTTP 服务器与类似 NGINX PLUS 和 NGINX 这样的负载均衡起也能用作服务端的发现均衡器。Graham Jenson 的Scalable Architecture DR CoN: Docker, Registrator, Consul, Consul Template and Nginx 一文就描述如何使用 Consul Template 来动态配置 NGINX 反向代理。Consul Template 定期从 Consul Template 注册表中的配置数据中生成配置文件;文件发生更改即运行任意命令。在这篇文章中,Consul Template 生成 nginx.conf 文件,用于配置反向代理,然后运行命令,告诉 NGINX 重新加载配置文件。在更复杂的实现中,需要使用 HTTP API 或 DNS 来动态配置 NGINX Plus。

Kubernetes 和 Marathon 这样的部署环境会在每个集群上运行一个代理,将代理用作服务端发现的负载均衡器。客户端使用主机 IP 地址和分配的端口通过代理将请求路由出去,向服务发送请求。代理将请求透明地转发到集群中可用的服务实例。

服务端发现模式兼具优缺点。它最大的优点是客户端无需关注发现的细节,只需要简单地向负载均衡器发送请求,这减少了编程语言框架需要完成的发现逻辑。并且如上文所述,某些部署环境免费提供这一功能。这种模式也有缺点。除非负载均衡器由部署环境提供,否则会成为一个需要配置和管理的高可用系统组件。

服务注册表

服务注册表是服务发现的核心部分,是包含服务实例的网络地址的数据库。服务注册表需要高可用而且随时更新。客户端能够缓存从服务注册表中获取的网络地址,然而,这些信息最终会过时,客户端也就无法发现服务实例。因此,服务注册表会包含若干服务端,使用复制协议保持一致性。

如前所述,Netflix Eureka 是服务注册表的上好案例,为注册和请求服务实例提供了 REST API。服务实例使用 POST 请求来注册网络地址,每三十秒使用 PUT 请求来刷新注册信息。注册信息也能通过 HTTP DELETE 请求或者实例超时来被移除。以此类推,客户端能够使用 HTTP GET 请求来检索已注册的服务实例。

Netflix 通过在每个 AWS EC2 域运行一个或者多个 Eureka 服务实现高可用性。每个 Eureka 服务器都运行在拥有弹性 IP 地址的 EC2 实例上。DNS TEXT 记录被用来保存 Eureka 集群配置,后者包括可用域和 Eureka 服务器的网络地址列表。Eureka 服务在启动时会查询 DNS 去获取 Eureka 集群配置,确定同伴位置,以及给自己分配一个未被使用的弹性 IP 地址。

Eureka 客户端,包括服务和服务客户端,查询 DNS 去发现 Eureka 服务的网络地址。客户端首选同一域内的 Eureka 服务。然而,如果没有可用服务,客户端会使用其它可用域中的 Eureka 服务。

其它的服务注册表包括:

  • etcd – 高可用、分布式、一致性的键值存储,用于共享配置和服务发现。Kubernetes 和 Cloud Foundry 是两个使用 etcd 的著名项目。
  • consul – 发现和配置的服务,提供 API 实现客户端注册和发现服务。Consul 通过健康检查来判断服务的可用性。
  • Apache ZooKeeper – 被分布式应用广泛使用的高性能协调服务。Apache ZooKeeper 最初是 Hadoop 的子项目,现在已成为顶级项目。

此外,如前所强调,像 Kubernetes、Marathon 和 AWS 并没有明确的服务注册,相反,服务注册已经内置在基础设施中。

了解了服务注册的概念后,现在了解服务实例如何在注册表中注册。

服务注册的方式

如前所述,服务实例必须在注册表中注册和注销。注册和注销有两种不同的方法。方法一是服务实例自己注册,也叫自注册模式(self-registration pattern);另一种是采用管理服务实例注册的其它系统组件,即第三方注册模式。

自注册方式

当使用自注册模式时,服务实例负责在服务注册表中注册和注销。另外,如果需要的话,一个服务实例也要发送心跳来保证注册信息不会过时。下图描述了这种架构:

Richardson-microservices-part4-4_self-registration-pattern

Netflix OSS Eureka 客户端是非常好的案例,它负责处理服务实例的注册和注销。Spring Cloud 能够执行包括服务发现在内的各种模式,使得利用 Eureka 自动注册服务实例更简单,只需要给 Java 配置类注释 @EnableEurekaClient。

自注册模式优缺点兼备。它相对简单,无需其它系统组件。然而,它的主要缺点是把服务实例和服务注册表耦合,必须在每个编程语言和框架内实现注册代码。

另一个方案将服务与服务注册表解耦合,被称作第三方注册模式。

第三方注册模式

使用第三方注册模式,服务实例则不需要向服务注册表注册;相反,被称为服务注册器的另一个系统模块会处理。服务注册器会通过查询部署环境或订阅事件的方式来跟踪运行实例的更改。一旦侦测到有新的可用服务实例,会向注册表注册此服务。服务管理器也负责注销终止的服务实例。下面是这种模式的架构图。

Richardson-microservices-part4-5_third-party-pattern

Registrator 是一个开源的服务注册项目,它能够自动注册和注销被部署为 Docker 容器的服务实例。Registrator 支持包括 etcd 和 Consul 在内的多种服务注册表。

NetflixOSS Prana 是另一个服务注册器,主要面向非 JVM 语言开发的服务,是一款与服务实例一起运行的并行应用。Prana 使用 Netflix Eureka 来注册和注销服务实例。

服务注册器是部署环境的内置组件。由 Autoscaling Group 创建的 EC2 实例能够自动向 ELB 注册。Kubernetes 服务自动注册并能够被发现。

第三方注册模式也是优缺点兼具。在第三方注册模式中,服务与服务注册表解耦合,无需为每个编程语言和框架实现服务注册逻辑;相反,服务实例通过一个专有服务以中心化的方式进行管理。它的不足之处在于,除非该服务内置于部署环境,否则需要配置和管理一个高可用的系统组件。

总结

在微服务应用中,服务实例的运行环境会动态变化,实例网络地址也是如此。因此,客户端为了访问服务必须使用服务发现机制。

服务注册表是服务发现的关键部分。服务注册表是可用服务实例的数据库,提供管理 API 和查询 API。服务实例使用管理 API 来实现注册和注销,系统组件使用查询 API 来发现可用的服务实例。

服务发现有两种主要模式:客户端发现和服务端发现。在使用客户端服务发现的系统中,客户端查询服务注册表,选择可用的服务实例,然后发出请求。在使用服务端发现的系统中,客户端通过路由转发请求,路由器查询服务注册表并转发请求到可用的实例。

服务实例的注册和注销也有两种方式。一种是服务实例自己注册到服务注册表中,即自注册模式;另一种则是由其它系统组件处理注册和注销,也就是第三方注册模式。

在一些部署环境中,需要使用 Netflix Eureka、etcd、Apache Zookeeper 等服务发现来设置自己的服务发现基础设施。而另一些部署环境则内置了服务发现。例如,Kubernetes 和 Marathon 处理服务实例的注册和注销,它们也在每个集群主机上运行代理,这个代理具有服务端发现路由的功能。

HTTP 反向代理和 NGINX 这样的负载均衡器能够用做服务器端的服务发现均衡器。服务注册表能够将路由信息推送到 NGINX,激活配置更新,譬如使用 Cosul Template。NGINX Plus 支持额外的动态配置机制,能够通过 DNS 从注册表中获取服务实例的信息,并为远程配置提供 API。

英文原文:https://www.nginx.com/blog/service-discovery-in-a-microservices-architecture/



英文原文:Introduction to Microservices

  这篇文章作者是Chris Richardson,他是早期基于Java的Amazonite EC2 PaaS平台CloudFoundry.com的创始人。现在他为企业提供如何开发和部署应用的咨询服务。他也经常在http://microservices.io上发表有关微服务的文章。

  微服务正在博客、社交媒体讨论组和会议演讲中获得越来越多的关注,在Gartner的2014 Hype Cycle上它的排名非常靠前。同时,软件社区中也有不少持怀疑论者,认为微服务不是什么新东西。Naysayers认为这就是SOA架构的重新包装。然而,尽管存在着不同的争论,微服务架构模式却正在为敏捷部署以及复杂企业应用实施提供巨大的帮助。

  这篇博客是关于如何设计、开发和部署微服务的七篇系列文章中的第一篇。读者将会从中学到方法,并且和单体式架构模式(译者注:本文中会将 Monolithic翻译为单体)进行对比。这一系列文章将描述微服务架构中不同元素。你将了解到微服务架构模式的优缺点,以便决定是否更好的将微服务架构应用到自己的项目中,以及如何应用这一模式。

  首先我们看看为什么要考虑使用微服务。

  开发单体式应用

  假设你正准备开发一款与Uber和Hailo竞争的出租车调度软件,经过初步会议和需求分析,你可能会手动或者使用基于Rails、Spring Boot、Play或者Maven的生成器开始这个新项目,它的六边形架构是模块化的 ,架构图如下:

1.png
  应用核心是业务逻辑,由定义服务、域对象和事件的模块完成。围绕着核心的是与外界打交道的适配器。适配器包括数据库访问组件、生产和处理消息的消息组件,以及提供API或者UI访问支持的web模块等。

  尽管也是模块化逻辑,但是最终它还是会打包并部署为单体式应用。具体的格式依赖于应用语言和框架。例如,许多Java应用会被打包为WAR格式,部署在Tomcat或者Jetty上,而另外一些Java应用会被打包成自包含的JAR格式,同样,Rails和Node.js会被打包成层级目录。

  这种应用开发风格很常见,因为IDE和其它工具都擅长开发一个简单应用,这类应用也很易于调试,只需要简单运行此应用,用Selenium链接UI就可以完成端到端测试。单体式应用也易于部署,只需要把打包应用拷贝到服务器端,通过在负载均衡器后端运行多个拷贝就可以轻松实现应用扩展。在早期这类应用运行的很好。

  单体式应用的不足

  不幸的是,这种简单方法却有很大的局限性。一个简单的应用会随着时间推移逐渐变大。在每次的sprint中,开发团队都会面对新“故事”,然后开发许多新代码。几年后,这个小而简单的应用会变成了一个巨大的怪物。这儿有一个例子,我最近和一个开发者讨论,他正在写一个工具,用来分析他们一个拥有数百万行代码的应用中JAR文件之间的依赖关系。我很确信这个代码正是很多开发者经过多年努力开发出来的一个怪物。

  一旦你的应用变成一个又大又复杂的怪物,那开发团队肯定很痛苦。敏捷开发和部署举步维艰,其中最主要问题就是这个应用太复杂,以至于任何单个开发者都不可能搞懂它。因此,修正bug和正确的添加新功能变的非常困难,并且很耗时。另外,团队士气也会走下坡路。如果代码难于理解,就不可能被正确的修改。最终会走向巨大的、不可理解的泥潭。

  单体式应用也会降低开发速度。应用越大,启动时间会越长。比如,最近的一个调查表明,有时候应用的启动时间居然超过了12分钟。我还听说某些应用需要40分钟启动时间。如果开发者需要经常重启应用,那么大部分时间就要在等待中渡过,生产效率受到极大影响。

  另外,复杂而巨大的单体式应用也不利于持续性开发。今天,SaaS应用常态就是每天会改变很多次,而这对于单体式应用模式非常困难。另外,这种变化带来的影响并没有很好的被理解,所以不得不做很多手工测试。那么接下来,持续部署也会很艰难。

  单体式应用在不同模块发生资源冲突时,扩展将会非常困难。比如,一个模块完成一个CPU敏感逻辑,应该部署在AWS EC2 Compute Optimized instances,而另外一个内存数据库模块更合适于EC2 Memory-optimized instances。然而,由于这些模块部署在一起,因此不得不在硬件选择上做一个妥协。

  单体式应用另外一个问题是可靠性。因为所有模块都运行在一个进程中,任何一个模块中的一个bug,比如内存泄露,将会有可能弄垮整个进程。除此之外,因为所有应用实例都是唯一的,这个bug将会影响到整个应用的可靠性。

  最后,单体式应用使得采用新架构和语言非常困难。比如,设想你有两百万行采用XYZ框架写的代码。如果想改成ABC框架,无论是时间还是成本都是非常昂贵的,即使ABC框架更好。因此,这是一个无法逾越的鸿沟。你不得不在最初选择面前低头。

  总结一下:一开始你有一个很成功的关键业务应用,后来就变成了一个巨大的,无法理解的怪物。因为采用过时的,效率低的技术,使得雇佣有潜力的开发者很困难。应用无法扩展,可靠性很低,最终,敏捷性开发和部署变的无法完成。

  那么如何应对呢?

  微处理架构——处理复杂事物

  许多公司,比如Amazon、eBay和NetFlix,通过采用微处理结构模式解决了上述问题。其思路不是开发一个巨大的单体式的应用,而是将应用分解为小的、互相连接的微服务。

  一个微服务一般完成某个特定的功能,比如下单管理、客户管理等等。每一个微服务都是微型六角形应用,都有自己的业务逻辑和适配器。一些微服务还会发布API给其它微服务和应用客户端使用。其它微服务完成一个Web UI,运行时,每一个实例可能是一个云VM或者是Docker容器。

  比如,一个前面描述系统可能的分解如下:

2.png
  每一个应用功能区都使用微服务完成,另外,Web应用会被拆分成一系列简单的Web应用(比如一个对乘客,一个对出租车驾驶员)。这样的拆分对于不同用户、设备和特殊应用场景部署都更容易。

  每一个后台服务开放一个REST API,许多服务本身也采用了其它服务提供的API。比如,驾驶员管理使用了告知驾驶员一个潜在需求的通知服务。UI服务激活其它服务来更新Web页面。所有服务都是采用异步的,基于消息的通讯。微服务内部机制将会在后续系列中讨论。

  一些REST API也对乘客和驾驶员采用的移动应用开放。这些应用并不直接访问后台服务,而是通过API Gateway来传递中间消息。API Gateway负责负载均衡、缓存、访问控制、API 计费监控等等任务,可以通过NGINX方便实现,后续文章将会介绍到API Gateway。

3.png
  ·微服务架构模式在上图中对应于代表可扩展Scale Cube的Y轴,这是一个在《The Art of Scalability》书中描述过的三维扩展模型。另外两个可扩展轴,X轴由负载均衡器后端运行的多个应用副本组成,Z轴是将需求路由到相关服务。

  应用基本可以用以上三个维度来表示,Y轴代表将应用分解为微服务。运行时,X轴代表运行多个隐藏在负载均衡器之后的实例,提供吞吐能力。一些应用可能还是用Z轴将服务分区。下面的图演示行程管理服务如何部署在运行于AWS EC2上的Docker上。

4.png
  运行时,行程管理服务由多个服务实例构成。每一个服务实例都是一个Docker容器。为了保证高可用,这些容器一般都运行在多个云VM上。服务实例前是一层诸如NGINX的负载均衡器,他们负责在各个实例间分发请求。负载均衡器也同时处理其它请求,例如缓存、权限控制、API统计和监控。

  这种微服务架构模式深刻影响了应用和数据库之间的关系,不像传统多个服务共享一个数据库,微服务架构每个服务都有自己的数据库。另外,这种思路也影响到了企业级数据模式。同时,这种模式意味着多份数据,但是,如果你想获得微服务带来的好处,每个服务独有一个数据库是必须的,因为这种架构需要这种松耦合。下面的图演示示例应用数据库架构。

5.png
  每种服务都有自己的数据库,另外,每种服务可以用更适合自己的数据库类型,也被称作多语言一致性架构。比如,驾驶员管理(发现哪个驾驶员更靠近乘客),必须使用支持地理信息查询的数据库。

  表面上看来,微服务架构模式有点像SOA,他们都由多个服务构成。但是,可以从另外一个角度看此问题,微服务架构模式是一个不包含Web服务(WS-)和ESB服务的SOA。微服务应用乐于采用简单轻量级协议,比如REST,而不是WS-,在微服务内部避免使用ESB以及ESB类似功能。微服务架构模式也拒绝使用canonical schema等SOA概念。

  微服务架构的好处

  微服务架构模式有很多好处。首先,通过分解巨大单体式应用为多个服务方法解决了复杂性问题。在功能不变的情况下,应用被分解为多个可管理的分支或服务。每个服务都有一个用RPC-或者消息驱动API定义清楚的边界。微服务架构模式给采用单体式编码方式很难实现的功能提供了模块化的解决方案,由此,单个服务很容易开发、理解和维护。

  第二,这种架构使得每个服务都可以有专门开发团队来开发。开发者可以自由选择开发技术,提供API服务。当然,许多公司试图避免混乱,只提供某些技术选择。然后,这种自由意味着开发者不需要被迫使用某项目开始时采用的过时技术,他们可以选择现在的技术。甚至于,因为服务都是相对简单,即使用现在技术重写以前代码也不是很困难的事情。

  第三,微服务架构模式是每个微服务独立的部署。开发者不再需要协调其它服务部署对本服务的影响。这种改变可以加快部署速度。UI团队可以采用AB测试,快速的部署变化。微服务架构模式使得持续化部署成为可能。

  最后,微服务架构模式使得每个服务独立扩展。你可以根据每个服务的规模来部署满足需求的规模。甚至于,你可以使用更适合于服务资源需求的硬件。比如,你可以在EC2 Compute Optimized instances上部署CPU敏感的服务,而在EC2 memory-optimized instances上部署内存数据库。

  微服务架构的不足

  Fred Brooks在30年前写道,“there are no silver bullets”,像任何其它科技一样,微服务架构也有不足。其中一个跟他的名字类似,『微服务』强调了服务大小,实际上,有一些开发者鼓吹建立稍微大一些的,10-100 LOC服务组。尽管小服务更乐于被采用,但是不要忘了这只是终端的选择而不是最终的目的。微服务的目的是有效的拆分应用,实现敏捷开发和部署。

  另外一个主要的不足是,微服务应用是分布式系统,由此会带来固有的复杂性。开发者需要在RPC或者消息传递之间选择并完成进程间通讯机制。更甚于,他们必须写代码来处理消息传递中速度过慢或者不可用等局部失效问题。当然这并不是什么难事,但相对于单体式应用中通过语言层级的方法或者进程调用,微服务下这种技术显得更复杂一些。

  另外一个关于微服务的挑战来自于分区的数据库架构。商业交易中同时给多个业务分主体更新消息很普遍。这种交易对于单体式应用来说很容易,因为只有一个数据库。在微服务架构应用中,需要更新不同服务所使用的不同的数据库。使用分布式交易并不一定是好的选择,不仅仅是因为CAP理论,还因为今天高扩展性的NoSQL数据库和消息传递中间件并不支持这一需求。最终你不得不使用一个最终一致性的方法,从而对开发者提出了更高的要求和挑战。

  测试一个基于微服务架构的应用也是很复杂的任务。比如,采用流行的Spring Boot架构,对一个单体式web应用,测试它的REST API,是很容易的事情。反过来,同样的服务测试需要启动和它有关的所有服务(至少需要这些服务的stubs)。再重申一次,不能低估了采用微服务架构带来的复杂性。

  另外一个挑战在于,微服务架构模式应用的改变将会波及多个服务。比如,假设你在完成一个案例,需要修改服务A、B、C,而A依赖B,B依赖C。在单体式应用中,你只需要改变相关模块,整合变化,部署就好了。对比之下,微服务架构模式就需要考虑相关改变对不同服务的影响。比如,你需要更新服务C,然后是B,最后才是A,幸运的是,许多改变一般只影响一个服务,而需要协调多服务的改变很少。

  部署一个微服务应用也很复杂,一个分布式应用只需要简单在复杂均衡器后面部署各自的服务器就好了。每个应用实例是需要配置诸如数据库和消息中间件等基础服务。相对比,一个微服务应用一般由大批服务构成。例如,根据Adrian Cockcroft,Hailo有160个不同服务构成,NetFlix有大约600个服务。每个服务都有多个实例。这就造成许多需要配置、部署、扩展和监控的部分,除此之外,你还需要完成一个服务发现机制(后续文章中发表),以用来发现与它通讯服务的地址(包括服务器地址和端口)。传统的解决问题办法不能用于解决这么复杂的问题。接续而来,成功部署一个微服务应用需要开发者有足够的控制部署方法,并高度自动化。

  一种自动化方法是使用PaaS服务,例如Cloud Foundry。PaaS给开发者提供一个部署和管理微服务的简单方法,它把所有这些问题都打包内置解决了。同时,配置PaaS的系统和网络专家可以采用最佳实践和策略来简化这些问题。另外一个自动部署微服务应用的方法是开发对于你来说最基础的PaaS系统。一个典型的开始点是使用一个集群化方案,比如配合Docker使用Mesos或者Kubernetes。后面的系列我们会看看如何基于软件部署方法例如NGINX,可以方便的在微服务层面提供缓存、权限控制、API统计和监控。

  总结

  构建复杂的应用真的是非常困难。单体式的架构更适合轻量级的简单应用。如果你用它来开发复杂应用,那真的会很糟糕。微服务架构模式可以用来构建复杂应用,当然,这种架构模型也有自己的缺点和挑战。

  在后续的博客中,我会深入探索微服务架构模式,并讨论诸如服务发现、服务部署选择和如何分解一个分布式应用为多个服务的策略。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值