滴滴大型微服务框架设计实践

 

 

 

 

 

 

 

 

发现问题:服务开发过程当中的痛点

 

复杂业务开发过程当中的痛点

 

咱们在进行复杂业务开发的过程当中,有如下几个常见的痛点:php

• 时间紧、任务多、团队⼤、业务增⻓快,如何还能保证架构稳定可靠?前端

• 研发⽔平参差不⻬、项⽬压⼒⾃顾不暇,如何保证质量基线不被突破?linux

• 公司有各类⼯具平台、SDK、最佳实践,如何尽量的在业务中使⽤?git

 

互联网业务研发的特色是“快”、“糙”、“猛”:开发节奏快、质量较粗糙、增加迅猛。咱们可否作到“快”、“猛”而“不糙”呢?这就须要有一些技术架构来守住质量基线,在业务快速堆砌代码的时候也能保持技术架构的健康。github

 

在大型项目中,咱们也常常会短期汇集一批人参与开发,很显然咱们没有办法保证这些人的能力和风格是彻底拉齐的,咱们须要尽量减小“人”在项目质量中的影响。redis

 

公司内有大量优秀的技术平台和工具,业务中确定是但愿尽量都用上的,但又不想付出太多的使用成本,一定须要有一些技术手段让业务与公司基础设施无缝集成起来。sql

 

很天然咱们会想到,有没有一种“框架”能够解决这个问题,带着这个问题咱们探索了全部的可能性并找到一些答案。docker

服务框架进化史

服务框架的历史能够追溯到 1995 年,PHP 在那一年诞生。PHP 是一个服务框架,这个语言首先是一个模板,其次才是一种语言,默认状况下全部的 PHP 文件内容都被直接发送到客户端,只有使用了 <?php ?> 标签的部分才是代码。在这段时间里,咱们也称做 Web 1.0 时代里,浏览器功能还不算强,不少的设计理念来源于 C/S 架构的想法。这时候的服务框架的巅峰是 2002 年推出的 ASP.net,当年真的是很是惊艳,咱们能够在 Visual Studio 里面经过拖动界面、双击按钮写代码来完成一个网页的开发,很是具备颠覆性。固然,因为当时技术所限,这样作出来的网页体验并不行,最终没有成为主流。后端

 

接着,Web 2.0 时代来临了,你们愈来愈以为传统软件中常用的 MVC 模式特别适合于服务端开发。Django 发布于 2003 年,这是一款很是经典的 MVC 框架,包含了全部 MVC 框架必有的设计要素。MVC 框架的巅峰当属 Ruby on Rails,它给咱们带来了很是多先进的设计理念,例如“约定大于配置”、Active Record、很是好用的工具链等。设计模式

 

2005 年后,各类 MVC 架构的服务框架开始井喷式出现,这里我就不作一一介绍。

 

标志性服务框架

随着互联网业务愈来愈复杂,前端逻辑愈来愈重,咱们发现业务服务开始慢慢分化:页面渲染的工做回到了前端;Model 层逐步下沉成独立服务,而且催生了 RPC 协议的流行;业务接入层只须要提供 API。因而,MVC 中的 V 和 M 逐步消失,演变成了路由框架和 RPC 框架两种形态,分别知足不一样的需求。2007 年,Sinatra 发布了,它是一个很是极致的纯路由框架,大量使用 middleware 设计来扩展框架能力,业务代码能够实现的很是简洁优雅。这个框架相对小众(Github Stars 10k,实际也算颇有名了),其设计思想影响了不少后续框架,包括 Express.js、Go martini 等。同年,Thrift 开源,这是 Facebook 内部使用 RPC 框架,如今被普遍用于各类微服务之中。Google 其实更早就在内部使用 Protobuf,不过直到 2008 年才首次开源。

 

再日后,咱们的基础设施开始发生重大变革,微服务概念兴起,虚拟化、docker 开始愈来愈流行,服务框架与业务愈加解耦,甚至能够作到业务几乎无感知。2018 年刚开源的 Istio 就是其中的典型,它专一于解决网络触达问题,包括服务治理、负载均衡、动态扩缩容等。

 

服务框架的演进趋势

 

经过回顾服务框架的发展史,咱们发现服务框架变得愈来愈像一种新的“操做系统”,愈来愈多的框架让咱们忘记了 Web 开发有多么复杂,让咱们能专一于业务自己。就像操做系统同样,咱们在业务代码中觉得直接操做了内存,但其实并否则,操做系统为咱们屏蔽了总线寻址、虚地址空间、缺页中断等一系列细节,这样咱们才能将注意力放在怎么使用内存上,而不是这些跟业务无关的细节。

