CI/CD 之 GitLab CI

接着上篇文章整理,这篇文章主要介绍一下 GitLab CI 相关功能,并通过 GitLab CI 实现自动化构建项目;项目中所用的示例项目已经上传到了 GitHub

一、环境准备

首先需要有一台 GitLab 服务器,然后需要有个项目;这里示例项目以 Spring Boot 项目为例,然后最好有一台专门用来 Build 的机器,实际生产中如果 Build 任务不频繁可适当用一些业务机器进行 Build;本文示例所有组件将采用 Docker 启动, GitLab HA 等不在本文阐述范围内

  • Docker Version : 1.13.1
  • GitLab Version : 10.1.4-ce.0
  • GitLab Runner Version : 10.1.0
  • GitLab IP : 172.16.0.37
  • GitLab Runner IP : 172.16.0.36

二、GitLab CI 简介

GitLab CI 是 GitLab 默认集成的 CI 功能,GitLab CI 通过在项目内 .gitlab-ci.yaml 配置文件读取 CI 任务并进行相应处理;GitLab CI 通过其称为 GitLab Runner 的 Agent 端进行 build 操作;Runner 本身可以使用多种方式安装,比如使用 Docker 镜像启动等;Runner 在进行 build 操作时也可以选择多种 build 环境提供者;比如直接在 Runner 所在宿主机 build、通过新创建虚拟机(vmware、virtualbox)进行 build等;同时 Runner 支持 Docker 作为 build 提供者,即每次 build 新启动容器进行 build;GitLab CI 其大致架构如下
未分类

三、搭建 GitLab 服务器

3.1、GitLab 搭建

GitLab 搭建这里直接使用 docker compose 启动,compose 配置如下

version: '2'
services:
  gitlab:
    image: 'gitlab/gitlab-ce:10.1.4-ce.0'
    restart: always
    container_name: gitlab
    hostname: 'git.mritd.me'
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        external_url 'http:/git.mritd.me'
        # Add any other gitlab.rb configuration here, each on its own line
    ports:
      - '80:80'
      - '443:443'
      - '8022:22'
    volumes:
      - './data/gitlab/config:/etc/gitlab'
      - './data/gitlab/logs:/var/log/gitlab'
      - './data/gitlab/data:/var/opt/gitlab'

直接启动后,首次登陆需要设置初始密码如下,默认用户为 root
未分类
登陆成功后创建一个用户(该用户最好给予 Admin 权限,以后操作以该用户为例),并且创建一个测试 Group 和 Project,如下所示
未分类
未分类

3.2、增加示例项目

这里示例项目采用 Java 的 SpringBoot 项目,并采用 Gradle 构建,其他语言原理一样;如果不熟悉 Java 的没必要死磕此步配置,任意语言(最好 Java)整一个能用的 Web 项目就行,并不强求一定 Java 并且使用 Gradle 构建,以下只是一个样例项目;SpringBoot 可以采用 Spring Initializr 直接生成(依赖要加入 WEB),如下所示
未分类
将项目导入 IDEA,然后创建一个 index 示例页面,主要修改如下
– build.gradle

