宕机后的架构思考

背景

上个月公司服务器频繁宕机,第一次是因为日志撑爆了服务器,已经在[服务器日志空间解决方案-外挂磁盘]中解决了;

后面又有两次宕机,都是数据库 RDS 使用率太高导致的;

由于阿里云监控 RDS 报警不及时,所以比较难及时进行处理;

后来我使用了 processlist 查看:

1
2
3
4
select id, db, user, host, command, time, state, info
from information_schema.processlist
where command != 'Sleep'
order by time desc

可以实时的观测到是哪些 sql 现在执行慢,当然这些 sql 可能本身不慢,而是因为被慢 sql 阻塞了,导致执行时间太久;

我们可以通过 kill 命令将进程号杀掉(需要用有权限的账号登录);

这些慢 sql 是 java 的新项目部署上去没有增加有效索引导致的,而新项目 和 已经稳定并且拥有大部分用户的 PHP 项目放在同一个 ECS,数据库也是同一台 RDS; 一个库占用了大部分 CPU 牵连另一个库执行 sql,造成部分 PHP 项目的用户开始投诉;

不过该问题已经得到解决,目前已经为不同项目拆分不同 ECS 和 不同的 RDS;


当前的 PHP 项目一些表的数据也都到达 2,3k万,现在 java 的项目有数据仓,有中台; 数据增长速度会比 PHP 项目快;

虽然已经稳定运行,但是这件事让我思考一个问题,如果以后用户级上去了,该怎么办?


每秒请求数 qps

前阵子看过一篇关于架构设计的文章,这里借用他的方法来设计为当前项目的架构做分析;

架构设计一般通过估量 qps,然后来设计架构;

首先我去查看项目的 qps,因为目前项目已经在线上,可以通过动态截取每秒日志请求数来估算 QPS:

1
tail -f laravel-2019-04-01.log | cut -d ' ' f2 | uniq -c

得出结论,大概 qps 为 100 左右;

目前 100 个请求每秒对于 8 核的 ECS 已经足够;

如果按照每个请求一秒要处理 3 个 sql 请求,那么对于 RDS 就是 300 请求每秒,也是足够的;

所以,就目前位置,服务器 ECS 和 数据库 RDS 都能够承载当前用户量访问。



集群化部署

系统集群化部署

那么以后销售人员推广力度变大,qps 将逐渐变大,那么一台 ECS 可能承载不了;

这时候我们可以加多一台 ECS 来拆分 qps;

假设 qps 为 500,我们使用“负载均衡”策略,将 qps 平分到两台 ECS 服务器上:


架构

          |
        500/s
          |
          ↓
   { nginx 负载均衡 }
      |        |
   250/s     250/s
      |        |
      ↓        ↓
  { ECS1 }  { ECS2 }
      |        |
    750/s     750/s
      |        |
      +————————+
         |  |
         ↓  ↓
     { 数据库 RDS }

简单实现:

Nginx 通过 upstream 来给两台 ECS 分配权重, 以下是 tomcat + nginx 配置,PHP + nginx 也类似,只是换一下 tomcat_server 和 端口 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 负载均衡到 2 台 ECS 上,一个 130 的权重为 1/3,129 的权重为 2/3;
upstream tomcat_server {
  server 192.168.200.130:8080 weight=10
  server 192.168.200.129:8080 weight=20
}

# 监听 80 端口,并转发到 tomcat_server 这个 upstream
server {
  listen 80
  location / {
    proxy_pass http://tomcat_server;
    root html;
    index index.html, index.htm;
  }
}


数据库分库分表 + 读写分离

但是现在有一个问题,就是数据量庞大,例如开单表已经到达了 26,934,626 条,虽然都有效的利用了索引,但是明显开单已经变慢;

再加上每天店铺小妹都在同一个高峰时段不停开单,若有卡顿,则可能会拖累其他 sql 执行,并且造成 CPU 使用率上升,从而宕机;