随着框架对底层的抽象愈来愈高,框架的入门门槛在变低,之前咱们须要逐步学习框架的各类概念以后才能开始写业务代码,到如今,不少框架都提供了很是简洁好用的工具链,使用者很快就能专一输出业务代码。不过这也使得使用者更难以懂得框架背后发生的事情,想要作一些更深层次定制和优化时变得相对困难不少,这使得框架的学习曲线愈加趋近于“阶跃式”。

 

随着技术进步,框架也从代码框架变成一种运行环境,框架代码与业务代码也不断解耦。这时候就体现出 Go 的一些优越性了,在容器生态里面,Go 占据着先发优点,同时 Go 的 interface 也很是适合于实现 duck-typing 模式,避免业务代码显式的与框架耦合,同时 Go 的语法相对简单,也比较容易用一些编译器技巧来透明的加强业务代码。

 

 

⼤道⾄简:⼤型微服务框架的设计要点

 

站在全局视角观察微服务架构

 

服务框架的演进过程是有历史必然性的。

 

传统 Web 网站最开始只是在简单的呈现内容和完成一些单纯的业务流程,传统的“三层结构”(网站、中间件、存储)就能够很是好的知足需求。

 

Web 2.0 时代,随着网络带宽和浏览器技术升级,更多的网站开始使用前端渲染,服务端则更多的退化成 API Gateway,先后端有了明显的分层。同时,因为互联网业务愈来愈复杂,存储变得愈来愈多,不一样业务模块之间的存储隔离势在必行,这种场景催生了微服务架构,而且让微服务框架、服务发现、全链路跟踪、容器化等技术日渐兴盛,成为如今讨论的热点话题,而且也出现了大量成熟可用的技术方案。

 

再日后呢?咱们在滴滴的实践中发现,当一个公司的组织结构成长为多事业群架构,每一个事业群里面又有不少事业部,下面还有各类独立的部门,在这种场景下,微服务之间也须要进行隔离和分层,一个部门每每会须要提供一个 API 或 broker 服务来屏蔽公司内其余服务对这个部门服务的调用,在逻辑上就造成了由多个独立微服务构成的“大型微服务”。

在大型微服务架构中,技术挑战会发生什么变化?

 

据我所知,国内某一线互联网公司的一个事业群里部署了超过 10,000 个微服务。你们能够思考一下,假如一个项目里面有 10,000 个 class 而且互相会有各类调用关系,要设计好这样的项目而且让它容易扩展和维护是否是很困难?这是必定的。若是咱们把一个微服务类比成一个 class,为了可以让这么复杂的体系能够正常运转,咱们必须给 class 进行更进一步的分类,造成各类 class 之上的设计模式,好比 MVC。以咱们开发软件的经验来看,当开发单个 class 再也不成为一件难事的时候,如何架构这些 class 会变成咱们设计的焦点。

 

咱们看到前面是框架,更多解决是平常基础的东西,可是对于人与人之间如何高效合做、很是复杂的软件架构如何设计与维护,这些方面并无解决太好。

 

大型微服务的挑战刚好就在于此。当咱们解决了最基本的微服务框架所面临的挑战以后,如何进一步方便架构师像操做 class 同样来重构微服务架构,这成了大型微服务框架应该解决的问题。这对于互联网公司来讲是一个问题,好比我所负责的业务整个代码量几百万行,看起来听多了,但跟传统软件比就没那么吓人。之前 Windows 7 操做系统,总体代码量一亿行,其中最大的单体应用是 IE 有几百万行代码,里面的 class 也有上万个了。对于这样规模的软件要注意什么呢?是各类重构工具,要能一键生成或合并或拆分 class,要让软件的组织形式足够灵活。这里面的解决方法能够借鉴传统软件的开发思路。

 

大型微服务框架的设计目标

结合上面这些分析,咱们意识到大型微服务框架其实是开发人员的“效率产品”,咱们不但要让一线研发专一于业务开发,也要让你们几乎无感知的使用公司各类基础设计,还要让架构师可以很是轻易的调整微服务总体架构,方便像重构代码同样重构微服务总体架构,从而提高架构的可维护性。

 

