docker-compose 启动顺序

在docker-compose的配置文件中,通过配置depends_on, links, volumes_from, 以及 network_mode: “service:…”.可以控制服务的启动顺序,但是却不能知道被依赖的服务是否启动完毕,在一个服务必须要依赖另一个服务完成的时候,这样就会有问题。

比如在微服务需要依赖微服务配置中心,在配置中心没有加载完毕的时候,微服务就会出现无法加载配置的错误。

这个问题,官网的文档上有描述

depends_on
Controlling startup order in Compose

解决的办法有以下几种:

1、足够的容错和重试机制,比如连接数据库,在初次连接不上的时候,服务消费者可以不断重试,直到连接上位置

2、docker-compose拆分,分成两部分部署,将要先启动的服务放在一个docker-compose中,后启动的服务放在两一个docker-compose中,启动两次,两者使用同一个网络。

3、同步等待,使用wait-for-it.sh或者其他shell脚本将当前服务启动阻塞,直到被依赖的服务加载完毕
wait-for-it的github地址为:wait-for-it

改写后的docker-compose如下:

version: "2"
services:
  web:
    build: .
    ports:
      - "80:8000"
    depends_on:
      - "db"
    command: ["./wait-for-it.sh", "db:5432", "--", "python", "app.py"]
  db:
    image: postgres

配置 PHP 的 Session 存储到 Redis

PHP 的会话默认是以文件的形式存在的,可以配置到 NoSQL 中,即提高了访问速度,又能很好地实现会话共享,,,爽歪歪!

配置方式如下:

方法一:修改 php.ini 的设置

session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379"

修改完之后,重启一下 php-fpm。

方式二:通过 ini_set() 函数设置

ini_set("session.save_handler", "redis");
ini_set("session.save_path", "tcp://127.0.0.1:6379");

如果配置文件 /etc/redis.conf 里设置了连接密码 requirepass,保存 session 的时候会报错,save_path 这样写 tcp://127.0.0.1:6379?auth=authpwd 即可。

测试代码:

<?php
// 如果未修改php.ini下面两行注释去掉
// ini_set('session.save_handler', 'redis');
// ini_set('session.save_path', 'tcp://127.0.0.1:6379');
session_start();
$_SESSION['sessionid'] = 'this is session content!';
echo $_SESSION['sessionid'];
echo '<br/>';
$redis = new redis();
$redis->connect('127.0.0.1', 6379);
// redis 用 session_id 作为 key 并且是以 string 的形式存储
echo $redis->get('PHPREDIS_SESSION:' . session_id());

Posted in 运行环境

Linux 安装配置Redis

下载安装

wget http://download.redis.io/releases/redis-4.0.1.tar.gz
解压tar zxvf redis-4.0.1.tar.gz
make && make install

用cp命令复制到usr目录下运行

cp redis-server /usr/local/bin/
cp redis-cli /usr/local/bin/

新建目录,存放配置文件

mkdir /etc/redis
mkdir /var/redis
mkdir /var/redis/log
mkdir /var/redis/run
mkdir /var/redis/6379

在redis解压根目录中找到配置文件模板,复制到如下位置。

cp redis.conf /etc/redis/6379.conf

通过vim命令修改

daemonize yes
pidfile /var/redis/run/redis_6379.pid
logfile /var/redis/log/redis_6379.log
dir /var/redis/6379

最后运行redis:

$ redis-server /etc/redis/6379.conf

添加到系统服务并配置开机启动

进入到redis源码的utils目录,执行 ./install_server.sh ,所有选项默认即可。
修改 /etc/init.d/redis_6379 为 redis : mv /etc/init.d/redis_6379 /etc/init.d/redis

添加到系统服务:chkconfig –add redis
开机启动:chkconfig redis on
这样执行 service redis start|restart|stop 就可以控制redis的启动、重启、停止了。

参考:redis安装部署维护备份 http://blog.csdn.net/huwei2003/article/details/40536905

基于 git 和 CI/CD 的集中化配置管理服务

分享一种基于 git 和 CI/CD 的集中化配置管理服务。这种方案最大的好处就是,简单直接,可以快速先把配置管理的坑儿占好。

功能点

首先,我们先整理一下集中化配置管理的主要 feature:

  • 可以记录、审核配置的修改
  • 支持多种环境(生产、测试、开发、演示等等)
  • 修改配置之后,应用的配置能够及时得到更新

主要思路

我们的主要思路是:将配置服务直接写成一个独立的 webserver,webserver 对外提供 http 接口,配置直接写在 webserver 的代码当中,每次提交代码时通过 CI/CD 自动发布。

这样做的好处是:

  • 可以直接通过 git 来记录、审核配置数据的修改,每次有人要修改配置时,直接提 PR,leader review 通过之后合并到 master 分支
  • 代码合并到 master 分支之后,通过 CI/CD 自动发布到线上

可能大家会有下面的一些顾虑:

  • 不应该直接在代码当中硬编码 MySQL 的账号、密码之类的敏感数据,这样是不安全的
  • 简单通过 http 接口来读取配置,效率不高

针对第一个问题,我们算是使用了点 “反模式” 吧。代码肯定是要确保在私有代码库当中的,你需要授权才能够访问代码库,从这个角度来说,直接在代码里面写配置数据其实也不是大问题,特别是在产品研发初期,这个时候团队规模也不大。而且,像 gitlab、github 这类的服务,本身就有很好的权限管理机制,加上 git 本身就是版本管理工具,为什么不充分使用一下呢。

第二个问题呢,其实和第一个一样:初期,服务压力较小,配置数据不复杂,通过 http 接口来读取配置,性能其实没有大问题。

这种方案的意义就在于把这个配置管理的坑儿先占上,确保各个服务是通过统一的接口来读取配置的,日后可以慢慢优化。 实践发现,随着产品迭代,这种方案能够持续的时间还是挺长的,投入成本还很小。

主要功能设计和实现

我们自己的项目使用 Node.js 开发的,所以下面以 Node.js 为例,来说一下具体设计。

首先,说一下 webserver 的接口设计,接口要尽可能简化,我们只提供了一个接口:

GET /api/profiles/:profile HTTP/1.1

profile 参数表示你想要的环境,比如:

  • 你想要测试环境的配置,应该发送 GET /api/profiles/dev
  • 如果想要同事 Jack 的本地开发环境配置,你应该发送 GET /api/profiles/jack-local-dev

返回的数据自然应该是 json 数据,比如像下面这种:

{
    "revision": "5d41402abc4b2a76b9719d911017c592",
    "config": {
        "debug": true,
        "wechat": {
            "appId": "xxxx",
            "secret": "xxxxx"
        },
        "mysql": {
            "host": "localhost",
            "port": 3306
        }
    }
}

revision 表示配置的版本,config 就是实际的配置数据啦。

根据上面的设计,我们的 webserver 服务的代码库大概是下面这样的:

├── Dockerfile
├── README.md
├── app.js
└──  config
    ├── dev.yml
    ├── prod.yml
    └── jack-local-dev.yml

其中:

  • 有一个 app.js ,里面封装了 http 接口
  • 有一个 config 文件夹,里面放置不同环境的配置文件。我们推荐使用 .yml 文件,.yml 文件写起配置其实更清爽,当然 json 也可以
  • 再有一个 Dockerfile 用于配置镜像打包和自动发布

实现这样一个接口,app.js 的代码也比较简单,大概就像下面这样:

const fs = require('fs');
const yaml = require('js-yaml');
const hash = require('object-hash');
const express = require('express');

const app = express();
app.get('/api/profiles/:profile', (req, res) => {
    const path = `${__dirname}/config/${req.params.profile}.yml`
    fs.readFile(path, {
        "ecoding": "utf-8"
    }, (e, content) => {
        if (e) {
            return res.status(500).json({
                errorId: 'internal-server-error',
                errorMsg: e.message
            });
        }

        const config = yaml.safeLoad(content);
        const revision = hash(config);
        res.json({
            config,
            revision
        });
    });
});

app.get('/ping', (req, res) => res.send('pong'));

const PORT = 8080;
app.listen(PORT, () => {
  console.log('listening on port', PORT);
});

当然你也可以在上面加一些性能上的优化哈,特别是加载 yaml 文件的部分。除了读取 yaml 配置文件的内容外,里面还通过 object-hash 来计算了配置的 revision,方便客户端来检查配置数据的版本更新。

提供统一的客户端 library

主体设计和实现就是上面说的这些内容了。不过,还有一项工作很重要,就是提供统一的客户端 library。当大家使用同样的客户端 library 来读取配置的时候,配置管理的坑儿才能算真正占好,后面才方便替换配置管理服务的技术方案。

library 设计

首先说一下这个 library 的接口设计吧

config.get(path)

提供一个 get 方法,注意:

  • 参数里面应该是一个 path,准确的说应该是一个 property path
  • 这个方法应该是 同步执行 的,所以下面我提供了一个 sync 方法,专门用来同步配置数据

假设完整的配置数据是这样的:

{
    "mysql": {
        "host": "111.111.11.11",
        "port": 3306,
        "username": "root",
        "password": "123456"
    },
    "redis": {
        "host": "111.111.11.12",
        "port": 6379
    },
    "wechat": {
        "appId": "wx888888888"
    },
    "secret": "foobar"
}

那么通过 get 方法应该能够做到下面这些事情:

config.get("mysql") 
// => {"host": "111.111.11.11", "port": 3306, ...}

config.get("wechat.appId") 
// => "wx888888888"

config.get() 
// => {"mysql": {...}, "wechat": {...}, ...}

也就是说,大家可以通过 get 方法灵活的获取到配置数据的某一部分。这块我们使用了 object-path 这个模块。

config.sync(host, profile, token)

提供一个 sync 方法,用来初始话和轮训同步配置数据

config.on(event, listener)

应该提供事件回调接口,用来检测是否有数据发生变化,这个接口在 Node.js 服务中有一定用处,其他的同步的技术框架应该就不需要了。

config.mock(object)

最后,应该有一个 mock 方法,方便支持自动化测试

一些补充内容

这里想补充说明的是,关于 sync 方法的一些小问题。上面说到 get 方法应该是一个同步方法,毕竟如果读取配置信息也要异步的话,那对工程的来说复杂度反而增加了。

所以我多设计了一个 sync 方法。在 Node.js 项目中,应用启动之前,应该先调用 sync 方法,轮训同步配置数据。这样保证 get 方法被调用的时候,始终是能够返回数据的。

还有一点就是,sync 方法被调用的时候,应该先发一个 同步的 http 方法来获取数据,这块我们使用了 sync-request 来实现。

最后,补充一下主要的实现代码,供大家参考:

const EventEmitter = require('events').EventEmitter;
const objectPath = require('object-path');

