Category Archives: 未分类

未分类

移动终端网络接入

1.1 终端接入网络

1.1.1 移动终端接入网络有如下的几种情况

  • 终端设备在属地,终端设备通过基站,接入属地的网络
  • 终端设备在国内漫游时,移动设备会漫游连接到当地的网络,联通和电信则需要漫游回属地的网络
  • 终端设备在国外漫游时,需要漫游回属地的网络

1.1.2 基站接入分析

移动终端通过基站接入移动运营商网络,终端与基站之间是数据链路层,不涉及网络层(IP)和传输层(TCP),终端设备的IP地址是由运营商分配的,在切换基站时一般不会引起IP地址的变化

有以下几种情况:

  • 当设备进行重启、飞行模式切换等时,设备会重新发起接入,这时IP地址会发生改变
  • 设备在同一区域内切换基站的过程中,如果没有发生断网情况下,即没有重新接入,IP地址是不会变化的
  • 设备在区域间切换基站,比如联通设备从北京到河北,接入由北京联通变成河北联通,IP地址会发生变化

终端设备切换基站一般情况下可在50ms~200ms完成,TCP是基于连接的协议,连接状态由状态机来维护,连接完毕后,双方都会处于established状态,它们之间的连接由各自的IP和TCP的端口唯一标识,即使这个连接没有任何数据,但仍是保持连接状态。TCP的KeepAlive机制用于检测连接死活,一般时间为 7200 s,失败后重试 10 次,每次超时时间 75 s,以释放无效链接。这个时间比切换基站时间要大的多,因此TCP通道在切换基站时,其IP地址一般没有变化,所以基于IP和端口的已建立的TCP连接不会失效。

1.1.3 DNS解析

当前移动 DNS 的现状:

  • 运营商 LocalDNS 出口根据权威 DNS 目标 IP 地址进行 NAT,或将解析请求转发到其他DNS 服务器,导致权威 DNS 无法正确识别运营商的 LocalDNS IP,引发域名解析错误、流量跨网。
  • 域名被劫持的后果:网站无法访问(无法连接服务器)、访问到钓鱼网站等。
  • 解析结果跨域、跨省、跨运营商、国家的后果:网站访问缓慢甚至无法访问。

为了解决这些问题,通常TCP网关的地址可以通过HttpDNS技术获取,以避免DNS解析异常、域名劫持的问题。客户端直接访问HTTPDNS接口,获取服务最优IP,返回给客户端,客户拿到IP地址后,直接使用此IP地址进行连接。

1.2 接入层

接入层最靠近客户端,接入层一般使用LVS(DR模式)+VIP+HAProxy来实现,如果使用公有云也可以使用云服务提供的负载均衡服务,如使用腾讯云的CLB,阿里云的ALB,配置按TCP转发;有矿的话可以使用F5硬件来做接入层;保留这一层有如下好处:

  • 负载均衡:均衡客户端连接,尽量保证连接在连接服务器上均衡
  • 真实服务不需要公网IP,因为它不需要对外暴露IP地址,更安全
  • 会话保持

1.3 长连接服务器

长连接服务部署的机器关注以下几个配置项

nf_conntrack_max
nf_conntrack_max 决定连接跟踪表的大小,当nf_conntrack模块被装置且服务器上连接超过这个设定的值时,系统会主动丢掉新连接包,直到连接小于此设置值才会恢复。
Backlognet.core.somaxconn排队等待接受的最大连接数

net.core.netdev_max_backlog数据包在发送给cpu之ueej被网卡缓冲的速率,增加可以提高有高带宽机器的性能
文件描述符sys.fs.file-max允许的最大文件描述符 /proc/sys/fs/file-max

nofile应用层面允许的最大文件描述数 /etc/security/limits.conf
portsnet.ipv4.ip_local_port_range端口范围

net.ipv4.tcp_tw_reuse端口复用,允许time wait的socket重新用于新的连接,默认为0,关闭短连接设置为1

net.ipv4.tcp_tw_recycletcp连接中的time wait的sockets快速回收,默认为0,表示关闭

1.4 服务实现

1.4.1 认证:

验证终端身份,确保只有合法的终端才能够使用服务,流程如下

  1. 服务端生成设备私钥、公钥;私钥执久化到设备上,公钥保存在服务端
  2. 握手:客户端发起TCP连接,TCP连接建立成功后,服务端生成256字符随机字串 randomMsg,返回客户端
  3. 客户端登录:客户端拿出token+randomMsg,使用其私钥签名得到 secretChap,并把token、secretChap 通过TCP通道上报服务端
  4. 服务端验证:服务端使用设备公钥验证签名,并调用 Passport 服务验证 token,拿到用户信息;服务端配置用户、设备的路由信息
  5. 协商对称密钥:服务端验证后,生成并返回对称密钥 secureKey,返回的对称密钥 secureKey 使用终端的公钥加密,只有使用设备的私钥才可以解密;客户端解密对称密钥 secureKey,至此服务端和终端完成密钥协商,之后可以愉快并安全的通信了。

模拟代码:

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.RandomStringUtils;

import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;

public class AuthClientTest {
    private static final byte[] PARAM_IV;

    static {
        String PARAM_IV_CONFIG = Base64.encodeBase64String(new SecureRandom().generateSeed(32));
        PARAM_IV = Base64.decodeBase64(PARAM_IV_CONFIG);
    }

    private static class ClientStore {
        private static final byte[] PARAM_IV = AuthClientTest.PARAM_IV;
        String privateKey;
        String token;// 用户标识信息

        String randomMsg;
        // 对称密钥
        byte[] secretKey;
    }

    private static class ServerStore {
        private static final String SECRET_KEY = RandomStringUtils.random(32);
        private static final byte[] PARAM_IV = AuthClientTest.PARAM_IV;

        // 对称密钥
        byte[] secretKey = new SecureRandom().generateSeed(32);

        String publicKey;
        String randomMsg;
    }

    public static void main(String[] args) throws Exception {
        // s0 初始化,生成设备的公私钥
        SHA256SignUtil.RsaKeys rsaKeys = SHA256SignUtil.generateKeyBytes();
        ClientStore clientStore = new ClientStore();
        ServerStore serverStore = new ServerStore();
        // 发送私钥到客户端
        clientStore.privateKey = Base64.encodeBase64String(rsaKeys.getPrivateKey());
        // 公钥保存在服务器端
        serverStore.publicKey = Base64.encodeBase64String(rsaKeys.getPublicKey());
        // s1: 握手
        // s1.1 客户端连接服务端 app -> server
        serverStore.randomMsg = RandomStringUtils.randomAlphanumeric(256);
        // s1.2 TCP建立成功后 server -(randomMsg)-> app
        clientStore.randomMsg = serverStore.randomMsg;
        // s2 端侧签名登录 app -(token,secretChap:privateKey签名)-> server
        clientStore.token = genToken(1000L);
        Map<String, Object> data = new HashMap<>();
        data.put("token", clientStore.token);
        PrivateKey privateKey = SHA256SignUtil.restorePrivateKey(Base64.decodeBase64(clientStore.privateKey));
        byte[] secretChap = SecretChapUtils.createSecretChap(data, clientStore.randomMsg, privateKey);
        // s3 服务端验证登录
        PublicKey publicKey = SHA256SignUtil.restorePublicKey(Base64.decodeBase64(serverStore.publicKey));
        boolean verify = SecretChapUtils.verifySecretChap(data, serverStore.randomMsg, secretChap, publicKey);
        System.out.println(verify);
        // s4 服务端下发对称密钥 server -(secretKey:publicKey加密)-> app
        byte[] secureKey = SHA256SignUtil.encryptByPublicKey(serverStore.secretKey, publicKey.getEncoded());
        // s5 端侧解密对称密钥并存储
        clientStore.secretKey = SHA256SignUtil.decryptByPrivateKey(secureKey, privateKey.getEncoded());
        // s6 正常加密传输数据
        byte[] encrypt = AesUtil.encrypt("田加国是好人".getBytes(StandardCharsets.UTF_8), clientStore.secretKey, ClientStore.PARAM_IV);
        byte[] decrypt = AesUtil.decrypt(encrypt, serverStore.secretKey, ServerStore.PARAM_IV);
        String sourceData = new String(decrypt, StandardCharsets.UTF_8);
        System.out.println(sourceData);
    }

    private static String genToken(long uid) {
        HashMap<String, Object> claims = new HashMap<>();
        claims.put("iss", "user.tianjiaguo.com");
        claims.put("expire", System.currentTimeMillis() + 24 * 60 * 60 * 1000L * 30);
        claims.put("uid", uid);
        claims.put("type", 2);
        return Jwts.builder().setClaims(claims)
                .signWith(SignatureAlgorithm.HS256, ServerStore.SECRET_KEY.getBytes())
                .compact();
    }
}

1.4.2 连接保持

心跳机制+自适应心跳:

  1. 端侧定时发送心跳包,服务端重置心跳检查点
  2. 端侧会根据业务数据,决定是否、何时上报心跳包
  3. 心跳包频率可控

附录:

示例架构图

未完

未分类

博客网站搭建

最近把网站迁移到国内的云服务厂商上,之前几次迁移部署都是采用编译安装的方式,非常的不方便,此次采用的是docker方式搭建环境的配置,前端也不再采用nginx,采用了能够自动生成和替换证书的ca#ddy来实现的。

安装workpress

安装docker和docker-compose

# 安装docker
...
# 安装docker-compose
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

编写wordpress的docker-compose.yml文件

version: "3.9"
    