公司现有架构就是业务软件的操做系统,无论公司现有架构是什么,全部业务架构必须基于公司现有基础进行构建,没有哪一个部门会在作业务的时候分精力去作运维系统。如今全部的开源微服务框架都不知道你们底层实际在用什么,只解决一些通用性问题,要想真的落地使用还须要作不少改造以适应公司现有架构,典型的例子就是 dubbo 和阿里内部的 HSF。为何内部不直接使用 dubbo?由于 HSF 作了不少跟内部系统绑定的事情,这样可让开发人员用的更爽,但也就跟开源的系统渐行渐远了。

 

大型微服务框架是微服务框架之上的东西,它是在一个或多个微服务框架之上,进一步解决效率问题的框架。提高效率的核心是让全部业务方真正专一于业务自己,而不是想不少很重复的问题。若是 10,000 个服务花 5,000 人维护,每一个人都思考怎么接公司系统和怎么作好稳定性,就算每次开发过程当中花 10% 的时间思考这些,也浪费了 5,000 人的 10% 时间,想一想都不少,省下来能够作不少业务。

 

Rule of least power

 

要想设计好大型微服务框,咱们必须遵循“Rule of least power”(够用就好)的原则。

 

这个原则是由 WWW 发明者 Tim Berners-Lee 提出的,它被普遍用于指导各类 W3C 标准制定。Tim BL 说,最好的设计不是解决全部问题,而是刚好解决当下问题。就是由于咱们面对的需求其实是多变的,咱们也不肯定别人会怎么用,因此咱们要尽量只设计最本质的东西,减小复杂性,这样作反而让框架具备更多可能性。

 

Rule of least power 其实跟咱们一般的设计思想相左,通常在设计框架的时候,架构师会比较倾向于“大而全”,因为咱们通常都很难预测框架的使用者会如何使用,因而天然而然的会提供想象中“可能会被用到”的各类功能,致使设计愈来愈可扩展的同时也愈来愈复杂。各类软件框架的演进历史告诉咱们,“大而全”的框架最终都会被使用者抛弃,并且抛弃它的理由每每都是“过重了”,很是具备讽刺意味。

 

框架要想设计的“好”,就须要抓住需求的本质,只有真正不变的东西才能进入框架,还没想清楚的部分不要轻易归入框架,这种思想就是 Rule of least power 的一种应用方式。

 

大型微服务框架的设计要点

 

结合 Rule of least power 设计思想,咱们在这里列举了大型微服务框架的设计要点。

 

最基本的,咱们须要实现各类微服务框架必有的功能,例如服务治理、水平扩容等。须要注意的是,在这里咱们并不会再次重复造轮子,而是大量使用公司内外已有的技术积累,框架所作的事情是统一并抽象相关接口,让业务代码与具体实现解耦。

 

从工具链层面来讲,咱们让业务无需操心开发调试以外的事情,这也要求与公司各类进行无缝集成,下降使用难度。

 

从设计风格上来讲,咱们提供很是有限度的扩展度,仅在必要的地方提供 interceptor 模式的扩展接口,全部框架组件都是以“组合”(composite)而不是“继承”(inherit)方式提供给开发者。框架会提供依赖注入的能力,但这种依赖注入与传统意义上 IoC 有一点区别,咱们并不追求框架全部东西均可以 IoC,只在咱们以为必要的地方有限度的开放这种能力,用来方便框架兼容一些开源的框架或者库,而不是让业务代码轻易的改变框架行为。

 

大型微服务框架最有特点的部分是提供了很是多的“可靠性”设计。咱们刻意让 RPC 调用的使用体验跟普通的函数调用保持一致,使用者只用关系返回值,永远不须要思考崩溃处理、重试、服务异常处理等细节。访问基础服务时,开发者能够像访问本地文件同样的访问分布式存储,也是不须要关心任何可用性问题,正常的处理各类返回值便可。在服务拆分和合并过程当中,咱们的框架可让拆分变得很是简单,真的就跟类重构相似,只须要将一个普通的 struct methods 进行拆分便可,剩下的全部事情天然而然会由框架作好。

 

 

精雕细琢:框架关键实现细节

 

业务实践

 

接下来,咱们聊聊这个框架在具体项目中的表现,以及咱们在打磨细节的过程当中积累的一些经验。

 

