2016年微服务架构很火。

Martin Fowler的那篇《Microservices》文章被翻译成了中文在技术圈广为流传。各种微服务的框架比如Spring Cloud、Dubbo也越来越成熟。很多公司都开始把单体应用拆成微服务好像不搞微服务就落伍了。

我们公司也跟风把一个用了好几年的单体应用拆成了微服务。

但是拆完之后才发现微服务不是银弹。它解决了单体应用的一些问题但是也带来了很多新的问题。服务拆分、服务注册发现、配置中心、分布式事务、服务监控、链路追踪……每一个都是坑。

今天就来聊聊我在微服务架构初探实战中踩过的坑以及从中得到的经验和教训。

一、服务拆分:拆得太细,反而更复杂

第一个坑就是服务拆分。

微服务第一步就是把单体应用拆分成多个小服务。但是怎么拆?拆多细?这是一个很有学问的问题。

我们那时候对微服务理解不深觉得微服务就是要拆得越细越好每个服务越小越好。于是我们把一个本来只有几个模块的单体应用拆成了二十多个微服务。

比如用户相关的功能我们拆成了用户注册服务、用户登录服务、用户信息服务、用户权限服务、用户头像服务等等。订单相关的功能拆成了订单创建服务、订单查询服务、订单支付服务、订单取消服务、订单退款服务等等。

拆完之后我们发现问题来了。

1. 服务太多管理困难

二十多个服务每个服务都要单独部署单独运维单独监控。光部署就很麻烦每次发布要发布二十多个服务每个服务都要测试、部署、验证很费时间。

而且服务多了出问题的概率也大了。今天这个服务出问题了明天那个服务出问题了运维的同事天天救火很累。

2. 调用链太长性能差

因为服务拆得太细一个简单的业务流程要调用好几个服务。比如用户下单要调用用户服务(验证用户)、商品服务(查询商品)、库存服务(扣减库存)、订单服务(创建订单)、支付服务(发起支付)、通知服务(发送通知),一共六个服务。

调用链太长了每个服务调用都有网络开销性能比单体应用差了很多。而且调用链越长出问题的概率越大任何一个服务出问题都会影响整个流程。

3. 分布式事务很头疼

单体应用的时候事务很简单用数据库的本地事务就能搞定。但是微服务之后一个业务流程涉及多个服务每个服务有自己的数据库本地事务不管用了需要分布式事务。

分布式事务是微服务的一大难题。我们试过两阶段提交(2PC),但是性能太差而且有锁容易死锁。也试过TCC(Try-Confirm-Cancel),但是开发成本太高每个服务都要写Try、Confirm、Cancel三个接口很麻烦。最后只能用最终一致性通过消息队列异步处理但是业务的复杂度增加了很多。

4. 数据聚合很麻烦

单体应用的时候查数据一个SQL,join几张表就搞定了。但是微服务之后数据分散在不同的服务不同的数据库里不能直接join了。

要查一个聚合的数据比如用户的订单列表包含用户信息、商品信息、订单信息就要调用用户服务、商品服务、订单服务三个服务然后在内存里聚合数据。这样不仅性能差而且代码也很复杂。

后来我们才明白服务拆分不是越细越好而是要根据业务的边界合理拆分。一般按照领域驱动设计(DDD)的限界上下文来拆分比较合理。一个限界上下文拆成一个服务就够了不需要拆得太细。

而且微服务不是所有的系统都适合。如果系统不大业务不复杂团队不大单体应用可能更合适。微服务是为了解决大型复杂系统的问题如果系统本身就不大搞微服务反而会增加复杂度。

二、注册中心:选了一个,不成熟的方案

第二个坑就是注册中心。

微服务服务很多服务之间要互相调用就需要知道对方的地址。如果把地址写死在配置里服务扩容、缩容、迁移都要改配置很麻烦。所以需要一个注册中心服务启动的时候把自己的地址注册到注册中心调用的时候从注册中心获取对方的地址。

我们那时候在选型的时候纠结了很久。当时主流的注册中心有ZooKeeper、Eureka、Consul、etcd。

ZooKeeper是比较老牌的用的人很多但是它是为分布式协调设计的用来做注册中心有点重而且CAP模型是CP的网络分区的时候会拒绝服务可用性不够好。

