夏日小草

夏日小草 查看完整档案

杭州编辑杭州电子科技大学  |  Unknown 编辑unemployment  |  unemployment 编辑 homeway.me/ 编辑
编辑

个人动态

夏日小草 发布了文章 · 2015-10-22

Nginx流量拦截算法

nginx-lua-static-merger

0x00.About

电商平台营销时候,经常会碰到的大流量问题,除了做流量分流处理,可能还要做用户黑白名单、信誉分析,进而根据用户ip信誉权重做相应的流量拦截、限制流量。

Nginx自身有的请求限制模块ngx_http_limit_req_module、流量限制模块ngx_stream_limit_conn_module基于令牌桶算法,可以方便的控制令牌速率,自定义调节限流,就能很好的限制请求数量,然而,nginx.conf问题还是在于无法热加载。

之前做过的流量限制方案,《Nginx+Lua+Redis访问频率控制》,原理是动态的基于ip,实现简单的漏桶算法,限制访问频率。

这里的话,就简单分析下流量限制算法:漏桶算法、令牌桶算法、滑动窗口等在Nginx+Lua中如何动态绑定uri,动态设定rate实现。

0x01.Leaky Bucket Algorithm

漏桶算法可以很好地限制容量池的大小,从而防止流量暴增。如果针对uri+ip作为监测的key,就可以实现定向的设定指定ip对指定uri容量大小,超出的请求做队列处理(队列处理要引入消息机制)或者丢弃处理。这也是v2ex对流量拦截的算法,针对uri+ip做流量监测。

Leaky Bucket Algorithm

漏桶算法实现上来说,就是建立一个队列,在Redis中以uri:ip作为key,队列上实现FIFO,在请求的前奏实现插入,请求完成后实现删除。

实现方法是在Nginx发送http数据给用户后,通过ngx.eof()关闭TCP协议,做其他操作,可以参见请求返回后继续执行

下面是部分代码:

local _M = { _VERSION = "2015.10.19", OK = 1, BUSY = 2, FORBIDDEN = 3 }

function _M.do_list(red, uri, key, size, rate)
    local ok, err = red:expire(uri .. ":" .. key, size)
    if not ok then
        ngx.log(ngx.WARN, "redis set expire error: ", err)
        return nil
    end
    local ok, err = red:rpush(uri .. ":" .. key, ngx.time())
    if not ok then
        ngx.log(ngx.WARN, "redis rpush error: ", err)
        return nil
    end
    local res, err = red:lrange(uri .. ":" .. key, -(size * rate), -1)
    if not ok then
        ngx.log(ngx.WARN, "redis lrange error: ", err)
        return nil
    end
    if #res < (size * rate) or res[#res] - res[1] < size then
        return _M.OK
    end
    return nil
end

漏桶算法优点很明显,简单、高效,能恰当拦截容量外的暴力流量。

但缺点也明显,无法对流量做频率处理,比如桶size大小设置范围内,进行并发攻击依然能大流量并发效果,桶容量不可以过小,否则容易卡死正常用户。

0x02.Token Bucket Algorithm

令牌桶算法通过发放令牌,根据令牌的rate频率做请求频率限制,容量限制等。

Token Bucket Algorithm

  • 系统根据rate(r/s)频率参数向指定桶中添加token,满则保持,不添加

  • 当用户请求Nginx时候,分析uri是否需要限制流量,限制则执行令牌桶算法

  • 如果桶满了,则请求通过,消耗令牌一枚;如果请求Redis发现key不存在,则通过size装满令牌桶;如果桶内令牌空,则废弃或等待流量。

Nginx + Lua 模型中实现必然不能跑一个程序添加令牌了,这个时候需要在分析令牌时候,通过计算时间间隔一次性添加完令牌桶内令牌。具体算法是:rate * time_distance = token_count令牌数量, if token_count > size 桶容量, token_count = size。

实现的存储结构是用Hash哈希存储 uri:ip -> token_count,字段通过EXPIRE设定过期时间,达到长时间不访问清除桶数据效果。

桶的大小、请求的频率限制用Redis哈希表存储,不存在则默认不做流量拦截。

用户黑白名单通过Order SET设定信誉权重,权重越大,代表危险性越大,进而通过百分比改变接口限定rate频率。

令牌桶算法优势在于能针对uri做定向rate、size等,不仅限制总请求大小,还限制平均频率大小。缺点是,还是容易导致误判等问题,并切用户的信誉无法完全准确。

参考:

1.Token Bucket Algorithm

2.Token Bucket Algorithm

3.电商课题I:集群环境下业务限流


本文出自 夏日小草,转载请注明出处:http://homeway.me/2015/10/21/nginx-lua-traffic-limit-algorithm

-by小草

2015-10-21 21:49:10

查看原文

赞 2 收藏 45 评论 0

夏日小草 发布了文章 · 2015-09-13

MySQL索引之哈希索引

mysql-log


0x00.About

索引(Index)是帮助MySQL高效获取数据的数据结构。提取句子主干,就可以得到索引的本质:索引是数据结构。

从MySQL逻辑架构来看,MySQL有三层架构,第一层连接,第二层查询解析、分析、优化、视图、缓存,第三层,存储引擎。

MySQL逻辑架构