buildscript {
    ext {
        springBootVersion = '1.5.8.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'

group = 'me.mritd'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}


dependencies {
    compile('org.springframework.boot:spring-boot-starter')
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}
  • 新建一个 HomeController
package me.mritd.TestProject;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/*******************************************************************************
 * Copyright (c) 2005-2017 Mritd, Inc.
 * TestProject
 * me.mritd.TestProject
 * Created by mritd on 2017/11/24 下午12:23.
 * Description: 
 *******************************************************************************/
@Controller
public class HomeController {

    @RequestMapping("/")
    public String home(){
        return "index";
    }
}
  • templates 下新建 index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>Title</title>
</head>
<body>
<h1>Test...</h1>
</body>
</html>

最后项目整体结构如下
未分类
执行 assemble Task 打包出可执行 jar 包,并运行 java -jar TestProject-0.0.1-SNAPSHOT.jar 测试下能启动访问页面即可
未分类
最后将项目提交到 GitLab 后如下
未分类

四、GitLab CI 配置

针对这一章节创建基础镜像以及项目镜像,这里仅以 Java 项目为例;其他语言原理相通,按照其他语言对应的运行环境修改即可

4.1、增加 Runner

GitLab CI 在进行构建时会将任务下发给 Runner,让 Runner 去执行;所以先要添加一个 Runner,Runner 这里采用 Docker Compose 启动,build 方式也使用 Docker 方式 Build;compose 文件如下

version: '2'
services:
  gitlab-runner:
    container_name: gitlab-runner
    image: gitlab/gitlab-runner:alpine-v10.1.0
    restart: always
    network_mode: "host"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./config.toml:/etc/gitlab-runner/config.toml
    extra_hosts:
      - "git.mritd.me:172.16.0.37"

在启动前,我们需要先 touch 一下这个 config.toml 配置文件;该文件是 Runner 的运行配置,此后 Runner 所有配置都会写入这个文件(不 touch 出来 docker-compose 发现不存在会挂载一个目录进去,导致 Runner 启动失败);启动 docker-compose 后,需要进入容器执行注册,让 Runner 主动去连接 GitLab 服务器

# 生成 Runner 配置文件
touch config.toml
# 启动 Runner
docker-compose up -d
# 激活 Runner
docker exec -it gitlab-runner gitlab-runner register

在执行上一条激活命令后,会按照提示让你输入一些信息;首先输入 GitLab 地址,然后是 Runner Token,Runner Token 可以从 GitLab 设置中查看,如下所示
未分类
整体注册流程如下
未分类
注册完成后,在 GitLab Runner 设置中就可以看到刚刚注册的 Runner,如下所示
未分类
Runner 注册成功后会将配置写入到 config.toml 配置文件;由于两个测试宿主机都没有配置内网 DNS,所以为了保证 runner 在使用 docker build 时能正确的找到 GitLab 仓库地址,还需要增加一个 docker 的 host 映射( extra_hosts );同时为了能调用 宿主机 Docker 和持久化 build 的一些缓存还挂载了一些文件和目录;完整的 配置如下(配置文件可以做一些更高级的配置,具体参考 官方文档 )
– config.toml

concurrent = 1
check_interval = 0

[[runners]]
  name = "Test Runner"
  url = "http://git.mritd.me"
  token = "c279ec1ac08aec98c7141c7cf2d474"
  executor = "docker"
  builds_dir = "/gitlab/runner-builds"
  cache_dir = "/gitlab/runner-cache"
  [runners.docker]
    tls_verify = false
    image = "debian"
    privileged = false
    disable_cache = false
    shm_size = 0
    volumes = ["/data/gitlab-runner:/gitlab","/var/run/docker.sock:/var/run/docker.sock","/data/maven_repo:/data/repo","/data/maven_repo:/data/maven","/data/gradle:/data/gradle","/data/sonar_cache:/root/.sonar","/data/androidsdk:/usr/local/android","/data/node_modules:/data/node_modules"]
    extra_hosts = ["git.mritd.me:172.16.0.37"]
  [runners.cache]

注意,这里声明的 Volumes 会在每个运行的容器中都生效;也就是说 build 时新开启的每个容器都会被挂载这些目录;修改完成后重启 runner 容器即可,由于 runner 中没啥可保存的东西,所以可以直接 docker-compose down && docker-compose up -d 重启

4.2、创建基础镜像

由于示例项目是一个 Java 项目,而且是采用 Spring Boot 的,所以该项目想要运行起来只需要一个 java 环境即可,中间件已经被打包到了 jar 包中;以下是一个作为基础运行环境的 openjdk 镜像的 Dockerfile

FROM alpine:edge 

LABEL maintainer="mritd <[email protected]>"

ENV JAVA_HOME /usr/lib/jvm/java-1.8-openjdk
ENV PATH $PATH:/usr/lib/jvm/java-1.8-openjdk/jre/bin:/usr/lib/jvm/java-1.8-openjdk/bin

RUN apk add --update bash curl tar wget ca-certificates unzip 
        openjdk8 font-adobe-100dpi ttf-dejavu fontconfig 
    && rm -rf /var/cache/apk/* 

CMD ["bash"]

这个 openjdk Dockerfile 升级到了 8.151 版本,并且集成了一些字体相关的软件,以解决在 Java 中某些验证码库无法运行问题,详见 Alpine 3.6 OpenJDK 8 Bug;使用这个 Dockerfile,在当前目录执行 docker build -t mritd/openjdk:8 . build 一个 openjdk8 的基础镜像,然后将其推送到私服,或者 Docker Hub 即可

4.3、创建项目镜像

有了基本的 openjdk 的 docker 镜像后,针对于项目每次 build 都应该生成一个包含发布物的 docker 镜像,所以对于项目来说还需要一个项目本身的 Dockerfile;项目的 Dockerfile 有两种使用方式;一种是动态生成 Dockerfile,然后每次使用新生成的 Dockerfile 去 build;还有一种是写一个通用的 Dockerfile,build 时利用 ARG 参数传入变量;这里采用第二种方式,以下为一个可以反复使用的 Dockerfile

FROM mritd/openjdk:8-144-01

MAINTAINER mritd <[email protected]>

ARG PROJECT_BUILD_FINALNAME

ENV TZ 'Asia/Shanghai'
ENV PROJECT_BUILD_FINALNAME ${PROJECT_BUILD_FINALNAME}


COPY build/libs/${PROJECT_BUILD_FINALNAME}.jar /${PROJECT_BUILD_FINALNAME}.jar

CMD ["bash","-c","java -jar /${PROJECT_BUILD_FINALNAME}.jar"]

该 Dockerfile 通过声明一个 PROJECT_BUILD_FINALNAME 变量来表示项目的发布物名称;然后将其复制到根目录下,最终利用 java 执行这个 jar 包;所以每次 build 之前只要能拿到项目发布物的名称即可

4.4、Gradle 修改

上面已经创建了一个标准的通用型 Dockerfile,每次 build 镜像只要传入 PROJECT_BUILD_FINALNAME 这个最终发布物名称即可;对于发布物名称来说,最好不要固定死;当然不论是 Java 还是其他语言的项目我们都能将最终发布物变成一个固定名字,最不济可以写脚本重命名一下;但是不建议那么干,最好保留版本号信息,以便于异常情况下进入容器能够分辨;对于当前 Java 项目来说,想要拿到 PROJECT_BUILD_FINALNAME 很简单,我们只需要略微修改一下 Gradle 的 build 脚本,让其每次打包 jar 包时将项目的名称及版本号导出到文件中即可;同时这里也加入了镜像版本号的处理,Gradle 脚本修改如下
– build.gradle 最后面增加如下

bootRepackage {

    mainClass = 'me.mritd.TestProject.TestProjectApplication'
    executable = true

    doLast {
        File envFile = new File("build/tmp/PROJECT_ENV")

        println("Create ${archivesBaseName} ENV File ===> " + envFile.createNewFile())
        println("Export ${archivesBaseName} Build Version ===> ${version}")
        envFile.write("export PROJECT_BUILD_FINALNAME=${archivesBaseName}-${version}n")

        println("Generate Docker image tag...")
        envFile.append("export BUILD_DATE=`date +%Y%m%d%H%M%S`n")
        envFile.append("export IMAGE_NAME=mritd/test:`echo ${CI_BUILD_REF_NAME} | tr '/' '-'`-`echo ${CI_COMMIT_SHA} | cut -c1-8`-${BUILD_DATE}n")
        envFile.append("export LATEST_IMAGE_NAME=mritd/test:latestn")
    }
}

这一步操作实际上是修改了 bootRepackage 这个 Task(不了解 Gradle 或者不是 Java 项目的请忽略),在其结束后创建了一个叫 PROJECT_ENV 的文件,里面实际上就是写入了一些 bash 环境变量声明,以方便后面 source 一下这个文件拿到一些变量,然后用户 build 镜像使用,PROJECT_ENV 最终生成如下

export PROJECT_BUILD_FINALNAME=TestProject-0.0.1-SNAPSHOT
export BUILD_DATE=`date +%Y%m%d%H%M%S`
export IMAGE_NAME=mritd/test:`echo ${CI_BUILD_REF_NAME} | tr '/' '-'`-`echo ${CI_COMMIT_SHA} | cut -c1-8`-${BUILD_DATE}
export LATEST_IMAGE_NAME=mritd/test:latest

未分类

4.5、创建 CI 配置文件

一切准备就绪以后,就可以编写 CI 脚本了;GitLab 依靠读取项目根目录下的 .gitlab-ci.yml 文件来执行相应的 CI 操作;以下为测试项目的 .gitlab-ci.yml 配置

# 调试开启
#before_script:
#  - pwd
#  - env

cache:
  key: $CI_PROJECT_NAME/$CI_COMMIT_REF_NAME-$CI_COMMIT_SHA
  paths:
    - build

stages:
  - build
  - deploy

auto-build:
  image: mritd/build:2.1.1
  stage: build
  script:
    - gradle --no-daemon clean assemble
  tags:
    - test

deploy:
  image: mritd/docker-kubectl:v1.7.4
  stage: deploy
  script:
    - source build/tmp/PROJECT_ENV
    - echo "Build Docker Image ==> ${IMAGE_NAME}"
    - docker build -t ${IMAGE_NAME} --build-arg PROJECT_BUILD_FINALNAME=${PROJECT_BUILD_FINALNAME} .
#    - docker push ${IMAGE_NAME}
    - docker tag ${IMAGE_NAME} ${LATEST_IMAGE_NAME}
#    - docker push ${LATEST_IMAGE_NAME}
#    - docker rmi ${IMAGE_NAME} ${LATEST_IMAGE_NAME}
#    - kubectl --kubeconfig ${KUBE_CONFIG} set image deployment/test test=$IMAGE_NAME
  tags:
    - test
  only:
    - master
    - develop
    - /^chore.*$/

关于 CI 配置的一些简要说明如下

stages
stages 字段定义了整个 CI 一共有哪些阶段流程,以上的 CI 配置中,定义了该项目的 CI 总共分为 build、deploy 两个阶段;GitLab CI 会根据其顺序执行对应阶段下的所有任务;在正常生产环境流程可以定义很多个,比如可以有 test、publish,甚至可能有代码扫描的 sonar 阶段等;这些阶段没有任何限制,完全是自定义的,上面的阶段定义好后在 CI 中表现如下图
未分类

task
task 隶属于 stages 之下;也就是说一个阶段可以有多个任务,任务执行顺序默认不指定会并发执行;对于上面的 CI 配置来说 auto-build 和 deploy 都是 task,他们通过 stage: xxxx 这个标签来指定他们隶属于哪个 stage;当 Runner 使用 Docker 作为 build 提供者时,我们可以在 task 的 image 标签下声明该 task 要使用哪个镜像运行,不指定则默认为 Runner 注册时的镜像(这里是 debian);同时 task 还有一个 tags 的标签,该标签指明了这个任务将可以在哪些 Runner 上运行;这个标签可以从 Runner 页面看到,实际上就是 Runner 注册时输入的哪个 tag;对于某些特殊的项目,比如 IOS 项目,则必须在特定机器上执行,所以此时指定 tags 标签很有用,当 task 运行后如下图所示
未分类
除此之外 task 还能指定 only 标签用于限定那些分支才能触发这个 task,如果分支名字不满足则不会触发;默认情况下,这些 task 都是自动执行的,如果感觉某些任务太过危险,则可以通过增加 when: manual 改为手动执行;注意: 手动执行被 GitLab 认为是高权限的写操作,所以只有项目管理员才能手动运行一个 task,直白的说就是管理员才能点击;手动执行如下图所示
未分类

cache
cache 这个参数用于定义全局那些文件将被 cache;在 GitLab CI 中,跨 stage 是不能保存东西的;也就是说在第一步 build 的操作生成的 jar 包,到第二部打包 docker image 时就会被删除;GitLab 会保证每个 stage 中任务在执行时都将工作目录(Docker 容器 中)还原到跟 GitLab 代码仓库中一模一样,多余文件及变更都会被删除;正常情况下,第一步 build 生成 jar 包应当立即推送到 nexus 私服;但是这里测试没有搭建,所以只能放到本地;但是放到本地下一个 task 就会删除它,所以利用 cache 这个参数将 build 目录 cache 住,保证其跨 stage 也能存在

关于 .gitlab-ci.yml 具体配置更完整的请参考 官方文档

五、其他相关

5.1、GitLab 内置环境变量

上面已经基本搞定了一个项目的 CI,但是有些变量可能并未说清楚;比如在创建的 PROJECT_ENV 文件中引用了 ${CI_COMMIT_SHA} 变量;这种变量其实是 GitLab CI 的内置隐藏变量,这些变量在每次 CI 调用 Runner 运行某个任务时都会传递到对应的 Runner 的执行环境中;也就是说这些变量在每次的任务容器 SHELL 环境中都会存在,可以直接引用,具体的完整环境变量列表可以从 官方文档 中获取;如果想知道环境变量具体的值,实际上可以通过在任务执行前用 env 指令打印出来,如下所示
未分类
未分类

5.2、GitLab 自定义环境变量

在某些情况下,我们希望 CI 能自动的发布或者修改一些东西;比如将 jar 包上传到 nexus、将 docker 镜像 push 到私服;这些动作往往需要一个高权限或者说有可写入对应仓库权限的账户来支持,但是这些账户又不想写到项目的 CI 配置里;因为这样很不安全,谁都能看到;此时我们可以将这些敏感变量写入到 GitLab 自定义环境变量中,GitLab 会像对待内置变量一样将其传送到 Runner 端,以供我们使用;GitLab 中自定义的环境变量可以有两种,一种是项目级别的,只能够在当前项目使用,如下
未分类
另一种是组级别的,可以在整个组内的所有项目中使用,如下
未分类
这两种变量添加后都可以在 CI 的脚本中直接引用

5.3、Kubernetes 集成

对于 Kubernetes 集成实际上有两种方案,一种是对接 Kubernetes 的 api,纯代码实现;另一种取巧的方案是调用 kubectl 工具,用 kubectl 工具来实现滚动升级;这里采用后一种取巧的方式,将 kubectl 二进制文件封装到镜像中,然后在 deploy 阶段使用这个镜像直接部署就可以
未分类
其中 mritd/docker-kubectl:v1.7.4 这个镜像的 Dockerfile 如下

FROM docker:dind 

LABEL maintainer="mritd <[email protected]>"

ARG TZ="Asia/Shanghai"

ENV TZ ${TZ}

ENV KUBE_VERSION v1.8.0

RUN apk upgrade --update 
    && apk add bash tzdata wget ca-certificates 
    && wget https://storage.googleapis.com/kubernetes-release/release/${KUBE_VERSION}/bin/linux/amd64/kubectl -O /usr/local/bin/kubectl 
    && chmod +x /usr/local/bin/kubectl 
    && ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime 
    && echo ${TZ} > /etc/timezone 
    && rm -rf /var/cache/apk/*

CMD ["/bin/bash"]

这里面的 ${KUBE_CONFIG} 是一个自定义的环境变量,对于测试环境我将配置文件直接挂载入了容器中,然后 ${KUBE_CONFIG} 只是指定了一个配置文件位置,实际生产环境中可以选择将配置文件变成自定义环境变量使用

5.4、GitLab CI 总结

关于 GitLab CI 上面已经讲了很多,但是并不全面,也不算太细致;因为这东西说起来实际太多了,现在目测已经 1W 多字了;以下总结一下 GitLab CI 的总体思想,当思路清晰了以后,我想后面的只是查查文档自己试一试就行了

CS 架构
GitLab 作为 Server 端,控制 Runner 端执行一系列的 CI 任务;代码 clone 等无需关心,GitLab 会自动处理好一切;Runner 每次都会启动新的容器执行 CI 任务

容器即环境
在 Runner 使用 Docker build 的前提下;所有依赖切换、环境切换应当由切换不同镜像实现,即 build 那就使用 build 的镜像,deploy 就用带有 deploy 功能的镜像;通过不同镜像容器实现完整的环境隔离

CI即脚本
不同的 CI 任务实际上就是在使用不同镜像的容器中执行 SHELL 命令,自动化 CI 就是执行预先写好的一些小脚本

敏感信息走环境变量
一切重要的敏感信息,如账户密码等,不要写到 CI 配置中,直接放到 GitLab 的环境变量中;GitLab 会保证将其推送到远端 Runner 的 SHELL 变量中

基于 GitLab 的简单项目管理与协作流程

GitLab 是一个类似于 GitHub 的开源源码托管服务,它除了提供基于 git 的基本代码托管服务外。还具备很多与软件开发协作相关的其他功能。比如 issues、Merge Requests 等。

利用 GitLab 提供的这些功能,我们可以实践一些简单的项目管理和协作流程。这套流程借鉴于很多成功的开源项目,非常适合在小型开发团队里面使用。

使用 issues 来管理需求与缺陷

GitLab issues 类似于“工单系统”,是一个发布项目相关信息的地方。项目的所有成员都可以创建新的 issue,其他成员可以在 issue 下进行相关的讨论。

issues 本身是一个非常简单的功能,但是如果配合 “标签”、“里程碑” 等功能一起使用,就可以承担起一定的项目管理工作。

录入 issue

在项目的开发过程中,我们会碰到很多新的需求、软件 bug 等。这些需求与 bug ,就是 issue 最大的来源,它们都可以作为 issue 录入到项目的 issues 中。

因为 issue 的录入门槛很低,鼓励项目成员录入 issue 后,项目很容易就会出现大量的 issues。所以我们应该严格控制每个 issue 的内容质量,确保其他人可以通过这个 issue 获取足够多的信息,提高沟通效率。

不光是和需求和 bug,任何和项目有关的内容都可以录入到 issue 中。

编写优秀的“需求” issue
如果你要录入一个需求类的 issue,最好在内容主体中包含下面这些内容:

  • 用一句话描述你的需求,并用它作为标题
  • 这个需求是解决什么问题的?
  • 这个需求对软件现有功能会造成什么影响?
  • 这个需求应该实现什么样的功能?
  • 这个需求是否依赖其他模块提供相关支持?
  • [可选] 这个需求有哪些实现方式?
  • [可选] 这些可选的实现方式分别有哪些优缺点?

编写优秀的“bug” issue
如果你要录入一个 bug issue,最好在内容主体中包含下面这些内容:

  • 提供出现问题的软件版本号、操作系统环境等相关信息
  • 提供能够稳定复现问题的相关步骤
  • 描述期待行为与当前行为
  • [可选] 你对这个 bug 原因的相关分析

Review issue 并为其打上标签

当 issue 被创建后,应该等待项目的 owner (owner 指项目的所有者,是对项目各方面都比较了解的人,可以为多个人) 对 issue 进行 Review。

Review 时,如果 owner 觉得这个 issue 满足下面的任意条件:

  • 与项目本身的功能、市场定位有冲突
  • 与现存 issue 有重复
  • 其他不应该被保留的情况

则应该在评论中说明相关情况,并关闭该 issue。如果经过上面的过滤后,觉得 issue 应该被继续跟进,那就应该为它打上标签,方便之后的筛选、排期等工作。

“标签”是 issue 的核心特性,为了更好的使用它,我建议采用 “{type}/{value}” 这样的二维标签来取代传统的 “{value}” 单维标签,下面是一些常用的 issue 标签:

优先级:priority
优先级(priority)是最重要的标签之一。它直接影响 bug 需要被响应的速度、或需求的具体排期。

  • priority/P3:十分紧急
  • priority/P2:较为紧急
  • priority/P1:普通
  • priority/P0:不紧急

类型:kind
kind(类别)表示 issue 属于哪种类型。

  • kind/bug:软件缺陷
  • kind/feature:新功能
  • kind/enhancement:改进项,模块代码重构等不影响项目功能但是改善工程质量的 issue 可归入此类
  • kind/research:技术调研类,一般以输出某类结论或报告视为结束

工作量:size
size(工作量)表示 issue 需要大约花费多少时间/精力,可以用来做简单的工作量评估参考。

  • size/XL
  • size/M
  • size/SM

领域/模块:area
area用于标记当前 issue 属于项目中的什么领域/模块。这个分类下的具体标签由项目本身决定。比如 area/apiserver、area/controller 等。给 issue 打上 area 标签后,项目不同模块的相关负责人可以更方便的找到自己负责的相关 issues。

GitLab 的标签是一个非常灵活的功能,在具体使用中,不必拘泥于上面列出来的这几种标签,可以根据当前项目特点随意调整。

issue 的后续操作

当 issue 被创建、打上标签以后,就可以进行后续操作了。issue 的后续操作主要包括下面几种:

  • 认领 issue:每一个 issue 都有一个 Assignee(受理人),表示当前 issue 由谁在处理。在你准备开始具体的工作前,一定要记得将 issue 认领为自己所有。
  • 在 issue 下进行讨论:在 issue 下可以围绕 issue 进行讨论,在讨论过程中,可以通过 @USERNAME 的方式通知其他人关注当前 issue。

使用 issue 做项目里程碑管理

除了为 issue 打上标签以外,你还可以为 issue 绑定上 milestone(里程碑),来将 issue 与某些特定的项目节点关联起来。之后便可以在 milestones 页面查看每一个里程碑的进展。

和 labels 一样,里程碑也是一个十分灵活的功能,你可以根据项目需要建立不同的里程碑,比如:

  • 基于软件版本号:基于未来将要发布的版本号建立里程碑,比如 v1.0.3、v2.0.1 等等
  • 基于时间周期:基于特定的时间周期 – 比如敏捷开发中的一个 sprint – 来建立里程碑,比如 Y2017-M7W3、Y2017-M7W4 等等

使用 issue board

使用 issue board(类似于敏捷开发中的“看板”),可以在一个页面看到当前处于不同阶段的所有 issues。这个功能非常适合站立晨会时使用。

勤于关闭 issues

随着项目越来越大,项目累积的 issue 也会越来越多。而这些 issue 中有很多已经失去它的价值。

所以,为了避免有价值的 issue 淹没在这些过时的信息当中,我们应该定期 Review 现有的 issues,关闭掉那些已经过时的 issues。

基于 Merge Request 的开发流程

在 GitLab 上创建的项目,所有人都不应该直接往 master 分支推送代码。而是应该在其他分支(或者 fork 项目的分支)进行开发。并最终通过创建 Merge Request(类似 GitHub pull request)将代码合并到 master 分支。

创建 Merge Request 并进行 Code Review

基于 MR 的开发流程如下:

  • 开发者在自己的分支下进行开发,开发完成后,创建将该分支合并到 Master 的 Merge Request,改动进入 Review 状态
  • 进入 Review 状态的代码,将由团队内的其他一位成员(经验比较丰富、或者对该工作模块比较熟悉)对代码改动进行 Code Review
  • 大家对 Reivew 结果进行讨论,并提交新的修改
  • 最终达成一致后,代码被 Merge 进 Master 分支

灵活创建新分支来避免 MR 冲突

我们一般会用类似于 dev_piglei 这样的分支名称进行开发,遵循着 “开发” -> “push 并创建 MR” -> “开发” 这样的工作流程。

但是,因为一个分支是严格对应到一个 MR 的,当你在同一个分支上开发不同功能时,如果 MR 一直处于 open 状态,那这些不同功能都会被推送到同一个 MR 上,对 Review 过程产生困扰。

为了避免这种情况,最好为不同的功能项创建不同的分支并各自创建 MR,比如dev_feature_add_member、dev_feature_disabled_user。

在 git 工作流方面,git-flow workflow 是一个值得学习的内容

分拆大的 Merge Request

如果开发一些比较大的需求,我们通常会将他们一次实现完,然后作为一个大的 MR 来提交 Review。

但是如果每个 MR 过于复杂,会大大影响 Code Review 的效率。所以,如果你要实现一个比较复杂的特性,建议将它拆解为多个比较小的 MR 来依次提交。

假如,你要为网站的 feed 页面从零开始添加 redis 缓存功能。可能一开始想的提交这个大 MR:

  • 添加基于 redis 的缓存模块并为 feed 页面添加缓存并主动过期
    但这个 MR 里面包含了太多内容,会增加 Review 的难度。所以可以试着将这个功能拆解为下面三个更小的 MR:

  • 添加基于缓存模块

  • 为缓存模块添加 Redis 作为存储后端
  • 为 feed 页面提供缓存,并主动过期
    谨记:

  • 超过 1000 行的代码改动 Review 起来非常困难

  • 可以使用 feature flag(功能开关)在 PR 完全完成前屏蔽部分功能

Zabbix监控nginx进程

本文将介绍:

  • Nginx开启状态模板
  • 创建Nginx检测脚本
  • 图形化导入Nginx模块

此操作均在Zabbix3.2.6

Nginx开启状态模板(agent端)

yum install nginx -y

vi /etc/nginx/conf.d/default.conf

    location /nginx_status{
        stub_status on;
        access_log on;
    allow 127.0.0.1;
        allow 192.168.31.0/24;   只允许192.168.31.0这个网段访问
        deny all;
}
## deny all , 拒绝除 allow 中的主机之外所有主机访问此 URL ,实现过程中如果遇到 403 ,有可能是你把自己测试的机器拒绝了!


nginx -t 检查语法
nginx -s reload 重新加载配置文件  

查看nginx 状态信息:

[root@localhost ~]#  curl http://127.0.0.1/nginx_status
Active connections: 1 
server accepts handled requests
 3721 3721 3714 
Reading: 0 Writing: 1 Waiting: 0

注解:
Active connections: 1 当前活动的连接数

server accepts handled requests

3721 3721 3714

3721 从启动到现在一共处理的连接数

3721 从启动到现在成功创建的握手的次数

3714 总共处理的请求数(requests)
请求的丢失数=(握手-连接)
connection 连接数,tcp连接 
request http请求,GET/POST/DELETE

Reading: 0 Writing: 1 Waiting: 0 
Reading: 0 读取客户端Header的信息数 请求头
Writing: 1 返回给客户端的header的信息数 响应头 
Waiting: 0 等待的请求数

创建Nginx检测脚本

mkdir /usr/local/zabbix/scripts -p
vi /usr/local/zabbix/scripts/nginx_status.sh 

#!/bin/bash 
#Time:2017-11-19
#Author: bks.com

#HOST=`/sbin/ifconfig eth0 | sed -n '/inet /{s/.*addr://;s/ .*//;p}'` 
HOST="127.0.0.1" 
PORT="80" 

