WEB跨域与同源

这几天遇到跨域同源的问题。整理下知识点。

同源策略

浏览器有一个很重要的概念——同源策略(Same-Origin Policy)。所谓同源是指,域名,协议,端口相同。不同源的客户端脚(javascript、ActionScript)本在没明确授权的情况下,不能读写对方的资源。

如果Web世界没有同源策略,当你登录Gmail邮箱并打开另一个站点时,这个站点上的JavaScript就可以跨域读取你的Gmail邮箱数据,这样整个Web世界就无隐私可言了。

不同源举例:

1
2
3
4
5
6
7
8
9
10
11
# 同源要求同域名同协议同端口
域名:
http://www.bigertech.com
http://bigertech.com
http://www.bigertech.cn

协议:
httphttps

端口
127.0.0.1:3000 127.0.0.1:3001

常见的:

我的 A 站点,发了个 AJAX 请求到 B 站点,会收到这样的错误

1
No 'Access-Control-Allow-Origin' header is present on the requested resource

解决方法

1、服务器返回CORS响应头

Rails 方式

1
2
3
4
5
6
7
8
after_action :set_access_control_headers

def set_access_control_header
  headers['Access-Control-Allow-Origin'] = '*'
  headers['Access-Control-Request-Method'] = "GET, POST, PUT, PATCH, OPTIONS, DELETE, HEAD, TRACE"
  headers['Access-Control-Allow-Headers'] = 'Origin, Access-Control-Allow-Headers, X-Requested-With, Content-Type, Accept, Connection, User-Agent, Cookie'
    headers['Access-Control-Allow-Credentials'] = 'true'
end

当然这里偷懒指定了所有域名都允许跨域,一般来说是指定我们允许的域名

JAVA 方式

1
2
3
4
5
6
7
8
9
10
11
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
  HttpServletResponse response = (HttpServletResponse) servletResponse;
  String origin = (String) servletRequest.getRemoteHost()+":"+servletRequest.getRemotePort();
  response.setHeader("Access-Control-Allow-Origin", "*");
  response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, OPTIONS, DELETE, HEAD, TRACE");
  response.setHeader("Access-Control-Max-Age", "3600");
  response.setHeader("Access-Control-Allow-Headers", "Origin, Access-Control-Allow-Headers, X-Requested-With, Content-Type, Accept, Connection, User-Agent, Cookie");
  response.setHeader("Access-Control-Allow-Credentials","true");
  filterChain.doFilter(servletRequest, servletResponse);
}

当然,也可以使用 Spring 提供的 corsFilter,或使用注解等等方式

说明

Access-Control-Allow-Origin

它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

在前端 JS 也要设置该值:

1
2
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

Access-Control-Expose-Headers

该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader(‘FooBar’)可以返回FooBar字段的值

Access-Control-Allow-Methods

该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。

Access-Control-Allow-Headers

如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在”预检”中请求的字段。

Access-Control-Max-Age

该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

关于 Options 请求

浏览器将 CORS 请求分为两大类: 简单请求 和 非简单请求。

只要同时满足以下两个条件,则为简单请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
1) 请求方法是以下三种方法之一:

HEAD
GET
POST

2HTTP的头信息不超出以下几种字段:

Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

如果是非简单请求,那么在请求之前,浏览器会发出一条 OPTIONS 预检请求,如:

1
2
3
4
5
6
7
8
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

Access-Control-Request-Method

该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。

Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。

因此,服务端必须要处理 OPTIONS 请求的情况

Rails

1、使用 rack-cors gem

2、自己实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
before_action :cors_set_access_control_headers

def cors_preflight_check
  if request.method == 'OPTIONS'
    cors_set_access_control_headers
    render text: '', content_type: 'text/plain'
  end
end

protected

def cors_set_access_control_headers
  response.headers['Access-Control-Allow-Origin'] = '*'
  response.headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
  response.headers['Access-Control-Allow-Headers'] = 'Origin, Content-Type, Accept, Authorization, Token, Auth-Token, Email, X-User-Token, X-User-Email'
  response.headers['Access-Control-Max-Age'] = '1728000'
end


#routes
match '*all', controller: 'application', action: 'cors_preflight_check', via: [:options]

Java

  1. 默认在 Spring3.x 起,就会自动处理 Options 的请求了,所以可以不用做什么操作。
  2. 在一些旧版本,可以考虑在 filter 监测 options 请求,返回 true

2、使用JSONP方式

JSONP(JSON with Padding)是一个非官方的协议,它允许在服务器端集成Script tags返回至客户端,通过javascript callback的形式实现跨域访问(这仅仅是JSONP简单的实现形式)

虽然浏览器有同源策略,但是HTML中的img,script等标签是不用遵循同源策略的, JSONP 则是利用了 script 这个特点。

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
# callback 是 rails 直接支持 JSONP 的参数
# hello 一般是客户端给过来的 callback 函数名
# `callback函数` 将在客户端进行执行 script
def index
   render json: {hello: :world}, callback: hello
end

# 关闭 csrf 防护
# 因为返回格式为 xxx(),属于危险字符script
# protect_from_forgery with: :exception

# 返回结果
/**/hello({"hello":"world"})

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 方式1
<script src="localhost:3001/students">

# 方式2 ( 使用 jquery )
function hello(){
  console.log("something");
}

$.ajax({
    type: 'GET',
    jsonp: 'callback函数名',
    dataType: "jsonp",
    url: "http://localhost:3001/students",
    success: function(data) {
       # 一旦返回将会自动执行hello()
    },
    error: function(XMLHttpRequest, textStatus) {
      console.log("error");
    }
});

Tips

虽然说有同源策略的存在,站点 A 拿不到站点 B 的信息。但其实站点 B 已经处理完请求 response 回去了,只不过被站点 A 的浏览器给拦截了。

1
2
3
4
# 站点 B,此时已经成功处理请求返回 response 了
Started GET "/students" for ::1 at 2015-04-21 20:32:32 +0800
Processing by StudentsController#index as */*
Completed 200 OK in 1ms (Views: 0.1ms | ActiveRecord: 0.0ms)

因此,如果你在后台代码中 / Shell 中进行访问请求的话:

1
2
3
4
# 是成功返回信息的
$ curl http://localhost:3001/students

=>   /**/hello({"hello":"world"})%

有人说这样子那同源策略有什么用?不是一下子就被破解了么?

首先,同源策略是浏览器的行为。

其次,当你登录了网银后,打开站点A,此时站点A网银发送一条request,注意,此时是会附带你网银cookies过去的,相当于你在网银自己发的request一样,如果没有同源策略,这时你的网银就相当于别人的了。。。(当然网银的安全程度远远大于此- -)

而如果你用后台进行发送请求的话,是不会附带任何cookies的。因此也就无效了。

注意 postman是可以成功请求的。(不知道postman做了什么,总之postman没有阻止跨域的情况)

XSS 与 CSRF

说起安全,这两者可是 WEB 届大名鼎鼎的漏洞明星啊!

XSS : 跨站脚本攻击

CSRF : 跨站请求伪造攻击。

以下這篇文章写得非常好,值得一看。 http://snoopyxdy.blog.163.com/blog/static/60117440201281294147873/

Rails-CSRF-Explore

目录

  1. 引言
  2. session
  3. CSRF
  4. 真相大白
  5. 破解?

一、引言

这段时间一直在想,Rails 的 CSRF token 要破解不是很简单么?GET请求到网页拿到 token,随后发送恶意POST请求到服务器不就破解了么?事实证明我还是 Too young too simple.

二、尝试

  1. 写了个程序在ruby中去GET其他服务器的网页,获取到token后,伪造恶意POST请求,附带token,结果402 invalid token

  2. 真实打开浏览器,获取网页中token,随后复制该token,在ruby中恶意伪造POST请求并附带token,结果还是402 invalid token

  3. 真实打开浏览器,获取token,打开postman,发送POST请求。成功200

这是为什么呢?

二、session

在 Rails 中,默认的session存储方式是:ActionDispatch::Session::CookieStore

也就是默认将session的内容存放到客户端cookie中。

1
2
_glassx_session:
TE9xZ3Zud1dxRFYxdEtSek5mYldTMkpnZ1NWUlF5SjdzODVRSkJYNGN6VmNDc1VGb1lzSGJPU0FLYWhoMU5ZSHZCeXUwNTFWdWFQaWpKZmZSUC96c0dWbFdDcmlZK3RTcENabXZoaFVScWx1SWlxR1dEbmcwU1BXSDBZWUVOVW1EN0ZscmZpWkJsOFBZajZST0Z3VWxJM09NOGVTRFp2djRyYVB4SVJZNkVWRlM4dmw0TVNYb01jOGJYdVRXKzYxY1pyeXNtT0VkVWZ4YjZFcTdVU1FLVEU2aXlXbGRIOWY3c0Q1THlPRGtWeFhOb1BCd1M0S3hOQ04xTHNSajh3MC0tS3I0UVZkK2tuWGx0d1BDSVp3b1diZz09--cd6094c15ae50026d0377b6c32b7e0986b447d74

session中的数据保存在cookie中时是先marshal后,然后利用密码来加密的。

1
marshal(data)---digest_with_secret_key(marshal(data))

这里所用secret key则是我们在config/secrets.yml设置的。

加密的原因是防止 session 被偷看,并且防止 session 被修改

三、CSRF

我们在访问服务器的网页时,会找到这样的 token

1
2
<meta name="csrf-param" content="authenticity_token">
<meta name="csrf-token" content="yT5/q+GxMDy96ISmdNfE4esNrc8YZOBUrQefXO21tJ19iGD1XjRkJ2/ELC1A952U5qDp3vpo6MhhHQB8fOmivw==">

token是服务器生成后返回的。

具体流程是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 生成加密的token(也就是我们在网页上见到的csrf-token)
def masked_authenticity_token(session)
  one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
  encrypted_csrf_token = xor_byte_strings(one_time_pad, real_csrf_token(session))
  masked_token = one_time_pad + encrypted_csrf_token
  Base64.strict_encode64(masked_token)
end

# 生成 _csrf_token,并且将其放入`session[:_csrf_token]`中
def real_csrf_token(session)
  session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
  Base64.strict_decode64(session[:_csrf_token])
end

# 验证 token 是否有效(这段代码我简化过)
def valid_authenticity_token?(session, encoded_masked_token)
  masked_token = Base64.strict_decode64(encoded_masked_token)
  if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
       compare_with_real_token masked_token, session
  elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
     one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
       encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
       csrf_token = xor_byte_strings(one_time_pad, encrypted_csrf_token)

       compare_with_real_token csrf_token, session
    end
end

# 开始验证
def compare_with_real_token(unmasked_token, session)
   ActiveSupport::SecurityUtils.secure_compare(unmasked_token, real_csrf_token(session))
end

从源码可以看出,服务器会将生成的_csrf_token放入用户的session中,当客户端将csrf_token随着request发送过来时,服务器会将csrf_token转换回非加密,随后与session中的_csrf_token进行验证。

四、真相大白

  1. 验证时需要session内的_csrf_token,而用ruby代码直接发送http请求时是不会附带cookies的,所以验证肯定不会通过,这也是上面尝试第一二步失败的原因。
  2. 使用postman是会自带cookies发送过去的,因此验证通过。

Tips

附带cookies是浏览器的行为

五、破解?

这时有人会问,rails是开源的,有人看了源码之后,拿到网页的csrf_token,按照步骤一步步转换回非加密状态,不就破解了?

还是 too young!

在服务器是将session[_csrf_token]跟用户的csrf_token进行验证,就算你将csrf_token还原了也没用,还是需要session[_csrf_token],而session是经过secret加密过的,因为无法篡改session,因而也破解不了。

使用Ruby生成二维码

前言

偶尔项目中会用到二维码,这时生成二维码有几种方式:

  1. 自己动手,丰衣足食,自己来生成
  2. 通过调用网络API来拉取二维码

但是,由于担心网络的不稳定 或者 项目只能对内部开放,此时就需要第一种方式来实现了。

所需 Gem

  1. gem ‘qrencoder’
  2. gem ‘rqrencoder-magick’
  3. gem ‘rqrcode_png’

