[go]json处理的一些总结

https://colobu.com/2017/06/21/json-tricks-in-Go/

本文是基于 Go 官方和 https://eager.io/blog/go-and-json/ 进行翻译整理的

JSON 是一种轻量级的数据交换格式,常用作前后端数据交换,Go 在 encoding/json 包中提供了对 JSON 的支持。

序列化

把 Go struct 序列化成 JSON 对象,Go 提供了 Marshal 方法,正如其含义所示表示编排序列化,函数签名如下:

1
func Marshal(v interface{}) ([]byte, error)

举例来说,比如下面的 Go struct:

1
2
3
4
5
type Message struct {
    Name string
    Body string
    Time int64
}

使用 Marshal 序列化:

1
2
3
m := Message{"Alice", "Hello", 1294706395881547000}
b, err := json.Marshal(m) 
fmt.Println(b) //{"Name":"Alice","Body":"Hello","Time":1294706395881547000}

在 Go 中并不是所有的类型都能进行序列化:

  • JSON object key 只支持 string
  • Channel、complex、function 等 type 无法进行序列化
  • 数据中如果存在循环引用,则不能进行序列化,因为序列化时会进行递归
  • Pointer 序列化之后是其指向的值或者是 nil

还需要注意的是:只有 struct 中支持导出的 field 才能被 JSON package 序列化,即首字母大写的 field

反序列化

反序列化函数是 Unmarshal ,其函数签名如下:

1
func Unmarshal(data []byte, v interface{}) error

如果要进行反序列化,我们首先需要创建一个可以接受序列化数据的 Go struct:

1
2
var m Message
err := json.Unmarshal(b, &m)

JSON 对象一般都是小写表示,Marshal 之后 JSON 对象的首字母依然是大写,如果序列化之后名称想要改变如何实现,答案就是 struct tags

Struct Tag

Struct tag 可以决定 Marshal 和 Unmarshal 函数如何序列化和反序列化数据。

指定 JSON filed name

JSON object 中的 name 一般都是小写,我们可以通过 struct tag 来实现:

1
2
3
type MyStruct struct {
    SomeField string `json:"some_field"`
}

SomeField 序列化之后会变成 some_field。

指定 field 是 empty 时的行为

使用 omitempty 可以告诉 Marshal 函数如果 field 的值是对应类型的 zero-value,那么序列化之后的 JSON object 中不包含此 field:

1
2
3
4
5
6
type MyStruct struct {
    SomeField string `json:"some_field,omitempty"`
}

m := MyStruct{}
b, err := json.Marshal(m) //{}

如果 SomeField == “” ,序列化之后的对象就是 {}

跳过 field

Struct tag “-” 表示跳过指定的 filed:

1
2
3
4
5
6
type MyStruct struct {
    SomeField string `json:"some_field"`
    Passwd string `json:"-"`
}
m := MyStruct{}
b, err := json.Marshal(m) //{"some_feild":""}

即序列化的时候不输出,这样可以有效保护需要保护的字段不被序列化。

反序列化任意 JSON 数据

默认的 JSON 只支持以下几种 Go 类型:

  • bool for JSON booleans
  • float64 for JSON numbers
  • string for JSON strings
  • nil for JSON null

在序列化之前如果不知道 JSON 数据格式,我们使用 interface{} 来存储。interface {} 的作用详见本博的其他文章。

有如下的数据格式:

1
b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)

如果我们序列化之前不知道其数据格式,我们可以使用 interface{} 来存储我们的 decode 之后的数据:

1
2
var f interface{}
err := json.Unmarshal(b, &f)

反序列化之后 f 应该是像下面这样:

1
2
3
4
5
6
7
8
f = map[string]interface{}{
    "Name": "Wednesday",
    "Age":  6,
    "Parents": []interface{}{
        "Gomez",
        "Morticia",
    },
}

key 是 string,value 是存储在 interface{} 内的。想要获得 f 中的数据,我们首先需要进行 type assertion,然后通过 range 迭代获得 f 中所有的 key :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
m := f.(map[string]interface{})
for k, v := range m {
    switch vv := v.(type) {
    case string:
        fmt.Println(k, "is string", vv)
    case float64:
        fmt.Println(k, "is float64", vv)
    case []interface{}:
        fmt.Println(k, "is an array:")
        for i, u := range vv {
            fmt.Println(i, u)
        }
    default:
        fmt.Println(k, "is of a type I don't know how to handle")
    }
}

反序列化对 slice、map、pointer 的处理

我们定义一个 struct 继续对上面例子中的 b 进行反序列化:

1
2
3
4
5
6
7
8
type FamilyMember struct {
    Name    string
    Age     int
    Parents []string
}

var m FamilyMember
err := json.Unmarshal(b, &m)

这个例子是能够正常工作的,你一定也注意到了,struct 中包含一个 slice Parents ,slice 默认是 nil,之所以反序列化可以正常进行就是因为 Unmarshal 在序列化时进行了对 slice Parents 做了初始化,同理,对 map 和 pointer 都会做类似的工作,比如序列化如果 Pointer 不是 nil 首先进行 dereference 获得其指向的值,然后再进行序列化,反序列化时首先对 nil pointer 进行初始化

Stream JSON

除了 marshal 和 unmarshal 函数,Go 还提供了 Decoder 和 Encoder 对 stream JSON 进行处理,常见 request 中的 Body、文件等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
jsonFile, err := os.Open("post.json")
if err != nil {
    fmt.Println("Error opening json file:", err)
    return
}

defer jsonFile.Close()
decoder := json.NewDecoder(jsonFile)
for {
    var post Post
    err := decoder.Decode(&post)
    if err == io.EOF {
        break
    }

    if err != nil {
        fmt.Println("error decoding json:", err)
        return
    }

    fmt.Println(post)
}

嵌入式 struct 的序列化

Go 支持对 nested struct 进行序列化和反序列化:

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
type App struct {
	Id string `json:"id"`
}

type Org struct {
	Name string `json:"name"`
}

type AppWithOrg struct {
	App
	Org
}

func main() {
	data := []byte(`
        {
            "id": "k34rAT4",
            "name": "My Awesome Org"
        }
    `)

	var b AppWithOrg

	json.Unmarshal(data, &b)
	fmt.Printf("%#v", b)

	a := AppWithOrg{
		App: App{
			Id: "k34rAT4",
		},
		Org: Org{
			Name: "My Awesome Org",
		},
	}
	data, _ = json.Marshal(a)
	fmt.Println(string(data))
}

Nested struct 虽然看起来有点怪异,有些时候它将非常有用。

自定义序列化函数

Go JSON package 中定了两个 Interface Marshaler 和 Unmarshaler ,实现这两个 Interface 可以让你定义的 type 支持序列化操作。

错误处理

总是记得检查序列或反序列化的错误,可以让你的程序更健壮,而不是在出错之后带着错误继续执行下去。

参考资料

  1. 临时忽略struct空字段
  2. 临时添加额外的字段
  3. 临时粘合两个struct
  4. 一个json切分成两个struct
  5. 临时改名struct的字段
  6. 用字符串传递数字
  7. 容忍字符串和数字互转
  8. 容忍空数组作为对象
  9. 使用 MarshalJSON支持time.Time
  10. 使用 RegisterTypeEncoder支持time.Time
  11. 使用 MarshalText支持非字符串作为key的map
  12. 使用 json.RawMessage
  13. 使用 json.Number
  14. 统一更改字段的命名风格
  15. 使用私有的字段
  16. 忽略掉一些字段
  17. 忽略掉一些字段2

taowenjson-iterator的作者。 序列化和反序列化需要处理JSON和struct的关系,其中会用到一些技巧。 原文 Golang 中使用 JSON 的小技巧是他的经验之谈,介绍了一些struct解析成json的技巧,以及 json-iterator 库的一些便利的处理。

有的时候上游传过来的字段是string类型的,但是我们却想用变成数字来使用。 本来用一个json:”,string” 就可以支持了,如果不知道golang的这些小技巧,就要大费周章了。

参考文章:http://attilaolah.eu/2014/09/10/json-and-struct-composition-in-go/

临时忽略struct空字段

1
2
3
4
5
type User struct {
Email string `json:”email”`
Password string `json:”password”`
// many more fields…
}

如果想临时忽略掉空Password字段,可以用omitempty:

1
2
3
4
5
6
json.Marshal(struct {
*User
Password bool `json:”password,omitempty”`
}{
User: user,
})

临时添加额外的字段

1
2
3
4
5
type User struct {
Email string `json:”email”`
Password string `json:”password”`
// many more fields…
}

临时忽略掉空Password字段,并且添加token字段

1
2
3
4
5
6
7
8
json.Marshal(struct {
*User
Token string `json:”token”`
Password bool `json:”password,omitempty”`
}{
User: user,
Token: token,
})

临时粘合两个struct

通过嵌入struct的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type BlogPost struct {
URL string `json:”url”`
Title string `json:”title”`
}
type Analytics struct {
Visitors int `json:”visitors”`
PageViews int `json:”page_views”`
}
json.Marshal(struct{
*BlogPost
*Analytics
}{post, analytics})

一个json切分成两个struct

1
2
3
4
5
6
7
8
9
json.Unmarshal([]byte(`{
“url”: “attila@attilaolah.eu”,
“title”: “Attila’s Blog”,
“visitors”: 6,
“page_views”: 14
}`), &struct {
*BlogPost
*Analytics
}{&post, &analytics})

临时改名struct的字段

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
type CacheItem struct {
Key string `json:”key”`
MaxAge int `json:”cacheAge”`
Value Value `json:”cacheValue”`
}
json.Marshal(struct{
*CacheItem
// Omit bad keys
OmitMaxAge omit `json:”cacheAge,omitempty”`
OmitValue omit `json:”cacheValue,omitempty”`
// Add nice keys
MaxAge int `json:”max_age”`
Value *Value `json:”value”`
}{
CacheItem: item,
// Set the int by value:
MaxAge: item.MaxAge,
// Set the nested struct by reference, avoid making a copy:
Value: &item.Value,
})

用字符串传递数字

1
2
3
type TestObject struct {
Field1 int `json:”,string”`
}

这个对应的json是 {"Field1": "100"}

如果json是 {"Field1": 100} 则会报错

容忍字符串和数字互转

如果你使用的是jsoniter,可以启动模糊模式来支持 PHP 传递过来的 JSON。

1
2
3
import “github.com/json-iterator/go/extra”
extra.RegisterFuzzyDecoders()

这样就可以处理字符串和数字类型不对的问题了。比如

1
2
var val string
jsoniter.UnmarshalFromString(`100`, &val)

又比如

1
2
var val float32
jsoniter.UnmarshalFromString(`”1.23″`, &val)

容忍空数组作为对象

PHP另外一个令人崩溃的地方是,如果 PHP array是空的时候,序列化出来是[]。但是不为空的时候,序列化出来的是{"key":"value"}。 我们需要把 [] 当成 {} 处理。

如果你使用的是jsoniter,可以启动模糊模式来支持 PHP 传递过来的 JSON。

1
2
3
import “github.com/json-iterator/go/extra”
extra.RegisterFuzzyDecoders()

这样就可以支持了

1
2
var val map[string]interface{}
jsoniter.UnmarshalFromString(`[]`, &val)

使用 MarshalJSON支持time.Time

golang 默认会把 time.Time 用字符串方式序列化。如果我们想用其他方式表示 time.Time,需要自定义类型并定义 MarshalJSON

1
2
3
4
5
6
type timeImplementedMarshaler time.Time
func (obj timeImplementedMarshaler) MarshalJSON() ([]byte, error) {
seconds := time.Time(obj).Unix()
return []byte(strconv.FormatInt(seconds, 10)), nil
}

序列化的时候会调用 MarshalJSON

1
2
3
4
5
6
7
8
9
type TestObject struct {
Field timeImplementedMarshaler
}
should := require.New(t)
val := timeImplementedMarshaler(time.Unix(123, 0))
obj := TestObject{val}
bytes, err := jsoniter.Marshal(obj)
should.Nil(err)
should.Equal(`{“Field”:123}`, string(bytes))

使用 RegisterTypeEncoder支持time.Time

jsoniter 能够对不是你定义的type自定义JSON编解码方式。比如对于 time.Time 可以用 epoch int64 来序列化

1
2
3
4
5
import “github.com/json-iterator/go/extra”
extra.RegisterTimeAsInt64Codec(time.Microsecond)
output, err := jsoniter.Marshal(time.Unix(1, 1002))
should.Equal(“1000001”, string(output))

如果要自定义的话,参见 RegisterTimeAsInt64Codec 的实现代码

使用 MarshalText支持非字符串作为key的map

虽然 JSON 标准里只支持 string 作为 key 的 map。但是 golang 通过 MarshalText() 接口,使得其他类型也可以作为 map 的 key。例如

1
2
3
4
f, _, _ := big.ParseFloat(“1”, 10, 64, big.ToZero)
val := map[*big.Float]string{f: “2”}
str, err := MarshalToString(val)
should.Equal(`{“1″:”2”}`, str)

其中 big.Float 就实现了 MarshalText()

使用 json.RawMessage

如果部分json文档没有标准格式,我们可以把原始的信息用[]byte保存下来。

1
2
3
4
5
6
7
type TestObject struct {
Field1 string
Field2 json.RawMessage
}
var data TestObject
json.Unmarshal([]byte(`{“field1”: “hello”, “field2”: [1,2,3]}`), &data)
should.Equal(` [1,2,3]`, string(data.Field2))

使用 json.Number

默认情况下,如果是 interface{} 对应数字的情况会是 float64 类型的。如果输入的数字比较大,这个表示会有损精度。所以可以 UseNumber() 启用 json.Number 来用字符串表示数字。

1
2
3
4
5
decoder1 := json.NewDecoder(bytes.NewBufferString(`123`))
decoder1.UseNumber()
var obj1 interface{}
decoder1.Decode(&obj1)
should.Equal(json.Number(“123”), obj1)

jsoniter 支持标准库的这个用法。同时,扩展了行为使得 Unmarshal 也可以支持 UseNumber 了。

1
2
3
4
json := Config{UseNumber:true}.Froze()
var obj interface{}
json.UnmarshalFromString(“123”, &obj)
should.Equal(json.Number(“123”), obj)

统一更改字段的命名风格

经常 JSON 里的字段名 Go 里的字段名是不一样的。我们可以用 field tag 来修改。