Eureka是Spring Cloud默认的注册中心AP模型可用性好但是那时候Eureka 2.0宣布不开源了未来不确定。

Consul功能很全不仅能做注册中心还能做配置中心支持健康检查支持多数据中心但是那时候Consul还比较新用的人不多资料也少。

etcd是CoreOS开发的轻量级高性能但是那时候etcd主要用在Kubernetes里单独做注册中心的不多。

我们那时候图新鲜选了Consul。因为Consul功能全不仅能做注册中心还能做配置中心一举两得。而且Consul是Go写的性能好部署简单一个二进制文件就搞定了。

但是用了之后才发现踩坑了。

1. 资料少遇到问题没人问

那时候Consul还比较新国内用的人不多资料也少。遇到问题查不到解决方案只能自己看官方文档看源码摸索很费时间。

比如Consul的健康检查怎么配置才合理?Consul的多数据中心怎么配置?Consul的KV存储怎么用做配置中心?这些问题都没有现成的答案只能自己摸索。

2. 和Spring Cloud集成不够好

我们的服务是用Java + Spring Boot写的用了Spring Cloud。Spring Cloud默认是支持Eureka的对Consul的支持那时候还不够好有一些兼容性的问题。

比如服务注册的时候有时候会注册失败但是不报错服务以为自己注册成功了实际上注册中心里没有这个服务调用的时候就会找不到服务。

再比如健康检查Spring Cloud和Consul的健康检查集成得不好有时候服务已经挂了但是Consul还认为服务是健康的还会把请求转发过去导致调用失败。

3. 运维复杂

Consul虽然部署简单但是运维还是有一些复杂的地方。比如Consul的集群怎么搭建?节点怎么加入集群?数据怎么同步?备份怎么做?这些都需要学习和经验。

而且Consul是Go写的我们团队主要是Java开发对Go不熟悉遇到Consul本身的问题比如性能问题bug很难排查和解决。

后来我们把注册中心换成了Eureka。虽然Eureka 2.0不开源了但是Eureka 1.x已经很稳定了而且和Spring Cloud集成得最好资料也多遇到问题很容易找到解决方案。

换了Eureka之后注册中心的问题少了很多稳定了很多。

这次经历让我明白技术选型不要图新鲜不要追新要选成熟的稳定的资料多的团队熟悉的技术。新的技术虽然看起来很美好但是可能有很多未知的坑踩起来很痛苦。

三、配置中心:配置更新,不及时

第三个坑就是配置中心。

微服务服务很多每个服务都有自己的配置。如果配置都写在本地的配置文件里修改配置要改文件重新部署很麻烦。而且有些配置需要动态更新比如限流的阈值开关的状态不需要重新部署就能生效。所以需要一个配置中心统一管理所有服务的配置支持动态更新。

我们那时候因为选了Consul做注册中心而Consul自带KV存储可以做配置中心所以就用Consul的KV做配置中心。

但是用了之后发现配置更新不及时。

1. 配置更新有延迟

Consul的KV更新配置之后服务端要过一会儿才能收到配置更新的通知。因为Consul是用长轮询(long polling)的方式通知客户端配置更新了。长轮询有一个超时时间默认是5分钟也就是说最坏的情况下配置更新要5分钟才能生效。

对于一些需要立即生效的配置比如开关限流阈值5分钟的延迟太长了。有时候出问题了想关一个开关要等5分钟才能生效这5分钟可能已经造成了很大的影响。

2. 配置更新可能丢失

Consul的长轮询有时候会因为网络问题断开断开之后客户端要重新建立连接在重新建立连接的过程中如果配置更新了客户端可能收不到通知导致配置更新丢失。

我们就遇到过一次配置更新丢失的问题。我们在配置中心改了一个限流的阈值但是有一个服务没有收到更新通知还是用的旧的阈值导致那个服务被流量打挂了。排查了很久才发现是配置更新丢失了。

3. 没有版本管理和回滚

Consul的KV做配置中心没有版本管理配置改了之后旧的版本就没了不能查看历史版本也不能一键回滚。如果配置改错了想回滚只能手动改回去而且如果你不记得旧的配置是什么就麻烦了。

