Browsed by
月份:2016年12月

多进程&多线程 echo server实现

多进程&多线程 echo server实现

本文为 高性能网络编程学习 系列文章的第1篇

 

多进程版本

本文承接上文的单进程 echo server 建立一个多进程的echo server。代码的改动不多,下面贴一下完整的代码

首先来查看一下fork的整个流程,document_2016-12-24_17-31-43

如图,fork之后子进程会调用read/write进行IO操作,而父进程会再次转到执行accept,使得每一个client都会fork一个进程来执行IO操作。下面记录并描述一下在写这个代码的时候遇到的几个问题

在使用fork的时候可能大家也遇到过这个现象,就是自己的进程莫名其妙的变成了后台进程,比如下面这段代码:

这段代码fork了两个进程,其中一个执行while(1)死循环在那里,另一个调用exit(0)退出,执行这段代码之后, 用ps可以查看到此进程的PPID(parent pid)变为了1。 其实这个问题很好解释,注意看 if 的判断,执行while(1)的并不是父进程,而是父进程fork出来的子进程,而父进程在子进程退出前就退出了,因而子进程变为孤儿进程,被init(pid=1)收养,所以就出现了上述的PPID=1的效果

 

第二个问题要注意的是关闭socket的操作,要在父子进程均关闭,只关闭子进程的socket对父进程同样fd的socket是没有影响的。

运行一下我们的benchmark程序,效果如下

可以看到,此性能相比单进程的已经好了很多,不过fork进程有个很致命的问题,当进程数过多的时候,fork会失败掉;以及进程切换开销会很大,fork进程数不受我们控制,导致fork进程过多系统load变高,系统性能下降

 

多线程版本

和多进程的类似,不过有一些地方需要注意,代码如下

相对于多进程版, 我们把子进程交互IO的逻辑拿到了handle_conn这个函数里进行处理,通过pthread_create创建线程,这里我们需要为每一个线程分配pthread_t结构,pthread_create创建的进程所拿到的函数参数是由(void *)指向的地址传递的。

在编写代码的时候遇到这样几个问题,

 

其实都是因为同一个原因导致的。下面把出问题的代码片段post一下

 

上面的代码就是把targs作为一个Stack上的变量,每一次循环不改变targs的地址,只改变targs的值。而多线程之间是共享同一个内存空间的,因而,每一次改变的都是同一个targs,所以不同线程的handl_conn看到的targs都是同一个,这不是我们期望要的效果,这里和多进程就不一样了,如果是多进程改变一个同名的变量,是通过COW技术修改同名变量的副本,而不是直接对父进程上的同一个变量进行修改。

 

运行上述正确的代码的benchmark如下

 

Linux 下C语言简单echo server的实现

Linux 下C语言简单echo server的实现

本文为 高性能网络编程学习 系列文章的第0篇

本文目的为讲述简单的echo server的实现, echo server的含义如字面意思, 客户端发送什么数据,服务器就返回什么数据作为响应,本系列文章的目的不在于实现复杂的服务器端业务逻辑,在于学习如何开发出高性能的网络通信程序,因而所有的代码均为echo server的实现

Linux下用C进行网络通信的话, 需要使用socket, 即Linux socket interface, socket是一种IPC(Inter Process Communication进程间交互)的方式,同时socket还可以允许跨host,即在不同的主机上的进程之间进行通信,在Linux上,所有的IO操作都是通过对文件的写入读出实现的,socket也作为一种文件存在,可以通过read, write进行IO

客户端我们采用的是一个benchmark程序,即1000个client,每一个client发送1000个请求,客户端为了简洁和快速编写,使用golang开发,之后所有的server都使用这个benchmark进行测试,代码如下

客户端程序不进行赘述,我们下面实现服务器端,通过socket监听一个端口并且与客户端进行读写交互有以下几个关键步骤

  1. socket创建
  2. 将socket绑定到指定的地址和端口
  3. 将socket变为listen状态
  4. 通过accept接受来自客户端的链接
  5. read/write与客户端进行交互

本例我们实现的是一个单进程的echo server, 下面是server的代码

