SEO (Search Engine Optimization),即搜索引擎优化。对网站做SEO优化,有利于提高搜索引擎的收录速度及网页排名。下面讲解一些简单的SEO优化方法,主要针对Hexo网站。

SEO优化之title

编辑站点目录下的themes/layout/index.swig文件,将下面的代码

{% block title %} {{ config.title }} {% endlock %}

改成

{% block title %} {{ config.title }} - {{ theme.description }} {% endlock %}

这时将网站的描述及关键词加入了网站的title中,更有利于详细地描述网站。

添加robots.txt

robots.txt是一种存放于网站根目录下的ASCII编码的文本文件,它的作用是告诉搜索引擎此网站中哪些内容是可以被爬取的,哪些是禁止爬取的。robots.txt应该放在站点目录下的source文件中,网站生成后在网站的根目录(站点目录/public/)下。

我的robots.txt文件内容如下

User-agent: *
Allow: /
Allow: /archives/
Allow: /categories/
Allow: /about/

Disallow: /vendors/
Disallow: /js/
Disallow: /css/
Disallow: /fonts/
Disallow: /vendors/
Disallow: /fancybox/

添加sitemap

Sitemap即网站地图,它的作用在于便于搜索引擎更加智能地抓取网站。最简单和常见的sitemap形式,是XML文件,在其中列出网站中的网址以及关于每个网址的其他元数据(上次更新时间、更新的频率及相对其他网址重要程度等)。

Step 1: 安装sitemap生成插件

npm install hexo-generator-sitemap --save
npm install hexo-generator-baidu-sitemap --save

Step 2: 编辑站点目录下的_config.yml,添加

# hexo sitemap网站地图
sitemap:
path: sitemap.xml
baidusitemap:
path: baidusitemap.xml

Step 3: 在robots.txt文件中添加

Sitemap: http://www.jeyzhang.com/sitemap.xml
Sitemap: http://www.jeyzhang.com/baidusitemap.xml

本文转自:http://www.jeyzhang.com/hexo-website-seo.html

MySQL在线更改schema的工具很多,如Percona的pt-online-schema-change、 Facebook的 OSCLHM 等,但这些都是基于触发器(Trigger)的,今天咱们介绍的 gh-ost 号称是不需要触发器(Triggerless)支持的在线更改表结构的工具。

gh-ost

原文地址:gh-ost: GitHub’s online schema migration tool for MySQL

本文先介绍一下当前业界已经存在的这些工具的使用场景和原理,然后再详细介绍 gh-ost 的工作原理和特性。

今天我们开源了GitHub内部使用的一款 不需要触发器支持的 MySQL 在线更改表结构的工具 gh-ost

开发 gh-ost 是为了应付GitHub在生产环境中面临的持续的、不断变化的在线修改表结构的需求。gh-ost 通过提供低影响、可控、可审计和操作友好的解决方案改变了现有的在线迁移表工具的工作模式。

MySQL表迁移及结构更改操作是业界众所周知的问题,2009年以来已经可以通过在线(不停服务)变更的工具来解决。迅速增长,快速迭代的产品往往需要频繁的需改数据库的结构。增加/更改/删除/ 字段和索引等等,这些操作在MySQL中默认都会锁表,影响线上的服务。 向这种数据库结构层面的变更我们每天都会面临多次,当然这种操作不应该影响用户的正常服务。

在开始介绍 gh-ost 工具之前,咱们先来看一下当前现有的这些工具的解决方案。

在线修改表结构,已存在的场景

如今,在线修改表结构可以通过下面的三种方式来完成:

  • 在从库上修改表结构,操作会在其他的从库上生效,将结构变更了的从库设置为主库
  • 使用 MySQL InnoDB 存储引擎提供的在线DDL特性
  • 使用在线修改表结构的工具。现在最流行的是 pt-online-schema-change 和 Facebook 的 OSC;当然还有 LHM 和比较原始的 oak-online-alter-table 工具。

其他的还包括 Galera 集群的Schema滚动更新,以及一些其他的非InnoDB的存储引擎等待,在 GitHub 我们使用通用的 主-从 架构 和 InnoDB 存储引擎。

为什么我们决定开始一个新的解决方案,而不是使用上面的提到的这些呢?现有的每种解决方案都有其局限性,下文会对这些方式的普遍问题简单的说明一下,但会对基于触发器的在线变更工具的问题进行详细说明。

  • 基于主从复制的迁移方式需要很多的前置工作,如:大量的主机,较长的传输时间,复杂的管理等等。变更操作需要在一个指定的从库上或者基于sub-tree的主从结构中执行。需要的情况也比较多,如:主机宕机、主机从早先的备份中恢复数据、新主机加入到集群等等,所有这些情况都有可能对我们的操作造成影响。最要命的是可能这些操作一天要进行很多次,如果使用这种方法我们操作人员每天的效率是非常高的(译者注:现如今很少有人用这种方式了吧)

  • MySQL针对Innodb存储引擎的在线DDL操作在开始之前都需要一个短时间排它锁(exclusive)来准备环境,所以alter命令发出后,会首先等待该表上的其它操作完成,在alter命令之后的请求会出现等待waiting meta data lock。同样在ddl结束之前,也要等待alter期间所有的事务完成,也会堵塞一小段时间,这对于繁忙的数据库服务来说危险系数是非常高的。另外DDL操作不能中断,如果中途kill掉,会造成长时间的事务回滚,还有可能造成元数据的损坏。它操作起来并不那么的Nice,不能限流和暂停,在大负载的环境中甚至会影响正常的业务。

  • 我们用了很多年的 pt-online-schema-change 工具。然而随着我们不断增长的业务和流量,我们遇到了很多的问题,我们必须考虑在操作中的哪些 危险操作 (译者注:pt工具集的文档中经常会有一些危险提示)。某些操作必须避开高峰时段来进行,否则MySQL可能就挂了。所有现存的在线表结构修改的工具都是利用了MySQL的触发器来执行的,这种方式有一些潜藏的问题。

