如何使用Docker、Docker-Compose和Rancher搭建部署Pipeline(三)

该系列文章分享了用Docker、Docker-Compose和Rancher完成容器部署工作流的经验。本文将探讨怎样运行Rancher,解决只使用Docker-Compose时面临的主机负载失衡、获取服务状态困难、多环境下主机与集群难以管理等的问题。

在这一部分,我们将一步步的走进Rancher,细致的探讨Rancher将如何解决在部署与容器管理时出现的种种的问题。回顾教程的第二部分,你会发现我们已经将应用的部署迁移至Docker Compose,并且已经建立了一系列工作步骤来部署我们的应用。这将使得开发人员能够轻松的对他们的应用部署逻辑进行修正,运维人员也可以查看应用的部署时间。当然,在上一个部分教程的一系列操作中,也存在一些显而易见的问题需要解决。

使用Docker-Compose时面临的挑战

首先,运维人员必须手动地调整所有服务的执行计划。部署人员需要决定将哪一个应用部署至哪一台主机,这意味着部署人员需要时刻对每一台主机的剩余可用资源都有了解,如果某一台主机或者容器崩溃了,部署的操作人员将需要对应用进行重新部署。实际生产中,这意味着主机常常处于负载失衡的状态,并且服务在崩溃之后需要很长时间才能得到恢复。

其次,使用Docker-Compose时,想要获得你的服务的当前状态是十分困难的。举个例子来说,我们经常会从运维人员、项目经理以及开发者口中听到这样的问题:“现在部署环境中运行的到底是XX应用程序的哪个版本?”如果我们采用的是手动调整服务的执行计划的方式,想要得到这个问题的答案通常需要询问指定的进行操作的工程师,工程师们需要登陆服务器并运行docker中的ps命令来查看容器的信息。然而面对这些问题,Rancher将会给我们提供极大的便利:每个人都可以非常容易地获取已经部署的服务的信息,而不需要临时请求运维人员的帮助。

使用Rancher之前,我们试着了解过不少其他能够管理Docker主机或集群的解决方案。然而这些解决方案都没有注意到这是对Docker主机或集群在多种环境(multi-environment)下的管理,这将成为最大的麻烦与负担之一。如果有服务以不同的负载运行在8种不同的环境下,我们需要的是一个统一的方式来管理集群,而不会想要访问8个不同的服务。并且,我们希望让重新构建环境对于我们而言,变成分分钟就能完成的任务,这样开发者就可以随意地更改开发环境。然而,对于生产环境而言,我们希望提供给他们的只是有限的只读访问权限。面对这样的需求,一个采用基于角色的访问控制(RBAC)模型的集中管理方案就显得十分必要了。我们最初决定尝试Rancher就是因为它在部署上非常简单。

当Rancher面临这些挑战

在短短半天的时间里,使用AWS ELB、Elasticache、RDS和现有的Docker主机,我们已经将Rancher部署好并成功运行。能够方便地配置认证信息也是Rancher的优点之一。

我们并不会深入Rancher本身部署的细节,Rancher部署文档中已经说的很明白了。相反,我们将从刚刚完成初始设置那一步开始,说明将如何将原有的设置(教程第一部分和第二部分中所提及的)迁移进来。

我们就从创建不同的环境开始吧,为了使得这个过程尽量简单些,我们将对开发环境(dev)、部署环境(stage)以及生产环境(prod)分别进行设置。每个环境都已有运行在Ubuntu之上的Docker主机,且这些Docker主机是由内部的Ansible配置的,Ansible安装了Docker、我们的监控代理、并进行了一些组织特定的更改。在Rancher上,你只需要运行一条命令,将Docker主机在Rancher server内部进行注册,就可以将已有的Docker主机添加至每个环境中。

添加一台Rancher主机

在大多数情况下,想要添加一台主机需要经过一系列的操作:通过鼠标在网页上完成一些点击,接下来切换至某个特定的环境,最后在终端系统上输入命令。然而,如果你使用Rancher API,我们可以在Ansible工具的帮助下使得这一系列的操作转化为完全自动化的设置。出于好奇,在下面我们截取了playbook中有关这一操作的部分内容(大多是根据 Hussein Galas的repo中的内容做出的逻辑上的修改而得到的)。

name: install dependencies for uri module
  apt: name=python-httplib2 update_cache=yes
  name: check if the rancher-agent is running
  command: docker ps –filter ‘name=rancher-agent’
  register: containers
  name: get registration command from rancher
  uri:
  method: GET
  user: “{{ RANCHER_API_KEY }}”
  password: “{{ RANCHER_SECRET_KEY }}”
  force_basic_auth: yes
  status_code: 200
  url: “https://rancher.abc.net/v1/projects/{{ RANCHER_PROJECT_ID }}/registrationtokens”
  return_content: yes
  validate_certs: yes
  register: rancher_token_url
  when: “‘rancher-agent’ not in containers.stdout”
  name: register the host machine with rancher
  shell: >
  docker run -d –privileged
  -v /var/run/docker.sock:/var/run/docker.sock
  {{ rancher_token_url.json[‘data’][0][‘image’] }}
  {{ rancher_token_url.json[‘data’][0][‘command’].split() | last}}
  when: “‘rancher-agent’ not in containers.stdout”

