前线系列

运维前线：一线运维专家的运维方法、技巧与实践

云技术社区　著

ISBN：978-7-111-55697-8

本书纸版由机械工业出版社于2017年出版，电子版由华章分社（北京华章图文信息有限公司，北京奥维博世图书发行有限公司）全球范围内制作与发行。

版权所有，侵权必究

客服热线：+ 86-10-68995265

客服信箱：service@bbbvip.com

官方网址：www.hzmedia.com.cn

新浪微博 @华章数媒

微信公众号 华章电子书（微信号：hzebook）

目录

推荐序

前言

第1章　自动化运维之深度解码

1.1　概述

1.2　运维自动化的三重境界

1.3　运维自动化的困境和价值

1.3.1　运维自动化的困境

1.3.2　运维自动化的价值

1.4　运维自动化的多维解读

1.4.1　基于应用变更场景的维度划分

1.4.2　基于系统层次的维度划分

1.4.3　基于与业务程序耦合紧密程度的维度划分

1.4.4　面向服务的自动化能力划分

1.5　运维自动化的方法论

1.6　运维自动化系统的实现

1.6.1　DNS管理系统

1.6.2　CMDB管理系统

1.6.3　名字服务中心系统

1.6.4　持续部署管理系统

1.6.5　运维调度管理系统

1.7　运维自动化系统的API参考实现

1.8　运维自动化依赖的团队模型

1.8.1　团队的能力模型

1.8.2　团队的驱动模型

1.8.3　团队的技能模型

1.8.4　参考的运维团队组织结构

1.9　小结

第2章　利用Facter和Django快速构建CMDB

2.1　CMDB简介

2.2　开源CMDB介绍

2.2.1　OneCMDB介绍

2.2.2　CMDBuild介绍

2.2.3　其他的开源CMDB

2.3　Puppet及Facter介绍

2.3.1　什么是Puppet

2.3.2　为什么是Puppet

2.3.3　什么是Facter

2.3.4　Facter的特点

2.3.5　Facter变量

2.4　如何利用Python获取Facts

2.4.1　工作原理

2.4.2　利用Python脚本获取Facts

2.5　使用Django快速构建CMDB系统

2.5.1　Django介绍

2.5.2　Django安装

2.5.3　Django常用命令

2.5.4　Django的配置

2.6　高级进阶

2.6.1　历史查询功能

2.6.2　API功能

2.6.3　数据表结构

2.6.4　用户管理功能

2.6.5　用户组管理功能

2.7　小结

第3章　数据中心搬迁中的x86自动化运维

3.1　数据中心搬迁准备

3.1.1　数据中心搬迁介绍

3.1.2　搬迁环境介绍

3.1.3　搬迁前的准备工作

3.1.4　搬迁信息收集

3.2　利用VMware脚本简化虚拟化层的搬迁

3.2.1　通过脚本完成ESXI安装后的基础设置

3.2.2　批量挂载数据盘

3.2.3　批量注册虚拟机

3.2.4　vCenter目录结构的调整

3.2.5　批量更改虚拟机名称及port　group

3.2.6　批量设置虚拟机版本和CPU、内存保留值

3.3　利用批处理与Shell脚本简化逻辑节点的搬迁

3.3.1　逻辑节点切换脚本的思路

3.3.2　利用批处理脚本简化Windows逻辑节点的搬迁

3.3.3　利用Shell脚本简化Linux逻辑节点的搬迁

3.3.4　通过SFTP和WMIC指令将脚本文件上传至所有虚拟机

3.3.5　搬迁期间的注意事项

3.4　小结

第4章　集中配置管理工具Puppet

4.1　如何同步puppet-agent端上的常用服务

4.2　如何在puppet-agent端上自动安装常用的软件包

4.3　如何自动同步puppet-agent端的yum源

4.4　如何根据不同名字的节点机器推送不同的文件

4.5　如何根据节点机器名来选择性地执行Shell程序

4.6　如何快速同步puppet-server端的www目录文件

4.7　如何利用ERB模板自动配置Apache虚拟主机

4.8　如何利用ERB模板自动配置Nginx虚拟主机

4.9　小结

第5章　深度实践iptables

5.1　禁用连接追踪

5.1.1　排查连接追踪导致的故障

5.1.2　分析连接追踪的原理

5.1.3　禁用连接追踪的方法

5.1.4　确认禁用连接追踪的效果

5.2　慎重禁用ICMP协议

5.2.1　禁用ICMP协议导致的一则故障案例

5.2.2　MTU发现的原理

5.2.3　解决问题的方法

5.3　网络地址转换在实践中的案例

5.3.1　源地址NAT

5.3.2　目的地址NAT

5.4　深入理解iptables的各种表和各种链

5.5　小结

第6章　使用systemd管理Linux系统服务

6.1　systemd和sysVinit之间的关系

6.1.1　sysVinit方式下系统的启动特点

6.1.2　systemd方式下系统的启动特点

6.2　systemd的原理和启动顺序

6.2.1　sysVinit的启动顺序

6.2.2　systemd的启动顺序

6.3　systemd的进程控制命令

6.3.1　systemctl命令

6.3.2　hostnamectl命令

6.3.3　localectl命令

6.3.4　loginctl命令

6.3.5　timedatectl命令

6.4　systemd服务管理

6.4.1　编写Nginx的sysVinit启动脚本

6.4.2　编写Nginx的systemd启动脚本

6.4.3　systemd的其他功能

6.5　优化

6.5.1　使用systemd-analyze优化启动时间

6.5.2　使用systemd　journal功能

6.6　小结

第7章　PHP运维实践

7.1　PHP再认识

7.1.1　PHP进程的工作方式

7.1.2　PHP代码的编译和部署

7.1.3　PHP内部实现和生命周期

7.1.4　PHP在互联网技术栈的位置

7.2　PHP开发、架构、运维问题及解决思路

7.2.1　运维对PHP研发提要求

7.2.2　运维参与PHP项目架构设计

7.2.3　PHP运维常见问题及解决之道

7.3　PHP进程部署和配置、代码发布

7.3.1　PHP进程的部署

7.3.2　PHP配置文件变更

7.3.3　PHP配置项

7.3.4　PHP进程部署及配置文件管理实践

7.3.5　PHP代码发布

7.3.6　PHP代码发布实践：代码发布系统

7.4　PHP性能分析

7.4.1　性能问题概述

7.4.2　PHP性能问题

7.4.3　性能分析方法

7.4.4　PHP性能分析实践：性能分析系统

7.5　PHP故障处理与监控

7.5.1　PHP故障分类及处理思路

7.5.2　业务监控和故障发现

7.5.3　PHP故障消除的方法

7.5.4　故障分析案例

7.6　小结

第8章　应用系统运行分析

8.1　分析模型

8.1.1　数据采集

8.1.2　数据模型

8.2　运行分析平台建设

8.2.1　数据采集接口

8.2.2　数据分析模块

8.2.3　推广

8.3　呼叫中心系统运行分析示例

8.3.1　确定分析方案

8.3.2　问题分析案例介绍

8.4　小结

第9章　虚拟化中存储配置典型场景：启动风暴

9.1　oVirt虚拟化平台配置介绍

9.1.1　存储配置背景知识

9.1.2　模板与实例同一存储

9.1.3　模板与实例分离存储

9.1.4　无状态实例的硬盘与快照分离存储

9.2　启动风暴相关系列实验

9.2.1　模板配置

9.2.2　实验脚本

9.2.3　WD　1TB机械硬盘启动Windows　XP实验

9.2.4　Intel　480GB　SSD启动Windows　XP实验

9.2.5　实验结论

9.3　私有云中处理启动风暴的常用方法

9.3.1　启动排队

9.3.2　存储分层选择

9.3.3　其他提升桌面云存储性能的方式

9.4　小结

第10章　私有云桌面网络组建

10.1　桌面云常用网络

10.1.1　NAT网络

10.1.2　桥接网络

10.1.3　VLAN网络

10.1.4　Access模式

10.1.5　Trunk模式

10.1.6　Open　vSwitch

10.2　oVirt/OpenStack的桌面网络应用

10.2.1　oVirt/OpenStack组网方式

10.2.2　应用场景举例

10.3　小结

第11章　浅谈服务器交付的那些事儿

11.1　设备签收的学问

11.2　服务器设置

11.3　Cobbler的流程与规划

11.4　服务器安装时遇到的各种坑

11.4.1　DHCP客户端获取IP地址失败

11.4.2　TFTP加载失败

11.4.3　TFTP　Client交互后无响应

11.4.4　yum安装失败

11.4.5　Linux内核无法识别新硬件

11.4.6　恶意PXE启动导致原有系统被误装

11.5　交接后的故事

11.6　小结

第12章　企业级Nginx　Web服务优化实战

12.1　Nginx基本安全优化

12.1.1　调整参数隐藏Nginx软件版本号信息

12.1.2　更改源码隐藏Nginx软件名及版本号

12.1.3　更改Nginx服务的默认用户

12.2　根据参数优化Nginx服务性能

12.2.1　优化Nginx服务的worker进程个数

12.2.2　优化绑定不同的Nginx进程到不同的CPU上

12.2.3　Nginx事件处理模型优化

12.2.4　调整Nginx单个进程允许的客户端最大连接数

12.2.5　配置Nginx　worker进程的最大打开文件数

12.2.6　优化服务器域名的散列表大小

12.2.7　开启高效文件传输模式

12.2.8　优化Nginx连接参数，调整连接超时时间

12.2.9　上传文件大小的限制（动态应用）

12.2.10　FastCGI相关参数调优（配合PHP引擎动态服务）

12.2.11　配置Nginx　gzip压缩实现性能优化

12.2.12　配置Nginx　expires缓存实现性能优化

12.3　Nginx日志相关的优化与安全

12.3.1　编写脚本实现Nginx　access日志轮询

12.3.2　不记录不需要的访问日志

12.3.3　访问日志的权限设置

12.4　Nginx站点目录及文件URL访问控制

12.4.1　根据扩展名限制程序和文件访问

12.4.2　禁止访问指定目录下的所有文件和目录

12.4.3　限制网站来源IP访问

12.4.4　配置Nginx，禁止非法域名解析访问企业网站

12.5　Nginx图片及目录防盗链解决方案

12.6　Nginx错误页面的优雅显示

12.6.1　生产环境中常见的HTTP状态码列表

12.6.2　为什么要配置错误页面优雅显示

12.7　Nginx站点目录文件及目录权限优化

12.8　Nginx防爬虫优化

12.9　利用Nginx限制HTTP的请求方法

12.10　使用CDN做网站内容加速

12.10.1　什么是CDN

12.10.2　CDN的特点

12.10.3　企业使用CDN的基本要求

12.11　Nginx程序架构优化

12.12　使用普通用户启动Nginx（监牢模式）

12.12.1　为什么要让Nginx服务使用普通用户

12.12.2　给Nginx服务降权的解决方案

12.12.3　给Nginx服务降权实战

12.13　控制Nginx并发连接数量

12.14　控制客户端请求Nginx的速率

12.15　小结

第13章　游戏运维的思考

13.1　游戏运维最关键的几件事

13.1.1　安全

13.1.2　稳定

13.1.3　高效

13.1.4　成本节约

13.2　游戏运维人的发展

13.3　小结

第14章　数据库平台建设实战

14.1　规范建立

14.1.1　安装规范

14.1.2　配置规范

14.1.3　账号、权限规范

14.1.4　目录规范

14.1.5　其他规范

14.2　架构设计

14.2.1　架构图

14.2.2　各个模块介绍

14.3　功能介绍与实践

14.3.1　操作部分

14.3.2　日志部分

14.3.3　资产部分

14.3.4　信息展示

14.3.5　入口（LVS/Redir）

14.4　后期功能展望

14.5　小结

附录A　求职者与面试官

推荐序

许式伟，七牛云CEO。曾就职于金山、百度、盛大，拥有超过15年的技术积累，连续8年组织ECUG实效云计算开发组年会。曾获国家科学技术进步奖二等奖。

2011年，与吕桂华一起创立七牛云，现已服务50多万家企业。

非常荣幸能受邀为本书作序。

2011年我从盛大离职，创办了七牛云。

在移动互联网时代，初创企业和巨头们之间的力量相差悬殊，这滋生了创业扶持产业的出现与兴起。所谓创业扶持产业，从技术革新上看就是“云”，云服务让所有的初创企业有了和巨头们一样的基础设施。

很快，云服务的时代就在预期中到来了。很多人开始担心，自己的业务知识能否胜任“新运维”的需求。其实我想告诉大家，互联网中各类技术的革新总是很快的，无论你做哪一类工作，当跟不上技术发展的时候，就会面临被淘汰的危险。传统运维的革新是一个必然的过程，如果你对运维知识的了解一直停留在原地，那你离悬崖就不远了。

云服务下的运维相较于传统服务器的模式，优势已相当明显。企业无需花费巨额的资金来购买新的服务器和托管机房的机位，就能够低成本、低风险地实现新增业务。同时，运维人员不再只能利用传统的网管手段来定位系统故障，他们可以通过云计算管理平台以及虚拟设备管理平台进行分析。

此外，云服务的蓬勃发展也令互联网产业发生了巨变，互联网产业整体的蛋糕做大了，这使得其对运维的总体需求呈现上升的趋势。这对运维人员是一个非常好的机会，只要知识与能力满足了岗位需求，你的待遇和发展就能上一个台阶。

作为云服务行业以及创业公司的代表，七牛云深谙创业的艰辛。我们反复说，七牛云的目标是打造一个场景化的PaaS（Platform-as-a-Service，平台即服务）平台，帮助开发者缩短从想法到产品的距离。我们打造了很多子产品，包括大数据、通用计算、云计算等。从我们发布产品的服务类型来看，第一类是对象存储服务，第二类是融合CDN服务，第三类是数据处理服务，第四类是直播云以及实时流网络LiveNet服务。

《运维前线》这本书集合14位资深运维专家的实践经验，覆盖了互联网和传统行业运维的各个领域。其中所述的运维方法、技巧与实践，都和七牛云息息相关，这也正是我为本书作序的原因。

这里我举两个例子：

在本书第3章中讲到，动静分离的架构是基于Web开发的互联网服务中常见的架构的，它是指将数据库中的动态内容存储和文件存储分开，常见的做法是将动态内容存储在原有的数据库系统中，而将静态文件，如图片和音视频等，托管存储在七牛云提供的对象存储服务中，这样可以更方便地维护不同类型的数据。

在本书第12章中讲到，CDN节点由于数量众多，承受的流量巨大，再加上国内网络的复杂度极高，因此真正商用CDN的建设有一定难度，并且硬件和运维成本都不低，因此一般而言，企业不会去自建CDN，而是选择七牛云这样的企业来解决问题。

云服务的兴起改变了运维，而这种改变不会停止。

云服务厂商可以将所有事情标准化，然后以服务的形式打包提供给客户。而运维人员将告别烦琐的工作内容，但是他们可能要承担更多的职责——解决监控、评估、采购、报修等问题。

当然，你如果在云服务厂商做运维工作，便需要对传统运维有更深刻的理解了。

前言

为什么要写这本书

《运维前线：一线运维专家的运维方法、技巧与实践》（以下简称《运维前线》）是前线系列的一个子集，前线系列图书的出版理念是邀请多位业界专家，总结所在行业的最新理念或深度实践经验。前线系列图书不同于市面上的很多图书，这类书并不系统，有的只是一线专家的实战经验，人们常称之为“干货”。一篇文章、一家公司、一个案例、一个场景，独立成篇，在满足碎片化阅读的同时，也能让读者进行横向比较和深入思考。本系列图书不强调大而全，追求的是每篇文章都是精品，希望能给读者带来深度的启发和收获。

按照这个理念，之前著名产品经理兰军（笔名Blues）策划的《产品前线》，出版之后大获成功，随之而来的《运维前线》令我感觉到压力巨大。《运维前线》的出版犹如十月怀胎，中间充满波折，好几次我都以为要半途而废了，最后终于得以出版，在此要特别感谢机械工业出版社华章公司著名出版人杨福川，是他的坚持和鼓励让我总能在迷茫中看到希望。我和福川有共同的理念，希望把《运维前线》做成精品，如果有哪点不符合要求，那么我们宁愿耗时长一些，多打磨打磨，很庆幸能与福川一起合作。

本书共有14位作者，包含了在腾讯、YY语音、UC、京东、盛大游戏、金山西山居、猎豹移动、广发银行、优维科技等多家公司工作的实践经验，基本覆盖了互联网和传统行业运维的各个领域，估计这是迄今为止第一本由这么多资深运维专家联合写成的图书，也是第一本分享了众多一线运维专家亲身实践的图书。本书的出版也充分体现了互联网开放合作的精神。

看到本书的目录时，我激动不已，即使书中的内容我已经看了好多遍，但是在回顾目录的时候，我依然感到这是一本非常有吸引力的书，是一本每名运维工程师都应该案头常备的书！

本书特色

当前IT领域的概念层出不穷，云计算、物联网、移动互联网、大数据、人工智能、VR，所有的这一切都基于IT系统，IT系统正在向规模更大、更复杂、更高级的方向演进，一切IT资源都掌握在运维手里，通过运维来操作。这个时代对运维的要求越来越高，运维如果稍有不慎，就会造成重大的损失，所以随着IT系统的发展，运维的重要性也越来越高。

根据量子力学理论，世界由基本粒子组成，因此世界是不连续的，这个理论在运维知识体系的建立上同样适用。仔细回想一下自己运维体系的建立，就是逐个攻克和掌握知识点，再进一步通过实践不断加深的过程。《运维前线》也是这样，其中的每一篇文章都能够协助读者更快地掌握一个或多个知识点，相信通过运维前线系列的逐步出版，最终能够覆盖更多的运维知识点，为读者带来更多的实践经验和理念。

作为运维前线系列的第一本书，本书覆盖了运维自动化、系统运维、云及虚拟化、Web运维、游戏运维、DBA运维等6个方面14个知识点，都是各位作者总结自己多年实践经验的干货，每一篇文章都很精彩，都值得读者仔细品味。

本书作者

本书第1章“自动化运维之深度解码”，来自订阅号“互联网运维杂谈”的作者、优维科技创始人王津银，人称“老王”。我在多个场合听过“老王”的分享，有40分钟的，也有长达3个小时的，令我惊讶的是，“老王”每次总能带来新的东西。这篇“自动化运维之深度解码”也是如此，凝结了老王许多最新的见解，值得深读。

胥峰是我在盛大游戏的前同事，从胥峰身上我学到了不少知识，比如解决问题的思路和方法，有时候碰到运维难题，也许换一个角度就能迎刃而解。

刘宇、尹会生、陈立军是我多年的同事。刘宇、会生已经出版了多本运维图书，他们都是非常资深的运维专家。刘宇无论演讲还是文章都逻辑清晰，丝丝入扣。会生和立军分享的都是我亲眼所见的、在工作中碰到的难题及解决方法。

张观石是欢聚时代（YY）互娱事业部业务运维负责人，有多年的将PHP运用到日常运维中的经验。观石将PHP用到了极致，即使不懂PHP，也可以通过本书一窥观石在运维方面的丰富经验。

马亮有多年的游戏运维经验，目前在腾讯云专注做游戏云的架构设计，对游戏运维有深刻的理解。

本书的作者还有冉宏元（老男孩）、余洪春（抚琴煮酒）、吴传玉、彭华盛、蒋迪、赵旻、赵海军。虽然我与他们未曾谋面，但是彼此都是熟悉的网友，他们的文章我都曾仔细拜读并多次请教过，其中的运维思想让我深深折服，非常期待能有机会向他们当面请教。

读者对象

本书面向所有的运维工程师，无论是资深运维，还是刚入行的运维，相信都能从本书中获益。本书的读者对象包括如下几类：

·系统运维工程师

·安全工程师

·数据库运维工程师

·业务运维工程师

·网络运维工程师

·运维系统开发工程师及架构师

·云计算/虚拟化运维工程师

·其他对运维感兴趣的读者

勘误和支持

由于作者的水平有限，编写时间比较仓促，书中难免会出现一些错误或不准确的地方，恳请读者批评指正。如果您有更多宝贵的意见，欢迎关注我的订阅号“云技术实践”，然后在后台将您的宝贵意见发送给我。本书的勘误也会通过订阅号进行发布，关注订阅号请扫描下面的二维码：

“运维前线”是一个系列，欢迎在平时工作中积累了实战经验的广大运维工程师继续参与“运维前线”的写作，带来更多的干货分享！

期待能够得到你们的真挚反馈，在运维之路上互勉共进。

致谢

从开始构思到《运维前线》的出版，本书的创作过程犹如十月怀胎，期间也获得了无数的支持与肯定。借此机会，向所有为此奉献力量的人表示深深的感谢。

感谢参与本书写作的14位行业专家，他们在百忙之中伏案写作，将自己的经验共享给广大读者。能够和你们合作，我深感荣幸，经过将近一年时间的沟通和交流，你们的专业和执着深深地打动了我，同时也让我学到了不少东西。

感谢机械工业出版社华章公司的首席策划杨福川，编辑高婧雅、孙海亮。在近一年的时间中，你们的敬业精神不断地鼓舞着我前进，你们的鼓励、帮助和坚持引导了本书的完成。

这里，我还要特别感谢七牛云。七牛云是我见过的最具技术气质的云服务企业，肩负“帮助开发者缩短从想法到产品的距离”的使命，立志打造以数据为核心的场景化PaaS服务。七牛云主张技术共享并热衷于技术的传播，发起架构师实践日，推出各类创业扶持计划，这些都是七牛云正在做的事。

特别致谢

本书在成书过程中，得到了一批热心志愿者的协助，他们是陈家豪、曹学朋、邓荣兴、刘海文、李斯朗、韩海林，尤其刘海文做了大量的义务工作，在此特别感谢！

肖力

志愿者的话

能在力哥组织的这本书中担任助理，我感到非常荣幸。本书出自一群经验老到并乐于分享的运维前辈之手，是一本案头必备的好书。

刘海文

第1章　自动化运维之深度解码

作者简介

王津银，2005年硕士毕业，参与电信BOSS系统研发两年。而后于2007年进入腾讯公司接触运维，经历服务器从百到万的运维历程，先后在YY和UC参与不同业务形态的运维，期间带过前端运维、数据存储运维、YY语音、游戏运维、运维研发等多种运维团队，对运维有着全面的理解。极力倡导互联网价值的运维理念，即面向用户的价值是由自动化平台来交付和传递，同时由数据化来提炼和衡量的。“精益运维”理论的创始人。个人微信公众号“互联网运维杂谈”（waynewang_ops），粉丝2.5万人，现创办优维科技公司，旨在缩短企业到达互联网运维的路径。

自动化运维是一个人让人兴奋且容易失控的话题！兴奋是因为我想做一次尝试，把它的全貌和细节说清楚；容易失控是因为涉及点太多，一则怕遗漏，二则怕顾此失彼。带着这份复杂的心情，我们来一次自动化运维的解析之旅吧。说实话，一个运维团队的运维能力究竟如何，其实看一个自动化管理系统便可得知！

1.1　概述

作为开篇，首先让我们来熟悉一下运维全平台的规划体系吧，如图1-1所示。

很多人看到这样一个架构图，可能会纳闷，难道对于一个小型企业来说，也要实施如此复杂的运维自动化体系吗？其实，对于不同规模的企业来说，对运维自动化的诉求的确是不同的。对于大规模企业，如BAT，这些能力基本上都是必不可少的；而对于小型互联网企业，比如说App开发公司，则核心的自动化诉求可能更多的是配置管理工具，比如说Puppet、SaltStack或Jenkins+Rsync等。

图1-1　运维全平台规划体系

我们不禁要问，有什么样的准则可以让我们作为依据来判断何时该如何导入自动化？应该导入自动化的哪些部分？当你需要持续、频繁地进行一些事情时，此时就需要引入自动化，比如说版本发布，如果这个时候你感觉到很痛苦，那么此时就需要引入自动化了。关于应该导入自动化的哪些部分，我个人的经验是根据角色去梳理他的工作现状（持续、频繁的工作），然后引入自动化的能力，再根据角色人数的多与少来确定事情的优先级，比如说系统管理和业务发布，很明显业务发布的优先级更高，因为它的自动化所带来的人力解放的收益更大。当然还有一种更理想的情况，那就是根据整体业务交付流来构建，以它的全流程自动化为目标，此时引入的是该交付链上所有的自动化能力，当然对于很多企业来说，这种自动化实现的代价很高，而得到的收益却很小。

1.2　运维自动化的三重境界

宋代禅宗大师青原行思（六祖门下首座）提出参禅的三重境界：

参禅之初，看山是山，看水是水；

禅有悟时，看山不是山，看水不是水；

禅中彻悟，看山仍然山，看水仍然是水。

这三重境界其实和我们眼中运维自动化的三重境界是类似的。

运维自动化第一重境界：看山是山，看水是水。
开始接触运维自动化的时候，我们看到了很多工具认为它们就代表着自动化，比如说早期将Expect+SSH封装在一起之后，就认为可以实现批量运维了。看到有人说Puppet可以做配置管理，这个时候就会认为Puppet可以做配置管理，甚至是发布管理。这个时期的典型问题就是以偏概全，对于某个开源自动化工具来说，还没法去界定它的使用场景和范围，这样将直接影响系统的建设效益。这个时候才开始知道我们看到的山不是真正的山，而是迷雾环绕的深山。

运维自动化第二重境界：看山不是山，看水不是水。
此时我们已经知道只有Expect+SSH还不够，随着业务规模的变化，我们还需要一个更完整的概念来做发布系统，真正的发布系统要做版本管理、环境管理、配置管理，还有生命周期管理等；配置管理工具想让自动化变得更加完美，其实还要依赖于OS和应用层的标准化规范，比如说应用交付规范、应用打包规范、OS的统一等等。对于其他资源对象的管理来说，生命周期的概念均穿行其中，比如说DNS、LVS、接口、配置、应用包等。为了有效地标识资源的生命周期状态，需要用大量的数据来实时反馈。这是运维自动化更具体的层面，将一个个的山貌都看清楚了。

运维自动化第三重境界：看山还是山，看水还是水。这是一种自动化本质上的追究，站在高山之巅，俯览众山，会发出原来如此的感叹：所有自动化的本质都是为了可视化，让所有的人看到一致的服务，从而确保结果一致；从底层来说，你可以认为所有自动化的本质都是指令+文件分发的组合；你会进一步抽象系统的运维自动化能力，提供即插即用的机制；结合服务化的需求，进一步云化所有的运维系统，确保内外使用的一致性，最终自动化的平台就是一个整合的持续交付平台。这是化境！

1.3　运维自动化的困境和价值

1.3.1　运维自动化的困境

谈到运维自动化的困境，我觉得要带着两大行业特点去看待这个问题，一个是互联网行业、另外一个就是传统行业。这两个行业面临的运维自动化的困境完全不同，普遍的共性是运维研发资源能力的不足。

对于互联网行业，业务的发展速度很快，底层运维自动化能力可通过IaaS公有云来解决。在OS之上的运维自动化，则是通过一些开源工具来解决的，比如说Puppet、SaltStack、Ansible等。大部分都是以开源工具为主，开源产品的引入，也在不断加大维护的难度和复杂度，带来的另外一个问题就是平台可扩展的能力非常弱。所以一般成规模的互联网企业，最后都走向了自研的道路。不过有利于互联网行业运维平台建设的条件是互联网的基础比较标准，在硬件和软件的差异上不像传统企业那么大。

对于传统行业，业务的互联网是瞬间展开的，另外传统的封闭式系统架构也走向了开放式x86架构，导致运维维护的基础设施对象和上层的业务对象提升了一个数量级。而传统企业的运维手段之前都依赖于商业产品和人肉运维等方式。

无论是成规模的互联网企业还是传统企业，在业务的倒逼之下，运维的突破力都是不断向前的，但是这个整体的规划蓝图是什么样的、实施路径如何、需要什么样的方法论，则需要有一个全面的解答。

1.3.2　运维自动化的价值

谈到运维自动化的价值，运维人员应该很容易就能达成我所说的如下共识。

首先是效率的提升、人力的解放，通过工具或平台来提升人均的运维效率和产出，比如说之前通过人肉发布一天只能发布10张单，现在通过工具，一天可以发布100张单。

其次提升了产品的交付效率，提高了业务的竞争能力。快是制胜的法宝之一，如果你的产品推出得比对方更快，那么你就能更快地接触到用户或客户。

还能提升产品的质量，通过工具不断去提高持续交付链上各角色的能力，比如说测试组的自动化测试、配置管理组的持续集成服务，等等，通过能力的整合，不断提高软件交付的质量；还有在发生故障的时候，能有更快的恢复手段来确保故障的恢复，也是质量保障的一部分。

最后运维自动化的收益是成本的节省，一种是最直接的人力成本的节省，可以让更少的人做更多的事情；间接的成本受益是把很多运维经验固化成平台的经验，从而减少了整个交付链上的文档化内容的输出。

1.4　运维自动化的多维解读

1.4.1　基于应用变更场景的维度划分

我们曾经探讨过，所有运维的价值导向最终都是面向业务、面向用户，所以自然而然就需要从业务的维度进行划分。而运维是有很多种场景的，但从业务的角度来说，核心的业务场景一般就包括如下5种：业务上线、业务下线、业务扩容、业务缩容和应用升级。下面将以其中一种场景为例，将整个流程穿起来看看，以此识别流程的节点到底对接了哪些系统？针对其他的业务场景，我们也可以用同类的方法进行分析。首先预设业务的架构如图1-2所示。

图1-2　业务架构示例图

（1）业务上线。
表示上线一个完整的应用。从无到有部署整个业务上线，具体的流程如图1-3所示。

仔细看图1-3中所示的流程，我们会发现该流程涉及多个系统，每个系统所完成的职能又都有不同，这里只是大概地描述了一下。但一旦将这个流程清晰地梳理出来，我们就能知道真正地将一个应用全部上线到底有多复杂了。但看完图1-3又会觉得其实比较简单了，因为从业务上线的流程来看，我们只需要一个上层的流程调度引擎再加上对应的执行器，执行器通过API和底层的各个系统对接即可。这也是为什么之前在框架图（图1-2）中，要求各个专业系统一定要向上提供API，并且要求这个API的风格必须是一致的。

图1-3　业务上线流程

最复杂的业务上线流程梳理完成之后，业务下线
其实很简单，它是上线过程的逆过程，上线负责装，下线负责拆。

业务上线之后，随着用户活跃度的上升，业务的容量逐渐会出现不足的情况，此时就需要进行业务扩容
。业务扩容其实很简单，当某类节点出现不足的时候，就对它进行扩容。业务扩容所要做的变更，其实都是业务上线的子流程。比如说如果Web层容量不够，那就申请机器，安装组件、下发应用包，进行自动化测试。这个时候需要注意的是：在业务上线的过程中，我们把很多的配置信息都下放到CMDB中了，因此我们在选择扩容的时候，就要从CMDB中把信息读取出来，以指导变更。

应用升级
，目前持续集成所讲的自动化都集中在这块。简单来讲，就是升级程序包、升级配置、执行额外的指令等，一般来说逃脱不了这几种模式。读者可能会问，如果正如你所说的这么简单，那么是不是将SSH封装成一个UI就可以了。当然不是，这个时候还需要你以对运维的理解，在底层进行一些标准化的工作，否则你提供的就只是一个工具，而完全没有运维的思路，比如说程序运行属主、运行路径、监控的策略等。另外建设应用发布平台的目的就是要让测试和生产环境的运维变得更可控。

以上几个运维场景的自动化是否要一次性全部做完呢？当然不是，它们也是有先后和主次之分的。对于以上的运维场景，我在当前所负责的游戏运维中做过统计，数据如图1-4所示。

图1-4　持续部署的数量

（2）持续部署的数量
，一个月2000次左右。

（3）其他场景的分布情况
。一个月上线一次、下线两次、扩容一次左右。

有了这个数据，我们在建设一个自动化系统的时候，就能意识到应该先做什么后做什么。当然，不同的企业有不同的实际情况，还是应该找到核心痛点，而不是一上来就建设完整的业务变更系统，那样不仅见效不快，且容易让项目收益不大，从而遇到很大的阻力。

1.4.2　基于系统层次的维度划分

首先来看下系统层次的维度划分，如图1-5所示。

图1-5　基于系统层次的维度划分

给出如图1-5所示的分层体系图，其实就是为了让大家更好地识别系统的职责和范围，下层干上层的活，或者上层干下层的活都是越界，越界将带来耦合的问题。举个例子，系统服务层由Puppet（或者Chef）配置管理，但网上的很多资料都说Puppet可以用来做发布，也就是说可以做应用服务层的事情。后来我看过几家用Puppet来做应用服务层发布的公司，最后都走不下去，因为应用包的需求变化太大，只靠Puppet编写factor的模式来适应所有的场景，基本上是不可能的，所以说Puppet适合的是系统服务层配置管理。以上所举的例子就是一种越界！

1.4.3　基于与业务程序耦合紧密程度的维度划分

基于与业务程序耦合紧密程度的维度划分，这点非常重要，这个划分其实是确定系统建设的Owner，从而避免让运维团队承担过多的系统建设职能，否则将会导致运维能力提升缓慢。那么应该如何判断与业务程序耦合的紧密程度呢？我的准则非常简单，线上程序直接调用的就是紧耦合，或者由研发主导的公共服务，类似于API/SDK类的后端服务，应该由测试来主导系统建设；有些服务与程序不是直接关联的，或者是由运维牵头建设的，则由运维来主导，例如LVS、DNS服务等。

有这样一种情况，在很多应用程序中，DNS和LVS服务也存在于程序调用链中，怎么办？在我的方案中，绝对不允许内部服务走DNS和LVS。我们都知道DNS和LVS的服务对于服务异常的处理（DNS无状态、LVS是七层能力弱），远远达不到线上服务的要求，所以要坚决拒绝。如果真的有人要使用DNS和LVS，那么第一告诉他们业务的风险；第二，发生故障的时候，需要让研发参与处理。另外这也是系统的边界没划分清楚的问题，是让运维组件去承担业务上应该具备的容灾容错功能，这会令后面的运维系统建设增加很多不必要的功能。

1.4.4　面向服务的自动化能力划分

运维最终是需要对外提供服务的，这些服务能力应该由很多底层的自动化平台来承载，个人对其能力划分如下，其实细分到每一层都将发现自动化无处不在，而服务化的能力其实是自动化平台的核心能力，能力划分具体如图1-6所示。

图1-6　自动化平台

1.运营能力层

运营能力可体现IT运营价值，把IT的运营价值和业务场景紧密联系在一起，这些场景和之前所谈的运营价值体系（质量、成本、效率和安全）是一致的。在运维发展的不同阶段，IT系统的运营价值体现会有所不同，IT运营的核心方法是要有迭代式的思维
。

对于很多企业来说，自动化提升效率是运维的第一个价值突破点；再往后，业务的高可用保证和成本控制，则是下一个价值方向；再之后，精细化运营的业务支撑则是更高的诉求，类似于质量要求（质量的概念非常宽泛）。越往后，越能凸显数据的价值，而非自动化工具的价值。因此我个人觉得在某一个阶段，自动化平台突破之后，主要的瓶颈将不是效率，而是数据化IT运营的能力。该能力在依赖平台的同时，更依赖于运维团队的业务理解能力和经验总结
。数据化运营能力是精细化的运营能力，是面向产品的，从底层的基础设施质量、到应用的访问体验、再到产品发布后的用户满意度，等等。

这一层的能力表现为一个具体的产品形式＋运营方法
，从而可以确保能够很好地闭环起来。举个例子来说：基于资源容量管理的成本优化能力，首先需要一个贴合面向应用的容量分析模型，现实中一般是通过应用所消耗的资源（CPU、内存、I/O、网络等）进行分析运算；其次是需要通过IT运营分析产品来呈现应用的容量水平（当前、历史等）；基于可视化的数据呈现，建立相应的容量管理机制。这里又分为如下三点：

（1）建立标准。低负载需要提升资源使用容量、高负载则需要扩容资源（降低资源使用容量）。

（2）明确责任人和职责。确定容量管理的负责人，可以按照应用的粒度进行划分，同时还要明确这部分的管理要求，对于大规模服务来说，可以借用考核的工具来进行驱动。

（3）结果驱动。通过定时的结果可视化来驱动容量管理的持续优化。

2.平台能力层

一个完整的运维平台应具有以下特点：

·其能力是集成的
，而非离散的
——平台需要提供很好的集成能力，让系统得到收敛，避免将系统分割成单个的执行单元，用户也会为此痛苦不堪。

·其能力是场景化的
，而非基于功能需求的
——场景能够串联工具。

·其能力是基于角色的
，而非基于单一用户的
——运维的角色能够清晰地定义场景需求，用户的需求往往是片面而不真实的需求。

·其能力是基于事务的
，而非基于职能的
——事务能够跨越职能组，让运维组织的自动化和数据能力流动起来。

平台能力是指基于底层平台构建起来的具有的运维自动化／数据化（监控＋分析）／安全的能力，这层能力实现了底层能力的组合与封装，屏蔽了底层各个专业子平台的实现细节，是面向业务运维场景的，比如说应用交付、资源交付、业务交付、持续反馈等。

3.通用能力层

通用能力层是基于基础设施之上封装的公共服务能力，这层架构的能力可分成两个部分：一部分是面向业务技术架构的，另一部分是面向运维服务架构的
。图1-6中所列的服务只是其中的一部分，这个也是我经常和交流者强调的能力建设的核心，不能把这个问题留给下面的资源能力层，也不能交给上层的平台能力层。

对于线上技术架构来说，通用能力层将会涉及名字服务、负载均衡服务、分布式缓存、消息队列、分布式关系存储等，运维需要对其技术实现的工作人员要求API直接调用的服务能力。

对于运维服务来说，通用能力层提供了资源服务、作业服务、部署服务、F5管理、GSLB等。这层的平台能力我一直将其理解成是PaaS平台的核心，有了它们其实就可以实现端到端的能力调度。

该层服务能力平台可以很好地对上层平台进行积木式的支撑，同时还可以对底层设施层能力做服务化能力交付，脱离了资源交付的范畴。

4.基础设施层

基础设施层是资源交付层，对于一个运维系统来说，应该屏蔽底层基础设施的交付能力，无论是IaaS，还是物理层基础设施。尤其是对于一些IaaS云平台来说，更应该屏蔽IaaS底层实现的细节差异，通过API服务向上提供能力。国外早年就有了同类的产品，如RightScale，它很好地实现了多云管理的能力。

1.5　运维自动化的方法论

1.全局驱动

无论是全部自动化管理平台的规划，还是某个平台的规划，都希望大家能够找到一个全局的立足点。比如说我们当时成立持续部署服务平台的时候，大家把全局的目标对齐于提高产品交付的速度和质量，开发、测试、运维很快就达成共识了。目前这个平台建设完成之后，运维已经从发布变更流程中彻底退出了，真正实现了让运维变成审核者。

2.分而治之

从上面的几个维度中可以看到有很多系统，如果每个系统都要建设的话，那么周期和难度都将很大。所以需要分而治之，特别是线上架构组件的管理系统，更需要随着组件的交付一并交付运维管理能力，比如面向组件的自动化管理能力、运维的监控能力、运维的数据分析能力等。之前我也表达过类似的观点，所有只交付组件，不交付管理能力的研发都是耍流氓。因为从运维的角度来说，这样低价值的交付产品越多，越会导致运维不堪重负。而如果让运维从头去构建这个管理，则他们需要花费很多的时间去了解，从而导致系统建设周期拉长。举个例子，比如说某个分布式cache服务，做得不好的，是通过读取日志然后对其进行监控；做得好的，是给你开启一个管理端口，让你从端口中读取状态信息。这就大大降低了系统的复杂度（不用进行日志采集和处理组件了）。

分而治之，其实就是让不同的团队做不同的事情，不要将所有事情全部压给运维；其次不同的时期建设不同的系统，不要在同一时刻做很多系统，从而避免战线过长。当然如果有很多运维研发人员的话，就另当别论了。

3.自底向上

自底向上，其实是让大家找到一个更清晰更具体的系统建设目标来展开工作。从系统分解上，来让大家规避被一个庞大而模糊的目标带入歧途。如果一上来，我们就说要做一个全自动的运维管理系统，那样很容易就会让运维研发团队迷失方向。所以这里可以先设定全局和最终目标（全自动化），然后从底层逐步构建地基，做框架，最后再盖一个完整的房子，详见图1-1。

4.边界清晰

边界有两个维度，一个是管理边界；一个是职能边界。

首先是管理边界，其是从Owner的角度出发的，谁产生服务，谁就是Owner，管理统一都是运维。比如研发提供了一个统一的分布式消息队列服务，那么Owner就是研发，他应该对可运维性负第一责任，不要让运维去承担这个服务的WebAdmin管理系统建设任务。

其次是职能边界，深层次的理解是组件的功能范围。对运维架构师的考验也就在这儿，比如说让LVS去承担业务异常的容灾和容错切换是不合适的；让DNS跨过LVS层，负责后端服务异常的自动容错处理也是不合适的。如果不把职能界定清楚，将会导致系统做很多无用功，这会增加系统建设的复杂度。

5.插件化

插件化的思维无处不在，在面对纷繁复杂的管理对象时，我们进行抽象，提供管理模式，然后将具体的实现交给用户，这点在我们日常所见的运维系统中经常可以看到，比如说Nagios就是一种插件化的采集思路。对于配置管理来说，Puppet采用的也是这个思路。对于最上层的调度管理系统，可以让运维自己去编写执行器，特别是和业务紧密相关的，但最终运维整个控制权还是要交给平台。我的经验是，在应用服务层
和架构服务层
，不要引入插件化的管理方案，过多的插件化部署，会让生产环境的管理最终混乱不堪，甚至失控。所以提供类SSH界面的运维发布和部署平台，是没有任何运维价值的。

1.6　运维自动化系统的实现

挑战自动化的极致场景（可视化），是运维人员对极致的追求。极致的自动化是运维事务全流程的自动化，运维事务全流程自动化是包含了一次应用完整交付所涉及的所有资源的自动化能力，比如说DNS资源、负载均衡资源、数据库资源、服务器资源、配置资源等。下面将列举几个典型的运维自动化系统以供大家参考。

1.6.1　DNS管理系统

DNS是Web形态下的一个重要入口，用户服务的访问严格依赖于这个服务入口。现在一般被称为GSLB（全局服务负载均衡调度），目前是CDN服务中的重要服务节点。实现的目标都是要解决运维从哪里来，到哪里去最快，当目标机房发生故障的时候，如何把服务调度走。

在移动APP大量应用的今天，DNS协议的缺点已经逐渐暴露出来了，DNS解析时间长，另外还经常会被劫持。因为有端的控制，现在逐渐开始走HTTPDNS的服务，通过HTTP服务的方式获取域名对应的IP地址，此时由DNS平台直接对外提供HTTP服务。在有端App的情况下，还可以借助端的数据挖掘技术，识别非权威DNS域名是否存在被劫持的情况。系统需要保持和业务的与时俱进。

这里还需要注意一个问题，内部DNS能否统一管理？理论上是可以的，把单个机房当作单个的view，不过我不建议将两个场景耦合在一起，尽管这样能够实现统一管理。

系统Demo如图1-7所示。

图1-7　DNS管理系统

1.6.2　CMDB管理系统

CMDB管理系统的建设这里就不展开介绍了，感兴趣的读者可以关注微信公众号“互联网运维杂谈”，并参阅《运维平台之CMDB系统建设》一文。

系统Demo如图1-8所示。

1.6.3　名字服务中心系统

“名字服务中心系统”的概念最初来自于Zookeeper，该系统结合实际情况，实现了名字服务中心。把程序接口之间的调用抽象成单个服务之间的调用，在服务中心实现调度的统一注册、鉴权、ACL、容灾容错控制。将其看作线上服务最核心的系统，一点也不为过，并且它还是收益最大的系统，可直接替换掉DNS、LVS，降低线上系统对运维系统的依赖性。

系统Demo如图1-9所示。

图1-8　CMDB管理系统

图1-9　名字服务中心系统

1.6.4　持续部署管理系统

持续部署是应用升级的核心系统，该系统每个月都承担着大量的变更。在系统规划之初，我们就给它设定了清晰的业务管理目标：持续交付的一部分，实现图1-10中的4个维度管理目标；也设定了具体业务的运维目标：升级所有的包和配置，且让业务运维彻底退出业务的变更流程。具体如图1-10所示。

系统Demo如图1-11所示。

持续部署系统是持续交付系统的核心（持续集成、持续测试、持续部署、持续反馈），它是产品发布到达生产环境的关键步骤。在这个平台的建设上，运维人员应该将它作为突破的第一个点。在该平台搭建完成之后，运维就可以从日常的部署事务中解放出来了。

图1-10　持续部署管理系统示意图

图1-11　发布系统

1.6.5　运维调度管理系统

运维调度平台又称为调度编排系统，编排是一种场景化的运维能力封装，是对复杂运维事务的封装。我们在平时的运维过程中能够看到很多复杂的运维场景，比如说容灾切换、故障处理、服务迁移等。这些场景，很多时候都不是单一的动作就能够完成的，往往需要借助多种运维能力组合，如图1-12所示。

图1-12　运维调度管理系统示意图

在图1-12中，我们把Ops自动化调度下面的服务支撑层分解为三部分：工具平台OpsStore，用来编写日常的运维工具；外部服务，用于公共API对外提供封装；Ops发布，用于提供代码持续部署服务。

一个完整的自动化调度平台应具备能够对接一切服务的能力，例如通过配置管理来初始化内核、通过OpenStack来初始化资源、通过DNS来获取全局调度服务、通过存储来获取存储的服务，甚至还可以通过公有云API来获取外部公有云的资源服务能力，如图1-13所示。

图1-13　自动化调度平台示意图

还有数据库运维管理平台、分布式Cache管理系统等也都有相应的实现，由于篇幅所限，这里就不贴图介绍了。

1.7　运维自动化系统的API参考实现

所有的底层系统都是通过API对外提供服务的，API可供各个系统使用。接口的使用需要通过授权来获得，建议这个授权可以是基于系统级别的，也可以是接口级别的，而不是采用统一开放的模式。另外接口内需要有相应的一些权限控制，以避免底层服务被任意操作。

可以仿照AWS的接口实现方式，统一实现API的接口开放访问地址，同时统一协议（HTTP、HTTPS），协议可以使用Get的方式进行访问。图1-14所示是一个开放API的结构。

图1-14　开放API的结构

1.8　运维自动化依赖的团队模型

下面将从能力模型、驱动模型和技能模型三个角度来阐述运维团队和个人的能力要求，最后给出一个参考的组织结构。

1.8.1　团队的能力模型

具体的团队能力模型示意图如图1-15所示。

图1-15　团队能力模型

对于我带过的应用运维团队，我都会从如上三个方面对组员提出运维能力要求。

1）业务运维。
因为对这块能力的要求越来越低，因此其在我们的考核体系中所占的比重也越来越低。日常的变更、扩容、故障定位、运维规划对人的能力要求都非常低，这些工作都能模式化且平台化，从而减少了对人的倚重。

2）运维研发。
我希望每一个应用运维人员都有运维研发的能力，但这在现实中是不可能的。对于应用运维团队和运维部门来说，运维研发的配备必不可少。在应用运维团队的内部，可以让有研发能力的人迅速承担面向业务运维平台的建设，或者参与到部门的运维系统建设中，可以抽出50%的时间参与研发。运维研发能力是能够让团队价值迅速达成的有效保证，没有研发能力的运维不能成为一个好运维。

3）技术研究。
运维是一个技术团队，需要通过技术来体现价值，当找到好的技术时就要想着如何将技术应用到业务上，为用户带来价值，比如说提升用户体验，减少成本等。

这个时候就会产生一个问题，应用运维团队内的人也会运维研发，同时又有专职的运维研发团队，那么他们的职责分工如何解决，在工作上是否会存在重复建设？我的回答是这样的：

首先，可以把运维研发初期定位在公共服务平台的研发上，比如说DNS、LVS、配置管理、监控系统、CMDB、数据分析平台等。

其次，运维研发还需要制定相应的运维研发规范，代码规范、UI规范、测试规范等，让所有参与运维研发的人统一遵守，包括应用运维研发的组员。

最后来说一下应用运维小组内的研发能力该如何发挥的问题。其实在很多运维团队中，运维都是跟随业务的，一则可以让应用运维研发人员开发面向业务的运维系统，因为他们最了解该业务的需求，能够实现自己想要的；另外一种更好的操作方式，是让应用运维小组内的研发人员抽出50%的时间参与到以运维研发牵头成立的虚拟研发小组中。一则可以进一步提高应用运维的研发水平；另外还可以提高运维研发对业务运维的理解，同时还能提高带队作战的能力。

那么，运维研发和应用运维的比例应该设置成多少比较合适？我个人认为3：1比较合适，大家也可以自检一下，自己的运维团队到底设置了多少运维研发人员？另外想要检测运维研发配备是否足够，可以周期性地看看运维团队取得的进步，特别是效率和质量等维度。

一个高性能的运维团队一定是以应用运维和运维研发为核心构建的！

1.8.2　团队的驱动模型

具体的团队驱动模型如图1-16所示。

图1-16　团队驱动模型

团队的驱动力不同，带来结果的就会完全不同。为什么很多运维人员都说自己很辛苦？这时你可以思考一下到底是什么在引导着你进行运维工作？传统的维护，往往都集中在第一阶段和第二阶段，而进入到高阶运维体系之后，我们需要迅速切换到价值驱动和用户驱动的维度上来。有了用户驱动和价值驱动，对运维的效率和质量就都会有更高的要求，对于外部驱动我们必须走自动化和平台这条道路。建议大家在平时的工作中加入质量、效率、成本等一些KPI要求，不要只局限于自己所做的事情，而是要关注自己所做的事情对产品和用户的影响。

1.8.3　团队的技能模型

BAT（百度、阿里、腾讯）很早就实施了职业通道体系，在运维侧细分了多个能力通道，比如说网络运维、业务运维、运维研发、DBA等。对于运维人员的成长也有明确的要求和衡量体系，在此我就不详细介绍了。

1.8.4　参考的运维团队组织结构

我们不一定要按照这个结构明确设置运维小组，但是运维的职能差不多就是这样。我还有另外一个建议，最好将公共服务研发团队和运维团队放在一个组织结构下，这样将会有利于公共化服务的推广，而公共化服务对运维效率的影响是最大的，如图1-17所示。

图1-17　运维团队组织结构

至此，自动化平台的深度解码已经完成。本章从多个层面带领大家了解运维自动化，其实还是希望能给大家带来一点借鉴意义。大胆地往前走吧，一切都有可能，唯独那些实现不了的，都是我们人的问题，无它。

1.9　小结

无论是传统企业还是中小型互联网公司，运维自动化都是当前运维阶段的核心能力。实现了运维自动化，传统的人肉运维状况才会被改变，才会有更多的精力去思考更大的运维价值。在建设运维自动化平台的过程中，对运维自动化的需求进行清晰的识别、界定、规划和落地是很考验运维研发者能力的事情，本章从整体规划入手，给出了一些具体的方法论，同时还给出了几个实现的例子，目的是帮助大家看到一条清晰的建设路径。

第2章　利用Facter和Django快速构建CMDB

作者简介

陈立军，金山西山居DevOps，原新浪研发系统开发。

刘宇，网名守住每一天，金山西山居架构师，《Puppet实战》一书作者，《Puppet实战手册》译者之一，《Python高级进阶》译者之一，InfoQ社区编辑，自动化运维专家。

CMDB（Configuration Management Database），又称配置管理数据库，更多的时候我们习惯将其称为资产管理系统。它既是ITIL标准体系的核心，又是运维的基础核心系统。它通常位于整个运维自动架构的底层，但在运维自动化体系中，它又起到了极其关键性的作用。据我个人了解目前很多中小型企业，乃至一些中大型互联网公司，都还在采用传统的Excel来管理资产。

采用Excel维护资产时，需要耗费大量的人力和精力，这些工作包括：数据的采集、整合、记录、维护、检验和更新，每一项都比较繁琐。而采用开源CMDB系统却又不够灵活，无法和公司的其他系统相结合。面对这些问题，自行研发一套适用于公司的CMDB系统，是当务之急。

本章将会详细讲解，如何利用开源软件Facter和Django快速构建一个小型的CMDB系统。界面与功能不算复杂，这也是它轻量的体现，本章更多的是提供一种思路，以达到抛砖引玉的作用，仅供大家参考。

说明

本章的所有代码都托管在Github网站上https://github.com/oysterclub/open-cmdb
。其中脚本集中在CMDB目录中。

2.1　CMDB简介

CMDB用于存储和管理企业IT架构中各种设备的配置信息，其中包括主机、项目、用户、机房、网络等。其被认为是ITIL服务管理的核心，所有流程所需要使用的配置信息都将通过CMDB来进行获取，例如监控、Dashboard、自动化、流程等。CMDB在企业IT架构中的核心地位如图2-1所示。

图2-1　CMDB的核心地位

CMDB特性如下：

·设备信息统一化、标准化。

·设备信息可维护性更强。

·设备信息关系清晰化、可视化。

·设备信息的查询、更新更快。

当然，在实际生产中，会对上述特性和涵盖范围有轻重大小的区分，为了能够更加合理、更加准确地设计出适合自有业务模式的CMDB，下面我们来看下几大主流开源CMDB是如何设计的。

2.2　开源CMDB介绍

目前主流的开源CMDB软件包括：OneCMDB、CMDBuild、Itop CMDB、Rapid OSS、ECDB、i-doit等，其中比较出名的是前二者，因此本节将重点对比这两款开源软件。

2.2.1　OneCMDB介绍

OneCMDB主要面向的是中小型企业。可以作为一个独立的CMDB来保持软件和硬件资产及其相互关系的轨道。由于其具有开放的API，因此其也可以是拥有灵活的强大的配置管理引擎的其他服务管理软件。

OneCMDB易于安装和填充数据，它有一个无需用户具有编程能力就能改变和增强的数据模型，它能让用户轻松做到如下几点：

·创建CMDB数据模型，而无需写代码。

·填充数据，可以通过网络自动发现。

·通过各种灵活的导入和转换机制来从外部源获取信息。

·导入/导出网络配置信息从/到Nagios网络监控系统。

OneCMDB也有如下一些缺点：

·纯英文操作界面，增加了误操作率。

·只支持从Nagios系统自动发现导入。

·现在该产品基本无人维护。

·UI可定制化低。

官网地址http://www.onecmdb.org/
，纯英文操作界面，官网wiki上的截图如图2-2所示。

图2-2　OneCMDB操作界面

2.2.2　CMDBuild介绍

CMDBuild是一个通过Web界面配置的CMDB系统。可以通过Web界面来进行建模、创建资产数据库，并处理相关的工作流程。

CMDBuild可用于集中管理数据库模块和外部应用：自动库存、文档管理、文本处理、目录服务、电子邮件、监控系统、用户网站、其他信息系统等。

官网地址http://www.cmdbuild.org/
，也是纯英文操作界面，截图如图2-3所示。

图2-3　CMDBuild操作界面

CMDBuild应该是使用者比较多的一款产品，它具有如下优点：

·Ajax操作十分便捷（采用了ExtJS作为支持）。

·数据格式自由定制（在GLPI中，资产的数据格式都已经定义好了，用户很难再做修改）。

·支持多种开源标准（XPDL）。

·可以自定义Workflow，便于ITIL。

·有专门的团队在不断进行维护，截至本章写作时，最新版本为于2016年6月16日更新的2.4.1版本。

·SOAP和REST的WebService接口。

要说其缺点也就只有一条让人望而却步：文档少、资料极少。

2.2.3　其他的开源CMDB

随着开源潮流的发展壮大，开源的CMDB也越来越多，比如Itop CMDB、Rapid OSS、ECDB、i-doit等。然而这些开源的样式长得都差不多。最大的弊端在于不能有效地与其他系统友好结合。如果只是单纯地进行统计使用，也未尝不是一种选择。有时，现有的开源工具和系统不能满足业务发展的需求，我们就需要修改或完全自行编写一个符合业务需求的工具或系统，即“造轮子”。

自己造轮子有如下几个优点。

·需求：满足公司内部的各种需求。

·可控：自行控制。

·体验：增加联动性，打通各个流程。

·提升：提升个人开发水平。

2.3　Puppet及Facter介绍

本节所要讲的不是重复造轮子，而是如何有效地利用开源工具构建自己的平台。首先我们来看看CMDBuild操作界面，如图2-4所示。

图2-4　CMDBuild操作界面

从图2-4中可以看出，CMDB的信息收集可以是多种多样、共融共存的：

（1）通过一些Agent客户端收集信息，然后注册到中心服务器数据库。

（2）通过监控系统收集，比如Zabbix、Nagios、IPMI等。

（3）通过配置管理工具收集，比Puppet、SaltStack、Ansible等。

我在本文中使用配置管理工具Puppet的Facts来进行信息收集，最主要的原因是，目前我使用的是Puppet来管理所有的机器，因此使用Facts最方便、简单、快捷，成本低而且效率高，如果读者使用的是Ansible或其他配置管理工具来做管理，本文中所介绍的方法也可以通用。

2.3.1　什么是Puppet

通常定义：Puppet是一个跨平台的集中化配置管理系统，它使用自身的描述语言，可管理配置文件、用户、Cron、软件包、系统服务等，Puppet把这些统称为“资源”。Puppet设计的目标就是简化对这些资源的安装、配置、管理，以及妥善处理资源之间的依赖关系。

2.3.2　为什么是Puppet

Puppet能够帮助系统管理员方便、快速地管理所有机器，目前Puppet已经能够支持所有的客户端，主流的有RedHat、CentOS、Gentoo、Debian、Ubuntu、Solaris、SuSe、Mac OS X、FreeBSD、OpenBSD、Windows，等等。

支持的资源众多：目前Puppet支持的资源有很多，其中常用的包括File、Package、Exec、Service、Group、Host、Router、Yumrepo、User、Cron、SSHKey等，更多的相关信息和使用方法可以参考https://docs.puppetlabs.com/references/latest/type.html
。

第三方模块众多：目前Puppet的第三方模块众多，基本包含了我们所能使用的全部软件模块，例如Mysql、Django、Java、PHP、Nginx、Apche、Openstack、SSH、Tomcat、Jboss、Yum等。

2.3.3　什么是Facter

Facter是Puppet跨平台的系统性能分析库。它能发现并报告每个节点的信息，在Puppet代码中是以变量的形式出现的。它返回的是每个Agent的fact信息，这些信息包括主机名、IP地址、操作系统、内存大小及其他的系统配置选项，这些fact信息在Puppet Agent运行的时候进行收集并传递给Master，同时被自动创建为可以被Puppet使用的变量。

2.3.4　Facter的特点

Facter最大的作用就是收集服务器系统信息，包括主机名、IP地址、操作系统、内存大小及其他的系统配置选项。这些系统配置选项正是CMDB所需要的基础核心数据。

我们先来看看Facter收集的系统数据，下面将列举一些常用的系统数据，具体信息如下。

（1）Facter获取fqdn信息，在Facter中fqdn=hostname+domain：

$ facter fqdn
puppet.domain.com

（2）Facter获取IP地址：

$ facter ipaddress
10.20.122.100

（3）Facter获取MAC地址：

$ facter macaddress
00:1A:4A:25:E2:10

（4）Facter获取空闲内存大小：

$ facter memoryfree
1.61G

（5）Facter获取内存大小：

$ facter memorysize
1.83G

（6）Facter获取操作系统：

$ facter operatingsystem
CentOS

（7）Facter获取CPU信息：

$ facter processors
{"models"=>["Intel Core 2 Duo P9xxx (Penryn Class Core 2)", "Intel Core 2 Duo P9xxx (Penryn Class Core 2)"], "physicalcount"=>2, "count"=>2}

（8）Facter获取机器运行时间：

$ facter uptime
1 day

2.3.5　Facter变量

Facter目前的最新版本为3.4.1（截至本章写作时），支持的变量有131个，所支持的变量可以在官方网站http://docs.puppetlabs.com/facter/latest/core_facts.html
中查看。Facter所支持的变量可以按使用频率简单划分为常用类型和不常用类型，具体如图2-5所示。

图2-5　Facter变量分类图

2.4　如何利用Python获取Facts

2.4.1　工作原理

通过2.3节的学习可以知道Facts可以获取主机的系统信息，并以K-V形式进行存储，我们只需要处理Puppet Server收集的Agent Facts信息、入库，然后通过Django来读取数据库信息即可，如图2-6所示。

图2-6　Facts信息获取流程图

2.4.2　利用Python脚本获取Facts

首先要确定系统中已经安装了Puppet，如果没有，可以从http://yum.puppetlabs.com/
下载，并参考https://docs.puppetlabs.com/puppet/latest/reference/install_pre.html
进行安装，接下来看看如何通过Python程序来获取Facts信息（注意：下面的程序是查看当前机器的fact信息，下面的这个程序对结果不会做过多的处理，在后面进行CMDB项目的时候将详细讲解fact的数据处理），实例程序facter_message.py的内容如下：

#!/usr/bin/python
# encoding: utf-8
__authors__ = ['LiuYu']
__version__ = 1.0
__date__ = '2015-08-19 14:34:44'
__licence__ = 'GPL licence'
# 导入模块

import commands
import re
# 定义一个变量

command = 'facter'
# 定义要打印的列表

show_list = [('fqdn', u'主机名

'),
('domain', u'域名

'),
('uptime', u'运行时间

'),
('operatingsystem', u'系统

'),
('kernelrelease', u'内核版本

'),
('ipaddress', u'IP'),
('macaddress', u'MAC'),
('memorysize_mb', u'内存

MB'),
('processors', u'CPU'),
('blockdevices', u'磁盘

'),
]
# 定义一个处理命令的函数

def handle_command_message(command):
status, content = commands.getstatusoutput(command)
if status == 0:
return content
else:
return
# 通过函数处理名称，然后打印结果

if __name__ == '__main__':
result_dict = {}
result = handle_command_message(command)
if result:
for line in result.strip().split('\n'):
if re.findall('=>', line):
key, value = line.split('=>', 1)
result_dict[key.strip()] = value.strip()
for f_k, f_s in show_list:
if f_k in result_dict:
print f_s, ':', result_dict[f_k]

下面我们来运行facter_message.py程序，打印结果：

$ python facter_message.py主机名

: puppetclient.domain.com域名

: domain.com运行时间

: 1 day系统

: CentOS内核版本

: 2.6.32-431.1.2.0.1.el6.x86_64
IP: 10.20.122.111
MAC : 00:22:E2:5E:4D:10内存

MB : 996.48
CPU : {"count"=>1, "models"=>["QEMU Virtual CPU version 1.1.2"], "磁盘

: sr0,vda,vdb,vdc

通过如上的简单代码就可以将Facts的信息进行集中处理。

2.5　使用Django快速构建CMDB系统

2.5.1　Django介绍

Django是一个免费的、开源的Web框架，由Python语言编写，由于其是在一个快节奏的新闻编译室环境中开发出来的，因此它的设计目的是让普通开发者的工作变得简单。Django遵循模型-视图-控制器（MVC）框架模式，目前由一个非盈利的独立组织的软件基金会（DSF）维持。

Django鼓励快速开发和干净实用的设计。Django可以更容易更快速地构建更好的Web应用程序。它是由经验丰富的开发人员来创建的，省去了Web开发的很多麻烦，因此你可以专注地开发应用程序而不需要去白费力气地重复工作。

Django目前已经被运维圈广泛使用，本文在此不会详细介绍Django的基础知识，有兴趣的朋友可以去Django官网查看更为详细的介绍，同时也有Django中文文档可供学习。

2.5.2　Django安装

Django的安装分为4个步骤，下面以Django 1.7.1、CentOS 6.5 x86_64为例进行讲解，详细步骤如下。

1.安装Python 2.7.x

用CentOS 7以下版本的朋友需要将Python升级到2.7.x以上，Django对Python版本存在依赖，具体如图2-7所示。

图2-7　Django与Python版本的依赖关系

编译步骤如下：

# yum install -y zlib-dev openssl-devel sqlite-devel bzip2-devel
# wget http://www.python.org/ftp/python/2.7.6/Python-2.7.6.tgz
# tar zxf Python-2.7.6.tgz
# cd Python-2.7.6
# ./configure --prefix=/usr/local
# make && make altinstall

（1）安装easy_install工具，操作命令如下：

$ wget https://bootstrap.pypa.io/ez_setup.py -O -| python

（2）安装Django，使用easy_install来安装，安装的版本为1.7.1，具体命令如下：

$ easy_install django==1.7.1

（3）测试Django的安装，操作命令如下：

$ easy_install django==1.7.1
$ django-admin –

version
1.7.1

2.MySQL安装

本文推荐使用yum命令进行安装，并设置MySQL root密码，创建cmdbtest数据库，具体安装步骤如下：

$ yum -y install mysql mysql-server
$ mysql_install_db --user=mysql
$ /etc/init.d/mysqld start
$ mysqladmin -u root password 'cmdbtest'
$ mysql -u root -pcmdbtest -e 'create database if not exists cmdbtest'

2.5.3　Django常用命令

完成Django的安装后，可以通过如下命令快速熟悉Django的操作，以便快速创建一个CMDB App，如果你对如下这些Django命令很熟悉，可以直接跳过。

（1）新建一个django-project：

$ django-admin startproject project-name

（2）新建App：

$ django-admin startapp app-name

（3）同步数据库：

$ python manage.py syncdb

（4）启动Django服务器：

$ python manage.py runserver

（5）Django Shell调试：

$ python manage.py shell

（6）帮助：

$ django-admin --help
$ python manage.py --help

2.5.4　Django的配置

1.环境准备

笔者准备了两台测试机器用来进行代码测试，测试机器的环境信息分别如下。

（1）服务端机器信息：

IP: 10.20.122.100
Role: puppet server + cmdb
System OS: CentOS release 6.5 x86_64
Python version: 2.7.8 Django version: 1.7.1
Mysql version: 5.1.73

（2）客户端机器信息：

IP: 10.20.122.111
Role: puppet agent
System OS: CentOS release 6.5 x86_64

2.软件安装

前几节已经对所需要的环境进行了安装，在这里我们再回顾一下：

（1）master安装Puppet Server。

（2）master安装Python。

（3）master安装MySQL。

（4）master安装Django。

（5）master安装项目依赖的Python模块。

（6）Agent安装Puppet Agent。

3.创建CMDB项目

创建CMDB项目的同时，在这个项目中创建一个CMDB App，登录10.20.122.100，运行如下命令。

（1）创建一个Project：

$ django-admin startproject myproject

（2）进入myproject目录：

$ cd myproject

（3）创建一个CMDB App：

$ django-admin startapp cmdb

（4）创建一个存放静态文件和模板的目录：

$ mkdir static templates

运行成功后使用11命令就可以看到如图2-8所示的目录结构。

4.配置CMDB项目信息

在图2-8中我们可以在myproject目录下看到settings.py的全局配置文件，Django在运行时会默认先加载此配置文件，因此我们需要先对它进行定义，需要配置如下6个地方，操作步骤具体如下。

图2-8　Django安装后的目录组织结构

（1）修改数据库设置：

DATABASES = {
'default': { 'ENGINE': 'django.db.backends.mysql',
'NAME': 'cmdbtest',
'HOST': 'localhost',
'USER': 'root',
'PASSWORD': 'cmdbtest',
'PORT': '3306',
'OPTIONS': {'init_command': 'SET storage_engine=INNODB', 'charset': 'utf8', }
} }

（2）设置App，把我们新建的CMDB App加到末尾，代码如下：

NSTALLED_APPS = ('django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles', 'cmdb',
)

（3）设置静态文件存放目录：

STATICFILES_DIRS = ( os.path.join(BASE_DIR, 'static/'), )

（4）设置模板文件存放目录：

TEMPLATE_DIRS = [ os.path.join(BASE_DIR, 'templates'), ]

（5）设置登录URL：

LOGIN_URL = '/cmdb/login/'

（6）设置其他参数，可以根据自己的需求进行设置：

TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.static', 'cmdb.context_processors.menu', "django.contrib.auth.context_processors.auth", "django.core.context_processors.request",
) CMDB_VERSION = '1.0' CMDB_NAME = u'测试

CMDB' LOGIN_REDIRECT_URL = '/home/'

到此为止基础环境已经准备完毕，接下来需要设计数据库，并定义好视图。

5.数据表设计

Django遵循MVC设计，其中M（模型）就是数据库模型，也就是App中的models.py文件的设置，Django自带数据库的ORM（Object Relational Mapping）架构，这使得我们不用再需要学习复杂的数据库操作，只需要通过定义models即可。下面是我设置的最简单的CMDB数据结构：

# 指定解析器为

Python
# !/usr/bin/env python
# 指定字符编码为

utf8
# encoding:utf8
# 从

django.db中导入

models模块

from django.db import models
# 导入

User模块

from django.contrib.auth.models import User
# Create your models here.
# 定义一个

Server_Group类，从

models.Model中继承，这里就是所谓的数据表结构

class Server_Group(models.Model):
# 定义主机组名称字段

name = models.CharField(u'主机组

', max_length=255, unique=True)
# 关联的项目字段，这里是关联一个外键

project = models.ForeignKey("Project", verbose_name='项目名称

')
# 备注字段

memo = models.CharField(u'备注

', max_length=255, blank=True)
# unicode返回值

def __unicode__(self):
# 返回的格式

return '%s-%s' % (self.project.name, self.name)
# 定义

Meta属性

class Meta:
# 数据库中的表名

db_table = 'server_group'
# 存储的时候需要确认组合键是唯一的

unique_together = (("name", "project"),)
# 定义一个

IDC类，主要存储

IDC信息，数据表结构有

2个字段

class IDC(models.Model):
# 定义

IDC的名称字段

name = models.CharField(u'IDC名称

', max_length=255, unique=True)
memo = models.CharField(u'备注

', max_length=255, blank=True)
def __unicode__(self):
return self.name
class Meta:
db_table = 'idc'
# 定义一个

Project类，主要存储项目信息，数据表结构有

2个字段

class Project(models.Model):
name = models.CharField(u'项目名称

', max_length=255, unique=True)
memo = models.CharField(u'备注

', max_length=255, blank=True)
def __unicode__(self):
return self.name
class Meta:
db_table = 'project'
# 定义一个

Server_Role类，主要存储服务器角色信息，数据表结构有

3个字段

class Server_Role(models.Model):
name = models.CharField(u'角色

', max_length=255)
# 关联

Server_Group，也就是服务器组

group = models.ForeignKey("Server_Group", verbose_name='项目组

')
memo = models.CharField(u'备注

', max_length=255, blank=True)
def __unicode__(self):
return '%s-%s-%s' % (self.group.project.name, self.group.name, self.name)
class Meta:
# 设置数据库表名

db_table = 'server_role'
# 存储的时候需要确认组合键是唯一的

unique_together = (("name", "group"),)
# CMDB核心数据表结构，用来存储服务器系统信息

class Server_Device(models.Model):
# 服务器状态选择，具体的字段存储数据为

0~

3的

int数字

SERVER_STATUS = (
(0, u'下线

'),
(1, u'在线

'),
(2, u'待上线

'),
(3, u'测试

'),
)
# 定义一个名称字段

,若

blank没有设置则默认为

False，不能为空，且

unique=True必须唯一

name = models.CharField(u'主机名称

', max_length=100, unique=True)
# 定义

SN编号字段，

blank=True，可以为空

sn = models.CharField(u'SN号

', max_length=200, blank=True)
# 公网

IP字段，可以为空

public_ip = models.CharField(u'外网

IP', max_length=200, blank=True)
# 私网

IP字段，可以为空

private_ip = models.CharField(u'内网

IP', max_length=200, blank=True)
# 定义

MAC地址字段

mac = models.CharField(u'MAC地址

', max_length=200, blank=True)
# 定义操作系统字段

os = models.CharField(u'操作系统

', max_length=200, blank=True)
# 定义磁盘信息字段

disk = models.CharField(u'磁盘

', max_length=200, blank=True)
# 定义内存信息字段

mem = models.CharField(u'内存

', max_length=200, blank=True)
# 定义

CPU信息字段

cpu = models.CharField(u'CPU', max_length=200, blank=True)
# 关联

IDC信息

idc = models.ForeignKey(IDC, max_length=255, blank=True, null=True, verbose_
name='机房名称

')
# 定义一个多对多字段，一台服务器可以对应多个角色

role = models.ManyToManyField("Server_Role", verbose_name='角色

', blank=True)
# 机器状态，默认都为在线状态

status = models.SmallIntegerField(verbose_name='机器状态

', choices=SERVER_STATUS,
default=1)
# 管理用户信息

admin = models.ForeignKey('auth.User', verbose_name='管理员

', null=True, blank=True)
# 定义备注字段

memo = models.CharField(u'备注

', max_length=200, blank=True)
def __unicode__(self):
return self.name
class Meta:
db_table = 'server_device'

初始化数据库，同时设置登录所需要的username和password，命令如下：

$ python manage.py syncdb
Operations to perform:
Apply all migrations: admin, contenttypes, auth, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying sessions.0001_initial... OK
You have installed Django's auth system, and don't have any superusers defined.
Would you like to create one now? (yes/no): yes
# 这里输入用户名

Username (leave blank to use 'root'): admin
Email address:
# 这里输入密码

Password:
# 重复输入密码

Password (again):
Superuser created successfully.

在命令行登录数据库，并查看数据库信息，就能看到如图2-9所示的内容，说明数据库已创建成功。

图2-9　查看数据库是否已创建

6.视图设置

上文中我们已经成功设置了Django的M（Models，模型），下面我们来设置V（View，视图），如下代码是一个登出页面和一个home页面的View：

# encoding:utf8
# Create your views here.
# 导入需要使用的模块

from django.template import RequestContext
from django.shortcuts import render_to_response
from django.contrib.auth.decorators import login_required
from django.contrib.auth.views import logout_then_login
# 判断用户是否登录

@login_required
# 登出时的调用

def logout_view(request):
return logout_then_login(request)
# 判断用户是否登录

@login_required
# 登录后调用

home页所展示的页面，

template为

home.html
def home(request):
return render_to_response('home.html', locals(), context_instance=RequestCo
ntext(request))
URL设置

(这里直接使用了

Django自带的

login函数，所以不需要自己写

login view)：

# 设置字符编码

# encoding:utf8
# 从

urls中导入

patterns、

include、

url模块

from django.conf.urls import patterns, include, url
# 从

contrib中导入

admin模块

from django.contrib import admin
# 从

http中导入

HttpResponseRedirect模块

from django.http import HttpResponseRedirect
# 设置前端访问的

URL对应的后端视图

urlpatterns = patterns('',
# url什么参数都不带时，直接重定向到

login
url(r'^$', lambda x: HttpResponseRedirect('/login/')),
# 登出对应的视图为

cmdb.views.logout_view
url(r'^logout/$','cmdb.views.logout_view', name='cmdb_logout'),
# 登录对应的

view为

django.contrib.auth.views.login，对应的

template为

login.html
url(r'^login/$','django.contrib.auth.views.login', {'template_
name': 'login.html'},
name='cmdb_login'),
# home页面

,对应的

view为

cmdb.views.home
url(r'^home/$', 'cmdb.views.home', name='home'),
)

通过如上定义，现在就启动Django服务，登录后即可看到如图2-10所示的界面。代码已托管至Github网站https://github.com/oysterclub/open-cmdb
，有兴趣的朋友可以去复制下来查看、修改或使用。下面就来展示一下登录界面的效果图（注，前端框架为bootstrap）。

图2-10　系统登录界面

利用python manage.py syncdb命令输入的用户名和密码登录。登录后的页面为home空白页（见图2-11），具体如下（home空白页主要是为了以后做导向流页面或数据图表展示页面，这里先留空）：

图2-11　系统登录后的界面

7.使用Python程序获取Facts数据

通过如上定义，我们已经完成了视图、数据表结构的定义。而数据的来源既可以通过添加，也可以通过Facter工具来获取。下面我们就来讲讲如何自动获取Agent机器的系统数据（如果想要充分了解Facter工具，可以参考阅读《Puppet实战》的第9章“Facter介绍”）。Facter工具会在Puppet Agent与Puppet Master通信的时候把获取到的Agent主机系统信息和自己定义的Facts信息汇报给Puppet Master，生成一个hostname.yaml格式的文件，文件存放在/var/lib/puppet/yaml/facts目录下，文件的格式如图2-12所示，其中的values数据：domain、ipaddress、macaddress等正是构建CMDB所需要的系统数据，因此我们可以通过一个Python程序来处理这些数据，并录入MySQL中，最终通过Django来实现前端展示。因此一个最简单的CMDB系统就构建完成了。

图2-12　Facter上报至Puppet Master后的yaml部分信息

我们先来具体看一下facter_message.py程序（Python程序处理Facter数据），完整代码如下：

#!/usr/bin/env python
# encoding: utf8
__authors__ = ['liuyu', 'chenlijun']
__version__ = 1.0
__date__ = '2015-09-06 14:58:23'
__licence__ = 'GPL licence'
# 导入模块

import yaml
import os
# IPy主要用来判断

IP类型，

IPy.IP('ip').iptype()
import IPy
# yaml文件目录

yaml_dir = '/var/lib/puppet/yaml/facts'
# 结果集，结果集的格式

{'cmdb_agent':()}
all_host_facter_message = {}
# 结果列表

result_list = ['name',
'SN',
'public_ip',
'private_ip',
'mac',
'os',
'disk',
'mem',
'cpu',
'idc',
'role',
'status',
'admin',
'memo']
# db对应的

Facter字段

,需要获取其他的字段时可以一一对应

list_field = {'name': 'fqdn',
'public_ip': 'ipaddress__interfaces',
'private_ip': 'ipaddress__interfaces',
'mac': 'macaddress__interfaces',
'os': ['operatingsystem', 'operatingsystemrelease', 'hardwaremodel'],
'disk': 'blockdevice__blockdevices',
'mem': 'memorysize',
'cpu': ['processorcount', 'processor0']}
# ruby objectobjectconstruct
def construct_ruby_object(loader, suffix, node):
return loader.construct_yaml_map(node)
def construct_ruby_sym(loader, node):
return loader.construct_yaml_str(node)
# 读取数据

def yaml_file_handle(filename):
stream = open(filename)
mydata = yaml.load(stream)
return mydata
# 获取

IP的类型

def get_ip_type(ip):
try:
return IPy.IP(ip).iptype().lower()
except Exception, e:
print e
# 处理单个

Agent的数据

def handle_facter_message(data):
# 定义一个结果字典，字段和

db一样，处理完的结果和

db中的一样

result_dict = {}
# 对结果进行处理

for db_field in result_list:
# 定义一个字段结果字符

value = ''
# result_list中的字段是否存在于我们需要的

Facter取值列表中，如果存在

if db_field in list_field:
facter_field = list_field[db_field]
# 先判断

facter_field的类型，然后进行处理

if type(facter_field) == type([]):
for tag in facter_field:
if data.get(tag):
value += data[tag] + ' '
else:
# 由于

disk、

IP等需要进一步处理，所以用了一个

__来分隔，然后再进行处理

field_tmp = facter_field.split("__")
if len(field_tmp) == 2:
if db_field == 'disk':
for tag in data[field_tmp[1]].split(","):
# 对磁盘进行处理

, 由于磁盘的字段为

blockdevice_type_size，

所以需要单独进行处理

f = field_tmp[0] + '_' + tag + '_' + 'size'
if data.get(f):
# 去除

sr0 tag的字段

if tag != 'sr0':
# 结果字符串

value += tag + ':' + str(int(data[f]) / 1024 /
1024 / 1024) + 'G' + ' '
# 对外网

IP进行处理

elif db_field == 'public_ip':
for tag in data[field_tmp[1]].split(","):
f = field_tmp[0] + '_' + tag
if data.get(f):
# 去除

lo tag的字段

if tag != 'lo':
if get_ip_type(data[f]) == 'public':
# 结果字符串

value += data[f] + ' '
# 对内外

IP进行处理

elif db_field == 'private_ip':
for tag in data[field_tmp[1]].split(","):
f = field_tmp[0] + '_' + tag
if data.get(f):
# 去除

lo tag的字段

if tag != 'lo':
if get_ip_type(data[f]) == 'private':
# 结果字符串

value += data[f] + ' '
else:
# 其他的字段直接就处理了

for tag in data[field_tmp[1]].split(","):
f = field_tmp[0] + '_' + tag
if data.get(f):
# 去除

lo tag的字段

if tag != 'lo':
# 结果字符串

value += tag + ':' + data[f] + ' '
else:
if data.get(facter_field):
# 结果字符串

value = data[facter_field]
# 将结果添加到

result列表中

result_dict[db_field] = value.strip()
# 如果不存在

else:
result_dict[db_field] = ''
# 返回结果字典

return result_dict
# 定义获取

facter的函数

def get_all_host_facter_message():
# 由于

Puppet的

yaml文件是

Ruby格式的，因此需要进行转换

yaml.add_multi_constructor(u"!ruby/object:", construct_ruby_object)
yaml.add_constructor(u"!ruby/sym", construct_ruby_sym)
# 获取所有有

Facter信息的主机文件名称

for dirpath, dirnames, filenames in os.walk(yaml_dir):
# 只需要处理

yaml目录下以

yaml结尾的文件

if dirpath == yaml_dir:
for file in filenames:
file_name, file_ext = os.path.splitext(file)
if file_ext == '.yaml':
host_yaml_path = yaml_dir + '/' + file
# 得到

yaml文件的内容

, 字典形式

host_yaml_result_dict = yaml_file_handle(host_yaml_path)
# 对单个

Agent的数据进行处理

if host_yaml_result_dict:
# 由于有

key为

facts,所以可以直接查找

facts key的值

if host_yaml_result_dict.has_key('facts'):
data_dict = host_yaml_result_dict['facts']['values']
# 没有的就直接取

else:
data_dict = host_yaml_result_dict['values']
# 现在就可以对

data进行处理，获取我们所需要的数据了

result_dict = handle_facter_message(data_dict)
all_host_facter_message[file_name] = result_dict
#返回我们最终的数据结果集

return all_host_facter_message

以上程序可以过滤Facter中我们想要得到的Agent数据，运行facter_message.py程序，结果输出如下：

$ python facter_message.py
{'puppetclient.domain.com':
{'status': '',
'name': 'puppetclient.domain.com',
'mem': '1.83 GB',
'memo': '',
'idc': '',
'public_ip': '',
'admin': '',
'mac': 'eth0:00:1A:4A:25:E2:12 eth1:00:1A:4A:25:E2:13',
'role': '',
'private_ip': '10.20.122.111',
'disk': 'vda:20G vdb:30G',
'os': 'CentOS 6.5 x86_64',
'cpu': '2 Intel Core 2 Duo P9xxx (Penryn Class Core 2)',
'SN': ''}
}

到这里，我们能够看到facter_message.py得到的数据字段和models.py中数据结构的字段正好一样，下一步我们就可以直接将facter_message.py的数据导入到数据库中了，具体程序如下：

# 检测用户是否登录

@login_required
# 定义一个

views，用来处理导入信息

def import_data(request, model):
# 导入计数器

import_num = 0
# 查看

model是否存在于定义的模板中

if model in BASE_ADMIN:
# 获取

tag名称

tag_name = BASE_ADMIN[model]['name']
# 获取

model名称

model_name = BASE_ADMIN[model]['model']
# 这里只处理

server_device的导入信息

if model == 'server_device':
server_device_data = get_all_host_facter_message()
# 进行数据入库处理

for hostname, facter_message in server_device_data.items():
# 主机名处理，判断

facter_message中

name key是否有值，

if facter_message['name']:
# 如果有值，

name就使用该值

name = facter_message['name']
# 如果没有这个值

else:
# 就使用

hostname
name = hostname
# 对于

IDC信息、

User信息、项目角色信息的处理都需要自己去写

Facter插件，不

写的都为空，然后进行处理

# IDC关联处理，如果

facter_message中的

idc key有值

if facter_message['idc']:
# idc_name就为该值

idc_name = facter_message['idc']
# 同时处理该

IDC信息是否存在于

IDC表中，如果有则取出

ID
if IDC.objects.filter(name=idc_name):
idc_id = IDC.objects.get(name=idc_name).id
# 如果没有

,则进行保存，然后取出

ID
else:
idc_sql = IDC(name=idc_name)
try:
idc_sql.save()
# 取出

ID
idc_id = IDC.objects.get(name=idc_name).id
except Exception, e:
return e
# 如果

idc key没有值，则为

None
else:
idc_id = None
# 管理员信息关联处理，如果用户存在则关联，不存在则跳过

if facter_message['admin']:
admin_name = facter_message['admin']
# 如果用户存在

User表中则取

ID，若没有则为空

if User.objects.filter(username=admin_name):
user_id = User.objects.get(username=admin_name).id
else:
user_id = None
# 没有就为空

else:
user_id = None
# 这里还有一个角色多对多关系的处理，由于这里没有定义机器角色，因此此处不处理

角色信息

# 判断主机是否存在于

server_device表中，如果不存在则添加

if not model_name.objects.filter(name=name):
import_sql = model_name(name=name,
sn=facter_message['sn'],
public_ip=facter_message['public_ip'],
private_ip=facter_message['private_ip'],
mac=facter_message['mac'],
idc=idc_id,
os=facter_message['os'],
disk=facter_message['disk'],
mem=facter_message['mem'],
cpu=facter_message['cpu'],
admin=user_id,
memo=facter_message['memo'],
)
try:
# 保存

import_sql.save()
except Exception, e:
return e
# 如果有了，则查询数据，若信息不对则更新

elif not model_name.objects.filter(name=name,
sn=facter_message['sn'],
public_ip=facter_message['public_ip'],
private_ip=facter_message['private_ip'],
mac=facter_message['mac'],
os=facter_message['os'],
disk=facter_message['disk'],
mem=facter_message['mem'],
cpu=facter_message['cpu'],
memo=facter_message['memo']):
try:
# 更新数据库

model_name.objects.filter(name=name).update(sn=
facter_message['sn'],
public_ip=facter_message['public_ip'],
private_ip=facter_message['private_ip'],
mac=facter_message['mac'],
os=facter_message['os'],
disk=facter_message['disk'],
mem=facter_message['mem'],
cpu=facter_message['cpu'],
memo=facter_message['memo'],
)
except Exception, e:
return e
# 如果有了，且信息

ok，则跳过

else:
continue
return HttpResponseRedirect('/cmdb/%s/show/' % model)
return render_to_response('all_data_show.html', locals(), context_instance=RequestContext(request))

重新登录CMDB之后的页面，有一个导入主机的按钮，点击导入主机按钮，就可以自动导入通过Facter获取的Agent主机信息了，如图2-13所示。

我们还可以通过添加的方式来维护CMDB的内容，到目前为止我们已经完成了使用Python和Puppet来构建一个小型的、简单的CMDB系统。

CMDB是需要我们定期进行维护和更新的，因此它还需要提供历史查看、API等更实用的功能，为此在2.6节中我们将介绍一下Django提供的几个好用的功能模块。

图2-13　信息导入界面

2.6　高级进阶

2.6.1　历史查询功能

操作历史，应该是管理系统必备的功能之一。Django-echelon就是一个很好的功能模块，该功能模块可以单独使用于任何的Django项目中，非常方便，接下来我们讲解如何将该功能添加到CMDB系统中，设置步骤具体如下。

（1）修改settings.py设置。

①添加echelon APP：

INSTALLED_APPS = (
'......',
'echelon',
)

②添加echelon中间件：

MIDDLEWARE_CLASSES = (
'......',
'echelon.middleware.EchelonMiddleware',
)

（2）修改urls.py设置：

# encoding:utf8
from django.conf.urls import patterns, include, url
urlpatterns = patterns('',
'......',
# 操作历史

url(r'^cmdb/changelog/', include('echelon.urls')),
)

（3）将echelon代码复制到Django项目的主目录中，将html代码复制到templates目录中。

（4）刷新DB，创建数据表：

$ python manage.py syncdb

（5）把操作历史的URL信息添加到前端页面的导航栏中就可以了（配置cmdb/cmdb_menu.py程序），具体操作如下：

CMDB_TOP_MENU = [
'......',
#导航名称、

URL、图标、子导航信息

[u'操作历史

', '/cmdb/changelog/', 'time', []],
]

（6）启动Django服务，打开前端页面，然后点击操作历史，就可以看到用户的操作信息了（见图2-14），具体如下。

图2-14　用户的操作信息

以上代码都可以在open-cmdb库上找到源代码。

2.6.2　API功能

CMDB作为一个数据源中心，很多运维工具都会调用CMDB数据进行使用，因此API接口就非常有必要了，由于各个系统的需求不一样，从头到尾开发一套适用于各个系统的CMDB API也比较困难，那么有没有什么简单的方法呢？答案肯定是有，这里我推荐Django API利器Django REST framework。

Django REST framework是一个非常强大、灵活的API构建工具，它能很容易、很快速地帮我们构建Web API。下面来讲解构建的过程，具体步骤如下。

（1）安装Django REST framework。

主要安装3个模块：djangorestframework、markdown、django-filter。具体代码如下：

pip install djangorestframework
pip install markdown
pip install django-filter

（2）配置settings.py：

INSTALLED_APPS = (
'......',
'rest_framework',
)
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
]
}

（3）设置urls.py：

urlpatterns += patterns(
'',
url(r'^api/', include(myproject.api.router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
)

（4）创建我们的CMDB API（根据models信息来创建，这里只附上一个简单的讲解，具体的内容请看源码）：

# -*- coding: utf-8 -*-
# 导入

cmdb.models模块

import cmdb.models
# 从

rest_framework中导入模块

from rest_framework import routers, serializers, viewsets
# 给需要生成

API的

model定义一个数据序列

# Serializers define the API representation.
class IDCSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
# 使用的

model名称

model = cmdb.models.IDC
# 字段序列

fields = ('url', 'name', 'memo')
# 定义视图

class IDCViewSet(viewsets.ModelViewSet):
# 查询所有数据，这个可以根据自己的需要来展示几个

queryset = cmdb.models.IDC.objects.all()
# 序列化信息

serializer_class = IDCSerializer
# 配置路由注册，自动生成

API URL
# Routers provide an easy way of automatically determining the URL conf.
router = routers.DefaultRouter()
router.register(r'idcs', IDCViewSet)

（5）配置导航栏信息：

cmdb_menu.py CMDB_TOP_MENU = [ [u'API文档

', '/api/', 'book', []], ]

（6）启动Django服务，展示如下（图2-15）。

图2-15　启动Django服务

通过API文档，可以看到全部的API信息，如图2-16所示。

图2-16　全部的API信息

点击IDC的具体URL，就能看到展示的信息和用法，如图2-17所示。

到这里CMDB API就完成了最基本的功能，后续还需要读者自己去调整页面和授权等。

图2-17　展示的信息和用法

2.6.3　数据表结构

对于数据表结构，前面的定义是比较简单的，有兴趣的读者可以根据自己的实际需求去增加，这里仅列举一个简单的优化例子——IDC表优化。

IDC表前面就一个IDC名称字段，这里将根据作者所在公司的需求来做优化，优化后的字段具体如下：

class IDC(models.Model):
# 机房名称字段

name = models.CharField(u'机房名称

', max_length=200, unique=True)
# 名称缩写

short_name = models.CharField(u'名称缩写

', max_length=200, unique=True)
# 机房地域

zone = models.CharField(u'机房地域

', max_length=200, blank=True)
# 机房地域缩写

zone_short_name = models.CharField(u'地域缩写

', max_length=200, blank=True)
# 机房地址

address = models.CharField(u'机房地址

', max_length=200, blank=True)
# 机柜

U数

unit_size = models.PositiveIntegerField(u'机柜

U数

', default=42, help_text="机房每个机柜拥有

Unit数目

")
# 机柜电数

power_size = models.PositiveIntegerField(u'机柜电数

', default=12, help_text="机房每个机柜拥有电力

(An)数目

")
INTERFACE_TYPE_CHOICES = (
(0, '光纤

'),
(1, '普通

'),
(2, '其他

'),
)
# 网络接入方式是光纤、普通还是其他

interface_type = models.SmallIntegerField(
'网络接入

', choices=INTERFACE_TYPE_CHOICES, default=0)
# 机房所有设备的总带宽

bandwidth_equipment = models.BigIntegerField(u'设备带宽

', default=0, help_text="设备带宽

(Mbps)")
# 机房可用带宽

bandwidth_usable = models.BigIntegerField(u'可用带宽

', default=0, help_text="可使用最大带宽

(Mbps)")
# 物理接入带宽

bandwidth_line = models.BigIntegerField(u'接入带宽

', default=0, help_text="物理接入带宽

(Mbps)")
# 保底带宽

bandwidth_min = models.BigIntegerField(u'保底带宽

', default=0, help_text="保底带宽

(Mbps)")
# 报警阀值

bandwidth_threshold = models.BigIntegerField(u'报警阈值

', default=0, help_text="报警阈值

(Mbps)")
LINE_MODEL_CHOICES = (
(0, '单线

'),
(1, '双线

'),
(2, 'BGP'),
)
# 机房线路类型是单线、双线还是

BGP
line_model = models.SmallIntegerField(
'线路类型

', choices=LINE_MODEL_CHOICES, default=0)
# 接入线路条数

line_count = models.IntegerField(u'线路条数

', default=1, help_text="接入线路条数

")
# 是否冗余

line_redundancy = models.BooleanField("是否冗余

", default=False, help_text="接入线路是否冗余

")
# 是否限速，限速大小

speed_limit = models.IntegerField(u'限速大小

', default=0, help_text="限速大小

(Mbps, 0:不限速

)")
CHARGE_TYPE_CHOICES = (
(0, '峰值

'),
(1, '95值

'),
(2, '保底

'),
(3, '计时

'),
(4, '包月

'),
(5, '包年

'),
)
# 计费模式

charge_type = models.SmallIntegerField(
'计费模式

', choices=CHARGE_TYPE_CHOICES, default=0)
# 是否直连

ISP
isp_direct_link = models.BooleanField("是否直连

ISP", default=False)
# 所属运营商，这里需要再建立一个

ISP的类

isp = models.ForeignKey("ISP", verbose_name="运营商

")
# 联系人

contact = models.ForeignKey(UserProfile, related_name="idc_contact", verbose_name="联系人

", help_text="IDC机房联系人

")
# IDC经理

manager = models.ForeignKey(UserProfile, related_name="idc_manager", verbose_name="经理

", help_text="IDC经理

")
# 值班人

duty_officer = models.ForeignKey(UserProfile, related_name="duty_officer", verbose_name="值班人

", help_text="IDC机房值班联系人

")
# IDC报障邮箱

support_email = models.EmailField(u'报障邮箱

', help_text="IDC报障邮箱

")
# 公司联系人

company_contact = models.ForeignKey(UserProfile, related_name="xsj_contact", verbose_name="公司联系人

", help_text="公司联系人

")
# 开通时间

start_time = models.DateTimeField(u'开通时间

',
auto_now_add=True)
# 到期时间

end_time = models.DateTimeField(u'到期时间

',
default=datetime.datetime(2140, 1, 1, 0, 0, 1))
memo = models.CharField(u'备注

', max_length=200, blank=True)
def __unicode__(self):
return self.name
class Meta:
db_table = 'idc'

现在这个表结构就是一个复杂的数据表结构，通过修改base_admin.py就可以直接展示了。

2.6.4　用户管理功能

Django有一套自己的用户管理、用户组管理和权限管理，我们在初始化Django项目的时候，Django会默认创建User、Permission、Group这3个表及对应的关联关系表。

本章所介绍的这套简单的CMDB系统，已经将展示、添加、编辑和删除都模块化了。通过模板构建用户管理也是一件很轻松的事情，只需要把Django自带的用户信息提取出来套用模板即可，具体步骤如下。

（1）在cmdb/base_admin.py中设置。

导入User模块：

from django.contrib.auth.models import User
'user': {
# model名称

'model': User,
# form表单

'form': User_CheckFrom,
#名称

'name': u'用户管理

',
#是否可以导入信息

'import': '',
# table展示字段

'list_display': ['username',
'password', 'email', 'is_superuser', 'is_active', 'is_staff',
'groups',
'user_permissions'],
#编辑只读字段

'readonly': ['username'],
#动作

'action_list': [(u'编辑

', 'pencil', '/cmdb/user/modify/'),]
},

（2）把用户管理的URL信息添加到前端页面的导航栏中。（配置cmdb/cmdb_menu.py程序），具体操作如下：

CMDB_TOP_MENU = [
'......',
#导航名称、

URL、图标、子导航信息

[u'用户管理

', '/cmdb/user/show/', 'user', []],
]

（3）启动Django服务，打开前端页面，点击用户管理，信息如图2-18所示。

图2-18　用户组管理信息

（4）到这里一个简单的用户管理功能就实现了，如果读者需要更详细的用户信息，可以去创建一个UserProfile表，然后关联到Django自带的User信息中，限于篇幅，本章就不逐一详细介绍了。

2.6.5　用户组管理功能

用户组管理和用户管理的原理是相通的，添加方式也是一样的，这里只是简述讲解一下添加的步骤。

（1）在cmdb/base_admin.py中设置：

'group': {
# model名称

'model': Group,
# form表单

'form': Group_CheckFrom,
# 名称

'name': u'用户组管理

',
# 是否可以导入信息

'import': '',
# table展示字段

'list_display': ['name',
'permissions'],
# 编辑只读字段

'readonly': ['name', ],
# 动作

'action_list': [(u'编辑

', 'pencil', '/cmdb/group/modify/'),]
},

（2）把用户组管理的URL信息添加到前端页面的导航栏中就可以了（配置cmdb/cmdb_menu.py程序），具体操作如下：

CMDB_TOP_MENU = [
'......',
#导航名称、

URL、图标、子导航信息

[u'用户组管理

', '/cmdb/group/show/', 'list', []],
]

（3）启动Django服务，打开前端页面，点击用户组管理（默认为空），信息如图2-19所示。

图2-19　添加用户组管理

（4）到这里组管理功能也实现成功了，现在我们就可以创建自己想要的组信息，把相应的用户添加到组中了。

到此为止，我们已经基于CMDB完善了用户、用户组、操作历史、开放API几大实用功能。通过这些构建，可以将CMDB和自动化运维平台的其他系统逐一打通，构建属于自己的平台。

2.7　小结

无论IT系统多么庞大或微小，也无论提供的服务是多是少，是繁杂还是单一；作为实际管理和掌控这套系统和服务的读者来说，必须要明确的一点是：人是整个系统中最薄弱和不可预测的一环。如果由人工来进行维护，那么数据不准确的情况100%是会出现的。而通过程序自动采集数据，进行规则入库这种自动化操作，则使数据达到100%的准确率成为了现实。

本文所展现的快速构建方法，仅仅是整套优化、完善动作的持续进行步骤中的一个环节，但此方法不但可以实现CMDB的快速构建，还对整个运维体系的建立、自动化平台架构的完善，也有非常重要的参考价值和借鉴意义。

最后希望读者能通过对本文的阅读，不仅能够构建出属于自己的、定制化的CMDB平台，还能通过更深入的思考，为完善运维工作、完美自动化平台增加更多更深刻的理解认知。

第3章　数据中心搬迁中的x86自动化运维

作者简介

吴传玉，具有10年以上x86服务器平台系统管理经验，熟悉Windows及Redhat Linux系列运维，自2008年接触VMware虚拟化产品，获得RHCT、VMware VCP和HCNA认证。善于使用各类脚本工具编制日常运维的脚本。目前就职于某大型金融企业基础设施运维部门，负责容灾及研发测试环境x86整体运维工作。

本章主要介绍在大型数据中心搬迁的过程中，如何利用自行编制的各类脚本，低成本、高效率又准确地完成大量节点的逻辑搬迁工作。

3.1　数据中心搬迁准备

3.1.1　数据中心搬迁介绍

世间万物都存在着自身的生命周期，大到宇宙恒星的诞生与消亡，小到细胞从一次分裂完成开始到下一次分裂结束所经历的全过程，都离不开生命周期。

作为承载着公司核心竞争、运维能力的数据中心，也无法回避生命周期的问题。当数据中心的空间、电力等基础硬件无法支持公司业务需求的高速扩展时，公司的决策层势必会考虑采用更合理的方案，以满足日益增长的业务需求。第一种是在保持现有数据资源不变的情况下，建立第二个数据中心（非容灾中心）；另一种是新建一个条件更优越的数据中心以替代旧的数据中心。

采用第一种方式，可以平滑地进行扩容，但从长远来看双倍的运维成本会加重公司的负担。如果采用第二种方式，虽然在搬迁过程中对基础层、应用层运维人员的要求相对较高，但从长远来看可为公司节省可观的运维开支，主要的成本仅在于一次性搬迁的方案所承担的开支。

大型金融企业中，基础运维人员相对于应用运维、开发人员、测试人员来说是属于更底层、更核心的角色。从整体运维的宏观结构来看，基础运维位于一个漏斗的底部出口位置，支撑着众多应用业务系统的基础需求。总结起来就是底层硬件架构多元化、需求种类多、运维量大，如果涉及搬迁则工作量更大更繁琐。

因我主要负责的是x86平台的非生产环境日常运维工作，且正参与数据中心的搬迁项目。故本章将从系统层基础运维人员的角度出发，介绍在大型数据中心搬迁的过程中，如何自行编制脚本，从而降低成本、高效准确地完成大量节点的逻辑搬迁工作。

首先介绍一下物理搬迁与逻辑搬迁的对比。

数据中心的搬迁一般分为两类：物理搬迁和逻辑搬迁。

如果物理设备及其上的逻辑节点数量少且搬迁距离较短，对可用性的要求不高，能够进行短期的停机，那么这种小型数据中心，一般较适用于同城数据中心的物理搬迁。

如果涉及的节点数量多且搬迁距离较长，对可用性要求较高，无法满足短期停机的要求，则需要考虑逻辑搬迁。

3.1.2　搬迁环境介绍

此次我所参与的搬迁存在诸多制约因素，如硬件种类繁多、应用关联性大、搬迁周期短、搬迁成本缩减等。基于以上现状分析，最终确定按应用环境，逐批次实施逻辑搬迁。

搬迁批次的定义如表3-1所示。

表3-1　搬迁批次定义

3.1.3　搬迁前的准备工作

搬迁过程不仅仅是物理设备位置的改变，还需要对虚拟化层及各个节点内的参数进行调整。由于金融企业的环境特殊，公司安全部门禁止自行配置自动化运维管理工具，因此只能利用现有的技术做文章了。

在此次迁移过程中，我利用了平时运维常用的一些系统自带的脚本工具。

这些工具分为三类，其中有用于虚拟化层的工具vCLI、PowerCLI，有用于Linux环境的Bash，还有用于Windows环境的批处理、WMIC和注册表。

·vCLI：vSphere CLI，命令行管理接口。

·PowerCLI：VMware vSphere PowerCLI，是一款功能强大的命令行工具，可自动执行vSphere的各方面管理，包括主机、网络、存储、虚拟机、客户操作系统等。

·Bash：Bourne-Again Shell，是一个为GNU计划编写的Unix Shell，是许多Linux发行版的默认Shell，Bash的命令语法是Bourne Shell命令语法的超集。数量庞大的Bourne Shell脚本大多不经修改即可在Bash中正常执行。

·批处理：Batch，也称为批处理脚本。顾名思义，批处理就是对某些对象进行批量处理，通常被认为是一种简化的脚本语言，它一般应用于DOS和Windows系统中。批处理文件的扩展名为bat。

·WMIC：WMIC扩展WMI（Windows Management Instrumentation，Windows管理工具），提供了对从命令行接口和批命令脚本执行系统管理的支持。

·注册表：Registry，是Microsoft Windows中的一个重要的数据库，用于存储系统和应用程序的设置信息。

3.1.4　搬迁信息收集

俗话说“九层之台，起于垒土”，因此对于搬迁来讲，基础信息至关重要。我们需要准确掌握现有搬迁环境所涉及的所有物理与逻辑信息，才能更好地完成搬迁任务。因此搬迁的首要任务就是收集所有的资源信息。

1.计算资源信息收集

作为目标端新设备的配置依据，应收集（包含但不限于）如下信息：型号、序列号、CPU、MEM、硬件管理IP、HBA WWN号
[1]

。

编辑文件/tmp/getESXIinfo.sh，保存内容如下：

# 获取物理设备型号

xh=`esxcli hardware platform get | grep "Product Name" | awk -F: '{print $2}'`
# 获取物理设备序列号

SN=`esxcli hardware platform get | grep "Serial Number" | awk -F: '{print $2}'`
# 获取物理

CPU个数

cpunum=`esxcli hardware cpu list | grep "CPU:" | wc -l`
# 获取物理内存容量（以

GB计算）

mem=`esxcli hardware memory get | grep "Physical Memory" | awk -F" " '{print
$3}'`
TotalMem=` expr $mem / 1024 / 1024 / 1024 `
# 获取硬件管理

IP
vmkip=` esxcli network ip interface ipv4 get | grep vmk0 | awk '{print $2}'`
#获取

HBA WWN信息

wwn=`esxcli storage core adapter list | grep link-up | awk -F: '{print $2}'
| awk '{print $1}'`
wwn1=`echo $wwn | awk -F" " '{print $1}'`
wwn2=`echo $wwn | awk -F" " '{print $2}'`
echo $xh,$SN,$cpunum,$TotalMem,$vmkip,$wwn1,$wwn2 >/tmp/$vmkip.csv

可将以上脚本文件上传至所有ESXI系统，以备批量收集信息之用。图3-1为显示结果。

图3-1　显示结果

2.存储资源信息收集

基于现有x86环境对ESXI主机通过FC和FCoE协议分配外挂存储，因此目标数据中心仍保持原有架构不变，数据传输采用基于同构存储的LUN COPY方式。

首先需要确定哪些LUN COPY盘在目标端会挂给哪些ESXI主机，需要收集的信息如下：

·目标ESXI IP

·逻辑卷名

·存储naa号（WWN号）

因为同一组中的外挂存储盘是通过共享的方式同时映射给同一群集内的多台ESXI主机的，所以只需要对同一群集中的一台ESXI进行操作即可。

命令收集信息如下：

esxcli storage vmfs extent list | sort $1 | awk '{if($5==1){print $1 "," $4}}'（注：

1 表明是外挂盘，

3 表明是内置盘）。

以下为同一群集内某台ESXI的外挂存储盘的逻辑卷名及存储号，如图3-2所示。

图3-2　ESXI的外挂存储盘逻辑卷名及存储号

3.虚拟网络信息收集

虚拟网络信息的收集，对于在新环境中每台ESXI需要分配多少个逻辑上联口、多少vSwitch和网段VLAN的定义至关重要。

因此我们编辑以下/tmp/getnetworkinfo.awk文件，用于收集现有ESXI的vSwitch Name、Uplinks（上联口）和Portgroups信息。

文件内容如下：

$1 ~ /Name:/ {print $1,$2}
$1 ~ /Uplinks:/ {print $0}
$1 ~/Portgroups:/ {print $0}

我们编辑并执行以下/tmp/getnetworkinfo.sh文件，该文件会将getnetworkinfo.awk文件作为执行的过滤参数。

文件内容如下：

esxcli network vswitch standard list | awk –

f /tmp/getnetworkinfo.awk

接下来就可以根据整理的各类信息完成后续逻辑迁移所需要的自动配置了。

[1]
以上信息均通过ESXCLI指令进行收集，当然也可以通过vCLI、PowerCLI等不同的管理方式获取相关信息。

3.2　利用VMware脚本简化虚拟化层的搬迁

我们需要在完成ESXI安装后执行如下虚拟化层的操作：

（1）为安装完成的ESXI开通ESXI Shell和远程SSH功能，确保可以批量管理ESXI。

（2）打开VMware底层的VMotion功能以保证虚拟机的灵活迁移。

（3）为ESXI建立vSwitch，并新建port group（也就是网络交换机的概念，这种技术也称之为VLAN），以保证注册的虚拟机节点能通过新建的虚拟网络与外部节点正常通信。

（4）批量挂载外挂存储，以确保虚拟机节点有可用的空间。

（5）注册虚拟机至vCenter。

（6）将源端vCenter的目录结构复制到目标端vCenter，并将虚拟机移动至指定的位置。

（7）在vCenter显示界面为每个虚拟机改名，并配置对应的port group。

（8）启动虚拟机。

3.2.1　通过脚本完成ESXI安装后的基础设置

（1）手工完成ESXI的安装后，请确认已经开启了ESXI Shell和SSH，如图3-3所示。

我们可利用以下命令来开启VMotion：

vim-cmd hostsvc/vmotion/vnic_set vmk0

VMotion功能可以实现虚拟机人工漂移或自动漂移。开启这个功能之后，注册在同一台ESXI的虚拟机在开启时就可以漂移至同一群集的所有物理机上，从而实现均衡运行。

图3-3　确认开启ESXI Shell和SSH

（2）网络配置（利用收集的虚拟化层的信息，根据规划完成ESXI的虚拟网络设置）。

由于公司网络规划采用了不同的区域，不同区域设备的上联端口并不统一，因此不适合使用DVS vSwitch，另外如果采用Host Profile绑定的方式，一旦宿主机配置有改动，就可能产生不合规提示。因此我决定根据规划，在不同的区域通过批量脚本的方式建立vSwitch和绑定port group

ESXI安装后默认生成vSwtich0作为管理用的交换机，我们需根据规划增加业务用的交换机，举例如下。

（1）添加名为vSwitch1的虚拟交换机，可使用如下命令：

esxcli network vswitch standard add -v vSwitch1

或者使用如下命令：

esxcfg-vswitch -a vSwitch1

注意

VMware公司发布vSphere 4时引入了ESXCLI命令集，在vSphere5时大大强化了ESXCLI指令的功能，使其逐步替代类似于esxcfg的旧指令集。

执行命令后，运行结果如图3-4所示。

（2）查看上联口情况，可使用如下命令：

esxcli network nic list

或者使用如下命令：

esxcfg-nics –

l

执行命令后，运行结果如图3-5所示。

（3）将对应的物理网卡绑定到vSwitch1（vmnic2/vmnic3，根据实际连线情况绑定到指定的物理网卡），可使用如下命令：

图3-4　确认ESXI网络配置增加了vSwitch1

图3-5　确认vmnic0和vmnic1为活动的上联端口

esxcli network vswitch standard uplink add -u vmnic2(vmnic3) -v vSwitch1

或者使用如下命令：

esxcfg-vswitch -L vmnic2(vmnic3) vSwitch1

执行命令后，运行结果如图3-6所示。

图3-6　确认vmnic2和vmnic3被绑定至vSwitch1，作为vSwitch1的上联端口

（4）激活已添加在vSwitch1内的物理网卡vmnic2/vmnic3（双活模式），可使用如下命令：

esxcli network vswitch standard policy failover set -a vmnic2,vmnic3 -v vSwitch1

执行命令后，运行结果如图3-7所示。

图3-7　确认vmnic2和vmnic3作为双活策略被激活

（5）添加名为vlan1到vlan20的port group到vSwitch1，可使用如下命令：

for i in `seq 1 20`;do esxcli network vswitch standard portgroup add -p vlan$i
-v vSwitch1;done

或者使用如下命令：

for i in `seq 1 20`;do esxcfg-vswitch -A vlan$i vSwitch1;done

执行命令后，运行结果如图3-8所示。

（6）将对应vlanid为1到20的vlan与对应的port group绑定，可使用如下命令：

for i in `seq 1 20`;do esxcli network vswitch standard portgroup set -p vlan$i
-v $i;done

在同一ESXI主机下不同vSwitch中的port group是不能使用同一名字命名的，因此以上ESXCLI的命令可直接针对指定的port group分配vlanid，而不必指明确切的vSwitch。

或者使用如下命令：

for i in `seq 1 20`;do esxcfg-vswitch -v $i -p vlan$i vSwitch1;done

为减少循环，提高效率可将以上语句合并如下：

for i in `seq 1 20`;do esxcfg-vswitch -A vlan$i vSwitch1;esxcfg-vswitch -v $i
-p vlan$i vSwitch1;done

执行命令后，运行结果如图3-9所示。

图3-8　确认建立了以vlan1到vlan20命名的port group

图3-9　确认绑定了vlan1到vlan20的ID到对应的port group

3.2.2　批量挂载数据盘

ESXI对于生成的每个VMFS DATASTORE都存在一个唯一的UUID，当对其进行LUN COPY操作时，原有的UUID也一并被复制到目标盘上。如果试图将目标盘挂载在与源盘同在一个的vCenter环境中时，系统会显示UUID已存在，从而导致挂载失败。只有通过重新签名更改目标盘的UUID，方可与源盘挂载在同一环境中。

因此我们事先在目标数据中心搭建了新的vCenter服务器。

下面来举例说明VMware主机批量挂载LUN COPY华为存储的具体步骤。

1.批量挂载LUN COPY的数据盘

（1）确认外挂盘数量：

esxcli storage core device list | grep "HUAWEI" | awk -F"(" '{print $2}' | awk
-F ")" '{print $1}' | grep naa. | wc -l
50

以上命令表示搜索ESXI宿主机上属于华为存储盘的个数，返回值为50，表明有50块外挂盘被挂载到宿主机物理设备上。我们可与存储管理人员确认挂载的数量是否一致，如有不一致的情况，可重新扫盘确认。

（2）收集挂载设备的标识符：

esxcli storage vmfs snapshot list | grep "Volume Name" | awk -F":" '{print
$2}' >/tmp/devices

以上命令将确认的LUN COPY属性盘的WWN号输出至/tmp/devices文件。当完成LUN COPY盘WWN号的收集后，可将devices文件与存储管理人员提供的存储盘WWN号进行比对，以此确认提取信息是否正确。

（3）批量挂载LUN COPY盘：

for i in `cat /tmp/devices` ;do esxcli storage vmfs snapshot mount -l $i;done

以上命令循环读取输出至/tmp/devices文件的外挂LUN COPY盘的WWN号，并挂载这些LUN COPY盘至ESXI宿主机。

最终结果如图3-10所示。

图3-10　显示批量挂载的LUN COPY盘

注意

存储挂载名称建议根据“存储品牌名_主机序列号_LUN号”命名。

以上操作只完成了对同一群集中某台主机的挂盘操作，我们只需要在同一群集的其他主机上通过执行扫描光纤链路的操作，即可让这些外挂盘被群集中的其他主机识别。具体步骤如下：配置→存储器→全部重新扫描。或者使用如下命令：

esxcli storage core adapter rescan

执行命令，运行结果如图3-11所示。

图3-11　显示同群集中其他主机批量挂载的LUN COPY盘

2.批量挂载新分配的盘

下面将举例说明为VMware主机批量挂载LUN COPY华为存储新盘的具体步骤
[1]

。

（1）确认新分配外挂盘的数量：

esxcli storage core device list | grep "HUAWEI" | awk -F"(" '{print $2}' | awk
-F ")" '{print $1}' | grep naa. | wc -l
50

以上命令表示搜索ESXI宿主机上属于华为存储盘的个数，返回值为50，表明有50块外挂盘被挂载到宿主机物理设备上。我们可与存储管理人员确认挂载的数量是否一致，如有不一致的情况，可重新扫盘确认。

（2）收集新分配外挂盘的标识符：

esxcli storage core device list | grep "HUAWEI" | awk -F"(" '{print $2}' | awk
-F ")" '{print $1}' | grep naa. >>/tmp/newdevice
naa.69017ac100aec92a7f8bfc7800000021
naa.69017ac100aec92a7f8bffa10000002a
naa.69017ac100aec92a7f8bfce200000022……

(省略中间的若干块盘

)
naa.69017ac100aec92a7f8bf85100000013
naa.69017ac100aec92a7f8bf54e0000000a
naa.69017ac100aec92a7f8bf6b30000000e

以上命令只将新分配盘的WWN号输出至/tmp/newdevice文件。当完成新分配外挂盘WWN号的收集后，可将newdevices文件与存储管理人员提供的存储盘WWN号进行比对，以此确认提取信息是否正确。

（3）通过列编辑软件，将新挂盘名称列加入/tmp/newdevice文件。接着就可通过列编辑软件在WWN号所属的列前增加一列，用于标记新分配盘的名称，格式如下：

HUAWEI_ 214????26_ 214????28_LUN01,naa.69017ac100aec92a7f8bfc7800000021
HUAWEI_ 214????26_ 214????28_LUN02,naa.69017ac100aec92a7f8bffa10000002a
HUAWEI_ 214????26_ 214????28_LUN03,naa.69017ac100aec92a7f8bfce200000022……

(省略中间的若干块盘

)
HUAWEI_ 214????26_ 214????28_LUN48,naa.69017ac100aec92a7f8bf85100000013
HUAWEI_ 214????26_ 214????28_LUN49,naa.69017ac100aec92a7f8bf54e0000000a
HUAWEI_ 214????26_ 214????28_LUN50,naa.69017ac100aec92a7f8bf6b30000000e

也可以远程连接至ESXI的CONSOLE界面，执行以下命令更新/tmp/newdevice文件的内容：

cat > /tmp/newdevice <<EOF
HUAWEI_ 214????26_ 214????28_LUN01,naa.69017ac100aec92a7f8bfc7800000021
HUAWEI_ 214????26_ 214????28_LUN02,naa.69017ac100aec92a7f8bffa10000002a
HUAWEI_ 214????26_ 214????28_LUN03,naa.69017ac100aec92a7f8bfce200000022……

(省略中间的若干块盘

)
HUAWEI_ 214????26_ 214????28_LUN48,naa.69017ac100aec92a7f8bf85100000013
HUAWEI_ 214????26_ 214????28_LUN49,naa.69017ac100aec92a7f8bf54e0000000a
HUAWEI_ 214????26_ 214????28_LUN50,naa.69017ac100aec92a7f8bf6b30000000e
EOF

通过以上操作，我们定义了新分配盘的盘名和WWN号，作为后续新加盘命令的参数。

（4）批量为新增盘分区（以1TB和2TB的格式为例）：

for i in `cat /tmp/newdevice | awk -F"," '{print $2}'`; do partedUtil setptbl
/vmfs/devices/disks/$i gpt "1 2048 2147483614
AA31E02A400F11DB9590000C2911D1B8 0"; done (按

1TB盘的格式

)
for i in `cat /tmp/newdevice | awk -F"," '{print $2}'`; do partedUtil setptbl
/vmfs/devices/disks/$i gpt "1 2048 4294961684
AA31E02A400F11DB9590000C2911D1B8 0"; done (按

2TB盘的格式

)

以上命令分别按1TB和2TB的参数，对新盘进行了分区。

（5）批量新建VMFS5文件系统并自动挂盘：

for i in `cat /tmp/newdevice` ;do name=`echo $i | awk -F"," '{print $1}'`
vid=`echo $i | awk -F"," '{print $2}'` ; vmkfstools -C vmfs5 -S $name -b 1m
/vmfs/devices/disks/$vid:1 ;done

以上命令对新盘进行了格式化操作，将其格式化为VMFS5的文件系统后，系统自动进行了挂盘操作。

如图3-12所示为正在挂盘操作的示例图。

图3-12　正在执行新盘的挂载

最终结果如图3-13所示：

图3-13　显示批量挂载的新分配盘

注意

存储挂载的名称请以“存储品牌名_主机序列号_LUN号”命名。

以上操作只完成了对同一群集中某台主机的挂盘操作，我们只需要在同一群集的其他主机上通过执行扫描光纤链路的操作，即可让这些外挂盘被群集中的其他主机识别。具体步骤如下：配置→存储器→全部重新扫描。或者使用如下命令：

esxcli storage core adapter rescan

执行命令，运行结果如图3-14所示。

图3-14　显示同群集中其他主机批量挂载的新分配盘

[1]
此步骤只针对新采购资源，如需对已使用的资源添加新分配外挂盘，则需要在ESXI上利用diff进行比对。

3.2.3　批量注册虚拟机

3.2.2节中，我们已将50块LUN COPY的外挂盘挂给了一组刀片式服务器，接下来我们需要将所涉及的该批次的所有虚拟机注册进vCenter Server。

对群集中的第一台ESXI执行以下脚本，将所有虚拟机的配置文件.vmx导出到一个文件中。

find /vmfs/volumes/ -name *.vmx >/tmp/vmlist

为避免开机时因VM UUID重复而产生如图3-15所示的交互式问答的提示，我们可以执行如下命令，批量删除VMX中的uuid.bios和uuid.location，使虚拟机重新产生新的UUID：

for i in `cat /tmp/vmlist` ; do sed -i '/^uuid.bios/d' $i ; done
for i in `cat /tmp/vmlist` ; do sed -i '/^uuid.location/d' $i ; done

图3-15　因VM UUID重复而产生的提示

为避免虚拟机注册后遗留下如图3-16所示的“影子”网段信息，我们可执行以下命令，批量删除VMX中的ethernet0.networkName，使虚拟机去除老旧的网络信息：

for i in `cat /tmp/vmlist` ; do sed -i '/^ethernet0.networkName/d' $i ; done

图3-16　“影子”网段情况

以上是对VMX配置文件的信息修改，必须在将虚拟机注册进vCenter之前完成。如果先将虚拟机注册进vCenter，然后再执行以上操作，则不会有任何效果。

运行如下命令，注册虚拟机：

IFS=$(echo -en "\n\b")
for i in `cat /tmp/vmlist` ; do vim-cmd solo/registervm $i ; done

在注册的同时，我们会看见命令行窗口的数字正在逐渐增加，该数字表明为注册入vCenter Server的虚拟机分配的VMID，图3-17显示有1004个虚拟机完成了注册：

图3-17　显示1004个虚拟机注册成功

3.2.4　vCenter目录结构的调整

我们需要将源vCenter的目录结构原封不动地挪移到目标vCenter中，以确保新注册的虚拟机位置不变，因此我们需要导出源vCenter的目录结构，并导入至目标vCenter中。

（1）从源vCenter中导出目录结构：

New-VIProperty -Name 'BlueFolderPath' -ObjectType 'VirtualMachine' -Value {
param($vm)
function Get-ParentName{
param($object)
if($object.Folder){
$blue = Get-ParentName $object.Folder #获取路径

$name = $object.Folder.Name #获取

vm_name
}
elseif($object.Parent -and $object.Parent.GetType().Name -like
"Folder*"){
$blue = Get-ParentName $object.Parent
$name = $object.Parent.Name
}
elseif($object.ParentFolder){
$blue = Get-ParentName $object.ParentFolder
$name = $object.ParentFolder.Name
}
if("vm","Datacenters" -notcontains $name){
$blue + "/" + $name
}
else{
$blue
}
}
(Get-ParentName $vm).Remove(0,1)
} -Force | Out-Null
$dcName = "zbvc00-vcenter"
Get-VM -Location (Get-Datacenter -Name $dcName) | Select Name,BlueFolderPath
| Export-Csv "C:\vm-folder.csv" -NoTypeInformation -UseCulture -encoding utf8

（2）将目录结构导入目标vCenter中（并将虚拟机移动至指定目录路径下）：

$newDatacenter = "zbvc00-vcenter"
$startFolder = get-Folder -Name vm -Location (Get-Folder -Name vm -Location
(Get-Datacenter -Name $newDatacenter))
Import-Csv "C:\vm-folder.csv" -UseCulture | %{
$location = $startFolder
$_.BlueFolderPath.TrimStart('/').Split('/') | %{
$tgtFolder = Get-Folder -Name vm -Location $location -ErrorAction
SilentlyContinue
if(!$tgtFolder){
$location = New-Folder -Name $_ -Location $location
}
else{
$location = $tgtFolder
}
}
$vm = Get-VM -Name $_.Name -ErrorAction SilentlyContinue
if($vm){
Move-VM -VM $vm -Destination $location -Confirm:$false
}
}

注意

以上脚本截取自VMware社区论坛，并根据实际情况修改而成。

3.2.5　批量更改虚拟机名称及port group

为了批量更改虚拟机名称及虚拟机所属的port group（VLAN），我们需要建立一个参数文件，参数文件的内容如图3-18所示。

图3-18　vCneter中为虚拟机改名和port group的文件格式

我们将参数文件保存为c：\zbportgroup.csv，由于主机名采用的是中文字符，因此保存参数文件时编码务必采用UTF-8字符集，以确保PowerCLI中可以正常识别。

接下来登录PowerCLI控制机，连接入指定的vCenter Server，如图3-19所示。

图3-19　使用Connect-VIServer登录PowerCLI管理界面

导入参数文件：

$vmlist=Import-CSV "c:\zbportgroup.csv"

为虚拟机重命名：

FOREACH ($vm in $vmlist)
{ Get-VM $($vm.oldname) | Set-vm -name $($vm.NEWNAME) -confirm:$false |
Out-File –

Append changname.txt}

为虚拟机重新设置port group：

FOREACH ($vm in $vmlist)
{Get-vm $($vm.NEWNAME) | get-networkadapter | set-networkadapter -networkname
$($vm.portgroup) -confirm:$false | Out-File –

Append changportgroup.txt }

为减少循环以提高效率，可将以上语句合并如下：

FOREACH ($vm in $vmlist)
{(Get-VM $($vm.oldname) | Set-vm -name $($vm.NEWNAME) -confirm:$false) -and
(Get-vm $($vm.NEWNAME) | get-networkadapter | set-networkadapter -networkname
$($vm.portgroup) -confirm:$false)}

正在执行批量更新port group的虚拟机，如图3-20所示。

图3-20　执行更改VM name和port group的操作

3.2.6　批量设置虚拟机版本和CPU、内存保留值

因搬迁的环境中虚拟机版本不统一，既有version 4的，又有version 7和8的虚拟机，为保证虚拟机的版本统一，故需将虚拟机版本调整为version 8，同时为保证群集中所有的虚拟机都能顺利开启，还需要将CPU和内存的预留资源调整为0，命令如下：

foreach ($vm in $vmlist)
{
set-vm $vm.oldname -version v8 -confirm:$false;
(Get-VM $vm.oldname | Get-VMResourceConfiguration | where {$_.CPULimitMhz -ne
'0'} | Set-VMResourceConfiguration -cpuReservationMhz 0);
(Get-VM $vm.oldname | Get-VMResourceConfiguration | where
{$_.MemReservationMB -ne '0'} | Set-VMResourceConfiguration -MemReservationMB
0)
}批量启动虚拟机

FOREACH ($vm in $vmlist)
{start-vm -VM $vm }

3.3　利用批处理与Shell脚本简化逻辑节点的搬迁

3.3.1　逻辑节点切换脚本的思路

众所周知，对于服务器的搬迁，不只会涉及物理层面的设备搬迁。在物理设备搬迁到新机房后，往往还需要对虚拟机中的多种参数进行相关调整。

如果管理的服务器台数比较少的话，则可以采用手工设置的方式逐一更改参数。但是，如果手中管理的Windows和Linux虚拟机有数千台的话，那么这些繁琐的参数调整一定会让你头疼不已。其中需要调整的参数如下：

·IP

·GATEWAY

·DNS

·WSUS

·NTP

·HOSTS

也许大家要问，像DNS、WSUS、NTP这类服务器地址是有固定IP的，设置起来应该相对容易，但像IP地址和GATEWAY怎么办呢？下面就来讲述下如何编写这个脚本。

服务器条件如下：

·搬迁环境涉及两类资源，一类为容灾环境节点、一类为研发测试环境节点。

·每台服务器都利用的是内网网段IP，通过NAT转换到公网。

·每台服务器都只有一个本地连接的网口。

·所有网段的网关地址的最后一位为254（基于C类网段进行分配）。

由于新注册到目标数据中心的所有节点，在开启后仍然保持了源端LUN COPY节点的所有参数。而且并非所有节点都安装了适合虚拟机版本的VMware-Tools组件，因此无法统一通过PowerCLI的invoke-vmscript命令直接对虚拟机发送指令进行操作。结合工作中的实际需求，我考虑在虚拟机未断开复制之前对源端主机预先上传切换用的脚本，搬迁至目标端之后，人工执行切换脚本并选取对应的环境，系统再根据人工反馈的环境值，自动更改对应的参数来完成虚拟机内所有参数的切换工作。

每台服务器都有一个搬迁前的在用IP地址，另外根据规划已知要在搬迁后为每台服务器分配一个新的IP地址。因此可根据项目规划的要求制作一张新旧IP切换对照表，俗称字典文件，该对照表包含如下内容：

第一列为原有的旧IP，第二列为搬迁后的新IP，第三列为搬迁后需要调整的新主机名（如无需调整主机名，则第三列可忽略。Linux的字典文件用TAB作为分隔符，Windows的字典文件用空格作为分隔符），该文件将最终随同切换用脚本文件在搬迁之前一并上传至逻辑节点的指定目录中待用。

以下表3-2仅为参考示例。

表3-2　新旧IP切换对照表

获取目前主机上的IP地址情况，然后与以上字典文件中的现有IP列的IP地址进行比对，如获取到对应的现有IP，则再提取与现有IP同一行中对应的新IP和新主机名。并将新IP的值自动赋予Windows的网卡，将新的主机名赋予操作系统（如果有需要更改主机名的情况），这样就完成了IP的自动替换和主机名更改的工作。

3.3.2　利用批处理脚本简化Windows逻辑节点的搬迁

下面是基于Windows批处理的脚本，主要实现功能为判断操作系统（Win2003/Win2008）和应用环境（研发和容灾），执行不同的批处理指令，将字典文件中的旧IP切换为对应的新IP：

@echo ++++++++++++++++++++++++++++++++++++++++++++++++
@echo +欢迎使用

Win2003/Win2008切换脚本，请根据切换环境输入指定的参数！

+
@echo ++++++++++++++++++++++++++++++++++++++++++++++++
@choice /C:123 /N /M "1:研发测试环境

2:容灾环境

3:退出

"
if errorlevel 3 goto end
if errorlevel 2 goto zb
if errorlevel 1 goto kfcs
# 以上语句显示欢迎页面，同时还显示应用环境菜单，根据操作的选取，可跳转到不同的环境

:kfcs #（设置开发测试环境的所有参数）

set adapter= #（初始化网卡变量）

set oldip= #（初始化旧

IP变量）

set newip= #（初始化新

IP变量）

set gateway= #（初始化新网关变量）

set dns1= #（初始化主

DNS变量）

set dns2= #（初始化辅

DNS变量）

set mask=255.255.255.0 #（设置子网掩码变量为

255.255.255.0）

set wsusserver= #（初始化

WSUS服务器变量）

set ntpserver= #（初始化

NTP服务器变量）

set wsusserver=http://192.168.127.198 #（为

WSUS服务器赋予指定

IP）

set ntpserver="192.168.127.103" #（为

NTP服务器赋予指定

IP）

goto ver
:zb #（设置灾备环境的所有参数）

set adapter= #（初始化网卡变量）

set oldip= #（初始化旧

IP变量）

set newip= #（初始化新

IP变量）

set gateway= #（初始化新网关变量）

set dns1=192.168.127.1 #（为

DNS1服务器赋予指定

IP）

set dns2=192.168.127.200 #（为

DNS2服务器赋予指定

IP）

set mask=255.255.255.0 #（设置子网掩码变量为

255.255.255.0）

set wsusserver= #（初始化

WSUS服务器变量）

set ntpserver= #（初始化

NTP服务器变量）

set wsusserver=http://192.168.16.54 #（为

WSUS服务器赋予指定

IP）

set ntpserver="192.168.16.54" #（为

NTP服务器赋予指定

IP）

goto ver
:ver #（判断

Windows版本是

2003还是

2008）

ver | find /i "6.1." > NUL
if %errorlevel% equ 0 (goto win2008)
ver | find /i "5.2." > NUL
if %errorlevel% equ 0 (goto win2003)
:win2008 # （如果是

Win2008，则执行如下语句）

ipconfig | findstr /i "以太网适配器

">c:\tmp\ipchange\adapter.txt
for /f "tokens=2*" %%i in (c:\tmp\ipchange\adapter.txt) do
@echo %%i %%j>c:\tmp\ipchange\adapter.txt
for /f "tokens=1 delims=:" %%i in (c:\tmp\ipchange\adapter.txt) do (
set adapter=%%i
echo %%i>c:\tmp\ipchange\adapter.txt
)
# 以上语句保存以太网适配器名称

ipconfig | findstr /i "IPv4">c:\tmp\ipchange\oldip.txt
for /f "tokens=2 delims=:" %%i in (c:\tmp\ipchange\oldip.txt) do
@echo %%i>c:\tmp\ipchange\oldip.txt
for /f "tokens=*" %%i in (c:\tmp\ipchange\oldip.txt) do
@echo %%i>c:\tmp\ipchange\oldip.txt
for /f %%i in (c:\tmp\ipchange\oldip.txt) do set oldip=%%i
# 以上语句保存旧

IP
rem 获取

DNS并保存

ipconfig /all | findstr /C:"DNS Servers" /C:"DNS 服务器

">c:\tmp\ipchange\olddns.txt
for /f "tokens=2 delims=:" %%i in (c:\tmp\ipchange\olddns.txt) do @echo %%i
| findstr "^[0-9]*.[0-9]*.[0-9]*.[0-9]">c:\tmp\ipchange\olddns.txt
for /f "tokens=*" %%i in (c:\tmp\ipchange\olddns.txt) do
@echo %%i>c:\tmp\ipchange\olddns.txt
# 以上语句保存旧

DNS服务器，以备更换后有旧

DNS的回溯

findstr /i "%oldip%"
c:\tmp\ipchange\ipcheck.txt>c:\tmp\ipchange\oldtonewip.txt
for /f "tokens=2" %%i in (c:\tmp\ipchange\oldtonewip.txt) do
@echo %%i>c:\tmp\ipchange\newip.txt
for /f %%i in (c:\tmp\ipchange\newip.txt) do set newip=%%i
# 以上语句查找字典文件

ipcheck.txt中是否存在旧

IP的条目，如果存在则提取与旧

IP对应

的新

IP值，并赋给

newip变量

for /f "tokens=1-3 delims=." %%i in (c:\tmp\ipchange\newip.txt) do
echo %%i.%%j.%%k.254>c:\tmp\ipchange\gatewayip.txt
for /f %%i in (c:\tmp\ipchange\gatewayip.txt) do set gatewayip=%%i
# 以上语句提取

newip变量中

以

.为分隔符的前三位，最后一位设置为

254，作为

GATEWAY的新值，并赋给

gatewayip变量

rem 备份

hosts文件，并根据字典文件的定义，替换

hosts内关联节点对应的

IP
setlocal enabledelayedexpansion
for /f "tokens=*" %%i in (C:\WINDOWS\system32\drivers\etc\hosts) do (set
var=%%i
set "var=!var:%oldip%=%newip%!"
echo !var! >> C:\WINDOWS\system32\drivers\etc\hosts.new
)
ren C:\WINDOWS\system32\drivers\etc\hosts hosts.old
copy C:\WINDOWS\system32\drivers\etc\hosts.old C:\tmp\ipchange
ren C:\WINDOWS\system32\drivers\etc\hosts.new hosts
endlocal
# 以上语句备份

c:\windows\system32\drivers\etc\hosts文件，并根据字典文件的定义替换

hosts文件内关联节点对应的

IP与主机名的映射关系

rem 自动设置新

IP和网关

#（

rem为脚本中的注释语句）

rem 自动设置新

DNS #（

rem为脚本中的注释语句）

netsh interface ipv4 set address "%adapter%" static %newip% %mask%
gateway=%gatewayip% 1
netsh interface ipv4 del dnsservers name="%adapter%" all
netsh interface ipv4 set dnsservers name="%adapter%" source=static %dns1%
register=primary validate=no
# netsh interface ipv4 add dnsservers "%adapter%" %dns2% index=2 validate=no
# 以上语句将先前几个步骤获取到的

adapter、

newip、

gateway、

dns1、

dns2的变量，作为设置

的参数直接为

Windows系统设置新

IP、新网关和新

DNS
goto envir # （跳转到

envir，调整其他附加环境）

:win2003 # （如果是

Win2003，则执行如下语句）

ipconfig | findstr /i "Ethernet adapter">c:\tmp\ipchange\adapter.txt
for /f "tokens=3* delims= " %%i in (c:\tmp\ipchange\adapter.txt) do
@echo %%i %%j>c:\tmp\ipchange\adapter.txt
for /f "tokens=1 delims=:" %%i in (c:\tmp\ipchange\adapter.txt) do (
set adapter=%%i
echo %%i>c:\tmp\ipchange\adapter.txt
)
# 以上语句保存以太网适配器的名称

ipconfig | findstr /i "IP Address">c:\tmp\ipchange\oldip.txt
for /f "tokens=2 delims=:" %%i in (c:\tmp\ipchange\oldip.txt) do
@echo %%i>c:\tmp\ipchange\oldip.txt
for /f "tokens=*" %%i in (c:\tmp\ipchange\oldip.txt) do
@echo %%i>c:\tmp\ipchange\oldip.txt
for /f %%i in (c:\tmp\ipchange\oldip.txt) do set oldip=%%i
# 以上语句保存旧

IP
rem 获取

DNS并保存

ipconfig /all | findstr /C:"DNS Servers" /C:"DNS 服务器

">c:\tmp\ipchange\olddns.txt
for /f "tokens=2 delims=:" %%i in (c:\tmp\ipchange\olddns.txt) do @echo %%i
| findstr "^[0-9]*.[0-9]*.[0-9]*.[0-9]">c:\tmp\ipchange\olddns.txt
for /f "tokens=*" %%i in (c:\tmp\ipchange\olddns.txt) do
@echo %%i>c:\tmp\ipchange\olddns.txt
# 以上语句保存旧

DNS服务器，以备更换后有旧

DNS的回溯

findstr /i "%oldip%\>"
c:\tmp\ipchange\ipcheck.txt>c:\tmp\ipchange\oldtonewip.txt
for /f "tokens=2" %%i in (c:\tmp\ipchange\oldtonewip.txt) do
@echo %%i>c:\tmp\ipchange\newip.txt
for /f %%i in (c:\tmp\ipchange\newip.txt) do set newip=%%i
# 以上语句查找字典文件

ipcheck.txt中是否存在旧

IP的条目，如果存在则提取与旧

IP对应

的新

IP值，并赋给

newip变量

for /f "tokens=1-3 delims=." %%i in (c:\tmp\ipchange\newip.txt) do
echo %%i.%%j.%%k.254>c:\tmp\ipchange\gatewayip.txt
for /f %%i in (c:\tmp\ipchange\gatewayip.txt) do set gatewayip=%%i
# 以上语句提取

newip变量中

以

.为分隔符的前三位，最后一位设置为

254，作为

GATEWAY的新值，并赋给

gatewayip变量

rem 备份并替换

hosts文件

setlocal enabledelayedexpansion
for /f "tokens=*" %%i in (C:\WINDOWS\system32\drivers\etc\hosts) do (set
var=%%i
set "var=!var:%oldip%=%newip%!"
echo !var! >> C:\WINDOWS\system32\drivers\etc\hosts.new
)
ren C:\WINDOWS\system32\drivers\etc\hosts hosts.old
copy C:\WINDOWS\system32\drivers\etc\hosts.old C:\tmp\ipchange
ren C:\WINDOWS\system32\drivers\etc\hosts.new hosts
endlocal
# 以上语句备份

c:\windows\system32\drivers\etc\hosts文件，并根据字典文件的定义替换

hosts文件内关联节点对应的

IP与主机名的映射关系

rem 自动设置新

IP和网关

rem 自动设置新

DNS
netsh interface ip set address name="%adapter%" source=static %newip% %mask%
gateway=%gatewayip% auto
netsh interface ip del dns name="%adapter%" all
netsh interface ip set dns name="%adapter%" source=static %dns1%
register=primary
#netsh interface ip add dns "%adapter%" %dns2% index=2
# 以上语句将先前几个步骤获取到的

adapter、

newip、

gateway、

dns1、

dns2的变量，作为设置

的参数直接为

Windows系统设置新

IP、新网关和新

DNS
goto envir # （跳转到

envir，调整其他附加环境）

:envir #（执行调整其他附加环境的语句）

rem ==========================================================================
rem 2.设置

Patrol参数

echo Windows Registry Editor Version 5.00>c:\tmp\ipchange\patrolagent.reg
echo.>>c:\tmp\ipchange\patrolagent.reg
echo
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\PatrolAgent\Paramete
rs]>>c:\tmp\ipchange\patrolagent.reg
echo "Port"=dword:00000c6d>>c:\tmp\ipchange\patrolagent.reg
echo "ID"="%newip%">>c:\tmp\ipchange\patrolagent.reg
regedit /s c:\tmp\ipchange\patrolagent.reg
# 以上语句将新

IP写入

PatrolAgent的注册表参数，确保

Patrol监控软件正常工作

rem=======================================================================
rem 3.设置

WSUS服务器参数

echo Windows Registry Editor Version 5.00>c:\tmp\ipchange\wsus.reg
echo.>>c:\tmp\ipchange\wsus.reg
echo
[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\windows\WindowsUpdate]>>c:
\tmp\ipchange\wsus.reg
echo "WUServer"=%wsusserver%>>c:\tmp\ipchange\wsus.reg
echo "WUStatusServer"=%wsusserver%>>c:\tmp\ipchange\wsus.reg
echo.>>c:\tmp\ipchange\wsus.reg
echo
[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\windows\WindowsUpdate\AU]>
>c:\tmp\ipchange\wsus.reg
echo "NoAutoUpdate"=dword:00000000>>c:\tmp\ipchange\wsus.reg
echo "AUOptions"=dword:00000003>>c:\tmp\ipchange\wsus.reg
echo "ScheduledInstallDay"=dword:00000000>>c:\tmp\ipchange\wsus.reg
echo "ScheduledInstallTime"=dword:00000017>>c:\tmp\ipchange\wsus.reg
echo "UseWUServer"=dword:00000001>>c:\tmp\ipchange\wsus.reg
regedit /s c:\tmp\ipchange\wsus.reg
# 以上语句将

WSUS的

IP及配置参数写入注册表，确保

Windows客户端能从

WSUS正确获取

更新补丁

rem=======================================================================
rem 4.设置

NTP服务器参数

echo Windows Registry Editor Version 5.00>c:\tmp\ipchange\ntp.reg
echo.>>c:\tmp\ipchange\ntp.reg
echo
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\DateTime\Ser
vers]>>c:\tmp\ipchange\ntp.reg
echo @="0">>c:\tmp\ipchange\ntp.reg
echo "0"=%ntpserver%>>c:\tmp\ipchange\ntp.reg
echo
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\W32Time\TimeProvider
s\NtpClient]>>c:\tmp\ipchange\ntp.reg
echo "Enabled"=dword:00000001>>c:\tmp\ipchange\ntp.reg
echo "SpecialPollInterval"=dword:2a300>>c:\tmp\ipchange\ntp.reg
regedit /s c:\tmp\ipchange\ntp.reg
w32tm /config /manualpeerlist:%ntpserver% /syncfromflags:manual
/reliable:yes
w32tm /config /update
net stop w32time
net start w32time
w32tm /resync
sc config w32time start= auto
# 以上语句将

NTP的

IP及配置参数写入注册表，确保

Windows客户端定期与

NTP服务器同步

rem=======================================================================
rem 5.设置

UAC禁用

reg add
"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Sys
tem" /v "LocalAccountTokenFilterPolicy" /t REG_DWORD /d "1" /f
# 以上语句设置

Windows的

UAC功能

rem=======================================================================
rem 6.设置

Administrator(管理员

)及

Patrol(监控平台用户

)的密码

#net user administrator ???????? ????????请用实际密码代替

#net user patrol ******** ********请用实际密码代替

# 以上语句用于初始化重置

Administrator和

Patrol用户密码

rem=======================================================================
rem 7.关闭计算机

#shutdown /t 3 /s （

shutdown前标注了

#，所以此句只标注不执行）

:end

3.3.3　利用Shell脚本简化Linux逻辑节点的搬迁

下面是基于Linux Shell的脚本，主要实现功能为判断应用环境（研发和容灾），执行不同的Shell指令，将字典文件中的旧IP切换为对应的新IP：

#!/bin/bash
cd /tmp/ipchange
#dos2unix ipchange.sh
#dos2unix ipcheck.txt
echo ++++++++++++++++++++++++++++++++++++++++++++++++++++
echo + Welcome use redhat linux change patameter script +
echo ++++++++++++++++++++++++++++++++++++++++++++++++++++
read -p "Disaster Recovery Environment(R)|Develop Testing
Environment(T)|Exit(E):" evar
# 以上语句显示欢迎页面，同时还显示应用环境菜单，根据操作的选取，可跳转到不同的环境

zb_ntp1=192.168.16.105 #（为

NTP1服务器赋予指定

IP）

zb_ntp2=192.168.20.73 #（为

NTP2服务器赋予指定

IP）

zb_dns1=192.168.16.105 #（为主

DNS服务器赋予指定

IP）

zb_dns2=192.168.16.106 #（为辅

DNS服务器赋予指定

IP）

zb_yum= #（初始化

YUM服务器变量）

# 以上语句设置灾备环境的所有参数

kfcs_ntp1=192.168.127.103 #（为

NTP1服务器赋予指定

IP）

kfcs_ntp2= #（初始化

NTP2服务器变量）

kfcs_dns1=192.168.127.105 #（为主

DNS服务器赋予指定

IP）

kfcs_dns2=192.168.127.106 #（为辅

DNS服务器赋予指定

IP）

kfcs_yum= #（初始化

YUM服务器变量）

# 以上语句设置开发环境的所有参数

dr(){ #（设置灾备环境的主程序）

################################
# Disaster Recovery Environment #
################################
# 保存旧

IP信息

grep -i ipaddr /etc/sysconfig/network-scripts/ifcfg-eth0>/tmp/oldip.txt
awk -F= '{print $2}' /tmp/oldip.txt>/tmp/ipchange/oldip.txt
rm -f /tmp/oldip.txt
oldip=`awk '{print $0}' /tmp/ipchange/oldip.txt`
echo oldip=$oldip
echo
# 保存旧网关信息

grep -i gateway
/etc/sysconfig/network-scripts/ifcfg-eth0>/tmp/oldgatewayip.txt
awk -F= '{print $2}' /tmp/oldgatewayip.txt>/tmp/ipchange/oldgatewayip.txt
rm -f /tmp/oldgatewayip.txt
oldgatewayip=`awk '{print $0}' /tmp/ipchange/oldgatewayip.txt`
echo oldgatewayip=$oldgatewayip
echo
# 从字典文件中截取新旧

IP比对值，提取新

IP
grep "$oldip\>" /tmp/ipchange/ipcheck.txt>/tmp/oldtonewip.txt
awk '{print $2}' /tmp/oldtonewip.txt>/tmp/ipchange/newip.txt
mv /tmp/oldtonewip.txt /tmp/ipchange/oldtonewip.txt
newip=`awk '{print $1}' /tmp/ipchange/newip.txt`
echo newip=$newip
echo
# 在字典文件中进行比对，如果找不到

IP对应关系则退出

count=`grep "$oldip\>" /tmp/ipchange/ipcheck.txt |wc –

l`
if [ $count -eq 0 ];then
echo "no ip matched in ipcheck.txt, will exit in 5 seconds!!!"
sleep 5
exit 100
fi
grep "$oldip\>" /tmp/ipchange/ipcheck.txt>/tmp/oldtonewip.txt
awk '{print $2}' /tmp/oldtonewip.txt>/tmp/ipchange/newip.txt
mv /tmp/oldtonewip.txt /tmp/ipchange/oldtonewip.txt
newip=`awk '{print $1}' /tmp/ipchange/newip.txt`
echo newip=$newip
echo
# 定义新网关

awk -F. '{print $1"."$2"."$3"."254}'
/tmp/ipchange/newip.txt>/tmp/ipchange/newgatewayip.txt
newgatewayip=`awk '{print $1}' /tmp/ipchange/newgatewayip.txt`
echo newgatewayip=$newgatewayip
echo
#保存网卡的旧配置，并设置新定义的配置信息

cd /etc/sysconfig/network-scripts/
cp ifcfg-eth0 /tmp/ipchange/ifcfg-eth0.old
sed -e "s/$oldip/$newip/g" ifcfg-eth0>ifcfg-eth0.tmp1
sed -e "s/$oldgatewayip/$newgatewayip/g" ifcfg-eth0.tmp1>ifcfg-eth0.tmp2
sed -e "/NETMASK/d" ifcfg-eth0.tmp2>ifcfg-eth0.tmp1
sed -e "/IPADDR/a\\NETMASK=255.255.255.0" ifcfg-eth0.tmp1>ifcfg-eth0.tmp2
rm -f ifcfg-eth0
cp ifcfg-eth0.tmp2 /tmp/ipchange/ifcfg-eth0.new
cp ifcfg-eth0.tmp2 ifcfg-eth0
rm -f ifcfg-eth0.tmp1
rm -f ifcfg-eth0.tmp2
cat /etc/sysconfig/network-scripts/ifcfg-eth0
#################################################################
# 备份

hosts文件

cp -f /etc/hosts /tmp/ipchange/oldhosts.txt
# 根据字典定义的文件，替换

hosts内关联节点对应的

IP
for ip in $(cat /etc/hosts |egrep ^[0-9] |awk '{print $1}'|grep -v 127.0.0.1)
do
iprule=$(grep $ip /tmp/ipchange/ipcheck.txt |head -n 1)
if [ "$iprule" != "" ]
then
shipsec=$(echo $iprule |awk '{print $1}' |awk -F'.' '{print $1"."$2"."$3}')
cdipsec=$(echo $iprule |awk '{print $2}' |awk -F'.' '{print $1"."$2"."$3}')
echo "shipsec:$shipsec"
echo "cdipsec:$cdipsec"
sed -i "s/^$shipsec/$cdipsec/g" /etc/hosts
else
echo "WARNING: ip $ip not matched in ipcheck.txt"
fi
done
#################################################################
# 设置

NTP信息（通过

crontab定时更新）

crontab -l>/tmp/crontab.old
if [ -f /var/spool/cron/root ]
then
sed -i '/ntpdate/d' /var/spool/cron/root
fi
echo "*/5 * * * * /sbin/ntpdate $zb_ntp1">>/var/spool/cron/root
echo "*/5 * * * * /sbin/ntpdate $zb_ntp2">>/var/spool/cron/root
echo "tinker panic 0">/etc/ntp.conf
echo "restrict 127.0.0.1">>/etc/ntp.conf
echo "restrict default kod nomodify notrap">>/etc/ntp.conf
echo "server $zb_ntp1">>/etc/ntp.conf
echo "server $zb_ntp2">>/etc/ntp.conf
echo "keys /etc/ntp/keys">>/etc/ntp.conf
echo "driftfile /var/lib/ntp/drift">>/etc/ntp.conf
echo "$zb_ntp1">/etc/ntp/ntpservers
echo "$zb_ntp2">>/etc/ntp/ntpservers
echo "clock.redhat.com">>/etc/ntp/ntpservers
echo "clock2.redhat.com">>/etc/ntp/ntpservers
echo "$zb_ntp1">/etc/ntp/step-tickers
echo "$zb_ntp2">>/etc/ntp/step-tickers
echo "clock.redhat.com">>/etc/ntp/step-tickers
echo "clock2.redhat.com">>/etc/ntp/step-tickers
ntp=`cat /etc/ntp/ntpservers | head -1`
crontab –

l
##################################################################
# 设置

DNS信息

cp /etc/resolv.conf /tmp/ipchange/resolv.conf.old
echo domain cpic.com.cn>/etc/resolv.conf
echo nameserver $zb_dns1>>/etc/resolv.conf
echo nameserver $zb_dns2>>/etc/resolv.conf
dns=`sed -e '/nameserver/g' /etc/resolv.conf | awk -F " " '{print $2}'
/etc/resolv.conf | head -2 | tail -1`
###################################################################
# 设置监控平台参数

mv /home/patrol/startagent.sh /tmp/startagent.sh.old
echo su - patrol -c \"/home/patrol/Patrol3/PatrolAgent -id
$newip\">/home/patrol/startagent.sh
chown patrol:patrol /home/patrol/startagent.sh
chmod 774 /home/patrol/startagent.sh
####################################################################
# 重置关键用户密码

#echo ???????? | passwd root --stdin ????????请用实际密码代替

#echo ******** | passwd patrol --stdin ********请用实际密码代替

###################################################################
#shutdown computer
#shutdown -h now （

shutdown前标注了

#，所以此句只标注不执行）

service network restart
service ntpd stop
chkconfig ntpd off
echo "New IP is $newip"
echo "New primary DNS is $dns"
echo "New primary NTP is $ntp"
echo "Change is successful!"
}
# 以上语句在设置完所有配置信息后，需要重启网络服务，以确保新

IP能够正常工作。

dt(){ #（设置开发环境的主程序）

####################################################
# 保存旧

IP信息

grep -i ipaddr /etc/sysconfig/network-scripts/ifcfg-eth0>/tmp/oldip.txt
awk -F= '{print $2}' /tmp/oldip.txt>/tmp/ipchange/oldip.txt
rm -f /tmp/oldip.txt
oldip=`awk '{print $0}' /tmp/ipchange/oldip.txt`
echo oldip=$oldip
echo
# 保存旧网关信息

grep -i gateway
/etc/sysconfig/network-scripts/ifcfg-eth0>/tmp/oldgatewayip.txt
awk -F= '{print $2}' /tmp/oldgatewayip.txt>/tmp/ipchange/oldgatewayip.txt
rm -f /tmp/oldgatewayip.txt
oldgatewayip=`awk '{print $0}' /tmp/ipchange/oldgatewayip.txt`
echo oldgatewayip=$oldgatewayip
echo
# 从字典中文件中截取新旧

IP比对值，提取新

IP
grep "$oldip\>" /tmp/ipchange/ipcheck.txt>/tmp/oldtonewip.txt
awk '{print $2}' /tmp/oldtonewip.txt>/tmp/ipchange/newip.txt
mv /tmp/oldtonewip.txt /tmp/ipchange/oldtonewip.txt
newip=`awk '{print $1}' /tmp/ipchange/newip.txt`
echo newip=$newip
echo
# 在字典文件中比对，如果找不到

IP对应关系则退出

count=`grep "$oldip\>" /tmp/ipchange/ipcheck.txt |wc –

l`
if [ $count -eq 0 ];then
echo "no ip matched in ipcheck.txt, will exit in 5 seconds!!!"
sleep 5
exit 100
fi
grep "$oldip\>" /tmp/ipchange/ipcheck.txt>/tmp/oldtonewip.txt
awk '{print $2}' /tmp/oldtonewip.txt>/tmp/ipchange/newip.txt
mv /tmp/oldtonewip.txt /tmp/ipchange/oldtonewip.txt
newip=`awk '{print $1}' /tmp/ipchange/newip.txt`
echo newip=$newip
echo
# 定义新网关

awk -F. '{print $1"."$2"."$3"."254}'
/tmp/ipchange/newip.txt>/tmp/ipchange/newgatewayip.txt
newgatewayip=`awk '{print $1}' /tmp/ipchange/newgatewayip.txt`
echo newgatewayip=$newgatewayip
echo
#保存网卡的旧配置，并设置新定义的配置信息

cd /etc/sysconfig/network-scripts/
cp ifcfg-eth0 /tmp/ipchange/ifcfg-eth0.old
sed -e "s/$oldip/$newip/g" ifcfg-eth0>ifcfg-eth0.tmp1
sed -e "s/$oldgatewayip/$newgatewayip/g" ifcfg-eth0.tmp1>ifcfg-eth0.tmp2
sed -e "/NETMASK/d" ifcfg-eth0.tmp2>ifcfg-eth0.tmp1
sed -e "/IPADDR/a\\NETMASK=255.255.255.0" ifcfg-eth0.tmp1>ifcfg-eth0.tmp2
rm -f ifcfg-eth0
cp ifcfg-eth0.tmp2 /tmp/ipchange/ifcfg-eth0.new
cp ifcfg-eth0.tmp2 ifcfg-eth0
rm -f ifcfg-eth0.tmp1
rm -f ifcfg-eth0.tmp2
cat /etc/sysconfig/network-scripts/ifcfg-eth0
#################################################################
# 备份

hosts文件

cp -f /etc/hosts /tmp/ipchange/oldhosts.txt
for ip in $(cat /etc/hosts |egrep ^[0-9] |awk '{print $1}'|grep -v 127.0.0.1)
do
iprule=$(grep $ip /tmp/ipchange/ipcheck.txt |head -n 1)
if [ "$iprule" != "" ]
then
shipsec=$(echo $iprule |awk '{print $1}' |awk -F'.' '{print $1"."$2"."$3}')
cdipsec=$(echo $iprule |awk '{print $2}' |awk -F'.' '{print $1"."$2"."$3}')
echo "shipsec:$shipsec"
echo "cdipsec:$cdipsec"
sed -i "s/^$shipsec/$cdipsec/g" /etc/hosts
else
echo "WARNING: ip $ip not matched in ipcheck.txt"
fi
done
#################################################################
# 设置

NTP信息（通过

crontab定时更新）

crontab -l>/tmp/crontab.old
if [ -f /var/spool/cron/root ]
then
sed -i '/ntpdate/d' /var/spool/cron/root
fi
echo "*/5 * * * * /sbin/ntpdate $kfcs_ntp1">>/var/spool/cron/root
#echo "*/5 * * * * /sbin/ntpdate $kfcs_ntp2">>/var/spool/cron/root
echo "tinker panic 0">/etc/ntp.conf
echo "restrict 127.0.0.1">>/etc/ntp.conf
echo "restrict default kod nomodify notrap">>/etc/ntp.conf
echo "server $kfcs_ntp1">>/etc/ntp.conf
echo "server $kfcs_ntp2">>/etc/ntp.conf
echo "keys /etc/ntp/keys">>/etc/ntp.conf
echo "driftfile /var/lib/ntp/drift">>/etc/ntp.conf
echo "$kfcs_ntp1">/etc/ntp/ntpservers
echo "$kfcs_ntp2">>/etc/ntp/ntpservers
echo "clock.redhat.com">>/etc/ntp/ntpservers
echo "clock2.redhat.com">>/etc/ntp/ntpservers
echo "$kfcs_ntp1">/etc/ntp/step-tickers
echo "$kfcs_ntp2">>/etc/ntp/step-tickers
echo "clock.redhat.com">>/etc/ntp/step-tickers
echo "clock2.redhat.com">>/etc/ntp/step-tickers
ntp=`cat /etc/ntp/ntpservers | head -1`
crontab -l
##################################################################
# 设置

DNS信息

cp /etc/resolv.conf /tmp/ipchange/resolv.conf.old
> /etc/resolv.conf
dns=`sed -e '/nameserver/g' /etc/resolv.conf | awk -F " " '{print $2}' /etc/
resolv.conf | head -2 | tail -1`
###################################################################
# 设置监控平台参数

mv /home/patrol/startagent.sh /tmp/startagent.sh.old
echo su - patrol -c \"/home/patrol/Patrol3/PatrolAgent -id
$newip\">/home/patrol/startagent.sh
chown patrol:patrol /home/patrol/startagent.sh
chmod 774 /home/patrol/startagent.sh
####################################################################
# 重置关键用户密码

#echo ???????? | passwd root --stdin ????????请用实际密码代替

#echo ******** | passwd patrol --stdin ********请用实际密码代替

###################################################################
#shutdown computer
#shutdown -h now （

shutdown前标注了

#，所以此句只标注不执行）

service network restart
service ntpd stop
chkconfig ntpd off
echo "New IP is $newip"
echo "New primary DNS is $dns"
echo "New primary NTP is $ntp"
echo "Change is successful!"
}
# 以上语句在设置完所有配置信息后，需要重启网络服务，以确保新

IP能够正常工作。

ex() {
echo "Nothing is changed!"
exit 0
}
# 以上语句为设置

CASE语句场景的

ex函数，选取

E|e时，不执行任何操作，直接退出。

case "$evar" in
R|r)
dr
;;
T|t)
dt
;;
E|e)
ex
;;
*)
# echo $"Usage: $0 {R|T|E}"
# exit 1
Esac
# 以上语句为

CASE语句场景，可根据不同的选择执行灾备环境、开发环境的参数调整指令，

或者不执行任何操作，直接退出。

以下为Linux示例的截图：

原始状态，打开虚拟机后初始IP的后两位为192.101，如图3-21所示。

图3-21　旧IP显示截图

执行ipchange脚本时，要求用户根据提示选取执行环境，我们输入R（灾备环境），如图3-22所示。

图3-22　选取执行环境截图

执行ipchange脚本后，我们可以看到切换成功的提示，如图3-23所示。

图3-23　显示IP替换完成

执行ipchange脚本后，可以看到最后两位已经替换为100.101了，如图3-24所示。

图3-24　新IP显示截图

3.3.4　通过SFTP和WMIC指令将脚本文件上传至所有虚拟机

3.3.2和3.3.3节我们完成了切换脚本的编制，我们要在源存储与目标存储保持复制关系的期间，将切换脚本及字典文件上传至所有逻辑节点的指定目录，以确保复制关系断开后目标虚拟机中存在切换用的脚本。

1.基于Linux的上传

所有Linux默认都通过SSH方式访问，因此我们可以利用SFTP功能模块将切换脚本和字典文件上传至所有Linux节点的指定位置。

（1）先搭建一台上传用的Linux脚本机，安装LFTP包，将涉及Linux节点的IP保存为/tmp/ip.txt。

（2）同时编制以下脚本：

#！

/bin/bash
for i in `cat /tmp/ip.txt`
do
lftp –

u 用户名

@密码

sftp://$i <<EOF >>/tmp/sftp.log
mkdir /tmp/ipchange
cd /tmp/ipchange
lcd /tmp
mput ipchange.sh ipcheck.txt
EOF
done

2.基于Windows的上传

环境中所有Windows节点均启用了WMI管理模块，WMIC提供了从命令行接口和批命令脚本执行系统管理的支持。因此我们可以利用WMIC将切换脚本和字典文件上传至所有Windows节点的指定位置。

我们同样首先搭建一台上传用的Windows脚本机，将涉及Windows节点的IP保存为d：\windows-script\ip.txt，同时编制以下脚本：

@echo off
del d:\windows-script\ip.log
for /f "skip=1 eol=# tokens=1,2,3 delims= " %%i in (d:\windows-script\ip.txt)
do (@echo %%i >> d:\windows-script\ip.log
net use s: \\%%i\c$ /user:%%j %%k >> d:\windows-script\ip.log
mkdir s:\ipchange
xcopy d:\windows-script\ipchange.cmd s:\ipchange /s /y >>
d:\windows-script\ip.log
xcopy d:\windows-script\ipcheck.txt s:\ipchange /s /y >>
d:\windows-script\ip.log
net use s: /delete >> ip.log
)

3.3.5　搬迁期间的注意事项

由于是通过存储底层复制技术进行的逻辑迁移，因此在目标端完成注册，并调整虚拟机名称后，在vCenter管理界面看到的是改名为目标IP的虚拟机名称。但存储底层的数据文件仍然是以原始IP命名的。

为防止给以后的运维留下后患，需要对虚拟机进行一次存储VMotion的操作，这样存储内的虚拟机文件名在存储迁移的同时就能自动调整为新IP的文件名了。

浏览存储底层所看到的文件情况，如图3-25所示。

图3-25　数据存储内部虚拟机的文件结构

以上的脚本是利用Shell脚本简化Linux逻辑节点的搬迁，在实际运行中还有不完善的地方。由于未包含操作系统判别语句，目前仅在RHEL5和RHEL6系列上测试通过，因此大家可以根据实际情况添加判断语句以适应RHEL较新的7版本或其他Linux版本。另外也可完善逻辑语句使得脚本更加严谨可靠。

字典文件ipcheck.txt中的IP对应关系，需要确保IP地址的唯一，相同的新旧IP对应关系只能有一行。同时也要防止同一个旧IP对应多个新IP的情况。

3.4　小结

作为一名IT运维人员，在日常的运维中，只要熟练掌握基本工具并融会贯通加以灵活应用，就可以将看似不可能完成的任务变为可能，同时达到事半功倍的效果。以上经验仅供大家分享。在此感谢李彬厚和齐特两位同事对我的大力支持。

第4章　集中配置管理工具Puppet

作者简介

余洪春（抚琴煮酒），运维架构师、资深系统管理员，《构建高可用Linux服务器》《Linux集群和自动化运维》作者，ChinaUnix论坛“集群和高可用”、“监控及自动化运维”版版主。从事云计算和电子商务网站运维工作十多年，在Linux集群、自动化运维、云计算及高并发高流量网站架构设计等方面进行了深入的研究，在大量一线实践中积累了丰富的经验。精通负载均衡高可用和自动化运维技术，擅长高并发高流量网站系统架构设计。

大数据时代高伸缩性、容错性、分布式的特点给系统运维提出了更高的要求，现在已不再是系统管理员疲于安装操作系统、对系统参数进行逐一配置与优化、打补丁、安装软件、配置软件、添加某个服务的时代。为了提高效率、避免重复劳动、减少错误、积累知识，系统管理员已经开始做一些局部的自动化工作。但只有这些还远远不够，为了满足运维需求，还需要更彻底地应用自动化运维工具。

常见的运维工作流程包括：安装系统→优化系统与配置→安装软件→配置软件→添加监控→检查，等网站或系统正式上线后，后续可能还会有添加服务→配置变更→打补丁修复漏洞等工作，是不是觉得很繁琐？尤其当我们负责部署和维护大量服务器，凭借一己之力无法完成时，便需要一些自动化工具来帮忙了。

系统运维工作面临的各种不确定性更让人头疼，在10台机器上变更应用还是很简单的事，就算到了100台也有轻量级的Fabric自动化运维工具，但是如果上升到1000台甚至上万台时就会变得非常复杂。重复性的劳动还会让人觉得疲惫和乏味，久而久之可能还会产生厌倦工作的情绪，这个时候使用集中配置管理工具Puppet则能很轻松地解决这些问题，这也是现在Puppet越来越流行的原因之一。

下面将介绍Puppet的进阶操作。

这里的操作环境按照前面的操作已经搭建完毕，具体如下所示：

server.cn7788.com 192.168.1.205 puppet-master
fabric.cn7788.com 192.168.1.204 puppet-client
client.cn7788.com 192.168.1.206 puppet-client

我们清理好前面的环境，清空/etc/puppet/manifests/里的site.pp文件的内容，并且利用ntpdate做好时间的精准对时，不然puppet-client在连接时会报错，连接不上。

4.1　如何同步puppet-agent端上的常用服务

如何同步puppet-agent端（也称为Puppet客户端）上的几个常用服务，要求如下：

·开启httpd服务和postfix服务。

·关闭vsftpd服务。

实现步骤其实并不复杂，我们还是在服务器端的/etc/puppet/manifests的site.pp文件上进行操作，内容如下所示（这里为了实验方便，请提前用yum安装好下面的服务）：

service {
["httpd","postfix"]:
ensure => running;
"vsftpd":
ensure => stopped;
}

Puppet客户端顺利连接上服务器端后，正常显示结果应该如下所示（完成需求，以节点机器client.cn7788.com
举例说明）：

Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Caching catalog for client.cn7788.com
Info: Applying configuration version '1446608434'
Notice: /Stage[main]/Main/Service[postfix]/ensure: ensure changed 'stopped' to 'running'
Info: /Stage[main]/Main/Service[postfix]: Unscheduling refresh on Service[postfix]
Notice: Finished catalog run in 1.60 seconds

我们可在Puppet客户端下面输入命令来验证下实验结果，具体命令如下所示：

service postfix status
master (pid 10028) is running...
service httpd status
httpd (pid 9603) is running...
service vsftpd status
vsftpd is stopped

4.2　如何在puppet-agent端上自动安装常用的软件包

在Puppet客户端自动安装screen、ntp和sysstat包，具体步骤如下所示。

（1）编辑/etc/puppet/manifests/site.pp文件，内容如下：

package {
["screen","ntp","sysstat"]:
ensure => "installed";
}

（2）客户端顺利连接上服务器端后，正常显示结果应该如下所示（完成需求，以节点机器client.cn7788.com
举例说明）：

Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Caching catalog for client.cn7788.com
Info: Applying configuration version '1446608680'
Notice: /Stage[main]/Main/Package[sysstat]/ensure: created
Notice: Finished catalog run in 15.03 seconds

4.3　如何自动同步puppet-agent端的yum源

通过以下命令我们可以观察到puppet-master端机器上更新了yum源：

ls ll /etc/yum.repos.d/

命令显示结果如下所示：

total 24
-rw-r--r--. 1 root root 1926 Feb 25 2013 CentOS-Base.repo
-rw-r--r--. 1 root root 638 Feb 25 2013 CentOS-Debuginfo.repo
-rw-r--r--. 1 root root 630 Feb 25 2013 CentOS-Media.repo
-rw-r--r--. 1 root root 3664 Feb 25 2013 CentOS-Vault.repo
-rw-r--r--. 1 root root 957 Nov 4 03:46 epel.repo
-rw-r--r--. 1 root root 0 Nov 4 03:46 nginx.repo
-rw-r--r--. 1 root root 1250 Apr 12 2013 puppetlabs.repo

如果想通过Puppet将这些文件都推送到与client.cn7788.com
和fabric.cn7788.com
相对应的目录/etc/yum.repos.d/下面去，那么这个该如何实现呢？

（1）修改/etc/puppet/fileserver.conf文件，内容如下所示：

[files]
path /etc/yum.repos.d/
allow *

（2）修改/etc/puppet/manifests/site.pp文件，内容如下所示：

file
{ "/etc/yum.repos.d/":
source => "puppet://server.cn7788.com/files/",
group => root,
owner => root,
mode => 644,
recurse => true,
force => true,
purge => true
}

Puppet客户端正确连接到Puppet服务器端后，大家会发现，puppet-server会向puppet-client端推送文件，我们分别在两台节点机器上输入如下命令：

puppet agent --test --server server.cn7788.com

两台机器的结果是一样的，输入命令的反馈结果如下所示：

Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Caching catalog for client.cn7788.com
Info: Applying configuration version '1446619419'
Notice: /Stage[main]/Main/File[/etc/yum.repos.d/puppetlabs.repo]/ensure: defined content as '{md5}14d68f86efb69e928d626c7ea8a974b3'
Notice: /Stage[main]/Main/File[/etc/yum.repos.d/CentOS-Debuginfo.repo]/ensure: defined content as '{md5}8b95e819eeea42849932309b5be5533d'
Info: Computing checksum on file /etc/yum.repos.d/puppetlabs-release-6-7.noarch.rpm
Info: /Stage[main]/Main/File[/etc/yum.repos.d/puppetlabs-release-6-7.noarch.rpm]: Filebucketed /etc/yum.repos.d/puppetlabs-release-6-7.noarch.rpm to puppet with sum 2aa0affe57ade441bfb9dcd6126a6cd6
Notice: /Stage[main]/Main/File[/etc/yum.repos.d/puppetlabs-release-6-7.noarch.rpm]/ensure: removed
Notice: /Stage[main]/Main/File[/etc/yum.repos.d/CentOS-Base.repo]/ensure: defined content as '{md5}d03052aaa85e5d26451f0ada9054f50f'
Info: Computing checksum on file /etc/yum.repos.d/softlist
Info: /Stage[main]/Main/File[/etc/yum.repos.d/softlist]: Filebucketed /etc/yum.repos.d/softlist to puppet with sum 36fe51e7e690fe7d65046e9868ed2fa4
Notice: /Stage[main]/Main/File[/etc/yum.repos.d/softlist]/ensure: removed
Info: /Stage[main]/Main/File[/etc/yum.repos.d/test]: Recursively backing up to filebucket
Info: Computing checksum on file /etc/yum.repos.d/test/dd
Info: /Stage[main]/Main/File[/etc/yum.repos.d/test]: Filebucketed /etc/yum.repos.d/test/dd to puppet with sum d41d8cd98f00b204e9800998ecf8427e
Info: Computing checksum on file /etc/yum.repos.d/test/cc
Info: FileBucket got a duplicate file {md5}d41d8cd98f00b204e9800998ecf8427e
Info: /Stage[main]/Main/File[/etc/yum.repos.d/test]: Filebucketed /etc/yum.repos.d/test/cc to puppet with sum d41d8cd98f00b204e9800998ecf8427e
Notice: /Stage[main]/Main/File[/etc/yum.repos.d/test]/ensure: removed
Notice: /Stage[main]/Main/File[/etc/yum.repos.d/nginx.repo]/ensure: defined content as '{md5}d41d8cd98f00b204e9800998ecf8427e'
Notice: /Stage[main]/Main/File[/etc/yum.repos.d/CentOS-Media.repo]/ensure: defined content as '{md5}db3010a594efc3043651d78741ac02ff'
Notice: /Stage[main]/Main/File[/etc/yum.repos.d/epel.repo]/ensure: defined content as '{md5}e8950030d9c72cf3e7a6469e8c1404ca'
Notice: /Stage[main]/Main/File[/etc/yum.repos.d/CentOS-Vault.repo]/ensure: defined content as '{md5}62b394965682a15877a36357fe55689d'
Notice: Finished catalog run in 0.75 seconds

同样，我们在Puppet客户端用ll命令进行观察，将会发现已经将puppet-server端的/etc/yum.repos.d目录下的文件同步过来了，完成此工作需求，检查结果如下：

total 24
-rw-r--r--. 1 root root 1926 Nov 4 06:44 CentOS-Base.repo
-rw-r--r--. 1 root root 638 Nov 4 06:44 CentOS-Debuginfo.repo
-rw-r--r--. 1 root root 630 Nov 4 06:44 CentOS-Media.repo
-rw-r--r--. 1 root root 3664 Nov 4 06:44 CentOS-Vault.repo
-rw-r--r--. 1 root root 957 Nov 4 06:44 epel.repo
-rw-r--r--. 1 root root 0 Nov 4 06:44 nginx.repo
-rw-r--r--. 1 root root 1250 Nov 4 06:44 puppetlabs.repo

4.4　如何根据不同名字的节点机器推送不同的文件

Puppet服务器端向名为client.cn7788.com
的客户端推送/etc/crontab文件，向名为fabric.cn7788.com
的客户端推送/etc/hosts文件，向名为nginx.cn7788.com
的客户端推送/etc/resolv.conf文件，这个应该如何实现呢？

这里的需求比较复杂，我们可以通过Puppet模块来实现需求，模块文件一般是存放在服务器端的/etc/puppet/modules/下，在它下面我们会定义一个名为pushfile的模块。模块是Puppet生态系统中的核心部分，我们一般通过资源的定义告诉Puppet应该做什么，一般一个应用就编写成一个模块，例如我们定义的pushfile模块。

我们用#注释掉fileserver.conf文件里面的相关内容，清空site.pp文件，然后在/etc/puppet/modules下面建立名为pushfile的模块，操作如下所示：

mkdir -p /etc/puppet/modules/pushfile/{manifests,files,templates}

而site.pp文件的内容如下：

import "node.pp"

这里扩展了site.pp文件的内容，它会载入node.pp文件，这样puppet-master在启动的时候，就会自动接入并处理node.pp文件了。

服务器端的/etc/puppet/manifests/node.pp文件内容如下所示：

node 'client.cn7788.com'{
file
{"/etc/crontab":
source => "puppet://server.cn7788.com/modules/pushfile/crontab",
group => root,
owner => root,
mode => 644,
}
}
node 'fabric.cn7788.com'{
file
{"/etc/hosts":
source => "puppet://server.cn7788.com/modules/pushfile/hosts",
group => root,
owner => root,
mode => 644,
}
}
node 'nginx.cn7788.com'{
file
{"/etc/resolv.conf":
source => "puppet://server.cn7788.com/modules/pushfile/resolv.conf",
group => root,
owner => root,
mode => 644,
}
}

node.pp配置文件比较长并且也很复杂，我们究竟应该使用什么方法呢？我们可以输入如下命令：

puppet parser validate node.pp

如果配置文件是正确的，则什么也不显示；如果检测到是错误的，则会以红色醒目字体来提示。

Puppet利用node（节点）来区分不同的客户端，并且会给不同的客户端分配不同的资源，我们观察下node.pp文件，在这个文件里，source值会告诉Puppet去哪里寻找文件，我们将文件都置于puppet-server的/etc/puppet/modules/site/files目录下面，它相当于根目录，然后我们将服务器端的文件依次复制到此目录的/etc下，操作如下所示：

cp /etc/{crontab,hosts,resolv.conf} /etc/puppet/modules/pushfile/files

我们用tree命令来查看下pushfile模块的目录树结构，命令如下：

tree /etc/puppet/modules/pushfile

命令显示结果如下所示：

/etc/puppet/modules/pushfile├──

files│

├──

crontab│

├──

hosts│

└──

resolv.conf├──

manifests└──

templates
3 directories, 3 files

我们依次在3台puppet-client机器上面执行puppet命令，可以发现配置是成功的，这里以client.cn7788.com
机器举例说明下，输入如下命令：

puppet agent --test --server server.cn7788.com

命令显示结果如下所示：

Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Caching catalog for client.cn7788.com
Info: Applying configuration version '1446623887'
Notice: /Stage[main]/Main/Node[client.cn7788.com]/File[/etc/crontab]/content:
--- /etc/crontab 2015-11-03 07:34:26.372044003 +0000
+++ /tmp/puppet-file20151104-17989-alqkh6-0 2015-11-04 08:02:50.127072946 +0000
@@ -14,10 +14,9 @@
# | | | | |
# * * * * * user-name command to be executed
-00 01 * * * root /bin/bash /usr/local/nginx/sbin/cut_nginx_log.sh >> /dev/null 2>&1
+#00 01 * * * root /bin/bash /usr/local/nginx/sbin/cut_nginx_log.sh >> /dev/null 2>&1
01 02 * * * root /bin/bash /root/backup.sh >> /dev/null 2>&1
-03 03 * * * root /bin/bash /root/sshdeny.sh >> /dev/null 2>&1
-#01 04 * * * root /bin/bash /root/rsync_dir.sh >>/dev/null 2>&1
+#03 03 * * * root /bin/bash /root/sshdeny.sh >> /dev/null 2>&1
-#*/5 * * * * root /etc/init.d/iptables stop
+*/5 * * * * root /etc/init.d/iptables stop
Info: Computing checksum on file /etc/crontab
Info: /Stage[main]/Main/Node[client.cn7788.com]/File[/etc/crontab]: Filebucketed /etc/crontab to puppet with sum 7e76ef490e02dde0dd8e82a5cf7c0c69
Notice: /Stage[main]/Main/Node[client.cn7788.com]/File[/etc/crontab]/content: content changed '{md5}7e76ef490e02dde0dd8e82a5cf7c0c69' to '{md5}2022061d798f3933e0caafb614272212'
Notice: Finished catalog run in 0.47 seconds

配置是成功的，/etc/crontab被成功地推送过来了，而且内容也进行了置换，其他节点机器的结果这里就不打印了。

注意

在/etc/puppet/modules下定义的模块都是自动载入的，所以不需要用import来加载。

4.5　如何根据节点机器名来选择性地执行Shell程序

客户端机器nginx.cn7788.com
没有安装Nagios客户端程序，这时想通过puppet-server推送Shell脚本自动安装，其他节点机器暂时不需要安装这个程序，那么这个应该如何实现呢？

我们主要还是通过模块的方法来实现这个需求，和4.4节一样，先在这里建立一个名为nagioscli的模块，命令如下所示：

mkdir -p /etc/puppet/modules/nagiosins/{manifests,files,templates}

在/etc/puppet/modules/nagioscli/files目录下安装Nagios客户端名为nagiosclil.sh的Shell程序，具体内容如下：

#!/bin/bash
useradd nagios
cd /usr/local/src
wget wget http://syslab.comsenz.com/downloads/linux/nagios-plugins-1.4.13.tar.gz
wget http://syslab.comsenz.com/downloads/linux/nrpe-2.12.tar.gz
tar zxvf nagios-plugins-1.4.13.tar.gz
cd nagios-plugins-1.4.13
./configure
make
make install
chown nagios:nagios /usr/local/nagios
chown -R nagios:nagios /usr/local/nagios/libexec
cd ../
tar zxvf nrpe-2.12.tar.gz
cd nrpe-2.12
./configure
make all
make install-plugin
make install-daemon
make install-daemon-config
sed -i 's@allowed_hosts=127.0.0.1@allowed_hosts=114.112.11.11@'/usr/local/nagios/etc/nrpe.cfg
/usr/local/nagios/bin/nrpe -c /usr/local/nagios/etc/nrpe.cfg -d
echo "/usr/local/nagios/bin/nrpe -c /usr/local/nagios/etc/nrpe.cfg -d" >> /etc/rc.local

node.pp文件的内容如下所示：

node 'nginx.cn7788.com'{
file
{"/usr/local/src/nagiosins.sh":
source => "puppet://server.cn7788.com/modules/nagiosins/nagiosins.sh",
group => root,
owner => root,
mode => 755,
}
exec {
"auto install naigios client":
command =>"sh /usr/local/src/nagiosins.sh",
user =>"root",
path => ["/usr/bin","/usr/sbin","/bin","/bin/sh"],
}
}
node 'client.cn7788.com'{
file
{"/usr/local/src/nagiosins.sh":
source => "puppet://server.cn7788.com/modules/nagiosins/nagiosins.sh",
group => root,
owner => root,
mode => 755,
}
exec {
"auto install naigios client":
command =>"sh /usr/local/src/nagiosins.sh",
user =>"root",
path => ["/usr/bin","/usr/sbin","/bin","/bin/sh"],
}
}
node 'fabric.cn7788.com'{
}

节点机器client和fabric机器后面什么都没有，表示在此节点机器上面没有任何操作，因为client和fabirc节点机器也在此Puppet环境里，并配置成了自动连接。如此配置，是为了防止自动连接时Puppet频繁报错。

这里以client.cn7788.com
为例，在其主机上输入如下命令：

puppetd --test --server server.cn7788.com

命令显示结果如下所示：

Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Caching catalog for client.cn7788.com
Info: Applying configuration version '1446693418'
Notice: /Stage[main]/Main/Node[client.cn7788.com]/Exec[auto install naigios client]/returns: executed successfully
Notice: Finished catalog run in 165.27 seconds

执行时间比较长，总共耗时165.27秒，我们先检查下client.cn7788.com
的节点机器上是否开启了nrpe进程，输入命令如下所示：

ps aux | grep nrpe | grep –

v grep

命令显示结果如下所示：

nagios 22331 0.0 0.1 5108 924 ? Ss 22:35 0:00 /usr/local/nagios/bin/nrpe -c /usr/local/nagios/etc/nrpe.cfg -d

我们再检查下/etc/rc.local，看看此命令有没有添加进去，命令如下所示：

grep -v "^#" /etc/rc.local

命令执行结果显示如下所示：

touch /var/lock/subsys/local
/usr/local/nagios/bin/nrpe -c /usr/local/nagios/etc/nrpe.cfg -d

检查结果说明puppet-master的nagioscli模块是正常的，nginx.cn7788.com
的结果与此类似，这里就不贴出检测结果了。

4.6　如何快速同步puppet-server端的www目录文件

当我们拥有大规模Web集群的时候，所有服务器/var/www/html的数据要求必须迅速统一，这个又该如何实现呢？

实例说明：server.cn7788.com
机器下的/data/svn/resource目录文件或子目录发生改变时，要求nginx.cn7788.com
、client.cn7788.com
和fabric.cn7788.com
对应的目录/var/www/html文件或子目录也发生改变。

前面已经提到了，Puppet分发大文件和海量图片小文件的效果并不好，但这里我们其实可以用rsync+Puppet的方式来实现工作中的需求。这里要用到Puppet Kick的知识点，Puppet Kick是puppet-master端使用此命令强制其Agent节点机器运行Puppet Agent，从而达到立即更新或同步文件的目的。当然也可以用Puppet rsync模块，但我个人觉得使用这个太麻烦了，所以这里还是采用自己摸索出来的方法，具体步骤如下（这里以client.cn7788.com
节点机器举例说明，其他Puppet客户端操作与此类似，这里就不一一列举了）。

（1）在所有puppet-client客户端上配置puppet.conf文件，使其固定使用8139端口，我们在其/etc/puppet/puppet.conf文件下添加如下命令：

[puppet-client]
listen = true
server=server.cn7788.com

listen=true：此选项将使puppet agnet监听8139端口。

server=server.cn7788.com：此选项必须要配置，经过测试，可以发现如果无此选项时，puppet-client会连接不到puppet-master机器，从而导致文件同步不过去。

（2）修改客户端的/etc/puppet/auth.conf，允许server.cn7788.com
的服务器端进行推送。

在最末行的path/之后添加allow*，保证代码内容相同：

path /run
auth any
allow *

如果不进行此项操作的话，会有如下报错：

Debug: /File[/var/lib/puppet/ssl/private]: Autorequiring File[/var/lib/puppet/ssl]
Debug: /File[/var/lib/puppet/ssl/certs/server.cn7788.com.pem]: Autorequiring File[/var/lib/puppet/ssl/certs]
Debug: /File[/var/lib/puppet/ssl/private_keys]: Autorequiring File[/var/lib/puppet/ssl]
Debug: /File[/var/lib/puppet/lib]: Autorequiring File[/var/lib/puppet]
Debug: /File[/var/lib/puppet/ssl/private_keys/server.cn7788.com.pem]: Autorequiring File[/var/lib/puppet/ssl/private_keys]
Debug: /File[/var/lib/puppet/ssl/certificate_requests]: Autorequiring File[/var/lib/puppet/ssl]
Debug: /File[/var/lib/puppet/state]: Autorequiring File[/var/lib/puppet]
Debug: /File[/var/lib/puppet/ssl]: Autorequiring File[/var/lib/puppet]
Debug: /File[/var/lib/puppet/ssl/crl.pem]: Autorequiring File[/var/lib/puppet/ssl]
Debug: /File[/var/lib/puppet/facts.d]: Autorequiring File[/var/lib/puppet]
Debug: /File[/var/lib/puppet/ssl/certs]: Autorequiring File[/var/lib/puppet/ssl]
Debug: /File[/var/lib/puppet/ssl/public_keys]: Autorequiring File[/var/lib/puppet/ssl]
Debug: /File[/var/lib/puppet/preview]: Autorequiring File[/var/lib/puppet]
Debug: /File[/var/lib/puppet/ssl/certs/ca.pem]: Autorequiring File[/var/lib/puppet/ssl/certs]
Debug: Finishing transaction 69854562006000
Debug: Creating new connection for https://client.cn7788.com:8139
Error: Host client.cn7788.com failed: Error 403 on SERVER: Forbidden request: server.cn7788.com(192.168.1.205) access to /run/client.cn7788.com [save] authenticated at :119最后，在

puppet-client端重启

Puppet服务，命令如下所示：

service puppet restart

最后，在puppet-client端重启Puppet服务，命令如下所示：

service puppet restart

（3）在server.cn7788.com
机器的/etc目录下建立rsyncd.pass文件并分配内容，注意：这个是推送到客户端的文件，需要注意与/etc/rsyncd.password文件进行区分，/etc/rsyncd.pass文件只需要指定同步用户的密码即可，/etc/rsyncd.password文件内容如下所示：

test:test101

/etc/rsyncd.pass文件内容如下所示：

test101

（4）配置Puppet服务器端的rsync服务，/etc/rsyncd.conf文件内容如下所示：

uid = www
gid = www
user chroot= no
max connections =200
timeout = 600
pid file = /var/run/rsyncd.pid
lock file = /var/run/rsyncd.lock
log file = /var/log/rsyncd.log
[www]
path=/var/www/html/
ignore errors
read only = no
list = no
hosts allow = 192.168.1.0/255.255.255.0
auth users = test
secrets file = /etc/rsyncd.password

我们的Apache服务的属主和属组是www：www，让rsync也以www用户运行，这样可以保证通过rsync同步过去的文件属性。我这里采用xinetd管理rsync，将其中的disable改为no，并重启xinetd进程，如下所示：

service xinetd restart

到了这一步其实还要仔细检查一下，有时因为配置文件的错误或文件权限分配的错误，rsync进程其实没有正确启动，所以我们还要用如下命令来检查下：

lsof -i:873

命令显示结果如下，这表明rsync进程已经在监听873端口了，服务已经被正确启动起来了：

COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
xinetd 7008 root 5u IPv4 24249 0t0 TCP *:rsync (LISTEN)

（5）创建名为rsyncfile的模块，命令如下所示：

mkdir -p /etc/puppet/modules/wwwrsync/{manifests,files,templates}

并将/etc/rsyncd.pass复制到/etc/puppet/modules/wwwrsync/files目录下，命令如下所示：

cp /etc/rsyncd.pass /etc/puppet/modules/wwwrsync/files/

（6）我们在/etc/puppet/modules/wwwrsync/manifests/init.pp里定义一个名为wwwrsync的类，init.pp文件内容如下所示：

class wwwrsync{
package { httpd:
ensure => present,
}
file {
"/etc/rsyncd.pass":
source =>"puppet://server.cn7788.com/modules/wwwrsync/rsyncd.pass",
owner =>"root",
group =>"root",
mode =>"600",
}
exec {
"auto rsync web directory":
command =>"rsync -vzrtopg --delete test@192.168.1.205::www /var/www/html --password-file=/etc/rsyncd.pass",
user =>"root",
path => ["/usr/bin","/usr/sbin","/bin","/bin/sh"],
}
}

init.pp文件包含了名为wwwrsync的类，此类包含了三个资源，第一个是名为httpd的资源包，如果此服务没有安装的话，Puppet客户端会自行安装httpd服务，保证在本机自动生成/var/www/htnl目录；第二个是file和exec命令，它会将/etc/puppet/modules/wwwrsync/files/rsyncd.pass文件推送到puppet-client端后，在puppet-client端执行rsync同步命令，达到同步/var/www/html目录的目的，所以rsync-vzrtopg后面应该接rsync服务器地址，即192.168.1.205，这点请大家注意不要产生混淆。

注意

wwwrsync模块中定义的wwwrsync类要跟wwwrsync模块同名，不然Puppet客户端在连接服务器端时会产生找不到wwwrsync类名的报错，实验过程中如果遇到错误，请注意多查看Puppet和系统日志。

（7）我们接着在/etc/puppet/manifetes/site.pp中定义一个default的特殊节点，这是一个默认节点，它会将wwwrsync类中的内容应用到所有主机上面，其内容如下所示：

node default {
include wwwrsync
}

（8）我们在server.cn7788.com
上面执行推送命令，命令如下所示：

puppet kick -d --host `cat /etc/puppet/iplist.txt`

命令结果如下所示（反馈结果太长，这里只截取部分）：

Debug: /File[/var/lib/puppet/ssl/certs]/selrange: Found selrange default 's0' for /var/lib/puppet/ssl/certs
Debug: /File[/var/lib/puppet/ssl/certs/ca.pem]: Autorequiring File[/var/lib/puppet/ssl/certs]
Debug: /File[/var/lib/puppet/ssl/public_keys/server.cn7788.com.pem]: Autorequiring File[/var/lib/puppet/ssl/public_keys]
Debug: /File[/var/lib/puppet/ssl/certs/server.cn7788.com.pem]: Autorequiring File[/var/lib/puppet/ssl/certs]
Debug: /File[/var/lib/puppet/ssl/private_keys]: Autorequiring File[/var/lib/puppet/ssl]
Debug: /File[/var/lib/puppet/lib]: Autorequiring File[/var/lib/puppet]
Debug: /File[/var/lib/puppet/ssl/private_keys/server.cn7788.com.pem]: Autorequiring File[/var/lib/puppet/ssl/private_keys]
Debug: /File[/var/lib/puppet/ssl/private]: Autorequiring File[/var/lib/puppet/ssl]
Debug: /File[/var/lib/puppet/ssl/crl.pem]: Autorequiring File[/var/lib/puppet/ssl]
Debug: /File[/var/lib/puppet/ssl/certificate_requests]: Autorequiring File[/var/lib/puppet/ssl]
Debug: /File[/var/lib/puppet/state]: Autorequiring File[/var/lib/puppet]
Debug: /File[/var/lib/puppet/ssl]: Autorequiring File[/var/lib/puppet]
Debug: /File[/var/lib/puppet/ssl/certs]: Autorequiring File[/var/lib/puppet/ssl]
Debug: /File[/var/lib/puppet/facts.d]: Autorequiring File[/var/lib/puppet]
Debug: /File[/var/lib/puppet/ssl/public_keys]: Autorequiring File[/var/lib/puppet/ssl]
Debug: /File[/var/lib/puppet/preview]: Autorequiring File[/var/lib/puppet]
Debug: Finishing transaction 69995814564700
Debug: Creating new connection for https://client.cn7788.com:8139
Getting status
status is success
client.cn7788.com finished with exit code 0

/etc/puppet/iplist.txt文件内容如下所示：

client.cn7788.com
nginx.cn7788.com
fabric.cn7788.com

我们观察名为client.cn7788.com
的节点机器，它的/var/www/html文件立即就跟server.cn7788.com
的/var/www/html目录同步了，从而实现了此需求，我们用tail命令观察test1.cn7788.com
机器的messages日志，结果如下所示：

Nov 5 03:26:46 client puppet-agent[27782]: (/Stage[main]/Wwwrsync/Exec[auto rsync web directory]/returns) executed successfully
Nov 5 03:26:47 client puppet-agent[27782]: Finished catalog run in 0.74 seconds
Nov 5 03:41:50 client puppet-agent[28422]: (/Stage[main]/Wwwrsync/Exec[auto rsync web directory]/returns) executed successfully
Nov 5 03:41:51 client puppet-agent[28422]: Finished catalog run in 1.32 seconds
Nov 5 04:11:50 client puppet-agent[28690]: (/Stage[main]/Wwwrsync/Exec[auto rsync web directory]/returns) executed successfully
Nov 5 04:11:50 client puppet-agent[28690]: (/Stage[main]/Wwwrsync/File[/etc/rsyncd.pass]/content) content changed '{md5}d8e8fca2dc0f896fd7cb4cb0031ba249' to '{md5}93412aea2e70977a362530b0dba2498a'
Nov 5 04:11:52 client puppet-agent[28690]: Finished catalog run in 1.97 seconds
Nov 5 04:14:17 client puppet-agent[27782]: triggered run
Nov 5 04:14:25 client puppet-agent[27782]: (/Stage[main]/Wwwrsync/Exec[auto rsync web directory]/returns) executed successfully
Nov 5 04:14:26 client puppet-agent[27782]: Finished catalog run in 1.69 seconds

4.7　如何利用ERB模板自动配置Apache虚拟主机

线上环境有不少Apache主机需要增加基于域名的虚拟主机，特别是有的机器，虚拟主机达到几十台之多，这种情况下，如何能够方便快速地部署httpd.conf和虚拟主机配置文件呢？

这里就需要用到Puppet的ERB模板功能了，模板文件就是在模块下面templates目录中以“.erb”结尾的文件，Puppet模板主要用于文件，例如各种服务的配置文件，相同的服务，不同的配置就可以考虑使用模板文件，例如Nginx和Apache的虚拟主机配置就可以考虑采用ERB模板的方案，我们在这里先以Apache为例来说明下，其模块的目录树结构如下：

/etc/puppet/├──

auth.conf├──

environments│

└──

example_env│

├──

manifests│

├──

modules│

└──

README.environment├──

fileserver.conf├──

manifests│

├──

nodes│

│

├──

client.pp│

│

└──

nginx.pp│

└──

site.pp├──

modules│

└──

apache│

├──

files│

├──

manifests│

│

└──

init.pp│

└──

templates│

└──

httpd.conf.erb└──

puppet.conf
11 directories, 9 files

Apache模块的具体配置步骤如下。

（1）首先我们建立一个Apache模块，命令如下所示：

mkdir –

p /etc/puppet/modules/apache/{files,manifests,templates}

（2）/etc/puppet/modules/apache/manifests/init.pp文件内容如下所示：

class apache{
package{"httpd":
ensure =>present,
}
service{"httpd":
ensure =>running,
require =>Package["httpd"],
}
}
define apache::vhost ( $sitedomain, $rootdir,$port ) {
file { "/etc/httpd/conf.d/httpd_vhost_${sitedomain}.conf":
#path => '/etc/httpd/conf/httpd_vhost.conf',
content => template("apache/httpd.conf.erb"),
require => Package["httpd"],
}
}

这里用到了Puppet中defind（定义）的概念，定义和类都属于资源容器，不过定义的特点是能够在一台主机上被赋值多次，此外它还能接受参数。类在Puppet中是单例的，它们能够在一台主机节点机器上被包含多次，但是只会被求值一次；而定义因为能够接受参数，所以可以被声明多次，并且每一个声明都会被求值。

（3）/etc/puppet/modules/apache/templates中的httpd.conf.erb模板文件内容如下所示：

<VirtualHost *:<%= port %>>
ServerName <%= sitedomain %>
DocumentRoot /var/www/html/<%= rootdir %>
<Directory <%= rootdir %>>
Options Indexes FollowSymLinks
AllowOverride None
Order allow,deny
Allow from all
</Directory>
ErrorLog logs/<%= sitedomain %>_error.log
CustomLog logs/<%= sitedomain %>_access.log common
</VirtualHost>

在httpd.conf.erb中我们提前定义了两个变量$sitedomain、$port，Puppet中使用这种格式<%=变量名%>来定义变量，我们可以检测一下模板是否存在语法问题，命令如下：

erb -x -T '-' -P /etc/puppet/modules/apache/templates/httpd.conf.erb | ruby -c

结果显示如下，表示语法不存在任何问题：

Syntax OK

（4）server机器的/etc/puppet/manifests/site.pp内容如下：

import 'nodes/*.pp'

其下目录nodes中有两个文件：一个为client.pp，另一个为nginx.pp。server机器的/etc/puppet/manifests/nodes/nginx.pp文件内容如下：

node 'nginx.cn7788.com' {
include apache
apache::vhost {'webmaster.cn7788.com':
sitedomain => "webmaster.cn7788.com",
rootdir => webmaster,
port => 80,
}
apache::vhost {'webtest.cn7788.com':
sitedomain => "webtest.cn7788.com",
rootdir => webtest,
port => 80,
}
apache::vhost {'webrsync.cn7788.com':
sitedomain => "webrsync.cn7788.com",
rootdir => webrsync,
port => 80,
}
}

另一台节点机器client.cn7788.com
的client.pp配置文件内容，如下所示：

node 'client.cn7788.com' {
include apache
apache::vhost {'clientmaster.cn7788.com':
sitedomain => "webmaster.cn7788.com",
rootdir => webmaster,
port => 80,
}
apache::vhost {'clienttest.cn7788.com':
sitedomain => "webtest.cn7788.com",
rootdir => webtest,
port => 80,
}
}

我们在nginx.cn7788.com
的机器上输入如下命令进行验证：

puppet agent --test --server server.cn7788.com

结果如下所示：

Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Caching catalog for nginx.cn7788.com
Info: Applying configuration version '1446792027'
Notice: /Stage[main]/Apache/Package[httpd]/ensure: created
Notice: /Stage[main]/Main/Node[nginx.cn7788.com]/Apache::Vhost[webtest.cn7788.com]/File[/etc/httpd/conf.d/httpd_vhost_webtest.cn7788.com.conf]/ensure: defined content as '{md5}d5befc97a115a5069b6f8fd7e904b919'
Notice: /Stage[main]/Main/Node[nginx.cn7788.com]/Apache::Vhost[webmaster.cn7788.com]/File[/etc/httpd/conf.d/httpd_vhost_webmaster.cn7788.com.conf]/ensure: defined content as '{md5}07017828a5d085223c6635afff1d0f69'
Notice: /Stage[main]/Apache/Service[httpd]/ensure: ensure changed 'stopped' to 'running'
Info: /Stage[main]/Apache/Service[httpd]: Unscheduling refresh on Service[httpd]
Notice: /Stage[main]/Main/Node[nginx.cn7788.com]/Apache::Vhost[webrsync.cn7788.com]/File[/etc/httpd/conf.d/httpd_vhost_webrsync.cn7788.com.conf]/ensure: defined content as '{md5}5fa6f92aa7ebb670a08e6e0968df9be6'
Notice: Finished catalog run in 87.65 seconds

结果表示配置是成功的，整个过程耗时87.65秒。

注意

很多资料和文档都是复制/etc/httpd/conf/httpd.conf文件来作为httpd.conf.erb模板的，我觉得这种做法还是欠缺考虑的，一般来说，每台Aapche主机上面至少有一个基于域名的虚拟主机，有的更多，十几个也很常见，所以我们才需要用独立的虚拟主机文件来管理虚拟主机并自动载入（注意配置文件httpd.conf中存在着这么一行include conf.d/*.conf，这个指令的意思是指将conf.d目录下所有以.conf结尾的文件都引进来），这也是我们利用ERB模板文件将虚拟主机文件的定义路径放在/etc/httpd/conf.d目录下的原因。

4.8　如何利用ERB模板自动配置Nginx虚拟主机

对于已经上线的Web集群环境，如何才能方便快速地部署Nginx及其虚拟主机呢？要想实现这个需求我们可以参考4.7节的内容，在这里建议用第三方yum源来安装Nginx，如果是用Nginx的官方源来安装Nginx的话，我们可以查看下/etc/yum.repos.d/nginx.repo文件的内容，如下所示：

[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=0
enabled=1

第二种方式是通过createrepo建立自己的yum源，这种方式更加灵活，我们可以在Nginx官网上下载适合自己的rpm源码包，然后rebuild成rpm包添加进自己的yum源，在自动化运维要求严格的定制环境中，绝大多数运维人员都会选择这种方法。大家通过这种方式安装Nginx后会发现，确实比用源码安装Nginx方便多了，就像自动分配了运行Nginx的用户nginx：nginx。Nginx的日志存放会被自动保存在/var/log/Nginx下，其工作目录为/etc/nginx，这一点跟源码编译安装的Nginx区别很大，请大家注意甄别。

Puppet中的server.cn7788.com
机器的/etc/puppet的文件结构如下所示：

├──

auth.conf├──

environments│

└──

example_env│

├──

manifests│

├──

modules│

└──

README.environment├──

fileserver.conf├──

manifests│

├──

nodes│

│

├──

client.cn7788.com.pp│

│

└──

nginx.cn7788.com.pp│

└──

site.pp├──

modules│

└──

nginx│

├──

files│

├──

manifests│

│

└──

init.pp│

└──

templates│

├──

nginx.conf.erb│

└──

nginx_vhost.conf.erb└──

puppet.conf

（1）首先我们要建立Nginx模块，命令如下所示：

mkdir –

p /etc/puppet/modules/nginx/{files,manifests,templates}

（2）Nginx模块的配置文件挺多的，这里将贴出其详细的配置说明。

site.pp的文件内容如下：

import "nodes/*.pp"

client.cn7788.com.pp的文件内容如下所示：

node 'client.cn7788.com' {
include nginx
nginx::vhost {'client.cn7788.com':
sitedomain => "client.cn7788.com" ,
rootdir => "client",
}
}

nginx.cn7788.com.pp的文件内容如下所示：

node 'nginx.cn7788.com' {
include nginx
nginx::vhost {'nginx.cn7788.com':
sitedomain => "nginx.cn7788.com" ,
rootdir => "nginx",
}
}

/etc/puppet/modules/nginx/manifests/init.pp文件内容如下所示：

class nginx{
package{"nginx":
ensure =>present,
}
service{"nginx":
ensure =>running,
require =>Package["nginx"],
}
file{"nginx.conf":
ensure => present,
mode => 644,
owner => root,
group => root,
path => "/etc/nginx/nginx.conf",
content=> template("nginx/nginx.conf.erb"),
require=> Package["nginx"],
}
}
define nginx::vhost($sitedomain,$rootdir) {
file{ "/etc/nginx/conf.d/${sitedomain}.conf":
content => template("nginx/nginx_vhost.conf.erb"),
require => Package["nginx"],
}
}

/etc/puppet/modules/nginx/templates/nginx.conf.erb的文件内容如下所示：

user nginx;
worker_processes 8;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
use epoll;
worker_connections 51200;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}

我们检查下此ERB模板文件的语法，命令如下所示：

erb -x -T '-' -P /etc/puppet/modules/apache/templates/nginx.conf.erb | ruby -c

没有任何显示，这就说明文件在语法上是不存在任何问题的。

/etc/puppet/modules/nginx/templates/nginx_vhost_conf.erb文件是Nginx虚拟主机的ERB模板，其文件内容如下所示：

server {
listen 80;
server_name <%= sitedomain %>;
access_log /var/log/nginx/<%= sitedomain %>.access.log;
location / {
root /var/www/<%= rootdir %>;
index index.php index.html index.htm;
}
}

最后我们可以在节点名为client.cn7788.com
和nginx.cn7788.com
的机器上验证效果，命令如下所示：

puppet agent --test --server server.cn7788.com

这里以nginx.cn7788.com
进行举例说明，上述命令执行结果如下：

Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Caching catalog for nginx.cn7788.com
Info: Applying configuration version '1446798263'
Notice: /Stage[main]/Nginx/Package[nginx]/ensure: created
Notice: /Stage[main]/Nginx/Service[nginx]/ensure: ensure changed 'stopped' to 'running'
Info: /Stage[main]/Nginx/Service[nginx]: Unscheduling refresh on Service[nginx]
Notice: /Stage[main]/Main/Node[nginx.cn7788.com]/Nginx::Vhost[nginx.cn7788.com]/File[/etc/nginx/conf.d/nginx.cn7788.com.conf]/ensure: defined content as '{md5}5f08d10788e3c82b41336a40edc5350f'
Notice: /Stage[main]/Nginx/File[nginx.conf]/content:
--- /etc/nginx/nginx.conf 2015-04-21 15:34:33.000000000 +0000
+++ /tmp/puppet-file20151106-5957-1f964a8-0 2015-11-06 08:27:14.267072983 +0000
@@ -1,32 +1,22 @@
-
user nginx;
-worker_processes 1;
-
+worker_processes 8;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
-
-
events {
- worker_connections 1024;
+ use epoll;
+ worker_connections 51200;
}
-
-
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
-
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
-
access_log /var/log/nginx/access.log main;
-
sendfile on;
#tcp_nopush on;
-
keepalive_timeout 65;
-
#gzip on;
-
include /etc/nginx/conf.d/*.conf;
}
+
Info: Computing checksum on file /etc/nginx/nginx.conf
Info: FileBucket got a duplicate file {md5}f7984934bd6cab883e1f33d5129834bb
Info: /Stage[main]/Nginx/File[nginx.conf]: Filebucketed /etc/nginx/nginx.conf to puppet with sum f7984934bd6cab883e1f33d5129834bb
Notice: /Stage[main]/Nginx/File[nginx.conf]/content: content changed '{md5}f7984934bd6cab883e1f33d5129834bb' to '{md5}34e85800459aaf9b40ebfbdfa33614c0'
Notice: Finished catalog run in 42.19 seconds

我们在nginx.cn7788.com
的机器上检查生成的Nginx相关配置文件，发现都已经顺利生成了，这就说明Nginx模板配置是成功的。

4.9　小结

集中配置管理工具Puppet这个软件越来越成熟和强大了，它有着很好的发展前景，由于时间和环境的关系，本章只是简单介绍了自动化部署管理工具Puppet的安装、部署及平时工作中的常见用法，像Puppet的控制台产品Dashboad和Foreman都没有涉及，有兴趣的朋友可以结合工作实际尝试研究一下Puppet更高级的用法。

第5章　深度实践iptables

作者简介

胥峰，盛大游戏高级研究员，《Linux运维最佳实践》的作者。2006年毕业于南京大学，2011年加入盛大游戏。拥有十年运维经验，曾参与盛大游戏多款大型网络游戏的运维，主导统一运维平台的产品功能设计和实施，拥有工信部认证高级信息系统项目管理师资格。微信公众号“运维技术实践”、QQ技术交流群群号434242482。

在Linux运维工作中，每一个运维或许都使用过iptables进行网络安全设定。在涉及Linux安全时，我们或许会在第一时间想起iptables这个强大的工具。毋庸置疑的是，iptables在Linux中具有重要的地位。但是，我们对这个工具是否还有更深入的理解呢？我们是否知道在使用iptables的过程中应该注意的事项呢？我们是否知道iptables除了在安全方面的作用之外，它还可以实现更多的功能呢？本章将会针对以上的问题，逐一进行详细阐述。

本章由一些使用iptables中的经典案例开始，对iptables的状态追踪功能进行生动细致的讲解，对在iptables中限制ICMP协议时的注意事项运用案例进行讲解，然后介绍iptables在非安全方面的功能：网络地址转换。最后，作为总结，我们将以一张图来展示iptables中的各种表和链的作用时间。

5.1　禁用连接追踪

5.1.1　排查连接追踪导致的故障

1.问题描述

虚拟机用户在测试网络连通性时，发现连接到某主机的网络ping时断时续，丢包问题严重。同时，它在与该虚拟机同网段的Windows物理机上测试同一IP时，未发现该问题。

用户给我们提供的截图如图5-1所示。

图5-1　ping丢包截图

用户使用的测试脚本testping.sh内容如下：

#!/bin/bash
host=$1
wait=$2
if [ -z $host ]; then
echo "Usage: `basename $0` [HOST]"
exit 1
fi
if [ -z $wait ]; then
wait=1
fi
let index=1
let lost=0
while :; do
result=`ping -W 1 -c 1 $host | grep 'bytes from '`
if [ $? -gt 0 ]; then
echo -e "$lost/$index - `date +'%Y/%m/%d %H:%M:%S'` - host $host is \033
[0;31mdown\033[0m"
let lost=$lost+1
else
echo -e "$lost/$index - `date +'%Y/%m/%d %H:%M:%S'` - host $host is \033[0;
32mok\033[0m -`echo $result | cut -d ':' -f 2`"
sleep $wait # avoid ping rain
fi
let index=$index+1
done

用户执行以下命令进行测试：

sh testping.sh xxx.yyy.zzz.76

2.排查过程

在收到故障申报后，我们首先分析了系统日志/var/log/messages，发现在对应的时间点，有关于nf_conntrack的报错。报错内容如下：

Dec 30 17:19:02 gcloud-whcq-ISpeaker-198 kernel: __ratelimit: 877 callbacks suppressed
Dec 30 17:19:02 gcloud-whcq-ISpeaker-198 kernel: nf_conntrack: table full, dropping packet.
Dec 30 17:19:02 gcloud-whcq-ISpeaker-198 kernel: nf_conntrack: table full, dropping packet.
Dec 30 17:19:02 gcloud-whcq-ISpeaker-198 kernel: nf_conntrack: table full, dropping packet.
Dec 30 17:19:02 gcloud-whcq-ISpeaker-198 kernel: nf_conntrack: table full, dropping packet.
Dec 30 17:19:02 gcloud-whcq-ISpeaker-198 kernel: nf_conntrack: table full, dropping packet.
Dec 30 17:19:02 gcloud-whcq-ISpeaker-198 kernel: nf_conntrack: table full, dropping packet.
Dec 30 17:19:02 gcloud-whcq-ISpeaker-198 kernel: nf_conntrack: table full, dropping packet.
Dec 30 17:19:07 gcloud-whcq-ISpeaker-198 kernel: __ratelimit: 1356 callbacks suppressed
Dec 30 17:19:07 gcloud-whcq-ISpeaker-198 kernel: nf_conntrack: table full, dropping packet.

我们使用如下2个命令可以看出，服务器上的已有连接追踪条目数量已接近于我们配置的最大追踪数，这一点进一步确认了丢包问题是由连接追踪导致的。

# sysctl net.netfilter.nf_conntrack_max
net.netfilter.nf_conntrack_max = 65536
# sysctl net.netfilter.nf_conntrack_count
net.netfilter.nf_conntrack_count = 65000

5.1.2　分析连接追踪的原理

概要地说，连接追踪系统在一个内存数据结构中记录了连接的状态，这些信息包括源IP、目的IP、双方端口号（对TCP和UDP）、协议类型、状态和超时信息等。有了这些信息，我们就可以设置更灵活的过滤策略。

注意

连接追踪系统本身不进行任何过滤动作，它只是为上层应用（如iptables）提供基于状态的过滤功能。

下面来看一个实际的例子（通过cat/proc/net/nf_conntrack命令可以查看当前连接追踪的表）：

ipv4 2 tcp 6 62 SYN_SENT src=xxx.yyy.19.201 dst=87.240.131.117 sport=24943 dport=443 [UNREPLIED] src=87.240.131.117 dst=xxx.yyy.19.201 sport=443 dport=24943 mark=0 secmark=0 use=2
#该条目的意思是：系统收到了来自

xxx.yyy.19.201:24943发送到

87.240.131.117:443的第一个

TCP SYN包，但此时对方还没有回复这个

SYN包（

UNREPLIED）

ipv4 2 tcp 6 30 SYN_RECV src=106.38.214.126 dst=xxx.yyy.19.202 sport=18102 dport=6400 src=xxx.yyy.19.202 dst=106.38.214.126 sport=6400 dport=18102 mark=0 secmark=0 use=2
#该条目的意思是：系统收到了来自

106.38.214.126:18102发送到

xxx.yyy.19.202:6400的第一个

TCP SYN包

ipv4 2 tcp 6 158007 ESTABLISHED src=xxx.yyy.19.201 dst=211.151.144.188 sport=
48153 dport=80 src=211.151.144.188 dst=xxx.yyy.19.201 sport=80 dport=48153 [ASSURED] mark=0 secmark=0 use=2
#该条目的意思是：

xxx.yyy.19.201:48153<-->211.151.144.188:80之间的

TCP连接是

ESTABLISHED状态，这个连接是被保证的（

ASSURED，不会因为内存耗尽而丢弃）

该表中的数据提供的状态信息，可以使用iptables的state模块进行状态匹配，进而执行一定的过滤规则。目前iptables支持基于以下4种状态的过滤规则：INVALID、ESTABLISHED、NEW和RELATED。

启用连接追踪后，在某些情况下，设置iptables会变得比较简单，如图5-2所示。

图5-2　主动访问外网服务的TCP3次握手示意图

当我们的服务器需要主动访问https://www.amazon.com
提供的接口时，3次握手的过程如图5-2所示。在基于状态进行iptables设置时，使用如下的规则即可：

iptables -A INPUT -p tcp -m state --state ESTABLISHED -j ACCEPT # rule1
iptables -A OUTPUT -p tcp -j ACCEPT # rule2

工作流程如下：

（1）第1个包①匹配到规则rule2，允许。

（2）第2个包②因为在nf_conntrack表中有如下的规则匹配到rule1，允许：

ipv4 2 tcp 6 431995 ESTABLISHED src=172.30.16.1 dst=54.239.25.200 sport=50611 dport=443 src=54.239.25.200 dst=172.30.16.1 sport=443 dport=50611 [ASSURED] mark=0 secmark=0 use=2

（3）第3个包③匹配到规则rule2，允许。

5.1.3　禁用连接追踪的方法

通过以上的分析，我们知道在进行大量网络传输连接的时候，启用连接追踪可能会导致网络丢包、TCP重传等问题，因此，我们需要在适用的情况下禁用连接追踪。

禁用连接追踪的方法有3种，具体如下。

（1）在内核中禁用Netfilter Connection tracking support。

编译内核时，依次进入Networking support→Networking options→Network packet filtering framework（Netfilter）→Core Netfilter Configuration，禁用的方法如图5-3所示（取消选中Netfilter connection tracking support）。

这样编译出来的内核，将不支持连接追踪的功能，也就是不会生成以下的ko文件了：

kernel/net/netfilter/nf_conntrack.ko
kernel/net/netfilter/nf_conntrack_proto_dccp.ko
kernel/net/netfilter/nf_conntrack_proto_gre.ko
kernel/net/netfilter/nf_conntrack_proto_sctp.ko
kernel/net/netfilter/nf_conntrack_proto_udplite.ko
kernel/net/netfilter/nf_conntrack_netlink.ko
kernel/net/netfilter/nf_conntrack_amanda.ko
kernel/net/netfilter/nf_conntrack_ftp.ko
kernel/net/netfilter/nf_conntrack_h323.ko
kernel/net/netfilter/nf_conntrack_irc.ko
kernel/net/netfilter/nf_conntrack_broadcast.ko
kernel/net/netfilter/nf_conntrack_netbios_ns.ko
kernel/net/netfilter/nf_conntrack_snmp.ko
kernel/net/netfilter/nf_conntrack_pptp.ko
kernel/net/netfilter/nf_conntrack_sane.ko
kernel/net/netfilter/nf_conntrack_sip.ko
kernel/net/netfilter/nf_conntrack_tftp.ko
kernel/net/netfilter/xt_conntrack.ko
kernel/net/ipv4/netfilter/nf_conntrack_ipv4.ko
kernel/net/ipv6/netfilter/nf_conntrack_ipv6.ko

图5-3　编译内核时禁用连接追踪的方法

此时，在iptables中不能再使用NAT功能，同时也不能再使用-m state模块了，否则会产生如下的报错信息：

[root@localhost ~]# iptables -t nat -A POSTROUTING -o eth0 -s 172.30.4.0/24 -j SNAT --to 172.30.4.11
iptables v1.4.7: can't initialize iptables table `nat': Table does not exist (do you need to insmod?)
Perhaps iptables or your kernel needs to be upgraded.
[root@localhost ~]# iptables -I INPUT -p tcp -m state --state NEW -j ACCEPT
iptables: No chain/target/match by that name.

（2）在iptables中，禁用-m state模块，同时在filter表的INPUT链中显式指定ACCEPT。

以图5-2为例，在满足这样的访问需求时，我们使用的iptables必须修改为如下的内容：

iptables -A INPUT -p tcp -s 54.239.25.200 --sport 443 -j ACCEPT # rule1
iptables -A OUTPUT -p tcp -j ACCEPT # rule2

同时，在/etc/init.d/iptables中，修改如下的内容：

修改前：

NF_MODULES_COMMON=(x_tables nf_nat nf_conntrack) # Used by netfilter v4 and v6修改后：

NF_MODULES_COMMON=(x_tables) # Used by netfilter v4 and v6

（3）在iptables中，使用raw表指定NOTRACK。

iptables -t raw -A PREROUTING -p tcp -j NOTRACK
iptables -t raw -A OUTPUT -p tcp -j NOTRACK
iptables -A INPUT -p tcp -s 54.239.25.200 --sport 443 -j ACCEPT # rule1
iptables -A OUTPUT -p tcp -j ACCEPT # rule2

在以上的3种方法中，大家可以根据自己的业务情况，参考实施其中的一种。

注意

（1）对于使用NAT功能的服务器来说，不能禁用连接追踪。

（2）对于FTP的被动模式，在FTP服务器上需要显式地打开需要进行数据传输的端口范围。关于主动FTP和被动FTP的内容，大家可以参考相关资料，本书不再赘述。

在配置了NAT的服务器上，不能禁用连接追踪，此时可以使用如下的方法来提高连接追踪的条目上限。在/etc/sysctl.conf中，新增如下的内容：

net.nf_conntrack_max = 524288
net.netfilter.nf_conntrack_max = 524288

新增配置文件/etc/modprobe.d/netfilter.conf，内容如下：

options nf_conntrack hashsize=131072

执行以下的命令使其生效：

/etc/init.d/iptables restart #重新加载连接追踪模块，同时更新

nf_conntrack配置

hashsize
sysctl -p #使得修改的

sysctl.conf中

nf_conntrack的上限提高

系统nf_conntrack_max的值，在未指定时，可根据以下公式计算得出：

nf_conntrack_max = nf_conntrack_buckets * 4

系统nf_conntrack_buckets的值，在未指定时，可根据以下公式计算得出：

在系统内存大于等于

4GB时，

nf_conntrack_buckets = 65536在系统内存小于

4GB时，

nf_conntrack_buckets = 内存大小

/ 16384

在本案例中，我们使用options nf_conntrack hashsize=131072自主指定了Buckets的大小。

Buckets和连接追踪表的关系如图5-4所示。

图5-4　Buckets和连接追踪表的关系

为Buckets设置一个合理的值（一般为预计的连接追踪表上限的1/4），使得连接追踪表的定位效率最高。

5.1.4　确认禁用连接追踪的效果

我们在禁用了连接追踪之后，可以使用如下的两个方法来验证禁用的效果：

（1）确认/var/log/messages内容中不再出现table full的报错信息。

（2）检查lsmod|grep nf_conntrack的输出，确认没有任何输出即可。

如果是在NAT服务器上，则需要执行以下的命令来检查效果：

sysctl net.netfilter.nf_conntrack_max #确认该值是我们修改后的结果

sysctl net.netfilter.nf_conntrack_count #确认该值能够突破出现问题时的最大追踪数

5.2　慎重禁用ICMP协议

5.2.1　禁用ICMP协议导致的一则故障案例

1.问题描述

我们负责维护的某系统分布于多个机房之中，之前文件传输一直走的公网，这是很正常的（架构如图5-5所示）。

后来建成了大内网（使用GRE VPN技术在Internet上组建的私有网络，目前通过北京电信通转发，使电信和联通之间的互访变得更快速），为了安全和快速，文件传输改走大内网（架构如图5-6所示）。

图5-5　通过公网传输正常

图5-6　通过大内网传输失败

结果碰到了奇怪的问题：使用scp或wget通过大内网传输文件时，只能传输1KB大小左右的文件，稍大一点的文件，比如2KB以上的文件，在传输中就被卡住了。当停止iptables后，则可以传输大文件。更奇怪的事还在后面，再次启用iptables，大约在10分钟内仍然可以传输大文件，但超过10分钟后，上面的问题又会重现。

2.排查过程

在启动iptables之后，我们进行抓包。

在抓包中（文件：ICMP_Fragmentation_Needed.pcap），我们看到了有ICMP的报错信息（如图5-7中的
所示的数据帧）。

图5-7　ICMP报错信息

编号为20的数据帧，是服务器10.10.60.69向网卡提交了长度为2974的数据帧（该网卡支持TSO，进行自动分片传输到网络上）后，在编号为21的数据帧中，被路由器10.10.251.2返回了ICMP Destination unreachable（Fragmentation needed）的信息。

ICMP信息的具体内容如图5-8所示。

图5-8中，
是ICMP类型，
是该类型ICMP的错误代码，
是通知10.10.60.69（发送方）应该使用的MTU（1400字节），
和
是引起这个ICMP的数据帧的源IP和目的IP（IP层信息），
和
是引起这个ICMP的数据帧的源端口和目的端口（TCP层信息），
是引起这个ICMP的数据帧的TCP序列号。
、
、
、
、
正好与图5-7中的编号为20的数据帧相匹配。

图5-8　ICMP信息内容

而这个ICMP的信息，正好被iptables给过滤了。因为我们只开放了ICMP的以下类型为允许：

iptables -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT

5.2.2　MTU发现的原理

通过以上的分析，我们认识到有一类ICMP专门用于通知MTU信息，那么什么是MTU呢？

MTU（Maximum Transmission Unit，最大传输单元）是指一种通信协议的某一层所能通过的最大数据包大小（以字节为单位）。由于以太网传输电气方面的限制，每个以太网帧最大不能超过1518字节，刨去以太网帧的帧头（源MAC+目标MAC+Type+CRC）18字节，那么剩下承载上层协议的Data域（IP头+TCP头+应用数据）最大就只能有1500字节（如图5-9所示），我们称该值为MTU。

图5-9　以太网数据帧封装结构图

以下是一些与MTU密切相关的概念。

GRE：通用路由封装协议（Generic Routing Encapsulation），规定了如何用一种网络协议去封装另一种网络协议的方法，它是一种应用非常广泛的第三层VPN隧道协议。

MSS：最大分段尺寸（Maximum Segment Size），是TCP数据包每次能够传输的最大数据分段。这个值等于MTU减去IP数据包包头的20字节和TCP数据段包头的20字节，所以MSS一般为1460字节。TCP协议在建立连接的时候双方将会协商本次通信使用的MSS值。

通过图5-10，我们可以看到以太网中MTU与MSS、以太网数据帧大小的关系。

图5-10　MTU与MSS、以太网数据帧关系图

PMTU：路径最大传输单元（Path Maximum Transmission Unit）。在因特网上，两台主机之间的通信要经过多个网络，而每个不同的网络在IP层可能会有不同的MTU。两台通信主机路径中的最小MTU被称作路径MTU（PMTU）。默认情况下，PMTU的老化时间是10分钟。我们可以通过配置PMTU的老化时间来更改PMTU项在缓存中的时间。在Linux中，我们可以使用如下的命令检查该老化时间：

sysctl net.ipv4.route.mtu_expires

PMTUD：路径最大传输单元发现（Path MTU Discovery）。通过在IP首部中设置“不要分片位（Don't Fragment，DF）”，来确定当前路径的路由器是否需要对正在发送的IP数据报进行分片。如果一个待转发的IP数据报被设置成DF比特，而其长度又超过了MTU，那么路由器就会丢弃这个报文，并返回一个ICMP不可达的差错报文（类型为3、代码为4：需要进行分片但设置了不分片位），其中填有下一跳正确的MTU。如果发送端主机接收到这个ICMP差错控制报文，它就可以调整使用正确的MTU重新传送这个报文，并在以后的传输中沿用这个MTU大小。PMTUD的工作流程如图5-11所示。

图5-11　PMTUD工作流程

为什么图5-8中的
提示下一跳的MTU是1400呢？

公司的大内网使用了GRE VPN技术。GRE隧道需要对IP包进行再封装，这样就额外增加了GRE报文头（4字节）+外层IP报文头（20字节），总共24字节。而以太网默认的MTU为1500字节，减去GRE封装的24字节，因此VPN网关的MTU应该是1476字节了。在VPN路由器上，网络管理员将MTU手动减少到1400字节。因此，服务器10.10.60.69发出MTU为1500字节的报文时，就会被路由器返回ICMP的报错，并通知服务器以1400字节的MTU重新发包。

表5-1是常见网络环境下的一些MTU值。

表5-1　常见网络环境下的MTU值

5.2.3　解决问题的方法

在iptables中，增加以下的条目：

iptables -A INPUT -p icmp --icmp-type fragmentation-needed -j ACCEPT

在这个案例中，我们可以看到，如果简单地禁止iptables会导致MTU协商不成功，从而引发网络传输的问题。因此，在实践中，我们建议不要完全禁止iptables，至少应该打开以下的访问权限：

iptables -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
iptables -A INPUT -p icmp --icmp-type fragmentation-needed -j ACCEPT

5.3　网络地址转换在实践中的案例

在实践中，iptables除了可用于网络安全之外，还经常用于网络地址转换（NAT）的环境中。网络地址转换分为源地址转换（源地址NAT）和目的地址转换（目的地址NAT）。

5.3.1　源地址NAT

源地址NAT，主要用于如图5-12所示的网络示意图中无外网IP的服务器（Server B）需要访问互联网的场景。

在图5-12中，Server B没有外网IP，若其需要访问互联网，则需要进行如下的设置。

图5-12　网络地址转换的网络示意图

（1）在服务器Server B上，指定其网络的默认网关是10.128.70.112（即Server A的内网地址）。

（2）在服务器Server A上，启用路由功能。启用的方法是执行以下的命令：

sysctl -w net.ipv4.ip_forward=1

（3）在Server A上，设置iptables规则如下：

iptables -t filter -A FORWARD -j ACCEPT
iptables -t nat -A POSTROUTING -o eth0 -j SNAT --to x.y.z.173 #eth0是

Server A的外网网卡，

x.y.z.173是

Server A的外网

IP

经过以上3个步骤的设置之后，Server B将会通过Server A访问互联网。此时，在互联网上看到的源地址将是Server A的外网IP。

以Server B访问8.8.8.8的DNS服务为例，其数据流程如下。

（1）在Server B上，网络层数据包的格式为：目的地址IP 8.8.8.8，源地址IP 10.128.70.111。

（2）在Server A上经过源地址NAT后的网络层数据包格式为：目的地址IP 8.8.8.8，源地址IP x.y.z.173。该转换条目被记录在/proc/net/nf_conntrack中。

（3）8.8.8.8的响应（源地址IP 8.8.8.8，目的地址IP x.y.z.173）到达Server A之后，Server A将改写网络层数据包为源地址IP 8.8.8.8，目的地址IP 10.128.70.111。

这就是源地址NAT的工作过程。

5.3.2　目的地址NAT

目的地址NAT用于如图5-12所示的网络示意图中，外部用户直接访问无外网IP的服务器（Server B）提供的服务。例如，外部用户希望通过互联网访问到Server B上的Oracle数据库（监听端口是TCP 1521）时，我们可以使用如下的命令在Server A上进行目的地址NAT设置：

iptables -t nat -A PREROUTING -d x.y.z.173 -p tcp -m tcp --dport 1521 -j DNAT --to-destination 10.128.70.111:1521 #改写目的地址为

10.128.70.111，目的端口为

1521
iptables -t nat -A POSTROUTING -d 10.128.70.111 -p tcp -m tcp --dport 1521 -j SNAT --to-source 10.128.70.112 #改写源地址

IP为

Server A的内网

IP，此时与

Server B相当于是和

Server A进行通信

网络地址转换是运维人员在工作中经常会用到的技术，我们需要非常熟悉源地址转换和目的地址转换这两种方案。

5.4　深入理解iptables的各种表和各种链

通过以上几节的实践我们知道，iptables为系统工程师提供了强大的包过滤功能和NAT网络地址转换功能。在Linux中，为iptables提供这些功能的底层模块是netfilter框架。netfilter是Linux内核中的一系列钩子（hook），它为内核模块在网络栈中的不同位置注册回调函数（callback function）提供了支持。数据包在协议栈中将依次经过这些处于不同位置的回调函数的处理。

下面我们以图5-13讲解netfilter钩子与iptables各种表和链的处理顺序：

图5-13　netfilter钩子与iptables各种表和链的处理顺序图

netfilter提供了5个钩子可以供程序去注册。在数据包经过网络栈的时候，这些钩子上注册的内核模块将依次被触发。这5个钩子的处理时间分别如下。

NF_IP_PRE_ROUTING：在数据流量进入网络栈后立即被触发，这个钩子上注册的模块在路由决策前即被执行。如图5-13中
所示的阶段。

NF_IP_LOCAL_IN：在路由判断确定了包是发送到本机时执行这个钩子。如图5-13中
所示的阶段。

NF_IP_FORWARD：在路由判断确定了包需要转发给其他主机时执行这个钩子。如图5-13中的
所示的阶段。

NF_IP_LOCAL_OUT：在本机进程产生的网络被送到网络栈上时执行这个钩子，如图5-13中
所示的阶段。

NF_IP_POST_ROUTING：在数据包经过路由判断后即将发送到网络前执行这个钩子。如图5-13中
所示的阶段。

iptables中有5个链（chain），分别如下。

PREROUTING：NF_IP_PRE_ROUTING钩子触发。

INPUT：NF_IP_LOCAL_IN钩子触发。

FORWARD：NF_IP_FORWARD钩子触发。

OUTPUT：NF_IP_LOCAL_OUT钩子触发。

POSTROUTING：NF_IP_POST_ROUTING钩子触发。

iptables中有5种表（table），分别如下。

·filter表：filter表是iptables中使用最广泛的表，这个表的作用是进行过滤，也就是由这个表来决定一个数据包是否继续它的目的地址，或者被拒绝。

·nat表：顾名思义，这个表是进行网络地址转换用的，如本章5.3节的“网络地址转换在实践中的案例”所示，可以改变数据包的源地址或目的地址。

·mangle表：mangle表用于修改IP的头部信息，如修改TTL（Time to Live）。

·raw表：raw表为iptables提供了一种不经过状态追踪的机制，在大流量对外业务的服务器上使用这个表可以避免状态追踪带来的性能问题。如本章5.1节的“禁用连接追踪”中的案例所示。

·security表：提供在数据包中加入SELinux特性的功能。该表一般用得不多，本章将不再叙述这一部分的内容。

通过以上的分析，我们知道netfilter只有5个钩子，而iptables有5个链和5种表，由此可见在一个钩子上可能有多个表的不同链需要处理，如图5-13中的raw表、mangle表、nat表都有POSTROUTING链，这些链根据自己向内核注册时的优先级（priority）依次进行处理。

5.5　小结

Linux中的iptables既是强大的网络安全工具，又是网络地址转换工具。本章重点剖析了连接追踪的机制及使用中的注意事项、提出了慎重禁用ICMP协议的论点并进行了分析。对iptables在网络地址转换中的两种经典使用场景进行了配置案例的说明。作为总结部分，我们给出了iptables中各种表和链的作用和关系，希望能帮读者形成一个清晰的图像，在分析iptables问题时能够参照这个图像进行快速定位。

第6章　使用systemd管理Linux系统服务

作者简介

尹会生，金山西山居高级系统工程师。拥有多年的企业集群解决方案培训经验和内核调优经验，擅长高性能和高可用性集群技术。近四年专注于Hadoop集群、Spark集群在推荐系统和BI相关领域的解决方案。曾历任北京尚观科技高级讲师、新浪研发中心技术经理等职。在新浪广告、微博广告、西山居大数据平台架构中担任关键角色，并提供咨询服务。

随着Red Hat Enterprise 7的发布和Ubuntu系统的普及，systemd开始逐渐进入到运维工程师的视线，从启动过程到服务管理，再到常用的计划任务、日志等日常运维工具都有systemd的影子，它对运维工程师的影响越来越大，这里就为大家介绍一下systemd在Linux日常管理中的使用。

6.1　systemd和sysVinit之间的关系

systemd是优秀的进程管理工具，在CentOS 5.x版本下管理进程的这类工具称为sysVinit，管理员经常用的service、chkconfig等命令就是sysVinit提供的。但是systemd与sysVinit不同，它能够从系统的启动过程就开始进程的管控，对非终端依赖型的程序（服务）进行配置，而且还提供了很多进程通信和进程调用的简便用法；这里以CentOS的发行版本为例来为大家讲解，在启动过程中systemd和sysVinit的区别。

6.1.1　sysVinit方式下系统的启动特点

运维工程师最熟悉的、也是目前应用最为广泛的生产环境的系统就是CentOS 5.x和CentOS 6.x了，sysVinit是Linux操作系统上非常经典的系统初始化和进程管理工具，甚至在实际在工作中，很多熟练的运维工程师会使用Shell脚本来编写自己的服务启动脚本，以方便应用程序在系统初始化时完成启动，从而简化运维工作。但是我在长时间的使用中也发现了如下一些问题：

（1）服务只能按照之前编排好的顺序来执行，这会使启动过程变得缓慢，而由于人为失误会导致某些服务在启动的时候“hang住”，操作系统不能继续引导，从而导致无法正常开机和关机，我们希望能够通过简单的配置就可以绕过这种加载不正常的服务，而不是用Shell来做各种异常的判断。

（2）不能根据硬件状态的改变，动态调整服务的启动和停止。无论硬件设备是否被激活，所有的启动服务都要被加载，不够灵活。

随着Linux的不断发展，越来越多的人将Linux安装到个人设备上，这时sysVinit在个人系统上的问题变得尤为突出，因此另一个重要的桌面发行版Ubuntu操作系统的技术人员，开发了Upstart方式来代替sysVinit方式。Upstart支持分组顺序启动的方式，即将有依赖关系的服务分到相同的组，在组内按照顺序进行引导，以达到并行启动的目的。但是这种方式依然不是启动速度最快的。

6.1.2　systemd方式下系统的启动特点

为了进一步提升系统启动的速度，以及方便对服务器进行管理，CentOS 7采用了更先进的systemd来替代sysVinit和Upstart，而且管理命令和启动脚本的编写与sysVinit还可以兼容，降低了运维工程师的学习成本，所以之前在CentOS 5、CentOS 6上可以正常运行的服务启动脚本能够无缝迁移到CentOS 7上运行。

那么systemd有哪些新的特性呢？其最大的特点就是可以真正地并行启动了。这种并行启动的方式和Upstart
[1]

不同，不需要用户来解决依赖关系，例如：在系统启动的过程中，都会有依赖关系的问题，像是Apache、Nginx这类的服务在启动之前要判断端口是否被占用，这就需要获得连接到网络接口的IP地址，如果网络服务没有启动，就会导致依赖它的服务启动失败。在真正的启动过程中，会出现更复杂的依赖，即多层依赖，如A服务依赖B，B服务依赖C，以至于更多的依赖关系，Upstart启动的过程就会要求服务必须按照C→B→A的启动顺序才能正确运行。那么，一旦C服务启动缓慢就会导致B和A这两个服务进入等待状态，从而导致系统启动缓慢。而systemd可以让A、B、C三个服务并行启动起来，而且还可以支持按需启动。

那么，什么是按需启动呢？按需启动是指服务在没有被其他服务依赖时，是不会随着系统启动一起被加载的，这样就可以大大缩短启动的时间。这么“神奇”的功能是怎么实现的呢？首先要解决的就是依赖关系，systemd作者总结出了三种服务依赖关系：

（1）端口依赖，即服务A若正常启动，则需要先连接服务B的端口。解决这种依赖需要systemd先“制作”一个端口，当程序A启动时，systemd检测到对该端口的socket请求，然后检测B服务是否启动了，如果没有启动则对A服务的请求进行缓存，直到程序B运行为止，这样就解决了端口依赖的问题。而且还很体贴地为用户提供了通过systemd拉起B服务的功能，进一步减少用户的工作量。

（2）程序通信的依赖，systemd采用了D-Bus作为程序之间的通信工具，在启动过程中，一旦被依赖的服务程序没有运行，同样会将请求进行缓存直到被依赖的程序运行为止。

（3）文件系统依赖，除了“/”目录必须串行启动之外，其他被程序依赖的目录都可以采用和autofs相同的触发机制，即先创建临时挂载点（一个空目录），一旦有服务程序使用到这个挂载点则触发挂载的系统调用，将真正的目录挂载到事先定义好的挂载点，这就是systemd能进行并行启动的原因。

有了这三种解决依赖关系的方法之后，可以将启动过程中产生的依赖关系先缓存到内存中，然后继续启动下一个服务；systemd就是这样让系统启动过程并行运行起来的，最后通过多CPU核心来并行处理，从而缩短了启动的时间。接下来我们来具体分析各种启动方式在系统底层都做了哪些工作。

[1]
Upstart是一个基于事件的init守护进程的替代工具，用于在系统启动时进行程序的管理。由Canonical公司的前雇员Scott James Remnant开发，最初用于Ubuntu发行版。

6.2　systemd的原理和启动顺序

6.2.1　sysVinit的启动顺序

既然systemd由sysVinit进化而来，因此为了更好地掌握systemd的原理和启动过程，首先我们详细分析一下sysVinit的启动方式。目前我所在的公司还有很多业务运行在CentOS 5系统上面，该版本也是用于生产环境非常稳定的发行版本。下面我们以典型的CentOS 5系统为例，为大家介绍sysVinit体系下的启动顺序。

（1）在Linux系统的启动过程中，首先由BIOS加电自检，之后将引导功能交给GRUB进行引导，用户可以根据界面提示来自由地选择不同版本的内核。

（2）内核在引导完成之后，会加载第一个进程init，这时就正式进入了sysVinit的引导环境，init进程在磁盘的“/sbin/init”这个位置。之后所有的进程都是由它派生（fork）出来的，它的PID永远为1。

（3）init程序运行时会加载配置文件/etc/inittab，在配置文件中根据指定的参数来确定自己的启动级别（runlevel），不同的启动级别可用来定义一组不同服务的启动顺序，在默认情况下会有0~6共7个启动级别，也就是说用户可以定义7种（实际上是5种）不同的启动方式，以保证用户可以灵活地进行服务启动顺序的设置。默认情况下，在生产环境中我们将启动级别设置为3，其他启动级别的用途将在6.2.2节为大家详细介绍。

（4）init程序会按照默认的级别来逐个加载一组服务，首先会加载各个外设的硬件驱动，然后加载基础服务，如网络参数、蓝牙、软件raid、LVM逻辑等服务，之后再加载其他的应用服务。全部加载完成之后会进入等待用户登录的界面，来完成一次启动过程。

这就是sysVinit的启动过程，引导过程大量使用了Shell脚本，以方便运维工程师分析启动过程中都执行了哪些操作，这种简单又直接的启动方式很适合运维工程师去理解启动的过程，但启动效率却很低。

sysVinit的启动过程如图6-1所示：

图6-1　sysVinit的启动过程

为了便于对比不同的启动程序之间的差异，下面将更具体地分析下/etc/inittab文件中的内容。

（1）init进程加载/etc/inittab配置文件的部分，获得运行级别后并不会根据级别立即启动这组服务程序，init进程会为系统执行初始化动作。这个初始化动作是由/etc/inittab的下列选项来完成的：

si::sysinit:/etc/rc.d/rc.sysinit

它的含义是在系统启动各个服务程序之前执行/etc/rc.d/rc.sysinit脚本，这个脚本是串行地将主机名、文件系统、swap、SELinux、udev、内核参数、系统时钟、RAID、LVM等服务启动时可能会依赖的功能先运行起来，然后再加载指定级别的服务。如果我们测量一次完整的启动时间，会发现rc.sysinit脚本的运行时间占用了大部分的启动时间，因为它为后面的各级别服务准备了基础环境，而不关心后面的服务是否会用到这些资源，从而造成了大量的启动时间的浪费。

（2）加载服务又是怎样实现的呢？可以通过设置/etc/inittab配置文件的“id：3：initdefault：”这一行语句来更改系统默认的运行级别。在配置文件中还有这样的内容：

l0:0:wait:/etc/rc.d/rc 0
l1:1:wait:/etc/rc.d/rc 1
l2:2:wait:/etc/rc.d/rc 2
l3:3:wait:/etc/rc.d/rc 3
l4:4:wait:/etc/rc.d/rc 4
l5:5:wait:/etc/rc.d/rc 5
l6:6:wait:/etc/rc.d/rc 6

这里是一个分支，用于保证系统会加载到一组指定的服务，而不是加载全部的服务。而前面的wait标志，会让系统逐个加载服务直到加载完所有的服务后才继续下一步的启动。

根据initdefault给定的级别（这里是级别3），执行相应的服务。系统会执行/etc/rc.d/rc 3的Shell脚本，下面将跟踪/etc/rc.d/rc这个脚本，主要逻辑如下：

runlevel="$1" # 获取命令的参数

for i in /etc/rc$runlevel.d/K* ; do
#进入

/etc/rc3.d/ 目录，依次执行以

K00~

K99开头的程序并增加

stop 参数

$i stop
done
for i in /etc/rc$runlevel.d/S* ; do
# 进入

/etc/rc3.d/ 目录，依次执行以

S00~

S99开头的程序并增加

start 参数

$i start
done

打开/etc/rc1.d到/etc/rc5.d目录会发现大部分服务是以S开头的符号链接，而rc0.d和rc6.d下则是大量的以K开头的符号链接。运维工程师通过chkconfig命令就可以改变这些符号链接开头的S或K标记。而S或K标记后面的两位数字就是用来确定启动顺序的。

从这个逻辑我们不难发现，服务启动的过程是顺序完成的，而且在前一个服务没有执行完成之前，后面的程序都要等待；利用这种方式启动服务的好处还是简单，但这一过程会让现代计算机（多核并发）的机制产生严重的资源浪费，因为仅有单线程在工作。鉴于上面的这种顺序引导的情况，CentOS 6采用了Upstart工具来代替sysVinit进行引导，Upstart对rc.sysinit脚本做了大量的优化，它将启动需要做的每一个服务都封装成一个任务（job），并且为每一个任务增加一个状态，在启动时制定一些状态，并根据状态来确认每个任务运行到哪一步了，然后使用事件（event）机制和其他任务进行通信，这样就缩短了系统初始化时的启动时间。

有了Upstart之后，我们在同样的硬件环境下进行了启动时间的比较，发现启动一套标准的CentOS 6系统要比CentOS 5系统快很多。但是我们还发现如果在服务引导的部分还有可以优化的余地，即rc3.d部分引导服务的时候仍然是串行的，那么我们还可以对这部分缩短启动时间，接下来我们看看systemd是怎么做的。

6.2.2　systemd的启动顺序

CentOS 5、CentOS 6加载一组服务通常称作runlevel（运行级别）。而在systemd启动的过程中，将使用target（目标）取代runlevel，这是启动过程中最大的变化。

默认的runlevel从0~6共7个级别，可以在运行时使用runlevel命令来查询当前的运行级别，通过init命令切换运行级别。

·级别0和级别6分别表示关机和重启，它们都会调用/etc/rc$runlevel.d/目录下以K开头的服务，按照顺序调用service<服务名称>stop来关闭服务；唯一的区别就是级别0最后会调用halt命令关机，而级别6会调用reboot命令重启：

[user@localhost ~]$ runlevel
N 3

·级别1会在系统启动时加载/etc/rc1.d/S开头的服务，这些服务只包含启动必需的服务，只会加载bash而不进行用户身份的校验，该级别可用于服务器异常或忘记管理员（root）密码时的紧急救援工作。

·级别2会在级别1的基础上增加除网络和网络服务之外的其他所有服务，其最初是为无网络功能的桌面主机设计的，期望通过减少加载网络的功能，来加快启动速度。但现在网络已经成为使用Linux的必备所需，所以级别2很少用于生产环境中。

·级别3是用于生产环境最多的级别，除了KDE、GNOME桌面环境之外的其他级别都会加载。而级别5则是将桌面环境一起启动，普遍用于娱乐图形和终端用户环境中。

·级别4为用户自定义。

对于以上的7个级别，我们一般会在生产环境中采用级别3来加载服务，运维工程师会根据自己的业务来调整哪些服务需要在开机时就加载。但是在设置自己的服务过程中一旦有依赖关系就要管理员自行判断了，例如想要关闭network服务，就需要使用如下命令：

[user@localhost ~]$ chkconfig --level 3 network off

但其他依赖network服务的命令在下次启动的时候会失败，需要管理员根据经验自行判断，这点极易造成系统无法正常启动，而且灵活性也较低。

而在systemd中，target默认有40多种组成方式，当系统启动时，会根据配置文件的关键字来确定依赖关系并进行加载。下面是runlevel和常用的target对比：

sysVinit运行级别（

runlevel）

systemd目标（

target）

0 runlevel0.target -> poweroff.target
1 runlevel1.target -> rescue.target
2 runlevel2.target -> multi-user.target
3 runlevel3.target -> multi-user.target
4 runlevel4.target -> multi-user.target
5 runlevel5.target -> graphical.target
6 runlevel6.target -> reboot.target

我们可以看到级别0和级别6使用的数字已经转换成了更好识别功能的英文，其中级别1为恢复模式；级别2、3、4合并成了一个target，这个target被称作多用户模式，即不带图形的多用户网络模式；级别5单独使用一个target用于启动时启动X服务，即图形服务器。精简了基本的模式，并转化为英文之后，用户可以更好地区分每个target的功能；其他的target可通过基本target文件中的依赖形成树形结构，继续进行引导。

所有的target都可以通过如下命令来查看：

[user@localhost ~]$ ls -l /lib/systemd/system/*.target
user.target
lrwxrwxrwx. 1 root root 17 Oct 29 17:20 /lib/systemd/system/runlevel3.target ->multi-user.target
lrwxrwxrwx. 1 root root 17 Oct 29 17:20 /lib/systemd/system/runlevel4.target -> multi-user.target
lrwxrwxrwx. 1 root root 16 Oct 29 17:20 /lib/systemd/system/runlevel5.target -> graphical.target
lrwxrwxrwx. 1 root root 13 Oct 29 17:20 /lib/systemd/system/runlevel6.target -> reboot.target
-rw-r--r--. 1 root root 402 Sep 15 21:21 /lib/systemd/system/shutdown.target
-rw-r--r--. 1 root root 402 Sep 15 21:21 /lib/systemd/system/multi-user.target…

…

这里需要重点关注一个参数，它是涉及依赖关系的，打开这些target会发现它们使用了Requires/Wants关键字来确认各个target之间的依赖关系。其中Requires表示目标之间是强制依赖关系，Wants表示目标之间是非强制依赖关系，运维工程师只需要关注需要启动的服务即可，而不再需要分析各个默认的target之间的依赖关系。那么依赖关系为什么会分为两种呢？通常用于来解决一种极端情况下的依赖关系——循环依赖，即A→B→C→A，出现这种循环依赖关系的时候，systemd会尝试去掉Wants依赖，看能否正常进行引导。如果依然无法打破依赖关系，则会报错。

[root@localhost ~]# cat /lib/systemd/system/multi-user.target
[Unit]
Description=Multi-User System
Documentation=man:systemd.special(7)
Requires=basic.target
Conflicts=rescue.service rescue.target
After=basic.target rescue.service rescue.target
AllowIsolate=yes
[Install]
Alias=default.target

如果我们自己编写一个target，那么怎样才能知道一个目标需要哪些服务呢？可使用如下的命令：

systemctl show -p "Wants" multi-user.target

除了Wants，还可以有其他各种形式的依赖和被依赖关键字，如WantedBy、Requires、RequiredBy、Conflicts、ConflictedBy、Before、After等。我们会在后面陆续用到这些依赖关系。介绍了这么多，接下来让我们看一个完整的目标配置文件的实例。

下面以CentOS 7.0系统自带的NTP服务为大家分析一下systemd引导的全过程。systemd在内核加载完成之后，第一个引导的程序称为systemd命令。命令的绝对路径为/usr/lib/systemd/systemd，它被作为1号进程，替代了sysVinit方式的/sbin/init：

[root@localhost ~]# ps -ef | grep systemd
root 1 0 0 Dec02 ? 00:00:02 /usr/lib/systemd/systemd --switched-root --system --deserialize 21
[root@localhost ~]# ls -l /usr/lib/systemd/systemd
-rwxr-xr-x. 1 root root 1230912 Sep 15 21:21 /usr/lib/systemd/systemd

systemd已经使用目标替代了运行级别，所以在/etc/inittab中是看不到默认启动级别的。第一个目标会加载/etc/systemd/system/default.target配置文件；default.target文件通常是符号链接，会链接到/lib/systemd/system/<target name>.target，<target name>.target我们通常会使用multi-user.target和graphical.target，即sysVinit的级别3和级别5：

[root@localhost ~]# ls -l /etc/systemd/system/default.target
lrwxrwxrwx. 1 root root 37 Oct 29 16:41 /etc/systemd/system/default.target -> /lib/systemd/system/multi-user.target

如果需要更改默认的目标则可以使用ln-sf/lib/systemd/system/<target name>.target/etc/systemd/system/default.target命令，我们这里以multi-user.target为例，systemd找到默认级别后会读取multi-user.target，主要内容如下：

[Unit]
Requires=basic.target
After=basic.target rescue.service rescue.target
[Install]
Alias=default.target

这是一个标准的目标文件内容，格式上一般会有[Unit]和[Install]两部分，[Unit]部分的“Requires=”表示强制依赖关系，即multi-user.target若要正常启动则需要先调用basic.target；“After=”表示要在basic.target rescue.service rescue.target这3个target之后才能引导，是起引导顺序的功能，我们还会在[Unit]里面见到“Wants=”的设置项，它和Requires一样都是表示依赖关系，但是会在依赖冲突的时候暂时解除依赖关系。

[Install]关键字在这里表示开机是否需要加载，和sysVinit的chkconfig on设置开机时启动哪个服务器的功能是相同的。在这里第一个引导的target必须为Alias=default.target固定值才能继续加载服务。

继续跟踪引导过程，由于存在Requires=basic.target的设置项，因此systemd会读取basic.target是否还有依赖，直到所有的Requires都检测完，当依赖的[Unit]都被激活后，这个[Unit]才会被引导；如果有任何一个没有被激活，那么启动的时候会跳过这个[Unit]。如果要自定义启动逻辑，除了启动时激活必要的[Unit]，自定义的启动过程建议使用Wants关键字，以避免人为失误导致系统无法加载。

除了target文件本身可以使用“Wants=”关键字，还可以使用<target名字>.wants文件夹，并在里面写入其他target或服务来表示依赖，例如在/lib/systemd/system下，不但有multi-user.target文件，还有multi-user.target.wants/目录，里面还包含了其他target和service的符号链接。它们在我的测试机上面的格式如下：

[root@localhost system]# pwd
/lib/systemd/system
[root@localhost system]# ls -l multi-user.target.wants/
total 0
lrwxrwxrwx. 1 root root 16 Mar 24 16:06 brandbot.path -> ../brandbot.path
lrwxrwxrwx. 1 root root 15 Mar 24 15:34 dbus.service -> ../dbus.service
lrwxrwxrwx. 1 root root 15 Mar 24 16:05 getty.target -> ../getty.target
lrwxrwxrwx. 1 root root 24 Mar 24 15:35 plymouth-quit.service -> ../plymouth-quit.service
lrwxrwxrwx. 1 root root 29 Mar 24 15:35 plymouth-quit-wait.service -> ../plymouth-quit-wait.service
lrwxrwxrwx. 1 root root 33 Mar 24 16:05 systemd-ask-password-wall.path -> ../systemd-ask-password-wall.path
lrwxrwxrwx. 1 root root 25 Mar 24 16:05 systemd-logind.service -> ../systemd-logind.service
lrwxrwxrwx. 1 root root 32 Mar 24 16:05 systemd-user-sessions.service -> ../systemd-user-sessions.service

我们可以看到multi-user.target的依赖关系中除了其他的target还有service，service在这里称作服务单元配置文件，特点是以.service结尾，service文件用来封装真正需要启动的服务。可以简单地将其理解为sysVinit时代的启动脚本（它们通常被放在/etc/init.d下的可执行文件中）。通常情况下service文件会隐含地包含以下4个设置：

·Requires=basic.target

·After=basic.target

·Conflicts=shutdown.target

·Before=shutdown.target

这4个设置用于保证该服务是在基础环境加载完成之后才开始加载的，在系统关机前正确的结束，然后将缓存数据刷新到磁盘中。如果手工启动或停止某个不存在的service，那么systemd会在/etc/init.d中寻找同名的sysVinit脚本，并且根据同名脚本自动创建一个service单元，这就是之前我们提到的systemd兼容sysVinit脚本的方式了。

具体应该怎么引导呢？接下来我们看一个.service文件，这里是通过一个简单的NTP服务单元来为大家分析一下：

ntp.service
[Unit]
Description=Set time via NTP
After=syslog.target network.target nss-lookup.target
Before=time-sync.target
Wants=time-sync.target
[Service]
Type=oneshot
ExecStart=/usr/libexec/ntpdate-wrapper
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target

首先我们来看[Service]部分，每个.service配置文件都必须包含[Service]部分，这个部分用于对允许程序的行为进行管理，其中“Type=”设置的是进程启动的类型，常见的类型包括simple和oneshot两种，当Type=simple的时候，“ExecStart=”指定的进程就是该服务的主进程，会一直运行下去，直到systemd结束为止，或者运维工程师手动将此服务结束。而oneshot则表示systemd启动后续的服务之后这个程序会退出。在这个例子中NTP服务是onshot类型，因此在这个servie文件启动成功之后，“ExecStart=”后面跟着的ntpdate-wrapper会退出，而“RemainAfter=”参数一般会作为onshot类型的补充，用来表示这个service下所有的执行文件都执行完退出之后，状态是否还是active状态。因为在使用一次性程序，特别是这种典型的网络程序的时候，程序执行一次后就会退出了，如果通过检查进程是否存在还是无法确定程序是否成功执行过，那么通过这个参数就可以确定，systemd会保持这个service是active状态，以供其他的service进行检测。

另外，我们再介绍一种Type类型：Type=Dbus，它最典型的应用就是NetworkManager.service，使用Dbus类型不但要指定类型，还要设置一个BusName，后续启动的进程有依赖的时候，就会检测到这个BusName，进而通过socket通信。这个隐含选项调用了dbus.socket文件的[Socket]来指定socket通信的位置。

从这个文件里面我们看到还有[Unit]小节，这里有两个关键字“Before=”和“Wants=”，这两个关键字用于保证这个服务的加载顺序，要在syslog.target network.target nss-lookup.target加载完成之后才能加载，但不必考虑依赖关系，而Before=time-sync.target则要求它在time-sync这个target之前就加载。这就是sytemd解决引导顺序的方法。

systemd就是利用这些关键字完成了复杂的启动依赖关系，运维工程师甚至可以在不懂Shell脚本的情况下就可以配置各个服务之间的依赖关系了，而且还是并行引导的。systemd的引导过程就为大家介绍到这里了，接下来我们来看看systemd提供了哪些系统管理的命令，可用来方便我们进行运维工作。

6.3　systemd的进程控制命令

systemd兼容了sysVinit的进程控制类命令，同时还把需要修改配置文件才能生效的功能也封装成了新的命令。这里介绍一下systemd新增加的systemctl、hostnamectl、localectl、loginctl、timedatectl命令。

6.3.1　systemctl命令

sysVinit环境下最常使用的命令是service和chkconfig，chkconfig命令可用于查看和设置开机自动加载的服务，service可用于控制服务当前的运行状态。这里先来看下chkconfig命令和service命令的常用方法：

[root@localhost ~]# chkconfig --list network | grep network
network 0:off 1:off 2:on 3:on 4:on 5:on 6:off
[root@localhost ~]# service network help
Usage: /etc/init.d/network {start|stop|status|restart|reload|force-reload}

service和chkconfig这两个命令可被systemctl替代，以network服务为例，来看下它们的对应关系，如表6-1所示：

表6-1　systemd与sysVinit服务管理命令对照表

通过表6-1的对比，大家应该很容易理解systemctl命令，它将两个命令整合到了一起，因此更方便使用。

6.3.2　hostnamectl命令

在CentOS 5.x/6.x的工作环境中，设置主机名往往需要多步操作才能完成，首先要通过hostname命令设置当前的主机名，为避免主机名失效还需要将主机名写入配置文件；通过systemd提供的hostnamectl命令可以只用一条命令就能完成上述操作，hostnamectl命令的执行方法如下：

[root@localhost ~]# hostnamectl set-hostname host1
[root@localhost ~]# hostnamectl status
Static hostname: host1
Icon name: host1
Chassis: n/a
Machine ID: 9f9558a2a43914e8b079acbdbe9804c7
Boot ID: 4084e70ebc8047d3ae7eb01f5a41dbdc
Virtualization: kvm
Operating System: CentOS Linux 7 (Core)
CPE OS Name: cpe:/o:centos:centos:7
Kernel: Linux 3.10.0-229.20.1.el7.x86_64
Architecture: x86_64

6.3.3　localectl命令

修改字符集，这也是管理员在服务器初始化时经常要做的事情，通常使用echo$LANG的方式查看当前系统的语言和字符集，为持久化语言和字符集，通常设置/etc/locale.conf配置文件来确保下次启动时生效。systemd提供了localectl命令来进行设置，通常的用法是：

localectl list-locales 查看系统支持的字符集

localectl set-locale LANG=<字符集

>设置字符集

例如将系统改为简体中文GBK字符集，可以使用如下命令：

[root@localhost ~]# localectl set-locale LANG=zh_CN.gbk

打开新的终端和重启之后再查询字符集会发现它已经变成新的状态了，当前终端如果想要立即生效，可以使用如下命令进行修改：

export LANG=zh_CN.gbk

6.3.4　loginctl命令

loginctl是一个会话状态控制命令，它能够列出用户的会话信息，帮助管理员定位用户使用命令的情况，loginctl命令常用的参数有list-sessions和session-status，list-sessions可以查看当前系统的用户sessionID，通过指定的session ID可以查看某一会话的详细信息。loginctl命令的执行方法如下：

[root@localhost ~]# loginctl list-sessions
SESSION UID USER SEAT
1 0 root
3 0 root
200 0 root
3 sessions listed.
[root@localhost ~]# loginctl session-status 200
200 - root (0)
Since: Thu 2015-12-10 17:47:15 CST; 38min ago
Leader: 3596 (sshd)
Remote: remote.kvm.vt
Service: sshd; type tty; class user
State: active
Unit: session-200.scope
├─

3596 sshd: root@pts/2
├─

3600 -bash
├─

4060 loginctl session-status 200
└─

4061 less

6.3.5　timedatectl命令

为了统一早期版本的date hwclock这些查看和设置时间的命令，systemd增加了一个时间管理命令timedatectl，使用status参数可以查看当前的系统时间。该命令的执行方法具体如下：

[root@localhost ~]# timedatectl status
Local time: Thu 2015-12-10 18:32:33 CST
Universal time: Thu 2015-12-10 10:32:33 UTC
RTC time: Thu 2015-12-10 10:32:32
Timezone: Asia/Shanghai (CST, +0800)
NTP enabled: n/a
NTP synchronized: no
RTC in local TZ: no
DST active: n/a

使用set-ntp参数可以开启或关闭NTP时间同步服务，开启时间同步的命令如下：

[root@localhost ~]# timedatectl set-ntp yes

6.4　systemd服务管理

除了系统自带的软件之外，在实际应用中我们经常还会部署其他的开源软件，如果也需要使用systemd管理它们的启动，那就需要自行编写systemd的service文件，在这里我们以Nginx为例，从大家熟悉的sysVinit方式开始，来介绍如何编写自己的systemd服务脚本。

6.4.1　编写Nginx的sysVinit启动脚本

（1）下载Nginx：

[root@localhost ~]# wget http://nginx.org/download/nginx-1.9.4.tar.gz

（2）安装：

[root@localhost ~]# tar zxf nginx-1.9.4.tar.gz
[root@localhost ~]# cd nginx-1.9.4/
[root@localhost ~]#./configure --prefix=/usr/local/nginx
[root@localhost ~]# make
[root@localhost ~]# make install

（3）编写启动脚本：

#!/bin/bash
# nginx.sh
# 设置

Nginx在

runlevel 为

3 或

5 时，随着系统一起启动，如果不需要开机启动，

# 则可以设置为

chkconifg: - 13 68 ；这里的

13 和

68 分别表示开机启动和关机时

# Nginx的顺序

# chkconfig: 35 13 68
# description: nginx server
# http://nginx.org/download/nginx-1.9.5.tar.gz
# 定义变量

nginx_bin="/usr/local/nginx/sbin/nginx"
nginx_prefix="/usr/local/nginx/"
nginx_pid="/usr/local/nginx/logs/nginx.pid"
# 加载系统函数

. /etc/rc.d/init.d/functions
# 进程是否正在运行

,若运行则返回

0,若没有运行则返回

1
function nginx_is_running {
local nginx_pid_number=$(cat $nginx_pid)
# /proc下有和进程

PID 相同名称的目录，即为该进程正在运行

if [ -d /proc/$ngunx_pid_number ]
then
echo 0
else
echo 1
fi
}
# 启动函数

function start {
if [ -f ${nginx_pid} ]; then
[ $(nginx_is_running) = 0 ] && echo "nginx is running"
exit 2
else
#启动

Nginx
${nginx_bin} -p ${nginx_prefix}
fi
}
# 停止函数

function stop {
if [ $(nginx_is_running) = 1 ] ; then
echo "nginx is not run"
exit 2
else
# 停止

Nginx
${nginx_bin} -p ${nginx_prefix} -s stop
fi
}
# 重启函数

function restart {
stop
start
}
function status {
if [ -f ${nginx_pid} ]; then
[ $(nginx_is_running) = 0 ] && echo "nginx is running"
exit 0
fi
echo "nginx is not run"
exit 2
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
status)
status
;;
*)
echo "usage: $0 {start|stop|restart|status}"
;;
esac

那么，应该怎样使用呢，我们将脚本复制到/etc/init.d/目录下，并为其设置可执行权限：

[root@localhost ~]# cp nginx.sh /etc/init.d/nginx
[root@localhost ~]# chmod 755 /etc/init.d/nginx

然后使用命令：

[root@localhost ~]# chkconfig --add nginx

使Nginx开机能够自动运行。

sysVinit脚本是在Centos5.x/6.x系统上面运行的，接下来我们看看systemd如何编写Nginx的启动脚本。

6.4.2　编写Nginx的systemd启动脚本

systemd启动脚本不需要编写Shell脚本，这里建立一个Nginx的systemd启动脚本，脚本用nginx.service命名，nginx.service文件的内容具体如下：

[Unit]
Description=nginx - high performance web server
Documentation=http://nginx.org/en/docs/
#After参数设置项用来确认启动的顺序

After=network.target remote-fs.target nss-lookup.target
[Service]
Type=forking
PIDFile=/usr/local/nginx/logs/nginx.pid
# ExecStartPre参数可确保

ExecStart参数启动之前执行的命令，这里是在启动之前进行配置文件正确性的检测

ExecStartPre=/usr/local/nginx/sbin/nginx -t
# ExecStart参数用来启动

Nginx服务

ExecStart=/usr/local/nginx/sbin/nginx
# ExecReload 参数指定重新加载时执行的命令

ExecReload=/bin/kill -s HUP $MAINPID
# ExecStop 参数指定停止

Nginx服务时执行的命令

ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true
[Install]
WantedBy=multi-user.target

将脚本放在/usr/lib/systemd/system/目录下，使用如下命令可重新载入所有的[Unit]文件，确保我们添加进去的[Unit]被系统加载。

systemctl daemon-reload

使用systemctl start命令运行Nginx服务，命令如下：

[root@localhost ~]# systemctl start nginx.service

如果Nginx成功启动，则可以查看到如下信息：

[root@localhost ~]# systemctl status nginx.service
nginx.service - nginx - high performance web server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled)
Active: active (running)
Docs: http://nginx.org/en/docs/
Process: 21244 ExecStart=/usr/local/nginx/sbin/nginx (code=exited, status=0/SUCCESS)
Process: 21242 ExecStartPre=/usr/local/nginx/sbin/nginx -t (code=exited, status=0/SUCCESS)
Main PID: 21247 (nginx)
CGroup: /system.slice/nginx.service├─

21247 nginx: master process /usr/local/nginx/sbin/nginx└─

21248 nginx: worker process
nginx[21242]: nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx[21242]: nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful
systemd[1]: Started nginx - high performance web server.

使用systemctl stop可以停止服务，再使用systemctl status命令可以看到如下信息：

[root@localhost ~]# systemctl stop nginx.service
[root@localhost ~]# systemctl status nginx.service
nginx.service - nginx - high performance web server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled)
Active: inactive (dead)
Docs: http://nginx.org/en/docs/

通过上述信息可以得知，Nginx服务已被正确关闭。这就是systemd的启动脚本，相信大家根据6.2节所述的原理可以实现大多数开源软件的启动脚本。

6.4.3　systemd的其他功能

systemd的功能不仅限于以上几种，它还能代替很多系统的功能，例如：socket激活进程、识别硬件设备、挂载磁盘、执行计划任务等。

1.使用systemd的socket激活进程

在systemd没有出现之前，系统管理员经常会编写一些Shell脚本，有些脚本需要跨主机通信，那么就需要使用类似于xinetd的方式来进行端口绑定，我们先来看下xinetd封装的rsync服务：

service rsync
{
disable = yes
socket_type = stream
wait = no
user = root
server = /usr/bin/rsync
log_on_failure += USERID
}

在这个配置文件中，service参数后面的服务名称可用于区别其他的服务，这里用的是service rsync，还要注意此名称需要和/etc/services配置文件的名称相对应，因为在/etc/services里面绑定了服务名称和对应的TCP或UDP端口。

[root@localhost ~]# grep rsync /etc/services
rsync 873/tcp # rsync

下面继续来看看其他参数：

·disable参数用来标记是否开机启动此项服务，这里设置为yes的时候表示开机不启动rsync服务。

·socket_type为接受请求的类型，如果TCP协议为流类型，UDP协议为数据报类型，那么这里使用TCP协议，所以类型使用stream方式。

·wait表示请求是否会阻塞，如果希望并发处理用户的请求可以将此参数设置为no。

·user是xinetd调用rsync的用户权限。

·server是最重要的参数，这里表示的是绑定的应用程序，网络请求会转发到这个应用程序。这里将转发给rsync以处理接收到的数据。

·log_on_failure是将错误信息记录到日志，这里使用了+=USERID参数，将把用户信息一起记录进去。

配置完成后可使用如下命令启动sync服务：

[root@localhost ~]# chkconfig rsync on
[root@localhost ~]# service xinetd restart

使用rsync命令访问873端口即可连接到rsync服务，过程略显繁琐，使用systemd集成的socket功能就会简单很多。

systemd的这个特性称为socket激活进程功能，但是在使用socket激活进程的时候需要注意，应用程序必须支持socket激活，而且能够处理socket请求，所以像Apache Nginx这种daemon类型的程序是无法支持的，一般应用于Shell或docker上。例如，编写一个接收用户socket的请求，然后根据请求的内容启动Nginx程序的脚本。

首先需要创建一个socket类型的[Unit]文件，命名为start-nginx.unit，内容如下：

[Socket]
LinstenStream=8080
[Install]
WantedBy=sockets.target

这里的ListenStream表示的是8080端口，一旦收到请求就会调用和它同名的servie文件，我们将这个文件写入/etc/systemd/system/start-nginx.unit，然后再编写同名的service，将下面的配置信息写入/etc/systemd/system/start-nginx.service文件：

[Unit]
Description=start nginx
[Service]
ExecStart=/usr/local/startnginx.sh

很简单，ExecStart是指在这个service启动的时候会调用/usr/local/startnginx.sh脚本，在脚本中可以使用read类命令接收用户的输入请求，然后根据自己的需要调用Nginx启动脚本即可。

最后将这个socket激活：

systemctl enable start-nginx.socket
systemctl start start-nginx.socket

当使用者请求到8080端口的时候就会调用Shell脚本来启动Nginx了。

目前这个功能在CentOS7上面并不完善，如果需要在生产环境中使用类似的功能建议还是使用supervisord等软件来代替。socket激活进程要在systemd 209版本的时候才能体现出作用，目前的CentOS7和Ubuntu LTS还在使用208版本，从209版本开始systemd工具提供了一个systemd-socket-proxyd小工具，可以将socket代理给后端的应用，用法如下：

[service]
ExecStart=/lib/systemd/systemd-socket-proxyd 127.0.0.1:8090

将请求转发到本机的8090端口上，然后通过[Unit]段Requires/After调用其他的service来控制程序的启动和停止。完整的示例，大家可以参考官方文档的systemd通过socket调用Docker的经典示例。这里面使用了systemd-nspawn来产生新的进程，这对我们掌握自己编写systemd的service文件也很有帮助。

2.使用device进行设备识别

在使用systemd的device功能之前，我们先来介绍一下什么是udev，相信大家都用过Windows的设备管理器，当有新的硬件添加或删除的时候，资源管理器会自动识别硬件的改变。而这一切在Linux（2.6内核以上）上就是靠udev来实现的，它在底层依赖了一个uevent接口，接收硬件的添加和删除事件。这种机制在桌面系统下没有问题，但是到服务器的生产环境中如果识别多块网卡的时候顺序是随机的，识别多块硬盘的顺序也是随机的，那么在系统的管理上就会造成很多困扰，而udev会根据硬件的特有属性对硬件设备和名称进行绑定，从而让用户访问固定的硬件设备。

device功能可以将udev的规则封装在.device文件中，并以设备对应的名称来命名device文件，例如，我们插入一个特定的u盘，将其命名为/dev/usb1，就可以建立一个dev-usb1.device。在配置文件里写标准的udev规则库就可以了。dev-usb1.device文件的内容如下：

[root@localhost ~]# cat dev-usb1.device
ACCTION=="add",BUS=="usb",SYSFS{serial}=="0013729925234704A0",SYSFS{manufacturer}=="SanDisk",KERNEL=="sd*",NAME="%k",SYMLINK+="usb1",RUN+="/bin/mount /dev/usb1 /mnt"

3.使用mount代替/etc/fstab功能实现文件系统挂载

在Linux的启动过程中，能进行开机自动挂载的配置文件有很多，而且还有automount实现触发式挂载，systemd为大家提供了一种新的方法，让大家可以在服务启动的过程中进行挂载，systemd还可以设置启动顺序和基于socket等方式的激活，这样就实现了automount方式的触发式挂载。这里我们以CentOS7系统自带的将tmpfs挂载到/tmp目录为例，来看下挂载是如何实现的：

…

[Mount]
What=tmpfs
Where=/tmp
Type=tmpfs
Options=mode=1777…

这里截取最关键的部分[Mount]段，它的写法是mount命令的变形，mount命令一般用法如下：

mount [ -t 文件系统类型

] [ -o 选项

]<设备名称

>[ 目录

]

其中：

·-t-o根据用户需要都可以被省略。

·What=是设备或虚拟设备的名称。

·Where是本地目录。

·Type=是文件系统的类型，这里的tmpfs是指内存。

·Options=是挂载的时候要用到的选项。

systemd的配置可以翻译成如下的mount命令：

[root@localhost ~]# mount -t tmpfs -o mode=1777 tmpfs /tmp

如果增加一个选项又该如何修改呢？例如增加一个参数限制最大内存只能使用100MB：

[root@localhost ~]# mount -t tmpfs -o mode=1777,size=100M tmpfs /tmp

只需要修改“Options=mode=1777，size=100M”即可。这就是systemd的mount方式了，配合[Unit]部分的Before/After就可以让管理员按照自己的需要来挂载文件系统了。

4.使用timer替代cron

cron是管理员经常会用到的工具，是用于定时执行脚本和备份的重要工具，这里给大家介绍另外一种方法，那就是使用systemd的timer来代替cron进行计划任务。

首先编写一个简单的测试脚本，来验证程序是否能够正常运行：

#!/bin/bash
# test cron
date >> /tmp/cron_test.log

它的功能非常简单，是将执行脚本的时间记录到临时文件。方便我们来测试脚本是否成功执行了。接下来编写一个调用这个脚本的service文件crontimer.service：

[Unit]
Description=testcron
[Service]
Type=simple
ExecStart=/tmp/timer.sh

Type所代表的执行方式有如下两种：

·Simple表示基本的执行方式，让Shell正常解析我们的脚本就可以了。

·ExecStart是执行任务的时候调用的脚本。如果有多个任务可以编写多个service文件，那么有依赖关系的话还可以使用我们在前面介绍过的Before/After方式来进行顺序的编排。

继续编写timer文件crontimer.timer：

[Unit]
Description=cron test
[Timer]
OnBootSec=10min
OnUnitActiveSec=1m
Unit=crontimer.service
[Install]
WantedBy=multi-user.target

其中：

·OnBootSec表示在系统启动之后10分钟才开始运行。

·OnUnitAcriveSec是运行的周期，这里用做测试，所以每分钟运行一次。

·Unit表示调用的是哪个service，可通过上面的service来调用脚本文件。

最后让这个timer生效：

[root@localhost ~]# systemctl start crontimer.timer
[root@localhost ~]# systemctl start crontimer.service
[root@localhost ~]# systemctl start crontimer.timer
[root@localhost ~]# systemctl start crontimer.service

这些就是我们经常使用的运维工具在systemd中的实现了。

6.5　优化

我们可以看到systemd的功能已经非常强大了，除了系统自带的服务之外我们还可以增加自己的脚本，systemd也为系统管理员提供了很多性能分析工具，可以根据分析结果来进行启动的优化。

6.5.1　使用systemd-analyze优化启动时间

系统的启动时间是管理员关注的最重要的一个标准，因此systemd提供了一个systemd-analyze命令，来分析启动时间，用到的第一个参数是time，执行方法如下：

[root@localhost ~]# systemd-analyze time
Startup finished in 466ms (kernel) + 5.834s (initrd) + 40.483s (userspace) = 46.784s

通过命令的返回结果可以看到内核、initrd和userspace三部分各自的启动时间，而userspace就包括了systemd加载各个服务的时间，那么具体是哪个应用程序启动耗时最长，这里要用到第二个参数blame，执行方法是：

[root@localhost ~]# systemd-analyze blame
12.166s firewalld.service
9.211s tuned.service
4.254s postfix.service
3.841s lvm2-pvscan@252:2.service
3.292s boot.mount
…

…

它会将启动耗时最长的服务（service）显示在最上面，我们根据这个提示就可以找到指定的服务来优化它。

另一种优化是根据依赖关系和启动顺序进行优化，这种优化需要观察全局的启动顺序和依赖关系。systemd不仅提供了这样的分析功能，还提供了图形化的跟踪，使用的命令如下：

[root@localhost ~]# system-analyze plot > bootlog.svg

显示的结果如图6-2所示，非常便于管理员分析启动过程来优化启动时间。

图6-2　服务启动时间

除了要关注启动的速度之外，我们还会关注哪些服务在启动的时候没有按照预想的方式启动起来，这里可以使用systemctl-failed命令，使用方法如下：

[root@localhost ~]# systemctl --failed
UNIT LOADACTIVE SUB DESCRIPTION
rngd.service loaded failed failed Hardware RNG Entropy Gatherer Daemon
LOAD = Reflects whether the unit definition was properly loaded.
ACTIVE = The high-level unit activation state, i.e. generalization of SUB.
SUB = The low-level unit activation state, values depend on unit type.
1 loaded units listed. Pass --all to see loaded but inactive units, too.
To show all installed unit files use 'systemctl list-unit-files'.

该命令列举了启动失败的服务，可供管理员进行排错。当然，用户也可以使用systemctl-status<服务名称>，用于确定单个服务的运行状态。

6.5.2　使用systemd journal功能

systemd还提供了日志查看和筛选的功能，我们可以利用此功能方便地进行日志查看，在没有使用systemd之前管理员大多是使用cat、more或tail配合管道使用grep进行过滤的，现在有了systemd journal的功能，就可以简化查询日志的命令了。

下面来看看有哪些常用的参数：

·journalctl<参数>。

·--all这里会显示所有的日志，包括所有service打印的和系统的日志。（/var/log/messages）。

·-f类似于tail-f的用法，用于查看/var/log/message的实时变化。

·-b-p err显示error级别的日志。

例如：我们查看firewalld.service服务产生的日志可以使用如下命令：

[root@localhost ~]# journalctl -u firewalld.service
-- Logs begin at Fri 2015-08-21 08:33:15 CST, end at Thu 2015-10-29 16:27:01 CST. --
Aug 21 08:33:40 yumserver-2.yhs systemd[1]: Starting firewalld - dynamic firewall daemon...
Aug 21 08:33:52 yumserver-2.yhs systemd[1]: Started firewalld - dynamic firewall daemon.

可以查看指定的服务产生的日志。

6.6　小结

systemd是现代Linux操作系统上的非常实用的工具，通过上面的基本功能可以简化运维工程师大量的重复工作，但是systemd在使用习惯和用法上的改进也给运维工程师带来了巨大的挑战，希望这里能够通过对systemd的引导过程分析和特性的讲解，为大家掌握systemd的高级功提供帮助。

第7章　PHP运维实践

作者简介

张观石，2002年大学毕业，毕业后即从事桌面软件开发设计工作，后转型于互联网Web项目的开发和运维。有10年PHP开发和网站运维经验，曾负责运维“英雄联盟盒子”等多个以PHP为主要开发语言的大型Web项目，所负责的项目从日PV百万、千万到数亿的都有，在这些项目的成长过程中积累了大量的PHP运维经验，因此对运维也有了更深入的认识。现为欢聚时代（YY）互娱事业部业务运维负责人。

PHP是互联网创业公司开发网站的首选，国内外也有很多大型互联网公司正在使用PHP技术，包括BAT、YY、Facebook等。PHP语言容易上手，运维入手也很“简单”，所以很多团队对PHP运维和架构的重视和投入都不够，从而导致很多PHP技术栈的团队无法解决大规模、高并发的问题。不少人已经习惯于Nginx+PHP+MySQL+HTML的模式，甚至还有人把运维建设不足的问题也归罪于PHP，认为PHP做不了大网站。我注意到业界很少有关于大型PHP项目运维技术的分享和讨论，于是尝试着借助本书分享一下自己的一些思考和实践。

本章首先会从开发和运维的多个新视角来重新认识PHP，再讲解PHP运维与架构设计过程中的常见问题和解决方法，然后用3个小节的内容来分享运维最常见的工作：部署、性能分析和故障处理。先讲解PHP部署包括进程部署、配置变更及PHP代码发布；然后讲解如何分析PHP的性能问题、如何加强PHP的监控、如何调优PHP；最后讲解如何快速定位和处理故障。本章中我们会说到业界的运维现状，会重点介绍我对PHP运维的思考和实践。本章的每一节都会介绍一些PHP运维实践，其中部分内容不仅仅局限于PHP，对于其他技术的运维工作也是通用的。

7.1　PHP再认识

PHP是网站技术中最流行的语言，权威语言排行网站TIOBE上显示全球有83%的网站使用了PHP技术。那么PHP到底是什么样的技术呢，接下来我们会试着从几个全新的视角来重新解析PHP，以帮助读者更深入地理解PHP，为提升PHP运维能力打好基础。

7.1.1　PHP进程的工作方式

我们首先来了解下PHP是如何工作的，PHP作为应用服务器时，目前普遍使用的是多进程工作模式，Web服务器Apache/Nginx通过FastCGI协议把请求转发到PHP-FPM进程。下面就来分析一个Web请求生命的全过程（如图7-1所示）。

假设用户在浏览器地址栏输入http://www.test.com/index.php
发起一个请求，然后：

·域名被DNS解析到Nginx管理进程（Master process）所在的服务器IP。

·Nginx管理进程选择一个工作进程（Worker process）。

·Nginx工作进程把请求转发到PHP-FPM管理进程（默认是9000端口）。

·PHP-FPM管理进程分配一个工作进程处理index.php请求。

·工作进程在服务器路径中找到index.php文件，解析编译。

·执行PHP代码，可能还要请求后端存储等。

·得到请求的结果，先返回给Nginx，然后再返回给用户浏览器。

图7-1　HTTP请求在服务器上请求过程

PHP-FPM管理进程不仅要负责分配PHP请求给工作进程，同时也要控制工作进程的创建、结束和启停。单个PHP工作进程服务完若干个请求后会结束进程，释放资源，管理进程再启动新的工作进程。PHP多进程模式中内存等资源管理将由工作进程自行分配，满足一定的条件后重启工作进程会自动释放内存，即使内存泄漏也不会造成严重的问题，也不会出现多线程死锁的问题。所以PHP的可靠性较高，系统运行也更稳定，多进程需要不断地分配和回收进程资源，且需要消耗比线程模式更多的资源。多进程在大规模集群下的可扩展性很好，只需要简单地增加机器或增加进程即可实现扩展。

对于运维人员来说，多进程模式还有一个极大的好处，如果修改了配置文件则只需要重启工作进程即可，这也是为什么Nginx、PHP-FPM支持平滑重启，而Tomcat、MySQL等多线程模式不支持平滑重启的缘故。

7.1.2　PHP代码的编译和部署

PHP属于半解释型语言，程序源代码先编译为中间代码，然后缓存起来，再由解释器读取缓存并逐行解释执行。PHP的中间代码称为Opcode，是介于PHP源码和机器代码中间的一种代码，只有Zend的执行器能读懂它。PHP源码在首次执行时先编译为Opcode然后保存于内存中，下次将直接从内存读取Opcode并执行。PHP 7.X也在大力推广内置的Zend Opcode扩展，以替换独立的APC扩展。JAVA会被编译为class文件，发布出去，然后再执行，这就是所谓的一次编译，到处执行，而PHP是一次编译，多次运行。多次运行是指只要原文件不被修改，不刷新Opcode，则内存中的Opcode缓存一直有效，这样就减少了从磁盘读取文件、词法、语法分析、编译的过程时间，从而提升了网站的性能。

PHP的编译执行过程（如图7-2）具体如下。

图7-2　PHP首次编译执行和从缓存执行的过程

第一次是执行语法词法分析和编译过程（如图7-2中的1-1），编译后会保存为Opcode（如图7-2中的1-3），第二次直接从Opcode执行（如图7-2中2-1），不再需要词法、语法和编译过程。

7.1.3　PHP内部实现和生命周期

下面我们换个视角来理解PHP的实现，首先来看看“PHP”的组成部分。我们常说的PHP是指一门语言，其实PHP包括一系列相关的技术。

PHP内部组成如图7-3所示，上层是SAPI，它是连接Web Server的接口，可以服务于各种Web服务器，如Apache，Nginx；第二层是PHP语言本身和PHP的扩展和函数的实现等；第三层是Zend API，它把Zend引擎的指令封装成为API以供PHP语法层和扩展调用。整个架构的最底层是Zend引擎，他提供了API给上层使用，来实现PHP API、PHP语言和扩展。所以整个PHP技术最核心的就是Zend引擎，而“PHP语言”只是PHP技术中不算很大的一块。

图7-3　PHP的内部架构和组成

我们再来看看PHP内部是如何处理请求的，以及我们所熟悉的php.ini配置项又是如何配合执行的（如图7-4）。了解内部工作过程有利于我们理解PHP配置和管理扩展。工作进程启动后首先会初始化各种全局变量和常量，初始化Zend引擎和核心组件，然后分析和应用php.ini文件的配置；启动核心后再依次注册各个模块（PHP内部的模块化做得不错），模块“注册”完成后就可以开始处理PHP请求了。在处理请求的过程中会“激活”注册的所有模块，如果是通过URL请求某个PHP文件，则在每次处理请求之前都会进行模块激活（RINIT请求）。然后处理PHP源文件，包括解析词法、分析语法、编译代码、生成中间代码Opcode、执行。处理完成后，返回给调用方，然后是反向操作，停用模块、释放变量等。当整个工作进程满足某个条件时就会关闭，正常关闭会反注册所有模块，然后结束工作进程。

图7-4　PHP进程的内部处理过程

7.1.4　PHP在互联网技术栈的位置

接下来我们以一种更宏观的视野来了解PHP。PHP是应用层的技术，而大多数网站都使用了多种技术，PHP只是其中的一块。我们列举常见的技术（如图7-5），可以很容易就能PHP在整个互联网技术栈中的位置。

PHP只是众多技术中的一个，后面还有Memcached、Redis、MongoDB、MySQL、Node.js等各种常见的技术，甚至还有队列服务、Java服务、分布式数据库、文件存储等。用PHP技术开发互联网应用或想运维好一个PHP互联网应用，都必须要先掌握整个技术栈中与PHP相关的技术，如Web服务器、MySQL等各种存储、缓存技术，才能完全掌控住你的网站。

本节讲述了PHP进程的运行模式、请求在进程间传递的流程、代码在PHP进程内部的处理过程，以及PHP在互联网技术栈中的位置，从宏观和微观的多个不同视角重新认识了PHP技术。希望有助于读者对PHP有更深入的了解，从而有助于我们更好地架构和运维好一个PHP技术的网站。

图7-5　PHP在互联网技术栈中的位置

7.2　PHP开发、架构、运维问题及解决思路

业界使用PHP技术已经有多年的历史，对于创业公司，PHP更是首选。业界已经形成了一些良好的最佳实践，但也有一些普遍存在的问题。比如最流行的LNMP架构，帮助了很多初创公司快速上线网站，但有些人却跳不出这个框框。有些研发人员喜欢用大型PHP框架，有些人喜欢用自己的微型函数库、有些人会做较全的架构设计，还有些人喜欢从头到尾流水模式执行下来。本节我们将讨论在PHP网站开发、架构设计、运维过程中运维的角色及可以发挥的作用。

7.2.1　运维对PHP研发提要求

PHP代码不只是跟研发相关，跟运维工作也是密切相关的，研发的开发方式和模式会直接影响业务上线后的运维工作。有些开发人员以实现业务功能逻辑为唯一追求，缺少全局观，套个HTML页面完成业务功能就完事，代码效率、性能、代码风格通通不管。没有意识到PHP写得好坏也会直接影响到项目在线的运营质量，还会直接影响到运维人员的工作。如果PHP开发团队有多个小组，每个小组负责不同的业务，那么就很有必要从整个技术团队的范围来做一些统一规范，统一大家的开发风格，提升网站的可运维性。作为运维我们可以从自身角度提出一些具体的要求。

·统一的代码风格：MVC模式组织代码、驼峰风格/下划线风格。

·统一文件路径：将所有代码统一放在protected目录下，根路径只放index.php，通过URL rewrite访问业务逻辑。

·统一的配置文件路径及格式：运维可能要通过自动化管理配置文件，或者在排除故障时要修改配置文件的路径，比如protected/config.php。

·统一的应用日志路径：只有规范了日志路径才可以通过自动化去收集或清理日志，如protected/logs/xxx.log。

·统一的部署路径：将所有代码都部署在/data/webapps/xxx.yyy.com中，以业务域名作为部署路径。

·要求代码做好防SQL注入：在业务正常、发生错误甚至遭受恶意输入时都不能有出错的输出。我曾接手过几个麻烦的项目，接手后的第一件事就是修复SQL注入漏洞。

·性能和效率分析：上线前要了解代码的运行效率，所写的SQL的效率，整个系统的吞吐能力，整个系统和细分环节的延时情况。可以通过压力测试工具来分析，比如ab（Apache Bench）工具。

·容错容灾分析：一旦出现故障，要有一定的容错容灾能力，比如说做到无状态化，session和产生的内容不存储在本机中，当然机器出现故障或挂掉的时候要能够快速切换到其他服务器，在业务负载较大时要能够快速扩容。

·较大型的团队开发一定要用框架，在选择框架时要充分考虑到性能问题和运维方法。

PHP运维人员可以不懂怎么写PHP代码，但需要了解PHP代码是怎么工作的。优秀的研发和运维人员要有全局的视野，要了解HTTP协议、浏览器、DNS、CDN、前端反向代理、后端缓存、MySQL等数据库存储、文件存储等组件特性及其与PHP代码之间的关系。

下面列举两个我遇到过的反面例子。

【案例1】
有研发使用curl_exec函数访问外部接口，却不控制超时时间，当外部接口不稳定时，工作进程一直在等待对方返回，直到对方返回超时时间才继续执行。这时如果有多个这样的请求，那么所有的PHP进程都会被阻塞住，没有工作进程来处理其他请求。

【案例2】
有些开发人员认为缓存与自己无关，是架构师或运维维护的责任。还有些开发人员不了解缓存的特性，认为只要能往里面写数据、读数据就行了，结果造成内存利用不合理的问题，而且也没起到缓存的作用。某程序员还把大量的一次性数据写入缓存，他不了解一次性数据下次是不会再读写的，没必要缓存，缓存一次性数据反而会让内存无法释放，他还找运维说这缓存有问题，内存不够要扩容。

7.2.2　运维参与PHP项目架构设计

PHP之所以这么流行，离不开LNMP/LAMP（Linux+Nginx/Apache+PHP+MySQL，后面所讲的内容均以LNMP为代表，如图7-6所示）架构的完美组合。这个组合开发运维的效率很高，所有软件都是开源的，拿来即用。在项目的早期这个组合能够发挥很大的作用，时间久了整个团队已经习惯于这样一种组合，在架构上反而成了一种局限。在这种架构中，Web服务器收到请求，PHP通过SQL语句增删查改数据，把复杂的逻辑丢给后面的数据库（SQL语句），PHP数组充当临时的数据结构，获取到数据后把结果和HTML元素拼装在一起展示给用户。这种做法是把PHP当作一门胶水语言来使用，在逻辑较为简单、性能要求不是那么高的时候，这是一个不错的方式。但是随着项目越来越大，流量越来越大时，负载全都压给了数据库，性能问题就会出现了，PHP往往要等待数据库读写完成后才能继续执行，这时可能会认为是MySQL出现了性能瓶颈，进而认为是MySQL服务器不行，以为增加内存，更换更好的服务器就能解决这个问题。其实这是整个网站的架构出现了问题，是要重新考虑整个网站架构的时候了，运维要和研发一起讨论可能存在的性能瓶颈，找到解决方法，比如加缓存、静态化等。

图7-6　PHP常见简单架构

解决之道：重新思考大型网站技术架构

网站架构不只是与PHP有关，而是要考虑全局的问题。目前互联网技术的趋势是分布式、缓存化（所谓的把内存当缓存用、把SSD当存储用、把磁盘当冷备用）、异构化、微服务化，对于架构和技术选型要有宽广的视野。

限于篇幅，这里只将总体思路总结如下：

（1）理解整个请求的生命周期及PHP的位置，PHP只是处理数据的一环，不能纯当胶水技术，而把逻辑都给了数据库；也不能完全依赖MySQL处理所有数据，在PHP层可适当地使用算法和适当的数据结构、缓存等技术来减轻数据库的压力。

（2）对于实时性要求不是很高的页面应尽量静态化，以减少对PHP和MySQL的请求。

（3）能缓存就缓存，包括浏览器缓存、CDN缓存、反代缓存、Web缓存、数据缓存比如Memcache、Redis、Opcode缓存、Mysql缓存等，每一层缓存技术都值得大家深入研究。

（4）需要用户等待较长时间的重型操作则用队列和异步操作来实现，将结果快速返回给用户。

（5）若数据库达到瓶颈则要考虑做优化或拆分，横向分库分表、纵向按时间分表、按ID分表、读写分离等。

（6）复杂逻辑比如特别消耗CPU和IO资源的，可在应用层使用其他技术来实现，比如JAVA、C++、Go、Node、js、不要局限于PHP，应合理利用各种技术的优势。

（7）微服务架构，把重型服务拆分为微服务，进行面向服务的架构设计（SOA）。

PHP的开发、运维和架构都与业务结合得很紧密，现在还没有一招能够通吃的架构，不同的业务场景需要不同的架构来支撑。网站技术的本质是把存储在服务端的信息展示给用户，把用户提交的数据写入到服务端这两种数据流向。下面来列举一些常见场景下的架构方法。

大量读的业务场景之解决策略：
大量用户从服务器端读取数据的业务场景，架构设计应该充分利用各层的缓存（如图7-7所示），把读请求的数据尽量推到前面，以减少后端的读压力。

图7-7　网站架构中各层缓存的效果图

比如用浏览器缓存使得部分资源不需要连接到服务器，利用CDN可以把读请求放在最后一公里，利用反向代理缓存，使得只需要读取到前端反代等。利用更多逻辑层的缓存，而不需要读取数据库。

【案例1】秒杀业务场景：
秒杀业务是一种比较特殊，但又经常会用到的场景。秒杀本质上是大量用户在同一时间去访问业务，判断产品是否还有剩余，如果还有就购买，没有了就返回失败。这种场景是典型的读多写少的场景，大量用户查询（读）是否有剩余商品，只有极少数商品可以进行下一步更新（写）状态，极端情况下同时有几十万次读操作而只有几次写操作。按照上面所说的策略是把读取和判断往前推，比如在JS阶段就判断时间是否在秒杀开启的范围内，如果超过开始时间多少秒就直接返回失败，把剩余商品数量存在内存缓存中，对超过并发的用户直接返回读取失败数据等。

【案例2】大量写的业务场景：
某客户端开启自动上传游戏战绩的功能，游戏拥有百万的同时在线用户，游戏结束后把10个玩家的比赛信息上传到服务器，经过分析、处理和统计玩家的游戏细节数据后存入数据库。每个上传文件3MB左右，分析处理比较耗时。架构思路：首先考虑去重，客户端去重，若10个玩家的信息是一样的（可通过文件MD5码是否一样进行判断）那么第一个玩家上传后其他九个人就不用再传了，最大可以减少90%的上传量；客户端也有可能会上传失败，所以要保证有重试和延时上传的机会；先把上传的原始比赛详情数据包存入内存，同时写队列，把耗时的解析过程异步化，消费者读队列时再去解压原始数据进行分析入库；数据冷热分离，要将比赛战绩保留1个月，用Redis来保存，将过期数据存入冷库，MySQL可做长期数据，比如用户数据、排名数据等。

7.2.3　PHP运维常见问题及解决之道

PHP的运维能力很容易被忽视，不信的话可以搜索PHP运维，看看有多少资料系统地讲解了PHP的运维工作。本书的读者大部分都是运维人员，更不能忽视PHP的运维，这也是本章的写作初衷所在。下面我们来说说PHP运维常见的问题及解决之道。

如果有人问我们，PHP运维是做什么的？或许回答是这样的：首先安装PHP软件包，再修改配置文件，然后启动PHP-FPM，上传PHP代码。这种回答看起来也没什么错，然而并没有体现运维的价值。我个人很认同Google某大牛所说的，“运维是保证网站的可持续性运营”，在腾讯运维岗位被称为技术运营。在我的理解中运维是把产品设计、研发开发、测试完成的代码部署到服务器上，并保证以良好的状态持续运行，监控随时发生的状况，出了问题能够快速定位并解决，甚至进一步让运行数据反向推动产品、研发、运营人员改进业务。概括地说运维是服务于从立项到研发完成、到上线运营、到下线的整个业务生命周期。从中可以粗略地看出运维应该做的事情包括如下几项。

·部署：包括基础设施的资源部署、软件环境的部署。

·业务上线：PHP代码发布。

·稳定运行：保障用户进入网站页面后能一直顺利地完成整个业务流程。

·监控：监控网站运行过程中的各种状态，如发生异常则告警。

·性能优化：网站出现性能问题时能进行运维侧的优化，可帮助研发优化代码性能，容量不够时可快速扩容。

·故障处理：出现故障后能快速定位到问题点。

·数据分析反馈：对线上运行的状态能进行充分分析，把有用的结果反馈给研发和产品，推动产品进行下一次迭代。

运维要做的事情其实很多，每一个点都能体现出强大的运维能力。接下来几节将展开来讲其中的进程部署、代码发布、性能优化和故障处理这几个方面。

7.3　PHP进程部署和配置、代码发布

在一台服务器中进行PHP进程的部署、配置和代码发布都很简单，只要安装PHP进程，对配置文件按需要进行修改，启动进程，把PHP代码复制上去即可，稍有经验的人都能完成这些工作。如何在较短时间内完成多台、十几台、甚至几十、上百台服务器的部署，而且不出差错，其实是很有挑战性的工作。网上有大量关于PHP部署的教程，不难发现网上教程的方法有介绍单台服务器PHP部署的，有讲PHP配置参数的，却很少有讲“大规模集群”下PHP的部署和配置变更的，讲PHP代码发布的也极少。本节将为大家分享一下我个人的一些思考和实践，PHP部署可以包括以下三个内容：

（1）PHP进程的部署、升级，包括PHP扩展的部署。

（2）PHP配置文件的部署和变更。

（3）PHP应用代码的发布。

7.3.1　PHP进程的部署

首先来看看PHP进程的部署，常见的两种部署方式包括源码编译和官方软件包管理，然后介绍下互联网公司采用较多的自建包部署系统的方式，这里将重点介绍系统的思路。因为自建包部署系统不是每个公司都能做的，因此我们在最后分析出几个核心点，简述用批量运维工具来实现包部署的方法。

1.源码编译

若要采用这种方式首先要下载PHP的源码，执行configure、make、make install三板斧，然后修改配置参数，启动进程。

源码编译可以更好地利用平台的特性，更灵活地指定一些优化参数，灵活地选择组件、扩展，可以编译、运行多个软件版本，但是会存在如下一些问题：

·需要把源代码下载到每台服务器，时间长，效率低。

·编译比较复杂，需要掌握更多的技术，如要解决包之间的各种依赖。

·编译速度慢，时间长。

·若优化不好性能可能会更差。

也有可能会因为服务器Linux稍微有点不一致（版本差异或人为配置差异），编译过程和结果会很不一样，甚至编译通不过。所以对服务器进行源码编译的方式不适合大规模部署。

2.软件包管理

每个Linux发行版都有各自的包安装方式和工具，官方会提供包的仓库。比如RedHat有RPM和Debian、Ubuntu有apt-get、CentOS有YUM。通过Linux官方包管理工具从仓库进行安装，然后修改配置参数，这种方式简单又高效，一些公司也建立了自己的软件仓库，采用软件包管理的方式同样也存在一些问题，具体如下：

·官方仓库安装的版本太老旧，跟不上新版。

·安装方式的目录结构零散，不能完全按照自己的意愿组织文件。

·自建仓库也比较难实现多版本、多配置的服务。

·在自动化和运维可控性上较弱。

·不方便定制各种优化参数。

在服务器规模较大的环境中进行逐台手工编译或安装的方式会造成很多问题：耗时费力，严重拖延业务的上线或变更时间，结果很难预期，很难保证每台都完全一样，会有很多意外情况。运维部署进程还会碰到一些需要解决的问题，比如：发布生产环境要控制安全，要做DO分离，不是每个人都可以去部署；软件要不断进行迭代升级和更新；配置文件要一致，不能跟进程部署完全耦合，也不能完全隔离分开；跟公司的CMDB等运维系统打通。这些用手工的方式都没办法做到。所以很多大型互联网公司都会选择自建包部署系统，接下来我们介绍一下YY自建包部署系统的设计思路。

3.自建包部署系统

我所在的公司YY在服务端使用的是自建的包部署系统。大规模集群的部署必须要通过自动化来实现，只有通过自动化部署的平台，才能实现线上软件一致、版本一致、配置一致、目录一致，这些都对后续的运维效率影响重大，至关重要，只有这样也才能实现与其他系统打通，与整个运维体系整合在一起。接下来简单介绍下建设思路。

我们定义的软件包指的是一组目标执行代码文件、脚本和配置文件，按照打包规范组成可以自行安装和自行部署的组合，打包过程如图7-8所示。

1）运维目标

通过包部署系统来实现我们的运维目标。

·统一管理：不用大家都去编译一个版本，一次编译，全网部署，全网一致。

·发布效率：包部署的效率比每次编译和仓库安装快N倍，而且不容易出错，包括回滚的能力和效率。

·管理配置：配置文件的修改也是经常会发生的事情，可以通过系统统一修改下发配置文件。

·灵活的运维需求：可以定制前后置脚本，比如监控、定期任务，跟其他运维系统打通等。

·有记录可供回溯：每一次的发布都有记录，方便管理、控制安全、后续审计等。

2）包部署规范

打包和部署的工作不只是一件将软件文件组成一个包的事情，还包括一系列的规范。

·打包规范：是指包括哪些内容、目录、版本、配置规范路径，文件名称，通用变量等。

·部署规范：如何部署到目标服务器、路径、文件，对其他系统的依赖，启停脚本规范。

·监控规范：需要哪些脚本，脚本名称规范、定期执行规范、上报规范、自动监控、拉起。

·环境规范：OS发行版本、系统库版本。

按规范打完包后接下来讲解如何部署软件包。

3）PHP打包规范

具体到PHP包，我们的打包流程及其规范具体如下。

·专人编译PHP，全公司统一版本，编译参数是经过运维团队仔细考虑和优化过的，充分考虑公司的Linux发行版，不同OS发行版采用不同的包，收集了大部分PHP团队的意见，兼顾了大家的需求。

·配置文件php.ini和php-fpm.conf均经过仔细优化，以适应公司的服务器机型，如Web类服务器的内存和CPU情况。

·日志路径、配置路径、脚本路径、监听端口都统一规定好。

·把核心扩展（MySQL、APC等）编译进PHP可执行文件，常用的扩展（Redis、MongoDB、Memcached等）编译成独立的.so文件，放在扩展目录下。

·把所依赖的一些系统库文件，也放置到一个依赖目录，比如libjpeg、libevent等，总之把包发下去不会影响现在的系统库，也不会依赖太多的系统和第三方库。

·写好了统一的监控脚本，在固定目录中

定期执行监控脚本，并上报到公司监控系统。

4）PHP包的升级和发布

首次打好的PHP-FPM包要能够满足当前的需要，后期可能会根据需要做一些调整。这种调整可以通过升级包版本的方式来完成，而不是重新打另外一个包。对于升级的包我们会分配一个新的包版本号。升级包可以替换包的任何部分。这样各个版本的包还是可以在线继续运行的，在包部署系统中也能进行启动、停止、升级等各种操作。

当业务需要往一台服务器中部署PHP-FPM包时，我们一般的流程是先到包部署系统中选择你的PHP-FPM需要的包和版本，选择或输入目标服务器IP，点击部署就可以开始了。部署过程如图7-9所示：

图7-8　打包过程示意图

图7-9　包部署过程示意图

不是每个公司都有能力开发这样的部署系统，了解了核心思路后通过一些简化的做法也能基本实现整个流程。打包过程还是一样，最后不用传到系统，而是以tar包的形式保存在集中服务器，部署的时候通过Puppet、SaltStack、Ansible等批量运维工具分发到目标服务器上，解压，执行前置，部署，后置脚本，部署监控脚本等。

说到大规模部署，目前容器技术也很热门，也有公司把PHP等进程直接做成VM镜像，或者打成Docker镜像。通过VM和Docker的部署来实现进程的部署。容器技术属于另外的范畴，这里不会展开来讲，有兴趣的读者可以自行查找资料。

7.3.2　PHP配置文件变更

配置文件是服务器软件的重要组成部分，使用不同参数的配置文件可以满足各种特定的业务场景，比如不同的服务器资源，负载不同的业务。配置文件也是线上修改比较频繁的部分，很多时候不需要变更软件，而只需要修改配置文件。接下来我们讲解PHP配置文件的变更。

进行手工部署的时候，也需要登录服务器修改配置文件，当服务器有10台以上时，不可能逐台去登录修改了，必须通过SaltStack、Ansible等批量运维工具来实现。再进一步就要考虑建立配置文件管理系统。配置文件是运维标准化、自动化中非常重要的一环，决定了软件的运行方式和运行状态，修改配置文件是运维的日常工作。是否能够高效、准确又灵活地管理变更这些配置文件，变更后不会造成混乱，这是运维团队及运维平台是否成熟的体现。

PHP有两个重要的配置文件，php.ini和php-fpm.conf。

php-fpm.conf文件：用于控制PHP-FPM管理进程的相关参数。比如工作子进程的数量、文件描述符数量、队列、监听端口等。

php.ini文件：用于控制PHP工作进程处理请求时的相关参数。配置项对应于工作进程，对通过这个进程处理的每一个请求都有效。比如PHP工作进程启动时要载入哪些扩展、将session保存在哪里、此次请求最大的内存大小、错误日志、语言特性、可以上传多大的文件、日志文件的路径等。

那我们应该怎样快速修改这些配置文件呢，有如下几种常见的做法。

·包部署系统管理配置文件：将配置文件的变更当做一次软件升级。

·配置文件跟PHP代码一起发布：发布业务代码时将配置文件也随之发布。

·独立的配置文件管理系统：通过自动化运维系统变更配置文件，首次部署软件时初始化了配置文件，后续的变更都通过配置文件管理系统来进行。

不管是采用哪种配置文件管理方式，都应该包含以下的核心功能。

（1）配置查看：查看配置文件的内容、配置变更历史、查看某版本的配置文件。查看的内容是保存在数据库中的。可以根据服务器和应用模块来查看。

（2）变更部署：一般是在默认配置文件的基础上修改参数，修改了配置后可以快速下发到目标服务器，并重启PHP进程。这里的目标服务器一般以应用模块为单位，比如说登录模块有4台服务器，一般会同时修改这个模块的全部服务器。当然有灰度能力就更方便了，先发1-2台服务器，成功了再发全部。

（3）如果是与包部署系统结合的方式，可以为每个包先做多个满足不同场景的配置文件。比如新的高性能服务器需要使用更多的进程数，为它单独分配一个配置文件，又比如为某特殊的应用使用一份特殊的配置文件。测试环境、预发布环境、生产环境可以使用各自不同的配置文件。在部署包的时候选择需要的配置文件。

（4）历史记录可以记录每次的变更历史、发布历史，以及修改了的配置文件，需要进行单独的管理。比如说某些服务器使用了特殊配置，应该既能从应用模块或服务器查配置，也能从配置查服务器和模块。

7.3.3　PHP配置项

PHP配置文件php.ini和php-fpm.conf中的配置项非常多，因为本节的重点不在于此，加上这类资料也比较容易获取，因此我们仅用思维导图把这些配置项进行分类，希望能帮助大家了解配置项（php-fpm.conf配置项的分类如图7-10所示，php.ini配置项的分类如图7-11所示）：

图7-10　php-fpm.conf配置项分类

图7-11　php.ini配置项分类

7.3.4　PHP进程部署及配置文件管理实践

前面介绍了进程部署的思路和方法，接下来我们看看YY的包部署系统。该系统不只用于发布PHP进程，很多C++进程也用这套系统来进行发布。首先看下PHP文件组织的目录结构（如图7-12所示）。可以看出结构中包括了几个目录：data、conf、log、bin、lib，其实还有scripts、admin等目录。

图7-12　PHP包的目录结构

1.PHP包的目录说明

·data/：存放程序运行期间生成的数据。

·conf/：存放配置文件，php.in、php-fpm.conf等配置文件存放于此。

·log/：日志，这里一般是软连接到系统大磁盘分区的日志目录。

·bin/：存放二进制可执行文件，如php、php-fpm。

·lib/：存放依赖的lib库文件。

PHP包在首次部署之后，可执行的动作包括升级、回滚、启动、重启、停止等，如图7-13所示。

图7-13　PHP包的可执行动作

2.配置文件管理

YY包部署系统中配置了文件模板编辑功能，可为每个场景或应用生成一份定制的配置文件。如图7-14所示，这里生成了一份名为“php-fpm.conf for highperf”的PHP配置，这份配置包含了三个文件。

图7-14　PHP配置文件管理

3.配置文件变更

从图7-14这里可以点击进入编辑配置文件的内容，比如修改php-fpm.conf文件，如图7-15所示。保存之后这个配置模板就发生了变化，有了新的配置文件版本号，可以通过配置升级功能部署到需要的目标机器上。

图7-15　配置文件变更

7.3.5　PHP代码发布

PHP的发布一般是把源代码文件直接发布出去，PHP进程会读取源码并执行，所以PHP发布起来也是最简单的，简单到通过FTP软件把PHP文件上传到服务器上就可以了。如果只有一两台服务器，通过FTP传上去也许可行，但是当服务器很多的时候，就不能靠手工FTP了。从专业的运维角度来看，仅把文件上传到服务器上是不够的，而且还可能会出现各种问题。我们先来看看几种常见的发布方式。

1.常见的发布方式及问题

（1）FTP方式：
业务规模很小的时候，为了简便，越简单越好，越快越好，最常见的发布方式就是FTP。工作电脑本地固定目录写好保存代码，服务器安装好FTP服务，在FTP软件中建好FTP配置项，设置本地代码目录对应线上服务器网站根路径。变更了的代码只要把文件或目录上传到相同的目录覆盖即可。当然也有人每次都把所有的代码打成tar包，上传到服务器再解压到目标目录，本质上还是上传。有些人使用sceureFTP，直接在secureCRT软件中进入代码上传过程。如果有2~3台，可能需要手工上传多次，或者从一台服务器scp到别的服务器上。

这种方式存在一些问题：不知道谁在什么时候会发布，多人合作容易互相覆盖；没有代码版本管理，若发布出现故障可能会找不到之前正确的版本；也没有发版的概念，随意性太强，无法快速回滚；没有记录，找不到发布历史；对一两台服务器一般没有问题，但多几台服务器就会无法接受。

（2）svn update方式：
有些团队有使用SVN等版本管理工具，使用SVN进行管理，于是发布也随之改变。有些团队把代码提交到SVN服务器，在所有服务器的文件根目录中执行svn update。

这种方式比较统一，结果也能保持一致，但安全性存在问题，很容易把SVN信息泄露出去。而且要在每台服务器上逐台执行，不太方便。回滚等也需要逐台进行手工操作，记录只在SVN里。需要掌握SVN的技术才能顺利发布。

（3）rsync方式：
rsync是一个文件同步的软件，提供了快速增量的上传方式，实现了弱一致性和强一致性两级检查文件是否变更，能按区块把文件发布到服务器，同时还会进行文件对比以查看是否有修改，若没修改则不覆盖，所以在覆盖式的方式中速度更快，是一种普遍使用的方式。

这种方式方便、准确、统一，但缺少友好的用户操作界面，且需要执行脚本，要掌握Linux命令行技术，发布多台服务器需要执行多次命令，替换IP，需要配置服务器。

也有人使用rsync+inotify-tools实现Web发布目录同步，采用这种方式先把代码发布到一台服务器，然后通过inotify触发文件同步到多台。有人用脚本来实现，也有人用sersync等类似软件来实现。

还有人把rsync与SVN结合起来，发布时将SVN更新到一个目录，然后rsync把这个目录同步到多台服务器上。

2.代码发布系统

手工部署无论如何都会存在较多问题，即使有完善的文档，也很难完全避免发生意外。建设发布系统才是发布代码的正确姿势，为了解决发布的众多问题，一个优秀的发布系统需要具备以下特性。

·对发布人员友好：不需要掌握太多的Linux Shell技术，不需要记住rsync复杂的参数配置。通过Web页面点击鼠标，即可完成发布，把技术人员从发布过程中解放出来。

·可重复操作，幂等：不会发布多次而出现不一致的情况，不需要反复执行命令，发布到多台也只需发布一次即可。

·支持持续集成和持续部署：可以与SVN等代码库整合，可以持续集成，持续部署。

·支持版本管理：可以选择版本发布、可以回滚到历史版本。

·支持灰度发布：可以选择先发布少量服务器，再逐步扩大到所有服务器。

·支持前后置脚本：可以在发布前后执行相应的动作，以满足灵活的发布需求。

·发布记录：记录每次的发布时间、发布文件、发布原因等，能统计发布次数、发布耗时。

·支持多个环境发布：同一套代码可以发布到开发环境、测试环境、生产环境，可为不同的环境配置不同的发布任务。

发布系统自动化是运维自动化的重要组成环节，如果能把自动化发布做好，那么很多运维工作才好开展，也能更好地推动下一步自动化的工作。有必要将自动化发布在团队中推行开来，自动化运维能带来很多收益。自动化发布的目标是与高度自动化的测试、自动化部署和全面的配置管理结合在一起，能够实现一键部署，就可以把代码部署到开发、测试和生产环境中，且能在出现故障时快速回滚，加上各种管理功能。

7.3.6　PHP代码发布实践：代码发布系统

在很多团队中，PHP代码的发布还是靠FTP等人工的方式。我们实现了一套PHP代码发布系统，由研发人员“一键发布”
即可快速发布，在研发团队应用得比较广泛，形成了较好的口碑。这里简单介绍一下其工作流程（如图7-16所示）和实现方法，以供大家参考。

图7-16　发布系统的流程图

这是一个简单的系统，主要功能包括如下几点。

·可为不同的环境创建不同的发布任务，如测试、开发、生产、灰度环境等，如图7-17所示。

图7-17　发布系统-任务列表

·指定目标服务器：rsync模块路径、用户名、密码端口等信息，如图7-18所示。

图7-18　在发布项目中配置目标服务器的rsync信息

·创建项目，指定SVN地址，也可以指定持续集成脚本，如图7-19所示。

·指定前置、后置动作，包括刷新APC缓存、请求某个URL等。

·指定全部发布，或者只发布指定目录，或者忽略目录。

·发布过程查看，支持记录发布时间、发布人、发布耗时等信息，如图7-20所示。

·查看发布历史：如图7-21所示。

·支持指定用户权限，跟公司账户体系打通。

图7-19　创建发布任务详情

图7-20　发布过程及结果

图7-21　发布历史

7.4　PHP性能分析

7.4.1　性能问题概述

随着网站用户的持续增长，如何以有限的资源尽可能地服务于更多的用户呢，PHP性能优化在这里就显得特别重要。PHP具有入门简单、容错性强、性能较高的特点，也很容易由于测试不完备、代码随意、对性能不够重视等原因导致上线后出现各种问题，且不容易在第一时间就能找到性能瓶颈点，没有经验的人甚至无从下手去收集全面的信息加以分析。由于PHP运维人员面对的是生产环境，因此掌握PHP的性能分析和解决性能问题是PHP运维人员必备的非常重要的能力。

影响PHP性能的点有很多，我们先来了解“性能”这一概念。性能的本质是延时和吞吐率，即所谓的latency和throughput。吞吐率是指在单位时间里完成的工作量，延迟是指一个请求从开始到完成所用的时间，有时也会认为与响应时间是同一概念。

·延时/响应时间：一个PHP的请求（request）需要花费多长时间来处理，PHP执行需要多长时间。影响响应时间的因素包括PHP代码的执行时间、CPU、内存、磁盘IO、网络IO、SQL请求等。

·吞吐率（对PHP-FPM来说是RPM：Request Per Minute）：吞吐率跟响应时间成反比，响应时间越短，单位时间内可处理的请求次数越多，即吞吐率越大。

·错误率/失败率：PHP处理缓慢会导致超时或触发错误、异常，性能问题也体现在请求的错误率升高。

延迟和吞吐可以体现出PHP应用的性能，我们可以将它们作为运维质量的指标来进行统计分析。从业务请求来看，我们要判断一个请求的响应时间少于多少是合理的（比如1.5s），超过阈值则可用性指标会下降，吞吐率下降到多少是异常的，要触发预警和告警，反馈给业务研发。错误是直接影响用户使用的错误结果，用户看到的可能是502、500、503等错误。错误率上升了一定是出现了故障。

造成性能问题的原因多种多样，深入了解PHP的性能问题是我们快速定位问题点、快速解决问题的基础。接下来我们分析下PHP的性能问题。

如何才能知道网站和PHP能承载多大的访问量呢？正常的情况下延时是多少呢。我们通常用ab（Apache Bench）工具来分析，ab可以模拟用户对网站发起请求，常用参数为-n、-c，参数-n表示总请求次数，-c表示同时发起的并发请求数。例如要测试某网站的性能，命令如下：

ab -n 300 -c 20 http://www.test.com/index.html

关注结果中的“Requests per second”和“Time per request”：

Requests per second: 285.54 [#/sec] (mean)
Time per request: 1053.502 [ms] (mean)

可以发现平均响应时间大概在1s左右，并发数在285左右。结果中还有很多有用的信息，有兴趣的读者可以查阅相关资料。

7.4.2　PHP性能问题

广义的PHP性能是指PHP站点的整体性能（如图7-22所示），是指整个站点软硬件架构，包括硬件、操作系统、Web服务器、PHP、到后端的缓存、存储及调用的外部接口性能等。狭义的PHP性能是单指PHP自身（包括PHP进程和PHP脚本）的性能（图7-22中的灰色背景部分）。

网站整体输出的性能也就是整个网站架构的性能，从外部来看，URL每秒的请求数和响应时间就是站点的整体性能。7.1节中讲整个请求流程时讲到请求要经过很多环节，每个环节都可以用吞吐和延迟两个指标来衡量性能。例如我们常说的Nginx能支持每秒多少个请求，MySQL支持多少个并发查询，磁盘支持多少IOPS等。分布式架构正是为了解决某个环节的性能瓶颈问题而设计的，比如Nginx集群解决Web层性能问题，PHP集群解决应用层性能问题，MySQL主从、分库分表等措施都是为了通过集群来解决数据存储层的性能问题，分布式分散压力都是使得本层的性能不会成为整个站点的瓶颈。本节的重点是讲解其中的应用服务这一层，也就是PHP-FPM这一层的性能问题。

一个PHP网站可能包括多个服务（如登录、首页、列表、上传服务等），也有给其他服务调用的API。每个服务又由一系列的类或函数调用所组成，函数要访问外部接口、外部存储、本地IO，也包括PHP本身执行过程中内存和CPU的消耗。每一个函数都可能是性能瓶颈点，也可能在某些条件下引发性能问题，从而导致整个服务，甚至整个业务出现故障。

图7-23展示了逐级细化后的PHP性能组成部分。

图7-22　PHP广义和狭义性能

图7-23　PHP应用性能-逐级细化

我们可以以不同的视角来看待整个网站和PHP的性能问题，从宏观定位出问题的是哪一层级和哪一个服务，然后再到微观层面看是哪个服务的哪个函数或接口，消耗了什么资源。

7.4.3　性能分析方法

性能问题牵涉到的技术点很多，且发生在生产环境中，没办法离线进行慢慢检查，再加上与系统中的其他软件互相依赖互相影响，所以定位起来不太容易。当PHP发生了性能问题，一定是有某个地方达到了瓶颈，我们要做的就是找到这个点，然后消除它。本节将讲解通用的PHP性能分析方法。首先我们分几个层级来进行分析，先从操作系统的层级来看看系统的负载，再看看PHP自身输出的运行状态，比如各种日志，这些日志是PHP运行情况的表现，然后通过PHP内部的运行细节，发现PHP运行过程中的性能瓶颈。

1.系统/进程级分析

硬件和操作系统是PHP进程正常运行的基础，如果系统本身资源不够或其他进程抢占了太多的资源，那么肯定会导致PHP发生性能问题，所以本节先讲解系统负载和PHP进程资源占用的分析方法。

（1）系统负载分析：
top命令是最基本的Linux命令，可通过top命令查看机器整体负载和PHP进程的资源消耗情况，如图7-24所示：

图7-24　top命令查看系统及进程负载

系统整体负载（5分钟load avg）在5左右，表示CPU任务队列的平均长度为5，每个PHP-FPM进程占CPU 13%左右，可以理解为PHP进程占用了这个CPU线程13%的执行时间，且多个PHP同时都占这么高；单进程占内存30MB左右，所占内存也偏多（正常情况下是20MB左右）。下面来看内存的使用情况，如图7-25所示：

图7-25　服务器内存使用情况

从图7-25所示的内存使用情况来看，本机有16GB的内存，已基本被用完，但有8466MB是被用在了缓存上，实际只使用了6643MB，所以内存实际上还是比较充裕的。下面来看服务器IO使用情况，如图7-26所示：

图7-26　服务器IO使用情况

从图7-26分析的服务器IO使用情况可知，IO利用率在2.76%，也是很低的。还有其他的很多信息，限于篇幅不能逐一展开。总之这台服务器目前负载较低，有一定的业务访问，系统负载问题并不突出。

（2）系统调用分析：
所有软件对磁盘、内存、网络等硬件的操作都是通过操作系统来执行的，而系统调用是操作系统内核提供给用户态进程的接口方法。strace工具可以帮助查看用户进程调用的那些系统调用（如图7-27），比如磁盘读写、网络连接、网络读写、内存分配等操作及其耗时，从而进一步分析进程的资源消耗，进而有针对性地改进代码，优化程序。

命令格式：strace-p<PID>-T

其中PID是PHP-FPM工作进程的进程ID。

图7-27　通过strace观察进程的系统调用

从图7-27可以看出大部分系统调用都在执行读取文件和从MySQL获取数据，且MySQL返回的字节数都有1000多字节，说明SQL返回的结果较大，但每次调用耗时还是比较短的（0.000015s），如果这里出现较长时间，很可能就是瓶颈所在。

strace还能统计一段时间内各个系统调用的执行次数和消耗时间，命令如下，结果如图7-28所示：

$strace -p <PID> -c

从图7-28可以看出stat（获得文件的状态）占了27%的时间，poll（监测网络连接队列的事件）占了27%。说明主要负载还是在查找文件和网络连接处理这两方面。解决办法就是和研发一起找到读取文件最多的代码部分，通过优化PHP代码进行解决，如果上面读取文件太多，那么从上面strace的输出可以粗略看出是读取的PHP文件太多，应该能分析到是每次都读取了PHP文件，而且遍历了多层路径，还指明了具体的文件和路径。所以加入PHP的Opcode缓存，减少对PHP代码文件的读写也许是最好的解决办法。另外在PHP代码中include其他PHP文件时，用了相对路径也会由于路径遍历造成多次读取文件，将include文件改为绝对路径是更好的方式。

图7-28　strace统计系统调用的分布情况

2.PHP运行状态

PHP充分考虑了运维的友好性，可以把运行过程的内部状态通过日志和接口反馈出来，让研发人员和运维人员得以了解PHP的运行状态。主要方式有PHP-FPM的错误日志、访问日志（access log）、慢日志（slow log）、PHP错误日志（error log）、内部状态接口（php status）等。接下来讲解如何根据各种日志和状态分析PHP性能问题。

1）访问日志分析每个请求的耗时

如图7-29所示，access log可以查看每次PHP的访问日志，包含了请求方法，请求的PHP文件及参数、返回码等，也有CPU、内存、时间消耗等。

图7-29　PHP access log性能分析

从图7-29可知所有请求都是成功状态200，大部分请求耗时都在20ms左右（注意，这个性能时间只包括PHP脚本本身的执行时间，也就是PHP字节码执行时间的总和，不包括IO时间和等待SQL的时间），内存3KB左右，CPU在60~85%之间。取最繁忙的1分钟或一段时间内的一个平均值，可以得知每分钟的请求次数（Request Per Minute，RPM），如图7-30所示：

图7-30　PHP access log性能分析，计算RPM

从图7-30可以看出一分钟处理了49012个请求，也就是每秒800次左右。

可以在php-fpm.conf文件中配置访问日志的格式，PHP提供了丰富的选项来获得所需要的信息，具体如下。

·%%：表示百分号（%）。

·%C：请求消耗的CPU百分比。

·%d：请求消耗的时间。

·%e：环境变量或服务器变量，与$_ENV、$_SERVER的效果一样。

·%f：脚本文件名。

·%l：POST请求的内容长度。

·%m：请求的方法GET/POST。

·%M：这个PHP工作进程分配的内存的峰值。

·%n：工作进程池名称，以区别多个进程池的日志。

·%o：输出HTTP HEADER信息。

·%p：工作进程的PID。

·%P：PHP-FPM管理进程PID。

·%q：查询字符串。

·%Q：问号。

·%r：请求的URI信息。

·%R：客户端的IP，一般会显示为Nginx的IP。

·%s：返回状态码。

·%t：收到请求的服务器时间。

·%T：写日志的时间，可以理解为请求完成的时间。

·%u：remote user，远程用户。

系统默认是access.format="%R-%u%t\"%m%r\"%s"，也就是打印远程IP、远程用户、请求时间、请求方法、请求URI、返回状态码，不带性能数据。

图7-29中日志格式的配置如下：

access.format = "%R - %u [%{%Y-%m-%d:%H:%M:%S}t] %{REMOTE_ADDR}e \"%m %r%Q%q\" %s %f %{seconds}ds %{megabytes}M %{user}C%% %{system}C%% %{total}C%%"

PHP访问日志非常有用，经常查看有利于持续发现某些性能比较差的服务和请求。

2）慢日志分析性能

通过慢日志（slow log）可以看到那些超过规定时间的请求，包括慢的代码文件及函数级的调用栈，可以在php-fpm.conf中进行配置：

slowlog = /data/weblog/php/$pool.slow.log
request_slowlog_timeout = 3

指定慢日志的文件路径和请求出现在慢日志中的临界值，若一个请求超过了这个时间（3s）即认为是慢请求。慢日志的超时时间可根据日常情况来定，一般2~3s即可。

slowlog信息展示如图7-31所示：

图7-31　PHP-FPM slow log

图7-31中慢日志显示了时间、进程池、工作进程pid、PHP文件名。接下来是PHP请求的内部调用栈，函数从下往上顺序调用。可以看到入口函数是viewthread_procpost，此函数调用栈是viewthread_procpost→discuzcode→preg_replace→parsemedia→parseflv→parse_video_url→getContent→curl_exec，发现PHP执行慢的根本原因是curl_exec函数，找到function_core.php文件的第3524行，分析curl_exec请求了哪个URL即可知道是哪个外部服务有问题。

3）PHP-FPM status分析性能

PHP-FPM比之前的FastCGI管理工具强大的地方也是因为它自带了一些内部的信息的工具，php status能较好地显示工作进程的信息，查看PHP内部运行过程中的状态。

在Nginx中配置location：

location ~ ^/php_status$
{
include fastcgi_params;
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $fastcgi_script_name;
}

在php-fpm.conf中配置：

pm.status_path = /php_status

Nginx收到/php_status请求，转给PHP-FPM的服务，PHP内部若发现匹配到配置项pm.status_path，即返回状态信息。通过浏览器访问Web页面或通过curl访问状态路径（status path）时，例如在浏览器中打开http://www.foo.com/php_status
，或者curl http://www.foo.com/php_status
，可看到如图7-32所示的信息：

图7-32　PHP-FPM status

下面来解释一下各个字段的意思。

·pool：PHP-FPM工作进程池的名称，默认为www。

·process manager：进程管理方式，可取值为static.dynamic或ondemand。

·start time：启动日期，如果重载了PHP-FPM，则时间会更新。

·start since：PHP-FPM运行时长，从启动到当前的时长。

·accepted conn：当前进程池接受的请求数。

·listen queue：请求等待队列，如果这个值不为0，那么要增加FPM的进程数量。

·max listen queue：请求等待队列最高的数量，曾经达到的最大的请求等待队列的长度。

·listen queue len：socket等待队列的长度。

·idle processes：空闲进程数量。

·active processes：活跃进程数量，表示有多少工作进程在处理请求。

·total processes：总进程数量。

·max active processes：最大的活跃进程数量（从启动PHP开始计算）。

·max children reached：工作进程数量达到的最大数量，如果这个数量不为0，那就说明你的最大进程数量太小了，需要调大一点。

·slow requests：慢请求的数量，如果这个数字较大，则有必须去分析性能问题了。

从图7-32中可以看出一些信息：一共有105个进程，93个处于活跃状态，最大的活跃进程数达到128，这个服务器的负载较高。PHP进程启动有一个月了，慢日志有71656条，性能问题较严重。最大等待队列达到129个，说明有些用户访问起来有点慢了。

status获取到的是PHP进程的总体信息，PHP-FPM同时还提供了每个工作进程的详细信息。在URI中加上full参数（http://www.foo.com/php_statusfull
），可以指定返回的格式（json/html/xml），默认是普通文本格式：

http://www.foo.com/php_status?full

可以看到类似如下结果：

************************
pid: 15342
state: Running
start time: 21/Oct/2015:22:20:53 +0800
start since: 148
requests: 238
request duration: 28431
request method: GET
request URI: /forum.php?mod=viewthread&tid=455
content length: 0
user: -
script: /data/www.foo.com/forum.php
last request cpu: 0.00
last request memory: 0

各字段的意义具体如下。

·pid：工作进程PID，可以单独kill这个进程。

·state：当前进程的状态（Idle，Running等）。

·start time：进程启动的时间。

·start since：当前进程已经运行的时长。

·requests：当前工作进程处理了多少个请求，达到max_request值后会自动被kill掉，防止内存泄漏的长期影响。

·request duration：请求时长（单位为微秒）。

·request method：请求方法（GET、POST等）。

·request URI：请求URI。

·content length：请求内容的长度（仅用于POST）。

·user：用户（PHP_AUTH_USER）（如果没有设置；则为‘-’）。

·script：PHP脚本（如果没有设置；则为‘-’）。

·last request cpu：上一个请求的CPU使用率。

·last request memory：上一个请求使用的内存。

从结果中可以看出正在处理forum.php这个脚本，URI是/forum.php?mod=viewthread&tid=455，这个工作进程存活了148s，一共处理了238个请求，这次请求已经耗费了28431微秒（28毫秒）。如果看到所有的进程都处于running状态，则表示很繁忙。如果经常看到有请求时长比较大的请求，则表示可能存在问题；如果看到多个进程都在处理同一个URI，则表示这个URI的请求可能很多，或者很慢。

3.内部运行状态分析，代码级性能

PHP代码性能是属于研发侧的工作，不是光靠运维就可以解决的，不过运维面对的是生产环境，更有可能比研发先发现隐性问题。运维不会亲自修改代码，不过运维可以告诉研发，哪里存在性能问题，可能是什么原因引起的。所以这里不会具体讲代码的优化方法，而是讲解如何分析和发现PHP代码的性能问题。

1）通过xhprof分析PHP代码级性能

xhprof是Facebook推出的PHP扩展，能够快速发现PHP执行的内部执行和资源消耗情况。关于具体的安装和配置过程这里不做解释，有兴趣的读者可以上网查找资料。效果图如图7-33所示：

图7-33　xhprof查看PHP性能，显示函数调用链

从图7-33中可以看出PHP代码函数之间的调用关系，箭头表示调用关系。粗箭头表示最耗时的调用链，深色背景的是最耗时的函数。百分号表示本函数占了本次调用链的耗时比例是多少。一个函数在调用链上可能被调用多次，箭头线上的“call”表示调用了多少次。从图7-33中可以看出最耗时的是最粗的箭头，往下看最终是调用了memcached：：get函数，该函数占了本调用链45.9%的时间。

图7-34以表格形式展示了另外一种视图，包括调用次数、比例、耗时情况、消耗内存情况。

图7-34　xhprof查看PHP代码性能，以表格展现

下面来解释一下图7-34中各个字段的意思。

·Function Name：函数名。

·Calls：调用次数。

·Calls%：此函数调用次数占所有函数调用的百分比。

·Incl.Wall Time（microsec）：函数调用花费的所有时间（包括子函数），单位是微秒。

·IWall%：调用花费的所有时间包括子函数所占的百分比。

·Excl.Wall Time（microsec）：函数执行本身花费的时间，不包括子函数执行的时间。

·EWall%：函数执行本身花费的时间所占的百分比，不包括子函数执行的时间

2）通过xdebug分析PHP代码级性能

xdebug是一个开放源代码的PHP程序调试器，和xhprof类似，可以用来跟踪、调试和分析PHP程序的运行状况，性能数据可以通过WinCacheGrind/WebGrind进行输出，其输出的结果也可以帮助我们离线分析PHP的性能。配置方式也与xhprof类似。

4.常见外部存储的性能分析

（1）MySQL：PHP+MySQL是最常见的搭配，如果怀疑MySQL存在性能问题，那么我们可以通过如下几个方法来分析性能问题。

·show processlist：显示当前正在运行的SQL的信息，分析SQL的数量及每条SQL的当前状态。

·explain：取出可疑SQL执行explain SQL语句，可以分析SQL的执行计划、索引是否合理等。

·show profile：通过show profile命令，可以分析出SQL的详细执行情况。

·show status/show global status：可以查看MySQL内部的各种状态，包括连接、线程、内存使用、数据表的打开数量、SQL命令执行情况等。like子命令可以过滤信息，如show status like“%cache%”。

·show varaibles：可以查看当前生效的变量值，结合show status可以查看配置是否合理，这个比查看my.cnf文件更准确。

（2）慢日志（slow log）：如果是经常性的慢，则可以执行mysqldumpslow slowlog文件分析MySQL的慢查询日志。

·Memcached：Memcached也是常用的缓存组件，如果怀疑Memcached出现性能问题，可以通过stats、stats items、stats slabs等命令分析内存的使用情况，内存中项目（item）的淘汰情况，每秒cmd_get、cmd_set等命令的执行次数，并判断是不是比正常情况下更大。

·Redis：Redis是常用的缓存，也有不少业务将其拿来当作存储组件使用。Redis可以通过redis-cli命令进行管理，info命令分析Redis内部状态：包括连接情况、主从情况、持久化、内存使用情况等。也可以通过monitor命令查看正在Redis内部执行的指令。

7.4.4　PHP性能分析实践：性能分析系统

xhprof扩展是深入分析PHP性能的神器，但是它也存在一些问题，比如xhprof的部署比较麻烦，为了监控一台服务器的PHP程序，需要做多处修改，还得改PHP代码，排障时无法及时部署。我们的技术团队在xhprof的基础上开发了一套PHP性能分析系统。该系统在xhprof的基础上进行了简单修改，取其核心部分，重新整合为一套系统，解决了如下几个方面的问题：

（1）快速部署，从需要开发来执行，到只需要开发修改框架，再到无需开发参与。

（2）采集性能数据，集中存放在中心服务器。

（3）可以控制采样概率，对性能的影响较小。

（4）将性能数据查看功能整合到了运维系统。

纯粹介绍系统的使用对读者没有什么意义，本节着重介绍下系统的建设思路和方法，大家可以结合这些方法去建设自己的性能分析系统。先说说xhprof原本的做法，如图7-35所示：

改进后的性能分析系统的做法如图7-36所示：

图7-35　使用原本xhprof扩展的必要工作

图7-36　改进后的性能分析系统的必要工作

xhprof管理程序是一套PHP系统代码，展示图（图7-33）和表格（图7-34）都是通过这套程序实现的，Graphviz是一套绘图程序，可以读取性能文件中的调用链绘制调用关系图（如图7-35所示）

那么，自动部署是如何做的呢，我们写好了两个PHP程序，分别称为xhprof_start.php和xhprof_end.php，在为某台服务器开启性能分析的同时将这两个文件发布到目标服务器，并自动修改php.ini文件中的选项：

auto_append_file =xhprof_start.php，

auto_prepend_file =xhprof_end.php

这两个文件将分别执行图7-35中右边框的所有工作，而且还能控制采集性能数据的频次。性能分析系统的工作流程如图7-37所示：

图7-37　性能分析系统工作流程

这套系统做得比较小，使用起来也比较简单，工作原理如图7-37所示。系统的工作量较小，利用自动化运维技术和一些PHP技巧简化了xhprof的部署，取得了较好的收益，在性能出现问题时，可以在几分钟内部署好并查看性能分析的结果，对性能分析和排障都起到了较好的作用。

7.5　PHP故障处理与监控

我们先来看一个场景。

某天晚上你下班回到家，刚准备吃饭。同事打来电话：“X工，网站打不开了，打开首页都很卡，你赶紧看看吧”。你：“收到，我马上就看”。

在女朋友的抱怨声中你打开了电脑，趁着开机的时间把刚刚放松的心情重新收拾起来，然后静下心来思考30秒。回想这个业务的服务器架构，把域名输入到浏览器自己验证一遍问题。发现真的是打开首页都很慢，想想或查找这个业务的服务器IP，登录服务器开始各项检查。有时你发现PHP进程挂了，重启一下就能解决，有时运气则没有那么好，发现访问不断出现错误502，排查半天也找不到原因。还得叫上研发、DBA上线一起分析。等你查完问题，女朋友已经吃完饭了。

是不是很熟悉的场景？也许你觉得这些好像都很正常，都是这样的过程呀。等等，故障从用户反馈时到解决完已经不知道发生了多久，你如果很熟悉业务则可以回忆起架构，但是如果不熟悉，那就还得找架构图或CMDB，这资料还得是最新的。如果机器负载看起来还比较正常，你得再静下心来想想问题到底出在哪里了，即使发现是机器负载的问题，你又怎样才能快速处理呢？可是你想过没有，时间就这样在一分一秒地过去，而我们对故障还是有点懵。如果是经验丰富的研发或运维或许还能知道问题大概出在哪里。但如果是运维新手呢，只能找研发一起慢慢排查，而用户是不会等你的，所以你只好眼睁睁地看着用户流失了。大型网站上是不允许出现这种情况的，一旦出现老板肯定要追责，要进行事故定级，如果背上个一二级事故，那么年终奖恐怕得大打折扣。

故障问题跟性能问题息息相关，又有不同之处。性能可以体现为吞吐低和高延时，达到一定的量或突破极限后往往就成为了故障。本节将尝试为大家分析PHP故障，介绍快速分析方法和解决方法。

7.5.1　PHP故障分类及处理思路

有经验的运维一般都能够快速定位到问题点，他们接到报障时就已经在心里根据故障的描述来对问题进行分类了，进行准确的分类判断是进行下一步分析的基础，只有准确分类才能一矢中的。大部分PHP故障会反映在用户端，用户看到的可能是HTTP错误返回码、也有可能表现为卡顿。在没有经过异常处理的网站中，常见的错误码有如下几种：

·502错误：
打开页面显示的是502 bad gateway（错误网关）的信息。原因一般是PHP执行得太久而超过了参数request_terminate_timeout和max_execution_time所设定的时间，导致FPM终止了PHP工作进程，无法为fastcgi接口返回信息。如果PHP的错误日志里出现了SIGxxxx，那就有可能会出现这个现象。而罪魁祸首很可能是后端存储或调用外部连接反应太慢，或者挂了，导致PHP一直等待，达到了自行了断的条件。

·502错误的一般处理方法：
如果是执行超时时间配置不合理，则需要延长超时时间。另一种处理方法是从后端入手，找到关键的瓶颈点，比如分析数据库的慢查询、死锁、负载，执行中的SQL等，strace是个发现PHP连接后端超时的好方法。

·503错误：
伴随503错误出现的描述一般是整个服务临时不可用（Service temply Unavailable），这种错误一般是Web服务器（如Nginx）这一层端口还活着，但无法响应用户的请求。

·503错误的一般处理方法：
应该重点分析机器负载，Nginx进程是否存活，端口是否存活，Nginx进程的工作饱和度，如果以上都正常则应该参照7.4节所讲的方法分析Nginx状态。

·504错误：
504错误表示网关超时（gateway timeout），可能是请求量突发，导致PHP负载太高，无法为请求分配工作进程，或者达到了连接数限制、某些组件达到性能瓶颈了，导致系统可用性明显下降；影响504错误的是Nginx的超时设置fastcgi_connect_timeout、fastcgi_send_timeout、fastcgi_read_timeout。

·504错误的一般处理方法：
还是要从PHP层进行分析，如果每个请求的处理时间都是正常的，那么就是整个PHP层的资源不够用了，需要扩容；如果是某些请求的时间太长，那么就得找到性能瓶颈点了。

·404错误：
表示页面不存在，静态页面或PHP文件被删除。

日常正常运行的网站如果突然变得卡顿，那么出现的故障大部分是性能问题造成的，往往是到了高峰期用户大量涌进网站，导致某些组件达到性能瓶颈。由PHP的性能问题所引起的故障比较常见的有如下几种。

·PHP进程不存在：可能由于进程意外掉挂了，再没有起来，比较常见的是请求一直在处理中，达到max_execute_time时间，管理进程把工作进程干掉了。

·PHP进程响应慢，Web服务器响应慢，达到超时后，返回503错误。

·PHP进程本身慢：这种情况反而不多，只出现在少数特别复杂的框架写的程序中。

·PHP依赖的接口慢：比如请求外部的接口，外部接口很慢等。

·依赖的后端存储慢：这种情况比较普遍，比如MysQL、Redis、Memcached出现故障或性能问题。

7.5.2　业务监控和故障发现

大部分网站可能是等用户抱怨的时候才发现有故障，而这时不知道已经影响了多少人、出现了多久，这时候老板很可能也已经发现了，运维处理起来会很仓促，压力也很大。好的故障处理是要做到比用户和老板先发现故障，甚至还没有出现故障，而只是一个风险点的时候就把它解决掉。所以故障发现是非常有必要的，故障发现最重要的方式就是监控。大部分故障的原因是性能问题达到了不可容忍的程度，7.4节已经讲解了很多PHP性能分析的方法，只要关注这些性能指标的变化，就可以提前发现故障的苗头。说起来很容易，但是应该如何做呢，首先我们可以回顾有哪些点可以发现故障线索，哪些点需要监控，应尽可能地收集性能数据和用户访问数据，智能监控，关联分析，而不是等到业务完全不可用了时才发现。

1.监控点和监控方法

·业务的URL：如果用户访问的页面都出现了问题，这是故障最直观的体现，则要加入URL监控，设定合理的超时值。可以使用服务器模拟用户访问，或者使用基调、博锐、监控宝等第三方服务。

·PHP状态信息：监控PHP-FPM的PHP状态（status）页面，查看当前正在处理的请求、RPM等，定期获取，并记录到监控系统，设定好阈值，进行告警，多台服务器可以归并告警。

·错误日志的数量和比例：监控PHP的错误日志和FPM的错误日志，还有慢日志。可以通过数量得到错误率。通过日志内容进行双重监控。

·PHP访问处理时间：通过监控PHP的访问日志得到每个PHP请求的响应时间。

·业务日志：可以在业务代码中的关键处埋点，如果出现错误或特别耗时的操作超时了则打印日志，使得运维对日志做监控。

·吞吐量（单位为rpm，每分钟请求数）：通过访问日志可以得到RPM，对突发的请求量提前介入观察，并记录下来作为长期统计值。

·进程存活和端口：监控PHP进程是否存活，监控PHP的端口是否存活。

·Web服务器的错误日志：Nginx到PHP的fastcgi_connect、read_timeout、send_timeout等的超时时间，如果出现超时则写入Nginx错误日志，可以监控出现超时的日志数量。

·MySQL等的监控：PHP的性能严重依赖后端的性能，所以后端也要加强监控。MySQL可监控的项目非常多，具体请查阅MySQL的show status命令。

·性能分析系统：上面都是各种总体的监控，一旦出现故障，排查还是无法准确定位，这时如果有一套性能分析系统，也许就能迅速发现PHP的故障点了，从而可以反馈给研发做快速处理，而不是呼叫研发上线慢慢分析和排查并最终处理。

·外部接口的性能：跨应用性能问题跟踪，比如脚本调用外部资源失败或超时。

·合适的报警机制：根据阈值告警出来之后，可以通过各种渠道发送给运维和研发，预警用邮件，告警用短信、IM、电话、定期出报告等。

·业务日志监控：很多PHP程序员不太习惯使用日志、也不太喜欢打日志、对监控问题不够重视，容易犯以实现功能为唯一目标的错误。这些对大型互联网来说都是不可取的，需要运维和研发一起配合，做好日志和监控，只有这样才能打造出高性能的PHP服务，为海量用户提供服务。

2.监控系统

监控系统是运维的核心系统。我们不可能每次都靠人工上服务器去分析问题，所以好的做法是自动把分析结果存入监控系统中，定期持续地记录数据。需要分析时到监控系统中去查看即可。监控系统可以使用开源的Zabbix、Nagios等，这些都是优秀的开源监控系统，大公司往往会选择自建监控系统。

例如，如何把PHP状态（status）监控上报到Zabbix，在Zabbix中展示出来。在服务器中定期执行：

/usr/bin/curl -s "http://127.0.0.1/php_status?xml" |awk -F'<|>' '$2 ~ /^active-processes/ {print $3}'

即可得到活动的工作进程数，也就是当前并发处理的请求数。

配置zabbix.agentd.conf：

UserParameter=active.processes,/usr/bin/curl -s "http://127.0.0.1/php_status?xml" |awk -F'<|>' '$2 ~ /^active-processes/ {print $3}', active

通过Zabbix相关模板，即可展示出来。网上有大量的此类教程，请自行查阅。

3.可用性管理

通过上面所讲的各种方法可以发现网站各个点的性能问题，有没有什么办法可以衡量整个网站业务的质量呢？如果老板问起，最近网站的运行质量怎么样，运维总不能说最近告警少了很多，或者多了很多吧。这里我们引入可用性管理的概念，可用性管理是把业务综合质量通过量化的数据表达出来，并且能够报告给业务方和老板，从而量化业务的质量，和运维工作的价值。

·可用性管理：
在长期分析性能数据的基础上形成的一套可用性的标准。例如我们定义首页响应时间在1500ms内即认为是正常的；或者PHP-FPM工作进程最大达到总进程的80%是安全的；如果某时刻慢日志数量大幅增加了则可能是出问题了。前面列出的分析点都可以形成一个指标，业界称服务等级指标（SLI）。我们可以在团队内部确定几个大家普遍关心的指标，形成服务等级目标（SLO），期望达到的性能目标（如响应时间在1500ms以内，整个服务的RPM达到500以上等）。

·持续性能管理：
性能是一个长期的、动态变化的过程，应该长期自动采集性能数据，以反映业务的性能变化情况。比如每分钟定期采集请求的响应时间，就能得到一条性能曲线图。上面所说的每一个性能分析点都是可以通过长期采集数据来得到曲线图的。拿着这个指标就能

定期汇报说，我们的网站性能下降了或提高了。

7.5.3　PHP故障消除的方法

1.快速修复代码问题

·回滚：如果是业务上线所导致的故障，那么最好的做法就是回滚。当然如果涉及有数据库的修改，那么要回滚将会是很困难的。必须要做出选择。是回滚容易解决问题还是应该直接扛住。

·快速修改发布上线：在做了很多修改后发布上线，如果仅是某些小功能点故障，那么一般是不做回滚，而是快速修改，重新上线。同样，这也是权衡后的结果。

2.弹性调度

PHP故障有时可能只是因为PHP的工作进程处理不过来了，如果仅是CPU和内存等资源不够用的原因，可以考虑弹性增加PHP应用层的服务器，在架构合理时弹性调度可以实现自动扩容、缩容。

·前提条件：
应用服务要求做到无状态化，不在本机中保存与业务相关的数据，如图片、用户Session等。Session可以通过分布式存储来管理，图片等业务生产的内容要放到分布式的文件系统中。

·实现难点：
自动扩容或缩容要求自动化程度很高，资源池的管理，能分配机器资源，能自动开通数据库等后端存储的权限，能自动部署应用程序等。Web服务器能自动扩缩应用服务器，如动态修改Nginx的upstream配置。

·弹性条件：
能动态计算整个应用层的可用性指标，可以是PHP的RPM或响应时间、或者是CPU使用率、网络状况或负载均衡的请求响应时间、健康次数等，如果达到设定的阈值则进行自动扩容或缩容。

3.柔性可用

当整个线上一下子找不到问题了时，或者是找到了问题又修复不了时，无资源池可用时，那就只能考虑降级服务，以保证核心业务可以正常提供服务，对不那么重要的就暂时停止提供服务，比如一个业务提供了看图和上传图片的服务器，如果出现了故障，就要考虑先把上传图片暂时停止，而保证看图业务的正常运行。柔性可用有很多细节策略，总体上就是牺牲不重要的，以保证核心重要功能的可用性。

7.5.4　故障分析案例

【案例1】
SQL问题的发现和分析过程

现象：
某业务反馈比较慢，有时报502错误。

分析：
PHP访问日志（access log）看到有个查询列表页执行的时间比较长，进一步通过strace工具分析发现连接数据库和等待时间都比较长，估计是数据库负载较高。在MySQL中通过show processlist和分析慢查询，发现了问题SQL，有上千条，其中几条显示被锁了，分析问题SQL，发现是表索引不合理引起的。

结论和解决方案：
是SQL写得不合理和表设计索引不合理引起的问题，加索引和优化SQL后即可解决，通过MySQL explain命令来分析SQL。关于SQL优化和MySQL优化，有专门的文章详细介绍，这里就不展开了。如何事前发现问题SQL，是运维和DBA的课题，可以考虑通过SQL的审计来实现。

【案例2】
调用外部接口引起的故障

现象：
访问某页面卡，经常打不开

分析：
研发刚换人，也不熟悉代码，代码很复杂，无法逐行阅读。运维上去看访问日志（access log），没有特别慢的情况，慢日志（slow log）中可以看到偶尔有curl_exec的调用出现，利用Linux的strace工具分析进程，发现有连接某个IP，但不知道是什么服务器，不是我们自己的。有可能是第三方的，怀疑是通过HTTP协议调用了第三方的接口，外部接口慢导致PHP进程慢。于是我们通过基于xhprof的性能分析系统，发现（如图7-38所示）：

图7-38　通过xhprof发现PHP代码性能瓶颈

结论和解决方案：
curl访问外部接口占用了太多时间，而且没有设置访问超时时间。让外部接口提供方查找性能问题，同时在curl_exec中设置超时时间为200ms，如果外部接口不稳定就快速返回失败，而不是卡在那里影响全局。

strace能在系统层面和PHP进程层面发现问题，而不只是单个PHP脚本。curl是访问外部HTTP接口的工具，PHP代码中经常会使用，访问过程需要多个步骤，DNS、连接、等待处理、返回等，时间较长。

【案例3】
缓存失效引起的

现象
：
告警系统发现某台缓存服务器的流量突然暴增，达到900Mbit/s。

分析：
通过iftop等工具发现流量来自于多台应用服务器，缓存服务器端口是11211，是Memcached服务，分析mc状态，发现连接数也特别多。

慢日志显示缓存的获取函数较多，access log显示也较长，但是没有更多的信息。有人认为是缓存失效了，有人认为是缓存服务负载顶不住了，应该增加更多的mc服务器负载，也有研发认为是缓存内存不够了，要求加大Memcached的内存空间。运维通过xhprof分析到有个PHP get mc缓存的函数调用（call）次数很多。分析PHP代码发现几乎每调用一次都要获取一次缓存。从开发经验来讲，如果获取太慢，这已经失去了缓存的作用。和开发沟通后发现是他新增了一个功能，key只有10个，分析后发现value都在40KB一个，访问量一多，流量就会暴增。

结论和解决方案：
很大的key而且调用频繁将会导致带宽暴增。这需求有点不太合理，但是业务需要，也无法否定。可通过增加本地缓存来减少读取网络缓存的次数。最后将PHP apc的用户缓存作为本地缓存，缓存30秒后过期，这样就大大缓解了带宽和调用次数。

7.6　小结

很多人对PHP有一种固有的印象：它是一种传统的PHP开发语言，开发简单，运维简单，似乎很容易就能掌控住。其实在业务量还小的时候，业务如果正常运行，一切似乎都是那么简单、自然。然而在运维几个较大的项目后，我改变了这种看法，其实我们了解的可能只是一小部分，且我们对PHP的了解并不像自己想象的那么深入。也有不少人认为PHP技术栈的团队无法解决较大规模、高并发项目的性能问题，其实我认为这也许是团队运维建设不足的原因吧。

本章是个人几年来积累的一点点经验总结，希望能对读者在PHP运维工作上有所帮助。7.1、7.2节讲了一些PHP认识和开发、架构等较偏原理方面的内容，有了较深的认识和理解之后，对运维工作会有很大的帮助。7.3节讲了PHP进程的部署，PHP代码的发布，这些在有些读者看来可能无关紧要，但在稍大的团队或多团队的公司里，这些会显得重要得多，提升的不仅是个人的效率，更是提升了整个团队乃至整个公司的效率，减少了沟通、等待和争论的成本。PHP的性能问题和故障是分不开的，故障处理可能是运维做得最多的工作，7.4、7.5节讲PHP的性能分析和故障处理时，讲到了多种方法用于分析PHP的性能问题，以及故障处理的一些思考。

随着云计算、移动互联网、各种海量的服务器规模和业务规模的网站项目的发展，运维所面临的挑战也越来越大，同时也要看到最近几年运维界的发展很快、交流分享也很活跃，各种技术、平台都涌现出来，我认为这对广大运维既是挑战也是机遇。本章是对PHP运维技术的一些总结，也融合了对运维技术、自动化、平台化的一些思考，希望对读者有所帮助，这也是我莫大的荣幸。

第8章　应用系统运行分析

作者简介

彭华盛，目前任职广发银行数据中心，是渠道交易应用系统团队负责人，主要负责互联网、中间业务、呼叫中心等渠道交易应用系统运维管理，同时也负责监控等自动化运维建设，对应用生产运维管理、自动化建设等工程有较深刻的见解，有兴趣的读者可以关注作者的微信公众号“运维之路”（微信号：HuashengPeng001）。

随着业务的发展，企业里新建应用系统的数量迅速增加，架构变得越来越复杂，业务对应用系统的稳定性和可靠性的要求也越来越高。在传统企业中，运维部门主要往基础云建设与业务大数据建设的方向发展，而对于承载业务运营开展的应用系统的运行情况缺乏足够的关注，导致应用运维团队疲于被动处理故障应急和善后，以及重复的整改处理工作中。

应用运维团队除了多干活以外，还面临这样一个窘境：老板认为应用运维做得不够细致，保障不力；业务认为应用运维不给力，系统不稳定；开发觉得应用运维不懂应用，只会做操作性的工作。

为解决应用运维的困境，我们开展了一系列应用运维的优化工作，包括操作性工作的自动化建设，并探索应用运维向运维开发、运维分析转型。本文是从运维分析的角度来讲解应用运维团队在应用系统运行分析方面所做的自我解放，从被动式向前赡性运维方式的转变。本章的写作目的就是帮助应用运维人员顺利完成这种转变，这里主要从应用系统运行分析的角度展开来讲。

应用系统运行分析具有以下特点：

·涵盖系统资源、数据库、中间件等标准化的性能容量数据分析。

·涵盖应用交易级（交易量、耗时、成功率、响应率等）标准化的性能数据分析。

·提供各应用系统自身业务特点的业务专家式数据分析的数据采集、数据建模的自动化支持。

·提供可视化、数据采集、数据建模分析、数据归档、数据访问接口的系统自动化支持。

·提供可订阅式的可视化报告，供业务和运维管理团队决策之用。

运行分析所涉及的运营分析系统架构如图8-1所示：

图8-1　运行分析架构图

8.1　分析模型

在制定运行分析方案时，不仅要考虑运行分析需要分析什么信息，还需要考虑如何减少方案的实施推广难度。为了便于推广，建议将数据的采集、保存和标准化分析交给机器去处理，运维人员尽量只负责观察和分析数据，为此在应用运行分析的过程中，我们定了如下3个原则。

·能自动化的则用自动化来实现：分析涉及的数据采集、加工消费、可视化都可由自动化实现。

·解决主要的问题：评估应用运维成熟度的角度有很多，鉴于各应用系统的逻辑不一致，运行分析采用2/8原则，主要进行性能、容量、客户体验的分析。

·简化分析方法：找出运行分析指标的共性，过滤不同应用系统的差异性，将分析方法原子化。

运行分析是一个长期实施的过程，仅仅依靠运维人员经验的分析很难保证运行分析的质量，因此需要将经验转化为机器分析模型。在分析模型方面，主要是对不同类型系统的分析方法进行分解，以得到一些共性的分析方法，即将经验总结出来的分析方法进行标准化的过程。以下将简单介绍其中的几个模块。

8.1.1　数据采集

要分析应用的运行情况，首先需要获得反映应用系统情况的数据，因为运行分析主要是解决性能、容量、客户体验的问题，建议从以下两方面定位好数据来源：

·反映系统软件运行情况的容量性能数据，比如CPU、内存、数据库事务等。

·反映交易指标的性能及客户体验数据，比如交易量、耗时、响应率等。

1.性能容量数据

关于性能容量数据，首先会想到的就是监控系统，监控系统的确是性能容量数据采集的重要渠道。我们在监控体系的建设过程中，对监控体系的定位除了生产运行事件报警、辅助问题定位及应急配套工具关联、事件驱动等建设目标，还定位监控体系中的监控工具作为生产运行数据采集的目标。

如果没有监控系统也没有关系，可以使用一些简单的性能工具，或者使用一些NMON工具，还可以使用一些开源的脚本去定制适合自己的系统资源数据，下面以Python获取系统CPU资源为例进行说明：

Import psutil
cpu_data = psutil.cpu_times(percpu=TRUE)
print(cpu_data)
#以上的

Python脚本可以输出

Linux操作系统下

CPU的使用率情况

通过对以上数据的分析，可以得到CPU的SYS、USER等进程使用的资源占比，可以通过IDLE看到系统CPU空闲的百分比。

2.交易性能数据

交易性能的评估指标有很多，比如交易平均处理时间、交易成功率、交易失败率、指定时间范围内的交易笔数等。对于这些数据的获取，不同的应用系统有不同的方法，比如数据库层面、应用日志等可以以入侵应用系统的方式主动获取，也可以采用类似于旁路APM等工具来获取（比如上海天旦的BPC工具），以下简单介绍这两种数据获取方式。

第一种方式：修改应用系统代码，由应用主动将性能数据记录在数据库或日志中。以数据库层面为例，推荐在交付运维部门前，在应用系统中提供一个数据运维流水表，通过这个数据流水表来获取交易的概况，以下是其中一个较为通用的数据运维流水表结构，如图8-2所示。

图8-2所示的数据运维流水表获取了交易标识、交易时间、处理状态、上下游系统关系、具体交易类型等信息，有了这些信息不仅可以知道待分析的应用系统的交易性能情况，还可以看出是什么交易、哪个关联系统影响了整体性能。当然，如果应用系统没有设计此类数据运维流水表，也可以根据实际情况收集类似的数据，但最好能够获得一笔交易的处理时间、状态、下游系统这3个关键数据，进而通过这3个数据定位一笔交易的处理性能。数据运维流水表的思路同样也适用于文本日志的设计，比如单独记录满足条件的应用日志。

主动在应用代码中注入数据运维流水表或文本日志的方案，还依赖于开发团队的支持力度。在开发团队的支持力度相对较弱时，建议优先在总线类应用系统中注入，主要是因为总线类的节点在企业内部制定了规范的接口标准，总线类节点往往能够比较方便地获得成功率、耗时、交易量波动、响应率等数据。

图8-2　数据运维流水表结构

但是需要记住一点，无论是数据库还是日志，都要通过异步方式来实现。

第二种方式：借鉴基于网络旁路APM工具抓取应用性能数据。这类APM工具的工作原理主要是在网络层面部署探针抓取应用报文，并将应用报文发送到集中的应用工具，通过对报文数据进行解码、清洗、合并等操作，获取以下数据信息。

·交易报文类的信息：返回码、交易码、操作码、成功率、渠道标识、交易响应率、交易笔数、响应时间、流水号、金融交易里的金额、账户等关键数据。

·网络通信信息：丢包、TCP重传、带宽、连接成功率等。

·系统软件信息：数据库、中间件使用响应、并发、连接数情况等。

APM工具往往可以提供实时数据或批量接口，数据来源更加稳定，且网络旁路抓包的方式不会影响应用性能。

3.数据采集解决方案

我们利用监控工具解决了上面提到的容量性能与交易性能数据来源，采集方案如图8-3所示。

常见的应用架构主要可以分为Web、队列、应用、缓存、数据库这5层，每一层均由负载均衡或主备方式部署的服务器节点组成，服务器节点可通过传统专业的监控工具来采集涉及资源类、系统软件运行指标等的数据。除了要获得每个节点的运行数据之外，还要获得每个节点之间的运行情况，在这里我们选用基于网络旁路抓取报文的APM工具来获取各个点之间的数据。

通过传统的监控工具与APM工具，我们获取了点、线形成的二维平面的运行数据，这种组合方式，就如同我们的GPS地图软件，传统监控工具获取每个坐标站点的情况，APM工具获取站点与站点之间是否塞车情况的数据。

图8-3　数据采集技术方案

8.1.2　数据模型

在应用运行分析的过程中，不同应用系统的架构和业务逻辑均不一样，需要通过标准化的数据分析模型简化分析方法，主要建立了以下几类数据模型：

·系统资源数据模型。

·数据库数据模型。

·交易性能数据模型。

1.系统资源数据模型

对系统资源数据模型的分析，需要将日常分析系统资源的经验指标化，转化为分析策略，由分析平台以定时批量的方式，对一个应用系统一段时期内的资源使用情况数据，按预指定的策略进行统计分析，并输出可供决策的报表数据。

下面仍以CPU为例，CPU数据通常有以下3项需要关注。

·%user：显示用户进程消耗CPU的时间百分比。

·%system：显示系统进程消耗CPU的时间百分比。

·%idle：显示CPU处于空闲状态的时间百分比。

我们在日常实时分析过程中，总结出以下经验。

·%usr+%system＜70%，%idle＞30%：较好。

·%usr+%system＞80%，%idle＜20%：报警。

·%usr+%system＞90%，%idle＜10%：糟糕。

在运行分析的过程中，还需要结合应用系统的特点对一段时间内的趋势进行分析，比如应用系统日终备份或清算等操作，CPU使用率增大，系统会繁忙；业务查询一个报表的查询功能占用CPU的资源可能会在短时间内有一个高峰值；应用系统功能设计不合理，导致功能的执行会消耗大量的系统资源；这些性能数据不能准确反映出系统资源是否满足现有应用系统的性能要求。为此需要根据不同的业务系统制定一个时间段的分析策略，下面为一个实时交易系统制定一个策略（策略可根据企业性质的不同进行定制）。

抽取周一到周五工作日业务高峰期（9：30~11：30）的CPU性能数据，以15秒为采样间隔。连续20次采样出现CPU性能超阈值（%usr+%system>80%或%idle<20%），则当天存在潜在性能瓶颈，这个潜在性能瓶颈可能由一个正常的操作引起并会在30分钟后得到释放，所以出现一次并不会马上输出CPU资源容量不足的建议。只有当连续3天出现上述情况的潜在性能瓶颈时，才会输出容量不足的建议。

以上的模型中，高峰期的时间段、连续的天数、每天连续出现的次数和指标数据都可以根据运维人员的需要进行调整。

在策略制定之后，分析平台定时执行对以上数据的分析，将扩容的建议与原因作为一个运行分行模块报告，先由应用运维确认是否存在应用缺陷所导致的因素，如非应用缺陷所导致，则提交到基础设施团队，由基础设施团队评估扩容事宜。

类似资源层面的指标，还包括内存、磁盘IO、磁盘空间使用、网络信息的使用情况，相应策略的制定需要有一个使用调优的过程。

2.数据库数据模型

关系型数据库仍然是目前应用系统最重要的数据库，对数据库的分析可以直接反映系统的性能情况。

（1）对数据库进行实时数据分析，往往是通过一些数据库工具对数据库进行监控分析来实现的。下面以DB2为例，我们往往使用Event Monitor和Snapshot，两者都可以用于实时采集并分析数据库的使用情况，例如数据库缓冲池（buffer pool）的使用状况、即时的数据库locking状态、SQL语句的信息等。

选择任意一个工具都可以，两者都提供如表8-1所示的信息：

表8-1　数据库运行指标类型

通过在高峰期对数据库锁、SQL语句等进行分析，不仅可以得到一些数据库参数配置的建议，还可以得到一些应用层面的调优建议，比如对Statements项的分析可以得到高峰期执行时间最长，资源消耗最大的数据：

·执行次数top20的SQL。

·消耗时间top20的SQL。

·返回记录数top20的SQL。

有了这类SQL语句信息，就可以通过DB2的建议工具生成SQL优化建议，通过新增一些数据库索引来提高应用的系统性能。

（2）通过数据库的数据统计信息可以得到数据量大的表，任由数据库表数据量增大而不做数据迁移或清理，将会导致数据库性能瓶颈的出现。仍以BD2为例，可以通过以下SQL语句查到一个数据库中各表的数据（该表的数据需要在重整后才能得到最新的统计信息）：

select a.card as rows_num,b.colcard as Cardinality,create_time,alter_time,stats_time from syscat.tables a left join syscat.columns b on a.tabschema=b.tabschema and a.tabname=b.tabname order by a.card desc

上述代码各字段具体说明如下。

·rows_num：数据行数。

·Cardinality：数据基数。

·create_time：表创建时间。

·alter_time：表修改时间。

·stats_time：信息统计时间。

通过以上SQL语句可以导出数据量排行前10位的数据表，提供给运维人员以重点分析是否需要对该10张数据表进行数据迁移，以减少存量数据。

（3）另外，数据库索引在实际调优过程中是最直接、最快速、最有效的方法，索引的分析有极为重要的意义。同样，数据库也为分析数据库索引提供了丰富的方法：分析索引的引用次数，删除无用的索引；也可以对索引是否重复进行分析，比如索引1（id，name），索引2（id），这里的索引2即为重复多余的索引。

3.交易性能数据模型

衡量一个交易系统的性能，还需要从交易层面进行分析，以下将介绍几个常见的交易性能数据模型。

（1）交易笔数

交易笔数的分析，主要是通过在指定时间内对交易发生的笔数进行分析，建议在交易笔数的分析中定义如下4种模型：

1）按笔数分析的情况。

交易总笔数：

交易成功笔数：

交易失败笔数：

交易异常笔数：

对交易总笔数进行分析，可以得到系统每天或每天的高峰期指定时间段的处理的交易总笔数、交易成功率、交易失败率、交易异常比率（交易异常主要是指交易超时，有别于交易失败，交易失败可能是正常的，比如余额不足导致的交易失败）。通过连续一个月的数据分析，运维人员可以查看交易的分布，分析交易是否呈上升的趋势，系统处理交易的成功率是否下降等情况，再进行深入分析是什么因素引起的，最终提出优化建议。

2）按渠道分析的情况。

渠道：

交易总笔数：

交易成功笔数：

交易失败笔数：

交易异常笔数：

上面是对应用系统的整体处理能力进行分析，但由于现在的交易系统往往不是孤立存在的，关联系统的性能下降也可能会导致应用系统的性能下降，比如：分析应用系统的并发连接数为100，如果下游某个系统交易缓慢，每笔交易都是30秒才返回，那么当该下游系统交易数量变大时，可能会导致并发处理线程不能及时释放而无法处理更多的交易。所以还需要按渠道分析的模型，该模型可以看出哪些下游系统处理能力比较差，是系统整体处理笔数模型的一个补充。

3）按交易码分析的情况。

交易码：

交易总笔数：

交易成功笔数：

交易失败笔数：

交易异常笔数：

交易码分析的情况则是从某个功能设计的角度来分析，比如：如果某几个交易码的失败率明显高于其他系统，则有可能是该交易设计本身有问题，或者该功能有缺陷，失败率高。

4）按交易返回码分析的情况。

错误码：

交易总笔数：

交易成功笔数：

交易失败笔数：

交易异常笔数：

交易返回码是对一些交易渠道处理结果的反映，例如：如果有大量的交易码都是通信超时的，那么可能是某个下游系统的处理能力有问题所导致的。这样的分析模型可以帮助发现交易异常的类型是性能问题，还是功能设计问题。

（2）交易平均耗时

1）系统整体交易处理笔数：对应用系统整体交易处理耗时进行分析，可以看到当前系统的平均处理能力，再通过一个周期的交易耗时来进行分析，可以看到平台整体的处理能力是否下降。例如：一个应用系统在上线初期，虽然交易性能不高，但由于存量数据不多，一般不会出现性能瓶颈。当存量数据越来越多时，系统每笔交易的平均处理性能就会显现出来。

2）按渠道、交易码统计交易耗时：类似于上面交易笔数的分析模型，对交易耗时的分析，除了要对整体交易进行分析以外，还要分渠道、交易码进行分析。这两个模型可参考上面交易笔数的模型进行设计。

（3）异常交易笔数

单独对异常交易建立一个分析模型，因为在实际的应用性能分析中，除了交易成功、交易失败以外，还有一些无反馈交易处理结果的数据，这类交易可能导致一些单边账交易，不容易被发现，但其出现后影响又会比较大，比如在呼叫系统中如果IVR（自助语音）交易超时，那么该交易将直接转接到人工坐席；如果异常交易过多，就需要更多的坐席支持，如果坐席人力不足则会导致人工坐席满线而无法提供服务的情况。故有下面所述的这个分析模型。

4.数据模型解决方案

建立数据模型并非将数据做一个简单的汇总和展示，而是从当前运行的数据中发现潜在的运行问题。那么，如何判断当前运行数据是否存在潜在问题则十分关键。在这里我们主要是建立运行基线，通过将当前运行数据与运行基线进行偏离分析来开展工作。

所谓运行基线即根据上一分析周期历史运行数据进行采样，并按一定的周期计算一个平均值，作为这个时点的基线，图8-4是运行基线的建设方案。对各应用系统建立工作日和节假日的系统基线，每一条基线由24个基线值组成，每个基线值是某一项运行指标数据模型在这个时间周期内数据采样的平均值。在实际运行过程中，我们发现每个小时的基线点都出现了一些分析误差，比如每天晚上凌晨1时到6时之间，实际发生的交易数据很少，波动性很强；或者每天凌晨3时50分至4时15分之间因为批量定时任务导致数据库使用性能突增，3时与4时的数据波动较大。为此，一是要去除干扰的运行数据，二是将基线粒度进一步缩小，计划后续在工作日与节假日的基础上，再细化为星期线，基线值由原来的1小时细化到10分钟。

图8-4　运行基线技术方案

有了基线，就有了系统运行好坏的参考值，接下来要制定策略以定位什么是好，什么是坏。为了减少误报率，主要从以下三个方面进行处理。

·按偏离次数：以CPU为例，我们将CPU值在基线上方超过25%的点取出，根据点的多少判断是否存在某些时点出现了性能上升的问题，需要应用运维人员分析对应时点的运行情况。

·按整体偏离趋势：仍以CPU为例，我们根据当前运行点的指标与基线进行对比，查看两者正负比率，如出现当前运行点大部分均偏离基线上方，则整体运行性能有下降的趋势，需重点评估是否需要扩容。

·清理干扰数据：主要是在分析过程中，针对波动性很强的数据进行清理，比如因为交易量少而导致的波动性问题就不在运行分析范围之内。

8.2　运行分析平台建设

通过前面所讲数据采集与数据模型的建立，数据分析的基本模块已经实现，接下来需要考虑如何将运行分析推广下去。运行分析平台架构如图8-5所示。在运行分析的推广过程中，可能会遇到如下这些问题：

图8-5　运行分析平台架构

·需要手工采集运行数据，且数据收集耗时很长。

·平均耗时120毫秒是正常的吗？无法快速找到上个月的分析情况进行对比。

·分析的报告无法及时提交给决策层进行分析。

·缺少一个可视化的系统。

本节主要介绍数据的采集、分析、决策，以及数据可视化平台的建设方案。

8.2.1　数据采集接口

在数据采集接口方面，我们最初考虑由开发部门支持运维，将应用系统的运行数据主动输出成标准化的数据格式：

·与开发部门沟通，由开发部门提供标准化的数据运维流水表和运维流水日志，运维部门在变更版本交付流程中进行标准化落地控制。

·着手重构LOG4J日志包，即将LOG4J的主函数进行重构，在应用按标准异步写入应用日志文件时，格式化一份格式统一的运行日志文件，这个改造减少了开发对应用代码的修改，同时无须安装其他日志代理。

在实施过程中，我们发现由开发部门配合运维部门修改应用旁路，向运维输出有关应用运行情况的数据，阻力会比较大，所以我们分成如下两个步骤进行：

第一步是挑选节点总线类的应用系统进行改造，因为是挑选个别应用系统来进行改造，开发部门没有很多推脱的理由。选择总线类的应用系统主要是因为这类系统接收了大量渠道系统的交易，并负责转发到关联系统，在总线类的应用中可以获得各渠道的交易处理时间、交易处理情况、关联系统返回的情况等信息。也就是对这类系统进行改造所获得的数据，可以用来分析各渠道的整体运行情况，能起到事半功倍的作用。

第二步是我们发现基于网络旁路抓包的APM工具可以实现无须改造应用就能获得交易处理时间、成功率等数据，这些数据虽然不能像应用在开发阶段注入运行数据时那么精细，但是采用旁路抓包的方式实施落地更高效，而且在性能分析方面也能满足80%的需求。

8.2.2　数据分析模块

关于数据分析模块，就如上文提到的，建立运行基线，拿当前运行的数据与运行基线进行比较，通过偏离度与偏离数量判断运行的情况。在建设数据分析模块时，应采用如下几个步骤：

（1）手工分析数据，发现运行问题，并将分析到的运行问题反馈给应用管理员，由应用管理员进行分析，最终统一用分析数据反推开发中心进行优化，比如刚开始的按渠道分析，将交易单笔平均耗时超过2秒的交易渠道发给开发团队，督促开发进行优化。

（2）第一步所述的手工分析的方法可行后，第二步就是将数据分析的数据采集范围、运行数据比较的策略进行简化、原子化，并将原子化后的操作实现自动化，通过定期批量的方式对数据做汇总分析，分析的模型可参考上面提到的基线比较的方式。

（3）第三步是持续深化运行分析的深度与粒度，第二个步骤主要是整体的运行情况，到了第三个步骤就是抓运行毛刺，通过将毛刺出现的时间、性能表现集中展示出来，督促运维或开发团队优化性能。

8.2.3　推广

上面已经提到如何在运维团队中推广运行分析工作的开展，这还不够，运行数据分析除了为保障运维团队生产的决策提供数据支持外，还需要推广到IT管理团队及业务团队。为了更好地推广，需要强化数据分析结果的可视化效果，例如如下两个方面。

（1）权限：控制不同的机构、不同的用户查看不同的运行情况分析报告。

（2）提供订阅式的报告推送方式：用户可以通过订阅的方式，查看不同的应用系统或场景的运行分析报告，在审核通过后，系统将通过邮件的方式将报告发送到用户邮箱中。

8.3　呼叫中心系统运行分析示例

本节主要通过介绍呼叫中心系统的运行分析来回顾上面提到的运行分析技术方案。呼叫中心系统的运行状态主要包括话务、坐席两部分，其中话务部分又包括网关、自助语音、录音、多媒体等十几个子模块；坐席部分则主要包括坐席应用、运管、报表、网关等模块。整个应用系统涉及150个操作系统分区，如此多服务模块的运维管理必须进行前瞻性的运行分析才能提前发现问题，减少问题。

运行分析主要如下几点：

·根据运维及业务的需求，对运行分析的服务器范围及交易分析指标做加法与减法。

·根据业务需求增加个性化的业务分析方向。

·根据分析的数据问题做进一步分析，并转化为后续优化计划的决策依据。

8.3.1　确定分析方案

针对呼叫中心系统的运行分析情况，我们与业务部门进行了沟通，确认了客服满意度、话务量、坐席菜单响应率是业务关注的几个重要指标，同时业务部门发现近期人工坐席的话务较以往有所增长，希望知道问题的原因。这个过程就是根据业务的需求做减法与加法。

针对上述沟通情况，我们制定了如下分析方案。

（1）资源类指标。

·系统资源分析：分析系统CPU、内存、IO的资源使用情况。

·数据库性能分析：分析高峰期数据库索引、锁、大数据表等维度的数据建议。

·WebSphere中间件分析：分析中间件连接数、GC回收等维度的数据建议。

（2）应用性能指标。

·交易平均耗时分析：交易平均耗时分析可以整体感知平台的处理能力概况。

·按渠道统计交易平均耗时：发现是否涉及关联系统耗时过长的情况。

·按交易码统计交易平均耗时：发现某几笔交易耗时异常的情况。

（3）细化的交易级指标。

·IVR超时交易转人工分析：业务希望尽可能多的客户需求直接在IVR（自助语音系统）中完成交易，如IVR交易超时过多，将导致客服人力不足、系统等一系列影响。

·IVR交易量情况分析：IVR交易量的分析主要是对交易量分布、交易最多的菜单进行分析，通过对交易最多的菜单进行优化将会最快地提高效率。

·按交易码统计成功率：发现交易的成功率情况，以确认是否存在交易设计的问题或交易异常的情况。

（4）业务指标：
业务指标个性化较强，主要以人工分析为主。

·呼叫中心客服满意度分析：从业务最关心的客服满意度指标进行分析，包括总进线量、总接通数、接通率、人工转接数、排队返回主菜单、20秒水平、平均应急速度、平均通话时长、话后处理时长、平均整体时长、坐席待机率几个指标。

·话务量情况分析：分析话务量在一个月内的分布情况，发现话务量高峰期的特点，以协助业务人员安排座席人员班次的人数。

·重复来电情况分析：发现是否有恶意来电攻击的情况。

·话务平台录音丢失专项分析：分析录音丢失的情况。

8.3.2　问题分析案例介绍

在分析过程中，以业务部门需要分析的关于近期人工话务量增长较多的情况为例，分析的情况如下。

·近一个月话务量的分布有如下特点：星期五的话务量相比其他几天增长了20%，本月的话务量相比上月话务量有明显增长的趋势。

·本月下旬25日开始每天均出现IVR超时的情况，IVR超时情况集中出现在上午9时到11时。

·交易整体耗时方面，从本月25日开始，每笔交易的平均耗时由原来的150毫秒上升到250毫秒（500毫秒将对客户体验产生明显影响）。

·客服满意度分析中排队返回主菜单数、坐席待机率指标均有变差，问题同样集中在9时到11时，存在坐席不足爆线的情况。

从以上三个分析点可以看出：在话务量未明显增长的情况下，IVR在上午9时至11时出现了IVR交易超时情况，这些超时交易全部转到人工坐席，人工坐席出现爆线情况。初步结论是：业务部门需要先应急扩大9时至11时的坐席人员数量，技术方面需要进一步分析9时至11时是否有一些特别的交易导致整体超时的问题，重点评估24日晚是否有因生产变更而带来一些业务功能，这些业务功能主要在上午9时至11时发生交易。

经过分析最终发现了问题的原因，24日晚投产了一个查询前一日客服处理情况的实时报表，业务人员往往会在次日上班9时后进行查询。由于该功能需要在多张上亿条数据的表中关联查询数据，效率低下，因此影响了整体的性能。

所以运维人员针对IVR人工话务量增多方面的分析结论附上问题原因之后，提出了如下建议。

（1）近期工作建议：

·因该查询功能是T-1日数据，建议开发团队将该功能设置在凌晨批次生产报表，减少在业务高峰期进行统计类的查询业务。

·建议运维人员对IVR超时交易进行交易级监控配置，当IVR出现交易超时时提供监控报警。

·建议对生产实时表数据过亿的表格进行历史数据迁移，以保持实时表的高效。

·建议业务人员对爆线情况进行现场监督，及时调配好班次人员。

（2）中期工作建议：

·建议将查询报表类的模块单独拆分为独立的应用服务。

·建议将查询报表类的数据库进行配套的拆分。

·建议对数据库中的数据实现读写分离，优先考虑增加一个复制库，在业务空闲期间定时生成批量报表，对于需要实时查询的数据采用读取复制库的方式；同时研究读写分离中间件。

·建议对报表类交易设计降级开关，当实时交易发生异常时，要能够快速暂停报表类交易。

8.4　小结

上述应用运行方案仍在实施中不断进行完善，后续主要的优化方向是：

（1）通过应用运行分析的开展，全面转变运维人员的工作思维，由操作向分析进行转变，由局部到整体进行把控，由被动应急向主动出击进行转变。

（2）为应用系统的资源、性能运行情况建立一个运行基线（优、正常、差），为系统管理提供容量决策支持，反推开发团队优化应用性能，推动应用架构改造，进一步保障系统稳定运行。

（3）通过大数据技术的引入，提高运行分析效率，将运行分析由T+N向T日实时分析的方向发展，为监控自动化工具提供预测性的事件预警等，完善运维自动化体系的建设。

（4）提高应用运行分析的深度，将运行数据与业务运营活动的开展、客户体验等方面进行数据关联分析，挖掘影响业务开展的功能设计或应用性能因素，促进业务开展。

第9章　虚拟化中存储配置典型场景：启动风暴

作者简介

蒋迪，上海沃帆信息科技有限公司资深虚拟化基础架构工程师，从事云平台研发工作，主要包括OpenStack、oVirt的虚拟化、网络、存储组件及其在桌面云领域的应用。涉及虚拟化产品应用（云桌面、云服务器），云平台架构与相关开发（自主产品与开源软件），嵌入式系统与设备（ARM架构），分布式存储（GlusterFS）。多次参与银行与教育领域的私有云架构设计与实施。

启动风暴是私有云桌面中经常会遇到的问题，不管是开源平台还是闭源平台，或多或少都会遇到此类问题。本章将以oVirt为基础平台介绍其平台存储配置，并针对这种配置下的桌面云启动风暴进行调优，然后在本章末尾处会介绍类似于杀毒风暴的解决办法，希望通过本章的介绍，读者在解决此类问题时能够尝试从不同的角度进行思考。

在启动虚拟桌面时，虚拟机系统内部会产生远高于正常运行状态时的CPU负载与I/O请求，当同时启动的虚拟机达到一定数量时，就会对服务器造成极大的压力，这种情况一般称为“启动风暴”。在私有桌面云中，尤其是教学环境中，会定时定量集中启动一批桌面，如果启动时服务器的CPU能力或存储能力未能满足其需要，那么就会对全部桌面的启动造成影响，甚至假死。类似于启动风暴对存储I/O会造成压力的场景还有登录风暴、杀毒风暴（扫描风暴）等。

启动风暴的发生条件并不确定，因为它与服务器的CPU、存储、系统配置及虚拟机的参数直接相关，所以改善启动风暴大多是从这些相关方面直接入手。

以我个人的经验和客户反馈的情况来说，其中存储的改变对启动风暴的影响是最大的，而针对存储的优化则可以从最基本的存储性能及其存储结构方面进行调整。

本章将尝试模拟桌面云的一般场景，启动相当数量的虚拟机来重现启动风暴，尝试使用分层存储的方法处理之，然后在现有方法的基础上提出一些较为常用的改进措施，希望对读者有所帮助。

9.1　oVirt虚拟化平台配置介绍

在开始存储结构优化的实验之前，我们先来了解一下云平台的一般存储配置，包括实例硬盘的创建方式与所在的存储位置，以便基于此进行优化。

9.1.1　存储配置背景知识

1.硬盘分配方式

比较成熟的开源虚拟化或云计算平台包括OpenStack、CloudStack和oVirt等，它们都有比较清晰的模板和实例概念，同时也有模板与实例镜像存储位置相关的存储域配置。为了区分虚拟机中的模板与实例，本章默认模板即原始虚拟机，所有新实例的创建都依赖于它；实例即依赖模板创建的虚拟机，是具有运行生命周期的虚拟机的通称。

在这些平台中，一般有两种新建实例的方式：克隆与增量。

（1）克隆，
即完整克隆模板配置与硬盘，硬盘的创建方式一般为cp或qemu-img convert。通过上述方式创建的实例，其硬盘与模板硬盘相对独立，在服务器存储上拥有各自的扇区位置，所以在读写操作时受机械硬盘磁头引起的小区域并发问题影响较小，缺点是创建时需要消耗一定的时间，不能满足秒级创建的需求。

（2）增
量，
即新建的虚拟机只复制模板配置信息，但其硬盘文件是通过qemu-img的backing file方法创建的，基于backing file创建的硬盘不能是raw格式，命令如下：

[root@localhost ~]# qemu-img create -b hda.qcow2 -f qcow2 hda-new.qcow2

如此创建的硬盘对模板硬盘的依赖较大，非增量文件的读操作（比如启动系统）绝大部分在模板硬盘上进行，所以在传统机械硬盘上多个实例的并发启动风暴更容易发生在此种格式的硬盘上。

2.集群调度策略

实例的启动在虚拟化平台中的一般调度如图9-1所示。

从图9-1中我们可以看到，一个实例在真正启动之前会根据一定的策略被安排到某集群的某个主机上，而且根据集群状态可以动态迁移至其他主机，限于篇幅，本章在此将不展开迁移的情况。

我们在实现调度策略的时候应主要考量如下几点：

·集群的CPU类型（Family）、集群负载状态。

图9-1　虚拟机调度简图

·主机中运行的虚拟机数量、主机历史健康度。

·主机CPU用度、网络用度、内存用度、存储I/O用度，以及它们超过一定阈值的时间。

9.1.2　模板与实例同一存储

当模板与实例存储在同一文件系统中时，我们创建虚拟机的常用方法有两种——使用模板硬盘的hardlink作为实例硬盘的backing file来创建增量硬盘，或者直接复制模板硬盘。

在oVirt的数据存储域中我们可以看到如下代码段中的目录结构，其中每个UUID都代表了一个对象，lease文件、metadata分别为文件锁和元数据：

[root@localhost ~]#tree data/
4440c3f9-ba1f-4803-86ac-d59c694c61c1/ # 存储域

├──

dom_md│

├──

ids│

├──

inbox│

├──

leases│

├──

metadata│

└──

outbox├──

images│

├──

d25c4d22-15c1-4f3c-8aef-2ed74deb4723 # 模板硬盘

UUID│

│

├──

2bf1ba4c-3fb5-4e25-9368-cc96724b1a63 # 模板硬盘文件

│

│

├──

2bf1ba4c-3fb5-4e25-9368-cc96724b1a63.lease│

│

└──

2bf1ba4c-3fb5-4e25-9368-cc96724b1a63.meta│

├──

d6d7e74d-76fc-48c6-b3c6-3c506b98e73e # 实例硬盘

UUID│

│

├──

2bf1ba4c-3fb5-4e25-9368-cc96724b1a63 # 模板硬盘文件硬链接

│

│

├──

2bf1ba4c-3fb5-4e25-9368-cc96724b1a63.lease│

│

├──

2bf1ba4c-3fb5-4e25-9368-cc96724b1a63.meta│

│

├──

81031024-5942-4086-901b-f991777ff32a # 实例增量硬盘文件

│

│

├──

81031024-5942-4086-901b-f991777ff32a.lease│

│

└──

81031024-5942-4086-901b-f991777ff32a.meta│

└──

ed834c83-82c3-4ea3-a08d-82cfbb213af4│

├──

3e56f6dd-be45-48d0-aba8-830d0ee1f068│

├──

3e56f6dd-be45-48d0-aba8-830d0ee1f068.lease│

└──

3e56f6dd-be45-48d0-aba8-830d0ee1f068.meta└──

master
├──

tasks
└──

vms

模板硬盘和实例硬盘处于同一文件系统上，oVirt使用了hardlink链接了模板d25c4d22-15c1-4f3c-8aef-2ed74deb4723下的硬盘2bf1ba4c-3fb5-4e25-9368-cc96724b1a63和实例6d7e74d-76fc-48c6-b3c6-3c506b98e73e下的2bf1ba4c-3fb5-4e25-9368-cc96724b1a63。

注意

在同一文件系统上使用hardlink而非softlink的原因如下。

·节省inode使用量。

·由于省去了访问softlink的inode这一步，直接访问源文件的inode会带来性能上的稍许提升。

·改变源文件路径不会影响hardlink文件的访问。

·冗余性及安全性考虑，只有删除了inode上的所有引用，此inode才会被删除。

9.1.3　模板与实例分离存储

当模板硬盘与实例硬盘分别存储在不同的文件系统中时，就不能使用hardlink创建虚拟机。oVirt跨存储域创建实例的方式为克隆创建，而不是直接使用backing file创建增量硬盘。我们从其存储域的概念就可以推测出它这么做的原因，即：

·存储域间没有相关联的模板硬盘与实例硬盘，因此易于存储域的管理（删除、导入存储域与其中的虚拟机）。

·对于跨文件系统的存储域，使用复制而不是增量创建更易于减少模板所在存储域的负担。

有时我们在真实部署虚拟桌面的场景中，往往需要多个本地存储域（比如SAS与SSD）混合使用。所以在以下的测试中，我也会使用跨存储域创建增量硬盘的方式。

9.1.4　无状态实例的硬盘与快照分离存储

oVirt中存在一种“无状态”实例，该实例的创建过程如下：

已有模板硬盘A，我们采用克隆或增量的方式创建实例硬盘B，勾选“无状态”以后，虚拟机运行会自动创建硬盘B的增量硬盘C，对实例所有的改动都在C上，当虚拟机关机之后，平台删除C。这样一来，所有文件的改变便随之删除，我们称这种工作方式的实例为“无状态”实例。

这种状态下的虚拟机，存在1~2个增量硬盘，在以下的实验中，我会将其分别放置在不同的文件系统中进行测试。

9.2　启动风暴相关系列实验

接下来，我们直接使用QEMU启动一批虚拟机，并观察期间的服务器负载情况，然后在更换存储配置的情况下再一次启动它们，最后对比两次的服务器负载情况。实验过程中，不考虑qcow2格式与raw格式的影响，统一使用qcow2格式。所有的虚拟机均使用VirtIO接口、qcow2格式硬盘、Windows XP 32位操作系统，无任何附加软件。同时，为减少Windows XP系统启动后对快照硬盘产生额外的硬盘写入操作，比如日志记录、例行任务等系统操作，我在其开机运行1个小时之后再进行模板制作。

实验服务器配置为双路X5670@2.93GHz、64GB内存、一块Intel 480GB企业级SSD、一块WD 1TB企业级机械硬盘、操作系统为CentOS 7.1。

9.2.1　模板配置

模板的具体配置方法如下：

# 虚拟机配置单路单核

1 GB内存，显示为

Spice协议

[root@localhost ~]# cat base_xp.sh
#!/bin/bash
/usr/libexec/qemu-kvm -no-user-config -nodefaults \
-m 1024M -cpu host -smp 1,sockets=1,cores=1 \
-net user\
-monitorstdio -vgaqxl -global qxl-vga.vram_size=67108864 \
-spice port=7001,ipv4,disable-ticketing \
-drive file=hda.qcow2,if=none,id=drive-virtio-disk0,format=qcow2,cache=none,werror=stop,rerror=stop,aio=threads \
-device virtio-blk-pci,scsi=off,bus=pci.0,addr=0x7,drive=drive-virtio-disk0,id=virtio-disk0,bootindex=1 \
-device virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x8

9.2.2　实验脚本

（1）创建20个以hda.qcow2为backing file的硬盘：

[root@localhost ~]# create-imgs.sh
#!/bin/bash
fori in `seq 11 30`
do
qemu-img create -f qcow2 -b hda.qcow2 hda-$i.qcow2
done

（2）一次性启动20台实例：

[root@localhost ~]# start-vms.sh
#!/bin/bash
functionstartvm {
/usr/libexec/qemu-kvm -no-user-config -nodefaults \
-m 1024M -cpu host -smp 1,sockets=1,cores=1 \
-net user\
-vgaqxl -global qxl-vga.vram_size=67108864 \
-spice port=$1,ipv4,disable-ticketing \
-drive file=$2,if=none,id=drive-virtio-disk0,format=qcow2,cache=none,werror
=stop,rerror=stop,aio=threads \
-device virtio-blk-pci,scsi=off,bus=pci.0,addr=0x7,drive=drive-virtio-
disk0,id=virtio-disk0,bootindex=1 \
-device virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x8
}
fori in `seq 11 30`
do
startvm 70$i hda-$i.qcow2
done

（3）测量，采样频率为1Hz：

[root@localhost ~]# iostat -cdmx 1|tee 20-xp.iostat-cdm.out

（4）数据预处理，我们只需要总读写速度（MB/s）、总读写请求（Requests/s）、CPU利用率（%user，%sys）即可：

[root@localhost ~]# awk 'BEGIN {print "cpu usage\n";i=0};$1 ~ /[0-9]/ {print i,$1+$3;i+=1;}' 20-xp.iostat-cdm.out > 20-xp.iostat-cdm-cpu.out
[root@localhost ~]# awk 'BEGIN {print "sdb info\nTime IOPS RMBpsWMBps";i=0};$1 ~ /^sda/ {iops=$4+$5;print i,iops,$6,$7;i+=1;}' 20-xp.iostat-cdm.out > 20-xp.iostat-cdm-sdb.out
[root@localhost ~]# awk 'BEGIN {print "sdb info\nTime IOPS RMBpsWMBps";i=0};$1 ~ /^sdb/ {iops=$4+$5;print i,iops,$6,$7;i+=1;}' 20-xp.iostat-cdm.out > 20-xp.iostat-cdm-sdb.out

（5）可视化示例，由于请求数比速度高很多倍，因此为了方便数据显示我们将请求数除以10：

[root@localhost ~]# cat plot_storage.py
#!/usr/bin/env python
importnumpy as np
importmatplotlib.pyplot as plt
f_c = file('20-xp.iostat-cdm-sda.out').readlines()
c = np.array(map(str.split,f_c[2:]),dtype='float')
plt.plot(c[:,0],c[:,1]/10,label="IO Requests/s", color="red", linewidth=2)
plt.plot(c[:,0],c[:,2],label="Read MB/s", color="blue", linewidth=2)
plt.plot(c[:,0],c[:,3],label="Write MB/s", color="blue", linewidth=2)
plt.legend()
plt.show()

9.2.3　WD 1TB机械硬盘启动Windows XP实验

在WD 1TB机械硬盘上虚拟机在实例启动期间其服务器的CPU、I/O用度分别如图9-2和图9-3所示，可以看到，系统在第5秒左右完成启动，第8秒左右自动登录进入桌面，期间CPU用度较为平稳、读请求较多。

图9-2　WD 1TB硬盘上启动单台XP时的CPU用度

图9-3　WD 1TB硬盘上启动单台XP时的I/O用度

在WD 1TB硬盘上20台使用增量硬盘的虚拟机实例，在启动期间其服务器的CPU、I/O用度分别如图9-4和图9-5所示。期间虚拟机实例先后进入桌面，在第300秒左右全部进入，服务器CPU用度在刚开始很繁忙，读请求也很多并且几乎达到硬盘的极限IOPS，一直持续到登录前，然后开始下降并趋于平稳。

图9-4　WD 1TB上启动20台XP时的CPU用度

图9-5　WD 1BT上启动20台XP时的I/O用度

9.2.4　Intel 480GB SSD启动Windows XP实验

在Intel 480GB SSD上使用增量硬盘的虚拟机实例，在启动期间其服务器的CPU、I/O用度分别如图9-6和图9-7所示。虚拟机实例在第6秒左右便进入桌面，且服务器CPU用度平稳，读写请求数也比较正常。

图9-6　SSD上启动单台XP时的CPU用度

图9-7　SSD上启动单台XP时的I/O用度

在Intel 480G SSD上使用增量硬盘的虚拟机实例，在启动期间其服务器的CPU、I/O用度分别如图9-8和图9-9所示。相比于增量硬盘在机械硬盘上时，虚拟机实例在第35秒左右便全部进入桌面，期间服务器读请求与预期一致。需要注意的是，由于所有虚拟机在30秒左右几乎同时开始登录，所有写请求累加才导致图9-9中I/O读请求在第40秒产生的高峰。

图9-8　SSD上启动20台XP时的CPU用度

图9-9　SSD上启动20台XP时的I/O用度

9.2.5　实验结论

从9.2.3和9.2.4两小节的实验结果可以看出，在同样的硬盘配置下，启动时写请求数量明显高于读请求数，且服务器CPU较为繁忙；而使用SSD作为存储的虚拟机实例即使是在多个同时启动的情况下，也能较快地将其全部启动，而在机械硬盘存储的情况下启动则比较吃力。原因就是在启动20台的实验中，由于硬盘本身的I/O带宽及IOPS限制，WD 1TB的实例需要在读写队列中等待较长的时间，而SSD的实例的等待时间则很短，所以前者启动较慢、后者启动较快，因而此实验可以说明基础存储性能的提升能够有效地改善启动风暴。

同时，读者也可以参考以上脚本对虚拟机硬盘文件的存储结构进行变化，即将增量硬盘文件与模板硬盘文件分别置于不同的存储介质上进行实验，本章后续章节中将直接阐述其结果。需要注意的是，在进行实验时，制作模板之前应确保尽量关闭自检服务与计划任务，防止虚拟机实例在进入系统后仍然对硬盘进行扫描活动而产生干扰。

9.3　私有云中处理启动风暴的常用方法

本节将介绍一下我们在私有云中处理启动风暴的方法，这里所介绍的方法可能并不适用于所有应用场景，但相信仍有一定的参考价值。

9.3.1　启动排队

启动排队是改善启动风暴比较常见的一种做法，它在虚拟化平台分配实例到某台服务器后执行，即我们不考虑实例在多台服务器上的调度，其基本原理如下图9-10所示。

图9-10　启动排队简图

实例在启动时都会被加进固定长度为m+n的队列末端，其中m为实例运行时硬盘所在存储的最优实例启动并发数，我们称为启动队列。n为等待启动的实例数目，我们称为待启动队列。我们将实例自启动到其IOPS降至其空载运行水平视为完全启动，此段时间为t。当队列m中的实例完全启动后，就将其从队列中移除，队列n中的首端的实例加入至m队列尾端并开始启动。

使用启动排队时，有以下几点设计原则需要注意：

（1）t和m的值可以根据实例运行所处的环境自动进行动态调整（后台自动测量服务器、存储服务水平），管理员也可进行手动调整。

（2）实例会根据其优先级插队至n的首端，但不可插队至m队列中。

（3）当队列n满时，可以选择拒绝启动或等待资源以启动实例；某些设计中也可将m与n合并为一个启动队列。

此种方式能够比较高效地利用服务器和存储资源，而且对已启动实例的附带影响又较小。

9.3.2　存储分层选择

启动排队的方式是从策略上来解决问题，接下来我们从实例硬盘存储位置的选择上进一步降低启动风暴带来的影响。

模板硬盘、实例硬盘、增量硬盘及“无状态硬盘”所在存储位置的性能将直接影响实例的启动过程。

1.统一高速存储改善整体I/O分布

从9.2节的实验中我们可以看出，单台XP实例启动的前10秒是IOPS消耗最大的时期。在启动风暴来临之前，我们只要准备好IOPS充足且适当的存储设备，再适量调整我们的I/O调度算法即可在一定程度上缓冲第一波风暴，而后的登录风暴便不再是问题了。

假设现在有IOPS充足且适当的存储设备，我们将模板、实例、快照硬盘全部放置到这台设备上，从而就可以比较“不负责任”地缓解这个问题。

为什么说“不负责任”呢？

因为虽然一股脑地全部上SSD会改善I/O情况而减缓启动风暴带来的影响，但在成本上对比根据实例实际读写I/O分配合理规划存储的方案，SSD就显得有些浪费了。

当然，不论是后端存储还是前端服务器OS，使用响应时间短、读写带宽大的硬盘总是有益的。现在制造SSD的技术正在不断提高，成本、寿命、速度、容量等与机械硬盘相比，优势也越来越突出，所以虚拟化桌面领域中SSD会代替机械硬盘成为主流。

2.增量硬盘高速存储池改善实例I/O分布

从9.2节的实验中我们可以看出，实例启动时会有大量的读请求和相对较少的写请求，而我们的实例就是从模板母盘中读取数据，并将CED（创建、修改、删除）数据写入增量盘。那么，我们不妨将模板硬盘放置在一般的SSD（MLC、TLC）存储上用来满足读请求的要求，将增量硬盘放置在成本相对低廉的SATA盘RAID阵列存储上来满足平稳且分散的写请求。其I/O路径如图9-11所示，与之对应的图9-12，即为此种存储方式下启动20台虚拟机实例的读写请求数，可以看出它在第60秒时便基本稳定，说明全部实例已进入桌面。这样一来，我们就让它们各取所需，从而可以在成本上升较少的情况下改善启动风暴带来的影响。

图9-11　增量硬盘读写请求

图9-12　模板位于SSD，实例位于机械硬盘的读写请求

当然，随着桌面中安装的自启动软件和服务越来越多，而这些后来者都写入了我们的增量盘中，所以增量盘的读请求数在后期会有一定量的上升，从而拖慢整体的读请求水平。根据安装软件的“重度”，我们可以选择提前在模板中安装比较“重”的软件，尽量使增量硬盘中静态数据的（比如文档、媒体文件等）占比扩大，这样就可以使后期的读请求时间较为分散，从而保证系统的启动速度不会受到太多影响。

3.无状态实例硬盘高速存储池改善实例I/O分布

由于无状态实例在桌面云中拥有很重要的位置，所以本节在此介绍一种改善无状态桌面的存储方式以改善I/O分布从而减少启动风暴的影响，其结构如图9-13所示。

图9-13　无状态实例的读写请求

首先，模板会承受启动时的大量读请求，那么我们肯定是要把模板放置到SSD这样的高IOPS存储中了。

然后，我们再看一下“无状态”硬盘，即将实例硬盘作为backing file的硬盘。在桌面云中它的生命周期短，生成频率高，并且用户使用的软件和文档都已经被“模板化”了，后续对系统很少会产生大量写的情况。所以，它承受的读写请求量较低，但是它的每一次生成和删除对所在的物理存储又有频繁的重复写入操作。考虑到SSD的擦除次数有限，我们最好将“无状态”硬盘放置在机械硬盘的存储中。

最后，从模板创建的实例硬盘也会承受大量的读请求，但是接下来我们要考虑如下两种状况：当实例硬盘中有较多的拖慢启动时间的软件和服务时，为保证启动时IOPS不会成为瓶颈，我们最好把它也放置到SSD中；当实例硬盘中静态数据的占比较多时，我们可以优先考虑将其放置在机械盘的阵列存储中，并且可以与“无状态”硬盘放置在同一文件系统中。

9.3.3　其他提升桌面云存储性能的方式

1.FS-Cache

FS-Cache是一种将通过网络获取的数据缓存到本地常驻存储中以加速本地应用的访问，从而减少网络流量的技术，其使用过程如图9-14所示。

图9-14　FS-Cache使用示意图

FS-Cache在设计之初，就尽量保持其对管理员和用户透明的特性。不同于Solaris系统的cachefs，FS-Cache允许服务器端的文件系统直接与客户端的本地cache进行交互，而不需要额外挂载文件系统。比如在NFS上使用FS-Cache时，我们只需要在挂载选项中加一个参数就可以启用它了。

FS-Cache在不改变网络文件系统基本操作的前提下，在文件系统中提供了一个常驻cache用于数据的暂存。比如，一个NFS客户端即使之前配置了FS-Cache并且有一部分数据已被cache，关闭它后仍然可以挂载NFS并且使用被cache的数据部分。FS-Cache也可隐藏掉客户端文件系统驱动的所有I/O错误。在配置FS-Cache的时候，我们需要一个cache的后端，这个后端的文件系统需要支持bmap和扩展属性。

但FS-Cache不能cache任意网络文件系统，它需要的共享文件系统驱动要能够与FS-Cache交互、存储数据、建立和验证元数据。FS-Cache通过cache后端文件系统中数据的键索引和一致性检查来保证数据的持久特性，即数据的有效性校验。

FS-Cache在私有云中一般适用于教学、办公等模板相同的批量桌面，不适用于存储模板大多相异的虚拟服务器。

2.bcache

bcache是Linux内核块设备层的cache模块，它可以将一块或多块SSD用作为普通硬盘的cache，有点类似于“混合硬盘”。

bcache有点类似于ZFS中的L2Arc，但是除去当作write through的cache外，它也可当作write back的cache，并且文件系统对它来说是透明的。在使用上bcache可以很方便地启用，并且不需要进行额外的设置就可以满足我们大部分的需求。但是，它不会去cache顺序I/O，只会cache SSD擅长的随机I/O，这个特性让它的应用范围在一定程度上有所限制了。

还有一点比较有用的特性，即掉电后它的数据不会丢失，这样它就有点像一个带电池的raid控制器了。关于这点，社区和商业公司已经做了很多工作，所以我们放心用就好了。

由于它是块设备级别的cache，因此它对单个服务器的提升比较大，但是对于运行在外接存储设备上的虚拟机就没有什么效果了。所以除去商业存储设备的选择之外，bcache和FScache让我们多了一个自建存储的理由。

3.SSD PCI-E卡

PCI-E SSD产品通常采用特殊的驱动器通过PCI总线进行直接存储器访问（Direct Memory Access，DMA），而非只是将闪存或DRAM内存封装成SCSI连接的硬盘驱动器。这是一种比较革命性的改变，它使得随机读写性能相比其他存储设备有了质的提升。

存储设备连接服务器有以下几种常见的方式：

·PCI-E总线连接RAID控制器，再连接SAS或SATA硬盘。

·通过PCI-E总线连接HBA卡，再连接到硬盘阵列。

·SSD挂载到PCI-E SSD卡，再挂载到PCI-E插槽。

我们可以看到CPU通过PCI-E SSD卡提供的短路径来访问SSD，结合flash的高速读写性能，极大地提升了存储性能，突破了存储的I/O瓶颈。

但因为其连接SSD的数目有限，且单卡成本较为高昂，所以对于旨在降低用户成本、提高管理效率的私有云桌面来说，目前这是一种相对“奢侈”的解决方案。

拓展：其他存储I/O风暴介绍

私有云中有几种I/O风暴比较常见，比如登录风暴、杀毒风暴。登录风暴发生在系统启动后但还未进入桌面时，多人同时登录而对服务器造成压力的情况，这点我们从图9-9和图9-12中可以明显看出。对于登录风暴，我们可以使用自动登录的方式，将其与启动划分到同一顺序过程中，这样启动排队策略也能作用于它了。

而杀毒风暴则是指多个虚拟机实例的某些应用程序在对硬盘分区乃至全盘同时进行扫描时，对服务器存储产生的大量压力而导致系统响应缓慢的现象，这些应用程序包括杀毒软件、监控软件等。开源私有云平台中的杀毒风暴可以利用GuestFS工具作为解决方案，即将虚拟机的硬盘挂载到统一的杀毒服务器上，从而可以在实例离线状态下进行查杀、在离线状态下进行扫描。

9.4　小结

从模拟试验中我们可以看到服务器运行一定数量（20~50）的虚拟桌面下的存储I/O负载，了解到桌面在启动时的读写请求分布状况，而后我们再根据实验结果提出桌面功能的简单优化策略。

总之，目前要完全避免启动风暴、登录风暴还需要较大的成本投入，而在私有桌面云中，我们最好合理地规划存储以减少风暴带来的影响。

对于读者来说，找到合适的就好，毕竟没有完美的架构，都是在发展中逐渐进化而满足甚至超越需求的。

第10章　私有云桌面网络组建

作者简介

蒋迪，上海沃帆信息科技有限公司资深虚拟化基础架构工程师，从事云平台研发工作，主要包括OpenStack、oVirt的虚拟化、网络、存储组件及其在桌面云领域的应用。涉及虚拟化产品应用（云桌面、云服务器），云平台架构与相关开发（自主产品与开源软件），嵌入式系统与设备（ARM架构），分布式存储（GlusterFS）。多次参与银行与教育领域的私有云架构设计与实施。

2016年年初，Amazon刚刚推出Workspace，即公有云形式的桌面。在此之前的几年中，桌面云主要是以私有云的形式存在，主要原因除了安全性以外，还有就是桌面协议性能所引起的带宽与延迟问题。虽然带宽问题在私有云中并不是很明显，但是仍然需要合理规划以增强其整体安全性与利用率。本章将介绍私有云桌面中组网的基础技术及其实现方式，然后再列举oVirt和OpenStack中关于虚拟桌面网络的组建方式。最后我将尝试从多个方面解析典型私有云桌面中网络配置的选择，希望对私有云厂商同仁及企业网管有一定的帮助。

10.1　桌面云常用网络

我所接触的虚拟化桌面云平台中，其组网实现可以是VLAN、NAT、桥接、OpenvSwitch等。接下来本章将介绍如何在libvirt/QEMU组合中使用这些网络。

10.1.1　NAT网络

对于NAT（Network Address Translation，网络地址转译）网络，我们或许并不陌生。一般家庭路由器接入网络提供商（ISP）时，路由器会获得一个公网IP（或者广域网IP），局域网电脑接入路由器后获得内网IP，用户就是通过NAT来访问互联网的。

NAT的原理是在客户端发出的IP封包到达NAT网关后，其中的源IP地址会被替换为网关IP（这个过程称为SNAT，源IP和端口会被记录）再继续发送至外部服务器，当服务器将IP封包返回到NAT网关后，其中的目的地址将变更为客户端IP（这个过程称为DNAT，它会利用SNAT记录的IP与端口信息）再传递回客户端，外部地址一般不能直接访问NAT后端的地址（需要在NAT网关打开端口映射），其基本过程如图10-1所示。如果图10-1中的请求是从外网客户端发起的，内网服务器做出响应，经过DNAT和SNAT之后返回，那就是我们常说的端口映射了。

图10-1　NAT实现

NAT在libvirt/QEMU组合中的默认结构如图10-2所示，对应到路由器中，物理网口eth0就相当于路由器的WAN接口，虚拟网口veth0就相当于路由器的LAN接口，虚拟机对应着我们的客户端，NAT即在主机网络协议栈中进行包的地址转译。

为了在libvirt中使用NAT网络，我们首先需要在libvirt中定义一个名为default的NAT网络的配置：

[root@node1 ~]# cat> default-nat.xml<<EOF
<network>
<name>default-nat</name>
<forward mode=’

nat’

/>
<bridge name=’

virbr0’

stp=’

on’

delay=’

0’

/>
<ip address=’

192.168.122.1’

netmask=’

255.255.255.0’

>
<dhcp>
<range start=’

192.168.122.2’

end=’

192.168.122.254’

/>
</dhcp>
</ip>
</network>
[root@node1 ~]#virsh net-define default-nat.xml
[root@node1 ~]#virsh net-start default-nat
[root@node1 ~]#virsh net-autostart default-nat

图10-2　NAT网络示意图

在libvirt中定义并启动NAT网络之后，系统会创建虚拟网桥virbr0、添加NAT规则并在虚拟网桥上启动DHCP服务。添加的NAT规则（SNAT或MASQUERADE皆可，主要区别是在SNAT期间前者的内核将一直记录连接信息，网口关闭后连接虽然失效但信息仍然存在，当接口重启后连接会继续生效；而后者的连接信息将随着网口的关闭而丢失，除非虚拟机再次发起连接）会将来自virbr0的流量传递至物理网口eth0，DHCP服务则由轻量级的dnsmasq提供，主机需要做的配置如下所示：

# 查看网桥

[root@node1 ~]# brctl show virbr0
bridge name bridge id STP enabled interfaces
virbr0 8000.52540026be99 yes virbr0-nic
# 查看防火墙

NAT规则

[root@node1 ~]# iptables -L
Chain INPUT (policy ACCEPT)
targetprot opt source destination
ACCEPT udp -- anywhere anywhereudpdpt:domain
ACCEPT tcp -- anywhere anywheretcpdpt:domain
ACCEPT udp -- anywhere anywhereudpdpt:bootps
ACCEPT tcp -- anywhere anywheretcpdpt:bootps
Chain FORWARD (policy ACCEPT)
targetprot opt source destination
ACCEPT all -- anywhere 192.168.122.0/24 ctstate RELATED,ESTABLISHED
ACCEPT all -- 192.168.122.0/24 anywhere
ACCEPT all -- anywhere anywhere
REJECT all -- anywhere anywhere reject-with icmp-port-unreachable
REJECT all -- anywhere anywhere reject-with icmp-port-unreachable
Chain OUTPUT (policy ACCEPT)
targetprot opt source destination
ACCEPT udp -- anywhere anywhereudpdpt:bootpc
# 查看

dnsmasq配置，即

DHCP服务配置

[root@node1 ~]# cat /var/lib/libvirt/dnsmasq/default-nat.conf
##WARNING: THIS IS AN AUTO-GENERATED FILE. CHANGES TO IT ARE LIKELY TO BE
##OVERWRITTEN AND LOST. Changes to this configuration should be made using:
## virsh net-edit default
## or other application using the libvirt API.
##
## dnsmasqconf file created by libvirt
strict-order
pid-file=/var/run/libvirt/network/default-nat.pid
except-interface=lo
bind-dynamic
interface=virbr0
dhcp-range=192.168.122.2,192.168.122.254
dhcp-no-override
dhcp-lease-max=253
dhcp-hostsfile=/var/lib/libvirt/dnsmasq/default-nat.hostsfile
addn-hosts=/var/lib/libvirt/dnsmasq/default-nat.addnhosts

然后在虚拟机设备段中添加如下定义，即可通过DHCP获得地址并以NAT方式访问外部网络：

<interface type='network'>
<source network='default-nat'/>
<model type='rtl8139'/>
</interface>

在虚拟化中使用NAT的好处不言而喻，主要有以下几点：

·解决了局域网中IP不足的隐患。

·网络拓扑更具弹性。

·客户端网络行为更易控制，比如局域网或外网访问权限。

10.1.2　桥接网络

桥接是将两个以上的网络通过设备进行连接从而达到网络汇聚的目的。桥接位于OSI模型中的数据链路层。桥接技术共有4种：简单桥接、多端口桥接、透明桥接和源路由桥接，具体如表10-1所示：

表10-1　桥接技术

在虚拟化中，我们通常使用的是透明桥接，它一般是将主机的物理网口与虚拟机网口进行桥接，虚拟机网口的流量虽然是通过物理网口进入外部网络的但地位仍与物理网口相同。桥接是很多云平台都有提供的组网方式，主机与虚拟机之间的连接示意图如图10-3所示。

图10-3　桥接网络示意图

使用桥接网络，我们首先需要创建网桥br0，然后将物理网口eno16777736（同图10-3中的物理网口eth0）添加至br0中：

# 将

eth0作为

br0的桥接接口

[root@node1 ~]# cat>/etc/sysconfig/network-scripts/ifcfg-eno16777736 <<EOF
DEVICE=eno16777736
ONBOOT=yes
BRIDGE=br0
NM_CONTROLLED=no
EOF
# 定义网桥

br0
[root@node1 ~]# cat> /etc/sysconfig/network-scripts/ifcfg-br0 <<EOF
DEVICE=br0
TYPE=Bridge
BOOTPROTO=static
IPADDR=192.168.0.140
NETMASK=255.255.255.0
GATEWAY=192.168.0.1
DNS1=192.168.0.1
ONBOOT=yes
DELAY=0
NM_CONTROLLED=no
EOF
# 重启网络使设置生效

[root@node1 ~]# service network restart
# 查看网桥信息

[root@node1 ~]# brctl show
bridge name bridge id STP enabled interfaces
br0 8000.000c29c93e66 no eno16777736

在libvirt中定义一个桥接网络的配置：

# 在

libvirt中定义此网络，名称为

host-bridge
[root@node1 ~]# cat>bridge.xml<<EOF
<network>
<name>host-bridge</name>
<forward mode='bridge'/>
<bridge name='br0' />
</network>
EOF
[root@node1 ~]# virsh net-define bridge.xml
Network host-bridge defined from bridge.xml
#启动此网络，并将其标记为随

libvirt启动

[root@node1 ~]# virsh net-start host-bridge
Network host-bridge started
[root@node1 ~]# virsh net-autostart host-bridge
Network host-bridge marked as autostarted

然后在虚拟机的设备段定义中添加如下配置即可：

<interface type='network'>
<source network='host-bridge'/>
<model type='rtl8139'/>
</interface>

启动虚拟机后，可以查看br0桥接的桥接状况：

[root@node1 ~]# brctl show br0
bridge name bridge id STP enabled interfaces
br0 8000.000c29c93e66 no eno16777736
vnet0

其中br0上的vnet0接口是由libvirt自动创建的与虚拟机eth0相连的虚拟TAP设备接口，我们也可以通过ip命令进行手动创建，然后在QEMU命令中指定使用此接口，代码如下所示：

#创建

TAP设备接口，也可以使用命令“

tunctl-u root -t tap0”

[root@node1 ~]# iptuntap add tap0 mode tap
# 将

tap0加入

br0中

[root@node1 ~]# brctladdif br0 tap0
[root@node1 ~]# brctl show br0
bridge name bridge id STP enabled interfaces
br0 8000.000c29c93e66 no eno16777736
tap0
# 启动虚拟机测试

[root@node1 ~]#qemu-system-x86_64 -net nic -net tap,ifname=tap0,script=no-hdahda.qcow2

10.1.3　VLAN网络

QEMU虚拟机使用802.1q VLAN网络有两种模式，即Access模式与Trunk模式。使用Access模式需要在主机中将指定的tagged VLAN接口（比如eth0.123）加入对应的网桥，虚拟机的数据包将在流入/流出网桥时将被添加/剥离标签；Trunk模式下的物理接口（比如eth0）将直接加入网桥，虚拟机在其网口上主动添加/剥离VLAN标签后才能与外部通信。某些发行版内核可能不支持QEMU虚拟机的Trunk模式，一般需要借助ebtables工具将二层帧进入broute时对其进行路由处理（routing）而不是桥接处理（bridging）（命令形如“ebtables-t broute-A BROUTING-i enp3s0-p 802_1Q-j DROP”）。

接下来笔者用libvirt/QEMU进行示例，主机的物理网口enp3s0与交换机上的Hybrid接口相连，它属于untagged VLAN 1及tagged VLAN 10、11。

10.1.4　Access模式

Access模式在交换机中表示为只允许untagged流量进入的网口，在libvirt中它表示为与虚拟机网口相连的网桥是Access模式。此时我们需要在主机中创建三个网桥br1、br10、br11，同时物理网口enp3s0及其VLAN子网口enp3s0.10、enp3s0.11分别加入到这三个网桥中，虚线部分的右侧网桥是untagged流量，然后流向左侧主机时被添加标签成为tagged流量，反之亦然，如图10-4所示。

图10-4　Access模式虚拟机流量路径

创建三个网桥并将对应的VLAN子接口分别加入其中：

# 定义

enp3s0及其

VLAN子接口

enp3s0.10、

enp3s0.11
[root@node1 ~]# cat>/etc/sysconfig/network-scripts/ifcfg-enp3s0 <<EOF
DEVICE=enp3s0
ONBOOT=yes
BRIDGE=br1
NM_CONTROLLED=no
EOF
[root@node1 ~]# cat>/etc/sysconfig/network-scripts/ifcfg-enp3s0.10 <<EOF
DEVICE=enp3s0.10
ONBOOT=yes
BRIDGE=br110
VLAN=yes
NM_CONTROLLED=no
EOF
[root@node1 ~]# cat>/etc/sysconfig/network-scripts/ifcfg-enp3s0.11 <<EOF
DEVICE=enp3s0.11
ONBOOT=yes
BRIDGE=br11
VLAN=yes
NM_CONTROLLED=no
EOF
# 定义网桥

br1、

br10、

br11
[root@node1 ~]# cat> /etc/sysconfig/network-scripts/ifcfg-br1<<EOF
DEVICE=br1
TYPE=Bridge
BOOTPROTO=static
IPADDR=192.168.0.141
NETMASK=255.255.255.0
GATEWAY=192.168.0.1
DNS1=192.168.0.1
ONBOOT=yes
DELAY=0
NM_CONTROLLED=no
EOF
[root@node1 ~]# cat> /etc/sysconfig/network-scripts/ifcfg-br10<<EOF
DEVICE=br10
TYPE=Bridge
BOOTPROTO=static
IPADDR=172.20.10.141
NETMASK=255.255.255.0
GATEWAY=172.20.10.254
ONBOOT=yes
DELAY=0
NM_CONTROLLED=no
EOF
[root@node1 ~]# cat> /etc/sysconfig/network-scripts/ifcfg-br11<<EOF
DEVICE=br11
TYPE=Bridge
BOOTPROTO=static
IPADDR=172.20.11.141
NETMASK=255.255.255.0
GATEWAY=172.20.11.254
ONBOOT=yes
DELAY=0
NM_CONTROLLED=no
EOF

然后在libvirt中定义3个网络：

# 在

libvirt中定义此网络，名称为

host-bridge
[root@node1 ~]# cat> vlan1.xml<<EOF
<network>
<name> vlan1</name>
<forward mode='bridge'/>
<bridge name='br1' />
</network>
EOF
[root@node1 ~]# cat> vlan10.xml<<EOF
<network>
<name> vlan10</name>
<forward mode='bridge'/>
<bridge name='br10' />
</network>
EOF
[root@node1 ~]# cat> vlan11.xml<<EOF
<network>
<name> vlan11</name>
<forward mode='bridge'/>
<bridge name='br11' />
</network>
EOF
[root@node1 ~]# virsh net-define vlan1.xml
Network vlan1 defined from vlan1.xml
[root@node1 ~]# virsh net-define vlan10.xml
Network host-bridge defined from vlan10.xml
[root@node1 ~]# virsh net-define vlan11.xml
Network host-bridge defined from vlan11.xml
#启动网络，并将其标记为随

libvirt启动

[root@node1 ~]# virsh net-start vlan1
Network vlan1 started
[root@node1 ~]# virsh net-start vlan10
Network vlan10 started
[root@node1 ~]# virsh net-start vlan11
Network vlan11 started
[root@node1 ~]# virsh net-autostartvlan1
Network vlan1 marked as autostarted
[root@node1 ~]# virsh net-autostartvlan10
Network vlan10 marked as autostarted
[root@node1 ~]# virsh net-autostartvlan11
Network vlan11 marked as autostarted

最后在虚拟机设备段中添加如下定义，启动后在虚拟机系统内设置对应VLAN地址段的网络信息（不需要添加VLAN标签）
即可接入网络：

<interface type='network'>
<source network='vlan1'/>
<model type='rtl8139'/>
</interface>
<interface type='network'>
<source network='vlan10'/>
<model type='rtl8139'/>
</interface>
<interface type='network'>
<source network='vlan11'/>
<model type='rtl8139'/>
</interface>
</interface>

10.1.5　Trunk模式

Trunk模式在交换机中表示为只允许tagged流量进入的网口，在libvirt中它表示为与虚拟机网口相连的网桥是Trunk模式。此时我们只需要在主机中创建一个网桥br0，将物理网口enp3s0加入网桥，但需要同时设置br0的VLAN子接口。虚拟机的三个网口与网桥br0相连，虚线部分的左侧网桥中是tagged流量，然后流向右侧虚拟机时标签被剥离成为untagged流量，如图10-5所示。

图10-5　Access模式虚拟机流量路径

创建一个网桥br0，并将物理网口enp3s0加入其中：

[root@node1 ~]# cat>/etc/sysconfig/network-scripts/ifcfg-enp3s0 <<EOF
DEVICE=enp3s0
ONBOOT=yes
BRIDGE=br0
NM_CONTROLLED=no
EOF
[root@node1 ~]# cat> /etc/sysconfig/network-scripts/ifcfg-br0<<EOF
DEVICE=br0
TYPE=Bridge
BOOTPROTO=static
IPADDR=192.168.0.141
NETMASK=255.255.255.0
GATEWAY=192.168.0.1
DNS1=192.168.0.1
ONBOOT=yes
DELAY=0
NM_CONTROLLED=no
EOF

创建br0的两个VLAN子接口，由于VLAN子接口是定义到网桥上的，所以当流量到达网桥时VLAN标签会被保持，代码如下所示：

[root@node1 ~]# cat>/etc/sysconfig/network-scripts/ifcfg-br0.10 <<EOF
DEVICE=br0.10
ONBOOT=yes
VLAN=yes
NM_CONTROLLED=no
EOF
[root@node1 ~]# cat>/etc/sysconfig/network-scripts/ifcfg-br0.11 <<EOF
DEVICE=br0.11
ONBOOT=yes
BRIDGE=br0
VLAN=yes
NM_CONTROLLED=no
EOF

然后在libvirt中定义此网络并启动：

[root@node1 ~]# cat> vlan-all.xml<<EOF
<network>
<name>vlan-all</name>
<forward mode='bridge'/>
<bridge name='br0' />
</network>
EOF
[root@node1 ~]# virsh net-define vlan-all.xml
Network vlan-all defined from vlan-all.xml
#启动网络，并将其标记为随

libvirt启动

[root@node1 ~]# virsh net-start vlan-all
Network vlan-all started
[root@node1 ~]# virsh net-autostartvlan-all
Network vlan-all marked as autostarted

最后在虚拟机设备段中添加如下定义，启动后在虚拟机系统内设置网口eth0及要加入的VLAN子标签（比如eth0.10、eth0.11）的网络信息即可接入网络：

<interface type='network'>
<source network='vlan-all'/>
<model type='rtl8139'/>
</interface>

10.1.6　Open vSwitch

Open vSwitch（下面简称为OVS）是由Nicira Networks主导的，运行在虚拟化平台上的虚拟交换机。OVS可以为动态变化的端点提供两层交换功能，从而很好地控制虚拟网络中的访问策略、网络隔离、流量监控等。OVS遵循Apache 2.0许可证，能同时支持多种标准的管理接口和协议。OVS也提供了对OpenFlow协议的支持，用户可以使用与OpenFlow协议兼容的任意控制器对OVS进行远程管理控制，如果配合SDN交换机则可较大规模地替代传统网管方式。

在OVS中，有几个非常重要的概念。

·Bridge：Bridge代表一个虚拟交换机（vSwitch），一个主机中可以创建一个或多个Bridge设备。

·Port：端口与物理交换机的端口概念类似，每个Port都隶属于一个Bridge。

·Interface：连接到Port的网络接口设备。在通常情况下，Port和Interface是一对一的关系，只有在将Port配置为bond模式后，Port和Interface才是一对多的关系。

·Controller：OpenFlow控制器。OVS可以同时接受一个或多个OpenFlow控制器的管理。

·Datapath：在OVS中，Datapath负责执行数据交换，也就是把从接收端口收到的数据包在流表中进行匹配，并执行匹配到的动作。

·Flow Table：每个Datapath都和一个Flow Table关联，当Datapath接收到数据之后，OVS会在Flow Table中查找可以匹配的Flow，执行对应的操作，例如转发数据到其他指定端口。

一般在虚拟化平台中，使用OVS时多数会配合局域网的VLAN以便于管理。在流量较小的网络环境中，将GRE网络用于多虚拟机互联的场景也很常见。图10-6是使用GRE隧道将两个主机中的OVS网桥进行连接，从而使虚拟机能够获得外部路由器的地址进行上网，其中内部交换机用于主机管理。

在libvirt中使用OVS网络时，仅需要进行如下定义：

# cat ovs-br0.xml
<network>
<name>ovs-net</name>
<forward mode='bridge'/>
<bridge name='ovs-br0' />
<virtualport type='openvswitch'/>
</network>
# virsh define ovs-br0.xml
# virsh start ovs-br0

图10-6　OVS使用GRE管道网络示意图

10.2　oVirt/OpenStack的桌面网络应用

oVirt的虚拟机网络默认使用桥接，而OpenStack可以选配使用桥接、OVS等作为后端。本节将以oVirt为主，OpenStack网络为辅，为大家介绍一些典型的桌面云网络配置场景。

10.2.1　oVirt/OpenStack组网方式

oVirt/OpenStack中的组网方式都以10.1节所介绍的三种组网为基础，搭配VLAN（tagged、untagged）进行组合。

1.OpenStack典型组网方式

OpenStack的组网后端有Linux Bridge和OpenvSwitch两种，在绝大多数的实施中我们一般都会使用VLAN配合组网。

（1）Linux Bridge组网

使用Linux Bridge作为Neutron后端时，网络节点会使用诸如ip、dnsmasq、iptables、brctl等命令完成二三层功能（Linux Bridge网络节点总体情况如图10-7所示）。每一个接口与桥的信息都会以元数据的形式保留在数据库中。

图10-7　Linux Bridge网络节点概图

然后实例与划分出的各种接口接驳，这些接口经过Linux Bridge Agent的部署之后，再与网络节点相连。Linux Bridge计算节点概况如图10-8所示。

图10-8　Linux Bridge计算节点概图

图10-9所示的是典型的实例运行时网络拓扑：

（2）OVS组网

使用Linux Bridge作为Neutron后端时，网络节点（见图10-10）会使用OVS命令完成所有三层、DHCP、交换功能，同时这些信息也会保留在元数据中。

图10-9　Linux Bridge组网拓扑

图10-10　OVS网络节点概图

实例在通信时，数据包经过管道、VLAN标签处理之后，再经过网络节点Flow Table控制，最终数据得以传输。OVS计算节点概况如图10-11所示。

图10-11　OVS计算节点概图

图10-12所示的是使用固定IP的实例网络拓扑。

图10-12　固定IP实例OVS网络拓扑

2.oVirt默认组网方式

从oVirt的管理界面中我们可以很方便地拖动网络标签并将之与物理网口绑定，并且物理网口之间可以进行多种模式的bonding，然后每一个网络标签都可以配置IP信息、VLAN信息。当网络设置完成并附加到虚拟机之后，所有的虚拟机网口都会与标签的网络进行桥接（可以选择保留或剥离VLAN信息）。图10-13是oVirt的逻辑网络配置截图，其中左侧有4个物理网口，前两个进行了bonding，右侧是与之对应的逻辑网络，在主机中是对应网桥的名称。

图10-13　oVirt管理界面设置网络

oVirt也可以使用OpenStack Neutron的网络，从而弥补了oVirt网络虚拟化的不足。图10-14和图10-15是在oVirt中添加OpenStack网络的截图，在配置时需要注意输入OpenStack服务的服务用户名与密码，以及主机逻辑网络连接。

图10-14　oVirt管理界面添加Neutron网络——配置Tenant

除OpenStack Neutron外，还oVirt可以使用的外部服务有OpenStack Glance、Foreman、Docker等。从其默认的ovirt-image-repository外部的Glance源中我们可以将Neutron实例导入到本地，然后很方便地搭建OpenStack Neutron网络而不需要完整的OpenStack环境。图10-16是oVirt导入Glance源中的Neutron实例的过程截图。

图10-15　oVirt管理界面添加Neutron网络——配置MQ

图10-16　oVirt中导入Neutron实例

10.2.2　应用场景举例

1.教学机房

教学机房内不管是软件还是硬件，向来都是各厂商的“兵家必争之所”。首先，它具有一定的规模，能够创造很好的利润；其次，它所面向的群体主要是学生，他们在参加工作后很有可能会不自觉地帮助推广这些产品。

接下来我们以教学机房桌面为例分析一下使用哪种网络最具优势。

一般教学机房桌面，有安装软件繁多、使用时间固定、并发量大等特点，比较考验虚拟化产品的综合素质。桌面安装软件除日常办公软件之外，还包括各种文字、图形密集类教学软件，同时还可能会安装影音广播教学类软件。如无特殊要求，很少会安装杀毒软件。

接下来我们从需求出发，对所有的组网方式进行筛选：

·学生、教师的桌面，要求可以连接网络，可以控制外网访问。以上介绍的几种组网方式都可以做到，那么我们可根据最小环境IP资源占用原则，尽量减少桥接网络的使用。

·教学机房网络管理人员对虚拟化平台进行管理时，他们在虚拟桌面中更倾向于分配简单的NAT网络网段，从而减少拓扑的整体复杂性。

·对于纯软件方式的多媒体广播教学环境，当教师进行广播时，教师端编码往往在具有硬件显卡的物理机上才高效，此时教师端机器需要与学生端机器在同一网段，那么NAT或OVS组网要求就会对现有环境提出更高的要求（比如双网卡、服务器交换机额外VLAN）。这种环境下，一般使用桥接网络更合适。

以上是我对教学机房虚拟桌面内组网方式的建议，读者应根据实际环境做出判断，在工作量与性能、功能间做出取舍。接下来我们以上海某中学的教学桌面云为例，介绍他们场景中的虚拟化网络部署。

其中学生机全部位于区教育网网段，服务器亦然，无单独内网；教师主要依赖桌面广播软件进行教学，且有比较多的视频广播课程。这种场景中，我们为了不改变用户原有的教学习惯，直接使用了虚拟机桥接的方式。

具体是将两台服务器的第一个网口连接到服务器网段交换机，第二个网口连接到教室所连接的上层交换机中。然后在第一台服务器中启动DHCP服务，用于两台服务器中虚拟机的IP分配。这样一来，教师机与学生机仍然在同一网段中，广播软件仍然能继续，且显示流量与虚拟机内部流量也进行了隔离。

2.办公桌面

面向办公的桌面，一般需求为Microsoft Office、邮件处理等文字密集型软件；通信类软件一般为国内厂商开发的非广域网通信软件及QQ；防病毒软件种类比较多，目前卡巴斯基、诺顿等占多数，360使用得较少；影音类软件的使用仅局限于网页Flash，或者国内厂商定制的流媒体客户端。对于财务桌面，需求除普通办公桌面外还有一些财务类软件，而这些软件对桌面的负荷相较于普通办公桌面会高出一定量的资源消耗，同时也会有U-Key、指纹仪等终端设备，所以对于这类桌面，我们一般进行特殊设置，比如将其固定到某台服务器上运行，并赋予一定优先级，保证资源优先分配。在普通桌面与财务桌面之外，还有浮动桌面可供出差人员或来访人员临时使用，此种桌面与一般的办公桌面无异，但可能要求有严格的用户检查控制及无状态模式要求，以防止恶意使用而导致损失。

以上就是办公桌面的典型场景，我们的组网方式也基本明确了。一般桌面使用OVS或NAT，优先考虑OVS；浮动桌面则使用NAT；对于财务这类有特殊需求的桌面我们也可考虑使用桥接网络。另外当办公室中有类似打印机、扫描仪、指纹仪等共享设备时，它们的组网优先选择桥接方式，这样便保证了所有人员都能对其进行访问。接下来以上海某高校的教师办公桌面云为例，介绍该场景中的网络部署。

其中，服务器（共6台）与教室机都位于教育网的不同网段中，服务器的4个网口进行bonding后连接到校园服务器网段的交换机；教师办公桌面需要文字办公软件、图形编辑软件、公式编辑软件等，并且支持USB设备重定向（手机、U盘等）。这种场景中，由于作为客户端的原有教师机所在的网段无多余IP，所以考虑使用OVS（GRE通道）方式进行组网。

我们选择了固定一台服务器作为OVS网段的外网出口，并且在其上启动了DHCP服务用于给虚拟机提供IP，其他服务器中的OVS网桥通过GRE通道与出口服务器的OVS网桥相连。此时，出口服务器上具有3个IP，分别属于服务器网段、显示网段、出口网段（用户网段）。最后，作为客户端的教师机便能够保证在显示带宽充足的情况下连接到云桌面，且其上网行为不会影响到服务器网段的通信质量。

10.3　小结

在平时的实施中，我们可以将组网按照IP资源稀缺性依次排列出NAT>OVS>桥接；同时按照访问权限范围排序桥接>OVS=NAT。对于使用频率较高的私有云而言，我们需要根据客户的运维水平、实施改造难度，结合现场环境和特殊需求进行综合考虑，这样才能得到比较满意的组网规划。

第11章　浅谈服务器交付的那些事儿

作者简介

赵旻，RHCA/RHCSS/MCITP，熟悉x86平台基础架构系统的建设、管理及运维工作，现就职于京东金融——网银在线，担任支付产品技术部高级系统工程师一职；9年以上互联网金融、电信、政府等多领域背景的从业资历，千万级大型项目经验，优秀的文档撰写能力及沟通技巧；曾参与中国国家电子政务多项重点工程的安全信任体系建设工作，曾为中国移动、国航等大型企业提供运维技术支持；喜欢从事新技术研究、优化方案等工作。工作前倾向于制定有计划且详细的执行方案，擅长处理远期规划设计类事件。善于在工作实践中分析问题、总结经验，属于改进优化能力类型的工作者。致力于对IT技术的精研和业务分析，帮助企业定制更加实际高效的解决方案。

人们一提到IT运维，必然离不开IT建设。随着市场化的不断壮大，企业对设备的需求量也呈现出井喷式的增长。这对于运维来说，海量的系统交付工作成为了一种严苛的考验。作为运维大军中的一个无名小卒，我也经历过那样一段艰苦的岁月历程。

刚加入京东金融的时候，当时的系统组只有两个人，几百台服务器。我和另外一个负责网络的同事从无到有完成了两个IDC机房的建设工作。现如今，京东金融已经实现了同城多活多机房的建设，拥有了近万台服务器的规模。回想于此，多少有些心得。因此我在这里撰以小文，权当抛砖引玉，以供大家参考。如有不到之处，还望高人多多斧正。

11.1　设备签收的学问

签收地址的详细填写非常重要。我们采购的服务器非常多，收货就是个大工程，最少也得几百台。有时候物流公司忙不过来，就会外包给第三方派送。而这些第三方有时会显得太过“精明”，他们很了解IDC机房会有专人负责拆箱上架的工作。如果收货地址写得不详细，他们把设备拉到楼下就算完成任务了。有些人甚至连卸货都不负责，还一个劲儿地催促你赶快签收。遇上这种情况将会很麻烦，因为IDC不是为你一家公司服务的，配备人员太少的话就会延误收货速度。另外，服务器不拆箱也没法正常验收（因物流原因造成损坏的设备应当拒收）。如果你把签收地址写到了机房里面，你就可以要求对方负责卸货并拆箱验收（这本来就是他们的分内工作），IDC负责上架。这样我们就把人员全部都调动了起来，有效地提升了工作效率。签收地址在机房里面，只要设备没上架，你就有理由拒签。

对于设备的验收，一般我的做法是这样的：

（1）将设备卸到空场上（这需要IDC有一定的场地条件），要求物流按照箱体朝上、标签冲外的规则码放。如果外包装箱上有服务器型号，还可以分类码放。

（2）用记号笔在箱体上标注记号，清点设备数量。

（3）拆箱验货，验收完毕的设备准许上架。

在这个过程中有一个困难：我们有一些型号相同但配置不同的服务器。虽然标签上有配置清单，但要求物流按照配置清单进行分类就显得不太合理了，而且这些设备数量占比不大，也没法单独发送。因此我就养成了一个习惯：每次收货时都带着记号笔，在卸货的过程中，我按照配置自己做分类标记。卸货完成时，分类标记也就做完了。最后的数量清点工作就非常轻松了。

我曾经建议过厂商设置一个彩标方案，自定义一套彩色背景的标签。厂商和大客户签单时，在客户制订的类型与自定义色彩列表之间建立一个关系映射实例。例如A型设备对应红色标签，B型设备对应黄色标签。在进行设备验收时，客户可根据标签颜色进行分类，从而有效提升签收效率，增强用户的体验效果，但是这个建议始终未被采纳。也许作为局外人，他们不曾有过这样的体会。一个人面对几百台型号各异的设备，核对配置是一件非常辛苦甚至是痛苦的事情。因为亲身经历过这种事，所以我也希望各大厂商今后能多一些同理心，多注重一些细节上的改善，做好客户服务才是与竞争对手拉开距离的制胜法宝。

11.2　服务器设置

服务器的有些配置可能无法预定义，所以你一定会遇到二次配置的场景。这里大体上可以分为BIOS配置、电源策略配置和带外管理配置三种类型。

对于带外管理地址的配置，通常有两种做法：

（1）事先规划各机架服务器的带外地址，设备上架后按照规划进行配置。

（2）提前向厂商索取带外管理卡的MAC地址表，采用DHCP地址绑定进行分配。

第一种方式的资产信息在部署前完成，设备排列规范整齐，便于机房人员的日常运维和管理。适用于尚不具备完善有效的CMDB系统的场景。第二种方式的部署效率更高，还需要配合IPMI与LLDP完成数据采集的工作，将资产信息填写入库。但这也带来了一个比较大的挑战。设备是随机码放的，使用DHCP分配后的带外地址在机架上也是散列分布的。因此设备维护对CMDB系统有很强的依赖性，这就要求CMDB系统所采集的信息必须准确无误。其实现难度还是比较高的，里面存在着很多不确定因素的干扰（例如线路接错），会对数据采集产生较大的影响。因此对于程序的逻辑判断方面，有着非常复杂而严格的要求。因为当时整个运维部的成员不超过20个人，没有精力去做CMDB系统，所以早期我们采用的是第一种方式来实现。

但是，服务器设置工作也是一项繁重的劳动。纯手工操作的效率只有100台/人天，因此使用命令行接口来完成大批量修改的工作势在必行。本章将以DELL的Rack系列服务器为例向大家介绍一下如何进行带外管理的初始化。

准备一台装有Linux系统的笔记本，一根30米长的网线，设备正常加电，带外管理网络调通。参考命令如下，请注意执行的顺序。

1.设置带外地址可用

具体的设置方法如下：

ipmitool -I lanplus -H 192.168.0.120 -U <USER> -P <PASSWORD> lan set <CHANNEL> netmask <NETMASK>
ipmitool -I lanplus -H 192.168.0.120 -U <USER> -P <PASSWORD> lan set <CHANNEL> defgw ipaddr <GATEWAY>
ipmitool -I lanplus -H 192.168.0.120 -U <USER> -P <PASSWORD> lan set <CHANNEL> ipaddr <IPADDR>注：

192.168.0.120是出厂的默认地址，只能接一台改一台，因此长网线可以让你在机房的正中间固定位置操作，而不必抱着笔记本跑来跑去。当然前提是有人能在另一端帮你插网线。

^_^

2.修改带外管理的用户名和密码

具体的修改方法如下：

ipmitool -I lanplus -H <IPADDR> -U <USER> -P <PASSWORD> user set name <USER_ID> <USER_NAME>
ipmitool -I lanplus -H <IPADDR> -U <USER> -P <PASSWORD> user set password <USER_ID> <NEW_PASSWORD>

3.开启VNC功能

具体的开启方法如下：

racadm set idrac.vncserver.enable enabled
racadm set idrac.vncserver.password <PASSWORD>
racadm set idrac.vncserver.port <PORT>
racadm set idrac.vncserver.timeout <SECOND>注：

DELL的

VNC功能非常棒，登录速度超快。另外

1.57以后版本的

firmware对花屏问题做了很大修缮，稳定性大大增加。建议把

timeout的值调大一些。现在我已经把慢吞吞的虚拟控制台丢到一边去了。

4.其他注意事项

（1）关于执行ipmitool命令时提示“Unable to establish LAN session”的问题。一般有两种情况。第一种，带外管理卡的问题。如果反复插拔网线不起效，可以关闭电源并拔掉电源线，然后按住电源按钮至少30秒进行放电操作。第二种，BIOS设置问题，如果选项“IPMI Over LAN”没有启用也会触发这个错误。执行racadm set iDRAC.IPMILan.Enable Enabled即可。

（2）Boot Setting的启动模式。Boot Setting的默认值是BIOS，但是有些做了RAID后磁盘空间大于2TB的机型，这个值有可能会被设置成UEFi，从而导致PXE引导失败。大家可以看到图11-1所示的提示还是比较迷惑人的。

图11-1　PXE引导失败

解决方案是执行racadm set BIOS.BiosBootSettings.BootMode Bios进行调整。注意，命令执行完成后并不会立即生效，此时的键值只是一个待定值（Pending Value）。需要创建一个Job，然后重启服务器电源才能完成修改。创建Job的命令是racadm jobqueue create BIOS.Setup.1-1。另外还可以使用命令racadm jobqueue view来查看Job的内容。

关于设置Boot Setting启动模式的完整过程。

①设置Boot Setting的启动模式为BIOS：

racadm set BIOS.BiosBootSettings.BootMode Bios

②检查设置结果：

racadm get BIOS.BiosBootSettings.BootMode
/admin1-> racadm get BIOS.BiosBootSettings.BootMode
[Key=BIOS.Setup.1-1#BiosBootSettings]
BootMode=Uefi (Pending Value=Bios)

注意：此时只是Pending Value，需要创建Job并重启主机电源。

③创建Job检查无误后重启主机：

racadm jobqueue create BIOS.Setup.1-1
racadm jobqueue view
ipmitool -H <IPADDR> -U <USER> -P <IPADDR> power reset

④重启完成后Job为完成状态，再次检查设置结果：

racadm jobqueue view
racadm get BIOS.BiosBootSettings.BootMode

（3）关于电源策略的调整。电源策略有均衡和冗余两种模式。机柜电源AB两路本身是独立的，只要总体耗电不超过阈值，怎么使用那是用户的自由。不过调整成均衡模式对IDC会比较有利。就像你有两双鞋，两双来回倒换着穿，总比只啃着一双穿强。但要是你遇到那种强制要求用户改成均衡模式的IDC，不妨多留个心眼儿，要小心他们的进线线径是否不够标准，或者是存在线路老化的问题。调整电源策略的命令如下：

racadm set System.Power.RedundancyPolicy 'Not Redundant'
racadm set System.Power.RedundancyPolicy 'Input Power Redundant'

11.3　Cobbler的流程与规划

下面我们着重探讨一下Cobbler的安装流程和规划问题。Cobbler各组件的关联结构如图11-2所示。

图11-2　Cobbler的组件架构图

鉴于DNS的特殊性，我们将其从Cobbler的管理中剥离出来，其他组件都交给Cobbler管理。前期已经对地址进行了分配，这里只需要在Cobbler中生成system，最后通过PXE引导启动完成系统部署。生成system这一步比较麻烦，因此我写了一个小程序来完成这项工作。因为代码中有部分代码和逻辑涉及公司的敏感信息，不方便直接展示给各位读者。因此这里只把这个程序是如何实现的具体思路和几个关键命令列举出来。

实现思路如下：

（1）对于支持WS-Management标准的服务器，使用wsman命令直接采集网卡的MAC地址。

（2）生成带外地址、业务地址和MAC地址之间的绑定关系，部署时IP就和设备对应起来了。

（3）指定TCP/IP设置、主机名、Bonding模式等，并据此生成创建Cobbler system的命令。

（4）Cobbler system创建完成之后，使用ipmitool命令将将下一次启动设置为由PXE引导。

（5）使用ipmitool命令启动服务器开始安装部署。

关键命令如下：

# Collect mac address by ws-management.
wsman enumerate \
http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/root/dcim/DCIM_NICView \
-h <HOST> -V -v -c dummy.cert -P 443 -u <USER> -p <PASSWORD> \
-j utf-8 -y basic |grep PermanentMACAddress
# Add a system and bond master into cobbler.
cobbler system add --name=<NAME> --hostname=<HOSTNAME> \
--interface=<BOND_NIC> --interface-type=bond \
--bonding-opts="miimon=100 mode=4 xmit_hash_policy=layer2+3" \
--ip-address=<BOND_IP> --subnet=<NETMASK> --gateway=<GATEWAY> --static=1 \
--profile=<PROFILE>
# Append slave A to bond master.
cobbler system edit --name=<NAME> --mac==<MAC> \
--interface=<SLAVE1_NIC> --interface-type=bond_slave \
--interface-master=<BOND_NIC> \
--profile=<PROFILE>
# Append slave B to bond master.
cobbler system edit --name=<NAME> --mac==<MAC> \
--interface=<SLAVE2_NIC> --interface-type=bond_slave \
--interface-master=<BOND_NIC> \
--profile=<PROFILE>
# Set host next boot from pxe.
ipmitool -I lanplus -H <IPADDR> -U <USER> -P <PASSWORD> chassis bootdev pxe
# Reboot host.
ipmitool -I lanplus -H <IPADDR> -U <USER> -P <PASSWORD> power reset

拓展1：关于Bonding模式的选用

如果你研读一下《RHEL Deployment Guide》就可以得知，Bonding模式一共有7种。从带宽利用率的角度来看，肯定不会考虑模式1和模式3。模式0存在数据包收发乱序的问题，模式2仅仅基于二层作为定义域的hash算法对带宽的利用不够充分，而模式5属于单向均衡。这样一来就只剩下802.3ad和alb两种模式了。alb无须交换机配置，采用ARP协商进行均衡，每发送一次ARP请求广播或添加、删除一个slave节点的时候，都需要重新协商分配流量，而在协商之前所有流量都会涌向一个当前的slave端口。802.3ad是一个业界标准，通过创建一个聚合组，确保组内所有链路的速率和工作模式一致，这种方式对带宽的利用更为充分。关于xmit_hash_policy的选择我们使用了layer2+3的方式。layer3+4的兼容性不好，而layer2+3在默认layer2的基础上增加了对三层因素的考量，分配会更加均衡。详细请参见https://www.kernel.org/doc/Documentation/networking/bonding.txt
。

拓展2：关于WS-Management标准

WS-Management（全称为Web Services-Management）是DMTF组织基于SOAP（Simple Object Access Protocal，简单对象访问协议）制定的一种开源标准，该标准致力于在不同的x86设备厂商当中，提供一种IT基础架构信息访问与修改的统一接口。凡是支持该标准的x86设备，对于有效管理资产配置工作均提供了极大的帮助。例如，我们有很多不同厂商的服务器，如果它们都支持WS-Management标准，那么我们就可以通过wsmancli工具，去统一采集所有服务器的硬件配置信息，也可以修改所有服务器的带外管理配置，而不是使用各厂商的私有化工具去分别管理。像AMD、DELL、Intel、Microsoft这些知名厂商都是该项标准组的成员。当然最新的类似标准还有redfish，支持的成员更多，但redfish目前还没有推广开来。

有兴趣的读者请参见http://www.dmtf.org/standards/wsman
和http://www.dmtf.org/standards/redfish
。

在谈部署架构的规划之前，我们先看一下Cobbler体系架构中各组件的作用。Distro指的是操作系统的分发版本。如果你的生产环境需要CentOS 5、CentOS 6和RHEL 7三个版本，那么你就需要创建三个Distro。Distro下面包含了Profile，你可以认为Profile就是你的一个部署模板，不同的Profile之间的差异应该包括RAID配置、分区和软件包。如果你要用CentOS 6部署MySQL数据库和Web Server，很显然这两者的安装要求应该是不一样的，你应当为此创建两个不同的Profile。每一个Profile都对应了kickstart、yum repo和system，这里重点讲一下system的概念，system里面包含了一台实际需要部署的服务器的相关系统信息，这些信息包括主机名、TCP/IP设置、网卡Bonding模式、VLAN配置、使用哪个操作系统（由Profile对应的Distro来决定）、使用哪些yum源（由Profile对应的yum repo来决定）及部署成什么样子（由Profile对应的Kickstart来决定）等。按照面向对象的讲法，如果说Profile是类的话，那么一个system就是一个部署实例。如果你用名为MySQL的Profile安装了200台主机，那么就要创建200个system。

对于部署架构的规划，我会根据现有业务场景的需要，针对不同场景定制不同的Distro和Profile，每个Profile对应自己的Kickstart。具体实现还是以MySQL和Web Server的部署来举例：首先做一个最小化系统安装的模板——名为Base的Profile和名为base.ks的Kickstart，在base.ks的基础上添加MySQL需要安装的软件包并修改分区设置，并为此创建MySQL和mysql.ks，照方抓药再完成Web和web.ks即可。我希望Kickstart文件只描述两个内容——分区设置与安装所需的软件包，其他的调整均通过脚本实现。

对于大批量部署还有一种见解不同的做法，那就是采用镜像部署方案。这种方案倾向于制作一个涵盖所有场景需求的镜像文件，部署时把这个镜像文件推送并解压到系统上，最后用脚本修改差异化配置。

两种方案各有优势。第一种方案如果发生了配置变更，修改内容越靠近分支，其修改成本就越低，其影响范围是可控的。而且不受多场景差异化的干扰，不必刻意考虑融合所带来的种种兼容性问题。第二种方案并发能力更强，交付速度更快。但是我个人不太欣赏这种做法。尤其是在对现有模板进行软件包删除、Patch更新或新驱动编译的时候，繁琐的封包操作真的是非常麻烦。而且我也不认为前者的交付速度会影响到业务。实测过200以上的并发是没有任何问题的，基本上一轮部署也就是几分钟的事情。照此计算，如果不算机房的前置工作，一天至少也可以交付5000台左右。与其斤斤计较两种部署方式所相差的那一两分钟，还不如去做好业务申请及交付的流程优化来得更加实在。

关于%post脚本的设计，我仅仅使用curl调用的方式来实现。例如有一个场景A需要交付，我利用curl调用脚本Profile_A，而Profile_A里面也只是利用curl调用二级脚本Detail01、Detail02、Detail03等。对于场景B的交付如法炮制。也就是说，每一个Profile_X.ks都对应于一个Profile_X，Profile_X又对应于若干个DetailXX，而DetailXX才是真正完成配置修改的部分。在这里我把所有的Profile_X都放置到一个名为/opt/scripts/post/的子目录中，把所有的DetailXX放置到一个名为/opt/scripts/post.d/的子目录中。相信大家也看懂了，这种方式其实就是效仿系统/etc/rcX.d/和/etc/init.d/的关系建立的。当进行系统调试的时候，可以直接修改脚本，运行一下就能看到效果，而不是重新kick一遍主机。

11.4　服务器安装时遇到的各种坑

每次做批量部署，在安装第一台主机时总是会不顺利，遇到的问题也是千奇百怪。下面我就把这些问题的解决方法分享给大家。

11.4.1　DHCP客户端获取IP地址失败

这类问题基本上都出现在跨网段的场景，如图11-3所示。

图11-3　DHCP客户端获取IP地址失败的场景

我总结了以下几种情况：

·某品牌交换机需要禁用STP，否则收不到DHCP请求（据说后来升级firmware就解决了）。

·对VRRP支持不好，网关不接收广播报文，尝试禁用或使用HSRP。

·网卡模块损坏。

·此外还有一种情况是插错网线所导致的，PXE在启动时会提示“media failure”。

11.4.2　TFTP加载失败

TFTP可用于加载pxelinux.cfg、vmlinuz和initrd.img的文件传输服务。这道关卡的故障处理难度也比DHCP的环节增加了不少。

注意

因为以下这些截图全部来自于我们的生产环境，所以我针对部分敏感信息做了遮蔽处理，请各位读者见谅。

没有开启ARP代理，访问TFTP服务时会提示“ARP timeout”，如图11-4所示。

图11-4　TFTP加载失败案例一

11.4.3　TFTP Client交互后无响应

这种倒霉事我遇到过两次，而且还是出自两个不同的原因。第一次是在安装物理机时，TFTP运行了一下就退出了，如图11-5所示。

图11-5　TFTP加载失败案例二

messages日志提供不了太多的帮助，我通过使用tcpdump抓包来进行故障分析，结果在里面看到了这样一行——缺少filename“undionly.kpxe”。这个undionly.kpxe看着非常眼熟，不正是配置文件/etc/cobbler/dhcp.template中的if$iface.enable_gpxe部分么？原来是某同事在测试VM安装时启用了gpxe，后来又忘记关掉了。注意，修改了/etc/cobbler/setting中的enable_gpxe：0后还不算完，因为system已经生成了，所以需要更新配置system或重建system。

第二次是一上来就直接报“TFTP open timeout”，如图11-6所示。

图11-6　TFTP加载失败案例三

当时使用的是Cobbler 2.6.3，在进行get-loaders的时候，被系统告知部分loader文件无法下载。因为我手上有一份备份，对此也没在意，就只复制了失败的部分，结果后面就出现了这个悲剧。

tcpdump抓包也只看到Extra Error，再往后就语焉不详了。于是，我冷静下来反思系统安装的流程。当想到TFTP本来就是要提供loader文件时，自己一下子就释然了。使用md5sum命令很快就印证了loader文件传输损坏的想法。我把所有loader文件重新复制了一份，故障自然解除。

11.4.4　yum安装失败

当安装过程中报告文件缺失或损坏的时候（如图11-7所示），你可别天真地认为是镜像有问题。检查一下日志输出，有可能是网络断了。切换到shell后用route和ip addr去判断。还有一种情况是我同事碰到的。他把update的软件包也放到yum源里面了，但要更新的包不属于原生的yum源，结果导致yum在计算依赖关系时出现了逻辑错误，产生了一个先有鸡还是先有蛋的悖论问题。解决的办法就是把更新部分放到%post里面去，或者到网站去更新最新版本的repo文件。

图11-7　yum安装失败

11.4.5　Linux内核无法识别新硬件

如果Linux内核无法识别新硬件（例如RAID卡）则有可能会导致安装失败。采用镜像部署方案的同学们会非常痛苦。因为你需要重新编译内核，还要不断地进行封包测试。对于采用Cobbler的同学们就有福了。首先找到硬件驱动的rpm包，将其制作成ISO放到部署服务器的Web服务上面，然后在kickstart文件里找到关于分区描述的地方，在其下添加这样一条语句：

driverdisk --source=http://x.x.x.x/newdriver.iso

如此一来，就可以在开机时自动加载驱动光盘了。

11.4.6　恶意PXE启动导致原有系统被误装

如果在维修过程中遇到主板损坏的服务器一定要非常小心，因为新更换主板的BIOS设置有可能是PXE启动，如果不注意贸然开机将导致PXE引导触发系统重装的悲剧。将Cobbler主配置文件里面的pxe_just_once选项设置为1并重启cobbled服务即可确保以后生成的system只响应一次PXE引导请求。对于再此之前已经生成或安装完毕的system强烈建议使用如下命令来禁止Cobbler响应PXE引导请求。

cobbler system edit --name=<NAME> --netboot-enabled=False

11.5　交接后的故事

后来团队的规模慢慢壮大起来了，我就把这份“前线差事”交付给了其他同事。原以为“艰苦岁月”总算告以段落，谁想清静了没两个月，又冒出了一个新问题。

一位监控同事找到我，说昨天新交付的一批主机要部署Check_MK，但是用Salt-Master批量推送时特别慢。我当时觉得问题很简单，无非就是如下几种可能：

·群组里面存在状态为down的minion节点。

·Salt-Master大批量并发操作的时候有瓶颈。

·资源占用问题。

第一种情况确实存在，但是排除后仍然没有解决问题。新主机的资源占用很低，而且只有salt命令慢。我使用time命令对salt的test.ping进行了测试，发现usr和sys所消耗的时间加起来也不过0.2秒，但wait的时间竟然多达20多秒。我想看一下版本号，哪知salt--version也同样慢得要死。看来人永远不能靠经验混日子。我使用strace命令去跟踪salt-version运行的整个过程，程序在进行了大量的debug信息输出后，终于在某个时刻停了下来。这个关键时段就是我们刚才所谓的wait时间，此时屏幕上没有任何输出。趁此机会，我赶紧截取了当前屏幕上的内容，然后等待着程序继续运行直至完毕。当strace执行完成后，我把这个命令又重复执行了一次。但与上一次不同的是，这次我把输出的结果保存到了strace.log文件中以便于分析。根据第一次程序暂停时所截取的屏幕内容，我在strace.log中找到了它的位置。这段代码是标准的socket网络通信连接的过程，重点关注connect（）函数提交的sin_port和sin_addr这两个参数，很显然这是一个DNS查询：

socket(PF_INET, SOCK_DGRAM|SOCK_NONBLOCK, IPPROTO_IP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("x.x.x.x")}, 16) = 0 // 问题就出在这里

poll([{fd=3, events=POLLOUT}], 1, 0) = 1 ([{fd=3, revents=POLLOUT}])
sendto(3, "~\347\1\0\0\1\0\0\0\0\0\0\fexample\1h\tchin"..., 68, MSG_NOSIGNAL, NULL, 0) = 68
poll([{fd=3, events=POLLIN|POLLOUT}], 1, 3000) = 1 ([{fd=3, revents=POLLOUT}])
sendto(3, "\311\333\1\0\0\1\0\0\0\0\0\0\fexample\1h\tchin"..., 68, MSG_NOSIGNAL, NULL, 0) = 68
poll([{fd=3, events=POLLIN}], 1, 2999) = 0 (Timeout)
socket(PF_INET, SOCK_DGRAM|SOCK_NONBLOCK, IPPROTO_IP) = 4
connect(4, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("y.y.y.y")}, 16) = 0 // 问题就出在这里

poll([{fd=4, events=POLLOUT}], 1, 0) = 1 ([{fd=4, revents=POLLOUT}])
sendto(4, "~\347\1\0\0\1\0\0\0\0\0\0\fexample\1h\tchin"..., 68, MSG_NOSIGNAL, NULL, 0) = 68
poll([{fd=4, events=POLLIN|POLLOUT}], 1, 6000) = 1 ([{fd=4, revents=POLLOUT}])
sendto(4, "\311\333\1\0\0\1\0\0\0\0\0\0\fexample\1h\tchin"..., 68, MSG_NOSIGNAL, NULL, 0) = 68
poll([{fd=4, events=POLLIN}], 1, 5999) = 1 ([{fd=4, revents=POLLERR}])
close(3) = 0
close(4) = 0
brk(0x11f3000) = 0x11f3000

x.x.x.x和y.y.y.y是我们的两台生产DNS Server，而这台Salt-Master是访问不了的。我们再向上翻看，确实有读取nsswitch.conf，hosts.conf和reslov.conf等文件内容的操作，代码如下所示：

open("/etc/nsswitch.conf", O_RDONLY) = 3 // 打开

nsswitch.conf文件

fstat(3, {st_mode=S_IFREG|0644, st_size=1688, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd2dac4a000
read(3, "#\n# /etc/nsswitch.conf\n#\n# An ex"..., 4096) = 1688
read(3, "", 4096) = 0
close(3) = 0
munmap(0x7fd2dac4a000, 4096) = 0
open("/etc/host.conf", O_RDONLY) = 3 // 打开

host.conf文件

fstat(3, {st_mode=S_IFREG|0644, st_size=9, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd2dac4a000
read(3, "multi on\n", 4096) = 9
read(3, "", 4096) = 0
close(3) = 0
munmap(0x7fd2dac4a000, 4096) = 0
futex(0x30d5d91384, FUTEX_WAKE_PRIVATE, 2147483647) = 0
open("/etc/resolv.conf", O_RDONLY) = 3 // 打开

resolv.conf文件

fstat(3, {st_mode=S_IFREG|0644, st_size=194, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd2dac4a000
read(3, "# Generated by Cobbler post_scri"..., 4096) = 194
read(3, "", 4096) = 0
close(3) = 0

我们再向上翻看，观察程序运行初始化的过程，代码如下所示。

open("/etc/salt/minion_id", O_RDONLY) = -1 ENOENT (No such file or directory)
uname({sys="Linux", node="stationX.example.com", ...}) = 0
socket(PF_NETLINK, SOCK_RAW, 0) = 3
bind(3, {sa_family=AF_NETLINK, pid=0, groups=00000000}, 12) = 0
getsockname(3, {sa_family=AF_NETLINK, pid=34099, groups=00000000}, [12]) = 0
sendto(3, "\24\0\0\0\26\0\1\3\1q\304U\0\0\0\0\0\0\0\0", 20, 0, {sa_family=AF_NETLINK, pid=0, groups=00000000}, 12) = 20
recvmsg(3, {msg_name(12)={sa_family=AF_NETLINK, pid=0, groups=00000000}, msg_iov(1)=[{"0\0\0\0\24\0\2\0\1q\304U3\205\0\0\2\10\200\376\1\0\0\0\10\0\1\0\177\0\0\1"..., 4096}], msg_controllen=0, msg_flags=0}, 0) = 108
recvmsg(3, {msg_name(12)={sa_family=AF_NETLINK, pid=0, groups=00000000}, msg_iov(1)=[{"@\0\0\0\24\0\2\0\1q\304U3\205\0\0\n\200\200\376\1\0\0\0\24\0\1\0\0\0\0\0"..., 4096}], msg_controllen=0, msg_flags=0}, 0) = 128
recvmsg(3, {msg_name(12)={sa_family=AF_NETLINK, pid=0, groups=00000000}, msg_iov(1)=[{"\24\0\0\0\3\0\2\0\1q\304U3\205\0\0\0\0\0\0\1\0\0\0\24\0\1\0\0\0\0\0"..., 4096}], msg_controllen=0, msg_flags=0}, 0) = 20
close(3) = 0

Salt-Master运行任何salt命令之前，都会先取得主机的信息并尝试进行名称解析，因为/etc/resolv.conf文件中配置了不可访问的DNS Server，而且在resolv.conf文件中关于超时的设置没有明确定义，该设置项的默认值为10秒，两个不能访问的DNS各自都等待了10秒钟，最终引发了这个wait时间长达20多秒的问题。删除了/etc/resolv.conf中的错误条目后，故障立即解除。

这个解决问题的思路很巧妙。首先利用time观察到了时间瓶颈，既然是wait，就说明程序在这段时间除了等待什么都没干。而strace的作用就是把程序运行时的细节展示到前台上来，那么在wait时段的Debug信息肯定也是停止输出的，相当于程序自动设置了一个断点。我们第一次操作的目的是取得断点内容，第二次操作的目的是为了拿到完整的Debug信息。最后根据断点内容在整个Debug信息中找到其所在的位置，该断点位置附近的上下文中一定隐藏着真正的故障原因。

当然，最后我们还要提醒一下负责部署的同事，对相关问题进行同步修改。

11.6　小结

想要做好基础架构的工作，除了要掌握应有的技术之外，我认为更重要的是以下几点：

（1）要有全局的观念，考虑问题不要总是从自我利益出发。在技术的选用上，只关心如何让自己的工作更轻松，却不管别人的死活。不要坐了架构的位置，思想还停留在工程师的水平。

（2）要注重细节，不要做浮云架构，光会画大饼是不成的。做任何设计一定要考虑能否落地？自己是否亲自实践过？我觉得每个架构师都应该背着自己设计的降落伞至少跳一次飞机，而不是用别人的生命和鲜血去验证结果。

（3）杜绝无底线的服务意识，实际业务需求和整体基础架构规范应当和谐共存，不能相互绑架。

（4）不要觉得自己了不起，应当虚心并且不断地学习，多向别人讨教，因为总有你不会的东西，即便是你认为再简单的事情。

（5）有了成绩要多分享不要舍不得（超级解霸作者梁肇新的话）。当然，请一定要分享干货，也就是有细节、能落地、经得起验证的真成绩。

（6）要注意技术和知识的灵活运用，11.5节strace命令的使用就是一个好例子。

（7）要有一个可靠的伙伴，我的同事张望在网络方面给予了我很多支持，他是这方面的技术专家，能够快速地定位并解决部署过程中遇到的各种奇葩故障，而且还是一个很帅气的小伙儿。

（8）要有一位能认可你的能力、支持你的想法、并且包容你犯错误的好领导，这条最重要。

关于服务器交付的事儿，我们就聊到这里。有些读者可能会觉得SA不就是一个装机器的么？但基础架构无小事，即便是小小的装机，也同样严苛地考验着一个SA的技术水平。要知道，安装部署和服务配置只是SA的基本工作，能够真正地深入系统核心，透析系统机制，胜任内核参数调整、故障快速定位、解析CoreDump以及编写内核代码的工作，才是SA应有的境界和高度。

第12章　企业级Nginx Web服务优化实战

作者简介

冉宏元（老男孩），北京老男孩IT教育创始人，拥有10多年一线大规模网站集群实战运维架构经验及教学培训经验，经历并主导了服务器从几台到近千台大规模集群运维架构的发展过程；运维架构实战知识体系全面，擅长大规模集群架构部署调优、虚拟化、云计算、大数据及MySQL数据库等技术，是IT界最资深的Linux集群架构实战专家之一。

国内NLP心理学运维思想体系创始人，将心理学运维思想大量应用于教学培训实践，取得了显著效果，所教学生平均就业工资及后期发展速度连续多年在国内同行业排名第一！

12.1　Nginx基本安全优化

12.1.1　调整参数隐藏Nginx软件版本号信息

一般来说，软件的漏洞都和版本有关，这一点很像汽车的缺陷，同一批次的产品要有问题就都有问题，别的批次可能就都是好的。因此，我们应尽量隐藏或消除Web服务对访问用户显示各类敏感信息（例如Web软件名称及版本号等信息），这样恶意的用户就很难猜到他攻击的服务器所用的是否有特定漏洞的软件，或者是否有对应漏洞的某一特定版本，从而加强了Web服务的安全性。这在武侠小说里，就相当于隐身术，你隐身了，对手就很难打着你了。

想要隐身，首先要了解所使用软件的版本号，对于Linux客户端，可通过命令行查看Nginx版本号，最简单的方法就是在Linux客户端系统命令行执行如下curl命令：

[root@oldboy ~]# curl -I 10.0.0.7
HTTP/1.1 200 OK
Server: nginx/1.6.3 #<==这里很清晰地暴露了

Web版本号（

1.6.3）及软件名称

(Nginx)
Date: Thu, 09 Oct 2014 01:58:51 GMT
Content-Type: text/html
Content-Length: 18
Last-Modified: Thu, 25 Sep 2014 20:01:01 GMT
Connection: keep-alive
ETag: "5424747d-12"
Accept-Ranges: bytes

在Windows客户端上，通过浏览器访问Web服务时，若找不到页面，那么默认的报错信息如图12-1所示：

图12-1　找不到对应地址的错误页面显示

以上虽然是不同的客户端，但是都获得了Nginx软件名称，而且查到了Nginx的版本号，这就使得Nginx Web服务的安全存在一定的风险，因此，应隐藏掉这些敏感信息或用一个其他的名字将其替代。例如，下面是百度搜索引擎网站Web软件的更名做法：

[root@oldboy ~]# curl -I baidu.com
HTTP/1.1 200 OK
Date: Tue, 21 Oct 2014 03:31:01 GMT
Server: Apache #<==将

Web服务软件更名为了

Apache，并且版本号也去掉了

[root@oldboy ~]# curl -I -s www.baidu.com|grep Server
Server: BWS/1.1 #<==将

Web服务软件更名为了

BWS，并且版本号被改为

1.1（闭源的软件名称

和版本就无所谓了）

门户网站尚且如此，我们也学着隐藏或改掉应用服务的软件名和版本号吧！

事实上，还可以通过配置文件加参数来隐藏Nginx版本号。

编辑nginx.conf配置文件增加参数，实现隐藏Nginx版本号的方式如下。

在Nginx配置文件nginx.conf中的http标签段内加入“server_tokens off；”参数，具体如下：

http
{
......
server_tokens off;
......
}

此参数放置在http标签内，作用是控制http response header内的Web服务版本信息的显示，以及错误信息中Web服务版本信息的显示。

server_tokens参数的官方说明如下：

syntax: server_tokens on | off; #?<==此行为参数语法，

on为开启状态，

off为关闭状态

default: server_tokens on; #?<==此行的意思是不配置该参数，软件默认情况的结果

context: http, server, location #?<==此行为

server_tokens参数可以放置的位置

参数作用：激活或禁止

Nginx的版本信息显示在报错信息和

Server的响应首部位置中

Enables or disables emitting of nginx version in error messages and in the "Server" response header field. #?<==此行是参数的作用原文，一定要细看

官方资料地址：http://nginx.org/en/docs/http/ngx_http_core_module.html
。

配置完毕后保存，重新加载配置文件，再次通过curl查看，结果如下：

[root@oldboy conf]# /application/nginx/sbin/nginx -s reload
[root@oldboy conf]# curl -I 10.0.0.7
HTTP/1.1 200 OK
Server: nginx #?<==版本号已消失

Date: Thu, 09 Oct 2014 02:03:32 GMT
Content-Type: text/html
Content-Length: 18
Last-Modified: Thu, 25 Sep 2014 20:01:01 GMT
Connection: keep-alive
ETag: "5424747d-12"
Accept-Ranges: bytes

此时，浏览器的报错提示中没有了版本号，如图12-2所示，修改成功。

图12-2　无版本号的错误页面显示

12.1.2　更改源码隐藏Nginx软件名及版本号

隐藏了Nginx版本号后，更进一步，可以通过一些手段把Web服务软件的名称也隐藏起来，或者更改为其他Web服务软件名以迷惑黑客。但对于软件名字的隐藏修改，一般情况下不会有配置参数和入口，Nginx也不例外，这可能是由于商业及品牌展示等原因，软件提供商不希望使用者把软件名字隐藏起来。因此，此处需要更改Nginx源代码，具体的解决方法如下。

第一步是依次修改3个Nginx源码文件。

修改的第一个文件为nginx-1.6.3/src/core/nginx.h，代码如下：

[root@oldboy core]# sed -n '13,17p' nginx.h
#define NGINX_VERSION "1.6.3" #<==修改为想要显示的版本号，如

2.2.23
#define NGINX_VER "nginx/" NGINX_VERSION
#<==将

Nginx修改为想要修改的软件名称，如

OWS
#define NGINX_VAR "NGINX" #<==将

Nginx修改为想要修改的软件名称，

如

OWS（

Oldboy Web Server）

#define NGX_OLDPID_EXT ".oldbin"

修改后的结果如下：

[root@oldboy core]# sed -n '13,17p' nginx.h
#define NGINX_VERSION "2.2.23" #<==已修改为想要显示的版本号

2.2.23
#define NGINX_VER "OWS/" NGINX_VERSION #<==已修改为想要显示的名字

OWS
#define NGINX_VAR "OWS" #<==已修改为想要显示的名字

OWS
#define NGX_OLDPID_EXT ".oldbin"

修改的第二个文件是nginx-1.6.3/src/http/ngx_http_header_filter_module.c的第49行，需要修改的字符串如下：

[root@oldboy http]# grep -n 'Server: nginx' ngx_http_header_filter_module.c
49:static char ngx_http_server_string[] = "Server: nginx " CRLF; #<==修改本行结尾的

Nginx。

通过sed替换修改，当然，也可以通过编辑器进行如下修改：

[root@oldboy http]# sed -i 's#Server: nginx#Server: OWS#g' ngx_http_header_filter_module.c
#<==把

Server: nginx替换为

Server: OWS，注意

Nginx字符串很多，所以加上

Server: nginx一起替换。当然也可以指定行替换，这样就可以直接替换

nginx字符串了。

sed -i '49 s#nginx#OWS#g' ngx_http_header_filter_module.c

修改后的结果如下：

[root@oldboy http]# grep -n 'Server: OWS' ngx_http_header_filter_module.c
49:static char ngx_http_server_string[] = "Server: OWS" CRLF; #<==变成

OWS了。

修改的第三个文件是nginx-1.6.3/src/http/ngx_http_special_response.c，对外页面报错时，它会控制是否展示敏感信息。这里输出修改前的信息ngx_http_special_response.c中的第21~30行，代码如下：

[root@oldboy http]# sed -n '21,30p' ngx_http_special_response.c
static u_char ngx_http_error_full_tail[] =
"<hr><center>" NGINX_VER "</center>" CRLF#<==此行需要修改

"</body>" CRLF
"</html>" CRLF
;
static u_char ngx_http_error_tail[] =
"<hr><center>Nginx</center>" CRLF#<==此行需要修改

"</body>" CRLF

将上述内容中的“"<hr><center>"NGINX_VER"</center>"CRLF”修改为“"<hr><center>"NGINX_VER"（http://oldboy.blog.51cto.com）</center>"CRLF”，然后将“"<hr><center>Nginx</center>"CRLF”修改为“"<hr><center>OWS</center>"CRLF”。

修改后的结果如下：

[root@oldboy http]# sed -n '21,30p' ngx_http_special_response.c
static u_char ngx_http_error_full_tail[] =
"<hr><center>" NGINX_VER " (http://oldboy.blog.51cto.com)</center>" CRLF#<==此行是定义对外展示的内容

"</body>" CRLF
"</html>" CRLF
;
static u_char ngx_http_error_tail[] =
"<hr><center>OWS</center>" CRLF #<==此行将对外展示的

Nginx名字更改为

OWS
"</body>" CRLF

第二步是修改后编译软件，使其生效。

修改后再编译安装软件，如果是已安装好的服务，则需要重新编译Nginx，配好配置，启动服务。

再次使浏览器出现404错误，然后查看访问结果，如图12-3所示：

图12-3　去掉软件名字和版本的404页面图

如图12-3所示，Nginx的软件和版本名都被改掉了，并且加上了我们自己的博客地址。

再看看Linux curl命令响应的头部信息，代码如下：

[root@oldboy http]# curl -I bbs.etiantian.org/oldboy/
HTTP/1.1 404 Not Found
Server: OWS/2.2.23 #<==也更改了

Date: Thu, 12 Feb 2015 07:00:52 GMT
Content-Type: text/html
Content-Length: 198
Connection: keep-alive

说明

·提升网站安全，要从最简单、最短板、最低点的地方入手解决问题，如果门打开着，即使给窗户安装再结实的护栏也没有意义。

·向有经验的人及优秀的网站公司学习。

·学会看官方文档，根据第一手资料去分析。

·命令输出结果中含有需要过滤或要保留的内容时，命令自身可能有参数可直接实现。

·掌握技术思想比解决问题本身更重要，详情请见老男孩的博客：http://oldboy.blog.51cto.com/2561410/1196298
。

12.1.3　更改Nginx服务的默认用户

为了让Web服务更安全，需要尽可能地改掉软件默认的所有配置，包括端口、用户等。

下面就来更改Nginx服务的默认用户。

首先，查看Nginx服务对应的默认用户。一般情况下，Nginx服务启动后，默认使用的用户是nobody，查看默认的配置文件，代码如下：

[root@oldboy conf]# grep '#user' nginx.conf.default
#user nobody;

为了防止黑客猜到这个Web服务的用户，我们需要将其更改成特殊的用户名，例如nginx或特殊点的inca，但是这个用户必须是系统里事先存在的，下面以nginx用户为例进行说明。

（1）为Nginx服务建立新用户。

为Nginx服务建立新用户的操作过程如下：

useradd nginx -s /sbin/nologin -M
#<==不需要有系统登录权限，应当禁止其登录能力，相当于

Apache里的用户

id nginx #<==检查用户

（2）配置Nginx服务，让其使用刚建立的nginx用户。

更改Nginx服务默认使用的用户，方法有两种：

第一种为直接更改配置文件参数，将默认的“#user nobody；”改为如下内容：

user nginx nginx;

如果注释或不设置上述参数，则默认为nobody用户，不推荐使用nobody用户名，最好采用一个普通的用户名，此处用大家习惯的、前文建立好的nginx用户。

第二种方法为在编译Nginx软件时直接指定编译的用户和组，命令如下：

./configure --user=nginx --group=nginx --prefix=/application/nginx1.6.3 --with-http_stub_status_module --with-http_ssl_module

提示

前文在编译Nginx服务时，就是这样带着参数的，因此无论配置文件中是否添加参数，默认都是nginx用户。

（3）检查更改用户的效果。

重新加载配置后，检查Nginx服务进程的对应用户，代码如下：

[root@oldboy conf]# ps -ef|grep nginx|grep -v grep
root 1428 1 0 09:56 ? 00:00:00 nginx: master process /
application/nginx/sbin/nginx
nginx 1610 1428 0 10:03 ? 00:00:00 nginx: worker process
nginx 1611 1428 0 10:03 ? 00:00:00 nginx: worker process

通过查看上述更改后的Nginx进程，可以看到worker processes对应的用户都变成了nginx。所以，我们有理由得出结论，上述的两种方法都可设置Nginx的worker进程运行的用户。当然，Nginx的主进程还是以root身份运行的，本章后面也会有更改root主进程服务用户的深度安全优化与架构技巧讲解。

12.2　根据参数优化Nginx服务性能

12.2.1　优化Nginx服务的worker进程个数

在高并发、高访问量的Web服务场景中，需要事先启动好更多的Nginx进程，以保证快速响应并处理大量并发用户的请求。

这类似于开饭店，在营业前，需要事先招聘一定数量的服务员准备接待顾客，但这里存在一个问题，如果饭店对客流量没有一个正确的预估，那么就会导致一些问题发生，例如：服务员招聘过多，客流却很少，那么服务员可能就会很闲，没事干，饭店的成本也高了；如果客流很大，而服务员人数少了，那么可能就会接待不过来顾客，导致顾客吃饭体验差。因此，饭店要根据客户的流量及并发量来调整接待的服务人员数量，然后根据顾客量变化的监测结果及时调整到最佳的配置。

Nginx服务就相当于饭店，网站用户就相当于顾客，Nginx的进程就相当于服务员，下面就来讲解如何优化Nginx进程的个数。

1.优化Nginx进程对应的配置

优化Nginx进程对应Nginx服务的配置参数如下：

worker_processes 1; #<==指定了

Nginx要开启的进程数，结尾的数字就是进程的个数

worker_processes 1；#<==指定了Nginx要开启的进程数，结尾的数字就是进程的个数上述参数调整的是Nginx服务的worker进程数，Nginx有Master进程和worker进程之分，Master为管理进程，真正接待“顾客”的是worker进程。

2.优化Nginx进程个数的策略

前面已经讲解过，worker_processes参数大小的设置最好和网站的用户数量相关联，可如果是新配置，不知道网站的用户数量时该怎么办呢？

搭建服务器时，worker进程数最开始的设置可以等于CPU的核数，且worker进程数要多一些，这样起始提供服务时就不会出现因为访问量快速增加而需要临时启动新进程提供服务的问题，缩短了系统的瞬时开销和提供服务的时间，提升了服务用户的速度。高流量高并发场合也可以考虑将进程数提高至CPU核数×2，具体情况要根据实际的业务来选择，因为这个参数除了要和CPU核数匹配之外，也与硬盘存储的数据及系统的负载有关，设置为CPU的核数是一个好的起始配置，这也是官方的建议。

3.查看Web服务器CPU硬件资源信息

下面介绍查看Linux服务器CPU总核数的方法。

通过/proc/cpuinfo可查看CPU的个数及总核数。查看CPU总核数的示例如下：

[oldboy@oldboy ~]$ grep processor /proc/cpuinfo|wc -l
4 #<==表示为

1颗

CPU四核

[root@oldboy ~]# grep -c processor /proc/cpuinfo
4 #<==表示为

1颗

CPU四核

查看CPU总颗数的示例如下：

[oldboy@oldboy ~]$ grep 'physical id' /proc/cpuinfo|sort|uniq|wc -l
1 #<==对

physical id去重计数，表示

1颗

CPU

通过执行top命令，然后按数字1，即可显示所有的CPU核数，代码如下：

[root@oldboy-server ~]# top ←

按

1显示多核

cpu
top - 11:31:10 up 608 days, 16:04, 2 users, load average: 0.00, 0.00, 0.00
Tasks: 121 total, 1 running, 114 sleeping, 6 stopped, 0 zombie
Cpu0 : 0.2%us, 0.1%sy, 0.0%ni, 99.2%id, 0.5%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu1 : 0.1%us, 0.0%sy, 0.0%ni, 99.5%id, 0.3%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu2 : 0.2%us, 0.1%sy, 0.0%ni, 99.4%id, 0.3%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu3 : 0.2%us, 0.1%sy, 0.0%ni, 99.4%id, 0.3%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 8173172k total, 8126340k used, 46832k free, 419508k buffers
Swap: 4192956k total, 156k used, 4192800k free, 6681084k cached
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 15 0 10368 680 572 S 0.0 0.0 0:01.94 init
2 root RT -5 0 0 0 S 0.0 0.0 0:02.48 migration/0
#<==这是单

CPU四核的信息

例如：

CPU核数为

4，就配置

worker_processes 4

4.实践修改Nginx配置

假设服务器的CPU颗数为1颗，核数为4核，则初始的配置可通过查看默认的nginx.conf里的worker_processes数来了解，命令如下：

[root@oldboy conf]# grep worker_processes nginx.conf
worker_processes 1;

这里修改参数值为CPU的总核数4，然后重新加载Nginx服务。

修改配置的方法如下：

[root@oldboy conf]# sed -i 's#worker_processes 1#worker_processes 4#g' nginx.conf
[root@oldboy conf]# grep worker_processes nginx.conf
worker_processes 4;提示：可以通过

vi修改

优雅重启Nginx，使修改生效，代码如下：

[root@oldboy ~]# /application/nginx/sbin/nginx -t
nginx: the configuration file /application/nginx1.6.3/conf/nginx.conf syntax is ok
nginx: configuration file /application/nginx1.6.3/conf/nginx.conf test is successful
[root@oldboy ~]# /application/nginx/sbin/nginx -s reload

现在检查修改后的worker进程数量，代码如下：

[root@oldboy ~]# ps -ef|grep nginx|grep -v grep
root 1428 1 0 09:56 ? 00:00:00 nginx: master process /application/nginx/sbin/nginx
nginx 1832 1428 0 10:24 ? 00:00:00 nginx: worker process
nginx 1833 1428 0 10:24 ? 00:00:00 nginx: worker process
nginx 1834 1428 0 10:24 ? 00:00:00 nginx: worker process
nginx 1835 1428 0 10:24 ? 00:00:00 nginx: worker process

从“worker_processes 4”可知，worker的进程数为4个。Nginx Master主进程不包含在这个参数内，Nginx Master的主进程为管理进程，负责调度和管理worker进程。

有关worker_processes参数的官方说明如下：

syntax: worker_processes number; #<==此行为参数语法，

number为数量

default: worker_processes 1; #<==此行的意思是不配置该参数，软件默认情况下数量为

1
context: main #<==此行为

worker_processes参数可以放置的位置

worker_processes为定义

worker进程数的数量，建议设置为

CPU的核数或

CPU核数

×2，具体情况要根据实际的业务来选择，因为这个参数，除了要和

CPU核数匹配之外，还与硬盘存储的数据及系统的负载有关，设置为

CPU的个数或核数是一个好的起始配置。

From : http://nginx.org/en/docs/ngx_core_module.html

12.2.2　优化绑定不同的Nginx进程到不同的CPU上

默认情况下，Nginx的多个进程有可能运行在某一个CPU或CPU的某一核上，导致Nginx进程使用硬件的资源不均，本节的优化将尽可能地将不同的Nginx进程分配给不同的CPU处理，达到充分有效利用硬件的多CPU多核资源的目的。

在优化不同的Nginx进程对应不同的CPU配置时，四核CPU服务器的参数配置参考如下：

worker_processes 4;
worker_cpu_affinity 0001 0010 0100 1000;
#<== worker_cpu_affinity就是配置

Nginx进程与

CPU亲和力的参数，即把不同的进程分给不同的

CPU处理。这里

0001 0010 0100 1000是掩码，分别代表第

1、

2、

3、

4核

CPU，由于

worker_
processes进程数为

4，因此，上述配置会为每个进程分配一核

CPU处理，默认情况下进程不会绑定任何

CPU，参数位置为

main段。

八核CPU服务器的参数配置参考如下：

worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000;
worker_cpu_affinity 0001 0010 0100 1000 0001 0010 0100 1000;

worker_cpu_affinity参数的官方说明如下：

syntax: worker_cpu_affinity cpumask ...; #<==此行为

CPU亲和力参数语法，

?cpumask为

CPU掩码

default: —

#<==默认不设置

context: main #<==此行为

worker_cpu_affinity
?参数可以放置的位置

worker_cpu_affinity的作用是绑定不同的worker进程数到一组CPU上。通过设置bitmask控制进程允许使用的CPU，默认worker进程不会绑定到任何CPU上。

下面是绑定的示例配置：

worker_processes 4;
worker_cpu_affinity 0001 0010 0100 1000;
binds each worker process to a separate CPU, while
worker_processes 2;
worker_cpu_affinity 0101 1010;
binds the first worker process to CPU0/CPU2, and the second worker process to CPU1/CPU3. The second example is suitable for hyper-threading.The directive is only available on FreeBSD and Linux.
From : http://nginx.org/en/docs/ngx_core_module.html by oldboy

下面是压力测试配置结果。配置前的压力测试结果为：

top - 16:30:17 up 41 min, 2 users, load average: 2.30, 1.50, 1.35
Tasks: 104 total, 4 running, 100 sleeping, 0 stopped, 0 zombie
Cpu0 : 6.7%us, 9.3%sy, 0.0%ni, 4.3%id, 0.0%wa, 5.0%hi, 74.7%si, 0.0%st
Cpu1 : 18.3%us, 26.6%sy, 0.0%ni, 48.8%id, 0.0%wa, 0.0%hi, 6.3%si, 0.0%st
Cpu2 : 15.9%us, 24.9%sy, 0.0%ni, 54.8%id, 0.0%wa, 0.0%hi, 4.3%si, 0.0%st
Cpu3 : 16.0%us, 27.0%sy, 0.0%ni, 53.3%id, 0.0%wa, 0.0%hi, 3.7%si, 0.0%st
top - 16:30:38 up 41 min, 2 users, load average: 2.65, 1.63, 1.39
Tasks: 104 total, 3 running, 101 sleeping, 0 stopped, 0 zombie
Cpu0 : 7.7%us, 11.3%sy, 0.0%ni, 4.7%id, 0.0%wa, 5.0%hi, 71.3%si, 0.0%st
Cpu1 : 17.3%us, 29.2%sy, 0.0%ni, 47.8%id, 0.3%wa, 0.0%hi, 5.3%si, 0.0%st
Cpu2 : 17.7%us, 24.0%sy, 0.0%ni, 53.7%id, 0.0%wa, 0.0%hi, 4.7%si, 0.0%st
Cpu3 : 17.7%us, 27.7%sy, 0.0%ni, 51.3%id, 0.3%wa, 0.0%hi, 3.0%si, 0.0%st
top - 16:30:57 up 42 min, 2 users, load average: 2.48, 1.65, 1.41
Tasks: 104 total, 5 running, 99 sleeping, 0 stopped, 0 zombie
Cpu0 : 6.0%us, 11.4%sy, 0.0%ni, 4.7%id, 0.0%wa, 5.0%hi, 72.9%si, 0.0%st
Cpu1 : 17.9%us, 26.9%sy, 0.0%ni, 50.5%id, 0.0%wa, 0.0%hi, 4.7%si, 0.0%st
Cpu2 : 16.9%us, 26.8%sy, 0.0%ni, 51.7%id, 0.0%wa, 0.0%hi, 4.6%si, 0.0%st
Cpu3 : 16.6%us, 27.6%sy, 0.0%ni, 51.8%id, 0.0%wa, 0.0%hi, 4.0%si, 0.0%st
top - 16:31:22 up 42 min, 2 users, load average: 2.65, 1.76, 1.45
Tasks: 104 total, 4 running, 100 sleeping, 0 stopped, 0 zombie
Cpu0 : 7.7%us, 12.7%sy, 0.0%ni, 5.3%id, 0.0%wa, 5.0%hi, 69.3%si, 0.0%st
Cpu1 : 19.4%us, 28.4%sy, 0.0%ni, 47.2%id, 0.0%wa, 0.0%hi, 5.0%si, 0.0%st
Cpu2 : 17.0%us, 26.3%sy, 0.0%ni, 52.3%id, 0.0%wa, 0.0%hi, 4.3%si, 0.0%st
Cpu3 : 16.9%us, 25.6%sy, 0.0%ni, 54.2%id, 0.0%wa, 0.0%hi, 3.3%si, 0.0%st

通过观察，我们发现配置前不同CPU的使用率相对平均。

配置worker_cpu_affinity，代码如下：

[root@www ~]# grep worker_cpu nginx.conf
worker_cpu_affinity 0001 0010 0100 1000;

压力测试的命令及结果如下：

[root@www ~]#webbench -c 20000 -t 180 http://10.0.0.190/
Webbench - Simple Web Benchmark 1.5
Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.
Benchmarking: GET http://10.0.0.190/
20000 clients, running 180 sec.
Speed=1521283 pages/min, 3588193 bytes/sec.
Requests: 3661785 susceed, 902066 failed.

查看CPU调度的结果如下：

top - 16:24:58 up 36 min, 2 users, load average: 1.65, 1.58, 1.34
Tasks: 104 total, 4 running, 100 sleeping, 0 stopped, 0 zombie
Cpu0 : 4.7%us, 8.0%sy, 0.0%ni, 7.3%id, 0.0%wa, 5.3%hi, 74.8%si, 0.0%st
Cpu1 : 16.9%us, 32.2%sy, 0.0%ni, 43.2%id, 0.0%wa, 0.0%hi, 7.6%si, 0.0%st
Cpu2 : 16.6%us, 26.2%sy, 0.0%ni, 52.2%id, 0.0%wa, 0.0%hi, 5.0%si, 0.0%st
Cpu3 : 16.9%us, 28.2%sy, 0.0%ni, 49.8%id, 0.0%wa, 0.0%hi, 5.0%si, 0.0%st
top - 16:25:36 up 36 min, 2 users, load average: 1.83, 1.64, 1.37
Tasks: 104 total, 2 running, 102 sleeping, 0 stopped, 0 zombie
Cpu0 : 5.0%us, 8.1%sy, 0.0%ni, 9.7%id, 0.0%wa, 5.0%hi, 72.1%si, 0.0%st
Cpu1 : 17.6%us, 31.2%sy, 0.0%ni, 43.9%id, 0.0%wa, 0.0%hi, 7.3%si, 0.0%st
Cpu2 : 16.0%us, 26.7%sy, 0.0%ni, 52.7%id, 0.0%wa, 0.0%hi, 4.7%si, 0.0%st
Cpu3 : 17.9%us, 28.6%sy, 0.0%ni, 48.8%id, 0.0%wa, 0.0%hi, 4.7%si, 0.0%st
top - 16:26:09 up 37 min, 2 users, load average: 2.01, 1.71, 1.40
Tasks: 104 total, 4 running, 100 sleeping, 0 stopped, 0 zombie
Cpu0 : 6.3%us, 9.0%sy, 0.0%ni, 10.6%id, 0.0%wa, 4.7%hi, 69.4%si, 0.0%st
Cpu1 : 19.3%us, 32.6%sy, 0.0%ni, 41.9%id, 0.0%wa, 0.0%hi, 6.3%si, 0.0%st
Cpu2 : 17.9%us, 26.9%sy, 0.0%ni, 51.8%id, 0.0%wa, 0.0%hi, 3.3%si, 0.0%st
Cpu3 : 19.7%us, 30.0%sy, 0.0%ni, 46.3%id, 0.0%wa, 0.0%hi, 4.0%si, 0.0%st
top - 16:26:22 up 37 min, 2 users, load average: 2.21, 1.77, 1.42
Tasks: 104 total, 4 running, 100 sleeping, 0 stopped, 0 zombie
Cpu0 : 5.3%us, 8.3%sy, 0.0%ni, 10.3%id, 0.0%wa, 5.0%hi, 71.0%si, 0.0%st
Cpu1 : 19.3%us, 29.2%sy, 0.0%ni, 45.8%id, 0.0%wa, 0.0%hi, 5.6%si, 0.0%st
Cpu2 : 15.9%us, 24.3%sy, 0.0%ni, 55.8%id, 0.0%wa, 0.0%hi, 4.0%si, 0.0%st
Cpu3 : 16.7%us, 27.0%sy, 0.0%ni, 52.7%id, 0.0%wa, 0.0%hi, 3.7%si, 0.0%st

通过观察，我们发现配置后不同CPU的使用率相对平均，和测试前的变化不大。因此可知，默认就是比较平均的，一方面，可能是Nginx软件自身正在逐渐优化，使其使用多核CPU时更为均衡；另一方面，测试的数据可能还有待调整。

另外（taskset-retrieve or set a process’s CPU affinity）命令本身也有分配CPU的功能，这里留给大家测试（例如：taskset-c 1，2，3/etc/init.d/mysql start）：

-c, --cpu-list
specify a numerical list of processors instead of a bitmask.
The list may contain multiple items, separated by comma, and
ranges. For example, 0,5,7,9-11.

12.2.3　Nginx事件处理模型优化

Nginx的连接处理机制在不同的操作系统中会采用不同的I/O模型，在Linux下，Nginx使用epoll的I/O多路复用模型，在Freebsd中使用kqueue的I/O多路复用模型，在Solaris中使用/dev/poll方式的I/O多路复用模型，在Windows中使用的是icop，等等。

要根据系统类型选择不同的事件处理模型，可供使用的选择有“use[kqueue|rtsig|epoll|/dev/poll|select|poll]；”。本书使用的是CentOS 6.6 Linux，因此将Nginx的事件处理模型调整为epoll模型。

具体的配置参数如下：

events
#<==events指令是设定

Nginx的工作模式及连接数上限

{
use epoll;
#<==use是一个事件模块指令，用来指定

Nginx的工作模式。

Nginx支持的工作模式有

select、

poll、

kqueue、

epoll、

rtsig和

/dev/poll。其中

select和

poll都是标准的工作模式，

kqueue和

epoll是高效的工作模式，不同的是

epoll用在

Linux平台上，而

kqueue用在

BSD系统中。对于

Linux系统

Linux 2.6+的内核，推荐选择

epoll工作模式，这是高性能高并发的设置

}

根据Nginx的官方文档建议，也可以不指定事件处理模型，Nginx会自动选择最佳的事件处理模型服务。

events区块及use事件处理参数的官方说明如下：

syntax: events { ... } #<==语法配置

default: —

#<==默认没有设置

context: main #<==events标签的放置位置，放在

main段

events区块是一个用来设置连接进程的区块，例如：设置Nginx的网络I/O模型，以及连接数等。

use事件的处理参数说明如下：

syntax: use method; #<==网络模型配置，

method选择模型之一

default: —

#<==默认没有设置

context: events #<==网络模型配置放置于

events区块内

对于使用连接进程的方法，通常不需要进行任何设置，Nginx会自动选择最有效的方法。

提示

事件处理模型参数需要在events区块中进行配置。

12.2.4　调整Nginx单个进程允许的客户端最大连接数

接下来，调整Nginx单个进程允许的客户端最大连接数，控制连接数的参数为worker_connections。

worker_connections的值要根据具体服务器的性能和程序的内存使用量来指定（单个进程启动使用的内存根据程序来确定），代码如下：

events #<==events指令是设定

Nginx的工作模式及连接数上限

{
worker_connections 20480;
#<==worker_connections也是个事件模块指令，用于定义

Nginx每个进程的最大连接数，默认是

1024。最大客户端连接数由

worker_processes和

worker_connections决定，即

Max_client= worker_processes*worker_connections。

进程的最大连接数受

Linux系统进程的最大打开文件数限制，在执行操作系统命令“

ulimit -HSn 65535”或配置相应文件后，

worker_connections的设置才能生效

下面是worker_connections的官方说明。

参数语法：worker_connections number

默认配置：worker_connections 512

放置位置：events

说明：worker_connections用来设置一个worker process支持的最大并发连接数，这个连接数包括了所有连接，例如：代理服务器的连接、客户端的连接等，实际的并发连接数除了受worker_connections参数控制之外，还与最大打开文件数worker_rlimit_nofile有关（见下文），Nginx总并发连接=worker数量×worker_connections。

参考资料：http://nginx.org/en/docs/ngx_core_module.html
。

12.2.5　配置Nginx worker进程的最大打开文件数

接下来，调整配置Nginx worker进程的最大打开文件数，这个控制连接数的参数为worker_rlimit_nofile。该参数的实际配置如下：

worker_rlimit_nofile 65535;
#<==最大打开文件数，可设置为系统优化后的

ulimit -HSn的结果

下面是worker_rlimit_nofile number的官方说明。

参数语法：worker_rlimit_nofile number

默认配置：无

放置位置：主标签段

说明：此参数的作用是改变worker processes能打开的最大文件数

参考资料：http://nginx.org/en/docs/ngx_core_module.html

12.2.6　优化服务器域名的散列表大小

先要将确切名字和通配符名字存储在散列表中。散列表与监听端口关联，每个端口最多关联到三张表：确切名字的散列表、以星号起始的通配符名字的散列表和以星号结束的通配符名字的散列表。散列表的尺寸在配置阶段进行了优化，可以以最小的CPU缓存命中失败来找到名字。Nginx首先会搜索确切名字的散列表；如果没有找到，则搜索以星号起始的通配符名字的散列表；如果还是没有找到，则继续搜索以星号结束的通配符名字的散列表。因为名字是按照域名的字节来搜索的，所以搜索通配符名字的散列表比搜索确切名字的散列表慢。注意.nginx.org存储在通配符名字的散列表中，而不在确切名字的散列表中。由于正则表达式是逐个进行串行测试的，因此该方式也是最慢的，而且不可扩展。

鉴于以上原因，请尽可能地使用确切的名字。举个例子，如果使用nginx.org和www.nginx.org
来访问服务器是最频繁的，那么将它们明确地定义出来就更为有效，命令如下：

server {
listen 80;
server_name nginx.org www.nginx.org *.nginx.org;
...
}

下面这种方法更简单，但是效率也更低：

server {
listen 80;
server_name .nginx.org;
...
}

如果定义了大量的名字，或者定义了非常长的名字，那就需要在HTTP配置块中调整server_names_hash_max_size和server_names_hash_bucket_size的值。server_names_hash_bucket_size的默认值可能是32或64，也可能是其他值，这取决于CPU的缓存行的长度。如果这个值是32，那么定义“too.long.server.name.nginx.org”作为虚拟主机名就会失败，此时会显示如下的错误信息：

could not build the server_names_hash,
you should increase server_names_hash_bucket_size: 32

若出现了这种情况，那就需要将设置值扩大一倍，命令如下：

http {
server_names_hash_bucket_size 64;
...

如果定义了大量名字，则会得到如下的另外一个错误信息：

could not build the server_names_hash,
you should increase either server_names_hash_max_size: 512
or server_names_hash_bucket_size: 32

应该先尝试设置server_names_hash_max_size的值，此值差不多等于名字列表的名字总量。如果还不能解决问题，或者服务器启动非常缓慢，那么再尝试增加server_names_hash_bucket_size的值，具体信息如下：

server_names_hash_max_size 512; #默认是

512KB，一般要查看系统给出确切

的值。这里一般是

CPU L1 的

4-5倍

server_names_hash_max_size的官方说明如下：

syntax: server_names_hash_max_size size; #<==参数语法

default: server_names_hash_max_size 512; #<==参数默认大小

context: http #<==仅能放置在

http标签段

参数作用：设置存放域名（server names）的最大散列表大小。细节见http://nginx.org/en/docs/ngx_core_module.html
。

第二个参数如下：

server_names_hash_bucket_size 128;
#<==不能带单位！配置主机时必须设置该值，否则无法运行

Nginx，或者无法通过测试。该设置与

server_names_hash_max_size共同控制保存服务器名的

hash表，

hash bucket size总是等于

hash表的大小，并且是一路处理器缓存大小的倍数。若

hash bucket size等于一路处理器缓存的大小，那么在查找键时，最坏的情况下在内存中查找的次数为

2。第一次是确定存储单元的地址，第二次是在存储单元中查找键值。若报出

hash max size 或

hash bucket size的提示，则需要增加

server_names_hash_max_size的值

server_names_hash_bucket_size的官方说明如下：

syntax: server_names_hash_bucket_size size; #<==参数语法

default: server_names_hash_bucket_size 32|64 |128; #<==参数默认大小

context: http#<==仅能放置在

http标签段

参数作用：设置存放域名（server names）的最大散列表的存储桶（bucket）的大小。默认值依赖CPU的缓存行。细节见http://nginx.org/en/docs/ngx_core_module.html
。

12.2.7　开启高效文件传输模式

1.设置参数：sendfile on；

sendfile参数用于开启文件的高效传输模式。同时将tcp_nopush和tcp_nodelay两个指令设置为on，可防止网络及磁盘I/O阻塞，提升Nginx工作效率。

sendfile参数的官方说明如下：

syntax: sendfile on | off; #<==参数语法

default: sendfile off; #<==参数默认大小

context: http, server, location, if in location #<==可以放置的标签段

参数作用：激活或禁用sendfile（）功能。sendfile（）是作用于两个文件描述符之间的数据复制函数，这个复制操作是发生在内核之中的，被称为“零复制”，sendfile（）比read和write函数要高效很多，因为，read和wrtie函数要把数据复制到应用层再进行操作。相关控制参数还有sendfile_max_chunk，读者可以自行查询。细节见http://nginx.org/en/docs/http/ngx_http_core_module.html#sendfile
。

2.设置参数：tcp_nopush on；

tcp_nopush参数的官方说明如下：

syntax: tcp_nopush on | off; #<==参数语法

default: tcp_nopush off; #<==参数默认大小

context: http, server, location #<==可以放置的标签段

参数作用：激活或禁用Linux上的TCP_CORK socket选项，此选项仅仅当开启sendfile时才生效，激活这个.tcp_nopush参数可以允许把http response header和文件的开始部分放在一个文件里发布，其积极的作用是减少网络报文段的数量。细节见http://nginx.org/en/docs/http/ngx_http_core_module.html
。

12.2.8　优化Nginx连接参数，调整连接超时时间

1.什么是连接超时

先来个比喻吧，某饭店请了服务员招待顾客，但是现在饭店不景气，此时，为多余的服务员发工资使得成本被提高，想减少饭店开支成本就得解雇服务员。

这里的服务员就相当于Nginx服务建立的连接，当服务器建立的连接没有接收处理请求时，可在指定的时间内让它超时自动退出。还有当Nginx和FastCGI服务建立连接请求PHP时，如果因为一些原因（负载高、停止响应），FastCGI服务无法给Nginx返回数据，此时可以通过配置Nginx服务参数使其不会死等，因为前面的用户还等着它返回数据呢，例如，可设置为如果请求FastCGI 10秒内不能返回数据，那么Nginx就中断本次请求，向用户汇报取不到数据的错误。

2.连接超时的作用

·将无用的连接设置为尽快超时，可以保护服务器的系统资源（CPU、内存、磁盘）。

·当连接很多时，及时断掉那些已经建立好的但又长时间不做事的连接，以减少其占用的服务器资源，因为服务器维护连接也是消耗资源的。

·有时黑客或恶意用户攻击网站，就会不断地和服务器建立多个连接，消耗连接数，但是啥也不干，大量消耗服务器的资源，此时就应该及时断掉这些恶意占用资源的连接。

·LNMP环境中，如果用户请求了动态服务，则Nginx就会建立连接，请求FastCGI服务及后端MySQL服务，此时这个Nginx连接就要设定一个超时时间，在用户容忍的时间内返回数据，或者再多等一会后端服务返回数据，具体的策略要根据具体业务进行具体分析。当然了，后端的FastCGI服务及MySQL服务也有对连接的超时控制。

简单地说，连接超时是服务自我管理、自我保护的一种重要机制。

3.连接超时带来的问题，以及不同程序连接设定的知识

服务器建立新连接也是要消耗资源的，因此，超时设置得太短而并发很大，就会导致服务器瞬间无法响应用户的请求，导致用户体验下降。

企业生产中有些PHP程序站点会希望设置成短连接，因为PHP程序建立连接消耗的资源和时间相对要少些。而对于Java程序站点来说，一般建议设置成长连接，因为Java程序建立连接消耗的资源和时间更多，这是语言运行机制决定的。

4.Nginx连接超时的参数设置

1）设置参数：keepalive_timeout 60；

用于设置客户端连接保持会话的超时时间为60秒。若超过这个时间，服务器就会关闭该连接，此数值仅为参考值。

keepalive_timeout参数的官方说明如下：

syntax: keepalive_timeout timeout [header_timeout]; #<==参数语法

default: keepalive_timeout 75s; #<==参数默认大小

context: http, server, location #<==可以放置的标签段

参数作用：keep-alive可以使客户端到服务器端已经建立的连接一直工作不退出，当服务器有持续请求时，keep-alive会使用已经建立的连接提供服务，从而避免服务器重新建立新连接处理请求。

此参数设置一个keep-alive（客户端连接在服务器端保持多久后退出），其单位是秒，和HTTP响应header域的“Keep-Alive：timeout=time”参数有关，这些header信息也会被客户端浏览器识别并处理，不过有些客户端并不能按照服务器端的设置来处理，例如：MSIE大约60秒后会关闭keep-alive连接。细节见http://nginx.org/en/docs/http/ngx_http_core_module.html
。

2）设置参数：tcp_nodelay on；

用于激活tcp_nodelay功能，提高I/O性能。

tcp_nodelay参数的官方说明如下：

syntax: tcp_nodelay on | off; #<==参数语法

default: tcp_nodelay on; #<==参数默认大小

context: http, server, location #<==可以放置的标签段

参数作用：默认情况下当数据发送时，内核并不会马上发送，可能会等待更多的字节组成一个数据包，这样可以提高I/O性能。但是，在每次只发送很少字节的业务场景中，使用tcp_nodelay功能，等待时间会比较长。

参数生效条件：激活或禁用TCP_NODELAY选项，当一个连接进入keep-alive状态时生效。细节见http://nginx.org/en/docs/http/ngx_http_core_module.html
。

3）设置参数：client_header_timeout 15；

用于设置读取客户端请求头数据的超时时间。此处的数值为15，其单位是秒，为经验参考值。

client_header_timeout参数的官方说明如下：

syntax: client_header_timeout time; #<==参数语法

default: client_header_timeout 60s; #<==参数默认大小

context: http, server #<==可以放置的标签段

参数作用：设置读取客户端请求头数据的超时时间。如果超过这个时间，客户端还没有发送完整的header数据，那么服务器端将返回“Request time out（408）”错误，可指定一个超时时间，防止客户端利用http协议进行攻击。细节见http://nginx.org/en/docs/http/ngx_http_core_module.html
。

4）设置参数：client_body_timeout 15；

用于设置读取客户端请求主体的超时时间，默认值是60。

client_body_timeout参数的官方说明如下：

syntax: client_body_timeout time; #<==参数语法

default: client_body_timeout 60s; #<==默认值是

60秒

context: http, server, location #<==可以放置的标签段

参数作用：设置读取客户端请求主体的超时时间。这个超时仅仅为两次成功读取操作之间的一个超时，而不是请求整个主体数据的超时时间，如果在这个超时时间内，客户端没有发送任何数据，Nginx将返回“Request time out（408）”错误，默认值是60，这样效果更好。细节见http://nginx.org/en/docs/http/ngx_http_core_module.html
。

5）设置参数：send_timeout 25；

用于指定响应客户端的超时时间。这个超时仅限于两个连接活动之间的时间，如果超过这个时间，客户端没有任何活动，Nginx将会关闭连接，默认值为60秒，可以改为参考值25秒。

send_timeout参数的官方说明如下：

syntax: send_timeout time; #<==参数语法

default: send_timeout 60s; #<==默认值是

60秒

context: http, server, location #<==可以放置的标签段

参数作用：设置服务器端传送HTTP响应信息到客户端的超时时间，这个超时仅仅为两次成功握手后的一个超时，而不是请求整个响应数据的超时时间，如在这个超时时间内，客户端没有接收任何数据，那么连接将被关闭。细节见http://nginx.org/en/docs/http/ngx_http_core_module.html
。

下面画图（如图12-4所示）讲解上述几个超时参数。

图12-4　画图讲解Nginx配置超时参数图

12.2.9　上传文件大小的限制（动态应用）

下面介绍如何调整上传文件的大小（http Request body size）限制。

首先，在Nginx的主配置文件里加入如下参数：

client_max_body_size 8m;

具体大小可根据公司的业务做调整，如果不清楚就先设置为8m吧，一般情况下，HTTP的post方法在提交数据时才会携带请求主体信息。

client_max_body_size参数的官方说明如下：

syntax: client_max_body_size size;#<==参数语法

default: client_max_body_size 1m; #<==默认值是

1m
context: http, server, location #<==可以放置的标签段

参数作用：设置允许的最大客户端请求主体大小，在请求头域有“Content-Length”，如果超过了此配置值，则客户端会收到413错误，意思是请求的条目过大，有可能浏览器不能正确显示。设置为0则表示禁止检查客户端请求主体大小。此参数对提高服务器端的安全性有一定的作用。细节见http://nginx.org/en/docs/http/ngx_http_core_module.html
。

12.2.10　FastCGI相关参数调优（配合PHP引擎动态服务）

FastCGI参数是配合Nginx向后请求PHP动态引擎服务的相关参数，要想较好地理解参数细节，需要熟悉和了解Nginx FastCGI客户端配合php-fpm（FastCGI服务器端）的原理。Nginx FastCGI工作的逻辑图如图12-5所示。

图12-5　Nginx FastCGI模式工作原理图

此处讲解的参数均为Nginx FastCGI客户端向后请求PHP动态引擎服务（php-fpm（FastCGI服务器端））的相关参数，属于Nginx的配置参数。表12-1是Nginx FastCGI常见参数的说明。

表12-1　Nginx FastCGI常见参数列表说明

FastCGI Cache资料见http://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_cache
。

FastCGI常见参数的Nginx配置示例如下：

[root@nginx conf]# cat nginx.conf
worker_processes 4;
worker_cpu_affinity 0001 0010 0100 1000;
worker_rlimit_nofile 65535;
user nginx;
events {
use epoll;
worker_connections 10240;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
tcp_nodelay on;
client_header_timeout 15;
client_body_timeout 15;
send_timeout 15;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
server_tokens off;
fastcgi_connect_timeout 240;
fastcgi_send_timeout 240;
fastcgi_read_timeout 240;
fastcgi_buffer_size 64k;
fastcgi_buffers 4 64k;
fastcgi_busy_buffers_size 128k;
fastcgi_temp_file_write_size 128k;
#fastcgi_temp_path /data/ngx_fcgi_tmp;
fastcgi_cache_path /data/ngx_fcgi_cache levels=2:2 keys_zone=ngx_fcgi_cache:512m inactive=1d max_size=40g;
#web...............
server {
listen 80;
server_name blog.etiantian.org;
root html/blog;
location / {
root html/blog;
index index.php index.html index.htm;
}
location ~ .*\.(php|php5)?$
{
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi.conf;
fastcgi_cache ngx_fcgi_cache;
fastcgi_cache_valid 200 302 1h;
fastcgi_cache_valid 301 1d;
fastcgi_cache_valid any 1m;
fastcgi_cache_min_uses 1;
fastcgi_cache_use_stale error timeout invalid_header http_500;
fastcgi_cache_key http://$host$request_uri;
}
access_log logs/web_blog_access.log main;
}
upstream blog_etiantian{
server 10.0.0.8:8000 weight=1;
}
server {
listen 8000;
server_name blog.etiantian.org;
location / {
proxy_pass http://blog_etiantian;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
access_log logs/proxy_blog_access.log main;
}
}

为了让读者更加容易理解上述参数的作用及原理，我为大家画了一个原理图，如图12-6所示。

图12-6　根据HTTP原理及FastCGI原理讲解FastCGI参数优化

Nginx的FastCGI的相关参数和反向代理proxy的相关参数非常接近，大家在理解后文Nginx反向代理的参数时，可以结合此处FastCGI的参数进行对比理解。

12.2.11　配置Nginx gzip压缩实现性能优化

1.Nginx gzip压缩功能介绍

Nginx gzip压缩模块提供了压缩文件内容的功能，用户请求的内容在发送到用户客户端之前，Nginx服务器会根据一些具体的策略实施压缩，以节约网站出口带宽，同时加快数据传输效率，来提升用户访问体验。

2.Nginx gzip压缩的优点

·提升网站的用户体验：发送给用户的内容小了，用户访问单位大小的页面就加快了，用户体验提升了，网站口碑就好了。

·节约网站带宽成本：数据是压缩传输的，因此节省了网站的带宽流量成本，不过压缩时会稍微消耗一些CPU资源，这个一般可以忽略不计。

此功能既能提升用户体验，又能使公司少花钱，一举多得。对于几乎所有的Web服务来说，这是一个非常重要的功能，Apache服务也有此功能。

3.需要和不需要压缩的对象

·纯文本内容的压缩比很高，因此，纯文本的内容最好进行压缩，例如：html、js、css、xml、shtml等格式的文件。

·被压缩的纯文本文件必须要大于1KB，由于压缩算法的特殊原因，极小的文件压缩后可能反而变大。

·图片、视频（流媒体）等文件尽量不要压缩，因为这些文件大多都是经过压缩的，如果再压缩很可能不会减小或减小得很少，甚至还有可能增大，同时压缩时还会消耗大量的CPU、内存资源。

4.参数介绍及配置说明

此压缩功能与早期Apache服务的mod_deflate压缩功能很相似，Nginx的gzip压缩功能依赖于ngx_http_gzip_module模块，默认已安装。

对应的压缩参数说明如下：

#压缩配置

gzip on;
#<==开启

gzip压缩功能。

gzip_min_length 1k;
#<==设置允许压缩的最小页面字节数，页面字节数从

header头的

Content-Length中获取。默认值是

0，表示不管页面多大都进行压缩。建议设置成大于

1KB，如果小于

1KB可能会越压越大。

gzip_buffers 4 16k;
#<==压缩缓冲区大小。表示申请

4个单位为

16KB的内存作为压缩结果流缓存，默认值是申请与原始数据大小相同的内存空间来存储

gzip压缩结果。

gzip_http_version 1.1;
#<==压缩版本（默认为

1.1，前端为

squid2.5时使用

1.0），用于设置识别

HTTP协议版本，默认是

1.1，目前大部分浏览器已经支持

gzip解压，使用默认值即可。

gzip_comp_level 2;
#<==压缩比率。用来指定

gzip压缩比，

1表示压缩比最小，处理速度最快；

9表示压缩比最大，传输速度快，但处理最慢，也比较消耗

CPU资源。

gzip_types text/plain application/x-javascript text/css application/xml;
#<==用来指定压缩的类型，“

text/html”类型总是会被压缩，这个就是

HTTP原理部分所讲的媒体类型。

gzip_vary on;
#<==vary header支持。该选项可以让前端的缓存服务器缓存经过

gzip压缩的页面，例如用

Squid缓存经过

Nginx压缩的数据

完整的配置如下：

gzip on;
gzip_min_length 1k;
gzip_buffers 4 32k;
gzip_http_version 1.1;
gzip_comp_level 9;
#gzip_types text/plain application/x-javascript text/css application/xml; #<==老的不正确写法

gzip_types text/css text/xml application/javascript;
gzip_vary on;

不同的Nginx版本中，gzip_types的配置可能会有所不同，上述配置示例适合Nginx 1.6.3。对应的文件类型，请查看安装目录下的mime.types文件。

更多官方资料请看http://nginx.org/en/docs/http/ngx_http_gzip_module.html
。

5.Nginx压缩配置效果检查

可通过火狐浏览器加Yslow插件查看gzip压缩及expires缓存结果。提前安装好火狐浏览器，并且安装好Yslow插件，开启监控，然后打开LNMP时安装的博客地址，就可以看到如图12-7所示的压缩结果。

图12-7　检查Nginx gzip压缩效果图

6.重要的前端网站调试工具介绍

常见的前端网站调试工具有如下几种。

·Google浏览器（Chrome）：通过该浏览器直接按F12键即可查看压缩及缓存结果，另外，谷歌浏览器上也可以直接安装Yslow插件（如图12-8所示）。

图12-8　调试工具yslow.org网站

·火狐浏览器：在该浏览器上安装firebug、Yslow（http://yslow.org/
），即可进行调试（如图12-9所示）。

·IE浏览器：在该浏览器上安装httpwatch即可进行调试（省略）。

图12-9　调试工具Yslow安装方法图

12.2.12　配置Nginx expires缓存实现性能优化

1.Nginx expires功能介绍

简单地说，Nginx expires的功能就是为用户访问的网站内容设定一个过期时间，当用户第一次访问这些内容时，会把这些内容存储在用户浏览器本地，这样用户第二次及以后继续访问该网站时，浏览器会检查加载已经缓存在用户浏览器本地的内容，就不会再去服务器下载了，直到缓存的内容过期或被清除为止。

更深入地理解：expires的功能就是允许通过Nginx配置文件控制HTTP的"Expires"和"Cache-Control"响应头部内容，告诉客户端浏览器是否缓存和缓存多长时间以内访问的内容。这个expires模块控制Nginx服务器应答时的expires头内容和Cache-Control头的max-age指令。缓存的有效期可以设置为相对于源文件的最后修改时刻或客户端的访问时刻。

这些HTTP头向客户端表明了内容的有效性和持久性。如果客户端本地有内容缓存，则内容就可以从缓存（除非已经过期）而不是从服务器中读取，然后客户端会检查缓存中的副本，看其是否过期或失效，以决定是否重新从服务器中获得内容更新。

2.Nginx expires作用介绍

在网站的开发和运营中，视频、图片、CSS、JS等网站元素的更改机会较少，特别是图片，这时可以将图片设置在客户浏览器本地缓存365天或3650天，而将CSS、JS、html等代码缓存10~30天。这样用户第一次打开页面后，会在本地的浏览器中按照过期日期缓存相应的内容，下次用户再打开类似的页面时，重复的元素就无需下载了，从而加快了用户访问速度。用户的访问请求和数据减少了，也可以节省大量的服务器端带宽。此功能同Apache的expires功能类似。

3.Nginx expires功能优点

·expires可以降低网站的带宽，节约成本。

·加快用户访问网站的速度，提升用户的访问体验。

·服务器访问量降低了，服务器压力就减轻了，服务器的成本也会降低，甚至还可以节约人力成本。

对于几乎所有的Web服务来说，这是非常重要的功能之一，Apache服务也有此功能。

4.Nginx expires配置详解

前面已经介绍了expires的功能原理，接下来就来配置Nginx expires的功能。这里以location标签为例进行讲解，通过location URI规则将需要缓存的扩展名罗列出来，然后指定缓存时间。如果针对所有内容设置缓存，也可以不用location。Nginx默认安装了expires功能。

1）根据文件扩展名进行判断，添加expires功能范例

范例1：

location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
{
expires 3650d;
}

该范例的意思是当用户访问网站URL结尾的文件扩展名为上述指定类型的图片时，设置缓存为3650天，即10年。

范例2：

location ~ .*\.(js|css)?$
{
expires 30d;
}

该范例的意思是当用户访问网站URL结尾的文件扩展名为js、css类型的元素时，设置缓存为30天，即1个月。

上述两个范例的实际配置结果如下：

###www
server {
listen 80;
server_name www.etiantian.org;
location / {
root html/www;
index index.html index.htm;
}
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
{
expires 10y;
root html/www;
}
location ~ .*\.(js|css)?$
{
expires 30d;
}
access_log /app/logs/www_access.log main;
}

需要特别注意的是，location内容一般要放到虚拟主机配置中，即server标签中。

2）根据URI中的路径（目录）进行判断，添加expires功能范例

范例3：

## Add expires header according to URI(path or dir).
location ~ ^/(images|javascript|js|css|flash|media|static)/ {
expires 360d;
}

该范例的意思是当用户访问网站URL中包含上述的路径（例：images、js、css，这些在服务器端是程序目录）时，将访问的内容设置缓存为360天，即1年。

3）单个文件添加expires功能的范例

下面的命令会给robots.txt机器人文件设置过期时间（这里为7天），在这7天内并不记录404错误日志。

location ~(robots.txt) {
expires 7d;
break;
}

5.Nginx expires配置效果检查

检查Nginx expires的方法和检查Nginx gzip的方法相同。

通过火狐浏览器加Yslow插件查看gzip压缩及expires缓存结果时，要提前安装好火狐浏览器，并且要安装好Yslow插件，开启监控，然后打开LNMP时安装的博客地址（带有图片、JS、CSS），就可以看到如图12-10所示的缓存结果了。

图12-10　查看expires是否生效图

在Linux客户端可通过如下curl命令查看图片URL缓存的header信息：

[root@oldboy ~]# curl -I http://blog.etiantian.org/23.jpg
HTTP/1.1 200 OK
Server: nginx
Date: Fri, 21 Sep 2012 02:07:18 GMT
Content-Type: image/jpeg
Content-Length: 59491
Last-Modified: Mon, 13 Aug 2012 02:26:18 GMT
Connection: keep-alive
ETag: "502865ca-e863"
Expires: Mon, 19 Sep 2022 02:07:18 GMT #<==缓存的过期时间

Cache-Control: max-age=315360000 #<==缓存的总时间，单位为秒

Accept-Ranges: bytes

6.Nginx expires功能的缺点及解决方法

几乎所有的事物都有两面性，没有十全十美的人和事。Nginx expires功能也不例外，虽然这个功能很好，但是也会给企业带来一些困惑。

当网站被缓存的页面或数据更新了，此时用户端看到的可能还是旧的已经缓存的内容，这样就会影响用户的体验，那么如何解决这个问题呢？解决办法如下。

第一，对于经常发生变动的图片等文件，可以缩短对象缓存时间，例如：谷歌和百度首页的图片经常会根据不同的日期换成一些节日的图片，所以这里可以将这个图片的缓存期设置为1天。

第二，当网站改版或更新时，可以在服务器中将缓存的对象改名（网站代码程序）。

·对于网站的图片、附件，一般不会被用户直接修改，用户层面上的修改图片，实际上是重新传到服务器，虽然内容一样但是是一个新的图片名了。

·网站改版升级会修改JS、CSS元素，若改版时对这些元素名进行了修改，则会使得前端的CDN及用户端需要重新缓存内容。

7.企业网站缓存日期曾经的案例参考

若企业的业务和网站的访问量不同，那么网站缓存期的时间设置也是不同的，比如，如下企业所用的缓存日期就是不一样的。

·51CTO：1周

·新浪：15天

·京东：25年

·淘宝：10年

8.企业网站有可能不希望被缓存的内容

·广告图片，用于广告服务，都缓存了就不好控制展示了。

·网站流量统计工具（JS代码），都缓存了流量统计就不准了。

·更新很频繁的文件（Google的logo），如果按天缓存，效果还是显著的。

12.3　Nginx日志相关的优化与安全

12.3.1　编写脚本实现Nginx access日志轮询

当用户请求一个软件时，绝大多数软件都会记录用户的访问情况，Nginx服务也不例外。Nginx软件目前还没有类似Apache的通过cronolog或rotatelog对日志进行分割处理的功能，但是，运维人员可以利用脚本开发、Nginx的信号控制功能或reload重新加载，来实现日志的自动切割、轮询。

详细操作过程如下。

（1）配置日志切割脚本，命令如下：

[root@oldboy ~]# mkdir /server/scripts/ -p
[root@oldboy ~]# cd /server/scripts/
[root@oldboy scripts]# vim cut_nginx_log.sh
cd /application/nginx/logs &&\
/bin/mv www_access.log www_access_$(date +%F -d -1day).log #<==将日志按日期改成前一天的名称

/application/nginx/sbin/nginx -s reload #<==重新加载

nginx使得触发重新生成访问

日志文件

提示：实际上脚本的功能很简单，就是改日志名，然后加载

Nginx，重新生成文件记录日志

（2）将这段脚本保存后加入到服务器端的定时任务配置里，使得此脚本在每天的凌晨0点执行，就可以实现日志每天的分割功能了，操作结果如下：

[root@oldboy scripts]# crontab -e #<==打开编辑功能后，加入如下内容

#cut nginx access log by oldboy at 201409
00 00 * * * /bin/sh /server/scripts/cut_nginx_log.sh >/dev/null 2>&1

此处的意思是每天凌晨0点执行后面的/server/scripts/cut_nginx_log.sh脚本，>/dev/null 2>&1表示任何输出都不要。这个是Linux的基础服务之一，具体请参考相关资料。

最终切割后的日志效果如下：

[root@oldboy scripts]# ll /application/nginx/logs/总用量

27752
-rw-r--r--. 1 root root 20241716 10月

9 12:03 access.log
-rw-r--r--. 1 root root 27428 10月

10 04:22 error.log
-rw-r--r--. 1 root root 5 10月

9 09:56 nginx.pid
-rw-r--r--. 1 root root 264 9月

26 05:08 www_access_2014-09-25.log
-rw-r--r--. 1 root root 0 9月

26 05:12 www_access_2014-09-26.log
-rw-r--r--. 1 root root 0 9月

28 00:00 www_access_2014-09-27.log
-rw-r--r--. 1 root root 48717 9月

28 03:11 www_access_2014-09-28.log
-rw-r--r--. 1 root root 8072741 10月

9 17:25 www_access_2014-10-09.log

说明

这里是按照不同的日期生成日志。

12.3.2　不记录不需要的访问日志

在实际工作中，对于负载均衡器健康节点的检查或某些特定文件（比如图片、JS、CSS）的日志，一般不需要记录下来，因为在统计PV时是按照页面进行计算的，而且日志如果写入太频繁会消耗大量的磁盘I/O，降低服务的性能。

具体配置方法如下：

location ~ .*\.(js|jpg|JPG|jpeg|JPEG|css|bmp|gif|GIF)$ {
access_log off;
}

这里用location标签匹配不记录日志的元素扩展名，然后关掉日志。

12.3.3　访问日志的权限设置

假如日志目录为/app/logs，则授权方法如下：

chown -R root.root /app/logs
chmod -R 700 /app/logs

不需要在日志目录上给Nginx用户读或写的许可，但很多网友都没有注意这个问题，他们把该权限直接给了Nginx或Apache用户，这就造成了安全隐患。

12.4　Nginx站点目录及文件URL访问控制

12.4.1　根据扩展名限制程序和文件访问

Web 2.0时代，绝大多数网站都是以用户为中心的，例如：bbs、blog、sns产品，这几个产品都有一个共同特点，那就是不但允许用户发布内容到服务器，还允许用户发图片甚至上传附件到服务器上，由于为用户开通了上传的功能，因此给服务器带来了很大的安全风险。虽然很多程序在上传前会对文件做一定的控制，例如：文件大小、类型等，但是，一不小心就会被黑客钻了空子，上传了木马程序。

下面将利用Nginx配置禁止访问上传资源目录下的PHP、Shell、Perl、Python程序文件，这样用户即使上传了木马文件也没法执行，从而加强了网站的安全。

范例1：配置Nginx，禁止解析指定目录下的指定程序。

location ~ ^/images/.*\.(php|php5|sh|pl|py)$
{
deny all;
}
location ~ ^/static/.*\.(php|php5|sh|pl|py)$
{
deny all;
}
location ~* ^/data/(attachment|avatar)/.*\.(php|php5)$
{
deny all;
}

对上述目录的限制必须写在Nginx处理PHP服务配置的前面，如下：

location ~ .*\.(php|php5)?$
{
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fcgi.conf;
}

范例2：Nginx下配置禁止访问*.txt和*.doc文件。

实际配置信息如下：

location ~* \.(txt|doc)$ {
if (-f $request_filename) {
root /data/www/www;
#rewrite …

..可以重定向到某个

URL
break;
}
}
location ~* \.(txt|doc)${
root /data/www/www;
denyall;
}

12.4.2　禁止访问指定目录下的所有文件和目录

范例1：配置禁止访问指定的单个或多个目录。

禁止访问单个目录的命令如下：

location ~ ^/(static)/ {
deny all;
}
location ~ ^/static {
deny all;
}

禁止访问多个目录的命令如下：

location ~ ^/(static|js) {
deny all;
}

范例2：禁止访问目录并返回指定的HTTP状态码，命令如下。

server {
listen 80;
server_name www.etiantian.org etiantian.org;
root /data0/www/www;
index index.html index.htm;
access_log /app/logs/www_access.log commonlog;
location /admin/ { return 404; }
location /templates/ { return 403; }
}

作用：禁止访问目录下的指定文件，或者禁止访问指定目录下的所有内容。

最佳应用场景：对于集群的共享存储，一般是存放静态资源文件，所以可以禁止执行指定扩展名的程序，例如：.php、.sh、.pl、.py。

12.4.3　限制网站来源IP访问

下面介绍如何使用ngx_http_access_module限制网站来源IP访问。

案例环境：phpmyadmin数据库的Web客户端，内部开发人员所用的。

范例1：禁止外界访问某目录，但允许某IP访问该目录，且支持PHP解析，命令如下。

location ~ ^/oldboy/ {
allow 202.111.12.211;
deny all;
}
location ~ .*\.(php|php5)?$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}

范例2：限制指定IP或IP段访问，命令如下。

location / {
deny 192.168.1.1;
allow 192.168.1.0/24;
allow 10.1.1.0/16;
deny all;
}

参考：http://nginx.org/en/docs/http/ngx_http_access_module.html
。

企业问题案例：Nginx做反向代理的时候可以限制客户端IP吗？

解答：可以，具体方法如下。

方法1：使用if来控制，命令如下。

if ( $remote_addr = 10.0.0.7 ) {
return 403;
}
if ( $remote_addr = 218.247.17.130 ) {
set $allow_access_root 'true';
}
http://nginx.org/en/docs/varindex.html

方法2：利用deny和allow只允许IP访问，命令如下。

location / {
root html/blog;
index index.php index.html index.htm;
allow 10.0.0.7;
deny all;
}

方法3：只拒绝某些IP访问，命令如下。

location / {
root html/blog;
index index.php index.html index.htm;
deny 10.0.0.7;
allow all;
}

注意事项

·deny一定要加一个IP，否则会直接跳转到403，不再往下执行了，如果403默认页是在同一域名下，那么会造成死循环访问。

·对于allow的IP段，将允许访问的段位从小到大排列，如127.0.0.0/24的下面才能是10.10.0.0/16，其中：

·24表示子网掩码：255.255.255.0。

·16表示子网掩码：255.255.0.0。

·8表示子网掩码：255.0.0.0。

·以“deny all；”结尾，表示除了上面允许的，其他的都禁止。如：

deny 192.168.1.1;
allow 127.0.0.0/24;
allow 192.168.0.0/16;
allow 10.10.0.0/16;
denyall;

12.4.4　配置Nginx，禁止非法域名解析访问企业网站

问题：Nginx如何防止用户IP访问网站（恶意域名解析，也相当于是直接IP访问企业网站）？

方法1：让使用IP访问网站的用户，或者恶意解析域名的用户，收到501错误，命令如下。

server {
listen 80 default_server;
server_name _;
return 501;
}

说明

直接报501错误，从用户体验上不是很好。

方法2：通过301跳转到主页，命令如下。

server {
listen 80 default_server;
server_name _;
rewrite ^(.*) http://blog.etiantian.org/$1 permanent;
}

方法3：发现某域名恶意解析到公司的服务器IP，在server标签里添加以下代码即可，若有多个server则要多处添加。

if ($host !~ ^www/.eduoldboy/.com$){
rewrite ^(.*) http://www.eduoldboy.com$1 permanent;
}

上面代码的意思是如果header信息的host主机名字段非www.eduoldboy.com
，就301跳转到www.eduoldboy.com
。

12.5　Nginx图片及目录防盗链解决方案

1.什么是资源盗链

简单地说，就是某些不法网站未经许可，通过在其自身网站程序里非法调用其他网站的资源，然后在自己的网站上显示这些调用的资源，达到填充自身网站的效果。这一举动不仅浪费了调用资源网站的网络流量，还造成其他网站的带宽及服务压力吃紧，甚至宕机。

下面通过示意图阐述资源盗链的原理，如图12-11所示。

2.网站资源被盗链带来的问题

若网站图片及相关资源被盗链，最直接的影响就是网络带宽占用加大了，带宽费用多了，网络流量也可能忽高忽低，Nagios/Zabbix等报警服务频繁报警，类似图12-12所示。

最严重的情况就是网站的资源被非法使用，使网站带宽成本加大和服务器压力加大，这有可能会导致数万元的损失，且网站的正常用户访问也会受到影响。

图12-11　资源盗链原理的详细示意图

图12-12　因盗链导致的流量飙升图

3.企业真实案例：网站资源被盗链，出现严重问题

某日，我接到从事运维工作的朋友的紧急求助，其公司的CDN源站的流量没有变动，但CDN加速那边的流量无故超出了好几个GB，不知道该怎么处理。

该故障的影响：由于是购买的CDN网站加速服务，因此虽然流量多了几个GB，但是业务未受影响。只是，这么大的异常流量，持续下去可直接导致公司无故损失数万元。解决这个问题可体现运维的价值。

那么该如何及时发现，又如何处理这样的问题呢？

本节先给大家讲述几个发现问题的方法，如何处理盗链，后文会详细讲解。

第一，对IDC及CDN带宽做监控报警。

第二，作为高级运维或运维经理，每天上班的重要任务，就是经常查看网站流量图，关注流量变化，关注异常流量。

第三，对访问日志做分析，迅速定位异常流量，并且和公司市场推广等保持较好的沟通，以便调度带宽和服务器资源，确保网站正常的访问体验。

更多企业案例及实战解决方案请参见：http://oldboy.blog.51cto.com/2561410/909696
。

4.常见防盗链解决方案的基本原理

1）根据HTTP referer实现防盗链

在HTTP协议中，有一个表头字段叫referer，可使用URL格式来表示是哪里的链接用了当前网页的资源。通过referer可以检测访问的来源网页，如果是资源文件，可以跟踪到显示它的网页地址，一旦检测出来源不是本站，马上进行阻止或返回指定的页面。

HTTP referer是header的一部分，当浏览器向Web服务器发送请求时，一般会带上referer，告诉服务器我是从哪个页面链接过来的，服务器借此获得一些信息用于处理。Apache、Nginx、Lighttpd三者都支持根据HTTP referer实现防盗链，referer是目前网站图片、附件、html等最常用的防盗链手段。图12-13是referer防盗链的基本原理图。

图12-13　通过referer解决盗链问题的基本原理图

2）根据Cookie防盗链

对于一些特殊的业务数据，例如流媒体应用通过ActiveX显示的内容（例如，Flash、Windows Media视频、流媒体的RTSP协议等），因为它们不向服务器提供referer header，所以若采用上述的referer防盗链手段，将会达不到想要的效果。

对于Flash、Windows Media视频这种占用流量较大的业务数据，防盗链是比较困难的，此时可以采用Cookie技术，解决Flash、Windows Media视频等的防盗链问题。

例如：ActiveX插件不传递referer，但会传递Cookie，可以在显示ActiveX的页面的<head></head>标签内嵌入一段JavaScript代码，设置“Cookie：Cache=av”，代码如下：

<script> document.cookie="Cache=av;domain=domain.com;path=/"; </script>

然后就可以通过各种手段来判断这个Cookie是否存在，以及验证其值的操作了。

根据Cookie来防盗链的技术并非本章所要讲的内容，读者了解一下即可，如果企业确实有需要，可以阅读其他书籍或进入交流群以获取这部分的知识。

3）通过加密变换访问路径以实现防盗链

此种方法比较适合视频及下载类业务数据的网站。例如：Lighttpd有类似的插件mod_secdownload来实现此功能。先在服务器端配置此模块，设置一个固定的用于加密的字符串，比如oldboy，然后设置一个url前缀，比如/mp4/，再设置一个过期时间，比如1小时，然后写一段PHP代码，利用加密字符串和系统时间等通过md5算法生成一个加密字符串。最终获取到的文件的URL链接中会带有一个时间戳和一个加密字符的md5数值，在访问时系统会对这两个数据进行验证。如果时间不在预期的时间段内（如1小时内）则失效；如果时间戳符合条件，但是加密的字符串不符合条件也会失效，从而达到防盗链的效果。

PHP代码实例如下：

<?php
$secret = "oldboy"; // 加密字符串，必须和

lighttpd.conf里的保持一致

$uri_prefix = "/mp4/"; // 虚拟的路径、前缀，必须和

lighttpd.conf里的保持一致

$file = "/test.mp4"; // 实际文件名，必须加

"/"（斜杠）

$timestamp = time(); // current timestamp
$t_hex = sprintf("%08x", $timestamp);
$m = md5($secret.$file.$t_hex);
printf('%s', $uri_prefix, $m, $t_hex, $file, $file); //生成

url地址串

?>

根据Lighttpd的插件mod_secdownload来进行防盗链的技术并非本章所要讲的内容，读者了解一下即可，如果企业确实有需要，可以阅读其他书籍，或者进入交流群以获取这部分的知识。

5.Nginx Web服务实现防盗链实战

在默认情况下，只需要进行简单的配置，即可实现防盗链处理。请看下面的实例。

1）利用referer，并且针对扩展名rewrite重定向

下面的代码为利用referer且针对扩展名rewrite重定向，即实现防盗链的Nginx配置：

location ~* \.(jpg|gif|png|swf|flv|wma|wmv|asf|mp3|mmf|zip|rar)$ {
valid_referers none blocked *.etiantian.org etiantian.org;
if ($invalid_referer) {
rewrite ^/ http://www.etiantian.org/img/nolink.jpg;
}
}

提示

要根据自己公司的实际业务（是否有外链的合作），进行域名设置。

设置expires的方法如下：

[root@oldboy bbs]# cat /application/nginx/conf/extra/www.conf
server {
listen 80;
server_name www.etiantian.org;
root html/www;
index index.html index.htm;
access_log logs/www_access.log main;
#Preventing hot linking of images and other file types
location ~* ^.+\.(gif|jpg|png|swf|flv|rar|zip)$ {
valid_referers none blocked server_names *.etiantian.org etiantian.org;
if ($invalid_referer) {
rewrite ^/ http://bbs.etiantian.com/img/nolink.jpg;
}
access_log off;
root html/www;
expires 1d;
break;
}
}

2）利用referer，并且针对站点目录过滤返回错误码

针对目录的方法如下：

location /images {
root /data0/www/www;
valid_referers none blocked *.etiantian.org etiantian.org;
if ($invalid_referer){
return 403;
}
}

在上面这段防盗链设置中，分别针对不同的文件类型和不同的目录进行了设置，读者可以根据自己的需求进行类似设定。下面是上述代码的说明：

·“jpg|gif|png|swf|flv|wma|wmv|asf|mp3|mmf|zip|rar”表示对以.jpg、.gif、.png、.swf、.flv、.wma、.wmv、.asf、.mp3、.mmf、.zip和.rar为后缀的文件实行防盗链处理。

·“*.etiantian.org etiantian.org”表示这个请求可以正常访问上面指定的文件资源。

·if{}中内容的意思是如果地址不是上面指定的地址就跳转到通过rewrite指定的地址，也可以直接通过return返回403错误。

·return 403为自定义的http返回状态码。

·“rewrite^/http://www.etiantian.org/img/nolink.jpg
；”表示显示一张防盗链图片。

·“access_log off；”表示不记录访问日志，减轻压力。

·expires 3d指的是对所有文件设置3天的浏览器缓存。

6.NginxHttpAccessKeyModule实现防盗链介绍

如果不怕麻烦，有条件实现的话，推荐使用NginxHttpAccessKeyModule。

其运行方式是：如果download目录下有一个file.zip的文件。对应的URI是http://www.abc.com/download/file.zip
，使用ngx_http_accesskey_module模块后就成了http://www.abc.com/download/file.zip?key=09093abeac094
，只有正确地给定了key值，才能下载download目录下的file.zip，而且key值是与用户的IP相关的，这样就可以避免被盗链了。据说，现在NginxHttpAccessKeyModule连迅雷都可以防了，读者可以尝试一下。

7.在产品设计上解决盗链方案

产品在设计时，处理盗链问题可以将计就计，为网站上传的图片增加水印。例如，图12-14就是为网站上传的图片增加的水印。

图12-14　网站被盗链后的提示图片示例

为图片添加版权水印是很有效的方法。网站直接转载图片一般是为了快捷，但是对于有水印的图片，很多站长是不愿意转载的。

8.Nginx防盗链花絮

下面实战模拟演示盗链。

（1）假定blog.etiantian.com是非法盗链的网站域名，先编写如下的oldboy.html程序：

<html>
<head>
<title>老男孩教育

</title>
</head>
<body bgcolor=green>老男孩的博客！

<br>我的博客是

<a href="http://oldboy.blog.51cto.com" target="_blank">博客地址

</a>
<img src="http://www.etiantian.org/stu.jpg">
</body>
</html>

这个非法盗链的访问地址是http://blog.etiantian.com/oldboy.html
，网站里会加载www.etiantian.org
网站的图片stu.jpg，即盗用该网站图片并消耗了www.etiantian.org
正常网站的带宽。

（2）假定我们维护的网站为www.etiantian.org
，设定一张存在的图片的地址为www.etiantian.org/stu.jpg
，此时发现被blog.etiantian.com
盗链了。通过查看流量，分析日志查看referfer，就可以看到被盗链的具体情况。

下面是查看Web用户的http_referer的实战演示。

Nginx日志格式为www.etiantian.org
，其内容如下：

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

当盗链的网站blog.etiantian.com
访问我们的站点时，记录的日志如下：

[root@nginx conf]# tail -f ../logs/www_access.log
10.0.0.125 - - [10/Mar/2015:19:34:15 +0800] "GET /stu.jpg HTTP/1.1" 200 68080 "http://blog.etiantian.com/oldboy.html" "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0" "-"

我们自己正常访问用户网站时，站点记录的日志如下：

10.0.0.125 - - [10/Mar/2015:19:37:45 +0800] "GET /img/nolink.jpg HTTP/1.1" 304 0 "-" "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.3)" "-"

对比可知，盗链的网站多出了一个http_referer的信息，即“http://blog.etiantian.com/oldboy.html
”，表示使用我们网站资源的用户来自于该网站，也就是该站点盗链了网站www.etiantian.org
资源。

可在www.etiantian.org网站下设置防盗链，Nginx的方法如下：

#Preventing hot linking of images and other file types
location ~* ^.+\.(jpg|png|swf|flv|rar|zip)$ {
valid_referers none blocked *.etiantian.org etiantian.org;
if ($invalid_referer) {
rewrite ^/ http://bbs.etiantian.org/img/nolink.gif;
}
root html/www;
}

提示

如果referers不是“*.etiantian.org etiantian.org；”，给它一个http://bbs.etiantian.org/img/nolink.gif
。

此外，给盗链网站显示的图片域名不要和被盗链的网站域名一样。也就是http://bbs.etiantian.org/img/nolink.gif
里面的bbs.etiantian.org不能是www.etiantian.org
。

（3）单独搭建一个虚拟机主机bbs.etiantian.org，指定盗链后展示的图片为http://bbs.etiantian.org/img/nolink.gif
。

上面演示的盗链操作步骤汇总如下：

（1）将www.etiantian.org/stu.jpg
指定被盗链的图片地址。

（2）将http://bbs.etiantian.org/img/nolink.gif
指定盗链后展示的图片。

（3）输入非法盗链网站提供给用户的URL，为http://blog.etiantian.com/oldboy.html
。

在没有设置盗链之前，显示www.etiantian.org/stu.jpg
，设置盗链之后，显示http://bbs.etiantian.org/img/nolink.gif
对应的图片，但地址还是盗链的地址。

12.6　Nginx错误页面的优雅显示

12.6.1　生产环境中常见的HTTP状态码列表

企业生产环境中常见的HTTP状态码列表如表12-2所示。

表12-2　常见的HTTP状态码列表

参考资料请见http://oldboy.blog.51cto.com/2561410/716294
。

12.6.2　为什么要配置错误页面优雅显示

在网站的运行过程中，可能因为页面不存在或系统过载等原因，导致网站无法正常响应用户的请求，此时Web服务会返回系统默认的错误码，或者很不友好的页面（如图12-15所示）。

图12-15　访问不到内容时出现的404界面

我们可以将404、403等的错误信息页面重定向到网站首页或其他事先指定的页面，以提升网站的用户访问体验。

例如：老男孩的博客所在的网站，在这方面的处理就不是很严谨，给了用户不好的页面体验。

范例1：对错误代码403实行本地页面跳转，命令如下。

###www
server {
listen 80;
server_name www.etiantian.org;
location / {
root html/www;
index index.html index.htm;
}
error_page 403 /403.html; #<==当出现

403错误时，会跳转到

403.html页面

}

上面的/403.html是相对于站点根目录html/www的。

范例2：对错误代码404实行本地页面优雅显示，命令如下。

server {
listen 80;
server_name www.oldboyedu.com;
location / {
root html/www;
index index.html index.htm;
error_page 404 /404.html;
#<==当出现

404错误时，会跳转到

404.html页面

access_log /app/logs/bbs_access.log commonlog;
}
}

代码中的/404.html是相对于站点根目录html/www的。

范例3：50x页面放到本地单独目录下，进行优雅显示。

# redirect server error pages to the static page /50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /data0/www/html;
}

这里指定单独的站点目录存放到50x.html文件中。

范例4：改变状态码为新的状态码，并显示指定的文件内容，命令如下。

error_page 404 =200 /empty.gif;
server {
listen 80;
server_name www.linuxpeixun.com;
location / {
root /data0/www/bbs;
index index.html index.htm;
fastcgi_intercept_errors on;
error_page 404 =200 /ta.jpg;
access_log /app/logs/bbs_access.log commonlog;
}
}

范例5：错误状态码URL重定向，命令如下。

server {
listen 80;
server_name www.oldboyedu.com;
location / {
root html/www;
index index.html index.htm;
error_page 404 http://oldboy.blog.51cto.com;
#<==当出现

404错误时，会跳转到指定的

URL http://oldboy.blog.51cto.com页面显示

给用户，这个

URL一般是企业另外的可用地址

access_log /app/logs/bbs_access.log commonlog;
}
}

代码中的/404.html是相对于站点根目录html/www的。

范例6：将错误状态码重定向到一个location，命令如下。

location / {
error_page 404 = @fallback;
}
location @fallback {
proxy_pass http://backend;
}

官方说明如下：

syntax: error_page code ... [=[response]] uri;
default: —

context: http, server, location, if in location
Defines the URI that will be shown for the specified errors. These directives are inherited from the previous level if and only if there are noerror_pagedirectives on the current level. A uri value can contain variables.
Example:
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
Furthermore, it is possible to change the response code to another using the "=code" syntax, for example:
error_page 404 =200 /empty.gif;
If an error response is processed by a proxied server, or a FastCGI server, and the server may return different response codes (e.g., 200, 302, 401 or 404), it is possible to respond with a returned code:
error_page 404 = /404.php;
It is also possible to use redirects for error processing:
error_page 403 http://example.com/forbidden.html;
error_page 404 =301 http://example.com/notfound.html;
In this case, the response code 302 is returned to the client. It can only be changed to one of the redirect status codes (301, 302, 303 and 307).
If there is no need to change URI during internal redirection it is possible to pass error processing into a named location:
location / {
error_page 404 = @fallback;
}
location @fallback {
proxy_pass http://backend;
}
If the uri processing led to an error, the HTTP status code indicating last occurred problem will be returned.

阿里门户网站天猫的Nginx优雅显示配置案例如下：

error_page 500 501 502 503 504 http://err.tmall.com/error2.html;
error_page 400 403 404 405 408 410 411 412 413 414 415 http://err.tmall.com/error1.html;

12.7　Nginx站点目录文件及目录权限优化

1.单机LNMP环境目录权限严格控制措施

为了保证网站不遭受木马入侵，所有站点目录的用户和组都应该为root，所有的目录权限都是755；所有的文件权限都是644。设置如下：

[root@www ~]# ls -l /var/html/blog/|tail -5
-rw-r--r-- 1 root root 7712 5月

2 2012 wp-mail.php
-rw-r--r-- 1 root root 9916 4月

27 2012 wp-settings.php
-rw-r--r-- 1 root root 18299 4月

21 2012 wp-signup.php
-rw-r--r-- 1 root root 3700 1月

9 2012 wp-trackback.php
-rw-r--r-- 1 root root 2788 2月

17 2012 xmlrpc.php

以上的权限设置可以防止黑客上传木马，以及修改站点文件，但是，合理的网站用户上传的内容也会被拒之门外。那么如何让合法的用户可以上传文件，而又不至于被黑客利用攻击呢？

如果是单机的LNMP环境，那么站点目录和文件属性的设置如下。

先把所有的目录权限设置为755，所有的文件权限设置为644，所用目录和文件的用户和组都是root；然后把用户上传资源的目录权限设置为755，将用户和组设置为Nginx服务的用户；最后针对上传资源的目录做资源访问限制（前文已述）。

部分公司所采用的授权方式不是很安全，常见的有如下两种：

·chmod-R 777/sitedir

·chown-R nginx.nginx/sitedir

上述两种授权方法虽然不能说是错误的，但是没有做到授权最小化，会给网站带来非常大的安全隐患，特别是木马入侵的时候。

在比较好的网站业务架构中，应把资源文件，包括用户上传的图片、附件等服务和程序服务分离，最好把上传程序服务也分离出来，这样就可以从容地按照上文所述的方法进行安全授权了。

2.Nginx企业网站集群超级安全设置

结合Linux权限体系及Nginx大型集群架构进行配置，严格控制针对Nginx目录的访问才能降低网站被入侵的风险。比如，可根据图12-16中的企业集群架构逻辑图和不同角色提供的不同服务来严格控制不同服务器的Nginx目录权限。

图12-16　专业的Nginx集群架构角色逻辑图

表12-3为集群架构中不同于前面Web业务的权限管理细化。

表12-3　集群架构中不同角色的授权具体思路说明

做到上述的设置后，网站服务在系统层面被入侵的风险就大大降低了。

12.8　Nginx防爬虫优化

1.Robots.txt机器人协议介绍

Robots协议（也称为爬虫协议、机器人协议等）的全称是“网络爬虫排除标准”（Robots Exclusion Protocol），网站通过Robots协议告诉搜索引擎哪些页面可以抓取，哪些页面不能抓取。有关robots.txt的配置，本书不会详细讲解。

2.机器人协议八卦

2011年10月25日，京东商城正式将一淘网的搜索爬虫屏蔽，以防止一淘网对其内容进行抓取，具体措施如图12-17所示。

2008年9月8日，淘宝网宣布封杀百度爬虫，百度忍痛遵守爬虫协议。因为一旦破坏协议，用户的隐私和利益就无法得到保障，搜索网站就谈不上人性关怀。

淘宝的robots.txt设置如下：

http://www.taobao.com/robots.txt
User-agent: Baiduspider
Disallow: /
User-agent: baiduspider
Disallow: /

图12-18为taobao.com robots.txt设置情况。

2012年8月，360综合搜索被指违反Robots协议（如图12-19所示）。

3.Nginx防爬虫优化配置

我们可以根据客户端的user-agents信息，轻松地阻止指定的爬虫爬取我们的网站。下面来看几个案例。

图12-17　jd.com robots.txt设置情况

图12-18　taobao.com robots.txt设置情况

图12-19　360.cn robots.txt设置情况

范例1：阻止下载协议代理，命令如下。

## Block download agents ##
if ($http_user_agent ~* LWP::Simple|BBBike|wget) {
return 403;
}

说明

如果用户匹配了if后面的客户端（例如wget），就返回403。

这里根据$http_user_agent获取客户端agent，然后判断是否允许或返回指定错误码。

范例2：添加内容防止N多爬虫代理访问网站，命令如下。

这些爬虫代理使用“|”进行分隔，具体要处理的爬虫可以根据需求增加或减少，添加的内容如下：

if ($http_user_agent ~*
"qihoobot|Baiduspider|Googlebot|Googlebot-Mobile|Googlebot-Image|Mediapartners-Google|Adsbot-Google|Yahoo! Slurp China|YoudaoBot|Sosospider|Sogou spider|Sogou web spider|MSNBot") {
return 403;
}

范例3：测试禁止不同的浏览器软件访问。

示例代码如下：

if ($http_user_agent ~* "Firefox|MSIE")
{
rewrite ^(.*) http://blog.etiantian.org/$1 permanent;
}

如果浏览器为Firefox或IE，就会跳转到http://blog.etiantian.org
。

12.9　利用Nginx限制HTTP的请求方法

最常用的HTTP方法为GET、POST，我们可以通过Nginx限制HTTP请求的方法来达到提升服务器安全的目的，例如，让HTTP只能使用GET、HEAD和POST方法的配置如下：

#Only allow these request methods
if ($request_method !~ ^(GET|HEAD|POST)$ ) {
return 501;
}
#Do not accept DELETE, SEARCH and other methods

12.7节中提到过，当上传服务器将数据上传到存储服务器时，用户上传写入的目录就不得不给Nginx对应的用户赋予相关权限，这样一旦程序出现漏洞，木马就有可能被上传到服务器挂载的对应存储服务器的目录里，虽然我们也做了禁止PHP、SH、PL、PY等扩展名的解析限制，但还是会遗漏一些意想不到的可执行文件。对于这种情况，该怎么办呢？事实上，还可以通过限制上传服务器的Web服务（可以具体到文件）使用GET方法，防止用户通过上传服务器访问存储内容，让访问存储渠道只能从静态或图片服务器入口进入。例如，在上传服务器上限制HTTP的GET方法的配置如下：

## Only allow GET request methods ##
if ($request_method ~* ^(GET)$ ) {
return 501;
}

提示

还可以加一层location，更具体地限制文件名。

实际效果如图12-20所示。

图12-20　禁止HTTP GET方法的实际访问效果

12.10　使用CDN做网站内容加速

12.10.1　什么是CDN

CDN的全称是Content Delivery Network，中文意思是内容分发网络。简单地讲，通过在现有的Internet中增加一层新的网络架构，将网站的内容发布到最接近用户的Cache服务器内，通过智能DNS负载均衡技术，判断用户的来源，让用户就近使用与服务器相同线路的带宽访问Cache服务器，取得所需的内容。例如：天津网通用户访问天津网通Cache服务器上的内容，北京电信访问北京电信Cache服务器上的内容。这样可以有效减少数据在网络上传输的时间，从提高访问速度。

CDN是一套全国或全球的分布式缓存集群，其实质是通过智能DNS判断用户的来源地域及上网线路，为用户选择一个最接近用户地域，以及和用户上网线路相同的服务器节点，因为地域近，且线路相同，所以，可以大幅提升用户浏览网站的体验。

CDN产生背景之一：BGP机房虽然可以提升用户体验，但是价格昂贵，对于用户来说，CDN的诞生可以提供比BGP机房更好的体验（让同一地区、同一线路的用户访问和当地同一线路的网站），BGP机房和普通机房有将近5~10倍的价格差。CDN多使用单线的机房，根据用户的线路及位置，为用户选择靠近用户的位置，以及相同的运营商线路，不但提升了用户体验，价格也降了下来。

CDN的价值：

·为架设网站的企业省钱。

·提升企业网站的用户访问体验（相同线路、相同地域、内存访问）。

·可以阻挡大部分流量攻击，例如：DDOS攻击。

经常有朋友问我，日100万PV的架构如何设计？对于这样的问题，下面简单为大家提供解答思路：首先应尽量考虑把网站数据放到CDN中缓存，这样计算在网站中的总流量和总访问量后减去CDN的访问流量，剩下的访问量规模需要的架构才是我们需要设计和考虑的。一个良好的网站架构设计，访问量应尽量都交给CDN。

12.10.2　CDN的特点

CDN就是一个具备根据用户区域和线路智能调度的分布式内存缓存集群。其特点如下：

·通过服务器内存缓存网站数据，提高了企业站点（尤其是含有大量图片、视频等的站点）的访问速度，并大大提高了企业站点的稳定性（省钱且提升用户体验）。

·用户根据智能DNS技术自动选择最适合的Cache服务器，降低了不同运营商之间互联瓶颈造成的影响，实现了跨运营商的网络加速，保证不同网络中的用户都能得到良好的访问质量。

·加快了访问速度，减少了原站点的带宽。

·用户访问时从服务器的内存中读取数据，分担了网络流量，同时减轻了原站点负载的压力等。

·使用CDN可以分担源站的网络流量，同时还可以减轻原站点的负载压力，并降低黑客入侵及各种DDoS攻击对网站的影响，从而保证网站有较好的服务质量。

下面是通过curl命令访问163网站的header信息，可以看到163网站的首页就使用了CDN进行加速。

[oldboy@student ~]$ curl -I www.163.com
HTTP/1.1 200 OK
Expires: Sat, 25 Feb 2012?02:58:57 GMT
Date: Sat, 25 Feb?2012?02:57:37 GMT
Server: nginx
Content-Type: text/html; charset=GBK
Transfer-Encoding: chunked
Vary: Accept-Encoding
Cache-Control: max-age=80
Vary: User-Agent
Vary: Accept
Age: 60
X-Via: 1.1 zb200:80 (Cdn Cache Server V2 0), 1.1 jxq206:8107 (Cdn Cache Server V2.0)
Connection: keep-alive

上面的“（Cdn Cache Server V2.0）”表示163首页使用了CDN加速。

12.10.3　企业使用CDN的基本要求

首先要说的是，不是所有的网站都可以一上来就能用CDN的。要加速的业务数据应该存在独立的域名，例如：img1-4.etiantian.org/video1-4.etiantian.org，业务内容图片、附件、JS、CSS等静态元素，这样的静态网站域名才可以使用CDN。

下面来看一个DNS解析范例。DNS服务器加速前的A记录如下：

;A records
img.etiantian.org IN A 124.106.0.21（企业服务器的

IP）

删除上面的记录，命令如下：

img.etiantian.org IN A 124.106.0.21（服务器的

IP）

然后，做下面的别名解析：

; CNAME records
img1.etiantian.org IN CNAME bbs
img.etiantian.org 3M IN CNAME img.etiantian.org.cachecn.com.

这个img.etiantian.org.cachecn.com.
地址必须是事先由CDN公司配置好的CDN公司的域名。国内较大的CDN提供商为网宿、蓝讯、快网。

12.11　Nginx程序架构优化

解耦是开发人员中流行的一个名词，简单地说就是把一堆程序代码按照业务用途分开，然后提供服务，例如：注册登录、上传、下载、浏览列表、商品内容页面、订单支付等都应该是独立的程序服务，只不过在客户端看来是一个整体而已。如果中小公司做不到上述细致的解耦，起码也要让下面的几个程序模块独立。

·网页页面服务。

·图片附件及下载服务。

·上传图片服务。

上述三者的功能应尽量分离。分离的最佳方式是分别使用独立的服务器（需要改动程序），如果程序实在不易更改，那么次选方案是在前端负载均衡器Haproxy/Nginx上，根据URI（例如目录或扩展名）过滤请求，然后抛给后面对应的服务器。

例如：根据扩展名分发，请求http://www.etiantian.org/a/b.jpg
的就应抛给图片服务器（独立的静态服务器最适合使用CDN）；根据URL路径分发，请求http://www.etiantian.org/upload/index.php
的就应抛给上传服务器。不符合上面两个要求的，就默认抛给Web服务器。

说明

可以部署3台服务器，人为分布请求服务器。当然了，这适合并发比较高、服务器较多的情况。程序架构分离了，效率、安全性都会提高很多。

12.12　使用普通用户启动Nginx（监牢模式）

12.12.1　为什么要让Nginx服务使用普通用户

默认情况下，Nginx的Master进程使用的是root用户，worker进程使用的是Nginx指定的普通用户，使用root用户运行Nginx的Master进程有两个最大的问题：

·管理权限必须是root，这就使得最小化分配权限原则遇到难题。

·使用root运行Nginx服务，一旦网站出现漏洞，用户就可以很容易地获得服务器的root权限。

因此，我想出了一种不用为开发人员，甚至普通运维人员赋予管理员权限，就可以很好地管理Nginx服务的方法，具体内容将在下节为大家讲解。

12.12.2　给Nginx服务降权的解决方案

解决方案如下：

·给Nginx服务降权，用inca用户运行Nginx服务，给开发及运维设置普通账号，只要与inca同组即可管理Nginx，该方案解决了Nginx的管理问题，防止root分配权限过大。

·开发人员使用普通账户即可管理Nginx服务及站点下的程序和日志。

·采取项目负责制度，即谁负责项目维护，出了问题就是谁负责。

很多公司的开发和运维为root权限争得不可开交，甚至大打出手。

参考资料：到底要不要给开发人员管理服务器的权限？（http://down.51cto.com/data/844517
）。

12.12.3　给Nginx服务降权实战

本优化属于架构优化（同样适合于其他软件），通过Nginx启动命令的-c参数指定不同的Nginx配置文件，可以同时启动多个实例，并使用普通的用户运行服务。

Nginx安装后的启动命令路径为“/application/nginx/sbin/nginx”，可以通过加-h参数查看相关参数的用法，命令及结果如下：

[root@oldboy ~]# /application/nginx/sbin/nginx -h
nginx version: nginx/1.3.4
Usage: nginx [- hvVtq] [-s signal] [-c filename] [-p prefix] [-g directives]
Options:
-?,-h : this help
-v : show version and exit#<==显示

Nginx版本号后退出

-V : show version and configure options then exit#<==显示

Nginx版本

号和配置选项后退出

-t : test configuration and exit#<==测试配置文件是否正确，在运行时需要重

新加载配置。此命令非常重要，用来检测所修改的配置文件是否有语法错误

-q : suppress non-error messages during configuration testing
-s signal : send signal to a master process: stop, quit, reopen, reload
#<==发送信号给一个

master进程，这里的

reload参数很重要，是优雅重启

Nginx的参数，类似

Apache的

graceful参数

-c filename : set configuration file (default: conf/nginx.conf)#<==使用指定

的配置文件而不是

conf 目录下的

nginx.conf这里我们就是通过

nginx -c 路径的功能来实现运行多实例

Nginx

较常用的方法是使服务运行在指定用户的根目录下面，这样相对比较安全，同时有利于批量业务部署和上线。

配置普通用户启动Nginx的过程如下。

（1）添加用户并创建相关的目录和文件，操作如下：

[root@www ~]# useradd inca
[root@www ~]# su - inca
[inca@www ~]$ pwd
/home/inca
[inca@www ~]$ mkdir conf logs www
[inca@www ~]$ cp /application/nginx/conf/mime.types ~/conf/
[inca@www ~]$ echo inca >www/index.html

（2）配置Nginx配置文件。配置后的查看命令如下：

[inca@www ~]$ cat conf/nginx.conf
worker_processes 4;
worker_cpu_affinity 0001 0010 0100 1000;
worker_rlimit_nofile 65535;
error_log /home/inca/logs/error.log;
user inca inca;
pid /home/inca/logs/nginx.pid;
events {
use epoll;
worker_connections 10240;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
#web.fei fa daolian..............
server {
listen 8080;
server_name www.etiantian.org;
root /home/inca/www;
location / {
index index.php index.html index.htm;
}
access_log /home/inca/logs/web_blog_access.log main;
}
}
[inca@www ~]$ LANG=en
[inca@www ~]$ tree
.
|-- conf
| |-- mime.types
| `-- nginx.conf
|-- logs
`-- www
`-- index.html
3 directories, 3 files

说明如下：

·所有参数的值，带路径的都要改成/home/inca。

·特权用户root使用的是80端口，改为普通用户使用的端口，在1024以上，这里为8080。

（3）启动Nginx，命令如下：

[inca@www ~]$ ps -ef|grep nginx|grep -v grep
[inca@www ~]$ /application/nginx/sbin/nginx -c /home/inca/conf/nginx.conf &>/dev/null &
[1] 4736
[inca@www ~]$ ps -ef|grep nginx|grep -v grep
inca 4738 1 0 13:02 ? 00:00:00 nginx: master process /appli-cation/nginx/sbin/nginx -c /home/inca/conf/nginx.conf #<==主进程也是

inca用户了

inca 4739 4738 0 13:02 ? 00:00:00 nginx: worker process
inca 4740 4738 0 13:02 ? 00:00:00 nginx: worker process
inca 4741 4738 0 13:02 ? 00:00:00 nginx: worker process
inca 4742 4738 0 13:02 ? 00:00:00 nginx: worker process
[inca@www ~]$ ss -lntup|grep nginx
tcp LISTEN 0 128 *:8080 *:* users:(("nginx",4738,5),("nginx",4739,5),("nginx",4740,5),("nginx",4741,5),("nginx",4742,5))
#特别提示：此处启动

Nginx，如果不定向到空会显示一些提示，不是错误，可以通过加

&>/dev/null定向到空，从而忽略不见。

[inca@www ~]$ killall nginx
[inca@www ~]$ killall nginx
nginx: no process killed
[inca@www ~]$ /application/nginx/sbin/nginx -c /home/inca/conf/nginx.conf
nginx: [alert] could not open error log file: open() "/application/nginx-1.6.2/logs/error.log" failed (13: Permission denied)
2015/04/26 13:06:44 [warn] 4762#0: the "user" directive makes sense only if the master process runs with super-user privileges, ignored in /home/inca/conf/nginx.conf:5

（4）查看访问，命令如下：

[inca@www ~]$ lsof -i :8080
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nginx 4764 inca 5u IPv4 333019 0t0 TCP *:webcache (LISTEN)
nginx 4765 inca 5u IPv4 333019 0t0 TCP *:webcache (LISTEN)
nginx 4766 inca 5u IPv4 333019 0t0 TCP *:webcache (LISTEN)
nginx 4767 inca 5u IPv4 333019 0t0 TCP *:webcache (LISTEN)
nginx 4768 inca 5u IPv4 333019 0t0 TCP *:webcache (LISTEN)

配置好运行解析，浏览器带端口访问结果如图12-21所示。

图12-21　成功展示页面内容

（5）解决普通端口非80提供服务的问题。

用负载均衡器解决Web服务非80端口的转换问题，负载均衡器可为Haproxy、Nginx、F5等。

本解决方案的优点如下：

·给Nginx服务降权，让网站更安全。

·按用户设置站点权限，使站点更独立（无需虚拟化隔离）。

·开发不需要用root即可完整管理服务及站点。

·可实现责任划分，即网络问题属于运维的责任，网站打不开就是开发的责任，或者两者共同承担。

12.13　控制Nginx并发连接数量

ngx_http_limit_conn_module这个模块用于限制每个定义的key值的连接数，特别是单IP的连接数。

不是所有的连接数都会被计数。一个符合计数要求的连接是整个请求头已经被读取的连接。

控制Nginx并发连接数量参数的说明如下。

1）limit_conn_zone参数

语法：limit_conn_zone key zone=name：size；

上下文：http

用于设置共享内存区域，key可以是字符串，Nginx自带变量或前两个组合，如$binary_remote_addr、$server_name。name为内存区域的名称，size为内存区域的大小。

2）limit_conn参数

语法：limit_conn zone number；

上下文：http、server、location

用于指定key设置的最大连接数。当超过最大连接数时，服务器会返回503（Service Temporarily Unavailable）错误。

1.限制单IP并发连接数

Nginx的配置文件如下：

[root@oldboy ~]# cat /application/nginx/conf/nginx.conf
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
limit_conn_zone $binary_remote_addr zone=addr:10m;
server {
listen 80;
server_name www.etiantian.org;
location / {
root html;
index index.html index.htm;
limit_conn addr 1; #<==限制单

IP的并发连接为

1
}
}
}

在客户端10.0.0.5使用Apache的ab测试工具进行测试。

测试1：模拟并发连接1，访问10次服务器，即执行ab-c 1-n 10 http://10.0.0.3/进行测试。

注意

-c为并发数，-n为请求总数，10.0.0.3为Nginx的IP地址。

测试过程中查看Nginx的访问日志，结果如下：

[root@oldboy ~]# tailf /application/nginx/logs/access.log
10.0.0.5 - - [14/Sep/2015:11:50:31 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:50:31 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:50:31 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:50:31 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:50:31 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:50:31 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:50:31 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:50:31 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:50:31 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:50:31 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"

根据上述日志可以看出当并发为1时，返回值都是200，即访问正常。

测试2：模拟并发连接2，访问10次服务器，即执行ab-c 2-n 10 http://10.0.0.3/进行测试。

测试过程中查看Nginx的访问日志，结果如下：

10.0.0.5 - - [14/Sep/2015:11:52:15 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:52:15 +0800] "GET / HTTP/1.0" 503 212 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:52:15 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:52:15 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:52:15 +0800] "GET / HTTP/1.0" 503 212 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:52:15 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:52:15 +0800] "GET / HTTP/1.0" 503 212 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:52:15 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:52:15 +0800] "GET / HTTP/1.0" 503 212 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:52:15 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:52:15 +0800] "GET / HTTP/1.0" 503 212 "-" "ApacheBench/2.3"

可以看到状态码200与503间隔1：1出现，即Nginx已经做了并发连接限制，对超过限制的请求返回503。

测试3：模拟并发连接3，访问10次服务器，即执行ab-c3-n 10 http://10.0.0.3/进行测试。

测试过程中查看Nginx的访问日志，结果如下：

10.0.0.5 - - [14/Sep/2015:11:53:59 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:53:59 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:53:59 +0800] "GET / HTTP/1.0" 503 212 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:53:59 +0800] "GET / HTTP/1.0" 503 212 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:53:59 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:53:59 +0800] "GET / HTTP/1.0" 503 212 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:53:59 +0800] "GET / HTTP/1.0" 503 212 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:53:59 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:53:59 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:11:53:59 +0800] "GET / HTTP/1.0" 503 212 "-" "ApacheBench/2.3"

由于采用的样本较小，所以出现的次数并不均衡。但看其中间数据，200与503出现的次数为1：2，即Nginx已经做了并发连接限制，对超过限制的请求返回503。

以上功能的应用场景之一是用于服务器下载，命令如下：

location /download/ {
limit_conn addr 1;
}

上面的命令限制访问download下载目录的连接数，该连接数为1。

2.限制虚拟主机总连接数

不仅可以限制单IP的并发连接数，还可以限制虚拟主机的总连接数，甚至可以对两者同时进行限制。Nginx的配置文件如下：

[root@oldboy ~]# cat /application/nginx/conf/nginx.conf
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn_zone $server_name zone=perserver:10m;
server {
listen 80;
server_name www.etiantian.org;
location / {
root html;
index index.html index.htm;
#limit_conn addr 1;
limit_conn perserver 2; #<==设置虚拟主机连接数为

2
}
}
}

提示

Nginx的内部变量列表见官网http://nginx.org/en/docs/varindex.html
。

测试：执行ab-c 5-n 1000 http://10.0.0.3/进行测试，即并发连接数为5，访问1000次。

统计日志中200与503出现的次数（测试前清空日志，测完将日志复制到root根目录），代码如下：

[root@oldboy ~]# grep -c 200 access.log
634
[root@oldboy ~]# grep -c 503 access.log
366

结论：出现的次数近似为2：1。

至此，Nginx限制连接数的应用实践就讲解完毕了。

更多内容可参考：http://nginx.org/en/docs/http/ngx_http_limit_conn_module.html
。

12.14　控制客户端请求Nginx的速率

ngx_http_limit_req_module模块用于限制每个IP访问每个定义key的请求速率。limit_req_zone参数说明如下。

语法：limit_req_zone key zone=name：size rate=rate；

上下文：http

用于设置共享内存区域，key可以是字符串，Nginx自带变量或前两个组合，如$binary_remote_addr。name为内存区域的名称，size为内存区域的大小，rate为速率，单位为r/s，每秒一个请求。

limit_req参数说明如下。

语法：limit_req zone=name[burst=number][nodelay]；

上下文：http、server、location

这里运用了令牌桶原理，burst=num，一共有num块令牌，令牌发完后，多出来的那些请求就会返回503。

换句话说，一个银行，只有一个营业员，银行很小，等候室只有5个人的位置。因此，营业员一个时刻只能为一个人提供服务，剩下的不超过5个人可以在银行内等待，超出的人不提供服务，直接返回503。

nodelay默认在不超过burst值的前提下会排队等待处理，如果使用此参数，就会处理完num+1次请求，剩余的请求都视为超时，返回503。

用于测试的Nginx配置文件如下：

[root@oldboy ~]# cat /application/nginx/conf/nginx.conf
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
#<==以请求的客户端

IP作为

key值，内存区域命名为

one，分配

10MB内存空间，访问速率限制为

1
秒

1次请求

(request)
server {
listen 80;
server_name www.etiantian.org;
location / {
root html;
index index.html index.htm;
limit_req zone=one burst=5;
#<==使用前面定义的名为

one的内存空间，队列值为

5，即可以有

5个请求排队等待

}
}
}

测试1：执行ab-c 4-n 1000 http://10.0.0.3/和ab-c 5-n 1000 http://10.0.0.3/进行测试，命令及结果如下。

[root@oldboy ~]# tailf /application/nginx/logs/access.log
10.0.0.5 - - [14/Sep/2015:13:50:19 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:13:50:20 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:13:50:21 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:13:50:22 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:13:50:23 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:13:50:24 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:13:50:25 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"…………

可以发现，访问日志中的时间和请求是1秒钟1条请求，证明配置生效。

测试2：执行ab-c 6-n 1000 http://10.0.0.3/进行测试。

统计日志中的200与503出现的次数（测试前已清空日志，测完将日志复制到root根目录）如下：

[root@oldboy ~]# grep -c 200 access.log
6
[root@oldboy ~]# grep -c 503 access.log
994
[root@oldboy ~]# more access.log
10.0.0.5 - - [14/Sep/2015:13:48:42 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:13:48:42 +0800] "GET / HTTP/1.0" 503 212 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:13:48:42 +0800] "GET / HTTP/1.0" 503 212 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:13:48:42 +0800] "GET / HTTP/1.0" 503 212 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:13:48:42 +0800] "GET / HTTP/1.0" 503 212 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:13:48:42 +0800] "GET / HTTP/1.0" 503 212 "-" "ApacheBench/2.3"……省略若干……

……省略若干……

10.0.0.5 - - [14/Sep/2015:13:48:42 +0800] "GET / HTTP/1.0" 503 212 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:13:48:42 +0800] "GET / HTTP/1.0" 503 212 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:13:48:42 +0800] "GET / HTTP/1.0" 503 212 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:13:48:43 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:13:48:44 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:13:48:45 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:13:48:46 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
10.0.0.5 - - [14/Sep/2015:13:48:47 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"

通过过滤排重及部分日志，可以看到第一个请求返回200，为正常处理，剩余的994次超过限制的请求在1秒内执行完成，但是都返回了503。

具体过程原理为：Nginx在第1秒先处理第一个请求，同时接下来的5个请求等待排队，剩下的所有（994次）请求返回503。接着第2秒到第6秒处理等待的5个请求。

更多内容可参考：http://nginx.org/en/docs/http/ngx_http_limit_req_module.html
。

12.15　小结

本章重点回顾：

（1）安全优化：隐藏Nginx软件名及版本号。

（2）性能加安全优化：连接超时参数及FastCGI相关参数调优。

（3）性能优化：gzip压缩功能及调试查看方法。

（4）性能优化：expires缓存功能及调试查看方法。

（5）安全优化：集群中各角色服务站点目录权限控制策略。

（6）安全优化：站点目录下所有的文件和目录访问控制。

（7）性能加安全优化：robots.txt协议及防爬虫优化解决方案。

（8）性能加安全优化：静态资源防盗链解决方案。

（9）用户体验优化：错误页面优雅显示方法。

（10）安全优化：限制HTTP请求方法。

（11）性能加安全优化：CDN加速知识。

（12）安全优化：监牢模式运行Nginx方案策略。

（13）性能加安全优化：Nginx并发连接数及请求速率控制。

第13章　游戏运维的思考

作者简介

马亮，原搜狐畅游端游研发，端游、手游、运维开发主管，现腾讯云自身游戏资深售前架构师、游戏云高级产品经理。

13.1　游戏运维最关键的几件事

个人认为，游戏运维的核心指标主要包含4个方面：安全、稳定、高效、成本节约。其中，安全最为优先，任何因安全问题造成的玩家数据丢失或版本泄露都是致命的打击；业务的稳定次之；高效的前提是稳定，没有稳定的基础再高效都是无用功；成本问题应该是在满足上述前提下最终的考量指标，切不可为了节约成本而影响了上述的三个指标。

13.1.1　安全

安全游戏运维领域主要关注版本安全、数据安全、网络安全、主机安全和业务安全几个维度，其中版本安全和数据安全需要我们对数据和版本的每个环节都做到全路径掌控，要冗余再冗余、备份再备份，关键路径用人必须谨慎。网络、主机及业务安全需要我们对当前面临的安全攻击行为提前做好检测和防御，严格控制入口，对系统的选型应尽可能稳重些，尽可能降低操作的权限。

1.版本安全

无论游戏发展到哪个阶段，私服都会给官服的正常运营带来巨大的负面影响，无论是当年私服兴起的端游时代，还是现在私服屡禁不止的页游和手游时代，各大游戏公司都会想尽各种办法，采用各种安全措施来防范版本泄露。

从全路径掌控上来说，需要管控如下几个方面。

·游戏版本的全内网研发环境管控。

·游戏版本从内网到外网迁出的流程通道的管控：数据默认不能直接流出，物理上做了隔离，流出环节中间要有转换，而且流出环节要有审核关键的操作人进行控制。

·游戏版本到外网的操作环境管控：对文件系统做软加密PGP、对操作版本的通道做堡垒机及操作指令的限制、对版本部署的环境做三层的安全管控。

·对游戏运行方式的管控：对游戏服的启动要有认证环，以控制启动环节。

·尽最大范围地管控版本的全路径环境。

·提供最小范围的允许度，对涉及人员的环节，权限拆分得越细越好（系统、应用运维和数据库运维），降低一个人对整体版本的可控度，而且对涉及关键环节的人物的选择需要绝对的谨慎和重视。

另外，每个环节都需要有冗余的架构，从版本的内网环境到通道环境再到线上环境都要做到硬件的冗余（硬件上是双节点之间进行定期同步、磁盘会做RAID、网络设备上会做双链路冗余bond架构），系统操作层面的冗余（服务本身也会做定期同步和冗余），任何的单点风险都应提前杜绝，之前游戏日志会有定期的保留。之前发生日志服务单节点的故障会造成业务数据的丢失或难以恢复，这样造成的直接影响就是历史数据将会难以分析，一些被盗召回的行为将难以定位，等等。采用了冗余的架构，单点的风险就能够降低很多，架构上的冗余能保证业务的整体流程不会因为硬件或软件的故障带来风险，与此同时我们还需要对其中的数据操作环节提前做好备份，确保每个环节中的数据都有冗余和留存，整体的数据环节也需要按照不同的保留环节做好保留的策略（见图13-1）：

图13-1　版本安全示意图

2.数据安全

游戏数据是玩家的命根子，处理的时候千万马虎不得。从数据的生成到数据的传输，再到数据的保留、归档，每个环节都需要把可能影响数据完整性、数据一致性的因素提前考虑进来，提前就每个环节做好冗余和管控。

首先，数据怎么做备份、，数据库怎么做冗余都不过分，任何一次的回档对一款游戏而言都是很大甚至致命的伤害。数据通常是存储到数据库中或文件系统中，那么数据存储的环节，会有硬件层、系统层和应用层，哪一层出现问题对数据的可靠性都会有安全隐患，目前的技术都很成熟了，无论是NoSQL还SQL基于系统和软件层都做了冗余，就连硬件的问题也做了冗余的方案。

其次，从数据的生命周期的环节来看，整个游戏数据的构建、更新、备份和清理环节都存在失败的风险和不可控度，就拿备份环节来说吧，你怎么备份，备份到哪里，备份完怎么校验，这些都需要做好每个环节的检测和验收，有的业务备份会放到集中的存储地区，传输环节需要做验证，备份完成了对目标存储空间的大小也要做检测，至少不要发生传过去的资料丢失的问题。

总之，做数据的工作一定要细致，做任何操作之前都要先备份一份再说，哪怕用不到也没关系，数据操作中本来就存在很多的不可控性，数据更新环节、流程至关重要，直接跳流程会对业务库造成不可挽回的损失。一定要先做好验证，数据操作脚本内容的完整性即使确认3次都不为过。提前拿备份来做验证，运行通了再说。真正更新前也一定要先进行备份，再进行操作，因为你不知道在操作环节中会遇到什么样的问题，有可能操作一半系统就崩溃了，软件发生崩溃也有可能，我们在这里能多做一些就多做一些，当然所做的都是必需的环节，做多了反而会添乱。很多时候解决问题表面上添加一个环节就是管控，其实也增加了一层的风险。如果每次都这么做，操作环节将会越来越复杂，人为的风险更不可控，从底层来做反而更直接，例如分布式存储，实现对数据的3份分散的备份对存储的安全就做得很彻底。

图13-2为数据安全示意图：

图13-2　数据安全示意图

3.网络、主机及业务安全

网络、主机及业务安全是游戏运维安全的重中之重，没有问题的时候一切都好说，一旦出现问题了就有可能是致命的打击，例如DDoS攻击，平时可能都没事，一旦在游戏的关键阶段出现了问题，将会是非常麻烦的事情，这里有很多工作需要尽快去做。目前的公有云，或多或少都有安全设备来解决DDoS攻击的问题。之前没有的时候要么自己购买，要么使用IDC的，对于这方面的分析能力和定位能力需要投入人力来把关，如果要用IDC，则需要测试下是否真的有效，提前模拟DDoS攻击以检测IDC是否真的能防得住，如果防不住要么就迁移到公有云上要么就购买相关产品。如果都不想选择，那么就要找运营商对一些协议做控制，假如游戏使用的是TCP协议，就要限制UDP协议，把IP的访问规则做得谨慎些。另外就是找一些靠谱的IDC，将出口带宽设置得大一些。有时可能不是你的业务受到了攻击，而是同一出口其他的业务受到了攻击从而影响了你的业务。把业务分散放置，不要都放在一个地方，否则一个地方出了问题整个业务都会挂掉。另外就是和IDC做好联动，用一些流量分析的软件关注下攻击源或目标源，如果攻击源有一定的规律就让IDC协助来做限制，只要先把安全攻击想明白了再做最简单的控制就容易多了。

当然还会有更多的安全攻击行为，这里能做的就是严格控制访问，使操作行为可控制，如果还是不行就借助第三方服务。对于主机访问尽可能采用中间代理的模式，限制代理的访问模式，严格管控好防火墙，并且对操作方式做严格的审查和操作权限细粒度的控制，定期关注端口的开放情况及漏洞情况，及时打补丁。如有必要则找专家来做安全渗透的测试。一旦出现问题，如果业务架构足够冗余则直接重装；如果实在不行则找专家做好安全分析，找出后门和攻击方式。

图13-3为网络、主机安全示意图：

图13-3　网络、主机安全示意图

13.1.2　稳定

稳定性上需要从我们提供服务的每个关键环节去梳理（网络稳定，硬件环境的稳定，游戏稳定，游戏运维的人员稳定），把握好了每个环节的稳定，整体就相对稳定了。那么具体该如何做到呢？

1.网络稳定

先说说网络，国内网络的复杂程度不言而喻，从整体的体制架构来看，管理上的分权，业务架构运维的标准均不统一，加上运营商的垄断特性，对我们而言的不透明和任意性，给本身就复杂的网络环境增加了更多的不可控性，我们能做的就是找到标杆的IDS，尽可能地对业务资源进行整合管理以提高我们在IDC环节的话语权，让IDC的变更对我们透明化，尽可能地了解运营商的内部，业务上线前在资源采购上先做好一定的冗余，之前我们就发现运营商提供的资源和实际采购的资源有误差，刚开始多买点没关系，稳定了就退掉，对可控的地方尽量都做到可控，尽可能减少不可控的环节，或者通过第三方云服务来帮我们做到可控，对于任何一家云服务我们都需要提前做好稳定性的监控和分析，还要看该公司的经验和沉淀，该云服务公司是否有抱负，是否有资源，是否有足够专业的人在做，是否能够满足网络架构的多出口和网络设备的双上联容灾，并且在业务上应尽可能地尝试脱敏。

图13-4为网络稳定示意图：

图13-4　网络稳定示意图

2.硬件环境和游戏程序的稳定

要想保证硬件环境和游戏程序的稳定性，需要从整体的业务流程来关注每个环节的稳定性，这里我们需要从用户接入后端的各个环节逐一来分析，我们需要保障好承载业务的每个物理单元的可靠性、业务架构的冗余性及逻辑单元之间的互通性。那么如何来保障每个环节的硬件稳定性呢？可以从硬件本身的特性来提高硬件的可靠性。另外就是能够提前发现硬件的故障是很关键的，之前我们紧急采购的资源发现内存的故障很高，另外一次就是磁盘出现了故障，提前做好主机性能压力验证很关键。另外就是从硬件故障的发生中查找规律，看看smart的报错信息，然后分别来看如何才能提前发现这些问题。

业务架构的冗余性就是从登录逻辑到游戏场景再到游戏数据的存储，每个环节都需要从业务架构上做到足够的冗余，业务单元应尽可能做到无状态，否则对后端的压力就会更大。例如登录环节，能做网关的就做网关，一定不能有状态，任何一处故障对玩家而言就需要重新连接。逻辑服通常都是有状态的，应尽可能地把数据和逻辑处理拆开，数据处理能做文件或者缓存的存储的方式更好，逻辑和数据层之间的同步节奏应尽可能地快，以降低逻辑服宕机对数据造成的风险。而且还需要关注逻辑单元之间的互通性，这里最核心的就是管道的畅通性和效率。管道的畅通性是指关注管道不拥塞，不断掉，例如，管道的带宽是否充裕，是否存在被争抢的可能性。链路上至少做到双通道冗余（可以从硬件上做绑定或从配置上做自动切换），能够随时切换。另外就是效率和延迟，这里需要从硬件层、系统层、链路层，对每个传输单元的性能做好预估，传输管道是否按照最佳路径来配置的，例如这里我们需要关注到网卡、交换机的硬件性能处理能力，业务接口的处理能力，对消息的传输路径确认是否已经做到最佳路径，能走私网就走私网，如果走公网的话是否会有跨网的问题，或者是否按照最佳的路径来传递。

图13-5所示的为硬件和游戏环境稳定示意图：

图13-5　硬件和游戏环境稳定示意图

3.游戏运维操作人员靠谱、工具靠谱

我们需要对每次操作的必要性，每次操作的流程都要有清晰的认知，任何一次没有必要的变更都会带来不稳定的因素，不动最好，或者少做无用功，这属于业务的决策。之前做项目的时候有时做一个决策是为了修复业务，对新增有影响，但对在线无影响，我们是应该马上去操作呢还是在玩家人数少的时候去操作呢。或者偶尔会因为一些故障我们无法连接到主机，但玩家实际上还在体验游戏，那么我们是否应该操作呢。对于这类问题我们都是争取在维护的周期去做，或者在玩家人数少的时候去操作。还有就是之前有遇到过需要马上升级一个脚本之类的问题，其实解决的都是体验性的问题，但是每次维护都需要花费时间，难道这样的升级对玩家的体验会好吗。

在操作环节上，应提前做好操作前的评估，对存在的风险点和操作时间上的预估要相对科学。检查环节必不可少，应尽可能做到每个环节都有验收最好。最好能够做到交叉检查，检查工作多做几次没有坏处，工具上能自动的就不要手动，最大范围地降低对人的判断，每次操作应尽可能地分环境逐步来发布，先在内部的测试环节中更新一次，测试下游戏的基本功能是否正常，核心的消费环节是否有异常，如果没有问题则发布到测试服。测试服根据版本的大小来做一定时间的验证，如果验证没有问题再全网发布。我们可以根据玩家的属性来对版本包做选择性的变更发布，保证最大范围内降低发布的问题。并且从每一步的发布中发现问题并及时解决和优化，操作环节也要做好最坏的打算，做好回退或分步操作的准备。业务操作环节越少越好，操作步骤越多出现的事故就会越多，能一步做完的不要分成两步，每个环节都会报错，要处理的问题和定位的问题会随着步骤的增加而增加。

操作人员方面，最好是由与项目利益相关的人员的来做。之前我们曾选择值班人员来操作，那个时候值班的人员都是平台性质的岗位，对游戏的架构和配置说明及操作的每一个步骤的理解都不是很深刻，加上平台的岗位与项目利益无关，对人为事故的处罚不严厉，对监控人员的培训不到位等因素，各种问题都爆发出来了，人为事故频率居高不下。后来把业务操作的权限开放给业务的接口人并提供独立的笔记本和弹性的工作时长，整体的人为事故频率才低了些。当然如果工具化做得足够好，操作足够简单，再加上足够的培训和明确的奖惩制度，将业务的操作内容交付给平台的值班人员也是没有问题的。不过有些人做一段时间了就得换掉，换掉那些容易麻痹大意或压根就不适合这个岗位的人，其实人的行为是可以从一点一滴的细节中观察出来的，发生人为事故不是巧合，而是经过一段时间的积累，或者已经形成了习惯。

操作环节的心态也很重要，操作者的精神状态必须要好，昏昏沉沉的工作就容易遗漏步骤或做错，俗话说得好，要想让这个岗位做得好，一定要让这个岗位干得爽，不能过度疲累也不能无所事事，其实之前我们出现的好多人为事故都是和操作者的心态和状态有关，加上工作工具化做得不够完善，一次更新维护整体做下来至少有20个步骤，怎么可能不犯错呢？

图13-6所示的为操作靠谱示意图：

图13-6　操作靠谱示意图

13.1.3　高效

高效的核心就是处理速度快，但快的前提是稳定，千万不能为了快速处理故障导致出现不可控的因素。之前有一次我们的业务线上出现了硬件报警，提示内存有一位校验位错误，为保守起见需要更换一台机器，当时想的就是直接把硬盘切换到另外一台上就好了，更换环节中硬盘更换的时间太长大大出乎我们的意料之外，主要是硬盘更换的操作需要IDC的人员来操作，对他们的经验和能力都不是很清楚，更换硬盘操作大大延长了故障的处理时间。如果我们选择切换备机，虽然看似没有这种方式来的简单，但实际确实更为可控，同样很多时候对故障的处理一定要慎之又慎，不能光图快而丢掉一些验证的环节和思考的环节。

那么整体运维如何才能做到高效呢，高效就是让业务操作或业务故障对客户的影响更少，我们需要先从整体的业务操作流程来开始梳理，把每个环节需要做些什么都思考好，能一个人做完的就一个人做，多人协作反而会很低效，主要是沟通和协调的成本就很高，能工具化的就用工具化来支持。整体的自动化操作和标准都是一点一滴积累起来的，之前做自动化时总是想整体来做，做什么框架之类的，其实并不重要，重要的是先把步骤积累和标准积累做好。如果有精力的话可以对过去的步骤做盘点，把能优化掉的尽可能优化掉或取消掉，而且要学会以点带面的思考，尽可能地复制和延续同样的套路和思维。对于标准来说，重要的才添加，不重要的不要急于添加，添加的环节太多未必是好事，对人的依赖度会太高，如果需要添加那么同步的自动化必须要做好，不能因为环节多了效率反而降低了，事故率也变高了。之前发生了事故之后总是先添加环节，后来发现添加的环节多了不但没有治本，反而增加了出现问题的概率，因此需要了解问题背后的原因。

自动化的工作千万不能耽误，能马上做得最好马上就做好，打铁要趁热，做好了自动化再去测试和线上验证，而且做好自动化后要定期回顾和更新。之前做游戏的时候最开始管理的资源有限，业务的操作范围也有限，而且业务架构比较简单，后来随着业务的发展越来越快，业务的类型随之增多而且整体的业务模型也越来越复杂，业务的考核指标越来越高。同样一个工具，架构上或资源上的投入上也需要有相应的变化。原有的自动化方式都是在一台中控上操作执行，然后做个多线程的批量化执行就可以了，但是业务量大了后就需要逐台机器自己去执行，中控只需要做汇总就行了。自动化的工具是给人用的，操作起来不能太复杂，人都是懒惰的，能不操作就不操作，能直接一览无余就不要逐个点击，能自动判断的就不需要人为判断。其实事故的发生或流程执行不到位很多时候未必就是流程的问题，标准本身就不一定合理，框架也未必对，需要有足够开放的心态，让大家放开了去做，而且整体都不要太急，不要太急于一下子就做成什么样子，自动化是需要时间来积累的。如果我要写一个自动化的工具且不想重新构建，那么找到足够多可靠的小工具就好。自动化未必是工具，而是一种习惯，是需要持续思考的一种方式，要把底层的平台做得足够简单和灵活，组件需要足够的多，然后在这些组件上以搭积木的方式不断地完善平台。假如我们最基本的操作模型就是以中控机SSH的方式来连接操作，操作的框架可以是中控执行，或者说是中控发起执行，每遇到一个问题可以通过添加一个脚本或配置就能解决了。如果发现操作环节太多，那就把中控的脚本编写得交互性强一些，按照交互的方式来分布执行。如果发现执行的方式不够直观，易用性不够好，安全度不够高，那么就做个前端，把前端对接到中控上。如果发现中控不够高效，那就直接对接到业务的服务器上。如果发现操作关联的环节不仅仅局限于服务器，那就把各个业务的接口都开放出来，在此基础上再做一层，支持不同系统之间流程的配置加上各个系统API的调用，当然这些都是从操作本身来做优化的，那么我们是否可以从维护的系统和应用本身去做优化呢。之前腾讯推出的MySQL就是从底层来做优化把整体版本的更新效率优化掉了。有些时候我们不仅要思考如何实现自动化，还需要思考从哪里来优化会比较合适，从哪里优化会更直接，尝试找到最直接的优化方法，降低操作中存在的风险。可控度和稳定性是基础。就像现在的蓝鲸就做得很好，它们的组件足够可靠，易操作，效率高，随着业务的发展又在组件的基础上做了一层PAAS。对于实际业务来讲，自动化需要量体裁衣，不能什么都一锅端，适合的才是最好的，可控和稳定永远是前提，逐步积累，如果有能力从底层就做优化将会更佳。

图13-7所示的是高效示意图：

图13-7　高效示意图

13.1.4　成本节约

需要对成本节约的目标有一定的定义，其实成本的核心就是投入产出比，一定要综合考虑，在保障整体游戏安全和稳定的情况下做到整体的投入成本可控。那么如何来控制成本呢，这里需要我们对业务资源的投入有自己的小算盘，对业务的每次故障所带来的影响都能够做到完整的核算。业务资源的投入主要包括计算、存储、网络、安全的资源及运维的增值服务，在计算、存储和网络的资源投入上，前期的评估，资源的投入、配置、冗余管控及回收策略都比较关键。

关于前期的性能选型方面，需要我们和研发能够先界定好评估的标准，对资源投入的消耗使用率有统一的概念，对客户体验的效果有明确的标准。内部接入产品其实还好，大家相互协作，运营、研发一体化，大家都会对整体成本做控制，标准和对应的评测工具相对都很容易做出来。而代理产品方面则相对有些困难，这就需要我们尽可能多地积累实际客户案例和效果数据来引导研发做管控。

资源投入方面，对资源投放的力度需要有足够的弹性。例如，关于资源提供的弹性方面，我们之前就和硬件厂商建立了一整套资源的快速支撑方案，这里需要找到尽可能多的资源消耗口，现在有了云就相对简单了很多。业务资源的投放策略是先少量开启，随着业务场景的扩大再逐步来扩充资源的投入。

关于就资源的配置策略方面，前期的配置可以设定得高一些，提供一定的冗余，避免业务架构的死角被忽略，造成硬件性能损耗太大，从而影响业务的正常体验。稳定上线后再根据实际业务高峰期的数据来降低配置，或者尽可能地控制业务架构以实现每个逻辑单元的灵活增减，或者支持将多个逻辑单元整合到一台主机上，对外的端口支持可以灵活配置，数据库也要能够灵活地整合和部署。

关于硬件冗余方面，上线前关键路径至少需要保证双路径的冗余，非核心业务至少能够做到多对1的冗余，业务上线后根据实际的故障影响收益和实际的硬件投入成本来做对比，再决策相应的架构。

关于安全方面的资源投入，需要我们对安全隐患带来的影响有深刻的评估，建议前期提前做好临时数据的冗余，否则在上线的关键期间，如果真的因为恶意攻击造成了业务的瘫痪，其效果将会大打折扣，上线后也要根据业务实际的收益和投入成本，看看是否有必要继续投入或尽可能地降低故障带来的影响，例如更换IP方式之类的故障恢复措施。

关于增值服务方面，建议具体问题具体分析，投入首先要有核算。那么如何对业务的故障带来的影响做到精准的核算呢？有些方面可以直接从日志中提取，有些方面需要我们发挥对比的价值，同期对比、环比都可以作为参考指标。

图13-8所示的是成本节约示意图：

图13-8　成本节约示意图

13.2　游戏运维人的发展

对运维最初的认识就是该行业容易出错，经过一段时间实际的操作之后，发现这个行业简直就是出错的“高危”行业。而且运维往往会处于整个公司价值链的最底层，相对被动，价值难以被量化，有时你会发现干得好是应该的，干得不好就是你的错，比较苦。经过长时间的琢磨，我们做了很多实际的工作来量化运维的价值，例如成本上的节约、效率上的优化与新增收益的转换、体验上的改进与新增收益的转换，等等。成本上的节约很容易理解（人力的节约、硬件的节约都算成本上的节约），关于效率上的优化与新增收益的转换，在这里我可以举个例子，之前我们很关注更新维护对业务带来的影响，如何与能够做到在线更新，如何才能够最大范围地降低更新维护的消耗，如何才能够尽可能地保证每次更新维护的有效性。我们把整体更新维护环节按照内部测试服、线上测试服、线上全量的方式分布更新，保障业务版本的问题能够在内部测试服或线上测试服时就提前发现，经过内部测试服（环境需要绝对的完整），版本发错的问题得到了杜绝，大大降低了版本正式发布的风险（之前曾有过这样的经历，发布到全量失败）。对发布的方式我们做到了一键更新维护，配置文件实现了统一，把数据的维护和业务的维护串连起来了，整体的更新维护时间缩短了50%。关于脚本类的更新和一些配置文件的更新也做到了热更新，动态加载，减少了停服和起服的环节。经过这一番工作的努力，可由此同比新增的用户消费来核算实际的价值。关于体验上的改进与新增收益的转换，我们可以从游戏玩家接入的效率和充值的效率两方面同期对比新增消费来核算实际的价值。找到了价值点后，如何让价值点转换成我们的实际收益呢。我们做了一些工作，之前运维侧属于平台侧，项目奖金和他们没有关系，经过几番的争取，运维核心接口也慢慢纳入到了项目核心团队中，能够从项目的奖金中得到一定的收益。有了价值点的体现，我们就需要将价值点持续放大，这里就需要深入理解业务，深入理解这个行业的深度，并且主动参与各类实际的案例以持续积累经验，主动向上游学习，把自我的价值在公司内部持续放大。

另外一方面，我们也在持续推动如何量化运维人员的行业价值，这里我们借鉴了同行业不同公司对人员能力模型的界定标准及不同公司对运维人员的薪酬标准，慢慢摸索出公司内部关于人员自身行业价值的评价标准，由此也开拓了另外一条价值升值的通道。从目前的行业来看，游戏运维有四个方向：游戏运营侧的技术专家，一方面，能够增加对整体游戏技术的理解，另一方面，能够在游戏的价值输出中提供更多的价值，这里需要他们能够对游戏的测试、评测和研发都有所涉猎，从长远看可以往研发、制作人或运营经理方向发展，离用户最近，价值就放大得越大；游戏运维专家，这里需要他们经历更多产品的游戏运维，对游戏运维梳理总结出一套标准和框架，能够从整体来放大游戏运维的价值；资深游戏运维架构师，转行做售前，将游戏运维的经验和云相结合并输出给客户，为云公司带来收益，由此体现自身的价值；平台运维，将游戏运维的经验下沉和底层的技术相结合，因为他们具备实际的业务经验，对于技术和业务的结合更具优势，而且设计出的技术产品更贴近用户。

图13-9所示的为游戏运维人发展示意图：

图13-9　游戏运维人发展示意图

13.3　小结

经过几年运维经验的积累和沉淀，游戏运维整体平台智能化的演变从“小米加步枪”转化为蓝鲸运维平台，运维思想从以成本为中心转化为以价值为中心，岗位定位从支撑岗位转化收入岗位，过程中运维人一直在努力寻找可量化自我价值的标准，并主动从自身技能上不断进行突破和改变，尝试了各类方案的验证。随着云时代的到来，游戏运维人和云厂商可以共同为行业转变出谋划策，并逐步摆脱底层建设的依赖，真正实现运维人的价值。

第14章　数据库平台建设实战

作者简介

赵海军，猎豹移动数据库负责人，先后做过IDC运维和应用运维，最近三年在做数据库运维，负责公司的MySQL、RDS、Redis、MongoDB、Memcached数据库运维，主导设计了公司的MySQL数据库平台，并对800+MySQL实例进行了迁移，对LVS运维、Redis、Codis运维有一定的经验。博客地址http://navyaijm.blog.51cto.com/
。

本章将为大家介绍一下猎豹移动数据库平台相关的一些技术，这个平台也是我亲自参与设计和规划的，很多东西都是从日常工作中总结出来的。在此要感谢我们平台的核心开发工程师李剑辉、邓瑶还有前同事李恒，还要感谢我们数据库团队的刘建，他负责了后期的需求整理和测试。本章不会介绍平台的代码实现，而主要是介绍一下平台的前期规划、架构选型、需求梳理，我觉得任何一个平台若要成功上线，这些都是基础，所以，本章将主要介绍猎豹移动数据库运维的一些规范和这个平台的架构，以及实现了什么样的功能，希望对大家在今后自己的平台化建设方面有所帮助。

14.1　规范建立

近年来，平台化、自动化这两个名词在运维的圈子里已经“火”到高潮了，各家公司都在搞平台化、自动化，我们也不例外，但是过程很痛苦，痛苦的根源就是历史包袱太重，对于这点相信很多人都有同感吧。比如软件安装，有使用yum安装的，还有编译安装的，并且路径都各不相同，所以，我个人的建议是暂且抛开沉重的历史包袱，建立相关的规范，为平台化建设奠定基础，待我们的平台建设完成之后再逐步瓦解“历史包袱”，逐步向平台上迁移，实现最终的统一管理。

14.1.1　安装规范

说到安装规范，相信维护过大规模数据库的DBA都深有感触，我们这边最初也没有规范，安装数据库时有些人喜欢使用yum安装，有些人喜欢使用源码编译安装，并且安装目录、数据目录各不相同，这样导致的问题就给后人带来了比较大的运维成本，还不方便进行统一管理，或者给平台化带来了很多麻烦，需要兼容工作情况。

MySQL安装，我们统一采用“模板”安装，所谓的模板就是将源码用我们指定的编译参数，编译出来一份可以直接启动数据库的文件，需要说明一下的是，我们这边的环境99%都是单机多实例部署，大家常见的单机多实例部署都是在服务器上安装好MySQL，然后用不同的配置文件，启动多个实例，这样做的缺点是多个版本不能共存，而我们通过不同版本的模板部署，多个实例之间不会互相影响（硬件资源除外），单机可以启动任意版本的MySQL实例，下面是我们这边5.5和5.6的编译参数，在此列出以供大家参考。

1.MySQL 5.5编译参数

Liunx下可用源码编译安装软件，下面给出MySQL 5.5的编译参数。

参数详解具体如下。

·CMAKE_INSTALL_PREFIX：PATH：指定数据库的安装路径。

·MYSQL_DATADIR：指定数据的存储目录。

·MYSQL_UNIX_ADDR：指定sock的文件。

·DEFAULT_CHARSET：默认字符集。

·DEFAULT_COLLATION：默认排序字符集。

·WITH_MYISAM_STORAGE_ENGINE：开启MYISAM引擎。

·WITH_INNOBASE_STORAGE_ENGINE：开启InnoDB引擎。

cmake -DCMAKE_INSTALL_PREFIX:PATH=/data/mysql/example
-DMYSQL_DATADIR=/data/mysql/example/db
-DMYSQL_UNIX_ADDR=/data/mysql/example/logs/mysql.sock
-DDEFAULT_CHARSET=utf8
-DDEFAULT_COLLATION=utf8_general_ci
-DWITH_MYISAM_STORAGE_ENGINE=1
-DWITH_INNOBASE_STORAGE_ENGINE=1
-DWITH_READLINE=1 -DENABLED_LOCAL_INFILE=1

2.MySQL 5.6编译参数

参数详解具体如下。

·CMAKE_BUILD_TYPE：指定产品编译说明信息。

·CMAKE_INSTALL_PREFIX：PATH：指定数据库的安装路径。

·MYSQL_DATADIR：指定数据的存储目录。

·DEFAULT_CHARSET：默认字符集。

·DEFAULT_COLLATION：默认排序字符集。

·WITH_MYISAM_STORAGE_ENGINE：开启MYISAM引擎。

·WITH_INNOBASE_STORAGE_ENGINE：开启InnoDB引擎。

·WITH_SSL：是否支持SSL。

·CMAKE_EXE_LINKER_FLAGS：指定使用jemalloc管理内存。

下面给出MySQL 5.6的编译参数：

cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo
-DCMAKE_INSTALL_PREFIX=/data/mysql/example
-DMYSQL_DATADIR=/data/mysql/example/db
-DSYSCONFDIR=/data/mysql/example/my.cnf
-DWITH_INNOBASE_STORAGE_ENGINE=1
-DWITH_FEDERATED_STORAGE_ENGINE=1
-DWITH_PARTITION_STORAGE_ENGINE=1
-DDEFAULT_CHARSET=utf8
-DDEFAULT_COLLATION=utf8_general_ci
-DENABLE_DEBUG_SYNC=0
-DENABLED_LOCAL_INFILE=1
-DENABLED_PROFILING=1
-DMYSQL_UNIX_ADDR=/data/mysql/example/logs/mysql.sock
-DWITH_DEBUG=0
-DWITH_SSL=yes
-DWITH_SAFEMALLOC=OFF
-DWITH_BDB=1
-DWITH_blackhole-storage=1
-DCMAKE_EXE_LINKER_FLAGS="-ljemalloc"

14.1.2　配置规范

关于MySQL的配置参数，我们也可以用统一的模板文件来生成，当然，大家会问，不同配置的机器所用的配置参数值都是一样的吗？我们这边的环境99%都是单机多实例，所以配置参数值的大小和机器的配置没有必然联系，所以默认使用的都是我们认为“合适”的值。随着后续库访问量的上升，我们也会对一些需要调优的参数做调整，下面给出我们这边所用的5.5和5.6的My.cnf参数。

1.my.cnf就是启动MySQL的配置参数文件，默认的配置不太能满足我们线上的需求，所以有些参数的值需要调整一下，下面是我们所用的关于MySQL 5.5和MySQL 5.6的参数说明。

1.My.cnf的重要参数详解

下面来看下MySQL 5.5的My.cnf的参数设置。

·key_buffer：索引块是缓冲的并且被所有的线程所共享。key_buffer可用于设置索引块的缓冲区大小，增加它可以得到更好处理的索引（对所有读和多重写），可将它增大到你能负担得起那样多。如果你使它太大，系统将开始换页并且可能还会变慢。

·max_allowed_packet：服务所能处理的请求包的最大大小及服务所能处理的最大的请求大小，当与大的BLOB字段一起工作时该参数相当必要。

·sort_buffer_size：MySQL执行排序所使用的缓冲大小。如果想要提高ORDER BY的速度，首先要看是否可以让MySQL使用索引而不是进行额外的排序，如果不能，则可以尝试增加sort_buffer_size变量的大小。

·read_buffer_size：MySQL读入缓冲区的大小。对表进行顺序扫描的请求将分配一个读入缓冲区，MySQL会为它分配一段内存缓冲区。read_buffer_size变量可控制这一缓冲区的大小。如果对表的顺序扫描请求非常频繁，并且你认为频繁扫描进行得太慢，那么可以通过增加该变量值及内存缓冲区的大小来提高其性能。

·read_rnd_buffer_size：MySQL的随机读缓冲区大小。当按任意顺序读取行时（例如，按照排序的顺序），将分配一个随机读缓冲区。进行排序查询时，MySQL首先会扫描一遍该缓冲，以避免磁盘搜索，提高查询速度，如果需要排序大量数据，则可适当调高该值。但MySQL会为每个客户连接发放该缓冲空间，所以应尽量适当地设置该值，以避免内存开销过大。

·thread_cache_size：这个值（默认为8）表示可以重新利用保存在缓存中的线程的数量，当断开连接时如果缓存中还有空间，那么客户端的线程将被放到缓存中。如果线程重新被请求，那么请求将从缓存中读取。如果缓存中是空的或是新的请求，那么这个线程将被重新创建。如果有很多新线程，那么增加这个值可以改善系统的性能。通过比较Connections和Threads_created状态的变量，可以看到这个变量的作用，根据物理内存设置规则如下：

·1G→8

·2G→16

·3G→32

·大于3G→64

·query_cache_size：MySQL的查询缓冲大小（从4.0.1开始，MySQL提供了查询缓冲机制）。使用查询缓冲，MySQL将SELECT语句和查询结果存放在缓冲区中，今后对于同样的SELECT语句（区分大小写），将直接从缓冲区中读取查询结果。根据MySQL用户手册所述，使用查询缓冲最多可以达到238%的效率，通过检查状态值“Qcache_%”，可以得知query_cache_size的设置是否合理——如果Qcache_lowmem_prunes的值非常大，则表明经常出现缓冲不够的情况；如果Qcache_hits的值也非常大，则表明查询缓冲使用得非常频繁，此时需要增加缓冲的大小；如果Qcache_hits的值不大，则表明查询的重复率很低。

·log-bin：打开二进制日志功能，在复制（replication）配置中，作为MASTER主服务器必须打开此项，如果需要从最后的备份中做基于时间点的恢复，那么你也同样需要二进制日志。

·binlog_format：将binlog的日志格式设置为ROW模式，binlog有三种日志格式，分别是STATEMENT模式、ROW模式、MIXED模式，具体每种模式的优缺点大家自行参考相关资料，生产环境中基本上都是ROW模式。

·log-slave-updates：需要做主主环境或级联复制的，需要开启此参数。

·wait_timeout：服务器关闭非交互连接之前等待活动的秒数。在线程启动之时，要根据全局wait_timeout的值或全局interactive_timeout的值初始化会话wait_timeout值

·interactive_timeout：服务器关闭交互式连接之前等待活动的秒数，交互式客户端定义为在mysql_real_connect（）中使用CLIENT_INTERACTIVE选项的客户端。默认值为28800秒（8小时）

·tmp_table_size：MySQL的heap（堆积）表缓冲大小。所有联合都在一个DML指令内完成，并且大多数联合甚至可以不用临时表即可完成。大多数临时表都是基于内存的（heap）表。具有大的记录长度的临时表（所有列的长度之和）或包含BLOB列的表存储在硬盘上。如果某个内部heap（堆积）表的大小超过了tmp_table_size，那么MySQL可以根据需要自动将内存中的heap表改为基于硬盘的MyISAM表，还可以通过设置tmp_table_size选项来增加临时表的大小。也就是说，如果调高该值，那么MySQL同时将增加heap表的大小，可达到提高连接查询速度的效果。

·replicate-wild-ignore-table：这个参数是在从库上设置的，是用来设置需要忽略同步的表，通过上面的配置文件可以看到，我们对MySQL自带的一些库都设置了忽略同步，包括MySQL库，也就是说我们的数据库账号、权限信息是不同步的，这样能最大力度地控制在从库上的误写，然而如果给从库上加了可写账号那就要另说了。

·innodb_file_per_table：设置InnoDB为独立表空间模式，每个数据库的每个表都会生成一个数据空间，独立表空间的优点具体如下。

·每个表都有自已独立的表空间。

·每个表的数据和索引都会存储在自已的表空间中。

·可以实现单表在不同的数据库中移动。

·空间可以回收（除drop table操作之外，表空间不能自已回收）。

·innodb_buffer_pool_size：主要作用是缓存InnoDB表的索引、数据、插入数据时的缓冲等信息，该项设置得越大，你在存取表中的数据时所需要的磁盘I/O就越少，如果是专用的MySQL服务器，则建议设置为服务器内存的60%~80%；如果是单机多实例，则可视情况而定。

·innodb_flush_log_at_trx_commit：当innodb_flush_log_at_trx_commit被设置为0时，日志缓冲将以每秒一次的频率被写到日志文件中，并且对日志文件做到磁盘操作的刷新，但是在一个事务中提交不做任何操作；当这个值为1（默认值）时，在每个事务提交时，日志缓冲被写到日志文件，对日志文件做到磁盘操作的刷新；当设置为2时，在每个事务提交时，日志缓冲被写到日志文件，但不对日志文件做到磁盘操作的刷新。需要注意的是，并不能保证100%每秒都一定会刷新到磁盘，这点还要取决于进程的调度。每次进行事务提交的时候将数据写入事务日志，而这里的写入仅仅是调用了文件系统的写入操作，而文件系统是有缓存的，所以这个写入并不能保证数据已经写入到了物理磁盘。综上所述，设置为1最安全，0性能最好，2折中，生产环境中建议设置为2。

·sync_binlog：sync_binlog=0时，当事务提交后，MySQL仅仅将binlog_cache中的数据写入binlog文件，但不执行fsync之类的磁盘同步指令，通知文件系统将缓存刷新到磁盘，而让Filesystem自行决定什么时候来做同步，这个性能是最好的。sync_binlog=n时，在进行n次事务提交以后，MySQL将执行一次fsync之类的磁盘同步指令，通知文件系统将binlog文件缓存刷新到磁盘；MySQL中默认的设置是sync_binlog=0，即不做任何强制性的磁盘刷新指令，这时性能是最好的，但风险也是最大的。一旦系统崩溃，文件系统缓存中的所有binlog信息都将丢失。

·innodb_lock_wait_timeout：InnoDB事务在被回滚之前可以等待一个锁定的超时秒数。InnoDB在它自己的锁定表中自动检测事务死锁并且回滚事务。InnoDB用LOCK TABLES语句实现锁定设置。默认值是50秒。

下面是我们线上MySQL的配置参数，在此列出，仅供参考：

[mysqld_safe]
log-error= /data/mysql/example/logs/mysqld.log
pid-file= /data/mysql/example/logs/mysqld.pid
[client]
port = 3307
socket = /data/mysql/example/logs/mysql.sock
[mysqld]
port = 3307
socket = /data/mysql/example/logs/mysql.sock
key_buffer = 384M
max_allowed_packet = 16M
table_cache = 4096
sort_buffer_size = 2M
read_buffer_size = 2M
read_rnd_buffer_size = 8M
myisam_sort_buffer_size = 64M
thread_cache_size = 8
query_cache_size = 64M
basedir= /data/mysql/example
datadir= /data/mysql/example/db
thread_concurrency = 8
log-bin=mysql-bin
binlog_format = row
log-slave-updates = 1
expire_logs_days = 7
server-id = 103
max_connections=2048
character_set_server=utf8
wait_timeout=1800
interactive_timeout=1800
skip-show-database
skip-name-resolve
tmp_table_size = 512M
max_heap_table_size = 512M
replicate-wild-ignore-table=mysql.%
replicate-wild-ignore-table=performance_schema.%
replicate-wild-ignore-table=test.%
replicate-wild-ignore-table=information_schema.%
innodb_data_home_dir = /data/mysql/example/db
innodb_file_per_table=1
innodb_log_group_home_dir = /data/mysql/example/db
innodb_buffer_pool_size = 8000M
innodb_additional_mem_pool_size = 20M
innodb_log_file_size = 100M
innodb_log_buffer_size = 8M
innodb_flush_log_at_trx_commit = 2
sync_binlog =0
innodb_lock_wait_timeout = 80
default-storage-engine = Innodb
[mysqldump]
quick
max_allowed_packet = 16M
[mysql]
no-auto-rehash
[isamchk]
key_buffer = 256M
sort_buffer_size = 256M
read_buffer = 2M
write_buffer = 2M
[myisamchk]
key_buffer = 256M
sort_buffer_size = 256M
read_buffer = 2M
write_buffer = 2M
[mysqlhotcopy]
interactive-timeout

2.MySQL 5.6的My.cnf

MySQL 5.6的My.cnf的具体代码如下：

[client]
port = 10321
socket = /var/mysql/mysql.sock
[mysqld_safe]
socket = /data/mysql/example/logs/mysql.sock
nice = 0
[mysqld]
pid-file = /data/mysql/example/logs/mysql.pid
basedir = /data/mysql/example
bind-address = 0.0.0.0
character-set-server = utf8
character-set-client = utf8
datadir = /data/mysql/example/db
default-storage-engine = Innodb
event-scheduler = ON
expire_logs_days = 7
general-log = 0
log-output = FILE
log-error = /data/mysql/example/logs/mysqld.log
log-warnings = 1
port = 10321
socket = /data/mysql/example/logs/mysql.sock
user = mysql
server-id = 103
log-slave-updates = 1
skip-name-resolve = ON
skip-external-locking = ON
replicate-wild-ignore-table=mysql.%
replicate-wild-ignore-table=performance_schema.%
replicate-wild-ignore-table=test.%
replicate-wild-ignore-table=information_schema.%
long_query_time = 3
slow_query_log = ON
log-queries-not-using-indexes = OFF
slow_query_log_file = /data/mysql/example/logs/slowquery.log
wait_timeout = 1800
interactive_timeout = 1800
connect_timeout = 5
net_read_timeout = 120
max_connections = 2048
max_user_connections = 2150
key_buffer = 64M
max_allowed_packet = 32M
table_open_cache = 5000
table_definition_cache = 500
sort_buffer_size = 1M
join_buffer_size = 1M
read_buffer_size = 1M
read_rnd_buffer_size = 512KB
thread_cache_size = 300
tmp_table_size = 2G
max_heap_table_size = 2G
query_cache_type = 0
innodb-status-file = 1
innodb_additional_mem_pool_size = 20M
innodb_buffer_pool_size = 1G
innodb_buffer_pool_instances = 8
innodb_data_home_dir = /data/mysql/example/db
innodb_file_per_table = 1
innodb_doublewrite = 1
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
read_buffer_size
innodb_lock_wait_timeout = 120
innodb_log_buffer_size = 4M
innodb_log_file_size = 512MB
innodb_log_files_in_group = 2
innodb_log_group_home_dir = /data/mysql/example/db
innodb_support_xa = 0
innodb_thread_concurrency = 64
innodb_use_sys_malloc = 0
log-bin = /data/mysql/example/db/mysql-bin
max_binlog_size = 100M
expire_logs_days = 7
sync_binlog = 0
binlog_format = row
relay_log_recovery = 1
federated
[mysqldump]
quick
quote-names
[isamchk]
key_buffer = 16M

14.1.3　账号、权限规范

我们对账号也做了相关的规范，账号的命名、权限、来源IP限制，都有相关的规范约束，为什么要这样做，我大概总结了以下几个要点：

·方便运维，通过账号DBA就能知道这个账号有什么样的权限。

·规范运维，方便后续自动化建设。

·降低安全风险，给特定账号对应的权限，监视误操作的发生。

下面用一个具体的例子来说明一下，例如，开发部门申请一个数据库实例，数据库名为navy_db，我们为他提供了3个账号，一个是用于程序连接的读写账号navy_db_pro，具有增、删、改、查的权限；一个是用于程序连接的只读账号navy_db_sel；还有一个是供开发人员在公司访问时所用的只读账号navy_db，每个账号都有严格的来源IP限制，不容许有授权网段的账号。

总结起来，有以下几点。

·账号命名规范：dbname_pro是程序连接的读写账号，dbname_sel是程序连接的只读账号，dbname是办公网连接的只读账号。

·授权规范：读账号只有select权限，读写账号默认只有insert、delete、update、select权限，如果需要其他特殊权限，需要部门领导审批，才能开放。

·授权规范：每个账号都对应有严格的来源IP限制，禁止授权网段，比如：10.10.10.%。关于数据库授权来源IP，我了解到好多公司都是授权了整个网段，最开始我们也是授权整个网段的，但是现在为什么不容许这样做了呢。是这样的，我们曾经有一个项目，最初是A团队在做，可能几个月之后又交接给了B团队，这都不是重点，重点是他们的工作交接不清楚，还有就是离职交接，关于哪些服务器可以连接这个项目的数据库，对应的账号都有哪些权限，谁都不知道，只能在库里面抓，后来我们就不容许授权整个网段了。如果项目扩容，需要新增服务器，则申请数据库加白名单，这样DBA就可以把控整个库的信息了。

14.1.4　目录规范

前面介绍过我们的数据库都是单机多实例部署的，每个实例只有并且只能有一个database，需要保证目录名称和dbname一一对应，那么目录规范的重要性就不言而喻了，图14-1是一台服务器上两个数据库实例（navy_db_1、navy_db_2）的目录结构，14.1.2一节“配置规范”已经体现了含义：

Basedir /data/mysql/dbname
Datadir /data/mysql/dbname/db
innodb_data_home_dir /data/mysql/dbname/db
innodb_log_group_home_dir /data/mysql/dbname/db
Socket、

log、

PID相关

/data/mysql/dbname/logs

图14-1　服务器上navy_db_1、navy_db_2的目录结构

14.1.5　其他规范

1.文件系统及挂载参数

我们推荐文件系统使用XFS，挂载参数如下：

rw,noatime,nodiratime,noikeep,nobarrier,allocsize=8M,attr2,largeio,inode64,swalloc

关于XFS挂载参数的解释，请参考：http://blog.csdn.net/fangaohua200/article/details/8284019

2.本地MySQL的操作记录审计

关于MySQL的操作记录，下面这个方法适用于你有一个跳板机的情况，你可以从这个跳板机上用MySQL客户端登录线上的数据库，那么你执行的所有MySQL的命令都会记录下来，保存到一个文件里面。其实现原理就是利用MySQL的prompt命令把你登录数据库的账号、地址、数据库名还有执行命令的当前时间显示在终端窗口，再配合tee把终端窗口的屏显保存到定义的文件里面，然后再去分析这个文件就可以了，具体实现其实很简单，在My.cnf里面的MySQL区域中添加如下两行参数就能搞定。

具体配置参数如下：

[root@运维男

~]# cat /etc/my.cnf
[mysql]
prompt="\u@\h:\p [\d] : \D >"
tee=/data/log/mysql_history/mysql_history.log

3.文件最大打开数

文件最大打开数，见如下代码：

[root@运维男

~]# cat /etc/security/limits.conf
* soft nproc 10000
* hard nproc 10000
* soft nofile 4194304
* hard nofile 4194304

4.统一内核参数

统一内核参数，见如下代码：

[root@运维男

~]# cat /etc/sysctl.conf
fs.nr_open = 5242880
fs.file-max = 4194304
kernel.core_uses_pid = 1
kernel.msgmax = 1048560
kernel.msgmnb = 1073741824
kernel.shmall = 4294967296
kernel.shmmax = 68719476736
kernel.sysrq = 0
net.core.netdev_max_backlog = 1048576
net.core.rmem_default = 2097152
net.core.rmem_max = 16777216
net.core.somaxconn = 1048576
net.core.wmem_default = 2097152
net.core.wmem_max = 16777216
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.default.rp_filter = 1
net.ipv4.ip_forward = 0
net.ipv4.ip_local_port_range = 11001 65000
net.ipv4.neigh.default.gc_thresh1 = 10240
net.ipv4.neigh.default.gc_thresh2 = 40960
net.ipv4.neigh.default.gc_thresh3 = 81920
net.nf_conntrack_max = 819200
net.netfilter.nf_conntrack_max = 819200
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 60
net.netfilter.nf_conntrack_tcp_timeout_fin_wait = 120
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 120
net.ipv4.tcp_fin_timeout = 10
net.ipv4.tcp_keepalive_intvl = 15
net.ipv4.tcp_keepalive_probes = 5
net.ipv4.tcp_keepalive_time = 30
net.ipv4.ip_local_port_range = 1024 65000
net.ipv4.tcp_max_orphans = 3276800
net.ipv4.tcp_max_syn_backlog = 1048576
net.ipv4.tcp_max_tw_buckets = 10000
net.ipv4.tcp_mem = 94500000 915000000 927000000
net.ipv4.tcp_orphan_retries = 3
net.ipv4.tcp_reordering = 5
net.ipv4.tcp_retrans_collapse = 0
net.ipv4.tcp_retries2 = 5
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_sack = 1
net.ipv4.tcp_synack_retries = 1
net.ipv4.tcp_syncookies = 0
net.ipv4.tcp_syn_retries = 1
net.ipv4.tcp_timestamps = 0
net.ipv4.tcp_tw_recycle = 0
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_wmem = 4096 16384 16777216
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
vm.swappiness = 1

对于上述代码中所涉及的重要配置参数，我们来具体解读一下。

·net.ipv4.ip_local_port_range：这个参数是预留一段端口范围作为服务（MySQL）的监听端口，默认值是32768~61000，当你的MySQL监听端口大于1024的时候，重启MySQL服务，你发现经常会有TCP连接占用你的MySQL监听端口，这让你很恼火，那么设置这个参数吧，把MySQL的监听端口放在这个范围里面即可解决上述问题。

·net.ipv4.tcp_max_tw_buckets：表示系统同时保持TIME_WAIT套接字的最大数量，如果超过了这个数字，TIME_WAIT套接字将立刻被清除并在/var/log/messages中打印警告信息，默认为180000，改为10000。

·net.ipv4.tcp_tw_recycle：开启TCP连接中TIME_WAIT sockets的快速回收，默认是0，关闭。

·net.ipv4.tcp_tw_reuse：设置为1表示开启重用，允许将TIME_WAIT sockets重新用于新的TCP连接，默认是0，关闭。

·net.ipv4.tcp_fin_timeout：表示如果套接字由本端要求关闭，那么这个参数决定了它保持在FIN-WAIT-2状态的时间，默认值是60秒。

·net.ipv4.tcp_keepalive_intvl：当探测没有确认时，重新发送探测的频度。默认是75秒。

·net.ipv4.tcp_keepalive_probes：在认定连接失效之前，发送多少个TCP的keepalive探测包。默值是9。这个值乘以tcp_keepalive_intvl之后决定了，一个连接发送了keepalive之后可以有多少时间没有回应。

·net.ipv4.tcp_keepalive_time：当keepalive启用的时候，TCP发送keepalive消息的频度。默认是2小时。

·vm.swappiness：默认值是60，设置为0的时候表示最大限度地使用物理内存，然后才是swap空间，而不是网上所说的是禁止使用swap；设置为100的时候表示积极地使用swap分区，并且把内存上的数据及时搬运到swap空间里面。

14.2　架构设计

本节主要分为两部分，第一部分给出架构图大概简述一下平台的功能，第二部分介绍一下平台各个模块的主要功能。

14.2.1　架构图

这个平台完成了MySQL日常运维80%的工作，包括数据库实例开通、从库扩容、主从切换、权限管理，等等。

图14-2为数据库平台的架构设计。该架构由LVS、Redir、底层DB服务器、管理平台等几个模块组成。LVS就是一个统一入口的角色，数据库开通之后，我们提供给业务人员的地址是LVS的虚拟IP端口，业务人员并不了解数据库真正运行在哪台服务器上；Redir就是提供一个NAT功能的工具，每个库都会有一个公网地址映射到主库，供业务人员在公司查询数据库；底层DB服务器就是用来运行MySQL实例的；管理平台就是管理调度前面的3个模块，提供UI界面供DBA操作。

14.2.2　各个模块介绍

1.LVS

在这个平台中，LVS充当着入口的角色，即我们对外提供的数据库连接地址是VIP：Port。一套LVS环境（主备、DR模式）有两个VIP，一个是读业务的VIP，一个是写业务的VIP，然后通过Port区分数据库。一个Port对应一个数据库，Port在整个平台中全局唯一，不容许有重复的Port，平台中可以有多套LVS环境。多个从库前面加一层LVS转发，做负载均衡调度，这个比较常见，但是对于主库前面加一次LVS，对大家来说可能有些陌生，下面就来分析一下。关于主库的高可用，最常见的就是VIP了，主库挂掉时VIP漂移到新的主库，但是在我们这个架构里面，如果用VIP，那就需要很多IP（一个库一个IP）了，不过这都不是问题，其实最主要的问题是维护成本大。IP变动还得更新运维的资产系统，如果忘记更新了，IP一旦被其他机器抢占，就会出大问题。还有就是我们的这个方案，主库没有做真正的高可用，当主库挂掉时，需要在平台上点一下迁移按钮，才会做故障迁移，如果直接把主库的地址提供给业务人员使用，那么发生迁移之后主库的地址变了，还需要通知业务改代码，更换主库的地址，这显然是不科学的，这也是我们在主库迁移前加一层LVS的一个主要原因。

图14-2　数据库平台架构图

2.Redir

Redir是一个特别轻量级的端口映射工具，从图14-2中可以看到，我们这边需要从公司内网访问数据库，而我们的数据库服务器都是内网地址，所以就需要一个端口映射工具了，然后结合小米开源的进程管理工具god，实现端口映射的进程监控和维护，当然也可以用iptables实现端口映射功能，或者其他的开源工具。

3.底层DB服务器

底层的DB服务器就是整个平台的资源池，用于部署MySQL实例。DB服务器分为两类，一类是提供线上的MySQL实例部署；另一类是提供备份的MySQL实例部署。也就是说只要在我们的平台上开通一个库，至少要有一主库、两从库，一个从库用于候选主库，另一个用于备份，备份DB服务器上的MySQL从库不提供服务，只是用作备份。我们的备份策略都是在备份专用的从库上进行的，冷备+增量备份。

4.管理平台

管理平台类似于一个调度中心，管理调度LVS、Redir、DB服务器资源，底层使用的SaltStack，管理平台是master角色，其他都是client角色；那么实现这个平台的语言当然是采用大家熟悉的Python了，Web框架也是大家常用的Django。

14.3　功能介绍与实践

14.3.1　操作部分

这部分是整个平台的核心功能，日常运维的一些功能都包含在这里，包括新增实例、从库、迁入（把一个库从平台外迁入平台管理）、实例下线、迁移、权限管理等，下面将逐一介绍各个功能。

1.新增实例、从库

如图14-3所示的是新增一个数据库的操作截图，接下来逐一说一下各个选项的含义。

业务名称
，这个就是数据库的名字，在14.1.4节的“目录规范”中已经介绍过，每一个实例只能有一个database，并且database的名字和目录的名字一一对应，与平台对应的业务名称也有一定的限制，比如，整个平台的业务名称（数据库名）是唯一的，还有就是数据库名不支持的特殊符号，我们在平台上也是做了限制的。

数据库版本
，这个是可以自己选择的，前提是需要我们在后台把相应的版本模板部署好，然后在平台上录入版本号，接下来新建实例的时候就可以选择你的新版本了，个人觉得这个功能很实用，只要你把模板部署好，无论是Percona还是MariaDB，还是官方的都可以安装。

Master机器IP
，就是这组实例的主库所在的DB服务器IP，需要自己选择，底层的DB服务器有几十台甚至上百台，每台的配置也不尽相同，所以选择一台合适的DB服务器也是一个体力活，我们正在考虑优化，比如平台可以动态推荐给我们低负载的Top5供我们选择，而不是在茫茫IP列表里去选择合适的DB服务器。

图14-3　新增数据库

端口
，这个是平台全局把控的，并且整个平台全局唯一，所有端口都是平台自己生成的，而且不可以修改，包括Master机器端口、LVS映射端口、Redir映射端口。除此之外，我们还规定，一个实例用一个端口，相信大家从图14-3中已经看出来了。

LVS宿主机
，在上面功能模块介绍的小节中，已经介绍过了LVS的内容，我们本着不把所有鸡蛋放在一个篮子中的原则，一个平台有多套LVS环境，这个地方就是让你选择你这组实例要选用哪组LVS，这里选择的IP是LVS调度Master的IP。

Slave机器IP
，就是选择从库所在的DB服务器，和Master机器IP的功能一样。

是否开启主从分离，如果这个库的读压力很大，业务方要求开通读写分离，那么需要把这个勾选上，部署完成之后，LVS的读VIP就会转发到从库（默认不开启读写分离的话，LVS的写VIP和读VIP都会转发到主库）。另外，读写分离功能需要程序支持，我们会为业务方提供两个地址，一个写地址，一个读地址，估计很多人又要疑惑了，市面上有很多开源的DBproxy都支持读写分离，为什么你们不用呢。首先，我个人觉得在程序上实现读写分离是最靠谱的方案了，没有之一，前提是你得想办法让开发改代码支持读写分离；其次，我们也在测试调研各种开源的DBproxy，后期可能会让平台自身支持读写分离。

冷备机器IP
，就是上面介绍的DB服务器的另一类备份服务器，冷备份服务器上的从库不提供业务，只是用来备份，想必大家都会说：“哇，你们好土豪啊，还有专门的备份从库”，好吧，我只能哈哈了。

新增从库
，如图14-4，线上zabbix这个库现在压力比较大，主库已经不能承受住所有的读写请求，现在需要扩展一个从库，实例名，选择zabbix这个库；新从库的IP，选择这个从库需要放在哪个DB服务器上。

图14-4　扩容从库

关于新添加的从库是否加入LVS只读列表中这个选项，我重点说明一下。

首先，我们加从库的原理是把备份服务器的从库停掉，将文件拉到你所选择的新从库的IP服务器上启动。

然后在主库上加一个主从同步账号，授权给新从库的IP，这时，新的从库会自动连主库（主从同步状态，两个yes），然后启动备份服务器上的实例，如果新的从库不能启动或主从同步状态不好，那么平台返回添加从库失败，反之，平台返回添加从库成功。

注意

这里需要注意的是平台不会去判断新加的从库是否有延时，并且新加的从库也不会有账号信息，所以，如果之前没有开启过读写分离的库，现在要加一个从库开启读写分离的话，我们就可以勾选此选项，待从库的账号添加完成，确认新加的从库没有延时，最后把LVS读的地址提供给业务方；如果线上之前已经有一个库开启了读写分离，现在读的压力大，那就需要扩展一个从库，这样的场景下这个选项一定不能勾选。

2.迁入

迁入，就是把平台之外的库迁入到平台管理中，这个平台一上线，我们就经历了一次大迁移，把将近500多个实例迁移到了这个平台上，历时半年之久，如图14-5所示，其他选项和新增实例一样，这里就不再阐述了，下面重点说一下备份地址这个选项。

从图14-5中不难看出，这是一个rsync的路径，1.1.1.10是rsync服务端机器的IP，MySQL是rsync的模块名称，navy_db_2是我们要迁移的这个库的目录，navy_db_2是一份可用的从库的文件，什么是可用的从库文件，关于这点估计很多人又会有疑惑，下面我用具体的例子来说明一下吧。

图14-5　迁入

例如，需要将navy_db_2这个库迁移到平台上，那么现在需要对这个库按照我们相关的安装配置规范做一个从库，放在1.1.1.10上，然后给1.1.1.10上配置rsync服务端，模块名是MySQL，路径指定为/data/mysql/，然后再把这个从库停掉，那么/data/mysql/navy_db_2就是一份可用的从库文件，然后就可以在平台上迁入了，迁入完成后，需要在navy_db_2这个主库上添加一个主从同步的账号授权给平台上的主库，现在就是A（平台外的主库）←B（平台上的主库）←C（平台上的从库）这样的同步关系了，待程序把程序的读写切换到平台提供的地址上时，B和A的同步关系断开，就完成了navy_db_2这个库的迁移。程序从老地址切换到新地址，中间有很多细节问题，在这里就不做过多的说明了。

3.调整

如图14-6，调整这个模块包括从库的下线迁移、主库的下线迁移和冷备库的迁移这些内容，从图14-6可知，冷备库只能迁移和不能下线内容，是的，我们规定过，只要是线上的实例必须要有冷备库，因为备份操作就是在冷备库上进行的。

图14-6　DB实例调整

（1）下线从库

下线从库，如图14-6所示，选择你需要下线的从库，点击删除即可，然后平台会把这个从库停掉，并且在目录上加上当前时间戳重命名，而非直接删除。

（2）迁移从库

如图14-7所示，大概的迁移流程步骤如下。

图14-7　迁移从库

步骤1：选择待迁移的从库。

步骤2：选择新的从库所在的DB服务器IP。

步骤3：停冷备库实例。

步骤4：推送冷备库实例文件到新的从库服务器。

步骤5：启动冷备库。

步骤6：启动新的从库。

步骤7：主库上授权主从同步账号给新的从库IP。

步骤8：判断主从一致性。

步骤9：判断老的从库是否在LVS读的VIP下面，如果在则剔除。

步骤10：关闭老的从库。

步骤11：在主库上取消对老库的主从同步授权。

步骤12：迁移完成。

（3）迁移主库

迁移主库即计划内的迁移，比如主库所在的DB服务器需要关机维护，这个时候就需要做计划内的迁移了，当然，我们这种迁移方式是需要业务中断的，不过时间很短，分钟级别的，大概流程如下。

步骤1：选择待迁移的主库点击迁移跳至图14-8。

图14-8　迁移主库

步骤2：选择新主库的DB服务器IP。

步骤3：判断所有从库和主库的主从状态和一致性。

步骤4：停止备份服务器的实例。

步骤5：推送冷备服务器的数据到新主库的IP。

步骤6：启动备份服务器的实例。

步骤7：启动新主库的实例。

步骤8：老的主库添加主从同步账号，授权给新的主库。

步骤9：判断主从同步数据一致性。

步骤10：把老主库的账号权限添加到新的主库。

步骤11：在新主库上添加老的从库的主从同步账号。

步骤12：删除LVS、Redir到老主库的映射。

步骤13：判断主从同步数据一致性。

步骤14：停止新的从库，删除master.info文件。

步骤15：启动新的主库。

步骤16：把老的从库change master to到新的主库。

步骤17：判断主从同步数据一致性。

步骤18：添加LVS、Redir到老主库的映射。

步骤19：停掉老的主库并重命名命令。

步骤20：迁移完成。

（4）强制迁移主库

强制迁移主库即计划外的迁移，比如一个库突然挂掉了或底层的DB服务器硬件出问题了，短时间内无法恢复。我们线上真出现过服务器主板挂掉的情况，当时就用到了强制迁移主库的功能，如图14-9所示，大概迁移流程如下。

图14-9　强制迁移主库

在说流程之前首先再次强调一下备份从库的概念，备份从库就是运行在冷备服务器上的从库，不提供业务，只是用于备份，所有的备份操作都是在这个从库上进行的。

步骤1：判断从库和备份从库同步主库的位置点哪个最新（Relay_Master_Log_File、Exec_Master_Log_Pos），结果有3种情况，备份从库和普通从库同步主库的点是一样的、备份从库同步主库的点最新、其中一个普通从库同步主库的点最新。

步骤2：如果步骤1的结果是备份从库和普通从库同步主库的点是一样的，那么随机找一个从库提升为主库，对备份从库和其他普通从库执行如下的命令重新指到新的主库。

change master to
master_host='master_ip',
master_user='slave',
master_password='123456',
master_log_file='主库

binlog的位置

',
master_log_pos=主库

binlog的

pos点

;

步骤3：把故障主库的账号权限添加的新的主库上。

步骤4：把故障主库的LVS、Redir映射删掉，并映射到新的主库上。

步骤5：如果步骤1的结果是备份从库同步主库的点最新，那么把其他所有的普通从库都停掉并且把目录重命名带上时间戳，用备份从库的数据重新建立一组主从环境，随机选择一个旧从库的地址作为新的主库。

步骤6：把故障主库的LVS、Redir映射删掉，并映射到新的主库上。

步骤7：如果步骤1的结果是其中一个普通从库同步主库的点最新，那么就以这份从库的数据为基础，重新做备份从库。

步骤8：把这个从库提升为主库。

步骤9：把故障主库的LVS、Redir映射删掉，并映射到新的主库。

步骤10：迁移完成。

（5）迁移冷备库

这个功能就是迁移备份从库，比如备份DB服务器磁盘或IO快到瓶颈了，我们需要把一部分库迁移到其他备份DB服务器上时，就使用这个功能。这个功能也很简单，就是把当前的数据库实例停止推送到新的备份DB服务器上启动，然后把老的目录加上时间戳重命名就完成了迁移，如图14-10所示。

图14-10　迁移冷备库

（6）下线主库

下线主库会把从库、冷备库还有其他对应的资源都做下线处理，当然，这个下线只是下线而已，不做删除操作，如图14-11所示的，是操作日志，我们可以看到一些具体的信息，这些信息都是入库的，方便后续的资源回收分析及审计使用。

图14-11　下线主库

4.权限管理

这部分的主库功能包括新加账号、删除账号、修改密码、修改权限等，有了这个功能再也不用登录MySQL操作了，是不是很方便呢，下面就为大家依次曝光功能点，如图14-12所示：

图14-12　权限管理

（1）新增账号

新增账号，搜索你要加账号的库，比如我们需要给zabbix这个库加一个账号，单击图14-12中的“新增”按钮进入到图14-13，主机地址就是你需要给哪个DB服务器的库添加账号；授权用户就是你要加的账号名称；授权地址就是你授权哪个IP可以通过这个账号访问这个库；权限就是你需要开什么样的权限给这个账号，这里我们把MySQL常用的权限都放在列表里面，你只需要把你需要开放的权限从左边拖到右边即可；数据库就是你授权这个账号访问哪个库；表就是你要授权哪个表给这个账号。

图14-13　新增授权

（2）修改已有账号密码

比如我们想修改刚才新的账号navy_sel的密码，那么我们单击图14-12中的“修改：密码按钮跳转至图14-14，输入授权账号navy_sel，输入授权地址8.8.8.8，输入新的密码，单击”确认修改“，即完成了密码修改。

图14-14　修改密码

（3）修改权限

比如刚才开通的navy_sel这个账号需要create权限，那么我们单击图14-12中的“修改”按钮跳转至图14-15，从权限列表里面把create权限拖至右边单击“确认修改”，即完成了权限修改，立即生效。

图14-15　修改权限

14.3.2　日志部分

本节主要是为大家介绍一下这个平台与日志相关的功能，包括3个模块：平台操作日志、数据库账号管理日志和mysqld.log日志，当然还有慢查询日志，不过，慢查询日志分析是另外一个平台，后期会集成在这个平台上面，后面的14-4会讲到。

1.平台操作日志

平台操作日志这个日志模块会记录新建数据库、删除数据库、修改LVS映射、下线数据库、迁移数据库、平台上新增账号、删除账号等一系列操作相关的日志，如图14-16所示：

图14-16　平台日志

单击图14-16中的“详情”还可以看到具体操作的内容，如图14-17所示：

图14-17　新建实例日志

2.数据库账号管理日志

数据库账号管理这个日志模块会记录通过平台向数据库中添加账号、删除账号的详细信息，如图14-18所示：

图14-18　权限管理日志

3.mysqld.log日志

mysld.log这个日志模块会展示线上所有数据库实例最近10分钟的mysqld.log，后台是每10分钟切割一次线上数据库实例的mysqld.log并把数据归档供前端展示，有了这个功能后再也不需要登录服务器查看MySQL的错误日志了，如图14-19所示：

图14-19　mysqld错误日志

选中你想要查看实例的“详细LOGS”查看内容，如图14-20所示：

14.3.3　资产部分

资产主要分为两个部分，第一部分是实例信息的一些基本信息，比如，库的负责人、负责人的联系方式、用途、版本、IDC等；第二部分是平台里面所有的DB服务器列表。

图14-20　mysqld错误日志详细内容

1.实例列表

如图14-21所示的就是实例信息的展示，支持模糊搜索，你可以根据实例名、负责人、IDC等一些信息进行搜索，还可对一些基础信息做变更，比如有一个库的用途和负责人都发生了变更，那么你可以单击“编辑”做相应的变更。

图14-21　实例列表

2.服务器列表

如图14-22所示的就是服务器列表的展示，这里只展示DB服务器，也就是运行MySQL实例的服务器，包括冷备服务器，关于“用途”这一列解释一下，就是记录一下这个服务器运行的具体是线上MySQL实例还是备份MySQL实例，还有一些服务器是PCIE-SSD卡的，都记录在“用途”这一列里。

图14-22　服务器列表

14.3.4　信息展示

信息展示这个模块主要是以一个全局的视野展现整个平台的相关信息，包括3个部分：实例信息关系、主机实例信息、数据权限信息。这个模块也是我们DBA日常工作经常会用到的，比如查看一下某个数据库有哪些账号，哪些服务器可以连到这个数据库，它的LVS映射地址是多少；主机实例信息可以查看一个服务器上有多少个MySQL实例、多少主库、多少从库等。

1.实例信息关系

实例信息关系展示了每组实例的实例信息关系（主库、从库、备份库），LVS映射关系，Redir映射关系，账号相关，等等，可以下拉选择你想查看的库，也可以通过实例名或端口搜索，业务人员申请数据库时，我们就是从这里提取相关信息给他们的，如图14-23所示：

图14-23　实例信息关系

2.主机实例信息

主机实例信息展示了每个DB服务器上有多少数据库实例，并且可以看到有多少主库，多少从库，当一个服务器发生了物理故障，我们需要迁移切换故障服务器上的所有MySQL实例，原则是先主库后从库，这个时候这个功能就很有用了，如图14-24所示：

图14-24　主机实例信息

3.数据库权限信息

数据库权限信息这个模块展示了整个平台所有的DB服务器对应的账号信息，可以查看某个账号在哪个DB服务器上有授权，还可以根据实例名查看该实例下有哪些账号，等等，如图14-25所示：

图14-25　权限信息

14.3.5　入口（LVS/Redir）

本节主要是介绍入口相关的功能，LVS是程序访问DB的入口，Redir是办公网访问DB的NAT映射入口，也就是说我们的真实DB地址是不对外暴露的，下面来具体介绍入口的相关功能。

1.LVS

LVS是整个数据库平台的入口，其重要性不言而喻，所以我们会有多套LVS（一套LVS有一个主节点和备节点），新建数据库的时候可以选择用哪套LVS，从这里可以查看平台有多少套LVS，哪些库用的哪套LVS，对应的读写VIP是多少，还可以停止、打开某一条LVS转发，还有就是可以把某个库的LVS转发从一套LVS迁移到另一套LVS，等等。

如图14-26所示，从“查看列表”选项区域可以临时断开一个映射，比如zabbix这个库配置了读写分离，而其中有一个从库有延时，我们需要从线上将其剔除，这时就可以选择从库的LVS映射，单击“open”按钮即可停止LVS转发；迁移LVS映射这个功能是为了防止一套LVS的主节点和备份节点全部不可用而设计的，不过这种情况发生的概率很低，除非是整个IDC挂掉或内网的出口交换机出现了问题。

图14-26　LVS管理

一组数据库，新建的时候如果没有开启读写分离的功能，LVS的读VIP默认是转发到主库上的，如果后续需要开启读写分离，则可以从“只读角色修改”这里添加从库，然后把主库从只读列表中删除，当然你也可以不删，这样主库除了承担写业务，也分流了一半的读业务，如图14-27所示：

图14-27　只读角色修改

LVS主机信息就是管理LVS调度主机的，从这里可以新增、删除一组LVS调度，添加到平台之前，需要把LVS+keepalived的基础环境配置好，平台不负责这部分工作，并且也不去判断你的环境是否正常，也就是说LVS的环境需要你通过其他方法来保证它的可用性，如图14-28所示：

图14-28　LVS主机信息

2.Redir

相对于LVS，Redir这块的功能比较简单，默认平台只会给主库做一条Redir映射，即平台上有多少组数据库实例，就会有多少条Redir映射，这些映射关系不能修改也不能删除，只有当主库迁移或下线时，Redir映射才会随着迁移或下线。如果只是给主库做一条Redir映射，那么问题来了，如果是从办公网连接从库或是合作厂商有连接从库的需求怎么办，在这里我们也想到了这个问题，并且也做了新增Redir映射的功能。

业务名称就是库名，即实例名，映射主机就是需要映射的从库所在的DB服务器的IP，映射端口就是这个从库的端口，Redir主机的地址在平台上是固定的，因为它只是提供一个nat功能，基本上没有性能瓶颈，在可用性方面也是做的双机，底层有两台Redir主机，对外提供一个VIP，Redir映射端口就是对外提供的端口，如图14-29所示：

图14-29　Redir映射

14.4　后期功能展望

至此，整个平台建设相关的内容和平台的功能已经基本介绍完毕，下面来说一下后期关于平台的重点规划。

1.集成慢日志分析平台

关于MySQL的慢日志，我们是用Flume+Elasticsearch+Kafka编写的一个分析展示平台，后期会集成到数据库平台，并开放给开发人员，对慢sql最多的库进行全公司排名之类的活动，促进开发人员主动优化sql。

2.账号统一认证

目前平台登录所用的是一个本地账户，并且没有权限控制，相当于一个admin，只有得到账号密码才可以登录，非常的不安全，后期会接入公司的ksso认证，登录必须通过ksso认证，不同的账号拥有不同的权限，比如DBA登录后可以查看和操作，而开发人员登录后只能看到自己所负责的库的相关信息，不可以做其他的任何操作，等等。

3.重写主库迁移逻辑（使用MHA）

上文已经介绍过我们的平台在故障切换和主库迁移方面的相关内容，大家也已经看到了，流程比较复杂并且过度依赖平台，库越大迁移完成的时间就越久，迁移一个T级别的库基本上需要一个小时以上的时间，简直无法接受，后期我们打算把底层故障切换这块结合MHA重新实现。之前也大概了解过MHA，MHA故障切换是可以通过漂VIP来实现的，即将VIP绑定在主库上，当主库不可用时，MHA启动切换策略，选取一个从库提升为主库，并尽可能地通过SSH获取补全故障主库的binlog，然后把VIP漂移到新的主库上，这与我们平台现在的单VIP、多端口这种架构的差别有点大，如何把MHA和平台现有的架构很好地结合起来，对我们来说是一个很大的挑战。

14.5　小结

到此，本章已基本完结，本文介绍了我公司MySQL的一些规范，从安装到配置，再到权限管理，也介绍了我们自动化、平台化所走过的路和一些心得，有很多朋友都问过我，你们的数据库平台是怎么做的，都有什么功能等问题。我个人觉得，每家公司的情况均不一样，你是DBA，你日常工作都需要做什么，你希望有一个什么样的平台来解决你的哪些痛点，自己的需求只有自己最清楚，当你把你的需求梳理清楚，然后结合公司的实际使用情况，比如架构、规范等，你才能知道你需要的究竟是什么了。本人水平有限，文中对于知识点的解释及观点如有不正确或不合理的地方，在此虚心接受广大读者的建议或意见，谢谢。

附录A　求职者与面试官

作者简介

赵旻，RHCA/RHCSS/MCITP，熟悉x86平台基础架构系统的建设、管理及运维工作，现就职于京东金融——网银在线，担任支付产品技术部高级系统工程师一职；9年以上互联网金融、电信、政府等多领域背景的从业资历，千万级大型项目经验，优秀的文档撰写能力及沟通技巧；曾参与中国国家电子政务多项重点工程的安全信任体系建设工作，曾为中移动、国航等大型企业提供运维技术支持；喜欢从事新技术研究、优化方案等工作。工作前倾向于制定有计划且详细的执行方案，擅长处理远期规划设计类事件。善于在工作实践中分析问题、总结经验，属于那种改进优化能力类型的工作者。致力于对IT技术的精研和业务分析，帮助企业定制更加实际高效的解决方案。

我曾在两家公司担任过面试官一职，在三年多的面试经历中参与过上百场的候选人面试。与传统的HR面试经类型的文章不同，我将以求职者和面试官的双重身份，以双方互视的独特视角，在为读者深度剖析问题的同时，与读者分享自己对职业发展和面试求职的一些观点与心得。

每个企业都渴望招到优秀的人才，而进入一家知名公司工作也是每个求职者追寻的目标。然而理想和现实似乎总是有着很大的落差，一方面，企业找不到合适的人选，另一方面求职者也总是在面试当中失利。这究竟是什么原因呢？我想这个问题是值得大家去反思的。多年来我以求职者或面试官的身份参加过很多知名企业的面试，在两家公司有过大约三年的面试官经历。我虽然不是HR，无法对面试进行专业的透彻分析，但却可以从技术人员的别样角度去观察这两种关系微妙的角色。

我是一个善于在工作实践中分析问题、总结经验的人，属于那种改进优化能力类型的工作者。多年来我一直保持着一个好习惯，那就是每完成一项工作之后，都会反思和自省。每一轮面试下来，我都会回忆每一个环节的全过程，去思索哪里值得肯定，哪里做得不好。

那么作为求职者，如何做好职业规划？如何撰写一份吸引人的简历呢？哪些问题会导致面试失败呢？是否做好了加入一个新团队的准备？作为面试官，如何发布招聘启事，如何识别合适的求职者？面试官又是否树立了正确的面试观？哪些糟糕的表现会引起求职者的反感与抵触情绪呢？

正所谓“不知庐山真面目，只缘身在此山中”。只有跳出圈外，站在对方的角度考虑，才能发现很多意想不到的细节问题。当然凡事都有两面性，人们对世界的认知和理解，都深受自身生存环境与经历的影响，不可能产生完全一致的观点。所以在这里我想强调两点：第一，这不是一篇教程，没有对错之分；第二，本文所讲述的案例均来自于多个不同场景的描述，不针对任何企业或个人，请广大读者切勿对号入座。

A.1　求职者篇

A.1.1　职业规划从入职开始

说到职业规划，可能是很多人非常困惑的一个问题。很多人其实都缺乏这方面的意识，根本没有认真地考虑过自己的职业规划。他们希望参考别人的意见来确定自己未来要走的道路，或者认为应当先发展几年后才能定位，这些观念都是错误的。职业规划是一项自始至终都在进行的动态工作，在进入职场前就需要开始准备了。对于普通人来讲，职业收入是你的重要经济来源，关系到生活尊严与家庭幸福。怎么能够不认真对待？怎么能够依赖别人来指明道路呢？职业发展和婚姻同等重要，都是人生大事，存亡之道，不可不察啊。

传统相声《小神仙》里面有这样一个片段：讲的是一个人丢了东西，找了一位失明的先生给他占算，祈求能够找回失物。逗哏的最后就讽刺说，“你睁着眼睛都找不着，他能给你找得回来么？”虽说是个笑话，但在生活中，此类现象却比比皆是。我发现很多刚入行的新人总喜欢问这样的问题：A职业和B职业，我到底应该选择哪一个？

作为新人，对职业的选择有困惑是正常的，但不应该盲目地跟从他人的建议，更不可让他人为你设计什么发展道路。正确的做法应当是：先向业内人士了解每个行业的发展趋势、职业特点和技能要求，再结合自身的性格和优缺点来选择发展道路
。请记住，不管走什么路都一定要自己去决断。因为只有你最了解自己。如果你迷惘到连自己都不了解，那么别人指出的道路又怎么能够确保就一定是正确的呢？一句话，你自己就是你最好的职业规划师。

当然只具备意识是远远不够的，还需要明确的计划和切实的行动。

有一个小伙子参加我的面试，基础知识很差。对于新人，我并不是十分计较技术方面的落差。我问了他一个问题：如果你加入团队，愿意做多久？有什么职业规划？他满怀激情地告诉我，要在一年内成为团队的核心，五年内成为架构师，第十年成为技术总监。我继续提问：团队核心需要掌握哪些技能？你打算如何实现？此时，他沉默了。从他脸上不自信的表情可以看得出，他对自己的目标定位和实现方法根本就不了解。职业规划不是喊口号，和商业企划一样，要有详尽的方案才行。这项工作至少要分成这几个部分：定位目标、找出差距、设定计划、付诸实施。

俗话说“男怕入错行，女怕嫁错郎”。制定一个长远的职业发展方向是非常有必要的。请注意，这里只是确定职业发展方向，不需要过于具体化。我从事IT行业近十年，在这个过程中始终思考着自己的职业发展，分析自己的不足之处并加以改进，不断地修正自己所走过的道路。就像松下幸之助的250年计划无法帮助松下走出困境一样，我们同样也不可能一开始就看得那么远，那么准确。只有随着经验和阅历的不断提升，你才能够深入地认知自己，正确地解读行业。这时候，位于你前方的发展道路才会逐渐地清晰起来。

在确立了大方向之后，你还需要制定几个短期的目标作为支撑。不用担心它们和你的终极目标是否匹配。因为以你现有的能力距离实现终极目标的要求还差得很远，所以不管做什么都不会是无用功。比如你想到南极洲去看企鹅，不管你是造飞机舰船，还是买羽绒服，或者干脆去挣钱，其实都是对的。当你完成了这些短期目标之后，就可以依靠这些新技能去进一步发展。短期目标的制定一定要切实可行，目标太多或难度太大都是错误的。

然后你要搞清楚，现有条件与目标实现所需要的条件这两者之间的差距是什么？正所谓“知己知彼，百战不殆”。你需要制定实现目标的详细计划、时间进度表和最终验收标准。你可以不断地对目标进行细化和分解。使用“脑图软件”是一个不错的选择，它可以帮助你把一个目标拆解成多个小任务。你可以计划每周去完成一个小任务。关于时间进度表可以使用微软的Project软件去做WBS（Work Breakdown Structure，工作分解结构）。对于新人，可以求助于有经验的前辈帮助你进行完善。

最后就是Just do it——付诸行动。在这个过程中，你要不断地审视工作进度和执行效果，有问题及时修正。所谓做比说强，聊胜于无。相比那些夸夸其谈的“立志人”，我更欣赏行动迅速的“践行者”。要知道，事情只要做起来，那么你就已经开始在通往成功的道路上前行了。

A.1.2　如何写简历

作为面试官，第一步工作就是简历筛选。一般我会花费两分钟左右的时间去阅读一份简历并加以评估。下面这种简历的内容形式和排列顺序是我比较推荐的。

·求职意向。

·个人基本信息（涵盖从业时间，行业背景的描述）。

·个人简介（涵盖对技术、管理、优势等情况的高度汇总）。

·工作经历（时间按照倒序排列，将主要的工作业绩以数据的形式体现出来）。

一般求职网站在填写工作经历时，都是以公司作为划分单位的，如图A-1所示。我认为这种划分方式过于粗犷。因为你在这家公司的所有业绩都只能填写在同一个表单之中，而表单的字数是有限制的。有些求职者长期效力于一家公司，成绩斐然。如果因为受到字数限制而影响了自身展示那将是不公平的。另外这种方式缺乏细粒度的时间划分，只有工作量没有时间点，也是很大的弊病。因为这些成绩也许都是前期产生的，而后期可能只是业绩平平，所以很难反映出求职者真正的工作状态。在众多求职网站当中，我最喜欢的就是猎聘网。猎聘网在创建简历时，针对每一家公司，会有一个“添加本公司其他任职”的按钮，可以根据时间和职位进行二次分类，如图A-2所示。这就很好地解决了上述问题。如果求职者在这家公司有职位晋升的情况，还可以据此观察求职者的职业发展历程。目前很少有求职网站效仿这种做法，该项功能对于在一家公司服务期超过五年的求职者来说是非常有必要的。

图A-1　其他网站的模板

图A-2　猎聘网的模板

A.1.3　面试前的观察与考量

首先我要在这里纠正一个错误的观点。面试不是企业对求职者单方面的考核，而是一次彼此都要接受检验的双向选择。当然，我们不得不承认，在面试中求职者往往处于不对等的弱势一方。但求职者在收到offer之后，同样也有反向选择的权利。然而是否要加入一家公司，也许不一定要在收到offer之后才决定，在一开始面试之前，你就可以有所观察。

不管这家公司的知名度如何，你最好都要先去了解一下它的背景和基本情况，包括行业地位、发展趋势、企业文化、业界评论等。另外，办公地点也要事先了解清楚。我个人认为如果有可能的话，最好还是能够进一家大中型企业。一个刚入行的新人就像一张白纸，第一家公司会对他们产生比较大的影响。大公司做事情相对比较规范，对新人的影响还是比较积极的。其次也可以考虑创业公司，相对于小公司，创业公司也许能学到更多的东西。但是关于创业公司，我只建议工作不足两年的新人去考虑。这个问题我会在后文中详细分析。总而言之，我们更愿意投身于一个做事规范，积极向上，有发展前途的公司。

在正式进入面试环节之前，你最好留意一下这家公司的办公环境、前台员工的职业形象及待人接物，等等。如果路过办公室，还可以观察一下公司员工的工作状态。他们是快乐奋进的？是疲惫痛苦的？还是百无聊赖的？员工的形象和工作状态是我判断一家公司优劣的基本依据。

另外，我会计算一下我的等待时间。因为等待时间是和重视程度成反比的。迟到是求职者的大忌，但有些面试官反而却不够守时，让候选人等待很久，实在是非常的不妥。虽然运维是个比较特殊的行业，但既然是事先约好的会晤就应该做好安排。除非真有特殊情况，否则没有理由让候选人等太久。如果技术环节的面试官迟到并告知你是因为在处理突发故障，那么有经验的候选人会就此发现一个问题，要面试官去处理临时故障，可能存在两种原因：①这个岗位严重缺人，目前可能只有这位面试官一个人；②这个团队的技术能力有问题，其他成员无法很好地胜任这个岗位的工作，表明团队成员技术落差大，人员存在单点。接下来你可能就要多留心了。你一定要搞清楚这种情况已经持续了多久。如果长期没有得到改善，则说明加入这个团队将会有很大的风险。

在反向提问的环节，求职者还应该更多地了解工作环境、团队情况、职位发展等问题。例如可以提出如下一些问题：

·请问我们最近两年的发展目标是什么？要做到怎样的程度？

·我能了解一下我们团队近两年的发展规模是怎样的么？未来我们还需要哪些成员加盟？

·请问您认为目前我们团队所面临的最大需求是什么？

·请问您最欣赏的员工应该是怎样的？

·请问目前我应聘的这个岗位未来三年的发展方向是什么？如果该岗位能够进一步发展，还需要哪些要求呢？

A.1.4　候选人为何面试失利

1.搞不清状况

作为面试官，我遇到过两次候选人向我询问薪酬问题的情况。首先我只负责技术考核，后面还有总监和HR的环节。和我谈薪水既不合时宜，恐怕也找错了对象。尤其是在候选人还没有获得认可的情况下，此举无异于自杀。几乎所有的面试官，尤其是HR的MM对此是非常反感的。我想提示所有的求职者：如果面试官在交谈中主动和你谈及薪酬问题，说明你已经成功了90%，但要是你自己率先触碰这个敏感话题，那么肯定不会有什么好结果。

2.狐假虎威的个人介绍

这类候选人在描述工作经历时总喜欢把主语设定成“我们”。例如，我们团队在某项目中开发了××软件，建设了××平台，完成了××工作等。请注意，这是面试而非招标。我希望了解候选人究竟做出了哪些成绩，而不是他的团队和公司。我在研读了一些心理学的书籍之后，通过对面试者面部表情的观察发现：这些候选人，在使用“我们”作为主语时言语激扬，显得比较兴奋。而当他们被要求只描述自己的工作业绩时，也就是把主语更换成“我”时，立刻就会显得底气不足，同时语速下降，眼神也开始变得黯淡。

3.基于私有化产品的工作经历

我经常会在简历里面看到关于私有化产品的描述。对于私有化产品，如果你做的是私有化产品研发，我会觉得很不错。但如果只是私有化产品运维，我希望对方能更加了解底层通用的技术，而非简单的手册操作。

4.长期重复性基础工作，技术深度不足

我见过很多工作经历超过七年的做产品实施类的工程师。他们的确接触过很多主流的软硬件产品，但其水平只停留在ICM层级。ICM这个名词来源于VMware的认证课程，是Install Configuration&Management的缩写。他们的任务就是完成产品安装配置的交付工作。他们接触过的产品非常多，但对其深层次的技术并不十分了解，自身更是缺乏大规模运维的实践经验。我对他们多年来一直只从事着一项重复性的劳动深感忧虑和惋惜。此类工作毫无技术壁垒，很容易就会被用工成本更低的人所接替。

5.动力不足，执行力差

工作意愿和学习动力不足，或者是执行力差的候选人，即便技术环节勉强过关，最终也无法通过部门总监的面试。没有工作热情、缺乏输出效率的人，就像是一只大电阻，白白地消耗着能源，有效功率却很低。聘请这样的人来工作，不但带来不了多少价值，而且这种坏风气还会在团队里传染开来，影响士气。这类候选人通常都是来混日子和寻开心的，他们的工作目的只是为了进入一个带光环的平台或是更换一个安逸的环境而已，我在他们眼中看不到进取的目光，更多的是迷茫和麻木，言谈话语中已经对运维工作没有了激情。

【案例分享】
C君，从业三年，传统行业，小型机工程师，希望到互联网行业发展。

面试官：你接触过哪些开源的产品？

候选人：主要关注过OpenStack的一些技术。

接下来面试官就OpenStack的体系架构及重要模块的工作实现方式等对候选人进行了技术考核，发现对方对此知之甚少。

面试官：你从什么时候有了想进入互联网的想法？

候选人：大概是半年之前有了这样的想法。

面试官：你现在所从事的工作，每天的工作量是多少？

候选人：现在的工作不是很忙碌，每天大概有两三个小时左右的空闲时间。

【案例点评】
到此为止，我想候选人已经把他最后的机会都浪费掉了。足足半年，每天两到三个小时的空余时间，如果认真学习应该也已经小有成绩了，所以我是不太看好那些光说不练的人。

6.可靠性差，根本不值得信任的人

打网游的朋友都知道一句话：“不怕强大的对手，就怕坑人的队友。”就像没人愿意用一个可靠性差的软件一样，与不可靠的人共事最终会毁了整个团队。下面这些人都是团队的大忌。

·做事随意性强、不遵守已有规章制度，工作缺乏规范性的人。

·凡事凭直觉做，缺乏细致分析，考虑问题不周全的人。

·工作不认真，喜欢糊弄，抱有“60分万岁”主义的人。

·责任心差，做事毛躁，对生产环境缺乏敬畏心的人。

·自我意识太强，无法融入团队的人。

·执行力差，工作效率低下，光说不练的人，

·缺乏担当和大局观的人。

·不喜欢做事，整天只会抱怨或到处搬弄是非的人。

【案例分享】
某公司招聘部门经理，目前只有两名候选人。

S君，从业八年，效力过四家公司，现任职某公司部门经理。

公司　任职（年）　担任职务

4th 0.5　部门经理岗　（有带团队经验）

3rd 2.5　高级工程师　（有带团队经验）

2nd 2.5　高级工程师

1st 2.5　中级工程师

L君，从业八年，效力过四家公司，现任职某公司资深工程师。

公司　任职（年）　担任职务

4th 3.5　资深工程师　（有带团队经验）

3rd 2.5　高级工程师　（有带团队经验）

2nd 1.5　中级工程师

1st 0.5　初级工程师

从他们所做过的工作上来看，这两位的技术能力都很强。S君还具备半年左右部门经理的工作经验，而且这两位都是猎头公司推荐的人选，那么作为面试官你会邀请谁参加面试呢？

【案例分析】
最终，S君在简历环节被面试官淘汰了。一个高级职位除了对技术和管理能力有较高的要求以外，更加看重的是候选人的责任心。一个部门的负责人要有担当，缺乏稳定性是不能容忍的。既然是猎头推荐，说明候选人是有离职意向的。这两个人平均一家公司的就职时间都在两年左右，从这一点上来看，应该都是不合格的。

不过L君最近一家就职时间已经三年多了，并且从历史时间上看，离职频率是呈下降趋势的。他之前晋升的速度非常快，说明他的学习能力比较强。而带团队的经验相较S君也更加丰富一些，有一定做管理的基础。综合考虑他几次离职有可能是碰到了职位天花板的缘故，但至少是值得约见的。当然在面试过程中也需要仔细考察衡量该候选人的职业发展意向、岗位稳定性、期望值要求及目前公司所能提供的条件等诸多方面因素。

但是S君就不同了。基本上是两年左右触发一次离职事件。频繁的离职表明了对方缺乏责任感，这种朝三暮四的人难以获得别人的信任。他作为部门机构的负责人，整个团队的领导者，刚刚半年就出现了离职的倾向。综合来看有两种可能：第一种，他目前所处的部门出现了一些棘手的问题，导致团队陷入了困境，个人承受不住压力，考虑离开。第二种，他在面对更好的机会时，就产生了放弃现有团队的想法，缺乏职业道德。不管是哪一种，这种遇到困难或利益就没有了担当的人在进入团队的第一天，就将是灾祸的开始。

【案例点评】
京东创始人刘强东曾经把人分为4种：第一种是金，即能力强又符合公司价值观的人才。第二种是铁，即符合公司价值观但是能力较差的人，通过锤炼锻造可以成为钢。第三种是废铁，这种两者都不具备的人在面试时就会被直接淘汰，不足为虑。最可怕的是铁锈，即能力极强但是不符合公司的价值观。这种人非常危险，因为他们不但难以识别，而且破坏力惊人，不及时清除会腐蚀掉整个团队。

我大概在五六年前也总结过一个类似的观点：“一个人的价值所在是一种矢量的体现，能力大小只决定其长度，但人品决定了最终方向，也决定了价值的正负。”假如在街上有两个恶棍正在持械行凶，一个持刀，另一个持枪，你认为哪一个更危险呢？尽管我是技术环节的面试官，但我始终把技术放在第二位。技术差一点可以学，就像老刘所讲的通过锤炼可以改变。但是如果技术非常强，而人又有问题，那么这种人的技术能力和他即将带来的破坏力是成正比的。

A.1.5　如何突破职业发展瓶颈

应该说，职业发展瓶颈（图A-3）是一个非常普遍的现象。因为优秀的岗位总数量是很少的。很多同行在工作了几年之后，都会出现类似的情况：发现自己在某个岗位上长期无法突破，工作内容没有什么本质上的变化，薪酬待遇也总是停滞不前，职业竞争力开始下降，同时也产生了倦怠的工作情绪。

图A-3　职业发展的瓶颈

那么问题症结在哪里呢？我认为首先要承认自身
能力出现了瓶颈，这也是根本原因。如果你无法认识
到这一点，那么你将在错误的道路上越走越远。很多
人发现薪酬待遇无法提升时，往往会在自身没有做出
改变的情况下，就想通过更换新的工作来改变目前的
窘境，这是一种非常不明智的选择。

这就好比这样一则寓言故事：西村的树林里住着一只猫头鹰。因为人们讨厌它难听的叫声，总是想方设法要赶走它。于是，猫头鹰决定搬到东村去。半路上，它遇到了一只斑鸠。斑鸠问：“你这是要飞去哪里？”猫头鹰叹了口气说：“西村的人嫌我声音难听，都讨厌我，我决定要搬到东村去。”斑鸠听后笑了起来：“老兄，依我看，如果你不能改变你的声音，不管你搬到哪里去，都会是一样的结果。”

当然，我不否认同等岗位的薪酬在行业内是有一定差异的。但如果你认识不到自身能力的不足，不去主动做出改变，转换平台也无益于根本问题的解决。即便上天真的给了你一个高薪的职位，你自身无法承载相应的要求，最终也只能放弃。我想让大家明白一个道理：做事情，改变目标不如改变方式，改变环境不如改变自己
。

图A-3简要概括了职业发展的瓶颈：

我总结了几种类型的人，他们往往很容易就出现职业发展瓶颈。

1.原地踏步型

这是最典型的职业瓶颈的体现。这类人长年累月重复着某一类工作，而且已经形成了一种惯性的工作模式，因为某些原因，他们无法或不愿意做出改变。

2.浅尝辄止型

这也是一类典型的现象，这类人接触过很多领域的知识，但可惜的是对每一样都不够深入。

他们喜欢对每种软硬件产品都进行尝试，当发现用不下去时就尝试着变换另一种产品，对产品的实现方式和体系架构都不甚了解，最终沦为了产品的试用者和宣传员。

3.挖深井型

这一类人钻研的领域往往应用面比较窄，可以说是前一种类型的反向极端。

【案例分享】
我曾经面试过一名在一家互联网企业做硬件测试的工程师，工作经历大约六到七年，一直在从事Bench Mark（IT行业中的Bench Mark是指：依照特定的标准，使用专业的工具，针对同种类型的软硬件产品进行性能检测，最后依据检测结果的比对，对产品的优劣做出评价）的工作，并且负责一个小团队。这位候选人对目前的薪水不够满意，但拒绝了一家知名硬件厂商的邀请。因为他希望继续留在互联网行业并从事原有的工作。可以说他在硬件测试方面确实是个行家，但是他似乎比较排斥学习他认为没用的其他领域的知识。因此他最终也没能通过我们的考核。

我们给出的落选理由如下：第一，Bench Mark可以为服务器采购大单节约可观的资金，而且节省成本的5%就足以支付这位工程师的薪水了，但是Bench Mark的下一个有效周期过长，在接下来一段时间内无法继续产生利益，虽然很重要但完全可以找人兼职完成。第二严重缺乏其他领域的知识，对职业发展产生了不利影响，难以胜任更高的管理或技术岗位。我个人认为这位候选人如果不愿意改变看法的话，拒绝那家厂商的邀请很显然是十分可惜的。

可见，挖深井是有极大风险的。除非你研究的领域是垄断性的或极具市场需求，否则很可能这口深井就是葬送自己职业生涯的坟墓。

职业发展最好是着重一个方向，在不断深入的同时也注意横向知识的积累。当达到一定核心深度时你会发现各领域之间的界限已经开始模糊了。专注一个方向发展并不会影响到你的知识拓展，相反这种学习方式也会使得广度学习更加深入且有针对性。也就是我们常说的T型人才，但我个人更愿意称其为扇形人才。切记不要做“万金油”，什么都会就是什么都不会。

A.1.6　如何实现自身能力的扩展

1.拒绝重复性劳动，日常工作工具化

在这里我认为不适宜讲自动化的概念，我认为现今自动化概念的门槛被降得太低了。不是说写个脚本或程序就能称为自动化运维了。如果把手动操作比作走路的话，自动化应该是无人驾驶的汽车才对。目前我们也只是做到了工具化的级别，最多就是自行车而已，仍旧需要大量的人工干预环节。事实上，能做出尽量减少人工干预的工具化产品，也是一件非常不容易的事情。

2.提升工作效率，多做第二时间象限的事情

我个人认为优秀团队不是看加班多少，而是看工作效率和有多少比例在做第二象限的事情，第二象限的完成目标应该在PKI中占有更高的比例才对，而不是欣欣然地看扩张速度，看团队成员为赶项目熬了多少个通宵。如果一件事情不做好品质，做得再快也只是产生了更多的廉价垃圾而已。

拓展：时间管理四象限的基本知识

第一象限：紧急重要，属于灾难型事务。出现这类事务往往会引发非常严重的后果，需要立即解决。

第二象限：重要不紧急，属于基础架构类的规划设计型事务。该类事务的影响效力是长期的、全局性的，一旦形成决策，不可以随意修改，错误的决策将导致成本不可控的重构工作。如果此类事务被延误，迟早会演变成第一象限事务。

第三象限：紧急不重要，大部分属于取悦别人的服务型事务。通常来说不得不做，但要学会尽快处理完毕。第三象限事务往往大量存在于常态化工作中。因此，在技术层面上要考虑工具化处理，减少人工干预；在管理层面上要考虑优化规范、精简流程。

第四象限：无关紧要，纯属垃圾事务。这些事务往往与工作无关或属于无效冗余，应该最大化地杜绝它们的出现。

3.审视自身目前的缺陷

如果认识不到自身的问题，就无法做出正确的调整，付出再多的努力也是白费的。

4.同伙伴一起成长

在艰难的旅途上行进的时候，如果有志向相同的伙伴便不会感到孤独和困苦。出现问题的时候可以一同探讨解决而不是独自苦思冥想，尽快结识这样的伙伴，有时候比拜一个好师傅更重要。

5.用心做事最重要

做事情有四个层次：做任务、做工作、做产品、做品牌。有些人做事就像挤牙膏，指使一样干一样，多一点儿都不带做的。还有些人做事好像机器人，遇到异常情况，既不会变通也不会提前汇报，继续按照原先的方案做，出了问题反倒责怪别人——“当初你没告诉我应该这么办啊”。与其聘用这样不用心的员工，还不如写个严谨的程序来做呢。要想有所成就，做事情不但要用心，更要有匠心。

欧米茄（Omega）是国际著名制表品牌，由路易士·勃兰特始创于1848年，拥有将近170年的悠久历史。其代表符号“Ω”是希腊文的最后一个字母。象征着事物的伊始与终极，代表了“完美、极致、卓越、成就”的非凡品质。作为世界上最具影响力的腕表品牌，曾缔造多项腕表精度纪录，它最具智慧之处就在于：没有任由工业化泛滥到制表的每一个环节，而是将腕表的最后一道工序保留手工完成，坚守着“拒绝批量生产，秉承手工制作”的瑞士制表业的传统。

但那些同为瑞士机芯的量产表，它们的命运却只能是成为地摊儿货。当前这个讲求高效高收益的社会，给人们带来的是急功近利与浮躁的心态，反而失去了本属于中国优秀传统文化的匠心精神。把事情做到极致、精益求精不过是匠人的最低标准，匠心精神还在于强调以几个下方面

·坚持、专注：无论外界环境如何变化，匠人对做事原则和骨气都有着高度的坚守。

·谦恭、自省：匠人实事求是，谦逊虚心，不自大，不吹嘘。

·敬畏、入魂：匠人对工作有着崇高的职业自豪感与使命感，将自己的生命与灵魂注入到作品当中。

总而言之，缺少匠心的产品就没有艺术和价值，只有真正的匠人才值得人们尊敬。

图A-4概括了如何实现自身能力的扩展：

图A-4　如何实现自身能力的扩展

A.1.7　是否为五斗米折腰

世界很小，圈子也很小。从业多年后，当你在行业内有了一定地位或声望的时候，难免会接到一些猎头或以前共事伙伴的邀请。而当你认为自己完全有能力胜任的时候，是否会考虑更换一个新的环境呢。面对所谓的诱惑，你又是否真的做好了接受的准备呢。

1.什么情况下考虑离开

我可以理解的离职原因只有三个：前途、家庭和工作环境。关于前途一般是团队平台影响到了个人职业发展的时候。前面讲过，职业发展瓶颈主要是基于自身的原因，但也有客观因素存在。比如一只超级球队，连替补都是一等一的球星。身为板凳队员并非不够优秀，而是优秀的人实在是太多了，而上场人数又有限。在职场也是一样，某人本来可以胜任更高的职位，但是已经有人占了位置，这个时候就只能选择离开，像这种情况就称为职位天花板。平台的机制问题（例如绩效考核、管理制度、团队建设等）也是导致人员流失的主要因素，但它与前者带有不可抗力的属性不同，这类问题是可以改善的。因此团队的领导者应当重视机制的建设和改善。

2.何为好的平台，什么样的团队值得加入

1）GDP与实际收益

很多新人刚开始会比较看重自己能学到多少东西，希望能加入那些拥有众多牛人团队的大平台。我想要说明一点的是：牛人其实在每一家公司都是必备的。也可以说这些牛人就是公司最主要的GDP来源，如果一个公司连这些都不具备那就离倒闭不远了。GDP越高，意味着人均GDP（你的成长机会）也就越高。但是大家都懂一个道理：人均GPD并不等于实际收益，它们完全就是两码事。

什么叫做实际收益呢？就是那些你真正能够获得的东西，实际收益不但要看得到，还要摸得着。关键不在于你的团队有多少牛人，而是有多少愿意和你一起分享，一同成长的伙伴。当然如果有一位大牛愿意带你成长带你飞，那当然是再好不过的了。

但是如果你与这些牛人在工作领域中没有任何交集，或者出于各种原因他们无法或是不愿与你分享，光有牛人却学不到东西，则是毫无意义的。那么能不能主动去学呢？没错，具备主动学习性是好的，但是如果团队成员无法自发地去做分享，或者所谓的分享知识只流于形式，那么再主动多半也学不到什么东西。毕竟公司不是学校，没法安排一个专门学习的岗位出来。

2）工作很忙是好事情吗

一个团队不怕忙、不怕累，关键看你在忙什么。大家看下图A-5中的小人儿，左右两边都在跑，但跑的含义却大大不同。一个是在健身，一个是在逃命。如果一个团队每天八小时甚至更多的时间都在处理故障、上线、维护、工单等一系列应急的事情，那怎么可能有时间去做技术提升呢，没有时间做第二象限的事情，迟早都会演变成第一象限的事情。

图A-5　何为好的平台

3）永远向前看，不要向钱看

十年磨一剑，大成就往往需要长时间的磨练才可能有所得。如果目前的工作环境正好有助于你的职业成长，在公司处于一个上升发展的良好阶段，切不可为了眼前的区区小利，急急忙忙地改投别家。否则，几年以后你就会发现，新东家似乎并不是那么的重视你，而自己原有的发展机会也白白地让别人捡了便宜。大家都玩过跳棋吧？往前跳的棋子未必能走多远，但一定会为后面的棋子让开道路。离职有时候看似是自己的机遇，却往往是为他人做了嫁衣。

图A-5简要概括了何为好的平台：

3.高薪水是有代价的

没有金刚钻，别揽瓷器活儿。如果你参加完一个面试后，发现自己胜任这项工作尚有困难，对方却发来了薪水诱人的offer，这时候最好是谨慎接受。

大家去肯德基就餐，会发现单点的东西总是比套餐要贵得多。再有比如可口可乐，330ml的罐装饮料售价大约在2元左右，而2L的大瓶装虽然售价高达6.5元，但容量却是前者的6倍。在职场里也会出现同样的情况。往往收入越高其性价比越低。比如当薪酬是1，此时的要求也是1。而当薪酬是10，此时的要求往往变成了12或13。我将其称为“全家桶现象”。没有一个公司是出来做慈善的，高薪水往往意味着你要为此付出更多的代价。

企业有时候急于求成也难免会看走眼，请来了不合适的人选，结果弄得双方压力都非常的大。行业圈子本来就很小，如果承担不了压力被迫离职是件非常糟糕的事情，传扬出去反倒坏了名声。

4.是否应该投身创业公司

假如你身处荒郊野外，现在这里有一辆越野车、一辆马车、一辆摩托车和一辆自行车，你会选择搭乘哪一种交通工具呢？越野车固然好，但你未必能够驾驭；马车速度虽说慢些，也还算不错；摩托车车速快，但安全系数低，也许能最先到达终点，也有可能一不小心就滚下了山涧；自行车很辛苦，不过总比走着强，先骑着看，遇到更好的可以再换。

很多人都会面临这种选择。大公司不一定进得去，有些人就会在中型企业和创业公司之间犹豫。实现财务自由是创业公司最大的卖点，但选择它的人却不一定做好了失败的准备，也许这是因为他们距离第一次互联网革命的时代太远了。

关于是否加入创业公司这个问题，以下这些话是我要对那些职业发展正处在上升期阶段的同学们讲的。所举的例子，只为讨论分析之用，没有恶意。一家之言，仅供参考。如果你已经具备了高管的实力，或者是初入职场的新人，那么这些观点并不适合你。

1）30%能买断几年的青春？

创业公司的特点是设备飞速扩张，团队人员不整，有大量的初始化工作需要有经验的实施团队来完成，因此有很多事需要你从零做起。但出于成本的考虑，薪酬涨幅往往只有20%~30%。对于那些正处在上升期阶段的同学来说，这意味着要你放弃现有的发展机会，一下倒回解放前。那么重新来过到再次追赶上现有进度，前后需要花费的时间周期有多长？你是否对此进行过评估呢？不说别的，一个60人左右的运维团队只能算中等吧？至少也需要两三年左右的时间才能找到合适的人选。人家用了30%的钱，就买断了你至少两三年的青春。而你有没有想过，两三年以后你和以前小伙伴的差距会有多大？所以，不要单纯地去看薪酬，要从多个维度去权衡利弊。

2）财务自由——不要让赌徒心理作怪

如果说30%不能撼动你的话，财务自由也是一个令人神往的理由啊。不过需要提醒大家的是，创业本身就有一定的风险。但实际上有些人是抱着押宝的心态去创业公司的。有这种想法是非常危险的，这种有着赌徒心态的人不管到哪里都是不会获得成功的。另外，我还要再给大家泼个凉水。宋江和卢俊义上梁山比很多人都要晚，但这两个人却依旧坐了头两把交椅。我的意思是：如果你成为不了团队的高层与核心，恐怕财务自由这句话也不是对你说的。

3）成功没有充要条件

牛人云集也好，资金雄厚也罢，这些都不能成为保本儿保息的有力依据。当年十八家诸侯讨伐董卓，有谁能确保自己的团队肯定就能成就霸业呢？刘备文有卧龙凤雏，武有五虎上将，坐拥西川，最后七十万大军还不是兵败于白帝城吗？猪八戒好吃懒做，工作能力差，还总在团队里散播负能量，但始终没离开取经队伍，最后他也修成了正果。有时候站队就是一种随机选择，真就没什么道理可讲。当下市场竞争，没有谁是绝对安全的。成功是受多种主客观因素共同影响的。首先自己要努力，同时还需要有适当的机遇来辅助，客观条件是不可控的，所以要摆正心态才对。

4）要弄清楚对方是求贤若渴还是求职若渴

要了解清楚，人家到底是需要你这个人，还是你这个角色。如果只是要你来撑场面救火的，一旦人员齐整后，你是否会被后来者居上呢？不要甘当了铺路泥，到时候连铺路石都算不上。

5）愿赌服输也是需要资本的

有一个笑话：有一个人和一只鹦鹉同坐飞机。在飞机上鹦鹉对空姐的表现非常无礼，那个人也跟着鹦鹉学，结果两位都被丢下了飞机。这时候鹦鹉拍着翅膀对那个人说：“不会飞你可牛什么呀。”

我常听人讲：年轻热血赌一把，错了也不后悔，大不了愿赌服输。要知道愿赌服输也要你有资本才能输得起，没有资本输的人是没资格讲这个话的。

你如果年轻又没有家庭负担，那很好，这正是你输得起的资本。这个社会喜欢同情弱者，年轻就是犯错误的资本，大家都能原谅。但当你已过而立之年的时候，社会对你的容忍度就开始降低了。你必须要有面对失败的准备，要有挽回局面的能力。有些路走错了，就再也没法回头了。

我不想剥夺大家的梦想，只是想提醒你们做事情有成功更有失败，如果你输不起最好老实本分一些。

A.2　面试官篇

A.2.1　团队需要什么人

作为一个运维团队，技术诚然是最基本的要求。但我不会把技术排在第一位，而是更看重一个人的综合素质（包括性格、价值观、处事方法、同理心、责任心、认知力、沟通力、洞察力、执行力、学习力，等等）。原因有两个：第一，综合素质与一个人的成长教育环境有关，是受长期影响的结果的累积体现。正所谓江山易改、本性难移，这方面不是短时间就可以轻易变更的。况且企业又不是学校，没有义务和成本去做这种素质教育。第二，运维工作关系着行业生产的正常运行，如果人员素质不达标，技术能力强在产生负面影响的时候只会更加糟糕。第三，有些技术差距是受工作环境所限造成的，综合素质过硬的话，这种技术差距很容易就能弥补。我讲这些并不是为了强调技术不重要，而是提醒大家不要唯技术至上。技术可以学，而综合素质从某种程度上来讲，只能影响却不能培养。因此综合素质良好的人才是可遇而不可求的，一定要珍惜。关于这一点，A.1.4节中，我想我已经讲得够多的了。

一般高级职位更倾向于从业五年以上有经验的人。除了基本技术要求之外，还应当具备如下能力：善于倾听，沟通协调能力好；做事规划性和规范性强；沉稳冷静，分析预见力强，考虑问题周全。

如果是招纳新人，最好能有两年左右的相关经验。需要具备一定的基础知识，另外还有三点要求：执行力强；工作及学习意愿强；做事认真负责。

A.2.2　招聘启事中的问题

和很多虚假简历一样，我也见到过很多前后描述不一致的招聘启事，给人的感觉就是缺乏诚信，无法引起求职者的兴趣。例如有些Title很高的职位（××专家或高级工程师等）对应聘者的要求却简单到连新人都能Cover的程度。有些则是要求过于苛刻，薪资待遇却不敢恭维。还有一些职位对薪资待遇的定位非常随意。例如某专家职位薪酬30万~100万。30万也就是中高级的水平，100万在很多公司都是VP的待遇了。我想求职者在面对这类职位时，对其真实性和真诚性都会秉持不信任的态度。因此HR应当同需求负责人与猎头公司做好有效的沟通，三方应对招聘启事的内容进行仔细的核实。

A.2.3　面试官的修养

还是之前提到过的一个观点。面试不是企业对求职者单方面的考核，而是一次彼此都要接受检验的双向选择。一场面试下来，也许淘汰的是求职者，但可能在求职者心中，面试官和公司的形象也同样被给予了差评。一个公司的品牌形象可能来源于产品和商业操作，而我认为其企业文化形象取决于另外三个出口：前台、外务（销售、项目经理、技术支持、培训师）和HR（包括面试官）。这三者才是还原企业内在本质的真实因素。我认为首先应该树立起一个正确的面试观，才能做好或才有资格担任面试官。有很多的个人细节问题，确实值得反思。这不仅仅是个人的修养之道，更关乎公司的对外形象，其实是有必要对面试官做一些培训的。

1.态度傲慢、自以为是

有些面试官会不自觉地有一种优越感，对候选人不是非常的友好。在交谈的过程中低着头，没有目视候选人，打断别人的讲话，语气冰冷，喜欢迟到等恶劣的行为都会引发候选人的不满。

另外有些公司还喜欢进行压力测试的考核，故意刁难或刺激候选人，然后借以观察对方的反应和心理承受能力。我认为除非岗位有这种职业需求（高层管理人员或销售），否则压力测试实在是有悖常理。如果换位思考的话，你是否愿意接受这类测试呢？

2.穷追猛打、令人难堪

在某一个技术问题上，如果候选人无法回答或答错了，面试官可以稍作提示，如果提示没有效果的话，就应该转入下一个问题。此时胜负已分，就该点到为止。穷追猛打不会显得你技高一筹，反而会令候选人更加紧张和不悦，正所谓得饶人处且饶人嘛。

3.闲聊式的面试

有些面试官实在是缺乏时间观念，面试没有主题，东拉西扯，想起什么问什么，完全是漫无目的的闲聊。和候选人一样，在面试之前，面试官也应当做好充足的准备功课，问问题应做到有的放矢。

4.马拉松式的面试

不要把面试弄成车轮战，更不要搞什么初试复试之类的。这又不是大奖赛，让候选人反复跑多趟是不合适的。一般职位的面试最好能够一次就完成，由技术、部门领导和HR三轮组成就够了。这样下来，可以把时间控制在两个小时左右。时间过长，人会感到疲惫，状态也会下降。顺便透露一个小秘密：作为一般职位，80%的面试结果其实当场就已经决定了。如果你见到HR并谈到了薪水，基本上就成功90%了。面试结束后你很快就能收到回复。相反的，如果没有见到HR或部门领导，那就是没戏了。除非明确是多场次的面试，否则那种被告知回去等消息的话，就是一种委婉的拒绝。

A.2.4　做讨人喜欢的面试官

俗话说“买卖不成仁义在”。不管结果如何，我都不希望面试过程给候选人留下不愉快的回忆。一个面试官最不愿意看到的就是，候选人在网站上的各种面试吐槽。也许面试时他们是弱势一方无法表现出自己的愤怒，可是到了网上，那些负面情绪就会全部倾泻出来。他们不仅是在表达不满，甚至还产生了对这家企业形象的敌视。对于这点我希望所有的面试官都能重视起来，做讨人喜欢的面试官。

1.避免令人抓狂的问题

1）手里拿着求职者的简历要求对方做自我介绍

我实在是找不到问这个问题的充分理由。是没时间看简历？还是想核实简历的真实性？我想可能是面试官自己都不知道该说什么好，为了打开尴尬的局面并找到切入点才问的吧？我是不喜欢问这个问题的，最好还是比较体贴地留给HR的MM吧。如果这个问题让每个环节的面试官都问上一遍，我想候选人一定会疯掉的。

2）为什么离职

这一点马云先生已经总结得很精辟了，我再补充两个比较合理的理由就是家庭原因和工作环境。除此之外，我想不到更加有说服力的理由了。所以这个问题本质上没有任何意义，稍有经验的候选人都会很有针对性的做这方面的准备，最终你得到的也只是千篇一律的巧妙说辞罢了，并不一定是对方的真实想法。

3）对我们公司了解吗

我会觉得这样的开场白真的很尴尬。即便是公司员工也不见得就了解公司的全部。有些企业文化会讲，对公司都不了解就不该来面试。但是面试官是不是因为对候选人都不了解就放弃招聘呢？我不了解，但很希望面试官能直白地告诉我这是一家怎样的企业，他们需要怎样的人，他们在做什么，他们的团队是怎样的，是否能燃起我心中的熊熊烈焰，迫不及待地想要加入这个伟大的团队。

4）如果你被淘汰了，你觉得会是因为什么原因

我会直白地告诉面试官，那肯定是因为我与你合不来。

2.什么是好的交流方式

我希望面试官能打破高高在上的那种我问你答的旧有模式。我认为让面试官先做自我介绍才是最好的开局效果。请面试官花费5分钟的时间先简要介绍一下公司的基本情况，还有你的团队规模、运维环境及所使用到的技术和职位要求，等等。如果是部门领导或HR，最好能对公司的商业运作、企业文化、福利待遇做一些宣传。这种自我介绍很容易打开话题，而且还可以缓解候选人的紧张情绪，对公司或岗位有一个清晰的了解，便于接下来有针对性的交流。事实上这也是一种商业宣传，对增强候选人入职的积极性是有帮助的。退一万步讲，即便候选人没能通过考核或放弃offer，这种待人的方式也会给对方留下一个非常美好的印象。这样做，有百利而无一害。

接下来进入技术环节，我建议问题不要超过5个。主要集中在三类问题上：第一，候选人在以往工作中最擅长的一两个技术，以考察候选人对以往工作是否用心钻研；第二，列出目前职位需要的技术点，让候选人选择一两个最有把握的，以考察候选人是否可以胜任工作；最后了解一下候选人最近在关注什么技术。每个问题要尽量深入，考察对方对原理或方法的理解程度。为什么不必问太多的问题呢？因为候选人挑选的已经是他最擅长的内容了，对最擅长的方面进行最深入的讨论足以考察对方的技术能力。一个问题的时间注意控制在5分钟以内。另外只要发现对方深入不下去了，结果也就显现出来了，应该停止讨论并切换到下一个问题。所谓点到为止，没有必要继续纠缠不清。这样计算大约总计花费半个小时。

最后再补充两个非技术问题：第一，自己最大的非技术优势是什么（举例说明）？借以考察对方的性格品质和综合能力。第二，你对新团队的期许是什么？这显然比问为什么离职要好得多。虽然我觉得这两者其实本质上并没有什么差别，但显然这样的提问更加有亲和力。

A.2.5　团队怎样增加吸引力和凝聚力

关于团队的建设和管理方面，本人也是个初学者。因此不再过多妄言，简单地提几点建议仅供参考。

·团队成员之间可以有差异和分层，但成员之间的技术能力不要过度分化。

·团队可以有带头人、但不要产生绝对核心，以免出现单点隐患。

·团队要建设导师制度，形成技术主动分享、愿意带领新人的好风气，确保成员有成长的空间。

·设置有效的PKI，为第二象限事务、导师制度、数据产出（降低成本或提升性能）设定PKI。

·PKI要具备确保有效性的三要素，包括验收标准、时间进度、完成后的奖励内容。

·不定期进行团队内部交流，明确目标、分析问题、消除误解，去除不利于团队发展的因素。

A.3　小结

讲了这么多，我觉得还是有很多话没有说完。这篇文章并非教科书，也并非要刻意针对什么，以上这些感悟不过是我近十年来的亲身经历和所想所得而已。我只是站在一个相对公正客观的立场之上，说了一些真诚的大实话，既讲述到了职业发展，求职攻略及关于面试官的修养问题，也希望大家就此能得到激励并重新正确地认识自己，不要昏昏沉沉地过日子，误了自己这一生。我相信我身边仍然有许多还未意识到自己的问题或希望求得帮助的朋友，这也正是我愿意鼓起勇气，奋笔疾书的最大动力。这篇文章中提到的最重要的优秀品质——用心做事的匠人态度和付诸行动的坚韧毅力，正是我人生的座右铭，这两点也希望每个人都能去践行。

最后和大家共勉一句话——也许你现在和我一样，不过是个无名小卒。但只要你愿意改变，世界一定会为你动容。