《极简微服务》的原始动机是为了总结本身所学,但后来发现不少同窗在学习微服务后仍然一头雾水,处于一种知道可是还没有深入的阶段。我但愿可以经过《极简微服务》这篇文章,用尽可能少的语言将各部分的知识串联起来,帮助刚入门的同窗们从新梳理一遍,但愿可以让你们对微服务造成一个更清晰的认识。由于是“极简”,因此本文不会涉及到任何代码的具体实现。java
我想要为你们带来的思路是:linux
首先要说明的是,我入门微服务走的是Spring Cloud相关技术栈,因此我可能很难作到彻底的脱离这个技术栈去总结。我对于微服务的了解可能也是肤浅的,某些方面上的理解可能也会存在一些误差,我更多的把这看做是一种记录和交流,由于我也不过是经过阅读书籍和一些博文后作了一些梳理,因此但愿读者朋友们可以有批判地阅读个人文字。数据库
当你们阅读个人文字,而后再提起微服务的时候,若是能说出更多本身想法,那这恐怕就是对我莫大的鼓励了。windows
由于叫作 “极简”,因此其实我已经尽可能想要控制好篇幅不要太长了,可是很无奈到最后仍是超过了本身所认为的最理想篇幅,由于我以为有些东西是不得不去了解的,望读者朋友可以见谅。安全
其实就是微服务的目标啦。服务器
终极目标网络
经过将大的项目拆分红不一样子服务,不一样子服务相互协做,创造出一个分布式的、稳定的、高可用的、可控制的系统。
管理对象架构
服务。为了实现终极目标管理好各个服务,让他们之间的沟通无障碍。
所以,接下来我将针对咱们的服务与服务之间的关系来描述微服务。负载均衡
微服务是一种架构风格 ,就是将业务系统拆分为不一样的服务,而后每一个服务是独立的。不一样的服务经过轻量级的协议通讯(如HTTP)。部署的时候,一个服务可能会有多个实例。服务与服务的合做,构成了一个完整的系统。框架
经过微服务咱们能够达到一些理想的效果。
...
有一种说法是,开发人员是被迫写配置的。由于开发者得让本身的软件去适应不一样的变化。
一开始是写配置文件,可是太零散,因而将配置都写在一个大的集合里面,好比数据库或者一个文件。
可是存在几个问题:
因此,后面提出了 配置中心 ,但愿可以解决上述的问题。
对于程序而言,它但愿:
对于配置中心而言,它但愿:
既然配置中心存了全部服务的配置,当服务们要加载配置就至少要和配置中心通讯一次。若是说服务要硬编码配置中心的地址,假如配置中心换个地址,那么就必须手动修改全部服务的这个配置。当服务数量达到几十个的时候,这简直是个噩耗。
因此硬编码地址显然不是一个好的方案。
微服务架构里面,配置中心也是一个服务。发散的思考一下:不一样服务之间的通讯是怎样的?总不可能都是硬编码吧?
回顾上一个章节的问题,服务之间应该如何通讯。简单来讲就是:服务太多了,不知道怎么才能找到对方。
其实很简单,咱们找一个第三者,让第三者告诉咱们就行了。就像是出去旅行同样,不认识路的话,你查百度地图就行了,让地图告诉你具体的位置。
因此咱们要抽象出一个中间服务,专门用来作这个事情,告诉你其余服务的位置,这个过程就叫作服务发现。而其余服务告诉这个中间服务本身的位置的过程,就叫作服务注册(相似于把本身的位置告诉百度地图,让他记录你的位置)。
服务注册/发现中心:
其余服务(客户端):
不一样的服务注册、发现中心实现,流程可能存在差别。我在这里只拿Eureka来看一下(目前我也只了解这个 = = )。
说明:
服务地址
,这个服务地址就像是一本地图小手册,上面记录了全部服务的地址。这个客户端只须要定时更新这个小手册,保证它是最新的就行了。服务注册于发现机制,解决了不一样服务之间的位置问题。可是仅仅是这样是否足够呢?
来听一听客户端的疑问:
我就想去吃鸭脖,你这个地图小手册,鸭脖店的地址怎么有两个啊?那我到底要去哪个店家呢?
选择鸭脖子店的问题,严肃一点来讲,就是当拥有多个服务实例的时候,选择哪一个实例去调用的问题。
这个过程实际上就叫作负载均衡。
负载均衡能够分两种类型:服务端的负载均衡和客户端的负载均衡。他们之间存在什么样的差别呢?
服务端负载均衡
顾名思义,就是把负载均衡(选择哪一个鸭脖子店的选择权)在服务端完成。在这种方案下,其实对于客户端来讲,就不存在选择的问题了。当客户端说 “要吃鸭脖” 的时候,服务端查看本身的地址列表,发现有两个鸭脖店,那么服务端会挑一个店的地址告诉客户端。
怎么挑选呢?一般能够根据目标服务实例的网络情况,负载的请求数量,甚至是随机分配等,通常都是事先制定好一套规则。
客户端负载均衡
相对于服务端的负载均衡,客户端负载均衡就是把所谓的选择权交给客户端。也就是说,有多少个可用的服务实例的地址,服务端就返回多少个地址。客户端要调用哪个服务实例,彻底由本身去选择。
一样,客户端怎么去选择,通常也都是预先设定好规则的。
如今,服务与服务之间的通讯,服务注册与发现解决了 “在哪里” 的问题;负载均衡解决了 “若是有多个位置应该去哪里” 的问题。服务注册/发现 和 负载均衡是属于相互合做的关系,共同解决了 “去哪里” 的问题。
可是咱们要如何保证客户端真的顺利到达目的地呢?
严肃点说就是:要如何保证客户端对目标服务的调用是正常的?
接过上一小节的问题,你以为咱们应该如何保障咱们服务之间的调用是正常的呢?
为了回答这个问题,咱们先抛出一个概念,叫作 “弹性”。
何谓弹性呢?弹性对于不一样的对象来讲略有不一样。
对于客户端来讲:
弹性就是是否有响应。任何一个请求都应该有响应,且响应时间不该太长。
对于被调用的服务端来讲:
弹性就是自我保护。为了保护正常的运做,我可能会拒绝一些请求,以保障服务没有由于过多的请求而崩溃。
咱们在这里打个比方。服务之间的调用是正常 就像是 去店里买蛋糕,这就是一个 “是否能顺利抵达目标地点并买到蛋糕” 的问题。
在这里咱们首先要明确,确定不能保证每一个人都可以顺利抵达商店,由于目标商店随时均可能会 “关门不干”;其次,就算抵达了店里,也不必定可以买到蛋糕,由于可能卖完了,或者机器出故障了。
因此咱们要换个思路,若是店家关门了,我就提早告诉它关门了,你最好别去了;或者你已经到了店家但发现蛋糕都卖完了,因而你改为买饼干,或者是你干脆直接去别的店里买。
维持弹性的方案有很多,其实以前也有碰到过,让咱们来总结一下。
以前咱们学习到的有:
再来看看其余的方案吧:
因为服务发现与注册机制、负载均衡在上面的章节已经有介绍了,故在此就再也不赘述。下面会围绕其余的方案来进行分析。
断路器的流程
微服务中最多见的就是断路器了,下面来看看断路器的流程(不一样的断路器实现可能会有所差别):
如上图所示,有四个对象,分别是用户、客户端、断路器、目标服务。
由用户向客户端发起请求,客户端调用目标服务来完成用户的指望。断路器在这其中承担了客户端与目标服务之间的 “中间人” 的角色。
通讯能够划分为三个阶段:
后备模式的流程
其实很简单,用伪码来表示就是:
boolean success = callService(); if (success) { doSomething(); } else { excutePlanB(); }
其实后备模式是能够和断路器结合起来使用的,当在断路器流程中,判断目标服务出现问题后的第二阶段,客户端的调用能够所有走Plan B。
舱壁模式的流程
其实就是隔离,隔离的方式有不少种,好比线程的隔离,模块的隔离,地域隔离等。这里咱们拿线程隔离的举个例子:
上图中,本来客户端对全部服务的调用都使用同一个线程池。
当某个目标服务A出现故障的时候,堆积在这个服务的调用越积越多,一直到线程池中的线程全被占用了,此时客户端新来一个须要对服务B的调用,却没有线程池能够用了,只能排队等待。
解决方案很简单,事先将不一样的服务划分到不一样的线程池中去,这样就不会出现一个服务占用资源,致使其余服务不可用的状况了。
如今咱们不只解决了 “去哪里” 的问题,还解决了 “是否能抵达并买到东西” 的问题。
让咱们好好想一想,咱们如今解决的都是服务与服务之间的通讯问题。若是是外部服务(咱们微服务体系外的客户端)要与咱们的服务通讯,如何解决呢?
接回上一小节的问题。
问题描述:
内部服务与服务之间,是经过一本字典来查找对方的位置,可是若是是一个“外国人”(外部服务),一方面我不能把字典给他由于字典是机密,其次,就算给了他他也看不懂毕竟文字不通。
分析:
所谓内部服务,通常对外都是不可见的。咱们经过服务之间的合做给外部提供一个总体,因此对外而言咱们应该是一个总体。基于安全性考虑咱们也不能讲内部服务暴露给外部服务使用。
解决方案
选举出一个中间人,专门用来和外部服务通讯。由这个中间人来判断对方是否是合法的请求,而后作相应转发工做。
这个中间人又像是一个守门人,只接纳有“令牌”的人,并“通风报信”。
其实没有太多想要说的。咱们看一下网关所处的位置,恰好就是处于对外来请求的“守门人”的位置。咱们只须要明白网关的做用便可。
很明显,网关的做用有:
想必到这里,你已经基本了解服务注册与发现、配置中心、负载均衡、断路器、网关之间是如何共同工做的了。
如今惟一的问题是,相对于以前的单体架构系统来讲,服务的数量变多了,服务之间的通讯也变多了。系统反而一会儿变得复杂了。若是服务与服务之间的调用出了问题,我应该怎么排查问题呢?
接回上一小节问题。
问题描述
相对于单体架构系统而言,微服务架构牺牲了原有的简单,换取了系统的灵活性。因此微服务看起来反而变得更复杂了。一个请求,可能要通过网关、服务与服务之间来回的调用,因此作的日志也都十分零散,怎么监控系统、排查问题,变成了使人头疼的问题。
来自风筝的启发
不管风筝怎么飞,只要我手里握着一条线,我就能知道风筝在哪里。咱们能够借鉴这个思路,为咱们的请求创造出一根看不见的 “线” 。
TraceID
如上图所示,当请求进来的时候,我只须要为这个请求添加上一个标识(traceID),无论后续这个请求被如何转发,只要带有这个traceID,那他们确定是同一请求。
这个traceID不只仅要可以被转发,作日志的时候,也要将traceID一并记录下来,这样无论日志分散在哪一个服务,我均可以知道他们是否是同一个请求产生的。
经过traceID咱们能够实现请求的链路跟踪问题。
日志搜集
咱们知道,不一样的服务多是部署在不一样的服务器上的,因此才会显得零散。若是咱们要查日志,确定不可以来回登陆到不一样服务器上去查。因此,咱们须要将全部的服务的日志统一搜集起来,统一管理。这叫作日志聚合。
经过将全部服务的日志统一搜集起来,统一存储,统一分析。再经过TraceID作链路的跟踪,咱们就能够搭建出一个日志管理和分析平台。
如此,在微服务架构下,监控和查找问题,变得一目了然。
如今来回顾一下,微服务的组件,都还记得有哪些吗?
来看一下他们是如何相互协做的吧!
(这只是从一种维度上去划分,划分的维度能够有不少)
是函数级别的。一般咱们在这里捕获到大部分的错误。
服务级别的集成测试。测试某个服务的功能。
系统测试,多个服务的测试。一般须要在页面上用鼠标点点点。
对于单元测试而言,有时候一个函数依赖于另一个函数,也就是说,另一个的正确与否,会直接影响到我这个测试的结果。因此有时候咱们须要制造必定的隔离空间。
对于服务测试来讲,也存在这样的问题。
如此来讲,测试的隔离性咱们须要重视。
为了创造这种隔离性,咱们能够用Mock、或者打桩技术。
所谓打桩,最简单的理解就是你能够认为是“写死”,a函数,须要依赖b函数的返回值。那么咱们能够将b函数的返回值“写死”。这就保证了b的返回确定不会出错了。
看似简单,可是你不能直接修改函数b,由于很容易出错,如果你忘了改回来,就把你“写死”的代码一并提交了。
Mock和打桩很相似,区别在于,打桩是不关心你函数b(即你这个测试要依赖的那个函数)执行了多少次的。而Mock不只仅能够模拟调用函数b不少次,还会不少其余的问题,好比,调用是否成功等。
咱们一般借助一些测试框架来打桩和Mock,好比Java最经常使用的JUnit。
如上图所示,不一样的测试,会带来不一样的效果。
随着服务愈来愈大,测试愈来愈多,人工的测试变得愈来愈困难。微服务离不开自动化测试。因此咱们须要借助CI/CD来让咱们的测试变得自动化。
持续集成对于微服务来讲真的很重要。微服务组件解决的是技术上的问题,而持续集成所解决的是流程和效率上的问题。
持续集成可以加快微服务架构下的开发、测试、部署,可以帮助咱们更快的发现和解决问题。
初闻持续集成,让人摸不着头脑,怎么会有如此晦涩的词语呢!
可是且慢,先来看看哪些典型的场景:
这些场景都使人头疼,他们的共同特征是:要么都是重复的工做,要么是常常出现的问题,对项目工期、团队协做产生了较大的影响。
咱们须要解决这些问题,用的手段就是持续集成。接下来咱们就来解释什么叫持续集成。
所谓持续集成(Continuous Integration)。
先来看一下什么叫作集成。什么是集成呢?集成就是将代码提交到主干,经过一系列自动化的工具来构建(你能够简单理解为打包)和测试,验证是否有错误,是否会对其余人的代码产生影响。
因此持续集成,就是不断的进行集成,即常常提交代码以集成。持续集成的目的就是尽早集成,早点发现错误,早点解决问题。
咱们谈到CI的时候,每每涉及到几个方面:
这其实也就是技术和流程问题。
一个好的持续集成环境是怎样的?
代码提交后自动(或者按期手动)触发持续集成:自动跑单元测试,自动构建代码,自动上传部署。不须要过多的人工参与。中间每一步出现问题的时候,都会立刻经过一些手段进行反馈,防止出现更多的错误。
咱们谈论CI的时候,不少时候也包含了持续部署(Continuous Delivery)的含义在里面。
何谓持续部署?
是CI的下一步。CI经过自动化的流程,确保软件语法和单元测试上是没有问题的。可是还不够,因此下一步须要将软件的新版本交给质量团队或者用户。让他们去继续测试、评审。若评审经过则说明代码就能够进入生产阶段了。
容器也是微服务架构不得不去思考的一个问题。它解决的是部署的问题。
部署有哪些问题呢?
能不能减小服务器的数量,在一台服务器部署多个服务,而且能作到能够支持多种服务的部署?
要支持不一样服务,确定要支持不一样的环境,好比windows,好比linux。
其实能够的,用虚拟机嘛。
虚拟机的大概思路是:
将整个环境先搭建好,而后打包成为虚拟机镜像,每次须要增长一个新环境的时候,直接实例化作好的镜像便可
一方面,虚拟机技术解决了环境的问题,部署比之前快了很多;
另外一方面,构建镜像耗费时间长(一旦环境须要改变的时候,或者须要多个不一样环境的时候),镜像文件大;而且,虚拟机的管理也须要占用额外的资源和空间;
虚拟机技术的特色是:
标准的虚拟机中存在一个Hypervisor,它会帮助咱们管理运行在Hypervisor之上的其余虚拟机,包括资源的分配,外部的请求路由管理,内存的映射等;虚拟机越多,Hypervisor占用的资源越多;
即Linux container,简称LXC。
LXC的原理是,建立一个隔离的进程空间,在这个空间中运行其余的进程。而且由物理机的内核来完成资源的分配工做。
以下图所示:
与传统虚拟机不同的地方是:
如今你们都知道Docker了
LXC是操做系统级别的虚拟化方案,毕竟是Linux下的容器。是否存在应用级别的容器技术呢?
那就是Docker了。
2013年,Docker横空出世的时候,在底层上也借助了LXC来管理容器,而后本身在上层作其余的管理工做。可是过了几年,Docker 0.9的时候有了新欢libcontainer,LXC就变成了一个“备胎”,此时Docker能够选择再也不依赖Linux部件了。再Docker 1.8的时候,LXC被认定为“前妻”。
再到后来,libcontainer上位。Docker致力于去实现容器化的标准,你只须要实现libcontainer提供的标准接口,那么你就可以运行Docker,这为Docker的全面跨平台提供了可能。
分层
一个镜像能够分为任意多层,好比:
若是几个镜像拥有相同的层。好比,镜像A和镜像B用的是相同的操做系统,那么下载A时已经下载好了这个操做系统,下载B的时候,就不须要从新下载了,只须要下载第一层之上的数据便可。
增量更新/版本管理
对于一个公共镜像A来讲,有许多镜像可能都是根据它来制做的,而镜像A制做好了便不能够改变。但对于其余镜像来讲,有时候一些环境变量不免须要改变,这时应该怎么办呢?
Docker中约定:
这样,只须要在最上层添加一个读写层,将须要改变的配置都集中到这个层来进行改写。在写的时候同时拷贝出一个只读属性的文件备份,应用实际读取的时候读取的是这个只读属性的拷贝。
如上图所示,底层中环境变量A是1,可是在读写层,咱们将A设置为3,那么应用程序读取的时候,读取到的是A=3。而读写层不只仅能够修改配置,还能够将移除某些组件。每新加一层发布出去的时候,就至关于发布了一个更新。这样的一个好处就是,对于以前发布出去的镜像,咱们拥有了 增量更新 和 版本管理 的能力。
Docker采用了C/S架构。分为Client和Server,而后还借鉴了Git的思想,能够搭建本身的中央镜像仓库。大致的架构以下图所示(图片来自于网络):
须要解释一下几个名词:
咱们所说的Docker,一般是指Docker Engine,它包括了:
其中,REST API和CLI都是用来跟server交互的。
咱们原先所谓的部署,对于Java来讲,都是将代码打包成jar或者war,也就是说,jar和war就是咱们CI的构建物。
采用了容器技术之后,CI每次集成的结果是一个容器镜像,那么咱们就只须要在服务器将这个镜像实例化就能够了,从而不须要关心环境的差别问题。
微服务经过将系统拆分为不一样的服务,经过服务与服务之间的相互协做,构成一个总体。
在微服务架构下,咱们关心服务与服务之间如何通讯。因此咱们会单独构建一个服务中心,专用用于帮助服务与服务之间更好的沟通;再经过断路器的方式,为咱们的服务提供保障,让服务间的调用具有弹性;此外,咱们还会独立出一个服务,专门管理服务的配置;考虑到咱们须要支持外部的访问,因此咱们独立出一个网关,用来承担“看门人”的角色。上述的种种为系统带来了巨大的复杂性,为此,咱们还单首创造创一套日志聚合和链路跟踪机制,用于监控微服务的状态。
除了这些,咱们还须要经过持续集成和容器技术来帮助咱们达成更高效的开发、测试、部署效率,让服务具有更好的伸缩性,要增长新的服务实例的时候,只须要再实例化一次镜像便可。
仿佛一切都变得美好了,这就是微服务。
(这是个人原创文章,若要转载,但愿你们可以注明出处,谢谢你们~)