版权声明:by DongBao https://blog.csdn.net/aaaadong/article/details/79356749
高并发是互联网应用的一大特点,也是互联网应用不可避免的一个问题;比如 淘宝双11购物狂欢节,京东618购物促销节,12306春节火车票,促销,秒杀等; 解决高并发问题是一个系统工程,需要站在全局高度统筹谋划,从多个角度进行架构设计,在实践中,我们探索、总结和提炼出来了很多应对高并发的方案或者说手段,分别如下: |
硬件 | 缓存 | 集群 | 拆分 | 静态化 | 动静分离 | 队列 | 池化 | 优化 | 压测 |
硬件 单体应用 单体应用也叫集中式应用; 产品或者网站初期,通常功能较少,用户量也不多,所以一般按照单体应用进行设计和开发; 按照经典的MVC三层架构设计,使用单台数据库,缓存也不是必须的,应用系统和数据库部署在同一台服务器上; 随着应用系统功能的增加,访问用户的增多,单台服务器已无法承受那么多的访问流量; 此时,根据业务情况、访问流量等因素,在成本也不高的情况下,可以采用提升硬件配置的方式来解决; 能通过提升硬件解决的时候,也需要提升硬件性能; 单体应用垂直扩容 单体应用垂直扩容:指的是提升服务器硬件配置; CPU从32位提升为64位; 内存从64GB提升为256GB(比如缓存服务器); 磁盘从HDD(Hard Disk Drive)提升为SSD(固态硬盘(Solid State Drives)),有大量读写的应用; 磁盘扩容,1TB扩展到2TB,比如文件系统; 千兆网卡提升为万兆网卡; 但是不管怎么提升硬件性能,硬件性能的提升不可能永无止尽,所以最终还是要靠分布式解决; |
缓存 概述 缓存可以说是处理高并发,优化系统性能首先要考虑的一个因素; 它是解决性能问题的利器,就像一把瑞士军刀,锋利强大; HTTP缓存 浏览器缓存 浏览器缓存是指当我们使用浏览器访问一些网站页面或者HTTP服务时,根据服务器端返回的缓存设置响应头将响应内容缓存到浏览器,下次可以直接使用缓存内容或者仅需要去服务器端验证内容是否过期即可,这样可以减少浏览器和服务器之间来回传输的数据量,节省带宽,提升性能; 比如新浪:http://www.sina.com.cn/ 第二次刷新访问,返回响应码为304,表示页面内容没有修改过,浏览器缓存的内容还是最新的,不需要从服务器获取,直接读取浏览器缓存即可; DateFormat format = new SimpleDateFormat("EEE,MMM yyyy HH: mm: ss 'GMT'", Locale. US); //当前时间 long now = System.currentTimeMillis() * 1000 * 1000; response.addHeader( "Date", format.format(new Date())); //过期时间http 1. 0支持 response.addHeader("Expires", format.format (new Date(now+ 20 * 1000))); //文档生存时间http 1.1支持 response.addHeader("Cache-Control", "max-age=20"); Nginx缓存 Nginx提供了expires指令来实现缓存控制,比如: location /static { root /opt/static/; expires 1d; } 当用户访问时,Nginx拦截到请求后先从Nginx本地缓存查询数据,如果有并且没有过期,则直接返回缓存内容; 启用压缩,减少数据传输量; gzip on; #开启gzip压缩输出 CDN缓存 CDN的全称是Content Delivery Network,即内容分发网络; CDN它本身也是一个缓存,它把后端应用的数据缓存起来,用户要访问的时候,直接从cdn上获取,不需要走后端的Nginx,以及具体应用服务器,它的作用主要是加速数据的传输,也提高稳定性; 如果从CDN上没有获取到数据,再走后端的Nginx缓存,Nginx上也没有,则走后端的应用服务器; 应用缓存 内存缓存 在内存中缓存数据; 效率高,速度快; 应用重启缓存丢失 磁盘缓存 在磁盘缓存数据; 读取效率较之内存缓存稍低; 应用重启缓存不会丢失; 代码组件:Guava、Ehcache 服务器:Redis、MemCache 多级缓存 在整个应用系统的不同层级进行数据的缓存,多层次缓存,来提升访问效率; 比如:CDN -> Nginx -> Redis -> DB 经常需要读取的数据 频繁访问的数据 热点数据缓存 IO瓶颈数据 计算昂贵的数据 无需实时更新的数据 |
集群 集群部署,也叫单体应用水平扩容; 应用层面: 原来通过部署一台服务器提供服务,现在就多部署几台,那么服务的能力就会提升; 部署了多台服务器,但是用户访问入口只能是一个,比如www.web.com,所以就需要负载均衡; 负载均衡是应用集群扩容后的必须步骤; 集群部署后,用户的会话session状态要保持的话,就需要实现session共享; 数据库层面: 将数据库服务器和应用服务器分离; 看应用是多读还是多写的应用,如果是多读的应用,可通过数据库主从架构解决,写数据时操作主库,读数据时操作从库; 如果应用是多写的应用,可通过互为主从的模式进行解决; |
拆分 应用拆分 分布式 单体应用,随着业务的发展,应用功能的增加,单体应用就逐步变得非常庞大,很多人维护这么一个系统,开发、测试、上线都会造成很大问题,比如代码冲突,代码重复,逻辑错综混乱,代码逻辑复杂度增加,响应新需求的速度降低,隐藏的风险增大,所以需要按照业务维度进行应用拆分,采用分布式开发(所以在互联网公司架构师是很重要的); 应用拆分之后,就将原来在同一进程里的调用变成了远程方法调用,此时就需要使用到一些远程调用技术:htppClient、hessian、dubbo、webservice等; 随着调用的错综复杂,我们需要对应用进行服务化,比如SOA,注册中心、服务治理 随着访问流量进一步增大,那么解决方案也就越来越复杂,限流、降级、基础化、通用化等; 中等互联网公司大约发展到服务化、注册中心就差不多了; 通过应用拆分和服务化之后,扩容就变得容易,如果此时系统处理能力跟不上,只需要增加服务器即可; 数据库拆分 数据库拆分分为:垂直拆分和水平拆分 按照业务维度把相同类型的表放在一个数据库,另一些表放在另一个数据库,这种方式的拆分叫垂直拆分,也就是在不同库建不同表,把表分散到各个数据库; 比如 产品、订单、用户 三类数据以前在一个数据库中,现在可以用三个数据库,分别为 产品数据库、订单数据库、用户数据库; 这样可以将不同的数据库部署在不同的服务器上,提升单机容量和性能问题,也解决多个表之间的IO竞争问题; 根据数据行的特点和规则,将表中的某些行切分到一个数据库,而另外的某些行又切分到另一个数据库,这种方式的拆分叫水平拆分; 单库单表在数据量和流量增大的过程中,大表往往会成为性能瓶颈,所以数据库要进行水平拆分; 主从复制/读写分离/SQL优化/索引优化; 一主多从:读多写少,读多的应用前面还需要添加缓存,比如Redis/Memcache; 互为主从:写多读少的应用; 采用MyCat分库分表/读写分离,降低开发难度; |
静态化 对于一些访问量大,更新频率较低的数据,可直接定时生成静态html页面,供前端访问,而不是访问jsp; freemaker velocity thymeleaf 页面静态化首先可以大大提升访问速度,不需要去访问数据库或者缓存来获取数据,浏览器直接加载html页即可; 页面静态化可以提升网站稳定性,如果程序或数据库出了问题,静态页面依然可以正常访问; |
动静分离 采用比如Nginx实现动静分离,Nginx负责代理静态页面; Nginx的效率极高,利用它处理静态资源,可以为后端服务器分担压力; |
队列 队列是数据结构中的一种线性表,从一端插入数据,从另一端删除数据,先进先出; 在实际项目中: 不是所有的处理都必须要实时处理; 不是所有的请求都必须要实时告诉用户结果; 不是所有的请求都必须100%一次性处理成功; 不知道哪个系统需要我的协助来实现它的业务处理,保证最终一致性,不需要强一致性; 这些情况下,我们都可以考虑使用队列来处理; 队列的作用就是:异步处理/系统解耦/流量削峰; 异步处理是使用队列的一个主要原因,比如注册成功了,发优惠券/送积分/送红包/发短信/发邮件等操作都可以异步处理; 使用队列实现系统解耦,比如支付成功了,发消息通知物流系统,发票系统,库存系统等,而无需直接调用这些系统; 使用队列流量削峰,比如并发下单、秒杀等,可以考虑使用队列将请求暂时入队,通过缓存+队列的方式将流量削平,变成平缓请求进行处理; 使用队列的削峰,主要是将高峰流量变成平缓流量进行异步处理,避免应用系统因瞬间的巨大压力而压垮; 请求队列,比如web访问,我们可以采用请求队列对请求进行限流,当请求流量大于队列长度后,抛弃请求,特别是在一些流量高峰时段,保护后端服务不会被突然的巨大流量压垮服务; 常见的消息队列产品:ActiveMQ/kafka/RabbitMQ/RocketMQ/Redis队列 |
池化 在实际开发中,我们经常会采用一些池化技术,减少资源消耗,提升系统性能; 对象池 通过复用对象,减少对象创建和垃圾收集器回收对象的资源开销; 可以采用commons-pool2实现; 数据库连接池 Druid/DBCP/C3P0 Redis连接池 JedisPool(内部基于commons-pool2) HttpClient连接池 PoolingClientConnectionManager 线程池 Java提供java.util.concurrent包可以实现线程池; Executors.newFixedThreadPool(8); Executors.newSingleThreadExecutor(); Executors.newScheduledThreadPool(10); |
优化 JVM优化 设置JVM参数 -server -Xmx4g -Xms4g -Xmn256m -XX:PermSize=128m -Xss256k -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 Tomcat优化 设置JVM参数,可以参考JVM优化参数 Java程序优化 不要重复创建太多对象; 流/文件/连接 一定要记得在finally块中关闭; 少用重量级同步锁synchronized Lock 不要在循环体中使用try/catch 多定义局部变量,少定义成员变量; ...... 数据库优化 数据库服务器优化 数据库服务器的参数设置 偏DBA 数据库索引优化 建立索引的字段尽量的小,最好是数值; 尽量在唯一性高的字段上创建索引,不要在性别这种唯一性很低的字段上创建索引; SQL优化 数据搜索引擎solr/elasticsearch Nginx优化 调整配置文件参数 worker_processes 16; events { worker_connections 4096; multi_accept on; use epoll; } Linux优化 优化内核参数 偏运维 网络优化 带宽、路由器等方面 运维 |
压测 在系统上线前,需要对系统各个环节进行压力测试,发现系统的瓶颈点,然后进行调优; 即便我们的优化工作已经做得很好了,但依然也会存在一些风险因素,比如网络不稳定,机房故障,所以我们需要提前有故障预备方案,比如多机房部署容灾、路由切换等; 即便我们的故障预备方案做好了,还需要提前进行演练,以确保预案的有效性; 压力测试工具:Apache JMeter/Webbench等; 技术总监、架构师牵头,测试团队、技术团队、运维团队 共同完成; |