linux服务器跨服务器备份文件

在工作中经常有遇到需要脚本自动化同步文件的地方,比如数据库异地备份。假设有两台机子A(192.168.16.218)和B(192.168.16.117),需要能够让A免密码连接B。

A机操作

生成密钥对

ssh-keygen -t rsa   

先把公钥id_rsa.pub传到B主机(192.168.16.117)上

scp id_rsa.pub root@192.168.16.117:~/.ssh/id_rsa.pub_temp

B机操作

将id_rsa公钥的内容添加到.ssh目录下的authorized_keys文件,记得以追加的方式添加,以免将别的公钥覆盖,若文件不存在则新建

cat id_rsa.pub_temp >> authorized_keys

跨服务器备份文件

A机编辑shell脚本

#!/bin/sh

time1=$(date "+%Y%m%d%H%M%S")
cd /data/sys_bak
mkdir  $time1

#bak
ssh 192.168.16.117 tar -czvf  /opt/test/test2/cms$time1.tar.gz    /opt/test/cms/
scp -P 22 -r root@192.168.16.117:/opt/test/test2/cms$time1.tar.gz /data/sys_bak/$time1
ssh 192.168.16.117 rm -f  /opt/test/test2/cms$time1.tar.gz

原生php操作mysql简单封装

有时可能会遇到用原生php写一些小脚本的场景,可以简单封装下sql语句写起来更方便

class mysql
{
    private $host = 'localhost';
    private $name = 'root';
    private $pass = 'root';
    private $database = 'test';
    private $port = 3306;
    private $mysql;

    /**------------------------------------------------
     *    构造函数 使用PHP内置的mysqli类对数据库进行连接
     *------------------------------------------------*/
    public function __construct()
    {
        $this->mysql = @new mysqli($this->host, $this->name, $this->pass, $this->database, $this->port);
        if($this->mysql->connect_errno){
            exit("数据库连接错误 {$this->mysql->connect_error}");
        }
    }

    /**------------------------------------------------
     *    根据表名, 字段名, 值 对数据表进行插入数据
     *    成功返回插入记录的ID 否则返回0
     *------------------------------------------------*/
    public function insert($table, $data)
    {
        //解析传递过来的字段 转为字串
        $field = '';
        $worth = '';
        $i = 1;
        $length = count($data);

        foreach ($data as $key => $value) {
            if($length != $i){
                $field .= $key . ',';
                if(gettype($value) == 'string'){
                    $worth .= "'$value',";
                }else{
                    $worth .= $value;
                }
            }else{
                $field .= $key;
                if(gettype($value) == 'string'){
                    $worth .= "'$value'";
                }else{
                    $worth .= $value;
                }
            }
            $i++;
        }

        $sql = "insert into {$table}({$field})values($worth)";
        $result = $this->mysql->query($sql);
        //获取返回的ID
        return $this->mysql->insert_id;
    }

    /**------------------------------------------------
     *    根据表名 where条件的键值 对数据记录进行删除
     *    成功返回 true 否则返回 false
     *------------------------------------------------*/
    public function delete($table, $key, $value)
    {
        if(gettype($value) == 'string'){
            $value = "'{$value}'";
        }
        $sql = "delete from {$table} where {$key} = {$value}";
        return $this->mysql->query($sql);
    }

    /**------------------------------------------------
     *    根据表名 where条件的键值 对数据记录进行查询
     *    成功返回数组 否则返回空数组
     *------------------------------------------------*/
    public function get($table, $condition)
    {
        $where = '';
        foreach ($condition as $key => $value) {
            if(gettype($value) == 'string'){
                $value = "'{$value}'";
            }
            $where .= " $key = $value and";
        }
        $where = "where " . mb_substr($where, 0, mb_strlen($where) - 3);
        $sql = "select * from {$table} {$where}";
        $sql = trim($sql);
        $result = $this->mysql->query($sql);
        $data = [];
        while ($res_data = $result->fetch_assoc()) {
            var_dump($res_data);
            $data[] = $res_data;
        }
        return $data;
    }