class Config {
  constructor(interval) {
    this.interval = interval || 5000;
    this.emitter = new EventEmitter();
  }

  sync(host, profile, token) {
    this.host = host;
    this.profile = profile;
    this.token = token;

    this.data = loadConfigSync(); // 首先同步获取配置数据
    setTimeOut(() => this.watch(), this.interval); // 之后,定时轮训数据
  }

  get(path) {
      return objectPath.get(this.data.config, path);
  }

  loadConfigSync() {
      // 这部分代码就先省略了~
  }

  async loadConfigAsync() {
      // 这部分代码就先省略了~
  }

  async watch() {
    const result = await this.loadConfigAsync();
    if (result.revision !== this.data.revision) {
        this.data = result;
        this.emitter.emit('update', this.data.config);
    }

    setTimeout(() => this.watch(), this.interval);
  }
}

module.exports = new Config();

使用客户端 library 的一般套路:

// server.js
const config = require('config-module-name');

// 1. 调用 sync 方法加载配置
config.sync(process.env.CONFIG_HOST, process.env.CONFIG_PROFILE, process.env.CONFIG_TOKEN);

// 2. 启动实际项目的 WebServer
const server = new WebServer();
server.serve();

增加配置覆盖功能

上面的 webserver 设计还是简单了一些,因为平时我们配置服务的时候,经常会有一系列通用的配置,而每个环境里面可能各有一些少量特殊的配置。

为了解决这个问题,我们在前面的方案基础之上,开发了一个简单的配置覆盖功能。我们是这么做的:

  • 在 config 文件夹当中提供一个 defaul.yml 配置文件,在这个文件当中去保存通用的配置数据
  • 假设,现在要访问 dev 环境的配置,webserver 就把 dev.yml 和 default.yml 配置文件都读取出来,将 dev.yml 和 default.yml 重合的部分 merge 到一起,这块我们使用的一个叫做 deepmerge 的模块来实现的

现在举一个实际的例子,假设生产(prod)和开发环境(dev)就数据库的名称不同,没有增加配置覆盖功能之前,配置文件是这样的:

# prod.yml
mysql:
    host: localhost
    port: 3306
    username: root
    password: root
    database: prod

# dev.yml
mysql:
    host: localhost
    port: 3306
    username: root
    password: root
    database: de

增加了配置覆盖的功能之后,配置文件变成了下面这个样子:

# default.yml
mysql:
    host: localhost
    port: 3306
    usrename: root
    password: root

# prod.yml
mysql:
    database: prod

# dev.yml
mysql:
    database: dev

在实际的项目当中,增加配置覆盖的一个最大好处是,有新的同事加入项目时,他需要增加的配置内容就会少很多,而不需要全量的 copy 一份别人的配置文件,主体的配置都可以放到 default.yml 文件中。

安全问题

这个方案现在还有一些明显的安全问题:

  • 接口访问没有增加鉴权
  • 有些数据就是不希望写到代码当中去,该怎么办

关于接口鉴权,我们的解决方案是提供一个 token 列表,token 是常量的 UUID 或者随机字符串即可。另外强制要求使用 https 来访问接口,不要直接在前端读取配置。

如果有些数据就是不希望写到代码当中去,改怎么办?

我们建议增加一个环境变量注入的 feature,比如配置文件改写成这样:

mysql:
    password: ${MYSQL_PASSWORD}

接口在返回数据之前,增加一道工序,将上面的 ${MYSQL_PASSWORD} 这类的表达式解析出来,然后将环境变量注入进去。我们目前是使用正则表达式简单粗暴的处理的,大概就是这样:

const traverse = require('traverse');
const delimeter = /${(.+?)}/g;

function enjectEnv(config) {
    return traverse(config).map(value => {
        value.replace(delimeter, (match, p1) => {
            return process.env[p1] || "";
        })
    })
}

通过这种方式,你就可以通过环境变量去配置一些敏感信息了。

总结

总结一下,这样一个方案,主要的工作:

  • 基于 git 和 CI/CD 搭建配置服务
  • 提供统一的客户端 library
  • 扩展功能,增加配置覆盖机制
  • 提供简单的接口鉴权和环境变量注入

这样一个方案,其实在产品初期阶段应该足够好用了。这种方案的好处就是快速占坑,将配置管理机制固化下来。整套方案充分使用了 git 和 CI/CD,整个服务也很轻,推荐大家尝试一下~

重写git历史记录

大家在使用git时做版本管理时,有时候会遇到下面的场景,

  • 不小心将一个很大的文件提交到仓库中了,导致仓库臃肿,上传下载都非常消耗网络,追悔莫及!

  • 随着项目的推进,突然不想用git来管理某个文件了,将其放入.gitignore文件中,发现git依然能够探测到这个文件的改动,一脸懵逼!

本文将会就以上两个问题给你指条明路。

git filter-branch命令就是我们的主角。git filter-branch命令在git中号称大杀器命令,基本上可以用这个命令能够到达任何操作要求,注意,是任何!

关于这个命令的文档我已经在上面的链接中给出了,一句话概括这个命令的作用:操作所有的git对象数据以重写历史记录。有兴趣掌握其原理的,可自行找虐。下面,我们将会用一个实际的案例来说明“如何重写git历史记录”。

案例

在前端开发中,npm是必不可少的包管理工具。在[email protected]之后,npm引入了package-lock.json文件。关于这个package-lock.json是干什么用的,这个官方的说明文档。

简单来说,这个package-lock.json文件是npm用来“锁版本”的。这里所谓的锁版本是指当在项目拥有package-lock.json文件时,npm会根据其自动解析出包依赖,不会出现在不同的环境场景下,安装了不同的包版本。

在开始使用[email protected]的时候,执行完npm install后,有一个提示,

npm notice created a lockfile as package-lock.json. You should commit this file.

然后我就按照这句提示来做了。后来发现这玩意坑很多。所以我们现在想将这个package-lock.json文件放到.gitignore中,希望git仓库不再追踪这个文件。

方案

想做的事情已经很明确了,接下来我们来动手了。

首先明确一点,只要一个文件被git管理中(即被添加到git仓库中),那么无论是删除这个文件,还是将其添加到.gitignore文件中,都是没办法组织git继续对齐进行管理和追踪的。

所以,我们改写git仓库的历史记录。

step 1,

git filter-branch --force --index-filter 'git rm --cached --ignore-unmatch package-lock.json' --prune-empty --tag-name-filter cat -- --all

我们使用了git filter-branch命令,对所有的分支上的commit执行额外的命令操作。这个操作就是git rm –cached –ignore-unmatch package-lock.json,忽略对package-lock.json文件的追踪,从git仓库中将其移除,并同时重写每一条记录。

step 2,

rm -rf .git/refs/original
git reflog expire --expire=now --all
git gc --prune=now
git gc --aggressive --prune=now

现在历史记录中已经不包含对那个文件的引用了。不过reflog以及运行filter-branch时,git往.git/refs/original添加的一些refs中仍有对它的引用,因此需要将这些引用删除并对仓库进行repack操作。在进行repack前需要将所有对这些commits的引用去除。

Step 3,

git push origin master --force

将本地重写的git仓库强制推送到远程origin。

额外的问题

如果之前签出了远程分支,比如在很早的时候,通过git checkout –track origin/a签出了分支a到本地。那么在重写历史之后,可能本地a分支会丢失对远程分支remote/a的追踪。造成这种现象的原因是在重写记录时,将origin/a分支上的记录重写了,导致本地a分支与远程remote/a分支不再匹配。

此时,我们应该删除本地游离的分支a,然后从remote/a上重新签出分支a到本地。

另外还可能会出现一个问题,就是我们是针对master分支进行重写记录的,如果团队中其他成员在自己的本地有尚未合并进master分支的开发分支,那么可能会出现本地开发分支与远程分支游离的情况。此时我们需要针对这个开发分支再做一个重写历史记录。

