当前微服务已经成为服务端开发的主流架构,而Go语言因其简单易学、内置高并发、快速编译、占用内存小等特点也越来越受到开发者的青睐,微服务实战系列文章将从实战的角度和大家一起学习微服务相关的知识。本系列文章将以一个“博客系统”由浅入深的和大家一起一步步搭建起一个完整的微服务系统
该篇文章为微服务实战系列的第一篇文章,我们将基于go-zero+gitlab+jenkins+k8s构建微服务持续集成和自动构建发布系统,先对以上模块做一个简单介绍:
go-zero是一个集成了各种工程实践的web和rpc框架。通过弹性设计保障了大并发服务端的稳定性,经受了充分的实战检验
gitlab是一款基于Git的完全集成的软件开发平台,另外,GitLab且具有wiki以及在线编辑、issue跟踪功能、CI/CD等功能
jenkins是基于Java开发的一种持续集成工具,用于监控持续重复的工作,旨在提供一个开放易用的软件平台,使软件的持续集成变成可能
kubernetes常简称为K8s,是用于自动部署、扩展和管理容器化应用程序”的开源系统。该系统由Google设计并捐赠给CloudNativeComputingFoundation(今属Linux基金会)来使用。它旨在提供“跨主机集群的自动部署、扩展以及运行应用程序容器的平台

实战主要分为五个步骤,下面针对以下的五个步骤分别进行详细的讲解
第一步环境搭建,这里我采用了两台服务器分别安装了gitlab和jenkins,采用xxx云弹性k8s集群
第二步生成项目,这里我采用go-zero提供的goctl工具快速生成项目,并对项目做简单的修改以便测试
第三部生成Dockerfile和k8s部署文件,k8s部署文件编写复杂而且容易出错,goctl工具提供了生成Dockerfile和k8s部署文件的功能非常的方便
JenkinsPipeline采用声明式语法构建,创建Jenkinsfile文件并使用gitlab进行版本管理
最后进行项目测试验证服务是否正常

