CentOS 6 Python2.6.6升级至Python2.7.6教程

有时候有些程序需要在Python 2.7才能运行,而CentOS6.x系统默认的是Python 2.6,遇到这个情况我们可以选择升级python2.6.6升级到Python2.7.6,或直接安装CentOS7.x版本,因为7.x版本系统默认的是Python 2.7。

1、首先使用“python -V”命令查看python版本,蜗牛测试主机显示的是2.6.6版,于是下面就着手将python2.6.6升级到Python2.7.6。

python -V #查看python版本

未分类

2、接下来先请完成系统升级及安装开发工具包及相关需要的组件。只需执行下面三个命令完成安装即可。

yum -y update
yum groupinstall "Development tools" -y
yum install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel -y

3、然后安装Python2.7.6。

cd /opt
wget --no-check-certificate https://www.python.org/ftp/python/2.7.6/Python-2.7.6.tar.xz
tar xf Python-2.7.6.tar.xz
./configure --prefix=/usr/local
sudo make && sudo make altinstall

4、查看是否安装Python2.7.6。

/usr/local/bin/python2.7 --version

5、移除老版本python2.6.6,然后创建2.7的软链接。之后可以直接使用Python -V可以看到Python版本已经是2.7.6。

mv /usr/bin/python /usr/bin/python2.6.6
ln -s /usr/local/bin/python2.7 /usr/bin/python
Python -V

6、修复兼容问题,升级Python版本到2.7.6后是无法直接执行yum安装软件的,需要修改yum兼容。请执行下面命令

vi /usr/bin/yum

编辑文件,在顶部修改成:

#!/usr/bin/python2.6.6

CentOS 7安装Python3.X

由于CentOS 7自带的Python是2.7,博主想用Python3.X的版本,考虑到卸载自带的2.7会带来诸多系统问题,于是就再编译安装一个Python3.X的版本了,与2.7版本共存,互不影响。

# 下载Python3.X安装包
[root@localhost Downloads]# wget https://www.python.org/ftp/python/3.6.2/Python-3.6.2.tar.xz
# 对于tar.xz文件,需要先用 xz -d xxx.tar.xz 将 xxx.tar.xz 解压成 xxx.tar 然后再用 tar xvf xxx.tar 来解压
[root@localhost Downloads]# xz -d Python-3.6.2.tar.xz
[root@localhost Downloads]# tar xvf Python-3.6.2.tar
# 解压后,在编译前需要安装Python依赖包,否则可能会出现编译错误,如"zipimport.ZipImportError"错误
[root@localhost Downloads]# yum install zlib zlib-devel -y
[root@localhost Python-3.6.2]# ./configure --prefix=/usr/local/python3.6
[root@localhost Python-3.6.2]# make
[root@localhost Python-3.6.2]# make install   # 如果这一步出现错误,再针对性解决,一般都是缺失依赖包导致的。
# 编译安装完成后,还需要将Python3.6添加至环境变量,便于全局使用
[root@localhost Python-3.6.2]# vim /etc/profile
export PATH=$PATH:/usr/local/python3.6/bin/  #新增内容,保存退出
[root@localhost Python-3.6.2]# source /etc/profile   #刷新环境变量
# 到这里,Python3.6已经安装完毕,并可正常使用。如下,系统默认Python是2.7
[root@localhost Downloads]# python
Python 2.7.5 (default, Nov  6 2016, 00:28:07) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-11)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> eixt()
# 退出后,我们进入python3.6
[root@localhost Downloads]# python3.6
Python 3.6.2 (default, Aug 24 2017, 14:25:11) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-11)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> exit()
[root@localhost Downloads]#

好了,到这里,就可以愉快的使用Python3.X了,暂时未发现问题,有问题会及时更新说明。

使用GitLab CI Runner自动创建Docker镜像

GitLab 来管理 Dockerfile 是一件高效率的事情. 在每次更改 Dockerfile 之后, 都要手动 build 然后 push 到 registry, 有点重复性. 自己开了个 registry. 那就采用一种自动的方法来帮助我们做这种机械重复的工作.

使用了 GitLab CI, 并且自 GitLab 8.x 开始已经集成了 GitLab CI Server. 所以也不用额外部署 CI Server 了. 我们要做的工作就是部署一下 GitLab CI runner. 然后在 Dockerfile 的项目里配置一下 .gitlab-ci.yml告诉 GitLab CI runner 如何做就行了.

1、安装 Docker

在你想用来 build image 的机器上, 显然需要先装好 Docker. 参见 Docker官方文档.

2、安装 GitLab CI Runner

在用来 build 的机器上我们需要安装 GitLab CI Runner. 官方项目里面提供了很多安装说明. 可以直接找你对应的需要. 这里我就说一下我怎么直接在 Ubuntu 16.04 上安装的.(对应的官方文档)

curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.deb.sh | sudo bash
sudo apt-get install gitlab-ci-multi-runner

3、然后注册 runner

sudo gitlab-ci-multi-runner register

当中会让你填写一些信息. 例如你的 gitlab-ci coordinator 的地址和注册这个 runner 的 token, 这两个在你 GitLab 中可以找到. 具体的内容我忘记截图了. 关于 executor 的话, 我这里使用的是 shell, 因为我将 runner 直接运行在物理机的系统上, 想其能够直接使用 docker.
注册好后, 我们可以看 runner 的配置文件, 类似如下:

# /etc/gitlab-runner/config.toml
concurrent = 1

[[runners]]
  url = "http://gitlab.com/ci"
  token = "******************"
  name = "image builder"
  executor = "shell"
  [runners.ssh]
  [runners.docker]
    image = ""
    privileged = false
  [runners.parallels]
    base_name = ""

关于这些参数可以在 官方文档(https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md) 中找到. 注册好的 runner 在 GitLab 中如下图所示.