参考链接

  • Removing sensitive data from a repository: (https://help.github.com/articles/removing-sensitive-data-from-a-repository/)

  • git-filter-branch – Rewrite branches: (https://git-scm.com/docs/git-filter-branch)

使用Python操作Zabbix Api

众所周知,zabbix是一款强大的分布式监控系统,集各家所长于一体,得到了广大SA的认可。其强大的管理界面也极其方便,但是美中不足的是,如果同时有大批量(50台+)的服务器需要添加监控时,这时,其图形界面反而显得有些臃肿了,好在zabbix提供了一套强大的API管理接口,我们可以使用它快速地添加或删除成千上万台服务器了。

下面的流程图代表了Zabbix API 工作的典型工作流。验证(方法user.login)是获取验证ID的强制步骤。这个ID又允许我们调用API提供的任何权限允许的方法来进行操作。在之前的例子中没有提到user.logout方法,这也是一次验证ID能够重复使用的原因所在。使用user.logout方法后将会使验证ID失效,后面的操作将不能再使用此ID。

未分类

1. 首先获取所有模板及ID

#!/usr/bin/python 
#coding:utf-8 

import json 
import urllib2 
from urllib2 import URLError 
import sys,argparse

class zabbix_api: 
    def __init__(self):
        self.url = 'http://10.0.8.8/api_jsonrpc.php'
        self.header = {"Content-Type":"application/json"}

    def user_login(self): 
        data = json.dumps({ 
                           "jsonrpc": "2.0", 
                           "method": "user.login", 
                           "params": { 
                                      "user": "pengdongwen",            #修改用户名
                                      "password": "pengdongwen"         #修改密码
                                      }, 
                           "id": 0 
                           }) 

        request = urllib2.Request(self.url, data) 
        for key in self.header: 
            request.add_header(key, self.header[key]) 

        try: 
            result = urllib2.urlopen(request) 
        except URLError as e: 
            print "33[041m 用户认证失败,请检查 !33[0m", e.code 
        else: 
            response = json.loads(result.read()) 
            result.close() 
            #print response['result'] 
            self.authID = response['result'] 
            return self.authID 

    def template_get(self,templateName=''): 
        data = json.dumps({ 
                           "jsonrpc":"2.0", 
                           "method": "template.get", 
                           "params": { 
                                      "output": "extend", 
                                      "filter": { 
                                                 "name":templateName                                                        
                                                 } 
                                      }, 
                           "auth":self.user_login(), 
                           "id":1, 
                           })

        request = urllib2.Request(self.url, data) 
        for key in self.header: 
            request.add_header(key, self.header[key]) 

        try: 
            result = urllib2.urlopen(request) 
        except URLError as e: 
            print "Error as ", e 
        else: 
            response = json.loads(result.read()) 
            result.close() 
            #print response
            for template in response['result']:                
                if len(templateName)==0:
                    print "template : 33[31m%s33[0mt  id : %s" % (template['name'], template['templateid'])
                else:
                    self.templateID = response['result'][0]['templateid'] 
            print "Template Name :  33[31m%s33[0m "%templateName
                    return response['result'][0]['templateid']

if __name__ == "__main__":
    zabbix=zabbix_api()
        zabbix_api().template_get()

2. 根据模板ID获取某个模板关联的主机ID

#!/usr/bin/python 
#coding:utf-8 

import json 
import urllib2 
from urllib2 import URLError 
import sys,argparse

class zabbix_api: 
    def __init__(self):
        self.url = 'http://10.0.8.8/api_jsonrpc.php'
        self.header = {"Content-Type":"application/json"}

    def user_login(self): 
        data = json.dumps({ 
                           "jsonrpc": "2.0", 
                           "method": "user.login", 
                           "params": { 
                                      "user": "pengdongwen",            #修改用户名
                                      "password": "pengdongwen" #修改密码
                                      }, 
                           "id": 0 
                           }) 

        request = urllib2.Request(self.url, data) 
        for key in self.header: 
            request.add_header(key, self.header[key]) 

        try: 
            result = urllib2.urlopen(request) 
        except URLError as e: 
            print "33[041m 用户认证失败,请检查 !33[0m", e.code 
        else: 
            response = json.loads(result.read()) 
            result.close() 
            self.authID = response['result'] 
            return self.authID 

    def host_get(self): 
        data=json.dumps({
                "jsonrpc": "2.0",
                "method": "host.get",
                "params": {
                          "output": ["hostid","name"],
                              "templateids":["10105"]
                          },
                "auth": self.user_login(),
                "id": 1
                })
        request = urllib2.Request(self.url,data) 
        for key in self.header: 
            request.add_header(key, self.header[key]) 

        try: 
            result = urllib2.urlopen(request) 
        except URLError as e: 
            if hasattr(e, 'reason'): 
                print 'We failed to reach a server.' 
                print 'Reason: ', e.reason 
            elif hasattr(e, 'code'): 
                print 'The server could not fulfill the request.' 
                print 'Error code: ', e.code 
        else: 
            response = json.loads(result.read()) 
            #print response
                for host in response['result']:
                    print "HostID : %st HostName : %st"%(host['hostid'],host['name'])
            result.close() 
            #print "主机数量: 33[31m%s33[0m"%(len(response['result']))

if __name__ == "__main__":
        zabbix=zabbix_api()
        zabbix.host_get()

3. 根据主机ID获取主机IP地址

#!/usr/bin/python 
#coding:utf-8 

import json 
import urllib2 
from urllib2 import URLError 
import sys,argparse

class zabbix_api: 
    def __init__(self):
        self.url = 'http://10.0.8.8/api_jsonrpc.php'
        self.header = {"Content-Type":"application/json"}

    def user_login(self): 
        data = json.dumps({ 
                           "jsonrpc": "2.0", 
                           "method": "user.login", 
                           "params": { 
                                      "user": "pengdongwen",            #修改用户名
                                      "password": "pengdongwen"         #修改密码
                                      }, 
                           "id": 0 
                           }) 

        request = urllib2.Request(self.url, data) 
        for key in self.header: 
            request.add_header(key, self.header[key]) 

        try: 
            result = urllib2.urlopen(request) 
        except URLError as e: 
            print "33[041m 用户认证失败,请检查 !33[0m", e.code 
        else: 
            response = json.loads(result.read()) 
            result.close() 
            self.authID = response['result'] 
            return self.authID 

    def host_ip(self): 
        data=json.dumps({
                "jsonrpc": "2.0",
                "method": "hostinterface.get",
                "params": {
                          "output": ["hostid","ip"],
                              "hostids": ["10031"]
                          },
                "auth": self.user_login(),
                "id": 1
                })
        request = urllib2.Request(self.url,data) 
        for key in self.header: 
            request.add_header(key, self.header[key]) 

        try: 
            result = urllib2.urlopen(request) 
        except URLError as e: 
            if hasattr(e, 'reason'): 
                print 'We failed to reach a server.' 
                print 'Reason: ', e.reason 
            elif hasattr(e, 'code'): 
                print 'The server could not fulfill the request.' 
                print 'Error code: ', e.code 
        else: 
            response = json.loads(result.read()) 
            #print response
                for host in response['result']:
                    print "HostID: %st IP: %st"%(host['hostid'],host['ip'])
            result.close() 
            print "主机数量: 33[31m%s33[0m"%(len(response['result']))

if __name__ == "__main__":
        zabbix=zabbix_api()
        zabbix.host_ip()

4. 多功能API

#!/usr/bin/python 
#coding:utf-8 

import json 
import urllib2 
from urllib2 import URLError 
import sys,argparse

class zabbix_api: 
    def __init__(self):
        self.url = 'http://nh.monitor.ejuops.com/api_jsonrpc.php'
        self.header = {"Content-Type":"application/json"}

    def user_login(self): 
        data = json.dumps({ 
                           "jsonrpc": "2.0", 
                           "method": "user.login", 
                           "params": { 
                                      "user": "pengdongwen",            #修改用户名
                                      "password": "pengdongwen@eju" #修改密码
                                      }, 
                           "id": 0 
                           }) 

        request = urllib2.Request(self.url, data) 
        for key in self.header: 
            request.add_header(key, self.header[key]) 

        try: 
            result = urllib2.urlopen(request) 
        except URLError as e: 
            print "33[041m 用户认证失败,请检查 !33[0m", e.code 
        else: 
            response = json.loads(result.read()) 
            result.close() 
            #print response['result'] 
            self.authID = response['result'] 
            return self.authID 

    def host_get(self,hostName=''): 
        data=json.dumps({
                "jsonrpc": "2.0",
                "method": "host.get",
                "params": {
                          "output": "extend",
                          "filter":{"host":hostName} 
                          },
                "auth": self.user_login(),
                "id": 1
                })
        request = urllib2.Request(self.url,data) 
        for key in self.header: 
            request.add_header(key, self.header[key]) 


        try: 
            result = urllib2.urlopen(request) 
        except URLError as e: 
            if hasattr(e, 'reason'): 
                print 'We failed to reach a server.' 
                print 'Reason: ', e.reason 
            elif hasattr(e, 'code'): 
                print 'The server could not fulfill the request.' 
                print 'Error code: ', e.code 
        else: 
            response = json.loads(result.read()) 
            #print response
            result.close() 
            print "主机数量: 33[31m%s33[0m"%(len(response['result']))
            for host in response['result']:      
                    status={"0":"OK","1":"Disabled"}
            available={"0":"Unknown","1":"available","2":"Unavailable"}
            #print host
            if len(hostName)==0:
                        print "HostID : %st HostName : %st Status :33[32m%s33[0m t Available :33[31m%s33[0m"%(host['hostid'],host['name'],status[host['status']],available[host['available']])
            else:
                        print "HostID : %st HostName : %st Status :33[32m%s33[0m t Available :33[31m%s33[0m"%(host['hostid'],host['name'],status[host['status']],available[host['available']])
                return host['hostid']

    def hostgroup_get(self, hostgroupName=''): 
        data = json.dumps({ 
                           "jsonrpc":"2.0", 
                           "method":"hostgroup.get", 
                           "params":{ 
                                     "output": "extend", 
                                     "filter": { 
                                                "name": hostgroupName 
                                                } 
                                     }, 
                           "auth":self.user_login(), 
                           "id":1, 
                           }) 

        request = urllib2.Request(self.url,data) 
        for key in self.header: 
            request.add_header(key, self.header[key]) 

        try: 
            result = urllib2.urlopen(request) 
        except URLError as e: 
            print "Error as ", e 
        else: 
            #print result.read()
            response = json.loads(result.read()) 
            result.close() 
            #print response()
            for group in response['result']:
                if  len(hostgroupName)==0:
                    print "hostgroup:  33[31m%s33[0m tgroupid : %s" %(group['name'],group['groupid'])
            else:
                    print "hostgroup:  33[31m%s33[0mtgroupid : %s" %(group['name'],group['groupid'])
                    self.hostgroupID = group['groupid'] 
                    return group['groupid'] 


    def template_get(self,templateName=''): 
        data = json.dumps({ 
                           "jsonrpc":"2.0", 
                           "method": "template.get", 
                           "params": { 
                                      "output": "extend", 
                                      "filter": { 
                                                 "name":templateName                                                        
                                                 } 
                                      }, 
                           "auth":self.user_login(), 
                           "id":1, 
                           })

        request = urllib2.Request(self.url, data) 
        for key in self.header: 
            request.add_header(key, self.header[key]) 

        try: 
            result = urllib2.urlopen(request) 
        except URLError as e: 
            print "Error as ", e 
        else: 
            response = json.loads(result.read()) 
            result.close() 
            #print response
            for template in response['result']:                
                if len(templateName)==0:
                    print "template : 33[31m%s33[0mt  id : %s" % (template['name'], template['templateid'])
                else:
                    self.templateID = response['result'][0]['templateid'] 
            print "Template Name :  33[31m%s33[0m "%templateName
                    return response['result'][0]['templateid']
    def hostgroup_create(self,hostgroupName):

        if self.hostgroup_get(hostgroupName):
            print "hostgroup  33[42m%s33[0m is exist !"%hostgroupName
            sys.exit(1)
        data = json.dumps({
                          "jsonrpc": "2.0",
                          "method": "hostgroup.create",
                          "params": {
                          "name": hostgroupName
                          },
                          "auth": self.user_login(),
                          "id": 1
                          })
        request=urllib2.Request(self.url,data)

        for key in self.header: 
            request.add_header(key, self.header[key]) 

        try: 
            result = urllib2.urlopen(request)
        except URLError as e: 
            print "Error as ", e 
        else: 
            response = json.loads(result.read()) 
            result.close()
            print "33[042m 添加主机组:%s33[0m  hostgroupID : %s"%(hostgroupName,response['result']['groupids'])



    def host_create(self, hostip, hostgroupName, templateName): 
        if self.host_get(hostip):
        print "33[041m该主机已经添加!33[0m" 
        sys.exit(1)
            if self.hostgroup_get(hostgroupName):
                print "33[041m该主机组存在!33[0m"
            else:
                data = json.dumps({
                                  "jsonrpc": "2.0",
                                  "method": "hostgroup.create",
                                  "params": {
                                  "name": hostgroupName
                                  },
                                  "auth": self.user_login(),
                                  "id": 1
                })
                request=urllib2.Request(self.url,data)

                for key in self.header:
                    request.add_header(key, self.header[key])

                try:
                    result = urllib2.urlopen(request)
                except URLError as e:
                    print "Error as ", e
                else:
                    response = json.loads(result.read())
                    result.close()
                    #print "33[042m 添加主机组:%s33[0m  hostgroupID : %s"%(hostgroupName,response['result']['groupids'])

        group_list=[]
        template_list=[]
        for i in hostgroupName.split(','):
            var = {}
            var['groupid'] = self.hostgroup_get(i)
            group_list.append(var)
        for i in templateName.split(','):
            var={}
            var['templateid']=self.template_get(i)
            template_list.append(var)   

        data = json.dumps({ 
                           "jsonrpc":"2.0", 
                           "method":"host.create", 
                           "params":{ 
                                     "host": hostip, 
                                     "interfaces": [ 
                                     { 
                                     "type": 1, 
                                     "main": 1, 
                                     "useip": 1, 
                                     "ip": hostip, 
                                     "dns": "", 
                                     "port": "10050" 
                                      } 
                                     ], 
                                   "groups": group_list,
                                   "templates": template_list,
                                     }, 
                           "auth": self.user_login(), 
                           "id":1                   
        }) 
        request = urllib2.Request(self.url, data) 
        for key in self.header: 
            request.add_header(key, self.header[key]) 

        try: 
            result = urllib2.urlopen(request) 
        except URLError as e: 
            print "Error as ", e 
        else: 
            response = json.loads(result.read()) 
            result.close() 
            print "添加主机 : 33[42m%s31[0m tid :33[31m%s33[0m" % (hostip, response['result']['hostids']) 



    def host_disable(self,hostip):
        data=json.dumps({
        "jsonrpc": "2.0",
        "method": "host.update",
        "params": {
        "hostid": self.host_get(hostip),
        "status": 1
        },
        "auth": self.user_login(),
        "id": 1
        })
        request = urllib2.Request(self.url,data)
            for key in self.header:
                request.add_header(key, self.header[key])       
            try: 
                result = urllib2.urlopen(request)
            except URLError as e: 
                print "Error as ", e 
            else: 
                response = json.loads(result.read()) 
                result.close()
                print '----主机现在状态------------'
            print self.host_get(hostip)


    def host_delete(self,hostid):
        hostid_list=[]
        #print type(hostid)
        for i in hostid.split(','):
            var = {}
            var['hostid'] = self.host_get(i)
            hostid_list.append(var)      
        data=json.dumps({
                "jsonrpc": "2.0",
                "method": "host.delete",
                "params": hostid_list,
                "auth": self.user_login(),
                "id": 1
                })

        request = urllib2.Request(self.url,data) 
        for key in self.header: 
            request.add_header(key, self.header[key]) 

        try: 
            result = urllib2.urlopen(request) 
        except Exception,e: 
            print  e
        else: 

            result.close() 
            print "主机 33[041m %s33[0m  已经删除 !"%hostid 


if __name__ == "__main__":
    zabbix=zabbix_api()
    parser=argparse.ArgumentParser(description='zabbix  api ',usage='%(prog)s [options]')
    parser.add_argument('-H','--host',nargs='?',dest='listhost',default='host',help='查询主机')
    parser.add_argument('-G','--group',nargs='?',dest='listgroup',default='group',help='查询主机组')
    parser.add_argument('-T','--template',nargs='?',dest='listtemp',default='template',help='查询模板信息')
    parser.add_argument('-A','--add-group',nargs=1,dest='addgroup',help='添加主机组')
    parser.add_argument('-C','--add-host',dest='addhost',nargs=3,metavar=('192.168.2.1', 'test01,test02', 'Template01,Template02'),help='添加主机,多个主机组或模板使用分号')
    parser.add_argument('-d','--disable',dest='disablehost',nargs=1,metavar=('192.168.2.1'),help='禁用主机')
    parser.add_argument('-D','--delete',dest='deletehost',nargs='+',metavar=('192.168.2.1'),help='删除主机,多个主机之间用分号')
    parser.add_argument('-v','--version', action='version', version='%(prog)s 1.0')
    if len(sys.argv)==1:
        print parser.print_help()
    else:
        args=parser.parse_args()

        if args.listhost != 'host' :
            if args.listhost:
                zabbix.host_get(args.listhost)
            else:
                zabbix.host_get()
        if args.listgroup !='group':
            if args.listgroup:
                zabbix.hostgroup_get(args.listgroup)
            else:
                zabbix.hostgroup_get()
        if args.listtemp != 'template':
            if args.listtemp:
                zabbix.template_get(args.listtemp)
            else:
                zabbix.template_get()
        if args.addgroup:
            zabbix.hostgroup_create(args.addgroup[0])
        if args.addhost:
            zabbix.host_create(args.addhost[0], args.addhost[1], args.addhost[2])
        if args.disablehost:
            zabbix.host_disable(args.disablehost)
        if args.deletehost:
            zabbix.host_delete(args.deletehost[0])

完结。。。

Zabbix部署及邮件报警(mysql主从 lamp)

一、部署说明

本次部署原本调用5台虚拟机,详情如下:

  • 192.168.8.134–NFS-ZABBIX-SERVER-WEB(用于存放LAP发布目录以及zabbix服务器端)

  • 192.168.8.135—LAP(用于安装Apache以及PHP)

  • 192.168.8.136—MYCAT(用于做mysql读写分离)

  • 192.168.8.137–MYSQL-MASTER(mysql主库)

  • 192.168.8.137–MYSQL-SLAVE(mysql从库)

注:由于笔者试图用mycat作为中间件来使mysql读写分离,但由于zabbix连接mycat一直连接错误,所以最后放弃使用mycat,zabbix直接连接mysql-master,后续在研究!

若文章中命令复制到CLI中执行出错,请手动输入一遍,命令是没问题,可能是格式问题。

二、部署拓扑

未分类

三、详细部署步骤

3.1 安装LAP环境

(Apache发布目录位于zabbix主机的/data/upload,使用nfs挂载到Apache主机,nfs搭建请查看我其他文档,这里不赘述)

[root@localhost ~]# yum install httpd httpd-devel php php-devel php-mysql –y

注意此处安装的为php5.3,由于Zabbix3.2+ PHP版本需要使用PHP5.4.0版本,请将本机PHP版本升级至5.4.0+,PHP5.3升级至PHP5.6

  • 更新yum源,如果是centos7就使用7的yum源
[root@localhost ~]#  rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-6.noarch.rpm
[root@localhost ~]# rpm -Uvh https://mirror.webtatic.com/yum/el6/latest.rpm
  • 移除之前安装的PHP
[root@localhost ~]# yum remove php* -y
  • 安装新版本PHP
[root@localhost ~]# yum install php56w.x86_64 php56w-cli.x86_64 
php56w-common.x86_64 php56w-gd.x86_64 php56w-ldap.x86_64 
php56w-mbstring.x86_64 php56w-mcrypt.x86_64 php56w-mysql.x86_64 php56w-pdo.x86_64 -y

3.2 部署mysql主从

3.2.1 Mysql主从原理

未分类

MySQL 主从复制原理剖析

Mysql主从同步其实是一个异步复制的过程,要实现复制首先需要在master上开启bin-log日志功能,整个过程需要开启3个线程,分别是Master开启IO线程,slave开启IO线程和SQL线程。

  • 在从服务器执行slave start,从服务器上IO线程会通过授权的用户连接上master,并请求master从指定的文件和位置之后发送bin-log日志内容。

  • Master服务器接收到来自slave服务器的IO线程的请求后,master服务器上的IO线程根据slave服务器发送的指定bin-log日志之后的内容,然后返回给slave端的IO线程。(返回的信息中除了bin-log日志内容外,还有本次返回日志内容后在master服务器端的新的binlog文件名以及在binlog中的下一个指定更新位置。)

  • Slave的IO线程接收到信息后,将接收到的日志内容依次添加到Slave端的relay-log文件的最末端,并将读取到的Master端的 bin-log的文件名和位置记录到master.info文件中,以便在下一次读取的时候能够清楚的告诉Master”我需要从某个bin-log的哪 个位置开始往后的日志内容,请发给我”;

  • Slave的Sql线程检测到relay-log中新增加了内容后,会马上解析relay-log的内容成为在Master端真实执行时候的那些可执行的内容,并在自身执行。

3.2.2安装mysql

  • 安装mysql环境,master和slave都执行
[root@localhost ~]# yum install mysql-server mysql-devel mysql-libs –y

启动mysql数据库,此时数据会自动初始化

[root@localhost ~]# service mysqld restart

master修改my.cnf文件

[root@localhost ~]# vim /etc/my.cnf

在文件[mysqld]中添加如下内容:

log-bin=mysql-bin 开启binlog功能

server-id = 137 指定服务器的id,通常id指定为ip地址的最后一位

若需要指定数据目录请修改datadir=/var/lib/mysql,创建目录后需要给mysql授权,最好是把用户和组指定给mysql,本实验不指定。

未分类

  • slave修改my.cnf文件

未分类

  • 在master上面给slave授权
mysql> grant all on *.* to slave@"%" identified by "123456";

其中all代表所有权限

%代表除本机外所有ip的主机均可连接,若本机连接需要把%改为localhost

mysql> flush privileges;

刷新权限

  • 重启master及slave

未分类

  • 查看mysql-bin及Position

在master上执行

mysql> show master status;

未分类

记录file及position

  • 在slave服务器指定master IP和同步的mysql-bin及pos点
mysql> change master to master_host='192.168.8.137',master_user='slave',master_password='123456',master_log_file='mysql-bin.000001',master_log_pos=106;

注意以上红色字体,必须按照实际填写

未分类

  • 启动slave
mysql> slave start;

未分类

在slave上查看同步状态

mysql> show slave statusG

如果如下两行为yes,代表主从同步成功。

未分类

3.3 安装zabbix server环境及web发布目录

  • 下载zabbix源码包,本实验使用3.2.6
[root@localhost tmp]# wget https://sourceforge.net/projects/zabbix/files/ZABBIX%20Latest%20Stable/3.2.6/zabbix-3.2.6.tar.gz
  • 安装依赖包
[root@localhost tmp]# yum -y install curl curl-devel net-snmp net-snmp-devel perl-DBI
  • 创建zabbix用户信息
[root@localhost tmp]# groupadd zabbix
[root@localhost tmp]# useradd -g zabbix zabbix
[root@localhost tmp]# usermod -s /sbin/nologin zabbix
  • 解压
[root@localhost tmp]# tar -zxvf zabbix-3.2.6.tar.gz

进入到zabbix目录

[root@localhost tmp]# cd zabbix-3.2.6
  • 把zabbix的SQL包导入mysql-master

由于zabbix与mysql是分开部署,故在zabbix服务器上安装mysql客户端并远程登陆到mysql-master上导入sql包。

[root@localhost tmp]# yum install mysql –y

安装mysql客户端

[root@localhost]# mysql -h 192.168.8.137 -uslave -p123456

此处使用之前创建的slave账户,若为安全着想可以单独创建管理账户,由于我之前slave账户给的all权限,故此次直接使用,后面不加端口默认使用3306

未分类

mysql> create database zabbix charset=utf8;

创建zabbix数据库并指定编码类型为utf8

未分类

未分类

mysql> grant all on zabbix.* to zabbix@"%" identified by '123456';

创建zabbix账户给zabbix使用,但注意如下报错:

未分类

此处是因为我在zabbix主机使用slave登陆的,所以现在需要直接到mysql-master上面使用以上命令来授权并刷新权限。

未分类

现在数据库与授权都做好了,可以开始导入sql包了

找到sql包

未分类

如上图所示,zabbix自带的有多种数据库包,当前我们直接使用mysql的即可;

未分类

进入到mysql程序中;

mysql> use zabbix;

进入zabbix库

mysql> source /tmp/zabbix-3.2.6/database/mysql/schema.sql

先导入schema包

mysql> source /tmp/zabbix-3.2.6/database/mysql/images.sql

再导入images包

mysql> source /tmp/zabbix-3.2.6/database/mysql/data.sql

最后导入data包

至此zabbix使用的sql包完全导入。

  • 开始编译安装zabbix(源码安装三步骤)
[root@localhost zabbix-3.2.6]# ./configure --prefix=/usr/local/zabbix/ --enable-server --enable-agent --with-mysql --enable-ipv6 --with-net-snmp --with-libcurl

注:全新服务器安装会报如下错误,是因为没有安装gcc

未分类

[root@localhost zabbix-3.2.6]# yum install gcc –y

安装GCC后再次编译还会遇到一个问题如下图:

未分类

提示找不到mysql library,如果是数据库在本地不会报这个错误。

解决办法有两种:

  • 修改—with-mysql为–with-mysql=/usr/local/mysql55

  • 安装mysql-devel:yum install mysql-devel –y

编译完成会提示如下图:

未分类

但还是建议先执行make

[root@localhost zabbix-3.2.6]# make –j8 && make install –j8

软连接服务程序到/usr/local/sbin/下

[root@localhost /]# ln -s /usr/local/zabbix/sbin/zabbix_* /usr/local/sbin/

进入到zabbix主配置文件目录

未分类

  • 修改主配置文件zabbix_server.conf
[root@localhost etc]# cp zabbix_server.conf zabbix_server.conf.bak

先备份然后修改

未分类

把主配置文件内容修改为如下内容:

LogFile=/tmp/zabbix_server.log log  #存放路径
DBHost=192.168.8.137                      #数据库master地址
DBName=zabbix                               #数据库名字
DBUser=zabbix                                  #数据库用户名
DBPassword=123456                          #数据库密码

未分类

  • cp zabbix_server启动脚本至/etc/init.d/目录并授权
[root@localhost zabbix-3.2.6]# cp misc/init.d/tru64/zabbix_server /etc/init.d/zabbix_server

未分类

注:若上述启动文件不可用,可以cp如下位置文件

cp /zabbix-2.2.6/misc/init.d/fedora/core/zabbix_server /etc/init.d/

讲zabbix web代码拷贝到Apache发布目录

cp -a /root/zabbix-3.2.6/frontends/php/* /data/

修改PHP时区

[root@localhost ~]# sed -i '/date.timezone/i date.timezone = PRC' /etc/php.ini

以上命令在LAP主机上执行

  • 启动zabbix server
[root@localhost data]# /etc/init.d/zabbix_server restart

查看zabbix启动状态

root@localhost data]# ps -ef | grep zabbix

未分类

[root@localhost data]# netstat -anlp | grep zabbix

访问Apache主页

未分类

至此整体环境已经搭建完成。

3.4 安装配置zabbix web

  • 解决PHP参数与依赖报错

点击下一步会遇到如下报错,这是由于PHP里的参数与依赖不符合zabbix要求

未分类

需要在LAP主机上安装并修改PHP主配置文件,若不安装zabbix会确实部分功能

[root@localhost html]# yum install php56w-mbstring php56w-bcmath php56w-gd php56w-xml –y
[root@localhost html]# yum install gd gd-devel -y

以上是安装依赖

[root@localhost html]# sed -i '/post_max_size/s/8/16/g;/max_execution_time/s/30/300/g;/max_input_time/s/60/300/g;s/;date.timezone.*/date.timezone = PRC/g;s/;always_populate_raw_post_data/always_populate_raw_post_data/g' /etc/php.ini

以上是利用sed修改PHP主配置文件,sed用法不赘述

安装修改完成后,刷新页面

未分类

  • 填写数据库信息

未分类

  • 填写监控信息

未分类

若zabbix与Apache不在一个服务器 host可以填写zabbix服务器地址,此处暂不填写,后面会报错,后面处理

  • 最后核对信息

未分类

  • 安装完成

未分类

注意此处由于我实现给发布目录赋予了写入权限,所以不会报如下图错误:

未分类

遇此错误是因为该路径没有写入权限,可以下载图中提示的文件,然后上传到相应目录即可。

  • 点击完成,登入系统,默认用户名为admin,密码为zabbix

  • 解决Zabbix server is not running

进入系统遇到如下错误

未分类

这是由于zabbix与web 发布软件Apache不在一个服务器导致,需要求该发布目录中一下文件

未分类

修改$ZBX_SERVER后跟zabbix服务器ip

未分类

重启Apache、zabbix服务端

未分类

至此全部部署完成。

3.5 zabbix邮件报警

  • 设置发件信息

进入管理à报警媒介类型àEmail

未分类

设置发件服务器信息,最后点击更新;

未分类

选择接受人信息,这里选择admin就可以;

未分类

选择报警媒介,点击添加;

未分类

填写收件人邮箱,也可以根据需求设置时间和报警级别,通常全选,点击添加;(可添加多人)

未分类

点击更新即可;

未分类

点击配置à动作选择事件源”触发器”,可以新建也可以使用默认的;

未分类

选择动作,添加触发条件,这里使用”触发器示警度”+”大于等于”+”警告”,代表警告级别以上就触发报警,点击添加;

未分类

点击操作,修改默认操作步骤时间为60,再点击新的;

未分类

点击发送到用户的添加,然后会弹出右边对话框,选择admin(若收件人不在admin里面则选择自己定义的);

未分类

此页可不修改,也可根据自己需求修改,最后点击更新;

未分类

至此,邮件告警配置完成,以下为测试告警邮件。

未分类

未分类

xtrabackup增量、全量备份mysql innodb教程

xtrabackup是Percona公司CTO Vadim参与开发的一款基于InnoDB的在线热备工具,具有开源,免费,支持在线热备,备份恢复速度快,占用磁盘空间小等特点,并且支持不同情况下的多种备份形式。xtrabackup的官方下载地址为http://www.percona.com/software/percona-xtrabackup。

xtrabackup包含两个主要的工具,即xtrabackup和innobackupex,二者区别如下:

  • xtrabackup只能备份innodb和xtradb两种引擎的表,而不能备份myisam引擎的表;

  • innobackupex是一个封装了xtrabackup的Perl脚本,支持同时备份innodb和myisam,但在对myisam备份时需要加一个全局的读锁。还有就是myisam不支持增量备份。

一、备份过程

innobackupex备份过程如下图:

未分类
(图1 innobackupex备份过程,本文中所有图都是google所得)

在图1中,备份开始时首先会开启一个后台检测进程,实时检测mysql redo的变化,一旦发现redo中有新的日志写入,立刻将日志记入后台日志文件xtrabackup_log中。之后复制innodb的数据文件和系统表空间文件ibdata1,待复制结束后,执行flush tables with read lock操作,复制.frm,MYI,MYD,等文件(执行flush tableswith read lock的目的是为了防止数据表发生DDL操作,并且在这一时刻获得binlog的位置)最后会发出unlock tables,把表设置为可读可写状态,最终停止xtrabackup_log。

二、全备恢复

这一阶段会启动xtrabackup内嵌的innodb实例,回放xtrabackup日志xtrabackup_log,将提交的事务信息变更应用到innodb数据/表空间,同时回滚未提交的事务(这一过程类似innodb的实例恢复)。恢复过程如下图:

未分类
(图2 innobackupex 恢复过程)

三、增量备份

innobackupex增量备份过程中的”增量”处理,其实主要是相对innodb而言,对myisam和其他存储引擎而言,它仍然是全拷贝(全备份)

“增量”备份的过程主要是通过拷贝innodb中有变更的”页”(这些变更的数据页指的是”页”的LSN大于xtrabackup_checkpoints中给定的LSN)。增量备份是基于全备的,第一次增备的数据必须要基于上一次的全备,之后的每次增备都是基于上一次的增备,最终达到一致性的增备。增量备份的过程如下,和全备的过程很类似,区别仅在第2步。

未分类
( 图 3 innobackupex增量备份过程)

四、增量备份恢复

和全备恢复类似,也需要两步,一是数据文件的恢复,如图4,这里的数据来源由3部分组成:全备份,增量备份和xtrabackup log。二是对未提交事务的回滚,如图5所示:

未分类
( 图4 innobackupex 增量备份恢复过程1)

未分类
( 图5 innobackupex增量备份恢复过程2)

五、innobackupex使用示例

1、安装使用xtrabackup

安装比较简单,我们使用二进制编译好的就行了,这种工具无需源码编译,因为没有什么功能需要俺们定制。

[root@MySQL-01 ~]# wget http://www.percona.com/redir/downloads/XtraBackup/LATEST/binary/Linux/x86_64/percona-xtrabackup-2.1.8-733-Linux-x86_64.tar.gz
[root@MySQL-01 ~]# tar xf percona-xtrabackup-2.1.8-733-Linux-x86_64.tar.gz -C /usr/local/
[root@MySQL-01 ~]# mv /usr/local/percona-xtrabackup-2.1.8-Linux-x86_64/ /usr/local/xtrabackup
[root@MySQL-01 ~]# echo "export PATH=$PATH:/usr/local/xtrabackup/bin" >> /etc/profile
[root@MySQL-01 ~]# source /etc/profile
[root@MySQL-01 ~]#

2、全量备份

创建备份用户:

mysql> create user 'backup'@'%' identified by 'yayun';
Query OK, 0 rows affected (0.01 sec)

mysql> grant reload,lock tables,replication client,create tablespace,super on *.* to 'backup'@'%';
Query OK, 0 rows affected (0.00 sec)

mysql>

进行全备份

备份数据存放在/data/backup/下面,innobackupex会自动创建一个文件夹,是当前系统的时间戳

mysql> select * from yayun.t1;
+------+-------+
| id   | name  |
+------+-------+
|    1 | yayun |
|    2 | atlas |
+------+-------+
2 rows in set (0.00 sec)

mysql>

测试数据就是yayun库中的t1表 (错误:–host=192.168.199.1

[root@MySQL-01 ~]# innobackupex --user=backup --password=yayun --socket=/tmp/mysqld.sock --defaults-file=/etc/my.cnf /data/backup/
xtrabackup: Creating suspend file '/data/backup/2014-04-07_23-05-04/xtrabackup_log_copied' with pid '57608'
xtrabackup: Transaction log of lsn (5324782783) to (5324782783) was copied.
140407 23:06:14  innobackupex: All tables unlocked
innobackupex: Backup created in directory '/data/backup/2014-04-07_23-05-04'
innobackupex: MySQL binlog position: filename 'mysql-bin.000014', position 2983
140407 23:06:14  innobackupex: Connection to database server closed
140407 23:06:14  innobackupex: completed OK!
[root@MySQL-01 ~]#

上面的过程中处理过,主要看最后是否提示innobackupex completed ok,可以看见备份成功。我们看看/data/backup目录下产生了什么复制代码

[root@MySQL-01 backup]# pwd
/data/backup
[root@MySQL-01 backup]# ll
total 4
drwxr-xr-x 12 root root 4096 Apr  7 23:06 2014-04-07_23-05-04
[root@MySQL-01 backup]# cd 2014-04-07_23-05-04/
[root@MySQL-01 2014-04-07_23-05-04]# ll
total 845888
-rw-r--r-- 1 root root       261 Apr  7 23:05 backup-my.cnf
drwx------ 2 root root      4096 Apr  7 23:06 employees
drwx------ 2 root root      4096 Apr  7 23:06 host
-rw-r----- 1 root root 866123776 Apr  7 23:05 ibdata1
drwx------ 2 root root      4096 Apr  7 23:06 menagerie
drwxr-xr-x 2 root root      4096 Apr  7 23:06 mysql
drwxr-xr-x 2 root root      4096 Apr  7 23:06 performance_schema
drwx------ 2 root root      4096 Apr  7 23:06 sakila
drwx------ 2 root root      4096 Apr  7 23:06 test
drwx------ 2 root root      4096 Apr  7 23:06 world_innodb
drwxr-xr-x 2 root root      4096 Apr  7 23:06 world_myisam
-rw-r--r-- 1 root root        13 Apr  7 23:06 xtrabackup_binary
-rw-r--r-- 1 root root        24 Apr  7 23:06 xtrabackup_binlog_info
-rw-r----- 1 root root        95 Apr  7 23:06 xtrabackup_checkpoints
-rw-r----- 1 root root      2560 Apr  7 23:06 xtrabackup_logfile
drwx------ 2 root root      4096 Apr  7 23:06 yayun
[root@MySQL-01 2014-04-07_23-05-04]#

可以看见有对应数据库的名字,比如yayun,还有一个以时间戳命名的目录。我们看看对应文件里面的内容,这几个比较重要

[root@MySQL-01 2014-04-07_23-05-04]# cat xtrabackup_checkpoints 
backup_type = full-backuped
from_lsn = 0
to_lsn = 5324782783
last_lsn = 5324782783
compact = 0
[root@MySQL-01 2014-04-07_23-05-04]# cat xtrabackup_binlog_info 
mysql-bin.000014        2983
[root@MySQL-01 2014-04-07_23-05-04]#

可以看见相关文件记录了LSN,日志偏移量,还可以看见这次是全备份,相信聪明的童鞋们一眼就看懂了。^_^

删除数据库,然后恢复全备(线上不要这样搞)

mysql> drop database yayun;
Query OK, 1 row affected (0.04 sec)

mysql>

恢复全备

恢复备份到mysql的数据文件目录,这一过程要先关闭mysql数据库,重命名或者删除原数据文件目录都可以,再创建一个新的数据文件目录,将备份数据复制到新的数据文件目录下,赋权,修改权限,启动数据库

[root@MySQL-01 ~]# /etc/init.d/mysqld stop
Shutting down MySQL.....                                   [  OK  ]
[root@MySQL-01 ~]# mv /data/mysql /data/mysql_bak
[root@MySQL-01 ~]# mkdir /data/mysql
[root@MySQL-01 ~]#
[root@MySQL-01 ~]# innobackupex --apply-log /data/backup/2014-04-07_23-05-04/ 
xtrabackup: starting shutdown with innodb_fast_shutdown = 1
140407 23:22:36  InnoDB: Starting shutdown...
140407 23:22:40  InnoDB: Shutdown completed; log sequence number 5324784140
140407 23:22:40  innobackupex: completed OK!

以上对应的目录就是innobackupex全备份自己创建的目录。

[root@MySQL-01 ~]# innobackupex --defaults-file=/etc/my.cnf --copy-back --rsync /data/backup/2014-04-07_23-05-04/
innobackupex: Starting to copy InnoDB log files
innobackupex: in '/data/backup/2014-04-07_23-05-04'
innobackupex: back to original InnoDB log directory '/data/mysql'
innobackupex: Copying '/data/backup/2014-04-07_23-05-04/ib_logfile1' to '/data/mysql/ib_logfile1'
innobackupex: Copying '/data/backup/2014-04-07_23-05-04/ib_logfile0' to '/data/mysql/ib_logfile0'
innobackupex: Finished copying back files.
140407 23:27:38  innobackupex: completed OK!
[root@MySQL-01 ~]#

可以看见已经成功恢复,修改数据目录权限,启动mysql,效验数据是否正常,查看yayun库下面的t1表中的数据。

[root@MySQL-01 ~]# chown -R mysql.mysql /data/mysql
[root@MySQL-01 ~]# /etc/init.d/mysqld start
Starting MySQL.................                            [  OK  ]
[root@MySQL-01 ~]#
mysql> use yayun
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> select * from t1;
+------+-------+
| id   | name  |
+------+-------+
|    1 | yayun |
|    2 | atlas |
+------+-------+
2 rows in set (0.00 sec)

mysql>

发现数据已经成功恢复。

3、增量备份

在进行增量备份时,首先要进行一次全量备份,第一次增量备份是基于全备的,之后的增量备份是基于上一次的增量备份,以此类推。

全备份放在/data/backup/full,增量备份放在/data/backup/incremental

[root@MySQL-01 ~]# tree /data/backup/
/data/backup/
├── full
└── incremental

2 directories, 0 files
[root@MySQL-01 ~]#

废话少说,咱们先来一次全备份

[root@MySQL-01 ~]# innobackupex --user=backup --password=yayun --socket=/tmp/mysqld.sock --defaults-file=/etc/my.cnf /data/backup/full/
innobackupex: Backup created in directory '/data/backup/full/2014-04-07_23-37-20'
innobackupex: MySQL binlog position: filename 'mysql-bin.000001', position 107
140407 23:38:29  innobackupex: Connection to database server closed
140407 23:38:29  innobackupex: completed OK!
[root@MySQL-01 ~]#

为了测试效果,我们在t1表中插入数据

mysql> select * from t1;
+------+-------+
| id   | name  |
+------+-------+
|    1 | yayun |
|    2 | atlas |
+------+-------+
2 rows in set (0.00 sec)

mysql> insert into t1 select 1,'love sql';
Query OK, 1 row affected (0.01 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> select * from t1;                  
+------+----------+
| id   | name     |
+------+----------+
|    1 | yayun    |
|    2 | atlas    |
|    1 | love sql |
+------+----------+
3 rows in set (0.00 sec)

mysql>

现在来一次增量备份1

[root@MySQL-01 ~]# innobackupex --user=backup --password=yayun --socket=/tmp/mysqld.sock --defaults-file=/etc/my.cnf --incremental /data/backup/incremental/ --incremental-basedir=/data/backup/full/2014-04-07_23-37-20/ --parallel=2
innobackupex: Backup created in directory '/data/backup/incremental/2014-04-07_23-42-46'
innobackupex: MySQL binlog position: filename 'mysql-bin.000001', position 301
140407 23:43:25  innobackupex: Connection to database server closed
140407 23:43:25  innobackupex: completed OK!
[root@MySQL-01 ~]#

我们看看增量备份的大小以及文件内容

[root@MySQL-01 ~]# du -sh /data/backup/full/2014-04-07_23-37-20/
1.2G    /data/backup/full/2014-04-07_23-37-20/
[root@MySQL-01 ~]# du -sh /data/backup/incremental/2014-04-07_23-42-46/
3.6M    /data/backup/incremental/2014-04-07_23-42-46/
[root@MySQL-01 ~]#

看见增量备份的数据很小吧,就是备份改变的数据而已。

[root@MySQL-01 2014-04-07_23-42-46]# pwd
/data/backup/incremental/2014-04-07_23-42-46
[root@MySQL-01 2014-04-07_23-42-46]# cat xtrabackup_checkpoints 
backup_type = incremental
from_lsn = 5324784718
to_lsn = 5324785066
last_lsn = 5324785066
compact = 0
[root@MySQL-01 2014-04-07_23-42-46]#

上面已经明显说明是增量备份了,该工具很人性化吧,呵呵

我们再次向t1表插入数据,然后创建增量备份2

mysql> select * from t1;
+------+----------+
| id   | name     |
+------+----------+
|    1 | yayun    |
|    2 | atlas    |
|    1 | love sql |
+------+----------+
3 rows in set (0.00 sec)

mysql> insert into t1 select 1,'mysql dba';
Query OK, 1 row affected (0.00 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> select * from t1;                   
+------+-----------+
| id   | name      |
+------+-----------+
|    1 | yayun     |
|    2 | atlas     |
|    1 | love sql  |
|    1 | mysql dba |
+------+-----------+
4 rows in set (0.00 sec)

mysql>

创建增量备份2(这次是基于上次的增量备份哦)

[root@MySQL-01 ~]# innobackupex --user=backup --password=yayun --socket=/tmp/mysqld.sock --defaults-file=/etc/my.cnf --incremental /data/backup/incremental/ --incremental-basedir=/data/backup/incremental/2014-04-07_23-42-46/ --parallel=2
innobackupex: Backup created in directory '/data/backup/incremental/2014-04-07_23-51-15'
innobackupex: MySQL binlog position: filename 'mysql-bin.000001', position 496
140407 23:51:55  innobackupex: Connection to database server closed
140407 23:51:55  innobackupex: completed OK!
[root@MySQL-01 ~]#

[root@MySQL-01 ~]# ls -ltr /data/backup/full/
total 4
drwxr-xr-x 12 root root 4096 Apr  7 23:38 2014-04-07_23-37-20
[root@MySQL-01 ~]# ls -ltr /data/backup/incremental/
total 8
drwxr-xr-x 12 root root 4096 Apr  7 23:43 2014-04-07_23-42-46
drwxr-xr-x 12 root root 4096 Apr  7 23:51 2014-04-07_23-51-15
[root@MySQL-01 ~]#

4、增量备份恢复

增量备份的恢复大体为3个步骤

  • 恢复完全备份

  • 恢复增量备份到完全备份(开始恢复的增量备份要添加–redo-only参数,到最后一次增量备份去掉–redo-only参数)

  • 对整体的完全备份进行恢复,回滚那些未提交的数据

恢复完全备份(注意这里一定要加–redo-only参数,该参数的意思是只应用xtrabackup日志中已提交的事务数据,不回滚还未提交的数据)

[root@MySQL-01 ~]# innobackupex --apply-log --redo-only /data/backup/full/2014-04-07_23-37-20/
xtrabackup: starting shutdown with innodb_fast_shutdown = 1
140407 23:59:43  InnoDB: Starting shutdown...
140407 23:59:43  InnoDB: Shutdown completed; log sequence number 5324784718
140407 23:59:43  innobackupex: completed OK!

将增量备份1应用到完全备份

[root@MySQL-01 ~]# innobackupex --apply-log --redo-only /data/backup/full/2014-04-07_23-37-20/ --incremental-dir=/data/backup/incremental/2014-04-07_23-42-46/
innobackupex: Copying '/data/backup/incremental/2014-04-07_23-42-46/mysql/func.frm' to '/data/backup/full/2014-04-07_23-37-20/mysql/func.frm'
innobackupex: Copying '/data/backup/incremental/2014-04-07_23-42-46/mysql/help_relation.frm' to '/data/backup/full/2014-04-07_23-37-20/mysql/help_relation.frm'
innobackupex: Copying '/data/backup/incremental/2014-04-07_23-42-46/mysql/help_category.MYD' to '/data/backup/full/2014-04-07_23-37-20/mysql/help_category.MYD'
innobackupex: Copying '/data/backup/incremental/2014-04-07_23-42-46/mysql/ndb_binlog_index.frm' to '/data/backup/full/2014-04-07_23-37-20/mysql/ndb_binlog_index.frm'
140408 00:02:07  innobackupex: completed OK!
[root@MySQL-01 ~]#

将增量备份2应用到完全备份(注意恢复最后一个增量备份时需要去掉–redo-only参数,回滚xtrabackup日志中那些还未提交的数据)

[root@MySQL-01 ~]# innobackupex --apply-log /data/backup/full/2014-04-07_23-37-20/ --incremental-dir=/data/backup/incremental/2014-04-07_23-51-15/
innobackupex: Copying '/data/backup/incremental/2014-04-07_23-51-15/mysql/help_relation.frm' to '/data/backup/full/2014-04-07_23-37-20/mysql/help_relation.frm'
innobackupex: Copying '/data/backup/incremental/2014-04-07_23-51-15/mysql/help_category.MYD' to '/data/backup/full/2014-04-07_23-37-20/mysql/help_category.MYD'
innobackupex: Copying '/data/backup/incremental/2014-04-07_23-51-15/mysql/ndb_binlog_index.frm' to '/data/backup/full/2014-04-07_23-37-20/mysql/ndb_binlog_index.frm'
140408 00:04:33  innobackupex: completed OK!
[root@MySQL-01 ~]#

把所有合在一起的完全备份整体进行一次apply操作,回滚未提交的数据:

[root@MySQL-01 ~]# innobackupex --apply-log /data/backup/full/2014-04-07_23-37-20/
xtrabackup: starting shutdown with innodb_fast_shutdown = 1
140408  0:06:32  InnoDB: Starting shutdown...
140408  0:06:36  InnoDB: Shutdown completed; log sequence number 5324785676
140408 00:06:36  innobackupex: completed OK!

把恢复完的备份复制到数据库目录文件中,赋权,然后启动mysql数据库,检测数据正确性

[root@MySQL-01 ~]# /etc/init.d/mysqld stop
Shutting down MySQL.                                       [  OK  ]
[root@MySQL-01 ~]# mv /data/mysql /data/mysql_bak
[root@MySQL-01 ~]# mkdir /data/mysql
[root@MySQL-01 ~]# innobackupex --defaults-file=/etc/my.cnf --copy-back --rsync /data/backup/full/2014-04-07_23-37-20/
innobackupex: Starting to copy InnoDB log files
innobackupex: in '/data/backup/full/2014-04-07_23-37-20'
innobackupex: back to original InnoDB log directory '/data/mysql'
innobackupex: Copying '/data/backup/full/2014-04-07_23-37-20/ib_logfile1' to '/data/mysql/ib_logfile1'
innobackupex: Copying '/data/backup/full/2014-04-07_23-37-20/ib_logfile0' to '/data/mysql/ib_logfile0'
innobackupex: Finished copying back files.
140408 00:12:42  innobackupex: completed OK!
[root@MySQL-01 ~]# chown -R mysql.mysql /data/mysql
[root@MySQL-01 ~]# /etc/init.d/mysqld start
Starting MySQL....                                         [  OK  ]
[root@MySQL-01 ~]#

查看数据是否正确

mysql> select * from t1;
+------+-----------+
| id   | name      |
+------+-----------+
|    1 | yayun     |
|    2 | atlas     |
|    1 | love sql  |
|    1 | mysql dba |
+------+-----------+
4 rows in set (0.00 sec)

mysql>

5、克隆slave

在日常工作中,我们有时候需要在线添加从库,比如线上有一主一从两个数据库,但是由于业务的需要,一台从库的读取无法满足现在的需求,这样就需要我们在线添加从库,由于出于安全考虑,我们通常需要在从库上进行在线克隆slave。

克隆slave时,常用参数–slave-info和–safe-slave-backup。

–slave-info会将master的binlog文件名和偏移量位置保存到xtrabackup_slave_info文件中

–safe-slave-backup会暂停slave的SQL线程直到没有打开的临时表的时候开始备份。备份结束后SQL线程会自动启动,这样操作的目的主要是确保一致性的复制状态。

下面的例子,将介绍一主一从情况下在线搭建新的从库,环境如下:

master 192.168.0.10    #主库

slave    192.168.0.20    #从库

newslave 192.168.0.100 # 新的从库

在上述示例中,newslave即为要新搭建的从库。在老的从库上面进行备份:

[root@MySQL-02 ~]# innobackupex --user=root --password=12345 --socket=/tmp/mysqld.sock --defaults-file=/etc/my.cnf --slave-info --safe-slave-backup --no-timestamp /data/cloneslave
innobackupex: Backup created in directory '/data/cloneslave'
innobackupex: MySQL binlog position: filename 'mysql-bin.000022', position 107
innobackupex: MySQL slave binlog position: master host '192.168.0.10', filename 'mysql-bin.000006', position 732
140413 23:25:13  innobackupex: completed OK!

这里的/data/cloneslave 目录要不存在,如果存在是会报错的。

查看目录下生成的文件:

[root@MySQL-02 ~]# ll /data/cloneslave/
total 26668
-rw-r--r-- 1 root root      261 Apr 13 23:24 backup-my.cnf
-rw-r--r-- 1 root root 27262976 Apr 13 23:24 ibdata1
drwxr-xr-x 2 root root     4096 Apr 13 23:25 mysql
drwxr-xr-x 2 root root     4096 Apr 13 23:25 performance_schema
drwxr-xr-x 2 root root     4096 Apr 13 23:25 sakila
drwxr-xr-x 2 root root     4096 Apr 13 23:25 world_innodb
-rw-r--r-- 1 root root       13 Apr 13 23:25 xtrabackup_binary
-rw-r--r-- 1 root root       23 Apr 13 23:25 xtrabackup_binlog_info
-rw-r--r-- 1 root root       79 Apr 13 23:25 xtrabackup_checkpoints
-rw-r--r-- 1 root root     2560 Apr 13 23:25 xtrabackup_logfile
-rw-r--r-- 1 root root       72 Apr 13 23:25 xtrabackup_slave_info
drwxr-xr-x 2 root root     4096 Apr 13 23:25 yayun
[root@MySQL-02 ~]#

查看xtrabackup_slave_info文件内容,这个内容就是为搭建从库时需要change master to的参数:

[root@MySQL-02 ~]# cat /data/cloneslave/xtrabackup_slave_info 
CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000006', MASTER_LOG_POS=732
[root@MySQL-02 ~]#

在老的slave服务器上进行还原,即192.168.0.20

[root@MySQL-02 ~]# innobackupex --apply-log --redo-only /data/cloneslave/
xtrabackup: starting shutdown with innodb_fast_shutdown = 1
140413 23:30:37  InnoDB: Starting shutdown...
140413 23:30:37  InnoDB: Shutdown completed; log sequence number 12981048
140413 23:30:37  innobackupex: completed OK!
[root@MySQL-02 ~]#

将还原的文件复制到新的从库newslave,即192.168.0.100

[root@MySQL-02 data]# rsync -avprP -e ssh /data/cloneslave/ 192.168.0.100:/data/mysql/

在主库master上添加对新从库newslave的授权:

mysql> grant replication slave on *.* to 'repl'@'192.168.0.100' identified by '123456';
Query OK, 0 rows affected (0.00 sec)

mysql> flush privileges;
Query OK, 0 rows affected (0.02 sec)

mysql>

拷贝老的从库的配置文件到新的从库newslave,并且修改server-id参数,修改完毕后,启动新的从库;

[root@MySQL-02 data]# scp /etc/my.cnf 192.168.0.100:/etc/
 [email protected]'s password: 
 my.cnf                                                                                                             100% 4881     4.8KB/s   00:00 
[root@MySQL-02 data]#
[root@newslave mysql]# egrep 'log-slave|^server-id|skip_slave' /etc/my.cnf 
server-id       = 3
skip_slave_start
log-slave-updates=1
[root@newslave mysql]#
[root@newslave mysql]# chown -R mysql.mysql .
[root@newslave mysql]# /etc/init.d/mysqld restart
Shutting down MySQL.                                       [  OK  ]
Starting MySQL..                                           [  OK  ]
[root@newslave mysql]#

查找老的从库备份后生成的xtrabackup_slave_info文件,提取其中的master_log_file和master_log_pos信息,然后在新的从库上进行change master to操作:

在新的从库上进行同步:

mysql> CHANGE MASTER TO MASTER_HOST='192.168.0.10',MASTER_USER='repl', MASTER_PASSWORD='123456',MASTER_LOG_FILE='mysql-bin.000006', MASTER_LOG_POS=732;
Query OK, 0 rows affected (0.09 sec)

mysql>

启动io线程和sql线程,并观察复制是否正常:

mysql> start slave;
Query OK, 0 rows affected (0.00 sec)

mysql>
mysql> show slave  statusG
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 192.168.0.10
                  Master_User: repl
                  Master_Port: 3306
                Connect_Retry: 2
              Master_Log_File: mysql-bin.000006
          Read_Master_Log_Pos: 1309
               Relay_Log_File: MySQL-02-relay-bin.000002
                Relay_Log_Pos: 830
        Relay_Master_Log_File: mysql-bin.000006
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB: 
          Replicate_Ignore_DB: 
           Replicate_Do_Table: 
       Replicate_Ignore_Table: 
      Replicate_Wild_Do_Table: yayun.%
  Replicate_Wild_Ignore_Table: 
                   Last_Errno: 0
                   Last_Error: 
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 1309
              Relay_Log_Space: 989
              Until_Condition: None
               Until_Log_File: 
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File: 
           Master_SSL_CA_Path: 
              Master_SSL_Cert: 
            Master_SSL_Cipher: 
               Master_SSL_Key: 
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error: 
               Last_SQL_Errno: 0
               Last_SQL_Error: 
  Replicate_Ignore_Server_Ids: 
             Master_Server_Id: 1
1 row in set (0.00 sec)

mysql>

查看主库,发现已经有两个线程(Binlog Dump)

mysql> show processlistG
*************************** 1. row ***************************
     Id: 8
   User: slave
   Host: 192.168.0.20:44251
     db: NULL
Command: Binlog Dump
   Time: 1088
  State: Master has sent all binlog to slave; waiting for binlog to be updated
   Info: NULL
*************************** 2. row ***************************
     Id: 9
   User: root
   Host: localhost
     db: yayun
Command: Query
   Time: 0
  State: NULL
   Info: show processlist
*************************** 3. row ***************************
     Id: 10
   User: repl
   Host: 192.168.0.100:45844
     db: NULL
Command: Binlog Dump
   Time: 124
  State: Master has sent all binlog to slave; waiting for binlog to be updated
   Info: NULL
3 rows in set (0.00 sec)

mysql>

正常工作,到此在线克隆slave就结束啦。

使用Docker compose配置WordPress运行环境并支持https

最近把 WordPress 迁移到了腾讯云,为了配置方便使用了 docker 来运行,这里记录下配置过程

准备 compose 文件

WordPress 的 docker compose 文件网上有很多,需要一个 mysql 的镜像,还有 WordPress 的镜像,大概长这样:

version: '3'
services:
   db:
     image: mysql:5.7
     volumes:
       - db_data:/var/lib/mysql
     restart: always
     environment:
       MYSQL_ROOT_PASSWORD: somewordpress
       MYSQL_DATABASE: wordpress
       MYSQL_USER: wordpress
       MYSQL_PASSWORD: wordpress
   wordpress:
     depends_on:
       - db
     image: wordpress:latest
     ports:
       - "8000:80"
     restart: always
     environment:
       WORDPRESS_DB_HOST: db:3306
       WORDPRESS_DB_USER: wordpress
       WORDPRESS_DB_PASSWORD: wordpress
volumes:
    db_data:

定制 Dockerfile 添加 https 支持

借助于 letsencrypt 这个项目, 给个人网站添加 letsencrypt 变得十分容易,详细见这篇文章:

如何免费的让网站启用HTTPS: (https://coolshell.cn/articles/18094.html)

大概流程就是安装一个软件包 letsencrypt ,然后配置你的网站信息即可,但是我们的 WordPress 是安装在 docker 里面,所以我们要想办法把这个软件包打进镜像里面。

接下来我们要对 WordPress 这个镜像进行自定义,参考这篇文章:

docker + wordpress + letsencrypt: (https://breeto.id.au/2017/03/docker-wordpress-letsencrypt/)

先定制 Dockerfile,集成 letsencrypt

新建文件夹 wordpress_tls 添加 Dockerfile

FROM wordpress:php7.1
RUN echo "export TERM=xterm LANG=en_US.UTF-8" >> ~/.bashrc 
    && apt-get update && apt-get -y install git 
    && rm -rf "/opt/letsencrypt" 
    && git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt 
    && cd /opt/letsencrypt 
    && ./letsencrypt-auto --version

wordpress 官方镜像使用的 ubuntu 源是国外源,打包镜像的速度会让你怀疑人生。可以把宿主机的 ubuntu 源放进 docker 镜像里。

$cp /etc/apt/sources.list ./

修改 Dockerfile

FROM wordpress:php7.1
ADD sources.list /etc/apt/sources.list
RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 
    && apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32 // 改成你的 key
RUN echo "export TERM=xterm LANG=en_US.UTF-8" >> ~/.bashrc 
    && apt-get update && apt-get -y install git 
    && rm -rf "/opt/letsencrypt" 
    && git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt 
    && cd /opt/letsencrypt 
    && ./letsencrypt-auto --version

添加新的源会有认证的问题,可以参考 http://naveenubuntu.blogspot.com/2011/08/fixing-gpg-keys-in-ubuntu.html 解决

配置 https

启动容器:

$docker-compose up -d

然后配置 https

$docker-compose exec wordpress_tls /opt/letsencrypt/certbot-auto --apache -d your.domain.com --agree-tos -n -m [email protected]

Let’s Encrypt 的证书90天就过期了,过期后执行

$ docker-compose exec wordpress_tls /opt/letsencrypt/certbot-auto renew

来更新,可以把更新脚本写进 crontab
$crontab -e

0 0 1 * * docker-compose exec wordpress_tls /opt/letsencrypt/certbot-auto renew

完整示例

https://github.com/myfjdthink/docker-wordpress

WordPress域名更换、备份,网站数据迁移教程

如何更换WordPress网站域名

情况一、空间不变,旧域名绑定不变,新域名已做好解析

这种情况WordPress网站域名更换最容易。只需要登陆网站的后台即可操作。

进入后台,点击 设置-常规

未分类

只要修改 WordPress 地址(URL)和 站点地址(URL),然后保存,基本上就可以了。

保存后,原域名就已经不能访问后台了,这个时候就要用新域名访问后台。但是,有一个重要事情还必须做。那就是旧域名下的文章中包含的图片地址是否已经更正,固定链接是否重新设置。如果网站的文章不多,才10篇8篇的,可以手动去更新一次新域名网站的文章包含的图片地址。同时,后台需要重新设置一次固定链接格式。如果网站的文章比较多,那么手动更新文章的图片链接地址,那就会很困难了。可以通过SQL更新数据库进行。

完成以上的步骤之后,还需要注意的一点就是将旧的域名301重定向到新域名,有利于权重的传递和旧域名收录访问更新到新的域名地址。301重定向规则,请自行百度搜索,这个是很重要对seo

情况二、空间变,旧域名绑定解析失败,新域名已绑定空间并解析成功

这种情况已经不是单纯的网站更换域名,还包括了网站的迁移,相比第一种情况,就复杂了一些。

方法一、使用 WordPress 自带导出工具

步骤一、登入旧域名网站的后台,点击 工具 – 导出 如下图:

未分类

选择要导出的内容,然后 下载导出的文件 即可获得一个 XML格式的文件。

步骤二、在新的空间里,用新域名安装 WordPress ,然后,登入网站后台,点击 工具 – 导入

选择 WordPress ,将会提示下载安装导入插件。下载安装后,启用,然后选择之前导出的文件即可导入数据。

步骤三、导入数据后,还需要把旧域名的里的主题、媒体库文件上传到新的空间里。即 wp-content 里的 theme 和 uploads 目录下载到本地再上传到新空间里。

步骤四、设置对应的固定链接格式。同样,根据情况是否需要做旧域名的301重定向。

上面的方法操作相对容易,但是因为是已经重新安装了一次 WordPress ,因此,导入的数据目录,在一定程度上,和原来有所不同。

方法二、利用 PHPMyadmin 数据库管理工具备份数据库迁移

步骤一、登陆 PHPMyadmin 数据库管理工具,备份旧域名的网站数据并下载到本地。

步骤二、通过新空间的 PHPMyadmin 数据库管理工具,导入旧域名的网站数据库。

步骤三、把旧空间文件打包通过FTP下载,并上传到新的空间。

以上的三个步骤不进行具体的截图演示,可以自行谷歌搜索。

步骤四、通过 PHPMyadmin 数据库管理工具 使用 SQL 命令更新 WordPress 地址和站点地址。

未分类

命令如下:(注意把 old.com 和 new.com 替换成你的新旧域名)

UPDATE wp_options SET option_value = replace( option_value, 'http://www.old.com', 'http://www.new.com' ) WHERE option_name = 'home' OR option_name = 'siteurl'; 

UPDATE wp_posts SET post_content = replace( post_content, 'http://www.old.com', 'http://www.new.com' ) ;

UPDATE wp_posts SET commment_content = replace( commment_content, 'http://www.old.com', 'http://www.new.com' ) ;

UPDATE wp_posts SET commment_author_url = replace( commment_author_url, 'http://www.old.com', 'http://www.new.com' ) ; 

UPDATE wp_posts SET guid = replace( guid, 'http://www.old.com', 'http://www.new.com' ) ;

其中,old.com 是你的老域名,而 new.com 是新域名。如果当初你的数据库表头不是默认的wp开头,注意修改成你自己数据库的表头,很多人都忘记了这点。

步骤五、修改新空间 WordPress 数据库配置文件 wp-config.php

使用notepad++ / editplus/ Dreamwaver 打开(切勿用记事本打开)。打开后,自己的新数据库名、数据库用户名、数据库密码进行修改配置。配置完之后FTP上传覆盖。

/** WordPress数据库的名称 */

define('DB_NAME', 'liaosam');

/** MySQL数据库用户名 */

define('DB_USER', 'liaosam');

/** MySQL数据库密码 */

define('DB_PASSWORD', '这里是数据库密码');

/** MySQL主机 */

define('DB_HOST', 'localhost');   一般Linux主机这里不用动,保持localhost即可。

/** 创建数据表时默认的文字编码 */

define('DB_CHARSET', 'utf8');

完成以上步骤后,基本上也就迁移和更换了网站的新域名,同样,还是需要对文章的图片进行一次更新,避免图片不能正常显示。

还是需要登陆后台设置一下网站的固定链接格式,以及根据个人情况是否需要对旧域名进行301重定向设置。

到了这里WordPress 网站关于域名更换和迁移基本上就完成了。

注意事项:

1、如果遇到 PhpMyAdmin导入数据库文件最大限制2048KB,可以找到php.ini,ctrl+F 找到upload_max_filesize,将值修改为20MB,覆盖即可。

2、更换后,一定要检查固定链接、文章图片地址、301重定向。