1
2
3
4
5
6
7
8
output, err := jsoniter.Marshal(struct {
UserName string `json:”user_name”`
FirstLanguage string `json:”first_language”`
}{
UserName: “taowen”,
FirstLanguage: “Chinese”,
})
should.Equal(`{“user_name”:”taowen”,”first_language”:”Chinese”}`, string(output))

但是一个个字段来设置,太麻烦了。如果使用 jsoniter,我们可以统一设置命名风格。

1
2
3
4
5
6
7
8
9
10
11
12
import “github.com/json-iterator/go/extra”
extra.SetNamingStrategy(LowerCaseWithUnderscores)
output, err := jsoniter.Marshal(struct {
UserName string
FirstLanguage string
}{
UserName: “taowen”,
FirstLanguage: “Chinese”,
})
should.Nil(err)
should.Equal(`{“user_name”:”taowen”,”first_language”:”Chinese”}`, string(output))

使用私有的字段

Go 的标准库只支持 public 的 field。jsoniter 额外支持了 private 的 field。需要使用 SupportPrivateFields() 来开启开关。

1
2
3
4
5
6
7
8
9
import “github.com/json-iterator/go/extra”
extra.SupportPrivateFields()
type TestObject struct {
field1 string
}
obj := TestObject{}
jsoniter.UnmarshalFromString(`{“field1″:”Hello”}`, &obj)
should.Equal(“Hello”, obj.field1)

下面是我补充的内容

忽略掉一些字段

原文中第一节有个错误,我更正过来了。omitempty不会忽略某个字段,而是忽略空的字段,当字段的值为空值的时候,它不会出现在JSON数据中。

如果想忽略某个字段,需要使用 json:"-"格式。

1
2
3
4
5
type User struct {
Email string `json:”email”`
Password string `json:”password”`
// many more fields…
}

如果想临时忽略掉空Password字段,可以用-:

1
2
3
4
5
6
json.Marshal(struct {
*User
Password bool `json:”-“`
}{
User: user,
})

忽略掉一些字段2

如果不想更改原struct,还可以使用下面的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type User struct {
Email string `json:”email”`
Password string `json:”password”`
// many more fields…
}
type omit *struct{}
type PublicUser struct {
*User
Password omit `json:”-“`
}
json.Marshal(PublicUser{
User: user,
})

python抓取的一些总结

python作为爬虫的最常见语言,很有自己的优势。这里举一些常见的用法。

1,使用scrapy框架。

https://scrapy-chs.readthedocs.io/zh_CN/0.24/intro/tutorial.html

2, 纯脚本

lib库

from concurrent.futures import ThreadPoolExecutor

from bs4 import BeautifulSoup

import Queue, time, random

import requests

提供一个比较粗糙的代码,使用到了python代理,queue,多线程,BeautifulSoup。最终print方法应该用线程锁避免打印错误。


#coding=utf-8
import requests
from concurrent.futures import ThreadPoolExecutor
import Queue, time, random
#import pymysql
from bs4 import BeautifulSoup
import re
import urllib
import urllib2
import gzip
import cStringIO
import datetime
import json
from StringIO import StringIO
import threading

import base64

index_url = 'https://www.juzimi.com/alltags'
page_url = 'https://www.juzimi.com/alltags?page=%s'

task_queue = Queue.Queue()

has_words = []
has_words_value = {}

lock=threading.Lock()

ip = ""
def getIp() :
global ip

url = 'http://s.zdaye.com/?api=201903221353043521&count=1&px=2'
ipret = curl(url, '', False, False)
time.sleep(5)
print "get ip:" + str(ipret)
ip = str(ipret)
def curl(url, data = '', isCompress = False, use_ip = True):

global ip
if(data):
data = urllib.urlencode(data)
else:
data = None
#headers = {"method":"GET","user-agent":self.ua,"Referer":self.refer, "Cookie":self.cookie, "Upgrade-Insecure-Requests": 1,"Accept-Encoding": "gzip, deflate, br"}
headers = {"method":"GET","Accept-Encoding": "gzip, deflate, br", "user-agent" : "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36"}

try :

#代理代码
if(use_ip) :
opener = urllib2.build_opener(urllib2.ProxyHandler({"https" : ip}))
urllib2.install_opener(opener)

request = urllib2.Request(url, data, headers)
response = urllib2.urlopen(request, timeout=10)

#response = opener.open(request)
if(isCompress) :
buf = StringIO(response.read())
data = gzip.GzipFile(fileobj=buf)
return data.read()
return response.read()

except :
exit()
getIp()
print "get ip retry"
self.curl(url, data, isCompress, use_ip)

#exit()

def setTaskR() :
task_queue.put({'url':'https://www.juzimi.com/tags/%E6%96%B0%E8%AF%97', 'title':'诗词'})

def setTask() :
#for i in range(0, 12) :
for i in range(0, 12) :
url = page_url % (i)
content = curl(url, '', True)
#print url
soup = BeautifulSoup(content, "html.parser")
span = soup.findAll('div',{'class':'views-field-name'})
for tmp in span :
data = {}
href = tmp.find('a').get('href')
title = tmp.find('a').get('title')
data = {"url":"https://www.juzimi.com" + href, "title" : title}
print data
task_queue.put(data)
#print tmp.get('title')
time.sleep(1)

def getFile() :

global has_words
global has_words_value

for line in open('word.log') :
if(line.find('juzimi.com') != -1) :
continue
line = line.split(":", 1)
if(len(line) > 1 and line[1] == 'test') :
continue

#print line[0]
if(not line[0] in has_words) :
has_words.append(line[0])
has_words_value[line[0]] = 1
#print line[0]
else :
has_words_value[line[0]] = has_words_value[line[0]] + 1
has_words = []
for k in has_words_value:
if(has_words_value[k] > 100) :
has_words.append(k)
for line in open('word.url') :
lines = eval(line)
url = lines['url']
title = lines['title'].encode('utf-8')
if(title in has_words) :
continue
task_queue.put(lines)

#runTask()
sleep_time = random.randint(30,60)
#time.sleep(sleep_time)
#time.sleep(60)

def runTask() :
while(not task_queue.empty()) :
data = task_queue.get()
printinfo =[]
hotword = data['title']
url = data['url']
hotword = hotword.encode('utf-8')
#print url
lastIndex = 0
content = curl(url, '', True)
#content = re.sub(r'\n|&nbsp|\xa0|\\xa0|\u3000|\\u3000|\\u0020|\u0020', '', str(content))
content = content.replace('<br/>', '')
content = content.replace('\r', ' ')
soup = BeautifulSoup(content, "html.parser")
last = soup.find('li', {'class', 'pager-last'})
if (not last) :

last = soup.findAll('li', {'class' : 'pager-item'})
if(not last) :
print "get empty:" + url
continue
for tmp in last :
if(int(tmp.text) > lastIndex) :
#print int(tmp.text)
lastIndex = int(tmp.text)
else :
lastIndex = last.text

span = soup.findAll('div',{'class', 'views-field-phpcode-1'})

if(not span) :
print "get empty:" + url
continue

#print url
for tmp in span :
words = tmp.findAll('a')
for word in words :
#printinfo.append({'hotword' : hotword, 'content' : word.text.encode('utf-8')})
print hotword + ":" + word.text.encode('utf-8')
#time.sleep(3)
sleep_time = random.randint(10,20)
#time.sleep(sleep_time)
for i in range(1, int(lastIndex)) :

url = "https://www.juzimi.com/tags/" +hotword + "?page=" + str(i)
#ret = getContent(url, hotword)

t = threading.Thread(target=getContent, args=(url, hotword))
t.start()
"""
for tmp in ret:
printinfo.append(tmp)
"""

"""
for tmp in printinfo :
print tmp['hotword'] + ":" + tmp['content']
"""

def getContent(url, hotword) :
printinfo =[]
#print url
content = curl(url, '', True)
#content = re.sub(r'\n|&nbsp|\xa0|\\xa0|\u3000|\\u3000|\\u0020|\u0020', '', str(content))
content = content.replace('<br/>', '')
content = content.replace('\r', ' ')
soup = BeautifulSoup(content, "html.parser")
last = soup.find('li', {'class', 'pager-last'})
span = soup.findAll('div',{'class', 'views-field-phpcode-1'})
for tmp in span :
words = tmp.findAll('a')
for word in words :
#printinfo.append({'hotword' : hotword, 'content' : word.text.encode('utf-8')})
print hotword + ":" + word.text.encode('utf-8')
sleep_time = random.randint(20,30)
#time.sleep(sleep_time)
#return printinfo

getIp()

getFile()

"""

#setTaskR()
#runTask()
#exit()
"""
executor = ThreadPoolExecutor(max_workers = 50)
#executor.submit(runTask)
#exit()
for i in range(0, 20) :
executor.submit(runTask)

&nbsp;

 

[转]python中str字符串和unicode对象字符串的拼接问题

一个很基本的问题,遇到了,这里记录下

str字符串

s = ‘中文’ # s: <type ‘str’>
s是个str对象,中文字符串。存储方式是字节码。字节码是怎么存的:

如果这行代码在python解释器中输入&运行,那么s的格式就是解释器的编码格式;

如果这行代码是在源码文件中写入、保存然后执行,那么解释器载入代码时就将s初始化为文件指定编码(比如py文件开头那行的utf-8);

unicode对象字符串

unicode是一种编码标准,具体的实现可能是utf-8,utf-16,gbk等等,这就是中文字符串和unicode有密切关系的原因。

python内部使用两个字节存储一个unicode对象(unicode对象并不只能是字符串,这两个字节还可以存其他内容),为什么要用unicode而不用str呢,因为中文转码的缘故,因为unicode的优点是便于跨平台。

s1 = u’中文’ # s1: <type ‘unicode’>
s2 = unicode(‘中文’, ‘utf-8’) # utf8是在指定解码方式, s2: <type ‘unicode’>
str字符串和unicode字符串拼接

只要注意正确的decode、encode方式,统一编码后就能顺利地拼接了。

# -*- coding: utf-8 -*-

s1 = ‘中文’
s2 = u’你好’
print s1 + unicode(s2, ‘utf-8’) # 中文你好
print s1 + s2.decode(‘utf-8’) # 中文你好
print s1.encode(‘utf-8’) + s2 # 中文你好

print type(s1) # <type ‘str’>
print type(s2) # <type ‘unicode’>
print type(s1.decode(‘utf-8’)) # <type ‘unicode’>
print type(s2.encode(‘utf-8’)) # <type ‘str’>
对于str要注意当前环境编码方式,也许是控制台那种设定好了的,也许是你自己在代码中指定的。(看你的代码是在哪里敲的了)
对于unicode对象,一般都是decode得到的,像直接【u’你好’】这种其实不是很常见,所以要注意字符串来源是什么编码,比如从gbk文件或utf8文件中读入的。
———————
作者:Joy_Shen
来源:CSDN
原文:https://blog.csdn.net/index20001/article/details/78974814
版权声明:本文为博主原创文章,转载请附上博文链接!

一款跨平台的Web目录扫描工具

工具是由安全盒子王松编写 是用Python编写的  其中包含脚本识别 后台地址 以及乌云抓的常见漏洞url

这对于Ubuntu系统的用户 特别方便  Windows上也有很多的扫描目录的好工具 比如御剑 wwwscan 等等很多

我自己加入了一点点字典

亲测

从github上下载即可

[AppleScript] 纯文本查看 复制代码
1
2
3
4
5
git clone [url]https://github.com/Strikersb/webdirscan[/url]
cd webdirscan
python webdirscan.py  url

报错的话 安装的第三方Python模块成功解决

[AppleScript] 纯文本查看 复制代码
1
pip install requests

运行图如下

[AppleScript] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
root@xaiSec:/home/hacker# cd webdirscan
root@xaiSec:/home/hacker/webdirscan# python webdirscan.py -h
usage: webdirscan.py [-h] [-d SCANDICT] [-o SCANOUTPUT] [-t THREADNUM]
                     scanSite
positional arguments:
  scanSite              The website to be scanned
optional arguments:
  -h, --help            show this help message and exit
  -d SCANDICT, --dict SCANDICT
                        Dictionary for scanning
  -o SCANOUTPUT, --output SCANOUTPUT
                        Results saved files
  -t THREADNUM, --thread THREADNUM
                        Number of threads running the program
root@xaiSec:/home/hacker/webdirscan#

翻译后

[AppleScript] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
用法:webdirscan.py [-h] [-d SCANDICT] [-o SCANOUTPUT] [-t THREADNUM]
                      scanSite
位置参数:
   scanSite要扫描的网站
可选参数:
   -h,--help显示此帮助消息并退出
   -d SCANDICT,--dict SCANDICT
                         扫描词典
   -o SCANOUTPUT,--output SCANOUTPUT
                         结果保存的文件
   -t THREADNUM, - thread THREADNUM
                         运行程序的线程数

首先我们先来扫描一个网站 比如我们来扫描下百度= =  嘿嘿
输出结果为以下

[AppleScript] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
root@xaiSec:/home/hacker/webdirscan# python webdirscan.py [url]www.baidu.com[/url]
Dirscan is running!
Scan target: [url]http://www.baidu.com[/url]
Total Dictionary: 85025
[200]http://www.baidu.com/robots.txt
[200]http://www.baidu.com/index.htm
[200]http://www.baidu.com/index.php
[200]http://www.baidu.com/index.html
[200]http://www.baidu.com/index.html
[200]http://www.baidu.com/cache/
[200]http://www.baidu.com/index.htm
[200]http://www.baidu.com/robots.txt

 

[转]百度的春晚战事(猜猜哪个是博主)

百度的春晚战事

原创: 史中 浅黑科技 浅黑科技 今天

编辑

请点击输入图片描述

浅友们大家好~我是史中,我的日常生活是开撩五湖四海的科技大牛,我会尝试各种姿势,把他们的无边脑洞和温情故事讲给你听。如果你特别想听到谁的故事,不妨加微信(微信号:shizhongpro)告诉我。

百度的春晚战事

文 | 史中

我们对春晚一无所知。

罗振宇曾在跨年演讲上如是说。

无论悲喜,反正每个中国人都为春晚辟出了一块“专属记忆”。而从2015年开始,中国人的春晚记忆里被点上了一颗“红痣”。那就是——总有一家顶尖互联网公司面带羞赧地走上舞台,给十几亿人发红包。

“一无所知”的形容,可谓精妙。春晚时,你只知道自己在对着电视刷红包,但从空中俯瞰,十多亿人同时拿起手机,将会汇聚起怎样一种数据海啸,即使《2012》《后天》里的那种惊天排浪,也难以企及十分之一。

