zoukankan      html  css  js  c++  java
  • 客服系统微服务架构的演化

     

    image

    微服务要求
    image

    image
    image
    image

    服务协作


    image

    服务治理

    image

    服务治理
    image
    image
    image

    1 怀疑第三方

    坚持一条信念:“所有第三方服务都不可靠”,不管第三方什么天花乱坠的承诺。基于这样的信念,我们需要有以下行动。

    1.1 有兜底,制定好业务降级方案

    如果第三方服务挂掉怎么办?我们业务也跟着挂掉?显然这不是我们希望看到的结果,如果能制定好降级方案,那将大大提高服务的可靠性。举几个例子以便大家更好的理解。

    比如我们做个性化推荐服务时,需要从用户中心获取用户的个性化数据,以便代入到模型里进行打分排序,但如果用户中心服务挂掉,我们获取不到数据了,那么就不推荐了?显然不行,我们可以在cache里放置一份热门商品以便兜底;

    又比如做一个数据同步的服务,这个服务需要从第三方获取最新的数据并更新到mysql中,恰好第三方提供了两种方式:1)一种是消息通知服务,只发送变更后的数据;2)一种是http服务,需要我们自己主动调用获取数据。我们一开始选择消息同步的方式,因为实时性更高,但是之后就遭遇到消息迟迟发送不过来的问题,而且也没什么异常,等我们发现一天时间已过去,问题已然升级为故障。合理的方式应该两个同步方案都使用,消息方式用于实时更新,http主动同步方式定时触发(比如1小时)用于兜底,即使消息出了问题,通过主动同步也能保证一小时一更新。

    有些时候第三方服务表面看起来正常,但是返回的数据是被污染的,这时还有什么方法兜底吗?有人说这个时候除了通知第三方快速恢复数据,基本只能干等了。举个例子,我们做移动端的检索服务,其中需要调用第三方接口获取数据来构建倒排索引,如果第三方数据出错,我们的索引也将出错,继而导致我们的检索服务筛选出错误的内容。第三方服务恢复数据最快要半小时,我们构建索引也需要半小时,即可能有超过1个多小时的时间检索服务将不能正常使用,这是不可接受的。如何兜底呢?我们采取的方法是每隔一段时间保存全量索引文件快照,一旦第三方数据源出现数据污染问题,我们先按下停止索引构建的开关,并快速回滚到早期正常的索引文件快照,这样尽管数据不是很新(可能1小时之前),但是至少能保证检索有结果,不至于对交易产生特别大的影响。

    1.2 遵循快速失败原则,一定要设置超时时间

    某服务调用的一个第三方接口正常响应时间是50ms,某天该第三方接口出现问题,大约有15%的请求响应时间超过2s,没过多久服务load飙高到10以上,响应时间也非常缓慢,即第三方服务将我们服务拖垮了。

    为什么会被拖垮?没设置超时!我们采用的是同步调用方式,使用了一个线程池,该线程池里最大线程数设置了50,如果所有线程都在忙,多余的请求就放置在队列里中。如果第三方接口响应时间都是50ms左右,那么线程都能很快处理完自己手中的活,并接着处理下一个请求,但是不幸的是如果有一定比例的第三方接口响应时间为2s,那么最后这50个线程都将被拖住,队列将会堆积大量的请求,从而导致整体服务能力极大下降。正确的做法是和第三方商量确定个较短的超时时间比如200ms,这样即使他们服务出现问题也不会对我们服务产生很大影响。

    1.3 适当保护第三方,慎重选择重试机制

    需要结合自己的业务以及异常来仔细斟酌是否使用重试机制。又如有些异常是接口处理超时异常,这个时候就需要结合业务来判断了,有些时候重试往往会给后方服务造成更大压力,启到雪上加霜的效果。

    2 防备使用方

    这里又要坚持一条信念:“所有的使用方都不靠谱”,不管使用方什么天花乱坠的保证。基于这样的信念,我们需要有以下行动。     

    2.1 设计一个好的api(RPC、Restful),避免误用

    如果你的接口让很多人误用,那要好好反思自己的接口设计了,接口设计虽然看着简单,但是学问很深,建议大家好好看看Joshua Bloch的演讲《How to Design a Good API & Why it Matters(如何设计一个好的API及为什么这很重要)》以及《Java API 设计清单》。

    a) 遵循接口最少暴露原则

    使用方用多少接口我们就提供多少,因为提供的接口越多越容易出现乱用现象,言多必失嘛。此外接口暴露越多自己维护成本就越高。

    b) 不要让使用方做接口可以做的事情

    如果使用方需要调用我们接口多次才能进行一个完整的操作,那么这个接口设计就可能有问题。比如获取数据的接口,如果仅仅提供getData(int id);接口,那么使用方如果要一次性获取20个数据,它就需要循环遍历调用我们接口20次,不仅使用方性能很差,也无端增加了我们服务的压力,这时提供getDataList(List<Integer> idList);接口显然是必要的。

    c避免长时间执行的接口

    还是以获取数据方法为例:getDataList(List<Integer> idList); 假设一个用户一次传1w个id进来,我们的服务估计没个几秒出不来结果,而且往往是超时的结果,用户怎么调用结果都是超时异常,那怎么办?限制长度,比如限制长度为100,即每次最多只能传100个id,这样就能避免长时间执行,如果用户传的id列表长度超过100就报异常。

    加了这样限制后,必须要让使用方清晰地知道这个方法有此限制。之前就遇到误用的情况,某用户一个订单买了超过100个商品,该订单服务需要调用商品中心接口获取该订单下所有商品的信息,但是怎么调用都失败,而且异常也没打出什么有价值的信息,后来排查好久才得知是商品中心接口做了长度限制。

    怎么才能做到加了限制,又不让用户误用呢?

    两种思路:1)接口帮用户做了分割调用操作,比如用户传了1w个id,接口内部分割成100个id列表(每个长度100),然后循环调用,这样对使用方屏蔽了内部机制,对使用方透明;2)让用户自己做分割,自己写循环显示调用,这样需要让用户知道我们方法做了限制,具体方法有:1)改变方法名,比如getDataListWithLimitLength(List<Integer> idList); ;2)增加注释;3)如果长度超过 100,很明确地抛出异常,很直白地进行告知。

    d)参数易用原则

    避免参数长度太长,一般超过3个后就较难使用,那有人说了我参数就是这么多,那怎么办?写个参数类嘛!

    此外避免连续的同类型的参数,不然很容易误用。能用其它类型如int等的尽量不要用String类型,这也是避免误用的方法。

    e)异常

    接口应当最真实的反应出执行中的问题,更不能用聪明的代码做某些特别处理。

    2.2 流量控制,按服务分配流量,避免滥用

    相信很多做过高并发服务的同学都碰到类似事件:某天A君突然发现自己的接口请求量突然涨到之前的10倍,没多久该接口几乎不可使用,并引发连锁反应导致整个系统崩溃。

    如何应对这种情况?生活给了我们答案:比如老式电闸都安装了保险丝,一旦有人使用超大功率的设备,保险丝就会烧断以保护各个电器不被强电流给烧坏。同理我们的接口也需要安装上“保险丝”,以防止非预期的请求对系统压力过大而引起的系统瘫痪,当流量过大时,可以采取拒绝或者引流等机制。具体限流算法参见《接口限流实践》一文。

    3 做好自己

    从需求分析、架构设计 、代码编写、测试、code review、上线、线上服务运维等阶段都可以重点展开介绍,这次简单分享下架构设计、代码编写上的几条经验原则。

    3.1 单一职责原则

    单一职责原则,在我们的需求分析、架构设计、编码等各个阶段都非常有指导意义。在需求分析阶段,单一职责原则可以界定我们服务的边界,如果服务边界如果没界定清楚,各种合理的不合理的需求都接,最后导致服务出现不可维护、不可扩展、故障不断的悲哀结局。

    对于架构来讲,单一职责也非常重要。比如读写模块放置在一起,导致读服务抖动非常厉害,如果读写分离那将大大提高读服务的稳定性(读写分离);比如一个服务上同时包含了订单、搜索、推荐的接口,那么如果推荐出了问题可能影响订单的功能,那这个时候就可以将不同接口拆分为独立服务,并独立部署,这样一个出问题也不会影响其他服务(资源隔离);又比如我们的图片服务使用独立域名、并放置到cdn上,与其它服务独立(动静分离)。

    从代码角度上讲,一个类只干一件事情,如果你的类干了多个事情,就要考虑将他分开。这样做的好处是非常清晰,以后修改起来非常方便,对其它代码的影响就很小。再细粒度看类里的方法,一个方法也只干一个事情,即只有一个功能,如果干两件事情,那就把它分开,因为修改一个功能可能会影响到另一个功能。

    3.2 控制资源的使用

    写代码脑子一定要绷紧一根弦,认知到我们所在的机器资源是有限的。机器资源有哪些?cpu、内存、网络、磁盘等,如果不做好保护控制工作,一旦某一资源满负荷,很容易导致出现线上问题。

    3.2.1 CPU资源怎么限制?

    a)计算算法优化

    如果服务需要进行大量的计算,比如推荐排序服务,那么务必对你的计算算法进行优化,比如笔者曾经对地理空间距离计算这一重度使用的算法进行了优化,取得了较好的效果,详见《地理空间距离计算优化》一文。

    b)锁

    对于很多服务而言,没有那么多耗费计算资源的算法,但cpu使用率也很高,这个时候需要看看锁的使用情况,我的建议是如无必要,尽量不用显式使用锁。

    c) 习惯问题

    比如写循环的时候,千万要检查看看是否能正确退出,有些时候一不小心,在某些条件下就成为死循环,很著名的案例就是《多线程下HashMap的死循环问题》。比如集合遍历时候使用性能较差的遍历方式、String +检查,如果有超过多个String相加,是否使用StringBuffer.append?

    d)尽量使用线程池

    通过线程池来限制线程的数目,避免线程过多造成的线程上下文切换的开销。

    e)jvm参数调优

    jvm参数也会影响cpu的使用,如《发布或重启线上服务时抖动问题解决方案》。

    3.2.2 内存资源怎么限制?

    a)Jvm参数设置

    通过JVM参数的设置来限制内存使用,jvm参数调优比较靠经验,有一篇朋友写的好文可以参考《Linux与JVM的内存关系分析》。

    b)初始化java集合类大小

    使用java集合类的时候尽量初始化大小,在长连接服务等耗费内存资源的服务中这种优化非常重要;

    c)使用内存池/对象池

    d)使用线程池的时候一定要设置队列的最大长度

    之前看过好多起故障都是由于队列最大长度没有限制最后导致内存溢出。

    e)如果数据较大避免使用本地缓存

         如果数据量较大,可以考虑放置到分布式缓存如redis、tair等,不然gc都可能把自己服务卡死;

    f)对缓存数据进行压缩

    比如之前做推荐相关服务时,需要保存用户偏好数据,如果直接保存可能有12G,后来采用短文本压缩算法直接压缩到6G,不过这时一定要考虑好压缩解压缩算法的cpu使用率、效率与压缩率的平衡,一些压缩率很高但是性能很差的算法,也不适合线上实时调用。

    有些时候直接使用probuf来序列化之后保存,这样也能节省内存空间。

    g)清楚第三方软件实现细节,精确调优

    在使用第三方软件时,只有清楚细节后才知道怎么节约内存,这点我在实际工作中深有体会,比如之前在阅读过lucene的源码后发现我们的索引文件原来是可以压缩的,而这在说明文档中都找不到,具体参考《lucene索引文件大小优化小结》一文。

    3.2.3 网络资源怎么限制?

    a)减少调用的次数

    减少调用的次数?经常看到有同学在循环里用redis/tair的get,如果意识到这里面的网络开销的话就应该使用批量处理;又如在推荐服务中经常遇到要去多个地方去取数据,一般采用多线程并行去取数据,这个时候不仅耗费cpu资源,也耗费网络资源,一种在实际中常常采用的方法就是先将很多数据离线存储到一块 ,这时候线上服务只要一个请求就能将所有数据获取。

    b)减少传输的数据量

    一种方法是压缩后传输,还有一种就是按需传输,比如经常遇到的getData(int id),如果我们返回该id对应的Data所有信息,一来人家不需要,二来数据量传输太大,这个时候可以改为getData(int id, List<String> fields),使用方传输相应的字段过来,服务端只返回使用方需要的字段即可。

    3.2.4 磁盘资源怎么限制?

         打日志要控制量,并定期清理。1)只打印关键的异常日志;2)对日志大小进行监控报警。我有一次就遇到了第三方服务挂了,然后我这边就不断打印调用该第三方服务异常的日志,本来我的服务有降级方案,如果第三方服务挂了会自动使用其它服务,但是突然收到报警说我服务挂了,登上机器一看才知道是磁盘不够导致的崩溃;3)定期对日志进行清理,比如用crontab,每隔几天对日志进行清理;4)打印日志到远端,对于一些比较重要的日志可以直接将日志打印到远端HDFS文件系统里;

    3.3 避免单点

    不要把鸡蛋放在一个篮子上!从大层次上讲服务可以多机房部署、异地多活;从自己设计角度上讲,服务应该能做到水平扩展。

    对于很多无状态的服务,通过nginx、zookeeper能轻松实现水平扩展;

    对一些job类型的服务,怎么避免单点呢,毕竟只能在一个节点上运行,可以参考《Quartz应用与集群原理分析》一文;

    对数据服务来说,怎么避免单点呢?简而言之、可以通过分片、分层等方式来实现。


    image
    image
    全栈小团队

    image
    image
    image
    image
    智能化平台
    image

    image

    ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    希望对您系统架构,软件项目开发,运维管理,系统架构与研发管理体系, 信息安全, 企业信息化等有帮助。 其它您可能感兴趣的文章:
    DevOps的基本原则与介绍
    Docker与CI持续集成/CD
    持续交付中高效率与高质量
    持续集成CI与自动化测试
    软件研发工程基础设施
    容器化实践金融业案例一
    云计算参考架构几例
    微服务与Docker介绍
    互联网直播平台架构案例一
    高可用架构案例一
    某互联网公司广告平台技术架构
    某大型电商云平台实践
    云计算参考架构几例
    移动应用App测试与质量管理一
    全面的软件测试
    著名ERP厂商的SSO单点登录解决方案介绍一
    软件项目风险管理介绍
    企业项目化管理介绍
    智能企业与信息化之一
    由企业家基本素质想到的
    敏捷软件质量保证的方法与实践
    构建高效的研发与自动化运维
    IT运维监控解决方案介绍
    IT持续集成之质量管理
    人才公司环境与企业文化
    企业绩效管理系统之平衡记分卡
    企业文化、团队文化与知识共享
    高效能的团队建设
    餐饮连锁公司IT信息化解决方案一

    如有想了解更多软件研发 , 系统 IT集成 , 企业信息化,项目管理,企业管理 等资讯,请关注我的微信订阅号:

    MegadotnetMicroMsg_thumb1_thumb1_thu[1]

     


    作者:Petter Liu
    出处:http://www.cnblogs.com/wintersun/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
    该文章也同时发布在我的独立博客中-Petter Liu Blog

  • 相关阅读:
    【转载】网站服务器运维记实:阿里云1核2G突发性能t5服务器突然变得卡顿
    【转载】C#中自定义Sort的排序规则IComparable接口
    【转载】C#将图片转换为二进制流调用
    【转载】ASP.NET MVC重写URL制作伪静态网页,URL地址以.html结尾
    Java通过反射机制修改类中的私有属性的值
    Android -- 分享功能和打开指定程序
    Java存储密码用字符数组
    java笔试题(3)
    Java中的String与常量池
    Android -- 距离感应器控制屏幕灭屏白屏
  • 原文地址:https://www.cnblogs.com/wintersun/p/6534627.html
Copyright © 2011-2022 走看看