OpenStack各组件逻辑关系、通信部署关系及工作流程

一、 OpenStack组件之间的逻辑关系

OpenStack 是一个不断发展的系统,所以 OpenStack 的架构是演进的,举个例子:

  • E 版本有5个组件 

Compute 是 Nova;Image 是 Glance,为 Nova 提供镜像存储服务;Object 是提供 Object 存储服务的 Swift;Dashboard 是我们平时说的 Horizon;Identity 是 Keystone;

  • F版本有7各组件,核心组件:

有这七个组件可以搭出一个相对完整的云计算环境,Heat、Sahala 是可选的;相对 E 版本,新增加的两个组件分别是 Block Storage Cinder 和 Network Neutron,这两个组件和 Glance,Swift 之间没有直接的联系,实际上是从 Compute Network 和 Compute Volume 发展出来的,Neutron 组件并没有直接的去替换 Compute Network,它是一个相对独立的,也是非常著名的 SDN 的一个项目,它为 Compute 提供网络连接,提供网络的资源管理这样一些服务,Block Storage(也就是 Cinder)为 Compute 提供块存储服务,替换了 Compute Volume.

 二、OpenStack的API

OpenStack 的逻辑关系是要各个组件之间的信息传输来实现的,而组件之间的信息传输主要是通过OpenStack 之间相互调用 API 来实现的,作为一个操作系统,作为一个框架,它的 API 有着重要的意义。

基于 HTTP 协议,RESTful Web API;

什么是 REST?

全称是:Representational State Transfer,表现状态传输。由 Fielding 博士(HTTP 协议的1.0 和 1.1 版本的主要设计者,Apache 服务器软件的作者之一,Apache 基金会的第一任主席)提出。REST 是通过操作资源的表现来操作资源的状态

另外一种 Web 服务接口协议是 SOAP。

两者的区别,RESTful Web API 的调用非常简单,但是我们平时编程的时候用 SOAP 可能是基于一些框架在去做,.Net,Java 的这些都已经很成熟了,我们感受不到底层机制的这种复杂性,而 REST 其实和 SOAP 比起来非常之简洁的,另外一方面,REST 描述的是一种风格一种架构一种原则,所以它并没有规定具体的实践方式或者说协议。
目前最常见的实现方式就是基于 HTTP 协议实现的 RESTful Web API,我们的 OpenStack 里面用的就是这种方式。REST 架构里面对资源的操作,包括:获取、创建、修改和删除,正好对应着 HTTP 协议提供的 GET、POST、PUT 和 DELETE 方法,所以用 HTTP 来实现 REST 是比较方便的。

RESTful Web API 主要有以下三个要点

1 资源地址与资源的 URI,比如:http://example.com/resources/
2 传输资源的表现形式,指的是 Web 服务接受与返回的互联网媒体类型,比如:JSON,XML 等,其中 JSON 具有轻量级的特点,移动互联网的飞速发展轻量级的协议非常受欢迎,JSON 得到了广泛的应用
3 对资源的操作,Web 服务在该资源上所支持的一系列请求方法,比如:POST,GET,PUT,DELETE

下面以 OpenStack Swift 的接口作为一个例子来说明:

1  首先,用 curl 命令访问一个 http 服务
2  crul -i -X GET http://storage.clouddrive.com/v1/my_account?format=json\ -H 
3  "X-Auth-User:jdoe" -H "X-Auth-Key:jdoepassword"

返回结果:

它是一个 HTTP 响应的头部,有 Account 的细节信息,包括这个 Account 有多少个 ,Container 有多少个 Object,占用了多少字节的存储空间等等。
然后是 JOSN 格式的内容,列出了这个 Account 下面的 Container

调用及调试 API 的几种方式

1 第一种方式:curl 命令(linux 下发送 HTTP 请求并接受响应的命令行工具),这种方式其实用的比较少,比较麻烦
2 第二种方式:比较常用的 OpenStack 命令行客户端,每一个 OpenStack 项目都有一个 Python 写的命令行客户端
3 第三种方式:用 Firefox 或 Chrome 浏览器的 REST 的客户端(图形界面的,浏览器插件)
4 第四种方式:用 OpenStack 的 SDK,可以不用手动写代码发送 HTTP 请求调用 REST 接口,还省去了一些管理诸如 Token 等数据的工作,能够很方便地基于 OpenStack 做开发,
那么 OpenStack 官方提供的是 Python 的 SDK,当然还有第三方提供的 SDK 比如说支持 Java 的著名的 Jclouds,还有支持 Node.js、Ruby、.Net 等等