随着工作的一步步进行,我们已经完成了环境的创建并已经将主机在Rancher server中注册,现在就让我们来了解一下,如何将我们的部署工作流整合至Rancher中。我们知道,对于每一台Docker来说,其中都有着一些正在运行的容器,这些系统的部署是通过Ansible工具借助Jenkins完成的。Rancher提供了以下开箱即用的功能:

  • 管理已有的容器(比如:启动、修改、查看日志、启动一个交互式的shell)
  • 获得关于运行中的和停止运行的容器的信息(比如:镜像信息、初始化命令信息、命令信息,端口映射信息以及环境变量信息)
  • 查看主机和容器层级上的资源使用情况(比如:CPU使用率、内存占用率、以及磁盘和网络的使用情况)

独立的容器

很快,我们就已经将Docker主机注册至Rancher Server中,现在我们可以查看容器在各种环境下的运行状态信息了。不仅如此,如果想要将这些信息分享给其他团队,我们仅仅需要针对某个环境给予他们一些有限的权限。通过以上的方式,在想要获得状态信息时我们就完全没有必要请求操作人员登录Docker主机,再通过人工的方式去查询,同时这样也减少了申请获得环境信息的请求的数目,因为我们已经将某些访问权限分配至各个团队了。举个例子来说,如果为开发团队分配环境信息的只读权限,那么将会在开发团队与部署操作团队之间架起一座沟通的桥梁,这样两个团队都会对这个环境的状态比以往更加的关心。在这个基础上,故障的排除也变成了一种小组间相互合作的过程,而不是以往的那种单向的、依赖同步信息流的解决方式,相互合作的方式也会减少解决突发事件的总时间。

到现在为止,我们已经将已有的Docker主机加入Rancher Server,并且基于已经阅读完了的教程的第一部分关于Jenkins和Rancher的内容,下一步,我们打算改进的部分是我们已有的部署流水线,我们将会对已有的部署流水线进行修改,以便于使用Rancher compose,Rancher Compose将代替之前Ansible工具提到的Docker compose。不过在我们深入下一部分之前,我们首先需要了解关于Rancher的应用、调度、Docker Compose和Rancher Compose的一些信息。

应用与服务:Rancher将每个独立的容器(指的是部署在Rancher之外的容器,或者是通过Rancher UI生成的一次性功能的容器)、应用和服务彼此分离开。简单地说,应用是一组服务,而所有容器都需要利用服务(关于应用和服务的内容之后将会由更加详细的介绍)以构建一个应用。独立的容器需要手动地进行调度。

调度:在之前的部署技术中,运维人员需要决定容器应当在哪一台主机上运行。如果使用的是部署脚本,那么意味着运维人员需要决定部署脚本在哪一台或哪几台主机上运行;如果使用Ansible,这将意味着运维人员需要决定哪些主机或组需要到Jenkins中工作。不论是哪一种方式,都需要运维人员去做一些决定,但是在大多数情况下,他们做出的决定都缺乏一些可靠的依据,这对我们的部署工作很是不利(比如说某一台主机的CPU使用率高达100%)。很多解决方案,比如像Docker Swarm、Kubernetes、Mesos和Rancher都采用了调度器来解决这类问题。对于需要执行的某个操作,调度器将会请求获得一组主机的信息,并判断出哪几台是适合执行这个操作的。调度器会根据默认的需求设定或者用户定义的特定需求,比如CPU使用率高低、亲和性或反亲和性规则(比如:禁止在同一台主机上部署两个相同容器)等类似的需求,以逐渐缩小主机选择的范围。如果我是一个负责部署的运维人员,调度器将会极大的减少我的工作负担(尤其是我在深夜加班忙于部署时),因为调度器对以上信息的计算比我快的多,也准的多。Rancher在我们通过应用部署服务的时候能够提供一个开箱即用调度器。

Docker compose:Rancher使用Docker compose来创建应用并定义服务。由于我们已经将服务转化为Docker compose的文件,我们在此基础上创建应用就变得容易了许多。应用可以手动的从UI界面中创建,也可以通过Rancher compose在命令行(CLI)下快速的创建。

Rancher compose:Rancher compose是一种通过命令行(CLI)让我们得以对Rancher中的每一种环境的应用和服务进行方便的管理的工具。同时,通过rancher-compse.yml文件,Rancher compose还能允许对Rancher工具进行一些其他访问。这是一个纯粹的附加的文件,将不会取代原有的docker-compose.yml文件。在rancher-compose.yml文件中,你可以定义以下内容,比如说:

  • 每种服务的升级策略信息
  • 每种服务的健康检查信息
  • 每种服务的需求规模信息

这些都是Rancher中非常实用的亮点,如果你使用Docker Compose或者Docker daemon,这些内容你都是获取不到的。如果想要查看Rancher Compose能提供的所有特性,你可以查看这个文档

通过将已有的部署工作交给Rancher Compose来替代之前的Ansible工具,我们能够很轻松的将服务迁移并部署为Rancher应用的形式。之后,我们就能够去除DESTINATION参数了,但我们依然保留VERSION参数,因为我们在插入docker-compose.uml文件的时候还要使用它。以下是使用Jenkins部署时,部署逻辑的shell片段:

export RANCHER_URL=http://rancher.abc.net/
  export RANCHER_ACCESS_KEY=…
  export RANCHER_SECRET_KEY=…