基于触发器的在线修改有哪些问题呢?

所有在线表结构修改工具的操作方式都类似:创建与原表结构一致的临时表,该临时表已经是按要求修改后的表结构了,缓慢增量的从原表中复制数据,同时记录原表的更改(所有的 INSERT, DELETE, UPDATE 操作) 并应用到临时表。当工具确认表数据已经同步完成,它会进行替换工作,将临时表更名为原表。

pt-online-schema-change, LHMoak-online-alter-table 这些工具都使用同步的方式,当原表有变更操作时利用一些事务的间隙时间将这些变化同步到临时表。Facebook 的工具使用异步的方式将变更写入到changelog表中,然后重复的将changelog表的变更应用到临时表。所有的这些工具都使用触发器来识别原表的变更操作。

当表中的每一行数据有 INSERT, DELETE, UPDATE 操作时都会调用存储的触发器。一个触发器可能在一个事务空间中包含一系列查询操作。这样就会造成一个原子操作不单会在原表执行,还会调用相应的触发器执行多个操作。

在基于触发器迁移实践中,遇到了如下的问题:

  • 触发器是以解释型代码的方式保存的。MySQL 不会预编译这些代码。 会在每次的事务空间中被调用,它们被添加到被操作的表的每个查询行为之前的分析和解释器中。

  • 锁表: 触发器在原始表查询中共享相同的事务空间,而这些查询在这张表中会有竞争锁,触发器在另外一张表会独占竞争锁。在这种极端情况下,同步方式的锁争夺直接关系到主库的并发写性能。以我们的经验来说,在生产环境中当竞争锁接近或者结束时,数据库可能会由于竞争锁而被阻塞住。触发锁的另一个方面是创建或销毁时所需要的元数据锁。我们曾经遇到过在繁忙的表中当表结构修改完成后,删除触发器可能需要数秒到分钟的时间。

  • 不可信:当主库的负载上升时,我们希望降速或者暂停操作,但基于触发器的操作并不能这么做。虽然它可以暂停行复制操作,但却不能暂停出触发器,如果删除触发器可能会造成数据丢失,因此触发器需要在整个操作过程中都要存在。在我们比较繁忙的服务器中就遇到过由于触发器占用CPU资源而将主库拖死的例子。

  • 并发迁移: 我们或者其他的人可能比较关注多个同时修改表结构(不同的表)的场景。鉴于上述触发器的开销,我们没有兴趣同时对多个表进行在线修改操作,我们也不确定是否有人在生产环境中这样做过。

  • 测试:我们修改表结构可能只是为了测试,或者评估其负载开销。基于触发器的表结构修改操作只能通过基于语句复制的方式来进行模拟实验,离真实的主库操作还有一定的距离,不能真实的反映实际情况。

gh-ost

gh-ost GitHub 的在线 Schema 修改工具,下面工作原理图:

gh-ost2

gh-ost 具有如下特性:

  • 无触发器
  • 轻量级
  • 可暂停
  • 可动态控制
  • 可审计
  • 可测试
  • 值得信赖 :blush:

无触发器

gh-ost 没有使用触发器。它通过分析binlog日志的形式来监听表中的数据变更。因此它的工作模式是异步的,只有当原始表的更改被提交后才会将变更同步到临时表(ghost table)

gh-ost 要求binlog是RBR格式 ( 基于行的复制);然而也不是说你就不能在基于SBR(基于语句的复制)日志格式的主库上执行在线变更操作。实际上是可以的。gh-ost 可以将从库的 SBR日志转换为RBR日志,只需要重新配置就可以了。

轻量级

由于没有使用触发器,因此在操作的过程中对主库的影响是最小的。当然在操作的过程中也不用担心并发和锁的问题。 变更操作都是以流的形式顺序的写到binlog文件中,gh-ost只是读取他们并应用到gh-ost表中。实际上,gh-ost 通过读取binlog的写事件来进行顺序的行复制操作。因此,主库只会有一个单独连接顺序的将数据写入到临时表(ghost table)。这和ETL操作有很大的不同。

可暂停

所有的写操作都是由gh-ost控制的,并且以异步的方式读取binlog,当限速的时候,gh-ost可以暂停向主库写入数据,限速意味着不会在主库进行复制,也不会有行更新。当限速时gh-ost会创建一个内部的跟踪(tracking)表,以最小的系统开销向这个表中写入心跳事件

gh-ost 支持多种方式的限速:

  • 负载: 为熟悉 pt-online-schema-change 工具的用户提供了类似的功能,可以设置MySQL中的状态阈值,如 Threads_running=30
  • 复制延迟: gh-ost 内置了心跳机制,可以指定不同的从库,从而对主从的复制延迟时间进行监控,如果达到了设定的延迟阈值程序会自动进入限速模式。
  • 查询: 用户可以可以设置一个限流SQL,比如 SELECT HOUR(NOW()) BETWEEN 8 and 17 这样就可以动态的设置限流时间。
  • 标示文件: 可以通过创建一个标示文件来让程序限速,当删除文件后可以恢复正常操作。
  • 用户命令: 可以动态的连接到 gh-ost (下文会提到) 通过网络连接的方式实现限速。

可动态控制

现在的工具,当执行操作的过程中发现负载上升了,DBA不得不终止操作,重新配置参数,如 chunk-size,然后重新执行操作命令,我们发现这种方式效率非常低。

gh-ost 可以通过 unix socket 文件或者TCP端口(可配置)的方式来监听请求,操作者可以在命令运行后更改相应的参数,参考下面的例子:

  • echo throttle | socat - /tmp/gh-ost.sock 打开限速,同样的,可以使用 no-throttle 来关闭限流。
  • 改变执行参数: chunk-size=1500, max-lag-millis=2000, max-load=Thread_running=30 这些参数都可以在运行时变更。

