使用redis的有序集合,
Zincrby
https://redis.io/commands/zincrby
使用的symfony框架的这个方法,会返回接口名称
$request->getPathInfo()
比如,www.baidu.com/api/login 会返回接口名 /api/login
redis记录如下,
奇技指南
笔者最近在做PHP接口的性能优化,在排查性能问题和优化的过程中总结了一些心得,分享给大家。
本文来自公众号Qtest之道
No.1
性能问题排查
首先,做性能优化要先进行性问题排查,即排查PHP接口的代码实现中那一块执行比较慢。
排查方法一
笔者使用的ThinkPHP框架中自带了G方法可以很方便的获取某个区间的运行时间和内存占用情况。例如:
G('begin');
// ...其他代码段
G('end');
// ...也许这里还有其他代码
// 进行统计区间
echo G('begin','end').'s';//输出代码运行时间
echo G('begin','end','m').'kb';//输出内存开销统计(单位为kb)
通过对不同代码片段的运行时间统计与接口总运行时间的对比,可以分析出哪块代码的运行存在性能问题,然后再针对性的排查。
排查方法二
使用PHP性能分析工具来辅助定位。目前市面上有很多PHP性能分析工具,老牌的调试工具XDebug,以及Facebook开源的Xhprof。以Xhprof举例,配置成功后,可以记录和生成函数的调用链图,包含每个函数的请求次数、阻塞时间、CPU时间和内存使用情况。
图中标记为红色或黄色的区域就是需要重点关注的地方。
No.2
性能问题解决
通常在工具的辅助下,性能问题排查相对来说还是比较简单的。比较有挑战的是如何解决这些性能问题。笔者在解决性能问题的过程中也做了一些解决方式的总结:
一. 代码层面
代码的风格因人而异,实现方式也非常灵活。两个人实现同样的查询列表功能,代码写法都会不同。
以下几点是我个人总结的代码性能避坑指南:
使用PHP7+版本。PHP7发布的时候号称比PHP5.6快两倍,而最新的PHP 7.3比PHP 5.6快3倍!所以升级PHP版本的收益还是很大的。
尽量减少循环嵌套写法。循环嵌套增加所带来的运行消耗是指数级的。例如第一层循环执行100次,第二层循环执行100次,第三层循环执行100次,那么最终循环的运行次数就是100100100 = 一百万次。所以要尽量减少循环的嵌套。
尽量减少数据库操作,例如避免在循环中进行数据库查询。数据库操作是比较消耗性能的动作,如果可以尽量少的做数据库操作,性能会有很大的提升。
尽量将对象的声明写在循环外部以减少声明次数。
使用unset()及时释放不使用的变量尤其是大数组,以便释放内存。
方法尽量静态化。静态方法在程序开始时生成内存,实例方法在程序运行中生成内存,所以静态方法可以直接调用,实例方法要先成生实例,通过实例调用方法,静态速度很快,但是多了会占内存。
尽量少用正则表达式。正则表达式的开销大,使用起来简单,但是性能低。因为正则表达式需要回溯;正则表达式越长,回溯的开销越大,优化正则表达式是需要技术水平的,正则技术不达标,不要乱用正则。
使用单引号。在php中,单引号和双引号是有区别的,作为一种习惯字符串我都用单引号,因为它无需编译。
二. 数据库层面
1. sql查询优化
排查问题过程中,会发现很多性能的瓶颈在于复杂sql的查询太慢。我们可以使用‘EXPLAIN ’关键字来分析复杂sql的运行慢的原因。将‘EXPLAIN’加在sql语句开始的位置,就能分析出这条SQL语句的执行计划,查看该SQL语句有没有使用索引,有没有做全表扫描等等。例如:
EXPLAIN select * from person where dept_id =(select did from dept where dname ='python');
概要描述:
id: 查询序列号,代表执行的先后顺序。
select_type:表示查询的类型。例子中的primary和subquery分别代表最外层查询和子查询。
table:显示这一步所访问数据库中表名称。
type:表示查询计划的连接类型。例子中的ALL代表MySQL将遍历全表以找到匹配的行,没有使用任何索引。这时就说明需要加索引进行优化了。性能由好到差:null > system/const > eq_ref > ref > ref_or_null >index_merge > range > index > all;如果有类型是 ALL 时,表示预计会进行全表扫描(full table scan)。通常全表扫描的代价是比较大的,建议创建适当的索引,通过索引检索避免全表扫描。
possible_keys:指出MySQL能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出。
key:显示MySQL实际决定使用的键(索引),必然包含在possible_keys中。
key_len:表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。不损失精确性的情况下,长度越短越好。
ref:列与索引的比较,表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值。
rows:估算出结果集行数,表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数。
Extra:该列包含MySQL解决查询的详细信息。如果有出现 Using temporary 或者 Using filesort 则要多加关注:
Using temporary,表示需要创建临时表以满足需求,通常是因为GROUP BY的列没有索引,或者GROUP BY和ORDER BY的列不一样,也需要创建临时表,建议添加适当的索引。
Using filesort,表示无法利用索引完成排序,也有可能是因为多表连接时,排序字段不是驱动表中的字段,因此也没办法利用索引完成排序,建议添加适当的索引。
Using where,通常是因为全表扫描或全索引扫描时(type 列显示为 ALL 或 index),又加上了WHERE条件,建议添加适当的索引。
2. 分库分表
基本思想就要把一个数据库切分成多个部分放到不同的数据库(server)上,从而缓解单一数据库的性能问题。
要做分库分表之前,首先要思考一个问题:MySQL 单表数据达到多少时才需要考虑分库分表?网上流传的文章有的说几千万行,有的说几百万行,都没有一个准确的数字。阿里巴巴《Java 开发手册》提出单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。
由于笔者所要优化的数据库单表还达不到这个量级,所以没有进行分库分表的实际操作。但是笔者又深入探索了一个问题:这个500W是怎么推算出来的?
事实上,这个数值和实际记录的条数无关,而与 MySQL 的配置以及机器的硬件有关。因为,MySQL 为了提高性能,会将表的索引装载到内存中。InnoDB buffer size 足够的情况下,其能完成全加载进内存,查询不会有问题。但是,当单表数据库到达某个量级的上限时,导致内存无法存储其索引,使得之后的 SQL 查询会产生磁盘 IO,从而导致性能下降。当然,这个还有具体的表结构的设计有关,最终导致的问题都是内存限制。所以,如果增加硬件配置,可能会带来立竿见影的性能提升。
三. 硬件配置
这种方式不用多介绍了。线上压力顶不住的时候,通常都会立即加机器加配置来抗。久而久之,很多开发人员对加机器加配置产生了依赖。虽然增加硬件配置是最简单粗暴的解决方案,但是作为有追求的技术人员,还是要多从代码层面和数据库层面做深度优化。所以这条解决方案笔者放在最后推荐大家使用。
更多测试知识,就在Qtest之道
界世的你当不
只做你的肩膀
无
360官方技术公众号
技术干货|一手资讯|精彩活动
空·
觉得内容还不错的话,给我点个“在看”呗
使用redis的有序集合,
Zincrby
https://redis.io/commands/zincrby
使用的symfony框架的这个方法,会返回接口名称
$request->getPathInfo()
比如,www.baidu.com/api/login 会返回接口名 /api/login
redis记录如下,
转载于:https://www.cnblogs.com/spectrelb/p/7233688.html
在我们开发api的过程中,有的时候我们还需要考虑单个用户(ip)访问频率控制,避免被恶意调用。
归根到底也就只有两个步骤:
用户访问要统计次数 执行操作逻辑之前要判断次数频率是否过高,过高则不执行
easyswoole中实现Ip访问频率限制
本文章举例的是在easyswoole框架中实现的代码,在swoole原生中实现方式是一样的。
只要在对应的回调事件做判断拦截处理即可。
使用
swooleTable
,储存用户访问情况(也可以使用其他组件、方式储存)使用定时器,将前一周期的访问情况清空,统计下一周期
如以下
IpList
类,实现了初始化Table
、统计IP访问次数、获取一个周期内次数超过一定值的记录<?php /** * Ip访问次数统计 * User: Siam * Date: 2019/7/8 0008 * Time: 下午 9:53 */ namespace App; use EasySwooleComponentSingleton; use EasySwooleComponentTableManager; use SwooleTable; class IpList { use Singleton; /** @var Table */ protected $table; public function __construct() { TableManager::getInstance()->add('ipList', [ 'ip' => [ 'type' => Table::TYPE_STRING, 'size' => 16 ], 'count' => [ 'type' => Table::TYPE_INT, 'size' => 8 ], 'lastAccessTime' => [ 'type' => Table::TYPE_INT, 'size' => 8 ] ], 1024*128); $this->table = TableManager::getInstance()->get('ipList'); } function access(string $ip):int { $key = substr(md5($ip), 8,16); $info = $this->table->get($key); if ($info) { $this->table->set($key, [ 'lastAccessTime' => time(), 'count' => $info['count'] + 1, ]); return $info['count'] + 1; }else{ $this->table->set($key, [ 'ip' => $ip, 'lastAccessTime' => time(), 'count' => $info['count'] + 1, ]); return 1; } } function clear() { foreach ($this->table as $key => $item){ $this->table->del($key); } } function accessList($count = 10):array { $ret = []; foreach ($this->table as $key => $item){ if ($item['count'] >= $count){ $ret[] = $item; } } return $ret; } }
封装完IP统计的操作之后
我们可以在
EasySwooleEvent.php
的mainServerCreate
回调事件中初始化IpList
和定时器<?php public static function mainServerCreate(EventRegister $register) { // 开启IP限流 IpList::getInstance(); $class = new class('IpAccessCount') extends AbstractProcess{ protected function run($arg) { $this->addTick(5*1000, function (){ /** * 正常用户不会有一秒超过6次的api请求 * 做列表记录并清空 */ $list = IpList::getInstance()->accessList(30); // var_dump($list); IpList::getInstance()->clear(); }); } }; }
接着我们在
OnRequest
回调中,判断和统计Ip的访问<?php public static function onRequest(Request $request, Response $response): bool { $fd = $request->getSwooleRequest()->fd; $ip = ServerManager::getInstance()->getSwooleServer()->getClientInfo($fd)['remote_ip']; // 如果当前周期的访问频率已经超过设置的值,则拦截 // 测试的时候可以将30改小,比如3 if (IpList::getInstance()->access($ip) > 30) { /** * 直接强制关闭连接 */ ServerManager::getInstance()->getSwooleServer()->close($fd); // 调试输出 可以做逻辑处理 echo '被拦截'.PHP_EOL; return false; } // 调试输出 可以做逻辑处理 echo '正常访问'.PHP_EOL; }
以上就实现了对同一IP访问频率的限制操作。
具体还可以根据自身需求进行扩展,如对具体的某个接口再进行限流。
最后推荐大家可以用下我开源的一个基于Swoole4.5+研发的PHP框架。该框架基于注解实现了很多好玩的功能,很适合新人快速上手Swoole扩展。
SW-X框架-专注高性能便捷开发而生的PHP-SwooleX框架www.sw-x.cn
dubbo是什么: dubbo是一款分布式框架,解决大量访问请求,属于微服务架构.
微服务架构的优点:降低复杂度,可独立部署,容错等特点.
Dubbo 核心部件
Provider(生产者): 暴露服务的提供方,可以通过jar或者容器的方式启动服务
Consumer(消费者):调用远程服务的服务消费方。
Registry(注册中心): 服务注册中心和发现中心。
Monitor: 统计服务和调用次数,调用时间监控中心。
Container:服务运行的容器。
**dubbo核心底层技术:**实现用的是Hessian,相比WebService,Hessian更简单、快捷。采用的是二进制RPC协议,因为采用的是二进制协议,所以它很适合于发送二进制数据。
dubbo生产者和消费者的区别:
dubbo是一款分布式的框架,最主要的核心是将项目独立部署,从而减轻服务器的压力,dubbo的生产者主要是发布接口,连接数据库,dubbo的消费者主要是调用接口,对接客户端;
下面分别介绍生产者与消费者项目的搭建:
生产者:
生产者主要包含model,service以及mapper(dao)层;
1.需要的jar包:(springboot项目用的是2.0.7版本)``` <!--dubbo 依赖 --> <dependency> <groupId>com.alibaba.spring.boot</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency> <!-- zookeeper client依赖 --> <dependency> <groupId>com.101tec</groupId> <artifactId>zkclient</artifactId> <version>0.10</version> </dependency> ```
2.在application.properties中需要 配置dubbo服务端生产者
在application.properties中配置
#配置dubbo服务提供者
#服务名称
spring.dubbo.application.name=provider
spring.dubbo.server=true
#注册中心地址
spring.dubbo.registry.address=zookeeper://127.0.0.1:2181
#dubbo 协议
spring.dubbo.protocol.name=dubbo
spring.dubbo.protocol.port=20880
3.配置本地zookeeper
在zookeeper/conf/zoo.cfg中配置数据目录和日志目录位置,注意目录需要手动创建
#数据位置 (绝对路径)
dataDir=D:\zookeeper-3.3.6\data
#日志位置 (绝对路径)
dataLogDir=D:\zookeeper-3.3.6\logs
* 本地启动zookeeper:在zookeeper/bin目录下文件地址栏中输入cmd,执行zkServer回车即可启动zookeeper
4.构建项目:
4.1: model实体类import java.io.Serializable; @Data public class **Order implements Serializable** { private Integer orderId; // 主键id private String orderName; // 订单名 } * Order implements Serializable 要实现序列化 所以实体类一定要实现Serializable . 4.2 service接口类 **必不可少的 因为要发布接口**; import com.model.Order; import java.util.List; public interface OrderService { // 新增 void saveOrder(Order order); // 查询 List<Order> selectOrderList(); // 删除 void deleteOrderById(Integer orderId); // 修改 回显 Order selectOrderById(Integer orderId); // 修改 void updateOrder(Order order); } 4.3 实现类 import com.alibaba.dubbo.config.annotation.Service; import com.mapper.OrderMapper; import com.model.Order; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.Date; import java.util.List; @Service(interfaceClass = OrderService.class) @Component public class OrderServiceImpl implements OrderService { @Resource private OrderMapper orderMapper; // 新增 @Override public void saveOrder(Order order) { order.setCreateTime(new Date()); orderMapper.saveOrder(order); } // 查询 @Override public List<Order> selectOrderList() { return orderMapper.selectOrderList(); } // 删除 @Override public void deleteOrderById(Integer orderId) { orderMapper.deleteOrderById(orderId); } // 修改 回显 @Override public Order selectOrderById(Integer orderId) { return orderMapper.selectOrderById(orderId); } // 修改 @Override public void updateOrder(Order order) { orderMapper.updateOrder(order); } } ** 4.3.1 @Service(interfaceClass = OrderService.class) @Service要导的包是alibaba.dubbo.config.annotation.Service, (interfaceClass =接口名.class) 4.3.2 要在类上加入 @Component注解 是spring注入的;
5. 项目构建的示例图:
5.1 需要在类加载文件配备的有:
5.1.1 @ComponentScan(basePackages = “com.*”) 由于类加载文件与所写的项目类不在同一级 ,需配置该注解,使其扫描到该项目中的内容;
5.1.2 @EnableDubboConfiguration:在application启动类中加上开启dubbo配置注解
5.1.3 @MapperScan(basePackages = “com.mapper”) 扫描mapper下的xml文件
springboot + dubbo + zookeeper项目搭建的生产者项目已经搭建完成;并且以上传需要的jar包及zookeeper压缩包
消费者请查看:https://blog.csdn.net/weixin_43813343/article/details/88198974