咱们落地的场景是一个很是大型的业务系统,2017 年末开始设计并开发。这个业务已经出现了五年,各个巨头已经投入上千名研发持续开发,很是复杂,咱们不可能在上线之初就完善全部功能,要这么作起码得几百人作一年,咱们等不起。实际落地过程当中,咱们投入上百人从一个最小系统慢慢迭代出来,最第一版本只开发了四个多月。

 

最开始作技术选型时,咱们也在思考应该用什么技术,甚至什么语言。因为滴滴从 2015 年以来已经积累了 1,500+ Go 代码模块、上线了 2,000+ 服务、储备了 1000+ Go 开发者,这使得咱们很是天然的就选择 Go 做为最核心的开发语言。

 

在这个业务中咱们实现了很是多的核心能力,基本实现了前面所说大型微服务框架的各类核心功能,并达成预期目标。

 

同时,也由于滴滴拥有相对完善的基础设施,咱们在开发框架的时候也并无花费太多时间重复造一些业务无关的轮子,这让咱们在开发框架的时候也能专一于实现最具备特点的部分,客观上帮助咱们快速落地了总体架构思想。

 

上图只是简单列了一些咱们业务中经常使用的基础设施,其实还有大量基础设施也在公司中被普遍使用,没有说起。

 

总体架构

上图是咱们框架的总体架构。绿色部分是业务代码,黄色部分是咱们的框架,其余部分是各类基础设施和第三方框架。

 

能够看到,绿色的业务代码被框架整个包起来,屏蔽了业务代码与底层的全部联系。其实咱们的框架只作了一点微小的工做:将业务与全部的 I/O 隔离。将来底层发生任何变化,即便换了下面的服务,咱们可以经过黄色的兼容层解决掉,业务一行代码不用,底层 driver 作了任何升级业务也彻底不受影响。

 

结合微服务开发的经验,咱们发现微服务开发与传统软件开发惟一的区别就是在于 I/O 的可靠程度不一样,之前咱们花费了大量的时间在各类不一样的业务中处理“稳定性”问题,其实归根结底都是相似的问题,本质上就是 I/O 不够可靠。咱们并非要真的让 I/O 变得跟读取本地文件同样可靠,而是由框架统一全部的 I/O 操做并针对各类不可靠场景进行各类兜底,包括重试、节点摘除、链路超时控制等,让业务获得一个肯定的返回值——要么成功,要么就完全失败,无需再挣扎。

 

实际业务中,咱们使用 I/O 的种类其实不多,也就不过十几种,咱们这个框架封装了全部可能用到的 I/O 接口,把它们所有变成 Go interface 提供给业务。

 

实现要点

 

前面说了不少思路和概念,接下来我来聊聊具体的细节。

 

咱们的框架跟不少框架都不同,为了实现框架与业务正交,这个框架干脆连最基本的框架特征都没有,MVC、middleware、AOP 等各类耳熟能详的框架要素在这里都不存在,咱们只是设计了一个执行环境,业务只须要提供一个入口 type,它实现了全部业务须要对外暴露的公开方法,框架就会自动让业务运转起来。

 

咱们同时使用两种技术来实现这一点。一方面,咱们提供了工具链,对于 IDL-based 的服务框架,咱们能够直接分析 IDL 和生成的 Go interface 代码的 AST,根据这些信息透明的生成框架代码,在每一个接口调用先后插入必要的 stub 方便框架扩展各类能力。另外一方面,咱们在程序启动的时候,经过反射拿到业务 type 的信息,动态生成业务路由。

 

作到了这些事情以后业务开发就彻底无需关注框架细节了,甚至咱们能够作到业务像调试本地程序同样调试微服务。同时,咱们用这种方式避免业务思考“版本”这个问题,咱们看到,不少服务框架都由于版本分裂形成了很大的维护成本,当咱们这个框架成为一个开发环境以后,框架升级就变得彻底透明,实际中咱们会要求业务始终使用最新的框架代码,历来不会使用 semver 标记版本号或者兼容性,这样让框架的维护成本也大大下降。“更大的权力意味着更大的责任”,咱们也为框架写了大量的单元测试用例保证框架质量,而且规定框架无限向前兼容,这种责任让咱们很是谨慎的开发上线功能,很是收敛的提供接口,从而保持业务对框架的信任。

 

 

你们也许据说过,Go 官方的 database/sql 的 Stmt 很好用可是有可能会出现链接泄漏的问题,当这个问题刚被发现的时候,公司不少业务线都不得不修改了代码,在业务中避免使用 Stmt,而咱们的业务代码彻底不须要作任何修改,框架用很巧妙的方法直接修复了这个问题。

 

