后端优化分为四个方向
- 组件配置调优,偏运维
- 架构调优,偏架构
- 代码层面的调优,偏开发
- CDN 加速
配置调优
以 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 扩展。
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。
代码层面
分为:
- 减少连接次数
- 多线程/多进程
- 缓存
- 数据库
减少连接次数
例如项目中有一个模块,要传输脚本到目标机器上执行。分为两步:
- 传输脚本
- 执行
要建立两次连接。
优化方式:将脚本 base64_encode,然后把执行命令拼接在后面。
echo "base64_encoded string" | base64 -d -i > /usr/local/src/xxx.sh; bash xxx.sh "param0";
多线程/多进程
碰到有多个耗时任务,为每个任务创建一个新的线程或者进程执行。
缓存
分为应用内缓存和外置缓存。
应用内缓存有些场景需要自己维护多台机器之间的缓存信息,根据情况使用。
外置缓存(如 Redis/Memcached)。
将请求外部接口的数据缓存到 Redis,减少接口调用的耗时。
MySQL(InnoDB)
总体思想是尽可能减少数据量,尽可能早结束查询,尽可能命中索引,尽可能减小锁的粒度。
在执行语句前,先用 Explain 查看执行计划,尽量命中索引,避免全表扫描。
-
尽量避免使用 select *,需要多少字段拿多少字段
-
非唯一索引尽量使用 limit
-
使用索引来代理 limit 处理分页
limit 会扫描前面不要的数据,然后逐一抛弃。在 Where 里面指定 ID 范围会更快。
业务层提供上一页和下一页的操作,避免用户一次跳多页。URL 要使用 after_xxx ,避免用户直接修改 page。例如 GitHub 的 release 列表界面。 -
用 Union 替代 OR
注:MySQL 的优化器会尝试使用索引合并来自动优化 OR。 -
当数据集不会重复时,用 Union All 替代 Union
-
联合索引最左匹配原则
-
联合索引在范围查询的字段后就不会再走索引了
-
删除由最左匹配原则覆盖的索引
-
使用 like 时,避免把 % 放前面
放在前面不走索引。 -
使用 Where 加更精确的条件限制来减少传输的数据量
以前见过判断用户登录用户名密码的时候,把整个用户表查出来再逐一判断的代码。 -
避免对索引列使用 MySQL 内置函数。
-
优先使用 Inner Join 而不是其他 Join。
-
如果使用 Left Join 或者 Right Join,驱动表数据量尽可能小。
-
避免在索引列上使用不等号。如果索引能用范围扫描,则使用范围操作符。
例如 a != 1,转化为 a < 1 AND a > 1。 -
大量数据使用批量分块插入数据
其中一个影响因素是锁。一个事务插入已知数量的多条数据,只需获取一次锁。 -
使用覆盖索引
使用索引就能获取想要的值,不需要从数据表中读。
用于辅助索引。
因为索引的执行顺序是:- 用辅助索引找到主键
- 通过主键索引找到数据
如果 select 的值只包括辅助索引和主键,则使用覆盖索引。
-
尽量不要在 select 字段多的时候使用 Distinct
-
批量删除数据要谨慎
- 分批操作。
- 如果全部数据删除,且不需要恢复,则使用 truncate 。
- 如果不是全部删除,则把保留的数据插入到新表,再整个删除旧表。
批量删除会加锁
批量删除过程要写 undo 日志,一旦回滚,需要更多时间 -
避免数据类型隐式转换
隐式转换会使索引失效