可审计

同样的,使用上文提到的程序接口可以获取 gh-ost 的状态。gh-ost 可以报告当前的进度,主要参数的配置以及当前服务器的标示等等。这些信息都可以通过网络接口取到,相对于传统的tail日志的方式要灵活很多。

可测试

因为日志文件和主库负载关系不大,因此在从库上执行修改表结构的操作可以更真实的体现出这些操作锁产生的实际影响。(虽然不是十分理想,后续我们会做优化工作)。

gh-ost 內建支持测试功能,通过使用 --test-on-replica 的参数来指定: 它可以在从库上进行变更操作,在操作结束时gh-ost 将会停止复制,交换表,反向交换表,保留2个表并保持同步,停止复制。可以在空闲时候测试和比较两个表的数据情况。

这是我们在GitHub的生产环境中的测试:我们生产环境中有多个从库;部分从库并不是为用户提供服务的,而是用来对所有表运行的连续覆盖迁移测试。我们生产环境中的表,小的可能没有数据,大的会达到数百GB,我们只是做个标记,并不会正在的修改表结构(engine=innodb)。当每一个迁移结束后会停止复制,我们会对原表和临时表的数据进行完整的checksum确保他们的数据一致性。然后我们会恢复复制,再去操作下一张表。我们的生产环境的从库中已经通过 gh-ost 成功的操作了很多表。

值得信赖

上文提到说了这么多,都是为了提高大家对 gh-ost 的信任程度。毕竟在业界它还是一个新手,类似的工具已经存在了很多年了。

  • 在第一次试手之前我们建议用户先在从库上测试,校验数据的一致性。我们已经在从库上成功的进行了数以千计的迁移操作。

  • 如果在主库上使用 gh-ost 用户可以实时观察主库的负载情况,如果发现负载变化很大,可以通过上文提到的多种形式进行限速,直到负载恢复正常,然后再通过命令微调参数,这样可以动态的控制操作风险。

  • 如果迁移操作开始后预完成计时间(ETA)显示要到夜里2点才能完成,结束时候需要切换表,你是不是要留下来盯着?你可以通过标记文件让gh-ost推迟切换操作。gh-ost 会完成行复制,但并不会切换表,它会持续的将原表的数据更新操作同步到临时表中。你第二天来到办公室,删除标记文件或者通过接口 echo unpostpone 告诉gh-ost开始切换表。我们不想让我们的软件把使用者绑住,它应该是为我们拜托束缚。

  • 说到 ETA, --exact-rowcount 参数你可能会喜欢。相对于一条漫长的 SELECT COUNT(*) 语句,gh-ost 会预估出迁移操作所需要花费的时间,还会根据当前迁移的工作状况更新预估时间。虽然ETA的时间随时更改,但进度百分比的显示是准确的。

gh-ost 操作模式

gh-ost 可以同时连接多个服务器,为了获取二进制的数据流,它会作为一个从库,将数据从一个库复制到另外一个。它有各种不同的操作模式,这取决于你的设置,配置,和要运行迁移环境。

gh-ost3

a. 连接到从库,在主库做迁移

这是 gh-ost 默认的工作方式。gh-ost 将会检查从库状态,找到集群结构中的主库并连接,接下来进行迁移操作:

  • 行数据在主库上读写
  • 读取从库的二进制日志,将变更应用到主库
  • 在从库收集表格式,字段&索引,行数等信息
  • 在从库上读取内部的变更事件(如心跳事件)
  • 在主库切换表

如果你的主库的日志格式是 SBR,工具也可以正常工作。但从库必须启用二级制日志(log_bin, log_slave_updates) 并且设置 binlog_format=ROW ( gh-ost 是读取从库的二级制文件)。

如果直接在主库上操作,当然也需要二进制日志格式是RBR。

b. 连接到主库

如果你没有从库,或者不想使用从库,你可以直接在主库上操作。gh-ost 将会直接在主库上进行所有操作。你需要持续关注复制延迟问题。

  • 你的主库的二进制日志必须是 RBR 格式。
  • 在这个模式中你必须指定 --allow-on-master 参数

c. 在从库迁移/测试

该模式会在从库执行迁移操作。gh-ost 会简单的连接到主库,此后所有的操作都在从库执行,不会对主库进行任何的改动。整个操作过程中,gh-ost 将控制速度保证从库可以及时的进行数据同步

  • --migrate-on-replica 表示 gh-ost 会直接在从库上进行迁移操作。即使在复制运行阶段也可以进行表的切换操作。
  • --test-on-replica 表示 迁移操作只是为了测试在切换之前复制会停止,然后会进行切换操作,然后在切换回来,你的原始表最终还是原始表。两个表都会保存下来,复制操作是停止的。你可以对这两个表进行一致性检查等测试操作。

gh-ost at GitHub

我们已经在所有线上所有的数据库在线操作中使用了gh-ost ,我们每天都需要使用它,根据数据库修改需求,可能每天要运行多次。凭借其审计和控制功能我们已经将它集成到了ChatOps流程中。我们的工程师可以清醒的了解到迁移操作的进度,而且可以灵活的控制其行为。

开源

gh-ost 在MIT的许可下发布到了开源社区

虽然gh-ost在使用中很稳定,我们还在不断的完善和改进。我们将其开源也欢迎社会各界的朋友能够参与和贡献。随后我们会发布 贡献和建议的页面。

我们会积极的维护 gh-ost 项目,同时希望广大的用户可以尝试和测试这个工具,我们做了很大努力使之更值得信赖。

译者注

gh-ost 是MySQL业界在线修改表结构工具中的一名新秀,通常我们都是通过Percona的pt-online-schema-change工具来做这项工作,gh-ost的出现给我们带来了一种全新的方式。本文是翻译了一篇gh-ost的介绍文章,还没有尝试过这个工具。欢迎喜欢尝鲜网友谈谈使用感受。