下图是框架的启动逻辑,能够看到,这个逻辑很是简单:首先建立一个 Server 实例 s,传入必要的配置参数;而后新建一个业务类型实例 handler,这个业务类型只是个简单的 type,并无任何约束;最后将接口 IDL interface 和 handler 传入 s,启动服务便可。

 

咱们在 handler 和 IDL interface 之间加一个夹层并作了不少事情,这至关于在业务代码的执行开始和结束先后插入了代码,作了参数预处理、日志、崩溃恢复和清理工做。

 

咱们还须要设计一个接口层来隔绝业务和底层之间的联系。接口层自己没什么特别技术含量,只是须要认真思考如何保证底层接口很是很是稳定,而且如何避免穿透接口直接调用底层能力,要作好这一点须要很是多的心力。

 

这个接口层的收益是比较容易理解的,能够很好的帮助业务减小无谓的代码修改。开源框架就不能保证这一点,说不定何时做者心情好了改了一个框架细节,没法向前兼容,那么业务就必须跟着作修改。公司内部框架则通常不太敢改接口,生怕形成不兼容被业务投诉,但有些接口一开始设计的并很差,只好不断打补丁,让框架愈来愈乱。

 

要是真能作到接口层设计出来就再也不变动,那就太好了。

 

 

那咱们真的能作到么?是的,咱们作到了,其中的诀窍就是始终思考最本质最不变的东西是什么,只抽象这些不变的部分。

 

上图就是一个经典案例,展现一下咱们是怎么设计 Redis 接口的。

 

左边是 github.com/go-redis/redis 代码(简称 go-redis),这是一个很是著名的 Redis driver;右边是咱们的 Redis 接口设计。

 

Go-redis 很是优秀,设计了一些很不错的机制,好比 Cmder,巧妙的解决了 Pipeline 读取结果的问题,每一个接口的返回值都是一个 Cmder 实例。但这种设计并不本质,包括函数的参数与返回值类型都出现屡次修改,包括我本身都曾经提过 Pull Request 修正它的一个参数错误问题,这种修改对于业务来讲是很是头疼的。

 

而咱们的接口设计相比 go-redis 则更加贴近本质,我阅读了 Redis 官方全部命令的协议设计和相关设计思路文档,Redis 里面最本质不变的东西是什么呢?固然是 Redis 协议自己。Redis 在设计各类命令时很是严谨,作到了极为严格的向前兼容,不管 Redis 从 1.0 到 3.x 如何变化,各个命令字的协议从未发生过不兼容的变化。所以,我严格参照 Redis 命令字协议设计了咱们的 Redis 接口,链接口的参数名都尽可能与 Redis 官方保持一致,并严格规定各类参数的类型。

 

咱们当心的进行接口封装以后,还有一些其余收获。

 

仍是以 Redis 为例,最开始咱们底层的 Redis driver 使用的是公司普遍采用的 github.com/gomodule/redigo,但后来发现不能很好的适配公司自研的 Redis 集群一些功能,因此考虑切换成 go-redis。因为咱们有这样一层 Redis 接口封装,这使得切换彻底透明。

 

咱们为了可以让业务研发不要关心不少的传输方面细节,咱们实现了协议劫持。HTTP 很好劫持,这里再也不赘述,我主要说一下如何劫持 thrift。

 

劫持协议的目的是控制业务参数收到或发送的协议细节,能够方便咱们根据传输内容输出必要的日志或打点,还能够自动处理各类输入或输出参数,把必要参数带上,省得业务忘记。

 

劫持思路很是简单,咱们作了一个有限状态机(FSM),在旁路监听协议的 read/write 过程并还原整个数据结构全貌。好比 Thrift  Protocol,咱们利用 Thrift 内置的责任链设计,本身实现了一个 protocol factory 来包装底层的 protocol,在实际 protocol 之上作了一个 proxy 层拦截全部的 ReadXXX/WriteXXX 方法,就像是在外部的观察者,记录如今 read/write 到哪个层级、读写了什么结构。当咱们发现如今正在 read/write 咱们感兴趣的内容,则开始劫持过程:对于 read,若是要“欺骗”应用层提供一些额外的框架数据或者屏蔽框架才关心的数据,咱们就会篡改各类 ReadXXX 返回值来让应用层误觉得读到了真实数据;对于 write,若是要偷偷注入框架才关心的内容,咱们会在调用 WriteXXX 时主动调用底层 protocol 的相关 write 函数来提早写入内容。

 