Begin

1. 先安装本机库 qrencode

Mac 用户:

1
brew install qrencode

Linux 用户:

1
apt-get install qrencode

2. 安装 qrencoder 这个 gem

项目主页: https://github.com/harrisj/qrencoder

安装之前需要先安装依赖库,否则会安装不上:

1
apt-get install libqrencode-dev python-qrencode qrencode

安装时需要指定路径:

1
gem install qrencoder –with-opt-include=/usr/local/include –with-opt-lib=/usr/local/lib

gem qrencoder

3. 安装 rqrencoder-magickrqrencoder

前者是利用了RMagick来生成二维码,因此需要事先安装 RMagick,安装方法就不说了。

gemfile

1
gem 'rqrencoder-magick'

shell

1
gem install rqrencoder –with-opt-include=/usr/local/include –with-opt-lib=/usr/local/lib

4. 安装rqrcode_png

gem rqrcode_png

这个 Gem 可以为生成的二维码指定宽度高度

5. DEMO

1
2
3
4
# 生成二维码
qr = RQRCode::QRCode.new( self.code, :size => 1, :level => :l )
png = qr.to_img
png.resize(300, 300).save(path)

6. 不足

这种方式生成的二维码图片会比现今网上提供的api所生成的二维码要「难扫」「复杂」,所以如果对二维码的扫描难度有要求的话,就….

使用 Mosquitto

引言

这段时间团队需要跟 Google Glass 进行交互,因此要做一个推送机制。而 MQTT 协议的推送是当今最火热的一个。

Mosquitto 则是实现了 MQTT 协议的服务。

安装

在 Ubuntu 下:

1
2
sudo apt-add-repository ppa:mosquitto-dev/mosquitto-ppa
sudo apt-get update

如果显示apt-add-repository没有识别,则可以:

1
sudo apt-get install python-software-properties

配置

安装完成后,所有配置都会在 /etc/mosquitto目录下。其中最重要的则是mosquitto.conf 文件,以下则是配置文件内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# =================================================================
# General configuration
# =================================================================

# 客户端心跳的间隔时间
#retry_interval 20

# 系统状态的刷新时间
#sys_interval 10

# 系统资源的回收时间,0表示尽快处理
#store_clean_interval 10

# 服务进程的PID
#pid_file /var/run/mosquitto.pid

# 服务进程的系统用户
#user mosquitto

# 客户端心跳消息的最大并发数
#max_inflight_messages 10

# 客户端心跳消息缓存队列
#max_queued_messages 100

# 用于设置客户端长连接的过期时间,默认永不过期
#persistent_client_expiration

# =================================================================
# Default listener
# =================================================================

# 服务绑定的IP地址
#bind_address

# 服务绑定的端口号
#port 1883

# 允许的最大连接数,-1表示没有限制
#max_connections -1

# cafile:CA证书文件
# capath:CA证书目录
# certfile:PEM证书文件
# keyfile:PEM密钥文件
#cafile
#capath
#certfile
#keyfile

# 必须提供证书以保证数据安全性
#require_certificate false

# 若require_certificate值为true,use_identity_as_username也必须为true
#use_identity_as_username false

# 启用PSK(Pre-shared-key)支持
#psk_hint

# SSL/TSL加密算法,可以使用“openssl ciphers”命令获取
# as the output of that command.
#ciphers

# =================================================================
# Persistence
# =================================================================

# 消息自动保存的间隔时间
#autosave_interval 1800

# 消息自动保存功能的开关
#autosave_on_changes false

# 持久化功能的开关
persistence true

# 持久化DB文件
#persistence_file mosquitto.db

# 持久化DB文件目录
#persistence_location /var/lib/mosquitto/

# =================================================================
# Logging
# =================================================================

# 4种日志模式:stdout、stderr、syslog、topic
# none 则表示不记日志,此配置可以提升些许性能
log_dest none

#还有一种写入文件的模式
log_dest file '/var/lib/mosquitto/mosquitto.log'

# 选择日志的级别(可设置多项)
#log_type error
#log_type warning
#log_type notice
#log_type information
#log_type all

# 是否记录客户端连接信息
#connection_messages true

# 是否记录日志时间
#log_timestamp true

# =================================================================
# Security
# =================================================================

# 客户端ID的前缀限制,可用于保证安全性
#clientid_prefixes

# 允许匿名用户
#allow_anonymous true

# 用户/密码文件,默认格式:username:password
#password_file

# PSK格式密码文件,默认格式:identity:key
#psk_file

# pattern write sensor/%u/data
# ACL权限配置,常用语法如下:
# 用户限制:user <username>
# 话题限制:topic [read|write] <topic>
# 正则限制:pattern write sensor/%u/data
#acl_file

# =================================================================
# Bridges
# =================================================================

# 允许服务之间使用“桥接”模式(可用于分布式部署)
#connection <name>
#address <host>[:<port>]
#topic <topic> [[[out | in | both] qos-level] local-prefix remote-prefix]

# 设置桥接的客户端ID
#clientid

# 桥接断开时,是否清除远程服务器中的消息
#cleansession false

# 是否发布桥接的状态信息
#notifications true

# 设置桥接模式下,消息将会发布到的话题地址
# $SYS/broker/connection/<clientid>/state
#notification_topic

# 设置桥接的keepalive数值
#keepalive_interval 60

# 桥接模式,目前有三种:automatic、lazy、once
#start_type automatic

# 桥接模式automatic的超时时间
#restart_timeout 30

# 桥接模式lazy的超时时间
#idle_timeout 60

# 桥接客户端的用户名
#username

# 桥接客户端的密码
#password

# bridge_cafile:桥接客户端的CA证书文件
# bridge_capath:桥接客户端的CA证书目录
# bridge_certfile:桥接客户端的PEM证书文件
# bridge_keyfile:桥接客户端的PEM密钥文件
#bridge_cafile
#bridge_capath
#bridge_certfile
#bridge_keyfile

# 自己的配置可以放到以下目录中
include_dir /etc/mosquitto/conf.d

权限

为了避免任何人都可以往服务器去 pull&push,我们需要设置权限。

