Quantcast
Channel: IT瘾博客推荐
Viewing all 532 articles
Browse latest View live

MHA高可用架构与Atlas读写分离 - 惨绿少年 - 博客园

$
0
0

1.1 MHA简介

1.1.1 MHA软件介绍

  MHA(Master High Availability)目前在MySQL高可用方面是一个相对成熟的解决方案,它由日本DeNA公司youshimaton(现就职于Facebook公司)开发,是一套优秀的作为MySQL高可用性环境下故障切换和主从提升的高可用软件。在MySQL故障切换过程中, MHA能做到在10~30秒之内自动完成数据库的故障切换操作,并且在进行故障切换的过程中,MHA能在最大程度上保证数据的一致性,以达到真正意义上的高可用。

  MHA能够在较短的时间内实现自动故障检测和故障转移,通常在10-30秒以内;在复制 框架中,MHA能够很好地解决复制过程中的数据一致性问题,由于不需要在现有的 replication中添加额外的服务器,仅需要一个manager节点,而一个Manager能管理多套复制,所以能大大地节约服务器的数量;另外,安装简单,无性能损耗,以及不需要修改现 有的复制部署也是它的优势之处。

  MHA还提供在线主库切换的功能,能够安全地切换当前运行的主库到一个新的主库中 (通过将从库提升为主库),大概 0.5-2秒内即可完成。

  该软件由两部分组成:MHA Manager(管理节点)和MHA Node(数据节点)。MHA Manager可以单独部署在一台独立的机器上管理多个master-slave集群,也可以部署在一台slave节点上。MHA Node运行在每台MySQL服务器上,MHA Manager会定时探测集群中的master节点, master出现故障时,它可以自动将最新数据的slave提升为新的master,然后将所有其他的slave重新指向新的master整个故障转移过程对应用程序完全透明。

  在MHA自动故障切换过程中,MHA试图从宕机的主服务器上保存二进制日志,最大程度的保证数据的不丢失,但这并不总是可行的。例如,如果主服务器硬件故障或无法通过ssh访问,MHA没法保存二进制日志,只进行故障转移而丢失了最新的数据。使用MySQL 5.5的半同步复制,可以大大降低数据丢失的风险。

  MHA可以与半同步复制结合起来。如果只有一个slave已经收到了最新的二进制日志,MHA可以将最新的二进制日志应用于其他所有的slave服务器上,因此可以保证所有节点的数据一致性。

  目前MHA主要支持一主多从的架构,要搭建MHA,要求一个复制集群中必须最少有三台数据库服务器,一主二从,即一台充当master,一台充当备用master,另外一台充当从库,因为至少需要三台服务器,出于机器成本的考虑,淘宝也在该基础上进行了改造,目前淘宝 TMHA已经支持一主一从。

1.1.2 MHA工作原理

 

工作原理说明:

 

   1、保存master上的所有binlog事件

    2、找到含有最新binlog位置点的slave

    3、通过中继日志将数据恢复到其他的slave

    4、将包含最新binlog位置点的slave提升为master

    5、将其他从库slave指向新的master原slave01 并开启主从复制

    6、将保存下来的binlog恢复到新的master上

 

1、监控所有node节点MHA功能说明:

2、自动故障切换(failover)

     前提是必须有三个节点存在,并且有两个从库

      (1)选主前提,按照配置文件的顺序进行,但是如果此节点后主库100M以上relay-log 就不会选

      (2)如果你设置了权重,总会切换带此节点;一般在多地多中心的情况下,一般会把权重设置在本地节点。

      (3)选择s1为新主

      (4)保存主库binlog日志

3、重新构建主从

      (1)将有问题的节点剔除MHA

          进行第一阶段数据补偿,S2缺失部分补全90

      (2)s1切换角色为新主,将s2指向新主S1

            s2  change master to s1

      (3) 第二阶段数据补偿

            将保存过来的新主和原有主缺失部分的binlog,应用到新主。

      (4)虚拟IP漂移到新主,对应用透明无感知

      (5)通知管理员故障切换

1.1.3 MHA高可用架构图

 

1.1.4 MHA工具介绍

  MHA软件由两部分组成,Manager工具包和Node工具包,具体的说明如下:

  Manager工具包主要包括以下几个工具:

masterha_check_ssh             #检査 MHA 的 ssh-key^
masterha_check_repl            #检査主从复制情况
masterha_manger                #启动MHA
masterha_check_status          #检测MHA的运行状态^
masterha_mast er_monitor       #检测master是否宕机一
masterha_mast er_switch        #手动故障转移—
masterha_conf_host             #手动添加server倍息一
masterha_secondary_check       #建立TCP连接从远程服务器v
masterha_stop                  #停止MHA

Node工具包主要包括以下几个工具:    

save_binary_1ogs       #保存宕机的master的binlog
apply_diff_relay_logs   #识别relay log的差异
filter_mysqlbinlog           #防止回滚事件一MHA已不再使用这个工具
purge_relay_logs           #清除中继曰志一不会阻塞SQL线程

1.1.5 MHA的优点

1、自动故障转移

2、主库崩溃不存在数据不一致的情况

3、不需要对当前的mysql环境做重大修改

4、不需要添加额外的服务器

5、性能优秀,可以工作再半同步和异步复制框架

6、只要replication支持的存储引擎mha都支持

1.2 环境说明

   在本次的实验中,共需要用到三台主机,系统、软件说明如下。

1.2.1 系统环境说明

db01主机(master)

[root@db01 ~]# cat /etc/redhat-release 
CentOS release 6.9 (Final)
[root@db01 ~]# uname -r
2.6.32-696.el6.x86_64
[root@db01 ~]# /etc/init.d/iptables status
iptables: Firewall is not running.
[root@db01 ~]# getenforce 
Disabled
[root@db01 ~]# hostname -I
10.0.0.51 172.16.1.51

db02主机(slave1)

 1 [root@db02 ~]# cat /etc/redhat-release 
 2 CentOS release 6.9 (Final)
 3 [root@db02 ~]# uname -r
 4 2.6.32-696.el6.x86_64
 5 [root@db02 ~]# /etc/init.d/iptables status
 6 iptables: Firewall is not running.
 7 [root@db02 ~]# getenforce 
 8 Disabled
 9 [root@db02 ~]# hostname -I
10 10.0.0.52 172.16.1.52

db03主机(slave1,MHA Manages、Atlas节点)

 1 [root@db03 ~]# cat /etc/redhat-release 
 2 CentOS release 6.9 (Final)
 3 [root@db03 ~]# uname -r
 4 2.6.32-696.el6.x86_64
 5 [root@db03 ~]# /etc/init.d/iptables status
 6 iptables: Firewall is not running.
 7 [root@db03 ~]# getenforce 
 8 Disabled
 9 [root@db03 ~]# hostname -I
10 10.0.0.53 172.16.1.53

1.2.2 mysql软件说明

         三台服务器上都全新安装mysql 5.6.36 :

[root@db01 ~]# mysql --version
mysql  Ver 14.14 Distrib 5.6.36, for Linux (x86_64) using  EditLine wrapper

  关于mysql数据库具体的安装方法参考: http://www.cnblogs.com/clsn/p/8038964.html#_label3

1.3 基于GTID的主从复制配置

1.3.1 先决条件

  🔊 主库和从库都要开启binlog

  🔊 主库和从库server-id必须不同

  🔊 要有主从复制用户

1.3.2 配置主从复制

db01 my.cnf文件

[root@db01 ~]# cat /etc/my.cnf
[mysqld]
basedir=/application/mysql
datadir=/application/mysql/data
socket=/tmp/mysql.sock
log-error=/var/log/mysql.log
log-bin=/data/mysql/mysql-bin
binlog_format=row
secure-file-priv=/tmp 
server-id=51
skip-name-resolve  # 跳过域名解析
gtid-mode=on    # 启用gtid类型,否则就是普通的复制架构
enforce-gtid-consistency=true    #强制GTID的一致性
log-slave-updates=1     # slave更新是否记入日志(5.6必须的)
relay_log_purge = 0 
[mysql]
socket=/tmp/mysql.sock

db02 my.cnf文件

 1 [root@db02 ~]# cat /etc/my.cnf
 2 [mysqld]
 3 basedir=/application/mysql
 4 datadir=/application/mysql/data
 5 socket=/tmp/mysql.sock
 6 log-error=/var/log/mysql.log
 7 log-bin=/data/mysql/mysql-bin
 8 binlog_format=row
 9 secure-file-priv=/tmp 
10 server-id=52
11 skip-name-resolve
12 gtid-mode=on
13 enforce-gtid-consistency=true
14 log-slave-updates=1
15 relay_log_purge = 0 
16 [mysql]
17 socket=/tmp/mysql.sock

db03 my.cnf文件

 1 [root@db03 ~]# cat /etc/my.cnf
 2 [mysqld]
 3 basedir=/application/mysql
 4 datadir=/application/mysql/data
 5 socket=/tmp/mysql.sock
 6 log-error=/var/log/mysql.log
 7 log-bin=/data/mysql/mysql-bin
 8 binlog_format=row
 9 secure-file-priv=/tmp 
10 server-id=53
11 skip-name-resolve
12 gtid-mode=on
13 enforce-gtid-consistency=true
14 log-slave-updates=1
15 relay_log_purge = 0 
16 skip-name-resolve
17 [mysql]
18 socket=/tmp/mysql.sock

创建复制用户 (51作为主节点,52、53为从)

GRANT REPLICATION SLAVE ON *.* TO repl@'10.0.0.%' IDENTIFIED BY '123';

从库开启复制

change master to 
    master_host='10.0.0.51',
    master_user='repl',
    master_password='123',
    MASTER_AUTO_POSITION=1;

启动从库复制

start slave;

1.3.3 GTID复制技术说明

MySQL GTID简介

  GTID的全称为 global transaction identifier ,可以翻译为全局事务标示符,GTID在原始master上的事务提交时被创建。GTID需要在全局的主-备拓扑结构中保持唯一性,GTID由两部分组成:

   GTID = source_id:transaction_id 

   source_id用于标示源服务器,用server_uuid来表示,这个值在第一次启动时生成,并写入到配置文件data/auto.cnf中

   transaction_id则是根据在源服务器上第几个提交的事务来确定。

GTID事件结构

 

GTID在二进制日志中的结构

  4

一个GTID的生命周期包括:

1.事务在主库上执行并提交给事务分配一个gtid(由主库的uuid和该服务器上未使用的最小事务序列号),该GTID被写入到binlog中。

2.备库读取relaylog中的gtid,并设置session级别的gtid_next的值,以告诉备库下一个事务必须使用这个值

3.备库检查该gtid是否已经被其使用并记录到他自己的binlog中。slave需要担保之前的事务没有使用这个gtid,也要担保此时已分读取gtid,但未提交的事务也不恩呢过使用这个gtid.

4.由于gtid_next非空,slave不会去生成一个新的gtid,而是使用从主库获得的gtid。这可以保证在一个复制拓扑中的同一个事务gtid不变。由于GTID在全局的唯一性,通过GTID,我们可以在自动切换时对一些复杂的复制拓扑很方便的提升新主库及新备库,例如通过指向特定的GTID来确定新备库复制坐标。

    GTID是用来替代以前classic的复制方法;

    MySQL5.6.2支持 MySQL5.6.10后完善;

GTID相比传统复制的优点:

1.一个事务对应一个唯一ID,一个GTID在一个服务器上只会执行一次

2.GTID是用来代替传统复制的方法,GTID复制与普通复制模式的最大不同就是不需要指定二进制文件名和位置

3.减少手工干预和降低服务故障时间,当主机挂了之后通过软件从众多的备机中提升一台备机为主机

GTID的限制:

1.不支持非事务引擎

2.不支持create table ... select 语句复制(主库直接报错)

  原理:( 会生成两个sql,一个是DDL创建表SQL,一个是insert into 插入数据的sql。

  由于DDL会导致自动提交,所以这个sql至少需要两个GTID,但是GTID模式下,只能给这个sql生成一个GTID )  

3.不允许一个SQL同时更新一个事务引擎表和非事务引擎表

4.在一个复制组中,必须要求统一开启GTID或者是关闭GTID

5.开启GTID需要重启(5.7除外)

6.开启GTID后,就不再使用原来的传统复制方式

7.对于create temporary table 和 drop temporary table语句不支持

8.不支持sql_slave_skip_counter

1.3.4 COM_BINLOG_DUMP_GTID

从机发送到主机执行的事务的标识符的主范围

   Master send all other transactions to slave 

  同样的GTID不能被执行两次,如果有同样的GTID,会自动被skip掉。

 

  slave1:将自己的UUID1:1发送给master,然后接收到了UUID1:2,UUID1:3 event

  slave2:将自己的UUID1:1,UUID1:2发送给master,然后接收到了UUID1:3事件

GTID组成

  GTID实际上是由UUID+TID组成的。其中UUID是一个MySQL实例的唯一标识。TID代表了该实例上已经提交的事务数量,并且随着事务提交单调递增

GTID = source_id :transaction_id
7E11FA47-31CA-19E1-9E56-C43AA21293967:29

1.3.5 【示例二】MySQL GTID复制配置

主节点my.cnf文件

# vi /etc/my.cnf
[mysqld]
basedir=/usr/local/mysql
datadir=/data/mysql
server-id=1
log-bin=mysql-bin
socket=/tmp/mysql.sock
binlog-format=ROW
gtid-mode=on
enforce-gtid-consistency=true
log-slave-updates=1

从节点my.cnf文件

# vi /etc/my.cnf
[mysqld]
basedir=/usr/local/mysql
datadir=/data/mysql
server-id=2
binlog-format=ROW
gtid-mode=on
enforce-gtid-consistency=true
log-bin=mysql-bin
log_slave_updates = 1
socket=/tmp/mysql.sock

配置文件注解

server-id=x                    # 同一个复制拓扑中的所有服务器的id号必须惟一
binlog-format=RO               # 二进制日志格式,强烈建议为ROW
gtid-mode=on                   # 启用gtid类型,否则就是普通的复制架构
enforce-gtid-consistency=true  # 强制GTID的一致性
log-slave-updates=1            # slave更新是否记入日志

  复制用户准备(Master主节点)

mysql>GRANT REPLICATION SLAVE ON *.* TO rep@'10.0.0.%' IDENTIFIED BY '123';

  开启复制(Slave从节点)

mysql>start slave;
mysql>show slave status\G

  现在就可以进行主从复制测试。

1.4 部署MHA

  本次MHA的部署基于GTID复制成功构建,普通主从复制也可以构建MHA架构。

1.4.1 环境准备(所有节点操作)

安装依赖包

yum install perl-DBD-MySQL -y

    下载mha软件,mha官网: https://code.google.com/archive/p/mysql-master-ha/

         github下载地址: https://github.com/yoshinorim/mha4mysql-manager/wiki/Downloads

下载软件包

mha4mysql-manager-0.56-0.el6.noarch.rpm

mha4mysql-manager-0.56.tar.gz

mha4mysql-node-0.56-0.el6.noarch.rpm

mha4mysql-node-0.56.tar.gz

在所有节点安装node

rpm -ivh mha4mysql-node-0.56-0.el6.noarch.rpm

创建mha管理用户

grant all privileges on *.* to mha@'10.0.0.%' identified by 'mha';

# 主库上创建,从库会自动复制(在从库上查看)

  创建命令软连接 (重要)

  如果不创建命令软连接,检测mha复制情况的时候会报错

ln -s /application/mysql/bin/mysqlbinlog /usr/bin/mysqlbinlog
ln -s /application/mysql/bin/mysql /usr/bin/mysql

1.4.2 部署管理节点(mha-manager)

在mysql-db03上部署管理节点

# 安装epel源,软件需要
wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-6.repo 
# 安装manager 依赖包
yum install -y perl-Config-Tiny epel-release perl-Log-Dispatch perl-Parallel-ForkManager perl-Time-HiRes
# 安装manager管理软件
rpm -ivh mha4mysql-manager-0.56-0.el6.noarch.rpm

创建必须目录

mkdir -p /etc/mha
mkdir -p /var/log/mha/app1    ----》可以管理多套主从复制

编辑mha-manager配置文件

[root@db03 ~]# cat  /etc/mha/app1.cnf
[server default]                        
manager_log=/var/log/mha/app1/manager
manager_workdir=/var/log/mha/app1
master_binlog_dir=/data/mysql
user=mha
password=mha
ping_interval=2
repl_password=123
repl_user=repl
ssh_user=root

[server1]
hostname=10.0.0.51
port=3306

[server2]
hostname=10.0.0.52
port=3306

[server3]
hostname=10.0.0.53
port=3306

【配置文件详解】

 1 [server default]
 2 #设置manager的工作目录
 3 manager_workdir=/var/log/masterha/app1
 4 #设置manager的日志
 5 manager_log=/var/log/masterha/app1/manager.log 
 6 #设置master 保存binlog的位置,以便MHA可以找到master的日志,我这里的也就是mysql的数据目录
 7 master_binlog_dir=/data/mysql
 8 #设置自动failover时候的切换脚本
 9 master_ip_failover_script= /usr/local/bin/master_ip_failover
10 #设置手动切换时候的切换脚本
11 master_ip_online_change_script= /usr/local/bin/master_ip_online_change
12 #设置mysql中root用户的密码,这个密码是前文中创建监控用户的那个密码
13 password=123456
14 #设置监控用户root
15 user=root
16 #设置监控主库,发送ping包的时间间隔,尝试三次没有回应的时候自动进行failover
17 ping_interval=1
18 #设置远端mysql在发生切换时binlog的保存位置
19 remote_workdir=/tmp
20 #设置复制用户的密码
21 repl_password=123456
22 #设置复制环境中的复制用户名 
23 repl_user=rep
24 #设置发生切换后发送的报警的脚本
25 report_script=/usr/local/send_report
26 #一旦MHA到server02的监控之间出现问题,MHA Manager将会尝试从server03登录到server02
27 secondary_check_script= /usr/local/bin/masterha_secondary_check -s server03 -s server02 --user=root --master_host=server02 --master_ip=10.0.0.51 --master_port=3306
28 #设置故障发生后关闭故障主机脚本(该脚本的主要作用是关闭主机放在发生脑裂,这里没有使用)
29 shutdown_script=""
30 #设置ssh的登录用户名
31 ssh_user=root 
32 
33 [server1]
34 hostname=10.0.0.51
35 port=3306
36 
37 [server2]
38 hostname=10.0.0.52
39 port=3306
40 #设置为候选master,如果设置该参数以后,发生主从切换以后将会将此从库提升为主库,即使这个主库不是集群中事件最新的slave
41 candidate_master=1
42 #默认情况下如果一个slave落后master 100M的relay logs的话,MHA将不会选择该slave作为一个新的master,因为对于这个slave的恢复需要花费很长时间,通过设置check_repl_delay=0,MHA触发切换在选择一个新的master的时候将会忽略复制延时,这个参数对于设置了candidate_master=1的主机非常有用,因为这个候选主在切换的过程中一定是新的master
43 check_repl_delay=0

配置ssh信任(密钥分发,在所有节点上执行)

# 生成密钥
ssh-keygen -t dsa -P '' -f ~/.ssh/id_dsa >/dev/null 2>&1
# 分发公钥,包括自己
for i in 1 2 3 ;do ssh-copy-id -i /root/.ssh/id_dsa.pub root@10.0.0.5$i ;done

         分发完成后测试分发是否成功

for i in 1 2 3 ;do ssh 10.0.0.5$i  date ;done
或
[root@db03 ~]# masterha_check_ssh --conf=/etc/mha/app1.cnf
最后一行信息为如下字样即为分发成功:
Thu Dec 28 18:44:53 2017 - [info] All SSH connection tests passed successfully.

1.4.3 启动mha

经过上面的部署过后,mha架构已经搭建完成

# 启动mha
nohup masterha_manager --conf=/etc/mha/app1.cnf --remove_dead_master_conf --ignore_last_failover < /dev/null > /var/log/mha/app1/manager.log 2>&1 &

         启动成功后,检查主库状态

[root@db03 ~]# masterha_check_status --conf=/etc/mha/app1.cnf 
app1 (pid:3298) is running(0:PING_OK), master:10.0.0.51

1.4.4 切换master测试

查看现在的主库是哪个

[root@db03 ~]# masterha_check_status --conf=/etc/mha/app1.cnf 
app1 (pid:11669) is running(0:PING_OK), master:10.0.0.51

手动停止主库

[root@db01 ~]# /etc/init.d/mysqld stop 
Shutting down MySQL..... SUCCESS!

再停止数据的同时查看日志信息的变化

[root@db03 ~]# tailf /var/log/mha/app1/manager
~~~
Fri Dec 29 15:51:14 2017 - [info]  All other slaves should start replication from
here. Statement should be: CHANGE MASTER TO MASTER_HOST='10.0.0.52', MASTER_PORT=3306, MASTER_AUTO_POSITION=1, MASTER_USER='repl', MASTER_PASSWORD='xxx';

修复主从

①  启动原主库,添加change master to 信息

[root@db01 ~]# /etc/init.d/mysqld start 
Starting MySQL. SUCCESS!
mysql> CHANGE MASTER TO MASTER_HOST='10.0.0.52', MASTER_PORT=3306, MASTER_AUTO_POSITION=1, MASTER_USER='repl', MASTER_PASSWORD='123';
mysql> start slave;

②  查看主从复制状态

mysql> show slave status\G
                   Master_Host: 10.0.0.52
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes

修复mha

①  修改app1.cnf配置文件,添加回被剔除主机

[root@db03 ~]# cat  /etc/mha/app1.cnf 
[binlog1]
hostname=10.0.0.53
master_binlog_dir=/data/mysql/binlog/
no_master=1

[server default]
manager_log=/var/log/mha/app1/manager
manager_workdir=/var/log/mha/app1
master_binlog_dir=/data/mysql
master_ip_failover_script=/usr/local/bin/master_ip_failover
password=mha
ping_interval=2
repl_password=123
repl_user=repl
ssh_user=root
user=mha

[server1]
hostname=10.0.0.51
port=3306

[server2]
hostname=10.0.0.52
port=3306

[server3]
hostname=10.0.0.53
port=3306

②  mha检查复制状态

[root@db03 ~]# masterha_check_repl --conf=/etc/mha/app1.cnf
MySQL Replication Health is OK.

③  启动mha程序

nohup masterha_manager --conf=/etc/mha/app1.cnf --remove_dead_master_conf --ignore_last_failover < /dev/null > /var/log/mha/app1/manager.log 2>&1 &

到此主库切换成功

[root@db03 ~]# masterha_check_status --conf=/etc/mha/app1.cnf 
app1 (pid:11978) is running(0:PING_OK), master:10.0.0.52

实验结束将主库切换回db01.

①  停止mha

[root@db03 ~]# masterha_stop --conf=/etc/mha/app1.cnf 
Stopped app1 successfully.

②  停止所有从库slave(所有库操作)

stop slave;
reset slave all;

③  重做主从复制(db02、db03)

CHANGE MASTER TO 
  MASTER_HOST='10.0.0.51', 
  MASTER_PORT=3306, 
  MASTER_AUTO_POSITION=1, 
  MASTER_USER='repl', 
  MASTER_PASSWORD='123';

④  启动slave

start slave;

                  启动之后检查从库是否为两个yes  show slave status\G 

⑤  mha检查主从复制

[root@db03 ~]# masterha_check_repl --conf=/etc/mha/app1.cnf
MySQL Replication Health is OK.

⑥  启动mha

nohup masterha_manager --conf=/etc/mha/app1.cnf --remove_dead_master_conf --ignore_last_failover < /dev/null > /var/log/mha/app1/manager.log 2>&1 &

  检查切换是否成功

[root@db03 ~]# masterha_check_status --conf=/etc/mha/app1.cnf
app1 (pid:12127) is running(0:PING_OK), master:10.0.0.51

         到此主主节点有切回到db01

1.4.5 设置权重

修改[server1]的权重

[server1]
hostname=10.0.0.51
port=3306
candidate_master=1
check_repl_delay=0

配置说明

candidate_master=1                  ----》不管怎样都切到优先级高的主机,一般在主机性能差异的时候用           
check_repl_delay=0                  ----》不管优先级高的备选库,数据延时多久都要往那切

注:

1、多地多中心,设置本地节点为高权重

2、在有半同步复制的环境中,设置半同步复制节点为高权重

3、你觉着哪个机器适合做主节点,配置较高的 、性能较好的

1.5 配置VIP漂移

1.5.1 IP漂移的两种方式

   🐶 通过keepalived的方式,管理虚拟IP的漂移

   🐶  通过MHA自带脚本方式,管理虚拟IP的漂移

1.5.2 MHA脚本方式

修改mha配置文件

[root@db03 ~]# grep "script" /etc/mha/app1.cnf 
[server default]
master_ip_failover_script=/usr/local/bin/master_ip_failover

         再主配置中添加VIP脚本

脚本内容

[root@db03 ~]# cat /usr/local/bin/master_ip_failover 
#!/usr/bin/env perl
use strict;
use warnings FATAL => 'all';
use Getopt::Long;
my (
    $command,          $ssh_user,        $orig_master_host, $orig_master_ip,
    $orig_master_port, $new_master_host, $new_master_ip,    $new_master_port
);
my $vip = '10.0.0.55/24';
my $key = '0';
my $ssh_start_vip = "/sbin/ifconfig eth0:$key $vip";
my $ssh_stop_vip = "/sbin/ifconfig eth0:$key down";

GetOptions(
    'command=s'          => \$command,'ssh_user=s'         => \$ssh_user,'orig_master_host=s' => \$orig_master_host,'orig_master_ip=s'   => \$orig_master_ip,'orig_master_port=i' => \$orig_master_port,'new_master_host=s'  => \$new_master_host,'new_master_ip=s'    => \$new_master_ip,'new_master_port=i'  => \$new_master_port,
);

exit &main();

sub main {

    print "\n\nIN SCRIPT TEST====$ssh_stop_vip==$ssh_start_vip===\n\n";

    if ( $command eq "stop" || $command eq "stopssh" ) {

        my $exit_code = 1;
        eval {
            print "Disabling the VIP on old master: $orig_master_host \n";&stop_vip();
            $exit_code = 0;
        };
        if ($@) {
            warn "Got Error: $@\n";
            exit $exit_code;
        }
        exit $exit_code;
    }
    elsif ( $command eq "start" ) {

        my $exit_code = 10;
        eval {
            print "Enabling the VIP - $vip on the new master - $new_master_host \n";&start_vip();
            $exit_code = 0;
        };
        if ($@) {
            warn $@;
            exit $exit_code;
        }
        exit $exit_code;
    }
    elsif ( $command eq "status" ) {
        print "Checking the Status of the script.. OK \n";
        exit 0;
    }
    else {&usage();
        exit 1;
    }
}

sub start_vip() {
    `ssh $ssh_user\@$new_master_host \" $ssh_start_vip \"`;
}
sub stop_vip() {
     return 0  unless  ($ssh_user);
    `ssh $ssh_user\@$orig_master_host \" $ssh_stop_vip \"`;
}

sub usage {
    print
    "Usage: master_ip_failover --command=start|stop|stopssh|status --orig_master_host=host --orig_master_ip=ip --orig_master_port=port --new_master_host=host --new_master_ip=ip --new_master_port=port\n";
}

  该脚本为软件自带,脚本获取方法:再mha源码包中的samples目录下有该脚本的模板,对该模板进行修改即可使用。路径如: mha4mysql-manager-0.56/samples/scripts 

  脚本修改内容

my $vip = '10.0.0.55/24';
my $key = '0';
my $ssh_start_vip = "/sbin/ifconfig eth0:$key $vip";
my $ssh_stop_vip = "/sbin/ifconfig eth0:$key down";

脚本添加执行权限否则mha无法启动

chmod +x /usr/local/bin/master_ip_failover

手动绑定VIP(主库)

ifconfig eth0:0 10.0.0.55/24

  检查

[root@db01 ~]# ip a s eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 00:0c:29:6c:7a:11 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.51/24 brd 10.0.0.255 scope global eth0
    inet 10.0.0.55/24 brd 10.0.0.255 scope global secondary eth0:0
    inet6 fe80::20c:29ff:fe6c:7a11/64 scope link 
       valid_lft forever preferred_lft forever

          至此vip漂移配置完成 

1.5.3 测试虚拟IP漂移

查看db02的slave信息

 1 mysql> show slave status\G
 2 *************************** 1. row ***************************
 3                Slave_IO_State: Waiting for master to send event
 4                   Master_Host: 10.0.0.51
 5                   Master_User: repl
 6                   Master_Port: 3306
 7                 Connect_Retry: 60
 8               Master_Log_File: mysql-bin.000007
 9           Read_Master_Log_Pos: 191
10                Relay_Log_File: db02-relay-bin.000002
11                 Relay_Log_Pos: 361
12         Relay_Master_Log_File: mysql-bin.000007
13              Slave_IO_Running: Yes
14             Slave_SQL_Running: Yes
15               Replicate_Do_DB: 
16           Replicate_Ignore_DB: 
17            Replicate_Do_Table: 
18        Replicate_Ignore_Table: 
19       Replicate_Wild_Do_Table: 
20   Replicate_Wild_Ignore_Table: 
21                    Last_Errno: 0
22                    Last_Error: 
23                  Skip_Counter: 0
24           Exec_Master_Log_Pos: 191
25               Relay_Log_Space: 564
26               Until_Condition: None
27                Until_Log_File: 
28                 Until_Log_Pos: 0
29            Master_SSL_Allowed: No
30            Master_SSL_CA_File: 
31            Master_SSL_CA_Path: 
32               Master_SSL_Cert: 
33             Master_SSL_Cipher: 
34                Master_SSL_Key: 
35         Seconds_Behind_Master: 0
36 Master_SSL_Verify_Server_Cert: No
37                 Last_IO_Errno: 0
38                 Last_IO_Error: 
39                Last_SQL_Errno: 0
40                Last_SQL_Error: 
41   Replicate_Ignore_Server_Ids: 
42              Master_Server_Id: 51
43                   Master_UUID: c8fcd56e-eb79-11e7-97b0-000c296c7a11
44              Master_Info_File: /application/mysql-5.6.36/data/master.info
45                     SQL_Delay: 0
46           SQL_Remaining_Delay: NULL
47       Slave_SQL_Running_State: Slave has read all relay log; waiting for the slave I/O thread to update it
48            Master_Retry_Count: 86400
49                   Master_Bind: 
50       Last_IO_Error_Timestamp: 
51      Last_SQL_Error_Timestamp: 
52                Master_SSL_Crl: 
53            Master_SSL_Crlpath: 
54            Retrieved_Gtid_Set: 
55             Executed_Gtid_Set: c8fcd56e-eb79-11e7-97b0-000c296c7a11:1-3
56                 Auto_Position: 1
57 1 row in set (0.00 sec)

停掉主库

[root@db01 ~]# /etc/init.d/mysqld stop

在db03上查看从库slave信息

 1 mysql>  show slave status\G
 2 *************************** 1. row ***************************
 3                Slave_IO_State: Waiting for master to send event
 4                   Master_Host: 10.0.0.52
 5                   Master_User: repl
 6                   Master_Port: 3306
 7                 Connect_Retry: 60
 8               Master_Log_File: mysql-bin.000009
 9           Read_Master_Log_Pos: 191
10                Relay_Log_File: db03-relay-bin.000002
11                 Relay_Log_Pos: 361
12         Relay_Master_Log_File: mysql-bin.000009
13              Slave_IO_Running: Yes
14             Slave_SQL_Running: Yes
15               Replicate_Do_DB: 
16           Replicate_Ignore_DB: 
17            Replicate_Do_Table: 
18        Replicate_Ignore_Table: 
19       Replicate_Wild_Do_Table: 
20   Replicate_Wild_Ignore_Table: 
21                    Last_Errno: 0
22                    Last_Error: 
23                  Skip_Counter: 0
24           Exec_Master_Log_Pos: 191
25               Relay_Log_Space: 564
26               Until_Condition: None
27                Until_Log_File: 
28                 Until_Log_Pos: 0
29            Master_SSL_Allowed: No
30            Master_SSL_CA_File: 
31            Master_SSL_CA_Path: 
32               Master_SSL_Cert: 
33             Master_SSL_Cipher: 
34                Master_SSL_Key: 
35         Seconds_Behind_Master: 0
36 Master_SSL_Verify_Server_Cert: No
37                 Last_IO_Errno: 0
38                 Last_IO_Error: 
39                Last_SQL_Errno: 0
40                Last_SQL_Error: 
41   Replicate_Ignore_Server_Ids: 
42              Master_Server_Id: 52
43                   Master_UUID: c8fa1d13-eb79-11e7-97b0-000c29d60ab3
44              Master_Info_File: /application/mysql-5.6.36/data/master.info
45                     SQL_Delay: 0
46           SQL_Remaining_Delay: NULL
47       Slave_SQL_Running_State: Slave has read all relay log; waiting for the slave I/O thread to update it
48            Master_Retry_Count: 86400
49                   Master_Bind: 
50       Last_IO_Error_Timestamp: 
51      Last_SQL_Error_Timestamp: 
52                Master_SSL_Crl: 
53            Master_SSL_Crlpath: 
54            Retrieved_Gtid_Set: 
55             Executed_Gtid_Set: c8fcd56e-eb79-11e7-97b0-000c296c7a11:1-3
56                 Auto_Position: 1
57 1 row in set (0.00 sec)

在db01上查看vip信息

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 00:0c:29:6c:7a:11 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.51/24 brd 10.0.0.255 scope global eth0
    inet6 fe80::20c:29ff:fe6c:7a11/64 scope link 
       valid_lft forever preferred_lft forever

在db02上查看vip信息

 [root@db02 ~]# ip a s eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 00:0c:29:d6:0a:b3 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.52/24 brd 10.0.0.255 scope global eth0
inet 10.0.0.55/24 brd 10.0.0.255 scope global secondary eth0:0
    inet6 fe80::20c:29ff:fed6:ab3/64 scope link 
       valid_lft forever preferred_lft forever

         至此,VIP漂移就测试成功

1.6 配置binlog-server

1.6.1 配置binlog-server

1)前期准备:

1、准备一台新的mysql实例(db03),GTID必须开启。

2、将来binlog接收目录,不能和主库binlog目录一样

2)停止mha

masterha_stop --conf=/etc/mha/app1.cnf

3)在app1.cnf开启binlogserver功能

    [binlog1]
    no_master=1
    hostname=10.0.0.53                         ----> 主机DB03
    master_binlog_dir=/data/mysql/binlog/  ----> binlog保存目录

4)开启binlog接收目录,注意权限

mkdir -p /data/mysql/binlog/ 
chown -R mysql.mysql /data/mysql
# 进入目录启动程序
  cd /data/mysql/binlog/ &&\
  mysqlbinlog  -R --host=10.0.0.51 --user=mha --password=mha --raw  --stop-never mysql-bin.000001 &

  参数说明:-R 远程主机

5)启动mha

nohup masterha_manager --conf=/etc/mha/app1.cnf --remove_dead_master_conf --ignore_last_failover < /dev/null > /var/log/mha/app1/manager.log 2>&1 &

1.6.2 测试binlog备份

#查看binlog目录中的binlog

[root@db03 binlog]# ll
total 44
-rw-r--r-- 1 root root 285 Mar  8 03:11 mysql-bin.000001

#登录主库

[root@mysql-db01 ~]# mysql -uroot -p123

#刷新binlog

mysql> flush logs;

#再次查看binlog目录

[root@db03 binlog]# ll
total 48
-rw-r--r-- 1 root root 285 Mar  8 03:11 mysql-bin.000001
-rw-r--r-- 1 root root 143 Mar  8 04:00 mysql-bin.000002

1.7 mysql中间件Atlas

1.7.1 atlas简介

  Atlas是由 Qihoo 360公司Web平台部基础架构团队开发维护的一个基于MySQL协议的数据中间层项目。它在MySQL官方推出的MySQL-Proxy 0.8.2版本的基础上,修改了大量bug,添加了很多功能特性。目前该项目在360公司内部得到了广泛应用,很多MySQL业务已经接入了Atlas平台,每天承载的读写请求数达几十亿条。

  同时,有超过50家公司在生产环境中部署了Atlas,超过800人已加入了我们的开发者交流群,并且这些数字还在不断增加。而且安装方便。配置的注释写的蛮详细的,都是中文。

1.7.2 主要功能

  读写分离、从库负载均衡、自动分表、IP过滤

  SQL语句黑白名单、DBA可平滑上下线DB、自动摘除宕机的DB

Atlas相对于官方MySQL-Proxy的优势

1.将主流程中所有Lua代码用C重写,Lua仅用于管理接口

2.重写网络模型、线程模型

3.实现了真正意义上的连接池

 

4.优化了锁机制,性能提高数十倍 

1.7.3 使用场景

  Atlas是一个位于前端应用与后端MySQL数据库之间的中间件,它使得应用程序员无需再关心读写分离、分表等与MySQL相关的细节,可以专注于编写业务逻辑,同时使得DBA的运维工作对前端应用透明,上下线DB前端应用无感知。

 

  Atlas是一个位于应用程序与MySQL之间中间件。在后端DB看来,Atlas相当于连接它的客户端,在前端应用看来,Atlas相当于一个DB。

  Atlas作为服务端与应用程序通讯,它实现了MySQL的客户端和服务端协议,同时作为客户端与MySQL通讯。它对应用程序屏蔽了DB的细节,同时为了降低MySQL负担,它还维护了连接池.

1.7.4 企业读写分离及分库分表其他方案介绍

    Mysql-proxy(oracle)

    Mysql-router(oracle)

    Atlas (Qihoo 360)

    Atlas-sharding (Qihoo 360)

    Cobar(是阿里巴巴(B2B)部门开发)

    Mycat(基于阿里开源的Cobar产品而研发)

    TDDL Smart Client的方式(淘宝)

    Oceanus(58同城数据库中间件)

    OneProxy(原支付宝首席架构师楼方鑫开发 )

    vitess(谷歌开发的数据库中间件)

    Heisenberg(百度)

    TSharding(蘑菇街白辉)

    Xx-dbproxy(金山的Kingshard、当当网的sharding-jdbc )

    amoeba

1.7.5 安装Atlas

  软件获取地址: https://github.com/Qihoo360/Atlas/releases

注意:

1、Atlas只能安装运行在64位的系统上

2、Centos 5.X安装 Atlas-XX.el5.x86_64.rpm,Centos 6.X安装Atlas-XX.el6.x86_64.rpm。

3、后端mysql版本应大于5.1,建议使用Mysql 5.6以上

  Atlas (普通) : Atlas-2.2.1.el6.x86_64.rpm

  Atlas (分表) : Atlas-sharding_1.0.1-el6.x86_64.rpm

 下载安装atlas

wget https://github.com/Qihoo360/Atlas/releases/download/2.2.1/Atlas-2.2.1.el6.x86_64.rpm
rpm -ivh Atlas-2.2.1.el6.x86_64.rpm

         至此安装完成

1.7.6 配置Atlas配置文件

  atlas配置文件中的密码需要加密,可以使用,软件自带的加密工具进行加密

cd /usr/local/mysql-proxy/conf/
/usr/local/mysql-proxy/bin/encrypt  密码      ---->制作加密密码

生产密文密码:

[root@db03 bin]# /usr/local/mysql-proxy/bin/encrypt 123
3yb5jEku5h4=
[root@db03 bin]# /usr/local/mysql-proxy/bin/encrypt mha
O2jBXONX098=

编辑配置文件

vim  /usr/local/mysql-proxy/conf/test.cnf
[mysql-proxy]
admin-username = user
admin-password = pwd
proxy-backend-addresses = 10.0.0.55:3306
proxy-read-only-backend-addresses = 10.0.0.52:3306,10.0.0.53:3306
pwds = repl:3yb5jEku5h4=,mha:O2jBXONX098=
daemon = true
keepalive = true
event-threads = 8
log-level = message
log-path = /usr/local/mysql-proxy/log
sql-log=ON
proxy-address = 0.0.0.0:33060
admin-address = 0.0.0.0:2345
charset=utf8

配置文件内为全中文注释,这里有一份较为详细的解释:

 View Code Atlas配置文件说明

1.7.7 启动Atlas

编写一个atlas的管理脚本,当然也可以写脚本,可以直接手动的管理:

/usr/local/mysql-proxy/bin/mysql-proxyd test start   #启动
/usr/local/mysql-proxy/bin/mysql-proxyd test stop    #停止
/usr/local/mysql-proxy/bin/mysql-proxyd test restart #重启

  注意:test是配置文件的名称

脚本内容:

 1 [root@db03 ~]# cat /etc/init.d/atlasd 
 2 #!/bin/sh
 3 #
 4 # atlas:    Atlas Daemon
 5 #
 6 # chkconfig:    - 90 25
 7 # description:  Atlas Daemon
 8 # user:clsn
 9 # Blog: http://blog.nmtui.com
10 # Source function library.
11 
12 Demo=test
13 
14 start()
15 {
16         echo -n $"Starting atlas: "
17         /usr/local/mysql-proxy/bin/mysql-proxyd $Demo start
18 }
19 stop()
20 {
21         echo -n $"Shutting down atlas: "
22         /usr/local/mysql-proxy/bin/mysql-proxyd $Demo stop
23 }
24 status()
25 {
26         echo  $"Atlas status: "
27         /usr/local/mysql-proxy/bin/mysql-proxyd $Demo status
28 }
29 restart()
30 {
31         echo $"Atlas Restart Info: "
32         /usr/local/mysql-proxy/bin/mysql-proxyd $Demo restart
33 }
34 
35 
36 ATLAS="/usr/local/mysql-proxy/bin/mysql-proxyd"
37 [ -f $ATLAS ] || exit 1
38 # See how we were called.
39 case "$1" in
40         start)
41                 start
42                 ;;
43         stop)
44                 stop
45                 ;;
46         restart)
47                 restart
48                 ;;
49         status)
50                 status
51                 ;;
52         *)
53                 echo $"Usage: $0 {start|stop|restart|status}"
54                 exit 1
55 esac
56 exit 0

检查端口是否正常

[root@db03 ~]# netstat -lntup|grep mysql-proxy
tcp        0      0 0.0.0.0:33060               0.0.0.0:*                   LISTEN      2125/mysql-proxy    
tcp        0      0 0.0.0.0:2345                0.0.0.0:*                   LISTEN      2125/mysql-proxy

1.7.8 Atlas管理操作

  登入管理接口

[root@db03 ~]# mysql -uuser -ppwd -h127.0.0.1 -P2345

  查看帮助信息

mysql> SELECT * FROM help;

  查看后端的代理库

mysql> SELECT * FROM backends;
+-------------+----------------+-------+------+
| backend_ndx | address        | state | type |
+-------------+----------------+-------+------+
|           1 | 10.0.0.55:3306 | up    | rw   |
|           2 | 10.0.0.52:3306 | up    | ro   |
|           3 | 10.0.0.53:3306 | up    | ro   |
+-------------+----------------+-------+------+
3 rows in set (0.00 sec)

  平滑摘除mysql

mysql>  REMOVE BACKEND 2;
Empty set (0.00 sec)

   检查是否摘除

mysql> SELECT * FROM backends;
+-------------+----------------+-------+------+
| backend_ndx | address        | state | type |
+-------------+----------------+-------+------+
|           1 | 10.0.0.55:3306 | up    | rw   |
|           2 | 10.0.0.53:3306 | up    | ro   |
+-------------+----------------+-------+------+
2 rows in set (0.00 sec)

  保存到配置文件中

mysql> SAVE CONFIG;

  将节点再添加回来

mysql> add slave 10.0.0.52:3306;
Empty set (0.00 sec)

  查看是否添加成功

mysql> SELECT * FROM backends;
+-------------+----------------+-------+------+
| backend_ndx | address        | state | type |
+-------------+----------------+-------+------+
|           1 | 10.0.0.55:3306 | up    | rw   |
|           2 | 10.0.0.53:3306 | up    | ro   |
|           3 | 10.0.0.52:3306 | up    | ro   |
+-------------+----------------+-------+------+
3 rows in set (0.00 sec)

 保存到配置文件中

mysql> SAVE CONFIG;

1.7.9 连接数据库查看负载

通过atlas登陆数据,注意,使用的是数据库上的用户及密码

shell> mysql -umha -pmha -h127.0.0.1 -P33060

第一次查询server_id

mysql> show variables like "server_id";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| server_id     | 53    |
+---------------+-------+
1 row in set (0.00 sec)

第二次查询server_id

mysql> show variables like "server_id";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| server_id     | 52    |
+---------------+-------+
1 row in set (0.00 sec)

          通过上面可以看到负载成功

1.7.10 读写分离的说明

  Atlas会透明的将事务语句和写语句发送至主库执行,读语句发送至从库执行。具体以下语句会在主库执行:

显式事务中的语句

autocommit=0时的所有语句

含有select GET_LOCK()的语句

除SELECT、SET、USE、SHOW、DESC、EXPLAIN外的。

从库负载均衡配置

proxy-read-only-backend-addresses=ip1:port1@权重,ip2:port2@权重

1.7.11 Atlas高级功能

自动分表

  使用Atlas的分表功能时,首先需要在配置文件test.cnf设置tables参数。

tables参数设置格式:数据库名.表名.分表字段.子表数量,比如:

你的数据库名叫school,表名叫stu,分表字段叫id,总共分为2张表,那么就写为school.stu.id.2,如果还有其他的分表,以逗号分隔即可。

用户需要手动建立2张子表(stu_0,stu_1,注意子表序号是从0开始的)。

所有的子表必须在DB的同一个database里。

当通过Atlas执行(SELECT、DELETE、UPDATE、INSERT、REPLACE)操作时,Atlas会根据分表结果(id%2=k),定位到相应的子表(stu_k)。

例如,执行select * from stu where id=3;,Atlas会自动从stu_1这张子表返回查询结果。

但如果执行SQL语句(select * from stu;)时不带上id,则会提示执行stu表不存在。

Atles功能的说明

Atlas暂不支持自动建表和跨库分表的功能。

Atlas目前支持分表的语句有SELECT、DELETE、UPDATE、INSERT、REPLACE。

IP过滤:client-ips

  该参数用来实现IP过滤功能。

  在传统的开发模式中,应用程序直接连接DB,因此DB会对部署应用的机器(比如web服务器)的IP作访问授权。

在引入中间层后,因为连接DB的是Atlas,所以DB改为对部署Atlas的机器的IP作访问授权,如果任意一台客户端都可以连接Atlas,就会带来潜在的风险。

  client-ips参数用来控制连接Atlas的客户端的IP,可以是精确IP,也可以是IP段,以逗号分隔写在一行上即可。

  如: client-ips=192.168.1.2, 192.168.2 

  这就代表192.168.1.2这个IP和192.168.2.*这个段的IP可以连接Atlas,其他IP均不能连接。如果该参数不设置,则任意IP均可连接Atlas。如果设置了client-ips参数,且Atlas前面挂有LVS,则必须设置lvs-ips参数,否则可以不设置lvs-ips。

SQL语句黑白名单功能: Atlas会屏蔽不带where条件的delete和update操作,以及sleep函数。

1.8 Atlas-Sharding版本

1.8.1 版本介绍

  Sharding的基本思想就是把一个数据表中的数据切分成多个部分,  存放到不同的主机上去(切分的策略有多种),  从而缓解单台机器的性能跟容量的问题.

  sharding是一种水平切分, 适用于单表数据庞大的情景. 目前atlas支持静态的

  sharding方案, 暂时不支持数据的自动迁移以及数据组的动态加入.

  Atlas以表为单位sharding, 同一个数据库内可以同时共有sharding的表和不sharding的表, 不sharding的表数据存在未sharding的数据库组中.

  目前Atlas sharding支持insert, delete, select, update语句,  只支持不跨shard的事务. 所有的写操作如insert, delete,  update只能一次命中一个组, 否则会报"ERROR 1105 (HY000):write operation  is only allow to one dbgroup!"错误.

  由于sharding取替了Atlas的分表功能, 所以在Sharding分支里面,  Atlas单机分表的功能已经移除, 配置tables将不会再有效.

1.8.2 Atlas-Sharding架构

 

1.8.3 Sharding配置示例

  Atlas支持非sharding跟sharding的表共存在同一个Atlas中, 2.2.1之前的配置可以直接运行. 之前的配置如

proxy-backend-addresses = 192.168.0.12:3306
proxy-read-only-backend-addresses = 192.168.0.13:3306,192.168.0.14:3306 ... 

  这配置了一个master和两个slave,这属于非sharding的组, 所有非sharding的表跟语句都会发往这个组内. 

  所以之前没有Sharding的Atlas的表可以无缝的在新版上使用,

          注意: 非Sharding的组只能配置一个, 而sharding的组可以配置多个. 下面的配置, 配置了Sharding的组,  注意与上面的配置区分

[shardrule-0]
table = test.sharding_test

  分表名,有数据库+表名组成  t

ype = range

  sharding类型:range 或 hash

shard-key = id

  sharding 字段

groups = 0:0-999,1:1000-1999

  分片的group,如果是range类型的sharding,则groups的格式是:group_id:id范围。如果是hash类型的sharding,则groups的格式是:group_id。例如groups = 0, 1

[group-0]
proxy-backend-addresses=192.168.0.15:3306
proxy-read-only-backend-addresses=192.168.0.16:3306
[group-1]
proxy-backend-addresses=192.168.0.17:3306
proxy-read-only-backend-addresses=192.168.0.18:3306

1.8.4 Sharding限制

关于支持的语句

  Atlas sharding只对sql语句提供有限的支持, 目前支持基本的Select, insert/replace, delete,update语句,支持全部的Where语法(SQL-92标准), 不支持DDL(create drop alter)以及一些管理语句,DDL请直连MYSQL执行, 请只在Atlas上执行Select, insert, delete, update(CRUD)语句.

  对于以下语句, 如果语句命中了多台dbgroup, Atlas均未做支持(如果语句只命中了一个dbgroup, 如  select count(*) from test where id < 1000, 其中dbgroup0范围是0 - 1000,  那么这些特性都是支持的)Limit Offset(支持Limit)  

Order by

Group by  Join

ON

Count, Max, Min等函数

增加节点

  注意: 暂时只支持range方式的节点扩展, hash方式由于需要数据迁移, 暂时未做支持.

  扩展节点在保证原来节点的范围不改变的情况下, 如已有dbgroup0为范围0 - 999, dbgroup1为范围 1000-1999, 这个时候可以增加范围>2000的节点. 如增加一个节点为2000 - 2999, 修改配置文件,  重启Atlas即可.

1.9 参考文献

[1]  http://www.cnblogs.com/cenalulu/p/4309009.html

[2]  https://yq.aliyun.com/articles/57731

[3]  https://www.jianshu.com/p/b68e429d09c7

[4]  https://www.cnblogs.com/yyhh/archive/2015/12/29/5084844.html

[5]  http://blog.csdn.net/jhq0113/article/details/44302703

[6]  http://blog.csdn.net/jhq0113/article/details/44239823

[7]  https://www.guokr.com/blog/475765/

[8]  http://blog.csdn.net/bluishglc/article/details/6161475/



容器平台选型的十大模式:Docker、DC/OS、K8S 谁与当先?(上)-社区博客-网易云

$
0
0

作者:刘超 网易云基础服务
无论是在社区,还是在同客户交流的过程中,总会被问到到底什么时候该用 Docker?什么时候用虚拟机?如果使用容器,应该使用哪个容器平台?
 
显而易见,我不会直接给大家一个答案,而是希望从技术角度进行分析具体的场景。例如客户是大公司还是小公司,将部署小集群还是大集群,倾向于私有云还是公有云,已经采购了 IaaS 还是没有 IaaS,IT 运维能力强还是弱,是否需要物理机、虚拟机、容器的混合部署,是一般的并发系统还是高并发,这里面所应该做的技术选型都不一样。举个例子,如果你是一个初创型的主营业务非 IT 的小公司,自然不应该花大力气在数据中心里面自己搭建一套大规模、高并发、高性能的容器平台。
 
接下来,首先,我们来谈下什么情况下应该使用 Docker 的问题。
 

 
如上图所示,左面是我们经常挂在嘴边的所谓容器的优势,但是虚拟机都能一一怼回去。
 
如果部署的是一个传统的应用,这个应用启动速度慢,进程数量少,基本不更新,那么虚拟机完全能够满足需求。
 
○ 应用启动慢:应用启动 15 分钟,容器本身秒级,虚拟机很多平台能优化到十几秒,两者几乎看不出差别;
○ 内存占用大:动不动 32G,64G 内存,一台机器跑不了几个;
○ 基本不更新:半年更新一次,虚拟机镜像照样能够升级和回滚;
○ 应用有状态:停机会丢数据,如果不知道丢了什么,就算秒级启动也没有用,照样恢复不了,而且还有可能因为丢数据,在没有修复的情况下,盲目重启带来数据混乱;
○ 进程数量少:两三个进程相互配置一下,不用服务发现,配置不麻烦
 
如果是一个传统应用,根本没有必要花费精力去容器化,因为白花了力气,享受不到好处。
 

 
那么什么情况下,才应该考虑做一些改变呢:
 
传统业务突然被互联网业务冲击了,应用老是变,三天两头要更新,而且流量增大了,原来支付系统是取钱刷卡的,现在要互联网支付了,流量扩大了 N 倍。
 
这种情况下就只能:拆。
 
拆开了,每个子模块独自变化,相互影响变少。
拆开了,原来一个进程扛流量,现在多个进程一起扛。
 
这被称为微服务。
 

 
微服务场景下,进程多,更新快,于是出现 100 个进程,每天一个镜像。
 
容器乐了,每个容器镜像小,没什么问题,虚拟机哭了,因为虚拟机每个镜像太大了。
 
所以微服务场景下,可以开始考虑用容器了。
 

 
这时虚拟机又怒了,我不用容器了,微服务拆分之后,用 Ansible 自动部署是一样的。
 
这从技术角度来讲没有任何问题,问题是从组织角度出现的。一般的公司,开发会比运维多得多,开发写完代码就不用管了,环境的部署完全是运维负责,运维为了自动化,写 Ansible 脚本来解决问题。
 
然而这么多进程,又拆又合并的,更新这么快,配置总是变,Ansible 脚本也要常改,每天都上线,不得累死运维。
 
所以在如此大的工作量情况下,运维很容易出错,哪怕通过自动化脚本。这时,容器就可以作为一个非常好的工具运用起来。
 
除了容器从技术角度,能够使得大部分的内部配置可以放在镜像里面之外,更重要的是从流程角度,将环境配置这件事情,往前推了,推到了开发这里,要求开发完毕之后,就需要考虑环境部署的问题,而不能当甩手掌柜。
 
这样做的好处就是,虽然进程多,配置变化多,更新频繁,但是对于某个模块的开发团队来讲,这个量是很小的,因为 5-10 个人专门维护这个模块的配置和更新,不容易出错。
 
如果这些工作量全交给少数的运维团队,不但信息传递会使得环境配置不一致,部署量也会大非常多。
 
容器是一个非常好的工具,就是让每个开发仅仅多做 5% 的工作,就能够节约运维 200% 的工作量,并且不容易出错。
 
然而原来运维该做的事情开发做了,开发的老大愿意么?开发的老大会投诉运维的老大么?
 
这就不是技术问题了,其实这就是 DevOps,DevOps 不是不区分开发和运维,而是公司从组织到流程能够打通,看如何合作,边界如何划分,对系统的稳定性更有好处。
 
所以微服务、DevOps、容器是相辅相成,不可分割的。不是微服务,根本不需要容器,虚拟机就能搞定,不需要 DevOps,一年部署一次,开发和运维沟通再慢都能搞定。
 
所以,容器的本质是基于镜像的跨环境迁移。
 
镜像是容器的根本性发明,是封装和运行的标准,其它什么 namespace,cgroup,早就有了,这是技术方面。
 
在流程方面,镜像是 DevOps 的良好工具。
 
容器是为了跨环境迁移的,第一种迁移的场景是开发、测试、生产环境之间的迁移。如果不需要迁移,或者迁移不频繁,虚拟机镜像也行,但总是要迁移,带着几百 G 的虚拟机镜像,太大了。
 
第二种迁移的场景是跨云迁移,跨公有云,跨 Region,跨两个 OpenStack 的虚拟机迁移都是非常麻烦,甚至不可能的,因为公有云不提供虚拟机镜像的下载和上传功能,而且虚拟机镜像太大了,一传传一天。
 
所以跨云场景下,混合云场景下,容器也是很好的使用场景。这也同时解决了仅仅私有云资源不足,扛不住流量的问题。
 
所以这是我认为的容器的本质,是最终应该使用容器的正确姿势,当然一开始你不一定完全按照这个来。
 

模式一:公有云虚拟机

 
适合场景:初创公司,无信息安全担忧
 
如果您是一家初创公司,人员少,IT 运维能力不足,要部署的系统很少,能够花在 IT 系统上的资金有限,当然应该选择公有云的虚拟机部署,它能够解决您的如下问题:
 
○ 基层 IT 资源的管理交给公有云平台,公司自身运维人员仅需要基本的 Linux 能力;
○ 少量的部署系统,例如 10 台以下的虚拟机,往往替换一个 war,重启 Tomcat 就能解决,如果稍微虚拟机多一点 10 到 20 台,Ansible 脚本可以很好地解决这个问题;
○ 公有云按量按时收费,可以在花费很少的情况下启动,并且在业务飞速扩展的时候,迅速申请大量虚拟机;
 
这里所说的信息安全担忧,真的仅仅是心理的担忧,公有云往往有大量的安全机制来保证每个租户的安全隔离,只要用好了这些机制,公有云的安全性绝对大于一般公司自己搭建的数据中心, 当客户在说要安全的时候,客户在想什么? 这篇文章讲到了绝对的端到端解决方案。
 
这里贴张图说明公有云的安全性:
 

 

公有云为支撑自身高并发业务积累了更强的安全防护能力和更多的安全防护经验:

 
○ 多线 BGP,外网线路冗余
○ 高吞吐量的 DDoS 外网防护
○ 更完善的防火墙,入侵检测,WAF
○ 更完善的流量清洗规则
 

公有云为支撑自身高并发业务推出了更安全、更高可靠、更高可用的 PaaS 服务:

 
数据库:
○ 高可用:主备切换数据零丢失
○ 高可靠:同城双活,异地备份
○ 安全性:访问控制,IP 白名单
 
对象存储:
○ 高可靠:超大容量,三份备份,异地同步
○ 安全性:访问控制,防盗链
 

公有云为支撑自身高并发业务推出更完善的监控运维的系统,流程,经验:

 
完善的监控系统,保障大促期间系统故障的快速定位和排障
保障大促能够极大的提升和训练一支有经验的运维团队
大促的业务层面的数据对运维也是机密的,需要流程保障

 

道高一尺魔高一丈,公有云为保证自身业务的安全性对云平台不断升级:

 
○ 越来越强的 DDoS 防护
○ 越来越完善的防火墙规则
○ 最新的云平台安全功能和机制
○ 不断更新的虚拟机和容器镜像建设漏洞
○ 不断更新的病毒库
 

模式二:无 IaaS,裸用容器

 
适用场景:初创公司无 IaaS,有信息安全担忧
 
但是即便如此,还是有初创公司或者初创项目,也许因为心理方面,也许因为合规方面,非常担心信息安全问题,还是希望采取部署在自己机房的方式。
 
但由于是初创公司,在机房里面一般是不能部署 IaaS,因为 IaaS 平台的运维难度,优化难度更大,没有一个 50 人的团队根本玩不起来,所以一般在使用容器之前,采用的是物理机部署的方式,当物理机数目非常小,比如部署 5 到 10 个应用的时候手动部署或者简单脚本部署就可以,但是一旦到了 20 个应用,手动部署和简单脚本就非常麻烦了:
 
○ 运维人员比例低,而应用相对较多
○ 部署在同一个物理机上的应用多,配置冲突,端口冲突,互相连接,运维需要一个 excel 去管理,还容易出错
○ 物理机容器被脚本和 Ansible 改的乱七八糟,难以保证环境一致性,重装物理机更加麻烦
○ 不同的应用依赖不同的操作系统和底层包,千差万别
 
这个时候,可以试一下裸用容器,即在原来的脚本,或者 Ansible 里面,将启动进程,改为使用 Docker run,可以有以下的作用:
 
○ 配置,端口隔离,冲突减少
○ 基于容器部署,使得环境一致性,安装和删除干干净净
○ 不同的操作系统和底层包,都可以用容器镜像搞定
 
在这个阶段,最简单的方式就是把容器当做虚拟机来使用,也即先启动容器,然后在里面下载 war 包等,当然也可以更进一步,将 war 包和配置直接打在容器镜像里面,这样需要一个持续集成的流程了,不仅仅是运维的事情,开发也要参与其中。
 
在这个阶段,网络的模式可以使用桥接打平的方式。
 

 
这种方式好处是访问 Docker 和访问物理机一样,可很方便地实现 Docker 里面和物理机里面的互通,兼容原来部署在物理机上的应用。
 
当然 Bridge 的性能一般,如果性能要求比较高,可使用 SR-IOV 网卡嵌入容器内。
 

 

模式三:有 IaaS,裸用容器

 
适用场景:创新项目,引入 DevOps 流程
 
有一些公司规模大一些,已经采购了 IaaS,只不过有一些创新的项目需要部署,这种状态下,基本虚拟机已经能够满足需求,而且由于能够运维 IaaS,IT 能力比较强,一般也采用了 Ansible 等部署工具。
 
这种情况下,使用容器的动力相对比较少,然而容器也是能够带来一定好处的,就是 DevOps。
 
创新项目迭代速度比较快,如果有比较多的创新项目,对运维的压力也是非常大的,这里的裸用容器和模式二的裸用容器不同的是,不是拿容器当做虚拟机来用,而是将容器当做交付物来用。
 
虽然容器化对于运维的整个过程来讲改进有限,但是关键就是要开发写一个 Dockerfile,这一点非常重要,意味着运行环境的配置提前到开发,而非直接交到运维,也即上面说的,开发 5% 的工作量增加减少大量运维工作,容器环境原子性升级回滚使得停服时间变短,可以保持开发、测试、运维环境的一致性。
 

 

模式四:使用 Docker Swarm Mode

 
适用场景:发展中公司,中等规模集群
 
当集群规模超过 50 台时,裸用容器已经非常难受了,因为网络、存储、编排、服务发现等全部要靠自己的脚本或 Ansible 来搞定,是时候引入容器平台了。
 
当容器平台规模不是很大时,Docker Swarm Mode 还是比较好用的:
 
○ 集群的维护不需要 Zookeeper,不需要 Etcd,自己内置
○ 命令行和 Docker 是一样的,用起来顺手
○ 服务发现和 DNS 是内置的
○ Docker Overlay 网络是内置的
 
总之 docker 帮你料理好了一切,你不用太关心细节,很容易就能够将集群运行起来。
 
而且可以通过 docker 命令,像在一台机器上使用容器一样使用集群上的容器,可以随时将容器当虚拟机来使用,这样对于中等规模集群,以及运维人员还是比较友好的。
 
当然内置的太多了也有缺点,就是不好定制化,不好 Debug,不好干预。当你发现有一部分性能不行时,你需要改整个代码,全部重新编译,当社区更新了,合并分支是很头疼的事情。当出现问题时,由于 Manager 大包大揽干了很多活,不知道哪一步出错了,反正就是没有返回,停在那里,如果重启整个 Manager,影响面又很大。
 

 

模式五:使用 Marathon 和 Mesos

 
使用场景:万节点集群,多定制
 
当集群规模大一些,几百个节点时,很多人就不愿意使用 Docker Swarm Mode 了,很多的选择是既没有用 DC/OS,也没有用 Kubernetes,而是仅仅用了 Marathon 和 Mesos。
 
因为 Mesos 是一个非常优秀的调度器,它的双层调度机制可以使得集群规模大很多。
 
Mesos 的调度过程如图所示:
 

 
Mesos 有 Framework、Master、Agent、Executor、Task 几部分组成。这里面有两层的 Scheduler,一层在 Master 里面,allocator 会将资源公平的分给每一个 Framework,二层在 Framework 里面,Framework 的 scheduler 将资源按规则分配给 Task。
 
其它框架的调度器是直接面对整个集群,Mesos 的优势在于,第一层调度先将整个 Node 分配给一个 Framework,然后 Framework 的调度器面对的集群规模小很多,然后在里面进行二次调度,而且如果有多个 Framework,例如有多个 Marathon,则可以并行调度不冲突。
 
详细的调度机制非常复杂,可以看 号称了解 mesos 双层调度的你,先来回答下面这五个问题!这篇文章。
 
而且 Mesos 的架构相对松耦合,有很多可以定制化的地方,从而运维人员可以根据自己的需要开发自己的模块。详细的定制方式看文章 定制化 Mesos 任务运行的几种方法。
 
这也是很多优秀的公司使用 Marathon 和 Mesos 的原因。
 
例如爱奇艺、去哪儿、携程、当当等都选择了使用 Mesos,需要提一下的是,大家如果参加社区,能发现裸用 Marathon 和 Mesos 的很多,但是整个 DC/OS 都用得比较少,而用 Marathon 和 Mesos 往往不能解决一些问题,因而这些 IT 能力非常强的互联网公司做了大量的自己的定制化,增加了 Marathon 和 Mesos 的外围模块。
 

模式六:使用开源 Kubernetes

 
使用场景:千节点集群,少定制
 
Kubernetes 模块划分得更细,模块比较多,比起裸 Marathon 和 Mesos 来讲功能丰富,而且模块之间完全的松耦合,可以非常方便地进行定制化。
 

 
而且 Kubernetes 的数据结构的设计层次比较细,非常符合微服务的设计思想。例如从容器->Pods->Deployment->Service,本来简单运行一个容器,被封装为这么多的层次,每个层次有自己的作用,每一层都可以拆分和组合,这样带来一个很大的缺点,就是学习门槛高,为了简单运行一个容器,需要先学习一大堆的概念和编排规则。
 
但是当需要部署的业务越来越复杂时,场景越来越多时,你会发现 Kubernetes 这种细粒度设计的优雅,使得你能够根据自己的需要灵活的组合,而不会因为某个组件被封装好了,从而导致很难定制。例如对于 Service 来讲,除了提供内部服务之间的发现和相互访问外,还灵活设计了 headless service,这使得很多游戏需要有状态的保持长连接有了很好的方式,另外访问外部服务时,例如数据库、缓存、headless service 相当于一个 DNS,使得配置外部服务简单很多。很多配置复杂的大型应用,更复杂的不在于服务之间的相互配置,可以有 Spring Cloud 或者 Dubbo 去解决,复杂的反而是外部服务的配置,不同的环境依赖不同的外部应用,External Name 这个提供了很好的机制。
 
包括统一的监控 cadvisor,统一的配置 confgMap,都是构建一个微服务所必须的。
 
然而 Kubernetes 当前也有一个瓶颈——集群规模还不是多么大,官方说法是几千个节点,所以超大规模的集群,还是需要有很强的 IT 能力进行定制化,这个在模式七中会说一下我们在网易云上做的事情。但是对于中等规模的集群也足够了。
 
而且 Kubernetes 社区的热度,可以使得使用开源 Kubernetes 的公司能够很快地找到帮助,等待到新功能的开发和 Bug 的解决。


 本文未结束,敬请期待下篇。

k8s docker集群搭建 - CSDN博客

$
0
0

一、Kubernetes系列之介绍篇

 
•Kubernetes介绍
1.背景介绍
  云计算飞速发展
    - IaaS
    - PaaS
    - SaaS
  Docker技术突飞猛进
    - 一次构建,到处运行
    - 容器的快速轻量
    - 完整的生态环境
2.什么是kubernetes
  首先,他是一个全新的基于容器技术的分布式架构领先方案。Kubernetes(k8s)是Google开源的容器集群管理系统(谷歌内部:Borg)。在Docker技术的基础上,为容器化的应用提供部署运行、资源调度、服务发现和动态伸缩等一系列完整功能,提高了大规模容器集群管理的便捷性。
  Kubernetes是一个完备的分布式系统支撑平台,具有完备的集群管理能力,多扩多层次的安全防护和准入机制、多租户应用支撑能力、透明的服务注册和发现机制、內建智能负载均衡器、强大的故障发现和自我修复能力、服务滚动升级和在线扩容能力、可扩展的资源自动调度机制以及多粒度的资源配额管理能力。同时Kubernetes提供完善的管理工具,涵盖了包括开发、部署测试、运维监控在内的各个环节。
Kubernetes中,Service是分布式集群架构的核心,一个Service对象拥有如下关键特征:
  • 拥有一个唯一指定的名字
  • 拥有一个虚拟IP(Cluster IP、Service IP、或VIP)和端口号
  • 能够体统某种远程服务能力
  • 被映射到了提供这种服务能力的一组容器应用上
  Service的服务进程目前都是基于Socket通信方式对外提供服务,比如Redis、Memcache、MySQL、Web Server,或者是实现了某个具体业务的一个特定的TCP Server进程,虽然一个Service通常由多个相关的服务进程来提供服务,每个服务进程都有一个独立的Endpoint(IP+Port)访问点,但Kubernetes能够让我们通过服务连接到指定的Service上。有了Kubernetes内奸的透明负载均衡和故障恢复机制,不管后端有多少服务进程,也不管某个服务进程是否会由于发生故障而重新部署到其他机器,都不会影响我们队服务的正常调用,更重要的是这个Service本身一旦创建就不会发生变化,意味着在Kubernetes集群中,我们不用为了服务的IP地址的变化问题而头疼了。
  容器提供了强大的隔离功能,所有有必要把为Service提供服务的这组进程放入容器中进行隔离。为此,Kubernetes设计了Pod对象,将每个服务进程包装到相对应的Pod中,使其成为Pod中运行的一个容器。为了建立Service与Pod间的关联管理,Kubernetes给每个Pod贴上一个标签Label,比如运行MySQL的Pod贴上name=mysql标签,给运行PHP的Pod贴上name=php标签,然后给相应的Service定义标签选择器Label Selector,这样就能巧妙的解决了Service于Pod的关联问题。
  在集群管理方面,Kubernetes将集群中的机器划分为一个Master节点和一群工作节点Node,其中,在Master节点运行着集群管理相关的一组进程kube-apiserver、kube-controller-manager和kube-scheduler,这些进程实现了整个集群的资源管理、Pod调度、弹性伸缩、安全控制、系统监控和纠错等管理能力,并且都是全自动完成的。Node作为集群中的工作节点,运行真正的应用程序,在Node上Kubernetes管理的最小运行单元是Pod。Node上运行着Kubernetes的kubelet、kube-proxy服务进程,这些服务进程负责Pod的创建、启动、监控、重启、销毁以及实现软件模式的负载均衡器。
  在Kubernetes集群中,它解决了传统IT系统中服务扩容和升级的两大难题。你只需为需要扩容的Service关联的Pod创建一个Replication Controller简称(RC),则该Service的扩容及后续的升级等问题将迎刃而解。在一个RC定义文件中包括以下3个关键信息。
  • 目标Pod的定义
  • 目标Pod需要运行的副本数量(Replicas)
  • 要监控的目标Pod标签(Label)
  在创建好RC后,Kubernetes会通过RC中定义的的Label筛选出对应Pod实例并实时监控其状态和数量,如果实例数量少于定义的副本数量,则会根据RC中定义的Pod模板来创建一个新的Pod,然后将新Pod调度到合适的Node上启动运行,知道Pod实例的数量达到预定目标,这个过程完全是自动化。
  
 Kubernetes优势:
    - 容器编排
    - 轻量级
    - 开源
    - 弹性伸缩
    - 负载均衡
•Kubernetes的核心概念
1.Master
  k8s集群的管理节点,负责管理集群,提供集群的资源数据访问入口。拥有Etcd存储服务(可选),运行Api Server进程,Controller Manager服务进程及Scheduler服务进程,关联工作节点Node。Kubernetes API server提供HTTP Rest接口的关键服务进程,是Kubernetes里所有资源的增、删、改、查等操作的唯一入口。也是集群控制的入口进程;Kubernetes Controller Manager是Kubernetes所有资源对象的自动化控制中心;Kubernetes Schedule是负责资源调度(Pod调度)的进程
 
2.Node
  Node是Kubernetes集群架构中运行Pod的服务节点(亦叫agent或minion)。Node是Kubernetes集群操作的单元,用来承载被分配Pod的运行,是Pod运行的宿主机。关联Master管理节点,拥有名称和IP、系统资源信息。运行docker eninge服务,守护进程kunelet及负载均衡器kube-proxy.
  • 每个Node节点都运行着以下一组关键进程
  • kubelet:负责对Pod对于的容器的创建、启停等任务
  • kube-proxy:实现Kubernetes Service的通信与负载均衡机制的重要组件
  • Docker Engine(Docker):Docker引擎,负责本机容器的创建和管理工作
  Node节点可以在运行期间动态增加到Kubernetes集群中,默认情况下,kubelet会想master注册自己,这也是Kubernetes推荐的Node管理方式,kubelet进程会定时向Master汇报自身情报,如操作系统、Docker版本、CPU和内存,以及有哪些Pod在运行等等,这样Master可以获知每个Node节点的资源使用情况,冰实现高效均衡的资源调度策略。、
 
3.Pod
  运行于Node节点上,若干相关容器的组合。Pod内包含的容器运行在同一宿主机上,使用相同的网络命名空间、IP地址和端口,能够通过localhost进行通。Pod是Kurbernetes进行创建、调度和管理的最小单位,它提供了比容器更高层次的抽象,使得部署和管理更加灵活。一个Pod可以包含一个容器或者多个相关容器。
  Pod其实有两种类型:普通Pod和静态Pod,后者比较特殊,它并不存在Kubernetes的etcd存储中,而是存放在某个具体的Node上的一个具体文件中,并且只在此Node上启动。普通Pod一旦被创建,就会被放入etcd存储中,随后会被Kubernetes Master调度到摸个具体的Node上进行绑定,随后该Pod被对应的Node上的kubelet进程实例化成一组相关的Docker容器冰启动起来,在。在默认情况下,当Pod里的某个容器停止时,Kubernetes会自动检测到这个问起并且重启这个Pod(重启Pod里的所有容器),如果Pod所在的Node宕机,则会将这个Node上的所有Pod重新调度到其他节点上。
 
4.Replication Controller
  Replication Controller用来管理Pod的副本,保证集群中存在指定数量的Pod副本。集群中副本的数量大于指定数量,则会停止指定数量之外的多余容器数量,反之,则会启动少于指定数量个数的容器,保证数量不变。Replication Controller是实现弹性伸缩、动态扩容和滚动升级的核心。
 
5.Service
  Service定义了Pod的逻辑集合和访问该集合的策略,是真实服务的抽象。Service提供了一个统一的服务访问入口以及服务代理和发现机制,关联多个相同Label的Pod,用户不需要了解后台Pod是如何运行。
外部系统访问Service的问题
  首先需要弄明白Kubernetes的三种IP这个问题
    Node IP:Node节点的IP地址
    Pod IP: Pod的IP地址
    Cluster IP:Service的IP地址
  首先,Node IP是Kubernetes集群中节点的物理网卡IP地址,所有属于这个网络的服务器之间都能通过这个网络直接通信。这也表明Kubernetes集群之外的节点访问Kubernetes集群之内的某个节点或者TCP/IP服务的时候,必须通过Node IP进行通信
  其次,Pod IP是每个Pod的IP地址,他是Docker Engine根据docker0网桥的IP地址段进行分配的,通常是一个虚拟的二层网络。
  最后Cluster IP是一个虚拟的IP,但更像是一个伪造的IP网络,原因有以下几点
  • Cluster IP仅仅作用于Kubernetes Service这个对象,并由Kubernetes管理和分配P地址
  • Cluster IP无法被ping,他没有一个“实体网络对象”来响应
  • Cluster IP只能结合Service Port组成一个具体的通信端口,单独的Cluster IP不具备通信的基础,并且他们属于Kubernetes集群这样一个封闭的空间。
Kubernetes集群之内,Node IP网、Pod IP网于Cluster IP网之间的通信,采用的是Kubernetes自己设计的一种编程方式的特殊路由规则。
 
6.Label
 Kubernetes中的任意API对象都是通过Label进行标识,Label的实质是一系列的Key/Value键值对,其中key于value由用户自己指定。Label可以附加在各种资源对象上,如Node、Pod、Service、RC等,一个资源对象可以定义任意数量的Label,同一个Label也可以被添加到任意数量的资源对象上去。Label是Replication Controller和Service运行的基础,二者通过Label来进行关联Node上运行的Pod。
我们可以通过给指定的资源对象捆绑一个或者多个不同的Label来实现多维度的资源分组管理功能,以便于灵活、方便的进行资源分配、调度、配置等管理工作。
一些常用的Label如下:
  • 版本标签:"release":"stable","release":"canary"......
  • 环境标签:"environment":"dev","environment":"qa","environment":"production"
  • 架构标签:"tier":"frontend","tier":"backend","tier":"middleware"
  • 分区标签:"partition":"customerA","partition":"customerB"
  • 质量管控标签:"track":"daily","track":"weekly"
  Label相当于我们熟悉的标签,给某个资源对象定义一个Label就相当于给它大了一个标签,随后可以通过Label Selector(标签选择器)查询和筛选拥有某些Label的资源对象,Kubernetes通过这种方式实现了类似SQL的简单又通用的对象查询机制。
 
  Label Selector在Kubernetes中重要使用场景如下:
    •   kube-Controller进程通过资源对象RC上定义Label Selector来筛选要监控的Pod副本的数量,从而实现副本数量始终符合预期设定的全自动控制流程
    •   kube-proxy进程通过Service的Label Selector来选择对应的Pod,自动建立起每个Service岛对应Pod的请求转发路由表,从而实现Service的智能负载均衡
    •   通过对某些Node定义特定的Label,并且在Pod定义文件中使用Nodeselector这种标签调度策略,kuber-scheduler进程可以实现Pod”定向调度“的特性
 
Kubernetes架构和组件

  - 服务分组,小集群,多集群
  - 服务分组,大集群,单集群
 
•Kubernetes 组件:
  Kubernetes Master控制组件,调度管理整个系统(集群),包含如下组件:
  1.Kubernetes API Server
    作为Kubernetes系统的入口,其封装了核心对象的增删改查操作,以RESTful API接口方式提供给外部客户和内部组件调用。维护的REST对象持久化到Etcd中存储。
  2.Kubernetes Scheduler
    为新建立的Pod进行节点(node)选择(即分配机器),负责集群的资源调度。组件抽离,可以方便替换成其他调度器。
  3.Kubernetes Controller
    负责执行各种控制器,目前已经提供了很多控制器来保证Kubernetes的正常运行。
  4. Replication Controller
    管理维护Replication Controller,关联Replication Controller和Pod,保证Replication Controller定义的副本数量与实际运行Pod数量一致。
  5. Node Controller
    管理维护Node,定期检查Node的健康状态,标识出(失效|未失效)的Node节点。
  6. Namespace Controller
    管理维护Namespace,定期清理无效的Namespace,包括Namesapce下的API对象,比如Pod、Service等。
  7. Service Controller
    管理维护Service,提供负载以及服务代理。
  8.EndPoints Controller
    管理维护Endpoints,关联Service和Pod,创建Endpoints为Service的后端,当Pod发生变化时,实时更新Endpoints。
  9. Service Account Controller
    管理维护Service Account,为每个Namespace创建默认的Service Account,同时为Service Account创建Service Account Secret。
  10. Persistent Volume Controller
    管理维护Persistent Volume和Persistent Volume Claim,为新的Persistent Volume Claim分配Persistent Volume进行绑定,为释放的Persistent Volume执行清理回收。
  11. Daemon Set Controller
    管理维护Daemon Set,负责创建Daemon Pod,保证指定的Node上正常的运行Daemon Pod。
  12. Deployment Controller
    管理维护Deployment,关联Deployment和Replication Controller,保证运行指定数量的Pod。当Deployment更新时,控制实现Replication Controller和 Pod的更新。
  13.Job Controller
    管理维护Job,为Jod创建一次性任务Pod,保证完成Job指定完成的任务数目
  14. Pod Autoscaler Controller
    实现Pod的自动伸缩,定时获取监控数据,进行策略匹配,当满足条件时执行Pod的伸缩动作。
 
•Kubernetes Node运行节点,运行管理业务容器,包含如下组件:
  1.Kubelet
    负责管控容器,Kubelet会从Kubernetes API Server接收Pod的创建请求,启动和停止容器,监控容器运行状态并汇报给Kubernetes API Server。
  2.Kubernetes Proxy
    负责为Pod创建代理服务,Kubernetes Proxy会从Kubernetes API Server获取所有的Service信息,并根据Service的信息创建代理服务,实现Service到Pod的请求路由和转发,从而实现Kubernetes层级的虚拟转发网络。
  3.Docker
    Node上需要运行容器服务

二、基于kubernetes构建Docker集群环境实战

kubernetes是google公司基于docker所做的一个分布式集群,有以下主件组成
  etcd: 高可用存储共享配置和服务发现,作为与minion机器上的flannel配套使用,作用是使每台 minion上运行的docker拥有不同的ip段,最终目的是使不同minion上正在运行的docker containner都有一个与别的任意一个containner(别的minion上运行的docker containner)不一样的IP地址。
  flannel: 网络结构支持
  kube-apiserver: 不论通过kubectl还是使用remote api 直接控制,都要经过apiserver
  kube-controller-manager: 对replication controller, endpoints controller, namespace controller, and serviceaccounts controller的循环控制,与kube-apiserver交互,保证这些controller工作
  kube-scheduler: Kubernetes scheduler的作用就是根据特定的调度算法将pod调度到指定的工作节点(minion)上,这一过程也叫绑定(bind)
  kubelet: Kubelet运行在Kubernetes Minion Node上. 它是container agent的逻辑继任者
  kube-proxy: kube-proxy是kubernetes 里运行在minion节点上的一个组件, 它起的作用是一个服务代理的角色
 
图为GIT+Jenkins+Kubernetes+Docker+Etcd+confd+Nginx+Glusterfs架构
如下:

 

环境:
centos7系统机器三台:
    10.0.0.81: 用来安装kubernetes master
    10.0.0.82: 用作kubernetes minion (minion1)
    10.0.0.83: 用作kubbernetes minion (minion2)
 
一、关闭系统运行的防火墙及selinux
1。如果系统开启了防火墙则按如下步骤关闭防火墙(所有机器)
# systemctl stop firewalld # systemctl disable firewalld
2.关闭selinux
1
2
#setenforce 0
#sed -i '/^SELINUX=/cSELINUX=disabled' /etc/sysconfig/selinux

  

 
二、MASTER安装配置
1. 安装并配置Kubernetes master(yum 方式)
1
# yum -y install etcd kubernetes

    

 配置etcd。确保列出的这些项都配置正确并且没有被注释掉,下面的配置都是如此 

1
2
3
4
5
6
#vim /etc/etcd/etcd.conf
  
ETCD_NAME=default
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379"
ETCD_ADVERTISE_CLIENT_URLS="http://localhost:2379"

   配置kubernetes

1
2
3
4
5
6
7
8
vim /etc/kubernetes/apiserver
  
KUBE_API_ADDRESS="--address=0.0.0.0"KUBE_API_PORT="--port=8080"
KUBELET_PORT="--kubelet_port=10250"
KUBE_ETCD_SERVERS="--etcd_servers=http://127.0.0.1:2379"
KUBE_SERVICE_ADDRESSES="--service-cluster-ip-range=10.254.0.0/16"
KUBE_ADMISSION_CONTROL="--admission_control=NamespaceLifecycle,NamespaceExists,LimitRanger,SecurityContextDeny,ResourceQuota"
KUBE_API_ARGS=""

   

2. 启动etcd, kube-apiserver, kube-controller-manager and kube-scheduler服务
1
# for SERVICES in etcd kube-apiserver kube-controller-manager kube-scheduler; do systemctl restart $SERVICES systemctl enable $SERVICES systemctl status $SERVICES done

3.设置etcd网络

1
#etcdctl -C 10.0.0.81:2379 set /atomic.io/network/config '{"Network":"10.1.0.0/16"}'
4. 至此master配置完成,运行kubectl get nodes可以查看有多少minion在运行,以及其状态。这里我们的minion还都没有开始安装配置,所以运行之后结果为空
1
# kubectl get nodes NAME LABELS STATUS

   

三、MINION安装配置(每台minion机器都按如下安装配置)
 
1. 环境安装和配置
1
# yum -y install flannel kubernetes

  配置kubernetes连接的服务端IP

1
2
3
#vim /etc/kubernetes/config
KUBE_MASTER="--master=http://10.0.0.81:8080"
KUBE_ETCD_SERVERS="--etcd_servers=http://10.0.0.81:2379"

   配置kubernetes  ,(请使用每台minion自己的IP地址比如10.0.0.81:代替下面的$LOCALIP)

 
1
2
3
4
5
#vim /etc/kubernetes/kubelet<br>KUBELET_ADDRESS="--address=0.0.0.0"
KUBELET_PORT="--port=10250"
# change the hostname to this host’s IP address KUBELET_HOSTNAME="--hostname_override=$LOCALIP"
KUBELET_API_SERVER="--api_servers=http://10.0.0.81:8080"
KUBELET_ARGS=""

  

2. 准备启动服务(如果本来机器上已经运行过docker的请看过来,没有运行过的请忽略此步骤)
    运行ifconfig,查看机器的网络配置情况(有docker0)
1
2
3
4
5
# ifconfig docker0
Link encap:Ethernet HWaddr 02:42:B2:75:2E:67 inet addr:172.17.0.1 Bcast:0.0.0.0 Mask:255.255.0.0 UP
BROADCAST MULTICAST MTU:1500 Metric:1 RX packets:0 errors:0 dropped:0 overruns:0 frame:0 TX packets:0
errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)

  warning:在运行过docker的机器上可以看到有docker0,这里在启动服务之前需要删掉docker0配置,在命令行运行:sudo ip link delete docker0

3.配置flannel网络

1
2
3
#vim /etc/sysconfig/flanneld
FLANNEL_ETCD_ENDPOINTS="http://10.0.0.81:2379"
FLANNEL_ETCD_PREFIX="/atomic.io/network"  
PS:其中atomic.io与上面etcd中的Network对应
 
4. 启动服务
1
# for SERVICES in flanneld kube-proxy kubelet docker; do systemctl restart $SERVICES systemctl enable $SERVICES systemctl status $SERVICES done

 

四、配置完成验证安装
    确定两台minion(10.0.0.82和10.0.0.83)和一台master(10.0.0.81)都已经成功的安装配置并且服务都已经启动了。
    切换到master机器上,运行命令kubectl get nodes 
1
2
3
4
# kubectl get nodes
NAME STATUS AGE
10.0.0.82 Ready 1m
10.0.0.83 Ready 1m

  可以看到配置的两台minion已经在master的node列表中了。如果想要更多的node,只需要按照minion的配置,配置更多的机器就可以了。

三、Kubernetes之深入了解Pod

 
1、yaml格式的Pod配置文件内容及注解
  深入Pod之前,首先我们来了解下Pod的yaml整体文件内容及功能注解。
如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# yaml格式的pod定义文件完整内容:
apiVersion: v1       #必选,版本号,例如v1
kind: Pod       #必选,Pod
metadata:       #必选,元数据
  name: string       #必选,Pod名称
  namespace: string    #必选,Pod所属的命名空间
  labels:      #自定义标签
    - name: string     #自定义标签名字
  annotations:       #自定义注释列表
    - name: string
spec:         #必选,Pod中容器的详细定义
  containers:      #必选,Pod中容器列表
  - name: string     #必选,容器名称
    image: string    #必选,容器的镜像名称
    imagePullPolicy: [Always | Never | IfNotPresent] #获取镜像的策略 Alawys表示下载镜像 IfnotPresent表示优先使用本地镜像,否则下载镜像,Nerver表示仅使用本地镜像
    command: [string]    #容器的启动命令列表,如不指定,使用打包时使用的启动命令
    args: [string]     #容器的启动命令参数列表
    workingDir: string     #容器的工作目录
    volumeMounts:    #挂载到容器内部的存储卷配置
    - name: string     #引用pod定义的共享存储卷的名称,需用volumes[]部分定义的的卷名
      mountPath: string    #存储卷在容器内mount的绝对路径,应少于512字符
      readOnly: boolean    #是否为只读模式
    ports:       #需要暴露的端口库号列表
    - name: string     #端口号名称
      containerPort: int   #容器需要监听的端口号
      hostPort: int    #容器所在主机需要监听的端口号,默认与Container相同
      protocol: string     #端口协议,支持TCP和UDP,默认TCP
    env:       #容器运行前需设置的环境变量列表
    - name: string     #环境变量名称
      value: string    #环境变量的值
    resources:       #资源限制和请求的设置
      limits:      #资源限制的设置
        cpu: string    #Cpu的限制,单位为core数,将用于docker run --cpu-shares参数
        memory: string     #内存限制,单位可以为Mib/Gib,将用于docker run --memory参数
      requests:      #资源请求的设置
        cpu: string    #Cpu请求,容器启动的初始可用数量
        memory: string     #内存清楚,容器启动的初始可用数量
    livenessProbe:     #对Pod内个容器健康检查的设置,当探测无响应几次后将自动重启该容器,检查方法有exec、httpGet和tcpSocket,对一个容器只需设置其中一种方法即可
      exec:      #对Pod容器内检查方式设置为exec方式
        command: [string]  #exec方式需要制定的命令或脚本
      httpGet:       #对Pod内个容器健康检查方法设置为HttpGet,需要制定Path、port
        path: string
        port: number
        host: string
        scheme: string
        HttpHeaders:
        - name: string
          value: string
      tcpSocket:     #对Pod内个容器健康检查方式设置为tcpSocket方式
         port: number
       initialDelaySeconds: 0  #容器启动完成后首次探测的时间,单位为秒
       timeoutSeconds: 0   #对容器健康检查探测等待响应的超时时间,单位秒,默认1秒
       periodSeconds: 0    #对容器监控检查的定期探测时间设置,单位秒,默认10秒一次
       successThreshold: 0
       failureThreshold: 0
       securityContext:
         privileged:false
    restartPolicy: [Always | Never | OnFailure]#Pod的重启策略,Always表示一旦不管以何种方式终止运行,kubelet都将重启,OnFailure表示只有Pod以非0退出码退出才重启,Nerver表示不再重启该Pod
    nodeSelector: obeject  #设置NodeSelector表示将该Pod调度到包含这个label的node上,以key:value的格式指定
    imagePullSecrets:    #Pull镜像时使用的secret名称,以key:secretkey格式指定
    - name: string
    hostNetwork:false      #是否使用主机网络模式,默认为false,如果设置为true,表示使用宿主机网络
    volumes:       #在该pod上定义共享存储卷列表
    - name: string     #共享存储卷名称 (volumes类型有很多种)
      emptyDir: {}     #类型为emtyDir的存储卷,与Pod同生命周期的一个临时目录。为空值
      hostPath: string     #类型为hostPath的存储卷,表示挂载Pod所在宿主机的目录
        path: string     #Pod所在宿主机的目录,将被用于同期中mount的目录
      secret:      #类型为secret的存储卷,挂载集群与定义的secre对象到容器内部
        scretname: string  
        items:     
        - key: string
          path: string
      configMap:     #类型为configMap的存储卷,挂载预定义的configMap对象到容器内部
        name: string
        items:
        - key: string
          path: string    

 

2、Pod基本用法:
  在使用docker时,我们可以使用docker run命令创建并启动一个容器,而在Kubernetes系统中对长时间运行的容器要求是:其主程序需要一直在前台运行。如果我们创建的docker镜像的启动命令是后台执行程序,例如Linux脚本:
  nohup ./startup.sh &
  则kubelet创建包含这个容器的pod后运行完该命令,即认为Pod执行结束,之后根据RC中定义的pod的replicas副本数量生产一个新的pod,而一旦创建出新的pod,将在执行完命令后陷入无限循环的过程中,这就是Kubernetes需要我们创建的docker镜像以一个前台命令作为启动命令的原因。
  对于无法改造为前台执行的应用,也可以使用开源工具supervisor辅助进行前台运行的功能。
****Pod可以由一个或多个容器组合而成
例如:两个容器应用的前端frontend和redis为紧耦合的关系,应该组合成一个整体对外提供服务,则应该将这两个打包为一个pod.
配置文件frontend-localredis-pod.yaml如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion:v1
kind: Pod
metadata:
  name: redis-php
  label:
    name: redis-php
spec:
  containers:
  - name: frontend
    image: kubeguide/guestbook-php-frontend:localredis
    ports:
    - containersPort: 80
  - name: redis-php
    image:kubeguide/redis-master
    ports:
    - containersPort: 6379

  

  属于一个Pod的多个容器应用之间相互访问只需要通过localhost就可以通信,这一组容器被绑定在一个环境中。
  使用kubectl create创建该Pod后,get Pod信息可以看到如下图:
1
2
3
#kubectl get gods
NAME READY STATUS RESTATS AGE
redis-php 2/2Running 0 10m

  可以看到READY信息为2/2,表示Pod中的两个容器都成功运行了.

  查看pod的详细信息,可以看到两个容器的定义和创建过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@kubernetes-master ~]# kubectl describe redis-php
the server doesn't have a resourcetype"redis-php"
[root@kubernetes-master ~]# kubectl describe pod redis-php
Name: redis-php
Namespace: default
Node: kubernetes-minion/10.0.0.23
Start Time: Wed, 12 Apr 2017 09:14:58 +0800
Labels: name=redis-php
Status: Running
IP: 10.1.24.2
Controllers: <none>
Containers:
nginx:
Container ID: docker://d05b743c200dff7cf3b60b7373a45666be2ebb48b7b8b31ce0ece9be4546ce77
Image: nginx
Image ID: docker-pullable://docker.io/nginx@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582
Port: 80/TCP
State: Running
Started: Wed, 12 Apr 2017 09:19:31 +0800

  

3、静态Pod
  静态pod是由kubelet进行管理的仅存在于特定Node的Pod上,他们不能通过API Server进行管理,无法与ReplicationController、Deployment或者DaemonSet进行关联,并且kubelet无法对他们进行健康检查。静态Pod总是由kubelet进行创建,并且总是在kubelet所在的Node上运行。
创建静态Pod有两种方式:配置文件或者HTTP方式
1)配置文件方式
  首先,需要设置kubelet的启动参数"--config",指定kubelet需要监控的配置文件所在的目录,kubelet会定期扫描该目录,冰根据目录中的 .yaml或 .json文件进行创建操作
假设配置目录为/etc/kubelet.d/配置启动参数:--config=/etc/kubelet.d/,然后重启kubelet服务后,再宿主机受用docker ps或者在Kubernetes Master上都可以看到指定的容器在列表中
由于静态pod无法通过API Server直接管理,所以在master节点尝试删除该pod,会将其变为pending状态,也不会被删除
1
2
3
4
5
#kubetctl delete pod static-web-node1
pod "static-web-node1"deleted
#kubectl get pods
NAME READY STATUS RESTARTS AGE
static-web-node1 0/1Pending 0 1s

  

  要删除该pod的操作只能在其所在的Node上操作,将其定义的.yaml文件从/etc/kubelet.d/目录下删除
1
2
#rm -f /etc/kubelet.d/static-web.yaml
#docker ps

  

4、Pod容器共享Volume
  Volume类型包括:emtyDir、hostPath、gcePersistentDisk、awsElasticBlockStore、gitRepo、secret、nfs、scsi、glusterfs、persistentVolumeClaim、rbd、flexVolume、cinder、cephfs、flocker、downwardAPI、fc、azureFile、configMap、vsphereVolume等等,可以定义多个Volume,每个Volume的name保持唯一。在同一个pod中的多个容器能够共享pod级别的存储卷Volume。Volume可以定义为各种类型,多个容器各自进行挂载操作,讲一个Volume挂载为容器内需要的目录。
如下图:

 

  如上图中的Pod中包含两个容器:tomcat和busybox,在pod级别设置Volume “app-logs”,用于tomcat想其中写日志文件,busybox读日志文件。
配置文件如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion:v1
kind: Pod
metadata:
  name: redis-php
  label:
    name: volume-pod
spec:
  containers:
  - name: tomcat
    image: tomcat
    ports:
    - containersPort: 8080
    volumeMounts:
    - name: app-logs
      mountPath:/usr/local/tomcat/logs
  - name: busybox
    image:busybox
    command: ["sh","-C","tail -f /logs/catalina*.log"]
  volumes:
  - name: app-logs
    emptyDir:{}
busybox容器可以通过kubectl logs查看输出内容
1
#kubectl logs volume-pod -c busybox 
tomcat容器生成的日志文件可以登录容器查看
1
#kubectl exec -ti volume-pod -c tomcat -- ls /usr/local/tomcat/logs
5.Pod的配置管理
  应用部署的一个最佳实践是将应用所需的配置信息于程序进行分离,这样可以使得应用程序被更好的复用,通过不用配置文件也能实现更灵活的功能。将应用打包为容器镜像后,可以通过环境变量或外挂文件的方式在创建容器时进行配置注入。ConfigMap是Kubernetes v1.2版本开始提供的一种统一集群配置管理方案。
   5.1 ConfigMap:容器应用的配置管理
  容器使用ConfigMap的典型用法如下:
  (1)生产为容器的环境变量。
  (2)设置容器启动命令的启动参数(需设置为环境变量)。
  (3)以Volume的形式挂载为容器内部的文件或目录。
  ConfigMap以一个或多个key:value的形式保存在Kubernetes系统中共应用使用,既可以用于表示一个变量的值,也可以表示一个完整的配置文件内容。
通过yuaml配置文件或者直接使用kubelet create configmap 命令的方式来创建ConfigMap
   5.2 ConfigMap的创建
   举个小例子cm-appvars.yaml来描述将几个应用所需的变量定义为ConfigMap的用法:
 
1
2
3
4
5
6
7
8
# vim cm-appvars.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: cm-appvars
data:
  apploglevel: info
  appdatadir:/var/data

 

  执行kubectl create命令创建该ConfigMap
1
2
#kubectl create -f cm-appvars.yaml
configmap "cm-appvars.yaml"created
  查看建立好的ConfigMap:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#kubectl get configmap
NAME DATA AGE
cm-appvars 2 3s
[root@kubernetes-master ~]# kubectl describe configmap cm-appvars
Name: cm-appvars
Namespace: default
Labels: <none>
Annotations: <none>
  
Data
====
appdatadir: 9 bytes
apploglevel: 4 bytes
[root@kubernetes-master ~]# kubectl get configmap cm-appvars -o yaml
apiVersion: v1
data:
appdatadir: /var/data
apploglevel: info
kind: ConfigMap
metadata:
creationTimestamp: 2017-04-14T06:03:36Z
name: cm-appvars
namespace: default
resourceVersion:"571221"
selfLink: /api/v1/namespaces/default/configmaps/cm-appvars
uid: 190323cb-20d8-11e7-94ec-000c29ac8d83 
 
  另:创建一个cm-appconfigfile.yaml描述将两个配置文件server.xml和logging.properties定义为configmap的用法,设置key为配置文件的别名,value则是配置文件的文本内容:
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: ConfigMap
metadata:
  name: cm-appvars
data:
  key-serverxml:
    <?xml Version='1.0'encoding='utf-8'?>
    <Server port="8005"shutdown="SHUTDOWN">
    .....
      </service>
    </Server>
  key-loggingproperties:
    "handlers=lcatalina.org.apache.juli.FileHandler,
    ...."
  在pod "cm-test-app"定义中,将configmap "cm-appconfigfile"中的内容以文件形式mount到容器内部configfiles目录中。
Pod配置文件cm-test-app.yaml内容如下:
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#vim cm-test-app.yaml
apiVersion: v1
kind: Pod
metadata:
  name: cm-test-app
spec:
  containers:
  - name: cm-test-app
    image: tomcat-app:v1
    ports:
    - containerPort: 8080
    volumeMounts:
    - name: serverxml                          #引用volume名
      mountPath:/configfiles                       #挂载到容器内部目录
        configMap:
          name: cm-test-appconfigfile                  #使用configmap定义的的cm-appconfigfile
          items:
          - key: key-serverxml                     #将key=key-serverxml
            path: server.xml                           #value将server.xml文件名进行挂载
          - key: key-loggingproperties                 #将key=key-loggingproperties    
            path: logging.properties                   #value将logging.properties文件名进行挂载 
  创建该Pod:
1
2
#kubectl create -f cm-test-app.yaml
Pod "cm-test-app"created  
  登录容器查看configfiles目录下的server.xml和logging.properties文件,他们的内容就是configmap “cm-appconfigfile”中定义的两个key的内容
1
2
3
#kubectl exec -ti cm-test-app -- bash
root@cm-rest-app:/# cat /configfiles/server.xml
root@cm-rest-app:/# cat /configfiles/logging.properties

 

  5.3使用ConfigMap的条件限制
  使用configmap的限制条件如下:
    • configmap必须在pod之间创建
    • configmap也可以定义为属于某个Namespace,只有处于相同namespaces中的pod可以引用
    • configmap中配额管理还未能实现
    • kubelet只支持被api server管理的pod使用configmap,静态pod无法引用
    • 在pod对configmap进行挂载操作时,容器内部职能挂载为目录,无法挂载文件。
6.Pod生命周期和重启策略
  Pod在整个生命周期过程中被定义为各种状态,熟悉Pod的各种状态有助于理解如何设置Pod的调度策略、重启策略
  Pod的状态包含以下几种,如图:
  
  Pod的重启策略(RestartPolicy)应用于Pod内所有的容器,并且仅在Pod所处的Node上由kubelet进行判断和重启操作。当某哥容器异常退出或者健康检查石柏师,kubelet将根据RestartPolicy的设置进行相应的操作
  Pod的重启策略包括Always、OnFailure及Nerver,默认值为Always。
  kubelet重启失效容器的时间间隔以sync-frequency乘以2n来计算,例如1、2、4、8倍等,最长延时5分钟,并且成功重启后的10分钟后重置该事件。
  Pod的重启策略和控制方式息息相关,当前可用于管理Pod的控制器宝库ReplicationController、Job、DaemonSet及直接通过kubelet管理(静态Pod),每种控制器对Pod的重启策略要求如下:
    • RC和DaemonSet:必须设置为Always,需要保证该容器持续运行
    • Job:OnFailure或Nerver,确保容器执行完成后不再重启
    • kubelet:在Pod失效时重启他,不论RestartPolicy设置什么值,并且也不会对Pod进行健康检查
 
7、Pod健康检查
  对Pod的健康检查可以通过两类探针来检查:LivenessProbe和ReadinessProbe
    • LivenessProbe探针:用于判断容器是否存活(running状态),如果LivenessProbe探针探测到容器不健康,则kubelet杀掉该容器,并根据容器的重启策略做响应处理
    • ReadinessProbe探针:用于判断容器是否启动完成(ready状态),可以接受请求。如果ReadinessProbe探针探测失败,则Pod的状态被修改。Endpoint Controller将从service的Endpoint中删除包含该容器所在的Pod的Endpoint。
  kubelet定制执行LivenessProbe探针来诊断容器的健康状况。LivenessProbe有三种事项方式。
 
(1)ExecAction:在容器内部执行一个命令,如果该命令的返回值为0,则表示容器健康
例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion:v1
kind: Pod
metadata:
  name: liveness-exec
  label:
    name: liveness
spec:
  containers:
  - name: tomcat
    image: grc.io/google_containers/tomcat
    args:
    -/bin/sh
    - -c
    -echook >/tmp.health;sleep10;rm-fr /tmp/health;sleep600
    livenessProbe:
      exec:
        command:
        -cat
        -/tmp/health
      initianDelaySeconds:15
      timeoutSeconds:1 
(2)TCPSocketAction:通过容器ip地址和端口号执行TCP检查,如果能够建立tcp连接表明容器健康
例:
1
2
3
4
5
6
7
8
9
10
11
12
kind: Pod
metadata:
  name: pod-with-healthcheck
spec:
  containers:
  - name: nginx
    image: nginx
    livenessProbe:
      tcpSocket:
        port: 80
      initianDelaySeconds:30
      timeoutSeconds:1
(3)HTTPGetAction:通过容器Ip地址、端口号及路径调用http get方法,如果响应的状态吗大于200且小于400,则认为容器健康
例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion:v1
kind: Pod
metadata:
  name: pod-with-healthcheck
spec:
  containers:
  - name: nginx
    image: nginx
    livenessProbe:
      httpGet:
        path:/_status/healthz
        port: 80
      initianDelaySeconds:30
      timeoutSeconds:1

  

对于每种探针方式,都需要设置initialDelaySeconds和timeoutSeconds两个参数,它们含义如下:
  • initialDelaySeconds:启动容器后首次监控检查的等待时间,单位秒
  • timeouSeconds:健康检查发送请求后等待响应的超时时间,单位秒。当发生超时就被认为容器无法提供服务无,该容器将被重启
 
8.玩转Pod调度
  在Kubernetes系统中,Pod在大部分场景下都只是容器的载体而已,通常需要通过RC、Deployment、DaemonSet、Job等对象来完成Pod的调度和自动控制功能。
  8.1 RC、Deployment:全自动调度
  RC的主要功能之一就是自动部署容器应用的多份副本,以及持续监控副本的数量,在集群内始终维护用户指定的副本数量。
在调度策略上,除了使用系统内置的调度算法选择合适的Node进行调度,也可以在Pod的定义中使用NodeSelector或NodeAffinity来指定满足条件的Node进行调度。
   1)NodeSelector:定向调度
  Kubernetes Master上的scheduler服务(kube-Scheduler进程)负责实现Pod的调度,整个过程通过一系列复杂的算法,最终为每个Pod计算出一个最佳的目标节点,通常我们无法知道Pod最终会被调度到哪个节点上。实际情况中,我们需要将Pod调度到我们指定的节点上,可以通过Node的标签和pod的nodeSelector属性相匹配来达到目的。
  (1)首先通过kubectl label命令给目标Node打上标签
kubectl label nodes <node-name> <label-key>=<label-value>
例:
1
#kubectllabel nodes k8s-node-1 zonenorth
  (2)然后在Pod定义中加上nodeSelector的设置
例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion:v1
kind: Pod
metadata:
  name: redis-master
  label:
    name: redis-master
spec:
  replicas: 1
  selector:
    name: redis-master
    template:
      metadata:
        labels:
          name: redis-master
      spec:
        containers:
        - name: redis-master
          images: kubeguide/redis-master
          ports:
          - containerPort: 6379
        nodeSelector:
          zone: north 
运行kubectl create -f命令创建Pod,scheduler就会将该Pod调度到拥有zone=north标签的Node上。 如果多个Node拥有该标签,则会根据调度算法在该组Node上选一个可用的进行Pod调度。
需要注意的是:如果集群中没有拥有该标签的Node,则这个Pod也无法被成功调度。
 
  2)NodeAffinity:亲和性调度
该调度策略是将来替换NodeSelector的新一代调度策略。由于NodeSelector通过Node的Label进行精确匹配,所有NodeAffinity增加了In、NotIn、Exists、DoesNotexist、Gt、Lt等操作符来选择Node。调度侧露更加灵活。
 
  8.2 DaemonSet:特定场景调度
DaemonSet用于管理集群中每个Node上仅运行一份Pod的副本实例,如图

 

这种用法适合一些有下列需求的应用:
  • 在每个Node上运行个以GlusterFS存储或者ceph存储的daemon进程
  • 在每个Node上运行一个日志采集程序,例如fluentd或者logstach
  • 在每个Node上运行一个健康程序,采集Node的性能数据。
DaemonSet的Pod调度策略类似于RC,除了使用系统内置的算法在每台Node上进行调度,也可以在Pod的定义中使用NodeSelector或NodeAffinity来指定满足条件的Node范围来进行调度。
 
  8.3 批处理调度
 
9.Pod的扩容和缩荣
  在实际生产环境中,我们经常遇到某个服务需要扩容的场景,也有可能因为资源精确需要缩减资源而需要减少服务实例数量,此时我们可以Kubernetes中RC提供scale机制来完成这些工作。
以redis-slave RC为例,已定义的最初副本数量为2,通过kubectl scale命令可以将Pod副本数量重新调整
1
2
3
4
5
6
7
#kubectl scale rc redis-slave --replicas=3
ReplicationController"redis-slave"scaled
#kubectl get pods
NAME READY STATUS RESTARTS AGE
redis-slave-1sf23 1/1Running 0 1h
redis-slave-54wfk 1/1Running 0 1h
redis-slave-3da5y 1/1Running 0 1h 
  除了可以手工通过kubectl scale命令完成Pod的扩容和缩容操作以外,新版本新增加了Horizontal Podautoscaler(HPA)的控制器,用于实现基于CPU使用路进行启动Pod扩容缩容的功能。该控制器基于Mastger的kube-controller-manager服务启动参数 --horizontal-pod-autoscler-sync-period定义的时长(默认30秒),周期性监控目标Pod的Cpu使用率并在满足条件时对ReplicationController或Deployment中的Pod副本数量进行调整,以符合用户定义的平均Pod Cpu使用率,Pod Cpu使用率来源于heapster组件,所以需预先安装好heapster。
 
10.Pod的滚动升级
  当集群中的某个服务需要升级时,我们需要停止目前与该服务相关的所有Pod,然后重新拉取镜像并启动。如果集群规模较大,因服务全部停止后升级的方式将导致长时间的服务不可用。由此,Kubernetes提供了rolling-update(滚动升级)功能来解决该问题。
滚动升级通过执行kubectl rolling-update命令一键完成,该命令创建一个新的RC,然后自动控制旧版本的Pod数量逐渐减少到0,同时新的RC中的Pod副本数量从0逐步增加到目标值,最终实现Pod的升级。需要注意的是,系统要求新的RC需要与旧的RC在相同的Namespace内,即不能把别人的资产转到到自家名下。
  例:将redis-master从1.0版本升级到2.0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
kind: replicationController
metadata:
  name: redis-master-v2
  labels:
    name: redis-master
    Version: v2
spec:
  replicas: 1
  selector:
    name: redis-master
    Version: v2
  template:
    labels:
      name: redis-master
      Version: v2
    spec:
      containers:
      - name: master
        images: kubeguide/redis-master:2.0
        ports:
        - containerPort: 6379

  需要注意的点:

  (1)RC的name不能与旧的RC名字相同
  (2)在sele中应至少有一个label与旧的RC的label不同,以标识为新的RC。本例中新增了一个名为version的label与旧的RC区分
  运行kubectl rolling-update来完成Pod的滚动升级:
1
#kubectl rolling-update redis-master -f redis-master-controller-v2.yaml 
  另一种方法就是不使用配置文件,直接用kubectl rolling-update加上--image参数指定新版镜像名来完成Pod的滚动升级
1
#kubectl rolling-update redis-master --image=redis-master:2.0
  与使用配置文件的方式不同的是,执行的结果是旧的RC被删除,新的RC仍然使用就的RC的名字。
  如果在更新过程总发现配置有误,则用户可以中断更新操作,并通过执行kubectl rolling-update-rollback完成Pod版本的回滚。

使用dlib中的深度残差网络(ResNet)实现实时人脸识别 - supersayajin - 博客园

$
0
0

opencv中提供的基于haar特征级联进行人脸检测的方法效果非常不好,本文使用dlib中提供的人脸检测方法(使用HOG特征或卷积神经网方法),并使用提供的深度残差网络(ResNet)实现实时人脸识别,不过本文的目的不是构建深度残差网络,而是利用已经训练好的模型进行实时人脸识别,实时性要求一秒钟达到10帧以上的速率,并且保证不错的精度。opencv和dlib都是非常好用的计算机视觉库,特别是dlib,前面文章提到了其内部封装了一些比较新的深度学习方法,使用这些算法可以实现很多应用,比如人脸检测、车辆检测、目标追踪、语义分割等等。由于这两个库相应的都包含了C++和Python的版本,而Python的配置和相对使用起来更加简单,因此这篇文章主要通过Python来实现。

先上测试的识别效果,第一张识别吴恩达和Bengio,后者我没有打标签所以识别的是“other”;另外一张gif 是识别梁朝伟、刘德华和一个女主持的过程,本地库中没有存储女主持的图片。

                                                      

              

 

因为博客园不方便上传本地视频,所以用的gif显示效果图,源视频要比gif清楚,640X480像素大小,总的来说效果识别的效果还不错。

一、准备

(1)需要安装opencv和dlib的Python库,之前的一篇文章提到了怎样安装: http://www.cnblogs.com/supersayajin/p/8446685.html;如果你有GPU并且开启了加速,那么实现的人脸识别程序速度非常快,可以满足实时性,以我运行的结果来看,检测+识别640X480像素的视频流一秒钟大约十几帧;如果你没有GPU那么速度就会很慢了,而且在检测阶段不能使用卷积神经网络的方法了,否则检测一帧数据可能需要几秒甚至几十秒:)

(2)需要一个和PC连接的摄像头;在本文中使用的是串口的摄像头,笔记本电脑集成的摄像头就是串口的,opencv中提供了直接获取串口摄像头的接口,非常方便使用;如果是网口的摄像头那么就要看摄像头提供方,比如大华、海康他们的摄像头可能会提供官方的SDK,如果有接口那是最好;或者,如果摄像头支持RTSP协议,opencv也可以通过RTSP协议获取摄像头的数据;否则可能就要写一套socket通信来实现数据传输,这个不在本文范围之内,默认使用的是串口的摄像头。

二、策略

人脸识别分为人脸检测和识别两个阶段,人脸检测会找到人脸区域的矩形窗口,识别则通过ResNet返回人脸特征向量,并进行匹配。

(1)人脸检测阶段。人脸检测算法需要用大小位置不同的窗口在图像中进行滑动,然后判断窗口中是否存在人脸。在深度学习之前的主流方法是特征提取+集成学习分类器,比如以前火热的haar特征+adaboost级联分类器,opencv中实现的人脸检测方法就采用了这种,不过实验结果来看,这种检测方法效果很不好,经常误检测人脸,或者检测不到真实的人脸;dlib中使用的是HOG(histogram of oriented gradient)+ 回归树的方法,使用dlib训练好的模型进行检测效果要好很多。dlib也使用了卷积神经网络来进行人脸检测,效果好于HOG的集成学习方法,不过需要使用GPU加速,不然程序会卡爆了,一张图片可能几秒甚至几十秒。

(2)识别阶段。识别也就是我们常说的“分类”,摄像头采集到这个人脸时,让机器判断是张三还是其他人。分类分为两个部分:

  • 特征向量抽取。本文用到的是dlib中已经训练好的ResNet模型的接口,此接口会返回一个128维的人脸特征向量。
  • 距离匹配。在获取特征向量之后可以使用欧式距离和本地的人脸特征向量进行匹配,使用最近邻分类器返回样本的标签。

根据以上,识别的大致过程如下:

图1 人脸识别分类过程

 

对于图1中的获取人脸特征向量,其过程如下:

图2 获取人脸特征向量过程

用简单的话总结,整个过程分为两个阶段,本地存储已标记人脸数据;识别阶段把从摄像头读取的人脸和本地进行匹配,得到分类结果。

三、程序实现

(1)构建本地人脸特征向量库,并且打标签。

首先加载需要的python库:

import dlib
import numpy as np
import cv2
import os
import json

 

然后加载模型参数:

detector = dlib.cnn_face_detection_model_v1('mmod_human_face_detector.dat')
sp = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat')
facerec = dlib.face_recognition_model_v1('dlib_face_recognition_resnet_model_v1.dat')

 

上面代码中的模型参数可以到这里下载: http://dlib.net/files/。detector是使用卷积神经网络(CNN)进行人脸检测的检测算子,当然如果你使用CNN的话需要使用GPU加速,否则速度会超级慢。也可以使用另一种方法,即HOG特征级联分类的检测方法,效果略差于CNN。变量sp,使用预测算子获取得到的人脸区域中的五官的几何点区域,这里加载的是68特征点的landmark模型;然后facerec会得到ResNet模型,He Kaiming(2009年和2015的CVPR best paper作者)提出的方法的一个实现,这里训练模型已经给出,因此不需要自己手动去训练了。

最后,对某个目录中的所有图片进行处理,处理的方式是一张一张地读取某个目录中的图片,每读取一张就检测人脸,如果存在人脸就使用ResNet的接口获取人脸特性向量,保存到事先准备好的矩阵中,并且按照文件名存取标签,完了之后把所有的人脸特征向量和标签都存到本地的文本文件中。注意这里给图片打标签的方式,我把每张图片命名为标签名+下划线+序号+点号+后缀名的形式,标签名是手动命名的标记名称,序号用以区分同一类中的第几张。以下是demo中存放的部分图片:

 

也有很多其他的方法打标签,这里不多举例。

复制代码
imagePath = 'LocalImage/'                                                                           #图像的目录
data = np.zeros((1,128))                                                                            #定义一个128维的空向量data
label = []                                                                                          #定义空的list存放人脸的标签

for file in os.listdir(imagePath):                                                                  #开始一张一张索引目录中的图像
    if '.jpg' in file or '.png' in file:
        fileName = file
        labelName = file.split('_')[0]                                                              #获取标签名
        print('current image: ', file)
        print('current label: ', labelName)
        img = cv2.imread(imagePath + file)                                                          #使用opencv读取图像数据
        if img.shape[0]*img.shape[1] > 500000:                                                      #如果图太大的话需要压缩,这里像素的阈值可以自己设置
            img = cv2.resize(img, (0,0), fx=0.5, fy=0.5)
        dets = detector(img, 1)                                                                     #使用检测算子检测人脸,返回的是所有的检测到的人脸区域
        for k, d in enumerate(dets):
            rec = dlib.rectangle(d.rect.left(),d.rect.top(),d.rect.right(),d.rect.bottom())
            shape = sp(img, rec)                                                                    #获取landmark
            face_descriptor = facerec.compute_face_descriptor(img, shape)                           #使用resNet获取128维的人脸特征向量
            faceArray = np.array(face_descriptor).reshape((1, 128))                                 #转换成numpy中的数据结构
            data = np.concatenate((data, faceArray))                                                #拼接到事先准备好的data当中去
            label.append(labelName)                                                                 #保存标签
            cv2.rectangle(img, (rec.left(), rec.top()), (rec.right(), rec.bottom()), (0, 255, 0), 2)       #显示人脸区域
        cv2.waitKey(2)
        cv2.imshow('image', img)

data = data[1:, :]                                                                                  #因为data的第一行是空的128维向量,所以实际存储的时候从第二行开始
np.savetxt('faceData.txt', data, fmt='%f')                                                          #保存人脸特征向量合成的矩阵到本地

labelFile=open('label.txt','w')                                      
json.dump(label, labelFile)                                                                         #使用json保存list到本地
labelFile.close()

cv2.destroyAllWindows()                                                                             #关闭所有的窗口
复制代码

 

上面的代码中,会索引imagePath这个存放图像的目录;然后定义一个128维的空向量data,在后续获取每一张人脸特征向量的时候可以往这个向量后面追加,即data的每一行是一个样本的特征向量;然后定义一个list来存储标签。之后开始索引某个目录下所有的图片文件。注意我这里用的是opencv的接口读取图像,也可以使用其他的图像读取接口,比如dlib自带的或者PIL接口中的,都可以使用,不过重要的是接口一定要统一,因为每个接口读取图片转成矩阵的数值可能会有差异。然后使用前面定义的测算子开始检测人脸,返回的是dlib中的一个数据结构,这个数据结构存储了所有检测到的人脸区域信息,对每个检测到的人脸区域获取landmark,并且调用深度残差模型的接口获取128维的人脸特征向量,之后我们把这个人脸向量存储到data中去,这里使用numpy中提供的concatenate方法进行拼接,同时把标签添加到label列表中去。最后,因为data事先定义的是一个128维的空向量,之后利用concatenate方法进行拼接得到,我们需要抛弃第一行;最后把得到的人脸特征和标签存储到本地文件。

这里使用的是CNN进行人脸检测,如果你没有GPU,或者你有GPU但没有进行GPU的配置,那么速度巨慢,此时你可以使用传统的HOG特征+级联分类的方法,不过效果没有CNN的好。这时代码的第6行中模型需要替换成:

detector = dlib.get_frontal_face_detector()

 

其余的基本保持不变。 

以上的代码可以直接运行,运行之后会检测所有的图像,类似于:

并且存取得到本地的人脸特征向量库和标签:

 

(2)实时读取摄像头进行人脸识别

在(1)中我们已经得到了本地的打过标签的人脸特征向量,这一部分是实现读取摄像头实时识别。首先加载需要的python库:

import dlib
import numpy as np
import cv2
import json

 

然后加载神经网络模型:

detector = dlib.cnn_face_detection_model_v1('mmod_human_face_detector.dat')
sp = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat')
facerec = dlib.face_recognition_model_v1('dlib_face_recognition_resnet_model_v1.dat')
threshold = 0.54

其中threshold是人脸识别的阈值,当测试图片和本地图片欧式距离最近的值大于这个值的时候,我们认为不属于本都图片的任何一个类别。然后定义最近邻分类器:

复制代码
def findNearestClassForImage(face_descriptor, faceLabel):
    temp =  face_descriptor - data
    e = np.linalg.norm(temp,axis=1,keepdims=True)
    min_distance = e.min() 
    print('distance: ', min_distance)
    if min_distance > threshold:
        return 'other'
    index = np.argmin(e)
    return faceLabel[index]
复制代码

 

 当距离值大于threshold的时候我们返回标签“other”,否则返回本地的标签,你可以根据实际情况来设置这个阈值。题外话,安全阈值很依赖于具体的场合,比如安检、银行里进行人脸验证、iPhone解锁,这些对安全要求很高的场合需要比较小的threshold来保证安全,在嫌犯追踪的时候,需要比较大的threshold以保证由嫌疑的人不会漏过。

然后是读取图像进行识别的函数:

复制代码
def recognition(img):
    dets = detector(img, 1)
    for k, d in enumerate(dets):
        print("Detection {}: Left: {} Top: {} Right: {} Bottom: {}".format(
            k, d.rect.left(), d.rect.top(), d.rect.right(), d.rect.bottom()))
        rec = dlib.rectangle(d.rect.left(),d.rect.top(),d.rect.right(),d.rect.bottom())
        print(rec.left(),rec.top(),rec.right(),rec.bottom())
        shape = sp(img, rec)
        face_descriptor = facerec.compute_face_descriptor(img, shape)        
        class_pre = findNearestClassForImage(face_descriptor, label)
        print(class_pre)
        cv2.rectangle(img, (rec.left(), rec.top()+10), (rec.right(), rec.bottom()), (0, 255, 0), 2)
        cv2.putText(img, class_pre , (rec.left(),rec.top()), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,255,0), 2, cv2.LINE_AA)

    cv2.imshow('image', img)
复制代码

 

 最后是实时读取摄像头图像,并且进行识别的过程:

复制代码
labelFile=open('label.txt','r')
label = json.load(labelFile)                                                   #载入本地人脸库的标签
labelFile.close()
    
data = np.loadtxt('faceData.txt',dtype=float)                                  #载入本地人脸特征向量

cap = cv2.VideoCapture(0)
fps = 10
size = (640,480)
fourcc = cv2.VideoWriter_fourcc(*'XVID')
videoWriter = cv2.VideoWriter('video.MP4', fourcc, fps, size)

while(1):
    ret, frame = cap.read()
    #frame = cv2.resize(frame, (0,0), fx=0.5, fy=0.5)
    recognition(frame)
    videoWriter.write(frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break
cap.release()
videoWriter.release()
cv2.destroyAllWindows()
复制代码

 在上面的代码中为了展示检测的效果,我用opencv的接口把图像保存到了视频当中。识别效果截图:

 

四、总结

利用已有的计算机视觉库可以实现很多好玩和有用的应用,本文只是粗略地展示了一个进行实时人脸识别的demo,还有很多可以改善的点来提高精度和效率,比如人脸受角度、表情影响很大,或者需要处理速度要求更高的场景;同时图像类别规模很大的情况下如何保证效果,如何优化这些都是难点。另外dlib中的提供的这些模型都是已经训练好的,我们可以到官方demo下载,demo给出了在一些benchmark中的效果,也可以自己训练得到这些模型,当然前提是你需要有GPU,并且要求很大量的数据以及丰富的调参经验,这些也都是深度学习中的点~

Python 3 利用 Dlib 实现摄像头实时人脸识别 - coneypo - 博客园

$
0
0

0.引言

  利用 Python 开发,借助 Dlib 库捕获摄像头中的人脸,提取人脸特征,通过计算特征值之间的欧氏距离,来和预存的人脸特征进行对比,判断是否匹配,达到人脸识别的目的;

  可以从摄像头中抠取人脸图片存储到本地,然后提取构建预设人脸特征;

  根据抠取的 / 已有的同一个人多张人脸图片提取 128D 特征值,然后计算该人的 128D 特征均值;

  然后和摄像头中实时获取到的人脸提取出的特征值,计算欧氏距离,判定是否为同一张人脸;  

    

   Features :

  • 支持人脸数据采集,自行建立人脸数据库
  • 调用摄像头实时人脸检测和识别
  • 支持多张人脸 ( updated on 08.23 )    

 

   人脸识别 / Face Recognition 的说明:

  Wikipedia 上关于 人脸识别系统 / Face Recognition System 的描述: they work by comparing selected facial features from given image with faces within a database.

  本项目中就是比较  预设的人脸的特征 和  摄像头实时获取到的人脸的特征 

  核心就是  提取 128D 人脸特征,然后计算 摄像头人脸特征 和 预设的特征脸的 欧式距离,进行比对;

 

  效果如下:  

 

 

图 1 摄像头多个人脸时识别效果 

 

1. 总体流程

  先说下  人脸检测 ( Face detection ) 和  人脸识别 ( Face Recognition ) ,前者是达到检测出场景中人脸的目的就可以了,而后者不仅需要检测出人脸,还要和 已有人脸数据进行比对,识别出是否在数据库中,或者进行身份标注之类处理,人脸检测和人脸识别两者有时候可能会被理解混淆;

  我的之前一些项目都是用 Dlib 做人脸检测这块,这个项目想要实现的功能是人脸识别功能,借助的是 Dlib 官网中 face_recognition.py 这个例程 ( Link: http://dlib.net/face_recognition.py.html );

  核心在于 利用“dlib_face_recognition_resnet_model_v1.dat” 这个 model,提取 人脸图像的 128D 特征,然后比对不同人脸图片的 128D 特征,设定阈值  计算欧氏距离 来判断是否为同一张脸;

1 # face recognition model, the object maps human faces into 128D vectors
2 facerec = dlib.face_recognition_model_v1("dlib_face_recognition_resnet_model_v1.dat")
3 
4 shape = predictor(img, dets[0])
5 face_descriptor = facerec.compute_face_descriptor(img, shape)

 

   

 

图 2 总体设计流程

 

2.源码介绍

  主要有

    get_face_from_camera.py , 

     get_features_into_CSV.py ,

    face_reco_from_camera.py

  这三个 Python 文件,接下来会分别介绍实现功能;

 

2.1 get_face_from_camera.py / 人脸注册录入

  人脸识别需要将  提取到的图像数据 和  已有图像数据 进行比对分析,所以这部分代码实现的功能就是  人脸录入

  程序会生成一个窗口,显示调用的摄像头实时获取的图像;

   (关于摄像头的调用方式可以参考这里:  Python 3 利用 Dlib 19.7 实现摄像头人脸检测特征点标定);

  

  然后根据键盘输入进行人脸捕获:

  • “N” 新录入人脸,新建文件夹 person_X/  用来存储某人的人脸图像
  •   "S" 开始捕获人脸,将捕获到的人脸放到 person_X/ 路径下
  • “Q” 退出窗口

  

  摄像头的调用是利用 opencv 库的  cv2.VideoCapture(0), 此处参数为 0 代表调用的是笔记本的默认摄像头,你也可以让它调用传入已有视频文件;

 

图 3  get_face_from_camera.py 的界面

   

  捕获到的一组人脸示例;

图 4 捕获到的一组人脸

 

  get_face_from_camera.py 源码

复制代码
  1 # created at 2018-05-11
  2 # updated at 2018-09-07
  3 
  4 # Author:   coneypo
  5 # Blog:     http://www.cnblogs.com/AdaminXie
  6 # GitHub:   https://github.com/coneypo/Dlib_face_recognition_from_camera
  7 
  8 # 进行人脸录入
  9 # 录入多张人脸
 10 import dlib         # 人脸识别的库 Dlib
 11 import numpy as np  # 数据处理的库 Numpy
 12 import cv2          # 图像处理的库 OpenCv
 13 import os
 14 import shutil
 15 
 16 # Dlib 预测器
 17 detector = dlib.get_frontal_face_detector()
 18 predictor = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat')
 19 
 20 # 创建 cv2 摄像头对象
 21 cap = cv2.VideoCapture(0)
 22 
 23 # cap.set(propId, value)
 24 # 设置视频参数,propId 设置的视频参数,value 设置的参数值
 25 cap.set(3, 480)
 26 
 27 # 截图 screenshoot 的计数器
 28 cnt_ss = 0
 29 
 30 # 人脸截图的计数器
 31 cnt_p = 0
 32 
 33 # 存储人脸的文件夹
 34 current_face_dir = 0
 35 
 36 # 保存
 37 path_make_dir = "data/faces_from_camera/"
 38 
 39 path_csv = "data/csvs_from_camera/"
 40 
 41 
 42 # clear the old folders at first
 43 def pre_clear():
 44     folders_rd = os.listdir(path_make_dir)
 45     for i in range(len(folders_rd)):
 46         shutil.rmtree(path_make_dir+folders_rd[i])
 47 
 48     csv_rd = os.listdir(path_csv)
 49     for i in range(len(csv_rd)):
 50         os.remove(path_csv+csv_rd[i])
 51 
 52 
 53 # clear the exist folders of faces and csv
 54 pre_clear()
 55 
 56 
 57 # 人脸种类数目的计数器
 58 person_cnt = 0
 59 
 60 # cap.isOpened() 返回 true/false 检查初始化是否成功
 61 while cap.isOpened():
 62 
 63     # cap.read()
 64     # 返回两个值:
 65     #    一个布尔值 true/false,用来判断读取视频是否成功/是否到视频末尾
 66     #    图像对象,图像的三维矩阵q
 67     flag, im_rd = cap.read()
 68 
 69     # 每帧数据延时 1ms,延时为 0 读取的是静态帧
 70     kk = cv2.waitKey(1)
 71 
 72     # 取灰度
 73     img_gray = cv2.cvtColor(im_rd, cv2.COLOR_RGB2GRAY)
 74 
 75     # 人脸数 rects
 76     rects = detector(img_gray, 0)
 77 
 78     # print(len(rects))q
 79 
 80     # 待会要写的字体
 81     font = cv2.FONT_HERSHEY_COMPLEX
 82 
 83     # 按下 'n' 新建存储人脸的文件夹
 84     if kk == ord('n'):
 85         person_cnt += 1
 86         # current_face_dir = path_make_dir + time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())
 87         current_face_dir = path_make_dir + "person_" + str(person_cnt)
 88         print('\n')
 89         for dirs in (os.listdir(path_make_dir)):
 90             if current_face_dir == path_make_dir + dirs:
 91                 shutil.rmtree(current_face_dir)
 92                 print("删除旧的文件夹:", current_face_dir)
 93         os.makedirs(current_face_dir)
 94         print("新建的人脸文件夹: ", current_face_dir)
 95 
 96         # 将人脸计数器清零
 97         cnt_p = 0
 98 
 99     if len(rects) != 0:
100         # 检测到人脸
101 
102         # 矩形框
103         for k, d in enumerate(rects):
104 
105             # 计算矩形大小
106             # (x,y), (宽度width, 高度height)
107             pos_start = tuple([d.left(), d.top()])
108             pos_end = tuple([d.right(), d.bottom()])
109 
110             # 计算矩形框大小
111             height = d.bottom() - d.top()
112             width = d.right() - d.left()
113 
114             # 根据人脸大小生成空的图像
115             cv2.rectangle(im_rd, tuple([d.left(), d.top()]), tuple([d.right(), d.bottom()]), (0, 255, 255), 2)
116             im_blank = np.zeros((height, width, 3), np.uint8)
117 
118             # 按下 's' 保存摄像头中的人脸到本地
119             if kk == ord('s'):
120                 cnt_p += 1
121                 for ii in range(height):
122                     for jj in range(width):
123                         im_blank[ii][jj] = im_rd[d.top() + ii][d.left() + jj]
124                 cv2.imwrite(current_face_dir + "/img_face_" + str(cnt_p) + ".jpg", im_blank)
125                 print("写入本地:", str(current_face_dir) + "/img_face_" + str(cnt_p) + ".jpg")
126 
127         # 显示人脸数
128     cv2.putText(im_rd, "Faces: " + str(len(rects)), (20, 100), font, 0.8, (0, 255, 0), 1, cv2.LINE_AA)
129 
130     # 添加说明
131     cv2.putText(im_rd, "Face Register", (20, 40), font, 1, (255, 255, 255), 1, cv2.LINE_AA)
132     cv2.putText(im_rd, "N: New face folder", (20, 350), font, 0.8, (255, 255, 255), 1, cv2.LINE_AA)
133     cv2.putText(im_rd, "S: Save face", (20, 400), font, 0.8, (255, 255, 255), 1, cv2.LINE_AA)
134     cv2.putText(im_rd, "Q: Quit", (20, 450), font, 0.8, (255, 255, 255), 1, cv2.LINE_AA)
135 
136     # 按下 'q' 键退出
137     if kk == ord('q'):
138         break
139 
140     # 窗口显示
141     # cv2.namedWindow("camera", 0) # 如果需要摄像头窗口大小可调
142     cv2.imshow("camera", im_rd)
143 
144 # 释放摄像头
145 cap.release()
146 
147 # 删除建立的窗口
148 cv2.destroyAllWindows()
复制代码

 

  get_face_from_camera.py 的输出 log

复制代码
删除旧的文件夹: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_1
新建的人脸文件夹:  F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_1
写入本地: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_1/img_face_1.jpg
写入本地: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_1/img_face_2.jpg
写入本地: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_1/img_face_3.jpg
写入本地: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_1/img_face_4.jpg
写入本地: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_1/img_face_5.jpg
写入本地: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_1/img_face_6.jpg


删除旧的文件夹: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_2
新建的人脸文件夹:  F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_2
写入本地: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_2/img_face_1.jpg
写入本地: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_2/img_face_2.jpg
写入本地: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_2/img_face_3.jpg
写入本地: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_2/img_face_4.jpg


删除旧的文件夹: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_3
新建的人脸文件夹:  F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_3
写入本地: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_3/img_face_1.jpg
写入本地: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_3/img_face_2.jpg
写入本地: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_3/img_face_3.jpg
写入本地: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_3/img_face_4.jpg
写入本地: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_3/img_face_5.jpg
写入本地: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_3/img_face_6.jpg
写入本地: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_3/img_face_7.jpg   
复制代码

 

2.2  get_features_into_CSV.py / 将图像文件中人脸数据提取出来存入 CSV

  这部分代码实现的功能是将之前捕获到的人脸图像文件,提取出 128D 特征,然后计算出某人人脸数据的特征均值存入 CSV 中,方便之后识别时候进行比对;

  利用 numpy.mean() 计算特征均值;

   get_features_into_CSV.py 源码:

复制代码
  1 # created at 2018-05-11
  2 
  3 # updated at 2018-09-06
  4 # 增加录入多张人脸到CSV的功能
  5 
  6 # By        coneypo
  7 # Blog:     http://www.cnblogs.com/AdaminXie
  8 # GitHub:   https://github.com/coneypo/Dlib_face_recognition_from_camera
  9 
 10 #   return_128d_features()          获取某张图像的128d特征
 11 #   write_into_csv()                将某个文件夹中的图像读取特征并写入csv
 12 #   compute_the_mean()              从csv中读取128d特征,并计算特征均值
 13 
 14 
 15 import cv2
 16 import os
 17 import dlib
 18 from skimage import io
 19 import csv
 20 import numpy as np
 21 import pandas as pd
 22 
 23 path_faces_rd = "data/faces_from_camera/"
 24 path_csv = "data/csvs_from_camera/"
 25 
 26 # detector to find the faces
 27 detector = dlib.get_frontal_face_detector()
 28 
 29 # shape predictor to find the face landmarks
 30 predictor = dlib.shape_predictor("shape_predictor_5_face_landmarks.dat")
 31 
 32 # face recognition model, the object maps human faces into 128D vectors
 33 facerec = dlib.face_recognition_model_v1("dlib_face_recognition_resnet_model_v1.dat")
 34 
 35 
 36 # 返回单张图像的128D特征
 37 def return_128d_features(path_img):
 38     img = io.imread(path_img)
 39     img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
 40     dets = detector(img_gray, 1)
 41 
 42     print("检测的人脸图像:", path_img, "\n")
 43 
 44     # 因为有可能截下来的人脸再去检测,检测不出来人脸了
 45     # 所以要确保是 检测到人脸的人脸图像 拿去算特征
 46     if len(dets) != 0:
 47         shape = predictor(img_gray, dets[0])
 48         face_descriptor = facerec.compute_face_descriptor(img_gray, shape)
 49     else:
 50         face_descriptor = 0
 51         print("no face")
 52 
 53     # print(face_descriptor)
 54     return face_descriptor
 55 
 56 
 57 # 将文件夹中照片特征提取出来,写入csv
 58 # 输入input:
 59 #   path_faces_personX:     图像文件夹的路径
 60 #   path_csv:               要生成的csv路径
 61 
 62 def write_into_csv(path_faces_personX, path_csv):
 63     dir_pics = os.listdir(path_faces_personX)
 64     with open(path_csv, "w", newline="") as csvfile:
 65         writer = csv.writer(csvfile)
 66         for i in range(len(dir_pics)):
 67             # 调用return_128d_features()得到128d特征
 68             print("正在读的人脸图像:", path_faces_personX + "/" + dir_pics[i])
 69             features_128d = return_128d_features(path_faces_personX + "/" + dir_pics[i])
 70             #  print(features_128d)
 71             # 遇到没有检测出人脸的图片跳过
 72             if features_128d == 0:
 73                 i += 1
 74             else:
 75                 writer.writerow(features_128d)
 76 
 77 
 78 # 读取 某人 所有的人脸图像的数据,写入 person_X.csv
 79 faces = os.listdir(path_faces_rd)
 80 for person in faces:
 81     print(path_csv + person + ".csv")
 82     write_into_csv(path_faces_rd + person, path_csv + person + ".csv")
 83 
 84 
 85 # 从csv中读取数据,计算128d特征的均值
 86 def compute_the_mean(path_csv_rd):
 87     column_names = []
 88 
 89     # 128列特征
 90     for i in range(128):
 91         column_names.append("features_" + str(i + 1))
 92 
 93     # 利用pandas读取csv
 94     rd = pd.read_csv(path_csv_rd, names=column_names)
 95 
 96     # 存放128维特征的均值
 97     feature_mean = []
 98 
 99     for i in range(128):
100         tmp_arr = rd["features_" + str(i + 1)]
101         tmp_arr = np.array(tmp_arr)
102 
103         # 计算某一个特征的均值
104         tmp_mean = np.mean(tmp_arr)
105         feature_mean.append(tmp_mean)
106     return feature_mean
107 
108 
109 # 存放所有特征均值的 CSV 的路径
110 path_csv_feature_all = "data/features_all.csv"
111 # 存放人脸特征的csv的路径
112 path_csv_rd = "data/csvs_from_camera/"
113 
114 with open(path_csv_feature_all, "w", newline="") as csvfile:
115     writer = csv.writer(csvfile)
116     csv_rd = os.listdir(path_csv_rd)
117     print("特征均值: ")
118     for i in range(len(csv_rd)):
119         feature_mean = compute_the_mean(path_csv_rd + csv_rd[i])
120         # print(feature_mean)
121         print(path_csv_rd + csv_rd[i])
122         writer.writerow(feature_mean)
复制代码

 

  我们可以看下对于某张图片,face_descriptor 的输出结果:

  绿色框内是我们的返回 128D 特征的函数;

  在红色框内调用该函数来计算 img_face_13.jpg;

  可以看到黄色框中的输出为 128D 的向量;

图 5 返回单张图像的 128D 特征的计算结果

 

  所以就把人脸图像进行批量化操作,提取出 128D 的特征,然后计算特征均值,存入 features_all.csv;

  features_all.csv 是一个 n 行 128 列的 CSV, n 是录入的人脸数,128 列是某人的 128D 特征;

  这存储的就是  录入的人脸数据,之后  摄像头捕获的人脸 将要拿过来和  这些特征值 进行比对,如果欧式距离比较近的话,就可以认为是同一张人脸

 

    get_features_into_CSV.py 的输出 log:

复制代码
F:/code/python/P_dlib_face_reco/data/csvs_from_camera/person_1.csv
正在读的人脸图像: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_1/img_face_1.jpg
检测的人脸图像: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_1/img_face_1.jpg 
   
...
正在读的人脸图像: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_5/img_face_3.jpg 检测的人脸图像: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_5/img_face_3.jpg 正在读的人脸图像: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_5/img_face_4.jpg 检测的人脸图像: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_5/img_face_4.jpg 正在读的人脸图像: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_5/img_face_5.jpg 检测的人脸图像: F:/code/python/P_dlib_face_reco/data/faces_from_camera/person_5/img_face_5.jpg 特征均值: F:/code/python/P_dlib_face_reco/data/csvs_from_camera/person_1.csv F:/code/python/P_dlib_face_reco/data/csvs_from_camera/person_2.csv F:/code/python/P_dlib_face_reco/data/csvs_from_camera/person_3.csv F:/code/python/P_dlib_face_reco/data/csvs_from_camera/person_4.csv F:/code/python/P_dlib_face_reco/data/csvs_from_camera/person_5.csv
复制代码

 

2.3 face_reco_from_camera.py / 实时人脸识别对比分析

  这部分源码实现的功能:调用摄像头,捕获摄像头中的人脸,然后如果检测到人脸,将  摄像头中的人脸提取出 128D 的特征,然后和  之前录入人脸的 128D 特征 进行计算欧式距离,如果比较小,可以判定为一个人,否则不是一个人;

  欧氏距离对比的阈值设定,是在  return_euclidean_distance 函数的  dist 变量;

  我这里程序里面指定的  欧氏距离判断阈值是 0.4,具体阈值可以根据实际情况或者测得结果进行修改;

  

  这边做了一个,让人名跟随显示在头像下方,如果想要在人脸矩形框下方显示人名,首先需要知道 Dlib 生成的矩形框的尺寸怎么读取;

  Dlib 返回的 dets 变量是一系列人脸的数据,此处对单张人脸处理,所以取 dets[0] 的参数;

  可以通过  dets[0].top()dets[0].bottom()dets[0].left() 和  dets[0].right() 来确定要显示的人名的坐标;

图 6 dets[0].top() 等参数说明 

  

  得到矩形框的坐标,就可以获取人名的相对位置;

  这是我这边取的坐标:

1 pos_text_1 = tuple([dets[0].left(), int(dets[0].bottom()+(dets[0].bottom()-dets[0].top())/4)])

 

  

 

图 7 face_reco_from_camera.py 生成的人脸识别窗口界面

 

  face_reco_from_camera.py 源码:

复制代码
  1 # created at 2018-05-11
  2 # updated at 2018-09-08
  3 # support multi-faces now
  4 
  5 # Author:   coneypo
  6 # Blog:     http://www.cnblogs.com/AdaminXie
  7 # GitHub:   https://github.com/coneypo/Dlib_face_recogqnition_from_camera
  8 
  9 import dlib         # 人脸识别的库dlib
 10 import numpy as np  # 数据处理的库numpy
 11 import cv2          # 图像处理的库OpenCv
 12 import pandas as pd # 数据处理的库Pandas
 13 
 14 # face recognition model, the object maps human faces into 128D vectors
 15 facerec = dlib.face_recognition_model_v1("dlib_face_recognition_resnet_model_v1.dat")
 16 
 17 
 18 # 计算两个向量间的欧式距离
 19 def return_euclidean_distance(feature_1, feature_2):
 20     feature_1 = np.array(feature_1)
 21     feature_2 = np.array(feature_2)
 22     dist = np.sqrt(np.sum(np.square(feature_1 - feature_2)))
 23     print("e_distance: ", dist)
 24 
 25     if dist > 0.4:
 26         return "diff"
 27     else:
 28         return "same"
 29 
 30 
 31 # 处理存放所有人脸特征的 CSV
 32 path_features_known_csv = "data/features_all.csv"
 33 csv_rd = pd.read_csv(path_features_known_csv, header=None)
 34 
 35 # 存储的特征人脸个数
 36 # print(csv_rd.shape[0])
 37 
 38 # 用来存放所有录入人脸特征的数组
 39 features_known_arr = []
 40 
 41 # known faces
 42 for i in range(csv_rd.shape[0]):
 43     features_someone_arr = []
 44     for j in range(0, len(csv_rd.ix[i, :])):
 45         features_someone_arr.append(csv_rd.ix[i, :][j])
 46     #    print(features_someone_arr)
 47     features_known_arr.append(features_someone_arr)
 48 print("Faces in Database:", len(features_known_arr))
 49 
 50 # Dlib 预测器
 51 detector = dlib.get_frontal_face_detector()
 52 predictor = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat')
 53 
 54 # 创建 cv2 摄像头对象
 55 cap = cv2.VideoCapture(0)
 56 
 57 # cap.set(propId, value)
 58 # 设置视频参数,propId 设置的视频参数,value 设置的参数值
 59 cap.set(3, 480)
 60 
 61 
 62 # 返回一张图像多张人脸的 128D 特征
 63 def get_128d_features(img_gray):
 64     dets = detector(img_gray, 1)
 65     if len(dets) != 0:
 66         face_des = []
 67         for i in range(len(dets)):
 68             shape = predictor(img_gray, dets[i])
 69             face_des.append(facerec.compute_face_descriptor(img_gray, shape))
 70     else:
 71         face_des = []
 72     return face_des
 73 
 74 
 75 # cap.isOpened() 返回true/false 检查初始化是否成功
 76 while cap.isOpened():
 77 
 78     flag, img_rd = cap.read()
 79     kk = cv2.waitKey(1)
 80 
 81     # 取灰度
 82     img_gray = cv2.cvtColor(img_rd, cv2.COLOR_RGB2GRAY)
 83 
 84     # 人脸数 dets
 85     faces = detector(img_gray, 0)
 86 
 87     # 待会要写的字体
 88     font = cv2.FONT_HERSHEY_COMPLEX
 89 
 90     cv2.putText(img_rd, "Press 'q': Quit", (20, 400), font, 0.8, (84, 255, 159), 1, cv2.LINE_AA)
 91 
 92     # 存储人脸名字和位置的两个 list
 93     # list 1 (faces): store the name of faces               Jack    unknown unknown Mary
 94     # list 2 (pos_namelist): store the positions of faces   12,1    1,21    1,13    31,1
 95 
 96     # 存储所有人脸的名字
 97     pos_namelist = []
 98     name_namelist = []
 99 
100     # 检测到人脸
101     if len(faces) != 0:
102         # 获取当前捕获到的图像的所有人脸的特征,存储到 features_cap_arr
103         features_cap_arr = []
104         for i in range(len(faces)):
105             shape = predictor(img_rd, faces[i])
106             features_cap_arr.append(facerec.compute_face_descriptor(img_rd, shape))
107 
108         # 遍历捕获到的图像中所有的人脸
109         for k in range(len(faces)):
110             # 让人名跟随在矩形框的下方
111             # 确定人名的位置坐标
112             # 先默认所有人不认识,是 unknown
113             name_namelist.append("unknown")
114 
115             # 每个捕获人脸的名字坐标
116             pos_namelist.append(tuple([faces[k].left(), int(faces[k].bottom() + (faces[k].bottom() - faces[k].top()) / 4)]))
117 
118             # 对于某张人脸,遍历所有存储的人脸特征
119             for i in range(len(features_known_arr)):
120                 print("with person_", str(i+1), "the ", end='')
121                 # 将某张人脸与存储的所有人脸数据进行比对
122                 compare = return_euclidean_distance(features_cap_arr[k], features_known_arr[i])
123                 if compare == "same":  # 找到了相似脸
124                     name_namelist[k] = "person_" + str(i+1)
125 
126             # 矩形框
127             for kk, d in enumerate(faces):
128                 # 绘制矩形框
129                 cv2.rectangle(img_rd, tuple([d.left(), d.top()]), tuple([d.right(), d.bottom()]), (0, 255, 255), 2)
130 
131         # 写人脸名字
132         for i in range(len(faces)):
133             cv2.putText(img_rd, name_namelist[i], pos_namelist[i], font, 0.8, (0, 255, 255), 1, cv2.LINE_AA)
134 
135     print("Name list now:", name_namelist, "\n")
136 
137     cv2.putText(img_rd, "Face Register", (20, 40), font, 1, (255, 255, 255), 1, cv2.LINE_AA)
138     cv2.putText(img_rd, "Faces: " + str(len(faces)), (20, 100), font, 1, (0, 0, 255), 1, cv2.LINE_AA)
139 
140     # 按下 q 键退出
141     if kk == ord('q'):
142         break
143 
144     # 窗口显示
145     cv2.imshow("camera", img_rd)
146 
147 # 释放摄像头
148 cap.release()
149 
150 # 删除建立的窗口
151 cv2.destroyAllWindows()
复制代码

 

  face_reco_from_camera.py 输出 log:

复制代码
Faces in Database: 5
Name list now: [] 

Name list now: [] 

Name list now: [] 

Name list now: [] 

Name list now: [] 

with person_ 1 the e_distance:  0.40770022710364756
with person_ 2 the e_distance:  0.41082186674421134
with person_ 3 the e_distance:  0.3961545573801463
with person_ 4 the e_distance:  0.3881850644563972
with person_ 5 the e_distance:  0.3495735780870818
Name list now: ['person_4'] 

with person_ 1 the e_distance:  0.4314467101915446
with person_ 2 the e_distance:  0.4299990464683071
with person_ 3 the e_distance:  0.4182695008637471
with person_ 4 the e_distance:  0.4173694262729763
with person_ 5 the e_distance:  0.38357217732017734
Name list now: ['person_4'] 

with person_ 1 the e_distance:  0.4262991040992263
with person_ 2 the e_distance:  0.43254966504500664
with person_ 3 the e_distance:  0.41576433114841965
with person_ 4 the e_distance:  0.4122140311433292
with person_ 5 the e_distance:  0.38073570942005236
Name list now: ['person_4'] 

with person_ 1 the e_distance:  0.42088261541728456
with person_ 2 the e_distance:  0.42064499551908163
with person_ 3 the e_distance:  0.404443147870785
with person_ 4 the e_distance:  0.4043774203639022
with person_ 5 the e_distance:  0.37271089160417986
Name list now: ['person_4'] 
复制代码

 

  实时输出结果:

图 9 实时输出的欧氏距离结果

 

  通过实时的输出结果,看的比较明显;

  输出绿色部分:当是我自己(即之前分析提取特征的 default_person)时,计算出来的欧式距离基本都在  0.2 左右

  输出红色部分:而换一张图片上去比如特朗普,明显看到欧式距离计算结果  达到了 0.8,此时就可以判定,后来这张人脸不是我们预设的人脸;

  所以之前提到的欧式距离计算对比的阈值可以由此设定,本项目中取的是  dist=0.4

 

3.总结

  核心就是  提取人脸特征,然后计算欧式距离和预设的特征脸进行比对;

  不过这个实时获取摄像头人脸进行比对,要实时的进行计算摄像头脸的特征值,然后还要计算欧氏距离,所以计算量比较大,可能摄像头视频流会出现卡顿;

   

  update on 09.06:

   满足多张人脸识别需要

 

# 请尊重他人劳动成果,转载或者使用源码请注明出处:http://www.cnblogs.com/AdaminXie

# 代码已上传到了我的 GitHub,如果对您有帮助欢迎 Star 下: https://github.com/coneypo/Dlib_face_recognition_from_camera

基于电影知识图谱的智能问答系统(八) -- 终极完结篇 - Appleyk的专栏 - CSDN博客

$
0
0


基于电影知识图谱的智能问答系统系列章节传送门:

 

基于电影知识图谱的智能问答系统(一) -- Mysql数据准备

基于电影知识图谱的智能问答系统(二) -- Neo4j导入CSV文件

基于电影知识图谱的智能问答系统(三) -- Spark环境搭建

 

基于电影知识图谱的智能问答系统(四) -- HanLP分词器

基于电影知识图谱的智能问答系统(五) -- Spark朴素贝叶斯分类器

基于电影知识图谱的智能问答系统(六) -- 问题训练样本集敲定

基于电影知识图谱的智能问答系统(七) -- Neo4j语句那点事

 

 

博主注:本篇不再过多的讲解demo了,集成也很简单,前面几章也已经给本篇做足了铺垫,项目demo中的注释也是非常的详细,最后会附上整个项目的下载地址,如有问题,另留言吧。

 

 

一、效果预览

 

 

(1)电影简介

 

前端展示:

 

 

后台效果:

 

 

 

 

 

(2)电影评分

 

前端展示:

 

 

 

 

后台效果:

 

 

 

 

 

 

(3)电影演员列表

 

前端展示:

 

 

 

 

后台效果:

 

 

 

 

 

 

(4)演员A和演员B合作过哪些电影

 

前端展示:

 

 

 

 

 

 

后端效果:

 

 

 

由于章子怡本来是一个完整的人名,但是HanLP分词的时候,却意外的“失手”了,因此导致最后查询无果

 

我们再换个问题试验一把

 

 

 

后台效果:

 

 

 

 

 

(5)某演员出演过那种类型的电影或演过某种类型的电影有哪些

 

 

 

 

......etc,其余不在做演示,下面直接来看如何利用Spring-Boot搭建我们的智能问答系统

 

 

 

二、项目目录结构图

 

 

 

 

 

 

三、Movie节点类

 

 

这里只拿电影信息的节点类来进行演示,比如,movie对应的节点在Java中定义类如下:

 

 

package com.appleyk.node;

import java.util.List;

import org.neo4j.ogm.annotation.NodeEntity;
import org.neo4j.ogm.annotation.Relationship;

import com.fasterxml.jackson.annotation.JsonProperty;

@NodeEntity
public class Movie extends BaseEntity{

	private Long mid;
	private Double rating;
	private String releasedate;
	private String title;
	private String introduction;

	@Relationship(type = "is")
	@JsonProperty("电影类型")
	private List<Genre> genres;

	public Movie() {

	}

	public Long getMid() {
		return mid;
	}

	public void setMid(Long mid) {
		this.mid = mid;
	}

	public Double getRating() {
		return rating;
	}

	public void setRating(Double rating) {
		this.rating = rating;
	}

	public String getReleasedate() {
		return releasedate;
	}

	public void setReleasedate(String releasedate) {
		this.releasedate = releasedate;
	}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getIntroduction() {
		return introduction;
	}

	public void setIntroduction(String introduction) {
		this.introduction = introduction;
	}

	public List<Genre> getGenres() {
		return genres;
	}

	public void setGenres(List<Genre> genres) {
		this.genres = genres;
	}

}

 

其中属性和neo4j中的movie节点的属性一一对应

 

 

 

 

 

电影信息节点里面带有关系is,对应neo4j中该电影的类型

 

 

 

通过Controller对外提供的查询接口如下:

 

 

 

外部调用效果如下:

 

 

 

 

 

四、加载自定义带词性的字典数据

 

 

 

注:不要使用HanLP提供的自定义词典路径,因为这个除了不能随心所欲的定义分词的词性以外,还极容易出现分词紊乱,词性对不上的bug,为了满足我们对专有电影名、电影分数及电影类型词性的定义,我们使用额外加载的方式设置HanLP的自定义分词,application.properties中设置自定义词典的路径如下:

 

 

 

 

 

 

 

 

 

 

这三个文件的下载地址: https://pan.baidu.com/s/13PYsF2X3v7BzkPFG37kKuw

 

注:也可以根据自己的需求进行设置

 

 

五、项目完整地址

 

 

GitHub下载地址: Spring-Boot集成Neo4j并利用Spark的朴素贝叶斯分类器实现基于电影知识图谱的智能问答系统

 

 

 

完结!!!

 

 

 

番外篇:如何将项目导入到IDEA并运行测试效果?

 

(1)IDEA项目结构图(导入pom文件,配置好Maven后如下)

 

 

 

 

(2)项目配置文件

 

 

 

 

(3)内嵌html测试前端访问页面

 

 

 

 

 

(4)启动项目

 

 

 

 

 

 

 

(5)浏览器访问主页Index.html

 

facebook-faiss库 - YiLiang - CSDN博客

$
0
0

三月初,Facebook AI Research(FAIR)开源了一个名为 Faiss 的库,Faiss 主要用于有效的相似性搜索(Similarity Search)和稠密矢量聚类(Clustering of dense vectors),包含了在任何大小的矢量集合里进行搜索的算法。Faiss 上矢量集合的大小甚至可以大到装不进 RAM。这个库基本上是用 C++ 实现的,带有可选的通过 CUDA 提供的 GPU 支持,以及一个可选的 Python 接口。

通过 Faiss 进行相似性搜索时,10 亿图像数据库上的一次查询仅耗时 17.7 微秒,速度较之前提升了 8.5 倍,且准确度也有所提升。

耗时 17.7 微秒、提速 8.5 倍,Facebook AI 相似性搜索库 Faiss 的核心奥义究竟在哪?

除图片检索外,相似性搜索还有更广阔的运用场景。例如,通过搜索数据库来判断某一罪行是否属于较严重的犯罪形式,或有重罪趋势;通过搜索和成功店铺所在地相似的人口特征和环境特征,来寻找零售商新店的最佳位置;通过相似城市的搜索,来衡量所在城市薪资水平是否合理等。

日前,Facebook 发布了一份关于 Faiss 原理的介绍,36氪对此进行了编译和整理,具体内容如下:

关于相似性搜索

传统的数据库由包含符号信息的结构化表格组成。举例来说,一个图片集合的呈现方式是一个列表,这个列表的每一行都有一张索引照片,同时包含图像标识、描述语句等信息。每一行的信息也可以连接其他表格,如一张包含人物的照片可以连接到姓名表上。

大部分 AI 工具都会生成高维矢量,如以 word2vec 为代表的文字嵌入工具,和用于深度学习训练的 CNN 描述符(Convolutional Neural Net)等。在这篇文章中,我们将阐述为什么高维矢量数据比固定符号数据更强大且灵活。不过,使用 SQL 查询的传统数据库并不适用这些新型表述方式:首先,海量的多媒体信息流创造了数十亿矢量;其次,更重要的一点是,找到类似的条目意味着要找到类似的高维矢量,这对标准的查询语言来说是极其低效甚至是不可能的。

如何应用矢量表述?

让我们假设你现在有一张建筑的照片,这个建筑是某个中型城市的市政府,但你已经记不清名字了,然后你想在整个图片集合中找到这个建筑的其他所有照片。这种情况下使用传统的 SQL 语句来完成关键字查询是不可能的,因为你已经忘记了这个城市的名字。

相似性搜索此刻却能派上用场,图片的矢量描述是为了针对相似图片制造出相似的矢量,这些矢量被定义为临近欧几里得空间的向量。

矢量描述的另一个应用是分类。假设你需要一个分类器来判定一个图像集合中哪些图片代表的是小雏菊。分类器的训练是一个比较知名的过程:该算法会输入雏菊图像和非雏菊图像(如汽车、绵羊、玫瑰、向日葵等)。如果分类器是线性的,则会输出一个分类矢量,

所以,针对相似性搜索和分类,我们需要进行如下操作:

  • 给定一个查询矢量,回到欧几里德空间中最接近这个矢量的数据库对象列表。

  • 给定一个查询矢量,回到有最高向量点积德数据库对象列表。

有一个挑战是,我们希望这些操作可以以数十亿矢量的规模来运行。

软件包

目前投入应用的软件工具还不足以支持上述数据研究的进行。传统的 SQL 数据库系统不切实际,因为它们是针对 hash-based searches 或 1D interval searches 而优化的。相似性搜索功能在 OpenCV 这类工具包中受到的扩展性限制较大,一些针对“小”数据集(比如仅 100 万个矢量)的相似性搜索算法库也是如此。

耗时 17.7 微秒、提速 8.5 倍,Facebook AI 相似性搜索库 Faiss 的核心奥义究竟在哪?

Faiss 是一个打破了以上提到的所有限制的算法库,其优点有:

  • Faiss 提供了多种相似性搜索方法,可以针对不同的使用方法,进行跨度较大的功能取舍。

  • Faiss 针对内存使用和速度进行了优化。

  • Faiss 为最相关的索引方法提供了先进的 GPU 实现方案。

评估相似性搜索

一旦矢量被学习机器提取(从图像、视频、文档等),就可以被输入到相似性搜索库中。

我们拥有一个作为参考的暴力算法,能精确而详尽地计算出所有相似性,并且返回到最相似的元素列表中。这提供了一个“黄金标准”参考结果列表。但值得注意的是,高效实施暴力算法并不容易,并且暴力算法经常会影响到系统其他组件的效果。

如果我们愿意牺牲一部分精确度,相似性搜索的速度可以提高几个数量级,但是会偏离参考结果。例如,将图像相似性搜索的第一和第二个结果交换,可能没有什么太大影响,因为它们都是给定查询的正确结果。加速搜索涉及一些数据集合的预处理,我们将这项操作称之为“索引”。

这使我们确定了三个感兴趣的研究指标:

  • 速度

在整个数据库中寻找 10 个(或其他数字)最相近的矢量需要花费多长时间?最好的情况是比暴力算法耗时短,否则索引就没有任何意义了。

  • 内存用量

这个方法需要多少 RAM?比传统矢量多还是少?Faiss 仅支持在 RAM 搜索,因为磁盘数据库的数量级要慢一些,即使是 SSD。

  • 准确度

返回的结果列表和暴力搜索结果的匹配度如何?准确度可以通过对结果列表中优先返回最邻近单位的检索数量进行评估,或者是通过衡量 10 个最先返回的最邻近单位的平均分数来评估(这种方法被称为“10-intersection”)。

我们通常会评估固定内存使用速度和精度之间的关联。Faiss 采用的是压缩原始矢量的方法,因为这是扩展到十亿级矢量数据库的唯一方法:以每个矢量占用 32 字节计,当规模达到 10 亿矢量后,这些矢量会占据大量的存储空间。

大部分索引库包含约 100 万个矢量,我们认为这个规模很小。比如说, nmslib 拥有非常有效的算法,速度比 Faiss 快,但同时需要更多的存储空间。

评估十亿个矢量

工程界中对于这种规模的数据集并没有一个完善的标准,我们比较了一些研究结果,并进行评估。

Deep1B 是一个有 10 亿张照片的图像库,我们在这上面评估精度。每张照片都被卷积神经网络(CNN)处理过,且 CNN 中的一个激活图(activation map)会被当作图像描述符(descriptor)。这些矢量可以和欧氏距离进行比较,以量化图像之间的相似度。

Deep1B 有一个小的图像检索库,且处理了这些图像的暴力算法提供了一个真实的相似性搜索结果。因此,如果我们运行搜索算法,我们可以评估结果中的 1-recall@1。

选择索引

为了评估,我们把内存空间大小限定为 30GB RAM。这个内存空间约束我们选择索引方法和参数。在 Faiss 中,索引方法具体表现为一个字符串,如:OPQ20_80,IMI2x14,PQ20。

这个字符串指示了用于矢量预处理的具体步骤(OPQ20_80),一个指示数据库应该如何被分割的选择机制(IMI2x14),以及一个指示产品量化编码矢量的编码组件(PQ20),这个编码组件会产生成 20 字节的代码。因此,内存使用(包括间接使用)低于 30 GB RAM。

我们知道这听起来有点“太技术”,因此 Faiss 的开发文件会提供相应的指导,即如何根据你的需求来提供最合适的索引类型。

一旦确定了索引类型,检索就开始了。这个算法会处理 10 亿个矢量,并将这些矢量置于所一个索引中。索引可以存储在磁盘上,或者立即使用,同时索引中的搜索、添加/删除可以交叉输入。

在索引中检索

索引就绪后,可以设置一组搜索事件参数来调整检索方法。为了评估,我们使用单线程进行搜索。由于内存使用量已经被固定,我们需要优化精确度和搜索时间之间的权衡。这也意味着,我们要能在尽可能少的时间内,使 1-recall@1 达到 40%。

幸好,Faiss 有一个自动调节机制,可以扫描参数空间,并收集提供最佳操作点的空间,也就是在给定精度的情况下最好的潜在搜索时间,反之亦然。在 Deep1B 中,操作点可以进行可视化,如下图所示:

耗时 17.7 微秒、提速 8.5 倍,Facebook AI 相似性搜索库 Faiss 的核心奥义究竟在哪?

在这个量化表中,我们可以看到,使 1-recall@1 达到 40% 的查询时间少于 2 微秒/矢量,或者将时间限定为 0.5 微秒,我们可以达到 30%。2 微秒的检索时间意味着在一个单核上每秒查询 500 次(500 QPS)。

这个结果可以和这个领域中最先进的研究结果进行比较。Babenko 和 Lempitsky 于 2016 年撰写了一篇名为 “ Efficient Indexing of Billion-Scale Datasets of Deep Descriptors” 的论文,论文中提到,使用 Deep1B 时,他们需要花费 20 微秒来使 1-recall@1 达到 45%。

用 GPU(图形处理器)处理十亿级数据集

许多的研究都在致力于  GPU 的实施,在原生多 GPU 的支持下,产生了令人惊讶的单机性能。GPU 的实施可以看作是对应 CPU 的替代,使用 GPU 时你甚至不需要知道 CUDA API。Faiss 支持所有 2012 年后发布的英伟达 GPU(开普勒,计算能力 3.5+)。

我们希望将  roofline model 作为指导,它指出,开发者应尽可能让内存带宽或浮点单位饱和。Faiss GPU 在单个 GPU 上的速度比相应的 Faiss CPU 快 5-10倍。如果是使用新的帕斯卡级硬件,如英伟达 P100,那么速度会快 20 倍以上。

以下是一些性能方面的数字:

  • 通过相似索引,可以在 35 分钟内(包括索引构建时间),在四路 Maxwell Titan X  GPU 上构建一个简单的 k-nearest-neighbor 图(k=10),基于 YFCC100M 数据集合 9500 万图像的 128D CNN 描述符,以及 0.8 的10-intersection。

  • 十亿矢量的 k-nearest-neighbor 图也已实现。开发者可以在 Deep1B 数据集上创建强力的 k-nearest-neighbor 图(k=10),0.65 的 10-intersection 在四路 Maxwell Titan X  GPU 下需要 12 个小时。而 0.8 的 10-intersection 在八路帕斯卡 P100-PCle GPU 上也需要 12 个小时。画质较低的图可以在五小时内通过 Titan X 生成。

  • 其他方面的性能也非常惊人。例如,构建上述 Deep1B 索引需要用 k-means 聚类生成 262,144 个几何中心和 6710 万 120-dim 矢量。在 25 E-M 次迭代下,四路 Titan X GPU(12.6 tflp/s)需要花 139 分钟处理,八路帕斯卡 P100 GPU(40 tflop/s)则需花 43.8 分钟。要注意的是聚类的训练集并不需要和 GPU 显存匹配,因为数据会按需及时导入到 GPU 中,不会影响性能。

其他技术

基于诸多研究成果和大量工程技术,Facebook AI  Research 团队自 2015 年开始研发 Faiss。针对 Faiss, Facebook 选择优化几项基础技术,尤其是在 CPU 方面,Facebook 大量运用了如下技术:

  • 多线程充分利用多核性能,并在多路 GPU 上进行并行搜索;

  • 运用 matrix/matrix 乘法在 BLAS 算法库中进行高效、精确的距离计算。如果没有 BLAS,高效的暴力算法很难呈现最优效果。BLAS/LAPACK 是 Faiss 必备的前提软件;

  • 机器 SIMD 矢量化和 popcount 被用于加速孤立向量的距离计算。

GPU 方面

由于典型的 CPU 算法(如 heap selection)并不适用于 GPU,此前应用在相似性搜索上的 GPU 和 k-selection(寻找 k-minimum 或 k-maximum 因素)一直存在性能方面的问题。对 Faiss GPU 来说,我们设计了文献记载中已知的最快的小 k-selection 算法(k<=1024)。所有的中间状态都被保存在寄存器中,这样有助于提升其速度。它能将输入的数据以 single pass 的方式进行 k-select,运行潜在峰值性能的 55%,这取决于峰值 GPU 的显存带宽。由于其状态仅保留在寄存器文件中,并且能和其他内核一起使用,从而能进行快速准确的相似搜索算法。

研究领域中,许多人把注意力放在高效的平铺策略和面向相似性搜索的内核执行上。Multi-GPU 支持由分片或复制数据来提供,开发者不会受到单 GPU 显存大小的限制。半精度浮点支持(float 16)也有提供,使得开发者可以在支持的 GPU 上进行完整的 float 16 运算。float 16 这样的编码矢量能在几乎不损失精度的情况下提高速度。

总之,连续不断的超量因素在实施中非常重要,Faiss 做了许多关注工程细节的痛苦的工作。

Faiss上手

Faiss 以 C++ 实现,支持 Python。想要上手,需要从 GitHub 上获取 Faiss,编译后将 Faiss 模块导入到 Python 中。Faiss 与 numpy 完全集成,所有的函数都是用 numpy 数组来实现的(in float32)

tensorflow提取VGG特征 - weixin_38208741的博客 - CSDN博客

$
0
0

vgg-16一种深度卷积神经网络模型,16表示其深度。模型可以达到92.7%的测试准确度。它的数据集包括1400万张图像,1000个类别。 

一个简单的演示,提取VGG的pool5层特征,存储为.mat文件
  1.     import scipy.io as sio  
  2.    from scipy.misc import imread, imresize  
  3.   
  4.    sess = tf.Session()  
  5.     imgs = tf.placeholder(tf.float32, [None, 224, 224, 3])  
  6.     vgg = vgg16(imgs, '/aa/data/vgg16_weights.npz', sess)  
  7.   
  8.     img1 = imread('/aa/data/laska.png', mode='RGB')  
  9.     img1 = imresize(img1, (224, 224))  
  10.     path = '/aa/data/AllSample/'  
  11.   
  12.     for i in range(1,211):  
  13.         img = imread(path+str(i)+'.jpg',mode='RGB')  
  14.         print(path+str(i)+'.jpg')  
  15.         img = imresize(img, (224, 224))  
  16.       
  17.         feature = sess.run(vgg.pool5, feed_dict={vgg.imgs: [img]})  
  18.         feature = np.reshape(feature,[7,7,512])  
  19.         dic = {'features':feature}  
  20.         sio.savemat('/aa/data/features/'+str(i)+'.mat',dic)  
  21.       
  22. #     features = feature.eval(session=sess)  
  23. #     features = np.reshape(features,[7,7,512])  
  24.      


在我们的实际项目中,一般不会直接从第一层直接开始训练,而是通过在大的数据集上(如ImageNet)训练好的模型,把前面那些层的参数固定,在运用到我们新的问题上,修改最后一到两层,用自己的数据去微调(finetuning),一般效果也很好。

所谓finetuning,就是说我们针对某相似任务已经训练好的模型,比如CaffeNet, VGG-16, ResNet等, 再通过自己的数据集进行权重更新, 如果数据量比较小,可以只更新最后一层,其他层的权重不变,如果数据量中等,可以训练后面几层,如果数据量很大,那OK,直接从头训练,只不过在训练时间上,需要花费比较多。

在网络训练好之后,只需要forward过程就能做预测,当然,我们也可以直接把这个网络当成一个feature extractor来用,可以直接用任何一层的输出作为特征,根据R-CNN论文对Alexnet的实验结果,如果不做fine-tuning,pool5和fc6和fc7的特征效果并没有很强的提升,所以,如果直接用作feature extractor,直接用pool的最后一层输出就OK。


vgg-16一种深度卷积神经网络模型,16表示其深度。模型可以达到92.7%的测试准确度。它的数据集包括1400万张图像,1000个类别。

TensorFlow VGG-16 预训练模型

https://github.com/ry/tensorflow-vgg16

vgg-16是我最喜欢运行的图像分类的模型,因为它的简单性和准确性。这种模型的创造者发表了可用于Caffe的预先训练二进制。

https://gist.github.com/ksimonyan/211839e770f7b538e2d8#file-readme-md

MD5 (VGG_ILSVRC_16_layers.caffemodel) = 441315b0ff6932dbfde97731be7ca852

这是将特定的文件转换到一个tensorflow模型并检查其正确性。

运行make下载原始的caffe模型并转换。tf_forword.py里有一个怎样使用产生的vgg16.tfmodel的例子。如果你不想按照caffe,你可以从这下载输出 https://github.com/ry/tensorflow-vgg16/raw/master/vgg16-20160129.tfmodel.torrent

TF模型的输入(“image”)应该是[批次,高度,宽度,通道],其中高度=宽度=224,通道=3.值应该在0到1之间。

输出(“prob”)是一个1000维的类概率向量,这个向量的索引对应syset.txt里的行号。

https://github.com/leihe001/tensorflow-vgg

这是个基于 tensorflow-vgg16Caffe to TensorFlow的VGG16和VGG19的一个TensorFlow的实现。

我们修改了tensorflow-vgg16的实现使用numpy加载取代默认的tensorflow加载,目的是加速初始化和减少总的内存使用量。此实现允许进一步修改网络,例如移除FC层,或者增加批大小。

为了使用VGG网络,需要下载 VGG16 NPYVGG19的npy文件。

## 使用 使用这个编译VGG工程

vgg = vgg19.Vgg19()
vgg.build(images)

或者

vgg = vgg16.Vgg16()
vgg.build(images)

图像是一个维度是[None,224,224,3]的张量。

张量可以是一个占位,一个变量甚至是一个常量。

所有的VGG层(张量)可以通过vgg对象访问。例如,vgg.conv1_1, vgg.conv1_2, vgg.pool5, vgg.prob, ...test_vgg16.py and test_vgg19.py包含样例的使用。

##额外 在我的另一个tensorflow图像风格合成项目中使用了这个库: stylenet

##更新 添加一个可训练的VGG19版本vgg19_trainable.支持从现有的变量或从开始训练。(但是不包括训练器)

test_vgg19_trainabel里添加了一个简单的测试。switch里有如何训练,关闭训练模型进行验证,以及如何保存的例子。

我添加了一个单独的文件(不是修改现有的),因为我想保持原始VGG网络的简单性。

基于深度学习的智能问答-博客-云栖社区-阿里云

$
0
0

作者:周小强 陈清财 曾华军


1引言

纵观自动问答系统的技术发展历史,从1950年代因图灵测试而诞生至今,已经有几十年的历史。但真正在产业界得到大家的广泛关注,则得益于2011年Siri和Watson成功所带来的示范效应。自此,自动问答系统较以往任何时候都显得离实际应用更近。这一方面归功于机器学习与自然语言处理技术的长足进步,另一方面得益于维基百科等大规模知识库以及海量网络信息的出现。然而,现有的自动问答系统所面临的问题远没有完全解决。事实上,无论是业界应用还是学术研究,问句的真实意图分析、问句与答案之间的匹配关系判别仍然是制约自动问答系统性能的两个关键难题。


2 问答系统概述

问答系统能够更为准确地理解以自然语言形式描述的用户提问,并通过检索异构语料库或问答知识库返回简洁、精确的匹配答案。相对于搜索引擎,问答系统能更好地理解用户提问的真实意图, 同时更有效地满足用户的信息需求。


2.1问答系统的发展历程

问答系统最早的实现构想可以追溯到图灵测试。为了测试机器是否具备人类智能,图灵测试要求电脑能在5分钟内回答由人类测试者提出的一系列问题,且其达到超过30%的回答让测试者误认为是人类所答。随着人工智能、自然语言处理等相关技术的发展,针对不同的数据形态的变化也衍生出不同种类的问答系统。早期由于智能技术和领域数据规模的局限性,问答系统主要是面向限定领域的AI系统或专家系统,例如STUDENT[1]、LUNAR[2]系统。该时期的问答系统处理的数据类型主要是结构化数据,系统一般是将输入问题转化为数据库查询语句,然后进行数据库检索反馈答案。随着互联网的飞速发展以及自然语言处理技术的兴起,问答系统进入了面向开放领域、基于自由文本数据的发展时期,例如英文问答式检索系统Ask Jeeves (http://www.ask.com)、START (http://start.csail.mit.edu)。这种问答系统的处理流程主要包括:问题分析、文档及段落检索、候选答案抽取、答案验证。特别自1999年文本检索会议(Text Retrieval Conference,简称TREC)引入问答系统评测专项(Question Answering Track,简称QA Track)以来,极大推动了基于自然语言处理技术在问答领域中的研究发展。随后网络上出现的社区问答(community question answering, CQA)提供了大规模的用户交互衍生的问题答案对(question-answer pair, QA pair)数据,这为基于问答对的问答系统提供了稳定可靠的问答数据来源。随着苹果公司Siri系统的问世,问答系统进入了智能交互式问答的发展阶段,这种形式的问答系统能够让用户体验更为自然的人机交互过程,并且也使信息服务的相关应用更为方便可行。


问答系统处理的数据对象主要包括用户问题和答案。依据用户问题的所属数据领域,问答系统可分为面向限定域的问答系统、面向开放域的问答系统、以及面向常用问题集(frequent asked questions, FAQ)的问答系统。依据答案的不同数据来源,问答系统可划分为基于结构化数据的问答系统、基于自由文本的问答系统、以及基于问答对的问答系统。此外,按照答案的生成反馈机制划分,问答系统可以分为基于检索式的问答系统和基于生成式的问答系统。本文主要阐述基于检索式的问答系统的处理框架和相关研究。


2.2 问答系统的处理框架

不同类型的问答系统对于数据处理的方法存在不同。例如,相对于面向FAQ的问答系统的问句检索直接得到候选答案,面向开放领域的问答系统首先需要根据问题分析的结果进行相关文档、文本片段信息的检索,然后进行候选答案的抽取。虽然不同类型的问答系统对于系统模块的功能分工和具体实现存在差异,但依据数据流在问答系统中的处理流程,一般问答系统的处理框架中都包括问句理解、信息检索、答案生成三个功能组成部分,如图2.1所示。


21769f02eefc709bf648dc9b0f3deee8d648dc92 


2.2.1 问句理解

问句理解是问答系统理解用户意图的关键一环,问句理解模块的性能直接制约着后续处理模块的效果。用户意图是一个抽象的概念,要想作为答案检索的依据,需要把它转换成机器可以理解的形式。用户的检索意图导致信息需求的产生,因此,研究中往往将信息需求作为用户意图的代表,根据问句的语义结构,可以从问题类别和问题内容两方面来表示。通常采用自然语言技术对问题进行深层次的理解,包括命名实体识别、依存句法分析、词义消歧等。

问句理解主要包括问句分类、主题焦点提取、问题扩展处理。问句分类是将用户提问归入不同的类别,使系统能够针对不同问题类型采用不同的答案反馈机制得到候选答案集合。问答系统通常使用机器学习算法训练问题分类器[3,4]来实现用户提问的分类。主题焦点提取主要完成用户问题的信息需求的精确定位,其中主题表示问句的主要背景或者用户的感兴趣的对象,焦点则是用户询问的有关主题的内容,通常是问句话题的相关信息或对话题起到描述性的作用,比如属性、动作、实例等等。问题扩展是将用户在提问中没有充分表达的意思补充出来,对问题中潜在的信息显化出来,从而提高答案检索的召回率。


2.2.2 信息检索

根据问句理解得到的查询表示,信息检索模块负责从异构语料库、问答知识库中检索相关信息,传递给后续的答案生成处理模块。对于基于不同的问答系统,系统的检索模型以及检索数据形式也不同。对于基于自由文本数据的问答系统,信息检索过程是一个逐渐缩小答案范围的过滤过程,主要包括文档检索和段落句群检索。对于基于问句答案对的问答系统,信息检索处理是通过问句检索得到与用户提问相似的候选问句,返回对应的候选答案列表。


首先,文档检索是根据问题理解的结果检索用户提问的相关文档集合。最简单的方法是直接用已有的检索系统(如Smart,Lucene等)对问题的非停用词进行全文索引,直接检索得到用户提问的相关文档集合,但是这种方法很难获得好的效果。通常问答系统中的文档检索模型包括布尔模型、向量空间模型、语言模型、概率模型等。布尔模型是最简单的一种检索模型,它把关键词组织成一个布尔表达式,使得文档中出现的关键词需要满足这个布尔表达式。向量空间模型把文档和查询都表示成向量,根据查询和文档对应向量的相似度(通常是两个向量夹角的余弦值)对文档进行排序。概率模型估计计算文档和查询相关的概率,并按照相关性概率对文档进行排序。语言模型是把查询和文档分别表示成语言模型,通过两个语言模型之间的KL距离来估计两者的相似度。其次,段落句群检索就是从候选文档集合中检索出最有可能含有答案的段落(自然段落或者文档片段),进一步过滤噪声信息,得到更为精确的答案相关信息。广泛使用的段落检索算法有三个:MultiText算法[6]、IBM 的算法[7,8]和SiteQ算法[9]。Tellex[10]等人的实验结果表明基于密度的算法可以获得相对较好的效果。所谓基于密度的算法, 就是通过考虑查询关键词在段落中的出现次数和接近程度来决定这个段落的相关性。相比之下,Cui[5]提出的检索算法通过把问句和答案都解析成语法树,从两者语法树的结构中找出一些相关性的信息。


问句检索的主要问题在于如何缩小用户提问与知识库中问句之间的语义鸿沟。近几年,研究人员采用基于翻模模型的方法计算从用户提问“翻译”到检索问句的翻译概率,从而实现相似性问句检索。例如,算法[11-14]都是把两个问句看作是不同表达方式的语句,计算两个问句之间的翻译概率。为了计算这种翻译的概率,就必须估计词与词之间进行翻译的概率。这种方法首先需要通过答案相似度计算得到同义或近义的问答对集合,该集合中的相似问题集合就构成了一个估计翻译概率的训练集,类似于机器翻译中多语言平行语料库。实验证明,这样做的效果会比语言模型,Okapi BM25和空间向量模型都好。


2.2.3 答案生成

基于信息检索得到的检索信息,答案生成模块主要实现候选答案的抽取和答案的置信度计算,最终返回简洁性、正确性的答案。按照答案信息粒度,候选答案抽取可以分为段落答案抽取、句子答案抽取、词汇短语答案抽取。段落答案抽取是将一个问题的多个相关答案信息进行汇总、压缩,整理出一个完整简洁的答案。句子答案抽取是将候选答案信息进行提纯,通过匹配计算过滤表面相关,实际语义不匹配的错误答案。词汇短语抽取是采用语言的深层结构分析技术从候选答案中准确地提取答案词或短语。


答案置信度计算是将问题与候选答案进行句法和语义层面上的验证处理,从而保证返回答案是与用户提问最为匹配的结果。应用最广泛是基于统计机器学习的置信度计算方法。这种方法通常定义一系列词法、句法、语义以及其他相关特征(如编辑距离、BM25等)来表示问题与候选答案之间的匹配关系,并使用分类器的分类置信度作为答案的置信度。例如IBM Waston中使用的答案融合和特征排序方法[15],以及基于关系主题空间特征的多核SVM分类方法[16]。近几年,基于自然语言处理的问答匹配验证通常是使用句子的浅层分析获得句子的浅层句法语法信息,然后将问句与答案的句法树(短语句法树或依存句法树)进行相似性计算[17-20]。然而,问答系统的答案正确性更需满足问题和答案之间的语义匹配,比如问“苹果6s plus最新活动价多少”,如果回答“红富士苹果降到了12元”,就属于所答非所问。常用的方法是通过引入诸如语义词典(WordNet),语义知识库(Freebase)等外部语义资源进行问答语义匹配建模[21-23],以此提高问句答案间的语义匹配计算性能。


传统问答系统中构建的机器学习模型基本属于浅层模型。譬如,问句分类过程中常用的基于支持向量机(SVM)的分类模型[24],答案抽取使用的基于条件随机场(CRF)的序列标注模型[25],以及候选答案验证过程中使用的基于逻辑回归(LR)的问答匹配模型[26]等。这种基于浅层模型研发的问答系统往往存在人工依赖性高,并且缺少对不同领域数据处理的泛化能力。人工依赖性主要表现在浅层模型的特征工程上,由于浅层模型缺乏对数据的表示学习的能力,于是在面对不同领域的问答数据以及不同的问答任务的情况下,研究人员不得不进行针对性的数据标注,并且需要依据研究人员的观察和经验来提取模型所需的有效特征,这也就造成了此类问答系统可移植性低的结果。


3 基于深度学习的相关问答技术

近年来,深度神经网络在诸如图像分类、语音识别等任务上被深入探索并取得了突出的效果,表现出了优异的表示学习能力。与此同时,通过深度神经网络对语言学习表示已逐渐成为一个新的研究趋势。然而,由于人类语言的灵活多变以及语义信息的复杂抽象,使得深度神经网络模型在语言表示学习上的应用面临比在图像、语音更大的挑战。其一,相比于语音和图像,语言是非自然信号,完全是人类文明进程中,由大脑产生和处理的符号系统,是人类文明智慧的高度体现。语言的变化性和灵活度远远超过图像和语音信号。其二,图像和语音具有明确的数学表示,例如灰度图像为数学上的数值矩阵,而且其表示的最小粒度元素都有确定的物理意义,图像像素的每个点的值表示一定的灰度色彩值。相比而言,以往的词袋表示方法会导致语言表示存在维数灾难、高度稀疏以及语义信息损失的问题。


当前,研究人员越来越对深度学习模型在NLP领域的应用感兴趣,其主要集中在对词语、句子和篇章的表示学习以及相关应用。例如,Bengio等使用神经网络模型得到一种名为词嵌入(Word Embedding)或词向量的新型向量表示[27],这种向量是一种低维、稠密、连续的向量表示,同时包含了词的语义以及语法信息。当前,基于神经网络的自然语言处理方法大都是基于词向量的表示基础上进行的。在此基础上,相关研究人员设计深度神经网络模型学习句子的向量表示,相关工作包括递归神经网络(Recursive Neural Network)、循环神经网络(Recurrent Neural Network,RNN)、卷积神经网络(Convolutional Neural Network, CNN)的句子建模[28-30]。句子表示被应用于大量的自然语言处理任务上,并在一些任务上取得了较为突出的效果。例如机器翻译[31, 32]、情感分析等[33, 34]。从句子的表示到篇章的表示学习仍然较为困难, 相关工作也较少,比较有代表性是Li等人通过层次循环神经网络对篇章进行编码,然后通过层次循环神经网络进行解码,从而实现对篇章的表示[35]。然而,NLP领域涵盖了不同性质, 不同层次的具体问题,这就需要针对不同问题的特点,设计深度模型学习到任务特定的本质特征。


问答领域所需解决的两个关键问题:一是如何实现问句及答案的语义表示。无论是对于用户提问的理解,还是答案的抽取验证,都需抽象出问题和答案的本质信息的表示。这不仅需要表示问答语句的句法语法信息,更需表示问句及答案在语义层面上的用户意图信息和语义层匹配信息。二是如何实现问句答案间的语义匹配。为了保证反馈用户提问的答案满足严格语义匹配,系统必须合理利用语句高层抽象的语义表示去捕捉到两个文本之间关键而细致的语义匹配模式。鉴于近几年卷积神经网络(CNN)和循环神经网络(RNN)在NLP领域任务中表现出来的语言表示能力,越来越多的研究人员尝试深度学习的方法完成问答领域的关键任务。例如问题分类(question classification),答案选择(answer selection),答案自动生成(answer generation)。此外,互联网用户为了交流信息而产生的大规模诸如微博回复、社区问答对的自然标注数据[50],给训练深度神经网络模型提供了可靠的数据资源,并很大程度上解决自动问答研究领域的数据匮乏问题。


接下来内容安排:首先,分别介绍基于CNN和RNN的问答语句的语义表示方法;然后,介绍基于DCNN的两种语义匹配架构;最后,介绍基于RNN的答案自动生成方法。


3.1 基于深度神经网络的语义表示方法

3.1.1 基于卷积神经网络(CNN)的语义表示方法

基于CNN的语义表示学习是通过CNN对句子进行扫描,抽取特征,选择特征,最后组合成句子的表示向量。首先从左到右用一个滑动窗口对句子进行扫描,每个滑动窗口内有多个单词,每个单词由一个向量表示。在滑动窗口内,通过卷积(convolution)操作,进行特征抽取。这样,在各个位置上得到一系列特征。之后再通过最大池化(max pooling)操作,对特征进行选择。重复以上操作多次,得到多个向量表示,将这些向量连接起来得到整个句子的语义表示。如图3.1所示,基于CNN的句子建模的输入是词向量矩阵,矩阵的每一行的多个点的值在一起才有明确的物理意义,其代表句子中对应的一个词。词向量矩阵是通过将句子中的词转换为对应的词向量,然后按照词的顺序排列得到。该模型通过多层交叠的卷积和最大池化操作,最终将句子表示为一个固定长度的向量。该架构可以通过在模型顶层增加一个分类器用于多种有监督的自然语言处理任务上。

47e1ee4242936d0991f2fddb4626bba9175ae516 

图3.1 基于CNN的句子建模


基于CNN的句子建模可以表现为具有局部选择功能的“组合算子”,随着模型层次的不断加深,模型得到的表示输出能够覆盖的句内词的范围越广,最后通过多层的运算得到固定维度的句子表示向量。该过程的功能与“递归自动编码”的循环操作机制[33]具有一定的功能类似。对于只使用了一层卷积操作和一层全局最大池化操作的句子建模,称之为浅层卷积神经网络模型,这种模型被广泛应用于自然语言处理中句子级分类任务上,如句子分类[36], 关系分类[37]。但是,浅层的卷积神经网络模型不能对句子中复杂的局部语义关系进行建模,也不能对句子中深层次的语义组合进行很好的表示,并且全局最大池化操作丢失了句子中的词序特征,所以浅层的卷积网络模型只能对语句间的局部特征匹配进行建模。面对问答中复杂多样化的自然语言表示形式(如多语同现,异构信息,表情符号等),问答匹配模型[38-40]往往使用深层卷积神经网络(DCNN)来完成问句和答案的句子建模,并将高层输出的问答语义表示传递给多层感知器(MLP)进行问答匹配。


面对开放领域中的关系性推理问题,例如“微软公司的创始人是谁?”,往往通过引入外部语义知识推理得到问题的答案,此时单一的句子建模很难实现逻辑关系的语义表示。通常先需要对问题进行语义解析(Semantic Parse),然后针对问句实体、实体关系等不同类型的语义信息进行表示学习。Yih将关系性问题拆分成实体集合和关系模板[41],其中实体集合为问题中连续词语的子序列,关系模板为问句实体被特殊符号替换后的句子,针对实体集合和关系模板分别使用CNN进行句子建模,从而实现问句在实体及关系两个层面上的语义表示。Dong提出多栏(Multi-Column)卷积神经网络模型[42]对关系推理性问题进行不同层面(词语表达层、实体关系层、语境信息层)的语义表示学习,并实现从关系知识库中抽取候选答案的多层面语义信息,最后与候选答案进行多层次匹配打分。


3.1.2 基于循环神经网络(RNN)的语义表示方法

基于RNN的句子建模是把一句话看成单词的序列,每个单词由一个向量表示,每一个位置上有一个中间表示,由向量组成,表示从句首到这个位置的语义。这里假设,每一个位置的中间表示由当前位置的单词向量以及前一个位置的中间表示决定,通过一个循环神经网络模型化。RNN把句末的中间表示当作整个句子的语义表示,如图3.2所示。

922ddb6e0bafcf4fab21a3ff8aa6fac8b5a1af35 

图3.2 基于RNN的语句建模


RNN与隐马尔可夫模型有相似的结构,但是具有更强的表达能力,中间表示没有马尔可夫假设,而且模型是非线性的。然而,随着序列长度的增加,RNN在训练的过程中存在梯度消失(Vanishing gradient problem)的问题[43]。为了解决这个问题,研究人员对循环神经网络中的循环计算单元进行改善设计,提出了不同的变形,如常用的长短记忆(Long Short Term Memory, LSTM)[44, 45]和门控循环单元(Gated Recurrent Unit, GRU)[56]。这两种RNN可以处理远距离依存关系,能够更好地表示整句的语义。Wang和Nyberg [47]通过双向LSTM学习问题答案对的语义表示,并将得到的表示输入到分类器计算分类置信度。


此外,对于近几年的看图回答的任务(Image QA),研究人员通过整合CNN和RNN完成问题的图像场景下的语义表示学习。基本想法:模型在RNN对问句进行词语序列扫描的过程中,使用基于深度学习的联合学习机制完成“图文并茂”的联合学习,从而实现图像场景下的问句建模,用于最终的问答匹配。例如,Malinowski等人[48]提出的学习模型在RNN遍历问句词语的过程中,直接将CNN得到的图像表示与当前词语位置的词向量作为RNN学习当前中间表示的输入信息,从而实现图像与问句的联合学习。相比之下,Gao等人[49]则是先用RNN完成问题的句子建模,然后在答案生成的过程中,将问句的语义表示向量和CNN得到的图像表示向量都作为生成答案的场景信息。


3.2 基于DCNN的语义匹配架构

问答系统中的语义匹配涉及到主要功能模块包括:问句检索,即问句的复述检测(paraphrase);答案抽取,即问句与候选文本语句的匹配计算;答案置信度排序,即问题与候选答案间的语义匹配打分。


3.2.1 并列匹配架构

第一种基于DCNN的语义匹配架构为并列匹配 [38-40]架构。这种架构的匹配模型分别将两句话输入到两个CNN句子模型,可以得到它们的语义表示(实数值向量)。之后,再将这两个语义表示输入到一个多层神经网络,判断两句话语义的匹配程度,从而判断给定的两句话和是否可以成为一对句子匹配对(问答对)。这就是基于DCNN的并列语义匹配模型的基本想法。如果有大量的信息和回复对的数据,就可以训练这个模型。

f6569854b61005de635f9df30b89e0ebffcad9ce 

图3.3 基于DCNN的并列匹配架构


从图3.3所示的并列匹配架构可以看出,这种匹配模型的特点是两个句子的表示分别通过两个独立的卷积神经网络(CNN)得到,在得到它们各自的表示之前,两个句子间的信息互不影响。这种模型是对两个需要匹配的句子从全局语义上进行匹配,但是忽略了两个句子间更为精细的局部匹配特征。然而,在语句匹配的相关问题中,两个待匹配的句子中往往存在相互间的局部匹配,例如问题答案对:


Sx: 好饿啊,今天去哪里吃饭呢。

Sy: 听说肯德基最近出了新品,要不要去尝尝。


在这一问答对中,“吃饭”和“肯德基”之间具有较强的相关性匹配关系,而并列匹配则是对句子两个句子全局的表示上进行匹配,在得到整个句子的表示之前,“吃饭”和“肯德基”之间并不会互相影响,然而,随着深度卷积句子模型对句子的表示层次不断深入,而句子中的细节信息会部分丢失,而更关注整个句子的整体语义信息。


3.2.2 交互匹配架构

第二种基于DCNN的语义匹配架构为交互匹配[39]架构。与并列匹配不同,交互匹配的基本想法是直接对两个句子的匹配模式进行学习,在模型的不同深度对两个句子间不同粒度的局部之间进行交互,学习得到句子匹配在不同层次上的表示,最终得到句子对固定维度的匹配表示,并对匹配表示进行打分。

4e134fd447d4980739056526a8a03dff29abd58e 

图3.4 基于DCNN的交互匹配架构


如图3.4所示,交互匹配架构在第一层通过两个句子间的滑动窗口的卷积匹配操作直接得到了两个句子间较为底层的局部匹配表示,并且在后续的高层学习中采用类似于图像领域处理过程中的二维卷积操作和二维局部最大池化操作,从而学到问句与答案句子之间的高层匹配表示。通过这种形式,使得匹配模型既能对两个句子的局部之间的匹配关系进行丰富建模,也使模型能够对每个句子内的信息进行建模。很显然,交互匹配学习得到的结果向量不仅包含来自两个句子的滑动窗口的位置信息,同时具有两个滑动窗口的匹配表示。


对于问答的语义匹配,交互匹配可以充分考虑到问句与答案间的内部匹配关系,并通过二维的卷积操作与二维局部最大池化操作学习得到问句与答案间的匹配表示向量。在整个过程中,交互匹配更为关注句子间的匹配关系,对两个句子进行更为细致的匹配。


相比于并列匹配,交互匹配不仅考虑到单个句子中滑动窗口内的词的组合质量,而且同时考虑到来自两个句子组合间的匹配关系的质量。并列匹配的优势在于匹配过程中可以很好的保持两个句子各自的词序信息,因为并列匹配是分别对两个句子在顺序的滑动窗口上进行建模。相对而言,交互匹配的问答匹配过程是学习语句间局部信息的交互模式。此外,由于交互匹配的局部卷积运算和局部最大池化操作都不改变两个句子的局部匹配表示的整体顺序,所以交互匹配模型同样可以保持问句与答案的词序信息。总之,交互匹配通过对问句与答案的匹配模式进行建模,可以学习到两个句子间的局部匹配模式,而这种匹配模式在正常顺序的句子中具备很大的学习价值。


3.3基于RNN的答案自动生成方法

与基于检索式的回复机制对比而言,基于生成式的答案反馈机制是根据当前用户输入信息自动生成由词语序列组成的答案,而非通过检索知识库中用户编辑产生答案语句。这种机制主要是利用大量交互数据对构建自然语言生成模型,给定一个信息,系统能够自动生成一个自然语言表示的回复。其中的关键问题是如何实现这个语言生成模型。


答案自动生成需要解决两个重要问题,其一是句子表示,其二是语言生成。近年来,循环神经网络在语言的表示以及生成方面都表现出了优异的性能,尤其是基于循环神经网络的编码-解码架构在机器翻译[31, 32]和自动文摘[51]任务上取得了突破。Shang[52]等人基于CRU(Gated Recurrent Unit, GRU)[46]循环神经网络的编码-解码框架,提出了完全基于神经网络的对话模型“神经响应机”(Neural Responding Machine,NRM),该模型用于实现人机之间的单轮对话(single-turn dialog)。NRM是从大规模的信息对(问题-答案对,微博-回复对)学习人的回复模式,并将学到的模式存于系统的近四百万的模型参数中,即学习得到一个自然语言生成模型。


如图3.5所示,NRM的基本想法是将输入的一句话看作一个单词表示的序列,通过编码器(Encoder),即一个RNN模型,将转换成一个中间表示的序列,再通过解码器(Decoder),是另一个RNN模型,将转换成一个单词的系列,作为一句话输出。由于NRM在编码部分采用一种混合机制,从而使编码得到中间表示的序列不仅能够实现用户语句信息的整体把握,同时还能充分保留句子的细节信息。并且在解码部分采用了注意力(attention)机制[31],从而使生成模型可以相对容易的掌握问答过程中的复杂交互模式。[52]中的实验结果表明基于生成式的问答机制与基于检索式的答案反馈机制各具特点:在表达形式个性化的微博数据上,生成式比检索式的准确率会高一些,检索系统的准确率是70%,生成系统的准确率是76%。但是,生成式得到的答案会出现语法不通,连贯性差的问题,而检索式的答案来源于真实的微博用户编辑,所以语句的表述更为合理可靠。

a6cd1208668cf92843df7520373e12ddba2c2ebb 

图3.5 基于编码-解码结构的答案生成模型


目前,NRM以及Google的Neural Conversational Model(NCM)[53]主要还是在对复杂语言模式记忆和组合上层面上实现语言生成,尚无法在交互过程使用外界的知识。例如,在对“五一期间杭州西湖相比去年怎么样吗?”这样的句子,无法给出真实的状况(旅游人数的对比结果)相关的回复。虽然如此,但是NRM和NCM的真正意义在于初步实现了类人的语言自动反馈,因为此前的近几十年,研究人员不懈努力而生成的问答或对话系统(dialogue model),大都是基于规则和模板,或者是在一个较大的数据库中进行搜索,而这种两种方式并非真正的产生反馈,并且缺乏有效的语言理解和表示。这往往是由于模板/例子的数量和表示的局限性,这些方式在准确性和灵活性上都存在一定不足,很难兼顾语言的自然通顺和语义内容上的匹配。


4 结语

本文简单介绍了问答系统的发展历程、基本体系结构。并针对问答系统所需解决的关键问题,介绍了基于深度神经网络的语义表示方法,不同匹配架构的语义匹配模型,以及答案生成模型。当前深度学习在解决问答领域中的关键问题取得了不错的效果,但是问答系统的技术研究仍然存在有待解决问题,比如,如何理解连续交互问答场景下的用户提问,例如与Siri系统交互中的语言理解。以及如何学习外部语义知识,使问答系统能够进行简单知识推理回复关系推理性问题,例如“胸闷总咳嗽,上医院应该挂什么科”。再者,随着最近注意(attention)机制、记忆网络(Memory Network)[54,55]在自然语言理解,知识推理上的研究推广,这也必将给自动问答的研究提供的新的发展方向和契机。



注:文章来源于阿里巴巴-哈尔滨工业大学的合作项目:基于深度学习的智能问答。

 

参 考 文 献

[1] Terry Winograd. Five Lectures on Artificial Intelligence [J]. Linguistic Structures Processing, volume 5 of Fundamental Studies in Computer Science, pages 399- 520, North Holland, 1977.

[2] Woods W A. Lunar rocks in natural English: explorations in natural language question answering [J]. Linguistic Structures Processing, 1977, 5: 521−569.

[3] Dell Zhang and Wee Sun Lee. Question classification using support vector machines. In SIGIR, pages 26–32. ACM, 2003

[4] Xin Li and Dan Roth. Learning question classifiers. In COLING, 2002

[5] Hang Cui, Min-Yen Kan, and Tat-Seng Chua. Unsupervised learning of soft patterns for generating definitions from online news. In Stuart I. Feldman, Mike Uretsky, Marc Najork, and Craig E. Wills, editors, Proceedings of the 13th international conference on World Wide Web, WWW 2004, New York, NY, USA, May 17-20, 2004, pages 90–99. ACM, 2004.

[6] Clarke C, Cormack G, Kisman D, et al. Question answering by passage selection (multitext experiments for TREC-9) [C]//Proceedings of the 9th Text Retrieval Conference(TREC-9), 2000.

[7] Ittycheriah A, Franz M, Zhu W-J, et al. IBM’s statistical question answering system[C]//Proceedings of the 9th Text Retrieval Conference (TREC-9), 2000.

[8] Ittycheriah A, Franz M, Roukos S. IBM’s statistical question answering system—TREC-10[C]//Proceedings of the 10th Text Retrieval Conference (TREC 2001), 2001.

[9] Lee G G, Seo J, Lee S, et al. SiteQ: engineering high performance QA system using lexico-semantic pattern.

[10] Tellex S, Katz B, Lin J, et al. Quantitative evaluation of passage retrieval algorithms for question answering[C]// Proceedings of the 26th Annual International ACM SIGIR Conference on Research and Development in Information Retrieval (SIGIR ’03). New York, NY, USA: ACM, 2003:41–47.

[11] Jiwoon Jeon, W. Bruce Croft, and Joon Ho Lee. Finding similar questions in large question and answer archives. In Proceedings of the 2005 ACM CIKM International Conference on Information and Knowledge Management, Bremen, Germany, October 31 – November 5, 2005, pages 84–90. ACM, 2005.

[12] S. Riezler, A. Vasserman, I. Tsochantaridis, V. Mittal, Y. Liu, Statistical machine translation for query expansion in answer retrieval, in: Proceedings of the 45th Annual Meeting of the Association of Computational Linguistics, Association for Computational Linguistics, Prague, Czech Republic, 2007, pp. 464–471.

[13] M. Surdeanu, M. Ciaramita, H. Zaragoza, Learning to rank answers on large online qa collections., in: ACL, The Association for Computer Linguistics, 2008, pp. 719–727.

[14] A. Berger, R. Caruana, D. Cohn, D. Freitag, V. Mittal, Bridging the lexical chasm: statistical approaches to answer-finding, in: SIGIR ’00: Proceedings of the 23rd annual international ACM SIGIR conference on Research and development in information retrieval, ACM, New York, NY, USA, 2000, pp. 192–199.

[15] Gondek, D. C., et al. "A framework for merging and ranking of answers in DeepQA." IBM Journal of Research and Development 56.3.4 (2012): 14-1.

[16] Wang, Chang, et al. "Relation extraction and scoring in DeepQA." IBM Journal of Research and Development 56.3.4 (2012): 9-1.

[17] Kenneth C. Litkowski. Question-Answering Using Semantic Triples[C]. Eighth Text REtrieval Conference (TREC-8). Gaithersburg, MD. November 17-19, 1999.

[18] H. Cui, R. Sun, K. Li, M.-Y. Kan, T.-S. Chua, Question answering passage retrieval using dependency relations., in: R. A. Baeza-Yates, N. Ziviani, G. Marchionini, A. Moffat, J. Tait (Eds.), SIGIR, ACM, 2005, pp. 400–407.

[19] M. Wang, N. A. Smith, T. Mitamura, What is the jeopardy model? a quasisynchronous grammar for qa., in: J. Eisner (Ed.), EMNLP-CoNLL, The Association for Computer Linguistics, 2007, pp. 22–32.

[20] K. Wang, Z. Ming, T.-S. Chua, A syntactic tree matching approach to finding similar questions in community-based qa services, in: Proceedings of the 32Nd International ACM SIGIR Conference on Research and Development in Information Retrieval, SIGIR ’09, 2009, pp. 187–194.

[21] Hovy, E.H., U. Hermjakob, and Chin-Yew Lin. 2001. The Use of External Knowledge of Factoid QA. In Proceedings of the 10th Text Retrieval Conference (TREC 2001) [C], Gaithersburg, MD, U.S.A., November 13-16, 2001.

[22] Jongwoo Ko, Laurie Hiyakumoto, Eric Nyberg. Exploiting Multiple Semantic Resources for Answer Selection. InProceedings of of LREC(Vol. 2006).

[23] Kasneci G, Suchanek F M, Ifrim G, et al. Naga: Searching and ranking knowledge. IEEE, 2008:953-962.

[24] Zhang D, Lee W S. Question Classification Using Support Vector Machines[C]. Proceedings of the 26th Annual International ACM SIGIR Conference on Research and Development in Information Retrieval. 2003. New York, NY, USA: ACM, SIGIR’03.

[25] X. Yao, B. V. Durme, C. Callison-Burch, P. Clark, Answer extraction as sequence tagging with tree edit distance., in: HLT-NAACL, The Association for Computer Linguistics, 2013, pp. 858–867.

[26] C. Shah, J. Pomerantz, Evaluating and predicting answer quality in community qa, in: Proceedings of the 33rd International ACM SIGIR Conference on Research and Development in Information Retrieval, SIGIR ’10, 2010, pp. 411–418.

[27] T. Mikolov, K. Chen, G. Corrado, J. Dean, Efficient estimation of word representations in vector space, CoRR abs/1301.3781.

[28] Socher R, Lin C C, Manning C, et al. Parsing natural scenes and natural language with recursive neural networks[C]. Proceddings of International Conference on Machine Learning. Haifa, Israel: Omnipress, 2011: 129-136.

[29] A. Graves, Generating sequences with recurrent neural networks, CoRR abs/1308.0850.

[30] Kalchbrenner N, Grefenstette E, Blunsom P. A Convolutional Neural Network for Modelling Sentences[C]. Proceedings of ACL. Baltimore and USA: Association for Computational Linguistics, 2014: 655-665.

[31] Bahdanau D, Cho K, Bengio Y. Neural machine translation by jointly learning to align and translate [J]. arXiv, 2014.

[32] Sutskever I, Vinyals O, Le Q V V. Sequence to Sequence Learning with Neural Networks[M]. Advances in Neural Information Processing Systems 27. 2014: 3104-3112.

[33] Socher R, Pennington J, Huang E H, et al. Semi-supervised recursive autoencoders for predicting sentiment distributions[C]. EMNLP 2011

[34] Tang D, Wei F, Yang N, et al. Learning Sentiment-Specific Word Embedding for Twitter Sentiment Classification[C]. Proceedings of the 52nd Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers). Baltimore, Maryland: Association for Computational Linguistics, 2014: 1555-1565.

[35] Li J, Luong M T, Jurafsky D. A Hierarchical Neural Autoencoder for Paragraphs and Documents[C]. Proceedings of ACL. 2015.

[36] Kim Y. Convolutional Neural Networks for Sentence Classification[C]. Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP). Doha, Qatar: Association for Computational Linguistics, 2014: 1746–1751.

[37] Zeng D, Liu K, Lai S, et al. Relation Classification via Convolutional Deep Neural Network[C]. Proceedings of COLING 2014, the 25th International Conference on Computational Linguistics: Technical Papers. Dublin, Ireland: Association for Computational Linguistics, 2014: 2335–2344.

[38] L. Yu, K. M. Hermann, P. Blunsom, and S. Pulman. Deep learning for answer sentence selection. CoRR, 2014.

[39] B. Hu, Z. Lu, H. Li, Q. Chen, Convolutional neural network architectures for matching natural language sentences., in: Z. Ghahramani, M. Welling, C. Cortes, N. D. Lawrence, K. Q. Weinberger (Eds.), NIPS, 2014, pp. 2042–2050.

[40] A. Severyn, A. Moschitti, Learning to rank short text pairs with convolutional deep neural networks., in: R. A. Baeza-Yates, M. Lalmas, A. Moffat, B. A. Ribeiro-Neto (Eds.), SIGIR, ACM, 2015, pp. 373-382.

[41] Wen-tau Yih, Xiaodong He, and Christopher Meek. 2014. Semantic parsing for single-relation question answering. In Proceedings of the 52nd Annual Meeting of the Association for Computational Linguistics, pages 643–648. Association for Computational Linguistics.

[42] Li Dong, Furu Wei, Ming Zhou, and Ke Xu. 2015. Question Answering over Freebase with Multi-Column Convolutional Neural Networks. In Proceedings of the 53rd Annual Meeting of the Association for Computational Linguistics (ACL) and the 7th International Joint Conference on Natural Language Processing.

[43] Hochreiter S, Bengio Y, Frasconi P, et al. Gradient flow in recurrent nets: the difficulty of learning long-term dependencies[M]. A Field Guide to Dynamical Recurrent Neural Networks. New York, NY, USA: IEEE Press, 2001.

[44] Hochreiter S, Schmidhuber J. Long Short-Term Memory[J]. Neural Comput., 1997, 9(8): 1735-1780.

[45] Graves A. Generating Sequences With Recurrent Neural Networks[J]. CoRR, 2013, abs/1308.0850.

[46] Chung J, Gülçehre Ç, Cho K, et al. Gated Feedback Recurrent Neural Networks[C]. Proceedings of the 32nd International Conference on Machine Learning (ICML-15). Lille, France: JMLR Workshop and Conference Proceedings, 2015: 2067-2075.

[47] D.Wang, E. Nyberg, A long short-term memory model for answer sentence selection in question answering., in: ACL, The Association for Computer Linguistics, 2015, pp. 707–712.

[48] Malinowski M, Rohrbach M, Fritz M. Ask your neurons: A neural-based approach to answering questions about images[C]//Proceedings of the IEEE International Conference on Computer Vision. 2015: 1-9.

[49] Gao H, Mao J, Zhou J, et al. Are You Talking to a Machine? Dataset and Methods for Multilingual Image Question[C]//Advances in Neural Information Processing Systems. 2015: 2287-2295.

[50] Sun M S. Natural Language Procesing Based on Naturaly Annotated Web Resources [J]. Journal of Chinese Information Processing, 2011, 25(6): 26-32.

[51] Hu B, Chen Q, Zhu F. LCSTS: a large scale chinese short text summarization dataset[J]. arXiv preprint arXiv:1506.05865, 2015.

[52] Shang L, Lu Z, Li H. Neural Responding Machine for Short-Text Conversation[C]. Proceedings of the 53rd Annual Meeting of the Association for Computational Linguistics and the 7th International Joint Conference on Natural Language Processing. Beijing, China: Association for Computational Linguistics, 2015: 1577-1586.

[53] O. Vinyals, and Q. V. Le. A Neural Conversational Model. arXiv: 1506.05869,2015.

[54] Kumar A, Irsoy O, Su J, et al. Ask me anything: Dynamic memory networks for natural language processing[J]. arXiv preprint arXiv:1506.07285, 2015.

[55] Sukhbaatar S, Weston J, Fergus R. End-to-end memory networks[C]//Advances in Neural Information Processing Systems. 2015: 2431-2439.

 

centos7使用lldb调试netcore应用转储dump文件 - czd890 - 博客园

$
0
0

centos7下安装lldb,dotnet netcore 进程生成转储文件,并使用lldb进行分析

随着netcore应用在linux上部署的应用越来越多,碰到cpu 100%,内存暴涨的情况也一直偶有发生,在windows平台下进程管理器右键转储,下载到本地使用windbg或者直接vs分析都比较方便。而在linux平台下因为一直接触的不深,所以对这一块也一直没有比较好的了解。所以接下来的文章将对在centos7下安装lldb,生成转储以及调试分析进行一些简单说明。
还有就是一般产线的机器也不太会有可以直接调试的机会,所以真出现问题也只能在产线机器dump进程,然后下载到本地来慢慢分析。

环境说明:
os:centos7
dotnet :2.1.1。查看官方文档2.0.0只能使用lldb 3.6;2.1以上必须是3.9.0;所以特别要注意版本问题,一个是createdump 2.0的有bug会失败。二个是dotnet版本和lldb版本要匹配
被调试分析的应用也是用2.1跑起来的。

测试目标程序

yum install dotnet-sdk-2.1
dotnet new mvc
vi /mvc.csproj
#netcoreapp2.0 to netcoreapp2.1
#PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" to Version="2.1.1"
dotnet restore
dotnet build
dotnet ./bin/Debug/netcoreapp2.1/mvc.dll

centos7 升级GCC,安装cmake

centos7 升级GCC版本到7.3.0
centos7 安装cmake

centos7下安装lldb调试工具

最开始直接使用给力网友的脚本进行安装(脚本地址查看文章结尾参考资料),后发现3.9.1不能调试分析netcore应用,必须要3.9.0,所以在给力网友的脚本上略作修改后使用。修改后脚本地址 https://github.com/czd890/shell/blob/master/llvm_clang_lldb/3.9.0/llvm_clang_install.sh。主要修改几个地方:把lldb,libunwind移动到build_llvm_toolchain中,一次性安装。check_and_download方法中检查本地是否已下载源码包的检查略作修改,只判断指定版本,编译的时候修改为make -j8(我本地机器8核)。

脚本大概思路就是下载如下所表示的组件所有源码,除llvm外的其他组件源代码解压到llvm/tools目录下,这样子源代码就全部准备好
BUILD_TARGET_COMPOMENTS="llvm clang compiler_rt libcxx libcxxabi clang_tools_extra lldb lld libunwind";
接下来就是编译的过程了。

#安装一些必要的依赖组件
yum install libedit-devel libxml2-devel ncurses-devel python-devel swig
#执行根据给力网友的脚本修改后的脚本

当然如果脚本下载速度慢,也是可以自己下载后上传的目录的。具体下载地址查看文章尾部参考资料 llvm,clang,lldb源代码下载地址(3.9.0)
准备源代码差不多就如下图。然后 sh llvm_clang_install.sh开始执行脚本;
默认安装目录在 PREFIX_DIR=/usr/local/llvm-$LLVM_VERSION;。也就是是 /usr/local/llvm-3.9.0;可以在脚本的最开始对此进行修改。

开始执行,又是一段漫长的等待时间,8核并发编译,耗费了估计得有1-2个小时。

刀片机的CPU都跑满了!!!

出去吃完饭后回来,就看到完成拉。具体的path路径可以选择加不加都可以,加的话,直接/etc/profile export PATH=$PATH:llvm-path/bin即可

lldb安装完成,我们的工作就完成一大半拉。

dotnet netcore应用如何生成内存转储文件

/usr/share/dotnet/shared/Microsoft.NETCore.App/2.1.1/createdump 9364


具体命令解释
createdump [options] pid
-f, --name - dump path and file name. The pid can be placed in the name with %d. The default is "/tmp/coredump.%d"
-n, --normal - create minidump (default).
-h, --withheap - create minidump with heap.
-t, --triage - create triage minidump.
-u, --full - create full core dump.
-d, --diag - enable diagnostic messages.

使用lldb调试分析netcore应用内存转储文件

#官方文档上是这样写的。
/usr/local/llvm-3.9.0/bin/lldb -O "settings set target.exec-search-paths /usr/share/dotnet/shared/Microsoft.NETCore.App/2.1.1"  \
  -o "plugin load /usr/share/dotnet/shared/Microsoft.NETCore.App/2.1.1/libsosplugin.so" \
   --core /opt/dump\_file/mvcdumpmindump /usr/share/dotnet/dotnet

 #网友调试参考博客上是这样写的。
 /usr/local/llvm-3.9.0/bin/lldb dotnet \
  -c /opt/dump\_file/mvcdumpmindump \
  -o "plugin load /usr/share/dotnet/shared/Microsoft.NETCore.App/2.1.1/libsosplugin.so"

2种写法都是可行的。然后具体的调试分析指令什么的都在 coreclr调试说明指导文档有说明。

参考资料:
coreclr调试说明指导文档
https://github.com/dotnet/coreclr/blob/master/Documentation/building/debugging-instructions.md
coreclr生成dmp说明指导文档
https://github.com/dotnet/coreclr/blob/master/Documentation/botr/xplat-minidump-generation.md
llvm,clang,lldb源代码下载地址(3.9.0)
http://releases.llvm.org/download.html#3.9.0
lldb源码安装指导文档
http://lldb.llvm.org/build.html#BuildingLldbOnLinux
llvm源码安装指导文档
http://releases.llvm.org/3.9.0/docs/GettingStarted.html
网友centos7安装llvm,clang,lldb等给力脚本
https://github.com/owent-utils/bash-shell/blob/master/LLVM%26Clang%20Installer/3.9/installer.sh
网友调试参考博客文章
使用SOS调试工具检查应用程序状态

基于知识图谱的人机对话系统 | 公开课笔记 - AI科技大本营 - CSDN博客

$
0
0

分享嘉宾 | 刘升平(云知声 AI Labs 资深技术专家)

来源 | AI 科技大本营在线公开课

人机对话系统,或者会话交互,有望成为物联网时代的主要交互方式。而语言的理解与表达和知识是密切联系的,知识图谱作为一种大规模知识的表示形式,在人机对话系统中各模块都有重要的应用。而知性对话,则是基于知识图谱的人机会话交互服务。

AI 科技大本营邀请到了云知声 AI Labs 资深专家刘升平针对 「基于知识图谱的人机对话系统方法与实践」做系统的讲解和梳理。

视频回放地址: https://edu.csdn.net/huiyiCourse/detail/847

本次公开课介绍了知性会话的架构和关键技术,并结合工业级的人机对话系统实践经验,阐述了知识图谱在人机对话系统的核心模块上面的应用等等。

 

640?wx_fmt=png

 刘升平:云知声 AI Labs 资深技术专家/高级研发总监。前 IBM 研究院资深研究员,中文信息学会语言与知识计算专委会委员。2005 年获得北京大学数学学院博士,国内语义网研究的开创者之一,是 2010 年和 2011 年国际语义网大会的程序委员会委员。曾在语义网,机器学习、信息检索,医学信息学等领域发表过 20 多篇论文。在 IBM 工作期间,两次获得 IBM 研究成就奖。2012 年底,刘博士加入云知声 AI Labs,领导 NLP 团队,全面负责自然语言理解和生成、人机对话系统、聊天机器人、知识图谱、智慧医疗等方面的研发及管理工作。

在本次公开课上,他全面而具体地讲述了知识图谱在人机对话系统中的发展与应用,主要分为以下四部分:

  • 语言知识、对话系统综述

  • 知性会话的基本概念及案例分析

  • 知性会话的关键技术:知识图谱的构建,实体发现与链接

  • 知性会话的关键技术:话语理解及自然应答生成

 

▌一、语言、知识与人机对话系统综述

 

1. 语言和知识

语言和知识是密切相关的。这张冰山图很形象地解释了语言和知识的关系,我们看到的语言只是冰山上的一小角,就是我们说的话。但是你如果想理解这句话,跟这句话相关的背景知识就像冰山下面这一大块。

所以,这也是自然语言跟语音、图像很不一样的地方,我们听语音或看一个图片,它的所有信息都在语音信号或者图像像素里,但是语言的话就完全不是这样,这也是自然语言理解远远比语音识别或者图像识别更难的一个地方。

这次的报告内容综合了我最近三年在CCKS会议上做的三个报告。

640?wx_fmt=png

 

2. 人机对话系统

 

640?wx_fmt=png

 

人机对话系统最早在工业界引起比较大的轰动是 Apple Siri,它当时还是 iPhone 上的一个 APP,2010 年被苹果收购了。Siri 的创新在于,我们传统的手机 GUI 界面上加了一个语音 Voice-UI。

真正引发人机对话系统革命性创新的是于 2014 年推出的 Amazon Echo,它是一个完全基于语音交互的硬件,其语音技术比 Siri 前进了一大截,因为它支持远讲。

2017 年亚马逊又推出了一个带屏幕的音箱 Amazon Echo Show,大家觉得这是不是又回到 Siri,还是基于 GUI 呢?这里要注意区别, Amazon Echo 是 VUI+GUI,也就是说它是以 VUI 优先的,因为语音的优势是输入很便捷,你说几个字就能代表一段指令,可以代替操作很多界面。但缺点是输出很低效,如果在屏幕上显示很多内容,但是你要用语音说出来,可能得花好几分钟。所以 VUI+GUI 的结合是把两者优势做了整合,VUI 用来做输入,GUI 用来做输出。

更高级的形态是现在很多电影都能看到的像Eva、《钢铁侠》或者《西部世界》里面这种人形的机器人,完全可以跟人自由对话,它的交互是 VUI++,真正模拟人的多模态的交互形态,这个时间点也许在 2045 年会出现。

为什么人机对话系统目前在工业界这么热门?它最重要的一个意义是有望取代目前在手机上的 APP,成为 IoT 时代的一个最重要的人机交互形式,这是它的最主要意义所在。

3. 人机对话系统的交互形式和应用场景

就像人和人说话有多种目的和形式一样,人机对话系统也包含很多种交互形式:

1、聊天。典型代表是小冰,它包括问候和寒暄,其特点是没有明确目的,而且不一定回答用户的问题。聊天在现有的人机对话系统中主要是起到情感陪伴的作用。

2、问答。它要对用户的问答给出精准的答案。这些问题可以是事实性的问题,如“姚明有多高”,也可能是其他定义类,描述类或者比较类的问题。问答系统可以根据问答的数据来源分为基于常见问题-答案列表的FAQ问答,基于问答社区数据的CQA问答,基于知识库的KBQA问答。

3、操控,只是解析出它的语义,来供第三方执行,最典型的操控是打开空调、打开台灯,或者播放某一首歌。

4、任务式对话。它是一个目的性很强的对话,目标是收集信息,以完成某个填表单式的任务,最常见的像订外卖、订酒店、订机票,这种方式通过对话来做。

5、主动对话。让机器主动发起话题,不同的是,前面的交互都是让人来主动发起这个交互。

640?wx_fmt=png

 

目前人机对话系统的应用场景有很多,像音箱、电视、空调等等,其显著特点是它不是人可以直接触摸到的,可以将语音交互看成遥控器的一种替代品,有遥控器的地方就可以用语音来交互。

另外一个应用场景是在车载方面,因为在开车时,你的眼睛和手脚都被占用着,所以这时通过语音来接听电话、导航甚至收发微信,是非常方便的,也比较安全。车载是刚需场景,所以目前出货量最多是在这块。像我们是从 2014 年开始做车载语音交互方案,到现在有 1500 多万的出货量。

另外一个应用领域是儿童教育机器人,右下角这些各种形状的儿童机器人,实际上可以看成儿童版的音箱,它的内容是面向儿童的,但是交互形式也是人机对话的方式。

4. 人机对话技术架构

 

640?wx_fmt=png

人机对话系统从学术界来讲,它的研究历史非常悠久,可能 AI 提出以后,在七八十年代就开始研究。它的技术分为五大部分:

 

1、语音识别:主要解决复杂真实场景噪声、用户口音多样的情况下,把人说的话转成文字,即做到“听得清”。

2、语义理解:主要是把用户说的话转成机器能理解执行的指令或查询,即做到“听得懂”。

3、对话管理:维护对话状态和目标,决定系统应该怎么说、怎么问下一句话,也就是生成一个应答的意图。

4、自然语言生成:就是根据系统应答的意图,用自然语言把这个应答意图表达出来。

5、语音合成: 用机器合成的语音把这句话播报出来。

这样形成一个完整人机对话的闭环。

5. 语音识别场景演进

 

640?wx_fmt=png

因为人机对话系统是以语音作为入口,所以需要讲讲语音技术这块的进展。强调一点的是,如果想真正做好人机对话系统,除了对自然语言处理技术了解之外,对语音技术也必须有所了解。

最早像 Siri 这样的场景是近讲模式,它最主要解决的问题是口音问题,目前这方面的识别准确率非常高,已经能做到 97% 左右,大家平时用的手机语音输入法就是这种模式,一般建议离麦克风的距离是30cm左右。

Amazon Echo 则是远讲模式,你可以离麦克风 3 米甚至 5 米这么远。它要解决的问题很多,因为你离它远了以后更容易受周边噪音的影响,还有一个更致命的影响是声音反射引起的混响问题,特别是在玻璃房里,声音不断在反射,麦克风收到的声音就是很多声音混杂在一起。还有一个很不一样的地方,就是我们用微信语音的时候可以按下说,或者按着一直说,但当你面对一个音箱时,因为你离它有 3-5 米远,不可能按着说话的,这时就有新的技术,叫“语音唤醒”,就像我们跟人说话时叫人的名字一样,像“Hi,Google”,先唤醒机器,再同它对话。

目前语音识别最难的场景是人人对话,在人和人对话的时候,先对它做录音,而且要把它转成文字,这个最常见的场景像开会,自动把不同的人说话转录下来,甚至自动形成会议纪要。还有像司法的庭审,只要是和人说话的场景下都可以用到。这里面最难的问题是鸡尾酒会问题,很多人在一起,环境很嘈杂,大家都在说话,人可以听到只关注的人的说话,即使很嘈杂,但两个人一样可以聊天对话,但这对机器来说很难。

6. 人机对话系统中的机器角色演进

 

640?wx_fmt=png

在人机对话里面机器的角色有个演进的过程:最早人机对话很简单,可以看成是个遥控器的替代品,用户通过固定句式或者单句指令来控制这个系统。

Siri、Amazon Echo 是一种助手的形态,也就是说,你可以通过自然语言交互,且对话是多轮的,甚至可以让机器有些情感。

但是下一个阶段是它会变成专家的角色,特别是面向行业或者特定领域时,当我们跟音箱对话时,希望这个音箱同时也是一个音乐专家,它可以跟你聊音乐的问题,可以跟你聊古典音乐,甚至教你一些音乐知识。我们跟儿童教育机器人对话时,希望这个机器人是一个儿童教育专家,我们跟空调对话时希望后面是个空调专家。这时它的特点是需要有这个领域的知识,而且能够帮你做推荐、做决策。

 

▌二、知性会话基本概念及示例分析

 

我们做对话必须理解这几个概念——语义、语境、语用。特别是语境,它就是在对话时才有含义,语境就是指人和人发生对话时的一个具体环境,这个环境又包括言语语境,就是我们所说的上下文,还有很多非言语语境,如说话的时间、地点、天气都是非言语语境,还有说话人的信息等等,我们今天强调的知识也是一种重要的非言语语境。

假如用户说「太冷了」这三个字,语义是温度有点低,但如果考虑语用,这句话在特定语境下面传递的会话意义、真实含义: 如果在车里面开着空调,理解这句话的意思是把空调温度调高一点;如果是冬天没有开空调,这句话的意思可能是把车的暖气打开,或者把车的窗户关上;现在马上到秋天了,如果一个女孩子对你说「太冷了」,她的含义可能是想让你给她一个拥抱之类的。所以语境和语用是非常重要的概念,如果做人机对话系统,都会接触到这两个概念。

1. 人机(设备)对话系统下的语境

 

640?wx_fmt=png

 

刚才是说人和人对话时,语境很关键,现在我们做人机对话系统,人和设备对话的时候有哪些语境呢?

1、物理语境。也就是你说话当时现场的信息,包括(1)时间、地点、场所,这个场所是指在车里或在家里等等。(2)天气。(3)情绪和情感。(4)设备上面显示的内容。(5)设备能感知到的信息,比如我们和空调对话,空调能够感知到室内外的温度、湿度。这个语境的生命周期是请求级的。

2、言语语境。(1)上下文,设备上和设备上面反馈的信息也是一种上下文,这个生命周期可以看成是会话级的。

3、知识语境。包括:

(1)人类的常识和领域知识。举个简单的例子,以前我们一句话叫「中国乒乓球队谁也赢不了」,还有「中国足球队也是谁也赢不了」,这两句话看起来字面是一样的,但人能够理解这两句话的差别,因为我们有常识是:中国足球队很弱,中国乒乓球队很强。所以知识对这句话的理解至关重要。

(2)用户画像,包括用户的一些基本信息,用户的性别、年龄、文化水平、爱好等等。(3)Agent 画像,就是这个机器人定义的信息,像小冰把它的 Agent 画像定义为一个 18 岁的邻家小妹。(4)设备信息库,如果把音箱作为中控的话,中控连接的设备信息、设备状态等都是语境。如果在家里对中控说「我回家了」这句话到底是什么含义?中控可能会根据你的设备状态、根据当前的环境情况,给你决定是开灯还是关灯,是给你开窗户还是拉窗帘等等。

2. 不要神话知识图谱

知识图谱的历史和概念大家已经比较理解了,我这里主要强调几个基本概念:最重要的知识图谱概念就是「Things,Not Strings」,知识图谱里面的东西都是一个个实体而不是字符串。

另外,我们也不要神化知识图谱,它其实只是一种知识的组织形式而已。因为不管做什么应用,在各种场景下都有知识,以前可能用其他方式来表示这个知识。在概念层,我们以前也接触过类似的东西,就像我们做关于数据库建模时用 ER 模型,它也是一种概念模型。我们写程序,做面向对象设计时会画些类图,这些都是概念模型,这些模型都可以很方便的转成知识图谱来表示。我认为知识图谱首先是知识的一种组织形式。在数据层,知识图谱是一种图模型,它是用节点、边来表达实体、值、关系和属性等。

3. 什么是知性会话?

什么叫知性会话?我这里举个例子,用户可能跟音箱聊天:「你喜欢谢霆锋?」「喜欢,他很酷」「你知道他女朋友是谁吗?」「王菲」「来一首她的《传奇》」这是一种操控,机器就会给你播放王菲的传奇,播放之后系统还可以接着问说「你还想听李键的原唱吗?」这是一种主动对话,用户说「好的」,系统可以播放李键的《传奇》,用户还可以问「他的音乐风格是什么样的?」系统说「李健的风格,有民谣的简洁,但比民谣华丽得多。」

你看这个例子的话,它涉及很多跟音乐相关的知识,还包括一些歌星的人物相关的知识。交互形式有聊天、问答、操控、主动对话,是通过知识把它们关联在一起,你会感觉整个对话是个很流畅的对话。

总结下来,知性会话的意思是:它以知识图谱为中心,通过实体发现与链接技术把各种各样可以用来作为对话的数据源融合在一起,实现跨领域、跨交互形式的多轮对话。

640?wx_fmt=png

 

知性会话的主要特点有: 一是跨领域,跨交互形式共享上下文,你可以看它的聊天和问答可以衔接在以前; 二是它体现了领域专家的机器人定位,它对这些领域的知识非常了解,可以在聊天或者问答中体现出它掌握的领域知识。它有这方面的知识后,也可以主动发起一些对话。

知性会话的核心技术有:

  • 离线处理,首先要有知识图谱,所以有一个知识图谱构建的问题。另外,我们要把各种跟对话相关的数据通过实体发现与链接技术跟知识图谱关联起来。

  • 在线处理。基于知识做话语理解,怎么在聊天里把知识融合进去,还有基于知识图谱的问答,基于知识图谱的主动对话等。

 

▌三、知性会话关键技术

 

(一)知识图谱构建

1. 知识图谱的构建方法

 

640?wx_fmt=png

 这里我引用复旦肖老师总结的知识图谱构建方法,第一步是做模式设计,我们要定义有哪些类或概念、哪些属性或关系。

第二步确定我们的知识从哪来,所谓的数据来源,这里可以通过对一些结构化的数据、非结构化的数据做转换、对非结构化的数据,即文本,从里面去信息抽取。

第三步,知识图谱里最重要的是词汇的挖掘,各种同义词、缩略词、短语等等。

第四步,有词汇不够,我们要把同义词聚集为一个概念,也就是所谓的实体发现,包括实体实现、实体归类、实体链接等等。

第五步,除了实体之外,知识图谱里还有边,也就是关系,我们要做关系的抽取。

第六步,因为我们的知识图谱可能来源于不同的数据源,所以我们要做知识的融合,主要是实体对齐、属性融合、值的规范化。

最后,对知识图谱的质量做检查控制,包括知识的补全,有错的话要纠错,还有知识更新,最后形成一个领域的知识图谱。

2. 知识图谱的评估方法

如果你不知道怎么评价知识图谱的话,就根本不知道你的知识图谱建得好还是坏、有用还是没用。评估的方法基本可以分为四大类别:最重要的类别是第二类基于应用,把知识图谱在应用里看效果怎样,通过应用效果来间接评估知识本体。我们不要先找几十个人花一两年建知识图谱然后再去找应用,而是知识图谱必须是应用驱动的,根据应用效果来评价知识图谱,这是推荐的一个方法。

还有基于黄金标准评估,也就是说如果我们有些好的知识图谱,或者我们可以建一个小的知识图谱,根据这个标准知识图谱去评估我们建的知识图谱的情况。我们可以看看计算概念和关系的覆盖率,即有多少出现在标准知识图谱中的概念和关系被包含了,这可以评价我们的建的知识图谱是否完整。

另外,简单的评估方式基于指标。可以定一些统计指标,比如这个知识图谱里有多少概念、多少关系、关系属性,然后我们还可以对它进行抽查,看它的准确率、一致性等指标。

3. 敏捷构建

640?wx_fmt=png

 

我们现在做应用很多情况都是做敏捷开发,也就是说可能半个月或者一个月就会发一次版本,这时候我们知识图谱也要跟着应用快速迭代,这时候是需要对知识图谱敏捷构建的过程。这里强调我们要对知识图谱做自动化的测试,测试完之后要判断它是否能够发版,发版之后要继续分析它目前的问题。可以把知识图谱看成一个软件,它是不是有哪些 bug 或者需要哪些新功能,根据这些制定下一个版本的发版计划。核心想法就是把知识图谱也看成是一个软件,也要有版本管理,也要有敏捷的开发。

(二)实体发现与链接

需要解决的问题:如果我们这时候已经有知识图谱了,现在还依赖于实体发现与链接技术。这个技术解决刚才那个问题,「Thinks,not Strings」,它最重要的问题是把字符串和知识图谱的实体关联起来。它要解决两个问题,一个是我们同一个意义可能有表达不同的形式,像「科比」、「黑曼巴」、「科神」很多是指的科比这个人。还有一个是自然语言或者字符串本身有歧义性,就像「苹果」可能是指苹果电脑、苹果手机,也可能是一个水果。

解决方法:所以它的做法是分两步,实体发现和实体链接,实体发现是发现文本中的 mention,就是字符串,像「这个苹果很贵」的「苹果」是 mention。实体链接是把这个 Mention 和知识图谱里的实体关联起来,知识图谱里的实体关于「苹果」可能有多个实体,有苹果公司,还有苹果这个品牌,还可能是苹果手机、苹果电脑,还有水果叫苹果等等,这里的「苹果」到底指哪个呢?可能要靠上下文的判断。

1. 基于实体的多源数据融合

 

640?wx_fmt=png

我这里举个很简单的知识图谱,谢霆锋的女友是王菲,王菲唱了《传奇》这首歌,《传奇》这首歌的原唱是李健。

我们在对话这块的数据来源有几个:一个是聊天库,像「你喜欢歌手谢霆锋吗」「喜欢,他很酷。」,还有 FAQ 库,我们可能从百度知道或者很多地方可以找到社区问答的数据,就像这里说「谁能说说李健的音乐风格?」「李健的风格,有民谣的简洁,但比民谣华丽得多。」

我们也会从网上找到很多文档,包括百科的文档或者网页性的文档,我们对这些文档、聊天库、FAQ 库、文档库,我们都要去做实体链接,把这里面出现的歌手和我们知识图谱的歌手关联起来。

2. 如何进行实体发现与链接?

 

640?wx_fmt=png

 

第一步预处理,首先建立一个 mention 到 entity(实体)的关系,这也是目前这个算法的局限性,我们事先要知道一个 mention 可能对应到哪些实体。然后抽取实体相关特征:

一是实体的先验概率。就像苹果可能是水果的先验概率为 40%,是苹果手机的先验概率为 60%,如果我们说葡萄呢?可能葡萄是水果的先验概率有 90%,10% 是其他东西。二是实体上下文的词分布,我们看这些实体周边到底是什么词,或者它篇章的主题词,就像苹果手机出现在文章里都是科技类的主题词。三是实体之间的语义关联度,因为知识图谱是一个图的结构,所以每个实体环绕它周边都有些其他的实体,这些实体都是相关的特征。

第二步,这时实体链接就变成一个排序问题,找到 mention 之后,我们可以根据前面 mention 关系表找到它的候选实体,现在保持只需要对候选实体排序,返回一个最可能的实体。

第三步,对候选实体进行排序,可以用最基本的方法。这个有两大类:一个是实体本身的信息,还有一个是可以利用实体和实体之间的协同关系做排序。如果是苹果旁边的实体都是偏电脑类的,那这个苹果可能就指苹果电脑。

(三)融合知识的话语理解

做完实体链接处理以后可以做真正的对话系统这一块,对话系统里最基本的是对用户话语的理解,我们怎么去理解用户说的一句话。

 

640?wx_fmt=png

第一步要做实体的发现与链接,像刚才那个例子,「你喜欢谢霆锋吗」,我们要把谢霆锋跟知识图谱的实体关联起来。

第二步做指代发现,比如「你知道他女朋友是谁」,那这个「他」到底是指谁,我们首先要发现他是一个指代词,然后再根据上下文去判断「他」在这个例子里面是谢霆锋这个实体。

另外,我们做语义理解还有一种情况是结合知识做消歧义。比如用户说「周巧文的生日」,因为《生日》是一首歌的名字,周巧文是这个歌的歌手,这时候我们理解它是个音乐,因为本来就在音箱下面,这时我们可以直接播放周巧文的《生日》这首歌。但是如果系统又问一下「刘德华的生日」,这时候虽然我们的命名实体识别很有可能把「生日」也可能打成歌名的标签,刘德华打成歌手的标签,歌手的歌名,很容易以为是播放音乐,但是我们通过知识的验证知道刘德华并没有唱过这首歌,这时候要转成问答,这不是一个操控性的指令。直接返回他的生日,说「刘德华的生日是 1961 年 9 月 27 日」。

这几个例子是我们通过知识帮助去理解用户的指令。我下面再讲一下怎么把知识和聊天结合起来。

(四)融合知识的聊天

1. 上下文

现在学术界都用深度学习模型,所以我会简单讲一下深度学习的方法,把它的基本思想讲一下。我们现在一般在学界把聊天变成一个 Sequence-to-Sequence 的模型,就是有一个 encoder对输入进行编码为向量, 通过 decoder 把应答生成出来。这时核心问题变成怎么把上下文加进去,最基本的方法是把上下文的文本跟当前文本的向量合在一起作为 encoder 的输入;另外我们可以把上下文作为向量,在 decoder 阶段输入;或者用主题模型对这个 session 去建模,把这个 session 主题模型也作为 decoder 的输入,这样就可以实现一并上下文的效果。

2. 一致性

聊天还有一个很重要的问题是一致性。我们刚才说语境里面有一个agent画像,跟我聊天的对象虽然是机器人,但是它有统一的人格,它的性别、年龄、籍贯、爱好应该是一致的,这是目前聊天机器人里面最难的一点。你对机器人问它「多大了?」它可能说「18 岁」,如果你再去问一下「你今年高寿」,它很有可能回答「我今年 88 岁」,或者问你「芳龄几许」,它很有可能回答「小女子今年芳龄二八等等」。

640?wx_fmt=png

 

为什么会出现这种情况?因为目前聊天的机器人都是靠从各个来源去收集各种语料堆在一起的,对这种语料并没有做归一化处理,因为有的语料说「我今年 88 岁」,有的语料里面可能说「我今年 18 岁」等等,这时候换个方式问它可能会出现问答不一致的地方。更复杂的例子,你问它「你出生地在哪里?」它说「我在北京」,然后问它「你是中国人吗?」它可能就回答不了,虽然人类常识知道北京属于中国等等。

在深度学习里如果想把这些所谓的机器人的信息,进行建模或向量化处理导入到 decoder 模型里去,这时候它会优先从身份信息的词向量去生成应答,这样也能达到一定一致性的效果。

3. 融合知识

 

640?wx_fmt=png

 

另外,做问答的时候,像我们这个例子问「姚明有多高」,我们生成比较自然的问答,说「他是两米二六,他是唯一一个可以从太空看到的人类。」当然,这是开玩笑的。这种聊天就融合了知识,它知道姚明的身高。这时候通过深度学习模型做 decode 时,除了生成常规的应答之外,有部分的应答还要从知识库里去检索,然后再把这个应答跟文本的应答拼在一起。

640?wx_fmt=png

更多的类似工作可以看看获得今年IJCAI杰出论文奖的黄民烈老师的工作。

(四)基于知识的问答

知识问答主要有两种方法:一种是基于 Semantic Parsing 的传统方法,它是把一个问题解析成一个形式化的查询语言,再把查询语言知识库里面做查询。这个方法的最大难点是把自然语言的问题转成这样一个形式化的查询语言。同样也有很多方法,最简单的基于规则、基于模板,复杂点的基于翻译模型、基于深度学习模型等。

640?wx_fmt=png

 

目前学术界比较多的是基于机器学习的知识库的问答方法,这里面它的基本思想是把问题建模成一个 embedding,然后对知识图谱也做 embedding,变成一个个向量,这个问答就转换成了一个相似度匹配的问题,把知识库里的子图的向量跟问题对应子图进行相似度匹配。

640?wx_fmt=png

 

还有很多其他方法,目前比较多的是基于网络的方法,基于带注意力机制的循环神经网络的方法。这块我给一个参考,大家可以看一下《揭开知识库问答 KB-QA 的面纱》这篇文章,讲得非常详尽、非常好。我个人的观点是现在基于深度学习的知识库问答目前在工业界这块不是很成熟,它的效果不太可控,我们在系统里还是用基于传统的 Semantic Parsing 问答。

在 CQA 上也有很多把知识结合进去的方法。CQA 最核心的问题是我们要算用户的问题和在我们问答库里问题的语义相似度,这里的核心问题是怎么能把知识放到对句子的向量表示里。最近的 SIGIR2018 中提到,把知识和注意力的神经网络结合在一起的方法。现在这种论文基本都是一个网络图。另外一篇文章也是类似的,总体是在文本做排序时把知识向量化。

(五)基于知识的主动会话

这个实际上是非常关键的。在我们人机对话系统,特别是在 VUI 交互下, VUI 音箱是没有界面的,这就意味着你无法知道这个音箱到底支持哪些功能。当你面对音箱的时候,你怎么知道它的功能,到底哪些话能说,哪些话不能说,或者它有什么东西?这时候很需要机器人主动的对话,能引导用户用它,知道它的功能。

640?wx_fmt=png

再举个例子,如果一个用户说「来首《传奇》」,机器可以主动问他说「播放以后还想听听李健原唱吗?」其实它的思想很简单,就是根据我们的知识图谱里面,看看相同实体下面有没有其他关系或者属性,或者推荐一个相同关系下面其他的实体。

640?wx_fmt=png

 

这里一篇百度的文章思想也是类似的,如果觉得聊天聊不下去了,会先在上下文里去做实体分析和实体链接,找到作为聊天主题的实体之后再根据知识图谱找相关的实体,根据相关的实体产生话题。

 

▌四、总结

 

前面把聊天、问答、对话、语义解析怎么跟知识结合起来做了简单的介绍。接下来做个总结:

第一,为什么人机对话系统很重要?

640?wx_fmt=png

1、它有可能成为物联网时代的最主要交互形式,类似于 OS。

2、知性会话的核心是知识图谱。它最重要的是做两件事情:一是线下要做基于知识图谱做多源数据的融合,二是在服务时要做基于知识图谱聊天、问答、对话、操控一体化。

3、从技术上来讲,深度学习和知识图谱技术的结合是目前最重要的一个趋势。我个人比较看好 Sequence-to-Sequence 模型,因为它的表达能力非常丰富,而且应用场景非常多,基本上自然语言处理里面大部分的问题都可以建模成一个 Sequence-to-Sequence。包括我们的翻译是一个语言到另外一个语言,还有聊天问答甚至拼音输入法,就是把拼音序列转成文字序列等等,还有做分词、词性识别、命名实体识别等等都是 Sequence-to-Sequence,这种模型分为 encoder 和 decoder 两个阶段,它在不同的阶段都可以把一些知识融合进去。

第二,在人机对话系统里的技术演进是怎样的?

 

640?wx_fmt=png

 

1、在对话里不能只看语义,还要看语用,语用就是「语义+语境」。

2、我们不能只做闲聊式的机器人,而且是希望我们机器人是掌握领域知识,它是有文化的,而且文化水平还很高,是个领域专家,是知性会话。

3、流式对话。我们目前跟音箱的交互都是先唤醒,说「小爱同学,给我点首歌」,又说「小爱同学,播放下一首」。非常麻烦,但人和人对话是不会总频繁叫人的名字的,这时候就需要流式对话,这块的技术难点是怎么判断一个人说话是不是说完了,你是否可以打断,这是目前技术上最难的一点。还有一个是怎么去拒绝噪音,因为现在对话是没有唤醒词的,这时候旁边人的说话甚至电视里面说的话很有可能被误识别,机器也会对它做响应。

 

▌五、答听众问

 

Q:我们公司在构建电商的知识图谱,但是电商的数据是每天都会更新的,有什么好的办法对知识图谱进行更新吗?而且基于 neo4j 的图谱如何做知识推理?

A:这是个好问题。我们刚才强调知识图谱要敏捷构建,敏捷构建就意味着你可以频繁的发版本,这时候就有版本合并的问题,其实也是更新的问题。更新这块主要的技术是知识本体的融合或者知识实体的匹配、实体的对齐。如果更新的数据量不是很大的话,我建议的方法是先通过实体对齐的技术,把更新的数据自动添加到知识图谱里去,如果量不大的话还需要做人工的 review,看更新的数据是否 OK。这个我认为也没有什么特别好的办法,因为更新本来就是知识图谱里最难的问题。

neo4j 的图谱如何做知识推理?首先,我个人认为它不太适合存储海量的知识图谱,电商的数量应该很大的,这时候用 neo4j 合适不合适还有待商榷。如何做知识推理?我们一般认为知识图谱最主要的是知识,尽量少去做推理,因为推理是挺难的一个东西,而且也没有特别工业化成熟度很高的工具。第二,如果非要做推理的话,我们一般做线下的推理,就是预先把推理做好,把它能展开的数据全展开,也叫「知识补全」,就像简单的传递性的关系或者预先把它都展开,相当于存储空间换时间,这是一个比较常用的方法。我们现在不太建议线上服务时做实时推理,因为那个性能一般很难达到要求。

Q:本体构建的大致方法能简单介绍一下吗?

A:本体构建的方法从大的面来讲有两种,一种是传统基于专家的方法,就是请一般专家全手工构建,他们对每个词、每个实体、词之间的关系都开会讨论,最后决定应该这样、应该那样,这是专家驱动的方法。但这种方法已经不太可行,而且这种方法也会成为我们做知识图谱的瓶颈,因为我们期望知识图谱是一个敏捷构建的。

目前大部分是数据驱动的方法,就是我们通过数据挖掘去自动构建知识图谱,适当地基于人工的 review。我倾向于极端的方法,我推荐的方式是知识图谱的构建整个是全自动,但是也需要专家的参与,但是专家参与不是做 review、不是做构建,而是做评测。整个知识图谱的效果根据应用的效果说话,这个应用不能假设整个知识图谱是完全正确的、完整的的。我们可以通过快速迭代,不断的对知识图谱去做更新,然后根据自动化的测试或者根据人工的抽样检查和应用的效果去看知识图谱的质量。只要我们知识图谱的质量能够满足应用的需求就 OK。

Q:实体抽取有一个大致的最佳实践吗?

A:最佳实践是这样的,如果从工业界角度看的话,实体抽取肯定是多个方法的融合,基于词典、基于规则、基于统计学习方法、基于深度学习方法,没有一个方法就能搞定所有的问题。虽然词典挖掘这个东西没有技术含量,但是实践中基于词典的方法是非常有效的方法,特别是在垂直领域里面,像医疗这种领域,当然,在有些领域可能这个方法不靠谱,比如在音乐领域,音乐里面有歌名,任何一个词都可能是歌名。

但基于词典方法还有一个重要考虑,一定要考虑这个词典的这个词有没有歧义,或者一个词的先验概率。比如「我爱你」也是一首歌名,但是它是歌名的概率可能不是特别大,但「忘情水」是歌名的概率就很大,所以词典不是简单的词条列表,而是要带先验概率的信息。

Q:知识图谱还需要语义网的知识吗?构建 OWL 可还需要很强的领域知识?

A:我们刚才说到知识图谱的前身是语义网,所以如果想更加深刻理解知识图谱,还是要了解一下语义网的知识,特别像 RDF OWL 的规范是要了解一下的。

OWL 的这个本体语言还是有点偏复杂,目前基本上不太推荐知识图谱搞得那么复杂,基本对应到 RDF 那种形态就差不多了。我们希望知识图谱可以构建尽量大,但是它从逻辑上来讲尽量简单,不要用 OWL 里面复杂的东西。一点点语义可以走得很远,没必要把模型搞得太复杂,因为把模型搞得太复杂的一个最重要难点是当你把实体放进去时你很难判断这个实体属于哪个概念。

Q:心理学出身的研究者在 NLP 学术领域是否有竞争力?对于心理学研究者转向 NLP 学术圈有哪些建议?

A:这个问题挺有意思的。我们组里有一个主力骨干就是学心理学出身的,但他当时学的心理学是偏统计方面的心理学,也就是计量心理学这方面的,所以他相对有一定的统计基础。这时候由统计基础转向到 NLP,因为有数学基础,是比较容易一点的。另外一点,心理学比较有意义的是认知这一块,因为神经网络这些原理跟认知心理学有一定的关系,所以心理学知识对转到 NLP 挺有帮助的。

关于具体的建议,不管哪个专业转到 NLP,最重要的是学好数学和机器学习最基础的东西,这个基础打好了,转向 NLP 就比较简单了。

Q:基于知识的方法和统计类的方法需要共融互补,老师有没有典型的合作思路,充分利用基于知识规则方法的稳定可控的同时,又能利用统计从有监督的大数据自动抽取模式?是否可以讲讲两者一起 NLP 的经验?

A:现在人工智能主要是三大学派——知识图谱派、统计学习派、深度学习派,从工业界角度来看,在解决具体问题时各有所长,所以需要把这三者融合在一起,真实的线上系统不会只有一个方法。所以知识方法是一个很重要的方法,而且它跟深度学习是有比较好的互补性,特别是可以提供深度学习方法里面没有的可解释性这一块。

具体怎么融合,最简单的融合方法就是做模型Ensemble,把几个分类器组装在一起,这个可以看周志华老师那本「西瓜书」,因为周老师做模型的 Ensemble是最拿手的。

此外,把知识或规则都可以作为特征,从这个角度融合在一起。另外,深度学习里的解码器也可以把知识融合进来,所以这块的方法是很多的。


2018 AI开发者大会

只讲技术,拒绝空谈

2018 AI开发者大会是一场由中美人工智能技术高手联袂打造的AI技术与产业的年度盛会!是一场以技术落地为导向的干货会议!大会设置了10场技术专题论坛,力邀15+硅谷实力讲师团和80+AI领军企业技术核心人物,多位一线经验大咖带你将AI从云端落地。

大会日程以及嘉宾议题请查看下方海报(点击查看大图)

 

迄今最全人脸识别开源 - qq_34654240的博客 - CSDN博客

$
0
0

人脸识别是目前深度学习领域应用最为广泛的领域之一,各大框架都有不错的开源项目,可以在短时间内实现刷榜。

首推 Demystifying Face Recognition,由浅入深实验了很多方法

人脸识别算法演化史

谷歌人脸识别系统FaceNet解析

模型评估

人脸识别系列

从0开始,一起玩人脸识别

深度挖坑系列

如何走近深度学习人脸识别: https://github.com/Joker316701882/Additive-Margin-Softmax

caffe

https://github.com/wy1iu/sphereface:lfw 99.30% with A-softmax loss 中文理解

https://github.com/happynear/NormFace:99.21%

https://github.com/ydwen/caffe-face:~99% centerloss,ECCV2016

https://github.com/AlfredXiangWu/face_verification_experiment98.80% with CASIA,Light-CNN

mxnet

https://github.com/deepinsight/insightface:lfw 99.83%

https://github.com/moli232777144/mobilefacenet-mxnet:轻量级版本99.5%

https://github.com/qidiso/mobilefacenet-V2:99.66%

tensorflow

https://github.com/davidsandberg/facenet:lfw 99.65%

多GPU版本: https://github.com/wangruichens/facenet_multigpu

https://github.com/auroua/InsightFace_TF:99.68%

https://github.com/xsr-ai/MobileFaceNet_TF

人脸对齐

https://github.com/CamlinZ/face_alignment一种人脸68特征点检测的深度学习方法

https://github.com/zeusees/HyperLandmark106点标注,含android端

https://github.com/tensor-yu/cascaded_mobilenet-v2: 级联MobileNet-V2进行人脸关键点(5点)检测,单模型仅 956 KB,GTX1080上运行为6ms左右

https://github.com/goodluckcwl/Face-alignment-mobilenet-v2

Loss Function

人脸识别的LOSS

https://github.com/KaleidoZhouYN/Loss-Functions

https://github.com/KaleidoZhouYN/Sphereface-Ms-celeb-1M:讨论对齐的影响

商业实践

InsightFace - 使用篇, 如何一键刷分LFW 99.80%, MegaFace 98%

从理论到实践, 用insightface构建人证识别系统

人脸识别最新进展以及工业级大规模人脸识别实践探讨

如何进行上亿类的人脸识别

facenet 代码阅读笔记:如何训练基于triplet-loss的模型

https://github.com/seetaface/SeetaFaceEngine:山世光老师的开源库,不过有点过时了

A-Softmax的总结及与L-Softmax的对比——SphereFace

A Discriminative Feature Learning Approach for Deep Face Recognition 原理及在caffe实验复现

android

https://github.com/GRAYKEY/mobilefacenet_android

https://github.com/zhanglaplace/MobileFaceNetAmsoftmax实现

https://github.com/KaleidoZhouYN/mobilefacenet-caffe

https://github.com/moli232777144/small_model_face_recognition:Light CNN for ncnn

https://github.com/mohanson/FaceDetectionServer:go服务器人脸识别服务

https://github.com/yanmeizhao/Sara/tree/master/sample_mobile_track_106:商汤106点人脸跟踪

数据集

CASIA-WebFace:

对齐后版本(112*112)

MS1M

对齐后版本(112*112)

VGGFace2

对齐后版本(112*112)

格灵深瞳数据集: All, Asia

IMDb-Face

--------------------- 本文来自 迷若烟雨 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/minstyrain/article/details/82292278?utm_source=copy

使用ElasticSearch完成百万级数据查询附近的人功能 - tianyaleixiaowu的专栏 - CSDN博客

$
0
0
我们来看一下使用ElasticSearch完成大数据量查询附近的人功能,搜索N米范围的内的数据。

准备环境
本机测试使用了ElasticSearch最新版5.5.1,SpringBoot1.5.4,spring-data-ElasticSearch2.1.4.
新建Springboot项目,勾选ElasticSearch和web。
pom文件如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
 
<groupId>com.tianyalei</groupId>
<artifactId>elasticsearch</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
 
<name>elasticsearch</name>
<description>Demo project for Spring Boot</description>
 
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
 
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
 
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
 
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.sun.jna</groupId>
<artifactId>jna</artifactId>
<version>3.0.9</version>
</dependency>
</dependencies>
 
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
 
 
</project>
新建model类Person
package com.tianyalei.elasticsearch.model;
 
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.GeoPointField;
 
import java.io.Serializable;
 
/**
 * model类
 */
@Document(indexName="elastic_search_project",type="person",indexStoreType="fs",shards=5,replicas=1,refreshInterval="-1")
public class Person implements Serializable {
    @Id
    private int id;
 
    private String name;
 
    private String phone;
 
    /**
     * 地理位置经纬度
     * lat纬度,lon经度 "40.715,-74.011"
     * 如果用数组则相反[-73.983, 40.719]
     */
    @GeoPointField
    private String address;
 
    public int getId() {
        return id;
    }
 
    public void setId(int id) {
        this.id = id;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public String getPhone() {
        return phone;
    }
 
    public void setPhone(String phone) {
        this.phone = phone;
    }
 
    public String getAddress() {
        return address;
    }
 
    public void setAddress(String address) {
        this.address = address;
    }
}
我用address字段表示经纬度位置。注意,使用String[]和String分别来表示经纬度时是不同的,见注释。
import com.tianyalei.elasticsearch.model.Person;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
 
public interface PersonRepository extends ElasticsearchRepository<Person, Integer> {
 
}
看一下Service类,完成插入测试数据的功能,查询的功能我放在Controller里了,为了方便查看,正常是应该放在Service里
package com.tianyalei.elasticsearch.service;
 
import com.tianyalei.elasticsearch.model.Person;
import com.tianyalei.elasticsearch.repository.PersonRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.stereotype.Service;
 
import java.util.ArrayList;
import java.util.List;
 
@Service
public class PersonService {
    @Autowired
    PersonRepository personRepository;
    @Autowired
    ElasticsearchTemplate elasticsearchTemplate;
 
    private static final String PERSON_INDEX_NAME = "elastic_search_project";
    private static final String PERSON_INDEX_TYPE = "person";
 
    public Person add(Person person) {
        return personRepository.save(person);
    }
 
    public void bulkIndex(List<Person> personList) {
        int counter = 0;
        try {
            if (!elasticsearchTemplate.indexExists(PERSON_INDEX_NAME)) {
                elasticsearchTemplate.createIndex(PERSON_INDEX_TYPE);
            }
            List<IndexQuery> queries = new ArrayList<>();
            for (Person person : personList) {
                IndexQuery indexQuery = new IndexQuery();
                indexQuery.setId(person.getId() + "");
                indexQuery.setObject(person);
                indexQuery.setIndexName(PERSON_INDEX_NAME);
                indexQuery.setType(PERSON_INDEX_TYPE);
 
                //上面的那几步也可以使用IndexQueryBuilder来构建
                //IndexQuery index = new IndexQueryBuilder().withId(person.getId() + "").withObject(person).build();
 
                queries.add(indexQuery);
                if (counter % 500 == 0) {
                    elasticsearchTemplate.bulkIndex(queries);
                    queries.clear();
                    System.out.println("bulkIndex counter : " + counter);
                }
                counter++;
            }
            if (queries.size() > 0) {
                elasticsearchTemplate.bulkIndex(queries);
            }
            System.out.println("bulkIndex completed.");
        } catch (Exception e) {
            System.out.println("IndexerService.bulkIndex e;" + e.getMessage());
            throw e;
        }
    }
}
注意看bulkIndex方法,这个是批量插入数据用的,bulk也是ES官方推荐使用的批量插入数据的方法。这里是每逢500的整数倍就bulk插入一次。

package com.tianyalei.elasticsearch.controller;
 
import com.tianyalei.elasticsearch.model.Person;
import com.tianyalei.elasticsearch.service.PersonService;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.index.query.GeoDistanceQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.sort.GeoDistanceSortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
 
@RestController
public class PersonController {
    @Autowired
    PersonService personService;
    @Autowired
    ElasticsearchTemplate elasticsearchTemplate;
 
    @GetMapping("/add")
    public Object add() {
        double lat = 39.929986;
        double lon = 116.395645;
 
        List<Person> personList = new ArrayList<>(900000);
        for (int i = 100000; i < 1000000; i++) {
            double max = 0.00001;
            double min = 0.000001;
            Random random = new Random();
            double s = random.nextDouble() % (max - min + 1) + max;
            DecimalFormat df = new DecimalFormat("######0.000000");
            // System.out.println(s);
            String lons = df.format(s + lon);
            String lats = df.format(s + lat);
            Double dlon = Double.valueOf(lons);
            Double dlat = Double.valueOf(lats);
 
            Person person = new Person();
            person.setId(i);
            person.setName("名字" + i);
            person.setPhone("电话" + i);
            person.setAddress(dlat + "," + dlon);
 
            personList.add(person);
        }
        personService.bulkIndex(personList);
 
//        SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(QueryBuilders.queryStringQuery("spring boot OR 书籍")).build();
//        List<Article> articles = elas、ticsearchTemplate.queryForList(se、archQuery, Article.class);
//        for (Article article : articles) {
//            System.out.println(article.toString());
//        }
 
        return "添加数据";
    }
 
    /**
     *
     geo_distance: 查找距离某个中心点距离在一定范围内的位置
     geo_bounding_box: 查找某个长方形区域内的位置
     geo_distance_range: 查找距离某个中心的距离在min和max之间的位置
     geo_polygon: 查找位于多边形内的地点。
     sort可以用来排序
     */
    @GetMapping("/query")
    public Object query() {
        double lat = 39.929986;
        double lon = 116.395645;
 
        Long nowTime = System.currentTimeMillis();
        //查询某经纬度100米范围内
        GeoDistanceQueryBuilder builder = QueryBuilders.geoDistanceQuery("address").point(lat, lon)
                .distance(100, DistanceUnit.METERS);
 
        GeoDistanceSortBuilder sortBuilder = SortBuilders.geoDistanceSort("address")
                .point(lat, lon)
                .unit(DistanceUnit.METERS)
                .order(SortOrder.ASC);
 
        Pageable pageable = new PageRequest(0, 50);
 
        NativeSearchQueryBuilder builder1 = new NativeSearchQueryBuilder().withFilter(builder).withSort(sortBuilder).withPageable(pageable);
        SearchQuery searchQuery = builder1.build();
 
        //queryForList默认是分页,走的是queryForPage,默认10个
        List<Person> personList = elasticsearchTemplate.queryForList(searchQuery, Person.class);
 
        System.out.println("耗时:" + (System.currentTimeMillis() - nowTime));
        return personList;
    }
}
看Controller类,在add方法中,我们插入90万条测试数据,随机产生不同的经纬度地址。
在查询方法中,我们构建了一个查询100米范围内、按照距离远近排序,分页每页50条的查询条件。如果不指明Pageable的话,ESTemplate的queryForList默认是10条,通过源码可以看到。
启动项目,先执行add,等待百万数据插入,大概几十秒。
然后执行查询,看一下结果。

第一次查询花费300多ms,再次查询后时间就大幅下降,到30ms左右,因为ES已经自动缓存到内存了。
可见,ES完成地理位置的查询还是非常快的。适用于查询附近的人、范围查询之类的功能。

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
后记,在后来的使用中,Elasticsearch2.3版本时,按上面的写法出现了geo类型无法索引的情况,进入es的为String,而不是标注的geofiled。在此记录一下解决方法,将String类型修改为GeoPoint,且是org.springframework.data.elasticsearch.core.geo.GeoPoint包下的。然后需要在创建index时,显式调用一下mapping方法,才能正确的映射为geofield。
如下
if (!elasticsearchTemplate.indexExists("abc")) {
elasticsearchTemplate.createIndex("abc");
elasticsearchTemplate.putMapping(Person.class);
}

--------------------- 

ElasticSearch位置搜索 - Spring , Hadoop, Spark , BI , ML - CSDN博客

$
0
0

在ElasticSearch中,地理位置通过 geo_point这个数据类型来支持。地理位置的数据需要提供经纬度信息,当经纬度不合法时,ES会拒绝新增文档。这种类型的数据支持距离计算,范围查询等。在底层,索引使用 Geohash实现。

1、创建索引

PUT创建一个索引 cn_large_cities, mapping为city:

{"mappings":{"city":{"properties":{"city":{"type":"string"},"state":{"type":"string"},"location":{"type":"geo_point"}}}}}

geo_point类型必须显示指定,ES无法从数据中推断。在ES中,位置数据可以通过对象,字符串,数组三种形式表示,分别如下:

#"lat,lon""location":"40.715,-74.011""location": {"lat":40.715,"lon":-74.011}

# [lon ,lat]"location":[-74.011,40.715]

POST下面4条测试数据:

{"city":"Beijing", "state":"BJ","location":{"lat":"39.91667", "lon":"116.41667"}}

{"city":"Shanghai", "state":"SH","location":{"lat":"34.50000", "lon":"121.43333"}}

{"city":"Xiamen", "state":"FJ","location":{"lat":"24.46667", "lon":"118.10000"}}

{"city":"Fuzhou", "state":"FJ","location":{"lat":"26.08333", "lon":"119.30000"}}

{"city":"Guangzhou", "state":"GD","location":{"lat":"23.16667", "lon":"113.23333"}}

查看全部文档:

curl -XGET"http://localhost:9200/cn_large_cities/city/_search?pretty=true"

返回全部的5条数据,score均为1:

这里写图片描述

2、位置过滤

ES中有4中位置相关的过滤器,用于过滤位置信息:

  • geo_distance: 查找距离某个中心点距离在一定范围内的位置
  • geo_bounding_box: 查找某个长方形区域内的位置
  • geo_distance_range: 查找距离某个中心的距离在min和max之间的位置
  • geo_polygon: 查找位于多边形内的地点。

geo_distance

该类型过滤器查找的范围如下图:

下面是一个查询例子:

{"query":{"filtered":{"filter":{"geo_distance":"1km","location":{"lat":40.715,"lon":-73.988}}}}}

以下查询,查找距厦门500公里以内的城市:

{"query":{"filtered":{"filter":{"geo_distance" :{"distance" :"500km","location" :{"lat" :24.46667,"lon" :118.10000}}}}}}

geo_distance_range

{"query":{"filtered":{"filter":{"geo_distance_range":{"gte":"1km","lt":"2km","location":{"lat":40.715,"lon":-73.988}}}}}

geo_bounding_box

{"query":{"filtered":{"filter":{"geo_bounding_box":{"location":{"top_left":{"lat":40.8,"lon":-74.0},"bottom_right":{"lat":40.715,"lon":-73.0}}}}}}

3、按距离排序

接着我们按照距离厦门远近查找:

{"sort" :[
      {"_geo_distance" :{"location" :{"lat" :24.46667,"lon" :118.10000}, "order" :"asc","unit" :"km"}}
  ],"query":{"filtered" :{"query" :{"match_all" :{}}}}}

结果如下,依次是厦门、福州、广州…。符合我们的常识:

{"took":8,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":5,"max_score":null,"hits":[
      {"_index":"us_large_cities","_type":"city","_id":"AVaiSGXXjL0tfmRppc_p","_score":null,"_source":{"city":"Xiamen","state":"FJ","location":{"lat":"24.46667","lon":"118.10000"}},"sort":[0]},
      {"_index":"us_large_cities","_type":"city","_id":"AVaiSSuNjL0tfmRppc_r","_score":null,"_source":{"city":"Fuzhou","state":"FJ","location":{"lat":"26.08333","lon":"119.30000"}},"sort":[216.61105485607183]},
      {"_index":"us_large_cities","_type":"city","_id":"AVaiSd02jL0tfmRppc_s","_score":null,"_source":{"city":"Guangzhou","state":"GD","location":{"lat":"23.16667","lon":"113.23333"}},"sort":[515.9964950041397]},
      {"_index":"us_large_cities","_type":"city","_id":"AVaiR7_5jL0tfmRppc_o","_score":null,"_source":{"city":"Shanghai","state":"SH","location":{"lat":"34.50000","lon":"121.43333"}},"sort":[1161.512141925948]},
      {"_index":"us_large_cities","_type":"city","_id":"AVaiRwLUjL0tfmRppc_n","_score":null,"_source":{"city":"Beijing","state":"BJ","location":{"lat":"39.91667","lon":"116.41667"}},"sort":[1725.4543712286697]}
    ]}}

结果返回的sort字段是指公里数。加上限制条件,只返回最近的一个城市:

{"from":0,"size":1,"sort" :[
      {"_geo_distance" :{"location" :{"lat" :24.46667,"lon" :118.10000}, "order" :"asc","unit" :"km"}}
  ],"query":{"filtered" :{"query" :{"match_all" :{}}}}}

4、地理位置聚合

ES提供了3种位置聚合:

  • geo_distance: 根据到特定中心点的距离聚合
  • geohash_grid: 根据Geohash的单元格(cell)聚合
  • geo_bounds: 根据区域聚合

4.1 geo_distance聚合

下面这个查询根据距离厦门的距离来聚合,返回0-500,500-8000km的聚合:

{"query":{"filtered":{"filter":{"geo_distance" :{"distance" :"10000km","location" :{"lat" :24.46667,"lon" :118.10000}}}}},"aggs":{"per_ring":{"geo_distance":{"field":"location","unit":"km","origin":{"lat" :24.46667,"lon" :118.10000},"ranges":[
                    {"from":0, "to":500},
                    {"from":500, "to":8000}
                ]}}}}

返回的聚合结果如下;

"aggregations": {"per_ring":{"buckets":[
        {"key":"*-500.0","from":0,"from_as_string":"0.0","to":500,"to_as_string":"500.0","doc_count":2},
        {"key":"500.0-8000.0","from":500,"from_as_string":"500.0","to":8000,"to_as_string":"8000.0","doc_count":3}
      ]}}

可以看到,距离厦门0-500km的城市有2个,500-8000km的有3个。

4.2 geohash_grid聚合

该聚合方式根据geo_point数据对应的geohash值所在的cell进行聚合,cell的划分精度通过 precision属性来控制,精度是指cell划分的次数。

{"query":{"filtered":{"filter":{"geo_distance" :{"distance" :"10000km","location" :{"lat" :24.46667,"lon" :118.10000}}}}},"aggs":{"grid_agg":{"geohash_grid":{"field":"location","precision":2}}}}

聚合结果如下:

"aggregations": {"grid_agg":{"buckets":[
        {"key":"ws","doc_count":3},
        {"key":"wx","doc_count":1},
        {"key":"ww","doc_count":1}
      ]}}

可以看到,有3个城市的的geohash值为ws。将精度提高到5,聚合结果如下:

"aggregations": {"grid_agg":{"buckets":[
        {"key":"wx4g1","doc_count":1},
        {"key":"wwnk7","doc_count":1},
        {"key":"wssu6","doc_count":1},
        {"key":"ws7gp","doc_count":1},
        {"key":"ws0eb","doc_count":1}
      ]}}

4.3 geo_bounds聚合

这个聚合操作计算能够覆盖所有查询结果中geo_point的最小区域,返回的是覆盖所有位置的最小矩形:

{"query":{"filtered":{"filter":{"geo_distance" :{"distance" :"10000km","location" :{"lat" :24.46667,"lon" :118.10000}}}}},"aggs":{"map-zoom":{"geo_bounds":{"field":"location"}}}}

结果如下:

"aggregations": {"map-zoom":{"bounds":{"top_left":{"lat":39.91666993126273,"lon":113.2333298586309},"bottom_right":{"lat":23.16666992381215,"lon":121.43332997336984}}}}

也就是说,这两个点构成的矩形能够包含所有到厦门距离10000km的区域。我们把距离调整为500km,此时覆盖这些城市的矩形如下:

"aggregations": {"map-zoom":{"bounds":{"top_left":{"lat":26.083329990506172,"lon":118.0999999679625},"bottom_right":{"lat":24.46666999720037,"lon":119.29999999701977}}}}

5、参考资料

图解 MongoDB 地理位置索引的实现原理: http://blog.nosqlfan.com/html/1811.html
Geopoint数据类型: https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-point.html

智能视频分析系统_视频智能分析漫谈-博客-云栖社区-阿里云

$
0
0

视频智能分析发展历程
  从世纪九十年代开始,美国的VSAM项目中进行视频监控的数据化研究以来,视频监控的智能化经过了二十年的发展,到现在已经发生了巨大的变化,智能分析技术也从传统的模式识别到机器学习再到现在家喻户晓的深度学习经历了多个阶段的发展。而当前深度学习的逐渐成熟让很多曾经尚未到达实用阶段的应用逐渐具备了可用性,典型的如人脸识别,高密度的人员统计等;也让很多已经成熟的应用具有更高的准确度,如车牌、车型识别、行为识别等等。

  视频智能分析发展历程

国内的视频监控从2006前后开始“监控IT化”以来,视频监控的规模呈现了快速扩展的势头,这在模拟时代是无法想象的。视频监控规模的扩大,已经让使用者意识到单靠人眼去监控所有视频或者在事后去追查录像都是基本不可能的事情了。2009~2010年国内视频监控发展比较好的地区如浙江,就开始出现了如何更好的应用视频的需求声音。

长期从事智慧城市建设、自动化监控研究的博士生导师王汝琳就提出,当时的监控业界存在“看不清、调不出、提取难、检索慢”的问题,对应的也就出现了三大趋势:高清化、网络化和智能化。

从2010年前后起,智能分析已经在慢慢地应用到监控和智能交通的行业中来,其中最早形成规模应用的是车牌识别的功能,到2013年左右,电子警察在全国就在如火如荼地展开,三大监控巨头和在那个时候大力切入到了智能交通行业,加速了行业格局的形成。

这几年来,智能的应用快速扩展到了很多的方面,如人的行为识别、车辆的异常行为检测、仪表识别、人脸识别、车脸识别,还有通用监控视频的结构化分析等,花样各出,大有遍地开花之势。

智能分析的本质

视频分析的本质目标,就是视频的语义化,使视频更好地被检索到,被精确调阅到,解决人眼长期看视频产生的熟视无睹的问题,也解决发生案件时要花上百人进行录像查阅的问题。

软件智能分析与硬件智能分析优劣势分析

引用邓小平的话“不管白猫黑猫,抓得住老鼠就是好猫”,软件智能分析和硬件智能分析只是分工不同的猫,他们都有擅长的一面,也都有自己的劣势。用一句话来概括,他们的区别就是“软件智能贵在适应性和广度、硬件智能分析贵在分析的速度”。

软件智能分析

软件智能分析,是指用CPU进行运算的智能分析方案,它的优势在于适应性强,具体体现在:

1)对各种数据源的适配性高,在安防行业中数据源基本为视频和图片,但是这些数据的来源可谓是五花八门,尤其的早期的设备厂家非常地多,对接方式和编码格式都不标准,只有通过软件对接来完成前期工作,才能给智能分析提供原始材料;

2)新的算法出来时,基本上都是在软件上实现的,因为在软件上编码实现最容易,很少有算法一开始出来就能在智能分析硬件上运行的;

3)软件智能分析可以运行在很多通用服务器和PC机上,不需要配备特殊的硬件就能运行,而硬件智能分析就不一样了。

当然软件智能分析的主要缺点就在于分析性能差。

硬件智能分析

硬件智能分析,对应于软件智能分析,一般指运行在DSP、FPGA以及GPU资源上的智能分析应用。它的优点就是一个字“快”,一旦一种智能算法有了能支撑它的硬件资源后,一般就能把软件分析的处理能力甩开几条街。但是相对的,它的局限也就是软件智能的优势所在。

但是现在,软件智能分析和硬件智能分析没有那么严格的区分,很多新的算法经过软件方案进行一段时间验证后,一旦存在较大的可用性,就会慢慢走上硬件化的道路。近几年炙手可热的深度学习的计算量基本上只有在GPU上才能跑起来。因此流行的是通过CPU做适配和前期处理,GPU来做智能运算,也就是Intel CPU+NVIDIA GPU的组合,而且这个组合的处理性能也确实非常之强劲。

前端智能分析与后端智能分析对比
前端智能分析与后端智能分析的关系,也类似于上面的软硬件智能分析一样,都是相辅相成的,互为补充的。

前端智能分析

前端智能分析,在安防的业内一般是指在摄像机等传感器内置或者在传感器附近放置一个专门分析的前置设备。前端智能分析的优势是离采集端近,分析比较及时,通过前置的智能分析可以有效降低需要传输的数据的量。同时前端智能也受限于较弱的计算资源和有限的空间,再好的智能分析技术到前端也都是“龙居浅潭”。当下前端智能中最成熟也是上面说的智能方面最早成熟的车牌识别功能了,现在前端设备上车牌识别的准度已经达到了99%。

后端智能分析

后端智能分析是指用后端服务器的方案进行智能分析,也是当前比较主流的智能分析方案,业界的代表有海康的猎鹰,宇视的IA系列等。

由于智能的需求越来越大,智能需求的种类也越来越多,各种智能服务器会慢慢占领客户的机房,因此如何综合利用这些服务器资源,如何提升单位空间的计算能力是众多厂家关注和研究的方向,从当前来看主要的方向是:硬件化、框架化、集群化。

硬件化,经过上一节的分析,智能分析的硬件化是一些智能分析技术逐渐成熟的必然方向,现在的深度学习经过几年的发展已经表现出了强劲的发展前景,而且智能硬件只要能很好地承载深度学习的计算需求,就能应对绝大部分基于深度学习发展起来的具体智能分析技术,比如现在一个很典型的应用就是人脸识别的应用,用了GPU之后,性能可以翻很多倍。

框架化,因为智能算法越来越多,如果为每一款智能算法单独开发一次软件,那软件的工作就会有很多的浪费,如何能够开发一次软件,然后尽量多地去适配各种算法对厂家和客户来说都是比较有意义的事情。

集群化,既然智能分析的需求越来越大,那么这些服务器如何统一管理,如何统一进行资源分配又是一个比较棘手的事情,总不能让客户自己一台一台服务器去做配置,一台一台去做管理吧。集群化的需求就应运而生,它需要解决的问题有:统一管理各个服务器,进行统一的配置;动态解决各个服务器的资源和任务分配问题,避免客户自行去选择服务器;解决设备的灾备问题,在有多台设备的时候,不要因为一台设备的失效而导致它所负责的业务不可运行。

除了专门的智能服务器,现在有很多的后端产品也开始内置简单的智能功能了,比如很多新的NVR已经具备了不少的智能分析能力,在小规模的方案里这种产品形态将越来越有市场。

前后端融合计算

同软硬件智能分析的方案,前后端的智能分析也不是完全割裂和互斥的,两者都是“抓得住老鼠”的好猫,但是如何把他们合在一起适当分工是不是能成为更强大,更高效的猫呢?答案是肯定的。

前端是存在计算性能上的不足,存在空间上的拘束,但是它还是能承担一些分析工作的,而且最重要的是它的保有量大,每台做一点基础的分析工作,后台再在这些分析的成果基础之上做进一步的分析,将能大大降低后端服务器的工作压力。典型的,如现在的人脸智能分析方案中,如果采用后端引前端实况流分析的方案,一台E5的服务器能做到4~5路已经很高了;但是同样的服务器如果用前后端融合的方案,前端负责人脸的跟踪和抓拍,后端负责分析,一台服务器就能做到24路甚至更高,这就能大大降低用户的部署成本。

当前,阻挡前后端方案融合的最大障碍是标准的问题,各个厂家都是按照自己的理解定义接口,定义边界,厂家之间互通的唯一方式就是对接开发;因此,现在如果涉及到跨厂家的智能分析都更多的还是采用纯后端或者纯前端的方案,这是智能发展需要解决的重要问题。

智能分析在智能交通中的应用
前面也多次提到过,智能分析功能在智能交通行业的应用是最成熟的,其中最重要的启动点是车牌识别的识别率达到了应用的要求,以及在此基础之上形成的电子警察以及其他各种解决方案,在这里简单介绍一下:

1) 电子警察。车牌识别、红绿灯口车辆跟踪和抓拍等技术形成的电子警察的出现极大的提升了交警的交通管理能力,让交通违法抓获由一个概率事件变成了必然事件,一举解决了2012年前需要雇一堆的小姑娘进行闯红灯抓拍而且还抓不全的问题。近两年随着技术的成熟,虚拟线圈又替代了地感线圈,又降低了电警的施工难度和安装成本,使得电子警察的覆盖面越来越广;

2) 电子卡口。在电子警察中尝到智能分析甜头的用户,在开始进一步考虑城市车辆的监控和管理,因此电子卡口的业务就产生了,记录车辆的行进轨迹,根据行进轨迹进行车辆异常行为的分析,对接车管库进行车辆的核查等各种业务都开始落地。随着技术的成熟,和开车犯案的案例增多,公安部门也开始介入到卡口业务的建设,希望通过对车辆的排查进行案件的侦破的技战技术开始提出来,并产生了不少的成果;

3) 交通综合管控。随着上述技术的成熟,交警希望把道路上的各种资源统一调度起来,根据车流量情况和道路状况自动进行红绿灯的调配、以及其他道路疏导提示系统的调度,交通综合管控平台的成熟将对交警行业产生又一次重大的影响。其后,只要源源不断地将新成熟的智能算法、分析算法纳入到这个平台中,这个平台就能具备越来越强的管理能力。

当然在智能交通中,还有其他很多的智能技术在逐渐成熟,包括车辆的异常行为检测,车型车款识别等等,这些都将助力智能交通行业的快速发展。

(本文作者陈燕兵现任宇视系统规划师、尹鹏现任职于天津市公安局和平分局)

本文转自d1net(转载)


Linux及Arm-Linux程序开发笔记(零基础入门篇) - 一点一滴的Beer - 博客园

$
0
0


目录

一、Arm-Linux程序开发平台简要介绍... 3

1.1程序开发所需系统及开发语言... 3

1.2系统平台搭建方式... 4

二、Linux开发平台搭建... 5

2.1安装虚拟工作站... 5

2.2安装Linux虚拟机... 5

2.3虚拟机的一些基本配置... 7

2.3.1建立共享目录... 8

2.3.2调整桌面分辨率... 9

2.4安装软件... 11

三、Fedora-linux系统... 11

3.1控制终端的基本操作命令... 12

3.2编程工具简介... 12

3.2.1 Fedora-Eclipse CDT. 13

3.2.2 Qt库及Qt界面设计软件... 14

四、使用Eclipse CDT开发Linux程序... 15

4.1建立控制台程序... 15

4.2建立Linux窗口程序... 16

五、使用Eclipse开发Arm-linux程序... 19

5.1 Arm-linux硬件平台... 19

5.2开发控制台程序... 20

5.2.1使用Eclipse CDT编写代码... 20

5.2.2建立交叉编译环境... 20

5.2.3编译并运行程序... 22

5.3一些常用的软件介绍... 23

5.4开发arm-linux窗口程序... 24

5.4.1建立Qt交叉编译环境... 25

5.4.2编译生成可执行窗体程序... 26

六、高级Linux程序设计... 28

6.1多文件控制台项目... 29

6.2多文件Qt项目... 29

6.2.1使用Qt Designer设置界面... 29

6.2.2多文件Qt开发时的一些经典错误... 31

6.3关于makefile. 32

七、学习资料... 32

 

 

前言:本文记录了自己从一个完全不懂Linux的人如何一步步学会Linux程序开发的过程。当然也希望本文能够达到它的目的,让那些和我一样没有任何基础的人也能快速入门Linux程序开发。

一、Arm-Linux程序开发平台简要介绍

    Arm-Linux程序的开发并不像我们以前接触的Windows程序开发那样,关于平台的搭建就繁琐很多,所以在正式进入程序开发之前先对这种开发模式进行简要介绍,让一个即使没有任何Linux开发经验的程序员也能够看懂后面的内容。

1.1程序开发所需系统及开发语言

    开发arm-linux程序至少需要三种系统:

    Windows系统
    主要用来文件传送和一些简单的文本文件编辑。这个系统其实并非必需,只是因为目前PC机上最流行的系统仍然是Windows系统,我们的很多关于PC的文件和数据的操作习惯都是在Windows系统上养成的,已经对其形成了严重的依赖,所以Windows系统扮演着的“辅助开发系统”的角色。

    PC-Linux系统
    在此系统上安装arm-Linux交叉编译器后,就可以对代码文本文件进行编译,生成可在arm-Linux系统中运行可执行程序。此系统被称为“Linux宿主机”,我们对Linux程序的开发工作(包括代码编写、调试和编译生成可执行文件)基本上就是在此系统上进行的。

    Arm-Linux系统
    Arm-Linux程序运行的平台。此系统的硬件载体是一块小型的嵌入式arm板,我们在Linux宿主机上开发好程序并编译生成arm-linux可执行程序后,将可执行程序文件传送到嵌入式arm板中,然后就可以在arm板上直接运行此程序了。

三种系统的职能和联系如下图:

image

“三系统”职能和关联图

  关于每个系统的职能及操作还有文件如何传送交流都在后面详细介绍。

  关于开发语言,因为目前购买的Arm-Linux嵌入式板提供的编译库都是C/C++的,所以一般选择C/C++进行程序设计。

1.2系统平台搭建方式

  关于“三系统”的搭建方式,有两种方法:三台分立的单系统机器组合或者一台双系统PC机和一台单系统Arm板机器。但是不管采用哪种组合方式,各种系统的职能都是一样的,而且都是通过FTP、Telnet或者SSH等网络协议进行文件传输交流。

  “三机器”组合模式。将三个系统分别安装在三台机器上并将三台机器组建局域网。

  “两机器”组合模式。在PC机上利用虚拟工作站可以同时运行两个系统,虚拟机上的Linux系统基本可以完成所有的实体PC-Linux机器的所有任务,当然也能够组建局域网。

clip_image004

“三机器”组合模式图

clip_image006

“两机器”组合模式图

  如果开发人员对机器性能要求比较高,那么建议采用“三机器”组合模式。因为“两机器”组合模式要求一台机器运行双系统,每个系统都需要分配一定的硬件资源,可能会对每个系统运行的流畅程序都造成影响。

  如果开发人员只进行一些简单的程序开发,则建议采用“两机器”组合模式。这样可以节省硬件设备的投资,而且开发环境的搭建、开发的流程等等都会简单一些。而且下面的内容都是基于这种“两机器”组合的开发模式。

二、Linux开发平台搭建

2.1安装虚拟工作站

  目前比较流行的虚拟工作站比较多,推荐使用VM-ware WorkStation。建立虚拟工作站的目的就是为了使一台机器同时运行多个不同类型的系统,方便开发人员进行跨平台开发应用程序。

  ( 关于VMware,可以到网上搜索到详细介绍和使用方法,在此不再赘述)

2.2安装Linux虚拟机

  安装虚拟机的过程和安装实体机一样,唯一的区别是,虚拟机安装是从VMware中打开并安装的。先准备Linux安装文件,一般可以到网上下载到Linux的iso安装文件,然后从VMware中安装此系统。

  ( 关于虚拟机安装,在网上可以搜索到详细的步骤,在此不再赘述)

  几点建议:

  1.安装Fedora-10并选择完全安装,即把所以的包和选项都勾上。这样在安装完毕后,系统就自带了很多开发工具和相关的库,避免自己去重新进行烦琐的基本开发平台搭建。

  2.在VMware中为虚拟系统配置硬件资源。建议分配硬盘空间16G,内存1G以上。因为今后主要的Linux程序开发工作是在虚拟机上进行了,所以为了保证虚拟机流畅运行,需要分配比较丰富的硬件资源。

  在第一次从ISO文件中安装完毕虚拟机后,在安装目录下面会生成一系列的文件,将这些文件进行备份后,在其它机器上安装虚拟机的时候就花几个小时去从ISO中安装了,将这个目录下的文件复制到相应的目录下面(在第一次从ISO中安装的时候设定的目录相同),然后只需要从VMware中打开此目录下的文件就行了。

执行VMware Workstation中的【View】--【Home】,调出Home视图,然后打开虚拟工作站系统文件。

clip_image008

  打开后,就可以看到一个虚拟机的硬件资源配置了。这些参数有一部分是在只能在初次使用iso安装的时候配置的,比如硬盘大小。另外一些参数比如内存大小、共享目录等等可以在后期随时人工更改的。

详细设置内容,到网上可以找到很多相关内容,在此不再赘述

clip_image010

  两种方法的区别就是WinXp的安装盘和Ghost盘的区别吧。

  安装完毕Fedora后,就可以从虚拟工作站中启动Fedora Linux系统了。系统桌面如下:

clip_image012

2.3虚拟机的一些基本配置

  在第一次安装完毕虚拟机后开启虚拟机,默认的分辨率是800*600,同时没有全能共享目录。为了获得比较好的操作界面,同时方便文件共享和转移,需要进行一些基本的配置。

2.3.1建立共享目录

  在同一个硬件PC机上运行的两个系统。Windows实体系统用来做一般的用途,比如联网,处理文档等等;Fedora Linux虚拟系统则用来开发Linux程序并进行编译生成可执行文件。

  先需要安装vmware tools

  在VMware上面,选择菜单【VM】--【Install VM tools】,然后在Linux虚拟机上会生成目录/media,同时里面产生一个rpm文件。

  然后打开控制终端并su登录超级用户:

cd /media

rpm –ivh VMwareTools-7.8.5-156735.i386.rpm

cd /usr/bin

然后在/usr/bin下面看到wmware-config-tools.pl

再在Teminal中输入wmware-config-tools.pl

    然后一路回车下去。然后今后就可以建立共享目录。

  ( 详细过程也可以在网上搜索相关内容

clip_image014

    共享目录的好处是可以将这些文件在Windows系统中提取出来,然后可以通过我们熟悉的windows系统进行网络传输和一些其它命令操作。这个设置在后面程序开发的时候会提到其使用方法。

    在VMware中建立共享目录,执行菜单命令【VM】--【Settings…】

clip_image016

  然后在Linux虚拟机文件系统中可以找到对应的共享目录:/mnt/hgfs/xplinux

clip_image018

  建立共享目录可以方便虚拟机和实体机进行文件交流。例如,我们可以将在Linux机器上编译生成好执行文件放置到共享目录中,然后可以通过Windows机器提取出来并通过网络传送到arm-linux嵌入式板上。

2.3.2调整桌面分辨率

    在VMware工作站中调整虚拟机桌面分辨率。一般默认是800*600,而且现在的显示器,一般是1024*768甚至更高,以我目前使用的电脑显示器为例,分辨率是1440*900。需要将虚拟机桌面分辨率进行适当调整,然后全屏,那么就可以获得比较友好的操作界面。

    在VMware中启动虚拟机,虚拟机启动后的登录界面如下:

clip_image020

 

 

    然后登录系统,设置分辨率,系统菜单:

    【System】--【Preferences】--【Hardware】--【Screen Resolution】

clip_image022

clip_image024

    设置好分辨率后,就可以在获得全屏状态的Linux操作界面了。

clip_image026

2.3.2其它设置

    比如设置桌面背景,系统字体,还有资源管理器的模式等等,这些可以自己去实地操作并进行熟悉。

2.4安装软件

    虽然Fedora已经自带了一些必要的软件,如Open Office办公软件,gedit记事本软件,Firefox网页浏览器等等。但是既然是操作系统,我们也可以安装一些我们需要的软件。比如中文输入法(如:小企鹅输入法),聊天工具(如QQ)等等。

    注意:在Terminal终端中安装程序的时候需要启用su超级用户权限。

三、Fedora-linux系统

    Fedora-linux虚拟机安装完毕并进行基本设置后,就可以开始了程序开发环节了。在正式开始程序开发时,还需要对Linux的一些基本编程工具进行简要介绍,当然如果已经有此基础的,可以直接略过这一章。

3.1控制终端的基本操作命令

    执行系统菜单命令【Applications】--【System Tools】--【Terminal】,打开Terminal终端:

clip_image028

    熟悉Windows系统的DOS操作界面的人肯定也会对此操作终端也不会陌生。通过在Terminal中输入命令,用户可以很方便地完成Linux下的一系列操作,在正式进入自主程序设计之前,用户有必要对下面一些最基本命令进行熟悉(其它的命令在项目需要的时候再到网上查找相关资料)。

文件操作命令

文件/文件夹的创建、修改、复制、删除、移动等等。(如rm/mv/vi/cat等等)

网络设置命令

个性IP、开启FTP、开启Telnet、开启SSH等等。(如ifconfig/telnetd等等)

用户管理命令

系统用户的添加、删除、密码修改等等。(如,addusr/passwd等等)

文件编译命令

这个涉及到编译环境的建立,将在后面正式进入到自主程序设计时进行详细介绍。

    虽然目前Fedora为用户提供了比较良好的操作界面,但是Linux最初是从命令行的操作系统发展起来的,很多强大的功能都是需要依靠Terminal控制终端来实现的,所以作为开发人员有必要先对这些基本命令进行熟悉并熟练操作。

3.2编程工具简介

    如果在安装Fedora的时候,选择的是完全安装,系统会自带很多开发工具。其中Fedora -Eclipse CDT和Qt库及相关设计器Qt Designer是我们今后开发的主要工具。

当然,如果用户没有选择安全安装,则可以到网上下载到各类开发工具的安装文件,并自己进行安装。

3.2.1 Fedora-Eclipse CDT

    在Fedora中执行系统菜单【Application】--【Programming】--【Eclipse】

clip_image030

    然后就可以看到Fedora Eclipse的启动画面

clip_image032

    然后就是下面就是Eclispe的IDE环境了,有过Windows下的Java编程经验的人来说,这个界面是再熟悉不过了,完全可以直接过渡到Linux下的C/C++程序开发上来,这样就极大地减少了程序员的学习周期。

clip_image034

    Eclipse的好处,第一就是开源,第二就是免费,第三就跨平台。

    因为开源,所以可扩展性很强,Eclipse实际上就是一个万能的程序开发环境,只需要在设置中对相关语言的库进行引用并连接相应的编译器,就可以对基于任何语言的程序进行开发;因为免费,所以省去了注册软件等等琐事。因为跨平台,所以当用户进行跨平台程序设计的时候,可以极大减少学习周期。

    Eclispe在Windows平台下一般都是作为Java的开发环境,用来开发Java桌面应用程序和Jsp网页应用程序甚至目前比较流行的Android手机终端软件。在Linux下面对C/C++的库编译器进行了连接后,则成为Linux下的C/C++开发环境,在Linux下安装JDK后则可以进行Linux下的Java程序设计,如果给Linux系统安装tomcat网页服务器,则可以进行Linux下的网页应用程序开发,如果给Linux安装mysql数据库服务,则可以进行数据库连接操作等等(因为目前我们的目标主要是arm-linux窗口程序开发,所以主要对C/C++进行研究)。

3.2.2 Qt库及Qt界面设计软件

    如果只是编写C/C++控制台软件,则只需要Eclipse CDT就完全可以解决。但是如果要进行窗体化编程,就需要引用一些开源的界面库。例如目前比较流行的Qt库。

    Qt是诺基亚开发的一个跨平台的C++图形用户界面应用程序框架。它提供给应用程序开发者建立艺术级的图形用户界面所需的所用功能。Qt是完全面向对象的,很容易扩展,并且允许真正地组件编程。基本上,Qt 同 X Window 上的 Motif,Openwin,GTK 等图形界 面库和 Windows 平台上的 MFC,OWL,VCL,ATL 是同类型的东西,但Qt具有优良的跨平台特性、面向对象 、丰富的API、大量的开发文档等优点。

    在Eclipse中对项目Build选项进行设置,对Qt库进行连接,然后就可以进行Linux下的Qt程序进行开发了。

    因为Eclipse没有提供可视化的窗体程序设计器,所以需要借助第三方工具,QtDesigner可以很好的解决Qt的UI设计问题并编译生成对应的*.h和*.cpp文件(具体的操作步骤将在下面的内容中详细介绍)。

clip_image036

    所谓“工欲善其事,必先利其器”,前面花了大量篇幅来介绍,都是为了“磨刀”,现在开始进入“砍柴”环节。下面将以Eclipse CDT作为开发环境来介绍Linux和arm-linux程序开发的一般方法。

四、使用Eclipse CDT开发Linux程序

4.1建立控制台程序

    用Eclipse CDT建立基于C++的“Hello World”控制台程序。

    打开Eclipse开发环境。执行【File】--【New】--【C++ Project】

clip_image038

    然后进入到C++ Project向导,有C++的Hello World模板

clip_image040

    点击Finish,然后Eclispe就会建立一个基于C++的HelloWorld项目,运行程序后,能够在控制台中输出“Hello World”:

clip_image042

    说明:如果是开发控制台程序,则不需要进行任何其它设计,只需要直接编写C++代码即可,不用对编译库和编译器进行设置,Eclipse已经为我们自动完成了这些内容。

4.2建立Linux窗口程序

    就像在Window环境下,如果要用C++开发窗口程序,我们一般需要使用Windows API或者MFC。同样,在Linux环境下,我们可能在C++项目中引用Qt库来进行Linux下的窗口程序开发。

    在完整版的Fedora中,自带了Qt的库,在目录/usr/lib下面:

clip_image044

    系统默认自带有两个版本的Qt库qt-3.3和qt4,一般我们用qt-3.3。

    编写窗口程序,我们只需要在建立了C++ Project的情况下,对C/C++ Build路径进行设置,指向qt-3.3的库即可。主要开发流程如下:

clip_image046

    第一步:使用Eclipse CDT建立C++ Project。

    第二步:设置Build路径指向Qt。在项目管理器中右击项目文件设置项目属性,在C/C++ Build的Settings的Tool Settings中进行设置。

clip_image048

    【GCC C++ Compiler】--【Directories】添加包含的文件路径:/usr/lib/qt-3.3/include。这个是C++的编译器包含的头文件路径,因为是建立的C++ Project,所以一定要设置。

clip_image050

    【GCC C Compiler】--【Directories】添加包含的文件路径:/usr/lib/qt-3.3/include。这个是设置C语言编译器包含的头文件路径,因为有时候C++中会考虑到兼容一些C语言的语法和关键字,所以最好也进行相关设置。

clip_image052

    【GCC C++ Linker】--【Libraries】添加编译时引用的库路径:/usr/lib/qt-3.3/lib(注意:不要写成include了)。并设置库搜索命令参数:qt-mt。

第三步:编写引用了Qt库的C++程序。

第四步:运行程序。

程序运行效果如下图:

clip_image054

    总结:通过上面对Linux下面的控制台程序及窗口程序的介绍,我们已经对Linux系统下的简单程序开发流程有了简单了解。下面开始介绍arm-Linux程序的开发。

五、使用Eclipse开发Arm-linux程序 

5.1 Arm-linux硬件平台 

clip_image056

    Arm-Linux机器采用的是飞凌嵌入式技术公司的FL2440开发板。在飞凌公司购买开发板的时候,会随开发板一起赠送的相关入门教程《飞凌开发板配套教程》并附有一张光盘,里面有各种写FL2440相关的开发资源。

    在正式进入程序开发之前,先对FL2440开发板及arm-linux系统进行熟悉。

    Fl2440开发板:熟悉Bootloader的使用方法、学会烧写内核、烧写文件系统等等。

    Arm-linux系统:熟悉利用一些文件系统和网络设置相关的命令。如果你对Linux命令已经有了一定了解,那么arm-linux上的命令也基本一样。

详细操作过程请参考《飞凌开发板配套教程》一书

5.2开发控制台程序

    主要开发流程图如下:

image

    在早期的开发环境方式中,是先用文本编辑器编写c或者cpp文件,然后再直接在Linux机器上通过交叉编译命令,编译代码文本文件并生成可执行程序,然后将可执行程序传送到arm-linux板上,然后就可以在arm-linux机器上运行程序了。

    现在的开发模式和早期的开发模式一样,只是在编写代码的工具上进行了改进。早期编写代码的工具只要是能编辑文本的软件就可以,甚至在Linux中通过终端的vi命令就可以搞定,Linux机器的职能仅仅是编写代码和编译代码。现在则使用Eclipse CDT,可以在Linux机器上编写C/C++代码并进行调试,Linux机器基本上可以模拟arm-linux系统上除了一些硬件相关度高的应用程序(如驱动程序)之外的绝大部分其它程序的运行环境。

5.2.1使用Eclipse CDT编写代码

    其实和用Eclipse CDT编写Linux环境下的控制台程序是完全一样。除了在一些特别的场合,比如编写驱动程序,需要注意差别外,其余的功能的实现方法基本上一样。

    这方面属于程序设计的基本功,需要长期学习和积累,所以在此不再多述。

5.2.2建立交叉编译环境

    虽然生成程序的源码是一样的,但是在Linux机器上编译生成的可执行文件是不能在arm-linux系统上运行的,需要用arm-linux专用的编译器进行编译后,才能生成可在arm-linux系统上运行的可执行文件,当然此时应用程序又显然不能在Linux系统上运行了。

    将交叉编译工具cross-2.95.3.tar.bz2(可以到网上下载,也可以到开发板附带的光盘资源中找到)通过共享目录传送到Linux系统中,然后在Linux系统终端中先进入其文件目录,然后执行解压命令:

tar xjvf cross-2.95.3.tar.bz2

    然后在/usr/local/arm文件目录下可以看到解压后的2.95.3的库了,或者如果你解压的是cross-3.4.1.tar.bz2的版本,那么将会在对应目录下生成3.4.1的目录。

clip_image060

    然后设置环境变量。因为Linux机器上存在不止一种编译器,为了避免环境变量冲突,最好新建一个账号,比如新建账号arm-linux-gcc,然后在对应的账号目录中找到.bash_profile文件,设置环境变量,编辑.bash_profile,在最后一行增加路径(vi打开文本文件,按i表示insert修改文本文件,然后Esc退出insert模式,再shift zz表示保存退出文件,详细的命令介绍可以到网上查阅相关资料):

export PATH=/usr/local/arm/2.95.3/bin:$PATH

clip_image062

    可以通过echo $PATH来查看环境变量是否设置成功:

clip_image064

5.2.3编译并运行程序

    在编辑好了代码文件并建立好了交叉编译环境后,就开始编译代码生成可执行程序,并移植到arm板上运行程序。

    在Linux系统的终端中通过输入arm-linux-gcc/arm-linux-g++来编译C/C++文件。例如,我们对前面编写的最简单的CppHelloWorld.cpp文件进行编译:

arm-linux-g++ CppHelloWorld.cpp –o CppHello

clip_image066

    然后在对应目录下会生成一个可在arm-linux系统上运行的应用程序CppHello。

    然后通过SSH服务和FTP服务,将可执行文件从Linux文件系统网络传送到arm-linux文件系统(在此用到了两个小软件SSH Secure和LeapFTP,在后面再对软件的功能进行一下简要介绍)。

clip_image068

    然后在Windows机器上远程登录arm-linux系统开发板,并控制程序运行

clip_image070

    注意:需要先通过chmod +x CppHello来告诉系统此文件是可执行文件,然后再通过./CppHello来运行程序。

    我们可以看到程序运行的结果:在屏幕上打印出一行字“Hello World”

5.3一些常用的软件介绍

    在上面介绍的一些操作中,在进行文件传送的时候用到了一些软件,在此进行简要介绍。

LeapFtp

一个基于Ftp协议的文件互传软件。一般情况下,我们对arm-linux机器上开启Ftp服务,然后就可以实现Windows机器和arm-linux机器之间文件互传。

SSH Secure File Transfer Client

一个基于SSH协议的文件互传软件,可以实现FTP的功能。虽然Linux机器和Windows机器之间可以通过共享目录进行文件交互,但是因为共享目录实际上相当于Linux机器的一个虚拟的外接设备,在每次编写代码保存后,系统都会重读一次,给操作上造成一点小麻烦,所以建议使用SSH进行文件传送。

SSH Secure Shell Client

一个基于SSH的远程登录软件,可以实现telnet功能。因为Fedora默认关闭了telnet功能,而采用更安全的SSH协议来实现telnet的相关功能。

    三系统之间的主要网络架构如下:

image

    注意:

    1.如果arm-linux系统的ftp和telnet连接不上,一般情况下是arm-linux默认没有开启相应的服务或者默认ip和windows机器不在同一网段。可以通过串口线将arm板连接到电脑上,然后打开超级终端,输入命令vsftpd&开启ftp服务,输入命令telnetd开启telnet服务,ifconfig eth0 192.168.1.16设置IP到同一网段。

    2.如果Linux系统的SSH连接不上,则一般情况下是Linux系统开启了SSH的防火墙,则只需要在Linux系统中对防火墙进行相关设置,关闭针对SSH的防火墙。【System】--【Administration】--【Firewall】,然后勾选全能SSH。

clip_image074

clip_image076

    还有一些其它软件,比如小组协作时候,需要使用SVN进行代码管理;还有远程桌面VNC Viewer,可以进行远程桌面控制(但是效果不太好,桌面显示的延时好像比较严重)等等。

5.4开发arm-linux窗口程序

5.4.1建立Qt交叉编译环境

    在Linux系统中新建目录/root/yizhi,然后将已经编译好的arm-QT库复制到此目录下面。

clip_image078

    然后将上面那六个tar.gz压缩文件解压到/root/yizhi目录。

clip_image080

    在进行arm-linux下的Qt编译的时候,也涉及到环境变量设置问题,所以我们也最好再新建一个账户,专门用于编译arm-Qt程序。

    例如,在Linux系统终端中添加用户zsm,然后进入到/home/zsm中,对.bash_profile进行修改,设置环境变量:

clip_image082

    在命令终端中ls –a 显示隐藏的所有文件 找到.bash_profile,输入:

gedit .bash_profile &

    用geidt打开此文件后,在最后面添加下面的环境变量设置:

export PATH=/root/yizhi/qtopia-free-2.2.0/qtopia/bin:/root/yizhi/qtopia-free-2.2.0/tmake/bin:/root/yizhi/qtopia-free-2.2.0/qt2/bin:/usr/local/arm/2.95.3/bin:$PATH

export QTDIR=/root/yizhi/qtopia-free-2.2.0/qt2

export QTEDIR=/root/yizhi/qtopia-free-2.2.0/qtopia/

export LD_LIBRARY_PATH=$QTDIR/lib:$QPEDIR/lib:$LD_LIBRARY_PATH

export CC=/usr/local/arm/2.95.3/bin/arm-linux-gcc

export TMAKEDIR=/root/yizhi/qtopia-free-2.2.0/tmake

export TMAKEPATH=$TMAKEDIR/lib/qws/linux-arm-g++

    设置好后最好重新登录此账号,然后在终端中输入echo $PATH来检验环境变量是否设置成功:

clip_image084

    如果出现上面的输出,则表示arm-linux平台下的Qt交叉编译环境的环境变量已经设置成功。接下来就可以进行Qt程序开发了。

5.4.2编译生成可执行窗体程序

    在3.2节中已经写好了一个单文件的项目QtHello,并在项目的src目录下生成了一个QtHello.cpp文件,然后我们要做的就是利用arm-linux下的Qt编译器对其进行编译。

    编译Qt窗体项目比编译普通控制台项目要稍微麻烦一点,需要自己写makefile来建立编译规则,编译如上的QtHello.cpp的makefile有如下模板:

#############################################################################

# Makefile for building hello

# Generated by tmake at 20:58, 2011/04/14

# Project: hello

# Template: app

#############################################################################

####### Compiler, tools and options

CC = arm-linux-gcc

CXX = arm-linux-g++

CFLAGS = -pipe -Wall -W -O2 -DNO_DEBUG

CXXFLAGS= -pipe -DQWS -fno-exceptions -fno-rtti -Wall -W -O2 -DNO_DEBUG

INCPATH = -I. -I$(QTDIR)/include

LINK = arm-linux-gcc

LFLAGS =

LIBS = $(SUBLIBS) -L$(QTDIR)/lib -lm -lqte

MOC = $(QTDIR)/bin/moc

UIC = $(QTDIR)/bin/uic

TAR = tar -cf

GZIP = gzip -9f

####### Files

TARGET = QtHello

HEADERS = $(TARGET).h

SOURCES = $(TARGET).cpp

OBJECTS = $(TARGET).o

DIST =

INTERFACE_DECL_PATH = .

####### Implicit rules

.SUFFIXES: .cpp .cxx .cc .C .c

.cpp.o:

$(CXX) -c $(CXXFLAGS) $(INCPATH) -o $@ $<

.cxx.o:

$(CXX) -c $(CXXFLAGS) $(INCPATH) -o $@ $<

.cc.o:

$(CXX) -c $(CXXFLAGS) $(INCPATH) -o $@ $<

.C.o:

$(CXX) -c $(CXXFLAGS) $(INCPATH) -o $@ $<

.c.o:

$(CC) -c $(CFLAGS) $(INCPATH) -o $@ $<

####### Build rules

all: $(TARGET)

$(TARGET): $(OBJECTS)

$(LINK) $(LFLAGS) -o $(TARGET) $(OBJECTS) $(LIBS)

dist:

$(TAR) hello.tar hello.pro $(SOURCES) $(HEADERS) $(INTERFACES) $(DIST)

$(GZIP) hello.tar

clean:

-rm -f $(OBJECTS) $(OBJMOC) $(SRCMOC) $(UICIMPLS) $(UICDECLS) $(TARGET)

-rm -f *~ core

####### Sub-libraries

###### Combined headers

####### Compile

$(TARGET).o: $(TARGET).cpp

    用zsm账号登录(因为关于arm-Qt库的编译环境变量是在此账号中设置的),然后执行如下步骤:

1.将makefile模板文件放置到src目录下面

2.用eclipse或者其它文本编辑器,修改makefile里面的TARGET一项为当前项目名称

3.打开Teminal控制终端,进入到src目录,输入make

clip_image086

4.然后会在src目录下面生成一个指定名称相应的QtHello文件,这个就是arm目标板上的运行程序了。

5.将QtHello文件传送到arm板并运行程序(详细操作方法在4.2.3节中有介绍)。

clip_image088

    在Windows机器上远程登录arm板,控制程序运行,然后可以看到arm板上运行的结果了。

六、高级Linux程序设计

    前面所介绍的不管是控制台还是窗体程序,都属于单文件项目的范围。而当程序的功能比较复杂时,则往往需要很多模块和文件,这样在向arm-linux上移植程序时会更加繁琐一些,需要开发人员自己写makefile,建立多文件的编译规则。

    因为我对此没有进入深入一点的研究,所以只能提供下大致思路和在开发程序时遇到的一些常见问题及解决方案。如果今后有机会有时间的话,则会对makefile进行深入一点的研究,最好是能以Eclipse自己生成的makefile模板为基础进行简单的修改,然后就可以编译生成arm板上的可执行程序。

6.1多文件控制台项目

    多文件的控制台程序的makefile可能会容易一些,自己也没有研究过,以前在使用Magic C++编写C++控制台程序的时候,只需要对Magic C++生成的makefile模块中的编译器进行修改就可以轻松为多文件生成基于arm-linux平台上的控制台程序。

clip_image090

    (关于如何对Eclipse的makefile模板进行修改,目前还没有研究过,今后有时间研究的话,再补充上吧。)

6.2多文件Qt项目

    目前只对简单的多文件项目进行了尝试:一个main函数文件,一个窗体头文件,一个窗体实现文件。

6.2.1使用Qt Designer设置界面

    在Linux系统中打开Qt Designer,然后进行可视化窗口设计。然后点击保存为mydialog.ui文件。

clip_image092

    打开mydialog.ui文件,发现其实只是一个xml文件:

clip_image094

    显然这个文件是不能直接被C++项目引用的,需要使用Qt Designer的编译器进行编译,生成和界面对应的h和cpp文件。

    在Terminal终端里面运行以下命令:

uic xxx.ui -o xxx.h 生成.h文件

uic xxx.ui -i xxx.h -o xxx.cpp 生成.cpp文件

clip_image096

    然后生成的mydialog.h和mydialog.cpp文件就是和mydialog.ui相对应的程序代码文件了。可以在Eclipse项目中直接对此文件进行引用,就可以显示对应的窗体了。

6.2.2多文件Qt开发时的一些经典错误

    经典错误一: 

“undefined reference to ……”

clip_image098

    这是在引用Qt的库时,产生了某些歧义,需要进行一些预先处理,生成和界面文件相对应的moc文件,关于问题的详细介绍可以参考下面的帖子:

http://hi.baidu.com/asky007/blog/item/7aad95ccbee5ba1601e928d7.html

   解决方案:

    在Linux控制终端中进入到项目代码文件目录,执行

qmake –project

qmake

make

    然后此目录下会生成一系列的文件,如moc_xx.cpp,moc_xx.o,src,src_pro等等。然后再到Eclipse中编译此项目,则错误消失。

    经典错误二:

    在Linux机器上能运行的Qt窗口程序,在arm-linux下的Qt编译器下无法通过。

    可能的问题是Qt库版本问题,或者是有些Qt运行环境在Linux机和arm-linux机上有所不同,这就需要重新寻找新的代替解决方案。正是因为这些很多不确定的因素,所以在进行arm-linux界面程序开发时,需要经常在编写一段新代码就要在arm板上进行测试,可以避免做太多无用功。

6.3关于makefile

    关于多文件Qt项目向arm-linux系统上的移植,比较核心的技术应该就在makefile上吧,目前自己了解太少,只限于对模板的应用,所以下面的就写不下去了。

    下面有一篇到网上找到的关于makefile结构分析的帖子,今后有机会再研究吧。

http://blog.csdn.net/liang13664759/archive/2007/09/04/1771246.aspx

七、学习资料

    关于Linux下的Qt开发,在安装了的Fedora里面有相关的本地reference和相关的源码。如:Qt Assistant

clip_image100

    关于arm-linux下的Qt开发,安装交叉编译环境时候,也有相关文档和源码:

clip_image102

//**************************************************************************

结语:OK,写到这里了。在上个学期就花过一段时间学习Linux,然后中断了半年,这个学期又进行了深入的学习,把以前的内容和现在的内容串接起来了,终于有了比较清晰一点的思路和体会了,所以总结一下,也让将来的自己再回头学习时有一点借鉴吧。感谢胡师兄的一点一滴的指导。

2011-5-5

于武汉大学

------------------------------------------------------------------

Author: 一点一滴的Beer

Email /Gtalk:dreamzsm@gmail.com

From: http://www.cnblogs.com/beer

Notes:欢迎转贴,但请在页面中加个链接注明出处

original: http://www.cnblogs.com/beer/archive/2011/05/05/2037449.html

给产品负责人的Sprint评审会建议 - Agile_zhanglao的博客 - CSDN博客

$
0
0

Sprint评审会议可能是产品人员最重要的Scrum活动,它可以帮助您收集反馈意见,做出正确的产品决策,从而增加创造成功产品的机会。但是我发现,产品负责人并不总是清楚谁应该参加会议,应该如何开展这个会议,以及如何收集相关反馈。本文将回答这些问题,并分享我的一些建议,以帮助您在Sprint评审会上得到更多收获。

邀请正确的人参与

从正确的人那里收集反馈信息对于做出正确的产品决策至关重要:如果您邀请了不合适的人参与,或者关键人物未到场,那么您不太可能收到您所需要的反馈。因此,你应该确保你邀请合适的人参与。
一般来说,邀请那些你需要获得反馈来验证最新产品增量,以及可以帮助您推进产品发展的人参与。这些人通常是您的关键干系人 - 对您的产品有兴趣的人员,以及您需要去发掘的产品用户和需要你提供产品的人。这些人可能包括营销,销售,服务和支持人员以及其他业务部门的人员,具体取决于您的产品和组织。
为了鼓励干系人参与,以及管理干系人的期望,告诉他们为什么需要他们参加会议,以及他们可能会看到什么,这是很有帮助的。

协作但不要畏惧说不

我参加过不止一个很快就结束的Sprint评审会:一位开发团队成员向那些看起来很困惑的干系人和站在这些干系人身后的产品负责人演示了产品功能。然后,Scrum Master询问大家是否有任何问题或反馈意见,这些干系人相互看着对方,有些人说:“干的不错”,“看起来还可以”,然后人们就离开了。这次会议收集到的有价值的意见是零。
因此,要鼓励人们积极参与并分享他们的观点,想法和担忧。使用开放式的问题,比如“你怎么看待我们对注册功能的改进?”,试着理解为什么有人喜欢或不喜欢这个迭代的产品增量。收到诸如“看起来很棒”的反馈可能会感觉很好,但是它并没有提供任何新的见解。为什么这个人喜欢这个功能?有什么可以进一步改善?
让所有的与会者有机会发表他们的观点,欣赏他们的意见和反馈,即使你不同意或难以接受他们。请记住:来自干系人的创造力,知识和反馈可以帮助您做出正确的产品决策,以提供可能的最好的产品。
同时,不要接受某些个人通过这个会议来达成他的个人目的或者某个业务单元的利益。我记得有一次Sprint评审会,当时一位高级干系人在产品负责人和开发团队中直言不讳地提出了他的要求,这当然不合适,也没有提供任何帮助。
作为产品负责人,善待和理解很重要。但是不要让干系人告诉你该怎么做。你对产品负责,你必须拥有最后的发言权,否则你无法获得足够的权力和尊重。
对于没有帮助的想法和不现实的要求,不要害怕说不。基于产品整体战略和产品路线图来决定是否需要接受相关请求。如果你怀疑你的决定,可以使用下一个sprint来测试这个想法或请求是否对用户有益。但请记住,提供一个令所有人满意的产品几乎是不可能的。
请您的ScrumMaster协助会议并制定基本规则。这使您可以放手去和参与者进行沟通,并收集反馈意见。

考虑把Sprint评审会分成两部分

在有些情况下,将Sprint评审会分为两部分是有帮助的。第一部分,你和开发团队参与,团队向您演示产品增量。然后,您向团队成员提供反馈意见,并确定哪些条目已完成,我们现在进展如何。你可以使用发布燃尽图来看看进展。如果你在Sprint期间已经有了足够多的参与,并且已经看过相关完成的功能并给与了反馈,那么第一部分的环节你可能根本不需要。
第二部分,干系人参加会议。我发现,作为产品负责人,您通常最适合向干系人呈现产品增量:您可能比开发团队成员更好地了解用户如何与产品进行交互并使用新功能。然后收集干系人的反馈意见,以了解我们是否开发出了具有正确用户体验和功能的正确产品。按照上面的建议询问开放式问题,以便了解为什么新功能很好或者为什么需要调整。
把会议分成两个部分,这样你可以与开发团队有一个线下沟通的机会,也可以在Scrum团队以外的人加入之前理清分歧。当您在Sprint期间没有机会与团队互动时,这种方式尤其有用,例如,您和团队不在一个地方办公,或者您忙于访问用户和客户,或参加展会或会议。但是要确保开发团队成员出席整个会议。直接听取干系人的意见是非常宝贵的。

考虑分开收集最终用户和干系人的反馈

Sprint评审会的原意是将所有合适的人聚集在一起,同时收集每个人的反馈意见。如果这对你有效,那很好。但是,我经常发现分别收集用户和内部干系人的反馈信息会更有帮助。为什么?这两个群体往往有不同的观点和利益。
通过用户测试产品增量可以让您了解产品是否适用于你的目标用户群,是否提供了正确的用户体验和正确的功能。与干系人讨论产品增量可帮助您了解是否高效地提供了产品,是否可以开始运营、销售和支撑你的组织了。
更重要的是,最好用不同的技术来收集用户和干系人的反馈:当实现的功能很少时,向最终用户演示产品增量是有意义的。否则,观察或测量用户实际使用产品的方式会更有帮助,例如可用性测试和早期版本。
相比Sprint评审会,这些技术通常需要更长的时间,可能需要几天的时间才能将产品增量发布给(选定的)用户后收集相关数据,这种方式自然而然地把收集最终用户反馈和收集干系人反馈分开了。
记住,用户胜过干系人:如果产品不利于用户,人们不会长期使用它,无论它是多么可销售或者可服务,或者CEO有多喜欢。

不要急于决定

有些情况下,你可以在Sprint评审会议上立即做出产品决策,甚至可以调整好产品Backlog,正如Scrum指南所建议的那样。但是,很多时候,特别是如果反馈的影响比较大,导致产品Backlog变化更大的时候, 花更多的时间来分析反馈,得出正确的结论,然后再决定如何调整产品Backlog,这样你能够得到更多的收获。
此外,如果您决定分开收集用户和干系人的反馈意见,如上所述,你可能不会在Sprint评审会获得相关反馈数据。因此,您应该考虑把收集反馈和数据,跟分析和采取行动分开进行。例如,您可以选择在下一次Sprint计划会议之前开展一个简短、聚焦的产品Backlog工作坊,用这种方式来客观评估这些反馈,并和开发团队讨论如何调整产品Backlog。 或者,您可以在分析工具的帮助下,收集到足够多的用户数据以后,在下个Sprint组织一次产品Backlog的会议来评估这些反馈。

讨论发布进度

想象一下,所有的反馈和数据表明,人们将会热爱你的产品。但是,如果你的产品发布延迟或者超出预算,那么您的产品可能不会成功,甚至可能不会正式发布。因此,您必须定期确保产品要有进展。
Sprint评审会是一个很好的机会,因为你现在应该知道哪些条目已经完成,距离终点还有多远。此外,参加会议的关键干系人可能需要知道新的产品版本是否能按计划发布,或者是否有延误,因为这可能会影响他们的工作。更重要的是,讨论发布进度,将当前的Sprint放到了上下文环境中,并和之前Sprint连接起来,让我们能开到整体的进展。
我喜欢使用发布燃尽图——Scrum的标准工具来跟踪发布进度,并预测项目的后续进展。发布燃尽图展示了产品Backlog中剩余的工作量随着迭代的开展不断减少的趋势。
无论您使用哪种工具:确保它可以帮助您了解您的进度如何,并进行必要的调整:比如,为团队新增UX设计师,或者推迟发布日期、只部分满足发布目标、比计划交付更少一些的功能等。

 

英文资料来源:
http://www.romanpichler.com/blog/sprint-review-tips-for-product-owners/

作者:罗马·皮赫勒

译者:廖靖斌Eric Liao, Scrum中文网资深敏捷教练、顾问和培训师,CSP


Spring Boot 项目启动时执行特定方法 - 坤哥的博客 - CSDN博客

$
0
0

Springboot给我们提供了两种“开机启动”某些方法的方式:ApplicationRunner和CommandLineRunner。


这两种方法提供的目的是为了满足,在项目启动的时候立刻执行某些方法。我们可以通过实现ApplicationRunner和CommandLineRunner,来实现,他们都是在SpringApplication 执行之后开始执行的。


CommandLineRunner接口可以用来接收字符串数组的命令行参数,ApplicationRunner 是使用ApplicationArguments 用来接收参数的,貌似后者更牛逼一些。


先看看CommandLineRunner :


package com.springboot.study;


import org.springframework.boot.CommandLineRunner;

import org.springframework.stereotype.Component;


/**

 * Created by pangkunkun on 2017/9/3.

 */

@Component

public class MyCommandLineRunner implements CommandLineRunner{


    @Override

    public void run(String... var1) throws Exception{

        System.out.println("This will be execute when the project was started!");

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

ApplicationRunner :


package com.springboot.study;


import org.springframework.boot.ApplicationArguments;

import org.springframework.boot.ApplicationRunner;

import org.springframework.stereotype.Component;


/**

 * Created by pangkunkun on 2017/9/3.

 */

@Component

public class MyApplicationRunner implements ApplicationRunner {


    @Override

    public void run(ApplicationArguments var1) throws Exception{

        System.out.println("MyApplicationRunner class will be execute when the project was started!");

    }


}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

这两种方式的实现都很简单,直接实现了相应的接口就可以了。记得在类上加@Component注解。


如果想要指定启动方法执行的顺序,可以通过实现org.springframework.core.Ordered接口或者使用org.springframework.core.annotation.Order注解来实现。


这里我们以ApplicationRunner 为例来分别实现。


Ordered接口:


package com.springboot.study;


import org.springframework.boot.ApplicationArguments;

import org.springframework.boot.ApplicationRunner;

import org.springframework.core.Ordered;

import org.springframework.stereotype.Component;


/**

 * Created by pangkunkun on 2017/9/3.

 */

@Component

public class MyApplicationRunner implements ApplicationRunner,Ordered{



    @Override

    public int getOrder(){

        return 1;//通过设置这里的数字来知道指定顺序

    }


    @Override

    public void run(ApplicationArguments var1) throws Exception{

        System.out.println("MyApplicationRunner1!");

    }


}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

Order注解实现方式:


package com.springboot.study;


import org.springframework.boot.ApplicationArguments;

import org.springframework.boot.ApplicationRunner;

import org.springframework.core.Ordered;

import org.springframework.core.annotation.Order;

import org.springframework.stereotype.Component;


/**

 * Created by pangkunkun on 2017/9/3.

 * 这里通过设定value的值来指定执行顺序

 */

@Component

@Order(value = 1)

public class MyApplicationRunner implements ApplicationRunner{


    @Override

    public void run(ApplicationArguments var1) throws Exception{

        System.out.println("MyApplicationRunner1!");

    }


}


Linux 单网卡通过NAT设置共享上网 - 百川汇海 - CSDN博客

$
0
0

第一步在单网卡上面绑定一个内网的地址

ifconfig em1:0 192.168.160.100 netmask 255.255.255.0 up

【重启动网卡后就消失了】

第二部配置nat的转发,创建一个脚本

#!/bin/sh
echo 1 > /proc/sys/net/ipv4/ip_forward
modprobe iptable_nat
iptables -F INPUT
iptables -F FORWARD
iptables -F POSTROUTING -t nat
iptables -t nat -F
iptables -P FORWARD ACCEPT
/sbin/iptables -A FORWARD -i em1 -o em1:0 -m state  --state RELATED,ESTABLISHED -j ACCEPT
/sbin/iptables -A FORWARD -i em1:0 -o em1 -j ACCEPT
/sbin/iptables -t nat -A POSTROUTING -o em1 -j MASQUERADE

完成后

service iptables save

service iptables restart


第三部 配置内网机器的ip地址为内网ip,网关为em1:0的地址


Tutk P2P的原理和常见的实现方式 - 书弋江山的博客 - CSDN博客

$
0
0

最近在做tutk P2p相关公司的IP摄像头项目,然后看了一下实现原理,然后改一了一篇文章来记录一下,天下文章一大抄,天下代码也是一样的,下面就是TUTK 的Demo
tutk实现的Demo

1.简介
  当今互联网到处存在着一些中间件(MIddleBoxes),如NAT和防火墙,导致两个(不在同一内网)中的客户端无法直接通信。这些问题即便是到了IPV6时代也会存在,因为即使不需要NAT,但还有其他中间件如防火墙阻挡了链接的建立。

  当今部署的中间件大多都是在C/S架构上设计的,其中相对隐匿的客户机主动向周知的服务端(拥有静态IP地址和DNS名称)发起链接请求。大多数中间件实现了一种非对称的通讯模型,即内网中的主机可以初始化对外的链接,而外网的主机却不能初始化对内网的链接,除非经过中间件管理员特殊配置。在中间件为常见的NAPT的情况下(也是本文主要讨论的),内网中的客户端没有单独的公网IP地址,而是通过NAPT转换,和其他同一内网用户共享一个公网IP。这种内网主机隐藏在中间件后的不可访问性对于一些客户端

软件如浏览器来说并不是一个问题,因为其只需要初始化对外的链接,从某方面来看反而还对隐私保护有好处。

  然而在P2P应用中,内网主机(客户端)需要对另外的终端(Peer)直接建立链接,但是发起者和响应者可能在不同的中间件后面,两者都没有公网IP地址。而外部对NAT公网IP和端口主动的链接或数据都会因内网未请求被丢弃掉。本文讨论的就是如何跨越NAT实现内网主机直接通讯的问题。

2.术语
防火墙(Firewall):

  防火墙主要限制内网和公网的通讯,通常丢弃未经许可的数据包。防火墙会检测(但是不修改)试图进入内网数据包的IP地址和TCP/UDP端口信息。

网络地址转换器(NAT):

  NAT不止检查进入数据包的头部,而且对其进行修改,从而实现同一内网中不同主机共用更少的公网IP(通常是一个)。

基本NAT(Basic NAT):

  基本NAT会将内网主机的IP地址映射为一个公网IP,不改变其TCP/UDP端口号。基本NAT通常只有在当NAT有公网IP池的时候才有用。

网络地址-端口转换器(NAPT):

  到目前为止最常见的即为NAPT,其检测并修改出入数据包的IP地址和端口号,从而允许多个内网主机同时共享一个公网IP地址。

锥形NAT(Cone NAT):

  在建立了一对(公网IP,公网端口)和(内网IP,内网端口)二元组的绑定之后,Cone NAT会重用这组绑定用于接下来该应用程序的所有会话(同一内网IP和端口),只要还有一个会话还是激活的。

  例如,假设客户端A建立了两个连续的对外会话,从相同的内部端点(10.0.0.1:1234)到两个不同的外部服务端S1和S2。Cone NAT只为两个会话映射了一个公网端点(155.99.25.11:62000),确保客户端端口的“身份”在地址转换的时候保持不变。由于基本NAT和防火墙都不改变数据包的端口号,因此这些类型的中间件也可以看作是退化的Cone NAT。

对称NAT(Symmetric NAT)

  对称NAT正好相反,不在所有公网-内网对的会话中维持一个固定的端口绑定。其为每个新的会话开辟一个新的端口。如下图所示:

其中Cone NAT根据NAT如何接收已经建立的(公网IP,公网端口)对的输入数据还可以细分为以下三类:

1) 全锥形NAT(Full Cone NAT)

  在一个新会话建立了公网/内网端口绑定之后,全锥形NAT接下来会接受对应公网端口的所有数据,无论是来自哪个(公网)终端。全锥NAT有时候也被称为“混杂”NAT(promiscuous NAT)。

2) 受限锥形NAT(Restricted Cone NAT)

  受限锥形NAT只会转发符合某个条件的输入数据包。条件为:外部(源)IP地址匹配内网主机之前发送一个或多个数据包的结点的IP地址。受限NAT通过限制输入数据包为一组“已知的”外部IP地址,有效地精简了防火墙的规则。

3) 端口受限锥形NAT(Port-Restricted Cone NAT)

  端口受限锥形NAT也类似,只当外部数据包的IP地址和端口号都匹配内网主机发送过的地址和端口号时才进行转发。端口受限锥形NAT为内部结点提供了和对称NAT相同等级的保护,以隔离未关联的数据。

  1. P2P通信
      根据客户端的不同,客户端之间进行P2P传输的方法也略有不同,这里介绍了现有的穿越中间件进行P2P通信的几种技术。

3.1 中继(Relaying)

  这是最可靠但也是最低效的一种P2P通信实现。其原理是通过一个有公网IP的服务器中间人对两个内网客户端的通信数据进行中继和转发。如下图所示:

  客户端A和客户端B不直接通信,而是先都与服务端S建立链接,然后再通过S和对方建立的通路来中继传递的数据。这钟方法的缺陷很明显,当链接的客户端变多之后,会显著增加服务器的负担,完全没体现出P2P的优势。

3.2 逆向链接(Connection reversal)

  第二种方法在当两个端点中有一个不存在中间件的时候有效。例如,客户端A在NAT之后而客户端B拥有全局IP地址,如下图:

  客户端A内网地址为10.0.0.1,且应用程序正在使用TCP端口1234。A和服务器S建立了一个链接,服务器的IP地址为18.181.0.31,监听1235端口。NAT A给客户端A分配了TCP端口62000,地址为NAT的公网IP地址155.99.25.11,作为客户端A对外当前会话的临时IP和端口。因此S认为客户端A就是155.99.25.11:62000。而B由于有公网地址,所以对S来说B就是138.76.29.7:1234。

  当客户端B想要发起一个对客户端A的P2P链接时,要么链接A的外网地址155.99.25.11:62000,要么链接A的内网地址10.0.0.1:1234,然而两种方式链接都会失败。链接10.0.0.1:1234失败自不用说,为什么链接155.99.25.11:62000也会失败呢?来自B的TCP SYN握手请求到达NAT A的时候会被拒绝,因为对NAT A来说只有外出的链接才是允许的。

  在直接链接A失败之后,B可以通过S向A中继一个链接请求,从而从A方向“逆向“地建立起A-B之间的点对点链接。

  很多当前的P2P系统都实现了这种技术,但其局限性也是很明显的,只有当其中一方有公网IP时链接才能建立。越来越多的情况下,通信的双方都在NAT之后,因此就要用到我们下面介绍的第三种技术了。

3.3 UDP打洞(UDP hole punching)

  第三种P2P通信技术,被广泛采用的,名为“P2P打洞“。P2P打洞技术依赖于通常防火墙和cone NAT允许正当的P2P应用程序在中间件中打洞且与对方建立直接链接的特性。以下主要考虑两种常见的场景,以及应用程序如何设计去完美地处理这些情况。第一种场景代表了大多数情况,即两个需要直接链接的客户端处在两个不同的NAT之后;第二种场景是两个客户端在同一个NAT之后,但客户端自己并不需要知道。

3.3.1. 端点在不同的NAT之下

  假设客户端A和客户端B的地址都是内网地址,且在不同的NAT后面。A、B上运行的P2P应用程序和服务器S都使用了UDP端口1234,A和B分别初始化了与Server的UDP通信,地址映射如图所示:

  现在假设客户端A打算与客户端B直接建立一个UDP通信会话。如果A直接给B的公网地址138.76.29.7:31000发送UDP数据,NAT B将很可能会无视进入的数据(除非是Full Cone NAT),因为源地址和端口与S不匹配,而最初只与S建立过会话。B往A直接发信息也类似。

  假设A开始给B的公网地址发送UDP数据的同时,给服务器S发送一个中继请求,要求B开始给A的公网地址发送UDP信息。A往B的输出信息会导致NAT A打开一个A的内网地址与与B的外网地址之间的新通讯会话,B往A亦然。一旦新的UDP会话在两个方向都打开之后,客户端A和客户端B就能直接通讯,而无须再通过引导服务器S了。

  UDP打洞技术有许多有用的性质。一旦一个的P2P链接建立,链接的双方都能反过来作为“引导服务器”来帮助其他中间件后的客户端进行打洞,极大减少了服务器的负载。应用程序不需要知道中间件具体是什么(如果有的话),因为以上的过程在没有中间件或者有多个中间件的情况下也一样能建立通信链路。

3.3.2. 端点在相同的NAT之下

  现在考虑这样一种情景,两个客户端A和B正好在同一个NAT之后(而且可能他们自己并不知道),因此在同一个内网网段之内。客户端A和服务器S建立了一个UDP会话,NAT为此分配了公网端口62000,B同样和S建立会话,分配到了端口62001,如下图:

  假设A和B使用了上节介绍的UDP打洞技术来建立P2P通路,那么会发生什么呢?首先A和B会得到由S观测到的对方的公网IP和端口号,然后给对方的地址发送信息。两个客户端只有在NAT允许内网主机对内网其他主机发起UDP会话的时候才能正常通信,我们把这种情况称之为”回环传输“(lookback translation),因为从内部到达NAT的数据会被“回送”到内网中而不是转发到外网。例如,当A发送一个UDP数据包给B的公网地址时,数据包最初有源IP地址和端口地址10.0.0.1:1234和目的地址155.99.25.11:62001,NAT收到包后,将其转换为源155.99.25.11:62000(A的公网地址)和目的10.1.1.3:1234,然后再转发给B。即便NAT支持回环传输,这种转换和转发在此情况下也是没必要的,且有可能会增加A与B的对话延时和加重NAT的负担。

  对于这个问题,解决方案是很直观的。当A和B最初通过S交换地址信息时,他们应该包含自身的IP地址和端口号(从自己看),同时也包含从服务器看的自己的地址和端口号。然后客户端同时开始从对方已知的两个的地址中同时开始互相发送数据,并使用第一个成功通信的地址作为对方地址。如果两个客户端在同一个NAT后,发送到对方内网地址的数据最有可能先到达,从而可以建立一条不经过NAT的通信链路;如果两个客户端在不同的NAT之后,发送给对方内网地址的数据包根本就到达不了对方,但仍然可以通过公网地址来建立通路。值得一提的是,虽然这些数据包通过某种方式验证,但是在不同NAT的情况下完全有可能会导致A往B发送的信息发送到其他A内网网段中无关的结点上去的。

3.3.3. 固定端口绑定

  UDP打洞技术有一个主要的条件:只有当两个NAT都是Cone NAT(或者非NAT的防火墙)时才能工作。因为其维持了一个给定的(内网IP,内网UDP)二元组和(公网IP, 公网UDP)二元组固定的端口绑定,只要该UDP端口还在使用中,就不会变化。如果像对称NAT一样,给每个新会话分配一个新的公网端口,就会导致UDP应用程序无法使用跟外部端点已经打通了的通信链路。由于Cone NAT是当今最广泛使用的,尽管有一小部分的对称NAT是不支持打洞的,UDP打洞技术也还是被广泛采纳应用。

  1. 具体实现
      如果理解了上面所说的内容,那么代码实现起来倒很简单了 。这里采用C++的异步IO库来实现引导服务器和P2P客户端的简单功能,目的是打通两个客户端的通信链路,使两个不同局域网之间的客户端可以实现直接通信。

4.1 引导服务端设计

  引导服务器运行在一个有公网地址的设备上,并且接收指定端口的来自客户的命令(这里是用端口号2333)。

客户端其实可以而且也最好应该与服务器建立TCP链接,但我这里为了图方便,也只采用了UDP的通信方式。服务端监听2333端口的命令,然后执行相应的操作,目前包含的命令有:

login, 客户端登录,使得其记录在服务器traker中,让其他peer可以对其发出链接请求。

logout,客户端登出,使其对peer隐藏。因为服务器不会追踪客户端的登录状态。

list,客户端查看目前的登录用户。

punch , 对指定用户(序号)进行打洞。

help, 查看有哪些可用的命令。

4.2 P2P客户端设计

  一般的网络编程,都是客户端比服务端要难,因为要处理与服务器的通信同时还要处理来自用户的事件;对于P2P客户端来说更是如此,因为P2P客户端不止作为客户端,同时也作为对等连接的服务器端。

这里的大体思路是,输入命令传输给服务器之后,接收来自服务器的反馈,并执行相应代码。例如A想要与B建立通信链路,先给服务器发送punch命令以及给B发送数据,服务器接到命令后给B发送punch_requst信息以及A的端点信息,B收到之后向A发送数据打通通路,然后A与B就可以进行P2P通信了。经测试,打通通路后即便把服务器关闭,A与B也能正常通信。

具体的代码见 https://github.com/pannzh/P2P-Over-MiddleBoxes-Demo/tree/boost_asio, 另外还实现了posix c版本的 https://github.com/pannzh/P2P-Over-MiddleBoxes-Demo/tree/master

以及python版本的 https://github.com/pannzh/P2P-Over-MiddleBoxes-Demo/tree/python

Viewing all 532 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>