# Functions to return nginx stats 
# 检测nginx进程是否存在
function ping {
    /sbin/pidof nginx | wc -l
}
function active { 
/usr/bin/curl "http://$HOST:$PORT/nginx_status" 2>/dev/null| grep 'Active' | awk '{print $NF}' 
} 
function reading { 
/usr/bin/curl "http://$HOST:$PORT/nginx_status" 2>/dev/null| grep 'Reading' | awk '{print $2}' 
} 
function writing { 
/usr/bin/curl "http://$HOST:$PORT/nginx_status" 2>/dev/null| grep 'Writing' | awk '{print $4}' 
} 
function waiting { 
/usr/bin/curl "http://$HOST:$PORT/nginx_status" 2>/dev/null| grep 'Waiting' | awk '{print $6}' 
} 
function accepts { 
/usr/bin/curl "http://$HOST:$PORT/nginx_status" 2>/dev/null| awk NR==3 | awk '{print $1}' 
} 
function handled { 
/usr/bin/curl "http://$HOST:$PORT/nginx_status" 2>/dev/null| awk NR==3 | awk '{print $2}' 
} 
function requests { 
/usr/bin/curl "http://$HOST:$PORT/nginx_status" 2>/dev/null| awk NR==3 | awk '{print $3}' 
} 
# Run the requested function 
$1

