目录

后端的优化

后端优化分为三个方向

  • 组件配置调优,偏运维
  • 架构调优,偏架构
  • 代码层面的调优,偏开发

配置调优

以 Nginx、PHP、MySQL 为例。

LNMP中web高并发优化配置以及配置详解
https://phpartisan.cn/news/55.html

Nginx

从简单粗暴的角度,就是提高连接数。

增加进程数,每个 CPU 配置一个进程。

进程数配置项: worker_processes
CPU 配置项: worker_cpu_affinity ,该选项使得 Nginx 每个进程都执行在不同 CPU

提高单进程允许的最多连接数。

配置项: worker_connections

理论上一台机器的最大连接数 = worker_processes * worker_connections

PHP-FPM

总体思想是控制进程数。

选项:pm

  • static
    固定进程数。如果是 PHP 专用服务器,则可以将其设置为固定,并给定一个比较大的值。
  • dynamic (默认)
    根据以下几个因素变化:
    • 启动时进程数
    • 最大进程数
    • 至少有多少个空闲进程,少了就创建新空闲进程
    • 至多有多少个空闲进程,多了就销毁空闲进程

每个 PHP-FPM 进程大致占用 20 MB 的内存,用内存除以 20 MB 就是极限数量。但是要注意,如果设置极限数量,在有其他应用占用较大内存时,会导致服务异常。

PHP

去掉没有用到的扩展。

启用 OPCache 扩展。

在某一台机器上启用 tideways 扩展,分析具体函数的耗时。

MySQL(InnoDB)

MySQL 的内存缓存大小对于性能的影响较大。

MySQL 的缓存分为两部分:

  • 索引
  • 行记录

配置项是: innodb_buffer_pool_size

这也是索引不能加太多的原因。索引加太多会导致索引占用更多的缓存,进而使得行记录的缓存减少。

索引的更多优化:

  • 索引不要加到重复数据多的列上。
    索引有一个参数 Cardinality,用于评估索引中唯一值的数目的估值。如果该值和表行数的比值小于一定程度,则不会使用索引。

  • 字段太长应使用部分索引。

  • 使用短 ID 作为主键。因为辅助索引的叶子节点存储的是主键,如果主键太大,会使得辅助索引也变大。因此通常使用自增 ID 而不是 UUID 作为主键。

  • 必要情况下创建联合索引。多条件情况下,单表只会命中其中一个单列索引。

架构调优

瓶颈主要在数据库。

Nginx

使用双 Nginx 服务器(或者更多),用上 Keepalived + VIPA 组合确保高可用。

可以设置多个 VIPA ,分布到不同机器上,这些机器互为主备。接着让域名同时解析到这些 VIPA。这样可以充分利用多台 Nginx 服务器,并且保证高可用。

MySQL(InnoDB)

从读性能和写性能两方面入手。

提高读性能:

  • 添加从机(冗余数据),读写分离。读取数据时,从不同的从机读取。
    一般一主三从,两从用于提供服务,一从用于后台访问。

    后台访问的服务如果是大数据服务,则可为这台机器设置更多索引来提升读性能。但会给运维带来维护的麻烦,所以慎用。通常来说保持与其他服务器相同的配置。

  • 水平切分。将表中的旧数据转存到同库其他表或者其他库。
    可以优先考虑分库。因为磁盘满的时候,还是要把表迁移到其他库。
  • 垂直切分。将表中不常用的和长度较大的字段拆到另一张表。
  • 冷热分离。如果只有近三个月的数据访问量大,则将近三个月的数据尽量放到固态硬盘。将三个月之前的数据放到机械硬盘。
  • 索引外置。把数据冗余一份到 Elastic Search 里面。
  • 外部缓存。业务数据缓存到 Redis 里面。Cache Aside Pattern。

注:所有数据冗余都会带来数据一致性的问题。

两种一致性问题:

  • 主从不一致

    • 业务允许时无视不一致
    • 强制读主。从库读不到时再去主库读一次。
    • 选择性读主(Redis)。数据更新通知 Redis,毫秒级缓存,查询前先看更新的数据是否在 Redis 里面,有则读主。
  • 缓存不一致(Redis)
    发生在写后立即读。缓存了旧数据。
    通过 binlog 了解主从同步进度,同步完删除缓存。

提高写性能:

  • 多主多写
    要解决 ID 冲突的问题。两种方式:
    • 设置不同起始 ID ,提高自增 ID 步长(会导致数据库配置不一致)
    • 客户端生成 ID。生成 ID 的方式可以参考分布式 ID 的几种生成方式。

