Hadoop简介

一、什么是Hadoop

Hadoop是一个由Apache基金会所开发的分布式系统基础架构。
用户可以在不了解分布式底层细节的情况下,开发分布式程序。充分利用集群的威力进行高速运算和存储。
 Hadoop实现了一个分布式文件系统(Hadoop Distributed File System),简称HDFS。HDFS有高容错性的特点,并且设计用来部署在低廉的(low-cost)硬件上;而且它提供高吞吐量(high throughput)来访问应用程序的数据,适合那些有着超大数据集(large data set)的应用程序。HDFS放宽了(relax)POSIX的要求,可以以流的形式访问(streaming access)文件系统中的数据。
Hadoop的框架最核心的设计就是:HDFS和MapReduce。HDFS为海量的数据提供了存储,则MapReduce为海量的数据提供了计算。 

二、MapReduce理论基础

每个MapReduce job都是Hadoop客户端想要执行的一个工作单元,它一般由输入数据、MapReduce程序和配置信息组成,而Hadoop会把每个job分隔成两类任务(task):map任务和reduce任务。在Hadoop集群中有两类节点来执行两类job进程的执行

2.1 大数据处理

任何基础业务包含了收集、分析、监控、过滤、搜索或组织web内容的公司或组织都面临着所谓的“大数据”问题:“web规模”处理即海量数据处理的代名词。社交类网站的兴起也使得这些组织面临着另一个问题:用户行为数据分析,
这涉及到通过日志文件记录用户的对web页面浏览、点击、停留时长等,而后对日志文件中的大量数据进行分析以支持进行合理、正确的商业决策。
那么,大数据处理究竟意味着对多大规模的数据进行处理?一个简单的例子:Google在2004年平均每天利用MapReduce处理100GB的数据,到2008年平均每天处理的数据已经达到20PB;2009年,Facebook的数据量达到2.5PB,
且以每天15TB的速度在增长。PB级别的数据集正变得越来越常见,大数据时代的到来已然是不争的事实,密集数据处理也正迅速成为现实需求。
大数据问题的处理需要以与传统数据处理方式所不同的方法去实现,这正是MapReduce思想得以大放光彩的核心所在。MapReduce在实现大数据处理上有着多个基础理论思想的支撑,然而这些基础理论甚至实现方法都未必是MapReduce所创,
它们只是被MapReduce采用独特的方式加以利用而已。 (
1)向外扩展(Scale out)而非向上扩展(Scale up):大数据的处理更适合采用大量低端商业服务器(scale out)而非少量高端服务器(scale up)。后者正是向上扩展的系统性能提升方式,它通常采用有着SMP架构的主机,然而有着大量的CPU插槽(成百上千个)及大量的共享内存(可以多达数百GB)的高端服务器非常昂贵,但其性能的增长却非线性上升的,因此性价比很一般。而大量的低端商业服务器价格低廉、易于更换和伸缩等特性有效避免了向上扩展的敝端。 (2)假设故障很常见(Assume failures are common):在数据仓库架构级别,故障是不可避免且非常普遍的。假设一款服务器出故障的平均概率为1000天1次,那么10000台这种服务器每天出错的可能性将达到10次。因此,大规模向外扩展的应用场景中,一个设计优良且具有容错能力的服务必须能有效克服非常普遍的硬件故障所带来的问题,即故障不能导致用户应用层面的不一致性或非确定性。MapReduce编程模型能通过一系列机制如任务自动重启等健壮地应付系统或硬件故障。 (3)将处理移向数据(Move processing to the data):传统高性能计算应用中,超级计算机一般有着处理节点(processing node)和存储节点(storage node)两种角色,它们通过高容量的设备完成互联。然而,大多数数据密集型的处理工作并不需要多么强大的处理能力,于是把计算与存储互相分开将使得网络成为系统性能瓶颈。为了克服计算如此类的问题,MapReduce在其架构中将计算和存储合并在了一起,并将数据处理工作直接放在数据存储的位置完成,只不过这需要分布式文件系统予以支撑。 (4)顺序处理数据并避免随机访问(Process data sequentially and avoid random access):大数据处理通常意味着海量的数量难以全部载入内存,因而必须存储在磁盘上。然而,机械式磁盘寻道操作的先天性缺陷使得随机数据访问成为非常昂贵的操作,因此避免随机数据访问并以顺序处理为目的完成数据组织成为亟待之需。固态磁盘虽然避免了机械磁盘的某此缺陷,然而其高昂的价格以及并没有消除的随机访问问题仍然无法带来性能上的飞跃发展。MapReduce则主要设计用来在海量数据集上完成批处理操作,即所有的计算被组织成较长的流式处理操作,以延迟换取较大的吞吐能力。 (5)向程序员隐藏系统级别的细节(Hide system-level details from the application developer) (6)无缝扩展(Seamless scalability)