据说,美国“超级碗”直播中间插播广告的时候,电视机前的几亿观众会集体上厕所冲马桶,导致美国各大城市的市政供水出现崩溃。超级碗直播的全球观众有1.3亿。而我们的春晚直播,全球观众有13亿。

2015年,微信曾经上春晚发过红包,在全国观众的冲击下一度跪倒长达一小时,俯首拜年。

 

2018年,淘宝为春晚准备了三倍于“双11”的服务器资源。而就在主持人口播活动开始的一瞬间,服务器瞬间超过负荷。事实证明,春晚观众肉身涌进淘宝服务器的瞬时流量是当年“双11”的15倍。

精明的腾讯阿里,都是提前三个月准备春晚,尚且如此狼狈。

而2019年,央视春晚红包招标时间很晚,距离除夕只有一个多月的时间。巨头们都觉得凶险异常,百度却高高举手:我来!我来!

所有吃瓜群众都侧目,这种“情商低”的状态,还真是百度的风格。。。

接下来中哥就告诉你,2019年2月4日除夕晚上,这片土地上究竟发生了什么。

编辑

请点击输入图片描述

(零)百度的一封信、一部引擎和一场战役 

故事,要从一封信说起。

2018年12月18日,李彦宏突然发布了一封内部信,宣布了一个神秘的“1218改革”。其中一条如下:

所有部门的基础技术整合到“TG”(基础技术体系),数据中心、基础架构、运维这些百度核心技术和技术大牛全部兵合一处将打一家。

在一般人眼里,这好像是个路人甲的架构调整;但技术人看后却内心一惊,百度正在把所有部门的核心发动机拆开,重新组装成了一个硕大无朋的“擎天柱”。

用破釜沉舟来形容,丝毫不过分。

而“擎天柱”的负责人,是十五年的老百度人,百度云的创立者之一:侯震宇。震宇隐隐然觉得“天将降大任”,但他又实在猜不透,自己手上这个变形金刚将会用什么姿势书写历史。

2019年1月4日,刚刚划归震宇部门的大牛架构师汪瑫从上海来到北京汇报。工作聊罢,汪瑫轻描淡写地告诉震宇一个“One more thing”。

“听说业务部门刚刚拿下了今年的春晚红包,你知道吗?”汪瑫说。

“什么??!!”震宇当时如同全身过电,血往上涌。就在前后脚,百度 App 部门联系震宇,确认了春晚红包的项目。

编辑

请点击输入图片描述

侯震宇

那一刻震宇意识到,“擎天柱”的第一战已经来了——在履职的第16天,组织架构都来不及调整,新任务也来不及制定,甚至连人都没认全的情况下,要负责保障一个“全中国人都对其威力一无所知”的春晚。

这还真是一个故事的好开头啊。

就在此时此刻,穿越几道门,坐在办公室里的“厂长”李彦宏一如既往地气定神闲,没人能看出他内心究竟是平静如水还是波涛汹涌。

过去几年,百度过得并不轻松。

公司的一些商业决策失误被人诟病,正在艰难地走出泥泞;而在百度文化里如同“定盘星”一样闪耀的技术人,也一边忍受着旁人的侧目,一边度过艰难的日子。

商业上的是非博弈,永远有回旋的余地。但技术人的心一旦散了,百度的未来将会被彻底改写。

李彦宏比任何人都清楚,对士兵最大的慷慨,就是为他们准备一个盛大的战场。

百度人渴望一场大战,洗刷十年风尘。

编辑

请点击输入图片描述

李彦宏

(一)生死战 

不用多想,你都能把百度的春晚发红包的姿势猜得八九不离十:

1、时间:春晚期间,分几轮发红包;

2、地点:打开“百度 App”;

3、人物:全体中国人;

4、动作:点点点点点。

万万想不到,就是这么简单的四步,引发了接下来整整一个月惊天动地血雨腥风的故事。

说回当时。

远在三亚休假的百度信息流主任架构师吴永巍也同时接到这个消息,一刻不停地赶回他所在的上海研发中心,又赶最早的飞机降落北京。

编辑

请点击输入图片描述

吴永巍

震宇代表基础技术保障团队,吴永巍代表百度 App 的技术团队组成了联合作战组。他们一秒都不敢耽搁,当天就开始筹备组建春晚红包技术团队。

贺峰,十一年百度人,运维负责人,他责无旁贷地成为“百度春晚红包”稳定性计划总制定者。

 

陈曦洋,十一年百度人,系统性能优化大神,他的能力是像庖丁解牛一样把一个 App 拆解成细碎的零件,为每一个零件做细致的优化,使得 App 所需的系统网络资源降低到理论最低极限。

 

汪瑫,七年百度人,不仅是技术大牛,还和百度登录帐号团队非常熟悉。

 

宋磊,十年百度人,系统部网络组专家,主要工作是维护百度网络的稳定运行。

 

张家军,八年百度人,系统部供应链负责人,百度平日所需的新增服务器,都由他来搞定。

 

等等等等,后面的名单还有长长一串。

所有的技术人,听到“春晚任务”,第一反应都是懵逼。因为他们的很多朋友都在阿里、腾讯,这些人曾经哭诉春晚有多么残忍的场景还历历在目。风水轮流转,这下轮到自己了。。。

但是,几乎只有一秒钟的迟疑,他们又变得像孩子一样兴奋。

外界不总是说,百度在移动时代不行了么?但是,行不行别人说了可不算。十年了,我们终于不用再等了。厂长既然给我们这次机会,我倒要证明给所有人看,我们到底是行还是不行!

贺峰对所有人说。

编辑

请点击输入图片描述

贺峰

很快,经过全部专家组讨论,贺峰把最终的计划表拿出来了。(原任务表极其复杂,中哥用自己的话解释给你。)

任务一:陈曦洋“拆解”百度 App 的每一个零件,精确计算出当天每一个 App 将会发出多少流量,进而计算出春晚时百度所需准备的总资源数量。(时间:第一周。)

任务二:贺峰亲自操刀,制定“春晚剧本”。春晚当天,百度集团所有系统资源全部切换档位,其他系统让位给红包系统。红包抢完,要在最短时间内再把系统资源还给其他部门。为此,需要一个极其精密无缝切换的操作预案。(时间:第一周+第二周。)

任务三:由于百度内部所能协调出来的系统资源不够,张家军要向全国厂商发出数万台服务器的紧急采购,确保打仗所需的全部“粮草”及时到位。(时间:第二周+第三周。)

任务四:宋磊向运营商全面采购带宽等软资源,建设足够的 IDC 网络  CDN 网络。(时间:第二周-第四周。)

任务五:陈曦洋协助百度 App 团队,百度 App 每秒数据传输量降到最少,然后打包成最新版百度 App 下发,确保春晚当天每个人手上的百度 App 都是性能最佳的最新版。(时间:第二周+第三周。)

任务六:陈曦洋协助小程序团队,一起开发春晚当天红包的小程序,确保这个小程序也占用最少的资源。(时间:第四周)

任务七:汪瑫负责协助百度帐号登录体系(Passport)进行全方位加固,应对春晚涌来的登录请求。(时间:第一周直到春晚。)

任务八:通知各大第三方应用市场,告诉他们春晚的时候,可能会有大量用户去下载百度 App,让他们也做好准备。

任务九:所有资源+调优全部到位,联合百度 App 的业务部门进行四轮“全链路联合测试”。(时间:第四周。)

任务十:春晚当天,所有人全力以赴,抗住十三亿人的流量海啸

十大任务列阵于此,旌旗猎猎。

编辑

请点击输入图片描述

彼时,即使是计划制定者贺峰也难以想象,在未来四周时间里,百度、三大运营商、全球硬件供应链、中国数家服务器厂商、外包建设团队、机房运维团队、全世界总计数万人和他们的家人将会为此付出怎样艰苦卓绝的努力。

但此时此刻,已经没人能阻挡这一切发生了。

这将是我职业生涯的生死战。

贺峰说。

(二)太空变轨 

计算结果出来了!我预计春晚的流量,每秒峰值将会达到5000万次!每分钟的峰值将会达到10亿次!

陈曦洋一夜没睡,拿着最新出炉的报表对贺峰说。

编辑

请点击输入图片描述

为了测算流量,陈曦洋研究了不少腾讯、阿里红包相关新闻。

经过计算,支撑这些流量的云计算系统,需要由10万台服务器组成。

这意味着什么呢?

2018年全年,全中国960万平方公里销售的全部服务器是300万台。而百度需要在一个月内,毫无准备的情况下,搞定去年全国销量的三十分之一,并且完成采购、生产、调试、接入百度云的全过程。

贺峰知道,如果全靠临时采购,这件事不可能完成。百度内部必须让出至少5万台服务器来支持春晚红包计划。

“凤巢广告系统、原生广告系统、网盟变现系统,统统要在春晚过程中熄火,把资源让给红包系统。”贺峰的计划白纸黑字。

要知道,凤巢系统是百度的广告收入核心。凤巢暂停四小时,意味着百度这艘火箭要在万米高空强行熄火,失速四小时。

编辑

请点击输入图片描述

而这不仅是真金白银的损失,还是一个极其危险的操作——万一熄火之后点不着,凤巢系统将面临对数百万客户的巨额赔偿。

当贺峰把计划拿给凤巢负责人王岳的时候,内心是很打鼓的,他知道对方有一万个理由拒绝他的计划。但是,王岳仅仅花十分钟看完了方案,说了三个字:没问题!

“没问题”,意味着一部包含5万台服务器的超级引擎将会在春晚四小时交由贺峰团队驾驶,这一下就满足了计划所需的一半。

剩下的5万台,交给采购部门,这个等下再说。回来继续看陈曦洋。

10万台服务器已经是团队的极限了。但是陈曦洋知道,把服务器总数控制在10万台,这还是有前提的。前提就是每个用户手机上的百度 App 还需要进行大量优化。

当陈曦洋带着三个兄弟完成对百度 App 的解析之后,他的眼泪都快流下来了。

百度 App 在启动时,会对自家服务器发送100多个连接。这些连接来自于百度不同的业务团队。

 

陈曦洋一下就懂了。由于百度 App 是目前百度装机量最高的超级 App,是“全村人的希望”。所以很多业务和技术同学都把展示自己团队成果的模块挤进百度 App,只是希望自己的努力能够被更多的用户感受到。

这些年逆风行船,百度的技术产品人其实一直在用力。

编辑

请点击输入图片描述

但是对于此刻的陈曦洋来说,这意味着灾难。在春晚当天,每一秒钟,每一个连接都要乘以几千万人这么一个倍数,每个 App 100 个连接,每秒就是几十亿次连接,BAT 加在一起都必跪无疑。

只见陈曦洋,此时已经红了眼,变成了“操刀鬼”——对百度 App 所有连接大砍大杀。

1月10日,陈曦洋和指挥部的领导开会,大家一致认定,百度 App 对外连接的数量,要从100个砍到3个。

由于 AppStore 和各大安卓商店都需要有审核时间,百度 App 必须在春晚前两周发布最新版本,所以很多连接来不及修改背后的整体逻辑,只能用暂时抑制的方式来处理。

编辑

请点击输入图片描述

花开两朵,各表一枝。

那边“操刀鬼”陈曦洋正在为百度 App 瘦身,这边“拼命三郎”汪瑫已经代表项目组进驻了百度帐号体系团队。

多说一句,汪瑫本来属于上海团队,和春晚项目关系不大。但是由于他技术非常牛,又和帐号团队比较熟悉,被震宇和吴永巍“强行”拉来参与春晚计划。接到任务时他二话没说,第二天就飞到北京,开始了长达一个月的加班。

编辑

请点击输入图片描述

汪瑫

业内八卦显示:2018年,淘宝春晚发红包,最大的问题就出在了登录系统。

讲真,由于搜索业务天然不必须登陆操作。这么多年来,百度的帐号体系建设是弱于电商阿里和社交腾讯的。

祸不单行,就在接到春晚任务之前,帐号团队的技术负责人李盈沉浸在苦恼中,因为他团队的一位优秀的同学提出了离职。但是,就在接到任务的当天,那位原本想离职的同学找到李盈,说自己不走了。

“我想打仗。”他说。

百度 App 登录状态的比例比较低。但是抢红包的时候,技术上必须要登录才能知道给谁“结账”。所以汪瑫预计,春晚当天百度 App 会遭受一波剧烈的登录冲击。有多剧烈呢?预计会是平常登录峰值的 2500 倍。

这意味着将会有很多人请求短信验证码。

编辑

请点击输入图片描述

时间有限,汪瑫一边连夜协助帐号团队做各种优化,一边让团队向各大运营商和第三方服务商购买短信发送的服务。一时间,所有省市的三大运营商的短信发送能力,被百度一家预定一空。

但即使是这样,还是不能满足春晚当天的预计值,汪瑫紧急派出百度最好的工程师进驻第三方服务商,帮助他们优化代码,提高短信发送的能力。终于达到了预期数值。

所有工程师的这些操作都在短短两周内叠加在一起。对于百度来说,这是一场史诗级的“太空变轨”。

软件这边忙得热火朝天,转回头来看,还有5万台服务器的硬件缺口。所有人的目光都落在张家军身上。

编辑

请点击输入图片描述

张家军

(三)那个疯狂的夜晚 

贺峰看着张家军说:“不要有压力,我们这次肯定‘一战成名’。”

“此话怎讲?”张家军问。

“就是说,成功也会出名,失败也会出名。”

“。。。”

张家军盘点了一下手上的“数据中心地图”,他发现自己简直是太幸运了:

北京数据中心曾经预定了4万台服务器,约定春节前交货。此时已经有2万多台交付完毕,还剩1.8万台未交付。

于是,张家军的计划很快出炉:

1、催促北京数据中心在一周内交付1.8万台服务器。

 

2、拼尽全力,就是抢,也要在两周内抢来1万台新的服务器,放到南京数据中心。

这些服务器都会并入百度云统一调配。张家军发现,虽然百度云平时默不作声,但是这么多年对于技术的极致追求,越是在艰难的时刻越能体现出闪光的价值:

百度设计的服务器,大多是以整体机柜的方式制造的。也就是说,在服务器厂家出厂的时候,就已经是一台大机柜里面固定好30台服务器的形态了。

这意味着,百度云不需要像其他云计算厂商那样,一台服务器一台服务器地在现场安装,而是把整个机柜直接推进去就可以进行测试安装了。

编辑