协议能够劫持以后,不少东西的处理就很简单了。好比 context,咱们只要求业务在各个接口里带上 context,RPC 过程当中则无需关心这个细节,框架会自动将 context 经过协议传递到下游。

 

咱们实现了协议劫持以后,要想实现跨服务边界的 context 就变得很简单了。

 

咱们根据 context interface 和设计规范实现了本身的 context 类型,用来作一些序列化与反序列化的事情,当上下游调用发生时,咱们会从 context 里提取框架关心的内容并注入到协议里面,在下游再透明解析出来从新放入 context。

 

使用 context 时候还有个小坑:context.WithDeadline 或者 context.WithTimeout 很容易被不当心忽略返回的 cancel 函数,致使 timer 资源泄露。咱们为了不出现这种状况设计了一个低精度 timer 来尽量避免建立真正的 time.Time 实例。

咱们发现,业务中根本不须要那么高精度的 timer,咱们说的各类超时通常精度都只到 ms,因而一个精度达 0.5ms 的 timer 就能知足全部业务需求。同时,在业务中也不是特别须要使用 Context interface 的 Done() 方法,更多的只是判断一下是否已经超时便可。为了不大量建立 timer 和 channel,也为了不让业务使用 cancel 函数,咱们实现了一个低精度 timer pool。这是一个 timer 的循环数组,将 1s 分割成若干个时间间隔,设置 timer 的时候其实就是在这个数组上找到对应的时刻。默认状况下,done channel 都不须要初始化,直到真正有业务方须要 done channel 的时候才会 make 出来。在框架里咱们很是注意的避免使用任何 done channel,从而避免消耗资源且极大的提升了性能。

 

业务压力大的时候,咱们比较容易在代码层面上犯错,不当心就放大单点故障形成雪崩,咱们借用前面全部的技术,让调用超时约束从上游传递到下游,若是单点崩溃了,框架会自动摘除故障节点并自动 fail-fast 避免压力进一步上升,从而实现防雪崩。

防雪崩的具体实现原理很简单:上游调用时会设置一个超时时间,这个时间经过跨边界 context 传递到下游,每一个下游节点在收到请求时开始记录本身消耗的时间,若是本身耗时已经超出上游规定的超时时间就会主动中止一切 I/O 调用,快速返回错误。

好比上游 A 调用下游 B 前设置 500ms 超时,B 收到请求后就知道只有 500ms 可用,从收到请求那一刻开始计时,每次在调用其余下游服务前,好比访问 B 的下游 C 自己须要 200ms,但当前 B 已经消耗了 400ms,只剩 100ms 了,那么框架会自动将 C 的超时收敛到 100ms,这样 C 就知道给本身的时间很少了,一旦 C 没能在 100ms 内返回就会主动 fail-fast,避免无谓的消耗系统资源,帮助 C 和 B 快速向上游报告错误。

 

业务收益

 

咱们实现的这个框架切实的给业务带来了显著的收益。

咱们总共用超过 100 名 Go 语言开发者,在很是大的压力下开发了好几个月便完成一个完整可运营的系统,实现了大量功能,开发效率至关的高。咱们后来代码量和服务数量也不断增长,而且因为业务发展咱们还支持了国际化,实现了多机房部署,这个过程是比较顺畅的。

 

我以为很是自豪的是,咱们刚上线一个月就作了全链路压测,框架层稍做修改就搞定了,显著提高了总体系统稳定性和抗压能力,而这个过程对业务是彻底透明的,对业务将来的迭代也是彻底透明的。咱们在线上也没有出现过任何单点故障形成的雪崩,各类监控和关键日志也是自动的透明的作好,服务注册发现、底层 driver 升级、一些框架 bug 修复等对业务都十分透明,业务只用每次升级到最新版就行了,十分省心。

 

版本管理

 

最后提一个细节:管理框架的各个库版本。

 

