拥之则安

拥之则安 查看完整档案

太原编辑  |  填写毕业院校孟令鑫  |  php 编辑填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

拥之则安 发布了文章 · 2020-10-20

Mycat数据库中间件

1.非分片字段查询

Mycat中的路由结果是通过分片字段分片方法来确定的。例如下图中的一个Mycat分库方案:

  • 根据 tt_waybill 表的 id 字段来进行分片
  • 分片方法为 id 值取 3 的模,根据模值确定在DB1,DB2,DB3中的某个分片

非分片字段查询

如果查询条件中有 id 字段的情况还好,查询将会落到某个具体的分片。例如:

mysql>select * from tt_waybill where id = 12330;

此时Mycat会计算路由结果

12330 % 3 = 0 –> DB1

并将该请求路由到DB1上去执行。
如果查询条件中没有 分片字段 条件,例如:

mysql>select * from tt_waybill where waybill_no =88661;

此时Mycat无法计算路由,便发送到所有节点上执行:

DB1 –> select * from tt_waybill where waybill_no =88661;
DB2 –> select * from tt_waybill where waybill_no =88661;
DB3 –> select * from tt_waybill where waybill_no =88661;

如果该分片字段选择度高,也是业务常用的查询维度,一般只有一个或极少数个DB节点命中(返回结果集)。示例中只有3个DB节点,而实际应用中的DB节点数远超过这个,假如有50个,那么前端的一个查询,落到MySQL数据库上则变成50个查询,会极大消耗Mycat和MySQL数据库资源。

如果设计使用Mycat时有非分片字段查询,请考虑放弃!

2.分页排序

先看一下Mycat是如何处理分页操作的,假如有如下Mycat分库方案:
一张表有30份数据分布在3个分片DB上,具体数据分布如下

DB1:[0,1,2,3,4,10,11,12,13,14]
DB2:[5,6,7,8,9,16,17,18,19]
DB3:[20,21,22,23,24,25,26,27,28,29]

(这个示例的场景中没有查询条件,所以都是全分片查询,也就没有假定该表的分片字段和分片方法)

分页

当应用执行如下分页查询时

mysql>select * from table limit 2;

Mycat将该SQL请求分发到各个DB节点去执行,并接收各个DB节点的返回结果

DB1: [0,1]
DB2: [5,6]
DB3: [20,21]

但Mycat向应用返回的结果集取决于哪个DB节点最先返回结果给Mycat。如果Mycat最先收到DB1节点的结果集,那么Mycat返回给应用端的结果集为 [0,1],如果Mycat最先收到DB2节点的结果集,那么返回给应用端的结果集为 [5,6]。也就是说,相同情况下,同一个SQL,在Mycat上执行时会有不同的返回结果。