后来我们把配置中心换成了Apollo(携程开源的配置中心)。Apollo专门为配置中心设计的功能很完善支持配置的实时推送版本管理一键回滚灰度发布权限管理等等。而且和Spring Cloud集成得也很好。

换了Apollo之后配置中心的问题解决了配置更新实时生效有版本管理能回滚很放心。

这次经历让我明白专业的事情要交给专业的工具。注册中心就用专门的注册中心;配置中心就用专门的配置中心。不要图省事用一个工具做所有的事情因为这样每个功能都做得不够好会有很多坑。

四、服务监控:出了问题,不知道

第四个坑就是服务监控。

单体应用的时候监控很简单看一下服务器的CPU、内存、磁盘看一下应用的日志就差不多了。但是微服务之后服务很多调用链很长出了问题很难快速定位是哪个服务出了问题因为什么原因。

我们那时候对微服务的监控重视不够只做了基础的监控比如服务器的CPU、内存服务的存活状态。没有做更深入的监控比如接口的响应时间调用量错误率调用链追踪。

结果出了一次线上故障我们排查了很久才找到问题所在。

那次用户反馈网站有时候很慢有时候报错。我们看了服务器的监控CPU、内存都正常。看了各个服务的存活状态也都正常。但是用户就是反馈有问题。

我们只能一个服务一个服务地看日志看有没有报错。看了半天才发现是商品服务的一个接口响应时间很长有时候会超时。因为商品服务依赖的一个第三方接口出问题了响应很慢导致商品服务的这个接口也很慢进而影响了整个调用链。

如果我们有接口级别的监控能看到每个接口的响应时间调用量错误率就能很快发现商品服务的这个接口响应时间异常很快就能定位问题。但是我们没有只能靠人工看日志排查很费时间。

而且因为没有调用链追踪我们不知道一个请求经过了哪些服务每个服务花了多长时间出了问题很难快速定位是哪个服务的问题。

后来我们补了监控系统。用Prometheus + Grafana做指标监控监控每个接口的响应时间调用量错误率还有服务器的CPU、内存、磁盘。用Zipkin做调用链追踪能看到一个请求经过了哪些服务每个服务花了多长时间。用ELK(Elasticsearch + Logstash + Kibana),做日志聚合能统一查询所有服务的日志。

补了监控系统之后出了问题就能很快定位是哪个服务的问题因为什么原因排查效率提高了很多。

这次经历让我明白微服务监控非常重要。微服务服务多调用链长出问题的概率大如果没有完善的监控出了问题很难快速定位会造成很大的影响。所以在做微服务架构的时候一定要同步建设监控系统不要等出了问题才想起来补监控。

五、团队协作:沟通成本,增加了

第五个坑就是团队协作。

单体应用的时候团队在一起开发共用一个代码库沟通很方便有问题喊一声就能讨论。但是微服务之后服务拆成了多个每个服务可能由不同的团队负责代码库也分开了沟通成本增加了很多。

我们那时候团队不大但是因为服务拆得太细二十多个服务每个人负责好几个服务。服务之间有依赖要定义接口联调测试沟通很费时间。

1. 接口定义不清晰

服务之间通过接口调用接口的定义很重要。但是我们那时候接口定义很随意有时候就是口头说一下就开始写代码了。结果联调的时候发现参数对不上返回值对不上字段名对不上要反复改反复联调很费时间。

后来我们用了Swagger来定义接口自动生成接口文档这样接口定义就清晰了大家都按照接口文档来开发联调的时候问题就少了很多。

2. 联调测试很麻烦

单体应用的时候测试很简单启动一个应用就能测试了。但是微服务之后一个业务流程涉及多个服务测试的时候要启动所有相关的服务才能联调测试。服务多了本地根本启动不了所有的服务只能部署到测试环境才能联调。

而且测试环境是大家共用的有时候A同学在测试改了一个服务B同学也在测试被影响了大家互相干扰测试效率很低。

后来我们用了Docker每个服务打包成一个Docker镜像测试的时候用Docker Compose一键启动所有相关的服务每个人都有自己的测试环境互不干扰测试效率提高了很多。

3. 发布协调很复杂

单体应用的时候发布很简单发布一个应用就行了。但是微服务之后一个功能可能涉及多个服务发布的时候要协调多个服务的发布顺序发布时间很复杂。

