Logstash的容器化与Ansible多环境下单配置文件发布

原因

为了发布、迁移方便,最近决定将公司项目中用的logstash容器化,最初原打算沿用原多进程方案,在容器内通过supervisor启动多个不同配置进程,但使用官方容器时并支持启动多个实例。

最终解决方式:将集群的配置文件组装在一起,不同的path对应不同的filter与output;由于不同主机上组件不同,日志也不同。因此在ansible发布时通过脚本检测生成log file 的path列表,适配集群上的不同组件。这样的好处是只需要维护一份配置,操作简单。

官方logstash镜像

在dockerhub上搜索了到一个即将被废弃的镜像:https://hub.docker.com/_/logstash/。
最新版镜像在es官方进行维护: https://www.elastic.co/guide/en/logstash/current/docker.html
由于直接使用原版镜像时映射到容器内部的pipeline.conf一直提示No permission,明明都已经设置成755,宿主机上并没有logstash这一用户,chown不方便。所以决定对官方镜像稍加修改,再推送到公司内的dockerhub私有docker registry上。

FROM docker.elastic.co/logstash/logstash:5.6.1
MAINTAINER CodingCrush
ENV LANG en_US.UTF-8
# Disable ES monitoring in the official image
ENV XPACK_MONITORING_ENABLED false
# TimeZone: Asia/Shanghai
ENV TZ Asia/Shanghai
ENV PIPELINE_WORKERS 5
# Default user is logstash
USER root

这个Dockerfile很简单,主要设置了User为root,解决conf文件的权限不足问题。另外关闭容器自带的es xpack检查功能,设置时区与pipeline的 worker数量。
然后打个tag,推到私有docker registry上。

docker build -t dockerhub.xxx.net/logstash:5.6.1
docker push dockerhub.xxx.net/logstash:5.6.1

容器便修改好了,在生产集群上可pull。

pipeline.conf

这个简化的配置文件中path应该是一个列表,但目前用了一个占位字符串,发布时动态检测通过sed进行更换,实际项目中的组件配置更多,这里写4个意思一下。
值得注意的是: 通过if else 对path进行匹配时如果出现/,需要进行转义,否则ruby进行正则匹配时会报错。

input {
    file {
        path => LOG_FILES_PLACEHOLDER
        type => "nginx"
        start_position => "beginning"
        sincedb_path => "/data/projects/logstash/data/logstash.db"
        codec=>plain{charset=>"UTF-8"}
    }
}
filter {
    if [path] =~ "component1/logs/access" {
        grok & mutate
    } else if [path] =~ "component1/logs/error" {
        grok & mutate
    } else if [path] =~ "component2/logs/access" {
        grok & mutate
    } else if [path] =~ "component2/logs/error" {
        grok & mutate
    }
}
output {
    if [path] =~ "component1/logs/access" {
        stdout{codec=>rubydebug}
    }  else if [path] =~ "component1/logs/error" {
        stdout{codec=>rubydebug}
    } else if [path] =~ "component2/logs/access.log" {
        stdout{codec=>rubydebug}
    } else if [path] =~ "component2/logs/error" {
        stdout{codec=>rubydebug}
    }
}

日志文件检测脚本 detect_logfiles.py

检测脚本同样很简单,用os.path.exists进行过滤。往标准输出打一个列表的string。
此处注意,需要用re.escape进行转义,因为后面还需要用sed进行替换。

import os
import sys
import re
paths = [
    "/data/projects/component1/logs/access.log*",
    "/data/projects/component1/logs/error.log",
    "/data/projects/component2/logs/access.log*",
    "/data/projects/component2/logs/error.log*",
    ......
]
available_paths = [path for path in paths if os.path.exists(path.replace("*", ""))]
sys.stdout.write(re.escape(str(available_paths)))

ansible剧本

在发布时通过sed替换占位串,之所以大费周章搞这么个方式,主要因为我们环境比较复杂,将来横向拓展时组件布局不定,多配置文件维护起来挺麻烦。
为什么用shell?而不用ansible的module? 因为我不会,还懒得去学ansible发明的DSL