if [ -f docker/docker-compose.yml ]; then
  docker_dir=docker
  elif [ -f /opt/abc/dockerfiles/java-service-1/docker-compose.yml ]; then
  docker_dir=/opt/abc/dockerfiles/java-service-1
  else
  echo “No docker-compose.yml found. Can’t continue!”
  exit 1
  fi

if ! [ -f ${docker_dir}/rancher-compose.yml ]; then
  echo “No rancher-compose.yml found. Can’t continue!”
  exit 1
  fi

/usr/local/bin/rancher-compose –verbose 
  -f ${docker_dir}/docker-compose.yml 
  -r ${docker_dir}/rancher-compose.yml 
  up -d –upgrade

阅读完代码段,我们可以发现其主要包括以下内容:

我们定义了以环境变量的方式如何访问我们的Rancher server。
需要找到docker-compose.yml文件,否则将会任务将会报错退出。
需要找到rancher-compose.yml文件,否则任务将会报错退出。
运行Rancher-compose,并告诉它不要block并且使用-d命令输出日志,使用-upgrade命令更新一个已经存在的服务。

也许你已经发现了,在绝大部分,代码的逻辑都是相同的,而最大的区别就是使用rancher-compose代替使用Ansible工具完成部署,并对每一个服务添加了rancher-compose.yml文件。具体到我们的java-service-1应用,docker-compose文件和rancher-compose文件现在是这样的:

docker-compose.yml
  java-service-1:
  image: registry.abc.net/java-service-1:${VERSION}
  container_name: java-service-1
  expose:
  – 8080
  ports:
  – 8080:8080
  rancher-compose.yml
  java-service-1:
  scale: 3

在开始部署工作之前,我们先回顾一下部署工作的流程:

  • 开发人员将代码的修改推送至git上
  • 使用Jenkins对代码进行单元测试,在测试工作结束之后触发下游工作
  • 下游工作采用新的代码构建一个docker镜像,并将其推送至我们自己的Docker镜像仓库中
  • 创建包含应用名、版本号、部署环境的deployment ticket
DEPLOY-111:
  App: JavaService1, branch “release/1.0.1”
  Environment: Production
  • 部署工程师针对应用运行Jenkins的部署工作,运行时需要将版本号作为参数。
  • Rancher compose开始运行,对于某个环境创建或更新应用,并且当达到所需规模的时候,结束这个工作
  • 部署工程师以及开发工程师分别手动地对服务进行校验
  • 部署工程师在Rancher UI中确认完成升级

关键点

使用Rancher进行我们的服务部署时,我们从Rancher内建的调度、弹性伸缩、还原、升级、和回滚等工具中获得极大的便利,使得我们在部署过程中没有花太大的力气。同时我们发现,在将部署工作从Ansible工具中迁移至Rancher的工作量也是很小的,仅仅需要在原有的基础上增加rancher-compose.yml文件。然而,使用Rancher来处理我们容器的调度意味着我们将难以确认我们的应用到底是在哪台主机上运行的。比方说,之前我们并没有决定java-service-1应用在哪里运行,对于后端,在进行负载均衡相关操作时,该应用就没有一个静态的IP。我们需要找到一种办法,使得我们的各种应用之间能够相互察觉到对方。最终,对于我们的java-service-1应用,我们将明确地将应用容器所在的docker主机的8080端口与应用绑定,不过,如果有其他服务与应用绑定为相同的端口,它将会启动失败。通常负责调度决策的工程师将会对以上的事务进行处理。然而,我们最好将这些信息通知调度器以避免这样的事情发生。

在本教程的最后一个部分,我们将继续探索一些方案来解决在使用亲和性规则、主机标签、服务探索以及智能升级和回滚等特性时出现的问题。

如何使用Docker、Docker-Compose和Rancher搭建部署Pipeline(二)

该系列文章分享了用Docker、Docker-Compose和Rancher完成容器部署工作流的经验。

本文将分享如何解决自动化不够、测试困难、追踪及新增环境变量易错等痛点,且使部署逻辑向使用Docker Compose的应用更近一步。

在这一系列文章的第一篇中,我们分享了只用Docker时我们开发的初步的工作流,如何创建一个基础的构建和部署流水线。容器的部署方式不再是在登陆server的时候从内存中输入Docker命令。我们已经通过Jenkins server实现了镜像的自动化构建。我们使用脚本将Docker命令进行封装,将其存储到GitHub中并且设置版本。目前我们正采取措施,通过逐步改善现有过程来实现持续部署。然而,仍有一些痛点需要我们去解决。在这篇文章中,我们将看看如何使用Docker Compose 和 Ansible来改善此设计。

在部署镜像时,工程师需要登录到服务器,并从shell运行我们的Docker wrapper脚本。这不是很好的解决方法,因为它也需要开发者进行等待。没有任何一方会从在这种方式中获益(作为一个工程师,当你去做某件你很了解并且很容易自动化的事情时,你有多少次被打断了?)由于每一次部署都是通过操作者电脑中的SSH会话来执行的,因此部署过程是不可见的。

如果你对我们的部署脚本还有印象,你会发现它看起来像下面的代码段:

未分类

实际上,我们做的是将Docker run命令语句进行抽象,由此工程师将不需要知道每个图像成功运行时所需要的确切的参数。虽然这改善了必须全部记住并且手动输入所有Docker参数的现状,但同时也会带来新的问题:

  • 每个容器的逻辑都存储在同一文件中,这使得对应用程序部署逻辑的更改更难追踪;
  • 当开发者需要测试或者修改参数时,需要被迫理清脚本中的逻辑,而不是能够在某一特定的程序中轻松地阅读和修改参数。