2.2 MapReduce和大数据问题

海量数据处理的核心思想无非是将一个较大的问题进行“分割包围、逐个歼灭”。然而其难点和关键点在于如何将一个大的问题分分割成多个可以分别在不同的CPU上或不同的主机上进行处理的独立小问题,
而且这些独立进行处理的小问题所产生的中间结果又该如何合并成最终结果并予以输出。因此,看似简单的化整为零的处理思想却不得不面临如下的难题: (
1) 如何将大问题分割为小任务?进一步地,如何将大问题分解为可以并行处理的小任务? (2) 如何将分解好的小任务派送给分布式系统中的某主机且是较为适合解决此问题的主机上的worker完成处理? (3) 如何保证某worker获取所需的数据? (4) 如何协调不同worker之间进行同步? (5) 如何将某worker的部分结果共享给其它需要此结果的worker? (6) 如何在出现软件或硬件故障时仍然能保证上述工作的顺利进行? 在传统的并行或分布式编程模型中,程序员不得不显式地解决上述的部分甚至是全部问题,而在共享内存编程中,程序员需要显式地协调对共享数据结构的如互斥锁的访问、显式地通过栅(barrier)等设备解决进程同步问题、并得时刻警惕着程序中可能出现的死锁或竞争条件。
虽然有些编程语言也或多或少地规避了让程序员面对上述问题,但却也避免不了将资源分配给各worker的问题。MapReduce的优势之一便是有效地向程序员隐藏了这些问题。

2.3 mapper和reducer

键值对儿(Key-value pair)是MapReduce的基础数据结构。Key和Value可以是基础类型数据,如整数、浮点数、字符串或未经加工的字节数据,也可以是任意形式的复杂数据类型。程序员可以自行定义所需的数据类型,也可借助于Protocol Buffer、Thrift或Avro提供的便捷方式完成此类工作。
MapReduce算法设计的工作之一就是在给定数据集上定义“键-值”数据结构,比如在搜索引擎搜集、存储网页类工作中,key可以使用URL来表示,而value则是网页的内容。而在有些算法中,Key也可以是没有任何实际意义的数据,其在数据处理过程中可被安全忽略。在MapReduce中,程序员需要基于如下方式定义mapper和reducer:
    map: (k1,v1)-->[(k2,v20)]
    reduce: (k2,[v2])-->[(k3,v3)]  