services:
  db:
    image: mysql:8.0.25
    volumes:
      - ~/docker/mysql/data:/var/lib/mysql
    # docker的重启策略:在容器退出时总是重启容器,但是不考虑在Docker守护进程启动时就已经停止了的容器
    restart: unless-stopped
    command:
      # MySQL8的密码验证方式默认是 caching_sha2_password,但是很多的连接工具还不支持该方式
      # 就需要手动设置下mysql的密码认证方式为以前的 mysql_native_password 方式
      --default-authentication-plugin=mysql_native_password
      --character-set-server=utf8mb4
      --collation-server=utf8mb4_general_ci
    environment:
      MYSQL_ROOT_PASSWORD: {数据库ROOT密码}
      MYSQL_DATABASE: {数据库名称}
      MYSQL_USER: {数据库普通用户名,可用来做wordpress的用户}
      MYSQL_PASSWORD: {数据库普通用户的密码}
      TZ: Asia/Shanghai
    ports:
      - 3306:3306
    volumes:
      - ~/docker/mysql/data:/var/lib/mysql
      - ~/docker/mysql/conf:/etc/mysql/conf.d
      - ~/docker/mysql/logs:/logs
    networks:
      - app-network
    
  wordpress:
    depends_on:
      - db
    image: wordpress:latest
    ports:
      - "8000:80"
    restart: always
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_NAME: {数据库名称}
      WORDPRESS_DB_USER: {数据库普通用户名,可用来做wordpress的用户}
      WORDPRESS_DB_PASSWORD: {数据库普通用户的密码}
    volumes:
      - ~/docker/wordpress:/var/www/html
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

本地需要创建mysql和wordpress的目录,docker会把本地创建的目录挂载到docker镜像中,这样mysql的数据和wordpress的配置才会在docker销毁时不会丢失。

mkdir mysql
mkdir mysql/conf
mkdir mysql/data
mkdir mysql/log
mkdir wordpress

启动docker镜像

docker-compose up

然后临时打云厂商的防火墙的8000端口,在本地看是docker是否能到安装配置页面,如果可以正常打开,说明wordpress已经安装配置完成,可以使用 docker-compose up -d 转到后台执行,我们现在需要把云厂商的防火墙关闭。

安装配置ca#ddy

编写执行脚本

#!/usr/bin/env sh

VERSION=2.4.1
IMAGE=caddy:${VERSION}
CMD=""

docker run -itd --restart=always \
  --name caddy \
  --net=host \
  -v $PWD/conf/Caddyfile:/etc/Caddyfile \
  -v $PWD/data:/data/caddy \
  -v $PWD/www:/opt/www \
  ${IMAGE} caddy run -config /etc/Caddyfile

编写Ca#ddy的配置文件

https://www.{域名}.com https://{域名}.com http://www.{域名}.com http://{域名}.com {

    tls internal {
        on_demand
    }

    route /* {
        reverse_proxy * localhost:8000 {
            header_up -Origin
        }
        encode * {
            gzip
            zstd
        }
    }

    log {
        output stdout
    }

}

预创建目录

mkdir caddy
mkdir caddy/conf
mkdir caddy/data
mkdir caddy/www

执行脚本启动服务器,服务启动后我们在本地机器上可以通过配置hosts的方式,连接上去,看是否能打开wordpress的安装页面。

配置证书

我们使用cloudflare的证书,需要先到其官方网站上注册 https://dash.cloudflare.com/login 账号。

接下来我们需要在网站上添加我们的网站

然后到自己的域名服务商修改域名服务器为cloudflare的,等cloudflare检测通过

# 域名服务器
braden.ns.cloudflare.com
shubhi.ns.cloudflare.com

接下来需要拿到我们的api key,点击cloudflare右上角 “我的个人资料” – “API令牌“

配置ca#ddy

在服务器 caddy/conf 文件夹下编写文件 caddy.service 文件内容为:

[Service]
Environment=CLOUDFLARE_EMAIL={你的邮箱地址}
Environment=CLOUDFLARE_API_KEY={刚拿到的cloudflare密钥}

接下来重启ca#ddy

docker restart caddy

此时用chrome打开网站,查看地址栏左侧是否提示锁的图标,如果不是也不用着急,有可能是域名解析还没有完全切换到新的域名服务器,这需要一定的时间。

未分类

qcow2镜像定制指南

背景

目前网络上关于定制镜像的说明很分散,需要搜寻很多文章才能完成镜像的定制任务。所以我尝试提供一个全面而系统的指南,遵循本指南,用户可以方便的完成镜像的定制。

实施步骤

一、环境配置

1、准备软件
mac pro、VmWare fusion、CentOS-7-x86_64-DVD-1708.iso、CentOS-7-x86_64-GenericCloud-1708-20180123.qcow2
2、安装嵌套CentOs环境
由于MacOs不支持Kvm,故需要在嵌套的操作系统中安装云镜像需要的软件,使用Fusion很容易在MacOs中虚拟出一个CentOs的环境。
3、修改嵌套操作系统配置
在centos关闭的情况下打开虚拟机“处理器和内存”配置,选择“高级配置”,选中“在此虚拟机中启用虚拟化管理程序”和“在此虚拟机中启用代码分析应用程序”,如无这步操作,则在启动virt-manager时会报:“
virt-manager 报 WARNING : KVM不可用.这可能是因为没有安装KVM软件包,或者没有载入KVM内核模块.您的虚拟机可能性很差。”的错误,启动虚拟机。以下操作如无特殊说明都是在嵌套操作系统中执行。
4、安装依赖

yum install qemu-kvm qemu-img qemu-kvm-tools qemu-kvm-common
yum install libvirt-admin libvirt-client libvirt-daemon libvirt-devel libvirt
yum install libguestfs libguestfs-tools libguestfs-bash-completion

5、编译nbd内核模块(如不使用“nbd挂载方式修改镜像”则不需要安装此模块)
执行命令,出现以下报错时,说明没有nbd模块,需要自己手动安装





执行下面的命令安装hbd模块

[@xx] # cat /etc/redhat-release
CentOS Linux release 7.4.1708 (Core)
[@xx] # uname -a
Linux localhost 3.10.0-693.el7.x86_64 #1 SMP Tue Aug 22 21:09:27 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
[] # uname -r
3.10.0-693.el7.x86_64

安装kernel组件

[@xx] sudo yum install kernel-devel kernel-headers

编译安装hbd组件

# 下载对应的内核源码包
[@xx] # wget http://vault.centos.org/7.4.1708/os/Source/SPackages/kernel-3.10.0-693.el7.src.rpm
[@xx] # rpm -ihv kernel-3.10.0-693.el7.src.rpm
[@xx] # cd /root/rpmbuild/SOURCES/
[@xx] # tar Jxvf linux-3.10.0-123.el7.tar.xz -C /usr/src/kernels/
[@xx] # cd /usr/src/kernels/
# 配置源码
[@xx] # mv $(uname -r) $(uname -r)-old
[@xx] # mv linux-3.10.0-693.el7 $(uname -r)
# 编译安装
[@xx 3.10.0-693.el7.x86_64] # cd $(uname -r)
[@xx 3.10.0-693.el7.x86_64] # make mrproper
[@xx 3.10.0-693.el7.x86_64] # cp ../$(uname -r)-old/Module.symvers ./
[@xx 3.10.0-693.el7.x86_64] # cp /boot/config-$(uname -r) ./.config
[@xx 3.10.0-693.el7.x86_64] # make oldconfig
[@xx 3.10.0-693.el7.x86_64] # make prepare
[@xx 3.10.0-693.el7.x86_64] # make scripts
[@xx 3.10.0-693.el7.x86_64] # make CONFIG_BLK_DEV_NBD=m M=drivers/block
[@xx 3.10.0-693.el7.x86_64] # cp drivers/block/nbd.ko /lib/modules/$(uname -r)/kernel/drivers/block/
[@xx 3.10.0-693.el7.x86_64] # depmod -a
# 查看nbd模块
[@xx 3.10.0-693.el7.x86_64]$ modinfo nbd
filename:       /lib/modules/3.10.0-693.el7.x86_64/kernel/drivers/block/nbd.ko
license:        GPL
description:    Network Block Device
rhelversion:    7.4
srcversion:     EDE909A294AC5FE08E81957
depends:        
vermagic:       3.10.0 SMP mod_unload modversions 
parm:           nbds_max:number of network block devices to initialize (default: 16) (int)
parm:           max_part:number of partitions per device (default: 0) (int)
parm:           debugflags:flags for controlling debug output (int)

编译安装时的错误处理
阶段:make CONFIG_BLK_DEV_NBD=m M=drivers/block

drivers/block/nbd.c: 在函数‘__nbd_ioctl’中:
drivers/block/nbd.c:619:19: 错误:‘REQ_TYPE_SPECIAL’未声明(在此函数内第一次使用)
   sreq.cmd_type = REQ_TYPE_SPECIAL;
                   ^
drivers/block/nbd.c:619:19: 附注:每个未声明的标识符在其出现的函数内只报告一次
make[1]: *** [drivers/block/nbd.o] 错误 1
make: *** [_module_drivers/block] 错误 2

处理:

[@xx 3.10.0-693.el7.x86_64] # vim include/linux/blkdev.h
# 由代码可知 REQ_TYPE_SPECIAL = 7
/*
 * request command types
 */