请点击输入图片描述

这就是整体机柜在安装时的场景

在接下来的一周里,百度的合作伙伴浪潮也体现出了国际顶尖的专业精神,一队队卡车整齐地并入高速公路,直接开赴北京。

你可能不相信,这两个大厂,共同创造了8小时安装1万台服务器的世界纪录。

编辑

请点击输入图片描述

北京如期搞定。

但最让张家军头疼的是,如何凭空变出来南京机房的1万台服务器。

他给我算了一笔账:

电子行业的硬件备货周期,通常就是8-12周。也就是说,你要至少提前两三个月向服务器生产商和零部件供应商下订单。

 

即使是服务器厂商完全停掉手中其他的活,产能也是有限的。从接到订单到生产出来,一般也要一周时间。

 

从卡车从服务器厂商的车间开出来,到百度数据中心,一般需要3-5天时间。

 

从服务器检测安装,到并网调试,一般需要1-2天。

“我接到任务的时间是1月6日,我的任务是:1月21日早晨8点,1万台服务器要一台不少齐装满员站在南京机房里。”张家军看着我,一字一顿地说。

“对不起,我们的生产能力不够,不能耽误百度春晚这么大的事儿,这个单子我们不敢接。”这几乎是所有服务器生产商对于张家军的标准回复。

只有几家大型服务器生产厂商接受订单,但是,限于原料储备不足,他们未来两周的生产能力上限是——4000台。

这下张家军的任务变成了:帮助服务器厂商协调全球供应链,从全世界找到另外几千台服务器的所有配件。

如果换成别人,任务到此已经可以宣告失败了。

但是张家军不准备认输。

过去几年,他和同事们拼命了解产业链的运作方式,和各大厂商建立联系,一点一点构建百度云的基础设施。他们赌上了自己的青春和年华,却依然能听到外界很多讥讽的声音。这么多年都扛过来了,就是因为他们相信,有朝一日,所有的证明都会如数归来,那些不该他们承受的东西,终会像雾霾一样退散。

此时此刻,他怎么舍得认输。

他和团队给全球的供应商一个一个打电话,买最近的一班飞机飞到国外,到每一个工厂里查看零件配给数量。一天,两天,三天,这些原本计划分配给美国、欧洲的零部件,从以色列、美国、东南亚调转航向,一起向中国飞来。

这是一场全球供应链的胜利。

服务器厂商开足马力,所有工人放弃了提前回家的计划,回到岗位三班倒,服务器被源源不断地生产出来。卡车等在工厂门口,放弃编队,装满一辆车就出发一辆,在通往南京的高速公路上,每隔一百公里就有一辆满载服务器的卡车在飞驰。

南京数据中心里,百度的工作人员、机房运维人员、建设外包队伍已经严阵以待。

张家军在北京总部协调,不能脱身,于是他和南京24小时通着电话,时刻指挥进展。服务器进驻机房, 还要有调试的过程,但凡有哪一步出现差错,就会导致满盘皆输。所以,张家军团队为每一种想到的意外都做了周密的预案。

1月20日夜里,最大一波服务器抵达南京,眼看大功告成。此时,一个最不可能发生的意外,却真的发生了。

机房的货梯,由于承受不住一吨多的机柜上下折磨,毫无预警地罢工了。他们马上转战客梯,客梯很快也出现故障。

编辑

请点击输入图片描述

有人拍下了当时紧急修复电梯的场景

临近春节,电梯检修人员大多回家过年,人手非常紧张,要天亮才能赶到。但是百度的同学们知道,仅仅这一夜,他们也是等不起的。

漆黑的夜里,所有现场的人员,用双手和双肩扛起来一百多斤的服务器,一步一步地从楼梯往上爬。

当时一位同学用视频记录下了现场的场景:

人肉抬服务器上楼

有同学的手被划破了,血珠渗出来。同事要把它换下来,他只是摇头,说没事。有人看到了他眼角的泪水。

现场还有很多一吨多的整机柜服务器,靠人力根本搬不上楼。现场指挥部的同学,甚至叫来了一辆吊车,要砸开机房的窗户运进去。

张家军在视频里看到现场吊车的钩子在风中摇晃,惊出一身冷汗。他怕同学受伤,言辞拒绝了这个方案。但是现场的负责人反复恳求:你就让我们试试吧!服务器要上楼啊!!

终于,现场维修团队想到了一个办法,紧急把隔壁楼的电梯配件拆过来,货梯终于缓缓启动,那时,已经是半夜两点了。

2019年1月21日早晨八点,负责服务器软件调试的同学如期赶到,所有的服务器静静地站在机房里。它们就那样沉默着,仿佛昨夜什么都没发生。

(四)核弹头就位 

1月17日,百度召开总监会。

贺峰回忆,李彦宏的表情“非常淡定”。他只是笑眯眯地看着大伙儿,说:“你们一定能搞成的。”这次总监会只是比平常多了一个小环节,晚上大家一起吃了个饭,李彦宏挨桌给大伙敬了酒。

实际上,那几天正是所有团队最焦头烂额的时候。

震宇告诉我,就在这一个月的时间,百度的 IDC 新增带宽资源超过了过去20年的历史总和,CDN 资源新增了2018年的一半。

根据宋磊回忆,两周左右的时间里,69位工程师飞了7万多公里。北上广的剩余带宽资源几乎都被百度拿空了。

编辑

请点击输入图片描述

宋磊

和他的朋友可乐君

网络团队有两位工程师是夫妻,一个负责网络建设,一个负责网络测试。一个人刚建设好,回家看孩子,换另一个人来测试。有时候建设工程师要陪着测试工程师一起工作,两个人只好把孩子扔给老人,一夜都不回家。

而宋磊本人,从1月6日计划启动到大年初一,睡得最长的一晚是五个小时,最短只有一个小时。

编辑

请点击输入图片描述

一位同学躺在桌子上睡觉,被无情偷拍。

时间一刻不停,已经是1月26日。距离春晚还有8天。

这一边,张家军的“粮草”已经全部到位,工程师也把所有的服务器完整无误地接入百度云,10万台机器全副武装准备就绪。宋磊新建了相当于支撑全澳洲人口同时观看视频的 CDN 和 IDC 网络,并且连续几个通宵完成了压力测试。

另一边,陈曦洋成功地把百度 App 对外连接数从100个砍到3个,汪瑫把百度 App 的登陆能力从1500人次每秒疯狂提升到15万人次每秒。

百度开启了“春晚红包计划”全链路测试。

贺峰为这四个半小时制定了几百页的“剧本”。剧本分为两部分:

1、主持人播报抢红包开始的每一个时间点,百度系统分别提前多少秒做好什么准备。就像火箭发射一般精密。

 

2、一旦遇到意外情况,哪个子系统要做出怎样的调整,根据意外程度的不同,做出的调整力度也不同,这套预案中,涉及到了上千条意外情况。一旦条件触发,指挥部的同学只要点击一个按钮,就能启动相应的更改。

编辑

请点击输入图片描述

这就是“剧本”

根据经验,运维人员真正遇到问题的时候,心理会承受巨大的冲击,很容易慌乱。为了让同学们临危不乱,贺峰还专门编写了《作战守则》,上面写着“指令要清晰,行动听指挥”等等要求贴在墙上,发给每一个同学提前学习。

编辑

请点击输入图片描述

对于百度这群工程师来说,这几百页作战计划里的每一条预案,都不是凭空想出来的。他们在写每一个字的时候,都可以回忆起十多年来自己在百度的运维经验,这厚厚一本,哪里是作战计划,分明是一个百度工程师的技术人生。

百度科技园 K2 大楼的整整一个大厅被改成作战指挥室,中心一个核心指挥室,旁边20个小屋是包括百度 App、大搜、摇一摇、帐号系统、BFE中台、网络、系统监控、IT团队、红包核心系统等等在内的分组作战室。加上地下一层本来就有的中控室,组成了联合指挥作战系统。

编辑

请点击输入图片描述

核心指挥室

这里要稍微插一句。

2019年百度春晚的红包设计和之前阿里腾讯的稍有不同。他们把百度 App 日常的功能和红包相结合。比如你要在百度搜索框里语音说出“幸福快乐年”,或者手动刷一下百度 App 首页的新闻瀑布流查看“拜年视频”后才能进入,这就造成了用户行为的不可预测性加大。

这就形成了一个开放空间:难免有人说出方言,或者刷新瀑布流搞错了方向,从而对后台的人工智能系统带来不可预知的巨大压力。

这会导致测试的时候,非常难以模拟春晚真实场景。

百度人的“实在”在这里体现得淋漓尽致。反正我就是要实现这样的效果,技术上的问题,一点点搞就好了。

编辑

请点击输入图片描述

必不可少的步骤:拜杨超越

演习过程中,专门有一队“蓝军”,负责为春晚系统制造各种麻烦,例如掐断某个机房的数据通路,让某一个模块停止响应,甚至直接对百度系统发起攻击。而在另一边,指挥团队严格按照剧本对所有问题瞬间应对。百度安全团队也加入了护航编队,对夹杂在正常访问之间的进攻进行拦截。

虽然中间几经波折,但是在倒数两次联合测试中,整个百度春晚红包系统都经受住每秒5000万次访问的考验。所有人悬了一个月的心,这才稍稍放下一些。一向严谨的吴永巍对团队成员说,我现在的信心指数是85-90分!

就在大家紧锣密鼓忙活的时候,发生了两件怪事:

宋磊在那几周新增了一个习惯:每天半夜两点把自己加班的情况拍照,晒到朋友圈。当时大家还纳闷,为什么老宋那么低调的一个人,却要天天晒加班呢?

 

从上海过来支援的汪瑫,1月24日神秘消失了一天。他究竟去哪了呢?

这些小八卦,当时谁也没空探秘。因为春晚已经近在眼前了。

编辑

请点击输入图片描述

(五)除夕 

2月4日,除夕。

从当天零点开始,已经有同学在作战部值班。早晨八点,全员就绪,大战一触即发。

所有百度的同学里,有两位是最为特殊的。他们当天晚上会进驻到央视直播现场。人们开玩笑说,他们是百度押在央视的“人质”。这两位同学在去之前还满怀激动地打听,我们去了要做什么呢?其他人冷冷地说,根据腾讯和阿里的经验,你们去了只有一个任务:我们这里如果砸了,你们两个负责“挨骂”。

临走时,这两个同学用幽怨的眼神看了一眼百度大部队,决绝地赶赴央视。嘴里唱着“风萧萧兮易水寒。。。”

根据设计,在除夕当天上午11点,百度会向用户推送一个小红包活动作为预热,让真实的用户来参与,从而对系统进行一波实打实的终极测试。

11:00,预热活动开始,后台数据直线上升。百度 App 瞬间访问峰值达到88万次每秒。这个数值已经是百度 App 历史最大峰值的几十倍。但是贺峰知道,这还仅仅是毛毛雨,他们为春晚设计了5000万次每秒的能力。

直到这时,陈曦洋所负责的重要任务——掐断百度 App 的多余回连数据突然有所抖动。陈曦洋和百度 App 的技术同学各个满头大汗,直到直播前一个小时,才把问题解决妥当。

晚上八点,春晚准时开播。

凤巢系统缓缓熄火,红包系统接管引擎驾驶。所有系统齿轮咬合,像起跑线前的赛车一样低吼着冲出去。

编辑

请点击输入图片描述

作战室的同学仿佛进入了另一个世界。整个大厦,掉一根针在地上都能听到。

按照央视彩排的时间表,第一次摇红包应该发生在晚上八点半。但是,就在八点十八分的时候,主持人突然提前预告了一下:“观众朋友们可以下载百度 App 参与今年的春晚摇红包活动。”

这之后一分钟,指挥部的舆情监控群里,突然有人甩进来一张图片:

编辑

请点击输入图片描述

苹果的 AppStore 被网友挤垮,已经打不开了。

陈曦洋赶紧拿出手机测试,哪里是 AppStore,小米、华为等等 AppStore 全部躺尸。他这才明白,虽然当时自己派人和各大应用商店提前打过预防针,但事实证明,他们对春晚“一无所知”。。。

作战组马上给出数据,预计全国有200万-300万人无法下载百度 App,这将带来不小的损失。而另一组数据显示,无法下载 App 的人们又涌向了百度搜索,在手机浏览器里用关键词搜索的方式尝试下载百度 App。

贺峰评估了一下百度 CDN 的占用量,马上下令,把链接指向百度自家的下载接口,让大家不用通过第三方市场,而是直接从百度家下载百度 App。就这样,下载高峰直接冲击百度自家网络,一点一点,几百万人都安装了百度 App。

编辑

请点击输入图片描述

这一切被汪瑫看在眼里。

他的第一反应是:我厂真牛逼。他的第二反应是:这300万人下载了 App 之后,肯定是要登录的啊,我负责的登录系统看来马上就要挂了。。。。

还好,有惊无险,大家分散下载了 App,也就分散登录 App,并没有对登录系统带来致命的打击。

随着春晚的进行,访问百度 App 的流量一轮比一轮大,逼近预计中的峰值。

百度20年积累下来的遍布全国的数据通路,顺利扛过了前三轮红包的数据洪峰,只剩零点钟声敲响前的最后一次。根据预测,这将是最大的一波浪潮。

在最后一次红包到来之前23分钟,贺峰突然接到驻场春晚那两个同学的消息:根据测算,春晚比预计延迟了4分钟。

编辑

请点击输入图片描述

贺峰心里咯噔一下。

这会造成百度 App 红包开抢的时间早于主持人播报的时间。也就是说,听到主持人播报才进来的用户,很可能发现红包已经被准点动手的用户抢完了。。。

贺峰要做一个决定:是保持原计划放开红包,还是要按照春晚的进度延后 App 上红包开抢的时间。

把这么大规模的调整部署到10万台服务器上,起码需要五分钟。

所有人都看着贺峰。贺峰盯着屏幕,两手一压,对大伙说:再等等。

就在红包开启前十分钟,贺峰判断央视应该是无法抢回时间了,下令马上对10万台服务器发出指令,延后4分钟开启红包系统。

上天眷顾,最后百度 App 红包开启的时间,和主持人宣布红包开抢的时间严丝合缝。

一瞬间,上亿人手机屏幕上显示着百度 App 的红包界面,巨大的数据浪潮涌向北京和南京两地的数据机房。那一刻,百度大厦里1000多位同事,百度散落在各地机房的100位同事,带着备用零件守候在机房的100多位服务器厂商的工程师,三大运营商为了保护网络通畅而留守在各地机房的1000多位同事,那些中国通信行业和互联网行业的梦想者,用自己的付出搭建出了人类历史上最宽的信息通路。