OpenStack 还提供了另外一套 API 兼容亚马逊的 EC2,能够方便应用在两套系统之间做迁移。

三、OpenStack组件间的通信关系

OpenStack 组件之间的通信分为四类:

1 基于 HTTP 协议
2 基于 AMQP(Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议) 协议(基于消息队列协议)
3 基于数据库连接(主要是 SQL 的通信)
4 Native API(基于第三方的 API)

有个概念需要了解一下:

1 Compute Node 是实际运行虚拟机的节点
2 Block Storage Node 主要是 Cinder 连接的存储后端(存储设备)
3 Network Node 通常是具有路由等一些网关功能的节点(网络设备)

3-1. 基于HTTP协议进行通信

通过各项目的 API 建立的通信关系,基本上都属于这一类,这些 API 都是 RESTful Web API,最常见的就是通过 Horizon 或者说命令行接口对各组件进行操作的时候产生的这种通信,
然后就是各组件通过 Keystone 对用户身份进行校验,进行验证的时候使用这种通信,还有比如说 Nova Compute 在获取镜像的时候和 Glance 之间,对 Glance API 的调用,
还有比方说 Swift 数据的读写,也是通过这个 HTTP 协议的 RESTful Web API 来进行的。

3-2. 基于高级消息队列协议

基于 AMQP 协议进行的通信,主要是每个项目内部各个组件之间的通信,比方说 Nova 的 Nova Compute 和 Scheduler 之间,然后 Cinder 的 Scheduler 和 Cinder Volume之间。
需要说明的是,Cinder 是从 Nova Volume 演化出来的,所以 Cinder 和 Nova 之间也有通过 AMQP 协议的通信关系,由于 AMQP 协议进行通信也属于面向服务的架构,
虽然大部分通过 AMQP 协议进行通信的组件属于同一个项目,但是并不要求它们安装在同一个节点上,给系统的横向扩展带来了很大的好处,
可以对其中的各个组件分别按照他们负载的情况进行横向扩展,因为他们不在一个节点上,分别用不同数量的节点去承载它们的这些服务。 ( AMQP 是一种协议,OpenStack 没有规定它是用什么实现,我们经常使用的是 Private MQ,实际上用户也可以根据自身的情况选择其它的消息中间件。)

3-3. 基于SQL的通信

通过数据库连接实现通信,这些通信大多也属于各个项目内部,也不要求数据库和项目其它组件安装在同一个节点上,它也可以分开安装,还可以专门部署数据库服务器,
把数据库服务放到上面,之间通过基于 SQL 的这些连接来进行通信。OpenStack 没有规定必须使用哪种数据库,虽然通常用的是 MySQL

3-4. 通过Native API实现通信

出现在 OpenStack 各组件和第三方的软硬件之间,比如说,Cinder 和存储后端之间的通信,Neutron 的 agent 或者说插件和网络设备之间的通信,
这些通信都需要调用第三方的设备或第三方软件的 API,我们称为它们为 Native API,那么这个就是我们前面说的基于第三方 API 的通信。

四、OpenStack几种不同的存储

OpenStack的存储服务分为三种:

Glance:

Glance(镜像存储)是一个镜像存储管理服务,本身不具备存储的功能;

Swift:

Swift (对象存储)提供的是对象存储服务,同样的具有像亚马逊 IWSS3 的特点,提供通过RESTful API 的方式去访问数据,
这样做是为了解决两个问题:第一个,我们可以直接去访问一个存储,而不需要在通过自己开发的 Web 服务器去做一次数据的转发,否则对服务器的负载来说是一种压力。
第二个,在我们的大数据时代,当数据量特别大的时候,如果我们用文件系统就会出现问题:文件的数量激增以后,存储的性能会急剧下降,而对象存储实际上则是解决这个问题的,
对象存储抛弃了那种目录树的结构,用一种扁平化的结构去管理数据。Swift 实际上只有三层结构,即 Account、Container、Object。Object 就是最终的那个数据了,
就是文件,前面有两级管理,一级是 Container 容器,它把 Object 放到容器里面,然后再上面一级是 Account,是和账户去关联的,Container 相当于是把这些 Object 做了分类,
用 Account 去跟账户关联起来。

Cinder:

Cinder (块存储)提供块存储的接口;本身也不提供数据的存储,后面也需要接一个存储的后端,像 EMC 的散设备,华为的存储设备,NetApp 的存储设备可以做它的后端。
还有一个比较火的开源分布式存储叫 Ceph,Ceph 也提供块存储服务,也可以用来作为 Cinder 的后端。Cinder 的作用就是为 OpenStack 提供块存储的接口,
有个很重要的功能叫卷管理功能,虚拟机并不直接去使用存储设备(并不直接去使用后端的存储系统),使用的是虚拟机上的块设备(卷 Volume),
实际上 Cinder 就是创建和管理这些 Volume 并且把它挂载到虚拟机上。Cinder 是从 Nova Volume 里面独立出来的,独立出来之后很受各种存储厂商的欢迎,
可以通过写 Cinder Driver 的形式把自己的存储设备纳入到 OpenStack 的生态圈里面去。

三种存储的概念

文件存储:

有 POSIX 接口或者 POSIX 兼容的接口,就可认为它是一个文件系统,比较典型的分布式文件系统有像 Glance 的 FS,Hadoop 里的 HDFS

块存储:

电脑上的一个盘格式化之后是一个文件系统,那么在格式化之前是一个块设备,也就是块存储,实际上我们在数据中心里面,像 EMC 的很多设备,
像华为的一些叫作 SAN 的设备,像 NetApp 的一些设备,如果是散存储一般来说就是提供块存储的;

对象存储:

对象存储的典型代表是亚马逊的 AWS S3,它的接口不是 POSIX,也不是像一块硬盘那样作为一个块存储的接口,是通过 RESTful Web API 去访问的,
对于应用开发者来说优势在于可以很方便的去访问存储里面存的数据,对象存储里存的数据通常叫做 Object,实际上它就是 File,
但是对象存储里面为了和文件系统做一个区别,便被叫作对象 Object。

五、 OpenStack工作流程

这里以创建一个虚拟机为例来了解 OpenStack 是如何工作的,下面的图是 OpenStack 创建虚拟机整个工作过程:

下面进行简要的文字说明:

 1 登录界面或命令行通过RESTful API向keystone获取认证信息。
 2 keystone通过用户请求认证信息,并生成auth-token返回给对应的认证请求。
 3 界面或命令行通过RESTful API向nova-api发送一个boot instance的请求(携带auth-token)。
 4 nova-api接受请求后向keystone发送认证请求,查看token是否为有效用户和token。
 5 keystone验证token是否有效,如有效则返回有效的认证和对应的角色(注:有些操作需要有角色权限才能操作)。
 6 通过认证后nova-api和数据库通讯。
 7 初始化新建虚拟机的数据库记录。
 8 nova-api通过rpc.call向nova-scheduler请求是否有创建虚拟机的资源(Host ID)。
 9 nova-scheduler进程侦听消息队列,获取nova-api的请求。
10 nova-scheduler通过查询nova数据库中计算资源的情况,并通过调度算法计算符合虚拟机创建需要的主机。
11 对于有符合虚拟机创建的主机,nova-scheduler更新数据库中虚拟机对应的物理主机信息。
12 nova-scheduler通过rpc.cast向nova-compute发送对应的创建虚拟机请求的消息。
13 nova-compute会从对应的消息队列中获取创建虚拟机请求的消息。
14 nova-compute通过rpc.call向nova-conductor请求获取虚拟机消息。(Flavor)
15 nova-conductor从消息队队列中拿到nova-compute请求消息。
16 nova-conductor根据消息查询虚拟机对应的信息。
17 nova-conductor从数据库中获得虚拟机对应信息。
18 nova-conductor把虚拟机信息通过消息的方式发送到消息队列中。
19 nova-compute从对应的消息队列中获取虚拟机信息消息。
20 nova-compute通过keystone的RESTfull API拿到认证的token,并通过HTTP请求glance-api获取创建虚拟机所需要镜像。
21 glance-api向keystone认证token是否有效,并返回验证结果。
22 token验证通过,nova-compute获得虚拟机镜像信息(URL)。
23 nova-compute通过keystone的RESTfull API拿到认证k的token,并通过HTTP请求neutron-server获取创建虚拟机所需要的网络信息。
24 neutron-server向keystone认证token是否有效,并返回验证结果。
25 token验证通过,nova-compute获得虚拟机网络信息。
26 nova-compute通过keystone的RESTfull API拿到认证的token,并通过HTTP请求cinder-api获取创建虚拟机所需要的持久化存储信息。
27 cinder-api向keystone认证token是否有效,并返回验证结果。
28 token验证通过,nova-compute获得虚拟机持久化存储信息。
29 nova-compute根据instance的信息调用配置的虚拟化驱动来创建虚拟机。