使用自带工具进行设置账号:

1
2
3
4
5
#add a count and then will ask you to enter a passwd
mosquitto_passwd -c /etc/mosquitto/passwd glassx

# delete
mosquitto_passwd -D /etc/mosquitto/passwd glassx

启动

启动服务很简单,直接运行:

1
2
3
4
5
6
mosquitto -c /etc/mosquitto/mosquitto.conf -d

#-c 表示加载指定配置文件
#-d 表示以后台服务运行
#-p 表示监听某个端口(默认为1883)
#-v 表示亢长的日志记录模式,相当于设置 log_type all

mosquitto 是个异步 IO 框架,经过测试可以处理 20000 个以上的客户端连接。

推送

上面所创建的只是服务器, MQTT 有三个角色, 发布角色,服务器角色,消费角色。 此时我们便使用 Ruby 来实现发布角色。

我们会使用到一个叫做 mqtt 的 gem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
require 'rubygems'
require 'mqtt'

# Publish example
MQTT::Client.connect('mqtt://glassx:glassxpw@127.0.0.1') do |c|
  p c.publish('topic', 'message')
end

# # Subscribe example
# MQTT::Client.connect('test.mosquitto.org') do |c|
#   # If you pass a block to the get method, then it will loop
#   c.get('test') do |topic,message|
#     puts "#{topic}: #{message}"
#   end
# end

接收

接收则是各个客户端的实现了,这里就不写了。

引用

官网:http://mosquitto.org/

简要教程:http://blog.csdn.net/shagoo/article/details/7910598

部署 RabbitMQ 经验

引用

在项目中,将一些无需即时返回且耗时的操作提取出来,放入消息队列,进行异步处理,而这种异步处理的方式大大的节省了服务器的请求响应时间,从而提高了系统的吞吐量。

RabbitMQ 是一个在 AMQP 基础上完整的,可复用的企业消息系统。他遵循Mozilla Public License开源协议。

安装

Ubuntu 下:

1
2
3
4
5
6
deb http://www.rabbitmq.com/debian/ testing main

wget http://www.rabbitmq.com/rabbitmq-signing-key-public.asc
sudo apt-key add rabbitmq-signing-key-public.asc

sudo apt-get install rabbitmq-server
1
2
#/etc/default/rabbitmq-server
ulimit -n 1024
1
2
3
4
5
# 开启服务
invoke-rc.d rabbitmq-server stop/start/etc.

# 开启 WEB 管理
rabbitmq-plugins enable rabbitmq_management

注意事项

  1. 要自己添加 /etc/rabbitmq/rabbitmq.config 才行
  2. 默认只有本机才能用 guest/guest 账号登陆。否则需要在rabbitmq.config 添加[{rabbit, [{loopback_users, []}]}].
  3. rabbitq.config 的格式是:
1
2
3
# 就算删除了里面的参数配置,也不能删除这个格式。
[
].

可能出现的错误

  1. 总是提示
1
  ls: cannot access /etc/rabbitmq/rabbitmq.conf.d: No such file or directory

原因: 妈蛋是缺了目录啊!手动创建就可以了,之前一直以为是缺了文件,原来是目录,注意了 conf.d .d 这个结尾的一般都是目录

具体就不写了,不过有篇文章写得很好,看看就明白了。

https://ruby-china.org/topics/22332

在 Ruby 中用 SSH 来跟服务器进行通信

在Ruby中用SSH跟服务器进行交流

前言

Net::SSH和Net::SCP是两个Ruby操作SSH的gem包。

Net::SSH相当于cmd,专门用于执行命令;

Net::SCP专门用于传输文件。

它们俩结合,可以做任何SSH client能做的事情。

所需 Gem

  1. gem ‘net-ssh’
  2. gem ‘net-scp’

Begin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Net::SSH.start(MYSQL_SYNC_CONFIG["#{sequence}_server_ip"], MYSQL_SYNC_CONFIG["#{sequence}_server_user"]) do |ssh|

        # 创建文件夹
        ssh.exec!(
          "if [ ! -d #{MYSQL_SYNC_CONFIG['endpoint_backup_path']} ]; then\n" \
          "mkdir #{MYSQL_SYNC_CONFIG['endpoint_backup_path']};" \
          "fi"
        )

        # 开始上传
        ssh.scp.upload!("#{backup_sql_path}", "#{MYSQL_SYNC_CONFIG['endpoint_backup_path']}/#{backup_sql_name}")

        # 客户端开始还原
        ssh.exec!("mysql -u" + MYSQL_SYNC_CONFIG["#{sequence}_server_mysql_user"] +
                  " -p" + MYSQL_SYNC_CONFIG["#{sequence}_server_mysql_pwd"] +
                  " red_mansions < #{MYSQL_SYNC_CONFIG['endpoint_backup_path']}/#{backup_sql_name}")
      end

引用

http://rubyer.me/blog/1133/

http://www.infoq.com/cn/articles/ruby-file-upload-ssh-intro

https://gist.github.com/lajunta/7305741

使用 Rsync

使用rsync

1. 服务器端

/etc/rsyncd.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
uid = deploy
gid = deploy
use chroot = no
max connections = 4
pid file = /var/run/rsyncd.pid
lock file = /var/run/rsyncd.lock
log file = /var/log/rsyncd.log

[test]
path = /var/test
ignore errors
read only = true
list = false
auth users = fuck
secrets file = /etc/backserver.pas

/etc/backserver.pas ( 需要设置权限400)

1
hello:world   (该账号密码最好不跟服务器一样)

usr/bin/rsync --daemon 启动服务

echo "/usr/local/rsync/bin/rsync --daemon" >> /etc/rc.local 开机自启动

2. 客户端

2.1 同步方式

2.1.1. 第一种方式是 服务器–客户端方式。

这种方式,服务器启动 daemon 守护线程,监听端口 873,并配置需要同步的模块,然后客户端连接 873 端口,认证并同步。

其中,同步所使用的账号密码是 rsync 单独配置的,与系统无关。

服务端运行rsync进程在daemon模式下, 客户端是普通的rsync进程。

2.1.2. 使用 ssh 方式