索引通过分开查询片,节省了扫描查找时间,大大提升查询效率。

大部分数据库系统及文件系统都采用B-Tree或其变种B+Tree作为索引结构。

索引主要在存储引擎层上,不同的引擎也就有不同的B-Tree算法。



0x01.Hash Index

哈希索引只有Memory, NDB两种引擎支持,Memory引擎默认支持哈希索引,如果多个hash值相同,出现哈希碰撞,那么索引以链表方式存储。

但是,Memory引擎表只对能够适合机器的内存切实有限的数据集。

要使InnoDB或MyISAM支持哈希索引,可以通过伪哈希索引来实现,叫自适应哈希索引。

主要通过增加一个字段,存储hash值,将hash值建立索引,在插入和更新的时候,建立触发器,自动添加计算后的hash到表里。

直接索引

假如有一个非常非常大的表,如下:

CREATE TABLE IF NOT EXISTS `User` (
  `id` int(10) NOT NULL COMMENT '自增id',
  `name` varchar(128) NOT NULL DEFAULT '' COMMENT '用户名',
  `email` varchar(128) NOT NULL DEFAULT '' COMMENT '用户邮箱',
  `pass` varchar(64) NOT NULL DEFAULT '' COMMENT '用户密码',
  `last` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后登录时间',
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

这个时候,比如说,用户登陆,我需要通过email检索出用户,通过explain得到如下:

mysql> explain SELECT id FROM User WHERE email = 'ooxx@gmail.com' LIMIT 1;

+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows   | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | User  | ALL  | NULL          | NULL | NULL    | NULL | 384742 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+

发现 rows = 384742 也就是要在384742里面进行比对email这个字段的字符串。

这条记录运行的时间是:Query took 0.1744 seconds,数据库的大小是40万。

从上面可以说明,如果直接在email上面建立索引,除了索引区间匹配,还要进行字符串匹配比对,email短还好,如果长的话这个查询代价就比较大。

如果这个时候,在email上建立哈希索引,查询以int查询,性能就比字符串比对查询快多了。

Hash 算法

建立哈希索引,先选定哈希算法,这里选用CRC32。

《高性能MySQL》说到的方法CRC32算法,建立SHA或MD5算法是划算的,本身位数都有可能比email段长了。

INSERT UPDATE SELECT 操作

在表中添加hash值的字段:

mysql> ALTER TABLE User ADD COLUMN email_hash int unsigned NOT NULL DEFAULT 0;

接下来就是在UPDATE和INSERT的时候,自动更新 email_hash 字段,通过MySQL触发器实现:

DELIMITER |
CREATE TRIGGER user_hash_insert BEFORE INSERT ON `User` FOR EACH ROW BEGIN
SET NEW.email_hash=crc32(NEW.email);
END;
|
CREATE TRIGGER user_hash_update BEFORE UPDATE ON `User` FOR EACH ROW BEGIN
SET NEW.email_hash=crc32(NEW.email);
END;
|
DELIMITER ;

这样的话,我们的SELECT请求就会变成这样:

mysql> SELECT email, email_hash FROM User WHERE email_hash = CRC32("F2dgTSWRBXSZ1d3O@gmail.com") AND email = "F2dgTSWRBXSZ1d3O@gmail.com";

+----------------------------+------------+
| email                      | email_hash |
+----------------------------+------------+
| F2dgTSWRBXSZ1d3O@gmail.com | 2765311122 |
+----------------------------+------------+

在没建立hash索引时候,请求时间是 0.2374 seconds,建立完索引后,请求时间直接变成 0.0003 seconds。

AND email = "F2dgTSWRBXSZ1d3O@gmail.com" 是为了防止哈希碰撞导致数据不准确。



0x02.Hash Index 缺点

哈希索引也有几个缺点:

  • 索引存放的是hash值,所以仅支持 < = > 以及 IN 操作

  • hash索引无法通过操作索引来排序,因为存放的时候经过hash计算,但是计算的hash值和存放的不一定相等,所以无法排序

  • 不能避免全表扫描,只是由于在memory表里支持非唯一值hash索引,就是不同的索引键,可能存在相同的hash值

  • 如果哈希碰撞很多的话,性能也会变得很差

  • 哈希索引无法被用来避免数据的排序操作



参考:

1] Baron Scbwartz等 著,王小东等 译;[高性能MySQL(High Performance MySQL);电子工业出版社,2010

2] [《MySQL的B-Tree索引和Hash索引的区别》

3] [《mysql 索引优化 btree hash rtree》


本文出自 夏日小草,转载请注明出处:http://homeway.me/2015/09/13/mysql-hash-index

-by小草

2015-09-13 16:27:10

查看原文

赞 0 收藏 8 评论 0

夏日小草 发布了文章 · 2015-09-13

Mysql日志分析

mysql-log


0x01.About

MySQL有四种类型的日志:Error Log、General Query Log、Binary Log 和 Slow Query Log。

第一种错误日志,记录MySQL运行过程ERROR,WARNING,NOTE等信息,系统出错或者某条记录出问题可以查看ERROR日志。

第二种日常运行日志,记录MySQL运行中的每条请求数据。

第三种二进制日志,包含了一些事件,这些事件描述了数据库的改动,如建表、数据改动等,也包括一些潜在改动,主要用于备份恢复、回滚等操作。

第四种慢查询日志,用于MySQL性能调优。



0x02.Error Log

MySQL错误日志默认以hostname.err存放在MySQL日志目录,如果不知道MySQL当前的错误日志目录可以使用查询语句:

mysql> show variables like 'log_error';

+---------------+--------------------------------------+
| Variable_name | Value                                |
+---------------+--------------------------------------+
| log_error     | /usr/local/var/mysql/mysql-error.log |
+---------------+--------------------------------------+

修改错误日志地址可以在/etc/my.cnf中添加--log-error[=file_name]选项来开启mysql错误日志。

错误日志记录了MySQL Server每次启动和关闭的详细信息以及运行过程中所有较为严重的警告和错误信息。

知道了MySQL错误日志地址,我们就可以查看MySQL错误日志:

2015-09-12 16:03:20 2624 [ERROR] InnoDB: Unable to lock ./ibdata1, error: 35
2015-09-12 16:03:20 2624 [Note] InnoDB: Check that you do not already have another mysqld process using the same InnoDB data or log files.
2015-09-13 00:03:21 2183 [Note] InnoDB: Shutdown completed; log sequence number 426783897

InnoDB: Unable to lock ./ibdata1, error: 35 可以得出资源被抢占,有可能是开了多个MySQL线程。



0x03.General Query Log

日常请求的SQL:
添加方式一样在 /etc/my.cnf 中添加 general-log-file[=file_name]



0x04.Binary Log

启用Binlog

修改 /etc/my.cnf

binlog_format = STATEMENT
binlog_cache_size = 2M
max_binlog_cache_size = 4M
max_binlog_size = 512M
log-bin = master-bin
log-bin-index = master-bin.index

log-bin-index 指向 master-bin 这个文件,记录有哪些分块的Binlog文件名。
log-bin 记录Binlog文件名前缀,后缀会用数字递增。

Binlog格式

Binlog有3种格式,STATMENT,ROW,MIXED。https://dev.mysql.com/doc/refman/5.1/en/binary-log-mixed.html

混合格式(mixed)会在适当时候切换row和statment格式,statment就是直接的SQL语句格式。

分析Binlog

通过MySQL自带的mysqlbinlog 命令,可以直接查看到Binlog转码数据:

mysqlbinlog /usr/local/var/mysql/master-bin.000117

得到:

# at 335
#150913  0:05:12 server id 1  end_log_pos 366 CRC32 0xa31b50db     Xid = 151
COMMIT/*!*/;
DELIMITER ;
# End of log file
ROLLBACK /* added by mysqlbinlog */;
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;

第一行包含日志文件偏移字节值(335)。

第二行包含:

  • 事件的日期事件,MySQL会使用他们来产生SET TIMESTAMP

  • 服务器的服务器id

  • end_log_pos 下一个事件的偏移字节

  • 事件类型,这里是Xid,常见的还有其他,例如:Intvar,Query,Stop,Format_desc

  • 原服务器上执行语句的线程id,用于审计和CONNECTION_ID()

  • exec_time对于master端的Binlog来说是执行这个event所花费的时间

  • 原服务器产生的错误代码

通过

mysql> show binlog events;

也可以的到binlog数据:

| master-bin.000002 | 3861 | Query       |         1 |        3954 | BEGIN                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| master-bin.000002 | 3954 | Intvar      |         1 |        3986 | INSERT_ID=5                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| master-bin.000002 | 3986 | Query       |         1 |        4475 | use `dropbox`; INSERT INTO `UserLog` (`uid`, `fids`, `nids`, `msg`, `log`, `from`, `type`, `ctime`) VALUES ('1', '[\"35\",\"33\",\"21\"]', '[\"22\",\"21\",\"11\",\"4\",\"3\"]', '从垃圾箱恢复: 恢复文件 \'[\"35\",\"33\",\"21\"]\' 恢复文件夹 \'[\"22\",\"21\",\"11\",\"4\",\"3\"]\'', '[[\"35\",\"33\",\"21\"],[\"22\",\"21\",\"11\",\"4\",\"3\"]]', 'cloud.jue.so', 'recover_by_trash', '2015-09-07 00:51:31')                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| master-bin.000002 | 4475 | Xid         |         1 |        4506 | COMMIT /* xid=423 */

查看Binlog信息

mysql> show variables like '%binlog%';

+-----------------------------------------+----------------------+
| Variable_name                           | Value                |
+-----------------------------------------+----------------------+
| binlog_cache_size                       | 2097152              |
| binlog_checksum                         | CRC32                |
| binlog_direct_non_transactional_updates | OFF                  |
| binlog_error_action                     | IGNORE_ERROR         |
| binlog_format                           | STATEMENT            |
| binlog_gtid_simple_recovery             | OFF                  |
| binlog_max_flush_queue_time             | 0                    |
| binlog_order_commits                    | ON                   |
| binlog_rows_query_log_events            | OFF                  |
| binlog_stmt_cache_size                  | 32768                |
| binlogging_impossible_mode              | IGNORE_ERROR         |
| innodb_api_enable_binlog                | OFF                  |
| innodb_locks_unsafe_for_binlog          | OFF                  |
| max_binlog_cache_size                   | 4194304              |
| max_binlog_size                         | 536870912            |
| max_binlog_stmt_cache_size              | 18446744073709547520 |
| simplified_binlog_gtid_recovery         | OFF                  |
+-----------------------------------------+----------------------+



Slow Query Log

开启 Slow Query

修改/etc/my.cnf

slow-query-log = 1
slow-query-log-file = /usr/loval/var/mysql/mysql-slow.log
long_query_time = 1 #设置满请求时间
log-queries-not-using-indexes

Slow Query工具

Slow Query有很多查看工具,比如:MySQL自带的mysqldumpslow 和 mysqlsla,用的比较多的 py-query-digest,还可以将满请求数据丢给zabbix做显示分析处理。

这里我用 py-query-digest /usr/local/var/mysql/mysql-slow.log 导出了满请求的数据,例如:

# Query 1: 0.02 QPS, 0.55x concurrency, ID 0xFC19E4D04D8E60BF at byte 12547
# This item is included in the report because it matches --limit.
# Scores: V/M = 118.26
# Time range: 2015-09-12 05:52:03 to 05:57:54
# Attribute    pct   total     min     max     avg     95%  stddev  median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count          1       7
# Exec time     78    194s   250ms    169s     28s    167s     57s   992ms
# Lock time      0   901us   111us   158us   128us   152us    18us   119us
# Rows sent      0       5       0       1    0.71    0.99    0.45    0.99
# Rows examine   7 545.01k  14.18k  97.66k  77.86k  97.04k  32.08k  97.04k
# Query size     0     868     123     125     124  124.25       1  118.34
# String:
# Databases    mysqltest
# Hosts        localhost
# Users        root
# Query_time distribution
#   1us
#  10us
# 100us
#   1ms
#  10ms
# 100ms  ################################################################
#    1s  ##########################################
#  10s+  ##########################################
# Tables
#    SHOW TABLE STATUS FROM `mysqltest` LIKE 'File'\G
#    SHOW CREATE TABLE `mysqltest`.`File`\G
#    SHOW TABLE STATUS FROM `mysqltest` LIKE 'User'\G
#    SHOW CREATE TABLE `mysqltest`.`User`\G
# EXPLAIN /*!50100 PARTITIONS*/
SELECT count(*) FROM `File` LEFT JOIN `User` ON `User`.`name`= `File`.`name` WHERE `User`.`name` LIKE '%r%' order by `last`\G

可以看到该SQL被调用7次,平均每次28s,好慢...平均检测数据大小77.86k。

再来看看SQL语句:

SELECT count(*) FROM File LEFT JOIN User ON User.name= File.name WHERE User.name LIKE '%r%' order by last

看着都觉得慢 ON User.name= File.name 在没有建立索引的情况下,所有数据将进行字符串匹配name字段。

这个库有 15W条User数据,10W条File数据,也就是要比对15*10 WW 次数据。

MySQL的slow log的作用也就在这里了,优化慢查询。



参考:

1.《高性能MySQL》

2.(Analyse slow-query-log using mysqldumpslow & pt-query-digest)[https://rtcamp.com/tutorials/mysql/slow-query-log/]

3.初探:MySQL 的 Binlog&version=11020201&pass_ticket=DNtPK7ePVYl93tx1FiRMBNsJMm3DEgwRdO1XEZUustRXuYf6KyUU4gID1Lv7aVTB)


本文出自 夏日小草,转载请注明出处:http://homeway.me/2015/09/12/mysql-log

-by小草

2015-09-12 01:49:10

查看原文

赞 0 收藏 25 评论 2

夏日小草 回答了问题 · 2015-08-13

解决如何避免用户上传相同的文件

寻求js版本qetag计算代码,非nodejs。

因为发现js没有buffer处理方法。

关注 2 回答 2

夏日小草 发布了文章 · 2015-08-11

Nginx+Lua+Redis访问频率控制

nginx-lua-redis


0x01.About

Nginx来处理访问控制的方法有多种,实现的效果也有多种,访问IP段,访问内容限制,访问频率限制等。

用Nginx+Lua+Redis来做访问限制主要是考虑到高并发环境下快速访问控制的需求。

Nginx处理请求的过程一共划分为11个阶段,分别是:

post-read、server-rewrite、find-config、rewrite、post-rewrite、 preaccess、access、post-access、try-files、content、log.

在openresty中,可以找到:

set_by_lua,access_by_lua,content_by_lua,rewrite_by_lua等方法。

那么访问控制应该是,access阶段。



0x02.How to do

1.Solution

按照正常的逻辑思维,我们会想到的访问控制方案如下:

1.检测是否被forbidden?
=》是,forbidden是否到期:是,清除记录,返回200,正常访问;否,返回403;
=》否,返回200,正常访问

2.每次访问,访问用户的访问频率+1处理

3.检测访问频率是否超过限制,超过即添加forbidden记录,返回403

这是简单地方案,还可以添加点枝枝叶叶,访问禁止时间通过算法导入,每次凹曲线增加。

2.Config

首先为nginx添加vhost配置文件,vhost.conf部分内容如下:

lua_package_path "/usr/local/openresty/lualib/?.lua;;";#告诉openresty库地址
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
error_log /usr/local/openresty/nginx/logs/openresty.debug.log debug;

server {
    listen 8080 default;
    server_name localhost;    
    root  /www/openresty;

    location /login {
        default_type 'text/html';
        access_by_lua_file "/usr/local/openresty/nginx/lua/access_by_redis.lua";#通过lua来处理访问控制
    }
}

3.Access_by_redis.lua

参考了下v2ex.com的做法,redis存储方案只做简单地string存储就足够了。key分别是:

用户登录记录:user:127.0.0.1:time(unix时间戳)
访问限制:block:127.0.0.1

先连接Redis吧:

local red = redis:new()
function M:redis()
    red:set_timeout(1000)
    local ok, err = red:connect("127.0.0.1", 6379)
    if not ok then
        ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end
end

按照我们的逻辑方案,第二步是,检测是否forbidden,下面我们就检测block:127.0.0.1,如果搜索到数据,检测时间是否过期,未过期返回403,否则直接返回200:

function M:check1()
    local time=os.time()    --system time
    local res, err = red:get("block:"..ngx.var.remote_addr)
    if not res then -- redis error
        ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) --redis get data error     end

    if type(res) == "string" then --if red not null then type(red)==string
        if tonumber(res) >= tonumber(time) then  --check if forbidden expired
            ngx.exit(ngx.HTTP_FORBIDDEN)
            --ngx.say("forbidden")
        end
    end
}