enum rq_cmd_type_bits {
        REQ_TYPE_FS             = 1,    /* fs request */
        REQ_TYPE_BLOCK_PC,              /* scsi command */
        REQ_TYPE_SENSE,                 /* sense request */
        REQ_TYPE_PM_SUSPEND,            /* suspend request */
        REQ_TYPE_PM_RESUME,             /* resume request */
        REQ_TYPE_PM_SHUTDOWN,           /* shutdown request */
#ifdef __GENKSYMS__
        REQ_TYPE_SPECIAL,               /* driver defined type */
#else
        REQ_TYPE_DRV_PRIV,              /* driver defined type */
#endif
        /*
         * for ATA/ATAPI devices. this really doesn"&gt;cmd[0] with the range of driver
         * private REQ_LB opcodes to differentiate what type of request this is
         */
        REQ_TYPE_ATA_TASKFILE,
        REQ_TYPE_ATA_PC,
};
# 修改nbd.c文件
[@xx 3.10.0-693.el7.x86_64] # vim drivers/block/nbd.c
# sreq.cmd_type = REQ_TYPE_SPECIAL;
sreq.cmd_type = 7;
# 重新执行命令
[@xx 3.10.0-693.el7.x86_64] # make CONFIG_BLK_DEV_NBD=m M=drivers/block

二、设置镜像共享

设置嵌套虚拟机文件夹共享
qcow2文件放置在mac本地文件夹中,嵌套虚拟机通过文件共享的方式使用qcow2文件。需要注意的是qcow2文件权限需要在macos中设置为可读写,否则在嵌套虚拟机中无法更新配置。

[[email protected]] # chmod 755 ./CentOS-7-x86_64-GenericCloud-1708-20180123.qcow2

嵌套虚拟机中,需要要关闭SeLinux否则同样无法更新镜像内容

sudo /usr/sbin/setenforce 0

三、guestfish工具使用

1、示例程序:获取镜像ip地址

guestfish --rw -a ./CentOS-7-x86_64-GenericCloud-1708-20180123.qcow2
# run
# list-filesystems
/dev/sda1: xfs
# mount /dev/sda1 /
# vi /var/log/cloud-init.log

2、示例程序:配置用户访问权限

guestfish --rw -a ./CentOS-7-x86_64-GenericCloud-1708-20180123.qcow2
# run
# list-filesystems
/dev/sda1: xfs
# mount /dev/sda1 /
# mkdir /home/dc2-user/.ssh
# 注意guestfish与shell的区别,权限位是四位
# chown 1001 1001 /home/dc2-user/.ssh
# chmod 0700 /home/dc2-user/.ssh
# touch /home/dc2-user/.ssh/authorized_keys
# chmod 0600 /home/dc2-user/.ssh/authorized_keys
# chown 1001 1001 /home/dc2-user/.ssh/authorized_keys
# ll /home/dc2-user/.ssh/authorized_keys
-rw------- 1 1001 1001 0 Mar  1 10:05 /sysroot/home/dc2-user/.ssh/authorized_keys
# vi /home/dc2-user/.ssh/authorized_keys
# 添加公钥...
# quit

四、nbd挂载方式修改镜像(qemu-nbd)

1、确保已安装nbd模块,加载模块

# modinfo nbd
# insmod /lib/modules/3.10.0-693.el7.x86_64/kernel/drivers/block/nbd.ko max_part=8

3、建立nbd连接,挂载到目录

# qemu-nbd -c /dev/nbd0 ./CentOS-7-x86_64-GenericCloud-1708-20180123.qcow2
# mkdir -p /mnt/qcows/qcow0
# mount /dev/nbd0p1  /mnt/qcows/qcow0

4、执行chroot

# chroot /mnt/qcows/qcow0/

5、执行修改,比如

$ passwd dc2-user
# 或其它操作

6、修改完毕后解除挂载点,解除连接

# umount /mnt/qcows/qcow0
# qemu-nbd -d /dev/nbd0p1

五、通过virt-manager挂载虚拟机

1、执行

virt-manager

2、新建虚拟机
选择“导入现有磁盘”,“使用ISO镜像”,选择qcow2文件…
如果报:“WARNING : KVM不可用.这可能是因为没有安装KVM软件包,或者没有载入KVM内核模块.您的虚拟机可能性很差。”的警告相应的解决方案是:

  • 关闭虚拟机
  • 进入虚拟机设置(可以配置网卡,硬盘,光驱的地方)
  • 点击“处理器和内存”,勾选虚拟化Inter VT-x/EPT 或AMD-V/RVI(V)

3、登录机器,修改
IP地址和密码、公钥方式登录,在前面已说明如何操作

六、清理痕迹

1、清理/var/log/文件夹
2、删除cloud-init执行记录
cloud-init是专门为云环境的虚拟机初始化而开发的工具,通过读取相应的数据,对虚拟机进行配置。其执行完毕后会在一个叫 sem 的目录下创建信号文件,以便下次启动模块时不再重复执行语句。其目录位置为:
/var/lib/cloud/instances/实例ID/sem

sudo rm -rf /var/lib/cloud/instances
sudo rm -rf /var/lib/cloud/instance

3、清理history和.ssh目录等

rm -rf /home/xxx/.ssh
echo '' # /home/xxx/.bash_history
echo '' # /root/.bash_history
 
rm -rf /root/.oracle_jre_usage

七、去除磁盘空洞

# 创建同样大小的镜像
$ qemu-img create -f qcow2 CentOS-7-x86_64-GenericCloud-1708-20180329.qcow2 40G
$ virt-sparsify -x ./CentOS-7-x86_64-GenericCloud-1708-20180123.qcow2 --convert qcow2 ./CentOS-7-x86_64-GenericCloud-1708-20180329.qcow2
$ du -sh *
7.3G	CentOS-7-x86_64-GenericCloud-1708-20180123.qcow2
5.3G	CentOS-7-x86_64-GenericCloud-1708-20180329.qcow2

未分类

Sqoop源码分析

Sqoop的Mysql数据导出实现分两种,一种是使用JDBC方式从Mysql中获取数据,一种是使用MysqlDump命令从MySql中获取数据,默认是 JDBC方式获取数据,如果要使用dump方式获取数据,需要添加 -direct 参数。

使用JDBC方式从Mysql中获取数据

配置语句时,需要添加 $CONDITIONS 点位符,比如:SELECT id FROM user WHERE $CONDITIONS,Sqoop在内部实现时会把它替换成需要的查询条件。

Sqoop启动后会先查询元数据,它会把 $CONDITIONS 替换为 (1=0) ,然后用得到的SQL语句查询数据表对应的Meta信息对于导出一个表的情况,Sqoop会使用这个SQL查询三次数据库,分别是: 1、获取 colInfo(最终得到columnTypes信息)2、查询ColumnNames信息3、生成QueryResult类执行 generateFields操作获取columnTypeNames时。

Sqoop会对获取的Fields做校验,列不能重复,它还会处理数据库的字段到Java属性名的转换

QueryResult类是通过构建java类文件,然后获取JavaCompiler,然后编译加载,为了提高处理性能,不是使用反射实现的,这个生成类内部处理mysql到hdfs属性值为空和分隔符的处理。

接着它会进行下面一个Sql查询操作,查询结果集为MIN(split列),MAX(split列),查询条件的处理逻辑为 $CONDITIONS 替换为(1=1),然后再添加外面SELECT查询 (举例:SELECT MIN(id), MAX(id) FROM (SELECT ID,NAME,PASSPORT WHERE (1=1) ) AS t1 ),这样就查询出来此次导出数据最大的split列值和最小的split列值。

对于为整数、布尔值、时间格式、Float等 的分区列,进行split时直接根据对应值的大小进行Split,Text文本的处理方式比较特殊,Sqoop先会对之前获取到的Min和Max的字串寻找它们最大的相同前缀子字串,然后把后面的字段转化为BigDecimal,结合时char占两个字节(65536),算法在 TextSplitter类中,比较简单,本质上就是一个进制转换。拆分好后,需要把Split的值再转换为String,然后加上相同前缀子字段,就构成了查询区间了(注意中文可能会被拆分)。

Sqoop对数据的获取是在DataDrivenDBRecordReader中,在查询时会把 $CONDITIONS 替换成 split 的范围比如 ( id >= 1) && (id<10),使用JDBC获取到结果集游标,然后移动游标处理数据。

使用MysqlDump命令从MySql中获取数据

第二种方法与第一种方式有下面的差别:

初始化元数据,它是在构建的查询语句后面添加 limit 1 ,比如:SELECT t. FROM AS t LIMIT 1,因为dump方式在查询指定获取列时使用的是 t.,当使用limit 0时,数据库不会给它返回必须的元数据信息。

dump方式在map进行数据的获取,其会构建mysqldump命令,然后使用java程序调用,获取输入输出流和错误流,其实现了 org.apache.sqoop.util.AsyncSink 抽象类,用来处理输入输出流和错误流。

优化策略:

Sqoop在执行时会执行三次相同的Sql语句用来查询无数据,可以合并查询,由于查询仅返回Meta信息很快,不需要修改这块的实现。

分区列选择对于查询元数据和导出的查询影响很大,应该对索引做调优,避免对分区列的排序操作,加快元数据查询速度和导出数据的速度,尽量选择自增加的主键ID做Split列,区分度好并且可以顺序读取数据。

导出操作的查询语句中,$CONDITIONS 会被替换为范围区间,创建索引时,要考虑做这个查询的优化。

索引建议,考虑三个规则(使查询数据集较少、减少点的查询、避免排序操作),Sqoop场景下,如果分区列不是主键(自增加)时,把分区列做为联合索引的第一个字段,其它被选择的查询条件做为索引的其它字段。

分区列的选择,要避免Split后数据不均衡。

从实现上来看-m参数是可以增加任务的并行度的,但数据库的读线程是一定的,所以 -m 过大对于数据库和hadoop集群会有压力,在Sqoop的场景下,数据库是一个影响并发的瓶颈。增加mapper数意义不大。

下面列出Sqoop目前1.4.6版本存在的两个问题。

查看Sqoop源码,发现其存在两个比较严重的问题。

问题 1、数据分片与Mapper设定不合理