其中[...]意味着其可以是一个列表。这些传递给MapReduce进行处理的数据存储于分布式文件上,mapper操作将应用于每一个传递过来的键-值对并生成一定数量的中间键值对(intermediate key-value),而后reduce操作将应用于这些中间键值对并输出最终的键值对。
然而,mapper操作和reducer操作之间还隐含着一个应用于中间键值对的“分组”操作,同一个键的键值对需要被归类至同一组中并发送至同一个reducer,而传送给每个reducer的分组中的键值对是基于键进行排序后的列表。
reducer生成的结果将会保存至分布式文件系统,并存储为一个或多个以r(即reducer号码)结尾的文件,但mapper生成的中间键值对数据则不会被保存。 在Hadoop中,mapper和reducer是分别由MAP和REDUCE方法实现的对象。每个map任务(接收一个称作input split的键值对列表)都被初始化一个mapper对象,并会由执行框架为每个输入的键值对调用一次其map方法。
程序员可以配置启动的map任务个数,但其真正启动的数目则由执行框架根据数据的物理分布最终给定。类似地,每个reduce任务由REDUCE方法初始化为一个reduce对象,并会由执行框架为其接受的每个中间键值对调用一次REDUCE方法,所不同的是,程序员可以明确限定启动的reduce任务的个数。
mapper和reducer可以直接在各自接收的数据上执行所需要的操作,然而,当使用到外部资源时,多个mapper或reducer之间可能会产生资源竞争,这势必导致其性能下降,因此,程序员必须关注其所用资源的竞争条件并加入适当处理。其次,mapper输出的中间键值对与接受的键值对可以是不同的数据类型,
类似地,reducer输出的键值对与其接收的中间键值对也可以是不同的数据类型,这可能会给编程过程及程序运行中的故障排除带来困难,但这也正是MapReduce强大功能的体现之一。
除了常规的两阶段MapReduce处理流外,其还有一些变化形式。比如将mapper输出的结果直接保存至磁盘中(每个mapper对应一个文件)的没有reducer的MapReduce作业,不过仅有reducer而没有mapper的作业是不允许的。
不过,就算用不着reducer处理具体的操作,利用reducer将mapper的输出结果进行重新分组和排序后进行输出也能以另一种形式提供的完整MapReduce模式。 MapReduce作业一般是通过HDFS读取和保存数据,但它也可以使用其它满足MapReduce应用的数据源或数据存储,比如Google的MapReduce实现中使用了Bigtable来完成数据的读入或输出。BigTable属于非关系的数据库,它是一个稀疏的、分布式的、持久化存储的多维度排序Map,
其设计目的是可靠的处理PB级别的数据,并且能够部署到上千台机器上。在Hadoop中有一个类似的实现HBase可用于为MapReduce提供数据源和数据存储。

2.4 Hadoop运行框架

MapReduce程序也称作为MapReduce作业,一般由mapper代码、reducer代码以及其配置参数(如从哪儿读入数据,以及输出数据的保存位置)组成。准备好的作业可通过JobTracker(作业提交节点)进行提交,然后由运行框架负责完成后续的其它任务。这些任务主要包括如下几个方面。
(1) 调度
每个MapReduce作业都会划分为多个称作任务(task)的较小单元,而较大的作业划分的任务数量也可能会超出整个集群可运行的任务数,此时就需要调度器程序维护一个任务队列并能够追踪正在运行态任务的相关进程,以便让队列中处于等待状态的任务派送至某转为可用状态的节点运行。
此外,调度器还要负责分属于不同作业的任务协调工作。 对于一个运行中的作业来说,只有所用的map任务都完成以后才能将中间数据分组、排序后发往reduce作业,因此,map阶段的完成时间取决于其最慢的一个作业的完成时间。类似的,reduce阶段的最后一个任务执行结束,其最终结果才为可用。
因此,MapReduce作业完成速度则由两个阶段各自任务中的掉队者决定,最坏的情况下,这可能会导致作业长时间得不到完成。出于优化执行的角度,Hadoop和Google MapReduce实现了推测执行(Speculative execution)机制,即同一个任务会在不同的主机上启动多个执行副本,
运行框架从其最快执行的任务中取得返回结果。不过,推测执行并不能消除其它的滞后场景,比如中间键值对数据的分发速度等。 (
2) 数据和代码的协同工作(data/code co-location) 术语“数据分布”可能会带来误导,因为MapReduce尽力保证的机制是将要执行的代码送至数据所在的节点执行,因为代码的数据量通常要远小于要处理的数据本身。当然,MapReduce并不能消除数据传送,比如在某任务要处理的数据所在的节点已经启动很多任务时,
任务将不得不在其它可用节点运行。此时,考虑到同一个机架内的服务器有着较充裕的网络带宽,一个较优选择是从数据节点同一个机架内挑选一个节点来执行此任务。 (
3) 同步(Synchronization) 异步环境下的一组并发进程因直接制约而互相发送消息而进行互相合作、互相等待,使得各进程按一定的速度执行的过程称为进程间同步,其可分为进程同步(或者线程同步)和数据同步。就编程方法来说,保持进程间同步的主要方法有内存屏障(Memory barrier),互斥锁(Mutex),
信号量(Semaphore)和锁(Lock),管程(Monitor),消息(Message),管道(Pipe)等。MapReduce是通过在map阶段的进程与reduce阶段的进程之间实施隔离来完成进程同步的,即map阶段的所有任务都完成后对其产生的中间键值对根据键完成分组、
排序后通过网络发往各reducer方可开始reduce阶段的任务,因此这个过程也称为“shuffle and sort”。 (
4) 错误和故障处理(Error and fault handling) MapReduce运行框架本身就是设计用来容易发生故障的商用服务器上了,因此,其必须有着良好的容错能力。在任何类别的硬件故障发生时,MapReduce运行框架均可自行将运行在相关节点的任务在一个新挑选出的节点上重新启动。同样,在任何程序发生故障时,
运行框架也要能够捕获异常、记录异常并自动完成从异常中恢复。另外,在一个较大规模的集群中,其它任何超出程序员理解能力的故障发生时,MapReduce运行框架也要能够安全挺过。