接下来会做检测,是否访问频率过高,如果过高,要拉到黑名单的,

实现的方法是,检测user:127.0.0.1:time的值是否超标:

function M:check2()
    local time=os.time()    --system time
    local res, err = red:get("user:"..ngx.var.remote_addr..":"..time)
    if not res then -- redis error
        ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) --redis get data error
    end

    if type(res) == "string" then
        if tonumber(res) >= 10 then -- attack, 10 times request/s
            red:del("block:"..self.ip)  
            red:set("block:"..self.ip, tonumber(time)+5*60 ) --set block time
            ngx.exit(ngx.HTTP_FORBIDDEN)
        end
    end
end

最后呢,还要记得,把每次访问时间做一个自增长,user:127.0.0.1:time

function M:add()
    local time=os.time()    --system time
    ok, err = red:incr("user:"..ngx.var.remote_addr..":"..time)
    if not ok then
        ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) --redis get data error
    end
end

那么,测试,强刷几次浏览器,发现过一会,返回了403,ok,搞定。



本文出自 夏日小草,转载请注明出处:http://homeway.me/2015/08/11/nginx-lua-redis-access-control/

-by小草

2015-08-10 01:20:10

查看原文

赞 6 收藏 33 评论 5

夏日小草 提出了问题 · 2015-08-11