配置

客户端配置

客户端配置比较简单,只需要配置一下server的地址就可以了,复杂的配置等到以后遇到的适合在进行介绍

# 配置salt服务端地址
vi /etc/salt/minion

# 这里也可以写主机名
master: 192.168.1.100

# 修改完成后重启服务
service salt-minion restart

服务端操作

# 查看有哪些client发起了验证请求 
salt-key list

Accepted Keys:
Denied Keys:
Unaccepted Keys: # 这里列出了为被接受的client 主机名
host01
host02
Rejected Keys:

# 接受所有验证请求
salt-key -A
The following keys are going to be accepted:
Unaccepted Keys:
host01
host02
Proceed? [n/Y] Y
Key for minion host01 accepted.
Key for minion host02 accepted.

# 再查看一下
salt-key list
Accepted Keys: # 已经接受
host01
host02
Denied Keys:
Unaccepted Keys:
Rejected Keys:

简单验证执行命令

远程执行命令

salt aws-bj* cmd.run uptime
host01:
09:42:13 up 2:02, 2 users, load average: 0.05, 0.03, 0.05
host02:
09:42:12 up 1:28, 1 user, load average: 0.00, 0.01, 0.05

下篇预告

系列文章

添加仓库

最新的salt包会发布在 Ubuntu saltstack PPA。如果你的系统中有add-apt-repository 工具,可以通过一条命令添加仓库并导入PPA key:

通过add-apt-repository添加仓库及PPA key

add-apt-repository ppa:saltstack/salt

Salt, the remote execution and configuration management tool.
More info: https://launchpad.net/~saltstack/+archive/ubuntu/salt
Press [ENTER] to continue or ctrl-c to cancel adding it

gpg: keyring `/tmp/tmpys0ah_nb/secring.gpg' created
gpg: keyring `/tmp/tmpys0ah_nb/pubring.gpg' created

gpg: requesting key 0E27C0A6 from hkp server keyserver.ubuntu.com
gpg: /tmp/tmpys0ah_nb/trustdb.gpg: trustdb created
gpg: key 0E27C0A6: public key "Launchpad PPA for Salt Stack" imported
gpg: Total number processed: 1
gpg: imported: 1 (RSA: 1)
OK

如果没有找到add-apt-repository命令可以执行下面的命令进行安装

apt-get install python-software-properties

可能也需要安装下面的包

apt-get install software-properties-common

手动添加仓库及PPA key

echo deb http://ppa.launchpad.net/saltstack/salt/ubuntu `lsb_release -sc` main | sudo tee /etc/apt/sources.list.d/saltstack.list
wget -q -O- "http://keyserver.ubuntu.com:11371/pks/lookup?op=get&search=0x4759FA960E27C0A6" |apt-key add -

更新仓库元数据

apt-get update

安装包

服务端

apt-get install salt-master salt-minion salt-syndic

客户端

apt-get install salt-minion

ZEROMQ 4

ZeroMQ 4 在 Ubuntu 14.04 以上版本已经与系统集成。因此Ubuntu 12.04 LTS 之前的版本需要升级到ZEROMQ 4

iptables-save

利用iptables-save命令可以将iptable规则保存到一个持久化存储的目录中,不同的系统保存的目录也有所不同(IPv4):

Debian/Ubuntu: iptables-save > /etc/iptables/rules.v4

RHEL/CentOS: iptables-save > /etc/sysconfig/iptables

保存之后,可以通过iptables-restore命令载入(IPv4):

Debian/Ubuntu: iptables-restore < /etc/iptables/rules.v4

RHEL/CentOS: iptables-restore < /etc/sysconfig/iptables