注:

keystone

User:指使用Openstack service的用户,可以是人、服务、系统,但凡使用了Openstack service的对象都可以称为User。
Project(Tenant):可以理解为一个人、或服务所拥有的 资源集合 。在一个Project(Tenant)中可以包含多个User,每一个User都会根据权限的划分来使用Project(Tenant)中的资源。比如通过Nova创建虚拟机时要指定到某个Project中,在Cinder创建卷也要指定到某个Project中。User访问Project的资源前,必须要与该Project关联,并且指定User在Project下的Role。
Role:用于划分权限。可以通过给User指定Role,使User获得Role对应的操作权限。Keystone返回给User的Token包含了Role列表,被访问的Services会判断访问它的User和User提供的Token中所包含的Role。系统默认使用管理Role admin和成员Role _member_ 
Policy:OpenStack对User的验证除了OpenStack的身份验证以外,还需要鉴别User对某个Service是否有访问权限。Policy机制就是用来控制User对Tenant中资源(包括Services)的操作权限。对于Keystone service来说,Policy就是一个JSON文件,默认是/etc/keystone/policy.json。通过配置这个文件,Keystone Service实现了对User基于Role的权限管理。
Token:是一个字符串表示,作为访问资源的令牌。Token包含了在 指定范围和有效时间内 可以被访问的资源。EG. 在Nova中一个tenant可以是一些虚拟机,在Swift和Glance中一个tenant可以是一些镜像存储,在Network中一个tenant可以是一些网络资源。Token一般被User持有。
Credentials:用于确认用户身份的凭证
Authentication:确定用户身份的过程
Service:Openstack service,即Openstack中运行的组件服务。
Endpoint:一个可以通过网络来访问和定位某个Openstack service的地址,通常是一个URL。比如,当Nova需要访问Glance服务去获取image 时,Nova通过访问Keystone拿到Glance的endpoint,然后通过访问该endpoint去获取Glance服务。我们可以通过Endpoint的region属性去定义多个region。Endpoint 该使用对象分为三类:
admin url> 给admin用户使用,Post:35357
internal url –> OpenStack内部服务使用来跟别的服务通信,Port:5000
public url –> 其它用户可以访问的地址,Post:5000
创建完service后创建API EndPoint. 在openstack中,每一个service都有三种end points. Admin, public, internalAdmin是用作管理用途的,如它能够修改user/tenant(project)。 
public 是让客户调用的,比如可以部署在外网上让客户可以管理自己的云。
internal是openstack内部调用的。
三种endpoints 在网络上开放的权限一般也不同。Admin通常只能对内网开放,public通常可以对外网开放internal通常只能对安装有openstack对服务的机器开放。

一个实例:

1 用户alice登录keystone系统(password或者token的方式),获取一个临时的token和catalog服务目录
(v3版本登录时,如果没有指定scope,project或者domain,获取的临时token没有任何权限,不能查询project或者catalog)。
2 alice通过临时token获取自己的所有的project列表。 3 alice选定一个project,然后指定project重新登录,获取一个正式的token,同时获得服务列表的endpoint,用户选定一个endpoint,
在HTTP消息头中携带token,然后发送请求(如果用户知道project name或者project id可以直接第3步登录)。
4 消息到达endpoint之后,由服务端(nova)的keystone中间件(pipeline中的filter:authtoken)向keystone发送一个验证token的请求。
(token类型:uuid需要在keystone验证token,pki类型的token本身是包含用户详细信息的加密串,可以在服务端完成验证)
5 keystone验证token成功之后,将token对应用户的详细信息,例如:role,username,userid等,返回给服务端(nova)。 6 服务端(nova)完成请求,例如:创建虚拟机。 7 服务端返回请求结果给alice。

cinder:

cinder主要组成:

1 Cinder-api 是 cinder 服务的 endpoint,提供 rest 接口,负责处理 client 请求,并将 RPC 请求发送至 cinder-scheduler 组件。
2 Cinder-scheduler 负责 cinder 请求调度,其核心部分就是 scheduler_driver, 作为 scheduler manager 的 driver,负责 cinder-volume 具体的调度处理,
发送 cinder RPC 请求到选择的 cinder-volume。 3 Cinder-volume 负责具体的 volume 请求处理,由不同后端存储提供 volume 存储空间。

