很多人可能已经知道微服务已成为明日黄花,它曾经作为最佳实践为Segment公司起到很大作用,但是并不适合所有场所。
简单说,微服务是将后台业务拆分成很多各自功能独立的面向服务软件架构,其模块化、减少测试压力、功能组合、开发团队自治等优点广为人知。与之对应的是单体式架构,即用单个服务为测试部署扩展提供所有功能模块。
2017年早些时候,Segment产品开发遇到了问题。如果在每个部门继续采用微服务,不但不会加速开发过程,反而会落入复杂的泥潭。这种架构的优势反而变成了负担。最终,团队发现需要三个全职工程师才能确保这套系统运转,这种无法承受的负担必须改变。这篇博文就是回顾如何将产品和团队需求更好嵌入开发过程的回顾。
多年前,产品刚发布时,架构很简单。API录入时间,将他们转发到消息队列。本例中的事件是一个由Web或者移动应用产生的JSON对象,其中包含用户和行为的信息。例如:
{
"type": "identify",
"traits": {
"name": "Alex Noonan",
"email": "anoonan@segment.com",
"company": "Segment",
"title": "Software Engineer"
},
"userId": "97980cfea0067"
}
队列中的时间被处理,客户端配置决定事件会转发到哪个目标端,事件会按照顺序转发到目标端API;这种架构对开发者很有用,他们只需要将事件发送到单一服务端,也就是Segment的API(Segment会接下来出来事件发送到哪个具体目标端),而不需要做其他复杂整合。
如果某个请求失败,一般会过一阵再发送事件请求。某些失败是可以恢复的,但是有些却不能。可重试的错误包括例如HTTP 500s,过快连接和超时等错误;不可重试错误包括无效加密信息或者请求信息不全等状况。
也就是说,单一队列会包含发往所有目标端的最新事件和重试过几次的事件,毫无疑问会引起排头阻塞问题。本例中,如果一个目标端变慢或者失效,重试会阻塞整个队列,从而拖慢所有目标端反应。
假设目标端X出现故障,每个请求都返回超时。请求会产生大量回滚日志,失败请求也会放回重试队列。因为系统架构会根据负载压力弹性扩展,突然增加的队列深度会超过扩展能力从而导致响应减慢。不能及时发送消息,导致等待时间增加对客户是不可接受的。
为了解决排头阻塞问题,我们为每个目标创建了独立的服务和队列。新架构配置了一个额外路由器,负责接收事件并将它们发送到选中的目标端。如果某个目标端出问题,只有与之相关的队列受影响。这种微服务风格的架构将目标端隔离,解决了我们的问题。
各自独立Repos的场景
const traits = {}
traits.dob = segmentEvent.birthday
Segment这种方式已经被广为接受。然而,根据目标端API不同这种转换非常复杂。例如,对老式或者无序伸展的目标端,需要更多手工配置XML的压力。
原来,当目标端被分成若干独立服务时,所有代码都存放在一个repo中。如果某个测试失败,就会波及所有代码池相关的应用,需要将每个目标端的代码池分解为各自独立的,随之代码转换也就很自然了。这种分离是的目标端测试和部署也很容易展开。
扩展微服务和Repos
例如,某事件需要用户名,event.name()可以从任何目标端代码调用。共享库检查请求字段是否正确和存在,如果不存在,则自动查找first name, firstName,first_name,FirstName等,同样会对last name进行同样操作,然后组合成full name返回。
Identify.prototype.name = function() {
var name = this.proxy('traits.name');
if (typeof name === 'string') {
return trim(name)
}
var firstName = this.firstName();
var lastName = this.lastName();
if (firstName && lastName) {
return trim(firstName + ' ' + lastName)
}
}
共享库使得新目标端创建加速,同样的概念,共享功能使得维护工作大为减轻。
然而,出现了新问题。测试和部署加快影响了共享库的部署。有时为了加速代码部署和测试,不同版本共享库会出现在不同目标端中,造成了共享库的不兼容,共享库带来的红利碰到了瓶颈。同时,微服务架构另外一个问题也开始显露出来。目标端数量不断快速增长(平均每月三个新目标端),带来更多代码池,更多队列和更多微服务,意味着更多的维护问题显现出来,迫使我们停下来反思整个架构。
打通微服务和Queues
然而,此时的架构对单一服务体系转向造成很大挑战。独立目标端队列,需要每个队列“工人”检查状态,给每个目标端添加了额外的复杂度。这个挑战催生了“Centrifuge”项目的诞生,它负责替代所有独立的队列,并将事件发送到单一的服务端。
转型Monorepo
面对超过120种不同依赖关系,需要最终转变为一种依赖关系,过程中我们不断检查依赖版本,更新到最新版本。结果是大大减少了复杂性,也大大减少了维护人力和成本。
我们还需要解决高效快速运行测试的方法,也就是之前讨论过的场景。幸运的是,目标端测试都有类似的架构。而且验证逻辑也都相仿(执行HTTP请求,验证事件被发送到正确目标端)。之前独立目标端代码池的目标是将测试失败分离开,但是这种“优势”并不能减少失败测试,反而使我们掉入了无法预测的技术泥沼。
我还记得第一次运行整合了“Traffic Recorder”的测试时,瞬间就完成了对140多个目标端的工作,运转如飞,太不可思议了。
2016年,微服务架构方兴未艾时,通过共享库模式做了32个优化。而现在,我们每年都会有46个优化,甚至过去的六个月的优化比整个2016年都多。
改变也为运维带来好处。因为目标端都在一个服务中,计算力(CPU和内存)使用都得以优化,性能不再是一个问题,运维工程师也不再需要被半夜叫醒解决负载问题。
容错。大家都运行在一个单体服务中,如果某个bug触发了灾难,会波及所有目标端。我们正在着手解决这个问题。
低效的内存级缓存。之前微服务模式,每个目标端只处理相关访问,内存级缓存信息能够保持命中率;现在内存要处理超过3000个进程,命中率自然会降低。尽管可以用Redis解决这个问题,但已经属于另外一种弹性扩展技术了。目前我们接受这种架构改变带来的效率损失。
升级版本问题。整合为一个服务解决了版本依赖的复杂性,但是也带来了版本升级对稳定服务的影响。这个问题需要用测试套件来解决。目前来看效果还比较理想。
转向单一服务模式使得开发效率大大提高,但是我们并没有对这种改变带来的问题掉以轻心。需要将测试套件整合到一个单一代码池,并且避免我们之前碰到的问题。
尽管单一服务也有问题,但是我们做了取舍,而且对这种改变带来的好处比较满意。
刚开始做取舍时,微服务确实对提高开发效率起到推动作用,但是我们的应用场景却证明,单一服务模式更适合。从微服务到单一服务改变要感谢Stephen Mathieson,Rick Branson,Achille Roussel,Tom Holmes,以及团队里的其他人。
特别感谢Rick Branson帮助修改这篇博文。
原文链接:https://segment.com/blog/goodbye-microservices/