Sqoop在抽取时可以指定 -m 的参数来控制mapper的数量的,sqoop根据mapper的数量,对数据库数据进行分片,每个分片交给一个mapper处理,mapper在处理的过程中会通过jdbc或mysqldump对相应分片的数据进行查询和拉取,最终产生和mapper数量相同的文件数。

当每天需求导入hdfs中的表非常大时,过小的mapper值,会导致写入hdfs中的文件过大,如果文件的格式不能被split,那么会对业务下游产生影响。过大的mapper值,在数据库读线程一定,网络带宽一定的情况下,并不能提高导入hdfs的效率,反而会对数据库造成并发读压力,同时过多的mapper也会占用hadoop集群的资源。

Sqoop做为一个数据导出框架,对数据的控制应该再细至一些,-m 只是控制mapper的数量,而数据分片数目应该由另外的参数控制。

我们为sqoop添加了 -split-per-map 参数,比如设置-m=4 -split-per-map=2,则对结果集分 8 片,每个Mapper处理两片数据,最后共产生 8 个文件。

问题 2、分片效率低

Sqoop在做分片处理时有问题,其实现会使用 “Select Max(splitKey),Min(splitKey) From ( –select参数 ) as t1” 的语句来查询分片信息,在Mysql下,这样的查询会产生一个以split-id为主键,包含需要导出的其它所有字段的临时表,如果数据量不大,临时表数据可以在内存中,处理速度还可以保证。但如果数据量很大,内存中已经存放不下时,这些数据会被保存为MyISAM表存放到磁盘文件中,如果数据量再大一些,磁盘文件已经存放不下临时表时,拆分数据会失败。

在我们场景下,由于表数据量很大,这个查询占到整个导出时间的45%(整个过程原来需要90多分钟),优化空间很大。

解决方案一:

修改所有导出脚本,分片语句自定义

解决方案二:

修改:org.apache.sqoop.mapreduce.DataDrivenImportJob的

@Contract(“null, _ -&gt; !null”)
private String buildBoundaryQuery(String col, String query)

修改代码如下

org.apache.sqoop.mapreduce.DataDrivenImportJob的
 
@Contract("null, _ -> !null") 
private String buildBoundaryQuery(String col, String query)
 
/**
   * Build the boundary query for the column of the result set created by
   * the given query.
   * @param col column name whose boundaries we're interested in.
   * @param query sub-query used to create the result set.
   * @return input boundary query as a string
   */
  private String buildBoundaryQuery(String col, String query) {
    if (col == null || options.getNumMappers() == 1) {
      return "";
    }
 
    // Replace table name with alias 't1' if column name is a fully
    // qualified name.  This is needed because "tableName"."columnName"
    // in the input boundary query causes a SQL syntax error in most dbs
    // including Oracle and MySQL.
    String alias = "t1";
    int dot = col.lastIndexOf('.');
    String qualifiedName = (dot == -1) ? col : alias + col.substring(dot);
 
    ConnManager mgr = getContext().getConnManager();
    String ret = mgr.getInputBoundsQuery(qualifiedName, query);
    if (ret != null) {
      return ret;
    }
 
//    return "SELECT MIN(" + qualifiedName + "), MAX(" + qualifiedName + ") "
//        + "FROM (" + query + ") AS " + alias;
    return initBoundaryQuery(qualifiedName, query, alias);
  }
 
  private String initBoundaryQuery(String qualifiedName, String query, String alias) {
    StringBuilder regex = new StringBuilder();
    regex.append("(\\s[A|a][S|s][\\s][`]?");
    for (char c : qualifiedName.toCharArray()) {
      regex.append('[').append(c).append(']');
    }
    regex.append("[`|\\s|,])");
    final Matcher matcher1 = Pattern.compile(regex.toString()).matcher(query);
    final boolean asCheckOk = !matcher1.find();
    if(asCheckOk) {
      final Matcher matcher2 = Pattern.compile("(\\s[F|f][R|r][O|o][M|m]\\s)").matcher(query);
      int count = 0;
      while (matcher2.find()) {
        count++;
      }
      boolean fromCheckOk = count == 1;
      if(fromCheckOk) {
        final Matcher matcher = Pattern.compile("(\\s[F|f][R|r][O|o][M|m]\\s[\\s\\S]*)").matcher(query);
        while (matcher.find()) {
          return "SELECT MIN(" + qualifiedName + "), MAX(" + qualifiedName + ") "
                  + matcher.group();
        }
      }
    }
    return "SELECT MIN(" + qualifiedName + "), MAX(" + qualifiedName + ") "
            + "FROM (" + query + ") AS " + alias;
  }

问题三:无法自定义特殊字符替换

解决方案一:

通过SQL的替换功能,修改脚本代价高,并且脚本可读性、可维护性都大大降低

解决文案二:

修改Sqoop实现,增加自定义特殊字符替换功能

未分类

Camus源码分析

协议:

输出文件压缩:Camus默认只支持两种压缩格式(snappy和deflate),默认是defalte,使用 StringRecordWriterProvider写入文本格式文档时,还可以指定gzip的压缩格式,扩展其它压缩格式很容易,只需要添加 两行代码就可以,建议增加lzo和lzop的压缩格式,以和我们Hive保持一致。

输出格文件类型:建议文本格式的文件

文件目录规则:(配置的目录)+ topic名 + daily|hour + (年/月/日)|(年/月/日/小时) + 数据文件,例如:/rocketmq/data/vip_ods_heartbeat/daily/2015/06/10 /vip_ods_heartbeat.broker-a.0.999.48388735.1433865600000.deflate

文件名规则:topic名+ (RocketBrokerId)|(kafka的对应分区的learder的BrokerId)+ (RocketQueueId)|(kafka分区号)+ 写入消息行数 + 最后一条消息的Offset + 编码的分区(时间 + 压缩格式后缀),例如:vip_ods_heartbeat.broker- a.0.999.48388735.1433865600000.deflate

Topic的命名规则:业务标识+数据库名+数据库表名(分表只需要BaseName就可以),例 如:vip_ods_heartbeat

消息格式:操作类型\t表名(分表的话是分表名)\t数据库名\t主键名\t唯一索引\tBinlog日志时间 \tCheckPoint字段\tDataBefore\tDataAfter,库名表名都是RockMQ中的原始数据,在生成列数据时,列中数 据如果有\t等特殊字符需要替换,例如:
insert\theartbeat\tvip_ods\tid\tname,pid\t1232132131\[email protected]\t{“字段名”,”字段 值”,…}\t{“字段名”,”字段值”,…}

确定数据导入是否完成:

  • Camus中会在History的目录中存放历次消费的状态,包括开始执行的分区和它们的Offset、 执行结束位置的分区和它们的Offset,这两个文件以SequenceFile的形式存放在HDFS文件中
  • Camus在执行结束后可以把 执行信息汇总发送到Kafka的Topic中,Topic的名字为:TrackingMonitoringEvent,如果监控程序监控这个 Topic,是可以得到当前执行的情况的信息的。

未分类

理解索引而不需要理解数据结构

Understanding Indexing

Without Needing to Understand Data Structures

MySQL UC 2011 – April 12, 2011

Zardosht Kasheff

什么是表?

(key,value)对集合的词典

  1. 确保你可以 修改 这个词典(插入、删除、修改)和 查询(点查询、范围查询)词典
  2. B-Tree和Fractal Tree是两个词典的例子
  3. 哈希则不是(不支持范围查询)

例子:

CREATE TABLE foo (a INT, b INT, c INT, PRIMARY KEY(a));

然后我们插入一批数据

abc
100545
101922
1565645
16562
19820256
20623252
256562
4124345

一个key定义了词典的排序规则

1、对于数据结构和存储引擎我们会认为,在排序上进行范围查询是 快速 

2、在其它顺序上的范围查询会进行表的扫描,这个操作是 很慢 

3、点查询需要检索一个特定的值也是 慢 

一个点查询是快速的,但是读取一批行用这种方法会比使用按顺序的范围查询慢2个数量级

词典 T 上的索引 I 也是一个词典

1、同样我们需要定义一个(key,value)对

2、索引上的key是主词典上的列的子集

3、索引I的值是T的主KEY

还有其它的方法去定义这个值,但我们还是坚持使用T的主KEY来定义这个值


例子:

ALTER TABLE foo ADD KEY(b);

然后我们得到

问题:COUNT( * ) WHERE a<120;

问题:COUNT( * ) WHERE b>50;

问题:SUM( c ) WHERE b >50;

索引的好处?

1、索引使得查询变得快速

索引会提升部分查询请求的速度

2、需要在心里根据查询设计索引

选出最重要的查询给它们设计索引

考虑索引本身的代价

设计一个好的索引有3个规则可以参考

1、避免任何数据结构的细节

B树和分形树对于计算机科学家来说是有趣和好玩的,但这三个规则同样适用于其它数据结构

所有我们需要考虑的是范围查询是快速的(对每行)而点查询则慢的多(对行)

2、世界上没有绝对的规则

索引像是一个数据问题

规则有帮助,但每个方案都有自己的问题,需要分析解决问题的方法

3、也就是说规则有很大的帮助

三个规则

1、查询较少的数据

少的带宽,少的处理…

2、减少点查询

数据访问成本是不一样的

顺序访问数据比无序访问  **快的多**

3、避免排序

GROUP BY和ORDER BY查询需要后期的检索工作

索引在这种查询中可以帮助获得RowID

我们来分别看下这三种规则

规则1:查询较少的数据

Rule1:慢查询的例子

表:(100万行数据,没有索引)

CREATE TABLE foo (a INT, b INT, c INT);

查询(匹配1000行数据)

SELECT SUM(c) FROM foo WHERE b=10 and a<150;