    /**------------------------------------------------
     *    根据表名 字段及值 条件 修改记录
     *    成功返回true 否则返回false 
     *------------------------------------------------*/
    public function update($table, $data, $where)
    {
        $field = '';
        foreach ($data as $key => $value) {
            if(gettype($value) == 'string'){
                $value = "'{$value}'";
            }
            $field .= "{$key} = {$value}, ";
        }
        $field = mb_substr($field, 0, mb_strlen($field) -2);

        //条件
        $worth = '';
        foreach ($where as $key => $value) {
            if(gettype($value) == 'string'){
                $value = "'{$value}'";
            }
            $worth .= "{$key} = {$value} and ";
        }
        $worth = 'where ' . mb_substr($worth, 0, mb_strlen($worth) - 5);
        $sql = "update {$table} set {$field} {$worth}";
        return $this->mysql->query($sql);
    }

    /**------------------------------------------------
     *    将传递过来的值转义并返回
     *------------------------------------------------*/
    public function escape($string)
    {
        return $this->mysql->escape_string($string);
    }

    /**------------------------------------------------
     *    析构函数 关闭数据库连接
     *------------------------------------------------*/
    public function __destruct()
    {
        $this->mysql->close();
    }
}

调用

//引入 MySql 类
include_once('mysql.php');

$mysql = new mysql();

//对数据记录进行删除操作
$res = $mysql->delete('表名', '条件字段名', '值');

//对数据表进行插入数据操作
$res = $mysql->insert('表名', [条件字段及值 如 'name' => 'root'...]);

//对数据表进行查询操作
$res = $mysql->get('表名', [条件字段及值 如 'name' => 'root'...]);

//对数据记录进行修改更新
$res = $mysql->update('表名', [条件字段及值 如 'name' => 'root'...]);

CentOS下配置MySQL允许root用户远程登录

# root用户登录
$ mysql -u root -p

# 切换到mysql这个数据库
mysql> use mysql;

# 查看root用户配置
mysql> select host,user from user where user='root';

如果查询结果中不包含以下记录,请添加,否则请忽略次步骤

host    user
%   root
mysql> update user set host = '%' where user = 'root' and host='127.0.0.1';

grant all privileges on *.* to 'root'@'%' identified by '123456' with grant option;

 flush privileges;

Laravel集成AWS S3 SDK使用Minio对象存储

毕业第一年的假期,碰上了新冠肺炎的疫情,在家休息了一个月。期间募集了幻想游戏的同学们向疫情捐款,同时也和朋友研究了Minio对象存储,以改善幻想游戏目前杂乱的存储方式。

旧的存储方式

幻想游戏的项目部署在阿里云的云端服务器,由于我们一直不以营利为目的,因此也没有租用配置过高的服务器,面对2T多的游戏文件,存储在云服务器是一件很耗钱的事。幻想游戏的游戏文件一直存放在另一位站长家一台弃用的电脑上,搭建为服务器,提供下载功能。
这样面临的问题就是,项目服务器和文件服务器不部署在一起,维护起来比较麻烦,并且文件服务器受限于带宽、停电、硬盘损坏的问题,非常不方便。期间我们也将文件放入阿里云的OSS服务上,但无奈费用实在过高,最终放弃。

Minio

文档地址

Minio是一个开源的对象存储服务。

使用phpsocket.io开发web即时通讯系统(在线客服)记录

整理下目录 明天周六有时间写

开始

我司是做B端SaaS业务的,移动端比较有代表性的项目是办公微服务,下面涵盖了员工食堂、便利店、物业公司的入驻办公楼服务。既然有B端用户又有C端用户,那就避免不了两个端之间有交流,于是需要开发一款类似淘宝阿里旺旺功能的B2C的即时通讯客服系统。使用的websocket技术是基于workman开发的phpsocket.io。

