设为首页收藏本站

数码鹭岛论坛

 找回密码
 注-册

QQ登录

只需一步,快速开始

搜索
查看: 4220|回复: 0
打印 上一主题 下一主题

轻量的分布式服务框架 Skynet

[复制链接]
跳转到指定楼层
1#
发表于 2013-5-16 22:48:27 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
项目地址: [url=https://github.com/mikespook/skynet/wiki/%E8%BD%BB%E9%87%8F%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E6%9C%8D%E5%8A%A1%E6%A1%86%E6%9E%B6-Skynet/74ab121b9ac12fd4e17796c2ec829dd2b827d49d]https://github.com/mikespook/skynet/
原文: [url=https://github.com/mikespook/skynet/wiki/%E8%BD%BB%E9%87%8F%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E6%9C%8D%E5%8A%A1%E6%A1%86%E6%9E%B6-Skynet/74ab121b9ac12fd4e17796c2ec829dd2b827d49d]https://github.com/mikespook/skynet/wiki/%E8%BD%BB%E9%87%8F%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E6%9C%8D%E5%8A%A1%E6%A1%86%E6%9E%B6-Skynet/74ab121b9ac12fd4e17796c2ec829dd2b827d49d

概述随着互联网的持续发展和近年来移动互联网的爆发,越来越多的团队和个人进入这个领域创业。这类创业者、开发者经常会陷入高昂的基础投入与业务高速扩张的矛盾中。所幸的是,云技术的兴起,让许多初创团队找到了合理的平衡点。纷纷选择云托管、云服务、云存储等作为开展业务的基础平台。这时,技术架构的平滑扩容能力就成为制约业务扩张速度的瓶颈。尤其是当系统作为一个单一的大型应用构建时,如何在不影响其他模块的情况下,将系统的某些子模块平滑的迁移到新的主机上,就变成了一件极为具有挑战性的工作。
从技术上说,有许多办法解决这一矛盾。将应用打散为更小的组件,使得对某些部分的修改独立于系统的其他组件。并且通过允许在多个服务器上部署多个服务实例的方式使得系统获得较好的扩容能力。但是当系统被拆分为相对独立的若干个组件后,必定会遇到一个巨大的问题:如何组织和管理这些服务。分布式服务框架就是能够有效解决这一困难的技术方案之一。但是,一方面许多传统分布式服务框架如企业服务总线过于“笨重”,对软硬件环境有着极为严格的要求。另一方面,像 Hadoop 子项目 Zookeeper 这样的分布式框架,由于其持久化数据方面的要求,也不能很好的工作在像 AWS 这样的云平台上。
因此许多团队都在尝试用不同的技术手段全新研发或在已有技术体系上改造自有的分布式服务框架,并在这些框架的基础上构建高可用、高扩展的服务集群。
由 Brian Ketelsen 于 2011 年 6 月发起的 [url=https://github.com/bketelsen/skynet]Skynet 是使用 [url=http://golang.org]Go 语言编写一个分布式服务框架,该框架设计初衷是 用于构建大规模分布式应用的服务通讯框架。截止 2012 年 9 月,该项目有 6 人全职维护。并且在 Brian 创立的提供信用卡信息服务的公司中,已经有两个数据中心部署了 Skynet 集群,用于提供信用卡相关服务。其中最大的应用已经使用超过 50 个 Skynet 独立服务。
Skynet 提供了统一的配置管理与监控服务,使得客户端不再担心服务在哪里,是否已经宕机。同时其内部的连接池会自动对负载进行均衡,以便适配集群中的变化。虽然 Skynet 是采用 Go 编写的,但作为分布式框架,并不仅限于 Go 语言。它的通信协议标准可以在多种语言环境下使用。
为了构建高可用的服务集群,Skynet 重度依赖 [url=http://zh.wikipedia.org/wiki/Paxos%E7%AE%97%E6%B3%95]Paxos 算法的实现:Doozer。其设计思想非常类似 Zookeeper,但更为轻量,适合用于 AWS 这样的构建于虚拟化的云平台上。它用于存储小量、极端重要的数据,保证了高可用和完全一致性。当数据变化时,它立刻通知接入的客户端(无缓存)。对于那些很少更新,但是希望更新发生时实时性高的客户端来说是非常理想的。它的[url=https://github.com/ha/doozer]原始版本由 HeroKu 的工程师开发并开源,但是由于这个版本已经差不多已经 9 个月没有更新和维护,因此 Skynet 使用的是 Doozer 众多分支中的一个相对[url=https://github.com/4ad/doozerd]稳定的版本。
本文将从零开始,介绍如何安装、配置基于 Go 语言开发的 Skynet 框架。并通过具体的例子展示如何为该框架编写服务并进行调用。
下面我们就来了解一下如何安装、部署一个 Skynet 框架。并按照其规范编写运行在框架上的服务,并调用这些服务完成工作。
安装与部署Go 编译环境的设置Skynet 和 Doozer 都是 Go 语言开发的,同时例子中的 Skynet 的服务和客户端也需要使用 Go 语言进行开发。因此需要有一个能够编译 Go 代码的开发环境。关于 Go 语言的安装,[url=http://golang.org/doc/install/source]语言官方网站和网络上已经有大量的[url=http://www.mikespook.com/2012/02/%e7%bf%bb%e8%af%91go-%e7%8e%af%e5%a2%83%e8%ae%be%e7%bd%ae/]相关资料。不过为了更加清晰,这里简要说明一下安装步骤。需要注意的是,本文所有操作将假设在 POSIX 系统的环境下进行。
# 创建目录# 可将 $HOME/bin 添加到 $PATH 中mkdir $HOME/binmkdir $HOME/go/gomkdir $HOME/go/ownmkdir $HOME/go/3rdpkg# 下载源码hg clone -u release https://code.google.com/p/go# 设置环境变量,也可以加入 .bashrc 等配置文件中export GOROOT=$HOME/go/goexport GOBIN=$HOME/bin# $HOME/go/3rdpkg 放在第一个,将使得 `go get` 自动下载到这里export GOPATH=$HOME/go/3rdpkg:$HOME/go/own:$GOROOTexport GOTOOLDIR=$GOROOT/pkg/tool# 编译 go 工具链cd $GOROOT/src/./all.bash
在执行完上面的命令后,会有一个大约 3-10 分钟的编译、测试过程。当你在屏幕上看到类似:
Installed Go for linux/386 in /home/mikespook/go/go
Installed commands in /home/mikespook/bin
这样的信息的时候,就表示你的 Go 工具链已经安装完毕。
安装与部署在完成了 Go 工具链的编译安装后,强大的 go 命令将使得 Skynet 和 Doozer 的部署异常简单。
首先下载 Doozer 和 Skynet 的代码和相关依赖。
# 安装 Doozer 和依赖库go get code.google.com/p/goprotobuf/protogo get github.com/kr/prettygo get github.com/4ad/doozergo get github.com/4ad/doozerd# 安装 Skynet 和依赖库go get github.com/kballard/go-shellquotego get github.com/sbinet/linergo get github.com/bketelsen/skynetgo get labix.org/v2/mgo/bson
Skynet 有三个独立的命令提供服务:
  • sky - Skynet 命令行/控制台工具
  • skydaemon - Skynet 守护进程服务
  • dashboard - Web 界面的控制台工具(尚未完成,仅有基本的信息查看功能)
用下面的命令分别编译并安装以上三个可执行文件到 $GOBIN:
cd $HOME/go/3rdpkg/src/github.com/bketelsen/skynet/cmd/skygo installcd $HOME/go/3rdpkg/src/github.com/bketelsen/skynet/cmd/skydaemongo installcd $HOME/go/3rdpkg/src/github.com/bketelsen/skynet/cmd/dashboardgo install
Doozer 有两个独立的命令提供服务:
  • doozerd - Doozer 守护进程服务
  • doozer - Doozer 命令行工具,仅用于测试
用下面的命令分别编译并安装以上两个可执行文件到 $GOBIN:
cd $HOME/go/3rdpkg/src/github.com/4ad/doozerdgo installcd $HOME/go/3rdpkg/src/github.com/4ad/doozer/cmd/doozergo install
执行完以上命令后,在 $GOBIN 中,也就是 $HOME/bin 中应当出现 5 个可执行文件 sky、skydaemon、dashboard、doozerd 和 doozer。
如果希望 Skynet 将日志记录到 [url=http://www.mongodb.org]MongoDB,那么还应当安装该服务器,并创建一个独立的 collection 用于日志存储。不过这并不是必须的。
建立集群并运行Skynet 的集群是建立在 Doozer 集群的基础上。运行一个 Doozer 集群,需要首先启动一个初始节点,然后将其他节点加入其中。或者使用 Doozer Name Service (DzNS)。
不使用 DzNS 的 Doozer 集群,在启动了初始节点后需要使用 doozer 命令添加节点槽位,以便让节点接入集群并工作。在初始节点启动完成后,任何时候都可以向集群增加新的节点。所以不论是先增加槽位,再附加节点;还是先附加节点,再增加槽位都是可以。
该过程大致如下:
# 启动一个集群 foobar,监听在 127.0.0.1:9046# 默认会提供一个 Web 页面 http://127.0.0.1:8000 查看集群信息doozerd -l=":9046" -c="foobar"# 向集群 foobar 添加节点,`-a` 参数指定了初始节点监听的 TCP 地址。# 在有槽位使用之前,节点都处于连接并等待的状态doozerd -a=":9046" -l=":9047" -w=false -c="foobar"# 增加节点槽位printf "" | doozer -a="doozer:?ca=:9046" set /ctl/cal/1 0# 先增加节点槽位,再附加节点printf "" | doozer -a="doozer:?ca=:9046" set /ctl/cal/2 0doozerd -a=":9046" -l=":9048" -w=false -c="foobar"
Doozer 服务自身并未提供守护进程。可以使用 nohup 执行以上节点启动命令。在以上命令执行完成后,可在浏览器中访问 [url=http://127.0.0.1:8000]http://127.0.0.1:8000 。可以看到全部节点都在工作中,并且有心跳包通讯。这种集群建立方式存在一个严重的单点隐患:*当初始节点宕机后,无法再向集群附加新的节点。*
可以使用 doozer 命令添加节点到 /ctl/ns/foobar/* 目录下,使该节点成为一个初始节点。
printf "127.0.0.1:9048" | doozer -a="doozer:?ca=:9046" set /ctl/ns/foobar/4YEKU3WP6P7WHRRL 0
这样一来,向 :9046 或 :9048 增加节点,都可以接入 foobar 这个集群。
现在可以用 doozer 命令操作这个集群存取数据或监视某个数据的变化。这部分内容与本文并无直接联系,因此不再赘述。但是理解 Doozer 的工作原理和使用过程对于理解 Skynet 框架很有帮助,所以还是建议大家对此做进一步的尝试和探索。
在频繁变更的业务集群中,为了去除单点隐患,手工维护 /ctl/ns/cluster_name 中的内容是一件繁琐而麻烦的事情。因此 DzNS 应运而生。
DzNS 也是一个 Doozer 集群。它有两个作用:a. 用于其他 doozerd 进程发现并加入已有集群;b. 创建新集群时,决定哪个节点作为初始节点。新启动的 doozerd 首先会连接到 DzNS。然后查询 /ctl/boot/<name> 中的地址。如果地址不存在,则尝试根据设置创建集群,并将自身作为初始节点。与之前一样,要让一个接入到集群的节点工作,必须为其在集群中设置槽位。槽位的设置也可以使用 DzNS 来完成。
例如,可以使用以下命令,建立一个新的集群 skynet,并将上面建立的集群 foobar 作为新集群的 DzNS。
# `-b` 参数指定了 DzNS 的 Doozer Uridoozerd -l=":8046" -b="doozer:?ca=:9046&:9047&:9048" -w=":8001" -c="skynet"doozerd -l=":8047" -b="doozer:?ca=:9046&:9047&:9048" -w=false -c="skynet"# 利用 Doozer Uri 就可以通过指定名字 `cn=skynet` 来操作集群printf ""|doozer -a="doozer:?cn=skynet" -b="doozer:?ca=:9046&:9047&:9048" set /ctl/cal/1 0
至此,Doozer 的集群建立就完成。
Skynet 的集群建立相比 Doozer 要简单许多。执行命令 skydaemon 将其连接到 Doozer 集群即可。多次运行 skydaemon 可以启动多个实例。Skynet 会根据参数 -l 来控制用于 RPC 调用的监听端口。如果业务 Doozer 集群没有监听在默认端口 8046 上,可以用参数 -doozer 指定 Doozer 集群的地址。
启动了 Skynet 后,即可使用 sky 命令部署指定的服务。但是在部署之前,应当已经使用 go install 按照规则将服务端的可执行文件安装在正确的目录下。
sky deploy github.com/bketelsen/skynet/examples/testing/fibonacci/fibservice
服务启动后,便可以使用客户端调用对应的服务。
集群最终结构当集群配置完成后,架构概念图如下:

需要特别说明的是,为了保证服务的稳定,建议手工将 DzNS 的所有 Doozer 节点都添加到 /ctl/ns/foobar/ 下。这样只要任意 DzNS 的节点还能服务,集群就可以保持正常的工作。并且,应该在有条件的情况下,将各个节点分布到不同的服务器中。扩容时只要根据业务需要增加业务集群和 SkyDaemon 节点即可。
编写服务分布式框架本身来说,是作为基础设施出现在产品的技术架构中。为了满足产品的业务需求,就要根据实际需求开发相应服务。下面将介绍如何使用 Go 语言开发 Skynet 服务。
为了便于说明,这里假设一个服务场景:在一个大型系统中,需要向用户提供消息发送服务。消息发送服务包括SendMail、SendSMS 和 SendMsg,分别表示电子邮件、手机短信和站内消息。每个消息都含有发送人 Sender:string、接收人 Receiver:string,还有消息内容 Data:string。
初始设定在目录 $HOME/go/own 下新建一个目录叫做 msgservice。并在该目录下新建 go 代码文件 msgservice.go。
Skynet 的每个服务都是独立的可执行文件,因此使用 main 作为包名。为了能够正常的与 Skynet 通讯,服务代码需要引入两个 Skynet 提供的包。
package mainimport (        "github.com/bketelsen/skynet"        "github.com/bketelsen/skynet/service")
框架接口对于 Skynet 的服务来说,必须实现 service.ServiceDelegate 接口。从这个接口的名字可以看出,这里用到了委托的模式。接口原型如下:
type ServiceDelegate interface {    Started(s *Service)    Stopped(s *Service)    Registered(s *Service)    Unregistered(s *Service)}
从接口的方法名就可以知道其含义:在启动、停止、注册和注销的时候,以回调的方式分别调用这四个方法。
服务逻辑由于业务的需要,希望服务将向外发送消息的过程不会阻塞客户端的请求。也就是说,客户端只需要确认将消息发送到服务,而无需关心消息最终发送的时间和结果。这里将利用 Go 语言的 channel 开发一个队列池。
type MsgRequest struct {    Sender, Receiver, Data string}type MsgResponse struct {    Code int}type MsgService struct {    mail, sms, msg chan *MsgRequest}func (s *MsgService) Registered(service *service.Service)   {}func (s *MsgService) Unregistered(service *service.Service) {}func (s *MsgService) Started(service *service.Service)      {    go s.sendingMail()    go s.sendingSMS()    go s.sendingMsg()}func (s *MsgService) Stopped(service *service.Service) {    close(s.mail)    close(s.sms)    close(s.msg)}func (s *MsgService) sendingMail() {    mail = make(chan *MsgRequest, 128)    for req := range s.mail {                // 调用 SMTP 发送邮件        // 省略代码若干...     }}func (s *MsgService) sendingSMS() { // 省略代码若干... }func (s *MsgService) sendingMsg() { // 省略代码若干... }
在服务启动时,分别启动了三个 goroutine 用于发送邮件、短信和站内消息。这三个 goroutine 在后台不断从 channel 中取出消息请求然后发送。当 channel 中没有消息时,则阻塞等待。
服务接口实现在实现了 service.ServiceDelegate 接口后,接下来需要实现 SendMail、SendSMS 和 SendMsg 三个服务。Skynet 的服务要求符合原型:
func (ri *skynet.RequestInfo, req interface{}, resp interface{}) error
interface{} 相当于 C 语言的 void *,那么可以将它具体到某个指针类型上去。因此三个服务可以定义为:
func (s *MsgService) SendMail(ri *skynet.RequestInfo,    req *MsgRequest, resp *MsgResponse) error {    s.mail <- req    resp.Code = 0    return nil}func (s *MsgService) SendSMS(ri *skynet.RequestInfo,     req *MsgRequest, resp *MsgResponse) error { // 省略代码若干... }func (s *MsgService) SendMsg(ri *skynet.RequestInfo,    req *MsgRequest, resp *MsgResponse) error { // 省略代码若干... }
主函数Skynet 的服务是作为一个独立的进程运行,通过网络与集群通讯。发布服务就需要一个可执行文件,在 Go 语言中也就是需要一个 main.main 函数。
func main(){        s := &MsgService{}        config, _ := skynet.GetServiceConfig()          config.Name = "MsgService"          config.Version = "1"          config.Region = "Development"        service := service.CreateService(s, config)        defer func() {                service.Shutdown()        }()        waiter := service.Start(true)        waiter.Wait()}
这部分代码相当容易理解,不过有个地方需要额外需要说明一下。在代码 waiter := service.Start(true) 中的 bool 参数,设置为 true 表示无需再调用 service.Register(),服务将会自动注册;设置为 false 表示服务接入集群后需要在代码中调用 service.Register() 注册服务以便让服务可用。这给我们提供了更小粒度控制服务的可能。在复杂的业务场景中,比如在服务依赖或一个初始化过程很长的服务做热切换时相当有用。
部署与运行前面已经提到了,使用 sky 命令部署服务。实际上每个服务都是一个独立的可执行文件,在开发和测试时,在命令行手工执行来启动它会更加方便。
go build./msgservice
当输出为下面的内容时,表示我们的服务已经能够正常使用。
skynet: 2012/09/12 14:24:48 Created service "MsgService"
skynet: 2012/09/12 14:24:48 Registered methods: [SendMail, SendSMS, SendMsg]
skynet: 2012/09/12 14:24:48 Service "MsgService" listening on 127.0.0.1:9000
skynet: 2012/09/12 14:24:48 Service "MsgService" listening for admin on 127.0.0.1:9001
skynet: 2012/09/12 14:24:48 Connected to doozer at 127.0.0.1:8046
skynet: 2012/09/12 14:24:48 Discovered new doozer WE7CFFRW4CGMYL5O at 127.0.0.1:8046
客户端调用Skynet 的设计目标是一个同语言无关的分布式集群框架,那么也就意味着客户端可能是其他语言开发的。实际上在几个月前的 Skynet 的源码中提供了 Ruby 调用 Go 编写的 Skynet 的服务的例子。但后来因为框架重构的原因,将其移出了版本库。如果想要了解 Skynet 的协议描述,可以阅读源代码目录下的 protocol.md 文件。可以根据这个协议描述编写其他语言的客户端库。而在本文中,为了简化,仍然使用 Go 语言作为客户端的开发语言。
下面就来了解一下如何调用上面编写的服务来通过不同方式发送消息。
初始设定在目录 $HOME/go/own 下新建一个目录叫做 msgclient。并在该目录下新建 go 代码文件 msgclient.go。
为了能够正常的与 Skynet 通讯,客户端代码需要引入两个 Skynet 提供的包。
package mainimport (    "github.com/bketelsen/skynet"    "github.com/bketelsen/skynet/client"    "fmt")
数据结构同服务端一样,需要在客户端定义请求和响应的数据结构。实际上,这里可以将 MsgRequest 和 MsgResponse 放在一个独立的包中,使程序更容易维护。关于具体的做法,可以参考 Skynet 代码里 examples/testing/ 目录中的例子。同样这里为了简化,直接在客户端代码中声明了这两个类型。
type MsgRequest struct {    Sender, Receiver, Data string}type MsgResponse struct {    Code int}
主函数客户端的工作逻辑是,根据客户端的配置(环境变量和命令行参数)连接到 Skynet 集群;然后向集群查询所需的服务;使用查询到的服务发起服务调用请求;最后检查错误并处理响应结果。
代码如下:
func main(){    config, _ := skynet.GetClientConfig()    client := client.NewClient(config)    service := client.GetService("MsgService", "1", "Development", "")    // 发送电子邮件    req := &MsgRequest {Sender: "me@local",        Receiver: "foobar@host", Data: "Blablabla..."}    resp := &MsgResponse{}    err := service.Send(nil, "SendMail", req, resp)    if err != nil { fmt.Println(err) } else { fmt.Println(resp.Code) }    // 发送站内消息    req = &MsgRequest {Sender: "user1",        Receiver: "user2", Data: "Blablabla..."}    resp = &MsgResponse{}    err = service.Send(nil, "SendMsg", req, resp)    if err != nil { fmt.Println(err) } else { fmt.Println(resp.Code) }}
代码与其他系统的 RPC 调用大致相似,需要注意的是 Skynet 的服务调用是同步的。这也就是在文中的服务端实现为什么要通过 channel 的方式向 goroutine 发送调用请求。如果不但要用异步的方式调用服务,还需要接收服务返回的结果,可以将其拆分成两个独立的服务,如 Foobar、GerFoobarResult。然后使用服务接口的第一个参数 skynet.RequestInfo 中的 UUID 即可判断请求和其结果的对应关系。这里不再赘述,请聪明的读者设计实现吧。
总结Skynet 以其轻量的架构,合理的功能为互联网应用提供了良好的稳定性与扩容能力。尤其适用于以云平台、VPS 作为基础设施的小型团队和快速发展的初创产品使用。
同时 Skynet 也向我们展示了 Go 语言在开发后端服务,尤其是集群服务中的强大能力。它具有良好的开发效率和运行效率的平衡。作为一个较为新的语言,这是相当难得的。随着 Go 语言的继续发展,我们有理由相信在互联网应用开发上,它必将占有一席之地。

分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 分享分享
您需要登录后才可以回帖 登录 | 注-册

本版积分规则

小黑屋|手机版|Archiver|数码鹭岛 ( 闽ICP备20006246号 )  

counter

GMT+8, 2025-12-3 23:50 , Processed in 0.071186 second(s), 23 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表