所以面临的问题主要是两个:

1:数据量太大,导致查询缓慢:

2:面对较大的 qps,例如前面 500 qps 已经分成两个请求 750/s 到 RDS 上;

当 qps 不断增加,ECS 都可以用负载均衡增加机器解决,但是系统层面上解决了,数据库层面上并发量到达 3000/s 就有问题了;


解决方案:

  1. 通过将经常查询并且数据量大的表进行 “水平分割”;

  2. 分库分表 + 读写分离; 也就是把一个库拆分为多个库,部署在多个数据库服务上,这是作为主库承载写入请求的;然后每个主库都挂载至少一个从库,由从库来承载读请求。 此时假设对数据库层面的读写并发是 3000/s,其中写并发占到了 1000/s,读并发占到了 2000/s。 那么一旦分库分表之后,采用两台数据库服务器上部署主库来支撑写请求,每台服务器承载的写并发就是 500/s。每台主库挂载一个服务器部署从库,那么 2 个从库每个从库支撑的读并发就是 1000/s。


架构

                   |
              高峰 1000/s
                   ↓
           {  nginx 负载均衡  }
            |       |       |
          300/s   300/s   300/s
            ↓       ↓       ↓
   +———→ { ECS1 } { ECS2 } { ECS3 } ←————+
   |        |                 |          |
   |     500/s             500/s         |
 1000/s     ↓                 ↓       1000/s
   |    { 主库1 }          { 主库2 }       |
   |        ↓                 ↓           |
   +———— { 从库1 }         { 从库2 } ——————+

简单实现

主从数据库:

如果数据库是 ECS 自己创建的,需要通过开启 my.cnf 的 Binary log 功能实现 ;

如果是直接购买 RDS,可以购买两台,一台做主库,一台做从库,通过阿里云自带服务 DTS 同步数据;

分库分表:

最好的做法,是服务在 搭建之初 就设计为分库分表的存储模式,从根本上杜绝中后期的风险。 不过,会牺牲一些便利性,例如列表式的查询,同时,也增加了维护的复杂度。

如何分表: 像 Laravel,我们可以事先写一个 model,对应这个 model 的所有分表,然后为这个 model 添加一个方法;

这个方法 通过传入查询条件,对每个表进行查询,然后 union 起来返回;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/* 传入查询的开始日期和结束日期用于计算跨越的表和达到约束表数据的目的。
   外部可以调整查询的列,还可以添加where条件
*/
public function setUnionAllTable($startTime = LARAVEL_START, $endTime = LARAVEL_START, $attributes = ['*'], $wheres = [])
{
    //约束条件
    $whereConditions = [];
    $wheres = array_merge([['time', '>=', $startTime], ['time', '<', $endTime]], $wheres);
    //时间戳转日期
    $startDate = date('Y-m', $startTime);
    $endDate = date('Y-m', $endTime);
    //涉及的表数组
    $tables = [];
    //循环where数组,格式是[['字段','表达式','值',' and|or '],['字段','表达式','值',' and|or ']]
    //例子[['beauty_uid', '=', '2011654', 'and']]
    foreach ($wheres as $val) {
        //组装每个where条件
        $val[2] = $val[2] ? $val[2] : "''";
        if (isset($val[3])) {
            $whereConditions[] = " {$val[3]} {$val[0]} {$val[1]} {$val[2]}";
        } else {
            $whereConditions[] = " and {$val[0]} {$val[1]} {$val[2]}";
        }
    }
    //循环开始日期和结束日期计算跨越的表
    for ($i = $startDate; $i <= $endDate; $i = date('Y-m', strtotime($i . '+1month'))) {
        $tables[] = 'select ' . implode(',', $attributes) . ' from cdb_honey_log_' . date('Yn', strtotime($i)) . ' where 1' . implode('', $whereConditions);
    }
    //会得到每一个表的子查询,因为都有约束条件,所以每一个子查询得结果集都不会很多
    //用setTable的方法把这个子查询union all 后 as一个表名作为model的table属性
    //sql大概会是:(select xxx,xxx from honey_log_20177 where time >= 开始日期 and time < 结束日期 and xxx union all select xxx,xxx from honey_log_20178 where time >= 开始日期 and time < 结束日期 and xxx) as cdb_honey_log
    //核心是看你输入的开始日期和结束日期和约束条件,组装成一个union all的子查询然后作为table赋予model
    return $this->setTable(DB::raw('(' . implode(' union all ', $tables) . ') as cdb_honey_log'));
}