业务场景分析

业务场景还是相对比较复杂的,因为我们公司的C端相当于是写字楼租户,有自己的后台,相当于说C端有前台、后台账号两个身份。B端也有前台、后台账号。排列组合,近B端与C端通信就有四种情况需要考虑。

第一版我们只考虑C端前台账号和B端所有的账号通信问题。不考虑C2C、B2B的情况。由于是客服系统,需要考虑到B端子账号所有有权限的账号都可被看做是客服,存在一个客服分配的情况。

数据表设计

数据表这块走了一些弯路,之前只用了一张表来存储消息记录。主流的C2C私聊的即时通讯一张表也许就够用了,但是这里分为B端和C端,且B端是店铺为单位的,后面的客服不是唯一的,一张表的话,逻辑不是很清晰。于是我对B端、C端的消息记录分开存放。

im_message_c2b
CREATE TABLE `im_message_c2b` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `uuid` char(36) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '全局唯一uuid',
  `store_id` int(11) unsigned NOT NULL COMMENT '店铺id',
  `store_name` varchar(100) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '店铺名称',
  `type` smallint(3) unsigned NOT NULL DEFAULT '0' COMMENT '消息类型(0-系统消息 1-用户消息)',
  `chat_id` int(11) DEFAULT NULL COMMENT '对话id',
  `sender_from` tinyint(1) DEFAULT NULL COMMENT '发送人 1前台  2后台',
  `sender_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '消息发送者ID(前台账号ID,若为系统消息此字段为0)',
  `sender_name` varchar(100) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '消息发送者名称(仅用于前台展示,不做字段关联)',
  `subject` varchar(255) NOT NULL DEFAULT '' COMMENT '消息标题(仅支持文字)',
  `content_type` int(11) NOT NULL DEFAULT '1' COMMENT '消息类型 1文字 2图片 3文件',
  `content` text COMMENT '消息内容(可支持文字、图片、表情、链接等)',
  `receiver_from` tinyint(1) DEFAULT NULL COMMENT '接收人 1前台 2后台',
  `receiver_id` int(11) unsigned DEFAULT NULL COMMENT '消息接收者ID(前台账号ID)',
  `receiver_name` varchar(100) CHARACTER SET utf8 DEFAULT '' COMMENT '消息接收者名称(仅用于前台展示,不做字段关联)',
  `disabled` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否删除(0-否 1-是)',
  `add_time` int(10) unsigned DEFAULT NULL COMMENT '发送时间(unix时间戳)',
  `state` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '状态 默认0未发送  1发送成功  2发送失败',
  `is_read` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '默认0未读  1已读',
  PRIMARY KEY (`id`) USING BTREE,
  KEY ```receiver_id``` (`receiver_id`) USING BTREE,
  KEY ```add_time``` (`add_time`) USING BTREE,
  KEY ```mixkey``` (`sender_id`,`receiver_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=164 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='C端发给B端的消息';
im_message_b2c