在我们的工作流中,Docker Compose是一个更适合使用的工具,它同样可以将部署参数进行编码,并且在YAML文件中指定,此文件就是docker-compose.yml。Docker Compose不仅帮助我们解决了上面提到的难点,而且也可以使我们从社区未来的工作中获益。下面让我们理清部署脚本,并且为我们的JAVA程序示例创建一个Compose文件。首先,我们需要基于原来的部署逻辑创建一个docker-compose.yml文件:

未分类

现在,部署容器只需要在与docker-compose.yml文件相同目录下输入以下命令:

docker-compose up

它将根据compose文件中设置的参数启动一个容器。在compose文件中一个重要的变量是${VERSION} 。Docker Compose 可以从当前的shell环境中插入compose文件里所列出的参数。我们可以通过简单地运行以下语句来设置参数:

VERSION=1.0.0 docker-compose up

它将从我们的私有镜像仓库挑出标记1.0.0的镜像,以此启动java-service-1程序。如果没有设置VERSION变量,Docker Compose将产生一条警告信息,并且用空字符串代替变量值,由此,具有最新版本标签的镜像将会被挑出。因此,正确地设置变量是相当重要的。

作为开发过程的一部分,我们希望开发人员能够在本地建立服务并且测试他们的服务。然而,由于docker-compose.yml指向私有镜像仓库的镜像,运行docker-compose将从最近构建的镜像中开启服务而不是从本地资源中开启。理想情况下,开发者可以通过运行以下代码使用典型的docker-compose工作流:

未分类

Docker Compose能在不修改docker-compose.yml文件的情况下,让我们做到这一点。我们可以使用多个文件来覆盖我们在本地测试中想要改变的任何参数。在docker-compose.override.yml中,我们指定一个key而不是一个镜像,并且移除了对VERSION变量的需求。由于这是一个覆盖文件,我们不需要复制任何额外的设置,如端口设置:

未分类

使用Docker Compose而非部署脚本之后,我们可以:

  • 在源代码中存储每个compose文件,这与Dockerfile类似;
  • 不再需要复杂的部署脚本;
  • 允许开发人员在本地轻松地测试并修改应用程序。

现在我们有了java-service-1程序的compose文件,我们可以将它从我们的部署脚本中删除,因此文件组织与下面的结构类似:

未分类

此时,我们仍然没有解决镜像构建和部署之间的问题。在docker-compose.yml文件中包含了所有的部署逻辑,但是它如何在环境中运行直至结束的呢?正好现在我们在运行与UNIX和TPC socket相关的Docker守护进程,是时候讨论一些与安全有关的问题了。

我们的情况是,工程师登录到服务器上,手动运行每个服务器所需容器的部署脚本。默认情况下,当在局部运行Docker命令时,它将使用UNIX socket /var/run/docker.sock连接Docker守护进程;或者让守护进程监听TCP socket,这允许用户远程连接到每个Docker守护进程,使得工程师能够像登录到主机一样运行命令。这为连接方式提供了更大的灵活性,但是没有考虑到一些开销和安全问题:

  • 通过网络连接增加了安全隐患;
  • 增加了对于基于主机或者基于网络的ACLs需求;
  • 保护守护进程需要分布式CA和客户端认证。

另一种可能的方法是不使用基于UNIX socket的方式运行Docker守护进程,而使用SSH来运行命令。已经建立的ACLs将保护SSH端口,并且它只允许通过SSH授权的特定的用户才能使用Docker守护进程。虽然这不是最简洁的方法,但是它有助于保持较低的运行开销,并且使安全隐患降到最低。这点是非常重要的,尤其是对于细粒度的稀疏的任务队列而言。

为了有利于通过SSH运行Docker命令,我们可以使用Ansible——一个流行的编排和配置管理工具。它是无代理的,并且允许通过SSH连接运行“剧本”(服务器任务集合)。一个运行docker-compose命令的简单的剧本如下所示:

未分类

如果你对Ansible没有过多了解,你也许可以通过上面的剧本大致了解到我们想做什么。它们按顺序一步步执行,具体如下所示:

  1. Ansible将通过SSH连接到目标服务器(允许通过使用DESTINATION变量来指定主机)

  2. 在每个服务器中,Ansible会通过执行shell命令登录到公司私有的镜像仓库

  3. Ansible将位于Jenkins(运行ansible剧本的服务器)中的docker-compose.yml文件复制到每个目标服务器中的/tmp/docker-compose.yml下

  4. 在每个目标服务器中运行docker-compose命令

  5. 通过删除远程的/tmp/docker-compose.yml文件进行清理

一个shell脚本可以被运用在同一个事件中。然而在Ansible中,我们将很容易的使任务并行化并且得到经过良好测试的模块,通过使Ansible与新的部署剧本相结合,我们可以远程启动容器,相较于工程师登录到主机、人工运行命令,这是一个重要的进步。为了在部署过程和状态中提供更大的可视性,我们将建立Jenkins任务来运行Ansible代码。通过使用Jenkins,在未来我们可以轻松地将构建和部署任务集成起来,从而得到额外的好处。