暗夜无声,大地上烟花四起。

这个古老的民族,迈入了新的一年。

(六)那些小事 

燃烧了四个半小时的红包系统渐渐熄火。

间断了四个半小时的凤巢系统缓缓启动。

伴随着10万台服务器的嘶嘶声,百度这架火箭完成了太空中的第二次惊险变轨。

指挥部里,欢声雷动。

编辑

请点击输入图片描述

直到这个时候,吴永巍、震宇、贺峰、陈曦洋他们才敢确定,这个曾经击垮了阿里巴巴和腾讯的春晚,并没有击垮百度。他们用了三十个日夜,证明了自己是当之无愧的“老司机”;证明了自己对春晚并不是一无所知。

可叹的是,如此宏大的工程,调动了全球的供应链体系,调动了全中国的网络带宽,调动了全百度的技术资源。中间如果百度系统有任何一个技术环节卡住,百度云的服务器有任何一个螺丝钉松动,百度工程师、商务采购团队有任何一个人掉链子,全中国人都会面对一个完全不同的结局。

百度无疑是幸运的,但这世界上,只有勇者才有资格谈运气。

我听到很多百度同学回忆起那个瞬间,他们描绘的,正如《少林足球》里那个场景:

历经磨难,少林师兄师弟的武功全部觉醒,周星驰跪在地上,说:欢迎各位师兄弟归位!

编辑

请点击输入图片描述

直到正月末,我来到百度科技园来采访春晚红包故事的时候,我依然能够感觉到,在大厦里洋溢着的喜悦气氛。

哦对了,还有一些小事忘记交代。

那两个被“质押”在央视的小伙伴,期待中的指责并没有来,反而被冲进来的央视同事们握着手说:百度真是太牛了,你们是第一个没有出现问题的合作伙伴。你们不是有 AI 技术吗?你们不是有视频技术吗?合作起来呀~

编辑

请点击输入图片描述

而好奇的人们,也终于打听到了前几天神秘事件的原因:

宋磊羞涩地表示,之所以每天凌晨两点发朋友圈,是因为他怕老婆想多了,晒朋友圈以证清白。。。

而汪瑫消失的那一天,是飞回上海给儿子过两岁生日。因为去年儿子一周岁的生日时,他就在外地加班,没能陪他。这次儿子喊着要爸爸,他才连夜飞回去一天。第二天给儿子过完生日,晚上八点他陪儿子上床睡觉,确定儿子睡熟了,才赶紧跑出来,赶十点的飞机飞回北京。

而且,除夕那天其实是吴永巍的生日。但直到整个春晚战役结束,第二天凌晨他才告诉几个小伙伴这件事儿。“我和兄弟们过了最有意义的一个生日。”他说。

离别之前,我颇为郑重地问陈曦洋:“为百度付出了这么多,你觉得值得吗?”

“我只是想证明百度。”陈曦洋说完,沉默良久。

十一年前,我一毕业就加入百度,我的所有技术积累都是百度给我的。我把百度当成家。之前,百度遇到很多问题,我比外面的人更难受,就像对自己的孩子一样,恨铁不成钢。

 

我记得,在2016年百度最困难的时候,我们团队去杭州团建,秋天的杭州特别美。有一位家就在杭州附近的同学,回来之后就辞职去了杭州。

 

我知道,大家都有权做出自己的选择。从2016年到现在,几乎每周都有猎头给我打电话,但我从来都是直接挂掉。我觉得,总有人要让百度变得更好,而我应该是那些人中的一员。

他就这么安静地说着。

听到这里,我突然明白,在春晚红包成功的那一刻,那些百度技术人眼里的泪花,告慰的不是过去一个月的艰苦卓绝,而是十年前的自己,那个不解风情却无问西东的少年。

编辑

请点击输入图片描述

陈曦洋

他们在漫长的岁月里安静地忍受着孤独,这一次却像疯子一样拼尽全力,押上全部韶华和热血。他们也许并没想赢,他们,只是不想输。

有人说,生活是一场战役,结局或是千秋标榜,或是万古遗憾。

但我更相信,岁月是一张长长的考题,没人会逼你交卷。

你尽可以用一生的时间,慢慢给出自己的答案。

编辑

请点击输入图片描述

再自我介绍一下吧。我是史中,是一个倾心故事的科技记者。我的日常是和各路大神聊天。如果想和我做朋友,

可以搜索微信:shizhongpro

或者关注微博:@史中方枪枪 @浅黑科技

不想走丢的话,你也可以关注我的公众号“浅黑科技”。(记得给浅黑加星标哦)

编辑

请点击输入图片描述

岁月

是一张长长的考卷

编辑

请点击输入图片描述

阅读原文

阿里云CentOS 7上安装配置Docker

Docker 是一个开源工具,它可以让创建和管理 Linux 容器变得简单。容器就像是轻量级的虚拟机,并且可以以毫秒级的速度来启动或停止。Docker 帮助系统管理员和程序员在容器中开发应用程序,并且可以扩展到成千上万的节点。

1

这是一只鲸鱼,它托着许多集装箱。我们可以把宿主机可当做这只鲸鱼,把相互隔离的容器可看成集装箱,每个集装箱中都包含自己的应用程序。

Docker与传统虚拟区别

传统虚拟化技术的体系架构:

2

Docker技术的体系架构:

3

容器和 VM(虚拟机)的主要区别是:

  • 容器提供了基于进程的隔离,而虚拟机提供了资源的完全隔离。
  • 虚拟机可能需要一分钟来启动,而容器只需要一秒钟或更短。
  • 容器使用宿主操作系统的内核,而虚拟机使用独立的内核。

Doker 平台的基本构成

4

Docker 平台基本上由三部分组成:

  • 客户端:用户使用 Docker 提供的工具(CLI 以及 API 等)来构建,上传镜像并发布命令来创建和启动容器
  • Docker 主机:从 Docker registry 上下载镜像并启动容器
  • Docker registry:Docker 镜像仓库,用于保存镜像,并提供镜像上传和下载
  • 后面的文章会具体分析。

Docker 容器的状态机

5

一个容器在某个时刻可能处于以下几种状态之一:

  • created:已经被创建 (使用 docker ps -a 命令可以列出)但是还没有被启动 (使用 docker ps 命令还无法列出)
  • running:运行中
  • paused:容器的进程被暂停了
  • restarting:容器的进程正在重启过程中
  • exited:上图中的 stopped 状态,表示容器之前运行过但是现在处于停止状态(要区别于 created 状态,它是指一个新创出的尚未运行过的容器)。可以通过 start 命令使其重新进入 running 状态
  • destroyed:容器被删除了,再也不存在了

Docker 的安装

RedHat/CentOS必须要6.6版本以上,或者7.x才能安装docker,建议在RedHat/CentOS 7上使用docker,因为RedHat/CentOS 7的内核升级到了kernel 3.10,对lxc容器支持更好。

查看Linux内核版本(内核版本必须是3.10或者以上):

cat /proc/version

uname -a

lsb_release -a

##无法执行命令安装
yum install -y redhat-lsb

更新YUM源:

yum update

安装:

yum  install docker -y

检查版本:

docker -v

安装完成后,使用下面的命令来启动 docker 服务,并将其设置为开机启动:

service docker start
chkconfig docker on

下载官方的 CentOS 镜像:

docker pull centos

检查CentOS 镜像是否被获取:

docker images

下载完成后,你应该会看到:

[root@iZ2ze74fkxrls31tr2ia2fZ ~]# docker images centos
REPOSITORY       TAG        IMAGE ID     CREATED         SIZE
docker.io/centos latest    3fa822599e10    3weeks ago   203.5 MB

如果看到以上输出,说明你可以使用“docker.io/centos”这个镜像了,或将其称为仓库(Repository),该镜像有一个名为“latest”的标签(Tag),此外还有一个名为“3fa822599e10 ”的镜像 ID(可能您所看到的镜像 ID 与此处的不一致,那是正常现象,因为这个数字是随机生成的)。此外,我们可以看到该镜像只有 203.5 MB,非常小巧,而不像虚拟机的镜像文件那样庞大。

启动容器:

docker run -i -t -v /root/software/:/mnt/software/ 3fa822599e10 /bin/bash

docker run -ti ubuntu:14.04 /bin/bash

命令参数说明:
docker run <相关参数> <镜像 ID> <初始命令>

  • -i:表示以“交互模式”运行容器
  • -t:表示容器启动后会进入其命令行
  • -v:表示需要将本地哪个目录挂载到容器中,格式:-v <宿主机目录>:<容器目录>

Docker命令

我们可以把Docker 的命令大概地分类如下:

镜像操作:
    build     Build an image from a Dockerfile
    commit    Create a new image from a container's changes
    images    List images
    load      Load an image from a tar archive or STDIN
    pull      Pull an image or a repository from a registry
    push      Push an image or a repository to a registry
    rmi       Remove one or more images
    search    Search the Docker Hub for images
    tag       Tag an image into a repository
    save      Save one or more images to a tar archive 
    history   显示某镜像的历史
    inspect   获取镜像的详细信息

    容器及其中应用的生命周期操作:
    create    创建一个容器
    kill      Kill one or more running containers
    inspect   Return low-level information on a container, image or task
    pause     Pause all processes within one or more containers
    ps        List containers
    rm        删除一个或者多个容器
    rename    Rename a container
    restart   Restart a container
    run       创建并启动一个容器
    start     启动一个处于停止状态的容器
    stats     显示容器实时的资源消耗信息
    stop      停止一个处于运行状态的容器
    top       Display the running processes of a container
    unpause   Unpause all processes within one or more containers
    update    Update configuration of one or more containers
    wait      Block until a container stops, then print its exit code
    attach    Attach to a running container
    exec      Run a command in a running container
    port      List port mappings or a specific mapping for the container
    logs      获取容器的日志

    容器文件系统操作:
    cp        Copy files/folders between a container and the local filesystem
    diff      Inspect changes on a container's filesystem
    export    Export a container's filesystem as a tar archive
    import    Import the contents from a tarball to create a filesystem image

    Docker registry 操作:
    login     Log in to a Docker registry.
    logout    Log out from a Docker registry.

    Volume 操作
    volume    Manage Docker volumes

    网络操作
    network   Manage Docker networks

    Swarm 相关操作
    swarm     Manage Docker Swarm
    service   Manage Docker services
    node      Manage Docker Swarm nodes

    系统操作:
    version   Show the Docker version information
    events    持续返回docker 事件
    info      显示Docker 主机系统范围内的信息
# 查看运行中的容器
docker ps

# 查看所有容器
docker ps -a

# 退出容器
按Ctrl+D 即可退出当前容器【但退出后会停止容器】

# 退出不停止容器:
组合键:Ctrl+P+Q

# 启动容器
docker start 容器名或ID

# 进入容器
docker attach 容器名或ID

# 停止容器
docker stop 容器名或ID

# 暂停容器
docker pause 容器名或ID

#继续容器
docker unpause 容器名或ID

# 删除容器
docker rm 容器名或ID

# 删除全部容器--慎用
docker stop $(docker ps -q) & docker rm $(docker ps -aq)

#保存容器,生成镜像
docker commit 容器ID 镜像名称

#从 host 拷贝文件到 container 里面
docker cp /home/soft centos:/webapp

docker run与start的区别

docker run 只在第一次运行时使用,将镜像放到容器中,以后再次启动这个容器时,只需要使用命令docker start 即可。

docker run相当于执行了两步操作:将镜像放入容器中(docker create),然后将容器启动,使之变成运行时容器(docker start)。

6

而docker start的作用是,重新启动已存在的镜像。也就是说,如果使用这个命令,我们必须事先知道这个容器的ID,或者这个容器的名字,我们可以使用docker ps找到这个容器的信息。

7

因为容器的ID是随机码,而容器的名字又是看似无意义的命名,我们可以使用命令:

docker rename jovial_cori  centos

给这个容器命名。这样以后,我们再次启动或停止容器时,就可以直接使用这个名字:

docker [stop] [start]  new_name

而要显示出所有容器,包括没有启动的,可以使用命令:

docker ps -a

Docker配置

更改存储目录:

#复制docker存储目录
rsync -aXS /var/lib/docker/. /home/docker

#更改 docker 存储文件目录
ln -s  /home/docker  /var/lib/docker

获取IP:

docker inspect <container id>

要获取所有容器名称及其IP地址只需一个命令:

docker inspect -f '{{.Name}} - {{.NetworkSettings.IPAddress }}' $(docker ps -aq)

docker inspect --format='{{.Name}} - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(docker ps -aq)

Docker 镜像加速器

注册个帐号

https://dev.aliyun.com/search.html

阿里云会自动为用户分配一个镜像加速器的地址,登录后进入”管理中心”–>”加速器”,里面有分配给你的镜像加速器的地址以及各个环境的使用说明。

镜像加速器地址:https://xxxxx.mirror.aliyuncs.com

如何配置镜像加速器

针对Docker客户端版本大于1.10.0的用户
您可以通过修改daemon配置文件/etc/docker/daemon.json来使用加速器:

{
    "registry-mirrors": ["<your accelerate address>"]
}

重启Docker Daemon:

sudo systemctl daemon-reload
sudo systemctl restart docker

后续:
1,查看镜像
docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1fef6edc3f23 docker.io/centos "bin/bash" About a minute ago Exited (0) 8 seconds ago affectionate_khorana
66f6e4ff1098 docker.io/centos "bin/bash" About a minute ago Exited (0) About a minute ago elastic_khorana
a44b68ea8265 75835a67d134 "/bin/bash" 7 minutes ago Exited (0) 2 minutes ago angry_meitner
d0c8cbacb8ce 75835a67d134 "/bin/bash" 25 minutes ago Exited (0) 13 minutes ago mystifying_perlman
2.更新镜像
docker commit #第一列容器ID centos:update
3.保存镜像
docker save centos:update > centos.tar
4.加载镜像
docker load < centos.tar



常见命令:

1,后台启动docker并且端口映射

docker run -itd -p 3390<物理机端口>:3390<容器端口> cd3c0fd5029a /bin/bash

2,进入docker

docker attach <容器ID>

3,退出docker,不关闭container

ctrl+p ctrl+q

4.更新镜像

docker commit <容器ID> centos:update<自己定义的名字:标签>

5.保存镜像

docker save centos:update > centos.tar