# 对脚本赋予执行权限

chmod +x /usr/local/zabbix/scripts/nginx_status.sh 


编辑agent配置文件,定义Key

vim /etc/zabbix/zabbix_agentd.conf

UserParameter=nginx.status[*],/usr/local/zabbix/scripts/nginx_status.sh $1

# $1表示[*]这里面的参数,在此为"active",如果命令有$存在,那么在$前面在加上一个$


配置完后一定记得重启:
/etc/init.d/zabbix-agent restart

图形化导入Nginx模块

server端测试:

[root@CentOS7 ~]# zabbix_get -s 192.168.31.155 -p 10050 -k "nginx.status[active]"
1
#有数字信息返回则表示配置正常


图形化操作:

    配置-->模板-->导入   
模板下载:
zbx_export_templates.xml
http://pan.baidu.com/s/1pLFUJc3 密码:1234


为对应的主机导入nginx模板
    配置-->主机-->模板-->添加

yum部署zabbix监控

第1章 yum部署zabbix服务端

1.1 命令行部署

自己搭建的yum仓库(推荐-因为局限于网络-很坑)

[root@m01 ~]# tail -1 /etc/hosts
192.168.19.200 repo.zabbix.com mirrors.aliyun.com

获取yum源(只提供安装zabbix的源)

rpm -ivh http://repo.zabbix.com/zabbix/3.0/rhel/7/x86_64/zabbix-release-3.0-1.el7.noarch.rpm

安装zabbix,httpd,php(依赖中有httpd和php)

yum install zabbix-server-mysql zabbix-web-mysql

安装、启动 mariadb (mysql被oracle甲骨文公司收购)

yum -y install mariadb-server
systemctl start mariadb.service

创建数据库,权限

mysql
create database zabbix character set utf8 collate utf8_bin;
grant all privileges on zabbix.* to zabbix@localhost identified by 'zabbix';
flush privileges;
exit

导入数据库sql文件(安装server时下载的)

zcat /usr/share/doc/zabbix-server-mysql-3.0.13/create.sql.gz|mysql -uzabbix -pzabbix zabbix

配置zabbix Server连接mysql(yum安装时,大部分不需要修改)

sed -i.ori '115a DBPassword=zabbix' /etc/zabbix/zabbix_server.conf

修改apache-php配置 (yum安装时,大部分不需要修改)

sed -i.ori '18a php_value date.timezone  Asia/Shanghai' /etc/httpd/conf.d/zabbix.conf

启动 zabbix-server httpd

systemctl start zabbix-server
systemctl start httpd

=======至此,进入web界面进行操作=====

1.1.1 yum源配置-解释

推荐自己搭建yum仓库,安装会很顺利,不局限于网络yum源的各种安装不上的问题。(特别全的需要50G以上)
先准备yum源

[root@CentOS7 ~]# rpm -ql zabbix-release-3.0-1.el7.noarch
/etc/pki/rpm-gpg/RPM-GPG-KEY-ZABBIX
/etc/yum.repos.d/zabbix.repo
/usr/share/doc/zabbix-release-3.0
/usr/share/doc/zabbix-release-3.0/GPL

方法一:网上直接下载(网上安装)

rpm -ivh http://repo.zabbix.com/zabbix/3.0/rhel/7/x86_64/zabbix-release-3.0-1.el7.noarch.rpm

方法二:先自行下载此rpm包,再上传,最后安装(网上安装)
未分类

 [root@CentOS7 ~]# rpm -ivh zabbix-release-3.0-1.el7.noarch.rpm

方法三:自己搭建本地yum仓库,(推荐-下载速度更快)
未分类

1.1.2 MariaDB 与 mysql

MariaDB数据库管理系统是MySQL的一个分支,主要由开源社区在维护,采用GPL授权许可。开发这个分支的原因之一是:甲骨文公司收购了MySQL后,有将MySQL闭源的潜在风险,因此社区采用分支的方式来避开这个风险。
MariaDB的目的是完全兼容MySQL,包括API和命令行,使之能轻松成为MySQL的代替品。

1.2 zabbix-web界面设置

1.2.1 web图文过程详解

未分类
未分类
未分类
未分类
未分类

1.3 添加被监控主机

所有需要被监控的服务器都要执行
更新yum源(或者将此rpm包下载到本地再安装)

rpm -ivh http://repo.zabbix.com/zabbix/3.0/rhel/7/x86_64/zabbix-release-3.0-1.el7.noarch.rpm

安装zabbix-agent

yum install zabbix-agent

配置文件

sed -i.ori 's#Server=127.0.0.1#Server=172.16.1.61#' /etc/zabbix/zabbix_agentd.conf

启动

systemctl start zabbix-agent.service

1.3.1 zabbix-web界面操作

未分类

1.3.2 字符集优化

安装 “文泉驿-微米黑字体”

yum -y install wqy-microhei-fonts

替换原有字体

cp /usr/share/fonts/wqy-microhei/wqy-microhei.ttc /usr/share/fonts/dejavu/DejaVuSans.ttf

刷新web页面即可
未分类

Nginx 504报错,PHP-FPM无响应的问题

问题

测试环境,压测接口中间件时遇到报错Nginx 504,查询nginx日志

2017/11/21 15:20:15 [error] 26954#0: *1835 connect() failed (111: Connection refused) while connecting to upstream, client: 192.168.1.46, server: 192.168.23.95, request: "POST /screenInterface/FunctionByTime.php HTTP/1.1", upstream: "fastcgi://127.0.0.1:9000", host: "192.168.23.95:6010", referrer: "http://192.168.23.95:6010/screen2/"
2017/11/21 15:20:15 [error] 26954#0: *1821 connect() failed (111: Connection refused) while connecting to upstream, client: 192.168.1.46, server: 192.168.23.95, request: "POST /screenInterface/FUnctionByMobile.php HTTP/1.1", upstream: "fastcgi://127.0.0.1:9000", host: "192.168.23.95:6010", referrer: "http://192.168.23.95:6010/screen2/"
2017/11/21 15:20:15 [error] 26954#0: *1836 connect() failed (111: Connection refused) while connecting to upstream, client: 192.168.1.46, server: 192.168.23.95, request: "POST /screenInterface/LeftGraph.php HTTP/1.1", upstream: "fastcgi://127.0.0.1:9000", host: "192.168.23.95:6010", referrer: "http://192.168.23.95:6010/screen2/"

资料

分析与解决

初步推测为php-fpm无响应:

  • 能够成功访问nginx静态资源
  • 本地9000端口成功访问php资源php ./info.php
  • nginx error code 504

查询PHP-fpm的error日志发现报错:

[21-Nov-2017 12:28:30] WARNING: [pool www] server reached pm.max_children setting (5), consider raising it
[21-Nov-2017 12:33:29] WARNING: [pool www] server reached pm.max_children setting (5), consider raising it
[21-Nov-2017 14:10:57] WARNING: [pool www] server reached pm.max_children setting (5), consider raising it
[21-Nov-2017 14:31:05] WARNING: [pool www] server reached pm.max_children setting (5), consider raising it
[21-Nov-2017 14:57:54] ERROR: unable to bind listening socket for address '127.0.0.1:9000': Address already in use (98)
[21-Nov-2017 14:57:54] ERROR: FPM initialization failed
[21-Nov-2017 14:58:32] NOTICE: fpm is running, pid 26714
[21-Nov-2017 14:58:32] NOTICE: ready to handle connections
[21-Nov-2017 14:59:50] WARNING: [pool www] server reached pm.max_children setting (5), consider raising it
[21-Nov-2017 15:20:12] NOTICE: Terminating ...
[21-Nov-2017 15:20:12] NOTICE: exiting, bye-bye!
[21-Nov-2017 15:20:25] NOTICE: fpm is running, pid 26972
[21-Nov-2017 15:20:25] NOTICE: ready to handle connections
[21-Nov-2017 15:20:44] WARNING: [pool www] server reached pm.max_children setting (5), consider raising it

推测原因为pm.max_children设置过小,将增大该值后重启中间件,问题解决。

反思

Nginx 502 & Nginx 504

  • Nginx 502 Bad Gateway的含义是请求的PHP-CGI已经执行,但是由于某种原因(一般是读取资源的问题)没有执行完毕而导致PHP-CGI进程终止。
  • Nginx 504 Gateway Time-out的含义是所请求的网关没有请求到,简单来说就是没有请求到可以执行的PHP-CGI。

关于pm.max_children

一个前提设置: pm = static/dynamic,这个选项是标识fpm子进程的产生模式:

  • static :表示在fpm运行时直接fork出pm.max_chindren个worker进程,
  • dynamic:表示,运行时fork出start_servers个进程,随着负载的情况,动态的调整,最多不超过max_children个进程。

一般推荐用static,优点是不用动态的判断负载情况,提升性能,缺点是多占用些系统内存资源。

max_chindren代表的worker的进程数。对于配置越多能同时处理的并发也就越多,则是一个比较大的误区:

  • 管理进程和worker进程是通过pipe进行数据通讯的。所以进程多了,增加进程管理的开销,系统进程切换的开销,更核心的是,能并发执行的fpm进程不会超过cpu个数。因此通过多开worker的个数来提升qps是错误的。
  • 但worker进程开少了,如果server比较繁忙的话,会导到nginx把数据打到fpm的时候,发现所有的woker都在工作中,没有空闲的worker来接受请求,从而导致502。

如何配置max_children及优化PHP-FPM

php-fpm.conf有两个至关重要的参数:一个是”max_children”,另一个是”request_terminate_timeout”.

  • request_terminate_timeout的值可以根 据你服务器的性能进行设定。一般来说性能越好你可以设置越高,20分钟-30分钟都可以。由于服务器PHP脚本需要长时间运行,有的可能会超过10分钟因此我设置了900秒,这样不会导致PHP-CGI死掉而出现502 Bad gateway这个错误。
  • max_children的值原则上是越大越好,php-cgi的进程多了就会处理的很快,排队的请求就会很少。设置”max_children” 也需要根据服务器的性能进行设定,一般来说一台服务器正常情况下每一个php-cgi所耗费的内存在20M左右,因此”max_children”我设置成40个,20M*40=800M也就是说在峰值的时候所有PHP-CGI所耗内存在800M以内,低于我的有效内存1Gb。而如果我 的”max_children”设置的较小,比如5-10个,那么php-cgi就会“很累”,处理速度也很慢,等待的时间也较长。如果长时间没有得到处 理的请求就会出现504 Gateway Time-out这个错误,而正在处理的很累的那几个php-cgi如果遇到了问题就会出现502 Bad gateway这个错误。
  • max_requests:每个进程若超过这个数目(跟php进程有一点点关系,关系不大),就自动杀死。

Linux 配置 nginx、mysql、php-fpm、redis 开机启动

Linux(CentOS)上配置 nginx、mysql、php-fpm、redis 开机启动,编写开机启动脚本。

系统环境: CentOS Linux

I、nginx开机启动

1. 在/etc/init.d/目录下创建脚本

vim  /etc/init.d/nginx

2. 更改脚本权限

chmod 775 /etc/init.d/nginx

3. 编写脚本内容

#!/bin/bash
# nginx Startup script for the Nginx HTTP Server
# it is v.0.0.2 version.
# chkconfig: - 85 15
# description: Nginx is a high-performance web and proxy server.
#              It has a lot of features, but it's not for everyone.
# processname: nginx
# pidfile: /var/run/nginx.pid
# config: /usr/local/nginx/conf/nginx.conf
nginxd=/usr/local/webserver/nginx/sbin/nginx
nginx_config=/usr/local/webserver/nginx/conf/nginx.conf
nginx_pid=/usr/local/webserver/nginx/logs/nginx.pid
RETVAL=0
prog="nginx"
# Source function library.
.  /etc/rc.d/init.d/functions
# Source networking configuration.
.  /etc/sysconfig/network
# Check that networking is up.
[ ${NETWORKING} = "no" ] && exit 0
[ -x $nginxd ] || exit 0
# Start nginx daemons functions.
start() {
if [ -e $nginx_pid ];then
   echo "nginx already running...."
   exit 1
fi
   echo -n $"Starting $prog: "
   daemon $nginxd -c ${nginx_config}
   RETVAL=$?
   echo
   [ $RETVAL = 0 ] && touch /var/lock/subsys/nginx
   return $RETVAL
}
# Stop nginx daemons functions.
stop() {
        echo -n $"Stopping $prog: "
        killproc $nginxd
        RETVAL=$?
        echo
        [ $RETVAL = 0 ] && rm -f /var/lock/subsys/nginx /usr/local/webserver/nginx/logs/nginx.pid
}

reload() {
    echo -n $"Reloading $prog: "
    #kill -HUP `cat ${nginx_pid}`
    killproc $nginxd -HUP
    RETVAL=$?
    echo
}
# See how we were called.
case "$1" in
start)
        start
        ;;
stop)
        stop
        ;;
reload)
        reload
        ;;
restart)
        stop
        start
        ;;
status)
        status $prog
        RETVAL=$?
        ;;
*)
        echo $"Usage: $prog {start|stop|restart|reload|status|help}"
        exit 1
esac
exit $RETVAL

4. 设置开机启动

chkconfig nginxd on

II、设置mysql开机启动

将mysql安装目录下 support-files目录下的mysql.server文件拷贝到/etc/init.d/目录下并改名为mysqld,并更改权限

chmod 775 /etc/init.d/mysqld

设置开机启动

chkconfig mysqld on

III、php-fpm开机启动

1. 在/etc/init.d/目录下创建脚本

vim /etc/init.d/php-fpm

2. 更改脚本权限

chmod 775 /etc/init.d/php-fpm

3. 编写脚本内容

#!/bin/sh
#
# php-fpm - this script starts and stops the php-fpm daemin
#
# chkconfig: - 85 15
# processname: php-fpm
# config:      /usr/local/php/etc/php-fpm.conf

set -e

PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
DESC="php-fpm daemon"
NAME=php-fpm
DAEMON=/usr/local/php/sbin/$NAME     //这里设成自己的目录
CONFIGFILE=/usr/local/php/etc/php-fpm.conf   //这里设成自己的目录
PIDFILE=/usr/local/php/var/run/$NAME.pid   //这里设成自己的目录
SCRIPTNAME=/etc/init.d/$NAME   //这里设成自己的目录

# If the daemon file is not found, terminate the script.
test -x $DAEMON || exit 0

d_start(){
    $DAEMON -y $CONFIGFILE || echo -n " already running"
}

d_stop(){
    kill -QUIT `cat $PIDFILE` || echo -n " no running"
}

d_reload(){
    kill -HUP `cat $PIDFILE` || echo -n " could not reload"
}

case "$1" in
    start)
        echo -n "Starting $DESC: $NAME"
        d_start
        echo "."
        ;;
    stop)
        echo -n "Stopping $DESC: $NAME"
        d_stop
        echo "."
        ;;
    reload)
        echo -n "Reloading $DESC configuration..."
        d_reload
        echo "Reloaded."
        ;;
    restart)
        echo -n "Restarting $DESC: $NAME"
        d_stop
        # Sleep for two seconds before starting again, this should give the nginx daemon some time to perform a graceful stop
        sleep 2
        d_start
        echo "."
        ;;
    *)
        echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload)" >&2
        exit 3
        ;;
esac
exit 0

4. 设置开机启动

chkconfig php-fpm on

Ⅳ、redis 开机启动

1. 在/etc/init.d/目录下创建脚本

vim /etc/init.d/redis

2. 更改脚本权限

chmod 775 /etc/init.d/redis

3. 编写脚本内容

###########################
PATH=/usr/local/bin:/sbin:/usr/bin:/bin

REDISPORT=6379
EXEC=/usr/local/bin/redis-server
REDIS_CLI=/usr/local/bin/redis-cli

PIDFILE=/var/run/redis.pid
CONF="/etc/redis.conf"

case "$1" in
    start)
        if [ -f $PIDFILE ]
        then
                echo "$PIDFILE exists, process is already running or crashed"
        else
                echo "Starting Redis server..."
                $EXEC $CONF
        fi
        if [ "$?"="0" ]
        then
              echo "Redis is running..."
        fi
        ;;
    stop)
        if [ ! -f $PIDFILE ]
        then
                echo "$PIDFILE does not exist, process is not running"
        else
                PID=$(cat $PIDFILE)
                echo "Stopping ..."
                $REDIS_CLI -p $REDISPORT SHUTDOWN
                while [ -x ${PIDFILE} ]
               do
                    echo "Waiting for Redis to shutdown ..."
                    sleep 1
                done
                echo "Redis stopped"
        fi
        ;;
   restart|force-reload)
        ${0} stop
        ${0} start
        ;;
  *)
    echo "Usage: /etc/init.d/redis {start|stop|restart|force-reload}" >&2
        exit 1