Jenkins任务需要两个参数:目标主机(传递给剧本中的DESTINATION变量)和部署镜像的版本(在docker-compose.yml文件中插入VERSION变量)。大多数任务的构建部分是一个shell构建器,它将试图找到程序中的docker-compose.yml文件,然后通过传递变量(用-e)到剧本中,运行ansible-playbook命令:

未分类

虽然看起来我们似乎只对工作流做了微小的变化,但是我们正一步一步地向构建一个持续部署模型迈进:

  • 部署是可以被审查的。我们使用日志来记录输出什么、何时输出、以及哪些主机是目标主机等信息,这一切都归功于Jenkins。
  • 程序部署逻辑已经从一个单一的脚本分散到存储在程序源代码中的单独的docker-compose.yml文件中,这意味着我们可以轻松地通过git更改程序部署逻辑。在程序源文件或者部署文件发生变化时,我们也可以容易地进行构建和部署。

虽然这些改进解决了某些问题,但是它们所带来的新的问题也成为了焦点:

  • 哪个容器的哪个版本会被部署到何地?
  • 容器在被部署后会处于哪种状态?
  • 我们如何确定哪个主机成为程序的目标主机?

在这一系列接下来的文章中,我们将探讨怎样运行Rancher以及使用它的原因,尤其是它如何解决上述的问题。与此同时,我们也讨论它在业务和开发团队中所起到的意想不到的桥梁作用。

学习使用Docker、Docker-Compose和Rancher搭建部署Pipeline(一)

这篇文章是一系列文章的第一篇,在这一系列文章中,我们想要分享我们如何使用Docker、Docker-Compose和Rancher完成容器部署工作流的故事。我们想带你从头开始走过pipeline的革命历程,重点指出我们这一路上遇到的痛点和做出的决定,而不只是单纯的回顾。

幸好有很多优秀的资源可以帮助你使用Docker设置持续集成和部署工作流。这篇文章并不属于这些资源之一。一个简单的部署工作流相对比较容易设置。但是我们的经验表明,构建一个部署系统的复杂性主要在于原本容易的部分需要在拥有很多依赖的遗留环境中完成,以及当你的开发团队和运营组织发生变化以支持新的过程的时候。希望我们在解决构建我们的pipeline的困难时积累下的经验会帮助你解决你在构建你的pipeline时遇到的困难。

在这第一篇文章里,我们将从头开始,看一看只用Docker时我们开发的初步的工作流。在接下来的文章中,我们将进一步介绍Docker-compose,最后介绍如何将Rancher应用到我们的工作流中。

为了为之后的工作铺平道路,假设接下来的事件都发生在一家SaaS提供商那里,我们曾经在SaaS提供商那里提供过长时间服务。仅为了这篇文章的撰写,我们姑且称这家SaaS提供商为Acme Business Company, Inc,即ABC。这项工程开始时,ABC正处在将大部分基于Java的微服务栈从裸机服务器上的本地部署迁移到运行在AWS上的Docker部署的最初阶段。这项工程的目标很常见:发布新功能时更少的前置时间(lead time)以及更可靠的部署服务。

为了达到该目标,软件的部署计划大致是这样的:

未分类

这个过程从代码的变更、提交、推送到git仓库开始。当代码推送到git仓库后,我们的CI系统会被告知运行单元测试。如果测试通过,就会编译代码并将结果作为产出物(artifact)存储起来。如果上一步成功了,就会触发下一步的工作,利用我们的代码产出物创建一个Docker镜像并将镜像推送到一个Docker私有注册表(private Docker registry)中。最后,我们将我们的新镜像部署到一个环境中。

要完成这个过程,如下几点是必须要有的:

  • 一个源代码仓库。ABC已经将他们的代码存放在GitHub私有仓库上了。
  • 一个持续集成和部署的工具。ABC已经在本地安装了Jenkins。
  • 一个私有registry。我们部署了一个Docker registry容器,由Amazon S3支持。
  • 一个主机运行Docker的环境。ABC拥有几个目标环境,每个目标环境都包含过渡性(staging)部署和生产部署。

这样去看的话,这个过程表面上简单,然而实际过程中会复杂一些。像许多其它公司一样,ABC曾经(现在仍然是)将开发团队和运营团队划分为不同的组织。当代码准备好部署时,会创建一个包含应用程序和目标环境详细信息的任务单(ticket)。这个任务单会被分配到运营团队,并将会在几周的部署窗口内执行。现在,我们已经不能清晰地看到一个持续部署和分发的方法了。

最开始,部署任务单可能看起来是这样的:

DEPLOY-111:
App: JavaService1, branch "release/1.0.1"
Environment: Production

部署过程是:

  • 部署工程师用了一周时间在Jenkins上工作,对相关的工程执行”Build Now“,将分支名作为参数传递。之后弹出了一个被标记的Docker镜像。这个镜像被自动的推送到了注册表中。工程师选择了环境中的一台当前没有在负载均衡器中被激活的Docker主机。工程师登陆到这台主机并从注册表中获取新的版本。
docker pull registry.abc.net/javaservice1:release-1.0.1
  • 找到现存的容器。
docker ps
  • 终止现存容器运行。
docker stop [container_id]

开启一个新容器,这个容器必须拥有所有正确启动容器所需的标志。这些标志可以从之前运行的容器那里,主机上的shell历史,或者其它地方的文档借鉴。

docker run -d -p 8080:8080 … registry.abc.net/javaservice1:release-1.0.1
  • 连接这个服务并做一些手工测试确定服务正常工作。
