代理、VPN和内网穿透

[TOC]

代理

代理的意思就是代替你处理。在这里指网络服务相关的代理,做代理的服务器就叫代理服务器。有一些东西你自己无法直接获取,而代理服务器能做,那这些事情就可以交给代理服务器去做。这就是代理服务的价值所在。比如公司限制内部电脑不能上网,需要上网的员工可以申请连接到代理服务器上,这样员工的电脑就可以上网了。于是公司达到了网络管控的效果。

代理服务一般分为两种:正向代理和反向代理。唯一的差别在于代理服务器是作为客户端使用,还是作为服务端使用。

正向代理

正向代理中代理服务作为客户端,代替真实的客户端去访问服务。就比如前面提到的员工利用代理服务访问网络的例子。

正向代理

反向代理

反向代理中,代理服务代替真实的服务器来迎接客户端的请求,然后将请求转发给真实的服务器。这样可以很好的隐藏真实的服务器地址,只需要让客户端知道代理服务器地址即可。在这里代理服务器可以做很多过滤和限制从而达到更好的保护好服务器正常的提供服务。

反向代理

VPN

内网穿透

Go语言的线程

[TOC]

线程与锁

现代CPU一般含有多个核,并且一个核可能支持多线程。换句话说,现代CPU可以同时执行多条指令流水线。 为了将CPU的能力发挥到极致,我们常常需要使我们的程序支持并发(concurrent)计算。并发计算是指若干计算可能在某些时间片段内同时运行的情形。 在并行计算中,多个计算在任何时间点都在同时运行。并行计算属于特殊的并发计算。

而并发编程会存在数据竞争(data race)的情况,在不同线程同时修改统一内存控制时。并发编程的一大任务就是要调度不同计算,控制它们对资源的访问时段,以使数据竞争的情况不会发生。 此任务常称为并发同步(或者数据同步)。

锁是解决数据竞争的一种方法,在go语言中提供了sync包。

1
2
3
4
5
sync.Mutex: 互斥锁
sync.RWMutex: 读写分离锁
sync.WaitGroup: 等待一组goroutine 返回
sync.Once: 保证某段代码只执行一次
sync.Cond: 让一组goroutine 在满足特定条件时被唤醒

sync.Mutex

Lock()加锁,Unlock()解锁

1
2
3
4
5
6
7
8
9
10
11
12
13
var m sync.Mutex

func f1() {
m.Lock()
defer m.Unlock()
doSomething()
}

func f2() {
m.Lock()
doSomething()
m.Unlock()
}

sync.RWMutex

简单来说:不限制并发读,只限制并发写和并发读写

详细来说:

1
2
3
4
5
6
7
8
9
10
11
12
一个RWMutex值常称为一个读写互斥锁,它的内部包含两个锁:一个写锁和一个读锁。 对于一个可寻址的RWMutex值rwm,数据写入者可以通过方法调用rwm.Lock()对rwm加写锁,或者通过rwm.RLock()方法调用对rwm加读锁。 方法调用rwm.Unlock()和rwm.RUnlock()用来解开rwm的写锁和读锁。 rwm的读锁维护着一个计数。当rwm.RLock()调用成功时,此计数增1;当rwm.Unlock()调用成功时,此计数减1; 一个零计数表示rwm的读锁处于未加锁状态;反之,一个非零计数(肯定大于零)表示rwm的读锁处于加锁状态。

对于一个可寻址的RWMutex值rwm,下列规则存在:
rwm的写锁只有在它的写锁和读锁都处于未加锁状态时才能被成功加锁。 换句话说,rwm的写锁在任何时刻最多只能被一个数据写入者成功加锁,并且rwm的写锁和读锁不能同时处于加锁状态。
当rwm的写锁正处于加锁状态的时候,任何新的对之加写锁或者加读锁的操作试图都将导致当前协程进入阻塞状态,直到此写锁被解锁,这样的操作试图才有机会成功。
当rwm的读锁正处于加锁状态的时候,新的加写锁的操作试图将导致当前协程进入阻塞状态。 但是,一个新的加读锁的操作试图将成功,只要此操作试图发生在任何被阻塞的加写锁的操作试图之前(见下一条规则)。 换句话说,一个读写互斥锁的读锁可以同时被多个数据读取者同时加锁而持有。 当rwm的读锁维护的计数清零时,读锁将返回未加锁状态。
假设rwm的读锁正处于加锁状态的时候,为了防止后续数据写入者没有机会成功加写锁,后续发生在某个被阻塞的加写锁操作试图之后的所有加读锁的试图都将被阻塞。
假设rwm的写锁正处于加锁状态的时候,(至少对于标准编译器来说,)为了防止后续数据读取者没有机会成功加读锁,发生在此写锁下一次被解锁之前的所有加读锁的试图都将在此写锁下一次被解锁之后肯定取得成功,即使所有这些加读锁的试图发生在一些仍被阻塞的加写锁的试图之后。
后两条规则是为了确保数据读取者和写入者都有机会执行它们的操作。

请注意:一个锁并不会绑定到一个协程上,即一个锁并不记录哪个协程成功地加锁了它。

example

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
package main

import (
"fmt"
"runtime"
"sync"
)

type Counter struct {
m sync.RWMutex
n uint64
}

func (c *Counter) Value() uint64 {
c.m.RLock()
defer c.m.RUnlock()
return c.n
}

func (c *Counter) Increase(delta uint64) {
c.m.Lock()
c.n += delta
c.m.Unlock()
}

func main() {
var c Counter
for i := 0; i < 100; i++ {
go func() {
for k := 0; k < 100; k++ {
c.Increase(1)
}
}()
}


for c.Value() < 10000 {
runtime.Gosched()
}
fmt.Println(c.Value()) // 10000
}

sync.WatiGroup

每个sync.WaitGroup值在内部维护着一个计数,此计数的初始默认值为零。

*sync.WaitGroup类型有三个方法:Add(delta int)、Done()和Wait()。

对于一个可寻址的sync.WaitGroup值wg:

  • 我们可以使用方法调用wg.Add(delta)来改变值wg维护的计数。
  • 方法调用wg.Done()和wg.Add(-1)是完全等价的。
  • 如果一个wg.Add(delta)或者wg.Done()调用将wg维护的计数更改成一个负数,一个恐慌将产生。
  • 当一个协程调用了wg.Wait()时,
    • 如果此时wg维护的计数为零,则此wg.Wait()此操作为一个空操作(no-op);
    • 否则(计数为一个正整数),此协程将进入阻塞状态。 当以后其它某个协程将此计数更改至0时(一般通过调用wg.Done()),此协程将重新进入运行状态(即wg.Wait()将返回)。

一般,一个sync.WaitGroup值用来让某个协程等待其它若干协程都先完成它们各自的任务。 一个例子:

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
package main

import (
"fmt"
"math/rand"
"sync"
"time"
)

func main() {
rand.Seed(time.Now().UnixNano())

const N = 5
var values [N]int32

var wg sync.WaitGroup
wg.Add(N)
for i := 0; i < N; i++ {
i := i
go func() {
values[i] = 50 + rand.Int31n(50)
fmt.Println("Done:", i)
wg.Done() // <=> wg.Add(-1)
}()
}

wg.Wait()
// 所有的元素都保证被初始化了。
fmt.Println("values:", values)
}

sync.Once

对一个可寻址的sync.Once值o,o.Do()方法调用可以在多个协程中被多次并发地执行,但有且只有一个调用的实参函数(值)将得到调用。 此被调用的实参函数保证在任何o.Do()方法调用返回之前退出。

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
package main

import (
"log"
"sync"
)

func main() {
log.SetFlags(0)

x := 0
doSomething := func() {
x++
log.Println("Hello")
}

var wg sync.WaitGroup
var once sync.Once
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
once.Do(doSomething)
log.Println("world!")
}()
}

wg.Wait()
log.Println("x =", x) // x = 1
}

在此例中,Hello将仅被输出一次,而world!将被输出5次,并且Hello肯定在所有的5个world!之前输出。

sync.Cond

sync.Cond类型提供了一种有效的方式来实现多个协程间的通知。
每个sync.Cond值拥有一个sync.Locker类型的名为L的字段。 此字段的具体值常常为一个sync.Mutex值或者sync.RWMutex值。
*sync.Cond类型有三个方法:Wait()、Signal()和Broadcast()。

每个Cond值维护着一个先进先出等待协程队列。 对于一个可寻址的Cond值c,

  • c.Wait()必须在c.L字段值的锁处于加锁状态的时候调用;否则,c.Wait()调用将造成一个恐慌。 一个c.Wait()调用将
    • 首先将当前协程推入到c所维护的等待协程队列;
    • 然后调用c.L.Unlock()对c.L的锁解锁;
    • 然后使当前协程进入阻塞状态;(当前协程将被另一个协程通过c.Signal()或c.Broadcast()调用唤醒而重新进入运行状态。)一旦当前协程重新进入运行状态,c.L.Lock()将被调用以试图重新对c.L字段值的锁加锁。 此c.Wait()调用将在此试图成功之后退出。
  • 一个c.Signal()调用将唤醒并移除c所维护的等待协程队列中的第一个协程(如果此队列不为空的话)。
  • 一个c.Broadcast()调用将唤醒并移除c所维护的等待协程队列中的所有协程(如果此队列不为空的话)。