Nginx+Lua+Redis访问频率控制

关注 1 回答 0

夏日小草 发布了文章 · 2015-08-03

站内消息设计与实现

http


0x01.About

最近在处理系统消息模块,查阅了很多实践案例,各有针对性。

首先站内消息主要包括:个人消息(评论,点赞),系统消息,订阅消息,私信。

其中,订阅区分用户群,即系统消息是一个特殊的所有人订阅的订阅消息,特点是一对多。

前三个实时性比较低,最后一个实时性高,离线状态下是私信,如果双方在线要转为聊天室,特点是一对一。

那么,接下来,该选个方案了,SQL or NoSQL?



0x02.Mysql实现

首先,对于个人消息、私信("UserMessage"),一条消息插一句,Mysql跑跑没问题。

对于系统消息或订阅消息,必然不可以,假如有10万用户,一次性那么要插入10万条消息,Mysql必死。

那么就是说,要设立一个系统库("SystemMessage"),每当用户登录,就去跑跑系统库("SystemMessage"),把未读的系统库跑到个人库。

关于订阅消息就比较麻烦了,对用户分组?对消息分组?

关系型数据库处理集合问题是比较麻烦的,目前想到的结论是建立一个表("RssMessage")存储消息类型,消息索引。

下面列了大致的数据库模型:

Mysql

看完这个数据库设计,我也觉得好难受,吐槽前先来想想为什么吧。

UserSystemRelation表用于记录用户读取到哪个位置的标记。

可以看到,UserMessage与SystemMessage表中,title、tid、ctime、type字段冗余了,好像也没必要,

但是从用户功能上看,当用户登陆后,查找自己站内消息,必然要用到的有:status,必然要显示的有:title、ctime,type作为用户进入消息面板后,要筛选的方式之一,这样的话,Mysql就只要跑一个表就可以完成显示给用户的最新站内消息了。

由于MessageText可能是一个大信息通知,用户查看个人消息时候,并未查看MessageText内容,所以单独放一张表。

相应处理流程

  • 用户登录后,先通过"UserSystemRelation"表查询是否有新的系统消息
  • 如果"UserSystemRelation",查找到自身uid,同步系统消息到个人消息;如果"UserSystemRelation"未查找到自身uid,直接插入"UserSystemRelation",并读取最近50条系统消息。
  • 用户点击未读消息,获取"MessageText"",并更新状态(status)为已读。
  • 用户通过"status"、"type",可以筛选系统消息。



0x03.Mysql+MongoDB实现

由于Mongodb是一种文档型的数据结构,所以,可以考虑把所有数据转成json直接塞给Mongodb。