本机 rsync 进程 直接通过 ssh 通道连接到远程, 并在远程ssh通道执行命令

两者是走不同协议,不同端口的,因此第一种方式服务器是不需要启动 rsync 服务的,当然还是需要安装这个程序

2.2 使用账号密码登录(此时服务器需要启动 rsync –daemon 服务)

1
USER@HOST::MODE  # mode 是服务器设置好的模块名,这里不能用路径,注意两个冒号

/etc/rsync_client.pas

1
glassx # 只需要配置连接时使用的密码即可,必须与A服务器上定义的密码相同.

chmod 600 /etc/rsync_client.pas

usr/bin/rsync -vzrtopg --progress --delete --password-file=/etc/rsync_client.secrets hello@192.168.10.240::test /var/rsync/test

跟在IP后的test 是指服务端配置的模块

2.3 使用SSH ( 此时服务器不需要启动 rsync –daemon 服务)

1
2
3
USER@HOST:Folder # 注意只有一个冒号,rsync由此判断使用ssh通道。而不是直接连接远端的873端口。

# 使用了SSH后,就不能再用 :test 了,而是要跟纯路径。

usr/bin/rsync -vzrtopg --progress --delete -e ssh deploy@192.168.10.240:/home/deploy/test /home/deploy/test

注意权限的问题,本地文件夹要有足够权限去同步文件(也不能在该命令前加 sudo, 这样用ssh时会错乱,如果用密码连接则可以)

2.4 参考

http://blog.sina.com.cn/s/blog_544f183101013zlo.html

http://tech.huweishen.com/gongju/1529.html

http://www.cszhi.com/20120312/rsync-simple.html

Mina 简介使用

说下 Mina

一、基本使用

1
2
3
gem 'ruby'

mina init    =>  生成 config/deploy.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# config/deploy.rb
set :user, 'username'  # 部署用户
set :domain, 'your.server.com'   # 域名
set :deploy_to, '/var/www/flipstack.com' # 部署路径
set :repository, 'git@github.......xxx' #git 仓库
set :branch, 'master' #分支
set :shared_paths, ['config/database.yml', 'public/system' ...] #共享文件夹

然后:

1.
task: environment do
  invoke :'rvm:use[ruby-2.0.0@rails4.0.4]'
end

2.
# 主要为 deploy 做准备,创建各个文件夹
task :setup => :environment do
  queue! %[mkdir -p "#{deploy_to}/shared/log"]
  queue! %[chmod g+rx,u+rwx "#{deploy_to}/shared/log"]

  queue! %[mkdir -p "#{deploy_to}/shared/config"]
  queue! %[chmod g+rx,u+rwx "#{deploy_to}/shared/config"]

  queue! %[mkdir -p "#{deploy_to}/shared/public/qrcodes"]
  queue! %[mkdir -p "#{deploy_to}/shared/public/qrcodesZip"]

  queue! %[mkdir -p "#{deploy_to}/shared/public/uploads"]
  queue! %[mkdir -p "#{deploy_to}/shared/public/system"]

  queue! %[mkdir -p "#{deploy_to}/shared/coupons"]
  queue! %[chmod g+rx,u+rwx "#{deploy_to}/shared/coupons"]

  queue! %[mkdir -p "#{deploy_to}/shared/couponsZip"]
  queue! %[chmod g+rx,u+rwx "#{deploy_to}/shared/couponsZip"]

  queue! %[touch "#{deploy_to}/shared/config/database.yml"]
  queue  %[echo "-----> Be sure to edit 'shared/config/database.yml'."]
end

3.

# 编写部署任务
task :deploy => :environment do
  deploy do
    # Put things that will set up an empty directory into a fully set-up
    # instance of your project.
    invoke :'sidekiq:quiet'
    invoke :'git:clone'
    invoke :'deploy:link_shared_paths'
    invoke :'bundle:install'
    invoke :'rails:db_migrate'
    invoke :'rails:assets_precompile'

    to :launch do
      queue "if [ -d #{deploy_to}/current/tmp ]
      then
        touch #{deploy_to}/current/tmp/restart.txt
      else
        mkdir #{deploy_to}/current/tmp
        touch #{deploy_to}/current/tmp/restart.txt 
      fi"
      invoke :'sidekiq:restart'
    end
  end
end

终端执行

1
2
3
mina setup

mina deploy

二、应该要注意的 point

1、我们可以自己写 task 任务

2、queue 命令用在执行 bash 命令,如:

1
2
3
4
task :logs do
     queue 'echo "Contents of the log file are as follows:"'
     queue "tail -f /var/log/apache.log"
end

3、invoke 命令用在引用已经写好的 task 任务,如:

1
2
3
4
5
6
7
task :down do
     invoke :maintenance
end

task :maintenance do
     queue 'touch maintenance.txt'
end

4、Mina 已经有一些已经写好的 task 任务,如:

1
2
3
4
invoke :'git:clone'
invoke :'bundle:install'

#我一开始以为这是执行 bash 命令,让我理不清 invoke 跟 queue 的关系

5、run! 这个命令是指 SSH 进主机,然后执行所有已经 queue 的命令。这个命令会在 Rake 退出前,自动调用。

1
2
3
4
# 当 rake 退出时会调用。
  def mina_cleanup
    run! if commands.any?
  end

6、command 包含所有已经 queue 的任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
queue "sudo restart"
queue "true"

to :clean do     # 这里 clean 的queue 会被放到 :clean 命名空间下
  queue "rm"
end

commands == ["sudo restart", "true"]
commands(:clean) == ["rm"]


注意了,commands 是一个方法,而不是变量

# 源代码
def commands(aspect=:default)
  (@commands ||= begin
    @to = :default
    Hash.new { |h, k| h[k] = Array.new }
  end)[aspect]
end

因此如果要清空 commands,应该是执行:

@commands[:default] = []

7、isolate 命令会开辟一个新的 block,包含 queue 的任务

1
2
3
4
5
6
7
8
9
10
11
queue "sudo restart"
queue "true"

commands.should == ['sudo restart', 'true']

isolate do
  queue "reload"
  commands.should == ['reload']
end

commands.should == ['sudo restart', 'true']