c.Signal()c.Broadcast()调用常用来通知某个条件的状态发生了变化。 一般说来,c.Wait()应该在一个检查某个条件是否已经得到满足的循环中调用。

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
package main

import (
"fmt"
"math/rand"
"sync"
"time"
)

func main() {
rand.Seed(time.Now().UnixNano())

const N = 10
var values [N]string

cond := sync.NewCond(&sync.Mutex{})

for i := 0; i < N; i++ {
d := time.Second * time.Duration(rand.Intn(10)) / 10
go func(i int) {
time.Sleep(d) // 模拟一个工作负载
cond.L.Lock()
// 下面的修改必须在cond.L被锁定的时候执行
values[i] = string('a' + i)
cond.Broadcast() // 可以在cond.L被解锁后发出通知
cond.L.Unlock()
// 上面的通知也可以在cond.L未锁定的时候发出。
//cond.Broadcast() // 上面的调用也可以放在这里
}(i)
}

// 此函数必须在cond.L被锁定的时候调用。
checkCondition := func() bool {
fmt.Println(values)
for i := 0; i < N; i++ {
if values[i] == "" {
return false
}
}
return true
}

cond.L.Lock()
defer cond.L.Unlock()
for !checkCondition() {
cond.Wait() // 必须在cond.L被锁定的时候调用
}
}

参考:

Go基础使用

[TOC]

go控制语句

if

  • 基本形式
1
2
3
4
5
6
7
8
// 大括号的左边必须跟在语句的后面
if condition1 {
// do something
} else if condition2 {
// do something
} else {
// catch-all or default
}
  • 简短语句
1
2
3
4
// 处理完分号“;”前面的语句之后,再做判断
if v:=x-100; v<0 {
// do something
}

switch

  • fallthrough: 划过执行下一个case
1
2
3
4
5
6
7
8
9
10
switch var1 {
case value1: // do sth
case value2:
fallthrough // do next case
case value3:
// so sth
default:
...
}

for

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// eg 1
for i:=0; i<10; i++ {
sum += i
}

// eg 2
for ; sum < 1000; {
sum += sum
}

// always loop
for {
// do something

if condition {
break
}
}

for range

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// string
for index,char := range myString {
// do something
}

// map
for k,v := range myMap {
// do
}

// array or slice
for index,value := range myArray {
// do
}

数据结构

variable

var varName type

1
2
3
4
5
6
var myName string
var myAge int
var isBoy bool

var Name string = superman

constants

const constName type

array and slice

var arrName []type

1
2
3
4
5
6

var studentsName []string

myArray := [5]int{1,2,3,4,5}
mySlice := myArray[2:4]

make and new

  • new 返回指针地址
  • make返回第一个元素
    • make([]type,[length],[cap])
1
2
3
4
mySlice1 := new([]int)
mySlice2 := make([]int,0)
mySlice2 := make([]int,0,10)
mySlice2 := make([]int,0,10,20)

map

var mapName map[keyType]valueType

1
2
myMap := make(map[string]string, 10)
myMap["a"] = "b"

struct

1
2
3
4
5
6
7
type MyType struct{
Name string
}

func printMyType(t *MyType){
println(t.Name)
}

函数

main

每个go语言程序都应该有一个main package,里面的main函数就是go语言程序的入口。

1
2
3
4
5
package main

func main(){
print("hello world.")
}

init

  • init 函数会在包初始化时运行。类似python里面class中的init函数

return

  • return: 函数可以返回任意数量的返回值,忽略部分用“_”即可,同python

args

函数支持可变长度参数

1
2
3
4
5
6
7
8
func append(slice []Type, elems ...Type) []Type {
// do sth
}

// use it
myArray := []string{}
myArray = append(myArray, "a", "B", "c", "d")

内置函数

  • close
  • len
  • cap: 计算array, slice, map的容量
  • copy, append
  • panic, recover
  • print, println
  • complex, real, imag: 操作复数

方法

上面的例子中已经有示例了

1
2
3
func(recv receiver_type) methodName(parameter_list) (return_value_list) {
// do sth
}

接口

go语言比较新颖的interface

1
2
3
type interfaceName interface{
Method1(param_list) return_type
}
  • struct 无需显示声明interface,只需直接实现方法
  • struct除实现interface外,还可以有额外的方法
  • 一个类型可以实现多个接口
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
package main

import "fmt"

type IF interface{
getName() string
}

type Human struct{
firstName, lastName string
}

func(h *Human) getName() string{
return h.firstName + "," + h.lastName
}

type Car struct{
factory, model string
}

func(c *Car) getName() string{
return c.factory + "," + d.model
}

func main(){
interface := []IF{}

h:=new(Human)
h.firstName = "first"
h.lastName = "last"
interfaces = append(interfaces, h)

c := new(Car)
c.factory = "benz"
c.model = "s"
interfaces = append(interfaces, c)

for _,f := range interfaces{
fmt.Println(f.getName())
}
}

其他

reflect

  • reflect.TypeOf() 返回被检查对象的类型
  • reflect.ValueOf() 返回被检查对象的值
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
package main

import (
"fmt"
"reflect"
)

func main() {
// basic type
myMap := make(map[string]string, 10)
myMap["a"] = "b"
t := reflect.TypeOf(myMap)
fmt.Println("type:", t)
v := reflect.ValueOf(myMap)
fmt.Println("value:", v)
// struct
myStruct := T{A: "a"}
v1 := reflect.ValueOf(myStruct)
for i := 0; i < v1.NumField(); i++ {
fmt.Printf("Field %d: %v\n", i, v1.Field(i))
}
for i := 0; i < v1.NumMethod(); i++ {
fmt.Printf("Method %d: %v\n", i, v1.Method(i))
}
// 需要注意receive是struct还是指针
result := v1.Method(0).Call(nil)
fmt.Println("result:", result)
}

type T struct {
A string
}

// 需要注意receive是struct还是指针
func (t T) String() string {
return t.A + "1"
}

json编解码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// from string to struct
func unmarshal2Struct(humanStr string) Human{
h := Human{}
err := json.Unmarshal([]byte(humanStr), &h)
if err != nil{
println(err)
}
return h
}

// from struct to string
func marshal2JsonString(h Human) string{
h.Age = 30
updatedBytes, err := json.Marshal(&h)
if err != nil{
println(err)
}
return string(updatedBytes)
}

defer

函数退出前执行某个语句

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
package main

import (
"fmt"
"sync"
"time"
)

func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
loopFunc()
time.Sleep(time.Second)
}

func loopFunc() {
lock := sync.Mutex{}
for i := 0; i < 3; i++ {
go func(i int) {
lock.Lock()
defer lock.Unlock()
fmt.Println("loopFunc:", i)
}(i)
}
}

output:

1
2
3
4
5
6
7
8
# ./main
loopFunc: 2
loopFunc: 0
loopFunc: 1
3
2
1

panic and recover

  • panic: 在系统出现不可恢复错误时主动调用panic,当前线程直接crash
  • defer: 保证defer的代码执行,而后并把控制权交还给接收到panic的函数调用者
  • recover: 函数从panic或者错误场景中恢复

通过再 defer的代码中执行recover,使得panic的内容得到了处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
)

func main() {
defer func() {
fmt.Println("defer func is called")
if err := recover(); err != nil {
fmt.Println(err)
}
}()
panic("a panic is triggered")

}

Go工具链

[TOC]

Go

为了从任意目录运行Go,安装目录下的bin子目录路径必须配置在PATH环境变量中。

1、Go环境变量

  • GOPATH: 此环境变量的默认值为当前用户的HOME目录下的名为go文件夹对应的目录路径。 GOPATH文件夹中的pkg子文件夹用来缓存被本地项目所依赖的Go模块(一个Go模块为若干Go库包的集合)的版本。src子文件夹用来存放源码。
  • GOBIN: GOBIN环境变量用来指定go install子命令产生的Go应用程序二进制可执行文件应该存储在何处。 它的默认值为GOPATH文件夹中的bin子目录所对应的目录路径。

2、Go子命令

go run

编译和运行main包中的go程序。比如有一个 example.go 的文件,只需要执行 go run example.go 即可。

如果程序的main包中有多个go源码文件,我们可以指定目录。例如:

1
2
3
$ ls
a.go b.go
$ go run ./

go install

编译和安装main包中的go程序,并不会执行,而是将可执行文件放入GOBIN指定的目录中。

我们可以运行go install example.com/program@latest来安装一个第三方Go程序的最新版本(至GOBIIN目录)。

