1. 无状态:设计的应用是无状态的,那么应用容易水平扩展,生产中常见的是应用无状态,配置文件有状态,例如不同机房读取不同的数据源,这就需要配置文件或配置中心指定。
2. 拆分:垂直拆分、水平拆分、读写服务拆分、功能维度拆分。
3. 服务化:基础服务模块化,引进服务框架,服务注册与发现。服务的分组与隔离,服务限流、黑白名单等等。
4. 消息队列:解耦不同应用、异步处理、流量削峰等。要注意处理消息失败和重复接收的问题。对不能忍受失败的场景,要做好数据持久化工作,同时增加日志,报警等。
5. 缓存使用:浏览器缓存、客户端缓存、CDN、Nginx缓存、应用曾缓存、分布式缓存等。
1. 降级:开关集中化配置、可降级的多级读服务、开关前置化、业务降级。
2. 限流:恶意请求只到cache、恶意IP进行屏蔽。
3. 切流量:DNS、LVS/Nginx层切换。
1. 外网DNS用来实现全局负载均衡的流量调度。为提高Nginx性能,一般在DNS和Nginx之间接入LVS/F5做4层负载均衡。首先DNS解析请求到LVS/F5,然后LVS/F5转发给Nginx,再由Nginx转发给后端应用服务器。
2. 负载均衡算法:
1. round-robin(轮询):默认负载均衡算法,即以轮询方式将请求发给应用服务器。
2. ip_hash:根据客户IP进行负载均衡,即同一个IP的请求发给相同的服务器。
3. hash_key:对某一个key使用hash或一致性hash算法来进行负载均衡。
注:使用一致性Hash来解决增减节点造成的key重新映射的命中率低的问题。
3. 失败重试:在一定时间内重试多次都失败,则认为服务器不可用/不存活,然后可以通知服务中心将上有服务器摘掉,过一段时间后,再继续将服务器加入到存活服务器列表进行重试。
4. 健康检查:两种方式:TCP心跳检查、HTTP心跳检查。检查间隔时间不能太短、否则可能因为心跳检查太多造成压力过大。
public void main(String[] args){ SpringApplication.run(Bootstrap.Class,args); //服务注册 agentClient.register(); //JVM停止时摘除服务 RunTime.getRunTime().addShutDownHook(new Thread(){ @Override Public void run(){ agentClient.deregister(serviceId); } })
} |
主要是指线程池隔离,实际中通常是将请求分类,交给不同线程池处理。
不同应用进行拆分单独部署,这样某个服务出现问题不会影响到其他系统。
DB和redis集群的读写分离。读服务只从redistribution集群获取数据,当主redis集群出现问题时,从redis集群还可以使用。
当出现秒杀、抢购等热点活动时,为避免给其他正常服务产生冲击,将这些服务做成独立系统或对服务进行隔离,保证这些活动不会影响到主交易流程。
tomcat收到http请求后的处理流程:
1)容器负责接收并解析请求为HttpServletRequest;
2)交给Servlet进行业务处理;
3)最后通过HttpServletResponse写回响应。
在servlet2.x中,上述过程是在同一个线程中处理的,即同步处理方式;在servlet3.x中,变化有一下几点:基于NIO的异步处理能处理更高的并发连接数,请求解析和业务处理线程池分开;根据业务重要性对业务分级,并分级线程池;对线程池进行监控、运维、降级等处理。
请求解析使用单线程,解析完成后放入业务队列时,由业务线程池处理。
限流主要是对访问/请求进行限速,使其在一个时间窗口内的请求数目不会过大,从而来保护系统,一旦达到限制速率则可拒绝服务、排队或等待、降级等。
常见的限流策略有:限制总并发数(如数据库连接池、线程池等)、限制瞬时并发数(如Nginx的limit_conn参数)、限制时间窗口内的平均速率(如Nginx的limit_req参数)、以及限制远程接口的调用速率等等。
常见有令牌桶、漏桶等算法。
1)令牌桶算法:一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。桶中令牌达到最大值则丢弃或暂停添加令牌。每次请求过来则从桶中获取一个令牌,无令牌则排队等候。
2)漏桶算法:允许任意请求进来排队,应用程序按固定速率取请求并处理。当排队请求过多时则拒绝接收请求。
tomcat连接数配置:acceptCount、maxConnections、maxThreads等参数。
限流稀缺资源(如数据库连接池、线程池)数量。
限制某接口的总并发数、总请求量。可以使用AtomicLong或Semaphore来进行限流。
限流某接口的时间窗请求数:限制某接口每秒/每分钟/每小时的请求量,可以使用Guava的Cache来存储计数,过期时间设置为1分钟、1小时等等。
服务降级通常是通过开关来实现的,开关可以存放在配置文件、数据库、Redis/Zookeeper中,应用可以定期同步开关数据,然后来判断是否需要降级。
读服务降级:读服务降价的策略有:暂时切换读(降级到缓存、页面静态化)、暂时屏蔽读。
写服务降级:通常将同步操作改成异步操作,或限制写操作的请求量/请求比例。
配置中心:通常需要通过配置方式来开启/关闭降级开关,考虑通过配置文件或配置中心来进行配置,实现时要做到不需要重新启动就能动态配置开关。
实际中,如果不设置超时,可能导致请求响应过慢,慢请求累积至系统拥塞,甚至造成雪崩。
6.1 代理层超时
Nginx超时:主要有客户端超时、DNS解析超时、代理超时等。客户端超时主要设置有读取请求头超时时间、读取请求体超时时间、发送响应超时时间、长连接超时时间。
6.2 web容器超时
主要是tomcat超时,包括connectTimeout、socket.soTimeout、asyncTimeout等。
6.3 中间件客户端超时与重试
中间件超时设置包括有各个环节的调用超时,如消息中间件的注册中心、borker、生产者、消费者。服务框架的注册中心、服务提供者、服务消费者等。
重试机制可能会产生重复消息,需要消费者进行去重处理。
基于空间:超过设置的存储上限,开始清除数据;
基于容量:缓存条数大于上限值时,开始清除缓存数目;
基于时间:即设置缓存存活期,超过存活期的缓存数据清除;
基于Java对象引用:
软引用:当JVM堆内存不足时,垃圾回收器将回收软引用对象,因此,软引用对象可用来做缓存。
弱引用:当垃圾回收期回收对象时,若发现弱引用,那么会立即将其回收(每次GC都会回收弱引用对象)。
堆缓存:使用Java堆来存储缓存对象,不需要进行序列化和反序列化,是最快的缓存,缺点是缓存数据大时,GC暂停时间会变得很长,容量受堆大小限制。一般通过软引用/弱引用来存储缓存对象,内存不足时能回收掉这些对象,释放内存空间。可以使用Guava.Cache来实现。
堆外缓存:即缓存数据存放在堆外内存,可以减小GC暂停的时间。读取需要序列化/反序列化。
分布式缓存一般采用分片实现,即将数据分散到多个实例或多台机器上,算法一般采用取模和一致性hash。
缓存崩溃与修复:一是主从备份,做好冗余机制;二是如果整个缓存机制失效,考虑应用降级,慢慢减小请求量,同时慢慢将缓存重建。让一部分用户先用起来,同时更新缓存数据。另外后台worker需要对缓存数据进行预热。