- name: Modify lostash.conf
  args:
    chdir: "/data/projects/logstash/"
  shell: 'sed -i "s|LOG_FILES_PLACEHOLDER|$(python detect_logfiles.py)|" logstash.conf'
  become: true
  become_method: sudo
- name: Modify docker-compose.yml
  args:
    chdir: "/data/projects/logstash/"
  shell: 'sed -i "s|HOST_PLACEHOLDER|{{ host }}|" docker-compose.yml'
  become: true
  become_method: sudo

docker-compose

挂载整个文件卷,HOST_PLACEHOLDER占位符是因为我希望通过container名字直观看到组件所在的主机名,而避免生产服务器上误操作。

version: '2'
services:
  logstash:
    image: dockerhub.xxx.net/logstash:5.6.1
    volumes:
       - /etc/localtime:/etc/localtime:ro
       - /data/projects/logstash/hosts:/etc/hosts
       - /data/projects:/data/projects
    container_name: HOST_PLACEHOLDER.logstash
    command: logstash -f /data/projects/logstash/logstash.conf --path.data=/data/projects/logstash/data

启动的command时指定工作目录,将运行状态持久化到外部存储上,方便迁移。

超快的文件搜索工具Ag

前言

Ag 是类似ack, grep的工具, 它来在文件中搜索相应关键字。
官方列出了几点选择它的理由:

  • 它比ack还要快 (和grep不在一个数量级上)
  • 它会忽略.gitignore和.hgignore中的匹配文件
  • 如果有你想忽略的文件,你需要将(congh .min.js cough*)加入到.ignore文件中
  • 它的命令名称更短:-)

安装

下载源码

下载地址: http://geoff.greer.fm/ag

安装PCRE

目前已经有PCRE2,但这里需要PCRE

https://downloads.sourceforge.net/pcre/pcre-8.41.tar.bz2

从官网下载.tar.gz的版本,注意不要下载zip版本

下载后解压缩正常安装

./configure --prefix=/usr                    
            --docdir=/usr/share/doc/pcre-8.41 
            --enable-unicode-properties      
            --enable-pcre16                  
            --enable-pcre32                  
            --enable-pcregrep-libz            
            --enable-pcregrep-libbz2          
            --enable-pcretest-libreadline    
            --disable-static                &&
make && make install

默认是安装到/usr/local下

安装lzma

yum install xz-libs.x86_64 xz-devel.x86_64

安装Ag

./configure PCRE_CFLAGS="-I /usr/local/include" PCRE_LIBS="-L /usr/local/lib -lpcre" && make && make install

命令

Usage: ag [FILE-TYPE] [OPTIONS] PATTERN [PATH]

Recursively search for PATTERN in PATH.
Like grep or ack, but faster.

Example:
ag -i foo /bar/