esac
##############################

4. 设置开机启动

chkconfig redis on

至此,大功告成。

可以用命令 chkconfig 查看开机启动服务列表

chkconfig --list

附录:

1、nigx重启错误

bind() to 0.0.0.0:80 failed (98: Address already in use)

这个是nginx重启是 经常遇到的。 网上找了很多信息 都是没有啥用。说的乱七八糟的。 发现原来是nginx重复重启。自己占用了端口。 解决方法

killall -9 nginx

杀掉nginx 进程 然后重启就行了。

service nginx restart

2、php-fpm 启动 关闭

php-fpm 不再支持 php-fpm 补丁具有的 /usr/local/php/sbin/php-fpm (start|stop|reload)等命令,需要使用信号控制:

master 进程可以理解以下信号

  • SIGINT, SIGTERM 立刻终止
  • SIGQUIT 平滑终止
  • SIGUSR1 重新打开日志文件
  • SIGUSR2 平滑重载所有worker进程并重新载入配置和二进制模块

示例:

php-fpm 关闭:

kill -SIGINT `cat /usr/local/php/var/run/php-fpm.pid`

php-fpm 重启:

kill -SIGUSR2 `cat /usr/local/php/var/run/php-fpm.pid`

其次配置文件不再使用的xml 格式,改为了INI,但是配置参数几乎和以前一样,可参照xml格式的格式配置。

3、nginx 启动 关闭

  • nginx的启动 (nginx.conf文件基本上位于nginx主目录中的conf目录中)
nginx -c nginx.conf
  • nginx的停止 (nginx.pid文件基本上位于nginx主目录中的logs目录中)
ps -ef | grep nginx

可发现数个nginx进程,其中标有master的为主进程,其它为子进程, 停止nginx主要就是对主进程进行信号控制.

从容停止

kill -QUIT `cat nginx.pid`

快速停止

kill -TERM `cat nginx.pid`

or

kill -INT `cat nginx.pid`

强制停止

kill -9 `cat nginx.pid`

nginx的平滑重启

首先要验证新的配置文件是否正确:

nginx -t -c nginx.conf

成功后向主进程发送HUP信号即可: [/shell]kill -HUP cat nginx.pid[/shell]

4、nginx 平滑升级

  1. 备份好旧的可执行文件,使用新版本替换旧版本

  2. kill -USR2 旧版本的主进程PID 进行平滑升级, 此时新老版本共存

  3. kill -WINCH 旧版本的主进程PID 逐步关闭旧主进程的工作进程

  4. 当旧主进程产生的工作进程全部关闭后, 可以决定是否使用新版本还是旧版本.(需要使用kill命令来杀死新或旧主进程)

#!/bin/sh
BASE_DIR='/usr/local/'
${BASE_DIR}nginx/sbin/nginx -t -c ${BASE_DIR}nginx/conf/nginx.conf >& ${BASE_DIR}nginx/logs/nginx.start
info=`cat ${BASE_DIR}nginx/logs/nginx.start`
if [ `echo $info | grep -c "syntax is ok" ` -eq 1 ]; then
if [ `ps aux|grep "nginx"|grep -c "master"` == 1 ]; then
kill -HUP `cat ${BASE_DIR}nginx/logs/nginx.pid`
echo "ok"
else
killall -9 nginx
sleep 1
${BASE_DIR}nginx/sbin/nginx
fi
else
echo "######## error: ########"
cat ${BASE_DIR}nginx/logs/nginx.start
fi

5、CentOS修改系统环境变量

我这里拿php作为一个例子,我的php安装在/usr/local/webserver/php下,没有把php加入环境变量时,你在命令行执行

# 查看当前php的版本信息
[root@CentOS ~]# php -v

会提示你此命令不存在。

下面详细说说linux下修改环境变量的方法

方法一:

在/etc/profile文件中添加变量【对所有用户生效(永久的)】
用VI在文件/etc/profile文件中增加变量,该变量将会对Linux下所有用户有效,并且是“永久的”。

[root@CentOS ~]# vim /etc/profile

在文件末尾加上如下两行代码

PATH=/usr/local/webserver/php/bin:$PATH
export PATH

如:

# /etc/profile

# System wide environment and startup programs, for login setup
# Functions and aliases go in /etc/bashrc

# It's NOT a good idea to change this file unless you know what you
# are doing. It's much better to create a custom.sh shell script in
# /etc/profile.d/ to make custom changes to your environment, as this
# will prevent the need for merging in future updates.

pathmunge () {
    case ":${PATH}:" in
        *:"$1":*)
            ;;
        *)
            if [ "$2" = "after" ] ; then
                PATH=$PATH:$1
            else
                PATH=$1:$PATH
            fi
    esac
}

if [ -x /usr/bin/id ]; then
    if [ -z "$EUID" ]; then
        # ksh workaround
        EUID=`id -u`
        UID=`id -ru`
    fi
    USER="`id -un`"
    LOGNAME=$USER
    MAIL="/var/spool/mail/$USER"
fi

# Path manipulation
if [ "$EUID" = "0" ]; then
    pathmunge /sbin
    pathmunge /usr/sbin
    pathmunge /usr/local/sbin
else
    pathmunge /usr/local/sbin after
    pathmunge /usr/sbin after
    pathmunge /sbin after
fi

HOSTNAME=`/bin/hostname 2>/dev/null`
HISTSIZE=1000
if [ "$HISTCONTROL" = "ignorespace" ] ; then
    export HISTCONTROL=ignoreboth
else
    export HISTCONTROL=ignoredups
fi

export PATH USER LOGNAME MAIL HOSTNAME HISTSIZE HISTCONTROL

# By default, we want umask to get set. This sets it for login shell
# Current threshold for system reserved uid/gids is 200
# You could check uidgid reservation validity in
# /usr/share/doc/setup-*/uidgid file
if [ $UID -gt 199 ] && [ "`id -gn`" = "`id -un`" ]; then
    umask 002
else
    umask 022
fi

