日志处理之logstash

Introduction

Logstash是一个开源数据收集引擎,具有实时管道功能。Logstash将来自不同数据源的数据统一搜集起来,并根据需求将数据标准化输出到你所选择的目的地。如下图所示。

img

Input/Filter/Output

Logstash可以从多个数据源获取数据,并对其进行处理、转换,最后将其发送到不同类型的“存储”

输入

采集各种样式、大小和来源的数据

分布式系统中,数据往往是以各种各样的形式(结构化、非结构话)存在于不同的节点中。Logstash 支持不同数据源的选择 ,日志、报表、数据库的内容等等。可以在同一时间从众多常用来源捕捉事件。

  • 文件类型
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
input{
file{
# path属性接受的参数是一个数组,其含义是标明需要读取的文件位置
path => [‘pathA’,‘pathB’]

# 表示多就去path路径下查看是够有新的文件产生。默认是15秒检查一次。
discover_interval => 15

# 排除那些文件,也就是不去读取那些文件
exclude => [‘fileName1’,‘fileNmae2’]

# 被监听的文件多久没更新后断开连接不在监听,默认是一个小时。
close_older => 3600

# 在每次检查文件列 表的时候, 如果一个文件的最后 修改时间 超过这个值, 就忽略这个文件。 默认一天。
ignore_older => 86400

# logstash 每隔多久检查一次被监听文件状态(是否有更新), 默认是 1 秒。
stat_interval => 1

#sincedb记录数据上一次的读取位置的一个index
sincedb_path => ’$HOME/. sincedb‘

#logstash 从什么 位置开始读取文件数据, 默认是结束位置 也可以设置为:beginning 从头开始
start_position => ‘beginning’

# 注意:这里需要提醒大家的是,如果你需要每次都从开始读取文件的话,只设置start_position => beginning是没有用的,你可以选择sincedb_path 定义为 /dev/null
}

}
  • 数据库类型
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
input{
jdbc{
# jdbc sql server 驱动,各个数据库都有对应的驱动,需自己下载
jdbc_driver_library => "/etc/logstash/driver.d/sqljdbc_2.0/enu/sqljdbc4.jar"
#jdbc class 不同数据库有不同的 class 配置
jdbc_driver_class => "com.microsoft.sqlserver.jdbc.SQLServerDriver"
#配置数据库连接 ip 和端口,以及数据库
jdbc_connection_string => "jdbc:sqlserver://xxxxxx:1433;databaseName=test_db"
#配置数据库用户名
jdbc_user =>
#配置数据库密码
jdbc_password =>

# 上面这些主要配置数据库java驱动,账号配置
# 定时器 多久执行一次SQL,默认是一分钟
# schedule => 分 时 天 月 年
# schedule => * 22 * * * 表示每天22点执行一次
schedule => "* * * * *"
# 是否清除 last_run_metadata_path 的记录,如果为真那么每次都相当于从头开始查询所有的数据库记录
clean_run => false
# 是否需要记录某个column 的值,如果 record_last_run 为真,可以自定义我们需要表的字段名称,
#此时该参数就要为 true. 否则默认 track 的是 timestamp 的值.
use_column_value => true
#如果 use_column_value 为真,需配置此参数. 这个参数就是数据库给出的一个字段名称。当然该字段必须是递增的,可以是 数据库的数据时间这类的
tracking_column => create_time
#是否记录上次执行结果, 如果为真,将会把上次执行到的 tracking_column 字段的值记录下来,保存到 last_run_metadata_path 指定的文件中
record_last_run => true
# 我们只需要在 SQL 语句中 WHERE MY_ID > :last_sql_value 即可. 其中 :last_sql_value 取得就是该文件中的值
last_run_metadata_path => "/etc/logstash/run_metadata.d/my_info"
# 是否将字段名称转小写。
# 这里有个小的提示,如果你之前就处理过一次数据,并且在Kibana中有对应的搜索需求的话,还是改为true,
# 因为默认是true,并且Kibana是大小写区分的。准确的说应该是ES大小写区分
lowercase_column_names => false
# 你的SQL的位置,当然,你的SQL也可以直接写在这里。
# statement => SELECT * FROM tabeName t WHERE t.creat_time > :last_sql_value
statement_filepath => "/etc/logstash/statement_file.d/my_info.sql"
# 数据类型,标明数据来源,es索引的时候可以建立不同的额索引
type => "my_info"
}
# 注意:外在的SQL文件就是一个文本文件就可以了,还有需要注意的是,一个jdbc{}插件就只能处理一个SQL语句,
# 如果你有多个SQL需要处理的话,只能在重新建立一个jdbc{}插件。
}
  • beats

主要是接受filebeats的数据导入

1
2
3
4
5
6
7
8
input {
beats {
# 接受数据端口
port => 5044
# 数据类型
type => "logs"
}
}

过滤器

实时解析和转换数据

数据从源传输到存储库的过程中,需要对不同的数据进行不同的存储,Logstash 过滤器能够解析每条记录,识别每条数据的字段内容,并将它们转换成自定义数据,以便进行处理分析计算。

Logstash 动态地转换和解析数据,支持各种格式或复杂度数据的解析:

  • 利用 Grok 从非结构化数据中派生出结构
  • 从 IP 地址破译出地理坐标
  • 将 PII 数据匿名化,完全排除敏感字段
  • 整体处理不受数据源、格式或架构的影响

输出

尽管 ES是logstash的常用输出方向,能够为我们的搜索和分析带来无限可能,但它并非唯一选择。

Logstash 提供众多输出选择,您可以将数据发送到您要指定的地方,并且能够灵活地解锁众多下游用例。

img

Install && config

  • 安装

安装比较简单,官网直接有现成的二进制包,下载地址: https://artifacts.elastic.co/downloads/logstash/logstash-7.10.1-linux-x86_64.tar.gz

安装也比较简单,解压设置path即可使用。

本人经常使用,就写了个安装elk的脚本,需要的可以拿去使用:https://github.com/shiguofu2012/scripts/blob/master/install_elk.sh。

  • 配置intput/output

Logstash配置有两个必需的元素,输入和输出,以及一个可选过滤器。输入插件从数据源那里消费数据,过滤器插件根据你的期望修改数据,输出插件将数据写入目的地。

img

  1. 接下来,允许Logstash最基本的管道,例如:
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@VM-145-82-centos ~]# logstash -e 'input { stdin {} } output { stdout {} }'
Sending Logstash logs to /usr/local/logstash/logs which is now configured via log4j2.properties
[2021-01-07T22:15:40,409][INFO ][logstash.runner ] Starting Logstash {"logstash.version"=>"7.9.3", "jruby.version"=>"jruby 9.2.13.0 (2.5.7) 2020-08-03 9a89c94bcc Java HotSpot(TM) 64-Bit Server VM 25.261-b12 on 1.8.0_261-b12 +indy +jit [linux-x86_64]"}
[2021-01-07T22:15:40,803][WARN ][logstash.config.source.multilocal] Ignoring the 'pipelines.yml' file because modules or command line options are specified
[2021-01-07T22:15:42,097][INFO ][org.reflections.Reflections] Reflections took 39 ms to scan 1 urls, producing 22 keys and 45 values
[2021-01-07T22:15:43,158][INFO ][logstash.javapipeline ][main] Starting pipeline {:pipeline_id=>"main", "pipeline.workers"=>8, "pipeline.batch.size"=>125, "pipeline.batch.delay"=>50, "pipeline.max_inflight"=>1000, "pipeline.sources"=>["config string"], :thread=>"#<Thread:0x41654bea run>"}
[2021-01-07T22:15:43,809][INFO ][logstash.javapipeline ][main] Pipeline Java execution initialization time {"seconds"=>0.64}
[2021-01-07T22:15:43,866][INFO ][logstash.javapipeline ][main] Pipeline started {"pipeline.id"=>"main"}
The stdin plugin is now waiting for input:
[2021-01-07T22:15:43,914][INFO ][logstash.agent ] Pipelines running {:count=>1, :running_pipelines=>[:main], :non_running_pipelines=>[]}
[2021-01-07T22:15:44,235][INFO ][logstash.agent ] Successfully started Logstash API endpoint {:port=>9600}
{
"host" => "VM-145-82-centos",
"message" => "",
"@timestamp" => 2021-01-07T14:15:43.902Z,
"@version" => "1"
}
hello
{
"host" => "VM-145-82-centos",
"message" => "hello",
"@timestamp" => 2021-01-07T14:15:47.996Z,
"@version" => "1"
}

{
"host" => "VM-145-82-centos",
"message" => "",
"@timestamp" => 2021-01-07T14:15:50.766Z,
"@version" => "1"
}

从标准输入获取数据,输出到标准输出。

  1. input 从filebeat获取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
