三、OpenStack入门 之 各组件解析

OpenStack入门 之 各组件解析

写在前面

学习目标:

  • 掌握 OpenStack 的各组件的架构和功能

本次笔记的内容有:

  • Nova 组件解析
  • Swift 组件解析
  • Cinder 组件解析
  • Neutron 组件解析
  • Horizon 组件解析
  • Glance 组件解析
  • Keystone 组件解析

是常用的 7 个组件:

  • 负责虚拟机创建、管理和销毁、提供计算资源服务的 Nova;
  • 提供对象存储服务的分布式存储 Swift;
  • 提供块存储服务的 Cinder;
  • 提供虚拟机镜像管理和存储服务的 Glance;
  • 软件定义网络项目 Neutron;
  • 提供身份认证和授权的 Keystone;
  • 提供基于 Web 的一个 GUI 的 Horizon;

OpenStack 每半年进行一次集成发布,版本号从 A 开始,按字母表顺序逐步提升,每个集成发布版本都包括若干个项目,A 版本是 Nova 和 Swift。

1. Nova组件解析

通过 Nova 可以实现:

  • 访问虚拟机
  • 创建和启动虚拟机

Nova 是怎么运作的?除了图形界面和命令行之外,我们还需要怎样去配置和使用 Nova?

1-1. Nova 的架构

Nova-console 和 Nova-consoleauth 这两个组件,主要是为了让用户能够通过 VNC 客户端或 SPICE 客户端来访问虚拟机的界面。推荐使用 SPICE 客户端,因为SPICE 客户端提供了很多高级的特性,比方说,对 USB 设备的支持,SPICE 还支持多屏显示等等功能,这些特性在桌面虚拟化环境中非常有用处,在桌面云环境中也非常有用,但使用 SPICE 就需要对 nova.conf 文件做一些修改,比如:

// nova.conf的修改:
[spice]
enabled = True
[default]
vnc_enabled = False

1-2. 虚拟机的调度机制

  • 第一个方面:placement(放置)。
    把虚拟机放在哪个物理机上启动
  • 第二个方面:migration(迁移)。
    从哪个物理机迁移到哪个物理机上

Nova 有一个组件叫作调度器,Scheduler 直译过来就是调度器,它实际上只完成了 placement 的工作,migration 实际上是由其它组件来协同完成的。

在 OpenStack 的上下文里,特指这个运行的 nova-compute 服务的机器才叫做宿主机。

1-3. Nova 对虚拟机的调度机制

通过修改 nova.conf 文件修改调度器的配置,nova 默认的这个调度器成为 filter scheduler,这种调度器分两步来决定虚拟机的位置。第一步先经过一些 filters,filters 的每一种都代表着一种限制条件,通过这些 filters 选出来一些可用的 host,作为第二步的输入;第二步是称重,实际上是对宿主机进行排序。

下面举个例子进行说明,这里有一个非常常用的 filter 叫作 Ramfilter,Ram:设置超配比例

[ Nova 对虚拟机的调度机制:]

(1)把所有的 filters 都用上:

scheduler_available_filters = nova.scheduler.filters.all_filters

(2)选择其中的一部分:

scheduler_default_filter = RetryFilter, AvailabilityZoneFilter, RamFilter, ComputeFilter,ComputeCapabilitiesFilter, ImagePropertiesFilter, ServerGroupAntiAffinityFilter

(3)用 Python 实现自己的 filter,老师用 RamFilter 做的例子如图所示:

对虚拟机进行排序(称重 weighting)

两种选择:

  • 希望负载尽可能均衡
  • 希望负载尽可能集中,用尽可能少的 host 来承载我们的虚拟机,我们使用的 host 就会比较少,那么运维的成本包括用电、制冷的费用都会比较低

2. Swift组件解析

