gorouter源码分析

本文严格意义上讲,也不能算是原创,大量参考了sslazww的文章,并在其基础上,加上自己的理解。主要是更多的源码解释。


gorouter

  The originalrouter is backed by nginx, that uses Lua code to connect to a Ruby server that-- based on the headers of a client's request -- will tell nginx whick backendit should use. The main limitations in this architecture are that nginx doesnot support non-HTTP (e.g. traffic to services) and non-request/response typetraffic (e.g. to support WebSockets), and that it requires a round trip to aRuby server for every request.

  First, with full control over every connection to therouter, it can more easily support WebSockets,and other types of traffic (e.g. via HTTP CONNECT). Second, all logic iscontained in a single process, removing unnecessary latency.

 1 gorouter功能概述:

gorouter的功能主要可以分为三个部分

1. 1       负责接收Cloud Foundry内部组件及应用uri注册以及注销的请求

  CloudFoundry内一个组件需要提供HTTP服务的时候,那么这个组件则必须将自己的uriIP一起注册到gorouter处,典型的有,CloudFoundryServiceGatewayCloud Controller通过HTTP建立连接的,另外Cloud Controller也需要对外提供HTTP服务,所以这些组件必须在gorouter中进行注册,以便可以顺利通信或访问。

       除了平台级的组件uri注册,最常见的是应用级的应用uri册,也就是在CloudFoundry中新部署应用时,应用所在的DEA会向gorouter发送一个uriIPport的注册请求。gorouter收到这个请求后,会添加该记录,并保证可以解析外部的URL访问形式。当然,反过来,当一个应用被删除的时候,为了不浪费Cloud Foundry内部的uri资源,CloudFoundry会将该urigorouter中注销,随即gorouter在节点处删除这条记录。

1.2        负责转发所有外部对CloudFoundry的访问请求

  gorouter接受到的访问请求大致可以分为三种:

   外部请求有:用户对应用的访问请求,用户对Cloud Foundry内部资源的管理请求;

   内部的请求有:内部组件之间通过HTTP的各类通信

  虽然说请求的类型可以分为三种,但是gorouter对于这些请求的操作都是一致的,找到相应的uri,提取出相应的IPport,然后进行转发。需要注意的是,在原先版本的router中,router只能接收HTTP请求,然而现在gorouter中,已经考虑了TCP连接,以及websocket

 