在1.16版本之前,可以是用go get -u example.com/program 来安装。

go build

编译main包中的go程序,并不会安装,也不会执行。

go vet

go vet子命令可以用来检查可能的代码逻辑错误(即警告)。

go fmt

go fmt子命令来用同一种代码风格格式化Go代码。

go test

go test子命令来运行单元和基准测试用例。

go doc

go doc子命令用来(在终端中)查看Go代码库包的文档。

go mod init

go mod init 命令可以用来在当前目录中生成一个go.mod文件。此go.mod文件将被用来记录当前项目需要的依赖模块和版本信息。

go mod tidy

go mod tidy命令用来通过扫描当前项目中的所有代码来添加未被记录的依赖至go.mod文件或从go.mod文件中删除不再被使用的依赖。

go mod vendor

将依赖包都保存到当前项目的的vendor中。这样做有一个好处,保持不同go项目对依赖库版本的准确。比如出现A项目依赖mod中C的1.1版本,而B项目依赖mod中C的2.1版本,使用vendor即可很好的解决该问题。这种方式很类似Python中的venv或者conda中的不同env

go get

安装第三方go程序包

在1.16之后使用 go install

go help aSubCommand

查看帮助

Go语言简洁

[TOC]

Go语言简介

Go是一门编译型和静态型的编程语言。

1、Go语言卖点

1、做为一门静态语言,Go却和很多动态脚本语言一样得灵活

2、节省内存、程序启动快和代码执行速度快

3、内置并发编程支持

4、良好的代码可读性,Go的语法很简洁并且和其它流行语言相似。

5、良好的跨平台支持

6、一个稳定的Go核心设计和开发团队以及一个活跃的社区

7、Go拥有一个比较齐全的标准库

和C家族语言相比有以下优点:

  • 程序编译时间短
  • 像动态语言一样灵活
  • 内置并发支持

2、Go语言特性

  • 内置并发编程支持:
    • 使用协程(goroutine)做为基本的计算单元。轻松地创建协程。
    • 使用通道(channel)来实现协程间的同步和通信。
  • 内置了映射(map)和切片(slice)类型。
  • 支持多态(polymorphism)。
  • 使用接口(interface)来实现裝盒(value boxing)和反射(reflection)。
  • 支持指针。
  • 支持函数闭包(closure)。
  • 支持方法。
  • 支持延迟函数调用(defer)。
  • 支持类型内嵌(type embedding)。
  • 支持类型推断(type deduction or type inference)。
  • 内存安全。
  • 自动垃圾回收。
  • 良好的代码跨平台性。

3、开源Go项目

图片来源于极客时间GitHub_go_projects

Ceph学习笔记-03-luminous版本部署

[TOC]

1、配置ceph.repo并安装批量管理工具ceph-deploy

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
[root@ceph-node1 ~]# vim /etc/yum.repos.d/ceph.repo 
[ceph]
name=Ceph packages for $basearch
baseurl=http://mirrors.aliyun.com/ceph/rpm-luminous/el7/$basearch
enabled=1
gpgcheck=1
priority=1
type=rpm-md
gpgkey=https://mirrors.aliyun.com/ceph/keys/release.asc

[ceph-noarch]
name=Ceph noarch packages
baseurl=http://mirrors.aliyun.com/ceph/rpm-luminous/el7/noarch
enabled=1
gpgcheck=1
priority=1
type=rpm-md
gpgkey=https://mirrors.aliyun.com/ceph/keys/release.asc