curl localhost:8080/api/v1/version
  • 在生产维护窗口中,更新负载均衡器使其指向更新过的主机。

  • 一旦通过验证,这个更新会被应用到环境中所有其它主机上,以防将来需要故障切换(failover)。

不可否认的是,这个部署过程并不怎么让人印象深刻,但这是通往持续部署伟大的第一步。这里有好多地方仍可改进,但我们先考虑一下这么做的优点:

  • 运营工程师有一套部署的方案,并且每个应用的部署都使用相同的步骤。在Docker运行那一步中需要为每个服务查找参数,但是大体步骤总是相同的:Docker pull、Docker stop、Docker run。这个过程非常简单,而且很难忘掉其中一步。

  • 当环境中最少有两台主机时,我们便拥有了一个可管理的蓝绿部署(blue-green deployment)。一个生产窗口只是简单地从负载均衡器配置转换过来。这个生产窗口拥有明显且快速的回滚方法。当部署变得更加动态时,升级、回滚以及发现后端服务器变得愈发困难,需要更多地协调工作。因为部署是手动的,蓝绿部署代价是最小的,并且同样能提供优于就地升级的主要优点。

好吧,现在看一看痛点:

  • 重复输入相同的命令。或者更准确地说,重复地在bash命令行里敲击输入。解决这一点很简单:使用自动化技术!有很多工具可以帮助你启动Docker容器。对于运营工程师,最明显的解决方案是将重复的逻辑包装成bash脚本,这样只需一条命令就可以执行相应逻辑。如果你将自己称作一个开发-运营(devops)工程师,你可能会去使用Ansible、Puppet、Chef或者SaltStack。编写脚本或剧本(playbooks)很简单,但是这里仍有几个问题需要说明:部署逻辑到底放在那里?你怎样追踪每个服务的不同参数?这些问题将带领我们进入下一点。

  • 即便一个运营工程师拥有超能力,在办公室工作一整天后的深夜里仍能避免拼写错误,并且清晰的思考,他也不会知道有一个服务正在监听一个不同的端口并且需要改变Docker端口参数。问题的症结在于开发者确实了解应用运行的详细信息(但愿如此),但是这些信息需要被传递给运营团队。很多时候,运营逻辑放在另外的代码仓库中或这根本没有代码仓库。这种情况下保持应用相关部署逻辑的同步会变得困难。由于这个原因,一个很好的做法是将你的部署逻辑只提交到包含你的Dockerfile的代码仓库。如果在一些情况下无法做到这点,有一些方法可以使这么做可行(更多细节将在稍后谈到)。把细节信息提交到某处是重要的。代码要比部署任务单好,虽然在一些人的脑海中始终认为部署任务单更好。

  • 可见性。对一个容器进行一个故障检测须要登陆主机并且运行相应命令。在现实中,这就意味着登陆许多主机然后运行“docker ps”和“docker logs –tail=100”的命令组合。有很多解决方案可以做到集中登陆。如果你有时间的话,还是相当值得设置成集中登陆的。我们发现,通常情况下我们缺少的能力是查看哪些容器运行在那些主机上的。这对于开发者而言是个问题。开发者想要知道什么版本被部署在怎样的范围内。对于运营人员来说,这也是个主要问题。他们须要捕获到要进行升级或故障检测的容器。

基于以上的情况,我们开始做出一些改变,解决这些痛点。

第一个改进是写一个bash脚本将部署中相同的步骤包装起来。一个简单的包装脚本可以是这样的:

!/bin/bash
APPLICATION=$1
VERSION=$2

docker pull "registry.abc.net/${APPLICATION}:${VERSION}"
docker rm -f $APPLICATION
docker run -d --name "${APPLICATION}" "registry.abc.net/${APPLICATION}:${VERSION}"

这样做行得通,但仅对于最简单的容器而言,也就是那种用户不需要连接到的容器。为了能够实现主机端口映射和卷挂载(volume mounts),我们须要增加应用程序特定的逻辑。这里给出一个使用蛮力实现的方法:

APPLICATION=$1
VERSION=$2

case "$APPLICATION" in
java-service-1)
 EXTRA_ARGS="-p 8080:8080";;
java-service-2)
 EXTRA_ARGS="-p 8888:8888 --privileged";;
*)
 EXTRA_ARGS="";;
esac

docker pull "registry.abc.net/${APPLICATION}:${VERSION}"
docker stop $APPLICATION
docker run -d --name "${APPLICATION}" $EXTRA_ARGS "registry.abc.net/${APPLICATION}:${VERSION}"

现在这段脚本被安装在了每一台Docker主机上以帮助部署。运营工程师会登陆到主机并传递必要的参数,之后脚本会完成剩下的工作。部署时的工作被简化了,工程师的需要做的事情变少了。然而将部署代码化的问题仍然存在。我们回到过去,把它变成一个关于向一个共同脚本提交改变并且将这些改变分发到主机上的问题。通常来说,这样做很值得。将代码提交到仓库会给诸如代码审查、测试、改变历史以及可重复性带来巨大的好处。在关键时刻,你要考虑的事情越少越好。

理想状况下,一个应用的相关部署细节和应用本身应当存在于同一个源代码仓库中。有很多原因导致现实情况不是这样,最突出的原因是开发人员可能会反对将运营相关的东西放入他们的代码仓库中。尤其对于一个用于部署的bash脚本,这种情况更可能发生,当然Dockerfile文件本身也经常如此。

