|
本文结合自身后台开发经验,从高可用、高性能、易维护和低风险(安全)角度出发,尝试总结业界常见微服务接口设计原则,帮助大家设计出优秀的微服务。1.前言微服务是一种系统架构风格,是SOA(面向服务架构)的一种实践。微服务架构通过业务拆分实现服务组件化,通过组件组合快速开发系统,业务单一的服务组件又可以独立部署,使得整个系统变得清晰灵活:原子服务独立进程隔离部署去中心化服务治理一个大型复杂的软件应用,都可以拆分成多个微服务。各个微服务可被独立部署,各个微服务之间是松耦合的。现如今后台服务大部分以微服务的形式存在,每个微服务负责实现应用的一个功能模块。而微服务由一个个接口组成,每个接口实现某个功能模块下的子功能。以一个IM应用为例,它的功能架构可能是下面这样的:所以如果是后台开发的同学,经常需要实现一个后台微服务来提供相应的能力,完成业务功能。服务以接口形式提供服务。在实现服务时,我们要将一个大的功能拆分成一个个独立的子功能来实现,每一个子功能就是我们要在服务中实现的一个接口。有时一个服务会有很多接口,每个接口所要实现的功能可能会有关联,那么这就非常考验设计服务接口的功底,让服务变得简单可靠。业界已经有很多比较成熟的实践原则,可以帮助我们设计实现出一个可靠易维护的服务。微服务设计原则并没有严格的规范,下面结合业界成熟的方法和个人多年后台开发经验,介绍高可用,高性能,易维护,低风险服务常用的设计原则。2.高可用2.1降级兜底大部分服务是如下的结构,既要给使用方使用,又依赖于他人提供的第三方服务,中间又穿插了各种业务逻辑,这里每一块都可能是故障的来源。如果第三方服务挂掉怎么办?我们业务也跟着挂掉?显然这不是我们希望看到的结果,如果能制定好降级兜底的方案,那将大大提高服务的可靠性。比如我们做个性化推荐服务时,需要从用户中心获取用户的个性化数据,以便代入到模型里进行打分排序,但如果用户中心服务挂掉,我们获取不到数据了,那么就不推荐了?显然不行,我们可以在本地cache里放置一份热门商品以便兜底。又比如做一个数据同步的服务,这个服务需要从第三方获取最新的数据并更新到MySQL中,恰好第三方提供了两种方式:一种是消息通知服务,只发送变更后的数据;一种是HTTP服务,需要我们自己主动调用获取数据。我们一开始选择消息同步的方式,因为实时性更高,但是之后就遭遇到消息迟迟发送不过来的问题,而且也没什么异常,等我们发现一天时间已过去,问题已然升级为故障。合理的方式应该两个同步方案都使用,消息方式用于实时更新,HTTP主动同步方式定时触发(比如1小时)用于兜底,即使消息出了问题,通过主动同步也能保证一小时一更新。2.2过载保护(保护自己)如果是高并发场景使用的接口,那么需要做过载保护,防止服务过载引发雪崩。相信很多做过高并发服务的同学都碰到类似事件:某天A君突然发现自己的接口请求量突然涨到之前的10倍,没多久该接口几乎不可使用,并引发连锁反应导致整个系统崩溃。如何应对这种情况?生活给了我们答案:比如老式电闸都安装了保险丝,一旦有人使用超大功率的设备,保险丝就会烧断以保护各个电器不被强电流给烧坏。同理我们的接口也需要安装上“保险丝”,以防止非预期的请求对系统压力过大而引起的系统瘫痪,当流量过大时,可以采取拒绝或者引流等机制。过载保护的做法:请求等待时间超时比如把接收到的请求放在指定的队列中排队处理,如果请求等待时间超时了(假设是100ms),这个时候直接拒绝超时请求;再比如队列满了之后,就清除队列中一定数量的排队请求,保护服务不过载,保障服务高可用。服务过载及早拒绝根据服务当前指标(如CPU、内存使用率、平均耗时等)判断服务是否处于过载,过载则及早拒绝请求并带上特殊错误码,告知上游下游已经过载,应做限流处理。2.3流量控制(保护下游)流量控制,或者叫限流,一般用户保护下游不被大流量压垮。常见的场景有:(1)下游有严格的请求限制;比如银行转账接口,微信支付接口等都有严格的接口限频;(2)调用的下游不是为高并发场景设计;比如提供异步计算结果拉取的服务,并不需要考虑各种复杂的高并发业务场景,提供高并发流量场景的支持。每个业务场景应该在拉取数据时缓存下来,而不是每次业务请求都过来拉取,将业务流量压垮下游。(3)失败重试。调用下游失败了,一定要重试吗?如果不管三七二十一直接重试,这样是不对的,比如有些业务返回的异常表示业务逻辑出错,那么你怎么重试结果都是异常;又如有些异常是接口处理超时异常,这个时候就需要结合业务来判断了,有些时候重试往往会给后方服务造成更大压力,造成雪上加霜的效果。所有失败重试要有收敛策略,必要时才重试,做好限流处理。控制流量,常用的限流算法有漏桶算法和令牌桶算法。必要的情况下,需要实现分布式限流。2.4快速失败遵循快速失败原则,一定要设置超时时间。某服务调用的一个第三方接口正常响应时间是50ms,某天该第三方接口出现问题,大约有15%的请求响应时间超过2s,没过多久服务load飙高到10倍以上,响应时间也非常缓慢,即第三方服务将我们服务拖垮了。为什么会被拖垮?没设置超时!我们采用的是同步调用方式,使用了一个线程池,该线程池里最大线程数设置了50,如果所有线程都在忙,多余的请求就放置在队列里中。如果第三方接口响应时间都是50ms左右,那么线程都能很快处理完自己手中的活,并接着处理下一个请求,但是不幸的是如果有一定比例的第三方接口响应时间为2s,那么最后这50个线程都将被拖住,队列将会堆积大量的请求,从而导致整体服务能力极大下降。正确的做法是和第三方商量确定个较短的超时时间比如200ms,这样即使他们服务出现问题也不会对我们服务产生很大影响。2.5无状态服务尽可能地使微服务无状态。无状态服务,可以横向扩展,从而不会成为性能瓶颈。状态即数据。如果某一调用方的请求一定要落到某一后台节点,使用服务在本地缓存的数据(状态),那么这个服务就是有状态的服务。我们以前在本地内存中建立的数据缓存、Session缓存,到现在的微服务架构中就应该把这些数据迁移到分布式缓存中存储,让业务服务变成一个无状态的计算节点。迁移后,就可以做到按需动态伸缩,微服务应用在运行时动态增删节点,就不再需要考虑缓存数据如何同步的问题。2.6最少依赖能不依赖的,尽可能不依赖,越少越好。减少依赖,便可以减少故障发生的可能性,提高服务可靠性。任何依赖都有可能发生故障,即使其如何保证,我们在设计上应尽可能地减少对第三方的依赖。如果无法避免,则需要对第三方依赖在发生故障时做好相应处理,避免因第三方依赖的抖动或不可用导致我们自身服务不可用,比如降级兜底。2.7简单可靠可靠性只有靠不断追求最大程度的简化而得到。乏味是一种美德。与生活中的其他东西不同,对于软件而言,“乏味”实际上是非常正面的态度。我们不想要自发性的和有趣的程序;我们希望这些程序按设计执行,可以预见性地完成目标。与侦探小说不同,缺少刺激、悬念和困惑是源代码的理想特征。因为工程师也是人,他们经常对于自己编写的代码形成一种情感依附,这些冲突在大规模清理源代码的时候并不少见。一些人可能会提出抗议,“如果我们以后需要这个代码怎么办?”,“我们为什么不只是把这些代码注释掉,这样稍后再使用它的时候会更容易。”,“为什么不增加一个功能开关?”,这些都是糟糕的建议。源代码控制系统中的更改反转很容易,数百行的注释代码则会造成干扰和混乱;那些由于功能开关没有启用而没有被执行的代码,就像一个定时炸弹等待爆炸。极端地说,当你指望一个Web服务7*24可以用时,某种程度上,每一行新代码都是负担。法国诗人AntoinedeSaint-Exupéry曾写道:“不是在不能添加更多的时候,而是没有什么可以去掉的时候,才能达到完美”。这个原则同样适用于软件设计。API设计是这个规则应该被遵循的一个清晰的例子。书写一个明确的、简单的API是接口可靠的保证。我们向API消费者提供的方法和参数越少,这些API就越容易理解。在软件工程上,少就是多!一个很小的,很简单的API通常也是一个对问题深刻理解的标志。软件的简单性是可靠性的前提条件。当我们考虑如何简化一个给定的任务的每一步时,我们并不是在偷懒。相反,我们是在明确实际上要完成的任务是什么,以及如何容易地做到。我们对新功能说“不”的时候,不是在限制创新,而是在保持环境整洁,以免分心。这样我们可以持续关注创新,并且可以进行真正的工程工作。2.8分散原则鸡蛋不要放一个篮子,分散风险。比如一个模块的所有接口不应该放到同一个服务中,如果服务不可用,那么该模块的所有接口都不可用了。我们可以基于主次进行服务拆分,将重要接口放到一个服务中,次要接口放到另外一个服务中,避免相互影响。再如所有交易数据都放在同一个库同一张表里面,万一这个库挂了,此时影响所有交易。我们可以对数据库水平切分,分库分表。2.9隔离原则控制风险不扩散,不放大。不同模块之间要相互隔离,避免单个模块有问题影响其他模块,传播扩散了影响范围。比如部署隔离:每个模块的服务部署在不同物理机上;再如DB隔离:每个模块单独使用自身的存储实例。古代赤壁之战就是一个典型的反面例子,铁锁连船导致隔离性被破坏,一把大火烧了80W大军。隔离是有级别的,隔离级别越高,风险传播扩散的难度就越大,容灾能力越强。例如:一个应用集群由N台服务器组成,部署在同一台物理机上,或同一个机房的不同物理机上,或同一个城市的不同机房里,或不同城市里,不同的部署代表不同的容灾能力。例如:人类由无数人组成,生活在同一个地球的不同洲上,这意味着人类不具备星球级别的隔离能力,当地球出现毁灭性影响时,人类是不具备容灾的。2.10幂等设计(可重入)所谓幂等,简单地说,就是对接口的多次调用所产生的结果和调用一次是一致的。数据发生改变才需要做幂等,有些接口是天然保证幂等性的。比如查询接口,有些对数据的修改是一个常量,并且无其他记录和操作,那也可以说是具有幂等性的。其他情况下,所有涉及对数据的修改、状态的变更就都有必要防止重复性操作的发生。实现接口的幂等性可防止重复操作所带来的影响。重复请求很容易发生,比如用户误触,超时重试等。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果时网络异常(超时成功),此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条,就没有保证接口的幂等性。2.11故障自愈没有100%可靠的系统,故障不可避免,但要有自愈能力。人体拥有强大的自愈能力,比如手指划破流血,会自动止血,结痂,再到皮肤再生。微服务应该像人体一样,当面对非毁灭性伤害(故障)时,在不借助外力的情况下,自行修复故障。比如消息处理或异步逻辑等非关键操作失败引发的数据不一致,需要有最终一致的修复操作,如兜底的定时任务,失败重试队列,或由用户在下次请求时触发修复逻辑。2.12CAP定理2000年,加州大学伯克利分校的计算机科学家EricBrewer在分布式计算原理研讨会(PODC)上提出了一个猜想,分布式系统有三个指标:一致性(Consistency)可用性(Availability)分区容错性(Partition tolerance)它们的第一个字母分别是C、A、P。EricBrewer说,这三个指标最多只能同时实现两点,不可能三者兼顾,这便是著名的布鲁尔猜想。在随后的2002年,麻省理工学院(MIT)的SethGilbert和NancyLynch发表了布鲁尔猜想的证明,使之成为一个定理,即CAP定理。CAP定理告诉我们,如果服务是分布式服务,那么不同节点间通信必然存在失败可能性,即我们必须接受分区容错性(P),那么我们必须在一致性(C)和可用性(A)之间做出取舍,即要么CP,要么AP。如果你的服务偏业务逻辑,对接用户,那么可用性显得更加重要,应该选择AP,遵守BASE理论,这是大部分业务服务的选择。如果你的服务偏系统控制,对接服务,那么一致性显得更加重要,应该选择CP,遵守ACID理论,经典的比如Zookeeper。总体来说BASE理论面向的是大型高可用、可扩展的分布式系统。与传统ACID特性相反,不同于ACID的强一致性模型,BASE提出通过牺牲强一致性来获得可用性,并允许数据段时间内的不一致,但是最终达到一致状态。同时,在实际分布式场景中,不同业务对数据的一致性要求不一样,因此在设计中,ACID和BASE应做好权衡和选择。2.13BASE理论在CAP定理的背景下,大部分分布式系统都偏向业务逻辑,面向用户,那么可用性相对一致性显得更加重要。如何构建一个高可用的分布式系统,BASE理论给出了答案。2008年,eBay公司选则把资料库事务的ACID原则放宽,于计算机协会(AssociationforComputingMachinery,ACM)上发表了一篇文章Base:AnAcidAlternative,正式提出了一套BASE原则。BASE基于CAP定理逐步演化而来,其来源于对大型分布式系统实践的总结,是对CAP中一致性和可用性权衡的结果,其核心思想是即使无法做到强一致性,但每个业务根据自身的特点,采用适当的方式来使系统达到最终一致性。BASE可以看作是CAP定理的延伸。BASE理论指:BasicallyAvailable(基本可用)基本可用就是假设系统出现故障,要保证系统基本可用,而不是完全不能使用。比如采用降级兜底的策略,假设我们在做个性化推荐服务时,需要从用户中心获取用户的个性化数据,以便代入到模型里进行打分排序。但如果用户中心服务挂掉,我们获取不到数据了,那么就不推荐了?显然不行,我们可以在本地cache里放置一份热门商品以便兜底。Softstate(软状态)软状态指的是允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。Eventualconsistency(最终一致性)上面讲到的软状态不可能一直是软状态,必须有时间期限。在期限过后,应当保证所有副本保持数据一致性,从而达到数据的最终一致性,因此所有客户端对系统的数据访问最终都能够获取到最新的值,而这个时间期限取决于网络延时,系统负载,数据复制方案等因素。3.高性能3.1无锁3.1.1锁的问题高性能系统中使用锁,往往带来的坏处要大于好处。并发编程中,锁带解决了安全问题,同时也带来了性能问题,因为锁让并发处理变成了串行操作,所以如无必要,尽量不要显式使用锁。锁和并发,貌似有一种相克相生的关系。为了避免严重的锁竞争导致性能的下降,有些场景采用了无锁化设计,特别是在底层框架上。无锁化主要有两种实现,无锁队列和无锁数据结构。3.1.2串行无锁串行无锁最简单的实现方式可能就是单线程模型了,如Redis/Nginx都采用了这种方式。在网络编程模型中,常规的方式是主线程负责处理I/O事件,并将读到的数据压入队列,工作线程则从队列中取出数据进行处理,这种单Reactor多线程模型需要对队列进行加锁,这种模型叫单Reactor多线程模型。如下图所示:上图的模式可以改成串行无锁的形式,当MainReactoraccept一个新连接之后从众多的SubReactor选取一个进行注册,通过创建一个Queue与I/O线程进行绑定,此后该连接的读写都在同一个队列和线程中执行,无需进行队列的加锁。这种模型叫主从Reactor多线程模型。3.1.3无锁数据结构利用硬件支持的原子操作可以实现无锁的数据结构,很多语言都提供CAS原子操作(如Go中的atomic包和C++11中的atomic库),可以用于实现无锁数据结构,如无锁链表。我们以一个简单的线程安全单链表的插入操作来看下无锁编程和普通加锁的区别。templatestruct Node { Node(const T &value) : data(value) {} T data; Node *next = nullptr;};有锁链表WithLockList:templateclass WithLockList { mutex mtx; Node *head;public: void pushFront(const T &value) { auto *node = new Node(value); lock_guard lock(mtx); // (1) node->next = head; head = node; }};无锁链表LockFreeList:templateclass LockFreeList { atomic *> head;public: void pushFront(const T &value) { auto *node = new Node(value); node->next = head.load(); while(!head.compare_exchange_weak(node->next, node)); // (2) }};从代码可以看出,在有锁版本中(1)进行了加锁。在无锁版本中,(2)使用了原子CAS操作compare_exchange_weak,该函数如果存储成功则返回true,同时为了防止伪失败(即原始值等于期望值时也不一定存储成功,主要发生在缺少单条比较交换指令的硬件机器上),通常将CAS放在循环中。下面对有锁和无锁版本进行简单的性能比较,分别执行1000,000次push操作。测试代码如下:int main() { const int SIZE = 1000000; //有锁测试 auto start = chrono::steady_clock::now(); WithLockList wlList; for(int i = 0; i micro = end - start; cout << "with lock list costs micro:" << micro.count() << endl; //无锁测试 start = chrono::steady_clock::now(); LockFreeList lfList; for(int i = 0; i idList)显然是必要的。对于批量接口,我们也要注意接口的吞吐能力,避免长时间执行。还是以获取数据的接口为例:getDataList(ListidList),假设一个用户一次传1w个id进来,那么接口可能需要很长的时间才能处理完,这往往会导致超时,用户怎么调用结果都是超时异常,那怎么办?限制长度,比如限制长度为100,即每次最多只能传100个id,这样就能避免长时间执行,如果用户传的id列表长度超过100就报异常。加了这样限制后,必须要让使用方清晰地知道这个方法有此限制,尽可能地避免用户误用。有三种方法:改变方法名,比如getDataListWithLimitLength(ListidList);在接口说明文档中增加必要的注释说明;接口明确抛出超长异常,直白告知主调。3.6并发3.6.1请求并发如果一个任务需要处理多个子任务,可以将没有依赖关系的子任务并发化,这种场景在后台开发很常见。如一个请求需要查询3个数据,分别耗时T1、T2、T3,如果串行调用总耗时T=T1+T2+T3。对三个任务执行并发,总耗时T=max(T1,T2,T3)。同理,写操作也如此。对于同种请求,还可以同时进行批量合并,减少RPC调用次数。3.6.2冗余请求冗余请求指的是同时向后端服务发送多个同样的请求,谁响应快就是使用谁,其他的则丢弃。这种策略缩短了主调方的等待时间,但也使整个系统调用量猛增,一般适用于初始化或者请求少的场景。比如腾讯公司WNS的跑马模块其实就是这种机制,跑马模块为了快速建立长连接同时向后台多个IP/Port发起请求,谁快就用谁,这在弱网的移动设备上特别有用,如果使用等待超时再重试的机制,无疑将大大增加用户的等待时间。这种方式较少使用,知道即可。3.7存储设计任何一个系统,从单机到分布式,从前端到后台,功能和逻辑各不相同,但干的只有两件事:读和写。而每个系统的业务特性可能都不一样,有的侧重读、有的侧重写,有的两者兼备,本节主要探讨在不同业务场景下存储读写的一些方法论。3.7.1读写分离大多数业务都是读多写少,为了提高系统处理能力,可以采用读写分离的方式将主节点用于写,从节点用于读,如下图所示。读写分离架构有以下几个特点:(1)数据库服务为主从架构;(2)主节点负责写操作,从节点负责读操作;(3)主节点将数据复制到从节点;基于读写分离思想,可以设计出多种主从架构,如主-主-从、主-从-从等。主从节点也可以是不同的存储,如MySQL+Redis。读写分离的主从架构一般采用异步复制,会存在数据复制延迟的问题,适用于对数据一致性要求不高的业务。可采用以下几个方式尽量避免复制滞后带来的问题。写后读一致即读自己的写,适用于用户写操作后要求实时看到更新。典型的场景是,用户注册账号或者修改账户密码后,紧接着登录,此时如果读请求发送到从节点,由于数据可能还没同步完成,用户登录失败,这是不可接受的。针对这种情况,可以将自己的读请求发送到主节点上,查看其他用户信息的请求依然发送到从节点。二次读取优先读取从节点,如果读取失败或者跟踪的更新时间小于某个阀值,则再从主节点读取。区分场景关键业务读写主节点,非关键业务读写分离。单调读保证用户的读请求都发到同一个从节点,避免出现回滚的现象。如用户在M主节点更新信息后,数据很快同步到了从节点S1,用户查询时请求发往S1,看到了更新的信息。接着用户再一次查询,此时请求发到数据同步没有完成的从节点S2,用户看到的现象是刚才的更新的信息又消失了,即以为数据回滚了。3.7.2分库分表读写分离虽然可以明显的提示查询的效率,但是无法解决更高的并发写入请求的场景,这时候就需要进行分库分表,提高并发写入的能力。通常,在以下情况下需要进行分库分表:(1)单表的数据量达到了一定的量级(如mysql一般为千万级),读写的性能会下降。这时索引也会很大,性能不佳,需要分解单表。(2)数据库吞吐量达到瓶颈,需要增加更多数据库实例来分担数据读写压力。分库分表按照特定的条件将数据分散到多个数据库和表中,分为垂直切分和水平切分两种模式。垂直切分按照一定规则,如业务或模块类型,将一个数据库中的多个表分布到不同的数据库上。以电商平台为例,将商品数据、订单数据、用户数据分别存储在不同的数据库上,如下图所示:优点:(1)切分规则清晰,业务划分明确;(2)可以按照业务的类型、重要程度进行成本管理,扩展也方便;(3)数据维护简单。缺点:(1)不同表分到了不同的库中,无法使用表连接Join。不过在实际的业务设计中,也基本不会用到Join操作,一般都会建立映射表通过两次查询或者写时构造好数据存到性能更高的存储系统中。(2)事务处理复杂,原本在事务中操作同一个库的不同表不再支持。这时可以采用柔性事务或者其他分布式事物方案。水平切分按照一定规则,如哈希或取模,将同一个表中的数据拆分到多个数据库上。可以简单理解为按行拆分,拆分后的表结构是一样的。如用户信息记录,日积月累,表会越来越大,可以按照用户ID或者用户注册日期进行水平切分,存储到不同的数据库实例中。优点:(1)切分后表结构一样,业务代码不需要改动;(2)能控制单表数据量,有利于性能提升。缺点:(1)Join、count、记录合并、排序、分页等问题需要跨节点处理;(2)相对复杂,需要实现路由策略;综上所述,垂直切分和水平切分各有优缺点,通常情况下这两种模式会一起使用。3.7.3动静分离动静分离将经常更新的数据和更新频率低的数据进行分离。最常见于CDN,一个网页通常分为静态资源(/JS/CSS等)和动态资源(JSP、PHP等),采取动静分离的方式将静态资源缓存在CDN边缘节点上,只需请求动态资源即可,减少网络传输和服务负载。在数据库和KV存储上也可以采取动态分离的方式。动静分离更像是一种垂直切分,将动态和静态的字段分别存储在不同的库表中,减小数据库锁的粒度,同时可以分配不同的数据库资源来合理提升利用率。3.7.4冷热分离冷热分离可以说是每个存储产品和海量业务的必备功能,MySQL、ElasticSearch等都直接或间接支持冷热分离。将热数据放到性能更好的存储设备上,冷数据下沉到廉价的磁盘,从而节约成本。3.7.5重写轻读基本思路就是写入数据时多写点(冗余写),降低读的压力。社交平台中用户可以互相关注,查看关注用户的最新消息,形成Feed流。用户查看Feed流时,系统需要查出此用户关注了哪些用户,再查询这些用户所发的消息,按时间排序。为了满足高并发的查询请求,可以采用重写轻读,提前为每个用户准备一个收件箱。每个用户都有一个收件箱和一个发件箱。比如一个用户有1000个粉丝,他发布一条消息时,写入自己的发件箱即可,后台异步的把这条消息放到那1000个粉丝的收件箱中。这样,用户读取Feed流时就不需要实时查询聚合了,直接读自己的收件箱就行了。把计算逻辑从”读”移到了”写”一端,因为读的压力要远远大于写的压力,所以可以让”写”帮忙干点活儿,提升整体效率。上图展示了一个重写轻度的一个例子,在实际应用中可能会遇到一些问题。如:(1)写扩散:这是个写扩散的行为,如果一个大V的粉丝很多,这写扩散的代价也是很大的,而且可能有些人万年不看朋友圈甚至屏蔽了朋友。需要采取一些其他的策略,如粉丝数在某个范围内是才采取这种方式,数量太多采取推拉结合和分析一些活跃指标等。(2)信箱容量:一般来说查看Feed流(如微信朋友圈)不会不断的往下翻页查看,这时候应该限制信箱存储条目数,超出的条目从其他存储查询。3.7.6数据异构数据异构顾名思义就是存储不同结构的数据,有很多种含义:数据格式的异构数据的存储格式不同,可以是关系型(如MySQL、SQLServer、DB2等),也可以是KV格式(如Redis、Memcache等),还可以是文件行二维数据(如txt、CSV、XLS等)。数据存储地点的异构据存储在分散的物理位置上,此类情况大多出现在大型机构中,如销售数据分别存储在北京、上海、日本、韩国等多个分支机构的本地销售系统中。数据存储逻辑的异构相同的数据按照不同的逻辑来存储,比如按照不同索引维度来存储同一份数据。这里主要说的是按照不同的维度建立索引关系以加速查询。如京东、天猫等网上商城,一般按照订单号进行了分库分表。由于订单号不在同一个表中,要查询一个买家或者商家的订单列表,就需要查询所有分库然后进行数据聚合。可以采取构建异构索引,在生成订单的时同时创建买家和商家到订单的索引表,这个表可以按照用户ID进行分库分表。3.8零拷贝3.8.1为什么要实现零拷贝?这里的拷贝指的是数据在内核缓冲区和应用程序缓冲区直接的传输,并非指进程空间中的内存拷贝(当然这方面也可以实现零拷贝,如传引用和C++中move操作)。现在假设我们有个服务,提供用户下载某个文件,当请求到来时,我们把服务器磁盘上的数据发送到网络中,这个流程伪代码如下:filefd = open(...); //打开文件sockfd = socket(...); //打开socketbuffer = new buffer(...); //创建bufferread(filefd, buffer); //从文件内容读到buffer中write(sockfd, buffer); //将buffer中的内容发送到网络数据拷贝流程如下图:上图中绿色箭头表示DMAcopy,DMA(DirectMemoryAccess)即直接存储器存取,是一种快速传送数据的机制,指外部设备不通过CPU而直接与系统内存交换数据的接口技术。红色箭头表示CPUcopy。即使在有DMA技术的情况下还是存在4次拷贝,DMAcopy和CPUcopy各2次。3.8.2内存映射内存映射将用户空间的一段内存区域映射到内核空间,用户对这段内存区域的修改可以直接反映到内核空间,同样,内核空间对这段区域的修改也直接反映用户空间,简单来说就是用户空间共享这个内核缓冲区。使用内存映射来改写后的伪代码如下:filefd = open(...); //打开文件sockfd = socket(...); //打开socketbuffer = mmap(filefd); //将文件映射到进程空间write(sockfd, buffer); //将buffer中的内容发送到网络使用内存映射后数据拷贝流如下图所示:从图中可以看出,采用内存映射后数据拷贝减少为3次,不再经过应用程序直接将内核缓冲区中的数据拷贝到Socket缓冲区中。RocketMQ为了消息存储高性能,就使用了内存映射机制,将存储文件分割成多个大小固定的文件,基于内存映射执行顺序写。3.8.3零拷贝零拷贝就是一种避免CPU将数据从一块存储拷贝到另外一块存储,从而有效地提高数据传输效率的技术。Linux内核2.4以后,支持带有DMA收集拷贝功能的传输,将内核页缓存中的数据直接打包发到网络上,伪代码如下:filefd = open(...); //打开文件sockfd = socket(...); //打开socketsendfile(sockfd, filefd); //将文件内容发送到网络使用零拷贝后流程如下图:零拷贝的步骤为:(1)DMA将数据拷贝到DMA引擎的内核缓冲区中。(2)将数据的位置和长度的信息的描述符加到套接字缓冲区。(3)DMA引擎直接将数据从内核缓冲区传递到协议引擎。可以看出,零拷贝并非真正的没有拷贝,还是有2次内核缓冲区的DMA拷贝,只是消除了内核缓冲区和用户缓冲区之间的CPU拷贝。Linux中主要的零拷贝系统函数有sendfile、splice、tee等。零拷贝比普通传输会快很多,如Kafka也使用零拷贝技术。下图是来住IBM官网上普通传输和零拷贝传输的性能对比,可以看出零拷贝比普通传输快了3倍左右。4.易维护4.1充分必要不是随便一个功能就要有个接口。虽然一个接口应该只专注一件事,但并不是每一个功能都要新建一个接口。要有充分的理由和考虑,即这个接口的存在是十分有意义和价值的。无意义的接口不仅浪费开发人力,更增加了服务的维护难度,服务将会十分臃肿。相关功能我们应该考虑合为一个接口来实现。4.2单一职责每个API应该只专注做一件事情。就像我们开发人员一样,要么从事后台开发,要么从事前端开发,要么从事服务器运维开发。公司一般不会让一个人包揽所有的开发工作,因为这让员工的职责不够单一,不利于员工在专业领域的深耕,很容易成为万金油。对公司的影响是因员工对专业知识掌握的不够深,导致开发出的软件质量得不到保证。让接口的功能保持单一,实现起来不仅简单,维护起来也会容易很多,不会因为大而全的冗杂功能导致接口经常出错。比如读写分离和动静分离的做法都是单一职责原则的具体体现。如果一个接口干了两件事情,就应该把它分开,因为修改一个功能可能会影响到另一个功能。4.3内聚解耦一个接口要包含完整的业务功能,而不同接口之间的关联要尽可能的小。这样便降低了对其他接口的依赖程度,如此其他接口的变动对当前接口的影响也会降低。一般都是通过消息中间件MQ来完成接口之间的耦合。4.4开闭原则对扩展开放,对修改关闭。这句话怎么理解呢,也就是说,我们在设计一个接口的时候,应当使这个接口可以在不被修改的前提下被扩展其功能。换句话说,应当可以在不修改源代码的情况下改变接口的行为。比如IM应用中,当用户输入简介时有个长度限制,我们不应该将长度限制写死在代码,可以通过配置文件的方式来动态扩展,这就做到了对扩展开放(用户简介长度可以变更),对修改关闭(不需要修改代码)。此外,在设计模式中模板方法模式和观察者模式都是开闭原则的极好体现。4.5统一原则接口要具备统一的命名规范、统一的出入参风格、统一的异常处理流程、统一的错误码定义、统一的版本规范等。统一规范的接口有很多优点,自解释、易学习,难误用,易维护等。4.6用户重试接口失败时,应该尽可能地由用户重试。失败不可避免,因为接口无法保证100%成功。一个简单可靠的异常处理策略便是由用户重试,而不是由后台服务进行处理。还是IM应用为例,有这样的需求场景。群管理员需要拉黑用户,被拉黑的用户要先剔出群,且后续不允许加入群。那么拉黑由一个独立的接口来完成,需要两个操作。一是将用户剔出群,二是将用户写入群的黑名单存储。此时两个操作无法做到事务,也就是我们无法保证两个操作要么同时成功,要么同时失败。这种情况下我们该怎么做,既让接口实现起来简单,要能满足需求呢?我们如果将用户剔出群放到第一步,那么可能会存在踢出群成功,但是写入群的黑名单存储失败,这种情况下提示用户拉黑失败,但却把用户给踢出了群,对用户来说,体验上是个功能bug。秉着用户尽可能地由用户重试的原则,我们应该将写入群的黑名单存储放到第一步,踢出群放到第二步。并且踢出群作为非关键逻辑,允许失败,因为者可以让用户手动将该用户踢出群,这就给了用户重试的机会,并且我们的接口在实现上也变得简单。如果要引入消息队列存储踢出群的失败日志,让后由后台服务消费重试来保证一定成功,那么实现上将变得复杂且难以维护。不是非常重要的操作,一定不要这么做。4.7最小惊讶代码应该尽可能避免让读者蒙圈。只需根据需求来设计实现即可,切勿刻意去设计一个复杂无用、华而不实的API,以免弄巧成拙。一个通俗易懂易维护的API比一个炫技复杂难理解的API更容易让人接受。4.8避免无效请求不要传递无效请求至下游。无效请求下游应及早检测发现并拒绝,可能会引发相关入参无效的告警,混淆视听且骚扰。我们应避免传递无效请求至下游,避免浪费带宽和计算资源。换位思考,谁都不想浪费力气做无用功。4.9入参校验自己收到的请求要做好入参校验,及早发现无效请求并拒绝,然后告警。发现垃圾请求后推动上游不要传递无效请求至下游。此时,我们是上游的下游,做好入参校验,避免做无用功。4.10设计模式适当的使用设计模式,让我们的代码更加简洁、易读、可扩展。设计模式(DesignPattern)是一套被反复使用、多人知晓、分类编目、代码设计经验的总结。使用设计模式可以带来如下益处。简洁。比如单例模式,减少多实例创建维护的成本,获取实例只需要一个Get函数。易读。业界经验,多人知晓。如果告知他人自己使用了相应的设计模式实现某个功能,那么他人便大概知晓了你的实现细节,更加容易读懂你的代码。可扩展。设计模式不仅能简洁我们的代码,还可以增加代码的可扩展性。比如Go推崇的Option模式,既避免了书写不同参数版本的函数,又达到了无限扩增函数参数的效果,增加了函数扩展性。4.11禁用flag标识为什么接口不要使用flag标识,因为这会使接口变得臃肿,违背单一职责,最终难以维护。这里说下,我们为什么会使用flag标识。有时,我们需要提供一个读接口供上游调用查询相关信息。如主调A需要信息a,主调B需要信息b,主调C需要信息c,主调D需要信息a和b。如果为每个主调获取信息都提供单独的接口,那么接口会变得很多。为了减少接口的数量,我们很容易想到给接口增加多个flag参数,每个主调在调用接口时携带不同的flag,表明需要获取哪些信息,然后接口根据入参flag获取对应的信息。比如主调A调用时将flag_a置为true,主调B将flag_b置为true,主调C将flag_c置为true,主调D将flag_a和flag_c置为true。在项目前期或者flag数量较少的情况下,接口功能不是很多时,一般不会暴露出问题。一但开了这个口子,随着需要不同信息主调的增多,接口会不停的增加flag,最终导致接口变得庞大臃肿,不仅难以阅读维护,还会使接口性能低下。所以,我们应该禁用flag标识,尽可能地保证接口功能单一。回到上面提到的场景,不适用flag标识,我们改如何是好呢?我们应该坚持单一职责的原则,将信息进行原子分割,每个原子信息作为一个独立的接口对外提供服务。如果需要多个原子信息,我们可以增加一个proxy层,以独立接口将需要的相关原子信息汇聚组合。这么做你可能会问,接口变多了,会导致服务难以维护。不用担心,如果服务接口数量过多,我们应该对服务进行拆分。还是以上面提及的例子为例,接口禁用flag前后组织形式对比如下:4.12页宜小不宜大对于设计和实现API来说,当结果集包含成千上万条记录时,返回一个查询的所有结果可能是一个挑战,它给服务器、客户端和网络带来了不必要的压力,于是就有了分页的功能。通常我们通过一个offset偏移量或者页码来进行分页,然后通过API一页一页的查询。那么页大小设为多少合适呢?常见的页大小有50,100,200和500。如何选择页大小,我们应该在满足特定业务场景需求下,宜少不宜多。太大的页,主要有以下几个问题:影响用户体验。页太大,加载会比较慢,用户等待时间会比较长;影响接口性能。页太大,会增加数据的拉取编解码耗时,降低接口性能;浪费带宽。很多场景下,用户在浏览的过程中,不会看完一页中的所有数据,返回太大的页是一种浪费;扩展性差。随着业务的发展,接口在页大小不变的情况下,返回的页数据可能会越来越大,导致接口性能越来越差,最终拖垮接口。页大小多少合适,没有标准答案,需要根据具体的业务场景来定。但是要坚持一点,页宜小不宜大。如果接口的页大小,能用50便可满足业务需求,就不要用100和200,更不要用500。5.低风险道路千万条,安全第一条。虽然很多时候感觉网络攻击和安全事故离我们很远,但一旦发生,后面不堪设想,所以服务接口的安全问题是设计实现过程中不得不考虑的一环。下面将列举常见的服务接口面临的安全问题与应对策略,来加固我们的服务,降低安全风险。5.1防XSS5.1.1简介XSS(CrossSiteScripting)名为跨站脚本攻击,因其缩写会与层叠样式表(CascadingStyleSheets,CSS)混淆,故将其缩写为XSS。XSS漏洞是Web安全中最为常见的漏洞,通常指的是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页中,使用户加载并执行攻击者恶意制造的网页程序。这些恶意网页程序通常是JavaScript,但实际上也可以包括Java、VBScript、ActiveX、Flash,甚至是普通的HTML。攻击成功后,攻击者可能得到包括但不限于更高的权限(如执行一些操作)、私密网页内容、会话和Cookie等各种内容。XSS本质是HTML注入。5.1.2分类XSS攻击通常可以分为3类:存储型(持久型)、反射型(非持久型)、DOM型。存储型XSS危害直接。跨站代码存储在服务器,如在个人信息或发表文章的地方加入代码,如果没有过滤或过滤不严,那么这些代码将储存到服务器中,每当有用户访问该页面的时候都会触发代码执行。反射型XSS最为普遍。反射型跨站脚本漏洞,需要欺骗用户去点击链接才能触发XSS代码,一般容易出现在搜索页面。用户打开带有恶意代码的URL时,网站服务端将恶意代码从URL中取出,拼接在HTML中返回给浏览器。用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。反射型和存储型XSS的区别是:存储型XSS的恶意代码存在数据库里,反射型XSS的恶意代码存在URL里。基于DOM的XSS通过修改原始的客户端代码,受害者浏览器的DOM环境改变,导致恶意脚本的执行。也就是说,页面本身并没有变化,但由于DOM环境被恶意修改,有客户端代码被包含进了页面,并且意外执行。DOM型XSS攻击,实际上就是网站前端JavaScript代码本身不够严谨,把不可信的数据当作代码执行了。DOM型XSS跟前两种XSS的区别:DOM型XSS攻击中,取出和执行恶意代码由浏览器端完成,属于前端JavaScript自身的安全漏洞,而其他两种XSS都属于服务端的安全漏洞。5.1.3防御措施通过前面的介绍可以得知,XSS攻击有两大要素:攻击者提交恶意代码。浏览器执行恶意代码。XSS攻击主要是由程序漏洞造成的,要完全防止XSS安全漏洞主要依靠程序员较高的编程能力和安全意识,当然安全的软件开发流程及其他一些编程安全原则也可以大大减少XSS安全漏洞的发生。这些防范XSS漏洞原则包括:预防存储型和反射型XSS攻击存储型和反射型XSS都是在服务端取出恶意代码后,插入到响应HTML里的,攻击者刻意编写的“数据”被内嵌到“代码”中,被浏览器所执行。预防这两种漏洞,常见做法:输入校验。不信任UGC(用户提交的任何内容),对所有用户提交内容进行验证,包括对URL、查询关键字、HTTP头、REFER、POST数据等,仅接受指定长度范围内、采用适当格式、采用所预期的字符的内容提交,对其他的一律过滤。改成纯前端渲染,把代码和数据分隔开。纯前端渲染的过程:(1)浏览器先加载一个静态HTML,此HTML中不包含任何跟业务相关的数据。(2)然后浏览器执行HTML中的JavaScript。(3)JavaScript通过Ajax加载业务数据,调用DOMAPI更新到页面上。在纯前端渲染中,我们会明确的告诉浏览器:下面要设置的内容是文本(.innerText),还是属性(.setAttribute),还是样式(.style)等等。浏览器不会被轻易的被欺骗,执行预期外的代码了。在很多内部、管理系统中,采用纯前端渲染是非常合适的。但对于性能要求高,或有SEO需求的页面,我们仍然要面对拼接HTML的问题。拼接HTML时转义如果拼接HTML是必要的,就需要采用合适的转义库,对HTML模板各处插入点进行充分的转义。常用的模板引擎,如doT.js、ejs、FreeMarker等,对于HTML转义通常只有一个规则,就是把&"'/这几个字符转义掉,确实能起到一定的XSS防护作用,但并不完善。XSS安全漏洞简单转义是否有防护作用HTML标签文字内容有HTML属性值有CSS内联样式无内联JavaScript无内联JSON无跳转链接无所以要完善XSS防护措施,我们要使用更完善更细致的转义策略。预防DOM型XSS攻击DOM型XSS攻击,实际上就是网站前端JavaScript代码本身不够严谨,把不可信的数据当作代码执行了。在使用.innerHTML、.outerHTML、document.write()时要特别小心,不要把不可信的数据作为HTML插到页面上,而应尽量使用.textContent、.setAttribute()等。如果用Vue/React技术栈,并且不使用v-html/dangerouslySetInnerHTML功能,就在前端render阶段避免innerHTML、outerHTML的XSS隐患。DOM中的内联事件监听器,如location、onclick、onerror、onload、onmouseover等,如果项目中有用到这些的话,一定要避免在字符串中拼接不可信数据。其他手段ContentSecurityPolicy严格的CSP在XSS的防范中可以起到以下的作用:(1)禁止加载外域代码,防止复杂的攻击逻辑。(2)禁止外域提交,网站被攻击后,用户的数据不会泄露到外域。(3)禁止内联脚本执行(规则较严格,目前发现GitHub使用)。(4)禁止未授权的脚本执行(新特性,GoogleMap移动版在使用)。(5)合理使用上报可以及时发现XSS,利于尽快修复问题。HTTP-onlyCookie禁止JavaScript读取某些敏感Cookie,攻击者完成XSS注入后也无法窃取此Cookie。验证码防止脚本冒充用户提交危险操作主动检测和发现(1)使用通用XSS攻击字符串手动检测XSS漏洞。(2)使用扫描工具自动检测XSS漏洞。例如Arachni、MozillaHTTPObservatory、w3af等。5.1.4小结防范XSS是不只是服务端的任务,需要后端和前端共同参与的系统工程。虽然很难通过技术手段完全避免XSS,但通过上面的做法可以有效减少漏洞的产生和XSS攻击带来的影响。5.2防CSRF5.2.1简介CSRF(CrossSiteRequestForgery)名为跨站请求伪造,是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。一个典型的CSRF攻击有着如下的流程:受害者登录a.com,并保留了登录凭证(Cookie)。攻击者引诱受害者访问了b.com。b.com向a.com发送了一个请求:a.com/act。浏览器会默认携带a.com的Cookie。a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。a.com以受害者的名义执行了act。攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作。5.2.2示例假如一家银行用以运行转账操作的URL地址如下:https://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName那么,一个恶意攻击者可以在另一个网站上放置如下代码:如果有账户名为Alice的用户访问了恶意站点,当被加载时,链接将被触发,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失1000资金。这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。透过例子能够看出,攻击者并不能通过CSRF攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义运行操作。5.2.3防御措施CSRF通常从第三方网站发起,被攻击的网站无法防止攻击发生,只能通过增强自己网站针对CSRF的防护能力来提升安全性。上文中讲了CSRF的两个特点:CSRF(通常)发生在第三方域名。CSRF攻击者不能获取到Cookie等信息,只是使用。针对这两点,我们可以专门制定防护策略,如下:阻止不明外域的访问(1)同源检测(2)SamesiteCookie提交时要求附加本域才能获取的信息(1)CSRFToken(2)双重Cookie验证以下我们对各种防护方法做详细说明。(1)同源检测:验证HTTPReferer字段。根据HTTP协议,在HTTP头中有一个字段叫Referer,它记录了该HTTP请求的来源地址。以上文银行操作为例,Referer字段地址通常应该是转账按钮所在的网页地址,应该也位于www.examplebank.com之下。而如果是CSRF攻击传来的请求,Referer字段会包含恶意网址的地址,不会位于www.examplebank.com之下,这时候服务器就能识别出恶意的访问。这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的Referer字段。虽然HTTP协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其Referer字段的可能。(2)SamesiteCookie。为了从源头上解决这个问题,Google起草了一份草案来改进HTTP协议,那就是为Set-Cookie响应头新增Samesite属性,它用来标明这个Cookie是个“同站Cookie”,同站Cookie只能作为第一方Cookie,不能作为第三方Cookie,Samesite有两个属性值,分别是Strict和Lax。Samesite=Strict这种称为严格模式,表明这个Cookie在任何情况下都不可能作为第三方Cookie。比如说a.com设置了如下Cookie:Set-Cookie: foo=1; Samesite=StrictSet-Cookie: bar=2; Samesite=LaxSet-Cookie: baz=3我们在b.com下发起对a.com的任意请求,foo这个Cookie都不会被包含在Cookie请求头中,但bar会。Samesite=Lax这种称为宽松模式,比Strict放宽了点限制:假如这个请求是这种请求(改变了当前页面或者打开了新页面)且同时是个GET请求,则这个Cookie可以作为第三方Cookie。比如说a.com设置了如下Cookie:Set-Cookie: foo=1; Samesite=StrictSet-Cookie: bar=2; Samesite=LaxSet-Cookie: baz=3当用户从b.com点击链接进入a.com时,foo这个Cookie不会被包含在Cookie请求头中,但bar和baz会,也就是说用户在不同网站之间通过链接跳转是不受影响了。但假如这个请求是从b.com发起的对a.com的异步请求,或者页面跳转是通过表单的post提交触发的,则bar也不会发送。(3)CSRFToken。CSRF攻击之所以能够成功,是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于Cookie中,因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的Cookie来通过安全验证。要抵御CSRF,关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于Cookie之中。可以在HTTP请求中以参数的形式加入一个随机产生的Token,并在服务器端建立一个拦截器来验证这个Token,如果请求中没有Token或者Token内容不正确,则认为可能是CSRF攻击而拒绝该请求。Token一般由服务端生成(也可以由前端生成)。一般Token由随机字符串和时间戳组合后通过哈希运算获得,用户首次加载页面时由服务端返回给前端。显然在提交时Token不能再放在Cookie中了,否则又会被攻击者冒用。因此,为了安全起见,前端在访问后台接口时,可以把Token放到如下三个地方:queryheaderrequestbody(4)双重Cookie验证。在会话中存储CSRFToken比较繁琐,而且不能在通用的拦截上统一处理所有的接口。那么另一种防御措施是使用双重提交Cookie。利用CSRF攻击不能获取到用户Cookie的特点,我们可以要求Ajax和表单请求携带一个Cookie中的值。双重Cookie采用以下流程:在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串(例如csrfcookie=v8g9e4ksfhw)。在前端向后端发起请求时,取出Cookie,并添加到URL的参数中(接上例POSThttps://www.a.com/comment?csrfcookie=v8g9e4ksfhw)。后端接口验证Cookie中的字段与URL参数中的字段是否一致,不一致则拒绝。此方法相对于CSRFToken就简单了许多。可以直接通过前后端拦截的的方法自动化实现。后端校验也更加方便,只需进行请求中字段的对比,而不需要再进行查询和存储Token。5.2.4小结CSRF和XSS完全是两种不同的Web攻击手段,所以有着不同的应对方法。二者的主要区别有:(1)XSS本质是HTML注入,和SQL注入差不多,而CSRF则是冒充用户发起非法请求;(2)CSRF需要用户登录后完成攻击,XSS不需要。5.3防SQL注入什么是SQL注入?SQL注入攻击是通过将恶意的SQL语句插入到应用的输入参数中,再在后台SQL服务器上解析执行进行的攻击,它目前黑客对数据库进行攻击的最常用手段之一。为什么要防SQL注入?如果用户输入的数据被构造成恶意SQL代码,程序又未对动态构造的SQL语句使用的参数进行审查,则会带来意想不到的危险。篡改后台数据盗取敏感信息如何防SQL注入?这是开发人员应该思考的问题,作为测试人员,了解如何预防SQL注入,可以在发现注入攻击Bug时,对Bug产生原因进行定位。严格检查输入变量的类型和格式。对于整数参数,加判断条件:不能为空、参数类型必须为数字。对于字符串参数,可以使用正则表达式进行过滤:如[0-9a-zA-Z]范围内的字符串。过滤和转义特殊字符。对用户输入的SQL参数进行转义,如'"/*#等特殊字符。使用参数化查询(ParameterizedQuery)而非手动拼接SQL。不仅可以防止SQL注入,还可以避免重复编译SQL带来性能提升。具体是怎样防止SQL注入的呢?实际上当将绑定的参数传到MySQL服务器,MySQL服务器对参数进行编译,即填充到相应的占位符的过程中,做了转义操作。5.4防刷为什么要防刷?后台服务接口都应该有一个合理的请求速度,尤其对于来自真人请求的接口,如果单个用户短时间内对某个接口的请求量很大,很有可能接口被恶意强刷或客户端请求逻辑有问题。比如IM应用中的加好友请求,正常用户请求频次不会超过1/s。如果每秒钟有10+次加好友的请求,那么说明接口很有可能被刷了。接口被刷,不管是读还是写接口,都会对后台服务造成巨大压力,严重的可能会导致服务不可用。所以,我们应该对接口做适当的限频,提早拒绝非法请求。如何防刷?可以通过接口限频来应对被刷。接口请求频次的统计一般有如下维度:基于用户ID基于IP基于设备ID每个接口应该有不同的合理阈值,这个需要结合具体的业务场景来定。这个功能为服务接口的公共功能,建议做在网关层或单独的安全层。5.5防篡改什么是篡改?在一次客户端与服务端的请求过程中,从请求方到接收方中间要经过很多路由器和交换机,黑客可以在中途截获请求的数据,篡改请求内容后再发往服务端,比如中间人攻击。假设在一个网上存款系统中,一条消息表示用户的一笔转账,攻击者完全可以多次将收款账号改为自己的账号后再将请求发到服务端。为什么要防篡改?假如客户端与服务端采用的是HTTPS协议,虽然HTTPS协议可以将传输的明文进行加密,但是黑客仍然可以截获传输的数据包,进一步伪造请求进行重放攻击。如果黑客使用特殊手段让请求方设备使用了伪造的证书进行通信,那么HTTPS加密的内容也会被解密。在API接口中我们除了使用HTTPS协议进行通信外,还需要有自己的一套加解密机制,对请求的参数进行保护,防止被篡改。如何防篡改?对请求包进行签名可以有效的防篡改。具体过程如下:客户端使用约定好的秘钥对传输的参数进行加密,得到签名值signature1,一般使用HMAC客户端将签名值也放入请求的参数中,发送请求给服务端。服务端接收到客户端的请求,然后使用约定好的秘钥对请求的参数再次进行签名,得到签名值signature2。服务端比对signature1和signature2的值,如果对比一致,认定为合法请求。如果对比不一致,说明参数被篡改,为非法请求。因为黑客不知道签名的密钥,所以即使截取到请求数据,对请求参数进行篡改,但是却无法对参数进行签名,无法得到修改后参数的签名值signature。5.6防重放什么是重放?如果恶意用户抓取真实的接口请求包,不停地发起重复请求,这就是对接口的重放。为什么要重放?接口重放一般是针对写接口的恶意请求,读接口不会有什么影响。比如发帖,发消息这种写接口,如果不防重放,会出现很多垃圾内容和骚扰消息。如何防重放?防重放的目的是不允许让相同内容的请求重复发起。对于一个具体的请求,我们可以限制某个请求的生命周期,如果超过其生命周期,认定为非法请求,这样便起到了防重放的效果。具体做法是:客户端基于"请求内容+时间戳+密钥"计算一个签名signature1,一般使用HMAC。客户端请求后台接口时带上签名signature1。后台拿到签名后,会使用相同的算法计算出一个签名与前端带来的签名做比较,如果不一致,说明请求非法,直接拒绝。因为黑客不知道签名秘钥,没有办法生成新的签名。以上做法需要注意几个问题:签名计算使用的算法可能会被坏人破解。因为对于APP或桌面应用,坏人可以反汇编获取。签名计算时使用密钥需要保存在客户端本地,可能会有泄露的风险。因为对于APP或桌面应用,坏人可以反汇编获取。终端使用的时间戳是由后台返回的,这样防止前后端的本地时间不一致导致生成的签名。不适用于Web应用,坏人是可以直接查看网页源码获取签名计算使用的算法和密钥。如果要严格做到一段时间内某个请求只能被请求一次,需要对请求进行次数的统计,会用到后台存储,实现起来会复杂一点。不过一般不需要这么做。这个功能为服务接口的公共功能,建议做在网关层或单独的安全层。5.7防DDoS什么是DDoS攻击?DDoS(DistributedDenialofService)是分布式拒绝服务攻击,攻击者利用分散在各地的设备发出海量实际上并不需要的互联网流量,耗尽目标的资源,造成正常流量无法到达其预定目的地或目标服务被压垮无法提供正常服务。可能我举个例子会更加形象点。我开了一家有五十个座位的重庆火锅店,由于用料上等,童叟无欺。平时门庭若市,生意特别红火,而对面二狗家的火锅店却无人问津。二狗为了对付我,想了一个办法,叫了五十个人来我的火锅店坐着却不点菜,让别的客人无法吃饭。上面这个例子讲的就是典型的DDoS攻击。一般来说是指攻击者利用“肉鸡”对目标网站在较短的时间内发起大量请求,大规模消耗目标网站的主机资源,让它无法正常服务。因为“肉鸡”分散在各地,有分布式的特性,所以叫分布式拒绝服务攻击。在线游戏、互联网金融等领域是DDoS攻击的高发行业。为什么要防DDoS?DDoS攻击带来的危害轻微的会降低目标服务的质量,增加响应延迟,严重的直接导致目标服务崩溃,无法提供服务。所以必须要防DDoS攻击。常见的DDoS攻击有哪些?网络层攻击(1)ICMPFlood攻击。ICMPFlood攻击属于流量型的攻击方式,是利用大的流量给服务器带来较大的负载,影响服务器的正常服务。由于目前很多防火墙直接过滤ICMP报文。因此ICMPFlood出现的频度较低。(2)UDP反射攻击DNS反射攻击是一种常见的攻击媒介,网络犯罪分子通过伪装其目标的IP地址,向开放的DNS服务器发送大量请求。作为回应,这些DNS服务器通过伪造的IP地址响应恶意请求,大量的DNS答复形成洪流,从而构成预定目标的攻击。很快,通过DNS答复产生的大量流量就会造成受害企业的服务不堪重负、无法使用,并造成合法流量无法到达其预定目的地。如NTPFlood攻击,这类攻击主要利用大流量拥塞被攻击者的网络带宽,导致被攻击者的业务无法正常响应客户访问。传输层攻击(1)SYNFlood攻击。SYNFlood攻击是当前网络上最为常见的DDoS攻击,它利用了TCP协议实现上的一个缺陷。通过向网络服务所在端口发送大量的伪造源地址的攻击报文,就可能造成目标服务器中的半连接队列被占满,从而阻止其他合法用户进行访问。(2)ConnectionFlood攻击。ConnectionFlood是典型的利用小流量冲击大带宽网络服务的攻击方式,这种攻击的原理是利用真实的IP地址向服务器发起大量的连接。并且建立连接之后很长时间不释放,占用服务器的资源,造成服务器上残余连接(WAIT状态)过多,效率降低,甚至资源耗尽,无法响应其他客户所发起的链接。(3)UDPFlood攻击。UDPFlood是日渐猖厥的流量型DDoS攻击,原理也很简单。常见的情况是利用大量UDP小包冲击DNS服务器或Radius认证服务器、流媒体视频服务器。由于UDP协议是一种无连接的服务,在UDPFlood攻击中,攻击者可发送大量伪造源IP地址的小UDP包。会话层攻击(1)SSL连接攻击。比较典型的攻击类型是SSL连接攻击,这类攻击占用服务器的SSL会话资源从而达到拒绝服务的目的。应用层攻击(1)HTTPGet攻击。和服务器建立正常的TCP连接之后,不断地向后端服务接口发起Get请求,压垮后台服务。这种攻击的特点是可以绕过普通的防火墙防护,可通过Proxy代理实施攻击。(2)UDPDNSQueryFlood攻击UDPDNSQueryFlood攻击采用的方法是向被攻击的服务器发送大量的域名解析请求,通常请求解析的域名是随机生成或者是网络世界上根本不存在的域名。域名解析的过程给服务器带来了很大的负载,每秒钟域名解析请求超过一定的数量就会造成DNS服务器解析域名超时。如何防DDoS?DDoS防御是保障系统安全运行的必要举措,虽然不属于服务接口层面需要考虑的事情,但是知道相关的防御措施还是很有必要的。防御DDoS攻击的策略方法,包括但不限于:(1)定期检查服务器漏洞。定期检查服务器软件安全漏洞,是确保服务器安全的最基本措施。无论是操作系统(Windows或linux),还是网站常用应用软件(mysql、Apache、nginx、FTP等),服务器运维人员要特别关注这些软件的最新漏洞动态,出现高危漏洞要及时打补丁修补。(2)隐藏服务器真实IP。通过CDN节点中转加速服务,可以有效的隐藏网站服务器的真实IP地址。CDN服务根据网站具体情况进行选择,对于普通的中小企业站点或个人站点可以先使用免费的CDN服务,比如百度云加速、七牛CDN等,待网站流量提升了,需求高了之后,再考虑付费的CDN服务。其次,防止服务器对外传送信息泄漏IP地址,最常见的情况是,服务器不要使用发送邮件功能,因为邮件头会泄漏服务器的IP地址。如果非要发送邮件,可以通过第三方代理(例如sendcloud)发送,这样对外显示的IP是代理的IP地址。(3)关闭不必要的服务或端口。这也是服务器运维人员最常用的做法。在服务器防火墙中,只开启使用的端口,比如网站Web服务的80端口、数据库的3306端口、SSH服务的22端口等。关闭不必要的服务或端口,在路由器上过滤假IP。(4)购买高防服务器提高承受能力。该措施是通过购买高防的盾机,提高服务器的带宽等资源,来提升自身的承受攻击能力。一些知名IDC服务商都有相应的服务提供,比如阿里云、腾讯云等。但该方案成本预算较高,对于普通中小企业甚至个人站长并不合适,且不被攻击时造成服务器资源闲置,所以这里不过多阐述。(5)限制SYN/ICMP流量。用户应在路由器上配置SYN/ICMP的最大流量来限制SYN/ICMP封包所能占有的最高频宽。这样,当出现大量的超过所限定的SYN/ICMP流量时,说明不是正常的网络访问,而是有黑客入侵。早期通过限制SYN/ICMP流量是最好的防范DOS的方法,虽然目前该方法对于DDoS效果不太明显了,不过仍然能够起到一定的作用。(6)黑名单。对于恶意流量,将IP或IP段拉黑。(7)DDoS清洗。DDoS清洗会对用户请求数据进行实时监控,及时发现DOS攻击等异常流量,在不影响正常业务开展的情况下清洗掉这些异常流量。(8)CDN加速。CDN指的是网站的静态内容分发到多个服务器,用户就近访问,提高速度。因此,CDN也是带宽扩容的一种方法,可以用来防御DDoS攻击。5.8小结道高一尺,魔高一丈,没有绝对的安全,我们能做的就是尽可能地提高坏人作恶的门槛,让我们的系统变得更加安全可靠。6.小结好的服务是设计出来的,而不是维护出来的。优秀的设计原则告诉我们如何写出好的服务来应对千变万化的业务场景。所有事物都不是100%可靠的,服务亦是如此,但遵守优秀的设计原则让我们的服务距离100%可靠更近一步。参考文献GoogleCloudAPIDesginGuide知乎.怎么理解软件设计中的开闭原则?微服务的4个设计原则和19个解决方案博客园.如何健壮你的后端服务?高可用的本质一文搞懂后台高性能服务器设计的常见套路,BAT高频面试系列【架构】高可用高并发系统设计原则CAP定理的含义-阮一峰的网络日志CAP理论该怎么理解?为什么是三选二?为什么是CP或者AP?面试题有哪些?mmap详解Base:AnAcidAlternativeCacheUsagePatterns-EhcacheSecuringAPIs:10WaystoKeepYourDataandInfrastructureSafe前端安全系列(一):如何防止XSS攻击?前端安全系列(二):如何防止CSRF攻击?SQL注入攻击常见方式及测试方法|CSDN博客API接口设计:防参数篡改+防二次请求|腾讯云什么是DDoS攻击?|知乎DDoS攻击是什么?如何防止DDos攻击?|SegmentFault
|
|