分库:

  • 单 key
    场景:用户表查询比登录多
    其他字段如果要加速,则专门做一个单字段到 UID 的映射表(可放入缓存加速)
  • 1 对多
    场景:用户查订单比订单查用户多 用户订单。订单 ID 携带用户 ID 的信息。让同一个用户的订单落在同一个库。
  • 多对多
    场景:关注与粉丝。
    创建两个库,分别用其中一个字段作为分库依据。
  • 多 key
    场景:买家比卖家查订单多,查订单比查用户多
    忽略最少的部分,退化为 1 对多。
    架构不能为 1% 的性能而带来 20% 甚至更高的复杂性。

服务

无状态化,可根据需要横向扩容。

用 JWT(Json Web Token)验证身份。

文件存储放分布式文件存储上面,如 MinIO。

代码层面

分为:

  • 减少连接次数
  • 多线程/多进程
  • 缓存
  • 异步处理
  • 数据库

减少连接次数

例如项目中有一个模块,要传输脚本到目标机器上执行。分为两步:

  1. 传输脚本
  2. 执行

要建立两次连接。

优化方式:将脚本 base64_encode,然后把执行命令拼接在后面。

1
echo "base64_encoded string" | base64 -d -i > /usr/local/src/xxx.sh; bash xxx.sh "param0";

多线程/多进程

碰到有多个耗时任务,为每个任务创建一个新的线程或者进程执行。

缓存

分为应用内缓存和外置缓存。

应用内缓存有些场景需要自己维护多台机器之间的缓存信息,根据情况使用。

外置缓存(如 Redis/Memcached)。

将请求外部接口的数据缓存到 Redis,减少接口调用的耗时。

异步任务

将业务中最重要的部分执行完后,尽快返回给前端。剩余的任务丢入队列,后台处理。

MySQL(InnoDB)

总体思想是尽可能减少数据量,尽可能早结束查询,尽可能命中索引,尽可能减小锁的粒度。

在执行语句前,先用 Explain 查看执行计划,尽量命中索引,避免全表扫描。

  1. 尽量避免使用 select *,需要多少字段拿多少字段

  2. 非唯一索引尽量使用 limit

  3. 使用索引来代理 limit 处理分页
    limit 会扫描前面不要的数据,然后逐一抛弃。在 Where 里面指定 ID 范围会更快。
    业务层提供上一页和下一页的操作,避免用户一次跳多页。URL 要使用 after_xxx ,避免用户直接修改 page。例如 GitHub 的 release 列表界面。

  4. 用 Union 替代 OR
    注:MySQL 的优化器会尝试使用索引合并来自动优化 OR。

  5. 当数据集不会重复时,用 Union All 替代 Union

  6. 联合索引最左匹配原则

  7. 联合索引在范围查询的字段后就不会再走索引了

  8. 删除由最左匹配原则覆盖的索引

  9. 使用 like 时,避免把 % 放前面
    放在前面不走索引。

  10. 使用 Where 加更精确的条件限制来减少传输的数据量
    以前见过判断用户登录用户名密码的时候,把整个用户表查出来再逐一判断的代码。

  11. 避免对索引列使用 MySQL 内置函数。

  12. 优先使用 Inner Join 而不是其他 Join。

  13. 如果使用 Left Join 或者 Right Join,驱动表数据量尽可能小。

  14. 避免在索引列上使用不等号。如果索引能用范围扫描,则使用范围操作符。
    例如 a != 1,转化为 a < 1 AND a > 1。

  15. 大量数据使用批量分块插入数据
    其中一个影响因素是锁。一个事务插入已知数量的多条数据,只需获取一次锁。

  16. 使用覆盖索引
    使用索引就能获取想要的值,不需要从数据表中读。
    用于辅助索引。
    因为索引的执行顺序是:

    • 用辅助索引找到主键
    • 通过主键索引找到数据
      如果 select 的值只包括辅助索引和主键,则使用覆盖索引。
  17. 尽量不要在 select 字段多的时候使用 Distinct

  18. 批量删除数据要谨慎

    • 分批操作。
    • 如果全部数据删除,且不需要恢复,则使用 truncate 。
    • 如果不是全部删除,则把保留的数据插入到新表,再整个删除旧表。

    批量删除会加锁
    批量删除过程要写 undo 日志,一旦回滚,需要更多时间

  19. 避免数据类型隐式转换
    隐式转换会使索引失效