这变成了一个文化问题并且只要有可能的话就值得被解决。尽管为你的部署代码维持两个分开的仓库确实是可行的,但是你将不得不耗费额外的精力保持两个仓库的同步。本篇文章当然会努力达到更好的效果,即便实现起来更困难。在ABC,Dockerfiles最开始在一个专门的仓库中,每个工程都对应一个文件夹,部署脚本存在于它自己的仓库中。

未分类

Dockerfiles仓库拥有一个工作副本,保存在Jenkins主机上一个熟知的地址中(就比如是‘/opt/abc/Dockerfiles’)。为了为一个应用创建Docker镜像,Jenkins会搜索Dockerfile的路径,在运行”docker build“前将Dockerfile和伴随的脚本复制进来。由于Dockerfile总是在掌控中,你便可能发现你是否处在Dockerfile超前(或落后)应用配置的状态,虽然实际中大部分时候都会处在正常状态。这是来自Jenkins构建逻辑的一段摘录:

if [ -f docker/Dockerfile ]; then
 docker_dir=Docker
elif [ -f /opt/abc/dockerfiles/$APPLICATION/Dockerfile ]; then
 docker_dir=/opt/abc/dockerfiles/$APPLICATION
else
 echo "No docker files. Can’t continue!"
 exit 1
if
docker build -t $APPLICATION:$VERSION $docker_dir

随着时间的推移,Dockerfiles以及支持脚本会被迁移到应用程序的源码仓库中。由于Jenkins最开始已经查看了本地的仓库,pipeline的构建不再需要任何变化。在迁移了第一个服务后,仓库的结构大致是这样的:

未分类

我们使用分离的仓库时遇到的一个问题是,如果应用源码或打包逻辑任意一个发生改变,Jenkins就会触发应用的重建。由于Dockerfiles仓库包含了许多项目的代码,当改变发生时我们不想触发所有的仓库重建。解决方法是:使用在Jenkins Git插件中一个很隐蔽的选项,叫做Included Regions。当配置完成后,Jenkins将一个变化引起的重建隔离在仓库的某个特定子集里面。这允许我们将所有的Dockerfiles放在一个仓库里,并且仍然能做到当一个改变发生时只会触发特定的构建(与当改变发生在仓库里特定的目录时构建所有的镜像相比)。

未分类

关于这个初步的工作流的另一个方面是部署工程师必须在部署前强制构建一个应用镜像。这将导致额外的延迟,尤其是构建存在问题并且开发人员需要参与其中的时候。为了减少这种延迟,并为更加持续的部署铺平道路,我们开始为熟知分支中的每一个提交构建Docker镜像。这要求每一个镜像有一个独一无二的版本标识符,而如果我们仅仅依赖官方的应用版本字符串往往不能满足这一点。最终,我们使用官方版本字符串、提交次数和提交sha码的组合作为版本标识符。

commit_count=$(git rev-list --count HEAD)
commit_short=$(git rev-parse --short HEAD)
version_string="${version}-${commit_count}-${commit_short}"

这样得到的版本字符串看起来是这样的:1.0.1-22-7e56158

在结束pipeline的Docker file部分的讨论之前,还有一些参数值得提及。如果我们不会在生产中操作大量的容器,我们很少用到这些参数。但是,它们被证明有助于我们维护Docker集群的线上运行。

  • 重启策略(Restart Policy)-一个重启策略允许你指定当一个容器退出时,每个容器采取什么动作。尽管这个可以被用作应用错误(application panic)时的恢复或当依赖上线时保持容器再次尝试连接,但对运营人员来说真正的好处是在Docker守护进程(daemon)或者主机重启后的自动恢复。从长远来看,你将希望实现一个适当的调度程序(scheduler),它能够在新主机上重启失败的容器。在那天到来之前,节省一些工作,设置一个重启策略吧。在现阶段的ABC中,我们将这项参数默认为“–restart always”,这将会使容器始终重启。简单地拥有一个重启策略就会使计划的(和非计划的)主机重启变得轻松得多。

  • 资源约束(Resource Constraints)-使用运行时的资源约束,你可以设置容器允许消耗的最大内存和CPU。它不会把你从一般的主机过载(over-subscription)中拯救出来,但是它可以抑制住内存泄漏和失控的容器。我们先对容器应用一个充足的内存限制(例如:–memory=”8g”) 。我们知道当内存增长时这样会产生问题。尽管拥有一个硬性限制意味着应用最终会达到内存不足(Out-of-Memory)的状态并产生错误(panic),但是主机和其它容器会保持正确运行。

结合重启策略和资源约束会给你的集群带来更好的稳定性,与此同时最小化失败的影响,缩短恢复的时间。这种类型的安全防护可以让你和开发人员一起专注于“起火”的根本原因,而不是忙于应付不断扩大的火势。

简而言之,我们从一个基础的构建pipeline,即从我们的源码仓库中创建被标记的Docker镜像开始。从使用Docker CLI部署容器一路到使用脚本和代码中定义的参数部署容器。我们也涉及了如何管理我们的部署代码,并且强调了几个帮助运营人员保持服务上线和运行的Docker参数。