2.5 partitioner和combiner

除了前述的内容中的组成部分,MapReduce还有着另外两个组件:partiontioner和combiner。
Partitioner负责分割中间键值对数据的键空间(即前面所谓的“分组”),并将中间分割后的中间键值对发往对应的reducer,也即partitioner负责完成为一个中间键值对指派一个reducer。最简单的partitioner实现是将键的hash码对reducer进行取余计算,
并将其发往余数对应编号的reducer,这可以尽力保证每个reducer得到的键值对数目大体上是相同的。不过,由于partitioner仅考虑键而不考虑“值”,因此,发往每个reducer的键值对在键数目上的近似未必意味着数据量的近似。 Combiner是MapReduce的一种优化机制,它的主要功能是在“shuffle and sort”之前先在本地将中间键值对进行聚合,以减少在网络上发送的中间键值对数据量。因此可以把combiner视作在“shuffle and sort”阶段之前对mapper的输出结果所进行聚合操作的“mini
-reducer”。
在实现中,各combiner之间的操作是隔离的,因此,它不会涉及到其它mapper的数据结果。需要注意的是,就算是某combiner可以有机会处理某键相关的所有中间数据,也不能将其视作reducer的替代品,因为combiner输出的键值对类型必须要与mapper输出的键值对类型相同。
无论如何,combiner的恰当应用将有机会有效提高作业的性能。

三、分布式文件系统(HDFS)

前面我们已经了解了Hadoop中实现的MapReduce是一个编程模型和运行框架,它能够通过JobTracker接收客户提交的作业而后将其分割为多个任务后并行运行在多个TaskTracker上。而问题是,这些TaskTracker如何高效获取所要处理的数据?

在传统的高性能集群中,计算节点和存储节点是各自独立的,它们之间通过高速网络完成互联,然而,在面临海量数据处理的问题时,网络必然会成为整个系统的性能瓶颈,这就需要引入超高速的网络如万兆以太网或Infiniband。
然而,对大数场景来讲它们属于“奢侈品”,且昂贵的投入并不能带来网络性能的线性提升,因此性价比不高。面对这种问题,MapReduce采取了将计算节点与存储节点合二为一的集群模型,它利用分布式文件系统将数据存储于多个节点上,
而后让处理过程在各数据节点本地直接进行,从而极大地降低了数据通过网络传送的需求。不过,这里仍然需要说明的是,MapReduce并非依赖于分布式文件系统,只不过运行在非分布式文件系统的MapReduce的诸多高级特性将无用武之地。 事实上,分布式文件系统并非MapReduce带来的新生事物,只不过,MapReduce站在前人的基础上将分布式文件系统进行了改造以使得它更能够适用于在MapReduce中完成海量数据处理。Google为在他们的MapReduce中实现的分布式文件系统为GFS(Google File System),
而Hadoop的实现称作HDFS(Hadoop Distributed File System)。

3.1 HDFS的设计理念