6.加载镜像

docker load < centos.tar

Mycat【数据库方式】实现全局序列号

说明:本文参考mycat官方提供的文档,结合自己的实践以及理解,做出如下整理,并附带一个分库分表的插入数据例子。
原理
在数据库中建立一张表,存放sequence名称(name),sequence当前值(current_value),步长(increment int类型每次读取多少个sequence,假设为K)等信息;
Sequence获取步骤:
1)当初次使用该sequence时,根据传入的sequence名称,从数据库这张表中读取current_value,和increment到MyCat中,并将数据库中的current_value设置为原current_value值+increment值;
2)MyCat将读取到current_value+increment作为本次要使用的sequence值,下次使用时,自动加1,当使用increment次后,执行步骤1)相同的操作.
3)MyCat负责维护这张表,用到哪些sequence,只需要在这张表中插入一条记录即可。若某次读取的sequence没有用完,系统就停掉了,则这次读取的sequence剩余值不会再使用。
配置方式
server.xml配置:

<system><property name=”sequnceHandlerType”>1</property></system>
1
注:sequnceHandlerType 需要配置为1,表示使用数据库方式生成sequence.
数据库配置:
1)创建sequence表

CREATE TABLE MYCAT_SEQUENCE (
name VARCHAR (50) NOT NULL comment “名称”,
current_value INT NOT NULL comment “当前值”,
increment INT NOT NULL DEFAULT 100 comment “步长”,
PRIMARY KEY (name)
) ENGINE = INNODB;
1
2
3
4
5
6
2)创建相关function

#取当前squence的值
DROP FUNCTION IF EXISTS mycat_seq_currval;
DELIMITER $$
CREATE FUNCTION mycat_seq_currval(seq_name VARCHAR(50))RETURNS VARCHAR(64) CHARSET ‘utf8′
BEGIN
DECLARE retval VARCHAR(64);
SET retval=’-999999999,NULL’;
SELECT CONCAT(CAST(current_value AS CHAR),’,’,CAST(increment AS CHAR)) INTO retval FROM
MYCAT_SEQUENCE WHERE NAME = seq_name;
RETURN retval;
END$$
DELIMITER ;

#设置 sequence 值
DROP FUNCTION IF EXISTS mycat_seq_setval;
DELIMITER $$
CREATE FUNCTION mycat_seq_setval(seq_name VARCHAR(50),VALUE INTEGER) RETURNS VARCHAR(64) CHARSET ‘utf8’
BEGIN
UPDATE MYCAT_SEQUENCE SET current_value = VALUE WHERE NAME = seq_name;
RETURN mycat_seq_currval(seq_name);
END$$
DELIMITER ;

#取下一个sequence的值
DROP FUNCTION IF EXISTS mycat_seq_nextval;
DELIMITER $$
CREATE FUNCTION mycat_seq_nextval(seq_name VARCHAR(50)) RETURNS VARCHAR(64) CHARSET ‘utf8′
BEGIN
UPDATE MYCAT_SEQUENCE SET current_value = current_value + increment
WHERE NAME = seq_name;
RETURN mycat_seq_currval(seq_name);
END$$
DELIMITER ;
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
3)sequence_db_conf.properties相关配置,指定sequence相关配置在哪个节点上:
例如:

COMPANY=dn3
1
注:COMPANY为表名,必须大写,dn3为schema.xml配置的dataNode节点。建议专门独立一个数据库,存放sequence表和相关的function,方便维护管理和隔离。

注意:MYCAT_SEQUENCE表和以上的3个function,需要放在同一个节点上。function请直接在具体节点的数据库上执行,如果执行的时候报:
you might want to use the less safe log_bin_trust_function_creators variable
需要对数据库做如下设置:
windows下my.ini[mysqld]加上log_bin_trust_function_creators=1
linux下/etc/my.cnf下my.ini[mysqld]加上log_bin_trust_function_creators=1
修改完后,即可在mysql数据库中执行上面的函数.
使用示例:

SELECT next value for MYCATSEQ_SAM_TEST
insert into sam_test(id_,name_) values(next value for MYCATSEQ_SAM_TEST,’test’);
# 数据库表定义了自增,在mycat也定义了主键和自增,可以用如下方式
insert into sam_test(name_) values(‘test’);
1
2
3
4
测试
1.配置schema.xml

<schema name=”TESTDB” checkSQLschema=”false” sqlMaxLimit=”100″>
<table name=”company” dataNode=”dn1,dn2″ rule=”companyRule” primaryKey=”id” autoIncrement=”true” />
</schema>

<dataNode name=”dn1″ dataHost=”localhost1″ database=”mycat_test” />
<dataNode name=”dn2″ dataHost=”localhost1″ database=”mycat_test2″ />
<dataNode name=”dn3″ dataHost=”localhost2″ database=”testmycat” />

<dataHost name=”localhost1″ maxCon=”1000″ minCon=”10″ balance=”0″
writeType=”0″ dbType=”mysql” dbDriver=”native” switchType=”1″ slaveThreshold=”100″>
<heartbeat>select user()</heartbeat>
<writeHost host=”hostM1″ url=”192.168.1.95:3306″ user=”admin” password=”admin”/>
<writeHost host=”hostM2″ url=”192.138.1.112:3306″ user=”root” password=”root”/>
</dataHost>
<!– 存放sequence数据库 –>
<dataHost name=”localhost2″ maxCon=”1000″ minCon=”10″ balance=”0″
writeType=”0″ dbType=”mysql” dbDriver=”native” switchType=”1″ slaveThreshold=”100″>
<heartbeat>select user()</heartbeat>
<writeHost host=”localhost2M2″ url=”192.138.1.112:3306″ user=”root” password=”root”/>
</dataHost>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2.配置server.xml

<property name=”sequnceHandlerType”>1</property><!– 1:使用数据库方式生成sequence –>
1
3.配置rule.xml

<tableRule name=”companyRule”>
<rule>
<columns>id</columns>
<algorithm>mod-long</algorithm>
</rule>
</tableRule>
<function name=”mod-long” class=”org.opencloudb.route.function.PartitionByMod”>
<!– how many data nodes –>
<property name=”count”>2</property>
</function>
1
2
3
4
5
6
7
8
9
10
4.配置sequence_db_conf.properties

COMPANY=dn3
1
5.数据库配置文件修改my.ini

log_bin_trust_function_creators=1
# 忽略大小写
lower_case_table_names=1
1
2
3
6.数据库表
1)分别到192.168.1.95的mycat_test数据库和mycat_test2数据库新建如下的表,由于是分库分表,所以两边都要创建。

DROP TABLE IF EXISTS `company`;
CREATE TABLE `company` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
1
2
3
4
5
6
注:只有数据库和mycat都设置AUTO_INCREMENT才能通过mycat命令LAST_INSERT_ID()获取插入的id
2)到192.168.1.112的testmycat数据库中执行上面的创建sequence和function过程。
3)插入数据到MYCAT_SEQUENCE表

insert into MYCAT_SEQUENCE(name,current_value,increment) values(‘COMPANY’,19,5);
1
7.mycat测试
配置完之后,重启mycat
执行

insert into company(id,name) values (next value for MYCATSEQ_COMPANY,”test”)

insert into company(name) values (“test”)
1
2
3
插入数据成功后
执行

select LAST_INSERT_ID()
1
可以看到本次插入的id

小结
如果要获取插入数据后的id,必须同时在mysql和mycat设置表的自增。
sequence_db_conf.properties配置的表名必须大写。
存放sequence表和function在同一个数据库中,且只有一个。
以上【Sequence获取步骤】是mycat原理,注意理解。
———————
作者:黄晓杰Aries
来源:CSDN
原文:https://blog.csdn.net/u010956470/article/details/70837876
版权声明:本文为博主原创文章,转载请附上博文链接!

mycat配置及使用

Mycat数据库分库分表中间件

详细文档在http://www.mycat.io/

本次主要想做分库分表的操作,将mysql分别部署在不同的机器上,mycat作为Proxy。

安装非常简单,下载相应的包就行。但是需要安装最新版本的java


yum install java-1.8.0-openjdk-src.x86_64

之后需要进行三个配置文件的配置

1,server.xml


<property name="sequnceHandlerType">3</property>

 

2,schema.xml

<dataNode name=”dn1″ dataHost=”localhost1″ database=”tt” />
<dataNode name=”dn2″ dataHost=”localhost2″ database=”tt” />

<dataHost name=”localhost2″ maxCon=”1000″ minCon=”10″ balance=”0″
writeType=”0″ dbType=”mysql” dbDriver=”native” switchType=”1″ slaveThreshold=”100″>
<heartbeat>select user()</heartbeat>
<writeHost host=”hostS1″ url=”IP:PORT” user=”root”
password=”XXX” />
</dataHost>

<dataHost name=”localhost2″ maxCon=”1000″ minCon=”10″ balance=”0″
writeType=”0″ dbType=”mysql” dbDriver=”native” switchType=”1″ slaveThreshold=”100″>
<heartbeat>select user()</heartbeat>
<writeHost host=”hostS1″ url=”IP:PORT” user=”root”
password=”XXX” />
</dataHost>

3,rule.xml

<function name=”mod-long” class=”io.mycat.route.function.PartitionByMod”>
<!– how many data nodes –>
<property name=”count”>2</property>
</function>

然后就可以根据主键id平均请求到不同的mysql。作为水平扩展。这里自增主键的方式可以参考官方文档。我用0的时候总是偶数,分表非常不友好。

如何优雅的设计和使用缓存?

背景

在之前的文章中你应该知道的缓存进化史介绍了爱奇艺的缓存架构和缓存的进化历史。俗话说得好,工欲善其事,必先利其器,有了好的工具肯定得知道如何用好这些工具,本篇将介绍如何利用好缓存。

1.确认是否需要缓存

在使用缓存之前,需要确认你的项目是否真的需要缓存。使用缓存会引入的一定的技术复杂度,后文也将会一一介绍这些复杂度。一般来说从两个方面来个是否需要使用缓存:

  1. CPU占用:如果你有某些应用需要消耗大量的cpu去计算,比如正则表达式,如果你使用正则表达式比较频繁,而其又占用了很多CPU的话,那你就应该使用缓存将正则表达式的结果给缓存下来。
  2. 数据库IO占用:如果你发现你的数据库连接池比较空闲,那么不应该用缓存。但是如果数据库连接池比较繁忙,甚至经常报出连接不够的报警,那么是时候应该考虑缓存了。笔者曾经有个服务,被很多其他服务调用,其他时间都还好,但是在每天早上10点的时候总是会报出数据库连接池连接不够的报警,经过排查,发现有几个服务选择了在10点做定时任务,大量的请求打过来,DB连接池不够,从而报出连接池不够的报警。这个时候有几个选择,我们可以通过扩容机器来解决,也可以通过增加数据库连接池来解决,但是没有必要增加这些成本,因为只有在10点的时候才会出现这个问题。后来引入了缓存,不仅解决了这个问题,而且还增加了读的性能。

如果并没有上述两个问题,那么你不必为了增加缓存而缓存。

2.选择合适的缓存

缓存又分进程内缓存和分布式缓存两种。很多人包括笔者在开始选缓存框架的时候都感到了困惑:网上的缓存太多了,大家都吹嘘自己很牛逼,我该怎么选择呢?

2.1 选择合适的进程缓存

首先看看几个比较常用的缓存的比较,具体原理可以参考你应该知道的缓存进化史:

比较项 ConcurrentHashMap LRUMap Ehcache Guava Cache Caffeine
读写性能 很好,分段锁 一般,全局加锁 好,需要做淘汰操作 很好
淘汰算法 LRU,一般 支持多种淘汰算法,LRU,LFU,FIFO LRU,一般 W-TinyLFU, 很好
功能丰富程度 功能比较简单 功能比较单一 功能很丰富 功能很丰富,支持刷新和虚引用等 功能和Guava Cache类似
工具大小 jdk自带类,很小 基于LinkedHashMap,较小 很大,最新版本1.4MB 是Guava工具类中的一个小部分,较小 一般,最新版本644KB
是否持久化
是否支持集群
  • 对于ConcurrentHashMap来说,比较适合缓存比较固定不变的元素,且缓存的数量较小的。虽然从上面表格中比起来有点逊色,但是其由于是jdk自带的类,在各种框架中依然有大量的使用,比如我们可以用来缓存我们反射的Method,Field等等;也可以缓存一些链接,防止其重复建立。在Caffeine中也是使用的ConcurrentHashMap来存储元素。
  • 对于LRUMap来说,如果不想引入第三方包,又想使用淘汰算法淘汰数据,可以使用这个。
  • 对于Ehcache来说,由于其jar包很大,较重量级。对于需要持久化和集群的一些功能的,可以选择Ehcache。笔者没怎么使用过这个缓存,如果要选择的话,可以选择分布式缓存来替代Ehcache。
  • 对于Guava Cache来说,Guava这个jar包在很多Java应用程序中都有大量的引入,所以很多时候其实是直接用就好了,并且其本身是轻量级的而且功能较为丰富,在不了解Caffeine的情况下可以选择Guava Cache。
  • 对于Caffeine来说,笔者是非常推荐的,其在命中率,读写性能上都比Guava Cache好很多,并且其API和Guava cache基本一致,甚至会多一点。在真实环境中使用Caffeine,取得过不错的效果。

总结一下:如果不需要淘汰算法则选择ConcurrentHashMap,如果需要淘汰算法和一些丰富的API,这里推荐选择Caffeine。

2.2 选择合适的分布式缓存

这里选取三个比较出名的分布式缓存来作为比较,MemCache(没有实战使用过),Redis(在美团又叫Squirrel),Tair(在美团又叫Cellar)。不同的分布式缓存功能特性和实现原理方面有很大的差异,因此他们所适应的场景也有所不同。

比较项 MemCache Squirrel/Redis Cellar/Tair
数据结构 只支持简单的Key-Value结构 String,Hash, List, Set, Sorted Set String,HashMap, List,Set
持久化 不支持 支持 支持
容量大小 数据纯内存,数据存储不宜过多 数据全内存,资源成本考量不宜超过100GB 可以配置全内存或内存+磁盘引擎,数据容量可无限扩充
读写性能 很高 很高(RT0.5ms左右) String类型比较高(RT1ms左右),复杂类型比较慢(RT5ms左右)
  • MemCache:这一块接触得比较少,不做过多的推荐。其吞吐量较大,但是支持的数据结构较少,并且不支持持久化。
  • Redis:支持丰富的数据结构,读写性能很高,但是数据全内存,必须要考虑资源成本,支持持久化。
  • Tair: 支持丰富的数据结构,读写性能较高,部分类型比较慢,理论上容量可以无限扩充。

