当前位置: 首页 > 科技观察

Go语言在生产环境中的最佳实践

时间:2023-03-11 22:10:35 科技观察

在SoundCloud,我们为客户构建产品API。也就是说,我们的主网站、移动客户端和移动应用是这个API的第一批客户。这个API的背后是一个领域服务:SoundCloud基本上作为一个面向服务的架构运行。我们也是一个多语言组织,因为我们讲多种语言。这些服务的许多部分(和基础设施支持)都是使用Golang开发的。事实上,我们都是Golang的早期采用者:我们已经在生产中使用Golang两年半了。相关项目包括:Bazooka,我们的内部服务平台;产品理念与Keroku或Flynn非常相似。我们的外围传输层使用常见的nginx、HAProxy等,但是需要配合Golang服务。我们的音频存储在AWSS3上,但上传、转码和链接生成需要Golang服务协调。搜索使用Elasticsearch,检测使用复杂的机器学习模型,但它们都集成了使用Golang开发的基础架构。Prometheus,一个早期的遥测系统,完全用Golang开发。目前,Cassandra用于流处理,但我们计划(几乎)完全使用Golang。我们还在试验使用Golnag开发的HTTP实时流媒体服务。许多其他面向产品的小服务。这些项目由大约六个团队开发,其中包括十几个SoundCloud杂工,其中大多数人全职使用Golang。毕竟在这个时候,在这些项目中,在这样混杂的工程师中,我们已经逐渐形成了在产品中使用Golang的最佳实践方法。我们的这些经验教训将帮助其他组织开始大力投资Golang。开发环境在我们的笔记本电脑上,我们已经设置了一个单一的全局GOPATH。就个人而言,我喜欢使用$HOME,但许多其他人使用$HOME下的子目录。我们把repository克隆到GOPATH的相对路径下,然后就可以直接工作了。即$mkdir-p$GOPATH/src/github.com/soundcloud$cd$GOPATH/src/github.com/soundcloud$gitclonegit@github.com:soundcloud/roshi我们很多人在早期使用的东西努力维护我们自己独特的代码组织方式。事实上,这根本不值得麻烦。对于编辑器,许多用户使用带有各种插件的Vim。(我使用vim-go,这很好。)许多人,包括我自己,将SublimeText与GoSublime结合使用。也有少数人使用Emacs,但没有人使用IDE。我不确定这是否是一个好的做法,但标记它很有趣。库结构我们的最佳实践是让一切都简单。许多服务源代码在主包中是半包的。github.com/soundcloud/simple/README.mdMakefilemain.gomain_test.gosupport.gosupport_test.go比如我们的搜索调度器,两年后还是一样。在确定需要之前不要创建新结构。也许在某个时候您需要创建一个新的支持包。使用主库中的子目录并使用完全限定名称导入。如果包只有一个文件或者一个结构,当然不需要fork出来。有时需要将多个二进制文件包含在存储库中;例如,任务需要服务、工作进程或监视器。在这种情况下,将每个二进制文件放在特定主包的单独子目录中,并使用其他子目录(或包)来实现共享功能。github.com/soundcloud/complex/README.mdMakefilecomplex-server/main.gomain_test.gohandlers.gohandlers_test.gocomplex-worker/main.gomain_test.goprocess.goprocess_test.goshared/foo.gofoo_test.gobar.gobar_test.go请注意不要引入asrc目录。由于vendor子目录异常(更多内容见下文),请勿在存储库中包含src目录,或将其添加到GOPATH。格式和风格一般来说,首先配置你的编辑器将代码保存到gofmt(或goimports),使用默认参数。这意味着使用制表符进行缩进,使用空格进行对齐。格式不正确的代码将不会被提交。风格指南过去非常广泛,但谷歌最近发布了他们的代码审查评论文档,这几乎是我们期望遵守的约定。因此,我们使用它。我们实际上更进一步:避免命名的返回参数,除非它们清楚且显着地提高了透明度。避免make和new,除非它们是必要的(new(int)或make(Chanint)),或者我们提前知道要分配的东西的大小(make(map[int]string,n),或make([]int,0,256)).对标记值使用struct{}而不是bool或interface{}。例如,一个集合是一个map[string]struct{};频道是一个频道结构{}。它清楚地表明明显缺乏信息。breaklonglines的参数也很好。这更符合Java风格://不要这样做。funcprocess(dstio.Writer,readTimeout,writeTimeouttime.Duration,allowInvalidbool,maxint,src<-chanutil.Job){//...}这会更好:funcprocess(dstio.Writer,readTimeout,writeTimeouttime.Duration,allowInvalidbool,maxint,src<-chanutil.Job,){//...}在构造对象时,也是分多行:f:=foo.New(foo.Config{Site:"zombo.com",Out:os.stdout,Dest:conference.KeyPair{Key:"gophercon",Value:2014,},})另外,在分配新对象时,最好在初始化部分(如上)传递成员值,而不是稍后像下面这样设置它们。//不要那样做。f:=&Foo{}//,evenworse:new(Foo)f.Site="zombo.com"f.Out=os.Stdoutf.Dest.Key="gophercon"f.Dest.Value=2014配置我们尝试通过各种方式将配置传递给Go程序:解析配置文件,使用os.Getenv直接从环境中提取配置,以及解析带有各种增值标志的包。***,最经济的是plainpackageflag,其严格的类型和简单的语义对于我们所需要的一切来说是绝对足够和足够好的。我们主要部署12-Factor应用程序,12-Factor应用程序通过环境传递配置。但即便如此,我们还是使用启动脚本将环境变量转换为标志。标志充当程序和它运行的环境之间的明确且完整记录的表面区域。它们对于理解和操作程序非常宝贵。使用标志的一个好习惯是在你的主函数中定义它们。这样可以防止您在代码中随意将它们用作全局变量,从而使您可以严格遵守依赖注入并方便测试。funcmain(){var(payload=flag.String("payload","abc","payloaddata")delay=flag.Duration("delay",1*time.Second,"writedelay"))flag.Parse()//...}日志记录和遥测我们已经尝试了几种日志记录框架,它们提供了日志级别、调试、路由输出、自定义格式等功能。最后我们选择了包日志。因为我们只记录可操作的信息。这意味着需要手动处理的严重的、紧急级别的错误或结构化数据将被其他机器使用。例如,搜索转发器将其处理的每个请求与上下文信息一起发送,因此我们的分析工作流可以看到新西兰人经常搜索Lorde或其他任何内容。我们考虑了遥测,运行期间发出的任何其他数量:请求响应时间、QPS、运行错误、队列深度等。遥测基本上包括两种模式:推和拉。push表示释放指向已知系统的指针。例如Graphite、Statsd和AirBrakepull意味着在某个已知位置公开指标并允许已知系统擦除它们。例如,expvar和Prometheus(可能还有其他)当然这两种方法都有自己的存在。当您开始使用时,推送是直观且简单的。但推送指标的增长是违反直觉的:你得到的越多,成本就越高。我们过去发现,在一定规模的基础设施中,拉动是该规模的唯一模型。反映一个运行系统的值也有很多。因此,最好的做法是:expvar或类似的风格。测试和验证在一年的时间里,我们尝试了许多测试库和框架,但很快就放弃了其中的大部分,今天我们所有的测试都通过了数据驱动(表驱动)测试,使用通用包测试。我们对测试/检查包没有强烈或明确的抱怨,只是它们根本没有提供很大的价值。有一点有帮助:reflect.DeepEqual可以更轻松地比较任意值(如expected与got)。包测试是面向单元测试的,但是对于集成测试来说,会有点麻烦。运行外部服务取决于您的集成环境,但我们找到了集成它们的好方法。写一个integration_test.go并给它一个集成构建标签。定义(全局)标志,例如服务地址和连接字符串,并在您的测试中使用它们。//+buildintegrationvarfooAddr=flag.String(...)funcTestToo(t*testing.T){f,err:=foo.Connect(*fooAddr)//...}gotest构建与gobuild相同的标志,所以你可以调用gotest-tags=integration。它还综合了flag.Parse包的主体,因此任何已声明和可见的标志都将被处理并可用于您的测试。通过验证,我的意思是静态代码验证。幸运的是,Go有一些很棒的工具。我发现在考虑使用哪种工具时考虑编写代码的阶段很有用。当做这类事情时,使用这个savegofmt(或goimports)构建govet、golint或gotestdeploygotest-tags=integration情节到目前为止没什么太疯狂的。在做这份清单的研究时,唯一引起我注意的是如何做。.....多么无聊的结论。这很无聊。我想强调的是,这些非常轻量级的纯标准库约定确实可以推广到大量开发人员和多样化的项目生态系统。你绝对不需要你自己的错误检查框架或测试库,只是因为你的代码库超过了一定的规模,或者只是因为它可能会增长到超过一定的行数。你真的不需要它。标准语法和用法在规模上仍然可以优雅地发挥作用。依赖管理依赖管理!啊!?(?)?依赖管理的状态是Go生态中的一个热议话题,我们还没有想到一个完美的解决方案。然而,我们最终达成了妥协,这似乎是一个很好的妥协。你的项目有多重要?您的依赖管理方案是……嗯……goget-d,祈祷吧!伟大的。VENDORING(值得一提的是,我们有数量惊人的长期生产服务仍然依赖于***选项。但是,由于我们通常不使用太多第三方代码,而且主要问题通常在compilestage,wegetawaywithit.)Vendoring意味着将依赖项复制到项目代码库中,然后编译时不时使用它们。根据您下载的内容,这里有两个供应商的最佳实践。下载Vendor目录名进程二进制_vendor前缀GOPATH编译库vendor重写import语句如果下载二进制,则在代码基目录的根目录下创建一个_vendor子目录。(带下划线以便go工具在处理过程中忽略它,例如gotest./...)就像GOPATH一样对待它;例如,将此依赖项github.com/user/dep复制到_vendor/src/github.com/user/dep。然后,编写一个所谓的神圣编译过程,它将_vendor添加到任何可能存在的GOPATH中。(记住:GOPATH实际上是go工具在处理导入时按顺序搜索的路径列表。)例如,您可能有一个顶层Makefile,如下所示:GO?=goGOPATH:=$(CURDIR)/_vendor:$(GOPATH)all:buildbuild:$(GO)build如果您正在下载一个库,请在您的根存储库上创建一个vendor子目录。处理这个就像在包目录上加上前缀。例如,将项目从github.com/user/dep复制到vendor/user/dep。之后,重写所有导入及其相互关系。这是一件令人头疼的事,而且看起来最有效的方法是在其余部分需要获得兼容性时确保实际可重现的构建。值得注意的是,我们在实践中很少下载类库,所以这种方法比较繁琐,但很有效。如何将依赖项实际复制到您自己的存储库中是另一个热门话题。最简单的方法是从克隆中手动复制文件,如果您不关心上游推送,这可能是最好的答案。有些人使用git子模块,但我们发现它们非常违反直觉且难以管理(对于许多人来说,它已被记录)。我们在git子目录方面取得了巨大的成功,它像子模块一样工作。还有大量旨在自动执行此工作的工具。现在,看起来godep的开发非常活跃,非常值得研究。构建和部署构建和部署是棘手的,因此它与您的操作环境紧密相关。我将描述我们的场景,因为我认为这是一个很好的模型,但它可能不会直接适用于您的组织。就构建而言,我们开发时一般直接使用gobuild,正式构建时裁剪一个Makefile。这主要是因为我们熟悉多种语言,我们的工具使用需要做到最小函数集合(最小公倍数)。此外,我们的构建系统从一个空环境开始,需要您自己的编译器(Makefile很丑!)。对于部署,真正吸引我们的是无状态和有状态的区别。模式示例模型部署名称部署表单无状态请求路由器12因素扩展容器有状态Redis无,真的是供应容器吗?我们主要部署无状态服务,类似于Heroku。$gitpushbazookamaster$bazookascale-r-n4...$#validate$bazookascale-r-n0...英文原文:Go:BestPracticesforProductionEnvironments翻译链接:http://www.oschina.网络/翻译/投入生产