部署更轻量
第一,我认为golang的部署更轻量,部署的轻量主要体现在两个方面,第一个方面是它占用的资源少,golang的微服务可以放在类似于BUSYBOX很轻量的容器里面,再结合自动化运维工具来部署,管理整个集群。这样以来整个开发成本就会非常少,在创业一开始你的业务量其实并没有那么多,如果你配置很多的硬件资源成本都比较高昂。每个微服务资源的占用比较小,体量比较小的时候,在运用运维工具都可以很均衡的把资源分配出去,能够很好的解决资源分配的问题。
在编译和部署上面,时间上面的开销也非常少,这就带来了时间上的节省,开发效率更高。
源码更可控
第二, golang的源码更可控,主要是golang生态圈里面我们用到第三方工具包大部分是以开源的形式开放给我们,所以我们用到它的时候,我们很容易知道它的源码。另外如果它缺少一些特性,我们可以手动添加一些特性。当然这里面最主要的一个原因,是因为golang从一开始设计的时候有一个很重要的思想,就是这个源码写起来要简单,不会同一个东西有很多种写法。这样在阅读别人源码的时候更轻松一些,不会有源代码看起来很费劲的情况。所以这也确实是golang的一个优势,就算是在团队内部互相去看其他同学代码的时候,也会发现比之前好很多。
生态圈内部渐渐成熟,很多优秀的工具
golang的生态圈里面有非常多优秀的工具,golang的工具,很多工具都非常优秀,业界鼎鼎大名,包括做分布式日志追踪、etcd等等,在各自行业里都非常好用,给了我们非常大的支持。抛开源本身,我觉得生态作用可能更大一点。
系统构建与运维
当时我们构建系统的时候遇到了一些问题,一开始我们没有专门的运维人员,其实到目前为止我们也没有专门的运维人员,开发人员数量也有限。在创业初期,我们对开发效率又看的比较高。当然我们有一个好处,在一开始业务量级不像大公司那么大,所以一开始不太需要考虑大规模的挑战。
系统部署框架
结合我们的业务场景,这是我们构建的部署方案,因为我们最终交付给用户的产品是一个web端的东西,所以需要把前端代码部署在OSS上,把所有后端服务部署到k8s里面去,大部分还是k8s请求,做各个微服务之间的转法,微服务之间的通信我们用的是GRPC和RMQ。
这是我们整体的部署方案,部署方案完了,我们从一开始定位了运维和开发辅助工具,来帮助我们完成开发。
在日志跟踪方面,因为微服务,你的服务数量又多,又都是分布式的。包括线上预警、资源配置、存储管理。
微服务架构
微服务架构
接下来,我来介绍一下在我们公司内部微服务架构使用的一些实践。
首先微服务的架构,现在微服务架构已经成为一个主流,几乎很少有新的业务场景会使用单体应用的架构模式,大部分都会从一开始都定义出微服务。就以典型的小电商网站为例,基本架构,以前我要做一个电商网站,代码是一套,部署也是一套,几台机器。现在我们做了拆分,我们会按照业务率,比如会员注册、会员登陆、修改密码,都在会员系统里,会拆分成很多微服务。
微服务架构优势
微服务有什么好处?从领域模型纬度上,把原来很复杂的庞大系统拆分成了各个系统,各个系统之间通过这个接口来进行交互,这样以来各个业务系统变得很清晰。另一个好处是从开发人员的角度,因为这些微服务的拆分,比如我是做交易开发的,我肯定是基于交易的实现。针对会员我只关心接口怎么给我,但不关心具体实现,我编译的代码也不会影响到它的系统。所以这是微服务拆分的好处。
微服务粒度划分
微服务的粒度划分,业界都认可的一种方式是按照业务领域模型进行划分,比如会员系统和交易系统,它们俩在业务上有很强的隔离性,会员就管会员注册登陆方面,交易主要完成交易。这时候划分很清晰,大家也不会有什么意见。但如果交易跟退款,这俩业务模型算不算一个独立的业务模型?是分开还是合在一起?人们可能会认为它们两个是独立的业务领域,这是可以的。我一般的做法是按照开发人员纬度来配,比如退款有专门的团队或者专门的同学来管理,这时候就会拆分。如果交易和退款是同一个团队开发的,我觉得拆不拆无所谓,这时候不是很重要的。
快速构建一个微服务
再讲一下在我们内部如何快速构建一个微服务。强调快速的原因,也是因为在我们内部构建一个微服务是一个很轻松平常的事,不像之前体量比较大的时候,或者微服务不那么微的时候,这时候不太会去创建一个新的应用,除非你开一条新的应用线。在我们公司内部,平均每周会创建一个微服务,每周可能都要创建一个新的微服务,然后再部署上线。我们内部的微服务相对来说都比较简单,重点是提供接口,第一步定义好微服务的接口,微服务主要分三步走,第一定义接口,第二实现接口定义上线。第一定义接口,在微服务里面一旦把你的接口定义清楚,整个业务域就定义的很清楚了,接口定义清楚之后其他伙伴就可以拿接口做一些定向开发了。
还有一个要谈的话题是接口可拓展性问题,我们想象一下,在微服务没有拆分的时候我们把所有代码都写在一个工程里,这时候这些微服务除了没有互相隔离开,没有互相部署,这时候没有太大的差别。这时候互相调用,比如交易系统要去获取会员的信息,这时候也会调用会员的接口,只是这是一个纯代码,编译上就直接调用的东西。现在由于微服务的调用,我们强行把它部署到两台机器上。因为现在微服务的存在,会员已经上线了,交易这边有一个接口,做一个改动就会带来问题,所以接口扩展性非常重要。这时候如果想要扩展接口,只要多加一个字段都可以。
说起接口扩展性,确实很重要,以前也有参与国其他的,比如我们在定义这个接口的时候,我一开始定义输入是一个参数端,这时候如果想再加一个参数,我这个接口就得再写一个XXXV2这样的版本。另外还有一种场景,我们想要统一去分流你的远程调用的时候成本比较高,所以一开始设计好可扩展性很重要。
至于实现接口部分就是写具体业务逻辑的事项,这部分主要的意见是要给大家提供丰富的工具,能够让大家快速的实现,有一些配置性的东西要提供便捷的方式。比如有一个应用要裂变数据库,最好你能提供获取知识库连接很便捷的方式。因为使用大量的微服务以后,微服务每天配的就会很多,你每天重复配就会很烦,所以尽量用工具化的方式,以及约定大于配置的方式避免这些问题。
最后是配置上线,一个新人或者老人,最好能用自动化配置上线的方式,而不是传统的方式,我有一个配置上线,我先找运维申请一台机器资源,再去申请帐号密码等等。这些东西我们在内部用了一些脚本和工具自动化解决掉了,会自动分配这些东西。
快速构建微服务网关
这张图讲了网关,前面同学也提到网关,这个网关是把我们内部的服务转变,因为我们微服务接口是两个,所以这部分是通过网关来完成的,网关用了grpc-gateway的形式,这也是grpc生态下面开源的库。它的主要作用是同时提供gRPC服务,这样就可以实现把IPC和http。典型的场景,像获取用户信息,比如在前端页面上可能有一个我的信息,来获取我自己的邮箱以及头像信息。交易系统可能也会获取这样的信息,这时候它们可以走一套服务。另外gRPC还有一个功能,它能够自动生成文档。
微服务间通信
proto
在服务间通信这个方面,gRPC设计的目的是实现跨语言远程调用的协议,这是官方的示意图,比如构建一个C++Service,在我们公司使用的场景是同语言的,因为我们内部服务大部分都是golang。我们把client和server统统包在k8s集群内部,也就是后端服务全都是部署在k8s内部的,结合k8s的API来实现gRPC没有实现的负载均衡和服务发现这个环节,比如这个客户端要调用这个服务,它拿这个服务的名字去k8s的API查询服务对应的IP,然后再通过这个返回值调用匹配目标服务。
RabbitMQ
另外是RabbitMQ,消息模式在微服务间通信也是非常重要的模型,在互联网系统和微服务架构比较流行的情况下会非常适用,对于系统的解耦和服务非常有价值。比如当一个订单支付成功的时候,我可能有其他三个微服务都去订阅这个订单支付成功这个消息,这三个系统根据订单支付成功分别做了自己的业务逻辑,比如同时卖家发货等等,这主要是业务通信的环节,也是微服务通信中重要的一环。
服务追踪与治理
微服务的追踪跟治理,当微服务数量比较多,而且人员又比较少的时候,服务追踪、治理就显得比较重要了。因为确实太多了,如果凭过去一个一个LOW的方式可能管不过来。这时候从服务标准化和工具上面,一方面是把服务变成非常标准,另外一方面是提供各种各样的工具来管理和运维微服务。
微服务标准化
服务标准化,分为4个部分:
第一是各种命名规范标准化
第二是工程目录标准化
第三是标准化以后可以利用工具生来梳理
第四是标准化以后开发的效率也会提升
全链路日志追踪
接下来介绍一下服务治理的工具。第一个是全链路的日志追踪,每个微服务输入主要有三种,http、grpc、mq,以及会调用DB、redis、API、GRPC、MQ。
这时候我们在每个节点上都打上了插件,各个都打一些插件,投入这些插件采集出这些节点上面的输入输出,还有一些性能相关的数据,把这些数据通过一些格来存储到ES上去。
我们在这些插件上几乎记录了所有的信息,每个输入、输出以及异常情况。还有每个请求的执行时间、每个请求链路以及每个请求的上下文环境。
投入把这些链路记录下来就很容易实现一些功能,比如我要回放之前的调用情况,我只要拿到我记录下来的数据就可以进行回放。
性能监控,异常报警,流量监控
还有性能监控和异常率监控,比如每个接口错误率超过30%就会报警,这样基于日志就很容易做出来。还有业务异常监控,比如1万单突然跌到1千单,就可以报警。还有调用链路梳理、依赖关系图、定向问题排查,这些都有相应的定义,也会有相应的支持,包括在问题排查的时候也非常有用,这时候用这种工具回放排查这些问题就非常容易。
这是我们内部全链路日志追踪的系统,可以精准定位到某一个请求,当时我们返回到输入。我们还可以查找它的兄弟节点以及下游节点的信息,这样整个可以在调用链路上进行游走,精准找到问题的请求。
这是uber日志记录,它是分布式日志追踪系统,现在被纳入一个云基金会的项目,最近也比较火。它可以把调用链路梳理的非常清楚,比如gRPC的调用下面产生了四条调用,又调用了其他的gRPC,然后又做了其他的逻辑,里面可以看到很详细的信息,这样整个链路看得很清楚,并且你的日志都有上下文,非常容易对。在之前,如果你没有加入关联工具,挨个去找这些日志也是一件非常头疼的事情。
这是基于全链路的日志追踪做的,像调用次数、调用耗时以及接入率的监控与报警。
开源技术栈使用经验
最后给大家讲一下我们用到的一些开源的技术。在这一年过程中,确实感觉到开源技术给我们带来的用处非常大,很多都是依赖这些东西,尤其是小公司没有精力造轮子,往往是踩着别人的轮子。gRPC都讲过,是远程的调用。prest是一个很强大的东西,但是也有很大的风险,它可以把数据库里面的表或者试图直接暴露出http的接口,基于这个业务场景就可以返回一个表,我们完全可以不写任何一个代码,通过prest暴露出来。它的前身是postgrest,但因为后面懂h语言的人越来越少,懂golang的人越来越多,于是就把它迁移到golang上面。gorm和etcd大家也了解。grafana支持现在所有主流的数据源,你可以配置一些监控报警,比如我前面给大家展示的那几个图上,都是通过它来做的。如果你有业务需求,自己内部业务系统的数据分析可以拿它当一个图表工具来用。influxdb是一个数据库,性能非常好,以及还有kubernetes。我们公司运维基本上没有人,所以也没有人专门做这个事情,开发的人也不愿意做这些事情,基本上通过开发工具和脚本来解决这些问题。
创业公司技术栈选择
最后是创业公司技术栈选择的一些思考。创业公司相对来说在技术栈选择上面可选的空间更大一点,因为它没有那么大的业务包袱,也没有人员的包袱,这时候往往选技术栈就可以选择更合适、更顺手的,选择的空间大一点。所以选一个好的轮子也就非常重要了,轮子选的好,业务和系统跑的就会更快一点,跑的块才能活下来。在业务上面我们肯定要想办法跑的快,性能尽量按需优化。构建好领域模型,我认为系统架构抛开那些常规的,比如性能、运维、前后端不谈,内部实现最大的挑战往往是在你的业务领域模型上面,你的业务是什么样子,你根据这个业务构建的系统应该是什么样子,这部分挑战才是最大的,所以领域模型的构建是非常关键的一环。剩下就是常规的迭代速度和保持简单的技术方案,不要用太复杂的设计,别人都看不懂,你设计一个东西别人读起来也很麻烦,这样无论团队协作还是业务系统都不是很有利的方案。
Q&A
提问:我们现在也在做golang微服务的技术选型,您也提到了go-kit,现在有一些微服务开源的东西,像go-kit,您能不能大概讲一下为什么当初你们选型的时候没有选这两个,而选了go-kit?
张晓明:我个人不太喜欢框架类的东西,框架的东西是什么概念呢?他给你一个筐子你来填。我喜欢工具类的东西,他给你一个工具你来用,你想用就用,不想用就不用。因为我最注重开发效率,你怎么样能够跑的更快,所以我们觉得它对我们效率支撑有限,另外我们希望能够用工具化的方式来解决这些问题,其实本身golang的框架就已经很简单了。比如我们有数据库的管理服务,就可以为我们每个服务创建数据库的帐号密码。像这种东西通用的框架很难做,所以我们自己做,因为这个也花不了太多时间。
提问:微服务现在对创业公司来说,可能我要把微服务玩转还是有点难度,可能开发起来相对方便一点,但是出现问题调用链比较长,你们这边有什么比较好的经验?
张晓明:你可以加我微信,我一会儿发你具体的名字,它叫jaeger,他能够把你所有的日志都记录下来,统一存储,并且提供到这样的界面中来做查询。这些都可以点开,这就是一个典型的调用链,每个调用链上面点开以后可以看到里面详细的信息,因为我们做的比较全,把每个请求全都记下来了,所以都能够看见,你还能够看见这个请求具体发生在哪里,以及它当时的耗时等等问题。所以整个链路追踪起来非常爽,完全不需要担心这个问题,你以前即便是单体应用,这个日志查起来也很麻烦,尤其是服务内部的调用链路,就很难去管理。用这种微服务再结合这种jaeger,这个调用链就很容易看见,看一眼就能够知道它是干什么的,很清晰。
提问:你说是全链路的日志追踪,所有的请求你都全部记录,日志安全能不能保证?因为推特信息泄露就是从日志当中泄露的。
张晓明:你说的日志安全是什么意思?是服务器被入侵了,日志被偷走了?
提问:大概是这个意思。
张晓明:首先我这个日志在我公司内部的,它的安全性跟数据库的安全性是一样的,如果我的服务器被入侵了,数据库一样会泄露信息。这些日志的记录,它是我们运维排查的工具,它跟数据库是同样的安全性。
提问:我想补充一下,数据库上面的密码一般会加严,但日志上的密码有可能会明文打出来。
张晓明:主要是在用户登陆场景上面,他会输入密码并且传递过来,只有这个地方有风险,这个地方我们做了单独的处理。
主持人:你那边说全链路追踪,它会把调用记录等等都会记录下来,这个是在jaeger里面把这些信息记下来的吗?
张晓明:有很多节点,比如gorm会记,发消息接消息的端都会记等等。我们每个服务,比如http每个服务暴露都是统一的,比如我们基础的服务框架,我们内部会分一下http,我们会把gorm包一下就能够记下来了。像gRPC也是往里面打plugin,gRPC会提供一个插件的接口。数据库访问主要是注册回调事件,通过这种方式来记录。
主持人:所以每个请求都包了一层plugin?
张晓明:对。
主持人:你们现在全链路的东西基本上都是在你们应用层面里面调一些中间库的时候,这个中间库里包了一层?
张晓明:是的。
提问:微服务里面用户权限怎么控制?内部不控制还是怎么样?
张晓明:我们内部是不控制的。
张晓明:消息机制有专门的服务器,会维护消息订阅的状态,这个消息并不是直接投到订阅,里面会有消息服务器,消息服务器会负责发给订阅者。
提问:消息比较多了,后面的比如……
张晓明:消息会有堆积的情况,比如这边订阅者挂了或者订阅者消费能力非常弱,这时候有可能会出现消息堆积,我们用RabbitMQ的接口做了监控和报警,所以消息堆积也是微服务里面经常出现的问题,尤其这里面某一个服务出现了问题,这时候会导致消息堆积,消息堆积我们就用报警的方式,及时发现。
提问:我并不关心真的消息丢了如何模拟出来补起来的过程,因为以前碰到过,也没有很好的办法来解决。
张晓明:这时候我们一般是拿一个脚本补一下,直接拿一个脚本发一个消息是很容易的。
主持人:golang里面有nsq,当初你们的选择,如果所有技术栈都是基于golang,选择nsq可能更好一点,选择RabbitmQ你们当时选择的时候处于怎么考虑?