8、已被 invoke 的 task, 再次被 invoke 时,是不会再次被执行的,除非:

1
2
3
4
# 该参数表明再次invoke时会继续执行一次。
invoke :setup, {reenable: true}

# 注意,任务是否已经被 invoke 过,并不是通过 commands 内的值来判断的,因此就算 commands 清空了,task 还是会被标记成已 invoke 过的。

9、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
task :hello do
  set :deploy_to, 'hellooooooooo'
end

task :world do
  p deploy_to
end

$-> mina hello world

===>  'hellooooooooo'

说明 mina task1 task2 是可以串行执行的,并且任务1的环境会跟任务2相连。(同个执行环境)
通常用在设置不同的 stage

三、多机部署

一、将同一项目部署到多个服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
set :domains, %w[192.168.0.12 192.168.0.13]

desc "multi deploy"
task :multi_deploy do
    domains.each_with_index do |domain, index|

      p "begin to deploy#{domain}"
      set :domain, domain
      invoke :deploy
      run!    => # 注意这里一定要加上 run! ,才会立即运行命令
      p "finish to deploy"
    end
end

自己写的一个 task, 这时候遇到一个难题,发现 invoke :deploy ,当第一次循环的时候正常,第二次循环的时候,会部署两次。

效果是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
 begin to deploy server 192.168.0.12

    deploying.....

 finish to deploy server 192.168.0.12

 begin to deploy server 192.168.0.13

    deploying.....

 finish to deploy server 192.168.0.13

    deploying......  ( server 192.168.0.13 )

如果我的 domains 是

1
set :domains, %w[192.168.0.12 192.168.0.13 192.168.0.14]

那么效果会是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 begin to deploy server 192.168.0.12

    deploying.....

 finish to deploy server 192.168.0.12

 begin to deploy server 192.168.0.13

    deploying.....

 finish to deploy server 192.168.0.13

 begin to deploy server 192.168.0.14

    deploying.....

 finish to deploy server 192.168.0.14

    deploying......  ( server 192.168.0.14 )

发觉最终总是会多部署最后一个server

于是我用

1
p "命令集合:#{commands(:default)}"

来查看,也没有什么异常。

然后我又做了下实验:

1
2
3
4
5
6
7
8
9
task :deploy_primary do
  p "begin to deploy primary #{primary_domain}"
  set :domain, primary_domain
  invoke :deploy
  invoke :mysql_sync
  run!
  p "finish to deploy primary #{primary_domain}"
  p commands
end

原因

在 rake 退出前会自动调用 run! 这个方法,因此到最后是会多执行一次 commands 内的命令的。

1
2
3
def mina_cleanup!
  run! if commands.any?
end

解决方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
set :domains, %w[192.168.0.12 192.168.0.13]


desc "multi deploy"
task :multi_deploy do
   isolate do   # =>  开辟一个新的 block (这样isolate外面的commands为[])
    domains.each_with_index do |domain, index|
      p "begin to deploy#{domain}"
      set :domain, domain
      # 这里其实只 invoke 了一次,之所以执行了三次,是因为有三次 run!
      # 而 domain 参数并不是写进 commands 的,而是在 run! 的时候进行处理
      # 因此这里可以成功的分别部署到三台机器
      invoke :deploy
      run!
      p "finish to deploy"
    end
  end
  # rake 任务退出时执行 run!, 但此时 commands 为 []
end

二、将同一项目部署到同一机器的两个目录

因为该项目需要做全站国际化,因此我将它分别部署为两套 server.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
task :deploy_all do
  isolate do
    2.times do |sequence|
      p "begin to deploy #{sequence}"
      if sequence == 0
        set :deploy_to, '/home/deploy/xshare_en'
      else
        set :deploy_to, '/home/deploy/xshare_zh'
      end
      invoke :deploy
      run!
      p "end to deploy #{sequence}"
    end
  end
end

这么做的后果是:部署了两次到 /home/deploy/xshare_en去了。

原因

1、 deploy_to 的值是直接写进 commands 内的