input {
beats {
host => "0.0.0.0" # 默认是127.0.0.1 只能本级访问
port => 5044
}
}
# output 索引至es
output {
elasticsearch {
hosts => ["localhost:9200"] # es地址
user => "xxxx" # 用户名
password => "xxxx" # 密码
index => "test-ap-%{+YYYY.MM.dd}" # 建立的索引,这里默认每天建一个索引
}
}

总体来讲,input/output是比较容易配置的,关键是对数据进行格式化。

  • filter
正则匹配

grok 匹配非格式化字段,提取字段格式化数据,强大的文本解析工具,支持正则表达式

1
2
3
4
5
6
7
8
9
10
11
grok {
match => { "[message]" => "%{TIMESTAMP_ISO8601:_timestamp} %{LOGLEVEL:level} %{DATA:stack}" }
}
# 解析失败的处理
if "_grokparsefailure" in [tags] {
mutate {
rename => ["message", "msg"]
remove_field => ["tags"]
}
}
}
ip解析
1
2
3
4
5
6
filter {
geoip {
source => "ip"
fields => ["city_name", "timezone"] # 选择解析的字段
}
}

解析出来的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"ip" => "183.60.92.253",
"@version" => "1",
"@timestamp" => "2014-08-07T10:32:55.610Z",
"host" => "raochenlindeMacBook-Air.local",
"geoip" => {
"ip" => "183.60.92.253",
"country_code2" => "CN",
"country_code3" => "CHN",
"country_name" => "China",
"continent_code" => "AS",
"region_name" => "30",
"city_name" => "Guangzhou",
"latitude" => 23.11670000000001,
"longitude" => 113.25,
"timezone" => "Asia/Chongqing",
"real_region_name" => "Guangdong",
"location" => [
[0] 113.25,
[1] 23.11670000000001
]
}
}
字段增删改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
filter {
mutate {
remove_field => ["ecs", "input", "agent", "tags", "@version", "@metadata"] # 移除字段
rename => ["host", "my_host"] # 重命名
rename => ["kubernetes", "my_k8s"] # 重命名
remove_field => ["[host][mac]", "[my_host][containerized]", "[my_host][os]", "[my_host][id]", "[my_host][name]", "[my_host][architecture]"] # 移除字段,使用已经重命名的字段
add_field => { "mytype" => "type" } # 增加字段

update => { "sample" => "My new message" } # 更新字段内容,如果字段不存在,不会新建
replace => { "message" => "%{source_host}: My new message" } # 与 update 功能相同,区别在于如果字段不存在则会新建字段
convert => ["request_time", "float"] # 数据类型转换
uppercase => [ "fieldname" ] # 大写转换
lowercase => [ "fieldname" ]

# 提供正则表达式替换
gsub => [
# replace all forward slashes with underscore
"fieldname", "/", "_",
# replace backslashes, question marks, hashes, and minuses
# with a dot "."
"fieldname2", "[\\?#-]", "."
]
}
}
条件判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
filter {
# 条件判断,字段my_k8s是否存在; 并且日志路径匹配
if ![my_k8s] and [log][file][path] =~ "/data/project_log/[\w-]+/[\w-\\.]+.log" {
mutate {
split => ["[log][file][path]", "/"]
# split操作 /data/project_log/app1/app.log => ["", data, project_log, app1, app.log]
add_field => { "[kubernets][labels][app]" => "%{[log][file][path][3]}" }
}
}
# 字段操作放在mutate中
mutate {
remove_field => [ "log" ]
}
}
json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
filter {
# 检查是否是json格式
if [message] =~ "^\{.*\}[\s\S]*$" {
json {
source => "[message]"
target => "jsoncontent"
}
# json 数据失败
if "_jsonparsefailure" in [tags] {
mutate {
rename => ["message", "msg"]
remove_field => ["tags"]
}
}
}
}

Example

这里介绍一个曾经搭建的ELK日志系统。

结构比较简单,kubetnets中filebeat damonSet方式运行,搜集所有container 标准输出的日志,并传入logstash中,logstash将数据导入elasticsearch。结构图如下所示:

image-20210111133718737

下面开始logstash的配置:

input比较简单,使用filebeat搜集日志,传入logstash

1
2
3
4
5
6
input {
beats {
host => "0.0.0.0"
port => 5044
}
}

output增加了几个条件判断,根据不同的字段日志类型,索引到不同的es索引中;如下所示

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
output {
# k8s app 索引到对应的app中
if ([kubernetes][labels][app]) {
if ([type] == "app") {
elasticsearch {
hosts => ["localhost:9200"]
index => "%{[kubernetes][labels][app]}-%{+YYYY.MM.dd}"
}
# 根据type区分索引
} else if ([type] == "user") {
elasticsearch {
hosts => ["localhost:9200"]
index => "%{[kubernetes][labels][app]}-[type]%{+YYYY.MM.dd}"
}
} else {
elasticsearch {
hosts => ["localhost:9200"]
index => "%{[kubernetes][labels][app]}-%{+YYYY.MM.dd}"
}
}

# 不存在k8s app字段
} else if ([type] == "user") {
elasticsearch {
hosts => ["localhost:9200"]
index => "default-%{+YYYY.MM.dd}"
}
} else if ([type] == "app") {
elasticsearch {
hosts => ["localhost:9200"]
index => "default-%{+YYYY.MM.dd}"
}
} else {
elasticsearch {
hosts => ["localhost:9200"]
index => "default-v1.0.0"
}
}
}

filter 配置,不同的日志格式,输出格式化的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
filter {
mutate {
# 移除filebeat发送的多余字段
remove_field => ["ecs", "input", "agent", "tags", "@version", "@metadata"]
remove_field => ["[host][mac]", "[host][containerized]", "[host][os]", "[host][id]", "[host][name]", "[host][architecture]"]
}
if ![kubernetes] and [log][file][path] =~ "/data/app_log/[\w-]+/[\w\.-]+.log" {
mutate {
split => ["[log][file][path]", "/"]
add_field => { "[kubernetes][labels][app]" => "%{[log][file][path][3]}-host" }
}
}
mutate {
remove_field => [ "log" ]
}

if [message] =~ "^\{.*\}[\s\S]*$" {
# json格式处理
json {
source => "[message]"
}
if "_jsonparsefailure" in [tags] {
mutate {
rename => ["message", "msg"]
remove_field => ["tags"]
}
}

if ([time]) {
date {
match => ["time", "ISO8601", "UNIX", "UNIX_MS", "TAI64N"]
target => "@timestamp"
}

mutate {
remove_field => ["time"]
}
}

# docker 日志格式
if [log] =~ "^\{.*\}[\s\S]*$" {
json {
source => "[log]"
}

mutate {
remove_field => ["log"]
}

if ([start_time]) {
date {
match => ["start_time", "ISO8601", "UNIX", "UNIX_MS", "TAI64N"]
target => "start_time"
}
}

if ([app_start_time]) {
date {
match => ["app_start_time", "ISO8601", "UNIX", "UNIX_MS", "TAI64N"]
target => "app_start_time"
}
}

if ([end_time]) {
date {
match => ["end_time", "ISO8601", "UNIX", "UNIX_MS", "TAI64N"]
target => "end_time"
}
}

if ([timestamp]) {
date {
match => ["timestamp", "ISO8601", "UNIX", "UNIX_MS", "TAI64N"]
target => "@timestamp"
}
}
} else {
if ([timestamp]) {
date {
match => ["timestamp", "ISO8601", "UNIX", "UNIX_MS", "TAI64N"]
target => "@timestamp"
}
}
}
} else {
# 匹配日志格式
grok {
match => { "[message]" => "%{TIMESTAMP_ISO8601:_timestamp} %{LOGLEVEL:level} %{DATA:stack} - (?<message>(.|\r|\n|\t)*)" }
}
# 匹配格式失败处理
if "_grokparsefailure" in [tags] {
mutate {
rename => ["message", "msg"]
remove_field => ["tags"]
}
}
# 解析时间格式
date {
match => ["_timestamp", "ISO8601", "UNIX", "UNIX_MS", "TAI64N"]
target => "@timestamp"
}

mutate {
remove_field => ["_timestamp"]
}
}

mutate {
remove_field => ["message"]
}
}

总结

总之 ,logstash具备强大的功能,将不同数据源的数据经过清洗格式化,转化为结构化的数据,存储到不同的存储单元。

gRPC tracing

介绍

gRPC

gRPC是什么可以用官网的一句话来概括

A high-performance, open-source universal RPC framework

所谓RPC(remote procedure call 远程过程调用)框架实际是提供了一套机制,使得应用程序之间可以进行通信,而且也遵从server/client模型。使用的时候客户端调用server端提供的接口就像是调用本地的函数一样。如下图所示就是一个典型的RPC结构图。

img

gRPC vs. Restful API