neutron

neutron主要组成:

1.Neutron-server可以理解为一个专门用来接收Neutron REST API调用的服务器,然后负责将不同的rest api分发到不同的neutron-plugin上。
2.Neutron-plugin可以理解为不同网络功能实现的入口,各个厂商可以开发自己的plugin。Neutron-plugin接收neutron-server分发过来的REST API,向neutron database完成一些信息的注册,然后将具体要执行的业务操作和参数通知给自身对应的neutron agent。
3.Neutron-agent可以直观地理解为neutron-plugin在设备上的代理,接收相应的neutron-plugin通知的业务操作和参数,并转换为具体的设备级操作,以指导设备的动作。当设备本地发生问题时,neutron-agent会将情况通知给neutron-plugin。
4.Neutron database,顾名思义就是Neutron的数据库,一些业务相关的参数都存在这里。
5.Network provider,即为实际执行功能的网络设备,一般为虚拟交换机(OVS或者Linux Bridge)。

虚拟机创建的四个阶段

1 scheduling
2 networking
3 block_ device_mapping
4 spawing

几种通信关系的体现

1 各个组件 API 之间的调用,这就属于 HTTP 通信;
2 Nova 和 Neutron 内部各个组件的通信属于通过 AMQP 协议的通信;
3 中间频繁的读写数据库的操作属于数据库连接进行通信的;
4 Nova 与 Hypervisor 或者说 Libvirt 交互属于通过 Native API 即第三方接口去进行通信的,还有一个就是在给虚拟机准备 Volume 的过程中 Cinder 还需要和存储设备进行交互,
这中间也需要用到 Native API 或者是第三方接口;

六、OpenStack的部署架构

前面已经从逻辑关系、通信关系分析了OpenStack 各组件之间的关系,并且也介绍了 OpenStack 的 API 和存储。

前面谈到的各种架构基本上都是属于软件上的逻辑架构,但是 OpenStack 是个分布式的系统,就得解决从逻辑架构到物理架构的映射的问题,也就是 OpenStack 的各个项目、组件按什么方式安装到实际的服务器节点上去,实际的存储设备上,如何通过把它们通过网络连接起来,这就是 OpenStack 的部署架构。

OpenStack的部署分为:

单节点部署,通常是用于学习或者是开发
多节点部署(集群部署)

OpenStack 的部署架构不是一成不变的,而是根据实际的需求设计不同的实施方案。

在实际生产过程中,我们首先要对计算、网络、存储所需要的资源进行规划,虽然说我们现在用的云计算技术,它比传统的 IT 架构在资源规划方面的困难和工作量要小一些,但是还是需要有一个规划,这里学习了解一下基本的和复杂的两种集群部署架构。

6-1. 简单部署架构

是一个最基本的生产环境所需要的 OpenStack 的部署情况。

根据实际需要设计不同的实施方案

下面解释一下 “三种网络和四种节点”

(1)绿色的管理网络 + 蓝色的存储网络 + 黄色的服务网络

  管理网络 是 OpenStack 的管理节点或者说是它的管理服务对其它的节点去进行管理的这样一个网络,他们之间有 “不同组件之间的 API 调用,虚拟机之间的迁移” 等等;
  存储网络 是计算节点访问存储服务的网络,包括向存储设备里面读写数据的流量基本上都需要从存储网络走,还有另外一种是服务网络;
  服务网络 是由 OpenStack 去管理的虚拟机对外提供服务的网络,服务器上通常都是一台服务器上带好几块网卡,好几块网口,我们可以给各个网络做隔离。隔离的好处是,
它们的流量不会交叉,比方说我们在读写存储设备的时候,可能在存储网络上的流量特别大,但是它不会影响到我们这些节点对外提供服务,
同样,在我们做虚拟机迁移的时候可能在管理网络上它的数据流量会非常大,但是它同样不会影响到我们这些计算节点对存储设备的读写性能。

(2)四种节点:

控制节点(OpenStack 的管理节点,OpenStack 的大部分服务都是运行在控制节点上的,比如说 Keystone 的认证服务,虚拟机镜像管理服务 Glance 等等)
计算节点(计算节点指的是实际运行虚拟机的节点)
存储节点(提供对象存储服务,提供对象存储的 Swift 的节点或者是 Swift 集群的 Proxy 节点,也可以是其它服务的存储后端)
网络节点(实现网关和路由的功能)