HDFS专为存储大文件而设计,可运行于普通的商业服务器上,基于流式数据访问模型完成数据存取。HDFS将所有文件的元数据存储于名称节点(NameNode)的内存中,能够利用分布式特性高效地管理“大”文件(GB级别甚至更大的文件),
对于有着海量小文件的应用场景则会给名称节点带去巨大压力并使得其成为系统性能瓶颈。再者,HDFS为MapReduce的计算框架而设计,存储下来数据主要用于后续的处理分析,其访问模型为“一次写入、多次读取”;
因此,数据在HDFS中存储完成后,仅能在文件尾部附加新数据,而不能对文件进行修改。另外,HDFS专为了高效地传输大文件进行了优化,其为了完成此目标,在“低延迟”特性上做出了很大让步,因此,其不适用于较小访问延迟的应用。

3.2 HDFS架构

3.2.1 HDFS数据块

传统文件系统一样,HDFS也在“块(block)”级别存取文件,所不同的是,传统文件系统数据块一般较小(1KB、2KB或4KB等),HDFS的数据块大小默认为64MB,甚至可以使用128MB或256MB级别的数据块。
HDFS使用块抽象层管理文件,可以实现将分块分为多个逻辑部分后分布于多个存储节点,也能够有效简化存储子系统。而对于存储节点来说,较大的块可以减少磁盘的寻道次数,进而提升I/O性能。

3.2.2 名称节点(NameNode) 辅助名称节点(SecondaryNameNode) 数据节点(DataNode) 

HDFS集群中节点的工作模型为“master-worker”:其包含一个名称节点(master)和多个数据节点(worker)。
NameNode是主节点,负责存储和管理文件系统元数据信息,包括namespace目录结构、文件块位置信息等;
DataNode是从节点,负责存储文件具体的数据块。
SecondaryNameNode 是主角色的辅助角色,帮助主角色进行元数据的合并,给备节点

名称节点负责管理HDFS的名称空间,即以树状结构组织的目录及文件的元数据信息,这些信息持久存储于名称节点本地磁盘上并保存为名称空间镜像(namespace image)和编辑日志(edit log)两个文件。
名称节点并不存储数据块,它仅需要知道每个文件对应数据块的存储位置,即真正存储了数据块的数据节点。然而,名称节点并不会持久存储数据块所与其存储位置的对应信息,因为这些信息是在HDFS集群启动由名称节点根据各数据节点发来的信息进行重建而来。
这个重建过程被称为HDFS的安全模式。数据节点的主要任务包括根据名称节点或客户的要求完成存储或读取数据块,并周期性地将其保存的数据块相关信息报告给名称节点。
默认情况下,HDFS会在集群中为每个数据块存储三个副本以确保数据的可靠性、可用性及性能表现。在一个大规模集群中,这三个副本一般会保存至不同机架中的数据节点上以应付两种常见的故障:单数据节点故障和导致某机架上的所有主机离线的网络故障。
另外,如前面MapReduce运行模型中所述,为数据块保存多个副本也有利于MapReduce在作业执行过程中透明地处理节点故障等,并为MapReduce中作业协同处理以提升性能提供了现实支撑。
名称节点会根据数据节点的周期性报告来检查每个数据块的副本数是否符合要求,低于配置个数要求的将会对其进行补足,而多出的将会被丢弃。
HDFS提供了POSIX网络的访问接口,所有的数据操作对客户端程序都是透明的。当客户端程序需要访问HDFS中的数据时,它首先基于TCP/IP协议与名称节点监听的TCP端口建立连接,接着通过客户端协议(Client Protocol)发起读取文件的请求,
而后名称节点根据用户请求返回相关文件的块标识符(blockid)及存储了此数据块的数据节点。接下来客户端向对应的数据节点监听的端口发起请求并取回所需要数据块。当需要存储文件并写数据时,客户端程序首先会向名称节点发起名称空间更新请求,
名称节点检查用户的访问权限及文件是否已经存在,如果没有问题,名称空间会挑选一个合适的数据节点分配一个空闲数据块给客户端程序。客户端程序直接将要存储的数据发往对应的数据节点,在完成存储后,数据节点将根据名称节点的指示将数据块复制多个副本至其它节点。

3.2.3 名称节点的可用性