CREATE TABLE `im_message_b2c` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `uuid` char(36) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '全局唯一uuid',
  `store_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '店铺id',
  `store_name` varchar(100) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '店铺名称',
  `type` smallint(3) unsigned NOT NULL DEFAULT '0' COMMENT '消息类型(0-系统消息 1-用户消息)',
  `chat_id` int(11) DEFAULT '0' COMMENT '对话id',
  `sender_from` tinyint(1) DEFAULT '0' COMMENT '发送人 1前台  2后台',
  `sender_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '消息发送者ID(前台账号ID,若为系统消息此字段为0)',
  `sender_name` varchar(100) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '消息发送者名称(仅用于前台展示,不做字段关联)',
  `subject` varchar(255) NOT NULL DEFAULT '' COMMENT '消息标题(仅支持文字)',
  `content_type` int(11) NOT NULL DEFAULT '1' COMMENT '消息类型 1文字 2图片 3文件',
  `content` text COMMENT '消息内容(可支持文字、图片、表情、链接等)',
  `receiver_from` tinyint(1) DEFAULT '0' COMMENT '接收人 1前台 2后台',
  `receiver_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '消息接收者ID(前台账号ID)',
  `receiver_name` varchar(100) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '消息接收者名称(仅用于前台展示,不做字段关联)',
  `disabled` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否删除(0-否 1-是)',
  `add_time` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '发送时间(unix时间戳)',
  `state` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '状态 默认0未发送  1发送成功  2发送失败',
  `is_read` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '默认0未读  1已读',
  PRIMARY KEY (`id`) USING BTREE,
  KEY ```receiver_id``` (`receiver_id`) USING BTREE,
  KEY ```add_time``` (`add_time`) USING BTREE,
  KEY ```mixkey``` (`sender_id`,`receiver_id`) USING BTREE,
  KEY ```type``` (`type`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=682754 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='B端发给C端的消息';

这里着重说明的一点是,为了让通信的uid逻辑性一致,receiver_id、sender_id都是取前台账号(后台账号与前台有对应关系),用receiver_from、sender_from区分是前台还是后台,uid用front、back前缀区分是前台还是后台。

另外还有记录会话id的表、聊天媒体文件的表,在这就不做记录了。

主要逻辑处理

  • 聊天列表

    • 对于C端聊天列表,肯定是group by store_id,按照店铺去分组。

    • 按照一般的逻辑,首先列表页排序肯定是按最后一次发送消息的时间戳倒序去排序的。并且要显示最新一条与店铺对话的消息。

    • 这里就很好玩了,因为mysql语句执行顺序group by是在order by前面的,所以兼顾这两个的用法是这里的重点。为实现上述逻辑,我的sql如下:

      SELECT * from (
          select * from (
              select * from (SELECT store_id,type,store_name,receiver_id as chat_user_id,receiver_name as chat_user_name,content,add_time FROM im_message_c2b WHERE sender_id = {$data['user_id']} AND type = 1 order by add_time desc limit 10000) as a group by store_id 
              union all 
              select * from (SELECT store_id,type,store_name,sender_id,sender_name,content,add_time FROM im_message_b2c WHERE receiver_id = {$data['user_id']} order by add_time desc limit 10000) as b group by b.store_id
          ) c order by add_time desc limit 10000
      ) as d GROUP BY store_id ORDER BY add_time desc LIMIT $data[offset],$data[limit]
    • (为什么加limit,因为mysql5.7后就得加,不加group by还是按最原始的排序)

    • B端对话逻辑列表逻辑类似,不过是以发送人id分组的。

  • 聊天记录

    • C端聊天记录:以店铺id分组,查找出与当前店铺所有聊天记录。
    • B端聊天记录:查看店铺与用户所有聊天记录(可能存在分配到其他客服,另一个客服要知道之前聊的内容)。
    • 聊天记录的显示涉及到一个问题,就是消息间隔大于三分钟的话,要显示一个发送时间。我这边想到的方案是,两两比对消息的时间戳,如果大于3分钟,则插入一条时间戳记录,回头把所有的数据放在json里面返给前端。要注意的是,遍历数组同时又插入数组,很容易导致条件判断的错误。贴一下我写的算法。
      // 两两比对,间隔大于两分钟插入时间显示
      $i = 0; 
      //$messageLog['rows']是消息数组
      foreach ($messageLog['rows'] as $k => $v) {
          $count = count($messageLog['rows']);
          if ($i < $count - 1) {
              $head_time = $messageLog['rows'][$i]['add_time'];
              $next_time = $messageLog['rows'][$i+1]['add_time'];
              if ($head_time - $next_time >= 120) {
                  $arr = array(
                      ['time' => $head_time,'mark' => 'middle']
                  );            
                  // 如果间隔大于2min,插一条记录显示时间
                  array_splice($messageLog['rows'], $i+1, 0, $arr);
                  $i += 2; //插入记录了,跳过时间显示这条数据的比对,永远只比对原始的消息时间戳
              } else {
                  $i ++;
              }
          }
      }
  • 标记为已读

    • 当点开聊天页面的时候,肯定要标记双方的消息记录为已读。
    • 当发送消息的时候,当前时间戳之前的双方消息,也要标记为已读。
聊天列表
聊天记录
标记为已读
分配客服

离线消息如何处理

群发消息-大数据量处理

幻想游戏网被攻击实录及总结

最近近一个月幻想游戏网都处于被攻击的状态,每天靠业余时间去维护、防御也搞得焦头烂额。昨晚终于恢复所有功能正式上线,整个过程大部分是我来搞的,应大佬要求,写一篇防御攻击的一些分析方法和总结。

前奏

11月的时候,网站偶尔会访问很慢,一开始也没放在心上,因为是偶尔出现的情况就以为是并发引起的问题,就没有太在意,准备有时间好好检查下程序的瓶颈(最后发现我们网站就没那么大并发-_-难过ing)。

最后有用户反映,简单的一个签到都要等十几秒。看下服务器负载,高的可怕,但当时对这方面经验真的很少,还以为是并发的问题,于是准备去优化下幻想游戏存在的性能问题。

优化

幻想游戏一直使用的是关系型数据库(MySQL),对于数据量比较大时候,读写还是比较消耗IO开销的。最开始做网站的时候,还是个学生,数据量也小,并没有考虑太多数据库层面的事情。一年时间,有的数据表数据已经几十万行了,竟然还没有建索引,着实有点尴尬,于是在数据库层面做了优化。

首先是MySQL的索引,按照经理的说法,每个表都至少有两个索引,虽然会对写操作有影响,但是带来的益处远大于弊端。我对所有读操作比较频繁的表都建立了索引,索引这块涉及的知识也还是很多的,“最左原则”、联合索引等等的(第一次面试的时候就在这块狠狠的被打击了)。

然后是签到数据,理论上来说,做操作的时候只要有用户前一天的数据就可以了(目前的逻辑是读取用户前一天的签到天数,如果是连续就累加,不连续就从1开始),所以之前的数据基本上是没用了,写了个脚本,清理了一个月前的签到数据(哈哈,毕竟感觉离做大数据分析还有点远)。

最后是redis,网站有排行榜功能,目前是实时更新的,像下载表、签到表什么的,数据已经几十万条,不停的读写,消耗应该说还是很大的,于是将排行榜的数据全部放入了redis里面,并配合Linux的crontab定时任务和Laravel的任务调度每周对排行榜进行一次更新。

攻击

本以为做了以上优化,负载会降下去了,现实却给了我一巴掌(最开始也说,网站目前的流量根本到不了优化性能那一步)。负载还是居高不下,网站基本处于瘫痪状态,把网站日志文件下载下来,准备分析一下。

结合Excel的分析能力,发现很多ip访问异常频繁,初步断定是ddos攻击,或者是有人在爬取我们网站。然后,安排,在程序基类写了一个限制ip访问函数,如果一分钟之内访问大于20次,就不让访问了,上线后果然负载降了下来,偶尔会满,但很快就降下来了。心里还挺美的,当时时间是下午五点多。

晚上和同事吃了顿饭,回到家九点多,打开宝塔面板,负载到了四五十···,一下子就懵了,看来对方是加大了力度,而且ip都很分散,基本无法判定是攻击还是普通用户。等到第二天到公司,发现了更可怕的事,由于匆忙上线,redis没有设置密码,被对方植入了挖矿程序(看到了一个shell脚本,查了里面关键的变量名,发现是和挖矿相关的东西)。于是搜相关资料,并在服务器全局搜了相关关键字,没有发现相关的脚本,而且服务器CPU正常。设置了redis密码(强度要高,平时感觉没啥,真被攻击就凉了),就继续搜防攻击的方法了。

防御

首先是预防一下对方是否是大面积的ddos攻击(其实能判定不是针对ip攻击的),还是对ip进行了隐藏,用的是加速乐提供的cdn隐藏服务器真实ip的服务,具体操作就不写了。

其次是分析日志,把一下访问频繁的ip直接禁掉。

但其实作用微乎其微,攻击还在继续。我们继续排查问题,我们把www开头的域名解析给停掉之后,发现负载正常了,所以判定对方攻击的www开头的域名,日志里面还有很多莫名的post请求(一看就不是网站内部路由),无奈我们先暂停了www的解析。

烦人的搜索引擎爬虫

之后我们一直用http://hxyou.cn 对用户进行开放,但也会存在负载爆满的情况,但不像之前那样高的离谱,不确定是不是又遭受到攻击。于是把网站日志下载下来,继续分析。

分析主要是看IP访问的频率以及请求的UA,可以看到有很多都是搜索引擎的爬虫,比如BaiduSpider,bingbot,YiSouSpider。但是也没听过爬虫能把网站爬垮的新闻。经过不断排查,发现有一个请求的UA出现的非常频繁,bytespider。分析得到的结果是今日头条的爬虫暴力爬取导致负载过高(真的很流氓),并且阿里系的神马搜索也很频繁,以及微软的必应。

既然这些搜索引擎这么流氓,不好意思,我也不需要您收录我们了。我在Nginx的配置文件里放了如下代码:

        #禁止Scrapy等工具的抓取
        if ($http_user_agent ~* (Scrapy|Curl|HttpClient)) 
        {
             return 403;
        }
        #禁止指定UA及UA为空的访问
        if ($http_user_agent ~ "FeedDemon|bingbot|JikeSpider|Indy Library|Alexa Toolbar|AskTbFXTV|AhrefsBot|CrawlDaddy|CoolpadWebkit|Java|Feedly|UniversalFeedParser|ApacheBench|Microsoft URL Control|Swiftbot|ZmEu|oBot|jaunty|Python-urllib|lightDeckReports Bot|YYSpider|DigExt|YisouSpider|HttpClient|MJ12bot|heritrix|EasouSpider|Ezooms|^$" ) 
        {
             return 403;             
        }
        #禁止非GET|HEAD|POST方式的抓取
        if ($request_method !~ ^(GET|HEAD|POST)$) 
        {
            return 403;
        }

这样,可以屏蔽你不想要爬取的搜索引擎爬虫。

同时也在网站根目录下面加了robots.txt文件,定义了一下,除了百度、谷歌,都不如爬取了。

监控

平时还有工作,也不能时时刻刻盯着服务器的负载,但是宝塔的监控推送好像是收费的,又不想掏钱,只能自己写一个了,思路如下:

利用php的sys_getloadavg()的函数读取当前服务器负载(这个函数只能在linux下用),把它写成laravel的命令,可以执行命令行返回负载值。然后结合linux的crontab定时任务和laravel的任务调度,每隔一分钟执行以下这条命令,如果负载大于1,就发邮件给我。

(小声bb:laravel的任务调度语法真优雅)

$schedule->command('SendLoadByEmail')->everyMinute()->between('8:00', '22:00');

结尾

网站终于恢复上线了,流失了不少用户,我们也给用户发放了积分、vip补偿,但是无法否认的是,这次真的是被对方吊打,幻想游戏网也是元气大伤。不过也算是给自己交了学费,积累了不少网络安全的知识(虽然很浅薄),要享受分析、解决问题的过程,也是一次很宝贵的经历了。

对于网络安全的一些研究和思考

一直感觉黑客、网络攻击这种词离我很远,但这次是真的遇到了,被打得惊慌失措,毫无还手之力。

开始

接着上篇优化负载的博客,等我把一些优化事宜做好之后,服务器负载还是很不正常,最高可以飙到50。我觉得事情可能并不是用户产生的并发那么简单。

加上之前ssh的暴力破解,我越来越感觉这是一起恶意的网络攻击。说实话之前对这块的知识几乎是为0的,但还是得想办法去防御。

防御

首先ssh暴力破解的问题,立马修改了ssh的端口,并且在不需要ssh登录时,关闭ssh。

然后查看了网站的日志,有很多国外的ip来访问,甚至有的访问记录非常奇怪,猜想应该是向我们这边发送post请求,尝试破解程序上的漏洞。结合Excel分析了一下网站日志,有的ip访问甚至达到数百次,一看就不正常,于是立马结合redis写了一个限制登录频率的方法放在程序控制器的基类,每分钟限制每个ip最多访问20次。这个上线后,负载立马降下来了,偶尔负载还会爆满,但慢慢负载就掉下去了。心想,至少不会长时间负载都爆满了,也算是一定程度解决了问题。

晚上和同事聚餐,到家九点多,查看了一下服务器负载,可能对方加大了攻击力度,服务器负载又满了,暂时想不到解决方案,遂直接没管了。第二天查看redis数据库,想看一下ip访问的记录,看到了cpuminerpool的字样,感觉不太正常,一搜,果然,出现了挖矿的相关信息。原来是因为昨天仓促上线限制ip访问频率,redis数据库没有设置密码,端口又开放了,对方直接连接了我的redis(设置一个强度高的密码很重要!!!)。登录服务器,没有太大异常,但还是很谨慎,和朋友商量晚上把网站迁移到另一个服务器,把这台服务器的系统重做一下。

但是今天的问题很严重,负载一直飚在30-50.自己的武器库已经没剩下多少了,于是去搜相关资料,首先明确ddos和cc攻击的区别,ddos一般针对的是ip,基本上是毁灭性的打击;cc攻击一般是针对域名。于是有了以下思路:

  1. 利用CDN技术隐藏服务器真实ip(针对ddos)
  2. 将域名DNS解析,境外的ip解析到127.0.0.1

具体等实施后继续更新。


后续

根据以上思路,周末做了些调整。

  1. 利用cdn隐藏服务器真实域名,我用的加速乐提供的免费的cdn服务。具体教程参考cdn隐藏服务器真实域名这篇教程。实施以后,ping幻想游戏的域名,ip的确不是真实域名了。
  2. 将域名dns解析,境外ip解析到本地,这个没法实测,但是可以确定对方攻击的是我们的主域名。将域名解析到本地后,负载马上降下来,如果改为自己真实ip,负载立马上去。对于对方请求频率限制的也不理想,最后不得已我们准备换一个域名,先观察一段时间。

最后在道德层面谴责一下这些黑客。

phpsocket.io推送json数据给前端被转义解决方案

最近在做一个功能,用到了phpsocket.io,可以主动推送数据到客户端,不需要客户端用ajax轮询,减小了资源的消耗。当数据通过cURL推送到前端时,json数据被转义。

前端接收的数据如下(转换过json的数据)

{&quot;left&quot;:{&quot;order_ids&quot;:[&quot;2092&quot;,&quot;2069&quot;,&quot;698&quot;],&quot;type&quot;:&quot;left&quot;},&quot;right&quot;:{&quot;oreder_ids&quot;:[&quot;1024&quot;],&quot;voice_str&quot;:&quot;\u8bf7\u4ee5\u4e0b\u987e\u5ba2\u5230\u51fa\u9910\u53e3\u53d6\u99101024\u53f7&quot;,&quot;type&quot;:&quot;right&quot;}}

解决方案

  1. 首先对后端传输的数据用urlencode处理。
  2. 前端处理数据:
    var ndt = decodeURIComponent(dt);
    var nndt = unescape(ndt.replace(/\u/g, ‘%u’));
    最后再转json,拿到想要的数据

我是如何优化幻想游戏服务器负载的

最近幻想游戏的服务器响应速度极其慢,用户签到可能要等十几秒才能成功。然而也么得钱买更高配置的服务器,只能在程序上想想办法。

分析问题

  1. 网站负载经常飙到100%,服务器配置很低,cpu是单核的,理论上平均负载在2以内才行,然后负载有时能飚到7。但是cpu、内存还算正常。
  2. 一般来说,网站大的开销应该是出在数据库上,现在数据库也有十几张表,类似游戏表、用户表、签到表、下载记录表、积分记录表、搜索记录表数据量非常大。有的表数据量已经几十万了,平时都没注意到。
  3. 网站有各种排行榜数据,实时更新的,都是从各个表读取计算来的,对数据库的读写很频繁。

解决问题

  1. 首先拿MySQL数据库开刀,像下载记录表,已经60多万条数据了,竟然连索引都没有,只能说自己很粗心啊。实习的时候,经理对我们说,每个表至少都要建立2-3个索引,索引的利远远大于弊。第一步,先给各个表建立索引,初步基本是给where条件查找的字段建立了索引。
  2. 然后是排行榜,排行榜其实没有实时更新的必要的,于是,上redis。
    2.1 用户的积分和签到天数排行榜,直接用redis的有序列表,简单粗暴:

    public function rankBySignin(){
    //如果没放在redis,加进去
        if (Redis::zcard('signinRank') == 0) {
            $list = User::where('isadmin','!==',1)->where('state',1)->orderBy('maxsignins','desc')->orderBy('viplevel','desc')->orderBy('coin','desc')->take(100)->get();
            foreach ($list as $k => $v) {
                Redis::zadd('signinRank',$v->maxsignins,$v->name);
            }
        }
        $rank = Redis::zrevrange('signinRank',0,-1,'WITHSCORES');    
        return view('web.rank.rankbysignin',compact('rank'));
    }

    2.2 对于游戏的排行榜,涉及的属性比较多,用了redis的List数据类型,将数据转为json存在redis里面。代码比较长,就不贴在这里了。
    至此把排行榜数据都缓存到redis了,写了个定时任务,每周更新一下数据。但是上线的时候composer安装predis的时候,被墙的厉害,没有办法,先把这块的代码注释了,等能安装了再上线。

  3. 将一些无用数据,直接干掉。类似签到表,我们目前的需求是记录用户连续签到天数,以发放积分奖励,理论上,1天前的数据都是可以没用的,所以我直接把一个月之前的数据全删了(其实不喜欢这种方式)。

后续

优化过后,负载状态改善了很多,但是并发过大,还是会扛不住,所以优化工作还得继续。后续准备把需要读取的数据且非实时的,全缓存到redis,将游戏的访问量下载量、访问量放在redis操作。但其实能发现,程序上的优化还是有限的,不过可以提高自己优化项目的能力,去分析问题再解决,感觉还是很爽的。等实在扛不住的时候再去阿里云“打钱打钱”。


更新

优化后,负载是有一定程度的降低。但是偶尔还是会飙到很高,高到一个很不正常的值,最后经确定,是被攻击了。分析一下整个防守攻击的过程——对于网络安全的一些研究和思考

关于mysql时间的一些操作

获取今天的数据(时间戳)

SELECT order_id,order_id as dining_no, add_time, mobile, is_dinein, is_shop, amount_fee,detail_address FROM order_info WHERE add_time >= UNIX_TIMESTAMP(curdate()) and add_time < UNIX_TIMESTAMP(curdate() + interval 1 day)

  • SELECT UNIX_TIMESTAMP(curdate()) 可返回当日0点的时间戳
  • SELECT UNIX_TIMESTAMP(curdate() + interval 1 day) 可返回明天0点的时间戳

这里不用between and,是因为between and会包括边界值,而not between的范围是不包含边界值的。


UNION ALL会合并两个查询结果的集(字段数要一样)。
UNION也会合并两个结果的集,并过滤重复的值。
UNION ALL效率高于UNION。