1.3     负责提供gorouter作为一个组件的状态监控(Varz

Cloud Foundry都有自己的状态监控,可以通过HTTP访问。这主要是每个组件在启动的时候,都作为一个componentCloudFoundry进行注册,注册的时候带有很多关于自身组件的信息,同时也启动了一个HTTP server


2 gorouter源码分析

2.1 基本框架介绍

  common文件夹定义varz,healthz,common,component,duration,

  proxy文件夹作为代理,处理外界进入CF的所有请求

  router/main.go程序入口,main函数router.newRouter(c).Run()

  registry.go处理组件或者DEA应用向gorouter注册uri的事件。负责请求访问应用时查找应用真实IP,port。

stats:主要负责一些应用记录的状态,还有一些其他零碎的东西,比如定义一个堆util:其中一般是工具源码,在这里只负责给gorouter进程写pid这件事。

varz.go主要涉及varz信息的处理,其实就是gorouter组件状态的查阅

router.go定义router的数据结构,实例初始化的过程,还有最终运行的流程

Router结构体:*Config,*Proxy,*nats.Client,*Registry,*Varz

其中varz处理gorouter自身作为一个组件的状态监控

1 newRouter时,分别对变量赋值,

r.establishNATS(与nats通信), 创建一个nats.Client

创建Component(“Router”),并vcap.Register(component,r.natsClient)

 

2 run(Router*)方法,

订阅“router.register”,订阅”router.unregister”,

send_start_message发布”router.start”消息。

scheduleFlushApps刷新活跃的app。

  等待一个start信息的发送时间间隔,已保证router的registry中已有路由信息,以便之后在代理外部请求的时候,可以找到路由表的映射关系。gorouter会将组件或者应用的uri注册信息存放在该自身的内存中,而gorouter关闭的时候,映射表中所有的信息丢失,每当重启的时候,需要通过发送一个start消息,然后靠订阅该消息的组件重新注册uri,从而获取所有的路由关系.

  以tcp的方式监听(l=net.Listen)本机一个端口,写pid文件,创建proxy.Server,server.Server(l)。

  最终创建一个协程来执行这个server服务于刚才创建的Listen对象。

  SubscribeRegistry接收其它组件的注册消息,并调用subscribeRegisry(“router.registry”),将注册信息记录到registry表中。

 

2.2 registry模块源码分析

  registry模块接管的是CloudFoundry中组件及应用的uri注册或者注销请求。从计算和存储的角度来分析该模块,即可发现该模块完成了请求的处理和自身内存路由表的设计与维护。

1 Registry结构体,sync.RWMutex,*ActiveApps, *TopApps,

  byUri(map[Uri][] *Backend)是一个map,key为Uri类型,value为[]*Backend是个数组。一个uri对应一组端口号。这个就是路由表!

  byBackendIdmap[BackendId]*Backend其中key为BackendId,value为*Backend

  Backend结构体:backend_id,host, port, app_id

  staleTracker *util.ListMap结构体:*List, map[interface{}]*Element

 

2 NewRegistry创建Registry,对实例赋值

 

3 (*Registry)RegisterUri (*Backend, Uri)调用backend.register(uri),

  r.byUri[uri] =append(old_r.byUri[uri],backend)

  backend.register:如果未注册,backend.uri= append(backend.uri,uri)

 

4 (*Registry)Registry(*registryMessage)

registryMessage结构体:host,port,uris,app,

r.lock, deferr.lock在register执行完毕后,由go语言完成registry对象。

对registryMessage中的每一个uri,调用r.registeryUri(backend,uri)

r.staleTracker.PushBack(backend)添加路由

 

5 pruneStaleDroplets剪枝?删除过期的droplet

  对于dropletStaleThreshold超时的droplet,调用r.unregister(backend,uri)

  对于需要注册的(uri,backend)组合,首先查看table属性中能都找到键为key的记录,如果找到,那说明该key(实为IP+port,uri的组合)已经存在于table中,所以将table中的记录赋值于endpointToRegister;如果没有找到,那说明该key还未存在于table中,属于一个全新的key,需要在table中相应的记录,则首先用请求中的backend赋值给endpointToRegister,然后在通过backend创建一个endtry对象,并使用语句:registry.table[key]= entry来实现最终在table中的存储。

 

lookup(host)方法,随机返回一个r.byUri[Uri(host)]中的backend

lookupByPrivateInstanceId(host,p_instanceid)返回一个指定的backend

2.3 proxy模块源码分析

2.3.1 server部分

  作为一个路由节点,proxy是其最为重要的功能,registry这样的模块,其实也是为了能够服务于proxy。对于CloudFoundry来说,所有通过uri访问Cloud Foundry内部资源的请求,都需要路由节点gorouter作代理。gorouter的proxy模块,首先监听底层的网络端口,然后再将端口发来的请求进行uri解析,最终将请求转发至指定的Cloud Foundry内部节点处。

  proxy模块,从代理流程来讲,proxy模块可以认为是一个方向代理server端,接收所有从nginx发来的请求,并把请求转发至Cloud Foundry内的某些组件处。从实现方式来看,proxy模块建立一条nginx发来请求的连接,根据请求的内部具体信息,做相应的HTTP处理,最终构建该请求的response信息,并通过刚才的连接,将response信息返回给Nginx,当然Nginx最后也会把请求返回给发起请求的用户。其中,刚才提到的相应的HTTP处理,也就是如何将请求发给CloudFoundry内的某些组件,并接收返回的信息。

  proxy模块可以分为server端的实现与proxy代理流程的实现:

proxy/server.go:

 conn结构体,服务器端的HTTP连接,代表一条连到proxy模块中server上的连接,或者说是从nginx到gorouter的连接。

 remoteAddr,*Server,net.Conn,*LimitedReader,*ReadWriter,hijacked布尔型

  Request结构体,*http.Requese,*response请求对象

  response结构体,*Conn,http.header,代表从server端返回去的一条response的HTTP信息,其中包括这条返回信息返回时的承载的连接,还有很多关于该HTTP响应的属性值。

  Server结构体,Addr,http.Handler,timeout, max_head_bytes

代表接收请求,转发请求的server端,其中包含一个远程地址,另外还有一个非常重要的handler对象,用来处理HTTP请求. handler其实就是proxy.go文件中定义的proxy结构体。

 

 请求流程

server的函数Server(listener),

  该Serve()函数的发起者为Server实例对象,传入的参数为对端口的监听对象。在执行该函数的时候,defer方法显性的定义了关于函数执行完毕后所需要处理的后续工作。

  服务器端需要不断轮询端口,并对端口处发来的请求进行相应的处理。在这里的代码实现即为一个for循环,在该循环中,首先server实例对象accept一个连接。  首先作为一个server端,先去监听listen,某一个端口,然后去accept这个端口发来的连接请求,也就是说,一旦有连接请求发来的话,server便会去accept该请求;然后作为一个client端,所需要做的操作就是去给server端的某一端口发送连接请求,如果有server监听了这个端口,那么它可以accept该连接请求;最后双方可以通信。

  server通过代码 rw,e := l.Accept() 实现对监听端口请求的接受;然后server再对这个net.Conn类型的rw,进行处理,最后生成一个新的连接,也就是上面涉及到的conn结构对象(c=server.newConn);接着,server创建一个协程来完成这条连接上的请求。可以发现的是,由于在gorouter中server对象只有一个,所以所有的请求都是经过这个server的,那在for循环中,server会接受很多的请求,创建很多的连接,然后对于一个连接上的请求,又会创建一个协程来完成,如果不借助协程的高并发处理能力,几乎不能应对大负载。真正处理请求的时候,是在goc.serve()处,其中go代表这开辟一个协程,c.serve()则是处理的具体实现。

  func (c*conn) server()

  defer关键字依旧是表示随后定义的函数是做来为serve()方法作善后处理。接着是一个for循环,在该for循环中,首先从连接中读取一个请求,然后对该请求的某些属性进行查阅并处理,接着创建一个handler,最后由该handler来处理HTTP请求,并结束一个请求,如果该请求是一个一次连接,那么关闭连接。如果该请求处于长连接上,则继续for循环的下一次迭代,继续从连接中读取请求并处理

  req, w, err := c.readRequest(), 其中w为response。该实现,返回两个对象,一个为读取的请求,另一个为需要生成的response。

  执行该函数的时候,首先通过http的函数ReadRequest()来实现从连接c中读取请求,然后通过该请求,分别创建一个request对象和response对象。需要注意的是代码w.conn=c,也就是在说创建reponse对象后,对象属性初始化时,将response的连接属性conn,依旧赋值为c,那么server将该请求转发给Cloud Foundry内部组件处理后收到回复,并对回复再进行处理,来完成这里的response重写后,依旧通过之前的连接发回去。

  在serve()函数可以看到,在读取连接请求readRequest()后,对请求进行一些处理之后,会创建一个handler对象来实现HTTP请求的处理,该部分的实现在proxy.go文件中,简单来讲就是给后台做代理,将请求发给后台,并接收后台的回复。

  当获得后台的响应请求后,server随即执行finishRequest()函数,其主要的功能就是将返回的后台回复,写入需要返回给用户的response对象中。然后判断response对象中的属性closeAfterReply,如果为真,则表示之前的请求是一个一次请求,该请求表明,自身发出之后接收到回复之后,不会再发起请求,就算有,也是再创建一个连接来实现,所以程序跳出for循环,关闭连接;如果为假的话,那说明请求需要在一条长连接上进行操作,换言之,在请求的回复发给用户后,用户还会有请求通过这条连接发给server,这样的话,无需关闭连接,只需有server继续对这条连接执行readRequest()函数即可。

  2.3.2 proxy部分

       proxy部分做的工作要比server部分少一些,它主要的工作就是解析请求的uri和转发请求。

proxy.go:

Proxy结构体:sync.RWMutext, *Config, *Registry,Varz,

Lookup(*http.Request)解析请求的uri,调用Registry.Lookup查路由表

  找到请求中的host,然后对于该请求,检查是否有StickyCookieKey,如果有的话,直接从中获取sticky,再通过uri和sticky.value的组合找到请求的backend。这里稍微解释一下StickyCookieKey的作用。一旦一个请求中含有该cookie,而且能被解析到相应的uri和sticky值,也就是说这个请求,希望被处理的时候,能继续被上次处理过这个用户发出的请求的appinstance上,这样的话,可以避免一些不必要的数据冲突等,或者减少DEA中appinstance的负载。如果没有找到cookie的话,那么proxy就老老实实通过host来找到相应的ip:port,如果一个host有多个instance实例的话,proxy会通过某种策略(随机)来决策由哪个Instance来服务。

ServeHTTP()实现转发请求--- called by server.go/serve()方法

  首先调用p.Lookup(req)去解析请求,

  构建一个responseWriter并初始化某些属性,判断请求的类型并分别处理(TCP、websocket和HTTP),若为HTTP类型则通过http.DefaultTransport.RoundTrip方法发送请求并接收响应,最后填写reponseWriter和设置cookie:

(http.setCookie(rw,cookie))。

 p.CheckWebSocket检查连接是否为WebSocket类型。如果是,p.ServeWebSocket,Dialbackend,建立tcp连接

 

3 杂记

cf file app_name

cf push时,都会用到router转发。

 

4 参考文献

http://blog.csdn.net/shlazww/article/details/11974411强烈建议阅读该作者的文章,相当详细,受益匪浅!


评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值