一、最初的需求
2014年初,加入一家互联网创业型公司,做O2O商城,项目最初是从第三方外包公司购买。随后组建开发团队进行内部升级迭代,我一毕业就加入,成为其中一员。
功能清单:
网站(PC)
用户注册、登录功能、商品展示、下单
安卓
用户注册、登录功能、商品展示、下单
管理后台 系统管理 、联盟管理、页面管理、积分优惠券管理、活动管理、会员管理、订单管理、商品管理、商品排名管理、报表管理。
技术选型:
系统采用Java Web单体架构,对应一个数据库。
前期数据库采用Mysql 5.1,服务器采用jboss 7.0,手机部分客户前期开发以android4.0版本为最低版本。
网站使用的框架有SPRING、HIBERNATE、FREEMAKER,三种框架分别对象软件三层架构的业务逻辑层、数据层、表现层。
Spring减少程序间的耦合性,方便网站升级。
Hibernate使数据库操作面向对象化,提升开发效率。
Freemaker是页面文本化输出提升用户访问速度。
整个系统的架构:
每项管理分层创建,比如会员管理和联盟管理分别在自己的层级。每个层级有包含controller、serviceImpl、service、dao、model、页面表示渲染。安卓也同样如此,这样开发人员在根据层级就可以方便的修改和更新自己的所负责的内容。
二、随着业务发展
在竞争的压力下,老板决定开展一些营销手段:
开展促销活动。比如元旦全场打折,春节买二送一,优惠券等等。
拓展渠道。新增移动端营销。
此时就增加处理活动相关和渠道拓展层。
按计划上线后,考虑到用户数不多,系统还是可以正常线上运行。假如后面用户数比较多了,就会出现一些性能瓶颈。
1) 所有应用都在一个数据库上操作,数据库出现性能瓶颈。特别是数据分析跑起来的时候,数据库性能急剧下降。
2) 开发、测试、部署、维护愈发困难。
3)即使只改动一个小功能,也需要整个应用一起发布。有时候发布会不小心带上了一些未经测试的代码,或者修改了一个功能后,另一个意想不到的地方出错了。为了减轻发布可能产生的问题的影响和线上业务停顿的影响,所有应用都要在凌晨三四点执行发布。发布后为了验证应用正常运行,还得盯到第二天白天的用户高峰期。。。
4)网站和移动端应用有很多相同业务逻辑的重复代码(这种情况可能单体架构设计抽象比较好的话不会出现)。
未雨而绸缪。尽管有着诸多问题,但也不能否认这一阶段的成果:快速地根据业务变化建设了系统。不过紧迫且繁重的任务容易使人陷入局部、短浅的思维方式,从而做出妥协式的决策。在这种架构中,每个人都只关注在自己的一亩三分地,缺乏全局的、长远的设计。长此以往,系统建设将会越来越困难,甚至陷入不断推翻、重建的循环。
三、系统升级改造
前提:要做升级改造,首先你需要有足够的精力和资源。如果你的需求方(业务人员、项目经理、上司等)很强势地一心追求需求进度,以致于你无法挪出额外的精力和资源的话,那么你可能无法做任何事。
在编程的世界中,最重要的便是抽象能力。架构改造过程中目前有三个,垂直架构、SOA架构(面向服务架构)、微服务架构。它们各有优缺点。如果几年前的话我们可以采用SOA结合消息总线进行将大项目拆分成几个小项目。基于现在,我们一次性做到底,用最主流的技术微服务进行拆分处理。
抽象后我们可以将系统拆分为
用户服务、商品服务、促销服务、订单服务、数据分析服务
各个应用后台只需从这些服务获取所需的数据,从而删去了大量冗余的代码,就剩个轻薄的控制层和前端。这一阶段的架构如下:
这个阶段只是将服务分开了,数据库依然是共用的,所以一些烟囱式系统的缺点仍然存在:
1、数据库成为性能瓶颈,并且有单点故障的风险。
2、数据管理趋向混乱。即使一开始有良好的模块化设计,随着时间推移,总会有一个服务直接从数据库取另一个服务的数据的现象。
3、数据库表结构可能被多个服务依赖,牵一发而动全身,很难调整。
如果一直保持共用数据库的模式,则整个架构会越来越僵化,失去了微服务架构的意义。因此一鼓作气,把数据库也拆分了。所有持久化层相互隔离,由各个服务自己负责。另外,为了提高系统的实时性,加入了消息队列机制。架构如下:
完全拆分后各个服务可以采用异构的技术。比如数据分析服务可以使用数据仓库作为持久化层,以便于高效地做一些统计计算;商品服务和促销服务访问频率比较大,因此加入了缓存机制等。
如果团队人员不多的情况下,数据库不是很大的情况下,还有一种抽象方法是根据具体大的业务方向进行拆分。包括后台管理服务、前端服务、消息服务、定时服务、促销服务。这种方法可以减少服务调用的性能损耗。
数据库拆分也有一些问题和挑战:比如说跨库级联的需求,通过服务查询数据颗粒度的粗细问题等。但是这些问题可以通过合理的设计来解决。总体来说,数据库拆分是一个利大于弊的。不过要根据公司的后续的业务体量和具体人员分配斟酌考量。
四、微服务架构产生的新问题
随着日订单数量蹭蹭地上涨。可惜好景不长,乐极生悲,突然嘣的一下,系统挂了。
以往单体应用,排查问题通常是看一下日志,研究错误信息和调用堆栈。而微服务架构整个应用分散成多个服务,定位故障点非常困难。需要一个台机器一台机器地查看日志,一个服务一个服务地手工调用。定位到问题故障比如促销服务由于接收的请求量太大而停止响应了,其他服务都直接或间接地会调用促销服务,于是也跟着宕机了。在微服务架构中,一个服务故障可能会产生雪崩效用,导致整个系统故障。其不过形势紧急,随着每一分每一秒流逝的都是白花花的银子,也没时间排查问题,当机立断在云上新建了几台虚拟机,然后一台一台地部署新的促销服务节点。几分钟的操作后,系统总算是勉强恢复正常了。
事后,问题是解决了,但谁也无法保证不会再发生类似的其他问题。微服务架构虽然逻辑设计上看是完美的,但就像积木搭建的华丽宫殿一样,经不起风吹草动。微服务架构虽然解决了旧问题,也引入了新的问题:
稳定性方面:
微服务架构整个应用分散成多个服务,定位故障点非常困难。
稳定性下降。
服务数量变多导致其中一个服务出现故障的概率增大,并且一个服务故障可能导致整个系统挂掉。
事实上,在大访问量的生产场景下,故障总是会出现的。
服务数量非常多,部署、管理的工作量很大。
开发方面:
如何保证各个服务在持续开发的情况下仍然保持协同合作。
测试方面:
服务拆分后,几乎所有功能都会涉及多个服务。
原本单个程序的测试变为服务间调用的测试。
测试变得更加复杂。
总结对故障的处理一般从两方面入手,一方面尽量减少故障发生的概率,另一方面降低故障造成的影响。
五、微服务监控架构
在高并发分布式的场景下,故障经常是突然间就雪崩式爆发。所以必须建立完善的监控体系,尽可能发现故障的征兆。
微服务架构中组件繁多,各个组件所需要监控的指标不同。比如Redis缓存一般监控占用内存值、网络流量,数据库监控连接数、磁盘空间,业务服务监控并发数、响应延迟、错误率等。因此如果做一个大而全的监控系统来监控各个组件是不大现实的,而且扩展性会很差。一般的做法是让各个组件提供报告自己当前状态的接口(metrics接口),这个接口输出的数据格式应该是一致的。然后部署一个指标采集器组件,定时从这些接口获取并保持组件状态,同时提供查询服务。最后还需要一个UI,从指标采集器查询各项指标,绘制监控界面或者根据阈值发出告警。
大部分组件都不需要自己动手开发,网络上有开源组件。下载了RedisExporter和MySQLExporter,这两个组件分别提供了Redis缓存和MySQL数据库的指标接口。微服务则根据各个服务的业务逻辑实现自定义的指标接口。然后采用Prometheus作为指标采集器,Grafana配置监控界面和邮件告警。这样一套微服务监控系统就搭建起来了:
六、定位问题之 链路跟踪
七、分析问题之 日志分析
日志分析组件应该在微服务兴起之前就被广泛使用了。即使单体应用架构,当访问数变大、或服务器规模增多时,日志文件的大小会膨胀到难以用文本编辑器进行访问,更糟的是它们分散在多台服务器上面。排查一个问题,需要登录到各台服务器去获取日志文件,一个一个地查找(而且打开、查找都很慢)想要的日志信息。
因此,在应用规模变大时,我们需要一个日志的“搜索引擎”。以便于能准确的找到想要的日志。另外,数据源一侧还需要收集日志的组件和展示结果的UI组件:
调查了一下,使用了大名鼎鼎地ELK日志分析组件。ELK是Elasticsearch、Logstash和Kibana三个组件的缩写。
Elasticsearch:
搜索引擎,同时也是日志的存储。
Logstash:
日志采集器,它接收日志输入,对日志进行一些预处理,然后输出到Elasticsearch。
Kibana:
UI组件,通过Elasticsearch的API查找数据并展示给用户。
最后还有一个小问题是如何将日志发送到Logstash。一种方案是在日志输出的时候直接调用Logstash接口将日志发送过去。这样一来又(咦,为啥要用“又”)要修改代码……于是选用了另一种方案:日志仍然输出到文件,每个服务里再部署个Agent扫描日志文件然后输出给Logstash。
八、网关权限控制,服务治理
拆分成微服务后,出现大量的服务,大量的接口,使得整个调用关系乱糟糟的。经常在开发过程中,写着写着,忽然想不起某个数据应该调用哪个服务。或者写歪了,调用了不该调用的服务,本来一个只读的功能结果修改了数据……
为了应对这些情况,微服务的调用需要一个把关的东西,也就是网关。在调用者和被调用者中间加一层网关,每次调用时进行权限校验。另外,网关也可以作为一个提供服务接口文档的平台。
使用网关有一个问题就是要决定在多大粒度上使用:最粗粒度的方案是整个微服务一个网关,微服务外部通过网关访问微服务,微服务内部则直接调用;最细粒度则是所有调用,不管是微服务内部调用或者来自外部的调用,都必须通过网关。折中的方案是按照业务领域将微服务分成几个区,区内直接调用,区间通过网关调用。
由于整个网上超市的服务数量还不算特别多,采用的最粗粒度的方案:
九、服务注册于发现 - 动态扩容
前面的组件,都是旨在降低故障发生的可能性。然而故障总是会发生的,所以另一个需要研究的是如何降低故障产生的影响。
最粗暴的(也是最常用的)故障处理策略就是冗余。一般来说,一个服务都会部署多个实例,这样一来能够分担压力提高性能,二来即使一个实例挂了其他实例还能响应。
冗余的一个问题是使用几个冗余?这个问题在时间轴上并没有一个切确的答案。根据服务功能、时间段的不同,需要不同数量的实例。比如在平日里,可能4个实例已经够用;而在促销活动时,流量大增,可能需要40个实例。因此冗余数量并不是一个固定的值,而是根据需要实时调整的。
一般来说新增实例的操作为:
1、部署新实例
2、将新实例注册到负载均衡或DNS上
操作只有两步,但如果注册到负载均衡或DNS的操作为人工操作的话,那事情就不简单了。想想新增40个实例后,要手工输入40个IP的感觉……
解决这个问题的方案是服务自动注册与发现。首先,需要部署一个服务发现服务,它提供所有已注册服务的地址信息的服务。DNS也算是一种服务发现服务。然后各个应用服务在启动时自动将自己注册到服务发现服务上。并且应用服务启动后会实时(定期)从服务发现服务同步各个应用服务的地址列表到本地。服务发现服务也会定期检查应用服务的健康状态,去掉不健康的实例地址。这样新增实例时只需要部署新实例,实例下线时直接关停服务即可,服务发现会自动检查服务实例的增减。
服务发现还会跟客户端负载均衡配合使用。由于应用服务已经同步服务地址列表在本地了,所以访问微服务时,可以自己决定负载策略。甚至可以在服务注册时加入一些元数据(服务版本等信息),客户端负载则根据这些元数据进行流量控制,实现A/B测试、蓝绿发布等功能。
服务发现有很多组件可以选择,比如说Zookeeper 、Eureka、Consul、Etcd等。不过觉得自己水平不错,想炫技,于是基于Redis自己写了一个……
十、熔断、服务降级、限流
1、熔断:
当一个服务因为各种原因停止响应时,调用方通常会等待一段时间,然后超时或者收到错误返回。如果调用链路比较长,可能会导致请求堆积,整条链路占用大量资源一直在等待下游响应。所以当多次访问一个服务失败时,应熔断,标记该服务已停止工作,直接返回错误。直至该服务恢复正常后再重新建立连接。
2、服务降级:
当下游服务停止工作后,如果该服务并非核心业务,则上游服务应该降级,以保证核心业务不中断。比如网上超市下单界面有一个推荐商品凑单的功能,当推荐模块挂了后,下单功能不能一起挂掉,只需要暂时关闭推荐功能即可。
3、限流:
一个服务挂掉后,上游服务或者用户一般会习惯性地重试访问。这导致一旦服务恢复正常,很可能因为瞬间网络流量过大又立刻挂掉,在棺材里重复着仰卧起坐。因此服务需要能够自我保护——限流。限流策略有很多,最简单的比如当单位时间内请求数过多时,丢弃多余的请求。另外,也可以考虑分区限流。仅拒绝来自产生大量请求的服务的请求。例如商品服务和订单服务都需要访问促销服务,商品服务由于代码问题发起了大量请求,促销服务则只限制来自商品服务的请求,来自订单服务的请求则正常响应。