而且服务之间有依赖新版本的服务可能依赖另一个服务的新版本如果发布顺序不对可能会导致调用失败影响线上业务。

后来我们制定了发布规范要求服务之间要向后兼容新版本不能破坏旧版本的接口这样发布的时候就不需要严格的顺序可以独立发布降低了发布的复杂度。

这次经历让我明白微服务不仅是技术的问题也是团队协作的问题。微服务会增加团队的沟通成本协作成本。所以在做微服务架构的时候不仅要考虑技术的问题也要考虑团队的问题要制定相应的规范流程工具来降低沟通成本协作成本。

六、我的反思和总结

踩了这么多坑我对微服务有了更深的理解。

1. 微服务不是银弹

微服务不是银弹不是用了微服务所有问题就解决了。微服务解决了单体应用的一些问题比如代码膨胀部署困难技术栈统一但是也带来了很多新的问题比如分布式事务服务监控调用链追踪团队协作等等。

所以不要盲目跟风搞微服务。要根据自己的业务团队基础设施来判断是不是适合微服务。如果系统不大业务不复杂团队不大单体应用可能更合适。

2. 微服务需要配套的基础设施

微服务不是把单体应用拆成多个服务就完事了。它需要配套的基础设施比如注册中心配置中心服务网关负载均衡熔断降级分布式事务服务监控调用链追踪日志聚合容器化部署自动化运维等等。

这些基础设施每一个都需要投入人力物力来建设和维护。如果没有这些配套的基础设施微服务只会让系统更复杂更难维护。

所以在做微服务架构之前要先评估自己有没有能力建设和维护这些配套的基础设施。如果没有还是先用单体应用比较好。

3. 服务拆分要合理不要太细

服务拆分是微服务的第一步也是最重要的一步。拆得太粗达不到微服务的效果;拆得太细会增加复杂度带来很多问题。

合理的拆分应该按照业务的边界领域驱动设计(DDD)的限界上下文来拆分。一个限界上下文拆成一个服务就够了。不要为了微服务而微服务把一个本来很内聚的业务拆成很多个小服务。

而且服务拆分不是一步到位的可以先拆成几个大的服务然后根据业务的发展再逐步拆细。不要一开始就拆得很细那样风险很大。

4. 技术选型要成熟稳定不要追新

微服务涉及的技术很多注册中心配置中心服务网关消息队列监控系统等等。每一个都有很多可选的技术。

在技术选型的时候不要图新鲜追新要选成熟的稳定的资料多的团队熟悉的技术。新的技术虽然看起来很美好但是可能有很多未知的坑踩起来很痛苦。

而且专业的事情要交给专业的工具。注册中心就用专门的注册中心;配置中心就用专门的配置中心。不要图省事用一个工具做所有的事情。

5. 监控要先行

微服务服务多调用链长出问题的概率,大。所以监控非常重要要先行不要等出了问题才想起来补监控。

在做微服务架构的时候就要同步建设监控系统。包括基础的服务器监控服务存活监控还有接口级别的指标监控调用链追踪日志聚合等等。

有了完善的监控出了问题才能快速定位快速解决减少对业务的影响。

七、写在最后

微服务架构初探实战:那些年我踩过的坑。

这一年在微服务的实践中踩了很多坑服务拆分注册中心配置中心服务监控团队协作每一个都有血泪史。但是也正是这些坑让我对微服务有了更深的理解也成长了很多。

微服务是一种架构风格也是一种组织方式。它有它的优势也有它的劣势。它不是银弹不是适合所有的系统所有的团队。

在决定要不要做微服务的时候要谨慎要根据自己的业务团队基础设施来判断。不要盲目跟风别人做微服务你也做微服务。适合自己的才是最好的。

如果决定做微服务也要循序渐进不要一步到位。先拆成几个大的服务然后根据业务的发展再逐步拆细。同时要同步建设配套的基础设施监控系统制定团队的规范流程工具来降低微服务带来的复杂度。

最后用一句话结尾:

"微服务不是目的而是手段。不要为了微服务而微服务。"

愿我们都能根据自己的实际情况选择最合适的架构写出最稳定最易维护的系统。