上面是针对IPv5的规则,如果你有使用IPv6的规则,通常需要执行下面对应的IPv6保存和恢复的命令(IPv4:

Debian/Ubuntu: ip6tables-save > /etc/iptables/rules.v6
RHEL/CentOS: ip6tables-save > /etc/sysconfig/ip6tables

注意: 这种方式只是保存规则和恢复的一种方式,并不是说保存规则后下次启动就会自动加载。一定要记住这点,如果要想系统启动后自动加载请看下面的方式。

iptables-persistent (Debian/Ubuntu)

从 Ubuntu 10.04 LTS (Lucid) 和 Debian 6.0 (Squeeze) 版本开始,可以通过安装一个名为 “iptables-persistent” 的包,安装后它以守护进程的方式来运行,系统重启后可以自动将保存的内容加载到iptables中。当然前提也是需要先保存规则。

安装

apt-get install iptables-persistent

保存规则

service  iptables-persistent save
* Saving rules... * IPv4... * IPv6...

ls -1 /etc/iptables/
rules.v4
rules.v6

cat /etc/iptables/rules.v4
# Generated by iptables-save v1.4.21 on Thu Aug 20 08:59:52 2015
*filter
:INPUT ACCEPT [5726:774869]
:FORWARD ACCEPT [170:27598]
:OUTPUT ACCEPT [5467:789045]
COMMIT
# Completed on Thu Aug 20 08:59:52 2015
# Generated by iptables-save v1.4.21 on Thu Aug 20 08:59:52 2015
*nat
:PREROUTING ACCEPT [23:1596]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [9:540]
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 10.0.0.0/16 -o eth0 -j MASQUERADE
COMMIT
# Completed on Thu Aug 20 08:59:52 2015

RHEL 和 CentOS 保存规则

RHEL/CentOS 提供了简单的方式来持久化存储iptables规则,可以直接通过iptables服务的命令来完成:

chkconfig --list | grep iptables
iptables 0:off 1:off 2:on 3:on 4:on 5:on 6:off

# 如果不是开机启动,需要执行下面命令
chkconfig iptables on

# 保存规则
service iptables save

IPv4规则信息会保存到 /etc/sysconfig/iptables 文件中,IPv6 规则保存到 /etc/sysconfig/ip6tables 文件中。 必须执行service iptables save 命令才会保存,保存后系统重启后会自动加载。

数据库备份与恢复

当前数据库结构

db01 (主)

db02 (从)

备份策略

  • 每天一次全量备份
  • 每小时一次增量备份
  • 通过热备工具(不锁表,不影响线上应用)备份主库数据
  • 备份数据保存到db01,备份完成scp到db02
  • 保留一个月的增量及全量备份
时间 备份类型
00:01 全量备份
01:01 增量备份(当天首次)
02:01~23:01 增量备份

随着数据量的增加,全量备份可以做成每周一次,每2~8小时一次增量备份

备份目录

db{01,02}:/data/backup/{full,incremental}/

backup/
├── full
│   ├── 2015-08-16 # 每日全量备份
│   └── 2015-08-16.log # 全量备份日志
└── incremental
├── 2015-08-16_14 # 每小时增量备份
├── 2015-08-16_14.log # 每小时增量备份日志
├── 2015-08-16_15
├── 2015-08-16_15.log
├── 2015-08-16_16
├── 2015-08-16_16.log
├── 2015-08-16_17
├── 2015-08-16_17.log
├── 2015-08-16_18
├── 2015-08-16_18.log
├── 2015-08-16_19
└── 2015-08-16_19.log

备份计划任务

# Info   : 数据库备份
# Author : zhouyq
# CTime : 2015-08-16
# 全量备份
1 0 * * * /bin/bash /root/bin/bakdb.sh full
# 第一次增量备份
1 1 * * * /bin/bash /root/bin/bakdb.sh incremental first
# 其他时间段增量备份
1 2-23 * * * /bin/bash /root/bin/bakdb.sh incremental

备份脚本

/root/bin/bakdb.sh

#!/bin/bash

fullPath="/data/backup/full"
incrPath="/data/backup/incremental"
bakdate=`date +'%F'`
bakhour=`date +'%H'`

oneHourAgo=`date -d '1 hours ago' +'%F_%H'`

BakBin="/usr/bin/innobackupex --no-timestamp --user=root --socket /data/db/tmp/mysql.sock --defaults-file=/usr/local/mysql/my.cnf --sleep 100"

# backup function
function hotbackup(){

baktype=$1
logfile=$2
incrpath=$3
bakpath=$4

if [ "$baktype" == "full" ];then
$BakBin $bakpath > $logfile 2>&1
elif [ "$baktype" == "incremental" ];then
$BakBin --incremental $incrpath --incremental-basedir $bakpath > $logfile 2>&1
fi
}

# ============= Main =============

if [ "$1" == "full" ];then
# 全量备份
hotbackup "full" "${fullPath}/${bakdate}.log" "none" "$fullPath/$bakdate"
/usr/bin/scp -P 9922 -rp ${fullPath}/${bakdate}* db02:${fullPath}

elif [ "$1" == "incremental" ];then
# 判断是否为第一次增量备份,只有第一次增量备份目录指向全量备份
# 第二次开始增量备份的上一次目录指向第一次增量目录即可
if [ "$2" == "first" ];then
hotbackup "incremental" "${incrPath}/${bakdate}_${bakhour}.log" "$incrPath/${bakdate}_${bakhour}" "$fullPath/$bakdate"
/usr/bin/scp -P 9922 -rp ${incrPath}/${bakdate}_${bakhour}* db02:${incrPath}
else
hotbackup "incremental" "${incrPath}/${bakdate}_${bakhour}.log" "$incrPath/${bakdate}_${bakhour}" "$incrPath/${oneHourAgo}"
/usr/bin/scp -P 9922 -rp ${incrPath}/${bakdate}_${bakhour}* db02:${incrPath}
fi
fi

恢复

全量备份恢复

innobackupex --user=root --defaults-file=/usr/local/mysql/my.cnf --apply-log /data/backup/full/2015-08-16

innobackupex --user=root --defaults-file=/usr/local/mysql/my.cnf --move-back /data/backup/full/2015-08-16

增量备份恢复

innobackupex --defaults-file=/usr/local/mysql/my.cnf --user=root --apply-log --redo-only /data/backup/full/2015-08-16 

innobackupex --defaults-file=/usr/local/mysql/my.cnf --user=root --apply-log --redo-only /data/backup/full/2015-08-16 --incremental-dir=/data/backup/incremental/2015-08-16_14

innobackupex --defaults-file=/usr/local/mysql/my.cnf --user=root --apply-log --redo-only /data/backup/full/2015-08-16 --incremental-dir=/data/backup/incremental/2015-08-16_15

原文在此,实用总结。


十条有用的 Go 技术

这里是我过去几年中编写的大量 Go 代码的经验总结而来的自己的最佳实践。我相信它们具有弹性的。这里的弹性是指:
某个应用需要适配一个灵活的环境。你不希望每过 3 到 4 个月就不得不将它们全部重构一遍。添加新的特性应当很容易。许多人参与开发该应用,它应当可以被理解,且维护简单。许多人使用该应用,bug 应该容易被发现并且可以快速的修复。我用了很长的时间学到了这些事情。其中的一些很微小,但对于许多事情都会有影响。所有这些都仅仅是建议,具体情况具体对待,并且如果有帮助的话务必告诉我。随时留言:)

1. 使用单一的 GOPATH

多个 GOPATH 的情况并不具有弹性。GOPATH 本身就是高度自我完备的(通过导入路径)。有多个 GOPATH 会导致某些副作用,例如可能使用了给定的库的不同的版本。你可能在某个地方升级了它,但是其他地方却没有升级。而且,我还没遇到过任何一个需要使用多个 GOPATH 的情况。所以只使用单一的 GOPATH,这会提升你 Go 的开发进度。