如果是第一次写socket通信的代码,有几个地方容易卡住,下面把上述代码中几个比较容易出错和忘记的地方注明一下

整体流程

  • Create socket的时候使用socket()函数,具体用法查看man page,关于domain, protocol family的指定,man page中说的是“implementation-defined”想要找到所有的定义,Linux下需要查看 bits/socket.h文件
  • sockaddr_in 这个结构体用于表示internet (IPV4)的address, 这里使用的是sockaddr_in而不是sockaddr
  • 使用bind的时候注意addr结构体为sockaddr*,我们使用的是sockaddr_in需要进行类型转换,不用担心转换数据丢失或者错乱,这些结构体在实现的时候就已经假定用户使用时一定会用到强制类型转换的
  • bind之后,我们通过listen,使这个socket变为可以接受外界链接的状态
  • listen之后通过accept接受client的链接,每一个client都会产生一个新的文件以及fd,只需要对这个fd进行读写,就可以实现client和server的交互了
  • accept的时候需要注意一点,accept的参数中传递的socket应该为用于listen的socket,这个socket可以accept多个连接,accept返回的文件描述符对应的文件虽然也是一个socket,不过不可以用于accept多个连接,应该再这个链接断开时关闭掉相应的fd

关于sockaddr_in的参数以及初始化

  • 对于sockaddr_in结构体的addr的指定,指定其监听所有网卡的最常用的方式是 htonl(INADDR_ANY) 注意这里一定要用htonl来进行不同机器的兼容性转换,网络上的机器的架构,数据表示均不同,为了让数据能够在各个机器上被正确解读,我们需要用到几个宏(函数)对数据进行转换,具体参考man page byteorder,  这里的数据INADDR_ANY为一个长整型,因而使用htonl进行转换
  • 另外在多说一句htonl等一系列函数的记忆方法,他的全称就是 Host TO Network Long, 其他的htons ntohl这些就都可以记住了不会搞混
  • 关于addr以及port为什么要转换为network上的binary数据呢, 原因很简单,因为这些数据要在网络上传递,因此我们就需要做这个转换咯

运行性能测评

使用benchmark工具运行代码参数如下

运行结果为

 

运行在4C 8G 的ArchLinux上 可以看到有很多链接失败,日志里的原因为connection refused,完成1000个client的通信共用3min40秒,因而这种单进程单链接的代码是没有任何实际工程意义的(废话)

 

My Yearly Goals

My Yearly Goals

 