[ceph-source]
name=Ceph source packages
baseurl=http://mirrors.aliyun.com/ceph/rpm-luminous/el7/SRPMS
enabled=0
gpgcheck=1
type=rpm-md
gpgkey=https://mirrors.aliyun.com/ceph/keys/release.asc
priority=1
[root@ceph-node1 ~]# yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
[root@ceph-node1 ~]# yum makecache
[root@ceph-node1 ~]# yum update -y
[root@ceph-node1 ~]# yum install -y ceph-deploy
​```

2、ceph的节点部署

(1)安装NTP 在所有 Ceph 节点上安装 NTP 服务(特别是 Ceph Monitor 节点),以免因时钟漂移导致故障

1
2
3
4
5
6
7
8
9
10
[root@ceph-node1 ~]# yum install -y ntp ntpdate ntp-doc
[root@ceph-node2 ~]# yum install -y ntp ntpdate ntp-doc
[root@ceph-node3 ~]# yum install -y ntp ntpdate ntp-doc

[root@ceph-node1 ~]# ntpdate ntp1.aliyun.com
31 Jul 03:43:04 ntpdate[973]: adjust time server 120.25.115.20 offset 0.001528 sec
[root@ceph-node1 ~]# hwclock
Tue 31 Jul 2018 03:44:55 AM EDT -0.302897 seconds
[root@ceph-node1 ~]# crontab -e
*/5 * * * * /usr/sbin/ntpdate ntp1.aliyun.com

确保在各 Ceph 节点上启动了 NTP 服务,并且要使用同一个 NTP 服务器

(2)安装SSH服务器并添加hosts解析

1
2
3
4
5
6
7
8
9
10
11
12
13
默认有ssh,可以省略
[root@ceph-node1 ~]# yum install openssh-server
[root@ceph-node2 ~]# yum install openssh-server
[root@ceph-node3 ~]# yum install openssh-server

确保所有 Ceph 节点上的 SSH 服务器都在运行。

[root@ceph-node1 ~]# cat /etc/hosts
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.56.11 ceph-node1
192.168.56.12 ceph-node2
192.168.56.13 ceph-node3

(3)允许无密码SSH登录

1
2
3
4
root@ceph-node1 ~]# ssh-keygen
root@ceph-node1 ~]# ssh-copy-id root@ceph-node1
root@ceph-node1 ~]# ssh-copy-id root@ceph-node2
root@ceph-node1 ~]# ssh-copy-id root@ceph-node3

推荐使用方式:
修改 ceph-deploy 管理节点上的 ~/.ssh/config 文件,这样 ceph-deploy 就能用你所建的用户名登录 Ceph 节点了,而无需每次执行 ceph-deploy 都要指定 –username {username} 。这样做同时也简化了 ssh 和 scp 的用法。把 {username} 替换成你创建的用户名。

1
2
3
4
5
6
7
8
9
10
11
12
[root@ceph-node1 ~]# cat .ssh/config 
Host node1
Hostname ceph-node1
User root
Host node2
Hostname ceph-node2
User root
Host node3
Hostname ceph-node3
User root
[root@ceph-node1 ~]# chmod 600 .ssh/config
[root@ceph-node1 ~]# systemctl restart sshd

(4)关闭Selinux

在 CentOS 和 RHEL 上, SELinux 默认为 Enforcing 开启状态。为简化安装,我们建议把 SELinux 设置为 Permissive 或者完全禁用,也就是在加固系统配置前先确保集群的安装、配置没问题。用下列命令把 SELinux 设置为 Permissive :

1
2
3
[root@ceph-node1 ~]# setenforce 0
[root@ceph-node2 ~]# setenforce 0
[root@ceph-node3 ~]# setenforce 0

要使 SELinux 配置永久生效(如果它的确是问题根源),需修改其配置文件 /etc/selinux/config 。

(5)关闭防火墙

1
2
3
4
5
6
[root@ceph-node1 ~]# systemctl stop firewalld.service
[root@ceph-node2 ~]# systemctl stop firewalld.service
[root@ceph-node3 ~]# systemctl stop firewalld.service
[root@ceph-node1 ~]# systemctl disable firewalld.service
[root@ceph-node2 ~]# systemctl disable firewalld.service
[root@ceph-node3 ~]# systemctl disable firewalld.service

(6)安装epel源和启用优先级

1
2
3
4
5
6
[root@ceph-node1 ~]# yum install -y epel-release
[root@ceph-node2 ~]# yum install -y epel-release
[root@ceph-node3 ~]# yum install -y epel-release
[root@ceph-node1 ~]# yum install -y yum-plugin-priorities
[root@ceph-node2 ~]# yum install -y yum-plugin-priorities
[root@ceph-node3 ~]# yum install -y yum-plugin-priorities

3、创建集群

创建一个 Ceph 存储集群,它有一个 Monitor 和两个 OSD 守护进程。一旦集群达到 active + clean 状态,再扩展它:增加第三个 OSD 、增加元数据服务器和两个 Ceph Monitors。在管理节点上创建一个目录,用于保存 ceph-deploy 生成的配置文件和密钥对。

(1)创建ceph工作目录并配置ceph.conf

1
2
[root@ceph-node1 ~]# mkdir /etc/ceph && cd /etc/ceph
[root@ceph-node1 ceph]# ceph-deploy new ceph-node1 #配置监控节点

ceph-deploynew子命令能够部署一个默认名称为ceph的新集群,并且它能生成集群配置文件和密钥文件。列出当前工作目录,你会看到ceph.confceph.mon.keyring文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@ceph-node1 ceph]# vim ceph.conf
public network =192.168.56.0/24
[root@ceph-node1 ceph]# ll
total 20
-rw-r--r-- 1 root root 253 Jul 31 21:36 ceph.conf   #ceph的配置文件
-rw-r--r-- 1 root root 12261 Jul 31 21:36 ceph-deploy-ceph.log  #monitor的日志文件
-rw------- 1 root root 73 Jul 31 21:36 ceph.mon.keyring  #monitor的密钥环文件

遇到的问题:
[root@ceph-node1 ceph]# ceph-deploy new ceph-node1
Traceback (most recent call last):
File "/usr/bin/ceph-deploy", line 18, in <module>
from ceph_deploy.cli import main
File "/usr/lib/python2.7/site-packages/ceph_deploy/cli.py", line 1, in <module>
import pkg_resources
ImportError: No module named pkg_resources
解决方案:
[root@ceph-node1 ceph]# yum install -y python-setuptools

(2)管理节点和osd节点都需要安装ceph 集群

1
[root@ceph-node1 ceph]# ceph-deploy install ceph-node1 ceph-node2 ceph-node3

ceph-deploy工具包首先会安装Ceph luminous版本所有依赖包。命令成功完成后,检查所有节点上Ceph的版本和健康状态,如下所示:

1
2
3
4
5
6
[root@ceph-node1 ceph]# ceph --version
ceph version 12.2.7 (3ec878d1e53e1aeb47a9f619c49d9e7c0aa384d5) luminous (stable)
[root@ceph-node2 ~]# ceph --version
ceph version 12.2.7 (3ec878d1e53e1aeb47a9f619c49d9e7c0aa384d5) luminous (stable)
[root@ceph-node3 ~]# ceph --version
ceph version 12.2.7 (3ec878d1e53e1aeb47a9f619c49d9e7c0aa384d5) luminous (stable)

(3)配置MON初始化

ceph-node1上创建第一个Ceph monitor

1
2
3
4
5
6
7
8
[root@ceph-node1 ceph]# ceph-deploy mon create-initial        #配置初始 monitor(s)、并收集所有密钥
[root@ceph-node1 ceph]# ll #完成上述操作后,当前目录里应该会出现这些密钥环
total 92
-rw------- 1 root root 113 Jul 31 21:48 ceph.bootstrap-mds.keyring
-rw------- 1 root root 113 Jul 31 21:48 ceph.bootstrap-mgr.keyring
-rw------- 1 root root 113 Jul 31 21:48 ceph.bootstrap-osd.keyring
-rw------- 1 root root 113 Jul 31 21:48 ceph.bootstrap-rgw.keyring
-rw------- 1 root root 151 Jul 31 21:48 ceph.client.admin.keyring

注意:只有在安装 Hammer 或更高版时才会创建 bootstrap-rgw 密钥环。

注意:如果此步失败并输出类似于如下信息 “Unable to find /etc/ceph/ceph.client.admin.keyring”,请确认ceph.conf中为monitor指定的IPPublic IP,而不是 Private IP。查看集群的状态信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@ceph-node1 ceph]# ceph -s
cluster:
id: c6165f5b-ada0-4035-9bab-1916b28ec92a
health: HEALTH_OK

services:
mon: 1 daemons, quorum ceph-node1
mgr: no daemons active
osd: 0 osds: 0 up, 0 in

data:
pools: 0 pools, 0 pgs
objects: 0 objects, 0 bytes
usage: 0 kB used, 0 kB / 0 kB avail
pgs:

(5)开启监控模块

查看集群支持的模块

1
2
[root@ceph-node1 ceph]# ceph mgr dump   
[root@ceph-node1 ceph]# ceph mgr module enable dashboard #启用dashboard模块

/etc/ceph/ceph.conf中添加

1
2
[mgr]
mgr modules = dashboard

设置dashboardip和端口

1
2
3
4
5
6
[root@ceph-node1 ceph]# ceph config-key put mgr/dashboard/server_addr 192.168.56.11
set mgr/dashboard/server_addr
[root@ceph-node1 ceph]# ceph config-key put mgr/dashboard/server_port 7000
set mgr/dashboard/server_port
[root@ceph-node1 ceph]# netstat -tulnp |grep 7000
tcp6 0 0 :::7000 :::* LISTEN 13353/ceph-mgr

访问:http://192.168.56.11:7000/

img

(5)在ceph-node1上创建OSD

1
2
3
4
5
6
7
8
9
[root@ceph-node1 ceph]# ceph-deploy disk zap ceph-node1 /dev/sdb

[root@ceph-node1 ceph]# ceph-deploy disk list ceph-node1 #列出ceph-node1上所有的可用磁盘
......
[ceph-node1][INFO ] Running command: fdisk -l
[ceph-node1][INFO ] Disk /dev/sdb: 1073 MB, 1073741824 bytes, 2097152 sectors
[ceph-node1][INFO ] Disk /dev/sdc: 1073 MB, 1073741824 bytes, 2097152 sectors
[ceph-node1][INFO ] Disk /dev/sdd: 1073 MB, 1073741824 bytes, 2097152 sectors
[ceph-node1][INFO ] Disk /dev/sda: 21.5 GB, 21474836480 bytes, 41943040 sectors

从输出中,慎重选择若干磁盘来创建Ceph OSD(除操作系统分区以外),并将它们分别命名为sdb、sdc和sdd。disk zap子命令会删除现有分区表和磁盘内容。运行此命令之前,确保你选择了正确的磁盘名称:
osd create子命令首先会准备磁盘,即默认地先用xfs文件系统格式化磁盘,然后会激活磁盘的第一、二个分区,分别作为数据分区和日志分区:

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
[root@ceph-node1 ceph]# ceph-deploy mgr create node1  #部署管理器守护程序,仅仅使用此版本
[ceph_deploy.conf][DEBUG ] found configuration file at: /root/.cephdeploy.conf
[ceph_deploy.cli][INFO ] Invoked (2.0.1): /usr/bin/ceph-deploy mgr create node1
[ceph_deploy.cli][INFO ] ceph-deploy options:
[ceph_deploy.cli][INFO ] username : None
[ceph_deploy.cli][INFO ] verbose : False
[ceph_deploy.cli][INFO ] mgr : [('node1', 'node1')]
[ceph_deploy.cli][INFO ] overwrite_conf : False
[ceph_deploy.cli][INFO ] subcommand : create
[ceph_deploy.cli][INFO ] quiet : False
[ceph_deploy.cli][INFO ] cd_conf : <ceph_deploy.conf.cephdeploy.Conf instance at 0xe1e5a8>
[ceph_deploy.cli][INFO ] cluster : ceph
[ceph_deploy.cli][INFO ] func : <function mgr at 0xda5f50>
[ceph_deploy.cli][INFO ] ceph_conf : None
[ceph_deploy.cli][INFO ] default_release : False
[ceph_deploy.mgr][DEBUG ] Deploying mgr, cluster ceph hosts node1:node1
[node1][DEBUG ] connected to host: node1
[node1][DEBUG ] detect platform information from remote host
[node1][DEBUG ] detect machine type
[ceph_deploy.mgr][INFO ] Distro info: CentOS Linux 7.4.1708 Core
[ceph_deploy.mgr][DEBUG ] remote host will use systemd
[ceph_deploy.mgr][DEBUG ] deploying mgr bootstrap to node1
[node1][DEBUG ] write cluster configuration to /etc/ceph/{cluster}.conf
[node1][WARNIN] mgr keyring does not exist yet, creating one
[node1][DEBUG ] create a keyring file
[node1][DEBUG ] create path recursively if it doesn't exist
[node1][INFO ] Running command: ceph --cluster ceph --name client.bootstrap-mgr --keyring /var/lib/ceph/bootstrap-mgr/ceph.keyring auth get-or-create mgr.node1 mon allow profile mgr osd allow * mds allow * -o /var/lib/ceph/mgr/ceph-node1/keyring
[node1][INFO ] Running command: systemctl enable ceph-mgr@node1
[node1][WARNIN] Created symlink from /etc/systemd/system/ceph-mgr.target.wants/ceph-mgr@node1.service to /usr/lib/systemd/system/ceph-mgr@.service.
[node1][INFO ] Running command: systemctl start ceph-mgr@node1
[node1][INFO ] Running command: systemctl enable ceph.target

(5)创建OSD

添加三个OSD。出于这些说明的目的,我们假设您在每个节点中都有一个未使用的磁盘/dev/sdb。 确保设备当前未使用且不包含任何重要数据。
语法格式:ceph-deploy osd create --data {device} {ceph-node}

1
2
3
[root@ceph-node1 ceph]# ceph-deploy osd create --data /dev/sdb node1
[root@ceph-node1 ceph]# ceph-deploy osd create --data /dev/sdc node1
[root@ceph-node1 ceph]# ceph-deploy osd create --data /dev/sdd node1

Ceph学习笔记-02-工作原理及流程

[TOC]

一、RADOS的对象寻址

  Ceph 存储集群从 Ceph 客户端接收数据——不管是来自 Ceph 块设备Ceph 对象存储Ceph 文件系统、还是基于 librados 的自定义实现——并存储为对象。每个对象是文件系统中的一个文件,它们存储在对象存储设备上。由 Ceph OSD 守护进程处理存储设备上的读/写操作。 

  在传统架构里,客户端与一个中心化的组件通信(如网关、中间件、 API 、前端等等),它作为一个复杂子系统的唯一入口,它引入单故障点的同时,也限制了性能和伸缩性(就是说如果中心化组件挂了,整个系统就挂了)。

  Ceph 消除了集中网关,允许客户端直接和 Ceph OSD 守护进程通讯。 Ceph OSD 守护进程自动在其它 Ceph 节点上创建对象副本来确保数据安全和高可用性;为保证高可用性,监视器也实现了集群化。为消除中心节点, Ceph 使用了 CRUSH 算法。

img

File —— 此处的file就是用户需要存储或者访问的文件。当用户要将数据存储到Ceph集群时,存储数据都会被分割成多个object。

Ojbect —— 每个object都有一个object id,每个object的大小是可以设置的,默认是4MB,object可以看成是Ceph存储的最小存储单元。

PG(Placement Group)—— 顾名思义,PG的用途是对object的存储进行组织和位置映射。由于object的数量很多,所以Ceph引入了PG的概念用于管理object,每个object最后都会通过CRUSH计算映射到某个pg中,一个pg可以包含多个object。

OSD —— 即object storage device,PG也需要通过CRUSH计算映射到osd中去存储,如果是二副本的,则每个pg都会映射到二个osd,比如[osd.1,osd.2],那么osd.1是存放该pg的主副本,osd.2是存放该pg的从副本,保证了数据的冗余。

(1)File -> object映射

  这次映射的目的是,将用户要操作的file,映射为RADOS能够处理的object。其映射十分简单,本质上就是按照object的最大size对file进行切分,相当于RAID中的条带化过程。这种切分的好处有二:一是让大小不限的file变成最大size一致、可以被RADOS高效管理的object;二是让对单一file实施的串行处理变为对多个object实施的并行化处理。

  每一个切分后产生的object将获得唯一的oid,即object id。其产生方式也是线性映射,极其简单。图中,ino是待操作file的元数据,可以简单理解为该file的唯一id。ono则是由该file切分产生的某个object的序号。而oid就是将这个序号简单连缀在该file id之后得到的。举例而言,如果一个id为filename的file被切分成了三个object,则其object序号依次为0、1和2,而最终得到的oid就依次为filename0、filename1和filename2。

  这里隐含的问题是,ino的唯一性必须得到保证,否则后续映射无法正确进行。

(2)Object -> PG映射

  在file被映射为一个或多个object之后,就需要将每个object独立地映射到一个PG中去。这个映射过程也很简单,如图中所示,其计算公式是:

  hash(oid) & mask -> pgid

  由此可见,其计算由两步组成。首先是使用Ceph系统指定的一个静态哈希函数计算oid的哈希值,将oid映射成为一个近似均匀分布的伪随机值。然后,将这个伪随机值和mask按位相与,得到最终的PG序号(pgid)。根据RADOS的设计,给定PG的总数为m(m应该为2的整数幂),则mask的值为m-1。因此,哈希值计算和按位与操作的整体结果事实上是从所有m个PG中近似均匀地随机选择一个。基于这一机制,当有大量object和大量PG时,RADOS能够保证object和PG之间的近似均匀映射。又因为object是由file切分而来,大部分object的size相同,因而,这一映射最终保证了,各个PG中存储的object的总数据量近似均匀。

  从介绍不难看出,这里反复强调了“大量”。只有当object和PG的数量较多时,这种伪随机关系的近似均匀性才能成立,Ceph的数据存储均匀性才有保证。为保证“大量”的成立,一方面,object的最大size应该被合理配置,以使得同样数量的file能够被切分成更多的object;另一方面,Ceph也推荐PG总数应该为OSD总数的数百倍,以保证有足够数量的PG可供映射。

(3)PG -> OSD映射

  第三次映射就是将作为object的逻辑组织单元的PG映射到数据的实际存储单元OSD。如图所示,RADOS采用一个名为CRUSH的算法,将pgid代入其中,然后得到一组共n个OSD。这n个OSD即共同负责存储和维护一个PG中的所有object。前已述及,n的数值可以根据实际应用中对于可靠性的需求而配置,在生产环境下通常为3。具体到每个OSD,则由其上运行的OSD deamon负责执行映射到本地的object在本地文件系统中的存储、访问、元数据维护等操作。

  Ceph通过三次映射,完成了从file到object、PG和OSD整个映射过程。通观整个过程,可以看到,这里没有任何的全局性查表操作需求。

二、数据的操作流程

以file写入过程为例,对数据操作流程进行说明。

为简化说明,便于理解,此处进行若干假定。首先,假定待写入的file较小,无需切分,仅被映射为一个object。其次,假定系统中一个PG被映射到3个OSD上。

基于上述假定,则file写入流程可以被下图表示:

img

  如图所示,当某个client需要向Ceph集群写入一个file时,首先需要在本地完成上面叙述的寻址流程,将file变为一个object,然后找出存储该object的一组三个OSD。这三个OSD具有各自不同的序号,序号最靠前的那个OSD就是这一组中的Primary OSD,而后两个则依次是Secondary OSD和Tertiary OSD。

  找出三个OSD后,client将直接和Primary OSD通信,发起写入操作(步骤1)。Primary OSD收到请求后,分别向Secondary OSD和Tertiary OSD发起写入操作(步骤2、3)。当Secondary OSD和Tertiary OSD各自完成写入操作后,将分别向Primary OSD发送确认信息(步骤4、5)。当Primary OSD确信其他两个OSD的写入完成后,则自己也完成数据写入,并向client确认object写入操作完成(步骤6)。

  之所以采用这样的写入流程,本质上是为了保证写入过程中的可靠性,尽可能避免造成数据丢失。同时,由于client只需要向Primary OSD发送数据,因此,在Internet使用场景下的外网带宽和整体访问延迟又得到了一定程度的优化。

  当然,这种可靠性机制必然导致较长的延迟,特别是,如果等到所有的OSD都将数据写入磁盘后再向client发送确认信号,则整体延迟可能难以忍受。因此,Ceph可以分两次向client进行确认。当各个OSD都将数据写入内存缓冲区后,就先向client发送一次确认,此时client即可以向下执行。待各个OSD都将数据写入磁盘后,会向client发送一个最终确认信号,此时client可以根据需要删除本地数据。

  分析上述流程可以看出,在正常情况下,client可以独立完成OSD寻址操作,而不必依赖于其他系统模块。因此,大量的client可以同时和大量的OSD进行并行操作。同时,如果一个file被切分成多个object,这多个object也可被并行发送至多个OSD。

从OSD的角度来看,由于同一个OSD在不同的PG中的角色不同,因此,其工作压力也可以被尽可能均匀地分担,从而避免单个OSD变成性能瓶颈。

  如果需要读取数据,client只需完成同样的寻址过程,并直接和Primary OSD联系。

三、集群维护

  从上面的学习,我们知道由若干个monitor共同负责整个Ceph集群中所有OSD状态的发现与记录,并且共同形成cluster map的master版本,然后扩散至全体OSD以及client。OSD使用cluster map进行数据的维护,而client使用cluster map进行数据的寻址。

  在集群中,各个monitor的功能总体上是一样的,其相互间的关系可以被简单理解为主从备份关系。monitor并不主动轮询各个OSD的当前状态。正相反,OSD需要向monitor上报状态信息。常见的上报有两种情况:一是新的OSD被加入集群,二是某个OSD发现自身或者其他OSD发生异常。在收到这些上报信息后,monitor将更新cluster map信息并加以扩散。

  Cluster map的实际内容包括:

Montior Map: 包含集群的 fsid 、位置、名字、地址和端口,也包括当前版本、创建时间、最近修改时间。要查看监视器图,用 ceph mon dump 命令。

OSD Map: 包含集群 fsid 、创建时间、最近修改时间、存储池列表、副本数量、归置组数量、 OSD 列表及其状态(如 upin )。要查看OSD运行图,用 ceph osd dump 命令。

OSD状态的描述分为两个维度:up或者down(表明OSD是否正常工作),in或者out(表明OSD是否在至少一个PG中)。因此,对于任意一个OSD,共有四种可能的状态:

—— Up且in:说明该OSD正常运行,且已经承载至少一个PG的数据。这是一个OSD的标准工作状态;

—— Up且out:说明该OSD正常运行,但并未承载任何PG,其中也没有数据。一个新的OSD刚刚被加入Ceph集群后,便会处于这一状态。而一个出现故障的OSD被修复后,重新加入Ceph集群时,也是处于这一状态;

—— Down且in:说明该OSD发生异常,但仍然承载着至少一个PG,其中仍然存储着数据。这种状态下的OSD刚刚被发现存在异常,可能仍能恢复正常,也可能会彻底无法工作;

—— Down且out:说明该OSD已经彻底发生故障,且已经不再承载任何PG。

PG Map::包含归置组版本、其时间戳、最新的 OSD 运行图版本、占满率、以及各归置组详情,像归置组 ID 、 up set 、 acting set 、 PG 状态(如 active+clean ),和各存储池的数据使用情况统计。

CRUSH Map::包含存储设备列表、故障域树状结构(如设备、主机、机架、行、房间、等等)、和存储数据时如何利用此树状结构的规则。要查看 CRUSH 规则,执行 ceph osd getcrushmap -o {filename} 命令;然后用 crushtool -d {comp-crushmap-filename} -o {decomp-crushmap-filename} 反编译;然后就可以用 cat 或编辑器查看了。

MDS Map: 包含当前 MDS 图的版本、创建时间、最近修改时间,还包含了存储元数据的存储池、元数据服务器列表、还有哪些元数据服务器是 upin 的。要查看 MDS 图,执行 ceph mds dump

四、OSD增加流程

(1)一个新的OSD上线后,首先根据配置信息与monitor通信。Monitor将其加入cluster map,并设置为up且out状态,再将最新版本的cluster map发给这个新OSD。收到monitor发来的cluster map之后,这个新OSD计算出自己所承载的PG(为简化讨论,此处我们假定这个新的OSD开始只承载一个PG),以及和自己承载同一个PG的其他OSD。然后,新OSD将与这些OSD取得联系。如图:

img

(2)如果这个PG目前处于降级状态(即承载该PG的OSD个数少于正常值,如正常应该是3个,此时只有2个或1个。这种情况通常是OSD故障所致),则其他OSD将把这个PG内的所有对象和元数据复制给新OSD。数据复制完成后,新OSD被置为up且in状态。而cluster map内容也将据此更新。这事实上是一个自动化的failure recovery过程。当然,即便没有新的OSD加入,降级的PG也将计算出其他OSD实现failure recovery,如图:

img

(3)如果该PG目前一切正常,则这个新OSD将替换掉现有OSD中的一个(PG内将重新选出Primary OSD),并承担其数据。在数据复制完成后,新OSD被置为up且in状态,而被替换的OSD将退出该PG(但状态通常仍然为up且in,因为还要承载其他PG)。而cluster map内容也将据此更新。这事实上是一个自动化的数据re-balancing过程,如图:

img

(4)如果一个OSD发现和自己共同承载一个PG的另一个OSD无法联通,则会将这一情况上报monitor。此外,如果一个OSD deamon发现自身工作状态异常,也将把异常情况主动上报给monitor。在上述情况下,monitor将把出现问题的OSD的状态设为down且in。如果超过某一预订时间期限,该OSD仍然无法恢复正常,则其状态将被设置为down且out。反之,如果该OSD能够恢复正常,则其状态会恢复为up且in。在上述这些状态变化发生之后,monitor都将更新cluster map并进行扩散。这事实上是自动化的failure detection过程。

Ceph学习笔记-01-初识Ceph

[TOC]

官方中文文档:http://docs.ceph.org.cn/

一、元数据和元数据管理

(1)元数据

  在学习Ceph之前,需要了解元数据的概念。元数据又称为中介数据、中继数据,为描述数据的数据。主要描述数据属性的信息,用来支持如指示存储位置、历史数据、资源查找、文件记录等功能。通俗地说,就 是用于描述一个文件的特征的系统数据,比如访问权限、文件拥有者以及文件数据库的分布信息(inode)等等。在集群文件系统中,分布信息包括文件在磁盘上的位置以 及磁盘在集群中的位置。用户需要操作一个文件就必须首先得到它的元数据,才能定位到文件的位置并且得到文件的内容或相关属性。

  使用stat命令,可以显示文件的元数据

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
[root@ceph-node1 ~]# stat 1.txt 
File: ‘1.txt’
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: 802h/2050d Inode: 33889728 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Context: unconfined_u:object_r:admin_home_t:s0
Access: 2018-08-05 16:38:22.137566272 +0800
Modify: 2018-08-05 16:38:22.137566272 +0800
Change: 2018-08-05 16:38:22.137566272 +0800
Birth: -

File:文件名

Size:文件大小(单位:B)

Blocks:文件所占扇区个数,为8的倍数(通常的Linux扇区大小为512B,连续八个扇区组成一个block)

IO Block:每个数据块的大小(单位:B)

regular file:普通文件(此处显示文件的类型)

Inode:文件的Inode号

Links:硬链接次数

Access:权限

Uid:属主id/属主名

Gid:属组id/属组名

Access:最近访问时间

Modify:数据改动时间

Change:元数据改动时间

以上的参数均属于文件的元数据,元数据即用来描述数据的数据。

(2)元数据管理

  元数据的管理方式有2种方式:集中式管理和分布式管理。

  集中式管理是指在系统中有一个节点专门司职元数据管理,所有元数据都存储在该节点的存储设备上。所有客户端对文件的请求前,都要先对该元数据管理器请求元数据。

  分布式管理是指将元数据存放在系统的任意节点并且能动态的迁移。对元数据管理的职责也分布到各个不同的节点上。大多数集群文件系统都采用集中式的元数据管理。

  因为集中式管理实现简单,一致性维护容易,在一定的操作频繁内可以提供较为满意的性能。缺点是单一失效的问题,若该服务器失效,整个系统将无法正常 工作。而且,当对元数据的操作过于频繁时,集中的元数据管理会成为整个系统的性能瓶颈。

  分布式元数据管理的好处是解决了集中式管理的单一失效点问题,而且性能不会随着操作频繁而出现瓶颈。其缺点是,实现复杂,一致性维护复杂,对性能有一 定的影响。

二、什么是Ceph?

  Ceph是一种为优秀的性能、可靠性和可扩展性而设计的统一的、分布式的存储系统。Ceph 独一无二地用统一的系统提供了对象、块、和文件存储功能,它可靠性高、管理简便、并且是开源软件。 Ceph 的强大足以改变公司的 IT 基础架构、和管理海量数据的能力。Ceph 可提供极大的伸缩性——供成千用户访问 PB 乃至 EB 级的数据。 Ceph 节点以普通硬件和智能守护进程作为支撑点, Ceph 存储集群组织起了大量节点,它们之间靠相互通讯来复制数据、并动态地重分布数据。

三、Ceph的核心组件

Ceph的核心组件包括Ceph OSD、Ceph Monitor和Ceph MDS三大组件。

**Ceph OSD:**OSD的英文全称是Object Storage Device,它的主要功能是存储数据、复制数据、平衡数据、恢复数据等,与其它OSD间进行心跳检查等,并将一些变化情况上报给Ceph Monitor。一般情况下一块硬盘对应一个OSD,由OSD来对硬盘存储进行管理,当然一个分区也可以成为一个OSD。

**Ceph Monitor:**由该英文名字我们可以知道它是一个监视器,负责监视Ceph集群,维护Ceph集群的健康状态,同时维护着Ceph集群中的各种Map图,比如OSD Map、Monitor Map、PG Map和CRUSH Map,这些Map统称为Cluster Map,Cluster Map是RADOS的关键数据结构,管理集群中的所有成员、关系、属性等信息以及数据的分发,比如当用户需要存储数据到Ceph集群时,OSD需要先通过Monitor获取最新的Map图,然后根据Map图和object id等计算出数据最终存储的位置。

**Ceph MDS:**全称是Ceph MetaData Server,主要保存的文件系统服务的元数据,但对象存储和块存储设备是不需要使用该服务的。

查看各种Map的信息可以通过如下命令:ceph osd(mon、pg) dump

四、Ceph的架构

架构图:

img

Ceph系统逻辑层次结构:
自下向上,可以将Ceph系统分为四个层次:

  • (1)基础存储系统RADOS(Reliable, Autonomic, Distributed Object Store,即可靠的、自动化的、分布式的对象存储)

  顾名思义,这一层本身就是一个完整的对象存储系统,所有存储在Ceph系统中的用户数据事实上最终都是由这一层来存储的。而Ceph的高可靠、高可扩展、高性能、高自动化等等特性本质上也是由这一层所提供的。因此,理解RADOS是理解Ceph的基础与关键。

  • (2)基础库librados

  这一层的功能是对RADOS进行抽象和封装,并向上层提供API,以便直接基于RADOS(而不是整个Ceph)进行应用开发。特别要注意的是,RADOS是一个对象存储系统,因此,librados实现的API也只是针对对象存储功能的。

  RADOS采用C++开发,所提供的原生librados API包括C和C++两种。物理上,librados和基于其上开发的应用位于同一台机器,因而也被称为本地API。应用调用本机上的librados API,再由后者通过socket与RADOS集群中的节点通信并完成各种操作。

  • (3)高层应用接口

  这一层包括了三个部分:RADOS GW(RADOS Gateway)、 RBD(Reliable Block Device)和Ceph FS(Ceph File System),其作用是在librados库的基础上提供抽象层次更高、更便于应用或客户端使用的上层接口。

  RADOS GW是一个提供与Amazon S3和Swift兼容的RESTful API的gateway,以供相应的对象存储应用开发使用。RADOS GW提供的API抽象层次更高,但功能则不如librados强大。因此,开发者应针对自己的需求选择使用。

  RBD则提供了一个标准的块设备接口,常用于在虚拟化的场景下为虚拟机创建volume。目前,Red Hat已经将RBD驱动集成在KVM/QEMU中,以提高虚拟机访问性能。

  Ceph FS是通过Linux内核客户端和FUSE来提供一个兼容POSIX的文件系统。

五、RADOS的存储逻辑架构

img

  RADOS如图所示,RADOS集群主要由2种节点组成。一种是负责数据存储和维护功能的OSD,另一种则是若干个负责完成系统状态监测和维护的monitor。OSD和monitor之间相互传输节点的状态信息,共同得出系统的总体工作运行状态,并形成一个全局系统状态记录数据结构,即所谓的cluster map。这个数据结构和RADOS提供的特定算法相结合,便实现了Ceph“无需查表,算算就好”的核心机制和若干优秀特性。

  在使用RADOS系统时,大量的客户端程序通过与OSD或者monitor的交互获取cluster map,然后直接在本地进行计算,得出对象的存储位置后,便直接与对应的OSD通信,完成数据的各种操作。可见,在此过程中,只要保证cluster map不频繁更新,则客户端显然可以不依赖于任何元数据服务器,不进行任何查表操作,便完成数据访问流程。在RADOS的运行过程中,cluster map的更新完全取决于系统的状态变化,而导致这一变化的常见事件只有两种:OSD出现故障,或者RADOS规模扩大。而正常应用场景下,这两种事件发生的频率显然远远低于客户端对数据进行访问的频率。

JWT及JJWT签发与验证token

[TOC]

JWT

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

头部(Header)

头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。

{“typ”:”JWT”,”alg”:”HS256”}

在头部指明了签名算法是HS256算法。 我们进行BASE64编码http://base64.xpcha.com/,编码后的字符串如下:

1
2
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

载荷(playload)

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

(1)标准中注册的声明(建议但不强制使用)

1
2
3
4
5
6
7
8
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token。

(2)公共的声明

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

(3)私有的声明

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

这个指的就是自定义的claim。比如前面那个结构举例中的admin和name都属于自定的claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。

定义一个payload:

1
2
{"sub":"1234567890","name":"John Doe","admin":true}

然后将其进行base64加密,得到Jwt的第二部分。

1
2
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

签证(signature)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

header (base64后的)

payload (base64后的)

secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

1
2
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

JJWT签发与验证token

JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。

官方文档:

https://github.com/jwtk/jjwt

创建token

(1)新建项目中的pom.xml中添加依赖:

1
2
3
4
5
6
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>

(2)创建测试类,代码如下

1
2
3
4
5
6
7
8
JwtBuilder builder= Jwts.builder()
.setId("888") //设置唯一编号
.setSubject("小白")//设置主题 可以是JSON数据
.setIssuedAt(new Date())//设置签发日期
.signWith(SignatureAlgorithm.HS256,"hahaha");//设置签名 使用HS256算法,并设置SecretKey(字符串)
//构建 并返回一个字符串
System.out.println( builder.compact() );

运行打印结果:

1
2
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDQxODF9.ThecMfgYjtoys3JX7dpx3hu6pUm0piZ0tXXreFU_u3Y

再次运行,会发现每次运行的结果是不一样的,因为我们的载荷中包含了时间。

解析token

我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。

1
2
3
4
String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDQxODF9.ThecMfgYjtoys3JX7dpx3hu6pUm0piZ0tXXreFU_u3Y";
Claims claims = Jwts.parser().setSigningKey("hahaha").parseClaimsJws(compactJwt).getBody();
System.out.println(claims);

运行打印效果:
{jti=888, sub=小白, iat=1557904181}

试着将token或签名秘钥篡改一下,会发现运行时就会报错,所以解析token也就是验证token.

设置过期时间

有很多时候,我们并不希望签发的token是永久生效的,所以我们可以为token添加一个过期时间。

(1)创建token 并设置过期时间

1
2
3
4
5
6
7
8
9
10
long now=System.currentTimeMillis();
long exp=now+1000*30;//30秒过期
JwtBuilder jwtBuilder = Jwts.builder().setId( "888" )
.setSubject( "小白" )
.setIssuedAt( new Date() )//签发时间
.setExpiration( new Date( exp ) )//过期时间
.signWith( SignatureAlgorithm.HS256, "hahaha" );
String token = jwtBuilder.compact();
System.out.println(token);

运行,打印效果如下:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDUzMDgsImV4cCI6MTU1NzkwNTMwOH0.4q5AaTyBRf8SB9B3Tl-I53PrILGyicJC3fgR3gWbvUI

(2)解析TOKEN

1
2
3
4
String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDUzMDgsImV4cCI6MTU1NzkwNTMwOH0.4q5AaTyBRf8SB9B3Tl-I53PrILGyicJC3fgR3gWbvUI";
Claims claims = Jwts.parser().setSigningKey("hahaha").parseClaimsJws(compactJwt).getBody();
System.out.println(claims);

当前时间超过过期时间,则会报错。

自定义claims

我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims。

1
2
3
4
5
6
7
8
9
10
11
long now=System.currentTimeMillis();
long exp=now+1000*30;//30秒过期
JwtBuilder jwtBuilder = Jwts.builder().setId( "888" )
.setSubject( "小白" )
.setIssuedAt( new Date() )//签发时间
.setExpiration( new Date( exp ) )//过期时间
.claim( "roles","admin" )
.signWith( SignatureAlgorithm.HS256, "hahaha" );
String token = jwtBuilder.compact();
System.out.println(token);

运行打印效果:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDU4MDIsImV4cCI6MTU1NzkwNjgwMiwicm9sZXMiOiJhZG1pbiJ9.AS5Y2fNCwUzQQxXh_QQWMpaB75YqfuK-2P7VZiCXEJI

解析TOKEN:

1
2
3
4
5
String token ="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIyNTM3NTQsImV4cCI6MTU2MjI1Mzc4Mywicm9sZXMiOiJhZG1pbiJ9.CY6CMembCi3mAkBHS3ivzB5w9uvtZim1HkizRu2gWaI";
Claims claims = Jwts.parser().setSigningKey( "hahaha" ).parseClaimsJws( token ).getBody();
System.out.println(claims);
System.out.println(claims.get( "roles" ));

Nginx通过CORS实现跨域

[TOC]

什么是CORS

CORS是一个W3C标准,全称是跨域资源共享(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

当前几乎所有的浏览器(Internet Explorer 8+, Firefox 3.5+, Safari 4+和 Chrome 3+)都可通过名为跨域资源共享(Cross-Origin Resource Sharing)的协议支持AJAX跨域调用。

Chrome,Firefox,Opera,Safari都使用的是XMLHttpRequest2对象,IE使用XDomainRequest。

简单来说就是跨域的目标服务器要返回一系列的Headers,通过这些Headers来控制是否同意跨域。跨域资源共享(CORS)也是未来的跨域问题的标准解决方案。

CORS提供如下Headers,Request包和Response包中都有一部分。

HTTP Response Header

  • Access-Control-Allow-Origin
  • Access-Control-Allow-Credentials
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Headers
  • Access-Control-Expose-Headers
  • Access-Control-Max-Age

HTTP Request Header

  • Access-Control-Request-Method
  • Access-Control-Request-Headers

其中最敏感的就是Access-Control-Allow-Origin这个Header, 它是W3C标准里用来检查该跨域请求是否可以被通过。(Access Control Check)。如果需要跨域,解决方法就是在资源的头中加入Access-Control-Allow-Origin 指定你授权的域。

启用CORS请求

假设您的应用已经在example.com上了,而您想要从www.example2.com提取数据。一般情况下,如果您尝试进行这种类型的AJAX调用,请求将会失败,而浏览器将会出现源不匹配的错误。利用CORS后只需www.example2.com 服务端添加一个HTTP Response头,就可以允许来自example.com的请求。

将Access-Control-Allow-Origin添加到某网站下或整个域中的单个资源

1
2
Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Credentials: true (可选)

将允许任何域向您提交请求

1
2
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true (可选)

提交跨域请求

如果服务器端已启用了CORS,那么提交跨域请求就和普通的XMLHttpRequest请求没什么区别。例如现在example.com可以向www.example2.com提交请求。

1
2
3
4
5
6
7
8
var xhr = new XMLHttpRequest();
// xhr.withCredentials = true; //如果需要Cookie等
xhr.open('GET', 'http://www.example2.com/hello.json');
xhr.onload = function(e) {
var data = JSON.parse(this.response);
...
}
xhr.send();

服务端Nginx配置

要实现CORS跨域,服务端需要下图中这样一个流程

Nginx通过CORS实现跨域

  • 对于简单请求,如GET,只需要在HTTP Response后添加Access-Control-Allow-Origin。
  • 对于非简单请求,比如POST、PUT、DELETE等,浏览器会分两次应答。第一次preflight(method: OPTIONS),主要验证来源是否合法,并返回允许的Header等。第二次才是真正的HTTP应答。所以服务器必须处理OPTIONS应答。

流程如下

  • 首先查看http头部有无origin字段;
  • 如果没有,或者不允许,直接当成普通请求处理,结束;
  • 如果有并且是允许的,那么再看是否是preflight(method=OPTIONS);
  • 如果是preflight,就返回Allow-Headers、Allow-Methods等,内容为空;
  • 如果不是preflight,就返回Allow-Origin、Allow-Credentials等,并返回正常内容。

用伪代码表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
location /pub/(.+) {
if ($http_origin ~ <允许的域(正则匹配)>) {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Credentials' "true";
if ($request_method = "OPTIONS") {
add_header 'Access-Control-Max-Age' 86400;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE';
add_header 'Access-Control-Allow-Headers' 'reqid, nid, host, x-real-ip, x-forwarded-ip, event-type, event-id, accept, content-type';
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain, charset=utf-8';
return 204;
}
}
# 正常nginx配置
......
}

Nginx配置实例

实例一:允许example.com的应用在www.example2.com上跨域提取数据

在nginx.conf里找到server项,并在里面添加如下配置

1
2
3
4
5
6
7
8
location /{

add_header 'Access-Control-Allow-Origin' 'http://example.com';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,X-Requested-With';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS';
...
}

如果需要允许来自任何域的访问,可以这样配置

1
add_header Access-Control-Allow-Origin *;

注释如下

第一条指令:授权从example.com的请求(必需)
第二条指令:当该标志为真时,响应于该请求是否可以被暴露(可选)
第三条指令:允许脚本访问的返回头(可选)
第四条指令:指定请求的方法,可以是GET, POST, OPTIONS, PUT, DELETE等(可选)

重启Nginx

1
$ service nginx reload

测试跨域请求

1
$ curl -I -X OPTIONS -H "Origin: http://example.com" http://www.example2.com

成功时,响应头是如下所示

1
2
3
HTTP/1.1 200 OK
Server: nginx
Access-Control-Allow-Origin: example.com

实例二:Nginx允许多个域名跨域访问

由于Access-Control-Allow-Origin参数只允许配置单个域名或者*,当我们需要允许多个域名跨域访问时可以用以下几种方法来实现。

  • 方法一

如需要允许用户请求来自www.example.com、m.example.com、wap.example.com访问www.example2.com域名时,返回头Access-Control-Allow-Origin,具体配置如下

在nginx.conf里面,找到server项,并在里面添加如下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
map $http_origin $corsHost {
default 0;
"~http://www.example.com" http://www.example.com;
"~http://m.example.com" http://m.example.com;
"~http://wap.example.com" http://wap.example.com;
}

server
{
listen 80;
server_name www.example2.com;
root /usr/share/nginx/html;
location /
{
add_header Access-Control-Allow-Origin $corsHost;
}
}
  • 方法二

如需要允许用户请求来自localhost、www.example.com或m.example.com的请求访问xxx.example2.com域名时,返回头Access-Control-Allow-Origin,具体配置如下

在Nginx配置文件中xxx.example2.com域名的location /下配置以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
set $cors '';
if ($http_origin ~* 'https?://(localhost|www\.example\.com|m\.example\.com)') {
set $cors 'true';
}

if ($cors = 'true') {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With';
}

if ($request_method = 'OPTIONS') {
return 204;
}
  • 方法三

如需要允许用户请求来自*.example.com访问xxx.example2.com域名时,返回头Access-Control-Allow-Origin,具体配置如下

在Nginx配置文件中xxx.example2.com域名的location /下配置以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if ( $http_origin ~ http://(.*).example.com){
set $allow_url $http_origin;
}
#CORS(Cross Orign Resource-Sharing)跨域控制配置
#是否允许请求带有验证信息
add_header Access-Control-Allow-Credentials true;
#允许跨域访问的域名,可以是一个域的列表,也可以是通配符*
add_header Access-Control-Allow-Origin $allow_url;
#允许脚本访问的返回头
add_header Access-Control-Allow-Headers 'x-requested-with,content-type,Cache-Control,Pragma,Date,x-timestamp';
#允许使用的请求方法,以逗号隔开
add_header Access-Control-Allow-Methods 'POST,GET,OPTIONS,PUT,DELETE';
#允许自定义的头部,以逗号隔开,大小写不敏感
add_header Access-Control-Expose-Headers 'WWW-Authenticate,Server-Authorization';
#P3P支持跨域cookie操作
add_header P3P 'policyref="/w3c/p3p.xml", CP="NOI DSP PSAa OUR BUS IND ONL UNI COM NAV INT LOC"';
  • 方法四

如需要允许用户请求来自xxx1.example.com或xxx1.example1.com访问xxx.example2.com域名时,返回头Access-Control-Allow-Origin,具体配置如下

在Nginx配置文件中xxx.example2.com域名的location /下配置以下内容

1
2
3
4
5
6
location / {

if ( $http_origin ~ .*.(example|example1).com ) {
add_header Access-Control-Allow-Origin $http_origin;
}
}

实例三:Nginx跨域配置并支持DELETE,PUT请求

默认Access-Control-Allow-Origin开启跨域请求只支持GET、HEAD、POST、OPTIONS请求,使用DELETE发起跨域请求时,浏览器出于安全考虑会先发起OPTIONS请求,服务器端接收到的请求方式就变成了OPTIONS,所以引起了服务器的405 Method Not Allowed。

解决方法

首先要对OPTIONS请求进行处理

1
2
3
4
5
6
if ($request_method = 'OPTIONS') { 
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS;
#其他头部信息配置,省略...
return 204;
}

当请求方式为OPTIONS时设置Allow的响应头,重新处理这次请求。这样发出请求时第一次是OPTIONS请求,第二次才是DELETE请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 完整配置参考
# 将配置文件的放到对应的server {}里

add_header Access-Control-Allow-Origin *;

location / {
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS;
return 204;
}
index index.php;
try_files $uri @rewriteapp;
}

实例四:更多配置示例

  • 示例一
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
The following Nginx configuration enables CORS, with support for preflight requests.

#
# Wide-open CORS config for nginx
#
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
#
# Custom headers and headers various browsers *should* be OK with but aren't
#
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
#
# Tell client that this pre-flight info is valid for 20 days
#
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
}
if ($request_method = 'GET') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
}
}
  • 示例二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if ($request_method = 'OPTIONS') {  
add_header 'Access-Control-Allow-Origin' 'https://docs.domain.com';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,token';
return 204;
}
if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' 'https://docs.domain.com';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,token';
}
if ($request_method = 'GET') {
add_header 'Access-Control-Allow-Origin' 'https://docs.domain.com';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,token';
}

其它技巧

Apache中启用CORS

在httpd配置或.htaccess文件中添加如下语句

1
2
SetEnvIf Origin "^(.*\.example\.com)$" ORIGIN_SUB_DOMAIN=$1  
Header set Access-Control-Allow-Origin "%{ORIGIN_SUB_DOMAIN}e" env=ORIGIN_SUB_DOMAIN

PHP中启用CORS

通过在服务端设置Access-Control-Allow-Origin响应头

  • 允许所有来源访问
1
2
3
<?php
header("Access-Control-Allow-Origin: *");
?>
  • 允许来自特定源的访问
1
2
3
<?php
header('Access-Control-Allow-Origin: '.$_SERVER['HTTP_ORIGIN']);
?>
  • 配置多个访问源

由于浏览器实现只支持了单个origin、*、null,如果要配置多个访问源,可以在代码中处理如下

1
2
3
4
5
6
7
8
9
10
<?php
$allowed_origins = array(
"http://www.example.com" ,
"http://app.example.com" ,
"http://cms.example.com" ,
);
if (in_array($_SERVER['HTTP_ORIGIN'], $allowed_origins)){
@header("Access-Control-Allow-Origin: " . $_SERVER['HTTP_ORIGIN']);
}
?>

HTML中启用CORS

1
<meta http-equiv="Access-Control-Allow-Origin" content="*">

参考文档

http://www.google.com

http://t.cn/RZEYPmD

http://t.cn/RhcAN2d

http://to-u.xyz/2016/06/30/nginx-cors/
http://coderq.github.io/2016/05/13/cross-domain/

作者:Mike

文章出处:运维之美