首页
关于
留言
接口
搜索
首页
登录
登录
搜索
KAKA 梦很美
累计撰写
47
篇文章
累计收到
0
条评论
首页
栏目
首页
登录
页面
首页
关于
留言
接口
自定义幻灯片
置顶
高并发的Redis缓存设计之高级篇
缓存设计可谓老生常谈了,早些时候都是采用memcache,现在大家更多倾向使用redis,除了知晓常用的数据存储类型,结合业务场景有针对性选择,好像其他也没有什么大的难点。 如果是几十、几百并发的业务场景,缓存设计可能并不需要考虑那么多,但如果是亿级的系统呢? 早期的缓存用于加速CPU数据交换的RAM。随着互联网的快速发展,缓存的应用更加宽泛,用于数据高速交换的存储介质都称之为缓存。 使用缓存时,我们要关注哪些指标?缓存有哪些应用模式?以及缓存设计时有哪些TIP技巧?了解缓存知识图谱, 一图胜千言。 一、缓存击穿 (缓存集中失效) 当业务系统查询数据时,首先会查询缓存,如果缓存中数据不存在,然后查询DB再将数据预热到Cache中,并返回。缓存的性能比 DB 高 50~100 倍以上。 很多业务场景,如:秒杀商品、微博热搜排行、或者一些活动数据,都是通过跑任务方式,将DB数据批量、集中预热到缓存中,缓存数据有着近乎相同的过期时间。 当过这批数据过期时,会一起过期,此时,对这批数据的所有请求,都会出现缓存失效,从而将压力转嫁到DB,DB的请求量激增,压力变大,响应开始变慢。 [ 解决方案 ] 从缓存的过期时间入口,将原来的固定过期时间,调整为 过期时间 = 基础时间 + 随机时间,让缓存慢慢过期,避免瞬间全部过期,对DB产生过大压力。 二、缓存穿透 不是所有的请求都能查到数据,不论是从缓存中还是DB中。 假如黑客攻击了一个论坛,用了一堆肉鸡访问一个不存的帖子id。按照常规思路,每次都会先查缓存,缓存中没有,接着又查DB,同样也没有,此时不会预热到Cache中,导致每次查询,都会cache miss。 由于DB的吞吐性能较差,会严重影响系统的性能,甚至影响正常用户的访问。 [ 解决方案 ] 方案一:查存DB 时,如果数据不存在,预热一个特殊空值到缓存中。这样,后续查询都会命中缓存,但是要对特殊值,解析处理。 方案二:构造一个BloomFilter过滤器,初始化全量数据,当接到请求时,在BloomFilter中判断这个key是否存在,如果不存在,直接返回即可,无需再查询缓存和DB。 三、缓存雪崩 缓存雪崩是指部分缓存节点不可用,进而导致整个缓存体系甚至服务系统不可用的情况。 分布式缓存设计一般选择一致性Hash,当有部分节点异常时,采用 rehash 策略,即把异常节点请求平均分散到其他缓存节点。但是,当较大的流量洪峰到来时,如果大流量 key 比较集中,正好在某 1~2 个缓存节点,很容易将这些缓存节点的内存、网卡过载,缓存节点异常 Crash,然后这些异常节点下线,这些大流量 key 请求又被 rehash 到其他缓存节点,进而导致其他缓存节点也被过载 Crash,缓存异常持续扩散,最终导致整个缓存体系异常,无法对外提供服务。 [ 解决方案 ] 方案一:增加实时监控,及时预警。通过机器替换、各种故障自动转移策略,快速恢复缓存对外的服务能力。 方案二:缓存增加多个副本,当缓存异常时,再读取其他缓存副本。为了保证副本的可用性,尽量将多个缓存副本部署在不同机架上,降低风险。 四、缓存热点 对于突发事件,大量用户同时去访问热点信息,这个突发热点信息所在的缓存节点就很容易出现过载和卡顿现象,甚至 Crash,我们称之为缓存热点。 这个在新浪微博经常遇到,某大V明星出轨、结婚、离婚,瞬间引发数百千万的吃瓜群众围观,访问同一个key,流量集中打在一个缓存节点机器,很容易打爆网卡、带宽、CPU的上限,最终导致缓存不可用。 [ 解决方案 ] 首先能先找到这个热key来,比如通过Spark实时流分析,及时发现新的热点key。 将集中化流量打散,避免一个缓存节点过载。由于只有一个key,我们可以在key的后面拼上有序编号,比如key#01、key#02。。。key#10多个副本,这些加工后的key位于多个缓存节点上。 每次请求时,客户端随机访问一个即可。 可以设计一个缓存服务治理管理后台,实时监控缓存的SLA,并打通分布式配置中心,对于一些hot key可以快速、动态扩容。 五、缓存大Key 当访问缓存时,如果key对应的value过大,读写、加载很容易超时,容易引发网络拥堵。另外缓存的字段较多时,每个字段的变更都会引发缓存数据的变更,频繁的读写,导致慢查询。如果大key过期被缓存淘汰失效,预热数据要花费较多的时间,也会导致慢查询。 所以我们在设计缓存的时候,要注意缓存的粒度,既不能过大,如果过大很容易导致网络拥堵;也不能过小,如果太小,查询频率会很高,每次请求都要查询多次。 [ 解决方案 ] 方案一:设置一个阈值,当value的长度超过阈值时,对内容启动压缩,降低kv的大小 评估大key所占的比例,由于很多框架采用池化技术,如:Memcache,可以预先分配大对象空间。真正业务请求时,直接拿来即用。 方案三:颗粒划分,将大key拆分为多个小key,独立维护,成本会降低不少。 方案四:大key要设置合理的过期时间,尽量不淘汰那些大key 六、缓存数据一致性 缓存是用来加速的,一般不会持久化储存。所以,一份数据通常会存在DB和缓存中,由此会带来一个问题,如何保证这两者的数据一致性。另外,缓存热点问题会引入多个副本备份,也可能会发生不一致现象。 [ 解决方案 ] 方案一:当缓存更新失败后,进行重试,如果重试失败,将失败的key写入MQ消息队列,通过异步任务补偿缓存,保证数据的一致性。 方案二:设置一个较短的过期时间,通过自修复的方式,在缓存过期后,缓存重新加载最新的数据。 七、数据并发竞争预热 互联网系统典型的特点就是流量大,一旦缓存中的数据过期、或因某些原因被删除等,导致缓存中的数据为空,大量的并发线程请求(查询同一个key)就会一起并发查询数据库,数据库的压力陡然增加。 如果请求量非常大,全部压在数据库,可能把数据库压垮,进而导致整个系统的服务不可用。 [ 解决方案 ] 方案一:引入一把全局锁,当缓存未命中时,先尝试获取全局锁,如果拿到锁,才有资格去查询DB,并将数据预热到缓存中。虽然,client端发起的请求非常多,但是由于拿不到锁,只能处于等待状态,当缓存中的数据预热成功后,再从缓存中获取 (这里面特别注意一个点,由于有一个并发时间差,所以会有一个二次check缓存是否有值的校验,防止缓存预热重复覆盖)。 方案二:缓存数据创建多个备份,当一个过期失效后,可以访问其他备份。 缓存设计时,有很多技巧,优化手段也是千变万化,但是我们要抓住核心要素。那就是,让访问尽量命中缓存,同时保持数据的一致性。
2021年-7月-17日
184 阅读
0 评论
Redis
2021-5-6
Swoole 模拟多服务器 Nginx 反向代理实现负载均衡
借用 PHP 的 Swoole 扩展根据不同的端口,启动多个服务器,然后使用 Nginx 反向代理。 如果没有安装 LNMP 开发环境或者没有安装 Swoole 扩展的话可以先查阅为您推荐的文章:CentOS7-4-干净环境配置及搭建LNMP-PHP7 编写服务程序 新建文件 swoole_server1.php <?php $serv = new swoole_http_server("0.0.0.0", 9501); $serv->on('Request', function($request, $response) { $response->cookie("User", "Swoole"); $response->header("X-Server", "Swoole"); $response->end("<meta charset='utf-8'/><h1>Hello Swoole!,你访问的是第一台机器9501</h1>"); }); $serv->start(); 然后复制3份文件 分别命名为: swoole_server2.php swoole_server3.php 和 swoole_server4.php 更改对应的端口和提示语 分别改为 9502、9503和9504 提示语改为 第二台机器 9502、第三台机器 9503 和 第四台机器 9504 配置Nginx反向代理 upstream webswoole { server 127.0.0.1:9501 weight=20; server 127.0.0.1:9502 weight=40; server 127.0.0.1:9503 weight=30; server 127.0.0.1:9504 weight=10; } server { listen 9500; server_name 127.0.0.1; location / { proxy_pass http://webswoole; } } 上面,我使用的是 9500 端口反向代理到四个不同的服务器端口。 启动服务 分别打开4个端口,使用命令 php swoole_server1.php php swoole_server2.php php swoole_server3.php php swoole_server4.php 重启nginx nginx -s reload 访问应用 (curl或打开浏览器网页) curl 127.0.0.1:9500 不断刷新会返回不同的服务器,为了看的更清楚,我写了一个测试 Shell 脚本 test.sh #!/bin/bash for ((i=1; i<=20; i++)) do curl 'http://127.0.0.1:9500/' echo '' sleep 1 done 然后执行脚本,我们看到访问的很均匀的出现了, 你也可以采用其他算法模式把流量分发到想要的服务器。 至此,Swoole 模拟负载均衡的测试完成, 如果是多台真实服务器的话直接替换 upstream 里的地址就可以了。
2021年-5月-6日
216 阅读
0 评论
PHP
2021-2-17
PHP 开发准则
命名规范 [强制] 代码文件必须以 <?php 或 <?= 标签开始 [强制] 代码文件必须以不带 BOM 的 UTF-8 编码 [强制] 代码中应该只定义类、函数、常量等声明,或其他会产生副作用的操作(如:生成文件输出以及修改 .ini 配置文件等),二者只能选其一 [强制] 命名空间以及类必须符合 PSR 的自动加载规范:PSR-4 中的一个 [强制] 类的命名必须遵循 StudlyCaps 大写开头的驼峰命名规范 [强制] 类中的常量所有字母都必须大写,单词间用下划线分隔 例如: CHANNEL, PLATFORM [强制] 类方法名称必须符合 camelCase 式的小写开头驼峰命名规范 [强制] 普通帮助函数名称必须全小写或小写、下划线、数组的命名规范 例如: get_client_ip, sort_array 编码规范 [强制] 控制结构的关键字后必须要有一个空格符,而调用方法或函数时则一定不可有 [强制] 每个 namespace 命名空间声明语句和 use 声明语句块后面,必须插入一个空白行 [强制] 类的开始花括号({)必须写在函数声明后自成一行,结束花括号(})也必须写在函数主体后自成一行 [强制] 函数方法的开始花括号({)必须写在函数声明后自成一行,结束花括号(})也必须写在函数主体后自成一行 [强制] 控制结构的开始花括号({)必须写在声明的同一行,而结束花括号(})必须写在主体后自成一行 [强制] 类的属性和方法必须添加访问修饰符(private、protected以及public),abstract以及final必须声明在访问修饰符之前,而static必须声明在访问修饰符之后 [强制] 所有关键字必须全部小写, 常量true、false和null必须全部小写 [强制] 关键词extends和implements必须写在类名称的同一行, <font color=#7255e6>[建议]</font>但当implements的继承列表有多个时, 则可以分成多行,每个继承接口名称都必须分开独立成行,包括第一个 [建议] 尽量避免 & 引用赋值的使用, 必要时可以使用 [建议] 方法内避免过度使用匿名函数, 不强制, 但需保持代码的强可读性 以下例子程序简单地展示了以上大部分编码规范: <?php namespace Vendor\Package; use FooInterface; use BarClass as Bar; use OtherVendor\OtherPackage\BazClass; use TIM; class Foo extends Bar implements FooInterface { use TIM; public function sampleFunction($a, $b = null) { if ($a === $b) { bar(); } elseif ($a > $b) { $foo->bar($arg1); } else { BazClass::bar($arg2, $arg3); } } final public static function bar() { // 方法的内容 } }
2021年-2月-17日
163 阅读
0 评论
规范准则
2021-1-10
缓存穿透、缓存击穿、缓存雪崩 解决方案
前言 设计一个缓存系统,就不得不要考虑:缓存穿透、缓存击穿与缓存雪崩。 缓存穿透 缓存穿透 是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是漏洞。 有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。 缓存击穿 对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,是一种非常 “热点” 的数据。这个时候,需要考虑一个问题:缓存被 “击穿” 的问题,这个和·缓存雪崩·的区别在于这里针对某一 key 缓存,前者则是很多 key。 缓存在某个时间点过期的时候,恰好在这个时间点对这个 Key 有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端 DB 压垮。 使用互斥锁 (mutex key) 业界比较常用的做法,使用 mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去 load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如 Redis 的 SETNX 或者 Memcache 的 ADD)去 set 一个 mutex key,当操作返回成功时,再进行 load db 的操作并回设缓存;否则,就重试整个 get 缓存的方法。 SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。 "提前" 使用互斥锁(mutex key) 在 value 内部设置1个超时值 (timeout1), timeout1 比实际的 memcache timeout (timeout2) 小。当从 cache 读取到 timeout1 发现它已经过期时候,马上延长 timeout1 并重新设置到 cache。然后再从数据库加载数据并设置到 cache 中。 "永远不过期" 这里的 “永远不过期” 包含两层意思: (1) 从 Redis 上看,确实没有设置过期时间,这就保证了,不会出现热点 key 过期问题,也就是“物理”不过期。 (2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在 key 对应的 value 里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期 从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。 资源保护 采用 netflix 的 hystrix,可以做资源的隔离保护主线程池,如果把这个应用到缓存的构建也未尝不可。 缓存雪崩 缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB,DB 瞬时压力过重雪崩。 缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。这里分享一个简单方案就是将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。 总结 针对业务系统,永远都是具体情况具体分析,没有最好,只有最合适。最后,对于缓存系统常见的缓存满了和数据丢失问题,需要根据具体业务分析,通常我们采用LRU策略处理溢出,Redis的RDB和AOF持久化策略来保证一定情况下的数据安全。
2021年-1月-10日
169 阅读
0 评论
基础原理
2020-12-22
MySQL 之执行计划
什么是执行计划 ? 执行计划,就是一条SQL语句,在数据库中实际执行的时候,一步步的分别都做了什么。也就是我们用EXPLAIN分析一条SQL语句时展示出来的那些信息。 EXPLAIN命令是查看查询优化器是如何决定执行查询的主要方法,从它的查询结果中可以知道一个SQL语句每一步是如何执行的,都经历了些什么,分为哪几步,有没有用到索引,哪些字段用到了什么样的索引,是否有一些可优化的地方等,这些信息都是我们SQL优化的依据。 要使用EXPLAIN,只需在查询中的SELECT关键字之前增加EXPLAIN。语法如下: EXPLAIN SELECT 查询语句 当执行执行计划时,只会返回执行计划中每一步的信息,它会返回一行或多行信息,显示出执行计划中的每一部分和执行的次序, 如: 如果查询的是多个关联表,执行计划结果可能是多行。 接下来涉及到的示例表,均来自于MySQL官方的示例数据库 sakila,点击脚本下载地址 执行计划中的列 EXPLAIN的结果总是有相同的列,每一列代表着不同的含义,可变的只是行数和内容。从上面的例子中,我们看到返回的有很多列,为了更加清楚的了解每一列的含义,便于我们更好的完成优化SQL。 列名 含义 id id列,表示查询中执行select子句或操作表的顺序 select_type 查询类型,主要是用于区分普通查询、联合查询、子查询等复杂的查询 table 表明对应行正在访问的是哪个表 partitions 查询涉及到的分区 type 访问类型,决定如何查找表中的行 possible_keys 查询可以使用哪些索引 key 实际使用的索引,如果为NULL,则没有使用索引 key_len 索引中使用的字节数,查询中使用的索引的长度(最大可能长度),并非实际使用长度,理论上长度越短越好 ref 显示索引的那一列被使用 rows 估算出找到所需行而要读取的行数 filtered 返回结果的行数占读取行数的百分比,值越大越好 Extra 额外信息,但又十分重要 id id列是一个编号,用于标识SELECT查询的序列号,表示执行SQL查询过程中SELECT子句或操作表的顺序。 如果在SQL中没有子查询或关联查询,那么id列都将显示一个1。否则,内层的SELECT语句一般会顺序编号。 id列分为三种情况 1) id 相同 如下普通查询,没有子查询 explain select f.* from film f,film_actor fa,actor a where f.film_id = fa.film_id and fa.actor_id = a.actor_id and a.first_name = 'NICK'; 2) id 不同 如果存在子查询,id的序号会递增,id值越大优先级越高,越先被执行 explain select * from film where film_id = (select film_id from film_actor where actor_id = 2 limit 1); 3) id相同又不同 1), 2) 两种情况同时存在。id如果相同,认为是一组,从上往下执行。在所有组中,id值越大,优先级越高,越先执行。 select_type select_type列表示对应行的查询类型,是简单查询还是复杂查询,主要用于区分普通查询、联合查询、子查询等复杂的查询。 该列有如下值: 值 说明 SIMPLE 简单查询,意味着不包括子查询或UNION PRIMARY 查询中包含任何复杂的子部分,最外层查询则被标记为PRIMARY SUBQUERY 在 select 或 where 列表中包含了子查询 DERIVED 表示包含在 from 子句的子查询中的 select,MySQL会递归执行并将结果放到一个临时表中,称其为“派生表”,因为该临时表是从子查询中派生而来的 UNION 第二个 select 出现在 UNION 之后,则被标记为UNION UNION RESULT 从UNION表获取结果的select table table列表示对应行正在执行的哪张表,指代对应表名,或者该表的别名(如果SQL中定义了别名)。 partitions 查询涉及到的分区。 type type列指代访问类型,是MySQL决定如何查找表中的行。 是SQL查询优化中一个很重要的指标,拥有很多值,依次从最优到最差: system < const < eq_ref < ref < fulltext < ref_or_null < index_merge < unique_subquery < index_subquery < range < index < ALL 1) ALL 众所周知的全表扫描,表示通过扫描整张表来找到匹配的行,很显然这样的方式查询速度很慢。 这种情况,性能最差,在写SQL时尽量避免此种情况的出现。 explain select * from film; 在平时写SQL时,避免使用select *,就不难理解了。换言之,是为了避免全表扫描,因为全面扫描是性能最差的。 2) index 全索引扫描,和全表扫描ALL类似,扫描表时按索引次序进行,而不是按行扫描,即:只遍历索引树。 index与ALL虽然都是读全表,但index是从索引中读取,而ALL是从硬盘读取。显然,index性能上优于ALL,合理的添加索引将有助于性能的提升。 explain select title from film; explain select description from film; 通过 explain 结果来看,只查询表film中字段title时,是按照索引扫描的(type列为index),倘若查询字段description,却是按照全表扫描的(type列为ALL)。这是为何呢? 接下来,我们不妨看看表film的结构 从 desc film 结果来看,字段title创建的有索引,而字段description没有,所以select title from film是按索引扫描,而select description from film按全表扫描。 从上面的举例对比中,也充分印证了索引的重要性。 3) range 只检索给定范围的行,使用一个索引来选择行。key列显示使用了那个索引。一般就是在where语句中出现了bettween、<、>、in等的查询。这种索引列上的范围扫描比全索引扫描index要好。 explain select * from film where film_id between 1 and 10; 4) ref 非唯一性索引扫描,返回匹配某个单独值的所有行。本质是也是一种索引访问,它返回所有匹配某个单独值的行,然而它可能会找到多个符合条件的行,所以它属于查找和扫描的混合体。 此类型只有当使用非唯一索引或者唯一索引的非唯一性前缀时,才会发生。 show index from film; explain select * from film where title = 'ACADEMY DINOSAUR'; 5) eq_ref 唯一索引扫描。常见于主键或唯一索引扫描。 6) const 通过索引一次就能找到,const用于比较primary key 或者unique索引。因为只需匹配一行数据,所有很快。如果将主键置于where列表中,MySQL就能将该查询转换为一个const。 show index from film; explain select * from film where film_id = 1; 7) system 表只有一行记录,这是const类型的特例,比较少见,如:系统表。 possible_keys 显示在查询中使用了哪些索引。 key 实际使用的索引,如果为NULL,则没有使用索引。查询中如果使用了覆盖索引,则该索引仅出现在key列中。 possible_keys列表明哪一个索引有助于更高效的查询,而key列表明实际优化采用了哪一个索引可以更加高效。 show index from film_actor; explain select actor_id,film_id from film_actor; key_len 表示索引中使用的字节数,查询中使用的索的长度(最大可能长度),并非实际使用长度,理论上长度越短越好。key_len是根据表定义计算而得的,不是通过表内检索出的。 ref 表示在key列记录的索引中查找值,所用的列或常量const。 rows 估算出找到所需行而要读取的行数 这个数字是内嵌循环关联计划里的循环数,它并不是最终从表中读取出来的行数,而是MySQL为了找到符合查询的那些行而必须读取行的平均数,只能作为一个相对数来进行衡量。 filtered 返回结果的行数占读取行数的百分比,值越大越好。 select count(1) from film_actor where actor_id = 1; explain select * from film_actor where actor_id = 1; 表film_actor中actor_id为1的记录有19条,而SQL查询时扫描了19行(rows:19),19条符合条件(filtered: 100 19/19) Extra 额外信息,但又十分重要。常见的值如下: 1) Using index 表示SQL中使用了覆盖索引。 show index from film_actor; explain select actor_id, film_id from film_actor; 2) Using where 许多where条件里是涉及索引中的列,当它读取索引时,就能被存储引擎检验,因此不是所有带where子句的查询都会显示“Using where”。 3) Using temporary 对查询结果排序时,使用了一个临时表,常见于order by 和group by。 4) Using filesort 对数据使用了一个外部的索引排序,而不是按照表内的索引进行排序读取。也就是说MySQL无法利用索引完成的排序操作成为“文件排序”。 总结 执行计划,真的很重要,尤其是SQL调优时,很香!
2020年-12月-22日
169 阅读
0 评论
MySQL
2020-4-17
CentOS 优化内存占满或使用过高
经常会遇到 CentOS 用着用着变慢了,free -m 查看内存使用情况,发现内存跑满了或过高。 2.输入 top ,按 shift+m ,按内存占用百分比排序,发现应用也没占80%多内存,那是什么占用的呢? 其实是CentOS为了提高效率,把部分使用过的文件缓存到了内存里。这样的话如果不需要这样的文件性能,可以直接释放来缓解内存压力。 3.优化方案 sync; echo 3 > /proc/sys/vm/drop_caches
2020年-4月-17日
164 阅读
0 评论
操作系统
2020-3-30
PHP 垃圾回收机制
机制介绍 PHP 使用了引用计数(reference counting)GC机制,同时使用根缓冲区机制,当php发现有存在循环引用的zval时,就会把其投入到根缓冲区,当根缓冲区达到配置文件中的指定数量后,就会进行垃圾回收,以此解决循环引用导致的内存泄漏问题。 循环引用造成的内存泄漏, 我们为了清理这些垃圾,引入了两个准则: 如果引用计数减少到零,所在变量容器将被清除(free),不属于垃圾 如果一个zval的引用计数减少后还大于0,那么它会进入垃圾周期。其次,在一个垃圾周期中,通过检查引用计数是否减1,并且检查哪些变量容器的引用次数是零,来发现哪部分是垃圾 每个对象都内含一个引用计数器 refcount,每个reference连接到对象,计数器加1。当reference离开生存空间或被设为 NULL,计数器减1。当某个对象的引用计数器为零时,PHP知道你将不再需要使用这个对象,释放其所占的内存空间。 GC处理完整流程图 机制概念 垃圾回收是一个多数编程语言中都带有的内存管理机制。与非托管性语言相反:C、 C++ 和 Objective C,用户需要手动收集内存,带有 GC 机制的语言:Java、 javaScript 和 PHP 可以自动管理内存。 垃圾回收机制(gc)顾名思义,就是废物重利用的意思,是一种动态存储分配的方案。它会自动释放程序不再需要的已分配的内存块。垃圾回收机制可以让程序员不必过分关心程序内存分配,从而将更多的精力投入到业务逻辑。 在现在的流行各种语言当中,垃圾回收机制是新一代语言所共有的特征,如Python、PHP、C#、Ruby 等都使用了垃圾回收机制。 回收原理 在PHP5.3版本之前,使用的垃圾回收机制是单纯的“引用计数”。 什么叫做引用计数? 由于PHP是用C来写的,C里面有一种东西叫做结构体,我们PHP的变量在C中就是用这种方式存储的。 每个PHP的变量都存在于一个叫做zval容器中,一个zval容器,除了包含变量名和值,还包括两个字节的额外信息: ● 一个叫做is_ref,是个布尔值,用来表示这个变量是否属于引用集合,通过这个字节,我们php才能把普通变量和引用变量区分开来。 ● 第二个额外字节就是refcount,用来表示指向这个容器的变量的个数。 怎么理解呢? 即: ① 每个内存对象都分配一个计数器,当内存对象被变量引用时,计数器+1 ② 当变量引用撤掉后(执行 unset 后),计数器-1 ③ 当计数器=0时,表明内存对象没有被使用,该内存对象则进行销毁,垃圾回收完成 并且PHP在一个生命周期结束后就会释放此进程/线程所占的内容,这种方式决定了PHP在前期不需要过多考虑内存的泄露问题。 但是当两个或多个对象互相引用形成环状后,内存对象的计数器则不会消减为0;这时候,这一组内存对象已经没用了,但是不能回收,从而导致内存泄露的现象。 php5.3开始,使用了新的垃圾回收机制,在引用计数基础上,实现了一种复杂的算法,来检测内存对象中引用环的存在,以避免内存泄露。 随着PHP的发展,PHP开发者的增加以及其所承载的业务范围的扩大,在PHP5.3中引入了更加完善的垃圾回收机制,新的垃圾回收机制解决了无法处理循环的引用内存泄漏问题。 官方文档所说,可以使用Xdebug来检查引用计数情况: <?php $a = "new string"; $c = $b = $a; xdebug_debug_zval( 'a' ); unset( $b, $c ); xdebug_debug_zval( 'a' ); // 以上例程会输出: /** * a: (refcount=3, is_ref=0)='new string' * a: (refcount=1, is_ref=0)='new string' **/ 注意:从PHP7的NTS版本开始,以上例子的引用将不再被计数,即$c=$b=$a之后a的引用计数也是,具体分类如下 在PHP7中,zval可以被引用计数或不被引用。在zval结构中有一个标志确定了这一点 ① 对于null,bool,int和double的类型变量,refcount 永远不会计数 ② 对于对象、资源类型,refcount计数和php5的一致 ③ 对于字符串,未被引用的变量被称为“实际字符串”。而那些被引用的字符串被重复删除(即只有一个带有特定内容的被插入的字符串)并保证在请求的整个持续时间内存在,所以不需要为它们使用引用计数;如果使用了opcache,这些字符串将存在于共享内存中,在这种情况下,您不能使用引用计数(因为我们的引用计数机制是非原子的) ④ 对于数组,未引用的变量被称为“不可变数组”。其数组本身计数与php5一致,但是数组里面的每个键值对的计数,则按前面三条的规则(即如果是字符串也不在计数);如果使用opcache,则代码中的常量数组文字将被转换为不可变数组 再次,这些生活在共享内存,因此不能使用 refcounting。 让我们看下例子来理解吧 <?php echo '测试字符串引用计数'; $a = "new string"; $b = $a; xdebug_debug_zval( 'a' ); unset( $b); xdebug_debug_zval( 'a' ); $b = &$a; xdebug_debug_zval( 'a' ); echo '测试数组引用计数'; $c = array('a','b'); xdebug_debug_zval( 'c' ); $d = $c; xdebug_debug_zval( 'c' ); $c[2]='c'; xdebug_debug_zval( 'c' ); echo '测试int型计数'; $e = 1; xdebug_debug_zval( 'e' ); 输出如下: 回收周期 默认的,PHP的垃圾回收机制是打开的,然后有个 php.ini 设置允许你修改它:zend.enable_gc 。 当垃圾回收机制打开时,算法会判断每当根缓存区存满时,就会执行循环查找。根缓存区有固定的大小,默认10,000,可以通过修改PHP源码文件 Zend/zend_gc.c 中的常量GC_ROOT_BUFFER_MAX_ENTRIES,然后重新编译PHP,来修改这个值。当垃圾回收机制关闭时,循环查找算法永不执行,然而,根将一直存在根缓冲区中,不管在配置中垃圾回收机制是否激活。 除了修改配置 zend.enable_gc ,也能通过分别调用 gc_enable() 和 gc_disable()函数在运行PHP时来打开和关闭垃圾回收机制。调用这些函数,与修改配置项来打开或关闭垃圾回收机制的效果是一样的。即使在可能根缓冲区还没满时,也能强制执行周期回收。你能调用 gc_collect_cycles() 函数达到这个目的。这个函数将返回使用这个算法回收的周期数。 允许打开和关闭垃圾回收机制并且允许自主的初始化的原因,是由于你的应用程序的某部分可能是高时效性的。在这种情况下,你可能不想使用垃圾回收机制。当然,对你的应用程序的某部分关闭垃圾回收机制,是在冒着可能内存泄漏的风险,因为一些可能根也许存不进有限的根缓冲区。 因此,就在你调用 gc_disable() 函数释放内存之前,先调用 gc_collect_cycles() 函数可能比较明智。因为这将清除已存放在根缓冲区中的所有可能根,然后在垃圾回收机制被关闭时,可留下空缓冲区以有更多空间存储可能根。 性能影响 内存占用空间的节省 首先,实现垃圾回收机制的整个原因是为了一旦先决条件满足,通过清理循环引用的变量来节省内存占用。在PHP执行中,一旦根缓冲区满了或者调用 gc_collect_cycles() 函数时,就会执行垃圾回收。 执行时间增加 垃圾回收影响性能的第二个领域是它释放已泄漏的内存耗费的时间。 通常,PHP中的垃圾回收机制,仅仅在循环回收算法确实运行时会有时间消耗上的增加。但是在平常的(更小的)脚本中根本就没有性能影响。 在平常脚本中有循环回收机制运行的情况下,内存的节省将允许更多这种脚本同时运行在你的服务器上。因为总共使用的内存没达到上限 这种好处在长时间运行脚本中尤其明显,诸如长时间的测试套件或者daemon脚本此类。同时,对通常比Web脚本运行时间长的脚本应用程序,新的垃圾回收机制,应该会大大改变一直以来认为内存泄漏问题难以解决的看法。 小结 unset : 只是断开一个变量到一块内存区域的连接,同时将该内存区域的引用计数减1,内存是否回收主要还是看refcount是否到0了 null : 将 null 赋值给一个变量是直接将该变量指向的数据结构置空,同时将其引用计数归0 脚本执行结束 : 该脚本中所有内存都会被释放,无论是否有环引用
2020年-3月-30日
171 阅读
0 评论
PHP
2020-2-11
MySQL InnoDB 数据页结构
数据页 在操作系统中,我们知道为了跟磁盘交互,内存也是分页的,一页大小4KB。同样的在 MySQL 中为了提高吞吐率,数据也是分页的,不过 MySQL 的数据页大小是16KB。(确切的说是 InnoDB 数据页大小16KB)。详细学习可以 参考官网 为了详细说明,这里先用图介绍一下页的结构: 而在MySQL内存中,多个这样的数据结构作为节点构成一个双向链表。 Page 结构 从上边的图也可以发现,它描述的是页外部的一些信息,比如上一页\下一页等。 我们重点可以看看 File Header 和 Page Header File Header Page Header 上图为 Page 数据结构,File Header 字段用于记录 Page 的头信息,其中比较重要的是 FIL_PAGE_PREV 和 FIL_PAGE_NEXT 字段,通过这两个字段,我们可以找到该页的上一页和下一页,实际上所有页通过两个字段可以形成一条双向链表。Page Header 字段用于记录 Page 的状态信息。接下来的 Infimum 和 Supremum 是两个伪行记录,Infimum(下确界)记录比该页中任何主键值都要小的值,Supremum (上确界)记录比该页中任何主键值都要大的值,这个伪记录分别构成了页中记录的边界。 User Records 中存放的是实际的数据行记录,具体的行记录结构将在本文的第二节中详细介绍。Free Space 中存放的是空闲空间,被删除的行记录会被记录成空闲空间。Page Directory 记录着与二叉查找相关的信息。File Trailer 存储用于检测数据完整性的校验和等数据。 行记录 Innodb 存储引擎提供了两种格式的行记录:Compact 和 Redundant。 Compact 行记录 变长字段长度列表:逆序记录每一个列的长度,如果列的长度小于 255 字节,则使用一个字节,否则使用 2 个字节。该字段的实际长度取决于列数和每一列的长度,因此是变长的。 NULL 标志位:一个字节,表示该行是否有 NULL 值 记录头信息:五个字节,其中 next_record 记录了下一条记录的相对位置,一个页中的所有记录使用这个字段形成了一条单链表。 列数据部分:除了记录每一列对应的数据外,还有隐藏列,它们分别是 Transaction ID、Roll Pointer 以及 row_id(当没有指定主键)。 注意:此处需要注意固定长度 CHAR 数据类型和变长 VCHAR 数据类型在 Compact 记录下为 NULL 时不占用任何存储空间。 Redundant 行记录 字段长度偏移列表:与 Compact 中的变长字段长度列表相同的是它们都是按照列的逆序顺序设置值的,不同的是字段长度偏移列表记录的是偏移量,每一次都需要加上上一次的偏移,同时对于 CHAR 的 NULL 值,会直接按照最大空间记录,而对于 VCHAR 的 NULL 值不占用任何存储空间。 此处需要注意 VCHAR 类型和 CHAR 类型在建表时传入的参数是字符长度而不是字节长度,实际的字节长度需要跟编码方式相关联,例如 UTF-8 一个中文字符需要 3 字节来表示,这样 CHAR(10) 以 UTF-8 来表示的话,它的字节长度在 10 - 30 之间。 行溢出 我们知道数据页的大小是 16KB,Innodb 存储引擎保证了每一页至少有两条记录,如果一页当中的记录过大,会截取前 768 个字节存入页中,其余的放入 BLOB Page。
2020年-2月-11日
159 阅读
0 评论
MySQL
2020-1-11
MySQL 开发准则
命名规范 [强制] 字段名称必须用小写或者小写、下划线、数字组成 例如: name, user_name [强制] 字段名称禁止使用 MySQL 保留关键字 例如: order, sum 等 [强制] 字段名称要见名知其意,不要超过 32 个字符 例如: nickname, created_at [强制] 临时表要以 tmp 为前缀,日期为后缀 例如: tmp_export_user_20200307 [强制] 备份表要以 bak 为前缀,日期为后缀 例如: bak_user_20200307 [强制] 表名不使用复数名词 例如: users [强制] 表字段名称需要表示是否概念时,用 is_xxx 表示 例如: is_default, is_use [强制] 索引名称,用特定 _ 字段表示 普通索引 idx_xxx 联合索引 un_xxx_xxx 唯一索引 uk_xxx [强制] 时间字段名称用特定 _at 字段表示, 推荐 DATETIME 类型 例如: created_at, updated_at, deleted_at 表设计规范 [强制] 如无特殊需求,存储引擎使用 InnoDB 支持事务 行级锁 并发性能好 [强制] 数据库和表的字符集统一使用 utf8 或 utf8mb4, 无特殊要求优先使用 utf8mb4 不同字符集转化可能会产生乱码 不同字符集比较前会进行字符转换,索引失效 UTF8 每个字符占用3字节,占用空间小,但是不能存储 emoj,emoj 占用4字节 UTF8MB4 每个字符占用4字节,是真正的 UTF8,推荐使用 <font color=#7255e6>[建议]</font> InnoDB 字符集默认排序使用 _general_ci 和 _unicode_ci,推荐使用 _general_ci [强制] 每张表都必须建立自增ID字段,不要问为什么,看看B+树原理你就懂了 [强制] 数据库表和字段都需要添加备注,更好理解建表思路 [强制] 不要使用触发器, 可以用事务替代 [强制] 数据量大的表要使用pt(pt-online-schema-change)工具修改表结构 原理是新建一张表并复制原表结构与数据,最终删除原表,可以有效避免行锁及表锁 字段设计规范 [强制] 表字段名称需要表示是否概念时,即 is_xxx, 1 表示是, 0 表示否, 使用 UNSIGNED TINYINT。 [强制] 小数类型都使用 DECIMAL 型 (DECIMAL 精确, 如果超出 DECIMAL 范围建议分两个字段存储) [强制] 固定长度字段用 CHAR, 根据实际情况 例如: 定长的手机号码 [强制] 选择合适的存储长度 可以减少表存储空间 可以减少索引长度,增加索引效率 <font color=#7255e6>[建议]</font> 避免使用TEXT、BLOB数据类型 内存临时表不支持TEXT和BLOB。会使用磁盘临时表,降低查询速度 TEXT和BLOB需要单独成表,提高查询效率 <font color=#7255e6>[建议]</font> 避免使用 ENUM 数据类型 枚举类型 ORDER BY 效率低 禁止使用数字作为枚举值 (在与PHP使用上1和’1’差别大,PHP是弱引用很容易把’1’写为1,1为key,’1’为内容) <font color=#7255e6>[建议]</font> 尽可能把列定义设置为 NOT NULL 索引NULL列,会额外增加开销,占用更多表空间 要做计算或者比较时,会对 NULL 做特别处理 在 SQL 中对 NULL 进行判断会全表扫描 索引设计规范 [强制] 不要使用外键和级联,应放在应用层去做 [强制] 一张表不要超过5个索引 索引过多会降低性能 合理分配索引会提高性能 [强制] 联合索引的最左前缀原则可以减少每个字段单独建立索引 避免每个字段都建立索引 联合索引区分度高的放在最左边 联合索引如果存在非等号和等号混合时,把等号的索引放在最左边 联合索引最左前缀原则,一定要注意顺序 [强制] InnoDB 必须有主键ID <font color=#7255e6>[建议]</font> 查询想走特定索引时可以用 FORCE INDEX MySQL的 optimizer 会执行它认为最优索引,但是往往不是我们需要或者最优的 使用 FORCE INDEX 可以强制使用索引,结合 EXPALIN 使用,确认为最优 [强制] 有唯一索引需求,该字段就应设置唯一索引 即使该字段是在联合索引内,也要单独设置唯一索引 唯一索引对 INSERT 速度影响可以忽略,但是提高查询速度和唯一性是明显的 应用层也建议做校验控制,但是根据墨菲定律,只要有可能就会出现脏数据 [强制] VARCHAR 型设置索引要设置索引长度 不设置默认是全部长度 建议索引长度为20, 区分度可以达到90% 区分度计算公式:SELECT COUNT(DISTINCT left(列名, 索引长度))/COUNT(*) FROM 表名。可以查出区分度百分比。 [强制] 模糊查询最好用搜索引擎 大表禁止使用 LIKE %str 和 LIKE %str%, 因为不走索引 可以使用 LIKE str%, 走索引 [强制] ORDER BY 需要注意索引的有序性 ORDER BY 后接索引的部分,如果是联合索引,应该是联合索引的最后,避免出现 file_sort,影响查询性能。WHERE a=? AND b=? ORDER BY c 那么索引是(a,b,c) file_sort 出现是没有走索引或者联合索引。出现情况:WHERE a=? ORDER BY b索引是a。改进优化:WHERE a=? ORDER BY b索引是(a,b) [强制] 避免冗余索引 重复索引:PRIMARY KEY(id)、INDEX(id)、UNIQUE INDEX(id) 冗余索引:KEY(a,b,c)、KEY(a,b)、KEY(a) [强制] 查询频率较高的SQL语句,应该使用覆盖索引 覆盖索引不是真正的索引,是一种使用索引方式 (原理是从索引中查询出想要内容,而不用回表查询,提高查询效率, 表现是 EXPLAIN 的 Extra 为 Using index) 例如 SELECT user_no FROM user WHERE user_age = 28索引为user_no时效率低,索引为(user_no,user_age)时为覆盖索引,查询效率高 [强制] 避免隐式类型转换 定义和使用不同数据会造成隐式转换 (隐式转换会不走索引,降低查询效率) 例如 user_no为INT, SELECT user_age FROM user WHERE user_no='111' [强制] 避免在字段位置写表达式,不走索引 反例:SELECT user_no FROM user WHERE user_age*2 = 36 正例:SELECT user_no FROM user WHERE user_age = 36/2 查询优化 [强制] SQL性能优化目标,由高到低 const 基本是只有一行匹配 ref 基本是走普通索引 range 基本是走范围索引 index 走索引最差,和全表查询相似 NULL 不走索引,全表查询 [强制] 不适用索引的几种情况 不等式:!=、<> NULL 判断:IS NULL、IS NOT NULL LIKE 模糊查询:LIKE %a、LIKE %a% NOT IN <font color=#7255e6>[建议]</font> 避免使用IN操作,如果避免不了,需小于1000条 多表查询IN会影响查询效率 可以用 BETWEEN 替代 IN(SELECT * FROM)索引会失效,可以使用JOIN(LEFT、RIGHT、INNER、FULL)来实现 <font color=#7255e6>[建议]</font> JOIN 优化 最好在三张表之内,最多不要超过5张表 ON 关联字段类型要相同 每关联一个表就会多分配一个关联缓存,和 join_buffer_size 设置相关。占用内存过大会形成溢出,影响性能和稳定性 LEFT JOIN 的驱动表是左侧表 INNER JOIN 的驱动表是数据少的表 RIGHT JOIN 的驱动表是右侧表 MySQL没有 FULL JOIN,可以用SQL实现。例如:SELECT FROM A LEFT JOIN B ON B.name = A.name WHERE B.name IS NULL UNION ALL SELECT FROM B 尽量利用小表驱动大表,可以减少循环嵌套次数 STRAIGHT_JOIN 的使用。前提是INNER JOIN内连接。INNER JOIN优先查询小表,但有GROUP BY、ORDER BY等file_sort, Using temporary时会想改变优先查询表顺序,这时可以使用STRAIGHT_JOIN。 STRAIGHT_JOIN强制优先查询表为左侧表 (一定要是内连接才能使用 STRAIGHT_JOIN,否则数据可能不准确) <font color=#7255e6>[建议]</font> *避免 SELECT 出现, 特别是多字段大表** SELECT * 增加额外解析成本 增减字段对前端映射不一致 无用字段增加网络消耗 无法使用覆盖索引 [强制] 禁止使用不带字段的 INSERT 出现 正例:INSERT INTO user(user_no,user_age) VALUES (123,18) 反例:INSERT INTO user VALUES (123,18) <font color=#7255e6>[建议]</font> 尽量避免子查询 子查询一般在IN中 子查询会创建临时表,不会存在索引 结果集大的子查询,性能越差 可以使用JOIN替代 [强制] 查询一条或者是否有数据时,要使用 LIMIT 1 索引效率最高 EXPLAIN 的 type 为 const <font color=#7255e6>[建议]</font> ORDER BY 字段没有索引就不要排序 ORDER BY 字段有索引会按索引排序。没有索引影响效率 可以设置索引,或者覆盖索引 <font color=#7255e6>[建议]</font> 尽量不使用 OR 同一字段用IN、BETWEEN等替代OR,因为很多情况不会走索引 多字段下OR两边都需要是索引且其他条件也是索引,才会走索引 最好使用UNION、UNION ALL来替换 <font color=#7255e6>[建议]</font> 尽量用 UNION ALL 替代 UNION UNION 会集合后进行唯一性去重,涉及到排序,加大资源开销 在没有重复数据情况强制使用 UNION ALL <font color=#7255e6>[建议]</font> 拆分大且复杂的SQL 一条SQL只会使用一个CPU 拆成多个小SQL可以通过并行提高查询效率 [强制] 禁止使用 ORDER BY RAND() 随机排序性能差 (可以用其他SQL替换) 原SQL: SELECT id FROM 'dynamic' ORDER BY RAND() LIMIT 1; 新SQL: SELECT id FROM 'dynamic' t1 JOIN (SELECT RAND() * (SELECT MAX(id) FROM 'dynamic') AS nid) t2 ON t1.id > t2.nid LIMIT 1。 注意, 此查询只能随机一条id,并连续查询该id的顺序条数,具体情况具体分析,适用随机取一条,不应用随机取多条, 随机取多条解决方案:先查询所有id->在后端业务层做随机id->IN该id组, rand()取值范围:(0, 1) [强制] 禁止对 WHERE 条件字段进行函数转换(不走索引) 正例:SELECT user_age FROM user WHERE created_at > '20200307' 反例:SELECT user_age FROM user WHERE DATE(created_at) > '20200307' <font color=#7255e6>[建议]</font> IN、EXISTS、NOT IN、NOT EXISTS IN是子查询,优先查询驱动表为内表,所以适合内表数据小的情况 EXISTS优先查询驱动表为外表,适合外表数据小的情况 不建议使用NOT IN和NOT EXISTS,不走索引且容易混淆(建议用其他SQL替代) 反例:SELECT a.user_age FROM user a WHERE a.user_no NOT IN (SELECT b.user_no FROM user_info b)。 正例:SELECT a.user_age FROM user a LEFT JOIN user_info b ON a.user_no = b.user_no WHERE b.user_no IS NULL。 <font color=#7255e6>[建议]</font> OFFSET 偏移量、分页 分页数据量大的情况会影响查询效率,因为不是跳过OFFSET行,而是查询OFFSET+N行,然后抛弃OFFSET行 优化举例1:SELECT user_age FROM user WHERE user_no > 13333 LIMIT 20。 优化举例2:SELECT a.user_age FROM user a,(SELECT user_no FROM user LIMIT 13333,20)b WHERE a.user_no = b.user_no。 [强制] 范围查询注意 BETWEEN、>、< 查询时,如果是走联合索引,那么范围查询后的索引失效 [强制] COUNT() 相关 统计行数要使用COUNT(*),不要使用COUNT(列名) COUNT(*)会统计NULL数据,COUNT(列名)不会统计NULL数据 当某一列的值全为NULL时,COUNT(列名)返回的结果为0,但SUM(列名)结果为NULL,因此使用SUM(列名)需要使用IFNULL判断 例如:SELECT IF(IFNULL(SUM(user_name)),0,SUM(user_name))user_age FROM user
2020年-1月-11日
167 阅读
0 评论
规范准则
2019-9-3
解决GitHub慢的若干问题
今天使用 go get github.com/beego/bee 命令时,发现很慢而且还出现超时现象导致 Package 下载不完整,于是感觉应该是访问 github.com 慢的问题。解决方案: 使用IP查找工具找到对应的IP地址 IP查找工具 1)查找 github.com 对应的 IP 2)查找 github.global.ssl.fastly.net 对应的 IP 将IP写入到host 192.30.253.112 github.com 151.101.185.194 github.global.ssl.fastly.net 享受吧 再次执行 go get github.com/beego/bee 已经完美的下载好了,速度很快。
2019年-9月-3日
174 阅读
0 评论
操作系统
2
3
4
5