[ Swift 的特点:]

  1. Swift 历史比较长
  2. Swift 比较独立(和其他组件的耦合度比较低;不依赖于第三方的软件或设备,甚至不依赖于 Keystone 做用户的认证和授权;)
  3. Swift 是做对象存储的(这里的对象存储就是指,采用 RESTful 接口,支持直接从互联网访问存储系统进行数据的读写。对象存储往往采用扁平的数据组织形式,在文件数量很多的情况下不至于出现明显的性能衰减,也就是我们说对象存储的概念哦

OpenStack 其它的很多项目实现具体功能的时候往往需要依赖于第三方的软件或设备,比如说 Nova 启动虚拟机、管理虚拟机就必须通过一个第三方的 Hypervisor,还有 Cinder 提供块存储服务必须通过第三方的存储设备。Swift 就不同了,Swift 可以从上面的 RESTful 接口把堆栈一直管到底,以至于管到最后把数据到底放在哪个服务器的哪个硬盘上都可以做到,整个是完整的存储系统,现在有专门基于 Swift 实现的存储系统例如 swiftstack

[ Swift 数据的组织形式:]

扁平的组织形式,Swift 的数据组织形式分为三级,只有这三级

  • Account
  • Container
  • Object

与可以分 N 多层能够不断有文件夹的创建的数据组织形式不一样,Swift 的整个存储结构从逻辑上划分为一个个的 Account,然后每个 Account 底下再分为一个个的 Container,每个 Object 实际上是放在每个 Container 底下的。

[ Swift 的接口:]

与 OpenStack 的其它组件的接口很类似,也是 RESTful Web API,总共有四个部分组成。第一个是 HTTP 操作(GET/PUT/DELETE/POST);第二个是认证信息(主要是身份信息);第三个是指示资源地址的 URL(具体到 Swift,应该包含存储系统的域名/地址加上存储数据的 Account、Container、Object 的整个信息的完整的 URL);第四个是数据与元数据。

例子:

crul -i -X GET 
https://storage.clouddrive.com/v1/my_account?format=json\-H 
"X-Auth-User:jdoe" -H "X-Auth-Key:jdoepassword"

storage.clouddrive.com 是对象存储的域名或者说地址,接着取 my_account 下面的 Container 的信息,后面带的是用户的认证信息 username 和 password 。先看下方返回结果:

根据上图我可以发现,API 调用以后返回的结果 Response 主要分为两段:1.头部。有两个要点,一个是它给出了这个 Account 下面 Container 的数量是 2,然后这个 Account 里面总共用了多大的存储空间,用了多少的字节或者说存了多少字节的数据进去,这里是 14。接着看下面的内容:

根据上图我可以发现,这是一个 json 的返回形式,把每一个 Container 都列出来了,可以看到第一个 Container 里面没有 Object,所以它占用的字节数也是 0。

[ Swift 的软件架构:]

如图所示:

从上面下来是 Swift API,通过 Proxy Server 来实现,Proxy Server 左右两边分别连了两个东西:一个是做身份认证用的,往往是 Keystone,右边是一个 Cache 服务,这个 Cache 不去缓存实际的 Object 的数据,缓存的主要是用户的身份信息,别的会缓存一点其他的东西,不会去缓存数据,所以 Swift 是没有缓存的,原生设计就是没有缓存的;从 Proxy Server 下来会有三个 ring:Object Ring,Container Ring 和 Account Ring,ring 是一致性 HASH 实现。(一致性 HASH 是什么? 分布式存储系统中为了把数据分布到各个节点上,用的一种数据结构就是一致性 HASH,后续会详细展开探讨。)下面是实际提供存储服务的进程或者说 Server,有 Object Server,Container Server 和 Account Server,有一个统一的称呼:Storage Server,对这些信息的默认的存储方式都是三副本。

[ 搭建 Swift 时需要的注意事项:]

Swift 的部署需要注意哪些?一个典型的 Swift 部署像图里画的样子:

图所示的是一个小型的 Swift 集群典型配置,想让 Swift 进行良好的工作,需要注意一些容易被人去忽视和经常犯错的地方,避免对 Swift 产生一些错误的认识,误认为 Swift 可靠性不行或性能不行,注意下面五个点:

  1. 采用的是全对等架构,就是每个节点每个 Server 都是一样的,有 Proxy Server,有 Object Server, 有 Container Server 等等都是一样的。(实际上 Swift 从软件到部署都是一个全对等的概念在里面,是 Swift 架构的最突出的特点之一,让 Swift 能够很好的去做横向扩展,没有单点故障)。
  2. 怎么把服务器组织起来呢?实际上,上面它有一个 Load Banlancer,能够把服务器(实际上是 Proxy Server)组成一个集群,Load Banlancer 能够把用户的这些请求给它分配到各个实际的服务节点上。
  3. 每个服务器是一个zone Swift 是要求分 zone 的,要把服务器存储区域给隔离开,然后把这些副本放到不同的 zone 里面去,所以这地方小规模集群一般都是让每个 Server 作为一个 zone。
  4. 作为小规模的部署,服务器的数量最少是5个(官方推荐的最小集群,达到比较好的效果)。
  5. 不要用虚拟机(存数据的地方都不可能说是用虚拟化去做支撑的),不要用 RAID(用服务器用盘阵的时候经常会对它做 RAID,但是做完 RAID 以后会对我们的 Swift 带来一些性能上不可预知的结果,Swift 本身具有数据管理的机制)。

[ Swift2.0的新功能:]

存储策略:让用户能够自由的选择数据的存储策略

Swift 的存储策略一部分由搭建存储系统的 Deployer 决定,另外一部分是由使用存储系统的 Client 决定的,用户在这个里面以 Container 为力度,可以去指定这个存储策略。

3. Cinder组件解析

传统数据中心的存储设备经常听到 SAN 或者盘阵这些词汇,是通过网络连接给服务器提供存储空间的一类设备。硬盘对应了一个术语:块设备,传统存储也叫块存储。SAN 是典型的块存储,还有一类存储设备叫作 NUS。硬盘或者 LUN 是挂在服务器上的。

如果是在虚拟化环境之下,也可以像云硬盘一样挂载到虚拟机上,作为虚拟机的一个硬盘,也就是虚拟机的一个卷 Volume。Cinder 是提供块存储服务的,有时候也会说 Cinder 提供卷管理或者说是提供卷服务的。

SAN 设备与网络的两种连接主要有两种形式:第一种是 Fibre Channel(光纤通道,简称 FC),另外一种是 iSCSI(是基于以太网协议的)。

如果是 FC,需要在服务器上装上 HBA 卡,用专用的光纤给它连到存储设备上。如果是 iSCSI 的话,走的是以太网协议和 IP 协议,所以不需要专用存储的网络连接了,用 iSCSI 的越来越多。

3-1 Cinder的组成架构

如上图所示,Cinder 的架构还是非常简单的,里面有 Cinder 的 API,是用来提供 RESTful API 的,供客户端或者是其它组件来访问;中间还有一个 database,同样还有消息队列,Cinder 比较核心的两个组件:左边的 Cinder Scheduler 调度器,右边的 Cinder Volume。

通常情况下,如果我们使用了多个存储设备来做 Cinder 的后端,通常需要运行多个 Volume 的实例,如图示意:

Scheduler 的任务是在有访问请求的时候、有创建 Volume 请求的时候选择通过哪个 Cinder Volume 实例来创建卷,这就是 Scheduler 和 Volume 的分工。

重点分析一下 Volume,特别需要注意的一点是数据的流向,与 Swift 的区别是比较大的,Swift 对它的操作不仅仅是通过 Swift Proxy Server 提供的 RESTful API 进行操作,它的数据也是通过 RESTful API 走的,我们要把数据放在 Swift 里面或者从 Swift 里把数据拿出来,都通过 RESTful Web API。但是在 Cinder 不一样,Cinder API 基本上只能提供一些操作和管理功能,是直接通过 FC/iSCSI 传输数据到服务器。那 Volume 的作用到底是什么呢?数据不会通过 Volume 去走,也不会通过 RESTful API。实际上 Volume 里边有一个模块叫 Volume Driver,这个 driver 针对每一个存储设备都不一样,也被称为 Volume 插件,这个插件往往是由存储设备的厂商提供的。Volume 的核心主要是作用在这个 Driver 上,driver 的作用是什么?Driver 跟我们通常的单机的操作系统的驱动的含义不一样,这里的 Volume Driver 驱动主要是相当于一个适配的功能,把后端的设备的接口适配成 Cinder 的统一接口。

[ 补充:什么是软件定义存储?]

随着我们数据中心的存储设备数量和种类以及上层应用的种类越来越多,上层应用对存储的需求也各不相同,所以就出现一个问题,怎么样去把这些设备给整合起来从而怎么样去更好的服务于上层应用,于是就出现了软件定义存储的概念。看一张从 EMC 官网上得到的图:

软件定义存储,一方面是为了隐藏底下的硬件的异构性,不论我们用的是什么硬件,不论我们用的是什么规模的硬件,不论我们用的是什么型号的硬件,不论我们用的是哪个厂商的硬件,只要软件定义存储这一套软件支持,它对上层隐藏了底层的异构性;另外一方面是对上层应用的支持,提供了多样化存储的接口,比如说经常提到的 Hadoop 的 HDFS,还有提供对象存储的接口,还有块存储的接口,用来支持不同的应用。有了这样一个系统,那么加存储设备的人和构建底下这些存储基础设施的人就不用去担心上层应用会怎么样,只需要按照现在存储规模的需求和性能的需求往上加存储设备就行了,或者说只要扩大现在的存储设备的容量就行了,上层应用也不用担心底下怎么样去适应。只要暴露接口就行了。

OpenStack 的存储最主要的两个组件是 Swift(提供的是对象存储的接口)和 Cinder(提供了一堆 driver,Volume 里面嵌入的,这些 driver 可以调用不同的存储设备,只要有 driver 就可以把存储设备给连接上,实际上就起到了屏蔽底层硬件异构性的作用。第二,Cinder 提供的是块存储的接口),实际上也就是说我们现在在网页上这个层面,至少已经支持了最常用的两种存储接口,正好对应了软件定义存储的两个方面。基于 OpenStack 我们其实是可以做一个软件定义存储的系统出来的。

4. Neutron组件解析

Nrutron 又被称为 OpenStack networking。

有一个概念叫作层(Tier/Layer)。两种层有什么区别呢?先说一下传统的网络,传统数据中心的网络是分为几个级别的,首先是服务器接入第一级交换机叫作接入交换机,接入交换机往往是在一个机架的顶部(所以也被称为架顶交换机 TOR),可能是有堆叠的比如说是两个交换机来实现的,如下图所示最上部分的两个交换机。然后,会接入一个叫作汇聚层的交换机(汇聚交换机),往往是放在一排机架的顶头位置,这一层往往被叫做汇聚层。然后接入的是核心交换机,把汇聚交换机连起来的交换机就是核心交换机,核心交换机是数据中心非常大的网络节点,往往也不只一个。

传统中心的数据网络分为接入层、汇聚层和核心层,也叫作三个层次(Tier)。如今的网络对三层网络做了演进。

但是在 OpenStack 里,层不是指的 Tier,而是协议栈的层,也就是 Layer。根据 ISO 的网络模型,实际上网络是分为七个 Layer,OpenStack 主要关注的是第二和第三层。第二层是数据量层 Datalink,第三层是网络层 Network。交换机 Switches 是工作在数据量层的典型设备,而路由器 Routers 是典型的工作在网络层的,Neutron 主要关注这两层。

假如没有 OpenStack Neutron ,虚拟化世界的网络是什么样子的? 会使用虚拟网桥,是一个二层设备,是软件实现的网桥,不存在物理信号的问题,被认为是一个虚拟的跑在服务器寄主机内部的交换机,把虚拟机都连在这个交换机上,如下拓扑图:

如果仅仅是做一个虚拟化的环境,问题倒是不大,但如果放在云的环境上就会出现问题了,因为云涉及到租户之间流量隔离的问题,网桥无法满足这种需求,网桥完全是一个连通的,跟一个普通的交换机没啥区别。那么,说一说 Nova-network 又是怎么样的情况呢?

Nova-network 在以前的网桥的基础上,加了一个模块,因此可以有两种工作模式。第一种是:Flat DHCP 工作模式。可以提供像 DHCP 一样的功能。

第二种是:基于 VLAN 的隔离。可以使用 VLAN 的方式把各种租户隔离开,勉强满足了云的需求,在一个对网络要求不是很高的简单云计算环境中使用 Nova-Network 就可以了。

事实上我们还有其它的一些需求,比如我们要实现一个比较大的二层,要实现对流量的控制,Nova-network 就不能满足要求了。那么,Neutron 在一种非常普遍的环境中跟 Nova-network 有多大的区别呢?

[ Neutron 有的功能:]

  • 提供 VLAN 的隔离
  • 提供软件定义网络 SDN(比如:L2-in-L3 的隧道,像 VXLAN、GRE 隧道;还支持 OpenFlow 协议)
  • 支持第三方的 API 扩展
  • 支持第三方的 Plugin 扩展

我们可以开发自己的组件、插件放在 Neutron 生态里,与 Cinder 非常类似。

Neutron 支持租户创建自己的虚拟网络,看一个例子如图:

图里有两个租户,每个租户创建了自己的网络,有自己的路由器,租户之间的网络是隔离的,并且每个租户还可以定义自己网络的拓扑,这点很重要,复拓扑(Rich Topologies)是开发 Neutron 项目的重要目的。

在传统的物理网络架构中,会希望对每层应用都互相使用相对隔离的网络,然后我们可以在上面对每层进行负载均衡或者是做一些集群的方案,但是在虚拟世界中,如果我们使用 Nova-network 很难实现。而 Neutron 诞生以后,这个问题就得到了解决。后续学习的 Heat 在工作的过程中就利用了 Neutron 的功能,是 Neutron 的一个很好的应用案例。

PS:构建一个基于三层架构的Web服务

  • 第一层:Web 页面服务器
  • 第二层:应用服务器
  • 第三层:数据库服务器

5. Horizon组件解析

Horizon 提供可视化的 GUI 图形界面。让用户去操作这些项目使用的各种资源。Horizon 的内部架构是怎么样的?我们如果要做二次开发,应该怎么去做?了解 Horizon 可以不妨先去了解 Python 的一个 Web 开发框架 Django。

[ 内部分为三个层次:]

  1. Dashboard
  2. PanelGroup
  3. Panel

每一组可视化的界面都是 PanelGroup,每一个 Dashboard 在 Django 里都是一个 App,OpenStack 的文件夹里的 openstack_dashboard 文件夹里有一个 settings.py 文件,里面有一个变量叫 INSTALLED_APP 定义了目前存在的这些 dashboard

官方的社区版里面一般有四个 dashboard

1. openstack_dashboard.dashboards.project // 普通用户
2. openstack_dashboard.dashboards.admin // 管理员
3. openstack_dashboard.dashboards.settings // 右上角设置面板
4. openstack_dashboard.dashboards.router // 一般是看不见的

6. Glance组件解析

虚拟机镜像存储服务的,典型的对象存储应用

[ Glance 的架构:]

Glance API Server 提供 RESTful API,所有的访问请求会通过它
Glance Registry Server 组件对镜像进行注册,还可以提供镜像检索服务
Glance 可以使用不同的存储后端,有亚马逊的 S3 Store,有 Swift 对象存储,有 HTTP Store,有 Filesystem Store(Glance 本地的一个文件存储)

PS:对象存储这种扁平的数据组织结构的存储方式一定是将来的一个趋势,为什么?人在使用计算机的时候会使用一级一级的目录去把文件塞到不同的目录底下,建立一个树状的结构是为了方便我们自己去查找。而在写程序的时候,是通过数据库去管理这些数据,通过索引去管理这些元数据,并不需要一级一级的目录结果,Glance 通过 Registry Server 去找到这些镜像、检索这些镜像,所以我们的程序真正需要的是一个地址加上一个 ID,或者是一个地址加上一个 key。那么,对象存储正好就是地址加上 ID。所以说,对象存储会逐渐取代分布式文件系统,成为 Server 端的主流。(PS:当然,块存储是被替代不了的)

[ Glance的缓存机制:]

把 Glance API Server 做横向扩展,做一个负载均衡的集群,下面再转到实际的存储后端去,但是可能某些镜像会特别的有很多访问,这样的话某一个点的压力还是会很大,于是就有了缓存机制,每个通过 Glance 到达服务器的 Server 端的镜像,都会缓存到 API Server 上,注意,如果我们有多个 API Server,随着用户访问请求的增多,被经常访问的那个同一个镜像会在每一个 API Server 上都有一个,酱紫的话,在后续再有访问请求的时候就会直接用 API Server 上的镜像文件,而不会再去访问到存储后端上来,这个机制对终端用户来说是透明的,终端用户并不清楚这个 Glance 服务获取的镜像到底是存在哪里的。

[ 缓存管理需要注意的几个地方:]

  • 对 Cache 总量大小的限制(周期性的运行,glance-cache-pruner --image_cache_max_size=*
  • 要清理一些状态异常的 Cache 文件(glance-cache-cleaner
  • 如果新加了一个 API Server 到负载均衡的集群环境里,新的 API Server 上没有缓存,所以还提供了预取某些热门镜像到新增的 API 节点中的机制(glance-cache-manage --host=<HOST> queue-image <IMAGE_ID>)PS:这也是在云环境中经常会去使用的一种方法
  • 我们可以手动删除缓存镜像来释放空间

7. Keystone 组件

在 OpenStack 里面 Keystone 有两个作用。

  • 第一个是:权限管理(用户的建权、授权;涉及到的概念有用户、租户、角色),租户是一组用户的集合,租户可以是一个企业一个部门一个小组等等。租户与用户之间往往是多对多的关系。
  • 第二个是:服务目录(服务、端点),OpenStack 的每个服务需要在 Keystone 里注册以后才能提供给用户去使用,端点 Endpoint 可以理解为服务暴露出来的访问点,,每一个端点都对应一个服务的访问接口的实例。

关于角色 Role,安全领域有一个叫作基于角色的访问控制,Keystone 也是这样的一种安全机制,Role 是 Keystone 的访问机制里面的核心。那么对于 Role 怎么去操作呢?它最重要的命令就是 user-role-add,简单来说这个命令就是指定某个用户具有某个角色。还有一个非常重要的用途,那就是实现用户和租户多对多的关系。下面举个例子怎么样去实现用户和租户之间多对多关系的操作方法。

// 创建租户tenant1
# keystone tenant-create --name tenant1 --enabled true

// 创建另外一个租户tenant2
# keystone tenant -create --name tenant2 --enabled true

// 创建两个用户 user1 和 user2 和登录密码
# keystone user -create --name user1 --tenant -id <tenant-id of tenant1> --pass password --enabled true
# keystone user -create --name user2 --tenant -id <tenant-id of tenant2> --pass password --enabled true

// user-role-add 命令,这里的-user-id 实际上是 上面的 user2 的 id,-tenant-id 实际上是前面我们的tenant1 的 id,将 user2 加到 tenant1 里面,tenant1 就对应了两个用户:user1 和 user2,user2 就对应了两个tenant:tenant1 和 tenant2
# keystone user-role-add -user-id 7b32f4fc92704947802d2eca95edff0d -role-id 2493283f09c1475198f2337a47aa398f -tenant-id 0647347fa21d4221b0197cd282465a00

Role 在 Keystone 里面是一个关键的东西。

有一个问题,Keystone 是否解决了 OpenStack 的云安全问题呢?其实远远不够,它只是实现了对 OpenStack 各种服务的访问权限的控制。Keystone 不能和云安全划等号的。公有云上常见的安全防护手段有:

  • 安全访问(OpenStack 会暴露出来一些 API 或 Endpoint,客户是通过 HTTP 协议访问的,实际上我们应该允许通过 HTTPS 协议访问
  • 内置防火墙功能(FWIIS,Firewall as a Service,用户可以通过配置内置防火墙让云上的和环境或者应用更安全)
  • 加密数据存储(云的管理员可以获取云存储上的文件,所以用户的文件需要加密让云的管理员无法查看、检索、分析,但是不是所有的客户端都提供给用户加密数据存储的功能)
  • 帮助用户检测安全状况(主动监测租户的安全状况并给出提示,给出一些安全防护的提示,甚至在出现严重安全问题的情况下,会主动去把租户的服务下线以保证整个云的安全)

8. 总结

Nova

  • nova-console
  • nova-consoleauth
  • nova-compute
  • nova-conductor
  • nova-scheduler

Swift

  • 对象存储的特点
  • 强调 Swift 的全对等架构
  • 极高的可扩展性
  • Swift 集群部署中的注意事项

Cinder

  • 提供块存储服务或者说卷服务
  • 通过 Cinder-volume 里面的 Driver 支持不同的存储设备

Neutron

  • 租用可以制定自己的 rich toplogy 的虚拟网络
  • 通过各种 agent 和 plugin 实现具体的功能并和第三方设备、软件对接

Horizon

  • 三级代码组织形式

Glance

  • 掌握了 Glance 的镜像
  • 对 Glance 做负载均衡

Keystone

  • 通过 Role 进行权限管理
  • 通过 Role 将同一个用户分配到不同的 tenant 中

网络 MOOC 学习笔记 
From 高校帮 《OpenStack 入门 @讲师 李明宇》
By 胡飞 From BUAA
2016/4/2 17:34:36 
  • 1
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值