1
[...Setting up /home/deploy/xshare_zh\" && (\n  mkdir -p \"/home/deploy/xshare_zh\" &&\n  chown -R `whoami` \"/home/deploy/xshare_zh\" &&\n  chmod g+rx,u+rwx \"/home/deploy/xshare_zh\" &&\....]

2、这里 :deployinvoke 了一次,因此 commands 内的值是没有变化的,所以相当于执行了两次一样的第一次commands

尝试1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
task :deploy_all do
  isolate do
    2.times do |sequence|
      p "begin to deploy #{sequence}"
      if sequence == 0
        set :deploy_to, '/home/deploy/xshare_en'
      else
        set :deploy_to, '/home/deploy/xshare_zh'
      end
      # 重复 invoke
      invoke :deploy, reenable: true
      run!
      # 清空 commands
      @commands[:default] = []
      p "end to deploy #{sequence}"
    end
  end
end

结果还是没达到预料中的结果。

因为task deploy 中还 invoke 了其他的 task,比如git:clone,因此其他的task只算invoke了一次,就算我手动将deploy所引用的task全部都加reenable参数,还是不行,因为难保其他task没invoke另外的task

尝试2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
task :deploy_all do
  2.times do |sequence|
  # 尝试将两者区分开block
    isolate do
      p "begin to deploy #{sequence}"
      if sequence == 0
        set :deploy_to, '/home/deploy/xshare_en'
      else
        set :deploy_to, '/home/deploy/xshare_zh'
      end
      # 重复 invoke
      invoke :deploy
      run!
      # 清空 commands
      @commands[:default] = []
      p "end to deploy #{sequence}"
    end
  end
end

但还是失败,:deploy 是否被 invoke,跟 isolate 无关,因为 isolate 控制的只是 commands 而已。

解决

没找到好办法,暂时分开task执行,不过这种使用场景也比较少。

单独执行命令

1
2
3
task :mysql_sync do
  queue %[cd #{deploy_to!}/current && whenever -i 'mysql_sync']
end

当初单独执行这个 task 的时候,一直提示找不到 whenever 这个命令,令人纳闷,最终发现需要加上环境。

1
2
3
task :mysql_sync => :environment do
  queue %[cd #{deploy_to!}/current && whenever -i 'mysql_sync']
end

重复的 queue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
set :domains, %w[192.168.0.12 192.168.0.13]


desc "multi deploy"
task :multi_deploy do
   isolate do   # =>  开辟一个新的 block ( 不过为何能解决我还没弄懂)
    domains.each_with_index do |domain, index|
      p "begin to deploy#{domain}"
      set :domain, domain
      invoke :deploy
      if index == 1
         queue 'do something one'
      else
         queue 'do something two'
      end
      run!    => # 注意这里一定要加上 run! ,才会立即运行命令
      p "finish to deploy"
    end
  end
end

这样子的话,当第二次循环时,会执行 onetwo 两个 queue, 因为第一次循环时, one 已经 queued 了,进入了 commands,因此第二次循环时,会执行这个命令 ( 执行所有 commands)

这是个难点。我还没找到解决方法。只能说分开来执行,或者写脚本根据不同服务器执行不同命令

使用 Capistrano 来部署应用

使用 Capistrano 部署应用

前言

由于 Mina 一直以简单好用为著称,因此一直都是用 Mina 来部署应用。但是 Mina 也有局限性,之前的多机部署就遇到了难题,于是学习下如何用 Capistrano 来部署 Rails 应用

使用环境

  1. Capistrano 3.x
  2. Rails4.1
  3. Ruby2.0
  4. RVM

安装

gemfile中添加支持的 gem

1
2
3
4
5
6
group :development do
  gem 'capistrano'
  gem 'capistrano-bundler'
  gem 'capistrano-rails'
  gem 'capistrano-rvm'
end

初始化

1
$ cap install

其中有几个比较重要的文件:

  1. Capfile用来配置Capistrano
  2. deploy.rb是一些共用task的定义
  3. production.rb / staging.rb用来定义具体的stage的tasks。

通过 cap -vT 可以查看当前可用的 task

配置

capfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Load DSL and Setup Up Stages
require 'capistrano/setup'

# Includes default deployment tasks
require 'capistrano/deploy'

# 配置支持的插件
require 'capistrano/rvm'
require 'capistrano/bundler'
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'

# Loads custom tasks from `lib/capistrano/tasks' if you have any defined.
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }

deploy.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# config valid only for Capistrano 3.1
lock '3.2.1'

set :application, 'student_lottery'
set :repo_url, 'git@bitbucket.org:linjunzhugg/student_lottery.git'

# Default branch is :master
# ask :branch, proc { `git rev-parse --abbrev-ref HEAD`.chomp }.call

# Default deploy_to directory is /var/www/my_app
set :deploy_to, '/home/deploy/student_lottery'

# Default value for :scm is :git
set :scm, :git

# Default value for :format is :pretty
# set :format, :pretty

# Default value for :log_level is :debug
# set :log_level, :debug

# Default value for :pty is false
# set :pty, true

# Default value for :linked_files is []
set :linked_files, %w{config/database.yml}

# Default value for linked_dirs is []
set :linked_dirs, %w{bin log tmp/pids tmp/cache tmp/sockets vendor/bundle public/system}

set :rvm_ruby_version, 'ruby-2.0.0-p481@rails4.0.4'

# Default value for default_env is {}
# set :default_env, { path: "/opt/ruby/bin:$PATH" }

# Default value for keep_releases is 5
# set :keep_releases, 5

namespace :deploy do

  desc 'Restart application'
  task :restart do
    on roles(:app), in: :sequence, wait: 5 do
      # Your restart mechanism here, for example:
      # execute :touch, release_path.join('tmp/restart.txt')
    end
  end

  # 就是这里插入的任务
  after :publishing, :restart

  after :restart, :clear_cache do
    on roles(:web), in: :groups, limit: 3, wait: 10 do
      # Here we can do anything such as:
      # within release_path do
      #   execute :rake, 'cache:clear'
      # end
    end
  end

end

config/production.rb

1
2
3
4
set :branch, 'master'
# roles 的存在是方便我们针对某个 role 来执行相应的 task
server '192.168.0.13', user: 'deploy', roles: %w{app, web}
set :rails_env, :production

开始部署

1
$ cap production deploy  # 开始部署

这时有人疑惑了,咦?这条命令的 task 在哪呢?我要如何定义自己的task 呢?

根据官方文档:一旦运行 cap production deploy ,就默认有以下这些任务:

1
2
3
4
5
6
7
8
deploy:starting    - start a deployment, make sure everything is ready
deploy:started     - started hook (for custom tasks)
deploy:updating    - update server(s) with a new release
deploy:updated     - updated hook
deploy:publishing  - publish the new release
deploy:published   - published hook
deploy:finishing   - finish the deployment, clean up everything
deploy:finished    - finished hook

而我们之前在 capfile 中添加了以下的代码:

1
2
3
4
5
# 配置支持的插件
require 'capistrano/rvm'
require 'capistrano/bundler'
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'

这些会自动在 starting updating 等任务中插入 before after 任务,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
deploy
  deploy:starting
    [before]
      deploy:ensure_stage
      deploy:set_shared_assets
    deploy:check
  deploy:started
  deploy:updating
    git:create_release
    deploy:symlink:shared
  deploy:updated
    [before]
      deploy:bundle
    [after]
      deploy:migrate
      deploy:compile_assets
      deploy:normalize_assets
  deploy:publishing
    deploy:symlink:release
  deploy:published
  deploy:finishing
    deploy:cleanup
  deploy:finished
    deploy:log_revision

从中我们可以看到里面有 bundle migrate 等任务。

其他插件

比如 sidekiq 的自动重启,可以加上 capistrano-sidekiq gem

Linux 使用 LVM 来进行硬盘扩容

Linux 使用 LVM 进行硬盘扩容

前言

这阵子某个项目的服务器硬盘爆了,导致服务出现了异常,汗,现在用着 Google 和 阿里云 的云服务器,但发现两者都没有直接扩容的功能。所以就买了第二块硬盘,但第二块硬盘以后用满了怎么办?因此需要用到 LVM 来对硬盘进行动态扩容,不过并不能在云服务器的默认硬盘上,需要另外购置一块硬盘。

什么是 LVM?

LVM 的全名是 Logical Volumn Manager,逻辑卷轴管理员,

一般来说,我们都会将某个硬盘映射到某个分区/目录,但总会有一天该硬盘会满掉,那怎么办呢?买第二块硬盘再把数据移过去?

LVM 就是为了解决这样的问题,它可以将多个硬盘不断的叠加起来,就像一块硬盘在使用一样。

其中有几个概念: PV VG LV PE

PV 指将硬盘转成 Linux LVM 格式

PE 是 LVM 的最小存储区块。

对硬盘进行分区

先切换到 root 用户

1
sudo su -

查看系统已经识别的硬盘

1
2
3
4
5
6
7
8
9
10
11
12
13
$ fdisk -l

# 显示主要信息,会发现有两块硬盘,sdb 则是我们刚刚购置的硬盘
Disk /dev/sda: 10.7 GB, 10737418240 bytes
Disk /dev/sdb: 75.2 GB, 75161927680 bytes

$ fdisk /dev/sdb      # 对硬盘进行操作

$  p(显示分区情况)
   n(新建分区)
   e(创建扩展分区)
   n -> l(创建逻辑分区)
   t(设置磁盘Hex code)——>#8e(LinuxLVM)——>#w(保存操作)
1
2
3
4
5
6
7
8
$ fdisk /dev/sdb
$ p

会发现:

  Device Boot      Start     End      Blocks      Id     System
    /dev/sdb1          1       2080     1048288+     5     Extended
    /dev/sdb5          1       2080     1048257      8e    Linux LVM

这里的逻辑分区 sdb5 已经成功的成为了 Linux LVM

1
$ partprobe (  kernel 重新读取磁盘分区表,即刻生效)

安装 LVM

1
apt-get install lvm2

创建 PV

1
2
3
4
5
6
$ pvcreate /dev/sdb5      # 将此分区转换成为 PV

$ pvs    # 显示所有 PV 情况

  PV         VG   Fmt  Attr PSize  PFree
  /dev/sdb5       lvm2 a-   70.00g 70.00g

此时发现还没有一个VG

创建 VG

1
2
3
4
5
6
7
8
9
10
11
12
13
$ vgcreate vg_data /dev/sdb5     # 这里的 vg_data 是自定义的

$ vgs   # 显示所有 VG 

  VG      #PV #LV #SN Attr   VSize  VFree
  vg_data   1   0   0 wz--n- 70.00g 70.00g

$ pvs  # 显示所有 PV

  PV         VG      Fmt  Attr PSize  PFree
  /dev/sdb5  vg_data lvm2 a-   70.00g 70.00g

  # 会发现 PV 下有个 VG 了  

创建 LV

1
2
3
4
5
6
$ lvcreate -n lv_data -L 69.5G vg_data  # 另外的 0.5 G 需要用于其他用途

$ lvs  # 显示所有 LV

LV      VG      Attr   LSize  Origin Snap%  Move Log Copy%  Convert
  lv_data vg_data -wi-a- 69.50g

格式化分区

1
2
3
$ mkfs.ext4 /dev/vg_data/lv_data
$ fdisk -l
$ mount /dev/vg_data/lv_data /mnt/   # 挂到 mnt 下

以后如何动态扩容?

  1. 用 fdisk 設定新的具有 8e system ID 的 partition
  2. 利用 pvcreate 建置 PV
  3. 利用 vgextend 將 PV 加入我們的 vg_data
  4. 用 lvresize 將新加入的 PV 內的 PE 加入 lv_data 中
  5. 透過 resize2fs 將檔案系統的容量確實增加!

实际操作

1
2
3
4
5
6
$ fdisk /dev/sdb10   => 其他动作参考上面
$ partprobe
$ pvcreate /dev/sdb10
$ vgextend vg_data /dev/sdb10
$ vgdisplay # 显示 VG 的具体信息,会看到剩余的 PE ,假设为 90个
$ lvresize -l +90 /dev/vg_data/lv_data    # 放大 LV

这时 LVM 扩容了,但是档案系统还显示原先的大小

1
2
3
resize2fs /dev/vg_data/lv_data  # 完整将 LV 容量扩充到整个 filesystem

# df /mnt/lvm   # 查看大小

机子重启后自动挂载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$blkid /dev/vg_data/lv_data  # 查看该设备的 UUID
$ vim /etc/fstab   # 编辑自动挂载的名单

=UUID=1eb099f3-5332-43d7-a98d-6c1238572cd5 /home/deploy/red_mansions        ext4    defaults 0   0

$ df -TH  # 查看下所有设备

Filesystem     Type      Size  Used Avail Use% Mounted on
/dev/sda1      ext4       11G  2.7G  7.5G  27% /
udev           devtmpfs  2.0G  8.2k  2.0G   1% /dev
tmpfs          tmpfs     389M  242k  388M   1% /run
none           tmpfs     5.3M     0  5.3M   0% /run/lock
none           tmpfs     2.0G     0  2.0G   0% /run/shm

$  mount -a # 按照 fstab 文件重新挂载一遍
$  df -TH

Filesystem                  Type      Size  Used Avail Use% Mounted on
/dev/sda1                   ext4       11G  2.7G  7.5G  27% /
udev                        devtmpfs  2.0G  8.2k  2.0G   1% /dev
tmpfs                       tmpfs     389M  242k  388M   1% /run
none                        tmpfs     5.3M     0  5.3M   0% /run/lock
none                        tmpfs     2.0G     0  2.0G   0% /run/shm
/dev/mapper/vg_data-lv_data ext4       74G  7.5G   63G  11% /home/deploy/red_mansions

# 会发现设备已经挂载上去了。

给 设备 赋予授权

1
$ chown -R deploy:deploy /home/deploy/  # 因为默认属于 root 权限

PS

如果即将要 LV 化的硬盘原先有数据怎么办? 需要先将数据移走,LV 化后再移进来。

参考

http://linux.vbird.org/linux_basic/0420quota.php#lvm http://www.ichiayi.com/wiki/tech/lvm http://allen7111382.blog.51cto.com/202304/268562