基于用户的习惯,读多写少,大部分时候都是看到消息,删除、更新比较少,如果数据没更新直接读Mongodb,如果数据更新,直接删除Mongodb
的索引。

这个考虑是在,用户数量很大的时候,要在"UserSystem"表里查找到用户消息比较慢的时候用,类似于吧Mongodb当缓存。



0x04.Redis实现

看了Mysql下站内消息的数据库设计,我也觉得很蛋疼,临时过渡没事,但是还是NoSQL合适。

Redis自带订阅与发布系统,http://redisbook.readthedocs.org/en/latest/feature/pubsub.html

在下图展示的这个 pubsub_channels 示例中, client2 、 client5 和 client1 就订阅了 channel1 , 而其他频道也分别被别的客户端所订阅:

订阅

只要是订阅了相应地频道,就会收到频道的消息。

把用户ID作为频道,私信就是反向的频道订阅,系统消息就是所有用户的订阅,那么离线的消息呢?

1、线上用户

还是存在系统或个人的哈希表里,等上线后再去读取。

在Python中,订阅发布消息(Publish)如下:

import redis,time
queue = redis.StrictRedis(host='localhost', port=6379, db=0)
channel = queue.pubsub()

for i in range(100): 
    queue.publish("test", i)
    time.sleep(0.1)

Python中,订阅监听消息(Subcribe)如下:

import redis,time
r = redis.StrictRedis(host='localhost', port=6379, db=0)
p = r.pubsub()
p.subscribe('test')

while True:
    message = p.get_message()
    if message:
        print "Subscriber: %s" % message['data']

Redis-py的API可以看GitHub:https://github.com/andymccurdy/redis-py

这是线上用户做法。

2、线下用户

看过一种做法是建立一个Redis链表,存储登陆用户,当用户登陆就直接发送,没登陆就暂存起来。

这里的话,可以用WebSocket实时监听,定期发送心跳包,如果在线直接返回Redis自带的订阅系统。

系统消息建立一个集合:

SADD system:2015-08-03 7 8 9 10 11

第一段标示系统信息,第二段标示日期,后面的数字标示message id。

个人消息建立一个集合:

SADD user:12345:read 1 2 3 4

第一段标示用户信息集合,第二段标示用户id,下一段标示消息类型为已读,后面的数字标示message id。

关于订阅消息如下:

SADD rss:xiaocao 12 13 14 15

那么你就收到小草的订阅消息,消息ID分别是 12, 13, 14, 15

还有很重要的消息数据存储,

HMSET message:12 title 标题 content 内容 date 2015-08-03

Python创建数据库的例子就是:

import redis,time,threading,random
pool = redis.ConnectionPool(host='localhost', port=6379, db=1)
rs = redis.Redis(connection_pool=pool)

rs.sadd("user:123:read", "1", "2")
rs.sadd("user:123:unread", "4", "5", "6")
rs.sadd("system:2015-08-03", "7", "8", "9", "10", "11")
rs.sadd("rss:xiaocao", "12", "13", "14", "15", "11")

for i in range(15):
    rs.hset("message:"+str(i), "title", "title=>"+str(random.uniform(1, 99999)))
    rs.hset("message:"+str(i), "content","content=>"+str(time.time()))
    rs.hset("message:"+str(i), "date", str(time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime())))


参考:



本文出自 夏日小草,转载请注明出处:http://homeway.me/2015/08/03/website-system-message/

-by小草

2015-08-03 01:35:10

查看原文

赞 10 收藏 63 评论 0

夏日小草 发布了文章 · 2015-07-20

理解HTTP之Content-Type

http


0x01.About

查看Restful API 报头插件:Chrome插件REST Console,以及发送Restful API工具:Chrome插件POST Man

在HTTP 1.1规范中,HTTP请求方式有OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE、CONNECT

通常我们用的只有GET、POST,然而对于Restful API规范来说,请求资源要用PUT方法,删除资源要用DELETE方法。

例如发送个DELETE包:

http://example.com/my/resource?id=12345

那么通过id就能获取到信息,这个包只有header,并不存在body,下面讨论几个包含body的发包的body传输格式。



0x02.Content-Type

Content-Type用于指定内容类型,一般是指网页中存在的Content-Type,Content-Type属性指定请求和响应的HTTP内容类型。如果未指定 ContentType,默认为text/html。

在nginx中有个配置文件mime.types,主要是标示Content-Type的文件格式。

下面是几个常见的Content-Type:

1.text/html
2.text/plain
3.text/css
4.text/javascript
5.application/x-www-form-urlencoded
6.multipart/form-data
7.application/json
8.application/xml
...

前面几个都很好理解,都是html,css,javascript的文件类型,后面四个是POST的发包方式。



0x03.application/x-www-form-urlencoded

application/x-www-form-urlencoded是常用的表单发包方式,普通的表单提交,或者js发包,默认都是通过这种方式,

比如一个简单地表单:

<form enctype="application/x-www-form-urlencoded" action="http://homeway.me/post.php" method="POST">
    <input type="text" name="name" value="homeway">
    <input type="text" name="key" value="nokey">
    <input type="submit" value="submit">
</form>

那么服务器收到的raw header会类似:

Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding:gzip, deflate
Accept-Language:zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4,gl;q=0.2,de;q=0.2
Cache-Control:no-cache
Connection:keep-alive
Content-Length:17
Content-Type:application/x-www-form-urlencoded