for i in /etc/profile.d/*.sh ; do
    if [ -r "$i" ]; then
        if [ "${-#*i}" != "$-" ]; then
            . "$i"
        else
            . "$i" >/dev/null 2>&1
        fi
    fi
done

unset i
unset pathmunge

PATH=/usr/local/webserver/php/bin:$PATH
export PATH

要是刚才的修改马上生效,需要执行以下代码

[root@CentOS ~]# source /etc/profile

这时再查看系统环境变量,就能看见刚才加的东西已经生效了

[root@CentOS ~]# echo $PATH
/usr/local/webserver/php/bin:/usr/lib/qt-3.3/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin

现在就能直接使用php命令了(而不是像之前写很长一串/usr/local/webserver/php/bin/php -v),例如查看当前php的版本

[root@CentOS ~]# php -v
PHP 5.3.8 (cli) (built: Jun 27 2012 14:28:20)
Copyright (c) 1997-2011 The PHP Group
Zend Engine v2.3.0, Copyright (c) 1998-2011 Zend Technologies

方法二:

在用户目录下的.bash_profile文件中增加变量【对单一用户生效(永久的)】
用VI在用户目录下的.bash_profile文件中增加变量,改变量仅会对当前用户有效,并且是“永久的”。具体操作和方法1一样,这里就不在列举代码了。

方法三:

直接运行export命令定义变量【只对当前shell(BASH)有效(临时的)】

在shell的命令行下直接使用[export变量名=变量值]定义变量,该变量只在当前的shell(BASH)或其子shell(BASH)下是有效的,shell关闭了,变量也就失效了,再打开新shell时就没有这个变量,需要使用的话还需要重新定义。例如

export PATH=/usr/local/webserver/php/bin:$PATH

Linux下安装配置OpenResty,并测试在Nginx中使用Lua编程

一、简介

OpenResty,也被称为“ngx_openresty”,是一个以Nginx为核心同时包含很多第三方模块的Web应用服务器。借助于Nginx的事件驱动模型和非阻塞IO,可以实现高性能的Web应用程序。 OpenResty不是Nginx的分支,它只是一个软件包。主要有章亦春维护。

OpenResty默认集成了Lua开发环境,而且提供了大量组件如Mysql、Redis、Memcached等,使得在Nginx上开发Web应用更方便简单。

二、安装OpenResty

[root@hbase31 src]# wget https://openresty.org/download/openresty-1.11.2.5.tar.gz
[root@hbase31 src]# tar -zxvf openresty-1.13.6.1.tar.gz
[root@hbase31 openresty-1.13.6.1]# ./configure --prefix=/usr/local/openresty --user=www --group=www --with-http_stub_status_module --with-http_ssl_module --with-openssl=/usr/local/ssl --with-pcre=/usr/local/src/pcre-8.38 --add-module=/usr/local/src/ngx_cache_purge-2.3 --with-http_gzip_static_module --with-luajit
[root@hbase31 openresty-1.13.6.1]# make && make install

注:关于这里的编译参数可以认为是在Nginx的编译参数的基础上添加了其他组件的参数。如需查看更多参数可以使用以下命令:

[root@hbase31 openresty-1.13.6.1]# ./configure --help

配置nginx的启动脚本:

[root@hbase31 openresty-1.13.6.1]# vim /etc/init.d/nginx

添加如下内容:

#!/bin/bash
# nginx Startup script for the Nginx HTTP Server
# it is v.1.3.0 version.
# chkconfig: - 85 15
# description: Nginx is a high-performance web and proxy server.
#              It has a lot of features, but it's not for everyone.
# processname: nginx
# pidfile: /var/run/nginx.pid
# config: /usr/local/openresty/nginx/conf/nginx.conf
nginxd=/usr/local/openresty/nginx/sbin/nginx
nginx_config=/usr/local/openresty/nginx/conf/nginx.conf
nginx_pid=/usr/local/openresty/nginx/logs/nginx.pid
RETVAL=0
prog="nginx"
# Source function library.
.  /etc/rc.d/init.d/functions
# Source networking configuration.
.  /etc/sysconfig/network
# Check that networking is up.
[ ${NETWORKING} = "no" ] && exit 0
[ -x $nginxd ] || exit 0
# Start nginx daemons functions.
start() {
if [ -e $nginx_pid ];then
   echo "nginx already running...."
   exit 1
fi
   echo -n $"Starting $prog: "
   daemon $nginxd -c ${nginx_config}
   RETVAL=$?
   echo
   [ $RETVAL = 0 ] && touch /var/lock/subsys/nginx
   return $RETVAL
}
# Stop nginx daemons functions.
stop() {
        echo -n $"Stopping $prog: "
        killproc $nginxd
        RETVAL=$?
        echo
        [ $RETVAL = 0 ] && rm -f /var/lock/subsys/nginx $nginx_pid
}
reload() {
    echo -n $"Reloading $prog: "
    #kill -HUP `cat ${nginx_pid}`
    killproc $nginxd -HUP
    RETVAL=$?
    echo
}
# See how we were called.
case "$1" in
start)
        start
        ;;
stop)
        stop
        ;;
reload)
        reload
        ;;
restart)
        stop
        start
        ;;

status)
        status $prog
        RETVAL=$?
        ;;
*)
        echo $"Usage: $prog {start|stop|restart|reload|status|help}"
        exit 1
esac
exit $RETVAL

添加可执行权限:

[root@hbase31 openresty-1.13.6.1]# chmod a+x /etc/init.d/nginx

启动nginx:

[root@hbase31 openresty-1.13.6.1]# service nginx start

三、在Nginx中使用Lua脚本

[root@hbase31 vhost]# cd /usr/local/openresty/nginx/conf
[root@hbase31 conf]# mkdir lua vhost

(1)测试在Nginx中使用Lua脚本

[root@hbase31 nginx]# vim /usr/local/openresty/nginx/conf/vhost/lua.conf

其内容如下:

server {
    server_name localhost;
    listen 3000;
    index index.html index.htm index.jsp;

    location / {
        root /usr/local/openresty/nginx/html;
    }

    location /lua {
        default_type text/plain;
        content_by_lua 'ngx.say("hello,lua!")';
    } 

    limit_conn perip 1000;
    access_log logs/access_rua.log;
}

测试是否可以访问:

[root@hbase31 nginx]# service nginx reload

然后访问:http://192.168.1.31:3000/lua

如果输出以下内容则证明在Nginx中可以执行Lua脚本:

hello,lua!

(2)在Nginx中使用Lua脚本访问Redis

i)连接Redis集群,然后添加测试参数:

192.168.1.30:7000> set '123' '456'

ii)添加连接Redis的Lua脚本:

[root@hbase31 nginx]# vim /usr/local/openresty/nginx/conf/lua/redis.lua

其内容如下:

local redis = require "resty.redis"
local conn = redis.new()
conn.connect(conn, '192.168.1.30', '7000')
local res = conn:get("123")
if res==ngx.null then
    ngx.say("redis集群中不存在KEY——'123'")
    return
end
ngx.say(res)

iii)在上面的lua.conf配置文件中添加以下location:

    location /lua_redis {
        default_type text/plain;
        content_by_lua_file /usr/local/openresty/nginx/conf/lua/redis.lua;
    }
1
2
3
4
    location /lua_redis {
        default_type text/plain;
        content_by_lua_file /usr/local/openresty/nginx/conf/lua/redis.lua;
    }

iv)测试是否可以访问:

[root@hbase31 nginx]# service nginx reload

然后访问:http://192.168.1.31:3000/lua_redis

如果输出以下内容则证明可以访问redis:

456

Openresty最佳案例 | 第9篇:Openresty实现的网关权限控制

简介

采用openresty 开发出的api网关有很多,比如比较流行的kong、orange等。这些API 网关通过提供插件的形式,提供了非常多的功能。这些组件化的功能往往能够满足大部分的需求,如果要想达到特定场景的需求,可能需要二次开发,比如RBAC权限系统。本小节通过整合前面的知识点,来构建一个RBAC权限认证系统。

技术栈

本小节采用了以下的技术栈:

  • Openresty(lua+nginx)
  • mysql
  • redis
  • cjson

验证流程

  • 用户请求经过nginx,nginx的openresty的模块通过拦截请求来进行权限判断
  • openresty的access_by_lua_file模块,进行了一系列的判断
    • 用户的请求是否为白名单uri,如果为白名单uri,则直接通过验证, 进入下一个验证环节content_by_lua_file,这个环节直接打印一句话:“恭喜,请求通过。”
    • 如果用户请求不为白名单url,则需要取出请求header中的token,如果请求的header不存在token,则直接返回结果401,无权限访问。
      如果用户请求的uri的请求头包含token ,则取出token,解密token取出用户id
    • 根据取出的userid去查询数据库获取该用户的权限,如果权限包含了该请求的uri,请求可以通过,否则,请求不通过。
  • 请求如果通过access_by_lua_file模块,则进入到content_by_lua_file模块,该模块直接返回一个字符串给用户请求,在实际的开发中,可能为路由到具体的应用程序的服务器。

验证流程图如下所示:

未分类

vim /usr/example/example.conf ,加上以下的配置:

 location / {
    default_type "text/html";
    access_by_lua_file /usr/example/lua/api_access.lua;
    content_by_lua_file /usr/example/lua/api_content.lua;
  }

以上的配置表示,要不符合已有location路径的所有请求,将走这个location为/ 的路径。符合这个location的请求将进入 access_by_lua_file和 content_by_lua_file的模块判断。

vim /usr/example/lua/access_by_lua_file ,加上以下代码:

local tokentool = require "tokentool"
local mysqltool = require "mysqltool"

 function is_include(value, tab)
   for k,v in ipairs(tab) do
      if v == value then
           return true
       end
    end
    return false
 end

local white_uri={"/user/login","/user/validate"}

--local user_id = ngx.req.get_uri_args()["userId"]
--获取header的token值
local headers = ngx.req.get_headers() 
local token=headers["token"]
local url=ngx.var.uri
if ( not token) or (token==null) or (token ==ngx.null) then
  if is_include(url,white_uri)then

  else
    return ngx.exit(401)
  end  
else 
  ngx.log(ngx.ERR,"token:"..token)
  local user_id=tokentool.get_user_id(token)
  if (not user_id) or( user_id ==null) or ( user_id == ngx.null) then
      return ngx.exit(401)   
  end 

  ngx.log(ngx.ERR,"user_id"..user_id)
  local permissions={}
  permissions =tokentool.get_permissions(user_id)
  if(not permissions)or(permissions==null)or( permissions ==ngx.null) then
      permissions= mysqltool.select_user_permission(user_id)
      if permissions and permissions ~= ngx.null then
         tokentool.set_permissions(user_id,permissions)
      end
  end  
  if(not permissions)or(permissions==null)or( permissions ==ngx.null) then
     return ngx.exit(401)
  end 
  local is_contain_permission = is_include(url,permissions) 

  if is_contain_permission == true  then
     -- ngx.say("congratuation! you have pass the api gateway")
  else
      return ngx.exit(401) 
  end   
end

在上述代码中:

  • is_include(value, tab),该方法判断某个字符串在不在这个table中。
  • white_uri={“/user/login”,”/user/validate”} 是一个白名单的列表。
  • local headers = ngx.req.get_headers()从请求的uri的请求头获取token
  • is_include(url,white_uri)判断该url是否为白名单url
  • local user_id=tokentool.get_user_id(token)根据token获取该token对应的用户的user_id,在常见情况下,是根据token解析出user_id,但在不同的语言加密和加密token存在盐值不一样的情况,比较麻烦,所以我偷了个懒,直接存了redis,用户登录成功后存一下。
  • permissions =tokentool.get_permissions(user_id)根据user_id
    从redis获取该用户的权限。
  • permissions= mysqltool.select_user_permission(user_id)如果redis没有存该用户的权限,则从数据库读。
  • tokentool.set_permissions(user_id,permissions),将从数据库中读取的权限点存在reddis中。
  • local is_contain_permission = is_include(url,permissions),判断该url 在不在该用户对应的权限列表中。

如果所有的判断通过,则该用户请求的具有权限访问,则进入content_by_lua_file模块,直接在这个模块给请求返回“congratulations! you have passed the api gateway”。

vim /usr/example/lua/api_content.lua ,添加以下内容:

ngx.say("congratulations!"," you have passed ","the api gateway")  
----200状态码退出  
return ngx.exit(200)  

验证演示

打开浏览器访问http://116.196.177.123/user/login,浏览器显示:

congratulations! you have passed the api gateway

/user/login这个url 在白名单的范围内,所以它是可以通过权限验证的。

打开浏览器访问http://116.196.177.123/user/sss,显示以下内容:

401 Authorization Required

openresty/1.11.2.4

在redis中添加一对key-value,key为token_forezp,value为1,即token_forezp对应的用户的id为1.

/usr/servers/redis-3.2.6

src/redis-cli

set token_forezp 1

初始化以下的sql脚本,即给用户id为1的用户关联角色,角色并关联权限:

INSERT INTO `permission` VALUES ('1', '/user/orgs');
INSERT INTO `role` VALUES ('1', 'user');
INSERT INTO `role_permission` VALUES ('1', '1', '1');
INSERT INTO `user` VALUES ('1', 'forezp');
INSERT INTO `user_role` VALUES ('1', '1', '1');

用postman请求,在请求头中加入token,值为token_forezp,请求结果如下:

未分类

Openresty最佳案例 | 第8篇:RBAC介绍、sql和redis模块工具类

RBAC介绍

RBAC(Role-Based Access Control,基于角色的访问控制),用户基于角色的访问权限控制。简单地说,一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般都是多对多的关系。如图所示:

未分类

sql_tool

在本案例中,采用的就是这种权限设计的方式。具体的sql语句脚本如下:

CREATE TABLE `user` (
`id`  int(11) NOT NULL AUTO_INCREMENT ,
`name`  varchar(255) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=latin1 COLLATE=latin1_swedish_ci
AUTO_INCREMENT=2
ROW_FORMAT=COMPACT
;


CREATE TABLE role(
`id`  int(11) NOT NULL AUTO_INCREMENT ,
`name`  varchar(255) CHARACTER SET latin5 NULL DEFAULT NULL ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=latin1 COLLATE=latin1_swedish_ci
AUTO_INCREMENT=2
ROW_FORMAT=COMPACT
;

CREATE TABLE permission(
`id`  int(11) NOT NULL AUTO_INCREMENT ,
`permission`  varchar(255) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=latin1 COLLATE=latin1_swedish_ci
AUTO_INCREMENT=3
ROW_FORMAT=COMPACT
;

CREATE TABLE user_role(
`id`  int(11) NOT NULL AUTO_INCREMENT ,
`user_id`  int(11) NULL DEFAULT NULL ,
`role_id`  int(11) NULL DEFAULT NULL ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=latin1 COLLATE=latin1_swedish_ci
AUTO_INCREMENT=2
ROW_FORMAT=COMPACT
;


CREATE TABLE role_permission(
`id`  int(11) NOT NULL AUTO_INCREMENT ,
`role_id`  int(11) NULL DEFAULT NULL ,
`permission_id`  int(11) NULL DEFAULT NULL ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=latin1 COLLATE=latin1_swedish_ci
AUTO_INCREMENT=3
ROW_FORMAT=COMPACT
;

初始化以下的sql脚本,即给用户id为1的用户关联角色,角色并关联权限:

INSERT INTO `permission` VALUES ('1', '/user/orgs');
INSERT INTO `role` VALUES ('1', 'user');
INSERT INTO `role_permission` VALUES ('1', '1', '1');
INSERT INTO `user` VALUES ('1', 'forezp');
INSERT INTO `user_role` VALUES ('1', '1', '1');

在本案例中,需要根据user表中的Id获取该Id对应的权限。首先根据userId获取该用户对应的角色,再根据根据该角色获取相应的权限,往往一个用户具有多个角色,而角色又有多个权限。比如查询userId为1 的用户的权限的sql语句如下:

SELECT  a.id,a.permission from permission a ,role_permission b,role c,user_role d,user e WHERE a.id=b.permission_id and c.id=b.role_id and d.role_id=c.id and d.user_id=e.id and e.id=1"

在Openresty中怎么连接数据库,怎么查询sql语句,在之前的文章已将讲述过了。根据用户id获取用户的权限的功能是一个使用率极高的功能,所以考虑将这个功能模块化。

vim /usr/example/lualib/sql_tool.lua ,编辑加入以下的代码:

local mysql = require("resty.mysql")  

local function close_db(db)  
    if not db then  
        return  
    end  
    db:close()  
end  

local function select_user_permission(user_id)

   local db, err = mysql:new()
   if not db then  
      ngx.say("new mysql error : ", err)  
      return  
   end 
   db:set_timeout(1000)  

   local props = {  
      host = "127.0.0.1",  
      port = 3306,  
      database = "test",  
      user = "root",  
      password = "123"  
   }

  local res, err, errno, sqlstate = db:connect(props)  

  if not res then  
     ngx.say("connect to mysql error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)  
     close_db(db)
  end

  local select_sql = "SELECT  a.id,a.permission from permission a ,role_permission b,role c,user_role d,user e WHERE a.id=b.permission_id and c.id=b.role_id and d.role_id=c.id and d.user_id=e.id and e.id="..user_id
  res, err, errno, sqlstate = db:query(select_sql)  
  if not res then  
     ngx.say("select error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)  
     return close_db(db)  
  end  

   local permissions={}
   for i, row in ipairs(res) do  
     for name, value in pairs(row) do
    if name == "permission" then
          table.insert(permissions, 1, value)
        end  

     end  
   end  
 return permissions 
end

local _M = {  
    select_user_permission= select_user_permission
}  

return _M  

在上面的代码中,有一个select_user_permission(user_id)方法,该方法根据用户名获取该用户的权限。查出来存在一个table 类型的 local permissions={}中。

vim /usr/example/example.conf 加上以下的代码:

location ~ /sql_tool{
  default_type 'text/html';
  content_by_lua_file /usr/example/lua/test_sql_tool.lua;
 }

在浏览器上访问http://116.196.177.123/sql_tool,浏览器显示如下的内容:

/user/orgs

tokentool

在之前的文章讲述了如何使用Openresty连接redis,并操作redis。 这小节将讲述如何使用openresty连接redis,并写几个方法,用于存储用户的token等,并将这些信息模块化,主要有以下几个方法:

  • close_redis(red) 通过连接池的方式释放一个连接
  • connect() 连接redis
  • has_token(token) redis中存在token 与否
  • get_user_id(token) 根据token获取用户id
  • set_permissions(user_id,permissions) 根据userid设置权限
  • get_permissions(user_id)根据userid获取权限

vim /usr/example/lualib/tokentool.lua 编辑一下内容:

module("tokentool", package.seeall)
local redis = require "resty.redis"
local str = require "resty.string"
local cjson = require("cjson")  


local redis_host = "127.0.0.1"
local redis_port = 6379

local function close_redis(red)  
    if not red then  
        return  
    end  
    local pool_max_idle_time = 10000 --毫秒  
    local pool_size = 100 --连接池大小  
    local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)  
    if not ok then  
        ngx.say("set keepalive error : ", err)  
    end  
end 

local function connect()
    local red = redis:new()
    red:set_timeout(1000)
    local ok, err = red:connect(redis_host, redis_port)
    if not ok then
        return false
    end
    --local res, err = red:auth("xiaoantimes")
    --if not res then
     -- ngx.say("failed to authenticate: ", err)
     -- return false
    --end
    --ok, err = red:select(1)
    --if not ok then
      --  return false
    --end
    return red
end

function has_token(token)
    local red = connect()
    if red == false then
        return false
    end

    local res, err = red:get(token)
    if not res then
        return false
    end
    close_redis(red)  
    return true
end

function set_permissions(user_id,permissions)
  if (permissions==null) or( permissions==ngx.null) then
     return false
  end 
  local str = cjson.encode(permissions)  
  ngx.log(ngx.ERR,"set redis p:"..str)
  local red=connect()
  if red== false then
     return false
  end
  local ok, err = red:set(user_id,str)
  if not ok then
     return false
  end
  return true 
end

function get_permissions(user_id)
  local red=connect()
  if red== false then
     return false
  end
  local res, err = red:get(user_id)
  if (not res) or (res == ngx.null) then
     return
  end 
  ngx.log(ngx.ERR,"get redis p:"..res);
  local permissions=cjson.decode(res)  
  return permissions
end

function get_user_id(token)
    local red = connect()
    local resp, err = red:get(token)  
    if not resp then  
      ngx.say("get msg error : ", err)  
      return close_redis(red)  
    end  
    close_redis(red)  
    return resp
end

vim /usr/example/lua/test_token_tool.lua,加上以下的内容:

local tokentool= require "tokentool"
local ret = tokentool.has_token("msg")
ngx.log(ngx.ERR,ret)
if ret == true then
   ngx.say("ok")
else
   ngx.say("oops,error")
end

在/usr/example/example.conf加上以下的内容:

 location ~ /token_tool{
     default_type 'text/html';
     lua_code_cache on;
     content_by_lua_file /usr/example/lua/test_token_tool.lua;

 }

打开浏览器访问http://116.196.177.123/token_tool,浏览器显示:

ok

如何通过命令行来监控zabbix

zabbix不用说都知道,监控利器,像我司用的就是zabbix来监控各种指标,但是有个问题就是,我们需要时刻盯着zabbix的界面(当然,zabbix可以配置短信报警,邮箱报警,甚至可以指定特殊事件调用的脚本,我们其实不用时刻盯着board)

有时候就想我们如果可以通过命令行来查看各种报警,是不是会非常好? 刚开始一直在想自己写一个,因为zabbix提供了丰富的api,完全可以自己写一个,直到发现了这个:

https://github.com/usit-gd/zabbix-cli.git

我大git真的是个好地方啊:

步骤:

yum install -y install python-devel python-setuptools git make python-docutils python-requests
git clone https://github.com/usit-gd/zabbix-cli.git
cd zabbix-cli
sudo ./setup.py install

然后就是初始化我们的zabbix地址:

zabbix-cli-init --zabbix-url https://zabbix.abc.com/zabbix/api_jsonrpc.php

这个运行之后,我们会得到这个文件:

~/.zabbix-cli/zabbix-cli.conf

然后我们要设置我们的帐号和密码:

vi ~/.zabbix-cli_auth

然后,我们就可以开始通过命令行进行监控了:

编写脚本:

#!/usr/bin/env bash
zabbix-cli -C "show_alarms * "'priority': '4'" prod true"

或者我们直接运行:

zabbix-cli -C "show_alarms * "'priority': '4'" prod true"

这个时候我们就得到了我们的报警列表了。