[DFS #0] HDFS论文解读

[DFS #0] HDFS论文解读

参考论文: The Hadoop Distributed File System

分布式文件系统HDFS作为HBase的下层服务,以及众多存储服务的支持项目,已经被广泛使用,下面通过解读上述论文来记录并介绍一下HDFS的架构,以及功能,还有设计

整体架构 Overview

如图所示

 

document_2016-12-17_23-04-40-1

 

每一个HDFS集群由一个NameNode,多个DataNode,以及BackupNode, CheckpointNode组成,上图中的三角形为使用HDFS的客户端,客户端和DataNode之间的线表示客户端到DataNode的物理距离,对于这个架构,有下面几点要说的

  • 一个集群的NameNode只有一个,客户端想要使用这个HDFS的时候,一定是先和NameNode进行交流,获得需要的DataNode信息之后,才能和DataNode进行通信
  • 因为NameNode只有一个,所以NameNode如果出现单点故障可就不好玩惹,而且对于存储这种要求数据一致性很高的系统,也不能容忍NameNode出故障系统就瘫痪这种情况,因而使用CheckpointNode和BackupNode对NameNode进行备份,他们的区别和备份方式下文会详细说
  • NameNode不会主动去和DataNode通信,他们之间的通信是这样的: DataNode每隔一定时间发送heartbeat到NameNode(默认3s)(通知NameNode他还活着可以干活=w=(划掉 ,然后NameNode在这个时候会将要发送到DataNode的指令通过Response的形式发送到DataNode
  • Client对数据的操作精简来看,无非是读写两种,当Client想要读取HDFS的数据的时候,首先Client先去询问NameNode,哪些DataNode有他要的数据,并且选择距离他最近的DataNode,然后Client直接连接距离最近的DataNode并获取数据 e.g: 如上图的ClientA想要获取一个数据,且数据在DataNode A B C内都有存放,那么ClientA首先要和NameNode进行通信,获得DataNode列表(ordered by distance to the client)然后选择距离它最近的DataNode B进行通信获取数据
  • 当Client想要向一个文件写入数据的时候(无论文件存不存在),其他的Client会无法对此文件进行写入操作(以此来保证文件不会因为同时写而corrupt data)写入完毕之后,会将Block进行Replica复制到不同的DataNode上(默认为复制3份),具体会在下面介绍

 

模块 & 术语

NameNode

HDFS Namespace ,是存储着所有DataNode以及每一个DataNode上的文件的元信息的一个目录结构,NameNode通过inode的方式来表示每一个文件和文件夹。Namespace 就可以理解为Linux的目录树的inode list,每一个inode指向一个datablock,datablock存在datanode上 HDFS将整个Namespace放在RAM内,完整的文件系统信息(包含inode信息,以及每一个inode对应的Datablock list 元信息)称为Image, Image持久存储在NameNode本地存储里,称为一个Checkpoint, NameNode同时维护了数据操作的日志journal

DataNode

显然,这就是数据真正存放的地方,每一个Datablock由两个文件组成,该Block的数据,和该Block的[checksum,创建时间]信息组成的元数据

如下图

document_2016-12-18_01-35-54

一个占3个Block的文件存放在DataNode里的情况简图,该文件有三个block,每一个block的信息含有Data & MetaData,三个Block组成了整个文件file1, 然后在NameNode的某一个inode存储着信息对应着文件名file1,block数3,物理位置在该DataNode上

DataNode的Startup

 

  • 当DataNode Startup时,会和NameNode进行handshake,进行namespaceID(每一个HDFS都有一个唯一的NamespaceID,在创建HDFS的时候就确定了)&version的认证,二者必须均与NameNode完全一致,如果不一致的话,该DataNode就会停止工作。保证了不会因为错误的NameNode或者软件版本不兼容而导致数据出问题,
  • 如果这个DataNode没有保存任何Namespace信息,说明这是一个新的DataNode,没有任何Block数据,因而允许加入此HDFS cluster。
  • 当DataNode握手成功之后,会register自己到该NameNode,如果为第一次加入该NameNode,则会被NameNode分配一个唯一的StorageID,并且永远不会改变,DataNode会将此ID持久存在自己的storage中,这个StorageID保证了就算IP变动或者端口变动,NameNode仍然可以识别该DataNode的身份

DataNode与NameNode的通信

  • Block Report: Block Report是DataNode用于通知NameNode自己所持有的Block的信息(如blockid,block大小,generation stamp)的一种数据,因为这个数据是会随时改变的,所以需要定期发送Block Report给NameNode,使得NameNode能得到整个Block分布的信息。当DataNode注册到NameNode时,会立即发送一个Block Report,之后会每隔一段时间(1h)发送一次BlockReport
  • HeartBeat:  DataNode通过定期的心跳与NameNode进行通信,使NameNode确认DataNode存活,并且同时carry其他信息,如存储可用量,目前正在传输的数据量等信息,用于NameNode进行负载均衡。 NameNode则在收到DataNode心跳,对DataNode进行Response的时候进行通信,可以发送的指令有:
    • 将自己持有的数据块复制到其他DataNode
    • 移除该DataNode上的数据块
    • 重新注册、或者停止该DataNode
    • 使DataNode立即发送一个Block Report

Block Scanner

  • 每一个DataNode都有一个BlockScanner,周期性地对Block的完整性进行认证
  • 当Block Corrupt的时候,Scanner会通知NameNode该Block Corrupt了
  • Verification Time存储在日志内,对Block的扫描顺序是按照VerificationTime早的先扫描进行排序的(待定)

HDFS Client

Client的作用就是用来Access HDFS的文件信息的,功能即为文件系统的基本操作

  • 读: 如上文所说,读距离Client最近的DataNode
  • 写: 通过流水线,写入三个DataNode

Image & Journal

  • NameNode上的全部元数据信息称作该NameNode的一个Image,Image的本地备份称作Checkpoint
  • Journal的写入方式是write ahead即在进行相应操作前,先写日志,保证数据强一致,对于每一个操作,首先进行记录,然后将日志flush & sync到磁盘,之后才会进行相应的操作
  • NameNode不会改变Checkpoint文件,每一次Checkpoint改变的时候,都是完整的重写,将原本的Checkpoint替换掉
  • Checkpoint的生成是在系统重启的时候,或者Admin手动操作,或者CheckpointNode自行生成
  • 因为flush & sync操作的耗时,该操作成为性能瓶颈,优化的方法是: 将所有的操作作为一批任务保存下来,当某一个thread触发了flush的时候,将全部thread的操作进行flush&sync(这样会不会不能保证数据的持久性?)
  • =A=因为Journal/Image丢了一个都会对存储造成影响,所以,将这些数据备份多份是一个好的选择,最好还要再NFS上备份一个

CheckpointNode & BackupNode

CheckpointNode

  • CheckpointNode就是一个对NameNode的快照,定期对NameNode的journal和checkpoint进行备份,并且将journal应用在Image上之后,清空journal,然后将新的Checkpoint发送给NameNode
  • CheckpointNode清理journal使得不会出现巨大的journal,巨大的journal文件出现corrupt的可能性较大,论文作者建议我们每天创建一个Checkpoint

BackupNode

  • BackupNode也会创建Checkpoint,具有和Checkpoint一样的功能,在此之上,BackupNode还可以实时的将NameNode Image同步到自己的内存中,保证了实时的同步性
  • 实现方式:BackupNode可以读取到NameNode的实时日志流,将日志存到本地,并且将日志里的每一个操作在自己的Image上进行操作
  • 当BackupNode创建Checkpoint的时候,不需要从NameNode下载Image和Journal,它本身就已经是和NameNode同步的了,只需要将Image存到本地,效率比CheckpointNode高很多
  • BackupNode可以看做是一个ReadOnly NameNode

Comparation

  • BackupNode具有CheckpointNode的全部功能
  • BackupNode还具有实时快照NameNode的功能,可以使Checkpoint的创建效率提升

Upgrade & File System SnapShots

系统升级的时候,数据丢失问题出现的可能性会增加,为了减小在升级的时候对数据损伤的可能性,HDFS引入了File System Snapshot,即一个全局的,唯一的,能够描述整个HDFScluster的所有信息的Snapshot

  • Snapshot在系统启动的时候Admin可以决定是否创建
  • 当选择创建Snapshot的时候,在NameNode启动时会对目前的NameNode Image生成Checkpoint,这个Checkpoint不会覆盖掉旧的Checkpoint,会存在另一个位置
  • 在DataNode与NameNode进行handshake的时候,NameNode通知DataNode,创建一个Snapshot,DataNode很显然不能复制每一个数据的内容一个副本,备份使用的方式是创建到每一个block的hardlink,当对数据进行修改时,使用COW,将被修改的数据生成一个新的副本,这样可以保证原来的所有block不会被修改
  • Rollback: 在系统重启的时候,Admin可以选择回滚到上一个Snapshot

功能及实现

文件读写

HDFS的Datablock在写入并关闭(Close the file)掉后不可以进行修改,除非这个修改是可以通过对原文件进行append就可以完成的, HDFS实现的为多读单写模型

  • 当客户端发起对数据的读的请求时,之前已经介绍了会找到最近的持有数据的Datanode进行读操作如果该操作失败,client会去读列表中的下一个Datanode
  • Checksum: 当Client读取一个Datablock时,会首先验证这个Datablock的Checksum是否和metadata吻合,如果不吻合,Client会通知DataNode当前读取的block corrupt了,并且去读取另一个DataNode上的该数据
  • 当读取一个正在被写入的文件时,Client首先询问最近的该文件的长度,然后再进行读取
  • Client读取一个正在被写入的文件时,被写入的那些修改HDFS不保证对Client可见

  • 当一个Client对文件进行写入的时候,该Client获得一个锁(lease),其他Client不能对此文件进行写入
  • 该锁有hardlimit和softlimit,正在写入的Client需要发送heartbeat到NameNode,未超过softlimimt时,该writer独占此文件的锁,超过softlimit之后,可以被其他client抢占此锁,超过hardlimit(即到了hardlimit writer也没有发送心跳也没有关闭文件,也没有人来抢占锁)HDFS认为client已经退出并且自动关闭文件,释放掉锁
  • 当写入需要分配新的Block的时候,NameNode会为该block分配一个新的blockID,并且决定哪几个DataNode会持有该Block的replica,这几个DataNode形成一个流水线(pipeline),然后Client通过向流水线写入数据,以TCPpacket的形式传递给每一个DataNode
  • Client发出一个packet不需要等待DataNode的ACK,可以继续发送packet,只要不超过packet窗口的大小即可
  • 正在被写入的数据HDFS不保证在Client读取的时候可以看到,不过可以通过hflush指令,将所有收到的Packet全部ACK响应之后,让数据对Client可见
  • 如下图,写入block数据的三个阶段
    • pipeline setup: Client将数据准备好,NameNode将要接受写入的NameNode准备好,并且建立pipeline
    • Data Streaming: Client传送packet到pipeline,DataNode对每一个packet发送ACK
    • close: 所有数据都已经发送完毕,ACK均接收到,关闭链接

pipeline

 

Replica管理

对于HDFS来说,每一个Block不能存储在所有的DataNode上一个副本,这样存储空间也十分有限,也造成了大量的不必要的重复和资源浪费,因而对Block的副本的管理就是很关键的一个问题,HDFS采用了如下的几个技术来对Replica进行管理

 

Block Placement

对于很大的集群,无法通过一个扁平的网络将其链接起来,而是将DataNode分布在多个rack里,一个rack里的所有Node使用一个交换机,rack与rack之间再通过交换机相连,构成树状结构。因而网络带宽在rack与rack之间和rack内部通信相比,通常是rack内的带宽更大。 HDFS给出的用于估计两个网络节点之间的带宽的方式是:将这两个节点到他们最近的公共祖先的距离加和

dn

如图,DN00 和 DN01之间的距离为2, DN00 和 DN12之间的距离为4

当DataNode注册到NameNode的时候,NameNode通过DataNode的IP以及设置好的脚本得出该DataNode在哪一个rack

Block replica的存放对于HDFS的数据的可靠性非常重要,默认的Replica placement policy是在cost和reliablility,以及bandiwidth 之间的一个tradeoff。具体的placement为:first replica on the node that client write to, second & third in a different rack , others randomly

Replication 管理

NameNode保证block的副本数是预期的数量,当过多or过少的时候,都会进行Replica Manage 通过多个策略对Block副本进行增加和删除,当Block过少的时候,会将该Block放入replica count越少优先级越高的优先队列里等待replicate,过多的时候则会根据一个策略在需要的时候对副本进行清理。

Balancer

HDFS的Block Placement策略没有考虑磁盘的利用情况,Block Placement策略的作用是防止数据集中分布在某几个DataNode上,却无法防止DataNode的磁盘利用不均匀的问题(可能出现一个DataNode有100GB数据,另一个却有1T数据)。Balancer就是为了解决此问题的工具

Balancer会对不均匀分布的数据进行移动,不过在移动的时候他也会保证满足Block Placement,即数据所分布的rack数不会减少,数据的replica不会减少,同时通过减少跨rack拷贝数据提高Balance效率

Balancer的带宽占用是可以config的,这样可以控制Balancer在不影响HDFS的正常运作的前提下移动数据

 

Decommission — DataNode退役

(即静默期)

  • 当一个DataNode被Admin标记为不能加入该集群(或者说退出该集群)时,该DataNode进入退役(Decommissioning)状态
  • 变为退役状态的DataNode不会再接收到Block Replica创建的请求,但是可以进行读取
  • NameNode会将该DataNode上的所有数据replicate到其他的DataNode上
  • 当Replicate结束时,这个节点进入到退役完毕(Decommissioned)状态,可以安全的移除