那么服务器收到的raw body会是,name=homeway&key=nokey,在php中,通过$_POST就可以获得数组形式的数据。



0x04.multipart/form-data

multipart/form-data用在发送文件的POST包。

这里假设我用python的request发送一个文件给服务器:

data = {
    "key1": "123",
    "key2": "456",
}
files = {'file': open('index.py', 'rb')}
res = requests.post(url="http://localhost/upload", method="POST", data=data, files=files)
print res

通过工具,可以看到我发送的数据内容如下:

POST http://www.homeway.me HTTP/1.1
Content-Type:multipart/form-data; boundary=------WebKitFormBoundaryOGkWPJsSaJCPWjZP

------WebKitFormBoundaryOGkWPJsSaJCPWjZP
Content-Disposition: form-data; name="key2"
456
------WebKitFormBoundaryOGkWPJsSaJCPWjZP
Content-Disposition: form-data; name="key1"
123
------WebKitFormBoundaryOGkWPJsSaJCPWjZP
Content-Disposition: form-data; name="file"; filename="index.py"

这里Content-Type告诉我们,发包是以multipart/form-data格式来传输,另外,还有boundary用于分割数据。

当文件太长,HTTP无法在一个包之内发送完毕,就需要分割数据,分割成一个一个chunk发送给服务端,

那么--用于区分数据快,而后面的数据633e61ebf351484f9124d63ce76d8469就是标示区分包作用。



0x05.text/xml

微信用的是这种数据格式发送请求的。

POST http://www.homeway.me HTTP/1.1 
Content-Type: text/xml

<?xml version="1.0"?>
<resource>
    <id>123</id>
    <params>
        <name>
            <value>homeway</value>
        </name>
        <age>
            <value>22</value>
        </age>
    </params>
</resource>

php中$_POST只能读取application/x-www-form-urlencoded数据,$_FILES只能读取multipart/form-data类型数据,

那么,要读取text/xml格式的数据,可以用:

$file = fopen('php://input', 'rb');
$data = fread($file, length);
fclose($file);

或者

$data = file_get_contents('php://input');



0x06.application/json

通过json形式将数据发送给服务器,一开始,我尝试通过curl,给服务器发送application/json格式包,

然而我收到的数据如下:

--------------------------e1e1406176ee348a Content-Disposition: form-data; name="nid" 2 --------------------------e1e1406176ee348a Content-Disposition: form-data; name="uuid" cf9dc994-a4e7-3ad6-bc54-41965b2a0dd7 --------------------------e1e1406176ee348a Content-Disposition: form-data; name="access_token" 956731586df41229dbfec08dd5d54eedb98d73d2 --------------------------e1e1406176ee348a--

后来想想明白了,HTTP通信中并不存在所谓的json,而是将string转成json罢了,也就是,application/json可以将它理解为text/plain,普通字符串。

之所以出现那么多乱七八糟的-------应该是php数组传输进去,存在的转换问题吧(我目前能想到的原因)。


本文出自 夏日小草,转载请注明出处:http://homeway.me/2015/07/19/understand-http-about-content-type/

-by小草

2015-07-19 01:35:10

查看原文

赞 19 收藏 68 评论 5

夏日小草 回答了问题 · 2015-07-17

解决求有关REST API设计的例子

关注 7 回答 3

夏日小草 发布了文章 · 2015-07-10

MAC重装各种的痛点

http://homeway.me/2015/07/10/rebuild-osx-environment/


OSX


0x01.About

最近不小心做死,删了很多安装包,最后只能重装了MAC,用了3天多修复开发环境。

想想时候该入Docker了,每次这么搞要玩命了。



0x02.Openresty

第一次装openresty没有事情的,然而重装MAC后,再装openresty出现了问题。安装openresty可以直接

git clone https://github.com/openresty

下来,运行make,自动下载依赖包,也可以直接到 http://openresty.org/ 下载打包好的。

1.openssl缺少错误如下

./configure: error: SSL modules require the OpenSSL library.
You can either do not enable the modules, or install the OpenSSL library
into the system, or build the OpenSSL library statically from the source
with nginx by using --with-openssl=<path> option.

ERROR: failed to run command: sh ./configure --prefix=/usr/local/openresty/nginx \…

缺少openssl库,那就把本机安装路径告诉它吧:

./configure --with-openssl=/usr/local/Cellar/openssl/1.0.2c

2.openssl源码安装错误

/Applications/Xcode.app/Contents/Developer/usr/bin/make -f objs/Makefile
cd /usr/local/Cellar/openssl/1.0.2c \
    && if [ -f Makefile ]; then /Applications/Xcode.app/Contents/Developer/usr/bin/make clean; fi \
    && ./config --prefix=/usr/local/Cellar/openssl/1.0.2c/.openssl no-shared  no-threads \
    && /Applications/Xcode.app/Contents/Developer/usr/bin/make \
    && /Applications/Xcode.app/Contents/Developer/usr/bin/make install LIBDIR=lib
/bin/sh: ./config: No such file or directory
make[2]: *** [/usr/local/Cellar/openssl/1.0.2c/.openssl/include/openssl/ssl.h] Error 127
make[1]: *** [build] Error 2
make: *** [all] Error 2