总结:如果服务对延迟比较敏感,Map/Set数据也比较多的话,比较适合Redis。如果服务需要放入缓存量的数据很大,对延迟又不是特别敏感的话,那就可以选择Tair。在美团的很多应用中对Tair都有应用,在笔者的项目中使用其存放我们生成的支付token,支付码,用来替代数据库存储。大部分的情况下两者都可以选择,互为替代。

3.多级缓存

很多人一想到缓存马上脑子里面就会出现下面的图:


Redis用来存储热点数据,Redis中没有的数据则直接去数据库访问。

在之前介绍本地缓存的时候,很多人都问我,我已经有Redis了,我干嘛还需要了解Guava,Caffeine这些进程缓存呢。我基本统一回复下面两个答案:

  1. Redis如果挂了或者使用老版本的Redis,其会进行全量同步,此时Redis是不可用的,这个时候我们只能访问数据库,很容易造成雪崩。
  2. 访问Redis会有一定的网络I/O以及序列化反序列化,虽然性能很高但是其终究没有本地方法快,可以将最热的数据存放在本地,以便进一步加快访问速度。这个思路并不是我们做互联网架构独有的,在计算机系统中使用L1,L2,L3多级缓存,用来减少对内存的直接访问,从而加快访问速度。

所以如果仅仅是使用Redis,能满足我们大部分需求,但是当需要追求更高的性能以及更高的可用性的时候,那就不得不了解多级缓存。

3.1使用进程缓存

对于进程内缓存,其本来受限于内存的大小的限制,以及进程缓存更新后其他缓存无法得知,所以一般来说进程缓存适用于:

  1. 数据量不是很大,数据更新频率较低,之前我们有个查询商家名字的服务,在发送短信的时候需要调用,由于商家名字变更频率较低,并且就算是变更了没有及时变更缓存,短信里面带有老的商家名字客户也能接受。利用Caffeine作为本地缓存,size设置为1万,过期时间设置为1个小时,基本能在高峰期解决问题。
  2. 如果数据量更新频繁,也想使用进程缓存的话,那么可以将其过期时间设置为较短,或者设置其较短的自动刷新的时间。这些对于Caffeine或者Guava Cache来说都是现成的API。

3.2使用多级缓存

俗话说得好,世界上没有什么是一个缓存解决不了的事,如果有,那就两个。

一般来说我们选择一个进程缓存和一个分布式缓存来搭配做多级缓存,一般来说引入两个也足够了,如果使用三个,四个的话,技术维护成本会很高,反而有可能会得不偿失,如下图所示:


利用Caffeine做一级缓存,Redis作为二级缓存。

  1. 首先去Caffeine中查询数据,如果有直接返回。如果没有则进行第2步。
  2. 再去Redis中查询,如果查询到了返回数据并在Caffeine中填充此数据。如果没有查到则进行第3步。
  3. 最后去Mysql中查询,如果查询到了返回数据并在Redis,Caffeine中依次填充此数据。

对于Caffeine的缓存,如果有数据更新,只能删除更新数据的那台机器上的缓存,其他机器只能通过超时来过期缓存,超时设定可以有两种策略:

  • 设置成写入后多少时间后过期
  • 设置成写入后多少时间刷新

对于Redis的缓存更新,其他机器立马可见,但是也必须要设置超时时间,其时间比Caffeine的过期长。

为了解决进程内缓存的问题,设计进一步优化:


通过Redis的pub/sub,可以通知其他进程缓存对此缓存进行删除。如果Redis挂了或者订阅机制不靠谱,依靠超时设定,依然可以做兜底处理。

4.缓存更新

一般来说缓存的更新有两种情况:

  • 先删除缓存,再更新数据库。
  • 先更新数据库,再删除缓存。 这两种情况在业界,大家对其都有自己的看法。具体怎么使用还得看各自的取舍。当然肯定会有人问为什么要删除缓存呢?而不是更新缓存呢?你可以想想当有多个并发的请求更新数据,你并不能保证更新数据库的顺序和更新缓存的顺序一致,那就会出现数据库中和缓存中数据不一致的情况。所以一般来说考虑删除缓存。

4.1先删除缓存,再更新数据库

对于一个更新操作简单来说,就是先去各级缓存进行删除,然后更新数据库。这个操作有一个比较大的问题,在对缓存删除完之后,有一个读请求,这个时候由于缓存被删除所以直接会读库,读操作的数据是老的并且会被加载进入缓存当中,后续读请求全部访问的老数据。


对缓存的操作不论成功失败都不能阻塞我们对数据库的操作,那么很多时候删除缓存可以用异步的操作,但是先删除缓存不能很好的适用于这个场景。

先删除缓存也有一个好处是,如果对数据库操作失败了,那么由于先删除的缓存,最多只是造成Cache Miss。

4.2先更新数据库,再删除缓存(推荐)

如果我们使用更新数据库,再删除缓存就能避免上面的问题。但是同样的引入了新的问题,试想一下有一个数据此时是没有缓存的,所以查询请求会直接落库,更新操作在查询请求之后,但是更新操作删除数据库操作在查询完之后回填缓存之前,就会导致我们缓存中和数据库出现缓存不一致。

为什么我们这种情况有问题,很多公司包括Facebook还会选择呢?因为要触发这个条件比较苛刻。

  1. 首先需要数据不在缓存中。
  2. 其次查询操作需要在更新操作先到达数据库。
  3. 最后查询操作的回填比更新操作的删除后触发,这个条件基本很难出现,因为更新操作的本来在查询操作之后,一般来说更新操作比查询操作稍慢。但是更新操作的删除却在查询操作之后,所以这个情况比较少出现。

对比上面4.1的问题来说这种问题的概率很低,况且我们有超时机制保底所以基本能满足我们的需求。如果真的需要追求完美,可以使用二阶段提交,但是其成本和收益一般来说不成正比。

当然还有个问题是如果我们删除失败了,缓存的数据就会和数据库的数据不一致,那么我们就只能靠过期超时来进行兜底。对此我们可以进行优化,如果删除失败的话 我们不能影响主流程那么我们可以将其放入队列后续进行异步删除。

5.缓存挖坑三剑客

大家一听到缓存有哪些注意事项,肯定首先想到的是缓存穿透,缓存击穿,缓存雪崩这三个挖坑的小能手,这里简单介绍一下他们具体是什么以及应对的方法。

5.1缓存穿透

缓存穿透是指查询的数据在数据库是没有的,那么在缓存中自然也没有,所以,在缓存中查不到就会去数据库取查询,这样的请求一多,那么我们的数据库的压力自然会增大。

为了避免这个问题,可以采取下面两个手段:

  1. 约定:对于返回为NULL的依然缓存,对于抛出异常的返回不进行缓存,注意不要把抛异常的也给缓存了。采用这种手段的会增加我们缓存的维护成本,需要在插入缓存的时候删除这个空缓存,当然我们可以通过设置较短的超时时间来解决这个问题。

2. 制定一些规则过滤一些不可能存在的数据,小数据用BitMap,大数据可以用布隆过滤器,比如你的订单ID 明显是在一个范围1-1000,如果不是1-1000之内的数据那其实可以直接给过滤掉。


5.2缓存击穿

对于某些key设置了过期时间,但是其是热点数据,如果某个key失效,可能大量的请求打过来,缓存未命中,然后去数据库访问,此时数据库访问量会急剧增加。

为了避免这个问题,我们可以采取下面的两个手段:

  1. 加分布式锁:加载数据的时候可以利用分布式锁锁住这个数据的Key,在Redis中直接使用setNX操作即可,对于获取到这个锁的线程,查询数据库更新缓存,其他线程采取重试策略,这样数据库不会同时受到很多线程访问同一条数据。
  2. 异步加载:由于缓存击穿是热点数据才会出现的问题,可以对这部分热点数据采取到期自动刷新的策略,而不是到期自动淘汰。淘汰其实也是为了数据的时效性,所以采用自动刷新也可以。

5.3缓存雪崩

缓存雪崩是指缓存不可用或者大量缓存由于超时时间相同在同一时间段失效,大量请求直接访问数据库,数据库压力过大导致系统雪崩。

为了避免这个问题,我们采取下面的手段:

  1. 增加缓存系统可用性,通过监控关注缓存的健康程度,根据业务量适当的扩容缓存。
  2. 采用多级缓存,不同级别缓存设置的超时时间不同,及时某个级别缓存都过期,也有其他级别缓存兜底。
  3. 缓存的过期时间可以取个随机值,比如以前是设置10分钟的超时时间,那每个Key都可以随机8-13分钟过期,尽量让不同Key的过期时间不同。

6.缓存污染

缓存污染一般出现在我们使用本地缓存中,可以想象,在本地缓存中如果你获得了缓存,但是你接下来修改了这个数据,但是这个数据并没有更新在数据库,这样就造成了缓存污染:


上面的代码就造成了缓存污染,通过id获取Customer,但是需求需要修改Customer的名字,所以开发人员直接在取出来的对象中直接修改,这个Customer对象就会被污染,其他线程取出这个数据就是错误的数据。要想避免这个问题需要开发人员从编码上注意,并且代码必须经过严格的review,以及全方位的回归测试,才能从一定程度上解决这个问题。

7.序列化

序列化是很多人都不注意的一个问题,很多人忽略了序列化的问题,上线之后马上报出一下奇怪的错误异常,造成了不必要的损失,最后一排查都是序列化的问题。列举几个序列化常见的问题:

  1. key-value对象过于复杂导致序列化不支持:笔者之前出过一个问题,在美团的Tair内部默认是使用protostuff进行序列化,而美团使用的通讯框架是thfift,thrift的TO是自动生成的,这个TO里面很多复杂的数据结构,但是将其存放到了Tair中。查询的时候反序列化也没有报错,单测也通过,但是到qa测试的时候发现这一块功能有问题,发现有个字段是boolean类型默认是false,把它改成true之后,序列化到tair中再反序列化还是false。定位到是protostuff对于复杂结构的对象(比如数组,List
    等等)支持不是很好,会造成一定的问题。后来对这个TO进行了转换,用普通的Java对象就能进行正确的序列化反序列化。
  2. 添加了字段或者删除了字段,导致上线之后老的缓存获取的时候反序列化报错,或者出现一些数据移位。
  3. 不同的JVM的序列化不同,如果你的缓存有不同的服务都在共同使用(不提倡),那么需要注意不同JVM可能会对Class内部的Field排序不同,而影响序列化。比如下面的代码,在Jdk7和Jdk8中对象A的排列顺序不同,最终会导致反序列化结果出现问题:
//jdk 7
class A{
    int a;
    int b;
}
//jdk 8
class A{
    int b;
    int a;
}
复制代码

序列化的问题必须得到重视,解决的办法有如下几点:

  1. 测试:对于序列化需要进行全面的测试,如果有不同的服务并且他们的JVM不同那么你也需要做这一块的测试,在上面的问题中笔者的单测通过的原因是用的默认数据false,所以根本没有测试true的情况,还好QA给力,将其给测试出来了。
  2. 对于不同的序列化框架都有自己不同的原理,对于添加字段之后如果当前序列化框架不能兼容老的,那么可以换个序列化框架。 对于protostuff来说他是按照Field的顺序来进行反序列化的,对于添加字段我们需要放到末尾,也就是不能插在中间,否则会出现错误。对于删除字段来说,用@Deprecated注解进行标注弃用,如果贸然删除,除非是最后一个字段,否则肯定会出现序列化异常。
  3. 可以使用双写来避免,对于每个缓存的key值可以加上版本号,每次上线版本号都加1,比如现在线上的缓存用的是Key_1,即将要上线的是Key_2,上线之后对缓存的添加是会写新老两个不同的版本(Key_1,Key_2)的Key-Value,读取数据还是读取老版本Key_1的数据,假设之前的缓存的过期时间是半个小时,那么上线半个小时之后,之前的老缓存存量的数据都会被淘汰,此时线上老缓存和新缓存他们的数据基本是一样的,切换读操作到新缓存,然后停止双写。采用这种方法基本能平滑过渡新老Model交替,但是不好的点就是需要短暂的维护两套新老Model,下次上线的时候需要删除掉老Model,增加了维护成本。

8. GC调优

对于大量使用本地缓存的应用,由于涉及到缓存淘汰,那么GC问题必定是常事。如果出现GC较多,STW时间较长,那么必定会影响服务可用性。这一块给出下面几点建议:

  1. 经常查看GC监控,如何发现不正常,需要想办法对其进行优化。
  2. 对于CMS垃圾收集器,如果发现remark过长,如果是大量本地缓存应用的话这个过长应该很正常,因为在并发阶段很容易有很多新对象进入缓存,从而remark阶段扫描很耗时,remark又会暂停。可以开启-XX:CMSScavengeBeforeRemark,在remark阶段前进行一次YGC,从而减少remark阶段扫描gc root的开销。
  3. 可以使用G1垃圾收集器,通过-XX:MaxGCPauseMillis设置最大停顿时间,提高服务可用性。

9. 缓存的监控

很多人对于缓存的监控也比较忽略,基本上线之后如果不报错然后就默认他就生效了。但是存在这个问题,很多人由于经验不足,有可能设置了不恰当的过期时间,或者不恰当的缓存大小导致缓存命中率不高,让缓存就成为了代码中的一个装饰品。所以对于缓存各种指标的监控,也比较重要,通过其不同的指标数据,我们可以对缓存的参数进行优化,从而让缓存达到最优化:


上面的代码中用来记录get操作的,通过Cat记录了获取缓存成功,缓存不存在,缓存过期,缓存失败(获取缓存时如果抛出异常,则叫失败),通过这些指标,我们就能统计出命中率,我们调整过期时间和大小的时候就可以参考这些指标进行优化。

10. 一款好的框架

一个好的剑客没有一把好剑怎么行呢?如果要使用好缓存,一个好的框架也必不可少。在最开始使用的时候大家使用缓存都用一些util,把缓存的逻辑写在业务逻辑中:


上面的代码把缓存的逻辑耦合在业务逻辑当中,如果我们要增加成多级缓存那就需要修改我们的业务逻辑,不符合开闭原则,所以引入一个好的框架是不错的选择。