在Mycat中执行分页操作时必须显示加上排序条件才能保证结果的正确性,下面看一下Mycat对排序分页的处理逻辑。
假如在前面的分页查询中加上了排序条件(假如表数据的列名为id

mysql>select * from table order by id limit 2;

Mycat的处理逻辑如下图:

排序分页

在有排序呢条件的情况下,Mycat接收到各个DB节点的返回结果后,对其进行最小堆运算,计算出所有结果集中最小的两条记录 [0,1] 返回给应用。

但是,当排序分页中有 偏移量 (offset)时,处理逻辑又有不同。假如应用的查询SQL如下:

mysql>select * from table order by id limit 5,2;

如果按照上述排序分页逻辑来处理,那么处理结果如下图:

排序偏移分页

Mycat将各个DB节点返回的数据 [10,11], [16,17], [20,21] 经过最小堆计算后返回给应用的结果集是 [10,11]。可是,对于应用而言,该表的所有数据明明是 0-29 这30个数据的集合,limit 5,2 操作返回的结果集应该是 [5,6],如果返回 [10,11] 则是错误的处理逻辑。

所以Mycat在处理 有偏移量的排序分页 时是另外一套逻辑——改写SQL 。如下图:

正确排序偏移分页

Mycat在下发有 limit m,n 的SQL语句时会对其进行改写,改写成 limit 0, m+n 来保证查询结果的逻辑正确性。所以,Mycat发送到后端DB上的SQL语句是

mysql>select * from table order by id limit 0,7;

各个DB返回给Mycat的结果集是

DB1: [0,1,2,3,4,10,11]
DB2: [5,6,7,8,9,16,17]
DB3: [20,21,22,23,24,25,26]

经过最小堆计算后得到最小序列 [0,1,2,3,4,5,6] ,然后返回偏移量为5的两个结果为 [5,6]

虽然Mycat返回了正确的结果,但是仔细推敲发现这类操作的处理逻辑是及其消耗(浪费)资源的。应用需要的结果集为2条,Mycat中需要处理的结果数为21条。也就是说,对于有 t 个DB节点的全分片 limit m, n 操作,Mycat需要处理的数据量为 (m+n)*t 个。比如实际应用中有50个DB节点,要执行limit 1000,10操作,则Mycat处理的数据量为 50500 条,返回结果集为10,当偏移量更大时,内存和CPU资源的消耗则是数十倍增加。

如果设计使用Mycat时有分页排序,请考虑放弃!

3.任意表JOIN

先看一下在单库中JOIN中的场景。假设在某单库中有 playerteam 两张表,player 表中的 team_id 字段与 team 表中的 id 字段相关联。操作场景如下图:

单个DB中JOIN

JOIN操作的SQL如下

mysql>select p_name,t_name from player p, team t where p.no = 3 and p.team_id = t.id;

此时能查询出结果

p_name

t_name

Wade

Heat

如果将这两个表的数据分库后,相关联的数据可能分布在不同的DB节点上,如下图:

分库JOIN

这个SQL在各个单独的分片DB中都查不出结果,也就是说Mycat不能查询出正确的结果集。

设计使用Mycat时如果要进行表JOIN操作,要确保两个表的关联字段具有相同的数据分布,否则请考虑放弃!

4.分布式事务

Mycat并没有根据二阶段提交协议实现 XA事务,而是只保证 prepare 阶段数据一致性的 弱XA事务 ,实现过程如下:

应用开启事务后Mycat标识该连接为非自动提交,比如前端执行

mysql>begin;

Mycat不会立即把命令发送到DB节点上,等后续下发SQL时,Mycat从连接池获取非自动提交的连接去执行。

弱XA事务1

Mycat会等待各个节点的返回结果,如果都执行成功,Mycat给该连接标识为 Prepare Ready 状态,如果有一个节点执行失败,则标识为 Rollback 状态。

弱XA事务2

执行完成后Mycat等待前端发送 commitrollback 命令。发送 commit 命令时,Mycat检测当前连接是否为 Prepare Ready 状态,若是,则将 commit 命令发送到各个DB节点。

弱XA事务3

但是,这一阶段是无法保证一致性的,如果一个DB节点在 commit 时故障,而其他DB节点 commit 成功,Mycat会一直等待故障DB节点返回结果。Mycat只有收到所有DB节点的成功执行结果才会向前端返回 执行成功 的包,此时Mycat只能一直 waiting 直至_TIMEOUT_,导致事务一致性被破坏。

设计使用Mycat时如果有分布式事务,得先看是否得保证事务得强一致性,否则请考虑放弃!

查看原文

赞 0 收藏 0 评论 0

拥之则安 赞了文章 · 2019-09-26

PHP 进阶之路 - 亿级 pv 网站架构实战之性能压榨

本博客并非全部原创,其实是一个知识的归纳和汇总,里面我引用了很多网上、书上的内容。也给出了相关的链接。

本文涉及的知识点比较多,大家可以根据关键字去搜索相关的内容和购买相应的书籍进行系统的学习。不对的地方大家予以批评指正。

有人给我留言说,亿级 PV 就别写文章了,随便用几个开源软件就能搞定了,只要不犯什么大错。我不以为然,如果你利用了相同的思想,使用了更高性能的基础服务,也许就能支持更多的流量并发,节约更多的服务器,优化的思路才是重点。

本内容的视频分享见我的直播

性能优化的原则

  1. 性能优化是建立在对业务的理解之上的
  2. 性能优化与架构、业务相辅相成、密不可分的

性能优化的引入

我们先看一张简单的 web 架构图

亿级 pv 网站架构实战之性能压榨.002.jpeg

从上到下从用户的浏览器到最后的数据库,那么我们说先前端的优化。

前端优化

雅虎军规:http://www.cnblogs.com/paul-3...
亿级 pv 网站架构实战之性能压榨.004.jpeg

减少 http 请求数

  1. 图片、css、script等等这些都会增加http请求数,减少这些元素的数量就能减少响应时间。

把多个JS、CSS在可能的情况下写进一个文件,页面里直接写入图片也是不好的做法,应该写进CSS里,小图拼合后利用 background 来定位。

  1. 现在很多 icon 都是直接做成字体,矢量高清,也减少网络请求数
  2. 现在的前端框架都会通过组件的方式开发,最后打包生成一个 js 或者 两个 js 文件 + 一个 css 或者两个 css 文件。

利用浏览器缓存

expires,cache-control,last-modified,etag
http://blog.csdn.net/eroswang...
防止缓存,比如资源更新了,原来的做法是?v=xxxx 现在前端的打包工作可以能会生成 /v1.2.0/xxx.js

使用分布式存储前端资源

接地气利用 cdn 存储前端资源

多域名访问资源

  • 原因一:浏览器对同一域名的并行请求数有上限,多个域名则支持更多并行请求
  • 原因二:使用同一域名的时候无用的 cookie 简直是噩梦

亿级 pv 网站架构实战之性能压榨.012.jpeg

数据压缩

  1. 开启gzip
  2. 前端资源本身的压缩,js/css 打包编译(去掉空格,语意简化)图片资源的压缩等。

优化首屏展示速度

  1. 资源的按需加载,延时加载 https://mengkang.net/229.html
  2. 图片的懒加载,淘宝的商品介绍太多图,用户点击进来又有多少人一直往下看图的呢?

nginx 优化

图片描述
分为下面三个部分来

nginx 本身配置的优化

  1. worker_processes auto 设置多少子进程
  2. worker_cpu_affinity 亲缘性绑定
  3. worker_rlimit_nofile 65535 worker 进程打开的文件描述符的最大数
  4. worker_connections 65535 子进程最多处理的连接数
  5. epoll 多路复用
  6. sendfile on 是对文件I/O的系统调用的一个优化,系统api
  7. 如果是反向代理web服务器,需要配置fastcgi相关的参数
  8. 数据返回开启gzip压缩
  9. 静态资源使用 http 缓存协议
  10. 开启长连接 keepalive_timeout
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;
        fastcgi_buffer_size 64k;
        fastcgi_buffers 4 64k;
        fastcgi_busy_buffers_size 128k;
        fastcgi_temp_file_write_size 256k;
        gzip on;
        gzip_min_length  1k;
        gzip_buffers     4 16k;
        gzip_http_version 1.0;
        gzip_comp_level 2;
        gzip_types       text/plain application/x-javascript text/css application/xml text/javascript application/json;
        gzip_vary on;
        gzip_proxied        expired no-cache no-store private auth;
        gzip_disable        "MSIE [1-6]\.";
        location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
            {
                expires      30d;
            }    

http://mailman.nginx.org/pipe...

tcp/ip 网络协议配置的优化

  1. /proc/sys/net/ipv4/tcp_tw_recycle 1 开启TCP连接中TIME-WAIT sockets的快速回收,保证tcp_timestamps = 1
  2. /proc/sys/net/ipv4/tcp_tw_reuse 1 允许将TIME-WAIT sockets重新用于新的TCP连接 https://mengkang.net/564.html
  3. /proc/sys/net/ipv4/tcp_syncookies 0 是否需要关闭洪水抵御 看自己业务,比如秒杀,肯定需要关闭了
  4. /proc/sys/net/ipv4/tcp_max_tw_buckets 180000 否则经常出现 time wait bucket table overflow
  5. tcp_nodelay on 小文件快速返回,我之前通过网络挂载磁盘出现找不到的情况
  6. tcp_nopush on
tcp_tw_recycle 快速回收可能导致丢包的问题 https://mengkang.net/1144.html

linux 系统的优化

除了上面的网络协议配置也是在系统基础之外,为了配合nginx自己里面的设定需要做如下修改

  1. /proc/sys/net/core/somaxconn 65535
  2. ulimit -a 65535

更多详细的优化配置说明:http://os.51cto.com/art/20140...

php 优化

升级到 php7

注意有很多函数和扩展被废弃,比如mysql相关的,有风险,做好测试再切换。

opcode 缓存

图片描述
php 5.5 之后好像就内置了吧,需要在php.ini里添加如下配置

opcache.revalidate_freq=60
opcache.validate_timestamps=1
opcache.max_accelerated_files=1000
opcache.memory_consumption=512
opcache.interned_strings_buffer=16
opcache.fast_shutdown=1
  1. opcache.revalidate_freq

这个选项用于设置缓存的过期时间(单位是秒),当这个时间达到后,opcache会检查你的代码是否改变,如果改变了PHP会重新编译它,生成新的opcode,并且更新缓存。

  1. opcache.validate_timestamps

当这个选项被启用(设置为1),PHP会在opcache.revalidate_freq设置的时间到达后检测文件的时间戳(timestamp)。

  1. opcache.max_accelerated_files

这个选项用于控制内存中最多可以缓存多少个PHP文件。

  1. opcache.memory_consumption

你可以通过调用opcachegetstatus()来获取opcache使用的内存的总量

  1. opcache.interned_strings_buffer

字符串opcache的复用,单位为MB

  1. opcache.fast_shutdown=1

开启快速停止续发事件,依赖于Zend引擎的内存管理模块

php7 hugepage 的使用

Hugepage 的作用:间接提高虚拟地址和内存地址转换过程中查表的TLB缓存命中率

opcache.huge_code_pages=1

鸟哥博客详细介绍:http://www.laruence.com/2015/...

代码伪编译

以thinkphp为例,它会把框架基础组件(必须用到的组件)合并压缩到一个文件中,不仅减少了文件目录查找,文件打开的系统调用。
图片描述

clipboard.png

通过stracephp-fpm子进程,可以清楚系统调用的过程,在我上面例子中有打开一个文件有12次系统调用(只是举例,我这里相对路径设置的原因导致多了两次文件查找)。如果有10个文件,那就是120次,优化的效果可能不是那么明显,但是这是一种思路。
顺便说下 set_include_path能不用就不要用,上面的demo的截图里面找不到目录就是证明。

模板编译

图片描述
模板把它们自定义的语法,最后转换成php语法,这样方便解析。而不是每次都解析一遍。

xhprof 查找性能瓶颈

我的截图一直上传不成功,正好社区有这样的博客,推荐下 https://segmentfault.com/a/11...

业务优化

非侵入式扩展开发

比如原来有一个model,叫问答,现在需要开发一个有奖问答,需要支持话题打赏,里面多了很多功能。这个时候应该利用面向对象的继承的特性。而不是做下面的开发

<?php
class AskModel {
    public function detail($id){
        $info = 从数据库查询到该问题的信息;
        // 逻辑1
        
        // 逻辑2
        
    }
}
<?php
class AskModel {
    public function detail($id){
        $info = 从数据库查询到该问题的信息;
        // 逻辑1
        if($info['type'] == 2){
            //...
        }else{
            
        }
        
        // 逻辑2
        if($info['type'] == 2){
            //...
        }else{
            
        }
    }
}

这样逻辑多了,子类型多了,逻辑判断就非常重复,程序运行起来低效可能是一方面,更多的是不可维护性。

业务和架构不分家,架构是建立在对业务的理解之上的。再放下上次直播的PPT (sf故障无法传图,等会补吧)

异步思想

举例:

  1. 处理邮件发送。
  2. gearman 图片裁剪。
  3. 页面上 ajax 加载动态数据。
  4. 图片的懒加载,双击图片看大图。
  5. sf 上通过websocket 通知你有新的消息,但是并没有告诉你有什消息,点击消息图标才会去异步请求具体的消息。

这些都是异步的思想。能分步走就分步走,能不能请求的就不请求。

静态化

专题页面,比如秒杀页面,为了应对更大的流量、并发。而且更新起来也比较方便。

业务解耦

比如刚刚上面说的专题页面,还有必要走整个框架的一套流程吗?进来引用一大堆的文件,初始化一大堆的东西?是不是特别低效呢?所以需要业务解耦,专题页面如果真要框架(可以首次访问之后生成静态页面)也应该是足够轻量级的。不能与传统业务混为一谈。

分布式以及 soa

说业务优化,真的不得不提架构方面的东西,业务解耦之后,就有了分布式和soa,因为这在上次分享中已经都说过了,就不多说了。
只说下 soa 自定义 socket 传输协议。
https://segmentfault.com/img/remote/1460000010158196
最重要的就是在自定义头里面强调body_len,注意设置为紧凑型,才能保证跨平台性 具体说明:https://mengkang.net/586.html

Mysql 优化

数据索引相关的文章网上很多了,不足的地方大家补充。

表设计 - 拥抱 innodb

现在大多数情况都会使用innodb类型了。具体原因是 mysql 专家给的意见。
我自己对 mysql 的优化不了解,每一个细分领域都是一片汪洋,每个人的时间精力是有限的,所以大家也不用什么都非要深入去研究,往往是一些计算机基础更为重要。
参考这份ppt https://static.mengkang.net/u...

表设计 - 主键索引

  1. innodb 需要一个主键,主键不要有业务用途,不要修改主键。
  2. 主键最好保持顺序递增,随机主键会导致聚簇索引树频繁分裂,随机I/O增多,数据离散,性能下降。

举例:
之前项目里有些索引是article_id + tag_id 联合做的主键,那么这种情况下,就是业务了属性了。主键也不是顺序递增,每插入新的数据都有可能导致很大的索引变动(了解下数据库b+索引的原理)

表设计 - 字段选择

  1. 能选短整型,不选长整型。比如一篇文章的状态值,不可能有超过100种吧,不过怎么扩展,没必要用int了。
  2. 能选 char 就避免 varchar,比如图片资源都有一个hashcode,固定长度20位,那么就可以选char了。
  3. 当使用 varchar 的时候,长度够用就行,不要滥用。
  4. 大文本单独分离,比如文章的详情,单独出一张表。其他基本信息放在一张表里,然后关联起来。
  5. 冗余字段的使用,比如文章的详情字段,增加一个文章markdown解析之后的字段。

索引优化

大多数情况下,索引扫描要比全表扫描更快,性能更好。但也不是绝对的,比如需要查找的数据占了整个数据表的很大比例,反而使用索引更慢了。

  1. 没有索引的更新,可能会导致全表数据都被锁住。所以更新的时候要根据索引来做。
  2. 联合索引的使用
  3. explain 的使用

联合索引“最左前缀”,查询优化器还会帮你调整条件表达式的顺序,以匹配组合索引的要求。

CREATE TABLE `test` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `a` int(10) unsigned NOT NULL,
  `b` int(10) unsigned NOT NULL,
  `c` int(10) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  KEY `index_abc` (`a`,`b`,`c`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

能使用到索引

explain select * from test where a=1;
explain select * from test where a=1 and b=2;
explain select * from test where a=1 and b=2 and c=3;
explain select * from test where a=1 and b in (2,3) and c=3;
explain select * from test where a=1 and b=2 order by c desc;

不能使用索引

explain select * from test where a=1 and b in (2,3) order by c desc;
explain select * from test where b=2;
索引更详细讲解 https://mengkang.net/1302.html

explain 搜到一篇不错的: http://blog.csdn.net/woshiqjs...
很重要的参数type,key,extra

type 最常见的

system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

说明
const通过索引直接找到一个匹配行,一般主键索引的时候
ref没有主键索引或者唯一索引的条件索引,查询结果多行,在联合查询中很常见
index利用到了索引,有可能有其它排序,where 或者 group by 等
all全表扫描,没有使用到索引

extra

如果有Using filesort或者Using temporary的话,就必须要优化了

收集慢查询

my.ini 配置里增加

long_query_time=2
log-slow-queries=/data/var/mysql_slow.log

使用 nosql

redis 丰富的数据类型,非常适合配合mysql 做一些关系型的查询。比如一个非常复杂的查询列表可以将其插入zset 做排序列表,然后具体的信息,通过zset里面的纸去mysql 里面去查询。

缓存优化

多级缓存

  1. 请求内缓存

static 变量存储,比如朋友圈信息流,在一次性获取20条信息的时候,有可能,点赞的人里面20条里面有30个人是重复的,他们点赞你的a图片也点赞了你的b图片,所以这时,如果能使用static数组来存放这些用户的基本信息就高效了些。

  1. 本地缓存

请求结束了,下拉更新朋友圈,里面又出现了上面的同样的好友,还得重新请求一次。所以本地常驻内存的缓存就更高效了。

  1. 分布式缓存

在A服务器上已经查询过了,在下拉更新的时候被分配到B服务器上了,难道同样的数据再查一次再存到B服务器的本地缓存里面吗,弄一个分布式缓存吧,这样防止了重复查询。但是多了网络请求这一步。

很多时候是三者共存的。

避免缓存的滥用

案例分析

  1. 用户积分更新

    • 比如用户的基本信息和积分混在一起,当用户登录的时候赠送积分。则需要更新用户的积分,这个时候更新整个用户的基本信息缓存么?
    • 所以这里也可以运用下面 hashes 分片的原则去更新
  2. 礼物和主题绑定缓存

为了取数据方便把多个数据源混合缓存了,这种情况,相比大家可能都见过,这是灾难性的设计。

{
id:x,
title:x,
gift:{
        id:x,
        name:x,
        img:x,
    }
}

如果需要更新礼物的图片,那么所有用到过这个礼物的话题的缓存都要更新。

redis 使用场景举例

由于比较基础基础好的老司机就可以忽略了,新人同学可以看下 https://mengkang.net/356.html

redis 优化

  1. 多实例化,更高效地利用服务器 cpu
  2. 内存优化,官方意见 https://redis.io/topics/memor... 有点老
  3. 尽可能的使用 hashes ,时间复杂度低,查询效率高。同时还节约内存。Instagram 最开始用string来存图片id=>uid的关系数据,用了21g,后来改为水平分割,图片id 1000 取模,然后将分片的数据存在一个hashse 里面,这样最后的内容减少了5g,四分之一基本上。
每一段使用一个Hash结构存储,由于Hash结构会在单个Hash元素在不足一定数量时进行压缩存储,所以可以大量节约内存。这一点在String结构里是不存在的。而这个一定数量是由配置文件中的hash-zipmap-max-entries参数来控制的。

服务器认知的提升

下面的内容,只能是让大家有一个大概的认识,了解一个优化的方向,具体的内容需要系统学习很多很多的知识。

多进程的优势

多进程有利于 CPU 计算和 I/O 操作的重叠利用。一个进程消耗的绝大部分时间都是在磁盘I/O和网络I/O中。
如果是单进程时cpu大量的时间都在等待I/O,所以我们需要使用多进程。

减少上下文切换

为了让所有的进程轮流使用系统资源,进程调度器在必要的时候挂起正在运行的进程,同时恢复以前挂起的某个进程。这个就是我们常说的“上下文切换”。

关于上下文我之前写一个简单笔记 https://mengkang.net/729.html

无限制增加进程数,则会增多 cpu 在各个进程间切换的次数。
如果我们希望服务器支持较大的并发数,那么久要尽量减少上下文切换的次数,比如在nginx服务上nginx的子进程数不要超过cpu的核数。
我们可以在压测的时候通过vmstat,nmon来监控系统上下文切换的次数。

IOwait 不一定是 I/O 繁忙

# top

top - 09:40:40 up 565 days,  5:47,  2 users,  load average: 0.03, 0.03, 0.00
Tasks: 121 total,   2 running, 119 sleeping,   0 stopped,   0 zombie
Cpu(s):  8.6%us,  0.3%sy,  0.0%ni, 90.7%id,  0.2%wa,  0.0%hi,  0.2%si,  0.0%st

一般情况下IOwait代表I/O操作的时间占(I/O操作的时间 + I/O和CPU时间)的比例。
但是也时候也不准,比如nginx来作为web服务器,当我们开启很多nginx子进程,IOwait会很高,当再减少进程数到cpu核数附近时,IOwait会减少,监控网络流量会发现也增加。

多路复用 I/O 的使用

只要是提供socket服务,就可以利用多路复用 I/O 模型。
需要补充的知识 https://mengkang.net/726.html

减少系统调用

strace 非常方便统计系统调用

# strace -c -p 23374
Process 23374 attached - interrupt to quit
^CProcess 23374 detached
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 30.68    0.000166           0       648           poll
 12.01    0.000065           0       228           munmap
 11.65    0.000063           0       228           mmap
 10.54    0.000057           0       660           recvfrom
 10.35    0.000056           0       708           fstat
  7.76    0.000042           0       252           open
  6.10    0.000033           1        36           write
  5.73    0.000031           0        72        24 access
  5.18    0.000028           0        72           read
  0.00    0.000000           0       276           close
  0.00    0.000000           0        13        13 stat
  0.00    0.000000           0       269       240 lstat
  0.00    0.000000           0        12           rt_sigaction
  0.00    0.000000           0        12           rt_sigprocmask
  0.00    0.000000           0        12           pwrite
  0.00    0.000000           0        48           setitimer
  0.00    0.000000           0        12           socket
  0.00    0.000000           0        12           connect
  0.00    0.000000           0        12           accept
  0.00    0.000000           0       168           sendto
  0.00    0.000000           0        12           shutdown
  0.00    0.000000           0        48           fcntl
  0.00    0.000000           0        12           flock
  0.00    0.000000           0       156           getcwd
  0.00    0.000000           0        24           chdir
  0.00    0.000000           0        24           times
  0.00    0.000000           0        12           getuid
------ ----------- ----------- --------- --------- ----------------
100.00    0.000541                  4038       277 total

通过strace查看“系统调用时间”和“调用次数”来定位问题 https://huoding.com/2013/10/0...

自己构建web服务器

要想理解web服务器优化的原理,最好的办法是了解它的来龙去脉,实践就是最好的方式,我分为以下几个步骤:

  1. 用 PHP 来实现一个动态 Web 服务器 https://mengkang.net/491.html
  2. 简单静态 web 服务器(循环服务器)的实现 https://mengkang.net/563.html
  3. 多进程并发的面向连接 Web 服务器的实践 https://mengkang.net/571.html
  4. 简单静态 Select Web 服务器的实现 https://mengkang.net/568.html
  5. I/O 多路复用 https://mengkang.net/726.html

上面是我的学习笔记,图片资源丢失了,大家可以根据相关关键词去搜搜相关的文章和书籍,更推荐大家去看书。

查看原文

赞 176 收藏 277 评论 15

拥之则安 赞了文章 · 2019-09-22

如何加快 Node.js 应用的启动速度

我们平时在开发部署 Node.js 应用的过程中,对于应用进程启动的耗时很少有人会关注,大多数的应用 5 分钟左右就可以启动完成,这个过程中会涉及到和集团很多系统的交互,这个耗时看起来也没有什么问题。

目前,集团 Serverless 大潮已至,Node.js serverless-runtime 作为前端新研发模式的基石,也发展的如火如荼。Serverless 的优势在于弹性、高效、经济,如果我们的 Node.js FaaS 还像应用一样,一次部署耗时在分钟级,无法快速、有效地响应请求,甚至在脉冲请求时引发资源雪崩,那么一切的优势都将变成灾难。

所有提供 Node.js FaaS 能力的平台,都在绞尽脑汁的把冷/热启动的时间缩短,这里面除了在流程、资源分配等底层基建的优化外,作为其中提供服务的关键一环 —— Node.js 函数,本身也应该参与到这场时间攻坚战中。

Faas平台从接到请求到启动业务容器并能够响应请求的这个时间必须足够短,当前的总目标是 500ms,那么分解到函数运行时的目标是 100ms。这 100ms 包括了 Node.js 运行时、函数运行时、函数框架启动到能够响应请求的时间。巧的是,人类反应速度的极限目前科学界公认为 100ms。

Node.js 有多快

在我们印象中 Node.js 是比较快的,敲一段代码,马上就可以执行出结果。那么到底有多快呢?

以最简单的 console.log 为例(例一),代码如下:

// console.js
console.log(process.uptime() * 1000);

在 Node.js 最新 LTS 版本 v10.16.0 上,在我们个人工作电脑上:

node console.js
// 平均时间为 86ms
time node console.js
// node console.js  0.08s user 0.03s system 92% cpu 0.114 total

看起来,在 100ms 的目标下,留给后面代码加载的时间不多了。。。

在来看看目前函数平台提供的容器里的执行情况:

node console.js
// 平均时间在 170ms
time node console.js
// real    0m0.177s
// user    0m0.051s
// sys     0m0.009s

Emmm… 情况看起来更糟了。

我们在引入一个模块看看,以 serverless-runtime 为例(例二):

// require.js
console.time('load');
require('serverless-runtime');
console.timeEnd('load');

本地环境:

node reuqire.js
// 平均耗时 329ms

服务器环境:

node require.js
// 平均耗时 1433ms

我枯了。。。
这样看来,从 Node.js 本身加载完,然后加载一个函数运行时,就要耗时 1700ms。
看来 Node.js 本身并没有那么快,我们 100ms 的目标看起来很困难啊!

为什么这么慢

为什么会运行的这么慢?而且两个环境差异这么大?我们需要对整个运行过程进行分析,找到耗时比较高的点,这里我们使用 Node.js 本身自带的 profile 工具。

node --prof require.js
node --prof-process isolate-xxx-v8.log > result
[Summary]:
ticks  total  nonlib   name
     60   13.7%   13.8%  JavaScript
    371   84.7%   85.5%  C++
     10    2.3%    2.3%  GC
      4    0.9%          Shared libraries
      3    0.7%          Unaccounted
[C++]:
ticks  total  nonlib   name
    198   45.2%   45.6%  node::contextify::ContextifyScript::New(v8::FunctionCallbackInfo<v8::Value> const&)
     13    3.0%    3.0%  node::fs::InternalModuleStat(v8::FunctionCallbackInfo<v8::Value> const&)
      8    1.8%    1.8%  void node::Buffer::(anonymous namespace)::StringSlice<(node::encoding)1>(v8::FunctionCallbackInfo<v8::V
alue> const&)
      5    1.1%    1.2%  node::GetBinding(v8::FunctionCallbackInfo<v8::Value> const&)
      4    0.9%    0.9%  __memmove_ssse3_back
      4    0.9%    0.9%  __GI_mprotect
      3    0.7%    0.7%  v8::internal::StringTable::LookupStringIfExists_NoAllocate(v8::internal::String*)
      3    0.7%    0.7%  v8::internal::Scavenger::ScavengeObject(v8::internal::HeapObjectReference**, v8::internal::HeapObject*)
      3    0.7%    0.7%  node::fs::Open(v8::FunctionCallbackInfo<v8::Value> const&)

对运行时启动做同样的操作

[Summary]:
ticks  total  nonlib   name
    236   11.7%   12.0%  JavaScript
   1701   84.5%   86.6%  C++
     35    1.7%    1.8%  GC
     47    2.3%          Shared libraries
     28    1.4%          Unaccounted
[C++]:
ticks  total  nonlib   name
    453   22.5%   23.1%  t node::fs::Open(v8::FunctionCallbackInfo<v8::Value> const&)
    319   15.9%   16.2%  T node::contextify::ContextifyContext::CompileFunction(v8::FunctionCallbackInfo<v8::Value> const&)
     93    4.6%    4.7%  t node::fs::InternalModuleReadJSON(v8::FunctionCallbackInfo<v8::Value> const&)
     84    4.2%    4.3%  t node::fs::Read(v8::FunctionCallbackInfo<v8::Value> const&)
     74    3.7%    3.8%  T node::contextify::ContextifyScript::New(v8::FunctionCallbackInfo<v8::Value> const&)
     45    2.2%    2.3%  t node::fs::InternalModuleStat(v8::FunctionCallbackInfo<v8::Value> const&)
   ...

可以看到,整个过程主要耗时是在 C++ 层面,相应的操作主要为 Open、ContextifyContext、CompileFunction。这些调用通常是出现在 require 操作中,主要覆盖的内容是模块查找,加载文件,编译内容到 context 等。

看来,require 是我们可以优化的第一个点。

如何更快

从上面得知,主要影响我们启动速度的是两个点,文件 I/O 和代码编译。我们分别来看如何优化。

▐ 文件 I/O

整个加载过程中,能够产生文件 I/O 的有两个操作:

一、查找模块

因为 Node.js 的模块查找其实是一个嗅探文件在指定目录列表里是否存在的过程,这其中会因为判断文件存不存在,产生大量的 Open 操作,在模块依赖比较复杂的场景,这个开销会比较大。

二、读取模块内容

找到模块后,需要读取其中的内容,然后进入之后的编译过程,如果文件内容比较多,这个过程也会比较慢。

那么,如何能够减少这些操作呢?既然模块依赖会产生很多 I/O 操作,那把模块扁平化,像前端代码一样,变成一个文件,是否可以加快速度呢?

说干就干,我们找到了社区中一个比较好的工具 ncc,我们把 serverless-runtime 这个模块打包一次,看看效果。

服务器环境:

ncc build node_modules/serverless-runtime/src/index.ts
node require.js
// 平均加载时间 934ms

看起来效果不错,大概提升了 34% 左右的速度。

但是,ncc 就没有问题嘛?我们写了如下的函数:

import * as _ from 'lodash';
import * as Sequelize from 'sequelize';
import * as Pandorajs from 'pandora';
console.log('lodash: ', _);
console.log('Sequelize: ', Sequelize);
console.log('Pandorajs: ', Pandorajs);

测试了启用 ncc 前后的差异:

可以看到,ncc 之后启动时间反而变大了。这种情况,是因为太多的模块打包到一个文件中,导致文件体积变大,整体加载时间延长。可见,在使用 ncc 时,我们还需要考虑 tree-shaking 的问题。

▐ 代码编译

我们可以看到,除了文件 I/O 外,另一个耗时的操作就是把 Javascript 代码编译成 v8 的字节码用来执行。我们的很多模块,是公用的,并不是动态变化的,那么为什么每次都要编译呢?能不能编译好了之后,以后直接使用呢?

这个问题,V8 在 2015 年已经替我们想到了,在 Node.js v5.7.0 版本中,这个能力通过 VM.Script 的 cachedData暴露了出来。而且,这些 cache 是跟 V8 版本相关的,所以一次编译,可以在多次分发。

我们先来看下效果:

//使用 v8-compile-cache 在本地获得 cache,然后部署到服务器上
node require.js
// 平均耗时 868ms

大概有 40% 的速度提升,看起来是一个不错的工具。

但它也不够完美,在加载 code cache 后,所有的模块加载不需要编译,但是还是会有模块查找所产生的文件 I/O 操作。

▐ 黑科技

如果我们把 require 函数做下修改,因为我们在函数加载过程中,所有的模块都是已知已经 cache 过的,那么我们可以直接通过 cache 文件加载模块,不用在查找模块是否存在,就可以通过一次文件 I/O 完成所有的模块加载,看起来是很理想的。

不过,可能对远程调试等场景不够优化,源码索引上会有问题。这个,之后会做进一步尝试。

近期计划

有了上面的一些理论验证,我们准备在生产环境中将上述优化点,如:ncc、code cache,甚至 require 的黑科技,付诸实践,探索在加载速度,用户体验上的平衡点,以取得速度上的提升。

其次,会 review 整个函数运行时的设计及业务逻辑,减少因为逻辑不合理导致的耗时,合理的业务逻辑,才能保证业务的高效运行。

最后,Node.js 12 版本对内部的模块默认做了 code cache,对 Node.js 默认进程的启动速度提升比较明显,在服务器环境中,可以控制在 120ms 左右,也可以考虑引用尝试下。

未来思考

其实,V8 本身还提供了像 Snapshot 这样的能力,来加快本身的加载速度,这个方案在 Node.js 桌面开发中已经有所实践,比如 NW.js、Electron 等,一方面能够保护源码不泄露,一方面还能加快进程启动速度。Node.js 12.6 的版本,也开启了 Node.js 进程本身的在 user code 加载前的 Snapshot 能力,但目前看起来启动速度提升不是很理想,在 10% ~ 15% 左右。我们可以尝试将函数运行时以 Snapshot 的形式打包到 Node.js 中交付,不过效果我们暂时还没有定论,现阶段先着手于比较容易取得成果的方案,硬骨头后面在啃。

另外,Java 的函数计算在考虑使用 GraalVM 这样方案,来加快启动速度,可以做到 10ms 级,不过会失去一些语言上的特性。这个也是我们后续的一个研究方向,将函数运行时整体编译成 LLVM IR,最终转换成 native 代码运行。不过又是另一块难啃的骨头。



本文作者:杜佳昆(凌恒) 

阅读原文

本文为云栖社区原创内容,未经允许不得转载。

查看原文

赞 8 收藏 6 评论 0

拥之则安 发布了文章 · 2019-09-16

redis过期策略和内存淘汰机制

redis使用的过期策略:定期删除+惰性删除

定期删除
redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定期遍历这个字典来删除到期的 key。

Redis 默认会每秒进行十次过期扫描(100ms一次),过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。

从过期字典中随机 20 个 key;
删除这 20 个 key 中已经过期的 key;
如果过期的 key 比率超过 1/4,那就重复步骤 1;
redis默认是每隔 100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!

惰性删除
所谓惰性策略就是在客户端访问这个key的时候,redis对key的过期时间进行检查,如果过期了就立即删除,不会给你返回任何东西。

定期删除可能会导致很多过期key到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这就是所谓的惰性删除,即当你主动去查过期的key时,如果发现key过期了,就立即进行删除,不返回任何东西.

定期删除是集中处理,惰性删除是零散处理。

为什么要采用定期删除+惰性删除2种策略呢?

如果过期就删除。假设redis里放了10万个key,都设置了过期时间,你每隔几百毫秒,就检查10万个key,那redis基本上就死了,cpu负载会很高的,消耗在你的检查过期key上了.

但是问题是,定期删除可能会导致很多过期key到了时间并没有被删除掉,那咋整呢?所以就是惰性删除了。这就是说,在你获取某个key的时候,redis会检查一下 ,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。

并不是key到时间就被删除掉,而是你查询这个key的时候,redis再懒惰的检查一下

通过上述两种手段结合起来,保证过期的key一定会被干掉。

所以说用了上述2种策略后,下面这种现象就不难解释了:数据明明都过期了,但是还占有着内存

redis内存淘汰机制

volatile-lru -> Evict using approximated LRU among the keys with an expire set.
                    在带有过期时间的键中选择最近最少使用的。(推荐)
allkeys-lru -> Evict any key using approximated LRU.
                    在所有的键中选择最近最少使用的。(不区分是否携带过期时间)(一般推荐)
volatile-lfu -> Evict using approximated LFU among the keys with an expire set.
                    在带有过期时间的键中选择最不常用的。
allkeys-lfu -> Evict any key using approximated LFU.
                    在所有的键中选择最不常用的。(不区分是否携带过期时间)
volatile-random -> Remove a random key among the ones with an expire set.
                    在带有过期时间的键中随机选择。
allkeys-random -> Remove a random key, any key.
                    在所有的键中随机选择。
volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
                    在带有过期时间的键中选择过期时间最小的。
noeviction -> Don't evict anything, just return an error on write operations.
                    不要删除任何东西,只是在写操作上返回一个错误。默认。
                    
                    
配置方式:
maxmemory-policy volatile-lru #默认是noeviction,即不进行数据淘汰

查看原文

赞 0 收藏 0 评论 0

拥之则安 发布了文章 · 2019-09-16

Redis缓存和数据库双写一致性问题

文章结构

本文由以下三个部分组成
1、讲解缓存更新策略
2、对每种策略进行缺点分析
3、针对缺点给出改进方案

正文

先做一个说明,从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。这种方案下,我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。因此,接下来讨论的思路不依赖于给缓存设置过期时间这个方案。
在这里,我们讨论三种更新策略:

先更新数据库,再更新缓存

先删除缓存,再更新数据库

先更新数据库,再删除缓存

应该没人问我,为什么没有先更新缓存,再更新数据库这种策略。

(1)先更新数据库,再更新缓存

这套方案,大家是普遍反对的。为什么呢?有如下两点原因。

  • 原因一(线程安全角度)

同时有请求A和请求B进行更新操作,那么会出现
(1)线程A更新了数据库
(2)线程B更新了数据库
(3)线程B更新了缓存
(4)线程A更新了缓存

这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。

  • 原因二(业务场景角度)

有如下两点:
(1)如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。
(2)如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。显然,删除缓存更为适合。

接下来讨论的就是争议最大的,先删缓存,再更新数据库。还是先更新数据库,再删缓存的问题。

(2)先删缓存,再更新数据库
该方案会导致不一致的原因是。同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:
(1)请求A进行写操作,删除缓存
(2)请求B查询发现缓存不存在
(3)请求B去数据库查询得到旧值
(4)请求B将旧值写入缓存
(5)请求A将新值写入数据库

上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。
那么,如何解决呢?采用延时双删策略
伪代码如下

public void write(String key,Object data){

    redis.delKey(key);

    db.updateData(data);

    Thread.sleep(1000);

    redis.delKey(key);

}

转化为中文描述就是
(1)先淘汰缓存
(2)再写数据库(这两步和原来一样)
(3)休眠1秒,再次淘汰缓存

这么做,可以将1秒内所造成的缓存脏数据,再次删除。
那么,这个1秒怎么确定的,具体该休眠多久呢?
针对上面的情形,读者应该自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
如果你用了mysql的读写分离架构怎么办?
ok,在这种情况下,造成数据不一致的原因如下,还是两个请求,一个请求A进行更新操作,另一个请求B进行查询操作。
(1)请求A进行写操作,删除缓存
(2)请求A将数据写入数据库了,
(3)请求B查询缓存发现,缓存没有值
(4)请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
(5)请求B将旧值写入缓存
(6)数据库完成主从同步,从库变为新值

上述情形,就是数据不一致的原因。还是使用双删延时策略。只是,睡眠时间修改为在主从同步的延时时间基础上,加几百ms。
采用这种同步淘汰策略,吞吐量降低怎么办?
ok,那就将第二次删除作为异步的。自己起一个线程,异步删除。这样,写的请求就不用沉睡一段时间后了,再返回。这么做,加大吞吐量。
第二次删除,如果删除失败怎么办?
这是个非常好的问题,因为第二次删除失败,就会出现如下情形。还是有两个请求,一个请求A进行更新操作,另一个请求B进行查询操作,为了方便,假设是单库:
(1)请求A进行写操作,删除缓存
(2)请求B查询发现缓存不存在
(3)请求B去数据库查询得到旧值
(4)请求B将旧值写入缓存
(5)请求A将新值写入数据库
(6)请求A试图去删除请求B写入对缓存值,结果失败了。

ok,这也就是说。如果第二次删除缓存失败,会再次出现缓存和数据库不一致的问题。
如何解决呢?
具体解决方案,且看博主对第(3)种更新策略的解析。

(3)先更新数据库,再删缓存
首先,先说一下。老外提出了一个缓存更新套路,名为《Cache-Aside pattern》。其中就指出

失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。

命中:应用程序从cache中取数据,取到后返回。

更新:先把数据存到数据库中,成功后,再让缓存失效。

另外,知名社交网站facebook也在论文《Scaling Memcache at Facebook》中提出,他们用的也是先更新数据库,再删缓存的策略。

这种情况不存在并发问题么?
不是的。假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生
(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存

ok,如果发生上述情况,确实是会发生脏数据。
然而,发生这种情况的概率又有多少呢?
发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。可是,大家想想,数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。
假设,有人非要抬杠,有强迫症,一定要解决怎么办?

如何解决上述并发问题?
首先,给缓存设有效时间是一种方案。其次,采用策略(2)里给出的异步延时删除策略,保证读请求完成以后,再进行删除操作。
还有其他造成不一致的原因么?
有的,这也是缓存更新策略(2)和缓存更新策略(3)都存在的一个问题,如果删缓存失败了怎么办,那不是会有不一致的情况出现么。比如一个写数据请求,然后写入数据库了,删缓存失败了,这会就出现不一致的情况了。这也是缓存更新策略(2)里留下的最后一个疑问。
如何解决?
提供一个保障的重试机制即可,这里给出两套方案。
方案一:
如下图所示
图片描述

流程如下所示
(1)更新数据库数据;
(2)缓存因为种种问题删除失败
(3)将需要删除的key发送至消息队列
(4)自己消费消息,获得需要删除的key
(5)继续重试删除操作,直到成功

然而,该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。

方案二:

图片描述

流程如下图所示:
(1)更新数据库数据
(2)数据库会将操作信息写入binlog日志当中
(3)订阅程序提取出所需要的数据以及key
(4)另起一段非业务代码,获得该信息
(5)尝试删除缓存操作,发现删除失败
(6)将这些信息发送至消息队列
(7)重新从消息队列中获得该数据,重试操作。

备注说明:上述的订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能。至于oracle中,博主目前不知道有没有现成中间件可以使用。另外,重试机制,博主是采用的是消息队列的方式。如果对一致性要求不是很高,直接在程序中另起一个线程,每隔一段时间去重试即可,这些大家可以灵活自由发挥,只是提供一个思路。

对于先删缓存,再更新数据库的更新策略,还有方案提出维护一个内存队列的方式,博主看了一下,觉得实现异常复杂,没有必要,因此没有必要在文中给出。最后,希望大家有所收获

查看原文

赞 1 收藏 1 评论 0

拥之则安 收藏了文章 · 2019-09-04

10 个开发者必知的 MySQL 8.0 新功能

文章转发自专业的Laravel开发者社区,原始链接:https://learnku.com/laravel/t...

下面将以 MySQL 社区的优先级从高到低来展示这些功能:

TOP 10

  1. MySQL 文档存储
  2. 默认 utf8mb4 编码
  3. JSON 增强
  4. CTEs(译者注:Common Table Expresssions 公共表格表达式)
  5. 窗口函数
  6. 降序索引
  7. 更好的优化器消费模型
  8. MySQL 服务器组件
  9. GIS(译者注:Geographic Information System 地理信息系统) 提升
  10. InnoDB 引擎的 NO WAIT 和 SKIP LOCKED 选项

1. MySQL 文档存储

这是 MySQL 8.0 中最受期待和最受欢迎的特性 ... 同时他非常容易理解。

我对 MySQL 文档存储非常兴奋,我在全球各地展示他快一年的时间,并收到了很多好的反馈。 为什么 MySQL DS 如此优秀? 因为使用一种解决方案你可以处理 SQL 和 NoSQL。你也可以将两种语言的优势结合起来。 你可以对相同数据执行 CRUD 命令,同时你也可以在 SQL 中执行如连接多个表及 and/or 集合这种更复杂的查询。

同时后端是众所周知强大的 InnoDB 引擎, MySQL 文档存储引擎完全符合 ACID 标准。 因为他都在 MySQL 内部,所以你可以从你熟悉的内容中收益,亦可以将其转换到文档存储: replication, performance_schema, ...

2. 默认字符集为 utf8mb4

使用 MySQL 8.0, 我们当然关注现代 Web 应用... 这是指移动端! 当我们提到手机端, 也是表情符号和大量的需要共存的字符集和归类。

这就是为什么我们决定将默认的字符集从 latin-1 转为 utf8mb4。 MySQL支持最新的 Unicode 9.0 基于 DUCET 的新分类, 重音和大小写敏感的归类,日语,俄语,...

3. JSON 强化

MySQL 带来了一些新的 JSON 相关变更:

  • 新增 ->> 表达式,作用等于 JSON_UNQUOTE(JSON_EXTRACT())
  • 新的聚合函数 JSON_ARRAYAGG() 和 JSON_OBJECTAGG()
  • 新增 JSON_PRETTY()
  • 新的 JSON 工具函数如 JSON_STORAGE_SIZE()JSON_STORAGE_FREE()

MySQL 8.0 中 JSON 最重要的优化之一,是提供了一个 JSON_TABLE() 函数。此函数接受 JSON 格式的数据,然后将其转化为关系型数据表。字段和数据的格式都可以被指定。你也可以对 JSON_TABLE() 以后的数据使用正常的 SQL 查询,如 JOINS, 聚合查询等, ... 你可以查阅 @stoker 的博文 ,当然你也可以阅读 官方文档 。

需要注意的是,这不仅仅影响到开发者的使用,MySQL 的执行性能也会受到影响。在老系统中,更新 JSON 时系统会删除老数据并写入新的数据,在新系统中,如果你要更新 JSON 数据里的某个字段,正确的做法是直接对 JSON 里的某个字段进行更新,这样执行效率更佳,并且数据库主从复制(Replication)性能也会受益。

4. 公共表格表达式 (CTEs)

MySQL 8.0 新增了 CTEs 功能(译者注:Common Table Expresssions 公共表格表达式)。CTE 是一个命名的临时结果集,仅在单个 SQL 语句的执行范围内存在,可以是自引用,也可以在同一查询中多次引用。

5. 统计分析方法

针对查询中的每一行,一个统计分析方法使用该行关联的行执行计算。 这就像 GROUP BY 方法但他是保留行而不是折叠他们。

以下是 MySQL 8.0.4 当前实现的统计分析方法列表:

名称描述
CUME_DIST()累计分配值
DENSE_RANK()当前行在分区的排名, 没有间隔
FIRST_VALUE()窗口框架第一行的参数值
LAG()分区中指定行落后于当前行的参数值
LAST_VALUE()窗口框架第一行的参数值
LEAD()分区中引导当前行的参数值
NTH_VALUE()从第N行窗口框架的参数值
NTILE()分区中当前行的桶号
PERCENT_RANK()百分比等级值
RANK()当前行在分区中的排名,含间隔
ROW_NUMBER()其分区中的当前行数

6. 降序索引

在 MySQL 8.0 之前, 当在索引定义中使用 DESC 时该标志将被忽略。 现在不再是这样了! 现在键值按降序存储。以前, 索引可能被按相反顺序扫描,但性能会受到影响。可以按顺序扫描倒序索引,这将更高效。

7. 更好的优化器开销模型

新的优化器开销模型(Optimizer Cost Model)现在会计算内存缓存数据和硬盘数据。推荐阅读 Øystein 的博客文章.

8. MySQL 服务器模块

你可以利用此特性来扩展 MySQL 服务器的功能,这将会比插件更加容易开发和维护,推荐阅读 官方文档

9. GIS 的提升

MySQL 8.0 对 GIS(译者注:Geographic Information System 地理信息系统) 的支持有非常高的提升,功能上直追 PostgreSQL。

一些例子:

  • 坐标轴将拥有单位
  • 地理坐标系统
  • 坐标轴将不会偏移
  • 坐标轴支持排序
  • 坐标轴支持方向相关性

10. InnoDB 引擎 NO WAIT 与 SKIP LOCKED

MySQL 8.0 的 InnoDB 引擎现在可以更好的处理热行争抢。 InnoDB 支持 NOWAIT 和 `SKIP\
LOCKED 选项与 SELECT ... FOR\
SHARE 和 SELECT ... FOR\
UPDATE 锁定读取语句。 NOWAIT 会在请求行被其他事务锁定的情况下立即返回语句。 SKIP LOCKED` 从结果集中删除被锁定的行。 参见 使用 NOWAIT 和 SKIP LOCKED 锁定并发读取.

当然好的 MySQL 8.0 特性列表不会在这里结束, 例如 支持正则表达式  也是一个刚刚出现在 [8.0.4] 版本中的有趣的特性 (https://mysqlserverteam.com/t...。 新的 SQL GROUPING() 功能,  IPV6 和 UUID 操作新业务,更多优化器提示...

我希望这给你一个很好的概述,是什么样的请求在驱动 MySQL 的创新。下一篇文章将介绍应该使得开发者满意的 MySQL 8.0 特性 ?

查看原文

拥之则安 赞了文章 · 2019-08-26

PHP & Swoole 与 Java、Go 等技术选型答疑

来自 SwooleVIP 群内一位成员的问题

  1. 感觉Swoole越来越复杂了,虽然特性也变得更强,可惜在项目组里面根本推不动,而且协程后需要注意事项也很多,稍不注意可能就是连接忘了回收,连接错乱的风险(当然有defer之类的可以规避)
  2. PHP的,基本上都是半路出家,或者大部分培训机构的,不招他们进来吧,项目赶不完,招他们吧,都是得过且过的学习性格。
  3. 稍微高端一点的特性,几乎推不动,他们还会反问,花时间看这个,为啥不学go呢?毕竟go是官方自带
  4. 搞得我们现在新项目(国内龙头电商),基本上都用Java了,也不知道咋说。。。用PHP确实太多只会做简单crud

Rango 回答

  1. Swoole4现在是越来越简单了,现在的协程比以前的异步回调好用。你应该使用社区内成熟的框架,比如 HyperfSwoftEasySwooleMixPHP。直接基于Swoole开发很容易犯错,这需要开发者具备更高的素质。而基于框架之上开发应用,无论是Swoole还是JavaGo对于使用者来说难度是一致的。
  2. 大多数JavaGo的工程师其实也都是用别人写好的框架,所以简单。如果是自行写一个多线程的Java框架,类似于Spring,这很困难。
  3. 也不只是PHP的工程师是CRUD,大部分做JavaGo的程序员也一样是在做CRUD的工作。并没有什么差别。提升对整个技术栈的理解深度、技术把控能力才是最重要的。
  4. Swoole4 的协程 和 Go 完全一样,但是 PHP 语言更简单易用。Go是强类型静态语言,没有泛型支持,面向对象也不完整,更适合搞底层软件的开发,各种组件生态也没有PHP丰富。如果你是从事服务器端应用开发用Go效率远不如PHP
  5. 编码、工程实践、抽象、业务理解、服务治理(包括性能、稳定性、健壮性、可用性、可扩展性)、架构设计,这些方面的能力与语言无关,编程语言对于优秀的工程师来说只是一个顺手的工具。掌握两个一模一样的技能,比如用 PHP、Go、Java 开发服务器后台程序的技能几乎是同质化的,实际上意义不大,浪费了时间
  6. PHP是很多有优势的,可以让工程师不再花时间用在对于语言技术栈的学习上,熟练掌握PHP整个技术栈只需要2-3年即可,其他的语言虽然功能上更强大,但是需要程序员花费更多时间用在学习语言API、运行原理、各种类库和工具上。时间需要3-6年才能达到精通的水平。
  7. PHP工程师因为精通这门技术更容易,反而可以把宝贵的时间投入到更高层面的工作上。PHP-FPM的健壮性是有目共睹的,算得上是工业级的技术。而Swoole目前也越来越成熟了,健壮性也越来越高。
  8. Swoole是一个帮助PHP工程师在PHP-FPMWeb编程之外功能范围的一个扩展,PHP工程师可以获得更大的操作空间。而不必花时间去重新学习一门新的编程语言技术栈,这通常需要几年时间才能完成,而学习掌握Swoole一般来说3-5个月即可,毕竟Swoole其实也是PHP技术栈范畴之内。

关于半路出家、学历低等问题

一个工程师是否优秀,很大的程度上取决于他的技术思考深度、持续学习能力。很多人说自己是非计算机专业、半路出家、学历不够高。我觉得这个并不是问题,以上并不妨碍你去学习与思考。

大部分人没有到拼天赋的阶段,我一直相信一万小时理论,如果你能坚持在技术上保持专注,进行大量工程实践,日积月累,大概率会成为这个领域的专家。

Swoole 微课程

图片描述

查看原文

赞 83 收藏 28 评论 37

拥之则安 发布了文章 · 2019-08-24

如何保证接口的幂等性

什么是幂等性

幂等性是系统服务对外一种承诺,承诺只要调用接口成功,外部多次调用对系统的影响是一致的。声明为幂等的服务会认为外部调用失败是常态,并且失败之后必然会有重试。

什么情况下需要幂等

以SQL为例:

SELECT col1 FROM tab1 WHER col2=2,无论执行多少次都不会改变状态,是天然的幂等。
UPDATE tab1 SET col1=1 WHERE col2=2,无论执行成功多少次状态都是一致的,因此也是幂等操作。
UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,这种不是幂等的。
insert into user(userid,name) values(1,'a') 如userid为唯一主键,即重复操作上面的业务,只会插入一条用户数据,具备幂等性。
如userid不是主键,可以重复,那上面业务多次操作,数据都会新增多条,不具备幂等性。
delete from user where userid=1,多次操作,结果一样,具备幂等性

如何保证幂等

token机制

1、服务端提供了发送token的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取token,服务器会把token保存到redis中。

2、然后调用业务接口请求时,把token携带过去,一般放在请求头部。

3、服务器判断token是否存在redis中,存在表示第一次请求,然后删除token,继续执行业务。

4、如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给client,这样就保证了业务代码,不被重复执行。

关键点 先删除token,还是后删除token。

后删除token:如果进行业务处理成功后,删除redis中的token失败了,这样就导致了有可能会发生重复请求,因为token没有被删除。这个问题其实是数据库和缓存redis数据不一致问题,后续会写文章进行讲解。

先删除token:如果系统出现问题导致业务处理出现异常,业务处理没有成功,接口调用方也没有获取到明确的结果,然后进行重试,但token已经删除掉了,服务端判断token不存在,认为是重复请求,就直接返回了,无法进行业务处理了。

先删除token可以保证不会因为重复请求,业务数据出现问题。出现业务异常,可以让调用方配合处理一下,重新获取新的token,再次由业务调用方发起重试请求就ok了。
token机制缺点
业务请求每次请求,都会有额外的请求(一次获取token请求、判断token是否存在的业务)。其实真实的生产环境中,1万请求也许只会存在10个左右的请求会发生重试,为了这10个请求,我们让9990个请求都发生了额外的请求。

乐观锁机制

这种方法适合在更新的场景中,update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1
根据version版本,也就是在操作库存前先获取当前商品的version版本号,然后操作的时候带上此version号。我们梳理下,我们第一次操作库存时,得到version为1,调用库存服务version变成了2;但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订单服务传如的version还是1,再执行上面的sql语句时,就不会执行;因为version已经变为2了,where条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。
乐观锁主要使用于处理读多写少的问题

唯一主键
这个机制是利用了数据库的主键唯一约束的特性,解决了在insert场景时幂等问题。但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键。

如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关。

防重表
使用订单号orderNo做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且他们在同一个事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。

唯一ID
调用接口时,生成一个唯一id,redis将数据保存到集合中(去重),存在即处理过。

查看原文

赞 11 收藏 10 评论 3

拥之则安 发布了文章 · 2019-08-22

mysql 存储过程

SQL语句需要先编译然后执行,而存储过程(Stored Procedure)是一组为了完成特定功能的SQL语句集,经编译后存储在数据库中,用户通过指定存储过程的名字并给定参数(如果该存储过程带有参数)来调用执行它。

存储过程是可编程的函数,在数据库中创建并保存,可以由SQL语句和控制结构组成。当想要在不同的应用程序或平台上执行相同的函数,或者封装特定功能时,存储过程是非常有用的。数据库中的存储过程可以看做是对编程中面向对象方法的模拟,它允许控制数据的访问方式。

存储过程的优点:

(1).增强SQL语言的功能和灵活性:存储过程可以用控制语句编写,有很强的灵活性,可以完成复杂的判断和较复杂的运算。

(2).标准组件式编程:存储过程被创建后,可以在程序中被多次调用,而不必重新编写该存储过程的SQL语句。而且数据库专业人员可以随时对存储过程进行修改,对应用程序源代码毫无影响。

(3).较快的执行速度:如果某一操作包含大量的Transaction-SQL代码或分别被多次执行,那么存储过程要比批处理的执行速度快很多。因为存储过程是预编译的。在首次运行一个存储过程时查询,优化器对其进行分析优化,并且给出最终被存储在系统表中的执行计划。而批处理的Transaction-SQL语句在每次运行时都要进行编译和优化,速度相对要慢一些。

(4).减少网络流量:针对同一个数据库对象的操作(如查询、修改),如果这一操作所涉及的Transaction-SQL语句被组织进存储过程,那么当在客户计算机上调用该存储过程时,网络中传送的只是该调用语句,从而大大减少网络流量并降低了网络负载。

(5).作为一种安全机制来充分利用:通过对执行某一存储过程的权限进行限制,能够实现对相应的数据的访问权限的限制,避免了非授权用户对数据的访问,保证了数据的安全。

查看原文

赞 1 收藏 0 评论 0

拥之则安 关注了收藏夹 · 2019-08-16

网络协议

关注 295

认证与成就

  • 获得 19 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-07-12
个人主页被 428 人浏览