不过,最好还是使用 数据库中间件,数据量再大的时候,放到 Elasticsearch 里面,自带分片处理,交给他底层实现。



缓存

当 qps 再次上升,系统层面可以加机器,但是数据库其实本身不是用来承载高并发请求的,

所以通常来说,数据库单机每秒承载的并发就在几千的数量级,而且数据库使用的机器都是比较高配置,比较昂贵的机器,成本很高。;

或者原先没有分库分表,想分库加机器又觉得贵;

对于这两个问题,可以通过引入缓存解决;

根据 “二八定律”,80%的请求只关注在20%的热点数据上。因此,我们应该建立 服务器 和 数据库 之间的缓存机制。

这种机制,可以用磁盘作为缓存,也可以用内存缓存的方式。通过它们,将大部分的 热点数据查询,阻挡在数据库之前;

可以根据系统的业务特性,对那种写少读多的请求,引入缓存集群。

写数据库的时候同时写一份数据到缓存集群里,然后用缓存集群来承载大部分的读请求,这里可以使用 Memcached 或是 Redis 实现;

像前面架构图,读请求目前是每秒 2000/s,两个从库各自抗了 1000/s 读请求,但是其中可能每秒 1800 次的读请求都是可以直接读缓存里的不怎么变化的数据的,

剩下落到数据库的只有 200 次;


架构

                       |
                  高峰 1000/s
                       |
                       ↓
             {  nginx 负载均衡  }
               |       |       |
             300/s   300/s   300/s
               ↓       ↓       ↓
+——————+——→ { ECS1 } { ECS2 } { ECS3 } ←————+———————+
|      |        |                 |         |       |
|      |     500/s             500/s        |       |
|    100/s      ↓                 ↓        100/s    |
|      |    { 主库1 }          { 主库2 }      |      |
|      |        |                 |          |      |
|      |       同步              同步         |      |
900/s  |        ↓                 ↓          |    900/s
|      +——  { 从库1 }         { 从库2 } ——————+      |
|                                                   |
+——————————————————— { 缓存集群 } ———————————————————+

参考:http://www.php.cn/linux-417207.html



消息中间件集群

上面对读的数据做了缓存,那么如果 写操作 比较多,那该怎么办,不可能一直加机器吧?

这里我们要引入 消息中间件集群,例如 RabbitMQ,他是非常好的做写请求异步化处理,实现 削峰填谷 的效果。

架构

                       |
                  高峰 1000/s
                       |
                       ↓
             {  nginx 负载均衡  }
               |       |       |
             300/s   300/s   300/s
               |       |       |   +————————————————————————————+
               ↓       ↓       ↓   |                            |
+——————+——→ { ECS1 } { ECS2 } { ECS3 } ←————+———————+           |
|      |        |                 |         |       |     { 消息中间件 MQ }
|      |     500/s             500/s        |       |           |
|      |        |                 |         |       |    削峰填谷 100/s 请求慢慢写
|    100/s      ↓                 ↓        100/s    |           |
|      |    { 主库1 }          { 主库2 } ←————+——————+———————————+
|      |        |                 |          |      |
900/s  |        ↓                 ↓          |    900/s
|      +——  { 从库1 }         { 从库2 } ——————+      |
|                                                   |
+——————————————————— { 缓存集群 } ———————————————————+

Comments