环境搭建
首先我们搭建实验环境,这里我采用了两台服务器,分别安装了gitlab和jenkins。gtilab使用apt-get直接安装,安装好后启动服务并查看服务状态,各组件为run状态说明服务已经启动,默认端口为9090直接访问即可
gitlab-ctlstart//启动服务gitlab-ctlstatus//查看服务状态run:alertmanager:(pid1591)15442s;run:log:(pid2087)439266srun:gitaly:(pid1615)15442s;run:log:(pid2076)439266srun:gitlab-exporter:(pid1645)15442s;run:log:(pid2084)439266srun:gitlab-workhorse:(pid1657)15441s;run:log:(pid2083)439266srun:grafana:(pid1670)15441s;run:log:(pid2082)439266srun:logrotate:(pid5873)1040s;run:log:(pid2081)439266srun:nginx:(pid1694)15440s;run:log:(pid2080)439266srun:node-exporter:(pid1701)15439s;run:log:(pid2088)439266srun:postgres-exporter:(pid1708)15439s;run:log:(pid2079)439266srun:postgresql:(pid1791)15439s;run:log:(pid2075)439266srun:prometheus:(pid10763)12s;run:log:(pid2077)439266srun:puma:(pid1816)15438s;run:log:(pid2078)439266srun:redis:(pid1821)15437s;run:log:(pid2086)439266srun:redis-exporter:(pid1826)15437s;run:log:(pid2089)439266srun:sidekiq:(pid1835)15436s;run:log:(pid2104)439266s
jenkins也是用apt-get直接安装,需要注意的是安装jenkins前需要先安装java,过程比较简单这里就不再演示,jenkins默认端口为8080,默认账号为admin,初始密码路径为/var/lib/jenkins/secrets/initialAdminPassword,初始化安装推荐的插件即可,后面可以根据自己的需要再安装其它插件
k8s集群搭建过程比较复杂,虽然可以使用kubeadm等工具快速搭建,但距离真正的生产级集群还是有一定差距,因为我们的服务最终是要上生产的,所以这里我选择了xxx云的弹性k8s集群版本为1.16.9,弹性集群的好处是按需收费没有额外的费用,当我们实验完成后通过kubectldelete立马释放资源只会产生很少的费用,而且xxx云的k8s集群给我们提供了友好的监控页面,可以通过这些界面查看各种统计信息,集群创建好后需要创建集群访问凭证才能访问集群
若当前访问客户端尚未配置任何集群的访问凭证,即~/.kube/config内容为空,可直接将访问凭证内容并粘贴入~/.kube/config中
若当前访问客户端已配置了其他集群的访问凭证,需要通过如下命令合并凭证
KUBECONFIG=~/.kube/config:~/Downloads/k8s-cluster-configkubectlconfigview--merge--flatten~/.kube/configexportKUBECONFIG=~/.kube/config
配置好访问权限后通过如下命令可查看当前集群
kubectlconfigcurrent-context
查看集群版本,输出内容如下
kubectlversionClientVersion:{Major:"1",Minor:"16",GitVersion:"",GitCommit:"a17149e1a189050796ced469dbd78d380f2ed5ef",GitTreeState:"clean",BuildDate:"2020-04-16T11:44:51Z",GoVersion:"",Compiler:"gc",Platform:"linux/amd64"}ServerVersion:{Major:"1",Minor:"16+",GitVersion:"",GitCommit:"f999b99a13f40233fc5f875f0607448a759fc613",GitTreeState:"clean",BuildDate:"2020-10-09T12:54:13Z",GoVersion:"",Compiler:"gc",Platform:"linux/amd64"}到这里我们的试验已经搭建完成了,这里版本管理也可以使用github
生成项目整个项目采用大仓的方式,目录结构如下,最外层项目命名为blog,app目录下为按照业务拆分成的不同的微服务,比如user服务下面又分为api服务和rpc服务,其中api服务为聚合网关对外提供restful接口,而rpc服务为内部通信提供高性能的数据缓存等操作
├──blog│├──app││├──user│││├──api│││└──rpc││├──article│││├──api│││└──rpc
项目目录创建好之后我们进入api目录创建文件,文件内容如下,定义服务端口为2233,同时定义了一个/user/info接口
typeUserInfoRequeststruct{Uidint64`form:"uid"`}typeUserInfoResponsestruct{Uidint64`json:"uid"`Namestring`json:"name"`Levelint`json:"level"`}@server(port:2233)serviceuser-api{@doc(summary:获取用户信息)@server(handler:UserInfo)get/user/info(UserInfoRequest)returns(UserInfoResponse)}定义好api文件之后我们执行如下命令生成api服务代码,一键生成真是能大大提升我们的生产力呢
代码生成后我们对代码稍作改造以便后面部署后方便进行测试,改造后的代码为返回本机的ip地址
func(ul*UserInfoLogic)UserInfo()(*,error){addrs,err:=()iferr!=nil{returnnil,err}varnamestringfor_,addr:=rangeaddrs{ifipnet,ok:=addr.(*);ok!()()!=nil{name=()}}{Uid:,Name:name,Level:666,},nil}到这里服务生成部分就完成了,因为本节为基础框架的搭建所以只是添加一些测试的代码,后面会继续丰富项目代码
生成镜像和部署文件一般的常用镜像比如mysql、memcache等我们可以直接从镜像仓库拉取,但是我们的服务镜像需要我们自定义,自定义镜像有多重方式而使用Dockerfile则是使用最多的一种方式,使用Dockerfile定义镜像虽然不难但是也很容易出错,所以这里我们也借助工具来自动生成,这里就不得不再夸一下goctl这个工具真的是棒棒的,还能帮助我们一键生成Dockerfile呢,在api目录下执行如下命令
生成后的文件稍作改动以符合我们的目录结构,文件内容如下,采用了两阶段构建,第一阶段构建可执行文件确保构建独立于宿主机,第二阶段会引用第一阶段构建的结果,最终构建出极简镜像
FROMgolang:alpineASbuilderLABELstage=gobuilderENVCGO_ENABLED0ENVGOOSlinuxENVGOPROXY"-s-w"-o/app/estzdataENVTZAsia/ShanghaiWORKDIR/appCOPY--from=builder/app/user/app/userCOPY--from=builder/app/etc/app/etcCMD["./user","-f","etc/"]
然后执行如下命令创建镜像
dockerbuild-tuser:v1app/user/api/
这个时候我们使用dockerimages命令查看镜像会发现user镜像已经创建,版本为v1
同样,k8s的部署文件编写也比较复杂很容易出错,所以我们也使用goctl自动来生成,在api目录下执行如下命令
goctlkubedeploy-nameuser-api-namespaceblog-imageuser:
生成的ymal文件如下
apiVersion:apps/v1kind:Deploymentmetadata:name:user-apinamespace:bloglabels:app:user-apispec:replicas:2revisionHistoryLimit:2selector:matchLabels:app:user-apitemplate:metadata:labels:app:user-apispec:containers:-name:user-apiimage:user:v1lifecycle:preStop:exec:command:["sh","-c","sleep5"]ports:-containerPort:2233readinessProbe:tcpSocket:port:2233initialDelaySeconds:5periodSeconds:10livenessProbe:tcpSocket:port:2233initialDelaySeconds:15periodSeconds:10resources:requests:cpu:500mmemory:512Milimits:cpu:1000mmemory:1024Mi
到此生成镜像和k8s部署文件步骤已经结束了,上面主要是为了演示,真正的生产环境中都是通过持续集成工具自动创建镜像的
JenkinsPipelinejenkins是常用的继续集成工具,其提供了多种构建方式,而pipeline是最常用的构建方式之一,pipeline支持声名式和脚本式两种方式,脚本式语法灵活、可扩展,但也意味着更复杂,而且需要学习Grovvy语言,增加了学习成本,所以才有了声明式语法,声名式语法是一种更简单,更结构化的语法,我们后面也都会使用声名式语法
这里再介绍下Jenkinsfile,其实Jenkinsfile就是一个纯文本文件,也就是部署流水线概念在Jenkins中的表现形式,就像Dockerfile之于Docker,所有的部署流水线逻辑都可在Jenkinsfile文件中定义,需要注意,Jenkins默认是不支持Jenkinsfile的,我们需要安装Pipeline插件,安装插件的流程为ManageJenkins-ManagerPlugins然后搜索安装即可,之后便可构建pipeline了