有些服务可以直接部署在 Controller 节点上或者说是直接部署在控制节点上,但是特别需要说明的一点是: Nova 和 Neutron 这两个组件必须采用分布式部署。说一下 Nova:Nova-Compute 是控制虚拟机的,是控制和管理虚拟机的,所以必须部署在计算节点上,而 Nova 的其它几个服务则应该部署在控制节点上,特别需要强调一点,Nova-Compute 和 Nova-Conducter 一定不能部署在同一个节点上,把这两个分开就是为了解耦。
说一下 Neutron:Neutron 的一些插件或 Agent 需要部署在网络节点和计算节点上,而其他的部分,比如说 Neutron Server 可以部署在控制节点上

6-2. 复杂部署架构

需要掌握三个要点:

1 规模较大的情况下,把各种管理服务部署到不同的服务器上。把这些 服务拆开部署 到不同的节点上,甚至要把同 一个服务的不同组件也拆开部署,
比如说可以把 Nova 的数据库给独立拧出来部署成一个 MySQL 数据库集群,还有 Cinder 里面的 Scheduler 和 Volume 可以部署到不同的节点上,
实际上因为 Swift 项目具有一定的独立性,所以 Swift 本身就有跨地域部署的生产环境,规模非常之大,跨地域部署,所以它的服务的可用性极高,自然有这种栽培的特性,
可以提供极高可用性和数据持久性 的对象存储服务。于是乎,很容易的对 OpenStack 的这些服务进行横向扩展,对 OpenStack 整个系统做横向扩展,
这些让 OpenStack 具有比较高的负载能力,可以达到一个比较大的规模。所有的这些都得益于 OpenStack 设计的时候采用了 SO 吻合的面向服务的架构,就是 SOA 架构,
具体到每个组件如何进行分布式的部署,如何进行横向扩展。
2 出于高可用的考虑,生产环境中我们会把 OpenStack 的同一个服务部署到不同的节点上,形成双机热备或者多机热备的高可用集群。(或者用负载均衡集群)。 3 在复杂的数据中心环境中,还有很多第三方服务,比方说 LDAP 服务、DNS 服务等等,考虑如何与第三方服务进行对接和集成。比如说,

我们可能需要使用 OPEN LDAP 服务器来作为 Keystone 的认证和健全的后端,这些一般是在管理网络中去完成的。

转:http://blog.csdn.net/q123_xi/article/details/78550273?locationNum=10&fps=1

posted @ 2018-02-12 21:45  百衲本  阅读(21522)  评论(0编辑  收藏  举报
cnblogs_post_body { color: black; font: 0.875em/1.5em "微软雅黑" , "PTSans" , "Arial" ,sans-serif; font-size: 15px; } cnblogs_post_body h1 { text-align:center; background: #333366; border-radius: 6px 6px 6px 6px; box-shadow: 0 0 0 1px #5F5A4B, 1px 1px 6px 1px rgba(10, 10, 0, 0.5); color: #FFFFFF; font-family: "微软雅黑" , "宋体" , "黑体" ,Arial; font-size: 23px; font-weight: bold; height: 25px; line-height: 25px; margin: 18px 0 !important; padding: 8px 0 5px 5px; text-shadow: 2px 2px 3px #222222; } cnblogs_post_body h2 { text-align:center; background: #006699; border-radius: 6px 6px 6px 6px; box-shadow: 0 0 0 1px #5F5A4B, 1px 1px 6px 1px rgba(10, 10, 0, 0.5); color: #FFFFFF; font-family: "微软雅黑" , "宋体" , "黑体" ,Arial; font-size: 20px; font-weight: bold; height: 25px; line-height: 25px; margin: 18px 0 !important; padding: 8px 0 5px 5px; text-shadow: 2px 2px 3px #222222; } cnblogs_post_body h3 { background: #2B6695; border-radius: 6px 6px 6px 6px; box-shadow: 0 0 0 1px #5F5A4B, 1px 1px 6px 1px rgba(10, 10, 0, 0.5); color: #FFFFFF; font-family: "微软雅黑" , "宋体" , "黑体" ,Arial; font-size: 18px; font-weight: bold; height: 25px; line-height: 25px; margin: 18px 0 !important; padding: 8px 0 5px 5px; text-shadow: 2px 2px 3px #222222; } 回到顶部 博客侧边栏 回到顶部 页首代码 回到顶部 页脚代码