既然是server/client模型,那么我们直接用restful api不是也可以满足吗,为什么还需要RPC呢?

gRPC和restful API都提供了一套通信机制,用于server/client模型通信,而且它们都使用http作为底层的传输协议(严格地说, gRPC使用的http2.0,而restful api则不一定)。不过gRPC还是有些特有的优势,如下:

  • gRPC可以通过protobuf来定义接口,从而可以有更加严格的接口约束条件。
  • 另外,通过protobuf可以将数据序列化为二进制编码,这会大幅减少需要传输的数据量,从而大幅提高性能。
  • gRPC可以方便地支持流式通信(理论上通过http2.0就可以使用streaming模式, 但是通常web服务的restful api似乎很少这么用,通常的流式数据应用如视频流,一般都会使用专门的协议如HLS,RTMP等,这些就不是我们通常web服务了,而是有专门的服务器应用。)

Tracing

微服务遍地都是,一个功能,一个接口都可能是一个微服务,微服务之间的调用混乱,无法追踪,很难找出瓶颈点,因此迫切需要一种方法来追踪服务之间的调用链路。

  • MetaData

Metadata 可以理解为一个 HTTP 请求的 Header(它的底层实现就是 HTTP/2 的 Header),用户可以通过访问和修改每个 gRPC Call 的 Metadata 来传递额外的信息:比如认证信息,比如用于追踪的 Request ID。

  • interceptor

Interceptor 有点类似于我们平时常用的 HTTP Middleware,不同的是它可以用在 Client 端和 Server 端。比如在收到请求之后输出日志,在请求出现错误的时候输出错误信息,比如获取请求中设置的 Request ID。

  • Golang实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// UnaryInvoker is called by UnaryClientInterceptor to complete RPCs.
type UnaryInvoker func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, opts ...CallOption) error

// UnaryClientInterceptor intercepts the execution of a unary RPC on the client. invoker is the handler to complete the RPC
// and it is the responsibility of the interceptor to call it.
// This is an EXPERIMENTAL API.
type UnaryClientInterceptor func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error

// UnaryHandler defines the handler invoked by UnaryServerInterceptor to complete the normal
// execution of a unary RPC. If a UnaryHandler returns an error, it should be produced by the
// status package, or else gRPC will use codes.Unknown as the status code and err.Error() as
// the status message of the RPC.
type UnaryHandler func(ctx context.Context, req interface{}) (interface{}, error)

// UnaryServerInterceptor provides a hook to intercept the execution of a unary RPC on the server. info
// contains all the information of this RPC the interceptor can operate on. And handler is the wrapper
// of the service method implementation. It is the responsibility of the interceptor to invoke handler
// to complete the RPC.
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)

Golang 的实现是把 Metadata 塞在了 context 里面,只需要使用 metadata.FromOutgoingContext(ctx)metadata.FromIncomingContext(ctx) 就能够访问本次请求的 Metadata。概念清楚之后代码应该非常好写了:

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
const requestIdKey = "requestId"

// client 请求RPC增加MetaData拦截器
func RequestIDClientInterceptor() grpc.UnaryClientInterceptor {
return func(
ctx context.Context,
method string, req, resp interface{},
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption,
) (err error) {
// 获取请求的MetaData
md, ok := metadata.FromOutgoingContext(ctx)
if !ok {
// 未添加则创建
md = metadata.Pairs()
}

value := ctx.Value(requestIdKey)
if requestID, ok := value.(string); ok && requestID != "" {
// md 不区分大小写,内部会转换为小写
md.Set(requestIdKey, requestID)
}
return invoker(metadata.NewOutgoingContext(ctx, md), method, req, resp, cc, opts...)
}
}

// server端获取MetaData数据
func RequestIDServerInterceptor() grpc.UnaryServerInterceptor {
return func(
ctx context.Context,
req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
) (resp interface{}, err error) {
// 获取请求过来的MetaData
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
md = metadata.Pairs()
}
// 后去请求的requstId,并设置到当前context中
requestIDs := md[requestIdKey]
if len(requestIDs) >= 1 {
ctx = context.WithValue(ctx, requestIdKey, requestIDs[0])
}
return handler(ctx, req)
}
}

日志切割工具 logrotate 配置

1. 安装

Linux一般是默认自带logrotate的,如果没有可以使用yum/apt安装

apt/yum 安装

1
2
3
4
5
6
[root@VM-145-82-centos ~]# yum install logrotate
Loaded plugins: fastestmirror
Loading mirror speeds from cached hostfile
Package logrotate-3.8.6-14.tl2.x86_64 already installed and latest version
Nothing to do
[root@VM-145-82-centos ~]#
1
2
3
4
5
6
root@VM-0-15-ubuntu:[10:59:47]:~# apt-get install logrotate
Reading package lists... Done
Building dependency tree
Reading state information... Done
logrotate is already the newest version (3.8.7-2ubuntu2.16.04.2).
0 upgraded, 0 newly installed, 0 to remove and 385 not upgraded.

源码安装

github地址: https://github.com/logrotate/logrotate

按照github安装

2. 配置

logrotate是利用系统crontab定时执行的,在目录/etc/cron.daily中有个logrotate的脚本。如果需要,可以在cron.hourly,也可以在/etc/crontab中增加自己的配置。

这些配置都是独立的,结合自己的配置以及服务的日志量来自定义达到最优配置。

1
2
3
4
5
6
7
8
#!/bin/sh

/usr/sbin/logrotate -s /var/lib/logrotate/logrotate.status /etc/logrotate.conf
EXITVALUE=$?
if [ $EXITVALUE != 0 ]; then
/usr/bin/logger -t logrotate "ALERT exited abnormally with [$EXITVALUE]"
fi
exit 0

配置文件主要存放在目录

1
2
[root@VM-145-82-centos ~]# ls /etc/logrotate.d/
conman cron.30m iptraf-ng mgetty mongodb mysql psacct syslog yum

一般安装会添加常用组件的配置,在生产环境中,我们会自定义一些配置。

如下是本人常用的配置:

1
2
3
4
5
6
7
8
9
10
11
/data/log/*.log
{
daily
notifempty
copytruncate
compress
rotate 60
missingok
dateext
dateformat -%s
}
配置参数 说明
monthly 日志文件将按月轮循。其它可用值为’daily’,’weekly’或者’yearly’。
rotate 5 一次将存储5个归档日志。对于第六个归档,时间最久的归档将被删除。
compress 在轮循任务完成后,已轮循的归档将使用gzip进行压缩。
delaycompress 总是与compress选项一起用,delaycompress选项指示logrotate不要将最近的归档压缩,压缩将在下一次轮循周期进行。这在你或任何软件仍然需要读取最新归档时很有用。
missingok 在日志轮循期间,任何错误将被忽略,例如“文件无法找到”之类的错误。
notifempty 如果日志文件为空,轮循不会进行。
create 644 root root 以指定的权限创建全新的日志文件,同时logrotate也会重命名原始日志文件。
size 日志文件大小的配置,如果没达到这个大小,将不会压缩
dateext 压缩文件带上日期,默认会使用编号(log.log.1.gz),该选项会是每次压缩都带上日期,如 log.log-2020
dateformat 日期格式 支持 %Y%m%d(年月日) %s(时间戳)
postrotate/endscript 在所有其它指令完成后,postrotate和endscript里面指定的命令将被执行。在这种情况下,rsyslogd 进程将立即再次读取其配置并继续运行。

3. 执行频度定制

logrotate是在cron中执行的,因此要自定义执行频度,可以增加crontab配置

比如,我们的服务需要每半小时压缩一次 hourly是不满足我们的需求,就需要在crontab中增加一条记录

1
2
3
4
[root@VM-145-82-centos /etc/logrotate.d/cron.30m]# crontab -e

# rotate nginx log every 30min
*/30 * * * * /usr/sbin/logrotate /etc/logrotate.d/cron.30m/* -f

可以将30min执行的所有配置文件放到一个目录,执行30min命令。

4. 手动执行

配置文件是否配置成功,执行后是什么效果。logrotate提供测试的功能。

1
2
3
4
5
6
7
8
9
10
11
[root@VM-145-82-centos /etc/logrotate.d/cron.30m]# logrotate -d /etc/logrotate.d/cron.30m/*
reading config file /etc/logrotate.d/cron.30m/nginx
Allocating hash table for state file, size 15360 B

Handling 1 logs

rotating pattern: /var/log/nginx/*log after 1 days (10 rotations)
empty log files are not rotated, old logs are removed
considering log /var/log/nginx/access.log
log does not need rotating (log is empty)considering log /var/log/nginx/error.log
log does not need rotating (log is empty)not running postrotate script, since no logs were rotated

执行后,会输出文件怎么变更,压缩重命名等。比如上面会提示 “old logs are removed” “10 rotate” 等记录。

总结

logrotate工具对于防止因庞大的日志文件而耗尽存储空间是十分有用的。配置完毕后,进程是全自动的,可以长时间在不需要人为干预下运行。可以根据需求及日志量定制自己的日志切割规则。

Golang context 源码分析

一般接触新东西都会有三问(what/why/how),我们也从这几个角度看下golang context。

WHAT IS CONTEXT

Goroutine和Channel是go语言的特色,并发编程会使用协程来处理工作,通道用来协程之间的通信。主协程创建子协程,子协程再创建子协程,就会形成一个协程树状结构,协程之间可以采用通道来进行通信。
每个Goroutine在执行之前,都要先知道程序当前的执行状态,通常将这些执行状态都会封装Context变量中,传递给要执行的Goroutine中。比如在网络编程中,处理一个网络Request时,我们可能需要开启不同的Goroutine来获取数据与逻辑处理。而这些Goroutine可能需要共享Request的一些信息;同时当Request异常、超时或者被取消的时候,所有从这个Request创建的所有Goroutine都应该被结束。

WHY CONTEXT

上面我们说过,由于异常、超时或者被取消的情况,都应该结束该Request创建的所有Goroutine。假如不用context,我们看怎么去控制协程树。

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

import (
"fmt"
"time"
)

func StartWork() {
data := make(chan int, 10)
done := make(chan struct{}) // 使用一个chan来控制协程的退出

defer close(data)
// consumer
go func() {
for {
select {
case <-done:
fmt.Println("child process interrupt...")
return
default:
fmt.Printf("send message: %d\n", <-data)
time.Sleep(time.Second * 1)
}
}
}()

// producer
for i := 0; i < 10; i++ {
data <- i
}
time.Sleep(5 * time.Second)
// 退出协程
close(done)
time.Sleep(1 * time.Second)
fmt.Println("main process exit!")
}

子协程运行着任务,如果主协程需要在某个时刻发送消息通知子协程中断任务退出,那么就可以让子协程监听这个done channel,一旦主协程关闭done channel,那么子协程就可以推出了,这样就实现了主协程通知子协程的需求。这种方式运行良好,但是这也是有限的。
考虑这种情况:如果主协程中有多个任务1, 2, …n,主协程对这些任务有时间限制;而任务1又有多个子任务1, 2, …,k, 任务1对这些子任务也有自己的时间控制,那么这些子任务既要感知主协程的取消信号,也需要感知任务1的取消信号。
如果使用done channel的用法,这个时候需要定义两个done channel,子任务们需要同时监听这两个done channel,这样也可以正常work。但是如果层级更深,如果这些子任务还有子任务,那么使用done channel的方式将会变得非常繁琐且混乱。
这个时候 context出场了。

context interface

1
2
3
4
5
6
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}

context包含四个方法

  • Deadline 返回绑定当前context的任务执行的截止时间,如果没有截止时间,则ok==false
  • Done 该函数返回一个只读的chan 数据是空结构体,类似我们上面的done,控制协程的退出
  • Err 如果协程退出,该函数返回退出的原因,被取消或者执行超时等错误
  • Value 获取context中传递的值

可以看到Done方法返回的chan正是用来传递结束信号以抢占并中断当前任务的;Deadline方法用来表示gorotine是否在指定时间被取消;Err方法是用来解释goroutine被取消的原因;而Value则用于获取特定于当前任务的数据。
而context所包含的额外信息键值对是如何存储的呢?其实可以想象一颗树,树的每个节点可能携带一组键值对,如果当前节点上无法找到key所对应的值,就会向上去父节点里找,直到根节点,具体后面会说到。

下面我们看下context的两个比较常用的实现,valueCtx/cancelCtx

valueCtx

1
2
3
4
type valueCtx struct {
Context
key, val interface{}
}

结构比较简单就是增加了key/val字段存储键值对
WithValue用以向其中存储键值对

1
2
3
4
5
6
7
8
9
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflect.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}

这里添加键值对不是直接在原context结构体上直接添加,而是重新创建一个新的valueCtx节点,作为原ctx的子节点,并将键值对添加在子节点上,由此形成一条链表。获取value就是从这条链表上尾部向前搜寻(代码如下):

1
2
3
4
5
6
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}

valueCtx的增加值与获取值的过程如下:
With就是往链表的尾部增加节点,value就是从尾部开始获取对应的值,找到就退出;否则找到头部返回空.

enter image description here

cancelCtx

1
2
3
4
5
6
7
8
9
10
11
12
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}

type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done chan struct{} // created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}

cancelCtx是一个可以取消的 Context,实现了 canceler 接口。它直接将接口 Context 作为它的一个匿名字段,这样,它就可以被看成一个 Context。done是一个空结构体类型的channel,用来传递关闭信号,在协程中一般结合select来监听父协程退出信号;children是一个map,存储了所有可取消context的子节点,这样任意层级的context取消,都会给所有子context发送取消信号;err用于存储错误信息表示任务结束的原因。
先看下Done方法:

1
2
3
4
5
6
7
8
9
func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock()
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d
}

c.done懒汉式创建,调用Done方法才会创建。返回一个只读的chan,一般结合select监听。一旦取消,立马可读。
重点看下cancel方法

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
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
// 必须要传 err
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // 已经被其他协程取消
}
// 给 err 字段赋值
c.err = err
// 关闭 channel,通知其他协程
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}

// 遍历它的所有子节点
for child := range c.children {
// 递归地取消所有子节点
child.cancel(false, err)
}
// 将子节点置空
c.children = nil
c.mu.Unlock()

if removeFromParent {
// 从父节点中移除自己
removeChild(c.Context, c)
}
}

总体来看,cancel() 方法的功能就是关闭 channel:c.done;递归地取消所有的子节点;将自己从父节点树中摘掉。通过关闭 channel,goroutine 这边接收取消信号的方式就是 select 语句中的读 ctx.Done 可读,执行相应的退出函数。
这里有个比较巧妙的地方,调用子节点 cancel 方法的时候,传入的第一个参数是 false,最终子节点是没有调用removeChild,把自己从父节点移除。
移除的操作很简单,找到最近的可取消的祖先节点,将自己从其map中删除。最关键的一行:delete(p.children, child)。

1
2
3
4
5
6
7
8
9
10
11
12
13
func removeChild(parent Context, child canceler) {
// 查找最近的可取消节点
p, ok := parentCancelCtx(parent)
if !ok {
return
}
p.mu.Lock()
if p.children != nil {
// 从最近的祖先节点的map中移除自己
delete(p.children, child)
}
p.mu.Unlock()
}

那什么时候需要移除,什么时候不需要移除呢?
我们先来看下创建一个可取消的context。
WithCancel函数创建一个可取消的context,即cancelCtx类型的context,传入一个父context,返回context和CancelFunc,调用CancelFunc即可触发cancel操作。直接看源码:

1
2
3
4
5
6
7
8
9
10
11
type CancelFunc func()

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) } // 这里cancel传入了true
}

func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}

可以看到,只有在使用WithCancel创建context的时候,返回的cancelFunc会传入true。这样当调用cancelFunc 时,会将这个 可取消的context从它的父节点里移除,因为父节点可能有很多子节点,取消之后要和父节点断绝关系,对其他没影响。而对于该context的所有子节点都会因为该节点的cancelFunc调用c.children = nil而化为灰烬,没有必要再一个一个移除。

enter image description here

如上左图,代表一棵 context 树,当然也可以看做是一个协程树。当调用左图子context的cancel 方法后,该 context会依次调用children中的cancel的方法,此时子context不会移除自己;该context将自己从它的父 context中去除掉了:从children中delete,实线箭头变成了虚线。且虚线圈框出来的 context 都被取消了,圈内的 context间的父子关系都被取消掉了。
再重点看propagateCancel():

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
func propagateCancel(parent Context, child canceler) {
// 父节点是个空节点
if parent.Done() == nil {
return // parent is never canceled
}
// 找到可以取消的父 context
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// 父节点已经被取消了,本节点(子节点)也要取消
child.cancel(false, p.err)
} else {
// 父节点未取消
if p.children == nil {
p.children = make(map[canceler]struct{})
}
// "挂到"父节点上
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
// 如果没有找到可取消的父 context。新启动一个协程监控父节点或子节点取消信号
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}

上面这段代码的功能就是向上寻找可以“依靠”的“可取消”的context,并且“挂靠”上去。这样,调用上层cancel方法的时候,就可以层层传递,将那些挂靠的子context同时“取消”。
这里也有一个比较巧妙的设计,就是else的情况。起初我一直不理解,怎么可能会有else的情况发生,parent.Done()不为空,怎么会找不到可取消的父节点。这里要翻看parentCancelCtx的源码了。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
for {
switch c := parent.(type) {
case *cancelCtx:
return c, true
case *timerCtx:
return &c.cancelCtx, true
case *valueCtx:
parent = c.Context
default:
return nil, false
}
}
}

这里只会找三种Context类型:cancelCtx,timerCtx,*valueCtx。若是把Context内嵌到自定义类型里,就识别不出来了,就会走default了。为避免这种情况父节点取消传递不到当前节点之后的节点,重新启动了一个协程来传递这种情况,所以使用可取消的context的时候,尽量避免将ctx塞入自定义的结构里,不然会多一个协程来处理。
另一个巧妙的地方就是select

1
2
3
4
5
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}

同时监听了父节点是否退出,也监听当前节点是否退出。这二者缺一不可。

第一个case 好理解,上层几点取消要继续传递下去,就监听了上层是否被取消。

第二个case 如果子节点自己退出了,那就不需要这个协程了,他的作用就是为了连接上层节点与自己。但如果去掉这个case,上层协程一直不取消,那这个goroutine就泄漏了。

总结

context主要用于处理父子协程之间的同步取消信号,本质上是一种协程同步调用协调的方式。
在使用context的时候,有两点要注意:

  • 父协程只是使用context通知所有的子协程,我已经不需要你了,但不会直接干涉和中断下游任务的执行,具体的操作是由子协程决定操作,因此子协程要使用select来监听ctx.Done。
  • context是线程安全的,可以放心地在多个协程中传递使用。

redis 内存淘汰策略

redis 内存淘汰策略

将 Redis 用作缓存时, 如果内存空间用满, 就会根据配置的最大内存策略返回错误或者自动驱逐老的数据。

maxmemory 配置指令

maxmemory 用于指定 Redis 能使用的最大内存。既可以在 redis.conf 文件中设置, 也可以在运行过程中通过 CONFIG SET 命令动态修改。

例如, 要设置 100MB 的内存限制, 可以在 redis.conf 文件中这样配置

1
maxmemory 100mb

maxmemory 设置为 0, 则表示不进行内存限制。当然, 对32位系统来说有一个隐性的限制条件: 最多 3GB 内存.

内存淘汰策略(maxmemory-policy)
  • noeviction

不删除,达到最大内存,如果继续写(新增)数据,直接返回错误信息。DEL还是可以正常执行。

  • allkeys-lru

所有key范围,优先删除最近最少使用(LRU)

  • volatile-lru

只限于设置了expire的部分,优先删除最近最少使用(LRU)的key

  • allkeys-random

所有key范围,随机删除一部分key

  • volatile-random

只限于设置了expire的部分key,随机删除

  • volatile-ttl

只限于设置了expire的部门;优先删除剩余时间短的key

如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-randomvolatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。

内存淘汰的实现

淘汰过程:

  • 客户端执行命令,导致redis中的数据增加,内存增加
  • Redis检查内存使用量,超过maxmemory限制,根据策略清除key

近似LRU算法

为了节约内存,采用了抽取少量的key样本,然后删除其中访问时间最久的key。

在 Redis 的 LRU 算法中, 可以通过设置样本(sample)的数量来调优算法精度。

1
maxmemory-samples <count>

体验CENTOS 8 并安装Docker

体验CENTOS 8 并安装Docker

安装CentOS 8

背景: 五一假期闲来无事,把自己的一台老本安装了centos8最新的系统玩玩。

  1. 去官网下载了最新的centos系统,太慢了,就到阿里云镜像中心下载,真是快,附上地址
1
https://mirrors.aliyun.com/centos/8.1.1911/isos/x86_64/

有包含所有软件的版本,有网络安装的版本,dvd版本的太大 7G,就选择了boot版本的,网络安装吧

  1. 坑一堆

    以前安装系统都是通过GRUB引导安装,现在命令统统忘记,折腾半天,总是启动失败 :(

    有知道的可以告诉我,感谢!

    太折腾了,就选择了一种简单的方式,UtrolISO烧录到U盘,直接安装,然而我太幼稚了,还是报错

    1
    no floppy found please insert floppy and go on....

    就是说没有软盘,我纳闷了,怎么会出现软盘错误。。。

    后来去看了isolinux的启动参数

    1
    2
    3
    4
    label linux
    menu label ^Install CentOS Linux 8
    kernel vmlinuz
    append initrd=initrd.img inst.stage2=hd:LABEL=CentOS-8-1-1911-x86_64-dvd quiet

    发现有个LABEL的选项,但是前面有个HD的选项,怎么会去找软盘,纳闷了,可能是遗留的问题,默认都是软盘,就把这个选项改下, 改成自己的U盘。我的u盘是hdb4;这里如果不知道可以让启动失败,等待几分钟后就会出现命令行模式,可以输入命令

    1
    ls /dev/hd* # 查看当前挂在的磁盘

    于是就把配置文件修改为,顺利启动

    1
    2
    3
    4
    label linux
    menu label ^Install CentOS Linux 8
    kernel vmlinuz
    append initrd=initrd.img inst.stage2=hd:/dev/hdb4 quiet

    由于我选的是网络安装版本的,后面会出现填写repo文件的网络地址,默认是会有个CLOSEST mirror,可以自己安装成功;如果网络失败,可以填写阿里云的repo文件

    https://mirrors.aliyun.com/centos/8/BaseOS/x86_64/os/

终于安装成功

安装docker

刚开始不知道阿里云也是有docker的镜像的,就从官网粘贴的配置docker镜像源

1
2
3
4
5
6
7
8
sudo yum install -y yum-utils \
device-mapper-persistent-data \
lvm2
sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo

sudo yum install docker-ce docker-ce-cli containerd.io

意外的是,竟然报错了

img

就是说containerd.io需要1.2.2-3以上的版本

难道镜像源中不满足,又纳闷,还是去官网下载了1.2.2-3版本,地址:

1
https://download.docker.com/linux/centos/7/x86_64/stable/Packages/containerd.io-1.2.13-3.1.el7.x86_64.rpm

当然,可能很慢,就找了阿里云的,自己选择版本

1
https://mirrors.aliyun.com/docker-ce/linux/centos/7/x86_64/stable/Packages/

继续,下载了rpm包以后,就直接安装

1
rpm -ivh containerd.io-1.2.13-3.1.el7.x86_64.rpm

问题还是有的。centos8自带了一个runc也是用来跑容器的,跟docker是类似的,然而我想用docker,就直接卸载吧。

1
yum remove runc

再安装就ok了

同时,把yum的repo也改下,改成阿里云的,速度不是一般的快

1
sudo yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
1
sudo yum install docker-ce

终于安装成功,启动docker daemon

1
service docker start

普通用户运行docker

docker启动后,默认只有root才可以使用,其他都会提示 “no permission…”

看docker的通信sock,明显是属于root的

1
2
ll /var/run/docker.sock
srw-rw----. 1 root root 0 May 25 14:43 /var/run/docker.sock

添加docker用户组,把需要使用docker的用户添加到docker组

1
2
groupadd docker
gpasswd -a gfshi docker

重启docker服务

普通用户也可以使用了~

可以玩docker了

mysql8/mysql 5.7.24基本操作

最近使用ubuntu 18.04LST 安装mysql,设置密码,搞得我怀疑人生,搞半天是设置密码的方式在mysql8中改变;记录下问题的过程及设置密码

密码咋修改不了

之前在使用MySQL的时候,都是使用grant/alter/set 去修改root的密码的,今天安装了5.7.29,使用同样的方法修改,一直修改失败,系统账户root登录mysql不需要密码,普通用户无法登录

使用grant修改密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> select User,Host,authentication_string from user;
+------------------+-----------+-------------------------------------------+
| User | Host | authentication_string |
+------------------+-----------+-------------------------------------------+
| root | localhost | *000BFFBA444B9D1E98861B0537ABAA4664A2CAA1 |
| mysql.session | localhost | *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE |
| mysql.sys | localhost | *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE |
| debian-sys-maint | localhost | *87BD4C29046C1B2C43D1FB7342F5F1BA286253BC |
+------------------+-----------+-------------------------------------------+
4 rows in set (0.00 sec)

mysql> grant all on *.* to 'root'@'localhost' identified by '123456';
Query OK, 0 rows affected, 1 warning (0.00 sec)
mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)

执行没有返回错误,退出后重启MySQL,发现密码并没有修改;

于是觉得自己是不是没有操作成功,重新执行了两边,未果;

于是乎,换了一种

1
2
3
4
5
mysql> SET PASSWORD FOR 'root'@'localhost' = PASSWORD('123456');
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)

同样,操作了两边,还是未果;

执行后看返回

后来在某次执行发现,有一个warning,两个执行都有返回,就看了这两个warnings

1
2
3
4
5
6
7
8
9
mysql>   grant all on *.* to 'root'@'localhost' identified by '123456';
Query OK, 0 rows affected, 1 warning (0.00 sec)
mysql> show warnings;
+---------+------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message |
+---------+------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Warning | 1287 | Using GRANT statement to modify existing user's properties other than privileges is deprecated and will be removed in future release. Use ALTER USER statement for this operation. |
+---------+------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

Using GRANT statement to modify existing user’s properties other than privileges is deprecated and will be removed in future release. Use ALTER USER statement for this operation.

就是说,grant已经要废弃了,要用alter User 修改

1
2
3
4
5
6
7
8
9
10
11
mysql> SET PASSWORD FOR 'root'@'localhost' = PASSWORD('123456');
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> show warnings
-> ;
+---------+------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message |
+---------+------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Warning | 1287 | 'SET PASSWORD FOR <user> = PASSWORD('<plaintext_password>')' is deprecated and will be removed in a future release. Please use SET PASSWORD FOR <user> = '<plaintext_password>' instead |
+---------+------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

‘SET PASSWORD FOR = PASSWORD(‘‘)’ is deprecated and will be removed in a future release. Please use SET PASSWORD FOR = ‘‘ instead

这种设置密码的方式也已经被废弃了,要使用

SET PASSWORD FOR = ‘

不过warnings只是警告,执行是成功的,只是说下一版本要去掉PASSWORD,继续查找问题。

好,那就用明文的方式再试一次

1
2
mysql> SET PASSWORD FOR 'root'@'localhost'='123456';
Query OK, 0 rows affected, 1 warning (0.00 sec)

又出现一个warnings,那就再看下有什么warnings的

1
2
3
4
5
6
7
8
9
10
mysql> SET PASSWORD FOR 'root'@'localhost'='123456';
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> show warnings;
+-------+------+------------------------------------------------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+------------------------------------------------------------------------------------------------------------+
| Note | 1699 | SET PASSWORD has no significance for user 'root'@'localhost' as authentication plugin does not support it. |
+-------+------+------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

SET PASSWORD has no significance for user ‘root’@’localhost’ as authentication plugin does not support it.

plugin不支持set password

那就看下当前用户的plugin,刚好user里面有一列plugin

1
2
3
4
5
6
7
8
9
10
mysql> select Host,User,plugin from user;
+-----------+------------------+-----------------------+
| Host | User | plugin |
+-----------+------------------+-----------------------+
| localhost | root | auth_socket |
| localhost | mysql.session | mysql_native_password |
| localhost | mysql.sys | mysql_native_password |
| localhost | debian-sys-maint | mysql_native_password |
+-----------+------------------+-----------------------+
4 rows in set (0.00 sec)

目测是这个plugin的问题,那就修改下plugin

1
2
3
user mysql;
update user set plugin="mysql_native_password" where User='root';
flush privileges;

修改完后重启mysql

终于成功修改密码

修改plugin成功后,这些密码修改的命令都可以使用了,终于,密码修改成功了

方法1,使用set password for , 使用明文
1
2
mysql> set password for 'root'@'localhost'='123456';
Query OK, 0 rows affected (0.00 sec)
方法2 ,alter user
1
2
mysql> alter user 'root'@'localhost' identified by 'P@55word';
Query OK, 0 rows affected (0.00 sec)

索引操作

  1. 查看索引
1
show index from [table name];
  1. 创建索引
1
2
3
4
5
ALTER TABLE 表名 ADD [UNIQUE | FULLTEXT | SPATIAL]  INDEX | KEY  [索引名] (字段名1 [(长度)] [ASC | DESC]) [USING 索引方法];



CREATE  [UNIQUE | FULLTEXT | SPATIAL]  INDEX  索引名 ON  表名(字段名) [USING 索引方法];
  1. 删除索引
1
2
3
4
5
DROP INDEX 索引名 ON 表名



ALTER TABLE 表名 DROP INDEX 索引名
  1. 查询语句是否命中索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
explain [sql]

mysql> explain select * from sentence_daily where date_str='2019-10-25';(命中索引date_str_type)
+----+-------------+----------------+------------+------+---------------+---------------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------------+------------+------+---------------+---------------+---------+-------+------+----------+-------+
| 1 | SIMPLE | sentence_daily | NULL | ref | date_str_type | date_str_type | 50 | const | 1 | 100.00 | NULL |
+----+-------------+----------------+------------+------+---------------+---------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)

mysql> explain select * from sentence_daily where type=1;(未命中)
+----+-------------+----------------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | sentence_daily | NULL | ALL | NULL | NULL | NULL | NULL | 101 | 10.00 | Using where |
+----+-------------+----------------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

redis 高可用系统

Redis的高可用方案有三种:

  • master/slave主从方案

主节点提供读写操作,从节点提供读操作;

主节点故障,需要手动主备切换;

读写主还是从,由客户端控制;

  • 哨兵(sentinel)模式来进行主从替换以及故障恢复

在主从模式的基础上增加sentinel监听,主节点不可用,自动将slave提升为master;

客户端通过sentinel获取主/从节点;

  • redis cluster集群方案

去中心化,平等结构,数据槽;

这里主要介绍下,简单的主从模式与sentinel结构;

redis master/slave (replication)

master/slave 是为了解决点单故障而出现的,slave是对master的镜像;

主要工作机制

  • master-slave使用异步方式复制数据,同样使用异步的确认slave-to-master
  • 一个master可以拥有多个slave
  • slave也可以连接slave (4.0以后,所有的sub-slaves从maser接受复制数据)
  • master在做复制操作的时候,不会block master的读写操作
  • slave在复制操作过程中,根据配置使用旧的数据提供服务或者返回错误(配置replica-serve-stale-data);完成复制后,旧的数据会被删除,新的会被加载,期间会拒绝连接( block incoming connections)
  • slave可做横向扩容或者数据的安全性与高可用
  • master可关闭持久化,直接内存读写,slave开启持久化,保存数据;注意master重启,slave也会同步空的数据

redis replication 工作原理

  • 两个参数确定了同步的信息

replication ID: 伪随机数,与当前数据集生成,每个master具有

offset:master/slave 之间同步的位置记录

当slave连接到master后,使用PSYNC命令发送当前master 的replication ID与offset

  • 正常情况,master发送增量数据给slave,offset开始
  • slave发送无效的replication ID/master没有足够的backlog,会开始全量复制(full resynchronization)
全量复制的过程
  1. master接受到全量复制的请求,启动bgsave进程生成rdb file;同时,会开启一个buffer缓存所有新的写命令

关于buffer的配置:

client-output-buffer-limit replica 256MB 64MB 60

如果在复制期间,内存缓冲区超过60秒一直消耗超过 64MB,或者一次性超过 256MB,那么停止复制,复制失败。后两个参数是配合使用的,假如:消耗超过64MB 一直持续了59秒,但是60秒的时候不超过64MB了,那么就保持连接继续复制。

  1. bgsave完成后,master会将rdb file 给slave;slave会将其存到磁盘并加载到内存

  2. master 发送缓存的命令给slave

下面为master的日志记录,同步的过程

1
2
3
4
5
6
7
8
3829:M 10 Oct 2019 14:47:08.193 * Replica 10.226.50.31:6379 asks for synchronization
3829:M 10 Oct 2019 14:47:08.193 * Full resync requested by replica 10.226.50.31:6379
3829:M 10 Oct 2019 14:47:08.193 * Starting BGSAVE for SYNC with target: disk
3829:M 10 Oct 2019 14:47:08.194 * Background saving started by pid 4102
4102:C 10 Oct 2019 14:47:09.637 * DB saved on disk
4102:C 10 Oct 2019 14:47:09.637 * RDB: 6 MB of memory used by copy-on-write
3829:M 10 Oct 2019 14:47:09.685 * Background saving terminated with success
3829:M 10 Oct 2019 14:47:09.731 * Synchronization with replica 10.226.50.31:6379 succeeded
无盘化复制

正常情况下,全量复制需要在磁盘上创建rdb文件,并从磁盘上读取该文件发送给slave

磁盘效率低下,会严重影响master的效率,可配置不经过磁盘,直接发送

1
2
3
4
repl-diskless-sync yes

# 等待 5s 后再开始复制,因为要等更多 replica 重新连接过来
repl-diskless-sync-delay 5

注意:

  1. 不建议使用slave作为master的热备,关掉master的持久化;master宕机后会立马同步到slave导致数据丢失
  2. master的备份方案需要做,防止本地文件丢失;sentinel可以监控并切换,但有可能master failure还未检测到就已经重启,也会导致情况1
至少N个slave才能写成功

Redis 2.8开始,可以配置至少有N个slave连接的情况下,master才能接受写请求

由于redis使用异步复制,不能保证slave真正接受到了某个写请求,于是有可能有一个丢失窗口

工作特点:

  • slave每秒钟ping master,确认复制的数据的数量
  • master记录每个slave最后一次ping的时间
  • 用户可配置不大于最大秒数的延迟的最小的slave数量

如果有N个slave,延迟小于M秒,则写入成功;否则返回错误,写入失败

redis配置参数:

  • min-slaves-to-write <number of slaves>
  • min-slaves-max-lag <number of seconds>
过期key处理
  • replica不会过期key,只会等待master过期key。如果master过期了一个key,或者通过LRU淘汰了一个key,那么会模拟一条del命令发送给replica
  • master过期数据,因此在slave内存中可能存在逻辑上已经过期的数据还未及时被删除,slave中使用(its logical clock)only for read operations确定key不存在,不违反一致性
  • Lua脚本执行期间不会执行过期策略;发送同样的Lua脚本到slave保证一致性
heartbeat

主从节点互相都会发送 heartbeat 信息。

master 默认每隔 10秒 发送一次 heartbeat,replica node 每隔 1秒 发送一个 heartbeat。

redis replication 配置

配置是比较简单的,了解基本原理主要是为了在出现问题时,能够快速定位问题

1
2
3
4
5
6
7
replicaof 10.226.50.31 6381


# master 配置认证密码
# requirepass foobared
# slave 认证
# masterauth foobared

replicaof [ip] [port]

在5.0.4之前使用的时slaveof;

修改配置的bind/port/logfile/dbfilename/dir等配置参数,启动redis

1
2
redis-server /etc/redis-6379.conf
redis-server /etc/redis-6381.conf

启动后查看redis的replication 6379, 角色为slave;写入数据报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root@localhost ~]# redis-cli 
127.0.0.1:6379> info replication
# Replication
role:slave
master_host:10.226.50.31
master_port:6381
master_link_status:up
master_last_io_seconds_ago:10
master_sync_in_progress:0
slave_repl_offset:128880
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:a868c97d4f90ce5e77d6d6bd1787e8f61df35eff
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:128880
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:128880
127.0.0.1:6379> set a b
(error) READONLY You can't write against a read only replica.

master的角色为master,存在master replid,slave 同步的位置offset;写入数据成功,查看slave,数据已经同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@localhost ~]# redis-cli -p 6381
127.0.0.1:6381> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=10.226.50.31,port=6379,state=online,offset=128978,lag=0
master_replid:a868c97d4f90ce5e77d6d6bd1787e8f61df35eff
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:128978
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:128978
127.0.0.1:6381> set a b
OK

redis sentinel

Sentinel(哨兵)是用于监控redis集群中Master状态的工具,sentinel哨兵模式已经被集成在redis2.4之后的版本中。

sentinel进程可以监视一个或者多个redis master/slave服务;当某个master服务下线时,自动将该master下的某个从服务升级为master服务替代已下线的master服务继续处理请求。

写博客太需要时间,未完,待续。。。

linux 一些配置

TCP最大监听队列修改

修改somaxconn参数值

该内核参数默认值一般是128(定义了系统中每一个端口最大的监听队列的长度),对于负载很大的服务程序来说大大的不够。一般会将它修改为2048或者更大。

1
echo 2048 > /proc/sys/net/core/somaxconn    # 临时修改,立马生效;系统重启丢失

在/etc/sysctl.conf中添加如下

1
net.core.somaxconn = 2048

然后在终端中执行

1
sysctl -p

redis (overcommit_memory)WARNING

redis 有时background save db不成功,log发现下面的告警,很可能由它引起的:

1
17427:M 17 Sep 2019 10:54:12.730 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.

看到这个顺道去查了下,发现

内核参数overcommit_memory

它是内存分配策略,可选值:0、1、2。

0 – 表示内核将检查是否有足够的可用内存供应用进程使用;如果有足够的可用内存,内存申请允许;否则,内存申请失败,并把错误返回给应用进程。
1 – 表示内核允许分配所有的物理内存,而不管当前的内存状态如何。
2 – 表示内核允许分配超过所有物理内存和交换空间总和的内存

Overcommit 与 OOM

Linux对大部分申请内存的请求都回复”yes”,以便能跑更多更大的程序。因为申请内存后,并不会马上使用内存。这种技术叫做Overcommit。当linux发现内存不足时,会发生OOM killer(OOM=out-of-memory)。它会选择杀死一些进程(用户态进程,不是内核线程),以便释放内存。

当oom-killer发生时,linux会选择杀死哪些进程?选择进程的函数是oom_badness函数(在mm/oom_kill.c中),该函数会计算每个进程的点数(0~1000)。点数越高,这个进程越有可能被杀死。每个进程的点数跟oom_score_adj有关,而且oom_score_adj可以被设置(-1000最低,1000最高)。

解决办法

同样是修改内核参数:

1
echo 1 > /proc/sys/vm/overcommit_memory # 临时修改,立马生效;系统重启丢失

编辑/etc/sysctl.conf ,新增下面一行,然后sysctl -p 使配置文件生效

1
vm.overcommit_memory=1

redis (Transparent Huge Pages) WARNING

redis 在centos 7 启动后有警告

1
17427:M 17 Sep 2019 10:54:12.730 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.

Transparent Huge Pages的一些官方介绍

The kernel will always attempt to satisfy a memory allocation using huge pages. If no huge pages are available (due to non availability of physically continuous memory for example) the kernel will fall back to the regular 4KB pages. THP are also swappable (unlike hugetlbfs). This is achieved by breaking the huge page to smaller 4KB pages, which are then swapped out normally.

禁止透明大页

1
2
[root@localhost 6379]# cat /sys/kernel/mm/transparent_hugepage/enabled
always madvise [never]

默认开启的,因此出现找不到文件,就是开启的

打开文件数的限制

用户级的文件数限制, 可以通过 ulimit -n 来查看

1
2
[root@localhost opt]# ulimit -n    # 查看当前用户能够打开的最大文件数
1024

而系统级别的文件数限制,则通过sysctl -a来查看

1
2
[root@localhost opt]# sysctl -a | grep file-max
fs.file-max = 284775

一般系统最大文件数会根据硬件资源计算出来的,如果强行需要修改最大打开文件数可以通过ulimit -n 10240来修改,当这种方式只对当前进程有效,如果需要永久有效则需要修改/etc/security/limits.conf(重启系统生效)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#<type> can have the two values:
# - "soft" for enforcing the soft limits
# - "hard" for enforcing hard limits
#
#<item> can be one of the following:
# - core - limits the core file size (KB)
# - data - max data size (KB)
# - fsize - maximum filesize (KB)
# - memlock - max locked-in-memory address space (KB)
# - nofile - max number of open file descriptors
# - rss - max resident set size (KB)
# - stack - max stack size (KB)
# - cpu - max CPU time (MIN)
# - nproc - max number of processes
# - as - address space limit (KB)
# - maxlogins - max number of logins for this user
# - maxsyslogins - max number of logins on the system
# - priority - the priority to run user process with
# - locks - max number of file locks the user can hold
# - sigpending - max number of pending signals
# - msgqueue - max memory used by POSIX message queues (bytes)
# - nice - max nice priority allowed to raise to values: [-20, 19]
# - rtprio - max realtime priority
#
#<domain> <type> <item> <value>
#

#* soft core 0
#* hard rss 10000
#@student hard nproc 20
#@faculty soft nproc 20
#@faculty hard nproc 50
#ftp hard nproc 0
#@student - maxlogins 4

# End of file

root soft nofile 65535 # 新增
root hard nofile 65535

各列意义:

1: 用户名称,对所有用户则“*”

2:soft 软限制/hard 硬件限制

3: 代表最大文件打开数

4: 数量

查看所有进程文件打开数
1
lsof | wc -l
查看某个进程打开文件数
1
lsof  -p [pid] | wc -l
查看系统中各个进程分别打开了多少句柄数
1
lsof -n|awk '{print $2}'|sort|uniq -c|sort -nr|more

SSH 密钥登录

  1. 生成公钥私钥对,linux下使用 ssh-keygen 命令生成,一路enter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@localhost ~]# ssh-keygen 
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa): # 存放路径
Enter passphrase (empty for no passphrase): # 密码
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa.
Your public key has been saved in /root/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:N9Q7EJosUhKrKw+Utf5jcSgpg7fzJS5EtVfa5iinWOk root@localhost.localdomain
The key's randomart image is:
+---[RSA 2048]----+
| o.. . |
| .+ ..o o |
| oo..++ o . |
| +.o.o.o. . . |
|.+...o.+S o o |
|oo+++oo... . . |
|oo+*o++ |
| +=.E= |
| .++.. |
+----[SHA256]-----+
  1. 生成密钥对后,将公钥上传到服务器要登陆的用户 ~/.ssh/authorized_keys
1
2
[root@localhost ~]# cat ~/.ssh/id_rsa.pub 
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCyUOANXFleDWrpJop7zrM2eZxrna8+uIiSbz72IRsAO3intbNkyHJpULv9yYUmT4iPf4Vn4QYmyogFhdpTktKSADBbyH5VXIGyFjbWeO7ix1iVr7YVQQ/4P/nELVytCUiIojFdZ+DvyYSariLzLuliFpYTMJ4jpmgpL/pAUobEazpGwjlRUOWik3+8kLGpsxHYJNUmrZKSnNaOYqDJVGfO3KBfozO+I5B/wcwSW5hje7Y5xyfdDzvuVh7uVmKQbjw3WoMiy64pTcKB1S3tQtPZfXnmOd3tUZU8SXSfcvhHdgrbG6kFBrJwqjpj/sE4zL9nWbSZlbpJD7gXtlkdrAH1 root@localhost.localdomain
  1. 登录服务器 ~/.ssh/authorized_keys
1
2
[root@wpspic6 opt]# cat ~/.ssh/authorized_keys
sh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCyUOANXFleDWrpJop7zrM2eZxrna8+uIiSbz72IRsAO3intbNkyHJpULv9yYUmT4iPf4Vn4QYmyogFhdpTktKSADBbyH5VXIGyFjbWeO7ix1iVr7YVQQ/4P/nELVytCUiIojFdZ+DvyYSariLzLuliFpYTMJ4jpmgpL/pAUobEazpGwjlRUOWik3+8kLGpsxHYJNUmrZKSnNaOYqDJVGfO3KBfozO+I5B/wcwSW5hje7Y5xyfdDzvuVh7uVmKQbjw3WoMiy64pTcKB1S3tQtPZfXnmOd3tUZU8SXSfcvhHdgrbG6kFBrJwqjpj/sE4zL9nWbSZlbpJD7gXtlkdrAH1 root@localhost.localdomain
  1. 测试登录,这里我用的是root

    1
    2
    [root@localhost opt]# ssh root@10.226.50.30 
    Last login: Tue Sep 17 16:23:57 2019 from 10.226.28.68 # 登录成功

    增加-v选项,输出登录过程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    [root@localhost opt]# ssh root@10.226.50.30 -v
    OpenSSH_7.4p1, OpenSSL 1.0.2k-fips 26 Jan 2017
    debug1: Reading configuration data /etc/ssh/ssh_config
    debug1: /etc/ssh/ssh_config line 58: Applying options for *
    debug1: Connecting to 10.226.50.30 [10.226.50.30] port 22.
    debug1: Connection established.
    debug1: permanently_set_uid: 0/0
    debug1: identity file /root/.ssh/id_rsa type 1
    debug1: key_load_public: No such file or directory
    debug1: identity file /root/.ssh/id_rsa-cert type -1
    debug1: key_load_public: No such file or directory
    debug1: identity file /root/.ssh/id_dsa type -1
    debug1: key_load_public: No such file or directory
    debug1: identity file /root/.ssh/id_dsa-cert type -1
    debug1: key_load_public: No such file or directory
    debug1: identity file /root/.ssh/id_ecdsa type -1
    debug1: key_load_public: No such file or directory
    debug1: identity file /root/.ssh/id_ecdsa-cert type -1
    debug1: key_load_public: No such file or directory
    debug1: identity file /root/.ssh/id_ed25519 type -1
    debug1: key_load_public: No such file or directory
    debug1: identity file /root/.ssh/id_ed25519-cert type -1
    debug1: Enabling compatibility mode for protocol 2.0
    debug1: Local version string SSH-2.0-OpenSSH_7.4
    debug1: Remote protocol version 2.0, remote software version OpenSSH_7.4
    debug1: match: OpenSSH_7.4 pat OpenSSH* compat 0x04000000
    debug1: Authenticating to 10.226.50.30:22 as 'root'
    debug1: SSH2_MSG_KEXINIT sent
    debug1: SSH2_MSG_KEXINIT received
    debug1: kex: algorithm: curve25519-sha256
    debug1: kex: host key algorithm: rsa-sha2-512
    debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none
    debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none
    debug1: kex: curve25519-sha256 need=64 dh_need=64
    debug1: kex: curve25519-sha256 need=64 dh_need=64
    debug1: expecting SSH2_MSG_KEX_ECDH_REPLY
    debug1: Server host key: ssh-rsa SHA256:yKoRmi5QgIlXrrhYQcP5W0Mx2PhSoTsm5Z+DhdeYFpU
    debug1: Host '10.226.50.30' is known and matches the RSA host key.
    debug1: Found key in /root/.ssh/known_hosts:1
    debug1: rekey after 134217728 blocks
    debug1: SSH2_MSG_NEWKEYS sent
    debug1: expecting SSH2_MSG_NEWKEYS
    debug1: SSH2_MSG_NEWKEYS received
    debug1: rekey after 134217728 blocks
    debug1: SSH2_MSG_EXT_INFO received
    debug1: kex_input_ext_info: server-sig-algs=<rsa-sha2-256,rsa-sha2-512>
    debug1: SSH2_MSG_SERVICE_ACCEPT received
    debug1: Authentications that can continue: publickey,gssapi-keyex,gssapi-with-mic,password
    debug1: Next authentication method: gssapi-keyex
    debug1: No valid Key exchange context
    debug1: Next authentication method: gssapi-with-mic
    debug1: Unspecified GSS failure. Minor code may provide more information
    No Kerberos credentials available (default cache: KEYRING:persistent:0)

    debug1: Unspecified GSS failure. Minor code may provide more information
    No Kerberos credentials available (default cache: KEYRING:persistent:0)

    debug1: Next authentication method: publickey
    debug1: Offering RSA public key: /root/.ssh/id_rsa
    debug1: Server accepts key: pkalg rsa-sha2-512 blen 279
    debug1: Authentication succeeded (publickey).
    Authenticated to 10.226.50.30 ([10.226.50.30]:22).
    debug1: channel 0: new [client-session]
    debug1: Requesting no-more-sessions@openssh.com
    debug1: Entering interactive session.
    debug1: pledge: network
    debug1: client_input_global_request: rtype hostkeys-00@openssh.com want_reply 0
    debug1: Sending environment.
    debug1: Sending env LANG = en_US.UTF-8
    Last login: Wed Sep 18 12:00:12 2019 from 10.226.50.31
    [root@wpspic6 ~]#
  1. 登录可能失败,可以在ssh命令后增加-v选项查看登录过程;
  2. 如果私钥是从其他地方拷贝,最好将id_rsa.pub也拷贝或者原来的删除,否则会影响登录;

redis 安装

安装

  1. 通过wget直接下载redis源码安装包,目前最新半5.0的
1
wget http://download.redis.io/releases/redis-5.0.5.tar.gz
  1. 解压
1
tar -xvf redis-5.0.5.tar.gz
  1. 进入源码目录
1
cd redis-5.0.5/src
  1. 编译
1
make && make install

运行

  • 安装成功后,配置文件/etc/redis.conf

默认daemon 为no,ip:127.0.0.1直接运行

1
redis-server /etc/redis.conf

运行后出现下图,则运行成功:

redis-start

  • 访问
1
2
3
4
5
6
[root@localhost ~]# redis-cli
127.0.0.1:6379> set foo bar
OK
127.0.0.1:6379> keys *
1) "foo"
127.0.0.1:6379>
  • 修改配置文件,daemon/ip,基本修改,重新运行便可启动了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# By default Redis does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.

daemonize yes

# Examples:
#
# bind 192.168.1.100 10.0.0.1
# bind 127.0.0.1 ::1
#
# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the
# internet, binding to all the interfaces is dangerous and will expose the
# instance to everybody on the internet. So by default we uncomment the
# following bind directive, that will force Redis to listen only into
# the IPv4 loopback interface address (this means Redis will be able to
# accept connections only from clients running into the same computer it
# is running).
#
# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES
# JUST COMMENT THE FOLLOWING LINE.
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

bind 127.0.0.1

安装过程种可能出现的问题

  1. centos没有安装gcc,会出现
1
command not found: CC

安装gcc

1
yum install -y gcc
  1. make时报如下错误:
1
2
zmalloc.h:50:31: error: jemalloc/jemalloc.h: No such file or directory
zmalloc.h:55:2: error: #error "Newer version of jemalloc required"

原因是jemalloc重载了Linux下的ANSI C的malloc和free函数。解决办法:make时添加参数。

1
make MALLOC=libc
  1. make之后,会出现一句提示
1
Hint: To run 'make test' is a good idea ;)

但是不测试,通常是可以使用的。若我们运行make test ,会有如下提示

1
2
You need tcl 8.5 or newer in order to run the Redis test
make: *** [test] Error 1

解决办法是用yum安装tcl8.5(或去tcl的官方网站http://www.tcl.tk/下载8.5版本,并参考官网介绍进行安装)

1
yum install tcl