推荐大家使用JetCache这款开源框架,其实现了Java缓存规范JSR107并且支持自动刷新等高级功能。笔者参考JetCache结合Spring Cache, 监控框架Cat以及美团的熔断限流框架Rhino实现了一套自有的缓存框架,让操作缓存,打点监控,熔断降级,业务人员无需关心。上面的代码可以优化成:


对于一些监控数据也能轻松从大盘上看到:


最后

想要真正的使用好一个缓存,必须要掌握很多的知识,并不是看几个Redis原理分析,就能把Redis缓存用得炉火纯青。对于不同场景,缓存有各自不同的用法,同样的不同的缓存也有自己的调优策略,进程内缓存你需要关注的是他的淘汰算法和GC调优,以及要避免缓存污染等。分布式缓存你需要关注的是他的高可用,如果其不可用了如何进行降级,以及一些序列化的问题。一个好的框架也是必不可少的,对其如果使用得当再加上上面介绍的经验,相信能让你很好的驾驭住这头野马——缓存。

作者:公众号_咖啡拿铁
链接:https://juejin.im/post/5b849878e51d4538c77a974a
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

[转]微博应对日访问量百亿级的缓存架构设计

作者介绍

陈波,新浪微博技术专家,《深入分布式缓存》作者。

注:本文转自中生代技术订阅号(ID:freshmanTechnology),经平台授权转载。

微博日活跃用户1.6亿+,每日访问量达百亿级,面对庞大用户群的海量访问,良好的架构且不断改进的缓存体系具有非常重要的支撑作用。本文将由新浪微博技术专家陈波老师,跟大家详细讲解那些庞大的数据都是如何呈现的。

本文大纲

1、微博在运行过程中的数据挑战

2、Feed平台系统架构

3、Cache架构及演进

4、总结与展望

数据挑战

Feed平台系统架构

Feed平台系统架构总共分为五层,最上面是端层,比如web端、客户端、大家用的IOS或安卓的一些客户端,还有一些开放平台、第三方接入的一些接口;下一层是平台接入层,不同的池子,主要是为了把好的资源集中调配给重要的核心接口,这样遇到突发流量的时候,就有更好的弹性来服务,提高服务稳定性。再下面是平台服务层,主要是Feed算法、关系等等。接下来是中间层,通过各种中间介质提供一些服务。最下面一层就是存储层。

1
Feed Timeline

大家日常刷微博的时候,比如在主站或客户端点一下刷新,最新获得了十到十五条微博,这是怎么构建出来的呢?

刷新之后,首先会获得用户的关注关系。比如他有一千个关注,会把这一千个ID拿到,再根据这一千个UID,拿到每个用户发表的一些微博。同时会获取这个用户的Inbox,就是他收到的特殊的一些消息,比如分组的一些微博、群的微博、下面的关注关系、关注人的微博列表。

拿到这一系列微博列表之后进行集合、排序,拿到所需要的那些ID,再对这些ID去取每一条微博ID对应的微博内容。如果这些微博是转发过来的,它还有一个原微博,会进一步取原微博内容。通过原微博取用户信息,进一步根据用户的过滤词对这些微博进行过滤,过滤掉用户不想看到的微博。

根据以上步骤留下的微博,会再进一步来看,用户对这些微博有没有收藏、点赞,做一些flag设置,还会对这些微博各种计数,转发、评论、赞数进行组装,最后才把这十几条微博返回给用户的各种端。

这样看来,用户一次请求得到的十几条记录,后端服务器大概要对几百甚至几千条数据进行实时组装,再返回给用户,整个过程对Cache体系强度依赖,所以Cache架构设计优劣会直接影响到微博体系表现的好坏。

2
Feed Cache架构

接下来我们看一下Cache架构,它主要分为六层。首先第一层是Inbox,主要是分组的一些微博,然后直接对群主的一些微博。Inbox比较少,主要是推的方式。

然后对于第二层的Outbox,每个用户都会发常规的微博,都会在它的Outbox里面去。根据存的ID数量,实际上分成多个Cache,普通的大概是200多条,如果长的大概是2000条。

第三层是一些关系,它的关注、粉丝、用户。

第四层是内容,每一条微博一些内容存在这里。

第五层就是一些存在性判断,比如某条微博我有没有赞过。之前有一些明星就说我没有点赞这条微博怎么显示我点赞了,引发了一些新闻。而这种就是记录,实际上她有在某个时候点赞过但可能忘记了。

最下面还有比较大的一层——计数,每条微博的评论、转发等计数,还有用户的关注数、粉丝数这些数据。

Cache架构及演进
1
简单KV数据类型

接下来我们着重讲一下微博的Cache架构演进过程。最开始微博上线时,我们是把它作为一个简单的KV数据类型来存储。我们主要采取哈希分片存储在MC池子里,上线几个月之后发现一些问题:有一些节点机器宕机或是其它原因,大量的请求会穿透Cache层达到DB上去,导致整个请求变慢,甚至DB僵死。

于是我们很快进行了改造,增加了一个HA层,这样即便Main层出现某些节点宕机情况或者挂掉之后,这些请求会进一步穿透到HA层,不会穿透到DB层。这样可以保证在任何情况下,整个系统命中率不会降低,系统服务稳定性有了比较大的提升。

对于这种做法,现在业界用得比较多,然后很多人说我直接用哈希,但这里面也有一些坑。比如我有一个节点,节点3宕机了,Main把它给摘掉,节点3的一些QA分给其他几个节点,这个业务量还不是很大,穿透DB,DB还可以抗住。但如果这个节点3恢复了,它又加进来之后,节点3的访问就会回来,稍后节点3因为网络原因或者机器本身的原因,它又宕机了,一些节点3的请求又会分给其他节点。这个时候就会出现问题,之前分散给其他节点写回来的数据已经没有人更新了,如果它没有被剔除掉就会出现混插数据。

实际上微博是一个广场型的业务,比如突发事件,某明星找个女朋友,瞬间流量就30%了。突发事件后,大量的请求会出现在某一些节点,会导致这些节点非常热,即便是MC也没办法满足这么大的请求量。这时MC就会变成瓶颈,导致整个系统变慢。

基于这个原因,我们引入了L1层,还是一个Main关系池,每一个L1大概是Main层的N分之一,六分之一、八分之一、十分之一这样一个内存量,根据请求量我会增加4到8个L1,这样所有请求来了之后首先会访问L1。L1命中的话就会直接访问,如果没有命中再来访问Main-HA层,这样在一些突发流量的时候,可以由L1来抗住大部分热的请求。对微博本身来说,新的数据就会越热,只要增加很少一部分内存就会抗住更大的量。

简单总结一下,通过简单KV数据类型的存储,我们实际上以MC为主的,层内HASH节点不漂移,Miss穿透到下一层去读取。通过多组L1读取性能提升,能够抗住峰值、突发流量,而且成本会大大降低。对读写策略,采取多写,读的话采用逐层穿透,如果Miss的话就进行回写。对存在里面的数据,我们最初采用Json/xml,2012年之后就直接采用Protocol Buffer格式,对一些比较大的用QuickL进行压缩。

2
集合类数据

刚才讲到简单的QA数据,那对于复杂的集合类数据怎么来处理?

比如我关注了2000人,新增一个人,就涉及到部分修改。有一种方式是把2000个ID全部拿下来进行修改,但这种对带宽、机器压力会很大。还有一些分页获取,我存了2000个,只需要取其中的第几页,比如第二页,也就是第十到第二十个,能不能不要全量把所有数据取回去。还有一些资源的联动计算,会计算到我关注的某些人里面ABC也关注了用户D。这种涉及到部分数据的修改、获取,包括计算,对MC来说实际上是不太擅长的。

各种关注关系都存在Redis里面取,通过Hash分布、储存,一组多存的方式来进行读写分离。现在Redis的内存大概有30个T,每天都有2-3万亿的请求。

在使用Redis的过程中,实际上还是遇到其他一些问题。比如从关注关系,我关注了2000个UID,有一种方式是全量存储,但微博有大量的用户,有些用户登陆得比较少,有些用户特别活跃,这样全部放在内存里成本开销是比较大的。所以我们就把Redis使用改成Cache,比如只存活跃的用户,如果你最近一段时间没有活跃,会把你从Redis里踢掉,再次有访问的时候再把你加进来。

这时存在一个问题,因为Redis工作机制是单线程模式,如果它加某一个UV,关注2000个用户,可能扩展到两万个UID,两万个UID塞回去基本上Redis就卡住了,没办法提供其他服务。所以我们扩展一种新的数据结构,两万个UID直接开了端,写的时候直接依次把它写到Redis里面去,读写的整个效率就会非常高。它的实现是一个long型的开放数组,通过Double Hash进行寻址。

我们对Redis进行了一些其他的扩展,大家可能也在网上看到过我们之前的一些分享,把数据放到公共变量里面,整个升级过程,我们测试1G的话加载要10分钟,10G大概要十几分钟以上,现在是毫秒级升级。

对于AOF,我们采用滚动的AOF,每个AOF是带一个ID的,达到一定的量再滚动到下一个AOF里去。对RDB落地的时候,我们会记录构建这个RDB时,AOF文件以及它所在的位置,通过新的RDB、AOF扩展模式,实现全增量复制。

3
其他数据类型-计数

接下来还有一些其他的数据类型,比如一个计数,实际上计数在每个互联网公司都可能会遇到,对一些中小型的业务来说,实际上MC和Redis足够用的,但在微博里计数出现了一些特点:单条Key有多条计数,比如一条微博,有转发数、评论数,还有点赞;一个用户有粉丝数、关注数等各种各样的数字。因为是计数,它的Value size是比较小的,根据它的各种业务场景,大概就是2-8个字节,一般4个字节为多,然后每日新增的微博大概十亿条记录,总记录就更可观了,然后一次请求,可能几百条计数要返回去。

4
计数器-Counter Service

最初是可以采取Memcached,但它有个问题,如果计数超过它内容容量时,会导致一些计数的剔除,宕机或重启后计数就没有了。另外可能有很多计数它为零,那这个时候怎么存,要不要存,存的话就占很多内存。微博每天上十亿的计数,光存0都要占大量的内存,如果不存又会导致穿透到DB里去,对服务的可溶性会存在影响。

2010年之后我们又采用Redis访问,随着数据量越来越大之后,发现Redis内存有效负荷还是比较低的,它一条KV大概需要至少65个字节,但实际上我们一个计数需要8个字节,然后Value大概4个字节,所以有效只有12个字节,还有四十多个字节都是被浪费掉的。这还只是单个KV,如果在一条Key有多个计数的情况下,它就浪费得更多了。比如说四个计数,一个Key 8个字节,四个计数每个计数是4个字节,16个字节大概需要26个字节就行了,但是用Redis存大概需要200多个字节。

后来我们通过自己研发的Counter Service,内存降至Redis的五分之一到十五分之一以下,而且进行冷热分离,热数据存在内存里,冷数据如果重新变热,就把它放到LRU里去。落地RDB、AOF,实现全增量复制,通过这种方式,热数据单机可以存百亿级,冷数据可以存千亿级。

整个存储架构大概是上图这样,上面是内存,下面是SSD,在内存里是预先把它分成N个Table,每个Table根据ID的指针序列,划出一定范围。任何一个ID过来先找到它所在的Table,如果有直接对它增增减减,有新的计数过来,发现内存不够的时候,就会把一个小的Table Dump到SSD里去,留着新的位置放在最上面供新的ID来使用。

有些人疑问说,如果在某个范围内,我的ID本来设的计数是4个字节,但是微博特别热,超过了4个字节,变成很大的一个计数怎么处理?对于超过限制的,我们把它放在Aux dict进行存放,对于落在SSD里的Table,我们有专门的IndAux进行访问,通过RDB方式进行复制。

5
其他数据类型-存在性判断

除了计数,微博还有一些业务,一些存在性判断。比如一条微博展现的,有没有点赞、阅读、推荐,如果这个用户已经读过这个微博了,就不要再显示给他。这种有一个很大的特点,它检查是否存在,每条记录非常小,比如Value1个bit就可以了,但总数据量巨大。比如微博每天新发表微博1亿左右,读的可能有上百亿、上千亿这种总的数据需要判断。怎么来存储是个很大的问题,而且这里面很多存在性就是0。还是前面说的,0要不要存?如果存了,每天就存上千亿的记录;如果不存,那大量的请求最终会穿透Cache层到DB层,任何DB都没办法抗住那么大的流量。

我们也进行了一些选型,首先直接考虑能不能用Redis。单条KV 65个字节,一个KV可以8个字节的话,Value只有1个bit,这样算下来每日新增内存有效率是非常低的。第二种我们新开发的Counter Service,单条KV Value1个bit,我就存1个byt,总共9个byt就可以了。这样每日新增内存900G,存的话可能就只能存最新若干天的,存个三天差不多快3个T了,压力也挺大,但比Redis已经好很多。

我们最终方案是自己开发Phantom,先采用把共享内存分段分配,最终使用的内存只用120G就可以。算法很简单,对每个Key可以进行N次哈希,如果哈希的某一个位它是1,那么进行3次哈希,三个数字把它设为1。把X2也进行三次哈希,后面来判断X1是否存在的时候,从进行三次哈希来看,如果都为1就认为它是存在的,如果某一个哈希X3,它的位算出来是0,那就百分百肯定是不存在的。

它的实现架构比较简单,把共享内存预先拆分到不同Table里,在里面进行开方式计算,然后读写,落地的话采用AOF+RDB的方式进行处理。整个过程因为放在共享内存里面,进程要升级重启数据也不会丢失。对外访问的时候,建Redis协议,它直接扩展新的协议就可以访问我们这个服务了。

6
小结

小结一下,到目前为止,我们关注了Cache集群内的高可用、扩展性、组件高性能,还有一个特别重要就是存储成本,还有一些我们没有关注到的,比如运维性如何,微博现在已经有几千差不多上万台服务器等。

7
进一步优化

8
服务化

采取的方案首先就是对整个Cache进行服务化管理,对配置进行服务化管理,避免频繁重启,另外如果配置发生变更,直接用一个脚本修改一下。

服务化还引入Cluster Manager,实现对外部的管理,通过一个界面来进行管理,可以进行服务校验。服务治理方面,可以做到扩容、缩容,SLA也可以得到很好的保障。另外,对于开发来说,现在就可以屏蔽Cache资源。

总结与展望

最后简单总结一下,对于微博Cache架构来说,我们从它的数据架构、性能、储存成本、服务化等不同方面进行了优化增强。欢迎对此有研究或有疑问的同行们留言,跟我们一起探讨。