实际显示情况可能和上图有不同, 因为我已经将这个 runner 设置为 specific runner 了. 关于 runner 的说明可以参考 官方文档 configuring runner(http://doc.gitlab.com/ci/runners/README.html)

使用 GitLab runner 来 build docker image 的相关说明, 可以参考 官方文档 using docker build(http://doc.gitlab.com/ce/ci/docker/using_docker_build.html) . 主要需要注意的是, 为了要让 runner 可以调用 docker 命令, 需要把 gitlab-runner 这个用户加入 docker 所在组.

sudo usermod -aG docker gitlab-runner

4、配置使用

可以参考官方文档 Configuring project(http://doc.gitlab.com/ce/ci/yaml/README.html) . 用的配置

tages:
  - build_image
  - push_image

before_script:
  - docker info

build_image:
  stage: build_image
  script:
    - docker build -t myregistry/aplusplus/ubuntu:16.04 .

push_image:
  stage: push_image
  script:
    - docker push myregistry/aplusplus/ubuntu:16.04

stages 定义了你要做几步(stage) 以及他们之间的顺序. 默认每个 stage 都是在之前所有 stage 成功执行后才会执行. 每个 stage 可以包含多个任务(job), 例如上面的 build_image 和 push_image. 这里只定义了一个. 当每个 stage 有多个 jobs 时, 每个 jobs 会并行执行.

当你每次修改项目并 push 到 gitlab 后, runner 就会开始执行你配置的任务了。

使用docker-compose实现容器编排

创建一个容器,我们可以通过Dockerfile模板文件对镜像进行创建并按照配置要求启动。然而,一般项目往往需要多个容器相互配合才能完成某项任务,比如说在一个web项目中,除了web服务容器,往往还需要后端的数据库服务容器,甚至还需要负载均衡容器等。如何有效地做好容器之间的编排,是Docker Compose要做的内容。

Docker Compose是定义和运行多个Docker容器的工具,它主要管理一个项目(project),这个项目是由一组关联的应用容器组成的一个完整业务单元,而每个应用容器则对应一个服务(service),当然服务可能只包含一个容器(container)实例,也可能包括若干运行相同镜像的容器(container)实例。

Docker Compose的核心就在于“一个文件”和“一条命令”。所谓“一个文件”,是指docker-compose.yml,在这个文件中我们可以进行项目的配置,包括服务的定义。而“一条命令”则指,我们只需要类似docker-compose up这样简单的命令即可管理项目。其他帮助和命令,我们可以通过docker-compose -h进行查询。

未分类

未分类

为了更好地理解docker-compose如何进行容器编排,下面运行一个python web应用,其中web框架基于flask,计数器基于redis。

我们要实现的效果就是,每访问一次web服务网址,计数器对应加1。

创建项目目录

如果在启动项目时,如果没有指定具体的项目名称,则项目所在的目录名作为默认的项目名称。

mkdir composetest
cd composetest

使用Dockerfile定义镜像配置

在composetest目录下,创建3个文件,内容依次为:

app.py

#coding: utf-8
from flask import Flask
from redis import Redis
app = Flask(__name__)
redis = Redis(host='redis', port=6379) # ''中的redis对应服务名
@app.route('/')
def hello():
    count = redis.incr('hits')
    return 'Hello World! I have been seen {} times.n'.format(count)
if __name__ == "__main__":
    app.run(host="0.0.0.0", debug=True)

requirements.txt

flask
redis

Dockefile

FROM python:2.7
ADD . /code
WORKDIR /code
RUN pip install -r requirements.txt
CMD ["python", "app.py"]

这样,我们定义了web服务镜像的配置。在第一次启动项目的时候,docker-compose会使用Dockerfile创建一个名为composetest_web的镜像。

使用Docker Compose来管理项目

定义项目配置文件docker-compose.yml
在composetest目录下,创建docker-compose.yml,内容为:

version: '3'
services:
 web:
  build: .
  ports:
   - "5000:5000"
  volumes:
   - .:/code
 redis:
  image: redis

docker-compose.yml中定义了web和redis服务,对于web服务:

build    在当前目录从Dockerfile创建一个镜像
ports    端口映射,开放容器5000端口映射到宿主机5000端口
volumes    挂载数据卷,挂载容器目录/code对应到宿主机当前目录

未分类

此时我们需要的文件都准备好了,开始启动项目:

docker-compose up

未分类

此时我们可以通过浏览器访问对应的 http://ip:5000 地址,如果在服务本地访问,则对应 http://localhost:5000 。

未分类

可以看到每访问一次web服务地址,计数器对应加1。如果要更新应用,因为挂载了数据卷,只需修改app.py保存即可。

使用容器互联

除了使用docker-compose来进行容器编排,如果情况不太复杂,我们也可以使用容器互联的手段实现项目需求。容器互联要求针对多个容器,分别按顺序启动并指明依赖关系,也就是利用好“–link”这个参数。

同样,在composetest目录下,先启动redis服务容器:

docker run -d --name redis redis

创建web服务镜像:

docker build -t dockerfile_web .

通过查看镜像,可以看到生成的dockerfile_web镜像,在docker-compose.yml中定义的服务依赖的镜像也已经具备了,如composetest_web。由于dockerfile_web和composetest_web镜像都是由同一个Dockerfile模板文件生成的,本质上它们是一样的。

未分类

启动web服务容器,并关联redis服务容器:

docker run --name web -p 5000:5000 -v $(pwd):/code --link redis:webredis dockerfile_web

这样也能得到同样的效果,但是,对于复杂的容器编排情况,还是乖乖地使用docker-compose等利器吧。

Ubuntu 16.04安装vsftpd ftp服务器

未分类

写在前面:

vsftpd 是“very secure FTP daemon”的缩写,安全性是它的一个最大的特点。vsftpd 是一个 UNIX 类操作系统上运行的服务器的名字,它可以运行在诸如 Linux、BSD、Solaris、 HP-UNIX等系统上面,是一个完全免费的、开放源代码的ftp服务器软件,支持很多其他的 FTP 服务器所不支持的特征。比如:非常高的安全性需求、带宽限制、良好的可伸缩性、可创建虚拟用户、支持IPv6、速率高等。

安装:

sudo apt-get install vsftpd

配置:

vim /etc/vsftpd.conf


#这些设置系统默认是开启的,可以不管
listen=NO
listen_ipv6=YES
dirmessage_enable=YES
use_localtime=YES
xferlog_enable=YES
connect_from_port_20=YES

#下面的就要自定义设置了,建议系统默认的不管,然后复制下面的

#是否允许匿名访问,NO为不允许
anonymous_enable=NO
#是否允许本地用户访问,就是linux本机中存在的用户,YES允许
local_enable=YES
#是否开启写模式,YES为开启
write_enable=YES
#新建文件权限,一般设置为022,那么新建后的文件的权限就是777-022=755
local_umask=022

####umask值与权限的对照表:
####umask 文件 目录 
####-------------------- 
####  0    6    7 
####  1    6    6 
####  2    4    5 
####  3    4    4 
####  4    2    3 
####  5    2    2 
####  6    0    1 
####  7    0    0 
####--------------------

#是否启动userlist为通过模式,YES的话只有存在于userlist文件中的用户才能登录ftp(可以理解为userlist是一个白名单),NO的话,白名单失效,和下面一个参数配合使用
userlist_enable=YES
#是否启动userlist为禁止模式,YES表示在userlist中的用户禁止登录ftp(黑名单),NO表示黑名单失效,我们已经让userlist作为一个白名单,所以无需使用黑名单功能
userlist_deny=NO
#指定哪个文件作为userlist文件,我们稍后编辑这个文件
userlist_file=/etc/vsftpd.user_list

#是否限制本地所有用户切换根目录的权限,YES为开启限制,即登录后的用户不能访问ftp根目录以外的目录,当然要限制啦
chroot_local_user=YES
#是否启动限制用户的名单list为允许模式,上面的YES限制了所有用户,可以用这个名单作为白名单,作为例外允许访问ftp根目录以外
chroot_list_enable=YES
#设置哪个文件是list文件,里面的用户将不受限制的去访问ftp根目录以外的目录
chroot_list_file=/etc/vsftpd.chroot_list
#是否开启写模式,开启后可以进行创建文件夹等写入操作
allow_writeable_chroot=YES

#设置ftp根目录的位置,这个文件我们稍后自己创建
local_root=/目录位置

重启:

sudo /etc/init.d/vsftpd restart

添加用户:

#将将用户目录设置为我们上面的ftp根目录
sudo useradd -d /目录位置 用户名字

#设置用户密码
sudo passwd 用户名字

限定用户只能访问自己的目录,无法返回上层及进入其它目录:

修改配置文件:/etc/vsftpd/vsftpd.conf

sudo vi /etc/vsftpd.conf

去掉下面两行的注释:

chroot_list_enable=YES

chroot_list_file=/etc/vsftpd/chroot_list

在/etc/vsftpd下创建chroot_list文件:

touch chroot_list

加入需要限制的用户名:

vi /etc/vsftpd/chroot_list

保存

:wq

重启服务(每次更改配置后必须重启才能生效):

sudo /etc/init.d/vsftpd restart

其它命令:

#关闭
sudo /etc/init.d/vsftpd stop
#开启
sudo /etc/init.d/vsftpd start
#查看当前用户
vi /etc/passwd

关于500错误的解决思路:

出现500的原因是你的主目录设置了可写权限,故无法通过安全验证,所以无法正常登录。

附:

关于文件及文件夹权限

读:r

写:w

执行:x

      所有者 同组用户 其它用户

         rwx   rwx   rwx       翻译:所有者同组用户其它用户都可读、写和执行

二进制: 111  111  111

十进制:  7    7     7



      所有者 同组用户 其它用户

         rwx  - - -  - - -       翻译:只有所有者有读、写和执行的权限

二进制: 111   000     000

十进制:  7    0     0

Redis 备份、容灾及高可用实战

Redis已经大量应用于各种互联网架构场景中,其优异的性能,良好的操作性,以及大量的场景应用案例,使得Redis备受瞩目。本文作者向大家介绍了一种Redis在非大集群分布式应用场景下的灾备解决方案。一起来品读一下吧~

一、Redis简单介绍

Redis是一个高性能的key-value非关系型数据库,由于其具有高性能的特性,支持高可用、持久化、多种数据结构、集群等,使其脱颖而出,成为常用的非关系型数据库。

此外,Redis的使用场景也比较多。

  • 会话缓存(Session Cache)
    Redis缓存会话有非常好的优势,因为Redis提供持久化,在需要长时间保持会话的应用场景中,如购物车场景这样的场景中能提供很好的长会话支持,能给用户提供很好的购物体验。

  • 全页缓存
    在WordPress中,Pantheon提供了一个不错的插件wp-redis,这个插件能以最快的速度加载你曾经浏览过的页面。

  • 队列

Reids提供list和set操作,这使得Redis能作为一个很好的消息队列平台来使用。

我们常通过Reids的队列功能做购买限制。比如到节假日或者推广期间,进行一些活动,对用户购买行为进行限制,限制今天只能购买几次商品或者一段时间内只能购买一次。也比较适合适用。

  • 排名
    Redis在内存中对数字进行递增或递减的操作实现得非常好。所以我们在很多排名的场景中会应用Redis来进行,比如小说网站对小说进行排名,根据排名,将排名靠前的小说推荐给用户。

  • 发布/订阅
    Redis提供发布和订阅功能,发布和订阅的场景很多,比如我们可以基于发布和订阅的脚本触发器,实现用Redis的发布和订阅功能建立起来的聊天系统。

此外还有很多其它场景,Redis都表现的不错。

二、Redis使用中单点故障问题

正是由于Redis具备多种优良特新,且应用场景非常丰富,以至于Redis在各个公司都有它存在的身影。那么随之而来的问题和风险也就来了。Redis虽然应用场景丰富,但部分公司在实践Redis应用的时候还是相对保守使用单节点部署,那为日后的维护带来了安全风险。

在2015年的时候,曾处理过一个因为单点故障原因导致的业务中断问题。当时的Redis都未采用分布式部署,采用单实例部署,并未考虑容灾方面的问题。

当时我们通过Redis服务器做用户购买优惠商品的行为控制,但后来由于未知原因Redis节点的服务器宕机了,导致我们无法对用户购买行为进行控制,造成了用户能够在一段时间内多次购买优惠商品的行为。

这种宕机事故可以说已经对公司造成了不可挽回的损失了,安全风险问题非常严重,作为当时运维这个系统的我来说有必要对这个问题进行修复和在架构上的改进。于是我开始了解决非分布式应用下Redis单点故障方面的研究学习。

三、非分布式场景下Redis应用的备份与容灾

Redis主从复制现在应该是很普遍了。常用的主从复制架构有如下两种架构方案。

常用Redis主从复制

方案一

未分类

这是最常见的一种架构,一个Master节点,两个Slave节点。客户端写数据的时候是写Master节点,读的时候,是读取两个Slave,这样实现读的扩展,减轻了Master节点读负载。

方案二

未分类

这种架构同样是一个Master和两个Slave。不同的是Master和Slave1使用keepalived进行VIP转移。Client连接Master的时候是通过VIP进行连接的。避免了方案一IP更改的情况。

Redis主从复制优点与不足

优点

实现了对master数据的备份,一旦master出现故障,slave节点可以提升为新的master,顶替旧的master继续提供服务

实现读扩展。使用主从复制架构, 一般都是为了实现读扩展。Master主要实现写功能, Slave实现读的功能

不足

架构方案一
当Master出现故障时,Client就与Master端断开连接,无法实现写功能,同时Slave也无法从Master进行复制。

未分类

此时需要经过如下操作(假设提升Slave1为Master):

  • 在Slave1上执slaveof no one命令提升Slave1为新的Master节点。
  • 在Slave1上配置为可写,这是因为大多数情况下,都将slave配置只读。
  • 告诉Client端(也就是连接Redis的程序)新的Master节点的连接地址。
  • 配置Slave2从新的Master进行数据复制。

架构方案二

当master出现故障后,Client可以连接到Slave1上进行数据操作,但是Slave1就成了一个单点,就出现了经常要避免的单点故障(single point of failure)。

未分类

之后需要经过如下操作:

  • 在Slave1上执行slaveof no one命令提升Slave1为新的Master节点
  • 在Slave1上配置为可写,这是因为大多数情况下,都将Slave配置只读
  • 配置Slave2从新的Master进行数据复制

可以发现,无论是哪种架构方案都需要人工干预来进行故障转移(failover)。需要人工干预就增加了运维工作量,同时也对业务造成了巨大影响。这时候可以使用Redis的高可用方案-Sentinel

四、Redis Sentinel介绍

Redis Sentinel为Redis提供了高可用方案。从实践方面来说,使用Redis Sentinel可以创建一个无需人为干预就可以预防某些故障的Redis环境。

Redis Sentinel设计为分布式的架构,运行多个Sentinel进程来共同合作的。运行多个Sentinel进程合作,当多个Sentinel同一给定的master无法
再继续提供服务,就会执行故障检测,这会降低误报的可能性。

五、Redis Sentinel功能

Redis Sentinel在Redis高可用方案中主要作用有如下功能:

  • 监控
    Sentinel会不断的检查master和slave是否像预期那样正常运行

  • 通知
    通过API,Sentinel能够通知系统管理员、程序监控的Redis实例出现了故障

  • 自动故障转移
    如果master不像预想中那样正常运行,Sentinel可以启动故障转移过程,其中的一个slave会提成为master,其它slave会重新配置来使用新的master,使用Redis服务的应用程序,当连接时,也会被通知使用新的地址。

  • 配置提供者
    Sentinel可以做为客户端服务发现的认证源:客户端连接Sentinel来获取目前负责给定服务的Redis master地址。如果发生故障转移,Sentinel会报告新的地址。

六、Redis Sentinel架构

未分类

七、Redis Sentinel实现原理

Sentinel集群对自身和Redis主从复制进行监控。当发现Master节点出现故障时,会经过如下步骤:

1、Sentinel之间进行选举,选举出一个leader,由选举出的leader进行failover

2、Sentinel leader选取slave节点中的一个slave作为新的Master节点。对slave选举需要对slave进行选举的方法如下:

  • 与master断开时间
    如果与master断开的时间超过down-after-milliseconds(sentinel配置) * 10秒加上从sentinel判定master不可用到sentinel开始执行故障转移之间的时间,就认为该slave不适合提升为master。

  • slave优先级
    每个slave都有优先级,保存在redis.conf配置文件里。如果优先级相同,则继续进行。

  • 复制偏移位置
    复制偏移纪录着从master复制数据复制到哪里,复制偏移越大表明从master接受的数据越多,如果复制偏移量也一样,继续进行选举

  • Run ID
    选举具有最小Run ID的Slave作为新的Master

流程图如下:

未分类

3、Sentinel leader会在上一步选举的新master上执行slaveof no one操作,将其提升为master节点

4、Sentinel leader向其它slave发送命令,让剩余的slave成为新的master节点的slave

5、Sentinel leader会让原来的master降级为slave,当恢复正常工作,Sentinel leader会发送命令让其从新的master进行复制

以上failover操作均有sentinel自己独自完成,完全无需人工干预。

总结

使用sentinel实现了Redis的高可用,当master出现故障时,完全无需人工干预即可实现故障转移。避免了对业务的影响,提高了运维工作效率。

在部署sentinel的时候,建议使用奇数个sentinel节点,最少三个sentinel节点。

CentOs7.3搭建Redis-4.0.1 Cluster集群服务

环境

  • VMware版本号:12.0.0
  • CentOS版本:CentOS 7.3.1611
  • 三台虚拟机(IP):192.168.252.101,192.168.102..102,192.168.252.103

注意事项

安裝 GCC 编译工具 不然会有编译不过的问题

$ yum install -y gcc g++ gcc-c++ make

升级所有的包,防止出现版本过久不兼容问题

$ yum -y update

关闭防火墙 节点之前需要开放指定端口,为了方便,生产不要禁用

centos 6.x

$ service iptables stop # 关闭命令:

centos 7.x

$ systemctl stop firewalld.service # 停止firewall

集群搭建

安装 Redis

下载,解压,编译安装

cd /opt
$ wget http://download.redis.io/releases/redis-4.0.1.tar.gz
$ tar xzf redis-4.0.1.tar.gz
$ cd redis-4.0.1
$ make

如果因为上次编译失败,有残留的文件

$ make distclean

创建节点

  • 首先在 192.168.252.101机器上 /opt/redis-4.0.1目录下创建 redis-cluster 目录
$ mkdir /opt/redis-4.0.1/redis-cluster
  • 在 redis-cluster 目录下,创建名为7000、7001、7002的目录
$ cd /opt/redis-4.0.1/redis-cluster
$ mkdir 7000 7001 7002
  • 分别修改这三个配置文件,把如下redis.conf 配置内容粘贴进去
$ vi 7000/redis.conf 
$ vi 7001/redis.conf
$ vi 7002/redis.conf

redis.conf 配置

port 7000
bind 192.168.252.101
daemonize yes
pidfile /var/run/redis_7000.pid
cluster-enabled yes
cluster-config-file nodes_7000.conf
cluster-node-timeout 10100
appendonly yes

redis.conf 配置说明

#端口7000,7001,7002
port 7000

#默认ip为127.0.0.1,需要改为其他节点机器可访问的ip,否则创建集群时无法访问对应的端口,无法创建集群
bind 192.168.252.101

#redis后台运行
daemonize yes

#pidfile文件对应7000,7001,7002
pidfile /var/run/redis_7000.pid

#开启集群,把注释#去掉
cluster-enabled yes

#集群的配置,配置文件首次启动自动生成 7000,7001,7002          
cluster-config-file nodes_7000.conf

#请求超时,默认15秒,可自行设置 
cluster-node-timeout 10100    

#aof日志开启,有需要就开启,它会每次写操作都记录一条日志
appendonly yes

···
接着在另外两台机器上(192.168.252.102,192.168.252.103)重复以上三步,只是把目录改为7003、7004、7005、7006、7007、7008对应的配置文件也按照这个规则修改即可

启动集群

#第一台机器上执行 3个节点
$ for((i=0;i<=2;i++)); do /opt/redis-4.0.1/src/redis-server /opt/redis-4.0.1/redis-cluster/700$i/redis.conf; done

#第二台机器上执行 3个节点
$ for((i=3;i<=5;i++)); do /opt/redis-4.0.1/src/redis-server /opt/redis-4.0.1/redis-cluster/700$i/redis.conf; done

#第三台机器上执行 3个节点 
$ for((i=6;i<=8;i++)); do /opt/redis-4.0.1/src/redis-server /opt/redis-4.0.1/redis-cluster/700$i/redis.conf; done

检查服务

检查各 Redis 各个节点启动情况

$ ps -ef | grep redis           //redis是否启动成功
$ netstat -tnlp | grep redis    //监听redis端口

安装 Ruby

$ yum -y install ruby ruby-devel rubygems rpm-build
$ gem install redis

创建集群

注意:在任意一台上运行 不要在每台机器上都运行,一台就够了

Redis 官方提供了 redis-trib.rb 这个工具,就在解压目录的 src 目录中

$ /opt/redis-4.0.1/src/redis-trib.rb create --replicas 1 192.168.252.101:7000 192.168.252.101:7001 192.168.252.101:7002 192.168.252.102:7003 192.168.252.102:7004 192.168.252.102:7005 192.168.252.103:7006 192.168.252.103:7007 192.168.252.103:7008

出现以下内容

[root@localhost redis-cluster]# /opt/redis-4.0.1/src/redis-trib.rb create --replicas 1 192.168.252.101:7000 192.168.252.101:7001 192.168.252.101:7002 192.168.252.102:7003 192.168.252.102:7004 192.168.252.102:7005 192.168.252.103:7006 192.168.252.103:7007 192.168.252.103:7008
>>> Creating cluster
>>> Performing hash slots allocation on 9 nodes...
Using 4 masters:
192.168.252.101:7000
192.168.252.102:7003
192.168.252.103:7006
192.168.252.101:7001
Adding replica 192.168.252.102:7004 to 192.168.252.101:7000
Adding replica 192.168.252.103:7007 to 192.168.252.102:7003
Adding replica 192.168.252.101:7002 to 192.168.252.103:7006
Adding replica 192.168.252.102:7005 to 192.168.252.101:7001
Adding replica 192.168.252.103:7008 to 192.168.252.101:7000
M: 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf 192.168.252.101:7000
   slots:0-4095 (4096 slots) master
M: 44c81c15b01d992cb9ede4ad35477ec853d70723 192.168.252.101:7001
   slots:12288-16383 (4096 slots) master
S: 38f03c27af39723e1828eb62d1775c4b6e2c3638 192.168.252.101:7002
   replicates f1abb62a8c9b448ea14db421bdfe3f1d8075189c
M: 987965baf505a9aa43e50e46c76189c51a8f17ec 192.168.252.102:7003
   slots:4096-8191 (4096 slots) master
S: 6555292fed9c5d52fcf5b983c441aff6f96923d5 192.168.252.102:7004
   replicates 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf
S: 2b5ba254a0405d4efde4c459867b15176f79244a 192.168.252.102:7005
   replicates 44c81c15b01d992cb9ede4ad35477ec853d70723
M: f1abb62a8c9b448ea14db421bdfe3f1d8075189c 192.168.252.103:7006
   slots:8192-12287 (4096 slots) master
S: eb4067373d36d8a8df07951f92794e67a6aac022 192.168.252.103:7007
   replicates 987965baf505a9aa43e50e46c76189c51a8f17ec
S: 2919e041dd3d1daf176d6800dcd262f4e727f366 192.168.252.103:7008
   replicates 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf
Can I set the above configuration? (type 'yes' to accept): yes

输入 yes

>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join.........
>>> Performing Cluster Check (using node 192.168.252.101:7000)
M: 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf 192.168.252.101:7000
   slots:0-4095 (4096 slots) master
   2 additional replica(s)
S: 6555292fed9c5d52fcf5b983c441aff6f96923d5 192.168.252.102:7004
   slots: (0 slots) slave
   replicates 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf
M: 44c81c15b01d992cb9ede4ad35477ec853d70723 192.168.252.101:7001
   slots:12288-16383 (4096 slots) master
   1 additional replica(s)
S: 2919e041dd3d1daf176d6800dcd262f4e727f366 192.168.252.103:7008
   slots: (0 slots) slave
   replicates 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf
M: f1abb62a8c9b448ea14db421bdfe3f1d8075189c 192.168.252.103:7006
   slots:8192-12287 (4096 slots) master
   1 additional replica(s)
S: eb4067373d36d8a8df07951f92794e67a6aac022 192.168.252.103:7007
   slots: (0 slots) slave
   replicates 987965baf505a9aa43e50e46c76189c51a8f17ec
S: 38f03c27af39723e1828eb62d1775c4b6e2c3638 192.168.252.101:7002
   slots: (0 slots) slave
   replicates f1abb62a8c9b448ea14db421bdfe3f1d8075189c
S: 2b5ba254a0405d4efde4c459867b15176f79244a 192.168.252.102:7005
   slots: (0 slots) slave
   replicates 44c81c15b01d992cb9ede4ad35477ec853d70723
M: 987965baf505a9aa43e50e46c76189c51a8f17ec 192.168.252.102:7003
   slots:4096-8191 (4096 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

关闭集群

这样也可以,推荐

$ pkill redis

循环节点逐个关闭

$ for((i=0;i<=2;i++)); do /opt/redis-4.0.1/src/redis-cli -c -h 192.168.252.101 -p 700$i shutdown; done

$ for((i=3;i<=5;i++)); do /opt/redis-4.0.1/src/redis-cli -c -h 192.168.252.102 -p 700$i shutdown; done

$ for((i=6;i<=8;i++)); do /opt/redis-4.0.1/src/redis-cli -c -h 192.168.252.103 -p 700$i shutdown; done

集群验证

连接集群测试

参数 -C 可连接到集群,因为 redis.conf 将 bind 改为了ip地址,所以 -h 参数不可以省略,-p 参数为端口号

  • 我们在192.168.252.101机器redis 7000 的节点set 一个key
$ /opt/redis-4.0.1/src/redis-cli -h 192.168.252.101 -c -p 7000
192.168.252.101:7000> set name www.ymq.io
-> Redirected to slot [5798] located at 192.168.252.102:7003
OK
192.168.252.102:7003> get name
"www.ymq.io"
192.168.252.102:7003>

发现redis set name 之后重定向到192.168.252.102机器 redis 7003 这个节点

  • 我们在192.168.252.103机器redis 7008 的节点get一个key
[root@localhost redis-cluster]# /opt/redis-4.0.1/src/redis-cli -h 192.168.252.103 -c -p 7008
192.168.252.103:7008> get name
-> Redirected to slot [5798] located at 192.168.252.102:7003
"www.ymq.io"
192.168.252.102:7003> 

发现redis get name 重定向到192.168.252.102机器 redis 7003 这个节点

如果您看到这样的现象,说明集群已经是可用的了

检查集群状态

$ /opt/redis-4.0.1/src/redis-trib.rb check 192.168.252.101:7000
>>> Performing Cluster Check (using node 192.168.252.101:7000)
M: 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf 192.168.252.101:7000
   slots:0-4095 (4096 slots) master
   2 additional replica(s)
S: 6555292fed9c5d52fcf5b983c441aff6f96923d5 192.168.252.102:7004
   slots: (0 slots) slave
   replicates 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf
M: 44c81c15b01d992cb9ede4ad35477ec853d70723 192.168.252.101:7001
   slots:12288-16383 (4096 slots) master
   1 additional replica(s)
S: 2919e041dd3d1daf176d6800dcd262f4e727f366 192.168.252.103:7008
   slots: (0 slots) slave
   replicates 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf
M: f1abb62a8c9b448ea14db421bdfe3f1d8075189c 192.168.252.103:7006
   slots:8192-12287 (4096 slots) master
   1 additional replica(s)
S: eb4067373d36d8a8df07951f92794e67a6aac022 192.168.252.103:7007
   slots: (0 slots) slave
   replicates 987965baf505a9aa43e50e46c76189c51a8f17ec
S: 38f03c27af39723e1828eb62d1775c4b6e2c3638 192.168.252.101:7002
   slots: (0 slots) slave
   replicates f1abb62a8c9b448ea14db421bdfe3f1d8075189c
S: 2b5ba254a0405d4efde4c459867b15176f79244a 192.168.252.102:7005
   slots: (0 slots) slave
   replicates 44c81c15b01d992cb9ede4ad35477ec853d70723
M: 987965baf505a9aa43e50e46c76189c51a8f17ec 192.168.252.102:7003
   slots:4096-8191 (4096 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

列出集群节点

列出集群当前已知的所有节点(node),以及这些节点的相关信息

$ /opt/redis-4.0.1/src/redis-cli -h 192.168.252.101 -c -p 7000

192.168.252.101:7000> cluster nodes
6555292fed9c5d52fcf5b983c441aff6f96923d5 192.168.252.102:7004@17004 slave 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf 0 1502815268317 5 connected
44c81c15b01d992cb9ede4ad35477ec853d70723 192.168.252.101:7001@17001 master - 0 1502815268000 2 connected 12288-16383
2919e041dd3d1daf176d6800dcd262f4e727f366 192.168.252.103:7008@17008 slave 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf 0 1502815269000 9 connected
7c622ac191edd40dd61d9b79b27f6f69d02a5bbf 192.168.252.101:7000@17000 myself,master - 0 1502815269000 1 connected 0-4095
f1abb62a8c9b448ea14db421bdfe3f1d8075189c 192.168.252.103:7006@17006 master - 0 1502815269000 7 connected 8192-12287
eb4067373d36d8a8df07951f92794e67a6aac022 192.168.252.103:7007@17007 slave 987965baf505a9aa43e50e46c76189c51a8f17ec 0 1502815267000 8 connected
38f03c27af39723e1828eb62d1775c4b6e2c3638 192.168.252.101:7002@17002 slave f1abb62a8c9b448ea14db421bdfe3f1d8075189c 0 1502815269327 7 connected
2b5ba254a0405d4efde4c459867b15176f79244a 192.168.252.102:7005@17005 slave 44c81c15b01d992cb9ede4ad35477ec853d70723 0 1502815270336 6 connected
987965baf505a9aa43e50e46c76189c51a8f17ec 192.168.252.102:7003@17003 master - 0 1502815271345 4 connected 4096-8191
192.168.252.101:7000> 

打印集群信息

$ 192.168.252.101:7000> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:9
cluster_size:4
cluster_current_epoch:9
cluster_my_epoch:1
cluster_stats_messages_ping_sent:485
cluster_stats_messages_pong_sent:485
cluster_stats_messages_sent:970
cluster_stats_messages_ping_received:477
cluster_stats_messages_pong_received:485
cluster_stats_messages_meet_received:8
cluster_stats_messages_received:970
192.168.252.101:7000> 

集群命令

语法格式

redis-cli -c -p port

集群

cluster info :打印集群的信息
cluster nodes :列出集群当前已知的所有节点( node),以及这些节点的相关信息。

节点

cluster meet <ip> <port> :将 ip 和 port 所指定的节点添加到集群当中,让它成为集群的一份子。
cluster forget <node_id> :从集群中移除 node_id 指定的节点。
cluster replicate <node_id> :将当前节点设置为 node_id 指定的节点的从节点。
cluster saveconfig :将节点的配置文件保存到硬盘里面。

槽(slot)

cluster addslots <slot> [slot ...] :将一个或多个槽( slot)指派( assign)给当前节点。
cluster delslots <slot> [slot ...] :移除一个或多个槽对当前节点的指派。
cluster flushslots :移除指派给当前节点的所有槽,让当前节点变成一个没有指派任何槽的节点。
cluster setslot <slot> node <node_id> :将槽 slot 指派给 node_id 指定的节点,如果槽已经指派给另一个节点,那么先让另一个节点删除该槽>,然后再进行指派。
cluster setslot <slot> migrating <node_id> :将本节点的槽 slot 迁移到 node_id 指定的节点中。
cluster setslot <slot> importing <node_id> :从 node_id 指定的节点中导入槽 slot 到本节点。
cluster setslot <slot> stable :取消对槽 slot 的导入( import)或者迁移( migrate)。

cluster keyslot <key> :计算键 key 应该被放置在哪个槽上。
cluster countkeysinslot <slot> :返回槽 slot 目前包含的键值对数量。
cluster getkeysinslot <slot> <count> :返回 count 个 slot 槽中的键 。

Redis出现Could not get a resource from the pool错误关于连接数的分析

缘起:

redis.clients.jedis.exceptions.JedisConnectionException:Could not get a resource from the pool

生产环境的业务服务器报了大量上面的错误。Jedis无法从连接池中获取一个可用的连接,所有客户端与Redis服务端保持通信的连接都在工作中,没有闲置的连接可以使用。
       
目前生产环境每天Redis的QPS在5000左右,连接池配置20个最大连接数貌似是真的很小,是不是增大连接池的配置就解决问题了?出现这个问题的根本原因是:连接池中的Jedis对象是有限的,如果Jedis一直被占用,没有归还,如果这时需要操作redis,就需要等待可用的Jedis,当等待时间超过maxWaitMillis,就会抛出could not get a resource from pool。以下几种场景会出现这个问题:

  • 并发实在太高了,连接池中的连接数确实太小了,大量的请求等待空闲的连接。

  • 由于Redis是单线程,某个查询太慢,阻塞了其他操作命令的执行。

  • Redis内部问题导致处理客户端的命令慢了,比如RDB持久化时,fork进程做内存快照;AOF持久化时,AOF文件重写时会占用大量的CPU资源;

  • 大量key同时过期。

以下数据来自于CAT对缓存的监控数据:蓝线表示出现Could not get a resource from the pool的次数,绿线表示QPS,从图中可以看出随着QPS的升高,出现异常的次数也在增高,难道真的是因为QPS高,连接池数小的原因?

未分类

CAT上按照小时为维度获取缓存出现异常的数据如下:

未分类

从以下数据可以发现缓存出现异常的时间段都比较集中,而且间隔的时间段貌似存在着某种规律。出现问题的时间段也并不是每天QPS最高的时候,QPS最高的几个时间段反而没有出现任何异常。取了一个出现异常的时间段的缓存情况如下

未分类

发现这个时间段有几个比较耗时的操作命令,但是这几个命令在其他时间段最大耗时就10多毫秒。业务上也不存在不合理使用Redis数据结构的问题。是该看看缓存的监控情况了(这一部分图片没截)。

找运维看了Redis的情况,发现Redis的某个时间段CPU飙到100%了,这个时间段和出现异常的时间段吻合。问题基本已经确认,这个时间段Redis内部一定发生了点什么,导致处理客户端的请求变慢了,导致大量的请求被阻塞,超过maxWaitMillis时,集中出现了大量的Could not get a resource from the pool异常。

生产环境Redis的持久化策略是AOF,AOF会将所有的写命令按照一定频率写入到日志文件中,随着AOF文件越来越大,里面会有大部分是重复命令或者可以合并的命令(比如100次incr = set key 100),重写可以减少AOF日志尺寸,减少内存占用,加快数据库恢复时间。AOF重写的过程会fork一个子进程,导致CPU飙到100%了。在这种情况下即使增大接池连接数也没什么卵用。这个问题的解决思路是减少AOF重写的频率,两种方式:

  • 让Redis决定是否做AOF重写操作,根据auto-aof-rewrite-percentage和auto-aof-rewrite-min-size两个参数,auto-aof-rewrite-percentage:当前写入日志文件的大小超过上一次rewrite之后的文件大小的百分之多少时重写;auto-aof-rewrite-min-size:当前aof文件大于多少字节后才触发

  • 用crontab定时重写,命令是:BGREWRITEAOF

上面提到慢查询会阻塞Redis,那么业务开发同学在使用时如何避免呢?

  • 避免让Redis执行耗时长的命令,绝大多数读写命令的时间复杂度都在O(1)到O(N)之间,O(1)的命令是安全的,O(N)命令在使用时需要注意,如果N的数量级不可预知,应避免使用,如对一个field数未知的Hash数据执行HGETALL/HKEYS/HVALS命令,通常来说这些命令执行的很快,但如果这个Hash中的field数量极多,耗时就会成倍增长

  • 避免在使用这些O(N)命令时发生问题主要有几个办法:不要把List当做列表使用,仅当做队列来使用,严格控制Hash、Set、Sorted Set的大小,将排序、并集、交集等操作放在客户端执行,禁止使用KEYS命令

  • 避免一次性遍历集合类型的所有成员,而应使用SCAN类的命令进行分批的,游标式的遍历SSCAN/HSCAN/ZSCAN等命令,分别用于对Set/Hash/Sorted Set中的元素进行游标式遍历

  • 尽可能使用长连接或连接池,避免频繁创建销毁连接,使用pipelining将连续执行的命令组合执行

redis增量订阅工具redis-canal分享

项目背景

该项目需求来源于点我达骑手实时压力系统,为了了解业务区块历史各个时点的压力状况,我们需要将历史数据持久化下来。

起初方案:数据双写

点我达压力系统基于spark,实时计算自然区域网格压力值并持久化到redis存储中,在写完redis后会再往hdfs上再写一份,用来保存历史数据。

未分类

改进的方案:数据订阅 尝试通过redis的数据同步机制,实时获取增量数据,并同步到hive alt

未分类

项目介绍

  • 名称:redis-canal
  • 释义:canal的redis版本
  • 语言:java
  • 定位:基于redis数据库的aof的增量日志,提供数据的订阅和消费
  • 依赖:kafka,jstorm(非必要)
  • 源码:https://github.com/bigdataATdianwoba/redis-canal

工作原理

未分类

alt redis-canal会将自己伪装成redis的slave,来进行数据同步请求,master接收到开始同步的命令后则会将data changes生成的aof日志信息实时通过socket方式传输给redis-canal,redis-canal这边接收到change后,进行aof文件解析,进行数据封装,写入到我们大数据的kafka集群,已供后续应用消费使用

AOF数据格式

比如一条redis命令:“set name silas” 转换成aof格式如下:

$3 # 第一个参数长度为 4 
SET # 第一个参数 
$4 # 第二参数长度为 4 
name # 第二个参数 
$4 # 第三个参数长度为 4 
Jhon # 第二参数长度为 4

伪装slave过程

未分类

使用redis的info命令查看master信息

未分类

架构设计

未分类

源码详见github https://github.com/bigdataATdianwoba/redis-canal

运行

启动方式:

  • 普通jar包启动方式
java -jar redis-canal-server.jar --name RedisCanal  
--host localhost  
--port 6379  
--password xxx  
--broker localhost:9092  
--topic redis.canal.data

  • jstorm启动方式
jstorm jar redis-canal-server.jar com.dianwoba.bigdata.redis.canal.bootstrap.jstorm.CanalTopo  
--name RedisCanal  
--host localhost  --port 6379  
--password xxx  
--broker localhost:9092  
--topic redis.canal.data

redis-canal启动时打印出来的aof日志信息

未分类

redis-canal默认从master最近给slave同步的offset开始同步,则接收到的第一条aof日志为“+CONTINUE”,该模式下,master不会在建立同步连接后将全量的rdb文件传输给slave,这样避免了长时间的等待,且全量同步一次rdb文件对master的性能是有消耗的。

一般来说生产环境redis架构大多为1主1备,redis-canal可以选择对主或者对从进行同步。唯一区别的地方在于,如果是对备库进行同步,备库自己是没有其他slave来同步自己的数据的,则备实例就不会有 master_repl_offset 标记,那么redis-canal开始进行订阅则必然会进行一次rdb全量数据传输,且备库在传输前会进行一次bgsave,这个对性能影响较大;如果是对master进行同步,则是增量同步,影响较小。

注意点:

  • redis实例每次接受到同步请求命令,不管是sync还是psync,都会触发一次自身的bgsave,这个目前避免不了 。

  • 实际生产上备库并没有业务应用的读写请求,对备库来说一次bgsave也不算啥,要是数据比较多的比如几十个G这种情况,那影响时间就有点较长了,开启同步建议在非业务高峰。

bash启动时加载配置文件过程

当用户登录系统时,会加载各种bash配置文件,还会设置或清空一系列变量,有时还会执行一些自定义的命令。这些行为都算是启动bash时的过程。

另外,有些时候登录系统是可以交互的(如正常登录系统),有些时候是无交互的(如执行一个脚本),因此总的来说bash启动类型可分为交互式shell和非交互式shell。更细分一层,交互式shell还分为交互式的登录shell和交互式非登录shell,非交互的shell在某些时候可以在bash命令后带上”–login”或短选项”-l”,这时也算是登录式,即非交互的登录式shell。

一、判断是否交互式、是否登录式

判断是否为交互式shell有两种简单的方法:

方法一:判断变量”-“,如果值中含有字母”i”,表示交互式。

[root@xuexi ~]# echo $-
himBH

[root@xuexi ~]# vim a.sh
#!/bin/bash
echo $-

[root@xuexi ~]# bash a.sh
hB

方法二:判断变量PS1,如果值非空,则为交互式,否则为非交互式,因为非交互式会清空该变量。

[root@xuexi ~]# echo $PS1
[u@h W]$

判断是否为登录式的方法也很简单,只需执行”shopt login”即可。值为”on”表示为登录式,否则为非登录式。

[root@xuexi ~]# shopt login_shell  
login_shell     on
[root@xuexi ~]# bash

[root@xuexi ~]# shopt login_shell
login_shell     off

所以,要判断是交互式以及登录式的情况,可简单使用如下命令:

echo $PS1;shopt login_shell
或者
echo $-;shopt login_shell

二、几种常见的bash启动方式

1、正常登录(伪终端登录如ssh登录,或虚拟终端登录)时,为交互式登录shell。

[root@xuexi ~]# echo $PS1;shopt login_shell 
[u@h W]$
login_shell     on

2、su命令,不带”–login”时为交互式、非登录式shell,带有”–login”时,为交互式、登录式shell。

[root@xuexi ~]# su root

[root@xuexi ~]# echo $PS1;shopt login_shell 
[u@h W]$
login_shell     off
[root@xuexi ~]# su -
Last login: Sat Aug 19 13:24:11 CST 2017 on pts/0

[root@xuexi ~]# echo $PS1;shopt login_shell
[u@h W]$
login_shell     on

3、执行不带”–login”选项的bash命令时为交互式、非登录式shell。但指定”–login”时,为交互式、登录式shell。

[root@xuexi ~]# bash

[root@xuexi ~]# echo $PS1;shopt login_shell
[u@h W]$
login_shell     off
[root@xuexi ~]# bash -l

[root@xuexi ~]# echo $PS1;shopt login_shell
[u@h W]$
login_shell     on

4、使用命令组合(使用括号包围命令列表)以及命令替换进入子shell时,继承父shell的交互和登录属性。

[root@xuexi ~]# (echo $BASH_SUBSHELL;echo $PS1;shopt login_shell)
1
[u@h W]$
login_shell     on
[root@xuexi ~]# su

[root@xuexi ~]# (echo $BASH_SUBSHELL;echo $PS1;shopt login_shell)
1
[u@h W]$
login_shell     off

5、ssh执行远程命令,但不登录时,为非交互、非登录式。

[root@xuexi ~]# ssh localhost 'echo $PS1;shopt login_shell'

login_shell     off

6、执行shell脚本时,为非交互、非登录式shell。但指定了”–login”时,将为非交互、登录式shell。

例如,脚本内容如下:

[root@xuexi ~]# vim b.sh
#!/bin/bash
echo $PS1
shopt login_shell

不带”–login”选项时,为非交互、非登录式shell。

[root@xuexi ~]# bash b.sh

login_shell     off

带”–login”选项时,为非交互、登录式shell。

[root@xuexi ~]# bash -l b.sh

login_shell     on

7、在图形界面下打开终端时,为交互式、非登录式shell。

未分类

但可以设置为使用交互式、登录式shell。

未分类

三、加载bash环境配置文件

无论是否交互、是否登录,bash总要配置其运行环境。bash环境配置主要通过加载bash环境配置文件来完成。但是否交互、是否登录将会影响加载哪些配置文件,除了交互、登录属性,有些特殊的属性也会影响读取配置文件的方法。

bash环境配置文件主要有/etc/profile、~/.bash_profile、~/.bashrc、/etc/bashrc和/etc/profile.d/*.sh,为了测试各种情形读取哪些配置文件,先分别向这几个配置文件中写入几个echo语句,用以判断该配置文件是否在启动bash时被读取加载了。

echo "echo '/etc/profile goes'" >>/etc/_profile
echo "echo '~/.bash_profile goes'" >>~/.bash_profile
echo "echo '~/.bashrc goes'" >>~/.bashrc
echo "echo '/etc/bashrc goes'" >>/etc/bashrc
echo "echo '/etc/profile.d/test.sh goes'" >>/etc/profile.d/test.sh
chmod +x /etc/profile.d/test.sh

1、交互式登录shell或非交互式但带有”–login”(或短选项”-l”,例如在shell脚本中指定”#!/bin/bash -l”时)的bash启动时,将先读取/etc/profile,再依次搜索~/.bash_profile、~/.bash_login和~/.profile,并仅加载第一个搜索到且可读的文件。当退出时,将执行~/.bash_logout中的命令。

但要注意,在/etc/profile中有一条加载/etc/profile.d/*.sh的语句,它会使用source加载/etc/profile.d/下所有可执行的sh后缀的脚本。

[root@xuexi ~]# grep -A 8 *.sh /etc/profile  
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

内层if语句中的【”${-#i}” != “$-“】表示将”$-“从左向右模式匹配”i”并将匹配到的内容删除(即进行变量切分),如果”$-“切分后的值不等于”$-“,则意味着是交互式shell,于是怎样怎样,否则怎样怎样。

同样的,在~/.bash_profile中也一样有加载~/.bashrc的命令。

[root@xuexi ~]# grep -A 1 ~/.bashrc ~/.bash_profile
if [ -f ~/.bashrc ]; then
        . ~/.bashrc
fi

而~/.bashrc中又有加载/etc/bashrc的命令。

[root@xuexi ~]# grep -A 1 /etc/bashrc ~/.bashrc
if [ -f /etc/bashrc ]; then
        . /etc/bashrc
fi

其实/etc/bashrc中还有加载/etc/profile.d/*.sh的语句,但前提是非登录式shell时才会执行。以下是部分语句:

if ! shopt -q login_shell ; then   # We're not a login shell
...
    for i in /etc/profile.d/*.sh; do
        if [ -r "$i" ]; then
            if [ "$PS1" ]; then
                . "$i"
            else
                . "$i" >/dev/null 2>&1
            fi
        fi
    done
...
fi

从内层if语句和/etc/profile中对应的判断语句的作用是一致的,只不过判断方式不同,写法不同。

因此,交互式的登录shell加载bash环境配置文件的实际过程如下图:

未分类

以下结果验证了结论:

Last login: Mon Aug 14 04:49:29 2017     # 新开终端登录时
/etc/profile.d/*.sh goes
/etc/profile goes
/etc/bashrc goes
~/.bashrc goes
~/.bash_profile goes
 [root@xuexi ~]# ssh localhost        # ssh远程登录时
root@localhost's password:
Last login: Mon Aug 14 05:05:50 2017 from 172.16.10.1
/etc/profile.d/*.sh goes
/etc/profile goes
/etc/bashrc goes
~/.bashrc goes
~/.bash_profile goes
[root@xuexi ~]# bash -l        # 执行带有"--login"选项的login时
/etc/profile.d/*.sh goes
/etc/profile goes
/etc/bashrc goes
~/.bashrc goes
~/.bash_profile goes
[root@xuexi ~]# su -          # su带上"--login"时
/etc/profile.d/*.sh goes
/etc/profile goes
/etc/bashrc goes
~/.bashrc goes
~/.bash_profile goes
[root@xuexi ~]# vim a.sh    # 执行shell脚本时带有"--login"时
#!/bin/bash -l
echo haha

[root@xuexi ~]# ./a.sh 
/etc/profile goes
/etc/bashrc goes
~/.bashrc goes
~/.bash_profile goes
haha

之所以执行shell脚本时没有显示执行/etc/profile.d/.sh,是因为它是非交互式的,根据/etc/profile中的【if [ “${-#i}” != “$-” ]】判断,它将会把/etc/profile.d/*.sh的执行结果重定向到/dev/null中。也就是说,即使是shell脚本(带”–login “选项),它也加载了所有bash环境配置文件。

2、交互式非登录shell的bash启动时,将读取~/.bashrc,不会读取/etc/profile和~/.bash_profile、~/.bash_login和~/.profile。

因此,交互式非登录shell加载bash环境配置文件的实际过程为下图内方框中所示:

未分类

例如,执行不带”–login”的bash命令或su命令时。

[root@xuexi ~]# bash
/etc/profile.d/*.sh goes
/etc/bashrc goes
~/.bashrc goes
[root@xuexi ~]# su
/etc/profile.d/*.sh goes
/etc/bashrc goes
~/.bashrc goes

3、非交互式、非登录式shell启动bash时,不会加载前面所说的任何bash环境配置文件,但会搜索变量BASH_ENV,如果搜索到了,则加载其所指定的文件。但有并非所有非交互式、非登录式shell启动时都会如此,见情况4。

它就像是这样的语句:

if [ -n "$BASH_ENV" ];then
    . "$BASH_ENV"
fi

几乎执行所有的shell脚本都不会特意带上”–login”选项,因此shell脚本不会加载任何bash环境配置文件,除非手动配置了变量BASH_ENV。

4、远程shell方式启动的bash,它虽然属于非交互、非登录式,但会加载~/.bashrc,所以还会加载/etc/bashrc,由于是非登录式,所以最终还会加载/etc/profile.d/*.sh,只不过因为是非交互式而使得执行的结果全部重定向到了/dev/null中。

如果了解rsync,就知道它有一种远程shell连接方式。所谓的远程shell方式,是指通过网络的方式启动bash并将bash的标准输出关联起来,就像它连接了一个远程的shell守护进程一样。一般由sshd实现这样的连接方式,老版的rshd也一样支持。

事实也确实如此,使用ssh连接但不登录远程主机时(例如只为了执行远程命令),就是远程shell的方式,但它却是非交互、非登录式的shell。

[root@xuexi ~]# ssh localhost echo haha
root@localhost's password:
/etc/bashrc goes
~/.bashrc goes
haha

正如上文所说,它同样加载了/etc/profile.d/*.sh,只不过/etc/bashrc中的if判断语句【if [ “$PS1” ]; then】使得非交互式的shell要将执行结果重定向到/dev/null中。