许多人不同意这一观点,接下来我会做一些澄清。像 etcdcamlistore 这样的大项目使用了像 godep 这样的工具,将所有依赖保存到某个目录中。也就是说,这些项目自身有一个单一的 GOPATH。它们只能在这个目录里找到对应的版本。除非你的项目很大并且极为重要,否则不要为每个项目使用不同的 GOPATH。如果你认为项目需要一个自己的 GOPATH 目录,那么就创建它,否则不要尝试使用多个 GOPATH。它只会拖慢你的进度。

2. 将 for-select 封装到函数中

如果在某个条件下,你需要从 for-select 中退出,就需要使用标签。例如:

func main() {

L:
for {
select {
case <-time.After(time.Second):
fmt.Println("hello")
default:
break L
}
}

fmt.Println("ending")
}

如你所见,需要联合break使用标签。这有其用途,不过我不喜欢。这个例子中的 for 循环看起来很小,但是通常它们会更大,而判断break的条件也更为冗长。

如果需要退出循环,我会将 for-select 封装到函数中:

func main() {
foo()
fmt.Println("ending")
}

func foo() {
for {
select {
case <-time.After(time.Second):
fmt.Println("hello")
default:
return
}
}
}

你还可以返回一个错误(或任何其他值),也是同样漂亮的,只需要:

// 阻塞
if err := foo(); err != nil {
// 处理 err
}

3. 在初始化结构体时使用带有标签的语法

这是一个无标签语法的例子:

type T struct {
Foo string
Bar int
}

func main() {
t := T{"example", 123} // 无标签语法
fmt.Printf("t %+v\n", t)
}

那么如果你添加一个新的字段到T结构体,代码会编译失败:

type T struct {
Foo string
Bar int
Qux string
}

func main() {
t := T{"example", 123} // 无法编译
fmt.Printf("t %+v\n", t)
}

