在上一篇文章中,我们从0到0.5用Golang写了一个web应用,到0.5是因为那坨代码离生产环境还差的很远。满分按100分的话,这篇文章继续补充从0.5到1的内容,尝试描述作为一个合格的在线服务应该具备的部件。读者朋友对本文有啥意见欢迎留言。本文范围如下:
上篇文章写的web demo勉强能算一个服务承担所有功能的"单体架构"应用,运气好的话,这个产品用户量激增,这个demo后续会迭代很多功能,比如增加用户模块,IM模块,商城模块等等。此时如果这些功能都放在一个进程里去服务用户,服务的拓展性会变差,上线一个新功能的迭代周期会很长。想象一下,如果30个人一起维护一个大项目,几个新需求同时到来时,几个人会同时去改一坨代码,可预见首先会发生git分支冲突,其次到上线的时候,排队等上线现象也会很严重。
此时一般会空降一位大厂架构师,架构师大佬在梳理完服务之后会大刀阔斧把服务按业务域拆分成若干个独立模块,这些独立模块也叫微服务。所谓微服务是一些协同工作的小而自治的服务。小是指这些服务职责单一,每个服务只做一件事;自治是指每个微服务都是一个独立进程,被部署到不同的"主机"上进行管理。
拆完后,之前的用户、IM、商城等模块会作为独立进程部署在不同机器上,彼此间的部署互不影响,他们之间会彼此通信,通信协议一般会选语言无关的轻量化协议。把单体服务拆解为微服务的好处有很多:不同模块可根据业务特点选择不同的语言和底层存储进行开发;模块之间的更新迭代互不影响;不同小组专职负责不同模块,组内沟通成本降低。但微服务不是银弹,引入它会带来很多技术挑战:微服务对基础设施要求较高;产品提一个需求会涉及到多个团队跨部门合作,沟通成本巨高,各个微服务间的通信需要引入rpc或消息队列;需求整体测试难度会提升等等。
微服务会带来很多技术挑战,但其带来的好处远大于坏处。下面来看一下如果之前的web demo变成了企业中的一个微服务,该如何应对这些技术挑战~
在很多互联网公司,一个需求的上线流程是:运营出brd -> 产品出prd -> 产品召集开发评审需求 -> 开发排期开发 -> 测试对代码进行测试 -> 产品验收后交付上线。其中测试是功能正确性最重要、也是最后一环保障。在 <Scrum敏捷软件开发> 一书中介绍了测试金字塔模型,这个模型把测试划分为单元测试、集成测试和UI测试,某些场景下还需要进行压力测试,下面来分别看一下这几种测试。
单元测试(Unit Tests)即对代码中的最小单元进行测试。最小单元在Golang中可以是一个函数、一个方法或一个xx.go文件,而单元测试就是对应的xx_test.go文件。单元测试大多数时候不需要启动整个服务,有些场景若需要通过网络连接与外部系统通信,可以使用monkey-patch进行mock。在Golang中运行所有的_test.go
文件很简单,只需要在需要测试的目录下执行go test ./...
就可以测试此目录下所有子目录中的单测文件。Goland里面有个生成单测模板的功能,在mac系统只需要按住CMD+N
就可以生成你刚写的函数的测试框架。
与单元测试相关的指标叫单测覆盖率,经常被应用于卡测试准入门槛,如果低于某个值,测试小姐姐可能就会打回让你补单测。个人认为单测覆盖率是个良心活,仅靠设置一个下限没办法保证不出问题,就像这个笑话里面讲的一样。
集成测试(Integration Tests)即 对服务的接口(API)进行测试。在线服务一般通过接口与外部交互,对于接口的测试是非常重要的一环,一般这个环节会测试同学会根据开发内容构造测试用例,然后通过调用接口测试这些case表现是否符合预期。一般还需要有一个case平台把这些case作为准入条件保存下来,这样当开发做了一些技术迭代时通过跑原有case可以观测到是否破坏了代码的原有功能。
UI测试(UI Tests)即 模拟真实用户使用对最终产品功能是否符合预期做的一些测试,如果一个点击10s之后才有数据响应,那你的测试、产品、老板可能都要指着你让你优化性能了。
压力测试(Stress Tests)即 对服务的承载能力进行测试。很多不讲武德的业务方会不按套路调用你的API,遇上某些节日你的服务可能需要承接翻倍的流量。所以需要通过压力测试对服务的承压能力进行摸底,你心里有了底之后,在大促到来之前就可以通过堆机器来提高服务的稳定性了。压测有很多命令行工具,比如上篇使用到的wrk
。
新需求在开发测试通过之后就要进行上线。一般公司会提供CI/CD平台,通过一个流水线,把CodeReview、测试、编译、部署、发布这些流程串联起来。前面讲了测试,这里说一下编译和部署。
有的朋友可能要说了,golang的编译和部署很简单啊,用go build
命令10s就能编译完并生成一个可执行文件,比Java、C艹快多了,然后直接./xx
运行就行了。没错,这位朋友确实很熟悉Golang的命令,如果只有一个项目或许可以这么做。但现在问题是,一个公司里经过微服务改造后可能有上百个项目,这个项目是Java写的,那个项目是用Go写的,还有一些运维管理平台是Python写的。此时需要有一个部署平台需要兼容不同语言项目的上线。一种可行的做法是:在项目框架中搞个编译、部署专用的脚本,脚本个性化定制项目编译、部署需要执行的动作。而平台读取脚本即可执行相应步骤。
编译脚本一般放在项目根目录下,其作用是由编译机编译项目源码、并把上线所需的文件打包放到指定目录下,再由部署系统从指定目录下载源文件进行部署。在golang中这个脚本主要还是执行go build
命令去编译代码的。
部署脚本类似于hook, 用于定制一些服务的启动和停止的行为。比如说:根据根据服务部署的集群读取对应的配置文件,再或者定制服务的启动方式。
对于多集群的服务,每个集群的底层存储可能是隔离的,所以做多集群的话,需要提前在不同集群的机器上悄悄写一个.
开头的集群信息文件,启动时读取这个文件加载不同的配置文件。
这里还建议用supervisor去托管你的程序,想象一下,如果你的项目由于一个偶发bug挂掉了,且服务只部署在一台主机上,那整个服务就不可用了,如果没有监控&报警机制,你可能开心的点外卖吃饭去了,浑然不知马上就要背一个p0级别的事故。有了supervisor之后,它可以监控你的进程,挂了之后重新给你把应用拉起来,推荐好用。
即使经过了完善的测试,项目上线后仍不能保证万事大吉,就像很多同学每天都会上称观察自己各种身体情况一样,线上还需要一个观察服务运行情况的渠道 -- 监控。
监控不仅可以发现异常,还能观测正常业务流量,当运营老板绞尽脑汁给你提了一大坨必火的需求,你吭哧瘪肚和别的团队边撕逼边合作的封闭开发了一个月,结果上线了用监控一看,qps只有5,你会觉得今天外面的风格外喧嚣。
没有监控的系统是很可怕的,想象一下,你的服务晚上12点刚上线,你睡得晚于是开了飞行模式想睡个好觉,第二天起来突然发现手机一堆未接来电,你打开微博、逗音一看:卧槽,这不是我昨天刚上线的功能吗,怎么一堆用户在骂我们的软件,此时你除了跑路貌似也没什么办法了。
微服务的形态会给监控系统带来一些挑战:如果你的后端服务只有一个且负载均衡的部署在4台主机上,当客服妹子给你扔来一个case时,你自信的打开iTerm
并哐哐哐开了4个窗口,飞快的进入日志路径用grep
查找问题原因,突然你看到了一个异常参数,于是开dingding跑去和前端妹子理论,催她修 corner case,堪称救火突击队队长。但是新来的架构师突然带着他在大厂的先进经验要拆你们的服务,拆成10个服务,分别部署在100台主机上,那刚刚的命令可能就不好用了。
如果不想上面的假设变成现实,那就需要一套监控系统:监控系统让你在页面上观察到被监控指标,当指标异常时还可以触发报警,此时你就可以第一时间发现异常,对症下药或是观察新功能的流量走势了。
监控系统的原理是服务上报监控指标给本地agent,agent做将信息聚合后把结果发送给监控平台。若聚合结果出现异常,则触发报警。
这里介绍一下这几种监控:系统监控、业务监控、链路追踪,对于Go语言的项目,项目中还需要引入pprof做性能检测。
系统监控主要采集服务运行主机的各种指标,包括:CPU、内存、网络、硬盘、IO、服务端口等。可使用命令实时获取相关信息并上报给监控系统。比如CPU信息可以用 htop、top、vmstat,内存信息可以用: free、top、vmstat,IO信息可以用: iostat、iotop、df等。这些命令不用自己搞,一般初期直接用开源产品Promethues即可。
作为业务开发,应当清楚你写下的那坨代码到底有没有人用。有一个万能定律叫做二八定律,放在这个场景是一个残酷的现实:应用80%的功能可能只有20%甚至更少的人使用。那么程序员是应该把80%的精力放在80%的人都会用的那20%的业务功能,还是应该把80%的精力放在20%的人会用的80%的功能呢?
此时你一定很纠结,STOP!恭喜你,作为一个优秀的底层搬砖程序员你不用纠结这个问题,老板叫你干啥你干啥就行。
但作为老板应该清楚流量都到哪里去了。此时你需要一个叫做Metrics的库,这个库里面提供了一些计数器,计时器,让你可以统计流量、调用质量(调用频率、错误率、吞吐情况)等。你需要在服务中引入这个库,在业务代码需要的地方进行埋点,让这个库把相关指标发送至监控平台的数据库中。
微服务环境下,用户能看到的绝大多数功能都由多个服务组合提供,也就是说从端上来的请求会经过多个服务之间的调用最终给用户返回结果。这种情况如果出现了case排查起来会比较痛苦,因为你不清楚是哪一环出现了问题。一种解决方案是在日志中引入trace这样的具有全局唯一性的关键标识。在第一个服务调用时生成这个traceID,并传递给下游服务使用,这样如果出了问题开发可以用traceID去日志里grep,提升效率。当然,如果有日志采集平台就不用去机器上现采了。
在Golang的web项目中,traceID需要放到middleware层由一个全局的ID生成器去生成,生成后可以放在HTTP Header中传递给下游。
pprof是Golang自带的一个performance检测工具。当线上应用出现问题时,通过pprof可以查看系统运行时各种状态,是个非常牛逼的工具,建议大家的线上项目都要引入pprof。引入pprof也非常简单,只需要一行代码就可以搞定:
import _ "net/http/pprof"
pprof可以用来查看某时间点的Goroutine情况、CPU情况、内存情况、互斥锁情况等等,引入pprof
之后,可以通过浏览器输入/debug/pprof/
路径查看信息。
要注意的是:如果线上已经发生了问题,你再去看pprof信息只能拿到事故发生后的现场,需要考虑一下把线上运行情况监控起来,在事故发生时及时采集下相关pprof。
RPC(Remote Procedure Call)即远程系统调用,这个玩意可以让你像调用本地函数一样调用远程服务。按照服务的耦合性分类,有的RPC比较松散,比如Prototocal Buffers、Thrift, 服务之间可以用不同语言进行开发,有的RPC支持WSDL(Web Service Definition Language, web服务描述语言),你对着他的IDL文件可以生成stub代码或API文档。有的RPC的耦合性比较紧密,与语言、框架深度绑定,比如Java RMI,它要求C/S端使用一种框架。
RPC作为服务端和客户端沟通的桥梁,需要遵守一些协议,比如你规定了一个RPC函数的签名是:
func NewUser(userName string, address string, age int)
若产品此时提了一个新需求要收集用户的手机号,你可能要在函数签名中加一个phone int
字段,如果你的服务被10个业务方调用,你需要push这10个业务方去升级SDK并改动代码,任何事情,只要跨团队了,都是难以推动的,所以协议要先考虑好先定义下来。
由于RPC把远程调用细节隐藏了起来,也会带来一些问题:有些开发同学并没有意识到他刚刚用tab键补全的某个方法是在调用一个金贵的用户系统接口,于是这个同学起了一个for循环去疯狂call用户系统,如果这个金贵的用户系统没有做一些稳定性机制,比如caller级别的限流,那就会出大问题。
gRPC 是个很多Go开发都喜欢使用的一个RPC框架,这个框架语言无关,非常轻量;支持PB(Protocol Buffer)这种二进制格式消息去做序列化,反序列化,且支持基于接口定义文件生成sdk代码(不用写client代码接入别人的服务了,开心不开心),理论上也可以局域proto语法去自动生成API wiki,就像Swagger那样;且gRPC是基于HTTP 2.0设计的,支持双向流,TCP级别多路复用等特性,我觉得有追求,想晋升的同学可以试试在你们的项目中引入gRPC。
不过这玩意由于是基于PB进行通信的,联调的可读性差了一些,大部分朋友还是喜欢HTTP POST + JSON
一把梭吧。
微服务架构下,面向C端的服务为了拓展性还需要一个web server。Nginx是一个不错的选择,它是用C语言写的高性能轻量级的web server,支持负载均衡和反向代理。
反向代理实际就是由Nginx做了用户请求的proxy, Nginx可以处理多个请求,并把消息转发给App Server。负载均衡是在反向代理的基础上,让Nginx可以按照权重下发给各个App Server。
你们的服务是直接部署在物理机上的吗,单台物理机上部署几个服务呢?
站在成本角度上单机器多服务是很有诱惑力,一方面管理主机量越多,运维的工作量就越大;另一方面机器少买点,老板的钱包会鼓一些。但是这样搞会让开发流程比较恶心,比如一台机器上部署了4个服务,此时监控系统报警cpu.idle过低了,你可能得先看看是哪个服务消耗cpu比较严重,再比如,如果这4个服务是如果分别是两个团队内在维护的话,那谁来维护机器这中边界问题可能会很模糊。
所以还是放弃单机器多服务的想法吧。在后现代虚拟化主义一般会用Docker这种轻量级虚拟化方案做容器。与之前的虚拟机方案相比,Docker这种容器技术不需要模拟整个操作系统,只需要虚拟一个"沙箱"环境即可,所以Docker所占空间一般很小。Docker官方维护了很多镜像,我们可以方便的使用镜像部署环境,需要注意Docker本身不是容器,而是一个管理容器的工具。
但是Docker只是一个PaaS服务,它运行在单台机器上,一般还需要个工具去管理多台机器上面的Docker服务,可选的开源方案有K8S。
作为一个健壮的线上服务,还需要一些服务治理手段保证稳定性,比如:重试、降级、熔断、限流等。
重试指请求失败后重新发送请求,它解决的场景比较有限,只能解决由于抖动引起的服务暂时不可用,然后通过重试再次发起请求获取想要的结果。
这里要注意一点,如果你的下游已经处于满负荷状态,请求成功率在重试后不会提高,反而会由于无效请求对下游造成更大压力,最终把下游服务干爆~
降级指在极端场景下直接干掉一些非核心流程的流量。比如双十一期间,淘宝直接告诉你不能查三个月前的订单信息了。
想降级的前提是根据你服务的性质去梳理哪些是核心流程。如果系统做了多租户,那就根据应用场景给租户定级(中心思想是服务好 狗大户)。如果系统里有一堆接口,那就把非核心且耗资源的接口定为可降级目标。
熔断是caller探测到callee处于快毁灭的状态时,对其做的保护措施。打开熔断开关之后,caller对callee的访问状态在可访问-> 不可访问 -> 半可访问
这几个状态间徘徊。
限流即根据服务的承载能力进行流量控制,如果请求超过了能力上限,直接在接口入口将流量抛弃。
系统的承载能力要结合系统类型以及底层存储类型一起看,如果是CPU密集型的服务,服务横向拓展或许就能解决问题,如果是重度依赖存储的数据服务,那服务承载能力很大程度上依赖底层存储的瓶颈。
大多数服务的用户可能都不止一个,搞限流的时候不能因为某个狗大户qps 10w就直接把系统能力榨干了,导致有些qps 10的小用户直接连汤都喝不到。如果有可能,设计限流方案的时候最好先考虑多租户和多集群的方案,分而治之去解决问题。
https://zhuanlan.zhihu.com/p/20353718
https://zhuanlan.zhihu.com/p/25038203
https://juejin.cn/post/6844904129987526663#comment
http://wiki.intra.xiaojukeji.com/pages/viewpage.action?pageId=457436866
https://www.cnblogs.com/misswangxing/p/10669444.html