此时此刻,在我们的构建pipeline和部署步骤之间仍然存在空缺。部署工程师会通过登入一个服务器并运行部署脚本的方法填补这个空缺。尽管较我们刚开始时有所改进,但仍然有进一步提高自动化水平的空间。所有的部署逻辑都集中在单一的脚本内,当开发者需要安装脚本以及应付它的复杂性时,会使本地测试会变得困难得多。此时此刻,我们的部署脚本也包含了通过环境变量处理任何环境特定信息的方法。追踪一个服务设置的环境变量以及增加新的环境变量是乏味且容易出错的。

在下一篇文章中,我们将看一看怎样通过解构(deconstructing)共同的包装脚本解决这些痛点,并使部署逻辑向使用Docker Compose的应用更近一步。

您也可以下载免费的电子书《Continuous Integration and Deployment with Docker and Rancher》(http://info.rancher.com/cicd-with-docker-ebook),这本书讲解了如何利用容器帮助你完成整个CI/CD过程。

docker-compose 启动顺序

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

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

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

depends_on
Controlling startup order in Compose

解决的办法有以下几种:

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

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

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

改写后的docker-compose如下:

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

配置 PHP 的 Session 存储到 Redis

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

配置方式如下:

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

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

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

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

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

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

测试代码:

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

Posted in 运行环境

Linux 安装配置Redis

下载安装

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

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

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

新建目录,存放配置文件

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

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

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

通过vim命令修改

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

最后运行redis:

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

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

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

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

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

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

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

功能点

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

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

主要思路

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

这样做的好处是:

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

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

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

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

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

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

主要功能设计和实现

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

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

GET /api/profiles/:profile HTTP/1.1

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

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

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

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

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

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

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

其中:

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

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

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

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

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

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

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

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

提供统一的客户端 library

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

library 设计

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

config.get(path)

提供一个 get 方法,注意:

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

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

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

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

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

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

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

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

config.sync(host, profile, token)

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

config.on(event, listener)

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

config.mock(object)

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

一些补充内容

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

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

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

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

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

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

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

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

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

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

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

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

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

module.exports = new Config();

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

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

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

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

增加配置覆盖功能

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

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

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

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

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

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

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

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

# prod.yml
mysql:
    database: prod

# dev.yml
mysql:
    database: dev

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

安全问题

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

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

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

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

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

mysql:
    password: ${MYSQL_PASSWORD}

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

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

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

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

总结

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

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

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

重写git历史记录

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

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

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

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

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

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

案例

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

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

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

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

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

方案

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

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

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

step 1,

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

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

step 2,

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

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

Step 3,

git push origin master --force

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

额外的问题

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

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

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

参考链接

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

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

使用Python操作Zabbix Api

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

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

未分类

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4. 多功能API

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

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

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

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

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

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

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


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

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

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

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


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

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

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

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

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

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



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

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

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

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

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

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



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


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

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

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

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


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

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

完结。。。

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

一、部署说明

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

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

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

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

  • 192.168.8.137–MYSQL-MASTER(mysql主库)

  • 192.168.8.137–MYSQL-SLAVE(mysql从库)

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

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

二、部署拓扑

未分类

三、详细部署步骤

3.1 安装LAP环境

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

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

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

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

3.2 部署mysql主从

3.2.1 Mysql主从原理

未分类

MySQL 主从复制原理剖析

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

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

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

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

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

3.2.2安装mysql

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

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

[root@localhost ~]# service mysqld restart

master修改my.cnf文件

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

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

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

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

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

未分类

  • slave修改my.cnf文件

未分类

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

其中all代表所有权限

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

mysql> flush privileges;

刷新权限

  • 重启master及slave

未分类

  • 查看mysql-bin及Position

在master上执行

mysql> show master status;

未分类

记录file及position

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

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

未分类

  • 启动slave
mysql> slave start;

未分类

在slave上查看同步状态

mysql> show slave statusG

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

未分类

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

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

进入到zabbix目录

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

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

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

安装mysql客户端

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

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

未分类

mysql> create database zabbix charset=utf8;

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

未分类

未分类

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

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

未分类

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

未分类

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

找到sql包

未分类

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

未分类

进入到mysql程序中;

mysql> use zabbix;

进入zabbix库

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

先导入schema包

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

再导入images包

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

最后导入data包

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

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

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

未分类

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

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

未分类

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

解决办法有两种:

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

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

编译完成会提示如下图:

未分类

但还是建议先执行make

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

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

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

进入到zabbix主配置文件目录

未分类

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

先备份然后修改

未分类

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

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

未分类

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

未分类

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

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

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

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

修改PHP时区

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

以上命令在LAP主机上执行

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

查看zabbix启动状态

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

未分类

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

访问Apache主页

未分类

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

3.4 安装配置zabbix web

  • 解决PHP参数与依赖报错

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

未分类

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

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

以上是安装依赖

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

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

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

未分类

  • 填写数据库信息

未分类

  • 填写监控信息

未分类

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

  • 最后核对信息

未分类

  • 安装完成

未分类

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

未分类

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

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

  • 解决Zabbix server is not running

进入系统遇到如下错误

未分类

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

未分类

修改$ZBX_SERVER后跟zabbix服务器ip

未分类

重启Apache、zabbix服务端

未分类

至此全部部署完成。

3.5 zabbix邮件报警

  • 设置发件信息

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

未分类

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

未分类

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

未分类

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

未分类

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

未分类

点击更新即可;

未分类

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

未分类

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

未分类

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

未分类

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

未分类

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

未分类

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

未分类

未分类