如果使用了标签语法,Go 的兼容性规则(http://golang.org/doc/go1compat)会处理代码。例如在向net包的类型添加叫做Zone的字段,参见:http://golang.org/doc/go1.1#library。回到我们的例子,使用标签语法:

type T struct {
Foo string
Bar int
Qux string
}

func main() {
t := T{Foo: "example", Qux: 123}
fmt.Printf("t %+v\n", t)
}

这个编译起来没问题,而且弹性也好。不论你如何添加其他字段到T结构体。你的代码总是能编译,并且在以后的 Go 的版本也可以保证这一点。只要在代码集中执行go vet,就可以发现所有的无标签的语法。

4. 将结构体的初始化拆分到多行

如果有两个以上的字段,那么就用多行。它会让你的代码更加容易阅读,也就是说不要:

T{Foo: "example", Bar:someLongVariable, Qux:anotherLongVariable, B: forgetToAddThisToo}

而是:

T{
Foo: "example",
Bar: someLongVariable,
Qux: anotherLongVariable,
B: forgetToAddThisToo,
}

这有许多好处,首先它容易阅读,其次它使得允许或屏蔽字段初始化变得容易(只要注释或删除它们),最后添加其他字段也更容易(只要添加一行)。

5. 为整数常量添加 String() 方法

如果你利用 iota 来使用自定义的整数枚举类型,务必要为其添加 String() 方法。例如,像这样:

type State int

const (
Running State = iota
Stopped
Rebooting
Terminated
)

如果你创建了这个类型的一个变量,然后输出,会得到一个整数(http://play.golang.org/p/V5VVFB05HB):

func main() {
state := Running

// print: "state 0"
fmt.Println("state ", state)
}

除非你回顾常量定义,否则这里的0看起来毫无意义。只需要为State类型添加String()方法就可以修复这个问题(http://play.golang.org/p/ewMKl6K302):

func (s State) String() string {
switch s {
case Running:
return "Running"
case Stopped:
return "Stopped"
case Rebooting:
return "Rebooting"
case Terminated:
return "Terminated"
default:
return "Unknown"
}
}

新的输出是:state: Running。显然现在看起来可读性好了很多。在你调试程序的时候,这会带来更多的便利。同时还可以在实现 MarshalJSON()、UnmarshalJSON() 这类方法的时候使用同样的手段。

6. 让 iota 从 a +1 开始增量

在前面的例子中同时也产生了一个我已经遇到过许多次的 bug。假设你有一个新的结构体,有一个State字段:

type T struct {
Name string
Port int
State State
}

现在如果基于 T 创建一个新的变量,然后输出,你会得到奇怪的结果(http://play.golang.org/p/LPG2RF3y39):

func main() {
t := T{Name: "example", Port: 6666}

// prints: "t {Name:example Port:6666 State:Running}"
fmt.Printf("t %+v\n", t)
}

看到 bug 了吗?State字段没有初始化,Go 默认使用对应类型的零值进行填充。由于State是一个整数,零值也就是0,但在我们的例子中它表示Running。

那么如何知道 State 被初始化了?还是它真得是在Running模式?没有办法区分它们,那么这就会产生未知的、不可预测的 bug。不过,修复这个很容易,只要让 iota 从 +1 开始(http://play.golang.org/p/VyAq-3OItv):

const (
Running State = iota + 1
Stopped
Rebooting
Terminated
)

现在t变量将默认输出Unknown,不是吗? :)

func main() {
t := T{Name: "example", Port: 6666}

// 输出: "t {Name:example Port:6666 State:Unknown}"
fmt.Printf("t %+v\n", t)
}

不过让 iota 从零值开始也是一种解决办法。例如,你可以引入一个新的状态叫做Unknown,将其修改为:

const (
Unknown State = iota
Running
Stopped
Rebooting
Terminated
)

7. 返回函数调用

我已经看过很多代码例如(http://play.golang.org/p/8Rz1EJwFTZ):

func bar() (string, error) {
v, err := foo()
if err != nil {
return "", err
}

return v, nil
}

然而,你只需要:

func bar() (string, error) {
return foo()
}

更简单也更容易阅读(当然,除非你要对某些内部的值做一些记录)。

8. 把 slice、map 等定义为自定义类型

将 slice 或 map 定义成自定义类型可以让代码维护起来更加容易。假设有一个Server类型和一个返回服务器列表的函数:

type Server struct {
Name string
}

func ListServers() []Server {
return []Server{
{Name: "Server1"},
{Name: "Server2"},
{Name: "Foo1"},
{Name: "Foo2"},
}
}

现在假设需要获取某些特定名字的服务器。需要对 ListServers() 做一些改动,增加筛选条件:

// ListServers 返回服务器列表。只会返回包含 name 的服务器。空的 name 将会返回所有服务器。
func ListServers(name string) []Server {
servers := []Server{
{Name: "Server1"},
{Name: "Server2"},
{Name: "Foo1"},
{Name: "Foo2"},
}

// 返回所有服务器
if name == "" {
return servers
}

// 返回过滤后的结果
filtered := make([]Server, 0)

for _, server := range servers {
if strings.Contains(server.Name, name) {
filtered = append(filtered, server)
}
}

return filtered
}

现在可以用这个来筛选有字符串Foo的服务器:

func main() {
servers := ListServers("Foo")

// 输出:“servers [{Name:Foo1} {Name:Foo2}]”
fmt.Printf("servers %+v\n", servers)
}

显然这个函数能够正常工作。不过它的弹性并不好。如果你想对服务器集合引入其他逻辑的话会如何呢?例如检查所有服务器的状态,为每个服务器创建一个数据库记录,用其他字段进行筛选等等……

现在引入一个叫做Servers的新类型,并且修改原始版本的 ListServers() 返回这个新类型:

type Servers []Server

// ListServers 返回服务器列表
func ListServers() Servers {
return []Server{
{Name: "Server1"},
{Name: "Server2"},
{Name: "Foo1"},
{Name: "Foo2"},
}
}

现在需要做的是只要为Servers类型添加一个新的Filter()方法:

// Filter 返回包含 name 的服务器。空的 name 将会返回所有服务器。
func (s Servers) Filter(name string) Servers {
filtered := make(Servers, 0)

for _, server := range s {
if strings.Contains(server.Name, name) {
filtered = append(filtered, server)
}

}

return filtered
}

现在可以针对字符串Foo筛选服务器:

func main() {
servers := ListServers()
servers = servers.Filter("Foo")
fmt.Printf("servers %+v\n", servers)
}

哈!看到你的代码是多么的简单了吗?还想对服务器的状态进行检查?或者为每个服务器添加一条数据库记录?没问题,添加以下新方法即可:

func (s Servers) Check()
func (s Servers) AddRecord()
func (s Servers) Len()
...

9. withContext 封装函数

有时对于函数会有一些重复劳动,例如锁/解锁,初始化一个新的局部上下文,准备初始化变量等等……这里有一个例子:

func foo() {
mu.Lock()
defer mu.Unlock()

// foo 相关的工作
}

func bar() {
mu.Lock()
defer mu.Unlock()

// bar 相关的工作
}

func qux() {
mu.Lock()
defer mu.Unlock()

// qux 相关的工作
}

如果你想要修改某个内容,你需要对所有的都进行修改。如果它是一个常见的任务,那么最好创建一个叫做withContext的函数。这个函数的输入参数是另一个函数,并用调用者提供的上下文来调用它:

func withLockContext(fn func()) {
mu.Lock
defer mu.Unlock()

fn()
}

只需要将之前的函数用这个进行封装:

func foo() {
withLockContext(func() {
// foo 相关工作
})
}

func bar() {
withLockContext(func() {
// bar 相关工作
})
}

func qux() {
withLockContext(func() {
// qux 相关工作
})
}

不要光想着加锁的情形。对此来说最好的用例是数据库链接。现在对 withContext 函数作一些小小的改动:

func withDBContext(fn func(db DB) error) error {
// 从连接池获取一个数据库连接
dbConn := NewDB()

return fn(dbConn)
}

如你所见,它获取一个连接,然后传递给提供的参数,并且在调用函数的时候返回错误。你需要做的只是:


func foo() {
withDBContext(func(db *DB) error {
// foo 相关工作
})
}

func bar() {
withDBContext(func(db *DB) error {
// bar 相关工作
})
}

func qux() {
withDBContext(func(db *DB) error {
// qux 相关工作
})
}

你在考虑一个不同的场景,例如作一些预初始化?没问题,只需要将它们加到withDBContext就可以了。这对于测试也同样有效。

这个方法有个缺陷,它增加了缩进并且更难阅读。再次提示,永远寻找最简单的解决方案。

10. 为访问 map 增加 setter,getters

如果你重度使用 map 读写数据,那么就为其添加 getter 和 setter 吧。通过 getter 和 setter 你可以将逻辑封分别装到函数里。这里最常见的错误就是并发访问。如果你在某个 goroutein 里有这样的代码:

m["foo"] = bar

还有这个:

delete(m, "foo")

会发生什么?你们中的大多数应当已经非常熟悉这样的竞态了。简单来说这个竞态是由于 map 默认并非线程安全。不过你可以用互斥量来保护它们:

mu.Lock()
m["foo"] = "bar"
mu.Unlock()

以及:

mu.Lock()
delete(m, "foo")
mu.Unlock()

假设你在其他地方也使用这个 map。你必须把互斥量放得到处都是!然而通过 getter 和 setter 函数就可以很容易的避免这个问题:

func Put(key, value string) {
mu.Lock()
m[key] = value
mu.Unlock()
}
func Delete(key string) {
mu.Lock()
delete(m, key)
mu.Unlock()
}

使用接口可以对这一过程做进一步的改进。你可以将实现完全隐藏起来。只使用一个简单的、设计良好的接口,然后让包的用户使用它们:

type Storage interface {
Delete(key string)
Get(key string) string
Put(key, value string)
}

这只是个例子,不过你应该能体会到。对于底层的实现使用什么都没关系。不光是使用接口本身很简单,而且还解决了暴露内部数据结构带来的大量的问题。

但是得承认,有时只是为了同时对若干个变量加锁就使用接口会有些过分。理解你的程序,并且在你需要的时候使用这些改进。

总结

抽象永远都不是容易的事情。有时,最简单的就是你已经实现的方法。要知道,不要让你的代码看起来很聪明。Go 天生就是个简单的语言,在大多数情况下只会有一种方法来作某事。简单是力量的源泉,也是为什么在人的层面它表现的如此有弹性。

如果必要的话,使用这些基数。例如将[]Server转化为Servers是另一种抽象,仅在你有一个合理的理由的情况下这么做。不过有一些技术,如 iota 从 1 开始计数总是有用的。再次提醒,永远保持简单。

特别感谢 Cihangir Savas、Andrew Gerrand、Ben Johnson 和 Damian Gryski 提供的极具价值的反馈和建议。

问题由来

阿里云的VPC与其他基于OpenStack的IaaS不同,他的路由只是作为多网段的路由交换,不提供内到外的路由,因此在VPC内的主机除非绑定EIP,否则是无法连接公网的。通过工单询问客服,得到的结论是通过在路由器上添加一个路由,通过一个绑定EIP的主机做NAT上网,通过设置iptables的方式来实现。

VPC结构图

虚拟路由器配置

添加路由

为了让内网服务器借助EIP访问公网,所以设置所有目标地址0.0.0.0/0下一跳都转发到绑定了公网IP的ECS实例上。这里的下一跳ECS不支持搜索,需要提前记号名称:

绑定EIP的主机配置

iptables添加SNAT规则

iptables -t nat -I POSTROUTING -s 192.168.2.0/24 -j SNAT --to-source 192.168.2.20

注意: ubuntu 14.04 系统保存iptables设置需要安装iptables-persistent包,然后通过 service iptables-persistent save 的方式保存配置,安装完iptables-persistent后该服务随系统一起启动并会把保存的配置应用

开启IP转发

echo "net.ipv4.ip_forward=1" >>  /etc/sysctl.conf && sysctl -p

数组定义

# 定义一个空数组
Result=()

# 定义并给数组赋值
arr=(a b c d e)

说明:

  • 默认数组中的元素是以空格分隔的,如果元素是包含空格的字符串,最好用双引号括起来

  • shell中的默认分隔符可以通过修改 $IFS变量来设置

数组读取/删除

# 初始化并赋值数组
arr=(a b c d e)

# 计算长度
echo ${#arr[@]} # 结果: 5
# 或
echo ${#arr[*]} # 结果: 5

# 取出所有数据
echo ${arr[@]} # 结果: a b c d e
# 或
echo ${arr[*]} # 结果: a b c d e

# 取出第二个元素的数据
echo ${arr[1]} # 结果: b

# 遍历数组
filelist=(`ls`)
for file in ${filelist[@]};do
echo $file
done

# 删除第二个元素
unset arr[1]
echo ${arr[*]} # 结果: a c d e

切片/元素替换

# 切片

# 初始化并赋值数组
arr=(a b c d e)

arr2=(${arr[@]:0:3})
echo ${arr2[@]} # 结果 a b c


# 替换
# 初始化并赋值数组
arr=(a b c d e)
echo ${arr[@]/a/aaa}
  • 切片(分片): 直接通过 ${数组名[@或*]:起始位置:长度} 切片原先数组,返回是字符串,中间用“空格”分开,因此如果加上”()”,将得到切片数组,上面例子:c 就是一个新数据。
  • 替换: ${数组名[@或*]/查找字符/替换字符} 该操作不会改变原先数组内容,如果需要修改,请重新定义变量并赋值。

实例

在一个多域名的web server环境中,通过分析访问日志,统计最近8小时有用户访问的域名(去重),并显示。

日志格式:X-Forworld-IP User-IP YYYY-MM-DD HH:mm:ss method “URL” HTTP响应码 服务器处理时间 返回大小 “Refer” “浏览器信息” “虚拟主机域名” 真实处理请求的主机

#!/bin/bash

AWKBin="/usr/bin/awk"
EGREPBin="/bin/egrep"
SORTBin="/usr/bin/sort"
SEDBin="/bin/sed"

# 虚拟主机
VSName=$1

# 时间段
TimePeriod=$2

# 过滤字符串
FilterKeys='''DNSPod-Monitor|JianKongBao'''
# 日志目录
LOGPath="/logs/nginx"

# 定义最终输出的数组变量
Result=()

while (( TimePeriod > 0 ))
do
LogTime=`date -d "- ${TimePeriod}hours" +%Y-%m-%d-%H`
LogFile=${VSName}_${LogTime}.log

Result=(${Result[@]} `$AWKBin '{if($5!~/HEAD/ && $5!~/\"\"/ ) print $0}' ${LOGPath}/${LogFile} | \
$EGREPBin -v $FilterKeys | $AWKBin '{print $NF}' | $AWKBin -F '@' '{print $1}' |\
$SORTBin -u`)

echo "第$TimePeriod 个日志:${Result[*]}"


((TimePeriod--))
done

echo "未去重:${Result[*]}"

Result=($(awk -vRS=' ' '!a[$1]++' <<< ${Result[@]}))

echo "去重以后:${Result[*]}"

由于之前的blog内容过于陈旧,很多文档现在来看会给广大朋友带来困扰,因此决定将之前的所有内容都抛弃,从今天开始重新写!

最近5年由于工作原因也没有顾得上更新blog,近期会有一系列文档更新,都是这五年来的一些工作经验。

由于大家共识的原因,本blog于2015-08-02正式迁移到GitHub