我们可以直接在pipeline的界面中输入构建脚本,但是这样没法做到版本化,所以如果不是临时测试的话是不推荐这种方式的,更通用的方式是让jenkins从git仓库中拉取Jenkinsfile并执行
首先需要安装Git插件,然后使用sshclone方式拉取代码,所以,需要将git私钥放到jenkins中,这样jenkins才有权限从git仓库拉取代码
将git私钥放到jenkins中的步骤是:ManageJenkins-Managecredentials-添加凭据,类型选择为SSHUsernamewithprivatekey,接下来按照提示进行设置就可以了,如下图所示

然后在我们的gitlab中新建一个项目,只需要一个Jenkinsfile文件

在user-api项目中流水线定义选择PipelinescriptfromSCM,添加gitlabssh地址和对应的token,如下图所示

接着我们就可以按照上面的实战步骤进行Jenkinsfile文件的编写了
从gitlab拉取代码,从我们的gitlab仓库中拉取代码,并使用commit_id用来区分不同版本
stage('从gitlab拉取服务代码'){steps{echo'从gitlab拉取服务代码'gitcredentialsId:'xxxxxxxx',url:''script{commit_id=sh(returnStdout:true,script:'gitrev-parse--shortHEAD').trim()}}}构建docker镜像,使用goctl生成的Dockerfile文件构建镜像
stage('构建镜像'){steps{echo'构建镜像'sh"dockerbuild-tuser:${commit_id}app/user/api/"}}上传镜像到镜像仓库,把生产的镜像push到镜像仓库
stage('上传镜像到镜像仓库'){steps{echo"上传镜像到镜像仓库"sh"dockerlogin-uxxx-pxxxxxxx"sh"dockertaguser:${commit_id}xxx/user:${commit_id}"sh"dockerpushxxx/user:${commit_id}"}}部署到k8s,把部署文件中的版本号替换,从远程拉取镜,使用kubectlapply命令进行部署
stage('部署到k8s'){steps{echo"部署到k8s"sh"sed-i's/COMMIT_ID_TAG/${commit_id}/'app/user/api/"sh"cpapp/user/api/"sh""}}完整的Jenkinsfile文件如下
pipeline{agentanystages{stage('从gitlab拉取服务代码'){steps{echo'从gitlab拉取服务代码'gitcredentialsId:'xxxxxx',url:''script{commit_id=sh(returnStdout:true,script:'gitrev-parse--shortHEAD').trim()}}}stage('构建镜像'){steps{echo'构建镜像'sh"dockerbuild-tuser:${commit_id}app/user/api/"}}stage('上传镜像到镜像仓库'){steps{echo"上传镜像到镜像仓库"sh"dockerlogin-uxxx-pxxxxxxxx"sh"dockertaguser:${commit_id}xxx/user:${commit_id}"sh"dockerpushxxx/user:${commit_id}"}}stage('部署到k8s'){steps{echo"部署到k8s"sh"sed-i's/COMMIT_ID_TAG/${commit_id}/'app/user/api/"sh"kubectlapply-fapp/user/api/"}}}}
构建详细输出如下,pipeline对应的每一个stage都有详细的输出
StartedbyuseradminObtainedJenkinsfilefromgitgit@:gitlab-instance-1ac0cea5/:MAX_SURVIVABILITY[Pipeline]StartofPipeline[Pipeline]nodeRunningonJenkinsin/var/lib/jenkins/workspace/user-api[Pipeline]{[Pipeline]stage[Pipeline]{(Declarative:CheckoutSCM)[Pipeline]:NONEusingcredentialgitlab_tokengitrev-parse--is-inside-work-treetimeout=10Fetchingupstreamchangesfromgit@:gitlab-instance-1ac0cea5/''usingGIT_SSHtosetcredentialsgitfetch--tags--progressgit@:gitlab-instance-1ac0cea5/+refs/heads/*:refs/remotes/origin/*timeout=10CheckingoutRevision77eac3a4ca1a5b6aea705159ce26523ddd179bdf(refs/remotes/origin/master)=10Commitmessage:"add"gitrev-list--no-walk77eac3a4ca1a5b6aea705159ce26523ddd179bdftimeout=10;git--version''usingGIT_ASKPASStosetcredentialsgitfetch--tags--progress*:refs/remotes/origin/*timeout=10CheckingoutRevisionb757e9eef0f34206414bdaa4debdefec5974c3f5(refs/remotes/origin/master)=10gitbranch-a-v--no-abbrevtimeout=10gitcheckout-bmasterb757e9eef0f34206414bdaa4debdefec5974c3f5timeout=10[Pipeline]script[Pipeline]{[Pipeline]sh+gitrev-parse--shortHEAD[Pipeline]}[Pipeline]//script[Pipeline]}[Pipeline]//stage[Pipeline]stage[Pipeline]{(构建镜像)[Pipeline]echo构建镜像[Pipeline]sh+dockerbuild-tuser:b757e9eapp/user/api//18:FROMgolang:alpineASbuilderalpine:Pullingfromlibrary/golang801bfaa63ef2:Pullingfslayeree0a1ba97153:Pullingfslayer1db7f31c0ee6:Pullingfslayerecebeec079cf:Pullingfslayer63b48972323a:Pullingfslayerecebeec079cf:Waiting63b48972323a:Waiting1db7f31c0ee6:VerifyingChecksum1db7f31c0ee6:Downloadcompleteee0a1ba97153:VerifyingChecksumee0a1ba97153:Downloadcomplete63b48972323a:VerifyingChecksum63b48972323a:Downloadcomplete801bfaa63ef2:VerifyingChecksum801bfaa63ef2:Downloadcomplete801bfaa63ef2:Pullcompleteee0a1ba97153:Pullcomplete1db7f31c0ee6:Pullcompleteecebeec079cf:VerifyingChecksumecebeec079cf:Downloadcompleteecebeec079cf:Pullcomplete63b48972323a:PullcompleteDigest:sha256:49b4eac11640066bc72c74b70202478b7d431c7d8918e0973d6e4aeb8b3129d2Status:Downloadednewerimageforgolang:alpine---1463476d8605Step2/18:LABELstage=gobuilder---Runninginc4f4dea39a32Removingintermediatecontainerc4f4dea39a32---c04bee317ea1Step3/18:ENVCGO_ENABLED0---Runningine8e848d64f71Removingintermediatecontainere8e848d64f71---ff82ee26966dStep4/18:ENVGOOSlinux---Runningin58eb095128acRemovingintermediatecontainer58eb095128ac---825ab47146f5Step5/18:ENVGOPROXY;Runningindf2add4e39d5Removingintermediatecontainerdf2add4e39d5---c31c1aebe5faStep6/18:WORKDIR/build/zero---Runninginf2a1da3ca048Removingintermediatecontainerf2a1da3ca048---5363d05f25f0Step7/18:RUNgomodinitblog/app/user/api---Runningin11d0adfa9d53[91mgo::moduleblog/app/user/api[0mRemovingintermediatecontainer11d0adfa9d53---3314852f00feStep8/18:RUNgomoddownload---Runninginaa9e9d9eb850Removingintermediatecontaineraa9e9d9eb850---a0f2a7ffe392Step9/18:COPY..---a807f60ed250Step10/18:COPY/etc/app/etc---c4c5d9f15dc0Step11/18:RUNgobuild-ldflags="-s-w"-o/app/[91mgo:/tal-tech/go-zero/core/conf[0m[91mgo:/tal-tech/go-zero/rest/httpx[0m[91mgo:/tal-tech/go-zero/rest[0m[91mgo:/tal-tech/go-zero/core/logx[0m[91mgo:/tal-tech/[0m[91mgo:/tal-tech/go-zero/core//tal-tech/:/tal-tech/go-zero//tal-tech/:/tal-tech/go-zero/rest//tal-tech/:/tal-tech/go-zero/core//tal-tech/[0m[91mgo:/[0m[91mgo:/justinas/[0m[91mgo:/dgrijalva/+incompatible[0m[91mgo:/[0m[91mgo:/spaolacci/[0m[91mgo:/google/[0m[91mgo:/[0m[91mgo:/prometheus/client_[0m[91mgo:/beorn7/[0m[91mgo:/golang/[0m[91mgo:/prometheus/[0m[91mgo:/cespare/xxhash/[0m[91mgo:/prometheus/client_[0m[91mgo:/prometheus/[0m[91mgo:/matttproud/golang_protobuf_[0m[91mgo:/[0mRemovingintermediatecontainera4321c3aa6e2---99ac2cd5fa39Step12/18:FROMalpinelatest:Pullingfromlibrary/alpine801bfaa63ef2:AlreadyexistsDigest:sha256:3c7497bf0c7af93428242d6176e8f7905f2201d8fc5861f45be7a346b5f23436Status:Downloadednewerimageforalpine:latest---389fef711851Step13/18:RUNapkupdate--no-cacheapkadd--no-cacheca-certificatestzdata---Runningin51694dcb96b6fetch[][]OK:12746distinctpackagesavailablefetch(1/2)Installingca-certificates(20191127-r4)(2/2)Installingtzdata(2020f-r0):10MiBin16packagesRemovingintermediatecontainer51694dcb96b6---e5fb2e4d5eeaStep14/18:ENVTZAsia/Shanghai---Runningin332fd0df28b5Removingintermediatecontainer332fd0df28b5---11c0e2e49e46Step15/18:WORKDIR/app---Runningin26e22103c8b7Removingintermediatecontainer26e22103c8b7---11d11c5ea040Step16/18:COPY--from=builder/app/user/app/user---f69f19ffc225Step17/18:COPY--from=builder/app/etc/app/etc---b8e69b663683Step18/18:CMD["./user","-f","etc/"]---Runningin9062b0ed752fRemovingintermediatecontainer9062b0ed752f---4867b4994e43Successfullybuilt4867b4994e43Successfullytaggeduser:b757e9e[Pipeline]}[Pipeline]//stage[Pipeline]stage[Pipeline]{(上传镜像到镜像仓库)[Pipeline]echo上传镜像到镜像仓库[Pipeline]sh+dockerlogin-uxxx-pxxxxxxxxWARNING!!Yourpasswordwillbestoredunencryptedin/var/lib/jenkins/.docker/[Pipeline]sh+dockertaguser:b757e9exxx/user:b757e9e[Pipeline]sh+dockerpushxxx/user:b757e9eThepushreferstorepository[/xxx/user]b19a970f64b9:Preparingf695b957e209:Preparingee27c5ca36b5:Preparing7da914ecb8b0:Preparing777b2c648970:Preparing777b2c648970:Layeralreadyexistsee27c5ca36b5:Pushedb19a970f64b9:Pushed7da914ecb8b0:Pushedf695b957e209:Pushedb757e9e:digest:sha256:6ce02f8a56fb19030bb7a1a6a78c1a7c68ad43929ffa2d4accef9c7437ebc197size:1362[Pipeline]}[Pipeline]//stage[Pipeline]stage[Pipeline]{(部署到k8s)[Pipeline]echo部署到k8s[Pipeline]sh+sed-is/COMMIT_ID_TAG/b757e9e/app/user/api/[Pipeline]sh+kubectlapply-fapp/user/api//user-apicreated[Pipeline]}[Pipeline]//stage[Pipeline]}[Pipeline]//withEnv[Pipeline]}[Pipeline]//node[Pipeline]ofPipelineFinished:SUCCESS可以看到最后输出了SUCCESS说明我们的pipeline已经成了,这个时候我们可以通过kubectl工具查看一下,-n参数为指定namespace
kubectlgetpods-nblogNAMEREADYSTATUSRESTARTSAGEuser-api-84ffd5b7b-c8c5w1/1Running010muser-api-84ffd5b7b-pmh921/1Running010m
我们在k8s部署文件中制定了命名空间为blog,所以在执行pipeline之前我们需要先创建这个namespance
kubectlcreatenamespaceblog
服务已经部署好了,那么接下来怎么从外部访问服务呢?这里使用LoadBalancer方式,Service部署文件定义如下,80端口映射到容器的2233端口上,selector用来匹配Deployment中定义的label
apiVersion:v1kind:Servicemetadata:name:user-api-servicenamespace:blogspec:selector:app:user-apitype:LoadBalancerports:-protocol:TCPport:80targetPort:2233
执行创建service,创建完后查看service输出如下,注意一定要加上-n参数指定namespace
kubectlgetservices-nblogNAMETYPECLUSTER-IPEXTERNAL-IPPORT(S):32470/TCP79m
这里的EXTERNAL-IP即为提供给外网访问的ip,端口为80
到这里我们的所有的部署任务都完成了,大家最好也能亲自动手来实践一下
测试最后我们来测试下部署的服务是否正常,使用EXTERNAL-IP来进行访问
curl""{"uid":1,"name":"172.17.0.5","level":666}curl\?uid\=1{"uid":1,"name":"172.17.0.8","level":666}curl访问了两次/user/info接口,都能正常返回,说明我们的服务没有问题,name字段分别输出了两个不同ip,可以看出LoadBalancer默认采用了RoundRobin的负载均衡策略
总结以上我们实现了从代码开发到版本管理再到构建部署的DevOps全流程,完成了基础架构的搭建,当然这个架构现在依然很简陋。在本系列后续中,我们将以这个博客系统为基础逐渐的完善整个架构,比如逐渐的完善CI、CD流程、增加监控、博客系统功能的完善、高可用最佳实践和其原理等等
工欲善其事必先利其器,好的工具能大大提升我们的工作效率而且能降低出错的可能,上面我们大量使用了goctl工具简直有点爱不释手了哈哈哈,下次见
由于个人能力有限难免有表达有误的地方,欢迎广大观众姥爷批评指正!
项目地址https://tal-tech/go-zero
欢迎使用并star支持我们!