由前一节所述的过程可以得知,名称节点的宕机将会导致HDFS文件系统中的所有数据变为不可用,而如果名称节点上的名称空间镜像文件或编辑日志文件损坏的话,整个HDFS甚至将无从重建,所有数据都会丢失。
因此,出于数据可用性、可靠性等目的,必须提供额外的机制以确保此类故障不会发生,Hadoop为此提供了两种解决方案。 最简单的方式是将名称节点上的持久元数据信息实时存储多个副本于不同的存储设备中。Hadoop的名称节点可以通过属性配置使用多个不同的名称空间存储设备,而名称节点对多个设备的写入操作是同步的。
当名称节点故障时,可在一台新的物理主机上加载一份可用的名称空间镜像副本和编辑日志副本完成名称空间的重建。然而,根据编辑日志的大小及集群规模,这个重建过程可能需要很长时间。
另一种方式是提供第二名称节点(Secondary NameNode)。第二名称节点并不真正扮演名称节点角色,它的主要任务是周期性地将编辑日志合并至名称空间镜像文件中以免编辑日志变得过大。
它运行在一个独立的物理主机上,并需要跟名称节点同样大的内存资源来完成文件合并。另外,它还保存一份名称空间镜像的副本。然而,根据其工作机制可知,第二名称节点要滞后于主节点,因此名称节点故障时,部分数据丢失仍然不可避免。
尽管上述两种机制可以最大程序上避免数据丢失,但其并不具有高可用的特性,名称节点依然是一个单点故障,因为其宕机后,所有的数据将不能够被访问,进而所有依赖于此HDFS运行的MapReduce作业也将中止。
就算是备份了名称空间镜像和编辑日志,在一个新的主机上重建名称节点并完成接收来自各数据节点的块信息报告也需要很长的时间才能完成。
在有些应用环境中,这可能是无法接受的,为此,Hadoop
0.23引入了名称节点的高可用机制——设置两个名称节点工作于“主备”模型,主节点故障时,其所有服务将立即转移至备用节点。进一步信息请参考官方手册。 在大规模的HDFS集群中,为了避免名称节点成为系统瓶颈,在Hadoop 0.23版本中引入了HDFS联邦(HDFS Federation)机制。HDFS联邦中,每个名称节点管理一个由名称空间元数据和包含了所有块相关信息的块池组成名称空间卷(namespace volume),
各名称节点上的名称空间卷是互相隔离的,因此,一个名称节点的损坏并不影响其它名称节点继续提供服务。进一步信息请参考官方手册。

四、HDFS集群角色职责

 4.1 NameNode职责

NameNode是HDFS的核心,集群的主角色,被称为Master。
NameNode仅存储管理HDFS的元数据:文件系统namespace操作维护目录树,文件和块的位置信息。
NameNode不存储实际数据或数据集。数据本身实际存储在DataNodes中。
NameNode知道HDFS中任何给定文件的块列表及其位置。使用此信息NameNode知道如何从块中构建文件。
NameNode并不持久化存储每个文件中各个块所在的DataNode的位置信息,这些信息会在系统启动时从DataNode汇报中重建。这也就是NameNode启动时会先进入安全模式的原因
NameNode对于HDFS至关重要,当NameNode关闭时,HDFS / Hadoop集群无法访问。
NameNode是Hadoop集群中的单点故障。
NameNode所在机器通常会配置有大量内存(RAM)。

4.2 DataNode职责

DataNode负责将实际数据存储在HDFS中。是集群的从角色,被称为Slave。
DataNode启动时,它将自己发布到NameNode并汇报自己负责持有的块列表。
根据NameNode的指令,执行块的创建、复制、删除操作。
DataNode会定期(dfs.heartbeat.interval配置项配置,默认是3秒)向NameNode发送心跳,如果NameNode长时间没有接受到DataNode发送的心跳, NameNode就会认为该DataNode失效。
DataNode会定期向NameNode进行自己持有的数据块信息汇报,汇报时间间隔取参数dfs.blockreport.intervalMsec,参数未配置的话默认为6小时.
DataNode所在机器通常配置有大量的硬盘空间。因为实际数据存储在DataNode中。

 

posted @ 2018-03-28 22:02  百衲本  阅读(3181)  评论(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; } 回到顶部 博客侧边栏 回到顶部 页首代码 回到顶部 页脚代码