Output Options:
--ackmate Print results in AckMate-parseable format
-A --after [LINES] Print lines after match (Default: 2)
-B --before [LINES] Print lines before match (Default: 2)
--[no]break Print newlines between matches in different files
(Enabled by default)
-c --count Only print the number of matches in each file.
(This often differs from the number of matching lines)
--[no]color Print color codes in results (Enabled by default)
--color-line-number Color codes for line numbers (Default: 1;33)
--color-match Color codes for result match numbers (Default: 30;43)
--color-path Color codes for path names (Default: 1;32)
--column Print column numbers in results
--[no]filename Print file names (Enabled unless searching a single file)
-H --[no]heading Print file names before each file's matches
(Enabled by default)
-C --context [LINES] Print lines before and after matches (Default: 2)
--[no]group Same as --[no]break --[no]heading
-g --filename-pattern PATTERN
Print filenames matching PATTERN
-l --files-with-matches Only print filenames that contain matches
(don't print the matching lines)
-L --files-without-matches
Only print filenames that don't contain matches
--print-all-files Print headings for all files searched, even those that
don't contain matches
--[no]numbers Print line numbers. Default is to omit line numbers
when searching streams
-o --only-matching Prints only the matching part of the lines
--print-long-lines Print matches on very long lines (Default: >2k characters)
--passthrough When searching a stream, print all lines even if they
don't match
--silent Suppress all log messages, including errors
--stats Print stats (files scanned, time taken, etc.)
--stats-only Print stats and nothing else.
(Same as --count when searching a single file)
--vimgrep Print results like vim's :vimgrep /pattern/g would
(it reports every match on the line)
-0 --null --print0 Separate filenames with null (for 'xargs -0')

Search Options:
-a --all-types Search all files (doesn't include hidden files
or patterns from ignore files)
-D --debug Ridiculous debugging (probably not useful)
--depth NUM Search up to NUM directories deep (Default: 25)
-f --follow Follow symlinks
-F --fixed-strings Alias for --literal for compatibility with grep
-G --file-search-regex PATTERN Limit search to filenames matching PATTERN
--hidden Search hidden files (obeys .*ignore files)
-i --ignore-case Match case insensitively
--ignore PATTERN Ignore files/directories matching PATTERN
(literal file/directory names also allowed)
--ignore-dir NAME Alias for --ignore for compatibility with ack.
-m --max-count NUM Skip the rest of a file after NUM matches (Default: 10,000)
--one-device Don't follow links to other devices.
-p --path-to-ignore STRING
Use .ignore file at STRING
-Q --literal Don't parse PATTERN as a regular expression
-s --case-sensitive Match case sensitively
-S --smart-case Match case insensitively unless PATTERN contains
uppercase characters (Enabled by default)
--search-binary Search binary files for matches
-t --all-text Search all text files (doesn't include hidden files)
-u --unrestricted Search all files (ignore .ignore, .gitignore, etc.;
searches binary and hidden files as well)
-U --skip-vcs-ignores Ignore VCS ignore files
(.gitignore, .hgignore; still obey .ignore)
-v --invert-match
-w --word-regexp Only match whole words
-W --width NUM Truncate match lines after NUM characters
-z --search-zip Search contents of compressed (e.g., gzip) files

File Types:
The search can be restricted to certain types of files. Example:
ag --html needle
- Searches for 'needle' in files with suffix .htm, .html, .shtml or .xhtml.

For a list of supported file types run:
ag --list-file-types

ag was originally created by Geoff Greer. More information (and the latest release)
can be found at http://geoff.greer.fm/ag

自动安装脚本示例

#!/usr/bin/env bash
#Author: Harris Zhu
#Dep: make sure you have the root permission
#Usage . install_ag.sh
set -x
TEMP_DIR=$(mktemp -d Leslie.Guan.XXXXXX)
cd ${TEMP_DIR}
wget https://github.com/ggreer/the_silver_searcher/archive/master.zip
TAR_DIR=$(unzip *.zip)
TAR_DIR=${TAR_DIR%%/*}
TAR_DIR=${TAR_DIR##*:}
cd ${TAR_DIR}
apt-get install -y automake pkg-config libpcre3-dev zlib1g-dev liblzma-dev --force-yes
./build.sh && make install
cd ../../
rm -rf ${TEMP_DIR}
ag -V
set +x

pattern

示例一

未分类

由上面例子可以知道ag的pattern支持s, w等正则

后言

ag的使用非常简单,它的选项也不多,所以我在上面列出了它的help内容。

Redis 乐观锁

乐观锁大多是以数据版本号来进行成功或者失败!

举个例子:

假设某个文章的点赞数为100,此时的version我们暂定没有异常为100.

当用户A对他进行点赞的时候进行操作,那此时的点赞数为100+1、version=101,提交更新时,由于版本号大于数据库记录的版本号,数据被更新,此时数据记录的version=101。

然而在特殊情况下用户B是和用户A是同时进行操作的,也就是说,他获得的version也是100、点赞数为100,提交结果是点赞数为100+1、version=101,但是此时对数据库的版本发现当前数据记录的version也为101,不满足当前提交版本号大于数据库版本号,所以此时更新操作被驳回。

执行实验代码:

WATCH test

value = GET test

value = value+1

MULTI

SET test value

EXEC

由于WATCH 的key会被监视,会校验这个key是否被更改,如果该监视的key 在EXEC执行前被修改了,那么整个事务都会被驳回。

php实现代码

$redis = new redis();  
$redis->connect('127.0.0.1', 6379);  
//获取当前点赞数 
$test = $redis->get("test");  
$count = 1;   //默认每次点赞+1  

$redis->watch("test");  
$redis->multi();  
//设置延迟,方便测试效果。  
sleep(5);  
//插入抢购数据  
$redis->set("test",$test+$count);  
$res = $redis->exec();  
if($res){  
    $new_test = $redis->get("test");  
    echo "点赞成功当前点赞数为:".$new_test."<br/>";  
}else{  
    echo "点赞失败";exit;  
}  

需要注意的是如果使用同一个浏览器的多个标签页同时访问同一个URL,那么浏览器认为这些不同的请求是同一个人,会对你的每个请求进行排队,不做并发处理。不管Nginx还是Apache,都是在并发处理,只不过你的浏览器自作主张,把你的请求阻塞了。

Redis分布式锁解决抢购问题

废话不多说,首先分享一个业务场景-抢购。一个典型的高并发问题,所需的最关键字段就是库存,在高并发的情况下每次都去数据库查询显然是不合适的,因此把库存信息存入Redis中,利用redis的锁机制来控制并发访问,是一个不错的解决方案。

首先是一段业务代码:

@Transactional
public void orderProductMockDiffUser(String productId){
    //1.查库存
    int stockNum  = stock.get(productId);
    if(stocknum == 0){
        throw new SellException(ProductStatusEnum.STOCK_EMPTY);
        //这里抛出的异常要是运行时异常,否则无法进行数据回滚,这也是spring中比较基础的   
    }else{
        //2.下单
        orders.put(KeyUtil.genUniqueKey(),productId);//生成随机用户id模拟高并发
        sotckNum = stockNum-1;
        try{
            Thread.sleep(100);
        } catch (InterruptedExcption e){
            e.printStackTrace();
        }
        stock.put(productId,stockNum);
    }
}

这里有一种比较简单的解决方案,就是synchronized关键字。

public synchronized void orderProductMockDiffUser(String productId)

这就是java自带的一种锁机制,简单的对函数加锁和释放锁。但问题是这个实在是太慢了,感兴趣的可以可以写个接口用apache ab压测一下。

ab -n 500 -c 100 http://localhost:8080/xxxxxxx

下面就是redis分布式锁的解决方法。首先要了解两个redis指令
SETNX 和 GETSET,可以在redis中文网上找到详细的介绍。
SETNX就是set if not exist的缩写,如果不存在就返回保存value并返回1,如果存在就返回0。
GETSET其实就是两个指令GET和SET,首先会GET到当前key的值并返回,然后在设置当前Key为要设置Value。

首先我们先新建一个RedisLock类:

@Slf4j
@Component
public class RedisService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    /***
     * 加锁
     * @param key
     * @param value 当前时间+超时时间
     * @return 锁住返回true
     */
    public boolean lock(String key,String value){
        if(stringRedisTemplate.opsForValue().setIfAbsent(key,value)){//setNX 返回boolean
            return true;
        }
        //如果锁超时 ***
        String currentValue = stringRedisTemplate.opsForValue().get(key);
        if(!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue)<System.currentTimeMillis()){
            //获取上一个锁的时间
            String oldvalue  = stringRedisTemplate.opsForValue().getAndSet(key,value);
            if(!StringUtils.isEmpty(oldvalue)&&oldvalue.equals(currentValue)){
                return true;
            }
        }
        return false;
    }
    /***
     * 解锁
     * @param key
     * @param value
     * @return
     */
    public void unlock(String key,String value){
        try {
            String currentValue = stringRedisTemplate.opsForValue().get(key);
            if(!StringUtils.isEmpty(currentValue)&&currentValue.equals(value)){
                stringRedisTemplate.opsForValue().getOperations().delete(key);
            }
        } catch (Exception e) {
            log.error("解锁异常");
        }
    }
}

这个项目是springboot的项目。首先要加入redis的pom依赖,该类只有两个功能,加锁和解锁,解锁比较简单,就是删除当前key的键值对。我们主要来说一说加锁这个功能。

首先,锁的value值是当前时间加上过期时间的时间戳,Long类型。首先看到用setiFAbsent方法也就是对应的SETNX,在没有线程获得锁的情况下可以直接拿到锁,并返回true也就是加锁,最后没有获得锁的线程会返回false。 最重要的是中间对于锁超时的处理,如果没有这段代码,当秒杀方法发生异常的时候,后续的线程都无法得到锁,也就陷入了一个死锁的情况。我们可以假设CurrentValue为A,并且在执行过程中抛出了异常,这时进入了两个value为B的线程来争夺这个锁,也就是走到了注释*的地方。currentValue==A,这时某一个线程执行到了getAndSet(key,value)函数(某一时刻一定只有一个线程执行这个方法,其他要等待)。这时oldvalue也就是之前的value等于A,在方法执行过后,oldvalue会被设置为当前的value也就是B。这时继续执行,由于oldValue==currentValue所以该线程获取到锁。而另一个线程获取的oldvalue是B,而currentValue是A,所以他就获取不到锁啦。多线程还是有些乱的,需要好好想一想。

接下来就是在业务代码中加锁啦:首要要@Autowired注入刚刚RedisLock类,不要忘记对这个类加一个@Component注解否则无法注入

private static final int TIMEOUT= 10*1000;
@Transactional
public void orderProductMockDiffUser(String productId){
     long time = System.currentTimeMillions()+TIMEOUT;
   if(!redislock.lock(productId,String.valueOf(time)){
    throw new SellException(101,"换个姿势再试试")
    }
    //1.查库存
    int stockNum  = stock.get(productId);
    if(stocknum == 0){
        throw new SellException(ProductStatusEnum.STOCK_EMPTY);
        //这里抛出的异常要是运行时异常,否则无法进行数据回滚,这也是spring中比较基础的   
    }else{
        //2.下单
        orders.put(KeyUtil.genUniqueKey(),productId);//生成随机用户id模拟高并发
        sotckNum = stockNum-1;
        try{
            Thread.sleep(100);
        } catch (InterruptedExcption e){
            e.printStackTrace();
        }
        stock.put(productId,stockNum);
    }
    redisLock.unlock(productId,String.valueOf(time));
}

大功告成了!比synchronized快了不知道多少倍,再也不会被老板骂了!

CENTOS 7搭建GIT服务器

Centos 下构建私有git服务器

以下操作都是root账户

安装

第一步,安装git服务

yun install -y git

第二步,新建git用户

useradd git

第三步,禁止git用户,shell登录

修改/etc/passwd

git:x:1010:1010:,,,:/home/git:/bin/bash  改为  git:x:1010:1010:,,,:/home/git:/usr/bin/git/git-shell

第四步,创建证书登录

使用命令ssh-keygen -t rsa -C “[email protected]生成公钥,Windows可以通过git bash执行命令,然后找到id_rsa.pub把文件内容导入 /home/git/.ssh/authorized_keys,如果没有文件

cd /home/git

mkdir .ssh

touch .ssh/authorized_keys

第五步,初始化git仓库

在/home/git 下新建仓库目录

mkdir repository

新建仓库,赋予权限

git init --bare test.git

chown -R git:git test.git

第六步,克隆仓库

git clone git@server:/home/git/repository/test.git  # server 可以是域名也可以是ip看配置
Cloning into 'test'...
warning: You appear to have cloned an empty repository.

遇到的问题

上面的都是理想状态下的流程

端口问题

git默认是22端口,如果服务器有修改端口,执行上面的clone会报错

$ git clone [email protected]:/home/git/repository/test.git
Cloning into 'test'...
ssh: connect to host 111.111.111.111 port 22: Connection timed out
fatal: Could not read from remote repository.
Please make sure you have the correct access rights
and the repository exists.

正确的方式

git clone ssh://[email protected]:1111/home/git/repository/test.git

权限问题

Cloning into 'xigoubao'...
fatal: '/home/git/repository/test.git' does not appear to be a git repository
fatal: Could not read from remote repository.
Please make sure you have the correct access rights
and the repository exists.

如果路径写错了,或者test.git拥有者和组不是git,会报这个错误

示例,路径写错

git clone ssh://[email protected]:1111/home/git/respository/test.git  # repository 拼错

git clone ssh://[email protected]:1111/repository/test.git   # 路径必须为相对于git用户的home目录路径  /home/git/repository/test.git

权限的话,执行命令就可以了

chown -R git.git test.git

git commit 时自动对所有 php 文件执行语法错误检查

使用 Shell 编写 hooks 下的 pre-commit 钩子,实现在 git commit 时检查所有的 .php 文件(忽略所有删除状态的文件)是否存在语法错误,如果存在错误,则终止提交,并输出相关错误信息。

#!/bin/bash

# @auth 后三排
# @site https://housanpai.com

# 错误消息内容
IS_ERROR_MESSAGE=()

while read st file
do

    # 文件状态为 D 时跳出本次循环
    if [ 'D' == "$st" ]
    then
        echo $file
        continue
    fi

    # 文件末为不是 .php 时输出文件,并跳出本次循环
    if [[ ! "$file"  =~ (.php$) ]]
    then
        echo $file
        continue
    fi

    PHP_LINT=`php -l $file`

    # 本文件不存在语法错误,输出结果,并跳出本次循环
    if [ 0 -eq $? ]
    then
        echo $PHP_LINT
        continue
    fi

    # 统计错误消息内容的数据个数
    ERROR_COUNT=${#IS_ERROR_MESSAGE[@]}

    # 将错误的存放到数组里面
    IS_ERROR_MESSAGE[${ERROR_COUNT}]=$PHP_LINT

done <<EOF
`git diff --cached --name-status`
EOF

if [ -n "${IS_ERROR_MESSAGE}" ]
then

    # 循环输出错误消息,并且指定文字颜色为红色
    for ((i=0;i<${#IS_ERROR_MESSAGE[@]};i++))
    do
        echo -e "33[31m ${IS_ERROR_MESSAGE[$i]} 33[0m"
    done

    exit 1

fi

exit 0

批量更新Git项目脚本

在平时的工作中,遇到一些优秀的开源项目,如volley、picasso、okhttp等,如果想阅读它的源代码,我通常都会clone项目到本地的GitHub文件夹,这样大神们后面再提交更新的话,只需要git pull更新一下本地的项目就能做到和远程仓库的代码同步了。可是时间长了就会遇到一个问题,如果GitHub文件夹里的项目太多,更新的话每个文件夹进去执行git pull将会是一件很麻烦的事。于是,花了几分钟,写了个批量更新的脚本。

#!/bin/sh
for dir in $(ls -d */)
do
  cd $dir
  echo "into $dir"
  if [ -d ".git" ]; then
     git pull
  elif [ -d ".svn" ]; then
     svn update
  fi
  cd ..
done

代码比较简单,就是遍历文件夹,发现项目目录下有.git文件夹,则执行git pull。很容易理解。

还加入了对svn项目的支持。命名为update.sh,放到GitHub文件夹,添加执行权限,执行./update.sh就可以了。

git 分支重命名

本地 branch 重命名 foo => bar

1. 本地重命名,切到分支 foo

git branch -m bar

或者直接重命名

git branch -m foo bar

2. 如果分支已经在远端,查看远端 origin

git remote show origin

  Remote branches:
    foo    tracked
    master tracked
  Local branch configured for 'git pull':
    master merges with remote master
  Local refs configured for 'git push':
    foo    pushes to foo    (up to date)
    master pushes to master (up to date)

这就需要把本地分支 bar push 上去,把 foo 分支删掉

git push origin :foo bar
 - [deleted]         foo
 * [new branch]      bar -> bar

最后在本地的 bar 分支下把 upstream 设置好

git push origin -u bar
Branch bar set up to track remote branch bar from origin.

查看远端 origin

git remote show origin

  Remote branches:
    bar    tracked
    master tracked
  Local branches configured for 'git pull':
    bar    merges with remote bar
    master merges with remote master
  Local refs configured for 'git push':
    bar    pushes to bar    (up to date)
    master pushes to master (up to date)