我相信不少开发者都有一种烦恼,就是管理各类分裂的代码版本。一方面因为框架会不断升级,须要不断用 semver 规则升级版本,另外一方面业务方又没有动力及时升级到最新版,致使框架各个库的版本事实上出现了分裂。这个事情实际上是不该该发生的,就像咱们用操做系统,好比你们开发业务须要跑在线上 linux 服务器上,咱们会关心 linux kernel 版本么?或者用 Go 开发,咱们会老是关心用什么 Go 版本么?通常都不会关心的,这跟开发业务没什么关系。咱们关心的是系统提供了哪些跟业务开发相关的接口,只要接口不变且稳定,业务代码就能正常的工做。

 

这是为何咱们在设计框架的时候会花费不少心力保证接口稳定的缘由,咱们就是但愿框架即操做系统,只有作到这一点,业务才能放心大胆的用框架作业务,真正把业务作到快而不糙。也正由于这一点,咱们甚至于不会给框架的各个库打 tag,每次上线都必须所有将框架升级到最新版,完全的解决了版本分裂的问题。

 

将来方向

 

将来咱们仍是有不少工做值得去作,好比完善工具链、接入更多的一些公司基础设施等。

 

咱们不肯定是否可以开源,大几率是不会开源,由于这个框架并不重要,它与滴滴各类基础设施绑定,服务于滴滴研发,重要的是设计理念和思路,你们能够用相似方法因地制宜的在本身的公司里实践这种设计思想。

 

今天这个活动就是一个很好的场所,我但愿经过这个机会跟你们分享这样的想法,若是你们有兴趣也欢迎跟我交流,我能够帮助你们在公司里实现相似的设计。

 

Q&A

 

提问:我也一直在写 Go 服务,大家每个服务启动是单进程仍是多进程,每一个进程怎么限制核数?

杜欢:对于 Go 来说这个问题不是问题,通常都用单进程模式,而后经过 GOMAXPROCS 设置须要占用的核数,默认会占满机器全部的核。

 

提问:我看到有 70+ 个微服务,微服务之间的接口和依赖关系怎么维护?接口变动或者兼容性怎么解决?

杜欢:微服务业务层的接口变动这个事情没法避免,咱们是经过 IDL 进行依赖管理,不是框架层保证,业务须要保证这个 IDL 是向前兼容的。框架能帮咱们作什么呢?它能够帮咱们作业务代码迁移,根据咱们的设计,只要把一个名为 service 的目录进行拆分合并便可,这里面只有一个简单的类型 type Service struct {},以及不少 Service 类型的方法,每一个文件都实现了这个类型的一个或多个方法,咱们能够方便的整合或者拆分这个目录里面的代码,从而就能更改微服务的接口实现。

你刚刚问题是很业务的问题,怎么管理之间依赖变化,这个没有什么好办法,咱们作重构的时候,仍是通知上下游,这个确实不是咱们真正在框架层可以解决的问题,咱们只能让重构的过程变得简单一些。

 

提问:上下游传输 context 时设置超时时间,每个接口超时时间是怎么设计的?

杜欢:咱们设的超时时间就是一般意义上的此次请求从发起到收到应答的总时间。

 

提问:超时时间怎么定?各个模块超时时间不同么?

杜欢:如今作得比较粗糙,尚未作到统一管理全部的超时时间,依然是业务方本身根据预期,在调用下游前本身在代码里面写的,但愿将来这个能够作到统一管理。

 

提问:开发者怎么知道下游通过了怎样的处理流程,能多长时间返回呢?

杜欢:这个东西通常开发者都是知道的,由于全部业务服务接口都会有 SLA,全部服务对上游承诺 SLA 是多少预先会定好。好比一个服务接口承诺 SLA 是 90 分位 50ms,上游就会在这个基础上打一些 buffer,将调用超时设置成 70ms,比 SLA 大一点。实际中咱们会结合这个服务接口在压测和线上实际表现来设置超时。咱们其实很但愿把 SLA 线上化管理,不过如今没有彻底作到这一点。

 

提问:我们这边有没有出现相似的超时状况?在测试期间或者线上?

杜欢:服务的时间超时状况很是常见,但业务影响很小,框架会自动重试。

 

提问:通常什么状况下会出现呢?

杜欢:最多的状况是调用外部的服务,好比咱们会调用 Google Map 一些接口,他们就相对比较不稳定,调用一次可能会超过 2s 才返回结果,致使这条链路上的全部接口都会超时。

 

提问:超时的状况能够避免么?

杜欢:不可能彻底避免。一个服务接口不可能 100% 承诺本身的处理时间,就算 SLA 是 99 分位小于 50ms,那依然有 1% 可能性会超过这个值。

相关文章
相关标签/搜索