微服务那么热,创业公司怎么选用实践?
近年来,后台服务领域最火的方向非微服务莫属。本文从微服务历史、现状回顾开始,用实际案例落地实践中的问题。虽是创业公司的经验,却可广而用之。
老司机简介
陈辉,曾任职Google、阿里巴巴、Facebook,现任杭州映兔科技CTO,创业中。
前言互联网公司的核心技术资产之一是云端运行的后台服务。
近些年在后台服务领域有多个热门趋势,如容器化、微服务、DevOps等,如果要找出一个最火的方向,非微服务莫属。事实上,其他的热门趋势很多是伴随微服务技术兴起的。
在这篇文章里,我们首先阐述了微服务的历史和现状,并用实际案例回答两个大家非常关注的问题,“为什么要采用微服务架构”和“如何构建微服务应用”。同时,我们也分享了创业公司在容器化、服务治理、DevOps、团队组织等方面的第一手案例,希望对同在创业的小伙伴有帮助。
微服务简史
微服务(microservices)的概念已经存在了好多年,其前身是“面向服务的架构”(SOA,serviceorientedarchitecture)。但SOA并没有大范围流行并取代单体应用(monolithicapplication,可以简单理解成只有一个binary的应用),主要有两个原因:
1、采用SOA的好处主要来自项目模块化而非模块服务化。
SOA化的第一步是将单体应用拆分为多个模块,当你完成了这一步,并且有好的开发、测试和集成工具保证大团队在一个巨型代码库里协同工作的时候,你已经得到了“SOA化”带来的绝大多数好处。然后大家会对进一步模块服务化产生合理质疑,因为这需要大量重构工作,而带来的生产效率的提升并不那么明显,投入产出比并不高。
实际上相当数量的公司止步于此,比如Google的Adsense系统枢纽(mixer)就是一个巨大的单体应用,数百个项目从一个中央repository里编译为一个上GB的二进制程序,然后通过金丝雀部署(canary)做线上版本迭代。
2、SOA本身并没有解决一个核心问题:多服务运维。
开源社区比较流行的方案比如thrift、protobufRPC、json-rpc等,只实现了多语言服务接口框架,最多加上服务治理的功能(比如dubbo),并没有提供完整的运维解决方案。缺乏运维工具的支持导致单体应用切换为SOA的代价远高于分布式服务带来的好处。之前需要运维一个单体应用,而现在需要运维100个小的服务,运维工作量随着服务数量的增加而线性增加。
相比SOA,微服务提供了一套更为完整的解决方案,很重要的原因是近几年开源界出现的一些工具极大降低了微服务的运维门槛,从而使得多服务的优势更为凸显。
对微服务最好的诠释是MartinFowler在2014年写的一篇文章,详细描述了微服务区别于单体服务的九个特征。具体内容大家可以看他的原文【备注1】,下面我尝试用最直白的语言来概况:
微服务的九大特征
1、服务即组件。一个服务实现一个组件,这有两个好处:服务可以独立部署,youonlydeploywhatyoudevelop;更清晰的模块边界,每个服务提供方可以专注发布API,隐藏实现细节和版本。
2、按照业务域来组织微服务。通过微服务边界划分业务边界,一个跨职能team(包含完整的UI、中间件、DBA工程师)完全掌控业务内的微服务。
3、按产品而非项目划分微服务。和第二条类似,一个团队负责完整的端到端开发和维护,youbuild,yourunit。
4、关注业务逻辑,而非服务间通讯。换句话说,所有微服务调用使用统一协议,这个协议将输入输出从底层实现细节中抽象出来,微服务团队只需要关注如何将输入转化为输出的逻辑,而不需要考虑网络层实现细节。
5、分散式管理。只关注接口实现的功能,对如何实现不做强制规范。换句话说,每个微服务团队有充分自由选择自己团队熟悉的编程语言、数据库和其他中间件等技术栈。
6、分散式数据。每个微服务有自己的数据库,并且这些数据库不可被其他微服务直接访问,所有数据的读写操作都要通过微服务接口完成。
7、基础设施自动化,包括服务自动化构建、部署。强大的自动化测试和部署工具是微服务的必要条件,我们在下文中会进一步阐述。
8、容错。微服务设计允许服务调用出错,调用方在调用失败时不应该产生灾难性结果。其实容错设计和微服务无关,任何好的模块化设计必须是允许调用失败的。只不过微服务将容错设计作为一个强条件,换句话说,如果你的服务决不允许调用失败的,那最好使用单体架构。
9、进化。使用合适的工具,你可以更快地更频繁更快地对系统做修改,因为你可以专注在一个服务上,而无需对整个单体应用做改动。
这九个特性都很抽象,我们在实践中发现,如果用一条特征来描述微服务带来的最大好处的话,那就是对团队协作方式的影响:
微服务极大降低了团队成员工作的耦合度,解放了每个人的工作效率。毕竟任何工程问题最后都是工程师问题,这可能不是微服务设计的本意,但对人的工作方式的改变却成了最大的意外惊喜。
何时不需要微服务
在开始对微服务的讨论之前,我们需要泼一盆冷水,帮你看到这项技术的适用边界在哪里。
如果你的项目满足下面的条件之一,那么不需要微服务。
1、你的代码没有模块化
不是所有的代码都模块化了,有可能你的项目还没有复杂到需要抽离成几个模块。但多数项目没有模块化都是因为没有很好地设计,建议你从最开始设计时就考虑到模块,或者把你的历史代码重构为多个模块。这是好的设计的一部分,和用不用微服务无关。
即便你的代码模块化了,也未必需要微服务,比如
2、你的服务要求极高的性能
要求极低的延迟或者无法容忍服务间调用出错,比如广告、高频交易中负责关键路径的系统,最好把所有的模块都打包编译成为单体应用。不过,多数互联网的业务系统不属于此类。
3、你没有一个好的容器编排系统
容器化几乎是微服务的先决条件,如果你没有一个好的容器编排系统帮你解决服务发现、负载均衡、弹性扩容、自动化部署等问题,运维上的负担就会大大超过微服务带来的好处。
MartinFowler有一张著名的图说明这个问题【来源2】
图中横轴是代码复杂度,纵轴是生产效率,蓝线是微服务,绿线是单体服务。当代码复杂度不高时(最左边),运维多服务的代价会超过微服务带来的效率提升,这时单体服务效率占优。而当代码变复杂时情况出现反转,微服务带来的解耦导致效率高于单体应用。
这个图做于2015年5月,这一年多来开源社区的容器编排解决方案已经有了长足进步,比如Kubernetes、Mesos、Swarm等越来越成熟,同时诞生了很多企业级的CaaS(containerasaservice)服务比如AmazonECS、GoogleContainerEngine、国内的DaoCloud、灵雀云等,实际上图中左边两条线的差距已经大大缩小。
微服务技术选型
微服务架构成功运用的关键是选择合适的自动化工具降低运维难度,这对人力资源有限的创业公司尤为重要。我们的《谈谈创业公司的技术选型》【来源3】一文详细说明了创业公司技术选型的几个原则,对微服务技术栈选型同样有效:
一是利用好新技术选型的后发优势。微服务技术是一项比较新的技术,在大公司很少有可以参考的经验,应该多关注开源技术,多方比较后大胆采用。
二是自力更生、造轮子。微服务是条创新的不归路,虽然有相当多可以使用的开源组件,但你仍然需要开发配套工具让这些组件协同工作。
下面是我们选择的一部分和微服务相关的开源工具,以及这些工具的使用经验。
容器编排系统:Kubernetes
微服务技术的核心是容器编排系统,现在最流行的三个容器编排系统是Kubernetes,Mesos,Swarm。
通过比较我们选择了Kubernetes(简称k8s),因为Kubernetes的设计最吸引我们,有Google支持,社区活跃度和发展前景俱佳。我们整个后台系统基于Kubernetes,并且已经完全微服务化,有近100个微服务数百个容器在运行。
我们在实战中使用Kubernetes的几点经验如下。
1、二进制版本和配置版本要做分离,且代码化
微服务的配置yaml文件check-in到git代码库,而且做binary/config分离,分别控制二进制和配置环境的版本,所有的线上部署的改动都在代码中反映出来。举个k8s中微服务配置的例子,如下图。
这是我们一个微服务的 deployment 文件,我们用 git 的版本号做 docker 镜像的 tag(Jenkins 自动打包后加上去的),docker 镜像里只包含 binary 文件,配置文件通过 configmap 的 volume mount 为容器内的一个目录,而且配置文件也做了版本号控制,数据库密码等不走代码,而是由集群管理员手工输入为 kubernetes 的 secret,不留任何记录,从而避免了敏感信息的泄露。
2、混合云管理
考虑混合云上多 k8s 集群的管理需求,我们用 zone 来标识不同数据中心的 kubernetes 集群,zone 由三个字母标识,如下图
通过三字母标识法,我们将混合云部署统一化,极大方便了代码和文档中的服务标识。
3、Namespace 使用
Kubernetes 的 namespace 极有用,我们用 production namespace 指代生产环境,staging 指代预发,kube-system 指代集群系统级别的服务比如 DNS、prometheus 监控和报警等。
另外 k8s 也支持通过 namespace 的 node selector 来指定某个服务需要运行在哪类机器节点上,这样就可以将预发和生产环境运行在不同的机器上,做到不同环境的资源隔离。
4、基于 DNS 的自动化服务注册、发现和负载均衡
通过 skyDNS 就可以将一个服务的分布在不同服务器上的 instance 命名归一化,比如通过调用 ama-server.production.svc.k8s:20001 就可以将调用请求自动路由到某个服务节点上,调用端不需要关心服务是怎么部署的,服务注册和服务发现自动完成。
5、Overlay network 我们使用 flannel。
编程语言:Go
使用哪种语言和微服务看上去是无关的,而且貌似微服务的设计原本就是鼓励用不同语言实现微服务。
但是,编程语言的确会影响到代码的微服务化难度,有两个原因:
1、你需要一个运行时环境较小的编程语言,最好是能静态编译不需要虚拟机的语言。运行环境庞大不适合容器化,如果你给 Java 程序打过 docker 包就知道了,动辄上百兆的运行时,启动就消耗数百兆内存。而 Go 可编译为 standalone binary 无需运行时环境,docker 镜像一般 10 几兆就搞定了,memory footprint 也小很多。
2、你需要一个适合写网络服务的语言。这种语言最好原生支持多线程编程,可以非常简洁高效地写 HTTP 或者 RPC 服务而不需要借助第三方框架。Go 就是为写后台服务而生的语言。
另外,Go 自带格式化工具能够统一团队的编程风格,而且学习上手快,Java 或者 C++ 程序员只要一个星期就可以达到熟练运用的水平。
实际上我们用 Go 从头实现了整套后台微服务,包括 RTMP 直播服务器、用户体系、交易、IM、搜索、监控、小二后台,我们甚至用 Go 写机器学习代码和机械臂控制程序。实践证明 Go 完全可以胜任所有的后台开发工作,而且有极高的效率和工程实现质量。
在线监控:Prometheus + Grafana
微服务的监控系统必不可少。有些人用 ELK,我们用 Prometheus+Grafana。比如我们在 gRPC 服务的 /metrics 下添加了类似下面的指标来监控 RPC 性能:
然后 Prometheus 会根据 Kubernetes 的 pod 注册信息自动找到这些 metrics,我们设置这样的语句
最后在 Grafana 里通过这样的界面展示出来:
如果你在 Google 工作过会心中窃喜,这不就是 Google 的 Borgmon 嘛!对的,Prometheus 就是由一位 xoogler 工程师写的,参考了 Google 内部数据监控系统的设计。最后在 Grafana 里通过这样的界面展示出来:
这套体系和 ELK 相比较轻,且非常容易扩展,我们写了几个模块把服务日志和前端访问记录融合在一起做分析,同时 Prometheus 指标描述能力非常强大几乎可以做任何运算(事实上这种语言是图灵完备的)。
离线数据分析:fluentd + ODPS
我们的数据分析有两类,离线和在线。
在线数据分析就是上面写的 Prometheus + Grafana,适合服务报警、调试等日常任务。
离线数据主要是 fluentd 从 log 中提取出来后直接发送到阿里云的 ODPS,然后写定时调度生成表格分析。另外,数据库数据也通过 datax 发到 ODPS,可以和 log joining 处理。
我们这套离线数据体系适合每日报表或者即席查询。
同步通讯:gRPC + HTTP RESTful API
集群内部服务间通讯我们用 Google 开源的 gRPC,最近出了 1.0 稳定版。
外部调用使用 JSON 格式的 HTTP API,通过负载均衡先经过多个 Nginx 节点(也部署在 Kubernetes),然后通过 Nginx 的重定向发送给后面各个业务的 “gateway” 微服务,这些微服务再把一部分逻辑通过 gRPC 发送给更后端的微服务。
异步通讯:RabbitMQ
除了同步的 RPC/HTTP 调用外,你还需要异步调用,通常使用消息队列来完成。有两个应用场景
1、广播类的请求。比如用户数据发生更新后,通过 RabbitMQ 通知搜索引擎的所有实例完成增量索引。
2、所有跨集群调用必须走消息队列。这是一个很好的设计习惯,跨集群调用一定要从设计上就是高度容错的,而且必须对延迟要求很低。如果不满足这两个条件,你需要将被调用的服务在多个集群都部署一份。
我们对 RabbitMQ 调用的另一个约定是,队列消息最小化原则:如果需要传递较多信息,请使用引用到数据库。比如在通知搜索引擎完成增量索引的时候,我们只往消息队列传递新文档的 docid 和数据源地址,然后由消息的接收方自行从对应数据源(MySQL 或者 Redis)中抓取对应数据完成更新。
持续集成、部署:Jenkins
我们的代码按照微服务划分,每个微服务是一个单独的 repo 存放在 github,共用组件放在 common repo,然后微服务用 import 调用(这可以说是 Go 的另一个适合微服务的好处,代码引用机制比较简单)。
所有的 github commit 都会通过 webhook 触发 Jenkins 里的构建行为,
Docker 私有仓库:Harbor
这是 vmware 的一个开源项目,实现了用户和项目的权限管理。
非容器化组件
有些组件不适合放在 k8s 集群中,比如负载均衡、数据库(MySQL,Redis)、NFS等带状态的服务,我们直接使用阿里云服务。
我们的微服务架构
先上一张图直观地展示给大家我们的微服务集群长什么样子。
我们一共有 3 个 Kubernetes 集群,这张图展示的是其中一个集群(zone hba)中所有的微服务和他们之间的调用关系(Call Graph)。其中,
- 蓝色节点是 k8s svc,如果你不熟悉 k8s,可以简单理解为服务网关,每个服务有且只有一个网关,这个网关本身也是去中心化的,分布在多台服务器上,可以自动将调用请求重定向到多个服务实例中的一个。服务网关地址通过域名自动解析。
- 绿色节点是 k8s deployment,可以理解为服务本身,每个节点实际上有多个实例(pod)分布在多台服务器上。
- 节点之间的单向箭头描述了服务间的调用关系,其中蓝线是预发环境的服务调用,橙线是生产环境的服务调用,灰线是生产和预发调用的公共服务,虚线是异步调用 rabbitmq。
从图中我们可以非常直观地总结出微服务架构的几个特征:
调用关系即架构
回忆一下你阅读过的所有讲架构的文章,里面画的架构图比较侧重业务关系,而且很非常抽象。在微服务体系下,架构图就等于调用关系图:微服务划分就代表了业务的划分,微服务间的调用关系就是业务依赖关系。
代码自动生成调用关系
如果你的 Call Graph 不能通过解析你的代码自动生成的话,你一定没有用好微服务架构。
我们上面的调用图是这样生成的:首先,所有的调用关系都保存在配置文件中,见我上文中对 k8s yaml 文件的描述;然后我们通过一个脚本解析这些配置文件,生成描述关联关系的 DOT 文件,这步并不难,因为配置文件是结构化的 yaml 文件;最后我们用 Graphviz 从 DOT 文件自动画出调用图。同时,yaml 配置都是 check-in 到 git repository 里的,带版本号,所以我们能够分析出架构演化路径。
工程师独立推动架构演化
架构图等于调用图,调用图由代码生成,代码由开发工程师控制的,一个微服务有且只有一个工程师独立负责。结果就是所有工程师都能直接修改架构,生产力得到了极大的解放!
事实上,每个采用微服务技术的团队初期都会经历一个“寒武纪大爆炸”阶段,在这段时间会很快有各种微服务被开发出来,架构图在几个星期的时间就会演化到相当的复杂度。比如图中 80 多个微服务主要由 3 个全职工程师开发,这在传统的大公司不敢想象。
开发即运维
You own your microservices. 每一个微服务都由一位工程师独立开发、测试、部署、运维。听起来非常恐怖,但在微服务时代这是最高效的,原因有两个
1、运维自动化:容器编排系统 kubernetes 基本上已经将部署和运维完全自动化了,作为开发不需要知道负载均衡、动态扩容、服务自愈合等所有这些运维细节,工具已经帮你搞定。
2、端到端开发:开发可以直接面对需求,端到端测试这些需求,如果出了问题,没有谁能比写代码的人更快地找到问题所在。
DevOps 的工作方式在微服务时代很好地实现了。
架构可视化
调用关系图非常好地可视化了架构的几个特征:
1、有向无环图。你的微服务调用必须是无闭环的,circular dependency 会带来意想不到的麻烦。
2、有层次。比如我们的架构图中最左边的几个 Nginx 节点负责接收来自浏览器或者移动端的 HTTP API 调用,然后 nginx 再调用内部的服务(中间),这些服务再调用更深层次的服务(右边)。服务的层次越深,提供的功能越基础。
3、衡量架构复杂度的几个指标:depth(深度,从 nginx 开始到最深路径上的微服务个数),fan-in(入度,一个服务被几个服务调用),fan-out(出度,一个微服务调用了几个微服务)。你应该尽力降低架构深度和出度,提升服务的入度。
我们上面的架构图不是那种只在做报告时用用然后就被遗忘的架构图。实际上,这张图帮我们定位到了架构中的一些不合理处并及时作出修正,比如有些服务忘记了部署在多个环境,或者已经废弃的服务并没有从架构中删除等。
下面我们结合两个具体例子讲讲我们是如何构建微服务的。
微服务案例一:搜索
调用流程是这样的:
1、客户端通过 HTTP API 请求网关 Nginx 服务。
2、然后 Nginx 会根据检索场景的不同(比如有些请求是查询视频,有些查询用户)分别分流到对应的检索微服务上。
3、然后检索微服务将用户请求翻译成悟空引擎的查询语句,调用引擎微服务完成检索。
4、检索微服务再调用其他的服务将客户端需要的附加信息添加在返回结果中。
这个设计最重要的部分是检索内容的更新机制,包括全量更新和实时增量更新:
1、无须持久化:不使用持久化存储,所有索引和排序字段存在内存。每次启动时全量更新索引表到内存。
2、增量更新:其他业务系统通过 RabbitMQ 的广播通知搜索系统增量更新,包括添加、删除、修改文档索引,RabbitMQ 不传输数据,只通知。然后搜索引擎从 Redis 和 MySQL 读入实际的增量数据,比如从 MySQL 读入文档基本信息,从 Redis 读入需要高频修改的信息(比如计数,其他实时计算的分数等)。这些增量信息都是各个业务系统生成并预先添加到数据库的。
3、避免全量增量冲突:全量更新的同时,监听 RabbitMQ 队列,保证全量添加的同时不会遗漏增量更新:实现增删改接口时,需要加锁避免全量和增量同时写冲突;如果增量更新的视频还没有先被全量载入,缓存这个增量更新,等全量更新完毕后再增量更新。
另外,排序规则需要频繁变动,我们通过悟空引擎的排序规则插件实现了多种排序规则共存,并借助 k8s 蓝绿部署的机制实现了服务不下线更新代码。
通过基于 k8s 的微服务架构,加上合理的增量全量更新策略,我们实现了一个非常灵活且可靠的搜索功能。
微服务案例二:深度学习
在这个案例里,我们实现了深度学习的分布式 serving。
机器学习业务的开发和多数后台服务不同,主要包括三个环节
1、实验性研究:机器学习工程师使用部分数据在单机上实验各种算法,找到最好的算法
2、大规模训练:使用第一阶段找到的算法,加上全量数据,训练得到一个最好的模型
3、分布式部署:将第二个阶段得到的模型部署到线上环境,实现分类、识别、预测等服务
其中分布式部署机器学习模型有 两个挑战
1、和数据查询式业务相比,机器学习特别是深度学习模型需要较长的计算时间,因此延迟较高,thoughtput 较低,业务上线后需要更好的动态扩容能力。
2、机器学习程序依赖的运行环境比较复杂,部署比较麻烦。
这里以我们的“图说”服务作为例子说明。
我们把这个服务实现在微信公众号上,向这个公众号发送一张图片,公众号会返回英语和汉语来描述图片的内容。比如下面的例子:
上面这张图上,准确识别出了一辆火车、这俩火车的颜色并描述了火车在铁轨上行驶这一行为。
请求流程是这样的:
1、用户向公众号发图片
2、微信服务器向我们的 SNT 微服务(多个实例负载均衡)发送 XML 请求,带有用户图片的地址
3、SNT 服务器将图片地址发送到 IM2TXT 微服务(k8s 部署多个实例),这个微服务自行从地址下载图片,调用深度学习模型完成分析,并将得到的英文描述返回给 SNT 服务
4、SNT 将英文描述发送给百度翻译 API 得到中文描述
5、SNT 将中英文描述一起返回给微信服务器
6、用户收到公众号回复
其中 IM2TXT 微服务使用 Tensorflow 的 Python API 开发,打包为 2GB 的 docker 镜像,然后推送到我们的私有 docker 仓库供 Kubernetes 使用。模型本身非常复杂,数百万个参数,而且要对 Top N 的可能性做 beam search,跑一张图片需要两秒左右。
通过这套系统我们实现了较低的延迟,把模型预测时间控制在了 2 秒以内,整体延迟控制在 5 秒以下(微信要求后台服务响应时间不超过 5 秒)。
最后的话
Kubernetes 容器编排系统和相关工具已经将微服务的使用门槛降到最低,运维自动化真正实现了 Dev 和 Ops 的统一,极大解放了工程师的生产效率。
我们在创业环境下实现了一整套基于微服务的后台系统,并运用了一系列新技术。这篇文章通过分享我们的实践经验,证明基于微服务的后台技术体系不仅是可行的,而且是未来大势所趋。
原文出处:InfoQ