查询计划:

行b=10 AND a<150可以在表的任何地方

没有索引的帮助,整个表都会被扫描

执行速度慢:

检索100万行数据仅仅查询1000行数据


Rule1:如何去添加一个索引

1、我们应该怎么去做?

减少检索到的数据
分析远少于100万的数据行

2、怎样做(对于一个简单的查询)?

设计索引时着眼于WHERE子句
    由查询关注那些行来决定
    其它行的数据对于这个查询来说并不重要

对于查询 SELECT SUM( c ) FROM foo WHERE b=10 and a<150;


Rule1:那个索引?

选择1:Key(a)

选择2:Key(b)

那个更好?由select来决定:

如果WHERE a<150的数据行更少,key(a)更好

如果WHERE b=10的数据更少,key(b)更好

选择3:key(a) AND key(b),然后MERGE我们稍后会进行讨论


Rule1:选择最好的索引

key(a)与key(b)都不是最佳的

考虑:

  • WHERE a<150有20万行数据
  • WHERE b=10有10万行数据
  • WHERE b=10 AND a<150有1000行数据

然后key(a)和key(b)都会检查很多的数据

为了获得更好的性能,索引必须尝试尽量优化WHERE条件

我们需要复合索引

复合索引可以减少数据检索

WHERE条件:b=5 AND a<150

选择1:key(a, b)

选择2:key(b, a)

问题又来了那种选择更好?

Key(b, a)!

索引规则:

当创建一个复合索引,需要对每个列进行检查,条件b是相等判断,但在a上不是。


问题:WHERE b=5 and a>150;

复合索引:没有平等的条件

如果WHERE条件是这样的:

WHERE a>100 AND a<200 AND b>100;

那个更好?

key(a),key(b),key(a,b),key(b,a)?


索引规则:

只要复合索引不被用于相等查询,复合索引的其它部分不会减少数据检索量

key(a,b)不再比key(a)好

key(b,a)不再比key(b)好


问题:WHERE b>=5 AND a>150;

复合索引:另一个例子

WHERE条件:b=5 AND c=100

key(b,a,c)和key(b)一样好
因为a没有在条件中使用,所以在索引中包含c并不会有帮助,key(b,c,a)会更好

规则1:总结

根据查询条件设计复合索引

把相等条件的查询列放在复合索引的开始位置

保证第一个非相等条件的列在索引中越有选择性越好

如果复合索引的第一个列没有被相等条件来使用,或者没有在条件中使用,复合索引的其它列对于减少数据的检索没有帮助

    是否就表明它们是无用的呢?

    它们在规则2中可能有用

规则2:避免点查询

表:

CREATE TABLE foo (a INT,b INT,c INT,PRIMARY KEY(a), KEY(b);

查询:

SELECT SUM( c ) FROM foo WHERE b>50;

查询计划:使用key(b)

因为随机的点查询的原因,对每个行都进行检索的代价很大

问题:SUM( c ) WHERE b>50;

还是这张表,但查询计划不同了

SUM(c) WEHRE b>50;

查询计划:扫描主表

每行的检索成本低

但是需要检索很多行

问题:SUM( c ) WEHRE b>50;


如果我们添加了另一个索引会怎么样?

如果添加key(b, c)呢?

由于我们在b上有索引,我们只检索我们需要的行

由于索引包含C的信息,我们不再需要再去检索主表了。  **没有点查询了**

覆盖索引:索引覆盖一个查询,如果这个索引包含足够的信息来回答这个查询。

例子:

问:SELECT SUM( c ) FROM foo WHERE b<100;

问:SELECT SUM( b ) FROM foo WHERE b<100;

索引:

key(b, c): 对第一个查询是覆盖索引

key(b, d):对第二个查询是覆盖索引

key(b, c, d):对每个索引都是覆盖索引


如何去构建一个覆盖索引

把检索的每个列都包含进去,并不仅仅是查询条件


问:SELECT c,d FROM foo WHERE a=10 AND b=100;

错误:ADD INDEX(a, b)

并不是覆盖索引。仍然需要点查询去检索c和d的值

正确:ADD INDEX(a, b, c, d)

包含所有相关的列

按照规则1规定把 a 和 b 放在索引开始位置

如果主键匹配WHERE条件呢?


问题:SELECT sum(c) FROM foo WHERE b>100 AND b<200;

SCHEMA:CREATE table foo (a INT, b INT, c INT, ai INT AUTO_INCREMENT, PRIMARY key(b, ai));

查询在主词典上做了范围查询

只有一个词典会按顺序访问到

这个查询很快


主键覆盖所有查询

如果排序规则匹配查询条件,问题得到解决

什么是聚簇索引


如果主键不匹配查询条件呢?

理想的情况下,必须确保辅助索引包含所有列

存储引擎不会让你去这样做

有一个例外。。。。TokuDB可以

TokuDB允许你定义任何聚簇索引

一个聚簇索引包含所有的查询,就像主键做的一样

聚簇索引的实践


问:SELECT SUM(c) FROM foo WHERE b<100;


问:SELECT SUM(b) FROM foo WEHRE b>200;


问:SELECT c,e FROM foo WEHRE b=1000;


索引:

key(b, c):第一个查询

key(b, d):第二个查询

key(b, c, e):第一个和第三个查询

key(b, c, d, e):所有的三个查询


索引需要大量的查询分析


考虑那个会涵盖所有的查询:

聚簇索引 b


聚簇索引可以让你更加关注于查询条件

它们减少了点查询并且使查询更加快

聚簇索引更多的信息:索引合并


例子:

CREATE TABLE foo (a INT, b INT, c INT);

SELECT SUM(c) FROM foo WHERE b=10 AND a<150;


假设:

a<150有20万行数据

b=10有10万行数据

b=10 AND a<150有1000行数据


如果我们使用key(a)和key(b)然后把结果集合合并会怎样?


合并计划:

查询20万行数据从key(a)中,where a<150

查询10万行数据从key(b)中,where b=10

合并结果集合,然后找到查询标识的1000行数据

执行1000行数据的点查询去得到c


这比没有索引好一点

和没有索引相比,减少了扫描行的数量

和没有合并相比,减少了点查询的数量

那么聚簇索引会对合并有帮助么?


考虑key(a)是个聚簇索引


查询计划:

扫描key(a)20万行数据,where a<150

扫描结果集合得到b=10的结果

使用得到的1000行的数据去检索C的值


一次得到,没有点查询


更好的选择还有没有?

聚簇索引(b, a)!

规则2总结:

避免点查询

确保索引覆盖查询

包含查询涉及到的所有列,并不仅仅是查询条件中的

使用聚簇索引

使用聚簇索引包含所有查询
允许用户把关注点集中在查询条件上
给多个查询提速,包括还没有预见到的查询--简化数据库设计

规则3:避免排序


简单的查询不需要后续的处理

select * from foo where b=100;


仅仅取得数据然后返回给用户


复杂的查询需要对数据进行后续的处理

GROUP BY 和 ORDER BY 会排序数据


选择正确的索引可以避免这些排序的步骤

考虑:


问:SELECT COUNT(c) FROM foo;


问:SELECT COUNT(c) FROM foo GROUP BY b, ORDER BY b;


查询计划1:

当进行表扫描时,给C计数


查询计划2:

扫描表把数据写到临时表中

对临时表按B进行排序

重新扫描排序后的数据,对每个b,使用c进行计数


如果我们使用key(b, c)会怎么样呢?

通过添加国所有需要的字段,我们覆盖了查询。 快速

通过提前对B进行排序,我们避免了排序 快速


总结:

通过给GROUP BY或者ORDER BY使用预先排序了的索引

把它们都放到一起:

简单查询

*

SELECT COUNT(

) FROM foo WHERE c=5, ORDER BY b;**


key(c, b):

把c放在索引第一个位置去减少行检索 R1

然后通过剩余的行排序去避免排序 R3

因为相等的检查在c上,所以剩余的行数据会被按b排好序


SELECT SUM(d) FROM foo WHERE c=100, GROUP BY b;


key(c, b, d):

c在索引的第一个位置可以减少行数据的检索 R1

然后其它的数据在b上排好序去避免查询 R3

确保了查询覆盖所有的查询,避免了点查询 R2

在一些情况下,并没有明确的答案

最优的索引是和数据相关的

*

问:SELECT COUNT(

) FROM foo WHERE c<100, GROUP BY b;**


索引:

key(c, b)

key(b, c)


key(c, b)的查询计划:

使用c<100对数据进行过滤

仍然需要对数据进行排序

    *检索的行不会被b进行排序

    *查询条件不需要对c进行相等的检查,因此b的值分布在不同的c值块中


key(b, c)的查询计划

按b进行排序,R3

列WHERE c>=100同样需要处理,因此没有R1的优势

那个更好一些呢?

答案依赖于数据是什么样的

如果c>=100有更多的数据,节省时间不去检索无用的行。使用key(c, b)

如果c>=100没有多少行,执行查询的时间是以排序为主,那么使用key(b, c)


问题的关键是,通常情况下,规则会有帮助,但它通常只是帮助我们去思考查询和索引,而不是给我们一个配方。

另一个重要的问题:为什么不把所有的加载都添加上索引呢?

需要跟上插入的负荷更多的索引 = 更小的系统负荷


索引的代价:

空间:

问题
    每个索引都会增加存储的需求

选项
    使用压缩

性能:

问题
    B-trees在某些索引任务中执行的很快(内存中,顺序的key),但是在其它类型的索引(辅助索引)会慢20倍

选项
    分形树索引对于所有的索引类型都很快速
        很容易索引
        可经常索引

范围查询性能:

问题
    规则2依赖于范围查询足够快
    B-tree会比较容易碎片化(删除、随机插入…),碎片化的B-tee在范围查询上会变慢

选项
    对于B-tee可以优化表,导出然后导入(时间和离线)
    对于Fractal Tree索引,不是问题,它不会碎片化


PS. 文档翻译自:

Understanding Indexing

Without Needing to Understand Data Structures

MySQL UC 2011 – April 12, 2011

Zardosht Kasheff

For more information…

• Please contact me at [email protected] for any

thoughts or feedback

• Please visit

Tokutek.com

for a copy of this presentation

to learn more about the power of indexing, read about

Fractal Tree indexes, or to download a free eval copy of

TokuDB

PS. 正确设计数据库索引并不只是DBA的事情,每个合格开发者都必须具备这样的能力,但最近老是发现乱使用索引和不使用索引的情况,很明显是不理解索引,希望这篇译文能够帮助到这部分开发者吧。另外还有一篇不错的文章,有时间也会翻译出来,敬请期待。

未分类

Vertical简要

数据分析工作负载VS事务性工作负载证明商业和技术可靠的大规模分布式数据库,支持ACID、有效存储PB级别数据的系统

分析数据库的架构,关注与C-Store不同的点 实施和部署的经验,为什么引入那些不同 现实世界的经验可以引导未来大规模数据分析系统研究的方向
2.背景 2.1.1设计目标:分析工作负载不是事务工作负载 事务工作负载:每秒事务数多,每个事务会影响少数几个元组,大部分的事务是增加一个新行或者修改部分已存在的列 分析工作负载:每秒事务数少,每个事务检查很多表中的元组,比如分析用户的行为,行处理每秒很多
数据量级的增加,即使是小公司

最初就是设计一个分布式数据库 1、新节点加入系统性能有线性的扩展。使用共享磁盘的架构也能获得这样的扩展,但这很快会成为系统的瓶颈 2、优化和执行的引擎避免大量的网络数据传输,以避免内部互联成为系统瓶颈 3、查询和加载数据每秒都会有很多,必须关注支持高插入,否则只能有限的应用,批量的加载应该很快而又不能影响并行的查询 4、操作可在线,管理和维护任务不应该停止或暂停查询 5、易于使用是一个明确的目标。用CPU换时间,形式上减少复杂的网络和磁盘设置,减少性能的调优,自动化的物理设计和管理
3数据模型 3.1 (列)计划 属性排序子集(经过编码和压缩并按某种次序排序和分割的纵列集合) 加载过程中自动维护 每个Projection都有自己的排序,数据是完全排序的 任意数量、带有不同排序的推算或表列的子集是被允许的,可针对不同的查询进行优化。
不同排序规则,可被看成物化视图,但它们是物理数据结构,不是辅助索引。它不包含聚集、连接、额外的查询,认为它们在现实的分布式数据库中是不切实际的。

2.2连接索引,没有被实现,代价比带来的优势要少,实现复杂,执行代价在重现全元组在分布式查询时很高。 明确存储行ID,会点用很多空间,我们高效的压缩实现有助于减少这种开销,但没有计划实现它 3.3预连接推算 没有预期使用的多和重要:在小表连接操作上已经够好(高度优化的hash和合并算法),用户一般不愿意减慢 批量数据加载数据去优化查询速度。在加载数据时比连接查询时能够进行的优化机会要少,因为数据库在加载流中没有什么先验。
3.4 编码和压缩 不同列有不同的编码,同样的列在他们的每个推测中也可有不同的编码 自动:根据类型和配置,当使用场景不同确时 替换:低基数列
3.5 分区 c-store节点内水平分区,在单个节点使用并行提升性能 获得节点性并行并不需要硬盘物理分隔,在运行时逻辑分区,然后并行处理。尽管能够自动并行化,也提供根据value保持在物理上隔离 分区原因:快速批量删除(其它系统分区也是如此),否则需要查询所有物理文件要删除的行,添加删除标记,比起直接删文件慢的多 增加了存储的需求,在元组没有执行合并操作前影响查询性能。如果分区在所有推算上都一致,批量删除才是快速的,所以它是表层次的,而不是推算层次的
分区原因:增加查询性能,它保存最小值和最大值在每个ROS中,在做计划时就能够修剪容器。分区使这种技术更高效,分区使列数据不混合

3.6 分割:集群分布 可以指定列做hash到段 c-store根据projection的排序字段的第一个列分割物理存储到段 完全的分布式存储系统,分配存储元组到不同计算结点 节点内水平分区和节点外水平分区(分割) 分割对每个推算在排序方式上可能不同,推算分割提供决定把元组映射到节点,可以做许多重要的优化。( 完全本地分式连接,高效的分布式聚合,计算高基数不同的集合特别高效 复制推算在每个node上分布,分割推算一个元组在单个推算节点上存储 保持元组物理隔离为本地段,以方便集群的在线扩展与收缩
3.7读写优化存储 写在内存中,读在磁盘中 读:完整的依据推算排序排序的元组,列存储为一系列文件,两个文件1真实列数据2列数据索引 (大约1/1000,包含MetaData,如,开始位置,最大,最小值,固定不变故没有采用B-Tree) 写:内存,行列格式无关紧要,无编码和压缩,为缓冲数据操作(增、删、改)保证操作有足够多的行数以减少写的代价, 会随时间在行列模式下切换(和性能无关,只是软件工程考虑) 但它会以推算的分段表达式进行分段 3.7.1数据修改删除象量 不直接修改,当U和D操作时,创建D象量,它是要删除行的位置的列表,可能会有多个 DVWOS-》DVROS 高效的压缩 修改=删除+插入
4、无组移动:提高数据存储和查询效率 异步把数据从写优化到读优化,当WOS饱和mover操作没有完成之前,加载的数据直接入ROS,直到WOS重新获得足够的能力。mover平衡工作,避免产生小的ROS 读优化文件合并,减少ROS文件数量 写填满-》自动开始移动操作(随后的数据直接写到读优化,直到写有足够的空间)-》移动操作不能过多也不能过少 小文件影响压缩、减慢查询,需要更多文件句柄、seek和更多的全并排序文件的操作。 合并操作:回收已标记为删除的元组 没有限制ROS Containers大小,但它不会创建超过一定大小的(当前2T),足够大(每文件开销分摊,管理不笨重) ROS层在Node间是私有的,不会在集群内协调
5、修改和事务 每个元组都和提交时间关联(纪元)(隐式64bit的列或删除象量) 所有节点在事务提交都同意其包含的纪元,就相当于有了全局的一致性快照,读无需要加锁 有一个分析工作量的表锁定模式 不使用传统的两阶段提交 共享锁:限制并发修改表,用来实现序列化隔离 插入锁:插入数据时到表使用,和自己兼容,支持匹量插入和加载同时发生,以保持高插入加载速度。但仍然提供事务语义 共享插入锁:读写 独占锁:删改 元组移动锁:和所有锁兼容除了X锁,在元组移动同时操作删除象量时 使用锁: 拥有锁:
分布式和组成员协议来协调集群内结点之间操作,使用广播和点对点来保证所有控制消息成功送达所有node提交在集群中成功如果它在选定数据的结点上成功,ROS与WOS创建的事务完成后其它事务才能看到

5.1纪元管理 纪元模型:纪元包含给定时间窗口所有提交了的事务 Last Good Epoch:所有数据都成功从WOS移动到ROS Ancient History Mark:
5.2宽容失败 Vertica的数据复制通过使用Projection分段机制,以提供容错 每个Projection,必须至少有一个伙伴projection含有相同的列和分割以确保任何行被存储在同一样节点的Projection,当节点故障,伙伴projection会做为故障节点的源。 不需要传统的事务日志,数据+纪元自己就做为过去系统活动日志。vertica使用这些历史数据,去重放节点丢失的DML。节点会从正常的伙伴projection数据段恢复数据,
恢复,更新,重新平衡和备份都是在线操作;在执行这些操作同时可进行数据的加载和查询。影响的只是计算和带宽资源。

5.3 以元数据目录在节点间管理状态,它记录着表、用户、节点、纪元等的信息。 没有保存在数据库表中,因为它的表设计无法恰当的通过catalog访问和修改。它采用自定义的内存结构中,然后通过它自己的事务机制保存到磁盘中。 k-safety:当有小于或等于K个节点故障,集群仍然可用。 数据库projection必须确保有K+1个每个段的拷贝在不同的节点上。当有一半节点故障,集群会保护性关闭。集群必须保证有n/2+1个节点确保脑裂后的两个集群继续提供服务。
6 查询的执行 私有扩展标准的SQL声明的查询语言。 Vertica的公司扩展设计使可轻松地查询在SQL时过于繁琐或不可能的时间序列和日志型数据。
6.1查询操作符和计划格式 执行引擎为完全天量化,并且在同时查询一块行数据而不是同时查询单一行 标准操作树,每个操作执行一个特定的算法。一个操作者的输出做为下面操作者的输入。 执行引擎是多线程和流水线的 在一个时间请求一块行数据而不是请求一条数据。 使用上拉处理模型,直到一个操作从磁盘或网络读到数据。 扫描、分组(有多个算法依据性能最大化、内存需求、操作是否必须产生单独的组决定使用那个,并且实现了管道聚合方式可选择是否保持数据编码)、连接(实现了hash join和merge join算法,能够在必要时外部化)、表达式执行、排序、分析、发送/接收
特别处理和复杂的实现,确保可直接操作未解码的数据,这对于扫描、joins、低级聚集操作很重要。

SIP:Sideways Infomation Passing,侧面消息传递,采用在查询计划中尽可能早的过滤数据来优化join性能。(可在优化查询计算时就构建SIP filter,然后在执行时使用。在执行时,扫描操作会扫描join的hash table,SIP会用来判断outer key值在hash 表中是否存在。)

运行期间分析数据调整算法(内存不够hashtable->sort-merge join;生成预处理操作,并发执行,最后根据它们的结果生成最终数据)

晚物化、压缩的成本和估算、流聚合、消除排序、合并连接、不可见索引(每列按选择性排序,不需要索引来减少IO和更快查找值),数据die dai(L2缓存,更好的利用二级缓存和多核并发的特性)

使用pipeline执行引擎带来挑战是共享公共资源,可能造成不必要的yi chu到磁盘。vartica会把计划分成多个不会在同一时间执行的区,下游操作会回收上游操作的资源。

不同表的projection的数据被复制到所有节点上或者在join key上分割相同的范围,使得计划可以在每个节点的内部执行,然后结果发送到客户端连接的结点。

6.2 优化

选择和连接projections,保证scan和join更快(保证之后join的数据被减少)

收集性能数据(压缩I/O,CPU,网络使用)

未分类

ToKuDB简要

介绍TokuDB存储引擎是Tokutek2009年发布的一个数据库存储引擎,于2013年4月开源。支持MySQL/MariaDB。它和InnoDB一样支持事务、MVCC。

特色: Franctal Tree而不是B-Tree 内部结点不仅有指向父子的指针还有Buffer区,数据写入先写buffer区,FIFO结构,写入只需要顺序添加到Buffer区就可返回,后续满时一次性刷新到下面的子树中,插入数据基本上是一个顺序添加的过程。可轻松应对随机IO,减少空间碎片。 出色的压缩性能 块大小默认是4MB 在线DDL
数据结构: 一、Buffered Tree:类B-Tree,写时直接写到Root结点,如果Root结点满了,就把数据刷新到它子结点上,如果子结点满了就继续刷新到子子结点,一直这样下去。因此,只有当结点满时才有Disk Seek产生。Node大小可设置很大,比如4MB,为提高读,需要对Node做更细划分,分成小块,随机读IO复杂度也为O(logN)。 写: 写时不产生disk seek,因为总是先写Root,由于经常被操作,可认为树顶部结点一直在Page Cache中。 节点数据紧凑,大Node压缩有优势 天然适合做事务,没有undo log,叶子结点上做mvcc
二、Fractal-Tree(Buffer-Tree的变种)buffered(4,16)-tree,OMT结构维护结点数据,大小4MB,nonleaf节点OMT结构,leaf节点多个OMT(4MB/64KB)

OMT:Order Maintenance Tree元素用数组表示,具有父子关系的元素尽量相邻存储,cpu cache line(如果一个节点的周边节点能在文件中紧邻的被存储,当读取其中一个的时候,其他节点被prefetching出来,io数则可以减少。这就是vEB layout)

结点刷新:Node完成写时检查是否满足flush条件,满足加到flush队列,后台线程并行处理从队列中读取的任务。

CheckPoint:60秒一次,sharp checkpoint无Fuzz Checkpoint,会对所有索引加只读锁,其它线程写node时,会clone一个Node。做完后检查LSN,以清理无用的log。基本上这个操作不影响前台读写操作,但是因为会进行数据压缩和clone node、写磁盘的操作,会造成一定的性能波动。

Cache:LRU,写cache时添加node到cache链表,然后检查cache状态,设置了四个水平位(低水平位,低警示点,高警示点,高水平位),高于高水平位,客户端线程进入等待状态,超过低警示点,开始收集逐出数据。

三、读: KEY读:每次读数据都要从ROOT到LEAF,做数据合并,才能得到完整的ROW数据。 范围读:对树做深度优先遍历,在LEAF结点返回,痛苦。
四、Schema Changes列修改:Broadcast类型的Message,从Root广播到每一节点,最后到达LEAF结点。(Mysql对列修改会先关闭表,再打开表,关闭时Tokudb会把脏Node写盘,有些性能消耗),腾讯的游戏运维部门在InnoDB上实现了类似的功能。

索引:两种索引,cst_(clustering index)和cvr_(covering index),每个都是一个单独的F-tree文件 offline方式:启动多个线程,遍历记录生成索引,速度快,但创建过程中写操作不可用。 hot方式:速度慢,创建过程读写不受影响
log:log manager来管理log文件,无重做日志组的概念,当日志写满后重新生成一个文件继续写,checkpoint后检查数据都被刷新到磁盘后会删除,达到InnoDB类似的效果;分in buffer和out buffer(都是16MB),在两块内存上来回倒,多线程下锁控制方便。

优点 高压缩比,写性能高,DDL速度超快 缺点 cpu usr态消耗,响应时间变长 场景 插入效率高、压缩效率好、可在线DDL,适合写性能要求高,数据量过亿,记录不是太大的场合。
大压缩比所以CPU的USR态有消耗 相同QPS时读IOPS差别不大,写IOPS InnoDB平均多消耗些 TokuDB数据经过压缩,但会有解压消耗,所以结果不好确定,测试上看响应时间高于InnoDB
IOPS (Input/Output Operations Per Second),即每秒进行读写(I/O)操作的次数。QPS:Queries Per Second意思是“每秒查询率”,是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。

未分类

go语言开发环境配置

这篇Blog的代码的高亮显示真心不错,花花绿绿的… ‘;’

1、编译vimgdb

下载vimgdb73和vim73

mkdir -p ./tmp
cd tmp
tar zxvf ../vim-7.3.tar.gz
unzip ../vimgdb-for-vim7.3-master.zip
mv vimgdb-for-vim7.3-master vimgdb-for-vim7.3
patch -p0 < vimgdb-for-vim7.3/vim73.patch
cd vim73

安装依赖

sudo apt-get install build-essential
sudo apt-get build-dep vim-gtk
sudo apt-get install libncurses5-dev

安装

// 这里直接执行make的操作
make
sudo make install

安装vimgdb runtime

cd ../vimgdb-for-vim7.3
cp vimgdb_runtime  ~/.vim/bundle

打开vim

:helptags ~/.vim/bundle/vimgdb_runtime/doc " 生成doc文件

添加配置.vimrc

" vimgdb插件
run macros/gdb_mappings.vim

在vim中执行gdb时,报 “Unable to read from GDB pseudo tty” 的错误,因为没有安装 gdb ,所以安装gdb

sudo apt-get install gdb

2、安装vundle

set up vundle $ git clone https://github.com/gmarik/vundle.git ~/.vim/bundle/vundle

Configure Plugins
在.vimrc文件的开头添加下面的内容,有些不是必须的,可以注掉

set nocompatible              " be iMproved, required
filetype off                  " required
 
" set the runtime path to include Vundle and initialize
set rtp+=~/.vim/bundle/vundle/
call vundle#rc()
" alternatively, pass a path where Vundle should install plugins
"let path = '~/some/path/here'
"call vundle#rc(path)
 
" let Vundle manage Vundle, required
Plugin 'gmarik/vundle'
 
" The following are examples of different formats supported.
" Keep Plugin commands between here and filetype plugin indent on.
" scripts on GitHub repos
Plugin 'tpope/vim-fugitive'
Plugin 'Lokaltog/vim-easymotion'
Plugin 'tpope/vim-rails.git'
" The sparkup vim script is in a subdirectory of this repo called vim.
" Pass the path to set the runtimepath properly.
Plugin 'rstacruz/sparkup', {'rtp': 'vim/'}
" scripts from http://vim-scripts.org/vim/scripts.html
Plugin 'L9'
Plugin 'FuzzyFinder'
" scripts not on GitHub
Plugin 'git://git.wincent.com/command-t.git'
" git repos on your local machine (i.e. when working on your own plugin)
Plugin 'file:///home/gmarik/path/to/plugin'
" ...
 
filetype plugin indent on     " required
" To ignore plugin indent changes, instead use:
"filetype plugin on
"
" Brief help
" : PluginList          - list configured plugins
" : PluginInstall(!)    - install (update) plugins
" : PluginSearch(!) foo - search (or refresh cache first) for foo
" : PluginClean(!)      - confirm (or auto-approve) removal of unused plugins
"
" see :h vundle for more details or wiki for FAQ
" NOTE: comments after Plugin commands are not allowed.
" Put your stuff after this line
Install Plugins
Launch vim and run
 
: PluginInstall
或者
In command line:

vim +PluginInstall +qall
3、官方vim-lang插件

Config vim file .vimrc,Add content bellow in bottom of the file
" 官方的插件
" Some Linux distributions set filetype in /etc/vimrc.
" Clear filetype flags before changing runtimepath to force Vim to
"   reload them.
filetype off
filetype plugin indent off
set runtimepath+=$GOROOT/misc/vim
filetype plugin indent on
syntax on
autocmd FileType go autocmd BufWritePre  Fmt

4、代码补全的插件gocode

配置go的环境变量,比如我的配置,GOPATH变量是必须要配置的,PATH中必须把GOPATH的bin也添加进去,否则没有自动提示,会提示找不到模式

export GOROOT=/usr/local/go
export GOPATH=/data/app/gopath
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

Set up gocode
Then you need to get the appropriate version of the gocode, for 6g/8g/5g compiler you can do this:
Then you need to get the appropriate
go get -u github.com/nsf/gocode (-u flag for “update”)

Configure vim in .vimrc file

Plugin 'nsf/gocode', {'rtp': 'vim/'}

Install Plugins
Launch vim and run

: PluginInstall

或者
In command line:

vim +PluginInstall +qall

写一个helloword程序,输入fmt后按如果能看到函数的声明展示出来,说明安装是正确的。

4、代码跳转提示godef
Set up godef

go get -v code.google.com/p/rog-go/exp/cmd/godef
go install -v code.google.com/p/rog-go/exp/cmd/godef
git clone https://github.com/dgryski/vim-godef ~/.vim/bundle/vim-godef

Configure vim in .vimrc file

Bundle 'dgryski/vim-godef'

Install Plugins
Launch vim and run

: PluginInstall

或者
In command line:

vim +PluginInstall +qall

5、代码结构提示gotags
Set up gotags

go get -u github.com/jstemmer/gotags

Put the following configuration in your vimrc:

Bundle 'majutsushi/tagbar'
nmap  :TagbarToggle
let g:tagbar_type_go = {
    \ 'ctagstype' : 'go',
    \ 'kinds'     : [
        \ 'p:package',
        \ 'i:imports:1',
        \ 'c:constants',
        \ 'v:variables',
        \ 't:types',
        \ 'n:interfaces',
        \ 'w:fields',
        \ 'e:embedded',
        \ 'm:methods',
        \ 'r:constructor',
        \ 'f:functions'
    \ ],
    \ 'sro' : '.',
    \ 'kind2scope' : {
        \ 't' : 'ctype',
        \ 'n' : 'ntype'
    \ },
    \ 'scope2kind' : {
        \ 'ctype' : 't',
        \ 'ntype' : 'n'
    \ },
    \ 'ctagsbin'  : 'gotags',
    \ 'ctagsargs' : '-sort -silent'
\ }

命令模式下按在右边就会显示当前文件下的函数名,结构体名等等,光标放到相应的tag上,按回车可以快速跳到程序中的相应位置。
再次按会关闭tag窗口。

PS:本地的.vimrc的配置

" 插件管理器 vundle
set nocompatible              " be iMproved, required
filetype off                  " required
 
" set the runtime path to include Vundle and initialize
set rtp+=~/.vim/bundle/vundle/
call vundle#rc()
" alternatively, pass a path where Vundle should install plugins
"let path = '~/some/path/here'
"call vundle#rc(path)
 
" let Vundle manage Vundle, required
Plugin 'gmarik/vundle'
 
" The following are examples of different formats supported.
" Keep Plugin commands between here and filetype plugin indent on.
" scripts on GitHub repos
" Plugin 'tpope/vim-fugitive'
" Plugin 'Lokaltog/vim-easymotion'
" Plugin 'tpope/vim-rails.git'
" The sparkup vim script is in a subdirectory of this repo called vim.
" Pass the path to set the runtimepath properly.
" Plugin 'rstacruz/sparkup', {'rtp': 'vim/'}
" scripts from http://vim-scripts.org/vim/scripts.html
" Plugin 'L9'
" Plugin 'FuzzyFinder'
" scripts not on GitHub
" Plugin 'git://git.wincent.com/command-t.git'
" git repos on your local machine (i.e. when working on your own plugin)
" Plugin 'file:///home/gmarik/path/to/plugin'
" ...
"
filetype plugin indent on     " required
" To ignore plugin indent changes, instead use:
" filetype plugin on
"
" Brief help
" : PluginList          - list configured plugins
" : PluginInstall(!)    - install (update) plugins
" : PluginSearch(!) foo - search (or refresh cache first) for foo
" : PluginClean(!)      - confirm (or auto-approve) removal of unused plugins
"
" see :h vundle for more details or wiki for FAQ
" NOTE: comments after Plugin commands are not allowed.
" Put your stuff after this line
 
syntax on
 
" ********************************************************************
 
" 这里省略了其它不相关的插件
 
" vimgdb插件
run macros/gdb_mappings.vim
 
" 官方的插件
" Some Linux distributions set filetype in /etc/vimrc.
" Clear filetype flags before changing runtimepath to force Vim to
"   reload them.
filetype off
filetype plugin indent off
set runtimepath+=$GOROOT/misc/vim
filetype plugin indent on
syntax on
autocmd FileType go autocmd BufWritePre <buffer> Fmt
 
" 代码补全的插件
Bundle 'Blackrush/vim-gocode'
 
" 代码跳转提示
Bundle 'dgryski/vim-godef'
 
" 代码结构提示
Bundle 'majutsushi/tagbar'
nmap <F8> :TagbarToggle<CR>
let g:tagbar_type_go = {
    \ 'ctagstype' : 'go',
    \ 'kinds'     : [
        \ 'p:package',
        \ 'i:imports:1',
        \ 'c:constants',
        \ 'v:variables',
        \ 't:types',
        \ 'n:interfaces',
        \ 'w:fields',
        \ 'e:embedded',
        \ 'm:methods',
        \ 'r:constructor',
        \ 'f:functions'
    \ ],
    \ 'sro' : '.',
    \ 'kind2scope' : {
        \ 't' : 'ctype',
        \ 'n' : 'ntype'
    \ },
    \ 'scope2kind' : {
        \ 'ctype' : 't',
        \ 'ntype' : 'n'
    \ },
    \ 'ctagsbin'  : 'gotags',
    \ 'ctagsargs' : '-sort -silent'
    \ }

<-EOF->

未分类

nginx动态处理图片模块

nginx的Http Image Filter Module可以动态处理JPEG,GIF,PNG格式的图片,它在0.7.54+版本后引入nginx,要使用它需要在./configure时添加 –with-http_image_filter_module 打开它,它需要libgd的支持,所以要使用它我们先要在机器上安装libgd,centos和debian的用户可以使用它们的包管理器去安装,如果要手动安装,对于64位的系统需要在编译时添加相应参数(使生成64位的二进制文件),这里只有32位的linux系统安装的例子。

1、需要的程序文件

jpegsrc.v6b.tar.gz
libpng-1.5.12.tar.gz
pierrejoye-gd-libgd-5551f61978e3.tar.gz
freetype-2.3.5.tar.gz
pcre-8.31.tar.gz
nginx-1.3.4.tar.gz

cd jpeg-6b
./configure --enable-shared --enable-static
make && make install
# 报错,提示找不到文件 /usr/local/man/man1/
# mkdir -p /usr/local/man/man1/ 然后再执行 make install

cd libpng-1.5.12
./configure
make && make install
# 报错,提示找不到文件夹 /usr/local/include/freetype2/freetype/
# mkdir -p /usr/local/include/freetype2/freetype/ 然后再 make install

cd freetype-2.3.5
./configure
make
make install
# 报错,提示找不到文件夹 /usr/local/include/freetype2/freetype/internal
# 执行 mkdir -p /usr/local/include/freetype2/freetype/internal 然后再 make install

cd pierrejoye-gd-libgd-5551f61978e3/src
./configure --with-jpeg=/usr/local/jpeg8 --with-png -with-zlib -with-freetype=/usr/local/freetype
make
make install

cd pcre-8.31
./configure
make && make intall

groupadd nginx
useradd -g nginx nginx
cd nginx-1.3.4
./configure --user=nginx --group=nginx --with-http_stub_status_module --with-http_ssl_module --with-http_image_filter_module
# 本地如果要打开debug日志,需要添加--with-debug
make && make install

3、定制

模块的crop是从中间去截取,并且对于宽图,只能得到中间的一片图像,不符合大多数的情况,我们需要从顶部截取并且当图宽时,把宽缩放到规定的大小,然后再截取(原始逻辑是把高缩放到指定大小再截取),没有直接在原crop基础上修改。
代码位置:src/http/modules/ngx_http_image_filter_module.c
新增加指令crop_guoguo
主要修改的代码

static ngx_buf_t *
ngx_http_image_resize(ngx_http_request_t *r, ngx_http_image_filter_ctx_t *ctx)
{
......
    } else if (conf->filter == NGX_HTTP_IMAGE_ROTATE) {
        resize = 0;
/* guoguo add start */
    } else if (conf->filter == NGX_HTTP_IMAGE_CROP_GUOGUO)  {
        resize = 0;
        if ((double) ctx->max_width / dx < 1) {// 新增加的代码段,处理宽图的缩放
            dy = dy * ctx->max_width / dx;
            dy = dy ? dy : 1;
            dx = ctx->max_width;
            resize = 1;
        }
    } 
/* guoguo add end */
......
if ((ngx_uint_t) dx > ctx->max_width) {
            ox = dx - ctx->max_width;

        } else {
            ox = 0;
        }
/* guoguo add start */
/* 处理顶部截取,可以更细致的做配置,比如增加一个seed参数用于表示截线的位置 */
            ox /= 2;
            if (conf->filter == NGX_HTTP_IMAGE_CROP_GUOGUO) {
               oy = 0;
            } else {
               oy /= 2;
            }
/* guoguo add end */
        if (ox || oy) {
......
    if (ngx_strcmp(value[i].data, "resize") == 0) {
        imcf->filter = NGX_HTTP_IMAGE_RESIZE;

    } else if (ngx_strcmp(value[i].data, "crop") == 0) {
        imcf->filter = NGX_HTTP_IMAGE_CROP;
/* guoguo add start */
    } else if (ngx_strcmp(value[i].data, "crop_guoguo") == 0) {
        imcf->filter = NGX_HTTP_IMAGE_CROP_GUOGUO;
/* guoguo add end */
......
}

4、nginx配置文件

       location /mblog/ {
           if ($uri ~* (/xxxxxx)/r_(xxxxxx\.(jpg|png|gif)) ) {
               set $pre_uri $1;
               set $file_name $2;
               rewrite ^(.*)$ /guoguo/stream/$pre_uri/m_$file_name last;
           }
           proxy_pass http://image_process;
       }

        location /guoguo/stream/ {
            proxy_pass http://image_process/;
            #image_filter resize 150 100;
            #image_filter rotate 90;
            # image_filter crop 50 50;
            #image_filter resize 200 -;
            #image_filter crop 200 700;
            #image_filter crop 200 700;
            image_filter crop_guoguo 200 700;
        }