上了github,https://github.com/torch/image/issues/16,查查,明白了,这里要的是source code,不是安装路径,够坑的了吧。

那么好了,到https://www.openssl.org/下载了最新的,openssl.1.0.2c版本,到bundle目录里:

./configure --with-openssl=bundle/openssl.1.0.2c

报了个warning:

WARNING! If you wish to build 64-bit library, then you have to
         invoke './Configure darwin64-x86_64-cc' *manually*.
         You have about 5 seconds to press Ctrl-C to abort.

看着是openssl与darwin的版本不兼容问题,后来发现是新版的openssl与nginx兼容问题。

3.pcre依赖报错

ld: symbol(s) not found for architecture x86_64 collect2: ld 
returned 1 exit status make[2]: *** [objs/nginx] 
Error 1 make[1]: *** [build] 
Error 2 make: *** 
[all] Error 2

找一找,发现了问题解决方案,是pcre依赖包没带上,也就是正则匹配依赖包的问题了:

在github上找到了issuse相关信息: https://github.com/openresty/ngx_openresty/issues/3#issuecomment-12022...

最后在issuse上问道了agentzh的解决方案,agentzh的makefile里在处理新版nginx与openssl依赖上的一点问题,后来他更新了github仓库:

export KERNEL_BITS=64
./configure --with-cc-opt='-I/usr/local/Cellar/pcre/8.37/include/' \
       --with-ld-opt='-L/usr/local/Cellar/pcre/8.37/lib' \
       --with-openssl=$HOME/work/openssl-1.0.2d -j9
make -j9
sudo make install

在我这里(MAC OSX 10.10.4)运行起来是没问题的。

两个问题:

1.openssl依赖,要用源码,要export告诉系统环境变量

2.pcre包要手动加上去。



0x03.Hexo

安装hexo 3.0各种骂声,都是从2.8升上去的人喊的不兼容问题。

嗯...照着官网的打吧,一步一步走下来吧:

npm install hexo-cli -g

hexo init

npm install

hexo server

报错了:

$ hexo s
{ [Error: Cannot find module './build/Release/DTraceProviderBindings'] code: 'MODULE_NOT_FOUND' }
{ [Error: Cannot find module './build/default/DTraceProviderBindings'] code: 'MODULE_NOT_FOUND' }
{ [Error: Cannot find module './build/Debug/DTraceProviderBindings'] code: 'MODULE_NOT_FOUND' }
ERROR Plugin load failed: hexo-server

后来看了下,问题很简单,少了几个库没装上,估计是天朝对外网络问题,没下载全就直接停掉了。

方案一:

换个国内npm源,然后:

npm install hexo —no-optional

方案二:

把之前的node_modules,放到新的目录下,大约50MB。

需要的可以mail我,base64 地址如下mailto: eGlhb2Nhby5ncmFzc2VzQGdtYWlsLmNvbQ==



0x04.Mysql

ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock'

mysql的2002报错估计很多人都碰到过了,也就是,/tmp/mysql.sock 文件不存在的问题,没有/tmp/mysql.sock的话,就不能phpmyadmin等客户端进行连接了。

很多地方解决方法无非就是添加个link,把/usr/var/mysql/mysqld.sock/tmp/mysql.sock关联起来吧,最后都不行。

mysql的默认配置文件为my.cnf,mysql默认回去这几个位置找这个文件,
/etc/my.cnf,
/etc/mysql/my.cnf,
/home/username/my.cnf,
/home/username/.mysql/my.cnf

然而在MAC中,默认是没有配置文件的,那么你要自己去新建。

下面是完整的解决方案:

  1. 执行安装:

$ brew install mysql

  1. 在 /usr/local/etc/ 下创建或修改 my.cnf,示例:
[client]
port = 3306
socket = /tmp/mysql.sock
default-character-set = utf8

[mysqld]
collation-server = utf8_unicode_ci
character-set-server = utf8
init-connect ='SET NAMES utf8'
max_allowed_packet = 64M
bind-address = 127.0.0.1
port = 3306
socket = /tmp/mysql.sock
innodb_file_per_table=1

[mysqld_safe]
timezone = '+0:00'

  1. 赞安装完 mysql 后,他会提示你 init database,并提供下面这两句让你执行,很可能你没执行这两句

$ unset TMPDIR
$ mysql_install_db --verbose --user=whoami --basedir="$(brew --prefix mysql)" --datadir=/usr/local/var/mysql --tmpdir=/tmp

  1. 然后就可以指向 mysql.server start 来启动了,为了安全,你还可以执行安全设置向导,根据提示一步一步配置

$ mysql_secure_installation



这里只记下几个重装的痛点,各种蛋疼,于是爽爽地入docker了,期待docker能有新发现。

将来,待续...


本文出自 夏日小草,转载请注明出处:http://homeway.me/2015/07/10/rebuild-osx-environment/



-by小草

2015-07-10 21:35:10

查看原文

赞 2 收藏 6 评论 10

认证与成就

  • 获得 147 次点赞
  • 获得 10 枚徽章 获得 0 枚金徽章, 获得 4 枚银徽章, 获得 6 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2013-11-14
个人主页被 2.8k 人浏览