一、性能度量的指标
如何衡量系统接口的响应时间?
-
平均值
平均值是把统计时间段内所有请求的响应时间数据相加,再除以总请求数。平均值的敏感度差 -
最大值
统计时间段内所有请求响应时间最长的值,最大值过于敏感 -
分位值
把统计时间段内请求的响应时间从小到大排序,假如一共有100个请求,那么排在第90位的响应时间就是90分位值。一般面向用户的线上系统,看 tp999 或者 tp9999 的响应时间。(99.9%的请求的响应时间在 x 毫秒以内)
分位值排除了偶发极慢请求对于数据的影响,能够很好地反应这段时间的性能情况,分位值越大,对于慢请求的影响就越敏感。
二、高并发性能优化
要想提高qps(query per seconds),先看示例:假设一次请求任务耗时100ms,一个线程1秒钟能处理10个请求任务。qps=(处理线程数)/(单次任务响应时间ms)/1000。由于单次任务的响应时间是以毫秒计,因此除以1000转化为秒。
-
提高处理线程数
提高处理线程数可以提升 qps,但是一个系统的线程数不可能无限地增加。它受限于阿姆达尔定律(Amdahl’s law)。这可以通过压测方式知道系统的核心线程数到多少时出现性能瓶颈。 -
降低单次任务的响应时间
要完成一次请求执行完所有的业务逻辑的耗时,就是响应时间。而要降低响应时间,这主要靠各种优化,比如:引入缓存、减少系统间的RPC调用、优化代码(减少加锁次数、优化算法)等。这就需要对系统的业务逻辑比较熟悉。
三、高可用
-
核心系统一般需要保证4个9的可用性(年故障时间52分钟)。
-
发现故障
业务指标监控打点(业务逻辑失败)、系统指标监控打点(缓存、数据库访问异常)。节点之间则是通过“heartbeat”心跳包检测异常 -
处理故障
对于业务系统而言,一般是无状态的节点,可以无限地扩容。在引入灰度发布和弹性伸缩后,可应对大部分的故障场景。业务系统依赖的底层存储而言,是有状态的节点,当QPS突增时,业务系统机器可借助弹性伸缩无限地扩容,但是当底层存储扛不住时,扩容就比较困难了(可能需要迁移数据、重新部署一套存储……)此时可采取的手段有:限流、降级、调整超时时间的配置。
对于业务系统之间的 RPC 调用而言,一般会有 thrift 线程池,客户端调用服务端时,会有一个超时时间,若超时时间设置得不合理(比如默认值30秒),当调用的下游服务出现慢查询时,这些慢请求会占用客户端 worker 线程,从而导致调用方没有 worker 线程来处理其他请求了,而如果设置合理的超时时间(比如200ms),那么 200ms 之后请求超时,客户端 worker 线程就释放了,从而能够处理其他请求。
总结一下:当出现QPS流量突增时,业务系统开始出现一些超时请求的故障了,第一件事就是先扩容。如果扩容达到了底层存储再也扛不住时(默认下游服务能扛住流量。在真实的线上系统中,如果上游系统扩容了能扛住高QPS但是调用的下游服务有可能扛不住高QPS,就需要评估是否保护下游?),这时候就要开始限流、降级了。与此同时,联系DBA申请更多的存储资源。如果扩容未生效,这时候看是否能够适当调大一点超时时间(有风险,因此流量是不断增长的,更大的超时时间可能会占用更多的处理线程,从而导致其他请求无线程可用),需要确保有足够的可用线程且超时时间是合理的。因此,更保险的做法是限流、降级。 -
failover
业务系统(无状态)的 failover 就是根据配置的 qps 或者 cpu 使用率等指标自动触发弹性扩容。有状态的系统的 failover 比较复杂,因此有状态的节点一般有 master 和 slave 之分。通过心跳检测到 master 宕机后,需要发起选主,选主需要保证其余节点一致认可 master(需要一致性算法 raft/paxos),然后由新的 master 来负责数据同步并恢复故障。 -
路由与负载均衡
服务之间通过 RPC 调用时,需要选择一台合适的下游机器将请求发送过去。如何选择?
路由是从下游节点集合中筛选出符合要求的一部分节点;负载均衡是从符合要求的节点中选择出一个节点。通常是先执行路由,再执行负载均衡,路由的输出是负载均衡的输入。
路由解决:如何从下游选择出一组机器,作为请求候选机器。常见的路由策略有:同机房优先、同地域/城市优先。
负载均衡解决:从候选集机器中,选择一台,将请求发送过去。常见的负载均衡策略有:按权随机负载(每个机器有个权重,权重高的分配到的请求多,权重一样时,则随机)、RoundRobin负载。
四、读写分离与分库分表
读写分离
访问量变大的情况下,写请求走主库,从请求走读库。
- 负载均衡,读能力水平扩展
- 避免单点故障
- 需要对SQL进行判断,如果是 select 走从库,是 update/insert/delete 走主库
- 主从同步延迟
- 事务问题,如果一个事务中同时包含了读和写,那么读不从走从库,所有操作都得走主库,避免跨库事务
- 高可用性,新增slave节点,需要及时被 client 感知,将读请求发送到此 slave;slave宕机,需要检测出来并隔离,后续读请求转发到其他正常的 slave
- master 宕机,需要主从切换,将某个 slave 提升为master,写请求走新的 master
分库分表
数据量变大的情况下,将单表的数据做拆分。
- 负载均衡,写能力水平扩展
- 单表数据量太大,水平分区(sharding),将一张表的数据分配给N个表维护,有三种分法:只分表、只分库、分库分表。示例如下:
- SQL的增删改需要分发到不同的DB上执行。假设有 user 大表,拆分成四张 user 子表:user_1、user_2、user_3、user_4
insert into user(id,name) values (1,"a"),(2,"b"), (3,"c"),(4,"d")
需要改写成:
insert into user_1(id,name) values (1,"a")
insert into user_2(id,name) values (2,"b")
insert into user_3(id,name) values (3,"c")
insert into user_0(id,name) values (4,"d")
经过分库分表后,原来单表下执行的SQL需要:SQL解析、SQL路由、SQL改写、SQL执行、结果集合并,最终得到执行结果。
4. 分布式事务
分库分表后不可避免地遇到跨库事务的问题,一般使用“柔性事务”来解决。
5. 分布式id
不能再使用mysql的自增主键,需要分布式唯一ID生成器,参考:Snowflake
数据库中间件
-
数据库代理
在DB和应用程序之间单独部署数据库代理。对于应用而言通过一个普通的数据源(c3p0、druid、dbcp等)与代理服务器建立连接,然后由代理服务访问DB -
数据源代理